[
  {
    "path": ".checkmarx/config.yml",
    "content": "version: 1\n\n# Checkmarx configuration file\n#\n# https://checkmarx.com/resource/documents/en/34965-68549-configuring-projects-using-config-as-code-files.html\ncheckmarx:\n  scan:\n    configs:\n      sast:\n        # Exclude test directory\n        filter: \"!test\"\n      kics:\n        filter: \"!dev,!.devcontainer\"\n      sca:\n        filter: \"!dev,!.devcontainer\"\n      containers:\n        filter: \"!dev,!.devcontainer\"\n"
  },
  {
    "path": ".claude/CLAUDE.md",
    "content": "# Bitwarden Server - Claude Code Configuration\n\n## Project Context Files\n\n**Read these files before reviewing to ensure that you fully understand the project and contributing guidelines**\n\n1. @README.md\n2. @CONTRIBUTING.md\n3. @.github/PULL_REQUEST_TEMPLATE.md\n\n## Critical Rules\n\n- **NEVER** use code regions: If complexity suggests regions, refactor for better readability\n\n- **NEVER** compromise zero-knowledge principles: User vault data must remain encrypted and inaccessible to Bitwarden\n\n- **NEVER** log or expose sensitive data: No PII, passwords, keys, or vault data in logs or error messages\n\n- **ALWAYS** use secure communication channels: Enforce confidentiality, integrity, and authenticity\n\n- **ALWAYS** encrypt sensitive data: All vault data must be encrypted at rest, in transit, and in use\n\n- **ALWAYS** prioritize cryptographic integrity and data protection\n\n- **ALWAYS** add unit tests (with mocking) for any new feature development\n\n## Project Structure\n\n- **Source Code**: `/src/` - Services and core infrastructure\n- **Tests**: `/test/` - Test logic aligning with the source structure, albeit with a `.Test` suffix\n- **Utilities**: `/util/` - Migration tools, seeders, and setup scripts\n- **Dev Tools**: `/dev/` - Local development helpers\n- **Configuration**: `appsettings.{Environment}.json`, `/dev/secrets.json` for local development\n\n## Security Requirements\n\n- **Compliance**: SOC 2 Type II, SOC 3, HIPAA, ISO 27001, GDPR, CCPA\n- **Principles**: Zero-knowledge, end-to-end encryption, secure defaults\n- **Validation**: Input sanitization, parameterized queries, rate limiting\n- **Logging**: Structured logs, no PII/sensitive data in logs\n\n## Common Commands\n\n- **Build**: `dotnet build`\n- **Test**: `dotnet test`\n- **Run locally**: `dotnet run --project src/Api`\n- **Database update**: `pwsh dev/migrate.ps1`\n- **Generate OpenAPI**: `pwsh dev/generate_openapi_files.ps1`\n\n## Development Workflow\n\n- Security impact assessed\n- xUnit tests added / updated\n- Performance impact considered\n- Error handling implemented\n- Breaking changes documented\n- CI passes: build, test, lint\n- Feature flags considered for new features\n- CODEOWNERS file respected\n\n### Key Architectural Decisions\n\n- Use .NET nullable reference types (ADR 0024)\n- TryAdd dependency injection pattern (ADR 0026)\n- Authorization patterns (ADR 0022)\n- OpenTelemetry for observability (ADR 0020)\n- Log to standard output (ADR 0021)\n\n## References\n\n- [Server architecture](https://contributing.bitwarden.com/architecture/server/)\n- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/)\n- [Contributing guidelines](https://contributing.bitwarden.com/contributing/)\n- [Setup guide](https://contributing.bitwarden.com/getting-started/server/guide/)\n- [Code style](https://contributing.bitwarden.com/contributing/code-style/)\n- [Bitwarden security whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)\n- [Bitwarden security definitions](https://contributing.bitwarden.com/architecture/security/definitions)\n"
  },
  {
    "path": ".claude/commands/bump-rust-sdk.md",
    "content": "---\ndescription: Bump sdk-internal Rust crate dependencies in util/RustSdk to align with a Bitwarden clients release\nargument-hint: [clients-release-tag]\nallowed-tools: Read, Write, Edit, Glob, Grep, Bash(cargo *), Bash(dotnet *), Bash(git *), Bash(gh *)\n---\n\nBump the sdk-internal Rust crate dependencies in `util/RustSdk/rust/Cargo.toml` to align with\nthe Bitwarden clients production release specified by `$ARGUMENTS`. If no release tag is given,\ndetermine the latest `web-v*` release tag from `bitwarden/clients`.\n\nInvoke the `bump-rust-sdk` skill using the task tool for the full methodology, API surface reference, and worked examples.\n\n## Required Context\n\nBefore starting, read these files to understand the current state:\n\n- `util/RustSdk/rust/Cargo.toml` — current rev pins\n- `util/RustSdk/rust/src/*.rs` — current API usage\n- `.claude/skills/bump-rust-sdk/references/api-surface.md` — documented API surface\n- `.claude/skills/bump-rust-sdk/references/methodology.md` — detailed process\n\n## Execution\n\nFollow the skill's process in order:\n\n1. **Identify target** — Find the `@bitwarden/sdk-internal` NPM version at the release tag\n2. **Map to git SHA** — Query the GitHub Actions API for the publish workflow run number\n3. **Analyze breaking changes** — Compare old rev to new rev across the three crates, using the API surface reference\n4. **Apply changes** — Update Cargo.toml, fix compilation errors, handle deprecations\n5. **Build and verify** — `cargo build`, `cargo test`, `dotnet test test/SeederApi.IntegrationTest/`, `cargo fmt --check`\n6. **Human verification** — Present the SeederUtility and SeederApi test commands to the human. **Do NOT run these yourself.** Wait for the human to confirm.\n7. **Regenerate API surface** — Read all `.rs` files in `util/RustSdk/rust/src/` and update `.claude/skills/bump-rust-sdk/references/api-surface.md` to reflect the current imports, types, and traits. This step is mandatory — the reference must always match the actual code.\n\n## Important Rules\n\n- All three crate rev pins MUST be the same SHA\n- Do NOT make unrelated formatting or style changes to the Rust source files\n- Do NOT run SeederUtility or SeederApi yourself — the human performs all end-to-end testing\n- Do NOT skip the API surface regeneration step — a Stop hook will block if it is missed\n- `util/RustSdk/NativeMethods.g.cs` should NOT change — verify with `git diff` after build\n- `util/RustSdk/rust/Cargo.lock` will change and must be included alongside the other changes\n"
  },
  {
    "path": ".claude/hooks/README.md",
    "content": "# Claude Code Hooks\n\nAll hooks are Stop hooks — they fire when Claude finishes responding and check\nwhether documentation or references need updating based on what was changed.\n\n## Configuration\n\nRegister hooks in `.claude/settings.local.json` — not `settings.json`. Local settings are gitignored, keeping your personal hook configuration out of source control.\n\n## Requirements\n\n- `jq` must be installed (`brew install jq`)\n- Must be run from within a git repository\n\n## Testing & Debugging\n\n- **Verbose mode**: Press `Ctrl+O` in Claude Code to see hook execution details\n- **Debug mode**: Run `claude --debug` for full execution logging\n\n## Disabling\n\n- Use the `/hooks` menu in Claude Code to toggle individual hooks\n- Or set `\"disableAllHooks\": true` in `.claude/settings.local.json`\n\n---\n\n## seeder-docs-check.sh\n\n**Event:** Stop\n\n**Purpose:** Reminds developers to update Seeder documentation when code in\n`util/Seeder/`, `util/SeederApi/`, or `util/SeederUtility/` was modified but no\n`.md` files in those directories were touched.\n\n**How it works:**\n\n1. Runs `git diff` to detect all changed files\n2. If non-markdown files were changed under a Seeder project AND no `.md` files\n   in any of the three Seeder projects were modified, blocks the stop with a\n   reminder (intentionally cross-project — a doc update anywhere in the Seeder\n   subsystem satisfies the check)\n3. The reminder lists all `.md` files in the affected projects (discovered dynamically)\n4. On the next stop, `stop_hook_active` is true, so the hook allows through\n5. Result: one reminder per stop, then the developer decides\n\n---\n\n## rust-sdk-surface-check.sh\n\n**Event:** Stop\n\n**Purpose:** Ensures the RustSdk API surface reference stays current when the\nsdk-internal dependency rev is bumped. Prevents `.claude/skills/bump-rust-sdk/references/api-surface.md`\nfrom going stale.\n\n**How it works:**\n\n1. Runs `git diff` to detect all changed files\n2. If `util/RustSdk/rust/Cargo.toml` was modified BUT\n   `.claude/skills/bump-rust-sdk/references/api-surface.md` was NOT, blocks the\n   stop with a reminder to regenerate the API surface inventory\n3. On the next stop, `stop_hook_active` is true, so the hook allows through\n4. Result: one reminder per stop — Claude reads the `.rs` source files and\n   regenerates the reference\n"
  },
  {
    "path": ".claude/hooks/rust-sdk-surface-check.sh",
    "content": "#!/bin/bash\n# rust-sdk-surface-check.sh\n# Stop hook: reminds developers to update the RustSdk API surface reference\n# when Cargo.toml rev pins were modified but api-surface.md was not touched.\n#\n# Behavior: blocks Claude from stopping exactly once with a reminder.\n# On the second stop (stop_hook_active=true), allows through.\n\nset -euo pipefail\n\nINPUT=$(cat)\n\n# Guard: if a Stop hook already blocked this turn, allow through.\nSTOP_HOOK_ACTIVE=$(echo \"$INPUT\" | jq -r '.stop_hook_active // false')\nif [[ \"$STOP_HOOK_ACTIVE\" == \"true\" ]]; then\n  exit 0\nfi\n\nCWD=$(echo \"$INPUT\" | jq -r '.cwd')\n\n# Gather all changed files (staged, unstaged, and untracked) relative to repo root.\nDIFF_HEAD=$(git -C \"$CWD\" diff --name-only HEAD 2>/dev/null || true)\nUNTRACKED=$(git -C \"$CWD\" ls-files --others --exclude-standard 2>/dev/null || true)\nALL_CHANGED=$(printf \"%s\\n%s\" \"$DIFF_HEAD\" \"$UNTRACKED\" | sort -u | grep -v '^$' || true)\n\nif [[ -z \"$ALL_CHANGED\" ]]; then\n  exit 0\nfi\n\n# Check if the RustSdk Cargo.toml was modified.\nif ! echo \"$ALL_CHANGED\" | grep -q '^util/RustSdk/rust/Cargo.toml$'; then\n  exit 0\nfi\n\n# Check if the API surface reference was already updated.\nif echo \"$ALL_CHANGED\" | grep -q '^\\.claude/skills/bump-rust-sdk/references/api-surface.md$'; then\n  exit 0\nfi\n\nREASON=\"util/RustSdk/rust/Cargo.toml was modified but the API surface reference was not updated. Read all .rs files in util/RustSdk/rust/src/ and regenerate .claude/skills/bump-rust-sdk/references/api-surface.md to reflect the current imports, types, and traits used from bitwarden-core, bitwarden-crypto, and bitwarden-vault.\"\n\njq -n --arg reason \"$REASON\" '{ \"decision\": \"block\", \"reason\": $reason }'\n"
  },
  {
    "path": ".claude/hooks/seeder-docs-check.sh",
    "content": "#!/bin/bash\n# seeder-docs-check.sh\n# Stop hook: reminds developers to update Seeder documentation when\n# Seeder code was modified but no documentation files were touched.\n#\n# Behavior: blocks Claude from stopping exactly once with a reminder.\n# On the second stop (stop_hook_active=true), allows through.\n\nset -euo pipefail\n\nINPUT=$(cat)\n\n# Guard: if a Stop hook already blocked this turn, allow through.\nSTOP_HOOK_ACTIVE=$(echo \"$INPUT\" | jq -r '.stop_hook_active // false')\nif [[ \"$STOP_HOOK_ACTIVE\" == \"true\" ]]; then\n  exit 0\nfi\n\nCWD=$(echo \"$INPUT\" | jq -r '.cwd')\n\n# Gather all changed files (staged, unstaged, and untracked) relative to repo root.\nDIFF_HEAD=$(git -C \"$CWD\" diff --name-only HEAD 2>/dev/null || true)\nUNTRACKED=$(git -C \"$CWD\" ls-files --others --exclude-standard 2>/dev/null || true)\nALL_CHANGED=$(printf \"%s\\n%s\" \"$DIFF_HEAD\" \"$UNTRACKED\" | sort -u | grep -v '^$' || true)\n\nif [[ -z \"$ALL_CHANGED\" ]]; then\n  exit 0\nfi\n\n# Check which Seeder projects have non-markdown code changes.\nSEEDER_CODE_CHANGED=false\nSEEDER_PROJECTS_CHANGED=()\n\nfor project in \"util/Seeder/\" \"util/SeederApi/\" \"util/SeederUtility/\"; do\n  if echo \"$ALL_CHANGED\" | grep -q \"^${project}\" && \\\n     echo \"$ALL_CHANGED\" | grep \"^${project}\" | grep -qv '\\.md$'; then\n    SEEDER_CODE_CHANGED=true\n    SEEDER_PROJECTS_CHANGED+=(\"$project\")\n  fi\ndone\n\nif [[ \"$SEEDER_CODE_CHANGED\" == \"false\" ]]; then\n  exit 0\nfi\n\n# Check if any Seeder .md files were already modified.\nif echo \"$ALL_CHANGED\" | grep -qE '^util/(Seeder|SeederApi|SeederUtility)/.*\\.md$'; then\n  exit 0\nfi\n\n# Dynamically discover all .md files in each modified project.\nDOCS_LIST=\"\"\nfor project in \"${SEEDER_PROJECTS_CHANGED[@]}\"; do\n  while IFS= read -r md_file; do\n    DOCS_LIST=\"${DOCS_LIST}\\n  - ${md_file}\"\n  done < <(find \"$CWD/$project\" -name \"*.md\" | sed \"s|^$CWD/||\" | sort)\ndone\n\nREASON=$(printf \"Seeder code was modified but no Seeder documentation was updated. Please check whether any of these docs need updating:%b\\n\\nIf the docs are already accurate, let the user know you verified them.\" \"$DOCS_LIST\")\n\njq -n --arg reason \"$REASON\" '{ \"decision\": \"block\", \"reason\": $reason }'\n"
  },
  {
    "path": ".claude/settings.json",
    "content": "{\n  \"attribution\": {\n    \"commit\": \"\",\n    \"pr\": \"\"\n  },\n  \"extraKnownMarketplaces\": {\n    \"bitwarden-marketplace\": {\n      \"source\": {\n        \"source\": \"github\",\n        \"repo\": \"bitwarden/ai-plugins\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": ".claude/skills/bump-rust-sdk/SKILL.md",
    "content": "---\nname: bump-rust-sdk\ndescription: This skill should be used when the user asks to \"bump the Rust SDK\", \"update sdk-internal\", \"bump bitwarden-crypto\", \"update RustSdk dependencies\", \"align server SDK with clients\", or needs to update the bitwarden/sdk-internal git rev pins in util/RustSdk/rust/Cargo.toml. Provides the methodology for mapping client NPM versions to git commit SHAs, analyzing breaking changes, auditing the API surface, and verifying the bump end-to-end.\n---\n\n# Bump sdk-internal Rust Crate Dependencies\n\n## Overview\n\nThe server's `util/RustSdk/rust/Cargo.toml` pins `bitwarden-crypto` from the\n`bitwarden/sdk-internal` repository by git rev. This must be periodically bumped to stay\naligned with the Bitwarden client applications.\n\nThe RustSdk is used by the Seeder to produce cryptographically correct Protected Data for\nintegration testing. It is NOT part of the production server runtime. The Rust layer provides\ngeneric field-level encryption (`encrypt_string`, `decrypt_string`, `encrypt_fields`) and\nkey generation — the C# Seeder drives which fields to encrypt via `EncryptPropertyAttribute`.\n\n## Key Challenge: NPM-to-Git-Rev Mapping\n\nThe clients consume sdk-internal via **NPM packages** (`@bitwarden/sdk-internal`), while the\nserver consumes it via **Rust git rev pins**. The NPM version (e.g., `0.2.0-main.522`) does not\ndirectly correspond to a git tag — it encodes a GitHub Actions **workflow run number**.\n\n### Version Format\n\n```\n0.2.0-main.522\n│     │     │\n│     │     └── GitHub Actions run number for publish-wasm-internal workflow\n│     └── Branch name (/ replaced with -)\n└── Base version from sdk-internal\n```\n\n### How to Find the Git Rev\n\n1. Determine the target NPM version from the clients repo (see Step 1 below)\n2. Find the `Publish @bitwarden/sdk-internal` workflow ID in the sdk-internal repo\n3. Query the GitHub Actions API for the specific run number\n4. Extract the `head_sha` — that is the git rev to pin in Cargo.toml\n\nThe specific API queries are documented in `references/methodology.md`.\n\n## Process Overview\n\n### Step 1: Identify Target Version\n\nDetermine which sdk-internal version to target. Check the latest production release tag from\n`bitwarden/clients` (e.g., `web-v2026.2.0`):\n\n```bash\ncd /path/to/clients\ngit show web-v2026.2.0:package.json | grep sdk-internal\n```\n\nThis gives the NPM version (e.g., `0.2.0-main.522`). Extract the run number (522).\n\n### Step 2: Map NPM Version to Git SHA\n\nQuery the GitHub Actions API to find the commit that produced that NPM build. See\n`references/methodology.md` for the exact commands.\n\n### Step 3: Analyze Breaking Changes\n\nCompare the current pinned rev against the target rev, focusing on `bitwarden-crypto`:\n\n```bash\ncd /path/to/sdk-internal\ngit log --oneline <old-rev>..<new-rev> -- crates/bitwarden-crypto\n```\n\nCross-reference each commit against the API surface documented in `references/api-surface.md`.\n\n### Step 4: Apply Changes\n\n1. Update `Cargo.toml` — bump the `bitwarden-crypto` rev pin to the new SHA\n2. Fix any compilation errors from breaking changes (type renames, new parameters, etc.)\n3. Add `#[allow(deprecated)]` for any newly-deprecated APIs (with a comment explaining why)\n\n### Step 5: Build and Verify (Claude)\n\n```bash\ncd util/RustSdk/rust\ncargo build                # Must compile cleanly\ncargo test                 # All tests must pass (roundtrip test is critical)\ncargo fmt --check          # Formatting must be clean\ngit diff ../NativeMethods.g.cs  # FFI signatures should be unchanged\n```\n\nAlso run the C# integration tests:\n\n```bash\ndotnet test test/SeederApi.IntegrationTest/\n```\n\n### Step 6: Human Verification (HUMAN ONLY)\n\n**Claude does NOT perform this step.** Present these commands to the human engineer and wait\nfor confirmation before proceeding.\n\nThe human runs SeederUtility and SeederApi to verify Protected Data is correctly produced and\ndecryptable by the web client. See `references/methodology.md` for the specific test commands\nand validation criteria.\n\n## Security Notes\n\n- The RustSdk lives in `util/` (test infrastructure), not `src/` (production)\n- The server never decrypts Vault Data — zero-knowledge invariant is unaffected\n- Aligning with the production client release ensures the Seeder produces Protected Data\n  using the same cryptographic primitives as real clients\n- Review `Cargo.lock` diff for unexpected transitive crypto crate changes (rsa, aes, sha2, etc.)\n\n## Keeping References Current\n\nThe API surface reference (`references/api-surface.md`) must always reflect the actual code.\nTwo mechanisms enforce this:\n\n1. **Post-bump step** — The `/bump-rust-sdk` command includes a mandatory final step to\n   regenerate `api-surface.md` by reading the actual `*.rs` source files.\n2. **Stop hook** — `.claude/hooks/rust-sdk-surface-check.sh` blocks if `Cargo.toml` was\n   modified but `api-surface.md` was not updated in the same session.\n\nTo regenerate: read all `.rs` files in `util/RustSdk/rust/src/`, extract every `use` statement\nfrom `bitwarden_crypto`, and rewrite `references/api-surface.md` to match.\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/methodology.md`** — Detailed step-by-step commands including GitHub Actions API\n  queries, breaking change analysis checklist, human verification commands, and a worked example\n  from the Feb 2026 bump\n- **`references/api-surface.md`** — Complete inventory of types, traits, and functions the RustSdk\n  imports from `bitwarden-crypto`, used to assess breaking change impact\n\n## Files Modified in a Typical Bump\n\n| File                              | Change                                          |\n| --------------------------------- | ----------------------------------------------- |\n| `util/RustSdk/rust/Cargo.toml`    | `bitwarden-crypto` rev pin update               |\n| `util/RustSdk/rust/src/*.rs`      | Type renames, new parameters, deprecation fixes |\n| `util/RustSdk/rust/Cargo.lock`    | Auto-regenerated (commit alongside)             |\n| `util/RustSdk/NativeMethods.g.cs` | Should NOT change (verify)                      |\n"
  },
  {
    "path": ".claude/skills/bump-rust-sdk/references/api-surface.md",
    "content": "# RustSdk API Surface Inventory\n\n> **Auto-generated from actual source files.** Last updated: 2026-02-25\n> Pinned rev: `abba7fdab687753268b63248ec22639dff35d07c`\n\nThis documents every type, trait, and function the server's RustSdk imports from\n`bitwarden-crypto`. Use this to assess breaking change impact when bumping revs.\n\n**Location:** `util/RustSdk/rust/src/`\n\n## bitwarden-crypto\n\n### Types Used — lib.rs (key generation and management)\n\n| Type                      | Usage                                                                                        |\n| ------------------------- | -------------------------------------------------------------------------------------------- |\n| `BitwardenLegacyKeyBytes` | `BitwardenLegacyKeyBytes::from()` — wraps raw key bytes for `SymmetricCryptoKey::try_from()` |\n| `HashPurpose`             | `HashPurpose::ServerAuthorization` enum variant                                              |\n| `Kdf`                     | `Kdf::PBKDF2 { iterations }` enum variant with `NonZeroU32`                                  |\n| `MasterKey`               | `MasterKey::derive()`, `.derive_master_key_hash()`, `.make_user_key()`                       |\n| `PrivateKey`              | `PrivateKey::from_pem()`, `.to_public_key()`, `.to_der()`                                    |\n| `PublicKey`               | `PublicKey::from_der()`                                                                      |\n| `RsaKeyPair`              | Struct literal: `RsaKeyPair { private, public }`                                             |\n| `SpkiPublicKeyBytes`      | `SpkiPublicKeyBytes::from()` — wraps public key DER bytes                                    |\n| `SymmetricCryptoKey`      | `.make_aes256_cbc_hmac_key()`, `::try_from()`, `.to_base64()`                                |\n| `UnsignedSharedKey`       | `::encapsulate_key_unsigned()` (deprecated — wrapped with `#[allow(deprecated)]`)            |\n| `UserKey`                 | `UserKey::new()`, `.make_key_pair()`, `.0` field access                                      |\n\n### Types Used — cipher.rs (field-level encryption)\n\n| Type                      | Usage                                                                                           |\n| ------------------------- | ----------------------------------------------------------------------------------------------- |\n| `BitwardenLegacyKeyBytes` | `BitwardenLegacyKeyBytes::from()` — wraps raw key bytes for `SymmetricCryptoKey::try_from()`    |\n| `EncString`               | `enc_str.parse::<EncString>()`, `.to_string()` — parsed from and serialized to EncString format |\n| `SymmetricCryptoKey`      | `::try_from()`, `.make_aes256_cbc_hmac_key()`, `.to_base64()` — key construction and testing    |\n\n### Traits Used\n\n| Trait            | File      | Methods Called                                                       |\n| ---------------- | --------- | -------------------------------------------------------------------- |\n| `KeyEncryptable` | lib.rs    | `.encrypt_with_key(&key)` — encrypts DER bytes and strings           |\n| `KeyEncryptable` | cipher.rs | `.encrypt_with_key(&key)` — encrypts plaintext strings to EncStrings |\n| `KeyDecryptable` | cipher.rs | `.decrypt_with_key(&key)` — decrypts EncString back to plaintext     |\n\n## FFI Functions Exposed\n\nThe Rust layer exposes these functions to C# via csbindgen:\n\n| Function                         | File      | Purpose                                                   |\n| -------------------------------- | --------- | --------------------------------------------------------- |\n| `generate_user_keys`             | lib.rs    | Derive master key, user key, key pair from email/password |\n| `generate_organization_keys`     | lib.rs    | Generate org symmetric key + RSA key pair                 |\n| `generate_user_organization_key` | lib.rs    | Encapsulate org key with user's public key (unsigned)     |\n| `encrypt_string`                 | cipher.rs | Encrypt a single plaintext string with a symmetric key    |\n| `decrypt_string`                 | cipher.rs | Decrypt an EncString with a symmetric key                 |\n| `encrypt_fields`                 | cipher.rs | Encrypt specified fields in a JSON object by dot-path     |\n| `free_c_string`                  | lib.rs    | Free a C string returned by any of the above functions    |\n\n## Breaking Change Risk Matrix\n\nWhen reviewing upstream commits, prioritize checking for changes to:\n\n**Critical (compilation failure):**\n\n- Any type rename or removal listed above\n- Changes to `EncString` parsing or serialization format\n- Changes to `KeyEncryptable` or `KeyDecryptable` trait method signatures\n- Changes to `SymmetricCryptoKey::try_from()` or `BitwardenLegacyKeyBytes`\n\n**High (runtime failure):**\n\n- Changes to `Kdf::PBKDF2` enum variant\n- Changes to `HashPurpose::ServerAuthorization`\n- Changes to `MasterKey::derive()` or key derivation behavior\n- Changes to `UnsignedSharedKey::encapsulate_key_unsigned()` signature\n\n**Medium (deprecation warnings):**\n\n- Functions annotated with `#[deprecated]` — suppress with `#[allow(deprecated)]`\n  and add a comment explaining why and what the migration path is\n\n**Low (transparent):**\n\n- Internal implementation changes that don't affect the public API\n- New methods added to existing types (additive, non-breaking)\n\n## How to Check for Changes\n\n```bash\ncd /path/to/sdk-internal\n# bitwarden-crypto public API\ngit diff <old>..<new> -- crates/bitwarden-crypto/src/lib.rs crates/bitwarden-crypto/src/keys/mod.rs\n```\n"
  },
  {
    "path": ".claude/skills/bump-rust-sdk/references/methodology.md",
    "content": "# Bump sdk-internal: Detailed Methodology\n\n## Step-by-Step Process\n\n### 1. Identify the Current Server Pin\n\n```bash\ngrep 'rev = ' util/RustSdk/rust/Cargo.toml\n```\n\n### 2. Identify the Target Version from Clients\n\nDetermine the latest production release tag from the clients repo:\n\n```bash\n# Check latest web release\ngh release list --repo bitwarden/clients --limit 5 | grep web-v\n\n# Get the sdk-internal NPM version at that tag\ncd /path/to/clients\ngit show <tag>:package.json | grep sdk-internal\n```\n\nExample output: `\"@bitwarden/sdk-internal\": \"0.2.0-main.522\"`\n\nThe run number is **522** — the last segment after the branch name.\n\n### 3. Map NPM Run Number to Git SHA\n\nThe NPM version encodes a GitHub Actions workflow run number, not a git tag. Query the API:\n\n```bash\n# Find the publish workflow ID\ngh api \"repos/bitwarden/sdk-internal/actions/workflows\" \\\n  --jq '.workflows[] | \"\\(.id) \\(.name)\"' | grep -i \"Publish.*sdk-internal\"\n\n# Query for the specific run number (replace WORKFLOW_ID and RUN_NUMBER)\ngh api \"repos/bitwarden/sdk-internal/actions/workflows/WORKFLOW_ID/runs?per_page=100\" \\\n  --jq '.workflow_runs[] | select(.run_number == RUN_NUMBER) | \"\\(.run_number) \\(.head_sha) \\(.created_at) \\(.head_branch)\"'\n```\n\nThe `head_sha` in the output is the git commit to pin in Cargo.toml.\n\n**If the run is older than 100 runs ago**, paginate:\n\n```bash\ngh api \"repos/bitwarden/sdk-internal/actions/workflows/WORKFLOW_ID/runs?per_page=100&page=2\" \\\n  --jq '.workflow_runs[] | select(.run_number == RUN_NUMBER) | ...'\n```\n\n### 4. Verify the Current Pin (for context)\n\n```bash\ncd /path/to/sdk-internal\ngit log --oneline -1 <current-rev>\n```\n\nThis shows when the current pin was made and what commit it corresponds to.\n\n### 5. Analyze Breaking Changes\n\nList all commits touching `bitwarden-crypto` between the old and new revs:\n\n```bash\ncd /path/to/sdk-internal\ngit log --oneline <old-rev>..<new-rev> -- crates/bitwarden-crypto\n```\n\nFor each commit, check for:\n\n- **Type renames** (e.g., `AsymmetricCryptoKey` -> `PrivateKey`)\n- **Removed or deprecated functions** (look for `#[deprecated]` annotations)\n- **Changed function signatures** (parameter types, return types)\n- **Trait changes** (new required methods, changed generic bounds)\n\nTo check the public API diff:\n\n```bash\ngit diff <old-rev>..<new-rev> -- crates/bitwarden-crypto/src/keys/mod.rs\ngit diff <old-rev>..<new-rev> -- crates/bitwarden-crypto/src/lib.rs\n```\n\nCross-reference findings against `references/api-surface.md` to assess impact.\n\n### 6. Apply Code Changes\n\n1. **Cargo.toml** — Update the `bitwarden-crypto` `rev = \"...\"` to the new SHA\n2. **Rust source files** — Fix compilation errors from breaking changes\n3. **Deprecation warnings** — Add `#[allow(deprecated)]` with a comment explaining why\n4. Do NOT make unrelated formatting or style changes\n\n### 7. Build and Test (Claude)\n\n```bash\ncd util/RustSdk/rust\n\n# Compile\ncargo build\n\n# Review transitive dependency changes (focus on crypto crates)\ngit diff Cargo.lock | grep \"^[+-]name\\|^[+-]version\" | head -40\n\n# Verify FFI signatures unchanged\ngit diff ../NativeMethods.g.cs\n\n# Run Rust tests (roundtrip test is critical)\ncargo test\n\n# Run C# integration tests\ndotnet test test/SeederApi.IntegrationTest/\n\n# Format checks\ncargo fmt --check\n```\n\n**Key validation:** The `encrypt_string_decrypt_string_roundtrip` test proves the new SDK\nversion correctly encrypts and decrypts data. If this passes, the crypto is working.\n\n### 8. Human Verification (HUMAN ONLY — Claude does NOT run these)\n\nPresent these commands to the human engineer. Wait for confirmation before proceeding.\n\n**SeederUtility — seed an org with vault data:**\n\n```bash\ncd util/SeederUtility\ndotnet run -- organization -n SdkBumpTest -d sdk-bump-test.example -u 3 -c 10 -g 5 -o Traditional -m\n```\n\n**SeederUtility — seed a fixture preset:**\n\n```bash\ndotnet run -- seed --preset dunder-mifflin-enterprise-full --mangle\n```\n\n**SeederApi — start and seed via HTTP:**\n\nYou need to replace the empty password argument with at least an 8-character master password for the fake user account\n\n```bash\ncd util/SeederApi\ndotnet run\n# In another terminal:\ncurl -X POST http://localhost:5000/seed \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-Play-Id: sdk-bump-test\" \\\n  -d '{\"template\": \"SingleUserScene\", \"arguments\": {\"email\": \"test@example.com\", \"password\": \"\"}}'\n```\n\n**SeederApi — cleanup:**\n\n```bash\ncurl -X DELETE http://localhost:5000/seed/sdk-bump-test\n```\n\n**Validation criteria (human checks):**\n\n- Seeded users can log in to the web vault with the fake master password\n  - See `util/SeederUtility/README.md` for the default master password used by Seeder\n- Vault Data (ciphers) is visible and decryptable in the web client\n- No errors in SeederUtility or SeederApi output\n- SeederApi cleanup deletes all tracked entities\n\n---\n\n## Worked Example: February 2026 Bump\n\nThis section documents the actual bump performed in Feb 2026 as a reference.\n\n### Context\n\n- **Old rev:** `7080159154a42b59028ccb9f5af62bf087e565f9` (2025-11-20)\n- **Target:** `web-v2026.2.0` production release\n- **NPM version:** `@bitwarden/sdk-internal` `0.2.0-main.522`\n- **Workflow ID:** `126086102` (Publish @bitwarden/sdk-internal)\n- **New rev:** `abba7fdab687753268b63248ec22639dff35d07c` (2026-02-05)\n\n### Breaking Changes Found\n\n| Change                                                | Impact                           | Fix                                      |\n| ----------------------------------------------------- | -------------------------------- | ---------------------------------------- |\n| `AsymmetricCryptoKey` renamed to `PrivateKey`          | Import + usage in lib.rs         | Rename type                              |\n| `AsymmetricPublicCryptoKey` renamed to `PublicKey`     | Import + usage in lib.rs         | Rename type                              |\n| `PrivateKey::to_der()` returns `Pkcs8PrivateKeyBytes` | Low risk — auto-refs             | No code change needed                    |\n| `encapsulate_key_unsigned` deprecated                  | Deprecation warning              | `#[allow(deprecated)]` + comment         |\n\n### Cargo.lock Review\n\n- All bitwarden-\\* crates: `1.0.0` -> `2.0.0` (workspace version bump — expected)\n- `coset`: `0.3.8` -> `0.4.1` (COSE library — minor bump)\n- New transitive deps: `mockall`, `predicates`, `tracing-attributes` (test/dev deps)\n- **No changes to core crypto crates** (rsa, aes, sha2, hmac, pbkdf2)\n\n### Results\n\n- All Rust unit tests passed\n- All C# integration tests passed\n- NativeMethods.g.cs unchanged\n- Human verification: login, vault decryption, seeding all confirmed working\n"
  },
  {
    "path": ".config/dotnet-tools.json",
    "content": "{\n  \"version\": 1,\n  \"isRoot\": true,\n  \"tools\": {\n    \"swashbuckle.aspnetcore.cli\": {\n      \"version\": \"10.1.0\",\n      \"commands\": [\"swagger\"]\n    },\n    \"dotnet-ef\": {\n      \"version\": \"8.0.8\",\n      \"commands\": [\"dotnet-ef\"]\n    }\n  }\n}\n"
  },
  {
    "path": ".devcontainer/bitwarden_common/docker-compose.yml",
    "content": "services:\n  bitwarden_server:\n    image: mcr.microsoft.com/devcontainers/dotnet:8.0\n    volumes:\n      - ../../:/workspace:cached\n    env_file:\n      - path: ../../dev/.env\n        required: false\n    # Overrides default command so things don't shut down after the process ends.\n    command: sleep infinity\n\n  bitwarden_mssql:\n    image: mcr.microsoft.com/mssql/server:2022-latest\n    platform: linux/amd64\n    restart: unless-stopped\n    env_file:\n      - path: ../../dev/.env\n        required: false\n    environment:\n      ACCEPT_EULA: \"Y\"\n      MSSQL_PID: Developer\n    volumes:\n      - mssql_dev_data:/var/opt/mssql\n      - ../../util/Migrator:/mnt/migrator/\n      - ../../dev/helpers/mssql:/mnt/helpers\n      - ../../dev/.data/mssql:/mnt/data\n    network_mode: service:bitwarden_server\n\n  bitwarden_mail:\n    image: sj26/mailcatcher:latest\n    restart: unless-stopped\n    network_mode: service:bitwarden_server\n    \nvolumes:\n  mssql_dev_data:\n"
  },
  {
    "path": ".devcontainer/community_dev/devcontainer.json",
    "content": "{\n  \"name\": \"Bitwarden Community Dev\",\n  \"dockerComposeFile\": \"../../.devcontainer/bitwarden_common/docker-compose.yml\",\n  \"service\": \"bitwarden_server\",\n  \"workspaceFolder\": \"/workspace\",\n  \"initializeCommand\": \"mkdir -p dev/.data/keys dev/.data/mssql dev/.data/azurite dev/helpers/mssql\",\n  \"features\": {\n    \"ghcr.io/devcontainers/features/node:1\": {\n      \"version\": \"22\"\n    },\n    \"ghcr.io/devcontainers/features/rust:1\": {}\n  },\n  \"mounts\": [\n    {\n      \"source\": \"../../dev/.data/keys\",\n      \"target\": \"/home/vscode/.aspnet/DataProtection-Keys\",\n      \"type\": \"bind\"\n    }\n  ],\n  \"customizations\": {\n    \"vscode\": {\n      \"settings\": {},\n      \"extensions\": [\"ms-dotnettools.csdevkit\"]\n    }\n  },\n  \"postCreateCommand\": \"bash .devcontainer/community_dev/postCreateCommand.sh\",\n  \"forwardPorts\": [1080, 1433, 3306, 5432],\n  \"portsAttributes\": {\n    \"default\": {\n      \"onAutoForward\": \"ignore\"\n    },\n    \"1080\": {\n      \"label\": \"Mail Catcher\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"1433\": {\n      \"label\": \"SQL Server\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"3306\": {\n      \"label\": \"MySQL\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"5432\": {\n      \"label\": \"PostgreSQL\",\n      \"onAutoForward\": \"notify\"\n    }\n  }\n}\n"
  },
  {
    "path": ".devcontainer/community_dev/postCreateCommand.sh",
    "content": "#!/usr/bin/env bash\nexport DEV_DIR=/workspace/dev\nexport CONTAINER_CONFIG=/workspace/.devcontainer/community_dev\ngit config --global --add safe.directory /workspace\n\nif [[ -z \"${CODESPACES}\" ]]; then\n    allow_interactive=1\nelse\n    echo \"Doing non-interactive setup\"\n    allow_interactive=0\nfi\n\nget_option() {\n    # Helper function for reading the value of an environment variable\n    # primarily but then falling back to an interactive question if allowed\n    # and lastly falling back to a default value input when either other\n    # option is available.\n    name_of_var=\"$1\"\n    question_text=\"$2\"\n    default_value=\"$3\"\n    is_secret=\"$4\"\n\n    if [[ -n \"${!name_of_var}\" ]]; then\n        # If the env variable they gave us has a value, then use that value\n        echo \"${!name_of_var}\"\n    elif [[ \"$allow_interactive\" == 1 ]]; then\n        # If we can be interactive, then use the text they gave us to request input\n        if [[ \"$is_secret\" == 1 ]]; then\n            read -r -s -p \"$question_text\" response\n            echo \"$response\"\n        else\n            read -r -p \"$question_text\" response\n            echo \"$response\"\n        fi\n    else\n        # If no environment variable and not interactive, then just give back default value\n        echo \"$default_value\"\n    fi\n}\n\nget_installation_id_and_key() {\n    pushd ./dev >/dev/null || exit\n    echo \"Please enter your installation id and key from https://bitwarden.com/host:\"\n    INSTALLATION_ID=\"$(get_option \"INSTALLATION_ID\" \"Installation id: \" \"00000000-0000-0000-0000-000000000001\")\"\n    INSTALLATION_KEY=\"$(get_option \"INSTALLATION_KEY\" \"Installation key: \" \"\" 1)\"\n    jq \".globalSettings.installation.id = \\\"$INSTALLATION_ID\\\" |\n        .globalSettings.installation.key = \\\"$INSTALLATION_KEY\\\"\" \\\n        secrets.json.example >secrets.json # create/overwrite secrets.json\n    popd >/dev/null || exit\n}\n\nconfigure_other_vars() {\n    pushd ./dev >/dev/null || exit\n    cp secrets.json .secrets.json.tmp\n    # set DB_PASSWORD equal to .services.mssql.environment.MSSQL_SA_PASSWORD, accounting for quotes\n    DB_PASSWORD=\"$(grep -oP 'MSSQL_SA_PASSWORD=[\"'\"'\"']?\\K[^\"'\"'\"'\\s]+' $DEV_DIR/.env)\"\n    SQL_CONNECTION_STRING=\"Server=localhost;Database=vault_dev;User Id=SA;Password=$DB_PASSWORD;Encrypt=True;TrustServerCertificate=True\"\n    jq \\\n        \".globalSettings.sqlServer.connectionString = \\\"$SQL_CONNECTION_STRING\\\" |\n        .globalSettings.postgreSql.connectionString = \\\"Host=localhost;Username=postgres;Password=$DB_PASSWORD;Database=vault_dev;Include Error Detail=true\\\" |\n        .globalSettings.mySql.connectionString = \\\"server=localhost;uid=root;pwd=$DB_PASSWORD;database=vault_dev\\\"\" \\\n        .secrets.json.tmp >secrets.json\n    rm -f .secrets.json.tmp\n    popd >/dev/null || exit\n}\n\none_time_setup() {\n    do_secrets_json_setup=\"$(get_option \"SETUP_SECRETS_JSON\" \"Would you like to configure your secrets and certificates for the first time?\nWARNING: This will overwrite any existing secrets.json and certificate files.\nProceed? [y/N] \" \"n\")\"\n    if [[ \"$do_secrets_json_setup\" =~ ^([yY][eE][sS]|[yY])+$ ]]; then\n        echo \"Running one-time setup script...\"\n        sleep 1\n        get_installation_id_and_key\n        configure_other_vars\n        pushd ./dev >/dev/null || exit\n        pwsh ./setup_secrets.ps1 || true\n        popd >/dev/null || exit\n\n        echo \"Running migrations...\"\n        sleep 5 # wait for DB container to start\n        dotnet run --project ./util/MsSqlMigratorUtility \"$SQL_CONNECTION_STRING\"\n\n    fi\n}\n\none_time_setup\n"
  },
  {
    "path": ".devcontainer/internal_dev/devcontainer.json",
    "content": "{\n  \"name\": \"Bitwarden Dev\",\n  \"dockerComposeFile\": [\n    \"../../.devcontainer/bitwarden_common/docker-compose.yml\",\n    \"../../.devcontainer/internal_dev/docker-compose.override.yml\"\n  ],\n  \"service\": \"bitwarden_server\",\n  \"workspaceFolder\": \"/workspace\",\n  \"initializeCommand\": \"mkdir -p dev/.data/keys dev/.data/mssql dev/.data/azurite dev/helpers/mssql\",\n  \"features\": {\n    \"ghcr.io/devcontainers/features/node:1\": {\n      \"version\": \"22\"\n    },\n    \"ghcr.io/devcontainers/features/rust:1\": {}\n  },\n  \"mounts\": [\n    {\n      \"source\": \"../../dev/.data/keys\",\n      \"target\": \"/home/vscode/.aspnet/DataProtection-Keys\",\n      \"type\": \"bind\"\n    }\n  ],\n  \"customizations\": {\n    \"vscode\": {\n      \"settings\": {},\n      \"extensions\": [\"ms-dotnettools.csdevkit\"]\n    }\n  },\n  \"onCreateCommand\": \"bash .devcontainer/internal_dev/onCreateCommand.sh\",\n  \"postCreateCommand\": \"bash .devcontainer/internal_dev/postCreateCommand.sh\",\n  \"forwardPorts\": [\n    1080, 1433, 3306, 5432, 10000, 10001, 10002,\n    4000, 4001, 33656, 33657, 44519, 44559,\n    46273, 46274, 50024, 51822, 51823,\n    54103, 61840, 61841, 62911, 62912\n  ],\n  \"portsAttributes\": {\n    \"default\": {\n      \"onAutoForward\": \"ignore\"\n    },\n    \"1080\": {\n      \"label\": \"Mail Catcher\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"1433\": {\n      \"label\": \"SQL Server\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"3306\": {\n      \"label\": \"MySQL\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"5432\": {\n      \"label\": \"PostgreSQL\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"10000\": {\n      \"label\": \"Azurite Storage Blob\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"10001\": {\n      \"label\": \"Azurite Storage Queue\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"10002\": {\n      \"label\": \"Azurite Storage Table\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"4000\": {\n      \"label\": \"Api (Cloud)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"4001\": {\n      \"label\": \"Api (SelfHost)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"33656\": {\n      \"label\": \"Identity (Cloud)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"33657\": {\n      \"label\": \"Identity (SelfHost)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"44519\": {\n      \"label\": \"Billing\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"44559\": {\n      \"label\": \"Scim\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"46273\": {\n      \"label\": \"Events (Cloud)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"46274\": {\n      \"label\": \"Events (SelfHost)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"50024\": {\n      \"label\": \"Icons\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"51822\": {\n      \"label\": \"Sso (Cloud)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"51823\": {\n      \"label\": \"Sso (SelfHost)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"54103\": {\n      \"label\": \"EventsProcessor\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"61840\": {\n      \"label\": \"Notifications (Cloud)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"61841\": {\n      \"label\": \"Notifications (SelfHost)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"62911\": {\n      \"label\": \"Admin (Cloud)\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"62912\": {\n      \"label\": \"Admin (SelfHost)\",\n      \"onAutoForward\": \"notify\"\n    }\n  }\n}\n"
  },
  {
    "path": ".devcontainer/internal_dev/docker-compose.override.yml",
    "content": "services:\n  bitwarden_storage:\n    image: mcr.microsoft.com/azure-storage/azurite:latest\n    restart: unless-stopped\n    volumes:\n      - ../../dev/.data/azurite:/data\n    network_mode: service:bitwarden_server\n"
  },
  {
    "path": ".devcontainer/internal_dev/onCreateCommand.sh",
    "content": "#!/usr/bin/env bash\nexport REPO_ROOT=\"$(git rev-parse --show-toplevel)\"\n\nfile=\"$REPO_ROOT/dev/custom-root-ca.crt\"\n\nif [ -e \"$file\" ]; then\n  echo \"Adding custom root CA\"\n  sudo cp \"$file\" /usr/local/share/ca-certificates/\n  sudo update-ca-certificates\nelse\n  echo \"No custom root CA found, skipping...\"\nfi\n"
  },
  {
    "path": ".devcontainer/internal_dev/postCreateCommand.sh",
    "content": "#!/usr/bin/env bash\nexport REPO_ROOT=\"$(git rev-parse --show-toplevel)\"\nexport CONTAINER_CONFIG=/workspace/.devcontainer/internal_dev\n\ngit config --global --add safe.directory /workspace\n\nif [[ -z \"${CODESPACES}\" ]]; then\n    allow_interactive=1\nelse\n    echo \"Doing non-interactive setup\"\n    allow_interactive=0\nfi\n\nget_option() {\n    # Helper function for reading the value of an environment variable\n    # primarily but then falling back to an interactive question if allowed\n    # and lastly falling back to a default value input when either other\n    # option is available.\n    name_of_var=\"$1\"\n    question_text=\"$2\"\n    default_value=\"$3\"\n    is_secret=\"$4\"\n\n    if [[ -n \"${!name_of_var}\" ]]; then\n        # If the env variable they gave us has a value, then use that value\n        echo \"${!name_of_var}\"\n    elif [[ \"$allow_interactive\" == 1 ]]; then\n        # If we can be interactive, then use the text they gave us to request input\n        if [[ \"$is_secret\" == 1 ]]; then\n            read -r -s -p \"$question_text\" response\n            echo \"$response\"\n        else\n            read -r -p \"$question_text\" response\n            echo \"$response\"\n        fi\n    else\n        # If no environment variable and not interactive, then just give back default value\n        echo \"$default_value\"\n    fi\n}\n\nremove_comments() {\n    # jq will not parse files with comments\n    file=\"$1\"\n\n    if [[ -f \"$file\" ]]; then\n        sed -e '/^\\/\\//d' -e 's@[[:blank:]]\\{1,\\}//.*@@' \"$file\" >\"$file.tmp\"\n        mv \"$file.tmp\" \"$file\"\n    fi\n}\n\nconfigure_other_vars() {\n    pushd ./dev >/dev/null || exit\n    cp \"$REPO_ROOT/dev/secrets.json\" \"$REPO_ROOT/dev/.secrets.json.tmp\"\n    # set DB_PASSWORD equal to .services.mssql.environment.MSSQL_SA_PASSWORD, accounting for quotes\n    DB_PASSWORD=\"$(grep -oP 'MSSQL_SA_PASSWORD=[\"'\"'\"']?\\K[^\"'\"'\"'\\s]+' $REPO_ROOT/dev/.env)\"\n    SQL_CONNECTION_STRING=\"Server=localhost;Database=vault_dev;User Id=SA;Password=$DB_PASSWORD;Encrypt=True;TrustServerCertificate=True\"\n    jq \\\n        \".globalSettings.sqlServer.connectionString = \\\"$SQL_CONNECTION_STRING\\\" |\n        .globalSettings.postgreSql.connectionString = \\\"Host=localhost;Username=postgres;Password=$DB_PASSWORD;Database=vault_dev;Include Error Detail=true\\\" |\n        .globalSettings.mySql.connectionString = \\\"server=localhost;uid=root;pwd=$DB_PASSWORD;database=vault_dev\\\"\" \\\n        .secrets.json.tmp >secrets.json\n    rm \"$REPO_ROOT/dev/.secrets.json.tmp\"\n    popd >/dev/null || exit\n}\n\none_time_setup() {\n    if [[ ! -f \"$REPO_ROOT/dev/dev.pfx\" ]]; then\n        # We do not have the cert file\n        if [[ ! -z \"${DEV_CERT_CONTENTS}\" ]]; then\n            # Make file for them\n            echo \"Making $REPO_ROOT/dev/dev.pfx file for you based on DEV_CERT_CONTENTS environment variable.\"\n            # Assume content is base64 encoded\n            echo \"$DEV_CERT_CONTENTS\" | base64 -d > \"$REPO_ROOT/dev/dev.pfx\"\n        else\n            if [[ $allow_interactive -eq 1 ]]; then\n                read -r -p \\\n                    \"Place the dev.pfx files from our shared Collection in the $REPO_ROOT/dev directory.\nPress <Enter> to continue.\"\n            fi\n        fi\n    fi\n\n    if [[ -f \"$REPO_ROOT/dev/dev.pfx\" ]]; then\n        dotnet tool install dotnet-certificate-tool -g >/dev/null\n        cert_password=\"$(get_option \"DEV_CERT_PASSWORD\" \"Paste the \\\"Licensing Certificate - Dev\\\" password: \" \"\" 1)\"\n        certificate-tool add --file \"$REPO_ROOT/dev/dev.pfx\" --password \"$cert_password\"\n    else\n        echo \"You don't have a $REPO_ROOT/dev/dev.pfx file setup.\" >/dev/stderr\n    fi\n    \n    do_secrets_json_setup=\"$(get_option \"SETUP_SECRETS_JSON\" \"Would you like us to setup your secrets.json file for you? [y/N] \" \"n\")\"\n    if [[ \"$do_secrets_json_setup\" =~ ^([yY][eE][sS]|[yY])+$ ]]; then\n        remove_comments \"$REPO_ROOT/dev/secrets.json\"\n        configure_other_vars\n        # setup_secrets needs to be ran from the dev folder\n        pushd \"$REPO_ROOT/dev\" >/dev/null || exit\n        echo \"Injecting dotnet secrets...\"\n        pwsh \"$REPO_ROOT/dev/setup_secrets.ps1\" || true\n        popd >/dev/null || exit\n    fi\n\n    do_azurite_setup=\"$(get_option \"SETUP_AZURITE\" \"Would you like us to setup your azurite environment? [y/N] \" \"n\")\"\n    if [[ \"$do_azurite_setup\" =~ ^([yY][eE][sS]|[yY])+$ ]]; then\n        echo \"Installing Az module. This will take ~a minute...\"\n        pwsh -Command \"Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force\"\n        pwsh \"$REPO_ROOT/dev/setup_azurite.ps1\"\n    fi\n\n    run_mssql_migrations=\"$(get_option \"RUN_MSSQL_MIGRATIONS\" \"Would you like us to run MSSQL Migrations for you? [y/N] \" \"n\")\"\n    if [[ \"$run_mssql_migrations\" =~ ^([yY][eE][sS]|[yY])+$ ]]; then\n        echo \"Running migrations...\"\n        sleep 5 # wait for DB container to start\n        dotnet run --project \"$REPO_ROOT/util/MsSqlMigratorUtility\" \"$SQL_CONNECTION_STRING\"\n    fi\n\n    stripe_response=\"$(get_option \"INSTALL_STRIPE_CLI\" \"Would you like to install the Stripe CLI? [y/N] \" \"n\")\"\n    if [[ \"$stripe_response\" =~ ^([yY][eE][sS]|[yY])+$ ]]; then\n        install_stripe_cli\n    fi\n}\n\n# Install Stripe CLI\ninstall_stripe_cli() {\n    echo \"Installing Stripe CLI...\"\n    # Add Stripe CLI GPG key so that apt can verify the packages authenticity.\n    # If Stripe ever changes the key, we'll need to update this. Visit https://docs.stripe.com/stripe-cli?install-method=apt if so\n    curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg >/dev/null\n    # Add Stripe CLI repository to apt sources\n    echo \"deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main\" | sudo tee -a /etc/apt/sources.list.d/stripe.list >/dev/null\n    sudo apt update\n    sudo apt install -y stripe\n}\n\none_time_setup\n"
  },
  {
    "path": ".dockerignore",
    "content": "**/bin\n**/obj\n**/node_modules\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: http://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Don't use tabs for indentation.\n[*]\nindent_style = space\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nguidelines = 120\n# (Please don't specify an indent_size here; that has too many unintended consequences.)\n\n# Code files\n[*.{cs,csx,vb,vbx}]\nindent_size = 4\ncharset = utf-8-bom\n\n# Xml project files\n[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]\nindent_size = 2\n\n# Xml config files\n[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]\nindent_size = 2\n\n# JSON files\n[*.json]\nindent_size = 2\n\n# Dotnet code style settings:\n[*.{cs,vb}]\n# Sort using and Import directives with System.* appearing first\ndotnet_sort_system_directives_first = true\n# Avoid \"this.\" and \"Me.\" if not necessary\ndotnet_style_qualification_for_field = false:suggestion\ndotnet_style_qualification_for_property = false:suggestion\ndotnet_style_qualification_for_method = false:suggestion\ndotnet_style_qualification_for_event = false:suggestion\n\n# Use language keywords instead of framework type names for type references\ndotnet_style_predefined_type_for_locals_parameters_members = true:suggestion\ndotnet_style_predefined_type_for_member_access = true:suggestion\n\n# Suggest more modern language features when available\ndotnet_style_object_initializer = true:suggestion\ndotnet_style_collection_initializer = true:suggestion\ndotnet_style_coalesce_expression = true:suggestion\ndotnet_style_null_propagation = true:suggestion\ndotnet_style_explicit_tuple_names = true:suggestion\n\n# Prefix private members with underscore\ndotnet_naming_rule.private_members_with_underscore.symbols = private_fields\ndotnet_naming_rule.private_members_with_underscore.style = prefix_underscore\ndotnet_naming_rule.private_members_with_underscore.severity = suggestion\n\ndotnet_naming_symbols.private_fields.applicable_kinds = field\ndotnet_naming_symbols.private_fields.applicable_accessibilities = private\n\ndotnet_naming_style.prefix_underscore.capitalization = camel_case\ndotnet_naming_style.prefix_underscore.required_prefix = _\n\n# Async methods should have \"Async\" suffix\ndotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods\ndotnet_naming_rule.async_methods_end_in_async.style = end_in_async\ndotnet_naming_rule.async_methods_end_in_async.severity = suggestion\n\ndotnet_naming_symbols.any_async_methods.applicable_kinds = method\ndotnet_naming_symbols.any_async_methods.applicable_accessibilities = *\ndotnet_naming_symbols.any_async_methods.required_modifiers = async\n\ndotnet_naming_style.end_in_async.required_prefix =\ndotnet_naming_style.end_in_async.required_suffix = Async\ndotnet_naming_style.end_in_async.capitalization = pascal_case\ndotnet_naming_style.end_in_async.word_separator =\n\n# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.\ndotnet_diagnostic.CS0618.severity = suggestion\n\n# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.\ndotnet_diagnostic.CS0612.severity = suggestion\n\n# Remove unnecessary using directives https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005\ndotnet_diagnostic.IDE0005.severity = warning\n\n# Specify CultureInfo https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1304\ndotnet_diagnostic.CA1304.severity = warning\n\n# Specify IFormatProvider https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1305\ndotnet_diagnostic.CA1305.severity = warning\n\n# CSharp code style settings:\n[*.cs]\n# Prefer \"var\" everywhere\ncsharp_style_var_for_built_in_types = true:suggestion\ncsharp_style_var_when_type_is_apparent = true:suggestion\ncsharp_style_var_elsewhere = true:suggestion\n\n# Prefer method-like constructs to have a expression-body\ncsharp_style_expression_bodied_methods = true:none\ncsharp_style_expression_bodied_constructors = true:none\ncsharp_style_expression_bodied_operators = true:none\n\n# Prefer property-like constructs to have an expression-body\ncsharp_style_expression_bodied_properties = true:none\ncsharp_style_expression_bodied_indexers = true:none\ncsharp_style_expression_bodied_accessors = true:none\n\n# Suggest more modern language features when available\ncsharp_style_pattern_matching_over_is_with_cast_check = true:suggestion\ncsharp_style_pattern_matching_over_as_with_null_check = true:suggestion\ncsharp_style_inlined_variable_declaration = true:suggestion\ncsharp_style_throw_expression = true:suggestion\ncsharp_style_conditional_delegate_call = true:suggestion\n\n# Newline settings\ncsharp_new_line_before_open_brace = all\ncsharp_new_line_before_else = true\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_members_in_anonymous_types = true\n\n# Namespace settings\ncsharp_style_namespace_declarations = file_scoped:warning\n\n# Switch expression\ndotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value\ndotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value\n\n# CA2253: Named placeholders should nto be numeric values\ndotnet_diagnostic.CA2253.severity = suggestion\n\n# CA2254: Template should be a static expression\ndotnet_diagnostic.CA2254.severity = warning\n\n# CA1727: Use PascalCase for named placeholders\ndotnet_diagnostic.CA1727.severity = suggestion\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": "# Apply .NET format https://github.com/bitwarden/server/pull/1764\n23b0a1f9df25058ab29785ecad9a233113c10889\n\n# Turn on file scoped namespaces (gets reverted) https://github.com/bitwarden/server/pull/2225\n34fb4cca2aa78deb84d4cbc359992a7c6bba7ea5\n\n# Revert filescoped https://github.com/bitwarden/server/pull/2227\nbae03feffecbef488cb52f5f5bc133dfdbbaa316\n\n# Run formatting for file scoped namespaces https://github.com/bitwarden/server/pull/2230\n7f5f010e1eea400300c47f776604ecf46c4b4f2d\n"
  },
  {
    "path": ".git-hooks/pre-commit",
    "content": "#!/bin/bash\n\nFILES=$(git diff --cached --name-only --diff-filter=ACM \"*.cs\")\nif [ -n \"$FILES\" ]\nthen\n    dotnet format ./bitwarden-server.sln --no-restore --include $FILES\n    echo \"$FILES\" | xargs git add\nfi\n"
  },
  {
    "path": ".gitattributes",
    "content": "*.sh eol=lf\n*.cs eol=lf\n.dockerignore eol=lf\ndockerfile eol=lf"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# Please sort into logical groups with comment headers. Sort groups in order of specificity.\n# For example, default owners should always be the first group.\n# Sort lines alphabetically within these groups to avoid accidentally adding duplicates.\n#\n# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners\n\n## Docker-related files\n**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-shot\n**/*.Dockerfile @bitwarden/team-appsec @bitwarden/dept-shot\n**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-shot\n**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-shot\n**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-shot\n\n# Scanning tools\n.checkmarx/ @bitwarden/team-appsec\n\n## BRE team owns these workflows ##\n.github/workflows/publish.yml @bitwarden/dept-bre\n\n## These are shared workflows ##\n.github/workflows/_move_edd_db_scripts.yml\n.github/workflows/release.yml\n\n# Database Operations for database changes\nsrc/Sql/** @bitwarden/dept-dbops\nutil/EfShared/** @bitwarden/dept-dbops\nutil/Migrator/** @bitwarden/team-platform-dev # The Platform team owns the Migrator project code\nutil/Migrator/DbScripts/** @bitwarden/dept-dbops\nutil/Migrator/DbScripts_finalization/** @bitwarden/dept-dbops\nutil/Migrator/DbScripts_transition/** @bitwarden/dept-dbops\nutil/Migrator/MySql/** @bitwarden/dept-dbops\nutil/MySqlMigrations/** @bitwarden/dept-dbops\nutil/PostgresMigrations/** @bitwarden/dept-dbops\nutil/SqlServerEFScaffold/** @bitwarden/dept-dbops\nutil/SqliteMigrations/** @bitwarden/dept-dbops\n\n# Shared util projects\nutil/MsSqlMigratorUtility/** @bitwarden/team-platform-dev\nutil/Setup/** @bitwarden/dept-shot @bitwarden/team-platform-dev\n\n# UIF\nsrc/Core/MailTemplates/Mjml @bitwarden/team-ui-foundation # Teams are expected to own sub-directories of this project\nsrc/Core/MailTemplates/Mjml/.mjmlconfig # This change allows teams to add components within their own subdirectories without requiring a code review from UIF.\n\n# Auth team\n**/Auth @bitwarden/team-auth-dev\nbitwarden_license/src/Sso @bitwarden/team-auth-dev\nsrc/Identity @bitwarden/team-auth-dev\nsrc/Core/Identity @bitwarden/team-auth-dev\nsrc/Core/IdentityServer @bitwarden/team-auth-dev\n\n# Autofill team\nsrc/Core/Utilities/StaticStore.cs @bitwarden/team-autofill-dev\nsrc/Core/Enums/GlobalEquivalentDomainsType.cs @bitwarden/team-autofill-dev\n\n# Key Management team\n**/KeyManagement @bitwarden/team-key-management-dev\n\n# Tools team\n**/Tools @bitwarden/team-tools-dev\n\n# Dirt (Data Insights & Reporting) team\n**/Dirt @bitwarden/team-data-insights-and-reporting-dev\nsrc/Events @bitwarden/team-data-insights-and-reporting-dev\nsrc/EventsProcessor @bitwarden/team-data-insights-and-reporting-dev\ntest/Events.IntegrationTest @bitwarden/team-data-insights-and-reporting-dev\ntest/Events.Test @bitwarden/team-data-insights-and-reporting-dev\ntest/EventsProcessor.Test @bitwarden/team-data-insights-and-reporting-dev\n\n# Vault team\n**/Vault @bitwarden/team-vault-dev\n**/Vault/AuthorizationHandlers @bitwarden/team-vault-dev @bitwarden/team-admin-console-dev  # joint ownership over authorization handlers that affect organization users\n\n# Admin Console team\n**/AdminConsole @bitwarden/team-admin-console-dev\nbitwarden_license/src/Scim @bitwarden/team-admin-console-dev\nbitwarden_license/src/test/Scim.IntegrationTest @bitwarden/team-admin-console-dev\nbitwarden_license/src/test/Scim.ScimTest @bitwarden/team-admin-console-dev\n\n# Billing team\n**/*billing* @bitwarden/team-billing-dev\n**/*bitpay* @bitwarden/team-billing-dev\n**/*braintree* @bitwarden/team-billing-dev\n**/*freshdesk* @bitwarden/team-billing-dev\n**/*freshsales* @bitwarden/team-billing-dev\n**/*paypal* @bitwarden/team-billing-dev\n**/*stripe* @bitwarden/team-billing-dev\n**/*subscription* @bitwarden/team-billing-dev\n**/*payment* @bitwarden/team-billing-dev\n**/*invoice* @bitwarden/team-billing-dev\n**/*OrganizationLicense* @bitwarden/team-billing-dev\n**/Billing @bitwarden/team-billing-dev\nsrc/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev\nsrc/Admin/Views/Tools @bitwarden/team-billing-dev\n\n# Platform team\n.github/workflows/build.yml @bitwarden/team-platform-dev\n.github/workflows/build_target.yml @bitwarden/team-platform-dev\n.github/workflows/cleanup-after-pr.yml @bitwarden/team-platform-dev\n.github/workflows/cleanup-rc-branch.yml @bitwarden/team-platform-dev\n.github/workflows/repository-management.yml @bitwarden/team-platform-dev\n.github/workflows/test-database.yml @bitwarden/team-platform-dev\n.github/workflows/test.yml @bitwarden/team-platform-dev\n**/*Platform* @bitwarden/team-platform-dev\n\n# The PushType enum is expected to be editted by anyone without need for Platform review\nsrc/Core/Platform/Push/PushType.cs\n\n# SDK\nutil/RustSdk @bitwarden/team-sdk-sme\n\n# Multiple owners - DO NOT REMOVE (BRE)\n**/packages.lock.json\nDirectory.Build.props\n.devcontainer/**\ndev/docker-compose.yml\n\n# Claude related files\n.claude/ @bitwarden/team-ai-sme\n.github/workflows/respond.yml @bitwarden/team-ai-sme\n.github/workflows/review-code.yml @bitwarden/team-ai-sme\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: Server Bug Report\ndescription: File a bug report\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n\n        Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: Steps To Reproduce\n      description: How can we reproduce the behavior.\n      value: |\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n        4. Click on '...'\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected Result\n      description: A clear and concise description of what you expected to happen.\n    validations:\n      required: true\n  - type: textarea\n    id: actual\n    attributes:\n      label: Actual Result\n      description: A clear and concise description of what is happening.\n    validations:\n      required: true\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: Screenshots or Videos\n      description: If applicable, add screenshots and/or a short video to help explain your problem.\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional Context\n      description: Add any other context about the problem here.\n  - type: input\n    id: version\n    attributes:\n      label: Build Version\n      description: What version of our software are you running?\n    validations:\n      required: true\n  - type: dropdown\n    id: environment\n    attributes:\n      label: Environment\n      description: Which environment (Cloud / Self-Hosted) are you using?\n      multiple: true\n      options:\n        - Cloud (bitwarden.com)\n        - Self-Hosted\n    validations:\n      required: true\n  - type: textarea\n    id: environment-details\n    attributes:\n      label: Environment Details\n      description: If Self-Hosted please provide some additional environment details.\n      placeholder: |\n        - Operating system: [e.g. Windows 10, Mac OS Catalina]\n        - Environment: [e.g. Docker, EKS, ECS, K8S]\n        - Hardware: [e.g. Intel 6-core, 8GB RAM]\n  - type: checkboxes\n    id: issue-tracking-info\n    attributes:\n      label: Issue Tracking Info\n      description: |\n        Issue tracking information\n      options:\n        - label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like \"assigned\", \"milestone\", or \"project\" to track progress.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bw-lite.yml",
    "content": "name: Bitwarden lite Deployment Bug Report\ndescription: File a bug report\nlabels: [bug, bw-lite-deploy]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n\n        Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: Steps To Reproduce\n      description: How can we reproduce the behavior.\n      value: |\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n        4. Click on '...'\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected Result\n      description: A clear and concise description of what you expected to happen.\n    validations:\n      required: true\n  - type: textarea\n    id: actual\n    attributes:\n      label: Actual Result\n      description: A clear and concise description of what is happening.\n    validations:\n      required: true\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: Screenshots or Videos\n      description: If applicable, add screenshots and/or a short video to help explain your problem.\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional Context\n      description: Add any other context about the problem here.\n  - type: input\n    id: version\n    attributes:\n      label: Githash Version\n      description: Please go to https://{your-bitwarden-domain}/api/config and copy the gitHash version\n    validations:\n      required: true\n  - type: textarea\n    id: environment-details\n    attributes:\n      label: Environment Details\n      description: If Self-Hosted please provide some additional environment details.\n      placeholder: |\n        - Operating system: [e.g. Windows 10, Mac OS Catalina]\n        - Environment: [e.g. Docker, EKS, ECS, K8S]\n        - Hardware: [e.g. Intel 6-core, 8GB RAM]\n  - type: textarea\n    id: database-image\n    attributes:\n      label: Database Image\n      description: Please include the image and version of your database\n      placeholder: |\n        # MariaDB Example\n        mariadb:10\n        # Postgres Example\n        postgres:14\n  - type: checkboxes\n    id: issue-tracking-info\n    attributes:\n      label: Issue Tracking Info\n      description: |\n        Issue tracking information\n      options:\n        - label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like \"assigned\", \"milestone\", or \"project\" to track progress.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Feature Requests\n    url: https://community.bitwarden.com/c/feature-requests/\n    about: Request new features using the Community Forums. Please search existing feature requests before making a new one.\n  - name: Bitwarden Community Forums\n    url: https://community.bitwarden.com\n    about: Please visit the community forums for general community discussion, support and the development roadmap.\n  - name: Customer Support\n    url: https://bitwarden.com/contact/\n    about: Please contact our customer support for account issues and general customer support.\n  - name: Security Issues\n    url: https://hackerone.com/bitwarden\n    about: We use HackerOne to manage security disclosures.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## 🎟️ Tracking\n\n<!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. -->\n\n## 📔 Objective\n\n<!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. -->\n\n## 📸 Screenshots\n\n<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->\n"
  },
  {
    "path": ".github/codecov.yml",
    "content": "ignore:\n  - \"test\" # Tests\n  - \"util\" # Utils (migrators)\n"
  },
  {
    "path": ".github/renovate.json5",
    "content": "{\n  $schema: \"https://docs.renovatebot.com/renovate-schema.json\",\n  extends: [\"github>bitwarden/renovate-config\"], // Extends our default configuration for pinned dependencies\n  enabledManagers: [\n    \"cargo\",\n    \"dockerfile\",\n    \"docker-compose\",\n    \"github-actions\",\n    \"npm\",\n    \"nuget\",\n  ],\n  packageRules: [\n    // ==================== Team Ownership Rules ====================\n    {\n      matchManagers: [\"dockerfile\", \"docker-compose\"],\n      commitMessagePrefix: \"[deps] BRE:\",\n    },\n    {\n      matchPackageNames: [\"DnsClient\"],\n      description: \"Admin Console owned dependencies\",\n      commitMessagePrefix: \"[deps] AC:\",\n      reviewers: [\"team:team-admin-console-dev\"],\n    },\n    {\n      matchPackageNames: [\n        \"DuoUniversal\",\n        \"Fido2.AspNet\",\n        \"Duende.IdentityServer\",\n        \"Microsoft.AspNetCore.Authentication.JwtBearer\",\n        \"Microsoft.Extensions.Caching.Cosmos\",\n        \"Microsoft.Extensions.Identity.Stores\",\n        \"Otp.NET\",\n        \"Sustainsys.Saml2.AspNetCore2\",\n        \"YubicoDotNetClient\",\n      ],\n      description: \"Auth owned dependencies\",\n      commitMessagePrefix: \"[deps] Auth:\",\n      reviewers: [\"team:team-auth-dev\"],\n    },\n    {\n      matchPackageNames: [\n        \"AutoFixture.AutoNSubstitute\",\n        \"AutoFixture.Xunit2\",\n        \"BenchmarkDotNet\",\n        \"BitPay.Light\",\n        \"Braintree\",\n        \"coverlet.collector\",\n        \"CsvHelper\",\n        \"Kralizek.AutoFixture.Extensions.MockHttp\",\n        \"Microsoft.AspNetCore.Mvc.Testing\",\n        \"Newtonsoft.Json\",\n        \"NSubstitute\",\n        \"Serilog.Extensions.Logging.File\",\n        \"Stripe.net\",\n        \"Swashbuckle.AspNetCore\",\n        \"Swashbuckle.AspNetCore.SwaggerGen\",\n        \"xunit\",\n        \"xunit.runner.visualstudio\",\n      ],\n      description: \"Billing owned dependencies\",\n      commitMessagePrefix: \"[deps] Billing:\",\n      reviewers: [\"team:team-billing-dev\"],\n    },\n    {\n      matchPackageNames: [\n        \"Dapper\",\n        \"dbup-sqlserver\",\n        \"dotnet-ef\",\n        \"linq2db.EntityFrameworkCore\",\n        \"Microsoft.Azure.Cosmos\",\n        \"Microsoft.Data.SqlClient\",\n        \"Microsoft.EntityFrameworkCore.Design\",\n        \"Microsoft.EntityFrameworkCore.InMemory\",\n        \"Microsoft.EntityFrameworkCore.Relational\",\n        \"Microsoft.EntityFrameworkCore.Sqlite\",\n        \"Microsoft.EntityFrameworkCore.SqlServer\",\n        \"Npgsql.EntityFrameworkCore.PostgreSQL\",\n        \"Pomelo.EntityFrameworkCore.MySql\",\n      ],\n      description: \"DbOps owned dependencies\",\n      commitMessagePrefix: \"[deps] DbOps:\",\n      reviewers: [\"team:dept-dbops\"],\n    },\n    {\n      matchPackageNames: [\"YamlDotNet\"],\n      description: \"BRE owned dependencies\",\n      commitMessagePrefix: \"[deps] BRE:\",\n      reviewers: [\"team:dept-bre\"],\n    },\n    {\n      matchPackageNames: [\n        \"AspNetCoreRateLimit\",\n        \"AspNetCoreRateLimit.Redis\",\n        \"Azure.Data.Tables\",\n        \"Azure.Extensions.AspNetCore.DataProtection.Blobs\",\n        \"Azure.Messaging.EventGrid\",\n        \"Azure.Messaging.ServiceBus\",\n        \"Azure.Storage.Blobs\",\n        \"Azure.Storage.Queues\",\n        \"LaunchDarkly.ServerSdk\",\n        \"MessagePack\",\n        \"Microsoft.AspNetCore.Http\",\n        \"Microsoft.AspNetCore.SignalR.Protocols.MessagePack\",\n        \"Microsoft.AspNetCore.SignalR.StackExchangeRedis\",\n        \"Microsoft.Extensions.Configuration.EnvironmentVariables\",\n        \"Microsoft.Extensions.Configuration.UserSecrets\",\n        \"Microsoft.Extensions.Configuration\",\n        \"Microsoft.Extensions.DependencyInjection.Abstractions\",\n        \"Microsoft.Extensions.DependencyInjection\",\n        \"Microsoft.Extensions.Logging\",\n        \"Microsoft.Extensions.Logging.Console\",\n        \"Microsoft.Extensions.Caching.SqlServer\",\n        \"Microsoft.Extensions.Caching.StackExchangeRedis\",\n        \"Quartz\",\n      ],\n      description: \"Platform owned dependencies\",\n      commitMessagePrefix: \"[deps] Platform:\",\n      reviewers: [\"team:team-platform-dev\"],\n    },\n    {\n      matchUpdateTypes: [\"lockFileMaintenance\"],\n      description: \"Platform owns lock file maintenance\",\n      commitMessagePrefix: \"[deps] Platform:\",\n      reviewers: [\"team:team-platform-dev\"],\n    },\n    {\n      matchPackageNames: [\n        \"AutoMapper.Extensions.Microsoft.DependencyInjection\",\n        \"AWSSDK.SimpleEmail\",\n        \"AWSSDK.SQS\",\n        \"Handlebars.Net\",\n        \"MailKit\",\n        \"Microsoft.Azure.NotificationHubs\",\n        \"SendGrid\",\n      ],\n      description: \"Tools owned dependencies\",\n      commitMessagePrefix: \"[deps] Tools:\",\n      reviewers: [\"team:team-tools-dev\"],\n    },\n    {\n      matchPackageNames: [\n        \"AngleSharp\",\n        \"AspNetCore.HealthChecks.AzureServiceBus\",\n        \"AspNetCore.HealthChecks.AzureStorage\",\n        \"AspNetCore.HealthChecks.Network\",\n        \"AspNetCore.HealthChecks.Redis\",\n        \"AspNetCore.HealthChecks.SendGrid\",\n        \"AspNetCore.HealthChecks.SqlServer\",\n        \"AspNetCore.HealthChecks.Uris\",\n      ],\n      description: \"Vault owned dependencies\",\n      commitMessagePrefix: \"[deps] Vault:\",\n      reviewers: [\"team:team-vault-dev\"],\n    },\n\n    // ==================== Grouping Rules ====================\n    // These come after any specific team assignment rules to ensure\n    // that grouping is not overridden by subsequent rule definitions.\n    {\n      groupName: \"cargo minor\",\n      matchManagers: [\"cargo\"],\n      matchUpdateTypes: [\"minor\"],\n    },\n    {\n      groupName: \"dockerfile minor\",\n      matchManagers: [\"dockerfile\"],\n      matchUpdateTypes: [\"minor\"],\n    },\n    {\n      groupName: \"docker-compose minor\",\n      matchManagers: [\"docker-compose\"],\n      matchUpdateTypes: [\"minor\"],\n    },\n    {\n      groupName: \"github-action minor\",\n      matchManagers: [\"github-actions\"],\n      matchUpdateTypes: [\"minor\"],\n      addLabels: [\"hold\"],\n    },\n    {\n      groupName: \"Admin and SSO npm dependencies\",\n      matchFileNames: [\"src/Admin/package.json\", \"src/Sso/package.json\"],\n      matchUpdateTypes: [\"minor\", \"patch\"],\n      description: \"Admin & SSO npm packages\",\n      commitMessagePrefix: \"[deps] Auth:\",\n      reviewers: [\"team:team-auth-dev\"],\n    },\n    {\n      matchPackageNames: [\"/^Microsoft\\\\.EntityFrameworkCore\\\\./\", \"/^dotnet-ef/\"],\n      groupName: \"EntityFrameworkCore\",\n      description: \"Group EntityFrameworkCore to exclude them from the dotnet monorepo preset\",\n    },\n    {\n      matchPackageNames: [\"https://github.com/bitwarden/sdk-internal.git\"],\n      groupName: \"sdk-internal\",\n      dependencyDashboardApproval: true\n    },\n\n    // ==================== Dashboard Rules ====================\n    {\n      // For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates.\n      // This overrides the default that ignores patch updates for nuget dependencies.\n      matchPackageNames: [\n          \"/^Microsoft\\\\.Extensions\\\\./\",\n          \"/^Microsoft\\\\.AspNetCore\\\\./\",\n      ],\n      matchUpdateTypes: [\"patch\"],\n      dependencyDashboardApproval: false,\n    },\n    {\n      // For the Platform-owned dependencies below, we have decided we will only be creating PRs\n      // for major updates, and sending minor (as well as patch, inherited from base config) to the dashboard.\n      // This rule comes AFTER grouping rules so that groups are respected while still\n      // sending minor/patch updates to the dependency dashboard for approval.\n      matchPackageNames: [\n        \"AspNetCoreRateLimit\",\n        \"AspNetCoreRateLimit.Redis\",\n        \"Azure.Data.Tables\",\n        \"Azure.Extensions.AspNetCore.DataProtection.Blobs\",\n        \"Azure.Messaging.EventGrid\",\n        \"Azure.Messaging.ServiceBus\",\n        \"Azure.Storage.Blobs\",\n        \"Azure.Storage.Queues\",\n        \"LaunchDarkly.ServerSdk\",\n        \"Quartz\",\n      ],\n      matchUpdateTypes: [\"minor\"],\n      dependencyDashboardApproval: true,\n    },\n  ],\n  ignoreDeps: [\"dotnet-sdk\"],\n}\n"
  },
  {
    "path": ".github/workflows/_move_edd_db_scripts.yml",
    "content": "name: _move_edd_db_scripts\nrun-name: Move EDD database scripts\n\non:\n  workflow_call:\n\npermissions:\n  pull-requests: write\n  contents: write\n\njobs:\n  setup:\n    name: Setup\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      id-token: write\n    outputs:\n      migration_filename_prefix: ${{ steps.prefix.outputs.prefix }}\n      copy_edd_scripts: ${{ steps.check-script-existence.outputs.copy_edd_scripts }}\n\n    steps:\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Retrieve secrets\n        id: retrieve-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        with:\n          keyvault: \"bitwarden-ci\"\n          secrets: \"github-pat-bitwarden-devops-bot-repo-scope\"\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Check out branch\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}\n          persist-credentials: false\n\n      - name: Get script prefix\n        id: prefix\n        run: echo \"prefix=$(date +'%Y-%m-%d')\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Check if any files in DB transition or finalization directories\n        id: check-script-existence\n        run: |\n          if [ -f util/Migrator/DbScripts_transition/* -o -f util/Migrator/DbScripts_finalization/* ]; then\n            echo \"copy_edd_scripts=true\" >> \"$GITHUB_OUTPUT\"\n          else\n            echo \"copy_edd_scripts=false\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n  move-scripts:\n    name: Move scripts\n    runs-on: ubuntu-22.04\n    needs: setup\n    permissions:\n      contents: write\n      pull-requests: write\n      id-token: write\n      actions: read\n    if: ${{ needs.setup.outputs.copy_edd_scripts == 'true' }}\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n          persist-credentials: true\n\n      - name: Generate branch name\n        id: branch_name\n        env:\n          PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}\n        run: echo \"branch_name=move_edd_db_scripts_$PREFIX\" >> \"$GITHUB_OUTPUT\"\n\n      - name: \"Create branch\"\n        env:\n          BRANCH: ${{ steps.branch_name.outputs.branch_name }}\n        run: git switch -c \"$BRANCH\"\n\n      - name: Move scripts and finalization database schema\n        id: move-files\n        env:\n          PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }}\n        run: |\n          # scripts\n          moved_files=\"Migration scripts moved:\\n\\n\"\n\n          src_dirs=\"util/Migrator/DbScripts_transition,util/Migrator/DbScripts_finalization\"\n          dest_dir=\"util/Migrator/DbScripts\"\n          i=0\n\n          for src_dir in ${src_dirs//,/ }; do\n            for file in \"$src_dir\"/*; do\n              filenumber=$(printf \"%02d\" $i)\n\n              filename=$(basename \"$file\")\n              new_filename=\"${PREFIX}_${filenumber}_${filename}\"\n              dest_file=\"$dest_dir/$new_filename\"\n\n              # Replace any finalization references due to the move\n              sed -i -e 's/dbo_finalization/dbo/g' \"$file\"\n\n              mv \"$file\" \"$dest_file\"\n              moved_files=\"$moved_files \\n $filename -> $new_filename\"\n\n              i=$((i+1))\n            done\n          done\n\n          # schema\n          moved_files=\"$moved_files\\n\\nFinalization scripts moved:\\n\\n\"\n\n          src_dir=\"src/Sql/dbo_finalization\"\n          dest_dir=\"src/Sql/dbo\"\n\n          # sync finalization schema back to dbo, maintaining structure\n          rsync -r \"$src_dir/\" \"$dest_dir/\"\n          rm -rf \"${src_dir}\"/*\n\n          # Replace any finalization references due to the move\n          find ./src/Sql/dbo -name \"*.sql\" -type f -exec sed -i \\\n            -e 's/\\[dbo_finalization\\]/[dbo]/g' \\\n            -e 's/dbo_finalization\\./dbo./g' {} +\n\n          for file in \"$src_dir\"/**/*; do\n            moved_files=\"$moved_files \\n $file\"\n          done\n\n          echo \"moved_files=$moved_files\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Retrieve secrets\n        id: retrieve-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        with:\n          keyvault: \"bitwarden-ci\"\n          secrets: \"github-gpg-private-key,\n            github-gpg-private-key-passphrase,\n            devops-alerts-slack-webhook-url\"\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Import GPG keys\n        uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0\n        with:\n          gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}\n          passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}\n          git_user_signingkey: true\n          git_commit_gpgsign: true\n\n      - name: Commit and push changes\n        id: commit\n        env:\n          BRANCH_NAME: ${{ steps.branch_name.outputs.branch_name }}\n        run: |\n          git config --local user.email \"106330231+bitwarden-devops-bot@users.noreply.github.com\"\n          git config --local user.name \"bitwarden-devops-bot\"\n          if [ -n \"$(git status --porcelain)\" ]; then\n            git add .\n            git commit -m \"Move EDD database scripts\" -a\n            git push -u origin \"${BRANCH_NAME}\"\n            echo \"pr_needed=true\" >> \"$GITHUB_OUTPUT\"\n          else\n            echo \"No changes to commit!\";\n            echo \"pr_needed=false\" >> \"$GITHUB_OUTPUT\"\n            echo \"### :mega: No changes to commit! PR was ommited.\" >> \"$GITHUB_STEP_SUMMARY\"\n          fi\n\n      - name: Create PR for ${{ steps.branch_name.outputs.branch_name }}\n        if: ${{ steps.commit.outputs.pr_needed == 'true' }}\n        id: create-pr\n        env:\n          BRANCH: ${{ steps.branch_name.outputs.branch_name }}\n          GH_TOKEN: ${{ github.token }}\n          MOVED_FILES: ${{ steps.move-files.outputs.moved_files }}\n          TITLE: \"Move EDD database scripts\"\n        run: |\n          PR_URL=$(gh pr create --title \"$TITLE\" \\\n            --base \"main\" \\\n            --head \"$BRANCH\" \\\n            --label \"automated pr\" \\\n            --body \"\n              Automated movement of EDD database scripts.\n\n              Files moved:\n              $(echo -e \"$MOVED_FILES\")\n              \")\n          echo \"pr_url=${PR_URL}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Notify Slack about creation of PR\n        if: ${{ steps.commit.outputs.pr_needed == 'true' }}\n        uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0\n        env:\n          SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}\n        with:\n          message: \"Created PR for moving EDD database scripts: ${{ steps.create-pr.outputs.pr_url }}\"\n          status: ${{ job.status }}\n"
  },
  {
    "path": ".github/workflows/automatic-issue-responses.yml",
    "content": "name: Automatic responses\non:\n  issues:\n    types:\n      - labeled\njobs:\n  close-issue:\n    name: Close issue with automatic response\n    runs-on: ubuntu-22.04\n    permissions:\n      issues: write\n    steps:\n      # Feature request\n      - if: github.event.label.name == 'feature-request'\n        name: Feature request\n        uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1\n        with:\n          comment: |\n            We use GitHub issues as a place to track bugs and other development related issues. The [Bitwarden Community Forums](https://community.bitwarden.com/) has a [Feature Requests](https://community.bitwarden.com/c/feature-requests) section for submitting, voting for, and discussing requests like this one.\n\n            Please [sign up on our forums](https://community.bitwarden.com/signup) and search to see if this request already exists. If so, you can vote for it and contribute to any discussions about it. If not, you can re-create the request there so that it can be properly tracked.\n\n            This issue will now be closed. Thanks!\n      # Intended behavior\n      - if: github.event.label.name == 'intended-behavior'\n        name: Intended behavior\n        uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1\n        with:\n          comment: |\n            Your issue appears to be describing the intended behavior of the software. If you want this to be changed, it would be a feature request.\n\n            We use GitHub issues as a place to track bugs and other development related issues. The [Bitwarden Community Forums](https://community.bitwarden.com/) has a [Feature Requests](https://community.bitwarden.com/c/feature-requests) section for submitting, voting for, and discussing requests like this one.\n\n            Please [sign up on our forums](https://community.bitwarden.com/signup) and search to see if this request already exists. If so, you can vote for it and contribute to any discussions about it. If not, you can re-create the request there so that it can be properly tracked.\n\n            This issue will now be closed. Thanks!\n      # Customer support request\n      - if: github.event.label.name == 'customer-support'\n        name: Customer Support request\n        uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1\n        with:\n          comment: |\n            We use GitHub issues as a place to track bugs and other development related issues. Your issue appears to be a support request, or would otherwise be better handled by our dedicated Customer Success team.\n\n            Please contact us using our [Contact page](https://bitwarden.com/contact). You can include a link to this issue in the message content.\n\n            Alternatively, you can also search for an answer in our [help documentation](https://bitwarden.com/help/) or get help from other Bitwarden users on our [community forums](https://community.bitwarden.com/c/support/). The issue here will be closed.\n      # Resolved\n      - if: github.event.label.name == 'resolved'\n        name: Resolved\n        uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1\n        with:\n          comment: |\n            We’ve closed this issue, as it appears the original problem has been resolved. If this happens again or continues to be an problem, please respond to this issue with any additional detail to assist with reproduction and root cause analysis.\n      # Stale\n      - if: github.event.label.name == 'stale'\n        name: Stale\n        uses: peter-evans/close-issue@276d7966e389d888f011539a86c8920025ea0626 # v3.0.1\n        with:\n          comment: |\n            As we haven’t heard from you about this problem in some time, this issue will now be closed.\n\n            If this happens again or continues to be an problem, please respond to this issue with any additional detail to assist with reproduction and root cause analysis.\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - \"main\"\n      - \"rc\"\n      - \"hotfix-rc\"\n  pull_request:\n    types: [opened, synchronize]\n  workflow_call:\n    inputs: {}\n\npermissions:\n  contents: read\n\nenv:\n  _AZ_REGISTRY: \"bitwardenprod.azurecr.io\"\n  _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n          persist-credentials: false\n\n      - name: Set up .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      - name: Verify format\n        run: dotnet format --verify-no-changes\n\n  build-artifacts:\n    name: Build Docker images\n    runs-on: ubuntu-22.04\n    needs: lint\n    outputs:\n      has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}\n    permissions:\n      security-events: write\n      id-token: write\n    timeout-minutes: 45\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - project_name: Admin\n            base_path: ./src\n            dotnet: true\n            node: true\n          - project_name: Api\n            base_path: ./src\n            dotnet: true\n          - project_name: Attachments\n            base_path: ./util\n          - project_name: Billing\n            base_path: ./src\n            dotnet: true\n          - project_name: Events\n            base_path: ./src\n            dotnet: true\n          - project_name: EventsProcessor\n            base_path: ./src\n            dotnet: true\n          - project_name: Icons\n            base_path: ./src\n            dotnet: true\n          - project_name: Identity\n            base_path: ./src\n            dotnet: true\n          - project_name: MsSql\n            base_path: ./util\n          - project_name: MsSqlMigratorUtility\n            base_path: ./util\n            dotnet: true\n          - project_name: Nginx\n            base_path: ./util\n          - project_name: Notifications\n            base_path: ./src\n            dotnet: true\n          - project_name: Scim\n            base_path: ./bitwarden_license/src\n            dotnet: true\n          - project_name: SeederApi\n            base_path: ./util\n            platforms: linux/amd64,linux/arm64\n            dotnet: true\n          - project_name: Setup\n            base_path: ./util\n            dotnet: true\n          - project_name: Sso\n            base_path: ./bitwarden_license/src\n            dotnet: true\n    steps:\n      - name: Check secrets\n        id: check-secrets\n        run: |\n          has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }}\n          echo \"has_secrets=$has_secrets\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n          persist-credentials: false\n\n      - name: Check branch to publish\n        env:\n          PUBLISH_BRANCHES: \"main,rc,hotfix-rc\"\n        id: publish-branch-check\n        run: |\n          IFS=\",\" read -a publish_branches <<< \"$PUBLISH_BRANCHES\"\n          if [[ \" ${publish_branches[*]} \" =~ \" ${GITHUB_REF:11} \" ]]; then\n            echo \"is_publish_branch=true\" >> \"$GITHUB_ENV\"\n          else\n            echo \"is_publish_branch=false\" >> \"$GITHUB_ENV\"\n          fi\n\n      - name: Set up .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      - name: Set up Node\n        uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0\n        with:\n          cache: \"npm\"\n          cache-dependency-path: \"**/package-lock.json\"\n          node-version: \"24\"\n\n      - name: Print environment\n        run: |\n          whoami\n          dotnet --info\n          node --version\n          npm --version\n          echo \"GitHub ref: $GITHUB_REF\"\n          echo \"GitHub event: $GITHUB_EVENT\"\n\n      - name: Build node\n        if: ${{ matrix.node }}\n        working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}\n        run: |\n          npm ci\n          npm run build\n\n      - name: Publish project\n        working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}\n        if: ${{ matrix.dotnet }}\n        run: |\n          echo \"Publish\"\n          dotnet publish -c \"Release\" -o obj/build-output/publish\n\n          cd obj/build-output/publish\n          zip -r ${{ matrix.project_name }}.zip .\n          mv ${{ matrix.project_name }}.zip ../../../\n\n          pwd\n          ls -atlh ../../../\n\n      - name: Upload project artifact\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        if: ${{ matrix.dotnet }}\n        with:\n          name: ${{ matrix.project_name }}.zip\n          path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip\n          if-no-files-found: error\n\n      ########## Set up Docker ##########\n      - name: Set up QEMU emulators\n        uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0\n\n      ########## ACRs ##########\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Log in to ACR - production subscription\n        run: az acr login -n bitwardenprod\n\n      ########## Generate image tag and build Docker image ##########\n      - name: Generate Docker image tag\n        id: tag\n        run: |\n          if [[ \"${GITHUB_EVENT_NAME}\" == \"pull_request\" || \"${GITHUB_EVENT_NAME}\" == \"pull_request_target\" ]]; then\n            IMAGE_TAG=$(echo \"${GITHUB_HEAD_REF}\" | sed \"s/[^a-zA-Z0-9]/-/g\") # Sanitize branch name to alphanumeric only\n          else\n            IMAGE_TAG=$(echo \"${GITHUB_REF:11}\" | sed \"s#/#-#g\")\n          fi\n\n          if [[ \"${{ github.event.pull_request.head.repo.fork }}\" == \"true\" ]]; then\n            SANITIZED_REPO_NAME=$(echo \"$_GITHUB_PR_REPO_NAME\" | sed \"s/[^a-zA-Z0-9]/-/g\") # Sanitize repo name to alphanumeric only\n            IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag\n            IMAGE_TAG=${IMAGE_TAG:0:128}  # Limit to 128 characters, as that's the max length for Docker image tags\n          fi\n\n          if [[ \"$IMAGE_TAG\" == \"main\" ]]; then\n            IMAGE_TAG=dev\n          fi\n\n          echo \"image_tag=$IMAGE_TAG\" >> \"$GITHUB_OUTPUT\"\n          echo \"### :mega: Docker Image Tag: $IMAGE_TAG\" >> \"$GITHUB_STEP_SUMMARY\"\n\n      - name: Set up project name\n        id: setup\n        run: |\n          PROJECT_NAME=$(echo \"${{ matrix.project_name }}\" | awk '{print tolower($0)}')\n          echo \"Matrix name: ${{ matrix.project_name }}\"\n          echo \"PROJECT_NAME: $PROJECT_NAME\"\n          echo \"project_name=$PROJECT_NAME\" >> \"$GITHUB_OUTPUT\"\n          echo \"platforms: ${{ matrix.platforms }}\" >> \"$GITHUB_STEP_SUMMARY\"\n\n      - name: Generate image tags(s)\n        id: image-tags\n        env:\n          IMAGE_TAG: ${{ steps.tag.outputs.image_tag }}\n          PROJECT_NAME: ${{ steps.setup.outputs.project_name }}\n          SHA: ${{ github.sha }}\n        run: |\n          TAGS=\"${_AZ_REGISTRY}/${PROJECT_NAME}:${IMAGE_TAG}\"\n          echo \"primary_tag=$TAGS\" >> \"$GITHUB_OUTPUT\"\n          if [[ \"${IMAGE_TAG}\" == \"dev\" ]]; then\n            SHORT_SHA=$(git rev-parse --short \"${SHA}\")\n            TAGS=$TAGS\",${_AZ_REGISTRY}/${PROJECT_NAME}:dev-${SHORT_SHA}\"\n          fi\n          echo \"tags=$TAGS\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Set platforms\n        id: platforms\n        run: |\n          PLATFORMS=\"${{ matrix.platforms }}\"\n          if [ -z \"$PLATFORMS\" ]; then\n            PLATFORMS=\"linux/amd64,linux/arm/v7,linux/arm64\"\n          fi\n          echo \"platforms=$PLATFORMS\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Build Docker image\n        id: build-artifacts\n        uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0\n        with:\n          context: .\n          file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile\n          platforms: ${{ steps.platforms.outputs.platforms }}\n          push: true\n          tags: ${{ steps.image-tags.outputs.tags }}\n\n      - name: Install Cosign\n        if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'\n        uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0\n\n      - name: Sign image with Cosign\n        if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'\n        env:\n          DIGEST: ${{ steps.build-artifacts.outputs.digest }}\n          TAGS: ${{ steps.image-tags.outputs.tags }}\n        run: |\n          IFS=',' read -r -a tags_array <<< \"${TAGS}\"\n          images=()\n          for tag in \"${tags_array[@]}\"; do\n            images+=(\"${tag}@${DIGEST}\")\n          done\n          cosign sign --yes ${images[@]}\n          echo \"images=${images[*]}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Scan Docker image\n        id: container-scan\n        uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2\n        with:\n          image: ${{ steps.image-tags.outputs.primary_tag }}\n          fail-build: false\n          output-format: sarif\n\n      - name: Upload Grype results to GitHub\n        uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10\n        with:\n          sarif_file: ${{ steps.container-scan.outputs.sarif }}\n          sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}\n          ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n  upload:\n    name: Upload\n    runs-on: ubuntu-22.04\n    needs: build-artifacts\n    permissions:\n      id-token: write\n      actions: read\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n          persist-credentials: false\n\n      - name: Set up .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Log in to ACR - production subscription\n        run: az acr login -n \"$_AZ_REGISTRY\" --only-show-errors\n\n      - name: Make Docker stubs\n        if: |\n          github.event_name != 'pull_request'\n          && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')\n        run: |\n          # Set proper setup image based on branch\n          case \"$GITHUB_REF\" in\n            \"refs/heads/main\")\n                SETUP_IMAGE=\"$_AZ_REGISTRY/setup:dev\"\n                ;;\n            \"refs/heads/rc\")\n                SETUP_IMAGE=\"$_AZ_REGISTRY/setup:rc\"\n                ;;\n            \"refs/heads/hotfix-rc\")\n                SETUP_IMAGE=\"$_AZ_REGISTRY/setup:hotfix-rc\"\n                ;;\n          esac\n\n          STUB_OUTPUT=$(pwd)/docker-stub\n\n          # Run setup\n          docker run -i --rm --name setup -v \"$STUB_OUTPUT/US:/bitwarden\" \"$SETUP_IMAGE\" \\\n            /app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region US\n          docker run -i --rm --name setup -v \"$STUB_OUTPUT/EU:/bitwarden\" \"$SETUP_IMAGE\" \\\n            /app/Setup -stub 1 -install 1 -domain bitwarden.example.com -os lin -cloud-region EU\n\n          sudo chown -R \"$(whoami):$(whoami)\" \"$STUB_OUTPUT\"\n\n          # Remove extra directories and files\n          rm -rf \"$STUB_OUTPUT/US/letsencrypt\"\n          rm -rf \"$STUB_OUTPUT/EU/letsencrypt\"\n          rm \"$STUB_OUTPUT/US/env/uid.env\" \"$STUB_OUTPUT/US/config.yml\"\n          rm \"$STUB_OUTPUT/EU/env/uid.env\" \"$STUB_OUTPUT/EU/config.yml\"\n\n          # Create uid environment files\n          touch \"$STUB_OUTPUT/US/env/uid.env\"\n          touch \"$STUB_OUTPUT/EU/env/uid.env\"\n\n          # Zip up the Docker stub files\n          cd docker-stub/US; zip -r ../../docker-stub-US.zip ./*; cd ../..\n          cd docker-stub/EU; zip -r ../../docker-stub-EU.zip ./*; cd ../..\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Upload Docker stub US artifact\n        if: |\n          github.event_name != 'pull_request'\n          && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: docker-stub-US.zip\n          path: docker-stub-US.zip\n          if-no-files-found: error\n\n      - name: Upload Docker stub EU artifact\n        if: |\n          github.event_name != 'pull_request'\n          && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: docker-stub-EU.zip\n          path: docker-stub-EU.zip\n          if-no-files-found: error\n\n      - name: Build Swagger files\n        run: |\n          cd ./dev\n          pwsh ./generate_openapi_files.ps1\n\n      - name: Upload Public API Swagger artifact\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: swagger.json\n          path: api.public.json\n          if-no-files-found: error\n\n      - name: Upload Internal API Swagger artifact\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: internal.json\n          path: api.json\n          if-no-files-found: error\n\n      - name: Upload Identity Swagger artifact\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: identity.json\n          path: identity.json\n          if-no-files-found: error\n\n  build-mssqlmigratorutility:\n    name: Build MSSQL migrator utility\n    runs-on: ubuntu-22.04\n    needs: lint\n    defaults:\n      run:\n        shell: bash\n        working-directory: \"util/MsSqlMigratorUtility\"\n    strategy:\n      fail-fast: false\n      matrix:\n        target:\n          - osx-x64\n          - linux-x64\n          - win-x64\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ github.event.pull_request.head.sha }}\n          persist-credentials: false\n\n      - name: Set up .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      - name: Print environment\n        run: |\n          whoami\n          dotnet --info\n          echo \"GitHub ref: $GITHUB_REF\"\n          echo \"GitHub event: $GITHUB_EVENT\"\n\n      - name: Publish project\n        run: |\n          dotnet publish -c \"Release\" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true \\\n          -p:IncludeNativeLibrariesForSelfExtract=true --self-contained true\n\n      - name: Upload project artifact for Windows\n        if: ${{ contains(matrix.target, 'win') == true }}\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: MsSqlMigratorUtility-${{ matrix.target }}\n          path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe\n          if-no-files-found: error\n\n      - name: Upload project artifact\n        if: ${{ contains(matrix.target, 'win') == false }}\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: MsSqlMigratorUtility-${{ matrix.target }}\n          path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility\n          if-no-files-found: error\n\n  bitwarden-lite-build:\n    name: Trigger Bitwarden lite build\n    if: |\n      github.event_name != 'pull_request'\n      && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')\n    runs-on: ubuntu-22.04\n    needs: build-artifacts\n    permissions:\n      id-token: write\n    steps:\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Get Azure Key Vault secrets\n        id: get-kv-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        with:\n          keyvault: gh-org-bitwarden\n          secrets: \"BW-GHAPP-ID,BW-GHAPP-KEY\"\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Generate GH App token\n        uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1\n        id: app-token\n        with:\n          app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}\n          private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}\n          owner: ${{ github.repository_owner }}\n          repositories: self-host\n\n      - name: Trigger Bitwarden lite build\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{ steps.app-token.outputs.token }}\n          script: |\n            await github.rest.actions.createWorkflowDispatch({\n              owner: 'bitwarden',\n              repo: 'self-host',\n              workflow_id: 'build-bitwarden-lite.yml',\n              ref: 'main',\n              inputs: {\n                server_branch: process.env.GITHUB_REF\n              }\n            });\n\n  trigger-k8s-deploy:\n    name: Trigger K8s deploy\n    if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'\n    runs-on: ubuntu-22.04\n    needs: build-artifacts\n    permissions:\n      id-token: write\n    steps:\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Get Azure Key Vault secrets\n        id: get-kv-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        with:\n          keyvault: gh-org-bitwarden\n          secrets: \"BW-GHAPP-ID,BW-GHAPP-KEY\"\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Generate GH App token\n        uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1\n        id: app-token\n        with:\n          app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}\n          private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}\n          owner: ${{ github.repository_owner }}\n          repositories: devops\n\n      - name: Trigger K8s deploy\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          github-token: ${{ steps.app-token.outputs.token }}\n          script: |\n            await github.rest.actions.createWorkflowDispatch({\n              owner: 'bitwarden',\n              repo: 'devops',\n              workflow_id: 'deploy-k8s.yml',\n              ref: 'main',\n              inputs: {\n                environment: 'US-DEV Cloud',\n                tag: 'main'\n              }\n            })\n\n  setup-ephemeral-environment:\n    name: Setup Ephemeral Environment\n    needs: build-artifacts\n    if: |\n      needs.build-artifacts.outputs.has_secrets == 'true'\n      && github.event_name == 'pull_request'\n      && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')\n    uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main\n    with:\n      project: server\n      pull_request_number: ${{ github.event.number || 0 }}\n    secrets: inherit\n    permissions:\n      contents: read\n      id-token: write\n\n  check-failures:\n    name: Check for failures\n    if: always()\n    runs-on: ubuntu-22.04\n    needs:\n      - lint\n      - build-artifacts\n      - upload\n      - build-mssqlmigratorutility\n      - bitwarden-lite-build\n      - trigger-k8s-deploy\n    permissions:\n      id-token: write\n    steps:\n      - name: Check if any job failed\n        if: |\n          github.event_name != 'pull_request'\n          && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')\n          && contains(needs.*.result, 'failure')\n        run: exit 1\n\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Retrieve secrets\n        id: retrieve-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        if: failure()\n        with:\n          keyvault: \"bitwarden-ci\"\n          secrets: \"devops-alerts-slack-webhook-url\"\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Notify Slack on failure\n        uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0\n        if: failure()\n        env:\n          SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}\n        with:\n          status: ${{ job.status }}\n"
  },
  {
    "path": ".github/workflows/build_target.yml",
    "content": "name: Build on PR Target\n\non:\n  pull_request_target:\n    types: [opened, synchronize, reopened]\n    branches:\n      - \"main\"\n\ndefaults:\n  run:\n    shell: bash\n\njobs:\n  check-run:\n    name: Check PR run\n    uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main\n    permissions:\n      contents: read\n\n  run-workflow:\n    name: Run Build on PR Target\n    needs: check-run\n    if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}\n    uses: ./.github/workflows/build.yml\n    secrets: inherit\n\n    permissions:\n      contents: read\n      actions: read\n      id-token: write\n      security-events: write\n"
  },
  {
    "path": ".github/workflows/cleanup-rc-branch.yml",
    "content": "name: Cleanup RC Branch\n\non:\n  push:\n    tags:\n      - v**\n\njobs:\n  delete-rc:\n    name: Delete RC Branch\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: write\n      id-token: write\n    steps:\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Retrieve bot secrets\n        id: retrieve-bot-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        with:\n          keyvault: bitwarden-ci\n          secrets: \"github-pat-bitwarden-devops-bot-repo-scope\"\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Checkout main\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: main\n          token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}\n          persist-credentials: false\n          fetch-depth: 0\n\n      - name: Check if a RC branch exists\n        id: branch-check\n        run: |\n          hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)\n          rc_branch_check=$(git ls-remote --heads origin rc | wc -l)\n\n          if [[ \"${hotfix_rc_branch_check}\" -gt 0 ]]; then\n            echo \"hotfix-rc branch exists.\" | tee -a \"$GITHUB_STEP_SUMMARY\"\n            echo \"name=hotfix-rc\" >> \"$GITHUB_OUTPUT\"\n          elif [[ \"${rc_branch_check}\" -gt 0 ]]; then\n            echo \"rc branch exists.\" | tee -a \"$GITHUB_STEP_SUMMARY\"\n            echo \"name=rc\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n      - name: Delete RC branch\n        env:\n          BRANCH_NAME: ${{ steps.branch-check.outputs.name }}\n        run: |\n          if ! [[ -z \"$BRANCH_NAME\" ]]; then\n            git push --quiet origin --delete \"$BRANCH_NAME\"\n            echo \"Deleted $BRANCH_NAME branch.\" | tee -a \"$GITHUB_STEP_SUMMARY\"\n          fi\n"
  },
  {
    "path": ".github/workflows/code-references.yml",
    "content": "name: Collect code references\n\non:\n  push:\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  check-secret-access:\n    name: Check for secret access\n    runs-on: ubuntu-22.04\n    outputs:\n      available: ${{ steps.check-secret-access.outputs.available }}\n    permissions: {}\n\n    steps:\n      - name: Check\n        id: check-secret-access\n        run: |\n          if [ \"${{ secrets.AZURE_CLIENT_ID }}\" != '' ]; then\n            echo \"available=true\" >> \"$GITHUB_OUTPUT\";\n          else\n            echo \"available=false\" >> \"$GITHUB_OUTPUT\";\n          fi\n\n  refs:\n    name: Code reference collection\n    runs-on: ubuntu-22.04\n    needs: check-secret-access\n    if: ${{ needs.check-secret-access.outputs.available == 'true' }}\n    permissions:\n      contents: read\n      pull-requests: write\n      id-token: write\n\n    steps:\n      - name: Check out repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Get Azure Key Vault secrets\n        id: get-kv-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        with:\n          keyvault: gh-server\n          secrets: \"LD-ACCESS-TOKEN\"\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Collect\n        id: collect\n        uses: launchdarkly/find-code-references@89a7d362d1d4b3725fe0fe0ccd0dc69e3bdcba58 # v2.14.0\n        with:\n          accessToken: ${{ steps.get-kv-secrets.outputs.LD-ACCESS-TOKEN }}\n          projKey: default\n          allowTags: true\n\n      - name: Add label\n        if: steps.collect.outputs.any-changed == 'true'\n        run: gh pr edit \"$PR_NUMBER\" --add-label feature-flag\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          PR_NUMBER: ${{ github.event.pull_request.number }}\n\n      - name: Remove label\n        if: steps.collect.outputs.any-changed == 'false'\n        run: gh pr edit \"$PR_NUMBER\" --remove-label feature-flag\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          PR_NUMBER: ${{ github.event.pull_request.number }}\n"
  },
  {
    "path": ".github/workflows/enforce-labels.yml",
    "content": "name: Enforce PR labels\n\non:\n  workflow_call:\n  pull_request:\n    types: [labeled, unlabeled, opened, reopened, synchronize]\n\npermissions: {}\n\njobs:\n  enforce-label:\n    if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') || contains(github.event.*.labels.*.name, 'ephemeral-environment') }}\n    name: Enforce label\n    runs-on: ubuntu-22.04\n\n    steps:\n      - name: Check for label\n        run: |\n          echo \"PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged\"\n          echo \"### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged\" >> \"$GITHUB_STEP_SUMMARY\"\n          exit 1\n"
  },
  {
    "path": ".github/workflows/ephemeral-environment.yml",
    "content": "name: Ephemeral Environment\n\non:\n  pull_request:\n    types: [labeled]\n\npermissions:\n  contents: read\n  id-token: write\n\njobs:\n  setup-ephemeral-environment:\n    name: Setup Ephemeral Environment\n    if: github.event.label.name == 'ephemeral-environment'\n    uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main\n    with:\n      project: server\n      pull_request_number: ${{ github.event.number }}\n      sync_environment: false\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/load-test.yml",
    "content": "name: Load test\n\non:\n  schedule:\n    - cron: \"0 0 * * 1\" # Run every Monday at 00:00\n  workflow_dispatch:\n    inputs:\n      test-id:\n        type: string\n        description: \"Identifier label for Datadog metrics\"\n        default: \"server-load-test\"\n      k6-test-path:\n        type: string\n        description: \"Path to load test files\"\n        default: \"perf/load/*.js\"\n      k6-flags:\n        type: string\n        description: \"Additional k6 flags\"\n      api-env-url:\n        type: string\n        description: \"URL of the API environment\"\n        default: \"https://api.qa.bitwarden.pw\"\n      identity-env-url:\n        type: string\n        description: \"URL of the Identity environment\"\n        default: \"https://identity.qa.bitwarden.pw\"\n\npermissions:\n  contents: read\n  id-token: write\n\nenv:\n  # Secret configuration\n  AZURE_KEY_VAULT_NAME: gh-server\n  AZURE_KEY_VAULT_SECRETS: DD-API-KEY, K6-CLIENT-ID, K6-AUTH-USER-EMAIL, K6-AUTH-USER-PASSWORD-HASH\n  # Specify defaults for scheduled runs\n  TEST_ID: ${{ inputs.test-id || 'server-load-test' }}\n  K6_TEST_PATH: ${{ inputs.k6-test-path || 'perf/load/*.js' }}\n  API_ENV_URL: ${{ inputs.api-env-url || 'https://api.qa.bitwarden.pw' }}\n  IDENTITY_ENV_URL: ${{ inputs.identity-env-url || 'https://identity.qa.bitwarden.pw' }}\n\njobs:\n  run-tests:\n    name: Run load tests\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Get Azure Key Vault secrets\n        id: get-kv-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        with:\n          keyvault: ${{ env.AZURE_KEY_VAULT_NAME }}\n          secrets: ${{ env.AZURE_KEY_VAULT_SECRETS }}\n\n      - name: Log out of Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      # Datadog agent for collecting OTEL metrics from k6\n      - name: Start Datadog agent\n        env:\n          DD_API_KEY: ${{ steps.get-kv-secrets.outputs.DD-API-KEY }}\n        run: |\n          docker run --detach \\\n            --name datadog-agent \\\n            -p 4317:4317 \\\n            -p 5555:5555 \\\n            -e DD_SITE=us3.datadoghq.com \\\n            -e DD_API_KEY=\"${DD_API_KEY}\" \\\n            -e DD_DOGSTATSD_NON_LOCAL_TRAFFIC=1 \\\n            -e DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 \\\n            -e DD_HEALTH_PORT=5555 \\\n            -e HOST_PROC=/proc \\\n            --volume /var/run/docker.sock:/var/run/docker.sock:ro \\\n            --volume /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \\\n            --health-cmd \"curl -f http://localhost:5555/health || exit 1\" \\\n            --health-interval 10s \\\n            --health-timeout 5s \\\n            --health-retries 10 \\\n            --health-start-period 30s \\\n            --pid host \\\n            datadog/agent:7-full@sha256:7ea933dec3b8baa8c19683b1c3f6f801dbf3291f748d9ed59234accdaac4e479\n\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Set up k6\n        uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2 # v1.1.0\n\n      - name: Run k6 tests\n        uses: grafana/run-k6-action@a15e2072ede004e8d46141e33d7f7dad8ad08d9d # v1.3.1\n        continue-on-error: false\n        env:\n          K6_OTEL_METRIC_PREFIX: k6_\n          K6_OTEL_GRPC_EXPORTER_INSECURE: true\n          # Load test specific environment variables\n          API_URL: ${{ env.API_ENV_URL }}\n          IDENTITY_URL: ${{ env.IDENTITY_ENV_URL }}\n          CLIENT_ID: ${{ steps.get-kv-secrets.outputs.K6-CLIENT-ID }}\n          AUTH_USER_EMAIL: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-EMAIL }}\n          AUTH_USER_PASSWORD_HASH: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-PASSWORD-HASH }}\n        with:\n          flags: >-\n            --tag test-id=${{ env.TEST_ID }}\n            -o experimental-opentelemetry\n            ${{ inputs.k6-flags }}\n          path: ${{ env.K6_TEST_PATH }}\n"
  },
  {
    "path": ".github/workflows/protect-files.yml",
    "content": "# Runs if there are changes to the paths: list.\n# Starts a matrix job to check for modified files, then sets output based on the results.\n# The input decides if the label job is ran, adding a label to the PR.\nname: Protect files\n\non:\n  pull_request:\n    types:\n      - opened\n      - synchronize\n      - unlabeled\n    paths:\n      - \"util/Migrator/DbScripts/**.sql\"\n\njobs:\n  changed-files:\n    name: Check for file changes\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      pull-requests: write\n    outputs:\n      changes: ${{steps.check-changes.outputs.changes_detected}}\n\n    strategy:\n      fail-fast: true\n      matrix:\n        include:\n          - name: Database Scripts\n            path: util/Migrator/DbScripts\n            label: \"DB-migrations-changed\"\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 2\n          persist-credentials: false\n\n      - name: Check for file changes\n        id: check-changes\n        run: |\n          MODIFIED_FILES=$(git diff --name-only --diff-filter=M HEAD~1)\n\n          for file in $MODIFIED_FILES\n          do\n            if [[ $file == *\"${{ matrix.path }}\"* ]]; then\n              echo \"changes_detected=true\" >> \"$GITHUB_OUTPUT\"\n              break\n            else echo \"changes_detected=false\" >> \"$GITHUB_OUTPUT\"\n            fi\n          done\n\n      - name: Add label to pull request\n        if: contains(steps.check-changes.outputs.changes_detected, 'true')\n        uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # 1.0.4\n        with:\n          add-labels: ${{ matrix.label }}\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish\nrun-name: Publish ${{ inputs.dry_run && '(Dry Run)' || '' }}\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version to publish (default: latest release)'\n        required: true\n        type: string\n        default: latest\n      branch:\n        description: \"Branch to publish from\"\n        required: true\n        type: choice\n        options:\n          - rc\n          - hotfix-rc\n        default: rc\n      dry_run:\n        description: \"Dry Run\"\n        type: boolean\n        default: false\n\nenv:\n  _AZ_REGISTRY: \"bitwardenprod.azurecr.io\"\n\njobs:\n  setup:\n    name: Setup\n    runs-on: ubuntu-22.04\n    permissions:\n      deployments: write\n    outputs:\n      deployment-id: ${{ steps.deployment.outputs.deployment_id }}\n      release-version: ${{ steps.version-output.outputs.version }}\n    steps:\n      - name: Version output\n        id: version-output\n        env:\n          INPUT_VERSION: ${{ inputs.version }}\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          if [[ \"${INPUT_VERSION}\" == \"latest\" || \"${INPUT_VERSION}\" == \"\" ]]; then\n            VERSION=$(gh api repos/bitwarden/server/releases/latest --jq '.tag_name | ltrimstr(\"v\")')\n            echo \"Latest Released Version: $VERSION\"\n            echo \"version=$VERSION\" >> \"$GITHUB_OUTPUT\"\n          else\n            VERSION=\"${INPUT_VERSION#v}\"\n            echo \"Release Version: ${VERSION}\"\n            echo \"version=${VERSION}\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n      - name: Create GitHub deployment\n        if: ${{ !inputs.dry_run }}\n        uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7\n        id: deployment\n        with:\n          token: '${{ secrets.GITHUB_TOKEN }}'\n          initial-status: 'in_progress'\n          environment: 'production'\n          description: 'Deployment ${{ steps.version-output.outputs.release-version }} from branch ${{ github.ref_name }}'\n          task: release\n\n  publish-docker:\n    name: Publish Docker images\n    runs-on: ubuntu-22.04\n    needs: setup\n    permissions:\n      contents: read\n      id-token: write\n    env:\n      _RELEASE_VERSION: ${{ needs.setup.outputs.release-version }}\n      _BRANCH_NAME: ${{ inputs.branch }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - project_name: Admin\n          - project_name: Api\n          - project_name: Attachments\n          - project_name: Billing\n          - project_name: Events\n          - project_name: EventsProcessor\n          - project_name: Icons\n          - project_name: Identity\n          - project_name: MsSql\n          - project_name: MsSqlMigratorUtility\n          - project_name: Nginx\n          - project_name: Notifications\n          - project_name: Scim\n          - project_name: Setup\n          - project_name: Sso\n    steps:\n      - name: Print environment\n        run: |\n          whoami\n          echo \"GitHub ref: $GITHUB_REF\"\n          echo \"GitHub event: $GITHUB_EVENT\"\n          echo \"Dry Run: ${{ inputs.dry_run }}\"\n\n      - name: Set up project name\n        id: setup\n        run: |\n          PROJECT_NAME=$(echo \"${{ matrix.project_name }}\" | awk '{print tolower($0)}')\n          echo \"Matrix name: ${{ matrix.project_name }}\"\n          echo \"PROJECT_NAME: $PROJECT_NAME\"\n          echo \"project_name=$PROJECT_NAME\" >> \"$GITHUB_OUTPUT\"\n\n      ########## ACR PROD ##########\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Log in to Azure ACR\n        run: az acr login -n \"$_AZ_REGISTRY\" --only-show-errors\n\n      - name: Push version and latest image\n        env:\n          PROJECT_NAME: ${{ steps.setup.outputs.project_name }}\n        run: |\n          if [[ \"${{ inputs.dry_run }}\" == \"true\" ]]; then\n            skopeo copy --all \\\n              \"docker://$_AZ_REGISTRY/$PROJECT_NAME:latest\" \\\n              \"docker://$_AZ_REGISTRY/$PROJECT_NAME:dryrun\"\n          else\n            skopeo copy --all \\\n              \"docker://$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME\" \\\n              \"docker://$_AZ_REGISTRY/$PROJECT_NAME:$_RELEASE_VERSION\"\n            skopeo copy --all \\\n              \"docker://$_AZ_REGISTRY/$PROJECT_NAME:$_BRANCH_NAME\" \\\n              \"docker://$_AZ_REGISTRY/$PROJECT_NAME:latest\"\n          fi\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n  update-deployment:\n    name: Update Deployment Status\n    runs-on: ubuntu-22.04\n    needs:\n      - setup\n      - publish-docker\n    permissions:\n      deployments: write\n    if: ${{ always() && !inputs.dry_run }}\n    steps:\n      - name: Check if any job failed\n        if: contains(needs.*.result, 'failure')\n        run: exit 1\n\n      - name: Update deployment status to Success\n        if: success()\n        uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3\n        with:\n          token: '${{ secrets.GITHUB_TOKEN }}'\n          state: 'success'\n          deployment-id: ${{ needs.setup.outputs.deployment-id }}\n\n      - name: Update deployment status to Failure\n        if: failure()\n        uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3\n        with:\n          token: '${{ secrets.GITHUB_TOKEN }}'\n          state: 'failure'\n          deployment-id: ${{ needs.setup.outputs.deployment-id }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\nrun-name: Release ${{ inputs.release_type }}\n\non:\n  workflow_dispatch:\n    inputs:\n      release_type:\n        description: \"Release Options\"\n        required: true\n        default: \"Initial Release\"\n        type: choice\n        options:\n          - Initial Release\n          - Redeploy\n          - Dry Run\n\nenv:\n  _AZ_REGISTRY: \"bitwardenprod.azurecr.io\"\n\npermissions:\n  contents: read\n\njobs:\n  setup:\n    name: Setup\n    runs-on: ubuntu-22.04\n    outputs:\n      release_version: ${{ steps.version.outputs.version }}\n      branch-name: ${{ steps.branch.outputs.branch-name }}\n    steps:\n      - name: Branch check\n        if: ${{ inputs.release_type != 'Dry Run' }}\n        run: |\n          if [[ \"$GITHUB_REF\" != \"refs/heads/rc\" ]] && [[ \"$GITHUB_REF\" != \"refs/heads/hotfix-rc\" ]]; then\n            echo \"===================================\"\n            echo \"[!] Can only release from the 'rc' or 'hotfix-rc' branches\"\n            echo \"===================================\"\n            exit 1\n          fi\n\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n          persist-credentials: false\n\n      - name: Check release version\n        id: version\n        uses: bitwarden/gh-actions/release-version-check@main\n        with:\n          release-type: ${{ inputs.release_type }}\n          project-type: dotnet\n          file: Directory.Build.props\n\n      - name: Get branch name\n        id: branch\n        run: |\n          BRANCH_NAME=$(basename \"${GITHUB_REF}\")\n          echo \"branch-name=$BRANCH_NAME\" >> \"$GITHUB_OUTPUT\"\n\n  release:\n    name: Create GitHub release\n    runs-on: ubuntu-22.04\n    needs: setup\n    permissions:\n      contents: write\n    steps:\n      - name: Download latest release Docker stubs\n        if: ${{ inputs.release_type != 'Dry Run' }}\n        uses: bitwarden/gh-actions/download-artifacts@main\n        with:\n          workflow: build.yml\n          workflow_conclusion: success\n          branch: ${{ needs.setup.outputs.branch-name }}\n          artifacts: \"docker-stub-US.zip,\n            docker-stub-EU.zip,\n            swagger.json\"\n\n      - name: Dry Run - Download latest release Docker stubs\n        if: ${{ inputs.release_type == 'Dry Run' }}\n        uses: bitwarden/gh-actions/download-artifacts@main\n        with:\n          workflow: build.yml\n          workflow_conclusion: success\n          branch: main\n          artifacts: \"docker-stub-US.zip,\n            docker-stub-EU.zip,\n            swagger.json\"\n\n      - name: Create release\n        if: ${{ inputs.release_type != 'Dry Run' }}\n        uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0\n        with:\n          artifacts: \"docker-stub-US.zip,\n            docker-stub-EU.zip,\n            swagger.json\"\n          commit: ${{ github.sha }}\n          tag: \"v${{ needs.setup.outputs.release_version }}\"\n          name: \"Version ${{ needs.setup.outputs.release_version }}\"\n          body: \"<insert release notes here>\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          draft: true\n"
  },
  {
    "path": ".github/workflows/repository-management.yml",
    "content": "name: Repository management\n\non:\n  workflow_dispatch:\n    inputs:\n      task:\n        default: \"Version Bump\"\n        description: \"Task to execute\"\n        options:\n          - \"Version Bump\"\n          - \"Version Bump and Cut rc\"\n          - \"Version Bump and Cut hotfix-rc\"\n        required: true\n        type: choice\n      target_ref:\n        default: \"main\"\n        description: \"Branch/Tag to target for cut\"\n        required: true\n        type: string\n      version_number_override:\n        description: \"New version override (leave blank for automatic calculation, example: '2024.1.0')\"\n        required: false\n        type: string\n\npermissions: {}\n\njobs:\n  setup:\n    name: Setup\n    runs-on: ubuntu-24.04\n    outputs:\n      branch: ${{ steps.set-branch.outputs.branch }}\n    permissions: {}\n    steps:\n      - name: Set branch\n        id: set-branch\n        env:\n          TASK: ${{ inputs.task }}\n        run: |\n          if [[ \"$TASK\" == \"Version Bump\" ]]; then\n            BRANCH=\"none\"\n          elif [[ \"$TASK\" == \"Version Bump and Cut rc\" ]]; then\n            BRANCH=\"rc\"\n          elif [[ \"$TASK\" == \"Version Bump and Cut hotfix-rc\" ]]; then\n            BRANCH=\"hotfix-rc\"\n          fi\n\n          echo \"branch=$BRANCH\" >> \"$GITHUB_OUTPUT\"\n\n  bump_version:\n    name: Bump Version\n    if: ${{ always() }}\n    runs-on: ubuntu-24.04\n    needs:\n      - setup\n    outputs:\n      version: ${{ steps.set-final-version-output.outputs.version }}\n    permissions:\n      id-token: write\n\n    steps:\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Get Azure Key Vault secrets\n        id: get-kv-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        with:\n          keyvault: gh-org-bitwarden\n          secrets: \"BW-GHAPP-ID,BW-GHAPP-KEY\"\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Validate version input format\n        if: ${{ inputs.version_number_override != '' }}\n        uses: bitwarden/gh-actions/version-check@main\n        with:\n          version: ${{ inputs.version_number_override }}\n\n      - name: Generate GH App token\n        uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1\n        id: app-token\n        with:\n          app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}\n          private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}\n          permission-contents: write\n\n      - name: Check out branch\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: main\n          token: ${{ steps.app-token.outputs.token }}\n          persist-credentials: true\n\n      - name: Configure Git\n        run: |\n          git config --local user.email \"actions@github.com\"\n          git config --local user.name \"Github Actions\"\n\n      - name: Install xmllint\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libxml2-utils\n\n      - name: Get current version\n        id: current-version\n        run: |\n          CURRENT_VERSION=$(xmllint -xpath \"/Project/PropertyGroup/Version/text()\" Directory.Build.props)\n          echo \"version=$CURRENT_VERSION\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Verify input version\n        if: ${{ inputs.version_number_override != '' }}\n        env:\n          CURRENT_VERSION: ${{ steps.current-version.outputs.version }}\n          NEW_VERSION: ${{ inputs.version_number_override }}\n        run: |\n          # Error if version has not changed.\n          if [[ \"$NEW_VERSION\" == \"$CURRENT_VERSION\" ]]; then\n            echo \"Specified override version is the same as the current version.\" >> \"$GITHUB_STEP_SUMMARY\"\n            exit 1\n          fi\n\n          # Check if version is newer.\n          if printf '%s\\n' \"${CURRENT_VERSION}\" \"${NEW_VERSION}\" | sort -C -V; then\n            echo \"Version is newer than the current version.\"\n          else\n            echo \"Version is older than the current version.\" >> \"$GITHUB_STEP_SUMMARY\"\n            exit 1\n          fi\n\n      - name: Calculate next release version\n        if: ${{ inputs.version_number_override == '' }}\n        id: calculate-next-version\n        uses: bitwarden/gh-actions/version-next@main\n        with:\n          version: ${{ steps.current-version.outputs.version }}\n\n      - name: Bump version props - Version Override\n        if: ${{ inputs.version_number_override != '' }}\n        id: bump-version-override\n        uses: bitwarden/gh-actions/version-bump@main\n        with:\n          file_path: \"Directory.Build.props\"\n          version: ${{ inputs.version_number_override }}\n\n      - name: Bump version props - Automatic Calculation\n        if: ${{ inputs.version_number_override == '' }}\n        id: bump-version-automatic\n        uses: bitwarden/gh-actions/version-bump@main\n        with:\n          file_path: \"Directory.Build.props\"\n          version: ${{ steps.calculate-next-version.outputs.version }}\n\n      - name: Set final version output\n        id: set-final-version-output\n        env:\n          VERSION: ${{ inputs.version_number_override }}\n          BUMP_VERSION_OVERRIDE_OUTCOME: ${{ steps.bump-version-override.outcome }}\n          BUMP_VERSION_AUTOMATIC_OUTCOME: ${{ steps.bump-version-automatic.outcome }}\n          CALCULATE_NEXT_VERSION: ${{ steps.calculate-next-version.outputs.version }}\n        run: |\n          if [[ \"${BUMP_VERSION_OVERRIDE_OUTCOME}\" = \"success\" ]]; then\n            echo \"version=$VERSION\" >> \"$GITHUB_OUTPUT\"\n          elif [[ \"${BUMP_VERSION_AUTOMATIC_OUTCOME}\" = \"success\" ]]; then\n            echo \"version=${CALCULATE_NEXT_VERSION}\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n      - name: Commit files\n        env:\n          FINAL_VERSION: ${{ steps.set-final-version-output.outputs.version }}\n        run: git commit -m \"Bumped version to $FINAL_VERSION\" -a\n\n      - name: Push changes\n        run: git push\n\n  cut_branch:\n    name: Cut branch\n    if: ${{ needs.setup.outputs.branch != 'none' }}\n    needs:\n      - setup\n      - bump_version\n    runs-on: ubuntu-24.04\n    permissions:\n      id-token: write\n\n    steps:\n      - name: Log in to Azure\n        uses: bitwarden/gh-actions/azure-login@main\n        with:\n          subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          tenant_id: ${{ secrets.AZURE_TENANT_ID }}\n          client_id: ${{ secrets.AZURE_CLIENT_ID }}\n\n      - name: Get Azure Key Vault secrets\n        id: get-kv-secrets\n        uses: bitwarden/gh-actions/get-keyvault-secrets@main\n        with:\n          keyvault: gh-org-bitwarden\n          secrets: \"BW-GHAPP-ID,BW-GHAPP-KEY\"\n\n      - name: Log out from Azure\n        uses: bitwarden/gh-actions/azure-logout@main\n\n      - name: Generate GH App token\n        uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1\n        id: app-token\n        with:\n          app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}\n          private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}\n          permission-contents: write\n\n      - name: Check out target ref\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ inputs.target_ref }}\n          token: ${{ steps.app-token.outputs.token }}\n          persist-credentials: true\n          fetch-depth: 0\n\n      - name: Check if ${{ needs.setup.outputs.branch }} branch exists\n        env:\n          BRANCH_NAME: ${{ needs.setup.outputs.branch }}\n        run: |\n          if [[ $(git ls-remote --heads origin \"$BRANCH_NAME\") ]]; then\n            echo \"$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again.\" >> \"$GITHUB_STEP_SUMMARY\"\n            exit 1\n          fi\n\n      - name: Cut branch\n        env:\n          BRANCH_NAME: ${{ needs.setup.outputs.branch }}\n        run: |\n          git switch --quiet --create \"$BRANCH_NAME\"\n          git push --quiet --set-upstream origin \"$BRANCH_NAME\"\n\n  move_edd_db_scripts:\n    name: Move EDD database scripts\n    needs: cut_branch\n    permissions: {}\n    uses: ./.github/workflows/_move_edd_db_scripts.yml\n"
  },
  {
    "path": ".github/workflows/respond.yml",
    "content": "name: Respond\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\npermissions: {}\n\njobs:\n  respond:\n    name: Respond\n    uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main\n    secrets:\n      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n      AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}\n      AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}\n    permissions:\n      actions: read\n      contents: write\n      id-token: write\n      issues: write\n      pull-requests: write\n"
  },
  {
    "path": ".github/workflows/review-code.yml",
    "content": "name: Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize, reopened]\n\npermissions: {}\n\njobs:\n  review:\n    name: Review\n    uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main\n    secrets:\n      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n      AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}\n      AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}\n    permissions:\n      actions: read\n      contents: read\n      id-token: write\n      pull-requests: write\n"
  },
  {
    "path": ".github/workflows/scan.yml",
    "content": "name: Scan\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - \"main\"\n      - \"rc\"\n      - \"hotfix-rc\"\n  pull_request:\n    types: [opened, synchronize, reopened]\n    branches-ignore:\n      - main\n  pull_request_target:\n    types: [opened, synchronize, reopened]\n    branches:\n      - \"main\"\n\npermissions: {}\n\njobs:\n  check-run:\n    name: Check PR run\n    uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main\n    permissions:\n      contents: read\n\n  sast:\n    name: Checkmarx\n    uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main\n    needs: check-run\n    secrets:\n      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n      AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}\n      AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}\n    permissions:\n      contents: read\n      pull-requests: write\n      security-events: write\n      id-token: write\n\n  quality:\n    name: Sonar\n    uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main\n    needs: check-run\n    secrets:\n      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n      AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}\n      AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}\n    permissions:\n      contents: read\n      pull-requests: write\n      id-token: write\n    with:\n      sonar-config: \"dotnet\"\n"
  },
  {
    "path": ".github/workflows/stale-bot.yml",
    "content": "name: Staleness\non:\n  workflow_dispatch:\n  schedule: # Run once a day at 5.23am (arbitrary but should avoid peak loads on the hour)\n    - cron: \"23 5 * * *\"\n\njobs:\n  stale:\n    name: Check for stale issues and PRs\n    runs-on: ubuntu-22.04\n    permissions:\n      actions: write\n      contents: read\n      issues: write\n      pull-requests: write\n    steps:\n      - name: Check\n        uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1\n        with:\n          stale-issue-label: \"needs-reply\"\n          stale-pr-label: \"needs-changes\"\n          days-before-stale: -1 # Do not apply the stale labels automatically, this is a manual process\n          days-before-issue-close: 14 # Close issue if no further activity after X days\n          days-before-pr-close: 21 # Close PR if no further activity after X days\n          close-issue-message: |\n            We need more information before we can help you with your problem. As we haven’t heard from you recently, this issue will be closed.\n\n            If this happens again or continues to be an problem, please respond to this issue with the information we’ve requested and anything else relevant.\n          close-pr-message: |\n            We can’t merge your pull request until you make the changes we’ve requested. As we haven’t heard from you recently, this pull request will be closed.\n\n            If you’re still working on this, please respond here after you’ve made the changes we’ve requested and our team will re-open it for further review.\n\n            Please make sure to resolve any conflicts with the main branch before requesting another review.\n"
  },
  {
    "path": ".github/workflows/test-database.yml",
    "content": "name: Database testing\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - \"main\"\n      - \"rc\"\n      - \"hotfix-rc\"\n    paths:\n      - \".github/workflows/test-database.yml\" # This file\n      - \"src/Sql/**\" # SQL Server Database Changes\n      - \"util/Migrator/**\" # New SQL Server Migrations\n      - \"util/MySqlMigrations/**\" # Changes to MySQL\n      - \"util/PostgresMigrations/**\" # Changes to Postgres\n      - \"util/SqliteMigrations/**\" # Changes to Sqlite\n      - \"src/Infrastructure.Dapper/**\" # Changes to SQL Server Dapper Repository Layer\n      - \"src/Infrastructure.EntityFramework/**\" # Changes to Entity Framework Repository Layer\n      - \"test/Infrastructure.IntegrationTest/**\" # Any changes to the tests\n      - \"src/**/Entities/**/*.cs\" # Database entity definitions\n  pull_request:\n    paths:\n      - \".github/workflows/test-database.yml\" # This file\n      - \"src/Sql/**\" # SQL Server Database Changes\n      - \"util/Migrator/**\" # New SQL Server Migrations\n      - \"util/MySqlMigrations/**\" # Changes to MySQL\n      - \"util/PostgresMigrations/**\" # Changes to Postgres\n      - \"util/SqliteMigrations/**\" # Changes to Sqlite\n      - \"src/Infrastructure.Dapper/**\" # Changes to SQL Server Dapper Repository Layer\n      - \"src/Infrastructure.EntityFramework/**\" # Changes to Entity Framework Repository Layer\n      - \"test/Infrastructure.IntegrationTest/**\" # Any changes to the tests\n      - \"src/**/Entities/**/*.cs\" # Database entity definitions\n\npermissions:\n  contents: read\n\njobs:\n  test:\n    name: Run tests\n    runs-on: ubuntu-22.04\n    permissions:\n      contents: read\n      actions: read\n      checks: write\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Set up .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      - name: Restore tools\n        run: dotnet tool restore\n\n      - name: Docker Compose databases\n        working-directory: \"dev\"\n        # We could think about not using profiles and pulling images directly to cover multiple versions\n        run: |\n          cp .env.example .env\n          docker compose --profile mssql --profile postgres --profile mysql up -d\n        shell: pwsh\n\n      - name: Add MariaDB for Bitwarden lite\n        # Use a different port than MySQL\n        run: |\n          docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10\n\n      # I've seen the SQL Server container not be ready for commands right after starting up and just needing a bit longer to be ready\n      - name: Sleep\n        run: sleep 15s\n\n      - name: Checking pending model changes (MySQL)\n        working-directory: \"util/MySqlMigrations\"\n        run: 'dotnet ef migrations has-pending-model-changes -- --GlobalSettings:MySql:ConnectionString=\"$CONN_STR\"'\n        env:\n          CONN_STR: \"server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true\"\n\n      - name: Checking pending model changes (Postgres)\n        working-directory: \"util/PostgresMigrations\"\n        run: 'dotnet ef migrations has-pending-model-changes -- --GlobalSettings:PostgreSql:ConnectionString=\"$CONN_STR\"'\n        env:\n          CONN_STR: \"Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev\"\n\n      - name: Checking pending model changes (SQLite)\n        working-directory: \"util/SqliteMigrations\"\n        run: 'dotnet ef migrations has-pending-model-changes -- --GlobalSettings:Sqlite:ConnectionString=\"$CONN_STR\"'\n        env:\n          CONN_STR: \"Data Source=${{ runner.temp }}/test.db\"\n\n      - name: Migrate SQL Server\n        run: 'dotnet run --project util/MsSqlMigratorUtility/ \"$CONN_STR\"'\n        env:\n          CONN_STR: \"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;\"\n\n      - name: Migrate MySQL\n        working-directory: \"util/MySqlMigrations\"\n        run: 'dotnet ef database update --connection \"$CONN_STR\" -- --GlobalSettings:MySql:ConnectionString=\"$CONN_STR\"'\n        env:\n          CONN_STR: \"server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true\"\n\n      - name: Migrate MariaDB\n        working-directory: \"util/MySqlMigrations\"\n        run: 'dotnet ef database update --connection \"$CONN_STR\" -- --GlobalSettings:MySql:ConnectionString=\"$CONN_STR\"'\n        env:\n          CONN_STR: \"server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true\"\n\n      - name: Migrate Postgres\n        working-directory: \"util/PostgresMigrations\"\n        run: 'dotnet ef database update --connection \"$CONN_STR\" -- --GlobalSettings:PostgreSql:ConnectionString=\"$CONN_STR\"'\n        env:\n          CONN_STR: \"Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev\"\n\n      - name: Migrate SQLite\n        working-directory: \"util/SqliteMigrations\"\n        run: 'dotnet ef database update --connection \"$CONN_STR\" -- --GlobalSettings:Sqlite:ConnectionString=\"$CONN_STR\"'\n        env:\n          CONN_STR: \"Data Source=${{ runner.temp }}/test.db\"\n\n      - name: Run tests\n        working-directory: \"test/Infrastructure.IntegrationTest\"\n        env:\n          # Default Postgres:\n          BW_TEST_DATABASES__0__TYPE: \"Postgres\"\n          BW_TEST_DATABASES__0__CONNECTIONSTRING: \"Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev\"\n          # Default MySql\n          BW_TEST_DATABASES__1__TYPE: \"MySql\"\n          BW_TEST_DATABASES__1__CONNECTIONSTRING: \"server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev\"\n          # Default Dapper SqlServer\n          BW_TEST_DATABASES__2__TYPE: \"SqlServer\"\n          BW_TEST_DATABASES__2__CONNECTIONSTRING: \"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;\"\n          # Default Sqlite\n          BW_TEST_DATABASES__3__TYPE: \"Sqlite\"\n          BW_TEST_DATABASES__3__CONNECTIONSTRING: \"Data Source=${{ runner.temp }}/test.db\"\n          # Bitwarden lite MariaDB\n          BW_TEST_DATABASES__4__TYPE: \"MySql\"\n          BW_TEST_DATABASES__4__CONNECTIONSTRING: \"server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true\"\n        run: dotnet test --logger \"trx;LogFileName=infrastructure-test-results.trx\" /p:CoverletOutputFormatter=\"cobertura\" --collect:\"XPlat Code Coverage\"\n        shell: pwsh\n\n      - name: Print MySQL Logs\n        if: failure()\n        run: 'docker logs \"$(docker ps --quiet --filter \"name=mysql\")\"'\n\n      - name: Print MariaDB Logs\n        if: failure()\n        run: 'docker logs \"$(docker ps --quiet --filter \"name=mariadb\")\"'\n\n      - name: Print Postgres Logs\n        if: failure()\n        run: 'docker logs \"$(docker ps --quiet --filter \"name=postgres\")\"'\n\n      - name: Print MSSQL Logs\n        if: failure()\n        run: 'docker logs \"$(docker ps --quiet --filter \"name=mssql\")\"'\n\n      - name: Report test results\n        uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0\n        if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}\n        with:\n          name: Test Results\n          path: \"./**/*-test-results.trx\"\n          reporter: dotnet-trx\n          fail-on-error: true\n\n      - name: Upload to codecov.io\n        uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2\n\n      - name: Docker Compose down\n        if: always()\n        working-directory: \"dev\"\n        run: docker compose down\n        shell: pwsh\n\n  validate:\n    name: Run validation\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Set up .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      - name: Print environment\n        run: |\n          dotnet --info\n          nuget help | grep Version\n          echo \"GitHub ref: $GITHUB_REF\"\n          echo \"GitHub event: $GITHUB_EVENT\"\n\n      - name: Build DACPAC\n        run: dotnet build src/Sql --configuration Release --verbosity minimal --output .\n        shell: pwsh\n\n      - name: Upload DACPAC\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: sql.dacpac\n          path: Sql.dacpac\n\n      - name: Docker Compose up\n        working-directory: \"dev\"\n        run: |\n          cp .env.example .env\n          docker compose --profile mssql up -d\n        shell: pwsh\n\n      - name: Migrate\n        run: 'dotnet run --project util/MsSqlMigratorUtility/ \"$CONN_STR\"'\n        env:\n          CONN_STR: \"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;\"\n\n      - name: Diff .sqlproj to migrations\n        run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:\"Sql.dacpac\" /TargetConnectionString:\"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;\" /OutputPath:\"report.xml\" /p:IgnoreColumnOrder=True /p:IgnoreComments=True\n        shell: pwsh\n\n      - name: Generate SQL file\n        run: /usr/local/sqlpackage/sqlpackage /action:Script /SourceFile:\"Sql.dacpac\" /TargetConnectionString:\"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;\" /OutputPath:\"diff.sql\" /p:IgnoreColumnOrder=True /p:IgnoreComments=True\n        shell: pwsh\n\n      - name: Report validation results\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: report.xml\n          path: |\n            report.xml\n            diff.sql\n\n      - name: Validate XML\n        run: |\n          if grep -q \"<Operations>\" \"report.xml\"; then\n             echo \"ERROR: Migration files are not in sync with the SQL project\"\n             echo \"\"\n             echo \"Check these locations:\"\n             echo \"  - Migration scripts: util/Migrator/DbScripts/\"\n             echo \"  - SQL project files: src/Sql/\"\n             echo \"  - Download 'report.xml' artifact for full details\"\n             echo \"\"\n             \n             # Show actual SQL differences - exclude database setup commands\n             if [ -s \"diff.sql\" ]; then\n               echo \"Key SQL differences:\"\n               # Show meaningful schema differences, filtering out database setup noise\n               grep -E \"^(CREATE|DROP|ALTER)\" diff.sql | grep -v \"ALTER DATABASE\" | grep -v \"DatabaseName\" | head -5\n               echo \"\"\n             fi\n             \n             echo \"Common causes: naming differences (underscores, case), missing objects, or definition mismatches\"\n             \n             exit 1\n           else\n             echo \"SUCCESS: Database validation passed\"\n           fi\n        shell: bash\n\n      - name: Docker Compose down\n        if: ${{ always() }}\n        working-directory: \"dev\"\n        run: docker compose down\n        shell: pwsh\n\n  validate-migration-naming:\n    name: Validate new migration naming and order\n    runs-on: ubuntu-22.04\n\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n          persist-credentials: false\n\n      - name: Validate new migrations for pull request\n        if: github.event_name == 'pull_request'\n        run: |\n          git fetch origin main:main\n          pwsh dev/verify_migrations.ps1 -BaseRef main\n        shell: pwsh\n\n      - name: Validate new migrations for push\n        if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'\n        run: pwsh dev/verify_migrations.ps1 -BaseRef HEAD~1\n        shell: pwsh\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Testing\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - \"main\"\n      - \"rc\"\n      - \"hotfix-rc\"\n  pull_request:\n\nenv:\n  _AZ_REGISTRY: \"bitwardenprod.azurecr.io\"\n\njobs:\n  testing:\n    name: Run tests\n    if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}\n    runs-on: ubuntu-22.04\n    permissions:\n      checks: write\n      contents: read\n      pull-requests: write\n\n    env:\n      NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages\n\n    steps:\n      - name: Check out repo\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n\n      - name: Set up .NET\n        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0\n\n      - name: Install rust\n        uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable\n        with:\n          toolchain: stable\n\n      - name: Cache cargo registry\n        uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2\n\n      - name: Print environment\n        run: |\n          dotnet --info\n          nuget help | grep Version\n          echo \"GitHub ref: $GITHUB_REF\"\n          echo \"GitHub event: $GITHUB_EVENT\"\n\n      - name: Remove SQL project\n        run: dotnet sln bitwarden-server.sln remove src/Sql/Sql.sqlproj\n\n      - name: Test OSS solution\n        run: dotnet test ./test --configuration Debug --logger \"trx;LogFileName=oss-test-results.trx\" /p:CoverletOutputFormatter=\"cobertura\" --collect:\"XPlat Code Coverage\"\n\n      - name: Test Bitwarden solution\n        run: dotnet test ./bitwarden_license/test --configuration Debug --logger \"trx;LogFileName=bw-test-results.trx\" /p:CoverletOutputFormatter=\"cobertura\" --collect:\"XPlat Code Coverage\"\n\n      - name: Report test results\n        uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0\n        if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}\n        with:\n          name: Test Results\n          path: \"**/*-test-results.trx\"\n          reporter: dotnet-trx\n          fail-on-error: true\n\n      - name: Upload to codecov.io\n        uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbuild/\nbld/\n[Bb]in/\n[Oo]bj/\n\n# Visual Studo 2015 cache/options directory\n.vs/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opensdf\n*.sdf\n*.cachefile\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding addin-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# TODO: Comment the next line if you want to checkin your web deploy settings\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\nPublishProfiles/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/packages/*\n# except build/, which is used as an MSBuild target.\n!**/packages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/packages/repositories.config\n\n# Windows Azure Build Output\ncsx/\n*.build.csdef\n\n# Windows Store app package directory\nAppPackages/\n\n# Others\n*.[Cc]ache\nClientBin/\n[Ss]tyle[Cc]op.*\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.pfx\n*.publishsettings\nnode_modules/\nbower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Other\nproject.lock.json\n*.jfm\nmail_dist/\n*.refactorlog\n*.scmp\nsrc/Core/Properties/launchSettings.json\n*.override.env\n**/*.DS_Store\nsrc/Admin/wwwroot/assets\n.vscode/*\n**/.vscode/*\nbitwarden_license/src/Sso/wwwroot/assets\n.github/test/build.secrets\n**/CoverageOutput/\n.idea/*\n**/**.swp\n.mono\nsrc/Core/MailTemplates/Mjml/out\nsrc/Core/MailTemplates/Mjml/out-hbs\nNativeMethods.g.cs\nutil/RustSdk/rust/target\n\nsrc/Admin/Admin.zip\nsrc/Api/Api.zip\nsrc/Billing/Billing.zip\nsrc/Events/Events.zip\nsrc/EventsProcessor/EventsProcessor.zip\nsrc/Identity/Identity.zip\nsrc/Notifications/Notifications.zip\nbitwarden_license/src/Portal/Portal.zip\nbitwarden_license/src/Sso/Sso.zip\n**/src/**/flags.json\n\n# Generated swagger specs\n/identity.json\n/api.json\n/api.public.json\n\n# Serena\n.serena/\n\n# Claude Code\nCLAUDE.local.md\n.worktrees/\n"
  },
  {
    "path": ".run/Full Server - Self-hosted.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"Full Server - Self-hosted\" type=\"CompoundRunConfigurationType\">\n    <toRun name=\"Admin: Admin-SelfHost\" type=\"LaunchSettings\" />\n    <toRun name=\"Api: Api-SelfHost\" type=\"LaunchSettings\" />\n    <toRun name=\"Events: Events-SelfHost\" type=\"LaunchSettings\" />\n    <toRun name=\"Identity: Identity-SelfHost\" type=\"LaunchSettings\" />\n    <toRun name=\"Notifications: Notifications-SelfHost\" type=\"LaunchSettings\" />\n    <toRun name=\"Sso: Sso-SelfHost\" type=\"LaunchSettings\" />\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": ".run/Full Server.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"Full Server\" type=\"CompoundRunConfigurationType\">\n    <toRun name=\"Admin\" type=\"LaunchSettings\" />\n    <toRun name=\"Api\" type=\"LaunchSettings\" />\n    <toRun name=\"Billing\" type=\"LaunchSettings\" />\n    <toRun name=\"Events\" type=\"LaunchSettings\" />\n    <toRun name=\"EventsProcessor\" type=\"LaunchSettings\" />\n    <toRun name=\"Icons\" type=\"LaunchSettings\" />\n    <toRun name=\"Identity\" type=\"LaunchSettings\" />\n    <toRun name=\"Notifications\" type=\"LaunchSettings\" />\n    <toRun name=\"Sso\" type=\"LaunchSettings\" />\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": ".run/Min Server - Self-hosted.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"Min Server - Self-hosted\" type=\"CompoundRunConfigurationType\">\n    <toRun name=\"Api: Api-SelfHost\" type=\"LaunchSettings\" />\n    <toRun name=\"Identity: Identity-SelfHost\" type=\"LaunchSettings\" />\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": ".run/Min Server.run.xml",
    "content": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"Min Server\" type=\"CompoundRunConfigurationType\">\n    <toRun name=\"Api\" type=\"LaunchSettings\" />\n    <toRun name=\"Identity\" type=\"LaunchSettings\" />\n    <method v=\"2\" />\n  </configuration>\n</component>"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to Contribute\n\nOur [Contributing Guidelines](https://contributing.bitwarden.com/contributing/) are located in our [Contributing Documentation](https://contributing.bitwarden.com/). The documentation also includes recommended tooling, code style tips, and lots of other great information to get you started.\n"
  },
  {
    "path": "Directory.Build.props",
    "content": "<Project>\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n\n    <Version>2026.3.1</Version>\n\n    <RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <IsTestProject Condition=\"'$(IsTestProject)' == '' and ($(MSBuildProjectName.EndsWith('.Test')) or $(MSBuildProjectName.EndsWith('.IntegrationTest')))\">true</IsTestProject>\n    <Nullable Condition=\"'$(Nullable)' == '' and '$(IsTestProject)' == 'true'\">annotations</Nullable>\n    <Nullable Condition=\"'$(Nullable)' == '' and '$(IsTestProject)' != 'true'\">enable</Nullable>\n    <TreatWarningsAsErrors Condition=\"'$(TreatWarningsAsErrors)' == ''\">true</TreatWarningsAsErrors>\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <BitIncludeAuthentication>false</BitIncludeAuthentication>\n    <BitIncludeFeatures>false</BitIncludeFeatures>\n  </PropertyGroup>\n\n  <PropertyGroup>\n\n    <MicrosoftNetTestSdkVersion>18.0.1</MicrosoftNetTestSdkVersion>\n\n    <XUnitVersion>2.6.6</XUnitVersion>\n\n    <XUnitRunnerVisualStudioVersion>2.5.6</XUnitRunnerVisualStudioVersion>\n\n    <CoverletCollectorVersion>6.0.0</CoverletCollectorVersion>\n\n    <NSubstituteVersion>5.1.0</NSubstituteVersion>\n\n    <AutoFixtureXUnit2Version>4.18.1</AutoFixtureXUnit2Version>\n\n    <AutoFixtureAutoNSubstituteVersion>4.18.1</AutoFixtureAutoNSubstituteVersion>\n  </PropertyGroup>\n</Project>"
  },
  {
    "path": "LICENSE.txt",
    "content": "Source code in this repository is covered by one of two licenses: (i) the GNU\nAffero General Public License (AGPL) v3.0 (ii) the Bitwarden License v1.0. The\ndefault license throughout the repository is AGPL v3.0 unless the header\nspecifies another license. Bitwarden Licensed code is found only in the\n/bitwarden_license directory.\n\nAGPL v3.0:\nhttps://github.com/bitwarden/server/blob/main/LICENSE_AGPL.txt\n\nBitwarden License v1.0:\nhttps://github.com/bitwarden/server/blob/main/LICENSE_BITWARDEN.txt\n\nNo grant of any rights in the trademarks, service marks, or logos of Bitwarden is\nmade (except as may be necessary to comply with the notice requirements as\napplicable), and use of any Bitwarden trademarks must comply with Bitwarden\nTrademark Guidelines \n<https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md>.\n"
  },
  {
    "path": "LICENSE_AGPL.txt",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<http://www.gnu.org/licenses/>."
  },
  {
    "path": "LICENSE_BITWARDEN.txt",
    "content": "BITWARDEN LICENSE AGREEMENT\nVersion 1, 4 September 2020\n\nPLEASE CAREFULLY READ THIS BITWARDEN LICENSE AGREEMENT (\"AGREEMENT\"). THIS\nAGREEMENT CONSTITUTES A LEGALLY BINDING AGREEMENT BETWEEN YOU AND BITWARDEN,\nINC. (\"BITWARDEN\") AND GOVERNS YOUR USE OF THE COMMERCIAL MODULES (DEFINED\nBELOW). BY COPYING OR USING THE COMMERCIAL MODULES, YOU AGREE TO THIS AGREEMENT.\nIF YOU DO NOT AGREE WITH THIS AGREEMENT, YOU MAY NOT COPY OR USE THE COMMERCIAL\nMODULES. IF YOU ARE COPYING OR USING THE COMMERCIAL MODULES ON BEHALF OF A LEGAL\nENTITY, YOU REPRESENT AND WARRANT THAT YOU HAVE AUTHORITY TO AGREE TO THIS\nAGREEMENT ON BEHALF OF SUCH ENTITY. IF YOU DO NOT HAVE SUCH AUTHORITY, DO NOT\nCOPY OR USE THE COMMERCIAL MODULES IN ANY MANNER.\n\nThis Agreement is entered into by and between Bitwarden and you, or the legal\nentity on behalf of whom you are acting (as applicable, \"You\" or \"Your\").\n\n1. DEFINITIONS\n\n\"Bitwarden Software\" means the Bitwarden server software, libraries, and\nCommercial Modules.\n\n\"Commercial Modules\" means the modules designed to work with and enhance the\nBitwarden Software to which this Agreement is linked, referenced, or appended.\n\n2. LICENSES, RESTRICTIONS AND THIRD PARTY CODE\n\n2.1   Commercial Module License. Subject to Your compliance with this Agreement,\nBitwarden hereby grants to You a limited, non-exclusive, non-transferable,\nroyalty-free license to use the Commercial Modules for the sole purposes of\ninternal development and internal testing, and only in a non-production\nenvironment.\n\n2.2   Reservation of Rights. As between Bitwarden and You, Bitwarden owns all\nright, title and interest in and to the Bitwarden Software, and except as\nexpressly set forth in Sections 2.1, no other license to the Bitwarden Software\nis granted to You under this Agreement, by implication, estoppel, or otherwise.\n\n2.3   Restrictions. You agree not to: (i) except as expressly permitted in\nSection 2.1, sell, rent, lease, distribute, sublicense, loan or otherwise\ntransfer the Commercial Modules to any third party; (ii) alter or remove any\ntrademarks, service mark, and logo included with the Commercial Modules, or\n(iii) use the Commercial Modules to create a competing product or service.\nBitwarden is not obligated to provide maintenance and support services for the\nBitwarden Software licensed under this Agreement.\n\n2.4   Third Party Software. The Commercial Modules may contain or be provided\nwith third party open source libraries, components, utilities and other open\nsource software (collectively, \"Open Source Software\"). Notwithstanding anything\nto the contrary herein, use of the Open Source Software will be subject to the\nlicense terms and conditions applicable to such Open Source Software. To the\nextent any condition of this Agreement conflicts with any license to the Open\nSource Software, the Open Source Software license will govern with respect to\nsuch Open Source Software only.\n\n2.5 This Agreement does not grant any rights in the trademarks, service marks, or\nlogos of any Contributor (except as may be necessary to comply with the notice\nrequirements in Section 2.3), and use of any Bitwarden trademarks must comply with\nBitwarden Trademark Guidelines\n<https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md>.\n\n3. TERMINATION\n\n3.1   Termination. This Agreement will automatically terminate upon notice from\nBitwarden, which notice may be by email or posting in the location where the\nCommercial Modules are made available.\n\n3.2   Effect of Termination. Upon any termination of this Agreement, for any\nreason, You will promptly cease use of the Commercial Modules and destroy any\ncopies thereof. For the avoidance of doubt, termination of this Agreement will\nnot affect Your right to Bitwarden Software, other than the Commercial Modules,\nmade available pursuant to an Open Source Software license.\n\n3.3   Survival. Sections 1, 2.2 -2.4, 3.2, 3.3, 4, and 5 will survive any\ntermination of this Agreement.\n\n4. DISCLAIMER AND LIMITATION OF LIABILITY\n\n4.1   Disclaimer of Warranties. TO THE MAXIMUM EXTENT PERMITTED UNDER APPLICABLE\nLAW, THE BITWARDEN SOFTWARE IS PROVIDED \"AS IS\" WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED REGARDING OR RELATING TO THE BITWARDEN SOFTWARE, INCLUDING\nANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,\nTITLE, AND NON-INFRINGEMENT. FURTHER, BITWARDEN DOES NOT WARRANT RESULTS OF USE\nOR THAT THE BITWARDEN SOFTWARE WILL BE ERROR FREE OR THAT THE USE OF THE\nBITWARDEN SOFTWARE WILL BE UNINTERRUPTED.\n\n4.2   Limitation of Liability. IN NO EVENT WILL BITWARDEN OR ITS LICENSORS BE\nLIABLE TO YOU OR ANY THIRD PARTY UNDER THIS AGREEMENT FOR (I) ANY AMOUNTS IN\nEXCESS OF US $25 OR (II) FOR ANY SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES OF\nANY KIND, INCLUDING FOR ANY LOSS OF PROFITS, LOSS OF USE, BUSINESS INTERRUPTION,\nLOSS OF DATA, COST OF SUBSTITUTE GOODS OR SERVICES, WHETHER ALLEGED AS A BREACH\nOF CONTRACT OR TORTIOUS CONDUCT, INCLUDING NEGLIGENCE, EVEN IF BITWARDEN HAS\nBEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\n5. MISCELLANEOUS\n\n5.1   Assignment. You may not assign or otherwise transfer this Agreement or any\nrights or obligations hereunder, in whole or in part, whether by operation of\nlaw or otherwise, to any third party without Bitwarden's prior written consent.\nAny purported transfer, assignment or delegation without such prior written\nconsent will be null and void and of no force or effect. Bitwarden may assign\nthis Agreement to any successor to its business or assets to which this\nAgreement relates, whether by merger, sale of assets, sale of stock,\nreorganization or otherwise. Subject to this Section 5.1, this Agreement will be\nbinding upon and inure to the benefit of the parties hereto, and their\nrespective successors and permitted assigns.\n\n5.2   Entire Agreement; Modification; Waiver. This Agreement represents the\nentire agreement between the parties, and supersedes all prior agreements and\nunderstandings, written or oral, with respect to the matters covered by this\nAgreement, and is not intended to confer upon any third party any rights or\nremedies hereunder. You acknowledge that You have not entered in this Agreement\nbased on any representations other than those contained herein. No modification\nof or amendment to this Agreement, nor any waiver of any rights under this\nAgreement, will be effective unless in writing and signed by both parties. The\nwaiver of one breach or default or any delay in exercising any rights will not\nconstitute a waiver of any subsequent breach or default.\n\n5.3   Governing Law. This Agreement will in all respects be governed by the laws\nof the State of California without reference to its principles of conflicts of\nlaws. The parties hereby agree that all disputes arising out of this Agreement\nwill be subject to the exclusive jurisdiction of and venue in the federal and\nstate courts within Los Angeles County, California. You hereby consent to the\npersonal and exclusive jurisdiction and venue of these courts. The parties\nhereby disclaim and exclude the application hereto of the United Nations\nConvention on Contracts for the International Sale of Goods.\n\n5.4   Severability. If any provision of this Agreement is held invalid or\nunenforceable under applicable law by a court of competent jurisdiction, it will\nbe replaced with the valid provision that most closely reflects the intent of\nthe parties and the remaining provisions of the Agreement will remain in full\nforce and effect.\n\n5.5   Relationship of the Parties. Nothing in this Agreement is to be construed\nas creating an agency, partnership, or joint venture relationship between the\nparties hereto. Neither party will have any right or authority to assume or\ncreate any obligations or to make any representations or warranties on behalf of\nany other party, whether express or implied, or to bind the other party in any\nrespect whatsoever.\n\n5.6   Notices. All notices permitted or required under this Agreement will be in\nwriting and will be deemed to have been given when delivered in person\n(including by overnight courier), or three (3) business days after being mailed\nby first class, registered or certified mail, postage prepaid, to the address of\nthe party specified in this Agreement or such other address as either party may\nspecify in writing.\n\n5.7   U.S. Government Restricted Rights. If Commercial Modules is being licensed\nby the U.S. Government, the Commercial Modules is deemed to be \"commercial\ncomputer software\" and \"commercial computer documentation\" developed exclusively\nat private expense, and (a) if acquired by or on behalf of a civilian agency,\nwill be subject solely to the terms of this computer software license as\nspecified in 48 C.F.R. 12.212 of the Federal Acquisition Regulations and its\nsuccessors; and (b) if acquired by or on behalf of units of the Department of\nDefense (\"DOD\") will be subject to the terms of this commercial computer\nsoftware license as specified in 48 C.F.R. 227.7202-2, DOD FAR Supplement and\nits successors.\n\n5.8   Injunctive Relief. A breach or threatened breach by You of Section 2 may\ncause irreparable harm for which damages at law may not provide adequate relief,\nand therefore Bitwarden will be entitled to seek injunctive relief in any\napplicable jurisdiction without being required to post a bond.\n\n5.9   Export Law Assurances. You understand that the Commercial Modules is\nsubject to export control laws and regulations. You may not download or\notherwise export or re-export the Commercial Modules or any underlying\ninformation or technology except in full compliance with all applicable laws and\nregulations, in particular, but without limitation, United States export control\nlaws. None of the Commercial Modules or any underlying information or technology\nmay be downloaded or otherwise exported or re- exported: (a) into (or to a\nnational or resident of) any country to which the United States has embargoed\ngoods; or (b) to anyone on the U.S. Treasury Department's list of specially\ndesignated nationals or the U.S. Commerce Department's list of prohibited\ncountries or debarred or denied persons or entities. You hereby agree to the\nforegoing and represents and warrants that You are not located in, under control\nof, or a national or resident of any such country or on any such list.\n\n5.10  Construction. The titles and section headings used in this Agreement are\nfor ease of reference only and will not be used in the interpretation or\nconstruction of this Agreement. No rule of construction resolving any ambiguity\nin favor of the non-drafting party will be applied hereto. The word \"including\",\nwhen used herein, is illustrative rather than exclusive and means \"including,\nwithout limitation.\"\n"
  },
  {
    "path": "LICENSE_FAQ.md",
    "content": "# Bitwarden and Open Source\n\nThe source code for all Bitwarden software products is hosted on GitHub and we welcome everyone to review, audit, and contribute to the Bitwarden codebase.\n\nWe believe that making our source code open and available is a defining feature of Bitwarden, and that source code transparency offers critically important customer benefits for security solutions like Bitwarden.\n\nAs an open solution, Bitwarden publishes the source code for various modules under different licenses.  We're providing this License Statement and FAQ document as an overview of our licensing philosophy, the specifics of module licensing, and to answer common questions regarding our licenses.\n\n# Bitwarden Software Licensing\n\nWe have two tiers of licensing for our software. The core products are offered under one of the GPL open source licenses: GPL 3 and  A-GPL 3. A select number of features, primarily those designed for use by larger organizations rather than individuals and families, are licensed under a \"Source Available\" commercial license [here](https://github.com/bitwarden/server/blob/main/LICENSE_BITWARDEN.txt).\n\nOur current software products have the following licenses:\n\n*Bitwarden clients:* The core password management code for individual password vaults, including Desktop, Web, Browser, Mobile, and CLI versions, is available under the GPL 3.0 license.\n\n*Bitwarden server:* The main Bitwarden server code is licensed under the AGPL 3.0 license.\n\n*Commercial.Core and SSO integration:* Code for certain new modules that are designed and developed for use by larger\norganizations and enterprise environments is released under the Bitwarden License, a \"source available\" license. The\nBitwarden License provides users access to product source code for non-production purposes such as development and\ntesting, but requires a paid subscription for production use of the product, and environments supporting production.\nAdditionally the Api module by default includes Commercial.Core which is under the Bitwarden License, however this can\nbe disabled by using `/p:DefineConstants=\"OSS\"` as an argument to `dotnet` while building the module.\n\n# Frequently Asked Questions\n\n***How can I contribute to Bitwarden open source projects?***\n\nWe welcome new members of our developer community and there are many ways for you to contribute to our projects. For more information visit our [Community Resources](https://community.bitwarden.com/), specifically our Forum on GitHub Contributions.\n\n***In your GitHub repositories, how can I determine what license applies to a given software program?***\n\nEach Bitwarden repository contains a `LICENSE.txt` file that spells out which license applies to the code in that repository.\n\nIn the case of the [Bitwarden server repository](https://github.com/bitwarden/server), the files are organized into various directories. These directories are not only used for logical code organization, but also to clearly distinguish the license that a given source file falls under. All source files under the `/bitwarden_license` directory at the root of the server repository are subject to the Bitwarden License. If a file is not organized under the `/bitwarden_license` directory, the AGPL 3.0 license applies.\n\n***Can I offer a managed service based on Bitwarden products?***\n\nAny individual or organization considering offering Bitwarden \"as a service\" must be mindful of the strong \"copyleft\" attributes of our open source licenses, as well as the Bitwarden License. With respect to the server software available under the Bitwarden License, production use requires a separate commercial agreement with Bitwarden. With respect to the server software available under the AGPL license, as software professionals we cannot conceive a scenario in which the offering of Bitwarden \"as-a-service\" would not involve a modification to the applicable Bitwarden code, thereby triggering the strong copyleft provisions of the AGPL 3.0 license. We encourage anyone considering offering Bitwarden as a service to join the Bitwarden Partner Program for access to the comprehensive resources and support we make available to our authorized solutions partners. Please [contact us](https://bitwarden.com/contact/) for information.\n\n***What rights do I receive under the \"Source Available\" Bitwarden License?*** \n\nUsers of software licensed under the Bitwarden License receive a right to use the software source code for non-production purposes of internal development and internal testing. The right to use the software in a production environment, or environments directly supporting production, requires a paid Bitwarden subscription. This approach is modeled on the licensing approaches taken by other successful open source companies including Elastic (NYSE: ESTC) and Confluent (NASDAQ: CFLT).\n\n***Is Bitwarden open source?***\n\nAs detailed above, the Bitwarden password management clients for individual use, the main Bitwarden server, and many libraries are available under the GPL family of licenses. The GPL licenses are widely used open source licenses created by the Free Software Foundation and endorsed as \"open source\" by the [Open Source Initiative](https://opensource.org/history). The Bitwarden License does not qualify as an open source license under the OSI definition, but we believe that the license successfully balances the principles of openness and community with our business goals.\n\n***If I redistribute or provide services related to Bitwarden open source software can I use the \"Bitwarden\" name?***\n\nOur licenses do not grant any rights in the trademarks, service marks, or logos of Bitwarden (except as may be necessary to comply with the notice requirements as applicable). The Bitwarden trademark is a trusted mark applied to products distributed by Bitwarden, Inc., owner of the Bitwarden trademarks and products. We have adopted and enforce strict rules governing use of our trademarks. Use of any Bitwarden trademarks must comply with Bitwarden [Trademark Guidelines](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md).\n\n***Bitwarden Trademark Usage***\n\nBecause Open Source plays a major part in how we build our products, we see it as a matter of course to give the same effort back to our community by creating valuable, free and easy-to-use software. We need to make sure our trademarks remain distinctive so you know what you're getting and from who.\n\n***Do I need permission to use the Bitwarden Trademarks?***\n\nYou don't need permission to use our marks when truthfully referring to our products, services or features, or to explain that your products or services are based on our open- source code so long as not misleading. Any other use requires our permission.\n\n***How should I use the Bitwarden Trademarks when allowed?***\n\nUse the Bitwarden Trademarks exactly as [shown](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md) and without modification. For example, do not abbreviate, hyphenate, or remove elements and separate them from surrounding text, images and other features. Always use the Bitwarden Trademarks as adjectives followed by a generic term, never as a noun or verb.\n\nUse the Bitwarden Trademarks only to reference one of our products or services, but never in a way that implies sponsorship or affiliation by Bitwarden. For example, do not use any part of the Bitwarden Trademarks as the name of your business, product or service name, application, domain name, publication or other offering – this can be confusing to others.\n\n***Where can I find more information?***\n\nFor more information on how to use the Bitwarden Trademarks, including in connection with self-hosted options and open-source code, see our [Trademark Guidelines](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md) or [contacts us](https://bitwarden.com/contact/).\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"https://github.com/bitwarden/brand/blob/main/screenshots/apps-combo-logo.png\" alt=\"Bitwarden\" />\n</p>\n<p align=\"center\">\n  <a href=\"https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:main\" target=\"_blank\">\n    <img src=\"https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=main\" alt=\"Github Workflow build on main\" />\n  </a>\n</p>\n\n---\n\nThe Bitwarden Server project contains the APIs, database, and other core infrastructure items needed for the \"backend\" of all bitwarden client applications.\n\nThe server project is written in C# using .NET Core with ASP.NET Core. The database is written in T-SQL/SQL Server. The codebase can be developed, built, run, and deployed cross-platform on Windows, macOS, and Linux distributions.\n\n## Developer Documentation\n\nPlease refer to the [Server Setup Guide](https://contributing.bitwarden.com/getting-started/server/guide) in the [Contributing Documentation](https://contributing.bitwarden.com/) for build instructions, recommended tooling, code style tips, and lots of other great information to get you started.\n\n## Deploy\n\n<p align=\"center\">\n  <a href=\"https://github.com/orgs/bitwarden/packages\" target=\"_blank\">\n    <img src=\"https://i.imgur.com/SZc8JnH.png\" alt=\"docker\" />\n  </a>\n</p>\n\nYou can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [GitHub Container Registry](https://github.com/orgs/bitwarden/packages).\n\nFull documentation for deploying Bitwarden with Docker can be found in our help center at: https://help.bitwarden.com/article/install-on-premise/\n\n### Requirements\n\n- [Docker](https://www.docker.com/community-edition#/download)\n- [Docker Compose](https://docs.docker.com/compose/install/) (already included with some Docker installations)\n\n_These dependencies are free to use._\n\n### Linux & macOS\n\n```sh\ncurl -s -L -o bitwarden.sh \\\n    \"https://func.bitwarden.com/api/dl/?app=self-host&platform=linux\" \\\n    && chmod +x bitwarden.sh\n./bitwarden.sh install\n./bitwarden.sh start\n```\n\n### Windows\n\n```cmd\nInvoke-RestMethod -OutFile bitwarden.ps1 `\n    -Uri \"https://func.bitwarden.com/api/dl/?app=self-host&platform=windows\"\n.\\bitwarden.ps1 -install\n.\\bitwarden.ps1 -start\n```\n\n## Production Container Images\n\n<details>\n<summary><b>View Current Production Image Hashes</b> (click to expand)</summary>\n<br>\n\n### US Production Cluster\n\n| Service | Image Hash |\n|---------|------------|\n| **Admin** | ![admin](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.admin&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **API** | ![api](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.api&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **Billing** | ![billing](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.billing&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **Events** | ![events](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.events&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **EventsProcessor** | ![eventsprocessor](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.eventsprocessor&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **Identity** | ![identity](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.identity&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **Notifications** | ![notifications](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.notifications&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **SCIM** | ![scim](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.scim&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **SSO** | ![sso](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-us.json&query=%24.sso&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n\n### EU Production Cluster\n\n| Service | Image Hash |\n|---------|------------|\n| **Admin** | ![admin](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.admin&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **API** | ![api](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.api&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **Billing** | ![billing](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.billing&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **Events** | ![events](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.events&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **EventsProcessor** | ![eventsprocessor](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.eventsprocessor&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **Identity** | ![identity](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.identity&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **Notifications** | ![notifications](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.notifications&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **SCIM** | ![scim](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.scim&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n| **SSO** | ![sso](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fbitwarden%2Fserver%2Frefs%2Fheads%2Fmetadata%2Fbadges%2Fshieldsio-badge-eu.json&query=%24.sso&style=flat-square&logo=docker&logoColor=white&label=&color=2496ED) |\n\n</details>\n\n## We're Hiring!\n\nInterested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.\n\n## Contribute\n\nCode contributions are welcome! Please commit any pull requests against the `main` branch. Learn more about how to contribute by reading the [Contributing Guidelines](https://contributing.bitwarden.com/contributing/). Check out the [Contributing Documentation](https://contributing.bitwarden.com/) for how to get started with your first contribution.\n\nSecurity audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file. We also run a program on [HackerOne](https://hackerone.com/bitwarden).\n\nNo grant of any rights in the trademarks, service marks, or logos of Bitwarden is made (except as may be necessary to comply with the notice requirements as applicable), and use of any Bitwarden trademarks must comply with [Bitwarden Trademark Guidelines](https://github.com/bitwarden/server/blob/main/TRADEMARK_GUIDELINES.md).\n\n### Dotnet-format\n\nConsider installing our git pre-commit hook for automatic formatting.\n\n```bash\ngit config --local core.hooksPath .git-hooks\n```\n"
  },
  {
    "path": "SECURITY.md",
    "content": "Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!\n\n# Disclosure Policy\n\n- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.\n- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.\n- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.\n- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).\n\nWhile researching, we'd like to ask you to refrain from:\n\n- Denial of service\n- Spamming\n- Social engineering (including phishing) of Bitwarden staff or contractors\n- Any physical attempts against Bitwarden property or data centers\n\n# We want to help you!\n\nIf you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.\n\nThank you for helping keep Bitwarden and our users safe!\n"
  },
  {
    "path": "TRADEMARK_GUIDELINES.md",
    "content": "# TRADEMARK GUIDELINES\n\nThis document outlines the policy for allowable uses of trademarks owned by Bitwarden, Inc.\n(“Bitwarden\") by other parties.\n\nBitwarden owns all Bitwarden-related trademarks, service marks, and logos and the names of\nall Bitwarden® projects are trademarks of Bitwarden.\n\nThe role of trademarks is to provide assurance about the source and quality of the products or\nservices with which the trademark is associated. We want to allow members of the Bitwarden\ncommunity to discuss our products and services and to accurately describe their affiliation with\nBitwarden. Bitwarden has established this Policy to encourage others to make accurate,\nnon-confusing use of the Bitwarden trademarks, while also ensuring that those trademarks\nmaintain their distinctiveness and strength as reliable indicators of the source and quality of\nBitwarden products and services. Although some Bitwarden projects may be available under\nfree and open licenses, those licenses cover copyright only and do not include any express or\nimplied right to use our trademarks. Bitwarden does not allow third parties to use its\ntrademarks without a written agreement or express permission. Thus, Bitwarden projects that\nare available under open source licenses may be copied, modified, distributed or sold by third\nparties in accordance with the terms of the applicable open source license, but they cannot be\nbranded or marketed with Bitwarden trademarks in the absence of a trademark license.\n\nWhile open-source licenses allow modification of copyrighted software and distribution in\noriginal or modified form, such distribution could be misleading if distributed under the same\nname as the source project. This could cause confusion among consumers of the software as to\nsource. The consumers may mistakenly believe they are receiving software that is produced or\nsupported by Bitwarden. This Policy describes the circumstances under which you may use our\ntrademarks, regardless of the type of license you may have from Bitwarden. In this Policy we\nare not trying to limit the lawful use of our trademarks, but rather describe for you what we\nconsider the parameters of lawful use to be. Trademark law can be ambiguous, so we hope to\nprovide enough clarity for you to understand whether we will consider your use licensed or\nnon-infringing.\n\n## Our Trademarks\n\nThis Policy covers the following non-exhaustive list of our trademarks:\n\n1. Our trademarks and service marks (the \"Marks\"):\n    \n    - Bitwarden®\n    - ![Logo](https://i.imgur.com/FIv4bYq.png)\n\n2. The unique visual styling of our website and elements used in or otherwise related to\n    the products and services we offer (the \"Trade Dress\"). See our Style Guide for further\n    information.\n\nThis Policy encompasses all trademarks and service marks, whether Word Marks, Logos or Trade\nDress, which are collectively referred to as the \"Marks.\" Some Marks may not be registered, but\nregistration is not necessarily required for ownership of trademarks. This Policy covers our\nMarks whether they are registered or not.\n\n## Universal Considerations for All Uses\n\nThe following guidelines show proper (and improper) use of Marks. Any use of the Marks must\nbe licensed and comply with these guidelines. Whenever you use one of the Marks, you must\nalways do so in a way that does not mislead anyone, either directly or by omission, about\nexactly what they are getting and from whom. For example, you cannot say you are distributing\nthe Bitwarden® software when you're distributing a modified version of it, because people\nwould be confused when they are not getting the same features and functionality they would\nget if they downloaded the software directly from us. You also cannot distribute Bitwarden®\nsoftware using the Marks if you do not have a license from us, because that would imply that\nyour distribution comes from or is supported by Bitwarden. You cannot use our Marks on your\nwebsite in a way that suggests that your website is an official website or that we endorse your\nwebsite, unless permitted in a written agreement with us. You can, though, say you like the\nBitwarden® software, say that you participate in the Bitwarden® community, or refer to\nBitwarden® products and services if these statements are true.\n\nThis fundamental requirement, that it is always clear to people what they are getting and from\nwhom, is reflected throughout this Policy. It should also serve as your guide if you are not sure\nabout how you are using the Marks.\n\n## Standards for Use\n\nAll uses of Bitwarden Marks must conform to the following principles:\n\n1. You may use the Marks only to identify and distinguish Bitwarden products and services.\n    Our Marks may not be applied to products or services provided by anyone else.\n\n2. If you are self-hosting the Bitwarden software or using the Bitwarden software\n    on-premise, in each case under a subscription or license agreement with Bitwarden, you\n    may use the Bitwarden Marks to describe or reference the password management\n    platform or use the Bitwarden login page shown below for your users or organization:\n    \n    ![Login page](https://i.imgur.com/J0o51cO.png)\n\n3. You may not use or register, in whole or in part, the Marks as part of your own\n    trademark, service mark, domain name, company name, trade name, product name,\n    service name or social media handle/account.\n\n4. Trademark law does not allow your use of names or trademarks that are too similar to\n    ours. You therefore may not use an obvious variation of any of our Marks or any\n    phonetic equivalent, foreign language equivalent, takeoff, or abbreviation for a similar or\n    compatible product or service. This includes combinations or integrations of all or\n    portions of the Marks in a way that the public may think of the use as a new mark (e.g.\n    SuperBitwarden, or Bitwarden2.0).\n\n5. You may not use the Marks in a way that disparages or harms Bitwarden or our products\n    or services. You also may not use our Marks in connection with harmful or\n    objectionable materials of any sort.\n\n6. We reserve the right to object to any use of our Marks that we view as non-compliant\n    with these Trademark Guidelines or applicable law. By using our brand materials, you\n    agree to take all necessary steps to resolve any objections we have. You also\n    acknowledge that Bitwarden is the sole owner of Bitwarden Marks and goodwill derived\n    from their use accrues only to Bitwarden.\n\n7. Provided that you have obtained a license from Bitwarden, you can use the word mark in\n    books and articles as long as the use does not suggest that we have published, endorse,\n    or agree with your work.\n\nYou agree that you will not acquire any rights in the Marks and that any goodwill generated by\nyour use of the Marks inures solely to our benefit.\n\n## Proper Use of the Marks\n\nAlways use the Word Marks in a manner distinguished from surrounding text, with initial capital\nletters, and in the exact form with the correct spelling (neither abbreviated, hyphenated, or\ncombined with any other word or words).\n\n**Correct**\n\n- Bitwarden®\n\n**Incorrect**\n\n- BITWARDEN\n- BitWarden\n- Bitward\n\n### Use Marks as Proper Adjectives Followed by a Generic Term.\n\nTrademarks should be used as adjectives followed by a generic modifier, and not as nouns\nor verbs. For example:\n\n**Correct**\n\n- The Bitwarden® platform is widely used in many industries.\n- I was able to quickly gain control over my organization's sensitive data with Bitwarden®\n    password management software.\n- I use the Bitwarden® application to manage my passwords.\n\n**Incorrect**\n\n- Bitwarden® is widely used.\n\n### Do Not Use Marks in the Possessive Form.\n\nBecause trademarks are not nouns, they should not be used in the possessive form.\nFor example:\n\n**Correct**\n\n- The Bitwarden® platform safely stores and protects passwords.\n\n**Incorrect**\n\n- Bitwarden's platform safely stores and protects passwords.\n\n### Do Not Use Marks to Suggest Endorsement by Bitwarden.\n\n**Correct**\n\n- Our platform was developed using Bitwarden® open source software\n\n**Incorrect**\n\n- \"Open Bitwarden\"\n- XYZ ENTERPRISE 3.4.9 (FREE AND OPEN UNRESTRICTED BITWARDEN ENTERPRISE FORK)\n\n## Use of Logos\n\nYou may not use our logos unless you have obtained written permission. If you have obtained a\nseparate license to use our logos, you may not change any logo except to scale it proportionally.\nThis means you may not add decorative elements, change the colors, change the proportions,\ndistort it, add elements, or combine it with other logos. The logo may only be used displaying\nthe exact colors shown in our Style Guide.\n\n## Mark Attribution and Notices\n\nThe first or most prominent mention of a Mark on a webpage, document, packaging, or\ndocumentation should be accompanied by a symbol indicating whether the Mark is a registered\ntrademark (\"®\") or an unregistered trademark (\"™\"). Also, if you are using our Marks for uses\nfor which we are granting a separate license, please put the following notice at the foot of the\npage where you have used the Mark (or, if in a book, on the credits page), on any packaging or\nlabeling, and on advertising or marketing materials: \"Bitwarden is a trademark or registered\ntrademark of Bitwarden, Inc. in the United States and/or other countries.\"\n\n## Possible Infringement\n\nIf you are aware of any confusing use or misuse of the Marks in any way, we would appreciate\nyou bringing this to our attention. Please contact us at <support@bitwarden.com> so that we can\ninvestigate it further.\n\n## Updates\n\nBitwarden reserves the right to modify or update this Policy at any time. You should review this\nPolicy from time to time so that you will be aware of any modifications or updates as they will\napply as soon as they are posted on this page.\n\n## Further Information\n\nBitwarden has tried to make this Trademark Policy as comprehensive and understandable as\npossible. If you have any questions about this Policy, please contact us.\n"
  },
  {
    "path": "bitwarden-server.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.14.36705.20 d17.14\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"src - AGPL\", \"src - AGPL\", \"{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"util\", \"util\", \"{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"test\", \"test\", \"{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Solution Items\", \"Solution Items\", \"{458155D3-BCBC-481D-B37A-40D2ED10F0A4}\"\n\tProjectSection(SolutionItems) = preProject\n\t\t.dockerignore = .dockerignore\n\t\t.editorconfig = .editorconfig\n\t\t.gitignore = .gitignore\n\t\tCONTRIBUTING.md = CONTRIBUTING.md\n\t\tDirectory.Build.props = Directory.Build.props\n\t\tglobal.json = global.json\n\t\tLICENSE.txt = LICENSE.txt\n\t\tLICENSE_AGPL.txt = LICENSE_AGPL.txt\n\t\tLICENSE_BITWARDEN.txt = LICENSE_BITWARDEN.txt\n\t\tLICENSE_FAQ.md = LICENSE_FAQ.md\n\t\tREADME.md = README.md\n\t\tSECURITY.md = SECURITY.md\n\t\tTRADEMARK_GUIDELINES.md = TRADEMARK_GUIDELINES.md\n\tEndProjectSection\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Core\", \"src\\Core\\Core.csproj\", \"{3973D21B-A692-4B60-9B70-3631C057423A}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Api\", \"src\\Api\\Api.csproj\", \"{E8548AD6-7FB0-439A-8EB5-549A10336D2D}\"\nEndProject\nProject(\"{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}\") = \"Sql\", \"src\\Sql\\Sql.sqlproj\", \"{58554E52-FDEC-4832-AFF9-302B01E08DCA}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Billing\", \"src\\Billing\\Billing.csproj\", \"{02BC2982-ED8D-4A6D-A41E-092B3DAEB98A}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Identity\", \"src\\Identity\\Identity.csproj\", \"{04148736-3C0B-445E-8B74-2020E7A53502}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Setup\", \"util\\Setup\\Setup.csproj\", \"{EF2164EF-1FC0-4518-A2ED-CE02D3630B00}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Server\", \"util\\Server\\Server.csproj\", \"{66B0A682-658A-4A82-B606-A077A4871448}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Icons\", \"src\\Icons\\Icons.csproj\", \"{9CF59342-3912-4B45-A2BA-0F173666586D}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Events\", \"src\\Events\\Events.csproj\", \"{994DD611-F266-4BD3-8072-3B1B57267ED5}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Admin\", \"src\\Admin\\Admin.csproj\", \"{B131CEF3-89FB-4C90-ADB0-9E9C4246EB56}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Notifications\", \"src\\Notifications\\Notifications.csproj\", \"{28635027-20E5-42FA-B218-B6C878DE5350}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Core.Test\", \"test\\Core.Test\\Core.Test.csproj\", \"{8EF31E6C-400A-4174-8BE3-502B08FB10B5}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"EventsProcessor\", \"src\\EventsProcessor\\EventsProcessor.csproj\", \"{79BB453F-D0D8-4DDF-9809-A405C56692BD}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Migrator\", \"util\\Migrator\\Migrator.csproj\", \"{54DED792-A022-417E-9804-21FCC9C7C610}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Api.Test\", \"test\\Api.Test\\Api.Test.csproj\", \"{860DE301-0B3E-4717-9C21-A9B4C3C2B121}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"src - Bitwarden License\", \"src - Bitwarden License\", \"{4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Sso\", \"bitwarden_license\\src\\Sso\\Sso.csproj\", \"{4866AF64-6640-4C65-A662-A31E02FF9064}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Icons.Test\", \"test\\Icons.Test\\Icons.Test.csproj\", \"{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Commercial.Core\", \"bitwarden_license\\src\\Commercial.Core\\Commercial.Core.csproj\", \"{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}\"\n\tProjectSection(ProjectDependencies) = postProject\n\t\t{3973D21B-A692-4B60-9B70-3631C057423A} = {3973D21B-A692-4B60-9B70-3631C057423A}\n\tEndProjectSection\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Commercial.Core.Test\", \"bitwarden_license\\test\\Commercial.Core.Test\\Commercial.Core.Test.csproj\", \"{0E99A21B-684B-4C59-9831-90F775CAB6F7}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"test - Bitwarden License\", \"test - Bitwarden License\", \"{287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"MySqlMigrations\", \"util\\MySqlMigrations\\MySqlMigrations.csproj\", \"{BDC1D592-5947-47ED-9903-7CDBB12A50C8}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"PostgresMigrations\", \"util\\PostgresMigrations\\PostgresMigrations.csproj\", \"{F72E0229-2EF7-49B3-9004-FF4C0043816E}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Common\", \"test\\Common\\Common.csproj\", \"{17DA09D7-0212-4009-879E-6B9CFDE5FA60}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Infrastructure.Dapper\", \"src\\Infrastructure.Dapper\\Infrastructure.Dapper.csproj\", \"{AD933445-27CE-4D30-A6ED-9065309464AD}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"SharedWeb\", \"src\\SharedWeb\\SharedWeb.csproj\", \"{713D44C0-1BC1-4024-96A3-A98A49F33908}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Infrastructure.EntityFramework\", \"src\\Infrastructure.EntityFramework\\Infrastructure.EntityFramework.csproj\", \"{ED880735-0250-43C7-9662-FDC7C7416E7F}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Billing.Test\", \"test\\Billing.Test\\Billing.Test.csproj\", \"{B8639B10-2157-44BC-8CE1-D9EB4B50971F}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Identity.Test\", \"test\\Identity.Test\\Identity.Test.csproj\", \"{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Identity.IntegrationTest\", \"test\\Identity.IntegrationTest\\Identity.IntegrationTest.csproj\", \"{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"IntegrationTestCommon\", \"test\\IntegrationTestCommon\\IntegrationTestCommon.csproj\", \"{0923DE59-5FB1-44F2-9302-A09D2236B470}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"Scim\", \"bitwarden_license\\src\\Scim\\Scim.csproj\", \"{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"SqlServerEFScaffold\", \"util\\SqlServerEFScaffold\\SqlServerEFScaffold.csproj\", \"{2F2E8BB0-6838-48DA-B581-71B9F13DE364}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Commercial.Infrastructure.EntityFramework\", \"bitwarden_license\\src\\Commercial.Infrastructure.EntityFramework\\Commercial.Infrastructure.EntityFramework.csproj\", \"{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Infrastructure.EFIntegration.Test\", \"test\\Infrastructure.EFIntegration.Test\\Infrastructure.EFIntegration.Test.csproj\", \"{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Api.IntegrationTest\", \"test\\Api.IntegrationTest\\Api.IntegrationTest.csproj\", \"{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Scim.IntegrationTest\", \"bitwarden_license\\test\\Scim.IntegrationTest\\Scim.IntegrationTest.csproj\", \"{FE998849-5FC8-41A2-B7C9-9227901471A0}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"perf\", \"perf\", \"{EC2D422A-6060-48E2-AAD2-37220D759F03}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"MicroBenchmarks\", \"perf\\MicroBenchmarks\\MicroBenchmarks.csproj\", \"{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Scim.Test\", \"bitwarden_license\\test\\Scim.Test\\Scim.Test.csproj\", \"{B1595DA3-4C60-41AA-8BF0-499A5F75A885}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Infrastructure.IntegrationTest\", \"test\\Infrastructure.IntegrationTest\\Infrastructure.IntegrationTest.csproj\", \"{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"SqliteMigrations\", \"util\\SqliteMigrations\\SqliteMigrations.csproj\", \"{07143DFA-F242-47A4-A15E-39C9314D4140}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"MsSqlMigratorUtility\", \"util\\MsSqlMigratorUtility\\MsSqlMigratorUtility.csproj\", \"{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Admin.Test\", \"test\\Admin.Test\\Admin.Test.csproj\", \"{52D22B52-26D3-463A-8EB5-7FDC849D3761}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Events.Test\", \"test\\Events.Test\\Events.Test.csproj\", \"{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"EventsProcessor.Test\", \"test\\EventsProcessor.Test\\EventsProcessor.Test.csproj\", \"{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Notifications.Test\", \"test\\Notifications.Test\\Notifications.Test.csproj\", \"{90D85D8F-5577-4570-A96E-5A2E185F0F6F}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Infrastructure.Dapper.Test\", \"test\\Infrastructure.Dapper.Test\\Infrastructure.Dapper.Test.csproj\", \"{4A725DB3-BE4F-4C23-9087-82D0610D67AF}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Events.IntegrationTest\", \"test\\Events.IntegrationTest\\Events.IntegrationTest.csproj\", \"{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Core.IntegrationTest\", \"test\\Core.IntegrationTest\\Core.IntegrationTest.csproj\", \"{3631BA42-6731-4118-A917-DAA43C5032B9}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Seeder\", \"util\\Seeder\\Seeder.csproj\", \"{9A612EBA-1C0E-42B8-982B-62F0EE81000A}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"SeederUtility\", \"util\\SeederUtility\\SeederUtility.csproj\", \"{17A89266-260A-4A03-81AE-C0468C6EE06E}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"RustSdk\", \"util\\RustSdk\\RustSdk.csproj\", \"{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"SharedWeb.Test\", \"test\\SharedWeb.Test\\SharedWeb.Test.csproj\", \"{AD59537D-5259-4B7A-948F-0CF58E80B359}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"SeederApi\", \"util\\SeederApi\\SeederApi.csproj\", \"{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"SeederApi.IntegrationTest\", \"test\\SeederApi.IntegrationTest\\SeederApi.IntegrationTest.csproj\", \"{A2E067EF-609C-4D13-895A-E054C61D48BB}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"SSO.Test\", \"bitwarden_license\\test\\SSO.Test\\SSO.Test.csproj\", \"{7D98784C-C253-43FB-9873-25B65C6250D6}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Sso.IntegrationTest\", \"bitwarden_license\\test\\Sso.IntegrationTest\\Sso.IntegrationTest.csproj\", \"{FFB09376-595B-6F93-36F0-70CAE90AFECB}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"Server.IntegrationTest\", \"test\\Server.IntegrationTest\\Server.IntegrationTest.csproj\", \"{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{3973D21B-A692-4B60-9B70-3631C057423A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3973D21B-A692-4B60-9B70-3631C057423A}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3973D21B-A692-4B60-9B70-3631C057423A}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3973D21B-A692-4B60-9B70-3631C057423A}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{E8548AD6-7FB0-439A-8EB5-549A10336D2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{E8548AD6-7FB0-439A-8EB5-549A10336D2D}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{E8548AD6-7FB0-439A-8EB5-549A10336D2D}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{E8548AD6-7FB0-439A-8EB5-549A10336D2D}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{58554E52-FDEC-4832-AFF9-302B01E08DCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{58554E52-FDEC-4832-AFF9-302B01E08DCA}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{58554E52-FDEC-4832-AFF9-302B01E08DCA}.Debug|Any CPU.Deploy.0 = Debug|Any CPU\n\t\t{58554E52-FDEC-4832-AFF9-302B01E08DCA}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{58554E52-FDEC-4832-AFF9-302B01E08DCA}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{58554E52-FDEC-4832-AFF9-302B01E08DCA}.Release|Any CPU.Deploy.0 = Release|Any CPU\n\t\t{02BC2982-ED8D-4A6D-A41E-092B3DAEB98A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{02BC2982-ED8D-4A6D-A41E-092B3DAEB98A}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{02BC2982-ED8D-4A6D-A41E-092B3DAEB98A}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{02BC2982-ED8D-4A6D-A41E-092B3DAEB98A}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{04148736-3C0B-445E-8B74-2020E7A53502}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{04148736-3C0B-445E-8B74-2020E7A53502}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{04148736-3C0B-445E-8B74-2020E7A53502}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{04148736-3C0B-445E-8B74-2020E7A53502}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{EF2164EF-1FC0-4518-A2ED-CE02D3630B00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{EF2164EF-1FC0-4518-A2ED-CE02D3630B00}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{EF2164EF-1FC0-4518-A2ED-CE02D3630B00}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{EF2164EF-1FC0-4518-A2ED-CE02D3630B00}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{66B0A682-658A-4A82-B606-A077A4871448}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{66B0A682-658A-4A82-B606-A077A4871448}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{66B0A682-658A-4A82-B606-A077A4871448}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{66B0A682-658A-4A82-B606-A077A4871448}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{9CF59342-3912-4B45-A2BA-0F173666586D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{9CF59342-3912-4B45-A2BA-0F173666586D}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{9CF59342-3912-4B45-A2BA-0F173666586D}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{9CF59342-3912-4B45-A2BA-0F173666586D}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{994DD611-F266-4BD3-8072-3B1B57267ED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{994DD611-F266-4BD3-8072-3B1B57267ED5}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{994DD611-F266-4BD3-8072-3B1B57267ED5}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{994DD611-F266-4BD3-8072-3B1B57267ED5}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{B131CEF3-89FB-4C90-ADB0-9E9C4246EB56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B131CEF3-89FB-4C90-ADB0-9E9C4246EB56}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B131CEF3-89FB-4C90-ADB0-9E9C4246EB56}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B131CEF3-89FB-4C90-ADB0-9E9C4246EB56}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{28635027-20E5-42FA-B218-B6C878DE5350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{28635027-20E5-42FA-B218-B6C878DE5350}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{28635027-20E5-42FA-B218-B6C878DE5350}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{28635027-20E5-42FA-B218-B6C878DE5350}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{8EF31E6C-400A-4174-8BE3-502B08FB10B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{8EF31E6C-400A-4174-8BE3-502B08FB10B5}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{8EF31E6C-400A-4174-8BE3-502B08FB10B5}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{8EF31E6C-400A-4174-8BE3-502B08FB10B5}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{79BB453F-D0D8-4DDF-9809-A405C56692BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{79BB453F-D0D8-4DDF-9809-A405C56692BD}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{79BB453F-D0D8-4DDF-9809-A405C56692BD}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{79BB453F-D0D8-4DDF-9809-A405C56692BD}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{54DED792-A022-417E-9804-21FCC9C7C610}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{54DED792-A022-417E-9804-21FCC9C7C610}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{54DED792-A022-417E-9804-21FCC9C7C610}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{54DED792-A022-417E-9804-21FCC9C7C610}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{4866AF64-6640-4C65-A662-A31E02FF9064}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{4866AF64-6640-4C65-A662-A31E02FF9064}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{4866AF64-6640-4C65-A662-A31E02FF9064}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{4866AF64-6640-4C65-A662-A31E02FF9064}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{BDC1D592-5947-47ED-9903-7CDBB12A50C8}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{AD933445-27CE-4D30-A6ED-9065309464AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD933445-27CE-4D30-A6ED-9065309464AD}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AD933445-27CE-4D30-A6ED-9065309464AD}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD933445-27CE-4D30-A6ED-9065309464AD}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{713D44C0-1BC1-4024-96A3-A98A49F33908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{713D44C0-1BC1-4024-96A3-A98A49F33908}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{713D44C0-1BC1-4024-96A3-A98A49F33908}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{713D44C0-1BC1-4024-96A3-A98A49F33908}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{ED880735-0250-43C7-9662-FDC7C7416E7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{ED880735-0250-43C7-9662-FDC7C7416E7F}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{ED880735-0250-43C7-9662-FDC7C7416E7F}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{ED880735-0250-43C7-9662-FDC7C7416E7F}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{0D3B2BD2-53F3-421D-AD8F-C19B954C796B}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{0923DE59-5FB1-44F2-9302-A09D2236B470}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{0923DE59-5FB1-44F2-9302-A09D2236B470}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{0923DE59-5FB1-44F2-9302-A09D2236B470}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{0923DE59-5FB1-44F2-9302-A09D2236B470}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{2F2E8BB0-6838-48DA-B581-71B9F13DE364}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{07143DFA-F242-47A4-A15E-39C9314D4140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{07143DFA-F242-47A4-A15E-39C9314D4140}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{07143DFA-F242-47A4-A15E-39C9314D4140}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{07143DFA-F242-47A4-A15E-39C9314D4140}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{52D22B52-26D3-463A-8EB5-7FDC849D3761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{52D22B52-26D3-463A-8EB5-7FDC849D3761}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{52D22B52-26D3-463A-8EB5-7FDC849D3761}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{52D22B52-26D3-463A-8EB5-7FDC849D3761}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{A2E067EF-609C-4D13-895A-E054C61D48BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{A2E067EF-609C-4D13-895A-E054C61D48BB}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{A2E067EF-609C-4D13-895A-E054C61D48BB}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{A2E067EF-609C-4D13-895A-E054C61D48BB}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{7D98784C-C253-43FB-9873-25B65C6250D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{7D98784C-C253-43FB-9873-25B65C6250D6}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{7D98784C-C253-43FB-9873-25B65C6250D6}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{7D98784C-C253-43FB-9873-25B65C6250D6}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{FFB09376-595B-6F93-36F0-70CAE90AFECB}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(NestedProjects) = preSolution\n\t\t{3973D21B-A692-4B60-9B70-3631C057423A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{E8548AD6-7FB0-439A-8EB5-549A10336D2D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{58554E52-FDEC-4832-AFF9-302B01E08DCA} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{02BC2982-ED8D-4A6D-A41E-092B3DAEB98A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{04148736-3C0B-445E-8B74-2020E7A53502} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{EF2164EF-1FC0-4518-A2ED-CE02D3630B00} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{66B0A682-658A-4A82-B606-A077A4871448} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{9CF59342-3912-4B45-A2BA-0F173666586D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{994DD611-F266-4BD3-8072-3B1B57267ED5} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{B131CEF3-89FB-4C90-ADB0-9E9C4246EB56} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{28635027-20E5-42FA-B218-B6C878DE5350} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{8EF31E6C-400A-4174-8BE3-502B08FB10B5} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{79BB453F-D0D8-4DDF-9809-A405C56692BD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{54DED792-A022-417E-9804-21FCC9C7C610} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{860DE301-0B3E-4717-9C21-A9B4C3C2B121} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{4866AF64-6640-4C65-A662-A31E02FF9064} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}\n\t\t{C7BA2255-C1B1-4789-8BB9-C27540DA6FB8} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{EDC0D688-D58C-4CE1-AA07-3606AC6874B8} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}\n\t\t{0E99A21B-684B-4C59-9831-90F775CAB6F7} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}\n\t\t{BDC1D592-5947-47ED-9903-7CDBB12A50C8} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{F72E0229-2EF7-49B3-9004-FF4C0043816E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{17DA09D7-0212-4009-879E-6B9CFDE5FA60} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{AD933445-27CE-4D30-A6ED-9065309464AD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{713D44C0-1BC1-4024-96A3-A98A49F33908} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{ED880735-0250-43C7-9662-FDC7C7416E7F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}\n\t\t{B8639B10-2157-44BC-8CE1-D9EB4B50971F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{0D3B2BD2-53F3-421D-AD8F-C19B954C796B} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{0923DE59-5FB1-44F2-9302-A09D2236B470} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}\n\t\t{2F2E8BB0-6838-48DA-B581-71B9F13DE364} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{5AB3BBFB-9D98-4EF8-BFCD-462D50A16EB1} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}\n\t\t{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{FE998849-5FC8-41A2-B7C9-9227901471A0} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}\n\t\t{9C8F8255-5F74-4085-AB9C-9075CF6DDC61} = {EC2D422A-6060-48E2-AAD2-37220D759F03}\n\t\t{B1595DA3-4C60-41AA-8BF0-499A5F75A885} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}\n\t\t{7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{07143DFA-F242-47A4-A15E-39C9314D4140} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{52D22B52-26D3-463A-8EB5-7FDC849D3761} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{D1513D90-E4F5-44A9-9121-5E46E3E4A3F7} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}\n\t\t{A2E067EF-609C-4D13-895A-E054C61D48BB} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\t\t{7D98784C-C253-43FB-9873-25B65C6250D6} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}\n\t\t{FFB09376-595B-6F93-36F0-70CAE90AFECB} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}\n\t\t{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "bitwarden_license/README.md",
    "content": "# Bitwarden Licensed Code\n\nAll source code under this directory is licensed under the [Bitwarden License Agreement](https://github.com/bitwarden/server/blob/main/LICENSE_BITWARDEN.txt).\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Providers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Commercial.Core.AdminConsole.Providers;\n\npublic class CreateProviderCommand : ICreateProviderCommand\n{\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IProviderService _providerService;\n    private readonly IUserRepository _userRepository;\n    private readonly IProviderPlanRepository _providerPlanRepository;\n\n    public CreateProviderCommand(\n        IProviderRepository providerRepository,\n        IProviderUserRepository providerUserRepository,\n        IProviderService providerService,\n        IUserRepository userRepository,\n        IProviderPlanRepository providerPlanRepository)\n    {\n        _providerRepository = providerRepository;\n        _providerUserRepository = providerUserRepository;\n        _providerService = providerService;\n        _userRepository = userRepository;\n        _providerPlanRepository = providerPlanRepository;\n    }\n\n    public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)\n    {\n        var providerId = await CreateProviderAsync(provider, ownerEmail);\n\n        await Task.WhenAll(\n            CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats),\n            CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats));\n    }\n\n    public async Task CreateResellerAsync(Provider provider)\n    {\n        await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);\n    }\n\n    public async Task CreateBusinessUnitAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats)\n    {\n        var providerId = await CreateProviderAsync(provider, ownerEmail);\n\n        await CreateProviderPlanAsync(providerId, plan, minimumSeats);\n    }\n\n    private async Task<Guid> CreateProviderAsync(Provider provider, string ownerEmail)\n    {\n        var owner = await _userRepository.GetByEmailAsync(ownerEmail);\n        if (owner == null)\n        {\n            throw new BadRequestException(\"Invalid owner. Owner must be an existing Bitwarden user.\");\n        }\n\n        provider.Gateway = GatewayType.Stripe;\n\n        await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);\n\n        var providerUser = new ProviderUser\n        {\n            ProviderId = provider.Id,\n            UserId = owner.Id,\n            Type = ProviderUserType.ProviderAdmin,\n            Status = ProviderUserStatusType.Confirmed,\n        };\n\n        await _providerUserRepository.CreateAsync(providerUser);\n        await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);\n\n        return provider.Id;\n    }\n\n    private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status)\n    {\n        provider.Status = status;\n        provider.Enabled = true;\n        provider.UseEvents = true;\n        await _providerRepository.CreateAsync(provider);\n    }\n\n    private async Task CreateProviderPlanAsync(Guid providerId, PlanType planType, int seatMinimum)\n    {\n        var plan = new ProviderPlan\n        {\n            ProviderId = providerId,\n            PlanType = planType,\n            SeatMinimum = seatMinimum,\n            PurchasedSeats = 0,\n            AllocatedSeats = 0\n        };\n        await _providerPlanRepository.CreateAsync(plan);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Providers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Stripe;\n\nnamespace Bit.Commercial.Core.AdminConsole.Providers;\n\npublic class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProviderCommand\n{\n    private readonly IEventService _eventService;\n    private readonly IMailService _mailService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository;\n    private readonly IStripeAdapter _stripeAdapter;\n    private readonly IFeatureService _featureService;\n    private readonly IProviderBillingService _providerBillingService;\n    private readonly ISubscriberService _subscriberService;\n    private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;\n    private readonly IPricingClient _pricingClient;\n\n    public RemoveOrganizationFromProviderCommand(\n        IEventService eventService,\n        IMailService mailService,\n        IOrganizationRepository organizationRepository,\n        IProviderOrganizationRepository providerOrganizationRepository,\n        IStripeAdapter stripeAdapter,\n        IFeatureService featureService,\n        IProviderBillingService providerBillingService,\n        ISubscriberService subscriberService,\n        IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,\n        IPricingClient pricingClient)\n    {\n        _eventService = eventService;\n        _mailService = mailService;\n        _organizationRepository = organizationRepository;\n        _providerOrganizationRepository = providerOrganizationRepository;\n        _stripeAdapter = stripeAdapter;\n        _featureService = featureService;\n        _providerBillingService = providerBillingService;\n        _subscriberService = subscriberService;\n        _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;\n        _pricingClient = pricingClient;\n    }\n\n    public async Task RemoveOrganizationFromProvider(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        Organization organization)\n    {\n        if (provider == null ||\n            providerOrganization == null ||\n            organization == null ||\n            providerOrganization.ProviderId != provider.Id)\n        {\n            throw new BadRequestException(\"Failed to remove organization. Please contact support.\");\n        }\n\n        if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(\n                providerOrganization.OrganizationId,\n                [],\n                includeProvider: false))\n        {\n            throw new BadRequestException(\"Organization must have at least one confirmed owner.\");\n        }\n\n        var organizationOwnerEmails =\n            (await _organizationRepository.GetOwnerEmailAddressesById(organization.Id)).ToList();\n\n        organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);\n\n        await ResetOrganizationBillingAsync(organization, provider, organizationOwnerEmails);\n\n        await _organizationRepository.ReplaceAsync(organization);\n\n        await _providerOrganizationRepository.DeleteAsync(providerOrganization);\n\n        await _eventService.LogProviderOrganizationEventAsync(\n            providerOrganization,\n            EventType.ProviderOrganization_Removed);\n    }\n\n    /// <summary>\n    /// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled\n    /// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because\n    /// the provider's payment method will be removed from their Stripe customer, causing ensuing charges to fail. Lastly,\n    /// we email the organization owners letting them know they need to add a new payment method.\n    /// </summary>\n    private async Task ResetOrganizationBillingAsync(\n        Organization organization,\n        Provider provider,\n        IEnumerable<string> organizationOwnerEmails)\n    {\n        if (provider.IsBillable() &&\n            organization.IsValidClient())\n        {\n            // An organization converted to a business unit will not have a Customer since it was given to the business unit.\n            if (string.IsNullOrEmpty(organization.GatewayCustomerId))\n            {\n                await _providerBillingService.CreateCustomerForClientOrganization(provider, organization);\n            }\n\n            var customer = await _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, new CustomerUpdateOptions\n            {\n                Description = string.Empty,\n                Email = organization.BillingEmail,\n                Expand = [\"tax\", \"tax_ids\"]\n            });\n\n            var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n\n            var subscriptionCreateOptions = new SubscriptionCreateOptions\n            {\n                Customer = organization.GatewayCustomerId,\n                CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,\n                DaysUntilDue = 30,\n                Metadata = new Dictionary<string, string>\n                {\n                    { \"organizationId\", organization.Id.ToString() }\n                },\n                OffSession = true,\n                ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,\n                Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]\n            };\n\n            subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };\n\n            var subscription = await _stripeAdapter.CreateSubscriptionAsync(subscriptionCreateOptions);\n\n            organization.GatewaySubscriptionId = subscription.Id;\n            organization.Status = OrganizationStatusType.Created;\n            organization.Enabled = true;\n\n            await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0);\n        }\n        else if (organization.IsStripeEnabled())\n        {\n            var subscription = await _stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId, new SubscriptionGetOptions\n            {\n                Expand = [\"customer\"]\n            });\n            if (subscription.Status is StripeConstants.SubscriptionStatus.Canceled or StripeConstants.SubscriptionStatus.IncompleteExpired)\n            {\n                return;\n            }\n\n            await _stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, new CustomerUpdateOptions\n            {\n                Email = organization.BillingEmail\n            });\n\n            if (subscription.Customer.Discount?.Coupon != null)\n            {\n                await _stripeAdapter.DeleteCustomerDiscountAsync(subscription.CustomerId);\n            }\n\n            await _stripeAdapter.UpdateSubscriptionAsync(organization.GatewaySubscriptionId, new SubscriptionUpdateOptions\n            {\n                CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,\n                DaysUntilDue = 30,\n            });\n\n            await _subscriberService.RemovePaymentSource(organization);\n        }\n\n        await _mailService.SendProviderUpdatePaymentMethod(\n            organization.Id,\n            organization.Name,\n            provider.Name!,\n            organizationOwnerEmails);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Business.Provider;\nusing Bit.Core.AdminConsole.Models.Business.Tokenables;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.DataProtection;\nusing Stripe;\n\nnamespace Bit.Commercial.Core.AdminConsole.Services;\n\npublic class ProviderService : IProviderService\n{\n    private static readonly PlanType[] _resellerDisallowedOrganizationTypes = [\n        PlanType.Free,\n        PlanType.FamiliesAnnually2025,\n        PlanType.FamiliesAnnually2019,\n        PlanType.FamiliesAnnually\n    ];\n\n    private readonly IDataProtector _dataProtector;\n    private readonly IMailService _mailService;\n    private readonly IEventService _eventService;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IUserRepository _userRepository;\n    private readonly IUserService _userService;\n    private readonly IOrganizationService _organizationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly IStripeAdapter _stripeAdapter;\n    private readonly IFeatureService _featureService;\n    private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IProviderBillingService _providerBillingService;\n    private readonly IPricingClient _pricingClient;\n    private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n\n    public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,\n        IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,\n        IUserService userService, IOrganizationService organizationService, IMailService mailService,\n        IDataProtectionProvider dataProtectionProvider, IEventService eventService,\n        IOrganizationRepository organizationRepository, GlobalSettings globalSettings,\n        ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,\n        IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,\n        IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient,\n        IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand,\n        IPolicyRequirementQuery policyRequirementQuery)\n    {\n        _providerRepository = providerRepository;\n        _providerUserRepository = providerUserRepository;\n        _providerOrganizationRepository = providerOrganizationRepository;\n        _organizationRepository = organizationRepository;\n        _userRepository = userRepository;\n        _userService = userService;\n        _organizationService = organizationService;\n        _mailService = mailService;\n        _eventService = eventService;\n        _globalSettings = globalSettings;\n        _dataProtector = dataProtectionProvider.CreateProtector(\"ProviderServiceDataProtector\");\n        _currentContext = currentContext;\n        _stripeAdapter = stripeAdapter;\n        _featureService = featureService;\n        _providerDeleteTokenDataFactory = providerDeleteTokenDataFactory;\n        _applicationCacheService = applicationCacheService;\n        _providerBillingService = providerBillingService;\n        _pricingClient = pricingClient;\n        _providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;\n        _policyRequirementQuery = policyRequirementQuery;\n    }\n\n    public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress)\n    {\n        var owner = await _userService.GetUserByIdAsync(ownerUserId);\n        if (owner == null)\n        {\n            throw new BadRequestException(\"Invalid owner.\");\n        }\n\n        if (provider.Status != ProviderStatusType.Pending)\n        {\n            throw new BadRequestException(\"Provider is already setup.\");\n        }\n\n        if (!CoreHelpers.TokenIsValid(\"ProviderSetupInvite\", _dataProtector, token, owner.Email, provider.Id,\n            _globalSettings.OrganizationInviteExpirationHours))\n        {\n            throw new BadRequestException(\"Invalid token.\");\n        }\n\n        var providerUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, ownerUserId);\n        if (!(providerUser is { Type: ProviderUserType.ProviderAdmin }))\n        {\n            throw new BadRequestException(\"Invalid owner.\");\n        }\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery\n                .GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerUserId);\n\n            if (organizationAutoConfirmPolicyRequirement\n                .CannotCreateProvider())\n            {\n                throw new BadRequestException(new UserCannotJoinProvider().Message);\n            }\n        }\n\n        var customer = await _providerBillingService.SetupCustomer(provider, paymentMethod, billingAddress);\n        provider.GatewayCustomerId = customer.Id;\n        var subscription = await _providerBillingService.SetupSubscription(provider);\n        provider.GatewaySubscriptionId = subscription.Id;\n        provider.Status = ProviderStatusType.Billable;\n        await _providerRepository.UpsertAsync(provider);\n\n        providerUser.Key = key;\n        await _providerUserRepository.ReplaceAsync(providerUser);\n\n        return provider;\n    }\n\n    public async Task UpdateAsync(Provider provider, bool updateBilling = false)\n    {\n        if (provider.Id == default)\n        {\n            throw new ArgumentException(\"Cannot create provider this way.\");\n        }\n\n        var existingProvider = await _providerRepository.GetByIdAsync(provider.Id);\n        var enabledStatusChanged = existingProvider != null && existingProvider.Enabled != provider.Enabled;\n\n        await _providerRepository.ReplaceAsync(provider);\n\n        if (enabledStatusChanged && (provider.Type == ProviderType.Msp || provider.Type == ProviderType.BusinessUnit))\n        {\n            await UpdateClientOrganizationsEnabledStatusAsync(provider.Id, provider.Enabled);\n        }\n    }\n\n    public async Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite)\n    {\n        if (!_currentContext.ProviderManageUsers(invite.ProviderId))\n        {\n            throw new InvalidOperationException(\"Invalid permissions.\");\n        }\n\n        var emails = invite?.UserIdentifiers;\n        var invitingUser = await _providerUserRepository.GetByProviderUserAsync(invite.ProviderId, invite.InvitingUserId);\n\n        var provider = await _providerRepository.GetByIdAsync(invite.ProviderId);\n        if (provider == null || emails == null || !emails.Any())\n        {\n            throw new NotFoundException();\n        }\n\n        var providerUsers = new List<ProviderUser>();\n        foreach (var email in emails)\n        {\n            // Make sure user is not already invited\n            var existingProviderUserCount =\n                await _providerUserRepository.GetCountByProviderAsync(invite.ProviderId, email, false);\n            if (existingProviderUserCount > 0)\n            {\n                continue;\n            }\n\n            var providerUser = new ProviderUser\n            {\n                ProviderId = invite.ProviderId,\n                UserId = null,\n                Email = email.ToLowerInvariant(),\n                Key = null,\n                Type = invite.Type,\n                Status = ProviderUserStatusType.Invited,\n                CreationDate = DateTime.UtcNow,\n                RevisionDate = DateTime.UtcNow,\n            };\n\n            await _providerUserRepository.CreateAsync(providerUser);\n\n            await SendInviteAsync(providerUser, provider);\n            providerUsers.Add(providerUser);\n        }\n\n        await _eventService.LogProviderUsersEventAsync(providerUsers.Select(pu => (pu, EventType.ProviderUser_Invited, null as DateTime?)));\n\n        return providerUsers;\n    }\n\n    public async Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(ProviderUserInvite<Guid> invite)\n    {\n        if (!_currentContext.ProviderManageUsers(invite.ProviderId))\n        {\n            throw new BadRequestException(\"Invalid permissions.\");\n        }\n\n        var providerUsers = await _providerUserRepository.GetManyAsync(invite.UserIdentifiers);\n        var provider = await _providerRepository.GetByIdAsync(invite.ProviderId);\n\n        var result = new List<Tuple<ProviderUser, string>>();\n        foreach (var providerUser in providerUsers)\n        {\n            if (providerUser.Status != ProviderUserStatusType.Invited || providerUser.ProviderId != invite.ProviderId)\n            {\n                result.Add(Tuple.Create(providerUser, \"User invalid.\"));\n                continue;\n            }\n\n            await SendInviteAsync(providerUser, provider);\n            result.Add(Tuple.Create(providerUser, \"\"));\n        }\n\n        return result;\n    }\n\n    public async Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token)\n    {\n        var providerUser = await _providerUserRepository.GetByIdAsync(providerUserId);\n        if (providerUser == null)\n        {\n            throw new BadRequestException(\"User invalid.\");\n        }\n\n        if (providerUser.Status != ProviderUserStatusType.Invited)\n        {\n            throw new BadRequestException(\"Already accepted.\");\n        }\n\n        if (!CoreHelpers.TokenIsValid(\"ProviderUserInvite\", _dataProtector, token, user.Email, providerUser.Id,\n            _globalSettings.OrganizationInviteExpirationHours))\n        {\n            throw new BadRequestException(\"Invalid token.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(providerUser.Email) ||\n            !providerUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))\n        {\n            throw new BadRequestException(\"User email does not match invite.\");\n        }\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery\n                .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);\n\n            if (organizationAutoConfirmPolicyRequirement\n                .CannotJoinProvider())\n            {\n                throw new BadRequestException(new UserCannotJoinProvider().Message);\n            }\n        }\n\n        providerUser.Status = ProviderUserStatusType.Accepted;\n        providerUser.UserId = user.Id;\n        providerUser.Email = null;\n\n        await _providerUserRepository.ReplaceAsync(providerUser);\n\n        return providerUser;\n    }\n\n    public async Task<List<Tuple<ProviderUser, string>>> ConfirmUsersAsync(Guid providerId, Dictionary<Guid, string> keys,\n        Guid confirmingUserId)\n    {\n        var providerUsers = await _providerUserRepository.GetManyAsync(keys.Keys);\n        var validProviderUsers = providerUsers\n            .Where(u => u.UserId != null)\n            .ToList();\n\n        if (!validProviderUsers.Any())\n        {\n            return new List<Tuple<ProviderUser, string>>();\n        }\n\n        var validOrganizationUserIds = validProviderUsers.Select(u => u.UserId.Value).ToList();\n\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n        var users = await _userRepository.GetManyAsync(validOrganizationUserIds);\n\n        var keyedFilteredUsers = validProviderUsers.ToDictionary(u => u.UserId.Value, u => u);\n\n        var result = new List<Tuple<ProviderUser, string>>();\n        var events = new List<(ProviderUser, EventType, DateTime?)>();\n\n        foreach (var user in users)\n        {\n            if (!keyedFilteredUsers.TryGetValue(user.Id, out var providerUser))\n            {\n                continue;\n            }\n            try\n            {\n                if (providerUser.Status != ProviderUserStatusType.Accepted || providerUser.ProviderId != providerId)\n                {\n                    throw new BadRequestException(\"Invalid user.\");\n                }\n\n                if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n                {\n                    var organizationAutoConfirmPolicyRequirement = await _policyRequirementQuery\n                        .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);\n\n                    if (organizationAutoConfirmPolicyRequirement\n                        .CannotJoinProvider())\n                    {\n                        result.Add(Tuple.Create(providerUser, new UserCannotJoinProvider().Message));\n                        continue;\n                    }\n                }\n\n                providerUser.Status = ProviderUserStatusType.Confirmed;\n                providerUser.Key = keys[providerUser.Id];\n                providerUser.Email = null;\n\n                await _providerUserRepository.ReplaceAsync(providerUser);\n                events.Add((providerUser, EventType.ProviderUser_Confirmed, null));\n                await _mailService.SendProviderConfirmedEmailAsync(provider.DisplayName(), user.Email);\n                result.Add(Tuple.Create(providerUser, \"\"));\n            }\n            catch (BadRequestException e)\n            {\n                result.Add(Tuple.Create(providerUser, e.Message));\n            }\n        }\n\n        await _eventService.LogProviderUsersEventAsync(events);\n\n        return result;\n    }\n\n    public async Task SaveUserAsync(ProviderUser user, Guid savingUserId)\n    {\n        if (user.Id.Equals(default))\n        {\n            throw new BadRequestException(\"Invite the user first.\");\n        }\n\n        if (user.Type != ProviderUserType.ProviderAdmin &&\n            !await HasConfirmedProviderAdminExceptAsync(user.ProviderId, new[] { user.Id }))\n        {\n            throw new BadRequestException(\"Provider must have at least one confirmed ProviderAdmin.\");\n        }\n\n        await _providerUserRepository.ReplaceAsync(user);\n        await _eventService.LogProviderUserEventAsync(user, EventType.ProviderUser_Updated);\n    }\n\n    public async Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId,\n        IEnumerable<Guid> providerUserIds, Guid deletingUserId)\n    {\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n\n        if (provider == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var providerUsers = await _providerUserRepository.GetManyAsync(providerUserIds);\n        var users = await _userRepository.GetManyAsync(providerUsers.Where(pu => pu.UserId.HasValue)\n            .Select(pu => pu.UserId.Value));\n        var keyedUsers = users.ToDictionary(u => u.Id);\n\n        if (!await HasConfirmedProviderAdminExceptAsync(providerId, providerUserIds))\n        {\n            throw new BadRequestException(\"Provider must have at least one confirmed ProviderAdmin.\");\n        }\n\n        var result = new List<Tuple<ProviderUser, string>>();\n        var deletedUserIds = new List<Guid>();\n        var events = new List<(ProviderUser, EventType, DateTime?)>();\n\n        foreach (var providerUser in providerUsers)\n        {\n            try\n            {\n                if (providerUser.ProviderId != providerId)\n                {\n                    throw new BadRequestException(\"Invalid user.\");\n                }\n                if (providerUser.UserId == deletingUserId)\n                {\n                    throw new BadRequestException(\"You cannot remove yourself.\");\n                }\n\n                events.Add((providerUser, EventType.ProviderUser_Removed, null));\n\n                var user = keyedUsers.GetValueOrDefault(providerUser.UserId.GetValueOrDefault());\n                var email = user == null ? providerUser.Email : user.Email;\n                if (!string.IsNullOrWhiteSpace(email))\n                {\n                    await _mailService.SendProviderUserRemoved(provider.DisplayName(), email);\n                }\n\n                result.Add(Tuple.Create(providerUser, \"\"));\n                deletedUserIds.Add(providerUser.Id);\n            }\n            catch (BadRequestException e)\n            {\n                result.Add(Tuple.Create(providerUser, e.Message));\n            }\n\n            await _providerUserRepository.DeleteManyAsync(deletedUserIds);\n        }\n\n        await _eventService.LogProviderUsersEventAsync(events);\n\n        return result;\n    }\n\n    public async Task AddOrganization(Guid providerId, Guid organizationId, string key)\n    {\n        var po = await _providerOrganizationRepository.GetByOrganizationId(organizationId);\n        if (po != null)\n        {\n            throw new BadRequestException(\"Organization already belongs to a provider.\");\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n\n        ThrowOnInvalidPlanType(provider.Type, organization.PlanType);\n\n        if (organization.UseSecretsManager)\n        {\n            throw new BadRequestException(\n                \"The organization is subscribed to Secrets Manager. Please contact Customer Support to manage the subscription.\");\n        }\n\n        var providerOrganization = new ProviderOrganization\n        {\n            ProviderId = providerId,\n            OrganizationId = organizationId,\n            Key = key,\n        };\n\n        await ApplyProviderPriceRateAsync(organization, provider);\n        await _providerOrganizationRepository.CreateAsync(providerOrganization);\n\n        organization.BillingEmail = provider.BillingEmail;\n        await _organizationRepository.ReplaceAsync(organization);\n\n        if (!string.IsNullOrEmpty(organization.GatewayCustomerId))\n        {\n            await _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, new CustomerUpdateOptions\n            {\n                Email = provider.BillingEmail\n            });\n        }\n\n        await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);\n    }\n\n    public async Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds)\n    {\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n        if (provider.Type != ProviderType.Reseller)\n        {\n            throw new BadRequestException(\"Provider must be of type Reseller in order to assign Organizations to it.\");\n        }\n\n        var orgIdsList = organizationIds.ToList();\n        var existingProviderOrganizationsCount = await _providerOrganizationRepository.GetCountByOrganizationIdsAsync(orgIdsList);\n        if (existingProviderOrganizationsCount > 0)\n        {\n            throw new BadRequestException(\"Organizations must not be assigned to any Provider.\");\n        }\n\n        var providerOrganizationsToInsert = orgIdsList.Select(orgId => new ProviderOrganization { ProviderId = providerId, OrganizationId = orgId });\n        var insertedProviderOrganizations = await _providerOrganizationRepository.CreateManyAsync(providerOrganizationsToInsert);\n\n        await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));\n    }\n\n    private async Task ApplyProviderPriceRateAsync(Organization organization, Provider provider)\n    {\n        // if a provider was created before Nov 6, 2023.If true, the organization plan assigned to that provider is updated to a 2020 plan.\n        if (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023)\n        {\n            return;\n        }\n\n        if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))\n        {\n            var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n\n            var subscriptionItem = await GetSubscriptionItemAsync(\n                organization.GatewaySubscriptionId,\n                plan.PasswordManager.StripeSeatPlanId);\n\n            var extractedPlanType = PlanTypeMappings(organization);\n            var extractedPlan = await _pricingClient.GetPlanOrThrow(extractedPlanType);\n\n            if (subscriptionItem != null)\n            {\n                await UpdateSubscriptionAsync(subscriptionItem, extractedPlan.PasswordManager.StripeSeatPlanId, organization);\n            }\n        }\n\n        await _organizationRepository.UpsertAsync(organization);\n    }\n\n    private async Task<SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId)\n    {\n        var subscriptionDetails = await _stripeAdapter.GetSubscriptionAsync(subscriptionId);\n        return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId);\n    }\n\n    private async Task UpdateSubscriptionAsync(SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization)\n    {\n        try\n        {\n            if (subscriptionItem.Price.Id != extractedPlanType)\n            {\n                await _stripeAdapter.UpdateSubscriptionAsync(subscriptionItem.Subscription,\n                    new Stripe.SubscriptionUpdateOptions\n                    {\n                        Items = new List<Stripe.SubscriptionItemOptions>\n                        {\n                            new()\n                            {\n                                Id = subscriptionItem.Id,\n                                Price = extractedPlanType,\n                                Quantity = organization.Seats.Value,\n                            },\n                        }\n                    });\n            }\n        }\n        catch (Exception)\n        {\n            throw new Exception(\"Unable to update existing plan on stripe\");\n        }\n\n    }\n\n    private static PlanType PlanTypeMappings(Organization organization)\n    {\n        var planTypeMappings = new Dictionary<PlanType, string>\n        {\n            { PlanType.EnterpriseAnnually2020, GetEnumDisplayName(PlanType.EnterpriseAnnually2020) },\n            { PlanType.EnterpriseMonthly2020, GetEnumDisplayName(PlanType.EnterpriseMonthly2020) },\n            { PlanType.TeamsMonthly2020, GetEnumDisplayName(PlanType.TeamsMonthly2020) },\n            { PlanType.TeamsAnnually2020, GetEnumDisplayName(PlanType.TeamsAnnually2020) }\n        };\n\n        foreach (var mapping in planTypeMappings)\n        {\n            if (mapping.Value.IndexOf(organization.Plan, StringComparison.Ordinal) != -1)\n            {\n                organization.PlanType = mapping.Key;\n                organization.Plan = mapping.Value;\n                return organization.PlanType;\n            }\n        }\n\n        throw new ArgumentException(\"Invalid PlanType selected\");\n    }\n\n    private static string GetEnumDisplayName(Enum value)\n    {\n        var fieldInfo = value.GetType().GetField(value.ToString());\n\n        var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo!, typeof(DisplayAttribute));\n\n        return displayAttribute?.Name ?? value.ToString();\n    }\n\n    public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,\n        OrganizationSignup organizationSignup, string clientOwnerEmail, User user)\n    {\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n\n        ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan);\n\n        var signUpResponse = await _providerClientOrganizationSignUpCommand.SignUpClientOrganizationAsync(organizationSignup);\n\n        var providerOrganization = new ProviderOrganization\n        {\n            ProviderId = providerId,\n            OrganizationId = signUpResponse.Organization.Id,\n            Key = organizationSignup.OwnerKey,\n        };\n\n        await _providerOrganizationRepository.CreateAsync(providerOrganization);\n        await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);\n\n        // Give the owner Can Manage access over the default collection\n        // The orgUser is not available when the org is created so we have to do it here as part of the invite\n        var defaultOwnerAccess = signUpResponse.DefaultCollection != null\n            ?\n            [\n                new CollectionAccessSelection\n                {\n                    Id = signUpResponse.DefaultCollection.Id,\n                    HidePasswords = false,\n                    ReadOnly = false,\n                    Manage = true\n                }\n            ]\n            : Array.Empty<CollectionAccessSelection>();\n\n        await _organizationService.InviteUsersAsync(signUpResponse.Organization.Id, user.Id, systemUser: null,\n            new (OrganizationUserInvite, string)[]\n            {\n                (\n                    new OrganizationUserInvite\n                    {\n                        Emails = new[] { clientOwnerEmail },\n                        Type = OrganizationUserType.Owner,\n                        Permissions = null,\n                        Collections = defaultOwnerAccess,\n                    },\n                    null\n                )\n            });\n\n        return providerOrganization;\n    }\n\n    public async Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId)\n    {\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n        var owner = await _userRepository.GetByIdAsync(ownerId);\n        if (owner == null)\n        {\n            throw new BadRequestException(\"Invalid owner.\");\n        }\n        await SendProviderSetupInviteEmailAsync(provider, owner.Email);\n    }\n\n    public async Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail)\n    {\n        var token = _dataProtector.Protect($\"ProviderSetupInvite {provider.Id} {ownerEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\");\n        await _mailService.SendProviderSetupInviteEmailAsync(provider, token, ownerEmail);\n    }\n\n    public async Task LogProviderAccessToOrganizationAsync(Guid organizationId)\n    {\n        if (organizationId == default)\n        {\n            return;\n        }\n\n        var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(organizationId);\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n        if (providerOrganization != null)\n        {\n            await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_VaultAccessed);\n        }\n        if (organization != null)\n        {\n            await _eventService.LogOrganizationEventAsync(organization, EventType.Organization_VaultAccessed);\n        }\n    }\n\n    public async Task InitiateDeleteAsync(Provider provider, string providerAdminEmail)\n    {\n        if (string.IsNullOrWhiteSpace(provider.Name))\n        {\n            throw new BadRequestException(\"Provider name not found.\");\n        }\n        var providerAdmin = await _userRepository.GetByEmailAsync(providerAdminEmail);\n        if (providerAdmin == null)\n        {\n            throw new BadRequestException(\"Provider admin not found.\");\n        }\n\n        var providerAdminOrgUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id);\n        if (providerAdminOrgUser == null || providerAdminOrgUser.Status != ProviderUserStatusType.Confirmed ||\n            providerAdminOrgUser.Type != ProviderUserType.ProviderAdmin)\n        {\n            throw new BadRequestException(\"Org admin not found.\");\n        }\n\n        var token = _providerDeleteTokenDataFactory.Protect(new ProviderDeleteTokenable(provider, 1));\n        await _mailService.SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, token);\n    }\n\n    public async Task DeleteAsync(Provider provider, string token)\n    {\n        if (!_providerDeleteTokenDataFactory.TryUnprotect(token, out var data) || !data.IsValid(provider))\n        {\n            throw new BadRequestException(\"Invalid token.\");\n        }\n        await DeleteAsync(provider);\n    }\n\n    public async Task DeleteAsync(Provider provider)\n    {\n        await _providerRepository.DeleteAsync(provider);\n        await _applicationCacheService.DeleteProviderAbilityAsync(provider.Id);\n    }\n\n    private async Task SendInviteAsync(ProviderUser providerUser, Provider provider)\n    {\n        var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);\n        var token = _dataProtector.Protect(\n            $\"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}\");\n        await _mailService.SendProviderInviteEmailAsync(provider.DisplayName(), providerUser, token, providerUser.Email);\n    }\n\n    private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)\n    {\n        var providerAdmins = await _providerUserRepository.GetManyByProviderAsync(providerId,\n            ProviderUserType.ProviderAdmin);\n        var confirmedOwners = providerAdmins.Where(o => o.Status == ProviderUserStatusType.Confirmed);\n        var confirmedOwnersIds = confirmedOwners.Select(u => u.Id);\n        return confirmedOwnersIds.Except(providerUserIds).Any();\n    }\n\n    private void ThrowOnInvalidPlanType(ProviderType providerType, PlanType requestedType)\n    {\n        switch (providerType)\n        {\n            case ProviderType.Msp:\n                if (requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly))\n                {\n                    throw new BadRequestException($\"Managed Service Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed.\");\n                }\n                break;\n            case ProviderType.BusinessUnit:\n                if (requestedType is not (PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually))\n                {\n                    throw new BadRequestException($\"Business Unit Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed.\");\n                }\n                break;\n            case ProviderType.Reseller:\n                if (_resellerDisallowedOrganizationTypes.Contains(requestedType))\n                {\n                    throw new BadRequestException($\"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.\");\n                }\n                break;\n            default:\n                throw new BadRequestException($\"Unsupported provider type {providerType}.\");\n        }\n    }\n\n    private async Task UpdateClientOrganizationsEnabledStatusAsync(Guid providerId, bool enabled)\n    {\n        var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);\n\n        foreach (var providerOrganization in providerOrganizations)\n        {\n            var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);\n            if (organization != null && organization.Enabled != enabled)\n            {\n                organization.Enabled = enabled;\n                await _organizationRepository.ReplaceAsync(organization);\n                await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Globalization;\nusing Bit.Core.Billing.Providers.Entities;\nusing CsvHelper.Configuration.Attributes;\n\nnamespace Bit.Commercial.Core.Billing.Providers.Models;\n\npublic class ProviderClientInvoiceReportRow\n{\n    public string Client { get; set; }\n    public string Id { get; set; }\n    public int Assigned { get; set; }\n    public int Used { get; set; }\n    public int Remaining { get; set; }\n    public string Plan { get; set; }\n    [Name(\"Estimated total\")]\n    public string Total { get; set; }\n\n    public static ProviderClientInvoiceReportRow From(ProviderInvoiceItem providerInvoiceItem)\n        => new()\n        {\n            Client = providerInvoiceItem.ClientName,\n            Id = providerInvoiceItem.ClientId?.ToString(),\n            Assigned = providerInvoiceItem.AssignedSeats,\n            Used = providerInvoiceItem.UsedSeats,\n            Remaining = providerInvoiceItem.AssignedSeats - providerInvoiceItem.UsedSeats,\n            Plan = providerInvoiceItem.PlanName,\n            Total = string.Format(new CultureInfo(\"en-US\", false), \"{0:C}\", providerInvoiceItem.Total)\n        };\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Providers.Models;\nusing Bit.Core.Billing.Providers.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Context;\nusing Stripe;\nusing Stripe.Tax;\n\nnamespace Bit.Commercial.Core.Billing.Providers.Queries;\n\nusing static StripeConstants;\nusing SuspensionWarning = ProviderWarnings.SuspensionWarning;\nusing TaxIdWarning = ProviderWarnings.TaxIdWarning;\n\npublic class GetProviderWarningsQuery(\n    ICurrentContext currentContext,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService) : IGetProviderWarningsQuery\n{\n    public async Task<ProviderWarnings?> Run(Provider provider)\n    {\n        var warnings = new ProviderWarnings();\n\n        var subscription =\n            await subscriberService.GetSubscription(provider,\n                new SubscriptionGetOptions { Expand = [\"customer.tax_ids\"] });\n\n        if (subscription == null)\n        {\n            return warnings;\n        }\n\n        warnings.Suspension = GetSuspensionWarning(provider, subscription);\n\n        warnings.TaxId = await GetTaxIdWarningAsync(provider, subscription.Customer);\n\n        return warnings;\n    }\n\n    private SuspensionWarning? GetSuspensionWarning(\n        Provider provider,\n        Subscription subscription)\n    {\n        if (provider.Enabled)\n        {\n            return null;\n        }\n\n        return subscription.Status switch\n        {\n            SubscriptionStatus.Unpaid => currentContext.ProviderProviderAdmin(provider.Id)\n                ? new SuspensionWarning { Resolution = \"add_payment_method\", SubscriptionCancelsAt = subscription.CancelAt }\n                : new SuspensionWarning { Resolution = \"contact_administrator\" },\n            _ => new SuspensionWarning { Resolution = \"contact_support\" }\n        };\n    }\n\n    private async Task<TaxIdWarning?> GetTaxIdWarningAsync(\n        Provider provider,\n        Customer customer)\n    {\n        if (TaxHelpers.IsDirectTaxCountry(customer.Address?.Country))\n        {\n            return null;\n        }\n\n        if (!currentContext.ProviderProviderAdmin(provider.Id))\n        {\n            return null;\n        }\n\n        // TODO: Potentially DRY this out with the GetOrganizationWarningsQuery\n\n        // Get active and scheduled registrations\n        var registrations = (await Task.WhenAll(\n                stripeAdapter.ListTaxRegistrationsAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),\n                stripeAdapter.ListTaxRegistrationsAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))\n            .SelectMany(registrations => registrations.Data);\n\n        // Find the matching registration for the customer\n        var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address?.Country);\n\n        // If we're not registered in their country, we don't need a warning\n        if (registration == null)\n        {\n            return null;\n        }\n\n        var taxId = customer.TaxIds.FirstOrDefault();\n\n        return taxId switch\n        {\n            // Customer's tax ID is missing\n            null => new TaxIdWarning { Type = \"tax_id_missing\" },\n            // Not sure if this case is valid, but Stripe says this property is nullable\n            not null when taxId.Verification == null => null,\n            // Customer's tax ID is pending verification\n            not null when taxId.Verification.Status == TaxIdVerificationStatus.Pending => new TaxIdWarning { Type = \"tax_id_pending_verification\" },\n            // Customer's tax ID failed verification\n            not null when taxId.Verification.Status == TaxIdVerificationStatus.Unverified => new TaxIdWarning { Type = \"tax_id_failed_verification\" },\n            _ => null\n        };\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs",
    "content": "﻿#nullable enable\nusing System.Diagnostics.CodeAnalysis;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.Logging;\nusing OneOf;\nusing Stripe;\n\nnamespace Bit.Commercial.Core.Billing.Providers.Services;\n\npublic class BusinessUnitConverter(\n    IDataProtectionProvider dataProtectionProvider,\n    GlobalSettings globalSettings,\n    ILogger<BusinessUnitConverter> logger,\n    IMailService mailService,\n    IOrganizationRepository organizationRepository,\n    IOrganizationUserRepository organizationUserRepository,\n    IPricingClient pricingClient,\n    IProviderOrganizationRepository providerOrganizationRepository,\n    IProviderPlanRepository providerPlanRepository,\n    IProviderRepository providerRepository,\n    IProviderUserRepository providerUserRepository,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService,\n    IUserRepository userRepository) : IBusinessUnitConverter\n{\n    private readonly IDataProtector _dataProtector =\n        dataProtectionProvider.CreateProtector($\"{nameof(BusinessUnitConverter)}DataProtector\");\n\n    public async Task<Guid> FinalizeConversion(\n        Organization organization,\n        Guid userId,\n        string token,\n        string providerKey,\n        string organizationKey)\n    {\n        var user = await userRepository.GetByIdAsync(userId);\n\n        var (subscription, provider, providerOrganization, providerUser) = await ValidateFinalizationAsync(organization, user, token);\n\n        var existingPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n        var updatedPlan = await pricingClient.GetPlanOrThrow(existingPlan.IsAnnual ? PlanType.EnterpriseAnnually : PlanType.EnterpriseMonthly);\n\n        // Bring organization under management.\n        organization.Plan = updatedPlan.Name;\n        organization.PlanType = updatedPlan.Type;\n        organization.MaxCollections = updatedPlan.PasswordManager.MaxCollections;\n        organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb;\n        organization.UsePolicies = updatedPlan.HasPolicies;\n        organization.UseMyItems = updatedPlan.HasMyItems;\n        organization.UseSso = updatedPlan.HasSso;\n        organization.UseOrganizationDomains = updatedPlan.HasOrganizationDomains;\n        organization.UseGroups = updatedPlan.HasGroups;\n        organization.UseEvents = updatedPlan.HasEvents;\n        organization.UseDirectory = updatedPlan.HasDirectory;\n        organization.UseTotp = updatedPlan.HasTotp;\n        organization.Use2fa = updatedPlan.Has2fa;\n        organization.UseApi = updatedPlan.HasApi;\n        organization.UseResetPassword = updatedPlan.HasResetPassword;\n        organization.SelfHost = updatedPlan.HasSelfHost;\n        organization.UsersGetPremium = updatedPlan.UsersGetPremium;\n        organization.UseCustomPermissions = updatedPlan.HasCustomPermissions;\n        organization.UseScim = updatedPlan.HasScim;\n        organization.UseKeyConnector = updatedPlan.HasKeyConnector;\n        organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb;\n        organization.BillingEmail = provider.BillingEmail!;\n        organization.GatewayCustomerId = null;\n        organization.GatewaySubscriptionId = null;\n        organization.ExpirationDate = null;\n        organization.MaxAutoscaleSeats = null;\n        organization.Status = OrganizationStatusType.Managed;\n\n        // Enable organization access via key exchange.\n        providerOrganization.Key = organizationKey;\n\n        // Complete provider setup.\n        provider.Gateway = GatewayType.Stripe;\n        provider.GatewayCustomerId = subscription.CustomerId;\n        provider.GatewaySubscriptionId = subscription.Id;\n        provider.Status = ProviderStatusType.Billable;\n\n        // Enable provider access via key exchange.\n        providerUser.Key = providerKey;\n        providerUser.Status = ProviderUserStatusType.Confirmed;\n\n        // Stripe requires that we clear all the custom fields from the invoice settings if we want to replace them.\n        await stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, new CustomerUpdateOptions\n        {\n            InvoiceSettings = new CustomerInvoiceSettingsOptions\n            {\n                CustomFields = []\n            }\n        });\n\n        var metadata = new Dictionary<string, string>\n        {\n            [StripeConstants.MetadataKeys.OrganizationId] = string.Empty,\n            [StripeConstants.MetadataKeys.ProviderId] = provider.Id.ToString(),\n            [\"convertedFrom\"] = organization.Id.ToString()\n        };\n\n        var updateCustomer = stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, new CustomerUpdateOptions\n        {\n            InvoiceSettings = new CustomerInvoiceSettingsOptions\n            {\n                CustomFields = [\n                    new CustomerInvoiceSettingsCustomFieldOptions\n                    {\n                        Name = provider.SubscriberType(),\n                        Value = provider.DisplayName()?.Length <= 30\n                            ? provider.DisplayName()\n                            : provider.DisplayName()?[..30]\n                    }\n                ]\n            },\n            Metadata = metadata\n        });\n\n        // Find the existing password manager price on the subscription.\n        var passwordManagerItem = subscription.Items.First(item =>\n        {\n            var priceId = existingPlan.HasNonSeatBasedPasswordManagerPlan()\n                ? existingPlan.PasswordManager.StripePlanId\n                : existingPlan.PasswordManager.StripeSeatPlanId;\n\n            return item.Price.Id == priceId;\n        });\n\n        // Get the new business unit price.\n        var updatedPriceId = ProviderPriceAdapter.GetActivePriceId(provider, updatedPlan.Type);\n\n        // Replace the existing password manager price with the new business unit price.\n        var updateSubscription =\n            stripeAdapter.UpdateSubscriptionAsync(subscription.Id,\n                new SubscriptionUpdateOptions\n                {\n                    Items = [\n                        new SubscriptionItemOptions\n                        {\n                            Id = passwordManagerItem.Id,\n                            Deleted = true\n                        },\n                        new SubscriptionItemOptions\n                        {\n                            Price = updatedPriceId,\n                            Quantity = organization.Seats\n                        }\n                    ],\n                    Metadata = metadata\n                });\n\n        await Task.WhenAll(updateCustomer, updateSubscription);\n\n        // Complete database updates for provider setup.\n        await Task.WhenAll(\n            organizationRepository.ReplaceAsync(organization),\n            providerOrganizationRepository.ReplaceAsync(providerOrganization),\n            providerRepository.ReplaceAsync(provider),\n            providerUserRepository.ReplaceAsync(providerUser));\n\n        return provider.Id;\n    }\n\n    public async Task<OneOf<Guid, List<string>>> InitiateConversion(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        var user = await userRepository.GetByEmailAsync(providerAdminEmail);\n\n        var problems = await ValidateInitiationAsync(organization, user);\n\n        if (problems is { Count: > 0 })\n        {\n            return problems;\n        }\n\n        var provider = await providerRepository.CreateAsync(new Provider\n        {\n            Name = organization.Name,\n            BillingEmail = organization.BillingEmail,\n            Status = ProviderStatusType.Pending,\n            UseEvents = true,\n            Type = ProviderType.BusinessUnit\n        });\n\n        var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        var managedPlanType = plan.IsAnnual\n            ? PlanType.EnterpriseAnnually\n            : PlanType.EnterpriseMonthly;\n\n        var createProviderOrganization = providerOrganizationRepository.CreateAsync(new ProviderOrganization\n        {\n            ProviderId = provider.Id,\n            OrganizationId = organization.Id\n        });\n\n        var createProviderPlan = providerPlanRepository.CreateAsync(new ProviderPlan\n        {\n            ProviderId = provider.Id,\n            PlanType = managedPlanType,\n            SeatMinimum = 0,\n            PurchasedSeats = organization.Seats,\n            AllocatedSeats = organization.Seats\n        });\n\n        var createProviderUser = providerUserRepository.CreateAsync(new ProviderUser\n        {\n            ProviderId = provider.Id,\n            UserId = user!.Id,\n            Email = user.Email,\n            Status = ProviderUserStatusType.Invited,\n            Type = ProviderUserType.ProviderAdmin\n        });\n\n        await Task.WhenAll(createProviderOrganization, createProviderPlan, createProviderUser);\n\n        await SendInviteAsync(organization, user.Email);\n\n        return provider.Id;\n    }\n\n    public Task ResendConversionInvite(\n        Organization organization,\n        string providerAdminEmail) =>\n        IfConversionInProgressAsync(organization, providerAdminEmail,\n            async (_, _, providerUser) =>\n            {\n                if (!string.IsNullOrEmpty(providerUser.Email))\n                {\n                    await SendInviteAsync(organization, providerUser.Email);\n                }\n            });\n\n    public Task ResetConversion(\n        Organization organization,\n        string providerAdminEmail) =>\n        IfConversionInProgressAsync(organization, providerAdminEmail,\n            async (provider, providerOrganization, providerUser) =>\n            {\n                var tasks = new List<Task>\n                {\n                    providerOrganizationRepository.DeleteAsync(providerOrganization),\n                    providerUserRepository.DeleteAsync(providerUser)\n                };\n\n                var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);\n\n                if (providerPlans is { Count: > 0 })\n                {\n                    tasks.AddRange(providerPlans.Select(providerPlanRepository.DeleteAsync));\n                }\n\n                await Task.WhenAll(tasks);\n\n                await providerRepository.DeleteAsync(provider);\n            });\n\n    #region Utilities\n\n    private async Task IfConversionInProgressAsync(\n        Organization organization,\n        string providerAdminEmail,\n        Func<Provider, ProviderOrganization, ProviderUser, Task> callback)\n    {\n        var user = await userRepository.GetByEmailAsync(providerAdminEmail);\n\n        if (user == null)\n        {\n            return;\n        }\n\n        var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);\n\n        if (provider is not\n            {\n                Type: ProviderType.BusinessUnit,\n                Status: ProviderStatusType.Pending\n            })\n        {\n            return;\n        }\n\n        var providerUser = await providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id);\n\n        if (providerUser is\n            {\n                Type: ProviderUserType.ProviderAdmin,\n                Status: ProviderUserStatusType.Invited\n            })\n        {\n            var providerOrganization = await providerOrganizationRepository.GetByOrganizationId(organization.Id);\n            await callback(provider, providerOrganization!, providerUser);\n        }\n    }\n\n    private async Task SendInviteAsync(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        var token = _dataProtector.Protect(\n            $\"BusinessUnitConversionInvite {organization.Id} {providerAdminEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\");\n\n        await mailService.SendBusinessUnitConversionInviteAsync(organization, token, providerAdminEmail);\n    }\n\n    private async Task<(Subscription, Provider, ProviderOrganization, ProviderUser)> ValidateFinalizationAsync(\n        Organization organization,\n        User? user,\n        string token)\n    {\n        if (organization.PlanType.GetProductTier() != ProductTierType.Enterprise)\n        {\n            Fail(\"Organization must be on an enterprise plan.\");\n        }\n\n        var subscription = await subscriberService.GetSubscription(organization);\n\n        if (subscription is not\n            {\n                Status:\n                StripeConstants.SubscriptionStatus.Active or\n                StripeConstants.SubscriptionStatus.Trialing or\n                StripeConstants.SubscriptionStatus.PastDue\n            })\n        {\n            Fail(\"Organization must have a valid subscription.\");\n        }\n\n        if (user == null)\n        {\n            Fail(\"Provider admin must be a Bitwarden user.\");\n        }\n\n        if (!CoreHelpers.TokenIsValid(\n                \"BusinessUnitConversionInvite\",\n                _dataProtector,\n                token,\n                user.Email,\n                organization.Id,\n                globalSettings.OrganizationInviteExpirationHours))\n        {\n            Fail(\"Email token is invalid.\");\n        }\n\n        var organizationUser =\n            await organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);\n\n        if (organizationUser is not\n            {\n                Status: OrganizationUserStatusType.Confirmed\n            })\n        {\n            Fail(\"Provider admin must be a confirmed member of the organization being converted.\");\n        }\n\n        var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);\n\n        if (provider is not\n            {\n                Type: ProviderType.BusinessUnit,\n                Status: ProviderStatusType.Pending\n            })\n        {\n            Fail(\"Linked provider is not a pending business unit.\");\n        }\n\n        var providerUser = await providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id);\n\n        if (providerUser is not\n            {\n                Type: ProviderUserType.ProviderAdmin,\n                Status: ProviderUserStatusType.Invited\n            })\n        {\n            Fail(\"Provider admin has not been invited.\");\n        }\n\n        var providerOrganization = await providerOrganizationRepository.GetByOrganizationId(organization.Id);\n\n        return (subscription, provider, providerOrganization!, providerUser);\n\n        [DoesNotReturn]\n        void Fail(string scopedError)\n        {\n            logger.LogError(\"Could not finalize business unit conversion for organization ({OrganizationID}): {Error}\",\n                organization.Id, scopedError);\n            throw new BillingException();\n        }\n    }\n\n    private async Task<List<string>?> ValidateInitiationAsync(\n        Organization organization,\n        User? user)\n    {\n        var problems = new List<string>();\n\n        if (organization.PlanType.GetProductTier() != ProductTierType.Enterprise)\n        {\n            problems.Add(\"Organization must be on an enterprise plan.\");\n        }\n\n        var subscription = await subscriberService.GetSubscription(organization);\n\n        if (subscription is not\n            {\n                Status:\n                StripeConstants.SubscriptionStatus.Active or\n                StripeConstants.SubscriptionStatus.Trialing or\n                StripeConstants.SubscriptionStatus.PastDue\n            })\n        {\n            problems.Add(\"Organization must have a valid subscription.\");\n        }\n\n        var providerOrganization = await providerOrganizationRepository.GetByOrganizationId(organization.Id);\n\n        if (providerOrganization != null)\n        {\n            problems.Add(\"Organization is already linked to a provider.\");\n        }\n\n        if (user == null)\n        {\n            problems.Add(\"Provider admin must be a Bitwarden user.\");\n        }\n        else\n        {\n            var organizationUser =\n                await organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);\n\n            if (organizationUser is not\n                {\n                    Status: OrganizationUserStatusType.Confirmed\n                })\n            {\n                problems.Add(\"Provider admin must be a confirmed member of the organization being converted.\");\n            }\n        }\n\n        return problems.Count == 0 ? null : problems;\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Globalization;\nusing Bit.Commercial.Core.Billing.Providers.Models;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Models;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Braintree;\nusing CsvHelper;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\nusing static Bit.Core.Billing.Utilities;\nusing Customer = Stripe.Customer;\nusing Subscription = Stripe.Subscription;\n\nnamespace Bit.Commercial.Core.Billing.Providers.Services;\n\nusing static StripeConstants;\n\npublic class ProviderBillingService(\n    IBraintreeGateway braintreeGateway,\n    IEventService eventService,\n    IGlobalSettings globalSettings,\n    ILogger<ProviderBillingService> logger,\n    IOrganizationRepository organizationRepository,\n    IPricingClient pricingClient,\n    IProviderInvoiceItemRepository providerInvoiceItemRepository,\n    IProviderOrganizationRepository providerOrganizationRepository,\n    IProviderPlanRepository providerPlanRepository,\n    IProviderUserRepository providerUserRepository,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService)\n    : IProviderBillingService\n{\n    public async Task AddExistingOrganization(\n        Provider provider,\n        Organization organization,\n        string key)\n    {\n        await stripeAdapter.UpdateSubscriptionAsync(organization.GatewaySubscriptionId,\n            new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });\n\n        var subscription =\n            await stripeAdapter.CancelSubscriptionAsync(organization.GatewaySubscriptionId,\n                new SubscriptionCancelOptions\n                {\n                    CancellationDetails = new SubscriptionCancellationDetailsOptions\n                    {\n                        Comment = $\"Organization was added to Provider with ID {provider.Id}\"\n                    },\n                    InvoiceNow = true,\n                    Prorate = true,\n                    Expand = [\"latest_invoice\", \"test_clock\"]\n                });\n\n        var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;\n\n        var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now;\n\n        if (!wasTrialing && subscription.LatestInvoice.Status == InvoiceStatus.Draft)\n        {\n            await stripeAdapter.FinalizeInvoiceAsync(subscription.LatestInvoiceId,\n                new InvoiceFinalizeOptions { AutoAdvance = true });\n        }\n\n        var managedPlanType = await GetManagedPlanTypeAsync(provider, organization);\n\n        var plan = await pricingClient.GetPlanOrThrow(managedPlanType);\n        organization.Plan = plan.Name;\n        organization.PlanType = plan.Type;\n        organization.MaxCollections = plan.PasswordManager.MaxCollections;\n        organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;\n        organization.UsePolicies = plan.HasPolicies;\n        organization.UseMyItems = plan.HasMyItems;\n        organization.UseSso = plan.HasSso;\n        organization.UseOrganizationDomains = plan.HasOrganizationDomains;\n        organization.UseGroups = plan.HasGroups;\n        organization.UseEvents = plan.HasEvents;\n        organization.UseDirectory = plan.HasDirectory;\n        organization.UseTotp = plan.HasTotp;\n        organization.Use2fa = plan.Has2fa;\n        organization.UseApi = plan.HasApi;\n        organization.UseResetPassword = plan.HasResetPassword;\n        organization.SelfHost = plan.HasSelfHost;\n        organization.UsersGetPremium = plan.UsersGetPremium;\n        organization.UseCustomPermissions = plan.HasCustomPermissions;\n        organization.UseScim = plan.HasScim;\n        organization.UseKeyConnector = plan.HasKeyConnector;\n        organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;\n        organization.BillingEmail = provider.BillingEmail!;\n        organization.GatewaySubscriptionId = null;\n        organization.ExpirationDate = null;\n        organization.MaxAutoscaleSeats = null;\n        organization.Status = OrganizationStatusType.Managed;\n\n        var providerOrganization = new ProviderOrganization\n        {\n            ProviderId = provider.Id,\n            OrganizationId = organization.Id,\n            Key = key\n        };\n\n        /*\n         * We have to scale the provider's seats before the ProviderOrganization\n         * row is inserted so the added organization's seats don't get double-counted.\n         */\n        await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value);\n\n        await Task.WhenAll(\n            organizationRepository.ReplaceAsync(organization),\n            providerOrganizationRepository.CreateAsync(providerOrganization)\n        );\n\n        var clientCustomer = await subscriberService.GetCustomer(organization);\n\n        if (clientCustomer.Balance != 0)\n        {\n            await stripeAdapter.CreateCustomerBalanceTransactionAsync(provider.GatewayCustomerId,\n                new CustomerBalanceTransactionCreateOptions\n                {\n                    Amount = clientCustomer.Balance,\n                    Currency = \"USD\",\n                    Description = $\"Unused, prorated time for client organization with ID {organization.Id}.\"\n                });\n        }\n\n        await eventService.LogProviderOrganizationEventAsync(\n            providerOrganization,\n            EventType.ProviderOrganization_Added);\n    }\n\n    public async Task ChangePlan(ChangeProviderPlanCommand command)\n    {\n        var (provider, providerPlanId, newPlanType) = command;\n\n        var providerPlan = await providerPlanRepository.GetByIdAsync(providerPlanId);\n\n        if (providerPlan == null)\n        {\n            throw new BadRequestException(\"Provider plan not found.\");\n        }\n\n        if (providerPlan.PlanType == newPlanType)\n        {\n            return;\n        }\n\n        var subscription = await subscriberService.GetSubscriptionOrThrow(provider);\n\n        var oldPriceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);\n        var newPriceId = ProviderPriceAdapter.GetPriceId(provider, subscription, newPlanType);\n\n        providerPlan.PlanType = newPlanType;\n        await providerPlanRepository.ReplaceAsync(providerPlan);\n\n        var oldSubscriptionItem = subscription.Items.SingleOrDefault(x => x.Price.Id == oldPriceId);\n\n        var updateOptions = new SubscriptionUpdateOptions\n        {\n            Items =\n            [\n                new SubscriptionItemOptions { Price = newPriceId, Quantity = oldSubscriptionItem!.Quantity },\n                new SubscriptionItemOptions { Id = oldSubscriptionItem.Id, Deleted = true }\n            ]\n        };\n\n        await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId, updateOptions);\n\n        // Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)\n        // 1. Retrieve PlanType and PlanName for ProviderPlan\n        // 2. Assign PlanType & PlanName to Organization\n        var providerOrganizations =\n            await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId);\n\n        var newPlan = await pricingClient.GetPlanOrThrow(newPlanType);\n\n        foreach (var providerOrganization in providerOrganizations)\n        {\n            var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);\n            if (organization == null)\n            {\n                throw new ConflictException($\"Organization '{providerOrganization.Id}' not found.\");\n            }\n\n            organization.PlanType = newPlanType;\n            organization.Plan = newPlan.Name;\n            await organizationRepository.ReplaceAsync(organization);\n        }\n    }\n\n    public async Task CreateCustomerForClientOrganization(\n        Provider provider,\n        Organization organization)\n    {\n        ArgumentNullException.ThrowIfNull(provider);\n        ArgumentNullException.ThrowIfNull(organization);\n\n        if (!string.IsNullOrEmpty(organization.GatewayCustomerId))\n        {\n            logger.LogWarning(\"Client organization ({ID}) already has a populated {FieldName}\", organization.Id,\n                nameof(organization.GatewayCustomerId));\n\n            return;\n        }\n\n        var providerCustomer =\n            await subscriberService.GetCustomerOrThrow(provider,\n                new CustomerGetOptions { Expand = [\"tax\", \"tax_ids\"] });\n\n        var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();\n\n        var organizationDisplayName = organization.DisplayName();\n\n        var customerCreateOptions = new CustomerCreateOptions\n        {\n            Address = new AddressOptions\n            {\n                Country = providerCustomer.Address?.Country,\n                PostalCode = providerCustomer.Address?.PostalCode,\n                Line1 = providerCustomer.Address?.Line1,\n                Line2 = providerCustomer.Address?.Line2,\n                City = providerCustomer.Address?.City,\n                State = providerCustomer.Address?.State\n            },\n            Name = organizationDisplayName,\n            Description = $\"{provider.Name} Client Organization\",\n            Email = provider.BillingEmail,\n            InvoiceSettings = new CustomerInvoiceSettingsOptions\n            {\n                CustomFields =\n                [\n                    new CustomerInvoiceSettingsCustomFieldOptions\n                    {\n                        Name = organization.SubscriberType(),\n                        Value = organizationDisplayName.Length <= 30\n                            ? organizationDisplayName\n                            : organizationDisplayName[..30]\n                    }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"region\", globalSettings.BaseServiceUri.CloudRegion } },\n            TaxIdData = providerTaxId == null\n                ? null\n                :\n                [\n                    new CustomerTaxIdDataOptions { Type = providerTaxId.Type, Value = providerTaxId.Value }\n                ]\n        };\n\n        var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(providerCustomer.Address?.Country, providerCustomer.TaxExempt);\n        customerCreateOptions.TaxExempt = providerCustomer switch\n        {\n            { Address.Country: not null and not \"\", TaxExempt: var customerTaxExemptStatus } when\n                determinedTaxExemptStatus != customerTaxExemptStatus => determinedTaxExemptStatus,\n            _ => providerCustomer.TaxExempt\n        };\n\n        var customer = await stripeAdapter.CreateCustomerAsync(customerCreateOptions);\n\n        organization.GatewayCustomerId = customer.Id;\n\n        await organizationRepository.ReplaceAsync(organization);\n    }\n\n    public async Task<byte[]> GenerateClientInvoiceReport(\n        string invoiceId)\n    {\n        ArgumentException.ThrowIfNullOrEmpty(invoiceId);\n\n        var invoiceItems = await providerInvoiceItemRepository.GetByInvoiceId(invoiceId);\n\n        if (invoiceItems.Count == 0)\n        {\n            logger.LogError(\"No provider invoice item records were found for invoice ({InvoiceID})\", invoiceId);\n\n            return null;\n        }\n\n        var csvRows = invoiceItems.Select(ProviderClientInvoiceReportRow.From);\n\n        using var memoryStream = new MemoryStream();\n\n        await using var streamWriter = new StreamWriter(memoryStream);\n\n        await using var csvWriter = new CsvWriter(streamWriter, CultureInfo.CurrentCulture);\n\n        await csvWriter.WriteRecordsAsync(csvRows);\n\n        await streamWriter.FlushAsync();\n\n        memoryStream.Seek(0, SeekOrigin.Begin);\n\n        return memoryStream.ToArray();\n    }\n\n    public async Task<IEnumerable<AddableOrganization>> GetAddableOrganizations(\n        Provider provider,\n        Guid userId)\n    {\n        var providerUser = await providerUserRepository.GetByProviderUserAsync(provider.Id, userId);\n\n        if (providerUser is not { Status: ProviderUserStatusType.Confirmed })\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var candidates = await organizationRepository.GetAddableToProviderByUserIdAsync(userId, provider.Type);\n\n        var active = (await Task.WhenAll(candidates.Select(async organization =>\n            {\n                var subscription = await subscriberService.GetSubscription(organization);\n                return (organization, subscription);\n            })))\n            .Where(pair => pair.subscription is\n            {\n                Status:\n                SubscriptionStatus.Active or\n                SubscriptionStatus.Trialing or\n                SubscriptionStatus.PastDue\n            }).ToList();\n\n        if (active.Count == 0)\n        {\n            return [];\n        }\n\n        return await Task.WhenAll(active.Select(async pair =>\n        {\n            var (organization, _) = pair;\n\n            var planName = await DerivePlanName(provider, organization);\n\n            var addable = new AddableOrganization(\n                organization.Id,\n                organization.Name,\n                planName,\n                organization.Seats!.Value);\n\n            if (providerUser.Type != ProviderUserType.ServiceUser)\n            {\n                return addable;\n            }\n\n            var applicablePlanType = await GetManagedPlanTypeAsync(provider, organization);\n\n            var requiresPurchase =\n                await SeatAdjustmentResultsInPurchase(provider, applicablePlanType, organization.Seats!.Value);\n\n            return addable with { Disabled = requiresPurchase };\n        }));\n\n        async Task<string> DerivePlanName(Provider localProvider, Organization localOrganization)\n        {\n            if (localProvider.Type == ProviderType.Msp)\n            {\n                return localOrganization.PlanType switch\n                {\n                    var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => \"Enterprise\",\n                    var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => \"Teams\",\n                    _ => throw new BillingException()\n                };\n            }\n\n            var plan = await pricingClient.GetPlanOrThrow(localOrganization.PlanType);\n            return plan.Name;\n        }\n    }\n\n    public async Task ScaleSeats(\n        Provider provider,\n        PlanType planType,\n        int seatAdjustment)\n    {\n        var providerPlan = await GetProviderPlanAsync(provider, planType);\n\n        var seatMinimum = providerPlan.SeatMinimum ?? 0;\n\n        var currentlyAssignedSeatTotal = await GetAssignedSeatTotalAsync(provider, planType);\n\n        var newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment;\n\n        var scaleQuantityTo = CurrySeatScalingUpdate(\n            provider,\n            providerPlan,\n            newlyAssignedSeatTotal);\n\n        /*\n         * Below the limit => Below the limit:\n         * No subscription update required. We can safely update the provider's allocated seats.\n         */\n        if (currentlyAssignedSeatTotal <= seatMinimum &&\n            newlyAssignedSeatTotal <= seatMinimum)\n        {\n            providerPlan.AllocatedSeats = newlyAssignedSeatTotal;\n\n            await providerPlanRepository.ReplaceAsync(providerPlan);\n        }\n        /*\n         * Below the limit => Above the limit:\n         * We have to scale the subscription up from the seat minimum to the newly assigned seat total.\n         */\n        else if (currentlyAssignedSeatTotal <= seatMinimum &&\n                 newlyAssignedSeatTotal > seatMinimum)\n        {\n            await scaleQuantityTo(newlyAssignedSeatTotal);\n        }\n        /*\n         * Above the limit => Above the limit:\n         * We have to scale the subscription from the currently assigned seat total to the newly assigned seat total.\n         */\n        else if (currentlyAssignedSeatTotal > seatMinimum &&\n                 newlyAssignedSeatTotal > seatMinimum)\n        {\n            await scaleQuantityTo(newlyAssignedSeatTotal);\n        }\n        /*\n         * Above the limit => Below the limit:\n         * We have to scale the subscription down from the currently assigned seat total to the seat minimum.\n         */\n        else if (currentlyAssignedSeatTotal > seatMinimum &&\n                 newlyAssignedSeatTotal <= seatMinimum)\n        {\n            await scaleQuantityTo(seatMinimum);\n        }\n    }\n\n    public async Task<bool> SeatAdjustmentResultsInPurchase(\n        Provider provider,\n        PlanType planType,\n        int seatAdjustment)\n    {\n        var providerPlan = await GetProviderPlanAsync(provider, planType);\n\n        var seatMinimum = providerPlan.SeatMinimum;\n\n        var currentlyAssignedSeatTotal = await GetAssignedSeatTotalAsync(provider, planType);\n\n        var newlyAssignedSeatTotal = currentlyAssignedSeatTotal + seatAdjustment;\n\n        return\n            // Below the limit to above the limit\n            (currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) ||\n            // Above the limit to further above the limit\n            (currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum &&\n             newlyAssignedSeatTotal > currentlyAssignedSeatTotal);\n    }\n\n    public async Task<Customer> SetupCustomer(\n        Provider provider,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(billingAddress.Country);\n        var options = new CustomerCreateOptions\n        {\n            Address = new AddressOptions\n            {\n                Country = billingAddress.Country,\n                PostalCode = billingAddress.PostalCode,\n                Line1 = billingAddress.Line1,\n                Line2 = billingAddress.Line2,\n                City = billingAddress.City,\n                State = billingAddress.State\n            },\n            Description = provider.DisplayBusinessName(),\n            Email = provider.BillingEmail,\n            InvoiceSettings = new CustomerInvoiceSettingsOptions\n            {\n                CustomFields =\n                [\n                    new CustomerInvoiceSettingsCustomFieldOptions\n                    {\n                        Name = provider.SubscriberType(),\n                        Value = provider.DisplayName()?.Length <= 30\n                            ? provider.DisplayName()\n                            : provider.DisplayName()?[..30]\n                    }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"region\", globalSettings.BaseServiceUri.CloudRegion } },\n            TaxExempt = determinedTaxExemptStatus\n        };\n\n        if (billingAddress.TaxId != null)\n        {\n            options.TaxIdData =\n            [\n                new CustomerTaxIdDataOptions { Type = billingAddress.TaxId.Code, Value = billingAddress.TaxId.Value }\n            ];\n\n            if (billingAddress.TaxId.Code == TaxIdType.SpanishNIF)\n            {\n                options.TaxIdData.Add(new CustomerTaxIdDataOptions\n                {\n                    Type = TaxIdType.EUVAT,\n                    Value = $\"ES{billingAddress.TaxId.Value}\"\n                });\n            }\n        }\n\n        var braintreeCustomerId = \"\";\n        var setupIntentId = \"\";\n\n        // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault\n        switch (paymentMethod.Type)\n        {\n            case TokenizablePaymentMethodType.BankAccount:\n                {\n                    var setupIntent =\n                        (await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions\n                        {\n                            PaymentMethod = paymentMethod.Token\n                        }))\n                        .FirstOrDefault();\n\n                    if (setupIntent == null)\n                    {\n                        logger.LogError(\n                            \"Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account\",\n                            provider.Id);\n                        throw new BillingException();\n                    }\n\n                    setupIntentId = setupIntent.Id;\n                    break;\n                }\n            case TokenizablePaymentMethodType.Card:\n                {\n                    options.PaymentMethod = paymentMethod.Token;\n                    options.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token;\n                    break;\n                }\n            case TokenizablePaymentMethodType.PayPal:\n                {\n                    braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, paymentMethod.Token);\n                    options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;\n                    break;\n                }\n        }\n\n        try\n        {\n            var customer = await stripeAdapter.CreateCustomerAsync(options);\n\n            if (!string.IsNullOrEmpty(setupIntentId))\n            {\n                await stripeAdapter.UpdateSetupIntentAsync(setupIntentId,\n                    new SetupIntentUpdateOptions { Customer = customer.Id });\n            }\n\n            return customer;\n        }\n        catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.TaxIdInvalid)\n        {\n            await Revert();\n            throw new BadRequestException(\n                \"Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.\");\n        }\n        catch\n        {\n            await Revert();\n            throw;\n        }\n\n        async Task Revert()\n        {\n            // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault\n            switch (paymentMethod.Type)\n            {\n                case TokenizablePaymentMethodType.BankAccount when !string.IsNullOrEmpty(setupIntentId):\n                    {\n                        await stripeAdapter.CancelSetupIntentAsync(setupIntentId,\n                            new SetupIntentCancelOptions { CancellationReason = \"abandoned\" });\n                        break;\n                    }\n                case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):\n                    {\n                        await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);\n                        break;\n                    }\n            }\n        }\n    }\n\n    public async Task<Subscription> SetupSubscription(\n        Provider provider)\n    {\n        ArgumentNullException.ThrowIfNull(provider);\n\n        var customerGetOptions = new CustomerGetOptions { Expand = [\"tax\", \"tax_ids\"] };\n        var customer = await subscriberService.GetCustomerOrThrow(provider, customerGetOptions);\n\n        var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);\n\n        if (providerPlans.Count == 0)\n        {\n            logger.LogError(\"Cannot start subscription for provider ({ProviderID}) that has no configured plans\",\n                provider.Id);\n\n            throw new BillingException();\n        }\n\n        var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();\n\n        foreach (var providerPlan in providerPlans)\n        {\n            var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);\n\n            if (!providerPlan.IsConfigured())\n            {\n                logger.LogError(\n                    \"Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan\",\n                    provider.Id, plan.Name);\n                throw new BillingException();\n            }\n\n            var priceId = ProviderPriceAdapter.GetActivePriceId(provider, providerPlan.PlanType);\n\n            subscriptionItemOptionsList.Add(new SubscriptionItemOptions\n            {\n                Price = priceId,\n                Quantity = providerPlan.SeatMinimum\n            });\n        }\n\n        var setupIntents = await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions\n        {\n            Customer = customer.Id,\n            Expand = [\"data.payment_method\"]\n        });\n\n        var hasUnverifiedBankAccount = setupIntents?.Any(si => si.IsUnverifiedBankAccount()) ?? false;\n\n        var usePaymentMethod =\n            !string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) ||\n            customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true ||\n            hasUnverifiedBankAccount;\n\n        int? trialPeriodDays = provider.Type switch\n        {\n            ProviderType.Msp when usePaymentMethod => 14,\n            ProviderType.BusinessUnit when usePaymentMethod => 4,\n            _ => null\n        };\n\n        var subscriptionCreateOptions = new SubscriptionCreateOptions\n        {\n            CollectionMethod =\n                usePaymentMethod\n                    ? CollectionMethod.ChargeAutomatically\n                    : CollectionMethod.SendInvoice,\n            Customer = customer.Id,\n            DaysUntilDue = usePaymentMethod ? null : 30,\n            Discounts = !string.IsNullOrEmpty(provider.DiscountId) ? [new SubscriptionDiscountOptions { Coupon = provider.DiscountId }] : null,\n            Items = subscriptionItemOptionsList,\n            Metadata = new Dictionary<string, string> { { \"providerId\", provider.Id.ToString() } },\n            OffSession = true,\n            ProrationBehavior = ProrationBehavior.CreateProrations,\n            TrialPeriodDays = trialPeriodDays,\n            AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }\n        };\n\n        try\n        {\n            var subscription = await stripeAdapter.CreateSubscriptionAsync(subscriptionCreateOptions);\n\n            if (subscription is\n                {\n                    Status: SubscriptionStatus.Active or SubscriptionStatus.Trialing\n                })\n            {\n                return subscription;\n            }\n\n            logger.LogError(\n                \"Newly created provider ({ProviderID}) subscription ({SubscriptionID}) has inactive status: {Status}\",\n                provider.Id,\n                subscription.Id,\n                subscription.Status);\n\n            throw new BillingException();\n        }\n        catch (StripeException stripeException) when (stripeException.StripeError?.Code ==\n                                                      ErrorCodes.CustomerTaxLocationInvalid)\n        {\n            throw new BadRequestException(\n                \"Your location wasn't recognized. Please ensure your country and postal code are valid.\");\n        }\n    }\n\n    public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)\n    {\n        var (provider, updatedPlanConfigurations) = command;\n\n        if (updatedPlanConfigurations.Any(x => x.SeatsMinimum < 0))\n        {\n            throw new BadRequestException(\"Provider seat minimums must be at least 0.\");\n        }\n\n        var subscription = await subscriberService.GetSubscriptionOrThrow(provider);\n\n        var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();\n\n        var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);\n\n        foreach (var updatedPlanConfiguration in updatedPlanConfigurations)\n        {\n            var (updatedPlanType, updatedSeatMinimum) = updatedPlanConfiguration;\n\n            var providerPlan =\n                providerPlans.Single(providerPlan => providerPlan.PlanType == updatedPlanType);\n\n            if (providerPlan.SeatMinimum != updatedSeatMinimum)\n            {\n                var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, updatedPlanType);\n\n                var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId);\n\n                if (providerPlan.PurchasedSeats == 0)\n                {\n                    if (providerPlan.AllocatedSeats > updatedSeatMinimum)\n                    {\n                        providerPlan.PurchasedSeats = providerPlan.AllocatedSeats - updatedSeatMinimum;\n\n                        subscriptionItemOptionsList.Add(new SubscriptionItemOptions\n                        {\n                            Id = subscriptionItem.Id,\n                            Price = priceId,\n                            Quantity = providerPlan.AllocatedSeats\n                        });\n                    }\n                    else\n                    {\n                        subscriptionItemOptionsList.Add(new SubscriptionItemOptions\n                        {\n                            Id = subscriptionItem.Id,\n                            Price = priceId,\n                            Quantity = updatedSeatMinimum\n                        });\n                    }\n                }\n                else\n                {\n                    var totalSeats = providerPlan.SeatMinimum + providerPlan.PurchasedSeats;\n\n                    if (updatedSeatMinimum <= totalSeats)\n                    {\n                        providerPlan.PurchasedSeats = totalSeats - updatedSeatMinimum;\n                    }\n                    else\n                    {\n                        providerPlan.PurchasedSeats = 0;\n                        subscriptionItemOptionsList.Add(new SubscriptionItemOptions\n                        {\n                            Id = subscriptionItem.Id,\n                            Price = priceId,\n                            Quantity = updatedSeatMinimum\n                        });\n                    }\n                }\n\n                providerPlan.SeatMinimum = updatedSeatMinimum;\n\n                await providerPlanRepository.ReplaceAsync(providerPlan);\n            }\n        }\n\n        if (subscriptionItemOptionsList.Count > 0)\n        {\n            await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId,\n                new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });\n        }\n    }\n\n    public async Task UpdateProviderNameAndEmail(Provider provider)\n    {\n        if (string.IsNullOrWhiteSpace(provider.GatewayCustomerId))\n        {\n            logger.LogWarning(\n                \"Provider ({ProviderId}) has no Stripe customer to update\",\n                provider.Id);\n            return;\n        }\n\n        var newDisplayName = provider.DisplayName();\n\n        // Provider.DisplayName() can return null - handle gracefully\n        if (string.IsNullOrWhiteSpace(newDisplayName))\n        {\n            logger.LogWarning(\n                \"Provider ({ProviderId}) has no name to update in Stripe\",\n                provider.Id);\n            return;\n        }\n\n        await stripeAdapter.UpdateCustomerAsync(provider.GatewayCustomerId,\n            new CustomerUpdateOptions\n            {\n                Email = provider.BillingEmail,\n                Description = newDisplayName,\n                InvoiceSettings = new CustomerInvoiceSettingsOptions\n                {\n                    CustomFields = [\n                        new CustomerInvoiceSettingsCustomFieldOptions\n                        {\n                            Name = provider.SubscriberType(),\n                            Value = newDisplayName\n                        }]\n                },\n            });\n    }\n\n    private Func<int, Task> CurrySeatScalingUpdate(\n        Provider provider,\n        ProviderPlan providerPlan,\n        int newlyAssignedSeats) => async newlySubscribedSeats =>\n    {\n        var subscription = await subscriberService.GetSubscriptionOrThrow(provider);\n\n        var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);\n\n        var item = subscription.Items.First(item => item.Price.Id == priceId);\n\n        await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions\n        {\n            Items =\n            [\n                new SubscriptionItemOptions { Id = item.Id, Price = priceId, Quantity = newlySubscribedSeats }\n            ]\n        });\n\n        var newlyPurchasedSeats = newlySubscribedSeats > providerPlan.SeatMinimum\n            ? newlySubscribedSeats - providerPlan.SeatMinimum\n            : 0;\n\n        providerPlan.PurchasedSeats = newlyPurchasedSeats;\n        providerPlan.AllocatedSeats = newlyAssignedSeats;\n\n        await providerPlanRepository.ReplaceAsync(providerPlan);\n    };\n\n    // TODO: Replace with SPROC\n    private async Task<int> GetAssignedSeatTotalAsync(Provider provider, PlanType planType)\n    {\n        var providerOrganizations =\n            await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id);\n\n        var plan = await pricingClient.GetPlanOrThrow(planType);\n\n        return providerOrganizations\n            .Where(providerOrganization => providerOrganization.Plan == plan.Name &&\n                                           providerOrganization.Status == OrganizationStatusType.Managed)\n            .Sum(providerOrganization => providerOrganization.Seats ?? 0);\n    }\n\n    // TODO: Replace with SPROC\n    private async Task<ProviderPlan> GetProviderPlanAsync(Provider provider, PlanType planType)\n    {\n        var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);\n\n        var providerPlan = providerPlans.FirstOrDefault(x => x.PlanType == planType);\n\n        if (providerPlan == null || !providerPlan.IsConfigured())\n        {\n            throw new BillingException(message: \"Provider plan is missing or misconfigured\");\n        }\n\n        return providerPlan;\n    }\n\n    private async Task<PlanType> GetManagedPlanTypeAsync(\n        Provider provider,\n        Organization organization)\n    {\n        if (provider.Type == ProviderType.BusinessUnit)\n        {\n            return (await providerPlanRepository.GetByProviderId(provider.Id)).First().PlanType;\n        }\n\n        return organization.PlanType switch\n        {\n            var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => PlanType.TeamsMonthly,\n            var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => PlanType.EnterpriseMonthly,\n            _ => throw new BillingException()\n        };\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/Commercial.Core.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"CsvHelper\" Version=\"33.1.0\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandler.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\npublic class\n    ProjectPeopleAccessPoliciesAuthorizationHandler : AuthorizationHandler<\n        ProjectPeopleAccessPoliciesOperationRequirement,\n        ProjectPeopleAccessPolicies>\n{\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly IProjectRepository _projectRepository;\n    private readonly ISameOrganizationQuery _sameOrganizationQuery;\n\n    public ProjectPeopleAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,\n        IAccessClientQuery accessClientQuery,\n        ISameOrganizationQuery sameOrganizationQuery,\n        IProjectRepository projectRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _sameOrganizationQuery = sameOrganizationQuery;\n        _projectRepository = projectRepository;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        ProjectPeopleAccessPoliciesOperationRequirement requirement,\n        ProjectPeopleAccessPolicies resource)\n    {\n        if (!_currentContext.AccessSecretsManager(resource.OrganizationId))\n        {\n            return;\n        }\n\n        // Only users and admins should be able to manipulate access policies\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)\n        {\n            return;\n        }\n\n        switch (requirement)\n        {\n            case not null when requirement == ProjectPeopleAccessPoliciesOperations.Replace:\n                await CanReplaceProjectPeopleAsync(context, requirement, resource, accessClient, userId);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported operation requirement type provided.\",\n                    nameof(requirement));\n        }\n    }\n\n    private async Task CanReplaceProjectPeopleAsync(AuthorizationHandlerContext context,\n        ProjectPeopleAccessPoliciesOperationRequirement requirement, ProjectPeopleAccessPolicies resource,\n        AccessClientType accessClient, Guid userId)\n    {\n        var access = await _projectRepository.AccessToProjectAsync(resource.Id, userId, accessClient);\n        if (access.Write)\n        {\n            if (resource.UserAccessPolicies != null && resource.UserAccessPolicies.Any())\n            {\n                var orgUserIds = resource.UserAccessPolicies.Select(ap => ap.OrganizationUserId!.Value).ToList();\n                if (!await _sameOrganizationQuery.OrgUsersInTheSameOrgAsync(orgUserIds, resource.OrganizationId))\n                {\n                    return;\n                }\n            }\n\n            if (resource.GroupAccessPolicies != null && resource.GroupAccessPolicies.Any())\n            {\n                var groupIds = resource.GroupAccessPolicies.Select(ap => ap.GroupId!.Value).ToList();\n                if (!await _sameOrganizationQuery.GroupsInTheSameOrgAsync(groupIds, resource.OrganizationId))\n                {\n                    return;\n                }\n            }\n\n            context.Succeed(requirement);\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\npublic class ProjectServiceAccountsAccessPoliciesAuthorizationHandler : AuthorizationHandler<\n    ProjectServiceAccountsAccessPoliciesOperationRequirement,\n    ProjectServiceAccountsAccessPoliciesUpdates>\n{\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public ProjectServiceAccountsAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,\n        IAccessClientQuery accessClientQuery,\n        IProjectRepository projectRepository,\n        IServiceAccountRepository serviceAccountRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _serviceAccountRepository = serviceAccountRepository;\n        _projectRepository = projectRepository;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,\n        ProjectServiceAccountsAccessPoliciesUpdates resource)\n    {\n        if (!_currentContext.AccessSecretsManager(resource.OrganizationId))\n        {\n            return;\n        }\n\n        // Only users and admins should be able to manipulate access policies\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)\n        {\n            return;\n        }\n\n        switch (requirement)\n        {\n            case not null when requirement == ProjectServiceAccountsAccessPoliciesOperations.Updates:\n                await CanUpdateAsync(context, requirement, resource, accessClient,\n                    userId);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported operation requirement type provided.\",\n                    nameof(requirement));\n        }\n    }\n\n    private async Task CanUpdateAsync(AuthorizationHandlerContext context,\n        ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        AccessClientType accessClient, Guid userId)\n    {\n        var access =\n            await _projectRepository.AccessToProjectAsync(resource.ProjectId, userId,\n                accessClient);\n        if (!access.Write)\n        {\n            return;\n        }\n\n        var serviceAccountIds = resource.ServiceAccountAccessPolicyUpdates.Select(update =>\n            update.AccessPolicy.ServiceAccountId!.Value).ToList();\n\n        var inSameOrganization =\n            await _serviceAccountRepository.ServiceAccountsAreInOrganizationAsync(serviceAccountIds,\n                resource.OrganizationId);\n        if (!inSameOrganization)\n        {\n            return;\n        }\n\n        // Users can only create access policies for service accounts they have access to.\n        // User can delete and update any service account access policy if they have write access to the project.\n        var serviceAccountIdsToCheck = resource.ServiceAccountAccessPolicyUpdates\n            .Where(update => update.Operation == AccessPolicyOperation.Create).Select(update =>\n                update.AccessPolicy.ServiceAccountId!.Value).ToList();\n\n        if (serviceAccountIdsToCheck.Count == 0)\n        {\n            context.Succeed(requirement);\n            return;\n        }\n\n        var serviceAccountsAccess =\n            await _serviceAccountRepository.AccessToServiceAccountsAsync(serviceAccountIdsToCheck, userId,\n                accessClient);\n        if (serviceAccountsAccess.Count == serviceAccountIdsToCheck.Count &&\n            serviceAccountsAccess.All(a => a.Value.Write))\n        {\n            context.Succeed(requirement);\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/SecretAccessPoliciesUpdatesAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\npublic class SecretAccessPoliciesUpdatesAuthorizationHandler : AuthorizationHandler<\n    SecretAccessPoliciesOperationRequirement,\n    SecretAccessPoliciesUpdates>\n{\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly ISameOrganizationQuery _sameOrganizationQuery;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public SecretAccessPoliciesUpdatesAuthorizationHandler(ICurrentContext currentContext,\n        IAccessClientQuery accessClientQuery,\n        ISecretRepository secretRepository,\n        ISameOrganizationQuery sameOrganizationQuery,\n        IServiceAccountRepository serviceAccountRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _sameOrganizationQuery = sameOrganizationQuery;\n        _serviceAccountRepository = serviceAccountRepository;\n        _secretRepository = secretRepository;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        SecretAccessPoliciesOperationRequirement requirement,\n        SecretAccessPoliciesUpdates resource)\n    {\n        if (!_currentContext.AccessSecretsManager(resource.OrganizationId))\n        {\n            return;\n        }\n\n        // Only users and admins should be able to manipulate access policies\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)\n        {\n            return;\n        }\n\n        switch (requirement)\n        {\n            case not null when requirement == SecretAccessPoliciesOperations.Updates:\n                await CanUpdateAsync(context, requirement, resource, accessClient,\n                    userId);\n                break;\n            case not null when requirement == SecretAccessPoliciesOperations.Create:\n                await CanCreateAsync(context, requirement, resource, accessClient,\n                    userId);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported operation requirement type provided.\",\n                    nameof(requirement));\n        }\n    }\n\n    private async Task CanUpdateAsync(AuthorizationHandlerContext context,\n        SecretAccessPoliciesOperationRequirement requirement,\n        SecretAccessPoliciesUpdates resource,\n        AccessClientType accessClient, Guid userId)\n    {\n        var access = await _secretRepository\n            .AccessToSecretAsync(resource.SecretId, userId, accessClient);\n        if (!access.Write)\n        {\n            return;\n        }\n\n        if (!await GranteesInTheSameOrganizationAsync(resource))\n        {\n            return;\n        }\n\n        // Users can only create access policies for service accounts they have access to.\n        // User can delete and update any service account access policy if they have write access to the secret.\n        if (await HasAccessToTargetServiceAccountsAsync(resource, accessClient, userId))\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanCreateAsync(AuthorizationHandlerContext context,\n        SecretAccessPoliciesOperationRequirement requirement,\n        SecretAccessPoliciesUpdates resource,\n        AccessClientType accessClient, Guid userId)\n    {\n        if (resource.UserAccessPolicyUpdates.Any(x => x.Operation != AccessPolicyOperation.Create) ||\n            resource.GroupAccessPolicyUpdates.Any(x => x.Operation != AccessPolicyOperation.Create) ||\n            resource.ServiceAccountAccessPolicyUpdates.Any(x => x.Operation != AccessPolicyOperation.Create))\n        {\n            return;\n        }\n\n        if (!await GranteesInTheSameOrganizationAsync(resource))\n        {\n            return;\n        }\n\n        // Users can only create access policies for service accounts they have access to.\n        if (await HasAccessToTargetServiceAccountsAsync(resource, accessClient, userId))\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task<bool> GranteesInTheSameOrganizationAsync(SecretAccessPoliciesUpdates resource)\n    {\n        var organizationUserIds = resource.UserAccessPolicyUpdates.Select(update =>\n            update.AccessPolicy.OrganizationUserId!.Value).ToList();\n        var groupIds = resource.GroupAccessPolicyUpdates.Select(update =>\n            update.AccessPolicy.GroupId!.Value).ToList();\n        var serviceAccountIds = resource.ServiceAccountAccessPolicyUpdates.Select(update =>\n            update.AccessPolicy.ServiceAccountId!.Value).ToList();\n\n        var usersInSameOrg = organizationUserIds.Count == 0 ||\n                             await _sameOrganizationQuery.OrgUsersInTheSameOrgAsync(organizationUserIds,\n                                 resource.OrganizationId);\n\n        var groupsInSameOrg = groupIds.Count == 0 ||\n                              await _sameOrganizationQuery.GroupsInTheSameOrgAsync(groupIds, resource.OrganizationId);\n\n        var serviceAccountsInSameOrg = serviceAccountIds.Count == 0 ||\n                                       await _serviceAccountRepository.ServiceAccountsAreInOrganizationAsync(\n                                           serviceAccountIds,\n                                           resource.OrganizationId);\n\n        return usersInSameOrg && groupsInSameOrg && serviceAccountsInSameOrg;\n    }\n\n    private async Task<bool> HasAccessToTargetServiceAccountsAsync(SecretAccessPoliciesUpdates resource,\n        AccessClientType accessClient, Guid userId)\n    {\n        var serviceAccountIdsToCheck = resource.ServiceAccountAccessPolicyUpdates\n            .Where(update => update.Operation == AccessPolicyOperation.Create).Select(update =>\n                update.AccessPolicy.ServiceAccountId!.Value).ToList();\n\n        if (serviceAccountIdsToCheck.Count == 0)\n        {\n            return true;\n        }\n\n        var serviceAccountsAccess =\n            await _serviceAccountRepository.AccessToServiceAccountsAsync(serviceAccountIdsToCheck, userId,\n                accessClient);\n\n        return serviceAccountsAccess.Count == serviceAccountIdsToCheck.Count &&\n               serviceAccountsAccess.All(a => a.Value.Write);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\npublic class ServiceAccountGrantedPoliciesAuthorizationHandler : AuthorizationHandler<\n    ServiceAccountGrantedPoliciesOperationRequirement,\n    ServiceAccountGrantedPoliciesUpdates>\n{\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public ServiceAccountGrantedPoliciesAuthorizationHandler(ICurrentContext currentContext,\n        IAccessClientQuery accessClientQuery,\n        IProjectRepository projectRepository,\n        IServiceAccountRepository serviceAccountRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _serviceAccountRepository = serviceAccountRepository;\n        _projectRepository = projectRepository;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        ServiceAccountGrantedPoliciesOperationRequirement requirement,\n        ServiceAccountGrantedPoliciesUpdates resource)\n    {\n        if (!_currentContext.AccessSecretsManager(resource.OrganizationId))\n        {\n            return;\n        }\n\n        // Only users and admins should be able to manipulate access policies\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)\n        {\n            return;\n        }\n\n        switch (requirement)\n        {\n            case not null when requirement == ServiceAccountGrantedPoliciesOperations.Updates:\n                await CanUpdateAsync(context, requirement, resource, accessClient,\n                    userId);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported operation requirement type provided.\",\n                    nameof(requirement));\n        }\n    }\n\n    private async Task CanUpdateAsync(AuthorizationHandlerContext context,\n        ServiceAccountGrantedPoliciesOperationRequirement requirement, ServiceAccountGrantedPoliciesUpdates resource,\n        AccessClientType accessClient, Guid userId)\n    {\n        var access =\n            await _serviceAccountRepository.AccessToServiceAccountAsync(resource.ServiceAccountId, userId,\n                accessClient);\n        if (access.Write)\n        {\n            var projectIdsToCheck = resource.ProjectGrantedPolicyUpdates.Select(update =>\n                update.AccessPolicy.GrantedProjectId!.Value).ToList();\n\n            var sameOrganization =\n                await _projectRepository.ProjectsAreInOrganization(projectIdsToCheck, resource.OrganizationId);\n            if (!sameOrganization)\n            {\n                return;\n            }\n\n            var projectsAccess =\n                await _projectRepository.AccessToProjectsAsync(projectIdsToCheck, userId, accessClient);\n            if (projectsAccess.Count == projectIdsToCheck.Count && projectsAccess.All(a => a.Value.Write))\n            {\n                context.Succeed(requirement);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountPeopleAccessPoliciesAuthorizationHandler.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\npublic class\n    ServiceAccountPeopleAccessPoliciesAuthorizationHandler : AuthorizationHandler<\n        ServiceAccountPeopleAccessPoliciesOperationRequirement,\n        ServiceAccountPeopleAccessPolicies>\n{\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly ISameOrganizationQuery _sameOrganizationQuery;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public ServiceAccountPeopleAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,\n        IAccessClientQuery accessClientQuery,\n        ISameOrganizationQuery sameOrganizationQuery,\n        IServiceAccountRepository serviceAccountRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _sameOrganizationQuery = sameOrganizationQuery;\n        _serviceAccountRepository = serviceAccountRepository;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        ServiceAccountPeopleAccessPoliciesOperationRequirement requirement,\n        ServiceAccountPeopleAccessPolicies resource)\n    {\n        if (!_currentContext.AccessSecretsManager(resource.OrganizationId))\n        {\n            return;\n        }\n\n        // Only users and admins should be able to manipulate access policies\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)\n        {\n            return;\n        }\n\n        switch (requirement)\n        {\n            case not null when requirement == ServiceAccountPeopleAccessPoliciesOperations.Replace:\n                await CanReplaceServiceAccountPeopleAsync(context, requirement, resource, accessClient, userId);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported operation requirement type provided.\",\n                    nameof(requirement));\n        }\n    }\n\n    private async Task CanReplaceServiceAccountPeopleAsync(AuthorizationHandlerContext context,\n        ServiceAccountPeopleAccessPoliciesOperationRequirement requirement, ServiceAccountPeopleAccessPolicies resource,\n        AccessClientType accessClient, Guid userId)\n    {\n        var access = await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId, accessClient);\n        if (access.Write)\n        {\n            if (resource.UserAccessPolicies != null && resource.UserAccessPolicies.Any())\n            {\n                var orgUserIds = resource.UserAccessPolicies.Select(ap => ap.OrganizationUserId!.Value).ToList();\n                if (!await _sameOrganizationQuery.OrgUsersInTheSameOrgAsync(orgUserIds, resource.OrganizationId))\n                {\n                    return;\n                }\n            }\n\n            if (resource.GroupAccessPolicies != null && resource.GroupAccessPolicies.Any())\n            {\n                var groupIds = resource.GroupAccessPolicies.Select(ap => ap.GroupId!.Value).ToList();\n                if (!await _sameOrganizationQuery.GroupsInTheSameOrgAsync(groupIds, resource.OrganizationId))\n                {\n                    return;\n                }\n            }\n\n            context.Succeed(requirement);\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Projects/ProjectAuthorizationHandler.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects;\n\npublic class ProjectAuthorizationHandler : AuthorizationHandler<ProjectOperationRequirement, Project>\n{\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly IProjectRepository _projectRepository;\n\n    public ProjectAuthorizationHandler(ICurrentContext currentContext, IAccessClientQuery accessClientQuery,\n        IProjectRepository projectRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _projectRepository = projectRepository;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        ProjectOperationRequirement requirement,\n        Project resource)\n    {\n        if (!_currentContext.AccessSecretsManager(resource.OrganizationId))\n        {\n            return;\n        }\n\n        switch (requirement)\n        {\n            case not null when requirement == ProjectOperations.Create:\n                await CanCreateProjectAsync(context, requirement, resource);\n                break;\n            case not null when requirement == ProjectOperations.Update:\n                await CanUpdateProjectAsync(context, requirement, resource);\n                break;\n            case not null when requirement == ProjectOperations.Delete:\n                await CanDeleteProjectAsync(context, requirement, resource);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported operation requirement type provided.\", nameof(requirement));\n        }\n    }\n\n    private async Task CanCreateProjectAsync(AuthorizationHandlerContext context,\n        ProjectOperationRequirement requirement, Project resource)\n    {\n        var (accessClient, _) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var hasAccess = accessClient switch\n        {\n            AccessClientType.NoAccessCheck => true,\n            AccessClientType.User => true,\n            AccessClientType.ServiceAccount => true,\n            _ => false,\n        };\n\n        if (hasAccess)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanUpdateProjectAsync(AuthorizationHandlerContext context,\n        ProjectOperationRequirement requirement, Project resource)\n    {\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n\n        var access = await _projectRepository.AccessToProjectAsync(resource.Id, userId, accessClient);\n\n        if (access.Write)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanDeleteProjectAsync(AuthorizationHandlerContext context,\n        ProjectOperationRequirement requirement, Project resource)\n    {\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n\n        var access = await _projectRepository.AccessToProjectAsync(resource.Id, userId, accessClient);\n\n        if (access.Write)\n        {\n            context.Succeed(requirement);\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/BulkSecretAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;\n\npublic class\n    BulkSecretAuthorizationHandler : AuthorizationHandler<BulkSecretOperationRequirement, IReadOnlyList<Secret>>\n{\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly ISecretRepository _secretRepository;\n\n    public BulkSecretAuthorizationHandler(ICurrentContext currentContext, IAccessClientQuery accessClientQuery,\n        ISecretRepository secretRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _secretRepository = secretRepository;\n    }\n\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        BulkSecretOperationRequirement requirement,\n        IReadOnlyList<Secret> resources)\n    {\n        // Ensure all secrets belong to the same organization.\n        var organizationId = resources[0].OrganizationId;\n        if (resources.Any(secret => secret.OrganizationId != organizationId) ||\n            !_currentContext.AccessSecretsManager(organizationId))\n        {\n            return;\n        }\n\n        switch (requirement)\n        {\n            case not null when requirement == BulkSecretOperations.ReadAll:\n                await CanReadAllAsync(context, requirement, resources, organizationId);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported operation requirement type provided.\", nameof(requirement));\n        }\n    }\n\n    private async Task CanReadAllAsync(AuthorizationHandlerContext context,\n        BulkSecretOperationRequirement requirement, IReadOnlyList<Secret> resources, Guid organizationId)\n    {\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, organizationId);\n\n        var secretsAccess =\n            await _secretRepository.AccessToSecretsAsync(resources.Select(s => s.Id), userId, accessClient);\n\n        if (secretsAccess.Count == resources.Count &&\n            secretsAccess.All(a => a.Value.Read))\n        {\n            context.Succeed(requirement);\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandler.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;\n\npublic class SecretAuthorizationHandler : AuthorizationHandler<SecretOperationRequirement, Secret>\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly IProjectRepository _projectRepository;\n    private readonly ISecretRepository _secretRepository;\n\n    public SecretAuthorizationHandler(ICurrentContext currentContext, IAccessClientQuery accessClientQuery,\n        IProjectRepository projectRepository, ISecretRepository secretRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _projectRepository = projectRepository;\n        _secretRepository = secretRepository;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        SecretOperationRequirement requirement,\n        Secret resource)\n    {\n        if (!_currentContext.AccessSecretsManager(resource.OrganizationId))\n        {\n            return;\n        }\n\n        switch (requirement)\n        {\n            case not null when requirement == SecretOperations.Create:\n                await CanCreateSecretAsync(context, requirement, resource);\n                break;\n            case not null when requirement == SecretOperations.Read:\n                await CanReadSecretAsync(context, requirement, resource);\n                break;\n            case not null when requirement == SecretOperations.Update:\n                await CanUpdateSecretAsync(context, requirement, resource);\n                break;\n            case not null when requirement == SecretOperations.Delete:\n                await CanDeleteSecretAsync(context, requirement, resource);\n                break;\n            case not null when requirement == SecretOperations.ReadAccessPolicies:\n                await CanReadAccessPoliciesAsync(context, requirement, resource);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported operation requirement type provided.\", nameof(requirement));\n        }\n    }\n\n    private async Task CanCreateSecretAsync(AuthorizationHandlerContext context,\n        SecretOperationRequirement requirement, Secret resource)\n    {\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var project = resource.Projects?.FirstOrDefault();\n\n        if (project == null && accessClient != AccessClientType.NoAccessCheck)\n        {\n            return;\n        }\n\n        // All projects should be apart of the same organization\n        if (resource.Projects != null\n            && resource.Projects.Any()\n            && !await _projectRepository.ProjectsAreInOrganization(resource.Projects.Select(p => p.Id).ToList(),\n                resource.OrganizationId))\n        {\n            return;\n        }\n\n        var hasAccess = accessClient switch\n        {\n            AccessClientType.NoAccessCheck => true,\n            AccessClientType.User => (await _projectRepository.AccessToProjectAsync(project!.Id, userId, accessClient))\n                .Write,\n            AccessClientType.ServiceAccount => (await _projectRepository.AccessToProjectAsync(project!.Id, userId, accessClient))\n                .Write,\n            _ => false,\n        };\n\n        if (hasAccess)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanReadSecretAsync(AuthorizationHandlerContext context,\n        SecretOperationRequirement requirement, Secret resource)\n    {\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n\n        var access = await _secretRepository.AccessToSecretAsync(resource.Id, userId, accessClient);\n\n        if (access.Read)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanUpdateSecretAsync(AuthorizationHandlerContext context,\n        SecretOperationRequirement requirement, Secret resource)\n    {\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n\n        // All projects should be in the same organization\n        if (resource.Projects != null\n            && resource.Projects.Count != 0\n            && !await _projectRepository.ProjectsAreInOrganization(resource.Projects.Select(p => p.Id).ToList(),\n                resource.OrganizationId))\n        {\n            return;\n        }\n\n        bool hasAccess;\n\n        switch (accessClient)\n        {\n            case AccessClientType.NoAccessCheck:\n                hasAccess = true;\n                break;\n            case AccessClientType.User:\n                hasAccess = await GetAccessToUpdateSecretAsync(resource, userId, accessClient);\n                break;\n            case AccessClientType.ServiceAccount:\n                hasAccess = await GetAccessToUpdateSecretAsync(resource, userId, accessClient);\n                break;\n            default:\n                hasAccess = false;\n                break;\n        }\n\n        if (hasAccess)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanDeleteSecretAsync(AuthorizationHandlerContext context,\n        SecretOperationRequirement requirement, Secret resource)\n    {\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n\n        var access = await _secretRepository.AccessToSecretAsync(resource.Id, userId, accessClient);\n\n        if (access.Write)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanReadAccessPoliciesAsync(AuthorizationHandlerContext context,\n        SecretOperationRequirement requirement, Secret resource)\n    {\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n\n        // Only users and admins can read access policies\n        if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)\n        {\n            return;\n        }\n\n        var access = await _secretRepository.AccessToSecretAsync(resource.Id, userId, accessClient);\n\n        if (access.Write)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task<bool> GetAccessToUpdateSecretAsync(Secret resource, Guid userId, AccessClientType accessClient)\n    {\n        // Request was to remove all projects from the secret. This is not allowed for non admin users.\n        if (resource.Projects?.Count == 0)\n        {\n            return false;\n        }\n\n        var access = (await _secretRepository.AccessToSecretAsync(resource.Id, userId, accessClient)).Write;\n\n        // No project mapping changes requested, return secret access.\n        if (resource.Projects == null)\n        {\n            return access;\n        }\n\n        var newProject = resource.Projects?.FirstOrDefault();\n        var accessToNew = newProject != null &&\n                          (await _projectRepository.AccessToProjectAsync(newProject.Id, userId, accessClient))\n                          .Write;\n        return access && accessToNew;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/AuthorizationHandlers/ServiceAccounts/ServiceAccountAuthorizationHandler.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.ServiceAccounts;\n\npublic class\n    ServiceAccountAuthorizationHandler : AuthorizationHandler<ServiceAccountOperationRequirement, ServiceAccount>\n{\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public ServiceAccountAuthorizationHandler(ICurrentContext currentContext,\n        IAccessClientQuery accessClientQuery,\n        IServiceAccountRepository serviceAccountRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _serviceAccountRepository = serviceAccountRepository;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        ServiceAccountOperationRequirement requirement,\n        ServiceAccount resource)\n    {\n        if (!_currentContext.AccessSecretsManager(resource.OrganizationId))\n        {\n            return;\n        }\n\n        switch (requirement)\n        {\n            case not null when requirement == ServiceAccountOperations.Create:\n                await CanCreateServiceAccountAsync(context, requirement, resource);\n                break;\n            case not null when requirement == ServiceAccountOperations.Read:\n                await CanReadServiceAccountAsync(context, requirement, resource);\n                break;\n            case not null when requirement == ServiceAccountOperations.Update:\n                await CanUpdateServiceAccountAsync(context, requirement, resource);\n                break;\n            case not null when requirement == ServiceAccountOperations.Delete:\n                await CanDeleteServiceAccountAsync(context, requirement, resource);\n                break;\n            case not null when requirement == ServiceAccountOperations.CreateAccessToken:\n                await CanCreateAccessTokenAsync(context, requirement, resource);\n                break;\n            case not null when requirement == ServiceAccountOperations.ReadAccessTokens:\n                await CanReadAccessTokensAsync(context, requirement, resource);\n                break;\n            case not null when requirement == ServiceAccountOperations.RevokeAccessTokens:\n                await CanRevokeAccessTokensAsync(context, requirement, resource);\n                break;\n            case not null when requirement == ServiceAccountOperations.ReadEvents:\n                await CanReadEventsAsync(context, requirement, resource);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported operation requirement type provided.\",\n                    nameof(requirement));\n        }\n    }\n\n    private async Task CanCreateServiceAccountAsync(AuthorizationHandlerContext context,\n        ServiceAccountOperationRequirement requirement, ServiceAccount resource)\n    {\n        var (accessClient, _) = await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var hasAccess = accessClient switch\n        {\n            AccessClientType.NoAccessCheck => true,\n            AccessClientType.User => true,\n            AccessClientType.ServiceAccount => false,\n            _ => false,\n        };\n\n        if (hasAccess)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanReadServiceAccountAsync(AuthorizationHandlerContext context,\n        ServiceAccountOperationRequirement requirement, ServiceAccount resource)\n    {\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var access =\n            await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId,\n                accessClient);\n\n        if (access.Read)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanUpdateServiceAccountAsync(AuthorizationHandlerContext context,\n        ServiceAccountOperationRequirement requirement, ServiceAccount resource)\n    {\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var access =\n            await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId,\n                accessClient);\n\n        if (access.Write)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanDeleteServiceAccountAsync(AuthorizationHandlerContext context,\n        ServiceAccountOperationRequirement requirement, ServiceAccount resource)\n    {\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var access =\n            await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId,\n                accessClient);\n\n        if (access.Write)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanCreateAccessTokenAsync(AuthorizationHandlerContext context,\n        ServiceAccountOperationRequirement requirement, ServiceAccount resource)\n    {\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var access =\n            await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId,\n                accessClient);\n\n        if (access.Write)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanReadAccessTokensAsync(AuthorizationHandlerContext context,\n        ServiceAccountOperationRequirement requirement, ServiceAccount resource)\n    {\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var access =\n            await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId,\n                accessClient);\n\n        if (access.Read)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanRevokeAccessTokensAsync(AuthorizationHandlerContext context,\n        ServiceAccountOperationRequirement requirement, ServiceAccount resource)\n    {\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var access =\n            await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId,\n                accessClient);\n\n        if (access.Write)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanReadEventsAsync(AuthorizationHandlerContext context,\n        ServiceAccountOperationRequirement requirement, ServiceAccount resource)\n    {\n        var (accessClient, userId) =\n            await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);\n        var access =\n            await _serviceAccountRepository.AccessToServiceAccountAsync(resource.Id, userId,\n                accessClient);\n\n        if (access.Read)\n        {\n            context.Succeed(requirement);\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateProjectServiceAccountsAccessPoliciesCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;\n\npublic class UpdateProjectServiceAccountsAccessPoliciesCommand : IUpdateProjectServiceAccountsAccessPoliciesCommand\n{\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n\n    public UpdateProjectServiceAccountsAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)\n    {\n        _accessPolicyRepository = accessPolicyRepository;\n    }\n\n    public async Task UpdateAsync(ProjectServiceAccountsAccessPoliciesUpdates accessPoliciesUpdates)\n    {\n        if (!accessPoliciesUpdates.ServiceAccountAccessPolicyUpdates.Any())\n        {\n            return;\n        }\n\n        await _accessPolicyRepository.UpdateProjectServiceAccountsAccessPoliciesAsync(accessPoliciesUpdates);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;\n\npublic class UpdateServiceAccountGrantedPoliciesCommand : IUpdateServiceAccountGrantedPoliciesCommand\n{\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n\n    public UpdateServiceAccountGrantedPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)\n    {\n        _accessPolicyRepository = accessPolicyRepository;\n    }\n\n    public async Task UpdateAsync(ServiceAccountGrantedPoliciesUpdates grantedPoliciesUpdates)\n    {\n        if (!grantedPoliciesUpdates.ProjectGrantedPolicyUpdates.Any())\n        {\n            return;\n        }\n\n        await _accessPolicyRepository.UpdateServiceAccountGrantedPoliciesAsync(grantedPoliciesUpdates);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommand.cs",
    "content": "﻿using System.Security.Cryptography;\nusing System.Text;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.AccessTokens;\n\npublic class CreateAccessTokenCommand : ICreateAccessTokenCommand\n{\n    private const int _clientSecretMaxLength = 30;\n    private readonly IApiKeyRepository _apiKeyRepository;\n\n    public CreateAccessTokenCommand(IApiKeyRepository apiKeyRepository)\n    {\n        _apiKeyRepository = apiKeyRepository;\n    }\n\n    public async Task<ApiKeyClientSecretDetails> CreateAsync(ApiKey apiKey)\n    {\n        if (apiKey.ServiceAccountId == null)\n        {\n            throw new BadRequestException();\n        }\n\n        var clientSecret = CoreHelpers.SecureRandomString(_clientSecretMaxLength);\n        apiKey.ClientSecretHash = GetHash(clientSecret);\n        var result = await _apiKeyRepository.CreateAsync(apiKey);\n        return new ApiKeyClientSecretDetails { ApiKey = result, ClientSecret = clientSecret };\n    }\n\n    private static string GetHash(string input)\n    {\n        using var sha = SHA256.Create();\n        var bytes = Encoding.UTF8.GetBytes(input);\n        var hash = sha.ComputeHash(bytes);\n        return Convert.ToBase64String(hash);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Porting/ImportCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.SecretsManager.Commands.Porting;\nusing Bit.Core.SecretsManager.Commands.Porting.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Porting;\n\npublic class ImportCommand : IImportCommand\n{\n    private readonly IProjectRepository _projectRepository;\n    private readonly ISecretRepository _secretRepository;\n\n    public ImportCommand(IProjectRepository projectRepository, ISecretRepository secretRepository)\n    {\n        _projectRepository = projectRepository;\n        _secretRepository = secretRepository;\n    }\n\n    public async Task ImportAsync(Guid organizationId, SMImport import)\n    {\n        var importedProjects = new List<Guid>();\n        var importedSecrets = new List<Guid>();\n\n        try\n        {\n            import = AssignNewIds(import);\n\n            if (import.Projects.Any())\n            {\n                importedProjects = (await _projectRepository.ImportAsync(import.Projects.Select(p => new Project\n                {\n                    Id = p.Id,\n                    OrganizationId = organizationId,\n                    Name = p.Name,\n                }))).Select(p => p.Id).ToList();\n            }\n\n            if (import.Secrets != null && import.Secrets.Any())\n            {\n                importedSecrets = (await _secretRepository.ImportAsync(import.Secrets.Select(s => new Secret\n                {\n                    Id = s.Id,\n                    OrganizationId = organizationId,\n                    Key = s.Key,\n                    Value = s.Value,\n                    Note = s.Note,\n                    Projects = s.ProjectIds?.Select(id => new Project { Id = id }).ToList(),\n                }))).Select(s => s.Id).ToList();\n            }\n        }\n        catch (Exception)\n        {\n            if (importedProjects.Any())\n            {\n                await _projectRepository.DeleteManyByIdAsync(importedProjects);\n            }\n\n            if (importedSecrets.Any())\n            {\n                await _secretRepository.HardDeleteManyByIdAsync(importedSecrets);\n            }\n\n            throw new Exception(\"Error attempting import\");\n        }\n    }\n\n    public SMImport AssignNewIds(SMImport import)\n    {\n        var projects = new Dictionary<Guid, SMImport.InnerProject>();\n        var secrets = new List<SMImport.InnerSecret>();\n\n        if (import.Projects != null && import.Projects.Any())\n        {\n            projects = import.Projects.ToDictionary(\n                p => p.Id,\n                p => new SMImport.InnerProject { Id = Guid.NewGuid(), Name = p.Name }\n            );\n        }\n\n        if (import.Secrets != null && import.Secrets.Any())\n        {\n            foreach (var secret in import.Secrets)\n            {\n                secrets.Add(new SMImport.InnerSecret\n                {\n                    Id = Guid.NewGuid(),\n                    Key = secret.Key,\n                    Value = secret.Value,\n                    Note = secret.Note,\n                    ProjectIds = secret.ProjectIds?.Select(id => projects[id].Id),\n                });\n            }\n        }\n\n        return new SMImport\n        {\n            Projects = projects.Values,\n            Secrets = secrets,\n        };\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Commands.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Projects;\n\npublic class CreateProjectCommand : ICreateProjectCommand\n{\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IProjectRepository _projectRepository;\n    private readonly ICurrentContext _currentContext;\n\n\n    public CreateProjectCommand(\n        IAccessPolicyRepository accessPolicyRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IProjectRepository projectRepository,\n        ICurrentContext currentContext)\n    {\n        _accessPolicyRepository = accessPolicyRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _projectRepository = projectRepository;\n        _currentContext = currentContext;\n    }\n\n    public async Task<Project> CreateAsync(Project project, Guid id, IdentityClientType identityClientType)\n    {\n        if (identityClientType != IdentityClientType.User && identityClientType != IdentityClientType.ServiceAccount)\n        {\n            throw new NotFoundException();\n        }\n\n        var createdProject = await _projectRepository.CreateAsync(project);\n\n        if (identityClientType == IdentityClientType.User)\n        {\n            var orgUser = await _organizationUserRepository.GetByOrganizationAsync(createdProject.OrganizationId, id);\n\n            var accessPolicy = new UserProjectAccessPolicy()\n            {\n                OrganizationUserId = orgUser.Id,\n                GrantedProjectId = createdProject.Id,\n                Read = true,\n                Write = true,\n            };\n\n            await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { accessPolicy });\n\n        }\n        else if (identityClientType == IdentityClientType.ServiceAccount)\n        {\n            var serviceAccountProjectAccessPolicy = new ServiceAccountProjectAccessPolicy()\n            {\n                ServiceAccountId = id,\n                GrantedProjectId = createdProject.Id,\n                Read = true,\n                Write = true,\n            };\n\n            await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { serviceAccountProjectAccessPolicy });\n        }\n\n        return createdProject;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/DeleteProjectCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Commands.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Projects;\n\npublic class DeleteProjectCommand : IDeleteProjectCommand\n{\n    private readonly IProjectRepository _projectRepository;\n\n    public DeleteProjectCommand(IProjectRepository projectRepository)\n    {\n        _projectRepository = projectRepository;\n    }\n\n    public async Task DeleteProjects(IEnumerable<Project> projects)\n    {\n        await _projectRepository.DeleteManyByIdAsync(projects.Select(p => p.Id));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/UpdateProjectCommand.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Projects;\n\npublic class UpdateProjectCommand : IUpdateProjectCommand\n{\n    private readonly IProjectRepository _projectRepository;\n\n    public UpdateProjectCommand(IProjectRepository projectRepository)\n    {\n        _projectRepository = projectRepository;\n    }\n\n    public async Task<Project> UpdateAsync(Project updatedProject)\n    {\n        var project = await _projectRepository.GetByIdAsync(updatedProject.Id);\n        if (project == null)\n        {\n            throw new NotFoundException();\n        }\n\n        project.Name = updatedProject.Name;\n        project.RevisionDate = DateTime.UtcNow;\n\n        await _projectRepository.ReplaceAsync(project);\n        return project;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Requests/RequestSMAccessCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.SecretsManager.Commands.Requests.Interfaces;\nusing Bit.Core.Services;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Requests;\n\npublic class RequestSMAccessCommand : IRequestSMAccessCommand\n{\n    private readonly IMailService _mailService;\n\n    public RequestSMAccessCommand(\n        IMailService mailService)\n    {\n        _mailService = mailService;\n    }\n\n    public async Task SendRequestAccessToSM(Organization organization, ICollection<OrganizationUserUserDetails> orgUsers, User user, string emailContent)\n    {\n        var emailList = orgUsers.Where(o => o.Type <= OrganizationUserType.Admin)\n            .Select(a => a.Email).Distinct().ToList();\n\n        if (!emailList.Any())\n        {\n            throw new BadRequestException(\"The organization is in an invalid state. Please contact Customer Support.\");\n        }\n\n        var userRequestingAccess = user.Name ?? user.Email;\n        await _mailService.SendRequestSMAccessToAdminEmailAsync(emailList, organization.Name, userRequestingAccess, emailContent);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/CreateSecretCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Commands.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;\n\npublic class CreateSecretCommand : ICreateSecretCommand\n{\n    private readonly ISecretRepository _secretRepository;\n\n    public CreateSecretCommand(ISecretRepository secretRepository)\n    {\n        _secretRepository = secretRepository;\n    }\n\n    public async Task<Secret> CreateAsync(Secret secret, SecretAccessPoliciesUpdates? accessPoliciesUpdates)\n    {\n        return await _secretRepository.CreateAsync(secret, accessPoliciesUpdates);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/DeleteSecretCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;\n\npublic class DeleteSecretCommand : IDeleteSecretCommand\n{\n    private readonly ISecretRepository _secretRepository;\n\n    public DeleteSecretCommand(ISecretRepository secretRepository)\n    {\n        _secretRepository = secretRepository;\n    }\n\n    public async Task DeleteSecrets(IEnumerable<Secret> secrets)\n    {\n        await _secretRepository.SoftDeleteManyByIdAsync(secrets.Select(s => s.Id));\n    }\n}\n\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/UpdateSecretCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Commands.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;\n\npublic class UpdateSecretCommand : IUpdateSecretCommand\n{\n    private readonly ISecretRepository _secretRepository;\n\n    public UpdateSecretCommand(ISecretRepository secretRepository)\n    {\n        _secretRepository = secretRepository;\n    }\n\n    public async Task<Secret> UpdateAsync(Secret secret, SecretAccessPoliciesUpdates? accessPolicyUpdates)\n    {\n        return await _secretRepository.UpdateAsync(secret, accessPolicyUpdates);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;\n\npublic class CreateServiceAccountCommand : ICreateServiceAccountCommand\n{\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly IEventService _eventService;\n    private readonly ICurrentContext _currentContext;\n\n    public CreateServiceAccountCommand(\n        IAccessPolicyRepository accessPolicyRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IServiceAccountRepository serviceAccountRepository,\n        IEventService eventService,\n        ICurrentContext currentContext)\n    {\n        _accessPolicyRepository = accessPolicyRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _serviceAccountRepository = serviceAccountRepository;\n        _eventService = eventService;\n        _currentContext = currentContext;\n    }\n\n    public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount, Guid userId)\n    {\n        var createdServiceAccount = await _serviceAccountRepository.CreateAsync(serviceAccount);\n\n        var user = await _organizationUserRepository.GetByOrganizationAsync(createdServiceAccount.OrganizationId,\n            userId);\n        var accessPolicy = new UserServiceAccountAccessPolicy\n        {\n            OrganizationUserId = user.Id,\n            GrantedServiceAccountId = createdServiceAccount.Id,\n            Read = true,\n            Write = true,\n        };\n        await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { accessPolicy });\n        await _eventService.LogServiceAccountPeopleEventAsync(user.Id, accessPolicy, EventType.ServiceAccount_UserAdded, _currentContext.IdentityClientType);\n        return createdServiceAccount;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/DeleteServiceAccountsCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;\n\npublic class DeleteServiceAccountsCommand : IDeleteServiceAccountsCommand\n{\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public DeleteServiceAccountsCommand(\n        IServiceAccountRepository serviceAccountRepository)\n    {\n        _serviceAccountRepository = serviceAccountRepository;\n    }\n\n    public async Task DeleteServiceAccounts(IEnumerable<ServiceAccount> serviceAccounts)\n    {\n        await _serviceAccountRepository.DeleteManyByIdAsync(serviceAccounts.Select(sa => sa.Id));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/RevokeAccessTokensCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;\n\npublic class RevokeAccessTokensCommand : IRevokeAccessTokensCommand\n{\n    private readonly IApiKeyRepository _apiKeyRepository;\n\n    public RevokeAccessTokensCommand(IApiKeyRepository apiKeyRepository)\n    {\n        _apiKeyRepository = apiKeyRepository;\n    }\n\n    public async Task RevokeAsync(ServiceAccount serviceAccount, IEnumerable<Guid> Ids)\n    {\n        var accessTokens = await _apiKeyRepository.GetManyByServiceAccountIdAsync(serviceAccount.Id);\n\n        var tokensToDelete = accessTokens.Where(at => Ids.Contains(at.Id));\n\n        await _apiKeyRepository.DeleteManyAsync(tokensToDelete);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommand.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;\n\npublic class UpdateServiceAccountCommand : IUpdateServiceAccountCommand\n{\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly ICurrentContext _currentContext;\n\n    public UpdateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository, ICurrentContext currentContext)\n    {\n        _serviceAccountRepository = serviceAccountRepository;\n        _currentContext = currentContext;\n    }\n\n    public async Task<ServiceAccount> UpdateAsync(ServiceAccount updatedServiceAccount)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(updatedServiceAccount.Id);\n        if (serviceAccount == null)\n        {\n            throw new NotFoundException();\n        }\n\n        serviceAccount.Name = updatedServiceAccount.Name;\n        serviceAccount.RevisionDate = DateTime.UtcNow;\n\n        await _serviceAccountRepository.ReplaceAsync(serviceAccount);\n        return serviceAccount;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/EmptyTrashCommand.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.Trash.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Trash;\n\npublic class EmptyTrashCommand : IEmptyTrashCommand\n{\n    private readonly ISecretRepository _secretRepository;\n\n    public EmptyTrashCommand(ISecretRepository secretRepository)\n    {\n        _secretRepository = secretRepository;\n    }\n\n    public async Task EmptyTrash(Guid organizationId, List<Guid> ids)\n    {\n        var secrets = await _secretRepository.GetManyByOrganizationIdInTrashByIdsAsync(organizationId, ids);\n\n        var missingId = ids.Except(secrets.Select(_ => _.Id)).Any();\n        if (missingId)\n        {\n            throw new NotFoundException();\n        }\n\n        await _secretRepository.HardDeleteManyByIdAsync(ids);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/RestoreTrashCommand.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.Trash.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Commands.Trash;\n\npublic class RestoreTrashCommand : IRestoreTrashCommand\n{\n    private readonly ISecretRepository _secretRepository;\n\n    public RestoreTrashCommand(ISecretRepository secretRepository)\n    {\n        _secretRepository = secretRepository;\n    }\n\n    public async Task RestoreTrash(Guid organizationId, List<Guid> ids)\n    {\n        var secrets = await _secretRepository.GetManyByOrganizationIdInTrashByIdsAsync(organizationId, ids);\n\n        var missingId = ids.Except(secrets.Select(_ => _.Id)).Any();\n        if (missingId)\n        {\n            throw new NotFoundException();\n        }\n\n        await _secretRepository.RestoreManyByIdAsync(ids);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessClientQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.Services;\n\nnamespace Bit.Commercial.Core.SecretsManager.Queries;\n\npublic class AccessClientQuery : IAccessClientQuery\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IUserService _userService;\n\n    public AccessClientQuery(ICurrentContext currentContext, IUserService userService)\n    {\n        _currentContext = currentContext;\n        _userService = userService;\n    }\n\n    public async Task<(AccessClientType AccessClientType, Guid UserId)> GetAccessClientAsync(\n        ClaimsPrincipal claimsPrincipal, Guid organizationId)\n    {\n        var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n        var userId = _userService.GetProperUserId(claimsPrincipal).Value;\n        return (accessClient, userId);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/ProjectServiceAccountsAccessPoliciesUpdatesQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;\n\npublic class ProjectServiceAccountsAccessPoliciesUpdatesQuery : IProjectServiceAccountsAccessPoliciesUpdatesQuery\n{\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n\n    public ProjectServiceAccountsAccessPoliciesUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)\n    {\n        _accessPolicyRepository = accessPolicyRepository;\n    }\n\n    public async Task<ProjectServiceAccountsAccessPoliciesUpdates> GetAsync(\n        ProjectServiceAccountsAccessPolicies projectServiceAccountsAccessPolicies)\n    {\n        var currentPolicies =\n            await _accessPolicyRepository.GetProjectServiceAccountsAccessPoliciesAsync(\n                projectServiceAccountsAccessPolicies.ProjectId);\n\n        if (currentPolicies == null)\n        {\n            return new ProjectServiceAccountsAccessPoliciesUpdates\n            {\n                ProjectId = projectServiceAccountsAccessPolicies.ProjectId,\n                OrganizationId = projectServiceAccountsAccessPolicies.OrganizationId,\n                ServiceAccountAccessPolicyUpdates =\n                    projectServiceAccountsAccessPolicies.ServiceAccountAccessPolicies.Select(p =>\n                        new ServiceAccountProjectAccessPolicyUpdate\n                        {\n                            Operation = AccessPolicyOperation.Create,\n                            AccessPolicy = p\n                        })\n            };\n        }\n\n        return currentPolicies.GetPolicyUpdates(projectServiceAccountsAccessPolicies);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/SameOrganizationQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\n\nnamespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;\n\npublic class SameOrganizationQuery : ISameOrganizationQuery\n{\n    private readonly IGroupRepository _groupRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public SameOrganizationQuery(IOrganizationUserRepository organizationUserRepository,\n        IGroupRepository groupRepository)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _groupRepository = groupRepository;\n    }\n\n    public async Task<bool> OrgUsersInTheSameOrgAsync(List<Guid> organizationUserIds, Guid organizationId)\n    {\n        var users = await _organizationUserRepository.GetManyAsync(organizationUserIds);\n        return users.All(user => user.OrganizationId == organizationId) &&\n               users.Count == organizationUserIds.Count;\n    }\n\n    public async Task<bool> GroupsInTheSameOrgAsync(List<Guid> groupIds, Guid organizationId)\n    {\n        var groups = await _groupRepository.GetManyByManyIds(groupIds);\n        return groups.All(group => group.OrganizationId == organizationId) &&\n               groups.Count == groupIds.Count;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/SecretAccessPoliciesUpdatesQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;\n\npublic class SecretAccessPoliciesUpdatesQuery : ISecretAccessPoliciesUpdatesQuery\n{\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n\n    public SecretAccessPoliciesUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)\n    {\n        _accessPolicyRepository = accessPolicyRepository;\n    }\n\n    public async Task<SecretAccessPoliciesUpdates> GetAsync(SecretAccessPolicies accessPolicies, Guid userId)\n    {\n        var currentPolicies = await _accessPolicyRepository.GetSecretAccessPoliciesAsync(accessPolicies.SecretId, userId);\n\n        return currentPolicies == null ? new SecretAccessPoliciesUpdates(accessPolicies) : currentPolicies.GetPolicyUpdates(accessPolicies);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;\n\npublic class ServiceAccountGrantedPolicyUpdatesQuery : IServiceAccountGrantedPolicyUpdatesQuery\n{\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n\n    public ServiceAccountGrantedPolicyUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)\n    {\n        _accessPolicyRepository = accessPolicyRepository;\n    }\n\n    public async Task<ServiceAccountGrantedPoliciesUpdates> GetAsync(\n        ServiceAccountGrantedPolicies grantedPolicies)\n    {\n        var currentPolicies =\n            await _accessPolicyRepository.GetServiceAccountGrantedPoliciesAsync(grantedPolicies.ServiceAccountId);\n        if (currentPolicies == null)\n        {\n            return new ServiceAccountGrantedPoliciesUpdates\n            {\n                ServiceAccountId = grantedPolicies.ServiceAccountId,\n                OrganizationId = grantedPolicies.OrganizationId,\n                ProjectGrantedPolicyUpdates = grantedPolicies.ProjectGrantedPolicies.Select(p =>\n                    new ServiceAccountProjectAccessPolicyUpdate\n                    {\n                        Operation = AccessPolicyOperation.Create,\n                        AccessPolicy = p\n                    })\n            };\n        }\n\n        return currentPolicies.GetPolicyUpdates(grantedPolicies);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Queries.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Settings;\n\nnamespace Bit.Commercial.Core.SecretsManager.Queries.Projects;\n\npublic class MaxProjectsQuery : IMaxProjectsQuery\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IPricingClient _pricingClient;\n\n    public MaxProjectsQuery(\n        IOrganizationRepository organizationRepository,\n        IProjectRepository projectRepository,\n        IGlobalSettings globalSettings,\n        IPricingClient pricingClient)\n    {\n        _organizationRepository = organizationRepository;\n        _projectRepository = projectRepository;\n        _globalSettings = globalSettings;\n        _pricingClient = pricingClient;\n    }\n\n    public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd)\n    {\n        // \"MaxProjects\" only applies to free 2-person organizations, which can't be self-hosted.\n        if (_globalSettings.SelfHosted)\n        {\n            return (null, null);\n        }\n\n        var org = await _organizationRepository.GetByIdAsync(organizationId);\n        if (org == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var plan = await _pricingClient.GetPlan(org.PlanType);\n\n        if (plan is not { SecretsManager: not null, Type: PlanType.Free })\n        {\n            return (null, null);\n        }\n\n        var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);\n        return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Secrets/SecretsSyncQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Queries.Secrets;\n\npublic class SecretsSyncQuery : ISecretsSyncQuery\n{\n    private readonly ISecretRepository _secretRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public SecretsSyncQuery(\n        ISecretRepository secretRepository,\n        IServiceAccountRepository serviceAccountRepository)\n    {\n        _secretRepository = secretRepository;\n        _serviceAccountRepository = serviceAccountRepository;\n    }\n\n    public async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetAsync(SecretsSyncRequest syncRequest)\n    {\n        if (syncRequest.LastSyncedDate == null)\n        {\n            return await GetSecretsAsync(syncRequest);\n        }\n\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(syncRequest.ServiceAccountId);\n        if (serviceAccount == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (syncRequest.LastSyncedDate.Value <= serviceAccount.RevisionDate)\n        {\n            return await GetSecretsAsync(syncRequest);\n        }\n\n        return (HasChanges: false, null);\n    }\n\n    private async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetSecretsAsync(SecretsSyncRequest syncRequest)\n    {\n        var secrets = await _secretRepository.GetManyByOrganizationIdAsync(syncRequest.OrganizationId,\n            syncRequest.ServiceAccountId, syncRequest.AccessClientType);\n        return (HasChanges: true, Secrets: secrets);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Queries/ServiceAccounts/CountNewServiceAccountSlotsRequiredQuery.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;\n\npublic class CountNewServiceAccountSlotsRequiredQuery : ICountNewServiceAccountSlotsRequiredQuery\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public CountNewServiceAccountSlotsRequiredQuery(\n        IOrganizationRepository organizationRepository,\n        IServiceAccountRepository serviceAccountRepository)\n    {\n        _organizationRepository = organizationRepository;\n        _serviceAccountRepository = serviceAccountRepository;\n    }\n\n    public async Task<int> CountNewServiceAccountSlotsRequiredAsync(Guid organizationId, int serviceAccountsToAdd)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n        if (organization == null || !organization.UseSecretsManager)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!organization.SmServiceAccounts.HasValue || serviceAccountsToAdd == 0)\n        {\n            return 0;\n        }\n\n        var serviceAccountCount = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organizationId);\n        var availableServiceAccountSlots = organization.SmServiceAccounts.Value - serviceAccountCount;\n\n        if (availableServiceAccountSlots >= serviceAccountsToAdd)\n        {\n            return 0;\n        }\n\n        return serviceAccountsToAdd - availableServiceAccountSlots;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/Queries/ServiceAccounts/ServiceAccountSecretsDetailsQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;\n\npublic class ServiceAccountSecretsDetailsQuery : IServiceAccountSecretsDetailsQuery\n{\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public ServiceAccountSecretsDetailsQuery(IServiceAccountRepository serviceAccountRepository)\n    {\n        _serviceAccountRepository = serviceAccountRepository;\n    }\n\n    public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdAsync(\n        Guid organizationId, Guid userId, AccessClientType accessClient, bool includeAccessToSecrets)\n    {\n        if (includeAccessToSecrets)\n        {\n            return await _serviceAccountRepository.GetManyByOrganizationIdWithSecretsDetailsAsync(organizationId,\n                userId,\n                accessClient);\n        }\n\n        var serviceAccounts =\n            await _serviceAccountRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);\n\n        return serviceAccounts.Select(sa => new ServiceAccountSecretsDetails\n        {\n            ServiceAccount = sa,\n            AccessToSecrets = 0,\n        });\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.ServiceAccounts;\nusing Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;\nusing Bit.Commercial.Core.SecretsManager.Commands.AccessTokens;\nusing Bit.Commercial.Core.SecretsManager.Commands.Porting;\nusing Bit.Commercial.Core.SecretsManager.Commands.Projects;\nusing Bit.Commercial.Core.SecretsManager.Commands.Requests;\nusing Bit.Commercial.Core.SecretsManager.Commands.Secrets;\nusing Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;\nusing Bit.Commercial.Core.SecretsManager.Commands.Trash;\nusing Bit.Commercial.Core.SecretsManager.Queries;\nusing Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;\nusing Bit.Commercial.Core.SecretsManager.Queries.Projects;\nusing Bit.Commercial.Core.SecretsManager.Queries.Secrets;\nusing Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;\nusing Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;\nusing Bit.Core.SecretsManager.Commands.Porting.Interfaces;\nusing Bit.Core.SecretsManager.Commands.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Commands.Requests.Interfaces;\nusing Bit.Core.SecretsManager.Commands.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Commands.Trash.Interfaces;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Commercial.Core.SecretsManager;\n\npublic static class SecretsManagerCollectionExtensions\n{\n    public static void AddSecretsManagerServices(this IServiceCollection services)\n    {\n        services.AddScoped<IAuthorizationHandler, ProjectAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, SecretAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, ServiceAccountGrantedPoliciesAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, ProjectServiceAccountsAccessPoliciesAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, SecretAccessPoliciesUpdatesAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, BulkSecretAuthorizationHandler>();\n        services.AddScoped<IAccessClientQuery, AccessClientQuery>();\n        services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();\n        services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();\n        services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();\n        services.AddScoped<IServiceAccountGrantedPolicyUpdatesQuery, ServiceAccountGrantedPolicyUpdatesQuery>();\n        services.AddScoped<ISecretAccessPoliciesUpdatesQuery, SecretAccessPoliciesUpdatesQuery>();\n        services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();\n        services.AddScoped<IProjectServiceAccountsAccessPoliciesUpdatesQuery, ProjectServiceAccountsAccessPoliciesUpdatesQuery>();\n        services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();\n        services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();\n        services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();\n        services.AddScoped<ICreateProjectCommand, CreateProjectCommand>();\n        services.AddScoped<IRequestSMAccessCommand, RequestSMAccessCommand>();\n        services.AddScoped<IUpdateProjectCommand, UpdateProjectCommand>();\n        services.AddScoped<IDeleteProjectCommand, DeleteProjectCommand>();\n        services.AddScoped<ICreateServiceAccountCommand, CreateServiceAccountCommand>();\n        services.AddScoped<IUpdateServiceAccountCommand, UpdateServiceAccountCommand>();\n        services.AddScoped<IDeleteServiceAccountsCommand, DeleteServiceAccountsCommand>();\n        services.AddScoped<ICountNewServiceAccountSlotsRequiredQuery, CountNewServiceAccountSlotsRequiredQuery>();\n        services.AddScoped<IRevokeAccessTokensCommand, RevokeAccessTokensCommand>();\n        services.AddScoped<ICreateAccessTokenCommand, CreateAccessTokenCommand>();\n        services.AddScoped<IImportCommand, ImportCommand>();\n        services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>();\n        services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>();\n        services.AddScoped<IUpdateServiceAccountGrantedPoliciesCommand, UpdateServiceAccountGrantedPoliciesCommand>();\n        services.AddScoped<IUpdateProjectServiceAccountsAccessPoliciesCommand, UpdateProjectServiceAccountsAccessPoliciesCommand>();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerServiceCollectionExtensions.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Commercial.Core.SecretsManager;\n\npublic static class SecretsManagerServiceCollectionExtensions\n{\n    public static void AddCommercialSecretsManagerServices(this IServiceCollection services)\n    {\n        services.AddSecretsManagerServices();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Commercial.Core.AdminConsole.Providers;\nusing Bit.Commercial.Core.AdminConsole.Services;\nusing Bit.Commercial.Core.Billing.Providers.Queries;\nusing Bit.Commercial.Core.Billing.Providers.Services;\nusing Bit.Core.AdminConsole.Providers.Interfaces;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Providers.Queries;\nusing Bit.Core.Billing.Providers.Services;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Commercial.Core.Utilities;\n\npublic static class ServiceCollectionExtensions\n{\n    public static void AddCommercialCoreServices(this IServiceCollection services)\n    {\n        services.AddScoped<IProviderService, ProviderService>();\n        services.AddScoped<ICreateProviderCommand, CreateProviderCommand>();\n        services.AddScoped<IRemoveOrganizationFromProviderCommand, RemoveOrganizationFromProviderCommand>();\n        services.AddTransient<IProviderBillingService, ProviderBillingService>();\n        services.AddTransient<IBusinessUnitConverter, BusinessUnitConverter>();\n        services.AddTransient<IGetProviderWarningsQuery, GetProviderWarningsQuery>();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Infrastructure.EntityFramework/Commercial.Infrastructure.EntityFramework.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"AutoMapper.Extensions.Microsoft.DependencyInjection\" Version=\"12.0.1\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\src\\Infrastructure.EntityFramework\\Infrastructure.EntityFramework.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n\nnamespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;\n\npublic class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPolicyRepository\n{\n    public AccessPolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(serviceScopeFactory,\n        mapper)\n    {\n    }\n\n    public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(\n        List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        var serviceAccountIds = new List<Guid>();\n        foreach (var baseAccessPolicy in baseAccessPolicies)\n        {\n            baseAccessPolicy.SetNewId();\n            switch (baseAccessPolicy)\n            {\n                case Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy:\n                    {\n                        var entity =\n                            Mapper.Map<UserProjectAccessPolicy>(accessPolicy);\n                        await dbContext.AddAsync(entity);\n                        break;\n                    }\n                case Core.SecretsManager.Entities.UserSecretAccessPolicy accessPolicy:\n                    {\n                        var entity =\n                            Mapper.Map<UserSecretAccessPolicy>(accessPolicy);\n                        await dbContext.AddAsync(entity);\n                        break;\n                    }\n                case Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy:\n                    {\n                        var entity =\n                            Mapper.Map<UserServiceAccountAccessPolicy>(accessPolicy);\n                        await dbContext.AddAsync(entity);\n                        break;\n                    }\n                case Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy:\n                    {\n                        var entity = Mapper.Map<GroupProjectAccessPolicy>(accessPolicy);\n                        await dbContext.AddAsync(entity);\n                        break;\n                    }\n                case Core.SecretsManager.Entities.GroupSecretAccessPolicy accessPolicy:\n                    {\n                        var entity = Mapper.Map<GroupSecretAccessPolicy>(accessPolicy);\n                        await dbContext.AddAsync(entity);\n                        break;\n                    }\n                case Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy:\n                    {\n                        var entity = Mapper.Map<GroupServiceAccountAccessPolicy>(accessPolicy);\n                        await dbContext.AddAsync(entity);\n                        break;\n                    }\n                case Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy:\n                    {\n                        var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy);\n                        await dbContext.AddAsync(entity);\n                        serviceAccountIds.Add(entity.ServiceAccountId!.Value);\n                        break;\n                    }\n                case Core.SecretsManager.Entities.ServiceAccountSecretAccessPolicy accessPolicy:\n                    {\n                        var entity = Mapper.Map<ServiceAccountSecretAccessPolicy>(accessPolicy);\n                        await dbContext.AddAsync(entity);\n                        serviceAccountIds.Add(entity.ServiceAccountId!.Value);\n                        break;\n                    }\n            }\n        }\n\n        if (serviceAccountIds.Count > 0)\n        {\n            var utcNow = DateTime.UtcNow;\n            await dbContext.ServiceAccount\n                .Where(sa => serviceAccountIds.Contains(sa.Id))\n                .ExecuteUpdateAsync(setters => setters.SetProperty(sa => sa.RevisionDate, utcNow));\n        }\n\n        await dbContext.SaveChangesAsync();\n        await transaction.CommitAsync();\n        return baseAccessPolicies;\n    }\n\n    public async Task<PeopleGrantees> GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var userGrantees = await dbContext.OrganizationUsers\n            .Where(ou =>\n                ou.OrganizationId == organizationId &&\n                ou.AccessSecretsManager &&\n                ou.Status == OrganizationUserStatusType.Confirmed)\n            .Include(ou => ou.User)\n            .Select(ou => new\n                UserGrantee\n            {\n                OrganizationUserId = ou.Id,\n                Name = ou.User.Name,\n                Email = ou.User.Email,\n                CurrentUser = ou.UserId == currentUserId\n            }).ToListAsync();\n\n        var groupGrantees = await dbContext.Groups\n            .Where(g => g.OrganizationId == organizationId)\n            .Include(g => g.GroupUsers)\n            .Select(g => new GroupGrantee\n            {\n                GroupId = g.Id,\n                Name = g.Name,\n                CurrentUserInGroup = g.GroupUsers.Any(gu =>\n                    gu.OrganizationUser.User.Id == currentUserId)\n            }).ToListAsync();\n\n        return new PeopleGrantees { UserGrantees = userGrantees, GroupGrantees = groupGrantees };\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>>\n        GetPeoplePoliciesByGrantedProjectIdAsync(Guid id, Guid userId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var entities = await dbContext.AccessPolicies.Where(ap =>\n                ap.Discriminator != AccessPolicyDiscriminator.ServiceAccountProject &&\n                (((UserProjectAccessPolicy)ap).GrantedProjectId == id ||\n                 ((GroupProjectAccessPolicy)ap).GrantedProjectId == id))\n            .Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User)\n            .Include(ap => ((GroupProjectAccessPolicy)ap).Group)\n            .Select(ap => new\n            {\n                ap,\n                CurrentUserInGroup = ap is GroupProjectAccessPolicy &&\n                                     ((GroupProjectAccessPolicy)ap).Group.GroupUsers.Any(g =>\n                                         g.OrganizationUser.UserId == userId),\n            })\n            .ToListAsync();\n\n        return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup));\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> ReplaceProjectPeopleAsync(\n        ProjectPeopleAccessPolicies peopleAccessPolicies, Guid userId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var peoplePolicyEntities = await dbContext.AccessPolicies.Where(ap =>\n            ap.Discriminator != AccessPolicyDiscriminator.ServiceAccountProject &&\n            (((UserProjectAccessPolicy)ap).GrantedProjectId == peopleAccessPolicies.Id ||\n             ((GroupProjectAccessPolicy)ap).GrantedProjectId == peopleAccessPolicies.Id)).ToListAsync();\n\n        var userPolicyEntities =\n            peoplePolicyEntities.Where(ap => ap.GetType() == typeof(UserProjectAccessPolicy)).ToList();\n        var groupPolicyEntities =\n            peoplePolicyEntities.Where(ap => ap.GetType() == typeof(GroupProjectAccessPolicy)).ToList();\n\n\n        if (peopleAccessPolicies.UserAccessPolicies == null || !peopleAccessPolicies.UserAccessPolicies.Any())\n        {\n            dbContext.RemoveRange(userPolicyEntities);\n        }\n        else\n        {\n            foreach (var userPolicyEntity in userPolicyEntities.Where(entity =>\n                         peopleAccessPolicies.UserAccessPolicies.All(ap =>\n                             ((Core.SecretsManager.Entities.UserProjectAccessPolicy)ap).OrganizationUserId !=\n                             ((UserProjectAccessPolicy)entity).OrganizationUserId)))\n            {\n                dbContext.Remove(userPolicyEntity);\n            }\n        }\n\n        if (peopleAccessPolicies.GroupAccessPolicies == null || !peopleAccessPolicies.GroupAccessPolicies.Any())\n        {\n            dbContext.RemoveRange(groupPolicyEntities);\n        }\n        else\n        {\n            foreach (var groupPolicyEntity in groupPolicyEntities.Where(entity =>\n                         peopleAccessPolicies.GroupAccessPolicies.All(ap =>\n                             ((Core.SecretsManager.Entities.GroupProjectAccessPolicy)ap).GroupId !=\n                             ((GroupProjectAccessPolicy)entity).GroupId)))\n            {\n                dbContext.Remove(groupPolicyEntity);\n            }\n        }\n\n        await UpsertPeoplePoliciesAsync(dbContext,\n            peopleAccessPolicies.ToBaseAccessPolicies().Select(MapToEntity).ToList(), userPolicyEntities,\n            groupPolicyEntities);\n\n        await dbContext.SaveChangesAsync();\n        return await GetPeoplePoliciesByGrantedProjectIdAsync(peopleAccessPolicies.Id, userId);\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>>\n        GetPeoplePoliciesByGrantedServiceAccountIdAsync(Guid id, Guid userId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var entities = await dbContext.AccessPolicies.Where(ap =>\n                ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id ||\n                ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id)\n            .Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User)\n            .Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group)\n            .Select(ap => new\n            {\n                ap,\n                CurrentUserInGroup = ap is GroupServiceAccountAccessPolicy &&\n                                     ((GroupServiceAccountAccessPolicy)ap).Group.GroupUsers.Any(g =>\n                                         g.OrganizationUser.UserId == userId)\n            })\n            .ToListAsync();\n\n        return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup));\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> ReplaceServiceAccountPeopleAsync(\n        ServiceAccountPeopleAccessPolicies peopleAccessPolicies, Guid userId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var peoplePolicyEntities = await dbContext.AccessPolicies.Where(ap =>\n            ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId == peopleAccessPolicies.Id ||\n            ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == peopleAccessPolicies.Id).ToListAsync();\n\n        var userPolicyEntities =\n            peoplePolicyEntities.Where(ap => ap.GetType() == typeof(UserServiceAccountAccessPolicy)).ToList();\n        var groupPolicyEntities =\n            peoplePolicyEntities.Where(ap => ap.GetType() == typeof(GroupServiceAccountAccessPolicy)).ToList();\n\n\n        if (peopleAccessPolicies.UserAccessPolicies == null || !peopleAccessPolicies.UserAccessPolicies.Any())\n        {\n            dbContext.RemoveRange(userPolicyEntities);\n        }\n        else\n        {\n            foreach (var userPolicyEntity in userPolicyEntities.Where(entity =>\n                         peopleAccessPolicies.UserAccessPolicies.All(ap =>\n                             ((Core.SecretsManager.Entities.UserServiceAccountAccessPolicy)ap).OrganizationUserId !=\n                             ((UserServiceAccountAccessPolicy)entity).OrganizationUserId)))\n            {\n                dbContext.Remove(userPolicyEntity);\n            }\n        }\n\n        if (peopleAccessPolicies.GroupAccessPolicies == null || !peopleAccessPolicies.GroupAccessPolicies.Any())\n        {\n            dbContext.RemoveRange(groupPolicyEntities);\n        }\n        else\n        {\n            foreach (var groupPolicyEntity in groupPolicyEntities.Where(entity =>\n                         peopleAccessPolicies.GroupAccessPolicies.All(ap =>\n                             ((Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy)ap).GroupId !=\n                             ((GroupServiceAccountAccessPolicy)entity).GroupId)))\n            {\n                dbContext.Remove(groupPolicyEntity);\n            }\n        }\n\n        await UpsertPeoplePoliciesAsync(dbContext,\n            peopleAccessPolicies.ToBaseAccessPolicies().Select(MapToEntity).ToList(), userPolicyEntities,\n            groupPolicyEntities);\n        await dbContext.SaveChangesAsync();\n        return await GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId);\n    }\n\n    public async Task<ServiceAccountGrantedPolicies?> GetServiceAccountGrantedPoliciesAsync(Guid serviceAccountId)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var entities = await dbContext.ServiceAccountProjectAccessPolicy\n            .Where(ap => ap.ServiceAccountId == serviceAccountId)\n            .Include(ap => ap.ServiceAccount)\n            .Include(ap => ap.GrantedProject)\n            .ToListAsync();\n\n        if (entities.Count == 0)\n        {\n            return null;\n        }\n        return new ServiceAccountGrantedPolicies(serviceAccountId, entities.Select(MapToCore).ToList());\n    }\n\n    public async Task<ServiceAccountGrantedPoliciesPermissionDetails?>\n        GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Guid serviceAccountId, Guid userId,\n            AccessClientType accessClientType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var accessPolicyQuery = dbContext.ServiceAccountProjectAccessPolicy\n            .Where(ap => ap.ServiceAccountId == serviceAccountId)\n            .Include(ap => ap.ServiceAccount)\n            .Include(ap => ap.GrantedProject);\n\n        var accessPoliciesPermissionDetails =\n            await ToPermissionDetails(accessPolicyQuery, userId, accessClientType).ToListAsync();\n        if (accessPoliciesPermissionDetails.Count == 0)\n        {\n            return null;\n        }\n\n        return new ServiceAccountGrantedPoliciesPermissionDetails\n        {\n            ServiceAccountId = serviceAccountId,\n            OrganizationId = accessPoliciesPermissionDetails.First().AccessPolicy.GrantedProject!.OrganizationId,\n            ProjectGrantedPolicies = accessPoliciesPermissionDetails\n        };\n    }\n\n    public async Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates updates)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy\n            .Where(ap => ap.ServiceAccountId == updates.ServiceAccountId)\n            .ToListAsync();\n\n        if (currentAccessPolicies.Count != 0)\n        {\n            var projectIdsToDelete = updates.ProjectGrantedPolicyUpdates\n                .Where(pu => pu.Operation == AccessPolicyOperation.Delete)\n                .Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)\n                .ToList();\n\n            var policiesToDelete = currentAccessPolicies\n                .Where(entity => projectIdsToDelete.Contains(entity.GrantedProjectId!.Value))\n                .ToList();\n\n            dbContext.RemoveRange(policiesToDelete);\n        }\n\n        await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,\n            updates.ProjectGrantedPolicyUpdates.Where(pu => pu.Operation != AccessPolicyOperation.Delete).ToList());\n        await UpdateServiceAccountRevisionAsync(dbContext, updates.ServiceAccountId);\n        await dbContext.SaveChangesAsync();\n    }\n\n    public async Task<ProjectServiceAccountsAccessPolicies?> GetProjectServiceAccountsAccessPoliciesAsync(Guid projectId)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var entities = await dbContext.ServiceAccountProjectAccessPolicy\n            .Where(ap => ap.GrantedProjectId == projectId)\n            .Include(ap => ap.ServiceAccount)\n            .Include(ap => ap.GrantedProject)\n            .ToListAsync();\n\n        if (entities.Count == 0)\n        {\n            return null;\n        }\n\n        return new ProjectServiceAccountsAccessPolicies(projectId, entities.Select(MapToCore).ToList());\n    }\n\n    public async Task UpdateProjectServiceAccountsAccessPoliciesAsync(\n        ProjectServiceAccountsAccessPoliciesUpdates updates)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy\n            .Where(ap => ap.GrantedProjectId == updates.ProjectId)\n            .ToListAsync();\n\n        if (currentAccessPolicies.Count != 0)\n        {\n            var serviceAccountIdsToDelete = updates.ServiceAccountAccessPolicyUpdates\n                .Where(pu => pu.Operation == AccessPolicyOperation.Delete)\n                .Select(pu => pu.AccessPolicy.ServiceAccountId!.Value)\n                .ToList();\n\n            var accessPolicyIdsToDelete = currentAccessPolicies\n                .Where(entity => serviceAccountIdsToDelete.Contains(entity.ServiceAccountId!.Value))\n                .Select(ap => ap.Id)\n                .ToList();\n\n            await dbContext.ServiceAccountProjectAccessPolicy\n                .Where(ap => accessPolicyIdsToDelete.Contains(ap.Id))\n                .ExecuteDeleteAsync();\n        }\n\n        await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,\n            updates.ServiceAccountAccessPolicyUpdates.Where(update => update.Operation != AccessPolicyOperation.Delete)\n                .ToList());\n        var effectedServiceAccountIds = updates.ServiceAccountAccessPolicyUpdates\n            .Select(sa => sa.AccessPolicy.ServiceAccountId!.Value).ToList();\n        await UpdateServiceAccountsRevisionAsync(dbContext, effectedServiceAccountIds);\n        await dbContext.SaveChangesAsync();\n        await transaction.CommitAsync();\n    }\n\n    public async Task<SecretAccessPolicies?> GetSecretAccessPoliciesAsync(\n        Guid secretId,\n        Guid userId)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var entities = await dbContext.AccessPolicies.Where(ap =>\n                ((UserSecretAccessPolicy)ap).GrantedSecretId == secretId ||\n                ((GroupSecretAccessPolicy)ap).GrantedSecretId == secretId ||\n                ((ServiceAccountSecretAccessPolicy)ap).GrantedSecretId == secretId)\n            .Include(ap => ((UserSecretAccessPolicy)ap).OrganizationUser.User)\n            .Include(ap => ((GroupSecretAccessPolicy)ap).Group)\n            .Include(ap => ((ServiceAccountSecretAccessPolicy)ap).ServiceAccount)\n            .Select(ap => new\n            {\n                ap,\n                CurrentUserInGroup = ap is GroupSecretAccessPolicy &&\n                                     ((GroupSecretAccessPolicy)ap).Group.GroupUsers.Any(g =>\n                                         g.OrganizationUser.UserId == userId)\n            })\n            .ToListAsync();\n\n        if (entities.Count == 0)\n        {\n            return null;\n        }\n\n        var organizationId = await dbContext.Secret.Where(s => s.Id == secretId)\n            .Select(s => s.OrganizationId)\n            .SingleAsync();\n\n        return new SecretAccessPolicies(secretId, organizationId,\n            entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup)).ToList());\n    }\n\n    private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext,\n        List<BaseAccessPolicy> policies, IReadOnlyCollection<AccessPolicy> userPolicyEntities,\n        IReadOnlyCollection<AccessPolicy> groupPolicyEntities)\n    {\n        var currentDate = DateTime.UtcNow;\n        foreach (var updatedEntity in policies)\n        {\n            var currentEntity = updatedEntity switch\n            {\n                UserProjectAccessPolicy ap => userPolicyEntities.FirstOrDefault(e =>\n                    ((UserProjectAccessPolicy)e).OrganizationUserId == ap.OrganizationUserId),\n                GroupProjectAccessPolicy ap => groupPolicyEntities.FirstOrDefault(e =>\n                    ((GroupProjectAccessPolicy)e).GroupId == ap.GroupId),\n                UserServiceAccountAccessPolicy ap => userPolicyEntities.FirstOrDefault(e =>\n                    ((UserServiceAccountAccessPolicy)e).OrganizationUserId == ap.OrganizationUserId),\n                GroupServiceAccountAccessPolicy ap => groupPolicyEntities.FirstOrDefault(e =>\n                    ((GroupServiceAccountAccessPolicy)e).GroupId == ap.GroupId),\n                _ => null\n            };\n\n            if (currentEntity != null)\n            {\n                dbContext.AccessPolicies.Attach(currentEntity);\n                currentEntity.Read = updatedEntity.Read;\n                currentEntity.Write = updatedEntity.Write;\n                currentEntity.RevisionDate = currentDate;\n            }\n            else\n            {\n                updatedEntity.SetNewId();\n                await dbContext.AddAsync(updatedEntity);\n            }\n        }\n    }\n\n    private async Task UpsertServiceAccountProjectPoliciesAsync(DatabaseContext dbContext,\n        IReadOnlyCollection<ServiceAccountProjectAccessPolicy> currentPolices,\n        List<ServiceAccountProjectAccessPolicyUpdate> policyUpdates)\n    {\n        var currentDate = DateTime.UtcNow;\n        foreach (var policyUpdate in policyUpdates)\n        {\n            var updatedEntity = MapToEntity(policyUpdate.AccessPolicy);\n            var currentEntity = currentPolices.FirstOrDefault(e =>\n                e.GrantedProjectId == policyUpdate.AccessPolicy.GrantedProjectId!.Value &&\n                e.ServiceAccountId == policyUpdate.AccessPolicy.ServiceAccountId!.Value);\n\n            switch (policyUpdate.Operation)\n            {\n                case AccessPolicyOperation.Create when currentEntity == null:\n                    updatedEntity.SetNewId();\n                    await dbContext.AddAsync(updatedEntity);\n                    break;\n\n                case AccessPolicyOperation.Update when currentEntity != null:\n                    dbContext.AccessPolicies.Attach(currentEntity);\n                    currentEntity.Read = updatedEntity.Read;\n                    currentEntity.Write = updatedEntity.Write;\n                    currentEntity.RevisionDate = currentDate;\n                    break;\n                default:\n                    throw new InvalidOperationException(\"Policy updates failed due to unexpected state.\");\n            }\n        }\n    }\n\n    private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore(\n        BaseAccessPolicy baseAccessPolicyEntity) =>\n        baseAccessPolicyEntity switch\n        {\n            UserProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.UserProjectAccessPolicy>(ap),\n            UserSecretAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.UserSecretAccessPolicy>(ap),\n            UserServiceAccountAccessPolicy ap =>\n                Mapper.Map<Core.SecretsManager.Entities.UserServiceAccountAccessPolicy>(ap),\n            GroupProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.GroupProjectAccessPolicy>(ap),\n            GroupSecretAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.GroupSecretAccessPolicy>(ap),\n            GroupServiceAccountAccessPolicy ap => Mapper\n                .Map<Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy>(ap),\n            ServiceAccountProjectAccessPolicy ap => Mapper\n                .Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),\n            ServiceAccountSecretAccessPolicy ap => Mapper\n                .Map<Core.SecretsManager.Entities.ServiceAccountSecretAccessPolicy>(ap),\n            _ => throw new ArgumentException(\"Unsupported access policy type\")\n        };\n\n    private BaseAccessPolicy MapToEntity(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy)\n    {\n        return baseAccessPolicy switch\n        {\n            Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy => Mapper.Map<UserProjectAccessPolicy>(\n                accessPolicy),\n            Core.SecretsManager.Entities.UserSecretAccessPolicy accessPolicy => Mapper.Map<UserSecretAccessPolicy>(\n                accessPolicy),\n            Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy => Mapper\n                .Map<UserServiceAccountAccessPolicy>(accessPolicy),\n            Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy => Mapper.Map<GroupProjectAccessPolicy>(\n                accessPolicy),\n            Core.SecretsManager.Entities.GroupSecretAccessPolicy accessPolicy => Mapper.Map<GroupSecretAccessPolicy>(\n                accessPolicy),\n            Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy => Mapper\n                .Map<GroupServiceAccountAccessPolicy>(accessPolicy),\n            Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy => Mapper\n                .Map<ServiceAccountProjectAccessPolicy>(accessPolicy),\n            Core.SecretsManager.Entities.ServiceAccountSecretAccessPolicy accessPolicy => Mapper\n                .Map<ServiceAccountSecretAccessPolicy>(accessPolicy),\n            _ => throw new ArgumentException(\"Unsupported access policy type\")\n        };\n    }\n\n    private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore(\n        BaseAccessPolicy baseAccessPolicyEntity, bool currentUserInGroup)\n    {\n        switch (baseAccessPolicyEntity)\n        {\n            case GroupProjectAccessPolicy ap:\n                {\n                    var mapped = Mapper.Map<Core.SecretsManager.Entities.GroupProjectAccessPolicy>(ap);\n                    mapped.CurrentUserInGroup = currentUserInGroup;\n                    return mapped;\n                }\n            case GroupSecretAccessPolicy ap:\n                {\n                    var mapped = Mapper.Map<Core.SecretsManager.Entities.GroupSecretAccessPolicy>(ap);\n                    mapped.CurrentUserInGroup = currentUserInGroup;\n                    return mapped;\n                }\n            case GroupServiceAccountAccessPolicy ap:\n                {\n                    var mapped = Mapper.Map<Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy>(ap);\n                    mapped.CurrentUserInGroup = currentUserInGroup;\n                    return mapped;\n                }\n            default:\n                return MapToCore(baseAccessPolicyEntity);\n        }\n    }\n\n    private IQueryable<ServiceAccountProjectAccessPolicyPermissionDetails> ToPermissionDetails(\n        IQueryable<ServiceAccountProjectAccessPolicy>\n            query, Guid userId, AccessClientType accessClientType)\n    {\n        var permissionDetails = accessClientType switch\n        {\n            AccessClientType.NoAccessCheck => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails\n            {\n                AccessPolicy =\n                    Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),\n                HasPermission = true\n            }),\n            AccessClientType.User => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails\n            {\n                AccessPolicy =\n                    Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),\n                HasPermission =\n                    (ap.GrantedProject.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||\n                     ap.GrantedProject.GroupAccessPolicies.Any(p =>\n                         p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write))) &&\n                    (ap.ServiceAccount.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||\n                     ap.ServiceAccount.GroupAccessPolicies.Any(p =>\n                         p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write)))\n            }),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessClientType), accessClientType, null)\n        };\n        return permissionDetails;\n    }\n\n    private static async Task UpdateServiceAccountRevisionAsync(DatabaseContext dbContext, Guid serviceAccountId)\n    {\n        var entity = await dbContext.ServiceAccount.FindAsync(serviceAccountId);\n        if (entity != null)\n        {\n            entity.RevisionDate = DateTime.UtcNow;\n        }\n    }\n\n    private static async Task UpdateServiceAccountsRevisionAsync(DatabaseContext dbContext, List<Guid> serviceAccountIds)\n    {\n        var utcNow = DateTime.UtcNow;\n        await dbContext.ServiceAccount\n            .Where(sa => serviceAccountIds.Contains(sa.Id))\n            .ExecuteUpdateAsync(setters =>\n                setters.SetProperty(sa => sa.RevisionDate, utcNow));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs",
    "content": "﻿using System.Linq.Expressions;\nusing AutoMapper;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;\n\npublic class ProjectRepository : Repository<Core.SecretsManager.Entities.Project, Project, Guid>, IProjectRepository\n{\n    public ProjectRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, db => db.Project)\n    { }\n\n    public override async Task<Core.SecretsManager.Entities.Project?> GetByIdAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var project = await dbContext.Project\n                                    .AsNoTracking()\n                                    .Where(c => c.Id == id && c.DeletedDate == null)\n                                    .FirstOrDefaultAsync();\n            return Mapper.Map<Core.SecretsManager.Entities.Project>(project);\n        }\n    }\n\n    public async Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(\n        Guid organizationId,\n        Guid userId,\n        AccessClientType accessType)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var query = dbContext.Project.AsNoTracking().Where(p => p.OrganizationId == organizationId && p.DeletedDate == null).OrderBy(p => p.RevisionDate);\n\n        var projects = ProjectToPermissionDetails(query, userId, accessType);\n\n        return await projects.ToListAsync();\n    }\n\n    public async Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await dbContext.Project\n                .CountAsync(ou => ou.OrganizationId == organizationId);\n        }\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyByOrganizationIdWriteAccessAsync(\n        Guid organizationId, Guid userId, AccessClientType accessType)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.Project.AsNoTracking().Where(p => p.OrganizationId == organizationId && p.DeletedDate == null);\n\n        query = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasWriteAccessToProject(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        var projects = await query.OrderBy(p => p.RevisionDate).ToListAsync();\n        return Mapper.Map<List<Core.SecretsManager.Entities.Project>>(projects);\n    }\n\n    public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        var serviceAccountIds = await dbContext.Project\n            .Where(p => ids.Contains(p.Id))\n            .Include(p => p.ServiceAccountAccessPolicies)\n            .SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))\n            .Distinct()\n            .ToListAsync();\n\n        var secretIds = await dbContext.Project\n            .Where(p => ids.Contains(p.Id))\n            .Include(p => p.Secrets)\n            .SelectMany(p => p.Secrets.Select(s => s.Id))\n            .Distinct()\n            .ToListAsync();\n\n        var utcNow = DateTime.UtcNow;\n        if (serviceAccountIds.Count > 0)\n        {\n            await dbContext.ServiceAccount\n                .Where(sa => serviceAccountIds.Contains(sa.Id))\n                .ExecuteUpdateAsync(setters =>\n                    setters.SetProperty(sa => sa.RevisionDate, utcNow));\n        }\n\n        if (secretIds.Count > 0)\n        {\n            await dbContext.Secret\n                .Where(s => secretIds.Contains(s.Id))\n                .ExecuteUpdateAsync(setters =>\n                    setters.SetProperty(s => s.RevisionDate, utcNow));\n        }\n\n        await dbContext.Project.Where(p => ids.Contains(p.Id)).ExecuteDeleteAsync();\n        await transaction.CommitAsync();\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyWithSecretsByIds(IEnumerable<Guid> ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var projects = await dbContext.Project\n                .AsNoTracking()\n                .Include(p => p.Secrets)\n                .Where(c => ids.Contains(c.Id) && c.DeletedDate == null)\n                .ToListAsync();\n            return Mapper.Map<List<Core.SecretsManager.Entities.Project>>(projects);\n        }\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> ImportAsync(IEnumerable<Core.SecretsManager.Entities.Project> projects)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var entities = projects.Select(p => Mapper.Map<Project>(p));\n        var dbContext = GetDatabaseContext(scope);\n        await GetDbSet(dbContext).AddRangeAsync(entities);\n        await dbContext.SaveChangesAsync();\n        return projects;\n    }\n\n    public async Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var projectQuery = dbContext.Project\n            .AsNoTracking()\n            .Where(s => s.Id == id);\n\n        var accessQuery = BuildProjectAccessQuery(projectQuery, userId, accessType);\n        var policy = await accessQuery.FirstOrDefaultAsync();\n\n        return policy == null ? (false, false) : (policy.Read, policy.Write);\n    }\n\n    public async Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var results = await dbContext.Project.AsNoTracking().Where(p => p.OrganizationId == organizationId && projectIds.Contains(p.Id)).ToListAsync();\n\n        return projectIds.Count == results.Count;\n    }\n\n    public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(\n        IEnumerable<Guid> projectIds,\n        Guid userId,\n        AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var projectsQuery = dbContext.Project.AsNoTracking().Where(p => projectIds.Contains(p.Id));\n        var accessQuery = BuildProjectAccessQuery(projectsQuery, userId, accessType);\n\n        return await accessQuery.ToDictionaryAsync(pa => pa.Id, pa => (pa.Read, pa.Write));\n    }\n\n    public async Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.Project.AsNoTracking().Where(p => p.OrganizationId == organizationId && p.DeletedDate == null);\n\n        query = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasReadAccessToProject(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        return await query.CountAsync();\n    }\n\n    public async Task<ProjectCounts> GetProjectCountsByIdAsync(Guid projectId, Guid userId, AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.Project.AsNoTracking().Where(p => p.Id == projectId && p.DeletedDate == null);\n\n        var queryReadAccess = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasReadAccessToProject(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        var queryWriteAccess = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasWriteAccessToProject(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        var secretsQuery = queryReadAccess.Select(project => project.Secrets.Count(s => s.DeletedDate == null));\n\n        var projectCountsQuery = queryWriteAccess.Select(project => new ProjectCounts\n        {\n            People = project.UserAccessPolicies.Count + project.GroupAccessPolicies.Count,\n            ServiceAccounts = project.ServiceAccountAccessPolicies.Count\n        });\n\n        var secrets = await secretsQuery.FirstOrDefaultAsync();\n        var projectCounts = await projectCountsQuery.FirstOrDefaultAsync() ?? new ProjectCounts { Secrets = 0, People = 0, ServiceAccounts = 0 };\n        projectCounts.Secrets = secrets;\n\n        return projectCounts;\n    }\n\n    private record ProjectAccess(Guid Id, bool Read, bool Write);\n\n    private static IQueryable<ProjectAccess> BuildProjectAccessQuery(IQueryable<Project> projectQuery, Guid userId,\n        AccessClientType accessType) =>\n        accessType switch\n        {\n            AccessClientType.NoAccessCheck => projectQuery.Select(p => new ProjectAccess(p.Id, true, true)),\n            AccessClientType.User => projectQuery.Select(p => new ProjectAccess\n            (\n                p.Id,\n                p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||\n                p.GroupAccessPolicies.Any(ap =>\n                    ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),\n                p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||\n                p.GroupAccessPolicies.Any(ap =>\n                    ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))\n            )),\n            AccessClientType.ServiceAccount => projectQuery.Select(p => new ProjectAccess\n            (\n                p.Id,\n                p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read),\n                p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write)\n            )),\n            _ => projectQuery.Select(p => new ProjectAccess(p.Id, false, false))\n        };\n\n    private IQueryable<ProjectPermissionDetails> ProjectToPermissionDetails(IQueryable<Project> query, Guid userId, AccessClientType accessType)\n    {\n        var projects = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query.Select(p => new ProjectPermissionDetails\n            {\n                Project = Mapper.Map<Bit.Core.SecretsManager.Entities.Project>(p),\n                Read = true,\n                Write = true,\n            }),\n            AccessClientType.User => query.Where(UserHasReadAccessToProject(userId)).Select(ProjectToPermissionsUser(userId, true)),\n            AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToProject(userId)).Select(ProjectToPermissionsServiceAccount(userId, true)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n        return projects;\n    }\n\n    private Expression<Func<Project, ProjectPermissionDetails>> ProjectToPermissionsUser(Guid userId, bool read) =>\n        p => new ProjectPermissionDetails\n        {\n            Project = Mapper.Map<Bit.Core.SecretsManager.Entities.Project>(p),\n            Read = read,\n            Write = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||\n                    p.GroupAccessPolicies.Any(ap =>\n                        ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),\n        };\n\n    private Expression<Func<Project, ProjectPermissionDetails>> ProjectToPermissionsServiceAccount(Guid userId, bool read) =>\n        p => new ProjectPermissionDetails\n        {\n            Project = Mapper.Map<Bit.Core.SecretsManager.Entities.Project>(p),\n            Read = read,\n            Write = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == userId && ap.Write),\n        };\n\n    private static Expression<Func<Project, bool>> UserHasReadAccessToProject(Guid userId) => p =>\n        p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||\n        p.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));\n\n    private static Expression<Func<Project, bool>> UserHasWriteAccessToProject(Guid userId) => p =>\n        p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||\n        p.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));\n\n    private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>\n        p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs",
    "content": "﻿using System.Linq.Expressions;\nusing AutoMapper;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Infrastructure.EntityFramework;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;\n\npublic class SecretRepository : Repository<Core.SecretsManager.Entities.Secret, Secret, Guid>, ISecretRepository\n{\n    public SecretRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, db => db.Secret)\n    { }\n\n    public override async Task<Core.SecretsManager.Entities.Secret?> GetByIdAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var secret = await dbContext.Secret\n                                    .AsNoTracking()\n                                    .Include(\"Projects\")\n                                    .Where(c => c.Id == id && c.DeletedDate == null)\n                                    .FirstOrDefaultAsync();\n            return Mapper.Map<Core.SecretsManager.Entities.Secret>(secret);\n        }\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByIds(IEnumerable<Guid> ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var secrets = await dbContext.Secret\n                .AsNoTracking()\n                .Where(c => ids.Contains(c.Id) && c.DeletedDate == null)\n                .Include(c => c.Projects)\n                .ToListAsync();\n            return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);\n        }\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var secrets = await dbContext.Secret\n                .AsNoTracking()\n                .Where(c => ids.Contains(c.Id) && c.DeletedDate != null)\n                .Include(c => c.Projects)\n                .ToListAsync();\n            return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);\n        }\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(\n        Guid organizationId, Guid userId, AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.Secret\n            .AsNoTracking()\n            .Include(c => c.Projects)\n            .Where(c => c.OrganizationId == organizationId && c.DeletedDate == null);\n\n        query = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)),\n            AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null)\n        };\n\n        var secrets = await query.OrderBy(c => c.RevisionDate).ToListAsync();\n        return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);\n    }\n\n    public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(\n        Guid organizationId,\n        Guid userId,\n        AccessClientType accessType)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var query = dbContext.Secret\n            .AsNoTracking()\n            .Include(c => c.Projects)\n            .Where(c => c.OrganizationId == organizationId && c.DeletedDate == null)\n            .OrderBy(s => s.RevisionDate);\n\n        var secrets = SecretToPermissionDetails(query, userId, accessType);\n        return await secrets.ToListAsync();\n    }\n\n    public async Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await dbContext.Secret\n                .CountAsync(ou => ou.OrganizationId == organizationId && ou.DeletedDate == null);\n        }\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var secrets = await dbContext.Secret\n                                    .AsNoTracking()\n                                    .Where(s => ids.Contains(s.Id) && s.OrganizationId == organizationId && s.DeletedDate != null)\n                                    .Include(\"Projects\")\n                                    .OrderBy(c => c.RevisionDate)\n                                    .ToListAsync();\n            return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);\n        }\n    }\n\n    public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var secrets = await dbContext.Secret\n                                    .AsNoTracking()\n                                    .Where(c => c.OrganizationId == organizationId && c.DeletedDate != null)\n                                    .Include(\"Projects\")\n                                    .OrderBy(c => c.RevisionDate)\n                                    .ToListAsync();\n\n            // This should be changed if/when we allow non admins to access trashed items\n            return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets).Select(s => new SecretPermissionDetails\n            {\n                Secret = s,\n                Read = true,\n                Write = true,\n            });\n        }\n    }\n\n    public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.Secret.AsNoTracking().Include(s => s.Projects)\n            .Where(s => s.Projects.Any(p => p.Id == projectId) && s.DeletedDate == null);\n\n        var secrets = SecretToPermissionDetails(query, userId, accessType);\n        return await secrets.ToListAsync();\n    }\n\n    public async Task<Core.SecretsManager.Entities.Secret> CreateAsync(\n        Core.SecretsManager.Entities.Secret secret, SecretAccessPoliciesUpdates? accessPoliciesUpdates = null)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        secret.SetNewId();\n        var entity = Mapper.Map<Secret>(secret);\n\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        if (secret.Projects?.Count > 0)\n        {\n            foreach (var project in entity.Projects)\n            {\n                dbContext.Attach(project);\n            }\n\n            var projectIds = entity.Projects.Select(p => p.Id).ToList();\n            await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);\n        }\n\n        await dbContext.AddAsync(entity);\n        await UpdateSecretAccessPoliciesAsync(dbContext, entity, accessPoliciesUpdates);\n        await dbContext.SaveChangesAsync();\n        await transaction.CommitAsync();\n        return secret;\n    }\n\n    public async Task<Core.SecretsManager.Entities.Secret> UpdateAsync(Core.SecretsManager.Entities.Secret secret,\n        SecretAccessPoliciesUpdates? accessPoliciesUpdates = null)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var mappedEntity = Mapper.Map<Secret>(secret);\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        var entity = await dbContext.Secret\n            .Include(s => s.Projects)\n            .Include(s => s.UserAccessPolicies)\n            .Include(s => s.GroupAccessPolicies)\n            .Include(s => s.ServiceAccountAccessPolicies)\n            .FirstAsync(s => s.Id == secret.Id);\n\n        dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);\n\n        if (secret.Projects != null)\n        {\n            entity = await UpdateProjectMappingAsync(dbContext, entity, mappedEntity);\n        }\n\n        if (accessPoliciesUpdates != null)\n        {\n            await UpdateSecretAccessPoliciesAsync(dbContext, entity, accessPoliciesUpdates);\n        }\n\n        await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, [entity.Id]);\n        await dbContext.SaveChangesAsync();\n        await transaction.CommitAsync();\n        return Mapper.Map<Core.SecretsManager.Entities.Secret>(entity);\n    }\n\n\n    public async Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        var secretIds = ids.ToList();\n        await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);\n\n        var utcNow = DateTime.UtcNow;\n\n        await dbContext.Secret.Where(c => secretIds.Contains(c.Id))\n            .ExecuteUpdateAsync(setters =>\n                setters.SetProperty(s => s.RevisionDate, utcNow)\n                    .SetProperty(s => s.DeletedDate, utcNow));\n\n        await transaction.CommitAsync();\n    }\n\n    public async Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        var secretIds = ids.ToList();\n        await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);\n\n        await dbContext.Secret.Where(c => secretIds.Contains(c.Id))\n            .ExecuteDeleteAsync();\n\n        await transaction.CommitAsync();\n    }\n\n    public async Task RestoreManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        var secretIds = ids.ToList();\n        await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);\n\n        var utcNow = DateTime.UtcNow;\n\n        await dbContext.Secret.Where(c => secretIds.Contains(c.Id))\n            .ExecuteUpdateAsync(setters =>\n                setters.SetProperty(s => s.RevisionDate, utcNow)\n                    .SetProperty(s => s.DeletedDate, (DateTime?)null));\n\n        await transaction.CommitAsync();\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> ImportAsync(IEnumerable<Core.SecretsManager.Entities.Secret> secrets)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entities = new List<Secret>();\n            var projects = secrets\n                .SelectMany(s => s.Projects ?? Enumerable.Empty<Core.SecretsManager.Entities.Project>())\n                .DistinctBy(p => p.Id)\n                .Select(p => Mapper.Map<Project>(p))\n                .ToDictionary(p => p.Id, p => p);\n\n            dbContext.AttachRange(projects.Values);\n\n            foreach (var s in secrets)\n            {\n                var entity = Mapper.Map<Secret>(s);\n\n                if (s.Projects?.Count > 0)\n                {\n                    entity.Projects = s.Projects.Select(p => projects[p.Id]).ToList();\n                }\n\n                entities.Add(entity);\n            }\n            await GetDbSet(dbContext).AddRangeAsync(entities);\n            await dbContext.SaveChangesAsync();\n        }\n        return secrets;\n    }\n\n    public async Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var secret = dbContext.Secret\n            .AsNoTracking()\n            .Where(s => s.Id == id);\n\n        var query = BuildSecretAccessQuery(secret, userId, accessType);\n\n        var policy = await query.FirstOrDefaultAsync();\n        return policy == null ? (false, false) : (policy.Read, policy.Write);\n    }\n\n    public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToSecretsAsync(\n        IEnumerable<Guid> ids,\n        Guid userId,\n        AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var secrets = dbContext.Secret\n            .AsNoTracking()\n            .Where(s => ids.Contains(s.Id));\n\n        var accessQuery = BuildSecretAccessQuery(secrets, userId, accessType);\n\n        return await accessQuery.ToDictionaryAsync(sa => sa.Id, sa => (sa.Read, sa.Write));\n    }\n\n    public async Task EmptyTrash(DateTime currentDate, uint deleteAfterThisNumberOfDays)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        await dbContext.Secret.Where(s => s.DeletedDate != null && s.DeletedDate < currentDate.AddDays(-deleteAfterThisNumberOfDays)).ExecuteDeleteAsync();\n\n        await dbContext.SaveChangesAsync();\n    }\n\n    public async Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.Secret.AsNoTracking().Where(s => s.OrganizationId == organizationId && s.DeletedDate == null);\n\n        query = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        return await query.CountAsync();\n    }\n\n    private IQueryable<SecretPermissionDetails> SecretToPermissionDetails(IQueryable<Secret> query, Guid userId, AccessClientType accessType)\n    {\n        var secrets = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query.Select(s => new SecretPermissionDetails\n            {\n                Secret = Mapper.Map<Bit.Core.SecretsManager.Entities.Secret>(s),\n                Read = true,\n                Write = true,\n            }),\n            AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)).Select(SecretToPermissionsUser(userId, true)),\n            AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)).Select(s =>\n                new SecretPermissionDetails\n                {\n                    Secret = Mapper.Map<Bit.Core.SecretsManager.Entities.Secret>(s),\n                    Read = true,\n                    Write = s.Projects.Any(p =>\n                        p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write)),\n                }),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n        return secrets;\n    }\n\n    private Expression<Func<Secret, SecretPermissionDetails>> SecretToPermissionsUser(Guid userId, bool read) =>\n        s => new SecretPermissionDetails\n        {\n            Secret = Mapper.Map<Core.SecretsManager.Entities.Secret>(s),\n            Read = read,\n            Write =\n                s.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||\n                s.GroupAccessPolicies.Any(ap =>\n                    ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)) ||\n                s.Projects.Any(p =>\n                    p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||\n                    p.GroupAccessPolicies.Any(ap =>\n                        ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)))\n        };\n\n    private static Expression<Func<Secret, bool>> ServiceAccountHasReadAccessToSecret(Guid serviceAccountId) => s =>\n        s.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == serviceAccountId && ap.Read) ||\n        s.Projects.Any(p =>\n            p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read));\n\n    private static Expression<Func<Secret, bool>> UserHasReadAccessToSecret(Guid userId) => s =>\n        s.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||\n        s.GroupAccessPolicies.Any(ap =>\n            ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read)) ||\n        s.Projects.Any(p =>\n            p.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||\n            p.GroupAccessPolicies.Any(ap =>\n                ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read)));\n\n    private static async Task UpdateServiceAccountRevisionsByProjectIdsAsync(DatabaseContext dbContext,\n        List<Guid> projectIds)\n    {\n        if (projectIds.Count == 0)\n        {\n            return;\n        }\n\n        var serviceAccountIds = await dbContext.Project.Where(p => projectIds.Contains(p.Id))\n            .Include(p => p.ServiceAccountAccessPolicies)\n            .SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))\n            .Distinct()\n            .ToListAsync();\n\n        await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);\n    }\n\n    private static async Task UpdateServiceAccountRevisionsBySecretIdsAsync(DatabaseContext dbContext,\n        List<Guid> secretIds)\n    {\n        if (secretIds.Count == 0)\n        {\n            return;\n        }\n\n        var projectAccessServiceAccountIds = await dbContext.Secret\n            .Where(s => secretIds.Contains(s.Id))\n            .SelectMany(s =>\n                s.Projects.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value)))\n            .Distinct()\n            .ToListAsync();\n\n        var directAccessServiceAccountIds = await dbContext.Secret\n            .Where(s => secretIds.Contains(s.Id))\n            .SelectMany(s => s.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))\n            .Distinct()\n            .ToListAsync();\n\n        var serviceAccountIds =\n            directAccessServiceAccountIds.Concat(projectAccessServiceAccountIds).Distinct().ToList();\n\n        await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);\n    }\n\n    private static async Task UpdateServiceAccountRevisionsAsync(DatabaseContext dbContext,\n        List<Guid> serviceAccountIds)\n    {\n        if (serviceAccountIds.Count > 0)\n        {\n            var utcNow = DateTime.UtcNow;\n            await dbContext.ServiceAccount\n                .Where(sa => serviceAccountIds.Contains(sa.Id))\n                .ExecuteUpdateAsync(setters => setters.SetProperty(b => b.RevisionDate, utcNow));\n        }\n    }\n\n    private static IQueryable<SecretAccess> BuildSecretAccessQuery(IQueryable<Secret> secrets, Guid accessClientId,\n        AccessClientType accessType) =>\n        accessType switch\n        {\n            AccessClientType.NoAccessCheck => secrets.Select(s => new SecretAccess(s.Id, true, true)),\n            AccessClientType.User => secrets.Select(s => new SecretAccess(\n                s.Id,\n                s.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == accessClientId && ap.Read) ||\n                s.GroupAccessPolicies.Any(ap =>\n                    ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == accessClientId && ap.Read)) ||\n                s.Projects.Any(p =>\n                    p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == accessClientId && ap.Read) ||\n                    p.GroupAccessPolicies.Any(ap =>\n                        ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == accessClientId && ap.Read))),\n                s.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == accessClientId && ap.Write) ||\n                s.GroupAccessPolicies.Any(ap =>\n                    ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == accessClientId && ap.Write)) ||\n                s.Projects.Any(p =>\n                    p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == accessClientId && ap.Write) ||\n                    p.GroupAccessPolicies.Any(ap =>\n                        ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == accessClientId && ap.Write)))\n            )),\n            AccessClientType.ServiceAccount => secrets.Select(s => new SecretAccess(\n                s.Id,\n                s.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == accessClientId && ap.Read) ||\n                s.Projects.Any(p =>\n                    p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == accessClientId && ap.Read)),\n                s.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == accessClientId && ap.Write) ||\n                s.Projects.Any(p =>\n                    p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == accessClientId && ap.Write))\n            )),\n            _ => secrets.Select(s => new SecretAccess(s.Id, false, false))\n        };\n\n    private static async Task<Secret> UpdateProjectMappingAsync(DatabaseContext dbContext, Secret currentEntity, Secret updatedEntity)\n    {\n        var projectsToRemove = currentEntity.Projects.Where(p => updatedEntity.Projects.All(mp => mp.Id != p.Id)).ToList();\n        var projectsToAdd = updatedEntity.Projects.Where(p => currentEntity.Projects.All(ep => ep.Id != p.Id)).ToList();\n\n        foreach (var p in projectsToRemove)\n        {\n            currentEntity.Projects.Remove(p);\n        }\n\n        foreach (var project in projectsToAdd)\n        {\n            var p = dbContext.AttachToOrGet<Project>(x => x.Id == project.Id, () => project);\n            currentEntity.Projects.Add(p);\n        }\n\n        var projectIds = projectsToRemove.Select(p => p.Id).Concat(projectsToAdd.Select(p => p.Id)).ToList();\n        if (projectIds.Count > 0)\n        {\n            await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);\n        }\n\n        return currentEntity;\n    }\n\n    private static async Task DeleteSecretAccessPoliciesAsync(DatabaseContext dbContext, Secret entity,\n        SecretAccessPoliciesUpdates accessPoliciesUpdates)\n    {\n        var userAccessPoliciesIdsToDelete = entity.UserAccessPolicies.Where(uap => accessPoliciesUpdates\n                .UserAccessPolicyUpdates\n                .Any(apu => apu.Operation == AccessPolicyOperation.Delete &&\n                            apu.AccessPolicy.OrganizationUserId == uap.OrganizationUserId))\n            .Select(uap => uap.Id)\n            .ToList();\n\n        var groupAccessPoliciesIdsToDelete = entity.GroupAccessPolicies.Where(gap => accessPoliciesUpdates\n                .GroupAccessPolicyUpdates\n                .Any(apu => apu.Operation == AccessPolicyOperation.Delete && apu.AccessPolicy.GroupId == gap.GroupId))\n            .Select(gap => gap.Id)\n            .ToList();\n\n        var serviceAccountAccessPoliciesIdsToDelete = entity.ServiceAccountAccessPolicies.Where(gap =>\n                accessPoliciesUpdates.ServiceAccountAccessPolicyUpdates\n                    .Any(apu => apu.Operation == AccessPolicyOperation.Delete &&\n                                apu.AccessPolicy.ServiceAccountId == gap.ServiceAccountId))\n            .Select(sap => sap.Id)\n            .ToList();\n\n        var accessPoliciesIdsToDelete = userAccessPoliciesIdsToDelete\n            .Concat(groupAccessPoliciesIdsToDelete)\n            .Concat(serviceAccountAccessPoliciesIdsToDelete)\n            .ToList();\n\n        await dbContext.AccessPolicies\n            .Where(ap => accessPoliciesIdsToDelete.Contains(ap.Id))\n            .ExecuteDeleteAsync();\n    }\n\n    private static async Task UpsertSecretAccessPolicyAsync(DatabaseContext dbContext, BaseAccessPolicy updatedEntity,\n        AccessPolicyOperation accessPolicyOperation, AccessPolicy? currentEntity, DateTime currentDate)\n    {\n        switch (accessPolicyOperation)\n        {\n            case AccessPolicyOperation.Create when currentEntity == null:\n                updatedEntity.SetNewId();\n                await dbContext.AddAsync(updatedEntity);\n                break;\n\n            case AccessPolicyOperation.Update when currentEntity != null:\n                dbContext.AccessPolicies.Attach(currentEntity);\n                currentEntity.Read = updatedEntity.Read;\n                currentEntity.Write = updatedEntity.Write;\n                currentEntity.RevisionDate = currentDate;\n                break;\n            default:\n                throw new InvalidOperationException(\"Policy updates failed due to unexpected state.\");\n        }\n    }\n\n    private async Task UpsertSecretAccessPoliciesAsync(DatabaseContext dbContext,\n        Secret entity,\n        SecretAccessPoliciesUpdates policyUpdates)\n    {\n        var currentDate = DateTime.UtcNow;\n\n        foreach (var policyUpdate in policyUpdates.UserAccessPolicyUpdates.Where(apu =>\n                     apu.Operation != AccessPolicyOperation.Delete))\n        {\n            var currentEntity = entity.UserAccessPolicies?.FirstOrDefault(e =>\n                e.OrganizationUserId == policyUpdate.AccessPolicy.OrganizationUserId!.Value);\n\n            await UpsertSecretAccessPolicyAsync(dbContext, MapToEntity(policyUpdate.AccessPolicy),\n                policyUpdate.Operation,\n                currentEntity,\n                currentDate);\n        }\n\n        foreach (var policyUpdate in policyUpdates.GroupAccessPolicyUpdates.Where(apu =>\n                     apu.Operation != AccessPolicyOperation.Delete))\n        {\n            var currentEntity = entity.GroupAccessPolicies?.FirstOrDefault(e =>\n                e.GroupId == policyUpdate.AccessPolicy.GroupId!.Value);\n\n            await UpsertSecretAccessPolicyAsync(dbContext, MapToEntity(policyUpdate.AccessPolicy),\n                policyUpdate.Operation,\n                currentEntity,\n                currentDate);\n        }\n\n        foreach (var policyUpdate in policyUpdates.ServiceAccountAccessPolicyUpdates.Where(apu =>\n                     apu.Operation != AccessPolicyOperation.Delete))\n        {\n            var currentEntity = entity.ServiceAccountAccessPolicies?.FirstOrDefault(e =>\n                e.ServiceAccountId == policyUpdate.AccessPolicy.ServiceAccountId!.Value);\n\n            await UpsertSecretAccessPolicyAsync(dbContext, MapToEntity(policyUpdate.AccessPolicy),\n                policyUpdate.Operation,\n                currentEntity,\n                currentDate);\n        }\n    }\n\n    private async Task UpdateSecretAccessPoliciesAsync(DatabaseContext dbContext,\n        Secret entity,\n        SecretAccessPoliciesUpdates? accessPoliciesUpdates)\n    {\n        if (accessPoliciesUpdates == null || !accessPoliciesUpdates.HasUpdates())\n        {\n            return;\n        }\n\n        if ((entity.UserAccessPolicies != null && entity.UserAccessPolicies.Count != 0) ||\n            (entity.GroupAccessPolicies != null && entity.GroupAccessPolicies.Count != 0) ||\n            (entity.ServiceAccountAccessPolicies != null && entity.ServiceAccountAccessPolicies.Count != 0))\n        {\n            await DeleteSecretAccessPoliciesAsync(dbContext, entity, accessPoliciesUpdates);\n        }\n\n        await UpsertSecretAccessPoliciesAsync(dbContext, entity, accessPoliciesUpdates);\n\n        await UpdateServiceAccountRevisionsAsync(dbContext,\n            accessPoliciesUpdates.ServiceAccountAccessPolicyUpdates\n                .Select(sap => sap.AccessPolicy.ServiceAccountId!.Value).ToList());\n    }\n\n    private BaseAccessPolicy MapToEntity(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy) =>\n        baseAccessPolicy switch\n        {\n            Core.SecretsManager.Entities.UserSecretAccessPolicy accessPolicy => Mapper.Map<UserSecretAccessPolicy>(\n                accessPolicy),\n            Core.SecretsManager.Entities.GroupSecretAccessPolicy accessPolicy => Mapper.Map<GroupSecretAccessPolicy>(\n                accessPolicy),\n            Core.SecretsManager.Entities.ServiceAccountSecretAccessPolicy accessPolicy => Mapper\n                .Map<ServiceAccountSecretAccessPolicy>(accessPolicy),\n            _ => throw new ArgumentException(\"Unsupported access policy type\")\n        };\n\n    private record SecretAccess(Guid Id, bool Read, bool Write);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretVersionRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;\n\npublic class SecretVersionRepository : Repository<Core.SecretsManager.Entities.SecretVersion, SecretVersion, Guid>, ISecretVersionRepository\n{\n    public SecretVersionRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, db => db.SecretVersion)\n    { }\n\n    public override async Task<Core.SecretsManager.Entities.SecretVersion?> GetByIdAsync(Guid id)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var secretVersion = await dbContext.SecretVersion\n            .Where(sv => sv.Id == id)\n            .FirstOrDefaultAsync();\n        return Mapper.Map<Core.SecretsManager.Entities.SecretVersion>(secretVersion);\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.SecretVersion>> GetManyBySecretIdAsync(Guid secretId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var secretVersions = await dbContext.SecretVersion\n            .Where(sv => sv.SecretId == secretId)\n            .OrderByDescending(sv => sv.VersionDate)\n            .ToListAsync();\n        return Mapper.Map<List<Core.SecretsManager.Entities.SecretVersion>>(secretVersions);\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var versionIds = ids.ToList();\n        var secretVersions = await dbContext.SecretVersion\n            .Where(sv => versionIds.Contains(sv.Id))\n            .OrderByDescending(sv => sv.VersionDate)\n            .ToListAsync();\n        return Mapper.Map<List<Core.SecretsManager.Entities.SecretVersion>>(secretVersions);\n    }\n\n    public override async Task<Core.SecretsManager.Entities.SecretVersion> CreateAsync(Core.SecretsManager.Entities.SecretVersion secretVersion)\n    {\n        const int maxVersionsToKeep = 10;\n\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        // Get the IDs of the most recent (maxVersionsToKeep - 1) versions to keep\n        var versionsToKeepIds = await dbContext.SecretVersion\n            .Where(sv => sv.SecretId == secretVersion.SecretId)\n            .OrderByDescending(sv => sv.VersionDate)\n            .Take(maxVersionsToKeep - 1)\n            .Select(sv => sv.Id)\n            .ToListAsync();\n\n        // Delete all versions for this secret that are not in the \"keep\" list\n        if (versionsToKeepIds.Any())\n        {\n            await dbContext.SecretVersion\n                .Where(sv => sv.SecretId == secretVersion.SecretId && !versionsToKeepIds.Contains(sv.Id))\n                .ExecuteDeleteAsync();\n        }\n\n        secretVersion.SetNewId();\n        var entity = Mapper.Map<SecretVersion>(secretVersion);\n\n        await dbContext.AddAsync(entity);\n        await dbContext.SaveChangesAsync();\n        await transaction.CommitAsync();\n\n        return secretVersion;\n    }\n\n    public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var secretVersionIds = ids.ToList();\n        await dbContext.SecretVersion\n            .Where(sv => secretVersionIds.Contains(sv.Id))\n            .ExecuteDeleteAsync();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs",
    "content": "﻿using System.Linq.Expressions;\nusing AutoMapper;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;\n\npublic class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.ServiceAccount, ServiceAccount, Guid>, IServiceAccountRepository\n{\n    public ServiceAccountRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, db => db.ServiceAccount)\n    { }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.ServiceAccount.Where(c => c.OrganizationId == organizationId);\n\n        query = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasReadAccessToServiceAccount(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        var serviceAccounts = await query.OrderBy(c => c.RevisionDate).ToListAsync();\n        return Mapper.Map<List<Core.SecretsManager.Entities.ServiceAccount>>(serviceAccounts);\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.ServiceAccount>> GetManyByIds(IEnumerable<Guid> ids)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var serviceAccounts = await dbContext.ServiceAccount\n            .Where(c => ids.Contains(c.Id))\n            .ToListAsync();\n        return Mapper.Map<List<Core.SecretsManager.Entities.ServiceAccount>>(serviceAccounts);\n    }\n\n    public async Task<IEnumerable<Core.SecretsManager.Entities.ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.ServiceAccount.Where(c => c.OrganizationId == organizationId);\n\n        query = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasWriteAccessToServiceAccount(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        var serviceAccounts = await query.OrderBy(c => c.RevisionDate).ToListAsync();\n        return Mapper.Map<List<Core.SecretsManager.Entities.ServiceAccount>>(serviceAccounts);\n    }\n\n    public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        var targetIds = ids.ToList();\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        // Policies can't have a cascade delete, so we need to delete them manually.\n        await dbContext.AccessPolicies.Where(ap =>\n                targetIds.Contains(((ServiceAccountProjectAccessPolicy)ap).ServiceAccountId!.Value) ||\n                targetIds.Contains(((ServiceAccountSecretAccessPolicy)ap).ServiceAccountId!.Value) ||\n                targetIds.Contains(((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId!.Value) ||\n                targetIds.Contains(((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId!.Value))\n            .ExecuteDeleteAsync();\n\n        await dbContext.ApiKeys\n            .Where(a => targetIds.Contains(a.ServiceAccountId!.Value))\n            .ExecuteDeleteAsync();\n\n        await dbContext.ServiceAccount\n            .Where(c => targetIds.Contains(c.Id))\n            .ExecuteDeleteAsync();\n\n        await transaction.CommitAsync();\n    }\n\n    public async Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId,\n        AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var serviceAccountQuery = dbContext.ServiceAccount.Where(sa => sa.Id == id);\n\n        var accessQuery = BuildServiceAccountAccessQuery(serviceAccountQuery, userId, accessType);\n        var access = await accessQuery.FirstOrDefaultAsync();\n\n        return access == null ? (false, false) : (access.Read, access.Write);\n    }\n\n    public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(\n        IEnumerable<Guid> ids,\n        Guid userId,\n        AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var serviceAccountsQuery = dbContext.ServiceAccount.Where(p => ids.Contains(p.Id));\n        var accessQuery = BuildServiceAccountAccessQuery(serviceAccountsQuery, userId, accessType);\n\n        return await accessQuery.ToDictionaryAsync(access => access.Id, access => (access.Read, access.Write));\n    }\n\n    public async Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await dbContext.ServiceAccount\n                .CountAsync(ou => ou.OrganizationId == organizationId);\n        }\n    }\n\n    public async Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.ServiceAccount.Where(sa => sa.OrganizationId == organizationId);\n\n        query = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasReadAccessToServiceAccount(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        return await query.CountAsync();\n    }\n\n    public async Task<ServiceAccountCounts> GetServiceAccountCountsByIdAsync(Guid serviceAccountId, Guid userId,\n        AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.ServiceAccount.Where(sa => sa.Id == serviceAccountId);\n\n        query = accessType switch\n        {\n            AccessClientType.NoAccessCheck => query,\n            AccessClientType.User => query.Where(UserHasReadAccessToServiceAccount(userId)),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        var serviceAccountCountsQuery = query.Select(serviceAccount => new ServiceAccountCounts\n        {\n            Projects = serviceAccount.ProjectAccessPolicies.Count,\n            People = serviceAccount.UserAccessPolicies.Count + serviceAccount.GroupAccessPolicies.Count,\n            AccessTokens = serviceAccount.ApiKeys.Count\n        });\n\n        var serviceAccountCounts = await serviceAccountCountsQuery.FirstOrDefaultAsync();\n        return serviceAccountCounts ?? new ServiceAccountCounts { Projects = 0, People = 0, AccessTokens = 0 };\n    }\n\n    public async Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var result = await dbContext.ServiceAccount.CountAsync(sa =>\n            sa.OrganizationId == organizationId && serviceAccountIds.Contains(sa.Id));\n        return serviceAccountIds.Count == result;\n    }\n\n    public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(\n        Guid organizationId, Guid userId, AccessClientType accessType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var serviceAccountQuery = dbContext.ServiceAccount.Where(c => c.OrganizationId == organizationId);\n        serviceAccountQuery = accessType switch\n        {\n            AccessClientType.NoAccessCheck => serviceAccountQuery,\n            AccessClientType.User => serviceAccountQuery.Where(c =>\n                c.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||\n                c.GroupAccessPolicies.Any(ap =>\n                    ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read))),\n            _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),\n        };\n\n        var projectSecretsAccessQuery = BuildProjectSecretsAccessQuery(dbContext, serviceAccountQuery);\n        var directSecretAccessQuery = BuildDirectSecretAccessQuery(dbContext, serviceAccountQuery);\n\n        var projectSecretsAccessResults = await projectSecretsAccessQuery.ToListAsync();\n        var directSecretAccessResults = await directSecretAccessQuery.ToListAsync();\n\n        var applicableDirectSecretAccessResults = FilterDirectSecretAccessResults(projectSecretsAccessResults, directSecretAccessResults);\n\n        var results = projectSecretsAccessResults.Concat(applicableDirectSecretAccessResults)\n            .GroupBy(g => g.ServiceAccount)\n            .Select(g =>\n                new ServiceAccountSecretsDetails\n                {\n                    ServiceAccount = Mapper.Map<Core.SecretsManager.Entities.ServiceAccount>(g.Key),\n                    AccessToSecrets = g.Sum(x => x.SecretIds.Count())\n                }).OrderBy(c => c.ServiceAccount.RevisionDate).ToList();\n\n        return results;\n    }\n\n    private record ServiceAccountAccess(Guid Id, bool Read, bool Write);\n\n    private static IQueryable<ServiceAccountAccess> BuildServiceAccountAccessQuery(IQueryable<ServiceAccount> serviceAccountQuery, Guid userId,\n        AccessClientType accessType) =>\n        accessType switch\n        {\n            AccessClientType.NoAccessCheck => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, true, true)),\n            AccessClientType.User => serviceAccountQuery.Select(sa => new ServiceAccountAccess\n            (\n                sa.Id,\n                sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||\n                sa.GroupAccessPolicies.Any(ap =>\n                    ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),\n                sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||\n                sa.GroupAccessPolicies.Any(ap =>\n                    ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))\n            )),\n            AccessClientType.ServiceAccount => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false)),\n            _ => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false))\n        };\n\n    private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>\n        sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||\n        sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));\n\n    private static Expression<Func<ServiceAccount, bool>> UserHasWriteAccessToServiceAccount(Guid userId) => sa =>\n        sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||\n        sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));\n\n    private static IQueryable<ServiceAccountSecretsAccess> BuildProjectSecretsAccessQuery(DatabaseContext dbContext,\n        IQueryable<ServiceAccount> serviceAccountQuery) =>\n        from sa in serviceAccountQuery\n        join ap in dbContext.ServiceAccountProjectAccessPolicy\n            on sa.Id equals ap.ServiceAccountId into grouping\n        from ap in grouping.DefaultIfEmpty()\n        select new ServiceAccountSecretsAccess\n        (\n            sa, ap.GrantedProject.Secrets.Where(s => s.DeletedDate == null).Select(s => s.Id)\n        );\n\n    private static IQueryable<ServiceAccountSecretsAccess> BuildDirectSecretAccessQuery(\n        DatabaseContext dbContext,\n        IQueryable<ServiceAccount> serviceAccountQuery) =>\n        from sa in serviceAccountQuery\n        join ap in dbContext.ServiceAccountSecretAccessPolicy\n            on sa.Id equals ap.ServiceAccountId into grouping\n        from ap in grouping.DefaultIfEmpty()\n        where ap.GrantedSecret.DeletedDate == null &&\n              ap.GrantedSecretId != null\n        select new ServiceAccountSecretsAccess(sa,\n            new List<Guid> { ap.GrantedSecretId!.Value });\n\n    private static List<ServiceAccountSecretsAccess> FilterDirectSecretAccessResults(\n        List<ServiceAccountSecretsAccess> projectSecretsAccessResults,\n        List<ServiceAccountSecretsAccess> directSecretAccessResults) =>\n        directSecretAccessResults.Where(directSecretAccessResult =>\n        {\n            var serviceAccountId = directSecretAccessResult.ServiceAccount.Id;\n            var secretId = directSecretAccessResult.SecretIds.FirstOrDefault();\n            if (secretId == Guid.Empty)\n            {\n                return false;\n            }\n\n            return !projectSecretsAccessResults\n                .Where(x => x.ServiceAccount.Id == serviceAccountId)\n                .Any(x => x.SecretIds.Contains(secretId));\n        }).ToList();\n\n    private record ServiceAccountSecretsAccess(ServiceAccount ServiceAccount, IEnumerable<Guid> SecretIds);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/SecretsManagerEFServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager;\n\npublic static class SecretsManagerEfServiceCollectionExtensions\n{\n    public static void AddSecretsManagerEfRepositories(this IServiceCollection services)\n    {\n        services.AddSingleton<IAccessPolicyRepository, AccessPolicyRepository>();\n        services.AddSingleton<ISecretRepository, SecretRepository>();\n        services.AddSingleton<ISecretVersionRepository, SecretVersionRepository>();\n        services.AddSingleton<IProjectRepository, ProjectRepository>();\n        services.AddSingleton<IServiceAccountRepository, ServiceAccountRepository>();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Context/IScimContext.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\n\nnamespace Bit.Scim.Context;\n\npublic interface IScimContext\n{\n    ScimProviderType RequestScimProvider { get; set; }\n    ScimConfig ScimConfiguration { get; set; }\n    Guid? OrganizationId { get; set; }\n    Organization Organization { get; set; }\n    Task BuildAsync(\n        HttpContext httpContext,\n        GlobalSettings globalSettings,\n        IOrganizationRepository organizationRepository,\n        IOrganizationConnectionRepository organizationConnectionRepository);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Context/ScimContext.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Scim.Context;\n\npublic class ScimContext : IScimContext\n{\n    private bool _builtHttpContext;\n\n    // See IP list from Ping in docs: https://support.pingidentity.com/s/article/PingOne-IP-Addresses\n    private static readonly HashSet<string> _pingIpAddresses =\n    [\n        \"18.217.152.87\",\n        \"52.14.10.143\",\n        \"13.58.49.148\",\n        \"34.211.92.81\",\n        \"54.214.158.219\",\n        \"34.218.98.164\",\n        \"15.223.133.47\",\n        \"3.97.84.38\",\n        \"15.223.19.71\",\n        \"3.97.98.120\",\n        \"52.60.115.173\",\n        \"3.97.202.223\",\n        \"18.184.65.93\",\n        \"52.57.244.92\",\n        \"18.195.7.252\",\n        \"108.128.67.71\",\n        \"34.246.158.102\",\n        \"108.128.250.27\",\n        \"52.63.103.92\",\n        \"13.54.131.18\",\n        \"52.62.204.36\"\n    ];\n\n    public ScimProviderType RequestScimProvider { get; set; } = ScimProviderType.Default;\n    public ScimConfig ScimConfiguration { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Organization Organization { get; set; }\n\n    public async virtual Task BuildAsync(\n        HttpContext httpContext,\n        GlobalSettings globalSettings,\n        IOrganizationRepository organizationRepository,\n        IOrganizationConnectionRepository organizationConnectionRepository)\n    {\n        if (_builtHttpContext)\n        {\n            return;\n        }\n\n        _builtHttpContext = true;\n\n        string orgIdString = null;\n        if (httpContext.Request.RouteValues.TryGetValue(\"organizationId\", out var orgIdObject))\n        {\n            orgIdString = orgIdObject?.ToString();\n        }\n\n        if (Guid.TryParse(orgIdString, out var orgId))\n        {\n            OrganizationId = orgId;\n            Organization = await organizationRepository.GetByIdAsync(orgId);\n            if (Organization != null)\n            {\n                var scimConnections = await organizationConnectionRepository.GetByOrganizationIdTypeAsync(Organization.Id,\n                    OrganizationConnectionType.Scim);\n                ScimConfiguration = scimConnections?.FirstOrDefault()?.GetConfig<ScimConfig>();\n            }\n        }\n\n        if (RequestScimProvider == ScimProviderType.Default &&\n            httpContext.Request.Headers.TryGetValue(\"User-Agent\", out var userAgent))\n        {\n            if (userAgent.ToString().StartsWith(\"Okta\"))\n            {\n                RequestScimProvider = ScimProviderType.Okta;\n            }\n        }\n\n        if (RequestScimProvider == ScimProviderType.Default &&\n            httpContext.Request.Headers.ContainsKey(\"Adscimversion\"))\n        {\n            RequestScimProvider = ScimProviderType.AzureAd;\n        }\n\n        var ipAddress = CoreHelpers.GetIpAddress(httpContext, globalSettings);\n        if (RequestScimProvider == ScimProviderType.Default &&\n            _pingIpAddresses.Contains(ipAddress))\n        {\n            RequestScimProvider = ScimProviderType.Ping;\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Controllers/InfoController.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Scim.Controllers;\n\n[AllowAnonymous]\npublic class InfoController : Controller\n{\n    [HttpGet(\"~/alive\")]\n    [HttpGet(\"~/now\")]\n    public DateTime GetAlive()\n    {\n        return DateTime.UtcNow;\n    }\n\n    [HttpGet(\"~/version\")]\n    public JsonResult GetVersion()\n    {\n        return Json(AssemblyHelpers.GetVersion());\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Scim.Groups.Interfaces;\nusing Bit.Scim.Models;\nusing Bit.Scim.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Scim.Controllers.v2;\n\n[Authorize(\"Scim\")]\n[Route(\"v2/{organizationId}/groups\")]\n[Produces(\"application/scim+json\")]\n[ExceptionHandlerFilter]\npublic class GroupsController : Controller\n{\n    private readonly IGroupRepository _groupRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IGetGroupsListQuery _getGroupsListQuery;\n    private readonly IDeleteGroupCommand _deleteGroupCommand;\n    private readonly IPatchGroupCommand _patchGroupCommand;\n    private readonly IPostGroupCommand _postGroupCommand;\n    private readonly IPutGroupCommand _putGroupCommand;\n\n    public GroupsController(\n        IGroupRepository groupRepository,\n        IOrganizationRepository organizationRepository,\n        IGetGroupsListQuery getGroupsListQuery,\n        IDeleteGroupCommand deleteGroupCommand,\n        IPatchGroupCommand patchGroupCommand,\n        IPostGroupCommand postGroupCommand,\n        IPutGroupCommand putGroupCommand\n        )\n    {\n        _groupRepository = groupRepository;\n        _organizationRepository = organizationRepository;\n        _getGroupsListQuery = getGroupsListQuery;\n        _deleteGroupCommand = deleteGroupCommand;\n        _patchGroupCommand = patchGroupCommand;\n        _postGroupCommand = postGroupCommand;\n        _putGroupCommand = putGroupCommand;\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<IActionResult> Get(Guid organizationId, Guid id)\n    {\n        var group = await _groupRepository.GetByIdAsync(id);\n        if (group == null || group.OrganizationId != organizationId)\n        {\n            throw new NotFoundException(\"Group not found.\");\n        }\n        return Ok(new ScimGroupResponseModel(group));\n    }\n\n    [HttpGet(\"\")]\n    public async Task<IActionResult> Get(\n        Guid organizationId,\n        [FromQuery] GetGroupsQueryParamModel model)\n    {\n        var groupsListQueryResult = await _getGroupsListQuery.GetGroupsListAsync(organizationId, model);\n        var scimListResponseModel = new ScimListResponseModel<ScimGroupResponseModel>\n        {\n            Resources = groupsListQueryResult.groupList.Select(g => new ScimGroupResponseModel(g)).ToList(),\n            ItemsPerPage = model.Count,\n            TotalResults = groupsListQueryResult.totalResults,\n            StartIndex = model.StartIndex,\n        };\n        return Ok(scimListResponseModel);\n    }\n\n    [HttpPost(\"\")]\n    public async Task<IActionResult> Post(Guid organizationId, [FromBody] ScimGroupRequestModel model)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n        var group = await _postGroupCommand.PostGroupAsync(organization, model);\n        var scimGroupResponseModel = new ScimGroupResponseModel(group);\n        return new CreatedResult(Url.Action(nameof(Get), new { group.OrganizationId, group.Id }), scimGroupResponseModel);\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task<IActionResult> Put(Guid organizationId, Guid id, [FromBody] ScimGroupRequestModel model)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n        var group = await _putGroupCommand.PutGroupAsync(organization, id, model);\n        var response = new ScimGroupResponseModel(group);\n\n        return Ok(response);\n    }\n\n    [HttpPatch(\"{id}\")]\n    public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)\n    {\n        var group = await _groupRepository.GetByIdAsync(id);\n        if (group == null || group.OrganizationId != organizationId)\n        {\n            throw new NotFoundException(\"Group not found.\");\n        }\n\n        await _patchGroupCommand.PatchGroupAsync(group, model);\n        return new NoContentResult();\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task<IActionResult> Delete(Guid organizationId, Guid id)\n    {\n        await _deleteGroupCommand.DeleteGroupAsync(organizationId, id, EventSystemUser.SCIM);\n        return new NoContentResult();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Controllers/v2/UsersController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Scim.Models;\nusing Bit.Scim.Users.Interfaces;\nusing Bit.Scim.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing IRevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;\nusing IRevokeOrganizationUserCommandV2 = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2.IRevokeOrganizationUserCommand;\n\nnamespace Bit.Scim.Controllers.v2;\n\n[Authorize(\"Scim\")]\n[Route(\"v2/{organizationId}/users\")]\n[Produces(\"application/scim+json\")]\n[ExceptionHandlerFilter]\npublic class UsersController : Controller\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IGetUsersListQuery _getUsersListQuery;\n    private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;\n    private readonly IPatchUserCommand _patchUserCommand;\n    private readonly IPostUserCommand _postUserCommand;\n    private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;\n    private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;\n    private readonly IFeatureService _featureService;\n    private readonly IRevokeOrganizationUserCommandV2 _revokeOrganizationUserCommandV2;\n\n    public UsersController(IOrganizationUserRepository organizationUserRepository,\n        IGetUsersListQuery getUsersListQuery,\n        IRemoveOrganizationUserCommand removeOrganizationUserCommand,\n        IPatchUserCommand patchUserCommand,\n        IPostUserCommand postUserCommand,\n        IRestoreOrganizationUserCommand restoreOrganizationUserCommand,\n        IRevokeOrganizationUserCommand revokeOrganizationUserCommand,\n        IFeatureService featureService,\n        IRevokeOrganizationUserCommandV2 revokeOrganizationUserCommandV2)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _getUsersListQuery = getUsersListQuery;\n        _removeOrganizationUserCommand = removeOrganizationUserCommand;\n        _patchUserCommand = patchUserCommand;\n        _postUserCommand = postUserCommand;\n        _restoreOrganizationUserCommand = restoreOrganizationUserCommand;\n        _revokeOrganizationUserCommand = revokeOrganizationUserCommand;\n        _featureService = featureService;\n        _revokeOrganizationUserCommandV2 = revokeOrganizationUserCommandV2;\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<IActionResult> Get(Guid organizationId, Guid id)\n    {\n        var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(id);\n        if (orgUser == null || orgUser.OrganizationId != organizationId)\n        {\n            throw new NotFoundException(\"User not found.\");\n        }\n        return Ok(new ScimUserResponseModel(orgUser));\n    }\n\n    [HttpGet(\"\")]\n    public async Task<IActionResult> Get(\n        Guid organizationId,\n        [FromQuery] GetUsersQueryParamModel model)\n    {\n        var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, model);\n        var scimListResponseModel = new ScimListResponseModel<ScimUserResponseModel>\n        {\n            Resources = usersListQueryResult.userList.Select(u => new ScimUserResponseModel(u)).ToList(),\n            ItemsPerPage = model.Count,\n            TotalResults = usersListQueryResult.totalResults,\n            StartIndex = model.StartIndex,\n        };\n        return Ok(scimListResponseModel);\n    }\n\n    [HttpPost(\"\")]\n    public async Task<IActionResult> Post(Guid organizationId, [FromBody] ScimUserRequestModel model)\n    {\n        var orgUser = await _postUserCommand.PostUserAsync(organizationId, model);\n        var scimUserResponseModel = new ScimUserResponseModel(orgUser);\n        return new CreatedResult(Url.Action(nameof(Get), new { orgUser.OrganizationId, orgUser.Id }), scimUserResponseModel);\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task<IActionResult> Put(Guid organizationId, Guid id, [FromBody] ScimUserRequestModel model)\n    {\n        var orgUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (orgUser == null || orgUser.OrganizationId != organizationId)\n        {\n            return new NotFoundObjectResult(new ScimErrorResponseModel\n            {\n                Status = 404,\n                Detail = \"User not found.\"\n            });\n        }\n\n        if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked)\n        {\n            await _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, EventSystemUser.SCIM);\n        }\n        else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)\n        {\n            if (_featureService.IsEnabled(FeatureFlagKeys.ScimRevokeV2))\n            {\n                var results = await _revokeOrganizationUserCommandV2.RevokeUsersAsync(\n                    new RevokeOrganizationUsersRequest(\n                        organizationId,\n                        [id],\n                        new SystemUser(EventSystemUser.SCIM)));\n\n                var errors = results.Select(x => x.Result.Match(\n                    y => $\"{y.Message} for user {x.Id}\",\n                    _ => null))\n                    .Where(x => !string.IsNullOrWhiteSpace(x))\n                    .ToList();\n\n                if (errors.Count != 0)\n                {\n                    return new BadRequestObjectResult(new ScimErrorResponseModel\n                    {\n                        Status = 400,\n                        Detail = string.Join(\", \", errors)\n                    });\n                }\n            }\n            else\n            {\n                await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);\n            }\n        }\n\n        // Have to get full details object for response model\n        var orgUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);\n        return new ObjectResult(new ScimUserResponseModel(orgUserDetails));\n    }\n\n    [HttpPatch(\"{id}\")]\n    public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)\n    {\n        await _patchUserCommand.PatchUserAsync(organizationId, id, model);\n        return new NoContentResult();\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task<IActionResult> Delete(Guid organizationId, Guid id)\n    {\n        await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, id, EventSystemUser.SCIM);\n        return new NoContentResult();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Dockerfile",
    "content": "###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\n# We put the value in a file to be read by later layers.\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/bitwarden_license/src/Scim\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    krb5 \\\n    icu-libs \\\n    shadow \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu\n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/bitwarden_license/src/Scim/out /app\nCOPY ./bitwarden_license/src/Scim/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\n\nHEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Scim.Groups.Interfaces;\nusing Bit.Scim.Models;\n\nnamespace Bit.Scim.Groups;\n\npublic class GetGroupsListQuery : IGetGroupsListQuery\n{\n    private readonly IGroupRepository _groupRepository;\n\n    public GetGroupsListQuery(IGroupRepository groupRepository)\n    {\n        _groupRepository = groupRepository;\n    }\n\n    public async Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(\n        Guid organizationId, GetGroupsQueryParamModel groupQueryParams)\n    {\n        string nameFilter = null;\n        string externalIdFilter = null;\n\n        int count = groupQueryParams.Count;\n        int startIndex = groupQueryParams.StartIndex;\n        string filter = groupQueryParams.Filter;\n\n        if (!string.IsNullOrWhiteSpace(filter))\n        {\n            if (filter.StartsWith(\"displayName eq \"))\n            {\n                nameFilter = filter.Substring(15).Trim('\"');\n            }\n            else if (filter.StartsWith(\"externalId eq \"))\n            {\n                externalIdFilter = filter.Substring(14).Trim('\"');\n            }\n        }\n\n        var groupList = new List<Group>();\n        var groups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);\n        var totalResults = 0;\n        if (!string.IsNullOrWhiteSpace(nameFilter))\n        {\n            var group = groups.FirstOrDefault(g => g.Name == nameFilter);\n            if (group != null)\n            {\n                groupList.Add(group);\n            }\n            totalResults = groupList.Count;\n        }\n        else if (!string.IsNullOrWhiteSpace(externalIdFilter))\n        {\n            var group = groups.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);\n            if (group != null)\n            {\n                groupList.Add(group);\n            }\n            totalResults = groupList.Count;\n        }\n        else if (string.IsNullOrWhiteSpace(filter))\n        {\n            groupList = groups.OrderBy(g => g.Name)\n                .Skip(startIndex - 1)\n                .Take(count)\n                .ToList();\n            totalResults = groups.Count;\n        }\n\n        return (groupList, totalResults);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Groups/Interfaces/IGetGroupsListQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Scim.Models;\n\nnamespace Bit.Scim.Groups.Interfaces;\n\npublic interface IGetGroupsListQuery\n{\n    Task<(IEnumerable<Group> groupList, int totalResults)> GetGroupsListAsync(Guid organizationId, GetGroupsQueryParamModel model);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Groups/Interfaces/IPatchGroupCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Scim.Models;\n\nnamespace Bit.Scim.Groups.Interfaces;\n\npublic interface IPatchGroupCommand\n{\n    Task PatchGroupAsync(Group group, ScimPatchModel model);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Groups/Interfaces/IPostGroupCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Scim.Models;\n\nnamespace Bit.Scim.Groups.Interfaces;\n\npublic interface IPostGroupCommand\n{\n    Task<Group> PostGroupAsync(Organization organization, ScimGroupRequestModel model);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Groups/Interfaces/IPutGroupCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Scim.Models;\n\nnamespace Bit.Scim.Groups.Interfaces;\n\npublic interface IPutGroupCommand\n{\n    Task<Group> PutGroupAsync(Organization organization, Guid id, ScimGroupRequestModel model);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Scim.Groups.Interfaces;\nusing Bit.Scim.Models;\nusing Bit.Scim.Utilities;\n\nnamespace Bit.Scim.Groups;\n\npublic class PatchGroupCommand : IPatchGroupCommand\n{\n    private readonly IGroupRepository _groupRepository;\n    private readonly IGroupService _groupService;\n    private readonly IUpdateGroupCommand _updateGroupCommand;\n    private readonly ILogger<PatchGroupCommand> _logger;\n    private readonly IOrganizationRepository _organizationRepository;\n\n    public PatchGroupCommand(\n        IGroupRepository groupRepository,\n        IGroupService groupService,\n        IUpdateGroupCommand updateGroupCommand,\n        ILogger<PatchGroupCommand> logger,\n        IOrganizationRepository organizationRepository)\n    {\n        _groupRepository = groupRepository;\n        _groupService = groupService;\n        _updateGroupCommand = updateGroupCommand;\n        _logger = logger;\n        _organizationRepository = organizationRepository;\n    }\n\n    public async Task PatchGroupAsync(Group group, ScimPatchModel model)\n    {\n        foreach (var operation in model.Operations)\n        {\n            await HandleOperationAsync(group, operation);\n        }\n    }\n\n    private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationModel operation)\n    {\n        switch (operation.Op?.ToLowerInvariant())\n        {\n            // Replace a list of members\n            case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.Members:\n                {\n                    var ids = GetOperationValueIds(operation.Value);\n                    await _groupRepository.UpdateUsersAsync(group.Id, ids);\n                    break;\n                }\n\n            // Replace group name from path\n            case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.DisplayName:\n                {\n                    group.Name = operation.Value.GetString();\n                    var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);\n                    if (organization == null)\n                    {\n                        throw new NotFoundException();\n                    }\n                    await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);\n                    break;\n                }\n\n            // Replace group name from value object\n            case PatchOps.Replace when\n                string.IsNullOrWhiteSpace(operation.Path) &&\n                operation.Value.TryGetProperty(\"displayName\", out var displayNameProperty):\n                {\n                    group.Name = displayNameProperty.GetString();\n                    var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);\n                    if (organization == null)\n                    {\n                        throw new NotFoundException();\n                    }\n                    await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);\n                    break;\n                }\n\n            // Add a single member\n            case PatchOps.Add when\n                !string.IsNullOrWhiteSpace(operation.Path) &&\n                operation.Path.StartsWith(\"members[value eq \", StringComparison.OrdinalIgnoreCase) &&\n                TryGetOperationPathId(operation.Path, out var addId):\n                {\n                    await AddMembersAsync(group, [addId]);\n                    break;\n                }\n\n            // Add a list of members\n            case PatchOps.Add when\n                operation.Path?.ToLowerInvariant() == PatchPaths.Members:\n                {\n                    await AddMembersAsync(group, GetOperationValueIds(operation.Value));\n                    break;\n                }\n\n            // Remove a single member\n            case PatchOps.Remove when\n                !string.IsNullOrWhiteSpace(operation.Path) &&\n                operation.Path.StartsWith(\"members[value eq \", StringComparison.OrdinalIgnoreCase) &&\n                TryGetOperationPathId(operation.Path, out var removeId):\n                {\n                    await _groupService.DeleteUserAsync(group, removeId, EventSystemUser.SCIM);\n                    break;\n                }\n\n            // Remove a list of members\n            case PatchOps.Remove when\n                operation.Path?.ToLowerInvariant() == PatchPaths.Members:\n                {\n                    var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();\n                    foreach (var v in GetOperationValueIds(operation.Value))\n                    {\n                        orgUserIds.Remove(v);\n                    }\n                    await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);\n                    break;\n                }\n\n            default:\n                {\n                    _logger.LogWarning(\"Group patch operation not handled: {OperationOp}:{OperationPath}\", operation.Op, operation.Path);\n                    break;\n                }\n        }\n    }\n\n    private async Task AddMembersAsync(Group group, HashSet<Guid> usersToAdd)\n    {\n        // Azure Entra ID is known to send redundant \"add\" requests for each existing member every time any member\n        // is removed. To avoid excessive load on the database, we check against the high availability replica and\n        // return early if they already exist.\n        var groupMembers = await _groupRepository.GetManyUserIdsByIdAsync(group.Id, useReadOnlyReplica: true);\n        if (usersToAdd.IsSubsetOf(groupMembers))\n        {\n            _logger.LogDebug(\"Ignoring duplicate SCIM request to add members {Members} to group {Group}\", usersToAdd, group.Id);\n            return;\n        }\n\n        await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd);\n    }\n\n    private static HashSet<Guid> GetOperationValueIds(JsonElement objArray)\n    {\n        var ids = new HashSet<Guid>();\n        foreach (var obj in objArray.EnumerateArray())\n        {\n            if (obj.TryGetProperty(\"value\", out var valueProperty))\n            {\n                if (valueProperty.TryGetGuid(out var guid))\n                {\n                    ids.Add(guid);\n                }\n            }\n        }\n        return ids;\n    }\n\n    private static bool TryGetOperationPathId(string path, out Guid pathId)\n    {\n        // Parse Guid from string like: members[value eq \"{GUID}\"}]\n        return Guid.TryParse(path.Substring(18).Replace(\"\\\"]\", string.Empty), out pathId);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Groups/PostGroupCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Scim.Groups.Interfaces;\nusing Bit.Scim.Models;\n\nnamespace Bit.Scim.Groups;\n\npublic class PostGroupCommand : IPostGroupCommand\n{\n    private readonly IGroupRepository _groupRepository;\n    private readonly ICreateGroupCommand _createGroupCommand;\n\n    public PostGroupCommand(\n        IGroupRepository groupRepository,\n        ICreateGroupCommand createGroupCommand)\n    {\n        _groupRepository = groupRepository;\n        _createGroupCommand = createGroupCommand;\n    }\n\n    public async Task<Group> PostGroupAsync(Organization organization, ScimGroupRequestModel model)\n    {\n        if (string.IsNullOrWhiteSpace(model.DisplayName))\n        {\n            throw new BadRequestException();\n        }\n\n        var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);\n        if (!string.IsNullOrWhiteSpace(model.ExternalId) && groups.Any(g => g.ExternalId == model.ExternalId))\n        {\n            throw new ConflictException();\n        }\n\n        var group = model.ToGroup(organization.Id);\n        await _createGroupCommand.CreateGroupAsync(group, organization, EventSystemUser.SCIM, collections: null);\n        await UpdateGroupMembersAsync(group, model);\n\n        return group;\n    }\n\n    private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)\n    {\n        if (model.Members == null)\n        {\n            return;\n        }\n\n        var memberIds = new List<Guid>();\n        foreach (var id in model.Members.Select(i => i.Value))\n        {\n            if (Guid.TryParse(id, out var guidId))\n            {\n                memberIds.Add(guidId);\n            }\n        }\n\n        if (!memberIds.Any())\n        {\n            return;\n        }\n\n        await _groupRepository.UpdateUsersAsync(group.Id, memberIds);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Groups/PutGroupCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Scim.Groups.Interfaces;\nusing Bit.Scim.Models;\n\nnamespace Bit.Scim.Groups;\n\npublic class PutGroupCommand : IPutGroupCommand\n{\n    private readonly IGroupRepository _groupRepository;\n    private readonly IUpdateGroupCommand _updateGroupCommand;\n\n    public PutGroupCommand(\n        IGroupRepository groupRepository,\n        IUpdateGroupCommand updateGroupCommand)\n    {\n        _groupRepository = groupRepository;\n        _updateGroupCommand = updateGroupCommand;\n    }\n\n    public async Task<Group> PutGroupAsync(Organization organization, Guid id, ScimGroupRequestModel model)\n    {\n        var group = await _groupRepository.GetByIdAsync(id);\n        if (group == null || group.OrganizationId != organization.Id)\n        {\n            throw new NotFoundException(\"Group not found.\");\n        }\n\n        group.Name = model.DisplayName;\n        await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);\n        await UpdateGroupMembersAsync(group, model);\n\n        return group;\n    }\n\n    private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)\n    {\n        if (model.Members == null)\n        {\n            return;\n        }\n\n        var memberIds = new List<Guid>();\n        foreach (var id in model.Members.Select(i => i.Value))\n        {\n            if (Guid.TryParse(id, out var guidId))\n            {\n                memberIds.Add(guidId);\n            }\n        }\n\n        await _groupRepository.UpdateUsersAsync(group.Id, memberIds);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Scim.Utilities;\n\nnamespace Bit.Scim.Models;\n\npublic abstract class BaseScimGroupModel : BaseScimModel\n{\n    public BaseScimGroupModel(bool initSchema = false)\n    {\n        if (initSchema)\n        {\n            Schemas = new List<string> { ScimConstants.Scim2SchemaGroup };\n        }\n    }\n\n    public string DisplayName { get; set; }\n    public string ExternalId { get; set; }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/BaseScimModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Scim.Models;\n\npublic abstract class BaseScimModel\n{\n    public BaseScimModel()\n    { }\n\n    public BaseScimModel(string schema)\n    {\n        Schemas = new List<string> { schema };\n    }\n\n    public List<string> Schemas { get; set; }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/BaseScimUserModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Scim.Utilities;\n\nnamespace Bit.Scim.Models;\n\npublic abstract class BaseScimUserModel : BaseScimModel\n{\n    public BaseScimUserModel(bool initSchema = false)\n    {\n        if (initSchema)\n        {\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser };\n        }\n    }\n\n    public string UserName { get; set; }\n    public NameModel Name { get; set; }\n    public List<EmailModel> Emails { get; set; }\n    public string PrimaryEmail => Emails?.FirstOrDefault(e => e.Primary)?.Value;\n    public string WorkEmail => Emails?.FirstOrDefault(e => e.Type == \"work\")?.Value;\n    public string DisplayName { get; set; }\n    public bool Active { get; set; }\n    public List<string> Groups { get; set; }\n    public string ExternalId { get; set; }\n\n    public class NameModel\n    {\n        public NameModel() { }\n\n        public NameModel(string name)\n        {\n            Formatted = name;\n        }\n\n        public string Formatted { get; set; }\n        public string GivenName { get; set; }\n        public string MiddleName { get; set; }\n        public string FamilyName { get; set; }\n    }\n\n    public class EmailModel\n    {\n        public EmailModel() { }\n\n        public EmailModel(string email)\n        {\n            Primary = true;\n            Value = email;\n            Type = \"work\";\n        }\n\n        public bool Primary { get; set; }\n        public string Value { get; set; }\n        public string Type { get; set; }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/GetGroupsQueryParamModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Scim.Models;\n\npublic class GetGroupsQueryParamModel\n{\n    public string Filter { get; init; } = string.Empty;\n\n    [Range(1, int.MaxValue)]\n    public int Count { get; init; } = 50;\n\n    [Range(1, int.MaxValue)]\n    public int StartIndex { get; init; } = 1;\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/GetUsersQueryParamModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Scim.Models;\n\npublic class GetUsersQueryParamModel\n{\n    public string Filter { get; init; } = string.Empty;\n\n    [Range(1, int.MaxValue)]\n    public int Count { get; init; } = 50;\n\n    [Range(1, int.MaxValue)]\n    public int StartIndex { get; init; } = 1;\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Scim.Utilities;\n\nnamespace Bit.Scim.Models;\n\npublic class ScimErrorResponseModel : BaseScimModel\n{\n    public ScimErrorResponseModel()\n        : base(ScimConstants.Scim2SchemaError)\n    { }\n\n    public string Detail { get; set; }\n    public int Status { get; set; }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Scim.Models;\n\npublic class ScimGroupRequestModel : BaseScimGroupModel\n{\n    public ScimGroupRequestModel()\n        : base(false)\n    { }\n\n    public Group ToGroup(Guid organizationId)\n    {\n        var externalId = string.IsNullOrWhiteSpace(ExternalId) ? CoreHelpers.RandomString(15) : ExternalId;\n        return new Group\n        {\n            Name = DisplayName,\n            ExternalId = externalId,\n            OrganizationId = organizationId\n        };\n    }\n\n    public List<GroupMembersModel> Members { get; set; }\n\n    public class GroupMembersModel\n    {\n        public string Value { get; set; }\n        public string Display { get; set; }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Scim.Models;\n\npublic class ScimGroupResponseModel : BaseScimGroupModel\n{\n    public ScimGroupResponseModel()\n        : base(true)\n    {\n        Meta = new ScimMetaModel(\"Group\");\n    }\n\n    public ScimGroupResponseModel(Group group)\n        : this()\n    {\n        Id = group.Id;\n        DisplayName = group.Name;\n        ExternalId = group.ExternalId;\n        Meta.Created = group.CreationDate;\n        Meta.LastModified = group.RevisionDate;\n    }\n\n    public Guid Id { get; set; }\n    public ScimMetaModel Meta { get; private set; }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/ScimListResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Scim.Utilities;\n\nnamespace Bit.Scim.Models;\n\npublic class ScimListResponseModel<T> : BaseScimModel\n{\n    public ScimListResponseModel()\n        : base(ScimConstants.Scim2SchemaListResponse)\n    { }\n\n    public int TotalResults { get; set; }\n    public int StartIndex { get; set; }\n    public int ItemsPerPage { get; set; }\n    public List<T> Resources { get; set; }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/ScimMetaModel.cs",
    "content": "﻿namespace Bit.Scim.Models;\n\npublic class ScimMetaModel\n{\n    public ScimMetaModel(string resourceType)\n    {\n        ResourceType = resourceType;\n    }\n\n    public string ResourceType { get; set; }\n    public DateTime? Created { get; set; }\n    public DateTime? LastModified { get; set; }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/ScimPatchModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\n\nnamespace Bit.Scim.Models;\n\npublic class ScimPatchModel : BaseScimModel\n{\n    public ScimPatchModel()\n        : base() { }\n\n    public List<OperationModel> Operations { get; set; }\n\n    public class OperationModel\n    {\n        public string Op { get; set; }\n        public string Path { get; set; }\n        public JsonElement Value { get; set; }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Scim.Models;\n\npublic class ScimUserRequestModel : BaseScimUserModel\n{\n    public ScimUserRequestModel()\n        : base(false)\n    {\n    }\n\n    public OrganizationUserInvite ToOrganizationUserInvite(ScimProviderType scimProvider)\n    {\n        return new OrganizationUserInvite\n        {\n            Emails = new[] { EmailForInvite(scimProvider) },\n\n            // Permissions cannot be set via SCIM so we use default values\n            Type = OrganizationUserType.User,\n            Collections = new List<CollectionAccessSelection>(),\n            Groups = new List<Guid>()\n        };\n    }\n\n    public InviteOrganizationUsersRequest ToRequest(\n        ScimProviderType scimProvider,\n        InviteOrganization inviteOrganization,\n        DateTimeOffset performedAt)\n    {\n        var email = EmailForInvite(scimProvider);\n\n        if (string.IsNullOrWhiteSpace(email) || !Active)\n        {\n            throw new BadRequestException();\n        }\n\n        return new InviteOrganizationUsersRequest(\n            invites:\n            [\n                new OrganizationUserInviteCommandModel(\n                        email: email,\n                        externalId: ExternalIdForInvite()\n                    )\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty, // SCIM does not have a user id\n            performedAt: performedAt);\n    }\n\n    private string EmailForInvite(ScimProviderType scimProvider)\n    {\n        var email = PrimaryEmail?.ToLowerInvariant();\n\n        if (!string.IsNullOrWhiteSpace(email))\n        {\n            return email;\n        }\n\n        switch (scimProvider)\n        {\n            case ScimProviderType.AzureAd:\n                return UserName?.ToLowerInvariant();\n            default:\n                email = WorkEmail?.ToLowerInvariant();\n                if (string.IsNullOrWhiteSpace(email))\n                {\n                    email = Emails?.FirstOrDefault()?.Value?.ToLowerInvariant();\n                }\n\n                return email;\n        }\n    }\n\n    public string ExternalIdForInvite()\n    {\n        if (!string.IsNullOrWhiteSpace(ExternalId))\n        {\n            return ExternalId;\n        }\n\n        if (!string.IsNullOrWhiteSpace(UserName))\n        {\n            return UserName;\n        }\n\n        return CoreHelpers.RandomString(15);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Models/ScimUserResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Scim.Models;\n\npublic class ScimUserResponseModel : BaseScimUserModel\n{\n    public ScimUserResponseModel()\n        : base(true)\n    {\n        Meta = new ScimMetaModel(\"User\");\n        Groups = new List<string>();\n    }\n\n    public ScimUserResponseModel(OrganizationUserUserDetails orgUser)\n        : this()\n    {\n        Id = orgUser.Id;\n        ExternalId = orgUser.ExternalId;\n        UserName = orgUser.Email;\n        DisplayName = orgUser.Name;\n        Emails = new List<EmailModel> { new EmailModel(orgUser.Email) };\n        Name = new NameModel(orgUser.Name);\n        Active = orgUser.Status != Core.Enums.OrganizationUserStatusType.Revoked;\n    }\n\n    public Guid Id { get; set; }\n    public ScimMetaModel Meta { get; private set; }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Scim;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        Host\n            .CreateDefaultBuilder(args)\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging()\n            .Build()\n            .Run();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:44558/\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchUrl\": \"http://localhost:44558\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Scim\": {\n      \"commandName\": \"Project\",\n      \"launchUrl\": \"http://localhost:44558\",\n      \"applicationUrl\": \"http://localhost:44559\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Scim.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-Scim</UserSecretsId>\n    <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Scim' \" />\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\src\\SharedWeb\\SharedWeb.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "bitwarden_license/src/Scim/ScimSettings.cs",
    "content": "﻿namespace Bit.Scim;\n\npublic class ScimSettings\n{\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Startup.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Context;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.SecretsManager.Repositories.Noop;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Scim.Context;\nusing Bit.Scim.Utilities;\nusing Bit.SharedWeb.Utilities;\nusing Duende.IdentityModel;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Stripe;\n\nnamespace Bit.Scim;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(\"en-US\");\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; }\n    public IWebHostEnvironment Environment { get; set; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n        services.Configure<ScimSettings>(Configuration.GetSection(\"ScimSettings\"));\n\n        // Data Protection\n        services.AddCustomDataProtectionServices(Environment, globalSettings);\n\n        // Stripe Billing\n        StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;\n        StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;\n\n        // Repositories\n        services.AddDatabaseRepositories(globalSettings);\n        services.AddTestPlayIdTracking(globalSettings);\n\n        // Context\n        services.AddScoped<ICurrentContext, CurrentContext>();\n        services.AddScoped<IScimContext, ScimContext>();\n\n        // Authentication\n        services.AddAuthentication(ApiKeyAuthenticationOptions.DefaultScheme)\n            .AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(\n                ApiKeyAuthenticationOptions.DefaultScheme, null);\n\n        services.AddAuthorization(config =>\n        {\n            config.AddPolicy(\"Scim\", policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.Scope, \"api.scim\");\n            });\n        });\n\n        // Identity\n        services.AddCustomIdentityServices(globalSettings);\n\n        // Services\n        services.AddBaseServices(globalSettings);\n        services.AddDefaultServices(globalSettings);\n        services.AddDistributedCache(globalSettings);\n        services.AddBillingOperations();\n\n        services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();\n\n        // TODO: Remove when OrganizationUser methods are moved out of OrganizationService, this noop dependency should\n        // TODO: no longer be required - see PM-1880\n        services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();\n\n        // Mvc\n        services.AddMvc(config =>\n        {\n            config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());\n        });\n        services.Configure<RouteOptions>(options => options.LowercaseUrls = true);\n\n        services.AddScimGroupCommands();\n        services.AddScimGroupQueries();\n        services.AddScimUserQueries();\n        services.AddScimUserCommands();\n    }\n\n    public void Configure(\n        IApplicationBuilder app,\n        IWebHostEnvironment env,\n        GlobalSettings globalSettings)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n\n        // Forwarding Headers\n        if (globalSettings.SelfHosted)\n        {\n            app.UseForwardedHeaders(globalSettings);\n        }\n\n        if (env.IsDevelopment())\n        {\n            app.UseDeveloperExceptionPage();\n        }\n\n        // Default Middleware\n        app.UseDefaultMiddleware(env, globalSettings);\n\n        // Add routing\n        app.UseRouting();\n\n        // Add Scim context\n        app.UseMiddleware<ScimContextMiddleware>();\n\n        // Add authentication and authorization to the request pipeline.\n        app.UseAuthentication();\n        app.UseAuthorization();\n\n        // Add current context\n        app.UseMiddleware<CurrentContextMiddleware>();\n\n        // Add MVC to the request pipeline.\n        app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Users/GetUsersListQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Scim.Models;\nusing Bit.Scim.Users.Interfaces;\n\nnamespace Bit.Scim.Users;\n\npublic class GetUsersListQuery : IGetUsersListQuery\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public GetUsersListQuery(IOrganizationUserRepository organizationUserRepository)\n    {\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    public async Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams)\n    {\n        string emailFilter = null;\n        string usernameFilter = null;\n        string externalIdFilter = null;\n\n        int count = userQueryParams.Count;\n        int startIndex = userQueryParams.StartIndex;\n        string filter = userQueryParams.Filter;\n\n        if (!string.IsNullOrWhiteSpace(filter))\n        {\n            var filterLower = filter.ToLowerInvariant();\n            if (filterLower.StartsWith(\"username eq \"))\n            {\n                usernameFilter = filterLower.Substring(12).Trim('\"');\n                if (usernameFilter.Contains(\"@\"))\n                {\n                    emailFilter = usernameFilter;\n                }\n            }\n            else if (filterLower.StartsWith(\"externalid eq \"))\n            {\n                externalIdFilter = filter.Substring(14).Trim('\"');\n            }\n        }\n\n        var userList = new List<OrganizationUserUserDetails>();\n        var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);\n        var totalResults = 0;\n        if (!string.IsNullOrWhiteSpace(emailFilter))\n        {\n            var orgUser = orgUsers.FirstOrDefault(ou => ou.Email.ToLowerInvariant() == emailFilter);\n            if (orgUser != null)\n            {\n                userList.Add(orgUser);\n            }\n            totalResults = userList.Count;\n        }\n        else if (!string.IsNullOrWhiteSpace(externalIdFilter))\n        {\n            var orgUser = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalIdFilter);\n            if (orgUser != null)\n            {\n                userList.Add(orgUser);\n            }\n            totalResults = userList.Count;\n        }\n        else if (string.IsNullOrWhiteSpace(filter))\n        {\n            userList = orgUsers.OrderBy(ou => ou.Email)\n                .Skip(startIndex - 1)\n                .Take(count)\n                .ToList();\n            totalResults = orgUsers.Count;\n        }\n\n        return (userList, totalResults);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Users/Interfaces/IGetUsersListQuery.cs",
    "content": "﻿using Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Scim.Models;\n\nnamespace Bit.Scim.Users.Interfaces;\n\npublic interface IGetUsersListQuery\n{\n    Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Users/Interfaces/IPatchUserCommand.cs",
    "content": "﻿using Bit.Scim.Models;\n\nnamespace Bit.Scim.Users.Interfaces;\n\npublic interface IPatchUserCommand\n{\n    Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Users/Interfaces/IPostUserCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Scim.Models;\n\nnamespace Bit.Scim.Users.Interfaces;\n\npublic interface IPostUserCommand\n{\n    Task<OrganizationUserUserDetails> PostUserAsync(Guid organizationId, ScimUserRequestModel model);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Users/PatchUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Scim.Models;\nusing Bit.Scim.Users.Interfaces;\nusing Bit.Scim.Utilities;\n\nnamespace Bit.Scim.Users;\n\npublic class PatchUserCommand : IPatchUserCommand\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;\n    private readonly ILogger<PatchUserCommand> _logger;\n    private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;\n\n    public PatchUserCommand(IOrganizationUserRepository organizationUserRepository,\n        IRestoreOrganizationUserCommand restoreOrganizationUserCommand,\n        ILogger<PatchUserCommand> logger,\n        IRevokeOrganizationUserCommand revokeOrganizationUserCommand)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _restoreOrganizationUserCommand = restoreOrganizationUserCommand;\n        _logger = logger;\n        _revokeOrganizationUserCommand = revokeOrganizationUserCommand;\n    }\n\n    public async Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model)\n    {\n        var orgUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (orgUser == null || orgUser.OrganizationId != organizationId)\n        {\n            throw new NotFoundException(\"User not found.\");\n        }\n\n        var operationHandled = false;\n        foreach (var operation in model.Operations)\n        {\n            // Replace operations\n            if (operation.Op?.ToLowerInvariant() == PatchOps.Replace)\n            {\n                // Active from path\n                if (operation.Path?.ToLowerInvariant() == \"active\")\n                {\n                    var active = operation.Value.ToString()?.ToLowerInvariant();\n                    var handled = await HandleActiveOperationAsync(orgUser, active == \"true\");\n                    if (!operationHandled)\n                    {\n                        operationHandled = handled;\n                    }\n                    // Re-fetch to pick up status changes persisted by restore/revoke\n                    if (handled)\n                    {\n                        orgUser = await _organizationUserRepository.GetByIdAsync(orgUser.Id)\n                            ?? throw new NotFoundException(\"User not found.\");\n                    }\n                }\n                // ExternalId from path\n                else if (operation.Path?.ToLowerInvariant() == PatchPaths.ExternalId)\n                {\n                    var newExternalId = operation.Value.GetString();\n                    await HandleExternalIdOperationAsync(orgUser, newExternalId);\n                    operationHandled = true;\n                }\n                // Value object with no path — check for each supported property independently\n                else if (string.IsNullOrWhiteSpace(operation.Path))\n                {\n                    if (operation.Value.TryGetProperty(\"active\", out var activeProperty))\n                    {\n                        var handled = await HandleActiveOperationAsync(orgUser, activeProperty.GetBoolean());\n                        if (!operationHandled)\n                        {\n                            operationHandled = handled;\n                        }\n                        // Re-fetch to pick up status changes persisted by restore/revoke\n                        if (handled)\n                        {\n                            orgUser = await _organizationUserRepository.GetByIdAsync(orgUser.Id)\n                                ?? throw new NotFoundException(\"User not found.\");\n                        }\n                    }\n                    if (operation.Value.TryGetProperty(\"externalId\", out var externalIdProperty))\n                    {\n                        var newExternalId = externalIdProperty.GetString();\n                        await HandleExternalIdOperationAsync(orgUser, newExternalId);\n                        operationHandled = true;\n                    }\n                }\n            }\n        }\n\n        if (!operationHandled)\n        {\n            _logger.LogWarning(\"User patch operation not handled: {operation} : \",\n                string.Join(\", \", model.Operations.Select(o => $\"{o.Op}:{o.Path}\")));\n        }\n    }\n\n    private async Task<bool> HandleActiveOperationAsync(Core.Entities.OrganizationUser orgUser, bool active)\n    {\n        if (active && orgUser.Status == OrganizationUserStatusType.Revoked)\n        {\n            await _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, EventSystemUser.SCIM);\n            return true;\n        }\n        else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)\n        {\n            await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);\n            return true;\n        }\n        return false;\n    }\n\n    private async Task HandleExternalIdOperationAsync(Core.Entities.OrganizationUser orgUser, string? newExternalId)\n    {\n        // Validate max length (300 chars per OrganizationUser.cs line 59)\n        if (!string.IsNullOrWhiteSpace(newExternalId) && newExternalId.Length > 300)\n        {\n            throw new BadRequestException(\"ExternalId cannot exceed 300 characters.\");\n        }\n\n        // Check for duplicate externalId (same validation as PostUserCommand.cs)\n        if (!string.IsNullOrWhiteSpace(newExternalId))\n        {\n            var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgUser.OrganizationId);\n            if (existingUsers.Any(u => u.Id != orgUser.Id &&\n                !string.IsNullOrWhiteSpace(u.ExternalId) &&\n                u.ExternalId.Equals(newExternalId, StringComparison.OrdinalIgnoreCase)))\n            {\n                throw new ConflictException(\"ExternalId already exists for another user.\");\n            }\n        }\n\n        orgUser.ExternalId = newExternalId;\n        await _organizationUserRepository.ReplaceAsync(orgUser);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Users/PostUserCommand.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.Utilities.Commands;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Scim.Context;\nusing Bit.Scim.Models;\nusing Bit.Scim.Users.Interfaces;\nusing static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors.ErrorMapper;\n\nnamespace Bit.Scim.Users;\n\npublic class PostUserCommand(\n    IOrganizationRepository organizationRepository,\n    IOrganizationUserRepository organizationUserRepository,\n    IOrganizationService organizationService,\n    IStripePaymentService paymentService,\n    IScimContext scimContext,\n    IFeatureService featureService,\n    IInviteOrganizationUsersCommand inviteOrganizationUsersCommand,\n    TimeProvider timeProvider,\n    IPricingClient pricingClient)\n    : IPostUserCommand\n{\n    public async Task<OrganizationUserUserDetails?> PostUserAsync(Guid organizationId, ScimUserRequestModel model)\n    {\n        if (featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization) is false)\n        {\n            return await InviteScimOrganizationUserAsync(model, organizationId, scimContext.RequestScimProvider);\n        }\n\n        return await InviteScimOrganizationUserAsync_vNext(model, organizationId, scimContext.RequestScimProvider);\n    }\n\n    private async Task<OrganizationUserUserDetails?> InviteScimOrganizationUserAsync_vNext(\n        ScimUserRequestModel model,\n        Guid organizationId,\n        ScimProviderType scimProvider)\n    {\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization is null)\n        {\n            throw new NotFoundException();\n        }\n\n        var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        var request = model.ToRequest(\n            scimProvider: scimProvider,\n            inviteOrganization: new InviteOrganization(organization, plan),\n            performedAt: timeProvider.GetUtcNow());\n\n        var orgUsers = await organizationUserRepository\n            .GetManyDetailsByOrganizationAsync(request.InviteOrganization.OrganizationId);\n\n        if (orgUsers.Any(existingUser =>\n                request.Invites.First().Email.Equals(existingUser.Email, StringComparison.OrdinalIgnoreCase) ||\n                request.Invites.First().ExternalId.Equals(existingUser.ExternalId, StringComparison.OrdinalIgnoreCase)))\n        {\n            throw new ConflictException(\"User already exists.\");\n        }\n\n        var result = await inviteOrganizationUsersCommand.InviteScimOrganizationUserAsync(request);\n\n        var invitedOrganizationUserId = result switch\n        {\n            Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,\n            Failure<ScimInviteOrganizationUsersResponse> { Error.Message: NoUsersToInviteError.Code } => (Guid?)null,\n            Failure<ScimInviteOrganizationUsersResponse> failure => throw MapToBitException(failure.Error),\n            _ => throw new InvalidOperationException()\n        };\n\n        var organizationUser = invitedOrganizationUserId.HasValue\n            ? await organizationUserRepository.GetDetailsByIdAsync(invitedOrganizationUserId.Value)\n            : null;\n\n        return organizationUser;\n    }\n\n    private async Task<OrganizationUserUserDetails?> InviteScimOrganizationUserAsync(\n        ScimUserRequestModel model,\n        Guid organizationId,\n        ScimProviderType scimProvider)\n    {\n        var invite = model.ToOrganizationUserInvite(scimProvider);\n\n        var email = invite.Emails.Single();\n        var externalId = model.ExternalIdForInvite();\n\n        if (string.IsNullOrWhiteSpace(email) || !model.Active)\n        {\n            throw new BadRequestException();\n        }\n\n        var orgUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);\n        var orgUserByEmail = orgUsers.FirstOrDefault(ou => ou.Email?.ToLowerInvariant() == email);\n        if (orgUserByEmail != null)\n        {\n            throw new ConflictException();\n        }\n\n        var orgUserByExternalId = orgUsers.FirstOrDefault(ou => ou.ExternalId == externalId);\n        if (orgUserByExternalId != null)\n        {\n            throw new ConflictException();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var hasStandaloneSecretsManager = await paymentService.HasSecretsManagerStandalone(organization);\n        invite.AccessSecretsManager = hasStandaloneSecretsManager;\n\n        var invitedOrgUser = await organizationService.InviteUserAsync(organizationId, invitingUserId: null,\n            EventSystemUser.SCIM,\n            invite,\n            externalId);\n        var orgUser = await organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);\n\n        return orgUser;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs",
    "content": "﻿using System.Security.Claims;\nusing System.Text.Encodings.Web;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Scim.Context;\nusing Duende.IdentityModel;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Scim.Utilities;\n\npublic class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;\n    private readonly IScimContext _scimContext;\n\n    public ApiKeyAuthenticationHandler(\n        IOptionsMonitor<ApiKeyAuthenticationOptions> options,\n        ILoggerFactory logger,\n        UrlEncoder encoder,\n        ISystemClock clock,\n        IOrganizationRepository organizationRepository,\n        IOrganizationApiKeyRepository organizationApiKeyRepository,\n        IScimContext scimContext) :\n        base(options, logger, encoder, clock)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationApiKeyRepository = organizationApiKeyRepository;\n        _scimContext = scimContext;\n    }\n\n    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()\n    {\n        var endpoint = Context.GetEndpoint();\n        if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)\n        {\n            return AuthenticateResult.NoResult();\n        }\n\n        if (!_scimContext.OrganizationId.HasValue || _scimContext.Organization == null)\n        {\n            Logger.LogWarning(\"No organization.\");\n            return AuthenticateResult.Fail(\"Invalid parameters\");\n        }\n\n        if (!Request.Headers.TryGetValue(\"Authorization\", out var authHeader) || authHeader.Count != 1)\n        {\n            Logger.LogWarning(\"An API request was received without the Authorization header\");\n            return AuthenticateResult.Fail(\"Invalid parameters\");\n        }\n        var apiKey = authHeader.ToString();\n        if (apiKey.StartsWith(\"Bearer \"))\n        {\n            apiKey = apiKey.Substring(7);\n        }\n\n        if (!_scimContext.Organization.Enabled || !_scimContext.Organization.UseScim ||\n            _scimContext.ScimConfiguration == null || !_scimContext.ScimConfiguration.Enabled)\n        {\n            Logger.LogInformation(\"Org {organizationId} not able to use Scim.\", _scimContext.OrganizationId);\n            return AuthenticateResult.Fail(\"Invalid parameters\");\n        }\n\n        var orgApiKey = (await _organizationApiKeyRepository\n            .GetManyByOrganizationIdTypeAsync(_scimContext.Organization.Id, OrganizationApiKeyType.Scim))\n            .FirstOrDefault();\n        if (orgApiKey?.ApiKey != apiKey)\n        {\n            Logger.LogWarning(\"An API request was received with an invalid API key: {apiKey}\", apiKey);\n            return AuthenticateResult.Fail(\"Invalid parameters\");\n        }\n\n        Logger.LogInformation(\"Org {organizationId} authenticated\", _scimContext.OrganizationId);\n\n        var claims = new[]\n        {\n            new Claim(JwtClaimTypes.ClientId, $\"organization.{_scimContext.OrganizationId.Value}\"),\n            new Claim(\"client_sub\", _scimContext.OrganizationId.Value.ToString()),\n            new Claim(JwtClaimTypes.Scope, \"api.scim\"),\n        };\n        var identity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));\n        var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity),\n            ApiKeyAuthenticationOptions.DefaultScheme);\n\n        return AuthenticateResult.Success(ticket);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationOptions.cs",
    "content": "﻿using Microsoft.AspNetCore.Authentication;\n\nnamespace Bit.Scim.Utilities;\n\npublic class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions\n{\n    public const string DefaultScheme = \"ScimApiKey\";\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Utilities/ExceptionHandlerFilterAttribute.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Scim.Models;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\n\nnamespace Bit.Scim.Utilities;\n\npublic class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute\n{\n    public override void OnException(ExceptionContext context)\n    {\n        var exception = context.Exception;\n        if (exception == null)\n        {\n            // Should never happen.\n            return;\n        }\n\n        int statusCode = StatusCodes.Status500InternalServerError;\n        var scimErrorResponseModel = new ScimErrorResponseModel\n        {\n            Detail = exception.Message\n        };\n\n        if (exception is NotFoundException)\n        {\n            statusCode = StatusCodes.Status404NotFound;\n        }\n        else if (exception is BadRequestException)\n        {\n            statusCode = StatusCodes.Status400BadRequest;\n        }\n        else if (exception is ConflictException)\n        {\n            statusCode = StatusCodes.Status409Conflict;\n        }\n\n        scimErrorResponseModel.Status = statusCode;\n\n        context.HttpContext.Response.StatusCode = statusCode;\n        context.Result = new ObjectResult(scimErrorResponseModel);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Utilities/ScimConstants.cs",
    "content": "﻿namespace Bit.Scim.Utilities;\n\npublic static class ScimConstants\n{\n    public const string Scim2SchemaListResponse = \"urn:ietf:params:scim:api:messages:2.0:ListResponse\";\n    public const string Scim2SchemaError = \"urn:ietf:params:scim:api:messages:2.0:Error\";\n    public const string Scim2SchemaUser = \"urn:ietf:params:scim:schemas:core:2.0:User\";\n    public const string Scim2SchemaGroup = \"urn:ietf:params:scim:schemas:core:2.0:Group\";\n}\n\npublic static class PatchOps\n{\n    public const string Replace = \"replace\";\n    public const string Add = \"add\";\n    public const string Remove = \"remove\";\n}\n\npublic static class PatchPaths\n{\n    public const string Members = \"members\";\n    public const string DisplayName = \"displayname\";\n    public const string ExternalId = \"externalid\";\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Utilities/ScimContextMiddleware.cs",
    "content": "﻿using Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Scim.Context;\n\nnamespace Bit.Scim.Utilities;\n\npublic class ScimContextMiddleware\n{\n    private readonly RequestDelegate _next;\n\n    public ScimContextMiddleware(RequestDelegate next)\n    {\n        _next = next;\n    }\n\n    public async Task Invoke(HttpContext httpContext, IScimContext scimContext, GlobalSettings globalSettings,\n        IOrganizationRepository organizationRepository, IOrganizationConnectionRepository organizationConnectionRepository)\n    {\n        await scimContext.BuildAsync(httpContext, globalSettings, organizationRepository, organizationConnectionRepository);\n        await _next.Invoke(httpContext);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/Utilities/ScimServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Scim.Groups;\nusing Bit.Scim.Groups.Interfaces;\nusing Bit.Scim.Users;\nusing Bit.Scim.Users.Interfaces;\n\nnamespace Bit.Scim.Utilities;\n\npublic static class ScimServiceCollectionExtensions\n{\n    public static void AddScimGroupCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IPatchGroupCommand, PatchGroupCommand>();\n        services.AddScoped<IPostGroupCommand, PostGroupCommand>();\n        services.AddScoped<IPutGroupCommand, PutGroupCommand>();\n    }\n\n    public static void AddScimGroupQueries(this IServiceCollection services)\n    {\n        services.AddScoped<IGetGroupsListQuery, GetGroupsListQuery>();\n    }\n\n    public static void AddScimUserCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IPatchUserCommand, PatchUserCommand>();\n        services.AddScoped<IPostUserCommand, PostUserCommand>();\n    }\n\n    public static void AddScimUserQueries(this IServiceCollection services)\n    {\n        services.AddScoped<IGetUsersListQuery, GetUsersListQuery>();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/appsettings.Development.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\",\n      \"internalScim\": \"http://localhost:44559\"\n    },\n    \"mail\": {\n      \"smtp\": {\n        \"host\": \"localhost\",\n        \"port\": 10250\n      }\n    },\n    \"attachment\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\",\n      \"baseUrl\": \"http://localhost:4000/attachments/\"\n    },\n    \"events\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"storage\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"pricingUri\": \"https://billingpricing.qa.bitwarden.pw\"\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/appsettings.Production.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.bitwarden.com\",\n      \"api\": \"https://api.bitwarden.com\",\n      \"identity\": \"https://identity.bitwarden.com\",\n      \"admin\": \"https://admin.bitwarden.com\",\n      \"notifications\": \"https://notifications.bitwarden.com\",\n      \"sso\": \"https://sso.bitwarden.com\",\n      \"internalNotifications\": \"https://notifications.bitwarden.com\",\n      \"internalAdmin\": \"https://admin.bitwarden.com\",\n      \"internalIdentity\": \"https://identity.bitwarden.com\",\n      \"internalApi\": \"https://api.bitwarden.com\",\n      \"internalVault\": \"https://vault.bitwarden.com\",\n      \"internalSso\": \"https://sso.bitwarden.com\",\n      \"internalScim\": \"https://scim.bitwarden.com\"\n    },\n    \"braintree\": {\n      \"production\": true\n    },\n    \"bitPay\": {\n      \"production\": true\n    }\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Warning\",\n          \"System\": \"Warning\",\n          \"Microsoft\": \"Warning\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/appsettings.QA.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.qa.bitwarden.pw\",\n      \"api\": \"https://api.qa.bitwarden.pw\",\n      \"identity\": \"https://identity.qa.bitwarden.pw\",\n      \"admin\": \"https://admin.qa.bitwarden.pw\",\n      \"notifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"sso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalNotifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"internalAdmin\": \"https://admin.qa.bitwarden.pw\",\n      \"internalIdentity\": \"https://identity.qa.bitwarden.pw\",\n      \"internalApi\": \"https://api.qa.bitwarden.pw\",\n      \"internalVault\": \"https://vault.qa.bitwarden.pw\",\n      \"internalSso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalScim\": \"https://scim.qa.bitwarden.pw\"\n    },\n    \"braintree\": {\n      \"production\": false\n    },\n    \"bitPay\": {\n      \"production\": false\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Debug\",\n          \"System\": \"Debug\",\n          \"Microsoft\": \"Debug\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/appsettings.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"selfHosted\": false,\n    \"siteName\": \"Bitwarden\",\n    \"projectName\": \"Scim\",\n    \"stripe\": {\n      \"apiKey\": \"SECRET\"\n    },\n    \"sqlServer\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"mail\": {\n      \"sendGridApiKey\": \"SECRET\",\n      \"amazonConfigSetName\": \"Email\",\n      \"replyToEmail\": \"no-reply@bitwarden.com\"\n    },\n    \"identityServer\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"dataProtection\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"storage\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"events\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"serviceBus\": {\n      \"connectionString\": \"SECRET\",\n      \"applicationCacheTopicName\": \"SECRET\"\n    },\n    \"notificationHub\": {\n      \"connectionString\": \"SECRET\",\n      \"hubName\": \"SECRET\"\n    },\n    \"braintree\": {\n      \"production\": false,\n      \"merchantId\": \"SECRET\",\n      \"publicKey\": \"SECRET\",\n      \"privateKey\": \"SECRET\"\n    },\n    \"bitPay\": {\n      \"production\": false,\n      \"token\": \"SECRET\",\n      \"notificationUrl\": \"https://bitwarden.com/SECRET\"\n    },\n    \"amazon\": {\n      \"accessKeyId\": \"SECRET\",\n      \"accessKeySecret\": \"SECRET\",\n      \"region\": \"SECRET\"\n    }\n  },\n  \"scimSettings\": {}\n}\n"
  },
  {
    "path": "bitwarden_license/src/Scim/build.ps1",
    "content": "$dir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\necho \"`n## Building Scim\"\n\necho \"`nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore $dir\\Scim.csproj\necho \"Clean\"\ndotnet clean $dir\\Scim.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\necho \"Publish\"\ndotnet publish $dir\\Scim.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\n"
  },
  {
    "path": "bitwarden_license/src/Scim/build.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && \"pwd\" )\"\n\necho -e \"\\n## Building SCIM\"\n\necho -e \"\\nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore \"$DIR/Scim.csproj\"\necho \"Clean\"\ndotnet clean \"$DIR/Scim.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\necho \"Publish\"\ndotnet publish \"$DIR/Scim.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\n"
  },
  {
    "path": "bitwarden_license/src/Scim/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/core\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    if [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n      chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos\n    fi\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nif [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n    cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf\n    $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab\nfi\n\nif [ \"$globalSettings__selfHosted\" = \"true\" ]; then\n    if [ -z \"$globalSettings__identityServer__certificateLocation\" ]; then\n        export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx\n    fi\nfi\n\nexec $gosu_cmd /app/Scim\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Controllers/AccountController.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.Registration;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Bit.Sso.Models;\nusing Bit.Sso.Utilities;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer;\nusing Duende.IdentityServer.Services;\nusing Duende.IdentityServer.Stores;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.Mvc;\nusing AuthenticationSchemes = Bit.Core.AuthenticationSchemes;\nusing DIM = Duende.IdentityServer.Models;\n\nnamespace Bit.Sso.Controllers;\n\npublic class AccountController : Controller\n{\n    private readonly IAuthenticationSchemeProvider _schemeProvider;\n    private readonly IClientStore _clientStore;\n\n    private readonly IIdentityServerInteractionService _interaction;\n    private readonly ILogger<AccountController> _logger;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationService _organizationService;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly ISsoUserRepository _ssoUserRepository;\n    private readonly IUserRepository _userRepository;\n    private readonly IPolicyQuery _policyQuery;\n    private readonly IUserService _userService;\n    private readonly II18nService _i18nService;\n    private readonly UserManager<User> _userManager;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly Core.Services.IEventService _eventService;\n    private readonly IDataProtectorTokenFactory<SsoTokenable> _dataProtector;\n    private readonly IOrganizationDomainRepository _organizationDomainRepository;\n    private readonly IRegisterUserCommand _registerUserCommand;\n\n    public AccountController(\n        IAuthenticationSchemeProvider schemeProvider,\n        IClientStore clientStore,\n        IIdentityServerInteractionService interaction,\n        ILogger<AccountController> logger,\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IOrganizationService organizationService,\n        ISsoConfigRepository ssoConfigRepository,\n        ISsoUserRepository ssoUserRepository,\n        IUserRepository userRepository,\n        IPolicyQuery policyQuery,\n        IUserService userService,\n        II18nService i18nService,\n        UserManager<User> userManager,\n        IGlobalSettings globalSettings,\n        Core.Services.IEventService eventService,\n        IDataProtectorTokenFactory<SsoTokenable> dataProtector,\n        IOrganizationDomainRepository organizationDomainRepository,\n        IRegisterUserCommand registerUserCommand)\n    {\n        _schemeProvider = schemeProvider;\n        _clientStore = clientStore;\n        _interaction = interaction;\n        _logger = logger;\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationService = organizationService;\n        _userRepository = userRepository;\n        _ssoConfigRepository = ssoConfigRepository;\n        _ssoUserRepository = ssoUserRepository;\n        _policyQuery = policyQuery;\n        _userService = userService;\n        _i18nService = i18nService;\n        _userManager = userManager;\n        _eventService = eventService;\n        _globalSettings = globalSettings;\n        _dataProtector = dataProtector;\n        _organizationDomainRepository = organizationDomainRepository;\n        _registerUserCommand = registerUserCommand;\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> PreValidateAsync(string domainHint)\n    {\n        try\n        {\n            // Validate domain_hint provided\n            if (string.IsNullOrWhiteSpace(domainHint))\n            {\n                _logger.LogError(new ArgumentException(\"domainHint is required.\"), \"domainHint not specified.\");\n                return InvalidJson(\"SsoInvalidIdentifierError\");\n            }\n\n            // Validate organization exists from domain_hint\n            var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);\n            if (organization is not { UseSso: true })\n            {\n                _logger.LogError(\"Organization not configured to use SSO.\");\n                return InvalidJson(\"SsoInvalidIdentifierError\");\n            }\n\n            // Validate SsoConfig exists and is Enabled\n            var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);\n            if (ssoConfig is not { Enabled: true })\n            {\n                _logger.LogError(\"SsoConfig not enabled.\");\n                return InvalidJson(\"SsoInvalidIdentifierError\");\n            }\n\n            // Validate Authentication Scheme exists and is loaded (cache)\n            var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());\n            if (scheme is not IDynamicAuthenticationScheme dynamicScheme)\n            {\n                _logger.LogError(\"Invalid authentication scheme for organization.\");\n                return InvalidJson(\"SsoInvalidIdentifierError\");\n            }\n\n            // Run scheme validation\n            try\n            {\n                await dynamicScheme.Validate();\n            }\n            catch (Exception ex)\n            {\n                _logger.LogError(ex, \"An error occurred while validating SSO dynamic scheme.\");\n                return InvalidJson(\"SsoInvalidIdentifierError\");\n            }\n\n            var tokenable = new SsoTokenable(organization, _globalSettings.Sso.SsoTokenLifetimeInSeconds);\n            var token = _dataProtector.Protect(tokenable);\n\n            return new SsoPreValidateResponseModel(token);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"An error occurred during SSO prevalidation.\");\n            return InvalidJson(\"SsoInvalidIdentifierError\");\n        }\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> LoginAsync(string returnUrl)\n    {\n        var context = await _interaction.GetAuthorizationContextAsync(returnUrl);\n\n        // FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n        if (!context.Parameters.AllKeys.Contains(\"domain_hint\") ||\n            string.IsNullOrWhiteSpace(context.Parameters[\"domain_hint\"]))\n        {\n            throw new Exception(_i18nService.T(\"NoDomainHintProvided\"));\n        }\n\n        var ssoToken = context.Parameters[SsoTokenable.TokenIdentifier];\n\n        if (string.IsNullOrWhiteSpace(ssoToken))\n        {\n            return Unauthorized(\"A valid SSO token is required to continue with SSO login\");\n        }\n\n        var domainHint = context.Parameters[\"domain_hint\"];\n        var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);\n#nullable restore\n\n        if (organization == null)\n        {\n            return InvalidJson(\"OrganizationNotFoundByIdentifierError\");\n        }\n\n        var tokenable = _dataProtector.Unprotect(ssoToken);\n\n        if (!tokenable.TokenIsValid(organization))\n        {\n            return Unauthorized(\"The SSO token associated with your request is expired. A valid SSO token is required to continue.\");\n        }\n\n        return RedirectToAction(nameof(ExternalChallenge), new\n        {\n            scheme = organization.Id.ToString(),\n            returnUrl,\n            state = context.Parameters[\"state\"],\n            userIdentifier = context.Parameters[\"session_state\"],\n            ssoToken\n        });\n    }\n\n    [HttpGet]\n    public IActionResult ExternalChallenge(string scheme, string returnUrl, string state, string userIdentifier, string ssoToken)\n    {\n        ValidateSchemeAgainstSsoToken(scheme, ssoToken);\n\n        if (string.IsNullOrEmpty(returnUrl))\n        {\n            returnUrl = \"~/\";\n        }\n\n        // Clean the returnUrl\n        returnUrl = CoreHelpers.ReplaceWhiteSpace(returnUrl, string.Empty);\n        if (!Url.IsLocalUrl(returnUrl) && !_interaction.IsValidReturnUrl(returnUrl))\n        {\n            throw new Exception(_i18nService.T(\"InvalidReturnUrl\"));\n        }\n\n        var props = new AuthenticationProperties\n        {\n            RedirectUri = Url.Action(nameof(ExternalCallback)),\n            Items =\n            {\n                // scheme will get serialized into `State` and returned back\n                { \"scheme\", scheme },\n                { \"return_url\", returnUrl },\n                { \"state\", state },\n                { \"user_identifier\", userIdentifier },\n            }\n        };\n\n        return Challenge(props, scheme);\n    }\n\n    /// <summary>\n    /// Validates the scheme (organization ID) against the organization ID found in the ssoToken.\n    /// </summary>\n    /// <param name=\"scheme\">The authentication scheme (organization ID) to validate.</param>\n    /// <param name=\"ssoToken\">The SSO token to validate against.</param>\n    /// <exception cref=\"Exception\">Thrown if the scheme (organization ID) does not match the organization ID found in the ssoToken.</exception>\n    private void ValidateSchemeAgainstSsoToken(string scheme, string ssoToken)\n    {\n        SsoTokenable tokenable;\n\n        try\n        {\n            tokenable = _dataProtector.Unprotect(ssoToken);\n        }\n        catch\n        {\n            throw new Exception(_i18nService.T(\"InvalidSsoToken\"));\n        }\n\n        if (!Guid.TryParse(scheme, out var schemeOrgId) || tokenable.OrganizationId != schemeOrgId)\n        {\n            throw new Exception(_i18nService.T(\"SsoOrganizationIdMismatch\"));\n        }\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> ExternalCallback()\n    {\n        // Read external identity from the temporary cookie\n        var result = await HttpContext.AuthenticateAsync(\n            AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);\n\n        if (!result.Succeeded)\n        {\n            throw new Exception(_i18nService.T(\"ExternalAuthenticationError\"));\n        }\n\n        // See if the user has logged in with this SSO provider before and has already been provisioned.\n        // This is signified by the user existing in the User table and the SSOUser table for the SSO provider they're using.\n        var (possibleSsoLinkedUser, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result);\n\n        // We will look these up as required (lazy resolution) to avoid multiple DB hits.\n        Organization? organization = null;\n        OrganizationUser? orgUser = null;\n\n        // The user has not authenticated with this SSO provider before.\n        // They could have an existing Bitwarden account in the User table though.\n        if (possibleSsoLinkedUser == null)\n        {\n            // FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n            // If we're manually linking to SSO, the user's external identifier will be passed as query string parameter.\n            var userIdentifier = result.Properties.Items.Keys.Contains(\"user_identifier\")\n                ? result.Properties.Items[\"user_identifier\"]\n                : null;\n\n            var (resolvedUser, foundOrganization, foundOrCreatedOrgUser) =\n                await CreateUserAndOrgUserConditionallyAsync(\n                    provider,\n                    providerUserId,\n                    claims,\n                    userIdentifier,\n                    ssoConfigData);\n#nullable restore\n\n            possibleSsoLinkedUser = resolvedUser;\n            organization = foundOrganization;\n            orgUser = foundOrCreatedOrgUser;\n        }\n\n        User resolvedSsoLinkedUser = possibleSsoLinkedUser\n                                              ?? throw new Exception(_i18nService.T(\"UserShouldBeFound\"));\n\n        await PreventOrgUserLoginIfStatusInvalidAsync(organization, provider, orgUser, resolvedSsoLinkedUser);\n\n        // This allows us to collect any additional claims or properties\n        // for the specific protocols used and store them in the local auth cookie.\n        // this is typically used to store data needed for signout from those protocols.\n        var additionalLocalClaims = new List<Claim>();\n        var localSignInProps = new AuthenticationProperties\n        {\n            IsPersistent = true,\n            ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1)\n        };\n        ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);\n\n        // Issue authentication cookie for user\n        await HttpContext.SignInAsync(\n            new IdentityServerUser(resolvedSsoLinkedUser.Id.ToString())\n            {\n                DisplayName = resolvedSsoLinkedUser.Email,\n                IdentityProvider = provider,\n                AdditionalClaims = additionalLocalClaims.ToArray()\n            }, localSignInProps);\n\n        // Delete temporary cookie used during external authentication\n        await HttpContext.SignOutAsync(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);\n\n        // FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n        // Retrieve return URL\n        var returnUrl = result.Properties.Items[\"return_url\"] ?? \"~/\";\n#nullable restore\n\n        // Check if external login is in the context of an OIDC request\n        var context = await _interaction.GetAuthorizationContextAsync(returnUrl);\n        if (context != null)\n        {\n            if (IsNativeClient(context))\n            {\n                // The client is native, so this change in how to\n                // return the response is for better UX for the end user.\n                HttpContext.Response.StatusCode = 200;\n                HttpContext.Response.Headers[\"Location\"] = string.Empty;\n                return View(\"Redirect\", new RedirectViewModel { RedirectUrl = returnUrl });\n            }\n        }\n\n        return Redirect(returnUrl);\n    }\n\n    // FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n    [HttpGet]\n    public async Task<IActionResult> LogoutAsync(string logoutId)\n    {\n        // Build a model so the logged out page knows what to display\n        var (updatedLogoutId, redirectUri, externalAuthenticationScheme) = await GetLoggedOutDataAsync(logoutId);\n\n        if (User?.Identity.IsAuthenticated == true)\n        {\n            // Delete local authentication cookie\n            await HttpContext.SignOutAsync();\n        }\n\n        // HACK: Temporary workaround for the time being that doesn't try to sign out of OneLogin schemes,\n        // which doesn't support SLO\n        if (externalAuthenticationScheme != null && !externalAuthenticationScheme.Contains(\"onelogin\"))\n        {\n            // Build a return URL so the upstream provider will redirect back\n            // to us after the user has logged out. this allows us to then\n            // complete our single sign-out processing.\n            var url = Url.Action(\"Logout\", new { logoutId = updatedLogoutId });\n\n            // This triggers a redirect to the external provider for sign-out\n            return SignOut(new AuthenticationProperties { RedirectUri = url }, externalAuthenticationScheme);\n        }\n\n        if (redirectUri != null)\n        {\n            return View(\"Redirect\", new RedirectViewModel { RedirectUrl = redirectUri });\n        }\n        else\n        {\n            return Redirect(\"~/\");\n        }\n    }\n#nullable restore\n\n    /// <summary>\n    /// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`.\n    /// The claims on the external identity are used to determine an `externalId`, and that is used to find the appropriate `SsoUser` and `User` records.\n    /// </summary>\n    private async Task<(\n        User? possibleSsoUser,\n        string provider,\n        string providerUserId,\n        IEnumerable<Claim> claims,\n        SsoConfigurationData config\n    )> FindUserFromExternalProviderAsync(AuthenticateResult result)\n    {\n        // FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n        var provider = result.Properties.Items[\"scheme\"];\n        //Todo: Validate provider is a valid GUID with TryParse instead. When this is invalid it throws an exception\n        var orgId = new Guid(provider);\n        var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);\n        if (ssoConfig == null || !ssoConfig.Enabled)\n        {\n            throw new Exception(_i18nService.T(\"OrganizationOrSsoConfigNotFound\"));\n        }\n\n        var ssoConfigData = ssoConfig.GetData();\n        var externalUser = result.Principal;\n\n        // Validate acr claim against expectation before going further\n        if (!string.IsNullOrWhiteSpace(ssoConfigData.ExpectedReturnAcrValue))\n        {\n            var acrClaim = externalUser.FindFirst(JwtClaimTypes.AuthenticationContextClassReference);\n            if (acrClaim?.Value != ssoConfigData.ExpectedReturnAcrValue)\n            {\n                throw new Exception(_i18nService.T(\"AcrMissingOrInvalid\"));\n            }\n        }\n\n        // Ensure the NameIdentifier used is not a transient name ID, if so, we need a different attribute\n        //  for the user identifier.\n        static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier\n                                                     && (c.Properties == null\n                                                         || !c.Properties.TryGetValue(SamlPropertyKeys.ClaimFormat,\n                                                             out var claimFormat)\n                                                         || claimFormat != SamlNameIdFormats.Transient);\n\n        // Try to determine the unique id of the external user (issued by the provider)\n        // the most common claim type for that are the sub claim and the NameIdentifier\n        // depending on the external provider, some other claim type might be used\n        var customUserIdClaimTypes = ssoConfigData.GetAdditionalUserIdClaimTypes();\n        var userIdClaim = externalUser.FindFirst(c => customUserIdClaimTypes.Contains(c.Type)) ??\n                          externalUser.FindFirst(JwtClaimTypes.Subject) ??\n                          externalUser.FindFirst(nameIdIsNotTransient) ??\n                          // Some SAML providers may use the `uid` attribute for this\n                          //    where a transient NameID has been sent in the subject\n                          externalUser.FindFirst(\"uid\") ??\n                          externalUser.FindFirst(\"upn\") ??\n                          externalUser.FindFirst(\"eppn\") ??\n                          throw new Exception(_i18nService.T(\"UnknownUserId\"));\n#nullable restore\n\n        // Remove the user id claim so we don't include it as an extra claim if/when we provision the user\n        var claims = externalUser.Claims.ToList();\n        claims.Remove(userIdClaim);\n\n        // find external user\n        var providerUserId = userIdClaim.Value;\n\n        var possibleSsoUser = await _userRepository.GetBySsoUserAsync(providerUserId, orgId);\n\n        return (possibleSsoUser, provider, providerUserId, claims, ssoConfigData);\n    }\n\n    /// <summary>\n    /// This function seeks to set up the org user record or create a new user record based on the conditions\n    /// below.\n    ///\n    /// This handles three different scenarios:\n    /// 1. Creating an SsoUser link for an existing User and OrganizationUser\n    ///     - User is a member of the organization, but hasn't authenticated with the org's SSO provider before.\n    /// 2. Creating a new User and a new OrganizationUser, then establishing an SsoUser link\n    ///     - User is joining the organization through JIT provisioning, without a pending invitation\n    /// 3. Creating a new User for an existing OrganizationUser (created by invitation), then establishing an SsoUser link\n    ///     - User is signing in with a pending invitation.\n    /// </summary>\n    /// <param name=\"provider\">The external identity provider.</param>\n    /// <param name=\"providerUserId\">The external identity provider's user identifier.</param>\n    /// <param name=\"claims\">The claims from the external IdP.</param>\n    /// <param name=\"userIdentifier\">The user identifier used for manual SSO linking.</param>\n    /// <param name=\"ssoConfigData\">The SSO configuration for the organization.</param>\n    /// <returns>Guaranteed to return the user to sign in as well as the found organization and org user.</returns>\n    /// <exception cref=\"Exception\">An exception if the user cannot be provisioned as requested.</exception>\n    private async Task<(User resolvedUser, Organization foundOrganization, OrganizationUser foundOrgUser)> CreateUserAndOrgUserConditionallyAsync(\n            string provider,\n            string providerUserId,\n            IEnumerable<Claim> claims,\n            string userIdentifier,\n            SsoConfigurationData ssoConfigData\n        )\n    {\n        // Try to get the email from the claims as we don't know if we have a user record yet.\n        var name = GetName(claims, ssoConfigData.GetAdditionalNameClaimTypes());\n        var email = TryGetEmailAddress(claims, ssoConfigData, providerUserId);\n\n        User? possibleExistingUser;\n        if (string.IsNullOrWhiteSpace(userIdentifier))\n        {\n            if (string.IsNullOrWhiteSpace(email))\n            {\n                throw new Exception(_i18nService.T(\"CannotFindEmailClaim\"));\n            }\n\n            possibleExistingUser = await _userRepository.GetByEmailAsync(email);\n        }\n        else\n        {\n            possibleExistingUser = await GetUserFromManualLinkingDataAsync(userIdentifier);\n        }\n\n        // Find the org (we error if we can't find an org because no org is not valid)\n        var organization = await GetOrganizationByProviderAsync(provider);\n\n        // Try to find an org user (null org user possible and valid here)\n        var possibleOrgUser = await GetOrganizationUserByUserAndOrgIdOrEmailAsync(possibleExistingUser, organization.Id, email);\n\n        //----------------------------------------------------\n        // Scenario 1: We've found the user in the User table\n        //----------------------------------------------------\n        if (possibleExistingUser != null)\n        {\n            User guaranteedExistingUser = possibleExistingUser;\n\n            if (guaranteedExistingUser.UsesKeyConnector &&\n                (possibleOrgUser == null || possibleOrgUser.Status == OrganizationUserStatusType.Invited))\n            {\n                throw new Exception(_i18nService.T(\"UserAlreadyExistsKeyConnector\"));\n            }\n\n            OrganizationUser guaranteedOrgUser = possibleOrgUser ?? throw new Exception(_i18nService.T(\"UserAlreadyExistsInviteProcess\"));\n\n            /*\n             * ----------------------------------------------------\n             *              Critical Code Check Here\n             *\n             * We want to ensure a user is not in the invited state\n             * explicitly. User's in the invited state should not\n             * be able to authenticate via SSO.\n             *\n             * See internal doc called \"Added Context for SSO Login\n             * Flows\" for further details.\n             * ----------------------------------------------------\n             */\n            if (guaranteedOrgUser.Status == OrganizationUserStatusType.Invited)\n            {\n                // Org User is invited – must accept via email first\n                throw new Exception(\n                    _i18nService.T(\"AcceptInviteBeforeUsingSSO\", organization.DisplayName()));\n            }\n\n            // If the user already exists in Bitwarden, we require that the user already be in the org,\n            // and that they are either Accepted or Confirmed.\n            EnforceAllowedOrgUserStatus(\n                guaranteedOrgUser.Status,\n                allowedStatuses: [\n                    OrganizationUserStatusType.Accepted,\n                    OrganizationUserStatusType.Confirmed\n                ],\n                organization.DisplayName());\n\n            // Since we're in the auto-provisioning logic, this means that the user exists, but they have not\n            // authenticated with the org's SSO provider before now (otherwise we wouldn't be auto-provisioning them).\n            // We've verified that the user is Accepted or Confirmed, so we can create an SsoUser link and proceed\n            // with authentication.\n            await CreateSsoUserRecordAsync(providerUserId, guaranteedExistingUser.Id, organization.Id, guaranteedOrgUser);\n\n            return (guaranteedExistingUser, organization, guaranteedOrgUser);\n        }\n\n        // Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one\n        if (possibleOrgUser == null && organization.Seats.HasValue)\n        {\n            var occupiedSeats =\n                await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);\n            var initialSeatCount = organization.Seats.Value;\n            var availableSeats = initialSeatCount - occupiedSeats.Total;\n            if (availableSeats < 1)\n            {\n                try\n                {\n                    if (_globalSettings.SelfHosted)\n                    {\n                        throw new Exception(\"Cannot autoscale on self-hosted instance.\");\n                    }\n\n                    await _organizationService.AutoAddSeatsAsync(organization, 1);\n                }\n                catch (Exception e)\n                {\n                    if (organization.Seats.Value != initialSeatCount)\n                    {\n                        await _organizationService.AdjustSeatsAsync(organization.Id,\n                            initialSeatCount - organization.Seats.Value);\n                    }\n\n                    _logger.LogInformation(e, \"SSO auto provisioning failed\");\n                    throw new Exception(_i18nService.T(\"NoSeatsAvailable\", organization.DisplayName()));\n                }\n            }\n        }\n\n        // If the email domain is verified, we can mark the email as verified\n        if (string.IsNullOrWhiteSpace(email))\n        {\n            throw new Exception(_i18nService.T(\"CannotFindEmailClaim\"));\n        }\n\n        var emailVerified = false;\n        var emailDomain = CoreHelpers.GetEmailDomain(email);\n        if (!string.IsNullOrWhiteSpace(emailDomain))\n        {\n            var organizationDomain =\n                await _organizationDomainRepository.GetDomainByOrgIdAndDomainNameAsync(organization.Id, emailDomain);\n            emailVerified = organizationDomain?.VerifiedDate.HasValue ?? false;\n        }\n\n        //--------------------------------------------------\n        // Scenarios 2 and 3: We need to register a new user\n        //--------------------------------------------------\n        var newUser = new User\n        {\n            Name = name,\n            Email = email,\n            EmailVerified = emailVerified,\n            ApiKey = CoreHelpers.SecureRandomString(30)\n        };\n\n        // Always use RegisterSSOAutoProvisionedUserAsync to ensure organization context is available\n        // for domain validation (BlockClaimedDomainAccountCreation policy) and welcome emails.\n        // The feature flag logic for welcome email templates is handled internally by RegisterUserCommand.\n        await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);\n\n        // If the organization has 2fa policy enabled, make sure to default jit user 2fa to email\n        var twoFactorPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.TwoFactorAuthentication);\n        if (twoFactorPolicy.Enabled)\n        {\n            newUser.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n            {\n                [TwoFactorProviderType.Email] = new TwoFactorProvider\n                {\n                    MetaData = new Dictionary<string, object> { [\"Email\"] = newUser.Email.ToLowerInvariant() },\n                    Enabled = true\n                }\n            });\n            await _userService.UpdateTwoFactorProviderAsync(newUser, TwoFactorProviderType.Email);\n        }\n\n        //-----------------------------------------------------------------\n        // Scenario 2: We also need to create an OrganizationUser\n        // This means that an invitation was not sent for this user and we\n        // need to establish their invited status now.\n        //-----------------------------------------------------------------\n        if (possibleOrgUser == null)\n        {\n            possibleOrgUser = new OrganizationUser\n            {\n                OrganizationId = organization.Id,\n                UserId = newUser.Id,\n                Type = OrganizationUserType.User,\n                Status = OrganizationUserStatusType.Invited\n            };\n            await _organizationUserRepository.CreateAsync(possibleOrgUser);\n        }\n\n        //-----------------------------------------------------------------\n        // Scenario 3: There is already an existing OrganizationUser\n        // That was established through an invitation. We just need to\n        // update the UserId now that we have created a User record.\n        //-----------------------------------------------------------------\n        else\n        {\n            possibleOrgUser.UserId = newUser.Id;\n            await _organizationUserRepository.ReplaceAsync(possibleOrgUser);\n        }\n\n        // Create the SsoUser record to link the user to the SSO provider.\n        await CreateSsoUserRecordAsync(providerUserId, newUser.Id, organization.Id, possibleOrgUser);\n\n        return (newUser, organization, possibleOrgUser);\n    }\n\n    /// <summary>\n    /// Validates an organization user is allowed to log in via SSO and blocks invalid statuses.\n    /// Lazily resolves the organization and organization user if not provided.\n    /// </summary>\n    /// <param name=\"organization\">The target organization; if null, resolved from provider.</param>\n    /// <param name=\"provider\">The SSO scheme provider value (organization id as a GUID string).</param>\n    /// <param name=\"orgUser\">The organization-user record; if null, looked up by user/org or user email for invited users.</param>\n    /// <param name=\"user\">The user attempting to sign in (existing or newly provisioned).</param>\n    /// <exception cref=\"Exception\">Thrown if the organization cannot be resolved from provider;\n    /// the organization user cannot be found; or the organization user status is not allowed.</exception>\n    private async Task PreventOrgUserLoginIfStatusInvalidAsync(\n        Organization? organization,\n        string provider,\n        OrganizationUser? orgUser,\n        User user)\n    {\n        // Lazily get organization if not already known\n        organization ??= await GetOrganizationByProviderAsync(provider);\n\n        // Lazily get the org user if not already known\n        orgUser ??= await GetOrganizationUserByUserAndOrgIdOrEmailAsync(\n            user,\n            organization.Id,\n            user.Email);\n\n        if (orgUser != null)\n        {\n            // Invited is allowed at this point because we know the user is trying to accept an org invite.\n            EnforceAllowedOrgUserStatus(\n                orgUser.Status,\n                allowedStatuses: [\n                    OrganizationUserStatusType.Invited,\n                    OrganizationUserStatusType.Accepted,\n                    OrganizationUserStatusType.Confirmed,\n                ],\n                organization.DisplayName());\n        }\n        else\n        {\n            throw new Exception(_i18nService.T(\"CouldNotFindOrganizationUser\", user.Id, organization.Id));\n        }\n    }\n\n    private async Task<User?> GetUserFromManualLinkingDataAsync(string userIdentifier)\n    {\n        User? user = null;\n        var split = userIdentifier.Split(\",\");\n        if (split.Length < 2)\n        {\n            throw new Exception(_i18nService.T(\"InvalidUserIdentifier\"));\n        }\n\n        var userId = split[0];\n        var token = split[1];\n\n        var tokenOptions = new TokenOptions();\n\n        var claimedUser = await _userService.GetUserByIdAsync(userId);\n        if (claimedUser != null)\n        {\n            var tokenIsValid = await _userManager.VerifyUserTokenAsync(\n                claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);\n            if (tokenIsValid)\n            {\n                user = claimedUser;\n            }\n            else\n            {\n                throw new Exception(_i18nService.T(\"UserIdAndTokenMismatch\"));\n            }\n        }\n\n        return user;\n    }\n\n    /// <summary>\n    /// Tries to get the organization by the provider which is org id for us as we use the scheme\n    /// to identify organizations - not identity providers.\n    /// </summary>\n    /// <param name=\"provider\">Org id string from SSO scheme property</param>\n    /// <exception cref=\"Exception\">Errors if the provider string is not a valid org id guid or if the org cannot be found by the id.</exception>\n    private async Task<Organization> GetOrganizationByProviderAsync(string provider)\n    {\n        if (!Guid.TryParse(provider, out var organizationId))\n        {\n            // TODO: support non-org (server-wide) SSO in the future?\n            throw new Exception(_i18nService.T(\"SSOProviderIsNotAnOrgId\", provider));\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            throw new Exception(_i18nService.T(\"CouldNotFindOrganization\", organizationId));\n        }\n\n        return organization;\n    }\n\n    /// <summary>\n    /// Attempts to get an <see cref=\"OrganizationUser\"/> for a given organization\n    /// by first checking for an existing user relationship, and if none is found,\n    /// by looking up an invited user via their email address.\n    /// </summary>\n    /// <param name=\"user\">The existing user entity to be looked up in OrganizationUsers table.</param>\n    /// <param name=\"organizationId\">Organization id from the provider data.</param>\n    /// <param name=\"email\">Email to use as a fallback in case of an invited user not in the Org Users\n    /// table yet.</param>\n    private async Task<OrganizationUser?> GetOrganizationUserByUserAndOrgIdOrEmailAsync(\n        User? user,\n        Guid organizationId,\n        string? email)\n    {\n        OrganizationUser? orgUser = null;\n\n        // Try to find OrgUser via existing User Id.\n        // This covers any OrganizationUser state after they have accepted an invite.\n        if (user != null)\n        {\n            var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(user.Id);\n            orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == organizationId);\n        }\n\n        // If no Org User found by Existing User Id - search all the organization's users via email.\n        // This covers users who are Invited but haven't accepted their invite yet.\n        if (email != null)\n        {\n            orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(organizationId, email);\n        }\n\n        return orgUser;\n    }\n\n    private void EnforceAllowedOrgUserStatus(\n        OrganizationUserStatusType statusToCheckAgainst,\n        OrganizationUserStatusType[] allowedStatuses,\n        string organizationDisplayNameForLogging)\n    {\n        // if this status is one of the allowed ones, just return\n        if (allowedStatuses.Contains(statusToCheckAgainst))\n        {\n            return;\n        }\n\n        // otherwise throw the appropriate exception\n        switch (statusToCheckAgainst)\n        {\n            case OrganizationUserStatusType.Revoked:\n                // Revoked users may not be (auto)‑provisioned\n                throw new Exception(\n                    _i18nService.T(\"OrganizationUserAccessRevoked\", organizationDisplayNameForLogging));\n            default:\n                // anything else is “unknown”\n                throw new Exception(\n                    _i18nService.T(\"OrganizationUserUnknownStatus\", organizationDisplayNameForLogging));\n        }\n    }\n\n    private IActionResult InvalidJson(string errorMessageKey, Exception? ex = null)\n    {\n        Response.StatusCode = ex == null ? 400 : 500;\n        return Json(new ErrorResponseModel(_i18nService.T(errorMessageKey))\n        {\n            ExceptionMessage = ex?.Message,\n            ExceptionStackTrace = ex?.StackTrace,\n            InnerExceptionMessage = ex?.InnerException?.Message,\n        });\n    }\n\n    private string? TryGetEmailAddressFromClaims(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)\n    {\n        var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value) && c.Value.Contains(\"@\"));\n\n        var email = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??\n                    filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,\n                        SamlClaimTypes.Email, \"mail\", \"emailaddress\");\n        if (!string.IsNullOrWhiteSpace(email))\n        {\n            return email;\n        }\n\n        var username = filteredClaims.GetFirstMatch(JwtClaimTypes.PreferredUserName,\n            SamlClaimTypes.UserId, \"uid\");\n        if (!string.IsNullOrWhiteSpace(username))\n        {\n            return username;\n        }\n\n        return null;\n    }\n\n    // FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n    private string GetName(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)\n    {\n        var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value));\n\n        var name = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??\n                   filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,\n                       SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, \"displayname\", \"cn\");\n        if (!string.IsNullOrWhiteSpace(name))\n        {\n            return name;\n        }\n\n        var givenName = filteredClaims.GetFirstMatch(SamlClaimTypes.GivenName, \"givenname\", \"firstname\",\n            \"fn\", \"fname\", \"nickname\");\n        var surname = filteredClaims.GetFirstMatch(SamlClaimTypes.Surname, \"sn\", \"surname\", \"lastname\");\n        var nameParts = new[] { givenName, surname }.Where(p => !string.IsNullOrWhiteSpace(p));\n        if (nameParts.Any())\n        {\n            return string.Join(' ', nameParts);\n        }\n\n        return null;\n    }\n#nullable restore\n\n    private async Task CreateSsoUserRecordAsync(string providerUserId, Guid userId, Guid orgId,\n        OrganizationUser orgUser)\n    {\n        // Delete existing SsoUser (if any) - avoids error if providerId has changed and the sso link is stale\n        var existingSsoUser = await _ssoUserRepository.GetByUserIdOrganizationIdAsync(orgId, userId);\n        if (existingSsoUser != null)\n        {\n            await _ssoUserRepository.DeleteAsync(userId, orgId);\n            await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_ResetSsoLink);\n        }\n        else\n        {\n            // If no stale user, this is the user's first Sso login ever\n            await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_FirstSsoLogin);\n        }\n\n        var ssoUser = new SsoUser { ExternalId = providerUserId, UserId = userId, OrganizationId = orgId, };\n        await _ssoUserRepository.CreateAsync(ssoUser);\n    }\n\n    // FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n    private void ProcessLoginCallback(AuthenticateResult externalResult,\n        List<Claim> localClaims, AuthenticationProperties localSignInProps)\n    {\n        // If the external system sent a session id claim, copy it over\n        // so we can use it for single sign-out\n        var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);\n        if (sid != null)\n        {\n            localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));\n        }\n\n        // If the external provider issued an idToken, we'll keep it for signout\n        var idToken = externalResult.Properties.GetTokenValue(\"id_token\");\n        if (idToken != null)\n        {\n            localSignInProps.StoreTokens(\n                new[] { new AuthenticationToken { Name = \"id_token\", Value = idToken } });\n        }\n    }\n\n    private async Task<(string, string, string)> GetLoggedOutDataAsync(string logoutId)\n    {\n        // Get context information (client name, post logout redirect URI and iframe for federated signout)\n        var logout = await _interaction.GetLogoutContextAsync(logoutId);\n        string externalAuthenticationScheme = null;\n        if (User?.Identity.IsAuthenticated == true)\n        {\n            var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;\n            if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)\n            {\n                var provider = HttpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();\n                var handler = await provider.GetHandlerAsync(HttpContext, idp);\n\n                if (handler is IAuthenticationSignOutHandler)\n                {\n                    if (logoutId == null)\n                    {\n                        // If there's no current logout context, we need to create one\n                        // this captures necessary info from the current logged in user\n                        // before we signout and redirect away to the external IdP for signout\n                        logoutId = await _interaction.CreateLogoutContextAsync();\n                    }\n\n                    externalAuthenticationScheme = idp;\n                }\n            }\n        }\n\n        return (logoutId, logout?.PostLogoutRedirectUri, externalAuthenticationScheme);\n    }\n#nullable restore\n\n    /**\n     * Tries to get a user's email from the claims and SSO configuration data or the provider user id if\n     * the claims email extraction returns null.\n     */\n    private string? TryGetEmailAddress(\n        IEnumerable<Claim> claims,\n        SsoConfigurationData config,\n        string providerUserId)\n    {\n        var email = TryGetEmailAddressFromClaims(claims, config.GetAdditionalEmailClaimTypes());\n\n        // If email isn't populated from claims and providerUserId has @, assume it is the email.\n        if (string.IsNullOrWhiteSpace(email) && providerUserId.Contains(\"@\"))\n        {\n            email = providerUserId;\n        }\n\n        return email;\n    }\n\n    public bool IsNativeClient(DIM.AuthorizationRequest context)\n    {\n        return !context.RedirectUri.StartsWith(\"https\", StringComparison.Ordinal)\n               && !context.RedirectUri.StartsWith(\"http\", StringComparison.Ordinal);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Controllers/HomeController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Diagnostics;\nusing Bit.Sso.Models;\nusing Duende.IdentityServer.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Diagnostics;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Sso.Controllers;\n\npublic class HomeController : Controller\n{\n    private readonly IIdentityServerInteractionService _interaction;\n\n    public HomeController(IIdentityServerInteractionService interaction)\n    {\n        _interaction = interaction;\n    }\n\n    [Route(\"~/Error\")]\n    [Route(\"~/Home/Error\")]\n    [AllowAnonymous]\n    public async Task<IActionResult> Error(string errorId)\n    {\n        var vm = new ErrorViewModel();\n\n        // retrieve error details from identityserver\n        var message = string.IsNullOrWhiteSpace(errorId) ? null :\n            await _interaction.GetErrorContextAsync(errorId);\n        if (message != null)\n        {\n            vm.Error = message;\n        }\n        else\n        {\n            vm.RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;\n            var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();\n            var exception = exceptionHandlerPathFeature?.Error;\n            if (exception is InvalidOperationException opEx && opEx.Message.Contains(\"schemes are: \"))\n            {\n                // Messages coming from aspnetcore with a message\n                //  similar to \"The registered sign-in schemes are: {schemes}.\"\n                //  will expose other Org IDs and sign-in schemes enabled on\n                //  the server. These errors should be truncated to just the\n                //  scheme impacted (always the first sentence)\n                var cleanupPoint = opEx.Message.IndexOf(\". \") + 1;\n                var exMessage = opEx.Message.Substring(0, cleanupPoint);\n                exception = new InvalidOperationException(exMessage, opEx);\n            }\n            vm.Exception = exception;\n        }\n\n        return View(\"Error\", vm);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Controllers/InfoController.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Sso.Controllers;\n\npublic class InfoController : Controller\n{\n    [HttpGet(\"~/alive\")]\n    [HttpGet(\"~/now\")]\n    public DateTime GetAlive()\n    {\n        return DateTime.UtcNow;\n    }\n\n    [HttpGet(\"~/version\")]\n    public JsonResult GetVersion()\n    {\n        return Json(AssemblyHelpers.GetVersion());\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Controllers/MetadataController.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Sso.Utilities;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Mvc;\nusing Sustainsys.Saml2.AspNetCore2;\nusing Sustainsys.Saml2.WebSso;\n\nnamespace Bit.Sso.Controllers;\n\npublic class MetadataController : Controller\n{\n    private readonly IAuthenticationSchemeProvider _schemeProvider;\n\n    public MetadataController(\n        IAuthenticationSchemeProvider schemeProvider)\n    {\n        _schemeProvider = schemeProvider;\n    }\n\n    [HttpGet(\"saml2/{scheme}\")]\n    public async Task<IActionResult> ViewAsync(string scheme)\n    {\n        if (string.IsNullOrWhiteSpace(scheme))\n        {\n            return NotFound();\n        }\n\n        var authScheme = await _schemeProvider.GetSchemeAsync(scheme);\n        if (authScheme == null ||\n            !(authScheme is DynamicAuthenticationScheme dynamicAuthScheme) ||\n            dynamicAuthScheme?.SsoType != SsoType.Saml2)\n        {\n            return NotFound();\n        }\n\n        if (!(dynamicAuthScheme.Options is Saml2Options options))\n        {\n            return NotFound();\n        }\n\n        var uri = new Uri(\n            Request.Scheme\n            + \"://\"\n            + Request.Host\n            + Request.Path\n            + Request.QueryString);\n\n        var pathBase = Request.PathBase.Value;\n        pathBase = string.IsNullOrEmpty(pathBase) ? \"/\" : pathBase;\n\n        var requestdata = new HttpRequestData(\n            Request.Method,\n            uri,\n            pathBase,\n            null,\n            Request.Cookies,\n            (data) => data);\n\n        var metadataResult = CommandFactory\n            .GetCommand(CommandFactory.MetadataCommand)\n            .Run(requestdata, options);\n        //Response.Headers.Add(\"Content-Disposition\", $\"filename= bitwarden-saml2-meta-{scheme}.xml\");\n        return new ContentResult\n        {\n            Content = metadataResult.Content,\n            ContentType = \"text/xml\",\n        };\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Dockerfile",
    "content": "###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\n# We put the value in a file to be read by later layers.\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/bitwarden_license/src/Sso\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    krb5 \\\n    icu-libs \\\n    shadow \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu\n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/bitwarden_license/src/Sso/out /app\nCOPY ./bitwarden_license/src/Sso/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\n\nHEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "bitwarden_license/src/Sso/IdentityServer/DistributedCachePersistedGrantStore.cs",
    "content": "﻿using Bit.Sso.Utilities;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Stores;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Sso.IdentityServer;\n\n/// <summary>\n/// Distributed cache-backed persisted grant store for short-lived grants.\n/// Uses IFusionCache (which wraps IDistributedCache) for horizontal scaling support,\n/// and fall back to in-memory caching if Redis is not configured.\n/// Designed for SSO authorization codes which are short-lived (5 minutes) and single-use.\n/// </summary>\n/// <remarks>\n/// This is purposefully a different implementation from how Identity solves Persisted Grants.\n/// Because even flavored grant store, e.g., AuthorizationCodeGrantStore, can add intermediary\n/// logic to a grant's handling by type, the fact that they all wrap IdentityServer's IPersistedGrantStore\n/// leans on IdentityServer's opinion that all grants, regardless of type, go to the same persistence\n/// mechanism (cache, database).\n/// <seealso href=\"https://docs.duendesoftware.com/identityserver/reference/stores/persisted-grant-store/\"/>\n/// </remarks>\npublic class DistributedCachePersistedGrantStore : IPersistedGrantStore\n{\n    private readonly IFusionCache _cache;\n\n    public DistributedCachePersistedGrantStore(\n        [FromKeyedServices(PersistedGrantsDistributedCacheConstants.CacheKey)] IFusionCache cache)\n    {\n        _cache = cache;\n    }\n\n    public async Task<PersistedGrant?> GetAsync(string key)\n    {\n        var result = await _cache.TryGetAsync<PersistedGrant>(key);\n\n        if (!result.HasValue)\n        {\n            return null;\n        }\n\n        var grant = result.Value;\n\n        // Check if grant has expired - remove expired grants from cache\n        if (grant.Expiration.HasValue && grant.Expiration.Value < DateTime.UtcNow)\n        {\n            await RemoveAsync(key);\n            return null;\n        }\n\n        return grant;\n    }\n\n    public Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)\n    {\n        // Cache stores are key-value based and don't support querying by filter criteria.\n        // This method is typically used for cleanup operations on long-lived grants in databases.\n        // For SSO's short-lived authorization codes, we rely on TTL expiration instead.\n\n        return Task.FromResult(Enumerable.Empty<PersistedGrant>());\n    }\n\n    public Task RemoveAllAsync(PersistedGrantFilter filter)\n    {\n        // Revocation Strategy: SSO's logout flow (AccountController.LogoutAsync) only clears local\n        // authentication cookies and performs federated logout with external IdPs. It does not invoke\n        // Duende's EndSession or TokenRevocation endpoints. Authorization codes are single-use and expire\n        // within 5 minutes, making explicit revocation unnecessary for SSO's security model.\n        // https://docs.duendesoftware.com/identityserver/reference/stores/persisted-grant-store/\n\n        // Cache stores are key-value based and don't support bulk deletion by filter.\n        // This method is typically used for cleanup operations on long-lived grants in databases.\n        // For SSO's short-lived authorization codes, we rely on TTL expiration instead.\n\n        return Task.FromResult(0);\n    }\n\n    public async Task RemoveAsync(string key)\n    {\n        await _cache.RemoveAsync(key);\n    }\n\n    public async Task StoreAsync(PersistedGrant grant)\n    {\n        // Calculate TTL based on grant expiration\n        var duration = grant.Expiration.HasValue\n            ? grant.Expiration.Value - DateTime.UtcNow\n            : TimeSpan.FromMinutes(5); // Default to 5 minutes if no expiration set\n\n        // Ensure positive duration\n        if (duration <= TimeSpan.Zero)\n        {\n            return;\n        }\n\n        // Cache key \"sso-grants:\" is configured by service registration. Going through the consumed KeyedService will\n        // give us a consistent cache key prefix for these grants.\n        await _cache.SetAsync(\n            grant.Key,\n            grant,\n            new FusionCacheEntryOptions { Duration = duration });\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/IdentityServer/OidcIdentityClient.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Duende.IdentityServer;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Sso.IdentityServer;\n\npublic class OidcIdentityClient : Client\n{\n    public OidcIdentityClient(GlobalSettings globalSettings)\n    {\n        ClientId = \"oidc-identity\";\n        RequireClientSecret = true;\n        RequirePkce = true;\n        ClientSecrets = new List<Secret> { new(globalSettings.OidcIdentityClientKey.Sha256()) };\n        AllowedScopes = new[]\n        {\n            IdentityServerConstants.StandardScopes.OpenId,\n            IdentityServerConstants.StandardScopes.Profile\n        };\n        AllowedGrantTypes = GrantTypes.Code;\n        Enabled = true;\n        RedirectUris = new List<string> { $\"{globalSettings.BaseServiceUri.Identity}/signin-oidc\" };\n        RequireConsent = false;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Models/ErrorViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Sso.Models;\n\npublic class ErrorViewModel\n{\n    private string _requestId;\n\n    public ErrorMessage Error { get; set; }\n    public Exception Exception { get; set; }\n\n    public string Message => Error?.Error;\n    public string Description => Error?.ErrorDescription ?? Exception?.Message;\n    public string RedirectUri => Error?.RedirectUri;\n    public string RequestId\n    {\n        get\n        {\n            return Error?.RequestId ?? _requestId;\n        }\n        set\n        {\n            _requestId = value;\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Models/RedirectViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Sso.Models;\n\npublic class RedirectViewModel\n{\n    public string RedirectUrl { get; set; }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Models/SamlEnvironment.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Cryptography.X509Certificates;\n\nnamespace Bit.Sso.Models;\n\npublic class SamlEnvironment\n{\n    public X509Certificate2 SpSigningCertificate { get; set; }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Models/SsoPreValidateResponseModel.cs",
    "content": "﻿using Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Sso.Models;\n\npublic class SsoPreValidateResponseModel : JsonResult\n{\n    public SsoPreValidateResponseModel(string token) : base(new\n    {\n        token\n    })\n    { }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Sso;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        Host\n            .CreateDefaultBuilder(args)\n            .ConfigureCustomAppConfiguration(args)\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging()\n            .Build()\n            .Run();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:51822\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Sso\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:51822\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Sso-SelfHost\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:51823\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"developSelfHosted\": \"true\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Sass/site.scss",
    "content": "﻿@import \"webfonts.css\";\n\n$primary: #175DDC;\n$primary-accent: #1252A3;\n$success: #00a65a;\n$info: #555555;\n$warning: #bf7e16;\n$danger: #dd4b39;\n\n$theme-colors: ( \"primary-accent\": $primary-accent );\n$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';\n\n$h1-font-size: 2rem;\n$h2-font-size: 1.3rem;\n$h3-font-size: 1rem;\n$h4-font-size: 1rem;\n$h5-font-size: 1rem;\n$h6-font-size: 1rem;\n\n@import \"../node_modules/bootstrap/scss/bootstrap.scss\";\n\nh1 {\n    border-bottom: 1px solid $border-color;\n    margin-bottom: 20px;\n\n    small {\n        color: $text-muted;\n        font-size: $h1-font-size * .5;\n    }\n}\n\nh2 {\n    text-transform: uppercase;\n    font-weight: bold;\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Sass/webfonts.css",
    "content": "@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 300;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-300.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 400;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-400.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 600;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-600.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 700;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-700.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 800;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-800.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 300;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-300.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 400;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-400.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 600;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-600.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 700;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-700.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 800;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-800.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Sso.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-Sso</UserSecretsId>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Sso' \" />\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Sso-SelfHost' \" />\n  <ItemGroup>\n    <!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->\n    <PackageReference Include=\"Microsoft.AspNetCore.Http\" Version=\"2.2.2\" />\n\n    <PackageReference Include=\"Sustainsys.Saml2.AspNetCore2\" Version=\"2.11.0\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\..\\src\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\src\\SharedWeb\\SharedWeb.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Startup.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Context;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.SecretsManager.Repositories.Noop;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.SharedWeb.Utilities;\nusing Bit.Sso.Utilities;\nusing Duende.IdentityServer.Services;\nusing Stripe;\n\nnamespace Bit.Sso;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; }\n    public IWebHostEnvironment Environment { get; set; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n\n        // Stripe Billing\n        StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;\n        StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;\n\n        // Data Protection\n        services.AddCustomDataProtectionServices(Environment, globalSettings);\n\n        // Repositories\n        services.AddDatabaseRepositories(globalSettings);\n        services.AddTestPlayIdTracking(globalSettings);\n\n        // Context\n        services.AddScoped<ICurrentContext, CurrentContext>();\n\n        // Caching\n        services.AddMemoryCache();\n        services.AddDistributedCache(globalSettings);\n\n        // Mvc\n        services.AddControllersWithViews();\n\n        // Cookies\n        if (Environment.IsDevelopment())\n        {\n            services.Configure<CookiePolicyOptions>(options =>\n            {\n                options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;\n                options.OnAppendCookie = ctx =>\n                {\n                    ctx.CookieOptions.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;\n                };\n            });\n        }\n\n        // Authentication\n        services.AddDistributedIdentityServices();\n        services.AddAuthentication()\n            .AddCookie(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);\n        services.AddSsoServices(globalSettings);\n\n        // IdentityServer\n        services.AddSsoIdentityServerServices(Environment, globalSettings);\n\n        // Identity\n        services.AddCustomIdentityServices(globalSettings);\n\n        // Services\n        services.AddBaseServices(globalSettings);\n        services.AddDefaultServices(globalSettings);\n        services.AddCoreLocalizationServices();\n        services.AddBillingOperations();\n\n        // TODO: Remove when OrganizationUser methods are moved out of OrganizationService, this noop dependency should\n        // TODO: no longer be required - see PM-1880\n        services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();\n    }\n\n    public void Configure(\n        IApplicationBuilder app,\n        IWebHostEnvironment environment,\n        IHostApplicationLifetime appLifetime,\n        GlobalSettings globalSettings,\n        ILogger<Startup> logger)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n\n        if (!environment.IsDevelopment())\n        {\n            var uri = new Uri(globalSettings.BaseServiceUri.Sso);\n            app.Use(async (ctx, next) =>\n            {\n                ctx.RequestServices.GetRequiredService<IServerUrls>().Origin = $\"{uri.Scheme}://{uri.Host}\";\n                await next();\n            });\n        }\n\n        if (globalSettings.SelfHosted)\n        {\n            app.UsePathBase(\"/sso\");\n            app.UseForwardedHeaders(globalSettings);\n        }\n\n        if (environment.IsDevelopment())\n        {\n            app.UseDeveloperExceptionPage();\n            app.UseCookiePolicy();\n        }\n        else\n        {\n            app.UseExceptionHandler(\"/Error\");\n        }\n\n        app.UseCoreLocalization();\n\n        // Add static files to the request pipeline.\n        app.UseStaticFiles();\n\n        // Add routing\n        app.UseRouting();\n\n        // Add Cors\n        app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))\n            .AllowAnyMethod().AllowAnyHeader().AllowCredentials());\n\n        // Add current context\n        app.UseMiddleware<CurrentContextMiddleware>();\n\n        // Add IdentityServer to the request pipeline.\n        app.UseIdentityServer(new IdentityServerMiddlewareOptions\n        {\n            AuthenticationMiddleware = app => app.UseMiddleware<SsoAuthenticationMiddleware>()\n        });\n\n        // Add Mvc stuff\n        app.UseAuthorization();\n        app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());\n\n        // Log startup\n        logger.LogInformation(Constants.BypassFiltersEventId, \"{Project} started.\", globalSettings.ProjectName);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/ClaimsExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing System.Text.RegularExpressions;\n\nnamespace Bit.Sso.Utilities;\n\npublic static class ClaimsExtensions\n{\n    private static readonly Regex _normalizeTextRegEx =\n        new Regex(@\"[^a-zA-Z]\", RegexOptions.CultureInvariant | RegexOptions.Singleline);\n\n    public static string GetFirstMatch(this IEnumerable<Claim> claims, params string[] possibleNames)\n    {\n        var normalizedClaims = claims.Select(c => (Normalize(c.Type), c.Value)).ToList();\n\n        // Order of precedence is by passed in names\n        foreach (var name in possibleNames.Select(Normalize))\n        {\n            // Second by order of claims (find claim by name)\n            foreach (var claim in normalizedClaims)\n            {\n                if (Equals(claim.Item1, name))\n                {\n                    return claim.Value;\n                }\n            }\n        }\n        return null;\n    }\n\n    private static bool Equals(string text, string compare)\n    {\n        return text == compare ||\n            (string.IsNullOrWhiteSpace(text) && string.IsNullOrWhiteSpace(compare)) ||\n            string.Equals(Normalize(text), compare, StringComparison.InvariantCultureIgnoreCase);\n    }\n\n    private static string Normalize(string text)\n    {\n        if (string.IsNullOrWhiteSpace(text))\n        {\n            return text;\n        }\n        return _normalizeTextRegEx.Replace(text, string.Empty);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/DiscoveryResponseGenerator.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Duende.IdentityServer.Configuration;\nusing Duende.IdentityServer.Services;\nusing Duende.IdentityServer.Stores;\nusing Duende.IdentityServer.Validation;\nusing DIR = Duende.IdentityServer.ResponseHandling;\n\nnamespace Bit.Sso.Utilities;\n\npublic class DiscoveryResponseGenerator : DIR.DiscoveryResponseGenerator\n{\n    private readonly GlobalSettings _globalSettings;\n\n    public DiscoveryResponseGenerator(\n        IdentityServerOptions options,\n        IResourceStore resourceStore,\n        IKeyMaterialService keys,\n        ExtensionGrantValidator extensionGrants,\n        ISecretsListParser secretParsers,\n        IResourceOwnerPasswordValidator resourceOwnerValidator,\n        ILogger<DiscoveryResponseGenerator> logger,\n        GlobalSettings globalSettings)\n        : base(options, resourceStore, keys, extensionGrants, secretParsers, resourceOwnerValidator, logger)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public override async Task<Dictionary<string, object>> CreateDiscoveryDocumentAsync(\n        string baseUrl, string issuerUri)\n    {\n        var dict = await base.CreateDiscoveryDocumentAsync(baseUrl, issuerUri);\n        return CoreHelpers.AdjustIdentityServerConfig(dict, _globalSettings.BaseServiceUri.Sso,\n            _globalSettings.BaseServiceUri.InternalSso);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/DynamicAuthenticationScheme.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Sustainsys.Saml2.AspNetCore2;\n\nnamespace Bit.Sso.Utilities;\n\npublic class DynamicAuthenticationScheme : AuthenticationScheme, IDynamicAuthenticationScheme\n{\n    public DynamicAuthenticationScheme(string name, string displayName, Type handlerType,\n        AuthenticationSchemeOptions options)\n        : base(name, displayName, handlerType)\n    {\n        Options = options;\n    }\n    public DynamicAuthenticationScheme(string name, string displayName, Type handlerType,\n        AuthenticationSchemeOptions options, SsoType ssoType)\n        : this(name, displayName, handlerType, options)\n    {\n        SsoType = ssoType;\n    }\n\n    public AuthenticationSchemeOptions Options { get; set; }\n    public SsoType SsoType { get; set; }\n\n    public async Task Validate()\n    {\n        switch (SsoType)\n        {\n            case SsoType.OpenIdConnect:\n                await ValidateOpenIdConnectAsync();\n                break;\n            case SsoType.Saml2:\n                ValidateSaml();\n                break;\n            default:\n                break;\n        }\n    }\n\n    private void ValidateSaml()\n    {\n        if (SsoType != SsoType.Saml2)\n        {\n            return;\n        }\n        if (!(Options is Saml2Options samlOptions))\n        {\n            throw new Exception(\"InvalidAuthenticationOptionsForSaml2SchemeError\");\n        }\n        samlOptions.Validate(Name);\n    }\n\n    private async Task ValidateOpenIdConnectAsync()\n    {\n        if (SsoType != SsoType.OpenIdConnect)\n        {\n            return;\n        }\n        if (!(Options is OpenIdConnectOptions oidcOptions))\n        {\n            throw new Exception(\"InvalidAuthenticationOptionsForOidcSchemeError\");\n        }\n        oidcOptions.Validate();\n        if (oidcOptions.Configuration == null)\n        {\n            if (oidcOptions.ConfigurationManager == null)\n            {\n                throw new Exception(\"PostConfigurationNotExecutedError\");\n            }\n            if (oidcOptions.Configuration == null)\n            {\n                try\n                {\n                    oidcOptions.Configuration = await oidcOptions.ConfigurationManager\n                        .GetConfigurationAsync(CancellationToken.None);\n                }\n                catch (Exception ex)\n                {\n                    throw new Exception(\"ReadingOpenIdConnectMetadataFailedError\", ex);\n                }\n            }\n        }\n        if (oidcOptions.Configuration == null)\n        {\n            throw new Exception(\"NoOpenIdConnectMetadataError\");\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Cryptography.X509Certificates;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Sso.Models;\nusing Bit.Sso.Utilities;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer;\nusing Duende.IdentityServer.Infrastructure;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Microsoft.Extensions.Options;\nusing Microsoft.IdentityModel.Tokens;\nusing Sustainsys.Saml2.AspNetCore2;\nusing Sustainsys.Saml2.Configuration;\nusing Sustainsys.Saml2.Saml2P;\n\nnamespace Bit.Core.Business.Sso;\n\npublic class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider\n{\n    private readonly IPostConfigureOptions<OpenIdConnectOptions> _oidcPostConfigureOptions;\n    private readonly IExtendedOptionsMonitorCache<OpenIdConnectOptions> _extendedOidcOptionsMonitorCache;\n    private readonly IPostConfigureOptions<Saml2Options> _saml2PostConfigureOptions;\n    private readonly IExtendedOptionsMonitorCache<Saml2Options> _extendedSaml2OptionsMonitorCache;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly ILogger _logger;\n    private readonly GlobalSettings _globalSettings;\n    private readonly SamlEnvironment _samlEnvironment;\n    private readonly TimeSpan _schemeCacheLifetime;\n    private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedSchemes;\n    private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedHandlerSchemes;\n    private readonly SemaphoreSlim _semaphore;\n    private readonly IServiceProvider _serviceProvider;\n\n    private DateTime? _lastSchemeLoad;\n    private IEnumerable<DynamicAuthenticationScheme> _schemesCopy = Array.Empty<DynamicAuthenticationScheme>();\n    private IEnumerable<DynamicAuthenticationScheme> _handlerSchemesCopy = Array.Empty<DynamicAuthenticationScheme>();\n\n    public DynamicAuthenticationSchemeProvider(\n        IOptions<AuthenticationOptions> options,\n        IPostConfigureOptions<OpenIdConnectOptions> oidcPostConfigureOptions,\n        IOptionsMonitorCache<OpenIdConnectOptions> oidcOptionsMonitorCache,\n        IPostConfigureOptions<Saml2Options> saml2PostConfigureOptions,\n        IOptionsMonitorCache<Saml2Options> saml2OptionsMonitorCache,\n        ISsoConfigRepository ssoConfigRepository,\n        ILogger<DynamicAuthenticationSchemeProvider> logger,\n        GlobalSettings globalSettings,\n        SamlEnvironment samlEnvironment,\n        IServiceProvider serviceProvider)\n        : base(options)\n    {\n        _oidcPostConfigureOptions = oidcPostConfigureOptions;\n        _extendedOidcOptionsMonitorCache = oidcOptionsMonitorCache as\n            IExtendedOptionsMonitorCache<OpenIdConnectOptions>;\n        if (_extendedOidcOptionsMonitorCache == null)\n        {\n            throw new ArgumentNullException(\"_extendedOidcOptionsMonitorCache could not be resolved.\");\n        }\n\n        _saml2PostConfigureOptions = saml2PostConfigureOptions;\n        _extendedSaml2OptionsMonitorCache = saml2OptionsMonitorCache as\n            IExtendedOptionsMonitorCache<Saml2Options>;\n        if (_extendedSaml2OptionsMonitorCache == null)\n        {\n            throw new ArgumentNullException(\"_extendedSaml2OptionsMonitorCache could not be resolved.\");\n        }\n\n        _ssoConfigRepository = ssoConfigRepository;\n        _logger = logger;\n        _globalSettings = globalSettings;\n        _schemeCacheLifetime = TimeSpan.FromSeconds(_globalSettings.Sso?.CacheLifetimeInSeconds ?? 30);\n        _samlEnvironment = samlEnvironment;\n        _cachedSchemes = new Dictionary<string, DynamicAuthenticationScheme>();\n        _cachedHandlerSchemes = new Dictionary<string, DynamicAuthenticationScheme>();\n        _semaphore = new SemaphoreSlim(1);\n        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));\n    }\n\n    private bool CacheIsValid\n    {\n        get => _lastSchemeLoad.HasValue\n            && _lastSchemeLoad.Value.Add(_schemeCacheLifetime) >= DateTime.UtcNow;\n    }\n\n    public override async Task<AuthenticationScheme> GetSchemeAsync(string name)\n    {\n        var scheme = await base.GetSchemeAsync(name);\n        if (scheme != null)\n        {\n            return scheme;\n        }\n\n        try\n        {\n            var dynamicScheme = await GetDynamicSchemeAsync(name);\n            return dynamicScheme;\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Unable to load a dynamic authentication scheme for '{0}'\", name);\n        }\n\n        return null;\n    }\n\n    public override async Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()\n    {\n        var existingSchemes = await base.GetAllSchemesAsync();\n        var schemes = new List<AuthenticationScheme>();\n        schemes.AddRange(existingSchemes);\n\n        await LoadAllDynamicSchemesIntoCacheAsync();\n        schemes.AddRange(_schemesCopy);\n\n        return schemes.ToArray();\n    }\n\n    public override async Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()\n    {\n        var existingSchemes = await base.GetRequestHandlerSchemesAsync();\n        var schemes = new List<AuthenticationScheme>();\n        schemes.AddRange(existingSchemes);\n\n        await LoadAllDynamicSchemesIntoCacheAsync();\n        schemes.AddRange(_handlerSchemesCopy);\n\n        return schemes.ToArray();\n    }\n\n    private async Task LoadAllDynamicSchemesIntoCacheAsync()\n    {\n        if (CacheIsValid)\n        {\n            // Our cache hasn't expired or been invalidated, ignore request\n            return;\n        }\n        await _semaphore.WaitAsync();\n        try\n        {\n            if (CacheIsValid)\n            {\n                // Just in case (double-checked locking pattern)\n                return;\n            }\n\n            // Save time just in case the following operation takes longer\n            var now = DateTime.UtcNow;\n            var newSchemes = await _ssoConfigRepository.GetManyByRevisionNotBeforeDate(_lastSchemeLoad);\n\n            foreach (var config in newSchemes)\n            {\n                DynamicAuthenticationScheme scheme;\n                try\n                {\n                    scheme = GetSchemeFromSsoConfig(config);\n                }\n                catch (Exception ex)\n                {\n                    _logger.LogError(ex, \"Error converting configuration to scheme for '{0}'\", config.Id);\n                    continue;\n                }\n                if (scheme == null)\n                {\n                    continue;\n                }\n                SetSchemeInCache(scheme);\n            }\n\n            if (newSchemes.Any())\n            {\n                // Maintain \"safe\" copy for use in enumeration routines\n                _schemesCopy = _cachedSchemes.Values.ToArray();\n                _handlerSchemesCopy = _cachedHandlerSchemes.Values.ToArray();\n            }\n            _lastSchemeLoad = now;\n        }\n        finally\n        {\n            _semaphore.Release();\n        }\n    }\n\n    private DynamicAuthenticationScheme SetSchemeInCache(DynamicAuthenticationScheme scheme)\n    {\n        if (!PostConfigureDynamicScheme(scheme))\n        {\n            return null;\n        }\n        _cachedSchemes[scheme.Name] = scheme;\n        if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))\n        {\n            _cachedHandlerSchemes[scheme.Name] = scheme;\n        }\n        return scheme;\n    }\n\n    private async Task<DynamicAuthenticationScheme> GetDynamicSchemeAsync(string name)\n    {\n        if (_cachedSchemes.TryGetValue(name, out var cachedScheme))\n        {\n            return cachedScheme;\n        }\n\n        var scheme = await GetSchemeFromSsoConfigAsync(name);\n        if (scheme == null)\n        {\n            return null;\n        }\n\n        await _semaphore.WaitAsync();\n        try\n        {\n            scheme = SetSchemeInCache(scheme);\n            if (scheme == null)\n            {\n                return null;\n            }\n\n            if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))\n            {\n                _handlerSchemesCopy = _cachedHandlerSchemes.Values.ToArray();\n            }\n            _schemesCopy = _cachedSchemes.Values.ToArray();\n        }\n        finally\n        {\n            // Note: _lastSchemeLoad is not set here, this is a one-off\n            //  and should not impact loading further cache updates\n            _semaphore.Release();\n        }\n        return scheme;\n    }\n\n    private bool PostConfigureDynamicScheme(DynamicAuthenticationScheme scheme)\n    {\n        try\n        {\n            if (scheme.SsoType == SsoType.OpenIdConnect && scheme.Options is OpenIdConnectOptions oidcOptions)\n            {\n                _oidcPostConfigureOptions.PostConfigure(scheme.Name, oidcOptions);\n                _extendedOidcOptionsMonitorCache.AddOrUpdate(scheme.Name, oidcOptions);\n            }\n            else if (scheme.SsoType == SsoType.Saml2 && scheme.Options is Saml2Options saml2Options)\n            {\n                _saml2PostConfigureOptions.PostConfigure(scheme.Name, saml2Options);\n                _extendedSaml2OptionsMonitorCache.AddOrUpdate(scheme.Name, saml2Options);\n            }\n            return true;\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Error performing post configuration for '{0}' ({1})\",\n                scheme.Name, scheme.DisplayName);\n        }\n        return false;\n    }\n\n    private DynamicAuthenticationScheme GetSchemeFromSsoConfig(SsoConfig config)\n    {\n        var data = config.GetData();\n        return data.ConfigType switch\n        {\n            SsoType.OpenIdConnect => GetOidcAuthenticationScheme(config.OrganizationId.ToString(), data),\n            SsoType.Saml2 => GetSaml2AuthenticationScheme(config.OrganizationId.ToString(), data),\n            _ => throw new Exception($\"SSO Config Type, '{data.ConfigType}', not supported\"),\n        };\n    }\n\n    private async Task<DynamicAuthenticationScheme> GetSchemeFromSsoConfigAsync(string name)\n    {\n        if (!Guid.TryParse(name, out var organizationId))\n        {\n            _logger.LogWarning(\"Could not determine organization id from name, '{0}'\", name);\n            return null;\n        }\n        var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);\n        if (ssoConfig == null || !ssoConfig.Enabled)\n        {\n            _logger.LogWarning(\"Could not find SSO config or config was not enabled for '{0}'\", name);\n            return null;\n        }\n\n        return GetSchemeFromSsoConfig(ssoConfig);\n    }\n\n    private DynamicAuthenticationScheme GetOidcAuthenticationScheme(string name, SsoConfigurationData config)\n    {\n        var oidcOptions = new OpenIdConnectOptions\n        {\n            Authority = config.Authority,\n            ClientId = config.ClientId,\n            ClientSecret = config.ClientSecret,\n            ResponseType = \"code\",\n            ResponseMode = \"form_post\",\n            SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,\n            SignOutScheme = IdentityServerConstants.SignoutScheme,\n            SaveTokens = false, // reduce overall request size\n            TokenValidationParameters = new TokenValidationParameters\n            {\n                NameClaimType = JwtClaimTypes.Name,\n                RoleClaimType = JwtClaimTypes.Role,\n            },\n            CallbackPath = SsoConfigurationData.BuildCallbackPath(),\n            SignedOutCallbackPath = SsoConfigurationData.BuildSignedOutCallbackPath(),\n            MetadataAddress = config.MetadataAddress,\n            // Prevents URLs that go beyond 1024 characters which may break for some servers\n            AuthenticationMethod = config.RedirectBehavior,\n            GetClaimsFromUserInfoEndpoint = config.GetClaimsFromUserInfoEndpoint,\n        };\n        oidcOptions.Scope\n            .AddIfNotExists(OpenIdConnectScopes.OpenId)\n            .AddIfNotExists(OpenIdConnectScopes.Email)\n            .AddIfNotExists(OpenIdConnectScopes.Profile);\n        foreach (var scope in config.GetAdditionalScopes())\n        {\n            oidcOptions.Scope.AddIfNotExists(scope);\n        }\n        if (!string.IsNullOrWhiteSpace(config.ExpectedReturnAcrValue))\n        {\n            oidcOptions.Scope.AddIfNotExists(OpenIdConnectScopes.Acr);\n        }\n\n        oidcOptions.StateDataFormat = new DistributedCacheStateDataFormatter(_serviceProvider, name);\n\n        // see: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest (acr_values)\n        if (!string.IsNullOrWhiteSpace(config.AcrValues))\n        {\n            oidcOptions.Events ??= new OpenIdConnectEvents();\n            oidcOptions.Events.OnRedirectToIdentityProvider = ctx =>\n            {\n                ctx.ProtocolMessage.AcrValues = config.AcrValues;\n                return Task.CompletedTask;\n            };\n        }\n\n        return new DynamicAuthenticationScheme(name, name, typeof(OpenIdConnectHandler),\n            oidcOptions, SsoType.OpenIdConnect);\n    }\n\n    private DynamicAuthenticationScheme GetSaml2AuthenticationScheme(string name, SsoConfigurationData config)\n    {\n        if (_samlEnvironment == null)\n        {\n            throw new Exception($\"SSO SAML2 Service Provider profile is missing for {name}\");\n        }\n\n        var spEntityId = new Sustainsys.Saml2.Metadata.EntityId(\n            SsoConfigurationData.BuildSaml2ModulePath(\n                _globalSettings.BaseServiceUri.Sso,\n                config.SpUniqueEntityId ? name : null));\n        bool? allowCreate = null;\n        if (config.SpNameIdFormat != Saml2NameIdFormat.Transient)\n        {\n            allowCreate = true;\n        }\n        var spOptions = new SPOptions\n        {\n            EntityId = spEntityId,\n            ModulePath = SsoConfigurationData.BuildSaml2ModulePath(null, name),\n            NameIdPolicy = new Saml2NameIdPolicy(allowCreate, GetNameIdFormat(config.SpNameIdFormat)),\n            WantAssertionsSigned = config.SpWantAssertionsSigned,\n            AuthenticateRequestSigningBehavior = GetSigningBehavior(config.SpSigningBehavior),\n            ValidateCertificates = config.SpValidateCertificates,\n        };\n        if (!string.IsNullOrWhiteSpace(config.SpMinIncomingSigningAlgorithm))\n        {\n            spOptions.MinIncomingSigningAlgorithm = config.SpMinIncomingSigningAlgorithm;\n        }\n        if (!string.IsNullOrWhiteSpace(config.SpOutboundSigningAlgorithm))\n        {\n            spOptions.OutboundSigningAlgorithm = config.SpOutboundSigningAlgorithm;\n        }\n        if (_samlEnvironment.SpSigningCertificate != null)\n        {\n            spOptions.ServiceCertificates.Add(_samlEnvironment.SpSigningCertificate);\n        }\n\n        var idpEntityId = new Sustainsys.Saml2.Metadata.EntityId(config.IdpEntityId);\n        var idp = new Sustainsys.Saml2.IdentityProvider(idpEntityId, spOptions)\n        {\n            Binding = GetBindingType(config.IdpBindingType),\n            AllowUnsolicitedAuthnResponse = config.IdpAllowUnsolicitedAuthnResponse,\n            DisableOutboundLogoutRequests = config.IdpDisableOutboundLogoutRequests,\n            WantAuthnRequestsSigned = config.IdpWantAuthnRequestsSigned,\n        };\n        if (!string.IsNullOrWhiteSpace(config.IdpSingleSignOnServiceUrl))\n        {\n            idp.SingleSignOnServiceUrl = new Uri(config.IdpSingleSignOnServiceUrl);\n        }\n        if (!string.IsNullOrWhiteSpace(config.IdpSingleLogoutServiceUrl))\n        {\n            idp.SingleLogoutServiceUrl = new Uri(config.IdpSingleLogoutServiceUrl);\n        }\n        if (!string.IsNullOrWhiteSpace(config.IdpOutboundSigningAlgorithm))\n        {\n            idp.OutboundSigningAlgorithm = config.IdpOutboundSigningAlgorithm;\n        }\n        if (!string.IsNullOrWhiteSpace(config.IdpX509PublicCert))\n        {\n            var cert = CoreHelpers.Base64UrlDecode(config.IdpX509PublicCert);\n            idp.SigningKeys.AddConfiguredKey(new X509Certificate2(cert));\n        }\n        idp.ArtifactResolutionServiceUrls.Clear();\n        // This must happen last since it calls Validate() internally.\n        idp.LoadMetadata = false;\n\n        var options = new Saml2Options\n        {\n            SPOptions = spOptions,\n            SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,\n            SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme,\n            CookieManager = new DistributedCacheCookieManager(),\n        };\n        options.IdentityProviders.Add(idp);\n\n        return new DynamicAuthenticationScheme(name, name, typeof(Saml2Handler), options, SsoType.Saml2);\n    }\n\n    private NameIdFormat GetNameIdFormat(Saml2NameIdFormat format)\n    {\n        return format switch\n        {\n            Saml2NameIdFormat.Unspecified => NameIdFormat.Unspecified,\n            Saml2NameIdFormat.EmailAddress => NameIdFormat.EmailAddress,\n            Saml2NameIdFormat.X509SubjectName => NameIdFormat.X509SubjectName,\n            Saml2NameIdFormat.WindowsDomainQualifiedName => NameIdFormat.WindowsDomainQualifiedName,\n            Saml2NameIdFormat.KerberosPrincipalName => NameIdFormat.KerberosPrincipalName,\n            Saml2NameIdFormat.EntityIdentifier => NameIdFormat.EntityIdentifier,\n            Saml2NameIdFormat.Persistent => NameIdFormat.Persistent,\n            Saml2NameIdFormat.Transient => NameIdFormat.Transient,\n            _ => NameIdFormat.NotConfigured,\n        };\n    }\n\n    private SigningBehavior GetSigningBehavior(Saml2SigningBehavior behavior)\n    {\n        return behavior switch\n        {\n            Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned => SigningBehavior.IfIdpWantAuthnRequestsSigned,\n            Saml2SigningBehavior.Always => SigningBehavior.Always,\n            Saml2SigningBehavior.Never => SigningBehavior.Never,\n            _ => SigningBehavior.IfIdpWantAuthnRequestsSigned,\n        };\n    }\n\n    private Sustainsys.Saml2.WebSso.Saml2BindingType GetBindingType(Saml2BindingType bindingType)\n    {\n        return bindingType switch\n        {\n            Saml2BindingType.HttpRedirect => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpRedirect,\n            Saml2BindingType.HttpPost => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpPost,\n            _ => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpPost,\n        };\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/ExtendedOptionsMonitorCache.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Collections.Concurrent;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Sso.Utilities;\n\npublic class ExtendedOptionsMonitorCache<TOptions> : IExtendedOptionsMonitorCache<TOptions> where TOptions : class\n{\n    private readonly ConcurrentDictionary<string, Lazy<TOptions>> _cache =\n        new ConcurrentDictionary<string, Lazy<TOptions>>(StringComparer.Ordinal);\n\n    public void AddOrUpdate(string name, TOptions options)\n    {\n        _cache.AddOrUpdate(name ?? Options.DefaultName, new Lazy<TOptions>(() => options),\n            (string s, Lazy<TOptions> lazy) => new Lazy<TOptions>(() => options));\n    }\n\n    public void Clear()\n    {\n        _cache.Clear();\n    }\n\n    public TOptions GetOrAdd(string name, Func<TOptions> createOptions)\n    {\n        return _cache.GetOrAdd(name ?? Options.DefaultName, new Lazy<TOptions>(createOptions)).Value;\n    }\n\n    public bool TryAdd(string name, TOptions options)\n    {\n        return _cache.TryAdd(name ?? Options.DefaultName, new Lazy<TOptions>(() => options));\n    }\n\n    public bool TryRemove(string name)\n    {\n        return _cache.TryRemove(name ?? Options.DefaultName, out _);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/IDynamicAuthenticationScheme.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Microsoft.AspNetCore.Authentication;\n\nnamespace Bit.Sso.Utilities;\n\npublic interface IDynamicAuthenticationScheme\n{\n    AuthenticationSchemeOptions Options { get; set; }\n    SsoType SsoType { get; set; }\n\n    Task Validate();\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/IExtendedOptionsMonitorCache.cs",
    "content": "﻿using Microsoft.Extensions.Options;\n\nnamespace Bit.Sso.Utilities;\n\npublic interface IExtendedOptionsMonitorCache<TOptions> : IOptionsMonitorCache<TOptions> where TOptions : class\n{\n    void AddOrUpdate(string name, TOptions options);\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Microsoft.IdentityModel.Protocols.OpenIdConnect;\n\nnamespace Bit.Sso.Utilities;\n\npublic static class OpenIdConnectOptionsExtensions\n{\n    public static async Task<bool> CouldHandleAsync(this OpenIdConnectOptions options, string scheme, HttpContext context)\n    {\n        // Determine this is a valid request for our handler\n        if (options.CallbackPath != context.Request.Path &&\n            options.RemoteSignOutPath != context.Request.Path &&\n            options.SignedOutCallbackPath != context.Request.Path)\n        {\n            return false;\n        }\n\n        if (context.Request.Query[\"scheme\"].FirstOrDefault() == scheme)\n        {\n            return true;\n        }\n\n        try\n        {\n            // Parse out the message\n            OpenIdConnectMessage message = null;\n            if (string.Equals(context.Request.Method, \"GET\", StringComparison.OrdinalIgnoreCase))\n            {\n                message = new OpenIdConnectMessage(context.Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));\n            }\n            else if (string.Equals(context.Request.Method, \"POST\", StringComparison.OrdinalIgnoreCase) &&\n                !string.IsNullOrEmpty(context.Request.ContentType) &&\n                context.Request.ContentType.StartsWith(\"application/x-www-form-urlencoded\", StringComparison.OrdinalIgnoreCase) &&\n                context.Request.Body.CanRead)\n            {\n                var form = await context.Request.ReadFormAsync();\n                message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));\n            }\n\n            var state = message?.State;\n            if (string.IsNullOrWhiteSpace(state))\n            {\n                // State is required, it will fail later on for this reason.\n                return false;\n            }\n\n            // Handle State if we've gotten that back\n            var decodedState = options.StateDataFormat.Unprotect(state);\n            if (decodedState != null && decodedState.Items.TryGetValue(\"scheme\", out var stateScheme))\n            {\n                return stateScheme == scheme;\n            }\n        }\n        catch\n        {\n            return false;\n        }\n\n        // This is likely not an appropriate handler\n        return false;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/OpenIdConnectScopes.cs",
    "content": "﻿namespace Bit.Sso.Utilities;\n\n/// <summary>\n/// OpenID Connect Clients use scope values as defined in 3.3 of OAuth 2.0\n/// [RFC6749]. These values represent the standard scope values supported\n/// by OAuth 2.0 and therefore OIDC.\n/// </summary>\n/// <remarks>\n/// See: https://openid.net/specs/openid-connect-basic-1_0.html#Scopes\n/// </remarks>\npublic static class OpenIdConnectScopes\n{\n    /// <summary>\n    /// REQUIRED. Informs the Authorization Server that the Client is making\n    /// an OpenID Connect request. If the openid scope value is not present,\n    /// the behavior is entirely unspecified.\n    /// </summary>\n    public const string OpenId = \"openid\";\n\n    /// <summary>\n    /// OPTIONAL. This scope value requests access to the End-User's default\n    /// profile Claims, which are: name, family_name, given_name,\n    /// middle_name, nickname, preferred_username, profile, picture,\n    /// website, gender, birthdate, zoneinfo, locale, and updated_at.\n    /// </summary>\n    public const string Profile = \"profile\";\n\n    /// <summary>\n    /// OPTIONAL. This scope value requests access to the email and\n    /// email_verified Claims.\n    /// </summary>\n    public const string Email = \"email\";\n\n    /// <summary>\n    /// OPTIONAL. This scope value requests access to the address Claim.\n    /// </summary>\n    public const string Address = \"address\";\n\n    /// <summary>\n    /// OPTIONAL. This scope value requests access to the phone_number and\n    /// phone_number_verified Claims.\n    /// </summary>\n    public const string Phone = \"phone\";\n\n    /// <summary>\n    /// OPTIONAL. This scope value requests that an OAuth 2.0 Refresh Token\n    /// be issued that can be used to obtain an Access Token that grants\n    /// access to the End-User's UserInfo Endpoint even when the End-User is\n    /// not present (not logged in).\n    /// </summary>\n    public const string OfflineAccess = \"offline_access\";\n\n    /// <summary>\n    /// OPTIONAL. Authentication Context Class Reference. String specifying\n    /// an Authentication Context Class Reference value that identifies the\n    /// Authentication Context Class that the authentication performed\n    /// satisfied.\n    /// </summary>\n    /// <remarks>\n    /// See: https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2\n    /// </remarks>\n    public const string Acr = \"acr\";\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/PersistedGrantsDistributedCacheConstants.cs",
    "content": "﻿namespace Bit.Sso.Utilities;\n\npublic static class PersistedGrantsDistributedCacheConstants\n{\n    /// <summary>\n    /// The SSO Persisted Grant cache key. Identifies the keyed service consumed by the SSO Persisted Grant Store as\n    /// well as the cache key/namespace for grant storage.\n    /// </summary>\n    public const string CacheKey = \"sso-grants\";\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/Saml2OptionsExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.IO.Compression;\nusing System.Text;\nusing System.Xml;\nusing Sustainsys.Saml2;\nusing Sustainsys.Saml2.AspNetCore2;\n\nnamespace Bit.Sso.Utilities;\n\npublic static class Saml2OptionsExtensions\n{\n    public static async Task<bool> CouldHandleAsync(this Saml2Options options, string scheme, HttpContext context)\n    {\n        // Determine this is a valid request for our handler\n        if (!context.Request.Path.StartsWithSegments(options.SPOptions.ModulePath, StringComparison.Ordinal))\n        {\n            return false;\n        }\n\n        var idp = options.IdentityProviders.IsEmpty ? null : options.IdentityProviders.Default;\n        if (idp == null)\n        {\n            return false;\n        }\n\n        if (context.Request.Query[\"scheme\"].FirstOrDefault() == scheme)\n        {\n            return true;\n        }\n\n        // We need to pull out and parse the response or request SAML envelope\n        XmlElement envelope = null;\n        try\n        {\n            if (string.Equals(context.Request.Method, \"POST\", StringComparison.OrdinalIgnoreCase) &&\n                context.Request.HasFormContentType)\n            {\n                string encodedMessage;\n                if (context.Request.Form.TryGetValue(\"SAMLResponse\", out var response))\n                {\n                    encodedMessage = response.FirstOrDefault();\n                }\n                else\n                {\n                    encodedMessage = context.Request.Form[\"SAMLRequest\"];\n                }\n                if (string.IsNullOrWhiteSpace(encodedMessage))\n                {\n                    return false;\n                }\n                envelope = XmlHelpers.XmlDocumentFromString(\n                    Encoding.UTF8.GetString(Convert.FromBase64String(encodedMessage)))?.DocumentElement;\n            }\n            else if (string.Equals(context.Request.Method, \"GET\", StringComparison.OrdinalIgnoreCase))\n            {\n                var encodedPayload = context.Request.Query[\"SAMLRequest\"].FirstOrDefault() ??\n                    context.Request.Query[\"SAMLResponse\"].FirstOrDefault();\n                try\n                {\n                    var payload = Convert.FromBase64String(encodedPayload);\n                    using var compressed = new MemoryStream(payload);\n                    using var decompressedStream = new DeflateStream(compressed, CompressionMode.Decompress, true);\n                    using var deCompressed = new MemoryStream();\n                    await decompressedStream.CopyToAsync(deCompressed);\n\n                    envelope = XmlHelpers.XmlDocumentFromString(\n                        Encoding.UTF8.GetString(deCompressed.GetBuffer(), 0, (int)deCompressed.Length))?.DocumentElement;\n                }\n                catch (FormatException ex)\n                {\n                    throw new FormatException($\"\\'{encodedPayload}\\' is not a valid Base64 encoded string: {ex.Message}\", ex);\n                }\n            }\n        }\n        catch\n        {\n            return false;\n        }\n\n        if (envelope == null)\n        {\n            return false;\n        }\n\n        // Double check the entity Ids\n        var entityId = envelope[\"Issuer\", Saml2Namespaces.Saml2Name]?.InnerText.Trim();\n        if (!string.Equals(entityId, idp.EntityId.Id, StringComparison.InvariantCultureIgnoreCase))\n        {\n            return false;\n        }\n\n        if (options.SPOptions.WantAssertionsSigned)\n        {\n            var assertion = envelope[\"Assertion\", Saml2Namespaces.Saml2Name];\n            var isAssertionSigned = assertion != null && XmlHelpers.IsSignedByAny(assertion, idp.SigningKeys,\n                options.SPOptions.ValidateCertificates, options.SPOptions.MinIncomingSigningAlgorithm);\n            if (!isAssertionSigned)\n            {\n                throw new Exception(\"Cannot verify SAML assertion signature.\");\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/SamlClaimTypes.cs",
    "content": "﻿namespace Bit.Sso.Utilities;\n\npublic static class SamlClaimTypes\n{\n    public const string Email = \"urn:oid:0.9.2342.19200300.100.1.3\";\n    public const string GivenName = \"urn:oid:2.5.4.42\";\n    public const string Surname = \"urn:oid:2.5.4.4\";\n    public const string DisplayName = \"urn:oid:2.16.840.1.113730.3.1.241\";\n    public const string CommonName = \"urn:oid:2.5.4.3\";\n    public const string UserId = \"urn:oid:0.9.2342.19200300.100.1.1\";\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/SamlNameIdFormats.cs",
    "content": "﻿namespace Bit.Sso.Utilities;\n\npublic static class SamlNameIdFormats\n{\n    // Common\n    public const string Unspecified = \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\";\n    public const string Email = \"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\";\n    public const string Persistent = \"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\";\n    public const string Transient = \"urn:oasis:names:tc:SAML:2.0:nameid-format:transient\";\n    // Not-so-common\n    public const string Upn = \"http://schemas.xmlsoap.org/claims/UPN\";\n    public const string CommonName = \"http://schemas.xmlsoap.org/claims/CommonName\";\n    public const string X509SubjectName = \"urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName\";\n    public const string WindowsQualifiedDomainName = \"urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName\";\n    public const string KerberosPrincipalName = \"urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos\";\n    public const string EntityIdentifier = \"urn:oasis:names:tc:SAML:2.0:nameid-format:entity\";\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/SamlPropertyKeys.cs",
    "content": "﻿namespace Bit.Sso.Utilities;\n\npublic static class SamlPropertyKeys\n{\n    public const string ClaimFormat = \"http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format\";\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Business.Sso;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.SharedWeb.Utilities;\nusing Bit.Sso.IdentityServer;\nusing Bit.Sso.Models;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.ResponseHandling;\nusing Duende.IdentityServer.Stores;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Sustainsys.Saml2.AspNetCore2;\n\nnamespace Bit.Sso.Utilities;\n\npublic static class ServiceCollectionExtensions\n{\n    public static IServiceCollection AddSsoServices(this IServiceCollection services,\n        GlobalSettings globalSettings)\n    {\n        // SAML SP Configuration\n        var samlEnvironment = new SamlEnvironment\n        {\n            SpSigningCertificate = CoreHelpers.GetIdentityServerCertificate(globalSettings),\n        };\n        services.AddSingleton(s => samlEnvironment);\n\n        services.AddSingleton<Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider,\n            DynamicAuthenticationSchemeProvider>();\n        // Oidc\n        services.AddSingleton<Microsoft.Extensions.Options.IPostConfigureOptions<OpenIdConnectOptions>,\n            OpenIdConnectPostConfigureOptions>();\n        services.AddSingleton<Microsoft.Extensions.Options.IOptionsMonitorCache<OpenIdConnectOptions>,\n            ExtendedOptionsMonitorCache<OpenIdConnectOptions>>();\n        // Saml2\n        services.AddSingleton<Microsoft.Extensions.Options.IPostConfigureOptions<Saml2Options>,\n            PostConfigureSaml2Options>();\n        services.AddSingleton<Microsoft.Extensions.Options.IOptionsMonitorCache<Saml2Options>,\n            ExtendedOptionsMonitorCache<Saml2Options>>();\n\n        return services;\n    }\n\n    public static IIdentityServerBuilder AddSsoIdentityServerServices(this IServiceCollection services,\n        IWebHostEnvironment env, GlobalSettings globalSettings)\n    {\n        services.AddTransient<IDiscoveryResponseGenerator, DiscoveryResponseGenerator>();\n\n        var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalSso);\n        var identityServerBuilder = services\n            .AddIdentityServer(options =>\n            {\n                options.LicenseKey = globalSettings.IdentityServer.LicenseKey;\n                options.IssuerUri = $\"{issuerUri.Scheme}://{issuerUri.Host}\";\n                if (env.IsDevelopment())\n                {\n                    options.Authentication.CookieSameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;\n                }\n                else\n                {\n                    options.UserInteraction.ErrorUrl = \"/Error\";\n                    options.UserInteraction.ErrorIdParameter = \"errorId\";\n                }\n                options.InputLengthRestrictions.UserName = 256;\n                options.KeyManagement.Enabled = false;\n            })\n            .AddInMemoryCaching()\n            .AddInMemoryClients(new List<Client>\n            {\n                new OidcIdentityClient(globalSettings)\n            })\n            .AddInMemoryIdentityResources(new List<IdentityResource>\n            {\n                new IdentityResources.OpenId(),\n                new IdentityResources.Profile()\n            })\n            .AddIdentityServerCertificate(env, globalSettings);\n\n        // PM-23572\n        // Register named FusionCache for SSO authorization code grants.\n        // Provides separation of concerns and automatic Redis/in-memory negotiation\n        // .AddInMemoryCaching should still persist above; this handles configuration caching, etc.,\n        // and is separate from this keyed service, which only serves grant negotiation.\n        services.AddExtendedCache(PersistedGrantsDistributedCacheConstants.CacheKey, globalSettings);\n\n        // Store authorization codes in distributed cache for horizontal scaling\n        // Uses named FusionCache which gracefully degrades to in-memory when Redis isn't configured\n        services.AddSingleton<IPersistedGrantStore, DistributedCachePersistedGrantStore>();\n\n        return identityServerBuilder;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Utilities/SsoAuthenticationMiddleware.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Sustainsys.Saml2.AspNetCore2;\n\nnamespace Bit.Sso.Utilities;\n\npublic class SsoAuthenticationMiddleware\n{\n    private readonly RequestDelegate _next;\n\n    public SsoAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)\n    {\n        _next = next ?? throw new ArgumentNullException(nameof(next));\n        Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));\n    }\n\n    public IAuthenticationSchemeProvider Schemes { get; set; }\n\n    public async Task Invoke(HttpContext context)\n    {\n        if ((context.Request.Method == \"GET\" && context.Request.Query.ContainsKey(\"SAMLart\"))\n            || (context.Request.Method == \"POST\" && context.Request.Form.ContainsKey(\"SAMLart\")))\n        {\n            throw new Exception(\"SAMLart parameter detected. SAML Artifact binding is not allowed.\");\n        }\n\n        context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature\n        {\n            OriginalPath = context.Request.Path,\n            OriginalPathBase = context.Request.PathBase\n        });\n\n        // Give any IAuthenticationRequestHandler schemes a chance to handle the request\n        var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();\n        foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())\n        {\n            // Determine if scheme is appropriate for the current context FIRST\n            if (scheme is IDynamicAuthenticationScheme dynamicScheme)\n            {\n                switch (dynamicScheme.SsoType)\n                {\n                    case SsoType.OpenIdConnect:\n                    default:\n                        if (dynamicScheme.Options is OpenIdConnectOptions oidcOptions &&\n                            !await oidcOptions.CouldHandleAsync(scheme.Name, context))\n                        {\n                            // It's OIDC and Dynamic, but not a good fit\n                            continue;\n                        }\n                        break;\n                    case SsoType.Saml2:\n                        if (dynamicScheme.Options is Saml2Options samlOptions &&\n                            !await samlOptions.CouldHandleAsync(scheme.Name, context))\n                        {\n                            // It's SAML and Dynamic, but not a good fit\n                            continue;\n                        }\n                        break;\n                }\n            }\n\n            // This far it's not dynamic OR it is but \"could\" be handled\n            if (await handlers.GetHandlerAsync(context, scheme.Name) is IAuthenticationRequestHandler handler &&\n                await handler.HandleRequestAsync())\n            {\n                return;\n            }\n        }\n\n        // Fallback to the default scheme from the provider\n        var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();\n        if (defaultAuthenticate != null)\n        {\n            var result = await context.AuthenticateAsync(defaultAuthenticate.Name);\n            if (result?.Principal != null)\n            {\n                context.User = result.Principal;\n            }\n        }\n\n        await _next(context);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Views/Shared/Error.cshtml",
    "content": "﻿@model ErrorViewModel\n@inject IWebHostEnvironment host\n@{\n    ViewData[\"Title\"] = i18nService.T(\"Error\");\n}\n\n<h1 class=\"text-danger\">@i18nService.T(\"Error\")@(string.IsNullOrWhiteSpace(Model?.Message) ? null : $\": {Model.Message}\")</h1>\n@if (!string.IsNullOrWhiteSpace(Model?.RedirectUri))\n{\n    <p class=\"text-danger\">@Html.Raw(i18nService.T(\"SsoErrorWithRedirect\", Model?.RedirectUri))</p>\n}\nelse\n{\n    <p class=\"text-danger\">@i18nService.T(\"SsoError\")</p>\n}\n@if (!string.IsNullOrWhiteSpace(Model?.Description))\n{\n    <p class=\"text-danger\">@Model?.Description</p>\n}\n@if (!string.IsNullOrWhiteSpace(Model?.RequestId))\n{\n    <p><strong>@i18nService.T(\"RequestId\"):</strong> <code>@Model?.RequestId</code></p>\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Views/Shared/Redirect.cshtml",
    "content": "﻿@model RedirectViewModel\n@section Head\n{\n    <meta http-equiv=\"refresh\" content=\"0;url=@Model.RedirectUrl\" data-url=\"@Model.RedirectUrl\">\n    <script>\n        window.onload = function () {\n            window.location.href = document.querySelector(\"meta[http-equiv=refresh]\").getAttribute(\"data-url\");\n        }\n    </script>\n}\n<h1>@i18nService.T(\"Redirecting\")</h1>\n<p>@i18nService.T(\"RedirectingMessage\")</p>\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Views/Shared/_Layout.cshtml",
    "content": "﻿@using static Bit.Core.Utilities.AssemblyHelpers;\n\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>@ViewData[\"Title\"] - SSO</title>\n\n    <link rel=\"stylesheet\" href=\"~/assets/site.css\" asp-append-version=\"true\" />\n    @RenderSection(\"Head\", required: false)\n</head>\n<body>\n    <nav class=\"navbar navbar-expand-md navbar-dark bg-primary mb-4\">\n        <div class=\"container\">\n            <a class=\"navbar-brand\" asp-controller=\"Home\" asp-action=\"Index\">\n                <i class=\"fa fa-shield\" aria-hidden=\"true\"></i>\n            </a>\n        </div>\n    </nav>\n\n    <div class=\"container page-content\">\n        @RenderBody()\n    </div>\n\n    <div class=\"container footer text-body-secondary\">\n        <div class=\"row\">\n            <div class=\"col\">\n                &copy; @DateTime.Now.Year, Bitwarden Inc.\n            </div>\n            <div class=\"col text-center\"></div>\n            <div class=\"col text-right\">\n                Version @GetVersion()\n            </div>\n        </div>\n    </div>\n\n    <script src=\"~/assets/site.js\" asp-append-version=\"true\"></script>\n    @RenderSection(\"Scripts\", required: false)\n</body>\n</html>\n"
  },
  {
    "path": "bitwarden_license/src/Sso/Views/_ViewImports.cshtml",
    "content": "﻿@using Bit.Sso\n@using Bit.Sso.Models\n@using Bit.Core.Services\n@using Microsoft.AspNetCore.Hosting\n@inject II18nService i18nService"
  },
  {
    "path": "bitwarden_license/src/Sso/Views/_ViewStart.cshtml",
    "content": "﻿@{\n    Layout = \"_Layout\";\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/appsettings.Development.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\",\n      \"internalScim\": \"http://localhost:44559\"\n    },\n    \"events\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"notifications\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"storage\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"developmentDirectory\": \"../../../dev\",\n    \"pricingUri\": \"https://billingpricing.qa.bitwarden.pw\",\n    \"mail\": {\n      \"smtp\": {\n        \"host\": \"localhost\",\n        \"port\": 10250\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/appsettings.Production.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.bitwarden.com\",\n      \"api\": \"https://api.bitwarden.com\",\n      \"identity\": \"https://identity.bitwarden.com\",\n      \"admin\": \"https://admin.bitwarden.com\",\n      \"notifications\": \"https://notifications.bitwarden.com\",\n      \"sso\": \"https://sso.bitwarden.com\",\n      \"internalNotifications\": \"https://notifications.bitwarden.com\",\n      \"internalAdmin\": \"https://admin.bitwarden.com\",\n      \"internalIdentity\": \"https://identity.bitwarden.com\",\n      \"internalApi\": \"https://api.bitwarden.com\",\n      \"internalVault\": \"https://vault.bitwarden.com\",\n      \"internalSso\": \"https://sso.bitwarden.com\",\n      \"internalScim\": \"https://scim.bitwarden.com\"\n    },\n    \"braintree\": {\n      \"production\": true\n    },\n    \"sso\": {\n      \"saml\": {\n        \"NameIdFormat\": \"Unspecified\",\n        \"WantAssertionsSigned\": true,\n        \"OutboundSigningAlgorithm\": \"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\",\n        \"SigningBehavior\": \"IfIdpWantAuthnRequestsSigned\",\n        \"ValidateCertificates\": false\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/appsettings.QA.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.qa.bitwarden.pw\",\n      \"api\": \"https://api.qa.bitwarden.pw\",\n      \"identity\": \"https://identity.qa.bitwarden.pw\",\n      \"admin\": \"https://admin.qa.bitwarden.pw\",\n      \"notifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"sso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalNotifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"internalAdmin\": \"https://admin.qa.bitwarden.pw\",\n      \"internalIdentity\": \"https://identity.qa.bitwarden.pw\",\n      \"internalApi\": \"https://api.qa.bitwarden.pw\",\n      \"internalVault\": \"https://vault.qa.bitwarden.pw\",\n      \"internalSso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalScim\": \"https://scim.qa.bitwarden.pw\"\n    },\n    \"braintree\": {\n      \"production\": false\n    },\n    \"sso\": {\n      \"saml\": {\n        \"NameIdFormat\": \"Unspecified\",\n        \"WantAssertionsSigned\": true,\n        \"OutboundSigningAlgorithm\": \"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\",\n        \"SigningBehavior\": \"IfIdpWantAuthnRequestsSigned\",\n        \"ValidateCertificates\": false\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/appsettings.SelfHosted.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": null,\n      \"api\": null,\n      \"identity\": null,\n      \"admin\": null,\n      \"notifications\": null,\n      \"sso\": null,\n      \"internalNotifications\": null,\n      \"internalAdmin\": null,\n      \"internalIdentity\": null,\n      \"internalApi\": null,\n      \"internalVault\": null,\n      \"internalSso\": null,\n      \"internalScim\": null\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/appsettings.json",
    "content": "{\n  \"globalSettings\": {\n    \"selfHosted\": false,\n    \"siteName\": \"Bitwarden\",\n    \"projectName\": \"SSO\",\n    \"stripe\": {\n      \"apiKey\": \"SECRET\"\n    },\n    \"oidcIdentityClientKey\": \"SECRET\",\n    \"sqlServer\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"mail\": {\n      \"sendGridApiKey\": \"SECRET\",\n      \"amazonConfigSetName\": \"Email\",\n      \"replyToEmail\": \"no-reply@bitwarden.com\",\n      \"smtp\": {\n        \"host\": \"localhost\",\n        \"port\": 10250\n      }\n    },\n    \"identityServer\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"dataProtection\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"storage\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"events\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"serviceBus\": {\n      \"connectionString\": \"SECRET\",\n      \"applicationCacheTopicName\": \"SECRET\"\n    },\n    \"notificationHub\": {\n      \"connectionString\": \"SECRET\",\n      \"hubName\": \"SECRET\"\n    },\n    \"amazon\": {\n      \"accessKeyId\": \"SECRET\",\n      \"accessKeySecret\": \"SECRET\",\n      \"region\": \"SECRET\"\n    },\n    \"sso\": {\n      \"cacheLifetimeInSeconds\": 30\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/build.ps1",
    "content": "$curDir = pwd\n$dir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\necho \"`n## Building Sso\"\n\necho \"`nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore $dir\\Sso.csproj\necho \"Clean\"\ndotnet clean $dir\\Sso.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\necho \"Node Build\"\ncd $dir\nnpm ci\nnpm run build\ncd $curDir\necho \"Publish\"\ndotnet publish $dir\\Sso.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\n"
  },
  {
    "path": "bitwarden_license/src/Sso/build.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nCUR_DIR=\"$(pwd)\"\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && \"pwd\" )\"\n\necho -e \"\\n## Building Sso\"\n\necho -e \"\\nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore \"$DIR/Sso.csproj\"\necho \"Clean\"\ndotnet clean \"$DIR/Sso.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\necho \"Node Build\"\ncd \"$DIR\"\nnpm ci\nnpm run build\ncd \"$CUR_DIR\"\necho \"Publish\"\ndotnet publish \"$DIR/Sso.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\n"
  },
  {
    "path": "bitwarden_license/src/Sso/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/core\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    if [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n      chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos\n    fi\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nif [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n    cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf\n    $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab\nfi\n\nif [ \"$globalSettings__selfHosted\" = \"true\" ]; then\n    if [ -z \"$globalSettings__identityServer__certificateLocation\" ]; then\n        export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx\n    fi\nfi\n\nexec $gosu_cmd /app/Sso\n"
  },
  {
    "path": "bitwarden_license/src/Sso/package.json",
    "content": "{\n  \"name\": \"bitwarden-sso\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Bitwarden SSO\",\n  \"repository\": \"https://github.com/bitwarden/enterprise\",\n  \"license\": \"-\",\n  \"scripts\": {\n    \"build\": \"webpack\"\n  },\n  \"dependencies\": {\n    \"bootstrap\": \"5.3.6\",\n    \"font-awesome\": \"4.7.0\",\n    \"jquery\": \"3.7.1\"\n  },\n  \"devDependencies\": {\n    \"css-loader\": \"7.1.2\",\n    \"expose-loader\": \"5.0.1\",\n    \"mini-css-extract-plugin\": \"2.9.2\",\n    \"sass\": \"1.97.2\",\n    \"sass-loader\": \"16.0.5\",\n    \"webpack\": \"5.104.1\",\n    \"webpack-cli\": \"6.0.1\"\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/src/Sso/webfonts.list",
    "content": "﻿Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext"
  },
  {
    "path": "bitwarden_license/src/Sso/webpack.config.js",
    "content": "const path = require(\"path\");\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\n\nconst paths = {\n  assets: \"./wwwroot/assets/\",\n  sassDir: \"./Sass/\",\n};\n\n/** @type {import(\"webpack\").Configuration} */\nmodule.exports = {\n  mode: \"production\",\n  devtool: \"source-map\",\n  entry: {\n    site: [\n      path.resolve(__dirname, paths.sassDir, \"site.scss\"),\n      \"bootstrap\",\n      \"jquery\",\n      \"font-awesome/css/font-awesome.css\",\n    ],\n  },\n  output: {\n    clean: true,\n    path: path.resolve(__dirname, paths.assets),\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.(sa|sc|c)ss$/,\n        use: [MiniCssExtractPlugin.loader, \"css-loader\", \"sass-loader\"],\n      },\n      {\n        test: /.(ttf|otf|eot|svg|woff(2)?)(\\?[a-z0-9]+)?$/,\n        exclude: /loading(|-white).svg/,\n        generator: {\n          filename: \"fonts/[name].[contenthash][ext]\",\n        },\n        type: \"asset/resource\",\n      },\n\n      // Expose jquery globally so they can be used directly in asp.net\n      {\n        test: require.resolve(\"jquery\"),\n        loader: \"expose-loader\",\n        options: {\n          exposes: [\"$\", \"jQuery\"],\n        },\n      },\n    ],\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n      filename: \"[name].css\",\n    }),\n  ],\n};\n"
  },
  {
    "path": "bitwarden_license/test/Bitwarden.License.Tests.proj",
    "content": "<Project Sdk=\"Microsoft.Build.Traversal\">\n  <ItemGroup>\n    <ProjectReference Include=\"**\\*.*proj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/AdminConsole/AutoFixture/ProviderUserFixtures.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.AdminConsole.Enums.Provider;\n\nnamespace Bit.Commercial.Core.Test.AdminConsole.AutoFixture;\n\ninternal class ProviderUser : ICustomization\n{\n    public ProviderUserStatusType Status { get; set; }\n    public ProviderUserType Type { get; set; }\n\n    public ProviderUser(ProviderUserStatusType status, ProviderUserType type)\n    {\n        Status = status;\n        Type = type;\n    }\n\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<Bit.Core.AdminConsole.Entities.Provider.ProviderUser>(composer => composer\n            .With(o => o.Type, Type)\n            .With(o => o.Status, Status));\n    }\n}\n\npublic class ProviderUserAttribute : CustomizeAttribute\n{\n    private readonly ProviderUserStatusType _status;\n    private readonly ProviderUserType _type;\n\n    public ProviderUserAttribute(\n        ProviderUserStatusType status = ProviderUserStatusType.Confirmed,\n        ProviderUserType type = ProviderUserType.ProviderAdmin)\n    {\n        _status = status;\n        _type = type;\n    }\n\n    public override ICustomization GetCustomization(ParameterInfo parameter)\n    {\n        return new ProviderUser(_status, _type);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/CreateProviderCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.AdminConsole.Providers;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.AdminConsole.ProviderFeatures;\n\n[SutProviderCustomize]\npublic class CreateProviderCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider<CreateProviderCommand> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.Msp;\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.CreateMspAsync(provider, default, default, default));\n\n        // Assert\n        Assert.Contains(\"Invalid owner.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider<CreateProviderCommand> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.Msp;\n\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        userRepository.GetByEmailAsync(user.Email).Returns(user);\n\n        // Act\n        await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);\n\n        // Assert\n        await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);\n        await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateResellerAsync_Success(Provider provider, SutProvider<CreateProviderCommand> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.Reseller;\n\n        // Act\n        await sutProvider.Sut.CreateResellerAsync(provider);\n\n        // Assert\n        await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);\n        await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateBusinessUnitAsync_Success(\n    Provider provider,\n    User user,\n    PlanType plan,\n    int minimumSeats,\n    SutProvider<CreateProviderCommand> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.BusinessUnit;\n\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        userRepository.GetByEmailAsync(user.Email).Returns(user);\n\n        // Act\n        await sutProvider.Sut.CreateBusinessUnitAsync(provider, user.Email, plan, minimumSeats);\n\n        // Assert\n        await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(provider);\n        await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateBusinessUnitAsync_UserIdIsInvalid_Throws(\n        Provider provider,\n        SutProvider<CreateProviderCommand> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.Msp;\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.CreateBusinessUnitAsync(provider, default, default, default));\n\n        // Assert\n        Assert.Contains(\"Invalid owner.\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.AdminConsole.Providers;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.AdminConsole.ProviderFeatures;\n\n[SutProviderCustomize]\npublic class RemoveOrganizationFromProviderCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_NoProvider_BadRequest(\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(null, null, null));\n\n        Assert.Equal(\"Failed to remove organization. Please contact support.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_NoProviderOrganization_BadRequest(\n        Provider provider,\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, null, null));\n\n        Assert.Equal(\"Failed to remove organization. Please contact support.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_NoOrganization_BadRequest(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(\n            provider, providerOrganization, null));\n\n        Assert.Equal(\"Failed to remove organization. Please contact support.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_MismatchedProviderOrganization_BadRequest(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        Organization organization,\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization));\n\n        Assert.Equal(\"Failed to remove organization. Please contact support.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_NoConfirmedOwners_BadRequest(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        Organization organization,\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        providerOrganization.ProviderId = provider.Id;\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(\n                providerOrganization.OrganizationId,\n                [],\n                includeProvider: false)\n            .Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization));\n\n        Assert.Equal(\"Organization must have at least one confirmed owner.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_OrganizationNotStripeEnabled_MakesCorrectInvocations(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        Organization organization,\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        providerOrganization.ProviderId = provider.Id;\n\n        organization.GatewayCustomerId = null;\n        organization.GatewaySubscriptionId = null;\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(\n                providerOrganization.OrganizationId,\n                [],\n                includeProvider: false)\n            .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([\n            \"a@example.com\",\n            \"b@example.com\"\n        ]);\n\n        await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);\n\n        await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == \"a@example.com\"));\n\n        await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)\n            .DeleteAsync(providerOrganization);\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);\n\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendProviderUpdatePaymentMethod(\n                organization.Id,\n                organization.Name,\n                provider.Name,\n                Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == \"a@example.com\"));\n\n        await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()\n            .UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_NonConsolidatedBilling_MakesCorrectInvocations(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        Organization organization,\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        providerOrganization.ProviderId = provider.Id;\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(\n                providerOrganization.OrganizationId,\n                [],\n                includeProvider: false)\n            .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([\n            \"a@example.com\",\n            \"b@example.com\"\n        ]);\n\n        sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(\n                options => options.Expand.Contains(\"customer\")))\n            .Returns(GetSubscription(organization.GatewaySubscriptionId, organization.GatewayCustomerId));\n\n        await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        await stripeAdapter.Received(1).UpdateCustomerAsync(organization.GatewayCustomerId,\n            Arg.Is<CustomerUpdateOptions>(options => options.Email == \"a@example.com\"));\n\n        await stripeAdapter.Received(1).DeleteCustomerDiscountAsync(organization.GatewayCustomerId);\n\n        await stripeAdapter.Received(1).DeleteCustomerDiscountAsync(organization.GatewayCustomerId);\n\n        await stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&\n                options.DaysUntilDue == 30));\n\n        await sutProvider.GetDependency<ISubscriberService>().Received(1).RemovePaymentSource(organization);\n\n        await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == \"a@example.com\"));\n\n        await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)\n            .DeleteAsync(providerOrganization);\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);\n\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendProviderUpdatePaymentMethod(\n                organization.Id,\n                organization.Name,\n                provider.Name,\n                Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == \"a@example.com\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_ConsolidatedBilling_MakesCorrectInvocations(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        Organization organization,\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        provider.Status = ProviderStatusType.Billable;\n\n        providerOrganization.ProviderId = provider.Id;\n\n        organization.Status = OrganizationStatusType.Managed;\n\n        organization.PlanType = PlanType.TeamsMonthly;\n\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(\n                providerOrganization.OrganizationId,\n                [],\n                includeProvider: false)\n            .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([\n            \"a@example.com\",\n            \"b@example.com\"\n        ]);\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Description == string.Empty &&\n            options.Email == organization.BillingEmail &&\n            options.Expand[0] == \"tax\" &&\n            options.Expand[1] == \"tax_ids\")).Returns(new Customer\n            {\n                Id = \"customer_id\",\n                Address = new Address\n                {\n                    Country = \"US\"\n                }\n            });\n\n        stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription\n        {\n            Id = \"subscription_id\"\n        });\n\n        await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);\n\n        await stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>\n            options.Customer == organization.GatewayCustomerId &&\n            options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&\n            options.DaysUntilDue == 30 &&\n            options.AutomaticTax.Enabled == true &&\n            options.Metadata[\"organizationId\"] == organization.Id.ToString() &&\n            options.OffSession == true &&\n            options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&\n            options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&\n            options.Items.First().Quantity == organization.Seats));\n\n        await sutProvider.GetDependency<IProviderBillingService>().Received(1)\n            .ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0);\n\n        await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(\n            org =>\n                org.BillingEmail == \"a@example.com\" &&\n                org.GatewaySubscriptionId == \"subscription_id\" &&\n                org.Status == OrganizationStatusType.Created &&\n                org.Enabled == true)); // Verify organization is enabled when new subscription is created\n\n        await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)\n            .DeleteAsync(providerOrganization);\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);\n\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendProviderUpdatePaymentMethod(\n                organization.Id,\n                organization.Name,\n                provider.Name,\n                Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == \"a@example.com\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_ConsolidatedBilling_ReverseCharge_MakesCorrectInvocations(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        Organization organization,\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        provider.Status = ProviderStatusType.Billable;\n\n        providerOrganization.ProviderId = provider.Id;\n\n        organization.Status = OrganizationStatusType.Managed;\n\n        organization.PlanType = PlanType.TeamsMonthly;\n\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(\n                providerOrganization.OrganizationId,\n                [],\n                includeProvider: false)\n            .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([\n            \"a@example.com\",\n            \"b@example.com\"\n        ]);\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Description == string.Empty &&\n            options.Email == organization.BillingEmail &&\n            options.Expand[0] == \"tax\" &&\n            options.Expand[1] == \"tax_ids\")).Returns(new Customer\n            {\n                Id = \"customer_id\",\n                Address = new Address\n                {\n                    Country = \"US\"\n                }\n            });\n\n        stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription\n        {\n            Id = \"subscription_id\"\n        });\n\n        await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);\n\n        await stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>\n            options.Customer == organization.GatewayCustomerId &&\n            options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&\n            options.DaysUntilDue == 30 &&\n            options.AutomaticTax.Enabled == true &&\n            options.Metadata[\"organizationId\"] == organization.Id.ToString() &&\n            options.OffSession == true &&\n            options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&\n            options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&\n            options.Items.First().Quantity == organization.Seats));\n\n        await sutProvider.GetDependency<IProviderBillingService>().Received(1)\n            .ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0);\n\n        await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(\n            org =>\n                org.BillingEmail == \"a@example.com\" &&\n                org.GatewaySubscriptionId == \"subscription_id\" &&\n                org.Status == OrganizationStatusType.Created &&\n                org.Enabled == true)); // Verify organization is enabled when new subscription is created\n\n        await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)\n            .DeleteAsync(providerOrganization);\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);\n\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendProviderUpdatePaymentMethod(\n                organization.Id,\n                organization.Name,\n                provider.Name,\n                Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == \"a@example.com\"));\n    }\n\n    private static Subscription GetSubscription(string subscriptionId, string customerId) =>\n        new()\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Customer = new Customer\n            {\n                Discount = new Discount\n                {\n                    Coupon = new Coupon\n                    {\n                        Id = \"coupon-id\"\n                    }\n                }\n            },\n            Status = StripeConstants.SubscriptionStatus.Active,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new()\n                    {\n                        Id = \"sub_item_123\",\n                        Price = new Price()\n                        {\n                            Id = \"2023-enterprise-org-seat-annually\"\n                        }\n                    }\n                }\n            }\n        };\n\n    [Theory, BitAutoData]\n    public async Task RemoveOrganizationFromProvider_DisabledOrganization_ConsolidatedBilling_EnablesOrganization(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        Organization organization,\n        SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)\n    {\n        // Arrange: Set up a disabled organization that meets the criteria for consolidated billing\n        provider.Status = ProviderStatusType.Billable;\n        providerOrganization.ProviderId = provider.Id;\n        organization.Status = OrganizationStatusType.Managed;\n        organization.PlanType = PlanType.TeamsMonthly;\n        organization.Enabled = false; // Start with a disabled organization\n\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(\n                providerOrganization.OrganizationId,\n                [],\n                includeProvider: false)\n            .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([\n            \"owner@example.com\"\n        ]);\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerUpdateOptions>())\n            .Returns(new Customer\n            {\n                Id = \"customer_id\",\n                Address = new Address\n                {\n                    Country = \"US\"\n                }\n            });\n\n        stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(new Subscription\n        {\n            Id = \"new_subscription_id\"\n        });\n\n        // Act\n        await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);\n\n        // Assert: Verify the disabled organization is now enabled\n        await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(\n            org =>\n                org.Enabled == true && // The previously disabled organization should now be enabled\n                org.Status == OrganizationStatusType.Created &&\n                org.GatewaySubscriptionId == \"new_subscription_id\"));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs",
    "content": "﻿using Bit.Commercial.Core.AdminConsole.Services;\nusing Bit.Commercial.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Business.Provider;\nusing Bit.Core.AdminConsole.Models.Business.Tokenables;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.DataProtection;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\nusing Provider = Bit.Core.AdminConsole.Entities.Provider.Provider;\nusing ProviderUser = Bit.Core.AdminConsole.Entities.Provider.ProviderUser;\n\nnamespace Bit.Commercial.Core.Test.AdminConsole.Services;\n\n[SutProviderCustomize]\npublic class ProviderServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null, null));\n        Assert.Contains(\"Invalid owner.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CompleteSetupAsync_TokenIsInvalid_Throws(User user, Provider provider,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByIdAsync(user.Id).Returns(user);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null, null));\n        Assert.Contains(\"Invalid token.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress,\n            [ProviderUser] ProviderUser providerUser,\n            SutProvider<ProviderService> sutProvider)\n    {\n        providerUser.ProviderId = provider.Id;\n        providerUser.UserId = user.Id;\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByIdAsync(user.Id).Returns(user);\n\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);\n\n        var dataProtectionProvider = DataProtectionProvider.Create(\"ApplicationName\");\n        var protector = dataProtectionProvider.CreateProtector(\"ProviderServiceDataProtector\");\n        sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(protector);\n\n        var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();\n\n        var customer = new Customer { Id = \"customer_id\" };\n        providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer);\n\n        var subscription = new Subscription { Id = \"subscription_id\" };\n        providerBillingService.SetupSubscription(provider).Returns(subscription);\n\n        sutProvider.Create();\n\n        var token = protector.Protect($\"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\");\n\n        await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod, billingAddress);\n\n        await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(\n            p =>\n                p.GatewayCustomerId == customer.Id &&\n                p.GatewaySubscriptionId == subscription.Id &&\n                p.Status == ProviderStatusType.Billable));\n\n        await sutProvider.GetDependency<IProviderUserRepository>().Received()\n            .ReplaceAsync(Arg.Is<ProviderUser>(pu => pu.UserId == user.Id && pu.ProviderId == provider.Id && pu.Key == key));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CompleteSetupAsync_WithAutoConfirmEnabled_ThrowsUserCannotJoinProviderError(User user, Provider provider,\n        string key,\n        TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress,\n        [ProviderUser] ProviderUser providerUser,\n        SutProvider<ProviderService> sutProvider)\n    {\n        providerUser.ProviderId = provider.Id;\n        providerUser.UserId = user.Id;\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByIdAsync(user.Id).Returns(user);\n\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);\n\n        var dataProtectionProvider = DataProtectionProvider.Create(\"ApplicationName\");\n        var protector = dataProtectionProvider.CreateProtector(\"ProviderServiceDataProtector\");\n        sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(protector);\n\n        var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();\n\n        var customer = new Customer { Id = \"customer_id\" };\n        providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer);\n\n        var subscription = new Subscription { Id = \"subscription_id\" };\n        providerBillingService.SetupSubscription(provider).Returns(subscription);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        var policyDetails = new List<PolicyDetails> { new() { OrganizationId = Guid.NewGuid(), IsProvider = false } };\n        var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(policyRequirement);\n\n        sutProvider.Create();\n\n        var token = protector.Protect(\n            $\"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\");\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod,\n                billingAddress));\n\n        Assert.Equal(new UserCannotJoinProvider().Message, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ProviderIdIsInvalid_Throws(Provider provider, SutProvider<ProviderService> sutProvider)\n    {\n        provider.Id = default;\n\n        var exception = await Assert.ThrowsAsync<ArgumentException>(\n            () => sutProvider.Sut.UpdateAsync(provider));\n        Assert.Contains(\"Cannot create provider this way.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_Success(Provider provider, SutProvider<ProviderService> sutProvider)\n    {\n        await sutProvider.Sut.UpdateAsync(provider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ExistingProviderIsNull_DoesNotCallUpdateClientOrganizationsEnabledStatus(\n        Provider provider, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n\n        providerRepository.GetByIdAsync(provider.Id).Returns((Provider)null);\n\n        // Act\n        await sutProvider.Sut.UpdateAsync(provider);\n\n        // Assert\n        await providerRepository.Received(1).ReplaceAsync(provider);\n        await providerOrganizationRepository.DidNotReceive().GetManyDetailsByProviderAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_EnabledStatusNotChanged_DoesNotCallUpdateClientOrganizationsEnabledStatus(\n        Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n\n        existingProvider.Id = provider.Id;\n        existingProvider.Enabled = provider.Enabled; // Same enabled status\n        provider.Type = ProviderType.Msp; // Set to a type that would trigger update if status changed\n\n        providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);\n\n        // Act\n        await sutProvider.Sut.UpdateAsync(provider);\n\n        // Assert\n        await providerRepository.Received(1).ReplaceAsync(provider);\n        await providerOrganizationRepository.DidNotReceive().GetManyDetailsByProviderAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_EnabledStatusChangedButProviderTypeIsReseller_DoesNotCallUpdateClientOrganizationsEnabledStatus(\n        Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n\n        existingProvider.Id = provider.Id;\n        existingProvider.Enabled = !provider.Enabled; // Different enabled status\n        provider.Type = ProviderType.Reseller; // Type that should not trigger update\n\n        providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);\n\n        // Act\n        await sutProvider.Sut.UpdateAsync(provider);\n\n        // Assert\n        await providerRepository.Received(1).ReplaceAsync(provider);\n        await providerOrganizationRepository.DidNotReceive().GetManyDetailsByProviderAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_EnabledStatusChangedAndProviderTypeIsMsp_CallsUpdateClientOrganizationsEnabledStatus(\n        Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n\n        existingProvider.Id = provider.Id;\n        existingProvider.Enabled = !provider.Enabled; // Different enabled status\n        provider.Type = ProviderType.Msp; // Type that should trigger update\n\n        // Create test provider organization details\n        var providerOrganizationDetails = new List<ProviderOrganizationOrganizationDetails>\n        {\n            new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() },\n            new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }\n        };\n\n        // Create test organizations with different enabled status than what we're setting\n        var organizations = providerOrganizationDetails.Select(po =>\n        {\n            var org = new Organization { Id = po.OrganizationId, Enabled = !provider.Enabled };\n            return org;\n        }).ToList();\n\n        providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);\n        providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails);\n\n        foreach (var org in organizations)\n        {\n            organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        }\n\n        // Act\n        await sutProvider.Sut.UpdateAsync(provider);\n\n        // Assert\n        await providerRepository.Received(1).ReplaceAsync(provider);\n        await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id);\n\n        foreach (var org in organizations)\n        {\n            await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(o =>\n                o.Id == org.Id && o.Enabled == provider.Enabled));\n            await applicationCacheService.Received(1).UpsertOrganizationAbilityAsync(Arg.Is<Organization>(o =>\n                o.Id == org.Id && o.Enabled == provider.Enabled));\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_EnabledStatusChangedAndProviderTypeIsBusinessUnit_CallsUpdateClientOrganizationsEnabledStatus(\n        Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n\n        existingProvider.Id = provider.Id;\n        existingProvider.Enabled = !provider.Enabled; // Different enabled status\n        provider.Type = ProviderType.BusinessUnit; // Type that should trigger update\n\n        // Create test provider organization details\n        var providerOrganizationDetails = new List<ProviderOrganizationOrganizationDetails>\n        {\n            new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() },\n            new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }\n        };\n\n        // Create test organizations with different enabled status than what we're setting\n        var organizations = providerOrganizationDetails.Select(po =>\n        {\n            var org = new Organization { Id = po.OrganizationId, Enabled = !provider.Enabled };\n            return org;\n        }).ToList();\n\n        providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);\n        providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails);\n\n        foreach (var org in organizations)\n        {\n            organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        }\n\n        // Act\n        await sutProvider.Sut.UpdateAsync(provider);\n\n        // Assert\n        await providerRepository.Received(1).ReplaceAsync(provider);\n        await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id);\n\n        foreach (var org in organizations)\n        {\n            await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(o =>\n                o.Id == org.Id && o.Enabled == provider.Enabled));\n            await applicationCacheService.Received(1).UpsertOrganizationAbilityAsync(Arg.Is<Organization>(o =>\n                o.Id == org.Id && o.Enabled == provider.Enabled));\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_OrganizationEnabledStatusAlreadyMatches_DoesNotUpdateOrganization(\n        Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n\n        existingProvider.Id = provider.Id;\n        existingProvider.Enabled = !provider.Enabled; // Different enabled status\n        provider.Type = ProviderType.Msp; // Type that should trigger update\n\n        // Create test provider organization details\n        var providerOrganizationDetails = new List<ProviderOrganizationOrganizationDetails>\n        {\n            new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() },\n            new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }\n        };\n\n        // Create test organizations with SAME enabled status as what we're setting\n        var organizations = providerOrganizationDetails.Select(po =>\n        {\n            var org = new Organization { Id = po.OrganizationId, Enabled = provider.Enabled };\n            return org;\n        }).ToList();\n\n        providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);\n        providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails);\n\n        foreach (var org in organizations)\n        {\n            organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        }\n\n        // Act\n        await sutProvider.Sut.UpdateAsync(provider);\n\n        // Assert\n        await providerRepository.Received(1).ReplaceAsync(provider);\n        await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id);\n\n        // Organizations should not be updated since their enabled status already matches\n        foreach (var org in organizations)\n        {\n            await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());\n            await applicationCacheService.DidNotReceive().UpsertOrganizationAbilityAsync(Arg.Any<Organization>());\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_OrganizationIsNull_SkipsNullOrganization(\n        Provider provider, Provider existingProvider, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n\n        existingProvider.Id = provider.Id;\n        existingProvider.Enabled = !provider.Enabled; // Different enabled status\n        provider.Type = ProviderType.Msp; // Type that should trigger update\n\n        // Create test provider organization details\n        var providerOrganizationDetails = new List<ProviderOrganizationOrganizationDetails>\n        {\n            new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() },\n            new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }\n        };\n\n        providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider);\n        providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails);\n\n        // Return null for all organizations\n        organizationRepository.GetByIdAsync(Arg.Any<Guid>()).Returns((Organization)null);\n\n        // Act\n        await sutProvider.Sut.UpdateAsync(provider);\n\n        // Assert\n        await providerRepository.Received(1).ReplaceAsync(provider);\n        await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id);\n\n        // No organizations should be updated since they're all null\n        await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());\n        await applicationCacheService.DidNotReceive().UpsertOrganizationAbilityAsync(Arg.Any<Organization>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task InviteUserAsync_ProviderIdIsInvalid_Throws(ProviderUserInvite<string> invite, SutProvider<ProviderService> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ProviderManageUsers(invite.ProviderId).Returns(true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.InviteUserAsync(invite));\n    }\n\n    [Theory, BitAutoData]\n    public async Task InviteUserAsync_InvalidPermissions_Throws(ProviderUserInvite<string> invite, SutProvider<ProviderService> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ProviderManageUsers(invite.ProviderId).Returns(false);\n        await Assert.ThrowsAsync<InvalidOperationException>(() => sutProvider.Sut.InviteUserAsync(invite));\n    }\n\n    [Theory, BitAutoData]\n    public async Task InviteUserAsync_EmailsInvalid_Throws(Provider provider, ProviderUserInvite<string> providerUserInvite,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        providerRepository.GetByIdAsync(providerUserInvite.ProviderId).Returns(provider);\n        sutProvider.GetDependency<ICurrentContext>().ProviderManageUsers(providerUserInvite.ProviderId).Returns(true);\n\n        providerUserInvite.UserIdentifiers = null;\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.InviteUserAsync(providerUserInvite));\n    }\n\n    [Theory, BitAutoData]\n    public async Task InviteUserAsync_AlreadyInvited(Provider provider, ProviderUserInvite<string> providerUserInvite,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        providerRepository.GetByIdAsync(providerUserInvite.ProviderId).Returns(provider);\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetCountByProviderAsync(default, default, default).ReturnsForAnyArgs(1);\n        sutProvider.GetDependency<ICurrentContext>().ProviderManageUsers(providerUserInvite.ProviderId).Returns(true);\n\n        var result = await sutProvider.Sut.InviteUserAsync(providerUserInvite);\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InviteUserAsync_Success(Provider provider, ProviderUserInvite<string> providerUserInvite,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        providerRepository.GetByIdAsync(providerUserInvite.ProviderId).Returns(provider);\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetCountByProviderAsync(default, default, default).ReturnsForAnyArgs(0);\n        sutProvider.GetDependency<ICurrentContext>().ProviderManageUsers(providerUserInvite.ProviderId).Returns(true);\n\n        var result = await sutProvider.Sut.InviteUserAsync(providerUserInvite);\n        Assert.Equal(providerUserInvite.UserIdentifiers.Count(), result.Count);\n        Assert.True(result.TrueForAll(pu => pu.Status == ProviderUserStatusType.Invited), \"Status must be invited\");\n        Assert.True(result.TrueForAll(pu => pu.ProviderId == providerUserInvite.ProviderId), \"Provider Id must be correct\");\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResendInviteUserAsync_InvalidPermissions_Throws(ProviderUserInvite<Guid> invite, SutProvider<ProviderService> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ProviderManageUsers(invite.ProviderId).Returns(false);\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.ResendInvitesAsync(invite));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResendInvitesAsync_Errors(Provider provider,\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser pu1,\n        [ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu2,\n        [ProviderUser(ProviderUserStatusType.Confirmed)] ProviderUser pu3,\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser pu4,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var providerUsers = new[] { pu1, pu2, pu3, pu4 };\n        pu1.ProviderId = pu2.ProviderId = pu3.ProviderId = provider.Id;\n\n        var invite = new ProviderUserInvite<Guid>\n        {\n            UserIdentifiers = providerUsers.Select(pu => pu.Id),\n            ProviderId = provider.Id\n        };\n\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        providerRepository.GetByIdAsync(provider.Id).Returns(provider);\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers.ToList());\n        sutProvider.GetDependency<ICurrentContext>().ProviderManageUsers(invite.ProviderId).Returns(true);\n\n        var result = await sutProvider.Sut.ResendInvitesAsync(invite);\n        Assert.Equal(\"\", result[0].Item2);\n        Assert.Equal(\"User invalid.\", result[1].Item2);\n        Assert.Equal(\"User invalid.\", result[2].Item2);\n        Assert.Equal(\"User invalid.\", result[3].Item2);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResendInvitesAsync_Success(Provider provider, IEnumerable<ProviderUser> providerUsers,\n        SutProvider<ProviderService> sutProvider)\n    {\n        foreach (var providerUser in providerUsers)\n        {\n            providerUser.ProviderId = provider.Id;\n            providerUser.Status = ProviderUserStatusType.Invited;\n        }\n\n        var invite = new ProviderUserInvite<Guid>\n        {\n            UserIdentifiers = providerUsers.Select(pu => pu.Id),\n            ProviderId = provider.Id\n        };\n\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        providerRepository.GetByIdAsync(provider.Id).Returns(provider);\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers.ToList());\n        sutProvider.GetDependency<ICurrentContext>().ProviderManageUsers(invite.ProviderId).Returns(true);\n\n        var result = await sutProvider.Sut.ResendInvitesAsync(invite);\n        Assert.True(result.All(r => r.Item2 == \"\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendProviderSetupInviteEmailAsync_Success(Provider provider, string email, SutProvider<ProviderService> sutProvider)\n    {\n        await sutProvider.Sut.SendProviderSetupInviteEmailAsync(provider, email);\n\n        await sutProvider.GetDependency<IMailService>().Received(1).SendProviderSetupInviteEmailAsync(provider, Arg.Any<string>(), email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_UserIsInvalid_Throws(SutProvider<ProviderService> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(default, default, default));\n        Assert.Equal(\"User invalid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_AlreadyAccepted_Throws(\n        [ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser providerUser, User user,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, default));\n        Assert.Equal(\"Already accepted.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_TokenIsInvalid_Throws(\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser, User user,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, default));\n        Assert.Equal(\"Invalid token.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_WrongEmail_Throws(\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser, User user,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);\n\n        var dataProtectionProvider = DataProtectionProvider.Create(\"ApplicationName\");\n        var protector = dataProtectionProvider.CreateProtector(\"ProviderServiceDataProtector\");\n        sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(protector);\n        sutProvider.Create();\n\n        var token = protector.Protect($\"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\");\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token));\n        Assert.Equal(\"User email does not match invite.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_Success(\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser, User user,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);\n\n        var dataProtectionProvider = DataProtectionProvider.Create(\"ApplicationName\");\n        var protector = dataProtectionProvider.CreateProtector(\"ProviderServiceDataProtector\");\n        sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(protector);\n        sutProvider.Create();\n\n        providerUser.Email = user.Email;\n        var token = protector.Protect($\"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\");\n\n        var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);\n        Assert.Null(pu.Email);\n        Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);\n        Assert.Equal(user.Id, pu.UserId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_WithAutoConfirmEnabledAndPolicyExists_Throws(\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,\n        User user,\n        SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetByIdAsync(providerUser.Id)\n            .Returns(providerUser);\n\n        var protector = DataProtectionProvider\n            .Create(\"ApplicationName\")\n            .CreateProtector(\"ProviderServiceDataProtector\");\n\n        sutProvider.GetDependency<IDataProtectionProvider>()\n            .CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(protector);\n\n        sutProvider.Create();\n\n        providerUser.Email = user.Email;\n        var token = protector.Protect($\"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\");\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        var policyDetails = new List<PolicyDetails>\n        {\n            new() { OrganizationId = Guid.NewGuid(), IsProvider = false }\n        };\n        var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(policyRequirement);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token));\n\n        Assert.Equal(new UserCannotJoinProvider().Message, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_WithAutoConfirmEnabledButNoPolicyExists_Success(\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,\n        User user,\n        SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetByIdAsync(providerUser.Id)\n            .Returns(providerUser);\n\n        var protector = DataProtectionProvider\n            .Create(\"ApplicationName\")\n            .CreateProtector(\"ProviderServiceDataProtector\");\n\n        sutProvider.GetDependency<IDataProtectionProvider>()\n            .CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(protector);\n        sutProvider.Create();\n\n        providerUser.Email = user.Email;\n        var token = protector.Protect($\"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\");\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        var policyRequirement = new AutomaticUserConfirmationPolicyRequirement([]);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(policyRequirement);\n\n        // Act\n        var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);\n\n        // Assert\n        Assert.Null(pu.Email);\n        Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);\n        Assert.Equal(user.Id, pu.UserId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_WithAutoConfirmDisabled_Success(\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser providerUser,\n        User user,\n        SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetByIdAsync(providerUser.Id)\n            .Returns(providerUser);\n\n        var protector = DataProtectionProvider\n            .Create(\"ApplicationName\")\n            .CreateProtector(\"ProviderServiceDataProtector\");\n\n        sutProvider.GetDependency<IDataProtectionProvider>()\n            .CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(protector);\n        sutProvider.Create();\n\n        providerUser.Email = user.Email;\n        var token = protector.Protect($\"ProviderUserInvite {providerUser.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\");\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(false);\n\n        // Act\n        var pu = await sutProvider.Sut.AcceptUserAsync(providerUser.Id, user, token);\n\n        // Assert\n        Assert.Null(pu.Email);\n        Assert.Equal(ProviderUserStatusType.Accepted, pu.Status);\n        Assert.Equal(user.Id, pu.UserId);\n\n        // Verify that policy check was never called when feature flag is disabled\n        await sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .DidNotReceive()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUsersAsync_NoValid(\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser pu1,\n        [ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu2,\n        [ProviderUser(ProviderUserStatusType.Confirmed)] ProviderUser pu3,\n        SutProvider<ProviderService> sutProvider)\n    {\n        pu1.ProviderId = pu3.ProviderId;\n        var providerUsers = new[] { pu1, pu2, pu3 };\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers);\n\n        var dict = providerUsers.ToDictionary(pu => pu.Id, _ => \"key\");\n        var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, default);\n\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUsersAsync_Success(\n        [ProviderUser(ProviderUserStatusType.Invited)] ProviderUser pu1, User u1,\n        [ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu2, User u2,\n        [ProviderUser(ProviderUserStatusType.Confirmed)] ProviderUser pu3, User u3,\n        Provider provider, User user, SutProvider<ProviderService> sutProvider)\n    {\n        pu1.ProviderId = pu2.ProviderId = pu3.ProviderId = provider.Id;\n        pu1.UserId = u1.Id;\n        pu2.UserId = u2.Id;\n        pu3.UserId = u3.Id;\n        var providerUsers = new[] { pu1, pu2, pu3 };\n\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers);\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        providerRepository.GetByIdAsync(provider.Id).Returns(provider);\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { u1, u2, u3 });\n\n        var dict = providerUsers.ToDictionary(pu => pu.Id, _ => \"key\");\n        var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, user.Id);\n\n        Assert.Equal(\"Invalid user.\", result[0].Item2);\n        Assert.Equal(\"\", result[1].Item2);\n        Assert.Equal(\"Invalid user.\", result[2].Item2);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUsersAsync_WithAutoConfirmEnabledAndPolicyExists_ReturnsError(\n        [ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,\n        Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        pu1.ProviderId = provider.Id;\n        pu1.UserId = u1.Id;\n        var providerUsers = new[] { pu1 };\n\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        var policyDetails = new List<PolicyDetails>\n        {\n            new() { OrganizationId = Guid.NewGuid(), IsProvider = false }\n        };\n        var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(policyDetails);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(u1.Id)\n            .Returns(policyRequirement);\n\n        var dict = providerUsers.ToDictionary(pu => pu.Id, _ => \"key\");\n\n        // Act\n        var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(new UserCannotJoinProvider().Message, result[0].Item2);\n\n        // Verify user was not confirmed\n        await providerUserRepository.DidNotReceive().ReplaceAsync(Arg.Any<ProviderUser>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUsersAsync_WithAutoConfirmEnabledButNoPolicyExists_Success(\n        [ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,\n        Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        pu1.ProviderId = provider.Id;\n        pu1.UserId = u1.Id;\n        var providerUsers = new[] { pu1 };\n\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        var policyRequirement = new AutomaticUserConfirmationPolicyRequirement(new List<PolicyDetails>());\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(u1.Id)\n            .Returns(policyRequirement);\n\n        var dict = providerUsers.ToDictionary(pu => pu.Id, _ => \"key\");\n\n        // Act\n        var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"\", result[0].Item2);\n\n        // Verify user was confirmed\n        await providerUserRepository.Received(1).ReplaceAsync(Arg.Is<ProviderUser>(pu =>\n            pu.Status == ProviderUserStatusType.Confirmed));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUsersAsync_WithAutoConfirmDisabled_Success(\n        [ProviderUser(ProviderUserStatusType.Accepted)] ProviderUser pu1, User u1,\n        Provider provider, User confirmingUser, SutProvider<ProviderService> sutProvider)\n    {\n        // Arrange\n        pu1.ProviderId = provider.Id;\n        pu1.UserId = u1.Id;\n        var providerUsers = new[] { pu1 };\n\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetManyAsync([]).ReturnsForAnyArgs(providerUsers);\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([u1]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(false);\n\n        var dict = providerUsers.ToDictionary(pu => pu.Id, _ => \"key\");\n\n        // Act\n        var result = await sutProvider.Sut.ConfirmUsersAsync(pu1.ProviderId, dict, confirmingUser.Id);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"\", result[0].Item2);\n\n        // Verify user was confirmed\n        await providerUserRepository.Received(1).ReplaceAsync(Arg.Is<ProviderUser>(pu =>\n            pu.Status == ProviderUserStatusType.Confirmed));\n\n        // Verify that policy check was never called when feature flag is disabled\n        await sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .DidNotReceive()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveUserAsync_UserIdIsInvalid_Throws(ProviderUser providerUser,\n        SutProvider<ProviderService> sutProvider)\n    {\n        providerUser.Id = Guid.Empty;\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SaveUserAsync(providerUser, Guid.Empty));\n        Assert.Equal(\"Invite the user first.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveUserAsync_Success(\n        [ProviderUser(type: ProviderUserType.ProviderAdmin)] ProviderUser providerUser, User savingUser,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetByIdAsync(providerUser.Id).Returns(providerUser);\n\n        await sutProvider.Sut.SaveUserAsync(providerUser, savingUser.Id);\n        await providerUserRepository.Received().ReplaceAsync(providerUser);\n        await sutProvider.GetDependency<IEventService>().Received()\n            .LogProviderUserEventAsync(providerUser, EventType.ProviderUser_Updated, null);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteUsersAsync_NoRemainingOwner_Throws(Provider provider, User deletingUser,\n        ICollection<ProviderUser> providerUsers, SutProvider<ProviderService> sutProvider)\n    {\n        var userIds = providerUsers.Select(pu => pu.Id);\n\n        providerUsers.First().UserId = deletingUser.Id;\n        foreach (var providerUser in providerUsers)\n        {\n            providerUser.ProviderId = provider.Id;\n        }\n        providerUsers.Last().ProviderId = default;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers);\n        providerUserRepository.GetManyByProviderAsync(default, default).ReturnsForAnyArgs(new ProviderUser[] { });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.DeleteUsersAsync(provider.Id, userIds, deletingUser.Id));\n        Assert.Equal(\"Provider must have at least one confirmed ProviderAdmin.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteUsersAsync_Success(Provider provider, User deletingUser, ICollection<ProviderUser> providerUsers,\n        [ProviderUser(ProviderUserStatusType.Confirmed, ProviderUserType.ProviderAdmin)] ProviderUser remainingOwner,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var userIds = providerUsers.Select(pu => pu.Id);\n\n        providerUsers.First().UserId = deletingUser.Id;\n        foreach (var providerUser in providerUsers)\n        {\n            providerUser.ProviderId = provider.Id;\n        }\n        providerUsers.Last().ProviderId = default;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetManyAsync(default).ReturnsForAnyArgs(providerUsers);\n        providerUserRepository.GetManyByProviderAsync(default, default).ReturnsForAnyArgs(new[] { remainingOwner });\n\n        var result = await sutProvider.Sut.DeleteUsersAsync(provider.Id, userIds, deletingUser.Id);\n\n        Assert.NotEmpty(result);\n        Assert.Equal(\"You cannot remove yourself.\", result[0].Item2);\n        Assert.Equal(\"\", result[1].Item2);\n        Assert.Equal(\"Invalid user.\", result[2].Item2);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddOrganization_OrganizationAlreadyBelongsToAProvider_Throws(Provider provider,\n        Organization organization, ProviderOrganization po, string key,\n        SutProvider<ProviderService> sutProvider)\n    {\n        po.OrganizationId = organization.Id;\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetByOrganizationId(organization.Id)\n            .Returns(po);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key));\n        Assert.Equal(\"Organization already belongs to a provider.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddOrganization_OrganizationHasSecretsManager_Throws(Provider provider, Organization organization, string key,\n        SutProvider<ProviderService> sutProvider)\n    {\n        organization.PlanType = PlanType.EnterpriseMonthly;\n        organization.UseSecretsManager = true;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key));\n        Assert.Equal(\"The organization is subscribed to Secrets Manager. Please contact Customer Support to manage the subscription.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddOrganization_Success(Provider provider, Organization organization, string key,\n        SutProvider<ProviderService> sutProvider)\n    {\n        organization.PlanType = PlanType.EnterpriseMonthly;\n\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        providerRepository.GetByIdAsync(provider.Id).Returns(provider);\n\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);\n\n        await providerOrganizationRepository.Received(1)\n            .CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>\n                providerOrganization.ProviderId == provider.Id &&\n                providerOrganization.OrganizationId == organization.Id &&\n                providerOrganization.Key == key));\n\n        await organizationRepository.Received(1)\n            .ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == provider.BillingEmail));\n\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateCustomerAsync(\n            organization.GatewayCustomerId,\n            Arg.Is<CustomerUpdateOptions>(options => options.Email == provider.BillingEmail));\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>\n                    providerOrganization.ProviderId == provider.Id &&\n                    providerOrganization.OrganizationId == organization.Id &&\n                    providerOrganization.Key == key),\n                EventType.ProviderOrganization_Added);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,\n        SutProvider<ProviderService> sutProvider)\n    {\n        provider.Type = ProviderType.Msp;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        var expectedPlanType = PlanType.EnterpriseMonthly;\n        organization.PlanType = PlanType.EnterpriseMonthly;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);\n\n        await providerOrganizationRepository.Received(1)\n            .CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>\n                providerOrganization.ProviderId == provider.Id &&\n                providerOrganization.OrganizationId == organization.Id &&\n                providerOrganization.Key == key));\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>\n                    providerOrganization.ProviderId == provider.Id &&\n                    providerOrganization.OrganizationId == organization.Id &&\n                    providerOrganization.Key == key),\n                EventType.ProviderOrganization_Added);\n\n        Assert.Equal(organization.PlanType, expectedPlanType);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,\n        SutProvider<ProviderService> sutProvider)\n    {\n        var newCreationDate = new DateTime(2023, 11, 5);\n        BackdateProviderCreationDate(provider, newCreationDate);\n        provider.Type = ProviderType.Msp;\n\n        organization.PlanType = PlanType.EnterpriseMonthly;\n        organization.Plan = \"Enterprise (Monthly)\";\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        var expectedPlanType = PlanType.EnterpriseMonthly2020;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(expectedPlanType)\n            .Returns(MockPlans.Get(expectedPlanType));\n\n        var expectedPlanId = \"2020-enterprise-org-seat-monthly\";\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);\n        sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .Returns(GetSubscription(organization.GatewaySubscriptionId));\n        await sutProvider.GetDependency<IStripeAdapter>().UpdateSubscriptionAsync(\n            organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));\n\n        await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);\n\n        await providerOrganizationRepository.Received(1)\n            .CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>\n                providerOrganization.ProviderId == provider.Id &&\n                providerOrganization.OrganizationId == organization.Id &&\n                providerOrganization.Key == key));\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>\n                    providerOrganization.ProviderId == provider.Id &&\n                    providerOrganization.OrganizationId == organization.Id &&\n                    providerOrganization.Key == key),\n                EventType.ProviderOrganization_Added);\n\n        Assert.Equal(organization.PlanType, expectedPlanType);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddOrganizationsToReseller_WithResellerProvider_Success(Provider provider, ICollection<Organization> organizations, SutProvider<ProviderService> sutProvider)\n    {\n        provider.Type = ProviderType.Reseller;\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        foreach (var organization in organizations)\n        {\n            organization.PlanType = PlanType.EnterpriseAnnually;\n        }\n\n        var organizationIds = organizations.Select(o => o.Id).ToArray();\n\n        await sutProvider.Sut.AddOrganizationsToReseller(provider.Id, organizationIds);\n\n        await providerOrganizationRepository.Received(1).CreateManyAsync(Arg.Is<IEnumerable<ProviderOrganization>>(i => i.All(po => po.ProviderId == provider.Id && organizations.Any(o => o.Id == po.OrganizationId))));\n        await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventsAsync(\n            Arg.Is<IEnumerable<(ProviderOrganization, EventType, DateTime?)>>(events => events.All(e =>\n                e.Item1.ProviderId == provider.Id && organizationIds.Contains(e.Item1.OrganizationId) && e.Item2 == EventType.ProviderOrganization_Added)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddOrganizationsToReseller_WithMspProvider_Throws(Provider provider, ICollection<Organization> organizations, SutProvider<ProviderService> sutProvider)\n    {\n        provider.Type = ProviderType.Msp;\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        foreach (var organization in organizations)\n        {\n            organization.PlanType = PlanType.EnterpriseAnnually;\n        }\n\n        var organizationIds = organizations.Select(o => o.Id).ToArray();\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddOrganizationsToReseller(provider.Id, organizationIds));\n        Assert.Contains(\"Provider must be of type Reseller in order to assign Organizations to it.\", exception.Message);\n\n        await providerOrganizationRepository.DidNotReceiveWithAnyArgs().CreateManyAsync(default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogProviderOrganizationEventsAsync(default);\n    }\n\n    [Theory, OrganizationCustomize, BitAutoData]\n    public async Task CreateOrganizationAsync_Success(Provider provider, OrganizationSignup organizationSignup,\n        Organization organization, string clientOwnerEmail, User user, SutProvider<ProviderService> sutProvider)\n    {\n        organizationSignup.Plan = PlanType.EnterpriseMonthly;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)\n            .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));\n\n        var providerOrganization =\n            await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);\n\n        await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);\n        await sutProvider.GetDependency<IEventService>()\n            .Received().LogProviderOrganizationEventAsync(providerOrganization,\n                EventType.ProviderOrganization_Created);\n        await sutProvider.GetDependency<IOrganizationService>()\n            .Received().InviteUsersAsync(organization.Id, user.Id, systemUser: null, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(\n                t => t.Count() == 1 &&\n                t.First().Item1.Emails.Count() == 1 &&\n                t.First().Item1.Emails.First() == clientOwnerEmail &&\n                t.First().Item1.Type == OrganizationUserType.Owner &&\n                t.First().Item1.Collections.Count() == 1 &&\n                t.First().Item2 == null));\n    }\n\n    [Theory, OrganizationCustomize, BitAutoData]\n    public async Task CreateOrganizationAsync_InvalidPlanType_ThrowsBadRequestException(\n        Provider provider,\n        OrganizationSignup organizationSignup,\n        Organization organization,\n        string clientOwnerEmail,\n        User user,\n        SutProvider<ProviderService> sutProvider)\n    {\n        provider.Type = ProviderType.Msp;\n        provider.Status = ProviderStatusType.Billable;\n\n        organizationSignup.Plan = PlanType.EnterpriseAnnually;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n\n        sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)\n            .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));\n\n        await providerOrganizationRepository.DidNotReceiveWithAnyArgs().CreateAsync(default);\n    }\n\n    [Theory, OrganizationCustomize, BitAutoData]\n    public async Task CreateOrganizationAsync_InvokeSignupClientAsync(\n        Provider provider,\n        OrganizationSignup organizationSignup,\n        Organization organization,\n        string clientOwnerEmail,\n        User user,\n        SutProvider<ProviderService> sutProvider)\n    {\n        provider.Type = ProviderType.Msp;\n        provider.Status = ProviderStatusType.Billable;\n\n        organizationSignup.Plan = PlanType.EnterpriseMonthly;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n\n        sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)\n            .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection()));\n\n        var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);\n\n        await providerOrganizationRepository.Received(1).CreateAsync(Arg.Is<ProviderOrganization>(\n            po =>\n                po.ProviderId == provider.Id &&\n                po.OrganizationId == organization.Id));\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received()\n            .LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);\n\n        await sutProvider.GetDependency<IOrganizationService>()\n            .Received()\n            .InviteUsersAsync(\n                organization.Id,\n                user.Id,\n                systemUser: null,\n                Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(\n                    t =>\n                        t.Count() == 1 &&\n                        t.First().Item1.Emails.Count() == 1 &&\n                        t.First().Item1.Emails.First() == clientOwnerEmail &&\n                        t.First().Item1.Type == OrganizationUserType.Owner &&\n                        t.First().Item1.Collections.Count() == 1 &&\n                        t.First().Item2 == null));\n    }\n\n    [Theory, OrganizationCustomize, BitAutoData]\n    public async Task CreateOrganizationAsync_SetsAccessAllToFalse\n        (Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,\n            User user, SutProvider<ProviderService> sutProvider, Collection defaultCollection)\n    {\n        organizationSignup.Plan = PlanType.EnterpriseMonthly;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n        var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();\n        sutProvider.GetDependency<IProviderClientOrganizationSignUpCommand>().SignUpClientOrganizationAsync(organizationSignup)\n            .Returns(new ProviderClientOrganizationSignUpResponse(organization, defaultCollection));\n\n        var providerOrganization =\n            await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);\n\n        await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);\n        await sutProvider.GetDependency<IEventService>()\n            .Received().LogProviderOrganizationEventAsync(providerOrganization,\n                EventType.ProviderOrganization_Created);\n        await sutProvider.GetDependency<IOrganizationService>()\n            .Received().InviteUsersAsync(organization.Id, user.Id, systemUser: null, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(\n                t => t.Count() == 1 &&\n                t.First().Item1.Emails.Count() == 1 &&\n                t.First().Item1.Emails.First() == clientOwnerEmail &&\n                t.First().Item1.Type == OrganizationUserType.Owner &&\n                t.First().Item1.Collections.Single().Id == defaultCollection.Id &&\n                !t.First().Item1.Collections.Single().HidePasswords &&\n                !t.First().Item1.Collections.Single().ReadOnly &&\n                t.First().Item1.Collections.Single().Manage &&\n                t.First().Item2 == null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Delete_Success(Provider provider, SutProvider<ProviderService> sutProvider)\n    {\n        var providerRepository = sutProvider.GetDependency<IProviderRepository>();\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n\n        await sutProvider.Sut.DeleteAsync(provider);\n\n        await providerRepository.Received().DeleteAsync(provider);\n        await applicationCacheService.Received().DeleteProviderAbilityAsync(provider.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderNameIsEmpty(string providerAdminEmail, SutProvider<ProviderService> sutProvider)\n    {\n        var provider = new Provider { Name = \"\" };\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminNotFound(Provider provider, SutProvider<ProviderService> sutProvider)\n    {\n        var providerAdminEmail = \"nonexistent@example.com\";\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(null));\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminStatusIsNotConfirmed(\n        Provider provider\n        , User providerAdmin\n        , ProviderUser providerUser\n        , SutProvider<ProviderService> sutProvider)\n    {\n        var providerAdminEmail = \"nonexistent@example.com\";\n        providerUser.Status = ProviderUserStatusType.Confirmed;\n        providerUser.Type = ProviderUserType.ServiceUser;\n\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));\n        Assert.Contains(\"Org admin not found.\", exception.Message);\n\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateDeleteAsync_SendsInitiateDeleteProviderEmail(Provider provider, User providerAdmin\n        , ProviderUser providerUser, SutProvider<ProviderService> sutProvider)\n    {\n        var providerAdminEmail = providerAdmin.Email;\n        providerUser.Status = ProviderUserStatusType.Confirmed;\n        providerUser.Type = ProviderUserType.ProviderAdmin;\n\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);\n        var mailService = sutProvider.GetDependency<IMailService>();\n\n        await sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail);\n        await mailService.Received().SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidToken(Provider provider, string invalidToken\n    , SutProvider<ProviderService> sutProvider)\n    {\n        var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();\n        providerDeleteTokenDataFactory.TryUnprotect(invalidToken, out Arg.Any<ProviderDeleteTokenable>()).Returns(false);\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, invalidToken));\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidTokenData(Provider provider, string validToken\n        , SutProvider<ProviderService> sutProvider)\n    {\n        var validTokenData = new ProviderDeleteTokenable();\n        var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();\n        providerDeleteTokenDataFactory.TryUnprotect(validToken, out validTokenData).Returns(false);\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, validToken));\n    }\n\n    private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>\n        new()\n        {\n            Items = new List<SubscriptionItemOptions>\n            {\n                new() { Id = subscriptionItem.Id, Price = expectedPlanId },\n            }\n        };\n\n    private static Subscription GetSubscription(string subscriptionId) =>\n        new()\n        {\n            Id = subscriptionId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new()\n                    {\n                        Id = \"sub_item_123\",\n                        Price = new Price()\n                        {\n                            Id = \"2023-enterprise-org-seat-annually\"\n                        }\n                    }\n                }\n            }\n        };\n\n    private static void BackdateProviderCreationDate(Provider provider, DateTime newCreationDate)\n    {\n        // Set the CreationDate to the desired value\n        provider.GetType().GetProperty(\"CreationDate\")?.SetValue(provider, newCreationDate, null);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs",
    "content": "﻿using Bit.Commercial.Core.Billing.Providers.Queries;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Stripe.Tax;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.Billing.Providers.Queries;\n\nusing static StripeConstants;\n\n[SutProviderCustomize]\npublic class GetProviderWarningsQueryTests\n{\n    private static readonly string[] _requiredExpansions = [\"customer.tax_ids\"];\n\n    [Theory, BitAutoData]\n    public async Task Run_NoSubscription_NoWarnings(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .ReturnsNull();\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.True(response is\n        {\n            Suspension: null,\n            TaxId: null\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ProviderEnabled_NoSuspensionWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Unpaid,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration> { Data = [] });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.Null(response!.Suspension);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_SuspensionWarning_AddPaymentMethod(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = false;\n        var cancelAt = DateTime.UtcNow.AddDays(7);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Unpaid,\n                CancelAt = cancelAt,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration> { Data = [] });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.True(response is\n        {\n            Suspension.Resolution: \"add_payment_method\"\n        });\n        Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_SuspensionWarning_ContactAdministrator(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = false;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Unpaid,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration> { Data = [] });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.True(response is\n        {\n            Suspension.Resolution: \"contact_administrator\"\n        });\n        Assert.Null(response.Suspension.SubscriptionCancelsAt);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_SuspensionWarning_ContactSupport(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = false;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Canceled,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration> { Data = [] });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.True(response is\n        {\n            Suspension.Resolution: \"contact_support\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NotProviderAdmin_NoTaxIdWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.Null(response!.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NoTaxRegistrationForCountry_NoTaxIdWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = [new Registration { Country = \"GB\" }]\n            });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.Null(response!.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_TaxIdMissingWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = [new Registration { Country = \"CA\" }]\n            });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.True(response is\n        {\n            TaxId.Type: \"tax_id_missing\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_TaxIdVerificationIsNull_NoTaxIdWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId>\n                    {\n                        Data = [new TaxId { Verification = null }]\n                    },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = [new Registration { Country = \"CA\" }]\n            });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.Null(response!.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_TaxIdPendingVerificationWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId>\n                    {\n                        Data = [new TaxId\n                        {\n                            Verification = new TaxIdVerification\n                            {\n                                Status = TaxIdVerificationStatus.Pending\n                            }\n                        }]\n                    },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = [new Registration { Country = \"CA\" }]\n            });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.True(response is\n        {\n            TaxId.Type: \"tax_id_pending_verification\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_TaxIdFailedVerificationWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId>\n                    {\n                        Data = [new TaxId\n                        {\n                            Verification = new TaxIdVerification\n                            {\n                                Status = TaxIdVerificationStatus.Unverified\n                            }\n                        }]\n                    },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = [new Registration { Country = \"CA\" }]\n            });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.True(response is\n        {\n            TaxId.Type: \"tax_id_failed_verification\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_TaxIdVerified_NoTaxIdWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId>\n                    {\n                        Data = [new TaxId\n                        {\n                            Verification = new TaxIdVerification\n                            {\n                                Status = TaxIdVerificationStatus.Verified\n                            }\n                        }]\n                    },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = [new Registration { Country = \"CA\" }]\n            });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.Null(response!.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_MultipleRegistrations_MatchesCorrectCountry(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"DE\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Active))\n            .Returns(new StripeList<Registration>\n            {\n                Data = [\n                    new Registration { Country = \"US\" },\n                    new Registration { Country = \"DE\" },\n                    new Registration { Country = \"FR\" }\n                ]\n            });\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Is<RegistrationListOptions>(opt => opt.Status == TaxRegistrationStatus.Scheduled))\n            .Returns(new StripeList<Registration> { Data = [] });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.True(response is\n        {\n            TaxId.Type: \"tax_id_missing\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_CombinesBothWarningTypes(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = false;\n        var cancelAt = DateTime.UtcNow.AddDays(5);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Unpaid,\n                CancelAt = cancelAt,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"CA\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = [new Registration { Country = \"CA\" }]\n            });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.True(response is\n        {\n            Suspension.Resolution: \"add_payment_method\",\n            TaxId.Type: \"tax_id_missing\"\n        });\n        Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_SwissCustomer_NoTaxIdWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"CH\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = [new Registration { Country = \"CH\" }]\n            });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.Null(response!.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_USCustomer_NoTaxIdWarning(\n        Provider provider,\n        SutProvider<GetProviderWarningsQuery> sutProvider)\n    {\n        provider.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Active,\n                Customer = new Customer\n                {\n                    TaxIds = new StripeList<TaxId> { Data = [] },\n                    Address = new Address { Country = \"US\" }\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);\n        sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = [new Registration { Country = \"US\" }]\n            });\n\n        var response = await sutProvider.Sut.Run(provider);\n\n        Assert.Null(response!.TaxId);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs",
    "content": "﻿#nullable enable\nusing System.Text;\nusing Bit.Commercial.Core.Billing.Providers.Services;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.Billing.Providers;\n\npublic class BusinessUnitConverterTests\n{\n    private readonly IDataProtectionProvider _dataProtectionProvider = Substitute.For<IDataProtectionProvider>();\n    private readonly GlobalSettings _globalSettings = new();\n    private readonly ILogger<BusinessUnitConverter> _logger = Substitute.For<ILogger<BusinessUnitConverter>>();\n    private readonly IMailService _mailService = Substitute.For<IMailService>();\n    private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();\n    private readonly IOrganizationUserRepository _organizationUserRepository = Substitute.For<IOrganizationUserRepository>();\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository = Substitute.For<IProviderOrganizationRepository>();\n    private readonly IProviderPlanRepository _providerPlanRepository = Substitute.For<IProviderPlanRepository>();\n    private readonly IProviderRepository _providerRepository = Substitute.For<IProviderRepository>();\n    private readonly IProviderUserRepository _providerUserRepository = Substitute.For<IProviderUserRepository>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();\n    private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();\n\n    private BusinessUnitConverter BuildConverter() => new(\n        _dataProtectionProvider,\n        _globalSettings,\n        _logger,\n        _mailService,\n        _organizationRepository,\n        _organizationUserRepository,\n        _pricingClient,\n        _providerOrganizationRepository,\n        _providerPlanRepository,\n        _providerRepository,\n        _providerUserRepository,\n        _stripeAdapter,\n        _subscriberService,\n        _userRepository);\n\n    #region FinalizeConversion\n\n    [Theory, BitAutoData]\n    public async Task FinalizeConversion_Succeeds_ReturnsProviderId(\n        Organization organization,\n        Guid userId,\n        string providerKey,\n        string organizationKey)\n    {\n        organization.PlanType = PlanType.EnterpriseAnnually2020;\n\n        var enterpriseAnnually2020 = MockPlans.Get(PlanType.EnterpriseAnnually2020);\n\n        var subscription = new Subscription\n        {\n            Id = \"subscription_id\",\n            CustomerId = \"customer_id\",\n            Status = StripeConstants.SubscriptionStatus.Active,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [\n                    new SubscriptionItem\n                    {\n                        Id = \"subscription_item_id\",\n                        Price = new Price\n                        {\n                            Id = enterpriseAnnually2020.PasswordManager.StripeSeatPlanId\n                        }\n                    }\n                ]\n            }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(subscription);\n\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = \"provider-admin@example.com\"\n        };\n\n        _userRepository.GetByIdAsync(userId).Returns(user);\n\n        var token = SetupDataProtection(organization, user.Email);\n\n        var organizationUser = new OrganizationUser { Status = OrganizationUserStatusType.Confirmed };\n\n        _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id)\n            .Returns(organizationUser);\n\n        var provider = new Provider\n        {\n            Type = ProviderType.BusinessUnit,\n            Status = ProviderStatusType.Pending\n        };\n\n        _providerRepository.GetByOrganizationIdAsync(organization.Id).Returns(provider);\n\n        var providerUser = new ProviderUser\n        {\n            Type = ProviderUserType.ProviderAdmin,\n            Status = ProviderUserStatusType.Invited\n        };\n\n        _providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);\n\n        var providerOrganization = new ProviderOrganization();\n\n        _providerOrganizationRepository.GetByOrganizationId(organization.Id).Returns(providerOrganization);\n\n        _pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually2020)\n            .Returns(enterpriseAnnually2020);\n\n        var enterpriseAnnually = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        _pricingClient.GetPlanOrThrow(PlanType.EnterpriseAnnually)\n            .Returns(enterpriseAnnually);\n\n        var businessUnitConverter = BuildConverter();\n\n        await businessUnitConverter.FinalizeConversion(organization, userId, token, providerKey, organizationKey);\n\n        await _stripeAdapter.Received(2).UpdateCustomerAsync(subscription.CustomerId, Arg.Any<CustomerUpdateOptions>());\n\n        var updatedPriceId = ProviderPriceAdapter.GetActivePriceId(provider, enterpriseAnnually.Type);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id, Arg.Is<SubscriptionUpdateOptions>(\n            arguments =>\n                arguments.Items.Count == 2 &&\n                arguments.Items[0].Id == \"subscription_item_id\" &&\n                arguments.Items[0].Deleted == true &&\n                arguments.Items[1].Price == updatedPriceId &&\n                arguments.Items[1].Quantity == organization.Seats));\n\n        await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(arguments =>\n            arguments.PlanType == PlanType.EnterpriseAnnually &&\n            arguments.Status == OrganizationStatusType.Managed &&\n            arguments.GatewayCustomerId == null &&\n            arguments.GatewaySubscriptionId == null));\n\n        await _providerOrganizationRepository.Received(1).ReplaceAsync(Arg.Is<ProviderOrganization>(arguments =>\n            arguments.Key == organizationKey));\n\n        await _providerRepository.Received(1).ReplaceAsync(Arg.Is<Provider>(arguments =>\n            arguments.Gateway == GatewayType.Stripe &&\n            arguments.GatewayCustomerId == subscription.CustomerId &&\n            arguments.GatewaySubscriptionId == subscription.Id &&\n            arguments.Status == ProviderStatusType.Billable));\n\n        await _providerUserRepository.Received(1).ReplaceAsync(Arg.Is<ProviderUser>(arguments =>\n            arguments.Key == providerKey &&\n            arguments.Status == ProviderUserStatusType.Confirmed));\n    }\n\n    /*\n     * Because the validation for finalization is not an applicative like initialization is,\n     * I'm just testing one specific failure here. I don't see much value in testing every single opportunity for failure.\n     */\n    [Theory, BitAutoData]\n    public async Task FinalizeConversion_ValidationFails_ThrowsBillingException(\n        Organization organization,\n        Guid userId,\n        string token,\n        string providerKey,\n        string organizationKey)\n    {\n        organization.PlanType = PlanType.EnterpriseAnnually2020;\n\n        var subscription = new Subscription\n        {\n            Status = StripeConstants.SubscriptionStatus.Canceled\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(subscription);\n\n        var businessUnitConverter = BuildConverter();\n\n        await Assert.ThrowsAsync<BillingException>(() =>\n            businessUnitConverter.FinalizeConversion(organization, userId, token, providerKey, organizationKey));\n\n        await _organizationUserRepository.DidNotReceiveWithAnyArgs()\n            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    #endregion\n\n    #region InitiateConversion\n\n    [Theory, BitAutoData]\n    public async Task InitiateConversion_Succeeds_ReturnsProviderId(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        organization.PlanType = PlanType.EnterpriseAnnually;\n\n        _subscriberService.GetSubscription(organization).Returns(new Subscription\n        {\n            Status = StripeConstants.SubscriptionStatus.Active\n        });\n\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = providerAdminEmail\n        };\n\n        _userRepository.GetByEmailAsync(providerAdminEmail).Returns(user);\n\n        var organizationUser = new OrganizationUser { Status = OrganizationUserStatusType.Confirmed };\n\n        _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id)\n            .Returns(organizationUser);\n\n        var provider = new Provider { Id = Guid.NewGuid() };\n\n        _providerRepository.CreateAsync(Arg.Is<Provider>(argument =>\n            argument.Name == organization.Name &&\n            argument.BillingEmail == organization.BillingEmail &&\n            argument.Status == ProviderStatusType.Pending &&\n            argument.Type == ProviderType.BusinessUnit)).Returns(provider);\n\n        var plan = MockPlans.Get(organization.PlanType);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        var token = SetupDataProtection(organization, providerAdminEmail);\n\n        var businessUnitConverter = BuildConverter();\n\n        var result = await businessUnitConverter.InitiateConversion(organization, providerAdminEmail);\n\n        Assert.True(result.IsT0);\n\n        var providerId = result.AsT0;\n\n        Assert.Equal(provider.Id, providerId);\n\n        await _providerOrganizationRepository.Received(1).CreateAsync(\n            Arg.Is<ProviderOrganization>(argument =>\n                argument.ProviderId == provider.Id &&\n                argument.OrganizationId == organization.Id));\n\n        await _providerPlanRepository.Received(1).CreateAsync(\n            Arg.Is<ProviderPlan>(argument =>\n                argument.ProviderId == provider.Id &&\n                argument.PlanType == PlanType.EnterpriseAnnually &&\n                argument.SeatMinimum == 0 &&\n                argument.PurchasedSeats == organization.Seats &&\n                argument.AllocatedSeats == organization.Seats));\n\n        await _providerUserRepository.Received(1).CreateAsync(\n            Arg.Is<ProviderUser>(argument =>\n                argument.ProviderId == provider.Id &&\n                argument.UserId == user.Id &&\n                argument.Email == user.Email &&\n                argument.Status == ProviderUserStatusType.Invited &&\n                argument.Type == ProviderUserType.ProviderAdmin));\n\n        await _mailService.Received(1).SendBusinessUnitConversionInviteAsync(\n            organization,\n            token,\n            user.Email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateConversion_ValidationFails_ReturnsErrors(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        organization.PlanType = PlanType.TeamsMonthly;\n\n        _subscriberService.GetSubscription(organization).Returns(new Subscription\n        {\n            Status = StripeConstants.SubscriptionStatus.Canceled\n        });\n\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = providerAdminEmail\n        };\n\n        _providerOrganizationRepository.GetByOrganizationId(organization.Id)\n            .Returns(new ProviderOrganization());\n\n        _userRepository.GetByEmailAsync(providerAdminEmail).Returns(user);\n\n        var organizationUser = new OrganizationUser { Status = OrganizationUserStatusType.Invited };\n\n        _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id)\n            .Returns(organizationUser);\n\n        var businessUnitConverter = BuildConverter();\n\n        var result = await businessUnitConverter.InitiateConversion(organization, providerAdminEmail);\n\n        Assert.True(result.IsT1);\n\n        var problems = result.AsT1;\n\n        Assert.Contains(\"Organization must be on an enterprise plan.\", problems);\n\n        Assert.Contains(\"Organization must have a valid subscription.\", problems);\n\n        Assert.Contains(\"Organization is already linked to a provider.\", problems);\n\n        Assert.Contains(\"Provider admin must be a confirmed member of the organization being converted.\", problems);\n    }\n\n    #endregion\n\n    #region ResendConversionInvite\n\n    [Theory, BitAutoData]\n    public async Task ResendConversionInvite_ConversionInProgress_Succeeds(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        SetupConversionInProgress(organization, providerAdminEmail);\n\n        var token = SetupDataProtection(organization, providerAdminEmail);\n\n        var businessUnitConverter = BuildConverter();\n\n        await businessUnitConverter.ResendConversionInvite(organization, providerAdminEmail);\n\n        await _mailService.Received(1).SendBusinessUnitConversionInviteAsync(\n            organization,\n            token,\n            providerAdminEmail);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResendConversionInvite_NoConversionInProgress_DoesNothing(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        SetupDataProtection(organization, providerAdminEmail);\n\n        var businessUnitConverter = BuildConverter();\n\n        await businessUnitConverter.ResendConversionInvite(organization, providerAdminEmail);\n\n        await _mailService.DidNotReceiveWithAnyArgs().SendBusinessUnitConversionInviteAsync(\n            Arg.Any<Organization>(),\n            Arg.Any<string>(),\n            Arg.Any<string>());\n    }\n\n    #endregion\n\n    #region ResetConversion\n\n    [Theory, BitAutoData]\n    public async Task ResetConversion_ConversionInProgress_Succeeds(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        var (provider, providerOrganization, providerUser, providerPlan) = SetupConversionInProgress(organization, providerAdminEmail);\n\n        var businessUnitConverter = BuildConverter();\n\n        await businessUnitConverter.ResetConversion(organization, providerAdminEmail);\n\n        await _providerOrganizationRepository.Received(1)\n            .DeleteAsync(providerOrganization);\n\n        await _providerUserRepository.Received(1)\n            .DeleteAsync(providerUser);\n\n        await _providerPlanRepository.Received(1)\n            .DeleteAsync(providerPlan);\n\n        await _providerRepository.Received(1)\n            .DeleteAsync(provider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResetConversion_NoConversionInProgress_DoesNothing(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        var businessUnitConverter = BuildConverter();\n\n        await businessUnitConverter.ResetConversion(organization, providerAdminEmail);\n\n        await _providerOrganizationRepository.DidNotReceiveWithAnyArgs()\n            .DeleteAsync(Arg.Any<ProviderOrganization>());\n\n        await _providerUserRepository.DidNotReceiveWithAnyArgs()\n            .DeleteAsync(Arg.Any<ProviderUser>());\n\n        await _providerPlanRepository.DidNotReceiveWithAnyArgs()\n            .DeleteAsync(Arg.Any<ProviderPlan>());\n\n        await _providerRepository.DidNotReceiveWithAnyArgs()\n            .DeleteAsync(Arg.Any<Provider>());\n    }\n\n    #endregion\n\n    #region Utilities\n\n    private string SetupDataProtection(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        var dataProtector = new MockDataProtector(organization, providerAdminEmail);\n        _dataProtectionProvider.CreateProtector($\"{nameof(BusinessUnitConverter)}DataProtector\").Returns(dataProtector);\n        return dataProtector.Protect(dataProtector.Token);\n    }\n\n    private (Provider, ProviderOrganization, ProviderUser, ProviderPlan) SetupConversionInProgress(\n        Organization organization,\n        string providerAdminEmail)\n    {\n        var user = new User { Id = Guid.NewGuid() };\n\n        _userRepository.GetByEmailAsync(providerAdminEmail).Returns(user);\n\n        var provider = new Provider\n        {\n            Id = Guid.NewGuid(),\n            Type = ProviderType.BusinessUnit,\n            Status = ProviderStatusType.Pending\n        };\n\n        _providerRepository.GetByOrganizationIdAsync(organization.Id).Returns(provider);\n\n        var providerUser = new ProviderUser\n        {\n            Id = Guid.NewGuid(),\n            ProviderId = provider.Id,\n            UserId = user.Id,\n            Type = ProviderUserType.ProviderAdmin,\n            Status = ProviderUserStatusType.Invited,\n            Email = providerAdminEmail\n        };\n\n        _providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id)\n            .Returns(providerUser);\n\n        var providerOrganization = new ProviderOrganization\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organization.Id,\n            ProviderId = provider.Id\n        };\n\n        _providerOrganizationRepository.GetByOrganizationId(organization.Id)\n            .Returns(providerOrganization);\n\n        var providerPlan = new ProviderPlan\n        {\n            Id = Guid.NewGuid(),\n            ProviderId = provider.Id,\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        _providerPlanRepository.GetByProviderId(provider.Id).Returns([providerPlan]);\n\n        return (provider, providerOrganization, providerUser, providerPlan);\n    }\n\n    #endregion\n}\n\npublic class MockDataProtector(\n    Organization organization,\n    string providerAdminEmail) : IDataProtector\n{\n    public string Token = $\"BusinessUnitConversionInvite {organization.Id} {providerAdminEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}\";\n\n    public IDataProtector CreateProtector(string purpose) => this;\n\n    public byte[] Protect(byte[] plaintext) => Encoding.UTF8.GetBytes(Token);\n\n    public byte[] Unprotect(byte[] protectedData) => Encoding.UTF8.GetBytes(Token);\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Commercial.Core.Billing.Providers.Models;\nusing Bit.Commercial.Core.Billing.Providers.Services;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Models;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Braintree;\nusing CsvHelper;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Stripe;\nusing Xunit;\nusing static Bit.Core.Test.Billing.Utilities;\nusing Address = Stripe.Address;\nusing Customer = Stripe.Customer;\nusing PaymentMethod = Stripe.PaymentMethod;\nusing Subscription = Stripe.Subscription;\n\nnamespace Bit.Commercial.Core.Test.Billing.Providers;\n\n[SutProviderCustomize]\npublic class ProviderBillingServiceTests\n{\n    #region ChangePlan\n\n    [Theory, BitAutoData]\n    public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException(\n        ChangeProviderPlanCommand command,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();\n        providerPlanRepository.GetByIdAsync(Arg.Any<Guid>()).Returns((ProviderPlan)null);\n\n        // Act\n        var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.ChangePlan(command));\n\n        // Assert\n        Assert.Equal(\"Provider plan not found.\", actual.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ChangePlan_ProviderNotFound_DoesNothing(\n        ChangeProviderPlanCommand command,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var existingPlan = new ProviderPlan\n        {\n            Id = command.ProviderPlanId,\n            PlanType = command.NewPlan,\n            PurchasedSeats = 0,\n            AllocatedSeats = 0,\n            SeatMinimum = 0\n        };\n        providerPlanRepository\n            .GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))\n            .Returns(existingPlan);\n\n        // Act\n        await sutProvider.Sut.ChangePlan(command);\n\n        // Assert\n        await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());\n        await stripeAdapter.Received(0).UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ChangePlan_SameProviderPlan_DoesNothing(\n        ChangeProviderPlanCommand command,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var existingPlan = new ProviderPlan\n        {\n            Id = command.ProviderPlanId,\n            PlanType = command.NewPlan,\n            PurchasedSeats = 0,\n            AllocatedSeats = 0,\n            SeatMinimum = 0\n        };\n        providerPlanRepository\n            .GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))\n            .Returns(existingPlan);\n\n        // Act\n        await sutProvider.Sut.ChangePlan(command);\n\n        // Assert\n        await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());\n        await stripeAdapter.Received(0).UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ChangePlan_UpdatesSubscriptionCorrectly(\n        Guid providerPlanId,\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.BusinessUnit;\n\n        var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();\n        var existingPlan = new ProviderPlan\n        {\n            Id = providerPlanId,\n            ProviderId = provider.Id,\n            PlanType = PlanType.EnterpriseAnnually,\n            PurchasedSeats = 2,\n            AllocatedSeats = 10,\n            SeatMinimum = 8\n        };\n        providerPlanRepository\n            .GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId))\n            .Returns(existingPlan);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(existingPlan.PlanType)\n            .Returns(MockPlans.Get(existingPlan.PlanType));\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider)\n            .Returns(new Subscription\n            {\n                Id = provider.GatewaySubscriptionId,\n                Items = new StripeList<SubscriptionItem>\n                {\n                    Data =\n                    [\n                        new SubscriptionItem\n                        {\n                            Id = \"si_ent_annual\",\n                            Price = new Price\n                            {\n                                Id = MockPlans.Get(PlanType.EnterpriseAnnually).PasswordManager\n                                    .StripeProviderPortalSeatPlanId\n                            },\n                            Quantity = 10\n                        }\n                    ]\n                }\n            });\n\n        var command =\n            new ChangeProviderPlanCommand(provider, providerPlanId, PlanType.EnterpriseMonthly);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(command.NewPlan)\n            .Returns(MockPlans.Get(command.NewPlan));\n\n        // Act\n        await sutProvider.Sut.ChangePlan(command);\n\n        // Assert\n        await providerPlanRepository.Received(1)\n            .ReplaceAsync(Arg.Is<ProviderPlan>(p => p.PlanType == PlanType.EnterpriseMonthly));\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        await stripeAdapter.Received(1)\n            .UpdateSubscriptionAsync(\n                Arg.Is(provider.GatewaySubscriptionId),\n                Arg.Is<SubscriptionUpdateOptions>(p =>\n                    p.Items.Count(si => si.Id == \"si_ent_annual\" && si.Deleted == true) == 1));\n\n        var newPlanCfg = MockPlans.Get(command.NewPlan);\n        await stripeAdapter.Received(1)\n            .UpdateSubscriptionAsync(\n                Arg.Is(provider.GatewaySubscriptionId),\n                Arg.Is<SubscriptionUpdateOptions>(p =>\n                    p.Items.Count(si =>\n                        si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId &&\n                        si.Deleted == default &&\n                        si.Quantity == 10) == 1));\n    }\n\n    #endregion\n\n    #region CreateCustomerForClientOrganization\n\n    [Theory, BitAutoData]\n    public async Task CreateCustomerForClientOrganization_ProviderNull_ThrowsArgumentNullException(\n        Organization organization,\n        SutProvider<ProviderBillingService> sutProvider) =>\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            sutProvider.Sut.CreateCustomerForClientOrganization(null, organization));\n\n    [Theory, BitAutoData]\n    public async Task CreateCustomerForClientOrganization_OrganizationNull_ThrowsArgumentNullException(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider) =>\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            sutProvider.Sut.CreateCustomerForClientOrganization(provider, null));\n\n    [Theory, BitAutoData]\n    public async Task CreateCustomerForClientOrganization_HasGatewayCustomerId_NoOp(\n        Provider provider,\n        Organization organization,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        organization.GatewayCustomerId = \"customer_id\";\n\n        await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);\n\n        await sutProvider.GetDependency<ISubscriberService>().DidNotReceiveWithAnyArgs()\n            .GetCustomerOrThrow(Arg.Any<ISubscriber>(), Arg.Any<CustomerGetOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateCustomer_ForClientOrg_Succeeds(\n        Provider provider,\n        Organization organization,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        organization.GatewayCustomerId = null;\n        organization.Name = \"Name\";\n        organization.BusinessName = \"BusinessName\";\n\n        var providerCustomer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"USA\",\n                PostalCode = \"12345\",\n                Line1 = \"123 Main St.\",\n                Line2 = \"Unit 4\",\n                City = \"Fake Town\",\n                State = \"Fake State\"\n            },\n            TaxIds = new StripeList<TaxId>\n            {\n                Data =\n                [\n                    new TaxId { Type = \"TYPE\", Value = \"VALUE\" }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(\n                options => options.Expand.Contains(\"tax\") && options.Expand.Contains(\"tax_ids\")))\n            .Returns(providerCustomer);\n\n        sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri\n            .Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings())\n            {\n                CloudRegion = \"US\"\n            });\n\n        sutProvider.GetDependency<IStripeAdapter>().CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(\n                options =>\n                    options.Address.Country == providerCustomer.Address.Country &&\n                    options.Address.PostalCode == providerCustomer.Address.PostalCode &&\n                    options.Address.Line1 == providerCustomer.Address.Line1 &&\n                    options.Address.Line2 == providerCustomer.Address.Line2 &&\n                    options.Address.City == providerCustomer.Address.City &&\n                    options.Address.State == providerCustomer.Address.State &&\n                    options.Name == organization.DisplayName() &&\n                    options.Description == $\"{provider.Name} Client Organization\" &&\n                    options.Email == provider.BillingEmail &&\n                    options.InvoiceSettings.CustomFields.FirstOrDefault().Name == \"Organization\" &&\n                    options.InvoiceSettings.CustomFields.FirstOrDefault().Value == \"Name\" &&\n                    options.Metadata[\"region\"] == \"US\" &&\n                    options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&\n                    options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value))\n            .Returns(new Customer { Id = \"customer_id\" });\n\n        await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);\n\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(\n            options =>\n                options.Address.Country == providerCustomer.Address.Country &&\n                options.Address.PostalCode == providerCustomer.Address.PostalCode &&\n                options.Address.Line1 == providerCustomer.Address.Line1 &&\n                options.Address.Line2 == providerCustomer.Address.Line2 &&\n                options.Address.City == providerCustomer.Address.City &&\n                options.Address.State == providerCustomer.Address.State &&\n                options.Name == organization.DisplayName() &&\n                options.Description == $\"{provider.Name} Client Organization\" &&\n                options.Email == provider.BillingEmail &&\n                options.InvoiceSettings.CustomFields.FirstOrDefault().Name == \"Organization\" &&\n                options.InvoiceSettings.CustomFields.FirstOrDefault().Value == \"Name\" &&\n                options.Metadata[\"region\"] == \"US\" &&\n                options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&\n                options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value));\n\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(\n            org => org.GatewayCustomerId == \"customer_id\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateCustomer_ForClientOrg_ReverseCharge_Succeeds(\n        Provider provider,\n        Organization organization,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        organization.GatewayCustomerId = null;\n        organization.Name = \"Name\";\n        organization.BusinessName = \"BusinessName\";\n\n        var providerCustomer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"CA\",\n                PostalCode = \"12345\",\n                Line1 = \"123 Main St.\",\n                Line2 = \"Unit 4\",\n                City = \"Fake Town\",\n                State = \"Fake State\"\n            },\n            TaxIds = new StripeList<TaxId>\n            {\n                Data =\n                [\n                    new TaxId { Type = \"TYPE\", Value = \"VALUE\" }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(\n                options => options.Expand.Contains(\"tax\") && options.Expand.Contains(\"tax_ids\")))\n            .Returns(providerCustomer);\n\n        sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri\n            .Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings())\n            {\n                CloudRegion = \"US\"\n            });\n\n        sutProvider.GetDependency<IStripeAdapter>().CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(\n                options =>\n                    options.Address.Country == providerCustomer.Address.Country &&\n                    options.Address.PostalCode == providerCustomer.Address.PostalCode &&\n                    options.Address.Line1 == providerCustomer.Address.Line1 &&\n                    options.Address.Line2 == providerCustomer.Address.Line2 &&\n                    options.Address.City == providerCustomer.Address.City &&\n                    options.Address.State == providerCustomer.Address.State &&\n                    options.Name == organization.DisplayName() &&\n                    options.Description == $\"{provider.Name} Client Organization\" &&\n                    options.Email == provider.BillingEmail &&\n                    options.InvoiceSettings.CustomFields.FirstOrDefault().Name == \"Organization\" &&\n                    options.InvoiceSettings.CustomFields.FirstOrDefault().Value == \"Name\" &&\n                    options.Metadata[\"region\"] == \"US\" &&\n                    options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&\n                    options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value &&\n                    options.TaxExempt == StripeConstants.TaxExempt.Reverse))\n            .Returns(new Customer { Id = \"customer_id\" });\n\n        await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);\n\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(\n            options =>\n                options.Address.Country == providerCustomer.Address.Country &&\n                options.Address.PostalCode == providerCustomer.Address.PostalCode &&\n                options.Address.Line1 == providerCustomer.Address.Line1 &&\n                options.Address.Line2 == providerCustomer.Address.Line2 &&\n                options.Address.City == providerCustomer.Address.City &&\n                options.Address.State == providerCustomer.Address.State &&\n                options.Name == organization.DisplayName() &&\n                options.Description == $\"{provider.Name} Client Organization\" &&\n                options.Email == provider.BillingEmail &&\n                options.InvoiceSettings.CustomFields.FirstOrDefault().Name == \"Organization\" &&\n                options.InvoiceSettings.CustomFields.FirstOrDefault().Value == \"Name\" &&\n                options.Metadata[\"region\"] == \"US\" &&\n                options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type &&\n                options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value));\n\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(Arg.Is<Organization>(\n            org => org.GatewayCustomerId == \"customer_id\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateCustomer_ForClientOrg_USCustomer_SetsTaxExemptToNone(\n        Provider provider,\n        Organization organization,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        organization.GatewayCustomerId = null;\n        organization.Name = \"Name\";\n\n        var providerCustomer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\",\n                Line1 = \"123 Main St.\",\n                Line2 = \"Unit 4\",\n                City = \"Fake Town\",\n                State = \"Fake State\"\n            },\n            TaxIds = new StripeList<TaxId>\n            {\n                Data =\n                [\n                    new TaxId { Type = \"TYPE\", Value = \"VALUE\" }\n                ]\n            },\n            TaxExempt = null\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(\n                options => options.Expand.Contains(\"tax\") && options.Expand.Contains(\"tax_ids\")))\n            .Returns(providerCustomer);\n\n        sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri\n            .Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings())\n            {\n                CloudRegion = \"US\"\n            });\n\n        sutProvider.GetDependency<IStripeAdapter>().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>())\n            .Returns(new Customer { Id = \"customer_id\" });\n\n        await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);\n\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).CreateCustomerAsync(\n            Arg.Is<CustomerCreateOptions>(options => options.TaxExempt == StripeConstants.TaxExempt.None));\n    }\n\n    #endregion\n\n    #region GenerateClientInvoiceReport\n\n    [Theory, BitAutoData]\n    public async Task GenerateClientInvoiceReport_NullInvoiceId_ThrowsArgumentNullException(\n        SutProvider<ProviderBillingService> sutProvider) =>\n        await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GenerateClientInvoiceReport(null));\n\n    [Theory, BitAutoData]\n    public async Task GenerateClientInvoiceReport_NoInvoiceItems_ReturnsNull(\n        string invoiceId,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        sutProvider.GetDependency<IProviderInvoiceItemRepository>().GetByInvoiceId(invoiceId).Returns([]);\n\n        var reportContent = await sutProvider.Sut.GenerateClientInvoiceReport(invoiceId);\n\n        Assert.Null(reportContent);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GenerateClientInvoiceReport_Succeeds(\n        string invoiceId,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        var clientId = Guid.NewGuid();\n\n        var invoiceItems = new List<ProviderInvoiceItem>\n        {\n            new ()\n            {\n                ClientId = clientId,\n                ClientName = \"Client 1\",\n                AssignedSeats = 50,\n                UsedSeats = 30,\n                PlanName = \"Teams (Monthly)\",\n                Total = 500\n            }\n        };\n\n        sutProvider.GetDependency<IProviderInvoiceItemRepository>().GetByInvoiceId(invoiceId).Returns(invoiceItems);\n\n        var reportContent = await sutProvider.Sut.GenerateClientInvoiceReport(invoiceId);\n\n        using var memoryStream = new MemoryStream(reportContent);\n\n        using var streamReader = new StreamReader(memoryStream);\n\n        using var csvReader = new CsvReader(streamReader, CultureInfo.InvariantCulture);\n\n        var records = csvReader.GetRecords<ProviderClientInvoiceReportRow>().ToList();\n\n        Assert.Single(records);\n\n        var record = records.First();\n\n        Assert.Equal(clientId.ToString(), record.Id);\n        Assert.Equal(\"Client 1\", record.Client);\n        Assert.Equal(50, record.Assigned);\n        Assert.Equal(30, record.Used);\n        Assert.Equal(20, record.Remaining);\n        Assert.Equal(\"Teams (Monthly)\", record.Plan);\n        Assert.Equal(\"$500.00\", record.Total);\n    }\n\n    #endregion\n\n    #region ScaleSeats\n\n    [Theory, BitAutoData]\n    public async Task ScaleSeats_BelowToBelow_Succeeds(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                PlanType = PlanType.TeamsMonthly,\n                ProviderId = provider.Id,\n                PurchasedSeats = 0,\n                SeatMinimum = 100,\n                AllocatedSeats = 50\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                PlanType = PlanType.EnterpriseMonthly,\n                ProviderId = provider.Id,\n                PurchasedSeats = 0,\n                SeatMinimum = 500,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams } },\n                    new SubscriptionItem\n                    {\n                        Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);\n\n        // 50 seats currently assigned with a seat minimum of 100\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(\n        [\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = teamsMonthlyPlan.Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 25\n            },\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = teamsMonthlyPlan.Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 25\n            }\n        ]);\n\n        await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10);\n\n        // 50 assigned seats + 10 seat scale up = 60 seats, well below the 100 minimum\n        await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs().UpdateSubscriptionAsync(\n            Arg.Any<string>(),\n            Arg.Any<SubscriptionUpdateOptions>());\n\n        await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            pPlan => pPlan.AllocatedSeats == 60));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ScaleSeats_BelowToAbove_Succeeds(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                PlanType = PlanType.TeamsMonthly,\n                ProviderId = provider.Id,\n                PurchasedSeats = 0,\n                SeatMinimum = 100,\n                AllocatedSeats = 95\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                PlanType = PlanType.EnterpriseMonthly,\n                ProviderId = provider.Id,\n                PurchasedSeats = 0,\n                SeatMinimum = 500,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        var providerPlan = providerPlans.First();\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams } },\n                    new SubscriptionItem\n                    {\n                        Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);\n\n        // 95 seats currently assigned with a seat minimum of 100\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(\n        [\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = teamsMonthlyPlan.Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 60\n            },\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = teamsMonthlyPlan.Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 35\n            }\n        ]);\n\n        await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10);\n\n        // 95 current + 10 seat scale = 105 seats, 5 above the minimum\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateSubscriptionAsync(\n            provider.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(\n                options =>\n                    options.Items.First().Price == ProviderPriceAdapter.MSP.Active.Teams &&\n                    options.Items.First().Quantity == 105));\n\n        // 105 total seats - 100 minimum = 5 purchased seats\n        await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 5 && pPlan.AllocatedSeats == 105));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ScaleSeats_AboveToAbove_Succeeds(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                PlanType = PlanType.TeamsMonthly,\n                ProviderId = provider.Id,\n                PurchasedSeats = 10,\n                SeatMinimum = 100,\n                AllocatedSeats = 110\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                PlanType = PlanType.EnterpriseMonthly,\n                ProviderId = provider.Id,\n                PurchasedSeats = 0,\n                SeatMinimum = 500,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        var providerPlan = providerPlans.First();\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams } },\n                    new SubscriptionItem\n                    {\n                        Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);\n\n        // 110 seats currently assigned with a seat minimum of 100\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(\n        [\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = teamsMonthlyPlan.Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 60\n            },\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = teamsMonthlyPlan.Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 50\n            }\n        ]);\n\n        await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, 10);\n\n        // 110 current + 10 seat scale up = 120 seats\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateSubscriptionAsync(\n            provider.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(\n                options =>\n                    options.Items.First().Price == ProviderPriceAdapter.MSP.Active.Teams &&\n                    options.Items.First().Quantity == 120));\n\n        // 120 total seats - 100 seat minimum = 20 purchased seats\n        await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 20 && pPlan.AllocatedSeats == 120));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ScaleSeats_AboveToBelow_Succeeds(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                PlanType = PlanType.TeamsMonthly,\n                ProviderId = provider.Id,\n                PurchasedSeats = 10,\n                SeatMinimum = 100,\n                AllocatedSeats = 110\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                PlanType = PlanType.EnterpriseMonthly,\n                ProviderId = provider.Id,\n                PurchasedSeats = 0,\n                SeatMinimum = 500,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        var providerPlan = providerPlans.First();\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams } },\n                    new SubscriptionItem\n                    {\n                        Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);\n\n        // 110 seats currently assigned with a seat minimum of 100\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(\n        [\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = teamsMonthlyPlan.Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 60\n            },\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = teamsMonthlyPlan.Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 50\n            }\n        ]);\n\n        await sutProvider.Sut.ScaleSeats(provider, PlanType.TeamsMonthly, -30);\n\n        // 110 seats - 30 scale down seats = 80 seats, below the 100 seat minimum.\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateSubscriptionAsync(\n            provider.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(\n                options =>\n                    options.Items.First().Price == ProviderPriceAdapter.MSP.Active.Teams &&\n                    options.Items.First().Quantity == providerPlan.SeatMinimum!.Value));\n\n        // Being below the seat minimum means no purchased seats.\n        await sutProvider.GetDependency<IProviderPlanRepository>().Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 0 && pPlan.AllocatedSeats == 80));\n    }\n\n    #endregion\n\n    #region SeatAdjustmentResultsInPurchase\n\n    [Theory, BitAutoData]\n    public async Task SeatAdjustmentResultsInPurchase_BelowToAbove_True(\n        Provider provider,\n        PlanType planType,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns([\n            new ProviderPlan\n            {\n                PlanType = planType,\n                SeatMinimum = 10,\n                AllocatedSeats = 0,\n                PurchasedSeats = 0\n            }\n        ]);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(MockPlans.Get(planType));\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(\n        [\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = MockPlans.Get(planType).Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 5\n            }\n        ]);\n\n        const int seatAdjustment = 10;\n\n        var result = await sutProvider.Sut.SeatAdjustmentResultsInPurchase(\n            provider,\n            planType,\n            seatAdjustment);\n\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SeatAdjustmentResultsInPurchase_AboveToFurtherAbove_True(\n        Provider provider,\n        PlanType planType,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns([\n            new ProviderPlan\n            {\n                PlanType = planType,\n                SeatMinimum = 10,\n                AllocatedSeats = 0,\n                PurchasedSeats = 5\n            }\n        ]);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(MockPlans.Get(planType));\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(\n        [\n            new ProviderOrganizationOrganizationDetails\n            {\n                Plan = MockPlans.Get(planType).Name,\n                Status = OrganizationStatusType.Managed,\n                Seats = 15\n            }\n        ]);\n\n        const int seatAdjustment = 5;\n\n        var result = await sutProvider.Sut.SeatAdjustmentResultsInPurchase(\n            provider,\n            planType,\n            seatAdjustment);\n\n        Assert.True(result);\n    }\n\n    #endregion\n\n    #region SetupCustomer\n\n    [Theory, BitAutoData]\n    public async Task SetupCustomer_NullPaymentMethod_ThrowsNullReferenceException(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider,\n        BillingAddress billingAddress)\n    {\n        await Assert.ThrowsAsync<NullReferenceException>(() =>\n            sutProvider.Sut.SetupCustomer(provider, null, billingAddress));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupCustomer_WithBankAccount_Error_Reverts(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider,\n        BillingAddress billingAddress)\n    {\n        provider.Name = \"MSP\";\n        billingAddress.Country = \"AD\";\n        billingAddress.TaxId = new TaxID(\"es_nif\", \"12345678Z\");\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = \"token\" };\n\n        stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n            options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([\n            new SetupIntent { Id = \"setup_intent_id\" }\n        ]);\n\n        stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>\n                o.Address.Country == billingAddress.Country &&\n                o.Address.PostalCode == billingAddress.PostalCode &&\n                o.Address.Line1 == billingAddress.Line1 &&\n                o.Address.Line2 == billingAddress.Line2 &&\n                o.Address.City == billingAddress.City &&\n                o.Address.State == billingAddress.State &&\n                o.Description == provider.DisplayBusinessName() &&\n                o.Email == provider.BillingEmail &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&\n                o.Metadata[\"region\"] == \"\" &&\n                o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&\n                o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))\n            .Throws<StripeException>();\n\n        await Assert.ThrowsAsync<StripeException>(() =>\n            sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));\n\n        await stripeAdapter.Received(1).CancelSetupIntentAsync(\"setup_intent_id\", Arg.Is<SetupIntentCancelOptions>(options =>\n            options.CancellationReason == \"abandoned\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupCustomer_WithPayPal_Error_Reverts(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider,\n        BillingAddress billingAddress)\n    {\n        provider.Name = \"MSP\";\n        billingAddress.Country = \"AD\";\n        billingAddress.TaxId = new TaxID(\"es_nif\", \"12345678Z\");\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = \"token\" };\n\n        sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)\n            .Returns(\"braintree_customer_id\");\n\n        stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>\n                o.Address.Country == billingAddress.Country &&\n                o.Address.PostalCode == billingAddress.PostalCode &&\n                o.Address.Line1 == billingAddress.Line1 &&\n                o.Address.Line2 == billingAddress.Line2 &&\n                o.Address.City == billingAddress.City &&\n                o.Address.State == billingAddress.State &&\n                o.Description == provider.DisplayBusinessName() &&\n                o.Email == provider.BillingEmail &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&\n                o.Metadata[\"region\"] == \"\" &&\n                o.Metadata[\"btCustomerId\"] == \"braintree_customer_id\" &&\n                o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&\n                o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))\n            .Throws<StripeException>();\n\n        await Assert.ThrowsAsync<StripeException>(() =>\n            sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));\n\n        await sutProvider.GetDependency<IBraintreeGateway>().Customer.Received(1).DeleteAsync(\"braintree_customer_id\");\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupCustomer_WithBankAccount_Success(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider,\n        BillingAddress billingAddress)\n    {\n        provider.Name = \"MSP\";\n        billingAddress.Country = \"AD\";\n        billingAddress.TaxId = new TaxID(\"es_nif\", \"12345678Z\");\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        var expected = new Customer\n        {\n            Id = \"customer_id\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = \"token\" };\n\n        stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n            options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([\n            new SetupIntent { Id = \"setup_intent_id\" }\n        ]);\n\n        stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>\n                o.Address.Country == billingAddress.Country &&\n                o.Address.PostalCode == billingAddress.PostalCode &&\n                o.Address.Line1 == billingAddress.Line1 &&\n                o.Address.Line2 == billingAddress.Line2 &&\n                o.Address.City == billingAddress.City &&\n                o.Address.State == billingAddress.State &&\n                o.Description == provider.DisplayBusinessName() &&\n                o.Email == provider.BillingEmail &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&\n                o.Metadata[\"region\"] == \"\" &&\n                o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&\n                o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))\n            .Returns(expected);\n\n        var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);\n\n        Assert.Equivalent(expected, actual);\n\n        await stripeAdapter.Received(1).UpdateSetupIntentAsync(\"setup_intent_id\",\n            Arg.Is<SetupIntentUpdateOptions>(options => options.Customer == expected.Id));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupCustomer_WithPayPal_Success(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider,\n        BillingAddress billingAddress)\n    {\n        provider.Name = \"MSP\";\n        billingAddress.Country = \"AD\";\n        billingAddress.TaxId = new TaxID(\"es_nif\", \"12345678Z\");\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        var expected = new Customer\n        {\n            Id = \"customer_id\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = \"token\" };\n\n        sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)\n            .Returns(\"braintree_customer_id\");\n\n        stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>\n                o.Address.Country == billingAddress.Country &&\n                o.Address.PostalCode == billingAddress.PostalCode &&\n                o.Address.Line1 == billingAddress.Line1 &&\n                o.Address.Line2 == billingAddress.Line2 &&\n                o.Address.City == billingAddress.City &&\n                o.Address.State == billingAddress.State &&\n                o.Description == provider.DisplayBusinessName() &&\n                o.Email == provider.BillingEmail &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&\n                o.Metadata[\"region\"] == \"\" &&\n                o.Metadata[\"btCustomerId\"] == \"braintree_customer_id\" &&\n                o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&\n                o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))\n            .Returns(expected);\n\n        var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);\n\n        Assert.Equivalent(expected, actual);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupCustomer_WithCard_Success(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider,\n        BillingAddress billingAddress)\n    {\n        provider.Name = \"MSP\";\n        billingAddress.Country = \"AD\";\n        billingAddress.TaxId = new TaxID(\"es_nif\", \"12345678Z\");\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        var expected = new Customer\n        {\n            Id = \"customer_id\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = \"token\" };\n\n        stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>\n                o.Address.Country == billingAddress.Country &&\n                o.Address.PostalCode == billingAddress.PostalCode &&\n                o.Address.Line1 == billingAddress.Line1 &&\n                o.Address.Line2 == billingAddress.Line2 &&\n                o.Address.City == billingAddress.City &&\n                o.Address.State == billingAddress.State &&\n                o.Description == provider.DisplayBusinessName() &&\n                o.Email == provider.BillingEmail &&\n                o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&\n                o.Metadata[\"region\"] == \"\" &&\n                o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&\n                o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))\n            .Returns(expected);\n\n        var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);\n\n        Assert.Equivalent(expected, actual);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupCustomer_WithCard_ReverseCharge_Success(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider,\n        BillingAddress billingAddress)\n    {\n        provider.Name = \"MSP\";\n        billingAddress.Country = \"FR\"; // Non-US country to trigger reverse charge\n        billingAddress.TaxId = new TaxID(\"fr_siren\", \"123456789\");\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        var expected = new Customer\n        {\n            Id = \"customer_id\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = \"token\" };\n\n        stripeAdapter.CreateCustomerAsync(Arg.Is<CustomerCreateOptions>(o =>\n                o.Address.Country == billingAddress.Country &&\n                o.Address.PostalCode == billingAddress.PostalCode &&\n                o.Address.Line1 == billingAddress.Line1 &&\n                o.Address.Line2 == billingAddress.Line2 &&\n                o.Address.City == billingAddress.City &&\n                o.Address.State == billingAddress.State &&\n                o.Description == provider.DisplayBusinessName() &&\n                o.Email == provider.BillingEmail &&\n                o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&\n                o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&\n                o.Metadata[\"region\"] == \"\" &&\n                o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&\n                o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value &&\n                o.TaxExempt == StripeConstants.TaxExempt.Reverse))\n            .Returns(expected);\n\n        var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);\n\n        Assert.Equivalent(expected, actual);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupCustomer_WithInvalidTaxId_ThrowsBadRequestException(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider,\n        BillingAddress billingAddress)\n    {\n        provider.Name = \"MSP\";\n        billingAddress.Country = \"AD\";\n        billingAddress.TaxId = new TaxID(\"es_nif\", \"invalid_tax_id\");\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = \"token\" };\n\n        stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>())\n            .Throws(new StripeException(\"Invalid tax ID\") { StripeError = new StripeError { Code = \"tax_id_invalid\" } });\n\n        var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));\n\n        Assert.Equal(\"Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.\", actual.Message);\n    }\n\n    #endregion\n\n    #region SetupSubscription\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_NullProvider_ThrowsArgumentNullException(\n        SutProvider<ProviderBillingService> sutProvider) =>\n        await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.SetupSubscription(null));\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_NoProviderPlans_ContactSupport(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider)\n    {\n        provider.GatewaySubscriptionId = null;\n\n        sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider).Returns(new Customer\n        {\n            Id = \"customer_id\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        });\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)\n            .Returns(new List<ProviderPlan>());\n\n        await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));\n\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_NoProviderTeamsPlan_ContactSupport(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider)\n    {\n        provider.GatewaySubscriptionId = null;\n\n        sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider).Returns(new Customer\n        {\n            Id = \"customer_id\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        });\n\n        var providerPlans = new List<ProviderPlan> { new() { PlanType = PlanType.EnterpriseMonthly } };\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)\n            .Returns(providerPlans);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.EnterpriseMonthly)\n            .Returns(MockPlans.Get(PlanType.EnterpriseMonthly));\n\n        await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));\n\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_NoProviderEnterprisePlan_ContactSupport(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider)\n    {\n        provider.GatewaySubscriptionId = null;\n\n        sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider).Returns(new Customer\n        {\n            Id = \"customer_id\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        });\n\n        var providerPlans = new List<ProviderPlan> { new() { PlanType = PlanType.TeamsMonthly } };\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)\n            .Returns(providerPlans);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly)\n            .Returns(MockPlans.Get(PlanType.TeamsMonthly));\n\n        await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));\n\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_SubscriptionIncomplete_ThrowsBillingException(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider)\n    {\n        provider.GatewaySubscriptionId = null;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(\n                provider,\n                Arg.Is<CustomerGetOptions>(p => p.Expand.Contains(\"tax\") || p.Expand.Contains(\"tax_ids\")))\n            .Returns(new Customer\n            {\n                Id = \"customer_id\",\n                Address = new Address { Country = \"US\" }\n            });\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.TeamsMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.EnterpriseMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)\n            .Returns(providerPlans);\n\n        sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>())\n            .Returns(\n                new Subscription { Id = \"subscription_id\", Status = StripeConstants.SubscriptionStatus.Incomplete });\n\n        await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_SendInvoice_Succeeds(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider)\n    {\n        provider.Type = ProviderType.Msp;\n        provider.GatewaySubscriptionId = null;\n\n        var customer = new Customer\n        {\n            Id = \"customer_id\",\n            Address = new Address { Country = \"US\" }\n        };\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(\n                provider,\n                Arg.Is<CustomerGetOptions>(p => p.Expand.Contains(\"tax\") || p.Expand.Contains(\"tax_ids\"))).Returns(customer);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.TeamsMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.EnterpriseMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)\n            .Returns(providerPlans);\n\n        var expected = new Subscription { Id = \"subscription_id\", Status = StripeConstants.SubscriptionStatus.Active };\n\n        sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(\n            sub =>\n                sub.AutomaticTax.Enabled == true &&\n                sub.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&\n                sub.Customer == \"customer_id\" &&\n                sub.DaysUntilDue == 30 &&\n                sub.Items.Count == 2 &&\n                sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&\n                sub.Items.ElementAt(0).Quantity == 100 &&\n                sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&\n                sub.Items.ElementAt(1).Quantity == 100 &&\n                sub.Metadata[\"providerId\"] == provider.Id.ToString() &&\n                sub.OffSession == true &&\n                sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations)).Returns(expected);\n\n        var actual = await sutProvider.Sut.SetupSubscription(provider);\n\n        Assert.Equivalent(expected, actual);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_ChargeAutomatically_HasCard_Succeeds(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider)\n    {\n        provider.Type = ProviderType.Msp;\n        provider.GatewaySubscriptionId = null;\n\n        var customer = new Customer\n        {\n            Id = \"customer_id\",\n            Address = new Address { Country = \"US\" },\n            InvoiceSettings = new CustomerInvoiceSettings\n            {\n                DefaultPaymentMethodId = \"pm_123\"\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(\n                provider,\n                Arg.Is<CustomerGetOptions>(p => p.Expand.Contains(\"tax\") || p.Expand.Contains(\"tax_ids\"))).Returns(customer);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.TeamsMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.EnterpriseMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)\n            .Returns(providerPlans);\n\n        var expected = new Subscription { Id = \"subscription_id\", Status = StripeConstants.SubscriptionStatus.Active };\n\n\n        sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(\n            sub =>\n                sub.AutomaticTax.Enabled == true &&\n                sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&\n                sub.Customer == \"customer_id\" &&\n                sub.DaysUntilDue == null &&\n                sub.Items.Count == 2 &&\n                sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&\n                sub.Items.ElementAt(0).Quantity == 100 &&\n                sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&\n                sub.Items.ElementAt(1).Quantity == 100 &&\n                sub.Metadata[\"providerId\"] == provider.Id.ToString() &&\n                sub.OffSession == true &&\n                sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&\n                sub.TrialPeriodDays == 14)).Returns(expected);\n\n        var actual = await sutProvider.Sut.SetupSubscription(provider);\n\n        Assert.Equivalent(expected, actual);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_ChargeAutomatically_HasBankAccount_Succeeds(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider)\n    {\n        provider.Type = ProviderType.Msp;\n        provider.GatewaySubscriptionId = null;\n\n        var customer = new Customer\n        {\n            Id = \"customer_id\",\n            Address = new Address { Country = \"US\" },\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(\n                provider,\n                Arg.Is<CustomerGetOptions>(p => p.Expand.Contains(\"tax\") || p.Expand.Contains(\"tax_ids\"))).Returns(customer);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.TeamsMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.EnterpriseMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)\n            .Returns(providerPlans);\n\n        var expected = new Subscription { Id = \"subscription_id\", Status = StripeConstants.SubscriptionStatus.Active };\n\n        sutProvider.GetDependency<IStripeAdapter>().ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n            options.Customer == customer.Id &&\n            options.Expand.Contains(\"data.payment_method\"))).Returns([\n            new SetupIntent\n            {\n                Id = \"seti_123\",\n                Status = \"requires_action\",\n                NextAction = new SetupIntentNextAction\n                {\n                    VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()\n                },\n                PaymentMethod = new PaymentMethod\n                {\n                    UsBankAccount = new PaymentMethodUsBankAccount()\n                }\n            }\n        ]);\n\n        sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(\n            sub =>\n                sub.AutomaticTax.Enabled == true &&\n                sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&\n                sub.Customer == \"customer_id\" &&\n                sub.DaysUntilDue == null &&\n                sub.Items.Count == 2 &&\n                sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&\n                sub.Items.ElementAt(0).Quantity == 100 &&\n                sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&\n                sub.Items.ElementAt(1).Quantity == 100 &&\n                sub.Metadata[\"providerId\"] == provider.Id.ToString() &&\n                sub.OffSession == true &&\n                sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&\n                sub.TrialPeriodDays == 14)).Returns(expected);\n\n        var actual = await sutProvider.Sut.SetupSubscription(provider);\n\n        Assert.Equivalent(expected, actual);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_ChargeAutomatically_HasPayPal_Succeeds(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider)\n    {\n        provider.Type = ProviderType.Msp;\n        provider.GatewaySubscriptionId = null;\n\n        var customer = new Customer\n        {\n            Id = \"customer_id\",\n            Address = new Address\n            {\n                Country = \"US\"\n            },\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>\n            {\n                [\"btCustomerId\"] = \"braintree_customer_id\"\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(\n                provider,\n                Arg.Is<CustomerGetOptions>(p => p.Expand.Contains(\"tax\") || p.Expand.Contains(\"tax_ids\"))).Returns(customer);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.TeamsMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.EnterpriseMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)\n            .Returns(providerPlans);\n\n        var expected = new Subscription { Id = \"subscription_id\", Status = StripeConstants.SubscriptionStatus.Active };\n\n\n        sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(\n            sub =>\n                sub.AutomaticTax.Enabled == true &&\n                sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&\n                sub.Customer == \"customer_id\" &&\n                sub.DaysUntilDue == null &&\n                sub.Items.Count == 2 &&\n                sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&\n                sub.Items.ElementAt(0).Quantity == 100 &&\n                sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&\n                sub.Items.ElementAt(1).Quantity == 100 &&\n                sub.Metadata[\"providerId\"] == provider.Id.ToString() &&\n                sub.OffSession == true &&\n                sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&\n                sub.TrialPeriodDays == 14)).Returns(expected);\n\n        var actual = await sutProvider.Sut.SetupSubscription(provider);\n\n        Assert.Equivalent(expected, actual);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetupSubscription_ReverseCharge_Succeeds(\n        SutProvider<ProviderBillingService> sutProvider,\n        Provider provider)\n    {\n        provider.Type = ProviderType.Msp;\n        provider.GatewaySubscriptionId = null;\n\n        var customer = new Customer\n        {\n            Id = \"customer_id\",\n            Address = new Address { Country = \"CA\" },\n            InvoiceSettings = new CustomerInvoiceSettings\n            {\n                DefaultPaymentMethodId = \"pm_123\"\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(\n                provider,\n                Arg.Is<CustomerGetOptions>(p => p.Expand.Contains(\"tax\") || p.Expand.Contains(\"tax_ids\"))).Returns(customer);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.TeamsMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.EnterpriseMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 0\n            }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)\n            .Returns(providerPlans);\n\n        var expected = new Subscription { Id = \"subscription_id\", Status = StripeConstants.SubscriptionStatus.Active };\n\n\n        sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(\n            sub =>\n                sub.AutomaticTax.Enabled == true &&\n                sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically &&\n                sub.Customer == \"customer_id\" &&\n                sub.DaysUntilDue == null &&\n                sub.Items.Count == 2 &&\n                sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams &&\n                sub.Items.ElementAt(0).Quantity == 100 &&\n                sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise &&\n                sub.Items.ElementAt(1).Quantity == 100 &&\n                sub.Metadata[\"providerId\"] == provider.Id.ToString() &&\n                sub.OffSession == true &&\n                sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&\n                sub.TrialPeriodDays == 14)).Returns(expected);\n\n        var actual = await sutProvider.Sut.SetupSubscription(provider);\n\n        Assert.Equivalent(expected, actual);\n    }\n\n    #endregion\n\n    #region UpdateSeatMinimums\n\n    [Theory, BitAutoData]\n    public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        var command = new UpdateProviderSeatMinimumsCommand(\n            provider,\n            [\n                (PlanType.TeamsMonthly, -10),\n                (PlanType.EnterpriseMonthly, 50)\n            ]);\n\n        // Act\n        var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(command));\n\n        // Assert\n        Assert.Equal(\"Provider seat minimums must be at least 0.\", actual.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.Msp;\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();\n\n        const string enterpriseLineItemId = \"enterprise_line_item_id\";\n        const string teamsLineItemId = \"teams_line_item_id\";\n\n        var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n        var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Id = enterpriseLineItemId,\n                        Price = new Price { Id = enterprisePriceId }\n                    },\n                    new SubscriptionItem\n                    {\n                        Id = teamsLineItemId,\n                        Price = new Price { Id = teamsPriceId }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new() { PlanType = PlanType.EnterpriseMonthly, SeatMinimum = 50, PurchasedSeats = 0, AllocatedSeats = 20 },\n            new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);\n\n        var command = new UpdateProviderSeatMinimumsCommand(\n            provider,\n            [\n                (PlanType.EnterpriseMonthly, 30),\n                (PlanType.TeamsMonthly, 20)\n            ]);\n\n        // Act\n        await sutProvider.Sut.UpdateSeatMinimums(command);\n\n        // Assert\n        await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30));\n\n        await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly && providerPlan.SeatMinimum == 20 && providerPlan.PurchasedSeats == 5));\n\n        await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(\n                options =>\n                    options.Items.Count == 2 &&\n                    options.Items.ElementAt(0).Id == enterpriseLineItemId &&\n                    options.Items.ElementAt(0).Quantity == 30 &&\n                    options.Items.ElementAt(1).Id == teamsLineItemId &&\n                    options.Items.ElementAt(1).Quantity == 25));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedLowerThanIncomingMinimum_SyncsStripeWithNewSeatMinimum(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.Msp;\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();\n\n        const string enterpriseLineItemId = \"enterprise_line_item_id\";\n        const string teamsLineItemId = \"teams_line_item_id\";\n\n        var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n        var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Id = enterpriseLineItemId,\n                        Price = new Price { Id = enterprisePriceId }\n                    },\n                    new SubscriptionItem\n                    {\n                        Id = teamsLineItemId,\n                        Price = new Price { Id = teamsPriceId }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new() { PlanType = PlanType.EnterpriseMonthly, SeatMinimum = 50, PurchasedSeats = 0, AllocatedSeats = 40 },\n            new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 15 }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);\n\n        var command = new UpdateProviderSeatMinimumsCommand(\n            provider,\n            [\n                (PlanType.EnterpriseMonthly, 70),\n                (PlanType.TeamsMonthly, 50)\n            ]);\n\n        // Act\n        await sutProvider.Sut.UpdateSeatMinimums(command);\n\n        // Assert\n        await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));\n\n        await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly && providerPlan.SeatMinimum == 50));\n\n        await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(\n                options =>\n                    options.Items.Count == 2 &&\n                    options.Items.ElementAt(0).Id == enterpriseLineItemId &&\n                    options.Items.ElementAt(0).Quantity == 70 &&\n                    options.Items.ElementAt(1).Id == teamsLineItemId &&\n                    options.Items.ElementAt(1).Quantity == 50));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateSeatMinimums_PurchasedSeats_NewMinimumLessThanTotal_UpdatesPurchasedSeats(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.Msp;\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();\n\n        const string enterpriseLineItemId = \"enterprise_line_item_id\";\n        const string teamsLineItemId = \"teams_line_item_id\";\n\n        var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n        var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Id = enterpriseLineItemId,\n                        Price = new Price { Id = enterprisePriceId }\n                    },\n                    new SubscriptionItem\n                    {\n                        Id = teamsLineItemId,\n                        Price = new Price { Id = teamsPriceId }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new() { PlanType = PlanType.EnterpriseMonthly, SeatMinimum = 50, PurchasedSeats = 20 },\n            new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);\n\n        var command = new UpdateProviderSeatMinimumsCommand(\n            provider,\n            [\n                (PlanType.EnterpriseMonthly, 60),\n                (PlanType.TeamsMonthly, 60)\n            ]);\n\n        // Act\n        await sutProvider.Sut.UpdateSeatMinimums(command);\n\n        // Assert\n        await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));\n\n        await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));\n\n        await stripeAdapter.DidNotReceiveWithAnyArgs()\n            .UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateSeatMinimums_PurchasedSeats_NewMinimumGreaterThanTotal_ClearsPurchasedSeats_SyncsStripeWithNewSeatMinimum(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.Msp;\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();\n\n        const string enterpriseLineItemId = \"enterprise_line_item_id\";\n        const string teamsLineItemId = \"teams_line_item_id\";\n\n        var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n        var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Id = enterpriseLineItemId,\n                        Price = new Price { Id = enterprisePriceId }\n                    },\n                    new SubscriptionItem\n                    {\n                        Id = teamsLineItemId,\n                        Price = new Price { Id = teamsPriceId }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new() { PlanType = PlanType.EnterpriseMonthly, SeatMinimum = 50, PurchasedSeats = 20 },\n            new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);\n\n        var command = new UpdateProviderSeatMinimumsCommand(\n            provider,\n            [\n                (PlanType.EnterpriseMonthly, 80),\n                (PlanType.TeamsMonthly, 80)\n            ]);\n\n        // Act\n        await sutProvider.Sut.UpdateSeatMinimums(command);\n\n        // Assert\n        await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));\n\n        await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));\n\n        await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(\n                options =>\n                    options.Items.Count == 2 &&\n                    options.Items.ElementAt(0).Id == enterpriseLineItemId &&\n                    options.Items.ElementAt(0).Quantity == 80 &&\n                    options.Items.ElementAt(1).Id == teamsLineItemId &&\n                    options.Items.ElementAt(1).Quantity == 80));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateSeatMinimums_SinglePlanTypeUpdate_Succeeds(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Type = ProviderType.Msp;\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();\n\n        const string enterpriseLineItemId = \"enterprise_line_item_id\";\n        const string teamsLineItemId = \"teams_line_item_id\";\n\n        var enterprisePriceId = MockPlans.Get(PlanType.EnterpriseMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n        var teamsPriceId = MockPlans.Get(PlanType.TeamsMonthly).PasswordManager.StripeProviderPortalSeatPlanId;\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Id = enterpriseLineItemId,\n                        Price = new Price { Id = enterprisePriceId }\n                    },\n                    new SubscriptionItem\n                    {\n                        Id = teamsLineItemId,\n                        Price = new Price { Id = teamsPriceId }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>().GetSubscriptionOrThrow(provider).Returns(subscription);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new() { PlanType = PlanType.EnterpriseMonthly, SeatMinimum = 50, PurchasedSeats = 0 },\n            new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0 }\n        };\n\n        foreach (var plan in providerPlans)\n        {\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)\n                .Returns(MockPlans.Get(plan.PlanType));\n        }\n\n        providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);\n\n        var command = new UpdateProviderSeatMinimumsCommand(\n            provider,\n            [\n                (PlanType.EnterpriseMonthly, 70),\n                (PlanType.TeamsMonthly, 30)\n            ]);\n\n        // Act\n        await sutProvider.Sut.UpdateSeatMinimums(command);\n\n        // Assert\n        await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));\n\n        await providerPlanRepository.DidNotReceive().ReplaceAsync(Arg.Is<ProviderPlan>(\n            providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly));\n\n        await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(\n                options =>\n                    options.Items.Count == 1 &&\n                    options.Items.ElementAt(0).Id == enterpriseLineItemId &&\n                    options.Items.ElementAt(0).Quantity == 70));\n    }\n\n    #endregion\n\n    #region UpdateProviderNameAndEmail\n\n    [Theory, BitAutoData]\n    public async Task UpdateProviderNameAndEmail_NullGatewayCustomerId_LogsWarningAndReturns(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.GatewayCustomerId = null;\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateProviderNameAndEmail(provider);\n\n        // Assert\n        await stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateProviderNameAndEmail_EmptyGatewayCustomerId_LogsWarningAndReturns(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.GatewayCustomerId = \"\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateProviderNameAndEmail(provider);\n\n        // Assert\n        await stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateProviderNameAndEmail_NullProviderName_LogsWarningAndReturns(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Name = null;\n        provider.GatewayCustomerId = \"cus_test123\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateProviderNameAndEmail(provider);\n\n        // Assert\n        await stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateProviderNameAndEmail_EmptyProviderName_LogsWarningAndReturns(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Name = \"\";\n        provider.GatewayCustomerId = \"cus_test123\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateProviderNameAndEmail(provider);\n\n        // Assert\n        await stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateProviderNameAndEmail_ValidProvider_CallsStripeWithCorrectParameters(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Name = \"Test Provider\";\n        provider.BillingEmail = \"billing@test.com\";\n        provider.GatewayCustomerId = \"cus_test123\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateProviderNameAndEmail(provider);\n\n        // Assert\n        await stripeAdapter.Received(1).UpdateCustomerAsync(\n            provider.GatewayCustomerId,\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.Email == provider.BillingEmail &&\n                options.Description == provider.Name &&\n                options.InvoiceSettings.CustomFields.Count == 1 &&\n                options.InvoiceSettings.CustomFields[0].Name == \"Provider\" &&\n                options.InvoiceSettings.CustomFields[0].Value == provider.Name));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateProviderNameAndEmail_LongProviderName_UsesFullName(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        var longName = new string('A', 50); // 50 characters\n        provider.Name = longName;\n        provider.BillingEmail = \"billing@test.com\";\n        provider.GatewayCustomerId = \"cus_test123\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateProviderNameAndEmail(provider);\n\n        // Assert\n        await stripeAdapter.Received(1).UpdateCustomerAsync(\n            provider.GatewayCustomerId,\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.InvoiceSettings.CustomFields[0].Value == longName));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateProviderNameAndEmail_NullBillingEmail_UpdatesWithNull(\n        Provider provider,\n        SutProvider<ProviderBillingService> sutProvider)\n    {\n        // Arrange\n        provider.Name = \"Test Provider\";\n        provider.BillingEmail = null;\n        provider.GatewayCustomerId = \"cus_test123\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateProviderNameAndEmail(provider);\n\n        // Assert\n        await stripeAdapter.Received(1).UpdateCustomerAsync(\n            provider.GatewayCustomerId,\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.Email == null &&\n                options.Description == provider.Name));\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderPriceAdapterTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Providers.Services;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.Billing.Providers;\n\npublic class ProviderPriceAdapterTests\n{\n    [Theory]\n    [InlineData(\"password-manager-provider-portal-enterprise-monthly-2024\", PlanType.EnterpriseMonthly)]\n    [InlineData(\"password-manager-provider-portal-teams-monthly-2024\", PlanType.TeamsMonthly)]\n    public void GetPriceId_MSP_Legacy_Succeeds(string priceId, PlanType planType)\n    {\n        var provider = new Provider\n        {\n            Id = Guid.NewGuid(),\n            Type = ProviderType.Msp\n        };\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = priceId } }\n                ]\n            }\n        };\n\n        var result = ProviderPriceAdapter.GetPriceId(provider, subscription, planType);\n\n        Assert.Equal(result, priceId);\n    }\n\n    [Theory]\n    [InlineData(\"provider-portal-enterprise-monthly-2025\", PlanType.EnterpriseMonthly)]\n    [InlineData(\"provider-portal-teams-monthly-2025\", PlanType.TeamsMonthly)]\n    public void GetPriceId_MSP_Active_Succeeds(string priceId, PlanType planType)\n    {\n        var provider = new Provider\n        {\n            Id = Guid.NewGuid(),\n            Type = ProviderType.Msp\n        };\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = priceId } }\n                ]\n            }\n        };\n\n        var result = ProviderPriceAdapter.GetPriceId(provider, subscription, planType);\n\n        Assert.Equal(result, priceId);\n    }\n\n    [Theory]\n    [InlineData(\"password-manager-provider-portal-enterprise-annually-2024\", PlanType.EnterpriseAnnually)]\n    [InlineData(\"password-manager-provider-portal-enterprise-monthly-2024\", PlanType.EnterpriseMonthly)]\n    public void GetPriceId_BusinessUnit_Legacy_Succeeds(string priceId, PlanType planType)\n    {\n        var provider = new Provider\n        {\n            Id = Guid.NewGuid(),\n            Type = ProviderType.BusinessUnit\n        };\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = priceId } }\n                ]\n            }\n        };\n\n        var result = ProviderPriceAdapter.GetPriceId(provider, subscription, planType);\n\n        Assert.Equal(result, priceId);\n    }\n\n    [Theory]\n    [InlineData(\"business-unit-portal-enterprise-annually-2025\", PlanType.EnterpriseAnnually)]\n    [InlineData(\"business-unit-portal-enterprise-monthly-2025\", PlanType.EnterpriseMonthly)]\n    public void GetPriceId_BusinessUnit_Active_Succeeds(string priceId, PlanType planType)\n    {\n        var provider = new Provider\n        {\n            Id = Guid.NewGuid(),\n            Type = ProviderType.BusinessUnit\n        };\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = priceId } }\n                ]\n            }\n        };\n\n        var result = ProviderPriceAdapter.GetPriceId(provider, subscription, planType);\n\n        Assert.Equal(result, priceId);\n    }\n\n    [Theory]\n    [InlineData(\"provider-portal-enterprise-monthly-2025\", PlanType.EnterpriseMonthly)]\n    [InlineData(\"provider-portal-teams-monthly-2025\", PlanType.TeamsMonthly)]\n    public void GetActivePriceId_MSP_Succeeds(string priceId, PlanType planType)\n    {\n        var provider = new Provider\n        {\n            Id = Guid.NewGuid(),\n            Type = ProviderType.Msp\n        };\n\n        var result = ProviderPriceAdapter.GetActivePriceId(provider, planType);\n\n        Assert.Equal(result, priceId);\n    }\n\n    [Theory]\n    [InlineData(\"business-unit-portal-enterprise-annually-2025\", PlanType.EnterpriseAnnually)]\n    [InlineData(\"business-unit-portal-enterprise-monthly-2025\", PlanType.EnterpriseMonthly)]\n    public void GetActivePriceId_BusinessUnit_Succeeds(string priceId, PlanType planType)\n    {\n        var provider = new Provider\n        {\n            Id = Guid.NewGuid(),\n            Type = ProviderType.BusinessUnit\n        };\n\n        var result = ProviderPriceAdapter.GetActivePriceId(provider, planType);\n\n        Assert.Equal(result, priceId);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/Billing/Tax/TaxServiceTests.cs",
    "content": "﻿using Bit.Core.Billing.Tax.Services.Implementations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.Billing.Tax;\n\n[SutProviderCustomize]\npublic class TaxServiceTests\n{\n    [Theory]\n    [BitAutoData(\"AD\", \"A-123456-Z\", \"ad_nrt\")]\n    [BitAutoData(\"AD\", \"A123456Z\", \"ad_nrt\")]\n    [BitAutoData(\"AR\", \"20-12345678-9\", \"ar_cuit\")]\n    [BitAutoData(\"AR\", \"20123456789\", \"ar_cuit\")]\n    [BitAutoData(\"AU\", \"01259983598\", \"au_abn\")]\n    [BitAutoData(\"AU\", \"123456789123\", \"au_arn\")]\n    [BitAutoData(\"AT\", \"ATU12345678\", \"eu_vat\")]\n    [BitAutoData(\"BH\", \"123456789012345\", \"bh_vat\")]\n    [BitAutoData(\"BY\", \"123456789\", \"by_tin\")]\n    [BitAutoData(\"BE\", \"BE0123456789\", \"eu_vat\")]\n    [BitAutoData(\"BO\", \"123456789\", \"bo_tin\")]\n    [BitAutoData(\"BR\", \"01.234.456/5432-10\", \"br_cnpj\")]\n    [BitAutoData(\"BR\", \"01234456543210\", \"br_cnpj\")]\n    [BitAutoData(\"BR\", \"123.456.789-87\", \"br_cpf\")]\n    [BitAutoData(\"BR\", \"12345678987\", \"br_cpf\")]\n    [BitAutoData(\"BG\", \"123456789\", \"bg_uic\")]\n    [BitAutoData(\"BG\", \"BG012100705\", \"eu_vat\")]\n    [BitAutoData(\"CA\", \"100728494\", \"ca_bn\")]\n    [BitAutoData(\"CA\", \"123456789RT0001\", \"ca_gst_hst\")]\n    [BitAutoData(\"CA\", \"PST-1234-1234\", \"ca_pst_bc\")]\n    [BitAutoData(\"CA\", \"123456-7\", \"ca_pst_mb\")]\n    [BitAutoData(\"CA\", \"1234567\", \"ca_pst_sk\")]\n    [BitAutoData(\"CA\", \"1234567890TQ1234\", \"ca_qst\")]\n    [BitAutoData(\"CL\", \"11.121.326-1\", \"cl_tin\")]\n    [BitAutoData(\"CL\", \"11121326-1\", \"cl_tin\")]\n    [BitAutoData(\"CL\", \"23.121.326-K\", \"cl_tin\")]\n    [BitAutoData(\"CL\", \"43651326-K\", \"cl_tin\")]\n    [BitAutoData(\"CN\", \"123456789012345678\", \"cn_tin\")]\n    [BitAutoData(\"CN\", \"123456789012345\", \"cn_tin\")]\n    [BitAutoData(\"CO\", \"123.456.789-0\", \"co_nit\")]\n    [BitAutoData(\"CO\", \"1234567890\", \"co_nit\")]\n    [BitAutoData(\"CR\", \"1-234-567890\", \"cr_tin\")]\n    [BitAutoData(\"CR\", \"1234567890\", \"cr_tin\")]\n    [BitAutoData(\"HR\", \"HR12345678912\", \"eu_vat\")]\n    [BitAutoData(\"HR\", \"12345678901\", \"hr_oib\")]\n    [BitAutoData(\"CY\", \"CY12345678X\", \"eu_vat\")]\n    [BitAutoData(\"CZ\", \"CZ12345678\", \"eu_vat\")]\n    [BitAutoData(\"DK\", \"DK12345678\", \"eu_vat\")]\n    [BitAutoData(\"DO\", \"123-4567890-1\", \"do_rcn\")]\n    [BitAutoData(\"DO\", \"12345678901\", \"do_rcn\")]\n    [BitAutoData(\"EC\", \"1234567890001\", \"ec_ruc\")]\n    [BitAutoData(\"EG\", \"123456789\", \"eg_tin\")]\n    [BitAutoData(\"SV\", \"1234-567890-123-4\", \"sv_nit\")]\n    [BitAutoData(\"SV\", \"12345678901234\", \"sv_nit\")]\n    [BitAutoData(\"EE\", \"EE123456789\", \"eu_vat\")]\n    [BitAutoData(\"EU\", \"EU123456789\", \"eu_oss_vat\")]\n    [BitAutoData(\"FI\", \"FI12345678\", \"eu_vat\")]\n    [BitAutoData(\"FR\", \"FR12345678901\", \"eu_vat\")]\n    [BitAutoData(\"GE\", \"123456789\", \"ge_vat\")]\n    [BitAutoData(\"DE\", \"1234567890\", \"de_stn\")]\n    [BitAutoData(\"DE\", \"DE123456789\", \"eu_vat\")]\n    [BitAutoData(\"GR\", \"EL123456789\", \"eu_vat\")]\n    [BitAutoData(\"HK\", \"12345678\", \"hk_br\")]\n    [BitAutoData(\"HU\", \"HU12345678\", \"eu_vat\")]\n    [BitAutoData(\"HU\", \"12345678-1-23\", \"hu_tin\")]\n    [BitAutoData(\"HU\", \"12345678123\", \"hu_tin\")]\n    [BitAutoData(\"IS\", \"123456\", \"is_vat\")]\n    [BitAutoData(\"IN\", \"12ABCDE1234F1Z5\", \"in_gst\")]\n    [BitAutoData(\"IN\", \"12ABCDE3456FGZH\", \"in_gst\")]\n    [BitAutoData(\"ID\", \"012.345.678.9-012.345\", \"id_npwp\")]\n    [BitAutoData(\"ID\", \"0123456789012345\", \"id_npwp\")]\n    [BitAutoData(\"IE\", \"IE1234567A\", \"eu_vat\")]\n    [BitAutoData(\"IE\", \"IE1234567AB\", \"eu_vat\")]\n    [BitAutoData(\"IL\", \"000012345\", \"il_vat\")]\n    [BitAutoData(\"IL\", \"123456789\", \"il_vat\")]\n    [BitAutoData(\"IT\", \"IT12345678901\", \"eu_vat\")]\n    [BitAutoData(\"JP\", \"1234567890123\", \"jp_cn\")]\n    [BitAutoData(\"JP\", \"12345\", \"jp_rn\")]\n    [BitAutoData(\"KZ\", \"123456789012\", \"kz_bin\")]\n    [BitAutoData(\"KE\", \"P000111111A\", \"ke_pin\")]\n    [BitAutoData(\"LV\", \"LV12345678912\", \"eu_vat\")]\n    [BitAutoData(\"LI\", \"CHE123456789\", \"li_uid\")]\n    [BitAutoData(\"LI\", \"12345\", \"li_vat\")]\n    [BitAutoData(\"LT\", \"LT123456789123\", \"eu_vat\")]\n    [BitAutoData(\"LU\", \"LU12345678\", \"eu_vat\")]\n    [BitAutoData(\"MY\", \"12345678\", \"my_frp\")]\n    [BitAutoData(\"MY\", \"C 1234567890\", \"my_itn\")]\n    [BitAutoData(\"MY\", \"C1234567890\", \"my_itn\")]\n    [BitAutoData(\"MY\", \"A12-3456-78912345\", \"my_sst\")]\n    [BitAutoData(\"MY\", \"A12345678912345\", \"my_sst\")]\n    [BitAutoData(\"MT\", \"MT12345678\", \"eu_vat\")]\n    [BitAutoData(\"MX\", \"ABC010203AB9\", \"mx_rfc\")]\n    [BitAutoData(\"MD\", \"1003600\", \"md_vat\")]\n    [BitAutoData(\"MA\", \"12345678\", \"ma_vat\")]\n    [BitAutoData(\"NL\", \"NL123456789B12\", \"eu_vat\")]\n    [BitAutoData(\"NZ\", \"123456789\", \"nz_gst\")]\n    [BitAutoData(\"NG\", \"12345678-0001\", \"ng_tin\")]\n    [BitAutoData(\"NO\", \"123456789MVA\", \"no_vat\")]\n    [BitAutoData(\"NO\", \"1234567\", \"no_voec\")]\n    [BitAutoData(\"OM\", \"OM1234567890\", \"om_vat\")]\n    [BitAutoData(\"PE\", \"12345678901\", \"pe_ruc\")]\n    [BitAutoData(\"PH\", \"123456789012\", \"ph_tin\")]\n    [BitAutoData(\"PL\", \"PL1234567890\", \"eu_vat\")]\n    [BitAutoData(\"PT\", \"PT123456789\", \"eu_vat\")]\n    [BitAutoData(\"RO\", \"RO1234567891\", \"eu_vat\")]\n    [BitAutoData(\"RO\", \"1234567890123\", \"ro_tin\")]\n    [BitAutoData(\"RU\", \"1234567891\", \"ru_inn\")]\n    [BitAutoData(\"RU\", \"123456789\", \"ru_kpp\")]\n    [BitAutoData(\"SA\", \"123456789012345\", \"sa_vat\")]\n    [BitAutoData(\"RS\", \"123456789\", \"rs_pib\")]\n    [BitAutoData(\"SG\", \"M12345678X\", \"sg_gst\")]\n    [BitAutoData(\"SG\", \"123456789F\", \"sg_uen\")]\n    [BitAutoData(\"SK\", \"SK1234567891\", \"eu_vat\")]\n    [BitAutoData(\"SI\", \"SI12345678\", \"eu_vat\")]\n    [BitAutoData(\"SI\", \"12345678\", \"si_tin\")]\n    [BitAutoData(\"ZA\", \"4123456789\", \"za_vat\")]\n    [BitAutoData(\"KR\", \"123-45-67890\", \"kr_brn\")]\n    [BitAutoData(\"KR\", \"1234567890\", \"kr_brn\")]\n    [BitAutoData(\"ES\", \"A12345678\", \"es_cif\")]\n    [BitAutoData(\"ES\", \"ESX1234567X\", \"eu_vat\")]\n    [BitAutoData(\"SE\", \"SE123456789012\", \"eu_vat\")]\n    [BitAutoData(\"CH\", \"CHE-123.456.789 HR\", \"ch_uid\")]\n    [BitAutoData(\"CH\", \"CHE123456789HR\", \"ch_uid\")]\n    [BitAutoData(\"CH\", \"CHE-123.456.789 MWST\", \"ch_vat\")]\n    [BitAutoData(\"CH\", \"CHE123456789MWST\", \"ch_vat\")]\n    [BitAutoData(\"TW\", \"12345678\", \"tw_vat\")]\n    [BitAutoData(\"TH\", \"1234567890123\", \"th_vat\")]\n    [BitAutoData(\"TR\", \"0123456789\", \"tr_tin\")]\n    [BitAutoData(\"UA\", \"123456789\", \"ua_vat\")]\n    [BitAutoData(\"AE\", \"123456789012345\", \"ae_trn\")]\n    [BitAutoData(\"GB\", \"XI123456789\", \"eu_vat\")]\n    [BitAutoData(\"GB\", \"GB123456789\", \"gb_vat\")]\n    [BitAutoData(\"US\", \"12-3456789\", \"us_ein\")]\n    [BitAutoData(\"UY\", \"123456789012\", \"uy_ruc\")]\n    [BitAutoData(\"UZ\", \"123456789\", \"uz_tin\")]\n    [BitAutoData(\"UZ\", \"123456789012\", \"uz_vat\")]\n    [BitAutoData(\"VE\", \"A-12345678-9\", \"ve_rif\")]\n    [BitAutoData(\"VE\", \"A123456789\", \"ve_rif\")]\n    [BitAutoData(\"VN\", \"1234567890\", \"vn_tin\")]\n    public void GetStripeTaxCode_WithValidCountryAndTaxId_ReturnsExpectedTaxIdType(\n        string country,\n        string taxId,\n        string expected,\n        SutProvider<TaxService> sutProvider)\n    {\n        var result = sutProvider.Sut.GetStripeTaxCode(country, taxId);\n\n        Assert.Equal(expected, result);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/Commercial.Core.Test.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Commercial.Core\\Commercial.Core.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\test\\Core.Test\\Core.Test.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectPeopleAccessPoliciesAuthorizationHandlerTests.cs",
    "content": "﻿using System.Reflection;\nusing System.Security.Claims;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class ProjectPeopleAccessPoliciesAuthorizationHandlerTests\n{\n    private static void SetupUserPermission(SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        AccessClientType accessClientType, ProjectPeopleAccessPolicies resource, Guid userId = new(), bool read = true,\n        bool write = true)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)\n            .ReturnsForAnyArgs(\n                (accessClientType, userId));\n        sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(resource.Id, userId, accessClientType)\n            .Returns((read, write));\n    }\n\n    private static void SetupOrganizationUsers(SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectPeopleAccessPolicies resource) =>\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .OrgUsersInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(true);\n\n    private static void SetupGroups(SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectPeopleAccessPolicies resource) =>\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .GroupsInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(true);\n\n    [Fact]\n    public void PeopleAccessPoliciesOperations_OnlyPublicStatic()\n    {\n        var publicStaticFields =\n            typeof(ProjectPeopleAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);\n        var allFields = typeof(ProjectPeopleAccessPoliciesOperations).GetFields();\n        Assert.Equal(publicStaticFields.Length, allFields.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_UnsupportedProjectPeopleAccessPoliciesOperationRequirement_Throws(\n        SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider, ProjectPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new ProjectPeopleAccessPoliciesOperationRequirement();\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)\n            .ReturnsForAnyArgs(\n                (AccessClientType.NoAccessCheck, new Guid()));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider, ProjectPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new ProjectPeopleAccessPoliciesOperationRequirement();\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(AccessClientType clientType,\n        SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider, ProjectPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new ProjectPeopleAccessPoliciesOperationRequirement();\n        SetupUserPermission(sutProvider, clientType, resource);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User)]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    public async Task ReplaceProjectPeople_UserNotInOrg_DoesNotSucceed(AccessClientType accessClient,\n        SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider, ProjectPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal, Guid userId)\n    {\n        var requirement = ProjectPeopleAccessPoliciesOperations.Replace;\n        SetupUserPermission(sutProvider, accessClient, resource, userId);\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .OrgUsersInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(false);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User)]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    public async Task ReplaceProjectPeople_GroupNotInOrg_DoesNotSucceed(AccessClientType accessClient,\n        SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider, ProjectPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal, Guid userId)\n    {\n        var requirement = ProjectPeopleAccessPoliciesOperations.Replace;\n        SetupUserPermission(sutProvider, accessClient, resource, userId);\n        SetupOrganizationUsers(sutProvider, resource);\n\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .GroupsInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId).Returns(false);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User, false, false, false)]\n    [BitAutoData(AccessClientType.User, false, true, true)]\n    [BitAutoData(AccessClientType.User, true, false, false)]\n    [BitAutoData(AccessClientType.User, true, true, true)]\n    [BitAutoData(AccessClientType.NoAccessCheck, false, false, false)]\n    [BitAutoData(AccessClientType.NoAccessCheck, false, true, true)]\n    [BitAutoData(AccessClientType.NoAccessCheck, true, false, false)]\n    [BitAutoData(AccessClientType.NoAccessCheck, true, true, true)]\n    public async Task ReplaceProjectPeople_AccessCheck(AccessClientType accessClient, bool read, bool write,\n        bool expected,\n        SutProvider<ProjectPeopleAccessPoliciesAuthorizationHandler> sutProvider, ProjectPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal, Guid userId)\n    {\n        var requirement = ProjectPeopleAccessPoliciesOperations.Replace;\n        SetupUserPermission(sutProvider, accessClient, resource, userId, read, write);\n        SetupOrganizationUsers(sutProvider, resource);\n        SetupGroups(sutProvider, resource);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests.cs",
    "content": "﻿#nullable enable\nusing System.Reflection;\nusing System.Security.Claims;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests\n{\n    [Fact]\n    public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()\n    {\n        var publicStaticFields =\n            typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);\n        var allFields = typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields();\n        Assert.Equal(publicStaticFields.Length, allFields.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;\n        SetupUserSubstitutes(sutProvider, accessClientType, resource);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_UnsupportedProjectServiceAccountsPoliciesOperationRequirement_Throws(\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new ProjectServiceAccountsAccessPoliciesOperationRequirement();\n        SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck, false, false)]\n    [BitAutoData(AccessClientType.NoAccessCheck, true, false)]\n    [BitAutoData(AccessClientType.User, false, false)]\n    [BitAutoData(AccessClientType.User, true, false)]\n    public async Task Handler_UserHasNoWriteAccessToProject_DoesNotSucceed(\n        AccessClientType accessClientType,\n        bool projectReadAccess,\n        bool projectWriteAccess,\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;\n        SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(resource.ProjectId, userId, accessClientType)\n            .Returns((projectReadAccess, projectWriteAccess));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_ServiceAccountsInDifferentOrganization_DoesNotSucceed(\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;\n        SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(resource.ProjectId, userId, AccessClientType.NoAccessCheck)\n            .Returns((true, true));\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_UserHasAccessToProject_NoCreatesRequested_Success(\n        AccessClientType accessClientType,\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;\n        resource = RemoveAllCreates(resource);\n        SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_UserHasNoAccessToCreateServiceAccounts_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);\n        var accessResult = resource.ServiceAccountAccessPolicyUpdates\n            .Where(x => x.Operation == AccessPolicyOperation.Create)\n            .Select(x => x.AccessPolicy.ServiceAccountId!.Value)\n            .ToDictionary(id => id, _ => (false, false));\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(accessResult);\n\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_AccessResultsPartial_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);\n\n        var accessResult = resource.ServiceAccountAccessPolicyUpdates\n            .Where(x => x.Operation == AccessPolicyOperation.Create)\n            .Select(x => x.AccessPolicy.ServiceAccountId!.Value)\n            .ToDictionary(id => id, _ => (false, false));\n\n        accessResult[accessResult.First().Key] = (true, true);\n        accessResult.Remove(accessResult.Last().Key);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(accessResult);\n\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_UserHasAccessToSomeCreateServiceAccounts_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);\n\n        var accessResult = resource.ServiceAccountAccessPolicyUpdates\n            .Where(x => x.Operation == AccessPolicyOperation.Create)\n            .Select(x => x.AccessPolicy.ServiceAccountId!.Value)\n            .ToDictionary(id => id, _ => (false, false));\n\n        accessResult[accessResult.First().Key] = (true, true);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(accessResult);\n\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_UserHasAccessToAllCreateServiceAccounts_Success(\n        AccessClientType accessClientType,\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);\n\n        var accessResult = resource.ServiceAccountAccessPolicyUpdates\n            .Where(x => x.Operation == AccessPolicyOperation.Create)\n            .Select(x => x.AccessPolicy.ServiceAccountId!.Value)\n            .ToDictionary(id => id, _ => (true, true));\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(accessResult);\n\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    private static void SetupUserSubstitutes(\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        AccessClientType accessClientType,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        Guid userId = new())\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default!, resource.OrganizationId)\n            .ReturnsForAnyArgs((accessClientType, userId));\n    }\n\n    private static void SetupServiceAccountsAccessTest(\n        SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,\n        AccessClientType accessClientType,\n        ProjectServiceAccountsAccessPoliciesUpdates resource,\n        Guid userId = new())\n    {\n        SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);\n\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(resource.ProjectId, userId, accessClientType)\n            .Returns((true, true));\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(true);\n    }\n\n    private static ProjectServiceAccountsAccessPoliciesUpdates AddServiceAccountCreateUpdate(\n        ProjectServiceAccountsAccessPoliciesUpdates resource)\n    {\n        resource.ServiceAccountAccessPolicyUpdates = resource.ServiceAccountAccessPolicyUpdates.Append(\n            new ServiceAccountProjectAccessPolicyUpdate\n            {\n                AccessPolicy = new ServiceAccountProjectAccessPolicy\n                {\n                    ServiceAccountId = Guid.NewGuid(),\n                    GrantedProjectId = resource.ProjectId,\n                    Read = true,\n                    Write = true\n                }\n            });\n        return resource;\n    }\n\n    private static ProjectServiceAccountsAccessPoliciesUpdates RemoveAllCreates(\n        ProjectServiceAccountsAccessPoliciesUpdates resource)\n    {\n        resource.ServiceAccountAccessPolicyUpdates =\n            resource.ServiceAccountAccessPolicyUpdates.Where(x => x.Operation != AccessPolicyOperation.Create);\n        return resource;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/SecretAccessPoliciesUpdatesAuthorizationHandlerTests.cs",
    "content": "﻿using System.Reflection;\nusing System.Security.Claims;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class SecretAccessPoliciesUpdatesAuthorizationHandlerTests\n{\n    [Fact]\n    public void SecretAccessPoliciesOperations_OnlyPublicStatic()\n    {\n        var publicStaticFields =\n            typeof(SecretAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);\n        var allFields = typeof(SecretAccessPoliciesOperations).GetFields();\n        Assert.Equal(publicStaticFields.Length, allFields.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n        SetupUserSubstitutes(sutProvider, accessClientType, resource);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_UnsupportedServiceAccountGrantedPoliciesOperationRequirement_Throws(\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new SecretAccessPoliciesOperationRequirement();\n        SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck, false, false)]\n    [BitAutoData(AccessClientType.NoAccessCheck, true, false)]\n    [BitAutoData(AccessClientType.User, false, false)]\n    [BitAutoData(AccessClientType.User, true, false)]\n    public async Task Handler_CanUpdateAsync_UserHasNoWriteAccessToSecret_DoesNotSucceed(\n        AccessClientType accessClientType,\n        bool readAccess,\n        bool writeAccess,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n        SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretAsync(resource.SecretId, userId, accessClientType)\n            .Returns((readAccess, writeAccess));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(false, false, false)]\n    [BitAutoData(true, false, false)]\n    [BitAutoData(false, true, false)]\n    [BitAutoData(true, true, false)]\n    [BitAutoData(false, false, true)]\n    [BitAutoData(true, false, true)]\n    [BitAutoData(false, true, true)]\n    public async Task Handler_CanUpdateAsync_TargetGranteesNotInSameOrganization_DoesNotSucceed(\n        bool orgUsersInSameOrg,\n        bool groupsInSameOrg,\n        bool serviceAccountsInSameOrg,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n        SetupSameOrganizationRequest(sutProvider, AccessClientType.NoAccessCheck, resource, userId, orgUsersInSameOrg,\n            groupsInSameOrg, serviceAccountsInSameOrg);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(false, false, false)]\n    [BitAutoData(true, false, false)]\n    [BitAutoData(false, true, false)]\n    [BitAutoData(true, true, false)]\n    [BitAutoData(false, false, true)]\n    [BitAutoData(true, false, true)]\n    [BitAutoData(false, true, true)]\n    public async Task Handler_CanUpdateAsync_TargetGranteesNotInSameOrganizationHasZeroRequests_DoesNotSucceed(\n        bool orgUsersCountZero,\n        bool groupsCountZero,\n        bool serviceAccountsCountZero,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n        resource = ClearAccessPolicyUpdate(resource, orgUsersCountZero, groupsCountZero, serviceAccountsCountZero);\n        SetupSameOrganizationRequest(sutProvider, AccessClientType.NoAccessCheck, resource, userId, false, false,\n            false);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanUpdateAsync_NoServiceAccountCreatesRequested_Success(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n\n        resource = RemoveAllServiceAccountCreates(resource);\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanUpdateAsync_NoAccessToTargetServiceAccounts_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n        SetupNoServiceAccountAccess(sutProvider, resource, userId, accessClientType);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanUpdateAsync_ServiceAccountAccessResultsPartial_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n        SetupPartialServiceAccountAccess(sutProvider, resource, userId, accessClientType);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanUpdateAsync_UserHasAccessToSomeServiceAccounts_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n        SetupSomeServiceAccountAccess(sutProvider, resource, userId, accessClientType);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanUpdateAsync_UserHasAccessToAllServiceAccounts_Success(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Updates;\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n        SetupAllServiceAccountAccess(sutProvider, resource, userId, accessClientType);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanCreateAsync_NotCreationOperations_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Create;\n        SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(false, false, false)]\n    [BitAutoData(true, false, false)]\n    [BitAutoData(false, true, false)]\n    [BitAutoData(true, true, false)]\n    [BitAutoData(false, false, true)]\n    [BitAutoData(true, false, true)]\n    [BitAutoData(false, true, true)]\n    public async Task Handler_CanCreateAsync_TargetGranteesNotInSameOrganization_DoesNotSucceed(\n        bool orgUsersInSameOrg,\n        bool groupsInSameOrg,\n        bool serviceAccountsInSameOrg,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Create;\n        resource = SetAllToCreates(resource);\n        SetupSameOrganizationRequest(sutProvider, AccessClientType.NoAccessCheck, resource, userId, orgUsersInSameOrg,\n            groupsInSameOrg, serviceAccountsInSameOrg);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(false, false, false)]\n    [BitAutoData(true, false, false)]\n    [BitAutoData(false, true, false)]\n    [BitAutoData(true, true, false)]\n    [BitAutoData(false, false, true)]\n    [BitAutoData(true, false, true)]\n    [BitAutoData(false, true, true)]\n    public async Task Handler_CanCreateAsync_TargetGranteesNotInSameOrganizationHasZeroRequests_DoesNotSucceed(\n        bool orgUsersCountZero,\n        bool groupsCountZero,\n        bool serviceAccountsCountZero,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Create;\n        resource = SetAllToCreates(resource);\n        resource = ClearAccessPolicyUpdate(resource, orgUsersCountZero, groupsCountZero, serviceAccountsCountZero);\n        SetupSameOrganizationRequest(sutProvider, AccessClientType.NoAccessCheck, resource, userId, false, false,\n            false);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanCreateAsync_NoServiceAccountCreatesRequested_Success(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Create;\n        resource = SetAllToCreates(resource);\n        resource = RemoveAllServiceAccountCreates(resource);\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanCreateAsync_NoAccessToTargetServiceAccounts_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Create;\n        resource = SetAllToCreates(resource);\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n        SetupNoServiceAccountAccess(sutProvider, resource, userId, accessClientType);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanCreateAsync_ServiceAccountAccessResultsPartial_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Create;\n        resource = SetAllToCreates(resource);\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n        SetupPartialServiceAccountAccess(sutProvider, resource, userId, accessClientType);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanCreateAsync_UserHasAccessToSomeServiceAccounts_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Create;\n        resource = SetAllToCreates(resource);\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n        SetupSomeServiceAccountAccess(sutProvider, resource, userId, accessClientType);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_CanCreateAsync_UserHasAccessToAllServiceAccounts_Success(\n        AccessClientType accessClientType,\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretAccessPoliciesOperations.Create;\n        resource = SetAllToCreates(resource);\n        resource = AddServiceAccountCreateUpdate(resource);\n        SetupSameOrganizationRequest(sutProvider, accessClientType, resource, userId);\n        SetupAllServiceAccountAccess(sutProvider, resource, userId, accessClientType);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    private static void SetupNoServiceAccountAccess(\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        AccessClientType accessClientType)\n    {\n        var createServiceAccountIds = resource.ServiceAccountAccessPolicyUpdates\n            .Where(ap => ap.Operation == AccessPolicyOperation.Create)\n            .Select(uap => uap.AccessPolicy.ServiceAccountId!.Value)\n            .ToList();\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(createServiceAccountIds.ToDictionary(id => id, _ => (false, false)));\n    }\n\n    private static void SetupPartialServiceAccountAccess(\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        AccessClientType accessClientType)\n    {\n        var accessResult = resource.ServiceAccountAccessPolicyUpdates\n            .Where(x => x.Operation == AccessPolicyOperation.Create)\n            .Select(x => x.AccessPolicy.ServiceAccountId!.Value)\n            .ToDictionary(id => id, _ => (true, true));\n        accessResult[accessResult.First().Key] = (true, true);\n        accessResult.Remove(accessResult.Last().Key);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(accessResult);\n    }\n\n    private static void SetupSomeServiceAccountAccess(\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        AccessClientType accessClientType)\n    {\n        var accessResult = resource.ServiceAccountAccessPolicyUpdates\n            .Where(x => x.Operation == AccessPolicyOperation.Create)\n            .Select(x => x.AccessPolicy.ServiceAccountId!.Value)\n            .ToDictionary(id => id, _ => (false, false));\n\n        accessResult[accessResult.First().Key] = (true, true);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(accessResult);\n    }\n\n    private static void SetupAllServiceAccountAccess(\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId,\n        AccessClientType accessClientType)\n    {\n        var accessResult = resource.ServiceAccountAccessPolicyUpdates\n            .Where(x => x.Operation == AccessPolicyOperation.Create)\n            .Select(x => x.AccessPolicy.ServiceAccountId!.Value)\n            .ToDictionary(id => id, _ => (true, true));\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(accessResult);\n    }\n\n    private static void SetupUserSubstitutes(\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        AccessClientType accessClientType,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId = new())\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)\n            .ReturnsForAnyArgs((accessClientType, userId));\n    }\n\n    private static void SetupSameOrganizationRequest(\n        SutProvider<SecretAccessPoliciesUpdatesAuthorizationHandler> sutProvider,\n        AccessClientType accessClientType,\n        SecretAccessPoliciesUpdates resource,\n        Guid userId = new(),\n        bool orgUsersInSameOrg = true,\n        bool groupsInSameOrg = true,\n        bool serviceAccountsInSameOrg = true)\n    {\n        SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);\n\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretAsync(resource.SecretId, userId, accessClientType)\n            .Returns((true, true));\n\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .OrgUsersInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(orgUsersInSameOrg);\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .GroupsInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(groupsInSameOrg);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(serviceAccountsInSameOrg);\n    }\n\n    private static SecretAccessPoliciesUpdates RemoveAllServiceAccountCreates(\n        SecretAccessPoliciesUpdates resource)\n    {\n        resource.ServiceAccountAccessPolicyUpdates =\n            resource.ServiceAccountAccessPolicyUpdates.Where(x => x.Operation != AccessPolicyOperation.Create);\n        return resource;\n    }\n\n    private static SecretAccessPoliciesUpdates SetAllToCreates(\n        SecretAccessPoliciesUpdates resource)\n    {\n        resource.UserAccessPolicyUpdates = resource.UserAccessPolicyUpdates.Select(x =>\n        {\n            x.Operation = AccessPolicyOperation.Create;\n            return x;\n        });\n        resource.GroupAccessPolicyUpdates = resource.GroupAccessPolicyUpdates.Select(x =>\n        {\n            x.Operation = AccessPolicyOperation.Create;\n            return x;\n        });\n        resource.ServiceAccountAccessPolicyUpdates = resource.ServiceAccountAccessPolicyUpdates.Select(x =>\n        {\n            x.Operation = AccessPolicyOperation.Create;\n            return x;\n        });\n\n        return resource;\n    }\n\n    private static SecretAccessPoliciesUpdates AddServiceAccountCreateUpdate(\n        SecretAccessPoliciesUpdates resource)\n    {\n        resource.ServiceAccountAccessPolicyUpdates = resource.ServiceAccountAccessPolicyUpdates.Append(\n            new ServiceAccountSecretAccessPolicyUpdate\n            {\n                AccessPolicy = new ServiceAccountSecretAccessPolicy\n                {\n                    ServiceAccountId = Guid.NewGuid(),\n                    GrantedSecretId = resource.SecretId,\n                    Read = true,\n                    Write = true\n                }\n            });\n        return resource;\n    }\n\n    private static SecretAccessPoliciesUpdates ClearAccessPolicyUpdate(SecretAccessPoliciesUpdates resource,\n        bool orgUsersCountZero,\n        bool groupsCountZero,\n        bool serviceAccountsCountZero)\n    {\n        if (orgUsersCountZero)\n        {\n            resource.UserAccessPolicyUpdates = [];\n        }\n\n        if (groupsCountZero)\n        {\n            resource.GroupAccessPolicyUpdates = [];\n        }\n\n        if (serviceAccountsCountZero)\n        {\n            resource.ServiceAccountAccessPolicyUpdates = [];\n        }\n\n        return resource;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandlerTests.cs",
    "content": "﻿#nullable enable\nusing System.Reflection;\nusing System.Security.Claims;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class ServiceAccountGrantedPoliciesAuthorizationHandlerTests\n{\n    [Fact]\n    public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()\n    {\n        var publicStaticFields =\n            typeof(ServiceAccountGrantedPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);\n        var allFields = typeof(ServiceAccountGrantedPoliciesOperations).GetFields();\n        Assert.Equal(publicStaticFields.Length, allFields.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountGrantedPoliciesOperations.Updates;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountGrantedPoliciesOperations.Updates;\n        SetupUserSubstitutes(sutProvider, accessClientType, resource);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_UnsupportedServiceAccountGrantedPoliciesOperationRequirement_Throws(\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new ServiceAccountGrantedPoliciesOperationRequirement();\n        SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck, false, false)]\n    [BitAutoData(AccessClientType.NoAccessCheck, true, false)]\n    [BitAutoData(AccessClientType.User, false, false)]\n    [BitAutoData(AccessClientType.User, true, false)]\n    public async Task Handler_UserHasNoWriteAccessToServiceAccount_DoesNotSucceed(\n        AccessClientType accessClientType,\n        bool saReadAccess,\n        bool saWriteAccess,\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountGrantedPoliciesOperations.Updates;\n        SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)\n            .Returns((saReadAccess, saWriteAccess));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_GrantedProjectsInDifferentOrganization_DoesNotSucceed(\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountGrantedPoliciesOperations.Updates;\n        SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(resource.ServiceAccountId, userId, AccessClientType.NoAccessCheck)\n            .Returns((true, true));\n        sutProvider.GetDependency<IProjectRepository>()\n            .ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_UserHasNoAccessToGrantedProjects_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountGrantedPoliciesOperations.Updates;\n        var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);\n\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(projectIds.ToDictionary(projectId => projectId, _ => (false, false)));\n\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_UserHasAccessToSomeGrantedProjects_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountGrantedPoliciesOperations.Updates;\n        var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);\n\n        var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));\n        accessResult[projectIds.First()] = (true, true);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(accessResult);\n\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_AccessResultsPartial_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountGrantedPoliciesOperations.Updates;\n        var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);\n\n        var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));\n        accessResult.Remove(projectIds.First());\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(accessResult);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task Handler_UserHasAccessToAllGrantedProjects_Success(\n        AccessClientType accessClientType,\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountGrantedPoliciesOperations.Updates;\n        var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);\n\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)\n            .Returns(projectIds.ToDictionary(projectId => projectId, _ => (true, true)));\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    private static void SetupUserSubstitutes(\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        AccessClientType accessClientType,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        Guid userId = new())\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default!, resource.OrganizationId)\n            .ReturnsForAnyArgs((accessClientType, userId));\n    }\n\n    private static List<Guid> SetupProjectAccessTest(\n        SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,\n        AccessClientType accessClientType,\n        ServiceAccountGrantedPoliciesUpdates resource,\n        Guid userId = new())\n    {\n        SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)\n            .Returns((true, true));\n        sutProvider.GetDependency<IProjectRepository>()\n            .ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(true);\n\n        return resource.ProjectGrantedPolicyUpdates\n            .Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)\n            .ToList();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountPeopleAccessPoliciesAuthorizationHandlerTests.cs",
    "content": "﻿using System.Reflection;\nusing System.Security.Claims;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;\n\n[SutProviderCustomize]\npublic class ServiceAccountPeopleAccessPoliciesAuthorizationHandlerTests\n{\n    private static void SetupUserPermission(\n        SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        AccessClientType accessClientType, ServiceAccountPeopleAccessPolicies resource, Guid userId = new(),\n        bool read = true,\n        bool write = true)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)\n            .ReturnsForAnyArgs(\n                (accessClientType, userId));\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(resource.Id, userId, accessClientType)\n            .Returns((read, write));\n    }\n\n    private static void SetupOrganizationUsers(\n        SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountPeopleAccessPolicies resource) =>\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .OrgUsersInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(true);\n\n    private static void SetupGroups(SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountPeopleAccessPolicies resource) =>\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .GroupsInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(true);\n\n    [Fact]\n    public void ServiceAccountPeopleAccessPoliciesOperations_OnlyPublicStatic()\n    {\n        var publicStaticFields =\n            typeof(ServiceAccountPeopleAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);\n        var allFields = typeof(ServiceAccountPeopleAccessPoliciesOperations).GetFields();\n        Assert.Equal(publicStaticFields.Length, allFields.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_UnsupportedServiceAccountPeopleAccessPoliciesOperationRequirement_Throws(\n        SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new ServiceAccountPeopleAccessPoliciesOperationRequirement();\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)\n            .ReturnsForAnyArgs(\n                (AccessClientType.NoAccessCheck, new Guid()));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new ServiceAccountPeopleAccessPoliciesOperationRequirement();\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(AccessClientType clientType,\n        SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new ServiceAccountPeopleAccessPoliciesOperationRequirement();\n        SetupUserPermission(sutProvider, clientType, resource);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User)]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    public async Task ReplaceServiceAccountPeople_UserNotInOrg_DoesNotSucceed(AccessClientType accessClient,\n        SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal, Guid userId)\n    {\n        var requirement = ServiceAccountPeopleAccessPoliciesOperations.Replace;\n        SetupUserPermission(sutProvider, accessClient, resource, userId);\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .OrgUsersInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User)]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    public async Task ReplaceServiceAccountPeople_GroupNotInOrg_DoesNotSucceed(AccessClientType accessClient,\n        SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal, Guid userId)\n    {\n        var requirement = ServiceAccountPeopleAccessPoliciesOperations.Replace;\n        SetupUserPermission(sutProvider, accessClient, resource, userId);\n        SetupOrganizationUsers(sutProvider, resource);\n\n        sutProvider.GetDependency<ISameOrganizationQuery>()\n            .GroupsInTheSameOrgAsync(Arg.Any<List<Guid>>(), resource.OrganizationId).Returns(false);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User, false, false, false)]\n    [BitAutoData(AccessClientType.User, false, true, true)]\n    [BitAutoData(AccessClientType.User, true, false, false)]\n    [BitAutoData(AccessClientType.User, true, true, true)]\n    [BitAutoData(AccessClientType.NoAccessCheck, false, false, false)]\n    [BitAutoData(AccessClientType.NoAccessCheck, false, true, true)]\n    [BitAutoData(AccessClientType.NoAccessCheck, true, false, false)]\n    [BitAutoData(AccessClientType.NoAccessCheck, true, true, true)]\n    public async Task ReplaceServiceAccountPeople_AccessCheck(AccessClientType accessClient, bool read, bool write,\n        bool expected,\n        SutProvider<ServiceAccountPeopleAccessPoliciesAuthorizationHandler> sutProvider,\n        ServiceAccountPeopleAccessPolicies resource,\n        ClaimsPrincipal claimsPrincipal, Guid userId)\n    {\n        var requirement = ServiceAccountPeopleAccessPoliciesOperations.Replace;\n        SetupUserPermission(sutProvider, accessClient, resource, userId, read, write);\n        SetupOrganizationUsers(sutProvider, resource);\n        SetupGroups(sutProvider, resource);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resource);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Projects/ProjectAuthorizationHandlerTests.cs",
    "content": "﻿using System.Reflection;\nusing System.Security.Claims;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Projects;\nusing Bit.Commercial.Core.Test.SecretsManager.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.Projects;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class ProjectAuthorizationHandlerTests\n{\n    private static void SetupPermission(SutProvider<ProjectAuthorizationHandler> sutProvider,\n        PermissionType permissionType, Guid organizationId, Guid userId = new())\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)\n            .Returns(true);\n\n        switch (permissionType)\n        {\n            case PermissionType.RunAsAdmin:\n                sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId)\n                    .ReturnsForAnyArgs(\n                        (AccessClientType.NoAccessCheck, userId));\n                break;\n            case PermissionType.RunAsUserWithPermission:\n                sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId)\n                    .ReturnsForAnyArgs(\n                        (AccessClientType.User, userId));\n                break;\n            case PermissionType.RunAsServiceAccountWithPermission:\n                sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId)\n                    .ReturnsForAnyArgs(\n                        (AccessClientType.ServiceAccount, userId));\n                break;\n            default:\n                throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);\n        }\n    }\n\n    [Fact]\n    public void ProjectOperations_OnlyPublicStatic()\n    {\n        var publicStaticFields = typeof(ProjectOperations).GetFields(BindingFlags.Public | BindingFlags.Static);\n        var allFields = typeof(ProjectOperations).GetFields();\n        Assert.Equal(publicStaticFields.Length, allFields.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_UnsupportedProjectOperationRequirement_Throws(\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)\n            .Returns(true);\n        var requirement = new ProjectOperationRequirement();\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_SupportedProjectOperationRequirement_DoesNotThrow(\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)\n            .Returns(true);\n\n        var requirements = typeof(ProjectOperations).GetFields(BindingFlags.Public | BindingFlags.Static)\n            .Select(i => (ProjectOperationRequirement)i.GetValue(null));\n\n        foreach (var req in requirements)\n        {\n            var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { req },\n                claimsPrincipal, project);\n\n            await sutProvider.Sut.HandleAsync(authzContext);\n        }\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanCreateProject_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)\n            .Returns(false);\n        var requirement = ProjectOperations.Create;\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task CanCreateProject_NotSupportedClientTypes_DoesNotSucceed(AccessClientType clientType,\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, project.OrganizationId)\n            .ReturnsForAnyArgs(\n                (clientType, new Guid()));\n        var requirement = ProjectOperations.Create;\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission)]\n    public async Task CanCreateProject_Success(PermissionType permissionType,\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupPermission(sutProvider, permissionType, project.OrganizationId);\n        var requirement = ProjectOperations.Create;\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanUpdateProject_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)\n            .Returns(false);\n        var requirement = ProjectOperations.Update;\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanUpdateProject_NullResource_DoesNotSucceed(\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)\n            .Returns(true);\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, project.OrganizationId);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(project.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((true, true));\n        var requirement = ProjectOperations.Update;\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanUpdateProject_NotSupportedClientType_DoesNotSucceed(\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(false);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, project.OrganizationId)\n            .ReturnsForAnyArgs(\n                (AccessClientType.Organization, new Guid()));\n        var requirement = ProjectOperations.Update;\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true, true)]\n    public async Task CanUpdateProject_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ProjectOperations.Update;\n        SetupPermission(sutProvider, permissionType, project.OrganizationId, userId);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(project.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanDeleteProject_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectOperations.Delete;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanDeleteProject_NullResource_DoesNotSucceed(\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ProjectOperations.Delete;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, project.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanDeleteProject_NotSupportedClientType_DoesNotSucceed(\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project, ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ProjectOperations.Delete;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(false);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, project.OrganizationId)\n            .ReturnsForAnyArgs(\n                (AccessClientType.Organization, new Guid()));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true, true)]\n    public async Task CanDeleteProject_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<ProjectAuthorizationHandler> sutProvider, Project project,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ProjectOperations.Delete;\n        SetupPermission(sutProvider, permissionType, project.OrganizationId, userId);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(project.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, project);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/BulkSecretAuthorizationHandlerTests.cs",
    "content": "﻿#nullable enable\nusing System.Reflection;\nusing System.Security.Claims;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.Secrets;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class BulkSecretAuthorizationHandlerTests\n{\n    [Fact]\n    public void BulkSecretOperations_OnlyPublicStatic()\n    {\n        var publicStaticFields = typeof(BulkSecretOperations).GetFields(BindingFlags.Public | BindingFlags.Static);\n        var allFields = typeof(BulkSecretOperations).GetFields();\n        Assert.Equal(publicStaticFields.Length, allFields.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_MisMatchedOrganizations_DoesNotSucceed(\n        SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = BulkSecretOperations.ReadAll;\n        resources[0].OrganizationId = Guid.NewGuid();\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Any<Guid>())\n            .ReturnsForAnyArgs(true);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resources);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_NoAccessToSecretsManager_DoesNotSucceed(\n        SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = BulkSecretOperations.ReadAll;\n        resources = SetSameOrganization(resources);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Any<Guid>())\n            .ReturnsForAnyArgs(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resources);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_UnsupportedSecretOperationRequirement_Throws(\n        SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = new BulkSecretOperationRequirement();\n        resources = SetSameOrganization(resources);\n        SetupUserSubstitutes(sutProvider, AccessClientType.User, resources.First().OrganizationId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resources);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User)]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    public async Task Handler_NoAccessToSecrets_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = BulkSecretOperations.ReadAll;\n        resources = SetSameOrganization(resources);\n        var secretIds =\n            SetupSecretAccessRequest(sutProvider, resources, accessClientType, resources.First().OrganizationId);\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretsAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .Returns(secretIds.ToDictionary(id => id, _ => (false, false)));\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resources);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User)]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    public async Task Handler_HasAccessToSomeSecrets_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = BulkSecretOperations.ReadAll;\n        resources = SetSameOrganization(resources);\n        var secretIds =\n            SetupSecretAccessRequest(sutProvider, resources, accessClientType, resources.First().OrganizationId);\n\n        var accessResult = secretIds.ToDictionary(secretId => secretId, _ => (false, false));\n        accessResult[secretIds.First()] = (true, true);\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretsAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .Returns(accessResult);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resources);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User)]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    public async Task Handler_PartialAccessReturn_DoesNotSucceed(\n        AccessClientType accessClientType,\n        SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = BulkSecretOperations.ReadAll;\n        resources = SetSameOrganization(resources);\n        var secretIds =\n            SetupSecretAccessRequest(sutProvider, resources, accessClientType, resources.First().OrganizationId);\n\n        var accessResult = secretIds.ToDictionary(secretId => secretId, _ => (false, false));\n        accessResult.Remove(secretIds.First());\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretsAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .Returns(accessResult);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resources);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.User)]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    public async Task Handler_HasAccessToAllSecrets_Success(\n        AccessClientType accessClientType,\n        SutProvider<BulkSecretAuthorizationHandler> sutProvider, List<Secret> resources,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = BulkSecretOperations.ReadAll;\n        resources = SetSameOrganization(resources);\n        var secretIds =\n            SetupSecretAccessRequest(sutProvider, resources, accessClientType, resources.First().OrganizationId);\n\n        var accessResult = secretIds.ToDictionary(secretId => secretId, _ => (true, true));\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretsAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .Returns(accessResult);\n\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, resources);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    private static List<Secret> SetSameOrganization(List<Secret> secrets)\n    {\n        var organizationId = secrets.First().OrganizationId;\n        foreach (var secret in secrets)\n        {\n            secret.OrganizationId = organizationId;\n        }\n\n        return secrets;\n    }\n\n    private static void SetupUserSubstitutes(\n        SutProvider<BulkSecretAuthorizationHandler> sutProvider,\n        AccessClientType accessClientType,\n        Guid organizationId,\n        Guid userId = new())\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default!, organizationId)\n            .ReturnsForAnyArgs((accessClientType, userId));\n    }\n\n    private static List<Guid> SetupSecretAccessRequest(\n        SutProvider<BulkSecretAuthorizationHandler> sutProvider,\n        IEnumerable<Secret> resources,\n        AccessClientType accessClientType,\n        Guid organizationId,\n        Guid userId = new())\n    {\n        SetupUserSubstitutes(sutProvider, accessClientType, organizationId, userId);\n        return resources.Select(s => s.Id).ToList();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/SecretAuthorizationHandlerTests.cs",
    "content": "﻿using System.Reflection;\nusing System.Security.Claims;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.Secrets;\nusing Bit.Commercial.Core.Test.SecretsManager.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.Secrets;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class SecretAuthorizationHandlerTests\n{\n    private static void SetupPermission(SutProvider<SecretAuthorizationHandler> sutProvider,\n        PermissionType permissionType, Guid organizationId, Guid userId = new(),\n        AccessClientType clientType = AccessClientType.User)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default)\n            .ReturnsForAnyArgs(true);\n\n        switch (permissionType)\n        {\n            case PermissionType.RunAsAdmin:\n                sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(\n                    (AccessClientType.NoAccessCheck, userId));\n                break;\n            case PermissionType.RunAsUserWithPermission:\n                sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(\n                    (clientType, userId));\n                break;\n            case PermissionType.RunAsServiceAccountWithPermission:\n                sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(\n                    (AccessClientType.ServiceAccount, userId));\n                break;\n            default:\n                throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);\n        }\n    }\n\n    [Fact]\n    public void SecretOperations_OnlyPublicStatic()\n    {\n        var publicStaticFields = typeof(SecretOperations).GetFields(BindingFlags.Public | BindingFlags.Static);\n        var allFields = typeof(SecretOperations).GetFields();\n        Assert.Equal(publicStaticFields.Length, allFields.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_UnsupportedSecretOperationRequirement_Throws(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret, ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)\n            .Returns(true);\n        var requirement = new SecretOperationRequirement();\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_SupportedSecretOperationRequirement_Throws(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret, ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)\n            .Returns(true);\n        var requirements = typeof(SecretOperations).GetFields(BindingFlags.Public | BindingFlags.Static)\n            .Select(i => (SecretOperationRequirement)i.GetValue(null));\n\n        foreach (var req in requirements)\n        {\n            var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { req },\n                claimsPrincipal, secret);\n\n            await sutProvider.Sut.HandleAsync(authzContext);\n        }\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanCreateSecret_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)\n            .Returns(false);\n        var requirement = SecretOperations.Create;\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task CanCreateSecret_NotSupportedClientTypes_DoesNotSucceed(AccessClientType clientType,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret, Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Create;\n        SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId, clientType);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, Arg.Any<AccessClientType>()).Returns(\n                (true, true));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanCreateSecret_ProjectsNotInOrg_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Create;\n        SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId);\n        sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default)\n            .ReturnsForAnyArgs(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanCreateSecret_WithoutProjectUser_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        secret.Projects = null;\n        var requirement = SecretOperations.Create;\n        SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanCreateSecret_WithoutProjectAdmin_Success(SutProvider<SecretAuthorizationHandler> sutProvider,\n        Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        secret.Projects = null;\n        var requirement = SecretOperations.Create;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false)]\n    public async Task CanCreateSecret_DoesNotSucceed(PermissionType permissionType, bool read, bool write,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Create;\n        SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, Arg.Any<AccessClientType>()).ReturnsForAnyArgs(\n                (read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true)]\n    [BitAutoData(PermissionType.RunAsAdmin, false, true)]\n    [BitAutoData(PermissionType.RunAsAdmin, true, false)]\n    [BitAutoData(PermissionType.RunAsAdmin, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true)]\n    public async Task CanCreateSecret_Success(PermissionType permissionType, bool read, bool write,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Create;\n        SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, Arg.Any<AccessClientType>()).ReturnsForAnyArgs(\n                (read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadSecret_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Read;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadSecret_NullResource_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = SecretOperations.Read;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true, true)]\n    public async Task CanReadSecret_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = SecretOperations.Read;\n        SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretAsync(secret.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanUpdateSecret_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)\n            .Returns(false);\n        var requirement = SecretOperations.Update;\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task CanUpdateSecret_NotSupportedClientTypes_DoesNotSucceed(AccessClientType clientType,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret, Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Update;\n        SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId, clientType);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, Arg.Any<AccessClientType>()).Returns(\n                (true, true));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanUpdateSecret_ProjectsNotInOrg_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Update;\n        SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId);\n        sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default)\n            .ReturnsForAnyArgs(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanUpdateSecret_ClearProjectsUser_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        secret.Projects = [];\n        var requirement = SecretOperations.Update;\n        SetupPermission(sutProvider, PermissionType.RunAsUserWithPermission, secret.OrganizationId, userId);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, Arg.Any<Guid>(), Arg.Any<AccessClientType>()).Returns(\n            (true, true));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanUpdateSecret_ClearProjectsAdmin_Success(SutProvider<SecretAuthorizationHandler> sutProvider,\n        Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        secret.Projects = [];\n        var requirement = SecretOperations.Update;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true, true)]\n    public async Task CanUpdateSecret_NoProjectChanges_ReturnsExpected(PermissionType permissionType, bool read,\n        bool write, bool expected,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Update;\n        secret.Projects = null;\n        SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretAsync(secret.Id, userId, Arg.Any<AccessClientType>()).Returns(\n                (read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true, true, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true, true, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false, false, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, false, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, false, false)]\n    public async Task CanUpdateSecret_DoesNotSucceed(PermissionType permissionType, bool read, bool write,\n        bool projectRead, bool projectWrite,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Update;\n        SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, Arg.Any<AccessClientType>()).Returns(\n            (read, write));\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, Arg.Any<AccessClientType>()).Returns(\n                (projectRead, projectWrite));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true)]\n    [BitAutoData(PermissionType.RunAsAdmin, false, true)]\n    [BitAutoData(PermissionType.RunAsAdmin, true, false)]\n    [BitAutoData(PermissionType.RunAsAdmin, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true)]\n    public async Task CanUpdateSecret_Success(PermissionType permissionType, bool read, bool write,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        Guid userId,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Update;\n        SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, Arg.Any<AccessClientType>()).Returns(\n            (read, write));\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(secret.Projects!.FirstOrDefault()!.Id, userId, Arg.Any<AccessClientType>()).Returns(\n                (read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanDeleteSecret_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.Delete;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanDeleteSecret_NullResource_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = SecretOperations.Delete;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsServiceAccountWithPermission, true, true, true)]\n    public async Task CanDeleteSecret_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = SecretOperations.Delete;\n        SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretAsync(secret.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadAccessPolicies_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.ReadAccessPolicies;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadAccessPolicies_NullResource_DoesNotSucceed(\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = SecretOperations.ReadAccessPolicies;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, secret.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task CanReadAccessPolicies_UnsupportedClient_DoesNotSucceed(\n        AccessClientType clientType,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = SecretOperations.ReadAccessPolicies;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), secret.OrganizationId)\n            .Returns((clientType, Guid.NewGuid()));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    public async Task CanReadAccessPolicies_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<SecretAuthorizationHandler> sutProvider, Secret secret,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = SecretOperations.ReadAccessPolicies;\n        SetupPermission(sutProvider, permissionType, secret.OrganizationId, userId);\n        sutProvider.GetDependency<ISecretRepository>()\n            .AccessToSecretAsync(secret.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, secret);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/ServiceAccounts/ServiceAccountAuthorizationHandlerTests.cs",
    "content": "﻿using System.Reflection;\nusing System.Security.Claims;\nusing Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.ServiceAccounts;\nusing Bit.Commercial.Core.Test.SecretsManager.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.ServiceAccounts;\n\n[SutProviderCustomize]\npublic class ServiceAccountAuthorizationHandlerTests\n{\n    private static void SetupPermission(SutProvider<ServiceAccountAuthorizationHandler> sutProvider,\n        PermissionType permissionType, Guid organizationId, Guid userId = new())\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)\n            .Returns(true);\n\n        switch (permissionType)\n        {\n            case PermissionType.RunAsAdmin:\n                sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId)\n                    .ReturnsForAnyArgs(\n                        (AccessClientType.NoAccessCheck, userId));\n                break;\n            case PermissionType.RunAsUserWithPermission:\n                sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId)\n                    .ReturnsForAnyArgs(\n                        (AccessClientType.User, userId));\n                break;\n            default:\n                throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);\n        }\n    }\n\n    [Fact]\n    public void ServiceAccountOperations_OnlyPublicStatic()\n    {\n        var publicStaticFields = typeof(ServiceAccountOperations).GetFields(BindingFlags.Public | BindingFlags.Static);\n        var allFields = typeof(ServiceAccountOperations).GetFields();\n        Assert.Equal(publicStaticFields.Length, allFields.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_UnsupportedServiceAccountOperationRequirement_Throws(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(true);\n        var requirement = new ServiceAccountOperationRequirement();\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Handler_SupportedServiceAccountOperationRequirement_DoesNotThrow(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(true);\n\n        var requirements = typeof(ServiceAccountOperations).GetFields(BindingFlags.Public | BindingFlags.Static)\n            .Select(i => (ServiceAccountOperationRequirement)i.GetValue(null));\n\n        foreach (var req in requirements)\n        {\n            var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { req },\n                claimsPrincipal, serviceAccount);\n\n            await sutProvider.Sut.HandleAsync(authzContext);\n        }\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanCreateServiceAccount_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(false);\n        var requirement = ServiceAccountOperations.Create;\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.ServiceAccount)]\n    [BitAutoData(AccessClientType.Organization)]\n    public async Task CanCreateServiceAccount_NotSupportedClientTypes_DoesNotSucceed(AccessClientType clientType,\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountOperations.Create;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(serviceAccount.OrganizationId)\n            .Returns(false);\n        sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, serviceAccount.OrganizationId)\n            .ReturnsForAnyArgs(\n                (clientType, new Guid()));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission)]\n    public async Task CanCreateServiceAccount_Success(PermissionType permissionType,\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountOperations.Create;\n        SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.True(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanUpdateServiceAccount_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountOperations.Update;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanUpdateServiceAccount_NullResource_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.Update;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    public async Task CanUpdateServiceAccount_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.Update;\n        SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadServiceAccount_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountOperations.Read;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadServiceAccount_NullResource_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.Read;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    public async Task CanReadServiceAccount_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.Read;\n        SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanCreateAccessToken_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountOperations.CreateAccessToken;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanCreateAccessToken_NullResource_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.CreateAccessToken;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    public async Task CanCreateAccessToken_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.CreateAccessToken;\n        SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadAccessTokens_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountOperations.ReadAccessTokens;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadAccessTokens_NullResource_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.ReadAccessTokens;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    public async Task CanReadAccessTokens_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.ReadAccessTokens;\n        SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanRevokeAccessTokens_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountOperations.RevokeAccessTokens;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanRevokeAccessTokens_NullResource_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.RevokeAccessTokens;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    public async Task CanRevokeAccessTokens_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.RevokeAccessTokens;\n        SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanDeleteServiceAccount_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountOperations.Delete;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanDeleteServiceAccount_NullResource_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.Delete;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    public async Task CanDeleteProject_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.Delete;\n        SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadEvents_AccessToSecretsManagerFalse_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        var requirement = ServiceAccountOperations.ReadEvents;\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId)\n            .Returns(false);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanReadEvents_NullResource_DoesNotSucceed(\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.ReadEvents;\n        SetupPermission(sutProvider, PermissionType.RunAsAdmin, serviceAccount.OrganizationId, userId);\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.False(authzContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin, true, true, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true)]\n    public async Task CanReadEvents_AccessCheck(PermissionType permissionType, bool read, bool write,\n        bool expected,\n        SutProvider<ServiceAccountAuthorizationHandler> sutProvider, ServiceAccount serviceAccount,\n        ClaimsPrincipal claimsPrincipal,\n        Guid userId)\n    {\n        var requirement = ServiceAccountOperations.ReadEvents;\n        SetupPermission(sutProvider, permissionType, serviceAccount.OrganizationId, userId);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(serviceAccount.Id, userId, Arg.Any<AccessClientType>())\n            .Returns((read, write));\n        var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, serviceAccount);\n\n        await sutProvider.Sut.HandleAsync(authzContext);\n\n        Assert.Equal(expected, authzContext.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/AccessPolicies/UpdateProjectServiceAccountsAccessPoliciesCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class UpdateProjectServiceAccountsAccessPoliciesCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_NoUpdates_DoesNotCallRepository(\n        SutProvider<UpdateProjectServiceAccountsAccessPoliciesCommand> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates data)\n    {\n        data.ServiceAccountAccessPolicyUpdates = [];\n        await sutProvider.Sut.UpdateAsync(data);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpdateProjectServiceAccountsAccessPoliciesAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_HasUpdates_CallsRepository(\n        SutProvider<UpdateProjectServiceAccountsAccessPoliciesCommand> sutProvider,\n        ProjectServiceAccountsAccessPoliciesUpdates data)\n    {\n        await sutProvider.Sut.UpdateAsync(data);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>()\n            .Received(1)\n            .UpdateProjectServiceAccountsAccessPoliciesAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/AccessPolicies/UpdateServiceAccountGrantedPoliciesCommandTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class UpdateServiceAccountGrantedPoliciesCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_NoUpdates_DoesNotCallRepository(\n        SutProvider<UpdateServiceAccountGrantedPoliciesCommand> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates data)\n    {\n        data.ProjectGrantedPolicyUpdates = [];\n        await sutProvider.Sut.UpdateAsync(data);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpdateServiceAccountGrantedPoliciesAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_HasUpdates_CallsRepository(\n        SutProvider<UpdateServiceAccountGrantedPoliciesCommand> sutProvider,\n        ServiceAccountGrantedPoliciesUpdates data)\n    {\n        await sutProvider.Sut.UpdateAsync(data);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>()\n            .Received(1)\n            .UpdateServiceAccountGrantedPoliciesAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.AccessTokens;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessTokens;\n\n[SutProviderCustomize]\npublic class CreateServiceAccountCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_NoServiceAccountId_ThrowsBadRequestException(\n        SutProvider<CreateAccessTokenCommand> sutProvider, ApiKey data)\n    {\n        data.ServiceAccountId = null;\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(data));\n\n        await sutProvider.GetDependency<IApiKeyRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_Success(SutProvider<CreateAccessTokenCommand> sutProvider, ApiKey data)\n    {\n        await sutProvider.Sut.CreateAsync(data);\n\n        await sutProvider.GetDependency<IApiKeyRepository>().Received(1)\n            .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Projects/CreateProjectCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.Projects;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.Projects;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class CreateProjectCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_CallsCreate(Project data,\n        Guid userId,\n        SutProvider<CreateProjectCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())\n            .Returns(new OrganizationUser() { Id = userId });\n\n        sutProvider.GetDependency<IProjectRepository>()\n            .CreateAsync(Arg.Any<Project>())\n            .Returns(data);\n\n        await sutProvider.Sut.CreateAsync(data, userId, sutProvider.GetDependency<ICurrentContext>().IdentityClientType);\n\n        await sutProvider.GetDependency<IProjectRepository>().Received(1)\n            .CreateAsync(Arg.Is(data));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)\n            .CreateManyAsync(Arg.Any<List<BaseAccessPolicy>>());\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Projects/DeleteProjectCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.Projects;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.Projects;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class DeleteProjectCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteProjects_Success(List<Project> data,\n      SutProvider<DeleteProjectCommand> sutProvider)\n    {\n        await sutProvider.Sut.DeleteProjects(data);\n        await sutProvider.GetDependency<IProjectRepository>()\n            .Received(1)\n            .DeleteManyByIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.Select(d => d.Id))));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Projects/UpdateProjectCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.Projects;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.Projects;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class UpdateProjectCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_Throws_NotFoundException(Project project, SutProvider<UpdateProjectCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).ReturnsNull();\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(project));\n\n        await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_Success(Project project, SutProvider<UpdateProjectCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);\n\n        var updatedProject = new Project { Id = project.Id, Name = \"newName\" };\n        var result = await sutProvider.Sut.UpdateAsync(updatedProject);\n\n        Assert.NotNull(result);\n        Assert.Equal(\"newName\", result.Name);\n\n        await sutProvider.GetDependency<IProjectRepository>().ReceivedWithAnyArgs(1).ReplaceAsync(default);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Requests/RequestSMAccessCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.Requests;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.Requests;\n\n[SutProviderCustomize]\npublic class RequestSMAccessCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task SendRequestAccessToSM_Success(\n          User user,\n          Organization organization,\n          ICollection<OrganizationUserUserDetails> orgUsers,\n          string emailContent,\n          SutProvider<RequestSMAccessCommand> sutProvider)\n    {\n        foreach (var userDetails in orgUsers)\n        {\n            userDetails.Type = OrganizationUserType.Admin;\n        }\n\n        orgUsers.First().Type = OrganizationUserType.Owner;\n\n        await sutProvider.Sut.SendRequestAccessToSM(organization, orgUsers, user, emailContent);\n\n        var adminEmailList = orgUsers\n            .Where(o => o.Type <= OrganizationUserType.Admin)\n            .Select(a => a.Email)\n            .Distinct()\n            .ToList();\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendRequestSMAccessToAdminEmailAsync(Arg.Is(AssertHelper.AssertPropertyEqual(adminEmailList)), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendRequestAccessToSM_NoAdmins_ThrowsBadRequestException(\n       User user,\n       Organization organization,\n       ICollection<OrganizationUserUserDetails> orgUsers,\n       string emailContent,\n       SutProvider<RequestSMAccessCommand> sutProvider)\n    {\n        // Set OrgUsers so they are only users, no admins or owners\n        foreach (OrganizationUserUserDetails userDetails in orgUsers)\n        {\n            userDetails.Type = OrganizationUserType.User;\n        }\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SendRequestAccessToSM(organization, orgUsers, user, emailContent));\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendRequestAccessToSM_SomeAdmins_EmailListIsAsExpected(\n       User user,\n       Organization organization,\n       ICollection<OrganizationUserUserDetails> orgUsers,\n       string emailContent,\n       SutProvider<RequestSMAccessCommand> sutProvider)\n    {\n        foreach (OrganizationUserUserDetails userDetails in orgUsers)\n        {\n            userDetails.Type = OrganizationUserType.User;\n        }\n\n        // Make the first orgUser an admin so it's a mix of Admin + Users\n        orgUsers.First().Type = OrganizationUserType.Admin;\n\n        var adminEmailList = orgUsers\n            .Where(o => o.Type == OrganizationUserType.Admin) // Filter by Admin type\n            .Select(a => a.Email)\n            .Distinct()\n            .ToList();\n\n        await sutProvider.Sut.SendRequestAccessToSM(organization, orgUsers, user, emailContent);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendRequestSMAccessToAdminEmailAsync(Arg.Is(AssertHelper.AssertPropertyEqual(adminEmailList)), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Secrets/CreateSecretCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.Secrets;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.Secrets;\n\n[SutProviderCustomize]\n[SecretCustomize]\npublic class CreateSecretCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_Success(Secret data,\n        SutProvider<CreateSecretCommand> sutProvider, Project mockProject)\n    {\n        data.Projects = new List<Project>() { mockProject };\n\n        await sutProvider.Sut.CreateAsync(data, null);\n\n        await sutProvider.GetDependency<ISecretRepository>().Received(1)\n            .CreateAsync(data, null);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Secrets/DeleteSecretCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.Secrets;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.Secrets;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class DeleteSecretCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteSecrets_Success(SutProvider<DeleteSecretCommand> sutProvider, List<Secret> data)\n    {\n        await sutProvider.Sut.DeleteSecrets(data);\n        await sutProvider.GetDependency<ISecretRepository>()\n            .Received(1)\n            .SoftDeleteManyByIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.Select(d => d.Id))));\n    }\n}\n\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Secrets/UpdateSecretCommandTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Commercial.Core.SecretsManager.Commands.Secrets;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.Secrets;\n\n[SutProviderCustomize]\n[SecretCustomize]\n[ProjectCustomize]\npublic class UpdateSecretCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_Success(SutProvider<UpdateSecretCommand> sutProvider, Secret data, Project project)\n    {\n        data.Projects = new List<Project> { project };\n\n        await sutProvider.Sut.UpdateAsync(data, null);\n\n        await sutProvider.GetDependency<ISecretRepository>().Received(1)\n            .UpdateAsync(data, null);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.ServiceAccounts;\n\n[SutProviderCustomize]\npublic class CreateServiceAccountCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_CallsCreate(ServiceAccount data,\n        Guid userId,\n        SutProvider<CreateServiceAccountCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())\n            .Returns(new OrganizationUser() { Id = userId });\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .CreateAsync(Arg.Any<ServiceAccount>())\n            .Returns(data);\n\n        await sutProvider.Sut.CreateAsync(data, userId);\n\n        await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)\n            .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/ServiceAccounts/DeleteServiceAccountsCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.ServiceAccounts;\n\n[SutProviderCustomize]\npublic class DeleteServiceAccountsCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteServiceAccounts_Success(SutProvider<DeleteServiceAccountsCommand> sutProvider,\n        List<ServiceAccount> data)\n    {\n        await sutProvider.Sut.DeleteServiceAccounts(data);\n        await sutProvider.GetDependency<IServiceAccountRepository>()\n            .Received(1)\n            .DeleteManyByIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.Select(d => d.Id))));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/ServiceAccounts/RevokeAccessTokenCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.ServiceAccounts;\n\n[SutProviderCustomize]\npublic class RevokeAccessTokenCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeAsyncAsync_Success(ServiceAccount serviceAccount, SutProvider<RevokeAccessTokensCommand> sutProvider)\n    {\n        var apiKey1 = new ApiKey\n        {\n            Id = Guid.NewGuid(),\n            ServiceAccountId = serviceAccount.Id,\n            Name = \"Test Name\",\n            Scope = \"Test Scope\",\n            EncryptedPayload = \"Test EncryptedPayload\",\n            Key = \"Test Key\",\n        };\n\n        var apiKey2 = new ApiKey\n        {\n            Id = Guid.NewGuid(),\n            ServiceAccountId = serviceAccount.Id,\n            Name = \"Test Name\",\n            Scope = \"Test Scope\",\n            EncryptedPayload = \"Test EncryptedPayload\",\n            Key = \"Test Key\",\n        };\n\n        sutProvider.GetDependency<IApiKeyRepository>()\n            .GetManyByServiceAccountIdAsync(serviceAccount.Id)\n            .Returns(new List<ApiKey> { apiKey1, apiKey2 });\n\n        await sutProvider.Sut.RevokeAsync(serviceAccount, new List<Guid> { apiKey1.Id });\n\n        await sutProvider.GetDependency<IApiKeyRepository>().Received(1)\n            .DeleteManyAsync(Arg.Is<IEnumerable<ApiKey>>(arg => arg.SequenceEqual(new List<ApiKey> { apiKey1 })));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.ServiceAccounts;\n\n[SutProviderCustomize]\npublic class UpdateServiceAccountCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, SutProvider<UpdateServiceAccountCommand> sutProvider)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data));\n\n        await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_Success(ServiceAccount data, SutProvider<UpdateServiceAccountCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);\n\n        await sutProvider.Sut.UpdateAsync(data);\n\n        await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)\n            .ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_DoesNotModifyOrganizationId(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);\n\n        var updatedOrgId = Guid.NewGuid();\n        var serviceAccountUpdate = new ServiceAccount()\n        {\n            OrganizationId = updatedOrgId,\n            Id = existingServiceAccount.Id,\n            Name = existingServiceAccount.Name,\n        };\n\n        var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);\n\n        Assert.Equal(existingServiceAccount.OrganizationId, result.OrganizationId);\n        Assert.NotEqual(existingServiceAccount.OrganizationId, updatedOrgId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);\n\n        var updatedCreationDate = DateTime.UtcNow;\n        var serviceAccountUpdate = new ServiceAccount()\n        {\n            CreationDate = updatedCreationDate,\n            Id = existingServiceAccount.Id,\n            Name = existingServiceAccount.Name,\n        };\n\n        var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);\n\n        Assert.Equal(existingServiceAccount.CreationDate, result.CreationDate);\n        Assert.NotEqual(existingServiceAccount.CreationDate, updatedCreationDate);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);\n\n        var updatedRevisionDate = DateTime.UtcNow.AddDays(10);\n        var serviceAccountUpdate = new ServiceAccount()\n        {\n            RevisionDate = updatedRevisionDate,\n            Id = existingServiceAccount.Id,\n            Name = existingServiceAccount.Name,\n        };\n\n        var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);\n\n        Assert.NotEqual(serviceAccountUpdate.RevisionDate, result.RevisionDate);\n        AssertHelper.AssertRecent(result.RevisionDate);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Trash/EmptyTrashCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.Trash;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.Trash;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class EmptyTrashCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task EmptyTrash_Throws_NotFoundException(Guid orgId, Secret s1, Secret s2, SutProvider<EmptyTrashCommand> sutProvider)\n    {\n        s1.DeletedDate = DateTime.Now;\n\n        var ids = new List<Guid> { s1.Id, s2.Id };\n        sutProvider.GetDependency<ISecretRepository>()\n            .GetManyByOrganizationIdInTrashByIdsAsync(orgId, ids)\n            .Returns(new List<Secret> { s1 });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.EmptyTrash(orgId, ids));\n\n        await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().RestoreManyByIdAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task EmptyTrash_Success(Guid orgId, Secret s1, Secret s2, SutProvider<EmptyTrashCommand> sutProvider)\n    {\n        s1.DeletedDate = DateTime.Now;\n\n        var ids = new List<Guid> { s1.Id, s2.Id };\n        sutProvider.GetDependency<ISecretRepository>()\n            .GetManyByOrganizationIdInTrashByIdsAsync(orgId, ids)\n            .Returns(new List<Secret> { s1, s2 });\n\n        await sutProvider.Sut.EmptyTrash(orgId, ids);\n\n        await sutProvider.GetDependency<ISecretRepository>().Received(1).HardDeleteManyByIdAsync(ids);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Commands/Trash/RestoreTrashCommandTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Commands.Trash;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Commands.Trash;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class RestoreTrashCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task RestoreTrash_Throws_NotFoundException(Guid orgId, Secret s1, Secret s2, SutProvider<RestoreTrashCommand> sutProvider)\n    {\n        s1.DeletedDate = DateTime.Now;\n\n        var ids = new List<Guid> { s1.Id, s2.Id };\n        sutProvider.GetDependency<ISecretRepository>()\n            .GetManyByOrganizationIdInTrashByIdsAsync(orgId, ids)\n            .Returns(new List<Secret> { s1 });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RestoreTrash(orgId, ids));\n\n        await sutProvider.GetDependency<ISecretRepository>().DidNotReceiveWithAnyArgs().RestoreManyByIdAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RestoreTrash_Success(Guid orgId, Secret s1, Secret s2, SutProvider<RestoreTrashCommand> sutProvider)\n    {\n        s1.DeletedDate = DateTime.Now;\n\n        var ids = new List<Guid> { s1.Id, s2.Id };\n        sutProvider.GetDependency<ISecretRepository>()\n            .GetManyByOrganizationIdInTrashByIdsAsync(orgId, ids)\n            .Returns(new List<Secret> { s1, s2 });\n\n        await sutProvider.Sut.RestoreTrash(orgId, ids);\n\n        await sutProvider.GetDependency<ISecretRepository>().Received(1).RestoreManyByIdAsync(ids);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/AccessPolicyType.cs",
    "content": "﻿namespace Bit.Commercial.Core.Test.SecretsManager.Enums;\n\npublic enum AccessPolicyType\n{\n    UserProjectAccessPolicy,\n    GroupProjectAccessPolicy,\n    ServiceAccountProjectAccessPolicy,\n    UserServiceAccountAccessPolicy,\n    GroupServiceAccountAccessPolicy,\n\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Enums/PermissionType.cs",
    "content": "﻿namespace Bit.Commercial.Core.Test.SecretsManager.Enums;\n\npublic enum PermissionType\n{\n    RunAsAdmin,\n    RunAsUserWithPermission,\n    RunAsServiceAccountWithPermission,\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/ProjectServiceAccountsAccessPoliciesUpdatesQueryTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class ProjectServiceAccountsAccessPoliciesUpdatesQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetAsync_NoCurrentAccessPolicies_ReturnsAllCreates(\n        SutProvider<ProjectServiceAccountsAccessPoliciesUpdatesQuery> sutProvider,\n        ProjectServiceAccountsAccessPolicies data)\n    {\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetProjectServiceAccountsAccessPoliciesAsync(data.ProjectId)\n            .ReturnsNullForAnyArgs();\n\n        var result = await sutProvider.Sut.GetAsync(data);\n\n        Assert.Equal(data.ProjectId, result.ProjectId);\n        Assert.Equal(data.OrganizationId, result.OrganizationId);\n        Assert.Equal(data.ServiceAccountAccessPolicies.Count(), result.ServiceAccountAccessPolicyUpdates.Count());\n        Assert.All(result.ServiceAccountAccessPolicyUpdates, p =>\n        {\n            Assert.Equal(AccessPolicyOperation.Create, p.Operation);\n            Assert.Contains(data.ServiceAccountAccessPolicies, x => x == p.AccessPolicy);\n        });\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAsync_CurrentAccessPolicies_ReturnsChanges(\n        SutProvider<ProjectServiceAccountsAccessPoliciesUpdatesQuery> sutProvider,\n        ProjectServiceAccountsAccessPolicies data, ServiceAccountProjectAccessPolicy currentPolicyToDelete)\n    {\n        foreach (var policy in data.ServiceAccountAccessPolicies)\n        {\n            policy.GrantedProjectId = data.ProjectId;\n        }\n\n        currentPolicyToDelete.GrantedProjectId = data.ProjectId;\n\n        var updatePolicy = new ServiceAccountProjectAccessPolicy\n        {\n            ServiceAccountId = data.ServiceAccountAccessPolicies.First().ServiceAccountId,\n            GrantedProjectId = data.ProjectId,\n            Read = !data.ServiceAccountAccessPolicies.First().Read,\n            Write = !data.ServiceAccountAccessPolicies.First().Write\n        };\n\n        var currentPolicies = new ProjectServiceAccountsAccessPolicies\n        {\n            ProjectId = data.ProjectId,\n            OrganizationId = data.OrganizationId,\n            ServiceAccountAccessPolicies = [updatePolicy, currentPolicyToDelete]\n        };\n\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetProjectServiceAccountsAccessPoliciesAsync(data.ProjectId)\n            .ReturnsForAnyArgs(currentPolicies);\n\n        var result = await sutProvider.Sut.GetAsync(data);\n\n        Assert.Equal(data.ProjectId, result.ProjectId);\n        Assert.Equal(data.OrganizationId, result.OrganizationId);\n        Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == currentPolicyToDelete));\n        Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Update &&\n            x.AccessPolicy.GrantedProjectId == updatePolicy.GrantedProjectId));\n        Assert.Equal(result.ServiceAccountAccessPolicyUpdates.Count() - 2,\n            result.ServiceAccountAccessPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/SameOrganizationQueryTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;\n\n[SutProviderCustomize]\npublic class SameOrganizationQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task OrgUsersInTheSameOrg_NoOrgUsers_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,\n        List<OrganizationUser> orgUsers, Guid organizationId)\n    {\n        var orgUserIds = orgUsers.Select(ou => ou.Id).ToList();\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(orgUserIds)\n            .ReturnsForAnyArgs(new List<OrganizationUser>());\n\n        var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId);\n\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task OrgUsersInTheSameOrg_OrgMismatch_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,\n        List<OrganizationUser> orgUsers, Guid organizationId)\n    {\n        var orgUserIds = orgUsers.Select(ou => ou.Id).ToList();\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(orgUserIds)\n            .ReturnsForAnyArgs(orgUsers);\n\n        var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId);\n\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task OrgUsersInTheSameOrg_CountMismatch_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,\n        List<OrganizationUser> orgUsers, Guid organizationId)\n    {\n        var orgUserIds = orgUsers.Select(ou => ou.Id).ToList();\n        foreach (var organizationUser in orgUsers)\n        {\n            organizationUser.OrganizationId = organizationId;\n        }\n\n        orgUsers.RemoveAt(0);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(orgUserIds)\n            .ReturnsForAnyArgs(orgUsers);\n\n        var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId);\n\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task OrgUsersInTheSameOrg_Success_ReturnsTrue(SutProvider<SameOrganizationQuery> sutProvider,\n        List<OrganizationUser> orgUsers, Guid organizationId)\n    {\n        var orgUserIds = orgUsers.Select(ou => ou.Id).ToList();\n        foreach (var organizationUser in orgUsers)\n        {\n            organizationUser.OrganizationId = organizationId;\n        }\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(orgUserIds)\n            .ReturnsForAnyArgs(orgUsers);\n\n        var result = await sutProvider.Sut.OrgUsersInTheSameOrgAsync(orgUserIds, organizationId);\n\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GroupsInTheSameOrg_NoGroups_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,\n        List<Group> groups, Guid organizationId)\n    {\n        var groupIds = groups.Select(ou => ou.Id).ToList();\n        sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(groupIds)\n            .ReturnsForAnyArgs(new List<Group>());\n\n        var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId);\n\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GroupsInTheSameOrg_OrgMismatch_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,\n        List<Group> groups, Guid organizationId)\n    {\n        var groupIds = groups.Select(ou => ou.Id).ToList();\n        sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(groupIds)\n            .ReturnsForAnyArgs(groups);\n\n        var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId);\n\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GroupsInTheSameOrg_CountMismatch_ReturnsFalse(SutProvider<SameOrganizationQuery> sutProvider,\n        List<Group> groups, Guid organizationId)\n    {\n        var groupIds = groups.Select(ou => ou.Id).ToList();\n        foreach (var group in groups)\n        {\n            group.OrganizationId = organizationId;\n        }\n\n        groups.RemoveAt(0);\n\n        sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(groupIds)\n            .ReturnsForAnyArgs(groups);\n\n        var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId);\n\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GroupsInTheSameOrg_Success_ReturnsTrue(SutProvider<SameOrganizationQuery> sutProvider,\n        List<Group> groups, Guid organizationId)\n    {\n        var groupIds = groups.Select(ou => ou.Id).ToList();\n        foreach (var group in groups)\n        {\n            group.OrganizationId = organizationId;\n        }\n\n        sutProvider.GetDependency<IGroupRepository>().GetManyByManyIds(groupIds)\n            .ReturnsForAnyArgs(groups);\n\n\n        var result = await sutProvider.Sut.GroupsInTheSameOrgAsync(groupIds, organizationId);\n\n        Assert.True(result);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/SecretAccessPoliciesUpdatesQueryTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class SecretAccessPoliciesUpdatesQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetAsync_NoCurrentAccessPolicies_ReturnsAllCreates(\n        SutProvider<SecretAccessPoliciesUpdatesQuery> sutProvider,\n        SecretAccessPolicies data,\n        Guid userId)\n    {\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetSecretAccessPoliciesAsync(data.SecretId, userId)\n            .ReturnsNullForAnyArgs();\n\n        var result = await sutProvider.Sut.GetAsync(data, userId);\n\n        Assert.Equal(data.SecretId, result.SecretId);\n        Assert.Equal(data.OrganizationId, result.OrganizationId);\n\n        Assert.Equal(data.UserAccessPolicies.Count(), result.UserAccessPolicyUpdates.Count());\n        Assert.All(result.UserAccessPolicyUpdates, p =>\n        {\n            Assert.Equal(AccessPolicyOperation.Create, p.Operation);\n            Assert.Contains(data.UserAccessPolicies, x => x == p.AccessPolicy);\n        });\n\n        Assert.Equal(data.GroupAccessPolicies.Count(), result.GroupAccessPolicyUpdates.Count());\n        Assert.All(result.GroupAccessPolicyUpdates, p =>\n        {\n            Assert.Equal(AccessPolicyOperation.Create, p.Operation);\n            Assert.Contains(data.GroupAccessPolicies, x => x == p.AccessPolicy);\n        });\n\n        Assert.Equal(data.ServiceAccountAccessPolicies.Count(), result.ServiceAccountAccessPolicyUpdates.Count());\n        Assert.All(result.ServiceAccountAccessPolicyUpdates, p =>\n        {\n            Assert.Equal(AccessPolicyOperation.Create, p.Operation);\n            Assert.Contains(data.ServiceAccountAccessPolicies, x => x == p.AccessPolicy);\n        });\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAsync_CurrentAccessPolicies_ReturnsChanges(\n        SutProvider<SecretAccessPoliciesUpdatesQuery> sutProvider,\n        SecretAccessPolicies data,\n        Guid userId,\n        UserSecretAccessPolicy userPolicyToDelete,\n        GroupSecretAccessPolicy groupPolicyToDelete,\n        ServiceAccountSecretAccessPolicy serviceAccountPolicyToDelete)\n    {\n        data = SetupSecretAccessPolicies(data);\n        var userPolicyChanges = SetupUserAccessPolicies(data, userPolicyToDelete);\n        var groupPolicyChanges = SetupGroupAccessPolicies(data, groupPolicyToDelete);\n        var serviceAccountPolicyChanges = SetupServiceAccountAccessPolicies(data, serviceAccountPolicyToDelete);\n\n        var currentPolicies = new SecretAccessPolicies\n        {\n            SecretId = data.SecretId,\n            OrganizationId = data.OrganizationId,\n            UserAccessPolicies = [userPolicyChanges.Update, userPolicyChanges.Delete],\n            GroupAccessPolicies = [groupPolicyChanges.Update, groupPolicyChanges.Delete],\n            ServiceAccountAccessPolicies = [serviceAccountPolicyChanges.Update, serviceAccountPolicyChanges.Delete]\n        };\n\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetSecretAccessPoliciesAsync(data.SecretId, userId)\n            .ReturnsForAnyArgs(currentPolicies);\n\n        var result = await sutProvider.Sut.GetAsync(data, userId);\n\n        Assert.Equal(data.SecretId, result.SecretId);\n        Assert.Equal(data.OrganizationId, result.OrganizationId);\n\n        Assert.Single(result.UserAccessPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == userPolicyChanges.Delete));\n        Assert.Single(result.UserAccessPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Update &&\n            x.AccessPolicy.OrganizationUserId == userPolicyChanges.Update.OrganizationUserId));\n        Assert.Equal(result.UserAccessPolicyUpdates.Count() - 2,\n            result.UserAccessPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));\n\n        Assert.Single(result.GroupAccessPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == groupPolicyChanges.Delete));\n        Assert.Single(result.GroupAccessPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Update &&\n            x.AccessPolicy.GroupId == groupPolicyChanges.Update.GroupId));\n        Assert.Equal(result.GroupAccessPolicyUpdates.Count() - 2,\n            result.GroupAccessPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));\n\n        Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == serviceAccountPolicyChanges.Delete));\n        Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Update &&\n            x.AccessPolicy.ServiceAccountId == serviceAccountPolicyChanges.Update.ServiceAccountId));\n        Assert.Equal(result.ServiceAccountAccessPolicyUpdates.Count() - 2,\n            result.ServiceAccountAccessPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));\n    }\n\n    private static (UserSecretAccessPolicy Update, UserSecretAccessPolicy Delete) SetupUserAccessPolicies(\n        SecretAccessPolicies data, UserSecretAccessPolicy currentPolicyToDelete)\n    {\n        currentPolicyToDelete.GrantedSecretId = data.SecretId;\n\n        var updatePolicy = new UserSecretAccessPolicy\n        {\n            OrganizationUserId = data.UserAccessPolicies.First().OrganizationUserId,\n            GrantedSecretId = data.SecretId,\n            Read = !data.UserAccessPolicies.First().Read,\n            Write = !data.UserAccessPolicies.First().Write\n        };\n\n        return (updatePolicy, currentPolicyToDelete);\n    }\n\n    private static (GroupSecretAccessPolicy Update, GroupSecretAccessPolicy Delete) SetupGroupAccessPolicies(\n        SecretAccessPolicies data, GroupSecretAccessPolicy currentPolicyToDelete)\n    {\n        currentPolicyToDelete.GrantedSecretId = data.SecretId;\n\n        var updatePolicy = new GroupSecretAccessPolicy\n        {\n            GroupId = data.GroupAccessPolicies.First().GroupId,\n            GrantedSecretId = data.SecretId,\n            Read = !data.GroupAccessPolicies.First().Read,\n            Write = !data.GroupAccessPolicies.First().Write\n        };\n\n        return (updatePolicy, currentPolicyToDelete);\n    }\n\n    private static (ServiceAccountSecretAccessPolicy Update, ServiceAccountSecretAccessPolicy Delete)\n        SetupServiceAccountAccessPolicies(SecretAccessPolicies data,\n            ServiceAccountSecretAccessPolicy currentPolicyToDelete)\n    {\n        currentPolicyToDelete.GrantedSecretId = data.SecretId;\n\n        var updatePolicy = new ServiceAccountSecretAccessPolicy\n        {\n            ServiceAccountId = data.ServiceAccountAccessPolicies.First().ServiceAccountId,\n            GrantedSecretId = data.SecretId,\n            Read = !data.ServiceAccountAccessPolicies.First().Read,\n            Write = !data.ServiceAccountAccessPolicies.First().Write\n        };\n\n        return (updatePolicy, currentPolicyToDelete);\n    }\n\n    private static SecretAccessPolicies SetupSecretAccessPolicies(SecretAccessPolicies data)\n    {\n        foreach (var policy in data.UserAccessPolicies)\n        {\n            policy.GrantedSecretId = data.SecretId;\n        }\n\n        foreach (var policy in data.GroupAccessPolicies)\n        {\n            policy.GrantedSecretId = data.SecretId;\n        }\n\n        foreach (var policy in data.ServiceAccountAccessPolicies)\n        {\n            policy.GrantedSecretId = data.SecretId;\n        }\n\n        return data;\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/AccessPolicies/ServiceAccountGrantedPolicyUpdatesQueryTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class ServiceAccountGrantedPolicyUpdatesQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetAsync_NoCurrentGrantedPolicies_ReturnsAllCreates(\n        SutProvider<ServiceAccountGrantedPolicyUpdatesQuery> sutProvider,\n        ServiceAccountGrantedPolicies data)\n    {\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetServiceAccountGrantedPoliciesAsync(data.ServiceAccountId)\n            .ReturnsNullForAnyArgs();\n\n        var result = await sutProvider.Sut.GetAsync(data);\n\n        Assert.Equal(data.ServiceAccountId, result.ServiceAccountId);\n        Assert.Equal(data.OrganizationId, result.OrganizationId);\n        Assert.Equal(data.ProjectGrantedPolicies.Count(), result.ProjectGrantedPolicyUpdates.Count());\n        Assert.All(result.ProjectGrantedPolicyUpdates, p =>\n        {\n            Assert.Equal(AccessPolicyOperation.Create, p.Operation);\n            Assert.Contains(data.ProjectGrantedPolicies, x => x == p.AccessPolicy);\n        });\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAsync_CurrentGrantedPolicies_ReturnsChanges(\n        SutProvider<ServiceAccountGrantedPolicyUpdatesQuery> sutProvider,\n        ServiceAccountGrantedPolicies data, ServiceAccountProjectAccessPolicy currentPolicyToDelete)\n    {\n        foreach (var grantedPolicy in data.ProjectGrantedPolicies)\n        {\n            grantedPolicy.ServiceAccountId = data.ServiceAccountId;\n        }\n\n        currentPolicyToDelete.ServiceAccountId = data.ServiceAccountId;\n\n        var updatePolicy = new ServiceAccountProjectAccessPolicy\n        {\n            ServiceAccountId = data.ServiceAccountId,\n            GrantedProjectId = data.ProjectGrantedPolicies.First().GrantedProjectId,\n            Read = !data.ProjectGrantedPolicies.First().Read,\n            Write = !data.ProjectGrantedPolicies.First().Write\n        };\n\n        var currentPolicies = new ServiceAccountGrantedPolicies\n        {\n            ServiceAccountId = data.ServiceAccountId,\n            OrganizationId = data.OrganizationId,\n            ProjectGrantedPolicies = [updatePolicy, currentPolicyToDelete]\n        };\n\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetServiceAccountGrantedPoliciesAsync(data.ServiceAccountId)\n            .ReturnsForAnyArgs(currentPolicies);\n\n        var result = await sutProvider.Sut.GetAsync(data);\n\n        Assert.Equal(data.ServiceAccountId, result.ServiceAccountId);\n        Assert.Equal(data.OrganizationId, result.OrganizationId);\n        Assert.Single(result.ProjectGrantedPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == currentPolicyToDelete));\n        Assert.Single(result.ProjectGrantedPolicyUpdates.Where(x =>\n            x.Operation == AccessPolicyOperation.Update &&\n            x.AccessPolicy.GrantedProjectId == updatePolicy.GrantedProjectId));\n        Assert.Equal(result.ProjectGrantedPolicyUpdates.Count() - 2,\n            result.ProjectGrantedPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Queries.Projects;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;\n\n[SutProviderCustomize]\npublic class MaxProjectsQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetByOrgIdAsync_SelfHosted_ReturnsNulls(SutProvider<MaxProjectsQuery> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);\n\n        var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1);\n\n        Assert.Null(max);\n        Assert.Null(overMax);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1));\n\n        await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()\n            .GetProjectCountByOrganizationIdAsync(organizationId);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsStarter)]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,\n        SutProvider<MaxProjectsQuery> sutProvider, Organization organization)\n    {\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);\n\n        organization.PlanType = planType;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1);\n\n        Assert.Null(limit);\n        Assert.Null(overLimit);\n\n        await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()\n            .GetProjectCountByOrganizationIdAsync(organization.Id);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Free, 0, 1, false)]\n    [BitAutoData(PlanType.Free, 1, 1, false)]\n    [BitAutoData(PlanType.Free, 2, 1, false)]\n    [BitAutoData(PlanType.Free, 3, 1, true)]\n    [BitAutoData(PlanType.Free, 4, 1, true)]\n    [BitAutoData(PlanType.Free, 40, 1, true)]\n    [BitAutoData(PlanType.Free, 0, 2, false)]\n    [BitAutoData(PlanType.Free, 1, 2, false)]\n    [BitAutoData(PlanType.Free, 2, 2, true)]\n    [BitAutoData(PlanType.Free, 3, 2, true)]\n    [BitAutoData(PlanType.Free, 4, 2, true)]\n    [BitAutoData(PlanType.Free, 40, 2, true)]\n    [BitAutoData(PlanType.Free, 0, 3, false)]\n    [BitAutoData(PlanType.Free, 1, 3, true)]\n    [BitAutoData(PlanType.Free, 2, 3, true)]\n    [BitAutoData(PlanType.Free, 3, 3, true)]\n    [BitAutoData(PlanType.Free, 4, 3, true)]\n    [BitAutoData(PlanType.Free, 40, 3, true)]\n    [BitAutoData(PlanType.Free, 0, 4, true)]\n    [BitAutoData(PlanType.Free, 1, 4, true)]\n    [BitAutoData(PlanType.Free, 2, 4, true)]\n    [BitAutoData(PlanType.Free, 3, 4, true)]\n    [BitAutoData(PlanType.Free, 4, 4, true)]\n    [BitAutoData(PlanType.Free, 40, 4, true)]\n    public async Task GetByOrgIdAsync_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax,\n        SutProvider<MaxProjectsQuery> sutProvider, Organization organization)\n    {\n        organization.PlanType = planType;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)\n            .Returns(projects);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlan(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd);\n\n        Assert.NotNull(max);\n        Assert.NotNull(overMax);\n        Assert.Equal(3, max.Value);\n        Assert.Equal(expectedOverMax, overMax);\n\n        await sutProvider.GetDependency<IProjectRepository>().Received(1)\n            .GetProjectCountByOrganizationIdAsync(organization.Id);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Secrets/SecretsSyncQueryTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Commercial.Core.SecretsManager.Queries.Secrets;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Queries.Secrets;\n\n[SutProviderCustomize]\npublic class SecretsSyncQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task GetAsync_NullLastSyncedDate_ReturnsHasChanges(\n        SutProvider<SecretsSyncQuery> sutProvider,\n        SecretsSyncRequest data)\n    {\n        data.LastSyncedDate = null;\n\n        var result = await sutProvider.Sut.GetAsync(data);\n\n        Assert.True(result.HasChanges);\n        await sutProvider.GetDependency<ISecretRepository>().Received(1)\n            .GetManyByOrganizationIdAsync(Arg.Is(data.OrganizationId),\n                Arg.Is(data.ServiceAccountId),\n                Arg.Is(data.AccessClientType));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_HasLastSyncedDateServiceAccountNotFound_Throws(\n        SutProvider<SecretsSyncQuery> sutProvider,\n        SecretsSyncRequest data)\n    {\n        data.LastSyncedDate = DateTime.UtcNow;\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)\n            .Returns((ServiceAccount?)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(data));\n\n        await sutProvider.GetDependency<ISecretRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .GetManyByOrganizationIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());\n    }\n\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task GetAsync_HasLastSyncedDateServiceAccountWithLaterOrEqualRevisionDate_ReturnsChanges(\n        bool datesEqual,\n        SutProvider<SecretsSyncQuery> sutProvider,\n        SecretsSyncRequest data,\n        ServiceAccount serviceAccount)\n    {\n        data.LastSyncedDate = DateTime.UtcNow.AddDays(-1);\n        serviceAccount.Id = data.ServiceAccountId;\n        serviceAccount.RevisionDate = datesEqual ? data.LastSyncedDate.Value : data.LastSyncedDate.Value.AddSeconds(600);\n\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)\n            .Returns(serviceAccount);\n\n        var result = await sutProvider.Sut.GetAsync(data);\n\n        Assert.True(result.HasChanges);\n        await sutProvider.GetDependency<ISecretRepository>().Received(1)\n            .GetManyByOrganizationIdAsync(Arg.Is(data.OrganizationId),\n                Arg.Is(data.ServiceAccountId),\n                Arg.Is(data.AccessClientType));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_HasLastSyncedDateServiceAccountWithEarlierRevisionDate_ReturnsNoChanges(\n        SutProvider<SecretsSyncQuery> sutProvider,\n        SecretsSyncRequest data,\n        ServiceAccount serviceAccount)\n    {\n        data.LastSyncedDate = DateTime.UtcNow.AddDays(-1);\n        serviceAccount.Id = data.ServiceAccountId;\n        serviceAccount.RevisionDate = data.LastSyncedDate.Value.AddDays(-2);\n\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)\n            .Returns(serviceAccount);\n\n        var result = await sutProvider.Sut.GetAsync(data);\n\n        Assert.False(result.HasChanges);\n        Assert.Null(result.Secrets);\n        await sutProvider.GetDependency<ISecretRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .GetManyByOrganizationIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/ServiceAccounts/CountNewServiceAccountSlotsRequiredQueryTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Queries.ServiceAccounts;\n\n[SutProviderCustomize]\npublic class CountNewServiceAccountSlotsRequiredQueryTests\n{\n    [Theory]\n    [BitAutoData(2, 5, 2, 0)]\n    [BitAutoData(0, 5, 2, 0)]\n    [BitAutoData(6, 5, 2, 3)]\n    [BitAutoData(2, 5, 10, 7)]\n    public async Task CountNewServiceAccountSlotsRequiredAsync_ReturnsCorrectCount(\n        int serviceAccountsToAdd,\n        int organizationSmServiceAccounts,\n        int currentServiceAccounts,\n        int expectedNewServiceAccountsRequired,\n        Organization organization,\n        SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)\n    {\n        organization.UseSecretsManager = true;\n        organization.SmServiceAccounts = organizationSmServiceAccounts;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .GetServiceAccountCountByOrganizationIdAsync(organization.Id)\n            .Returns(currentServiceAccounts);\n\n        var result = await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organization.Id, serviceAccountsToAdd);\n\n        Assert.Equal(expectedNewServiceAccountsRequired, result);\n\n        if (serviceAccountsToAdd > 0)\n        {\n            await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)\n                .GetServiceAccountCountByOrganizationIdAsync(organization.Id);\n        }\n    }\n\n    [Theory]\n    [BitAutoData(0)]\n    [BitAutoData(5)]\n    public async Task CountNewServiceAccountSlotsRequiredAsync_WithNullSmServiceAccounts_ReturnsZero(\n        int currentServiceAccounts,\n        int serviceAccountsToAdd,\n        Organization organization,\n        SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)\n    {\n        const int expectedRequiredServiceAccountsToScale = 0;\n\n        organization.UseSecretsManager = true;\n        organization.SmServiceAccounts = null;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .GetServiceAccountCountByOrganizationIdAsync(organization.Id)\n            .Returns(currentServiceAccounts);\n\n        var result = await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organization.Id, serviceAccountsToAdd);\n\n        Assert.Equal(expectedRequiredServiceAccountsToScale, result);\n\n        await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs()\n            .GetServiceAccountCountByOrganizationIdAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CountNewServiceAccountSlotsRequiredAsync_WithNonExistentOrganizationId_ThrowsNotFound(\n        Guid organizationId, int serviceAccountsToAdd,\n        SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organizationId, serviceAccountsToAdd));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CountNewServiceAccountSlotsRequiredAsync_WithOrganizationUseSecretsManagerFalse_ThrowsNotFound(\n        Organization organization, int serviceAccountsToAdd,\n        SutProvider<CountNewServiceAccountSlotsRequiredQuery> sutProvider)\n    {\n        organization.UseSecretsManager = false;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CountNewServiceAccountSlotsRequiredAsync(organization.Id, serviceAccountsToAdd));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/ServiceAccounts/ServiceAccountSecretsDetailsQueryTests.cs",
    "content": "﻿using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Queries.ServiceAccounts;\n\n[SutProviderCustomize]\npublic class ServiceAccountSecretsDetailsQueryTests\n{\n    [Theory]\n    [BitAutoData(false)]\n    [BitAutoData(true)]\n    public async Task GetManyByOrganizationId_CallsDifferentRepoMethods(\n        bool includeAccessToSecrets,\n        SutProvider<ServiceAccountSecretsDetailsQuery> sutProvider,\n        Guid organizationId,\n        Guid userId,\n        AccessClientType accessClient,\n        ServiceAccount mockSa,\n        ServiceAccountSecretsDetails mockSaDetails)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetManyByOrganizationIdAsync(default, default, default)\n            .ReturnsForAnyArgs(new List<ServiceAccount> { mockSa });\n\n        sutProvider.GetDependency<IServiceAccountRepository>().GetManyByOrganizationIdWithSecretsDetailsAsync(default, default, default)\n            .ReturnsForAnyArgs(new List<ServiceAccountSecretsDetails> { mockSaDetails });\n\n\n        var result = await sutProvider.Sut.GetManyByOrganizationIdAsync(organizationId, userId, accessClient, includeAccessToSecrets);\n\n        if (includeAccessToSecrets)\n        {\n            await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)\n                .GetManyByOrganizationIdWithSecretsDetailsAsync(Arg.Is(AssertHelper.AssertPropertyEqual(mockSaDetails.ServiceAccount.OrganizationId)),\n                    Arg.Any<Guid>(), Arg.Any<AccessClientType>());\n        }\n        else\n        {\n            await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)\n                .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(mockSa.OrganizationId)),\n                    Arg.Any<Guid>(), Arg.Any<AccessClientType>());\n            Assert.Equal(0, result.First().AccessToSecrets);\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Commercial.Core.Test/SecretsManager/Repositories/SecretVersionRepositoryTests.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Commercial.Core.Test.SecretsManager.Repositories;\n\npublic class SecretVersionRepositoryTests\n{\n    [Theory]\n    [BitAutoData]\n    public void SecretVersion_EntityCreation_Success(SecretVersion secretVersion)\n    {\n        // Arrange & Act\n        secretVersion.SetNewId();\n\n        // Assert\n        Assert.NotEqual(Guid.Empty, secretVersion.Id);\n        Assert.NotEqual(Guid.Empty, secretVersion.SecretId);\n        Assert.NotNull(secretVersion.Value);\n        Assert.NotEqual(default, secretVersion.VersionDate);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void SecretVersion_WithServiceAccountEditor_Success(SecretVersion secretVersion, Guid serviceAccountId)\n    {\n        // Arrange & Act\n        secretVersion.EditorServiceAccountId = serviceAccountId;\n        secretVersion.EditorOrganizationUserId = null;\n\n        // Assert\n        Assert.Equal(serviceAccountId, secretVersion.EditorServiceAccountId);\n        Assert.Null(secretVersion.EditorOrganizationUserId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void SecretVersion_WithOrganizationUserEditor_Success(SecretVersion secretVersion, Guid organizationUserId)\n    {\n        // Arrange & Act\n        secretVersion.EditorOrganizationUserId = organizationUserId;\n        secretVersion.EditorServiceAccountId = null;\n\n        // Assert\n        Assert.Equal(organizationUserId, secretVersion.EditorOrganizationUserId);\n        Assert.Null(secretVersion.EditorServiceAccountId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void SecretVersion_NullableEditors_Success(SecretVersion secretVersion)\n    {\n        // Arrange & Act\n        secretVersion.EditorServiceAccountId = null;\n        secretVersion.EditorOrganizationUserId = null;\n\n        // Assert\n        Assert.Null(secretVersion.EditorServiceAccountId);\n        Assert.Null(secretVersion.EditorOrganizationUserId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void SecretVersion_VersionDateSet_Success(SecretVersion secretVersion)\n    {\n        // Arrange\n        var versionDate = DateTime.UtcNow;\n\n        // Act\n        secretVersion.VersionDate = versionDate;\n\n        // Assert\n        Assert.Equal(versionDate, secretVersion.VersionDate);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void SecretVersion_ValueEncrypted_Success(SecretVersion secretVersion, string encryptedValue)\n    {\n        // Arrange & Act\n        secretVersion.Value = encryptedValue;\n\n        // Assert\n        Assert.Equal(encryptedValue, secretVersion.Value);\n        Assert.NotEmpty(secretVersion.Value);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void SecretVersion_MultipleVersions_DifferentIds(List<SecretVersion> secretVersions, Guid secretId)\n    {\n        // Arrange & Act\n        foreach (var version in secretVersions)\n        {\n            version.SecretId = secretId;\n            version.SetNewId();\n        }\n\n        // Assert\n        var distinctIds = secretVersions.Select(v => v.Id).Distinct();\n        Assert.Equal(secretVersions.Count, distinctIds.Count());\n        Assert.All(secretVersions, v => Assert.Equal(secretId, v.SecretId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void SecretVersion_VersionDateOrdering_Success(SecretVersion version1, SecretVersion version2, SecretVersion version3, Guid secretId)\n    {\n        // Arrange\n        var now = DateTime.UtcNow;\n        version1.SecretId = secretId;\n        version1.VersionDate = now.AddDays(-2);\n\n        version2.SecretId = secretId;\n        version2.VersionDate = now.AddDays(-1);\n\n        version3.SecretId = secretId;\n        version3.VersionDate = now;\n\n        var versions = new List<SecretVersion> { version2, version3, version1 };\n\n        // Act\n        var orderedVersions = versions.OrderByDescending(v => v.VersionDate).ToList();\n\n        // Assert\n        Assert.Equal(version3.Id, orderedVersions[0].Id); // Most recent\n        Assert.Equal(version2.Id, orderedVersions[1].Id);\n        Assert.Equal(version1.Id, orderedVersions[2].Id); // Oldest\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/SSO.Test/Controllers/AccountControllerTest.cs",
    "content": "﻿using System.Reflection;\nusing System.Security.Claims;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Bit.Sso.Controllers;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer.Configuration;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Services;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.DependencyInjection;\nusing NSubstitute;\nusing Xunit.Abstractions;\nusing AuthenticationOptions = Duende.IdentityServer.Configuration.AuthenticationOptions;\n\nnamespace Bit.SSO.Test.Controllers;\n\n[ControllerCustomize(typeof(AccountController)), SutProviderCustomize]\npublic class AccountControllerTest\n{\n    private readonly ITestOutputHelper _output;\n\n    public AccountControllerTest(ITestOutputHelper output)\n    {\n        _output = output;\n    }\n\n    private static IAuthenticationService SetupHttpContextWithAuth(\n        SutProvider<AccountController> sutProvider,\n        AuthenticateResult authResult,\n        IAuthenticationService? authService = null)\n    {\n        var schemeProvider = Substitute.For<IAuthenticationSchemeProvider>();\n        schemeProvider.GetDefaultAuthenticateSchemeAsync()\n            .Returns(new AuthenticationScheme(\"idsrv\", \"idsrv\", typeof(IAuthenticationHandler)));\n\n        var resolvedAuthService = authService ?? Substitute.For<IAuthenticationService>();\n        resolvedAuthService.AuthenticateAsync(\n                Arg.Any<HttpContext>(),\n                AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)\n            .Returns(authResult);\n\n        var services = new ServiceCollection();\n        services.AddSingleton(resolvedAuthService);\n        services.AddSingleton<IAuthenticationSchemeProvider>(schemeProvider);\n        services.AddSingleton(new IdentityServerOptions\n        {\n            Authentication = new AuthenticationOptions\n            {\n                CookieAuthenticationScheme = \"idsrv\"\n            }\n        });\n        var sp = services.BuildServiceProvider();\n\n        sutProvider.Sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext\n            {\n                RequestServices = sp\n            }\n        };\n\n        return resolvedAuthService;\n    }\n\n    private static AuthenticateResult BuildSuccessfulExternalAuth(Guid orgId, string providerUserId, string email)\n    {\n        var claims = new[]\n        {\n            new Claim(JwtClaimTypes.Subject, providerUserId),\n            new Claim(JwtClaimTypes.Email, email)\n        };\n        var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, \"External\"));\n        var properties = new AuthenticationProperties\n        {\n            Items =\n            {\n                [\"scheme\"] = orgId.ToString(),\n                [\"return_url\"] = \"~/\",\n                [\"state\"] = \"state\",\n                [\"user_identifier\"] = string.Empty\n            }\n        };\n        var ticket = new AuthenticationTicket(principal, properties, AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);\n        return AuthenticateResult.Success(ticket);\n    }\n\n    private static void ConfigureSsoAndUser(\n        SutProvider<AccountController> sutProvider,\n        Guid orgId,\n        string providerUserId,\n        User user,\n        Organization? organization = null,\n        OrganizationUser? orgUser = null)\n    {\n        var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        var ssoConfig = new SsoConfig { OrganizationId = orgId, Enabled = true };\n        var ssoData = new SsoConfigurationData();\n        ssoConfig.SetData(ssoData);\n        ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);\n\n        userRepository.GetBySsoUserAsync(providerUserId, orgId).Returns(user);\n\n        if (organization != null)\n        {\n            organizationRepository.GetByIdAsync(orgId).Returns(organization);\n        }\n        if (organization != null && orgUser != null)\n        {\n            organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id).Returns(orgUser);\n            organizationUserRepository.GetManyByUserAsync(user.Id).Returns([orgUser]);\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExternalCallback_ExistingUser_NoOrgUser_ThrowsCouldNotFindOrganizationUser(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"ext-missing-orguser\";\n        var user = new User { Id = Guid.NewGuid(), Email = \"missing.orguser@example.com\" };\n        var organization = new Organization { Id = orgId, Name = \"Org\" };\n\n        var authResult = BuildSuccessfulExternalAuth(orgId, providerUserId, user.Email!);\n        SetupHttpContextWithAuth(sutProvider, authResult);\n\n        // i18n returns the key so we can assert on message contents\n        sutProvider.GetDependency<II18nService>()\n            .T(Arg.Any<string>(), Arg.Any<object?[]>())\n            .Returns(ci => (string)ci[0]!);\n\n        // SSO config + user link exists, but no org user membership\n        ConfigureSsoAndUser(\n            sutProvider,\n            orgId,\n            providerUserId,\n            user,\n            organization,\n            orgUser: null);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organization.Id, user.Id).Returns((OrganizationUser?)null);\n\n        sutProvider.GetDependency<IIdentityServerInteractionService>()\n            .GetAuthorizationContextAsync(\"~/\").Returns((AuthorizationRequest?)null);\n\n        // Act + Assert\n        var ex = await Assert.ThrowsAsync<Exception>(() => sutProvider.Sut.ExternalCallback());\n        Assert.Equal(\"CouldNotFindOrganizationUser\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExternalCallback_ExistingUser_OrgUserInvited_AllowsLogin(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"ext-invited-orguser\";\n        var user = new User { Id = Guid.NewGuid(), Email = \"invited.orguser@example.com\" };\n        var organization = new Organization { Id = orgId, Name = \"Org\" };\n        var orgUser = new OrganizationUser\n        {\n            OrganizationId = orgId,\n            UserId = user.Id,\n            Status = OrganizationUserStatusType.Invited,\n            Type = OrganizationUserType.User\n        };\n\n        var authResult = BuildSuccessfulExternalAuth(orgId, providerUserId, user.Email!);\n        var authService = SetupHttpContextWithAuth(sutProvider, authResult);\n\n        sutProvider.GetDependency<II18nService>()\n            .T(Arg.Any<string>(), Arg.Any<object?[]>())\n            .Returns(ci => (string)ci[0]!);\n\n        ConfigureSsoAndUser(\n            sutProvider,\n            orgId,\n            providerUserId,\n            user,\n            organization,\n            orgUser);\n\n        sutProvider.GetDependency<IIdentityServerInteractionService>()\n            .GetAuthorizationContextAsync(\"~/\").Returns((AuthorizationRequest?)null);\n\n        // Act\n        var result = await sutProvider.Sut.ExternalCallback();\n\n        // Assert\n        var redirect = Assert.IsType<RedirectResult>(result);\n        Assert.Equal(\"~/\", redirect.Url);\n\n        await authService.Received().SignInAsync(\n            Arg.Any<HttpContext>(),\n            Arg.Any<string?>(),\n            Arg.Any<ClaimsPrincipal>(),\n            Arg.Any<AuthenticationProperties>());\n\n        await authService.Received().SignOutAsync(\n            Arg.Any<HttpContext>(),\n            AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,\n            Arg.Any<AuthenticationProperties>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExternalCallback_ExistingUser_OrgUserRevoked_ThrowsAccessRevoked(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"ext-revoked-orguser\";\n        var user = new User { Id = Guid.NewGuid(), Email = \"revoked.orguser@example.com\" };\n        var organization = new Organization { Id = orgId, Name = \"Org\" };\n        var orgUser = new OrganizationUser\n        {\n            OrganizationId = orgId,\n            UserId = user.Id,\n            Status = OrganizationUserStatusType.Revoked,\n            Type = OrganizationUserType.User\n        };\n\n        var authResult = BuildSuccessfulExternalAuth(orgId, providerUserId, user.Email!);\n        SetupHttpContextWithAuth(sutProvider, authResult);\n\n        sutProvider.GetDependency<II18nService>()\n            .T(Arg.Any<string>(), Arg.Any<object?[]>())\n            .Returns(ci => (string)ci[0]!);\n\n        ConfigureSsoAndUser(\n            sutProvider,\n            orgId,\n            providerUserId,\n            user,\n            organization,\n            orgUser);\n\n        sutProvider.GetDependency<IIdentityServerInteractionService>()\n            .GetAuthorizationContextAsync(\"~/\").Returns((AuthorizationRequest?)null);\n\n        // Act + Assert\n        var ex = await Assert.ThrowsAsync<Exception>(() => sutProvider.Sut.ExternalCallback());\n        Assert.Equal(\"OrganizationUserAccessRevoked\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExternalCallback_ExistingUser_OrgUserUnknown_ThrowsUnknown(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"ext-unknown-orguser\";\n        var user = new User { Id = Guid.NewGuid(), Email = \"unknown.orguser@example.com\" };\n        var organization = new Organization { Id = orgId, Name = \"Org\" };\n        var unknownStatus = (OrganizationUserStatusType)999;\n        var orgUser = new OrganizationUser\n        {\n            OrganizationId = orgId,\n            UserId = user.Id,\n            Status = unknownStatus,\n            Type = OrganizationUserType.User\n        };\n\n        var authResult = BuildSuccessfulExternalAuth(orgId, providerUserId, user.Email!);\n        SetupHttpContextWithAuth(sutProvider, authResult);\n\n        sutProvider.GetDependency<II18nService>()\n            .T(Arg.Any<string>(), Arg.Any<object?[]>())\n            .Returns(ci => (string)ci[0]!);\n\n        ConfigureSsoAndUser(\n            sutProvider,\n            orgId,\n            providerUserId,\n            user,\n            organization,\n            orgUser);\n\n        sutProvider.GetDependency<IIdentityServerInteractionService>()\n            .GetAuthorizationContextAsync(\"~/\").Returns((AuthorizationRequest?)null);\n\n        // Act + Assert\n        var ex = await Assert.ThrowsAsync<Exception>(() => sutProvider.Sut.ExternalCallback());\n        Assert.Equal(\"OrganizationUserUnknownStatus\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExternalCallback_WithExistingUserAndAcceptedMembership_RedirectsToReturnUrl(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"ext-123\";\n        var user = new User { Id = Guid.NewGuid(), Email = \"user@example.com\" };\n        var organization = new Organization { Id = orgId, Name = \"Test Org\" };\n        var orgUser = new OrganizationUser\n        {\n            OrganizationId = orgId,\n            UserId = user.Id,\n            Status = OrganizationUserStatusType.Accepted,\n            Type = OrganizationUserType.User\n        };\n\n        var authResult = BuildSuccessfulExternalAuth(orgId, providerUserId, user.Email!);\n        var authService = SetupHttpContextWithAuth(sutProvider, authResult);\n\n        ConfigureSsoAndUser(\n            sutProvider,\n            orgId,\n            providerUserId,\n            user,\n            organization,\n            orgUser);\n\n        sutProvider.GetDependency<IIdentityServerInteractionService>()\n            .GetAuthorizationContextAsync(\"~/\").Returns((AuthorizationRequest?)null);\n\n        // Act\n        var result = await sutProvider.Sut.ExternalCallback();\n\n        // Assert\n        var redirect = Assert.IsType<RedirectResult>(result);\n        Assert.Equal(\"~/\", redirect.Url);\n\n        await authService.Received().SignInAsync(\n            Arg.Any<HttpContext>(),\n            Arg.Any<string?>(),\n            Arg.Any<ClaimsPrincipal>(),\n            Arg.Any<AuthenticationProperties>());\n\n        await authService.Received().SignOutAsync(\n            Arg.Any<HttpContext>(),\n            AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,\n            Arg.Any<AuthenticationProperties>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExternalCallback_ExistingSsoLinkedAccepted_MeasureLookups(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"ext-measure-existing\";\n        var user = new User { Id = Guid.NewGuid(), Email = \"existing@example.com\" };\n        var organization = new Organization { Id = orgId, Name = \"Org\" };\n        var orgUser = new OrganizationUser\n        {\n            OrganizationId = orgId,\n            UserId = user.Id,\n            Status = OrganizationUserStatusType.Accepted,\n            Type = OrganizationUserType.User\n        };\n\n        var authResult = BuildSuccessfulExternalAuth(orgId, providerUserId, user.Email);\n        SetupHttpContextWithAuth(sutProvider, authResult);\n\n        ConfigureSsoAndUser(\n            sutProvider,\n            orgId,\n            providerUserId,\n            user,\n            organization,\n            orgUser);\n\n        sutProvider.GetDependency<IIdentityServerInteractionService>()\n            .GetAuthorizationContextAsync(\"~/\").Returns((AuthorizationRequest?)null);\n\n        // Act\n        try\n        {\n            _ = await sutProvider.Sut.ExternalCallback();\n        }\n        catch\n        {\n            // ignore for measurement only\n        }\n\n        // Assert (measurement only - no asserts on counts)\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        var userGetBySso = userRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IUserRepository.GetBySsoUserAsync));\n        var userGetByEmail = userRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IUserRepository.GetByEmailAsync));\n        var orgGet = organizationRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationRepository.GetByIdAsync));\n        var orgUserGetByOrg = organizationUserRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationUserRepository.GetByOrganizationAsync))\n            + organizationUserRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationUserRepository.GetManyByUserAsync));\n        var orgUserGetByEmail = organizationUserRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationUserRepository.GetByOrganizationEmailAsync));\n\n        _output.WriteLine($\"GetBySsoUserAsync: {userGetBySso}\");\n        _output.WriteLine($\"GetByEmailAsync: {userGetByEmail}\");\n        _output.WriteLine($\"GetByIdAsync (Org): {orgGet}\");\n        _output.WriteLine($\"GetByOrganizationAsync (OrgUser): {orgUserGetByOrg}\");\n        _output.WriteLine($\"GetByOrganizationEmailAsync (OrgUser): {orgUserGetByEmail}\");\n\n        // Snapshot assertions\n        Assert.Equal(1, userGetBySso);\n        Assert.Equal(0, userGetByEmail);\n        Assert.Equal(1, orgGet);\n        Assert.Equal(1, orgUserGetByOrg);\n        Assert.Equal(0, orgUserGetByEmail);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExternalCallback_JitProvision_MeasureLookups(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"ext-measure-jit\";\n        var email = \"jit.measure@example.com\";\n        var organization = new Organization { Id = orgId, Name = \"Org\", Seats = null };\n\n        var authResult = BuildSuccessfulExternalAuth(orgId, providerUserId, email);\n        SetupHttpContextWithAuth(sutProvider, authResult);\n\n        var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        var ssoConfig = new SsoConfig { OrganizationId = orgId, Enabled = true };\n        var ssoData = new SsoConfigurationData();\n        ssoConfig.SetData(ssoData);\n        ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);\n\n        // JIT (no existing user or sso link)\n        userRepository.GetBySsoUserAsync(providerUserId, orgId).Returns((User?)null);\n        userRepository.GetByEmailAsync(email).Returns((User?)null);\n        organizationRepository.GetByIdAsync(orgId).Returns(organization);\n        organizationUserRepository.GetByOrganizationEmailAsync(orgId, email).Returns((OrganizationUser?)null);\n\n        sutProvider.GetDependency<IIdentityServerInteractionService>()\n            .GetAuthorizationContextAsync(\"~/\").Returns((AuthorizationRequest?)null);\n\n        // Act\n        try\n        {\n            _ = await sutProvider.Sut.ExternalCallback();\n        }\n        catch\n        {\n            // JIT path may throw due to Invited status under enforcement; ignore for measurement\n        }\n\n        // Assert (measurement only - no asserts on counts)\n        var userGetBySso = userRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IUserRepository.GetBySsoUserAsync));\n        var userGetByEmail = userRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IUserRepository.GetByEmailAsync));\n        var orgGet = organizationRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationRepository.GetByIdAsync));\n        var orgUserGetByOrg = organizationUserRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationUserRepository.GetByOrganizationAsync));\n        var orgUserGetByEmail = organizationUserRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationUserRepository.GetByOrganizationEmailAsync));\n\n        _output.WriteLine($\"GetBySsoUserAsync: {userGetBySso}\");\n        _output.WriteLine($\"GetByEmailAsync: {userGetByEmail}\");\n        _output.WriteLine($\"GetByIdAsync (Org): {orgGet}\");\n        _output.WriteLine($\"GetByOrganizationAsync (OrgUser): {orgUserGetByOrg}\");\n        _output.WriteLine($\"GetByOrganizationEmailAsync (OrgUser): {orgUserGetByEmail}\");\n\n        // Snapshot assertions\n        Assert.Equal(1, userGetBySso);\n        Assert.Equal(1, userGetByEmail);\n        Assert.Equal(1, orgGet);\n        Assert.Equal(0, orgUserGetByOrg);\n        Assert.Equal(1, orgUserGetByEmail);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExternalCallback_ExistingUser_NoOrgUser_MeasureLookups(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"ext-measure-existing-no-orguser\";\n        var user = new User { Id = Guid.NewGuid(), Email = \"existing2@example.com\" };\n        var organization = new Organization { Id = orgId, Name = \"Org\" };\n\n        var authResult = BuildSuccessfulExternalAuth(orgId, providerUserId, user.Email!);\n        SetupHttpContextWithAuth(sutProvider, authResult);\n\n        ConfigureSsoAndUser(\n            sutProvider,\n            orgId,\n            providerUserId,\n            user,\n            organization,\n            orgUser: null);\n\n        // Ensure orgUser lookup returns null\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organization.Id, user.Id).Returns((OrganizationUser?)null);\n\n        sutProvider.GetDependency<IIdentityServerInteractionService>()\n            .GetAuthorizationContextAsync(\"~/\").Returns((AuthorizationRequest?)null);\n\n        // Act\n        try\n        {\n            _ = await sutProvider.Sut.ExternalCallback();\n        }\n        catch\n        {\n            // ignore for measurement only\n        }\n\n        // Assert (measurement only - no asserts on counts)\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        var userGetBySso = userRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IUserRepository.GetBySsoUserAsync));\n        var userGetByEmail = userRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IUserRepository.GetByEmailAsync));\n        var orgGet = organizationRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationRepository.GetByIdAsync));\n        var orgUserGetByOrg = organizationUserRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationUserRepository.GetByOrganizationAsync))\n            + organizationUserRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationUserRepository.GetManyByUserAsync));\n        var orgUserGetByEmail = organizationUserRepository.ReceivedCalls().Count(c => c.GetMethodInfo().Name == nameof(IOrganizationUserRepository.GetByOrganizationEmailAsync));\n\n        _output.WriteLine($\"GetBySsoUserAsync: {userGetBySso}\");\n        _output.WriteLine($\"GetByEmailAsync: {userGetByEmail}\");\n        _output.WriteLine($\"GetByIdAsync (Org): {orgGet}\");\n        _output.WriteLine($\"GetByOrganizationAsync (OrgUser): {orgUserGetByOrg}\");\n        _output.WriteLine($\"GetByOrganizationEmailAsync (OrgUser): {orgUserGetByEmail}\");\n\n        // Snapshot assertions\n        Assert.Equal(1, userGetBySso);\n        Assert.Equal(0, userGetByEmail);\n        Assert.Equal(1, orgGet);\n        Assert.Equal(1, orgUserGetByOrg);\n        Assert.Equal(1, orgUserGetByEmail);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateUserAndOrgUserConditionallyAsync_WithExistingAcceptedUser_CreatesSsoLinkAndReturnsUser(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"provider-user-id\";\n        var email = \"user@example.com\";\n        var existingUser = new User { Id = Guid.NewGuid(), Email = email };\n        var organization = new Organization { Id = orgId, Name = \"Org\" };\n        var orgUser = new OrganizationUser\n        {\n            OrganizationId = orgId,\n            UserId = existingUser.Id,\n            Status = OrganizationUserStatusType.Accepted,\n            Type = OrganizationUserType.User\n        };\n\n        // Arrange repository expectations for the flow\n        sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(email).Returns(existingUser);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(organization);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(existingUser.Id)\n            .Returns(new List<OrganizationUser> { orgUser });\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationEmailAsync(orgId, email).Returns(orgUser);\n\n        // No existing SSO link so first SSO login event is logged\n        sutProvider.GetDependency<ISsoUserRepository>().GetByUserIdOrganizationIdAsync(orgId, existingUser.Id).Returns((SsoUser?)null);\n\n        var claims = new[]\n        {\n            new Claim(JwtClaimTypes.Email, email),\n            new Claim(JwtClaimTypes.Name, \"Jit User\")\n        } as IEnumerable<Claim>;\n        var config = new SsoConfigurationData();\n\n        var method = typeof(AccountController).GetMethod(\n            \"CreateUserAndOrgUserConditionallyAsync\",\n            BindingFlags.Instance | BindingFlags.NonPublic);\n        Assert.NotNull(method);\n\n        // Act\n        var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method.Invoke(sutProvider.Sut, new object[]\n        {\n            orgId.ToString(),\n            providerUserId,\n            claims,\n            null!,\n            config\n        })!;\n\n        var returned = await task;\n\n        // Assert\n        Assert.Equal(existingUser.Id, returned.user.Id);\n\n        await sutProvider.GetDependency<ISsoUserRepository>().Received().CreateAsync(Arg.Is<SsoUser>(s =>\n            s.OrganizationId == orgId && s.UserId == existingUser.Id && s.ExternalId == providerUserId));\n\n        await sutProvider.GetDependency<Core.Services.IEventService>().Received().LogOrganizationUserEventAsync(\n            orgUser,\n            EventType.OrganizationUser_FirstSsoLogin);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateUserAndOrgUserConditionallyAsync_WithExistingInvitedUser_ThrowsAcceptInviteBeforeUsingSSO(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var providerUserId = \"provider-user-id\";\n        var email = \"user@example.com\";\n        var existingUser = new User { Id = Guid.NewGuid(), Email = email, UsesKeyConnector = false };\n        var organization = new Organization { Id = orgId, Name = \"Org\" };\n        var orgUser = new OrganizationUser\n        {\n            OrganizationId = orgId,\n            UserId = existingUser.Id,\n            Status = OrganizationUserStatusType.Invited,\n            Type = OrganizationUserType.User\n        };\n\n        // i18n returns the key so we can assert on message contents\n        sutProvider.GetDependency<II18nService>()\n            .T(Arg.Any<string>(), Arg.Any<object?[]>())\n            .Returns(ci => (string)ci[0]!);\n\n        // Arrange repository expectations for the flow\n        sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(email).Returns(existingUser);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(organization);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(existingUser.Id)\n            .Returns(new List<OrganizationUser> { orgUser });\n\n        var claims = new[]\n        {\n            new Claim(JwtClaimTypes.Email, email),\n            new Claim(JwtClaimTypes.Name, \"Invited User\")\n        } as IEnumerable<Claim>;\n        var config = new SsoConfigurationData();\n\n        var method = typeof(AccountController).GetMethod(\n            \"CreateUserAndOrgUserConditionallyAsync\",\n            BindingFlags.Instance | BindingFlags.NonPublic);\n        Assert.NotNull(method);\n\n        // Act + Assert\n        var task = (Task<(User user, Organization organization, OrganizationUser orgUser)>)method.Invoke(sutProvider.Sut, new object[]\n        {\n            orgId.ToString(),\n            providerUserId,\n            claims,\n            null!,\n            config\n        })!;\n\n        var ex = await Assert.ThrowsAsync<Exception>(async () => await task);\n        Assert.Equal(\"AcceptInviteBeforeUsingSSO\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public void ExternalChallenge_WithMatchingOrgId_Succeeds(\n        SutProvider<AccountController> sutProvider,\n        Organization organization)\n    {\n        // Arrange\n        var orgId = organization.Id;\n        var scheme = orgId.ToString();\n        var returnUrl = \"~/vault\";\n        var state = \"test-state\";\n        var userIdentifier = \"user-123\";\n        var ssoToken = \"valid-sso-token\";\n\n        // Mock the data protector to return a tokenable with matching org ID\n        var dataProtector = sutProvider.GetDependency<IDataProtectorTokenFactory<SsoTokenable>>();\n        var tokenable = new SsoTokenable(organization, 3600);\n        dataProtector.Unprotect(ssoToken).Returns(tokenable);\n\n        // Mock URL helper for IsLocalUrl check\n        var urlHelper = Substitute.For<IUrlHelper>();\n        urlHelper.IsLocalUrl(returnUrl).Returns(true);\n        sutProvider.Sut.Url = urlHelper;\n\n        // Mock interaction service for IsValidReturnUrl check\n        var interactionService = sutProvider.GetDependency<IIdentityServerInteractionService>();\n        interactionService.IsValidReturnUrl(returnUrl).Returns(true);\n\n        // Act\n        var result = sutProvider.Sut.ExternalChallenge(scheme, returnUrl, state, userIdentifier, ssoToken);\n\n        // Assert\n        var challengeResult = Assert.IsType<ChallengeResult>(result);\n        Assert.Contains(scheme, challengeResult.AuthenticationSchemes);\n        Assert.NotNull(challengeResult.Properties);\n        Assert.Equal(scheme, challengeResult.Properties.Items[\"scheme\"]);\n        Assert.Equal(returnUrl, challengeResult.Properties.Items[\"return_url\"]);\n        Assert.Equal(state, challengeResult.Properties.Items[\"state\"]);\n        Assert.Equal(userIdentifier, challengeResult.Properties.Items[\"user_identifier\"]);\n    }\n\n    [Theory, BitAutoData]\n    public void ExternalChallenge_WithMismatchedOrgId_ThrowsSsoOrganizationIdMismatch(\n        SutProvider<AccountController> sutProvider,\n        Organization organization)\n    {\n        // Arrange\n        var correctOrgId = organization.Id;\n        var wrongOrgId = Guid.NewGuid();\n        var scheme = wrongOrgId.ToString(); // Different from tokenable's org ID\n        var returnUrl = \"~/vault\";\n        var state = \"test-state\";\n        var userIdentifier = \"user-123\";\n        var ssoToken = \"valid-sso-token\";\n\n        // Mock the data protector to return a tokenable with different org ID\n        var dataProtector = sutProvider.GetDependency<IDataProtectorTokenFactory<SsoTokenable>>();\n        var tokenable = new SsoTokenable(organization, 3600); // Contains correctOrgId\n        dataProtector.Unprotect(ssoToken).Returns(tokenable);\n\n        // Mock i18n service to return the key\n        sutProvider.GetDependency<II18nService>()\n            .T(Arg.Any<string>())\n            .Returns(ci => (string)ci[0]!);\n\n        // Act & Assert\n        var ex = Assert.Throws<Exception>(() =>\n            sutProvider.Sut.ExternalChallenge(scheme, returnUrl, state, userIdentifier, ssoToken));\n        Assert.Equal(\"SsoOrganizationIdMismatch\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public void ExternalChallenge_WithInvalidSchemeFormat_ThrowsSsoOrganizationIdMismatch(\n        SutProvider<AccountController> sutProvider,\n        Organization organization)\n    {\n        // Arrange\n        var scheme = \"not-a-valid-guid\";\n        var returnUrl = \"~/vault\";\n        var state = \"test-state\";\n        var userIdentifier = \"user-123\";\n        var ssoToken = \"valid-sso-token\";\n\n        // Mock the data protector to return a valid tokenable\n        var dataProtector = sutProvider.GetDependency<IDataProtectorTokenFactory<SsoTokenable>>();\n        var tokenable = new SsoTokenable(organization, 3600);\n        dataProtector.Unprotect(ssoToken).Returns(tokenable);\n\n        // Mock i18n service to return the key\n        sutProvider.GetDependency<II18nService>()\n            .T(Arg.Any<string>())\n            .Returns(ci => (string)ci[0]!);\n\n        // Act & Assert\n        var ex = Assert.Throws<Exception>(() =>\n            sutProvider.Sut.ExternalChallenge(scheme, returnUrl, state, userIdentifier, ssoToken));\n        Assert.Equal(\"SsoOrganizationIdMismatch\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public void ExternalChallenge_WithInvalidSsoToken_ThrowsInvalidSsoToken(\n        SutProvider<AccountController> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var scheme = orgId.ToString();\n        var returnUrl = \"~/vault\";\n        var state = \"test-state\";\n        var userIdentifier = \"user-123\";\n        var ssoToken = \"invalid-corrupted-token\";\n\n        // Mock the data protector to throw when trying to unprotect\n        var dataProtector = sutProvider.GetDependency<IDataProtectorTokenFactory<SsoTokenable>>();\n        dataProtector.Unprotect(ssoToken).Returns(_ => throw new Exception(\"Token validation failed\"));\n\n        // Mock i18n service to return the key\n        sutProvider.GetDependency<II18nService>()\n            .T(Arg.Any<string>())\n            .Returns(ci => (string)ci[0]!);\n\n        // Act & Assert\n        var ex = Assert.Throws<Exception>(() =>\n            sutProvider.Sut.ExternalChallenge(scheme, returnUrl, state, userIdentifier, ssoToken));\n        Assert.Equal(\"InvalidSsoToken\", ex.Message);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/SSO.Test/IdentityServer/DistributedCachePersistedGrantStoreTests.cs",
    "content": "﻿using Bit.Sso.IdentityServer;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Stores;\nusing NSubstitute;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.SSO.Test.IdentityServer;\n\npublic class DistributedCachePersistedGrantStoreTests\n{\n    private readonly IFusionCache _cache;\n    private readonly DistributedCachePersistedGrantStore _sut;\n\n    public DistributedCachePersistedGrantStoreTests()\n    {\n        _cache = Substitute.For<IFusionCache>();\n        _sut = new DistributedCachePersistedGrantStore(_cache);\n    }\n\n    [Fact]\n    public async Task StoreAsync_StoresGrantWithCalculatedTTL()\n    {\n        // Arrange\n        var grant = CreateTestGrant(\"test-key\", expiration: DateTime.UtcNow.AddMinutes(5));\n\n        // Act\n        await _sut.StoreAsync(grant);\n\n        // Assert\n        await _cache.Received(1).SetAsync(\n            \"test-key\",\n            grant,\n            Arg.Is<FusionCacheEntryOptions>(opts =>\n                opts.Duration >= TimeSpan.FromMinutes(4.9) &&\n                opts.Duration <= TimeSpan.FromMinutes(5)));\n    }\n\n    [Fact]\n    public async Task StoreAsync_WithNoExpiration_UsesDefaultFiveMinuteTTL()\n    {\n        // Arrange\n        var grant = CreateTestGrant(\"no-expiry-key\", expiration: null);\n\n        // Act\n        await _sut.StoreAsync(grant);\n\n        // Assert\n        await _cache.Received(1).SetAsync(\n            \"no-expiry-key\",\n            grant,\n            Arg.Is<FusionCacheEntryOptions>(opts => opts.Duration == TimeSpan.FromMinutes(5)));\n    }\n\n    [Fact]\n    public async Task StoreAsync_WithAlreadyExpiredGrant_DoesNotStore()\n    {\n        // Arrange\n        var expiredGrant = CreateTestGrant(\"expired-key\", expiration: DateTime.UtcNow.AddMinutes(-1));\n\n        // Act\n        await _sut.StoreAsync(expiredGrant);\n\n        // Assert\n        await _cache.DidNotReceive().SetAsync(\n            Arg.Any<string>(),\n            Arg.Any<PersistedGrant>(),\n            Arg.Any<FusionCacheEntryOptions?>());\n    }\n\n    [Fact]\n    public async Task StoreAsync_EnablesDistributedCache()\n    {\n        // Arrange\n        var grant = CreateTestGrant(\"distributed-key\", expiration: DateTime.UtcNow.AddMinutes(5));\n\n        // Act\n        await _sut.StoreAsync(grant);\n\n        // Assert\n        await _cache.Received(1).SetAsync(\n            \"distributed-key\",\n            grant,\n            Arg.Is<FusionCacheEntryOptions>(opts =>\n                opts.SkipDistributedCache == false &&\n                opts.SkipDistributedCacheReadWhenStale == false));\n    }\n\n    [Fact]\n    public async Task GetAsync_WithValidGrant_ReturnsGrant()\n    {\n        // Arrange\n        var grant = CreateTestGrant(\"valid-key\", expiration: DateTime.UtcNow.AddMinutes(5));\n        _cache.TryGetAsync<PersistedGrant>(\"valid-key\")\n            .Returns(MaybeValue<PersistedGrant>.FromValue(grant));\n\n        // Act\n        var result = await _sut.GetAsync(\"valid-key\");\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(\"valid-key\", result.Key);\n        Assert.Equal(\"authorization_code\", result.Type);\n        Assert.Equal(\"test-subject\", result.SubjectId);\n        await _cache.DidNotReceive().RemoveAsync(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task GetAsync_WithNonExistentKey_ReturnsNull()\n    {\n        // Arrange\n        _cache.TryGetAsync<PersistedGrant>(\"nonexistent-key\")\n            .Returns(MaybeValue<PersistedGrant>.None);\n\n        // Act\n        var result = await _sut.GetAsync(\"nonexistent-key\");\n\n        // Assert\n        Assert.Null(result);\n        await _cache.DidNotReceive().RemoveAsync(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task GetAsync_WithExpiredGrant_RemovesAndReturnsNull()\n    {\n        // Arrange\n        var expiredGrant = CreateTestGrant(\"expired-key\", expiration: DateTime.UtcNow.AddMinutes(-1));\n        _cache.TryGetAsync<PersistedGrant>(\"expired-key\")\n            .Returns(MaybeValue<PersistedGrant>.FromValue(expiredGrant));\n\n        // Act\n        var result = await _sut.GetAsync(\"expired-key\");\n\n        // Assert\n        Assert.Null(result);\n        await _cache.Received(1).RemoveAsync(\"expired-key\");\n    }\n\n    [Fact]\n    public async Task GetAsync_WithNoExpiration_ReturnsGrant()\n    {\n        // Arrange\n        var grant = CreateTestGrant(\"no-expiry-key\", expiration: null);\n        _cache.TryGetAsync<PersistedGrant>(\"no-expiry-key\")\n            .Returns(MaybeValue<PersistedGrant>.FromValue(grant));\n\n        // Act\n        var result = await _sut.GetAsync(\"no-expiry-key\");\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(\"no-expiry-key\", result.Key);\n        Assert.Null(result.Expiration);\n        await _cache.DidNotReceive().RemoveAsync(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task RemoveAsync_RemovesGrantFromCache()\n    {\n        // Act\n        await _sut.RemoveAsync(\"remove-key\");\n\n        // Assert\n        await _cache.Received(1).RemoveAsync(\"remove-key\");\n    }\n\n    [Fact]\n    public async Task GetAllAsync_ReturnsEmptyCollection()\n    {\n        // Arrange\n        var filter = new PersistedGrantFilter\n        {\n            SubjectId = \"test-subject\",\n            SessionId = \"test-session\",\n            ClientId = \"test-client\",\n            Type = \"authorization_code\"\n        };\n\n        // Act\n        var result = await _sut.GetAllAsync(filter);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task RemoveAllAsync_CompletesWithoutError()\n    {\n        // Arrange\n        var filter = new PersistedGrantFilter\n        {\n            SubjectId = \"test-subject\",\n            ClientId = \"test-client\"\n        };\n\n        // Act & Assert - should not throw\n        await _sut.RemoveAllAsync(filter);\n\n        // Verify no cache operations were performed\n        await _cache.DidNotReceive().RemoveAsync(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task StoreAsync_PreservesAllGrantProperties()\n    {\n        // Arrange\n        var grant = new PersistedGrant\n        {\n            Key = \"full-grant-key\",\n            Type = \"authorization_code\",\n            SubjectId = \"user-123\",\n            SessionId = \"session-456\",\n            ClientId = \"client-789\",\n            Description = \"Test grant\",\n            CreationTime = DateTime.UtcNow.AddMinutes(-1),\n            Expiration = DateTime.UtcNow.AddMinutes(5),\n            ConsumedTime = null,\n            Data = \"{\\\"test\\\":\\\"data\\\"}\"\n        };\n\n        PersistedGrant? capturedGrant = null;\n        await _cache.SetAsync(\n            Arg.Any<string>(),\n            Arg.Do<PersistedGrant>(g => capturedGrant = g),\n            Arg.Any<FusionCacheEntryOptions?>());\n\n        // Act\n        await _sut.StoreAsync(grant);\n\n        // Assert\n        Assert.NotNull(capturedGrant);\n        Assert.Equal(grant.Key, capturedGrant.Key);\n        Assert.Equal(grant.Type, capturedGrant.Type);\n        Assert.Equal(grant.SubjectId, capturedGrant.SubjectId);\n        Assert.Equal(grant.SessionId, capturedGrant.SessionId);\n        Assert.Equal(grant.ClientId, capturedGrant.ClientId);\n        Assert.Equal(grant.Description, capturedGrant.Description);\n        Assert.Equal(grant.CreationTime, capturedGrant.CreationTime);\n        Assert.Equal(grant.Expiration, capturedGrant.Expiration);\n        Assert.Equal(grant.ConsumedTime, capturedGrant.ConsumedTime);\n        Assert.Equal(grant.Data, capturedGrant.Data);\n    }\n\n    private static PersistedGrant CreateTestGrant(string key, DateTime? expiration)\n    {\n        return new PersistedGrant\n        {\n            Key = key,\n            Type = \"authorization_code\",\n            SubjectId = \"test-subject\",\n            ClientId = \"test-client\",\n            CreationTime = DateTime.UtcNow,\n            Expiration = expiration,\n            Data = \"{\\\"test\\\":\\\"data\\\"}\"\n        };\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/SSO.Test/SSO.Test.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <TargetFramework>net8.0</TargetFramework>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n\n        <IsPackable>false</IsPackable>\n        <IsTestProject>true</IsTestProject>\n    </PropertyGroup>\n\n    <ItemGroup>\n      <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n      <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n      <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\n        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n        <PrivateAssets>all</PrivateAssets>\n      </PackageReference>\n      <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\n        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n        <PrivateAssets>all</PrivateAssets>\n      </PackageReference>\n      <PackageReference Include=\"NSubstitute\" Version=\"$(NSubstituteVersion)\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <Using Include=\"Xunit\"/>\n    </ItemGroup>\n\n    <ItemGroup>\n      <ProjectReference Include=\"..\\..\\src\\Sso\\Sso.csproj\" />\n      <ProjectReference Include=\"..\\..\\..\\test\\Common\\Common.csproj\" />\n    </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerPatchTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Scim.IntegrationTest.Factories;\nusing Bit.Scim.Models;\nusing Bit.Scim.Utilities;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Scim.IntegrationTest.Controllers.v2;\n\npublic class GroupsControllerPatchTests : IClassFixture<ScimApplicationFactory>, IAsyncLifetime\n{\n    private readonly ScimApplicationFactory _factory;\n\n    public GroupsControllerPatchTests(ScimApplicationFactory factory)\n    {\n        _factory = factory;\n    }\n\n    public Task InitializeAsync()\n    {\n        var databaseContext = _factory.GetDatabaseContext();\n        _factory.ReinitializeDbForTests(databaseContext);\n\n        return Task.CompletedTask;\n    }\n\n    Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;\n\n    [Fact]\n    public async Task Patch_ReplaceDisplayName_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = ScimApplicationFactory.TestGroupId1;\n        var newDisplayName = \"Patch Display Name\";\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse($\"{{\\\"displayName\\\":\\\"{newDisplayName}\\\"}}\").RootElement\n                }\n            },\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);\n        Assert.Equal(newDisplayName, group.Name);\n\n        Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount, databaseContext.GroupUsers.Count());\n        Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));\n        Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));\n    }\n\n    [Fact]\n    public async Task Patch_ReplaceMembers_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = ScimApplicationFactory.TestGroupId1;\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Path = \"members\",\n                    Value = JsonDocument.Parse($\"[{{\\\"value\\\":\\\"{ScimApplicationFactory.TestOrganizationUserId2}\\\"}}]\").RootElement\n                }\n            },\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Single(databaseContext.GroupUsers);\n\n        Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());\n        var groupUser = databaseContext.GroupUsers.FirstOrDefault();\n        Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId);\n    }\n\n    [Fact]\n    public async Task Patch_AddSingleMember_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = ScimApplicationFactory.TestGroupId1;\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"add\",\n                    Path = $\"members[value eq \\\"{ScimApplicationFactory.TestOrganizationUserId2}\\\"]\",\n                    Value = JsonDocument.Parse(\"{}\").RootElement\n                }\n            },\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());\n        Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));\n        Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));\n        Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));\n    }\n\n    [Fact]\n    public async Task Patch_AddListMembers_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = ScimApplicationFactory.TestGroupId2;\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"add\",\n                    Path = \"members\",\n                    Value = JsonDocument.Parse($\"[{{\\\"value\\\":\\\"{ScimApplicationFactory.TestOrganizationUserId2}\\\"}},{{\\\"value\\\":\\\"{ScimApplicationFactory.TestOrganizationUserId3}\\\"}}]\").RootElement\n                }\n            },\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));\n        Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));\n    }\n\n    [Fact]\n    public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = ScimApplicationFactory.TestGroupId1;\n        var newDisplayName = \"Patch Display Name\";\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"remove\",\n                    Path = $\"members[value eq \\\"{ScimApplicationFactory.TestOrganizationUserId1}\\\"]\",\n                    Value = JsonDocument.Parse(\"{}\").RootElement\n                },\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse($\"{{\\\"displayName\\\":\\\"{newDisplayName}\\\"}}\").RootElement\n                }\n            },\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());\n        Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());\n\n        var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);\n        Assert.Equal(newDisplayName, group.Name);\n    }\n\n    [Fact]\n    public async Task Patch_RemoveListMembers_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = ScimApplicationFactory.TestGroupId1;\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"remove\",\n                    Path = \"members\",\n                    Value = JsonDocument.Parse($\"[{{\\\"value\\\":\\\"{ScimApplicationFactory.TestOrganizationUserId1}\\\"}}, {{\\\"value\\\":\\\"{ScimApplicationFactory.TestOrganizationUserId4}\\\"}}]\").RootElement\n                }\n            },\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Empty(databaseContext.GroupUsers);\n    }\n\n    [Fact]\n    public async Task Patch_NotFound()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = Guid.NewGuid();\n        var inputModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>(),\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n        var expectedResponse = new ScimErrorResponseModel\n        {\n            Status = StatusCodes.Status404NotFound,\n            Detail = \"Group not found.\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaError }\n        };\n\n        var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);\n\n        Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/GroupsControllerTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Scim.IntegrationTest.Factories;\nusing Bit.Scim.Models;\nusing Bit.Scim.Utilities;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Scim.IntegrationTest.Controllers.v2;\n\npublic class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsyncLifetime\n{\n    private readonly ScimApplicationFactory _factory;\n\n    public GroupsControllerTests(ScimApplicationFactory factory)\n    {\n        _factory = factory;\n    }\n\n    public Task InitializeAsync()\n    {\n        var databaseContext = _factory.GetDatabaseContext();\n        _factory.ReinitializeDbForTests(databaseContext);\n        return Task.CompletedTask;\n    }\n\n    Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;\n\n    [Fact]\n    public async Task Get_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = ScimApplicationFactory.TestGroupId1;\n        var expectedResponse = new ScimGroupResponseModel\n        {\n            Id = groupId,\n            DisplayName = \"Test Group 1\",\n            ExternalId = \"A\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsGetAsync(organizationId, groupId);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimGroupResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n\n        Assert.Contains(\"application/scim+json\", context.Response.Headers.ContentType.ToString());\n    }\n\n    [Fact]\n    public async Task Get_NotFound()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = Guid.NewGuid();\n        var expectedResponse = new ScimErrorResponseModel\n        {\n            Status = StatusCodes.Status404NotFound,\n            Detail = \"Group not found.\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaError }\n        };\n\n        var context = await _factory.GroupsGetAsync(organizationId, groupId);\n\n        Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        string filter = null;\n        int? itemsPerPage = 2;\n        int? startIndex = 1;\n        var expectedResponse = new ScimListResponseModel<ScimGroupResponseModel>\n        {\n            ItemsPerPage = itemsPerPage.Value,\n            TotalResults = 3,\n            StartIndex = startIndex.Value,\n            Resources = new List<ScimGroupResponseModel>\n            {\n                new ScimGroupResponseModel\n                {\n                    Id = ScimApplicationFactory.TestGroupId1,\n                    DisplayName = \"Test Group 1\",\n                    ExternalId = \"A\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }\n                },\n                new ScimGroupResponseModel\n                {\n                    Id = ScimApplicationFactory.TestGroupId2,\n                    DisplayName = \"Test Group 2\",\n                    ExternalId = \"B\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n        var context = await _factory.GroupsGetListAsync(organizationId, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimGroupResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_SearchDisplayName_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        string filter = \"displayName eq Test Group 2\";\n        int? itemsPerPage = 10;\n        int? startIndex = 1;\n        var expectedResponse = new ScimListResponseModel<ScimGroupResponseModel>\n        {\n            ItemsPerPage = itemsPerPage.Value,\n            TotalResults = 1,\n            StartIndex = startIndex.Value,\n            Resources = new List<ScimGroupResponseModel>\n            {\n                new ScimGroupResponseModel\n                {\n                    Id = ScimApplicationFactory.TestGroupId2,\n                    DisplayName = \"Test Group 2\",\n                    ExternalId = \"B\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n        var context = await _factory.GroupsGetListAsync(organizationId, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimGroupResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_SearchExternalId_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        string filter = \"externalId eq C\";\n        int? itemsPerPage = 10;\n        int? startIndex = 1;\n        var expectedResponse = new ScimListResponseModel<ScimGroupResponseModel>\n        {\n            ItemsPerPage = itemsPerPage.Value,\n            TotalResults = 1,\n            StartIndex = startIndex.Value,\n            Resources = new List<ScimGroupResponseModel>\n            {\n                new ScimGroupResponseModel\n                {\n                    Id = ScimApplicationFactory.TestGroupId3,\n                    DisplayName = \"Test Group 3\",\n                    ExternalId = \"C\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n\n        var context = await _factory.GroupsGetListAsync(organizationId, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimGroupResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_EmptyResult_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        string filter = \"externalId eq Z\";\n        int? itemsPerPage = 10;\n        int? startIndex = 1;\n        var expectedResponse = new ScimListResponseModel<ScimGroupResponseModel>\n        {\n            ItemsPerPage = itemsPerPage.Value,\n            TotalResults = 0,\n            StartIndex = startIndex.Value,\n            Resources = new List<ScimGroupResponseModel>(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n\n        var context = await _factory.GroupsGetListAsync(organizationId, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimGroupResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_SearchDisplayNameWithoutOptionalParameters_Success()\n    {\n        string filter = \"displayName eq Test Group 2\";\n        int? itemsPerPage = null;\n        int? startIndex = null;\n        var expectedResponse = new ScimListResponseModel<ScimGroupResponseModel>\n        {\n            ItemsPerPage = 50, //default value\n            TotalResults = 1,\n            StartIndex = 1, //default value\n            Resources = new List<ScimGroupResponseModel>\n            {\n                new ScimGroupResponseModel\n                {\n                    Id = ScimApplicationFactory.TestGroupId2,\n                    DisplayName = \"Test Group 2\",\n                    ExternalId = \"B\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n        var context = await _factory.GroupsGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimGroupResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task Post_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var displayName = \"New Group\";\n        var externalId = Guid.NewGuid().ToString();\n        var inputModel = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            ExternalId = externalId.ToString(),\n            Members = new List<ScimGroupRequestModel.GroupMembersModel>\n            {\n                new ScimGroupRequestModel.GroupMembersModel { Display = \"user1@example.com\", Value = ScimApplicationFactory.TestOrganizationUserId1.ToString() }\n            },\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n        var expectedResponse = new ScimGroupResponseModel\n        {\n            DisplayName = displayName,\n            ExternalId = externalId,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPostAsync(organizationId, inputModel);\n\n        Assert.Equal(StatusCodes.Status201Created, context.Response.StatusCode);\n\n        // Verifying that the response includes a header with the URL of the created Group\n        Assert.Contains(context.Response.Headers, h => h.Key == \"Location\");\n\n        var responseModel = JsonSerializer.Deserialize<ScimGroupResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel, \"Id\");\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Equal(ScimApplicationFactory.InitialGroupCount + 1, databaseContext.Groups.Count());\n        Assert.True(databaseContext.Groups.Any(g => g.Name == displayName && g.ExternalId == externalId));\n\n        Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());\n        Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == responseModel.Id && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\" \")]\n    public async Task Post_InvalidDisplayName_BadRequest(string? displayName)\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var model = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            ExternalId = null,\n            Members = null,\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPostAsync(organizationId, model);\n\n        Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Post_ExistingExternalId_Conflict()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var model = new ScimGroupRequestModel\n        {\n            DisplayName = \"New Group\",\n            ExternalId = \"A\",\n            Members = null,\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPostAsync(organizationId, model);\n\n        Assert.Equal(StatusCodes.Status409Conflict, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());\n        Assert.False(databaseContext.Groups.Any(g => g.Name == \"New Group\"));\n    }\n\n    [Fact]\n    public async Task Put_ChangeNameAndMembers_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = ScimApplicationFactory.TestGroupId1;\n        var newGroupName = Guid.NewGuid().ToString();\n        var inputModel = new ScimGroupRequestModel\n        {\n            DisplayName = newGroupName,\n            ExternalId = \"A\",\n            Members = new List<ScimGroupRequestModel.GroupMembersModel>\n            {\n                new ScimGroupRequestModel.GroupMembersModel { Display = \"user2@example.com\", Value = ScimApplicationFactory.TestOrganizationUserId2.ToString() },\n                new ScimGroupRequestModel.GroupMembersModel { Display = \"user3@example.com\", Value = ScimApplicationFactory.TestOrganizationUserId3.ToString() }\n            },\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n        var expectedResponse = new ScimGroupResponseModel\n        {\n            Id = groupId,\n            DisplayName = newGroupName,\n            ExternalId = \"A\",\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n\n        var context = await _factory.GroupsPutAsync(organizationId, groupId, inputModel);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimGroupResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        var firstGroup = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);\n        Assert.Equal(newGroupName, firstGroup.Name);\n\n        Assert.Equal(2, databaseContext.GroupUsers.Count(gu => gu.GroupId == groupId));\n        Assert.NotNull(databaseContext.GroupUsers.FirstOrDefault(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));\n        Assert.NotNull(databaseContext.GroupUsers.FirstOrDefault(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));\n    }\n\n    [Fact]\n    public async Task Put_NotFound()\n    {\n        var newGroupName = \"Test Group 1 New Name\";\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = Guid.NewGuid();\n        var inputModel = new ScimGroupRequestModel\n        {\n            DisplayName = newGroupName,\n            ExternalId = \"A\",\n            Members = new List<ScimGroupRequestModel.GroupMembersModel>(),\n            Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }\n        };\n        var expectedResponse = new ScimErrorResponseModel\n        {\n            Status = StatusCodes.Status404NotFound,\n            Detail = \"Group not found.\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaError }\n        };\n\n        var context = await _factory.GroupsPutAsync(organizationId, groupId, inputModel);\n\n        Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task Delete_Success()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = ScimApplicationFactory.TestGroupId3;\n\n        var context = await _factory.GroupsDeleteAsync(organizationId, groupId);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Equal(ScimApplicationFactory.InitialGroupCount - 1, databaseContext.Groups.Count());\n        Assert.True(databaseContext.Groups.FirstOrDefault(g => g.Id == groupId) == null);\n    }\n\n    [Fact]\n    public async Task Delete_NotFound()\n    {\n        var organizationId = ScimApplicationFactory.TestOrganizationId1;\n        var groupId = Guid.NewGuid();\n        var expectedResponse = new ScimErrorResponseModel\n        {\n            Status = StatusCodes.Status404NotFound,\n            Detail = \"Group not found.\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaError }\n        };\n\n        var context = await _factory.GroupsDeleteAsync(organizationId, groupId);\n\n        Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core;\nusing Bit.Core.Enums;\nusing Bit.Core.Services;\nusing Bit.Scim.IntegrationTest.Factories;\nusing Bit.Scim.Models;\nusing Bit.Scim.Utilities;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Scim.IntegrationTest.Controllers.v2;\n\npublic class UsersControllerTests : IClassFixture<ScimApplicationFactory>, IAsyncLifetime\n{\n    private const int _initialUserCount = 4;\n\n    private readonly ScimApplicationFactory _factory;\n\n    public UsersControllerTests(ScimApplicationFactory factory)\n    {\n        _factory = factory;\n    }\n\n    public Task InitializeAsync()\n    {\n        var databaseContext = _factory.GetDatabaseContext();\n        _factory.ReinitializeDbForTests(databaseContext);\n        return Task.CompletedTask;\n    }\n\n    Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;\n\n    [Fact]\n    public async Task Get_Success()\n    {\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId1;\n        var expectedResponse = new ScimUserResponseModel\n        {\n            Id = ScimApplicationFactory.TestOrganizationUserId1,\n            DisplayName = \"Test User 1\",\n            ExternalId = \"UA\",\n            Active = true,\n            Emails = new List<BaseScimUserModel.EmailModel>\n            {\n                new BaseScimUserModel.EmailModel { Primary = true, Type = \"work\", Value = \"user1@example.com\" }\n            },\n            Groups = new List<string>(),\n            Name = new BaseScimUserModel.NameModel(\"Test User 1\"),\n            UserName = \"user1@example.com\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var context = await _factory.UsersGetAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimUserResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n\n        Assert.Contains(\"application/scim+json\", context.Response.Headers.ContentType.ToString());\n    }\n\n    [Fact]\n    public async Task Get_NotFound()\n    {\n        var organizationUserId = Guid.NewGuid();\n        var expectedResponse = new ScimErrorResponseModel\n        {\n            Status = StatusCodes.Status404NotFound,\n            Detail = \"User not found.\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaError }\n        };\n\n        var context = await _factory.UsersGetAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId);\n\n        Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_Success()\n    {\n        string filter = null;\n        int? itemsPerPage = 2;\n        int? startIndex = 1;\n        var expectedResponse = new ScimListResponseModel<ScimUserResponseModel>\n        {\n            ItemsPerPage = itemsPerPage.Value,\n            // Note: total matching results is larger than resources actually returned due to pagination settings. See https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2\n            TotalResults = 4,\n            StartIndex = startIndex.Value,\n            Resources = new List<ScimUserResponseModel>\n            {\n                new ScimUserResponseModel\n                {\n                    Id = ScimApplicationFactory.TestOrganizationUserId1,\n                    DisplayName = \"Test User 1\",\n                    ExternalId = \"UA\",\n                    Active = true,\n                    Emails = new List<BaseScimUserModel.EmailModel>\n                    {\n                        new BaseScimUserModel.EmailModel { Primary = true, Type = \"work\", Value = \"user1@example.com\" }\n                    },\n                    Groups = new List<string>(),\n                    Name = new BaseScimUserModel.NameModel(\"Test User 1\"),\n                    UserName = \"user1@example.com\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n                },\n                new ScimUserResponseModel\n                {\n                    Id = ScimApplicationFactory.TestOrganizationUserId2,\n                    DisplayName = \"Test User 2\",\n                    ExternalId = \"UB\",\n                    Active = true,\n                    Emails = new List<BaseScimUserModel.EmailModel>\n                    {\n                        new BaseScimUserModel.EmailModel { Primary = true, Type = \"work\", Value = \"user2@example.com\" }\n                    },\n                    Groups = new List<string>(),\n                    Name = new BaseScimUserModel.NameModel(\"Test User 2\"),\n                    UserName = \"user2@example.com\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n        var context = await _factory.UsersGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimUserResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_SearchUserName_Success()\n    {\n        string filter = \"userName eq user2@example.com\";\n        int? itemsPerPage = 10;\n        int? startIndex = 1;\n        var expectedResponse = new ScimListResponseModel<ScimUserResponseModel>\n        {\n            ItemsPerPage = itemsPerPage.Value,\n            TotalResults = 1,\n            StartIndex = startIndex.Value,\n            Resources = new List<ScimUserResponseModel>\n            {\n                new ScimUserResponseModel\n                {\n                    Id = ScimApplicationFactory.TestOrganizationUserId2,\n                    DisplayName = \"Test User 2\",\n                    ExternalId = \"UB\",\n                    Active = true,\n                    Emails = new List<BaseScimUserModel.EmailModel>\n                    {\n                        new BaseScimUserModel.EmailModel { Primary = true, Type = \"work\", Value = \"user2@example.com\" }\n                    },\n                    Groups = new List<string>(),\n                    Name = new BaseScimUserModel.NameModel(\"Test User 2\"),\n                    UserName = \"user2@example.com\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n        var context = await _factory.UsersGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimUserResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_SearchExternalId_Success()\n    {\n        string filter = \"externalId eq UC\";\n        int? itemsPerPage = 10;\n        int? startIndex = 1;\n        var expectedResponse = new ScimListResponseModel<ScimUserResponseModel>\n        {\n            ItemsPerPage = itemsPerPage.Value,\n            TotalResults = 1,\n            StartIndex = startIndex.Value,\n            Resources = new List<ScimUserResponseModel>\n            {\n                new ScimUserResponseModel\n                {\n                    Id = ScimApplicationFactory.TestOrganizationUserId3,\n                    DisplayName = \"Test User 3\",\n                    ExternalId = \"UC\",\n                    Active = false,\n                    Emails = new List<BaseScimUserModel.EmailModel>\n                    {\n                        new BaseScimUserModel.EmailModel { Primary = true, Type = \"work\", Value = \"user3@example.com\" }\n                    },\n                    Groups = new List<string>(),\n                    Name = new BaseScimUserModel.NameModel(\"Test User 3\"),\n                    UserName = \"user3@example.com\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n        var context = await _factory.UsersGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimUserResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_EmptyResult_Success()\n    {\n        string filter = \"externalId eq UZ\";\n        int? itemsPerPage = 10;\n        int? startIndex = 1;\n        var expectedResponse = new ScimListResponseModel<ScimUserResponseModel>\n        {\n            ItemsPerPage = itemsPerPage.Value,\n            TotalResults = 0,\n            StartIndex = startIndex.Value,\n            Resources = new List<ScimUserResponseModel>(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n        var context = await _factory.UsersGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimUserResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task GetList_SearchUserNameWithoutOptionalParameters_Success()\n    {\n        string filter = \"userName eq user2@example.com\";\n        int? itemsPerPage = null;\n        int? startIndex = null;\n        var expectedResponse = new ScimListResponseModel<ScimUserResponseModel>\n        {\n            ItemsPerPage = 50, //default value\n            TotalResults = 1,\n            StartIndex = 1, //default value\n            Resources = new List<ScimUserResponseModel>\n            {\n                new ScimUserResponseModel\n                {\n                    Id = ScimApplicationFactory.TestOrganizationUserId2,\n                    DisplayName = \"Test User 2\",\n                    ExternalId = \"UB\",\n                    Active = true,\n                    Emails = new List<BaseScimUserModel.EmailModel>\n                    {\n                        new BaseScimUserModel.EmailModel { Primary = true, Type = \"work\", Value = \"user2@example.com\" }\n                    },\n                    Groups = new List<string>(),\n                    Name = new BaseScimUserModel.NameModel(\"Test User 2\"),\n                    UserName = \"user2@example.com\",\n                    Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }\n        };\n\n        var context = await _factory.UsersGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);\n\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimUserResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task Post_Success(bool isScimInviteUserOptimizationEnabled)\n    {\n        var localFactory = new ScimApplicationFactory();\n        localFactory.SubstituteService((IFeatureService featureService)\n            => featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization)\n                .Returns(isScimInviteUserOptimizationEnabled));\n\n        localFactory.ReinitializeDbForTests(localFactory.GetDatabaseContext());\n\n        var email = \"user5@example.com\";\n        var displayName = \"Test User 5\";\n        var externalId = \"UE\";\n        var inputModel = new ScimUserRequestModel\n        {\n            Name = new BaseScimUserModel.NameModel(displayName),\n            DisplayName = displayName,\n            Emails = new List<BaseScimUserModel.EmailModel> { new BaseScimUserModel.EmailModel(email) },\n            ExternalId = externalId,\n            Active = true,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n        var expectedResponse = new ScimUserResponseModel\n        {\n            // DisplayName is not being saved\n            ExternalId = externalId,\n            Active = true,\n            Emails = new List<BaseScimUserModel.EmailModel>\n            {\n                new BaseScimUserModel.EmailModel { Primary = true, Type = \"work\", Value = email }\n            },\n            Groups = new List<string>(),\n            Name = new BaseScimUserModel.NameModel(),\n            UserName = email,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var context = await localFactory.UsersPostAsync(ScimApplicationFactory.TestOrganizationId1, inputModel);\n\n        Assert.Equal(StatusCodes.Status201Created, context.Response.StatusCode);\n\n        // Verifying that the response includes a header with the URL of the created Group\n        Assert.Contains(context.Response.Headers, h => h.Key == \"Location\");\n\n        var responseModel = JsonSerializer.Deserialize<ScimUserResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel, \"Id\");\n\n        var databaseContext = localFactory.GetDatabaseContext();\n        Assert.Equal(_initialUserCount + 1, databaseContext.OrganizationUsers.Count());\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\" \")]\n    public async Task Post_InvalidEmail_BadRequest(string? email)\n    {\n        var displayName = \"Test User 5\";\n        var externalId = \"UE\";\n        var inputModel = new ScimUserRequestModel\n        {\n            Name = new BaseScimUserModel.NameModel(displayName),\n            DisplayName = displayName,\n            Emails = new List<BaseScimUserModel.EmailModel> { new BaseScimUserModel.EmailModel(email) },\n            ExternalId = externalId,\n            Active = true,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var context = await _factory.UsersPostAsync(ScimApplicationFactory.TestOrganizationId1, inputModel);\n\n        Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Post_Inactive_BadRequest()\n    {\n        var displayName = \"Test User 5\";\n        var inputModel = new ScimUserRequestModel\n        {\n            DisplayName = displayName,\n            ExternalId = null,\n            Active = false,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var context = await _factory.UsersPostAsync(ScimApplicationFactory.TestOrganizationId1, inputModel);\n\n        Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(\"user1@example.com\", \"UZ\")]\n    [InlineData(\"userZ@example.com\", \"UA\")]\n    public async Task Post_ExistingData_Conflict(string email, string externalId)\n    {\n        var inputModel = new ScimUserRequestModel\n        {\n            DisplayName = \"New User\",\n            Emails = new List<BaseScimUserModel.EmailModel> { new BaseScimUserModel.EmailModel(email) },\n            ExternalId = externalId,\n            Schemas = null,\n            Active = true\n        };\n\n        var context = await _factory.UsersPostAsync(ScimApplicationFactory.TestOrganizationId1, inputModel);\n\n        Assert.Equal(StatusCodes.Status409Conflict, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Equal(_initialUserCount, databaseContext.OrganizationUsers.Count());\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task Put_RevokeUser_Success(bool scimRevokeV2Enabled)\n    {\n        var localFactory = new ScimApplicationFactory();\n        localFactory.SubstituteService((IFeatureService featureService)\n            => featureService.IsEnabled(FeatureFlagKeys.ScimRevokeV2)\n                .Returns(scimRevokeV2Enabled));\n\n        localFactory.ReinitializeDbForTests(localFactory.GetDatabaseContext());\n\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId2;\n        var inputModel = new ScimUserRequestModel\n        {\n            Active = false\n        };\n        var expectedResponse = new ScimUserResponseModel\n        {\n            Id = ScimApplicationFactory.TestOrganizationUserId2,\n            DisplayName = \"Test User 2\",\n            ExternalId = \"UB\",\n            Active = false,\n            Emails = new List<BaseScimUserModel.EmailModel>\n            {\n                new BaseScimUserModel.EmailModel { Primary = true, Type = \"work\", Value = \"user2@example.com\" }\n            },\n            Groups = new List<string>(),\n            Name = new BaseScimUserModel.NameModel(\"Test User 2\"),\n            UserName = \"user2@example.com\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var context = await localFactory.UsersPutAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        var responseModel = JsonSerializer.Deserialize<ScimUserResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n\n        var databaseContext = localFactory.GetDatabaseContext();\n        var revokedUser = databaseContext.OrganizationUsers.FirstOrDefault(g => g.Id == organizationUserId);\n        Assert.Equal(OrganizationUserStatusType.Revoked, revokedUser.Status);\n    }\n\n    [Fact]\n    public async Task Put_RestoreUser_Success()\n    {\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId3;\n        var inputModel = new ScimUserRequestModel\n        {\n            Active = true\n        };\n        var expectedResponse = new ScimUserResponseModel\n        {\n            Id = ScimApplicationFactory.TestOrganizationUserId3,\n            DisplayName = \"Test User 3\",\n            ExternalId = \"UC\",\n            Active = true,\n            Emails = new List<BaseScimUserModel.EmailModel>\n            {\n                new BaseScimUserModel.EmailModel { Primary = true, Type = \"work\", Value = \"user3@example.com\" }\n            },\n            Groups = new List<string>(),\n            Name = new BaseScimUserModel.NameModel(\"Test User 3\"),\n            UserName = \"user3@example.com\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var context = await _factory.UsersPutAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        var responseModel = JsonSerializer.Deserialize<ScimUserResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        var revokedUser = databaseContext.OrganizationUsers.FirstOrDefault(g => g.Id == organizationUserId);\n        Assert.NotEqual(OrganizationUserStatusType.Revoked, revokedUser.Status);\n    }\n\n    [Fact]\n    public async Task Put_NotFound()\n    {\n        var organizationUserId = Guid.NewGuid();\n        var inputModel = new ScimUserRequestModel\n        {\n            DisplayName = \"Test Group 1\",\n            ExternalId = \"AA\",\n            Schemas = new List<string>()\n        };\n        var expectedResponse = new ScimErrorResponseModel\n        {\n            Status = StatusCodes.Status404NotFound,\n            Detail = \"User not found.\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaError }\n        };\n\n        var context = await _factory.UsersPutAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task Patch_ReplaceRevoke_Success()\n    {\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId2;\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel { Op = \"replace\", Value = JsonDocument.Parse(\"{\\\"active\\\":false}\").RootElement  },\n            },\n            Schemas = new List<string>()\n        };\n\n        var context = await _factory.UsersPatchAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n\n        var organizationUser = databaseContext.OrganizationUsers.FirstOrDefault(g => g.Id == organizationUserId);\n        Assert.Equal(OrganizationUserStatusType.Revoked, organizationUser.Status);\n    }\n\n    [Fact]\n    public async Task Patch_ReplaceRestore_Success()\n    {\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId3;\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel { Op = \"replace\", Value = JsonDocument.Parse(\"{\\\"active\\\":true}\").RootElement  },\n            },\n            Schemas = new List<string>()\n        };\n\n        var context = await _factory.UsersPatchAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n\n        var organizationUser = databaseContext.OrganizationUsers.FirstOrDefault(g => g.Id == organizationUserId);\n        Assert.NotEqual(OrganizationUserStatusType.Revoked, organizationUser.Status);\n    }\n\n    [Fact]\n    public async Task Patch_NotFound()\n    {\n        var organizationUserId = Guid.NewGuid();\n        var inputModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>(),\n            Schemas = new List<string>()\n        };\n        var expectedResponse = new ScimErrorResponseModel\n        {\n            Status = StatusCodes.Status404NotFound,\n            Detail = \"User not found.\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaError }\n        };\n\n        var context = await _factory.UsersPatchAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task Patch_ExternalIdFromPath_Success()\n    {\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId1;\n        var newExternalId = \"new-external-id-path\";\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Path = \"externalId\",\n                    Value = JsonDocument.Parse($\"\\\"{newExternalId}\\\"\").RootElement\n                },\n            },\n            Schemas = new List<string>()\n        };\n\n        var context = await _factory.UsersPatchAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        var organizationUser = databaseContext.OrganizationUsers.FirstOrDefault(g => g.Id == organizationUserId);\n        Assert.Equal(newExternalId, organizationUser.ExternalId);\n    }\n\n    [Fact]\n    public async Task Patch_ExternalIdFromValue_Success()\n    {\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId2;\n        var newExternalId = \"new-external-id-value\";\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse($\"{{\\\"externalId\\\":\\\"{newExternalId}\\\"}}\").RootElement\n                },\n            },\n            Schemas = new List<string>()\n        };\n\n        var context = await _factory.UsersPatchAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        var organizationUser = databaseContext.OrganizationUsers.FirstOrDefault(g => g.Id == organizationUserId);\n        Assert.Equal(newExternalId, organizationUser.ExternalId);\n    }\n\n    [Fact]\n    public async Task Patch_ExternalIdDuplicate_ThrowsConflict()\n    {\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId1;\n        var duplicateExternalId = \"UB\"; // This is the externalId of TestOrganizationUserId2\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Path = \"externalId\",\n                    Value = JsonDocument.Parse($\"\\\"{duplicateExternalId}\\\"\").RootElement\n                },\n            },\n            Schemas = new List<string>()\n        };\n        var expectedResponse = new ScimErrorResponseModel\n        {\n            Status = StatusCodes.Status409Conflict,\n            Detail = \"ExternalId already exists for another user.\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaError }\n        };\n\n        var context = await _factory.UsersPatchAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        Assert.Equal(StatusCodes.Status409Conflict, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n    }\n\n    [Fact]\n    public async Task Patch_UnsupportedOperation_LogsWarningAndSucceeds()\n    {\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId1;\n        var inputModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>()\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"add\",\n                    Path = \"displayName\",\n                    Value = JsonDocument.Parse(\"\\\"John Doe\\\"\").RootElement\n                },\n            },\n            Schemas = new List<string>()\n        };\n\n        var context = await _factory.UsersPatchAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        // Unsupported operations are logged as warnings but don't fail the request\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Delete_Success()\n    {\n        var organizationUserId = ScimApplicationFactory.TestOrganizationUserId1;\n        var inputModel = new ScimUserRequestModel();\n\n        var context = await _factory.UsersDeleteAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Equal(_initialUserCount - 1, databaseContext.OrganizationUsers.Count());\n        Assert.False(databaseContext.OrganizationUsers.Any(g => g.Id == organizationUserId));\n    }\n\n    [Fact]\n    public async Task Delete_NotFound()\n    {\n        var organizationUserId = Guid.NewGuid();\n        var inputModel = new ScimUserRequestModel();\n        var expectedResponse = new ScimErrorResponseModel\n        {\n            Status = StatusCodes.Status404NotFound,\n            Detail = \"User not found.\",\n            Schemas = new List<string> { ScimConstants.Scim2SchemaError }\n        };\n\n        var context = await _factory.UsersDeleteAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);\n\n        Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);\n\n        var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });\n        AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);\n\n        var databaseContext = _factory.GetDatabaseContext();\n        Assert.Equal(_initialUserCount, databaseContext.OrganizationUsers.Count());\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.IntegrationTest/Factories/ScimApplicationFactory.cs",
    "content": "﻿using System.Net.Mime;\nusing System.Security.Claims;\nusing System.Text;\nusing System.Text.Encodings.Web;\nusing System.Text.Json;\nusing Bit.Core.Services;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.IntegrationTestCommon.Factories;\nusing Bit.Scim.Models;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.Extensions.Options;\nusing Microsoft.Net.Http.Headers;\n\nnamespace Bit.Scim.IntegrationTest.Factories;\n\npublic class ScimApplicationFactory : WebApplicationFactoryBase<Startup>\n{\n    public const int InitialGroupCount = 3;\n    public const int InitialGroupUsersCount = 2;\n\n    public static readonly Guid TestUserId1 = Guid.Parse(\"2e8173db-8e8d-4de1-ac38-91b15c6d8dcb\");\n    public static readonly Guid TestUserId2 = Guid.Parse(\"b57846fc-0e94-4c93-9de5-9d0389eeadfb\");\n    public static readonly Guid TestUserId3 = Guid.Parse(\"20713eb8-d0c5-4655-b855-1a0f3472ccb5\");\n    public static readonly Guid TestUserId4 = Guid.Parse(\"cee613af-d0cb-4db9-ab9d-579bb120fd2a\");\n    public static readonly Guid TestGroupId1 = Guid.Parse(\"dcb232e8-761d-4152-a510-be2778d037cb\");\n    public static readonly Guid TestGroupId2 = Guid.Parse(\"562e5371-7020-40b6-b092-099ac66dbdf9\");\n    public static readonly Guid TestGroupId3 = Guid.Parse(\"362c2782-0f1f-4c86-95dd-edbdf7d6040b\");\n    public static readonly Guid TestOrganizationId1 = Guid.Parse(\"fb98e04f-0303-4914-9b37-a983943bf1ca\");\n    public static readonly Guid TestOrganizationUserId1 = Guid.Parse(\"5d421196-8c59-485b-8926-2d6d0101e05f\");\n    public static readonly Guid TestOrganizationUserId2 = Guid.Parse(\"3a63d520-0d84-4679-b887-13fe2058d53b\");\n    public static readonly Guid TestOrganizationUserId3 = Guid.Parse(\"be2f9045-e2b6-4173-ad44-4c69c3ea8140\");\n    public static readonly Guid TestOrganizationUserId4 = Guid.Parse(\"1f5689b7-e96e-4840-b0b1-eb3d5b5fd514\");\n\n    protected override void ConfigureWebHost(IWebHostBuilder builder)\n    {\n        base.ConfigureWebHost(builder);\n\n        builder.ConfigureServices(services =>\n        {\n            services\n                .AddAuthentication(\"Test\")\n                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(\"Test\", options => { });\n\n            // Override to bypass SCIM authorization\n            services.AddAuthorization(config =>\n            {\n                config.AddPolicy(\"Scim\", policy =>\n                {\n                    policy.RequireAssertion(a => true);\n                });\n            });\n\n            var mailService = services.First(sd => sd.ServiceType == typeof(IMailService));\n            services.Remove(mailService);\n            services.AddSingleton<IMailService, NoopMailService>();\n        });\n    }\n\n    public async Task<HttpContext> GroupsGetAsync(Guid organizationId, Guid id)\n    {\n        return await Server.GetAsync($\"/v2/{organizationId}/groups/{id}\");\n    }\n\n    public async Task<HttpContext> GroupsGetListAsync(Guid organizationId, string filter, int? count, int? startIndex)\n    {\n        var queryString = new QueryString(\"?\");\n\n        if (!string.IsNullOrWhiteSpace(filter))\n        {\n            queryString = queryString.Add(\"filter\", filter);\n        }\n\n        if (count.HasValue)\n        {\n            queryString = queryString.Add(\"count\", count.ToString());\n        }\n\n        if (startIndex.HasValue)\n        {\n            queryString = queryString.Add(\"startIndex\", startIndex.ToString());\n        }\n\n        return await Server.GetAsync($\"/v2/{organizationId}/groups\", httpContext => httpContext.Request.QueryString = queryString);\n    }\n\n    public async Task<HttpContext> GroupsPostAsync(Guid organizationId, ScimGroupRequestModel model)\n    {\n        return await Server.PostAsync($\"/v2/{organizationId}/groups\", GetStringContent(model), httpContext => httpContext.Request.Headers.Append(HeaderNames.UserAgent, \"Okta\"));\n    }\n\n    public async Task<HttpContext> GroupsPutAsync(Guid organizationId, Guid id, ScimGroupRequestModel model)\n    {\n        return await Server.PutAsync($\"/v2/{organizationId}/groups/{id}\", GetStringContent(model), httpContext => httpContext.Request.Headers.Append(HeaderNames.UserAgent, \"Okta\"));\n    }\n\n    public async Task<HttpContext> GroupsPatchAsync(Guid organizationId, Guid id, ScimPatchModel model)\n    {\n        return await Server.PatchAsync($\"/v2/{organizationId}/groups/{id}\", GetStringContent(model));\n    }\n\n    public async Task<HttpContext> GroupsDeleteAsync(Guid organizationId, Guid id)\n    {\n        return await Server.DeleteAsync($\"/v2/{organizationId}/groups/{id}\", null);\n    }\n\n    public async Task<HttpContext> UsersGetAsync(Guid organizationId, Guid id)\n    {\n        return await Server.GetAsync($\"/v2/{organizationId}/users/{id}\");\n    }\n\n    public async Task<HttpContext> UsersGetListAsync(Guid organizationId, string filter, int? count, int? startIndex)\n    {\n        var queryString = new QueryString(\"?\");\n\n        if (!string.IsNullOrWhiteSpace(filter))\n        {\n            queryString = queryString.Add(\"filter\", filter);\n        }\n\n        if (count.HasValue)\n        {\n            queryString = queryString.Add(\"count\", count.ToString());\n        }\n\n        if (startIndex.HasValue)\n        {\n            queryString = queryString.Add(\"startIndex\", startIndex.ToString());\n        }\n\n        return await Server.GetAsync($\"/v2/{organizationId}/users\", httpContext => httpContext.Request.QueryString = queryString);\n    }\n\n    public async Task<HttpContext> UsersPostAsync(Guid organizationId, ScimUserRequestModel model)\n    {\n        return await Server.PostAsync($\"/v2/{organizationId}/users\", GetStringContent(model));\n    }\n\n    public async Task<HttpContext> UsersPutAsync(Guid organizationId, Guid id, ScimUserRequestModel model)\n    {\n        return await Server.PutAsync($\"/v2/{organizationId}/users/{id}\", GetStringContent(model));\n    }\n\n    public async Task<HttpContext> UsersPatchAsync(Guid organizationId, Guid id, ScimPatchModel model)\n    {\n        return await Server.PatchAsync($\"/v2/{organizationId}/users/{id}\", GetStringContent(model));\n    }\n\n    public async Task<HttpContext> UsersDeleteAsync(Guid organizationId, Guid id, ScimUserRequestModel model)\n    {\n        return await Server.DeleteAsync($\"/v2/{organizationId}/users/{id}\", GetStringContent(model));\n    }\n\n    public void InitializeDbForTests(DatabaseContext databaseContext)\n    {\n        databaseContext.Organizations.AddRange(GetSeedingOrganizations());\n        databaseContext.Groups.AddRange(GetSeedingGroups());\n        databaseContext.Users.AddRange(GetSeedingUsers());\n        databaseContext.OrganizationUsers.AddRange(GetSeedingOrganizationUsers());\n        databaseContext.GroupUsers.AddRange(GetSeedingGroupUsers());\n        databaseContext.SaveChanges();\n    }\n\n    public void ReinitializeDbForTests(DatabaseContext databaseContext)\n    {\n        databaseContext.Organizations.RemoveRange(databaseContext.Organizations);\n        databaseContext.Groups.RemoveRange(databaseContext.Groups);\n        databaseContext.Users.RemoveRange(databaseContext.Users);\n        databaseContext.OrganizationUsers.RemoveRange(databaseContext.OrganizationUsers);\n        databaseContext.GroupUsers.RemoveRange(databaseContext.GroupUsers);\n        databaseContext.SaveChanges();\n        InitializeDbForTests(databaseContext);\n    }\n\n    private List<Infrastructure.EntityFramework.Models.User> GetSeedingUsers()\n    {\n        return new List<Infrastructure.EntityFramework.Models.User>()\n        {\n            new Infrastructure.EntityFramework.Models.User { Id = TestUserId1, Name = \"Test User 1\", ApiKey = \"\", Email = \"user1@example.com\", SecurityStamp = \"\" },\n            new Infrastructure.EntityFramework.Models.User { Id = TestUserId2, Name = \"Test User 2\", ApiKey = \"\", Email = \"user2@example.com\", SecurityStamp = \"\" },\n            new Infrastructure.EntityFramework.Models.User { Id = TestUserId3, Name = \"Test User 3\", ApiKey = \"\", Email = \"user3@example.com\", SecurityStamp = \"\" },\n            new Infrastructure.EntityFramework.Models.User { Id = TestUserId4, Name = \"Test User 4\", ApiKey = \"\", Email = \"user4@example.com\", SecurityStamp = \"\" },\n        };\n    }\n\n    private List<Infrastructure.EntityFramework.Models.Group> GetSeedingGroups()\n    {\n        return new List<Infrastructure.EntityFramework.Models.Group>()\n        {\n            new Infrastructure.EntityFramework.Models.Group { Id = TestGroupId1, OrganizationId = TestOrganizationId1, Name = \"Test Group 1\", ExternalId = \"A\" },\n            new Infrastructure.EntityFramework.Models.Group { Id = TestGroupId2, OrganizationId = TestOrganizationId1, Name = \"Test Group 2\", ExternalId = \"B\" },\n            new Infrastructure.EntityFramework.Models.Group { Id = TestGroupId3, OrganizationId = TestOrganizationId1, Name = \"Test Group 3\", ExternalId = \"C\" }\n        };\n    }\n\n    private List<Organization> GetSeedingOrganizations()\n    {\n        return new List<Organization>()\n        {\n            new Organization\n            {\n                Id = TestOrganizationId1,\n                Name = \"Test Organization 1\",\n                BillingEmail = $\"billing-email+{TestOrganizationId1}@example.com\",\n                UseGroups = true,\n                Plan = \"Enterprise\",\n            },\n        };\n    }\n\n    private List<Infrastructure.EntityFramework.Models.OrganizationUser> GetSeedingOrganizationUsers()\n    {\n        return new List<Infrastructure.EntityFramework.Models.OrganizationUser>()\n        {\n            new Infrastructure.EntityFramework.Models.OrganizationUser { Id = TestOrganizationUserId1, OrganizationId = TestOrganizationId1, UserId = TestUserId1, Status = Core.Enums.OrganizationUserStatusType.Confirmed, ExternalId = \"UA\", Email = \"user1@example.com\" },\n            new Infrastructure.EntityFramework.Models.OrganizationUser { Id = TestOrganizationUserId2, OrganizationId = TestOrganizationId1, UserId = TestUserId2, Status = Core.Enums.OrganizationUserStatusType.Confirmed, ExternalId = \"UB\", Email = \"user2@example.com\" },\n            new Infrastructure.EntityFramework.Models.OrganizationUser { Id = TestOrganizationUserId3, OrganizationId = TestOrganizationId1, UserId = TestUserId3, Status = Core.Enums.OrganizationUserStatusType.Revoked, ExternalId = \"UC\", Email = \"user3@example.com\" },\n            new Infrastructure.EntityFramework.Models.OrganizationUser { Id = TestOrganizationUserId4, OrganizationId = TestOrganizationId1, UserId = TestUserId4, Status = Core.Enums.OrganizationUserStatusType.Confirmed, ExternalId = \"UD\", Email = \"user4@example.com\" },\n        };\n    }\n\n    private List<Infrastructure.EntityFramework.Models.GroupUser> GetSeedingGroupUsers()\n    {\n        return new List<Infrastructure.EntityFramework.Models.GroupUser>()\n        {\n            new Infrastructure.EntityFramework.Models.GroupUser { GroupId = TestGroupId1, OrganizationUserId = TestOrganizationUserId1 },\n            new Infrastructure.EntityFramework.Models.GroupUser { GroupId = TestGroupId1, OrganizationUserId = TestOrganizationUserId4 }\n        };\n    }\n\n    private static StringContent GetStringContent(object obj) => new(JsonSerializer.Serialize(obj), Encoding.Default, MediaTypeNames.Application.Json);\n\n    public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>\n    {\n        public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,\n            ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)\n            : base(options, logger, encoder, clock)\n        {\n        }\n\n        protected override Task<AuthenticateResult> HandleAuthenticateAsync()\n        {\n            var claims = new[]\n            {\n                new Claim(ClaimTypes.Name, \"Test user\"),\n                new Claim(\"orgadmin\", TestOrganizationId1.ToString())\n            };\n            var identity = new ClaimsIdentity(claims, \"Test\");\n            var principal = new ClaimsPrincipal(identity);\n            var ticket = new AuthenticationTicket(principal, \"Test\");\n\n            var result = AuthenticateResult.Success(ticket);\n\n            return Task.FromResult(result);\n        }\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.IntegrationTest/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true\n  },\n  \"profiles\": {\n    \"Bit.Scim.IntegrationTest\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:14696\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <IsPackable>false</IsPackable>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.Testing\" Version=\"8.0.10\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"NSubstitute\" Version=\"$(NSubstituteVersion)\" />\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"AutoFixture.Xunit2\" Version=\"$(AutoFixtureXUnit2Version)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Scim\\Scim.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\test\\Common\\Common.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\test\\IntegrationTestCommon\\IntegrationTestCommon.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Content Update=\"Properties\\launchSettings.json\">\n      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <CopyToPublishDirectory>Never</CopyToPublishDirectory>\n    </Content>\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "bitwarden_license/test/Scim.IntegrationTest/appsettings.Development.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\",\n      \"internalScim\": \"http://localhost:44559\"\n    },\n    \"mail\": {\n      \"smtp\": {\n        \"host\": \"localhost\",\n        \"port\": 10250\n      }\n    },\n    \"attachment\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\",\n      \"baseUrl\": \"http://localhost:4000/attachments/\"\n    },\n    \"events\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"storage\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"pricingUri\": \"https://billingpricing.qa.bitwarden.pw\"\n  }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.Test/Groups/GetGroupsListQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Scim.Groups;\nusing Bit.Scim.Models;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Scim.Test.Groups;\n\n[SutProviderCustomize]\npublic class GetGroupsListCommandTests\n{\n    [Theory]\n    [BitAutoData(10, 1)]\n    [BitAutoData(2, 1)]\n    [BitAutoData(1, 3)]\n    public async Task GetGroupsList_Success(int count, int startIndex, SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)\n    {\n        groups = SetGroupsOrganizationId(groups, organizationId);\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByOrganizationIdAsync(organizationId)\n            .Returns(groups);\n\n        var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Count = count, StartIndex = startIndex });\n\n        AssertHelper.AssertPropertyEqual(groups.Skip(startIndex - 1).Take(count).ToList(), result.groupList);\n        AssertHelper.AssertPropertyEqual(groups.Count, result.totalResults);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetGroupsList_FilterDisplayName_Success(SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)\n    {\n        groups = SetGroupsOrganizationId(groups, organizationId);\n        string name = groups.First().Name;\n        string filter = $\"displayName eq {name}\";\n\n        var expectedGroupList = groups\n            .Where(g => g.Name == name)\n            .ToList();\n        var expectedTotalResults = expectedGroupList.Count;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByOrganizationIdAsync(organizationId)\n            .Returns(groups);\n\n        var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });\n\n        AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);\n        AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetGroupsList_FilterDisplayName_Empty(string name, SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)\n    {\n        groups = SetGroupsOrganizationId(groups, organizationId);\n        string filter = $\"displayName eq {name}\";\n\n        var expectedGroupList = new List<Group>();\n        var expectedTotalResults = expectedGroupList.Count;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByOrganizationIdAsync(organizationId)\n            .Returns(groups);\n\n        var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });\n\n        AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);\n        AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetGroupsList_FilterExternalId_Success(SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)\n    {\n        groups = SetGroupsOrganizationId(groups, organizationId);\n        string externalId = groups.First().ExternalId;\n        string filter = $\"externalId eq {externalId}\";\n\n        var expectedGroupList = groups\n            .Where(ou => ou.ExternalId == externalId)\n            .ToList();\n        var expectedTotalResults = expectedGroupList.Count;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByOrganizationIdAsync(organizationId)\n            .Returns(groups);\n\n        var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });\n\n        AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);\n        AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetGroupsList_FilterExternalId_Empty(string externalId, SutProvider<GetGroupsListQuery> sutProvider, Guid organizationId, IList<Group> groups)\n    {\n        groups = SetGroupsOrganizationId(groups, organizationId);\n        string filter = $\"externalId eq {externalId}\";\n\n        var expectedGroupList = groups\n            .Where(ou => ou.ExternalId == externalId)\n            .ToList();\n        var expectedTotalResults = expectedGroupList.Count;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByOrganizationIdAsync(organizationId)\n            .Returns(groups);\n\n        var result = await sutProvider.Sut.GetGroupsListAsync(organizationId, new GetGroupsQueryParamModel { Filter = filter });\n\n        AssertHelper.AssertPropertyEqual(expectedGroupList, result.groupList);\n        AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);\n    }\n\n    private IList<Group> SetGroupsOrganizationId(IList<Group> groups, Guid organizationId)\n    {\n        return groups.Select(g =>\n        {\n            g.OrganizationId = organizationId;\n            return g;\n        }).ToList();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandTests.cs",
    "content": "﻿using System.Text.Json;\nusing AutoFixture;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Scim.Groups;\nusing Bit.Scim.Models;\nusing Bit.Scim.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Scim.Test.Groups;\n\n[SutProviderCustomize]\npublic class PatchGroupCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommand> sutProvider,\n        Organization organization, Group group, IEnumerable<Guid> userIds)\n    {\n        group.OrganizationId = organization.Id;\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"replace\",\n                    Path = \"members\",\n                    Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(\n            group.Id,\n            Arg.Is<IEnumerable<Guid>>(arg =>\n                arg.Count() == userIds.Count() &&\n                arg.ToHashSet().SetEquals(userIds)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(\n        SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)\n    {\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"replace\",\n                    Path = \"displayname\",\n                    Value = JsonDocument.Parse($\"\\\"{displayName}\\\"\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);\n        Assert.Equal(displayName, group.Name);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_ReplaceDisplayNameFromPath_MissingOrganization_Throws(\n        SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)\n    {\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns((Organization)null);\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"replace\",\n                    Path = \"displayname\",\n                    Value = JsonDocument.Parse($\"\\\"{displayName}\\\"\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PatchGroupAsync(group, scimPatchModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)\n    {\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse($\"{{\\\"displayName\\\":\\\"{displayName}\\\"}}\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);\n        Assert.Equal(displayName, group.Name);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_ReplaceDisplayNameFromValueObject_MissingOrganization_Throws(\n        SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)\n    {\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns((Organization)null);\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse($\"{{\\\"displayName\\\":\\\"{displayName}\\\"}}\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PatchGroupAsync(group, scimPatchModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)\n    {\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyUserIdsByIdAsync(group.Id, true)\n            .Returns(existingMembers);\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"add\",\n                    Path = $\"members[value eq \\\"{userId}\\\"]\",\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(\n            group.Id,\n            Arg.Is<IEnumerable<Guid>>(arg => arg.Single() == userId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_AddSingleMember_ReturnsEarlyIfAlreadyInGroup(\n        SutProvider<PatchGroupCommand> sutProvider,\n        Organization organization,\n        Group group,\n        ICollection<Guid> existingMembers)\n    {\n        // User being added is already in group\n        var userId = existingMembers.First();\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyUserIdsByIdAsync(group.Id, true)\n            .Returns(existingMembers);\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"add\",\n                    Path = $\"members[value eq \\\"{userId}\\\"]\",\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IGroupRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .AddGroupUsersByIdAsync(default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_AddListMembers_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, ICollection<Guid> userIds)\n    {\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyUserIdsByIdAsync(group.Id, true)\n            .Returns(existingMembers);\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"add\",\n                    Path = $\"members\",\n                    Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(\n            group.Id,\n            Arg.Is<IEnumerable<Guid>>(arg =>\n                arg.Count() == userIds.Count &&\n                arg.ToHashSet().SetEquals(userIds)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_AddListMembers_IgnoresDuplicatesInRequest(\n        SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group,\n        ICollection<Guid> existingMembers)\n    {\n        // Create 3 userIds\n        var fixture = new Fixture { RepeatCount = 3 };\n        var userIds = fixture.CreateMany<Guid>().ToList();\n\n        // Copy the list and add a duplicate\n        var userIdsWithDuplicate = userIds.Append(userIds.First()).ToList();\n        Assert.Equal(4, userIdsWithDuplicate.Count);\n\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyUserIdsByIdAsync(group.Id, true)\n            .Returns(existingMembers);\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"add\",\n                    Path = $\"members\",\n                    Value = JsonDocument.Parse(JsonSerializer\n                        .Serialize(userIdsWithDuplicate\n                            .Select(uid => new { value = uid })\n                            .ToArray())).RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(\n            group.Id,\n            Arg.Is<IEnumerable<Guid>>(arg =>\n                arg.Count() == 3 &&\n                arg.ToHashSet().SetEquals(userIds)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_AddListMembers_SuccessIfOnlySomeUsersAreInGroup(\n        SutProvider<PatchGroupCommand> sutProvider,\n        Organization organization, Group group,\n        ICollection<Guid> existingMembers,\n        ICollection<Guid> userIds)\n    {\n        // A user is already in the group, but some still need to be added\n        userIds.Add(existingMembers.First());\n\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyUserIdsByIdAsync(group.Id, true)\n            .Returns(existingMembers);\n\n        var scimPatchModel = new ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"add\",\n                    Path = $\"members\",\n                    Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IGroupRepository>()\n            .Received(1)\n            .AddGroupUsersByIdAsync(\n                group.Id,\n                Arg.Is<IEnumerable<Guid>>(arg =>\n                    arg.Count() == userIds.Count &&\n                    arg.ToHashSet().SetEquals(userIds)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_RemoveSingleMember_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, Guid userId)\n    {\n        group.OrganizationId = organization.Id;\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"remove\",\n                    Path = $\"members[value eq \\\"{userId}\\\"]\",\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommand> sutProvider,\n        Organization organization, Group group, ICollection<Guid> existingMembers)\n    {\n        List<Guid> usersToRemove = [existingMembers.First(), existingMembers.Skip(1).First()];\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyUserIdsByIdAsync(group.Id)\n            .Returns(existingMembers);\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new()\n                {\n                    Op = \"remove\",\n                    Path = $\"members\",\n                    Value = JsonDocument.Parse(JsonSerializer.Serialize(usersToRemove.Select(uid => new { value = uid }).ToArray())).RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        var expectedRemainingUsers = existingMembers.Skip(2).ToList();\n        await sutProvider.GetDependency<IGroupRepository>()\n            .Received(1)\n            .UpdateUsersAsync(\n                group.Id,\n                Arg.Is<IEnumerable<Guid>>(arg =>\n                    arg.Count() == expectedRemainingUsers.Count &&\n                    arg.ToHashSet().SetEquals(expectedRemainingUsers)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_InvalidOperation_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group)\n    {\n        group.OrganizationId = organization.Id;\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = [new ScimPatchModel.OperationModel { Op = \"invalid operation\" }],\n            Schemas = [ScimConstants.Scim2SchemaUser]\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        // Assert: no operation performed\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);\n        await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);\n        await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);\n\n        // Assert: logging\n        sutProvider.GetDependency<ILogger<PatchGroupCommand>>().ReceivedWithAnyArgs().LogWarning(\"\");\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchGroup_NoOperation_Success(\n        SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group)\n    {\n        group.OrganizationId = organization.Id;\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);\n\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);\n        await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);\n        await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.Test/Groups/PostGroupCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Scim.Groups;\nusing Bit.Scim.Models;\nusing Bit.Scim.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Scim.Test.Groups;\n\n[SutProviderCustomize]\npublic class PostGroupCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task PostGroup_Success(SutProvider<PostGroupCommand> sutProvider, string displayName, string externalId, Organization organization, ICollection<Group> groups)\n    {\n        var scimGroupRequestModel = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            ExternalId = externalId,\n            Members = new List<ScimGroupRequestModel.GroupMembersModel>(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var expectedResult = new Group\n        {\n            OrganizationId = organization.Id,\n            Name = displayName,\n            ExternalId = externalId,\n        };\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByOrganizationIdAsync(organization.Id)\n            .Returns(groups);\n\n        var group = await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel);\n\n        await sutProvider.GetDependency<ICreateGroupCommand>().Received(1).CreateGroupAsync(group, organization, EventSystemUser.SCIM, null);\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);\n\n        AssertHelper.AssertPropertyEqual(expectedResult, group, \"Id\", \"CreationDate\", \"RevisionDate\");\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostGroup_WithMembers_Success(SutProvider<PostGroupCommand> sutProvider, string displayName, string externalId, Organization organization, ICollection<Group> groups, IEnumerable<Guid> membersUserIds)\n    {\n        var scimGroupRequestModel = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            ExternalId = externalId,\n            Members = membersUserIds.Select(uid => new ScimGroupRequestModel.GroupMembersModel { Value = uid.ToString() }).ToList(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var expectedResult = new Group\n        {\n            OrganizationId = organization.Id,\n            Name = displayName,\n            ExternalId = externalId\n        };\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByOrganizationIdAsync(organization.Id)\n            .Returns(groups);\n\n        var group = await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel);\n\n        await sutProvider.GetDependency<ICreateGroupCommand>().Received(1).CreateGroupAsync(group, organization, EventSystemUser.SCIM, null);\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(Arg.Any<Guid>(), Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))));\n\n        AssertHelper.AssertPropertyEqual(expectedResult, group, \"Id\", \"CreationDate\", \"RevisionDate\");\n    }\n\n    [Theory]\n    [BitAutoData((string)null)]\n    [BitAutoData(\"\")]\n    [BitAutoData(\" \")]\n    public async Task PostGroup_NullDisplayName_Throws(string displayName, SutProvider<PostGroupCommand> sutProvider, Organization organization)\n    {\n        var scimGroupRequestModel = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            ExternalId = Guid.NewGuid().ToString(),\n            Members = new List<ScimGroupRequestModel.GroupMembersModel>(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostGroup_ExistingExternalId_Throws(string displayName, SutProvider<PostGroupCommand> sutProvider, Organization organization, ICollection<Group> groups)\n    {\n        var scimGroupRequestModel = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            ExternalId = groups.First().ExternalId,\n            Members = new List<ScimGroupRequestModel.GroupMembersModel>(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByOrganizationIdAsync(organization.Id)\n            .Returns(groups);\n\n        await Assert.ThrowsAsync<ConflictException>(async () => await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.Test/Groups/PutGroupCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Scim.Groups;\nusing Bit.Scim.Models;\nusing Bit.Scim.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Scim.Test.Groups;\n\n[SutProviderCustomize]\npublic class PutGroupCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task PutGroup_Success(SutProvider<PutGroupCommand> sutProvider, Organization organization, Group group, string displayName)\n    {\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetByIdAsync(group.Id)\n            .Returns(group);\n\n        var inputModel = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var expectedResult = new Group\n        {\n            Id = group.Id,\n            ExternalId = group.ExternalId,\n            Name = displayName,\n            OrganizationId = group.OrganizationId\n        };\n\n        var result = await sutProvider.Sut.PutGroupAsync(organization, group.Id, inputModel);\n\n        AssertHelper.AssertPropertyEqual(expectedResult, result, \"CreationDate\", \"RevisionDate\");\n        Assert.Equal(displayName, group.Name);\n\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutGroup_ChangeMembers_Success(SutProvider<PutGroupCommand> sutProvider, Organization organization, Group group, string displayName, IEnumerable<Guid> membersUserIds)\n    {\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetByIdAsync(group.Id)\n            .Returns(group);\n\n        var inputModel = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            Members = membersUserIds.Select(uid => new ScimGroupRequestModel.GroupMembersModel { Value = uid.ToString() }).ToList(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        var expectedResult = new Group\n        {\n            Id = group.Id,\n            ExternalId = group.ExternalId,\n            Name = displayName,\n            OrganizationId = group.OrganizationId\n        };\n\n        var result = await sutProvider.Sut.PutGroupAsync(organization, group.Id, inputModel);\n\n        AssertHelper.AssertPropertyEqual(expectedResult, result, \"CreationDate\", \"RevisionDate\");\n        Assert.Equal(displayName, group.Name);\n\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutGroup_NotFound_Throws(SutProvider<PutGroupCommand> sutProvider, Organization organization, Guid groupId, string displayName)\n    {\n        var scimGroupRequestModel = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PutGroupAsync(organization, groupId, scimGroupRequestModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutGroup_MismatchingOrganizationId_Throws(SutProvider<PutGroupCommand> sutProvider, Organization organization, Guid groupId, string displayName)\n    {\n        var scimGroupRequestModel = new ScimGroupRequestModel\n        {\n            DisplayName = displayName,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetByIdAsync(groupId)\n            .Returns(new Group\n            {\n                Id = groupId,\n                OrganizationId = Guid.NewGuid()\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PutGroupAsync(organization, groupId, scimGroupRequestModel));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.Test/Scim.Test.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n  <PropertyGroup>\r\n    <IsPackable>false</IsPackable>\r\n  </PropertyGroup>\r\n\r\n  <ItemGroup>\r\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\r\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\r\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\r\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n      <PrivateAssets>all</PrivateAssets>\r\n    </PackageReference>\r\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\r\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n      <PrivateAssets>all</PrivateAssets>\r\n    </PackageReference>\r\n    <PackageReference Include=\"NSubstitute\" Version=\"$(NSubstituteVersion)\" />\r\n  </ItemGroup>\r\n\r\n  <ItemGroup>\r\n    <ProjectReference Include=\"..\\..\\src\\Scim\\Scim.csproj\" />\r\n    <ProjectReference Include=\"..\\..\\..\\test\\Common\\Common.csproj\" />\r\n  </ItemGroup>\r\n</Project>"
  },
  {
    "path": "bitwarden_license/test/Scim.Test/Users/GetUsersListQueryTests.cs",
    "content": "﻿using Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Scim.Models;\nusing Bit.Scim.Users;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Scim.Test.Users;\n\n[SutProviderCustomize]\npublic class GetUsersListQueryTests\n{\n    [Theory]\n    [BitAutoData(10, 1)]\n    [BitAutoData(2, 1)]\n    [BitAutoData(1, 3)]\n    public async Task GetUsersList_Success(int count, int startIndex, SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)\n    {\n        organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns(organizationUserUserDetails);\n\n        var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Count = count, StartIndex = startIndex });\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);\n\n        AssertHelper.AssertPropertyEqual(organizationUserUserDetails.Skip(startIndex - 1).Take(count).ToList(), result.userList);\n        AssertHelper.AssertPropertyEqual(organizationUserUserDetails.Count, result.totalResults);\n    }\n\n    [Theory]\n    [BitAutoData(\"user1@example.com\")]\n    public async Task GetUsersList_FilterUserName_Success(string email, SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)\n    {\n        organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);\n        organizationUserUserDetails.First().Email = email;\n        string filter = $\"userName eq {email}\";\n\n        var expectedUserList = organizationUserUserDetails\n            .Where(u => u.Email == email)\n            .ToList();\n        var expectedTotalResults = expectedUserList.Count;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns(organizationUserUserDetails);\n\n        var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);\n\n        AssertHelper.AssertPropertyEqual(expectedUserList, result.userList);\n        AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);\n    }\n\n    [Theory]\n    [BitAutoData(\"user1@example.com\")]\n    public async Task GetUsersList_FilterUserName_Empty(string email, SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)\n    {\n        organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);\n        string filter = $\"userName eq {email}\";\n\n        var expectedUserList = new List<OrganizationUserUserDetails>();\n        var expectedTotalResults = expectedUserList.Count;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns(organizationUserUserDetails);\n\n        var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);\n\n        AssertHelper.AssertPropertyEqual(expectedUserList, result.userList);\n        AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetUsersList_FilterExternalId_Success(SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)\n    {\n        organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);\n        string externalId = organizationUserUserDetails.First().ExternalId;\n        string filter = $\"externalId eq {externalId}\";\n\n        var expectedUserList = organizationUserUserDetails\n            .Where(u => u.ExternalId == externalId)\n            .ToList();\n        var expectedTotalResults = expectedUserList.Count;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns(organizationUserUserDetails);\n\n        var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);\n\n        AssertHelper.AssertPropertyEqual(expectedUserList, result.userList);\n        AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetUsersList_FilterExternalId_Empty(string externalId, SutProvider<GetUsersListQuery> sutProvider, Guid organizationId, IList<OrganizationUserUserDetails> organizationUserUserDetails)\n    {\n        organizationUserUserDetails = SetUsersOrganizationId(organizationUserUserDetails, organizationId);\n        string filter = $\"externalId eq {externalId}\";\n\n        var expectedUserList = organizationUserUserDetails\n            .Where(u => u.ExternalId == externalId)\n            .ToList();\n        var expectedTotalResults = expectedUserList.Count;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns(organizationUserUserDetails);\n\n        var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);\n\n        AssertHelper.AssertPropertyEqual(expectedUserList, result.userList);\n        AssertHelper.AssertPropertyEqual(expectedTotalResults, result.totalResults);\n    }\n\n    private IList<OrganizationUserUserDetails> SetUsersOrganizationId(IList<OrganizationUserUserDetails> organizationUserUserDetails, Guid organizationId)\n    {\n        return organizationUserUserDetails.Select(ouud =>\n        {\n            ouud.OrganizationId = organizationId;\n            return ouud;\n        }).ToList();\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Scim.Models;\nusing Bit.Scim.Users;\nusing Bit.Scim.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Scim.Test.Users;\n\n[SutProviderCustomize]\npublic class PatchUserCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_RestorePath_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        organizationUser.Status = Core.Enums.OrganizationUserStatusType.Revoked;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Path = \"active\",\n                    Value = JsonDocument.Parse(\"true\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_RestoreValue_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        organizationUser.Status = Core.Enums.OrganizationUserStatusType.Revoked;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse(\"{\\\"active\\\":true}\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_RevokePath_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        organizationUser.Status = Core.Enums.OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Path = \"active\",\n                    Value = JsonDocument.Parse(\"false\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_RevokeValue_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        organizationUser.Status = Core.Enums.OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse(\"{\\\"active\\\":false}\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_NoAction_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM);\n        await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_NotFound_Throws(SutProvider<PatchUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)\n    {\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchUserAsync(organizationId, organizationUserId, scimPatchModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_MismatchingOrganizationId_Throws(SutProvider<PatchUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)\n    {\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>(),\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUserId)\n            .Returns(new OrganizationUser\n            {\n                Id = organizationUserId,\n                OrganizationId = Guid.NewGuid()\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchUserAsync(organizationId, organizationUserId, scimPatchModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_ExternalIdFromPath_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        var newExternalId = \"new-external-id-123\";\n        organizationUser.ExternalId = \"old-external-id\";\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationUser.OrganizationId)\n            .Returns(new List<OrganizationUserUserDetails>());\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Path = \"externalId\",\n                    Value = JsonDocument.Parse($\"\\\"{newExternalId}\\\"\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(\n            Arg.Is<OrganizationUser>(ou => ou.ExternalId == newExternalId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_ExternalIdFromValue_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        var newExternalId = \"new-external-id-456\";\n        organizationUser.ExternalId = null;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationUser.OrganizationId)\n            .Returns(new List<OrganizationUserUserDetails>());\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse($\"{{\\\"externalId\\\":\\\"{newExternalId}\\\"}}\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(\n            Arg.Is<OrganizationUser>(ou => ou.ExternalId == newExternalId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_ExternalIdDuplicate_ThrowsConflict(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser, OrganizationUserUserDetails existingUser)\n    {\n        var duplicateExternalId = \"duplicate-id\";\n        organizationUser.ExternalId = \"old-id\";\n        existingUser.ExternalId = duplicateExternalId;\n        existingUser.Id = Guid.NewGuid(); // Different user\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationUser.OrganizationId)\n            .Returns(new List<OrganizationUserUserDetails> { existingUser });\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Path = \"externalId\",\n                    Value = JsonDocument.Parse($\"\\\"{duplicateExternalId}\\\"\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await Assert.ThrowsAsync<ConflictException>(async () =>\n            await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_ExternalIdTooLong_ThrowsBadRequest(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        var tooLongExternalId = new string('a', 301); // Exceeds 300 character limit\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Path = \"externalId\",\n                    Value = JsonDocument.Parse($\"\\\"{tooLongExternalId}\\\"\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_ExternalIdNull_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        organizationUser.ExternalId = \"existing-id\";\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationUser.OrganizationId)\n            .Returns(new List<OrganizationUserUserDetails>());\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Path = \"externalId\",\n                    Value = JsonDocument.Parse(\"null\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(\n            Arg.Is<OrganizationUser>(ou => ou.ExternalId == null));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_UnsupportedOperation_LogsWarningAndSucceeds(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"add\",\n                    Path = \"displayName\",\n                    Value = JsonDocument.Parse(\"\\\"John Doe\\\"\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        // Should not throw - unsupported operations are logged as warnings but don't fail the request\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        // Verify no restore or revoke operations were called\n        await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM);\n        await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_ActiveAndExternalIdFromValue_Success(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        var newExternalId = \"combined-test-id\";\n        organizationUser.Status = OrganizationUserStatusType.Confirmed;\n        organizationUser.ExternalId = \"old-id\";\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationUser.OrganizationId)\n            .Returns(new List<OrganizationUserUserDetails>());\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse($\"{{\\\"active\\\":false,\\\"externalId\\\":\\\"{newExternalId}\\\"}}\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        // Verify both operations were processed\n        await sutProvider.GetDependency<IRevokeOrganizationUserCommand>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(\n            Arg.Is<OrganizationUser>(ou => ou.ExternalId == newExternalId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PatchUser_RestoreAndExternalIdFromValue_DoesNotRevertRestore(SutProvider<PatchUserCommand> sutProvider, OrganizationUser organizationUser)\n    {\n        var newExternalId = \"combined-restore-id\";\n        organizationUser.Status = OrganizationUserStatusType.Revoked;\n        organizationUser.ExternalId = \"old-id\";\n\n        // Simulate the re-fetch after restore returning a user with a non-revoked status\n        var restoredOrgUser = new OrganizationUser\n        {\n            Id = organizationUser.Id,\n            OrganizationId = organizationUser.OrganizationId,\n            Status = OrganizationUserStatusType.Confirmed,\n            ExternalId = organizationUser.ExternalId,\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser, restoredOrgUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationUser.OrganizationId)\n            .Returns(new List<OrganizationUserUserDetails>());\n\n        var scimPatchModel = new Models.ScimPatchModel\n        {\n            Operations = new List<ScimPatchModel.OperationModel>\n            {\n                new ScimPatchModel.OperationModel\n                {\n                    Op = \"replace\",\n                    Value = JsonDocument.Parse($\"{{\\\"active\\\":true,\\\"externalId\\\":\\\"{newExternalId}\\\"}}\").RootElement\n                }\n            },\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);\n\n        await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);\n        // ReplaceAsync must use the re-fetched (restored) user, not the stale revoked state\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(\n            Arg.Is<OrganizationUser>(ou => ou.ExternalId == newExternalId && ou.Status != OrganizationUserStatusType.Revoked));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Scim.Test/Users/PostUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Scim.Models;\nusing Bit.Scim.Users;\nusing Bit.Scim.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Scim.Test.Users;\n\n[SutProviderCustomize]\npublic class PostUserCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task PostUser_Success(SutProvider<PostUserCommand> sutProvider, string externalId, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers, Core.Entities.OrganizationUser newUser, Organization organization)\n    {\n        var scimUserRequestModel = new ScimUserRequestModel\n        {\n            ExternalId = externalId,\n            Emails = emails,\n            Active = true,\n            Schemas = [ScimConstants.Scim2SchemaUser]\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns(organizationUsers);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);\n\n        sutProvider.GetDependency<IStripePaymentService>().HasSecretsManagerStandalone(organization).Returns(true);\n\n        sutProvider.GetDependency<IOrganizationService>()\n            .InviteUserAsync(organizationId,\n                invitingUserId: null,\n                EventSystemUser.SCIM,\n                Arg.Is<OrganizationUserInvite>(i =>\n                    i.Emails.Single().Equals(scimUserRequestModel.PrimaryEmail.ToLowerInvariant()) &&\n                    i.Type == OrganizationUserType.User &&\n                    !i.Collections.Any() &&\n                    !i.Groups.Any() &&\n                    i.AccessSecretsManager),\n                externalId)\n            .Returns(newUser);\n\n        var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);\n\n        await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId,\n            invitingUserId: null, EventSystemUser.SCIM,\n            Arg.Is<OrganizationUserInvite>(i =>\n                i.Emails.Single().Equals(scimUserRequestModel.PrimaryEmail.ToLowerInvariant()) &&\n                i.Type == OrganizationUserType.User &&\n                !i.Collections.Any() &&\n                !i.Groups.Any() &&\n                i.AccessSecretsManager), externalId);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(newUser.Id);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostUser_NullEmail_Throws(SutProvider<PostUserCommand> sutProvider, Guid organizationId)\n    {\n        var scimUserRequestModel = new ScimUserRequestModel\n        {\n            Emails = new List<BaseScimUserModel.EmailModel>(),\n            Active = true,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostUser_Inactive_Throws(SutProvider<PostUserCommand> sutProvider, Guid organizationId, List<BaseScimUserModel.EmailModel> emails)\n    {\n        var scimUserRequestModel = new ScimUserRequestModel\n        {\n            Emails = emails,\n            Active = false,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostUser_DuplicateExternalId_Throws(SutProvider<PostUserCommand> sutProvider, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers)\n    {\n        var scimUserRequestModel = new ScimUserRequestModel\n        {\n            ExternalId = organizationUsers.First().ExternalId,\n            Emails = emails,\n            Active = true,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns(organizationUsers);\n\n        await Assert.ThrowsAsync<ConflictException>(async () => await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostUser_DuplicateUserName_Throws(SutProvider<PostUserCommand> sutProvider, Guid organizationId, List<BaseScimUserModel.EmailModel> emails, ICollection<OrganizationUserUserDetails> organizationUsers)\n    {\n        var scimUserRequestModel = new ScimUserRequestModel\n        {\n            UserName = organizationUsers.First().ExternalId,\n            Emails = emails,\n            Active = true,\n            Schemas = new List<string> { ScimConstants.Scim2SchemaUser }\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns(organizationUsers);\n\n        await Assert.ThrowsAsync<ConflictException>(async () => await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel));\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Sso.IntegrationTest/Controllers/AccountControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Sso.IntegrationTest.Utilities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bitwarden.License.Test.Sso.IntegrationTest.Utilities;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.Mvc.Testing;\nusing NSubstitute;\nusing Xunit;\nusing AuthenticationSchemes = Bit.Core.AuthenticationSchemes;\n\nnamespace Bit.Sso.IntegrationTest.Controllers;\n\npublic class AccountControllerTests(SsoApplicationFactory factory) : IClassFixture<SsoApplicationFactory>\n{\n    private readonly SsoApplicationFactory _factory = factory;\n\n    /*\n    * Test to verify the /Account/ExternalCallback endpoint exists and is reachable.\n    */\n    [Fact]\n    public async Task ExternalCallback_EndpointExists_ReturnsExpectedStatusCode()\n    {\n        // Arrange\n        var client = _factory.CreateClient();\n\n        // Act - Verify the endpoint is accessible (even if it fails due to missing auth)\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - The endpoint should exist and return 500 (not 404) due to missing authentication\n        Assert.NotEqual(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    /*\n    * Test to verify calling /Account/ExternalCallback without an authentication cookie\n    * results in an error as expected.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithNoAuthenticationCookie_ReturnsError()\n    {\n        // Arrange\n        var client = _factory.CreateClient();\n\n        // Act - Call ExternalCallback without proper authentication setup\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because there's no external authentication cookie\n        Assert.False(response.IsSuccessStatusCode);\n        // The endpoint will throw an exception when authentication fails\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify behavior of /Account/ExternalCallback simulating failed authentication.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithMockedAuthenticationService_FailedAuth_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithFailedAuthentication()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert\n        Assert.False(response.IsSuccessStatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when SSO config exists but is disabled.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithDisabledSsoConfig_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig(ssoConfig => ssoConfig!.Enabled = false)\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because SSO config is disabled\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Organization not found or SSO configuration not enabled\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task ExternalCallback_FindUserFromExternalProviderAsync_OrganizationOrSsoConfigNotFound_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because user has invalid status\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Organization not found or SSO configuration not enabled\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when SSO config expects an ACR value\n    * but the authentication response has a missing or invalid ACR claim.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithExpectedAcrValue_AndInvalidAcr_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n        .WithSsoConfig(ssoConfig => ssoConfig!.SetData(\n            new SsoConfigurationData\n            {\n                ExpectedReturnAcrValue = \"urn:expected:acr:value\"\n            }))\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because ACR claim is missing or invalid\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Expected authentication context class reference (acr) was not returned with the authentication response or is invalid\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when the authentication response\n    * does not contain any recognizable user ID claim (sub, NameIdentifier, uid, upn, eppn).\n    */\n    [Fact]\n    public async Task ExternalCallback_WithNoUserIdClaim_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .OmitProviderUserId()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\"); ;\n\n        // Assert - Should fail because no user ID claim was found\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Unknown userid\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when no email claim is found\n    * and the providerUserId cannot be used as a fallback email (doesn't contain @).\n    */\n    [Fact]\n    public async Task ExternalCallback_WithNoEmailClaim_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithNullEmail()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because no email claim was found\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Cannot find email claim\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when an existing user\n    * uses Key Connector but has no org user record (was removed from organization).\n    */\n    [Fact]\n    public async Task ExternalCallback_WithExistingKeyConnectorUser_AndNoOrgUser_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser(user =>\n            {\n                user.UsesKeyConnector = true;\n            })\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because user uses Key Connector but has no org user record\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"You were removed from the organization managing single sign-on for your account\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when an existing user\n    * uses Key Connector and has an org user record in the invited status.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithExistingKeyConnectorUser_AndInvitedOrgUser_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig(ssoConfig => { })\n            .WithUser(user =>\n            {\n                user.UsesKeyConnector = true;\n            })\n            .WithOrganizationUser(orgUser =>\n            {\n                orgUser.Status = OrganizationUserStatusType.Invited;\n            })\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because user uses Key Connector but the Org user is in the invited status\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"You were removed from the organization managing single sign-on for your account\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when an existing user\n    * (not using Key Connector) has no org user record - they were removed from the organization.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithExistingUser_AndNoOrgUser_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because user exists but has no org user record\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"You were removed from the organization managing single sign-on for your account. Contact the organization administrator\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when an existing user\n    * has an org user record with Invited status - they must accept the invite first.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithExistingUser_AndInvitedOrgUserStatus_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser()\n            .WithOrganizationUser(orgUser =>\n            {\n                orgUser.Status = OrganizationUserStatusType.Invited;\n            })\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because user must accept invite before using SSO\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"you must first log in using your master password\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when organization has no available seats\n    * and cannot auto-scale because it's a self-hosted instance.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithNoAvailableSeats_OnSelfHosted_ReturnsError()\n    {\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithOrganization(org =>\n            {\n                org.Seats = 5; // Organization has seat limit\n            })\n            .AsSelfHosted()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because no seats available and cannot auto-scale on self-hosted\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"No seats available for organization\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when organization has no available seats\n    * and auto-scaling fails (e.g., billing issue, max seats reached).\n    */\n    [Fact]\n    public async Task ExternalCallback_WithNoAvailableSeats_AndAutoAddSeatsFails_ReturnsError()\n    {\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithOrganization(org =>\n            {\n                org.Seats = 5;\n                org.MaxAutoscaleSeats = 5;\n            })\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because auto-adding seats failed\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"No seats available for organization\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when email cannot be found\n    * during new user provisioning (Scenario 2) after bypassing the first email check\n    * via manual linking path (userIdentifier is set).\n    */\n    [Fact]\n    public async Task ExternalCallback_WithUserIdentifier_AndNoEmail_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUserIdentifier(\"\")\n            .WithNullEmail()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because email cannot be found during new user provisioning\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Cannot find email claim\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when org user has an unknown/invalid status.\n    * This tests defensive code that handles future enum values or data corruption scenarios.\n    * We simulate this by casting an invalid integer to OrganizationUserStatusType.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithUnknownOrgUserStatus_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser()\n            .WithOrganizationUser(orgUser =>\n            {\n                orgUser.Status = (OrganizationUserStatusType)99; // Invalid enum value - simulates future status or data corruption\n            })\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because org user status is unknown/invalid\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"is in an unknown state\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    // Note: \"User should be found ln 304\" appears to be unreachable defensive code.\n    // CreateUserAndOrgUserConditionallyAsync always returns a non-null user or throws an exception,\n    // so possibleSsoLinkedUser cannot be null when the feature flag check executes.\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when userIdentifier\n    * is malformed (doesn't contain comma separator for userId,token format).\n    * There is only a single test case here but in the future we may need to expand the\n    * tests to cover other invalid formats.\n    */\n    [Theory]\n    [BitAutoData(\"No-Comas-Identifier\")]\n    public async Task ExternalCallback_WithInvalidUserIdentifierFormat_ReturnsError(\n        string UserIdentifier\n    )\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUserIdentifier(UserIdentifier)\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because userIdentifier format is invalid\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Invalid user identifier\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when userIdentifier\n    * contains valid userId but invalid/mismatched token.\n    *\n    * NOTE: This test uses the substitute pattern instead of SsoTestDataBuilder because:\n    * - The userIdentifier in the auth result must contain a userId that matches a user in the system\n    * - User.SetNewId() always overwrites the Id (unlike Organization.SetNewId() which has a guard)\n    * - This means we cannot pre-set a User.Id before database insertion\n    * - The auth mock must be configured BEFORE accessing factory.Services (required by SubstituteService)\n    * - Therefore, we cannot coordinate the userId between the auth mock and the seeded user\n    * - Using substitutes allows us to control the exact userId and mock UserManager.VerifyUserTokenAsync\n    */\n    [Fact]\n    public async Task ExternalCallback_WithUserIdentifier_AndInvalidToken_ReturnsError()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var providerUserId = Guid.NewGuid().ToString();\n        var userId = Guid.NewGuid();\n        var testEmail = \"test_user@integration.test\";\n        var testName = \"Test User\";\n        // Valid format but token won't verify\n        var userIdentifier = $\"{userId},invalid-token\";\n\n        var claimedUser = new User\n        {\n            Id = userId,\n            Email = testEmail,\n            Name = testName\n        };\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Name = \"Test Organization\",\n            Enabled = true,\n            UseSso = true\n        };\n\n        var ssoConfig = new SsoConfig\n        {\n            OrganizationId = organizationId,\n            Enabled = true\n        };\n        ssoConfig.SetData(new SsoConfigurationData());\n\n        var client = _factory.WithWebHostBuilder(builder =>\n        {\n            builder.ConfigureServices(services =>\n            {\n                // Mock organization repository\n                var orgRepo = Substitute.For<IOrganizationRepository>();\n                orgRepo.GetByIdAsync(organizationId).Returns(organization);\n                orgRepo.GetByIdentifierAsync(organizationId.ToString()).Returns(organization);\n                services.AddSingleton(orgRepo);\n\n                // Mock SSO config repository\n                var ssoConfigRepo = Substitute.For<ISsoConfigRepository>();\n                ssoConfigRepo.GetByOrganizationIdAsync(organizationId).Returns(ssoConfig);\n                services.AddSingleton(ssoConfigRepo);\n\n                // Mock user repository - no existing user via SSO\n                var userRepo = Substitute.For<IUserRepository>();\n                userRepo.GetBySsoUserAsync(providerUserId, organizationId).Returns((User?)null);\n                services.AddSingleton(userRepo);\n\n                // Mock user service - returns user for manual linking lookup\n                var userService = Substitute.For<IUserService>();\n                userService.GetUserByIdAsync(userId.ToString()).Returns(claimedUser);\n                services.AddSingleton(userService);\n\n                // Mock UserManager to return false for token verification\n                var userManager = Substitute.For<UserManager<User>>(\n                    Substitute.For<IUserStore<User>>(), null, null, null, null, null, null, null, null);\n                userManager.VerifyUserTokenAsync(\n                    claimedUser,\n                    Arg.Any<string>(),\n                    Arg.Any<string>(),\n                    Arg.Any<string>())\n                    .Returns(false);\n                services.AddSingleton(userManager);\n\n                // Mock authentication service with userIdentifier that has valid format but invalid token\n                var authService = Substitute.For<IAuthenticationService>();\n                authService.AuthenticateAsync(\n                        Arg.Any<HttpContext>(),\n                        AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)\n                    .Returns(MockSuccessfulAuthResult.Build(organizationId, providerUserId, testEmail, testName, null, userIdentifier));\n                services.AddSingleton(authService);\n            });\n        }).CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because token verification failed\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Supplied userId and token did not match\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error for revoked org user.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithRevokedOrgUser_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser()\n            .WithOrganizationUser(orgUser =>\n            {\n                orgUser.Status = OrganizationUserStatusType.Revoked;\n            })\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because user state is invalid\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\n            $\"Your access to organization {testData.Organization?.DisplayName()} has been revoked. Please contact your administrator for assistance.\",\n            stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when user is found via SSO\n    * but has no organization user record.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithSsoUser_AndNoOrgUser_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser()\n            .WithSsoUser()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because org user cannot be found\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Could not find organization user\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when the provider scheme\n    * is not a valid GUID (SSOProviderIsNotAnOrgId).\n    *\n    * NOTE: This test uses the substitute pattern instead of SsoTestDataBuilder because:\n    * - Organization.Id is of type Guid and cannot be set to a non-GUID value\n    * - The auth mock scheme must be a non-GUID string to trigger this error path\n    * - This cannot be tested since ln 438 in AccountController.FindUserFromExternalProviderAsync throws a different exception\n    *   before reaching the organization lookup exception.\n    */\n    [Fact(Skip = \"This test cannot be executed because the organization ID must be a GUID. See note in test summary.\")]\n    public async Task ExternalCallback_WithInvalidProviderGuid_ReturnsError()\n    {\n        // Arrange\n        var invalidScheme = \"not-a-valid-guid\";\n        var providerUserId = Guid.NewGuid().ToString();\n        var testEmail = \"test@example.com\";\n        var testName = \"Test User\";\n\n        var client = _factory.WithWebHostBuilder(builder =>\n        {\n            builder.ConfigureServices(services =>\n            {\n                // Mock authentication service with invalid (non-GUID) scheme\n                var authService = Substitute.For<IAuthenticationService>();\n                authService.AuthenticateAsync(\n                        Arg.Any<HttpContext>(),\n                        AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)\n                    .Returns(MockSuccessfulAuthResult.Build(invalidScheme, providerUserId, testEmail, testName));\n                services.AddSingleton(authService);\n            });\n        }).CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because provider is not a valid organization GUID\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Organization not found from identifier.\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * Test to verify /Account/ExternalCallback returns error when the organization ID\n    * in the auth result does not match any organization in the database.\n    * NOTE: This code path is unreachable because the SsoConfig must exist to proceed, but there is a circular dependency:\n    * - SsoConfig cannot exist without a valid Organization but the test is testing that an Organization cannot be found.\n    */\n    [Fact(Skip = \"This code path is unreachable because the SsoConfig must exist to proceed. But the SsoConfig cannot exist without a valid Organization.\")]\n    public async Task ExternalCallback_WithNonExistentOrganization_ReturnsError()\n    {\n        // Arrange\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithNonExistentOrganizationInAuth()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient();\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should fail because organization cannot be found by the ID in auth result\n        var stringResponse = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Could not find organization\", stringResponse);\n        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);\n    }\n\n    /*\n    * SUCCESS PATH: Test to verify /Account/ExternalCallback succeeds when an existing\n    * SSO-linked user logs in (user exists in SsoUser table).\n    */\n    [Fact]\n    public async Task ExternalCallback_WithExistingSsoUser_ReturnsSuccess()\n    {\n        // Arrange - User with SSO link already exists\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser()\n            .WithOrganizationUser()\n            .WithSsoUser()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions\n        {\n            AllowAutoRedirect = false // Prevent auto-redirects to capture initial response\n        });\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should succeed and redirect\n        Assert.True(\n            response.StatusCode == HttpStatusCode.Redirect,\n            $\"Expected success/redirect but got {response.StatusCode}\");\n\n        Assert.NotNull(response.Headers.Location);\n    }\n\n    /*\n    * SUCCESS PATH: Test to verify /Account/ExternalCallback succeeds when JIT provisioning\n    * a new user (user doesn't exist, gets created automatically).\n    */\n    [Fact]\n    public async Task ExternalCallback_WithJitProvisioning_ReturnsSuccess()\n    {\n        // Arrange - No user, no org user - JIT provisioning will create both\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions\n        {\n            AllowAutoRedirect = false // Prevent auto-redirects to capture initial response\n        });\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should succeed and redirect\n        Assert.True(\n            response.StatusCode == HttpStatusCode.Redirect,\n            $\"Expected success/redirect but got {response.StatusCode}\");\n\n        Assert.NotNull(response.Headers.Location);\n    }\n\n    /*\n    * SUCCESS PATH: Test to verify /Account/ExternalCallback succeeds when an existing user\n    * with a valid (Confirmed) organization user status logs in via SSO for the first time.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithExistingUserAndConfirmedOrgUser_ReturnsSuccess()\n    {\n        // Arrange - Existing user with confirmed org user status, no SSO link yet\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser()\n            .WithOrganizationUser(orgUser =>\n            {\n                orgUser.Status = OrganizationUserStatusType.Confirmed;\n            })\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions\n        {\n            AllowAutoRedirect = false // Prevent auto-redirects to capture initial response\n        });\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should succeed and redirect\n        Assert.True(\n            response.StatusCode == HttpStatusCode.Redirect,\n            $\"Expected success/redirect but got {response.StatusCode}\");\n\n        Assert.NotNull(response.Headers.Location);\n    }\n\n    /*\n    * SUCCESS PATH: Test to verify /Account/ExternalCallback succeeds when an existing user\n    * with Accepted organization user status logs in via SSO.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithExistingUserAndAcceptedOrgUser_ReturnsSuccess()\n    {\n        // Arrange - Existing user with accepted org user status\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser()\n            .WithOrganizationUser(orgUser =>\n            {\n                orgUser.Status = OrganizationUserStatusType.Accepted;\n            })\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions\n        {\n            AllowAutoRedirect = false // Prevent auto-redirects to capture initial response\n        });\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Should succeed and redirect\n        Assert.True(\n            response.StatusCode == HttpStatusCode.Redirect,\n            $\"Expected success/redirect but got {response.StatusCode}\");\n\n        Assert.NotNull(response.Headers.Location);\n    }\n\n    /*\n    * SUCCESS PATH: Test to verify /Account/ExternalCallback returns a View with 200 status\n    * when the client is a native application (uses custom URI scheme like \"bitwarden://callback\").\n    * Native clients get a different response for better UX - a 200 with redirect view instead of 302.\n    * See AccountController lines 371-378.\n    */\n    [Fact]\n    public async Task ExternalCallback_WithNativeClient_ReturnsViewWith200Status()\n    {\n        // Arrange - Existing SSO user with native client context\n        var testData = await new SsoTestDataBuilder()\n            .WithSsoConfig()\n            .WithUser()\n            .WithOrganizationUser()\n            .WithSsoUser()\n            .AsNativeClient()\n            .BuildAsync();\n\n        var client = testData.Factory.CreateClient(new WebApplicationFactoryClientOptions\n        {\n            AllowAutoRedirect = false\n        });\n\n        // Act\n        var response = await client.GetAsync(\"/Account/ExternalCallback\");\n\n        // Assert - Native clients get 200 status with a redirect view instead of 302\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // The Location header should be empty for native clients (set in controller)\n        // and the response should contain the redirect view\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.NotEmpty(content); // View content should be present\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Sso.IntegrationTest/Properties/launchSettings.json",
    "content": "{\n  \"profiles\": {\n    \"Sso.IntegrationTest\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      },\n      \"applicationUrl\": \"https://localhost:59973;http://localhost:59974\"\n    }\n  }\n}"
  },
  {
    "path": "bitwarden_license/test/Sso.IntegrationTest/Sso.IntegrationTest.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.AspNetCore.Mvc.Testing\" Version=\"8.0.10\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"NSubstitute\" Version=\"$(NSubstituteVersion)\" />\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"AutoFixture.Xunit2\" Version=\"$(AutoFixtureXUnit2Version)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Sso\\Sso.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\test\\Common\\Common.csproj\" />\n    <ProjectReference Include=\"..\\..\\..\\test\\IntegrationTestCommon\\IntegrationTestCommon.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Content Update=\"Properties\\launchSettings.json\">\n      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <CopyToPublishDirectory>Never</CopyToPublishDirectory>\n    </Content>\n  </ItemGroup>\n\n</Project>"
  },
  {
    "path": "bitwarden_license/test/Sso.IntegrationTest/Utilities/SsoApplicationFactory.cs",
    "content": "﻿using Bit.IntegrationTestCommon.Factories;\n\nnamespace Bit.Sso.IntegrationTest.Utilities;\n\npublic class SsoApplicationFactory : WebApplicationFactoryBase<Startup>\n{\n    protected override void ConfigureWebHost(IWebHostBuilder builder)\n    {\n        base.ConfigureWebHost(builder);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Sso.IntegrationTest/Utilities/SsoTestDataBuilder.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bitwarden.License.Test.Sso.IntegrationTest.Utilities;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Services;\nusing Microsoft.AspNetCore.Authentication;\nusing NSubstitute;\nusing AuthenticationSchemes = Bit.Core.AuthenticationSchemes;\n\nnamespace Bit.Sso.IntegrationTest.Utilities;\n\n/// <summary>\n/// Contains the factory and all entities created by <see cref=\"SsoTestDataBuilder\"/> for use in integration tests.\n/// </summary>\npublic record SsoTestData(\n    SsoApplicationFactory Factory,\n    Organization? Organization,\n    User? User,\n    OrganizationUser? OrganizationUser,\n    SsoConfig? SsoConfig,\n    SsoUser? SsoUser);\n\n/// <summary>\n/// Builder for creating SSO test data with seeded database entities.\n/// </summary>\npublic class SsoTestDataBuilder\n{\n    /// <summary>\n    /// This UserIdentifier is a mock for the UserIdentifier we get from the External Identity Provider.\n    /// </summary>\n    private string? _userIdentifier;\n    private Action<Organization>? _organizationConfig;\n    private Action<User>? _userConfig;\n    private Action<OrganizationUser>? _orgUserConfig;\n    private Action<SsoConfig>? _ssoConfigConfig;\n    private Action<SsoUser>? _ssoUserConfig;\n    private Action<SsoApplicationFactory>? _featureFlagConfig;\n\n    private bool _includeUser = false;\n    private bool _includeSsoUser = false;\n    private bool _includeOrganizationUser = false;\n    private bool _includeSsoConfig = false;\n    private bool _successfulAuth = true;\n    private bool _withNullEmail = false;\n    private bool _isSelfHosted = false;\n    private bool _includeProviderUserId = true;\n    private bool _useNonExistentOrgInAuth = false;\n    private bool _isNativeClient = false;\n\n    public SsoTestDataBuilder WithOrganization(Action<Organization> configure)\n    {\n        _organizationConfig = configure;\n        return this;\n    }\n\n    public SsoTestDataBuilder WithUser(Action<User>? configure = null)\n    {\n        _includeUser = true;\n        _userConfig = configure;\n        return this;\n    }\n\n    public SsoTestDataBuilder WithOrganizationUser(Action<OrganizationUser>? configure = null)\n    {\n        _includeOrganizationUser = true;\n        _orgUserConfig = configure;\n        return this;\n    }\n\n    public SsoTestDataBuilder WithSsoConfig(Action<SsoConfig>? configure = null)\n    {\n        _includeSsoConfig = true;\n        _ssoConfigConfig = configure;\n        return this;\n    }\n\n    public SsoTestDataBuilder WithSsoUser(Action<SsoUser>? configure = null)\n    {\n        _includeSsoUser = true;\n        _ssoUserConfig = configure;\n        return this;\n    }\n\n    public SsoTestDataBuilder WithFeatureFlags(Action<SsoApplicationFactory> configure)\n    {\n        _featureFlagConfig = configure;\n        return this;\n    }\n\n    public SsoTestDataBuilder WithFailedAuthentication()\n    {\n        _successfulAuth = false;\n        return this;\n    }\n\n    public SsoTestDataBuilder WithNullEmail()\n    {\n        _withNullEmail = true;\n        return this;\n    }\n\n    public SsoTestDataBuilder WithUserIdentifier(string userIdentifier)\n    {\n        _userIdentifier = userIdentifier;\n        return this;\n    }\n\n    public SsoTestDataBuilder OmitProviderUserId()\n    {\n        _includeProviderUserId = false;\n        return this;\n    }\n\n    public SsoTestDataBuilder AsSelfHosted()\n    {\n        _isSelfHosted = true;\n        return this;\n    }\n\n    /// <summary>\n    /// Causes the auth result to use a different (non-existent) organization ID than what is seeded\n    /// in the database. This simulates the \"organization not found\" scenario.\n    /// </summary>\n    public SsoTestDataBuilder WithNonExistentOrganizationInAuth()\n    {\n        _useNonExistentOrgInAuth = true;\n        return this;\n    }\n\n    /// <summary>\n    /// Configures the test to simulate a native client (non-browser) OIDC flow.\n    /// Native clients use custom URI schemes (e.g., \"bitwarden://callback\") instead of http/https.\n    /// This causes ExternalCallback to return a View with 200 status instead of a redirect.\n    /// </summary>\n    public SsoTestDataBuilder AsNativeClient()\n    {\n        _isNativeClient = true;\n        return this;\n    }\n\n    public async Task<SsoTestData> BuildAsync()\n    {\n        // Create factory\n        var factory = new SsoApplicationFactory();\n\n        // Pre-generate IDs and values needed for auth mock (before accessing Services)\n        var organizationId = Guid.NewGuid();\n        // Use a different org ID in auth if testing \"organization not found\" scenario\n        var authOrganizationId = _useNonExistentOrgInAuth ? Guid.NewGuid() : organizationId;\n        var providerUserId = _includeProviderUserId ? Guid.NewGuid().ToString() : \"\";\n        var userEmail = _withNullEmail ? null : $\"user_{Guid.NewGuid()}@test.com\";\n        var userName = \"TestUser\";\n\n        // 1. Configure mocked authentication service BEFORE accessing Services\n        factory.SubstituteService<IAuthenticationService>(authService =>\n        {\n            if (_successfulAuth)\n            {\n                authService.AuthenticateAsync(\n                        Arg.Any<HttpContext>(),\n                        AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)\n                    .Returns(MockSuccessfulAuthResult.Build(\n                        authOrganizationId,\n                        providerUserId,\n                        userEmail,\n                        userName,\n                        acrValue: null,\n                        _userIdentifier));\n            }\n            else\n            {\n                authService.AuthenticateAsync(\n                        Arg.Any<HttpContext>(),\n                        AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)\n                    .Returns(AuthenticateResult.Fail(\"External authentication error\"));\n            }\n        });\n\n        // 1.a Configure GlobalSettings for Self-Hosted and seat limit\n        factory.SubstituteService<IGlobalSettings>(globalSettings =>\n        {\n            globalSettings.SelfHosted.Returns(_isSelfHosted);\n        });\n\n        // 1.b configure setting feature flags\n        _featureFlagConfig?.Invoke(factory);\n\n        // 1.c Configure IIdentityServerInteractionService for native client flow\n        if (_isNativeClient)\n        {\n            factory.SubstituteService<IIdentityServerInteractionService>(interaction =>\n            {\n                // Native clients have redirect URIs that don't start with http/https\n                // e.g., \"bitwarden://callback\" or \"com.bitwarden.app://callback\"\n                var authorizationRequest = new AuthorizationRequest\n                {\n                    RedirectUri = \"bitwarden://sso-callback\"\n                };\n                interaction.GetAuthorizationContextAsync(Arg.Any<string>())\n                    .Returns(authorizationRequest);\n            });\n        }\n\n        if (!_successfulAuth)\n        {\n            return new SsoTestData(factory, null!, null!, null!, null!, null!);\n        }\n\n        // 2. Create Organization with defaults (using pre-generated ID)\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Name = \"Test Organization\",\n            BillingEmail = \"billing@test.com\",\n            Plan = \"Enterprise\",\n            Enabled = true,\n            UseSso = true\n        };\n        _organizationConfig?.Invoke(organization);\n\n        var orgRepo = factory.Services.GetRequiredService<IOrganizationRepository>();\n        organization = await orgRepo.CreateAsync(organization);\n\n        // 3. Create User with defaults (using pre-generated values)\n        User? user = null;\n        if (_includeUser)\n        {\n            user = new User\n            {\n                Email = userEmail ?? $\"email_{Guid.NewGuid()}@test.dev\",\n                Name = userName,\n                ApiKey = Guid.NewGuid().ToString(),\n                SecurityStamp = Guid.NewGuid().ToString()\n            };\n            _userConfig?.Invoke(user);\n\n            var userRepo = factory.Services.GetRequiredService<IUserRepository>();\n            user = await userRepo.CreateAsync(user);\n        }\n\n        // 4. Create OrganizationUser linking them\n        OrganizationUser? orgUser = null;\n        if (_includeOrganizationUser)\n        {\n            orgUser = new OrganizationUser\n            {\n                OrganizationId = organization.Id,\n                UserId = user!.Id,\n                Status = OrganizationUserStatusType.Confirmed,\n                Type = OrganizationUserType.User\n            };\n            _orgUserConfig?.Invoke(orgUser);\n\n            var orgUserRepo = factory.Services.GetRequiredService<IOrganizationUserRepository>();\n            orgUser = await orgUserRepo.CreateAsync(orgUser);\n        }\n\n        // 4.a Create many OrganizationUser to test seat count logic\n        if (organization.Seats > 1)\n        {\n            var orgUserRepo = factory.Services.GetRequiredService<IOrganizationUserRepository>();\n            var userRepo = factory.Services.GetRequiredService<IUserRepository>();\n            var additionalOrgUsers = new List<OrganizationUser>();\n            for (var i = 1; i <= organization.Seats; i++)\n            {\n                var additionalUser = new User\n                {\n                    Email = $\"additional_user_{i}_{Guid.NewGuid()}@test.dev\",\n                    Name = $\"AdditionalUser{i}\",\n                    ApiKey = Guid.NewGuid().ToString(),\n                    SecurityStamp = Guid.NewGuid().ToString()\n                };\n                var createdAdditionalUser = await userRepo.CreateAsync(additionalUser);\n\n                var additionalOrgUser = new OrganizationUser\n                {\n                    OrganizationId = organization.Id,\n                    UserId = createdAdditionalUser.Id,\n                    Status = OrganizationUserStatusType.Confirmed,\n                    Type = OrganizationUserType.User\n                };\n                additionalOrgUsers.Add(additionalOrgUser);\n            }\n            await orgUserRepo.CreateManyAsync(additionalOrgUsers);\n        }\n\n        // 5. Create SsoConfig, if ssoConfigConfig is not null\n        SsoConfig? ssoConfig = null;\n        if (_includeSsoConfig)\n        {\n            ssoConfig = new SsoConfig\n            {\n                OrganizationId = authOrganizationId,\n                Enabled = true\n            };\n            ssoConfig.SetData(new SsoConfigurationData());\n            _ssoConfigConfig?.Invoke(ssoConfig);\n\n            var ssoConfigRepo = factory.Services.GetRequiredService<ISsoConfigRepository>();\n            ssoConfig = await ssoConfigRepo.CreateAsync(ssoConfig);\n        }\n\n        // 6. Optionally create SsoUser (using pre-generated providerUserId as ExternalId)\n        SsoUser? ssoUser = null;\n        if (_includeSsoUser)\n        {\n            ssoUser = new SsoUser\n            {\n                OrganizationId = organization.Id,\n                UserId = user!.Id,\n                ExternalId = providerUserId\n            };\n            _ssoUserConfig?.Invoke(ssoUser);\n\n            var ssoUserRepo = factory.Services.GetRequiredService<ISsoUserRepository>();\n            ssoUser = await ssoUserRepo.CreateAsync(ssoUser);\n        }\n\n        return new SsoTestData(factory, organization, user, orgUser, ssoConfig, ssoUser);\n    }\n}\n"
  },
  {
    "path": "bitwarden_license/test/Sso.IntegrationTest/Utilities/SuccessfulAuthResult.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core;\nusing Duende.IdentityModel;\nusing Microsoft.AspNetCore.Authentication;\n\nnamespace Bitwarden.License.Test.Sso.IntegrationTest.Utilities;\n\n/// <summary>\n/// Creates a mock for use in tests requiring a valid external authentication result.\n/// </summary>\ninternal static class MockSuccessfulAuthResult\n{\n    /// <summary>\n    /// Since this tests the external Authentication flow, only the OrganizationId is strictly required.\n    /// However, some tests may require additional claims to be present, so they can be optionally added.\n    /// </summary>\n    /// <param name=\"organizationId\"></param>\n    /// <param name=\"providerUserId\"></param>\n    /// <param name=\"email\"></param>\n    /// <param name=\"name\"></param>\n    /// <param name=\"acrValue\"></param>\n    /// <param name=\"userIdentifier\"></param>\n    /// <returns></returns>\n    public static AuthenticateResult Build(\n            Guid organizationId,\n            string? providerUserId,\n            string? email,\n            string? name = null,\n            string? acrValue = null,\n            string? userIdentifier = null)\n    {\n        return Build(organizationId.ToString(), providerUserId, email, name, acrValue, userIdentifier);\n    }\n\n    /// <summary>\n    /// Overload that accepts a custom scheme string. Useful for testing invalid provider scenarios\n    /// where the scheme is not a valid GUID.\n    /// </summary>\n    public static AuthenticateResult Build(\n            string scheme,\n            string? providerUserId,\n            string? email,\n            string? name = null,\n            string? acrValue = null,\n            string? userIdentifier = null)\n    {\n        var claims = new List<Claim>();\n\n        if (!string.IsNullOrEmpty(email))\n        {\n            claims.Add(new Claim(JwtClaimTypes.Email, email));\n        }\n\n        if (!string.IsNullOrEmpty(providerUserId))\n        {\n            claims.Add(new Claim(JwtClaimTypes.Subject, providerUserId));\n        }\n\n        if (!string.IsNullOrEmpty(name))\n        {\n            claims.Add(new Claim(JwtClaimTypes.Name, name));\n        }\n\n        if (!string.IsNullOrEmpty(acrValue))\n        {\n            claims.Add(new Claim(JwtClaimTypes.AuthenticationContextClassReference, acrValue));\n        }\n\n        var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, \"External\"));\n        var properties = new AuthenticationProperties\n        {\n            Items =\n            {\n                [\"scheme\"] = scheme,\n                [\"return_url\"] = \"~/\",\n                [\"state\"] = \"test-state\",\n                [\"user_identifier\"] = userIdentifier ?? string.Empty\n            }\n        };\n\n        var ticket = new AuthenticationTicket(\n            principal,\n            properties,\n            AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);\n\n        return AuthenticateResult.Success(ticket);\n    }\n}\n"
  },
  {
    "path": "dev/.gitignore",
    "content": ".data\nsecrets.json\n*.db\n\n# Docker container configurations\n.env\nauthsources.php\n\n# Development certificates\nidentity_server_dev.crt\nidentity_server_dev.key\nidentity_server_dev.pfx\ndata_protection_dev.crt\ndata_protection_dev.key\ndata_protection_dev.pfx\n\nsigningkey.jwk\n\n# Reverse Proxy Conifg\nreverse-proxy.conf\n*.crt\n"
  },
  {
    "path": "dev/authsources.php.example",
    "content": "<?php\n\n$config = array(\n    'admin' => array(\n        'core:AdminPassword',\n    ),\n\n    'example-userpass' => array(\n        'exampleauth:UserPass',\n        'user1:password' => array(\n            'email' => 'user1@example.com',\n            'uid' => array('user1'),\n        ),\n        'user2:password' => array(\n            'email' => 'user2@example.com',\n            'uid' => array('user2'),\n        ),\n    ),\n\n);\n"
  },
  {
    "path": "dev/create_certificates_linux.sh",
    "content": "#!/usr/bin/env bash\n# Script for generating and installing the Bitwarden development certificates on Linux.\n\nIDENTITY_SERVER_KEY=identity_server_dev.key\nIDENTITY_SERVER_CERT=identity_server_dev.crt\nIDENTITY_SERVER_CN=\"Bitwarden Identity Server Dev\"\n\n# Detect management command to trust generated certificates.\nif [ -x \"$(command -v update-ca-certificates)\" ]; then\n  # Debian based\n  CA_CERT_DIR=/usr/local/share/ca-certificates/\n  UPDATE_CA_CMD=update-ca-certificates\nelif [ -x \"$(command -v update-ca-trust)\" ]; then\n  # Redhat based\n  CA_CERT_DIR=/etc/pki/ca-trust/source/anchors/\n  UPDATE_CA_CMD=update-ca-trust\nelse\n  echo 'Error: Update manager for CA certificates not found!'\n  exit 1\nfi\n\n\nopenssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 3650 \\\n    -keyout $IDENTITY_SERVER_KEY \\\n    -out $IDENTITY_SERVER_CERT \\\n    -subj \"/CN=$IDENTITY_SERVER_CN\"\n\nsudo cp $IDENTITY_SERVER_CERT $CA_CERT_DIR\n\nsudo $UPDATE_CA_CMD\n\nidentity=($(openssl x509 -in $IDENTITY_SERVER_CERT -outform der | sha1sum | tr a-z A-Z))\n\necho \"Certificate fingerprints:\"\n\necho \"Identity Server Dev: ${identity}\"\n"
  },
  {
    "path": "dev/create_certificates_mac.sh",
    "content": "#!/usr/bin/env bash\n\nopenssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout identity_server_dev.key -out identity_server_dev.crt \\\n    -subj \"/CN=Bitwarden Identity Server Dev\" -days 3650\nopenssl pkcs12 -export -legacy -out identity_server_dev.pfx -inkey identity_server_dev.key -in identity_server_dev.crt \\\n    -certfile identity_server_dev.crt\n\nsecurity import ./identity_server_dev.pfx -k ~/Library/Keychains/Login.keychain\n\nidentity=($(openssl x509 -in identity_server_dev.crt -outform der | shasum -a 1 | tr a-z A-Z));\n\necho \"Certificate fingerprints:\"\n\necho \"Identity Server Dev: ${identity}\"\n"
  },
  {
    "path": "dev/create_certificates_windows.ps1",
    "content": "# Script for generating and installing the Bitwarden development certificates on Windows.\n\n$params = @{\n    'KeyAlgorithm' = 'RSA';\n    'KeyLength' = 4096;\n    'NotAfter' = (Get-Date).AddDays(3650);\n    'CertStoreLocation' = 'Cert:\\CurrentUser\\My';\n};\n\n$params['Subject'] = 'CN=Bitwarden Identity Server Dev';\nNew-SelfSignedCertificate @params;\n"
  },
  {
    "path": "dev/docker-compose.yml",
    "content": "version: \"3.9\"\n\nservices:\n  mssql:\n    image: mcr.microsoft.com/mssql/server:2022-latest\n    platform: linux/amd64\n    environment:\n      ACCEPT_EULA: \"Y\"\n      MSSQL_SA_PASSWORD: ${MSSQL_PASSWORD}\n      MSSQL_PID: Developer\n    volumes:\n      - mssql_dev_data:/var/opt/mssql\n      - ../util/Migrator:/mnt/migrator/\n      - ./helpers/mssql:/mnt/helpers\n      - ./.data/mssql:/mnt/data\n    ports:\n      - \"1433:1433\"\n    profiles:\n      - cloud\n      - mssql\n\n  storage:\n    image: mcr.microsoft.com/azure-storage/azurite:latest\n    ports:\n      - \"10000:10000\"\n      - \"10001:10001\"\n      - \"10002:10002\"\n    volumes:\n      - ./.data/azurite:/data\n    profiles:\n      - storage\n      - cloud\n\n  mail:\n    image: sj26/mailcatcher:latest\n    ports:\n      - \"${MAILCATCHER_PORT}:1080\"\n      - \"10250:1025\"\n    profiles:\n      - mail\n\n  postgres:\n    image: postgres:14\n    ports:\n      - \"5432:5432\"\n    environment:\n      POSTGRES_DB: vault_dev\n      POSTGRES_USER: postgres\n      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}\n    volumes:\n      - postgres_dev_data:/var/lib/postgresql/data\n      - ./.data/postgres/config:/etc/postgresql\n      - ./.data/postgres/log:/var/log/postgresql\n    profiles:\n      - postgres\n      - ef\n\n  mysql:\n    image: mysql:8.0\n    ports:\n      - \"3306:3306\"\n    command:\n      - --default-authentication-plugin=mysql_native_password\n      - --innodb-print-all-deadlocks=ON\n    environment:\n      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}\n      MYSQL_DATABASE: vault_dev\n    volumes:\n      - mysql_dev_data:/var/lib/mysql\n    profiles:\n      - mysql\n      - ef\n\n  mariadb:\n    image: mariadb:12\n    ports:\n      - 4306:3306\n    environment:\n      MARIADB_USER: maria\n      MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD}\n      MARIADB_DATABASE: vault_dev\n      MARIADB_RANDOM_ROOT_PASSWORD: \"true\"\n    volumes:\n      - mariadb_dev_data:/var/lib/mysql\n    profiles:\n      - mariadb\n      - ef\n\n  idp:\n    image: kenchan0130/simplesamlphp:1.19.8\n    ports:\n      - \"8090:8080\"\n    environment:\n      SIMPLESAMLPHP_SP_ENTITY_ID: ${IDP_SP_ENTITY_ID}\n      SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: ${IDP_SP_ACS_URL}\n      SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: null\n    volumes:\n      - ./authsources.php:/var/www/simplesamlphp/config/authsources.php\n    profiles:\n      - idp\n\n  rabbitmq:\n    image: rabbitmq:4.2.0-management\n    ports:\n      - \"5672:5672\"\n      - \"15672:15672\"\n    environment:\n      RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}\n      RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}\n    volumes:\n      - rabbitmq_data:/var/lib/rabbitmq\n    profiles:\n      - rabbitmq\n\n  reverse-proxy:\n    image: nginx:alpine\n    volumes:\n      - \"./reverse-proxy.conf:/etc/nginx/conf.d/default.conf\"\n    ports:\n      - \"${API_PROXY_PORT}:${API_PROXY_PORT}\"\n      - \"${IDENTITY_PROXY_PORT}:${IDENTITY_PROXY_PORT}\"\n    profiles:\n      - proxy\n\n  service-bus:\n    image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest\n    pull_policy: always\n    volumes:\n      - \"./servicebusemulator_config.json:/ServiceBus_Emulator/ConfigFiles/Config.json\"\n    ports:\n      - \"5672:5672\"\n    environment:\n      SQL_SERVER: mssql\n      MSSQL_SA_PASSWORD: \"${MSSQL_PASSWORD}\"\n      ACCEPT_EULA: \"Y\"\n    profiles:\n      - servicebus\n\n  redis:\n    image: redis:alpine\n    ports:\n      - \"6379:6379\"\n    volumes:\n      - redis_data:/data\n    command: redis-server --appendonly yes\n    profiles:\n      - redis\n      - cloud\n\nvolumes:\n  mssql_dev_data:\n  postgres_dev_data:\n  mysql_dev_data:\n  mariadb_dev_data:\n  rabbitmq_data:\n  redis_data:\n"
  },
  {
    "path": "dev/ef_migrate.ps1",
    "content": "#!/usr/bin/env pwsh\nparam (\n  [Parameter(Mandatory)]\n  $Name\n)\n\n# DB service provider name\n$service = \"mysql\"\n\nWrite-Output \"--- Attempting to start $service service ---\"\n\n# Attempt to start mysql but if docker-compose doesn't\n# exist just trust that the user has it running some other way\nif (command -v docker-compose) {\n  docker-compose --profile $service up -d --no-recreate\n}\n\ndotnet tool restore\n\n$providers = @{\n    MySql = \"../util/MySqlMigrations\"\n    Postgres = \"../util/PostgresMigrations\"\n    Sqlite = \"../util/SqliteMigrations\"\n}\n\nforeach ($key in $providers.keys) {\n    Write-Output \"--- START $key ---\"\n    dotnet ef migrations add $Name -s $providers[$key]\n    Write-Output \"--- END $key ---\"\n}\n"
  },
  {
    "path": "dev/generate_openapi_files.ps1",
    "content": "Set-Location \"$PSScriptRoot/..\"\n\n$env:ASPNETCORE_ENVIRONMENT = \"Development\"\n$env:swaggerGen = \"True\"\n$env:DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX = \"2\"\n$env:GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING = \"placeholder\"\n\ndotnet tool restore\n\n# Identity\nSet-Location \"./src/Identity\"\ndotnet build\ndotnet swagger tofile --output \"../../identity.json\" --host \"https://identity.bitwarden.com\" \"./bin/Debug/net8.0/Identity.dll\" \"v1\"\nif ($LASTEXITCODE -ne 0) {\n    exit $LASTEXITCODE\n}\n\n# Api internal & public\nSet-Location \"../../src/Api\"\ndotnet build\ndotnet swagger tofile --output \"../../api.json\" \"./bin/Debug/net8.0/Api.dll\" \"internal\"\nif ($LASTEXITCODE -ne 0) {\n    exit $LASTEXITCODE\n}\ndotnet swagger tofile --output \"../../api.public.json\" \"./bin/Debug/net8.0/Api.dll\" \"public\"\nif ($LASTEXITCODE -ne 0) {\n    exit $LASTEXITCODE\n}\n"
  },
  {
    "path": "dev/migrate.ps1",
    "content": "#!/usr/bin/env pwsh\n# Creates the vault_dev database, and runs all the migrations.\n\nparam(\n  [switch]$all,\n  [switch]$postgres,\n  [switch]$mysql,\n  [switch]$mariadb,\n  [switch]$mssql,\n  [switch]$sqlite,\n  [switch]$selfhost,\n  [switch]$test\n)\n\n# Abort on any error\n$ErrorActionPreference = \"Stop\"\n$currentDir = Get-Location\n\nfunction Get-IsEFDatabase {\n  return $postgres -or $mysql -or $mariadb -or $sqlite;\n}\n\nif (!$all -and !$(Get-IsEFDatabase)) {\n  $mssql = $true;\n}\n\nif ($all -or $(Get-IsEFDatabase)) {\n  dotnet ef *> $null\n  if ($LASTEXITCODE -ne 0) {\n    Write-Host \"Entity Framework Core tools were not found in the dotnet global tools. Attempting to install\"\n    dotnet tool install dotnet-ef -g\n  }\n}\n\nfunction Get-UserSecrets {\n  # The dotnet cli command sometimes adds //BEGIN and //END comments to the output, Where-Object removes comments\n  # to ensure a valid json\n  return dotnet user-secrets list --json --project \"$currentDir/../src/Api\" | Where-Object { $_ -notmatch \"^//\" } | ConvertFrom-Json\n}\n\nif ($all -or $mssql) {\n  if ($all -or !$test) {\n    if ($selfhost) {\n      $msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'\n      $envName = \"self-host\"\n    } else {\n      $msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'\n      $envName = \"cloud\"\n    }\n\n    Write-Host \"Starting Microsoft SQL Server Migrations for $envName\"\n    dotnet run --project ../util/MsSqlMigratorUtility/ \"$msSqlConnectionString\"\n  }\n\n  if ($all -or $test) {\n    $testMsSqlConnectionString = $(Get-UserSecrets).'databases:3:connectionString'\n    if ($testMsSqlConnectionString) {\n      $testEnvName = \"test databases\"\n      Write-Host \"Starting Microsoft SQL Server Migrations for $testEnvName\"\n      dotnet run --project ../util/MsSqlMigratorUtility/ \"$testMsSqlConnectionString\"\n    } else {\n      Write-Host \"Connection string for a test MSSQL database not found in secrets.json!\"\n    }\n  }\n}\n\nForeach ($item in @(\n    @($postgres, \"PostgreSQL\", \"PostgresMigrations\", \"postgreSql\", 0),\n    @($sqlite, \"SQLite\", \"SqliteMigrations\", \"sqlite\", 1),\n    @($mysql, \"MySQL\", \"MySqlMigrations\", \"mySql\", 2),\n    # MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context.\n    # However they can still be run independently for integration tests.\n    @($mariadb, \"MariaDB\", \"MySqlMigrations\", \"mySql\", 4) \n)) {\n  if (!$item[0] -and !$all) {\n    continue\n  }\n\n  Set-Location \"$currentDir/../util/$($item[2])/\"\n  if(!$test -or $all) {\n    Write-Host \"Starting $($item[1]) Migrations\"\n    $connectionString = $(Get-UserSecrets).\"globalSettings:$($item[3]):connectionString\"\n    dotnet ef database update --connection \"$connectionString\"\n  }\n  if ($test -or $all) {\n    $testConnectionString = $(Get-UserSecrets).\"databases:$($item[4]):connectionString\"\n    if ($testConnectionString) {\n      Write-Host \"Starting $($item[1]) Migrations for test databases\"\n      dotnet ef database update --connection \"$testConnectionString\"\n    } else {\n      Write-Host \"Connection string for a test $($item[1]) database not found in secrets.json!\"\n    }\n  }\n}\n\nSet-Location \"$currentDir\"\n"
  },
  {
    "path": "dev/reverse-proxy.conf.example",
    "content": "# Begin API Service\n\nupstream api_loadbalancer {\n    # Add additional API services here uniquely identified by their port\n    # Below assumes two services running on the docker host machine on ports 4000 and 4002\n    server host.docker.internal:4000;\n    server host.docker.internal:4002;\n}\n\nserver {\n    listen 4100; # The port clients will connect to for the Api, must be exposed via Docker\n    location / {\n        proxy_pass http://api_loadbalancer;\n    }\n}\n\n# End API Service\n\n# Begin Identity Service\n\nupstream identity_loadbalancer {\n    # Add additional Identity services here uniquely identified by their port\n    # Below assumes two services running on the docker host machine on ports 33656 and 33658\n    server host.docker.internal:33656;\n    server host.docker.internal:33658;\n}\n\nserver {\n    listen 33756; # The port clients will connect to for the Identity, must be exposed via Docker\n    location / {\n        proxy_pass http://identity_loadbalancer;\n    }\n}\n\n# End Identity Service"
  },
  {
    "path": "dev/secrets.json.example",
    "content": "{\n  \"adminSettings\": {\n    \"admins\": \"admin@localhost,owner@localhost,cs@localhost,billing@localhost,sales@localhost\",\n    \"role\": {\n        \"owner\": \"owner@localhost\",\n        \"admin\": \"admin@localhost\",\n        \"cs\": \"cs@localhost\",\n        \"billing\": \"billing@localhost\",\n        \"sales\": \"sales@localhost\"\n    }\n  },\n  \"globalSettings\": {\n    \"selfHosted\": true,\n    \"sqlServer\": {\n      \"connectionString\": \"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True\"\n    },\n    \"postgreSql\": {\n      \"connectionString\": \"Host=localhost;Username=postgres;Password=SET_A_PASSWORD_HERE_123;Database=vault_dev;Include Error Detail=true\"\n    },\n    \"mySql\": {\n      \"connectionString\": \"server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev\"\n    },\n    \"sqlite\": {\n      \"connectionString\": \"Data Source=/path/to/bitwardenServer/repository/server/dev/db/bitwarden.db\"\n    },\n    \"identityServer\": {\n      \"certificateThumbprint\": \"<your Identity certificate thumbprint with no spaces>\"\n    },\n    \"dataProtection\": {\n      \"certificateThumbprint\": \"<your Data Protection certificate thumbprint with no spaces>\"\n    },\n    \"installation\": {\n      \"id\": \"<your Installation Id>\",\n      \"key\": \"<your Installation Key>\"\n    },\n    \"events\": {\n      \"connectionString\": \"\",\n      \"queueName\": \"event\"\n    },\n    \"licenseDirectory\": \"<full path to license directory>\",\n    \"enableNewDeviceVerification\": true,\n    \"enableEmailVerification\": true,\n    \"communication\": {\n      \"bootstrap\": \"none\",\n      \"ssoCookieVendor\": {\n        \"idpLoginUrl\": \"\",\n        \"cookieName\": \"\",\n        \"cookieDomain\": \"\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "dev/servicebusemulator_config.json",
    "content": "{\n  \"UserConfig\": {\n    \"Namespaces\": [\n      {\n        \"Name\": \"sbemulatorns\",\n        \"Topics\": [\n          {\n            \"Name\": \"event-logging\",\n            \"Subscriptions\": [\n              {\n                \"Name\": \"events-write-subscription\"\n              },\n              {\n                \"Name\": \"events-slack-subscription\"\n              },\n              {\n                \"Name\": \"events-webhook-subscription\"\n              },\n              {\n                \"Name\": \"events-hec-subscription\"\n              },\n              {\n                \"Name\": \"events-datadog-subscription\"\n              },\n              {\n                \"Name\": \"events-teams-subscription\"\n              }\n            ]\n          },\n          {\n            \"Name\": \"event-integrations\",\n            \"Subscriptions\": [\n              {\n                \"Name\": \"integration-slack-subscription\",\n                \"Rules\": [\n                  {\n                    \"Name\": \"slack-integration-filter\",\n                    \"Properties\": {\n                      \"FilterType\": \"Correlation\",\n                      \"CorrelationFilter\": {\n                        \"Label\": \"slack\"\n                      }\n                    }\n                  }\n                ]\n              },\n              {\n                \"Name\": \"integration-webhook-subscription\",\n                \"Rules\": [\n                  {\n                    \"Name\": \"webhook-integration-filter\",\n                    \"Properties\": {\n                      \"FilterType\": \"Correlation\",\n                      \"CorrelationFilter\": {\n                        \"Label\": \"webhook\"\n                      }\n                    }\n                  }\n                ]\n              },\n              {\n                \"Name\": \"integration-hec-subscription\",\n                \"Rules\": [\n                  {\n                    \"Name\": \"hec-integration-filter\",\n                    \"Properties\": {\n                      \"FilterType\": \"Correlation\",\n                      \"CorrelationFilter\": {\n                        \"Label\": \"hec\"\n                      }\n                    }\n                  }\n                ]\n              },\n              {\n                \"Name\": \"integration-datadog-subscription\",\n                \"Rules\": [\n                  {\n                    \"Name\": \"datadog-integration-filter\",\n                    \"Properties\": {\n                      \"FilterType\": \"Correlation\",\n                      \"CorrelationFilter\": {\n                        \"Label\": \"datadog\"\n                      }\n                    }\n                  }\n                ]\n              },\n              {\n                \"Name\": \"integration-teams-subscription\",\n                \"Rules\": [\n                  {\n                    \"Name\": \"teams-integration-filter\",\n                    \"Properties\": {\n                      \"FilterType\": \"Correlation\",\n                      \"CorrelationFilter\": {\n                        \"Label\": \"teams\"\n                      }\n                    }\n                  }\n                ]\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    \"Logging\": {\n      \"Type\": \"File\"\n    }\n  }\n}\n"
  },
  {
    "path": "dev/setup_azurite.ps1",
    "content": "#!/usr/bin/env pwsh\n# Script for configuring the initial state of Azurite Storage account\n#  Can be run multiple times without negative impact\n\n# Start configuration\n$corsRules = (@{\n        AllowedHeaders  = @(\"*\");\n        ExposedHeaders  = @(\"*\");\n        AllowedOrigins  = @(\"*\");\n        MaxAgeInSeconds = 30;\n        AllowedMethods  = @(\"Get\", \"PUT\");\n    });\n$containers = \"attachments\", \"sendfiles\", \"misc\";\n$queues = \"event\", \"notifications\", \"mail\";\n$tables = \"event\", \"metadata\", \"installationdevice\";\n# End configuration\n\n$context = New-AzStorageContext -Local\n\nforeach ($container in $containers) {\n    if (Get-AzStorageContainer -Name $container -Context $context -ErrorAction SilentlyContinue) {\n        Write-Host -ForegroundColor Magenta \"Container already exists:\" $container\n    }\n    else {\n        New-AzStorageContainer -Name $container -Context $context\n    }\n}\n\nforeach ($queue in $queues) {\n    if (Get-AzStorageQueue -Name $queue -Context $context -ErrorAction SilentlyContinue) {\n        Write-Host -ForegroundColor Magenta \"Queue already exists:\" $queue\n    }\n    else {\n        New-AzStorageQueue -Name $queue -Context $context\n    }\n}\n\nforeach ($table in $tables) {\n    if (Get-AzStorageTable -Name $table -Context $context -ErrorAction SilentlyContinue) {\n        Write-Host -ForegroundColor Magenta \"Table already exists:\" $table\n    }\n    else {\n        New-AzStorageTable -Name $table -Context $context\n    }\n}\n\nSet-AzStorageCORSRule -ServiceType Blob -CorsRules $corsRules -Context $context\n"
  },
  {
    "path": "dev/setup_secrets.ps1",
    "content": "#!/usr/bin/env pwsh\n# Helper script for applying the same user secrets to each project\nparam (\n    [switch]$clear,\n    [Parameter(ValueFromRemainingArguments = $true, Position = 1)]\n    $cmdArgs\n)\n\nif (!(Test-Path \"secrets.json\")) {\n    Write-Warning \"No secrets.json file found, please copy and modify the provided example\";\n    exit;\n}\n\nif ($clear -eq $true) {\n    Write-Output \"Deleting all existing user secrets\"\n}\n\n$projects = @{\n    Admin            = \"../src/Admin\"\n    Api              = \"../src/Api\"\n    Billing          = \"../src/Billing\"\n    Events           = \"../src/Events\"\n    EventsProcessor  = \"../src/EventsProcessor\"\n    Icons            = \"../src/Icons\"\n    Identity         = \"../src/Identity\"\n    Notifications    = \"../src/Notifications\"\n    Sso              = \"../bitwarden_license/src/Sso\"\n    Scim             = \"../bitwarden_license/src/Scim\"\n    IntegrationTests = \"../test/Infrastructure.IntegrationTest\"\n    SeederApi        = \"../util/SeederApi\"\n    SeederUtility    = \"../util/SeederUtility\"\n}\n\nforeach ($key in $projects.keys) {\n    if ($clear -eq $true) {\n        dotnet user-secrets clear -p $projects[$key]\n    }\n    $output = Get-Content secrets.json | & dotnet user-secrets set -p $projects[$key]\n    Write-Output \"$output - $key\"\n}\n"
  },
  {
    "path": "dev/verify_migrations.ps1",
    "content": "#!/usr/bin/env pwsh\n\n<#\n.SYNOPSIS\n    Validates that new database migration files follow naming conventions and chronological order.\n\n.DESCRIPTION\n    This script validates migration files to ensure:\n\n    For SQL migrations in util/Migrator/DbScripts/:\n    1. New migrations follow the naming format: YYYY-MM-DD_NN_Description.sql\n    2. New migrations are chronologically ordered (filename sorts after existing migrations)\n    3. Dates use leading zeros (e.g., 2025-01-05, not 2025-1-5)\n    4. A 2-digit sequence number is included (e.g., _00, _01)\n\n    For Entity Framework migrations in util/MySqlMigrations, util/PostgresMigrations, util/SqliteMigrations:\n    1. New migrations follow the naming format: YYYYMMDDHHMMSS_Description.cs\n    2. Each migration has both .cs and .Designer.cs files\n    3. New migrations are chronologically ordered (timestamp sorts after existing migrations)\n\n.PARAMETER BaseRef\n    The base git reference to compare against (e.g., 'main', 'HEAD~1')\n\n.PARAMETER CurrentRef\n    The current git reference (defaults to 'HEAD')\n\n.EXAMPLE\n    # For pull requests - compare against main branch\n    .\\verify_migrations.ps1 -BaseRef main\n\n.EXAMPLE\n    # For pushes - compare against previous commit\n    .\\verify_migrations.ps1 -BaseRef HEAD~1\n#>\n\nparam(\n    [Parameter(Mandatory = $true)]\n    [string]$BaseRef,\n\n    [Parameter(Mandatory = $false)]\n    [string]$CurrentRef = \"HEAD\"\n)\n\n# Use invariant culture for consistent string comparison\n[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::InvariantCulture\n\n$migrationPath = \"util/Migrator/DbScripts\"\n\n# Get list of migrations from base reference\ntry {\n    $baseMigrations = git ls-tree -r --name-only $BaseRef -- \"$migrationPath/\" 2>$null | Where-Object { $_ -like \"*.sql\" } | Sort-Object\n    if ($LASTEXITCODE -ne 0) {\n        Write-Host \"Warning: Could not retrieve migrations from base reference '$BaseRef'\"\n        $baseMigrations = @()\n    }\n}\ncatch {\n    Write-Host \"Warning: Could not retrieve migrations from base reference '$BaseRef'\"\n    $baseMigrations = @()\n}\n\n# Get list of migrations from current reference\n$currentMigrations = git ls-tree -r --name-only $CurrentRef -- \"$migrationPath/\" | Where-Object { $_ -like \"*.sql\" } | Sort-Object\n\n# Find added migrations\n$addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations }\n\n$sqlValidationFailed = $false\n\nif ($addedMigrations.Count -eq 0) {\n    Write-Host \"No new SQL migration files added.\"\n    Write-Host \"\"\n}\nelse {\n    Write-Host \"New SQL migration files detected:\"\n    $addedMigrations | ForEach-Object { Write-Host \"  $_\" }\n    Write-Host \"\"\n\n    # Get the last migration from base reference\n    if ($baseMigrations.Count -eq 0) {\n        Write-Host \"No previous SQL migrations found (initial commit?). Skipping chronological validation.\"\n        Write-Host \"\"\n    }\n    else {\n        $lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)\n        Write-Host \"Last SQL migration in base reference: $lastBaseMigration\"\n        Write-Host \"\"\n\n        # Required format regex: YYYY-MM-DD_NN_Description.sql\n        $formatRegex = '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}_.+\\.sql$'\n\n        foreach ($migration in $addedMigrations) {\n            $migrationName = Split-Path -Leaf $migration\n\n            # Validate NEW migration filename format\n            if ($migrationName -notmatch $formatRegex) {\n                Write-Host \"ERROR: Migration '$migrationName' does not match required format\"\n                Write-Host \"Required format: YYYY-MM-DD_NN_Description.sql\"\n                Write-Host \"  - YYYY: 4-digit year\"\n                Write-Host \"  - MM: 2-digit month with leading zero (01-12)\"\n                Write-Host \"  - DD: 2-digit day with leading zero (01-31)\"\n                Write-Host \"  - NN: 2-digit sequence number (00, 01, 02, etc.)\"\n                Write-Host \"Example: 2025-01-15_00_MyMigration.sql\"\n                $sqlValidationFailed = $true\n                continue\n            }\n\n            # Compare migration name with last base migration (using ordinal string comparison)\n            if ([string]::CompareOrdinal($migrationName, $lastBaseMigration) -lt 0) {\n                Write-Host \"ERROR: New migration '$migrationName' is not chronologically after '$lastBaseMigration'\"\n                $sqlValidationFailed = $true\n            }\n            else {\n                Write-Host \"OK: '$migrationName' is chronologically after '$lastBaseMigration'\"\n            }\n        }\n\n        Write-Host \"\"\n    }\n\n    if ($sqlValidationFailed) {\n        Write-Host \"FAILED: One or more SQL migrations are incorrectly named or not in chronological order\"\n        Write-Host \"\"\n        Write-Host \"All new SQL migration files must:\"\n        Write-Host \"  1. Follow the naming format: YYYY-MM-DD_NN_Description.sql\"\n        Write-Host \"  2. Use leading zeros in dates (e.g., 2025-01-05, not 2025-1-5)\"\n        Write-Host \"  3. Include a 2-digit sequence number (e.g., _00, _01)\"\n        Write-Host \"  4. Have a filename that sorts after the last migration in base\"\n        Write-Host \"\"\n        Write-Host \"To fix this issue:\"\n        Write-Host \"  1. Locate your migration file(s) in util/Migrator/DbScripts/\"\n        Write-Host \"  2. Rename to follow format: YYYY-MM-DD_NN_Description.sql\"\n        Write-Host \"  3. Ensure the date is after $lastBaseMigration\"\n        Write-Host \"\"\n        Write-Host \"Example: 2025-01-15_00_AddNewFeature.sql\"\n    }\n    else {\n        Write-Host \"SUCCESS: All new SQL migrations are correctly named and in chronological order\"\n    }\n\n    Write-Host \"\"\n}\n\n# ===========================================================================================\n# Validate Entity Framework Migrations\n# ===========================================================================================\n\nWrite-Host \"===================================================================\"\nWrite-Host \"Validating Entity Framework Migrations\"\nWrite-Host \"===================================================================\"\nWrite-Host \"\"\n\n$efMigrationPaths = @(\n    @{ Path = \"util/MySqlMigrations/Migrations\"; Name = \"MySQL\" },\n    @{ Path = \"util/PostgresMigrations/Migrations\"; Name = \"Postgres\" },\n    @{ Path = \"util/SqliteMigrations/Migrations\"; Name = \"SQLite\" }\n)\n\n$efValidationFailed = $false\n\nforeach ($migrationPathInfo in $efMigrationPaths) {\n    $efPath = $migrationPathInfo.Path\n    $dbName = $migrationPathInfo.Name\n\n    Write-Host \"-------------------------------------------------------------------\"\n    Write-Host \"Checking $dbName EF migrations in $efPath\"\n    Write-Host \"-------------------------------------------------------------------\"\n    Write-Host \"\"\n\n    # Get list of migrations from base reference\n    try {\n        $baseMigrations = git ls-tree -r --name-only $BaseRef -- \"$efPath/\" 2>$null | Where-Object { $_ -like \"*.cs\" -and $_ -notlike \"*DatabaseContextModelSnapshot.cs\" } | Sort-Object\n        if ($LASTEXITCODE -ne 0) {\n            Write-Host \"Warning: Could not retrieve $dbName migrations from base reference '$BaseRef'\"\n            $baseMigrations = @()\n        }\n    }\n    catch {\n        Write-Host \"Warning: Could not retrieve $dbName migrations from base reference '$BaseRef'\"\n        $baseMigrations = @()\n    }\n\n    # Get list of migrations from current reference\n    $currentMigrations = git ls-tree -r --name-only $CurrentRef -- \"$efPath/\" | Where-Object { $_ -like \"*.cs\" -and $_ -notlike \"*DatabaseContextModelSnapshot.cs\" } | Sort-Object\n\n    # Find added migrations\n    $addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations }\n\n    if ($addedMigrations.Count -eq 0) {\n        Write-Host \"No new $dbName EF migration files added.\"\n        Write-Host \"\"\n        continue\n    }\n\n    Write-Host \"New $dbName EF migration files detected:\"\n    $addedMigrations | ForEach-Object { Write-Host \"  $_\" }\n    Write-Host \"\"\n\n    # Get the last migration from base reference\n    if ($baseMigrations.Count -eq 0) {\n        Write-Host \"No previous $dbName migrations found. Skipping chronological validation.\"\n        Write-Host \"\"\n    }\n    else {\n        $lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)\n        Write-Host \"Last $dbName migration in base reference: $lastBaseMigration\"\n        Write-Host \"\"\n    }\n\n    # Required format regex: YYYYMMDDHHMMSS_Description.cs or YYYYMMDDHHMMSS_Description.Designer.cs\n    $efFormatRegex = '^[0-9]{14}_.+\\.cs$'\n\n    # Group migrations by base name (without .Designer.cs suffix)\n    $migrationGroups = @{}\n    $unmatchedFiles = @()\n\n    foreach ($migration in $addedMigrations) {\n        $migrationName = Split-Path -Leaf $migration\n\n        # Extract base name (remove .Designer.cs or .cs)\n        if ($migrationName -match '^([0-9]{14}_.+?)(?:\\.Designer)?\\.cs$') {\n            $baseName = $matches[1]\n            if (-not $migrationGroups.ContainsKey($baseName)) {\n                $migrationGroups[$baseName] = @()\n            }\n            $migrationGroups[$baseName] += $migrationName\n        }\n        else {\n            # Track files that don't match the expected pattern\n            $unmatchedFiles += $migrationName\n        }\n    }\n\n    # Flag any files that don't match the expected pattern\n    if ($unmatchedFiles.Count -gt 0) {\n        Write-Host \"ERROR: The following migration files do not match the required format:\"\n        foreach ($unmatchedFile in $unmatchedFiles) {\n            Write-Host \"  - $unmatchedFile\"\n        }\n        Write-Host \"\"\n        Write-Host \"Required format: YYYYMMDDHHMMSS_Description.cs or YYYYMMDDHHMMSS_Description.Designer.cs\"\n        Write-Host \"  - YYYYMMDDHHMMSS: 14-digit timestamp (Year, Month, Day, Hour, Minute, Second)\"\n        Write-Host \"  - Description: Descriptive name using PascalCase\"\n        Write-Host \"Example: 20250115120000_AddNewFeature.cs and 20250115120000_AddNewFeature.Designer.cs\"\n        Write-Host \"\"\n        $efValidationFailed = $true\n    }\n\n    foreach ($baseName in $migrationGroups.Keys | Sort-Object) {\n        $files = $migrationGroups[$baseName]\n\n        # Validate format\n        $mainFile = \"$baseName.cs\"\n        $designerFile = \"$baseName.Designer.cs\"\n\n        if ($mainFile -notmatch $efFormatRegex) {\n            Write-Host \"ERROR: Migration '$mainFile' does not match required format\"\n            Write-Host \"Required format: YYYYMMDDHHMMSS_Description.cs\"\n            Write-Host \"  - YYYYMMDDHHMMSS: 14-digit timestamp (Year, Month, Day, Hour, Minute, Second)\"\n            Write-Host \"Example: 20250115120000_AddNewFeature.cs\"\n            $efValidationFailed = $true\n            continue\n        }\n\n        # Check that both .cs and .Designer.cs files exist\n        $hasCsFile = $files -contains $mainFile\n        $hasDesignerFile = $files -contains $designerFile\n\n        if (-not $hasCsFile) {\n            Write-Host \"ERROR: Missing main migration file: $mainFile\"\n            $efValidationFailed = $true\n        }\n\n        if (-not $hasDesignerFile) {\n            Write-Host \"ERROR: Missing designer file: $designerFile\"\n            Write-Host \"Each EF migration must have both a .cs and .Designer.cs file\"\n            $efValidationFailed = $true\n        }\n\n        if (-not $hasCsFile -or -not $hasDesignerFile) {\n            continue\n        }\n\n        # Compare migration timestamp with last base migration (using ordinal string comparison)\n        if ($baseMigrations.Count -gt 0) {\n            if ([string]::CompareOrdinal($mainFile, $lastBaseMigration) -lt 0) {\n                Write-Host \"ERROR: New migration '$mainFile' is not chronologically after '$lastBaseMigration'\"\n                $efValidationFailed = $true\n            }\n            else {\n                Write-Host \"OK: '$mainFile' is chronologically after '$lastBaseMigration'\"\n            }\n        }\n        else {\n            Write-Host \"OK: '$mainFile' (no previous migrations to compare)\"\n        }\n    }\n\n    Write-Host \"\"\n}\n\nif ($efValidationFailed) {\n    Write-Host \"FAILED: One or more EF migrations are incorrectly named or not in chronological order\"\n    Write-Host \"\"\n    Write-Host \"All new EF migration files must:\"\n    Write-Host \"  1. Follow the naming format: YYYYMMDDHHMMSS_Description.cs\"\n    Write-Host \"  2. Include both .cs and .Designer.cs files\"\n    Write-Host \"  3. Have a timestamp that sorts after the last migration in base\"\n    Write-Host \"\"\n    Write-Host \"To fix this issue:\"\n    Write-Host \"  1. Locate your migration file(s) in the respective Migrations directory\"\n    Write-Host \"  2. Ensure both .cs and .Designer.cs files exist\"\n    Write-Host \"  3. Rename to follow format: YYYYMMDDHHMMSS_Description.cs\"\n    Write-Host \"  4. Ensure the timestamp is after the last migration\"\n    Write-Host \"\"\n    Write-Host \"Example: 20250115120000_AddNewFeature.cs and 20250115120000_AddNewFeature.Designer.cs\"\n}\nelse {\n    Write-Host \"SUCCESS: All new EF migrations are correctly named and in chronological order\"\n}\n\nWrite-Host \"\"\nWrite-Host \"===================================================================\"\nWrite-Host \"Validation Summary\"\nWrite-Host \"===================================================================\"\n\nif ($sqlValidationFailed -or $efValidationFailed) {\n    if ($sqlValidationFailed) {\n        Write-Host \"❌ SQL migrations validation FAILED\"\n    }\n    else {\n        Write-Host \"✓ SQL migrations validation PASSED\"\n    }\n\n    if ($efValidationFailed) {\n        Write-Host \"❌ EF migrations validation FAILED\"\n    }\n    else {\n        Write-Host \"✓ EF migrations validation PASSED\"\n    }\n\n    Write-Host \"\"\n    Write-Host \"OVERALL RESULT: FAILED\"\n    exit 1\n}\nelse {\n    Write-Host \"✓ SQL migrations validation PASSED\"\n    Write-Host \"✓ EF migrations validation PASSED\"\n    Write-Host \"\"\n    Write-Host \"OVERALL RESULT: SUCCESS\"\n    exit 0\n}\n"
  },
  {
    "path": "global.json",
    "content": "{\n  \"sdk\": {\n    \"version\": \"8.0.100\",\n    \"rollForward\": \"latestFeature\"\n  },\n  \"msbuild-sdks\": {\n    \"Microsoft.Build.Traversal\": \"4.1.0\",\n    \"Microsoft.Build.Sql\": \"1.0.0\",\n    \"Bitwarden.Server.Sdk\": \"1.5.1\"\n  }\n}\n"
  },
  {
    "path": "perf/MicroBenchmarks/Core/EncryptedStringAttributeTests.cs",
    "content": "﻿using BenchmarkDotNet.Attributes;\nusing Bit.Core.Utilities;\n\nnamespace Bit.MicroBenchmarks.Core;\n\n[MemoryDiagnoser]\npublic class EncryptedStringAttributeTests\n{\n    private readonly EncryptedStringAttribute _encryptedStringAttribute;\n    public EncryptedStringAttributeTests()\n    {\n        _encryptedStringAttribute = new EncryptedStringAttribute();\n    }\n\n    public const string Short = @\"2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\";\n    public const string Long = @\"2.KllcSp3E124kblnMlCNQuQ==|G0jvyocNBOEzCyJ/f951Pz348MPi2RLzmsbfOAOtszi5L9WAkWe4L5T2gJukRMgDSvdE76MVUqREsgQZME52t8JFpVzsQU7Ee56szoJu\\u002BAHHQkBNd5J2WirgprDRmIGiSdbvv2GoCszr1b2Ox/Pc\\u002B9wjjzMjR6ZA3xhvy1UPyFdT5edQvIFVRSOEG7ivtV7RmGxiq7buP/g\\u002BL/epeuiljkjU7KU8JbZRKaxhGLSiofEMlBtaTZtTGh3VKAHslUgPnRIRlHpjPDaT1Yw3la4YHbEOs4Y/XJ5sI4q5xXlK60gMgAlvlyH0mspezaBy367WBLu\\u002BWC/4GqGBudhftSOED/aIS4PoCUvMdftwHunXjE2s5zDUwT66w30lvEvJbs7m/36fKAj9ujqOv9vEm1Ukjxhzk\\u002BHHkLUcjrD57Zfn0P\\u002BzK6wbneYBORObF70gUhcINv64GqgLFlEUecmKjK0ZgcmTXp8XuG2ZfplXAN1y8gySM3UAREhFj4oQtZq/QB64SlnOTAPkRjqy0khqWCmySqSSqKYCnzTfE7RUhR6FAgpWTo9qHZHkwNVk2EBWvMN0UctjmYIpoUBtqKPF2Oidhjnck\\u002BqCa7jGIqGetERq9Cdg\\u002Btk6bvzLexj/bVEJpiK53ghrDJKs5Ncox7cmLjIfoL5cb3ZUgiV3g\\u002Bzqpq2RWn16d85Edmir4zlJPSA77BEGhFzZET3PyYqdWWcaAlDQbqeQ39zyuKUzRI0JH6yXsfCcTqZmsQ9Mj5OpqQdcgVlMXyvsY/mCsocpGKA0\\u002BcQGQPZGgZ3cDcN13K4MTjiZ8BO9P7aLvgk5k5a3GFFsYVhe2lgbPeoASDHY48AX9yTnPTXGCHAB\\u002BHiybFRyIabXhSnU491vRxPyCsEBMbL/GTUr0oulzixdOMYCKBYw3WcLGuA2gG1JyAl\\u002BEzS/iU1G97bWvi8yMG6QsDhiAvgAt3BjU1C7kgHseM\\u002BZHdpe1cwyFV\\u002BzSfS0Hn5iVYcJVnkO7JiiTaMSQEjIk0q54stRGJ\\u002BHC81F2kaf\\u002Bi4aQ4rpq82yAJJMoGsdSyzeC8P2jR4HpsfJroJRYAPHUl8CIe0ZeWdvWjFwKGqxmPeFU2lazMEyyMZGFB1/8zP4Gxu2KarubWG1NRQY1sGCnH1\\u002BvBsQrKtyBxyuwshNrUCN9UifOZDmYZ2FPHHdnNdGiaHyQVSDYqx/EpWkgkNl6h1Itpkb7sW/ByAu2EhHUJODfciYgyF4CsCs2iNboZNoMctY5H0VT704tt3AuS369PMWJzwKum\\u002BHUrpLEyav0SUUSUlhWUvkWfW1O/Q3bADW72IXWS\\u002BHUZ0E96tjpUDv60V7X0qLOCcjlhxRgGwPVw1V4FuxhmkX8UxSwlU39/aQUj8gpGr8Um8LWxIYw/mkV1ItTPh4IxpXxSnuGAIBjEBlRlqedFLF97tVDzYLddEh6SlIEh6ppdpDS6ghSpiylyh4JsaXtp1QGpoPUfkwcl1jWrRarj5G2hbQW3gkuug8cfGB0zHxfLwmKSHswFPa1Ne2kqf7jlO19ckIxqmHpbrn9myVcnqPQLhSxtzIjJUN6HG7xQdJ7Q7Tumj8gN9dN06IUEfhoYhUxcabTqm9jTspVQ7iXsvml3YeF8qJX/d\\u002BnSUqcOmOod6/B1k54Dd9mhosi0So9XTGuVmgKz82r185wTx7mVgcaKPQZXIVxIPqxtsk1UI6IK5wpyWhB77nS3L83KGdxpdRTbPAhAj8tIKDrELzr3u5WytStGeH8melF7KnIzWmAs2vUJIlByFxgjm26u0C4Eid0DQVJJ4xWp5yt62WodZ80B3dUyjXUUuTyyVL8Y26zRZNuF/3G\\u002BKc\\u002BkN/lja44BPG5/FoDofgto3IlWvYzkoKB8oXhhL58Rc8fNr5LZIz\\u002BTrweFXO2eoSP1xIcEuq2J\\u002BZGLHElEmkwQA1u3UE\\u002BySsElNAeWlx\\u002BkQ0f\\u002B0AqZzhei1PJ0VHqRXvwxYTqkeBQA2/95UTyKv15R/cbHVHexttqrc40oRwp7Cl\\u002BzIJcRvKSUdfXa6MiQ13HvBpiG\\u002BBoyvC2RHLlg\\u002BwToSmCZFoPDt5wEP9TOMmG2HbCab1fLGkfQ2Sov1cRnPWt6SgtZFsIRyxxOglr44O\\u002B56\\u002B9QH7KmpEHjXANdOhYnNsw47TQEoMy9v8r23CUu\\u002BX2yUcLxplTDxQXb5pNo5PXH0DlhIu3SGp/xu5tU5I7UXNdO85I9za/1wp/ylcl1PjXXnKx0E5FvXsOdUndgSE8TNxD8u6hfrbizTuAnFiJWCQzbDBMCT1SLggoq03iN\\u002BKTSEWoh4l28GF\\u002BwJsVU5PygNLuZVaDjlHVgeU5u5AhPO/0clIsFvJPaJ5WhstDlxGRXq0\\u002BRBrx1ikMDXwFLfdbtj/U7O8cPCeU0UIJRzjjd9mG1Uyk2UNccJHzXt7RWKk7pCuNhG7cVOo55cICvndT1n6hwTC6mpXXv1pHiB8Ddg1csw7p2E6BVj0YhV1vTWTlKHYrNjv2F2f1NNPtRsCzOh2rOd7UCtV5wfB92PorADP9r3FNAQupVwCVkYymMPfaOPm5lpvT1WdORQZbKQRc10OzTnnOtFNaKWI4DffPuIR93wHj7JByG7MGmRkW1sAnmOuif24sMOYULcjTS0SdvUQhvvkCMfl/rtsFYDXnnCrzhcWPQ\\u002BS2nIKt89h5rtjk73/rOmvAnIC8f\\u002Bl84yM54Ifm2jGC8H1VZS6HmEFAJmciKsPHaBEGRxBL9FxTOzPM3KKXEUFANvm0lGQrzrpl4PlhauSf04BwxM9s9iikCVtEVA1E9e4dNs/MZmwNzwQLN36ZkABdu8ZVzfW4CpXO0aA3ZXbtNQuEpDWvpIsMTZUL9IYcLFtLI9ITuEhrluPVX6vnzRghM/DmSLr5zH\\u002BDat9saZxvzpDpq4JW\\u002BXcJ\\u002Bk1D469N4grVNvbcymcQ8ZinJbyOOQS9t7H5hOG7zWbYb/paRQChCeCqGindCxnxjymGRnbShGtKzDq8GZHKDkkS0pm6g3laV6pPBlF0FMwzTm6IDFrWUXOjRgkPTr46t8PRkdEMKXHSNWMn4QiWC5\\u002BiKUEvqt\\u002BjeY1RzKQB2gd1xBfI9hLs2L\\u002BK7S3GOblvM/R1FAK3Rnd3oZ41hZ2wHSH4x1mvLjDWVWmLOQt9pzn3DSX7\\u002BJqhnWtF6nYNm\\u002BIyu5bZRtyBz1tpo1FezJxx1o2r1QTi3M1FTLLndAaxtXCwzcBLBxG7PZKI0rfVExcK3FYO2bUYZS/HpALLVkuWg\\u002BTuaZPmkDyUkxSsXIU9ERIVOz0\\u002BYgsCQrBj/Z/xY38jy5jCVvXAbmAwv1ZsyZOp657LXHUTz6EmQAVWDuMZfX97WyIn5stWtT\\u002B8Ppeda82TecJBVVKaDduMwvBb3jRNTtCappRoQm2ea1rBjvpWXEtGsAZcIu74Exz\\u002BmS\\u002BpKEOR0JAElqMIRLTNmCXVSXEjF5Oo1s3vIR4IZ2XGm9EF0Lm9Gld5OR1ganZR1AyPJpHOGclq2FGKSlamwyh6TzZDUnnExsyaQjr9Dkuy5mQdIeg8RdsjjvMqeCVZXV/jIo//dwkGm9nCVLud9jsKOdyl5ELhKyNHryXkyZVngF7qL\\u002BWpoEQkW9G5j5aO5FrIt\\u002B3fgqDr5KP/wJkdfKQM0GG0FI989t6CzzajfGxWgn\\u002B1nUhkjOyHBee6zBtoT8GA/K0g8SvrKCJQeaic1UFC\\u002BXZ1UcwbFZK43pc9iYyoxMboxi\\u002Bq5ZX0nQXseiv8OsijrFmm5d/0R\\u002BRpSLeHIz9uetBKZpSbLJnCNvugTVlFmYgocPC1zqWAj2MrqdYaf5Vzy0uXj5gKMZxAD9ZYeTatiL5C/GuMsaidvb8bh1E7XV5apf3lgLAKxHnqjE5CJzU9I/itBTHXysMeCZrkhDeAS0UhtRyNiMoahKBc6qFkiphnPi1kM0x0lzZRRnBz8cgkvdEt769c3BYqHa7gWC3DlDjaHKuvqSd1t4eDjSlU2PeI7RYwJwBfF5LdPLevsAkyvGCIHKmfjX38SeSrlgL9E2JsRQpeypKWiVpRunM9he/9jADAVFwEpck3bBz/ANo2c6gX3/haAw1xXunkq6\\u002BIp80XVLeoys44vFv3QPvZ6GZ5QCSJ/AP42fSdENTYkB7MhBwMgLmcyATVFYA16yS7q8dIAowO5GL3/vCTe4\\u002B47Sg6U9vgclFw1T55oZ/apQ5bdPCZ7N4M2nD5DfI74XuoQPfrDkXIhCP\\u002BHh/v8BV9t2mPH0LyXGviwl4ewrtrtsOCV3H4/KsxiMPVkTFofHjjXU30WkcBaCB985p1oPWii4d4QJvnubN3z4\\u002B51mhYPMCs7Uu05we9zEDH0tPfWC2Jm9C7Hv\\u002BT5LgWFOV/d7\\u002BYR0AajpF3xKWulZu\\u002BwDXruRAHhLqCwsGxq9GlLal8DURCGxQG6Keyx/PTv9L6yEZmMIlNdP3xNEOPaDNwNIWczPC6CkuPbufgsFxn9I3krlZS2x/5/ZuIdoZu9CqPr5af7TIqsXWdczEQZ7NhefA0\\u002BXeImBoflGx2I45UG8CeiF0FMnX16rzpF48rx/YZkl/PFwQYcN\\u002BVrQmXrqD6eSzyHvRWSi8KQP7te2LqBFw1p5dxgREbCTLlcvQckcGgiyctDjS1wYe1VaagedfrsQpYXdQkOxhL9aE6r1\\u002Bj8eHenC6ezxMc6LnsLog2wDMAXlzmq9KkladqyltzL2sgTyg7R57agFrjJtGQfLb0IY0O8vhkwqq2KYhWJ7jOJA\\u002BR0k62HZEeNMsreNBNO5l0QCQ0\\u002BUGKpEtvX/F5DNj07Swz\\u002BoiHWq2933yQ4pzXnYnIfNgb8Ni5Js/GMn5HNJFvsNgYQ/xV1iKnIH43qDoyw55t22Sg4kcHhQBSBSWEZ7u/iVHCSN3hW7ma5Eabbk15/Jg4TurDpthtufZqZ2ZAiOWBaiOUC/OHUI8PjquycGpOGrTauVh\\u002BCiS47S5nMPrF6ROVTT\\u002BjrfaC9cw0\\u002BIf0wEMG/W/p57n5YE7JnLr5kwKEDHCk3mMdxqhYzIoRBiKndblcM1\\u002B7Glm5dN78R4mgt2QUGERnVkP\\u002BoDxszTshjeZ0EX1l9zuF3V2zt1WzzDH8BiY/zVrhuJsRGSwLsVCpBKZArSfa1cLIufuF9cj5QtYu9NyKi2aVDILdN1fsFqaZ9i5Zsg8ohsLHzYQtY1RZVhNrmI=|MiBu248yFOf9eakCDaO\\u002BK7aU2gu/T9R8YSbf7kkHuXg=\";\n\n    [Params(Short, Long)]\n    public string EncryptedString { get; set; } = null!;\n\n    [Benchmark]\n    public bool IsValid() => _encryptedStringAttribute.IsValid(EncryptedString);\n}\n"
  },
  {
    "path": "perf/MicroBenchmarks/Identity/IdentityServer/PersistedGrantStoreTests.cs",
    "content": "﻿using BenchmarkDotNet.Attributes;\nusing Bit.Identity.IdentityServer;\nusing Bit.Infrastructure.Dapper.Auth.Repositories;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Stores;\n\nnamespace Bit.MicroBenchmarks.Identity.IdentityServer;\n\n[MemoryDiagnoser]\npublic class PersistedGrantStoreTests\n{\n    const string SQL = nameof(SQL);\n    const string Cosmos = nameof(Cosmos);\n\n    private readonly IPersistedGrantStore _sqlGrantStore;\n    private readonly IPersistedGrantStore _cosmosGrantStore;\n    private readonly PersistedGrant _updateGrant;\n\n    private IPersistedGrantStore _grantStore = null!;\n\n    // 1) \"ConsumedTime\"\n    // 2) \"\"\n    // 3) \"Description\"\n    // 4) \"\"\n    // 5) \"SubjectId\"\n    // 6) \"97f31e32-6e44-407f-b8ba-b04c00f51b41\"\n    // 7) \"CreationTime\"\n    // 8) \"638350407400000000\"\n    // 9) \"Data\"\n    // 10) \"{\\\"CreationTime\\\":\\\"2023-11-08T11:45:40Z\\\",\\\"Lifetime\\\":2592001,\\\"ConsumedTime\\\":null,\\\"AccessToken\\\":{\\\"AllowedSigningAlgorithms\\\":[],\\\"Confirmation\\\":null,\\\"Audiences\\\":[],\\\"Issuer\\\":\\\"http://localhost\\\",\\\"CreationTime\\\":\\\"2023-11-08T11:45:40Z\\\",\\\"Lifetime\\\":3600,\\\"Type\\\":\\\"access_token\\\",\\\"ClientId\\\":\\\"web\\\",\\\"AccessTokenType\\\":0,\\\"Description\\\":null,\\\"Claims\\\":[{\\\"Type\\\":\\\"client_id\\\",\\\"Value\\\":\\\"web\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"scope\\\",\\\"Value\\\":\\\"api\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"scope\\\",\\\"Value\\\":\\\"offline_access\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"sub\\\",\\\"Value\\\":\\\"97f31e32-6e44-407f-b8ba-b04c00f51b41\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"auth_time\\\",\\\"Value\\\":\\\"1699443940\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#integer64\\\"},{\\\"Type\\\":\\\"idp\\\",\\\"Value\\\":\\\"bitwarden\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"amr\\\",\\\"Value\\\":\\\"Application\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"premium\\\",\\\"Value\\\":\\\"false\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#boolean\\\"},{\\\"Type\\\":\\\"email\\\",\\\"Value\\\":\\\"jbaur+test@bitwarden.com\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"email_verified\\\",\\\"Value\\\":\\\"false\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#boolean\\\"},{\\\"Type\\\":\\\"sstamp\\\",\\\"Value\\\":\\\"a4f2e0f3-e9f8-4014-b94e-b761d446a34b\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"name\\\",\\\"Value\\\":\\\"Justin Test\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"orgowner\\\",\\\"Value\\\":\\\"8ff8fefb-b035-436b-a25c-b04c00e30351\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"accesssecretsmanager\\\",\\\"Value\\\":\\\"8ff8fefb-b035-436b-a25c-b04c00e30351\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"device\\\",\\\"Value\\\":\\\"64b49c58-7768-4c30-8396-f851176daca6\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"jti\\\",\\\"Value\\\":\\\"CE008210A8276DAB966D9C2607533E0C\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"iat\\\",\\\"Value\\\":\\\"1699443940\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#integer64\\\"}],\\\"Version\\\":4},\\\"Version\\\":4}\"\n    // 11) \"Type\"\n    // 12) \"refresh_token\"\n    // 13) \"SessionId\"\n    // 14) \"\"\n    // 15) \"ClientId\"\n    // 16) \"web\"\n\n    public PersistedGrantStoreTests()\n    {\n        var sqlConnectionString = \"YOUR CONNECTION STRING HERE\";\n        _sqlGrantStore = new PersistedGrantStore(\n            new GrantRepository(\n                sqlConnectionString,\n                sqlConnectionString\n            ),\n            g => new Bit.Core.Auth.Entities.Grant(g)\n        );\n\n        var cosmosConnectionString = \"YOUR CONNECTION STRING HERE\";\n        _cosmosGrantStore = new PersistedGrantStore(\n            new Bit.Core.Auth.Repositories.Cosmos.GrantRepository(cosmosConnectionString),\n            g => new Bit.Core.Auth.Models.Data.GrantItem(g)\n        );\n\n        var creationTime = new DateTime(638350407400000000, DateTimeKind.Utc);\n        _updateGrant = new PersistedGrant\n        {\n            Key = \"i11JLqd7PE1yQltB2o5tRpfbMkpDPr+3w0Lc2Hx7kfE=\",\n            ConsumedTime = null,\n            Description = null,\n            SubjectId = \"97f31e32-6e44-407f-b8ba-b04c00f51b41\",\n            CreationTime = creationTime,\n            Data = \"{\\\"CreationTime\\\":\\\"2023-11-08T11:45:40Z\\\",\\\"Lifetime\\\":2592001,\\\"ConsumedTime\\\":null,\\\"AccessToken\\\":{\\\"AllowedSigningAlgorithms\\\":[],\\\"Confirmation\\\":null,\\\"Audiences\\\":[],\\\"Issuer\\\":\\\"http://localhost\\\",\\\"CreationTime\\\":\\\"2023-11-08T11:45:40Z\\\",\\\"Lifetime\\\":3600,\\\"Type\\\":\\\"access_token\\\",\\\"ClientId\\\":\\\"web\\\",\\\"AccessTokenType\\\":0,\\\"Description\\\":null,\\\"Claims\\\":[{\\\"Type\\\":\\\"client_id\\\",\\\"Value\\\":\\\"web\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"scope\\\",\\\"Value\\\":\\\"api\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"scope\\\",\\\"Value\\\":\\\"offline_access\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"sub\\\",\\\"Value\\\":\\\"97f31e32-6e44-407f-b8ba-b04c00f51b41\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"auth_time\\\",\\\"Value\\\":\\\"1699443940\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#integer64\\\"},{\\\"Type\\\":\\\"idp\\\",\\\"Value\\\":\\\"bitwarden\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"amr\\\",\\\"Value\\\":\\\"Application\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"premium\\\",\\\"Value\\\":\\\"false\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#boolean\\\"},{\\\"Type\\\":\\\"email\\\",\\\"Value\\\":\\\"jbaur+test@bitwarden.com\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"email_verified\\\",\\\"Value\\\":\\\"false\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#boolean\\\"},{\\\"Type\\\":\\\"sstamp\\\",\\\"Value\\\":\\\"a4f2e0f3-e9f8-4014-b94e-b761d446a34b\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"name\\\",\\\"Value\\\":\\\"Justin Test\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"orgowner\\\",\\\"Value\\\":\\\"8ff8fefb-b035-436b-a25c-b04c00e30351\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"accesssecretsmanager\\\",\\\"Value\\\":\\\"8ff8fefb-b035-436b-a25c-b04c00e30351\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"device\\\",\\\"Value\\\":\\\"64b49c58-7768-4c30-8396-f851176daca6\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"jti\\\",\\\"Value\\\":\\\"CE008210A8276DAB966D9C2607533E0C\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#string\\\"},{\\\"Type\\\":\\\"iat\\\",\\\"Value\\\":\\\"1699443940\\\",\\\"ValueType\\\":\\\"http://www.w3.org/2001/XMLSchema#integer64\\\"}],\\\"Version\\\":4},\\\"Version\\\":4}\",\n            Type = \"refresh_token\",\n            SessionId = null,\n            ClientId = \"web\",\n            Expiration = creationTime.AddHours(1),\n        };\n    }\n\n    [Params(SQL, Cosmos)]\n    public string StoreType { get; set; } = null!;\n\n    [GlobalSetup]\n    public void Setup()\n    {\n        if (StoreType == SQL)\n        {\n            _grantStore = _sqlGrantStore;\n        }\n        else if (StoreType == Cosmos)\n        {\n            _grantStore = _cosmosGrantStore;\n        }\n        else\n        {\n            throw new InvalidProgramException();\n        }\n    }\n\n    [Benchmark]\n    public async Task StoreAsync()\n    {\n        await _grantStore.StoreAsync(_updateGrant);\n    }\n}\n"
  },
  {
    "path": "perf/MicroBenchmarks/Identity/IdentityServer/StaticClientStoreTests.cs",
    "content": "﻿using BenchmarkDotNet.Attributes;\nusing Bit.Core.Settings;\nusing Bit.Identity.IdentityServer;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.MicroBenchmarks.Identity.IdentityServer;\n\npublic class StaticClientStoreTests\n{\n    private readonly StaticClientStore _store;\n\n    public StaticClientStoreTests()\n    {\n        _store = new StaticClientStore(new GlobalSettings());\n    }\n\n    [Params(\"mobile\", \"connector\", \"invalid\", \"a_much_longer_invalid_value_that_i_am_making_up\", \"WEB\", \"\")]\n    public string ClientId { get; set; } = null!;\n\n    [Benchmark]\n    public Client? TryGetValue()\n    {\n        return _store.Clients.TryGetValue(ClientId, out var client)\n          ? client\n          : null;\n    }\n}\n"
  },
  {
    "path": "perf/MicroBenchmarks/MicroBenchmarks.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"BenchmarkDotNet\" Version=\"0.15.3\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Identity\\Identity.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "perf/MicroBenchmarks/Program.cs",
    "content": "﻿using BenchmarkDotNet.Running;\n\nBenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);\n"
  },
  {
    "path": "perf/load/config.js",
    "content": "import http from \"k6/http\";\nimport { check, fail } from \"k6\";\nimport { authenticate } from \"./helpers/auth.js\";\n\nconst IDENTITY_URL = __ENV.IDENTITY_URL;\nconst API_URL = __ENV.API_URL;\nconst CLIENT_ID = __ENV.CLIENT_ID;\nconst AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;\nconst AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;\n\nexport const options = {\n  scenarios: {\n    constant_load: {\n      executor: \"constant-arrival-rate\",\n      rate: 1,\n      timeUnit: \"1s\", // 1 request / second\n      duration: \"10m\",\n      preAllocatedVUs: 5,\n    },\n    ramping_load: {\n      executor: \"ramping-arrival-rate\",\n      startRate: 60,\n      timeUnit: \"1m\", // 1 request / second to start\n      stages: [\n        { duration: \"30s\", target: 60 },\n        { duration: \"2m\", target: 150 },\n        { duration: \"1m\", target: 90 },\n        { duration: \"2m\", target: 200 },\n        { duration: \"2m\", target: 120 },\n        { duration: \"1m\", target: 180 },\n        { duration: \"30s\", target: 250 },\n        { duration: \"30s\", target: 90 },\n        { duration: \"30s\", target: 0 },\n      ],\n      preAllocatedVUs: 40,\n    },\n  },\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n    http_req_duration: [\"p(95)<350\"],\n  },\n};\n\nexport function setup() {\n  return authenticate(IDENTITY_URL, CLIENT_ID, AUTH_USERNAME, AUTH_PASSWORD);\n}\n\nexport default function (data) {\n  const params = {\n    headers: {\n      Accept: \"application/json\",\n      Authorization: `Bearer ${data.access_token}`,\n      \"X-ClientId\": CLIENT_ID,\n    },\n    tags: { name: \"Config\" },\n  };\n\n  const res = http.get(`${API_URL}/config`, params);\n  if (\n    !check(res, {\n      \"config status is 200\": (r) => r.status === 200,\n    })\n  ) {\n    fail(\"config status code was *not* 200\");\n  }\n\n  const json = res.json();\n\n  check(json, {\n    \"config version is available\": (j) => j.version !== \"\",\n  });\n}\n"
  },
  {
    "path": "perf/load/groups.js",
    "content": "import http from \"k6/http\";\nimport { check, fail } from \"k6\";\nimport { authenticate } from \"./helpers/auth.js\";\nimport { uuidv4 } from \"https://jslib.k6.io/k6-utils/1.4.0/index.js\";\n\nconst IDENTITY_URL = __ENV.IDENTITY_URL;\nconst API_URL = __ENV.API_URL;\nconst CLIENT_ID = __ENV.CLIENT_ID;\nconst AUTH_CLIENT_ID = __ENV.AUTH_CLIENT_ID;\nconst AUTH_CLIENT_SECRET = __ENV.AUTH_CLIENT_SECRET;\n\nexport const options = {\n  scenarios: {\n    constant_load: {\n      executor: \"constant-arrival-rate\",\n      rate: 30,\n      timeUnit: \"1m\", // 0.5 requests / second\n      duration: \"10m\",\n      preAllocatedVUs: 5,\n    },\n    ramping_load: {\n      executor: \"ramping-arrival-rate\",\n      startRate: 30,\n      timeUnit: \"1m\", // 0.5 requests / second to start\n      stages: [\n        { duration: \"30s\", target: 30 },\n        { duration: \"2m\", target: 75 },\n        { duration: \"1m\", target: 60 },\n        { duration: \"2m\", target: 100 },\n        { duration: \"2m\", target: 90 },\n        { duration: \"1m\", target: 120 },\n        { duration: \"30s\", target: 150 },\n        { duration: \"30s\", target: 60 },\n        { duration: \"30s\", target: 0 },\n      ],\n      preAllocatedVUs: 20,\n    },\n  },\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n    http_req_duration: [\"p(95)<400\"],\n  },\n};\n\nexport function setup() {\n  return authenticate(\n    IDENTITY_URL,\n    CLIENT_ID,\n    null,\n    null,\n    AUTH_CLIENT_ID,\n    AUTH_CLIENT_SECRET\n  );\n}\n\nexport default function (data) {\n  const params = {\n    headers: {\n      Accept: \"application/json\",\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${data.access_token}`,\n      \"X-ClientId\": CLIENT_ID,\n    },\n    tags: { name: \"Groups\" },\n  };\n\n  let name = `Name ${uuidv4()}`;\n  const createPayload = {\n    name: name,\n    accessAll: true,\n    externalId: `External ${uuidv4()}`,\n  };\n\n  const createRes = http.post(\n    `${API_URL}/public/groups`,\n    JSON.stringify(createPayload),\n    params\n  );\n  if (\n    !check(createRes, {\n      \"group create status is 200\": (r) => r.status === 200,\n    })\n  ) {\n    fail(\"group create status code was *not* 200\");\n  }\n\n  const createJson = createRes.json();\n\n  if (\n    !check(createJson, {\n      \"group create id is available\": (j) => j.id !== \"\",\n    })\n  ) {\n    fail(\"group create id was *not* available\");\n  }\n\n  const id = createJson.id;\n  const getRes = http.get(`${API_URL}/public/groups/${id}`, params);\n  if (\n    !check(getRes, {\n      \"group get status is 200\": (r) => r.status === 200,\n    })\n  ) {\n    fail(\"group get status code was *not* 200\");\n  }\n\n  const getJson = getRes.json();\n\n  if (\n    !check(getJson, {\n      \"group get name matches\": (j) => j.name === name,\n    })\n  ) {\n    fail(\"group get name did *not* match\");\n  }\n\n  name = `Name ${uuidv4()}`;\n  const updatePayload = {\n    name: name,\n    accessAll: createPayload.accessAll,\n    externalId: createPayload.externalId,\n  };\n\n  const updateRes = http.put(\n    `${API_URL}/public/groups/${id}`,\n    JSON.stringify(updatePayload),\n    params\n  );\n  if (\n    !check(updateRes, {\n      \"group update status is 200\": (r) => r.status === 200,\n    })\n  ) {\n    fail(\"group update status code was *not* 200\");\n  }\n\n  const deleteRes = http.del(`${API_URL}/public/groups/${id}`, null, params);\n  if (\n    !check(deleteRes, {\n      \"group delete status is 200\": (r) => r.status === 200,\n    })\n  ) {\n    fail(\"group delete status code was *not* 200\");\n  }\n}\n"
  },
  {
    "path": "perf/load/helpers/auth.js",
    "content": "import http from \"k6/http\";\nimport { check, fail } from \"k6\";\nimport encoding from \"k6/encoding\";\n\n/**\n * Authenticate using OAuth against Bitwarden\n * @function\n * @param {string} identityUrl - Identity Server URL\n * @param {string} clientHeader - X-ClientId header value\n * @param {string} username - User email (password grant)\n * @param {string} password - User password (password grant)\n * @param {string} clientId - Client ID (client credentials grant)\n * @param {string} clientSecret - Client secret (client credentials grant)\n */\nexport function authenticate(\n  identityUrl,\n  clientHeader,\n  username,\n  password,\n  clientId,\n  clientSecret\n) {\n  const url = `${identityUrl}/connect/token`;\n  const params = {\n    headers: {\n      Accept: \"application/json\",\n      \"X-ClientId\": clientHeader,\n    },\n    tags: { name: \"Login\" },\n  };\n  const payload = {\n    deviceIdentifier: \"a455f262-3d24-4bcd-b178-39dcd67d5c3f\",\n  };\n\n  if (username !== null) {\n    payload[\"scope\"] = \"api offline_access\";\n    payload[\"grant_type\"] = \"password\";\n    payload[\"client_id\"] = \"web\";\n    payload[\"deviceType\"] = \"9\";\n    payload[\"deviceName\"] = \"chrome\";\n    payload[\"username\"] = username;\n    payload[\"password\"] = password;\n  } else {\n    payload[\"scope\"] = \"api.organization\";\n    payload[\"grant_type\"] = \"client_credentials\";\n    payload[\"client_id\"] = clientId;\n    payload[\"client_secret\"] = clientSecret;\n  }\n\n  const res = http.post(url, payload, params);\n\n  if (\n    !check(res, {\n      \"login status is 200\": (r) => r.status === 200,\n    })\n  ) {\n    fail(\"login status code was *not* 200\");\n  }\n\n  const json = res.json();\n\n  if (\n    !check(json, {\n      \"login access token is available\": (j) => j.access_token !== \"\",\n    })\n  ) {\n    fail(\"login access token was *not* available\");\n  }\n\n  return json;\n}\n"
  },
  {
    "path": "perf/load/login.js",
    "content": "import { authenticate } from \"./helpers/auth.js\";\n\nconst IDENTITY_URL = __ENV.IDENTITY_URL;\nconst CLIENT_ID = __ENV.CLIENT_ID;\nconst AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;\nconst AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;\n\nexport const options = {\n  scenarios: {\n    constant_load: {\n      executor: \"constant-arrival-rate\",\n      rate: 2,\n      timeUnit: \"1s\", // 2 requests / second\n      duration: \"10m\",\n      preAllocatedVUs: 10,\n    },\n    ramping_load: {\n      executor: \"ramping-arrival-rate\",\n      startRate: 60,\n      timeUnit: \"1m\", // 1 request / second to start\n      stages: [\n        { duration: \"30s\", target: 60 },\n        { duration: \"5m\", target: 120 },\n        { duration: \"2m\", target: 150 },\n        { duration: \"1m\", target: 180 },\n        { duration: \"30s\", target: 200 },\n        { duration: \"30s\", target: 90 },\n        { duration: \"30s\", target: 0 },\n      ],\n      preAllocatedVUs: 25,\n    },\n  },\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n    http_req_duration: [\"p(95)<800\"],\n  },\n};\n\nexport default function (data) {\n  authenticate(IDENTITY_URL, CLIENT_ID, AUTH_USERNAME, AUTH_PASSWORD);\n}\n"
  },
  {
    "path": "perf/load/sync.js",
    "content": "import http from \"k6/http\";\nimport { check, fail } from \"k6\";\nimport { authenticate } from \"./helpers/auth.js\";\n\nconst IDENTITY_URL = __ENV.IDENTITY_URL;\nconst API_URL = __ENV.API_URL;\nconst CLIENT_ID = __ENV.CLIENT_ID;\nconst AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;\nconst AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;\n\nexport const options = {\n  scenarios: {\n    constant_load: {\n      executor: \"constant-arrival-rate\",\n      rate: 30,\n      timeUnit: \"1m\", // 0.5 requests / second\n      duration: \"10m\",\n      preAllocatedVUs: 5,\n    },\n    ramping_load: {\n      executor: \"ramping-arrival-rate\",\n      startRate: 30,\n      timeUnit: \"1m\", // 0.5 requests / second to start\n      stages: [\n        { duration: \"30s\", target: 30 },\n        { duration: \"2m\", target: 75 },\n        { duration: \"1m\", target: 60 },\n        { duration: \"2m\", target: 100 },\n        { duration: \"2m\", target: 90 },\n        { duration: \"1m\", target: 120 },\n        { duration: \"30s\", target: 150 },\n        { duration: \"30s\", target: 60 },\n        { duration: \"30s\", target: 0 },\n      ],\n      preAllocatedVUs: 20,\n    },\n  },\n  thresholds: {\n    http_req_failed: [\"rate<0.01\"],\n    http_req_duration: [\"p(95)<1200\"],\n  },\n};\n\nexport function setup() {\n  return authenticate(IDENTITY_URL, CLIENT_ID, AUTH_USERNAME, AUTH_PASSWORD);\n}\n\nexport default function (data) {\n  const params = {\n    headers: {\n      Accept: \"application/json\",\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${data.access_token}`,\n      \"X-ClientId\": CLIENT_ID,\n    },\n    tags: { name: \"Sync\" },\n  };\n\n  const excludeDomains = Math.random() > 0.5;\n  \n  const syncRes = http.get(`${API_URL}/sync?excludeDomains=${excludeDomains}`, params);\n  if (\n    !check(syncRes, {\n      \"sync status is 200\": (r) => r.status === 200,\n    })\n  ) {\n    console.error(`Sync failed with status ${syncRes.status}: ${syncRes.body}`);\n    fail(\"sync status code was *not* 200\");\n  }\n\n  if (syncRes.status === 200) {\n    const syncJson = syncRes.json();\n\n    check(syncJson, {\n      \"sync response has profile\": (j) => j.profile !== undefined,\n      \"sync response has folders\": (j) => Array.isArray(j.folders),\n      \"sync response has collections\": (j) => Array.isArray(j.collections),\n      \"sync response has ciphers\": (j) => Array.isArray(j.ciphers),\n      \"sync response has policies\": (j) => Array.isArray(j.policies),\n      \"sync response has sends\": (j) => Array.isArray(j.sends),\n      \"sync response has correct object type\": (j) => j.object === \"sync\"\n    });\n  }\n}\n"
  },
  {
    "path": "src/Admin/Admin.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-Admin</UserSecretsId>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304;CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Admin' \" />\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Admin-SelfHost' \" />\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\util\\MySqlMigrations\\MySqlMigrations.csproj\" />\n    <ProjectReference Include=\"..\\..\\util\\PostgresMigrations\\PostgresMigrations.csproj\" />\n    <ProjectReference Include=\"..\\SharedWeb\\SharedWeb.csproj\" />\n    <ProjectReference Include=\"..\\..\\util\\Migrator\\Migrator.csproj\" />\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\..\\util\\SqliteMigrations\\SqliteMigrations.csproj\" />\n  </ItemGroup>\n\n  <Choose>\n    <When Condition=\"!$(DefineConstants.Contains('OSS'))\">\n      <ItemGroup>\n        <ProjectReference Include=\"..\\..\\bitwarden_license\\src\\Commercial.Core\\Commercial.Core.csproj\" />\n        <ProjectReference Include=\"..\\..\\bitwarden_license\\src\\Commercial.Infrastructure.EntityFramework\\Commercial.Infrastructure.EntityFramework.csproj\" />\n      </ItemGroup>\n    </When>\n  </Choose>\n\n</Project>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Controllers/OrganizationsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing Bit.Admin.AdminConsole.Models;\nusing Bit.Admin.Enums;\nusing Bit.Admin.Services;\nusing Bit.Admin.Utilities;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.Providers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities.v2;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Admin.AdminConsole.Controllers;\n\n[Authorize]\npublic class OrganizationsController : Controller\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationConnectionRepository _organizationConnectionRepository;\n    private readonly ISelfHostedSyncSponsorshipsCommand _syncSponsorshipsCommand;\n    private readonly ICipherRepository _cipherRepository;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IPolicyRepository _policyRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IProviderRepository _providerRepository;\n    private readonly ILogger<OrganizationsController> _logger;\n    private readonly IAccessControlService _accessControlService;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository;\n    private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;\n    private readonly IProviderBillingService _providerBillingService;\n    private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;\n    private readonly IPricingClient _pricingClient;\n    private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;\n    private readonly IOrganizationBillingService _organizationBillingService;\n    private readonly IEventService _eventService;\n    private readonly IAutomaticUserConfirmationOrganizationPolicyComplianceValidator _automaticUserConfirmationOrganizationPolicyComplianceValidator;\n    private readonly IOrganizationAutoConfirmEnabledNotificationCommand _organizationAutoConfirmEnabledNotificationCommand;\n\n    public OrganizationsController(\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IOrganizationConnectionRepository organizationConnectionRepository,\n        ISelfHostedSyncSponsorshipsCommand syncSponsorshipsCommand,\n        ICipherRepository cipherRepository,\n        ICollectionRepository collectionRepository,\n        IGroupRepository groupRepository,\n        IPolicyRepository policyRepository,\n        IStripePaymentService paymentService,\n        IApplicationCacheService applicationCacheService,\n        GlobalSettings globalSettings,\n        IProviderRepository providerRepository,\n        ILogger<OrganizationsController> logger,\n        IAccessControlService accessControlService,\n        ISecretRepository secretRepository,\n        IProjectRepository projectRepository,\n        IServiceAccountRepository serviceAccountRepository,\n        IProviderOrganizationRepository providerOrganizationRepository,\n        IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,\n        IProviderBillingService providerBillingService,\n        IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,\n        IPricingClient pricingClient,\n        IResendOrganizationInviteCommand resendOrganizationInviteCommand,\n        IOrganizationBillingService organizationBillingService,\n        IEventService eventService,\n        IAutomaticUserConfirmationOrganizationPolicyComplianceValidator automaticUserConfirmationOrganizationPolicyComplianceValidator,\n        IOrganizationAutoConfirmEnabledNotificationCommand organizationAutoConfirmEnabledNotificationCommand)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationConnectionRepository = organizationConnectionRepository;\n        _syncSponsorshipsCommand = syncSponsorshipsCommand;\n        _cipherRepository = cipherRepository;\n        _collectionRepository = collectionRepository;\n        _groupRepository = groupRepository;\n        _policyRepository = policyRepository;\n        _paymentService = paymentService;\n        _applicationCacheService = applicationCacheService;\n        _globalSettings = globalSettings;\n        _providerRepository = providerRepository;\n        _logger = logger;\n        _accessControlService = accessControlService;\n        _secretRepository = secretRepository;\n        _projectRepository = projectRepository;\n        _serviceAccountRepository = serviceAccountRepository;\n        _providerOrganizationRepository = providerOrganizationRepository;\n        _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;\n        _providerBillingService = providerBillingService;\n        _organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;\n        _pricingClient = pricingClient;\n        _resendOrganizationInviteCommand = resendOrganizationInviteCommand;\n        _organizationBillingService = organizationBillingService;\n        _eventService = eventService;\n        _automaticUserConfirmationOrganizationPolicyComplianceValidator = automaticUserConfirmationOrganizationPolicyComplianceValidator;\n        _organizationAutoConfirmEnabledNotificationCommand = organizationAutoConfirmEnabledNotificationCommand;\n    }\n\n    [RequirePermission(Permission.Org_List_View)]\n    public async Task<IActionResult> Index(string name = null, string userEmail = null, bool? paid = null,\n        int page = 1, int count = 25)\n    {\n        if (page < 1)\n        {\n            page = 1;\n        }\n\n        if (count < 1)\n        {\n            count = 1;\n        }\n\n        var encodedName = WebUtility.HtmlEncode(name);\n        var skip = (page - 1) * count;\n        var organizations = await _organizationRepository.SearchAsync(encodedName, userEmail, paid, skip, count);\n        return View(new OrganizationsModel\n        {\n            Items = organizations as List<Organization>,\n            Name = string.IsNullOrWhiteSpace(name) ? null : name,\n            UserEmail = string.IsNullOrWhiteSpace(userEmail) ? null : userEmail,\n            Paid = paid,\n            Page = page,\n            Count = count,\n            Action = _globalSettings.SelfHosted ? \"View\" : \"Edit\",\n            SelfHosted = _globalSettings.SelfHosted\n        });\n    }\n\n    public async Task<IActionResult> View(Guid id)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var provider = await _providerRepository.GetByOrganizationIdAsync(id);\n        var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(id);\n        var collections = await _collectionRepository.GetManyByOrganizationIdAsync(id);\n        IEnumerable<Group> groups = null;\n        if (organization.UseGroups)\n        {\n            groups = await _groupRepository.GetManyByOrganizationIdAsync(id);\n        }\n        IEnumerable<Policy> policies = null;\n        if (organization.UsePolicies)\n        {\n            policies = await _policyRepository.GetManyByOrganizationIdAsync(id);\n        }\n        var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);\n        var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;\n        var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;\n        var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;\n        var serviceAccounts = organization.UseSecretsManager ? await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(id) : -1;\n        var smSeats = organization.UseSecretsManager\n            ? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            : -1;\n        return View(new OrganizationViewModel(organization, provider, billingSyncConnection, users, ciphers, collections, groups, policies,\n            secrets, projects, serviceAccounts, smSeats));\n    }\n\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> Edit(Guid id)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var provider = await _providerRepository.GetByOrganizationIdAsync(id);\n        var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(id);\n        var collections = await _collectionRepository.GetManyByOrganizationIdAsync(id);\n        IEnumerable<Group> groups = null;\n        if (organization.UseGroups)\n        {\n            groups = await _groupRepository.GetManyByOrganizationIdAsync(id);\n        }\n        IEnumerable<Policy> policies = null;\n        if (organization.UsePolicies)\n        {\n            policies = await _policyRepository.GetManyByOrganizationIdAsync(id);\n        }\n        var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id);\n        var billingInfo = await _paymentService.GetBillingAsync(organization);\n        var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(organization);\n        var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null;\n        var secrets = organization.UseSecretsManager ? await _secretRepository.GetSecretsCountByOrganizationIdAsync(id) : -1;\n        var projects = organization.UseSecretsManager ? await _projectRepository.GetProjectCountByOrganizationIdAsync(id) : -1;\n        var serviceAccounts = organization.UseSecretsManager ? await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(id) : -1;\n\n        var smSeats = organization.UseSecretsManager\n            ? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            : -1;\n\n        var plans = await _pricingClient.ListPlans();\n\n        return View(new OrganizationEditModel(\n            organization,\n            provider,\n            users,\n            ciphers,\n            collections,\n            groups,\n            policies,\n            billingInfo,\n            billingHistoryInfo,\n            billingSyncConnection,\n            _globalSettings,\n            plans,\n            secrets,\n            projects,\n            serviceAccounts,\n            smSeats));\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> Edit(Guid id, OrganizationEditModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            TempData[\"Error\"] = ModelState.GetErrorMessage();\n            return RedirectToAction(\"Edit\", new { id });\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(id);\n\n        if (organization == null)\n        {\n            TempData[\"Error\"] = \"Could not find organization to update.\";\n            return RedirectToAction(\"Index\");\n        }\n\n        var existingOrganizationData = new Organization\n        {\n            Id = organization.Id,\n            Name = organization.Name,\n            BillingEmail = organization.BillingEmail,\n            Status = organization.Status,\n            PlanType = organization.PlanType,\n            Seats = organization.Seats,\n            UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation\n        };\n\n        if (model.PlanType.HasValue)\n        {\n            var freePlan = await _pricingClient.GetPlanOrThrow(model.PlanType.Value);\n            var isDowngradingToFree = organization.PlanType != PlanType.Free && model.PlanType.Value == PlanType.Free;\n            if (isDowngradingToFree)\n            {\n                if (model.Seats.HasValue && model.Seats.Value > freePlan.PasswordManager.MaxSeats)\n                {\n                    TempData[\"Error\"] = $\"Organizations with more than {freePlan.PasswordManager.MaxSeats} seats cannot be downgraded to the Free plan\";\n                    return RedirectToAction(\"Edit\", new { id });\n                }\n\n                if (model.MaxCollections > freePlan.PasswordManager.MaxCollections)\n                {\n                    TempData[\"Error\"] = $\"Organizations with more than {freePlan.PasswordManager.MaxCollections} collections cannot be downgraded to the Free plan. Your organization currently has {organization.MaxCollections} collections.\";\n                    return RedirectToAction(\"Edit\", new { id });\n                }\n\n                model.MaxStorageGb = null;\n                model.ExpirationDate = null;\n                model.Enabled = true;\n            }\n        }\n\n        UpdateOrganization(organization, model);\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n        if (organization.UseSecretsManager && !plan.SupportsSecretsManager)\n        {\n            TempData[\"Error\"] = \"Plan does not support Secrets Manager\";\n            return RedirectToAction(\"Edit\", new { id });\n        }\n\n        if (await CheckOrganizationPolicyComplianceAsync(existingOrganizationData, organization) is { } error)\n        {\n            TempData[\"Error\"] = error.Message;\n\n            return RedirectToAction(\"Edit\", new { id });\n        }\n\n        await HandlePotentialProviderSeatScalingAsync(\n            existingOrganizationData,\n            model);\n\n        await _organizationRepository.ReplaceAsync(organization);\n\n        await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n\n        if (existingOrganizationData.UseAutomaticUserConfirmation != organization.UseAutomaticUserConfirmation)\n        {\n            var eventType = organization.UseAutomaticUserConfirmation\n                ? EventType.Organization_AutoConfirmEnabled_Portal\n                : EventType.Organization_AutoConfirmDisabled_Portal;\n\n            await _eventService.LogOrganizationEventAsync(organization, eventType, EventSystemUser.BitwardenPortal);\n        }\n\n        if (!existingOrganizationData.UseAutomaticUserConfirmation && organization.UseAutomaticUserConfirmation)\n        {\n            try\n            {\n                var emailsToNotify =\n                    (await _organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id))\n                    .Where(x =>\n                        (x.Type == OrganizationUserType.Admin\n                         || x.Type == OrganizationUserType.Owner\n                         || x.GetPermissions()?.ManageUsers == true)\n                        && !string.IsNullOrWhiteSpace(x.Email))\n                    .Select(x => x.Email)\n                    .ToList();\n\n                await _organizationAutoConfirmEnabledNotificationCommand.SendEmailAsync(\n                    new OrganizationAutoConfirmEnabledNotificationRequest(organization, emailsToNotify));\n            }\n            catch (Exception ex)\n            {\n                _logger.LogError(ex, \"Failed to send email notification to admins when organization auto-confirm was enabled.\");\n                TempData[\"Warning\"] = \"Organization updated successfully, but email notification to admins failed.\";\n            }\n        }\n\n        // Sync name/email changes to Stripe\n        if (existingOrganizationData.Name != organization.Name || existingOrganizationData.BillingEmail != organization.BillingEmail)\n        {\n            try\n            {\n                await _organizationBillingService.UpdateOrganizationNameAndEmail(organization);\n            }\n            catch (Exception ex)\n            {\n                _logger.LogError(ex,\n                    \"Failed to update Stripe customer for organization {OrganizationId}. Database was updated successfully.\",\n                    organization.Id);\n                TempData[\"Warning\"] = \"Organization updated successfully, but Stripe customer name/email synchronization failed.\";\n            }\n        }\n\n        return RedirectToAction(\"Edit\", new { id });\n    }\n\n    private async Task<Error> CheckOrganizationPolicyComplianceAsync(Organization existingOrganizationData, Organization updatedOrganization)\n    {\n        if (!existingOrganizationData.UseAutomaticUserConfirmation && updatedOrganization.UseAutomaticUserConfirmation)\n        {\n            var validationResult = await _automaticUserConfirmationOrganizationPolicyComplianceValidator.IsOrganizationCompliantAsync(\n                new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(existingOrganizationData.Id));\n\n            return validationResult.Match(error => error, _ => null);\n        }\n\n        return null;\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Org_Delete)]\n    public async Task<IActionResult> Delete(Guid id)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(id);\n\n        if (organization == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        if (organization.IsValidClient())\n        {\n            var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);\n\n            if (provider.IsBillable())\n            {\n                await _providerBillingService.ScaleSeats(\n                    provider,\n                    organization.PlanType,\n                    -organization.Seats ?? 0);\n            }\n        }\n\n        await _organizationRepository.DeleteAsync(organization);\n        await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);\n\n        return RedirectToAction(\"Index\");\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Org_RequestDelete)]\n    public async Task<IActionResult> DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            TempData[\"Error\"] = ModelState.GetErrorMessage();\n        }\n        else\n        {\n            try\n            {\n                var organization = await _organizationRepository.GetByIdAsync(id);\n                if (organization != null)\n                {\n                    await _organizationInitiateDeleteCommand.InitiateDeleteAsync(organization, model.AdminEmail);\n                    TempData[\"Success\"] = \"The request to initiate deletion of the organization has been sent.\";\n                }\n            }\n            catch (Exception ex)\n            {\n                TempData[\"Error\"] = ex.Message;\n            }\n        }\n\n        return RedirectToAction(\"Edit\", new { id });\n    }\n\n    public async Task<IActionResult> TriggerBillingSync(Guid id)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n        var connection = (await _organizationConnectionRepository.GetEnabledByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();\n        if (connection != null)\n        {\n            try\n            {\n                var config = connection.GetConfig<BillingSyncConfig>();\n                await _syncSponsorshipsCommand.SyncOrganization(id, config.CloudOrganizationId, connection);\n                TempData[\"ConnectionActivated\"] = id;\n                TempData[\"ConnectionError\"] = null;\n            }\n            catch (Exception ex)\n            {\n                TempData[\"ConnectionError\"] = ex.Message;\n                _logger.LogWarning(ex, \"Error while attempting to do billing sync for organization with id '{OrganizationId}'\", id);\n            }\n\n            if (_globalSettings.SelfHosted)\n            {\n                return RedirectToAction(\"View\", new { id });\n            }\n            else\n            {\n                return RedirectToAction(\"Edit\", new { id });\n            }\n        }\n        return RedirectToAction(\"Index\");\n    }\n\n    [HttpPost]\n    public async Task<IActionResult> ResendOwnerInvite(Guid id)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner);\n        foreach (var organizationUser in organizationUsers)\n        {\n            await _resendOrganizationInviteCommand.ResendInviteAsync(id, null, organizationUser.Id, true);\n        }\n\n        return Json(null);\n    }\n\n    [HttpPost]\n    [RequirePermission(Permission.Provider_Edit)]\n    public async Task<IActionResult> UnlinkOrganizationFromProviderAsync(Guid id)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(id);\n        if (organization is null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var provider = await _providerRepository.GetByOrganizationIdAsync(id);\n        if (provider is null)\n        {\n            return RedirectToAction(\"Edit\", new { id });\n        }\n\n        var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(id);\n        if (providerOrganization is null)\n        {\n            return RedirectToAction(\"Edit\", new { id });\n        }\n\n        await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(\n            provider,\n            providerOrganization,\n            organization);\n\n        return Json(null);\n    }\n\n    private void UpdateOrganization(Organization organization, OrganizationEditModel model)\n    {\n        if (_accessControlService.UserHasPermission(Permission.Org_Name_Edit))\n        {\n            organization.Name = WebUtility.HtmlEncode(model.Name);\n        }\n\n        if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))\n        {\n            organization.Enabled = model.Enabled;\n        }\n\n        if (_accessControlService.UserHasPermission(Permission.Org_Plan_Edit))\n        {\n            organization.PlanType = model.PlanType.Value;\n            organization.Plan = model.Plan;\n            organization.Seats = model.Seats;\n            organization.MaxAutoscaleSeats = model.MaxAutoscaleSeats;\n            organization.MaxCollections = model.MaxCollections;\n            organization.MaxStorageGb = model.MaxStorageGb;\n\n            //features\n            organization.SelfHost = model.SelfHost;\n            organization.Use2fa = model.Use2fa;\n            organization.UseApi = model.UseApi;\n            organization.UseGroups = model.UseGroups;\n            organization.UsePolicies = model.UsePolicies;\n            organization.UseSso = model.UseSso;\n            organization.UseKeyConnector = model.UseKeyConnector;\n            organization.UseScim = model.UseScim;\n            organization.UseDirectory = model.UseDirectory;\n            organization.UseEvents = model.UseEvents;\n            organization.UseResetPassword = model.UseResetPassword;\n            organization.UseCustomPermissions = model.UseCustomPermissions;\n            organization.UseTotp = model.UseTotp;\n            organization.UsersGetPremium = model.UsersGetPremium;\n            organization.UseSecretsManager = model.UseSecretsManager;\n            organization.UseRiskInsights = model.UseRiskInsights;\n            organization.UseOrganizationDomains = model.UseOrganizationDomains;\n            organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;\n            organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation;\n            organization.UseDisableSmAdsForUsers = model.UseDisableSmAdsForUsers;\n            organization.UsePhishingBlocker = model.UsePhishingBlocker;\n            organization.UseMyItems = model.UseMyItems;\n\n            //secrets\n            organization.SmSeats = model.SmSeats;\n            organization.MaxAutoscaleSmSeats = model.MaxAutoscaleSmSeats;\n            organization.SmServiceAccounts = model.SmServiceAccounts;\n            organization.MaxAutoscaleSmServiceAccounts = model.MaxAutoscaleSmServiceAccounts;\n        }\n\n        if (_accessControlService.UserHasPermission(Permission.Org_Licensing_Edit))\n        {\n            organization.LicenseKey = model.LicenseKey;\n            organization.ExpirationDate = model.ExpirationDate;\n        }\n\n        if (_accessControlService.UserHasPermission(Permission.Org_Billing_Edit))\n        {\n            organization.BillingEmail = model.BillingEmail?.ToLowerInvariant()?.Trim();\n            organization.Gateway = model.Gateway;\n            organization.GatewayCustomerId = model.GatewayCustomerId;\n            organization.GatewaySubscriptionId = model.GatewaySubscriptionId;\n        }\n    }\n\n    private async Task HandlePotentialProviderSeatScalingAsync(\n        Organization organization,\n        OrganizationEditModel update)\n    {\n        var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);\n\n        // No scaling required\n        if (provider is not { Type: ProviderType.Msp, Status: ProviderStatusType.Billable } ||\n            organization is not { Status: OrganizationStatusType.Managed } ||\n            !organization.Seats.HasValue ||\n            update is { Seats: null, PlanType: null } ||\n            update is { PlanType: not PlanType.TeamsMonthly and not PlanType.EnterpriseMonthly } ||\n            (PlanTypesMatch() && SeatsMatch()))\n        {\n            return;\n        }\n\n        // Only scale the plan\n        if (!PlanTypesMatch() && SeatsMatch())\n        {\n            await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);\n            await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, organization.Seats.Value);\n        }\n        // Only scale the seats\n        else if (PlanTypesMatch() && !SeatsMatch())\n        {\n            var seatAdjustment = update.Seats!.Value - organization.Seats.Value;\n            await _providerBillingService.ScaleSeats(provider, organization.PlanType, seatAdjustment);\n        }\n        // Scale both\n        else if (!PlanTypesMatch() && !SeatsMatch())\n        {\n            var seatAdjustment = update.Seats!.Value - organization.Seats.Value;\n            var planTypeAdjustment = organization.Seats.Value;\n            var totalAdjustment = seatAdjustment + planTypeAdjustment;\n\n            await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);\n            await _providerBillingService.ScaleSeats(provider, update.PlanType!.Value, totalAdjustment);\n        }\n\n        return;\n\n        bool PlanTypesMatch()\n            => update.PlanType.HasValue && update.PlanType.Value == organization.PlanType;\n\n        bool SeatsMatch()\n            => update.Seats.HasValue && update.Seats.Value == organization.Seats;\n    }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Controllers/ProviderOrganizationsController.cs",
    "content": "﻿using Bit.Admin.Enums;\nusing Bit.Admin.Utilities;\nusing Bit.Core.AdminConsole.Providers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Admin.AdminConsole.Controllers;\n\n[Authorize]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class ProviderOrganizationsController : Controller\n{\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;\n\n    public ProviderOrganizationsController(IProviderRepository providerRepository,\n        IProviderOrganizationRepository providerOrganizationRepository,\n        IOrganizationRepository organizationRepository,\n        IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand)\n    {\n        _providerRepository = providerRepository;\n        _providerOrganizationRepository = providerOrganizationRepository;\n        _organizationRepository = organizationRepository;\n        _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;\n    }\n\n    [HttpPost]\n    [RequirePermission(Permission.Provider_Edit)]\n    public async Task<IActionResult> DeleteAsync(Guid providerId, Guid id)\n    {\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n        if (provider is null)\n        {\n            return RedirectToAction(\"Index\", \"Providers\");\n        }\n\n        var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(id);\n        if (providerOrganization is null)\n        {\n            return RedirectToAction(\"View\", \"Providers\", new { id = providerId });\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);\n        if (organization == null)\n        {\n            return RedirectToAction(\"View\", \"Providers\", new { id = providerId });\n        }\n\n        try\n        {\n            await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(\n                provider,\n                providerOrganization,\n                organization);\n        }\n        catch (BadRequestException ex)\n        {\n            return BadRequest(ex.Message);\n        }\n\n        return Json(null);\n    }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Controllers/ProvidersController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Net;\nusing Bit.Admin.AdminConsole.Models;\nusing Bit.Admin.Enums;\nusing Bit.Admin.Services;\nusing Bit.Admin.Utilities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.Providers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Models;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Stripe;\n\nnamespace Bit.Admin.AdminConsole.Controllers;\n\n[Authorize]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class ProvidersController : Controller\n{\n    private readonly string _stripeUrl;\n    private readonly string _braintreeMerchantUrl;\n    private readonly string _braintreeMerchantId;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IResellerClientOrganizationSignUpCommand _resellerClientOrganizationSignUpCommand;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository;\n    private readonly IProviderService _providerService;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly ICreateProviderCommand _createProviderCommand;\n    private readonly IProviderPlanRepository _providerPlanRepository;\n    private readonly IProviderBillingService _providerBillingService;\n    private readonly IPricingClient _pricingClient;\n    private readonly IStripeAdapter _stripeAdapter;\n    private readonly IAccessControlService _accessControlService;\n    private readonly ISubscriberService _subscriberService;\n    private readonly ILogger<ProvidersController> _logger;\n\n    public ProvidersController(IOrganizationRepository organizationRepository,\n        IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand,\n        IProviderRepository providerRepository,\n        IProviderUserRepository providerUserRepository,\n        IProviderOrganizationRepository providerOrganizationRepository,\n        IProviderService providerService,\n        GlobalSettings globalSettings,\n        IApplicationCacheService applicationCacheService,\n        ICreateProviderCommand createProviderCommand,\n        IProviderPlanRepository providerPlanRepository,\n        IProviderBillingService providerBillingService,\n        IWebHostEnvironment webHostEnvironment,\n        IPricingClient pricingClient,\n        IStripeAdapter stripeAdapter,\n        IAccessControlService accessControlService,\n        ISubscriberService subscriberService,\n        ILogger<ProvidersController> logger)\n    {\n        _organizationRepository = organizationRepository;\n        _resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand;\n        _providerRepository = providerRepository;\n        _providerUserRepository = providerUserRepository;\n        _providerOrganizationRepository = providerOrganizationRepository;\n        _providerService = providerService;\n        _globalSettings = globalSettings;\n        _applicationCacheService = applicationCacheService;\n        _createProviderCommand = createProviderCommand;\n        _providerPlanRepository = providerPlanRepository;\n        _providerBillingService = providerBillingService;\n        _pricingClient = pricingClient;\n        _stripeAdapter = stripeAdapter;\n        _accessControlService = accessControlService;\n        _stripeUrl = webHostEnvironment.GetStripeUrl();\n        _braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();\n        _braintreeMerchantId = globalSettings.Braintree.MerchantId;\n        _subscriberService = subscriberService;\n        _logger = logger;\n    }\n\n    [RequirePermission(Permission.Provider_List_View)]\n    public async Task<IActionResult> Index(string name = null, string userEmail = null, int page = 1, int count = 25)\n    {\n        if (page < 1)\n        {\n            page = 1;\n        }\n\n        if (count < 1)\n        {\n            count = 1;\n        }\n\n        var skip = (page - 1) * count;\n        var providers = await _providerRepository.SearchAsync(name, userEmail, skip, count);\n        return View(new ProvidersModel\n        {\n            Items = providers as List<Provider>,\n            Name = string.IsNullOrWhiteSpace(name) ? null : name,\n            UserEmail = string.IsNullOrWhiteSpace(userEmail) ? null : userEmail,\n            Page = page,\n            Count = count,\n            Action = _globalSettings.SelfHosted ? \"View\" : \"Edit\",\n            SelfHosted = _globalSettings.SelfHosted\n        });\n    }\n\n    public IActionResult Create()\n    {\n        return View(new CreateProviderModel());\n    }\n\n    [HttpGet(\"providers/create/msp\")]\n    public IActionResult CreateMsp(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)\n    {\n        return View(new CreateMspProviderModel\n        {\n            OwnerEmail = ownerEmail,\n            TeamsMonthlySeatMinimum = teamsMinimumSeats,\n            EnterpriseMonthlySeatMinimum = enterpriseMinimumSeats\n        });\n    }\n\n    [HttpGet(\"providers/create/reseller\")]\n    public IActionResult CreateReseller()\n    {\n        return View(new CreateResellerProviderModel());\n    }\n\n    [HttpGet(\"providers/create/business-unit\")]\n    public IActionResult CreateBusinessUnit(int enterpriseMinimumSeats, string ownerEmail = null)\n    {\n        return View(new CreateBusinessUnitProviderModel\n        {\n            OwnerEmail = ownerEmail,\n            EnterpriseSeatMinimum = enterpriseMinimumSeats\n        });\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Provider_Create)]\n    public IActionResult Create(CreateProviderModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        return model.Type switch\n        {\n            ProviderType.Msp => RedirectToAction(\"CreateMsp\"),\n            ProviderType.Reseller => RedirectToAction(\"CreateReseller\"),\n            ProviderType.BusinessUnit => RedirectToAction(\"CreateBusinessUnit\"),\n            _ => View(model)\n        };\n    }\n\n    [HttpPost(\"providers/create/msp\")]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Provider_Create)]\n    public async Task<IActionResult> CreateMsp(CreateMspProviderModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        var provider = model.ToProvider();\n\n        await _createProviderCommand.CreateMspAsync(\n            provider,\n            model.OwnerEmail,\n            model.TeamsMonthlySeatMinimum,\n            model.EnterpriseMonthlySeatMinimum);\n\n        return RedirectToAction(\"Edit\", new { id = provider.Id });\n    }\n\n    [HttpPost(\"providers/create/reseller\")]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Provider_Create)]\n    public async Task<IActionResult> CreateReseller(CreateResellerProviderModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n        var provider = model.ToProvider();\n        await _createProviderCommand.CreateResellerAsync(provider);\n\n        return RedirectToAction(\"Edit\", new { id = provider.Id });\n    }\n\n    [HttpPost(\"providers/create/business-unit\")]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Provider_Create)]\n    public async Task<IActionResult> CreateBusinessUnit(CreateBusinessUnitProviderModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n        var provider = model.ToProvider();\n\n        await _createProviderCommand.CreateBusinessUnitAsync(\n            provider,\n            model.OwnerEmail,\n            model.Plan.Value,\n            model.EnterpriseSeatMinimum);\n\n        return RedirectToAction(\"Edit\", new { id = provider.Id });\n    }\n\n    [RequirePermission(Permission.Provider_View)]\n    public async Task<IActionResult> View(Guid id)\n    {\n        var provider = await _providerRepository.GetByIdAsync(id);\n        if (provider == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);\n        var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);\n        var providerPlans = await _providerPlanRepository.GetByProviderId(id);\n        return View(new ProviderViewModel(provider, users, providerOrganizations, providerPlans.ToList()));\n    }\n\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> Edit(Guid id)\n    {\n        var provider = await GetEditModel(id);\n        if (provider == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        return View(provider);\n    }\n\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> Cancel(Guid id)\n    {\n        var provider = await GetEditModel(id);\n        if (provider == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        return RedirectToAction(\"Edit\", new { id });\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    [RequirePermission(Permission.Provider_Edit)]\n    public async Task<IActionResult> Edit(Guid id, ProviderEditModel model)\n    {\n        var provider = await _providerRepository.GetByIdAsync(id);\n\n        if (provider == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        if (provider.Type != model.Type)\n        {\n            var oldModel = await GetEditModel(id);\n            ModelState.AddModelError(nameof(model.Type), \"Provider type cannot be changed.\");\n            return View(oldModel);\n        }\n\n        if (!ModelState.IsValid)\n        {\n            var oldModel = await GetEditModel(id);\n            ModelState[nameof(ProviderEditModel.BillingEmail)]!.RawValue = oldModel.BillingEmail;\n            return View(oldModel);\n        }\n\n        var originalProviderStatus = provider.Enabled;\n\n        // Capture original billing email before modifications for Stripe sync\n        var originalBillingEmail = provider.BillingEmail;\n\n        model.ToProvider(provider);\n\n        // validate the stripe ids to prevent saving a bad one\n        if (provider.IsBillable())\n        {\n            if (!await _subscriberService.IsValidGatewayCustomerIdAsync(provider))\n            {\n                var oldModel = await GetEditModel(id);\n                ModelState.AddModelError(nameof(model.GatewayCustomerId), $\"Invalid Gateway Customer Id: {model.GatewayCustomerId}\");\n                return View(oldModel);\n            }\n            if (!await _subscriberService.IsValidGatewaySubscriptionIdAsync(provider))\n            {\n                var oldModel = await GetEditModel(id);\n                ModelState.AddModelError(nameof(model.GatewaySubscriptionId), $\"Invalid Gateway Subscription Id: {model.GatewaySubscriptionId}\");\n                return View(oldModel);\n            }\n        }\n\n        provider.Enabled = _accessControlService.UserHasPermission(Permission.Provider_CheckEnabledBox)\n            ? model.Enabled : originalProviderStatus;\n\n        await _providerService.UpdateAsync(provider);\n        await _applicationCacheService.UpsertProviderAbilityAsync(provider);\n\n        // Sync billing email changes to Stripe\n        if (!string.IsNullOrEmpty(provider.GatewayCustomerId) && originalBillingEmail != provider.BillingEmail)\n        {\n            try\n            {\n                await _providerBillingService.UpdateProviderNameAndEmail(provider);\n            }\n            catch (Exception ex)\n            {\n                _logger.LogError(ex,\n                    \"Failed to update Stripe customer for provider {ProviderId}. Database was updated successfully.\",\n                    provider.Id);\n                TempData[\"Warning\"] = \"Provider updated successfully, but Stripe customer email synchronization failed.\";\n            }\n        }\n\n        if (!provider.IsBillable())\n        {\n            return RedirectToAction(\"Edit\", new { id });\n        }\n\n        var providerPlans = await _providerPlanRepository.GetByProviderId(id);\n\n        switch (provider.Type)\n        {\n            case ProviderType.Msp:\n                var updateMspSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(\n                    provider,\n                    [\n                        (Plan: PlanType.TeamsMonthly, SeatsMinimum: model.TeamsMonthlySeatMinimum),\n                        (Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum)\n                    ]);\n                await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);\n\n                var customer = await _stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId);\n                if (model.PayByInvoice != customer.ApprovedToPayByInvoice())\n                {\n                    var approvedToPayByInvoice = model.PayByInvoice ? \"1\" : \"0\";\n                    await _stripeAdapter.UpdateCustomerAsync(customer.Id, new CustomerUpdateOptions\n                    {\n                        Metadata = new Dictionary<string, string>\n                        {\n                            [StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice\n                        }\n                    });\n                }\n                break;\n            case ProviderType.BusinessUnit:\n                {\n                    var existingMoePlan = providerPlans.Single();\n\n                    // 1. Change the plan and take over any old values.\n                    var changeMoePlanCommand = new ChangeProviderPlanCommand(\n                        provider,\n                        existingMoePlan.Id,\n                        model.Plan!.Value);\n                    await _providerBillingService.ChangePlan(changeMoePlanCommand);\n\n                    // 2. Update the seat minimums.\n                    var updateMoeSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(\n                        provider,\n                        [\n                            (Plan: model.Plan!.Value, SeatsMinimum: model.EnterpriseMinimumSeats!.Value)\n                        ]);\n                    await _providerBillingService.UpdateSeatMinimums(updateMoeSeatMinimumsCommand);\n                    break;\n                }\n        }\n\n        return RedirectToAction(\"Edit\", new { id });\n    }\n\n    private async Task<ProviderEditModel> GetEditModel(Guid id)\n    {\n        var provider = await _providerRepository.GetByIdAsync(id);\n        if (provider == null)\n        {\n            return null;\n        }\n\n        var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);\n        var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);\n\n        if (!provider.IsBillable())\n        {\n            return new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>(), false);\n        }\n\n        var providerPlans = await _providerPlanRepository.GetByProviderId(id);\n        var payByInvoice = ((await _subscriberService.GetCustomer(provider))?.ApprovedToPayByInvoice() ?? false);\n\n        return new ProviderEditModel(\n            provider, users, providerOrganizations,\n            providerPlans.ToList(), payByInvoice, GetGatewayCustomerUrl(provider), GetGatewaySubscriptionUrl(provider));\n    }\n\n    [RequirePermission(Permission.Provider_ResendEmailInvite)]\n    public async Task<IActionResult> ResendInvite(Guid ownerId, Guid providerId)\n    {\n        await _providerService.ResendProviderSetupInviteEmailAsync(providerId, ownerId);\n        TempData[\"InviteResentTo\"] = ownerId;\n        return RedirectToAction(\"Edit\", new { id = providerId });\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> AddExistingOrganization(Guid id, string name = null, string ownerEmail = null, int page = 1, int count = 25)\n    {\n        if (page < 1)\n        {\n            page = 1;\n        }\n\n        if (count < 1)\n        {\n            count = 1;\n        }\n\n        var encodedName = WebUtility.HtmlEncode(name);\n        var skip = (page - 1) * count;\n        var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(encodedName, ownerEmail, skip, count);\n        var viewModel = new OrganizationUnassignedToProviderSearchViewModel\n        {\n            OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name,\n            OrganizationOwnerEmail = string.IsNullOrWhiteSpace(ownerEmail) ? null : ownerEmail,\n            Page = page,\n            Count = count,\n            Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel\n            {\n                Id = uo.Id,\n                Name = uo.DisplayName(),\n                PlanType = uo.PlanType\n            }).ToList()\n        };\n\n        return View(viewModel);\n    }\n\n    [HttpPost]\n    public async Task<IActionResult> AddExistingOrganization(Guid id, OrganizationUnassignedToProviderSearchViewModel model)\n    {\n        var organizationIds = model.Items.Where(o => o.Selected).Select(o => o.Id).ToArray();\n        if (organizationIds.Any())\n        {\n            await _providerService.AddOrganizationsToReseller(id, organizationIds);\n        }\n\n        return RedirectToAction(\"Edit\", \"Providers\", new { id = id });\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> CreateOrganization(Guid providerId)\n    {\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n        if (provider is not { Type: ProviderType.Reseller })\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var plans = await _pricingClient.ListPlans();\n\n        return View(new OrganizationEditModel(provider, plans));\n    }\n\n    [HttpPost]\n    public async Task<IActionResult> CreateOrganization(Guid providerId, OrganizationEditModel model)\n    {\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n        if (provider is not { Type: ProviderType.Reseller })\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var organization = model.CreateOrganization(provider);\n        await _resellerClientOrganizationSignUpCommand.SignUpResellerClientAsync(organization, model.Owners);\n        await _providerService.AddOrganization(providerId, organization.Id, null);\n\n        return RedirectToAction(\"Edit\", \"Providers\", new { id = providerId });\n    }\n\n    [HttpPost]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    [RequirePermission(Permission.Provider_Edit)]\n    public async Task<IActionResult> Delete(Guid id, string providerName)\n    {\n        var provider = await _providerRepository.GetByIdAsync(id);\n\n        if (provider is null)\n        {\n            return BadRequest(\"Provider does not exist\");\n        }\n\n        if (provider.Status == ProviderStatusType.Pending)\n        {\n            await _providerService.DeleteAsync(provider);\n            return NoContent();\n        }\n\n        if (string.IsNullOrWhiteSpace(providerName))\n        {\n            return BadRequest(\"Invalid provider name\");\n        }\n\n        var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);\n\n        if (providerOrganizations.Count > 0)\n        {\n            return BadRequest(\"You must unlink all clients before you can delete a provider\");\n        }\n\n        if (!string.Equals(providerName.Trim(), provider.DisplayName(), StringComparison.OrdinalIgnoreCase))\n        {\n            return BadRequest(\"Invalid provider name\");\n        }\n\n        await _providerService.DeleteAsync(provider);\n        return NoContent();\n    }\n\n    [HttpPost]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    [RequirePermission(Permission.Provider_Edit)]\n    public async Task<IActionResult> DeleteInitiation(Guid id, string providerEmail)\n    {\n        var emailAttribute = new EmailAddressAttribute();\n        if (!emailAttribute.IsValid(providerEmail))\n        {\n            return BadRequest(\"Invalid provider admin email\");\n        }\n\n        var provider = await _providerRepository.GetByIdAsync(id);\n        if (provider != null)\n        {\n            try\n            {\n                await _providerService.InitiateDeleteAsync(provider, providerEmail);\n            }\n            catch (BadRequestException ex)\n            {\n                return BadRequest(ex.Message);\n            }\n        }\n\n        return NoContent();\n    }\n\n    private string GetGatewayCustomerUrl(Provider provider)\n    {\n        if (!provider.Gateway.HasValue || string.IsNullOrEmpty(provider.GatewayCustomerId))\n        {\n            return null;\n        }\n\n        return provider.Gateway switch\n        {\n            GatewayType.Stripe => $\"{_stripeUrl}/customers/{provider.GatewayCustomerId}\",\n            GatewayType.PayPal => $\"{_braintreeMerchantUrl}/{_braintreeMerchantId}/${provider.GatewayCustomerId}\",\n            _ => null\n        };\n    }\n\n    private string GetGatewaySubscriptionUrl(Provider provider)\n    {\n        if (!provider.Gateway.HasValue || string.IsNullOrEmpty(provider.GatewaySubscriptionId))\n        {\n            return null;\n        }\n\n        return provider.Gateway switch\n        {\n            GatewayType.Stripe => $\"{_stripeUrl}/subscriptions/{provider.GatewaySubscriptionId}\",\n            GatewayType.PayPal => $\"{_braintreeMerchantUrl}/{_braintreeMerchantId}/subscriptions/${provider.GatewaySubscriptionId}\",\n            _ => null\n        };\n    }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/CreateBusinessUnitProviderModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.SharedWeb.Utilities;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class CreateBusinessUnitProviderModel : IValidatableObject\n{\n    [Display(Name = \"Owner Email\")]\n    public string OwnerEmail { get; set; }\n\n    [Display(Name = \"Enterprise Seat Minimum\")]\n    public int EnterpriseSeatMinimum { get; set; }\n\n    [Display(Name = \"Plan\")]\n    [Required]\n    public PlanType? Plan { get; set; }\n\n    public virtual Provider ToProvider()\n    {\n        return new Provider\n        {\n            Type = ProviderType.BusinessUnit\n        };\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (string.IsNullOrWhiteSpace(OwnerEmail))\n        {\n            var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateBusinessUnitProviderModel>()?.GetName() ?? nameof(OwnerEmail);\n            yield return new ValidationResult($\"The {ownerEmailDisplayName} field is required.\");\n        }\n        if (EnterpriseSeatMinimum < 0)\n        {\n            var enterpriseSeatMinimumDisplayName = nameof(EnterpriseSeatMinimum).GetDisplayAttribute<CreateBusinessUnitProviderModel>()?.GetName() ?? nameof(EnterpriseSeatMinimum);\n            yield return new ValidationResult($\"The {enterpriseSeatMinimumDisplayName} field can not be negative.\");\n        }\n        if (Plan != PlanType.EnterpriseAnnually && Plan != PlanType.EnterpriseMonthly)\n        {\n            var planDisplayName = nameof(Plan).GetDisplayAttribute<CreateBusinessUnitProviderModel>()?.GetName() ?? nameof(Plan);\n            yield return new ValidationResult($\"The {planDisplayName} field must be set to Enterprise Annually or Enterprise Monthly.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/CreateMspProviderModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.SharedWeb.Utilities;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class CreateMspProviderModel : IValidatableObject\n{\n    [Display(Name = \"Owner Email\")]\n    public string OwnerEmail { get; set; }\n\n    [Display(Name = \"Subscription Discount\")]\n    public string DiscountId { get; set; }\n\n    [Display(Name = \"Teams (Monthly) Seat Minimum\")]\n    public int TeamsMonthlySeatMinimum { get; set; }\n\n    [Display(Name = \"Enterprise (Monthly) Seat Minimum\")]\n    public int EnterpriseMonthlySeatMinimum { get; set; }\n\n    public virtual Provider ToProvider()\n    {\n        return new Provider\n        {\n            Type = ProviderType.Msp,\n            DiscountId = DiscountId\n        };\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (string.IsNullOrWhiteSpace(OwnerEmail))\n        {\n            var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(OwnerEmail);\n            yield return new ValidationResult($\"The {ownerEmailDisplayName} field is required.\");\n        }\n        if (TeamsMonthlySeatMinimum < 0)\n        {\n            var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);\n            yield return new ValidationResult($\"The {teamsMinimumSeatsDisplayName} field can not be negative.\");\n        }\n        if (EnterpriseMonthlySeatMinimum < 0)\n        {\n            var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);\n            yield return new ValidationResult($\"The {enterpriseMinimumSeatsDisplayName} field can not be negative.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/CreateProviderModel.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums.Provider;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class CreateProviderModel\n{\n    public ProviderType Type { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.SharedWeb.Utilities;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class CreateResellerProviderModel : IValidatableObject\n{\n    [Display(Name = \"Name\")]\n    public string Name { get; set; }\n\n    [Display(Name = \"Business Name\")]\n    public string BusinessName { get; set; }\n\n    [Display(Name = \"Primary Billing Email\")]\n    public string BillingEmail { get; set; }\n\n    public virtual Provider ToProvider()\n    {\n        return new Provider\n        {\n            Name = Name,\n            BusinessName = BusinessName,\n            BillingEmail = BillingEmail?.ToLowerInvariant().Trim(),\n            Type = ProviderType.Reseller\n        };\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (string.IsNullOrWhiteSpace(Name))\n        {\n            var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);\n            yield return new ValidationResult($\"The {nameDisplayName} field is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(BusinessName))\n        {\n            var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);\n            yield return new ValidationResult($\"The {businessNameDisplayName} field is required.\");\n        }\n        if (string.IsNullOrWhiteSpace(BillingEmail))\n        {\n            var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);\n            yield return new ValidationResult($\"The {billingEmailDisplayName} field is required.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/OrganizationEditModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Net;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Entities;\nusing Bit.SharedWeb.Utilities;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class OrganizationEditModel : OrganizationViewModel, IValidatableObject\n{\n    private readonly List<Plan> _plans;\n\n    public OrganizationEditModel() { }\n\n    public OrganizationEditModel(Provider provider, List<Plan> plans)\n    {\n        Provider = provider;\n        BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty;\n        PlanType = Core.Billing.Enums.PlanType.TeamsMonthly;\n        Plan = Core.Billing.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName();\n        LicenseKey = RandomLicenseKey;\n        _plans = plans;\n    }\n\n    public OrganizationEditModel(\n        Organization org,\n        Provider provider,\n        IEnumerable<OrganizationUserUserDetails> orgUsers,\n        IEnumerable<Cipher> ciphers,\n        IEnumerable<Collection> collections,\n        IEnumerable<Group> groups,\n        IEnumerable<Policy> policies,\n        BillingInfo billingInfo,\n        BillingHistoryInfo billingHistoryInfo,\n        IEnumerable<OrganizationConnection> connections,\n        GlobalSettings globalSettings,\n        List<Plan> plans,\n        int secrets,\n        int projects,\n        int serviceAccounts,\n        int occupiedSmSeats)\n        : base(\n            org,\n            provider,\n            connections,\n            orgUsers,\n            ciphers,\n            collections,\n            groups,\n            policies,\n            secrets,\n            projects,\n            serviceAccounts,\n            occupiedSmSeats)\n    {\n        BillingInfo = billingInfo;\n        BillingHistoryInfo = billingHistoryInfo;\n        BraintreeMerchantId = globalSettings.Braintree.MerchantId;\n\n        Name = org.DisplayName();\n        BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;\n        PlanType = org.PlanType;\n        Plan = org.Plan;\n        Seats = org.Seats;\n        MaxAutoscaleSeats = org.MaxAutoscaleSeats;\n        MaxCollections = org.MaxCollections;\n        UsePolicies = org.UsePolicies;\n        UseSso = org.UseSso;\n        UseKeyConnector = org.UseKeyConnector;\n        UseScim = org.UseScim;\n        UseGroups = org.UseGroups;\n        UseDirectory = org.UseDirectory;\n        UseEvents = org.UseEvents;\n        UseTotp = org.UseTotp;\n        Use2fa = org.Use2fa;\n        UseApi = org.UseApi;\n        UseSecretsManager = org.UseSecretsManager;\n        UseRiskInsights = org.UseRiskInsights;\n        UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies;\n        UseResetPassword = org.UseResetPassword;\n        SelfHost = org.SelfHost;\n        UsersGetPremium = org.UsersGetPremium;\n        UseCustomPermissions = org.UseCustomPermissions;\n        MaxStorageGb = org.MaxStorageGb;\n        Gateway = org.Gateway;\n        GatewayCustomerId = org.GatewayCustomerId;\n        GatewaySubscriptionId = org.GatewaySubscriptionId;\n        Enabled = org.Enabled;\n        LicenseKey = org.LicenseKey;\n        ExpirationDate = org.ExpirationDate;\n        SmSeats = org.SmSeats;\n        MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;\n        SmServiceAccounts = org.SmServiceAccounts;\n        MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;\n        UseOrganizationDomains = org.UseOrganizationDomains;\n        UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation;\n        UseDisableSmAdsForUsers = org.UseDisableSmAdsForUsers;\n        UsePhishingBlocker = org.UsePhishingBlocker;\n        UseMyItems = org.UseMyItems;\n\n        _plans = plans;\n    }\n\n    public BillingInfo BillingInfo { get; set; }\n    public BillingHistoryInfo BillingHistoryInfo { get; set; }\n    public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);\n    public string FourteenDayExpirationDate => DateTime.Now.AddDays(14).ToString(\"yyyy-MM-ddTHH:mm\");\n    public string BraintreeMerchantId { get; set; }\n\n    [Required]\n    [Display(Name = \"Organization Name\")]\n    public string Name { get; set; }\n    [Display(Name = \"Billing Email\")]\n    public string BillingEmail { get; set; }\n    [Required]\n    [Display(Name = \"Plan\")]\n    public PlanType? PlanType { get; set; }\n    [Required]\n    [Display(Name = \"Plan Name\")]\n    public string Plan { get; set; }\n    [Display(Name = \"Seats\")]\n    public int? Seats { get; set; }\n    [Display(Name = \"Max. Autoscale Seats\")]\n    public int? MaxAutoscaleSeats { get; set; }\n    [Display(Name = \"Max. Collections\")]\n    public short? MaxCollections { get; set; }\n    [Display(Name = \"Policies\")]\n    public bool UsePolicies { get; set; }\n    [Display(Name = \"SSO\")]\n    public bool UseSso { get; set; }\n    [Display(Name = \"Key Connector with Customer Encryption\")]\n    public bool UseKeyConnector { get; set; }\n    [Display(Name = \"Groups\")]\n    public bool UseGroups { get; set; }\n    [Display(Name = \"Directory\")]\n    public bool UseDirectory { get; set; }\n    [Display(Name = \"Events\")]\n    public bool UseEvents { get; set; }\n    [Display(Name = \"TOTP\")]\n    public bool UseTotp { get; set; }\n    [Display(Name = \"2FA\")]\n    public bool Use2fa { get; set; }\n    [Display(Name = \"API\")]\n    public bool UseApi { get; set; }\n    [Display(Name = \"Reset Password\")]\n    public bool UseResetPassword { get; set; }\n    [Display(Name = \"SCIM\")]\n    public bool UseScim { get; set; }\n    [Display(Name = \"Secrets Manager\")]\n    public new bool UseSecretsManager { get; set; }\n    [Display(Name = \"Risk Insights\")]\n    public new bool UseRiskInsights { get; set; }\n    [Display(Name = \"Phishing Blocker\")]\n    public new bool UsePhishingBlocker { get; set; }\n    [Display(Name = \"Admin Sponsored Families\")]\n    public bool UseAdminSponsoredFamilies { get; set; }\n    [Display(Name = \"Self Host\")]\n    public bool SelfHost { get; set; }\n    [Display(Name = \"Users Get Premium\")]\n    public bool UsersGetPremium { get; set; }\n    [Display(Name = \"Custom Permissions\")]\n    public bool UseCustomPermissions { get; set; }\n    [Display(Name = \"Max. Storage GB\")]\n    public short? MaxStorageGb { get; set; }\n    [Display(Name = \"Gateway\")]\n    public GatewayType? Gateway { get; set; }\n    [Display(Name = \"Gateway Customer Id\")]\n    public string GatewayCustomerId { get; set; }\n    [Display(Name = \"Gateway Subscription Id\")]\n    public string GatewaySubscriptionId { get; set; }\n    [Display(Name = \"Enabled\")]\n    public bool Enabled { get; set; }\n    [Display(Name = \"License Key\")]\n    public string LicenseKey { get; set; }\n    [Display(Name = \"Expiration Date\")]\n    public DateTime? ExpirationDate { get; set; }\n    public bool SalesAssistedTrialStarted { get; set; }\n    [Display(Name = \"Seats\")]\n    public int? SmSeats { get; set; }\n    [Display(Name = \"Max Autoscale Seats\")]\n    public int? MaxAutoscaleSmSeats { get; set; }\n    [Display(Name = \"Machine Accounts\")]\n    public int? SmServiceAccounts { get; set; }\n    [Display(Name = \"Max Autoscale Machine Accounts\")]\n    public int? MaxAutoscaleSmServiceAccounts { get; set; }\n    [Display(Name = \"Use Organization Domains\")]\n    public bool UseOrganizationDomains { get; set; }\n    [Display(Name = \"Disable SM Ads For Users\")]\n    public new bool UseDisableSmAdsForUsers { get; set; }\n\n    [Display(Name = \"Automatic User Confirmation\")]\n    public bool UseAutomaticUserConfirmation { get; set; }\n    [Display(Name = \"Create My Items for organization ownership\")]\n    public bool UseMyItems { get; set; }\n    /**\n     * Creates a Plan[] object for use in Javascript\n     * This is mapped manually below to provide some type safety in case the plan objects change\n     * Add mappings for individual properties as you need them\n     */\n    public object GetPlansHelper() =>\n        _plans\n            .Select(p =>\n            {\n                var plan = new\n                {\n                    Type = p.Type,\n                    ProductTier = p.ProductTier,\n                    Name = p.Name,\n                    IsAnnual = p.IsAnnual,\n                    NameLocalizationKey = p.NameLocalizationKey,\n                    DescriptionLocalizationKey = p.DescriptionLocalizationKey,\n                    CanBeUsedByBusiness = p.CanBeUsedByBusiness,\n                    TrialPeriodDays = p.TrialPeriodDays,\n                    HasSelfHost = p.HasSelfHost,\n                    HasPolicies = p.HasPolicies,\n                    HasGroups = p.HasGroups,\n                    HasDirectory = p.HasDirectory,\n                    HasEvents = p.HasEvents,\n                    HasTotp = p.HasTotp,\n                    Has2fa = p.Has2fa,\n                    HasApi = p.HasApi,\n                    HasSso = p.HasSso,\n                    HasOrganizationDomains = p.HasOrganizationDomains,\n                    HasKeyConnector = p.HasKeyConnector,\n                    HasScim = p.HasScim,\n                    HasResetPassword = p.HasResetPassword,\n                    UsersGetPremium = p.UsersGetPremium,\n                    HasCustomPermissions = p.HasCustomPermissions,\n                    HasMyItems = p.HasMyItems,\n                    UpgradeSortOrder = p.UpgradeSortOrder,\n                    DisplaySortOrder = p.DisplaySortOrder,\n                    LegacyYear = p.LegacyYear,\n                    Disabled = p.Disabled,\n                    SupportsSecretsManager = p.SupportsSecretsManager,\n                    AutomaticUserConfirmation = p.AutomaticUserConfirmation,\n                    PasswordManager =\n                        new\n                        {\n                            StripePlanId = p.PasswordManager?.StripePlanId,\n                            StripeSeatPlanId = p.PasswordManager?.StripeSeatPlanId,\n                            StripeProviderPortalSeatPlanId = p.PasswordManager?.StripeProviderPortalSeatPlanId,\n                            BasePrice = p.PasswordManager?.BasePrice,\n                            SeatPrice = p.PasswordManager?.SeatPrice,\n                            ProviderPortalSeatPrice = p.PasswordManager?.ProviderPortalSeatPrice,\n                            AllowSeatAutoscale = p.PasswordManager?.AllowSeatAutoscale,\n                            HasAdditionalSeatsOption = p.PasswordManager?.HasAdditionalSeatsOption,\n                            MaxAdditionalSeats = p.PasswordManager?.MaxAdditionalSeats,\n                            BaseSeats = p.PasswordManager?.BaseSeats,\n                            HasPremiumAccessOption = p.PasswordManager?.HasPremiumAccessOption,\n                            StripePremiumAccessPlanId = p.PasswordManager?.StripePremiumAccessPlanId,\n                            PremiumAccessOptionPrice = p.PasswordManager?.PremiumAccessOptionPrice,\n                            MaxSeats = p.PasswordManager?.MaxSeats,\n                            BaseStorageGb = p.PasswordManager?.BaseStorageGb,\n                            HasAdditionalStorageOption = p.PasswordManager?.HasAdditionalStorageOption,\n                            AdditionalStoragePricePerGb = p.PasswordManager?.AdditionalStoragePricePerGb,\n                            StripeStoragePlanId = p.PasswordManager?.StripeStoragePlanId,\n                            MaxAdditionalStorage = p.PasswordManager?.MaxAdditionalStorage,\n                            MaxCollections = p.PasswordManager?.MaxCollections\n                        },\n                    SecretsManager = new\n                    {\n                        MaxServiceAccounts = p.SecretsManager?.MaxServiceAccounts,\n                        AllowServiceAccountsAutoscale = p.SecretsManager?.AllowServiceAccountsAutoscale,\n                        StripeServiceAccountPlanId = p.SecretsManager?.StripeServiceAccountPlanId,\n                        AdditionalPricePerServiceAccount = p.SecretsManager?.AdditionalPricePerServiceAccount,\n                        BaseServiceAccount = p.SecretsManager?.BaseServiceAccount,\n                        MaxAdditionalServiceAccount = p.SecretsManager?.MaxAdditionalServiceAccount,\n                        HasAdditionalServiceAccountOption = p.SecretsManager?.HasAdditionalServiceAccountOption,\n                        StripeSeatPlanId = p.SecretsManager?.StripeSeatPlanId,\n                        HasAdditionalSeatsOption = p.SecretsManager?.HasAdditionalSeatsOption,\n                        BasePrice = p.SecretsManager?.BasePrice,\n                        SeatPrice = p.SecretsManager?.SeatPrice,\n                        BaseSeats = p.SecretsManager?.BaseSeats,\n                        MaxSeats = p.SecretsManager?.MaxSeats,\n                        MaxAdditionalSeats = p.SecretsManager?.MaxAdditionalSeats,\n                        AllowSeatAutoscale = p.SecretsManager?.AllowSeatAutoscale,\n                        MaxProjects = p.SecretsManager?.MaxProjects\n                    }\n                };\n                return plan;\n            });\n\n    public Organization CreateOrganization(Provider provider)\n    {\n        BillingEmail = provider.BillingEmail;\n        return ToOrganization(new Organization());\n    }\n\n    public Organization ToOrganization(Organization existingOrganization)\n    {\n        existingOrganization.Name = WebUtility.HtmlEncode(Name.Trim());\n        existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();\n        existingOrganization.PlanType = PlanType.Value;\n        existingOrganization.Plan = Plan;\n        existingOrganization.Seats = Seats;\n        existingOrganization.MaxCollections = MaxCollections;\n        existingOrganization.UsePolicies = UsePolicies;\n        existingOrganization.UseSso = UseSso;\n        existingOrganization.UseKeyConnector = UseKeyConnector;\n        existingOrganization.UseScim = UseScim;\n        existingOrganization.UseGroups = UseGroups;\n        existingOrganization.UseDirectory = UseDirectory;\n        existingOrganization.UseEvents = UseEvents;\n        existingOrganization.UseTotp = UseTotp;\n        existingOrganization.Use2fa = Use2fa;\n        existingOrganization.UseApi = UseApi;\n        existingOrganization.UseSecretsManager = UseSecretsManager;\n        existingOrganization.UseRiskInsights = UseRiskInsights;\n        existingOrganization.UseAdminSponsoredFamilies = UseAdminSponsoredFamilies;\n        existingOrganization.UseResetPassword = UseResetPassword;\n        existingOrganization.SelfHost = SelfHost;\n        existingOrganization.UsersGetPremium = UsersGetPremium;\n        existingOrganization.UseCustomPermissions = UseCustomPermissions;\n        existingOrganization.MaxStorageGb = MaxStorageGb;\n        existingOrganization.Gateway = Gateway;\n        existingOrganization.GatewayCustomerId = GatewayCustomerId;\n        existingOrganization.GatewaySubscriptionId = GatewaySubscriptionId;\n        existingOrganization.Enabled = Enabled;\n        existingOrganization.LicenseKey = LicenseKey;\n        existingOrganization.ExpirationDate = ExpirationDate;\n        existingOrganization.MaxAutoscaleSeats = MaxAutoscaleSeats;\n        existingOrganization.SmSeats = SmSeats;\n        existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats;\n        existingOrganization.SmServiceAccounts = SmServiceAccounts;\n        existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;\n        existingOrganization.UseOrganizationDomains = UseOrganizationDomains;\n        existingOrganization.UseDisableSmAdsForUsers = UseDisableSmAdsForUsers;\n        existingOrganization.UsePhishingBlocker = UsePhishingBlocker;\n        existingOrganization.UseMyItems = UseMyItems;\n        return existingOrganization;\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (UseMyItems && !UsePolicies)\n        {\n            var displayName = nameof(UseMyItems).GetDisplayAttribute<OrganizationEditModel>()?.GetName() ?? nameof(UseMyItems);\n            yield return new ValidationResult(\n                $\"The {displayName} feature requires Policies to be enabled.\",\n                [nameof(UseMyItems)]);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class OrganizationInitiateDeleteModel\n{\n    [Required]\n    [EmailAddress]\n    [StringLength(256)]\n    [Display(Name = \"Admin Email\")]\n    public string AdminEmail { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/OrganizationSelectableViewModel.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class OrganizationSelectableViewModel : Organization\n{\n    public bool Selected { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/OrganizationUnassignedToProviderSearchViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Admin.Models;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class OrganizationUnassignedToProviderSearchViewModel : PagedModel<OrganizationSelectableViewModel>\n{\n    [Display(Name = \"Organization Name\")]\n    public string OrganizationName { get; set; }\n\n    [Display(Name = \"Owner Email\")]\n    public string OrganizationOwnerEmail { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/OrganizationViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class OrganizationViewModel\n{\n    public OrganizationViewModel()\n    {\n    }\n\n    public OrganizationViewModel(Organization org, Provider provider, IEnumerable<OrganizationConnection> connections,\n        IEnumerable<OrganizationUserUserDetails> orgUsers, IEnumerable<Cipher> ciphers,\n        IEnumerable<Collection> collections,\n        IEnumerable<Group> groups, IEnumerable<Policy> policies, int secretsCount, int projectCount,\n        int serviceAccountsCount,\n        int occupiedSmSeatsCount)\n\n    {\n        Organization = org;\n        Provider = provider;\n        Connections = connections ?? Enumerable.Empty<OrganizationConnection>();\n        HasPublicPrivateKeys = org.PublicKey != null && org.PrivateKey != null;\n        UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);\n        UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);\n        UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);\n        OccupiedSeatCount = UserInvitedCount + UserAcceptedCount + UserConfirmedCount;\n        CipherCount = ciphers.Count();\n        CollectionCount = collections.Count();\n        GroupCount = groups?.Count() ?? 0;\n        PolicyCount = policies?.Count() ?? 0;\n        var organizationUserStatus = org.Status == OrganizationStatusType.Pending\n            ? OrganizationUserStatusType.Invited\n            : OrganizationUserStatusType.Confirmed;\n        Owners = string.Join(\", \",\n            orgUsers\n                .Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus)\n                .Select(u => u.Email));\n        Admins = string.Join(\", \",\n            orgUsers\n                .Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)\n                .Select(u => u.Email));\n        OwnersDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus);\n        AdminsDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus);\n        SecretsCount = secretsCount;\n        ProjectsCount = projectCount;\n        ServiceAccountsCount = serviceAccountsCount;\n        OccupiedSmSeatsCount = occupiedSmSeatsCount;\n    }\n\n    public Organization Organization { get; set; }\n    public Provider Provider { get; set; }\n    public IEnumerable<OrganizationConnection> Connections { get; set; }\n    public string Owners { get; set; }\n    public string Admins { get; set; }\n    public int UserInvitedCount { get; set; }\n    public int UserConfirmedCount { get; set; }\n    public int UserAcceptedCount { get; set; }\n    public int OccupiedSeatCount { get; set; }\n    public int CipherCount { get; set; }\n    public int CollectionCount { get; set; }\n    public int GroupCount { get; set; }\n    public int PolicyCount { get; set; }\n    public bool HasPublicPrivateKeys { get; set; }\n    public int SecretsCount { get; set; }\n    public int ProjectsCount { get; set; }\n    public int ServiceAccountsCount { get; set; }\n    public int OccupiedSmSeatsCount { get; set; }\n    public bool UseSecretsManager => Organization.UseSecretsManager;\n    public bool UseRiskInsights => Organization.UseRiskInsights;\n    public bool UsePhishingBlocker => Organization.UsePhishingBlocker;\n    public bool UseDisableSmAdsForUsers => Organization.UseDisableSmAdsForUsers;\n    public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }\n    public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/OrganizationsModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Admin.Models;\nusing Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class OrganizationsModel : PagedModel<Organization>\n{\n    public string Name { get; set; }\n    public string UserEmail { get; set; }\n    public bool? Paid { get; set; }\n    public string Action { get; set; }\n    public bool SelfHosted { get; set; }\n\n    public double StorageGB(Organization org) => org.Storage.HasValue ? Math.Round(org.Storage.Value / 1073741824D, 2) : 0;\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/ProviderEditModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Enums;\nusing Bit.SharedWeb.Utilities;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class ProviderEditModel : ProviderViewModel, IValidatableObject\n{\n    public ProviderEditModel() { }\n\n    public ProviderEditModel(\n        Provider provider,\n        IEnumerable<ProviderUserUserDetails> providerUsers,\n        IEnumerable<ProviderOrganizationOrganizationDetails> organizations,\n        IReadOnlyCollection<ProviderPlan> providerPlans,\n        bool payByInvoice,\n        string gatewayCustomerUrl = null,\n        string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans)\n    {\n        Name = provider.DisplayName();\n        BusinessName = provider.DisplayBusinessName();\n        BillingEmail = provider.BillingEmail;\n        BillingPhone = provider.BillingPhone;\n        TeamsMonthlySeatMinimum = GetSeatMinimum(providerPlans, PlanType.TeamsMonthly);\n        EnterpriseMonthlySeatMinimum = GetSeatMinimum(providerPlans, PlanType.EnterpriseMonthly);\n        Gateway = provider.Gateway;\n        GatewayCustomerId = provider.GatewayCustomerId;\n        GatewaySubscriptionId = provider.GatewaySubscriptionId;\n        GatewayCustomerUrl = gatewayCustomerUrl;\n        GatewaySubscriptionUrl = gatewaySubscriptionUrl;\n        Type = provider.Type;\n        PayByInvoice = payByInvoice;\n        Enabled = provider.Enabled;\n\n        if (Type == ProviderType.BusinessUnit)\n        {\n            var plan = providerPlans.SingleOrDefault();\n            EnterpriseMinimumSeats = plan?.SeatMinimum ?? 0;\n            Plan = plan?.PlanType;\n        }\n    }\n\n    [Display(Name = \"Billing Email\")]\n    public string BillingEmail { get; set; }\n    [Display(Name = \"Billing Phone Number\")]\n    public string BillingPhone { get; set; }\n    [Display(Name = \"Business Name\")]\n    public string BusinessName { get; set; }\n    public string Name { get; set; }\n    [Display(Name = \"Teams (Monthly) Seat Minimum\")]\n    public int TeamsMonthlySeatMinimum { get; set; }\n\n    [Display(Name = \"Enterprise (Monthly) Seat Minimum\")]\n    public int EnterpriseMonthlySeatMinimum { get; set; }\n    [Display(Name = \"Gateway\")]\n    public GatewayType? Gateway { get; set; }\n    [Display(Name = \"Gateway Customer Id\")]\n    public string GatewayCustomerId { get; set; }\n    [Display(Name = \"Gateway Subscription Id\")]\n    public string GatewaySubscriptionId { get; set; }\n    public string GatewayCustomerUrl { get; }\n    public string GatewaySubscriptionUrl { get; }\n    [Display(Name = \"Pay By Invoice\")]\n    public bool PayByInvoice { get; set; }\n    [Display(Name = \"Provider Type\")]\n    public ProviderType Type { get; set; }\n\n    [Display(Name = \"Plan\")]\n    public PlanType? Plan { get; set; }\n\n    [Display(Name = \"Enterprise Seats Minimum\")]\n    public int? EnterpriseMinimumSeats { get; set; }\n\n    [Display(Name = \"Enabled\")]\n    public bool Enabled { get; set; }\n\n    public virtual Provider ToProvider(Provider existingProvider)\n    {\n        existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();\n        existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();\n        existingProvider.Enabled = Enabled;\n        if (Type.IsStripeSupported())\n        {\n            existingProvider.Gateway = Gateway;\n            existingProvider.GatewayCustomerId = GatewayCustomerId;\n            existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;\n        }\n\n        return existingProvider;\n    }\n\n    private static int GetSeatMinimum(IEnumerable<ProviderPlan> providerPlans, PlanType planType)\n        => providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == planType)?.SeatMinimum ?? 0;\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        switch (Type)\n        {\n            case ProviderType.Reseller:\n                if (string.IsNullOrWhiteSpace(BillingEmail))\n                {\n                    var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);\n                    yield return new ValidationResult($\"The {billingEmailDisplayName} field is required.\");\n                }\n                break;\n            case ProviderType.BusinessUnit:\n                if (Plan == null)\n                {\n                    var displayName = nameof(Plan).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Plan);\n                    yield return new ValidationResult($\"The {displayName} field is required.\");\n                }\n                if (EnterpriseMinimumSeats == null)\n                {\n                    var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);\n                    yield return new ValidationResult($\"The {displayName} field is required.\");\n                }\n                if (EnterpriseMinimumSeats < 0)\n                {\n                    var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);\n                    yield return new ValidationResult($\"The {displayName} field cannot be less than 0.\");\n                }\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/ProviderViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Admin.Billing.Models;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Providers.Entities;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class ProviderViewModel\n{\n    public ProviderViewModel() { }\n\n    public ProviderViewModel(\n        Provider provider,\n        IEnumerable<ProviderUserUserDetails> providerUsers,\n        IEnumerable<ProviderOrganizationOrganizationDetails> organizations,\n        IReadOnlyCollection<ProviderPlan> providerPlans)\n    {\n        Provider = provider;\n        UserCount = providerUsers.Count();\n        ProviderUsers = providerUsers;\n        ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id);\n\n        if (Provider.Type == ProviderType.Msp)\n        {\n            var usedTeamsSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.TeamsMonthly)\n                .Sum(po => po.OccupiedSeats) ?? 0;\n            var teamsProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.TeamsMonthly);\n            if (teamsProviderPlan != null && teamsProviderPlan.IsConfigured())\n            {\n                ProviderPlanViewModels.Add(new ProviderPlanViewModel(\"Teams (Monthly) Subscription\", teamsProviderPlan, usedTeamsSeats));\n            }\n\n            var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly)\n                .Sum(po => po.OccupiedSeats) ?? 0;\n            var enterpriseProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.EnterpriseMonthly);\n            if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured())\n            {\n                ProviderPlanViewModels.Add(new ProviderPlanViewModel(\"Enterprise (Monthly) Subscription\", enterpriseProviderPlan, usedEnterpriseSeats));\n            }\n        }\n        else if (Provider.Type == ProviderType.BusinessUnit)\n        {\n            var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly)\n                .Sum(po => po.OccupiedSeats).GetValueOrDefault(0);\n            var enterpriseProviderPlan = providerPlans.FirstOrDefault();\n            if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured())\n            {\n                var planLabel = enterpriseProviderPlan.PlanType switch\n                {\n                    PlanType.EnterpriseMonthly => \"Enterprise (Monthly) Subscription\",\n                    PlanType.EnterpriseAnnually => \"Enterprise (Annually) Subscription\",\n                    _ => string.Empty\n                };\n\n                ProviderPlanViewModels.Add(new ProviderPlanViewModel(planLabel, enterpriseProviderPlan, usedEnterpriseSeats));\n            }\n        }\n    }\n\n    public int UserCount { get; set; }\n    public Provider Provider { get; set; }\n    public IEnumerable<ProviderUserUserDetails> ProviderUsers { get; set; }\n    public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; }\n    public List<ProviderPlanViewModel> ProviderPlanViewModels { get; set; } = [];\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Models/ProvidersModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Admin.Models;\nusing Bit.Core.AdminConsole.Entities.Provider;\n\nnamespace Bit.Admin.AdminConsole.Models;\n\npublic class ProvidersModel : PagedModel<Provider>\n{\n    public string Name { get; set; }\n    public string UserEmail { get; set; }\n    public bool? Paid { get; set; }\n    public string Action { get; set; }\n    public bool SelfHosted { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Organizations/Connections.cshtml",
    "content": "@using Bit.Core.Enums\n@model OrganizationViewModel\n<h2>Connections</h2>\n<div class=\"row\">\n    <div class=\"col-8\">\n        <div class=\"table-responsive\">\n            <table class=\"table table-striped table-hover\">\n                <thead>\n                    <tr>\n                        <th style=\"width: 190px;\">Type</th>\n                        <th style=\"width: 40px;\">Status</th>\n                        <th style=\"width: 30px;\"></th>\n                    </tr>\n                </thead>\n                <tbody>\n                    @if(!Model.Connections.Any())\n                    {\n                        <tr>\n                            <td colspan=\"6\">No results to list.</td>\n                        </tr>\n                    }\n                    else\n                    {\n                        @foreach(var connection in Model.Connections)\n                        {\n                            <tr>\n                                <td class=\"align-middle\">\n                                    @if(connection.Type == OrganizationConnectionType.CloudBillingSync)\n                                    {\n                                        @:Billing Sync\n                                    }\n                                </td>\n                                <td class=\"align-middle\">\n                                    @if(@TempData[\"ConnectionError\"] != null)\n                                    {\n                                        <span class=\"text-danger\">\n                                            @TempData[\"ConnectionError\"]\n                                        </span>\n                                    }\n                                    else\n                                    {\n                                        @if(connection.Enabled)\n                                        {\n                                            @:Enabled\n                                        }\n                                        else\n                                        {\n                                            @:Disabled\n                                        }\n                                    }\n                                </td>\n                                <td>\n                                    @if(connection.Enabled)\n                                    {\n                                        @if(@TempData[\"ConnectionActivated\"] != null && @TempData[\"ConnectionActivated\"]!.ToString() == @Model.Organization.Id.ToString())\n                                        {\n                                            @if(connection.Type.Equals(OrganizationConnectionType.CloudBillingSync))\n                                            {\n                                                <button class=\"btn btn-outline-success btn-sm disabled\" disabled>Billing Synced!</button>\n                                            }\n                                        }\n                                        else\n                                        {\n                                            @if(connection.Type.Equals(OrganizationConnectionType.CloudBillingSync))\n                                            {\n                                                <a class=\"btn btn-outline-secondary btn-sm\"\n                                                    data-id=\"@connection.Id\" asp-controller=\"Organizations\"\n                                                    asp-action=\"TriggerBillingSync\" asp-route-id=\"@Model.Organization.Id\">\n                                                    Manually Sync\n                                                </a>\n                                            }\n                                        }\n                                    }\n                                </td>\n                            </tr>\n                        }\n                    }\n                </tbody>\n            </table>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Organizations/Edit.cshtml",
    "content": "@using Bit.Admin.Enums;\n@using Bit.Admin.Models\n@using Bit.Core.AdminConsole.Enums.Provider\n@using Bit.Core.Billing.Enums\n@using Bit.Core.Billing.Extensions\n@inject Bit.Admin.Services.IAccessControlService AccessControlService\n@model OrganizationEditModel\n@{\n    ViewData[\"Title\"] = (Model.Provider != null ? \"Client \" : string.Empty) + \"Organization: \" + Model.Name;\n\n    var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);\n    var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);\n    var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial);\n    var canRequestDelete = AccessControlService.UserHasPermission(Permission.Org_RequestDelete);\n    var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);\n    var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);\n\n    var canConvertToBusinessUnit = AccessControlService.UserHasPermission(Permission.Org_Billing_ConvertToBusinessUnit) &&\n                                   Model.Organization.PlanType.GetProductTier() == ProductTierType.Enterprise &&\n                                   !string.IsNullOrEmpty(Model.Organization.GatewaySubscriptionId) &&\n                                   Model.Provider is null or { Type: ProviderType.BusinessUnit, Status: ProviderStatusType.Pending };\n}\n\n@section Scripts {\n    @await Html.PartialAsync(\"~/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml\")\n\n    <script>\n        (() => {\n            const treamsTrialButton = document.getElementById('teams-trial');\n            if (treamsTrialButton != null) {\n                treamsTrialButton.addEventListener('click', () => {\n                    if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {\n                        alert('Organization is not on a free plan.');\n                        return;\n                    }\n                    setTrialDefaults('@((byte)PlanType.TeamsAnnually)');\n                    togglePlanFeatures('@((byte)PlanType.TeamsAnnually)');\n                    document.getElementById('@(nameof(Model.Plan))').value = 'Teams (Trial)';\n                });\n            }\n\n            const entTrialButton = document.getElementById('enterprise-trial');\n            if (entTrialButton != null) {\n                entTrialButton.addEventListener('click', () => {\n                    if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {\n                        alert('Organization is not on a free plan.');\n                        return;\n                    }\n                    setTrialDefaults('@((byte)PlanType.EnterpriseAnnually)');\n                    togglePlanFeatures('@((byte)PlanType.EnterpriseAnnually)');\n                    document.getElementById('@(nameof(Model.Plan))').value = 'Enterprise (Trial)';\n                });\n            }\n\n            const initDeleteButton = document.getElementById('initiate-delete-form');\n            if (initDeleteButton != null) {\n                initDeleteButton.addEventListener('submit', (e) => {\n                    const email = prompt('Enter the email address of the owner/admin that your want to ' +\n                        'request the organization delete verification process with.');\n                    document.getElementById('AdminEmail').value = email;\n                    if (email == null || email === '') {\n                        e.preventDefault();\n                    }\n                });\n            }\n\n            function setTrialDefaults(planType) {\n                // Plan\n                document.getElementById('@(nameof(Model.PlanType))').value = planType;\n                // Password Manager\n                document.getElementById('@(nameof(Model.Seats))').value = '10';\n                document.getElementById('@(nameof(Model.MaxCollections))').value = '';\n                document.getElementById('@(nameof(Model.MaxStorageGb))').value = '1';\n                // Secret Manager\n                if (document.getElementById('@(nameof(Model.UseSecretsManager))').checked) {\n                    document.getElementById('@(nameof(Model.SmSeats))').value = '10';\n                    document.getElementById('@(nameof(Model.SmServiceAccounts))').value = getPlan(planType)?.baseServiceAccount;\n                }\n                // Licensing\n                document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';\n                document.getElementById('@(nameof(Model.ExpirationDate))').value = '@Model.FourteenDayExpirationDate';\n                document.getElementById('@(nameof(Model.SalesAssistedTrialStarted))').value = true;\n            }\n        })();\n    </script>\n}\n\n<h1>@(Model.Provider != null ? \"Client \" : string.Empty)Organization <small>@Model.Name</small></h1>\n\n@if (Model.Provider != null)\n{\n    <h2>Provider Relationship</h2>\n    @await Html.PartialAsync(\"_ProviderInformation\", Model.Provider)\n}\n\n@if (canViewOrganizationInformation)\n{\n    <h2>Organization Information</h2>\n    @await Html.PartialAsync(\"_ViewInformation\", Model)\n}\n\n@if (canViewBillingInformation)\n{\n    <h2>Billing Information</h2>\n    @await Html.PartialAsync(\"_BillingInformation\",\n                new BillingInformationModel { BillingInfo = Model.BillingInfo, BillingHistoryInfo = Model.BillingHistoryInfo, OrganizationId = Model.Organization.Id, Entity = \"Organization\" })\n}\n\n@await Html.PartialAsync(\"~/AdminConsole/Views/Shared/_OrganizationForm.cshtml\", Model)\n\n<div class=\"d-flex mt-4\">\n    <button type=\"submit\" class=\"btn btn-primary\" form=\"edit-form\">Save</button>\n    <div class=\"ms-auto d-flex\">\n        @if (canInitiateTrial && Model.Provider is null)\n        {\n            <button class=\"btn btn-secondary me-2\" type=\"button\" id=\"teams-trial\">\n                Teams Trial\n            </button>\n            <button class=\"btn btn-secondary me-2\" type=\"button\" id=\"enterprise-trial\">\n                Enterprise Trial\n            </button>\n        }\n        @if (canConvertToBusinessUnit)\n        {\n            <a asp-controller=\"BusinessUnitConversion\"\n               asp-action=\"Index\"\n               asp-route-organizationId=\"@Model.Organization.Id\"\n               class=\"btn btn-secondary me-2\">\n                Convert to Business Unit\n            </a>\n        }\n        @if (canUnlinkFromProvider && Model.Provider is not null)\n        {\n            <button class=\"btn btn-outline-danger me-2\"\n                    onclick=\"return unlinkProvider('@Model.Organization.Id');\">\n                Unlink provider\n            </button>\n        }\n        @if (canRequestDelete)\n        {\n            <form asp-action=\"DeleteInitiation\" asp-route-id=\"@Model.Organization.Id\" id=\"initiate-delete-form\">\n                <input type=\"hidden\" name=\"AdminEmail\" id=\"AdminEmail\" />\n                <button class=\"btn btn-danger me-2\" type=\"submit\">Request Delete</button>\n            </form>\n        }\n        @if (canDelete)\n        {\n            <form asp-action=\"Delete\" asp-route-id=\"@Model.Organization.Id\"\n                  onsubmit=\"return confirm('Are you sure you want to hard delete this organization?')\">\n                <button class=\"btn btn-outline-danger\" type=\"submit\">Delete</button>\n            </form>\n        }\n    </div>\n</div>\n\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Organizations/Index.cshtml",
    "content": "﻿@model OrganizationsModel\n@{\n    ViewData[\"Title\"] = \"Organizations\";\n}\n\n<h1>Organizations</h1>\n\n<form class=\"row row-cols-lg-auto g-3 align-items-center mb-2\" method=\"get\">\n    <div class=\"col-12\">\n        <label class=\"visually-hidden\" asp-for=\"Name\">Name</label>\n        <input type=\"text\" class=\"form-control\" placeholder=\"Name\" asp-for=\"Name\" name=\"name\">\n    </div>\n    <div class=\"col-12\">\n        <label class=\"visually-hidden\" asp-for=\"UserEmail\">User email</label>\n        <input type=\"text\" class=\"form-control\" placeholder=\"User email\" asp-for=\"UserEmail\" name=\"userEmail\">\n    </div>\n    @if(!Model.SelfHosted)\n    {\n        <div class=\"col-12\">\n            <label class=\"visually-hidden\" asp-for=\"Paid\">Customer</label>\n            <select class=\"form-select\" asp-for=\"Paid\" name=\"paid\">\n                <option asp-selected=\"!Model.Paid.HasValue\" value=\"\">-- Customer --</option>\n                <option asp-selected=\"Model.Paid.GetValueOrDefault(false)\" value=\"true\">Paid</option>\n                <option asp-selected=\"!Model.Paid.GetValueOrDefault(true)\" value=\"false\">Freeloader</option>\n            </select>\n        </div>\n    }\n    <div class=\"col-12\">\n        <button type=\"submit\" class=\"btn btn-primary\" title=\"Search\">\n            <i class=\"fa fa-search\"></i> Search\n        </button>\n    </div>\n</form>\n\n<div class=\"table-responsive\">\n    <table class=\"table table-striped table-hover\">\n        <thead>\n            <tr>\n                <th>Name</th>\n                <th style=\"width: 190px;\">Plan</th>\n                <th style=\"width: 80px;\">Seats</th>\n                <th style=\"width: 150px;\">Created</th>\n                <th style=\"width: 170px; min-width: 170px;\">Details</th>\n            </tr>\n        </thead>\n        <tbody>\n            @if(!Model.Items.Any())\n            {\n                <tr>\n                    <td colspan=\"5\">No results to list.</td>\n                </tr>\n            }\n            else\n            {\n                @foreach(var org in Model.Items)\n                {\n                    <tr>\n                        <td>\n                            <a asp-action=\"@Model.Action\" asp-route-id=\"@org.Id\">@org.DisplayName()</a>\n                        </td>\n                        <td>\n                            @org.Plan\n                        </td>\n                        <td>\n                            @org.Seats\n                        </td>\n                        <td>\n                            <span title=\"@org.CreationDate.ToString()\">\n                                @org.CreationDate.ToShortDateString()\n                            </span>\n                        </td>\n                        <td>\n                            @if(!Model.SelfHosted)\n                            {\n                                if(!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))\n                                {\n                                    <i class=\"fa fa-usd fa-lg fa-fw\" title=\"Paid\"></i>\n                                }\n                                else\n                                {\n                                    <i class=\"fa fa-smile-o fa-lg fa-fw text-body-secondary\" title=\"Freeloader\"></i>\n                                }\n                            }\n                            <i class=\"fa fa-hdd-o fa-lg fa-fw\" title=\"Used Storage, @Model.StorageGB(org) GB\"></i>\n                            @if(org.Enabled)\n                            {\n                                <i class=\"fa fa-check-circle fa-lg fa-fw\"\n                                   title=\"Enabled, expires @(org.ExpirationDate?.ToShortDateString() ?? \"-\")\"></i>\n                            }\n                            else\n                            {\n                                <i class=\"fa fa-times-circle-o fa-lg fa-fw text-body-secondary\" title=\"Disabled\"></i>\n                            }\n                            @if(org.TwoFactorIsEnabled())\n                            {\n                                <i class=\"fa fa-lock fa-lg fa-fw\" title=\"2FA Enabled\"></i>\n                            }\n                            else\n                            {\n                                <i class=\"fa fa-unlock fa-lg fa-fw text-body-secondary\" title=\"2FA Not Enabled\"></i>\n                            }\n                        </td>\n                    </tr>\n                }\n            }\n        </tbody>\n    </table>\n</div>\n\n<nav>\n    <ul class=\"pagination\">\n        @if(Model.PreviousPage.HasValue)\n        {\n            <li class=\"page-item\">\n                <a class=\"page-link\" asp-action=\"Index\" asp-route-page=\"@Model.PreviousPage.Value\"\n                   asp-route-count=\"@Model.Count\" asp-route-userEmail=\"@Model.UserEmail\"\n                   asp-route-name=\"@Model.Name\" asp-route-paid=\"@Model.Paid\">Previous</a>\n            </li>\n        }\n        else\n        {\n            <li class=\"page-item disabled\">\n                <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Previous</a>\n            </li>\n        }\n        @if(Model.NextPage.HasValue)\n        {\n            <li class=\"page-item\">\n                <a class=\"page-link\" asp-action=\"Index\" asp-route-page=\"@Model.NextPage.Value\"\n                   asp-route-count=\"@Model.Count\" asp-route-userEmail=\"@Model.UserEmail\"\n                   asp-route-name=\"@Model.Name\" asp-route-paid=\"@Model.Paid\">Next</a>\n            </li>\n        }\n        else\n        {\n            <li class=\"page-item disabled\">\n                <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Next</a>\n            </li>\n        }\n    </ul>\n</nav>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Organizations/View.cshtml",
    "content": "﻿@inject Bit.Core.Settings.GlobalSettings GlobalSettings\n@model OrganizationViewModel\n@{\n    ViewData[\"Title\"] = \"Organization: \" + Model.Organization.DisplayName();\n}\n\n<h1>Organization <small>@Model.Organization.DisplayName()</small></h1>\n\n@if (Model.Provider != null)\n{\n    <h2>Provider Relationship</h2>\n    @await Html.PartialAsync(\"_ProviderInformation\", Model.Provider)\n}\n<h2>Information</h2>\n@await Html.PartialAsync(\"_ViewInformation\", Model)\n@if(GlobalSettings.SelfHosted)\n{\n    @await Html.PartialAsync(\"Connections\", Model)\n}\n<form asp-action=\"Delete\" asp-route-id=\"@Model.Organization.Id\"\n      onsubmit=\"return confirm('Are you sure you want to delete this organization?')\">\n    <button class=\"btn btn-danger\" type=\"submit\">Delete</button>\n</form>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Organizations/_ProviderInformation.cshtml",
    "content": "@using Bit.SharedWeb.Utilities\n@model Bit.Core.AdminConsole.Entities.Provider.Provider\n<dl class=\"row\">\n    <dt class=\"col-sm-4 col-lg-3\">Provider Name</dt>\n    <dd class=\"col-sm-8 col-lg-9\">\n        <a asp-controller=\"Providers\" asp-action=\"Edit\" asp-route-id=\"@Model.Id\">@Model.DisplayName()</a>\n    </dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Provider Type</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.Type.GetDisplayAttribute()?.GetName())</dd>\n</dl>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml",
    "content": "@inject Bit.Core.Services.IFeatureService FeatureService\n@model OrganizationViewModel\n\n<dl class=\"row\">\n    <dt class=\"col-sm-4 col-lg-3\">Id</dt>\n    <dd id=\"org-id\" class=\"col-sm-8 col-lg-9\"><code>@Model.Organization.Id</code></dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Plan</dt>\n    <dd id=\"org-plan\" class=\"col-sm-8 col-lg-9\">@Model.Organization.Plan</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Expires</dt>\n    <dd id=\"org-expiration-date\" class=\"col-sm-8 col-lg-9\">@(Model.Organization.ExpirationDate?.ToString() ?? \"-\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Users</dt>\n    <dd id=\"org-user-seats\" class=\"col-sm-8 col-lg-9\">\n        @Model.OccupiedSeatCount / @(Model.Organization.Seats?.ToString() ?? \"-\")\n        (<span id=\"org-invited-users\" title=\"Invited\">@Model.UserInvitedCount</span> /\n        <span id=\"org-accepted-users\" title=\"Accepted\">@Model.UserAcceptedCount</span> /\n        <span id=\"org-confirmed-users\" title=\"Confirmed\">@Model.UserConfirmedCount</span>)\n    </dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Using 2FA</dt>\n    <dd id=\"org-2fa\" class=\"col-sm-8 col-lg-9\">@(Model.Organization.TwoFactorIsEnabled() ? \"Yes\" : \"No\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Groups</dt>\n    <dd id=\"org-group-count\" class=\"col-sm-8 col-lg-9\">@Model.GroupCount</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Policies</dt>\n    <dd id=\"org-policy-count\" class=\"col-sm-8 col-lg-9\">@Model.PolicyCount</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Public/Private Keys</dt>\n    <dd id=\"org-has-keys\" class=\"col-sm-8 col-lg-9\">@(Model.HasPublicPrivateKeys ? \"Yes\" : \"No\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Created</dt>\n    <dd id=\"org-creation-date\" class=\"col-sm-8 col-lg-9\">@Model.Organization.CreationDate.ToString()</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Modified</dt>\n    <dd id=\"org-modified-date\" class=\"col-sm-8 col-lg-9\">@Model.Organization.RevisionDate.ToString()</dd>\n</dl>\n\n<h2>Password Manager</h2>\n<dl class=\"row\">\n    <dt class=\"col-sm-4 col-lg-3\">Items</dt>\n    <dd id=\"pm-item-count\" class=\"col-sm-8 col-lg-9\">@Model.CipherCount</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Collections</dt>\n    <dd id=\"pm-collection-count\" class=\"col-sm-8 col-lg-9\">@Model.CollectionCount</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Administrators manage all collections</dt>\n    <dd id=\"pm-manage-collections\" class=\"col-sm-8 col-lg-9\">@(Model.Organization.AllowAdminAccessToAllCollectionItems ? \"On\" : \"Off\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Limit collection creation to administrators</dt>\n    <dd id=\"pm-collection-creation\" class=\"col-sm-8 col-lg-9\">@(Model.Organization.LimitCollectionCreation ? \"On\" : \"Off\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Limit collection deletion to administrators</dt>\n    <dd id=\"pm-collection-deletion\" class=\"col-sm-8 col-lg-9\">@(Model.Organization.LimitCollectionDeletion ? \"On\" : \"Off\")</dd>\n</dl>\n\n<h2>Secrets Manager</h2>\n<dl class=\"row\">\n    <dt class=\"col-sm-4 col-lg-3\">Secrets</dt>\n    <dd id=\"sm-secret-count\" class=\"col-sm-8 col-lg-9\">@(Model.UseSecretsManager ? Model.SecretsCount: \"N/A\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Projects</dt>\n    <dd id=\"sm-project-count\" class=\"col-sm-8 col-lg-9\">@(Model.UseSecretsManager ? Model.ProjectsCount: \"N/A\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Machine Accounts</dt>\n    <dd id=\"sm-machine-account\" class=\"col-sm-8 col-lg-9\">@(Model.UseSecretsManager ? Model.ServiceAccountsCount: \"N/A\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Secrets Manager Seats</dt>\n    <dd id=\"sm-seat-count\" class=\"col-sm-8 col-lg-9\">@(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: \"N/A\" )</dd>\n</dl>\n\n<h2>Administrators</h2>\n<dl class=\"row\">\n    <div class=\"table-responsive\">\n        <div class=\"col-8\">\n            <table class=\"table table-striped table-hover\">\n                <thead>\n                    <tr>\n                        <th style=\"width: 190px;\">Email</th>\n                        <th style=\"width: 60px;\">Role</th>\n                        <th style=\"width: 40px;\">Status</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    @if(!Model.Admins.Any() && !Model.Owners.Any())\n                    {\n                        <tr>\n                            <td colspan=\"6\">No results to list.</td>\n                        </tr>\n                    }\n                    else\n                    {\n                        @foreach(var owner in Model.OwnersDetails)\n                        {\n                            <tr>\n                                <td class=\"align-middle\">@owner.Email</td>\n                                <td class=\"align-middle\">Owner</td>\n                                <td class=\"align-middle\">@owner.Status</td>\n                            </tr>\n                        }\n\n                        @foreach(var admin in Model.AdminsDetails)\n                        {\n                            <tr>\n                                <td class=\"align-middle\">@admin.Email</td>\n                                <td class=\"align-middle\">Admin</td>\n                                <td class=\"align-middle\">@admin.Status</td>\n                            </tr>\n\n                        }\n                    }\n                </tbody>\n            </table>\n        </div>\n    </div>\n</dl>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/AddExistingOrganization.cshtml",
    "content": "@using Bit.SharedWeb.Utilities\n@model OrganizationUnassignedToProviderSearchViewModel\n\n@{\n    ViewData[\"Title\"] = \"Add Existing Organization\";\n    var providerId = ViewContext.RouteData.Values[\"id\"];\n}\n\n<h1>Add Existing Organization</h1>\n<div class=\"row mb-2\">\n    <div class=\"col\">\n        <form class=\"row g-3 align-items-center mb-2\" method=\"get\" asp-route-id=\"@providerId\">\n            <div class=\"col\">\n                <label class=\"visually-hidden\" asp-for=\"OrganizationName\"></label>\n                <input type=\"text\" class=\"form-control\" placeholder=\"@Html.DisplayNameFor(m => m.OrganizationName)\" asp-for=\"OrganizationName\" name=\"name\">\n            </div>\n            <div class=\"col\">\n                <label class=\"visually-hidden\" asp-for=\"OrganizationOwnerEmail\"></label>\n                <input type=\"email\" class=\"form-control\" placeholder=\"@Html.DisplayNameFor(m => m.OrganizationOwnerEmail)\" asp-for=\"OrganizationOwnerEmail\" name=\"ownerEmail\">\n            </div>\n            <div class=\"col-auto\">\n                <button type=\"submit\" class=\"btn btn-primary\" title=\"Search\" formmethod=\"get\"><i class=\"fa fa-search\"></i> Search</button>\n            </div>\n        </form>\n    </div>\n</div>\n\n<form method=\"post\" id=\"select-form\" asp-route-id=\"@providerId\">\n    <div class=\"table-responsive\">\n        <table class=\"table table-striped table-hover\">\n            <thead>\n            <tr>\n                <th style=\"width: 20px;\">All</th>\n                <th>Name</th>\n                <th style=\"width: 190px;\">Plan</th>\n            </tr>\n            </thead>\n            <tbody>\n            @if (!Model.Items.Any())\n            {\n                <tr>\n                    <td colspan=\"5\">No results to list.</td>\n                </tr>\n            }\n            else\n            {\n                @for (var i = 0; i < Model.Items.Count; i++)\n                {\n                    <tr>\n                        <td class=\"text-center\">\n                            @Html.HiddenFor(m => Model.Items[i].Id, new { @readonly = \"readonly\", autocomplete = \"off\" })\n                            @Html.CheckBoxFor(m => Model.Items[i].Selected)\n                        </td>\n                        <td>@Html.ActionLink(Model.Items[i].DisplayName(), \"Edit\", \"Organizations\", new { id = Model.Items[i].Id }, new { target = \"_blank\" })</td>\n                        <td>@(Model.Items[i].PlanType.GetDisplayAttribute()?.Name ?? Model.Items[i].PlanType.ToString())</td>\n                    </tr>\n                }\n            }\n            </tbody>\n        </table>\n    </div>\n</form>\n\n<div class=\"row\">\n    <div class=\"col\">\n        <nav>\n            <ul class=\"pagination\">\n                @if (Model.PreviousPage.HasValue)\n                {\n                    <li class=\"page-item\">\n                        <a class=\"page-link\" asp-action=\"AddExistingOrganization\" asp-route-id=\"@providerId\" asp-route-page=\"@Model.PreviousPage.Value\"\n                           asp-route-count=\"@Model.Count\" asp-route-ownerEmail=\"@Model.OrganizationOwnerEmail\"\n                           asp-route-name=\"@Model.OrganizationName\">Previous</a>\n                    </li>\n                }\n                else\n                {\n                    <li class=\"page-item disabled\">\n                        <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Previous</a>\n                    </li>\n                }\n                @if (Model.NextPage.HasValue)\n                {\n                    <li class=\"page-item\">\n                        <a class=\"page-link\" asp-action=\"AddExistingOrganization\" asp-route-id=\"@providerId\" asp-route-page=\"@Model.NextPage.Value\"\n                           asp-route-count=\"@Model.Count\" asp-route-ownerEmail=\"@Model.OrganizationOwnerEmail\"\n                           asp-route-name=\"@Model.OrganizationName\">Next</a>\n                    </li>\n                }\n                else\n                {\n                    <li class=\"page-item disabled\">\n                        <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Next</a>\n                    </li>\n                }\n            </ul>\n        </nav>\n    </div>\n    <div class=\"col-auto\">\n        <button type=\"submit\" class=\"btn btn-primary\" form=\"select-form\">Add to Reseller</button>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/Admins.cshtml",
    "content": "@using Bit.Admin.Enums;\n@using Bit.Core.AdminConsole.Enums.Provider\n@inject Bit.Admin.Services.IAccessControlService AccessControlService\n@model ProviderViewModel\n\n@{\n    var canResendEmailInvite = AccessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite);\n}\n\n<h2>Administrators</h2>\n<div class=\"row\">\n    <div class=\"col-8\">\n        <div class=\"table-responsive\">\n            <table class=\"table table-striped table-hover\">\n                <thead>\n                    <tr>\n                        <th style=\"width: 190px;\">Email</th>\n                        <th style=\"width: 160px;\">Role</th>\n                        <th style=\"width: 40px;\">Status</th>\n                        <th style=\"width: 30px;\"></th>\n                    </tr>\n                </thead>\n                <tbody>\n                    @if(!Model.ProviderUsers.Any())\n                    {\n                        <tr>\n                            <td colspan=\"6\">No results to list.</td>\n                        </tr>\n                    }\n                    else\n                    {\n                        @foreach(var user in Model.ProviderUsers)\n                        {\n                            <tr>\n                                <td class=\"align-middle\">\n                                    @user.Email\n                                </td>\n                                <td class=\"align-middle\">\n                                    @if(@user.Type == 0)\n                                    {\n                                        <span>Provider Admin</span>\n                                    }\n                                    else\n                                    {\n                                        <span>Service User</span>\n                                    }\n                                </td>\n                                <td class=\"align-middle\">\n                                    @user.Status\n                                </td>\n                                <td>\n                                    @if(user.Status.Equals(ProviderUserStatusType.Confirmed)\n                                        && @Model.Provider.Status.Equals(ProviderStatusType.Pending)\n                                        && canResendEmailInvite)\n                                    {\n                                        @if(@TempData[\"InviteResentTo\"] != null && @TempData[\"InviteResentTo\"]!.ToString() == @user.UserId!.Value.ToString())\n                                        {\n                                            <button class=\"btn btn-outline-success btn-sm disabled\" disabled>Invite Resent!</button>\n                                        }\n                                        else\n                                        {\n                                            <a class=\"btn btn-outline-secondary btn-sm\"\n                                                data-id=\"@user.Id\" asp-controller=\"Providers\"\n                                                asp-action=\"ResendInvite\" asp-route-ownerId=\"@user.UserId\"\n                                                asp-route-providerId=\"@Model.Provider.Id\">\n                                                Resend Setup Invite\n                                            </a>\n                                        }\n                                    }\n                                </td>\n                            </tr>\n                        }\n                    }\n                </tbody>\n            </table>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/Create.cshtml",
    "content": "﻿@using Bit.SharedWeb.Utilities\n@using Bit.Core.AdminConsole.Enums.Provider\n@using Bit.Core\n\n@model CreateProviderModel\n\n@inject Bit.Core.Services.IFeatureService FeatureService\n\n@{\n    ViewData[\"Title\"] = \"Create Provider\";\n\n    var providerTypes = Enum.GetValues<ProviderType>()\n        .OrderBy(x => x.GetDisplayAttribute().Order)\n        .ToList();\n}\n\n<h1>Create Provider</h1>\n<form method=\"post\" asp-action=\"Create\">\n    <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n    <div class=\"mb-3\">\n        <label asp-for=\"Type\" class=\"form-label h2\"></label>\n        @foreach (var providerType in providerTypes)\n        {\n            var providerTypeValue = (int)providerType;\n            <div class=\"mb-3\">\n                <div class=\"row\">\n                    <div class=\"col\">\n                        <div class=\"form-check\">\n                            @Html.RadioButtonFor(m => m.Type, providerType, new { id = $\"providerType-{providerTypeValue}\", @class = \"form-check-input\" })\n                            @Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = \"form-check-label\", @for = $\"providerType-{providerTypeValue}\" })\n                        </div>\n                    </div>\n                </div>\n                <div class=\"row\">\n                    <div class=\"col\">\n                        @Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = \"form-check-label small text-body-secondary ps-4\", @for = $\"providerType-{providerTypeValue}\" })\n                    </div>\n                </div>\n            </div>\n        }\n    </div>\n    <button type=\"submit\" class=\"btn btn-primary mb-2\">Next</button>\n</form>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/CreateBusinessUnit.cshtml",
    "content": "@using Bit.Core.Billing.Enums\n@using Microsoft.AspNetCore.Mvc.TagHelpers\n\n@model CreateBusinessUnitProviderModel\n\n@{\n    ViewData[\"Title\"] = \"Create Business Unit Provider\";\n}\n\n<h1 class=\"mb-4\">Create Business Unit Provider</h1>\n<div>\n    <form method=\"post\" asp-action=\"CreateBusinessUnit\">\n        <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n        <div class=\"mb-3\">\n            <label asp-for=\"OwnerEmail\" class=\"form-label\"></label>\n            <input type=\"text\" class=\"form-control\" asp-for=\"OwnerEmail\">\n        </div>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    @{\n                        var businessUnitPlanTypes = new List<PlanType>\n                        {\n                            PlanType.EnterpriseAnnually,\n                            PlanType.EnterpriseMonthly\n                        };\n                    }\n                    <label asp-for=\"Plan\" class=\"form-label\"></label>\n                    <select class=\"form-select\" asp-for=\"Plan\" asp-items=\"Html.GetEnumSelectList(businessUnitPlanTypes)\">\n                        <option value=\"\">--</option>\n                    </select>\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"EnterpriseSeatMinimum\" class=\"form-label\"></label>\n                    <input type=\"number\" class=\"form-control\" asp-for=\"EnterpriseSeatMinimum\">\n                </div>\n            </div>\n        </div>\n        <button type=\"submit\" class=\"btn btn-primary\">Create Provider</button>\n    </form>\n</div>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml",
    "content": "@using Bit.Core.Billing.Constants\n@model CreateMspProviderModel\n\n@{\n    ViewData[\"Title\"] = \"Create Managed Service Provider\";\n}\n\n<h1>Create Managed Service Provider</h1>\n<div>\n    <form method=\"post\" asp-action=\"CreateMsp\">\n        <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n        <div class=\"mb-3\">\n            <label asp-for=\"OwnerEmail\" class=\"form-label\"></label>\n            <input type=\"text\" class=\"form-control\" asp-for=\"OwnerEmail\">\n        </div>\n        <div class=\"mb-3\">\n            @{\n                var selectList = new List<SelectListItem>\n                {\n                    new (\"No discount\", string.Empty, true),\n                    new (\"20% - Open\", StripeConstants.CouponIDs.MSPDiscounts.Open),\n                    new (\"35% - Silver\", StripeConstants.CouponIDs.MSPDiscounts.Silver),\n                    new (\"50% - Gold\", StripeConstants.CouponIDs.MSPDiscounts.Gold)\n                };\n            }\n            <label asp-for=\"DiscountId\" class=\"form-label\"></label>\n            <select class=\"form-select\" asp-for=\"DiscountId\" asp-items=\"selectList\"></select>\n        </div>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"TeamsMonthlySeatMinimum\" class=\"form-label\"></label>\n                    <input type=\"number\" class=\"form-control\" asp-for=\"TeamsMonthlySeatMinimum\">\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"EnterpriseMonthlySeatMinimum\" class=\"form-label\"></label>\n                    <input type=\"number\" class=\"form-control\" asp-for=\"EnterpriseMonthlySeatMinimum\">\n                </div>\n            </div>\n        </div>\n        <button type=\"submit\" class=\"btn btn-primary mb-2\">Create Provider</button>\n    </form>\n</div>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml",
    "content": "@model OrganizationEditModel\n@{\n    ViewData[\"Title\"] = \"Create Client Organization\";\n}\n\n@section Scripts {\n    @await Html.PartialAsync(\"~/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml\")\n\n    <script>\n        (() => {\n            togglePlanFeatures('@((byte)Model.PlanType!)');\n        })();\n    </script>\n}\n\n<h1>New Client Organization</h1>\n\n@await Html.PartialAsync(\"~/AdminConsole/Views/Shared/_OrganizationForm.cshtml\", Model)\n<div class=\"d-flex mt-4\">\n    <button type=\"submit\" class=\"btn btn-primary\" form=\"edit-form\">Save</button>\n    <div class=\"ms-auto d-flex\">\n        <form asp-controller=\"Providers\" asp-action=\"Cancel\" asp-route-id=\"@Model.Provider.Id\"\n            onsubmit=\"return confirm('Are you sure you want to cancel?')\">\n            <button class=\"btn btn-outline-secondary\" type=\"submit\">Cancel</button>\n        </form>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml",
    "content": "@model CreateResellerProviderModel\n\n@{\n    ViewData[\"Title\"] = \"Create Reseller Provider\";\n}\n\n<h1>Create Reseller Provider</h1>\n<div>\n    <form class=\"mb-3\" method=\"post\" asp-action=\"CreateReseller\">\n        <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n        <div class=\"mb-3\">\n            <label asp-for=\"Name\" class=\"form-label\"></label>\n            <input type=\"text\" class=\"form-control\" asp-for=\"Name\">\n        </div>\n        <div class=\"mb-3\">\n            <label asp-for=\"BusinessName\" class=\"form-label\"></label>\n            <input type=\"text\" class=\"form-control\" asp-for=\"BusinessName\">\n        </div>\n        <div class=\"mb-3\">\n            <label asp-for=\"BillingEmail\" class=\"form-label\"></label>\n            <input type=\"text\" class=\"form-control\" asp-for=\"BillingEmail\">\n        </div>\n        <button type=\"submit\" class=\"btn btn-primary mb-2\">Create Provider</button>\n    </form>\n</div>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/Edit.cshtml",
    "content": "﻿@inject IAccessControlService AccessControlService\n\n@using Bit.Admin.Enums\n@using Bit.Admin.Services\n@using Bit.Core.AdminConsole.Enums.Provider\n@using Bit.Core.Billing.Enums\n@using Bit.Core.Billing.Extensions\n@using Bit.Core.Enums\n@model ProviderEditModel\n@{\n    ViewData[\"Title\"] = \"Provider: \" + Model.Provider.DisplayName();\n    var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit);\n    var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Provider_CheckEnabledBox);\n}\n\n<h1>Provider <small>@Model.Provider.DisplayName()</small></h1>\n\n<h2>Provider Information</h2>\n@await Html.PartialAsync(\"_ViewInformation\", Model)\n@if (Model.ProviderPlanViewModels.Any())\n{\n    @await Html.PartialAsync(\"~/Billing/Views/Providers/ProviderPlans.cshtml\", Model.ProviderPlanViewModels)\n}\n@await Html.PartialAsync(\"Admins\", Model)\n<form method=\"post\" id=\"edit-form\">\n    <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n    <input type=\"hidden\" asp-for=\"Type\" readonly>\n    <h2>General</h2>\n    <dl class=\"row\">\n        <dt class=\"col-sm-4 col-lg-3\">Name</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@Model.Provider.DisplayName()</dd>\n    </dl>\n    @if (canCheckEnabled && (Model.Provider.Type == ProviderType.Msp || Model.Provider.Type == ProviderType.BusinessUnit))\n    {\n        <div class=\"form-check mb-3\">\n            <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"Enabled\" disabled='@(canCheckEnabled ? null : \"disabled\")'>\n            <label class=\"form-check-label\" asp-for=\"Enabled\"></label>\n        </div>\n    }\n    <h2>Business Information</h2>\n    <dl class=\"row\">\n        <dt class=\"col-sm-4 col-lg-3\">Business Name</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@Model.Provider.DisplayBusinessName()</dd>\n    </dl>\n    <h2>Billing</h2>\n    <div class=\"row\">\n        <div class=\"col-sm\">\n            <div class=\"mb-3\">\n                <label asp-for=\"BillingEmail\" class=\"form-label\"></label>\n                <input type=\"email\" class=\"form-control\" asp-for=\"BillingEmail\" readonly='@(!canEdit)'>\n            </div>\n        </div>\n    </div>\n    <div class=\"row\">\n        <div class=\"col-sm\">\n            <div class=\"mb-3\">\n                <label asp-for=\"BillingPhone\" class=\"form-label\"></label>\n                <input type=\"tel\" class=\"form-control\" asp-for=\"BillingPhone\">\n            </div>\n        </div>\n    </div>\n    @if (Model.Provider.IsBillable())\n    {\n        switch (Model.Provider.Type)\n        {\n            case ProviderType.Msp:\n            {\n                <div class=\"row\">\n                    <div class=\"col-sm\">\n                        <div class=\"mb-3\">\n                            <label asp-for=\"TeamsMonthlySeatMinimum\" class=\"form-label\"></label>\n                            <input type=\"number\" class=\"form-control\" asp-for=\"TeamsMonthlySeatMinimum\">\n                        </div>\n                    </div>\n                    <div class=\"col-sm\">\n                        <div class=\"mb-3\">\n                            <label asp-for=\"EnterpriseMonthlySeatMinimum\" class=\"form-label\"></label>\n                            <input type=\"number\" class=\"form-control\" asp-for=\"EnterpriseMonthlySeatMinimum\">\n                        </div>\n                    </div>\n                </div>\n                break;\n            }\n            case ProviderType.BusinessUnit:\n            {\n                <div class=\"row\">\n                    <div class=\"col-sm\">\n                        <div class=\"mb-3\">\n                            @{\n                                var businessUnitPlanTypes = new List<PlanType>\n                                {\n                                    PlanType.EnterpriseAnnually,\n                                    PlanType.EnterpriseMonthly\n                                };\n                            }\n                            <label asp-for=\"Plan\" class=\"form-label\"></label>\n                            <select class=\"form-control\" asp-for=\"Plan\" asp-items=\"Html.GetEnumSelectList(businessUnitPlanTypes)\">\n                                <option value=\"\">--</option>\n                            </select>\n                        </div>\n                    </div>\n                    <div class=\"col-sm\">\n                        <div class=\"mb-3\">\n                            <label asp-for=\"EnterpriseMinimumSeats\" class=\"form-label\"></label>\n                            <input type=\"number\" class=\"form-control\" asp-for=\"EnterpriseMinimumSeats\">\n                        </div>\n                    </div>\n                </div>\n                break;\n            }\n        }\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"Gateway\" class=\"form-label\"></label>\n                    <select class=\"form-control\" asp-for=\"Gateway\" asp-items=\"Html.GetEnumSelectList<GatewayType>()\">\n                        <option value=\"\">--</option>\n                    </select>\n                </div>\n            </div>\n        </div>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"GatewayCustomerId\" class=\"form-label\"></label>\n                    <div class=\"input-group\">\n                        <input type=\"text\" class=\"form-control\" asp-for=\"GatewayCustomerId\">\n                        <button class=\"btn btn-secondary\" type=\"button\" onclick=\"window.open('@Model.GatewayCustomerUrl', '_blank')\">\n                            <i class=\"fa fa-external-link\"></i>\n                        </button>\n                    </div>\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"GatewaySubscriptionId\" class=\"form-label\"></label>\n                    <div class=\"input-group\">\n                        <input type=\"text\" class=\"form-control\" asp-for=\"GatewaySubscriptionId\">\n                        <button class=\"btn btn-secondary\" type=\"button\" onclick=\"window.open('@Model.GatewaySubscriptionUrl', '_blank')\">\n                            <i class=\"fa fa-external-link\"></i>\n                        </button>\n                    </div>\n                </div>\n            </div>\n        </div>\n        @if (Model.Provider.Type == ProviderType.Msp && Model.Provider.IsBillable())\n        {\n            <div class=\"row\">\n                <div class=\"col-sm\">\n                    <div class=\"form-check mb-3\">\n                        <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"PayByInvoice\">\n                        <label class=\"form-check-label\" asp-for=\"PayByInvoice\"></label>\n                    </div>\n                </div>\n            </div>\n        }\n    }\n</form>\n@await Html.PartialAsync(\"Organizations\", Model)\n@if (canEdit)\n{\n    <!-- Modals -->\n    <div class=\"modal fade rounded\" id=\"requestDeletionModal\" tabindex=\"-1\" aria-labelledby=\"requestDeletionModal\" aria-hidden=\"true\">\n        <div class=\"modal-dialog\">\n            <div class=\"modal-content rounded\">\n                <div class=\"p-3\">\n                    <h4 class=\"fw-bolder\" id=\"exampleModalLabel\">Request provider deletion</h4>\n                </div>\n                <div class=\"modal-body\">\n                    <span class=\"fw-light\">\n                        Enter the email of the provider admin that will receive the request to delete the provider portal.\n                    </span>\n                    <form>\n                        <div class=\"mb-3\">\n                            <label for=\"provider-email\" class=\"col-form-label\">Provider email</label>\n                            <input type=\"email\" class=\"form-control\" id=\"provider-email\">\n                        </div>\n                    </form>\n                </div>\n                <div class=\"modal-footer\">\n                    <button type=\"button\" class=\"btn btn-outline-primary btn-pill\" data-bs-dismiss=\"modal\">Cancel</button>\n                    <button type=\"button\" class=\"btn btn-danger btn-pill\" onclick=\"initiateDeleteProvider('@Model.Provider.Id')\">Send email request</button>\n                </div>\n            </div>\n        </div>\n    </div>\n    <div class=\"modal fade\" id=\"DeleteModal\" tabindex=\"-1\" aria-labelledby=\"DeleteModal\" aria-hidden=\"true\">\n        <div class=\"modal-dialog\">\n            <div class=\"modal-content rounded\">\n                <div class=\"p-3\">\n                    <h4 class=\"fw-bolder\" id=\"exampleModalLabel\">Delete provider</h4>\n                </div>\n\n                @if (Model.Provider.Status == ProviderStatusType.Pending)\n                {\n                    <div class=\"modal-body\">\n                        <span class=\"fw-light\">\n                            This action is permanent and irreversible.\n                        </span>\n                    </div>\n                }\n                else\n                {\n                    <div class=\"modal-body\">\n                        <span class=\"fw-light\">\n                            This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.\n                        </span>\n                        <form>\n                            <div class=\"mb-3\">\n                                <label for=\"provider-name\" class=\"col-form-label\">Provider name</label>\n                                <input type=\"text\" class=\"form-control\" id=\"provider-name\">\n                            </div>\n                        </form>\n                    </div>\n                }\n                <div class=\"modal-footer\">\n                    <button type=\"button\" class=\"btn btn-outline-primary btn-pill\" data-bs-dismiss=\"modal\">Cancel</button>\n                    <button type=\"button\" class=\"btn btn-danger btn-pill\" onclick=\"deleteProvider('@Model.Provider.Id');\">Delete provider</button>\n                </div>\n            </div>\n        </div>\n    </div>\n    <div class=\"modal fade\" id=\"linkedWarningModal\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"linkedWarningModal\" aria-hidden=\"true\">\n        <div class=\"modal-dialog\" role=\"document\">\n            <div class=\"modal-content rounded\">\n                <div class=\"modal-body\">\n                    <h4 class=\"fw-bolder\">Cannot Delete @Model.Name</h4>\n                    <p class=\"fw-lighter\">You must unlink all clients before you can delete @Model.Name.</p>\n                </div>\n                <div class=\"modal-footer\">\n                    <button type=\"button\" class=\"btn btn-outline-primary btn-pill\" data-bs-dismiss=\"modal\">Cancel</button>\n                    <button type=\"button\" class=\"btn btn-primary btn-pill\" data-bs-dismiss=\"modal\">Ok</button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <!-- End of Modal Section -->\n\n    <div class=\"d-flex mt-4\">\n        <button type=\"submit\" class=\"btn btn-primary\" form=\"edit-form\">Save</button>\n        <div class=\"ms-auto d-flex\">\n            <button class=\"btn btn-danger\" onclick=\"openRequestDeleteModal(@Model.ProviderOrganizations.Count())\">Request Delete</button>\n            <button id=\"requestDeletionBtn\" hidden=\"hidden\" data-bs-toggle=\"modal\" data-bs-target=\"#requestDeletionModal\"></button>\n\n            <button class=\"btn btn-outline-danger ms-2\" onclick=\"openDeleteModal(@Model.ProviderOrganizations.Count())\">Delete</button>\n            <button id=\"deleteBtn\" hidden=\"hidden\" data-bs-toggle=\"modal\" data-bs-target=\"#DeleteModal\"></button>\n\n            <button id=\"linkAccWarningBtn\" hidden=\"hidden\" data-bs-toggle=\"modal\" data-bs-target=\"#linkedWarningModal\"></button>\n        </div>\n    </div>\n}\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/Index.cshtml",
    "content": "@using Bit.SharedWeb.Utilities\n@using Bit.Admin.Enums;\n@inject Bit.Admin.Services.IAccessControlService AccessControlService\n\n@model ProvidersModel\n@{\n    ViewData[\"Title\"] = \"Providers\";\n\n    var canCreateProvider = AccessControlService.UserHasPermission(Permission.Provider_Create);\n}\n\n<h1>Providers</h1>\n\n<form class=\"row row-cols-lg-auto g-3 align-items-center mb-2\" method=\"get\">\n    <div class=\"col-12\">\n        <label class=\"visually-hidden\" asp-for=\"Name\">Name</label>\n        <input type=\"text\" class=\"form-control\" placeholder=\"Name\" asp-for=\"Name\" name=\"name\">\n    </div>\n    <div class=\"col-12\">\n        <label class=\"visually-hidden\" asp-for=\"UserEmail\">User email</label>\n        <input type=\"text\" class=\"form-control\" placeholder=\"User email\" asp-for=\"UserEmail\" name=\"userEmail\">\n    </div>\n    <div class=\"col-12\">\n        <button type=\"submit\" class=\"btn btn-primary\" title=\"Search\">\n            <i class=\"fa fa-search\"></i> Search\n        </button>\n    </div>\n    @if (canCreateProvider)\n    {\n        <div class=\"col-auto ms-auto\">\n            <a asp-action=\"Create\" class=\"btn btn-secondary\">Create Provider</a>\n        </div>\n    }\n</form>\n\n<div class=\"table-responsive\">\n    <table class=\"table table-striped table-hover\">\n        <thead>\n            <tr>\n                <th>Name</th>\n                <th style=\"width: 190px;\">Provider Type</th>\n                <th style=\"width: 190px;\">Status</th>\n                <th style=\"width: 150px;\">Created</th>\n            </tr>\n        </thead>\n        <tbody>\n            @if(!Model.Items.Any())\n            {\n                <tr>\n                    <td colspan=\"5\">No results to list.</td>\n                </tr>\n            }\n            else\n            {\n                @foreach(var provider in Model.Items)\n                {\n                    <tr>\n                        <td>\n                            <a asp-action=\"@Model.Action\" asp-route-id=\"@provider.Id\">@(!string.IsNullOrEmpty(provider.DisplayName()) ? provider.DisplayName() : \"Pending\")</a>\n                        </td>\n                        <td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>\n                        <td>@provider.Status</td>\n                        <td>\n                            <span title=\"@provider.CreationDate.ToString()\">\n                                @provider.CreationDate.ToShortDateString()\n                            </span>\n                        </td>\n                    </tr>\n                }\n            }\n        </tbody>\n    </table>\n</div>\n\n<nav>\n    <ul class=\"pagination\">\n        @if(Model.PreviousPage.HasValue)\n        {\n            <li class=\"page-item\">\n                <a class=\"page-link\" asp-action=\"Index\" asp-route-page=\"@Model.PreviousPage.Value\"\n                   asp-route-count=\"@Model.Count\" asp-route-userEmail=\"@Model.UserEmail\"\n                   asp-route-name=\"@Model.Name\" asp-route-paid=\"@Model.Paid\">Previous</a>\n            </li>\n        }\n        else\n        {\n            <li class=\"page-item disabled\">\n                <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Previous</a>\n            </li>\n        }\n        @if(Model.NextPage.HasValue)\n        {\n            <li class=\"page-item\">\n                <a class=\"page-link\" asp-action=\"Index\" asp-route-page=\"@Model.NextPage.Value\"\n                   asp-route-count=\"@Model.Count\" asp-route-userEmail=\"@Model.UserEmail\"\n                   asp-route-name=\"@Model.Name\" asp-route-paid=\"@Model.Paid\">Next</a>\n            </li>\n        }\n        else\n        {\n            <li class=\"page-item disabled\">\n                <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Next</a>\n            </li>\n        }\n    </ul>\n</nav>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/Organizations.cshtml",
    "content": "@using Bit.Core.AdminConsole.Enums.Provider\n@using Microsoft.AspNetCore.Mvc.TagHelpers\n@using Bit.Admin.Enums\n@using Bit.Core.Enums\n@inject Bit.Admin.Services.IAccessControlService AccessControlService\n@model ProviderViewModel\n\n@{\n    var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);\n}\n\n@await Html.PartialAsync(\"_ProviderScripts\")\n@await Html.PartialAsync(\"_ProviderOrganizationScripts\")\n\n<h2>Provider Organizations</h2>\n<div class=\"row\">\n    <div class=\"col-sm\">\n        <div class=\"table-responsive\">\n            <table class=\"table table-striped table-hover\">\n                <thead>\n                    <tr>\n                        <th style=\"width: 50%;\">Name</th>\n                        <th style=\"width: 50%;\">Status</th>\n                        <th>\n                            @if (Model.Provider.Type == ProviderType.Reseller)\n                            {\n                                <div class=\"float-end text-nowrap\">\n                                    <a asp-controller=\"Providers\" asp-action=\"CreateOrganization\" asp-route-providerId=\"@Model.Provider.Id\" class=\"btn btn-sm btn-primary text-decoration-none\">New Organization</a>\n                                    <a asp-controller=\"Providers\" asp-action=\"AddExistingOrganization\" asp-route-id=\"@Model.Provider.Id\" class=\"btn btn-sm btn-outline-primary text-decoration-none\">Add Existing Organization</a>\n                                </div>\n                            }\n                        </th>\n                    </tr>\n                </thead>\n                <tbody>\n                    @if (!Model.ProviderOrganizations.Any())\n                    {\n                        <tr>\n                            <td colspan=\"6\">No results to list.</td>\n                        </tr>\n                    }\n                    else\n                    {\n                        @foreach (var providerOrganization in Model.ProviderOrganizations)\n                        {\n                            <tr>\n                                <td class=\"align-middle\">\n                                    <a asp-controller=\"Organizations\" asp-action=\"Edit\" asp-route-id=\"@providerOrganization.OrganizationId\">@providerOrganization.DisplayName()</a>\n                                </td>\n                                <td>\n                                    @providerOrganization.Status\n                                </td>\n                                <td>\n                                    <div class=\"float-end\">\n                                        @if (canUnlinkFromProvider)\n                                        {\n                                            <a href=\"#\" class=\"text-danger float-end\" onclick=\"return unlinkProvider('@Model.Provider.Id', '@providerOrganization.Id');\">\n                                                Unlink provider\n                                            </a>\n                                        }\n                                        @if (providerOrganization.Status == OrganizationStatusType.Pending)\n                                        {\n                                            <a href=\"#\" class=\"float-end me-3\" onclick=\"return resendOwnerInvite('@providerOrganization.OrganizationId');\">\n                                                Resend invitation\n                                            </a>\n                                        }\n                                    </div>\n                                </td>\n                            </tr>\n                        }\n                    }\n                </tbody>\n            </table>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/View.cshtml",
    "content": "﻿@model ProviderViewModel\n@{\n    ViewData[\"Title\"] = \"Provider: \" + Model.Provider.DisplayName();\n}\n\n<h1>Provider <small>@Model.Provider.DisplayName()</small></h1>\n\n<h2>Information</h2>\n@await Html.PartialAsync(\"_ViewInformation\", Model)\n@if (Model.ProviderPlanViewModels.Any())\n{\n    @await Html.PartialAsync(\"ProviderPlans\", Model.ProviderPlanViewModels)\n}\n@await Html.PartialAsync(\"Admins\", Model)\n@await Html.PartialAsync(\"Organizations\", Model)\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/_ProviderOrganizationScripts.cshtml",
    "content": "﻿<script>\n    function unlinkProvider(providerId, id) {\n        if (confirm('Are you sure you want to unlink this organization from its provider?')) {\n            $.ajax({\n                type: \"POST\",\n                url: `@Url.Action(\"Delete\", \"ProviderOrganizations\")?providerId=${providerId}&id=${id}`,\n                dataType: 'json',\n                contentType: false,\n                processData: false,\n                success: function (response) {\n                    alert(\"Successfully unlinked provider\");\n                    window.location.href = `@Url.Action(\"Edit\", \"Providers\")?id=${providerId}`;\n                },\n                error: function (response) {\n                    alert(\"Error!: \" + response.responseText);\n                }\n            });\n        }\n        return false;\n    }\n</script>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/_ProviderScripts.cshtml",
    "content": "<script>\n    function resendOwnerInvite(orgId) {\n        if (confirm('Resend invite to organization?')) {\n            $.ajax({\n                type: \"POST\",\n                url: '@Url.Action(\"ResendOwnerInvite\", \"Organizations\")' + '?id=' + orgId,\n                dataType: 'json',\n                contentType: false,\n                processData: false,\n                success: function (response) {\n                    alert('Invitation has been resent!');\n                },\n                error: function (response) {\n                    alert(\"Error!\");\n                }\n            });\n        }\n        return false;\n    }\n\n    function deleteProvider(id) {\n        const providerName = $('#DeleteModal input#provider-name').val();\n        const encodedProviderName = encodeURIComponent(providerName);\n        $.ajax({\n            type: \"POST\",\n            url: `@Url.Action(\"Delete\", \"Providers\")?id=${id}&providerName=${encodedProviderName}`,\n            dataType: 'json',\n            contentType: false,\n            processData: false,\n            success: function () {\n                $('#DeleteModal').modal('hide');\n                window.location.href = `@Url.Action(\"Index\", \"Providers\")`;\n            },\n            error: function (response) {\n                alert(\"Error!: \" + response.responseText);\n            }\n        });\n    }\n\n    function initiateDeleteProvider(id) {\n            const email = $('#requestDeletionModal input#provider-email').val();\n            const providerEmail = encodeURIComponent(email);\n            $.ajax({\n                type: \"POST\",\n                url: `@Url.Action(\"DeleteInitiation\", \"Providers\")?id=${id}&providerEmail=${providerEmail}`,\n                dataType: 'json',\n                contentType: false,\n                processData: false,\n                success: function () {\n                    $('#requestDeletionModal').modal('hide');\n                    window.location.href = `@Url.Action(\"Index\", \"Providers\")`;\n                },\n                error: function (response) {\n                    alert(\"Error!: \" + response.responseText);\n                }\n            });\n        }\n\n    function openDeleteModal(providerOrganizations) {\n\n      if (providerOrganizations > 0){\n        $('#linkAccWarningBtn').click()\n      } else {\n         $('#deleteBtn').click()\n      }\n    }\n\n    function openRequestDeleteModal(providerOrganizations) {\n\n        if (providerOrganizations > 0){\n              $('#linkAccWarningBtn').click()\n          } else {\n         $('#requestDeletionBtn').click()\n        }\n    }\n\n</script>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Providers/_ViewInformation.cshtml",
    "content": "﻿@using Bit.SharedWeb.Utilities\n@using Bit.Core.AdminConsole.Enums.Provider\n@model ProviderViewModel\n<dl class=\"row\">\n    <dt class=\"col-sm-4 col-lg-3\">Id</dt>\n    <dd class=\"col-sm-8 col-lg-9\"><code>@Model.Provider.Id</code></dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Status</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@Model.Provider.Status</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Users</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.Provider.Type == ProviderType.Reseller ? \"N/A\" : Model.UserCount)</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Provider Type</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.Provider.Type.GetDisplayAttribute()?.GetName())</dd>\n\n    @if (Model.Provider.Type == ProviderType.Msp || Model.Provider.Type == ProviderType.BusinessUnit)\n    {\n        <dt class=\"col-sm-4 col-lg-3\">Enabled</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@(Model.Provider.Enabled ? \"Yes\" : \"No\")</dd>\n    }\n\n    <dt class=\"col-sm-4 col-lg-3\">Created</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@Model.Provider.CreationDate.ToString()</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Modified</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@Model.Provider.RevisionDate.ToString()</dd>\n</dl>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml",
    "content": "@using Bit.Admin.Enums;\n@using Bit.Core\n@using Bit.Core.Enums\n@using Bit.Core.AdminConsole.Enums.Provider\n@using Bit.Core.Billing.Enums\n@using Bit.SharedWeb.Utilities\n@inject Bit.Admin.Services.IAccessControlService AccessControlService;\n@inject Bit.Core.Services.IFeatureService FeatureService\n\n@model OrganizationEditModel\n\n@{\n    var canViewGeneralDetails = AccessControlService.UserHasPermission(Permission.Org_GeneralDetails_View);\n    var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View);\n    var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View);\n    var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View);\n    var canEditName = AccessControlService.UserHasPermission(Permission.Org_Name_Edit);\n    var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox);\n    var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit);\n    var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit);\n    var canEditBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_Edit);\n    var canLaunchGateway = AccessControlService.UserHasPermission(Permission.Org_Billing_LaunchGateway);\n}\n\n<form method=\"post\" id=\"edit-form\" asp-route-providerId=\"@Model.Provider?.Id\">\n    <input asp-for=\"SalesAssistedTrialStarted\" type=\"hidden\">\n    @if (canViewGeneralDetails)\n    {\n        <h2>General</h2>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"Name\"></label>\n                    <input type=\"text\" class=\"form-control\" asp-for=\"Name\" value=\"@Model.Name\" required disabled=\"@(canEditName ? null : \"disabled\")\">\n                </div>\n            </div>\n        </div>\n\n        @if (Model.Provider?.Type == ProviderType.Reseller)\n        {\n            <div class=\"row\">\n                <div class=\"col-sm\">\n                    <div class=\"mb-3\">\n                        <label class=\"form-label\">Client Owner Email</label>\n                        @if (!string.IsNullOrWhiteSpace(Model.Owners))\n                        {\n                            <input type=\"text\" class=\"form-control\" asp-for=\"Owners\" readonly>\n                        }\n                        else\n                        {\n                            <input type=\"text\" class=\"form-control\" asp-for=\"Owners\" required>\n                        }\n                        <div class=\"form-text mt-0\">This user should be independent of the Provider. If the Provider is disassociated with the organization, this user will maintain ownership of the organization.</div>\n                    </div>\n                </div>\n            </div>\n        }\n        @if (Model.Organization != null)\n        {\n            <div class=\"form-check mb-3\">\n                <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"Enabled\" disabled='@(canCheckEnabled ? null : \"disabled\")'>\n                <label class=\"form-check-label\" asp-for=\"Enabled\"></label>\n            </div>\n        }\n    }\n\n    @if (canViewPlan)\n    {\n        <h2>Plan</h2>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"PlanType\"></label>\n                    @{\n                        var planTypes = Enum.GetValues<PlanType>()\n                            .Where(p =>\n                                (Model.Provider == null ||\n                                p is >= PlanType.TeamsMonthly2019 and <= PlanType.EnterpriseAnnually2019 or\n                                    >= PlanType.TeamsMonthly2020 and <= PlanType.EnterpriseAnnually) &&\n                                (Model.PlanType == PlanType.TeamsStarter || p is not PlanType.TeamsStarter)\n                            )\n                            .Select(e => new SelectListItem\n                            {\n                                Value = ((int)e).ToString(),\n                                Text = e.GetDisplayAttribute()?.GetName() ?? e.ToString()\n                            })\n                            .ToList();\n                    }\n                    <select class=\"form-select\" asp-for=\"PlanType\" asp-items=\"planTypes\" disabled='@(canEditPlan ? null : \"disabled\")'></select>\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"Plan\"></label>\n                    <input type=\"text\" class=\"form-control\" asp-for=\"Plan\" required readonly='@(!canEditPlan)'>\n                </div>\n            </div>\n        </div>\n        <h2>Features</h2>\n        <div class=\"row mb-4\">\n            <div class=\"col-4\">\n                <h3>General</h3>\n                <div class=\"form-check mb-2\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"SelfHost\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"SelfHost\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"Use2fa\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"Use2fa\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseApi\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseApi\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseGroups\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseGroups\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UsePolicies\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UsePolicies\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseSso\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseSso\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseOrganizationDomains\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseOrganizationDomains\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseKeyConnector\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseKeyConnector\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseScim\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseScim\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseDirectory\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseDirectory\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseEvents\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseEvents\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseResetPassword\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseResetPassword\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseCustomPermissions\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseCustomPermissions\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseAdminSponsoredFamilies\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseAdminSponsoredFamilies\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UsePhishingBlocker\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UsePhishingBlocker\"></label>\n                </div>\n                @if(FeatureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n                {\n                    <div class=\"form-check\">\n                        <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseAutomaticUserConfirmation\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                        <label class=\"form-check-label\" asp-for=\"UseAutomaticUserConfirmation\"></label>\n                    </div>\n                }\n            </div>\n            <div class=\"col-3\">\n                <h3>Password Manager</h3>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseTotp\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseTotp\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UsersGetPremium\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UsersGetPremium\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseMyItems\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseMyItems\"></label>\n                </div>\n            </div>\n            <div class=\"col-3\">\n                <h3>Secrets Manager</h3>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseSecretsManager\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseSecretsManager\"></label>\n                </div>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseDisableSmAdsForUsers\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseDisableSmAdsForUsers\"></label>\n                </div>\n            </div>\n            <div class=\"col-2\">\n                <h3>Access Intelligence</h3>\n                <div class=\"form-check\">\n                    <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"UseRiskInsights\" disabled='@(canEditPlan ? null : \"disabled\")'>\n                    <label class=\"form-check-label\" asp-for=\"UseRiskInsights\"></label>\n                </div>\n            </div>\n        </div>\n    }\n\n    @if (canViewPlan)\n    {\n        <h2>Password Manager Configuration</h2>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"Seats\"></label>\n                    <input type=\"number\" class=\"form-control\" asp-for=\"Seats\" min=\"1\" readonly='@(!canEditPlan)'>\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"MaxCollections\"></label>\n                    <input type=\"number\" class=\"form-control\" asp-for=\"MaxCollections\" min=\"1\" readonly='@(!canEditPlan)'>\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"MaxStorageGb\"></label>\n                    <input type=\"number\" class=\"form-control\" asp-for=\"MaxStorageGb\" min=\"1\" readonly='@(!canEditPlan)'>\n                </div>\n            </div>\n        </div>\n        <div class=\"row\">\n            <div class=\"col-4\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"MaxAutoscaleSeats\"></label>\n                    <input type=\"number\" class=\"form-control\" asp-for=\"MaxAutoscaleSeats\" min=\"1\" readonly='@(!canEditPlan)'>\n                </div>\n            </div>\n        </div>\n    }\n\n    @if (canViewPlan)\n    {\n        <div id=\"organization-secrets-configuration\" @(Model.UseSecretsManager ? null : \"lass='d-none'\")>\n            <h2>Secrets Manager Configuration</h2>\n            <div class=\"row\">\n                <div class=\"col-sm\">\n                    <div class=\"mb-3\">\n                        <label class=\"form-label\" asp-for=\"SmSeats\"></label>\n                        <input type=\"number\" class=\"form-control\" asp-for=\"SmSeats\" min=\"1\" readonly='@(!canEditPlan)'>\n                    </div>\n                </div>\n                <div class=\"col-sm\">\n                    <div class=\"mb-3\">\n                        <label class=\"form-label\" asp-for=\"MaxAutoscaleSmSeats\"></label>\n                        <input type=\"number\" class=\"form-control\" asp-for=\"MaxAutoscaleSmSeats\" min=\"1\" readonly='@(!canEditPlan)'>\n                    </div>\n                </div>\n                <div class=\"col-sm\">\n                    <div class=\"mb-3\">\n                        <label class=\"form-label\" asp-for=\"SmServiceAccounts\"></label>\n                        <input type=\"number\" class=\"form-control\" asp-for=\"SmServiceAccounts\" min=\"1\" readonly='@(!canEditPlan)'>\n                    </div>\n                </div>\n            </div>\n            <div class=\"row\">\n                <div class=\"col-4\">\n                    <div class=\"mb-3\">\n                        <label class=\"form-label\" asp-for=\"MaxAutoscaleSmServiceAccounts\"></label>\n                        <input type=\"number\" class=\"form-control\" asp-for=\"MaxAutoscaleSmServiceAccounts\" min=\"1\" readonly='@(!canEditPlan)'>\n                    </div>\n                </div>\n            </div>\n        </div>\n    }\n\n    @if(canViewLicensing)\n    {\n        <h2>Licensing</h2>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"LicenseKey\"></label>\n                    <input type=\"text\" class=\"form-control\" asp-for=\"LicenseKey\" readonly='@(!canEditLicensing)'>\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"ExpirationDate\"></label>\n                    <input type=\"datetime-local\" class=\"form-control\" asp-for=\"ExpirationDate\" readonly='@(!canEditLicensing)' step=\"1\">\n                </div>\n            </div>\n        </div>\n    }\n\n    @if (canViewBilling)\n    {\n        <h2>Billing</h2>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"BillingEmail\"></label>\n                    <input type=\"email\" class=\"form-control\" asp-for=\"BillingEmail\" readonly=\"readonly\">\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"Gateway\"></label>\n                    <select class=\"form-select\" asp-for=\"Gateway\" disabled=\"@(!canEditBilling)\"\n                            asp-items=\"Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()\">\n                        <option value=\"\">--</option>\n                    </select>\n                </div>\n            </div>\n        </div>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"GatewayCustomerId\"></label>\n                    <div class=\"input-group\">\n                        <input type=\"text\" class=\"form-control\" asp-for=\"GatewayCustomerId\" readonly='@(!canEditBilling)'>\n                        @if(canLaunchGateway)\n                        {\n                            <button class=\"btn btn-secondary\" type=\"button\" id=\"gateway-customer-link\">\n                                <i class=\"fa fa-external-link\"></i>\n                            </button>\n                        }\n                    </div>\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label class=\"form-label\" asp-for=\"GatewaySubscriptionId\"></label>\n                    <div class=\"input-group\">\n                        <input type=\"text\" class=\"form-control\" asp-for=\"GatewaySubscriptionId\" readonly='@(!canEditBilling)'>\n                        @if (canLaunchGateway)\n                        {\n                            <button class=\"btn btn-secondary\" type=\"button\" id=\"gateway-subscription-link\">\n                                <i class=\"fa fa-external-link\"></i>\n                            </button>\n                        }\n                    </div>\n                </div>\n            </div>\n        </div>\n    }\n</form>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml",
    "content": "@inject IWebHostEnvironment HostingEnvironment\n@using Bit.Admin.Utilities\n@using Bit.Core.Billing.Enums\n@using Bit.Core.Enums\n@using Bit.Core.Utilities\n@model OrganizationEditModel\n\n<script>\n    (() => {\n        document.getElementById('@(nameof(Model.PlanType))').addEventListener('change', () => {\n            const selectEl = document.getElementById('@(nameof(Model.PlanType))');\n            const selectText = selectEl.options[selectEl.selectedIndex].text;\n            document.getElementById('@(nameof(Model.Plan))').value = selectText;\n            togglePlanFeatures(selectEl.options[selectEl.selectedIndex].value);\n        });\n        document.getElementById('gateway-customer-link')?.addEventListener('click', () => {\n            const gateway = document.getElementById('@(nameof(Model.Gateway))');\n            const customerId = document.getElementById('@(nameof(Model.GatewayCustomerId))');\n            if (!gateway || gateway.value === '' || !customerId || customerId.value === '') {\n                return;\n            }\n            if (gateway.value === '@((byte)GatewayType.Stripe)') {\n                const url = `@(HostingEnvironment.GetStripeUrl())/customers/${customerId.value}/`;\n                window.open(url, '_blank');\n            } else if (gateway.value === '@((byte)GatewayType.Braintree)') {\n                const url = `@(HostingEnvironment.GetBraintreeMerchantUrl())/@Model.BraintreeMerchantId/${customerId.value}`;\n                window.open(url, '_blank');\n            }\n        });\n        document.getElementById('gateway-subscription-link')?.addEventListener('click', () => {\n            const gateway = document.getElementById('@(nameof(Model.Gateway))');\n            const subId = document.getElementById('@(nameof(Model.GatewaySubscriptionId))');\n            if (!gateway || gateway.value === '' || !subId || subId.value === '') {\n                return;\n            }\n            if (gateway.value === '@((byte)GatewayType.Stripe)') {\n                const url = `@(HostingEnvironment.GetStripeUrl())/subscriptions/${subId.value}/`;\n                window.open(url, '_blank');\n            } else if (gateway.value === '@((byte)GatewayType.Braintree)') {\n                const url = `@(HostingEnvironment.GetBraintreeMerchantUrl())/@Model.BraintreeMerchantId/subscriptions/${subId.value}`;\n                window.open(url, '_blank');\n            }\n        });\n        document.getElementById('@(nameof(Model.UseSecretsManager))').addEventListener('change', (event) => {\n            document.getElementById('organization-secrets-configuration').hidden = !event.target.checked;\n\n            if (event.target.checked) {\n                setInitialSecretsManagerConfiguration();\n                return;\n            }\n\n            clearSecretsManagerConfiguration();\n        });\n    })();\n\n    function togglePlanFeatures(planType) {\n        const plan = getPlan(planType);\n\n        if (!plan) {\n            return;\n        }\n\n        console.log(plan);\n\n        // General features\n        document.getElementById('@(nameof(Model.SelfHost))').checked = plan.hasSelfHost;\n        document.getElementById('@(nameof(Model.Use2fa))').checked = plan.has2fa;\n        document.getElementById('@(nameof(Model.UseApi))').checked = plan.hasApi;\n        document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups;\n        document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies;\n        document.getElementById('@(nameof(Model.UseSso))').checked = plan.hasSso;\n        document.getElementById('@(nameof(Model.UseOrganizationDomains))').checked = plan.hasOrganizationDomains;\n        document.getElementById('@(nameof(Model.UseScim))').checked = plan.hasScim;\n        document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory;\n        document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents;\n        document.getElementById('@(nameof(Model.UseResetPassword))').checked = plan.hasResetPassword;\n        document.getElementById('@(nameof(Model.UseCustomPermissions))').checked = plan.hasCustomPermissions;\n        // use key connector is intentionally omitted\n\n        // Password Manager features\n        document.getElementById('@(nameof(Model.UseTotp))').checked = plan.hasTotp;\n        document.getElementById('@(nameof(Model.UsersGetPremium))').checked = plan.usersGetPremium;\n        document.getElementById('@(nameof(Model.UseMyItems))').checked = plan.hasMyItems;\n\n        document.getElementById('@(nameof(Model.MaxStorageGb))').value =\n            document.getElementById('@(nameof(Model.MaxStorageGb))').value ||\n            plan.passwordManager.baseStorageGb ||\n            1;\n        document.getElementById('@(nameof(Model.Seats))').value = document.getElementById('@(nameof(Model.Seats))').value ||\n            plan.passwordManager.baseSeats ||\n            1;\n    }\n\n    function unlinkProvider(id) {\n        if (confirm('Are you sure you want to unlink this organization from its provider?')) {\n            $.ajax({\n                type: \"POST\",\n                url: `@Url.Action(\"UnlinkOrganizationFromProvider\", \"Organizations\")?id=${id}`,\n                dataType: 'json',\n                contentType: false,\n                processData: false,\n                success: function (response) {\n                    alert(\"Successfully unlinked provider\");\n                    window.location.href = `@Url.Action(\"Edit\", \"Organizations\")?id=${id}`;\n                },\n                error: function (response) {\n                    alert(\"Error!\");\n                }\n            });\n        }\n        return false;\n    }\n\n    /***\n    * Set Secrets Manager values based on current usage (for migrating from SM beta or reinstating an old subscription)\n    */\n    function setInitialSecretsManagerConfiguration() {\n        const planType = document.getElementById('@(nameof(Model.PlanType))').value;\n\n        // Seats\n        document.getElementById('@(nameof(Model.SmSeats))').value = Math.max(@Model.OccupiedSmSeatsCount, 1);\n\n        // Service accounts\n        const baseServiceAccounts = getPlan(planType)?.secretsManager?.baseServiceAccount ?? 0;\n        if (planType !== '@((byte)PlanType.Free)' && @Model.ServiceAccountsCount > baseServiceAccounts) {\n            document.getElementById('@(nameof(Model.SmServiceAccounts))').value = @Model.ServiceAccountsCount;\n        } else {\n            document.getElementById('@(nameof(Model.SmServiceAccounts))').value = baseServiceAccounts;\n        }\n\n        // Clear autoscale values (no defaults)\n        document.getElementById('@(nameof(Model.MaxAutoscaleSmSeats))').value = '';\n        document.getElementById('@(nameof(Model.MaxAutoscaleSmServiceAccounts))').value = '';\n    }\n\n    function clearSecretsManagerConfiguration() {\n        document.getElementById('@(nameof(Model.SmSeats))').value = '';\n        document.getElementById('@(nameof(Model.SmServiceAccounts))').value = '';\n        document.getElementById('@(nameof(Model.MaxAutoscaleSmSeats))').value = '';\n        document.getElementById('@(nameof(Model.MaxAutoscaleSmServiceAccounts))').value = '';\n    }\n\n    function getPlan(planType) {\n        const plans = @Html.Raw(Json.Serialize(Model.GetPlansHelper()));\n        return plans.find(p => p.type == planType);\n    }\n</script>\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/_ViewImports.cshtml",
    "content": "@using Microsoft.AspNetCore.Identity\n@using Bit.Admin.AdminConsole\n@using Bit.Admin.AdminConsole.Models\n@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers\n@addTagHelper \"*, Admin\"\n"
  },
  {
    "path": "src/Admin/AdminConsole/Views/_ViewStart.cshtml",
    "content": "﻿@{\n    Layout = \"_Layout\";\n}\n"
  },
  {
    "path": "src/Admin/AdminSettings.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Admin;\n\npublic class AdminSettings\n{\n    public virtual string Admins { get; set; }\n    public int? DeleteTrashDaysAgo { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/Auth/Controllers/LoginController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Admin.Auth.IdentityServer;\nusing Bit.Admin.Auth.Models;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Admin.Auth.Controllers;\n\npublic class LoginController : Controller\n{\n    private readonly PasswordlessSignInManager<IdentityUser> _signInManager;\n\n    public LoginController(\n        PasswordlessSignInManager<IdentityUser> signInManager)\n    {\n        _signInManager = signInManager;\n    }\n\n    public IActionResult Index(string returnUrl = null, int? error = null, int? success = null,\n        bool accessDenied = false)\n    {\n        if (!error.HasValue && accessDenied)\n        {\n            error = 4;\n        }\n\n        return View(new LoginModel\n        {\n            ReturnUrl = returnUrl,\n            Error = GetMessage(error),\n            Success = GetMessage(success)\n        });\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    public async Task<IActionResult> Index(LoginModel model)\n    {\n        if (ModelState.IsValid)\n        {\n            await _signInManager.PasswordlessSignInAsync(model.Email, model.ReturnUrl);\n            return RedirectToAction(\"Index\", new\n            {\n                success = 3\n            });\n        }\n\n        return View(model);\n    }\n\n    public async Task<IActionResult> Confirm(string email, string token, string returnUrl)\n    {\n        var result = await _signInManager.PasswordlessSignInAsync(email, token, true);\n        if (!result.Succeeded)\n        {\n            return RedirectToAction(\"Index\", new\n            {\n                error = 2\n            });\n        }\n\n        if (!string.IsNullOrWhiteSpace(returnUrl) && Url.IsLocalUrl(returnUrl))\n        {\n            return Redirect(returnUrl);\n        }\n\n        return RedirectToAction(\"Index\", \"Home\");\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    public async Task<IActionResult> Logout()\n    {\n        await _signInManager.SignOutAsync();\n        return RedirectToAction(\"Index\", new\n        {\n            success = 1\n        });\n    }\n\n    private string GetMessage(int? messageCode)\n    {\n        return messageCode switch\n        {\n            1 => \"You have been logged out.\",\n            2 => \"This login confirmation link is invalid. Try logging in again.\",\n            3 => \"If a valid admin user with this email address exists, \" +\n                \"we've sent you an email with a secure link to log in.\",\n            4 => \"Access denied. Please log in.\",\n            _ => null,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Admin/Auth/IdentityServer/PasswordlessSignInManager.cs",
    "content": "﻿using Bit.Core.Services;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Admin.Auth.IdentityServer;\n\npublic class PasswordlessSignInManager<TUser> : SignInManager<TUser> where TUser : class\n{\n    public const string PasswordlessSignInPurpose = \"PasswordlessSignIn\";\n\n    private readonly IMailService _mailService;\n\n    public PasswordlessSignInManager(UserManager<TUser> userManager,\n        IHttpContextAccessor contextAccessor,\n        IUserClaimsPrincipalFactory<TUser> claimsFactory,\n        IOptions<IdentityOptions> optionsAccessor,\n        ILogger<SignInManager<TUser>> logger,\n        IAuthenticationSchemeProvider schemes,\n        IUserConfirmation<TUser> confirmation,\n        IMailService mailService)\n        : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)\n    {\n        _mailService = mailService;\n    }\n\n    public async Task<SignInResult> PasswordlessSignInAsync(string email, string returnUrl)\n    {\n        var user = await UserManager.FindByEmailAsync(email);\n        if (user == null)\n        {\n            return SignInResult.Failed;\n        }\n\n        var token = await UserManager.GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,\n            PasswordlessSignInPurpose);\n        await _mailService.SendPasswordlessSignInAsync(returnUrl, token, email);\n        return SignInResult.Success;\n    }\n\n    public async Task<SignInResult> PasswordlessSignInAsync(TUser user, string token, bool isPersistent)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        var attempt = await CheckPasswordlessSignInAsync(user, token);\n        return attempt.Succeeded ?\n            await SignInOrTwoFactorAsync(user, isPersistent, bypassTwoFactor: true) : attempt;\n    }\n\n    public async Task<SignInResult> PasswordlessSignInAsync(string email, string token, bool isPersistent)\n    {\n        var user = await UserManager.FindByEmailAsync(email);\n        if (user == null)\n        {\n            return SignInResult.Failed;\n        }\n\n        return await PasswordlessSignInAsync(user, token, isPersistent);\n    }\n\n    public virtual async Task<SignInResult> CheckPasswordlessSignInAsync(TUser user, string token)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        var error = await PreSignInCheck(user);\n        if (error != null)\n        {\n            return error;\n        }\n\n        if (await UserManager.VerifyUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,\n            PasswordlessSignInPurpose, token))\n        {\n            return SignInResult.Success;\n        }\n\n        Logger.LogWarning(2, \"User {userId} failed to provide the correct token.\",\n            await UserManager.GetUserIdAsync(user));\n        return SignInResult.Failed;\n    }\n}\n"
  },
  {
    "path": "src/Admin/Auth/Jobs/DatabaseExpiredGrantsJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Jobs;\nusing Bit.Core.Repositories;\nusing Quartz;\n\nnamespace Bit.Admin.Auth.Jobs;\n\npublic class DatabaseExpiredGrantsJob : BaseJob\n{\n    private readonly IMaintenanceRepository _maintenanceRepository;\n\n    public DatabaseExpiredGrantsJob(\n        IMaintenanceRepository maintenanceRepository,\n        ILogger<DatabaseExpiredGrantsJob> logger)\n        : base(logger)\n    {\n        _maintenanceRepository = maintenanceRepository;\n    }\n\n    protected async override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: DeleteExpiredGrantsAsync\");\n        await _maintenanceRepository.DeleteExpiredGrantsAsync();\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Finished job task: DeleteExpiredGrantsAsync\");\n    }\n}\n"
  },
  {
    "path": "src/Admin/Auth/Jobs/DeleteAuthRequestsJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Jobs;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Quartz;\n\nnamespace Bit.Admin.Auth.Jobs;\n\npublic class DeleteAuthRequestsJob : BaseJob\n{\n    private readonly IAuthRequestRepository _authRepo;\n    private readonly IGlobalSettings _globalSettings;\n\n    public DeleteAuthRequestsJob(\n        IAuthRequestRepository authrepo,\n        IGlobalSettings globalSettings,\n        ILogger<DeleteAuthRequestsJob> logger)\n        : base(logger)\n    {\n        _authRepo = authrepo;\n        _globalSettings = globalSettings;\n    }\n\n    protected async override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: DeleteAuthRequestsJob: Start\");\n        var count = await _authRepo.DeleteExpiredAsync(\n            _globalSettings.PasswordlessAuth.UserRequestExpiration,\n            _globalSettings.PasswordlessAuth.AdminRequestExpiration,\n            _globalSettings.PasswordlessAuth.AfterAdminApprovalExpiration);\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"{Count} records deleted from AuthRequests.\", count);\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: DeleteAuthRequestsJob: End\");\n    }\n}\n"
  },
  {
    "path": "src/Admin/Auth/Models/LoginModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Admin.Auth.Models;\n\npublic class LoginModel\n{\n    [Required]\n    [EmailAddress]\n    public string Email { get; set; }\n    public string ReturnUrl { get; set; }\n    public string Error { get; set; }\n    public string Success { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/Auth/Views/Login/Index.cshtml",
    "content": "﻿@model LoginModel\n@{\n    ViewData[\"Title\"] = \"Login\";\n}\n\n<div class=\"row justify-content-center\">\n    <div class=\"col-lg-6 col-md-8\">\n        @if(!string.IsNullOrWhiteSpace(Model.Success))\n        {\n            <div class=\"alert alert-success\" role=\"alert\">@Model.Success</div>\n        }\n        else if(!string.IsNullOrWhiteSpace(Model.Error))\n        {\n            <div class=\"alert alert-danger\" role=\"alert\">@Model.Error</div>\n        }\n        <div class=\"card\">\n            <div class=\"card-body\">\n                <p>Please enter your email address below to log in.</p>\n                <form asp-action=\"\" method=\"post\">\n                    <input type=\"hidden\" asp-for=\"ReturnUrl\" />\n                    <div asp-validation-summary=\"ModelOnly\" class=\"text-danger\"></div>\n                    <div class=\"mb-3\">\n                        <label asp-for=\"Email\" class=\"visually-hidden\">Email Address</label>\n                        <input asp-for=\"Email\" type=\"email\" class=\"form-control\" placeholder=\"ex. john@example.com\"\n                               required autofocus>\n                        <span asp-validation-for=\"Email\" class=\"invalid-feedback\"></span>\n                        <div class=\"form-text\">We'll email you a secure login link.</div>\n                    </div>\n                    <button class=\"btn btn-primary\" type=\"submit\">Continue</button>\n                </form>\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Admin/Auth/Views/_ViewImports.cshtml",
    "content": "@using Microsoft.AspNetCore.Identity\n@using Bit.Admin.Auth\n@using Bit.Admin.Auth.Models\n@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers\n@addTagHelper \"*, Admin\"\n"
  },
  {
    "path": "src/Admin/Auth/Views/_ViewStart.cshtml",
    "content": "﻿@{\n    Layout = \"_Layout\";\n}\n"
  },
  {
    "path": "src/Admin/Billing/Controllers/BusinessUnitConversionController.cs",
    "content": "﻿#nullable enable\nusing Bit.Admin.Billing.Models;\nusing Bit.Admin.Enums;\nusing Bit.Admin.Utilities;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Admin.Billing.Controllers;\n\n[Authorize]\n[Route(\"organizations/billing/{organizationId:guid}/business-unit\")]\npublic class BusinessUnitConversionController(\n    IBusinessUnitConverter businessUnitConverter,\n    IOrganizationRepository organizationRepository,\n    IProviderRepository providerRepository,\n    IProviderUserRepository providerUserRepository) : Controller\n{\n    [HttpGet]\n    [RequirePermission(Permission.Org_Billing_ConvertToBusinessUnit)]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> IndexAsync([FromRoute] Guid organizationId)\n    {\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var model = new BusinessUnitConversionModel { Organization = organization };\n\n        var invitedProviderAdmin = await GetInvitedProviderAdminAsync(organization);\n\n        if (invitedProviderAdmin != null)\n        {\n            model.ProviderAdminEmail = invitedProviderAdmin.Email;\n            model.ProviderId = invitedProviderAdmin.ProviderId;\n        }\n\n        var success = ReadSuccessMessage();\n\n        if (!string.IsNullOrEmpty(success))\n        {\n            model.Success = success;\n        }\n\n        var errors = ReadErrorMessages();\n\n        if (errors is { Count: > 0 })\n        {\n            model.Errors = errors;\n        }\n\n        return View(model);\n    }\n\n    [HttpPost]\n    [RequirePermission(Permission.Org_Billing_ConvertToBusinessUnit)]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> InitiateAsync(\n        [FromRoute] Guid organizationId,\n        BusinessUnitConversionModel model)\n    {\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await businessUnitConverter.InitiateConversion(\n            organization,\n            model.ProviderAdminEmail!);\n\n        return result.Match(\n            providerId => RedirectToAction(\"Edit\", \"Providers\", new { id = providerId }),\n            errors =>\n            {\n                PersistErrorMessages(errors);\n                return RedirectToAction(\"Index\", new { organizationId });\n            });\n    }\n\n    [HttpPost(\"reset\")]\n    [RequirePermission(Permission.Org_Billing_ConvertToBusinessUnit)]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> ResetAsync(\n        [FromRoute] Guid organizationId,\n        BusinessUnitConversionModel model)\n    {\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await businessUnitConverter.ResetConversion(organization, model.ProviderAdminEmail!);\n\n        PersistSuccessMessage(\"Business unit conversion was successfully reset.\");\n\n        return RedirectToAction(\"Index\", new { organizationId });\n    }\n\n    [HttpPost(\"resend-invite\")]\n    [RequirePermission(Permission.Org_Billing_ConvertToBusinessUnit)]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> ResendInviteAsync(\n        [FromRoute] Guid organizationId,\n        BusinessUnitConversionModel model)\n    {\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await businessUnitConverter.ResendConversionInvite(organization, model.ProviderAdminEmail!);\n\n        PersistSuccessMessage($\"Invite was successfully resent to {model.ProviderAdminEmail}.\");\n\n        return RedirectToAction(\"Index\", new { organizationId });\n    }\n\n    private async Task<ProviderUser?> GetInvitedProviderAdminAsync(\n        Organization organization)\n    {\n        var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);\n\n        if (provider is not\n            {\n                Type: ProviderType.BusinessUnit,\n                Status: ProviderStatusType.Pending\n            })\n        {\n            return null;\n        }\n\n        var providerUsers =\n            await providerUserRepository.GetManyByProviderAsync(provider.Id, ProviderUserType.ProviderAdmin);\n\n        if (providerUsers.Count != 1)\n        {\n            return null;\n        }\n\n        var providerUser = providerUsers.First();\n\n        return providerUser is\n        {\n            Type: ProviderUserType.ProviderAdmin,\n            Status: ProviderUserStatusType.Invited,\n            UserId: not null\n        } ? providerUser : null;\n    }\n\n    private const string _errors = \"errors\";\n    private const string _success = \"Success\";\n\n    private void PersistSuccessMessage(string message) => TempData[_success] = message;\n    private void PersistErrorMessages(List<string> errors)\n    {\n        var input = string.Join(\"|\", errors);\n        TempData[_errors] = input;\n    }\n    private string? ReadSuccessMessage() => ReadTempData<string>(_success);\n    private List<string>? ReadErrorMessages()\n    {\n        var output = ReadTempData<string>(_errors);\n        return string.IsNullOrEmpty(output) ? null : output.Split('|').ToList();\n    }\n\n    private T? ReadTempData<T>(string key) => TempData.TryGetValue(key, out var obj) && obj is T value ? value : default;\n}\n"
  },
  {
    "path": "src/Admin/Billing/Controllers/ProcessStripeEventsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Admin.Billing.Models.ProcessStripeEvents;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Admin.Billing.Controllers;\n\n[Authorize]\n[Route(\"process-stripe-events\")]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class ProcessStripeEventsController(\n    IHttpClientFactory httpClientFactory,\n    IGlobalSettings globalSettings) : Controller\n{\n    [HttpGet]\n    public ActionResult Index()\n    {\n        return View(new EventsFormModel());\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    public async Task<IActionResult> ProcessAsync([FromForm] EventsFormModel model)\n    {\n        var eventIds = model.GetEventIds();\n\n        const string baseEndpoint = \"stripe/recovery/events\";\n\n        var endpoint = model.Inspect ? $\"{baseEndpoint}/inspect\" : $\"{baseEndpoint}/process\";\n\n        var (response, failedResponseMessage) = await PostAsync(endpoint, new EventsRequestBody\n        {\n            EventIds = eventIds\n        });\n\n        if (response == null)\n        {\n            return StatusCode((int)failedResponseMessage.StatusCode, \"An error occurred during your request.\");\n        }\n\n        response.ActionType = model.Inspect ? EventActionType.Inspect : EventActionType.Process;\n\n        return View(\"Results\", response);\n    }\n\n    private async Task<(EventsResponseBody, HttpResponseMessage)> PostAsync(\n        string endpoint,\n        EventsRequestBody requestModel)\n    {\n        var client = httpClientFactory.CreateClient(\"InternalBilling\");\n        client.BaseAddress = new Uri(globalSettings.BaseServiceUri.InternalBilling);\n\n        var json = JsonSerializer.Serialize(requestModel);\n        var requestBody = new StringContent(json, System.Text.Encoding.UTF8, \"application/json\");\n\n        var responseMessage = await client.PostAsync(endpoint, requestBody);\n\n        if (!responseMessage.IsSuccessStatusCode)\n        {\n            return (null, responseMessage);\n        }\n\n        var responseContent = await responseMessage.Content.ReadAsStringAsync();\n\n        var response = JsonSerializer.Deserialize<EventsResponseBody>(responseContent);\n\n        return (response, null);\n    }\n}\n"
  },
  {
    "path": "src/Admin/Billing/Controllers/SubscriptionDiscountsController.cs",
    "content": "﻿using Bit.Admin.Billing.Models;\nusing Bit.Admin.Enums;\nusing Bit.Admin.Utilities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Billing.Subscriptions.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Stripe;\n\nnamespace Bit.Admin.Billing.Controllers;\n\n[Authorize]\n[Route(\"subscription-discounts\")]\npublic class SubscriptionDiscountsController(\n    ISubscriptionDiscountRepository subscriptionDiscountRepository,\n    IStripeAdapter stripeAdapter,\n    ILogger<SubscriptionDiscountsController> logger) : Controller\n{\n    private const string SuccessKey = \"Success\";\n    private const string ErrorKey = \"Error\";\n\n    [HttpGet]\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public async Task<IActionResult> Index(int page = 1, int count = 25)\n    {\n        if (page < 1)\n        {\n            page = 1;\n        }\n\n        if (count < 1)\n        {\n            count = 1;\n        }\n\n        var skip = (page - 1) * count;\n        var discounts = await subscriptionDiscountRepository.ListAsync(skip, count);\n\n        var discountViewModels = discounts.Select(d => new SubscriptionDiscountViewModel\n        {\n            Id = d.Id,\n            StripeCouponId = d.StripeCouponId,\n            Name = d.Name,\n            PercentOff = d.PercentOff,\n            AmountOff = d.AmountOff,\n            Currency = d.Currency,\n            Duration = d.Duration,\n            DurationInMonths = d.DurationInMonths,\n            StartDate = d.StartDate,\n            EndDate = d.EndDate,\n            AudienceType = d.AudienceType,\n            CreationDate = d.CreationDate\n        }).ToList();\n\n        var model = new SubscriptionDiscountPagedModel\n        {\n            Items = discountViewModels,\n            Page = page,\n            Count = count\n        };\n\n        return View(model);\n    }\n\n    [HttpGet(\"create\")]\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public IActionResult Create()\n    {\n        return View(new CreateSubscriptionDiscountModel());\n    }\n\n    [HttpPost(\"import-coupon\")]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public async Task<IActionResult> ImportCoupon(CreateSubscriptionDiscountModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(\"Create\", model);\n        }\n\n        try\n        {\n            var existing = await subscriptionDiscountRepository.GetByStripeCouponIdAsync(model.StripeCouponId);\n            if (existing != null)\n            {\n                ModelState.AddModelError(nameof(model.StripeCouponId),\n                    \"This coupon has already been imported.\");\n                return View(\"Create\", model);\n            }\n\n            Coupon coupon;\n            try\n            {\n                var options = new CouponGetOptions();\n                options.AddExpand(StripeConstants.CouponExpandablePropertyNames.AppliesTo);\n                coupon = await stripeAdapter.GetCouponAsync(model.StripeCouponId, options);\n            }\n            catch (StripeException ex)\n            {\n                var errorMessage = ex.StripeError?.Code == \"resource_missing\"\n                    ? \"Coupon not found in Stripe. Please verify the coupon ID.\"\n                    : \"An error occurred while fetching the coupon from Stripe.\";\n\n                logger.LogError(ex, \"Stripe coupon error: {CouponId}\", model.StripeCouponId);\n                ModelState.AddModelError(nameof(model.StripeCouponId), errorMessage);\n                return View(\"Create\", model);\n            }\n\n            model.Name = coupon.Name;\n            model.PercentOff = coupon.PercentOff;\n            model.AmountOff = coupon.AmountOff;\n            model.Currency = coupon.Currency;\n            model.Duration = coupon.Duration;\n            model.DurationInMonths = (int?)coupon.DurationInMonths;\n\n            var productIds = coupon.AppliesTo?.Products;\n            if (productIds != null && productIds.Count != 0)\n            {\n                try\n                {\n                    var allProducts = await stripeAdapter.ListProductsAsync(new ProductListOptions\n                    {\n                        Ids = productIds.ToList()\n                    });\n\n                    model.AppliesToProducts = allProducts\n                        .ToDictionary(product => product.Id, product => product.Name);\n                }\n                catch (StripeException ex)\n                {\n                    logger.LogError(ex, \"Failed to fetch the coupon's associated products from Stripe. Coupon ID: {CouponId}\", model.StripeCouponId);\n                    ModelState.AddModelError(string.Empty, \"Failed to fetch the coupon's associated products from Stripe.\");\n                    return View(\"Create\", model);\n                }\n            }\n\n            model.IsImported = true;\n            return View(\"Create\", model);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Error importing coupon from Stripe. Coupon ID: {CouponId}\", model.StripeCouponId);\n            ModelState.AddModelError(string.Empty, \"An error occurred while importing the coupon.\");\n            return View(\"Create\", model);\n        }\n    }\n\n    [HttpPost(\"create\")]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public async Task<IActionResult> Create(CreateSubscriptionDiscountModel model)\n    {\n        if (!model.IsImported)\n        {\n            ModelState.AddModelError(string.Empty,\n                \"Please import the coupon from Stripe before submitting.\");\n            return View(model);\n        }\n\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        try\n        {\n            // Check for duplicate coupon to prevent race condition\n            var existing = await subscriptionDiscountRepository.GetByStripeCouponIdAsync(model.StripeCouponId);\n            if (existing != null)\n            {\n                ModelState.AddModelError(nameof(model.StripeCouponId),\n                    \"This coupon has already been imported.\");\n                return View(model);\n            }\n\n            var discount = new SubscriptionDiscount\n            {\n                StripeCouponId = model.StripeCouponId,\n                Name = model.Name,\n                PercentOff = model.PercentOff,\n                AmountOff = model.AmountOff,\n                Currency = model.Currency,\n                Duration = model.Duration,\n                DurationInMonths = model.DurationInMonths,\n                StripeProductIds = model.AppliesToProducts?.Keys.ToList(),\n                StartDate = model.StartDate,\n                EndDate = model.EndDate,\n                AudienceType = model.AudienceType,\n                CreationDate = DateTime.UtcNow,\n                RevisionDate = DateTime.UtcNow\n            };\n\n            await subscriptionDiscountRepository.CreateAsync(discount);\n\n            PersistSuccessMessage($\"Discount '{model.StripeCouponId}' imported successfully.\");\n            return RedirectToAction(nameof(Index));\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Error creating subscription discount. Coupon ID: {CouponId}\", model.StripeCouponId);\n            ModelState.AddModelError(string.Empty, \"An error occurred while saving the discount.\");\n            return View(model);\n        }\n    }\n\n    [HttpGet(\"{id}\")]\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public async Task<IActionResult> Edit(Guid id)\n    {\n        var discount = await subscriptionDiscountRepository.GetByIdAsync(id);\n        if (discount == null)\n        {\n            return NotFound();\n        }\n\n        var model = new EditSubscriptionDiscountModel(discount);\n\n        if (model.StripeProductIds is { Count: > 0 })\n        {\n            try\n            {\n                var products = await stripeAdapter.ListProductsAsync(new ProductListOptions\n                {\n                    Ids = model.StripeProductIds.ToList()\n                });\n                model.AppliesToProducts = products.ToDictionary(p => p.Id, p => p.Name);\n            }\n            catch (StripeException ex)\n            {\n                logger.LogError(ex, \"Failed to fetch the coupon's associated products from Stripe. Coupon ID: {CouponId}\", model.StripeCouponId);\n                ModelState.AddModelError(string.Empty, \"Failed to fetch the coupon's associated products from Stripe. However, editing is still possible.\");\n            }\n        }\n\n        return View(model);\n    }\n\n    [HttpPost(\"{id}\")]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public async Task<IActionResult> Edit(Guid id, EditSubscriptionDiscountModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        var discount = await subscriptionDiscountRepository.GetByIdAsync(id);\n        if (discount == null)\n        {\n            return NotFound();\n        }\n\n        try\n        {\n            discount.StartDate = model.StartDate;\n            discount.EndDate = model.EndDate;\n            discount.AudienceType = model.AudienceType;\n            discount.RevisionDate = DateTime.UtcNow;\n\n            await subscriptionDiscountRepository.ReplaceAsync(discount);\n\n            PersistSuccessMessage($\"Discount '{discount.StripeCouponId}' updated successfully.\");\n            return RedirectToAction(nameof(Index));\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Error updating subscription discount. Coupon ID: {CouponId}\", discount.StripeCouponId);\n            ModelState.AddModelError(string.Empty, \"An error occurred while updating the discount.\");\n            return View(model);\n        }\n    }\n\n    [HttpPost(\"{id}/delete\")]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public async Task<IActionResult> Delete(Guid id)\n    {\n        var discount = await subscriptionDiscountRepository.GetByIdAsync(id);\n        if (discount == null)\n        {\n            return NotFound();\n        }\n\n        try\n        {\n            await subscriptionDiscountRepository.DeleteAsync(discount);\n\n            PersistSuccessMessage($\"Discount '{discount.StripeCouponId}' deleted successfully.\");\n            return RedirectToAction(nameof(Index));\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Error deleting subscription discount. Coupon ID: {CouponId}\", discount.StripeCouponId);\n            PersistErrorMessage(\"An error occurred while attempting to delete the discount.\");\n            return RedirectToAction(nameof(Edit), new { id });\n        }\n    }\n\n    private void PersistSuccessMessage(string message) => TempData[SuccessKey] = message;\n    private void PersistErrorMessage(string message) => TempData[ErrorKey] = message;\n}\n"
  },
  {
    "path": "src/Admin/Billing/Models/BusinessUnitConversionModel.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Admin.Billing.Models;\n\npublic class BusinessUnitConversionModel\n{\n    [Required]\n    [EmailAddress]\n    [Display(Name = \"Provider Admin Email\")]\n    public string? ProviderAdminEmail { get; set; }\n\n    [BindNever]\n    public required Organization Organization { get; set; }\n\n    [BindNever]\n    public Guid? ProviderId { get; set; }\n\n    [BindNever]\n    public string? Success { get; set; }\n\n    [BindNever] public List<string>? Errors { get; set; } = [];\n}\n"
  },
  {
    "path": "src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel;\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Admin.Billing.Models.ProcessStripeEvents;\n\npublic class EventsFormModel : IValidatableObject\n{\n    [Required]\n    public string EventIds { get; set; }\n\n    [Required]\n    [DisplayName(\"Inspect Only\")]\n    public bool Inspect { get; set; }\n\n    public List<string> GetEventIds() =>\n        EventIds?.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries)\n            .Select(eventId => eventId.Trim())\n            .ToList() ?? [];\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        var eventIds = GetEventIds();\n\n        if (eventIds.Any(eventId => !eventId.StartsWith(\"evt_\")))\n        {\n            yield return new ValidationResult(\"Event Ids must start with 'evt_'.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Admin.Billing.Models.ProcessStripeEvents;\n\npublic class EventsRequestBody\n{\n    [JsonPropertyName(\"eventIds\")]\n    public List<string> EventIds { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Admin.Billing.Models.ProcessStripeEvents;\n\npublic class EventsResponseBody\n{\n    [JsonPropertyName(\"events\")]\n    public List<EventResponseBody> Events { get; set; }\n\n    [JsonIgnore]\n    public EventActionType ActionType { get; set; }\n}\n\npublic class EventResponseBody\n{\n    [JsonPropertyName(\"id\")]\n    public string Id { get; set; }\n\n    [JsonPropertyName(\"url\")]\n    public string URL { get; set; }\n\n    [JsonPropertyName(\"apiVersion\")]\n    public string APIVersion { get; set; }\n\n    [JsonPropertyName(\"type\")]\n    public string Type { get; set; }\n\n    [JsonPropertyName(\"createdUTC\")]\n    public DateTime CreatedUTC { get; set; }\n\n    [JsonPropertyName(\"processingError\")]\n    public string ProcessingError { get; set; }\n}\n\npublic enum EventActionType\n{\n    Inspect,\n    Process\n}\n"
  },
  {
    "path": "src/Admin/Billing/Models/ProviderPlanViewModel.cs",
    "content": "﻿using Bit.Core.Billing.Providers.Entities;\n\nnamespace Bit.Admin.Billing.Models;\n\npublic class ProviderPlanViewModel\n{\n    public string Name { get; set; }\n    public int PurchasedSeats { get; set; }\n    public int AssignedSeats { get; set; }\n    public int UsedSeats { get; set; }\n    public int RemainingSeats { get; set; }\n\n    public ProviderPlanViewModel(\n        string name,\n        ProviderPlan providerPlan,\n        int usedSeats)\n    {\n        var purchasedSeats = (providerPlan.SeatMinimum ?? 0) + (providerPlan.PurchasedSeats ?? 0);\n\n        Name = name;\n        PurchasedSeats = purchasedSeats;\n        AssignedSeats = providerPlan.AllocatedSeats ?? 0;\n        UsedSeats = usedSeats;\n        RemainingSeats = purchasedSeats - AssignedSeats;\n    }\n}\n"
  },
  {
    "path": "src/Admin/Billing/Models/SubscriptionDiscount/CreateSubscriptionDiscountModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Admin.Billing.Models;\n\npublic class CreateSubscriptionDiscountModel : IValidatableObject\n{\n    [Required]\n    [Display(Name = \"Stripe Coupon ID\")]\n    [MaxLength(50)]\n    public string StripeCouponId { get; set; } = null!;\n\n    public string? Name { get; set; }\n    public decimal? PercentOff { get; set; }\n    public long? AmountOff { get; set; }\n    public string? Currency { get; set; }\n    public string Duration { get; set; } = string.Empty;\n    public int? DurationInMonths { get; set; }\n    public Dictionary<string, string>? AppliesToProducts { get; set; } // Key: ProductId, Value: ProductName\n\n    [Required]\n    [Display(Name = \"Start Date\")]\n    public DateTime StartDate { get; set; } = DateTime.UtcNow.Date;\n\n    [Required]\n    [Display(Name = \"End Date\")]\n    public DateTime EndDate { get; set; } = DateTime.UtcNow.Date.AddMonths(1);\n\n    [Display(Name = \"Restrict to users with no previous subscriptions?\")]\n    public bool RestrictToNewUsersOnly { get; set; }\n\n    public DiscountAudienceType AudienceType => RestrictToNewUsersOnly\n        ? DiscountAudienceType.UserHasNoPreviousSubscriptions\n        : DiscountAudienceType.AllUsers;\n\n    public bool IsImported { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (EndDate < StartDate)\n        {\n            yield return new ValidationResult(\n                \"End Date must be on or after Start Date.\",\n                new[] { nameof(EndDate) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Billing/Models/SubscriptionDiscount/EditSubscriptionDiscountModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Subscriptions.Entities;\n\nnamespace Bit.Admin.Billing.Models;\n\npublic class EditSubscriptionDiscountModel : IValidatableObject\n{\n    public Guid Id { get; set; }\n\n    public string StripeCouponId { get; set; } = null!;\n    public string? Name { get; set; }\n    public decimal? PercentOff { get; set; }\n    public long? AmountOff { get; set; }\n    public string? Currency { get; set; }\n    public string Duration { get; set; } = string.Empty;\n    public int? DurationInMonths { get; set; }\n    public ICollection<string>? StripeProductIds { get; set; }\n    public Dictionary<string, string>? AppliesToProducts { get; set; } // Key: ProductId, Value: ProductName\n\n    [Required]\n    [Display(Name = \"Start Date\")]\n    public DateTime StartDate { get; set; }\n\n    [Required]\n    [Display(Name = \"End Date\")]\n    public DateTime EndDate { get; set; }\n\n    [Display(Name = \"Restrict to users with no previous subscriptions?\")]\n    public bool RestrictToNewUsersOnly { get; set; }\n\n    public DiscountAudienceType AudienceType => RestrictToNewUsersOnly\n        ? DiscountAudienceType.UserHasNoPreviousSubscriptions\n        : DiscountAudienceType.AllUsers;\n\n    public EditSubscriptionDiscountModel() { }\n\n    public EditSubscriptionDiscountModel(SubscriptionDiscount discount)\n    {\n        Id = discount.Id;\n        StripeCouponId = discount.StripeCouponId;\n        Name = discount.Name;\n        PercentOff = discount.PercentOff;\n        AmountOff = discount.AmountOff;\n        Currency = discount.Currency;\n        Duration = discount.Duration;\n        DurationInMonths = discount.DurationInMonths;\n        StripeProductIds = discount.StripeProductIds;\n        StartDate = discount.StartDate;\n        EndDate = discount.EndDate;\n        RestrictToNewUsersOnly = discount.AudienceType == DiscountAudienceType.UserHasNoPreviousSubscriptions;\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (EndDate < StartDate)\n        {\n            yield return new ValidationResult(\n                \"End Date must be on or after Start Date.\",\n                new[] { nameof(EndDate) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Billing/Models/SubscriptionDiscount/SubscriptionDiscountPagedModel.cs",
    "content": "﻿using Bit.Admin.Models;\n\nnamespace Bit.Admin.Billing.Models;\n\npublic class SubscriptionDiscountPagedModel : PagedModel<SubscriptionDiscountViewModel>\n{\n}\n"
  },
  {
    "path": "src/Admin/Billing/Models/SubscriptionDiscount/SubscriptionDiscountViewModel.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\n\nnamespace Bit.Admin.Billing.Models;\n\npublic class SubscriptionDiscountViewModel\n{\n    public Guid Id { get; set; }\n    public string StripeCouponId { get; set; } = null!;\n    public string? Name { get; set; }\n    public decimal? PercentOff { get; set; }\n    public long? AmountOff { get; set; }\n    public string? Currency { get; set; }\n    public string Duration { get; set; } = null!;\n    public int? DurationInMonths { get; set; }\n    public DateTime StartDate { get; set; }\n    public DateTime EndDate { get; set; }\n    public DiscountAudienceType AudienceType { get; set; }\n    public DateTime CreationDate { get; set; }\n    public bool IsActive => DateTime.UtcNow >= StartDate && DateTime.UtcNow <= EndDate;\n\n    public string DiscountDisplay => PercentOff.HasValue\n        ? $\"{PercentOff.Value:G29}% off\"\n        : $\"${AmountOff / 100m} off\";\n\n    public bool IsRestrictedToNewUsersOnly => AudienceType == DiscountAudienceType.UserHasNoPreviousSubscriptions;\n    public bool IsAvailableToAllUsers => AudienceType == DiscountAudienceType.AllUsers;\n}\n"
  },
  {
    "path": "src/Admin/Billing/Views/BusinessUnitConversion/Index.cshtml",
    "content": "@model Bit.Admin.Billing.Models.BusinessUnitConversionModel\n\n@{\n    ViewData[\"Title\"] = \"Convert Organization to Business Unit\";\n}\n\n@if (!string.IsNullOrEmpty(Model.ProviderAdminEmail))\n{\n    <h1>Convert @Model.Organization.Name to Business Unit</h1>\n    @if (!string.IsNullOrEmpty(Model.Success))\n    {\n        <div class=\"alert alert-success alert-dismissible fade show mb-3\" role=\"alert\">\n            @Model.Success\n            <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button>\n        </div>\n    }\n    @if (Model.Errors?.Any() ?? false)\n    {\n        @foreach (var error in Model.Errors)\n        {\n            <div class=\"alert alert-danger alert-dismissible fade show mb-3\" role=\"alert\">\n                @error\n                <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button>\n            </div>\n        }\n    }\n    <p>This organization has a business unit conversion in progress.</p>\n\n    <div class=\"mb-3\">\n        <label asp-for=\"ProviderAdminEmail\" class=\"form-label\"></label>\n        <input type=\"email\" class=\"form-control\" asp-for=\"ProviderAdminEmail\" disabled></input>\n    </div>\n\n    <div class=\"d-flex gap-2\">\n        <form method=\"post\" asp-controller=\"BusinessUnitConversion\" asp-action=\"ResendInvite\" asp-route-organizationId=\"@Model.Organization.Id\">\n            <input type=\"hidden\" asp-for=\"ProviderAdminEmail\" />\n            <button type=\"submit\" class=\"btn btn-primary mb-2\">Resend Invite</button>\n        </form>\n        <form method=\"post\" asp-controller=\"BusinessUnitConversion\" asp-action=\"Reset\" asp-route-organizationId=\"@Model.Organization.Id\">\n            <input type=\"hidden\" asp-for=\"ProviderAdminEmail\" />\n            <button type=\"submit\" class=\"btn btn-danger mb-2\">Reset Conversion</button>\n        </form>\n        @if (Model.ProviderId.HasValue)\n        {\n            <a asp-controller=\"Providers\"\n               asp-action=\"Edit\"\n               asp-route-id=\"@Model.ProviderId\"\n               class=\"btn btn-secondary mb-2\">\n                Go to Provider\n            </a>\n        }\n    </div>\n}\nelse\n{\n    <h1>Convert @Model.Organization.Name to Business Unit</h1>\n    @if (Model.Errors?.Any() ?? false)\n    {\n        @foreach (var error in Model.Errors)\n        {\n            <div class=\"alert alert-danger alert-dismissible fade show mb-3\" role=\"alert\">\n                @error\n                <button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button>\n            </div>\n        }\n    }\n    <form method=\"post\" asp-controller=\"BusinessUnitConversion\" asp-action=\"Initiate\" asp-route-organizationId=\"@Model.Organization.Id\">\n        <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n        <div class=\"mb-3\">\n            <label asp-for=\"ProviderAdminEmail\" class=\"form-label\"></label>\n            <input type=\"email\" class=\"form-control\" asp-for=\"ProviderAdminEmail\" />\n        </div>\n        <button type=\"submit\" class=\"btn btn-primary mb-2\">Convert</button>\n    </form>\n}\n"
  },
  {
    "path": "src/Admin/Billing/Views/ProcessStripeEvents/Index.cshtml",
    "content": "@model Bit.Admin.Billing.Models.ProcessStripeEvents.EventsFormModel\n\n@{\n    ViewData[\"Title\"] = \"Process Stripe Events\";\n}\n\n<h1>Process Stripe Events</h1>\n<form method=\"post\" asp-controller=\"ProcessStripeEvents\" asp-action=\"Process\">\n    <div class=\"row\">\n        <div class=\"col-1\">\n            <div class=\"form-group\">\n                <input type=\"submit\" value=\"Process\" class=\"btn btn-primary mb-2\"/>\n            </div>\n        </div>\n        <div class=\"col-2\">\n            <div class=\"form-group form-check\">\n                <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"Inspect\">\n                <label class=\"form-check-label\" asp-for=\"Inspect\"></label>\n            </div>\n        </div>\n    </div>\n    <div class=\"form-group\">\n        <textarea id=\"event-ids\" type=\"text\" class=\"form-control\" rows=\"100\" asp-for=\"EventIds\"></textarea>\n    </div>\n</form>\n"
  },
  {
    "path": "src/Admin/Billing/Views/ProcessStripeEvents/Results.cshtml",
    "content": "@using Bit.Admin.Billing.Models.ProcessStripeEvents\n@model Bit.Admin.Billing.Models.ProcessStripeEvents.EventsResponseBody\n\n@{\n    var title = Model.ActionType == EventActionType.Inspect ? \"Inspect Stripe Events\" : \"Process Stripe Events\";\n    ViewData[\"Title\"] = title;\n}\n\n<h1>@title</h1>\n<h2>Results</h2>\n\n<div class=\"table-responsive\">\n    @if (!Model.Events.Any())\n    {\n        <p>No data found.</p>\n    }\n    else\n    {\n        <table class=\"table table-striped table-hover\">\n            <thead>\n            <tr>\n                <th>ID</th>\n                <th>Type</th>\n                <th>API Version</th>\n                <th>Created</th>\n                @if (Model.ActionType == EventActionType.Process)\n                {\n                    <th>Processing Error</th>\n                }\n            </tr>\n            </thead>\n            <tbody>\n            @foreach (var eventResponseBody in Model.Events)\n            {\n                <tr>\n                    <td><a href=\"@eventResponseBody.URL\">@eventResponseBody.Id</a></td>\n                    <td>@eventResponseBody.Type</td>\n                    <td>@eventResponseBody.APIVersion</td>\n                    <td>@eventResponseBody.CreatedUTC</td>\n                    @if (Model.ActionType == EventActionType.Process)\n                    {\n                        <td>@eventResponseBody.ProcessingError</td>\n                    }\n                </tr>\n            }\n            </tbody>\n        </table>\n    }\n</div>\n"
  },
  {
    "path": "src/Admin/Billing/Views/Providers/ProviderPlans.cshtml",
    "content": "@model List<Bit.Admin.Billing.Models.ProviderPlanViewModel>\n@foreach (var plan in Model)\n{\n    <h2>@plan.Name</h2>\n    <dl class=\"row\">\n        <dt class=\"col-sm-4 col-lg-3\">Purchased Seats</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@plan.PurchasedSeats</dd>\n\n        <dt class=\"col-sm-4 col-lg-3\">Assigned Seats</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@plan.AssignedSeats</dd>\n\n        <dt class=\"col-sm-4 col-lg-3\">Used Seats</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@plan.UsedSeats</dd>\n\n        <dt class=\"col-sm-4 col-lg-3\">Remaining Seats</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@plan.RemainingSeats</dd>\n    </dl>\n}\n"
  },
  {
    "path": "src/Admin/Billing/Views/SubscriptionDiscounts/Create.cshtml",
    "content": "@model CreateSubscriptionDiscountModel\n@{\n    ViewData[\"Title\"] = \"Import Discount From Stripe\";\n}\n\n<h1>Import Discount From Stripe</h1>\n\n<div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n\n@if (!Model.IsImported)\n{\n    <partial name=\"Partials/_ImportCouponForm\" model=\"Model\" />\n}\nelse\n{\n    <partial name=\"Partials/_ConfigureDiscountForm\" model=\"Model\" />\n}\n"
  },
  {
    "path": "src/Admin/Billing/Views/SubscriptionDiscounts/Edit.cshtml",
    "content": "@model EditSubscriptionDiscountModel\n@{\n    ViewData[\"Title\"] = \"Edit Discount\";\n}\n\n<h1>Edit Discount</h1>\n\n<div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n\n<form method=\"post\" asp-action=\"Edit\" asp-route-id=\"@Model.Id\" id=\"edit-form\">\n    <input type=\"hidden\" asp-for=\"StripeCouponId\" />\n    <input type=\"hidden\" asp-for=\"Name\" />\n    <input type=\"hidden\" asp-for=\"PercentOff\" />\n    <input type=\"hidden\" asp-for=\"AmountOff\" />\n    <input type=\"hidden\" asp-for=\"Currency\" />\n    <input type=\"hidden\" asp-for=\"Duration\" />\n    <input type=\"hidden\" asp-for=\"DurationInMonths\" />\n    @if (Model.AppliesToProducts != null)\n    {\n        var index = 0;\n        @foreach (var product in Model.AppliesToProducts)\n        {\n            <input type=\"hidden\" name=\"AppliesToProducts[@index].Key\" value=\"@product.Key\" />\n            <input type=\"hidden\" name=\"AppliesToProducts[@index].Value\" value=\"@product.Value\" />\n            index++;\n        }\n    }\n    <div class=\"card mb-3\">\n        <div class=\"card-header\">\n            <h5>Stripe Coupon Details</h5>\n        </div>\n        <div class=\"card-body\">\n            <dl class=\"row\">\n                <dt class=\"col-sm-3\">Coupon ID:</dt>\n                <dd class=\"col-sm-9\"><code>@Model.StripeCouponId</code></dd>\n\n                @if (!string.IsNullOrEmpty(Model.Name))\n                {\n                    <dt class=\"col-sm-3\">Name:</dt>\n                    <dd class=\"col-sm-9\">@Model.Name</dd>\n                }\n\n                <dt class=\"col-sm-3\">Discount:</dt>\n                <dd class=\"col-sm-9\">\n                    @if (Model.PercentOff.HasValue)\n                    {\n                        <text>@Model.PercentOff% off</text>\n                    }\n                    else if (Model.AmountOff.HasValue)\n                    {\n                        <text>$@(Model.AmountOff / 100m) off</text>\n                        @if (!string.IsNullOrEmpty(Model.Currency))\n                        {\n                            <text> (@Model.Currency.ToUpper())</text>\n                        }\n                    }\n                </dd>\n\n                <dt class=\"col-sm-3\">Duration:</dt>\n                <dd class=\"col-sm-9\">\n                    @Model.Duration\n                    @if (Model.DurationInMonths.HasValue)\n                    {\n                        <text>(@Model.DurationInMonths months)</text>\n                    }\n                </dd>\n\n                @if (Model.AppliesToProducts != null && Model.AppliesToProducts.Count != 0)\n                {\n                    <dt class=\"col-sm-3\">Applies To Products:</dt>\n                    <dd class=\"col-sm-9\">\n                        <ul class=\"mb-0\">\n                            @foreach (var product in Model.AppliesToProducts)\n                            {\n                                <li>@product.Value</li>\n                            }\n                        </ul>\n                    </dd>\n                }\n                else if (Model.StripeProductIds != null && Model.StripeProductIds.Count != 0)\n                {\n                    <dt class=\"col-sm-3\">Applies To Products:</dt>\n                    <dd class=\"col-sm-9\">\n                        <ul class=\"mb-0\">\n                            @foreach (var productId in Model.StripeProductIds)\n                            {\n                                <li><code>@productId</code></li>\n                            }\n                        </ul>\n                    </dd>\n                }\n            </dl>\n        </div>\n    </div>\n\n    <div class=\"card mb-3\">\n        <div class=\"card-header\">\n            <h5>Bitwarden Configuration</h5>\n        </div>\n        <div class=\"card-body\">\n            <div class=\"row\">\n                <div class=\"col-md-6\">\n                    <div class=\"mb-3\">\n                        <label asp-for=\"StartDate\" class=\"form-label\"></label>\n                        <input asp-for=\"StartDate\" type=\"date\" class=\"form-control\" required />\n                    </div>\n                </div>\n                <div class=\"col-md-6\">\n                    <div class=\"mb-3\">\n                        <label asp-for=\"EndDate\" class=\"form-label\"></label>\n                        <input asp-for=\"EndDate\" type=\"date\" class=\"form-control\" required />\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"mb-3 form-check\">\n                <input asp-for=\"RestrictToNewUsersOnly\" type=\"checkbox\" class=\"form-check-input\" />\n                <label asp-for=\"RestrictToNewUsersOnly\" class=\"form-check-label\"></label>\n            </div>\n        </div>\n    </div>\n</form>\n\n<div class=\"d-flex mt-4\">\n    <button type=\"submit\" class=\"btn btn-primary\" form=\"edit-form\">Save</button>\n    <a asp-action=\"Index\" class=\"btn btn-secondary ms-2\">Cancel</a>\n    <div class=\"ms-auto d-flex\">\n        <form method=\"post\" asp-action=\"Delete\" asp-route-id=\"@Model.Id\"\n              onsubmit=\"return confirm('Are you sure you want to delete this discount?')\">\n            <button class=\"btn btn-danger\" type=\"submit\">Delete Discount</button>\n        </form>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Admin/Billing/Views/SubscriptionDiscounts/Index.cshtml",
    "content": "@model SubscriptionDiscountPagedModel\n@{\n    ViewData[\"Title\"] = \"Discounts\";\n}\n\n<div class=\"container-fluid\">\n    <div class=\"row mb-3\">\n        <div class=\"col\">\n            <h1>Discounts</h1>\n        </div>\n        <div class=\"col-auto\">\n            <a asp-action=\"Create\" class=\"btn btn-primary\">\n                <i class=\"fa fa-plus\"></i> Import Discount\n            </a>\n        </div>\n    </div>\n\n    @if (Model.Items.Any())\n    {\n        <div class=\"table-responsive\">\n            <table class=\"table table-striped\">\n                <thead>\n                    <tr>\n                        <th>Stripe Coupon ID</th>\n                        <th>Name</th>\n                        <th>Discount</th>\n                        <th>Duration</th>\n                        <th>Only New Users?</th>\n                        <th>Status</th>\n                        <th>Created</th>\n                    </tr>\n                </thead>\n                <tbody>\n                    @foreach (var discount in Model.Items)\n                    {\n                        var dateRange = $\"{discount.StartDate.ToString(\"MM/dd/yyyy\")} - {discount.EndDate.ToString(\"MM/dd/yyyy\")}\";\n                        <tr>\n                            <td><code>@discount.StripeCouponId</code></td>\n                            <td>\n                                <a asp-action=\"Edit\" asp-route-id=\"@discount.Id\">\n                                    @discount.Name\n                                </a>\n                            </td>\n                            <td>@discount.DiscountDisplay</td>\n                            <td>\n                                @discount.Duration\n                                @if (discount.DurationInMonths.HasValue)\n                                {\n                                    <text>(@discount.DurationInMonths months)</text>\n                                }\n                            </td>\n                            <td>\n                                @if (discount.IsRestrictedToNewUsersOnly)\n                                {\n                                    <i class=\"fa fa-check\"></i>\n                                }\n                            </td>\n                            <td>\n                                @if (discount.IsActive)\n                                {\n                                    <span class=\"badge bg-success\" title=\"@dateRange\">Active</span>\n                                }\n                                else if (DateTime.UtcNow < discount.StartDate)\n                                {\n                                    <span class=\"badge bg-secondary\" title=\"@dateRange\">Scheduled</span>\n                                }\n                                else\n                                {\n                                    <span class=\"badge bg-danger\" title=\"@dateRange\">Expired</span>\n                                }\n                            </td>\n                            <td>@discount.CreationDate.ToString(\"MM/dd/yyyy\")</td>\n                        </tr>\n                    }\n                </tbody>\n            </table>\n        </div>\n\n        <nav>\n            <ul class=\"pagination\">\n                @if (Model.PreviousPage.HasValue)\n                {\n                    <li class=\"page-item\">\n                        <a class=\"page-link\" asp-action=\"Index\"\n                           asp-route-page=\"@Model.PreviousPage.Value\"\n                           asp-route-count=\"@Model.Count\">\n                            Previous\n                        </a>\n                    </li>\n                }\n                else\n                {\n                    <li class=\"page-item disabled\">\n                        <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Previous</a>\n                    </li>\n                }\n\n                <li class=\"page-item active\">\n                    <span class=\"page-link\">Page @Model.Page</span>\n                </li>\n\n                @if (Model.NextPage.HasValue)\n                {\n                    <li class=\"page-item\">\n                        <a class=\"page-link\" asp-action=\"Index\"\n                           asp-route-page=\"@Model.NextPage.Value\"\n                           asp-route-count=\"@Model.Count\">\n                            Next\n                        </a>\n                    </li>\n                }\n                else\n                {\n                    <li class=\"page-item disabled\">\n                        <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Next</a>\n                    </li>\n                }\n            </ul>\n        </nav>\n    }\n    else\n    {\n        <div class=\"alert alert-info\">\n            No subscription discounts found. <a asp-action=\"Create\">Import one from Stripe</a>.\n        </div>\n    }\n</div>\n"
  },
  {
    "path": "src/Admin/Billing/Views/SubscriptionDiscounts/Partials/_ConfigureDiscountForm.cshtml",
    "content": "@model CreateSubscriptionDiscountModel\n\n<form method=\"post\" asp-action=\"Create\">\n    <input type=\"hidden\" asp-for=\"StripeCouponId\" />\n    <input type=\"hidden\" asp-for=\"Name\" />\n    <input type=\"hidden\" asp-for=\"PercentOff\" />\n    <input type=\"hidden\" asp-for=\"AmountOff\" />\n    <input type=\"hidden\" asp-for=\"Currency\" />\n    <input type=\"hidden\" asp-for=\"Duration\" />\n    <input type=\"hidden\" asp-for=\"DurationInMonths\" />\n    <input type=\"hidden\" asp-for=\"IsImported\" />\n    @if (Model.AppliesToProducts != null)\n    {\n        var index = 0;\n        @foreach (var product in Model.AppliesToProducts)\n        {\n            <input type=\"hidden\" name=\"AppliesToProducts[@index].Key\" value=\"@product.Key\" />\n            <input type=\"hidden\" name=\"AppliesToProducts[@index].Value\" value=\"@product.Value\" />\n            index++;\n        }\n    }\n\n    <div class=\"card mb-3\">\n        <div class=\"card-header\">\n            <h5>Stripe Coupon Details</h5>\n        </div>\n        <div class=\"card-body\">\n            <dl class=\"row\">\n                <dt class=\"col-sm-3\">Coupon ID:</dt>\n                <dd class=\"col-sm-9\"><code>@Model.StripeCouponId</code></dd>\n\n                @if (!string.IsNullOrEmpty(Model.Name))\n                {\n                    <dt class=\"col-sm-3\">Name:</dt>\n                    <dd class=\"col-sm-9\">@Model.Name</dd>\n                }\n\n                <dt class=\"col-sm-3\">Discount:</dt>\n                <dd class=\"col-sm-9\">\n                    @if (Model.PercentOff.HasValue)\n                    {\n                        <text>@Model.PercentOff% off</text>\n                    }\n                    else if (Model.AmountOff.HasValue)\n                    {\n                        <text>$@(Model.AmountOff / 100m) off</text>\n                        @if (!string.IsNullOrEmpty(Model.Currency))\n                        {\n                            <text> (@Model.Currency.ToUpper())</text>\n                        }\n                    }\n                </dd>\n\n                <dt class=\"col-sm-3\">Duration:</dt>\n                <dd class=\"col-sm-9\">\n                    @Model.Duration\n                    @if (Model.DurationInMonths.HasValue)\n                    {\n                        <text>(@Model.DurationInMonths months)</text>\n                    }\n                </dd>\n\n                @if (Model.AppliesToProducts != null && Model.AppliesToProducts.Count != 0)\n                {\n                    <dt class=\"col-sm-3\">Applies To Products:</dt>\n                    <dd class=\"col-sm-9\">\n                        <ul class=\"mb-0\">\n                            @foreach (var product in Model.AppliesToProducts)\n                            {\n                                <li>@product.Value</li>\n                            }\n                        </ul>\n                    </dd>\n                }\n            </dl>\n        </div>\n    </div>\n\n    <div class=\"card mb-3\">\n        <div class=\"card-header\">\n            <h5>Step 2: Bitwarden Configuration</h5>\n        </div>\n        <div class=\"card-body\">\n            <div class=\"row\">\n                <div class=\"col-md-6\">\n                    <div class=\"mb-3\">\n                        <label asp-for=\"StartDate\" class=\"form-label\"></label>\n                        <input asp-for=\"StartDate\" type=\"date\" class=\"form-control\" required />\n                    </div>\n                </div>\n                <div class=\"col-md-6\">\n                    <div class=\"mb-3\">\n                        <label asp-for=\"EndDate\" class=\"form-label\"></label>\n                        <input asp-for=\"EndDate\" type=\"date\" class=\"form-control\" required />\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"mb-3 form-check\">\n                <input asp-for=\"RestrictToNewUsersOnly\" type=\"checkbox\" class=\"form-check-input\" />\n                <label asp-for=\"RestrictToNewUsersOnly\" class=\"form-check-label\"></label>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"mb-3\">\n        <button type=\"submit\" class=\"btn btn-success\">\n            <i class=\"fa fa-save\"></i> Save Discount\n        </button>\n        <a asp-action=\"Create\" class=\"btn btn-secondary\">\n            Back\n        </a>\n        <a asp-action=\"Index\" class=\"btn btn-secondary\">Cancel</a>\n    </div>\n</form>\n"
  },
  {
    "path": "src/Admin/Billing/Views/SubscriptionDiscounts/Partials/_ImportCouponForm.cshtml",
    "content": "@model CreateSubscriptionDiscountModel\n\n<form method=\"post\" asp-action=\"ImportCoupon\">\n    <div class=\"card mb-3\">\n        <div class=\"card-header\">\n            <h5>Step 1: Import Stripe Coupon</h5>\n        </div>\n        <div class=\"card-body\">\n            <div class=\"mb-3\">\n                <label asp-for=\"StripeCouponId\" class=\"form-label\"></label>\n                <input asp-for=\"StripeCouponId\" class=\"form-control\" autofocus />\n                <small class=\"form-text text-muted\">\n                    Enter the Stripe coupon ID and click Import to fetch the coupon details from Stripe.\n                </small>\n            </div>\n        </div>\n    </div>\n\n    <div class=\"mb-3\">\n        <button type=\"submit\" class=\"btn btn-primary\">\n            <i class=\"fa fa-download\"></i> Import from Stripe\n        </button>\n        <a asp-action=\"Index\" class=\"btn btn-secondary\">Cancel</a>\n    </div>\n</form>\n"
  },
  {
    "path": "src/Admin/Billing/Views/_ViewImports.cshtml",
    "content": "@using Microsoft.AspNetCore.Identity\n@using Bit.Admin.AdminConsole\n@using Bit.Admin.AdminConsole.Models\n@using Bit.Admin.Billing.Models\n@using Bit.Core.Billing.Enums\n@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers\n@addTagHelper \"*, Admin\"\n"
  },
  {
    "path": "src/Admin/Billing/Views/_ViewStart.cshtml",
    "content": "@{\n    Layout = \"_Layout\";\n}\n"
  },
  {
    "path": "src/Admin/Controllers/ErrorController.cs",
    "content": "﻿using Microsoft.AspNetCore.Diagnostics;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Admin.Controllers;\n\npublic class ErrorController : Controller\n{\n    [Route(\"/error\")]\n    public IActionResult Error(int? statusCode = null)\n    {\n        var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();\n        TempData[\"Error\"] = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error.Message;\n\n        if (exceptionHandlerPathFeature != null)\n        {\n            return Redirect(exceptionHandlerPathFeature.Path);\n        }\n        else\n        {\n            return Redirect(\"/Home\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Controllers/HomeController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Diagnostics;\nusing System.Text.Json;\nusing Bit.Admin.Models;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Newtonsoft.Json;\n\nnamespace Bit.Admin.Controllers;\n\npublic class HomeController : Controller\n{\n    private readonly GlobalSettings _globalSettings;\n    private readonly HttpClient _httpClient = new HttpClient();\n    private readonly ILogger<HomeController> _logger;\n\n    public HomeController(GlobalSettings globalSettings, ILogger<HomeController> logger)\n    {\n        _globalSettings = globalSettings;\n        _logger = logger;\n    }\n\n    [Authorize]\n    public IActionResult Index()\n    {\n        return View(new HomeModel\n        {\n            GlobalSettings = _globalSettings,\n            CurrentVersion = Core.Utilities.AssemblyHelpers.GetVersion()\n        });\n    }\n\n    public IActionResult Error()\n    {\n        return View(new ErrorViewModel\n        {\n            RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier\n        });\n    }\n\n\n    public async Task<IActionResult> GetLatestVersion(ProjectType project, CancellationToken cancellationToken)\n    {\n        var requestUri = $\"https://selfhost.bitwarden.com/version.json\";\n        try\n        {\n            var response = await _httpClient.GetAsync(requestUri, cancellationToken);\n            if (response.IsSuccessStatusCode)\n            {\n                var latestVersions = JsonConvert.DeserializeObject<LatestVersions>(await response.Content.ReadAsStringAsync());\n                return project switch\n                {\n                    ProjectType.Core => new JsonResult(latestVersions.Versions.CoreVersion),\n                    ProjectType.Web => new JsonResult(latestVersions.Versions.WebVersion),\n                    _ => throw new System.NotImplementedException(),\n                };\n            }\n        }\n        catch (HttpRequestException e)\n        {\n            _logger.LogError(e, \"Error encountered while sending GET request to {RequestUri}\", requestUri);\n            return new JsonResult(\"Unable to fetch latest version\") { StatusCode = StatusCodes.Status500InternalServerError };\n        }\n\n        return new JsonResult(\"-\");\n    }\n\n    public async Task<IActionResult> GetInstalledWebVersion(CancellationToken cancellationToken)\n    {\n        var requestUri = $\"{_globalSettings.BaseServiceUri.InternalVault}/version.json\";\n        try\n        {\n            var response = await _httpClient.GetAsync(requestUri, cancellationToken);\n            if (response.IsSuccessStatusCode)\n            {\n                using var jsonDocument = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync(cancellationToken), cancellationToken: cancellationToken);\n                var root = jsonDocument.RootElement;\n                return new JsonResult(root.GetProperty(\"version\").GetString());\n            }\n        }\n        catch (HttpRequestException e)\n        {\n            _logger.LogError(e, \"Error encountered while sending GET request to {RequestUri}\", requestUri);\n            return new JsonResult(\"Unable to fetch installed version\") { StatusCode = StatusCodes.Status500InternalServerError };\n        }\n\n        return new JsonResult(\"-\");\n    }\n\n    private class LatestVersions\n    {\n        [JsonProperty(\"versions\")]\n        public Versions Versions { get; set; }\n    }\n\n    private class Versions\n    {\n        [JsonProperty(\"coreVersion\")]\n        public string CoreVersion { get; set; }\n\n        [JsonProperty(\"webVersion\")]\n        public string WebVersion { get; set; }\n\n        [JsonProperty(\"keyConnectorVersion\")]\n        public string KeyConnectorVersion { get; set; }\n    }\n}\n\npublic enum ProjectType\n{\n    Core,\n    Web,\n}\n"
  },
  {
    "path": "src/Admin/Controllers/InfoController.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Admin.Controllers;\n\npublic class InfoController : Controller\n{\n    [HttpGet(\"~/alive\")]\n    [HttpGet(\"~/now\")]\n    public DateTime GetAlive()\n    {\n        return DateTime.UtcNow;\n    }\n\n    [HttpGet(\"~/version\")]\n    public JsonResult GetVersion()\n    {\n        return Json(AssemblyHelpers.GetVersion());\n    }\n}\n"
  },
  {
    "path": "src/Admin/Controllers/ToolsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Admin.Enums;\nusing Bit.Admin.Models;\nusing Bit.Admin.Utilities;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Platform.Installations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Admin.Controllers;\n\n[Authorize]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class ToolsController : Controller\n{\n    private readonly GlobalSettings _globalSettings;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery;\n    private readonly IUserService _userService;\n    private readonly ITransactionRepository _transactionRepository;\n    private readonly IInstallationRepository _installationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IStripeAdapter _stripeAdapter;\n    private readonly IWebHostEnvironment _environment;\n\n    public ToolsController(\n        GlobalSettings globalSettings,\n        IOrganizationRepository organizationRepository,\n        IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery,\n        IUserService userService,\n        ITransactionRepository transactionRepository,\n        IInstallationRepository installationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IProviderUserRepository providerUserRepository,\n        IStripeAdapter stripeAdapter,\n        IWebHostEnvironment environment)\n    {\n        _globalSettings = globalSettings;\n        _organizationRepository = organizationRepository;\n        _getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery;\n        _userService = userService;\n        _transactionRepository = transactionRepository;\n        _installationRepository = installationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _providerUserRepository = providerUserRepository;\n        _stripeAdapter = stripeAdapter;\n        _environment = environment;\n    }\n\n    [RequirePermission(Permission.Tools_ChargeBrainTreeCustomer)]\n    public IActionResult ChargeBraintree()\n    {\n        return View(new ChargeBraintreeModel());\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_ChargeBrainTreeCustomer)]\n    public async Task<IActionResult> ChargeBraintree(ChargeBraintreeModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        var btGateway = new Braintree.BraintreeGateway\n        {\n            Environment = _globalSettings.Braintree.Production ?\n                Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,\n            MerchantId = _globalSettings.Braintree.MerchantId,\n            PublicKey = _globalSettings.Braintree.PublicKey,\n            PrivateKey = _globalSettings.Braintree.PrivateKey\n        };\n\n        var btObjIdField = model.Id[0] == 'o' ? \"organization_id\" : \"user_id\";\n        var btObjId = new Guid(model.Id.Substring(1, 32));\n\n        var transactionResult = await btGateway.Transaction.SaleAsync(\n            new Braintree.TransactionRequest\n            {\n                Amount = model.Amount.Value,\n                CustomerId = model.Id,\n                Options = new Braintree.TransactionOptionsRequest\n                {\n                    SubmitForSettlement = true,\n                    PayPal = new Braintree.TransactionOptionsPayPalRequest\n                    {\n                        CustomField = $\"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}\"\n                    }\n                },\n                CustomFields = new Dictionary<string, string>\n                {\n                    [btObjIdField] = btObjId.ToString(),\n                    [\"region\"] = _globalSettings.BaseServiceUri.CloudRegion\n                }\n            });\n\n        if (!transactionResult.IsSuccess())\n        {\n            ModelState.AddModelError(string.Empty, \"Charge failed. \" +\n                \"Refer to Braintree admin portal for more information.\");\n        }\n        else\n        {\n            model.TransactionId = transactionResult.Target.Id;\n            model.PayPalTransactionId = transactionResult.Target?.PayPalDetails?.CaptureId;\n        }\n        return View(model);\n    }\n\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public IActionResult CreateTransaction(Guid? organizationId = null, Guid? userId = null)\n    {\n        return View(\"CreateUpdateTransaction\", new CreateUpdateTransactionModel\n        {\n            OrganizationId = organizationId,\n            UserId = userId\n        });\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public async Task<IActionResult> CreateTransaction(CreateUpdateTransactionModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(\"CreateUpdateTransaction\", model);\n        }\n\n        await _transactionRepository.CreateAsync(model.ToTransaction());\n        if (model.UserId.HasValue)\n        {\n            return RedirectToAction(\"Edit\", \"Users\", new { id = model.UserId });\n        }\n        else\n        {\n            return RedirectToAction(\"Edit\", \"Organizations\", new { id = model.OrganizationId });\n        }\n    }\n\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public async Task<IActionResult> EditTransaction(Guid id)\n    {\n        var transaction = await _transactionRepository.GetByIdAsync(id);\n        if (transaction == null)\n        {\n            return RedirectToAction(\"Index\", \"Home\");\n        }\n        return View(\"CreateUpdateTransaction\", new CreateUpdateTransactionModel(transaction));\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_CreateEditTransaction)]\n    public async Task<IActionResult> EditTransaction(Guid id, CreateUpdateTransactionModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(\"CreateUpdateTransaction\", model);\n        }\n        await _transactionRepository.ReplaceAsync(model.ToTransaction(id));\n        if (model.UserId.HasValue)\n        {\n            return RedirectToAction(\"Edit\", \"Users\", new { id = model.UserId });\n        }\n        else\n        {\n            return RedirectToAction(\"Edit\", \"Organizations\", new { id = model.OrganizationId });\n        }\n    }\n\n    [RequirePermission(Permission.Tools_PromoteAdmin)]\n    public IActionResult PromoteAdmin()\n    {\n        return View();\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_PromoteAdmin)]\n    public async Task<IActionResult> PromoteAdmin(PromoteAdminModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        var orgUsers = await _organizationUserRepository.GetManyByOrganizationAsync(\n            model.OrganizationId.Value, null);\n        var user = orgUsers.FirstOrDefault(u => u.UserId == model.UserId.Value);\n        if (user == null)\n        {\n            ModelState.AddModelError(nameof(model.UserId), \"User Id not found in this organization.\");\n        }\n        else if (user.Type != Core.Enums.OrganizationUserType.Admin)\n        {\n            ModelState.AddModelError(nameof(model.UserId), \"User is not an admin of this organization.\");\n        }\n\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        user.Type = Core.Enums.OrganizationUserType.Owner;\n        await _organizationUserRepository.ReplaceAsync(user);\n        return RedirectToAction(\"Edit\", \"Organizations\", new { id = model.OrganizationId.Value });\n    }\n\n    [RequirePermission(Permission.Tools_PromoteProviderServiceUser)]\n    public IActionResult PromoteProviderServiceUser()\n    {\n        return View();\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_PromoteProviderServiceUser)]\n    public async Task<IActionResult> PromoteProviderServiceUser(PromoteProviderServiceUserModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        var providerUsers = await _providerUserRepository.GetManyByProviderAsync(\n            model.ProviderId.Value, null);\n        var serviceUser = providerUsers.FirstOrDefault(u => u.UserId == model.UserId.Value);\n        if (serviceUser == null)\n        {\n            ModelState.AddModelError(nameof(model.UserId), \"Service User Id not found in this provider.\");\n        }\n        else if (serviceUser.Type != Core.AdminConsole.Enums.Provider.ProviderUserType.ServiceUser)\n        {\n            ModelState.AddModelError(nameof(model.UserId), \"User is not a service user of this provider.\");\n        }\n\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        serviceUser.Type = Core.AdminConsole.Enums.Provider.ProviderUserType.ProviderAdmin;\n        await _providerUserRepository.ReplaceAsync(serviceUser);\n        return RedirectToAction(\"Edit\", \"Providers\", new { id = model.ProviderId.Value });\n    }\n\n    [RequirePermission(Permission.Tools_GenerateLicenseFile)]\n    public IActionResult GenerateLicense()\n    {\n        return View();\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.Tools_GenerateLicenseFile)]\n    public async Task<IActionResult> GenerateLicense(LicenseModel model)\n    {\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        User user = null;\n        Organization organization = null;\n        if (model.UserId.HasValue)\n        {\n            user = await _userService.GetUserByIdAsync(model.UserId.Value);\n            if (user == null)\n            {\n                ModelState.AddModelError(nameof(model.UserId), \"User Id not found.\");\n            }\n        }\n        else if (model.OrganizationId.HasValue)\n        {\n            organization = await _organizationRepository.GetByIdAsync(model.OrganizationId.Value);\n            if (organization == null)\n            {\n                ModelState.AddModelError(nameof(model.OrganizationId), \"Organization not found.\");\n            }\n            else if (!organization.Enabled)\n            {\n                ModelState.AddModelError(nameof(model.OrganizationId), \"Organization is disabled.\");\n            }\n        }\n        if (model.InstallationId.HasValue)\n        {\n            var installation = await _installationRepository.GetByIdAsync(model.InstallationId.Value);\n            if (installation == null)\n            {\n                ModelState.AddModelError(nameof(model.InstallationId), \"Installation not found.\");\n            }\n            else if (!installation.Enabled)\n            {\n                ModelState.AddModelError(nameof(model.OrganizationId), \"Installation is disabled.\");\n            }\n        }\n\n        if (!ModelState.IsValid)\n        {\n            return View(model);\n        }\n\n        if (organization != null)\n        {\n            var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization,\n                model.InstallationId.Value, model.Version);\n            var ms = new MemoryStream();\n            await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented);\n            ms.Seek(0, SeekOrigin.Begin);\n            return File(ms, \"text/plain\", \"bitwarden_organization_license.json\");\n        }\n        else if (user != null)\n        {\n            var license = await _userService.GenerateLicenseAsync(user, null, model.Version);\n            var ms = new MemoryStream();\n            ms.Seek(0, SeekOrigin.Begin);\n            await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented);\n            ms.Seek(0, SeekOrigin.Begin);\n            return File(ms, \"text/plain\", \"bitwarden_premium_license.json\");\n        }\n        else\n        {\n            throw new Exception(\"No license to generate.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Controllers/UsersController.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Admin.Enums;\nusing Bit.Admin.Models;\nusing Bit.Admin.Services;\nusing Bit.Admin.Utilities;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Admin.Controllers;\n\n[Authorize]\npublic class UsersController : Controller\n{\n    private readonly IUserRepository _userRepository;\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IAccessControlService _accessControlService;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IUserService _userService;\n    private readonly IFeatureService _featureService;\n\n    public UsersController(\n        IUserRepository userRepository,\n        ICipherRepository cipherRepository,\n        IStripePaymentService paymentService,\n        GlobalSettings globalSettings,\n        IAccessControlService accessControlService,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IUserService userService,\n        IFeatureService featureService)\n    {\n        _userRepository = userRepository;\n        _cipherRepository = cipherRepository;\n        _paymentService = paymentService;\n        _globalSettings = globalSettings;\n        _accessControlService = accessControlService;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _userService = userService;\n        _featureService = featureService;\n    }\n\n    [RequirePermission(Permission.User_List_View)]\n    public async Task<IActionResult> Index(string email, int page = 1, int count = 25)\n    {\n        if (page < 1)\n        {\n            page = 1;\n        }\n\n        if (count < 1)\n        {\n            count = 1;\n        }\n\n        var skip = (page - 1) * count;\n        var users = await _userRepository.SearchAsync(email, skip, count);\n\n        var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList();\n        var userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList();\n\n        return View(new UsersModel\n        {\n            Items = userModels,\n            Email = string.IsNullOrWhiteSpace(email) ? null : email,\n            Page = page,\n            Count = count,\n            Action = _globalSettings.SelfHosted ? \"View\" : \"Edit\"\n        });\n    }\n\n    public async Task<IActionResult> View(Guid id)\n    {\n        var user = await _userRepository.GetByIdAsync(id);\n\n        if (user == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, withOrganizations: false);\n\n        var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);\n        var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);\n        return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));\n    }\n\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> Edit(Guid id)\n    {\n        var user = await _userRepository.GetByIdAsync(id);\n        if (user == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, withOrganizations: false);\n        var billingInfo = await _paymentService.GetBillingAsync(user);\n        var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);\n        var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);\n        var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);\n        var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);\n\n        return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> Edit(Guid id, UserEditModel model)\n    {\n        var user = await _userRepository.GetByIdAsync(id);\n        if (user == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        var canUpgradePremium = _accessControlService.UserHasPermission(Permission.User_UpgradePremium);\n\n        if (_accessControlService.UserHasPermission(Permission.User_Premium_Edit) ||\n            canUpgradePremium)\n        {\n            user.MaxStorageGb = model.MaxStorageGb;\n            user.Premium = model.Premium;\n        }\n\n        if (_accessControlService.UserHasPermission(Permission.User_Billing_Edit))\n        {\n            user.Gateway = model.Gateway;\n            user.GatewayCustomerId = model.GatewayCustomerId;\n            user.GatewaySubscriptionId = model.GatewaySubscriptionId;\n        }\n\n        if (_accessControlService.UserHasPermission(Permission.User_Licensing_Edit) ||\n            canUpgradePremium)\n        {\n            user.LicenseKey = model.LicenseKey;\n            user.PremiumExpirationDate = model.PremiumExpirationDate;\n        }\n\n        await _userRepository.ReplaceAsync(user);\n        return RedirectToAction(\"Edit\", new { id });\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.User_Delete)]\n    public async Task<IActionResult> Delete(Guid id)\n    {\n        var user = await _userRepository.GetByIdAsync(id);\n        if (user != null)\n        {\n            await _userRepository.DeleteAsync(user);\n        }\n\n        return RedirectToAction(\"Index\");\n    }\n\n    [HttpPost]\n    [ValidateAntiForgeryToken]\n    [RequirePermission(Permission.User_NewDeviceException_Edit)]\n    public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)\n    {\n        var user = await _userRepository.GetByIdAsync(id);\n        if (user == null)\n        {\n            return RedirectToAction(\"Index\");\n        }\n\n        await _userService.ToggleNewDeviceVerificationException(user.Id);\n        return RedirectToAction(\"Edit\", new { id });\n    }\n}\n"
  },
  {
    "path": "src/Admin/Dockerfile",
    "content": "###############################################\n#           Node.js build stage               #\n###############################################\nFROM --platform=$BUILDPLATFORM node:20-alpine3.21 AS node-build\n\nWORKDIR /app\nCOPY src/Admin/package*.json ./\nCOPY /src/Admin/ .\nRUN npm ci\nRUN npm run build\n\n###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/src/Admin\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    icu-libs \\\n    tzdata \\\n    krb5 \\\n    shadow \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu\n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/src/Admin/out /app\nCOPY --from=node-build /app/wwwroot /app/wwwroot\nCOPY ./src/Admin/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\nHEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "src/Admin/Enums/HtmlHelperExtensions.cs",
    "content": "﻿\nusing Bit.SharedWeb.Utilities;\n\n// ReSharper disable once CheckNamespace\nnamespace Microsoft.AspNetCore.Mvc.Rendering;\n\npublic static class HtmlHelper\n{\n    public static IEnumerable<SelectListItem> GetEnumSelectList<T>(this IHtmlHelper htmlHelper, IEnumerable<T> values)\n    where T : Enum\n    {\n        return values.Select(v => new SelectListItem\n        {\n            Text = v.GetDisplayAttribute().Name,\n            Value = v.ToString()\n        });\n    }\n\n}\n"
  },
  {
    "path": "src/Admin/Enums/Permissions.cs",
    "content": "﻿namespace Bit.Admin.Enums;\n\npublic enum Permission\n{\n    User_List_View,\n    User_UserInformation_View,\n    User_GeneralDetails_View,\n    User_Delete,\n    User_UpgradePremium,\n    User_BillingInformation_View,\n    User_BillingInformation_DownloadInvoice,\n    User_BillingInformation_CreateEditTransaction,\n    User_Premium_View,\n    User_Premium_Edit,\n    User_Licensing_View,\n    User_Licensing_Edit,\n    User_Billing_View,\n    User_Billing_Edit,\n    User_Billing_LaunchGateway,\n    User_NewDeviceException_Edit,\n\n    Org_List_View,\n    Org_OrgInformation_View,\n    Org_GeneralDetails_View,\n    Org_Name_Edit,\n    Org_CheckEnabledBox,\n    Org_BusinessInformation_View,\n    Org_InitiateTrial,\n    Org_RequestDelete,\n    Org_Delete,\n    Org_BillingInformation_View,\n    Org_BillingInformation_DownloadInvoice,\n    Org_BillingInformation_CreateEditTransaction,\n    Org_Plan_View,\n    Org_Plan_Edit,\n    Org_Licensing_View,\n    Org_Licensing_Edit,\n    Org_Billing_View,\n    Org_Billing_Edit,\n    Org_Billing_LaunchGateway,\n    Org_Billing_ConvertToBusinessUnit,\n\n    Provider_List_View,\n    Provider_Create,\n    Provider_Edit,\n    Provider_View,\n    Provider_ResendEmailInvite,\n    Provider_CheckEnabledBox,\n\n    Tools_ChargeBrainTreeCustomer,\n    Tools_PromoteAdmin,\n    Tools_PromoteProviderServiceUser,\n    Tools_GenerateLicenseFile,\n    Tools_ManageTaxRates,\n    Tools_CreateEditTransaction,\n    Tools_ProcessStripeEvents\n}\n"
  },
  {
    "path": "src/Admin/HostedServices/AzureQueueMailHostedService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Azure.Storage.Queues;\nusing Azure.Storage.Queues.Models;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Admin.HostedServices;\n\npublic class AzureQueueMailHostedService : IHostedService\n{\n    private readonly ILogger<AzureQueueMailHostedService> _logger;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IMailService _mailService;\n    private CancellationTokenSource _cts;\n    private Task _executingTask;\n\n    private QueueClient _mailQueueClient;\n\n    public AzureQueueMailHostedService(\n        ILogger<AzureQueueMailHostedService> logger,\n        IMailService mailService,\n        GlobalSettings globalSettings)\n    {\n        _logger = logger;\n        _mailService = mailService;\n        _globalSettings = globalSettings;\n    }\n\n    public Task StartAsync(CancellationToken cancellationToken)\n    {\n        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        _executingTask = ExecuteAsync(_cts.Token);\n        return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;\n    }\n\n    public async Task StopAsync(CancellationToken cancellationToken)\n    {\n        if (_executingTask == null)\n        {\n            return;\n        }\n        _cts.Cancel();\n        await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));\n        cancellationToken.ThrowIfCancellationRequested();\n    }\n\n    private async Task ExecuteAsync(CancellationToken cancellationToken)\n    {\n        _mailQueueClient = new QueueClient(_globalSettings.Mail.ConnectionString, \"mail\");\n\n        QueueMessage[] mailMessages;\n        while (!cancellationToken.IsCancellationRequested)\n        {\n            if (!(mailMessages = await RetrieveMessagesAsync()).Any())\n            {\n                await Task.Delay(TimeSpan.FromSeconds(15));\n            }\n\n            foreach (var message in mailMessages)\n            {\n                try\n                {\n                    using var document = JsonDocument.Parse(message.DecodeMessageText());\n                    var root = document.RootElement;\n\n                    if (root.ValueKind == JsonValueKind.Array)\n                    {\n                        foreach (var mailQueueMessage in root.Deserialize<List<MailQueueMessage>>())\n                        {\n                            await _mailService.SendEnqueuedMailMessageAsync(mailQueueMessage);\n                        }\n                    }\n                    else if (root.ValueKind == JsonValueKind.Object)\n                    {\n                        var mailQueueMessage = root.Deserialize<MailQueueMessage>();\n                        await _mailService.SendEnqueuedMailMessageAsync(mailQueueMessage);\n                    }\n                }\n                catch (Exception e)\n                {\n                    _logger.LogError(e, \"Failed to send email\");\n                    // TODO: retries?\n                }\n\n                await _mailQueueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);\n\n                if (cancellationToken.IsCancellationRequested)\n                {\n                    break;\n                }\n            }\n        }\n    }\n\n    private async Task<QueueMessage[]> RetrieveMessagesAsync()\n    {\n        return (await _mailQueueClient.ReceiveMessagesAsync(maxMessages: 32))?.Value ?? new QueueMessage[] { };\n    }\n}\n"
  },
  {
    "path": "src/Admin/HostedServices/DatabaseMigrationHostedService.cs",
    "content": "﻿using System.Data.Common;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Admin.HostedServices;\n\npublic class DatabaseMigrationHostedService : IHostedService, IDisposable\n{\n    private readonly ILogger<DatabaseMigrationHostedService> _logger;\n    private readonly IDbMigrator _dbMigrator;\n\n    public DatabaseMigrationHostedService(\n        IDbMigrator dbMigrator,\n        ILogger<DatabaseMigrationHostedService> logger)\n    {\n        _logger = logger;\n        _dbMigrator = dbMigrator;\n    }\n\n    public virtual async Task StartAsync(CancellationToken cancellationToken)\n    {\n        // Wait 20 seconds to allow database to come online\n        await Task.Delay(20000, cancellationToken);\n\n        var maxMigrationAttempts = 10;\n        for (var i = 1; i <= maxMigrationAttempts; i++)\n        {\n            try\n            {\n                _dbMigrator.MigrateDatabase(true, cancellationToken);\n                // TODO: Maybe flip a flag somewhere to indicate migration is complete??\n                break;\n            }\n            catch (DbException e)\n            {\n                if (i >= maxMigrationAttempts)\n                {\n                    _logger.LogError(e, \"Database failed to migrate.\");\n                    throw;\n                }\n                else\n                {\n                    _logger.LogError(e,\n                        \"Database unavailable for migration. Trying again (attempt #{AttemptNumber})...\", i + 1);\n                    await Task.Delay(20000, cancellationToken);\n                }\n            }\n        }\n    }\n\n    public virtual Task StopAsync(CancellationToken cancellationToken)\n    {\n        return Task.FromResult(0);\n    }\n\n    public virtual void Dispose()\n    { }\n}\n"
  },
  {
    "path": "src/Admin/IdentityServer/CustomClaimsPrincipalFactory.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Admin.Services;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Options;\n\npublic class CustomClaimsPrincipalFactory : UserClaimsPrincipalFactory<IdentityUser>\n{\n    private IAccessControlService _accessControlService;\n    private readonly IGlobalSettings _globalSettings;\n\n    public CustomClaimsPrincipalFactory(\n        UserManager<IdentityUser> userManager,\n        IOptions<IdentityOptions> optionsAccessor,\n        IAccessControlService accessControlService,\n        IGlobalSettings globalSettings)\n            : base(userManager, optionsAccessor)\n    {\n        _accessControlService = accessControlService;\n        _globalSettings = globalSettings;\n    }\n\n    public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)\n    {\n        var principal = await base.CreateAsync(user);\n\n        if (!_globalSettings.SelfHosted &&\n            !string.IsNullOrEmpty(user.Email) &&\n            principal.Identity != null)\n        {\n            var role = _accessControlService.GetUserRole(user.Email);\n\n            if (!string.IsNullOrEmpty(role))\n            {\n                ((ClaimsIdentity)principal.Identity).AddClaims(\n                new[] { new Claim(ClaimTypes.Role, role) });\n            }\n        }\n\n        return principal;\n    }\n}\n"
  },
  {
    "path": "src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Admin.IdentityServer;\n\npublic class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore\n{\n    private readonly IConfiguration _configuration;\n\n    public ReadOnlyEnvIdentityUserStore(IConfiguration configuration)\n    {\n        _configuration = configuration;\n    }\n\n    public override Task<IdentityUser> FindByEmailAsync(string normalizedEmail,\n        CancellationToken cancellationToken = default)\n    {\n        var usersCsv = _configuration[\"adminSettings:admins\"];\n        if (!CoreHelpers.SettingHasValue(usersCsv))\n        {\n            return Task.FromResult<IdentityUser>(null);\n        }\n\n        var users = usersCsv.ToLowerInvariant().Split(',');\n        var usersDict = new Dictionary<string, string>();\n        foreach (var u in users)\n        {\n            var parts = u.Split(':');\n            if (parts.Length == 2)\n            {\n                var email = parts[0].Trim();\n                var stamp = parts[1].Trim();\n                usersDict.Add(email, stamp);\n            }\n            else\n            {\n                var email = parts[0].Trim();\n                usersDict.Add(email, email);\n            }\n        }\n\n        var userStamp = usersDict.GetValueOrDefault(normalizedEmail);\n        if (userStamp == null)\n        {\n            return Task.FromResult<IdentityUser>(null);\n        }\n\n        return Task.FromResult(new IdentityUser\n        {\n            Id = normalizedEmail,\n            Email = normalizedEmail,\n            NormalizedEmail = normalizedEmail,\n            EmailConfirmed = true,\n            UserName = normalizedEmail,\n            NormalizedUserName = normalizedEmail,\n            SecurityStamp = userStamp\n        });\n    }\n\n    public override Task<IdentityUser> FindByIdAsync(string userId,\n        CancellationToken cancellationToken = default)\n    {\n        return FindByEmailAsync(userId, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Admin/IdentityServer/ReadOnlyIdentityUserStore.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Admin.IdentityServer;\n\npublic abstract class ReadOnlyIdentityUserStore :\n    IUserEmailStore<IdentityUser>,\n    IUserSecurityStampStore<IdentityUser>\n{\n    public void Dispose() { }\n\n    public Task<IdentityResult> CreateAsync(IdentityUser user,\n        CancellationToken cancellationToken = default)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task<IdentityResult> DeleteAsync(IdentityUser user,\n        CancellationToken cancellationToken = default)\n    {\n        throw new NotImplementedException();\n    }\n\n    public abstract Task<IdentityUser> FindByEmailAsync(string normalizedEmail,\n        CancellationToken cancellationToken = default);\n\n    public abstract Task<IdentityUser> FindByIdAsync(string userId,\n        CancellationToken cancellationToken = default);\n\n    public async Task<IdentityUser> FindByNameAsync(string normalizedUserName,\n        CancellationToken cancellationToken = default)\n    {\n        return await FindByEmailAsync(normalizedUserName, cancellationToken);\n    }\n\n    public Task<string> GetEmailAsync(IdentityUser user,\n        CancellationToken cancellationToken = default)\n    {\n        return Task.FromResult(user.Email);\n    }\n\n    public Task<bool> GetEmailConfirmedAsync(IdentityUser user,\n        CancellationToken cancellationToken = default)\n    {\n        return Task.FromResult(user.EmailConfirmed);\n    }\n\n    public Task<string> GetNormalizedEmailAsync(IdentityUser user,\n        CancellationToken cancellationToken = default)\n    {\n        return Task.FromResult(user.Email);\n    }\n\n    public Task<string> GetNormalizedUserNameAsync(IdentityUser user,\n        CancellationToken cancellationToken = default)\n    {\n        return Task.FromResult(user.Email);\n    }\n\n    public Task<string> GetUserIdAsync(IdentityUser user,\n        CancellationToken cancellationToken = default)\n    {\n        return Task.FromResult(user.Id);\n    }\n\n    public Task<string> GetUserNameAsync(IdentityUser user,\n        CancellationToken cancellationToken = default)\n    {\n        return Task.FromResult(user.Email);\n    }\n\n    public Task SetEmailAsync(IdentityUser user, string email,\n        CancellationToken cancellationToken = default)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed,\n        CancellationToken cancellationToken = default)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail,\n        CancellationToken cancellationToken = default)\n    {\n        user.NormalizedEmail = normalizedEmail;\n        return Task.FromResult(0);\n    }\n\n    public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName,\n        CancellationToken cancellationToken = default)\n    {\n        user.NormalizedUserName = normalizedName;\n        return Task.FromResult(0);\n    }\n\n    public Task SetUserNameAsync(IdentityUser user, string userName,\n        CancellationToken cancellationToken = default)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task<IdentityResult> UpdateAsync(IdentityUser user,\n        CancellationToken cancellationToken = default)\n    {\n        return Task.FromResult(IdentityResult.Success);\n    }\n\n    public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task<string> GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken)\n    {\n        return Task.FromResult(user.SecurityStamp);\n    }\n}\n"
  },
  {
    "path": "src/Admin/IdentityServer/ServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Admin.Auth.IdentityServer;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Entities;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace Bit.Admin.IdentityServer;\n\npublic static class ServiceCollectionExtensions\n{\n    public static Tuple<IdentityBuilder, IdentityBuilder> AddPasswordlessIdentityServices<TUserStore>(\n        this IServiceCollection services, GlobalSettings globalSettings) where TUserStore : class\n    {\n        services.TryAddTransient<ILookupNormalizer, LowerInvariantLookupNormalizer>();\n        services.Configure<DataProtectionTokenProviderOptions>(options =>\n        {\n            options.TokenLifespan = TimeSpan.FromMinutes(15);\n        });\n\n        var passwordlessIdentityBuilder = services.AddIdentity<IdentityUser, Role>()\n            .AddUserStore<TUserStore>()\n            .AddRoleStore<RoleStore>()\n            .AddDefaultTokenProviders()\n            .AddClaimsPrincipalFactory<CustomClaimsPrincipalFactory>();\n\n        var regularIdentityBuilder = services.AddIdentityCore<User>()\n            .AddUserStore<UserStore>();\n\n        services.TryAddScoped<PasswordlessSignInManager<IdentityUser>, PasswordlessSignInManager<IdentityUser>>();\n\n        services.ConfigureApplicationCookie(options =>\n        {\n            options.LoginPath = \"/login\";\n            options.LogoutPath = \"/\";\n            options.AccessDeniedPath = \"/login?accessDenied=true\";\n            options.Cookie.Name = $\"Bitwarden_{globalSettings.ProjectName}\";\n            options.Cookie.HttpOnly = true;\n            options.ExpireTimeSpan = TimeSpan.FromDays(2);\n            options.ReturnUrlParameter = \"returnUrl\";\n            options.SlidingExpiration = true;\n        });\n\n        return new Tuple<IdentityBuilder, IdentityBuilder>(passwordlessIdentityBuilder, regularIdentityBuilder);\n    }\n}\n"
  },
  {
    "path": "src/Admin/Jobs/AliveJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Jobs;\nusing Bit.Core.Settings;\nusing Quartz;\n\nnamespace Bit.Admin.Jobs;\n\npublic class AliveJob : BaseJob\n{\n    private readonly GlobalSettings _globalSettings;\n    private HttpClient _httpClient = new HttpClient();\n\n    public AliveJob(\n        GlobalSettings globalSettings,\n        ILogger<AliveJob> logger)\n        : base(logger)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    protected async override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: Keep alive\");\n        var response = await _httpClient.GetAsync(_globalSettings.BaseServiceUri.Admin);\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Finished job task: Keep alive, {StatusCode}\",\n            response.StatusCode);\n    }\n}\n"
  },
  {
    "path": "src/Admin/Jobs/DatabaseExpiredSponsorshipsJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Jobs;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Quartz;\n\nnamespace Bit.Admin.Jobs;\n\npublic class DatabaseExpiredSponsorshipsJob : BaseJob\n{\n    private GlobalSettings _globalSettings;\n    private readonly IMaintenanceRepository _maintenanceRepository;\n\n    public DatabaseExpiredSponsorshipsJob(\n        IMaintenanceRepository maintenanceRepository,\n        ILogger<DatabaseExpiredSponsorshipsJob> logger,\n        GlobalSettings globalSettings)\n        : base(logger)\n    {\n        _maintenanceRepository = maintenanceRepository;\n        _globalSettings = globalSettings;\n    }\n\n    protected override async Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        if (_globalSettings.SelfHosted && !_globalSettings.EnableCloudCommunication)\n        {\n            return;\n        }\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: DeleteExpiredSponsorshipsAsync\");\n\n        // allow a 90 day grace period before deleting\n        var deleteDate = DateTime.UtcNow.AddDays(-90);\n\n        await _maintenanceRepository.DeleteExpiredSponsorshipsAsync(deleteDate);\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Finished job task: DeleteExpiredSponsorshipsAsync\");\n    }\n}\n"
  },
  {
    "path": "src/Admin/Jobs/DatabaseRebuildlIndexesJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Jobs;\nusing Bit.Core.Repositories;\nusing Quartz;\n\nnamespace Bit.Admin.Jobs;\n\npublic class DatabaseRebuildlIndexesJob : BaseJob\n{\n    private readonly IMaintenanceRepository _maintenanceRepository;\n\n    public DatabaseRebuildlIndexesJob(\n        IMaintenanceRepository maintenanceRepository,\n        ILogger<DatabaseRebuildlIndexesJob> logger)\n        : base(logger)\n    {\n        _maintenanceRepository = maintenanceRepository;\n    }\n\n    protected async override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: RebuildIndexesAsync\");\n        await _maintenanceRepository.RebuildIndexesAsync();\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Finished job task: RebuildIndexesAsync\");\n    }\n}\n"
  },
  {
    "path": "src/Admin/Jobs/DatabaseUpdateStatisticsJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Jobs;\nusing Bit.Core.Repositories;\nusing Quartz;\n\nnamespace Bit.Admin.Jobs;\n\npublic class DatabaseUpdateStatisticsJob : BaseJob\n{\n    private readonly IMaintenanceRepository _maintenanceRepository;\n\n    public DatabaseUpdateStatisticsJob(\n        IMaintenanceRepository maintenanceRepository,\n        ILogger<DatabaseUpdateStatisticsJob> logger)\n        : base(logger)\n    {\n        _maintenanceRepository = maintenanceRepository;\n    }\n\n    protected async override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: UpdateStatisticsAsync\");\n        await _maintenanceRepository.UpdateStatisticsAsync();\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Finished job task: UpdateStatisticsAsync\");\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: DisableCipherAutoStatsAsync\");\n        await _maintenanceRepository.DisableCipherAutoStatsAsync();\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Finished job task: DisableCipherAutoStatsAsync\");\n    }\n}\n"
  },
  {
    "path": "src/Admin/Jobs/DeleteCiphersJob.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core;\nusing Bit.Core.Jobs;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.Extensions.Options;\nusing Quartz;\n\nnamespace Bit.Admin.Jobs;\n\npublic class DeleteCiphersJob : BaseJob\n{\n    private readonly ICipherRepository _cipherRepository;\n    private readonly AdminSettings _adminSettings;\n\n    public DeleteCiphersJob(\n        ICipherRepository cipherRepository,\n        IOptions<AdminSettings> adminSettings,\n        ILogger<DeleteCiphersJob> logger)\n        : base(logger)\n    {\n        _cipherRepository = cipherRepository;\n        _adminSettings = adminSettings?.Value;\n    }\n\n    protected async override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: DeleteDeletedAsync\");\n        var deleteDate = DateTime.UtcNow.AddDays(-30);\n        var daysAgoSetting = (_adminSettings?.DeleteTrashDaysAgo).GetValueOrDefault();\n        if (daysAgoSetting > 0)\n        {\n            deleteDate = DateTime.UtcNow.AddDays(-1 * daysAgoSetting);\n        }\n        await _cipherRepository.DeleteDeletedAsync(deleteDate);\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Finished job task: DeleteDeletedAsync\");\n    }\n}\n"
  },
  {
    "path": "src/Admin/Jobs/DeleteUnverifiedOrganizationDomainsJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Jobs;\nusing Quartz;\n\nnamespace Bit.Admin.Jobs;\n\npublic class DeleteUnverifiedOrganizationDomainsJob : BaseJob\n{\n    private readonly IServiceProvider _serviceProvider;\n\n    public DeleteUnverifiedOrganizationDomainsJob(\n        IServiceProvider serviceProvider,\n        ILogger<DeleteUnverifiedOrganizationDomainsJob> logger)\n        : base(logger)\n    {\n        _serviceProvider = serviceProvider;\n    }\n\n    protected override async Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: DeleteUnverifiedOrganizationDomainsJob: Start\");\n        using (var serviceScope = _serviceProvider.CreateScope())\n        {\n            var organizationDomainService =\n                serviceScope.ServiceProvider.GetRequiredService<IOrganizationDomainService>();\n            await organizationDomainService.OrganizationDomainMaintenanceAsync();\n        }\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: DeleteUnverifiedOrganizationDomainsJob: End\");\n    }\n}\n"
  },
  {
    "path": "src/Admin/Jobs/JobsHostedService.cs",
    "content": "﻿using System.Runtime.InteropServices;\nusing Bit.Admin.Auth.Jobs;\nusing Bit.Admin.Tools.Jobs;\nusing Bit.Core.Jobs;\nusing Bit.Core.Settings;\nusing Quartz;\n\nnamespace Bit.Admin.Jobs;\n\npublic class JobsHostedService : BaseJobsHostedService\n{\n    public JobsHostedService(\n        GlobalSettings globalSettings,\n        IServiceProvider serviceProvider,\n        ILogger<JobsHostedService> logger,\n        ILogger<JobListener> listenerLogger)\n        : base(globalSettings, serviceProvider, logger, listenerLogger) { }\n\n    public override async Task StartAsync(CancellationToken cancellationToken)\n    {\n        var timeZone = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?\n            TimeZoneInfo.FindSystemTimeZoneById(\"Eastern Standard Time\") :\n            TimeZoneInfo.FindSystemTimeZoneById(\"America/New_York\");\n        if (_globalSettings.SelfHosted)\n        {\n            timeZone = TimeZoneInfo.Local;\n        }\n\n        var everyTopOfTheHourTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EveryTopOfTheHourTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 * * * ?\")\n            .Build();\n        var everyFiveMinutesTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EveryFiveMinutesTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 */5 * * * ?\")\n            .Build();\n        var everyFridayAt10pmTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EveryFridayAt10pmTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 22 ? * FRI\", x => x.InTimeZone(timeZone))\n            .Build();\n        var everySaturdayAtMidnightTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EverySaturdayAtMidnightTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 0 ? * SAT\", x => x.InTimeZone(timeZone))\n            .Build();\n        var everySundayAtMidnightTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EverySundayAtMidnightTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 0 ? * SUN\", x => x.InTimeZone(timeZone))\n            .Build();\n        var everyMondayAtMidnightTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EveryMondayAtMidnightTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 0 ? * MON\", x => x.InTimeZone(timeZone))\n            .Build();\n        var everyDayAtMidnightUtc = TriggerBuilder.Create()\n            .WithIdentity(\"EveryDayAtMidnightUtc\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 0 * * ?\")\n            .Build();\n        var everyFifteenMinutesTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"everyFifteenMinutesTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 */15 * ? * *\")\n            .Build();\n        var everyDayAtTwoAmUtcTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EveryDayAtTwoAmUtcTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 2 ? * * *\")\n            .Build();\n\n        var jobs = new List<Tuple<Type, ITrigger>>\n        {\n            new Tuple<Type, ITrigger>(typeof(DeleteSendsJob), everyFiveMinutesTrigger),\n            new Tuple<Type, ITrigger>(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger),\n            new Tuple<Type, ITrigger>(typeof(DeleteCiphersJob), everyDayAtMidnightUtc),\n            new Tuple<Type, ITrigger>(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger),\n            new Tuple<Type, ITrigger>(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger),\n            new Tuple<Type, ITrigger>(typeof(DeleteUnverifiedOrganizationDomainsJob), everyDayAtTwoAmUtcTrigger),\n        };\n\n        if (!(_globalSettings.SqlServer?.DisableDatabaseMaintenanceJobs ?? false))\n        {\n            jobs.Add(new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger));\n            jobs.Add(new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger));\n        }\n\n        if (!_globalSettings.SelfHosted)\n        {\n            jobs.Add(new Tuple<Type, ITrigger>(typeof(AliveJob), everyTopOfTheHourTrigger));\n        }\n\n        Jobs = jobs;\n        await base.StartAsync(cancellationToken);\n    }\n\n    public static void AddJobsServices(IServiceCollection services, bool selfHosted)\n    {\n        if (!selfHosted)\n        {\n            services.AddTransient<AliveJob>();\n        }\n        services.AddTransient<DatabaseUpdateStatisticsJob>();\n        services.AddTransient<DatabaseRebuildlIndexesJob>();\n        services.AddTransient<DatabaseExpiredGrantsJob>();\n        services.AddTransient<DatabaseExpiredSponsorshipsJob>();\n        services.AddTransient<DeleteSendsJob>();\n        services.AddTransient<DeleteCiphersJob>();\n        services.AddTransient<DeleteAuthRequestsJob>();\n        services.AddTransient<DeleteUnverifiedOrganizationDomainsJob>();\n    }\n}\n"
  },
  {
    "path": "src/Admin/Models/BillingInformationModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Models;\n\nnamespace Bit.Admin.Models;\n\npublic class BillingInformationModel\n{\n    public BillingInfo BillingInfo { get; set; }\n    public BillingHistoryInfo BillingHistoryInfo { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public string Entity { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/Models/ChargeBraintreeModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Admin.Models;\n\npublic class ChargeBraintreeModel : IValidatableObject\n{\n    [Required]\n    [Display(Name = \"Braintree Customer Id\")]\n    public string Id { get; set; }\n    [Required]\n    [Display(Name = \"Amount\")]\n    public decimal? Amount { get; set; }\n    public string TransactionId { get; set; }\n    public string PayPalTransactionId { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (Id != null)\n        {\n            if (Id.Length != 36 || (Id[0] != 'o' && Id[0] != 'u' && Id[0] != 'p') ||\n                !Guid.TryParse(Id.Substring(1, 32), out var guid))\n            {\n                yield return new ValidationResult(\"Customer Id is not a valid format.\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Models/CreateUpdateTransactionModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Admin.Models;\n\npublic class CreateUpdateTransactionModel : IValidatableObject\n{\n    public CreateUpdateTransactionModel() { }\n\n    public CreateUpdateTransactionModel(Transaction transaction)\n    {\n        Edit = true;\n        UserId = transaction.UserId;\n        OrganizationId = transaction.OrganizationId;\n        Amount = transaction.Amount;\n        RefundedAmount = transaction.RefundedAmount;\n        Refunded = transaction.Refunded.GetValueOrDefault();\n        Details = transaction.Details;\n        Date = transaction.CreationDate;\n        PaymentMethod = transaction.PaymentMethodType;\n        Gateway = transaction.Gateway;\n        GatewayId = transaction.GatewayId;\n        Type = transaction.Type;\n    }\n\n    public bool Edit { get; set; }\n\n    [Display(Name = \"User Id\")]\n    public Guid? UserId { get; set; }\n    [Display(Name = \"Organization Id\")]\n    public Guid? OrganizationId { get; set; }\n    [Required]\n    public decimal? Amount { get; set; }\n    [Display(Name = \"Refunded Amount\")]\n    public decimal? RefundedAmount { get; set; }\n    public bool Refunded { get; set; }\n    [Required]\n    public string Details { get; set; }\n    [Required]\n    public DateTime? Date { get; set; }\n    [Display(Name = \"Payment Method\")]\n    public PaymentMethodType? PaymentMethod { get; set; }\n    public GatewayType? Gateway { get; set; }\n    [Display(Name = \"Gateway Id\")]\n    public string GatewayId { get; set; }\n    [Required]\n    public TransactionType? Type { get; set; }\n\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if ((!UserId.HasValue && !OrganizationId.HasValue) || (UserId.HasValue && OrganizationId.HasValue))\n        {\n            yield return new ValidationResult(\"Must provide either User Id, or Organization Id.\");\n        }\n    }\n\n    public Transaction ToTransaction(Guid? id = null)\n    {\n        return new Transaction\n        {\n            Id = id.GetValueOrDefault(),\n            UserId = UserId,\n            OrganizationId = OrganizationId,\n            Amount = Amount.Value,\n            RefundedAmount = RefundedAmount,\n            Refunded = Refunded ? true : (bool?)null,\n            Details = Details,\n            CreationDate = Date.Value,\n            PaymentMethodType = PaymentMethod,\n            Gateway = Gateway,\n            GatewayId = GatewayId,\n            Type = Type.Value\n        };\n    }\n}\n"
  },
  {
    "path": "src/Admin/Models/CursorPagedModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Admin.Models;\n\npublic class CursorPagedModel<T>\n{\n    public List<T> Items { get; set; }\n    public int Count { get; set; }\n    public string Cursor { get; set; }\n    public string NextCursor { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/Models/ErrorViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Admin.Models;\n\npublic class ErrorViewModel\n{\n    public string RequestId { get; set; }\n\n    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);\n}\n"
  },
  {
    "path": "src/Admin/Models/HomeModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Settings;\n\nnamespace Bit.Admin.Models;\n\npublic class HomeModel\n{\n    public string CurrentVersion { get; set; }\n    public GlobalSettings GlobalSettings { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/Models/LicenseModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Admin.Models;\n\npublic class LicenseModel : IValidatableObject\n{\n    [Display(Name = \"User Id\")]\n    public Guid? UserId { get; set; }\n    [Display(Name = \"Organization Id\")]\n    public Guid? OrganizationId { get; set; }\n    [Display(Name = \"Installation Id\")]\n    public Guid? InstallationId { get; set; }\n    [Required]\n    [Display(Name = \"Version\")]\n    public int Version { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (UserId.HasValue && OrganizationId.HasValue)\n        {\n            yield return new ValidationResult(\"Use either User Id or Organization Id. Not both.\");\n        }\n\n        if (!UserId.HasValue && !OrganizationId.HasValue)\n        {\n            yield return new ValidationResult(\"User Id or Organization Id is required.\");\n        }\n\n        if (OrganizationId.HasValue && !InstallationId.HasValue)\n        {\n            yield return new ValidationResult(\"Installation Id is required for organization licenses.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Models/PagedModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Admin.Models;\n\npublic abstract class PagedModel<T>\n{\n    public List<T> Items { get; set; }\n    public int Page { get; set; }\n    public int Count { get; set; }\n    public int? PreviousPage => Page < 2 ? (int?)null : Page - 1;\n    public int? NextPage => Items.Count < Count ? (int?)null : Page + 1;\n}\n"
  },
  {
    "path": "src/Admin/Models/PromoteAdminModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Admin.Models;\n\npublic class PromoteAdminModel\n{\n    [Required]\n    [Display(Name = \"Admin User Id\")]\n    public Guid? UserId { get; set; }\n    [Required]\n    [Display(Name = \"Organization Id\")]\n    public Guid? OrganizationId { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/Models/PromoteProviderServiceUserModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Admin.Models;\n\npublic class PromoteProviderServiceUserModel\n{\n    [Required]\n    [Display(Name = \"Provider Service User Id\")]\n    public Guid? UserId { get; set; }\n    [Required]\n    [Display(Name = \"Provider Id\")]\n    public Guid? ProviderId { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/Models/UserEditModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Admin.Models;\n\npublic class UserEditModel\n{\n    public UserEditModel() { }\n\n    public UserEditModel(\n        User user,\n        bool isTwoFactorEnabled,\n        IEnumerable<Cipher> ciphers,\n        BillingInfo billingInfo,\n        BillingHistoryInfo billingHistoryInfo,\n        GlobalSettings globalSettings,\n        bool? claimedAccount,\n        bool? activeNewDeviceVerificationException)\n    {\n        User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, claimedAccount);\n\n        ActiveNewDeviceVerificationException = activeNewDeviceVerificationException ?? false;\n\n        BillingInfo = billingInfo;\n        BillingHistoryInfo = billingHistoryInfo;\n        BraintreeMerchantId = globalSettings.Braintree.MerchantId;\n\n        Name = user.Name;\n        Email = user.Email;\n        EmailVerified = user.EmailVerified;\n        Premium = user.Premium;\n        MaxStorageGb = user.MaxStorageGb;\n        Gateway = user.Gateway;\n        GatewayCustomerId = user.GatewayCustomerId;\n        GatewaySubscriptionId = user.GatewaySubscriptionId;\n        LicenseKey = user.LicenseKey;\n        PremiumExpirationDate = user.PremiumExpirationDate;\n    }\n\n    public UserViewModel User { get; init; }\n    public BillingInfo BillingInfo { get; init; }\n    public BillingHistoryInfo BillingHistoryInfo { get; init; }\n    public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);\n    public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString(\"yyyy-MM-ddTHH:mm\");\n    public string BraintreeMerchantId { get; init; }\n    public bool ActiveNewDeviceVerificationException { get; init; }\n\n\n    [Display(Name = \"Name\")]\n    public string Name { get; init; }\n    [Required]\n    [Display(Name = \"Email\")]\n    public string Email { get; init; }\n    [Display(Name = \"Email Verified\")]\n    public bool EmailVerified { get; init; }\n    [Display(Name = \"Premium\")]\n    public bool Premium { get; init; }\n    [Display(Name = \"Max. Storage GB\")]\n    public short? MaxStorageGb { get; init; }\n    [Display(Name = \"Gateway\")]\n    public Core.Enums.GatewayType? Gateway { get; init; }\n    [Display(Name = \"Gateway Customer Id\")]\n    public string GatewayCustomerId { get; init; }\n    [Display(Name = \"Gateway Subscription Id\")]\n    public string GatewaySubscriptionId { get; init; }\n    [Display(Name = \"License Key\")]\n    public string LicenseKey { get; init; }\n    [Display(Name = \"Premium Expiration Date\")]\n    public DateTime? PremiumExpirationDate { get; init; }\n}\n"
  },
  {
    "path": "src/Admin/Models/UserViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Admin.Models;\n\npublic class UserViewModel\n{\n    public Guid Id { get; }\n    public string Name { get; }\n    public string Email { get; }\n    public DateTime CreationDate { get; }\n    public DateTime? PremiumExpirationDate { get; }\n    public bool Premium { get; }\n    public short? MaxStorageGb { get; }\n    public bool EmailVerified { get; }\n    public bool? ClaimedAccount { get; }\n    public bool TwoFactorEnabled { get; }\n    public DateTime AccountRevisionDate { get; }\n    public DateTime RevisionDate { get; }\n    public DateTime? LastEmailChangeDate { get; }\n    public DateTime? LastKdfChangeDate { get; }\n    public DateTime? LastKeyRotationDate { get; }\n    public DateTime? LastPasswordChangeDate { get; }\n    public GatewayType? Gateway { get; }\n    public string GatewayCustomerId { get; }\n    public string GatewaySubscriptionId { get; }\n    public string LicenseKey { get; }\n    public int CipherCount { get; set; }\n\n    public UserViewModel(Guid id,\n        string name,\n        string email,\n        DateTime creationDate,\n        DateTime? premiumExpirationDate,\n        bool premium,\n        short? maxStorageGb,\n        bool emailVerified,\n        bool? claimedAccount,\n        bool twoFactorEnabled,\n        DateTime accountRevisionDate,\n        DateTime revisionDate,\n        DateTime? lastEmailChangeDate,\n        DateTime? lastKdfChangeDate,\n        DateTime? lastKeyRotationDate,\n        DateTime? lastPasswordChangeDate,\n        GatewayType? gateway,\n        string gatewayCustomerId,\n        string gatewaySubscriptionId,\n        string licenseKey,\n        IEnumerable<Cipher> ciphers)\n    {\n        Id = id;\n        Name = name;\n        Email = email;\n        CreationDate = creationDate;\n        PremiumExpirationDate = premiumExpirationDate;\n        Premium = premium;\n        MaxStorageGb = maxStorageGb;\n        EmailVerified = emailVerified;\n        ClaimedAccount = claimedAccount;\n        TwoFactorEnabled = twoFactorEnabled;\n        AccountRevisionDate = accountRevisionDate;\n        RevisionDate = revisionDate;\n        LastEmailChangeDate = lastEmailChangeDate;\n        LastKdfChangeDate = lastKdfChangeDate;\n        LastKeyRotationDate = lastKeyRotationDate;\n        LastPasswordChangeDate = lastPasswordChangeDate;\n        Gateway = gateway;\n        GatewayCustomerId = gatewayCustomerId;\n        GatewaySubscriptionId = gatewaySubscriptionId;\n        LicenseKey = licenseKey;\n        CipherCount = ciphers.Count();\n    }\n\n    public static IEnumerable<UserViewModel> MapViewModels(\n        IEnumerable<User> users,\n        IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>\n        users.Select(user => MapViewModel(user, lookup, false));\n\n    public static UserViewModel MapViewModel(User user,\n        IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? claimedAccount) =>\n        new(\n            user.Id,\n            user.Name,\n            user.Email,\n            user.CreationDate,\n            user.PremiumExpirationDate,\n            user.Premium,\n            user.MaxStorageGb,\n            user.EmailVerified,\n            claimedAccount,\n            IsTwoFactorEnabled(user, lookup),\n            user.AccountRevisionDate,\n            user.RevisionDate,\n            user.LastEmailChangeDate,\n            user.LastKdfChangeDate,\n            user.LastKeyRotationDate,\n            user.LastPasswordChangeDate,\n            user.Gateway,\n            user.GatewayCustomerId ?? string.Empty,\n            user.GatewaySubscriptionId ?? string.Empty,\n            user.LicenseKey ?? string.Empty,\n            Array.Empty<Cipher>());\n\n    public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>\n        MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>(), false);\n\n    public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? claimedAccount) =>\n        new(\n            user.Id,\n            user.Name,\n            user.Email,\n            user.CreationDate,\n            user.PremiumExpirationDate,\n            user.Premium,\n            user.MaxStorageGb,\n            user.EmailVerified,\n            claimedAccount,\n            isTwoFactorEnabled,\n            user.AccountRevisionDate,\n            user.RevisionDate,\n            user.LastEmailChangeDate,\n            user.LastKdfChangeDate,\n            user.LastKeyRotationDate,\n            user.LastPasswordChangeDate,\n            user.Gateway,\n            user.GatewayCustomerId ?? string.Empty,\n            user.GatewaySubscriptionId ?? string.Empty,\n            user.LicenseKey ?? string.Empty,\n            ciphers);\n\n    public static bool IsTwoFactorEnabled(User user,\n        IEnumerable<(Guid userId, bool twoFactorIsEnabled)> twoFactorIsEnabledLookup) =>\n        twoFactorIsEnabledLookup.FirstOrDefault(x => x.userId == user.Id).twoFactorIsEnabled;\n}\n"
  },
  {
    "path": "src/Admin/Models/UsersModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Admin.Models;\n\npublic class UsersModel : PagedModel<UserViewModel>\n{\n    public string Email { get; set; }\n    public string Action { get; set; }\n}\n"
  },
  {
    "path": "src/Admin/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Admin;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        Host\n            .CreateDefaultBuilder(args)\n            .UseBitwardenSdk()\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.ConfigureKestrel(o =>\n                {\n                    o.Limits.MaxRequestLineSize = 20_000;\n                });\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging()\n            .Build()\n            .Run();\n    }\n}\n"
  },
  {
    "path": "src/Admin/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:62911/\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Admin\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:62911/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Admin-SelfHost\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:62912/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"developSelfHosted\": \"true\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Admin/Sass/site.scss",
    "content": "﻿@import \"webfonts.scss\";\n@import \"bootstrap/scss/functions\";\n@import \"bootstrap/scss/variables\";\n@import \"bootstrap/scss/mixins\";\n\n$primary: #175DDC;\n$primary-accent: #1252A3;\n$success: #00a65a;\n$info: #555555;\n$warning: #bf7e16;\n$danger: #dd4b39;\n\n$theme-colors: map-merge($theme-colors, (\"primary-accent\": $primary-accent));\n\n$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';\n\n$h1-font-size: 2rem;\n$h2-font-size: 1.3rem;\n$h3-font-size: 1rem;\n$h4-font-size: 1rem;\n$h5-font-size: 1rem;\n$h6-font-size: 1rem;\n\n@import \"bootstrap/scss/bootstrap\";\n\nh1 {\n    border-bottom: 1px solid $border-color;\n    margin-bottom: 20px;\n\n    small {\n        color: $text-muted;\n        font-size: $h1-font-size * .5;\n    }\n}\n\nh2 {\n    text-transform: uppercase;\n    font-weight: bold;\n}\n\nh3 {\n    text-transform: uppercase;\n}\n\n.validation-summary-valid {\n    display: none;\n}\n\n.alert.validation-summary-errors > ul {\n    margin-bottom: 0;\n}\n\n.form-check-input {\n    margin-top: .45rem;\n}\n\na {\n    text-decoration: none;\n    \n    &:hover {\n      text-decoration: underline;\n    }\n}\n"
  },
  {
    "path": "src/Admin/Sass/webfonts.scss",
    "content": "@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 300;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-300.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 400;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-400.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 600;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-600.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 700;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-700.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: italic;\n\tfont-weight: 800;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-italic-800.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 300;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-300.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 400;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-400.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 600;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-600.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 700;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-700.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n@font-face {\n\tfont-family: 'Open Sans';\n\tfont-style: normal;\n\tfont-weight: 800;\n\tfont-display: auto;\n\tsrc: url(webfonts/Open_Sans-normal-800.woff) format('woff');\n\tunicode-range: U+0-10FFFF;\n}\n\n"
  },
  {
    "path": "src/Admin/Services/AccessControlService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing Bit.Admin.Enums;\nusing Bit.Admin.Utilities;\nusing Bit.Core.Settings;\n\nnamespace Bit.Admin.Services;\n\npublic class AccessControlService : IAccessControlService\n{\n    private readonly IHttpContextAccessor _httpContextAccessor;\n    private readonly IConfiguration _configuration;\n    private readonly IGlobalSettings _globalSettings;\n\n    public AccessControlService(\n        IHttpContextAccessor httpContextAccessor,\n        IConfiguration configuration,\n        IGlobalSettings globalSettings)\n    {\n        _httpContextAccessor = httpContextAccessor;\n        _configuration = configuration;\n        _globalSettings = globalSettings;\n    }\n\n    public bool UserHasPermission(Permission permission)\n    {\n        if (_globalSettings.SelfHosted)\n        {\n            return true;\n        }\n\n        var userRole = GetUserRoleFromClaim();\n        if (string.IsNullOrEmpty(userRole) || !RolePermissionMapping.RolePermissions.TryGetValue(userRole, out var rolePermissions))\n        {\n            return false;\n        }\n\n        return rolePermissions.Contains(permission);\n    }\n\n    public string GetUserRole(string userEmail)\n    {\n        var roles = _configuration.GetSection(\"adminSettings:role\").GetChildren();\n\n        if (roles == null || !roles.Any())\n        {\n            return null;\n        }\n\n        userEmail = userEmail.ToLowerInvariant();\n\n        var userRole = roles.FirstOrDefault(s => (s.Value != null ? s.Value.ToLowerInvariant().Split(',').Contains(userEmail) : false));\n\n        if (userRole == null)\n        {\n            return null;\n        }\n\n        return userRole.Key.ToLowerInvariant();\n    }\n\n    private string GetUserRoleFromClaim()\n    {\n        return _httpContextAccessor.HttpContext?.User?.Claims?\n                 .FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value;\n    }\n}\n"
  },
  {
    "path": "src/Admin/Services/IAccessControlService.cs",
    "content": "﻿using Bit.Admin.Enums;\n\nnamespace Bit.Admin.Services;\n\npublic interface IAccessControlService\n{\n    public bool UserHasPermission(Permission permission);\n    public string GetUserRole(string userEmail);\n}\n"
  },
  {
    "path": "src/Admin/Startup.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Admin.IdentityServer;\nusing Bit.Core.Context;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.SharedWeb.Utilities;\nusing Microsoft.AspNetCore.Identity;\nusing Stripe;\nusing Microsoft.AspNetCore.Mvc.Razor;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Bit.Admin.Services;\nusing Bit.Core.Billing.Extensions;\n\n#if !OSS\nusing Bit.Commercial.Core.Utilities;\nusing Bit.Commercial.Infrastructure.EntityFramework.SecretsManager;\n#endif\n\nnamespace Bit.Admin;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(\"en-US\");\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; private set; }\n    public IWebHostEnvironment Environment { get; set; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n        services.Configure<AdminSettings>(Configuration.GetSection(\"AdminSettings\"));\n\n        // Data Protection\n        services.AddCustomDataProtectionServices(Environment, globalSettings);\n\n        // Stripe Billing\n        StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;\n        StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;\n\n        // Repositories\n        var databaseProvider = services.AddDatabaseRepositories(globalSettings);\n        switch (databaseProvider)\n        {\n            case Core.Enums.SupportedDatabaseProviders.SqlServer:\n                services.AddSingleton<IDbMigrator, Migrator.SqlServerDbMigrator>();\n                break;\n            case Core.Enums.SupportedDatabaseProviders.MySql:\n                services.AddSingleton<IDbMigrator, MySqlMigrations.MySqlDbMigrator>();\n                break;\n            case Core.Enums.SupportedDatabaseProviders.Postgres:\n                services.AddSingleton<IDbMigrator, PostgresMigrations.PostgresDbMigrator>();\n                break;\n            case Core.Enums.SupportedDatabaseProviders.Sqlite:\n                services.AddSingleton<IDbMigrator, SqliteMigrations.SqliteDbMigrator>();\n                break;\n            default:\n                break;\n        }\n        services.AddTestPlayIdTracking(globalSettings);\n\n        // Context\n        services.AddScoped<ICurrentContext, CurrentContext>();\n        services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();\n\n        // Identity\n        services.AddPasswordlessIdentityServices<ReadOnlyEnvIdentityUserStore>(globalSettings);\n        services.Configure<SecurityStampValidatorOptions>(options =>\n        {\n            options.ValidationInterval = TimeSpan.FromMinutes(5);\n        });\n        if (globalSettings.SelfHosted)\n        {\n            services.ConfigureApplicationCookie(options =>\n            {\n                options.Cookie.Path = \"/admin\";\n            });\n        }\n\n        // Services\n        services.AddBaseServices(globalSettings);\n        services.AddDefaultServices(globalSettings);\n        services.AddScoped<IAccessControlService, AccessControlService>();\n        services.AddDistributedCache(globalSettings);\n        services.AddBillingOperations();\n        services.AddHttpClient();\n\n#if OSS\n        services.AddOosServices();\n#else\n        services.AddCommercialCoreServices();\n        services.AddSecretsManagerEfRepositories();\n#endif\n\n        // Mvc\n        services.AddMvc(config =>\n        {\n            config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());\n        });\n        services.Configure<RouteOptions>(options => options.LowercaseUrls = true);\n\n        services.Configure<RazorViewEngineOptions>(o =>\n         {\n             o.ViewLocationFormats.Add(\"/Auth/Views/{1}/{0}.cshtml\");\n             o.ViewLocationFormats.Add(\"/AdminConsole/Views/{1}/{0}.cshtml\");\n             o.ViewLocationFormats.Add(\"/Billing/Views/{1}/{0}.cshtml\");\n         });\n\n        // Jobs service\n        Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);\n        services.AddHostedService<Jobs.JobsHostedService>();\n        if (globalSettings.SelfHosted)\n        {\n            services.AddHostedService<HostedServices.DatabaseMigrationHostedService>();\n        }\n        else\n        {\n            if (CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString))\n            {\n                services.AddHostedService<HostedServices.AzureQueueMailHostedService>();\n            }\n        }\n    }\n\n    public void Configure(\n        IApplicationBuilder app,\n        IWebHostEnvironment env,\n        GlobalSettings globalSettings)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n\n        if (globalSettings.SelfHosted)\n        {\n            app.UsePathBase(\"/admin\");\n            app.UseForwardedHeaders(globalSettings);\n        }\n\n        if (env.IsDevelopment())\n        {\n            app.UseDeveloperExceptionPage();\n        }\n        else\n        {\n            app.UseExceptionHandler(\"/error\");\n        }\n\n        app.UseStaticFiles();\n        app.UseRouting();\n        app.UseAuthentication();\n        app.UseAuthorization();\n        app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());\n    }\n}\n"
  },
  {
    "path": "src/Admin/TagHelpers/ActivePageTagHelper.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Mvc.Controllers;\nusing Microsoft.AspNetCore.Mvc.Rendering;\nusing Microsoft.AspNetCore.Mvc.ViewFeatures;\nusing Microsoft.AspNetCore.Razor.TagHelpers;\n\nnamespace Bit.Admin.TagHelpers;\n\n[HtmlTargetElement(\"li\", Attributes = ActiveControllerName)]\n[HtmlTargetElement(\"li\", Attributes = ActiveActionName)]\npublic class ActivePageTagHelper : TagHelper\n{\n    private const string ActiveControllerName = \"active-controller\";\n    private const string ActiveActionName = \"active-action\";\n\n    private readonly IHtmlGenerator _generator;\n\n    public ActivePageTagHelper(IHtmlGenerator generator)\n    {\n        _generator = generator;\n    }\n\n    [HtmlAttributeNotBound]\n    [ViewContext]\n    public ViewContext ViewContext { get; set; }\n    [HtmlAttributeName(ActiveControllerName)]\n    public string ActiveController { get; set; }\n    [HtmlAttributeName(ActiveActionName)]\n    public string ActiveAction { get; set; }\n\n    public override void Process(TagHelperContext context, TagHelperOutput output)\n    {\n        if (context == null)\n        {\n            throw new ArgumentNullException(nameof(context));\n        }\n\n        if (output == null)\n        {\n            throw new ArgumentNullException(nameof(output));\n        }\n\n        if (ActiveAction == null && ActiveController == null)\n        {\n            return;\n        }\n\n        var descriptor = ViewContext.ActionDescriptor as ControllerActionDescriptor;\n        if (descriptor == null)\n        {\n            return;\n        }\n\n        var controllerMatch = ActiveMatch(ActiveController, descriptor.ControllerName);\n        var actionMatch = ActiveMatch(ActiveAction, descriptor.ActionName);\n        if (controllerMatch && actionMatch)\n        {\n            var classValue = \"active\";\n            if (output.Attributes[\"class\"] != null)\n            {\n                classValue += \" \" + output.Attributes[\"class\"].Value;\n                output.Attributes.Remove(output.Attributes[\"class\"]);\n            }\n\n            output.Attributes.Add(\"class\", classValue);\n        }\n    }\n\n    private bool ActiveMatch(string route, string descriptor)\n    {\n        return route == null || route == \"*\" ||\n            route.Split(',').Any(c => c.Trim().ToLower() == descriptor.ToLower());\n    }\n}\n"
  },
  {
    "path": "src/Admin/TagHelpers/OptionSelectedTagHelper.cs",
    "content": "﻿using Microsoft.AspNetCore.Mvc.ViewFeatures;\nusing Microsoft.AspNetCore.Razor.TagHelpers;\n\nnamespace Bit.Admin.TagHelpers;\n\n[HtmlTargetElement(\"option\", Attributes = SelectedName)]\npublic class OptionSelectedTagHelper : TagHelper\n{\n    private const string SelectedName = \"asp-selected\";\n\n    private readonly IHtmlGenerator _generator;\n\n    public OptionSelectedTagHelper(IHtmlGenerator generator)\n    {\n        _generator = generator;\n    }\n\n    [HtmlAttributeName(SelectedName)]\n    public bool Selected { get; set; }\n\n    public override void Process(TagHelperContext context, TagHelperOutput output)\n    {\n        if (context == null)\n        {\n            throw new ArgumentNullException(nameof(context));\n        }\n\n        if (output == null)\n        {\n            throw new ArgumentNullException(nameof(output));\n        }\n\n        if (Selected)\n        {\n            output.Attributes.Add(\"selected\", \"selected\");\n        }\n        else\n        {\n            output.Attributes.RemoveAll(\"selected\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Tools/Jobs/DeleteSendsJob.cs",
    "content": "﻿using Bit.Admin.Auth.Jobs;\nusing Bit.Core;\nusing Bit.Core.Jobs;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Commands.Interfaces;\nusing Quartz;\n\nnamespace Bit.Admin.Tools.Jobs;\n\npublic class DeleteSendsJob : BaseJob\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly IServiceProvider _serviceProvider;\n\n    public DeleteSendsJob(\n        ISendRepository sendRepository,\n        IServiceProvider serviceProvider,\n        ILogger<DatabaseExpiredGrantsJob> logger)\n        : base(logger)\n    {\n        _sendRepository = sendRepository;\n        _serviceProvider = serviceProvider;\n    }\n\n    protected async override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        var sends = await _sendRepository.GetManyByDeletionDateAsync(DateTime.UtcNow);\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Deleting {0} sends.\", sends.Count);\n        if (!sends.Any())\n        {\n            return;\n        }\n        using (var scope = _serviceProvider.CreateScope())\n        {\n            var nonAnonymousSendCommand = scope.ServiceProvider.GetRequiredService<INonAnonymousSendCommand>();\n            foreach (var send in sends)\n            {\n                await nonAnonymousSendCommand.DeleteSendAsync(send);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Utilities/RequirePermissionAttribute.cs",
    "content": "﻿using Bit.Admin.Enums;\nusing Bit.Admin.Services;\nusing Microsoft.AspNetCore.Mvc.Filters;\n\nnamespace Bit.Admin.Utilities;\n\npublic class RequirePermissionAttribute : ActionFilterAttribute\n{\n    public Permission Permission { get; set; }\n\n    public RequirePermissionAttribute(Permission permission)\n    {\n        Permission = permission;\n    }\n\n    public override void OnActionExecuting(ActionExecutingContext context)\n    {\n        var accessControlService = context.HttpContext.RequestServices.GetRequiredService<IAccessControlService>();\n\n        var hasPermission = accessControlService.UserHasPermission(Permission);\n        if (!hasPermission)\n        {\n            throw new UnauthorizedAccessException(\"Not authorized.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Admin/Utilities/RolePermissionMapping.cs",
    "content": "﻿using Bit.Admin.Enums;\n\nnamespace Bit.Admin.Utilities;\n\npublic static class RolePermissionMapping\n{\n    //This is temporary and will be moved to the db in the next round of the rbac implementation\n    public static readonly Dictionary<string, List<Permission>> RolePermissions = new Dictionary<string, List<Permission>>()\n    {\n        { \"owner\", new List<Permission>\n            {\n                Permission.User_List_View,\n                Permission.User_UserInformation_View,\n                Permission.User_GeneralDetails_View,\n                Permission.User_Delete,\n                Permission.User_UpgradePremium,\n                Permission.User_BillingInformation_View,\n                Permission.User_BillingInformation_DownloadInvoice,\n                Permission.User_Premium_View,\n                Permission.User_Premium_Edit,\n                Permission.User_Licensing_View,\n                Permission.User_Licensing_Edit,\n                Permission.User_Billing_View,\n                Permission.User_Billing_Edit,\n                Permission.User_Billing_LaunchGateway,\n                Permission.User_NewDeviceException_Edit,\n                Permission.Org_Name_Edit,\n                Permission.Org_CheckEnabledBox,\n                Permission.Org_List_View,\n                Permission.Org_OrgInformation_View,\n                Permission.Org_GeneralDetails_View,\n                Permission.Org_BusinessInformation_View,\n                Permission.Org_InitiateTrial,\n                Permission.Org_Delete,\n                Permission.Org_RequestDelete,\n                Permission.Org_BillingInformation_View,\n                Permission.Org_BillingInformation_DownloadInvoice,\n                Permission.Org_Plan_View,\n                Permission.Org_Plan_Edit,\n                Permission.Org_Licensing_View,\n                Permission.Org_Licensing_Edit,\n                Permission.Org_Billing_View,\n                Permission.Org_Billing_Edit,\n                Permission.Org_Billing_LaunchGateway,\n                Permission.Org_Billing_ConvertToBusinessUnit,\n                Permission.Provider_List_View,\n                Permission.Provider_Create,\n                Permission.Provider_View,\n                Permission.Provider_ResendEmailInvite,\n                Permission.Provider_CheckEnabledBox,\n                Permission.Tools_ChargeBrainTreeCustomer,\n                Permission.Tools_PromoteAdmin,\n                Permission.Tools_PromoteProviderServiceUser,\n                Permission.Tools_GenerateLicenseFile,\n                Permission.Tools_ManageTaxRates\n            }\n        },\n        { \"admin\", new List<Permission>\n            {\n                Permission.User_List_View,\n                Permission.User_UserInformation_View,\n                Permission.User_GeneralDetails_View,\n                Permission.User_Delete,\n                Permission.User_UpgradePremium,\n                Permission.User_BillingInformation_View,\n                Permission.User_BillingInformation_DownloadInvoice,\n                Permission.User_BillingInformation_CreateEditTransaction,\n                Permission.User_Premium_View,\n                Permission.User_Premium_Edit,\n                Permission.User_Licensing_View,\n                Permission.User_Licensing_Edit,\n                Permission.User_Billing_View,\n                Permission.User_Billing_Edit,\n                Permission.User_Billing_LaunchGateway,\n                Permission.User_NewDeviceException_Edit,\n                Permission.Org_Name_Edit,\n                Permission.Org_CheckEnabledBox,\n                Permission.Org_List_View,\n                Permission.Org_OrgInformation_View,\n                Permission.Org_GeneralDetails_View,\n                Permission.Org_BusinessInformation_View,\n                Permission.Org_Delete,\n                Permission.Org_RequestDelete,\n                Permission.Org_BillingInformation_View,\n                Permission.Org_BillingInformation_DownloadInvoice,\n                Permission.Org_BillingInformation_CreateEditTransaction,\n                Permission.Org_Plan_View,\n                Permission.Org_Plan_Edit,\n                Permission.Org_Licensing_View,\n                Permission.Org_Licensing_Edit,\n                Permission.Org_Billing_View,\n                Permission.Org_Billing_Edit,\n                Permission.Org_Billing_LaunchGateway,\n                Permission.Org_Billing_ConvertToBusinessUnit,\n                Permission.Org_InitiateTrial,\n                Permission.Provider_List_View,\n                Permission.Provider_Create,\n                Permission.Provider_View,\n                Permission.Provider_Edit,\n                Permission.Provider_ResendEmailInvite,\n                Permission.Provider_CheckEnabledBox,\n                Permission.Tools_ChargeBrainTreeCustomer,\n                Permission.Tools_PromoteAdmin,\n                Permission.Tools_PromoteProviderServiceUser,\n                Permission.Tools_GenerateLicenseFile,\n                Permission.Tools_ManageTaxRates,\n                Permission.Tools_CreateEditTransaction\n            }\n        },\n        { \"cs\", new List<Permission>\n            {\n                Permission.User_List_View,\n                Permission.User_UserInformation_View,\n                Permission.User_GeneralDetails_View,\n                Permission.User_UpgradePremium,\n                Permission.User_BillingInformation_View,\n                Permission.User_BillingInformation_DownloadInvoice,\n                Permission.User_Premium_View,\n                Permission.User_Licensing_View,\n                Permission.User_Billing_View,\n                Permission.User_Billing_LaunchGateway,\n                Permission.User_NewDeviceException_Edit,\n                Permission.Org_Name_Edit,\n                Permission.Org_CheckEnabledBox,\n                Permission.Org_List_View,\n                Permission.Org_OrgInformation_View,\n                Permission.Org_GeneralDetails_View,\n                Permission.Org_BusinessInformation_View,\n                Permission.Org_BillingInformation_View,\n                Permission.Org_BillingInformation_DownloadInvoice,\n                Permission.Org_Plan_View,\n                Permission.Org_Plan_Edit,\n                Permission.Org_Licensing_View,\n                Permission.Org_Billing_View,\n                Permission.Org_Billing_LaunchGateway,\n                Permission.Org_RequestDelete,\n                Permission.Provider_List_View,\n                Permission.Provider_View,\n                Permission.Provider_CheckEnabledBox\n            }\n        },\n        { \"billing\", new List<Permission>\n            {\n                Permission.User_List_View,\n                Permission.User_UserInformation_View,\n                Permission.User_GeneralDetails_View,\n                Permission.User_UpgradePremium,\n                Permission.User_BillingInformation_View,\n                Permission.User_BillingInformation_DownloadInvoice,\n                Permission.User_BillingInformation_CreateEditTransaction,\n                Permission.User_Premium_View,\n                Permission.User_Premium_Edit,\n                Permission.User_Licensing_View,\n                Permission.User_Billing_View,\n                Permission.User_Billing_Edit,\n                Permission.User_Billing_LaunchGateway,\n                Permission.Org_Name_Edit,\n                Permission.Org_CheckEnabledBox,\n                Permission.Org_List_View,\n                Permission.Org_OrgInformation_View,\n                Permission.Org_GeneralDetails_View,\n                Permission.Org_BusinessInformation_View,\n                Permission.Org_BillingInformation_View,\n                Permission.Org_BillingInformation_DownloadInvoice,\n                Permission.Org_BillingInformation_CreateEditTransaction,\n                Permission.Org_Plan_View,\n                Permission.Org_Plan_Edit,\n                Permission.Org_Licensing_View,\n                Permission.Org_Billing_View,\n                Permission.Org_Billing_Edit,\n                Permission.Org_Billing_LaunchGateway,\n                Permission.Org_Billing_ConvertToBusinessUnit,\n                Permission.Org_RequestDelete,\n                Permission.Provider_Edit,\n                Permission.Provider_View,\n                Permission.Provider_List_View,\n                Permission.Provider_CheckEnabledBox,\n                Permission.Tools_ChargeBrainTreeCustomer,\n                Permission.Tools_GenerateLicenseFile,\n                Permission.Tools_ManageTaxRates,\n                Permission.Tools_CreateEditTransaction,\n                Permission.Tools_ProcessStripeEvents\n            }\n        },\n        { \"sales\", new List<Permission>\n            {\n                Permission.User_List_View,\n                Permission.User_UserInformation_View,\n                Permission.User_GeneralDetails_View,\n                Permission.User_BillingInformation_View,\n                Permission.User_BillingInformation_DownloadInvoice,\n                Permission.User_Premium_View,\n                Permission.User_Licensing_View,\n                Permission.User_Licensing_Edit,\n                Permission.Org_Name_Edit,\n                Permission.Org_CheckEnabledBox,\n                Permission.Org_List_View,\n                Permission.Org_OrgInformation_View,\n                Permission.Org_GeneralDetails_View,\n                Permission.Org_BusinessInformation_View,\n                Permission.Org_InitiateTrial,\n                Permission.Org_BillingInformation_View,\n                Permission.Org_BillingInformation_DownloadInvoice,\n                Permission.Org_Plan_View,\n                Permission.Org_Plan_Edit,\n                Permission.Org_Licensing_View,\n                Permission.Org_Licensing_Edit,\n                Permission.Provider_List_View,\n                Permission.Provider_Create,\n                Permission.Provider_Edit,\n                Permission.Provider_View,\n                Permission.Provider_ResendEmailInvite\n            }\n        },\n    };\n}\n"
  },
  {
    "path": "src/Admin/Utilities/WebHostEnvironmentExtensions.cs",
    "content": "﻿namespace Bit.Admin.Utilities;\n\npublic static class WebHostEnvironmentExtensions\n{\n    public static string GetStripeUrl(this IWebHostEnvironment hostingEnvironment)\n    {\n        if (hostingEnvironment.IsDevelopment() || hostingEnvironment.IsEnvironment(\"QA\"))\n        {\n            return \"https://dashboard.stripe.com/test\";\n        }\n\n        return \"https://dashboard.stripe.com\";\n    }\n\n    public static string GetBraintreeMerchantUrl(this IWebHostEnvironment hostingEnvironment)\n    {\n        if (hostingEnvironment.IsDevelopment() || hostingEnvironment.IsEnvironment(\"QA\"))\n        {\n            return \"https://www.sandbox.braintreegateway.com/merchants\";\n        }\n\n        return \"https://www.braintreegateway.com/merchants\";\n    }\n}\n"
  },
  {
    "path": "src/Admin/Views/Home/Index.cshtml",
    "content": "﻿@using Bit.Admin.Controllers\n@model HomeModel\n@{\n    ViewData[\"Title\"] = \"Dashboard\";\n}\n\n@section Scripts {\n    <script>\n        (() => {\n            let loadedWebLatest = false;\n            let loadedWebInstalled = false;\n\n            fetch('@Url.Action(\"GetLatestVersion\", new { project = @ProjectType.Web })').then((response) => {\n                return response.json();\n            }).then((version) => {\n                document.getElementById('web-latest').innerText = version;\n                loadedWebLatest = true;\n                if (loadedWebInstalled) {\n                    checkVersions('web', document.getElementById('web-installed').innerText, version);\n                }\n            });\n\n            fetch('@Url.Action(\"GetLatestVersion\", new { project = @ProjectType.Core })').then((response) => {\n                return response.json();\n            }).then((version) => {\n                document.getElementById('server-latest').innerText = version;\n                checkVersions('core', '@Model.CurrentVersion', version);\n            });\n\n            fetch('@Url.Action(\"GetInstalledWebVersion\")').then((response) => {\n                return response.json();\n            }).then((version) => {\n                document.getElementById('web-installed').innerText = version;\n                loadedWebInstalled = true;\n                if (loadedWebLatest) {\n                    checkVersions('web', version, document.getElementById('web-latest').innerText);\n                }\n            });\n\n            function checkVersions(platform, installed, latest) {\n                if (installed === '-' || latest === '-') {\n                    return;\n                }\n\n                if (installed !== latest) {\n                    document.getElementById(platform + '-warning').classList.remove('d-none');\n                } else {\n                    document.getElementById(platform + '-success').classList.remove('d-none');\n                }\n            }\n        })();\n    </script>\n}\n\n<h1>Dashboard</h1>\n\n<p>Welcome to the Bitwarden System Administration Portal.</p>\n\n<h2>Version</h2>\n\n<div class=\"row\">\n    <div class=\"col-md\">\n        <dl class=\"row\">\n            <dt class=\"col-sm-5\">Server Installed</dt>\n            <dd class=\"col-sm-7\">\n                <span id=\"server-installed\">@Model.CurrentVersion</span>\n                <i class=\"fa fa-check text-success d-none\" id=\"core-success\" title=\"You are up to date!\"></i>\n                <i class=\"fa fa-warning text-warning d-none\" id=\"core-warning\"\n                   title=\"This version is not the latest. You should update.\"></i>\n            </dd>\n\n            <dt class=\"col-sm-5\">Server Latest</dt>\n            <dd class=\"col-sm-7\">\n                <span id=\"server-latest\" title=\"Checking version...\"><i class=\"fa fa-spinner fa-spin\"></i></span>\n            </dd>\n\n            <dt class=\"col-sm-5\">Web Installed</dt>\n            <dd class=\"col-sm-7\">\n                <span id=\"web-installed\"><i class=\"fa fa-spinner fa-spin\" title=\"Checking version...\"></i></span>\n                <i class=\"fa fa-check text-success d-none\" id=\"web-success\" title=\"You are up to date!\"></i>\n                <i class=\"fa fa-warning text-warning d-none\" id=\"web-warning\"\n                   title=\"This version is not the latest. You should update.\"></i>\n            </dd>\n\n            <dt class=\"col-sm-5\">Web Latest</dt>\n            <dd class=\"col-sm-7\">\n                <span id=\"web-latest\"><i class=\"fa fa-spinner fa-spin\" title=\"Checking version...\"></i></span>\n            </dd>\n        </dl>\n    </div>\n    <div class=\"col-md\">\n        <ul class=\"list-unstyled\">\n            <li><a href=\"https://github.com/bitwarden/server/releases\" target=\"_blank\" rel=\"noreferrer\">Check for Server\n                    updates</a></li>\n            <li><a href=\"https://github.com/bitwarden/clients/releases\" target=\"_blank\" rel=\"noreferrer\">Check for Web\n                    updates</a></li>\n            <li><a href=\"https://help.bitwarden.com/article/updating-on-premise/\" target=\"_blank\" rel=\"noreferrer\">How\n                    do I update?</a></li>\n        </ul>\n    </div>\n</div>\n\n<h2>Settings</h2>\n\n<h3>SMTP</h3>\n@if(!Bit.Core.Utilities.CoreHelpers.SettingHasValue(Model.GlobalSettings.Mail?.Smtp?.Host))\n{\n    <p class=\"text-body-secondary\">Not configured</p>\n}\nelse\n{\n    <dl class=\"row\">\n        <dt class=\"col-sm-4 col-lg-3\">Host</dt>\n        <dd class=\"col-sm-8 col-lg-9\">\n            @(string.IsNullOrWhiteSpace(Model.GlobalSettings.Mail.Smtp.Host) ? \"-\" : Model.GlobalSettings.Mail.Smtp.Host)\n        </dd>\n\n        <dt class=\"col-sm-4 col-lg-3\">Port</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@Model.GlobalSettings.Mail.Smtp.Port</dd>\n\n        <dt class=\"col-sm-4 col-lg-3\">SSL</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@(Model.GlobalSettings.Mail.Smtp.Ssl ? \"Yes\" : \"No\")</dd>\n\n        <dt class=\"col-sm-4 col-lg-3\">StartTLS</dt>\n        <dd class=\"col-sm-8 col-lg-9\">\n            @(Model.GlobalSettings.Mail.Smtp.StartTls ? \"Yes\" : Model.GlobalSettings.Mail.Smtp.Port == 25 ? \"No\" : \"Discover\")\n        </dd>\n\n        <dt class=\"col-sm-4 col-lg-3\">Username</dt>\n        <dd class=\"col-sm-8 col-lg-9\">\n            @(string.IsNullOrWhiteSpace(Model.GlobalSettings.Mail.Smtp.Username) ? \"-\" : Model.GlobalSettings.Mail.Smtp.Username)\n        </dd>\n    </dl>\n}\n\n<h3>Other</h3>\n\n<dl class=\"row\">\n    <dt class=\"col-sm-4 col-lg-3\">Installation Id</dt>\n    <dd class=\"col-sm-8 col-lg-9\">\n        @if(Model.GlobalSettings.Installation?.Id != null)\n        {\n            <code>@Model.GlobalSettings.Installation.Id</code>\n        }\n        else\n        {\n            <text>-</text>\n        }\n    </dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">User Registration</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.GlobalSettings.DisableUserRegistration ? \"Disabled\" : \"Enabled\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Push Notifications</dt>\n    <dd class=\"col-sm-8 col-lg-9\">\n        @if(Bit.Core.Utilities.CoreHelpers.SettingHasValue(Model.GlobalSettings.PushRelayBaseUri))\n        {\n            <span><i class=\"fa fa-check\"></i> Configured</span>\n        }\n        else\n        {\n            <span class=\"text-body-secondary\">Not configured</span>\n        }\n    </dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Yubico 2FA</dt>\n    <dd class=\"col-sm-8 col-lg-9\">\n        @if(Bit.Core.Utilities.CoreHelpers.SettingHasValue(Model.GlobalSettings.Yubico?.Key) && Bit.Core.Utilities.CoreHelpers.SettingHasValue(Model.GlobalSettings.Yubico?.ClientId))\n        {\n            <span><i class=\"fa fa-check\"></i> Configured</span>\n        }\n        else\n        {\n            <span class=\"text-body-secondary\">Not configured</span>\n        }\n    </dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Duo 2FA</dt>\n    <dd class=\"col-sm-8 col-lg-9\">\n        @if(Bit.Core.Utilities.CoreHelpers.SettingHasValue(Model.GlobalSettings.Duo?.AKey))\n        {\n            <span><i class=\"fa fa-check\"></i> Configured</span>\n        }\n        else\n        {\n            <span class=\"text-body-secondary\">Not configured</span>\n        }\n    </dd>\n</dl>\n"
  },
  {
    "path": "src/Admin/Views/Shared/Error.cshtml",
    "content": "﻿@model ErrorViewModel\n@{\n    ViewData[\"Title\"] = \"Error\";\n}\n\n<h1 class=\"text-danger\">Error</h1>\n<p class=\"text-danger\">An error occurred while processing your request.</p>\n\n@if(Model.ShowRequestId)\n{\n    <p>\n        <strong>Request ID:</strong> <code>@Model.RequestId</code>\n    </p>\n}\n"
  },
  {
    "path": "src/Admin/Views/Shared/_BillingInformation.cshtml",
    "content": "﻿@using Bit.Admin.Enums;\n@inject Bit.Admin.Services.IAccessControlService AccessControlService\n@model BillingInformationModel\n\n@{\n    var canManageTransactions = Model.Entity == \"User\" ? AccessControlService.UserHasPermission(Permission.User_BillingInformation_CreateEditTransaction)\n                                                       : AccessControlService.UserHasPermission(Permission.Org_BillingInformation_CreateEditTransaction);\n\n    var canDownloadInvoice = Model.Entity == \"User\" ? AccessControlService.UserHasPermission(Permission.User_BillingInformation_DownloadInvoice)\n                                                       : AccessControlService.UserHasPermission(Permission.Org_BillingInformation_DownloadInvoice);\n}\n\n<dl class=\"row\">\n    <dt id=\"billing-account-credit-balance\" class=\"col-sm-4 col-lg-3\">Account @(Model.BillingInfo.Balance <= 0 ? \"Credit\" : \"Balance\")</dt>\n    <dd id=\"billing-account-credit-balance-value\" class=\"col-sm-8 col-lg-9\">@Math.Abs(Model.BillingInfo.Balance).ToString(\"C\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Invoices</dt>\n    <dd class=\"col-sm-8 col-lg-9\">\n        @if(Model.BillingHistoryInfo.Invoices?.Any() ?? false)\n        {\n            <table class=\"table\">\n                <tbody>\n                @{ var invoiceIndex = 0; }\n                @foreach (var invoice in Model.BillingHistoryInfo.Invoices)\n                {\n                    <tr>\n                        <td id=\"invoice-@(invoiceIndex)-date\">@invoice.Date</td>\n                        <td><a id=\"invoice-@(invoiceIndex)-url\" target=\"_blank\" rel=\"noreferrer\" href=\"@invoice.Url\" title=\"View Invoice\">@invoice.Number</a>\n                        </td>\n                        <td id=\"invoice-@(invoiceIndex)-amount\">@invoice.Amount.ToString(\"C\")</td>\n                        <td id=\"invoice-@(invoiceIndex)-paid\">@(invoice.Paid ? \"Paid\" : \"Unpaid\")</td>\n                        @if (canDownloadInvoice)\n                        {\n                            <td>\n                                <a id=\"invoice-@(invoiceIndex)-pdf-url\" target=\"_blank\" rel=\"noreferrer\" href=\"@invoice.PdfUrl\" title=\"Download Invoice\">\n                                    <i class=\"fa fa-file-pdf-o\"></i>\n                                </a>\n                            </td>\n                        }\n                    </tr>\n                    invoiceIndex++;\n                }\n                </tbody>\n            </table>\n        }\n        else\n        {\n            @: No invoices.\n        }\n    </dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Transactions</dt>\n    <dd class=\"col-sm-8 col-lg-9\">\n        @if(Model.BillingHistoryInfo.Transactions?.Any() ?? false)\n        {\n            <table class=\"table\">\n                <tbody>\n                @{ var transactionIndex = 0; }\n                @foreach (var transaction in Model.BillingHistoryInfo.Transactions)\n                {\n                    <tr>\n                        <td id=\"transaction-@(transactionIndex)-created-date\">@transaction.CreatedDate</td>\n                        <td id=\"transaction-@(transactionIndex)-type\">@transaction.Type.ToString()</td>\n                        <td id=\"transaction-@(transactionIndex)-payment-method\">@transaction.PaymentMethodType.ToString()</td>\n                        <td id=\"transaction-@(transactionIndex)-details\">@transaction.Details</td>\n                        <td id=\"transaction-@(transactionIndex)-amount\">@transaction.Amount.ToString(\"C\")</td>\n                        @if (canManageTransactions)\n                        {\n                            <td>\n                                <a id=\"transaction-@(transactionIndex)-edit-link\" title=\"Edit Transaction\" asp-controller=\"Tools\" asp-action=\"EditTransaction\"\n                                   asp-route-id=\"@transaction.Id\"><i class=\"fa fa-edit\"></i></a>\n                            </td>\n                        }\n                    </tr>\n                    transactionIndex++;\n                }\n                </tbody>\n            </table>\n        }\n        else\n        {\n            <p>No transactions.</p>\n        }\n        @if (canManageTransactions)\n        {\n            <a id=\"transaction-create-transaction-link\" asp-action=\"CreateTransaction\" asp-controller=\"Tools\" asp-route-organizationId=\"@Model.OrganizationId\"\n            asp-route-userId=\"@Model.UserId\" class=\"btn btn-sm btn-outline-primary\">\n                <i class=\"fa fa-plus\"></i> New Transaction\n            </a>\n        }\n    </dd>\n</dl>\n"
  },
  {
    "path": "src/Admin/Views/Shared/_Layout.cshtml",
    "content": "@using Bit.Admin.Enums;\n@using Bit.Core;\n\n@inject SignInManager<IdentityUser> SignInManager\n@inject Bit.Core.Settings.GlobalSettings GlobalSettings\n@inject Bit.Admin.Services.IAccessControlService AccessControlService\n@inject Bit.Core.Services.IFeatureService FeatureService\n\n@{\n    var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View);\n    var canViewOrgs = AccessControlService.UserHasPermission(Permission.Org_List_View);\n    var canViewProviders = AccessControlService.UserHasPermission(Permission.Provider_List_View);\n    var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer);\n    var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction);\n    var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);\n    var canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);\n    var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);\n    var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);\n    var enablePersonalDiscounts = FeatureService.IsEnabled(FeatureFlagKeys.PM29108_EnablePersonalDiscounts);\n\n    var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser ||\n                        canGenerateLicense;\n}\n\n<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta name=\"robots\" content=\"noindex,nofollow\" />\n    <title>@ViewData[\"Title\"] - Bitwarden Admin Portal</title>\n\n    <link rel=\"stylesheet\" href=\"~/assets/site.css\" asp-append-version=\"true\" />\n</head>\n<body>\n    <nav class=\"navbar navbar-expand-md navbar-dark bg-dark mb-4\">\n        <div class=\"container\">\n            <a class=\"navbar-brand\" asp-controller=\"Home\" asp-action=\"Index\">\n                <i class=\"fa fa-lg fa-fw fa-shield\"></i> Admin\n            </a>\n            <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarCollapse\"\n                    aria-controls=\"navbarCollapse\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n                <span class=\"navbar-toggler-icon\"></span>\n            </button>\n            <div class=\"collapse navbar-collapse\" id=\"navbarCollapse\">\n                <ul class=\"navbar-nav me-auto mb-2 mb-md-0\">\n                    @if (SignInManager.IsSignedIn(User))\n                    {\n                        @if (canViewUsers)\n                        {\n                            <li class=\"nav-item\" active-controller=\"Users\">\n                                <a class=\"nav-link\" asp-controller=\"Users\" asp-action=\"Index\">Users</a>\n                            </li>\n                        }\n                        @if (canViewOrgs)\n                        {\n                            <li class=\"nav-item\" active-controller=\"Organizations\">\n                                <a class=\"nav-link\" asp-controller=\"Organizations\" asp-action=\"Index\">Organizations</a>\n                            </li>\n                        }\n                        @if (!GlobalSettings.SelfHosted)\n                        {\n                            @if (canViewProviders)\n                            {\n                                <li class=\"nav-item\" active-controller=\"Providers\">\n                                    <a class=\"nav-link\" asp-controller=\"Providers\" asp-action=\"Index\">Providers</a>\n                                </li>\n                            }\n                            @if (canCreateTransaction && enablePersonalDiscounts)\n                            {\n                                <li class=\"nav-item\" active-controller=\"SubscriptionDiscounts\">\n                                    <a class=\"nav-link\" asp-controller=\"SubscriptionDiscounts\" asp-action=\"Index\">\n                                        Discounts\n                                    </a>\n                                </li>\n                            }\n                            @if (canViewTools)\n                            {\n                                <li class=\"nav-item dropdown\" active-controller=\"tools\">\n                                    <a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"toolsDropdown\" role=\"button\"\n                                    data-bs-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n                                        Tools\n                                    </a>\n                                    <ul class=\"dropdown-menu\" aria-labelledby=\"toolsDropdown\">\n                                        @if (canChargeBraintree)\n                                        {\n                                            <a class=\"dropdown-item\" asp-controller=\"Tools\" asp-action=\"ChargeBraintree\">\n                                                Charge Braintree Customer\n                                            </a>\n                                        }\n                                        @if (canCreateTransaction)\n                                        {\n                                            <a class=\"dropdown-item\" asp-controller=\"Tools\" asp-action=\"CreateTransaction\">\n                                                Create Transaction\n                                            </a>\n                                        }\n                                        @if (canPromoteAdmin)\n                                        {\n                                            <a class=\"dropdown-item\" asp-controller=\"Tools\" asp-action=\"PromoteAdmin\">\n                                                Promote Organization Admin\n                                            </a>\n                                        }\n                                        @if (canPromoteProviderServiceUser)\n                                        {\n                                            <a class=\"dropdown-item\" asp-controller=\"Tools\" asp-action=\"PromoteProviderServiceUser\">\n                                                Promote Provider Service User\n                                            </a>\n                                        }\n                                        @if (canGenerateLicense)\n                                        {\n                                            <a class=\"dropdown-item\" asp-controller=\"Tools\" asp-action=\"GenerateLicense\">\n                                                Generate License\n                                            </a>\n                                        }\n                                         @if (canProcessStripeEvents)\n                                        {\n                                            <a class=\"dropdown-item\" asp-controller=\"ProcessStripeEvents\" asp-action=\"Index\">\n                                                Process Stripe Events\n                                            </a>\n                                        }\n                                    </ul>\n                                </li>\n                            }\n                        }\n                    }\n                    @if (GlobalSettings.SelfHosted)\n                    {\n                        <li class=\"nav-item\">\n                            <a class=\"nav-link\" href=\"https://help.bitwarden.com/hosting/\" target=\"_blank\"\n                                rel=\"noreferrer\">Docs</a>\n                        </li>\n                    }\n                </ul>\n                @if (SignInManager.IsSignedIn(User))\n                {\n                    <form asp-controller=\"Login\" asp-action=\"Logout\" method=\"post\">\n                        <button type=\"submit\" class=\"btn btn-sm btn-secondary\">Log Out</button>\n                    </form>\n                }\n                else\n                {\n                    <a class=\"btn btn-sm btn-secondary\" asp-controller=\"Login\" asp-action=\"Index\">Log In</a>\n                }\n            </div>\n        </div>\n    </nav>\n\n    <main role=\"main\" class=\"container\">\n        @RenderBody()\n    </main>\n\n    <footer class=\"container mb-4\">\n        <hr />\n        &copy; @DateTime.Now.Year, Bitwarden Inc.\n    </footer>\n\n    <script src=\"~/assets/site.js\" asp-append-version=\"true\"></script>\n\n    @if (TempData[\"Error\"] != null)\n    {\n        <script>\n            $(document).ready(function () {\n                toastr.error(\"@TempData[\"Error\"]\")\n            });\n        </script>\n    }\n    @if (TempData[\"Success\"] != null)\n    {\n        <script>\n            $(document).ready(function () {\n                toastr.success(\"@TempData[\"Success\"]\")\n            });\n        </script>\n    }\n\n    @RenderSection(\"Scripts\", required: false)\n</body>\n</html>\n"
  },
  {
    "path": "src/Admin/Views/Tools/ChargeBraintree.cshtml",
    "content": "﻿@model ChargeBraintreeModel\n@{\n    ViewData[\"Title\"] = \"Charge Braintree Customer\";\n}\n\n<h1>Charge Braintree Customer</h1>\n\n@if(!string.IsNullOrWhiteSpace(Model.TransactionId))\n{\n    <div class=\"alert alert-success\" role=\"alert\">\n        <p>Charged customer \"@Model.Id\" for @Model.Amount!.Value.ToString(\"C\").</p>\n        <strong>btTransactionId:</strong> @Model.TransactionId<br />\n        <strong>btPayPalTransactionId:</strong> @Model.PayPalTransactionId\n    </div>\n    <a asp-action=\"ChargeBraintree\" class=\"btn btn-secondary\">Charge Another Customer</a>\n}\nelse\n{\n    <form method=\"post\">\n        <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"form-group\">\n                    <label asp-for=\"Id\"></label>\n                    <input type=\"text\" class=\"form-control\" asp-for=\"Id\" required\n                           placeholder=\"ex. u298ccf9240b64f7f8b5da9e0003ba287cpz\">\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"form-group\">\n                    <label asp-for=\"Amount\"></label>\n                    <div class=\"input-group mb-3\">\n                        <div class=\"input-group-prepend\">\n                            <span class=\"input-group-text\">$</span>\n                        </div>\n                        <input type=\"number\" min=\"0.01\" max=\"10000.00\" step=\"0.01\" class=\"form-control\"\n                               asp-for=\"Amount\" required placeholder=\"ex. 10.00\">\n                    </div>\n                </div>\n            </div>\n        </div>\n        <button type=\"submit\" class=\"btn btn-primary mb-2\">Charge Customer</button>\n    </form>\n}\n"
  },
  {
    "path": "src/Admin/Views/Tools/CreateUpdateTransaction.cshtml",
    "content": "﻿@model CreateUpdateTransactionModel\n@{\n    var action = Model.Edit ? \"Edit\" : \"Create\";\n    ViewData[\"Title\"] = $\"{action} Transaction\";\n}\n\n<h1>@action Transaction</h1>\n\n<form method=\"post\">\n    <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n    <div class=\"row\">\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"UserId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"UserId\">\n            </div>\n        </div>\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"OrganizationId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"OrganizationId\">\n            </div>\n        </div>\n    </div>\n    <div class=\"row\">\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"Date\" class=\"form-label\"></label>\n                <input type=\"datetime-local\" class=\"form-control\" asp-for=\"Date\" required>\n            </div>\n        </div>\n        <div class=\"col-md\">\n            <div class=\"form-group\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"Type\" class=\"form-label\"></label>\n                    <select class=\"form-select\" asp-for=\"Type\" required\n                            asp-items=\"Html.GetEnumSelectList<Bit.Core.Enums.TransactionType>()\"></select>\n                </div>\n            </div>\n        </div>\n    </div>\n    <div class=\"row\">\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"Amount\" class=\"form-label\"></label>\n                <div class=\"input-group\">\n                    <span class=\"input-group-text\">$</span>\n                    <input type=\"number\" min=\"-1000000.00\" max=\"1000000.00\" step=\"0.01\" class=\"form-control\"\n                           asp-for=\"Amount\" required placeholder=\"ex. 10.00\">\n                </div>\n            </div>\n        </div>\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"RefundedAmount\" class=\"form-label\"></label>\n                <div class=\"input-group\">\n                    <span class=\"input-group-text\">$</span>\n                    <input type=\"number\" min=\"0.01\" max=\"1000000.00\" step=\"0.01\" class=\"form-control\"\n                           asp-for=\"RefundedAmount\" placeholder=\"ex. 10.00\">\n                </div>\n            </div>\n        </div>\n    </div>\n    <div class=\"form-check mb-3\">\n        <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"Refunded\">\n        <label class=\"form-check-label\" asp-for=\"Refunded\"></label>\n    </div>\n    <div class=\"mb-3\">\n        <label asp-for=\"Details\" class=\"form-label\"></label>\n        <input type=\"text\" class=\"form-control\" asp-for=\"Details\" required>\n    </div>\n    <div class=\"row\">\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"Gateway\" class=\"form-label\"></label>\n                <select class=\"form-select\" asp-for=\"Gateway\"\n                        asp-items=\"Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()\">\n                    <option value=\"\">--</option>\n                </select>\n            </div>\n        </div>\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"GatewayId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"GatewayId\">\n            </div>\n        </div>\n    </div>\n    <div class=\"row\">\n        <div class=\"col-md-6\">\n            <div class=\"mb-3\">\n                <label asp-for=\"PaymentMethod\" class=\"form-label\"></label>\n                <select class=\"form-select\" asp-for=\"PaymentMethod\"\n                        asp-items=\"Html.GetEnumSelectList<Bit.Core.Enums.PaymentMethodType>()\">\n                    <option value=\"\">--</option>\n                </select>\n            </div>\n        </div>\n    </div>\n    <button type=\"submit\" class=\"btn btn-primary mb-2\">@action Transaction</button>\n</form>\n"
  },
  {
    "path": "src/Admin/Views/Tools/GenerateLicense.cshtml",
    "content": "﻿@model LicenseModel\n@{\n    ViewData[\"Title\"] = \"Generate License File\";\n}\n\n<h1>Generate License File</h1>\n\n<form method=\"post\">\n    <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n    <div class=\"row\">\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"UserId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"UserId\">\n            </div>\n        </div>\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"OrganizationId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"OrganizationId\">\n            </div>\n        </div>\n    </div>\n    <div class=\"row\">\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"InstallationId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"InstallationId\">\n            </div>\n        </div>\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"Version\" class=\"form-label\"></label>\n                <input type=\"number\" class=\"form-control\" asp-for=\"Version\">\n            </div>\n        </div>\n    </div>\n    <button type=\"submit\" class=\"btn btn-primary mb-2\">Generate</button>\n</form>"
  },
  {
    "path": "src/Admin/Views/Tools/PromoteAdmin.cshtml",
    "content": "﻿@model PromoteAdminModel\n@{\n    ViewData[\"Title\"] = \"Promote Organization Admin\";\n}\n\n<h1>Promote Organization Admin</h1>\n\n<form method=\"post\">\n    <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n    <div class=\"row\">\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"UserId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"UserId\">\n            </div>\n        </div>\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"OrganizationId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"OrganizationId\">\n            </div>\n        </div>\n    </div>\n    <button type=\"submit\" class=\"btn btn-primary\">Promote Admin</button>\n</form>"
  },
  {
    "path": "src/Admin/Views/Tools/PromoteProviderServiceUser.cshtml",
    "content": "﻿@model PromoteProviderServiceUserModel\n@{\n    ViewData[\"Title\"] = \"Promote Provider Service User\";\n}\n\n<h1>Promote Provider Service User</h1>\n\n<form method=\"post\">\n    <div asp-validation-summary=\"All\" class=\"alert alert-danger\"></div>\n    <div class=\"row\">\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"UserId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"UserId\">\n            </div>\n        </div>\n        <div class=\"col-md\">\n            <div class=\"mb-3\">\n                <label asp-for=\"ProviderId\" class=\"form-label\"></label>\n                <input type=\"text\" class=\"form-control\" asp-for=\"ProviderId\">\n            </div>\n        </div>\n    </div>\n    <button type=\"submit\" class=\"btn btn-primary\">Promote Service User</button>\n</form>"
  },
  {
    "path": "src/Admin/Views/Users/Edit.cshtml",
    "content": "﻿@using Bit.Admin.Enums;\n@inject Bit.Admin.Services.IAccessControlService AccessControlService\n@inject Bit.Core.Services.IFeatureService FeatureService\n@inject Bit.Core.Settings.GlobalSettings GlobalSettings\n@inject IWebHostEnvironment HostingEnvironment\n@model UserEditModel\n@{\n    ViewData[\"Title\"] = \"User: \" + Model.User.Email;\n\n    var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View);\n    var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_NewDeviceException_Edit) &&\n                                    GlobalSettings.EnableNewDeviceVerification;\n    var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View);\n    var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View);\n    var canViewPremium = AccessControlService.UserHasPermission(Permission.User_Premium_View);\n    var canViewLicensing = AccessControlService.UserHasPermission(Permission.User_Licensing_View);\n    var canViewBilling = AccessControlService.UserHasPermission(Permission.User_Billing_View);\n\n    var canEditPremium = AccessControlService.UserHasPermission(Permission.User_Premium_Edit);\n    var canEditLicensing = AccessControlService.UserHasPermission(Permission.User_Licensing_Edit);\n    var canEditBilling = AccessControlService.UserHasPermission(Permission.User_Billing_Edit);\n    var canLaunchGateway = AccessControlService.UserHasPermission(Permission.User_Billing_LaunchGateway);\n    var canUpgradePremium = AccessControlService.UserHasPermission(Permission.User_UpgradePremium);\n    var canDeleteUser = AccessControlService.UserHasPermission(Permission.User_Delete);\n}\n\n@section Scripts {\n    <script>\n        (() => {\n            document.getElementById('upgrade-premium').addEventListener('click', () => {\n                if (document.getElementById('@(nameof(Model.Premium))').checked) {\n                    alert('User is already premium.');\n                    return;\n                }\n\n                // Premium\n                document.getElementById('@(nameof(Model.MaxStorageGb))').value = '1';\n                document.getElementById('@(nameof(Model.Premium))').checked = true;\n                // Licensing\n                document.getElementById('@(nameof(Model.LicenseKey))').value = '@Model.RandomLicenseKey';\n                document.getElementById('@(nameof(Model.PremiumExpirationDate))').value =\n                    '@Model.OneYearExpirationDate';\n            });\n\n            document.getElementById('gateway-customer-link').addEventListener('click', () => {\n                const gateway = document.getElementById('@(nameof(Model.Gateway))');\n                const customerId = document.getElementById('@(nameof(Model.GatewayCustomerId))');\n                if (!gateway || gateway.value === '' || !customerId || customerId.value === '') {\n                    return;\n                }\n\n                if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {\n                    const url = '@(HostingEnvironment.IsDevelopment()\n                ? \"https://dashboard.stripe.com/test\"\n                    : \"https://dashboard.stripe.com\")';\n                    window.open(`${url}/customers/${customerId.value}/`, '_blank');\n                } else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {\n                    const url = '@(HostingEnvironment.IsDevelopment()\n                ? $\"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}\"\n                    : $\"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}\")';\n                    window.open(`${url}/${customerId.value}`, '_blank');\n                }\n            });\n\n            document.getElementById('gateway-subscription-link').addEventListener('click', () => {\n                const gateway = document.getElementById('@(nameof(Model.Gateway))');\n                const subId = document.getElementById('@(nameof(Model.GatewaySubscriptionId))');\n                if (!gateway || gateway.value === '' || !subId || subId.value === '') {\n                    return;\n                }\n\n                if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {\n                    const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment(\"QA\")\n                ? \"https://dashboard.stripe.com/test\"\n                    : \"https://dashboard.stripe.com\")'\n                    window.open(`${url}/subscriptions/${subId.value}`, '_blank');\n                } else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {\n                    const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment(\"QA\")\n                ? $\"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}\"\n                    : $\"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}\")';\n                    window.open(`${url}/subscriptions/${subId.value}`, '_blank');\n                }\n            });\n        })();\n    </script>\n}\n\n<h1>User <small>@Model.User.Email</small></h1>\n\n@if (canViewUserInformation)\n{\n    <h2>User Information</h2>\n    @await Html.PartialAsync(\"_ViewInformation\", Model.User)\n}\n@if (canViewNewDeviceException)\n{\n    <h2>New Device Verification </h2>\n    <dl class=\"row\">\n        <dt class=\"col d-flex\">\n            <form asp-action=\"ToggleNewDeviceVerification\" asp-route-id=\"@Model.User.Id\" method=\"post\">\n                @if (Model.ActiveNewDeviceVerificationException)\n                {\n                    <p>Status: Bypassed</p>\n                    <button type=\"submit\" class=\"btn btn-success\" id=\"new-device-verification-exception\">Require New\n                        Device Verification</button>\n                }\n                else\n                {\n                    <p>Status: Required</p>\n                    <button type=\"submit\" class=\"btn btn-outline-danger\" id=\"new-device-verification-exception\">Bypass New\n                        Device Verification</button>\n                }\n            </form>\n\n        </dt>\n    </dl>\n}\n@if (canViewBillingInformation)\n{\n    <h2>Billing Information</h2>\n    @await Html.PartialAsync(\"_BillingInformation\",\n        new BillingInformationModel\n{\n    BillingInfo = Model.BillingInfo,\n    BillingHistoryInfo = Model.BillingHistoryInfo,\n    UserId = Model.User.Id,\n    Entity = \"User\"\n})\n}\n@if (canViewGeneral)\n{\n    <h2>General</h2>\n    <dl class=\"row\">\n        <dt class=\"col-sm-4 col-lg-3\">Name</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@Model.Name</dd>\n\n        <dt class=\"col-sm-4 col-lg-3\">Email</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@Model.Email</dd>\n    </dl>\n    <div class=\"form-check mb-3\">\n        <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"EmailVerified\" disabled>\n        <label class=\"form-check-label\" asp-for=\"EmailVerified\"></label>\n    </div>\n}\n<form method=\"post\" id=\"edit-form\">\n    @if (canViewPremium)\n    {\n        <h2>Premium</h2>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"MaxStorageGb\" class=\"form-label\"></label>\n                    <input type=\"number\" class=\"form-control\" asp-for=\"MaxStorageGb\" min=\"1\" readonly='@(!canEditPremium)'>\n                </div>\n            </div>\n        </div>\n        <div class=\"form-check mb-3\">\n            <input type=\"checkbox\" class=\"form-check-input\" asp-for=\"Premium\" readonly='@(!canUpgradePremium)'>\n            <label class=\"form-check-label\" asp-for=\"Premium\"></label>\n        </div>\n    }\n    @if (canViewLicensing)\n    {\n        <h2>Licensing</h2>\n        <div class=\"row\">\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"LicenseKey\" class=\"form-label\"></label>\n                    <input type=\"text\" class=\"form-control\" asp-for=\"LicenseKey\" readonly='@(!canEditLicensing)'>\n                </div>\n            </div>\n            <div class=\"col-sm\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"PremiumExpirationDate\" class=\"form-label\"></label>\n                    <input type=\"datetime-local\" class=\"form-control\" asp-for=\"PremiumExpirationDate\"\n                        readonly='@(!canEditLicensing)'>\n                </div>\n            </div>\n        </div>\n    }\n    @if (canViewBilling)\n    {\n        <h2>Billing</h2>\n        <div class=\"row\">\n            <div class=\"col-md\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"Gateway\" class=\"form-label\"></label>\n                    <select class=\"form-select\" asp-for=\"Gateway\" disabled='@(canEditBilling ? null : \"disabled\")'\n                        asp-items=\"Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()\">\n                        <option value=\"\">--</option>\n                    </select>\n                </div>\n            </div>\n            <div class=\"col-md\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"GatewayCustomerId\" class=\"form-label\"></label>\n                    <div class=\"input-group\">\n                        <input type=\"text\" class=\"form-control\" asp-for=\"GatewayCustomerId\" readonly='@(!canEditBilling)'>\n                        @if (canLaunchGateway)\n                        {\n                            <button class=\"btn btn-secondary\" type=\"button\" id=\"gateway-customer-link\">\n                                <i class=\"fa fa-external-link\"></i>\n                            </button>\n                        }\n                    </div>\n                </div>\n            </div>\n            <div class=\"col-md\">\n                <div class=\"mb-3\">\n                    <label asp-for=\"GatewaySubscriptionId\" class=\"form-label\"></label>\n                    <div class=\"input-group\">\n                        <input type=\"text\" class=\"form-control\" asp-for=\"GatewaySubscriptionId\"\n                            readonly='@(!canEditBilling)'>\n                        @if (canLaunchGateway)\n                        {\n                            <button class=\"btn btn-secondary\" type=\"button\" id=\"gateway-subscription-link\">\n                                <i class=\"fa fa-external-link\"></i>\n                            </button>\n                        }\n                    </div>\n                </div>\n            </div>\n        </div>\n    }\n</form>\n<div class=\"d-flex mt-4\">\n    <button type=\"submit\" class=\"btn btn-primary\" form=\"edit-form\">Save</button>\n    <div class=\"ms-auto d-flex\">\n        @if (canUpgradePremium)\n        {\n            <button class=\"btn btn-secondary me-2\" type=\"button\" id=\"upgrade-premium\">\n                Upgrade Premium\n            </button>\n        }\n        @if (canDeleteUser)\n        {\n            <form asp-action=\"Delete\" asp-route-id=\"@Model.User.Id\"\n                onsubmit=\"return confirm('Are you sure you want to delete this user?')\">\n                <button class=\"btn btn-danger\" type=\"submit\">Delete</button>\n            </form>\n        }\n    </div>\n</div>\n"
  },
  {
    "path": "src/Admin/Views/Users/Index.cshtml",
    "content": "﻿@model UsersModel\n@{\n    ViewData[\"Title\"] = \"Users\";\n}\n\n<h1>Users</h1>\n\n<form class=\"row row-cols-lg-auto g-3 align-items-center mb-2\" method=\"get\">\n    <div class=\"col-12\">\n        <label class=\"visually-hidden\" asp-for=\"Email\">Email</label>\n        <input type=\"text\" class=\"form-control\" placeholder=\"Email\" asp-for=\"Email\" name=\"email\">\n    </div>\n    <div class=\"col-12\">\n        <button type=\"submit\" class=\"btn btn-primary\" title=\"Search\">\n            <i class=\"fa fa-search\"></i> Search\n        </button>\n    </div>\n</form>\n\n<div class=\"table-responsive\">\n    <table class=\"table table-striped table-hover\">\n        <thead>\n        <tr>\n            <th>Email</th>\n            <th style=\"width: 150px;\">Created</th>\n            <th style=\"width: 170px; min-width: 170px;\">Details</th>\n        </tr>\n        </thead>\n        <tbody>\n        @if (!Model.Items.Any())\n        {\n            <tr>\n                <td colspan=\"4\">No results to list.</td>\n            </tr>\n        }\n        else\n        {\n            @foreach (var user in Model.Items)\n            {\n                <tr>\n                    <td>\n                        <a asp-action=\"@Model.Action\" asp-route-id=\"@user.Id\">@user.Email</a>\n                    </td>\n                    <td>\n                        <span title=\"@user.CreationDate.ToString()\">\n                            @user.CreationDate.ToShortDateString()\n                        </span>\n                    </td>\n                    <td>\n                        @if (user.Premium)\n                        {\n                            <i class=\"fa fa-star fa-lg fa-fw\"\n                               title=\"Premium, expires @(user.PremiumExpirationDate?.ToShortDateString() ?? \"-\")\">\n                            </i>\n                        }\n                        else\n                        {\n                            <i class=\"fa fa-star-o fa-lg fa-fw text-body-secondary\" title=\"Not Premium\"></i>\n                        }\n                        @if (user.MaxStorageGb.HasValue && user.MaxStorageGb > 1)\n                        {\n                            <i class=\"fa fa-plus-square fa-lg fa-fw\"\n                               title=\"Additional Storage, @(user.MaxStorageGb - 1) GB\">\n                            </i>\n                        }\n                        else\n                        {\n                            <i class=\"fa fa-plus-square-o fa-lg fa-fw text-body-secondary\"\n                               title=\"No Additional Storage\">\n                            </i>\n                        }\n                        @if (user.EmailVerified)\n                        {\n                            <i class=\"fa fa-check-circle fa-lg fa-fw\" title=\"Email Verified\"></i>\n                        }\n                        else\n                        {\n                            <i class=\"fa fa-times-circle-o fa-lg fa-fw text-body-secondary\" title=\"Email Not Verified\"></i>\n                        }\n                        @if (user.TwoFactorEnabled)\n                        {\n                            <i class=\"fa fa-lock fa-lg fa-fw\" title=\"2FA Enabled\"></i>\n                        }\n                        else\n                        {\n                            <i class=\"fa fa-unlock fa-lg fa-fw text-body-secondary\" title=\"2FA Not Enabled\"></i>\n                        }\n                    </td>\n                </tr>\n            }\n        }\n        </tbody>\n    </table>\n</div>\n\n<nav>\n    <ul class=\"pagination\">\n        @if (Model.PreviousPage.HasValue)\n        {\n            <li class=\"page-item\">\n                <a class=\"page-link\" asp-action=\"Index\" asp-route-page=\"@Model.PreviousPage.Value\"\n                   asp-route-count=\"@Model.Count\" asp-route-email=\"@Model.Email\">\n                    Previous\n                </a>\n            </li>\n        }\n        else\n        {\n            <li class=\"page-item disabled\">\n                <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Previous</a>\n            </li>\n        }\n        @if (Model.NextPage.HasValue)\n        {\n            <li class=\"page-item\">\n                <a class=\"page-link\" asp-action=\"Index\" asp-route-page=\"@Model.NextPage.Value\"\n                   asp-route-count=\"@Model.Count\" asp-route-email=\"@Model.Email\">\n                    Next\n                </a>\n            </li>\n        }\n        else\n        {\n            <li class=\"page-item disabled\">\n                <a class=\"page-link\" href=\"#\" tabindex=\"-1\">Next</a>\n            </li>\n        }\n    </ul>\n</nav>\n"
  },
  {
    "path": "src/Admin/Views/Users/View.cshtml",
    "content": "﻿@model UserViewModel\n@{\n    ViewData[\"Title\"] = \"User: \" + Model.Email;\n}\n\n<h1>User <small>@Model.Email</small></h1>\n\n<h2>Information</h2>\n@await Html.PartialAsync(\"_ViewInformation\", Model)\n<form asp-action=\"Delete\" asp-route-id=\"@Model.Id\"\n      onsubmit=\"return confirm('Are you sure you want to delete this user?')\">\n    <button class=\"btn btn-danger\" type=\"submit\">Delete</button>\n</form>\n"
  },
  {
    "path": "src/Admin/Views/Users/_ViewInformation.cshtml",
    "content": "@model UserViewModel\n<dl class=\"row\">\n    <dt class=\"col-sm-4 col-lg-3\">Id</dt>\n    <dd class=\"col-sm-8 col-lg-9\"><code>@Model.Id</code></dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Premium</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.Premium ? \"Yes\" : \"No\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Premium Expires</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.PremiumExpirationDate?.ToString() ?? \"-\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Email Verified</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.EmailVerified ? \"Yes\" : \"No\")</dd>\n\n    @if(Model.ClaimedAccount.HasValue)\n    {\n        <dt class=\"col-sm-4 col-lg-3\">Claimed Account</dt>\n        <dd class=\"col-sm-8 col-lg-9\">@(Model.ClaimedAccount.Value ? \"Yes\" : \"No\")</dd>\n    }\n\n    <dt class=\"col-sm-4 col-lg-3\">Using 2FA</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.TwoFactorEnabled ? \"Yes\" : \"No\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Items</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@Model.CipherCount</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Vault Modified</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@Model.AccountRevisionDate.ToString()</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Created</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@Model.CreationDate.ToString()</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Modified</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@Model.RevisionDate.ToString()</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Last Email Address Change</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.LastEmailChangeDate?.ToString() ?? \"-\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Last KDF Change</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.LastKdfChangeDate?.ToString() ?? \"-\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Last Key Rotation</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.LastKeyRotationDate?.ToString() ?? \"-\")</dd>\n\n    <dt class=\"col-sm-4 col-lg-3\">Last Password Change</dt>\n    <dd class=\"col-sm-8 col-lg-9\">@(Model.LastPasswordChangeDate?.ToString() ?? \"-\")</dd>\n\n</dl>\n"
  },
  {
    "path": "src/Admin/Views/_ViewImports.cshtml",
    "content": "@using Microsoft.AspNetCore.Identity\n@using Bit.Admin\n@using Bit.Admin.Models\n@using Bit.Core.Enums\n@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers\n@addTagHelper \"*, Admin\"\n"
  },
  {
    "path": "src/Admin/Views/_ViewStart.cshtml",
    "content": "﻿@{\n    Layout = \"_Layout\";\n}\n"
  },
  {
    "path": "src/Admin/appsettings.Development.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\",\n      \"internalScim\": \"http://localhost:44559\",\n      \"internalBilling\": \"http://localhost:44519\"\n    },\n    \"mail\": {\n      \"smtp\": {\n        \"host\": \"localhost\",\n        \"port\": 10250\n      }\n    },\n    \"events\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"storage\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"pricingUri\": \"https://billingpricing.qa.bitwarden.pw\"\n  }\n}\n"
  },
  {
    "path": "src/Admin/appsettings.Production.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.bitwarden.com\",\n      \"api\": \"https://api.bitwarden.com\",\n      \"identity\": \"https://identity.bitwarden.com\",\n      \"admin\": \"https://admin.bitwarden.com\",\n      \"notifications\": \"https://notifications.bitwarden.com\",\n      \"sso\": \"https://sso.bitwarden.com\",\n      \"internalNotifications\": \"https://notifications.bitwarden.com\",\n      \"internalAdmin\": \"https://admin.bitwarden.com\",\n      \"internalIdentity\": \"https://identity.bitwarden.com\",\n      \"internalApi\": \"https://api.bitwarden.com\",\n      \"internalVault\": \"https://vault.bitwarden.com\",\n      \"internalSso\": \"https://sso.bitwarden.com\",\n      \"internalScim\": \"https://scim.bitwarden.com\"\n    },\n    \"braintree\": {\n      \"production\": true\n    }\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n        \"Default\": \"Warning\",\n        \"System\": \"Warning\",\n        \"Microsoft\": \"Warning\",\n        \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Admin/appsettings.QA.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.qa.bitwarden.pw\",\n      \"api\": \"https://api.qa.bitwarden.pw\",\n      \"identity\": \"https://identity.qa.bitwarden.pw\",\n      \"admin\": \"https://admin.qa.bitwarden.pw\",\n      \"notifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"sso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalNotifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"internalAdmin\": \"https://admin.qa.bitwarden.pw\",\n      \"internalIdentity\": \"https://identity.qa.bitwarden.pw\",\n      \"internalApi\": \"https://api.qa.bitwarden.pw\",\n      \"internalVault\": \"https://vault.qa.bitwarden.pw\",\n      \"internalSso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalScim\": \"https://scim.qa.bitwarden.pw\"\n    },\n    \"braintree\": {\n      \"production\": false\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n        \"Default\": \"Debug\",\n        \"System\": \"Debug\",\n        \"Microsoft\": \"Debug\",\n        \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Admin/appsettings.SelfHosted.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": null,\n      \"api\": null,\n      \"identity\": null,\n      \"admin\": null,\n      \"notifications\": null,\n      \"sso\": null,\n      \"internalNotifications\": null,\n      \"internalAdmin\": null,\n      \"internalIdentity\": null,\n      \"internalApi\": null,\n      \"internalVault\": null,\n      \"internalSso\": null,\n      \"internalScim\": null\n    }\n  }\n}\n"
  },
  {
    "path": "src/Admin/appsettings.json",
    "content": "{\n  \"globalSettings\": {\n    \"selfHosted\": false,\n    \"siteName\": \"Bitwarden\",\n    \"projectName\": \"Admin\",\n    \"stripe\": {\n      \"apiKey\": \"SECRET\"\n    },\n    \"sqlServer\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"mail\": {\n      \"sendGridApiKey\": \"SECRET\",\n      \"amazonConfigSetName\": \"Email\",\n      \"replyToEmail\": \"no-reply@bitwarden.com\"\n    },\n    \"identityServer\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"dataProtection\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"storage\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"events\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"serviceBus\": {\n      \"connectionString\": \"SECRET\",\n      \"applicationCacheTopicName\": \"SECRET\"\n    },\n    \"notificationHub\": {\n      \"connectionString\": \"SECRET\",\n      \"hubName\": \"SECRET\"\n    },\n    \"amazon\": {\n      \"accessKeyId\": \"SECRET\",\n      \"accessKeySecret\": \"SECRET\",\n      \"region\": \"SECRET\"\n    },\n    \"send\": {\n      \"connectionString\": \"SECRET\"\n    }\n  },\n  \"adminSettings\": {\n    \"admins\": \"\"\n  },\n  \"braintree\": {\n    \"production\": false,\n    \"merchantId\": \"SECRET\",\n    \"publicKey\": \"SECRET\",\n    \"privateKey\": \"SECRET\"\n  }\n}\n"
  },
  {
    "path": "src/Admin/build.ps1",
    "content": "$curDir = pwd\n$dir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\necho \"`n## Building Admin\"\n\necho \"`nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore $dir\\Admin.csproj\necho \"Clean\"\ndotnet clean $dir\\Admin.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\necho \"Node Build\"\ncd $dir\nnpm ci\nnpm run build\ncd $curDir\necho \"Publish\"\ndotnet publish $dir\\Admin.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\n"
  },
  {
    "path": "src/Admin/build.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nCUR_DIR=\"$(pwd)\"\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\necho -e \"\\n## Building Admin\"\n\necho -e \"\\nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore \"$DIR/Admin.csproj\"\necho \"Clean\"\ndotnet clean \"$DIR/Admin.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\necho \"Node Build\"\ncd \"$DIR\"\nnpm ci\nnpm run build\ncd \"$CUR_DIR\"\necho \"Publish\"\ndotnet publish \"$DIR/Admin.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\n"
  },
  {
    "path": "src/Admin/bundleconfig.json",
    "content": "﻿// Configure bundling and minification for the project.\n// More info at https://go.microsoft.com/fwlink/?LinkId=808241\n[\n  {\n    \"outputFileName\": \"wwwroot/css/site.min.css\",\n    // An array of relative input file paths. Globbing patterns supported\n    \"inputFiles\": [\n      \"wwwroot/css/site.css\"\n    ]\n  },\n  {\n    \"outputFileName\": \"wwwroot/js/site.min.js\",\n    \"inputFiles\": [\n      \"wwwroot/js/site.js\"\n    ],\n    // Optionally specify minification options\n    \"minify\": {\n      \"enabled\": true,\n      \"renameLocals\": true\n    },\n    // Optionally generate .map file\n    \"sourceMap\": false\n  }\n]\n"
  },
  {
    "path": "src/Admin/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/core\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    if [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n      chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos\n    fi\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nif [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n    cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf\n    $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab\nfi\n\nexec $gosu_cmd /app/Admin\n"
  },
  {
    "path": "src/Admin/package.json",
    "content": "{\n  \"name\": \"bitwarden-admin\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Bitwarden System Admin Portal\",\n  \"repository\": \"https://github.com/bitwarden/server\",\n  \"license\": \"GPL-3.0\",\n  \"scripts\": {\n    \"build\": \"webpack\"\n  },\n  \"dependencies\": {\n    \"bootstrap\": \"5.3.6\",\n    \"font-awesome\": \"4.7.0\",\n    \"jquery\": \"3.7.1\",\n    \"toastr\": \"2.1.4\"\n  },\n  \"devDependencies\": {\n    \"css-loader\": \"7.1.2\",\n    \"expose-loader\": \"5.0.1\",\n    \"mini-css-extract-plugin\": \"2.9.2\",\n    \"sass\": \"1.97.2\",\n    \"sass-loader\": \"16.0.5\",\n    \"webpack\": \"5.104.1\",\n    \"webpack-cli\": \"6.0.1\"\n  }\n}\n"
  },
  {
    "path": "src/Admin/webfonts.list",
    "content": "﻿Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext"
  },
  {
    "path": "src/Admin/webpack.config.js",
    "content": "const path = require(\"path\");\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\n\nconst paths = {\n  assets: \"./wwwroot/assets/\",\n  sassDir: \"./Sass/\",\n};\n\n/** @type {import(\"webpack\").Configuration} */\nmodule.exports = {\n  mode: \"production\",\n  devtool: \"source-map\",\n  entry: {\n    site: [\n      path.resolve(__dirname, paths.sassDir, \"site.scss\"),\n      \"bootstrap\",\n      \"jquery\",\n      \"font-awesome/css/font-awesome.css\",\n      \"toastr\",\n      \"toastr/build/toastr.css\",\n    ],\n  },\n  output: {\n    clean: true,\n    path: path.resolve(__dirname, paths.assets),\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.(sa|sc|c)ss$/,\n        use: [MiniCssExtractPlugin.loader, \"css-loader\", \"sass-loader\"],\n      },\n      {\n        test: /.(ttf|otf|eot|svg|woff(2)?)(\\?[a-z0-9]+)?$/,\n        exclude: /loading(|-white).svg/,\n        generator: {\n          filename: \"fonts/[name].[contenthash][ext]\",\n        },\n        type: \"asset/resource\",\n      },\n\n      // Expose jquery and toastr globally so they can be used directly in asp.net\n      {\n        test: require.resolve(\"jquery\"),\n        loader: \"expose-loader\",\n        options: {\n          exposes: [\"$\", \"jQuery\"],\n        },\n      },\n      {\n        test: require.resolve(\"toastr\"),\n        loader: \"expose-loader\",\n        options: {\n          exposes: [\"toastr\"],\n        },\n      },\n    ],\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n      filename: \"[name].css\",\n    }),\n  ],\n};\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs",
    "content": "﻿using Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace Bit.Api.AdminConsole.Authorization;\n\npublic static class AuthorizationHandlerCollectionExtensions\n{\n    public static void AddAdminConsoleAuthorizationHandlers(this IServiceCollection services)\n    {\n        services.TryAddScoped<IOrganizationContext, OrganizationContext>();\n\n        services.TryAddEnumerable([\n            ServiceDescriptor.Scoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>(),\n            ServiceDescriptor.Scoped<IAuthorizationHandler, CollectionAuthorizationHandler>(),\n            ServiceDescriptor.Scoped<IAuthorizationHandler, GroupAuthorizationHandler>(),\n            ServiceDescriptor.Scoped<IAuthorizationHandler, OrganizationRequirementHandler>(),\n            ServiceDescriptor.Scoped<IAuthorizationHandler, RecoverAccountAuthorizationHandler>(),\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/AuthorizeAttribute.cs",
    "content": "﻿#nullable enable\n\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Api.AdminConsole.Authorization;\n\n/// <summary>\n/// An attribute which requires authorization using the specified requirement.\n/// This uses the standard ASP.NET authorization middleware.\n/// </summary>\n/// <typeparam name=\"T\">The IAuthorizationRequirement that will be used to authorize the user.</typeparam>\npublic class AuthorizeAttribute<T>\n    : AuthorizeAttribute, IAuthorizationRequirementData\n    where T : IAuthorizationRequirement, new()\n{\n    public IEnumerable<IAuthorizationRequirement> GetRequirements()\n    {\n        var requirement = new T();\n        return [requirement];\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/HttpContextExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\n\nnamespace Bit.Api.AdminConsole.Authorization;\n\npublic static class HttpContextExtensions\n{\n    public const string NoOrgIdError =\n        \"A route decorated with with '[Authorize<Requirement>]' must include a route value named 'orgId' or 'organizationId' either through the [Controller] attribute or through a '[Http*]' attribute.\";\n\n    /// <summary>\n    /// Returns the result of the callback, caching it in HttpContext.Features for the lifetime of the request.\n    /// Subsequent calls will retrieve the cached value.\n    /// Results are stored by type and therefore must be of a unique type.\n    /// </summary>\n    public static async Task<T> WithFeaturesCacheAsync<T>(this HttpContext httpContext, Func<Task<T>> callback)\n    {\n        var cachedResult = httpContext.Features.Get<T>();\n        if (cachedResult != null)\n        {\n            return cachedResult;\n        }\n\n        var result = await callback();\n        httpContext.Features.Set(result);\n\n        return result;\n    }\n\n    /// <summary>\n    /// Returns true if the user is a ProviderUser for a Provider which manages the specified organization, otherwise false.\n    /// </summary>\n    /// <remarks>\n    /// This data is fetched from the database and cached as a HttpContext Feature for the lifetime of the request.\n    /// </remarks>\n    public static async Task<bool> IsProviderUserForOrgAsync(\n        this HttpContext httpContext,\n        IProviderUserRepository providerUserRepository,\n        Guid userId,\n        Guid organizationId)\n    {\n        var organizations = await httpContext.GetProviderUserOrganizationsAsync(providerUserRepository, userId);\n        return organizations.Any(o => o.OrganizationId == organizationId);\n    }\n\n    /// <summary>\n    /// Returns the ProviderUserOrganizations for a user. These are the organizations the ProviderUser manages via their Provider, if any.\n    /// </summary>\n    /// <remarks>\n    /// This data is fetched from the database and cached as a HttpContext Feature for the lifetime of the request.\n    /// </remarks>\n    private static async Task<IEnumerable<ProviderUserOrganizationDetails>> GetProviderUserOrganizationsAsync(\n        this HttpContext httpContext,\n        IProviderUserRepository providerUserRepository,\n        Guid userId)\n        => await httpContext.WithFeaturesCacheAsync(() =>\n            providerUserRepository.GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed));\n\n\n    /// <summary>\n    /// Parses the {orgId} or {organizationId} route parameter into a Guid, or throws if neither are present or are not valid guids.\n    /// </summary>\n    /// <param name=\"httpContext\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"InvalidOperationException\"></exception>\n    public static Guid GetOrganizationId(this HttpContext httpContext)\n    {\n        var routeValues = httpContext.GetRouteData().Values;\n\n        routeValues.TryGetValue(\"orgId\", out var orgIdParam);\n        if (orgIdParam != null && Guid.TryParse(orgIdParam.ToString(), out var orgId))\n        {\n            return orgId;\n        }\n\n        routeValues.TryGetValue(\"organizationId\", out var organizationIdParam);\n        if (organizationIdParam != null && Guid.TryParse(organizationIdParam.ToString(), out var organizationId))\n        {\n            return organizationId;\n        }\n\n        throw new InvalidOperationException(NoOrgIdError);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/IOrganizationRequirement.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Context;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Api.AdminConsole.Authorization;\n\n/// <summary>\n/// A requirement that implements this interface will be handled by <see cref=\"OrganizationRequirementHandler\"/>,\n/// which calls AuthorizeAsync with the organization details from the route.\n/// This is used for simple role-based checks.\n/// This may only be used on endpoints with {orgId} in their path.\n/// </summary>\npublic interface IOrganizationRequirement : IAuthorizationRequirement\n{\n    /// <summary>\n    /// Whether to authorize a request that has this requirement.\n    /// </summary>\n    /// <param name=\"organizationClaims\">\n    /// The CurrentContextOrganization for the user if they are a member of the organization.\n    /// This is null if they are not a member.\n    /// </param>\n    /// <param name=\"isProviderUserForOrg\">\n    /// A callback that returns true if the user is a ProviderUser that manages the organization, otherwise false.\n    /// This requires a database query, call it last.\n    /// </param>\n    /// <returns>True if the requirement has been satisfied, otherwise false.</returns>\n    public Task<bool> AuthorizeAsync(\n        CurrentContextOrganization? organizationClaims,\n        Func<Task<bool>> isProviderUserForOrg);\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.AdminConsole.Authorization;\n\npublic static class OrganizationClaimsExtensions\n{\n    /// <summary>\n    /// Parses a user's claims and returns an object representing their claims for the specified organization.\n    /// </summary>\n    /// <param name=\"user\">The user who has the claims.</param>\n    /// <param name=\"organizationId\">The organizationId to look for in the claims.</param>\n    /// <returns>\n    /// A <see cref=\"CurrentContextOrganization\"/> representing the user's claims for that organization, or null\n    /// if the user does not have any claims for that organization.\n    /// </returns>\n    public static CurrentContextOrganization? GetCurrentContextOrganization(this ClaimsPrincipal user, Guid organizationId)\n    {\n        var hasClaim = GetClaimsParser(user, organizationId);\n\n        var role = GetRoleFromClaims(hasClaim);\n        if (!role.HasValue)\n        {\n            // Not an organization member\n            return null;\n        }\n\n        return new CurrentContextOrganization\n        {\n            Id = organizationId,\n            Type = role.Value,\n            AccessSecretsManager = hasClaim(Claims.SecretsManagerAccess),\n            Permissions = role == OrganizationUserType.Custom\n                ? GetPermissionsFromClaims(hasClaim)\n                : new Permissions()\n        };\n    }\n\n    /// <summary>\n    /// Returns a function for evaluating claims for the specified user and organizationId.\n    /// The function returns true if the claim type exists and false otherwise.\n    /// </summary>\n    private static Func<string, bool> GetClaimsParser(ClaimsPrincipal user, Guid organizationId)\n    {\n        // Group claims by ClaimType\n        var claimsDict = user.Claims\n            .GroupBy(c => c.Type)\n            .ToDictionary(\n                c => c.Key,\n                c => c.ToList());\n\n        return claimType\n            => claimsDict.TryGetValue(claimType, out var claims) &&\n               claims\n                   .ParseGuids()\n                   .Any(v => v == organizationId);\n    }\n\n    /// <summary>\n    /// Parses the provided claims into proper Guids, or ignore them if they are not valid guids.\n    /// </summary>\n    private static IEnumerable<Guid> ParseGuids(this IEnumerable<Claim> claims)\n    {\n        foreach (var claim in claims)\n        {\n            if (Guid.TryParse(claim.Value, out var guid))\n            {\n                yield return guid;\n            }\n        }\n    }\n\n    private static OrganizationUserType? GetRoleFromClaims(Func<string, bool> hasClaim)\n    {\n        if (hasClaim(Claims.OrganizationOwner))\n        {\n            return OrganizationUserType.Owner;\n        }\n\n        if (hasClaim(Claims.OrganizationAdmin))\n        {\n            return OrganizationUserType.Admin;\n        }\n\n        if (hasClaim(Claims.OrganizationCustom))\n        {\n            return OrganizationUserType.Custom;\n        }\n\n        if (hasClaim(Claims.OrganizationUser))\n        {\n            return OrganizationUserType.User;\n        }\n\n        return null;\n    }\n\n    private static Permissions GetPermissionsFromClaims(Func<string, bool> hasClaim)\n    => new()\n    {\n        AccessEventLogs = hasClaim(Claims.CustomPermissions.AccessEventLogs),\n        AccessImportExport = hasClaim(Claims.CustomPermissions.AccessImportExport),\n        AccessReports = hasClaim(Claims.CustomPermissions.AccessReports),\n        CreateNewCollections = hasClaim(Claims.CustomPermissions.CreateNewCollections),\n        EditAnyCollection = hasClaim(Claims.CustomPermissions.EditAnyCollection),\n        DeleteAnyCollection = hasClaim(Claims.CustomPermissions.DeleteAnyCollection),\n        ManageGroups = hasClaim(Claims.CustomPermissions.ManageGroups),\n        ManagePolicies = hasClaim(Claims.CustomPermissions.ManagePolicies),\n        ManageSso = hasClaim(Claims.CustomPermissions.ManageSso),\n        ManageUsers = hasClaim(Claims.CustomPermissions.ManageUsers),\n        ManageResetPassword = hasClaim(Claims.CustomPermissions.ManageResetPassword),\n        ManageScim = hasClaim(Claims.CustomPermissions.ManageScim),\n    };\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/OrganizationContext.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Services;\n\n// Note: do not move this into Core! See remarks below.\nnamespace Bit.Api.AdminConsole.Authorization;\n\n/// <summary>\n/// Provides information about a user's membership or provider relationship with an organization.\n/// Used for authorization decisions in the API layer, usually called by a controller or authorization handler or attribute.\n/// </summary>\n/// <remarks>\n/// This is intended to deprecate organization-related methods in <see cref=\"ICurrentContext\"/>.\n/// It should remain in the API layer (not Core) because it is closely tied to user claims and authentication.\n/// </remarks>\npublic interface IOrganizationContext\n{\n    /// <summary>\n    /// Parses the provided <see cref=\"ClaimsPrincipal\"/> for claims relating to the specified organization.\n    /// A user will have organization claims if they are a confirmed member of the organization.\n    /// </summary>\n    /// <param name=\"user\">The claims for the user.</param>\n    /// <param name=\"organizationId\">The organization to extract claims for.</param>\n    /// <returns>\n    /// A <see cref=\"CurrentContextOrganization\"/> representing the user's claims for the organization,\n    /// or null if the user has no claims.\n    /// </returns>\n    public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId);\n    /// <summary>\n    /// Used to determine whether the user is a ProviderUser for the specified organization.\n    /// </summary>\n    /// <param name=\"user\">The claims for the user.</param>\n    /// <param name=\"organizationId\">The organization to check the provider relationship for.</param>\n    /// <returns>True if the user is a ProviderUser for the specified organization, otherwise false.</returns>\n    /// <remarks>\n    /// This requires a database call, but the results are cached for the lifetime of the service instance.\n    /// Try to check purely claims-based sources of authorization first (such as organization membership with\n    /// <see cref=\"GetOrganizationClaims\"/>) to avoid unnecessary database calls.\n    /// </remarks>\n    public Task<bool> IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId);\n}\n\npublic class OrganizationContext(\n    IUserService userService,\n    IProviderUserRepository providerUserRepository) : IOrganizationContext\n{\n    public const string NoUserIdError = \"This method should only be called on the private api with a logged in user.\";\n\n    /// <summary>\n    /// Caches provider relationships by UserId.\n    /// In practice this should only have 1 entry (for the current user), but this approach ensures that a mix-up\n    /// between users cannot occur if <see cref=\"IsProviderUserForOrganization\"/> is called with a different\n    /// ClaimsPrincipal for any reason.\n    /// </summary>\n    private readonly Dictionary<Guid, IEnumerable<ProviderUserOrganizationDetails>> _providerUserOrganizationsCache = new();\n\n    public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId)\n    {\n        return user.GetCurrentContextOrganization(organizationId);\n    }\n\n    public async Task<bool> IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId)\n    {\n        var userId = userService.GetProperUserId(user);\n        if (!userId.HasValue)\n        {\n            throw new InvalidOperationException(NoUserIdError);\n        }\n\n        if (!_providerUserOrganizationsCache.TryGetValue(userId.Value, out var providerUserOrganizations))\n        {\n            providerUserOrganizations =\n                await providerUserRepository.GetManyOrganizationDetailsByUserAsync(userId.Value,\n                    ProviderUserStatusType.Confirmed);\n            providerUserOrganizations = providerUserOrganizations.ToList();\n            _providerUserOrganizationsCache[userId.Value] = providerUserOrganizations;\n        }\n\n        return providerUserOrganizations.Any(o => o.OrganizationId == organizationId);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/OrganizationRequirementHandler.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Api.AdminConsole.Authorization;\n\n/// <summary>\n/// Handles any requirement that implements <see cref=\"IOrganizationRequirement\"/>.\n/// Retrieves the Organization ID from the route and then passes it to the requirement's AuthorizeAsync callback to\n/// determine whether the action is authorized.\n/// </summary>\npublic class OrganizationRequirementHandler(\n    IHttpContextAccessor httpContextAccessor,\n    IProviderUserRepository providerUserRepository,\n    IUserService userService)\n    : AuthorizationHandler<IOrganizationRequirement>\n{\n    public const string NoHttpContextError = \"This method should only be called in the context of an HTTP Request.\";\n    public const string NoUserIdError = \"This method should only be called on the private api with a logged in user.\";\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IOrganizationRequirement requirement)\n    {\n        var httpContext = httpContextAccessor.HttpContext;\n        if (httpContext == null)\n        {\n            throw new InvalidOperationException(NoHttpContextError);\n        }\n\n        var organizationId = httpContext.GetOrganizationId();\n        var organizationClaims = httpContext.User.GetCurrentContextOrganization(organizationId);\n\n        var userId = userService.GetProperUserId(httpContext.User);\n        if (userId == null)\n        {\n            throw new InvalidOperationException(NoUserIdError);\n        }\n\n        Task<bool> IsProviderUserForOrg() => httpContext.IsProviderUserForOrgAsync(providerUserRepository, userId.Value, organizationId);\n\n        var authorized = await requirement.AuthorizeAsync(organizationClaims, IsProviderUserForOrg);\n\n        if (authorized)\n        {\n            context.Succeed(requirement);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/RecoverAccountAuthorizationHandler.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Api.AdminConsole.Authorization;\n\n/// <summary>\n/// An authorization requirement for recovering an organization member's account.\n/// </summary>\n/// <remarks>\n/// Note: this is different to simply being able to manage account recovery. The user must be recovering\n/// a member who has equal or lesser permissions than them.\n/// </remarks>\npublic class RecoverAccountAuthorizationRequirement : IAuthorizationRequirement;\n\n/// <summary>\n/// Authorizes members and providers to recover a target OrganizationUser's account.\n/// </summary>\n/// <remarks>\n/// This prevents privilege escalation by ensuring that a user cannot recover the account of\n/// another user with a higher role or with provider membership.\n/// </remarks>\npublic class RecoverAccountAuthorizationHandler(\n    IOrganizationContext organizationContext,\n    ICurrentContext currentContext,\n    IProviderUserRepository providerUserRepository)\n    : AuthorizationHandler<RecoverAccountAuthorizationRequirement, OrganizationUser>\n{\n    public const string FailureReason = \"You are not permitted to recover this user's account.\";\n    public const string ProviderFailureReason = \"You are not permitted to recover a Provider member's account.\";\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        RecoverAccountAuthorizationRequirement requirement,\n        OrganizationUser targetOrganizationUser)\n    {\n        // Step 1: check that the User has permissions with respect to the organization.\n        // This may come from their role in the organization or their provider relationship.\n        var canRecoverOrganizationMember =\n            AuthorizeMember(context.User, targetOrganizationUser) ||\n            await AuthorizeProviderAsync(context.User, targetOrganizationUser);\n\n        if (!canRecoverOrganizationMember)\n        {\n            context.Fail(new AuthorizationFailureReason(this, FailureReason));\n            return;\n        }\n\n        // Step 2: check that the User has permissions with respect to any provider the target user is a member of.\n        // This prevents an organization admin performing privilege escalation into an unrelated provider.\n        var canRecoverProviderMember = await CanRecoverProviderAsync(targetOrganizationUser);\n        if (!canRecoverProviderMember)\n        {\n            context.Fail(new AuthorizationFailureReason(this, ProviderFailureReason));\n            return;\n        }\n\n        context.Succeed(requirement);\n    }\n\n    private async Task<bool> AuthorizeProviderAsync(ClaimsPrincipal currentUser, OrganizationUser targetOrganizationUser)\n    {\n        return await organizationContext.IsProviderUserForOrganization(currentUser, targetOrganizationUser.OrganizationId);\n    }\n\n    private bool AuthorizeMember(ClaimsPrincipal currentUser, OrganizationUser targetOrganizationUser)\n    {\n        var currentContextOrganization = organizationContext.GetOrganizationClaims(currentUser, targetOrganizationUser.OrganizationId);\n        if (currentContextOrganization == null)\n        {\n            return false;\n        }\n\n        // Current user must have equal or greater permissions than the user account being recovered\n        var authorized = targetOrganizationUser.Type switch\n        {\n            OrganizationUserType.Owner => currentContextOrganization.Type is OrganizationUserType.Owner,\n            OrganizationUserType.Admin => currentContextOrganization.Type is OrganizationUserType.Owner or OrganizationUserType.Admin,\n            _ => currentContextOrganization is\n            { Type: OrganizationUserType.Owner or OrganizationUserType.Admin }\n                or { Type: OrganizationUserType.Custom, Permissions.ManageResetPassword: true }\n        };\n\n        return authorized;\n    }\n\n    private async Task<bool> CanRecoverProviderAsync(OrganizationUser targetOrganizationUser)\n    {\n        if (!targetOrganizationUser.UserId.HasValue)\n        {\n            // If an OrganizationUser is not linked to a User then it can't be linked to a Provider either.\n            // This is invalid but does not pose a privilege escalation risk. Return early and let the command\n            // handle the invalid input.\n            return true;\n        }\n\n        var targetUserProviderUsers =\n            await providerUserRepository.GetManyByUserAsync(targetOrganizationUser.UserId.Value);\n\n        // If the target user belongs to any provider that the current user is not a member of,\n        // deny the action to prevent privilege escalation from organization to provider.\n        // Note: we do not expect that a user is a member of more than 1 provider, but there is also no guarantee\n        // against it; this returns a sequence, so we handle the possibility.\n        var authorized = targetUserProviderUsers.All(providerUser => currentContext.ProviderUser(providerUser.ProviderId));\n        return authorized;\n    }\n}\n\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.AdminConsole.Authorization.Requirements;\n\n/// <summary>\n/// A base implementation of <see cref=\"IOrganizationRequirement\"/> which will authorize Owners, Admins, Providers,\n/// and custom users with the permission specified by the permissionPicker constructor parameter. This is suitable\n/// for most requirements related to a custom permission.\n/// </summary>\n/// <param name=\"permissionPicker\">A function that returns a custom permission which will authorize the action.</param>\npublic abstract class BasePermissionRequirement(Func<Permissions, bool> permissionPicker) : IOrganizationRequirement\n{\n    public async Task<bool> AuthorizeAsync(CurrentContextOrganization? organizationClaims,\n        Func<Task<bool>> isProviderUserForOrg)\n    => organizationClaims switch\n    {\n        { Type: OrganizationUserType.Owner } => true,\n        { Type: OrganizationUserType.Admin } => true,\n        { Type: OrganizationUserType.Custom } when permissionPicker(organizationClaims.Permissions) => true,\n        _ => await isProviderUserForOrg()\n    };\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.AdminConsole.Authorization.Requirements;\n\npublic class ManageGroupsOrUsersRequirement : IOrganizationRequirement\n{\n    public async Task<bool> AuthorizeAsync(CurrentContextOrganization? organizationClaims, Func<Task<bool>> isProviderUserForOrg) =>\n        organizationClaims switch\n        {\n            { Type: OrganizationUserType.Owner } => true,\n            { Type: OrganizationUserType.Admin } => true,\n            { Permissions.ManageGroups: true } => true,\n            { Permissions.ManageUsers: true } => true,\n            _ => await isProviderUserForOrg()\n        };\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/Requirements/MemberOrProviderRequirement.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Context;\n\nnamespace Bit.Api.AdminConsole.Authorization.Requirements;\n\n/// <summary>\n/// Requires that the user is a member of the organization or a provider for the organization.\n/// </summary>\npublic class MemberOrProviderRequirement : IOrganizationRequirement\n{\n    public async Task<bool> AuthorizeAsync(\n        CurrentContextOrganization? organizationClaims,\n        Func<Task<bool>> isProviderUserForOrg)\n        => organizationClaims is not null || await isProviderUserForOrg();\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/Requirements/MemberRequirement.cs",
    "content": "﻿using Bit.Core.Context;\n\nnamespace Bit.Api.AdminConsole.Authorization.Requirements;\n\n/// <summary>\n/// Requires that the user is a member of the organization.\n/// </summary>\npublic class MemberRequirement : IOrganizationRequirement\n{\n    public Task<bool> AuthorizeAsync(\n        CurrentContextOrganization? organizationClaims,\n        Func<Task<bool>> isProviderUserForOrg)\n        => Task.FromResult(organizationClaims is not null);\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs",
    "content": "﻿namespace Bit.Api.AdminConsole.Authorization.Requirements;\n\npublic class AccessEventLogsRequirement() : BasePermissionRequirement(p => p.AccessEventLogs);\npublic class AccessImportExportRequirement() : BasePermissionRequirement(p => p.AccessImportExport);\npublic class AccessReportsRequirement() : BasePermissionRequirement(p => p.AccessReports);\npublic class ManageAccountRecoveryRequirement() : BasePermissionRequirement(p => p.ManageResetPassword);\npublic class ManageGroupsRequirement() : BasePermissionRequirement(p => p.ManageGroups);\npublic class ManagePoliciesRequirement() : BasePermissionRequirement(p => p.ManagePolicies);\npublic class ManageScimRequirement() : BasePermissionRequirement(p => p.ManageScim);\npublic class ManageSsoRequirement() : BasePermissionRequirement(p => p.ManageSso);\npublic class ManageUsersRequirement() : BasePermissionRequirement(p => p.ManageUsers);\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/BaseAdminConsoleController.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2;\nusing Bit.Core.AdminConsole.Utilities.v2.Results;\nusing Bit.Core.Models.Api;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\npublic abstract class BaseAdminConsoleController : Controller\n{\n    protected static IResult Handle(CommandResult commandResult) =>\n        commandResult.Match<IResult>(\n            error => error switch\n            {\n                BadRequestError badRequest => TypedResults.BadRequest(new ErrorResponseModel(badRequest.Message)),\n                NotFoundError notFound => TypedResults.NotFound(new ErrorResponseModel(notFound.Message)),\n                InternalError internalError => TypedResults.Json(\n                    new ErrorResponseModel(internalError.Message),\n                    statusCode: StatusCodes.Status500InternalServerError),\n                _ => TypedResults.Json(\n                    new ErrorResponseModel(error.Message),\n                    statusCode: StatusCodes.Status500InternalServerError\n                )\n            },\n            _ => TypedResults.NoContent()\n        );\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/GroupsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.AdminConsole.Models.Response;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"organizations/{orgId}/groups\")]\n[Authorize(\"Application\")]\npublic class GroupsController : Controller\n{\n    private readonly IGroupRepository _groupRepository;\n    private readonly IGroupService _groupService;\n    private readonly IDeleteGroupCommand _deleteGroupCommand;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly ICreateGroupCommand _createGroupCommand;\n    private readonly IUpdateGroupCommand _updateGroupCommand;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IUserService _userService;\n    private readonly IFeatureService _featureService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ICollectionRepository _collectionRepository;\n\n    public GroupsController(\n        IGroupRepository groupRepository,\n        IGroupService groupService,\n        IOrganizationRepository organizationRepository,\n        ICurrentContext currentContext,\n        ICreateGroupCommand createGroupCommand,\n        IUpdateGroupCommand updateGroupCommand,\n        IDeleteGroupCommand deleteGroupCommand,\n        IAuthorizationService authorizationService,\n        IApplicationCacheService applicationCacheService,\n        IUserService userService,\n        IFeatureService featureService,\n        IOrganizationUserRepository organizationUserRepository,\n        ICollectionRepository collectionRepository)\n    {\n        _groupRepository = groupRepository;\n        _groupService = groupService;\n        _organizationRepository = organizationRepository;\n        _currentContext = currentContext;\n        _createGroupCommand = createGroupCommand;\n        _updateGroupCommand = updateGroupCommand;\n        _deleteGroupCommand = deleteGroupCommand;\n        _authorizationService = authorizationService;\n        _applicationCacheService = applicationCacheService;\n        _userService = userService;\n        _featureService = featureService;\n        _organizationUserRepository = organizationUserRepository;\n        _collectionRepository = collectionRepository;\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<GroupResponseModel> Get(string orgId, string id)\n    {\n        var group = await _groupRepository.GetByIdAsync(new Guid(id));\n        if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        return new GroupResponseModel(group);\n    }\n\n    [HttpGet(\"{id}/details\")]\n    public async Task<GroupDetailsResponseModel> GetDetails(string orgId, string id)\n    {\n        var groupDetails = await _groupRepository.GetByIdWithCollectionsAsync(new Guid(id));\n        if (groupDetails?.Item1 == null || !await _currentContext.ManageGroups(groupDetails.Item1.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        return new GroupDetailsResponseModel(groupDetails.Item1, groupDetails.Item2);\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<GroupResponseModel>> GetOrganizationGroups(Guid orgId)\n    {\n        var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);\n        if (!authResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);\n        var responses = groups.Select(g => new GroupResponseModel(g));\n        return new ListResponseModel<GroupResponseModel>(responses);\n    }\n\n    [HttpGet(\"details\")]\n    public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroupDetails(Guid orgId)\n    {\n        var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails);\n\n        if (!authResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId);\n        var responses = groups.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));\n        return new ListResponseModel<GroupDetailsResponseModel>(responses);\n    }\n\n    [HttpGet(\"{id}/users\")]\n    public async Task<IEnumerable<Guid>> GetUsers(string orgId, string id)\n    {\n        var idGuid = new Guid(id);\n        var group = await _groupRepository.GetByIdAsync(idGuid);\n        if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var groupIds = await _groupRepository.GetManyUserIdsByIdAsync(idGuid);\n        return groupIds;\n    }\n\n    [HttpPost(\"\")]\n    public async Task<GroupResponseModel> Post(Guid orgId, [FromBody] GroupRequestModel model)\n    {\n        if (!await _currentContext.ManageGroups(orgId))\n        {\n            throw new NotFoundException();\n        }\n\n        // Check the user has permission to grant access to the collections for the new group\n        if (model.Collections?.Any() == true)\n        {\n            var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));\n            var authorized =\n                (await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyGroupAccess))\n                .Succeeded;\n            if (!authorized)\n            {\n                throw new NotFoundException();\n            }\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgId);\n        var group = model.ToGroup(orgId);\n        await _createGroupCommand.CreateGroupAsync(group, organization, model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users);\n\n        return new GroupResponseModel(group);\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task<GroupResponseModel> Put(Guid orgId, Guid id, [FromBody] GroupRequestModel model)\n    {\n        if (!await _currentContext.ManageGroups(orgId))\n        {\n            throw new NotFoundException();\n        }\n\n        var (group, currentAccess) = await _groupRepository.GetByIdWithCollectionsAsync(id);\n        if (group == null || group.OrganizationId != orgId)\n        {\n            throw new NotFoundException();\n        }\n\n        // Authorization check:\n        // If admins are not allowed access to all collections, you cannot add yourself to a group.\n        // No error is thrown for this, we just don't update groups.\n        var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);\n        if (!orgAbility.AllowAdminAccessToAllCollectionItems)\n        {\n            var userId = _userService.GetProperUserId(User).Value;\n            var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, userId);\n            var currentGroupUsers = await _groupRepository.GetManyUserIdsByIdAsync(id);\n            // OrganizationUser may be null if the current user is a provider\n            if (organizationUser != null && !currentGroupUsers.Contains(organizationUser.Id) && model.Users.Contains(organizationUser.Id))\n            {\n                throw new BadRequestException(\"You cannot add yourself to groups.\");\n            }\n        }\n\n        // Authorization check:\n        // You must have authorization to ModifyUserAccess for all collections being saved\n        var postedCollections = await _collectionRepository\n            .GetManyByManyIdsAsync(model.Collections.Select(c => c.Id));\n        foreach (var collection in postedCollections)\n        {\n            if (!(await _authorizationService.AuthorizeAsync(User, collection,\n                    BulkCollectionOperations.ModifyGroupAccess))\n                .Succeeded)\n            {\n                throw new NotFoundException();\n            }\n        }\n\n        // The client only sends collections that the saving user has permissions to edit.\n        // We need to combine these with collections that the user doesn't have permissions for, so that we don't\n        // accidentally overwrite those\n        var currentCollections = await _collectionRepository\n            .GetManyByManyIdsAsync(currentAccess.Select(cas => cas.Id));\n\n        var readonlyCollectionIds = new HashSet<Guid>();\n        foreach (var collection in currentCollections)\n        {\n            if (!(await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyGroupAccess))\n                .Succeeded)\n            {\n                readonlyCollectionIds.Add(collection.Id);\n            }\n        }\n\n        var editedCollectionAccess = model.Collections\n            .Select(c => c.ToSelectionReadOnly());\n        var readonlyCollectionAccess = currentAccess\n            .Where(ca => readonlyCollectionIds.Contains(ca.Id));\n        var collectionsToSave = editedCollectionAccess\n            .Concat(readonlyCollectionAccess)\n            .ToList();\n\n        var organization = await _organizationRepository.GetByIdAsync(orgId);\n\n        await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization, collectionsToSave, model.Users);\n        return new GroupResponseModel(group);\n    }\n\n    [HttpPost(\"{id}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    public async Task<GroupResponseModel> PostPut(Guid orgId, Guid id, [FromBody] GroupRequestModel model)\n    {\n        return await Put(orgId, id, model);\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task Delete(string orgId, string id)\n    {\n        var group = await _groupRepository.GetByIdAsync(new Guid(id));\n        if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        await _deleteGroupCommand.DeleteAsync(group);\n    }\n\n    [HttpPost(\"{id}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDelete(string orgId, string id)\n    {\n        await Delete(orgId, id);\n    }\n\n    [HttpDelete(\"\")]\n    public async Task BulkDelete([FromBody] GroupBulkRequestModel model)\n    {\n        var groups = await _groupRepository.GetManyByManyIds(model.Ids);\n\n        foreach (var group in groups)\n        {\n            if (!await _currentContext.ManageGroups(group.OrganizationId))\n            {\n                throw new NotFoundException();\n            }\n        }\n\n        await _deleteGroupCommand.DeleteManyAsync(groups);\n    }\n\n    [HttpPost(\"delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostBulkDelete([FromBody] GroupBulkRequestModel model)\n    {\n        await BulkDelete(model);\n    }\n\n    [HttpDelete(\"{id}/user/{orgUserId}\")]\n    public async Task DeleteUser(string orgId, string id, string orgUserId)\n    {\n        var group = await _groupRepository.GetByIdAsync(new Guid(id));\n        if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        await _groupService.DeleteUserAsync(group, new Guid(orgUserId));\n    }\n\n    [HttpPost(\"{id}/delete-user/{orgUserId}\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDeleteUser(string orgId, string id, string orgUserId)\n    {\n        await DeleteUser(orgId, id, orgUserId);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/OrganizationAuthRequestsController.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.AdminConsole.Models.Response;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.OrganizationAuth.Interfaces;\nusing Bit.Core.Auth.Models.Api.Request.AuthRequest;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"organizations/{orgId}/auth-requests\")]\n[Authorize(\"Application\")]\npublic class OrganizationAuthRequestsController : Controller\n{\n    private readonly IAuthRequestRepository _authRequestRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly IAuthRequestService _authRequestService;\n    private readonly IUpdateOrganizationAuthRequestCommand _updateOrganizationAuthRequestCommand;\n\n    public OrganizationAuthRequestsController(IAuthRequestRepository authRequestRepository,\n        ICurrentContext currentContext, IAuthRequestService authRequestService,\n        IUpdateOrganizationAuthRequestCommand updateOrganizationAuthRequestCommand)\n    {\n        _authRequestRepository = authRequestRepository;\n        _currentContext = currentContext;\n        _authRequestService = authRequestService;\n        _updateOrganizationAuthRequestCommand = updateOrganizationAuthRequestCommand;\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<PendingOrganizationAuthRequestResponseModel>> GetPendingRequests(Guid orgId)\n    {\n        await ValidateAdminRequest(orgId);\n\n        var authRequests = await _authRequestRepository.GetManyPendingByOrganizationIdAsync(orgId);\n        var responses = authRequests\n            .Select(a => new PendingOrganizationAuthRequestResponseModel(a))\n            .ToList();\n        return new ListResponseModel<PendingOrganizationAuthRequestResponseModel>(responses);\n    }\n\n    [HttpPost(\"{requestId}\")]\n    public async Task UpdateAuthRequest(Guid orgId, Guid requestId, [FromBody] AdminAuthRequestUpdateRequestModel model)\n    {\n        await ValidateAdminRequest(orgId);\n\n        var authRequest =\n            (await _authRequestRepository.GetManyAdminApprovalRequestsByManyIdsAsync(orgId, new[] { requestId })).FirstOrDefault();\n\n        if (authRequest == null || authRequest.OrganizationId != orgId)\n        {\n            throw new NotFoundException();\n        }\n\n        await _updateOrganizationAuthRequestCommand.UpdateAsync(authRequest.Id, authRequest.UserId, model.RequestApproved, model.EncryptedUserKey);\n    }\n\n    [HttpPost(\"deny\")]\n    public async Task BulkDenyRequests(Guid orgId, [FromBody] BulkDenyAdminAuthRequestRequestModel model)\n    {\n        await ValidateAdminRequest(orgId);\n\n        var authRequests = await _authRequestRepository.GetManyAdminApprovalRequestsByManyIdsAsync(orgId, model.Ids);\n\n        foreach (var authRequest in authRequests)\n        {\n            await _authRequestService.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId,\n                new AuthRequestUpdateRequestModel { RequestApproved = false, });\n        }\n    }\n\n    [HttpPost(\"\")]\n    public async Task UpdateManyAuthRequests(Guid orgId, [FromBody] IEnumerable<OrganizationAuthRequestUpdateManyRequestModel> model)\n    {\n        await ValidateAdminRequest(orgId);\n        await _updateOrganizationAuthRequestCommand.UpdateAsync(orgId, model.Select(x => x.ToOrganizationAuthRequestUpdate()));\n    }\n\n    [NonAction]\n    public async Task ValidateAdminRequest(Guid orgId)\n    {\n        if (!await _currentContext.ManageResetPassword(orgId))\n        {\n            throw new UnauthorizedAccessException();\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Authorize(\"Application\")]\n[Route(\"organizations/connections\")]\npublic class OrganizationConnectionsController : Controller\n{\n    private readonly ICreateOrganizationConnectionCommand _createOrganizationConnectionCommand;\n    private readonly IUpdateOrganizationConnectionCommand _updateOrganizationConnectionCommand;\n    private readonly IDeleteOrganizationConnectionCommand _deleteOrganizationConnectionCommand;\n    private readonly IOrganizationConnectionRepository _organizationConnectionRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly ILicensingService _licensingService;\n\n    public OrganizationConnectionsController(\n        ICreateOrganizationConnectionCommand createOrganizationConnectionCommand,\n        IUpdateOrganizationConnectionCommand updateOrganizationConnectionCommand,\n        IDeleteOrganizationConnectionCommand deleteOrganizationConnectionCommand,\n        IOrganizationConnectionRepository organizationConnectionRepository,\n        ICurrentContext currentContext,\n        IGlobalSettings globalSettings,\n        ILicensingService licensingService)\n    {\n        _createOrganizationConnectionCommand = createOrganizationConnectionCommand;\n        _updateOrganizationConnectionCommand = updateOrganizationConnectionCommand;\n        _deleteOrganizationConnectionCommand = deleteOrganizationConnectionCommand;\n        _organizationConnectionRepository = organizationConnectionRepository;\n        _currentContext = currentContext;\n        _globalSettings = globalSettings;\n        _licensingService = licensingService;\n    }\n\n    [HttpGet(\"enabled\")]\n    public bool ConnectionsEnabled()\n    {\n        return _globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication;\n    }\n\n    [HttpPost]\n    public async Task<OrganizationConnectionResponseModel> CreateConnection([FromBody] OrganizationConnectionRequestModel model)\n    {\n        if (!await HasPermissionAsync(model?.OrganizationId))\n        {\n            throw new BadRequestException($\"You do not have permission to create a connection of type {model.Type}.\");\n        }\n\n        if (await HasConnectionTypeAsync(model, null, model.Type))\n        {\n            throw new BadRequestException($\"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.\");\n        }\n\n        switch (model.Type)\n        {\n            case OrganizationConnectionType.CloudBillingSync:\n                return await CreateOrUpdateOrganizationConnectionAsync<BillingSyncConfig>(null, model, ValidateBillingSyncConfig);\n            case OrganizationConnectionType.Scim:\n                return await CreateOrUpdateOrganizationConnectionAsync<ScimConfig>(null, model);\n            default:\n                throw new BadRequestException($\"Unknown Organization connection Type: {model.Type}\");\n        }\n    }\n\n    [HttpPut(\"{organizationConnectionId}\")]\n    public async Task<OrganizationConnectionResponseModel> UpdateConnection(Guid organizationConnectionId, [FromBody] OrganizationConnectionRequestModel model)\n    {\n        if (model == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var existingOrganizationConnection = await _organizationConnectionRepository.GetByIdOrganizationIdAsync(organizationConnectionId, model.OrganizationId);\n        if (existingOrganizationConnection == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!await HasPermissionAsync(model?.OrganizationId, model?.Type))\n        {\n            throw new BadRequestException(\"You do not have permission to update this connection.\");\n        }\n\n        if (await HasConnectionTypeAsync(model, organizationConnectionId, model.Type))\n        {\n            throw new BadRequestException($\"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.\");\n        }\n\n        switch (model.Type)\n        {\n            case OrganizationConnectionType.CloudBillingSync:\n                return await CreateOrUpdateOrganizationConnectionAsync<BillingSyncConfig>(organizationConnectionId, model, ValidateBillingSyncConfig);\n            case OrganizationConnectionType.Scim:\n                return await CreateOrUpdateOrganizationConnectionAsync<ScimConfig>(organizationConnectionId, model);\n            default:\n                throw new BadRequestException($\"Unknown Organization connection Type: {model.Type}\");\n        }\n    }\n\n    [HttpGet(\"{organizationId}/{type}\")]\n    public async Task<OrganizationConnectionResponseModel> GetConnection(Guid organizationId, OrganizationConnectionType type)\n    {\n        if (!await HasPermissionAsync(organizationId, type))\n        {\n            throw new BadRequestException($\"You do not have permission to retrieve a connection of type {type}.\");\n        }\n\n        var connections = await GetConnectionsAsync(organizationId, type);\n        var connection = connections.FirstOrDefault(c => c.Type == type);\n\n        switch (type)\n        {\n            case OrganizationConnectionType.CloudBillingSync:\n                if (!_globalSettings.SelfHosted)\n                {\n                    throw new BadRequestException($\"Cannot get a {type} connection outside of a self-hosted instance.\");\n                }\n                return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));\n            case OrganizationConnectionType.Scim:\n                return new OrganizationConnectionResponseModel(connection, typeof(ScimConfig));\n            default:\n                throw new BadRequestException($\"Unknown Organization connection Type: {type}\");\n        }\n    }\n\n    [HttpDelete(\"{organizationConnectionId}\")]\n    public async Task DeleteConnection(Guid organizationConnectionId)\n    {\n        var connection = await _organizationConnectionRepository.GetByIdAsync(organizationConnectionId);\n\n        if (connection == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!await HasPermissionAsync(connection.OrganizationId, connection.Type))\n        {\n            throw new BadRequestException($\"You do not have permission to remove this connection of type {connection.Type}.\");\n        }\n\n        await _deleteOrganizationConnectionCommand.DeleteAsync(connection);\n    }\n\n    [HttpPost(\"{organizationConnectionId}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDeleteConnection(Guid organizationConnectionId)\n    {\n        await DeleteConnection(organizationConnectionId);\n    }\n\n    private async Task<ICollection<OrganizationConnection>> GetConnectionsAsync(Guid organizationId, OrganizationConnectionType type) =>\n        await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organizationId, type);\n\n    private async Task<bool> HasConnectionTypeAsync(OrganizationConnectionRequestModel model, Guid? connectionId,\n        OrganizationConnectionType type)\n    {\n        var existingConnections = await GetConnectionsAsync(model.OrganizationId, type);\n\n        return existingConnections.Any(c => c.Type == model.Type && (!connectionId.HasValue || c.Id != connectionId.Value));\n    }\n\n    private async Task<bool> HasPermissionAsync(Guid? organizationId, OrganizationConnectionType? type = null)\n    {\n        if (!organizationId.HasValue)\n        {\n            return false;\n        }\n        return type switch\n        {\n            OrganizationConnectionType.Scim => await _currentContext.ManageScim(organizationId.Value),\n            _ => await _currentContext.OrganizationOwner(organizationId.Value),\n        };\n    }\n\n    private async Task ValidateBillingSyncConfig(OrganizationConnectionRequestModel<BillingSyncConfig> typedModel)\n    {\n        if (!_globalSettings.SelfHosted)\n        {\n            throw new BadRequestException($\"Cannot create a {typedModel.Type} connection outside of a self-hosted instance.\");\n        }\n        var license = await _licensingService.ReadOrganizationLicenseAsync(typedModel.OrganizationId);\n        if (!_licensingService.VerifyLicense(license))\n        {\n            throw new BadRequestException(\"Cannot verify license file.\");\n        }\n        typedModel.ParsedConfig.CloudOrganizationId = license.Id;\n    }\n\n    private async Task<OrganizationConnectionResponseModel> CreateOrUpdateOrganizationConnectionAsync<T>(\n        Guid? organizationConnectionId,\n        OrganizationConnectionRequestModel model,\n        Func<OrganizationConnectionRequestModel<T>, Task> validateAction = null)\n        where T : IConnectionConfig\n    {\n        var typedModel = new OrganizationConnectionRequestModel<T>(model);\n        if (validateAction != null)\n        {\n            await validateAction(typedModel);\n        }\n\n        var data = typedModel.ToData(organizationConnectionId);\n        var connection = organizationConnectionId.HasValue\n            ? await _updateOrganizationConnectionCommand.UpdateAsync(data)\n            : await _createOrganizationConnectionCommand.CreateAsync(data);\n\n        return new OrganizationConnectionResponseModel(connection, typeof(T));\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/OrganizationDomainController.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"organizations\")]\n[Authorize(\"Application\")]\npublic class OrganizationDomainController : Controller\n{\n    private readonly ICreateOrganizationDomainCommand _createOrganizationDomainCommand;\n    private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand;\n    private readonly IDeleteOrganizationDomainCommand _deleteOrganizationDomainCommand;\n    private readonly IGetOrganizationDomainByIdOrganizationIdQuery _getOrganizationDomainByIdAndOrganizationIdQuery;\n    private readonly IGetOrganizationDomainByOrganizationIdQuery _getOrganizationDomainByOrganizationIdQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationDomainRepository _organizationDomainRepository;\n\n    public OrganizationDomainController(\n        ICreateOrganizationDomainCommand createOrganizationDomainCommand,\n        IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,\n        IDeleteOrganizationDomainCommand deleteOrganizationDomainCommand,\n        IGetOrganizationDomainByIdOrganizationIdQuery getOrganizationDomainByIdAndOrganizationIdQuery,\n        IGetOrganizationDomainByOrganizationIdQuery getOrganizationDomainByOrganizationIdQuery,\n        ICurrentContext currentContext,\n        IOrganizationRepository organizationRepository,\n        IOrganizationDomainRepository organizationDomainRepository)\n    {\n        _createOrganizationDomainCommand = createOrganizationDomainCommand;\n        _verifyOrganizationDomainCommand = verifyOrganizationDomainCommand;\n        _deleteOrganizationDomainCommand = deleteOrganizationDomainCommand;\n        _getOrganizationDomainByIdAndOrganizationIdQuery = getOrganizationDomainByIdAndOrganizationIdQuery;\n        _getOrganizationDomainByOrganizationIdQuery = getOrganizationDomainByOrganizationIdQuery;\n        _currentContext = currentContext;\n        _organizationRepository = organizationRepository;\n        _organizationDomainRepository = organizationDomainRepository;\n    }\n\n    [HttpGet(\"{orgId}/domain\")]\n    public async Task<ListResponseModel<OrganizationDomainResponseModel>> GetAll(Guid orgId)\n    {\n        await ValidateOrganizationAccessAsync(orgId);\n\n        var domains = await _getOrganizationDomainByOrganizationIdQuery\n            .GetDomainsByOrganizationIdAsync(orgId);\n        var response = domains.Select(x => new OrganizationDomainResponseModel(x)).ToList();\n        return new ListResponseModel<OrganizationDomainResponseModel>(response);\n    }\n\n    [HttpGet(\"{orgId}/domain/{id}\")]\n    public async Task<OrganizationDomainResponseModel> Get(Guid orgId, Guid id)\n    {\n        await ValidateOrganizationAccessAsync(orgId);\n\n        var organizationDomain = await _getOrganizationDomainByIdAndOrganizationIdQuery\n            .GetOrganizationDomainByIdOrganizationIdAsync(id, orgId);\n        if (organizationDomain is null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new OrganizationDomainResponseModel(organizationDomain);\n    }\n\n    [HttpPost(\"{orgId}/domain\")]\n    public async Task<OrganizationDomainResponseModel> Post(Guid orgId,\n        [FromBody] OrganizationDomainRequestModel model)\n    {\n        await ValidateOrganizationAccessAsync(orgId);\n\n        var organizationDomain = new OrganizationDomain\n        {\n            OrganizationId = orgId,\n            DomainName = model.DomainName.ToLower()\n        };\n\n        organizationDomain = await _createOrganizationDomainCommand.CreateAsync(organizationDomain);\n\n        return new OrganizationDomainResponseModel(organizationDomain);\n    }\n\n    [HttpPost(\"{orgId}/domain/{id}/verify\")]\n    public async Task<OrganizationDomainResponseModel> Verify(Guid orgId, Guid id)\n    {\n        await ValidateOrganizationAccessAsync(orgId);\n\n        var organizationDomain = await _organizationDomainRepository.GetDomainByIdOrganizationIdAsync(id, orgId);\n        if (organizationDomain is null)\n        {\n            throw new NotFoundException();\n        }\n\n        organizationDomain = await _verifyOrganizationDomainCommand.UserVerifyOrganizationDomainAsync(organizationDomain);\n\n        return new OrganizationDomainResponseModel(organizationDomain);\n    }\n\n    [HttpDelete(\"{orgId}/domain/{id}\")]\n    public async Task RemoveDomain(Guid orgId, Guid id)\n    {\n        await ValidateOrganizationAccessAsync(orgId);\n\n        var domain = await _organizationDomainRepository.GetDomainByIdOrganizationIdAsync(id, orgId);\n        if (domain is null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _deleteOrganizationDomainCommand.DeleteAsync(domain);\n    }\n\n    [HttpPost(\"{orgId}/domain/{id}/remove\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostRemoveDomain(Guid orgId, Guid id)\n    {\n        await RemoveDomain(orgId, id);\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"domain/sso/details\")] // must be post to accept email cleanly\n    public async Task<OrganizationDomainSsoDetailsResponseModel> GetOrgDomainSsoDetails(\n        [FromBody] OrganizationDomainSsoDetailsRequestModel model)\n    {\n        var ssoResult = await _organizationDomainRepository.GetOrganizationDomainSsoDetailsAsync(model.Email);\n        if (ssoResult is null)\n        {\n            throw new NotFoundException(\"Claimed org domain not found\");\n        }\n\n        return new OrganizationDomainSsoDetailsResponseModel(ssoResult);\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"domain/sso/verified\")]\n    public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(\n        [FromBody] OrganizationDomainSsoDetailsRequestModel model)\n    {\n        var ssoResults = (await _organizationDomainRepository\n            .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email))\n            .ToList();\n\n        return new VerifiedOrganizationDomainSsoDetailsResponseModel(\n            ssoResults.Select(ssoResult => new VerifiedOrganizationDomainSsoDetailResponseModel(ssoResult)));\n    }\n\n    private async Task ValidateOrganizationAccessAsync(Guid orgIdGuid)\n    {\n        if (!await _currentContext.ManageSso(orgIdGuid))\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/OrganizationUsersController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n// NOTE: This file is partially migrated to nullable reference types. Remove inline #nullable directives when addressing the FIXME.\n#nullable disable\n\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities.v2;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing V1_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;\nusing V2_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"organizations/{orgId}/users\")]\n[Authorize(\"Application\")]\npublic class OrganizationUsersController : BaseAdminConsoleController\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationService _organizationService;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IUserService _userService;\n    private readonly IPolicyQuery _policyQuery;\n    private readonly ICurrentContext _currentContext;\n    private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;\n    private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;\n    private readonly IUpdateOrganizationUserCommand _updateOrganizationUserCommand;\n    private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;\n    private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;\n    private readonly IDeleteClaimedOrganizationUserAccountCommand _deleteClaimedOrganizationUserAccountCommand;\n    private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IFeatureService _featureService;\n    private readonly IPricingClient _pricingClient;\n    private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;\n    private readonly IBulkResendOrganizationInvitesCommand _bulkResendOrganizationInvitesCommand;\n    private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;\n    private readonly V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand _revokeOrganizationUserCommandVNext;\n    private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;\n    private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;\n    private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;\n    private readonly V1_RevokeOrganizationUserCommand _revokeOrganizationUserCommand;\n    private readonly IAdminRecoverAccountCommand _adminRecoverAccountCommand;\n    private readonly ISelfRevokeOrganizationUserCommand _selfRevokeOrganizationUserCommand;\n\n    public OrganizationUsersController(IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IOrganizationService organizationService,\n        ICollectionRepository collectionRepository,\n        IGroupRepository groupRepository,\n        IUserService userService,\n        IPolicyQuery policyQuery,\n        ICurrentContext currentContext,\n        ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,\n        IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,\n        IUpdateOrganizationUserCommand updateOrganizationUserCommand,\n        IAcceptOrgUserCommand acceptOrgUserCommand,\n        IAuthorizationService authorizationService,\n        IApplicationCacheService applicationCacheService,\n        ISsoConfigRepository ssoConfigRepository,\n        IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,\n        IRemoveOrganizationUserCommand removeOrganizationUserCommand,\n        IDeleteClaimedOrganizationUserAccountCommand deleteClaimedOrganizationUserAccountCommand,\n        IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IFeatureService featureService,\n        IPricingClient pricingClient,\n        IConfirmOrganizationUserCommand confirmOrganizationUserCommand,\n        IRestoreOrganizationUserCommand restoreOrganizationUserCommand,\n        IInitPendingOrganizationCommand initPendingOrganizationCommand,\n        V1_RevokeOrganizationUserCommand revokeOrganizationUserCommand,\n        IResendOrganizationInviteCommand resendOrganizationInviteCommand,\n        IBulkResendOrganizationInvitesCommand bulkResendOrganizationInvitesCommand,\n        IAdminRecoverAccountCommand adminRecoverAccountCommand,\n        IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand,\n        V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand revokeOrganizationUserCommandVNext,\n        ISelfRevokeOrganizationUserCommand selfRevokeOrganizationUserCommand)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationService = organizationService;\n        _collectionRepository = collectionRepository;\n        _groupRepository = groupRepository;\n        _userService = userService;\n        _policyQuery = policyQuery;\n        _currentContext = currentContext;\n        _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;\n        _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;\n        _updateOrganizationUserCommand = updateOrganizationUserCommand;\n        _acceptOrgUserCommand = acceptOrgUserCommand;\n        _authorizationService = authorizationService;\n        _applicationCacheService = applicationCacheService;\n        _ssoConfigRepository = ssoConfigRepository;\n        _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;\n        _removeOrganizationUserCommand = removeOrganizationUserCommand;\n        _deleteClaimedOrganizationUserAccountCommand = deleteClaimedOrganizationUserAccountCommand;\n        _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery;\n        _policyRequirementQuery = policyRequirementQuery;\n        _featureService = featureService;\n        _pricingClient = pricingClient;\n        _resendOrganizationInviteCommand = resendOrganizationInviteCommand;\n        _bulkResendOrganizationInvitesCommand = bulkResendOrganizationInvitesCommand;\n        _automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;\n        _revokeOrganizationUserCommandVNext = revokeOrganizationUserCommandVNext;\n        _confirmOrganizationUserCommand = confirmOrganizationUserCommand;\n        _restoreOrganizationUserCommand = restoreOrganizationUserCommand;\n        _initPendingOrganizationCommand = initPendingOrganizationCommand;\n        _revokeOrganizationUserCommand = revokeOrganizationUserCommand;\n        _adminRecoverAccountCommand = adminRecoverAccountCommand;\n        _selfRevokeOrganizationUserCommand = selfRevokeOrganizationUserCommand;\n    }\n\n    [HttpGet(\"{id}\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<OrganizationUserDetailsResponseModel> Get(Guid orgId, Guid id, bool includeGroups = false)\n    {\n        var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(id);\n        if (organizationUser == null || organizationUser.OrganizationId != orgId)\n        {\n            throw new NotFoundException();\n        }\n\n        var claimedByOrganizationStatus = await GetClaimedByOrganizationStatusAsync(\n            organizationUser.OrganizationId,\n            [organizationUser.Id]);\n\n        var response = new OrganizationUserDetailsResponseModel(organizationUser, claimedByOrganizationStatus[organizationUser.Id], collections);\n\n        if (includeGroups)\n        {\n            response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);\n        }\n\n        return response;\n    }\n\n    /// <summary>\n    /// Returns a set of basic information about all members of the organization. This is available to all members of\n    /// the organization to manage collections. For this reason, it contains as little information as possible and no\n    /// cryptographic keys or other sensitive data.\n    /// </summary>\n    /// <param name=\"orgId\">Organization identifier</param>\n    /// <returns>List of users for the organization.</returns>\n    [HttpGet(\"mini-details\")]\n    [Authorize<MemberOrProviderRequirement>]\n    public async Task<ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>> GetMiniDetails(Guid orgId)\n    {\n        var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId);\n        return new ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>(\n            organizationUserUserDetails.Select(ou => new OrganizationUserUserMiniDetailsResponseModel(ou)));\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> GetAll(Guid orgId, bool includeGroups = false, bool includeCollections = false)\n    {\n        var request = new OrganizationUserUserDetailsQueryRequest\n        {\n            OrganizationId = orgId,\n            IncludeGroups = includeGroups,\n            IncludeCollections = includeCollections\n        };\n\n        if ((await _authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())).Succeeded)\n        {\n            return GetResultListResponseModel(await _organizationUserUserDetailsQuery.Get(request));\n        }\n\n        if ((await _authorizationService.AuthorizeAsync(User, new ManageAccountRecoveryRequirement())).Succeeded)\n        {\n            return GetResultListResponseModel(await _organizationUserUserDetailsQuery.GetAccountRecoveryEnrolledUsers(request));\n        }\n\n        throw new NotFoundException();\n    }\n\n    private ListResponseModel<OrganizationUserUserDetailsResponseModel> GetResultListResponseModel(IEnumerable<(OrganizationUserUserDetails OrgUser,\n                bool TwoFactorEnabled, bool ClaimedByOrganization)> results)\n    {\n        return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(results\n            .Select(result => new OrganizationUserUserDetailsResponseModel(result))\n            .ToList());\n    }\n\n    [HttpGet(\"{id}/reset-password-details\")]\n    [Authorize<ManageAccountRecoveryRequirement>]\n    public async Task<OrganizationUserResetPasswordDetailsResponseModel> GetResetPasswordDetails(Guid orgId, Guid id)\n    {\n        var organizationUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (organizationUser is null || organizationUser.OrganizationId != orgId || organizationUser.UserId is null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Retrieve data necessary for response (KDF, KDF Iterations, ResetPasswordKey)\n        // TODO Reset Password - Revisit this and create SPROC to reduce DB calls\n        var user = await _userService.GetUserByIdAsync(organizationUser.UserId.Value);\n        if (user == null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Retrieve Encrypted Private Key from organization\n        var org = await _organizationRepository.GetByIdAsync(orgId);\n        if (org == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new OrganizationUserResetPasswordDetailsResponseModel(new OrganizationUserResetPasswordDetails(organizationUser, user, org));\n    }\n\n    [HttpPost(\"account-recovery-details\")]\n    [Authorize<ManageAccountRecoveryRequirement>]\n    public async Task<ListResponseModel<OrganizationUserResetPasswordDetailsResponseModel>> GetAccountRecoveryDetails(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        var responses = await _organizationUserRepository.GetManyAccountRecoveryDetailsByOrganizationUserAsync(orgId, model.Ids);\n        return new ListResponseModel<OrganizationUserResetPasswordDetailsResponseModel>(responses.Select(r => new OrganizationUserResetPasswordDetailsResponseModel(r)));\n    }\n\n    [HttpPost(\"invite\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task Invite(Guid orgId, [FromBody] OrganizationUserInviteRequestModel model)\n    {\n        // Check the user has permission to grant access to the collections for the new user\n        if (model.Collections?.Any() == true)\n        {\n            var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));\n            var authorized =\n                (await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyUserAccess))\n                .Succeeded;\n            if (!authorized)\n            {\n                throw new NotFoundException();\n            }\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        await _organizationService.InviteUsersAsync(orgId, userId.Value, systemUser: null,\n            [(new OrganizationUserInvite(model.ToData()), null)]);\n    }\n\n    [HttpPost(\"reinvite\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkReinvite(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User);\n\n        IEnumerable<Tuple<Core.Entities.OrganizationUser, string>> result;\n        result = await _bulkResendOrganizationInvitesCommand.BulkResendInvitesAsync(orgId, userId.Value, model.Ids);\n\n        return new ListResponseModel<OrganizationUserBulkResponseModel>(\n            result.Select(t => new OrganizationUserBulkResponseModel(t.Item1.Id, t.Item2)));\n    }\n\n    [HttpPost(\"{id}/reinvite\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task Reinvite(Guid orgId, Guid id)\n    {\n        var userId = _userService.GetProperUserId(User);\n        await _resendOrganizationInviteCommand.ResendInviteAsync(orgId, userId.Value, id);\n    }\n\n    [HttpPost(\"{organizationUserId}/accept-init\")]\n    public async Task<IResult> AcceptInit(Guid orgId, Guid organizationUserId, [FromBody] OrganizationUserAcceptInitRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.RefactorOrgAcceptInit))\n        {\n            var request = new InitPendingOrganizationRequest\n            {\n                User = user,\n                OrganizationId = orgId,\n                OrganizationUserId = organizationUserId,\n                OrganizationKeys = model.Keys.ToPublicKeyEncryptionKeyPairData(),\n                CollectionName = model.CollectionName,\n                EmailToken = model.Token,\n                EncryptedOrganizationSymmetricKey = model.Key\n            };\n\n            var result = await _initPendingOrganizationCommand.InitPendingOrganizationVNextAsync(request);\n\n            return Handle(result);\n        }\n\n        await _initPendingOrganizationCommand.InitPendingOrganizationAsync(user, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName, model.Token);\n        await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);\n        await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);\n\n        return TypedResults.Ok();\n    }\n\n    [HttpPost(\"{organizationUserId}/accept\")]\n    public async Task Accept(Guid orgId, Guid organizationUserId, [FromBody] OrganizationUserAcceptRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);\n        if (organizationUser == null || organizationUser.OrganizationId != orgId)\n        {\n            throw new NotFoundException(\"Organization user mismatch\");\n        }\n\n        var useMasterPasswordPolicy = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)\n        ? (await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id)).AutoEnrollEnabled(orgId)\n        : await ShouldHandleResetPasswordAsync(orgId);\n\n        if (useMasterPasswordPolicy && !OrganizationUser.IsValidResetPasswordKey(model.ResetPasswordKey))\n        {\n            throw new BadRequestException(\"Master Password reset is required, but not provided.\");\n        }\n\n        await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);\n\n        if (useMasterPasswordPolicy)\n        {\n            await _organizationService.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);\n        }\n    }\n\n    private async Task<bool> ShouldHandleResetPasswordAsync(Guid orgId)\n    {\n        var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);\n\n        if (organizationAbility is not { UsePolicies: true })\n        {\n            return false;\n        }\n\n        var masterPasswordPolicy = await _policyQuery.RunAsync(orgId, PolicyType.ResetPassword);\n        var useMasterPasswordPolicy = masterPasswordPolicy.Enabled &&\n                                      masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;\n\n        return useMasterPasswordPolicy;\n    }\n\n    [HttpPost(\"{id}/confirm\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task Confirm(Guid orgId, Guid id, [FromBody] OrganizationUserConfirmRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User);\n        _ = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, id, model.Key, userId.Value, model.DefaultUserCollectionName);\n    }\n\n    [HttpPost(\"confirm\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkConfirm(Guid orgId,\n        [FromBody] OrganizationUserBulkConfirmRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User);\n        var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgId, model.ToDictionary(), userId.Value, model.DefaultUserCollectionName);\n\n        return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>\n            new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));\n    }\n\n    [HttpPost(\"public-keys\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserPublicKeyResponseModel>> UserPublicKeys(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        var result = await _organizationUserRepository.GetManyPublicKeysByOrganizationUserAsync(orgId, model.Ids);\n        var responses = result.Select(r => new OrganizationUserPublicKeyResponseModel(r.Id, r.UserId, r.PublicKey)).ToList();\n        return new ListResponseModel<OrganizationUserPublicKeyResponseModel>(responses);\n    }\n\n    [HttpPut(\"{id}\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)\n    {\n        var (organizationUser, currentAccess) = await _organizationUserRepository.GetByIdWithCollectionsAsync(id);\n        if (organizationUser == null || organizationUser.OrganizationId != orgId)\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var editingSelf = userId == organizationUser.UserId;\n\n        // Authorization check:\n        // If admins are not allowed access to all collections, you cannot add yourself to a group.\n        // No error is thrown for this, we just don't update groups.\n        var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);\n        var groupsToSave = editingSelf && !organizationAbility.AllowAdminAccessToAllCollectionItems\n            ? null\n            : model.Groups;\n\n        // Authorization check:\n        // If admins are not allowed access to all collections, you cannot add yourself to collections.\n        // This is not caught by the requirement below that you can ModifyUserAccess and must be checked separately\n        var currentAccessIds = currentAccess.Select(c => c.Id).ToHashSet();\n        if (editingSelf &&\n            !organizationAbility.AllowAdminAccessToAllCollectionItems &&\n            model.Collections.Any(c => !currentAccessIds.Contains(c.Id)))\n        {\n            throw new BadRequestException(\"You cannot add yourself to a collection.\");\n        }\n\n        // Authorization check:\n        // You must have authorization to ModifyUserAccess for all collections being saved\n        var postedCollections = await _collectionRepository\n            .GetManyByManyIdsAsync(model.Collections.Select(c => c.Id));\n        foreach (var collection in postedCollections)\n        {\n            if (!(await _authorizationService.AuthorizeAsync(User, collection,\n                    BulkCollectionOperations.ModifyUserAccess))\n                .Succeeded)\n            {\n                throw new NotFoundException();\n            }\n        }\n\n        // The client only sends collections that the saving user has permissions to edit.\n        // We need to combine these with collections that the user doesn't have permissions for, so that we don't\n        // accidentally overwrite those\n        var currentCollections = await _collectionRepository\n            .GetManyByManyIdsAsync(currentAccess.Select(cas => cas.Id));\n\n        var readonlyCollectionIds = new HashSet<Guid>();\n        foreach (var collection in currentCollections)\n        {\n            if (!(await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyUserAccess))\n                .Succeeded)\n            {\n                readonlyCollectionIds.Add(collection.Id);\n            }\n        }\n\n        var editedCollectionAccess = model.Collections\n            .Select(c => c.ToSelectionReadOnly());\n        var readonlyCollectionAccess = currentAccess\n            .Where(ca => readonlyCollectionIds.Contains(ca.Id));\n        var collectionsToSave = editedCollectionAccess\n            .Concat(readonlyCollectionAccess)\n            .ToList();\n\n        var existingUserType = organizationUser.Type;\n\n        await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), existingUserType, userId,\n            collectionsToSave, groupsToSave);\n    }\n\n    [HttpPost(\"{id}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task PostPut(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)\n    {\n        await Put(orgId, id, model);\n    }\n\n    [HttpPut(\"{userId}/reset-password-enrollment\")]\n    public async Task PutResetPasswordEnrollment(Guid orgId, Guid userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);\n        var isTdeEnrollment = ssoConfig != null && ssoConfig.Enabled && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption;\n        if (!isTdeEnrollment && OrganizationUser.IsValidResetPasswordKey(model.ResetPasswordKey) && !await _userService.VerifySecretAsync(user, model.MasterPasswordHash))\n        {\n            throw new BadRequestException(\"Incorrect password\");\n        }\n\n        var callingUserId = user.Id;\n        await _organizationService.UpdateUserResetPasswordEnrollmentAsync(\n            orgId, userId, model.ResetPasswordKey, callingUserId);\n\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, user.Id);\n        if (orgUser.Status == OrganizationUserStatusType.Invited)\n        {\n            await _acceptOrgUserCommand.AcceptOrgUserByOrgIdAsync(orgId, user, _userService);\n        }\n    }\n\n#nullable enable\n    [HttpPut(\"{id}/reset-password\")]\n    [Authorize<ManageAccountRecoveryRequirement>]\n    public async Task<IResult> PutResetPassword(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model)\n    {\n        var targetOrganizationUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (targetOrganizationUser == null || targetOrganizationUser.OrganizationId != orgId)\n        {\n            return TypedResults.NotFound();\n        }\n\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, targetOrganizationUser, new RecoverAccountAuthorizationRequirement());\n        if (!authorizationResult.Succeeded)\n        {\n            // Return an informative error to show in the UI.\n            // The Authorize attribute already prevents enumeration by users outside the organization, so this can be specific.\n            var failureReason = authorizationResult.Failure?.FailureReasons.FirstOrDefault()?.Message ?? RecoverAccountAuthorizationHandler.FailureReason;\n            // This should be a 403 Forbidden, but that causes a logout on our client apps so we're using 400 Bad Request instead\n            return TypedResults.BadRequest(new ErrorResponseModel(failureReason));\n        }\n\n        var result = await _adminRecoverAccountCommand.RecoverAccountAsync(orgId, targetOrganizationUser, model.NewMasterPasswordHash, model.Key);\n        if (result.Succeeded)\n        {\n            return TypedResults.Ok();\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        await Task.Delay(2000);\n        return TypedResults.BadRequest(ModelState);\n    }\n#nullable disable\n\n    [HttpDelete(\"{id}\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task Remove(Guid orgId, Guid id)\n    {\n        var userId = _userService.GetProperUserId(User);\n        await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value);\n    }\n\n    [HttpPost(\"{id}/remove\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task PostRemove(Guid orgId, Guid id)\n    {\n        await Remove(orgId, id);\n    }\n\n    [HttpDelete(\"\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User);\n        var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value);\n        return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>\n            new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));\n    }\n\n    [HttpPost(\"remove\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> PostBulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        return await BulkRemove(orgId, model);\n    }\n\n    [HttpDelete(\"{id}/delete-account\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<IResult> DeleteAccount(Guid orgId, Guid id)\n    {\n        var currentUserId = _userService.GetProperUserId(User);\n        if (currentUserId == null)\n        {\n            return TypedResults.Unauthorized();\n        }\n\n        var commandResult = await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUserId.Value);\n\n        return commandResult.Result.Match<IResult>(\n            error => error is NotFoundError\n                ? TypedResults.NotFound(new ErrorResponseModel(error.Message))\n                : TypedResults.BadRequest(new ErrorResponseModel(error.Message)),\n            TypedResults.Ok\n        );\n    }\n\n    [HttpPost(\"{id}/delete-account\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task PostDeleteAccount(Guid orgId, Guid id)\n    {\n        await DeleteAccount(orgId, id);\n    }\n\n    [HttpDelete(\"delete-account\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        var currentUserId = _userService.GetProperUserId(User);\n        if (currentUserId == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await _deleteClaimedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUserId.Value);\n\n        var responses = result.Select(r => r.Result.Match(\n            error => new OrganizationUserBulkResponseModel(r.Id, error.Message),\n            _ => new OrganizationUserBulkResponseModel(r.Id, string.Empty)\n        ));\n\n        return new ListResponseModel<OrganizationUserBulkResponseModel>(responses);\n    }\n\n    [HttpPost(\"delete-account\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> PostBulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        return await BulkDeleteAccount(orgId, model);\n    }\n\n    [HttpPut(\"{id}/revoke\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task RevokeAsync(Guid orgId, Guid id)\n    {\n        await RestoreOrRevokeUserAsync(orgId, id, _revokeOrganizationUserCommand.RevokeUserAsync);\n    }\n\n    [HttpPut(\"revoke-self\")]\n    [Authorize<MemberRequirement>]\n    public async Task<IResult> RevokeSelfAsync(Guid orgId)\n    {\n        var userId = _userService.GetProperUserId(User);\n        if (!userId.HasValue)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await _selfRevokeOrganizationUserCommand.SelfRevokeUserAsync(orgId, userId.Value);\n        return Handle(result);\n    }\n\n    [HttpPatch(\"{id}/revoke\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task PatchRevokeAsync(Guid orgId, Guid id)\n    {\n        await RevokeAsync(orgId, id);\n    }\n\n    [HttpPut(\"revoke\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        var currentUserId = _userService.GetProperUserId(User);\n        if (currentUserId == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var results = await _revokeOrganizationUserCommandVNext.RevokeUsersAsync(\n            new V2_RevokeOrganizationUserCommand.RevokeOrganizationUsersRequest(\n                orgId,\n                model.Ids.ToArray(),\n                new StandardUser(currentUserId.Value, await _currentContext.OrganizationOwner(orgId))));\n\n        return new ListResponseModel<OrganizationUserBulkResponseModel>(results\n            .Select(result => new OrganizationUserBulkResponseModel(result.Id,\n                result.Result.Match(\n                    error => error.Message,\n                    _ => string.Empty\n                ))));\n    }\n\n    [HttpPatch(\"revoke\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> PatchBulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        return await BulkRevokeAsync(orgId, model);\n    }\n\n    [HttpPut(\"{id}/restore\")]\n    [Authorize<ManageUsersRequirement>]\n    [Obsolete(\"This endpoint is deprecated. Use _vNext endpoint instead. This will be removed in a future release.\")]\n    public async Task RestoreAsync(Guid orgId, Guid id)\n    {\n        await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId, null));\n    }\n\n\n    [HttpPut(\"{id}/restore/vnext\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task RestoreAsync_vNext(Guid orgId, Guid id, [FromBody] OrganizationUserRestoreRequest request)\n    {\n        await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId, request.DefaultUserCollectionName));\n    }\n\n    [HttpPatch(\"{id}/restore\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task PatchRestoreAsync(Guid orgId, Guid id)\n    {\n        await RestoreAsync(orgId, id);\n    }\n\n    [HttpPut(\"restore\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        return await RestoreOrRevokeUsersAsync(orgId, model,\n            (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds,\n                restoringUserId, _userService, model.DefaultUserCollectionName));\n    }\n\n    [HttpPatch(\"restore\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> PatchBulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        return await BulkRestoreAsync(orgId, model);\n    }\n\n    [HttpPut(\"enable-secrets-manager\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task BulkEnableSecretsManagerAsync(Guid orgId,\n        [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        var orgUsers = (await _organizationUserRepository.GetManyAsync(model.Ids))\n            .Where(ou => ou.OrganizationId == orgId && !ou.AccessSecretsManager).ToList();\n        if (orgUsers.Count == 0)\n        {\n            throw new BadRequestException(\"Users invalid.\");\n        }\n\n        var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(orgId,\n            orgUsers.Count);\n        if (additionalSmSeatsRequired > 0)\n        {\n            var organization = await _organizationRepository.GetByIdAsync(orgId);\n            // TODO: https://bitwarden.atlassian.net/browse/PM-17000\n            var plan = await _pricingClient.GetPlanOrThrow(organization!.PlanType);\n            var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)\n                .AdjustSeats(additionalSmSeatsRequired);\n            await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);\n        }\n\n        foreach (var orgUser in orgUsers)\n        {\n            orgUser.AccessSecretsManager = true;\n        }\n\n        await _organizationUserRepository.ReplaceManyAsync(orgUsers);\n    }\n\n    [HttpPatch(\"enable-secrets-manager\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    [Authorize<ManageUsersRequirement>]\n    public async Task PatchBulkEnableSecretsManagerAsync(Guid orgId,\n        [FromBody] OrganizationUserBulkRequestModel model)\n    {\n        await BulkEnableSecretsManagerAsync(orgId, model);\n    }\n\n    [HttpPost(\"{id}/auto-confirm\")]\n    [Authorize<ManageUsersRequirement>]\n    [RequireFeature(FeatureFlagKeys.AutomaticConfirmUsers)]\n    public async Task<IResult> AutomaticallyConfirmOrganizationUserAsync([FromRoute] Guid orgId,\n        [FromRoute] Guid id,\n        [FromBody] OrganizationUserConfirmRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User);\n\n        if (userId is null || userId.Value == Guid.Empty)\n        {\n            return TypedResults.Unauthorized();\n        }\n\n        return Handle(await _automaticallyConfirmOrganizationUserCommand.AutomaticallyConfirmOrganizationUserAsync(\n            new AutomaticallyConfirmOrganizationUserRequest\n            {\n                OrganizationId = orgId,\n                OrganizationUserId = id,\n                Key = model.Key,\n                DefaultUserCollectionName = model.DefaultUserCollectionName,\n                PerformedBy = new StandardUser(userId.Value, await _currentContext.OrganizationOwner(orgId)),\n            }));\n    }\n\n    private async Task RestoreOrRevokeUserAsync(\n        Guid orgId,\n        Guid id,\n        Func<Core.Entities.OrganizationUser, Guid?, Task> statusAction)\n    {\n        var userId = _userService.GetProperUserId(User);\n        var orgUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (orgUser == null || orgUser.OrganizationId != orgId)\n        {\n            throw new NotFoundException();\n        }\n\n        await statusAction(orgUser, userId);\n    }\n\n    private async Task<ListResponseModel<OrganizationUserBulkResponseModel>> RestoreOrRevokeUsersAsync(\n        Guid orgId,\n        OrganizationUserBulkRequestModel model,\n        Func<Guid, IEnumerable<Guid>, Guid?, Task<List<Tuple<Core.Entities.OrganizationUser, string>>>> statusAction)\n    {\n        var userId = _userService.GetProperUserId(User);\n        var result = await statusAction(orgId, model.Ids, userId.Value);\n        return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>\n            new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));\n    }\n\n    private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)\n    {\n        var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);\n        return usersOrganizationClaimedStatus;\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/OrganizationsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Auth.Models.Request.Organizations;\nusing Bit.Api.Auth.Models.Response.Organizations;\nusing Bit.Api.Models.Request.Accounts;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Api.Models.Response;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Business.Tokenables;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"organizations\")]\n[Authorize(\"Application\")]\npublic class OrganizationsController : Controller\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IPolicyQuery _policyQuery;\n    private readonly IOrganizationService _organizationService;\n    private readonly IUserService _userService;\n    private readonly ICurrentContext _currentContext;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly ISsoConfigService _ssoConfigService;\n    private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery;\n    private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;\n    private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;\n    private readonly IFeatureService _featureService;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderBillingService _providerBillingService;\n    private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;\n    private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;\n    private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;\n    private readonly IOrganizationDeleteCommand _organizationDeleteCommand;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IPricingClient _pricingClient;\n    private readonly IOrganizationUpdateKeysCommand _organizationUpdateKeysCommand;\n    private readonly IOrganizationUpdateCommand _organizationUpdateCommand;\n\n    public OrganizationsController(\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IPolicyQuery policyQuery,\n        IOrganizationService organizationService,\n        IUserService userService,\n        ICurrentContext currentContext,\n        ISsoConfigRepository ssoConfigRepository,\n        ISsoConfigService ssoConfigService,\n        IGetOrganizationApiKeyQuery getOrganizationApiKeyQuery,\n        IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,\n        ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,\n        IOrganizationApiKeyRepository organizationApiKeyRepository,\n        IFeatureService featureService,\n        GlobalSettings globalSettings,\n        IProviderRepository providerRepository,\n        IProviderBillingService providerBillingService,\n        IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,\n        IRemoveOrganizationUserCommand removeOrganizationUserCommand,\n        ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,\n        IOrganizationDeleteCommand organizationDeleteCommand,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IPricingClient pricingClient,\n        IOrganizationUpdateKeysCommand organizationUpdateKeysCommand,\n        IOrganizationUpdateCommand organizationUpdateCommand)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _policyQuery = policyQuery;\n        _organizationService = organizationService;\n        _userService = userService;\n        _currentContext = currentContext;\n        _ssoConfigRepository = ssoConfigRepository;\n        _ssoConfigService = ssoConfigService;\n        _getOrganizationApiKeyQuery = getOrganizationApiKeyQuery;\n        _rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;\n        _createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;\n        _organizationApiKeyRepository = organizationApiKeyRepository;\n        _featureService = featureService;\n        _globalSettings = globalSettings;\n        _providerRepository = providerRepository;\n        _providerBillingService = providerBillingService;\n        _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;\n        _removeOrganizationUserCommand = removeOrganizationUserCommand;\n        _cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;\n        _organizationDeleteCommand = organizationDeleteCommand;\n        _policyRequirementQuery = policyRequirementQuery;\n        _pricingClient = pricingClient;\n        _organizationUpdateKeysCommand = organizationUpdateKeysCommand;\n        _organizationUpdateCommand = organizationUpdateCommand;\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<OrganizationResponseModel> Get(string id)\n    {\n        var orgIdGuid = new Guid(id);\n        if (!await _currentContext.OrganizationOwner(orgIdGuid))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var plan = await _pricingClient.GetPlan(organization.PlanType);\n        return new OrganizationResponseModel(organization, plan);\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<ProfileOrganizationResponseModel>> GetUser()\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var organizations = await _organizationUserRepository.GetManyDetailsByUserAsync(userId,\n            OrganizationUserStatusType.Confirmed);\n\n        var organizationsClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(userId);\n        var organizationIdsClaimingActiveUser = organizationsClaimingActiveUser.Select(o => o.Id);\n\n        var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsClaimingActiveUser));\n        return new ListResponseModel<ProfileOrganizationResponseModel>(responses);\n    }\n\n    [HttpGet(\"{identifier}/auto-enroll-status\")]\n    public async Task<OrganizationAutoEnrollStatusResponseModel> GetAutoEnrollStatus(string identifier)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var organization = await _organizationRepository.GetByIdentifierAsync(identifier);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);\n        if (organizationUser == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))\n        {\n            var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id);\n            return new OrganizationAutoEnrollStatusResponseModel(organization.Id, resetPasswordPolicyRequirement.AutoEnrollEnabled(organization.Id));\n        }\n\n        var resetPasswordPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.ResetPassword);\n        if (!resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null)\n        {\n            return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false);\n        }\n\n        var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);\n        return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);\n    }\n\n    [HttpPost(\"\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<OrganizationResponseModel> Post([FromBody] OrganizationCreateRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var organizationSignup = model.ToOrganizationSignup(user);\n        var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);\n        var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);\n        return new OrganizationResponseModel(result.Organization, plan);\n    }\n\n    [HttpPost(\"create-without-payment\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<OrganizationResponseModel> CreateWithoutPaymentAsync([FromBody] OrganizationNoPaymentCreateRequest model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var organizationSignup = model.ToOrganizationSignup(user);\n        var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);\n        var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);\n        return new OrganizationResponseModel(result.Organization, plan);\n    }\n\n    [HttpPut(\"{organizationId:guid}\")]\n    public async Task<IResult> Put(Guid organizationId, [FromBody] OrganizationUpdateRequestModel model)\n    {\n        // If billing email is being changed, require subscription editing permissions.\n        // Otherwise, organization owner permissions are sufficient.\n        var requiresBillingPermission = model.BillingEmail is not null;\n        var authorized = requiresBillingPermission\n            ? await _currentContext.EditSubscription(organizationId)\n            : await _currentContext.OrganizationOwner(organizationId);\n\n        if (!authorized)\n        {\n            return TypedResults.Unauthorized();\n        }\n\n        var commandRequest = model.ToCommandRequest(organizationId);\n        var updatedOrganization = await _organizationUpdateCommand.UpdateAsync(commandRequest);\n\n        var plan = await _pricingClient.GetPlan(updatedOrganization.PlanType);\n        return TypedResults.Ok(new OrganizationResponseModel(updatedOrganization, plan));\n    }\n\n    [HttpPost(\"{id}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    public async Task<IResult> PostPut(Guid id, [FromBody] OrganizationUpdateRequestModel model)\n    {\n        return await Put(id, model);\n    }\n\n    [HttpPost(\"{id}/storage\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<PaymentResponseModel> PostStorage(string id, [FromBody] StorageRequestModel model)\n    {\n        var orgIdGuid = new Guid(id);\n        if (!await _currentContext.EditSubscription(orgIdGuid))\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await _organizationService.AdjustStorageAsync(orgIdGuid, model.StorageGbAdjustment.Value);\n        return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };\n    }\n\n    [HttpPost(\"{id}/leave\")]\n    public async Task Leave(Guid id)\n    {\n        if (!await _currentContext.OrganizationUser(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id);\n        if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector && user.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"Your organization's Single Sign-On settings prevent you from leaving.\");\n        }\n\n        if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))\n        {\n            throw new BadRequestException(\"Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.\");\n        }\n\n        await _removeOrganizationUserCommand.UserLeaveAsync(id, user.Id);\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task Delete(string id, [FromBody] SecretVerificationRequestModel model)\n    {\n        var orgIdGuid = new Guid(id);\n        if (!await _currentContext.OrganizationOwner(orgIdGuid))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!await _userService.VerifySecretAsync(user, model.Secret))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(string.Empty, \"User verification failed.\");\n        }\n\n        if (organization.IsValidClient())\n        {\n            var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);\n\n            if (provider.IsBillable())\n            {\n                await _providerBillingService.ScaleSeats(\n                    provider,\n                    organization.PlanType,\n                    -organization.Seats ?? 0);\n            }\n        }\n\n        await _organizationDeleteCommand.DeleteAsync(organization);\n    }\n\n    [HttpPost(\"{id}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDelete(string id, [FromBody] SecretVerificationRequestModel model)\n    {\n        await Delete(id, model);\n    }\n\n    [HttpPost(\"{id}/delete-recover-token\")]\n    [AllowAnonymous]\n    public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyDeleteRecoverRequestModel model)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!_orgDeleteTokenDataFactory.TryUnprotect(model.Token, out var data) || !data.IsValid(organization))\n        {\n            throw new BadRequestException(\"Invalid token.\");\n        }\n\n        if (organization.IsValidClient())\n        {\n            var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);\n            if (provider.IsBillable())\n            {\n                await _providerBillingService.ScaleSeats(\n                    provider,\n                    organization.PlanType,\n                    -organization.Seats ?? 0);\n            }\n        }\n\n        await _organizationDeleteCommand.DeleteAsync(organization);\n    }\n\n    [HttpPost(\"{id}/api-key\")]\n    public async Task<ApiKeyResponseModel> ApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model)\n    {\n        var orgIdGuid = new Guid(id);\n        if (!await HasApiKeyAccessAsync(orgIdGuid, model.Type))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim)\n        {\n            // Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types\n            var productTier = organization.PlanType.GetProductTier();\n            if (productTier is not ProductTierType.Enterprise and not ProductTierType.Teams)\n            {\n                throw new NotFoundException();\n            }\n        }\n\n        var organizationApiKey = await _getOrganizationApiKeyQuery\n                                     .GetOrganizationApiKeyAsync(organization.Id, model.Type) ??\n                                 await _createOrganizationApiKeyCommand.CreateAsync(organization.Id, model.Type);\n\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (model.Type != OrganizationApiKeyType.Scim\n            && !await _userService.VerifySecretAsync(user, model.Secret))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"MasterPasswordHash\", \"Invalid password.\");\n        }\n        else\n        {\n            var response = new ApiKeyResponseModel(organizationApiKey);\n            return response;\n        }\n    }\n\n    [HttpGet(\"{id}/api-key-information/{type?}\")]\n    public async Task<ListResponseModel<OrganizationApiKeyInformation>> ApiKeyInformation(Guid id,\n        [FromRoute] OrganizationApiKeyType? type)\n    {\n        if (!await HasApiKeyAccessAsync(id, type))\n        {\n            throw new NotFoundException();\n        }\n\n        var apiKeys = await _organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(id, type);\n\n        return new ListResponseModel<OrganizationApiKeyInformation>(\n            apiKeys.Select(k => new OrganizationApiKeyInformation(k)));\n    }\n\n    [HttpPost(\"{id}/rotate-api-key\")]\n    public async Task<ApiKeyResponseModel> RotateApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model)\n    {\n        var orgIdGuid = new Guid(id);\n        if (!await HasApiKeyAccessAsync(orgIdGuid, model.Type))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var organizationApiKey = await _getOrganizationApiKeyQuery\n                                     .GetOrganizationApiKeyAsync(organization.Id, model.Type) ??\n                                 await _createOrganizationApiKeyCommand.CreateAsync(organization.Id, model.Type);\n\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (model.Type != OrganizationApiKeyType.Scim\n            && !await _userService.VerifySecretAsync(user, model.Secret))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"MasterPasswordHash\", \"Invalid password.\");\n        }\n        else\n        {\n            await _rotateOrganizationApiKeyCommand.RotateApiKeyAsync(organizationApiKey);\n            var response = new ApiKeyResponseModel(organizationApiKey);\n            return response;\n        }\n    }\n\n    private async Task<bool> HasApiKeyAccessAsync(Guid orgId, OrganizationApiKeyType? type)\n    {\n        return type switch\n        {\n            OrganizationApiKeyType.Scim => await _currentContext.ManageScim(orgId),\n            _ => await _currentContext.OrganizationOwner(orgId),\n        };\n    }\n\n    [HttpGet(\"{id}/public-key\")]\n    public async Task<OrganizationPublicKeyResponseModel> GetPublicKey(string id)\n    {\n        var org = await _organizationRepository.GetByIdAsync(new Guid(id));\n        if (org == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new OrganizationPublicKeyResponseModel(org);\n    }\n\n    [Obsolete(\"TDL-136 Renamed to public-key (2023.8), left for backwards compatibility with older clients.\")]\n    [HttpGet(\"{id}/keys\")]\n    public async Task<OrganizationPublicKeyResponseModel> GetKeys(string id)\n    {\n        return await GetPublicKey(id);\n    }\n\n    [HttpPost(\"{id}/keys\")]\n    public async Task<OrganizationKeysResponseModel> PostKeys(Guid id, [FromBody] OrganizationKeysRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var org = await _organizationUpdateKeysCommand.UpdateOrganizationKeysAsync(id, model.PublicKey,\n            model.EncryptedPrivateKey);\n        return new OrganizationKeysResponseModel(org);\n    }\n\n    [HttpGet(\"{id:guid}/sso\")]\n    public async Task<OrganizationSsoResponseModel> GetSso(Guid id)\n    {\n        if (!await _currentContext.ManageSso(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id);\n\n        return new OrganizationSsoResponseModel(organization, _globalSettings, ssoConfig);\n    }\n\n    [HttpPost(\"{id:guid}/sso\")]\n    public async Task<OrganizationSsoResponseModel> PostSso(Guid id, [FromBody] OrganizationSsoRequestModel model)\n    {\n        if (!await _currentContext.ManageSso(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id);\n        ssoConfig = ssoConfig == null ? model.ToSsoConfig(id) : model.ToSsoConfig(ssoConfig);\n        organization.Identifier = model.Identifier;\n\n        await _ssoConfigService.SaveAsync(ssoConfig, organization);\n        await _organizationService.UpdateAsync(organization);\n\n        return new OrganizationSsoResponseModel(organization, _globalSettings, ssoConfig);\n    }\n\n    [HttpPut(\"{id}/collection-management\")]\n    public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)\n    {\n        if (!await _currentContext.OrganizationOwner(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationService.UpdateCollectionManagementSettingsAsync(id, model.ToSettings());\n        var plan = await _pricingClient.GetPlan(organization.PlanType);\n        return new OrganizationResponseModel(organization, plan);\n    }\n\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/PoliciesController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.AdminConsole.Models.Response.Helpers;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"organizations/{orgId}/policies\")]\n[Authorize(\"Application\")]\npublic class PoliciesController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;\n    private readonly IPolicyRepository _policyRepository;\n    private readonly IUserService _userService;\n    private readonly ISavePolicyCommand _savePolicyCommand;\n    private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;\n    private readonly IPolicyQuery _policyQuery;\n\n    public PoliciesController(IPolicyRepository policyRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IUserService userService,\n        ICurrentContext currentContext,\n        IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,\n        IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,\n        IOrganizationRepository organizationRepository,\n        ISavePolicyCommand savePolicyCommand,\n        IVNextSavePolicyCommand vNextSavePolicyCommand,\n        IPolicyQuery policyQuery)\n    {\n        _policyRepository = policyRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _userService = userService;\n        _currentContext = currentContext;\n        _organizationRepository = organizationRepository;\n        _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;\n        _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;\n        _savePolicyCommand = savePolicyCommand;\n        _vNextSavePolicyCommand = vNextSavePolicyCommand;\n        _policyQuery = policyQuery;\n    }\n\n    [HttpGet(\"{type}\")]\n    public async Task<PolicyStatusResponseModel> Get(Guid orgId, PolicyType type)\n    {\n        if (!await _currentContext.ManagePolicies(orgId))\n        {\n            throw new NotFoundException();\n        }\n\n        var policy = await _policyQuery.RunAsync(orgId, type);\n        if (policy.Type is PolicyType.SingleOrg)\n        {\n            return await policy.GetSingleOrgPolicyStatusResponseAsync(_organizationHasVerifiedDomainsQuery);\n        }\n\n        return new PolicyStatusResponseModel(policy);\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<PolicyResponseModel>> GetAll(string orgId)\n    {\n        var orgIdGuid = new Guid(orgId);\n        if (!await _currentContext.ManagePolicies(orgIdGuid))\n        {\n            throw new NotFoundException();\n        }\n\n        var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgIdGuid);\n\n        return new ListResponseModel<PolicyResponseModel>(policies.Select(p => new PolicyResponseModel(p)));\n    }\n\n    [AllowAnonymous]\n    [HttpGet(\"token\")]\n    public async Task<ListResponseModel<PolicyResponseModel>> GetByToken(Guid orgId, [FromQuery] string email,\n        [FromQuery] string token, [FromQuery] Guid organizationUserId)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(orgId);\n\n        if (organization is not { UsePolicies: true })\n        {\n            throw new NotFoundException();\n        }\n\n        var tokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(\n            _orgUserInviteTokenDataFactory, token, organizationUserId, email);\n\n        if (!tokenValid)\n        {\n            throw new NotFoundException();\n        }\n\n        var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);\n        if (orgUser == null || orgUser.OrganizationId != orgId)\n        {\n            throw new NotFoundException();\n        }\n\n        var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgId);\n        var responses = policies.Where(p => p.Enabled).Select(p => new PolicyResponseModel(p));\n        return new ListResponseModel<PolicyResponseModel>(responses);\n    }\n\n    // TODO: PM-4097 - remove GetByInvitedUser once all clients are updated to use the GetMasterPasswordPolicy endpoint below\n    [Obsolete(\"Deprecated API\", false)]\n    [AllowAnonymous]\n    [HttpGet(\"invited-user\")]\n    public async Task<ListResponseModel<PolicyResponseModel>> GetByInvitedUser(Guid orgId, [FromQuery] Guid userId)\n    {\n        var user = await _userService.GetUserByIdAsync(userId);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n        var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(user.Id);\n        var orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId);\n        if (orgUser == null)\n        {\n            throw new NotFoundException();\n        }\n        if (orgUser.Status != OrganizationUserStatusType.Invited)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgId);\n        var responses = policies.Where(p => p.Enabled).Select(p => new PolicyResponseModel(p));\n        return new ListResponseModel<PolicyResponseModel>(responses);\n    }\n\n    [HttpGet(\"master-password\")]\n    public async Task<PolicyResponseModel> GetMasterPasswordPolicy(Guid orgId)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(orgId);\n\n        if (organization is not { UsePolicies: true })\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, userId);\n\n        if (orgUser == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.MasterPassword);\n\n        if (policy == null || !policy.Enabled)\n        {\n            throw new NotFoundException();\n        }\n\n        return new PolicyResponseModel(policy);\n    }\n\n    [HttpPut(\"{type}\")]\n    [Authorize<ManagePoliciesRequirement>]\n    public async Task<PolicyResponseModel> Put(Guid orgId, PolicyType type, [FromBody] PolicyRequestModel model)\n    {\n        return await PutVNext(orgId, type, new SavePolicyRequest { Policy = model });\n    }\n\n    [HttpPut(\"{type}/vnext\")]\n    [Authorize<ManagePoliciesRequirement>]\n    public async Task<PolicyResponseModel> PutVNext(Guid orgId, PolicyType type, [FromBody] SavePolicyRequest model)\n    {\n        var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, type, _currentContext);\n\n        var policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyRequest);\n\n        return new PolicyResponseModel(policy);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/ProviderClientsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Billing.Controllers;\nusing Bit.Api.Billing.Models.Requests;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"providers/{providerId:guid}/clients\")]\npublic class ProviderClientsController(\n    ICurrentContext currentContext,\n    ILogger<BaseProviderController> logger,\n    IOrganizationRepository organizationRepository,\n    IProviderBillingService providerBillingService,\n    IProviderOrganizationRepository providerOrganizationRepository,\n    IProviderRepository providerRepository,\n    IProviderService providerService,\n    IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)\n{\n    private readonly ICurrentContext _currentContext = currentContext;\n\n    [HttpPost]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IResult> CreateAsync(\n        [FromRoute] Guid providerId,\n        [FromBody] CreateClientOrganizationRequestBody requestBody)\n    {\n        var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);\n\n        if (provider == null)\n        {\n            return result;\n        }\n\n        var user = await UserService.GetUserByPrincipalAsync(User);\n\n        if (user == null)\n        {\n            return Error.Unauthorized();\n        }\n\n        var organizationSignup = new OrganizationSignup\n        {\n            Name = requestBody.Name,\n            Plan = requestBody.PlanType,\n            AdditionalSeats = requestBody.Seats,\n            Owner = user,\n            BillingEmail = provider.BillingEmail,\n            OwnerKey = requestBody.Key,\n            Keys = requestBody.KeyPair.ToPublicKeyEncryptionKeyPairData(),\n            CollectionName = requestBody.CollectionName,\n            IsFromProvider = true\n        };\n\n        var providerOrganization = await providerService.CreateOrganizationAsync(\n            providerId,\n            organizationSignup,\n            requestBody.OwnerEmail,\n            user);\n\n        var clientOrganization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);\n\n        await providerBillingService.ScaleSeats(\n            provider,\n            requestBody.PlanType,\n            requestBody.Seats);\n\n        await providerBillingService.CreateCustomerForClientOrganization(\n            provider,\n            clientOrganization);\n\n        clientOrganization.Status = OrganizationStatusType.Managed;\n\n        await organizationRepository.ReplaceAsync(clientOrganization);\n\n        return TypedResults.Ok();\n    }\n\n    [HttpPut(\"{providerOrganizationId:guid}\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IResult> UpdateAsync(\n        [FromRoute] Guid providerId,\n        [FromRoute] Guid providerOrganizationId,\n        [FromBody] UpdateClientOrganizationRequestBody requestBody)\n    {\n        var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);\n\n        if (provider == null)\n        {\n            return result;\n        }\n\n        var providerOrganization = await providerOrganizationRepository.GetByIdAsync(providerOrganizationId);\n\n        if (providerOrganization == null)\n        {\n            return Error.NotFound();\n        }\n\n        if (providerOrganization.ProviderId != provider.Id)\n        {\n            return Error.NotFound();\n        }\n\n        var clientOrganization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);\n\n        if (clientOrganization is not { Status: OrganizationStatusType.Managed })\n        {\n            return Error.ServerError();\n        }\n\n        var seatAdjustment = requestBody.AssignedSeats - (clientOrganization.Seats ?? 0);\n\n        var seatAdjustmentResultsInPurchase = await providerBillingService.SeatAdjustmentResultsInPurchase(\n            provider,\n            clientOrganization.PlanType,\n            seatAdjustment);\n\n        if (seatAdjustmentResultsInPurchase && !_currentContext.ProviderProviderAdmin(provider.Id))\n        {\n            return Error.Unauthorized(\"Service users cannot purchase additional seats.\");\n        }\n\n        await providerBillingService.ScaleSeats(provider, clientOrganization.PlanType, seatAdjustment);\n\n        clientOrganization.Name = requestBody.Name;\n        clientOrganization.Seats = requestBody.AssignedSeats;\n\n        await organizationRepository.ReplaceAsync(clientOrganization);\n\n        return TypedResults.Ok();\n    }\n\n    [HttpGet(\"addable\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IResult> GetAddableOrganizationsAsync([FromRoute] Guid providerId)\n    {\n        var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);\n\n        if (provider == null)\n        {\n            return result;\n        }\n\n        var userId = _currentContext.UserId;\n\n        if (!userId.HasValue)\n        {\n            return Error.Unauthorized();\n        }\n\n        var addable =\n            await providerBillingService.GetAddableOrganizations(provider, userId.Value);\n\n        return TypedResults.Ok(addable);\n    }\n\n    [HttpPost(\"existing\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IResult> AddExistingOrganizationAsync(\n        [FromRoute] Guid providerId,\n        [FromBody] AddExistingOrganizationRequestBody requestBody)\n    {\n        var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);\n\n        if (provider == null)\n        {\n            return result;\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(requestBody.OrganizationId);\n\n        if (organization == null)\n        {\n            return Error.BadRequest(\"The organization being added to the provider does not exist.\");\n        }\n\n        await providerBillingService.AddExistingOrganization(provider, organization, requestBody.Key);\n\n        return TypedResults.Ok();\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Request.Providers;\nusing Bit.Api.AdminConsole.Models.Response.Providers;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Providers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"providers/{providerId:guid}/organizations\")]\n[Authorize(\"Application\")]\npublic class ProviderOrganizationsController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderService _providerService;\n    private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;\n    private readonly IUserService _userService;\n\n    public ProviderOrganizationsController(\n        ICurrentContext currentContext,\n        IOrganizationRepository organizationRepository,\n        IProviderOrganizationRepository providerOrganizationRepository,\n        IProviderRepository providerRepository,\n        IProviderService providerService,\n        IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,\n        IUserService userService)\n    {\n        _currentContext = currentContext;\n        _organizationRepository = organizationRepository;\n        _providerOrganizationRepository = providerOrganizationRepository;\n        _providerRepository = providerRepository;\n        _providerService = providerService;\n        _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;\n        _userService = userService;\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<ProviderOrganizationOrganizationDetailsResponseModel>> Get(Guid providerId)\n    {\n        if (!_currentContext.AccessProviderOrganizations(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);\n        var responses = providerOrganizations.Select(o => new ProviderOrganizationOrganizationDetailsResponseModel(o));\n        return new ListResponseModel<ProviderOrganizationOrganizationDetailsResponseModel>(responses);\n    }\n\n    [HttpPost(\"add\")]\n    public async Task Add(Guid providerId, [FromBody] ProviderOrganizationAddRequestModel model)\n    {\n        if (!_currentContext.ManageProviderOrganizations(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        await _providerService.AddOrganization(providerId, model.OrganizationId, model.Key);\n    }\n\n    [HttpPost(\"\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<ProviderOrganizationResponseModel> Post(Guid providerId, [FromBody] ProviderOrganizationCreateRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!_currentContext.ManageProviderOrganizations(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var organizationSignup = model.OrganizationCreateRequest.ToOrganizationSignup(user);\n        organizationSignup.IsFromProvider = true;\n        var result = await _providerService.CreateOrganizationAsync(providerId, organizationSignup, model.ClientOwnerEmail, user);\n        return new ProviderOrganizationResponseModel(result);\n    }\n\n    [HttpDelete(\"{id:guid}\")]\n    public async Task Delete(Guid providerId, Guid id)\n    {\n        if (!_currentContext.ManageProviderOrganizations(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var provider = await _providerRepository.GetByIdAsync(providerId);\n\n        var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(id);\n\n        var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);\n\n        await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(\n            provider,\n            providerOrganization,\n            organization);\n    }\n\n    [HttpPost(\"{id:guid}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDelete(Guid providerId, Guid id)\n    {\n        await Delete(providerId, id);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/ProviderUsersController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Request.Providers;\nusing Bit.Api.AdminConsole.Models.Response.Providers;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Models.Business.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"providers/{providerId:guid}/users\")]\n[Authorize(\"Application\")]\npublic class ProviderUsersController : Controller\n{\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IProviderService _providerService;\n    private readonly IUserService _userService;\n    private readonly ICurrentContext _currentContext;\n\n    public ProviderUsersController(\n        IProviderUserRepository providerUserRepository,\n        IProviderService providerService,\n        IUserService userService,\n        ICurrentContext currentContext)\n    {\n        _providerUserRepository = providerUserRepository;\n        _providerService = providerService;\n        _userService = userService;\n        _currentContext = currentContext;\n    }\n\n    [HttpGet(\"{id:guid}\")]\n    public async Task<ProviderUserResponseModel> Get(Guid providerId, Guid id)\n    {\n        var providerUser = await _providerUserRepository.GetByIdAsync(id);\n        if (providerUser == null || !_currentContext.ProviderManageUsers(providerUser.ProviderId))\n        {\n            throw new NotFoundException();\n        }\n\n        return new ProviderUserResponseModel(providerUser);\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<ProviderUserUserDetailsResponseModel>> GetAll(Guid providerId)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var providerUsers = await _providerUserRepository.GetManyDetailsByProviderAsync(providerId);\n        var responses = providerUsers.Select(o => new ProviderUserUserDetailsResponseModel(o));\n        return new ListResponseModel<ProviderUserUserDetailsResponseModel>(responses);\n    }\n\n    [HttpPost(\"invite\")]\n    public async Task Invite(Guid providerId, [FromBody] ProviderUserInviteRequestModel model)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var invite = ProviderUserInviteFactory.CreateInitialInvite(model.Emails, model.Type.Value,\n            _userService.GetProperUserId(User).Value, providerId);\n        await _providerService.InviteUserAsync(invite);\n    }\n\n    [HttpPost(\"reinvite\")]\n    public async Task<ListResponseModel<ProviderUserBulkResponseModel>> BulkReinvite(Guid providerId, [FromBody] ProviderUserBulkRequestModel model)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var invite = ProviderUserInviteFactory.CreateReinvite(model.Ids, _userService.GetProperUserId(User).Value, providerId);\n        var result = await _providerService.ResendInvitesAsync(invite);\n        return new ListResponseModel<ProviderUserBulkResponseModel>(\n            result.Select(t => new ProviderUserBulkResponseModel(t.Item1.Id, t.Item2)));\n    }\n\n    [HttpPost(\"{id:guid}/reinvite\")]\n    public async Task Reinvite(Guid providerId, Guid id)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var invite = ProviderUserInviteFactory.CreateReinvite(new[] { id },\n            _userService.GetProperUserId(User).Value, providerId);\n        await _providerService.ResendInvitesAsync(invite);\n    }\n\n    [HttpPost(\"{id:guid}/accept\")]\n    public async Task Accept(Guid providerId, Guid id, [FromBody] ProviderUserAcceptRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        await _providerService.AcceptUserAsync(id, user, model.Token);\n    }\n\n    [HttpPost(\"{id:guid}/confirm\")]\n    public async Task Confirm(Guid providerId, Guid id, [FromBody] ProviderUserConfirmRequestModel model)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        await _providerService.ConfirmUsersAsync(providerId, new Dictionary<Guid, string> { [id] = model.Key }, userId.Value);\n    }\n\n    [HttpPost(\"confirm\")]\n    public async Task<ListResponseModel<ProviderUserBulkResponseModel>> BulkConfirm(Guid providerId,\n        [FromBody] ProviderUserBulkConfirmRequestModel model)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        var results = await _providerService.ConfirmUsersAsync(providerId, model.ToDictionary(), userId.Value);\n\n        return new ListResponseModel<ProviderUserBulkResponseModel>(results.Select(r =>\n            new ProviderUserBulkResponseModel(r.Item1.Id, r.Item2)));\n    }\n\n    [HttpPost(\"public-keys\")]\n    public async Task<ListResponseModel<ProviderUserPublicKeyResponseModel>> UserPublicKeys(Guid providerId, [FromBody] ProviderUserBulkRequestModel model)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await _providerUserRepository.GetManyPublicKeysByProviderUserAsync(providerId, model.Ids);\n        var responses = result.Select(r => new ProviderUserPublicKeyResponseModel(r.Id, r.UserId, r.PublicKey)).ToList();\n        return new ListResponseModel<ProviderUserPublicKeyResponseModel>(responses);\n    }\n\n    [HttpPut(\"{id:guid}\")]\n    public async Task Put(Guid providerId, Guid id, [FromBody] ProviderUserUpdateRequestModel model)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var providerUser = await _providerUserRepository.GetByIdAsync(id);\n        if (providerUser == null || providerUser.ProviderId != providerId)\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        await _providerService.SaveUserAsync(model.ToProviderUser(providerUser), userId.Value);\n    }\n\n    [HttpPost(\"{id:guid}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    public async Task PostPut(Guid providerId, Guid id, [FromBody] ProviderUserUpdateRequestModel model)\n    {\n        await Put(providerId, id, model);\n    }\n\n    [HttpDelete(\"{id:guid}\")]\n    public async Task Delete(Guid providerId, Guid id)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        await _providerService.DeleteUsersAsync(providerId, new[] { id }, userId.Value);\n    }\n\n    [HttpPost(\"{id:guid}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDelete(Guid providerId, Guid id)\n    {\n        await Delete(providerId, id);\n    }\n\n    [HttpDelete(\"\")]\n    public async Task<ListResponseModel<ProviderUserBulkResponseModel>> BulkDelete(Guid providerId, [FromBody] ProviderUserBulkRequestModel model)\n    {\n        if (!_currentContext.ProviderManageUsers(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        var result = await _providerService.DeleteUsersAsync(providerId, model.Ids, userId.Value);\n        return new ListResponseModel<ProviderUserBulkResponseModel>(result.Select(r =>\n            new ProviderUserBulkResponseModel(r.Item1.Id, r.Item2)));\n    }\n\n    [HttpPost(\"delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task<ListResponseModel<ProviderUserBulkResponseModel>> PostBulkDelete(Guid providerId, [FromBody] ProviderUserBulkRequestModel model)\n    {\n        return await BulkDelete(providerId, model);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Controllers/ProvidersController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Request.Providers;\nusing Bit.Api.AdminConsole.Models.Response.Providers;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Controllers;\n\n[Route(\"providers\")]\n[Authorize(\"Application\")]\npublic class ProvidersController : Controller\n{\n    private readonly IUserService _userService;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderService _providerService;\n    private readonly ICurrentContext _currentContext;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IProviderBillingService _providerBillingService;\n    private readonly ILogger<ProvidersController> _logger;\n\n    public ProvidersController(IUserService userService, IProviderRepository providerRepository,\n        IProviderService providerService, ICurrentContext currentContext, GlobalSettings globalSettings,\n        IProviderBillingService providerBillingService, ILogger<ProvidersController> logger)\n    {\n        _userService = userService;\n        _providerRepository = providerRepository;\n        _providerService = providerService;\n        _currentContext = currentContext;\n        _globalSettings = globalSettings;\n        _providerBillingService = providerBillingService;\n        _logger = logger;\n    }\n\n    [HttpGet(\"{id:guid}\")]\n    public async Task<ProviderResponseModel> Get(Guid id)\n    {\n        if (!_currentContext.ProviderUser(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var provider = await _providerRepository.GetByIdAsync(id);\n        if (provider == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new ProviderResponseModel(provider);\n    }\n\n    [HttpPut(\"{id:guid}\")]\n    public async Task<ProviderResponseModel> Put(Guid id, [FromBody] ProviderUpdateRequestModel model)\n    {\n        if (!_currentContext.ProviderProviderAdmin(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var provider = await _providerRepository.GetByIdAsync(id);\n        if (provider == null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Capture original values before modifications for Stripe sync\n        var originalName = provider.Name;\n        var originalBillingEmail = provider.BillingEmail;\n\n        await _providerService.UpdateAsync(model.ToProvider(provider, _globalSettings));\n\n        // Sync name/email changes to Stripe\n        if (originalName != provider.Name || originalBillingEmail != provider.BillingEmail)\n        {\n            try\n            {\n                await _providerBillingService.UpdateProviderNameAndEmail(provider);\n            }\n            catch (Exception ex)\n            {\n                _logger.LogError(ex,\n                    \"Failed to update Stripe customer for provider {ProviderId}. Database was updated successfully.\",\n                    provider.Id);\n            }\n        }\n\n        return new ProviderResponseModel(provider);\n    }\n\n    [HttpPost(\"{id:guid}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead\")]\n    public async Task<ProviderResponseModel> PostPut(Guid id, [FromBody] ProviderUpdateRequestModel model)\n    {\n        return await Put(id, model);\n    }\n\n    [HttpPost(\"{id:guid}/setup\")]\n    public async Task<ProviderResponseModel> Setup(Guid id, [FromBody] ProviderSetupRequestModel model)\n    {\n        if (!_currentContext.ProviderProviderAdmin(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var provider = await _providerRepository.GetByIdAsync(id);\n        if (provider == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n\n        var paymentMethod = model.PaymentMethod.ToDomain();\n        var billingAddress = model.BillingAddress.ToDomain();\n\n        var response =\n            await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key,\n                paymentMethod, billingAddress);\n\n        return new ProviderResponseModel(response);\n    }\n\n    [HttpPost(\"{id}/delete-recover-token\")]\n    [AllowAnonymous]\n    public async Task PostDeleteRecoverToken(Guid id, [FromBody] ProviderVerifyDeleteRecoverRequestModel model)\n    {\n        var provider = await _providerRepository.GetByIdAsync(id);\n        if (provider == null)\n        {\n            throw new NotFoundException();\n        }\n        await _providerService.DeleteAsync(provider, model.Token);\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task Delete(Guid id)\n    {\n        if (!_currentContext.ProviderProviderAdmin(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var provider = await _providerRepository.GetByIdAsync(id);\n        if (provider == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        await _providerService.DeleteAsync(provider);\n    }\n\n    [HttpPost(\"{id}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDelete(Guid id)\n    {\n        await Delete(id);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Jobs/OrganizationSubscriptionUpdateJob.cs",
    "content": "﻿using System.Collections.Immutable;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Jobs;\nusing Bit.Core.Services;\nusing Quartz;\n\nnamespace Bit.Api.AdminConsole.Jobs;\n\npublic class OrganizationSubscriptionUpdateJob(ILogger<OrganizationSubscriptionUpdateJob> logger,\n    IGetOrganizationSubscriptionsToUpdateQuery query,\n    IBulkUpdateOrganizationSubscriptionsCommand command,\n    IFeatureService featureService) : BaseJob(logger)\n{\n    protected override async Task ExecuteJobAsync(IJobExecutionContext _)\n    {\n        if (!featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization))\n        {\n            return;\n        }\n\n        logger.LogInformation(\"OrganizationSubscriptionUpdateJob - START\");\n\n        var organizationSubscriptionsToUpdate =\n            (await query.GetOrganizationSubscriptionsToUpdateAsync())\n            .ToImmutableList();\n\n        logger.LogInformation(\"OrganizationSubscriptionUpdateJob - {numberOfOrganizations} organization(s) to update\",\n            organizationSubscriptionsToUpdate.Count);\n\n        await command.BulkUpdateOrganizationSubscriptionsAsync(organizationSubscriptionsToUpdate);\n\n        logger.LogInformation(\"OrganizationSubscriptionUpdateJob - COMPLETED\");\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/AdminAuthRequestUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request;\n\npublic class AdminAuthRequestUpdateRequestModel\n{\n    [EncryptedString]\n    public string EncryptedUserKey { get; set; }\n\n    [Required]\n    public bool RequestApproved { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/BulkDenyAdminAuthRequestRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Api.AdminConsole.Models.Request;\n\npublic class BulkDenyAdminAuthRequestRequestModel\n{\n    public IEnumerable<Guid> Ids { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/GroupRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.Models.Request;\nusing Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Api.AdminConsole.Models.Request;\n\npublic class GroupRequestModel\n{\n    [Required]\n    [StringLength(100)]\n    public string Name { get; set; }\n    public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }\n    public IEnumerable<Guid> Users { get; set; }\n\n    public Group ToGroup(Guid orgId)\n    {\n        return ToGroup(new Group\n        {\n            OrganizationId = orgId\n        });\n    }\n\n    public Group ToGroup(Group existingGroup)\n    {\n        existingGroup.Name = Name;\n        return existingGroup;\n    }\n}\n\npublic class GroupBulkRequestModel\n{\n    [Required]\n    public IEnumerable<Guid> Ids { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/OrganizationAuthRequestUpdateManyRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.OrganizationAuth.Models;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request;\n\npublic class OrganizationAuthRequestUpdateManyRequestModel\n{\n    public Guid Id { get; set; }\n\n    [EncryptedString]\n    public string Key { get; set; }\n\n    public bool Approved { get; set; }\n\n    public OrganizationAuthRequestUpdate ToOrganizationAuthRequestUpdate()\n    {\n        return new OrganizationAuthRequestUpdate\n        {\n            Id = Id,\n            Key = Key,\n            Approved = Approved\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request;\n\npublic class OrganizationDomainRequestModel\n{\n    [Required]\n    [DomainNameValidator]\n    public string DomainName { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationApiKeyRequestModel.cs",
    "content": "﻿using Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationApiKeyRequestModel : SecretVerificationRequestModel\n{\n    public OrganizationApiKeyType Type { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationConnectionRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationConnections;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationConnectionRequestModel\n{\n    public OrganizationConnectionType Type { get; set; }\n    public Guid OrganizationId { get; set; }\n    public bool Enabled { get; set; }\n    public JsonDocument Config { get; set; }\n\n    public OrganizationConnectionRequestModel() { }\n}\n\n\npublic class OrganizationConnectionRequestModel<T> : OrganizationConnectionRequestModel where T : IConnectionConfig\n{\n    public T ParsedConfig { get; private set; }\n\n    public OrganizationConnectionRequestModel(OrganizationConnectionRequestModel model)\n    {\n        Type = model.Type;\n        OrganizationId = model.OrganizationId;\n        Enabled = model.Enabled;\n        Config = model.Config;\n\n        try\n        {\n            ParsedConfig = model.Config.Deserialize<T>(JsonHelpers.IgnoreCase);\n        }\n        catch (JsonException)\n        {\n            throw new BadRequestException(\"Organization Connection configuration malformed\");\n        }\n    }\n\n    public OrganizationConnectionData<T> ToData(Guid? id = null) =>\n        new()\n        {\n            Id = id,\n            Type = Type,\n            OrganizationId = OrganizationId,\n            Enabled = Enabled,\n            Config = ParsedConfig,\n        };\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationCreateRequestModel : IValidatableObject\n{\n    [Required]\n    [StringLength(50, ErrorMessage = \"The field Name exceeds the maximum length.\")]\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; }\n\n    [StringLength(50, ErrorMessage = \"The field Business Name exceeds the maximum length.\")]\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string BusinessName { get; set; }\n\n    [Required]\n    [StringLength(256)]\n    [EmailAddress]\n    public string BillingEmail { get; set; }\n\n    public PlanType PlanType { get; set; }\n\n    [Required]\n    public string Key { get; set; }\n\n    public OrganizationKeysRequestModel Keys { get; set; }\n    public PaymentMethodType? PaymentMethodType { get; set; }\n    public string PaymentToken { get; set; }\n\n    [Range(0, int.MaxValue)]\n    public int AdditionalSeats { get; set; }\n\n    [Range(0, 99)]\n    public short? AdditionalStorageGb { get; set; }\n\n    public bool PremiumAccessAddon { get; set; }\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string CollectionName { get; set; }\n\n    public string TaxIdNumber { get; set; }\n\n    public string BillingAddressLine1 { get; set; }\n\n    public string BillingAddressLine2 { get; set; }\n\n    public string BillingAddressCity { get; set; }\n\n    public string BillingAddressState { get; set; }\n\n    public string BillingAddressPostalCode { get; set; }\n\n    [StringLength(2)]\n    public string BillingAddressCountry { get; set; }\n\n    public int? MaxAutoscaleSeats { get; set; }\n\n    [Range(0, int.MaxValue)]\n    public int? AdditionalSmSeats { get; set; }\n\n    [Range(0, int.MaxValue)]\n    public int? AdditionalServiceAccounts { get; set; }\n\n    [Required]\n    public bool UseSecretsManager { get; set; }\n\n    public bool IsFromSecretsManagerTrial { get; set; }\n\n    public string InitiationPath { get; set; }\n\n    public bool SkipTrial { get; set; }\n\n    public string[] Coupons { get; set; }\n\n    public virtual OrganizationSignup ToOrganizationSignup(User user)\n    {\n        var orgSignup = new OrganizationSignup\n        {\n            Owner = user,\n            OwnerKey = Key,\n            Name = Name,\n            Plan = PlanType,\n            PaymentMethodType = PaymentMethodType,\n            PaymentToken = PaymentToken,\n            AdditionalSeats = AdditionalSeats,\n            MaxAutoscaleSeats = MaxAutoscaleSeats,\n            AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(0),\n            PremiumAccessAddon = PremiumAccessAddon,\n            BillingEmail = BillingEmail,\n            BusinessName = BusinessName,\n            CollectionName = CollectionName,\n            AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(),\n            AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(),\n            UseSecretsManager = UseSecretsManager,\n            IsFromSecretsManagerTrial = IsFromSecretsManagerTrial,\n            TaxInfo = new TaxInfo\n            {\n                TaxIdNumber = TaxIdNumber,\n                BillingAddressLine1 = BillingAddressLine1,\n                BillingAddressLine2 = BillingAddressLine2,\n                BillingAddressCity = BillingAddressCity,\n                BillingAddressState = BillingAddressState,\n                BillingAddressPostalCode = BillingAddressPostalCode,\n                BillingAddressCountry = BillingAddressCountry,\n            },\n            InitiationPath = InitiationPath,\n            SkipTrial = SkipTrial,\n            Coupons = Coupons,\n            Keys = Keys?.ToPublicKeyEncryptionKeyPairData()\n        };\n\n        return orgSignup;\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (PlanType != PlanType.Free && string.IsNullOrWhiteSpace(PaymentToken))\n        {\n            yield return new ValidationResult(\"Payment required.\", new string[] { nameof(PaymentToken) });\n        }\n\n        if (PlanType != PlanType.Free && !PaymentMethodType.HasValue)\n        {\n            yield return new ValidationResult(\"Payment method type required.\",\n                new string[] { nameof(PaymentMethodType) });\n        }\n\n        if (PlanType != PlanType.Free && string.IsNullOrWhiteSpace(BillingAddressCountry))\n        {\n            yield return new ValidationResult(\"Country required.\",\n                new string[] { nameof(BillingAddressCountry) });\n        }\n\n        if (PlanType != PlanType.Free && TaxHelpers.IsDirectTaxCountry(BillingAddressCountry) &&\n            string.IsNullOrWhiteSpace(BillingAddressPostalCode))\n        {\n            yield return new ValidationResult(\"Zip / postal code is required.\",\n                new string[] { nameof(BillingAddressPostalCode) });\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationDomainSsoDetailsRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationDomainSsoDetailsRequestModel\n{\n    [Required]\n    [EmailAddress]\n    public string Email { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationKeysRequestModel\n{\n    [Required]\n    public string PublicKey { get; set; }\n    [Required]\n    public string EncryptedPrivateKey { get; set; }\n\n    public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData()\n    {\n        return new PublicKeyEncryptionKeyPairData(\n            wrappedPrivateKey: EncryptedPrivateKey,\n            publicKey: PublicKey);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationNoPaymentCreateRequest\n{\n    [Required]\n    [StringLength(50, ErrorMessage = \"The field Name exceeds the maximum length.\")]\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; }\n\n    [StringLength(50, ErrorMessage = \"The field Business Name exceeds the maximum length.\")]\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string BusinessName { get; set; }\n\n    [Required]\n    [StringLength(256)]\n    [EmailAddress]\n    public string BillingEmail { get; set; }\n\n    public PlanType PlanType { get; set; }\n\n    [Required]\n    public string Key { get; set; }\n\n    public OrganizationKeysRequestModel Keys { get; set; }\n    public PaymentMethodType? PaymentMethodType { get; set; }\n    public string PaymentToken { get; set; }\n\n    [Range(0, int.MaxValue)]\n    public int AdditionalSeats { get; set; }\n\n    [Range(0, 99)]\n    public short? AdditionalStorageGb { get; set; }\n\n    public bool PremiumAccessAddon { get; set; }\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string CollectionName { get; set; }\n\n    public string TaxIdNumber { get; set; }\n\n    public string BillingAddressLine1 { get; set; }\n\n    public string BillingAddressLine2 { get; set; }\n\n    public string BillingAddressCity { get; set; }\n\n    public string BillingAddressState { get; set; }\n\n    public string BillingAddressPostalCode { get; set; }\n\n    [StringLength(2)]\n    public string BillingAddressCountry { get; set; }\n\n    public int? MaxAutoscaleSeats { get; set; }\n\n    [Range(0, int.MaxValue)]\n    public int? AdditionalSmSeats { get; set; }\n\n    [Range(0, int.MaxValue)]\n    public int? AdditionalServiceAccounts { get; set; }\n\n    [Required]\n    public bool UseSecretsManager { get; set; }\n\n    public bool IsFromSecretsManagerTrial { get; set; }\n\n    public string InitiationPath { get; set; }\n\n    public virtual OrganizationSignup ToOrganizationSignup(User user)\n    {\n        var orgSignup = new OrganizationSignup\n        {\n            Owner = user,\n            OwnerKey = Key,\n            Name = Name,\n            Plan = PlanType,\n            PaymentMethodType = PaymentMethodType,\n            PaymentToken = PaymentToken,\n            AdditionalSeats = AdditionalSeats,\n            MaxAutoscaleSeats = MaxAutoscaleSeats,\n            AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(0),\n            PremiumAccessAddon = PremiumAccessAddon,\n            BillingEmail = BillingEmail,\n            BusinessName = BusinessName,\n            CollectionName = CollectionName,\n            AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(),\n            AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(),\n            UseSecretsManager = UseSecretsManager,\n            IsFromSecretsManagerTrial = IsFromSecretsManagerTrial,\n            TaxInfo = new TaxInfo\n            {\n                TaxIdNumber = TaxIdNumber,\n                BillingAddressLine1 = BillingAddressLine1,\n                BillingAddressLine2 = BillingAddressLine2,\n                BillingAddressCity = BillingAddressCity,\n                BillingAddressState = BillingAddressState,\n                BillingAddressPostalCode = BillingAddressPostalCode,\n                BillingAddressCountry = BillingAddressCountry,\n            },\n            InitiationPath = InitiationPath,\n            Keys = Keys?.ToPublicKeyEncryptionKeyPairData()\n        };\n\n        return orgSignup;\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationSeatRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationSeatRequestModel : IValidatableObject\n{\n    [Required]\n    public int? SeatAdjustment { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (SeatAdjustment == 0)\n        {\n            yield return new ValidationResult(\"Seat adjustment cannot be 0.\", new string[] { nameof(SeatAdjustment) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationUpdateRequestModel\n{\n    [StringLength(50, ErrorMessage = \"The field Name exceeds the maximum length.\")]\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string? Name { get; set; }\n\n    [EmailAddress]\n    [StringLength(256)]\n    public string? BillingEmail { get; set; }\n\n    public OrganizationKeysRequestModel? Keys { get; set; }\n\n    public OrganizationUpdateRequest ToCommandRequest(Guid organizationId) => new()\n    {\n        OrganizationId = organizationId,\n        Name = Name,\n        BillingEmail = BillingEmail,\n        Keys = Keys?.ToPublicKeyEncryptionKeyPairData()\n    };\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationUpgradeRequestModel\n{\n    [StringLength(50)]\n    public string BusinessName { get; set; }\n    public PlanType PlanType { get; set; }\n    [Range(0, int.MaxValue)]\n    public int AdditionalSeats { get; set; }\n    [Range(0, 99)]\n    public short? AdditionalStorageGb { get; set; }\n    [Range(0, int.MaxValue)]\n    public int? AdditionalSmSeats { get; set; }\n    [Range(0, int.MaxValue)]\n    public int? AdditionalServiceAccounts { get; set; }\n    [Required]\n    public bool UseSecretsManager { get; set; }\n    public bool PremiumAccessAddon { get; set; }\n    public string BillingAddressCountry { get; set; }\n    public string BillingAddressPostalCode { get; set; }\n    public OrganizationKeysRequestModel Keys { get; set; }\n\n    public OrganizationUpgrade ToOrganizationUpgrade()\n    {\n        var orgUpgrade = new OrganizationUpgrade\n        {\n            AdditionalSeats = AdditionalSeats,\n            AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(),\n            AdditionalServiceAccounts = AdditionalServiceAccounts.GetValueOrDefault(0),\n            AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(0),\n            UseSecretsManager = UseSecretsManager,\n            BusinessName = BusinessName,\n            Plan = PlanType,\n            PremiumAccessAddon = PremiumAccessAddon,\n            TaxInfo = new TaxInfo()\n            {\n                BillingAddressCountry = BillingAddressCountry,\n                BillingAddressPostalCode = BillingAddressPostalCode\n            },\n            Keys = Keys?.ToPublicKeyEncryptionKeyPairData()\n        };\n\n        return orgUpgrade;\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.Models.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationUserInviteRequestModel\n{\n    [Required]\n    [StrictEmailAddressList]\n    public IEnumerable<string> Emails { get; set; }\n    [Required]\n    [EnumDataType(typeof(OrganizationUserType))]\n    public OrganizationUserType? Type { get; set; }\n    public bool AccessSecretsManager { get; set; }\n    public Permissions Permissions { get; set; }\n    public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }\n    public IEnumerable<Guid> Groups { get; set; }\n\n    public OrganizationUserInviteData ToData()\n    {\n        return new OrganizationUserInviteData\n        {\n            Emails = Emails,\n            Type = Type,\n            AccessSecretsManager = AccessSecretsManager,\n            Collections = Collections?.Select(c => c.ToSelectionReadOnly()),\n            Groups = Groups,\n            Permissions = Permissions,\n        };\n    }\n}\n\npublic class OrganizationUserAcceptInitRequestModel\n{\n    [Required]\n    public string Token { get; set; }\n    [Required]\n    public string Key { get; set; }\n    [Required]\n    public OrganizationKeysRequestModel Keys { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string CollectionName { get; set; }\n}\n\npublic class OrganizationUserAcceptRequestModel\n{\n    [Required]\n    public string Token { get; set; }\n    // Used to auto-enroll in master password reset\n    public string ResetPasswordKey { get; set; }\n}\n\npublic class OrganizationUserConfirmRequestModel\n{\n    [Required]\n    public string Key { get; set; }\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string DefaultUserCollectionName { get; set; }\n}\n\npublic class OrganizationUserBulkConfirmRequestModelEntry\n{\n    [Required]\n    public Guid Id { get; set; }\n    [Required]\n    public string Key { get; set; }\n}\n\npublic class OrganizationUserBulkConfirmRequestModel\n{\n    [Required]\n    public IEnumerable<OrganizationUserBulkConfirmRequestModelEntry> Keys { get; set; }\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string DefaultUserCollectionName { get; set; }\n\n    public Dictionary<Guid, string> ToDictionary()\n    {\n        return Keys.ToDictionary(e => e.Id, e => e.Key);\n    }\n}\n\npublic class OrganizationUserUpdateRequestModel\n{\n    [Required]\n    [EnumDataType(typeof(OrganizationUserType))]\n    public OrganizationUserType? Type { get; set; }\n    public bool AccessSecretsManager { get; set; }\n    public Permissions Permissions { get; set; }\n    public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }\n    public IEnumerable<Guid> Groups { get; set; }\n\n    public OrganizationUser ToOrganizationUser(OrganizationUser existingUser)\n    {\n        existingUser.Type = Type.Value;\n        existingUser.Permissions = CoreHelpers.ClassToJsonData(Permissions);\n        existingUser.AccessSecretsManager = AccessSecretsManager;\n        return existingUser;\n    }\n}\n\npublic class OrganizationUserResetPasswordEnrollmentRequestModel\n{\n    public string ResetPasswordKey { get; set; }\n    public string MasterPasswordHash { get; set; }\n}\n#nullable enable\npublic class OrganizationUserBulkRequestModel\n{\n    [Required, MinLength(1)]\n    public IEnumerable<Guid> Ids { get; set; } = new List<Guid>();\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string? DefaultUserCollectionName { get; set; }\n}\n#nullable disable\n\npublic class ResetPasswordWithOrgIdRequestModel : OrganizationUserResetPasswordEnrollmentRequestModel\n{\n    [Required]\n    public Guid OrganizationId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRestoreRequest.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationUserRestoreRequest\n{\n    /// <summary>\n    /// This is the encrypted default collection name to be used for restored users if required\n    /// </summary>\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string? DefaultUserCollectionName { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Organizations;\n\npublic class OrganizationVerifyDeleteRecoverRequestModel\n{\n    [Required]\n    public string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.Utilities;\nusing Bit.Core.Context;\n\nnamespace Bit.Api.AdminConsole.Models.Request;\n\npublic class PolicyRequestModel\n{\n    [Required]\n    public bool? Enabled { get; set; }\n    public Dictionary<string, object>? Data { get; set; }\n\n    public async Task<PolicyUpdate> ToPolicyUpdateAsync(Guid organizationId, PolicyType type, ICurrentContext currentContext)\n    {\n        var serializedData = PolicyDataValidator.ValidateAndSerialize(Data, type);\n        var performedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId));\n\n        return new()\n        {\n            Type = type,\n            OrganizationId = organizationId,\n            Data = serializedData,\n            Enabled = Enabled.GetValueOrDefault(),\n            PerformedBy = performedBy\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationAddRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Providers;\n\npublic class ProviderOrganizationAddRequestModel\n{\n    [Required]\n    public Guid OrganizationId { get; set; }\n\n    [Required]\n    public string Key { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationCreateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Providers;\n\npublic class ProviderOrganizationCreateRequestModel\n{\n    [Required]\n    [StringLength(256)]\n    [StrictEmailAddress]\n    public string ClientOwnerEmail { get; set; }\n    [Required]\n    public OrganizationCreateRequestModel OrganizationCreateRequest { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Providers;\n\npublic class ProviderSetupRequestModel\n{\n    [Required]\n    [StringLength(50, ErrorMessage = \"The field Name exceeds the maximum length.\")]\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; }\n    [StringLength(50, ErrorMessage = \"The field Business Name exceeds the maximum length.\")]\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string BusinessName { get; set; }\n    [Required]\n    [StringLength(256)]\n    [EmailAddress]\n    public string BillingEmail { get; set; }\n    [Required]\n    public string Token { get; set; }\n    [Required]\n    public string Key { get; set; }\n    [Required]\n    public MinimalTokenizedPaymentMethodRequest PaymentMethod { get; set; }\n    [Required]\n    public BillingAddressRequest BillingAddress { get; set; }\n\n    public virtual Provider ToProvider(Provider provider)\n    {\n        provider.Name = Name;\n        provider.BusinessName = BusinessName;\n        provider.BillingEmail = BillingEmail;\n\n        return provider;\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Providers/ProviderUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Providers;\n\npublic class ProviderUpdateRequestModel\n{\n    [Required]\n    [StringLength(50, ErrorMessage = \"The field Name exceeds the maximum length.\")]\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; }\n    [StringLength(50, ErrorMessage = \"The field Business Name exceeds the maximum length.\")]\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string BusinessName { get; set; }\n    [EmailAddress]\n    [Required]\n    [StringLength(256)]\n    public string BillingEmail { get; set; }\n\n    public virtual Provider ToProvider(Provider existingProvider, GlobalSettings globalSettings)\n    {\n        if (!globalSettings.SelfHosted)\n        {\n            // These items come from the license file\n            existingProvider.Name = Name;\n            existingProvider.BusinessName = BusinessName;\n            existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();\n        }\n        return existingProvider;\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Providers/ProviderUserRequestModels.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Providers;\n\npublic class ProviderUserInviteRequestModel\n{\n    [Required]\n    [StrictEmailAddressList]\n    public IEnumerable<string> Emails { get; set; }\n    [Required]\n    public ProviderUserType? Type { get; set; }\n}\n\npublic class ProviderUserAcceptRequestModel\n{\n    [Required]\n    public string Token { get; set; }\n}\n\npublic class ProviderUserConfirmRequestModel\n{\n    [Required]\n    public string Key { get; set; }\n}\n\npublic class ProviderUserBulkConfirmRequestModelEntry\n{\n    [Required]\n    public Guid Id { get; set; }\n    [Required]\n    public string Key { get; set; }\n}\n\npublic class ProviderUserBulkConfirmRequestModel\n{\n    [Required]\n    public IEnumerable<ProviderUserBulkConfirmRequestModelEntry> Keys { get; set; }\n\n    public Dictionary<Guid, string> ToDictionary()\n    {\n        return Keys.ToDictionary(e => e.Id, e => e.Key);\n    }\n}\n\npublic class ProviderUserUpdateRequestModel\n{\n    [Required]\n    public ProviderUserType? Type { get; set; }\n\n    public ProviderUser ToProviderUser(ProviderUser existingUser)\n    {\n        existingUser.Type = Type.Value;\n        return existingUser;\n    }\n}\n\npublic class ProviderUserBulkRequestModel\n{\n    [Required]\n    public IEnumerable<Guid> Ids { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/Providers/ProviderVerifyDeleteRecoverRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.AdminConsole.Models.Request.Providers;\n\npublic class ProviderVerifyDeleteRecoverRequestModel\n{\n    [Required]\n    public string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.Utilities;\nusing Bit.Core.Context;\n\nnamespace Bit.Api.AdminConsole.Models.Request;\n\npublic class SavePolicyRequest\n{\n    [Required]\n    public PolicyRequestModel Policy { get; set; } = null!;\n\n    public Dictionary<string, object>? Metadata { get; set; }\n\n    public async Task<SavePolicyModel> ToSavePolicyModelAsync(Guid organizationId, PolicyType type, ICurrentContext currentContext)\n    {\n        var policyUpdate = await Policy.ToPolicyUpdateAsync(organizationId, type, currentContext);\n        var metadata = PolicyDataValidator.ValidateAndDeserializeMetadata(Metadata, type);\n        var performedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId));\n\n        return new SavePolicyModel(policyUpdate, performedBy, metadata);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/BaseProfileOrganizationResponseModel.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Response;\n\n/// <summary>\n/// Contains organization properties for both OrganizationUsers and ProviderUsers.\n/// Any organization properties in sync data should be added to this class so they are populated for both\n/// members and providers.\n/// </summary>\npublic abstract class BaseProfileOrganizationResponseModel : ResponseModel\n{\n    protected BaseProfileOrganizationResponseModel(\n        string type, IProfileOrganizationDetails organizationDetails) : base(type)\n    {\n        Id = organizationDetails.OrganizationId;\n        UserId = organizationDetails.UserId;\n        Name = organizationDetails.Name;\n        Enabled = organizationDetails.Enabled;\n        Identifier = organizationDetails.Identifier;\n        ProductTierType = organizationDetails.PlanType.GetProductTier();\n        UsePolicies = organizationDetails.UsePolicies;\n        UseSso = organizationDetails.UseSso;\n        UseKeyConnector = organizationDetails.UseKeyConnector;\n        UseScim = organizationDetails.UseScim;\n        UseGroups = organizationDetails.UseGroups;\n        UseDirectory = organizationDetails.UseDirectory;\n        UseEvents = organizationDetails.UseEvents;\n        UseTotp = organizationDetails.UseTotp;\n        Use2fa = organizationDetails.Use2fa;\n        UseApi = organizationDetails.UseApi;\n        UseResetPassword = organizationDetails.UseResetPassword;\n        UsersGetPremium = organizationDetails.UsersGetPremium;\n        UseCustomPermissions = organizationDetails.UseCustomPermissions;\n        UseActivateAutofillPolicy = organizationDetails.PlanType.GetProductTier() == ProductTierType.Enterprise;\n        UseRiskInsights = organizationDetails.UseRiskInsights;\n        UseOrganizationDomains = organizationDetails.UseOrganizationDomains;\n        UseAdminSponsoredFamilies = organizationDetails.UseAdminSponsoredFamilies;\n        UseAutomaticUserConfirmation = organizationDetails.UseAutomaticUserConfirmation;\n        UseSecretsManager = organizationDetails.UseSecretsManager;\n        UsePhishingBlocker = organizationDetails.UsePhishingBlocker;\n        UseDisableSMAdsForUsers = organizationDetails.UseDisableSMAdsForUsers;\n        UsePasswordManager = organizationDetails.UsePasswordManager;\n        UseMyItems = organizationDetails.UseMyItems;\n        SelfHost = organizationDetails.SelfHost;\n        Seats = organizationDetails.Seats;\n        MaxCollections = organizationDetails.MaxCollections;\n        MaxStorageGb = organizationDetails.MaxStorageGb;\n        Key = organizationDetails.Key;\n        HasPublicAndPrivateKeys = organizationDetails.PublicKey != null && organizationDetails.PrivateKey != null;\n        SsoBound = !string.IsNullOrWhiteSpace(organizationDetails.SsoExternalId);\n        ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(organizationDetails.ResetPasswordKey);\n        ProviderId = organizationDetails.ProviderId;\n        ProviderName = organizationDetails.ProviderName;\n        ProviderType = organizationDetails.ProviderType;\n        LimitCollectionCreation = organizationDetails.LimitCollectionCreation;\n        LimitCollectionDeletion = organizationDetails.LimitCollectionDeletion;\n        LimitItemDeletion = organizationDetails.LimitItemDeletion;\n        AllowAdminAccessToAllCollectionItems = organizationDetails.AllowAdminAccessToAllCollectionItems;\n        SsoEnabled = organizationDetails.SsoEnabled ?? false;\n        if (organizationDetails.SsoConfig != null)\n        {\n            var ssoConfigData = SsoConfigurationData.Deserialize(organizationDetails.SsoConfig);\n            KeyConnectorEnabled = ssoConfigData.MemberDecryptionType == MemberDecryptionType.KeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);\n            KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;\n            SsoMemberDecryptionType = ssoConfigData.MemberDecryptionType;\n        }\n    }\n\n    public Guid Id { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; } = null!;\n    public bool Enabled { get; set; }\n    public string? Identifier { get; set; }\n    public ProductTierType ProductTierType { get; set; }\n    public bool UsePolicies { get; set; }\n    public bool UseSso { get; set; }\n    public bool UseKeyConnector { get; set; }\n    public bool UseScim { get; set; }\n    public bool UseGroups { get; set; }\n    public bool UseDirectory { get; set; }\n    public bool UseEvents { get; set; }\n    public bool UseTotp { get; set; }\n    public bool Use2fa { get; set; }\n    public bool UseApi { get; set; }\n    public bool UseResetPassword { get; set; }\n    public bool UseSecretsManager { get; set; }\n    public bool UsePasswordManager { get; set; }\n    public bool UsersGetPremium { get; set; }\n    public bool UseCustomPermissions { get; set; }\n    public bool UseActivateAutofillPolicy { get; set; }\n    public bool UseRiskInsights { get; set; }\n    public bool UseOrganizationDomains { get; set; }\n    public bool UseAdminSponsoredFamilies { get; set; }\n    public bool UseAutomaticUserConfirmation { get; set; }\n    public bool UseDisableSMAdsForUsers { get; set; }\n    public bool UsePhishingBlocker { get; set; }\n    public bool UseMyItems { get; set; }\n    public bool SelfHost { get; set; }\n    public int? Seats { get; set; }\n    public short? MaxCollections { get; set; }\n    public short? MaxStorageGb { get; set; }\n    public string? Key { get; set; }\n    public bool HasPublicAndPrivateKeys { get; set; }\n    public bool SsoBound { get; set; }\n    public bool ResetPasswordEnrolled { get; set; }\n    public bool LimitCollectionCreation { get; set; }\n    public bool LimitCollectionDeletion { get; set; }\n    public bool LimitItemDeletion { get; set; }\n    public bool AllowAdminAccessToAllCollectionItems { get; set; }\n    public Guid? ProviderId { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string? ProviderName { get; set; }\n    public ProviderType? ProviderType { get; set; }\n    public bool SsoEnabled { get; set; }\n    public bool KeyConnectorEnabled { get; set; }\n    public string? KeyConnectorUrl { get; set; }\n    public MemberDecryptionType? SsoMemberDecryptionType { get; set; }\n    public bool AccessSecretsManager { get; set; }\n    public Guid? UserId { get; set; }\n    public OrganizationUserStatusType Status { get; set; }\n    public OrganizationUserType Type { get; set; }\n    public Permissions? Permissions { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/GroupResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.AdminConsole.Models.Response;\n\npublic class GroupResponseModel : ResponseModel\n{\n    public GroupResponseModel(Group group, string obj = \"group\")\n        : base(obj)\n    {\n        if (group == null)\n        {\n            throw new ArgumentNullException(nameof(group));\n        }\n\n        Id = group.Id;\n        OrganizationId = group.OrganizationId;\n        Name = group.Name;\n        ExternalId = group.ExternalId;\n    }\n\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string Name { get; set; }\n    public string ExternalId { get; set; }\n}\n\npublic class GroupDetailsResponseModel : GroupResponseModel\n{\n    public GroupDetailsResponseModel(Group group, IEnumerable<CollectionAccessSelection> collections)\n        : base(group, \"groupDetails\")\n    {\n        Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));\n    }\n\n    public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Helpers/PolicyStatusResponses.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Helpers;\n\npublic static class PolicyStatusResponses\n{\n    public static async Task<PolicyStatusResponseModel> GetSingleOrgPolicyStatusResponseAsync(\n        this PolicyStatus policy, IOrganizationHasVerifiedDomainsQuery hasVerifiedDomainsQuery)\n    {\n        if (policy.Type is not PolicyType.SingleOrg)\n        {\n            throw new ArgumentException($\"'{nameof(policy)}' must be of type '{nameof(PolicyType.SingleOrg)}'.\", nameof(policy));\n        }\n\n        return new PolicyStatusResponseModel(policy, await CanToggleState());\n\n        async Task<bool> CanToggleState()\n        {\n            if (!await hasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policy.OrganizationId))\n            {\n                return true;\n            }\n\n            return !policy.Enabled;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/OrganizationApiKeyInformationResponseModel.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class OrganizationApiKeyInformation : ResponseModel\n{\n    public OrganizationApiKeyInformation(OrganizationApiKey key) : base(\"keyInformation\")\n    {\n        KeyType = key.Type;\n        RevisionDate = key.RevisionDate;\n    }\n\n    public OrganizationApiKeyType KeyType { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/OrganizationAutoEnrollStatusResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class OrganizationAutoEnrollStatusResponseModel : ResponseModel\n{\n    public OrganizationAutoEnrollStatusResponseModel(Guid orgId, bool resetPasswordEnabled) : base(\"organizationAutoEnrollStatus\")\n    {\n        Id = orgId;\n        ResetPasswordEnabled = resetPasswordEnabled;\n    }\n\n    public Guid Id { get; set; }\n    public bool ResetPasswordEnabled { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/OrganizationConnectionResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class OrganizationConnectionResponseModel\n{\n    public Guid? Id { get; set; }\n    public OrganizationConnectionType Type { get; set; }\n    public Guid OrganizationId { get; set; }\n    public bool Enabled { get; set; }\n    public JsonDocument Config { get; set; }\n\n    public OrganizationConnectionResponseModel(OrganizationConnection connection, Type configType)\n    {\n        if (connection == null)\n        {\n            return;\n        }\n\n        Id = connection.Id;\n        Type = connection.Type;\n        OrganizationId = connection.OrganizationId;\n        Enabled = connection.Enabled;\n        Config = JsonDocument.Parse(connection.Config);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/OrganizationDomainResponseModel.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class OrganizationDomainResponseModel : ResponseModel\n{\n    public OrganizationDomainResponseModel(OrganizationDomain organizationDomain, string obj = \"organizationDomain\")\n        : base(obj)\n    {\n        if (organizationDomain == null)\n        {\n            throw new ArgumentNullException(nameof(organizationDomain));\n        }\n\n        Id = organizationDomain.Id;\n        OrganizationId = organizationDomain.OrganizationId;\n        Txt = organizationDomain.Txt;\n        DomainName = organizationDomain.DomainName;\n        CreationDate = organizationDomain.CreationDate;\n        NextRunDate = organizationDomain.NextRunDate;\n        JobRunCount = organizationDomain.JobRunCount;\n        VerifiedDate = organizationDomain.VerifiedDate;\n        LastCheckedDate = organizationDomain.LastCheckedDate;\n    }\n\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string Txt { get; set; }\n    public string DomainName { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime NextRunDate { get; set; }\n    public int JobRunCount { get; set; }\n    public DateTime? VerifiedDate { get; set; }\n    public DateTime? LastCheckedDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/OrganizationDomainSsoDetailsResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\nusing Bit.Core.Models.Data.Organizations;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class OrganizationDomainSsoDetailsResponseModel : ResponseModel\n{\n    public OrganizationDomainSsoDetailsResponseModel(OrganizationDomainSsoDetailsData data, string obj = \"organizationDomainSsoDetails\")\n        : base(obj)\n    {\n        if (data == null)\n        {\n            throw new ArgumentNullException(nameof(data));\n        }\n\n        SsoAvailable = data.SsoAvailable;\n        DomainName = data.DomainName;\n        OrganizationIdentifier = data.OrganizationIdentifier;\n        VerifiedDate = data.VerifiedDate;\n    }\n\n    public bool SsoAvailable { get; private set; }\n    public string DomainName { get; private set; }\n    public string OrganizationIdentifier { get; private set; }\n    public DateTime? VerifiedDate { get; private set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/OrganizationKeysResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class OrganizationKeysResponseModel : ResponseModel\n{\n    public OrganizationKeysResponseModel(Organization org) : base(\"organizationKeys\")\n    {\n        if (org == null)\n        {\n            throw new ArgumentNullException(nameof(org));\n        }\n\n        PublicKey = org.PublicKey;\n        PrivateKey = org.PrivateKey;\n    }\n\n    public string PublicKey { get; set; }\n    public string PrivateKey { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/OrganizationPublicKeyResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class OrganizationPublicKeyResponseModel : ResponseModel\n{\n    public OrganizationPublicKeyResponseModel(Organization org) : base(\"organizationPublicKey\")\n    {\n        if (org == null)\n        {\n            throw new ArgumentNullException(nameof(org));\n        }\n\n        PublicKey = org.PublicKey;\n    }\n\n    public string PublicKey { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing System.Text.Json.Serialization;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Licenses;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.Utilities;\nusing Constants = Bit.Core.Constants;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class OrganizationResponseModel : ResponseModel\n{\n    public OrganizationResponseModel(\n        Organization organization,\n        Plan plan,\n        string obj = \"organization\") : base(obj)\n    {\n        if (organization == null)\n        {\n            throw new ArgumentNullException(nameof(organization));\n        }\n\n        Id = organization.Id;\n        Name = organization.Name;\n        BusinessName = organization.BusinessName;\n        BusinessAddress1 = organization.BusinessAddress1;\n        BusinessAddress2 = organization.BusinessAddress2;\n        BusinessAddress3 = organization.BusinessAddress3;\n        BusinessCountry = organization.BusinessCountry;\n        BusinessTaxNumber = organization.BusinessTaxNumber;\n        BillingEmail = organization.BillingEmail;\n        // Self-Host instances only require plan information that can be derived from the Organization record.\n        Plan = plan != null ? new PlanResponseModel(plan) : new PlanResponseModel(organization);\n        PlanType = organization.PlanType;\n        Seats = organization.Seats;\n        MaxAutoscaleSeats = organization.MaxAutoscaleSeats;\n        MaxCollections = organization.MaxCollections;\n        MaxStorageGb = organization.MaxStorageGb;\n        UsePolicies = organization.UsePolicies;\n        UseSso = organization.UseSso;\n        UseKeyConnector = organization.UseKeyConnector;\n        UseScim = organization.UseScim;\n        UseGroups = organization.UseGroups;\n        UseDirectory = organization.UseDirectory;\n        UseEvents = organization.UseEvents;\n        UseTotp = organization.UseTotp;\n        Use2fa = organization.Use2fa;\n        UseApi = organization.UseApi;\n        UseResetPassword = organization.UseResetPassword;\n        UseSecretsManager = organization.UseSecretsManager;\n        UsersGetPremium = organization.UsersGetPremium;\n        UseCustomPermissions = organization.UseCustomPermissions;\n        SelfHost = organization.SelfHost;\n        HasPublicAndPrivateKeys = organization.PublicKey != null && organization.PrivateKey != null;\n        UsePasswordManager = organization.UsePasswordManager;\n        SmSeats = organization.SmSeats;\n        SmServiceAccounts = organization.SmServiceAccounts;\n        MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats;\n        MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;\n        LimitCollectionCreation = organization.LimitCollectionCreation;\n        LimitCollectionDeletion = organization.LimitCollectionDeletion;\n        LimitItemDeletion = organization.LimitItemDeletion;\n        AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;\n        UseRiskInsights = organization.UseRiskInsights;\n        UseOrganizationDomains = organization.UseOrganizationDomains;\n        UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;\n        UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;\n        UseDisableSmAdsForUsers = organization.UseDisableSmAdsForUsers;\n        UsePhishingBlocker = organization.UsePhishingBlocker;\n        UseMyItems = organization.UseMyItems;\n    }\n\n    public Guid Id { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string BusinessName { get; set; }\n    public string BusinessAddress1 { get; set; }\n    public string BusinessAddress2 { get; set; }\n    public string BusinessAddress3 { get; set; }\n    public string BusinessCountry { get; set; }\n    public string BusinessTaxNumber { get; set; }\n    public string BillingEmail { get; set; }\n    public PlanResponseModel Plan { get; set; }\n    public PlanResponseModel SecretsManagerPlan { get; set; }\n    public PlanType PlanType { get; set; }\n    public int? Seats { get; set; }\n    public int? MaxAutoscaleSeats { get; set; } = null;\n    public short? MaxCollections { get; set; }\n    public short? MaxStorageGb { get; set; }\n    public bool UsePolicies { get; set; }\n    public bool UseSso { get; set; }\n    public bool UseKeyConnector { get; set; }\n    public bool UseScim { get; set; }\n    public bool UseGroups { get; set; }\n    public bool UseDirectory { get; set; }\n    public bool UseEvents { get; set; }\n    public bool UseTotp { get; set; }\n    public bool Use2fa { get; set; }\n    public bool UseApi { get; set; }\n    public bool UseSecretsManager { get; set; }\n    public bool UseResetPassword { get; set; }\n    public bool UsersGetPremium { get; set; }\n    public bool UseCustomPermissions { get; set; }\n    public bool SelfHost { get; set; }\n    public bool HasPublicAndPrivateKeys { get; set; }\n    public bool UsePasswordManager { get; set; }\n    public int? SmSeats { get; set; }\n    public int? SmServiceAccounts { get; set; }\n    public int? MaxAutoscaleSmSeats { get; set; }\n    public int? MaxAutoscaleSmServiceAccounts { get; set; }\n    public bool LimitCollectionCreation { get; set; }\n    public bool LimitCollectionDeletion { get; set; }\n    public bool LimitItemDeletion { get; set; }\n    public bool AllowAdminAccessToAllCollectionItems { get; set; }\n    public bool UseRiskInsights { get; set; }\n    public bool UseOrganizationDomains { get; set; }\n    public bool UseAdminSponsoredFamilies { get; set; }\n    public bool UseAutomaticUserConfirmation { get; set; }\n    public bool UseDisableSmAdsForUsers { get; set; }\n    public bool UsePhishingBlocker { get; set; }\n    public bool UseMyItems { get; set; }\n}\n\npublic class OrganizationSubscriptionResponseModel : OrganizationResponseModel\n{\n    public OrganizationSubscriptionResponseModel(\n        Organization organization,\n        Plan plan) : base(organization, plan, \"organizationSubscription\")\n    {\n        Expiration = organization.ExpirationDate;\n        StorageName = organization.Storage.HasValue ?\n            CoreHelpers.ReadableBytesSize(organization.Storage.Value) : null;\n        StorageGb = organization.Storage.HasValue ?\n            Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB\n    }\n\n    public OrganizationSubscriptionResponseModel(\n        Organization organization,\n        SubscriptionInfo subscription,\n        Plan plan,\n        bool hideSensitiveData) : this(organization, plan)\n    {\n        Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;\n        UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;\n        CustomerDiscount = subscription.CustomerDiscount != null ? new BillingCustomerDiscount(subscription.CustomerDiscount) : null;\n        Expiration = DateTime.UtcNow.AddYears(1); // Not used, so just give it a value.\n\n        if (hideSensitiveData)\n        {\n            BillingEmail = null;\n            if (Subscription != null)\n            {\n                Subscription.Items = null;\n            }\n            if (UpcomingInvoice != null)\n            {\n                UpcomingInvoice.Amount = null;\n            }\n        }\n    }\n\n    public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) :\n        this(organization, (Plan)null)\n    {\n        if (license != null)\n        {\n            // License expiration should always include grace period (unless it's in a Trial) - See OrganizationLicense.cs.\n            Expiration = license.Expires;\n\n            // Use license.ExpirationWithoutGracePeriod if available, otherwise assume license expiration minus grace period unless it's in a Trial.\n            ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ?? (license.Trial\n                ? license.Expires\n                : license.Expires?.AddDays(-Constants.OrganizationSelfHostSubscriptionGracePeriodDays));\n        }\n    }\n\n    public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license, ClaimsPrincipal claimsPrincipal) :\n        this(organization, (Plan)null)\n    {\n        if (license != null)\n        {\n            // CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim\n            // The token's expiration is cryptographically secured and cannot be tampered with\n            // The file's Expires property can be manually edited and should NOT be trusted for display\n            if (claimsPrincipal != null)\n            {\n                Expiration = claimsPrincipal.GetValue<DateTime>(OrganizationLicenseConstants.Expires);\n                ExpirationWithoutGracePeriod = claimsPrincipal.GetValue<DateTime?>(OrganizationLicenseConstants.ExpirationWithoutGracePeriod);\n            }\n            else\n            {\n                // No token - use the license file expiration (for older licenses without tokens)\n                Expiration = license.Expires;\n                ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ?? (license.Trial\n                    ? license.Expires\n                    : license.Expires?.AddDays(-Constants.OrganizationSelfHostSubscriptionGracePeriodDays));\n            }\n        }\n    }\n\n    public string StorageName { get; set; }\n    public double? StorageGb { get; set; }\n    public BillingCustomerDiscount CustomerDiscount { get; set; }\n    public BillingSubscription Subscription { get; set; }\n    public BillingSubscriptionUpcomingInvoice UpcomingInvoice { get; set; }\n\n    /// <summary>\n    /// Date when a self-hosted organization's subscription expires, without any grace period.\n    /// </summary>\n    public DateTime? ExpirationWithoutGracePeriod { get; set; }\n\n    /// <summary>\n    /// Date when a self-hosted organization expires (includes grace period).\n    /// </summary>\n    public DateTime? Expiration { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Api.Models.Response;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class OrganizationUserResponseModel : ResponseModel\n{\n    public OrganizationUserResponseModel(OrganizationUser organizationUser, string obj = \"organizationUser\")\n        : base(obj)\n    {\n        if (organizationUser == null)\n        {\n            throw new ArgumentNullException(nameof(organizationUser));\n        }\n\n        Id = organizationUser.Id;\n        UserId = organizationUser.UserId;\n        Type = organizationUser.Type;\n        Status = organizationUser.Status;\n        ExternalId = organizationUser.ExternalId;\n        AccessSecretsManager = organizationUser.AccessSecretsManager;\n        Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);\n        ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(organizationUser.ResetPasswordKey);\n    }\n\n    public OrganizationUserResponseModel(OrganizationUserUserDetails organizationUser,\n        string obj = \"organizationUser\")\n        : base(obj)\n    {\n        if (organizationUser == null)\n        {\n            throw new ArgumentNullException(nameof(organizationUser));\n        }\n\n        Id = organizationUser.Id;\n        UserId = organizationUser.UserId;\n        Type = organizationUser.Type;\n        Status = organizationUser.Status;\n        ExternalId = organizationUser.ExternalId;\n        AccessSecretsManager = organizationUser.AccessSecretsManager;\n        Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);\n        ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(organizationUser.ResetPasswordKey);\n        UsesKeyConnector = organizationUser.UsesKeyConnector;\n        HasMasterPassword = organizationUser.HasMasterPassword;\n    }\n\n    public Guid Id { get; set; }\n    public Guid? UserId { get; set; }\n    public OrganizationUserType Type { get; set; }\n    public OrganizationUserStatusType Status { get; set; }\n    public string ExternalId { get; set; }\n    public bool AccessSecretsManager { get; set; }\n    public Permissions Permissions { get; set; }\n    public bool ResetPasswordEnrolled { get; set; }\n    public bool UsesKeyConnector { get; set; }\n    public bool HasMasterPassword { get; set; }\n}\n\npublic class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel\n{\n    public OrganizationUserDetailsResponseModel(\n        OrganizationUser organizationUser,\n        bool claimedByOrganization,\n        string ssoExternalId,\n        IEnumerable<CollectionAccessSelection> collections)\n        : base(organizationUser, \"organizationUserDetails\")\n    {\n        ClaimedByOrganization = claimedByOrganization;\n        SsoExternalId = ssoExternalId;\n        Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));\n    }\n\n    public OrganizationUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,\n        bool claimedByOrganization,\n        IEnumerable<CollectionAccessSelection> collections)\n        : base(organizationUser, \"organizationUserDetails\")\n    {\n        ClaimedByOrganization = claimedByOrganization;\n        SsoExternalId = organizationUser.SsoExternalId;\n        Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));\n    }\n\n    [Obsolete(\"Please use ClaimedByOrganization instead. This property will be removed in a future version.\")]\n    public bool ManagedByOrganization\n    {\n        get => ClaimedByOrganization;\n        set => ClaimedByOrganization = value;\n    }\n    public bool ClaimedByOrganization { get; set; }\n    public string SsoExternalId { get; set; }\n\n    public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }\n\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public IEnumerable<Guid> Groups { get; set; }\n}\n\n#nullable enable\npublic class OrganizationUserUserMiniDetailsResponseModel : ResponseModel\n{\n    public OrganizationUserUserMiniDetailsResponseModel(OrganizationUserUserDetails organizationUser)\n        : base(\"organizationUserUserMiniDetails\")\n    {\n        Id = organizationUser.Id;\n        UserId = organizationUser.UserId;\n        Type = organizationUser.Type;\n        Status = organizationUser.Status;\n        Name = organizationUser.Name;\n        Email = organizationUser.Email;\n    }\n\n    public Guid Id { get; }\n    public Guid? UserId { get; }\n    public OrganizationUserType Type { get; }\n    public OrganizationUserStatusType Status { get; }\n    public string? Name { get; }\n    public string Email { get; }\n}\n#nullable disable\n\npublic class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel\n{\n    public OrganizationUserUserDetailsResponseModel((OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization) data, string obj = \"organizationUserUserDetails\")\n        : base(data.OrgUser, obj)\n    {\n        if (data.OrgUser == null)\n        {\n            throw new ArgumentNullException(nameof(data.OrgUser));\n        }\n\n        Name = data.OrgUser.Name;\n        Email = data.OrgUser.Email;\n        AvatarColor = data.OrgUser.AvatarColor;\n        TwoFactorEnabled = data.TwoFactorEnabled;\n        SsoBound = !string.IsNullOrWhiteSpace(data.OrgUser.SsoExternalId);\n        Collections = data.OrgUser.Collections.Select(c => new SelectionReadOnlyResponseModel(c));\n        Groups = data.OrgUser.Groups;\n        // Prevent reset password when using key connector.\n        ResetPasswordEnrolled = ResetPasswordEnrolled && !data.OrgUser.UsesKeyConnector;\n        ClaimedByOrganization = data.ClaimedByOrganization;\n    }\n\n    public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,\n        bool twoFactorEnabled, bool claimedByOrganization, string obj = \"organizationUserUserDetails\")\n        : base(organizationUser, obj)\n    {\n        if (organizationUser == null)\n        {\n            throw new ArgumentNullException(nameof(organizationUser));\n        }\n\n        Name = organizationUser.Name;\n        Email = organizationUser.Email;\n        AvatarColor = organizationUser.AvatarColor;\n        TwoFactorEnabled = twoFactorEnabled;\n        SsoBound = !string.IsNullOrWhiteSpace(organizationUser.SsoExternalId);\n        Collections = organizationUser.Collections.Select(c => new SelectionReadOnlyResponseModel(c));\n        Groups = organizationUser.Groups;\n        // Prevent reset password when using key connector.\n        ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;\n        ClaimedByOrganization = claimedByOrganization;\n    }\n\n    public string Name { get; set; }\n    public string Email { get; set; }\n    public string AvatarColor { get; set; }\n    public bool TwoFactorEnabled { get; set; }\n    public bool SsoBound { get; set; }\n    [Obsolete(\"Please use ClaimedByOrganization instead. This property will be removed in a future version.\")]\n    public bool ManagedByOrganization\n    {\n        get => ClaimedByOrganization;\n        set => ClaimedByOrganization = value;\n    }\n    /// <summary>\n    /// Indicates if the organization claimed the user. If a user is \"claimed\" by an organization,\n    /// the organization has greater control over their account, and some user actions are restricted.\n    /// </summary>\n    public bool ClaimedByOrganization { get; set; }\n    public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }\n    public IEnumerable<Guid> Groups { get; set; }\n}\n\npublic class OrganizationUserResetPasswordDetailsResponseModel : ResponseModel\n{\n    public OrganizationUserResetPasswordDetailsResponseModel(OrganizationUserResetPasswordDetails orgUser,\n        string obj = \"organizationUserResetPasswordDetails\") : base(obj)\n    {\n        if (orgUser == null)\n        {\n            throw new ArgumentNullException(nameof(orgUser));\n        }\n\n        OrganizationUserId = orgUser.OrganizationUserId;\n        Kdf = orgUser.Kdf;\n        KdfIterations = orgUser.KdfIterations;\n        KdfMemory = orgUser.KdfMemory;\n        KdfParallelism = orgUser.KdfParallelism;\n        ResetPasswordKey = orgUser.ResetPasswordKey;\n        EncryptedPrivateKey = orgUser.EncryptedPrivateKey;\n    }\n\n    public Guid OrganizationUserId { get; set; }\n    public KdfType Kdf { get; set; }\n    public int KdfIterations { get; set; }\n    public int? KdfMemory { get; set; }\n    public int? KdfParallelism { get; set; }\n    public string ResetPasswordKey { get; set; }\n    public string EncryptedPrivateKey { get; set; }\n}\n\npublic class OrganizationUserPublicKeyResponseModel : ResponseModel\n{\n    public OrganizationUserPublicKeyResponseModel(Guid id, Guid userId,\n        string key, string obj = \"organizationUserPublicKeyResponseModel\") :\n        base(obj)\n    {\n        Id = id;\n        UserId = userId;\n        Key = key;\n    }\n\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public string Key { get; set; }\n}\n\npublic class OrganizationUserBulkResponseModel : ResponseModel\n{\n    public OrganizationUserBulkResponseModel(Guid id, string error)\n        : base(\"OrganizationBulkConfirmResponseModel\")\n    {\n        Id = id;\n        Error = error;\n    }\n    public Guid Id { get; set; }\n    public string Error { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class PolicyResponseModel : ResponseModel\n{\n    public PolicyResponseModel() : base(\"policy\")\n    {\n    }\n\n    public PolicyResponseModel(Policy policy, string obj = \"policy\")\n        : base(obj)\n    {\n        if (policy == null)\n        {\n            throw new ArgumentNullException(nameof(policy));\n        }\n\n        Id = policy.Id;\n        OrganizationId = policy.OrganizationId;\n        Type = policy.Type;\n        Enabled = policy.Enabled;\n        if (!string.IsNullOrWhiteSpace(policy.Data))\n        {\n            Data = JsonSerializer.Deserialize<Dictionary<string, object>>(policy.Data);\n        }\n        RevisionDate = policy.RevisionDate;\n    }\n\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public PolicyType Type { get; set; }\n    public Dictionary<string, object> Data { get; set; }\n    public bool Enabled { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/PolicyStatusResponseModel.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class PolicyStatusResponseModel : ResponseModel\n{\n    public PolicyStatusResponseModel(PolicyStatus policy, bool canToggleState = true) : base(\"policy\")\n    {\n        OrganizationId = policy.OrganizationId;\n        Type = policy.Type;\n\n        if (!string.IsNullOrWhiteSpace(policy.Data))\n        {\n            Data = JsonSerializer.Deserialize<Dictionary<string, object>>(policy.Data) ?? new();\n        }\n\n        Enabled = policy.Enabled;\n        CanToggleState = canToggleState;\n    }\n\n    public Guid OrganizationId { get; init; }\n    public PolicyType Type { get; init; }\n    public Dictionary<string, object> Data { get; init; } = new();\n    public bool Enabled { get; init; }\n\n    /// <summary>\n    /// Indicates whether the Policy can be enabled/disabled\n    /// </summary>\n    public bool CanToggleState { get; init; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\nusing Bit.Core.Models.Data.Organizations;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class VerifiedOrganizationDomainSsoDetailResponseModel : ResponseModel\n{\n    public VerifiedOrganizationDomainSsoDetailResponseModel(VerifiedOrganizationDomainSsoDetail data)\n        : base(\"verifiedOrganizationDomainSsoDetails\")\n    {\n        if (data is null)\n        {\n            throw new ArgumentNullException(nameof(data));\n        }\n\n        DomainName = data.DomainName;\n        OrganizationIdentifier = data.OrganizationIdentifier;\n        OrganizationName = data.OrganizationName;\n    }\n    public string DomainName { get; }\n    public string OrganizationIdentifier { get; }\n    public string OrganizationName { get; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Response;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Organizations;\n\npublic class VerifiedOrganizationDomainSsoDetailsResponseModel(\n    IEnumerable<VerifiedOrganizationDomainSsoDetailResponseModel> data,\n    string continuationToken = null)\n    : ListResponseModel<VerifiedOrganizationDomainSsoDetailResponseModel>(data, continuationToken);\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/PendingOrganizationAuthRequestResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Reflection;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.AdminConsole.Models.Response;\n\npublic class PendingOrganizationAuthRequestResponseModel : ResponseModel\n{\n    public PendingOrganizationAuthRequestResponseModel(OrganizationAdminAuthRequest authRequest, string obj = \"pending-org-auth-request\") : base(obj)\n    {\n        if (authRequest == null)\n        {\n            throw new ArgumentNullException(nameof(authRequest));\n        }\n\n        Id = authRequest.Id;\n        UserId = authRequest.UserId;\n        OrganizationUserId = authRequest.OrganizationUserId;\n        Email = authRequest.Email;\n        PublicKey = authRequest.PublicKey;\n        RequestDeviceIdentifier = authRequest.RequestDeviceIdentifier;\n        RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString())\n            .FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();\n        RequestIpAddress = authRequest.RequestIpAddress;\n        RequestCountryName = authRequest.RequestCountryName;\n        CreationDate = authRequest.CreationDate;\n    }\n\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public Guid OrganizationUserId { get; set; }\n    public string Email { get; set; }\n    public string PublicKey { get; set; }\n    public string RequestDeviceIdentifier { get; set; }\n    public string RequestDeviceType { get; set; }\n    public string RequestIpAddress { get; set; }\n    public string RequestCountryName { get; set; }\n    public DateTime CreationDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs",
    "content": "﻿using Bit.Core.Billing.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Response;\n\n/// <summary>\n/// Sync data for organization members and their organization.\n/// Note: see <see cref=\"ProfileProviderOrganizationResponseModel\"/> for organization sync data received by provider users.\n/// </summary>\npublic class ProfileOrganizationResponseModel : BaseProfileOrganizationResponseModel\n{\n    public ProfileOrganizationResponseModel(\n        OrganizationUserOrganizationDetails organizationDetails,\n        IEnumerable<Guid> organizationIdsClaimingUser)\n        : base(\"profileOrganization\", organizationDetails)\n    {\n        Status = organizationDetails.Status;\n        Type = organizationDetails.Type;\n        OrganizationUserId = organizationDetails.OrganizationUserId;\n        UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organizationDetails.OrganizationId);\n        Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationDetails.Permissions);\n        IsAdminInitiated = organizationDetails.IsAdminInitiated ?? false;\n        FamilySponsorshipFriendlyName = organizationDetails.FamilySponsorshipFriendlyName;\n        FamilySponsorshipLastSyncDate = organizationDetails.FamilySponsorshipLastSyncDate;\n        FamilySponsorshipToDelete = organizationDetails.FamilySponsorshipToDelete;\n        FamilySponsorshipValidUntil = organizationDetails.FamilySponsorshipValidUntil;\n        FamilySponsorshipAvailable = (organizationDetails.FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&\n            SponsoredPlans.Get(PlanSponsorshipType.FamiliesForEnterprise)\n            .UsersCanSponsor(organizationDetails);\n        AccessSecretsManager = organizationDetails.AccessSecretsManager;\n    }\n\n    public Guid OrganizationUserId { get; set; }\n    public bool UserIsClaimedByOrganization { get; set; }\n    public string? FamilySponsorshipFriendlyName { get; set; }\n    public bool FamilySponsorshipAvailable { get; set; }\n    public DateTime? FamilySponsorshipLastSyncDate { get; set; }\n    public DateTime? FamilySponsorshipValidUntil { get; set; }\n    public bool? FamilySponsorshipToDelete { get; set; }\n    public bool IsAdminInitiated { get; set; }\n    /// <summary>\n    /// Obsolete property for backward compatibility\n    /// </summary>\n    [Obsolete(\"Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.\")]\n    public bool UserIsManagedByOrganization\n    {\n        get => UserIsClaimedByOrganization;\n        set => UserIsClaimedByOrganization = value;\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.AdminConsole.Models.Response;\n\n/// <summary>\n/// Sync data for provider users and their managed organizations.\n/// Note: see <see cref=\"ProfileOrganizationResponseModel\"/> for organization sync data received by organization members.\n/// </summary>\npublic class ProfileProviderOrganizationResponseModel : BaseProfileOrganizationResponseModel\n{\n    public ProfileProviderOrganizationResponseModel(ProviderUserOrganizationDetails organizationDetails)\n        : base(\"profileProviderOrganization\", organizationDetails)\n    {\n        Status = OrganizationUserStatusType.Confirmed; // Provider users are always confirmed\n        Type = OrganizationUserType.Owner; // Provider users behave like Owners\n        ProviderId = organizationDetails.ProviderId;\n        ProviderName = organizationDetails.ProviderName;\n        ProviderType = organizationDetails.ProviderType;\n        Permissions = new Permissions();\n        AccessSecretsManager = false; // Provider users cannot access Secrets Manager\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Providers/ProfileProviderResponseModel.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Providers;\n\npublic class ProfileProviderResponseModel : ResponseModel\n{\n    public ProfileProviderResponseModel(ProviderUserProviderDetails provider)\n        : base(\"profileProvider\")\n    {\n        Id = provider.ProviderId;\n        Name = provider.Name;\n        Key = provider.Key;\n        Status = provider.Status;\n        Type = provider.Type;\n        Enabled = provider.Enabled;\n        Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(provider.Permissions);\n        UserId = provider.UserId;\n        UseEvents = provider.UseEvents;\n        ProviderStatus = provider.ProviderStatus;\n        ProviderType = provider.ProviderType;\n    }\n\n    public Guid Id { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; }\n    public string Key { get; set; }\n    public ProviderUserStatusType Status { get; set; }\n    public ProviderUserType Type { get; set; }\n    public bool Enabled { get; set; }\n    public Permissions Permissions { get; set; }\n    public Guid? UserId { get; set; }\n    public bool UseEvents { get; set; }\n    public ProviderStatusType ProviderStatus { get; set; }\n    public ProviderType ProviderType { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Providers/ProviderOrganizationResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Providers;\n\npublic class ProviderOrganizationResponseModel : ResponseModel\n{\n    public ProviderOrganizationResponseModel(ProviderOrganization providerOrganization,\n        string obj = \"providerOrganization\") : base(obj)\n    {\n        if (providerOrganization == null)\n        {\n            throw new ArgumentNullException(nameof(providerOrganization));\n        }\n\n        Id = providerOrganization.Id;\n        ProviderId = providerOrganization.ProviderId;\n        OrganizationId = providerOrganization.OrganizationId;\n        Key = providerOrganization.Key;\n        Settings = providerOrganization.Settings;\n        CreationDate = providerOrganization.CreationDate;\n        RevisionDate = providerOrganization.RevisionDate;\n    }\n\n    public ProviderOrganizationResponseModel(ProviderOrganizationOrganizationDetails providerOrganization,\n        string obj = \"providerOrganization\") : base(obj)\n    {\n        if (providerOrganization == null)\n        {\n            throw new ArgumentNullException(nameof(providerOrganization));\n        }\n\n        Id = providerOrganization.Id;\n        ProviderId = providerOrganization.ProviderId;\n        OrganizationId = providerOrganization.OrganizationId;\n        Key = providerOrganization.Key;\n        Settings = providerOrganization.Settings;\n        CreationDate = providerOrganization.CreationDate;\n        RevisionDate = providerOrganization.RevisionDate;\n        UserCount = providerOrganization.UserCount;\n        Seats = providerOrganization.Seats;\n        OccupiedSeats = providerOrganization.OccupiedSeats;\n        RemainingSeats = providerOrganization.Seats - providerOrganization.OccupiedSeats;\n        Plan = providerOrganization.Plan;\n    }\n\n    public Guid Id { get; set; }\n    public Guid ProviderId { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string Key { get; set; }\n    public string Settings { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime RevisionDate { get; set; }\n    public int UserCount { get; set; }\n    public int? Seats { get; set; }\n    public int? OccupiedSeats { get; set; }\n    public int? RemainingSeats { get; set; }\n    public string Plan { get; set; }\n}\n\npublic class ProviderOrganizationOrganizationDetailsResponseModel : ProviderOrganizationResponseModel\n{\n    public ProviderOrganizationOrganizationDetailsResponseModel(ProviderOrganizationOrganizationDetails providerOrganization,\n        string obj = \"providerOrganizationOrganizationDetail\") : base(providerOrganization, obj)\n    {\n        if (providerOrganization == null)\n        {\n            throw new ArgumentNullException(nameof(providerOrganization));\n        }\n\n        OrganizationName = providerOrganization.OrganizationName;\n    }\n\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string OrganizationName { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Providers/ProviderResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Providers;\n\npublic class ProviderResponseModel : ResponseModel\n{\n    public ProviderResponseModel(Provider provider, string obj = \"provider\") : base(obj)\n    {\n        if (provider == null)\n        {\n            throw new ArgumentNullException(nameof(provider));\n        }\n\n        Id = provider.Id;\n        Name = provider.Name;\n        BusinessName = provider.BusinessName;\n        BusinessAddress1 = provider.BusinessAddress1;\n        BusinessAddress2 = provider.BusinessAddress2;\n        BusinessAddress3 = provider.BusinessAddress3;\n        BusinessCountry = provider.BusinessCountry;\n        BusinessTaxNumber = provider.BusinessTaxNumber;\n        BillingEmail = provider.BillingEmail;\n        CreationDate = provider.CreationDate;\n        Type = provider.Type;\n    }\n\n    public Guid Id { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; }\n    public string BusinessName { get; set; }\n    public string BusinessAddress1 { get; set; }\n    public string BusinessAddress2 { get; set; }\n    public string BusinessAddress3 { get; set; }\n    public string BusinessCountry { get; set; }\n    public string BusinessTaxNumber { get; set; }\n    public string BillingEmail { get; set; }\n    public DateTime CreationDate { get; set; }\n    public ProviderType Type { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Models/Response/Providers/ProviderUserResponseModel.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Models.Response.Providers;\n\npublic class ProviderUserResponseModel : ResponseModel\n{\n    public ProviderUserResponseModel(ProviderUser providerUser, string obj = \"providerUser\")\n        : base(obj)\n    {\n        if (providerUser == null)\n        {\n            throw new ArgumentNullException(nameof(providerUser));\n        }\n\n        Id = providerUser.Id;\n        UserId = providerUser.UserId;\n        Type = providerUser.Type;\n        Status = providerUser.Status;\n        Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(providerUser.Permissions);\n    }\n\n    public ProviderUserResponseModel(ProviderUserUserDetails providerUser, string obj = \"providerUser\")\n        : base(obj)\n    {\n        if (providerUser == null)\n        {\n            throw new ArgumentNullException(nameof(providerUser));\n        }\n\n        Id = providerUser.Id;\n        UserId = providerUser.UserId;\n        Type = providerUser.Type;\n        Status = providerUser.Status;\n        Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(providerUser.Permissions);\n    }\n\n    public Guid Id { get; set; }\n    public Guid? UserId { get; set; }\n    public ProviderUserType Type { get; set; }\n    public ProviderUserStatusType Status { get; set; }\n    public Permissions Permissions { get; set; }\n}\n\npublic class ProviderUserUserDetailsResponseModel : ProviderUserResponseModel\n{\n    public ProviderUserUserDetailsResponseModel(ProviderUserUserDetails providerUser,\n        string obj = \"providerUserUserDetails\") : base(providerUser, obj)\n    {\n        if (providerUser == null)\n        {\n            throw new ArgumentNullException(nameof(providerUser));\n        }\n\n        Name = providerUser.Name;\n        Email = providerUser.Email;\n    }\n\n    public string Name { get; set; }\n    public string Email { get; set; }\n}\n\npublic class ProviderUserPublicKeyResponseModel : ResponseModel\n{\n    public ProviderUserPublicKeyResponseModel(Guid id, Guid userId, string key,\n        string obj = \"providerUserPublicKeyResponseModel\") : base(obj)\n    {\n        Id = id;\n        UserId = userId;\n        Key = key;\n    }\n\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public string Key { get; set; }\n}\n\npublic class ProviderUserBulkResponseModel : ResponseModel\n{\n    public ProviderUserBulkResponseModel(Guid id, string error,\n        string obj = \"providerBulkConfirmResponseModel\") : base(obj)\n    {\n        Id = id;\n        Error = error;\n    }\n    public Guid Id { get; set; }\n    public string Error { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Controllers/GroupsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Api.AdminConsole.Public.Models.Response;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Public.Controllers;\n\n[Route(\"public/groups\")]\n[Authorize(\"Organization\")]\npublic class GroupsController : Controller\n{\n    private readonly IGroupRepository _groupRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly ICreateGroupCommand _createGroupCommand;\n    private readonly IUpdateGroupCommand _updateGroupCommand;\n\n    public GroupsController(\n        IGroupRepository groupRepository,\n        IOrganizationRepository organizationRepository,\n        ICurrentContext currentContext,\n        ICreateGroupCommand createGroupCommand,\n        IUpdateGroupCommand updateGroupCommand)\n    {\n        _groupRepository = groupRepository;\n        _organizationRepository = organizationRepository;\n        _currentContext = currentContext;\n        _createGroupCommand = createGroupCommand;\n        _updateGroupCommand = updateGroupCommand;\n    }\n\n    /// <summary>\n    /// Retrieve a group.\n    /// </summary>\n    /// <remarks>\n    /// Retrieves the details of an existing group. You need only supply the unique group identifier\n    /// that was returned upon group creation.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the group to be retrieved.</param>\n    [HttpGet(\"{id}\")]\n    [ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Get(Guid id)\n    {\n        var groupDetails = await _groupRepository.GetByIdWithCollectionsAsync(id);\n        var group = groupDetails?.Item1;\n        if (group == null || group.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        var response = new GroupResponseModel(group, groupDetails.Item2);\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// Retrieve a groups's member ids\n    /// </summary>\n    /// <remarks>\n    /// Retrieves the unique identifiers for all members that are associated with this group. You need only\n    /// supply the unique group identifier that was returned upon group creation.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the group to be retrieved.</param>\n    [HttpGet(\"{id}/member-ids\")]\n    [ProducesResponseType(typeof(HashSet<Guid>), (int)HttpStatusCode.OK)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> GetMemberIds(Guid id)\n    {\n        var group = await _groupRepository.GetByIdAsync(id);\n        if (group == null || group.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        var orgUserIds = await _groupRepository.GetManyUserIdsByIdAsync(id);\n        return new JsonResult(orgUserIds);\n    }\n\n    /// <summary>\n    /// List all groups.\n    /// </summary>\n    /// <remarks>\n    /// Returns a list of your organization's groups.\n    /// Group objects listed in this call include information about their associated collections.\n    /// </remarks>\n    [HttpGet]\n    [ProducesResponseType(typeof(ListResponseModel<GroupResponseModel>), (int)HttpStatusCode.OK)]\n    public async Task<IActionResult> List()\n    {\n        var groups = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(_currentContext.OrganizationId.Value);\n        var groupResponses = groups.Select(g => new GroupResponseModel(g.Item1, g.Item2));\n        var response = new ListResponseModel<GroupResponseModel>(groupResponses);\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// Create a group.\n    /// </summary>\n    /// <remarks>\n    /// Creates a new group object.\n    /// </remarks>\n    /// <param name=\"model\">The request model.</param>\n    [HttpPost]\n    [ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    public async Task<IActionResult> Post([FromBody] GroupCreateUpdateRequestModel model)\n    {\n        var group = model.ToGroup(_currentContext.OrganizationId.Value);\n        var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId.Value);\n        var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList();\n        await _createGroupCommand.CreateGroupAsync(group, organization, associations);\n        var response = new GroupResponseModel(group, associations);\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// Update a group.\n    /// </summary>\n    /// <remarks>\n    /// Updates the specified group object. If a property is not provided,\n    /// the value of the existing property will be reset.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the group to be updated.</param>\n    /// <param name=\"model\">The request model.</param>\n    [HttpPut(\"{id}\")]\n    [ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Put(Guid id, [FromBody] GroupCreateUpdateRequestModel model)\n    {\n        var existingGroup = await _groupRepository.GetByIdAsync(id);\n        if (existingGroup == null || existingGroup.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n\n        var updatedGroup = model.ToGroup(existingGroup);\n        var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId.Value);\n        var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList();\n        await _updateGroupCommand.UpdateGroupAsync(updatedGroup, organization, associations);\n        var response = new GroupResponseModel(updatedGroup, associations);\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// Update a group's members.\n    /// </summary>\n    /// <remarks>\n    /// Updates the specified group's member associations.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the group to be updated.</param>\n    /// <param name=\"model\">The request model.</param>\n    [HttpPut(\"{id}/member-ids\")]\n    [ProducesResponseType((int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> PutMemberIds(Guid id, [FromBody] UpdateMemberIdsRequestModel model)\n    {\n        var existingGroup = await _groupRepository.GetByIdAsync(id);\n        if (existingGroup == null || existingGroup.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        await _groupRepository.UpdateUsersAsync(existingGroup.Id, model.MemberIds);\n        return new OkResult();\n    }\n\n    /// <summary>\n    /// Delete a group.\n    /// </summary>\n    /// <remarks>\n    /// Permanently deletes a group. This cannot be undone.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the group to be deleted.</param>\n    [HttpDelete(\"{id}\")]\n    [ProducesResponseType((int)HttpStatusCode.OK)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Delete(Guid id)\n    {\n        var group = await _groupRepository.GetByIdAsync(id);\n        if (group == null || group.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        await _groupRepository.DeleteAsync(group);\n        return new OkResult();\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Controllers/MembersController.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Api.AdminConsole.Public.Models.Response;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities.Commands;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors.ErrorMapper;\n\nnamespace Bit.Api.AdminConsole.Public.Controllers;\n\n[Route(\"public/members\")]\n[Authorize(\"Organization\")]\npublic class MembersController : Controller\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IOrganizationService _organizationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly IUpdateOrganizationUserCommand _updateOrganizationUserCommand;\n    private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;\n    private readonly IStripePaymentService _paymentService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;\n    private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;\n    private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommandV2;\n    private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;\n    private readonly IFeatureService _featureService;\n    private readonly IInviteOrganizationUsersCommand _inviteOrganizationUsersCommand;\n    private readonly IPricingClient _pricingClient;\n    private readonly TimeProvider _timeProvider;\n\n    public MembersController(\n        IOrganizationUserRepository organizationUserRepository,\n        IGroupRepository groupRepository,\n        IOrganizationService organizationService,\n        ICurrentContext currentContext,\n        IUpdateOrganizationUserCommand updateOrganizationUserCommand,\n        IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,\n        IStripePaymentService paymentService,\n        IOrganizationRepository organizationRepository,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IRemoveOrganizationUserCommand removeOrganizationUserCommand,\n        IResendOrganizationInviteCommand resendOrganizationInviteCommand,\n        IRevokeOrganizationUserCommand revokeOrganizationUserCommandV2,\n        IRestoreOrganizationUserCommand restoreOrganizationUserCommand,\n        IFeatureService featureService,\n        IInviteOrganizationUsersCommand inviteOrganizationUsersCommand,\n        IPricingClient pricingClient,\n        TimeProvider timeProvider)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _groupRepository = groupRepository;\n        _organizationService = organizationService;\n        _currentContext = currentContext;\n        _updateOrganizationUserCommand = updateOrganizationUserCommand;\n        _updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;\n        _paymentService = paymentService;\n        _organizationRepository = organizationRepository;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _removeOrganizationUserCommand = removeOrganizationUserCommand;\n        _resendOrganizationInviteCommand = resendOrganizationInviteCommand;\n        _revokeOrganizationUserCommandV2 = revokeOrganizationUserCommandV2;\n        _restoreOrganizationUserCommand = restoreOrganizationUserCommand;\n        _featureService = featureService;\n        _inviteOrganizationUsersCommand = inviteOrganizationUsersCommand;\n        _pricingClient = pricingClient;\n        _timeProvider = timeProvider;\n    }\n\n    /// <summary>\n    /// Retrieve a member.\n    /// </summary>\n    /// <remarks>\n    /// Retrieves the details of an existing member of the organization. You need only supply the\n    /// unique member identifier that was returned upon member creation.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the member to be retrieved.</param>\n    [HttpGet(\"{id}\")]\n    [ProducesResponseType(typeof(MemberResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Get(Guid id)\n    {\n        var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(id);\n        if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser),\n            collections);\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// Retrieve a member's group ids\n    /// </summary>\n    /// <remarks>\n    /// Retrieves the unique identifiers for all groups that are associated with this member. You need only\n    /// supply the unique member identifier that was returned upon member creation.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the member to be retrieved.</param>\n    [HttpGet(\"{id}/group-ids\")]\n    [ProducesResponseType(typeof(HashSet<Guid>), (int)HttpStatusCode.OK)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> GetGroupIds(Guid id)\n    {\n        var orgUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        var groupIds = await _groupRepository.GetManyIdsByUserIdAsync(id);\n        return new JsonResult(groupIds);\n    }\n\n    /// <summary>\n    /// List all members.\n    /// </summary>\n    /// <remarks>\n    /// Returns a list of your organization's members.\n    /// Member objects listed in this call include information about their associated collections.\n    /// </remarks>\n    [HttpGet]\n    [ProducesResponseType(typeof(ListResponseModel<MemberResponseModel>), (int)HttpStatusCode.OK)]\n    public async Task<IActionResult> List()\n    {\n        var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId!.Value, includeSharedCollections: true);\n\n        var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails);\n        var memberResponses = organizationUserUserDetails.Select(u =>\n        {\n            return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, u.Collections);\n        });\n        var response = new ListResponseModel<MemberResponseModel>(memberResponses);\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// Create a member.\n    /// </summary>\n    /// <remarks>\n    /// Creates a new member object by inviting a user to the organization.\n    /// </remarks>\n    /// <param name=\"model\">The request model.</param>\n    [HttpPost]\n    [ProducesResponseType(typeof(MemberResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    public async Task<IActionResult> Post([FromBody] MemberCreateRequestModel model)\n    {\n        var hasStandaloneSecretsManager = false;\n\n        var organization = await _organizationRepository.GetByIdAsync(_currentContext.OrganizationId!.Value);\n\n        if (organization != null)\n        {\n            hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);\n        }\n\n        var invite = model.ToOrganizationUserInvite();\n        if (_featureService.IsEnabled(FeatureFlagKeys.PublicMembersInviteRefactor))\n        {\n            return await PostInviteUserAsync_vNext(model, organization!, hasStandaloneSecretsManager);\n        }\n\n        invite.AccessSecretsManager = hasStandaloneSecretsManager;\n\n        var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId!.Value, null,\n            systemUser: null, invite, model.ExternalId);\n        var response = new MemberResponseModel(user, invite.Collections);\n        return new JsonResult(response);\n    }\n\n    private async Task<IActionResult> PostInviteUserAsync_vNext(\n        MemberCreateRequestModel model,\n        Core.AdminConsole.Entities.Organization organization,\n        bool hasStandaloneSecretsManager)\n    {\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n        var inviteOrganization = new InviteOrganization(organization, plan);\n        var request = model.ToInviteRequest(inviteOrganization, hasStandaloneSecretsManager, Guid.Empty, _timeProvider.GetUtcNow());\n\n        var result = await _inviteOrganizationUsersCommand.InviteImportedOrganizationUsersAsync(request);\n\n        switch (result)\n        {\n            case Success<InviteOrganizationUsersResponse> success:\n                var user = success.Value.InvitedUsers.First();\n                var collections = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList();\n                var response = new MemberResponseModel(user, collections);\n                return new JsonResult(response);\n            case Failure<InviteOrganizationUsersResponse> { Error.Message: NoUsersToInviteError.Code }:\n                throw new BadRequestException(\"This user has already been invited.\");\n            case Failure<InviteOrganizationUsersResponse> failure:\n                throw MapToBitException(failure.Error);\n            default:\n                throw new InvalidOperationException();\n        }\n    }\n\n    /// <summary>\n    /// Update a member.\n    /// </summary>\n    /// <remarks>\n    /// Updates the specified member object. If a property is not provided,\n    /// the value of the existing property will be reset.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the member to be updated.</param>\n    /// <param name=\"model\">The request model.</param>\n    [HttpPut(\"{id}\")]\n    [ProducesResponseType(typeof(MemberResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Put(Guid id, [FromBody] MemberUpdateRequestModel model)\n    {\n        var existingUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (existingUser == null || existingUser.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        var existingUserType = existingUser.Type;\n        var updatedUser = model.ToOrganizationUser(existingUser);\n        var associations = model.Collections?.Select(c => c.ToCollectionAccessSelection()).ToList();\n        await _updateOrganizationUserCommand.UpdateUserAsync(updatedUser, existingUserType, null, associations, model.Groups);\n        MemberResponseModel response;\n        if (existingUser.UserId.HasValue)\n        {\n            var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);\n            response = new MemberResponseModel(existingUserDetails!,\n                await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails!), associations);\n        }\n        else\n        {\n            response = new MemberResponseModel(updatedUser, associations);\n        }\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// Update a member's groups.\n    /// </summary>\n    /// <remarks>\n    /// Updates the specified member's group associations.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the member to be updated.</param>\n    /// <param name=\"model\">The request model.</param>\n    [HttpPut(\"{id}/group-ids\")]\n    [ProducesResponseType((int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> PutGroupIds(Guid id, [FromBody] UpdateGroupIdsRequestModel model)\n    {\n        var existingUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (existingUser == null || existingUser.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(existingUser, model.GroupIds);\n        return new OkResult();\n    }\n\n    /// <summary>\n    /// Remove a member.\n    /// </summary>\n    /// <remarks>\n    /// Removes a member from the organization. This cannot be undone. The user account will still remain.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the member to be removed.</param>\n    [HttpDelete(\"{id}\")]\n    [ProducesResponseType((int)HttpStatusCode.OK)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Remove(Guid id)\n    {\n        var user = await _organizationUserRepository.GetByIdAsync(id);\n        if (user == null || user.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId!.Value, id, null);\n        return new OkResult();\n    }\n\n    /// <summary>\n    /// Re-invite a member.\n    /// </summary>\n    /// <remarks>\n    /// Re-sends the invitation email to an organization member.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the member to re-invite.</param>\n    [HttpPost(\"{id}/reinvite\")]\n    [ProducesResponseType((int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> PostReinvite(Guid id)\n    {\n        var existingUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (existingUser == null || existingUser.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        await _resendOrganizationInviteCommand.ResendInviteAsync(_currentContext.OrganizationId!.Value, null, id);\n        return new OkResult();\n    }\n\n    /// <summary>\n    /// Revoke a member's access to an organization.\n    /// </summary>\n    /// <param name=\"id\">The ID of the member to be revoked.</param>\n    [HttpPost(\"{id}/revoke\")]\n    [ProducesResponseType((int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Revoke(Guid id)\n    {\n        var organizationUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (organizationUser == null || organizationUser.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n\n        var request = new RevokeOrganizationUsersRequest(\n            _currentContext.OrganizationId!.Value,\n            [id],\n            new SystemUser(EventSystemUser.PublicApi)\n        );\n\n        var results = await _revokeOrganizationUserCommandV2.RevokeUsersAsync(request);\n        var result = results.Single();\n\n        return result.Result.Match<IActionResult>(\n            error => new BadRequestObjectResult(new ErrorResponseModel(error.Message)),\n            _ => new OkResult()\n        );\n    }\n\n    /// <summary>\n    /// Restore a member.\n    /// </summary>\n    /// <remarks>\n    /// Restores a previously revoked member of the organization.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the member to be restored.</param>\n    [HttpPost(\"{id}/restore\")]\n    [ProducesResponseType((int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Restore(Guid id)\n    {\n        var organizationUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (organizationUser == null || organizationUser.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n\n        await _restoreOrganizationUserCommand.RestoreUserAsync(organizationUser, EventSystemUser.PublicApi);\n\n        return new OkResult();\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Controllers/OrganizationController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Public.Controllers;\n\n[Route(\"public/organization\")]\n[Authorize(\"Organization\")]\npublic class OrganizationController : Controller\n{\n    private readonly IOrganizationService _organizationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IImportOrganizationUsersAndGroupsCommand _importOrganizationUsersAndGroupsCommand;\n    private readonly IFeatureService _featureService;\n\n    public OrganizationController(\n        IOrganizationService organizationService,\n        ICurrentContext currentContext,\n        GlobalSettings globalSettings,\n        IImportOrganizationUsersAndGroupsCommand importOrganizationUsersAndGroupsCommand,\n        IFeatureService featureService)\n    {\n        _organizationService = organizationService;\n        _currentContext = currentContext;\n        _globalSettings = globalSettings;\n        _importOrganizationUsersAndGroupsCommand = importOrganizationUsersAndGroupsCommand;\n        _featureService = featureService;\n    }\n\n    /// <summary>\n    /// Import members and groups.\n    /// </summary>\n    /// <remarks>\n    /// Import members and groups from an external system.\n    /// </remarks>\n    /// <param name=\"model\">The request model.</param>\n    [HttpPost(\"import\")]\n    [ProducesResponseType(typeof(OkResult), (int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    public async Task<IActionResult> Import([FromBody] OrganizationImportRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && !model.LargeImport &&\n            (model.Groups.Count() > 2000 || model.Members.Count(u => !u.Deleted) > 2000))\n        {\n            throw new BadRequestException(\"You cannot import this much data at once.\");\n        }\n\n        await _importOrganizationUsersAndGroupsCommand.ImportAsync(\n                _currentContext.OrganizationId.Value,\n                model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)),\n                model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()),\n                model.Members.Where(u => u.Deleted).Select(u => u.ExternalId),\n                model.OverwriteExisting.GetValueOrDefault()\n                );\n\n        return new OkResult();\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Controllers/PoliciesController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Api.AdminConsole.Public.Models.Response;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.AdminConsole.Public.Controllers;\n\n[Route(\"public/policies\")]\n[Authorize(\"Organization\")]\npublic class PoliciesController : Controller\n{\n    private readonly IPolicyRepository _policyRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;\n\n    public PoliciesController(\n        IPolicyRepository policyRepository,\n        ICurrentContext currentContext,\n        IVNextSavePolicyCommand vNextSavePolicyCommand)\n    {\n        _policyRepository = policyRepository;\n        _currentContext = currentContext;\n        _vNextSavePolicyCommand = vNextSavePolicyCommand;\n    }\n\n    /// <summary>\n    /// Retrieve a policy.\n    /// </summary>\n    /// <remarks>\n    /// Retrieves the details of a policy.\n    /// </remarks>\n    /// <param name=\"type\">The type of policy to be retrieved.</param>\n    [HttpGet(\"{type}\")]\n    [ProducesResponseType(typeof(PolicyResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Get(PolicyType type)\n    {\n        var policy = await _policyRepository.GetByOrganizationIdTypeAsync(_currentContext.OrganizationId.Value, type);\n        if (policy == null)\n        {\n            return new NotFoundResult();\n        }\n\n        return new JsonResult(new PolicyResponseModel(policy));\n    }\n\n    /// <summary>\n    /// List all policies.\n    /// </summary>\n    /// <remarks>\n    /// Returns a list of your organization's policies.\n    /// </remarks>\n    [HttpGet]\n    [ProducesResponseType(typeof(ListResponseModel<PolicyResponseModel>), (int)HttpStatusCode.OK)]\n    public async Task<IActionResult> List()\n    {\n        var policies = await _policyRepository.GetManyByOrganizationIdAsync(_currentContext.OrganizationId.Value);\n\n        return new JsonResult(new ListResponseModel<PolicyResponseModel>(policies.Select(p => new PolicyResponseModel(p))));\n    }\n\n    /// <summary>\n    /// Update a policy.\n    /// </summary>\n    /// <remarks>\n    /// Updates the specified policy. If a property is not provided,\n    /// the value of the existing property will be reset.\n    /// </remarks>\n    /// <param name=\"type\">The type of policy to be updated.</param>\n    /// <param name=\"model\">The request model.</param>\n    [HttpPut(\"{type}\")]\n    [ProducesResponseType(typeof(PolicyResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Put(PolicyType type, [FromBody] PolicyUpdateRequestModel model)\n    {\n        var savePolicyModel = model.ToSavePolicyModel(_currentContext.OrganizationId!.Value, type);\n        var policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyModel);\n\n        var response = new PolicyResponseModel(policy);\n        return new JsonResult(response);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/AssociationWithPermissionsBaseModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.AdminConsole.Public.Models;\n\npublic abstract class AssociationWithPermissionsBaseModel\n{\n    /// <summary>\n    /// The associated object's unique identifier.\n    /// </summary>\n    /// <example>bfbc8338-e329-4dc0-b0c9-317c2ebf1a09</example>\n    [Required]\n    public Guid? Id { get; set; }\n    /// <summary>\n    /// When true, the read only permission will not allow the user or group to make changes to items.\n    /// </summary>\n    [Required]\n    public bool? ReadOnly { get; set; }\n    /// <summary>\n    /// When true, the hide passwords permission will not allow the user or group to view passwords.\n    /// This prevents easy copy-and-paste of hidden items, however it may not completely prevent user access.\n    /// </summary>\n    public bool? HidePasswords { get; set; }\n    /// <summary>\n    /// When true, the manage permission allows a user to both edit the ciphers within a collection and edit the users/groups that are assigned to the collection.\n    /// This field will not affect behavior until your organization is using the latest collection enhancements (Releasing Q1, 2024)\n    /// </summary>\n    public bool? Manage { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/GroupBaseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.AdminConsole.Public.Models;\n\npublic abstract class GroupBaseModel\n{\n    /// <summary>\n    /// The name of the group.\n    /// </summary>\n    /// <example>Development Team</example>\n    [Required]\n    [StringLength(100)]\n    public string Name { get; set; }\n    /// <summary>\n    /// External identifier for reference or linking this group to another system, such as a user directory.\n    /// </summary>\n    /// <example>external_id_123456</example>\n    [StringLength(300)]\n    public string ExternalId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/MemberBaseModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Diagnostics.CodeAnalysis;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\n#nullable enable\n\nnamespace Bit.Api.AdminConsole.Public.Models;\n\npublic abstract class MemberBaseModel\n{\n    public MemberBaseModel() { }\n\n    public MemberBaseModel(OrganizationUser user)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        Type = user.Type;\n        ExternalId = user.ExternalId;\n\n        if (Type == OrganizationUserType.Custom)\n        {\n            Permissions = new PermissionsModel(user.GetPermissions());\n        }\n    }\n\n    [SetsRequiredMembers]\n    public MemberBaseModel(OrganizationUserUserDetails user)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        Type = user.Type;\n        ExternalId = user.ExternalId;\n\n        if (Type == OrganizationUserType.Custom)\n        {\n            Permissions = new PermissionsModel(user.GetPermissions());\n        }\n    }\n\n    /// <summary>\n    /// The member's type (or role) within the organization.\n    /// </summary>\n    [Required]\n    [EnumDataType(typeof(OrganizationUserType))]\n    public required OrganizationUserType? Type { get; set; }\n    /// <summary>\n    /// External identifier for reference or linking this member to another system, such as a user directory.\n    /// </summary>\n    /// <example>external_id_123456</example>\n    [StringLength(300)]\n    public string? ExternalId { get; set; }\n    /// <summary>\n    /// The member's custom permissions if the member has a Custom role. If not supplied, all custom permissions will\n    /// default to false.\n    /// </summary>\n    public PermissionsModel? Permissions { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/PermissionsModel.cs",
    "content": "﻿#nullable enable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.AdminConsole.Public.Models;\n\n/// <summary>\n/// Represents a member's custom permissions if the member has a Custom role.\n/// </summary>\npublic class PermissionsModel\n{\n    [JsonConstructor]\n    public PermissionsModel() { }\n    public PermissionsModel(Permissions? data)\n    {\n        if (data is null)\n        {\n            return;\n        }\n\n        AccessEventLogs = data.AccessEventLogs;\n        AccessImportExport = data.AccessImportExport;\n        AccessReports = data.AccessReports;\n        CreateNewCollections = data.CreateNewCollections;\n        EditAnyCollection = data.EditAnyCollection;\n        DeleteAnyCollection = data.DeleteAnyCollection;\n        ManageGroups = data.ManageGroups;\n        ManagePolicies = data.ManagePolicies;\n        ManageSso = data.ManageSso;\n        ManageUsers = data.ManageUsers;\n        ManageResetPassword = data.ManageResetPassword;\n        ManageScim = data.ManageScim;\n    }\n\n    public bool AccessEventLogs { get; set; }\n    public bool AccessImportExport { get; set; }\n    public bool AccessReports { get; set; }\n    public bool CreateNewCollections { get; set; }\n    public bool EditAnyCollection { get; set; }\n    public bool DeleteAnyCollection { get; set; }\n    public bool ManageGroups { get; set; }\n    public bool ManagePolicies { get; set; }\n    public bool ManageSso { get; set; }\n    public bool ManageUsers { get; set; }\n    public bool ManageResetPassword { get; set; }\n    public bool ManageScim { get; set; }\n\n    public Permissions ToData()\n    {\n        return new Permissions\n        {\n            AccessEventLogs = AccessEventLogs,\n            AccessImportExport = AccessImportExport,\n            AccessReports = AccessReports,\n            CreateNewCollections = CreateNewCollections,\n            EditAnyCollection = EditAnyCollection,\n            DeleteAnyCollection = DeleteAnyCollection,\n            ManageGroups = ManageGroups,\n            ManagePolicies = ManagePolicies,\n            ManageSso = ManageSso,\n            ManageUsers = ManageUsers,\n            ManageResetPassword = ManageResetPassword,\n            ManageScim = ManageScim\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/PolicyBaseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.AdminConsole.Public.Models;\n\npublic abstract class PolicyBaseModel\n{\n    /// <summary>\n    /// Determines if this policy is enabled and enforced.\n    /// </summary>\n    [Required]\n    public bool? Enabled { get; set; }\n    /// <summary>\n    /// Data for the policy.\n    /// </summary>\n    public Dictionary<string, object> Data { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Request/AssociationWithPermissionsRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Request;\n\npublic class AssociationWithPermissionsRequestModel : AssociationWithPermissionsBaseModel\n{\n    public CollectionAccessSelection ToCollectionAccessSelection()\n    {\n        var collectionAccessSelection = new CollectionAccessSelection\n        {\n            Id = Id.Value,\n            ReadOnly = ReadOnly.Value,\n            HidePasswords = HidePasswords.GetValueOrDefault(),\n            Manage = Manage.GetValueOrDefault()\n        };\n\n        return collectionAccessSelection;\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Request/GroupCreateUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Request;\n\npublic class GroupCreateUpdateRequestModel : GroupBaseModel\n{\n    /// <summary>\n    /// The associated collections that this group can access.\n    /// </summary>\n    public IEnumerable<AssociationWithPermissionsRequestModel> Collections { get; set; }\n\n    public Group ToGroup(Guid orgId)\n    {\n        return ToGroup(new Group\n        {\n            OrganizationId = orgId\n        });\n    }\n\n    public Group ToGroup(Group existingGroup)\n    {\n        existingGroup.Name = Name;\n        existingGroup.ExternalId = ExternalId;\n        return existingGroup;\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Request;\n\npublic class MemberCreateRequestModel : MemberUpdateRequestModel\n{\n    /// <summary>\n    /// The member's email address.\n    /// </summary>\n    /// <example>jsmith@example.com</example>\n    [Required]\n    [StringLength(256)]\n    [StrictEmailAddress]\n    public string Email { get; set; }\n\n    public override OrganizationUser ToOrganizationUser(OrganizationUser existingUser)\n    {\n        throw new NotImplementedException();\n    }\n\n    public OrganizationUserInvite ToOrganizationUserInvite()\n    {\n        var invite = new OrganizationUserInvite\n        {\n            Emails = new[] { Email },\n            Type = Type.Value,\n            Collections = Collections?.Select(c => c.ToCollectionAccessSelection())?.ToList() ?? [],\n            Groups = Groups\n        };\n\n        // Permissions property is optional for backwards compatibility with existing usage\n        if (Type is OrganizationUserType.Custom && Permissions is not null)\n        {\n            invite.Permissions = Permissions.ToData();\n        }\n\n        return invite;\n    }\n\n    public InviteOrganizationUsersRequest ToInviteRequest(\n        InviteOrganization inviteOrganization,\n        bool accessSecretsManager,\n        Guid performedBy,\n        DateTimeOffset performedAt)\n    {\n        // Permissions property is optional for backwards compatibility with existing usage\n        var permissions = (Type is OrganizationUserType.Custom && Permissions is not null)\n            ? Permissions.ToData()\n            : new Permissions();\n\n        return new InviteOrganizationUsersRequest(\n            invites:\n            [\n                new OrganizationUserInviteCommandModel(\n                    email: Email,\n                    assignedCollections: Collections?.Select(c => c.ToCollectionAccessSelection()) ?? [],\n                    groups: Groups ?? [],\n                    type: Type!.Value,\n                    permissions: permissions,\n                    externalId: ExternalId,\n                    accessSecretsManager: accessSecretsManager)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: performedBy,\n            performedAt: performedAt);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Request/MemberUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Request;\n\npublic class MemberUpdateRequestModel : MemberBaseModel, IValidatableObject\n{\n    /// <summary>\n    /// The associated collections that this member can access.\n    /// </summary>\n    public IEnumerable<AssociationWithPermissionsRequestModel> Collections { get; set; }\n\n    /// <summary>\n    /// Ids of the associated groups that this member will belong to\n    /// </summary>\n    public IEnumerable<Guid> Groups { get; set; }\n\n    public virtual OrganizationUser ToOrganizationUser(OrganizationUser existingUser)\n    {\n        existingUser.Type = Type.Value;\n        existingUser.ExternalId = ExternalId;\n\n        // Permissions property is optional for backwards compatibility with existing usage\n        if (existingUser.Type is OrganizationUserType.Custom && Permissions is not null)\n        {\n            existingUser.SetPermissions(Permissions.ToData());\n        }\n\n        return existingUser;\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (Type is not OrganizationUserType.Custom && Permissions is not null)\n        {\n            yield return new ValidationResult(\"Only users with the Custom role may use custom permissions.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Request/OrganizationImportRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Request;\n\npublic class OrganizationImportRequestModel\n{\n    /// <summary>\n    /// Groups to import.\n    /// </summary>\n    public OrganizationImportGroupRequestModel[] Groups { get; set; }\n    /// <summary>\n    /// Members to import.\n    /// </summary>\n    public OrganizationImportMemberRequestModel[] Members { get; set; }\n    /// <summary>\n    /// Determines if the data in this request should overwrite or append to the existing organization data.\n    /// </summary>\n    [Required]\n    public bool? OverwriteExisting { get; set; }\n    /// <summary>\n    /// Indicates an import of over 2000 users and/or groups is expected\n    /// </summary>\n    public bool LargeImport { get; set; } = false;\n\n    public class OrganizationImportGroupRequestModel\n    {\n        /// <summary>\n        /// The name of the group.\n        /// </summary>\n        /// <example>Development Team</example>\n        [Required]\n        [StringLength(100)]\n        public string Name { get; set; }\n        /// <summary>\n        /// External identifier for reference or linking this group to another system, such as a user directory.\n        /// </summary>\n        /// <example>external_id_123456</example>\n        [Required]\n        [StringLength(300)]\n        [JsonConverter(typeof(PermissiveStringConverter))]\n        public string ExternalId { get; set; }\n        /// <summary>\n        /// The associated external ids for members in this group.\n        /// </summary>\n        [JsonConverter(typeof(PermissiveStringEnumerableConverter))]\n        public IEnumerable<string> MemberExternalIds { get; set; }\n\n        public ImportedGroup ToImportedGroup(Guid organizationId)\n        {\n            var importedGroup = new ImportedGroup\n            {\n                Group = new Group\n                {\n                    OrganizationId = organizationId,\n                    Name = Name,\n                    ExternalId = ExternalId\n                },\n                ExternalUserIds = new HashSet<string>(MemberExternalIds)\n            };\n\n            return importedGroup;\n        }\n    }\n\n    public class OrganizationImportMemberRequestModel : IValidatableObject\n    {\n        /// <summary>\n        /// The member's email address. Required for non-deleted users.\n        /// </summary>\n        /// <example>jsmith@example.com</example>\n        [EmailAddress]\n        [StringLength(256)]\n        public string Email { get; set; }\n        /// <summary>\n        /// External identifier for reference or linking this member to another system, such as a user directory.\n        /// </summary>\n        /// <example>external_id_123456</example>\n        [Required]\n        [StringLength(300)]\n        [JsonConverter(typeof(PermissiveStringConverter))]\n        public string ExternalId { get; set; }\n        /// <summary>\n        /// Determines if this member should be removed from the organization during import.\n        /// </summary>\n        public bool Deleted { get; set; }\n\n        public ImportedOrganizationUser ToImportedOrganizationUser()\n        {\n            var importedUser = new ImportedOrganizationUser\n            {\n                Email = Email.ToLowerInvariant(),\n                ExternalId = ExternalId\n            };\n\n            return importedUser;\n        }\n\n        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n        {\n            if (string.IsNullOrWhiteSpace(Email) && !Deleted)\n            {\n                yield return new ValidationResult(\"Email is required for enabled members.\",\n                    new string[] { nameof(Email) });\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Request/PolicyUpdateRequestModel.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.Utilities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Request;\n\npublic class PolicyUpdateRequestModel : PolicyBaseModel\n{\n    public Dictionary<string, object>? Metadata { get; set; }\n\n    public PolicyUpdate ToPolicyUpdate(Guid organizationId, PolicyType type)\n    {\n        var serializedData = PolicyDataValidator.ValidateAndSerialize(Data, type);\n\n        return new()\n        {\n            Type = type,\n            OrganizationId = organizationId,\n            Data = serializedData,\n            Enabled = Enabled.GetValueOrDefault(),\n            PerformedBy = new SystemUser(EventSystemUser.PublicApi)\n        };\n    }\n\n    public SavePolicyModel ToSavePolicyModel(Guid organizationId, PolicyType type)\n    {\n        var serializedData = PolicyDataValidator.ValidateAndSerialize(Data, type);\n\n        var policyUpdate = new PolicyUpdate\n        {\n            Type = type,\n            OrganizationId = organizationId,\n            Data = serializedData,\n            Enabled = Enabled.GetValueOrDefault()\n        };\n\n        var performedBy = new SystemUser(EventSystemUser.PublicApi);\n        var metadata = PolicyDataValidator.ValidateAndDeserializeMetadata(Metadata, type);\n\n        return new SavePolicyModel(policyUpdate, performedBy, metadata);\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Request/UpdateGroupIdsRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Api.AdminConsole.Public.Models.Request;\n\npublic class UpdateGroupIdsRequestModel\n{\n    /// <summary>\n    /// The associated group ids that this object can access.\n    /// </summary>\n    public IEnumerable<Guid> GroupIds { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Request/UpdateMemberIdsRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Api.AdminConsole.Public.Models.Request;\n\npublic class UpdateMemberIdsRequestModel\n{\n    /// <summary>\n    /// The associated member ids that have access to this object.\n    /// </summary>\n    public IEnumerable<Guid> MemberIds { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Response/AssociationWithPermissionsResponseModel.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Response;\n\npublic class AssociationWithPermissionsResponseModel : AssociationWithPermissionsBaseModel\n{\n    [JsonConstructor]\n    public AssociationWithPermissionsResponseModel() : base()\n    {\n    }\n\n    public AssociationWithPermissionsResponseModel(CollectionAccessSelection selection)\n    {\n        if (selection == null)\n        {\n            throw new ArgumentNullException(nameof(selection));\n        }\n        Id = selection.Id;\n        ReadOnly = selection.ReadOnly;\n        HidePasswords = selection.HidePasswords;\n        Manage = selection.Manage;\n    }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Response/GroupResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Response;\n\n/// <summary>\n/// A user group.\n/// </summary>\npublic class GroupResponseModel : GroupBaseModel, IResponseModel\n{\n    [JsonConstructor]\n    public GroupResponseModel()\n    {\n\n    }\n\n    public GroupResponseModel(Group group, IEnumerable<CollectionAccessSelection> collections)\n    {\n        if (group == null)\n        {\n            throw new ArgumentNullException(nameof(group));\n        }\n\n        Id = group.Id;\n        Name = group.Name;\n        ExternalId = group.ExternalId;\n        Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c));\n    }\n\n    /// <summary>\n    /// String representing the object's type. Objects of the same type share the same properties.\n    /// </summary>\n    /// <example>group</example>\n    [Required]\n    public string Object => \"group\";\n    /// <summary>\n    /// The group's unique identifier.\n    /// </summary>\n    /// <example>539a36c5-e0d2-4cf9-979e-51ecf5cf6593</example>\n    [Required]\n    public Guid Id { get; set; }\n    /// <summary>\n    /// The associated collections that this group can access.\n    /// </summary>\n    public IEnumerable<AssociationWithPermissionsResponseModel> Collections { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Text.Json.Serialization;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Response;\n\n/// <summary>\n/// An organization member.\n/// </summary>\npublic class MemberResponseModel : MemberBaseModel, IResponseModel\n{\n    [JsonConstructor]\n    public MemberResponseModel() { }\n\n    [SetsRequiredMembers]\n    public MemberResponseModel(OrganizationUser user, IEnumerable<CollectionAccessSelection> collections) : base(user)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        Id = user.Id;\n        UserId = user.UserId;\n        Email = user.Email;\n        Status = user.Status;\n        Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c));\n        ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(user.ResetPasswordKey);\n    }\n\n    [SetsRequiredMembers]\n    public MemberResponseModel(OrganizationUserUserDetails user, bool twoFactorEnabled,\n        IEnumerable<CollectionAccessSelection> collections) : base(user)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        Id = user.Id;\n        UserId = user.UserId;\n        Name = user.Name;\n        Email = user.Email;\n        TwoFactorEnabled = twoFactorEnabled;\n        Status = user.Status;\n        Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c));\n        ResetPasswordEnrolled = OrganizationUser.IsValidResetPasswordKey(user.ResetPasswordKey);\n        SsoExternalId = user.SsoExternalId;\n    }\n\n    /// <summary>\n    /// String representing the object's type. Objects of the same type share the same properties.\n    /// </summary>\n    /// <example>member</example>\n    [Required]\n    public string Object => \"member\";\n    /// <summary>\n    /// The member's unique identifier within the organization.\n    /// </summary>\n    /// <example>539a36c5-e0d2-4cf9-979e-51ecf5cf6593</example>\n    [Required]\n    public Guid Id { get; set; }\n    /// <summary>\n    /// The member's unique identifier across Bitwarden.\n    /// </summary>\n    /// <example>48b47ee1-493e-4c67-aef7-014996c40eca</example>\n    [Required]\n    public Guid? UserId { get; set; }\n    /// <summary>\n    /// The member's name, set from their user account profile.\n    /// </summary>\n    /// <example>John Smith</example>\n    public string Name { get; set; }\n    /// <summary>\n    /// The member's email address.\n    /// </summary>\n    /// <example>jsmith@example.com</example>\n    [Required]\n    public string Email { get; set; }\n    /// <summary>\n    /// Returns <c>true</c> if the member has a two-step login method enabled on their user account.\n    /// </summary>\n    [Required]\n    public bool TwoFactorEnabled { get; set; }\n    /// <summary>\n    /// The member's status within the organization. All created members start with a status of \"Invited\".\n    /// Once a member accept's their invitation to join the organization, their status changes to \"Accepted\".\n    /// Accepted members are then \"Confirmed\" by an organization administrator. Once a member is \"Confirmed\",\n    /// their status can no longer change.\n    /// </summary>\n    [Required]\n    public OrganizationUserStatusType Status { get; set; }\n    /// <summary>\n    /// The associated collections that this member can access.\n    /// </summary>\n    public IEnumerable<AssociationWithPermissionsResponseModel> Collections { get; set; }\n\n    /// <summary>\n    /// Returns <c>true</c> if the member has enrolled in Password Reset assistance within the organization\n    /// </summary>\n    [Required]\n    public bool ResetPasswordEnrolled { get; }\n\n    /// <summary>\n    /// SSO external identifier for linking this member to an identity provider.\n    /// </summary>\n    /// <example>sso_external_id_123456</example>\n    public string SsoExternalId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/AdminConsole/Public/Models/Response/PolicyResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Newtonsoft.Json;\nusing JsonSerializer = System.Text.Json.JsonSerializer;\n\nnamespace Bit.Api.AdminConsole.Public.Models.Response;\n\n/// <summary>\n/// A policy.\n/// </summary>\npublic class PolicyResponseModel : PolicyBaseModel, IResponseModel\n{\n    [JsonConstructor]\n    public PolicyResponseModel() { }\n\n    public PolicyResponseModel(Policy policy)\n    {\n        if (policy == null)\n        {\n            throw new ArgumentNullException(nameof(policy));\n        }\n\n        Id = policy.Id;\n        Type = policy.Type;\n        Enabled = policy.Enabled;\n        if (!string.IsNullOrWhiteSpace(policy.Data))\n        {\n            Data = JsonSerializer.Deserialize<Dictionary<string, object>>(policy.Data);\n        }\n    }\n\n    /// <summary>\n    /// String representing the object's type. Objects of the same type share the same properties.\n    /// </summary>\n    /// <example>policy</example>\n    [Required]\n    public string Object => \"policy\";\n    /// <summary>\n    /// The policy's unique identifier.\n    /// </summary>\n    /// <example>539a36c5-e0d2-4cf9-979e-51ecf5cf6593</example>\n    [Required]\n    public Guid Id { get; set; }\n    /// <summary>\n    /// The type of policy.\n    /// </summary>\n    [Required]\n    public PolicyType? Type { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Api.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-Api</UserSecretsId>\n    <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>\n    <DocumentationFile>bin\\$(Configuration)\\$(TargetFramework)\\$(AssemblyName).xml</DocumentationFile>\n    <ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304;CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|AnyCPU'\">\n    <NoWarn>1701;1702;1591</NoWarn>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|AnyCPU'\">\n    <NoWarn>1701;1702;1591</NoWarn>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Api' \" />\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Api-SelfHost' \" />\n  <ItemGroup>\n    <ProjectReference Include=\"..\\SharedWeb\\SharedWeb.csproj\" />\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n  <Choose>\n    <When Condition=\"!$(DefineConstants.Contains('OSS'))\">\n      <ItemGroup>\n        <ProjectReference Include=\"..\\..\\bitwarden_license\\src\\Commercial.Core\\Commercial.Core.csproj\" />\n        <ProjectReference Include=\"..\\..\\bitwarden_license\\src\\Commercial.Infrastructure.EntityFramework\\Commercial.Infrastructure.EntityFramework.csproj\" />\n      </ItemGroup>\n    </When>\n  </Choose>\n\n  <ItemGroup>\n    <PackageReference Include=\"AspNetCore.HealthChecks.SqlServer\" Version=\"8.0.2\" />\n    <PackageReference Include=\"AspNetCore.HealthChecks.Uris\" Version=\"8.0.1\" />\n    <PackageReference Include=\"Azure.Messaging.EventGrid\" Version=\"5.0.0\" />\n    <PackageReference Include=\"Swashbuckle.AspNetCore\" Version=\"10.1.0\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Api/Auth/Controllers/AccountsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Response;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Models.Request.Accounts;\nusing Bit.Api.Models.Response;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Kdf;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Models.Api.Response;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Auth.Controllers;\n\n[Route(\"accounts\")]\n[Authorize(Policies.Application)]\npublic class AccountsController : Controller\n{\n    private readonly IOrganizationService _organizationService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IUserService _userService;\n    private readonly IPolicyService _policyService;\n    private readonly ISetInitialMasterPasswordCommandV1 _setInitialMasterPasswordCommandV1;\n    private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;\n    private readonly ITdeSetPasswordCommand _tdeSetPasswordCommand;\n    private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IFeatureService _featureService;\n    private readonly IUserAccountKeysQuery _userAccountKeysQuery;\n    private readonly ITwoFactorEmailService _twoFactorEmailService;\n    private readonly IChangeKdfCommand _changeKdfCommand;\n    private readonly IUserRepository _userRepository;\n\n    public AccountsController(\n        IOrganizationService organizationService,\n        IOrganizationUserRepository organizationUserRepository,\n        IProviderUserRepository providerUserRepository,\n        IUserService userService,\n        IPolicyService policyService,\n        ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,\n        ISetInitialMasterPasswordCommandV1 setInitialMasterPasswordCommandV1,\n        ITdeSetPasswordCommand tdeSetPasswordCommand,\n        ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IFeatureService featureService,\n        IUserAccountKeysQuery userAccountKeysQuery,\n        ITwoFactorEmailService twoFactorEmailService,\n        IChangeKdfCommand changeKdfCommand,\n        IUserRepository userRepository\n        )\n    {\n        _organizationService = organizationService;\n        _organizationUserRepository = organizationUserRepository;\n        _providerUserRepository = providerUserRepository;\n        _userService = userService;\n        _policyService = policyService;\n        _setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;\n        _setInitialMasterPasswordCommandV1 = setInitialMasterPasswordCommandV1;\n        _tdeSetPasswordCommand = tdeSetPasswordCommand;\n        _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _featureService = featureService;\n        _userAccountKeysQuery = userAccountKeysQuery;\n        _twoFactorEmailService = twoFactorEmailService;\n        _changeKdfCommand = changeKdfCommand;\n        _userRepository = userRepository;\n    }\n\n\n    [HttpPost(\"password-hint\")]\n    [AllowAnonymous]\n    public async Task PostPasswordHint([FromBody] PasswordHintRequestModel model)\n    {\n        await _userService.SendMasterPasswordHintAsync(model.Email);\n    }\n\n    [HttpPost(\"email-token\")]\n    public async Task PostEmailToken([FromBody] EmailTokenRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (user.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"You cannot change your email when using Key Connector.\");\n        }\n\n        if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"MasterPasswordHash\", \"Invalid password.\");\n        }\n\n        var claimedUserValidationResult = await _userService.ValidateClaimedUserDomainAsync(user, model.NewEmail);\n\n        if (!claimedUserValidationResult.Succeeded)\n        {\n            throw new BadRequestException(claimedUserValidationResult.Errors);\n        }\n\n        await _userService.InitiateEmailChangeAsync(user, model.NewEmail);\n    }\n\n    [HttpPost(\"email\")]\n    public async Task PostEmail([FromBody] EmailRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (user.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"You cannot change your email when using Key Connector.\");\n        }\n\n        var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,\n            model.NewMasterPasswordHash, model.Token, model.Key);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        await Task.Delay(2000);\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"verify-email\")]\n    public async Task PostVerifyEmail()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        await _userService.SendEmailVerificationAsync(user);\n    }\n\n    [HttpPost(\"verify-email-token\")]\n    [AllowAnonymous]\n    public async Task PostVerifyEmailToken([FromBody] VerifyEmailRequestModel model)\n    {\n        var user = await _userService.GetUserByIdAsync(new Guid(model.UserId));\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n        var result = await _userService.ConfirmEmailAsync(user, model.Token);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        await Task.Delay(2000);\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"password\")]\n    public async Task PostPassword([FromBody] PasswordRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await _userService.ChangePasswordAsync(user, model.MasterPasswordHash,\n            model.NewMasterPasswordHash, model.MasterPasswordHint, model.Key);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        await Task.Delay(2000);\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"set-password\")]\n    public async Task PostSetPasswordAsync([FromBody] SetInitialPasswordRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (model.IsV2Request())\n        {\n            if (model.IsTdeSetPasswordRequest())\n            {\n                await _tdeSetPasswordCommand.SetMasterPasswordAsync(user, model.ToData());\n            }\n            else\n            {\n                await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(user, model.ToData());\n            }\n        }\n        else\n        {\n            // TODO removed with https://bitwarden.atlassian.net/browse/PM-27327\n            try\n            {\n                user = model.ToUser(user);\n            }\n            catch (Exception e)\n            {\n                ModelState.AddModelError(string.Empty, e.Message);\n                throw new BadRequestException(ModelState);\n            }\n\n            var result = await _setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(\n                user,\n                model.MasterPasswordHash,\n                model.Key,\n                model.OrgIdentifier);\n\n            if (result.Succeeded)\n            {\n                return;\n            }\n\n            foreach (var error in result.Errors)\n            {\n                ModelState.AddModelError(string.Empty, error.Description);\n            }\n\n            throw new BadRequestException(ModelState);\n        }\n    }\n\n    [HttpPost(\"verify-password\")]\n    public async Task<MasterPasswordPolicyResponseModel> PostVerifyPassword([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))\n        {\n            var policyData = await _policyService.GetMasterPasswordPolicyForUserAsync(user);\n\n            return new MasterPasswordPolicyResponseModel(policyData);\n        }\n\n        ModelState.AddModelError(nameof(model.MasterPasswordHash), \"Invalid password.\");\n        await Task.Delay(2000);\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"kdf\")]\n    public async Task PostKdf([FromBody] PasswordRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (model.AuthenticationData == null || model.UnlockData == null)\n        {\n            throw new BadRequestException(\"AuthenticationData and UnlockData must be provided.\");\n        }\n\n        var result = await _changeKdfCommand.ChangeKdfAsync(user, model.MasterPasswordHash, model.AuthenticationData.ToData(), model.UnlockData.ToData());\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        await Task.Delay(2000);\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"security-stamp\")]\n    public async Task PostSecurityStamp([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await _userService.RefreshSecurityStampAsync(user, model.Secret);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        await Task.Delay(2000);\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpGet(\"profile\")]\n    public async Task<ProfileResponseModel> GetProfile()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id,\n            OrganizationUserStatusType.Confirmed);\n        var providerUserDetails = await _providerUserRepository.GetManyDetailsByUserAsync(user.Id,\n            ProviderUserStatusType.Confirmed);\n        var providerUserOrganizationDetails =\n            await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,\n                ProviderUserStatusType.Confirmed);\n\n        var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);\n        var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);\n        var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);\n\n        var accountKeys = await _userAccountKeysQuery.Run(user);\n\n        var response = new ProfileResponseModel(user, accountKeys, organizationUserDetails, providerUserDetails,\n            providerUserOrganizationDetails, twoFactorEnabled,\n            hasPremiumFromOrg, organizationIdsClaimingActiveUser);\n        return response;\n    }\n\n    [HttpGet(\"organizations\")]\n    public async Task<ListResponseModel<ProfileOrganizationResponseModel>> GetOrganizations()\n    {\n        var userId = _userService.GetProperUserId(User);\n        var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(userId.Value,\n            OrganizationUserStatusType.Confirmed);\n        var organizationIdsClaimingUser = await GetOrganizationIdsClaimingUserAsync(userId.Value);\n\n        var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsClaimingUser));\n        return new ListResponseModel<ProfileOrganizationResponseModel>(responseData);\n    }\n\n    [HttpPut(\"profile\")]\n    public async Task<ProfileResponseModel> PutProfile([FromBody] UpdateProfileRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        await _userService.SaveUserAsync(model.ToUser(user));\n\n        var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);\n        var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);\n        var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);\n        var userAccountKeys = await _userAccountKeysQuery.Run(user);\n\n        var response = new ProfileResponseModel(user, userAccountKeys, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);\n        return response;\n    }\n\n    [HttpPost(\"profile\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /profile instead.\")]\n    public async Task<ProfileResponseModel> PostProfile([FromBody] UpdateProfileRequestModel model)\n    {\n        return await PutProfile(model);\n    }\n\n    [HttpPut(\"avatar\")]\n    public async Task<ProfileResponseModel> PutAvatar([FromBody] UpdateAvatarRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n        await _userService.SaveUserAsync(model.ToUser(user), true);\n\n        var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);\n        var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);\n        var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);\n        var accountKeys = await _userAccountKeysQuery.Run(user);\n\n        var response = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);\n        return response;\n    }\n\n    [HttpPost(\"avatar\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /avatar instead.\")]\n    public async Task<ProfileResponseModel> PostAvatar([FromBody] UpdateAvatarRequestModel model)\n    {\n        return await PutAvatar(model);\n    }\n\n    [HttpGet(\"revision-date\")]\n    public async Task<long?> GetAccountRevisionDate()\n    {\n        var userId = _userService.GetProperUserId(User);\n        long? revisionDate = null;\n        if (userId.HasValue)\n        {\n            var date = await _userService.GetAccountRevisionDateByIdAsync(userId.Value);\n            revisionDate = CoreHelpers.ToEpocMilliseconds(date);\n        }\n\n        return revisionDate;\n    }\n\n    [HttpPost(\"keys\")]\n    public async Task<KeysResponseModel> PostKeys([FromBody] KeysRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!string.IsNullOrWhiteSpace(user.PrivateKey) || !string.IsNullOrWhiteSpace(user.PublicKey))\n        {\n            throw new BadRequestException(\"User has existing keypair\");\n        }\n\n        if (model.AccountKeys != null)\n        {\n            var accountKeysData = model.AccountKeys.ToAccountKeysData();\n            if (!accountKeysData.IsV2Encryption())\n            {\n                throw new BadRequestException(\"AccountKeys are only supported for V2 encryption.\");\n            }\n            await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, accountKeysData);\n            return new KeysResponseModel(accountKeysData, user.Key);\n        }\n        else\n        {\n            // Todo: Drop this after a transition period. This will drop no-account-keys requests.\n            // The V1 check in the other branch should persist\n            // https://bitwarden.atlassian.net/browse/PM-27329\n            await _userService.SaveUserAsync(model.ToUser(user));\n            return new KeysResponseModel(new UserAccountKeysData\n            {\n                PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(\n                    user.PrivateKey,\n                    user.PublicKey\n                )\n            }, user.Key);\n        }\n\n    }\n\n    [HttpGet(\"keys\")]\n    public async Task<KeysResponseModel> GetKeys()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var accountKeys = await _userAccountKeysQuery.Run(user);\n        return new KeysResponseModel(accountKeys, user.Key);\n    }\n\n    [HttpDelete]\n    public async Task Delete([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!await _userService.VerifySecretAsync(user, model.Secret))\n        {\n            ModelState.AddModelError(string.Empty, \"User verification failed.\");\n            await Task.Delay(2000);\n        }\n        else\n        {\n            // Check if the user is claimed by any organization.\n            if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))\n            {\n                throw new BadRequestException(\"Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.\");\n            }\n\n            var result = await _userService.DeleteAsync(user);\n            if (result.Succeeded)\n            {\n                return;\n            }\n\n            foreach (var error in result.Errors)\n            {\n                ModelState.AddModelError(string.Empty, error.Description);\n            }\n        }\n\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE / instead.\")]\n    public async Task PostDelete([FromBody] SecretVerificationRequestModel model)\n    {\n        await Delete(model);\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"delete-recover\")]\n    public async Task PostDeleteRecover([FromBody] DeleteRecoverRequestModel model)\n    {\n        await _userService.SendDeleteConfirmationAsync(model.Email);\n    }\n\n    [HttpPost(\"delete-recover-token\")]\n    [AllowAnonymous]\n    public async Task PostDeleteRecoverToken([FromBody] VerifyDeleteRecoverRequestModel model)\n    {\n        var user = await _userService.GetUserByIdAsync(new Guid(model.UserId));\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await _userService.DeleteAsync(user, model.Token);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        await Task.Delay(2000);\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpDelete(\"sso/{organizationId}\")]\n    public async Task DeleteSsoUser(string organizationId)\n    {\n        var userId = _userService.GetProperUserId(User);\n        if (!userId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        await _organizationService.DeleteSsoUserAsync(userId.Value, new Guid(organizationId));\n    }\n\n    [HttpGet(\"sso/user-identifier\")]\n    public async Task<string> GetSsoUserIdentifier()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var token = await _userService.GenerateSignInTokenAsync(user, TokenPurposes.LinkSso);\n        var userIdentifier = $\"{user.Id},{token}\";\n        return userIdentifier;\n    }\n\n    [HttpPost(\"api-key\")]\n    public async Task<ApiKeyResponseModel> ApiKey([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!await _userService.VerifySecretAsync(user, model.Secret))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(string.Empty, \"User verification failed.\");\n        }\n\n        return new ApiKeyResponseModel(user);\n    }\n\n    [HttpPost(\"rotate-api-key\")]\n    public async Task<ApiKeyResponseModel> RotateApiKey([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!await _userService.VerifySecretAsync(user, model.Secret))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(string.Empty, \"User verification failed.\");\n        }\n\n        await _userService.RotateApiKeyAsync(user);\n        var response = new ApiKeyResponseModel(user);\n        return response;\n    }\n\n    [HttpPut(\"update-temp-password\")]\n    public async Task PutUpdateTempPasswordAsync([FromBody] UpdateTempPasswordRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await _userService.UpdateTempPasswordAsync(user, model.NewMasterPasswordHash, model.Key, model.MasterPasswordHint);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPut(\"update-tde-offboarding-password\")]\n    public async Task PutUpdateTdePasswordAsync([FromBody] UpdateTdeOffboardingPasswordRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await _tdeOffboardingPasswordCommand.UpdateTdeOffboardingPasswordAsync(user, model.NewMasterPasswordHash, model.Key, model.MasterPasswordHint);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"request-otp\")]\n    public async Task PostRequestOTP()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        await _userService.SendOTPAsync(user);\n    }\n\n    [HttpPost(\"verify-otp\")]\n    public async Task VerifyOTP([FromBody] VerifyOTPRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        if (!await _userService.VerifyOTPAsync(user, model.OTP))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"Token\", \"Invalid token\");\n        }\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"resend-new-device-otp\")]\n    public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();\n        if (!await _userService.VerifySecretAsync(user, request.Secret))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(string.Empty, \"User verification failed.\");\n        }\n\n        await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user);\n    }\n\n    [HttpPut(\"verify-devices\")]\n    public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();\n\n        if (!await _userService.VerifySecretAsync(user, request.Secret))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(string.Empty, \"User verification failed.\");\n        }\n        user.VerifyDevices = request.VerifyDevices;\n\n        await _userService.SaveUserAsync(user);\n    }\n\n    [HttpPost(\"verify-devices\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /verify-devices instead.\")]\n    public async Task PostSetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)\n    {\n        await SetUserVerifyDevicesAsync(request);\n    }\n\n    private async Task<IEnumerable<Guid>> GetOrganizationIdsClaimingUserAsync(Guid userId)\n    {\n        var organizationsClaimingUser = await _userService.GetOrganizationsClaimingUserAsync(userId);\n        return organizationsClaimingUser.Select(o => o.Id);\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Controllers/AuthRequestsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Auth.Models.Response;\nusing Bit.Api.Models.Response;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.Models.Api.Request.AuthRequest;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Auth.Controllers;\n\n[Route(\"auth-requests\")]\n[Authorize(Policies.Application)]\npublic class AuthRequestsController(\n    IUserService userService,\n    IAuthRequestRepository authRequestRepository,\n    IGlobalSettings globalSettings,\n    IAuthRequestService authRequestService) : Controller\n{\n    private readonly IUserService _userService = userService;\n    private readonly IAuthRequestRepository _authRequestRepository = authRequestRepository;\n    private readonly IGlobalSettings _globalSettings = globalSettings;\n    private readonly IAuthRequestService _authRequestService = authRequestService;\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<AuthRequestResponseModel>> GetAll()\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var authRequests = await _authRequestRepository.GetManyByUserIdAsync(userId);\n        var responses = authRequests.Select(a => new AuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault));\n        return new ListResponseModel<AuthRequestResponseModel>(responses);\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<AuthRequestResponseModel> Get(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var authRequest = await _authRequestService.GetAuthRequestAsync(id, userId);\n\n        if (authRequest == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);\n    }\n\n    [HttpGet(\"pending\")]\n    public async Task<ListResponseModel<PendingAuthRequestResponseModel>> GetPendingAuthRequestsAsync()\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var rawResponse = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId);\n        var responses = rawResponse.Select(a => new PendingAuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault));\n        return new ListResponseModel<PendingAuthRequestResponseModel>(responses);\n    }\n\n    [HttpGet(\"{id}/response\")]\n    [AllowAnonymous]\n    public async Task<AuthRequestResponseModel> GetResponse(Guid id, [FromQuery] string code)\n    {\n        var authRequest = await _authRequestService.GetValidatedAuthRequestAsync(id, code);\n\n        if (authRequest == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);\n    }\n\n    [HttpPost(\"\")]\n    [AllowAnonymous]\n    public async Task<AuthRequestResponseModel> Post([FromBody] AuthRequestCreateRequestModel model)\n    {\n        if (model.Type == AuthRequestType.AdminApproval)\n        {\n            throw new BadRequestException(\"You must be authenticated to create a request of that type.\");\n        }\n        var authRequest = await _authRequestService.CreateAuthRequestAsync(model);\n        var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);\n        return r;\n    }\n\n    [HttpPost(\"admin-request\")]\n    public async Task<AuthRequestResponseModel> PostAdminRequest([FromBody] AuthRequestCreateRequestModel model)\n    {\n        var authRequest = await _authRequestService.CreateAuthRequestAsync(model);\n        var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);\n        return r;\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task<AuthRequestResponseModel> Put(Guid id, [FromBody] AuthRequestUpdateRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n\n        // If the Approving Device is attempting to approve a request, validate the approval\n        if (model.RequestApproved == true)\n        {\n            await ValidateApprovalOfMostRecentAuthRequest(id, userId);\n        }\n\n        var authRequest = await _authRequestService.UpdateAuthRequestAsync(id, userId, model);\n        return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);\n    }\n\n    private async Task ValidateApprovalOfMostRecentAuthRequest(Guid id, Guid userId)\n    {\n        // Get the current auth request to find the device identifier\n        var currentAuthRequest = await _authRequestService.GetAuthRequestAsync(id, userId);\n        if (currentAuthRequest == null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Get all pending auth requests for this user (returns most recent per device)\n        var pendingRequests = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId);\n\n        // Find the most recent request for the same device\n        var mostRecentForDevice = pendingRequests\n            .FirstOrDefault(pendingRequest => pendingRequest.RequestDeviceIdentifier == currentAuthRequest.RequestDeviceIdentifier);\n\n        var isMostRecentRequestForDevice = mostRecentForDevice?.Id == id;\n        if (!isMostRecentRequestForDevice)\n        {\n            throw new BadRequestException(\"This request is no longer valid. Make sure to approve the most recent request.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Controllers/EmergencyAccessController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Auth.Models.Request;\nusing Bit.Api.Auth.Models.Response;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Auth.Controllers;\n\n[Route(\"emergency-access\")]\n[Authorize(Core.Auth.Identity.Policies.Application)]\npublic class EmergencyAccessController : Controller\n{\n    private readonly IUserService _userService;\n    private readonly IEmergencyAccessRepository _emergencyAccessRepository;\n    private readonly IEmergencyAccessService _emergencyAccessService;\n    private readonly IGlobalSettings _globalSettings;\n\n    public EmergencyAccessController(\n        IUserService userService,\n        IEmergencyAccessRepository emergencyAccessRepository,\n        IEmergencyAccessService emergencyAccessService,\n        IGlobalSettings globalSettings)\n    {\n        _userService = userService;\n        _emergencyAccessRepository = emergencyAccessRepository;\n        _emergencyAccessService = emergencyAccessService;\n        _globalSettings = globalSettings;\n    }\n\n    [HttpGet(\"trusted\")]\n    public async Task<ListResponseModel<EmergencyAccessGranteeDetailsResponseModel>> GetContacts()\n    {\n        var userId = _userService.GetProperUserId(User);\n        var granteeDetails = await _emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(userId.Value);\n\n        var responses = granteeDetails.Select(d =>\n            new EmergencyAccessGranteeDetailsResponseModel(d));\n\n        return new ListResponseModel<EmergencyAccessGranteeDetailsResponseModel>(responses);\n    }\n\n    [HttpGet(\"granted\")]\n    public async Task<ListResponseModel<EmergencyAccessGrantorDetailsResponseModel>> GetGrantees()\n    {\n        var userId = _userService.GetProperUserId(User);\n        var granteeDetails = await _emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(userId.Value);\n\n        var responses = granteeDetails.Select(d => new EmergencyAccessGrantorDetailsResponseModel(d));\n\n        return new ListResponseModel<EmergencyAccessGrantorDetailsResponseModel>(responses);\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<EmergencyAccessGranteeDetailsResponseModel> Get(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User);\n        var result = await _emergencyAccessService.GetAsync(id, userId.Value);\n        return new EmergencyAccessGranteeDetailsResponseModel(result);\n    }\n\n    [HttpGet(\"{id}/policies\")]\n    public async Task<ListResponseModel<PolicyResponseModel>> Policies(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var policies = await _emergencyAccessService.GetPoliciesAsync(id, user);\n        var responses = policies?.Select(policy => new PolicyResponseModel(policy));\n        return new ListResponseModel<PolicyResponseModel>(responses);\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task Put(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);\n        if (emergencyAccess == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        await _emergencyAccessService.SaveAsync(model.ToEmergencyAccess(emergencyAccess), user);\n    }\n\n    [HttpPost(\"{id}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /{id} instead.\")]\n    public async Task Post(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model)\n    {\n        await Put(id, model);\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task Delete(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User);\n        await _emergencyAccessService.DeleteAsync(id, userId.Value);\n    }\n\n    [HttpPost(\"{id}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE /{id} instead.\")]\n    public async Task PostDelete(Guid id)\n    {\n        await Delete(id);\n    }\n\n    [HttpPost(\"invite\")]\n    public async Task Invite([FromBody] EmergencyAccessInviteRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        await _emergencyAccessService.InviteAsync(user, model.Email, model.Type.Value, model.WaitTimeDays);\n    }\n\n    [HttpPost(\"{id}/reinvite\")]\n    public async Task Reinvite(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        await _emergencyAccessService.ResendInviteAsync(user, id);\n    }\n\n    [HttpPost(\"{id}/accept\")]\n    public async Task Accept(Guid id, [FromBody] OrganizationUserAcceptRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        await _emergencyAccessService.AcceptUserAsync(id, user, model.Token, _userService);\n    }\n\n    [HttpPost(\"{id}/confirm\")]\n    public async Task Confirm(Guid id, [FromBody] OrganizationUserConfirmRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User);\n        await _emergencyAccessService.ConfirmUserAsync(id, model.Key, userId.Value);\n    }\n\n    [HttpPost(\"{id}/initiate\")]\n    public async Task Initiate(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        await _emergencyAccessService.InitiateAsync(id, user);\n    }\n\n    [HttpPost(\"{id}/approve\")]\n    public async Task Approve(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        await _emergencyAccessService.ApproveAsync(id, user);\n    }\n\n    [HttpPost(\"{id}/reject\")]\n    public async Task Reject(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        await _emergencyAccessService.RejectAsync(id, user);\n    }\n\n    [HttpPost(\"{id}/takeover\")]\n    public async Task<EmergencyAccessTakeoverResponseModel> Takeover(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var (result, grantor) = await _emergencyAccessService.TakeoverAsync(id, user);\n        return new EmergencyAccessTakeoverResponseModel(result, grantor);\n    }\n\n    [HttpPost(\"{id}/password\")]\n    public async Task Password(Guid id, [FromBody] EmergencyAccessPasswordRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        await _emergencyAccessService.PasswordAsync(id, user, model.NewMasterPasswordHash, model.Key);\n    }\n\n    [HttpPost(\"{id}/view\")]\n    public async Task<EmergencyAccessViewResponseModel> ViewCiphers(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var viewResult = await _emergencyAccessService.ViewAsync(id, user);\n        return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers, user);\n    }\n\n    [HttpGet(\"{id}/{cipherId}/attachment/{attachmentId}\")]\n    public async Task<AttachmentResponseModel> GetAttachmentData(Guid id, Guid cipherId, string attachmentId)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var result =\n            await _emergencyAccessService.GetAttachmentDownloadAsync(id, cipherId, attachmentId, user);\n        return new AttachmentResponseModel(result);\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Controllers/TwoFactorController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Auth.Models.Request;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Auth.Models.Response.TwoFactor;\nusing Bit.Api.Models.Request;\nusing Bit.Api.Models.Response;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Fido2NetLib;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Auth.Controllers;\n\n[Route(\"two-factor\")]\n[Authorize(Policies.Web)]\npublic class TwoFactorController : Controller\n{\n    private readonly IUserService _userService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationService _organizationService;\n    private readonly UserManager<User> _userManager;\n    private readonly ICurrentContext _currentContext;\n    private readonly IAuthRequestRepository _authRequestRepository;\n    private readonly IDuoUniversalTokenService _duoUniversalTokenService;\n    private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;\n    private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;\n    private readonly ITwoFactorEmailService _twoFactorEmailService;\n    private readonly IStartTwoFactorWebAuthnRegistrationCommand _startTwoFactorWebAuthnRegistrationCommand;\n    private readonly ICompleteTwoFactorWebAuthnRegistrationCommand _completeTwoFactorWebAuthnRegistrationCommand;\n    private readonly IDeleteTwoFactorWebAuthnCredentialCommand _deleteTwoFactorWebAuthnCredentialCommand;\n\n    public TwoFactorController(\n        IUserService userService,\n        IOrganizationRepository organizationRepository,\n        IOrganizationService organizationService,\n        UserManager<User> userManager,\n        ICurrentContext currentContext,\n        IAuthRequestRepository authRequestRepository,\n        IDuoUniversalTokenService duoUniversalConfigService,\n        IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,\n        IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector,\n        ITwoFactorEmailService twoFactorEmailService,\n        IStartTwoFactorWebAuthnRegistrationCommand startTwoFactorWebAuthnRegistrationCommand,\n        ICompleteTwoFactorWebAuthnRegistrationCommand completeTwoFactorWebAuthnRegistrationCommand,\n        IDeleteTwoFactorWebAuthnCredentialCommand deleteTwoFactorWebAuthnCredentialCommand)\n    {\n        _userService = userService;\n        _organizationRepository = organizationRepository;\n        _organizationService = organizationService;\n        _userManager = userManager;\n        _currentContext = currentContext;\n        _authRequestRepository = authRequestRepository;\n        _duoUniversalTokenService = duoUniversalConfigService;\n        _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;\n        _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;\n        _twoFactorEmailService = twoFactorEmailService;\n        _startTwoFactorWebAuthnRegistrationCommand = startTwoFactorWebAuthnRegistrationCommand;\n        _completeTwoFactorWebAuthnRegistrationCommand = completeTwoFactorWebAuthnRegistrationCommand;\n        _deleteTwoFactorWebAuthnCredentialCommand = deleteTwoFactorWebAuthnCredentialCommand;\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<TwoFactorProviderResponseModel>> Get()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var providers = user.GetTwoFactorProviders()?.Select(\n            p => new TwoFactorProviderResponseModel(p.Key, p.Value));\n        return new ListResponseModel<TwoFactorProviderResponseModel>(providers);\n    }\n\n    [HttpGet(\"~/organizations/{id}/two-factor\")]\n    public async Task<ListResponseModel<TwoFactorProviderResponseModel>> GetOrganization(string id)\n    {\n        var orgIdGuid = new Guid(id);\n        if (!await _currentContext.OrganizationAdmin(orgIdGuid))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var providers = organization.GetTwoFactorProviders()?.Select(\n            p => new TwoFactorProviderResponseModel(p.Key, p.Value));\n        return new ListResponseModel<TwoFactorProviderResponseModel>(providers);\n    }\n\n    [HttpPost(\"get-authenticator\")]\n    public async Task<TwoFactorAuthenticatorResponseModel> GetAuthenticator(\n        [FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await CheckAsync(model, false);\n        var response = new TwoFactorAuthenticatorResponseModel(user);\n        var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key);\n        response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable);\n        return response;\n    }\n\n    [HttpPut(\"authenticator\")]\n    public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(\n        [FromBody] UpdateTwoFactorAuthenticatorRequestModel model)\n    {\n        var user = model.ToUser(await _userService.GetUserByPrincipalAsync(User));\n        _twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken);\n        if (!decryptedToken.TokenIsValid(user, model.Key))\n        {\n            throw new BadRequestException(\"UserVerificationToken\", \"User verification failed.\");\n        }\n\n        if (!await _userManager.VerifyTwoFactorTokenAsync(user,\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator), model.Token))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"Token\", \"Invalid token.\");\n        }\n\n        await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Authenticator);\n        var response = new TwoFactorAuthenticatorResponseModel(user);\n        return response;\n    }\n\n    [HttpPost(\"authenticator\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /authenticator instead.\")]\n    public async Task<TwoFactorAuthenticatorResponseModel> PostAuthenticator(\n        [FromBody] UpdateTwoFactorAuthenticatorRequestModel model)\n    {\n        return await PutAuthenticator(model);\n    }\n\n    [HttpDelete(\"authenticator\")]\n    public async Task<TwoFactorProviderResponseModel> DisableAuthenticator(\n    [FromBody] TwoFactorAuthenticatorDisableRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        _twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken);\n        if (!decryptedToken.TokenIsValid(user, model.Key))\n        {\n            throw new BadRequestException(\"UserVerificationToken\", \"User verification failed.\");\n        }\n\n        await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value);\n        return new TwoFactorProviderResponseModel(model.Type.Value, user);\n    }\n\n    [HttpPost(\"get-yubikey\")]\n    public async Task<TwoFactorYubiKeyResponseModel> GetYubiKey([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await CheckAsync(model, true, true);\n        var response = new TwoFactorYubiKeyResponseModel(user);\n        return response;\n    }\n\n    [HttpPut(\"yubikey\")]\n    public async Task<TwoFactorYubiKeyResponseModel> PutYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model)\n    {\n        var user = await CheckAsync(model, true);\n        model.ToUser(user);\n\n        await ValidateYubiKeyAsync(user, nameof(model.Key1), model.Key1);\n        await ValidateYubiKeyAsync(user, nameof(model.Key2), model.Key2);\n        await ValidateYubiKeyAsync(user, nameof(model.Key3), model.Key3);\n        await ValidateYubiKeyAsync(user, nameof(model.Key4), model.Key4);\n        await ValidateYubiKeyAsync(user, nameof(model.Key5), model.Key5);\n\n        await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.YubiKey);\n        var response = new TwoFactorYubiKeyResponseModel(user);\n        return response;\n    }\n\n    [HttpPost(\"yubikey\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /yubikey instead.\")]\n    public async Task<TwoFactorYubiKeyResponseModel> PostYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model)\n    {\n        return await PutYubiKey(model);\n    }\n\n    [HttpPost(\"get-duo\")]\n    public async Task<TwoFactorDuoResponseModel> GetDuo([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await CheckAsync(model, true, true);\n        var response = new TwoFactorDuoResponseModel(user);\n        return response;\n    }\n\n    [HttpPut(\"duo\")]\n    public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model)\n    {\n        var user = await CheckAsync(model, true);\n        if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientSecret, model.ClientId, model.Host))\n        {\n            throw new BadRequestException(\n                \"Duo configuration settings are not valid. Please re-check the Duo Admin panel.\");\n        }\n\n        model.ToUser(user);\n        await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Duo);\n        var response = new TwoFactorDuoResponseModel(user);\n        return response;\n    }\n\n    [HttpPost(\"duo\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /duo instead.\")]\n    public async Task<TwoFactorDuoResponseModel> PostDuo([FromBody] UpdateTwoFactorDuoRequestModel model)\n    {\n        return await PutDuo(model);\n    }\n\n    [HttpPost(\"~/organizations/{id}/two-factor/get-duo\")]\n    public async Task<TwoFactorDuoResponseModel> GetOrganizationDuo(string id,\n        [FromBody] SecretVerificationRequestModel model)\n    {\n        await CheckAsync(model, false, true);\n\n        var orgIdGuid = new Guid(id);\n        if (!await _currentContext.ManagePolicies(orgIdGuid))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException();\n        var response = new TwoFactorDuoResponseModel(organization);\n        return response;\n    }\n\n    [HttpPut(\"~/organizations/{id}/two-factor/duo\")]\n    public async Task<TwoFactorDuoResponseModel> PutOrganizationDuo(string id,\n        [FromBody] UpdateTwoFactorDuoRequestModel model)\n    {\n        await CheckAsync(model, false);\n\n        var orgIdGuid = new Guid(id);\n        if (!await _currentContext.ManagePolicies(orgIdGuid))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException();\n        if (!await _duoUniversalTokenService.ValidateDuoConfiguration(model.ClientSecret, model.ClientId, model.Host))\n        {\n            throw new BadRequestException(\n                \"Duo configuration settings are not valid. Please re-check the Duo Admin panel.\");\n        }\n\n        model.ToOrganization(organization);\n        await _organizationService.UpdateTwoFactorProviderAsync(organization,\n            TwoFactorProviderType.OrganizationDuo);\n        var response = new TwoFactorDuoResponseModel(organization);\n        return response;\n    }\n\n    [HttpPost(\"~/organizations/{id}/two-factor/duo\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/duo instead.\")]\n    public async Task<TwoFactorDuoResponseModel> PostOrganizationDuo(string id,\n        [FromBody] UpdateTwoFactorDuoRequestModel model)\n    {\n        return await PutOrganizationDuo(id, model);\n    }\n\n    [HttpPost(\"get-webauthn\")]\n    public async Task<TwoFactorWebAuthnResponseModel> GetWebAuthn([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await CheckAsync(model, false, true);\n        var response = new TwoFactorWebAuthnResponseModel(user);\n        return response;\n    }\n\n    [HttpPost(\"get-webauthn-challenge\")]\n    [ApiExplorerSettings(IgnoreApi = true)] // Disable Swagger due to CredentialCreateOptions not converting properly\n    public async Task<CredentialCreateOptions> GetWebAuthnChallenge([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await CheckAsync(model, false, true);\n        var reg = await _startTwoFactorWebAuthnRegistrationCommand.StartTwoFactorWebAuthnRegistrationAsync(user);\n        return reg;\n    }\n\n    [HttpPut(\"webauthn\")]\n    public async Task<TwoFactorWebAuthnResponseModel> PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model)\n    {\n        var user = await CheckAsync(model, false);\n\n        var success = await _completeTwoFactorWebAuthnRegistrationCommand.CompleteTwoFactorWebAuthnRegistrationAsync(\n            user, model.Id.Value, model.Name, model.DeviceResponse);\n        if (!success)\n        {\n            throw new BadRequestException(\"Unable to complete WebAuthn registration.\");\n        }\n\n        var response = new TwoFactorWebAuthnResponseModel(user);\n        return response;\n    }\n\n    [HttpPost(\"webauthn\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /webauthn instead.\")]\n    public async Task<TwoFactorWebAuthnResponseModel> PostWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model)\n    {\n        return await PutWebAuthn(model);\n    }\n\n    [HttpDelete(\"webauthn\")]\n    public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn(\n        [FromBody] TwoFactorWebAuthnDeleteRequestModel model)\n    {\n        var user = await CheckAsync(model, false);\n\n        if (!model.Id.HasValue)\n        {\n            throw new BadRequestException(\"Unable to delete WebAuthn credential.\");\n        }\n\n        var success = await _deleteTwoFactorWebAuthnCredentialCommand.DeleteTwoFactorWebAuthnCredentialAsync(user, model.Id.Value);\n        if (!success)\n        {\n            throw new BadRequestException(\"Unable to delete WebAuthn credential.\");\n        }\n\n        var response = new TwoFactorWebAuthnResponseModel(user);\n        return response;\n    }\n\n    [HttpPost(\"get-email\")]\n    public async Task<TwoFactorEmailResponseModel> GetEmail([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await CheckAsync(model, false, true);\n        var response = new TwoFactorEmailResponseModel(user);\n        return response;\n    }\n\n    /// <summary>\n    /// This endpoint is only used to set-up email two factor authentication.\n    /// </summary>\n    /// <param name=\"model\">secret verification model</param>\n    /// <returns>void</returns>\n    [HttpPost(\"send-email\")]\n    public async Task SendEmail([FromBody] TwoFactorEmailRequestModel model)\n    {\n        var user = await CheckAsync(model, false, true);\n        // Add email to the user's 2FA providers, with the email address they've provided.\n        model.ToUser(user);\n        await _twoFactorEmailService.SendTwoFactorSetupEmailAsync(user);\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"send-email-login\")]\n    public async Task SendEmailLoginAsync([FromBody] TwoFactorEmailRequestModel requestModel)\n    {\n        var user = await _userManager.FindByEmailAsync(requestModel.Email.ToLowerInvariant());\n\n        if (user != null)\n        {\n            // Check if 2FA email is from a device approval (\"Log in with device\") scenario.\n            if (!string.IsNullOrEmpty(requestModel.AuthRequestAccessCode))\n            {\n                var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(requestModel.AuthRequestId));\n                if (authRequest != null &&\n                    authRequest.IsValidForAuthentication(user.Id, requestModel.AuthRequestAccessCode))\n                {\n                    await _twoFactorEmailService.SendTwoFactorEmailAsync(user);\n                    return;\n                }\n            }\n            else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken))\n            {\n                if (ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user))\n                {\n                    await _twoFactorEmailService.SendTwoFactorEmailAsync(user);\n                    return;\n                }\n\n                await ThrowDelayedBadRequestExceptionAsync(\n                    \"Cannot send two-factor email: a valid, non-expired SSO Email 2FA Session token is required to send 2FA emails.\");\n            }\n            else if (await _userService.VerifySecretAsync(user, requestModel.Secret))\n            {\n                await _twoFactorEmailService.SendTwoFactorEmailAsync(user);\n                return;\n            }\n        }\n\n        await ThrowDelayedBadRequestExceptionAsync(\"Cannot send two-factor email.\");\n    }\n\n    [HttpPut(\"email\")]\n    public async Task<TwoFactorEmailResponseModel> PutEmail([FromBody] UpdateTwoFactorEmailRequestModel model)\n    {\n        var user = await CheckAsync(model, false);\n        model.ToUser(user);\n\n        if (!await _userManager.VerifyTwoFactorTokenAsync(user,\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), model.Token))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"Token\", \"Invalid token.\");\n        }\n\n        await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email);\n        var response = new TwoFactorEmailResponseModel(user);\n        return response;\n    }\n\n    [HttpPost(\"email\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /email instead.\")]\n    public async Task<TwoFactorEmailResponseModel> PostEmail([FromBody] UpdateTwoFactorEmailRequestModel model)\n    {\n        return await PutEmail(model);\n    }\n\n    [HttpPut(\"disable\")]\n    public async Task<TwoFactorProviderResponseModel> PutDisable([FromBody] TwoFactorProviderRequestModel model)\n    {\n        var user = await CheckAsync(model, false);\n        await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value);\n        var response = new TwoFactorProviderResponseModel(model.Type.Value, user);\n        return response;\n    }\n\n    [HttpPost(\"disable\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /disable instead.\")]\n    public async Task<TwoFactorProviderResponseModel> PostDisable([FromBody] TwoFactorProviderRequestModel model)\n    {\n        return await PutDisable(model);\n    }\n\n    [HttpPut(\"~/organizations/{id}/two-factor/disable\")]\n    public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id,\n        [FromBody] TwoFactorProviderRequestModel model)\n    {\n        await CheckAsync(model, false);\n\n        var orgIdGuid = new Guid(id);\n        if (!await _currentContext.ManagePolicies(orgIdGuid))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _organizationService.DisableTwoFactorProviderAsync(organization, model.Type.Value);\n        var response = new TwoFactorProviderResponseModel(model.Type.Value, organization);\n        return response;\n    }\n\n    [HttpPost(\"~/organizations/{id}/two-factor/disable\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/disable instead.\")]\n    public async Task<TwoFactorProviderResponseModel> PostOrganizationDisable(string id,\n        [FromBody] TwoFactorProviderRequestModel model)\n    {\n        return await PutOrganizationDisable(id, model);\n    }\n\n    [HttpPost(\"get-recover\")]\n    public async Task<TwoFactorRecoverResponseModel> GetRecover([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await CheckAsync(model, false);\n        var response = new TwoFactorRecoverResponseModel(user);\n        return response;\n    }\n\n    [Obsolete(\"Leaving this for backwards compatibility on clients\")]\n    [HttpGet(\"get-device-verification-settings\")]\n    public Task<DeviceVerificationResponseModel> GetDeviceVerificationSettings()\n    {\n        return Task.FromResult(new DeviceVerificationResponseModel(false, false));\n    }\n\n    [Obsolete(\"Leaving this for backwards compatibility on clients\")]\n    [HttpPut(\"device-verification-settings\")]\n    public Task<DeviceVerificationResponseModel> PutDeviceVerificationSettings(\n        [FromBody] DeviceVerificationRequestModel model)\n    {\n        return Task.FromResult(new DeviceVerificationResponseModel(false, false));\n    }\n\n    private async Task<User> CheckAsync(SecretVerificationRequestModel model, bool premium,\n        bool skipVerification = false)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!await _userService.VerifySecretAsync(user, model.Secret, skipVerification))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(string.Empty, \"User verification failed.\");\n        }\n\n        if (premium && !await _userService.CanAccessPremium(user))\n        {\n            throw new BadRequestException(\"Premium status is required.\");\n        }\n\n        return user;\n    }\n\n    private async Task ValidateYubiKeyAsync(User user, string name, string value)\n    {\n        if (string.IsNullOrWhiteSpace(value) || value.Length == 12)\n        {\n            return;\n        }\n\n        if (!await _userManager.VerifyTwoFactorTokenAsync(user,\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey), value))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(name, $\"{name} is invalid.\");\n        }\n\n        await Task.Delay(500);\n    }\n\n    private bool ValidateSsoEmail2FaToken(string ssoEmail2FaSessionToken, User user)\n    {\n        return _ssoEmailTwoFactorSessionDataProtector.TryUnprotect(ssoEmail2FaSessionToken, out var decryptedToken) &&\n               decryptedToken.Valid && decryptedToken.TokenIsValid(user);\n    }\n\n    private async Task ThrowDelayedBadRequestExceptionAsync(string message, int delayTime = 2000)\n    {\n        await Task.Delay(delayTime);\n        throw new BadRequestException(message);\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Controllers/WebAuthnController.cs",
    "content": "﻿using Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Auth.Models.Request.WebAuthn;\nusing Bit.Api.Auth.Models.Response.WebAuthn;\nusing Bit.Api.Models.Response;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.Models.Api.Response.Accounts;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.WebAuthnLogin;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Auth.Controllers;\n\n[Route(\"webauthn\")]\npublic class WebAuthnController : Controller\n{\n    private readonly IUserService _userService;\n    private readonly IPolicyService _policyService;\n    private readonly IWebAuthnCredentialRepository _credentialRepository;\n    private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;\n    private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;\n    private readonly IGetWebAuthnLoginCredentialCreateOptionsCommand _getWebAuthnLoginCredentialCreateOptionsCommand;\n    private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand;\n    private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;\n    private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IFeatureService _featureService;\n\n    public WebAuthnController(\n        IUserService userService,\n        IPolicyService policyService,\n        IWebAuthnCredentialRepository credentialRepository,\n        IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector,\n        IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,\n        IGetWebAuthnLoginCredentialCreateOptionsCommand getWebAuthnLoginCredentialCreateOptionsCommand,\n        ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand,\n        IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,\n        IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IFeatureService featureService)\n    {\n        _userService = userService;\n        _policyService = policyService;\n        _credentialRepository = credentialRepository;\n        _createOptionsDataProtector = createOptionsDataProtector;\n        _assertionOptionsDataProtector = assertionOptionsDataProtector;\n        _getWebAuthnLoginCredentialCreateOptionsCommand = getWebAuthnLoginCredentialCreateOptionsCommand;\n        _createWebAuthnLoginCredentialCommand = createWebAuthnLoginCredentialCommand;\n        _assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;\n        _getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;\n        _policyRequirementQuery = policyRequirementQuery;\n        _featureService = featureService;\n    }\n\n    [Authorize(Policies.Web)]\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<WebAuthnCredentialResponseModel>> Get()\n    {\n        var user = await GetUserAsync();\n        var credentials = await _credentialRepository.GetManyByUserIdAsync(user.Id);\n\n        return new ListResponseModel<WebAuthnCredentialResponseModel>(credentials.Select(c => new WebAuthnCredentialResponseModel(c)));\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpPost(\"attestation-options\")]\n    public async Task<WebAuthnCredentialCreateOptionsResponseModel> AttestationOptions([FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await VerifyUserAsync(model);\n        await ValidateIfUserCanUsePasskeyLogin(user.Id);\n        var options = await _getWebAuthnLoginCredentialCreateOptionsCommand.GetWebAuthnLoginCredentialCreateOptionsAsync(user);\n\n        var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options);\n        var token = _createOptionsDataProtector.Protect(tokenable);\n\n        return new WebAuthnCredentialCreateOptionsResponseModel\n        {\n            Options = options,\n            Token = token\n        };\n    }\n\n    [Authorize(Policies.Web)]\n    [HttpPost(\"assertion-options\")]\n    public async Task<WebAuthnLoginAssertionOptionsResponseModel> AssertionOptions([FromBody] SecretVerificationRequestModel model)\n    {\n        await VerifyUserAsync(model);\n        var options = _getWebAuthnLoginCredentialAssertionOptionsCommand.GetWebAuthnLoginCredentialAssertionOptions();\n\n        var tokenable = new WebAuthnLoginAssertionOptionsTokenable(WebAuthnLoginAssertionOptionsScope.UpdateKeySet, options);\n        var token = _assertionOptionsDataProtector.Protect(tokenable);\n\n        return new WebAuthnLoginAssertionOptionsResponseModel\n        {\n            Options = options,\n            Token = token\n        };\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpPost(\"\")]\n    public async Task<WebAuthnCredentialResponseModel> Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model)\n    {\n        var user = await GetUserAsync();\n        await ValidateIfUserCanUsePasskeyLogin(user.Id);\n        var tokenable = _createOptionsDataProtector.Unprotect(model.Token);\n\n        if (!tokenable.TokenIsValid(user))\n        {\n            throw new BadRequestException(\"The token associated with your request is expired. A valid token is required to continue.\");\n        }\n\n        var credential = await _createWebAuthnLoginCredentialCommand.CreateWebAuthnLoginCredentialAsync(user, model.Name, tokenable.Options, model.DeviceResponse, model.SupportsPrf, model.EncryptedUserKey, model.EncryptedPublicKey, model.EncryptedPrivateKey);\n        if (credential == null)\n        {\n            throw new BadRequestException(\"Unable to complete WebAuthn registration.\");\n        }\n\n        return new WebAuthnCredentialResponseModel(credential);\n    }\n\n    private async Task ValidateRequireSsoPolicyDisabledOrNotApplicable(Guid userId)\n    {\n        var requireSsoLogin = await _policyService.AnyPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso);\n\n        if (requireSsoLogin)\n        {\n            throw new BadRequestException(\"Passkeys cannot be created for your account. SSO login is required.\");\n        }\n    }\n\n    private async Task ValidateIfUserCanUsePasskeyLogin(Guid userId)\n    {\n        if (!_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))\n        {\n            await ValidateRequireSsoPolicyDisabledOrNotApplicable(userId);\n            return;\n        }\n\n        var requireSsoPolicyRequirement = await _policyRequirementQuery.GetAsyncVNext<RequireSsoPolicyRequirement>(userId);\n\n        if (!requireSsoPolicyRequirement.CanUsePasskeyLogin)\n        {\n            throw new BadRequestException(\"Passkeys cannot be created for your account. SSO login is required.\");\n        }\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpPut()]\n    public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)\n    {\n        var tokenable = _assertionOptionsDataProtector.Unprotect(model.Token);\n        if (!tokenable.TokenIsValid(WebAuthnLoginAssertionOptionsScope.UpdateKeySet))\n        {\n            throw new BadRequestException(\"The token associated with your request is invalid or has expired. A valid token is required to continue.\");\n        }\n\n        var (_, credential) = await _assertWebAuthnLoginCredentialCommand.AssertWebAuthnLoginCredential(tokenable.Options, model.DeviceResponse);\n        if (credential == null || credential.SupportsPrf != true)\n        {\n            throw new BadRequestException(\"Unable to update credential.\");\n        }\n\n        // assign new keys to credential\n        credential.EncryptedUserKey = model.EncryptedUserKey;\n        credential.EncryptedPrivateKey = model.EncryptedPrivateKey;\n        credential.EncryptedPublicKey = model.EncryptedPublicKey;\n\n        await _credentialRepository.UpdateAsync(credential);\n    }\n\n    [Authorize(Policies.Web)]\n    [HttpPost(\"{id}/delete\")]\n    public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)\n    {\n        var user = await VerifyUserAsync(model);\n        var credential = await _credentialRepository.GetByIdAsync(id, user.Id);\n        if (credential == null)\n        {\n            throw new NotFoundException(\"Credential not found.\");\n        }\n\n        await _credentialRepository.DeleteAsync(credential);\n    }\n\n    private async Task<Core.Entities.User> GetUserAsync()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n        return user;\n    }\n\n    private async Task<Core.Entities.User> VerifyUserAsync(SecretVerificationRequestModel model)\n    {\n        var user = await GetUserAsync();\n        if (!await _userService.VerifySecretAsync(user, model.Secret))\n        {\n            await Task.Delay(Constants.FailedSecretVerificationDelay);\n            throw new BadRequestException(string.Empty, \"User verification failed.\");\n        }\n\n        return user;\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Jobs/EmergencyAccessNotificationJob.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess;\nusing Bit.Core.Jobs;\nusing Quartz;\n\nnamespace Bit.Api.Auth.Jobs;\n\npublic class EmergencyAccessNotificationJob : BaseJob\n{\n    private readonly IServiceScopeFactory _serviceScopeFactory;\n\n    public EmergencyAccessNotificationJob(IServiceScopeFactory serviceScopeFactory, ILogger<EmergencyAccessNotificationJob> logger)\n        : base(logger)\n    {\n        _serviceScopeFactory = serviceScopeFactory;\n    }\n\n    protected override async Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        using var scope = _serviceScopeFactory.CreateScope();\n        var emergencyAccessService = scope.ServiceProvider.GetService(typeof(IEmergencyAccessService)) as IEmergencyAccessService;\n        await emergencyAccessService.SendNotificationsAsync();\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Jobs/EmergencyAccessTimeoutJob.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess;\nusing Bit.Core.Jobs;\nusing Quartz;\n\nnamespace Bit.Api.Auth.Jobs;\n\npublic class EmergencyAccessTimeoutJob : BaseJob\n{\n    private readonly IServiceScopeFactory _serviceScopeFactory;\n\n    public EmergencyAccessTimeoutJob(IServiceScopeFactory serviceScopeFactory, ILogger<EmergencyAccessNotificationJob> logger)\n        : base(logger)\n    {\n        _serviceScopeFactory = serviceScopeFactory;\n    }\n\n    protected override async Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        using var scope = _serviceScopeFactory.CreateScope();\n        var emergencyAccessService = scope.ServiceProvider.GetService(typeof(IEmergencyAccessService)) as IEmergencyAccessService;\n        await emergencyAccessService.HandleTimedOutRequestsAsync();\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/DeleteRecoverRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class DeleteRecoverRequestModel\n{\n    [Required]\n    [EmailAddress]\n    [StringLength(256)]\n    public string Email { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/EmailRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class EmailRequestModel : SecretVerificationRequestModel\n{\n    [Required]\n    [StrictEmailAddress]\n    [StringLength(256)]\n    public string NewEmail { get; set; }\n    [Required]\n    [StringLength(300)]\n    public string NewMasterPasswordHash { get; set; }\n    [Required]\n    public string Token { get; set; }\n    [Required]\n    public string Key { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/EmailTokenRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class EmailTokenRequestModel : SecretVerificationRequestModel\n{\n    [Required]\n    [StrictEmailAddress]\n    [StringLength(256)]\n    public string NewEmail { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataAndAuthenticationModel.cs",
    "content": "﻿#nullable enable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class MasterPasswordUnlockAndAuthenticationDataModel : IValidatableObject\n{\n    public required KdfType KdfType { get; set; }\n    public required int KdfIterations { get; set; }\n    public int? KdfMemory { get; set; }\n    public int? KdfParallelism { get; set; }\n\n    [StrictEmailAddress]\n    [StringLength(256)]\n    public required string Email { get; set; }\n    [StringLength(300)]\n    public required string MasterKeyAuthenticationHash { get; set; }\n    [EncryptedString] public required string MasterKeyEncryptedUserKey { get; set; }\n    [StringLength(50)]\n    public string? MasterPasswordHint { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (KdfType == KdfType.PBKDF2_SHA256)\n        {\n            if (KdfMemory.HasValue || KdfParallelism.HasValue)\n            {\n                yield return new ValidationResult(\"KdfMemory and KdfParallelism must be null for PBKDF2_SHA256\", new[] { nameof(KdfMemory), nameof(KdfParallelism) });\n            }\n        }\n        else if (KdfType == KdfType.Argon2id)\n        {\n            if (!KdfMemory.HasValue || !KdfParallelism.HasValue)\n            {\n                yield return new ValidationResult(\"KdfMemory and KdfParallelism must have values for Argon2id\", new[] { nameof(KdfMemory), nameof(KdfParallelism) });\n            }\n        }\n        else\n        {\n            yield return new ValidationResult(\"Invalid KdfType\", new[] { nameof(KdfType) });\n        }\n    }\n\n    public MasterPasswordUnlockAndAuthenticationData ToUnlockData()\n    {\n        var data = new MasterPasswordUnlockAndAuthenticationData\n        {\n            KdfType = KdfType,\n            KdfIterations = KdfIterations,\n            KdfMemory = KdfMemory,\n            KdfParallelism = KdfParallelism,\n\n            Email = Email,\n\n            MasterKeyAuthenticationHash = MasterKeyAuthenticationHash,\n            MasterKeyEncryptedUserKey = MasterKeyEncryptedUserKey,\n            MasterPasswordHint = MasterPasswordHint\n        };\n        return data;\n    }\n\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/PasswordHintRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class PasswordHintRequestModel\n{\n    [Required]\n    [EmailAddress]\n    [StringLength(256)]\n    public string Email { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.KeyManagement.Models.Api.Request;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class PasswordRequestModel : SecretVerificationRequestModel\n{\n    [Required]\n    [StringLength(300)]\n    public required string NewMasterPasswordHash { get; set; }\n    [StringLength(50)]\n    public string? MasterPasswordHint { get; set; }\n    [Required]\n    public required string Key { get; set; }\n\n    // Note: These will eventually become required, but not all consumers are moved over yet.\n    public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; }\n    public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/RegenerateTwoFactorRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class RegenerateTwoFactorRequestModel\n{\n    [Required]\n    public string MasterPasswordHash { get; set; }\n    [Required]\n    [StringLength(50)]\n    public string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class SecretVerificationRequestModel : IValidatableObject\n{\n    [StringLength(300)]\n    public string MasterPasswordHash { get; set; }\n    public string OTP { get; set; }\n    public string AuthRequestAccessCode { get; set; }\n    public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP;\n\n    public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (string.IsNullOrEmpty(Secret) && string.IsNullOrEmpty(AuthRequestAccessCode))\n        {\n            yield return new ValidationResult(\"MasterPasswordHash, OTP, or AccessCode must be supplied.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/SetInitialPasswordRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class SetInitialPasswordRequestModel : IValidatableObject\n{\n    // TODO will be removed with https://bitwarden.atlassian.net/browse/PM-27327\n    [Obsolete(\"Use MasterPasswordAuthentication instead\")]\n    [StringLength(300)]\n    public string? MasterPasswordHash { get; set; }\n\n    [Obsolete(\"Use MasterPasswordUnlock instead\")]\n    public string? Key { get; set; }\n\n    [Obsolete(\"Use AccountKeys instead\")]\n    public KeysRequestModel? Keys { get; set; }\n\n    [Obsolete(\"Use MasterPasswordAuthentication instead\")]\n    public KdfType? Kdf { get; set; }\n\n    [Obsolete(\"Use MasterPasswordAuthentication instead\")]\n    public int? KdfIterations { get; set; }\n\n    [Obsolete(\"Use MasterPasswordAuthentication instead\")]\n    public int? KdfMemory { get; set; }\n\n    [Obsolete(\"Use MasterPasswordAuthentication instead\")]\n    public int? KdfParallelism { get; set; }\n\n    public MasterPasswordAuthenticationDataRequestModel? MasterPasswordAuthentication { get; set; }\n    public MasterPasswordUnlockDataRequestModel? MasterPasswordUnlock { get; set; }\n    public AccountKeysRequestModel? AccountKeys { get; set; }\n\n    [StringLength(50)]\n    public string? MasterPasswordHint { get; set; }\n\n    [Required]\n    public required string OrgIdentifier { get; set; }\n\n    // TODO removed with https://bitwarden.atlassian.net/browse/PM-27327\n    public User ToUser(User existingUser)\n    {\n        existingUser.MasterPasswordHint = MasterPasswordHint;\n        existingUser.Kdf = Kdf!.Value;\n        existingUser.KdfIterations = KdfIterations!.Value;\n        existingUser.KdfMemory = KdfMemory;\n        existingUser.KdfParallelism = KdfParallelism;\n        existingUser.Key = Key;\n        Keys?.ToUser(existingUser);\n        return existingUser;\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (IsV2Request())\n        {\n            // V2 registration\n\n            // Validate Kdf\n            var authenticationKdf = MasterPasswordAuthentication!.Kdf.ToData();\n            var unlockKdf = MasterPasswordUnlock!.Kdf.ToData();\n\n            // Currently, KDF settings are not saved separately for authentication and unlock and must therefore be equal\n            if (!authenticationKdf.Equals(unlockKdf))\n            {\n                yield return new ValidationResult(\"KDF settings must be equal for authentication and unlock.\",\n                    [$\"{nameof(MasterPasswordAuthentication)}.{nameof(MasterPasswordAuthenticationDataRequestModel.Kdf)}\",\n                        $\"{nameof(MasterPasswordUnlock)}.{nameof(MasterPasswordUnlockDataRequestModel.Kdf)}\"]);\n            }\n\n            var authenticationValidationErrors = KdfSettingsValidator.Validate(authenticationKdf).ToList();\n            if (authenticationValidationErrors.Count != 0)\n            {\n                yield return authenticationValidationErrors.First();\n            }\n\n            var unlockValidationErrors = KdfSettingsValidator.Validate(unlockKdf).ToList();\n            if (unlockValidationErrors.Count != 0)\n            {\n                yield return unlockValidationErrors.First();\n            }\n\n            yield break;\n        }\n\n        // V1 registration\n        // TODO removed with https://bitwarden.atlassian.net/browse/PM-27327\n        if (string.IsNullOrEmpty(MasterPasswordHash))\n        {\n            yield return new ValidationResult(\"MasterPasswordHash must be supplied.\");\n        }\n\n        if (string.IsNullOrEmpty(Key))\n        {\n            yield return new ValidationResult(\"Key must be supplied.\");\n        }\n\n        if (Kdf == null)\n        {\n            yield return new ValidationResult(\"Kdf must be supplied.\");\n            yield break;\n        }\n\n        if (KdfIterations == null)\n        {\n            yield return new ValidationResult(\"KdfIterations must be supplied.\");\n            yield break;\n        }\n\n        if (Kdf == KdfType.Argon2id)\n        {\n            if (KdfMemory == null)\n            {\n                yield return new ValidationResult(\"KdfMemory must be supplied when Kdf is Argon2id.\");\n            }\n\n            if (KdfParallelism == null)\n            {\n                yield return new ValidationResult(\"KdfParallelism must be supplied when Kdf is Argon2id.\");\n            }\n        }\n\n        var validationErrors = KdfSettingsValidator\n            .Validate(Kdf!.Value, KdfIterations!.Value, KdfMemory, KdfParallelism).ToList();\n        if (validationErrors.Count != 0)\n        {\n            yield return validationErrors.First();\n        }\n    }\n\n    public bool IsV2Request()\n    {\n        // AccountKeys can be null for TDE users, so we don't check that here\n        return MasterPasswordAuthentication != null && MasterPasswordUnlock != null;\n    }\n\n    public bool IsTdeSetPasswordRequest()\n    {\n        return AccountKeys == null;\n    }\n\n    public SetInitialMasterPasswordDataModel ToData()\n    {\n        return new SetInitialMasterPasswordDataModel\n        {\n            MasterPasswordAuthentication = MasterPasswordAuthentication!.ToData(),\n            MasterPasswordUnlock = MasterPasswordUnlock!.ToData(),\n            OrgSsoIdentifier = OrgIdentifier,\n            AccountKeys = AccountKeys?.ToAccountKeysData(),\n            MasterPasswordHint = MasterPasswordHint\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/SetVerifyDevicesRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class SetVerifyDevicesRequestModel : SecretVerificationRequestModel\n{\n    [Required]\n    public bool VerifyDevices { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/UnauthenticatedSecretVerificationRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class UnauthenticatedSecretVerificationRequestModel : SecretVerificationRequestModel\n{\n    [Required]\n    [StrictEmailAddress]\n    [StringLength(256)]\n    public string Email { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.Auth.Models.Request.WebAuthn;\nusing Bit.Api.Tools.Models.Request;\nusing Bit.Api.Vault.Models.Request;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class UpdateKeyRequestModel\n{\n    [Required]\n    [StringLength(300)]\n    public string MasterPasswordHash { get; set; }\n    [Required]\n    public string Key { get; set; }\n    [Required]\n    public string PrivateKey { get; set; }\n    public IEnumerable<CipherWithIdRequestModel> Ciphers { get; set; }\n    public IEnumerable<FolderWithIdRequestModel> Folders { get; set; }\n    public IEnumerable<SendWithIdRequestModel> Sends { get; set; }\n    public IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessKeys { get; set; }\n    public IEnumerable<ResetPasswordWithOrgIdRequestModel> ResetPasswordKeys { get; set; }\n    public IEnumerable<WebAuthnLoginRotateKeyRequestModel> WebAuthnKeys { get; set; }\n\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/UpdateProfileRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class UpdateProfileRequestModel\n{\n    [StringLength(50)]\n    public string Name { get; set; }\n    [StringLength(50)]\n    [Obsolete(\"This field is ignored. Changes are made via the 'password' endpoint.\")]\n    public string MasterPasswordHint { get; set; }\n\n    public User ToUser(User existingUser)\n    {\n        existingUser.Name = Name;\n        return existingUser;\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/UpdateTdeOffboardingPasswordRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class UpdateTdeOffboardingPasswordRequestModel\n{\n    [Required]\n    [StringLength(300)]\n    public string NewMasterPasswordHash { get; set; }\n    [Required]\n    public string Key { get; set; }\n    [StringLength(50)]\n    public string MasterPasswordHint { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/UpdateTempPasswordRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.Models.Request.Organizations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class UpdateTempPasswordRequestModel : OrganizationUserResetPasswordRequestModel\n{\n    [StringLength(50)]\n    public string MasterPasswordHint { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/VerifyDeleteRecoverRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class VerifyDeleteRecoverRequestModel\n{\n    [Required]\n    public string UserId { get; set; }\n    [Required]\n    public string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/VerifyEmailRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class VerifyEmailRequestModel\n{\n    [Required]\n    public string UserId { get; set; }\n    [Required]\n    public string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/Accounts/VerifyOTPRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Auth.Models.Request.Accounts;\n\npublic class VerifyOTPRequestModel\n{\n    [Required]\n    public string OTP { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Auth.Models.Request;\n\npublic class EmergencyAccessInviteRequestModel\n{\n    [Required]\n    [StrictEmailAddress]\n    [StringLength(256)]\n    public string Email { get; set; }\n    [Required]\n    public EmergencyAccessType? Type { get; set; }\n    [Required]\n    [Range(1, short.MaxValue)]\n    public int WaitTimeDays { get; set; }\n}\n\npublic class EmergencyAccessUpdateRequestModel\n{\n    [Required]\n    public EmergencyAccessType Type { get; set; }\n    [Required]\n    [Range(1, short.MaxValue)]\n    public int WaitTimeDays { get; set; }\n    public string KeyEncrypted { get; set; }\n\n    public EmergencyAccess ToEmergencyAccess(EmergencyAccess existingEmergencyAccess)\n    {\n        // Ensure we only set keys for a confirmed emergency access.\n        if (!string.IsNullOrWhiteSpace(existingEmergencyAccess.KeyEncrypted) && !string.IsNullOrWhiteSpace(KeyEncrypted))\n        {\n            existingEmergencyAccess.KeyEncrypted = KeyEncrypted;\n        }\n        existingEmergencyAccess.Type = Type;\n        existingEmergencyAccess.WaitTimeDays = (short)WaitTimeDays;\n        return existingEmergencyAccess;\n    }\n}\n\npublic class EmergencyAccessPasswordRequestModel\n{\n    [Required]\n    [StringLength(300)]\n    public string NewMasterPasswordHash { get; set; }\n    [Required]\n    public string Key { get; set; }\n}\n\npublic class EmergencyAccessWithIdRequestModel : EmergencyAccessUpdateRequestModel\n{\n    [Required]\n    public Guid Id { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text.RegularExpressions;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Services;\nusing Bit.Core.Sso;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\n\nnamespace Bit.Api.Auth.Models.Request.Organizations;\n\npublic class OrganizationSsoRequestModel\n{\n    [Required]\n    public bool Enabled { get; set; }\n    [StringLength(50)]\n    public string Identifier { get; set; }\n    [Required]\n    public SsoConfigurationDataRequest Data { get; set; }\n\n    public SsoConfig ToSsoConfig(Guid organizationId)\n    {\n        return ToSsoConfig(new SsoConfig { OrganizationId = organizationId });\n    }\n\n    public SsoConfig ToSsoConfig(SsoConfig existingConfig)\n    {\n        existingConfig.Enabled = Enabled;\n        var configurationData = Data.ToConfigurationData();\n        existingConfig.SetData(configurationData);\n        return existingConfig;\n    }\n}\n\npublic class SsoConfigurationDataRequest : IValidatableObject\n{\n    public SsoConfigurationDataRequest() { }\n\n    [Required]\n    public SsoType ConfigType { get; set; }\n    public MemberDecryptionType MemberDecryptionType { get; set; }\n\n    [Obsolete(\"Use MemberDecryptionType instead\")]\n    public bool KeyConnectorEnabled\n    {\n        // Setter is kept for backwards compatibility with older clients that still use this property.\n        set { MemberDecryptionType = value ? MemberDecryptionType.KeyConnector : MemberDecryptionType.MasterPassword; }\n    }\n    public string KeyConnectorUrl { get; set; }\n\n    // OIDC\n    public string Authority { get; set; }\n    public string ClientId { get; set; }\n    public string ClientSecret { get; set; }\n    public string MetadataAddress { get; set; }\n    public OpenIdConnectRedirectBehavior RedirectBehavior { get; set; }\n    public bool? GetClaimsFromUserInfoEndpoint { get; set; }\n    public string AdditionalScopes { get; set; }\n    public string AdditionalUserIdClaimTypes { get; set; }\n    public string AdditionalEmailClaimTypes { get; set; }\n    public string AdditionalNameClaimTypes { get; set; }\n    public string AcrValues { get; set; }\n    public string ExpectedReturnAcrValue { get; set; }\n\n    // SAML2 SP\n    public bool? SpUniqueEntityId { get; set; }\n    public Saml2NameIdFormat SpNameIdFormat { get; set; }\n    public string SpOutboundSigningAlgorithm { get; set; }\n    public Saml2SigningBehavior SpSigningBehavior { get; set; }\n    public bool? SpWantAssertionsSigned { get; set; }\n    public bool? SpValidateCertificates { get; set; }\n    public string SpMinIncomingSigningAlgorithm { get; set; }\n\n    // SAML2 IDP\n    public string IdpEntityId { get; set; }\n    public Saml2BindingType IdpBindingType { get; set; }\n    public string IdpSingleSignOnServiceUrl { get; set; }\n    public string IdpSingleLogoutServiceUrl { get; set; }\n    public string IdpArtifactResolutionServiceUrl { get => null; set { /*IGNORE*/ } }\n    public string IdpX509PublicCert { get; set; }\n    public string IdpOutboundSigningAlgorithm { get; set; }\n    public bool? IdpAllowUnsolicitedAuthnResponse { get; set; }\n    public bool? IdpDisableOutboundLogoutRequests { get; set; }\n    public bool? IdpWantAuthnRequestsSigned { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext context)\n    {\n        var i18nService = context.GetService(typeof(II18nService)) as I18nService;\n\n        if (ConfigType == SsoType.OpenIdConnect)\n        {\n            if (string.IsNullOrWhiteSpace(Authority))\n            {\n                yield return new ValidationResult(i18nService.GetLocalizedHtmlString(\"AuthorityValidationError\"),\n                    new[] { nameof(Authority) });\n            }\n\n            if (string.IsNullOrWhiteSpace(ClientId))\n            {\n                yield return new ValidationResult(i18nService.GetLocalizedHtmlString(\"ClientIdValidationError\"),\n                    new[] { nameof(ClientId) });\n            }\n\n            if (string.IsNullOrWhiteSpace(ClientSecret))\n            {\n                yield return new ValidationResult(i18nService.GetLocalizedHtmlString(\"ClientSecretValidationError\"),\n                    new[] { nameof(ClientSecret) });\n            }\n        }\n        else if (ConfigType == SsoType.Saml2)\n        {\n            if (string.IsNullOrWhiteSpace(IdpEntityId))\n            {\n                yield return new ValidationResult(i18nService.GetLocalizedHtmlString(\"IdpEntityIdValidationError\"),\n                    new[] { nameof(IdpEntityId) });\n            }\n\n            if (string.IsNullOrWhiteSpace(IdpSingleSignOnServiceUrl))\n            {\n                yield return new ValidationResult(i18nService.GetLocalizedHtmlString(\"IdpSingleSignOnServiceUrlValidationError\"),\n                    new[] { nameof(IdpSingleSignOnServiceUrl) });\n            }\n\n            if (InvalidServiceUrl(IdpSingleSignOnServiceUrl))\n            {\n                yield return new ValidationResult(i18nService.GetLocalizedHtmlString(\"IdpSingleSignOnServiceUrlInvalid\"),\n                    new[] { nameof(IdpSingleSignOnServiceUrl) });\n            }\n\n            if (InvalidServiceUrl(IdpSingleLogoutServiceUrl))\n            {\n                yield return new ValidationResult(i18nService.GetLocalizedHtmlString(\"IdpSingleLogoutServiceUrlInvalid\"),\n                    new[] { nameof(IdpSingleLogoutServiceUrl) });\n            }\n\n            // TODO: On server, make public certificate required for SAML2 SSO: https://bitwarden.atlassian.net/browse/PM-26028\n            if (!string.IsNullOrWhiteSpace(IdpX509PublicCert))\n            {\n                // Validate the certificate is in a valid format\n                ValidationResult failedResult = null;\n                try\n                {\n                    var certData = CoreHelpers.Base64UrlDecode(StripPemCertificateElements(IdpX509PublicCert));\n                    new X509Certificate2(certData);\n                }\n                catch (FormatException)\n                {\n                    failedResult = new ValidationResult(i18nService.GetLocalizedHtmlString(\"IdpX509PublicCertInvalidFormatValidationError\"),\n                        new[] { nameof(IdpX509PublicCert) });\n                }\n                catch (CryptographicException cryptoEx)\n                {\n                    failedResult = new ValidationResult(i18nService.GetLocalizedHtmlString(\"IdpX509PublicCertCryptographicExceptionValidationError\", cryptoEx.Message),\n                        new[] { nameof(IdpX509PublicCert) });\n                }\n                catch (Exception ex)\n                {\n                    failedResult = new ValidationResult(i18nService.GetLocalizedHtmlString(\"IdpX509PublicCertValidationError\", ex.Message),\n                        new[] { nameof(IdpX509PublicCert) });\n                }\n                if (failedResult != null)\n                {\n                    yield return failedResult;\n                }\n            }\n        }\n    }\n\n    public SsoConfigurationData ToConfigurationData()\n    {\n        return new SsoConfigurationData\n        {\n            ConfigType = ConfigType,\n            MemberDecryptionType = MemberDecryptionType,\n            KeyConnectorUrl = KeyConnectorUrl,\n            Authority = Authority,\n            ClientId = ClientId,\n            ClientSecret = ClientSecret,\n            MetadataAddress = MetadataAddress,\n            GetClaimsFromUserInfoEndpoint = GetClaimsFromUserInfoEndpoint.GetValueOrDefault(),\n            RedirectBehavior = RedirectBehavior,\n            IdpEntityId = IdpEntityId,\n            IdpBindingType = IdpBindingType,\n            IdpSingleSignOnServiceUrl = IdpSingleSignOnServiceUrl,\n            IdpSingleLogoutServiceUrl = IdpSingleLogoutServiceUrl,\n            IdpArtifactResolutionServiceUrl = null,\n            IdpX509PublicCert = StripPemCertificateElements(IdpX509PublicCert),\n            IdpOutboundSigningAlgorithm = IdpOutboundSigningAlgorithm,\n            IdpAllowUnsolicitedAuthnResponse = IdpAllowUnsolicitedAuthnResponse.GetValueOrDefault(),\n            IdpDisableOutboundLogoutRequests = IdpDisableOutboundLogoutRequests.GetValueOrDefault(),\n            IdpWantAuthnRequestsSigned = IdpWantAuthnRequestsSigned.GetValueOrDefault(),\n            SpUniqueEntityId = SpUniqueEntityId.GetValueOrDefault(),\n            SpNameIdFormat = SpNameIdFormat,\n            SpOutboundSigningAlgorithm = SpOutboundSigningAlgorithm ?? SamlSigningAlgorithms.Sha256,\n            SpSigningBehavior = SpSigningBehavior,\n            SpWantAssertionsSigned = SpWantAssertionsSigned.GetValueOrDefault(),\n            SpValidateCertificates = SpValidateCertificates.GetValueOrDefault(),\n            SpMinIncomingSigningAlgorithm = SpMinIncomingSigningAlgorithm,\n            AdditionalScopes = AdditionalScopes,\n            AdditionalUserIdClaimTypes = AdditionalUserIdClaimTypes,\n            AdditionalEmailClaimTypes = AdditionalEmailClaimTypes,\n            AdditionalNameClaimTypes = AdditionalNameClaimTypes,\n            AcrValues = AcrValues,\n            ExpectedReturnAcrValue = ExpectedReturnAcrValue,\n        };\n    }\n\n    private string StripPemCertificateElements(string certificateText)\n    {\n        if (string.IsNullOrWhiteSpace(certificateText))\n        {\n            return null;\n        }\n        return Regex.Replace(certificateText,\n            @\"(((BEGIN|END) CERTIFICATE)|([\\-\\n\\r\\t\\s\\f]))\",\n            string.Empty,\n            RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);\n    }\n\n    private bool InvalidServiceUrl(string url)\n    {\n        if (string.IsNullOrWhiteSpace(url))\n        {\n            return false;\n        }\n        if (!url.StartsWith(\"http://\") && !url.StartsWith(\"https://\"))\n        {\n            return true;\n        }\n        return Regex.IsMatch(url, \"[<>\\\"]\");\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/TwoFactorRequestModels.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Fido2NetLib;\n\nnamespace Bit.Api.Auth.Models.Request;\n\npublic class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationRequestModel\n{\n    [Required]\n    [StringLength(50)]\n    public string Token { get; set; }\n    [Required]\n    [StringLength(50)]\n    public string Key { get; set; }\n    public string UserVerificationToken { get; set; }\n    public User ToUser(User existingUser)\n    {\n        var providers = existingUser.GetTwoFactorProviders();\n        if (providers == null)\n        {\n            providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();\n        }\n        else\n        {\n            providers.Remove(TwoFactorProviderType.Authenticator);\n        }\n\n        providers.Add(TwoFactorProviderType.Authenticator, new TwoFactorProvider\n        {\n            MetaData = new Dictionary<string, object> { [\"Key\"] = Key },\n            Enabled = true\n        });\n        existingUser.SetTwoFactorProviders(providers);\n        return existingUser;\n    }\n}\n\npublic class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject\n{\n    /*\n        String lengths based on Duo's documentation\n        https://github.com/duosecurity/duo_universal_csharp/blob/main/DuoUniversal/Client.cs\n    */\n    [Required]\n    [StringLength(20, MinimumLength = 20, ErrorMessage = \"Client Id must be exactly 20 characters.\")]\n    public string ClientId { get; set; }\n    [Required]\n    [StringLength(40, MinimumLength = 40, ErrorMessage = \"Client Secret must be exactly 40 characters.\")]\n    public string ClientSecret { get; set; }\n    [Required]\n    public string Host { get; set; }\n\n    public User ToUser(User existingUser)\n    {\n        var providers = existingUser.GetTwoFactorProviders();\n        if (providers == null)\n        {\n            providers = [];\n        }\n        else\n        {\n            providers.Remove(TwoFactorProviderType.Duo);\n        }\n\n        providers.Add(TwoFactorProviderType.Duo, new TwoFactorProvider\n        {\n            MetaData = new Dictionary<string, object>\n            {\n                [\"ClientSecret\"] = ClientSecret,\n                [\"ClientId\"] = ClientId,\n                [\"Host\"] = Host\n            },\n            Enabled = true\n        });\n        existingUser.SetTwoFactorProviders(providers);\n        return existingUser;\n    }\n\n    public Organization ToOrganization(Organization existingOrg)\n    {\n        var providers = existingOrg.GetTwoFactorProviders();\n        if (providers == null)\n        {\n            providers = [];\n        }\n        else\n        {\n            providers.Remove(TwoFactorProviderType.OrganizationDuo);\n        }\n\n        providers.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider\n        {\n            MetaData = new Dictionary<string, object>\n            {\n                [\"ClientSecret\"] = ClientSecret,\n                [\"ClientId\"] = ClientId,\n                [\"Host\"] = Host\n            },\n            Enabled = true\n        });\n        existingOrg.SetTwoFactorProviders(providers);\n        return existingOrg;\n    }\n\n    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        var results = new List<ValidationResult>();\n        if (string.IsNullOrWhiteSpace(ClientId))\n        {\n            results.Add(new ValidationResult(\"ClientId is required.\", [nameof(ClientId)]));\n        }\n\n        if (string.IsNullOrWhiteSpace(ClientSecret))\n        {\n            results.Add(new ValidationResult(\"ClientSecret is required.\", [nameof(ClientSecret)]));\n        }\n\n        if (string.IsNullOrWhiteSpace(Host) || !DuoUniversalTokenService.ValidDuoHost(Host))\n        {\n            results.Add(new ValidationResult(\"Host is invalid.\", [nameof(Host)]));\n        }\n        return results;\n    }\n}\n\npublic class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestModel, IValidatableObject\n{\n    public string Key1 { get; set; }\n    public string Key2 { get; set; }\n    public string Key3 { get; set; }\n    public string Key4 { get; set; }\n    public string Key5 { get; set; }\n    [Required]\n    public bool? Nfc { get; set; }\n\n    public User ToUser(User existingUser)\n    {\n        var providers = existingUser.GetTwoFactorProviders();\n        if (providers == null)\n        {\n            providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();\n        }\n        else\n        {\n            providers.Remove(TwoFactorProviderType.YubiKey);\n        }\n\n        providers.Add(TwoFactorProviderType.YubiKey, new TwoFactorProvider\n        {\n            MetaData = new Dictionary<string, object>\n            {\n                [\"Key1\"] = FormatKey(Key1),\n                [\"Key2\"] = FormatKey(Key2),\n                [\"Key3\"] = FormatKey(Key3),\n                [\"Key4\"] = FormatKey(Key4),\n                [\"Key5\"] = FormatKey(Key5),\n                [\"Nfc\"] = Nfc.Value\n            },\n            Enabled = true\n        });\n        existingUser.SetTwoFactorProviders(providers);\n        return existingUser;\n    }\n\n    private string FormatKey(string keyValue)\n    {\n        if (string.IsNullOrWhiteSpace(keyValue))\n        {\n            return null;\n        }\n\n        return keyValue.Substring(0, 12);\n    }\n\n    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (string.IsNullOrWhiteSpace(Key1) && string.IsNullOrWhiteSpace(Key2) && string.IsNullOrWhiteSpace(Key3) &&\n            string.IsNullOrWhiteSpace(Key4) && string.IsNullOrWhiteSpace(Key5))\n        {\n            yield return new ValidationResult(\"A key is required.\", new string[] { nameof(Key1) });\n        }\n\n        if (!string.IsNullOrWhiteSpace(Key1) && Key1.Length < 12)\n        {\n            yield return new ValidationResult(\"Key 1 in invalid.\", new string[] { nameof(Key1) });\n        }\n\n        if (!string.IsNullOrWhiteSpace(Key2) && Key2.Length < 12)\n        {\n            yield return new ValidationResult(\"Key 2 in invalid.\", new string[] { nameof(Key2) });\n        }\n\n        if (!string.IsNullOrWhiteSpace(Key3) && Key3.Length < 12)\n        {\n            yield return new ValidationResult(\"Key 3 in invalid.\", new string[] { nameof(Key3) });\n        }\n\n        if (!string.IsNullOrWhiteSpace(Key4) && Key4.Length < 12)\n        {\n            yield return new ValidationResult(\"Key 4 in invalid.\", new string[] { nameof(Key4) });\n        }\n\n        if (!string.IsNullOrWhiteSpace(Key5) && Key5.Length < 12)\n        {\n            yield return new ValidationResult(\"Key 5 in invalid.\", new string[] { nameof(Key5) });\n        }\n    }\n}\n\npublic class TwoFactorEmailRequestModel : SecretVerificationRequestModel\n{\n    [Required]\n    [EmailAddress]\n    [StringLength(256)]\n    public string Email { get; set; }\n    public string AuthRequestId { get; set; }\n    // An auth session token used for obtaining email and as an authN factor for the sending of emailed 2FA OTPs.\n    public string SsoEmail2FaSessionToken { get; set; }\n    public User ToUser(User existingUser)\n    {\n        var providers = existingUser.GetTwoFactorProviders();\n        if (providers == null)\n        {\n            providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();\n        }\n        else\n        {\n            providers.Remove(TwoFactorProviderType.Email);\n        }\n\n        providers.Add(TwoFactorProviderType.Email, new TwoFactorProvider\n        {\n            MetaData = new Dictionary<string, object> { [\"Email\"] = Email.ToLowerInvariant() },\n            Enabled = true\n        });\n        existingUser.SetTwoFactorProviders(providers);\n        return existingUser;\n    }\n\n    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (string.IsNullOrEmpty(Secret) && string.IsNullOrEmpty(AuthRequestAccessCode) && string.IsNullOrEmpty((SsoEmail2FaSessionToken)))\n        {\n            yield return new ValidationResult(\"MasterPasswordHash, OTP, AccessCode, or SsoEmail2faSessionToken must be supplied.\");\n        }\n    }\n}\n\npublic class TwoFactorWebAuthnRequestModel : TwoFactorWebAuthnDeleteRequestModel\n{\n    [Required]\n    public AuthenticatorAttestationRawResponse DeviceResponse { get; set; }\n    public string Name { get; set; }\n}\n\npublic class TwoFactorWebAuthnDeleteRequestModel : SecretVerificationRequestModel, IValidatableObject\n{\n    [Required]\n    public int? Id { get; set; }\n\n    public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        foreach (var validationResult in base.Validate(validationContext))\n        {\n            yield return validationResult;\n        }\n\n        if (!Id.HasValue)\n        {\n            yield return new ValidationResult(\"Invalid Key Id\", new string[] { nameof(Id) });\n        }\n    }\n}\n\npublic class UpdateTwoFactorEmailRequestModel : TwoFactorEmailRequestModel\n{\n    [Required]\n    [StringLength(50)]\n    public string Token { get; set; }\n}\n\npublic class TwoFactorProviderRequestModel : SecretVerificationRequestModel\n{\n    [Required]\n    public TwoFactorProviderType? Type { get; set; }\n}\n\npublic class TwoFactorRecoveryRequestModel : TwoFactorEmailRequestModel\n{\n    [Required]\n    [StringLength(32)]\n    public string RecoveryCode { get; set; }\n}\n\npublic class TwoFactorAuthenticatorDisableRequestModel : TwoFactorProviderRequestModel\n{\n    [Required]\n    public string UserVerificationToken { get; set; }\n    [Required]\n    public string Key { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/UntrustDevicesModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\n#nullable enable\n\nnamespace Bit.Api.Auth.Models.Request;\n\npublic class UntrustDevicesRequestModel\n{\n    [Required]\n    public IEnumerable<Guid> Devices { get; set; } = null!;\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/UpdateDevicesTrustRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Core.Auth.Models.Api.Request;\n\n#nullable enable\n\nnamespace Bit.Api.Auth.Models.Request;\n\npublic class UpdateDevicesTrustRequestModel : SecretVerificationRequestModel\n{\n    [Required]\n    public DeviceKeysUpdateRequestModel CurrentDevice { get; set; } = null!;\n    public IEnumerable<OtherDeviceKeysUpdateRequestModel>? OtherDevices { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\nusing Fido2NetLib;\n\nnamespace Bit.Api.Auth.Models.Request.WebAuthn;\n\npublic class WebAuthnLoginCredentialCreateRequestModel\n{\n    [Required]\n    public AuthenticatorAttestationRawResponse DeviceResponse { get; set; }\n\n    [Required]\n    public string Name { get; set; }\n\n    [Required]\n    public string Token { get; set; }\n\n    [Required]\n    public bool SupportsPrf { get; set; }\n\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedUserKey { get; set; }\n\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedPublicKey { get; set; }\n\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedPrivateKey { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\nusing Fido2NetLib;\n\nnamespace Bit.Api.Auth.Models.Request.WebAuthn;\n\npublic class WebAuthnLoginCredentialUpdateRequestModel\n{\n    [Required]\n    public AuthenticatorAssertionRawResponse DeviceResponse { get; set; }\n\n    [Required]\n    public string Token { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedUserKey { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedPublicKey { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedPrivateKey { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Auth.Models.Request.WebAuthn;\n\npublic class WebAuthnLoginRotateKeyRequestModel\n{\n    [Required]\n    public Guid Id { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedUserKey { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedPublicKey { get; set; }\n\n    public WebAuthnLoginRotateKeyData ToWebAuthnRotateKeyData()\n    {\n        return new WebAuthnLoginRotateKeyData\n        {\n            Id = Id,\n            EncryptedUserKey = EncryptedUserKey,\n            EncryptedPublicKey = EncryptedPublicKey\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/AuthRequestResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Reflection;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Auth.Models.Response;\n\npublic class AuthRequestResponseModel : ResponseModel\n{\n    public AuthRequestResponseModel(AuthRequest authRequest, string vaultUri, string obj = \"auth-request\")\n        : base(obj)\n    {\n        if (authRequest == null)\n        {\n            throw new ArgumentNullException(nameof(authRequest));\n        }\n\n        Id = authRequest.Id;\n        PublicKey = authRequest.PublicKey;\n        RequestDeviceIdentifier = authRequest.RequestDeviceIdentifier;\n        RequestDeviceTypeValue = authRequest.RequestDeviceType;\n        RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString())\n            .FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();\n        RequestIpAddress = authRequest.RequestIpAddress;\n        RequestCountryName = authRequest.RequestCountryName;\n        Key = authRequest.Key;\n        MasterPasswordHash = authRequest.MasterPasswordHash;\n        CreationDate = authRequest.CreationDate;\n        RequestApproved = authRequest.Approved ?? false;\n        Origin = new Uri(vaultUri).Host;\n        ResponseDate = authRequest.ResponseDate;\n    }\n\n    public Guid Id { get; set; }\n    public string PublicKey { get; set; }\n    public string RequestDeviceIdentifier { get; set; }\n    public DeviceType RequestDeviceTypeValue { get; set; }\n    public string RequestDeviceType { get; set; }\n    public string RequestIpAddress { get; set; }\n    public string RequestCountryName { get; set; }\n    public string Key { get; set; }\n    public string MasterPasswordHash { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime? ResponseDate { get; set; }\n    public bool RequestApproved { get; set; }\n    public string Origin { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Auth.Models.Response;\n\npublic class EmergencyAccessResponseModel : ResponseModel\n{\n    public EmergencyAccessResponseModel(EmergencyAccess emergencyAccess, string obj = \"emergencyAccess\") : base(obj)\n    {\n        if (emergencyAccess == null)\n        {\n            throw new ArgumentNullException(nameof(emergencyAccess));\n        }\n\n        Id = emergencyAccess.Id;\n        Status = emergencyAccess.Status;\n        Type = emergencyAccess.Type;\n        WaitTimeDays = emergencyAccess.WaitTimeDays;\n    }\n\n    public EmergencyAccessResponseModel(EmergencyAccessDetails emergencyAccess, string obj = \"emergencyAccess\") : base(obj)\n    {\n        if (emergencyAccess == null)\n        {\n            throw new ArgumentNullException(nameof(emergencyAccess));\n        }\n\n        Id = emergencyAccess.Id;\n        Status = emergencyAccess.Status;\n        Type = emergencyAccess.Type;\n        WaitTimeDays = emergencyAccess.WaitTimeDays;\n    }\n\n    public Guid Id { get; private set; }\n    public EmergencyAccessStatusType Status { get; private set; }\n    public EmergencyAccessType Type { get; private set; }\n    public int WaitTimeDays { get; private set; }\n}\n\npublic class EmergencyAccessGranteeDetailsResponseModel : EmergencyAccessResponseModel\n{\n    public EmergencyAccessGranteeDetailsResponseModel(EmergencyAccessDetails emergencyAccess)\n        : base(emergencyAccess, \"emergencyAccessGranteeDetails\")\n    {\n        if (emergencyAccess == null)\n        {\n            throw new ArgumentNullException(nameof(emergencyAccess));\n        }\n\n        GranteeId = emergencyAccess.GranteeId;\n        Email = emergencyAccess.GranteeEmail;\n        Name = emergencyAccess.GranteeName;\n        AvatarColor = emergencyAccess.GranteeAvatarColor;\n    }\n\n    public Guid? GranteeId { get; private set; }\n    public string Name { get; private set; }\n    public string Email { get; private set; }\n    public string AvatarColor { get; private set; }\n}\n\npublic class EmergencyAccessGrantorDetailsResponseModel : EmergencyAccessResponseModel\n{\n    public EmergencyAccessGrantorDetailsResponseModel(EmergencyAccessDetails emergencyAccess)\n        : base(emergencyAccess, \"emergencyAccessGrantorDetails\")\n    {\n        if (emergencyAccess == null)\n        {\n            throw new ArgumentNullException(nameof(emergencyAccess));\n        }\n\n        GrantorId = emergencyAccess.GrantorId;\n        Email = emergencyAccess.GrantorEmail;\n        Name = emergencyAccess.GrantorName;\n        AvatarColor = emergencyAccess.GrantorAvatarColor;\n    }\n\n    public Guid GrantorId { get; private set; }\n    public string Name { get; private set; }\n    public string Email { get; private set; }\n    public string AvatarColor { get; private set; }\n}\n\npublic class EmergencyAccessTakeoverResponseModel : ResponseModel\n{\n    /// <summary>\n    /// Creates a new instance of the <see cref=\"EmergencyAccessTakeoverResponseModel\"/> class.\n    /// </summary>\n    /// <param name=\"emergencyAccess\">Consumed for the Encrypted Key value</param>\n    /// <param name=\"grantor\">consumed for the KDF configuration</param>\n    /// <param name=\"obj\">name of the object</param>\n    /// <exception cref=\"ArgumentNullException\">emergencyAccess cannot be null</exception>\n    public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = \"emergencyAccessTakeover\") : base(obj)\n    {\n        if (emergencyAccess == null)\n        {\n            throw new ArgumentNullException(nameof(emergencyAccess));\n        }\n\n        KeyEncrypted = emergencyAccess.KeyEncrypted;\n        Kdf = grantor.Kdf;\n        KdfIterations = grantor.KdfIterations;\n        KdfMemory = grantor.KdfMemory;\n        KdfParallelism = grantor.KdfParallelism;\n        Salt = grantor.GetMasterPasswordSalt();\n    }\n\n    public int KdfIterations { get; private set; }\n    public int? KdfMemory { get; private set; }\n    public int? KdfParallelism { get; private set; }\n    public KdfType Kdf { get; private set; }\n    public string KeyEncrypted { get; private set; }\n    public string Salt { get; private set; }\n}\n\npublic class EmergencyAccessViewResponseModel : ResponseModel\n{\n    public EmergencyAccessViewResponseModel(\n        IGlobalSettings globalSettings,\n        EmergencyAccess emergencyAccess,\n        IEnumerable<CipherDetails> ciphers,\n        User user)\n        : base(\"emergencyAccessView\")\n    {\n        KeyEncrypted = emergencyAccess.KeyEncrypted;\n        Ciphers = ciphers.Select(cipher =>\n            new CipherResponseModel(\n                cipher,\n                user,\n                organizationAbilities: null, // Emergency access only retrieves personal ciphers so organizationAbilities is not needed\n                globalSettings));\n    }\n\n    public string KeyEncrypted { get; set; }\n    public IEnumerable<CipherResponseModel> Ciphers { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/OrganizationSsoResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Settings;\n\nnamespace Bit.Api.Auth.Models.Response.Organizations;\n\npublic class OrganizationSsoResponseModel : ResponseModel\n{\n    public OrganizationSsoResponseModel(Organization organization, GlobalSettings globalSettings,\n        SsoConfig config = null) : base(\"organizationSso\")\n    {\n        if (config != null)\n        {\n            Enabled = config.Enabled;\n            Data = config.GetData();\n        }\n\n        Identifier = organization.Identifier;\n        Urls = new SsoUrls(organization.Id.ToString(), globalSettings);\n    }\n\n    public bool Enabled { get; set; }\n    public string Identifier { get; set; }\n    public SsoConfigurationData Data { get; set; }\n    public SsoUrls Urls { get; set; }\n}\n\npublic class SsoUrls\n{\n    public SsoUrls(string organizationId, GlobalSettings globalSettings)\n    {\n        CallbackPath = SsoConfigurationData.BuildCallbackPath(globalSettings.BaseServiceUri.Sso);\n        SignedOutCallbackPath = SsoConfigurationData.BuildSignedOutCallbackPath(globalSettings.BaseServiceUri.Sso);\n        SpEntityIdStatic = SsoConfigurationData.BuildSaml2ModulePath(globalSettings.BaseServiceUri.Sso);\n        SpEntityId = SsoConfigurationData.BuildSaml2ModulePath(globalSettings.BaseServiceUri.Sso, organizationId);\n        SpMetadataUrl = SsoConfigurationData.BuildSaml2MetadataUrl(globalSettings.BaseServiceUri.Sso, organizationId);\n        SpAcsUrl = SsoConfigurationData.BuildSaml2AcsUrl(globalSettings.BaseServiceUri.Sso, organizationId);\n    }\n\n    public string CallbackPath { get; set; }\n    public string SignedOutCallbackPath { get; set; }\n    public string SpEntityId { get; set; }\n    public string SpEntityIdStatic { get; set; }\n    public string SpMetadataUrl { get; set; }\n    public string SpAcsUrl { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/PendingAuthRequestResponseModel.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\n\nnamespace Bit.Api.Auth.Models.Response;\n\npublic class PendingAuthRequestResponseModel : AuthRequestResponseModel\n{\n    public PendingAuthRequestResponseModel(PendingAuthRequestDetails authRequest, string vaultUri, string obj = \"auth-request\")\n        : base(authRequest, vaultUri, obj)\n    {\n        ArgumentNullException.ThrowIfNull(authRequest);\n        RequestDeviceId = authRequest.RequestDeviceId;\n    }\n\n    public Guid? RequestDeviceId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\nusing OtpNet;\n\nnamespace Bit.Api.Auth.Models.Response.TwoFactor;\n\npublic class TwoFactorAuthenticatorResponseModel : ResponseModel\n{\n    public TwoFactorAuthenticatorResponseModel(User user)\n        : base(\"twoFactorAuthenticator\")\n    {\n        ArgumentNullException.ThrowIfNull(user);\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);\n        if (provider?.MetaData?.TryGetValue(\"Key\", out var keyValue) ?? false)\n        {\n            Key = (string)keyValue;\n            Enabled = provider.Enabled;\n        }\n        else\n        {\n            var key = KeyGeneration.GenerateRandomKey(20);\n            Key = Base32Encoding.ToString(key);\n            Enabled = false;\n        }\n    }\n\n    public bool Enabled { get; set; }\n    public string Key { get; set; }\n    public string UserVerificationToken { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Auth.Models.Response.TwoFactor;\n\npublic class TwoFactorDuoResponseModel : ResponseModel\n{\n    private const string ResponseObj = \"twoFactorDuo\";\n\n    public TwoFactorDuoResponseModel(User user)\n        : base(ResponseObj)\n    {\n        ArgumentNullException.ThrowIfNull(user);\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);\n        Build(provider);\n    }\n\n    public TwoFactorDuoResponseModel(Organization organization)\n        : base(ResponseObj)\n    {\n        ArgumentNullException.ThrowIfNull(organization);\n\n        var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);\n        Build(provider);\n    }\n\n    public bool Enabled { get; set; }\n    public string Host { get; set; }\n    public string ClientSecret { get; set; }\n    public string ClientId { get; set; }\n\n    private void Build(TwoFactorProvider provider)\n    {\n        if (provider?.MetaData != null && provider.MetaData.Count > 0)\n        {\n            Enabled = provider.Enabled;\n\n            if (provider.MetaData.TryGetValue(\"Host\", out var host))\n            {\n                Host = (string)host;\n            }\n            if (provider.MetaData.TryGetValue(\"ClientSecret\", out var clientSecret))\n            {\n                ClientSecret = MaskSecret((string)clientSecret);\n            }\n            if (provider.MetaData.TryGetValue(\"ClientId\", out var clientId))\n            {\n                ClientId = (string)clientId;\n            }\n        }\n        else\n        {\n            Enabled = false;\n        }\n    }\n\n    private static string MaskSecret(string key)\n    {\n        if (string.IsNullOrWhiteSpace(key) || key.Length <= 6)\n        {\n            return key;\n        }\n\n        // Mask all but the first 6 characters.\n        return string.Concat(key.AsSpan(0, 6), new string('*', key.Length - 6));\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Auth.Models.Response.TwoFactor;\n\npublic class TwoFactorEmailResponseModel : ResponseModel\n{\n    public TwoFactorEmailResponseModel(User user)\n        : base(\"twoFactorEmail\")\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);\n        if (provider?.MetaData?.TryGetValue(\"Email\", out var email) ?? false)\n        {\n            Email = (string)email;\n            Enabled = provider.Enabled;\n        }\n        else\n        {\n            Enabled = false;\n        }\n    }\n\n    public bool Enabled { get; set; }\n    public string Email { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/TwoFactor/TwoFactorProviderResponseModel.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Auth.Models.Response.TwoFactor;\n\npublic class TwoFactorProviderResponseModel : ResponseModel\n{\n    private const string ResponseObj = \"twoFactorProvider\";\n\n    public TwoFactorProviderResponseModel(TwoFactorProviderType type, TwoFactorProvider provider)\n        : base(ResponseObj)\n    {\n        if (provider == null)\n        {\n            throw new ArgumentNullException(nameof(provider));\n        }\n\n        Enabled = provider.Enabled;\n        Type = type;\n    }\n\n    public TwoFactorProviderResponseModel(TwoFactorProviderType type, User user)\n        : base(ResponseObj)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        var provider = user.GetTwoFactorProvider(type);\n        Enabled = provider?.Enabled ?? false;\n        Type = type;\n    }\n\n    public TwoFactorProviderResponseModel(TwoFactorProviderType type, Organization organization)\n        : base(ResponseObj)\n    {\n        if (organization == null)\n        {\n            throw new ArgumentNullException(nameof(organization));\n        }\n\n        var provider = organization.GetTwoFactorProvider(type);\n        Enabled = provider?.Enabled ?? false;\n        Type = type;\n    }\n\n    public bool Enabled { get; set; }\n    public TwoFactorProviderType Type { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/TwoFactor/TwoFactorRecoverResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Auth.Models.Response.TwoFactor;\n\npublic class TwoFactorRecoverResponseModel : ResponseModel\n{\n    public TwoFactorRecoverResponseModel(User user)\n        : base(\"twoFactorRecover\")\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        Code = user.TwoFactorRecoveryCode;\n    }\n\n    public string Code { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Auth.Models.Response.TwoFactor;\n\npublic class TwoFactorWebAuthnResponseModel : ResponseModel\n{\n    public TwoFactorWebAuthnResponseModel(User user)\n        : base(\"twoFactorWebAuthn\")\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);\n        Enabled = provider?.Enabled ?? false;\n        Keys = provider?.MetaData?\n            .Where(k => k.Key.StartsWith(\"Key\"))\n            .Select(k => new KeyModel(k.Key, new TwoFactorProvider.WebAuthnData((dynamic)k.Value)));\n    }\n\n    public bool Enabled { get; set; }\n    public IEnumerable<KeyModel> Keys { get; set; }\n\n    public class KeyModel\n    {\n        public KeyModel(string id, TwoFactorProvider.WebAuthnData data)\n        {\n            Name = data.Name;\n            Id = Convert.ToInt32(id.Replace(\"Key\", string.Empty));\n            Migrated = data.Migrated;\n        }\n\n        public string Name { get; set; }\n        public int Id { get; set; }\n        public bool Migrated { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Auth.Models.Response.TwoFactor;\n\npublic class TwoFactorYubiKeyResponseModel : ResponseModel\n{\n    public TwoFactorYubiKeyResponseModel(User user)\n        : base(\"twoFactorYubiKey\")\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey);\n        if (provider?.MetaData != null && provider.MetaData.Count > 0)\n        {\n            Enabled = provider.Enabled;\n\n            if (provider.MetaData.TryGetValue(\"Key1\", out var key1))\n            {\n                Key1 = (string)key1;\n            }\n            if (provider.MetaData.TryGetValue(\"Key2\", out var key2))\n            {\n                Key2 = (string)key2;\n            }\n            if (provider.MetaData.TryGetValue(\"Key3\", out var key3))\n            {\n                Key3 = (string)key3;\n            }\n            if (provider.MetaData.TryGetValue(\"Key4\", out var key4))\n            {\n                Key4 = (string)key4;\n            }\n            if (provider.MetaData.TryGetValue(\"Key5\", out var key5))\n            {\n                Key5 = (string)key5;\n            }\n            if (provider.MetaData.TryGetValue(\"Nfc\", out var nfc))\n            {\n                Nfc = (bool)nfc;\n            }\n        }\n        else\n        {\n            Enabled = false;\n        }\n    }\n\n    public bool Enabled { get; set; }\n    public string Key1 { get; set; }\n    public string Key2 { get; set; }\n    public string Key3 { get; set; }\n    public string Key4 { get; set; }\n    public string Key5 { get; set; }\n    public bool Nfc { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialCreateOptionsResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Models.Api;\nusing Fido2NetLib;\n\nnamespace Bit.Api.Auth.Models.Response.WebAuthn;\n\npublic class WebAuthnCredentialCreateOptionsResponseModel : ResponseModel\n{\n    private const string ResponseObj = \"webauthnCredentialCreateOptions\";\n\n    public WebAuthnCredentialCreateOptionsResponseModel() : base(ResponseObj)\n    {\n    }\n\n    [Required]\n    public CredentialCreateOptions Options { get; set; }\n\n    [Required]\n    public string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Auth.Models.Response.WebAuthn;\n\npublic class WebAuthnCredentialResponseModel : ResponseModel\n{\n    private const string ResponseObj = \"webauthnCredential\";\n\n    public WebAuthnCredentialResponseModel(WebAuthnCredential credential) : base(ResponseObj)\n    {\n        Id = credential.Id.ToString();\n        Name = credential.Name;\n        PrfStatus = credential.GetPrfStatus();\n        EncryptedUserKey = credential.EncryptedUserKey;\n        EncryptedPublicKey = credential.EncryptedPublicKey;\n    }\n\n    public string Id { get; set; }\n    public string Name { get; set; }\n    public WebAuthnPrfStatus PrfStatus { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedUserKey { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedPublicKey { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Attributes/InjectOrganizationAttribute.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Repositories;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\n\nnamespace Bit.Api.Billing.Attributes;\n\n/// <summary>\n/// An action filter that facilitates the injection of a <see cref=\"Organization\"/> parameter into the executing action method arguments.\n/// </summary>\n/// <remarks>\n/// <para>This attribute retrieves the organization associated with the 'organizationId' included in the executing context's route data. If the organization cannot be found,\n/// the request is terminated with a not found response.</para>\n/// <para>The injected <see cref=\"Organization\"/>\n/// parameter must be marked with a [BindNever] attribute to short-circuit the model-binding system.</para>\n/// </remarks>\n/// <example>\n/// <code><![CDATA[\n/// [HttpPost]\n/// [InjectOrganization]\n/// public async Task<IResult> EndpointAsync([BindNever] Organization organization)\n/// ]]></code>\n/// </example>\n/// <seealso cref=\"Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute\"/>\npublic class InjectOrganizationAttribute : ActionFilterAttribute\n{\n    public override async Task OnActionExecutionAsync(\n        ActionExecutingContext context,\n        ActionExecutionDelegate next)\n    {\n        if (!context.RouteData.Values.TryGetValue(\"organizationId\", out var routeValue) ||\n            !Guid.TryParse(routeValue?.ToString(), out var organizationId))\n        {\n            context.Result = new BadRequestObjectResult(new ErrorResponseModel(\"Route parameter 'organizationId' is missing or invalid.\"));\n            return;\n        }\n\n        var organizationRepository = context.HttpContext.RequestServices\n            .GetRequiredService<IOrganizationRepository>();\n\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            context.Result = new NotFoundObjectResult(new ErrorResponseModel(\"Organization not found.\"));\n            return;\n        }\n\n        var organizationParameter = context.ActionDescriptor.Parameters\n            .FirstOrDefault(p => p.ParameterType == typeof(Organization));\n\n        if (organizationParameter != null)\n        {\n            context.ActionArguments[organizationParameter.Name] = organization;\n        }\n\n        await next();\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Attributes/InjectProviderAttribute.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\n\nnamespace Bit.Api.Billing.Attributes;\n\n/// <summary>\n/// An action filter that facilitates the injection of a <see cref=\"Provider\"/> parameter into the executing action method arguments after performing an authorization check.\n/// </summary>\n/// <remarks>\n/// <para>This attribute retrieves the provider associated with the 'providerId' included in the executing context's route data. If the provider cannot be found,\n/// the request is terminated with a not-found response. It then checks the authorization level for the provider using the provided <paramref name=\"providerUserType\"/>.\n/// If this check fails, the request is terminated with an unauthorized response.</para>\n/// <para>The injected <see cref=\"Provider\"/>\n/// parameter must be marked with a [BindNever] attribute to short-circuit the model-binding system.</para>\n/// </remarks>\n/// <example>\n/// <code><![CDATA[\n/// [HttpPost]\n/// [InjectProvider(ProviderUserType.ProviderAdmin)]\n/// public async Task<IResult> EndpointAsync([BindNever] Provider provider)\n/// ]]></code>\n/// </example>\n/// <param name=\"providerUserType\">The desired access level for the authorization check.</param>\n/// <seealso cref=\"Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute\"/>\npublic class InjectProviderAttribute(ProviderUserType providerUserType) : ActionFilterAttribute\n{\n    public override async Task OnActionExecutionAsync(\n        ActionExecutingContext context,\n        ActionExecutionDelegate next)\n    {\n        if (!context.RouteData.Values.TryGetValue(\"providerId\", out var routeValue) ||\n            !Guid.TryParse(routeValue?.ToString(), out var providerId))\n        {\n            context.Result = new BadRequestObjectResult(new ErrorResponseModel(\"Route parameter 'providerId' is missing or invalid.\"));\n            return;\n        }\n\n        var providerRepository = context.HttpContext.RequestServices\n            .GetRequiredService<IProviderRepository>();\n\n        var provider = await providerRepository.GetByIdAsync(providerId);\n\n        if (provider == null)\n        {\n            context.Result = new NotFoundObjectResult(new ErrorResponseModel(\"Provider not found.\"));\n            return;\n        }\n\n        var currentContext = context.HttpContext.RequestServices.GetRequiredService<ICurrentContext>();\n\n        var unauthorized = providerUserType switch\n        {\n            ProviderUserType.ProviderAdmin => !currentContext.ProviderProviderAdmin(providerId),\n            ProviderUserType.ServiceUser => !currentContext.ProviderUser(providerId),\n            _ => false\n        };\n\n        if (unauthorized)\n        {\n            context.Result = new UnauthorizedObjectResult(new ErrorResponseModel(\"Unauthorized.\"));\n            return;\n        }\n\n        var providerParameter = context.ActionDescriptor.Parameters\n            .FirstOrDefault(p => p.ParameterType == typeof(Provider));\n\n        if (providerParameter != null)\n        {\n            context.ActionArguments[providerParameter.Name] = provider;\n        }\n\n        await next();\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Attributes/InjectUserAttribute.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\n\nnamespace Bit.Api.Billing.Attributes;\n\n/// <summary>\n/// An action filter that facilitates the injection of a <see cref=\"User\"/> parameter into the executing action method arguments.\n/// </summary>\n/// <remarks>\n/// <para>This attribute retrieves the authorized user associated with the current HTTP context using the <see cref=\"IUserService\"/> service.\n/// If the user is unauthorized or cannot be found, the request is terminated with an unauthorized response.</para>\n/// <para>The injected <see cref=\"User\"/>\n/// parameter must be marked with a [BindNever] attribute to short-circuit the model-binding system.</para>\n/// </remarks>\n/// <example>\n/// <code><![CDATA[\n/// [HttpPost]\n/// [InjectUser]\n/// public async Task<IResult> EndpointAsync([BindNever] User user)\n/// ]]></code>\n/// </example>\n/// <seealso cref=\"ActionFilterAttribute\"/>\npublic class InjectUserAttribute : ActionFilterAttribute\n{\n    public override async Task OnActionExecutionAsync(\n        ActionExecutingContext context,\n        ActionExecutionDelegate next)\n    {\n        var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();\n\n        var user = await userService.GetUserByPrincipalAsync(context.HttpContext.User);\n\n        if (user == null)\n        {\n            context.Result = new UnauthorizedObjectResult(new ErrorResponseModel(\"Unauthorized.\"));\n            return;\n        }\n\n        var userParameter =\n            context.ActionDescriptor.Parameters.FirstOrDefault(parameter => parameter.ParameterType == typeof(User));\n\n        if (userParameter != null)\n        {\n            context.ActionArguments[userParameter.Name] = user;\n        }\n\n        await next();\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Attributes/NonTokenizedPaymentMethodTypeValidationAttribute.cs",
    "content": "﻿using Bit.Api.Utilities;\n\nnamespace Bit.Api.Billing.Attributes;\n\npublic class NonTokenizedPaymentMethodTypeValidationAttribute : StringMatchesAttribute\n{\n    private static readonly string[] _acceptedValues = [\"accountCredit\"];\n\n    public NonTokenizedPaymentMethodTypeValidationAttribute() : base(_acceptedValues)\n    {\n        ErrorMessage = $\"Payment method type must be one of: {string.Join(\", \", _acceptedValues)}\";\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Attributes/TokenizedPaymentMethodTypeValidationAttribute.cs",
    "content": "﻿using Bit.Api.Utilities;\n\nnamespace Bit.Api.Billing.Attributes;\n\npublic class TokenizedPaymentMethodTypeValidationAttribute : StringMatchesAttribute\n{\n    private static readonly string[] _acceptedValues = [\"bankAccount\", \"card\", \"payPal\"];\n\n    public TokenizedPaymentMethodTypeValidationAttribute() : base(_acceptedValues)\n    {\n        ErrorMessage = $\"Payment method type must be one of: {string.Join(\", \", _acceptedValues)}\";\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/AccountsBillingController.cs",
    "content": "﻿using Bit.Api.Billing.Models.Responses;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Route(\"accounts/billing\")]\n[Authorize(\"Application\")]\npublic class AccountsBillingController(\n    IStripePaymentService paymentService,\n    IUserService userService,\n    IPaymentHistoryService paymentHistoryService) : Controller\n{\n    // TODO: Migrate to Query / AccountBillingVNextController\n    [HttpGet(\"history\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<BillingHistoryResponseModel> GetBillingHistoryAsync()\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var billingInfo = await paymentService.GetBillingHistoryAsync(user);\n        return new BillingHistoryResponseModel(billingInfo);\n    }\n\n    // TODO: Migrate to Query / AccountBillingVNextController\n    [HttpGet(\"invoices\")]\n    public async Task<IResult> GetInvoicesAsync([FromQuery] string? status = null, [FromQuery] string? startAfter = null)\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var invoices = await paymentHistoryService.GetInvoiceHistoryAsync(\n            user,\n            5,\n            status,\n            startAfter);\n\n        return TypedResults.Ok(invoices);\n    }\n\n    // TODO: Migrate to Query / AccountBillingVNextController\n    [HttpGet(\"transactions\")]\n    public async Task<IResult> GetTransactionsAsync([FromQuery] DateTime? startAfter = null)\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var transactions = await paymentHistoryService.GetTransactionHistoryAsync(\n            user,\n            5,\n            startAfter);\n\n        return TypedResults.Ok(transactions);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/AccountsController.cs",
    "content": "﻿using Bit.Api.Models.Request;\nusing Bit.Api.Models.Request.Accounts;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Utilities;\nusing Bit.Core;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Route(\"accounts\")]\n[Authorize(\"Application\")]\npublic class AccountsController(\n    IUserService userService,\n    IFeatureService featureService,\n    ILicensingService licensingService) : Controller\n{\n    // TODO: Remove with deletion of pm-29594-update-individual-subscription-page\n    [HttpGet(\"subscription\")]\n    public async Task<SubscriptionResponseModel> GetSubscriptionAsync(\n        [FromServices] GlobalSettings globalSettings,\n        [FromServices] IStripePaymentService paymentService)\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        // Only cloud-hosted users with payment gateways have subscription and discount information\n        if (!globalSettings.SelfHosted)\n        {\n            if (user.Gateway != null)\n            {\n                // Note: PM23341_Milestone_2 is the feature flag for the overall Milestone 2 initiative (PM-23341).\n                // This specific implementation (PM-26682) adds discount display functionality as part of that initiative.\n                // The feature flag controls the broader Milestone 2 feature set, not just this specific task.\n                var includeMilestone2Discount = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);\n                var subscriptionInfo = await paymentService.GetSubscriptionAsync(user);\n                var license = await userService.GenerateLicenseAsync(user, subscriptionInfo);\n                var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);\n                return new SubscriptionResponseModel(user, subscriptionInfo, license, claimsPrincipal, includeMilestone2Discount);\n            }\n            else\n            {\n                var license = await userService.GenerateLicenseAsync(user);\n                var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);\n                return new SubscriptionResponseModel(user, null, license, claimsPrincipal);\n            }\n        }\n        else\n        {\n            return new SubscriptionResponseModel(user);\n        }\n    }\n\n    // TODO: Remove with deletion of pm-29594-update-individual-subscription-page\n    [HttpPost(\"storage\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<PaymentResponseModel> PostStorageAsync([FromBody] StorageRequestModel model)\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await userService.AdjustStorageAsync(user, model.StorageGbAdjustment!.Value);\n        return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };\n    }\n\n    /*\n     * TODO: A new version of this exists in the AccountBillingVNextController.\n     * The individual-self-hosting-license-uploader.component needs to be updated to use it.\n     * Then, this can be removed.\n     */\n    [HttpPost(\"license\")]\n    [SelfHosted(SelfHostedOnly = true)]\n    public async Task PostLicenseAsync(LicenseRequestModel model)\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, model.License);\n        if (license == null)\n        {\n            throw new BadRequestException(\"Invalid license\");\n        }\n\n        await userService.UpdateLicenseAsync(user, license);\n    }\n\n    // TODO: Migrate to Command / AccountBillingVNextController as DELETE /account/billing/vnext/subscription\n    [HttpPost(\"cancel\")]\n    public async Task PostCancelAsync(\n        [FromBody] SubscriptionCancellationRequestModel request,\n        [FromServices] ISubscriberService subscriberService)\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        await subscriberService.CancelSubscription(user,\n            new OffboardingSurveyResponse { UserId = user.Id, Reason = request.Reason, Feedback = request.Feedback },\n            user.IsExpired());\n    }\n\n    // TODO: Remove with deletion of pm-29594-update-individual-subscription-page\n    [HttpPost(\"reinstate-premium\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task PostReinstateAsync()\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        await userService.ReinstatePremiumAsync(user);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/BaseBillingController.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Models.Api;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Billing.Controllers;\n\npublic abstract class BaseBillingController : Controller\n{\n    /// <summary>\n    /// Processes the result of a billing command and converts it to an appropriate HTTP result response.\n    /// </summary>\n    /// <remarks>\n    /// Result to response mappings:\n    /// <list type=\"bullet\">\n    /// <item><description><typeparamref name=\"T\"/>: 200 OK</description></item>\n    /// <item><description><see cref=\"Core.Billing.Commands.BadRequest\"/>: 400 BAD_REQUEST</description></item>\n    /// <item><description><see cref=\"Core.Billing.Commands.Conflict\"/>: 409 CONFLICT</description></item>\n    /// <item><description><see cref=\"Unhandled\"/>: 500 INTERNAL_SERVER_ERROR</description></item>\n    /// </list>\n    /// </remarks>\n    /// <typeparam name=\"T\">The type of the successful result.</typeparam>\n    /// <param name=\"result\">The result of executing the billing command.</param>\n    /// <returns>An HTTP result response representing the outcome of the command execution.</returns>\n    protected static IResult Handle<T>(BillingCommandResult<T> result) =>\n        result.Match<IResult>(\n            TypedResults.Ok,\n            badRequest => Error.BadRequest(badRequest.Response),\n            conflict => Error.Conflict(conflict.Response),\n            unhandled => Error.ServerError(unhandled.Response, unhandled.Exception));\n\n    protected static class Error\n    {\n        public static BadRequest<ErrorResponseModel> BadRequest(string message) =>\n            TypedResults.BadRequest(new ErrorResponseModel(message));\n\n        public static JsonHttpResult<ErrorResponseModel> Conflict(string message) =>\n            TypedResults.Json(\n                new ErrorResponseModel(message),\n                statusCode: StatusCodes.Status409Conflict);\n\n        public static NotFound<ErrorResponseModel> NotFound() =>\n            TypedResults.NotFound(new ErrorResponseModel(\"Resource not found.\"));\n\n        public static JsonHttpResult<ErrorResponseModel> ServerError(\n            string message = \"Something went wrong with your request. Please contact support for assistance.\",\n            Exception? exception = null) =>\n            TypedResults.Json(\n                exception == null ? new ErrorResponseModel(message) : new ErrorResponseModel(message)\n                {\n                    ExceptionMessage = exception.Message,\n                    ExceptionStackTrace = exception.StackTrace\n                },\n                statusCode: StatusCodes.Status500InternalServerError);\n\n        public static JsonHttpResult<ErrorResponseModel> Unauthorized(string message = \"Unauthorized.\") =>\n            TypedResults.Json(\n                new ErrorResponseModel(message),\n                statusCode: StatusCodes.Status401Unauthorized);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/BaseProviderController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Context;\nusing Bit.Core.Services;\n\nnamespace Bit.Api.Billing.Controllers;\n\npublic abstract class BaseProviderController(\n    ICurrentContext currentContext,\n    ILogger<BaseProviderController> logger,\n    IProviderRepository providerRepository,\n    IUserService userService) : BaseBillingController\n{\n    protected readonly IUserService UserService = userService;\n\n    protected Task<(Provider, IResult)> TryGetBillableProviderForAdminOperation(\n        Guid providerId) => TryGetBillableProviderAsync(providerId, currentContext.ProviderProviderAdmin);\n\n    protected Task<(Provider, IResult)> TryGetBillableProviderForServiceUserOperation(\n        Guid providerId) => TryGetBillableProviderAsync(providerId, currentContext.ProviderUser);\n\n    private async Task<(Provider, IResult)> TryGetBillableProviderAsync(\n        Guid providerId,\n        Func<Guid, bool> checkAuthorization)\n    {\n        var provider = await providerRepository.GetByIdAsync(providerId);\n\n        if (provider == null)\n        {\n            logger.LogError(\n                \"Cannot find provider ({ProviderID}) for Consolidated Billing operation\",\n                providerId);\n\n            return (null, Error.NotFound());\n        }\n\n        if (!checkAuthorization(providerId))\n        {\n            var user = await UserService.GetUserByPrincipalAsync(User);\n\n            logger.LogError(\n                \"User ({UserID}) is not authorized to perform Consolidated Billing operation for provider ({ProviderID})\",\n                user?.Id, providerId);\n\n            return (null, Error.Unauthorized());\n        }\n\n        if (!provider.IsBillable())\n        {\n            logger.LogError(\n                \"Cannot run Consolidated Billing operation for provider ({ProviderID}) that is not billable\",\n                providerId);\n\n            return (null, Error.Unauthorized());\n        }\n\n        if (provider.IsStripeEnabled())\n        {\n            return (provider, null);\n        }\n\n        logger.LogError(\n            \"Cannot run Consolidated Billing operation for provider ({ProviderID}) that is missing Stripe configuration\",\n            providerId);\n\n        return (null, Error.ServerError());\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/LicensesController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api.OrganizationLicenses;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Route(\"licenses\")]\n[Authorize(\"Licensing\")]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class LicensesController : Controller\n{\n    private readonly IUserRepository _userRepository;\n    private readonly IUserService _userService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery;\n    private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand;\n    private readonly ICurrentContext _currentContext;\n\n    public LicensesController(\n        IUserRepository userRepository,\n        IUserService userService,\n        IOrganizationRepository organizationRepository,\n        IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery,\n        IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand,\n        ICurrentContext currentContext)\n    {\n        _userRepository = userRepository;\n        _userService = userService;\n        _organizationRepository = organizationRepository;\n        _getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery;\n        _validateBillingSyncKeyCommand = validateBillingSyncKeyCommand;\n        _currentContext = currentContext;\n    }\n\n    [HttpGet(\"user/{id}\")]\n    public async Task<UserLicense> GetUser(string id, [FromQuery] string key)\n    {\n        var user = await _userRepository.GetByIdAsync(new Guid(id));\n        if (user == null)\n        {\n            return null;\n        }\n        else if (!user.LicenseKey.Equals(key))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"Invalid license key.\");\n        }\n\n        var license = await _userService.GenerateLicenseAsync(user, null);\n        return license;\n    }\n\n    /// <summary>\n    /// Used by self-hosted installations to get an updated license file\n    /// </summary>\n    [HttpGet(\"organization/{id}\")]\n    public async Task<OrganizationLicense> OrganizationSync(string id, [FromBody] SelfHostedOrganizationLicenseRequestModel model)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(new Guid(id));\n        if (organization == null)\n        {\n            throw new NotFoundException(\"Organization not found.\");\n        }\n\n        if (!organization.LicenseKey.Equals(model.LicenseKey))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"Invalid license key.\");\n        }\n\n        if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(organization, model.BillingSyncKey))\n        {\n            throw new BadRequestException(\"Invalid Billing Sync Key\");\n        }\n\n        var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value);\n        return license;\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/OrganizationBillingController.cs",
    "content": "﻿using Bit.Api.Billing.Models.Requests;\nusing Bit.Api.Billing.Models.Responses;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Route(\"organizations/{organizationId:guid}/billing\")]\n[Authorize(\"Application\")]\npublic class OrganizationBillingController(\n    IBusinessUnitConverter businessUnitConverter,\n    ICurrentContext currentContext,\n    IOrganizationBillingService organizationBillingService,\n    IOrganizationRepository organizationRepository,\n    IStripePaymentService paymentService,\n    IPaymentHistoryService paymentHistoryService) : BaseBillingController\n{\n    // TODO: Migrate to Query / OrganizationBillingVNextController\n    [HttpGet(\"history\")]\n    public async Task<IResult> GetHistoryAsync([FromRoute] Guid organizationId)\n    {\n        if (!await currentContext.ViewBillingHistory(organizationId))\n        {\n            return Error.Unauthorized();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            return Error.NotFound();\n        }\n\n        var billingInfo = await paymentService.GetBillingHistoryAsync(organization);\n\n        return TypedResults.Ok(billingInfo);\n    }\n\n    // TODO: Migrate to Query / OrganizationBillingVNextController\n    [HttpGet(\"invoices\")]\n    public async Task<IResult> GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null)\n    {\n        if (!await currentContext.ViewBillingHistory(organizationId))\n        {\n            return TypedResults.Unauthorized();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            return TypedResults.NotFound();\n        }\n\n        var invoices = await paymentHistoryService.GetInvoiceHistoryAsync(\n            organization,\n            5,\n            status,\n            startAfter);\n\n        return TypedResults.Ok(invoices);\n    }\n\n    // TODO: Migrate to Query / OrganizationBillingVNextController\n    [HttpGet(\"transactions\")]\n    public async Task<IResult> GetTransactionsAsync([FromRoute] Guid organizationId, [FromQuery] DateTime? startAfter = null)\n    {\n        if (!await currentContext.ViewBillingHistory(organizationId))\n        {\n            return TypedResults.Unauthorized();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            return TypedResults.NotFound();\n        }\n\n        var transactions = await paymentHistoryService.GetTransactionHistoryAsync(\n            organization,\n            5,\n            startAfter);\n\n        return TypedResults.Ok(transactions);\n    }\n\n    // TODO: Can be removed once we do away with the organization-plans.component.\n    [HttpGet]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IResult> GetBillingAsync(Guid organizationId)\n    {\n        if (!await currentContext.ViewBillingHistory(organizationId))\n        {\n            return Error.Unauthorized();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            return Error.NotFound();\n        }\n\n        var billingInfo = await paymentService.GetBillingAsync(organization);\n\n        var response = new BillingResponseModel(billingInfo);\n\n        return TypedResults.Ok(response);\n    }\n\n    // TODO: Migrate to Command / OrganizationBillingVNextController\n    [HttpPost(\"setup-business-unit\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IResult> SetupBusinessUnitAsync(\n        [FromRoute] Guid organizationId,\n        [FromBody] SetupBusinessUnitRequestBody requestBody)\n    {\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            return Error.NotFound();\n        }\n\n        if (!await currentContext.OrganizationUser(organizationId))\n        {\n            return Error.Unauthorized();\n        }\n\n        var providerId = await businessUnitConverter.FinalizeConversion(\n            organization,\n            requestBody.UserId,\n            requestBody.Token,\n            requestBody.ProviderKey,\n            requestBody.OrganizationKey);\n\n        return TypedResults.Ok(providerId);\n    }\n\n    // TODO: Migrate to Command / OrganizationBillingVNextController\n    [HttpPost(\"change-frequency\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IResult> ChangePlanSubscriptionFrequencyAsync(\n        [FromRoute] Guid organizationId,\n        [FromBody] ChangePlanFrequencyRequest request)\n    {\n        if (!await currentContext.EditSubscription(organizationId))\n        {\n            return Error.Unauthorized();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            return Error.NotFound();\n        }\n\n        if (organization.PlanType == request.NewPlanType)\n        {\n            return Error.BadRequest(\"Organization is already on the requested plan frequency.\");\n        }\n\n        await organizationBillingService.UpdateSubscriptionPlanFrequency(\n            organization,\n            request.NewPlanType);\n\n        return TypedResults.Ok();\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Models.Response.Organizations;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api.Request.OrganizationSponsorships;\nusing Bit.Core.Models.Api.Response.OrganizationSponsorships;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Route(\"organization/sponsorship\")]\npublic class OrganizationSponsorshipsController : Controller\n{\n    private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand;\n    private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand;\n    private readonly ICreateSponsorshipCommand _createSponsorshipCommand;\n    private readonly ISendSponsorshipOfferCommand _sendSponsorshipOfferCommand;\n    private readonly ISetUpSponsorshipCommand _setUpSponsorshipCommand;\n    private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;\n    private readonly IRemoveSponsorshipCommand _removeSponsorshipCommand;\n    private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand;\n    private readonly ICurrentContext _currentContext;\n    private readonly IUserService _userService;\n    private readonly IPolicyQuery _policyQuery;\n    private readonly IFeatureService _featureService;\n\n    public OrganizationSponsorshipsController(\n        IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IValidateRedemptionTokenCommand validateRedemptionTokenCommand,\n        IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand,\n        ICreateSponsorshipCommand createSponsorshipCommand,\n        ISendSponsorshipOfferCommand sendSponsorshipOfferCommand,\n        ISetUpSponsorshipCommand setUpSponsorshipCommand,\n        IRevokeSponsorshipCommand revokeSponsorshipCommand,\n        IRemoveSponsorshipCommand removeSponsorshipCommand,\n        ICloudSyncSponsorshipsCommand syncSponsorshipsCommand,\n        IUserService userService,\n        ICurrentContext currentContext,\n        IPolicyQuery policyQuery,\n        IFeatureService featureService)\n    {\n        _organizationSponsorshipRepository = organizationSponsorshipRepository;\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _validateRedemptionTokenCommand = validateRedemptionTokenCommand;\n        _validateBillingSyncKeyCommand = validateBillingSyncKeyCommand;\n        _createSponsorshipCommand = createSponsorshipCommand;\n        _sendSponsorshipOfferCommand = sendSponsorshipOfferCommand;\n        _setUpSponsorshipCommand = setUpSponsorshipCommand;\n        _revokeSponsorshipCommand = revokeSponsorshipCommand;\n        _removeSponsorshipCommand = removeSponsorshipCommand;\n        _syncSponsorshipsCommand = syncSponsorshipsCommand;\n        _userService = userService;\n        _currentContext = currentContext;\n        _policyQuery = policyQuery;\n        _featureService = featureService;\n    }\n\n    [Authorize(\"Application\")]\n    [HttpPost(\"{sponsoringOrgId}/families-for-enterprise\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)\n    {\n        var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);\n        var freeFamiliesSponsorshipPolicy = await _policyQuery.RunAsync(sponsoringOrgId,\n            PolicyType.FreeFamiliesSponsorshipPolicy);\n\n        if (freeFamiliesSponsorshipPolicy.Enabled)\n        {\n            throw new BadRequestException(\"Free Bitwarden Families sponsorship has been disabled by your organization administrator.\");\n        }\n\n        var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(\n            sponsoringOrg,\n            await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),\n            model.PlanSponsorshipType,\n            model.SponsoredEmail,\n            model.FriendlyName,\n            model.IsAdminInitiated.GetValueOrDefault(),\n            model.Notes);\n        if (sponsorship.OfferedToEmail != null)\n        {\n            await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);\n        }\n    }\n\n    [Authorize(\"Application\")]\n    [Authorize<ManageUsersRequirement>]\n    [HttpPost(\"{organizationId}/families-for-enterprise/resend\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task ResendSponsorshipOffer([FromRoute(Name = \"organizationId\")] Guid sponsoringOrgId, [FromQuery] string sponsoredFriendlyName)\n    {\n        var freeFamiliesSponsorshipPolicy = await _policyQuery.RunAsync(sponsoringOrgId,\n            PolicyType.FreeFamiliesSponsorshipPolicy);\n\n        if (freeFamiliesSponsorshipPolicy.Enabled)\n        {\n            throw new BadRequestException(\"Free Bitwarden Families sponsorship has been disabled by your organization administrator.\");\n        }\n\n        var sponsoringOrgUser = await _organizationUserRepository\n            .GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);\n\n        var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);\n        var filteredSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));\n        if (filteredSponsorship != null)\n        {\n            await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(\n                await _organizationRepository.GetByIdAsync(sponsoringOrgId),\n                sponsoringOrgUser, filteredSponsorship);\n        }\n    }\n\n    [Authorize(\"Application\")]\n    [HttpPost(\"validate-token\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<PreValidateSponsorshipResponseModel> PreValidateSponsorshipToken([FromQuery] string sponsorshipToken)\n    {\n        var isFreeFamilyPolicyEnabled = false;\n        var (isValid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);\n        if (isValid && sponsorship.SponsoringOrganizationId.HasValue)\n        {\n            var policy = await _policyQuery.RunAsync(sponsorship.SponsoringOrganizationId.Value,\n                PolicyType.FreeFamiliesSponsorshipPolicy);\n            isFreeFamilyPolicyEnabled = policy.Enabled;\n        }\n\n        var response = PreValidateSponsorshipResponseModel.From(isValid, isFreeFamilyPolicyEnabled);\n\n        return response;\n    }\n\n    [Authorize(\"Application\")]\n    [HttpPost(\"redeem\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model)\n    {\n        var (valid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);\n\n        if (!valid)\n        {\n            throw new BadRequestException(\"Failed to parse sponsorship token.\");\n        }\n\n        if (!await _currentContext.OrganizationOwner(model.SponsoredOrganizationId))\n        {\n            throw new BadRequestException(\"Can only redeem sponsorship for an organization you own.\");\n        }\n\n        var freeFamiliesSponsorshipPolicy = await _policyQuery.RunAsync(\n            model.SponsoredOrganizationId, PolicyType.FreeFamiliesSponsorshipPolicy);\n\n        if (freeFamiliesSponsorshipPolicy.Enabled)\n        {\n            throw new BadRequestException(\"Free Bitwarden Families sponsorship has been disabled by your organization administrator.\");\n        }\n\n        await _setUpSponsorshipCommand.SetUpSponsorshipAsync(\n            sponsorship,\n            await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId));\n    }\n\n    [Authorize(\"Installation\")]\n    [HttpPost(\"sync\")]\n    public async Task<OrganizationSponsorshipSyncResponseModel> Sync([FromBody] OrganizationSponsorshipSyncRequestModel model)\n    {\n        var sponsoringOrg = await _organizationRepository.GetByIdAsync(model.SponsoringOrganizationCloudId);\n        if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(sponsoringOrg, model.BillingSyncKey))\n        {\n            throw new BadRequestException(\"Invalid Billing Sync Key\");\n        }\n\n        var (syncResponseData, offersToSend) = await _syncSponsorshipsCommand.SyncOrganization(sponsoringOrg, model.ToOrganizationSponsorshipSync().SponsorshipsBatch);\n        await _sendSponsorshipOfferCommand.BulkSendSponsorshipOfferAsync(sponsoringOrg.DisplayName(), offersToSend);\n        return new OrganizationSponsorshipSyncResponseModel(syncResponseData);\n    }\n\n    [Authorize(\"Application\")]\n    [HttpDelete(\"{sponsoringOrganizationId}\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task RevokeSponsorship(Guid sponsoringOrganizationId)\n    {\n\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default);\n        if (_currentContext.UserId != orgUser?.UserId)\n        {\n            throw new BadRequestException(\"Can only revoke a sponsorship you granted.\");\n        }\n\n        var existingOrgSponsorship = await _organizationSponsorshipRepository\n            .GetBySponsoringOrganizationUserIdAsync(orgUser.Id);\n\n        await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);\n    }\n\n    [Authorize(\"Application\")]\n    [HttpPost(\"{sponsoringOrganizationId}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE /{sponsoringOrganizationId} instead.\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task PostRevokeSponsorship(Guid sponsoringOrganizationId)\n    {\n        await RevokeSponsorship(sponsoringOrganizationId);\n    }\n\n    [Authorize(\"Application\")]\n    [Authorize<ManageUsersRequirement>]\n    [HttpDelete(\"{organizationId}/{sponsoredFriendlyName}/revoke\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task AdminInitiatedRevokeSponsorshipAsync([FromRoute(Name = \"organizationId\")] Guid sponsoringOrgId, string sponsoredFriendlyName)\n    {\n        var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);\n        var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));\n        if (existingOrgSponsorship == null)\n        {\n            throw new BadRequestException(\"The specified sponsored organization could not be found under the given sponsoring organization.\");\n        }\n        await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);\n    }\n\n    [Authorize(\"Application\")]\n    [HttpDelete(\"sponsored/{sponsoredOrgId}\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task RemoveSponsorship(Guid sponsoredOrgId)\n    {\n\n        if (!await _currentContext.OrganizationOwner(sponsoredOrgId))\n        {\n            throw new BadRequestException(\"Only the owner of an organization can remove sponsorship.\");\n        }\n\n        var existingOrgSponsorship = await _organizationSponsorshipRepository\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrgId);\n\n        await _removeSponsorshipCommand.RemoveSponsorshipAsync(existingOrgSponsorship);\n    }\n\n    [Authorize(\"Application\")]\n    [HttpPost(\"sponsored/{sponsoredOrgId}/remove\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE /sponsored/{sponsoredOrgId} instead.\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task PostRemoveSponsorship(Guid sponsoredOrgId)\n    {\n        await RemoveSponsorship(sponsoredOrgId);\n    }\n\n    [HttpGet(\"{sponsoringOrgId}/sync-status\")]\n    public async Task<object> GetSyncStatus(Guid sponsoringOrgId)\n    {\n        var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);\n\n        if (!await _currentContext.OrganizationOwner(sponsoringOrg.Id))\n        {\n            throw new NotFoundException();\n        }\n\n        var lastSyncDate = await _organizationSponsorshipRepository.GetLatestSyncDateBySponsoringOrganizationIdAsync(sponsoringOrg.Id);\n        return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate);\n    }\n\n    [Authorize(\"Application\")]\n    [HttpGet(\"{sponsoringOrgId}/sponsored\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<ListResponseModel<OrganizationSponsorshipInvitesResponseModel>> GetSponsoredOrganizations(Guid sponsoringOrgId)\n    {\n        var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);\n        if (sponsoringOrg == null)\n        {\n            throw new NotFoundException();\n        }\n        var organization = _currentContext.Organizations.First(x => x.Id == sponsoringOrg.Id);\n        if (!await _currentContext.OrganizationOwner(sponsoringOrg.Id) && !await _currentContext.OrganizationAdmin(sponsoringOrg.Id) && !organization.Permissions.ManageUsers)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);\n        return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(\n            sponsorships\n                .Where(s => s.IsAdminInitiated)\n                .Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))\n        );\n\n    }\n\n    private Task<User> CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value);\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/OrganizationsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Models.Request;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Organizations.Entities;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Billing.Organizations.Repositories;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Route(\"organizations\")]\n[Authorize(\"Application\")]\npublic class OrganizationsController(\n    IOrganizationRepository organizationRepository,\n    IOrganizationUserRepository organizationUserRepository,\n    IOrganizationService organizationService,\n    IUserService userService,\n    IStripePaymentService paymentService,\n    ICurrentContext currentContext,\n    IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery,\n    GlobalSettings globalSettings,\n    ILicensingService licensingService,\n    IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,\n    IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,\n    IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,\n    ISubscriberService subscriberService,\n    IOrganizationInstallationRepository organizationInstallationRepository,\n    IPricingClient pricingClient)\n    : Controller\n{\n    [HttpGet(\"{id:guid}/subscription\")]\n    public async Task<OrganizationSubscriptionResponseModel> GetSubscription(Guid id)\n    {\n        if (!await currentContext.ViewSubscription(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (globalSettings.SelfHosted)\n        {\n            var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);\n            var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(orgLicense);\n            return new OrganizationSubscriptionResponseModel(organization, orgLicense, claimsPrincipal);\n        }\n\n        var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        if (string.IsNullOrEmpty(organization.GatewaySubscriptionId))\n        {\n            return new OrganizationSubscriptionResponseModel(organization, plan);\n        }\n\n        var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);\n        if (subscriptionInfo == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var hideSensitiveData = !await currentContext.EditSubscription(id);\n\n        return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, plan, hideSensitiveData);\n    }\n\n    [HttpGet(\"{id:guid}/license\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<OrganizationLicense> GetLicense(Guid id, [FromQuery] Guid installationId)\n    {\n        if (!await currentContext.OrganizationOwner(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var org = await organizationRepository.GetByIdAsync(id);\n        var license = await getCloudOrganizationLicenseQuery.GetLicenseAsync(org, installationId);\n        if (license == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await SaveOrganizationInstallationAsync(id, installationId);\n\n        return license;\n    }\n\n    [HttpPost(\"{id:guid}/upgrade\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<PaymentResponseModel> PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model)\n    {\n        if (!await currentContext.EditSubscription(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = userService.GetProperUserId(User);\n\n        var (success, paymentIntentClientSecret) = await upgradeOrganizationPlanCommand.UpgradePlanAsync(id, model.ToOrganizationUpgrade(), userId);\n\n        if (model.UseSecretsManager && success && userId.HasValue)\n        {\n            await TryGrantOwnerAccessToSecretsManagerAsync(id, userId.Value);\n        }\n\n        return new PaymentResponseModel { Success = success, PaymentIntentClientSecret = paymentIntentClientSecret };\n    }\n\n    [HttpPost(\"{id}/sm-subscription\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<ProfileOrganizationResponseModel> PostSmSubscription(Guid id, [FromBody] SecretsManagerSubscriptionUpdateRequestModel model)\n    {\n        if (!await currentContext.EditSubscription(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        organization = await AdjustOrganizationSeatsForSmTrialAsync(id, organization, model);\n\n        var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n        var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization, plan);\n\n        await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);\n\n        var userId = userService.GetProperUserId(User)!.Value;\n\n        return await GetProfileOrganizationResponseModelAsync(id, userId);\n    }\n\n    [HttpPost(\"{id:guid}/subscription\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<ProfileOrganizationResponseModel> PostSubscription(Guid id, [FromBody] OrganizationSubscriptionUpdateRequestModel model)\n    {\n        if (!await currentContext.EditSubscription(id))\n        {\n            throw new NotFoundException();\n        }\n\n        await organizationService.UpdateSubscription(id, model.SeatAdjustment, model.MaxAutoscaleSeats);\n\n        var userId = userService.GetProperUserId(User)!.Value;\n\n        return await GetProfileOrganizationResponseModelAsync(id, userId);\n    }\n\n    [HttpPost(\"{id:guid}/subscribe-secrets-manager\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<ProfileOrganizationResponseModel> PostSubscribeSecretsManagerAsync(Guid id, [FromBody] SecretsManagerSubscribeRequestModel model)\n    {\n        if (!await currentContext.EditSubscription(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await addSecretsManagerSubscriptionCommand.SignUpAsync(organization, model.AdditionalSmSeats,\n            model.AdditionalServiceAccounts);\n\n        var userId = userService.GetProperUserId(User).Value;\n\n        await TryGrantOwnerAccessToSecretsManagerAsync(organization.Id, userId);\n\n        return await GetProfileOrganizationResponseModelAsync(organization.Id, userId);\n    }\n\n    [HttpPost(\"{id:guid}/seat\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<PaymentResponseModel> PostSeat(Guid id, [FromBody] OrganizationSeatRequestModel model)\n    {\n        if (!await currentContext.EditSubscription(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await organizationService.AdjustSeatsAsync(id, model.SeatAdjustment.Value);\n        return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };\n    }\n\n    [HttpPost(\"{id}/cancel\")]\n    public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)\n    {\n        if (!await currentContext.EditSubscription(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(id);\n\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await subscriberService.CancelSubscription(organization,\n            new OffboardingSurveyResponse\n            {\n                UserId = currentContext.UserId!.Value,\n                Reason = request.Reason,\n                Feedback = request.Feedback\n            },\n            organization.IsExpired());\n    }\n\n    [HttpPost(\"{id:guid}/reinstate\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task PostReinstate(Guid id)\n    {\n        if (!await currentContext.EditSubscription(id))\n        {\n            throw new NotFoundException();\n        }\n\n        await organizationService.ReinstateSubscriptionAsync(id);\n    }\n\n    /// <summary>\n    /// Tries to grant owner access to the Secrets Manager for the organization\n    /// </summary>\n    /// <param name=\"organizationId\"></param>\n    /// <param name=\"userId\"></param>\n    private async Task TryGrantOwnerAccessToSecretsManagerAsync(Guid organizationId, Guid userId)\n    {\n        var organizationUser = await organizationUserRepository.GetByOrganizationAsync(organizationId, userId);\n\n        if (organizationUser != null)\n        {\n            organizationUser.AccessSecretsManager = true;\n            await organizationUserRepository.ReplaceAsync(organizationUser);\n        }\n    }\n\n    /// <summary>\n    /// Adjusts the organization seats for the Secrets Manager trial to match the new seat count for secrets manager\n    /// </summary>\n    /// <param name=\"id\"></param>\n    /// <param name=\"organization\"></param>\n    /// <param name=\"model\"></param>\n    private async Task<Organization> AdjustOrganizationSeatsForSmTrialAsync(Guid id, Organization organization,\n        SecretsManagerSubscriptionUpdateRequestModel model)\n    {\n        if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) ||\n            string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId) ||\n            model.SeatAdjustment == 0)\n        {\n            return organization;\n        }\n\n        var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);\n        if (subscriptionInfo?.CustomerDiscount?.Id != StripeConstants.CouponIDs.SecretsManagerStandalone)\n        {\n            return organization;\n        }\n\n        await organizationService.UpdateSubscription(id, model.SeatAdjustment, null);\n\n        return await organizationRepository.GetByIdAsync(id);\n    }\n\n    private async Task SaveOrganizationInstallationAsync(Guid organizationId, Guid installationId)\n    {\n        var organizationInstallation =\n            await organizationInstallationRepository.GetByInstallationIdAsync(installationId);\n\n        if (organizationInstallation == null)\n        {\n            await organizationInstallationRepository.CreateAsync(new OrganizationInstallation\n            {\n                OrganizationId = organizationId,\n                InstallationId = installationId\n            });\n        }\n        else if (organizationInstallation.OrganizationId == organizationId)\n        {\n            organizationInstallation.RevisionDate = DateTime.UtcNow;\n            await organizationInstallationRepository.ReplaceAsync(organizationInstallation);\n        }\n    }\n\n    private async Task<ProfileOrganizationResponseModel> GetProfileOrganizationResponseModelAsync(\n        Guid organizationId,\n        Guid userId)\n    {\n        var organizationUserDetails = await organizationUserRepository.GetDetailsByUserAsync(\n            userId,\n            organizationId,\n            OrganizationUserStatusType.Confirmed);\n\n        var organizationIdsClaimingActiveUser = (await userService.GetOrganizationsClaimingUserAsync(userId))\n            .Select(o => o.Id);\n\n        return new ProfileOrganizationResponseModel(organizationUserDetails, organizationIdsClaimingActiveUser);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/PlansController.cs",
    "content": "﻿using Bit.Api.Models.Response;\nusing Bit.Core.Billing.Pricing;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Route(\"plans\")]\n[Authorize(\"Application\")]\npublic class PlansController(\n    IPricingClient pricingClient) : Controller\n{\n    [HttpGet(\"\")]\n    [AllowAnonymous]\n    public async Task<ListResponseModel<PlanResponseModel>> Get()\n    {\n        var plans = await pricingClient.ListPlans();\n        var responses = plans.Select(plan => new PlanResponseModel(plan));\n        return new ListResponseModel<PlanResponseModel>(responses);\n    }\n\n    [HttpGet(\"premium\")]\n    public async Task<IResult> GetPremiumPlanAsync()\n    {\n        var premiumPlan = await pricingClient.GetAvailablePremiumPlan();\n        return TypedResults.Ok(premiumPlan);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/PreviewInvoiceController.cs",
    "content": "﻿using Bit.Api.Billing.Attributes;\nusing Bit.Api.Billing.Models.Requests.PreviewInvoice;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Entities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Authorize(\"Application\")]\n[Route(\"billing/preview-invoice\")]\npublic class PreviewInvoiceController(\n    IPreviewOrganizationTaxCommand previewOrganizationTaxCommand,\n    IPreviewPremiumTaxCommand previewPremiumTaxCommand,\n    IPreviewPremiumUpgradeProrationCommand previewPremiumUpgradeProrationCommand) : BaseBillingController\n{\n    [HttpPost(\"organizations/subscriptions/purchase\")]\n    [InjectUser]\n    public async Task<IResult> PreviewOrganizationSubscriptionPurchaseTaxAsync(\n        [BindNever] User user,\n        [FromBody] PreviewOrganizationSubscriptionPurchaseTaxRequest request)\n    {\n        var (purchase, billingAddress) = request.ToDomain();\n        var result = await previewOrganizationTaxCommand.Run(user, purchase, billingAddress);\n        return Handle(result.Map(pair => new { pair.Tax, pair.Total }));\n    }\n\n    [HttpPost(\"organizations/{organizationId:guid}/subscription/plan-change\")]\n    [InjectOrganization]\n    public async Task<IResult> PreviewOrganizationSubscriptionPlanChangeTaxAsync(\n        [BindNever] Organization organization,\n        [FromBody] PreviewOrganizationSubscriptionPlanChangeTaxRequest request)\n    {\n        var (planChange, billingAddress) = request.ToDomain();\n        var result = await previewOrganizationTaxCommand.Run(organization, planChange, billingAddress);\n        return Handle(result.Map(pair => new { pair.Tax, pair.Total }));\n    }\n\n    [HttpPut(\"organizations/{organizationId:guid}/subscription/update\")]\n    [InjectOrganization]\n    public async Task<IResult> PreviewOrganizationSubscriptionUpdateTaxAsync(\n        [BindNever] Organization organization,\n        [FromBody] PreviewOrganizationSubscriptionUpdateTaxRequest request)\n    {\n        var update = request.ToDomain();\n        var result = await previewOrganizationTaxCommand.Run(organization, update);\n        return Handle(result.Map(pair => new { pair.Tax, pair.Total }));\n    }\n\n    [HttpPost(\"premium/subscriptions/purchase\")]\n    [InjectUser]\n    public async Task<IResult> PreviewPremiumSubscriptionPurchaseTaxAsync(\n        [BindNever] User user,\n        [FromBody] PreviewPremiumSubscriptionPurchaseTaxRequest request)\n    {\n        var (preview, billingAddress) = request.ToDomain();\n        var result = await previewPremiumTaxCommand.Run(user, preview, billingAddress);\n        return Handle(result.Map(pair => new { pair.Tax, pair.Total }));\n    }\n\n    [HttpPost(\"premium/subscriptions/upgrade\")]\n    [InjectUser]\n    public async Task<IResult> PreviewPremiumUpgradeProrationAsync(\n        [BindNever] User user,\n        [FromBody] PreviewPremiumUpgradeProrationRequest request)\n    {\n        var (planType, billingAddress) = request.ToDomain();\n\n        var result = await previewPremiumUpgradeProrationCommand.Run(\n            user,\n            planType,\n            billingAddress);\n\n        return Handle(result.Map(proration => new\n        {\n            proration.NewPlanProratedAmount,\n            proration.Credit,\n            proration.Tax,\n            proration.Total,\n            proration.NewPlanProratedMonths\n        }));\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/ProviderBillingController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Billing.Models.Responses;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Models;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Models.BitStripe;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Stripe;\n\nusing static Bit.Core.Billing.Utilities;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Route(\"providers/{providerId:guid}/billing\")]\n[Authorize(\"Application\")]\npublic class ProviderBillingController(\n    ICurrentContext currentContext,\n    ILogger<BaseProviderController> logger,\n    IPricingClient pricingClient,\n    IProviderBillingService providerBillingService,\n    IProviderPlanRepository providerPlanRepository,\n    IProviderRepository providerRepository,\n    ISubscriberService subscriberService,\n    IStripeAdapter stripeAdapter,\n    IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)\n{\n    // TODO: Migrate to Query / ProviderBillingVNextController\n    [HttpGet(\"invoices\")]\n    public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)\n    {\n        var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);\n\n        if (provider == null)\n        {\n            return result;\n        }\n\n        var invoices = await stripeAdapter.ListInvoicesAsync(new StripeInvoiceListOptions\n        {\n            Customer = provider.GatewayCustomerId\n        });\n\n        var response = InvoicesResponse.From(invoices);\n\n        return TypedResults.Ok(response);\n    }\n\n    // TODO: Migrate to Query / ProviderBillingVNextController\n    [HttpGet(\"invoices/{invoiceId}\")]\n    public async Task<IResult> GenerateClientInvoiceReportAsync([FromRoute] Guid providerId, string invoiceId)\n    {\n        var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);\n\n        if (provider == null)\n        {\n            return result;\n        }\n\n        var reportContent = await providerBillingService.GenerateClientInvoiceReport(invoiceId);\n\n        if (reportContent == null)\n        {\n            return Error.ServerError(\"We had a problem generating your invoice CSV. Please contact support.\");\n        }\n\n        return TypedResults.File(\n            reportContent,\n            \"text/csv\");\n    }\n\n    // TODO: Migrate to Query / ProviderBillingVNextController\n    [HttpGet(\"subscription\")]\n    public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId)\n    {\n        var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);\n\n        if (provider == null)\n        {\n            return result;\n        }\n\n        var subscription = await stripeAdapter.GetSubscriptionAsync(provider.GatewaySubscriptionId,\n            new SubscriptionGetOptions { Expand = [\"customer.tax_ids\", \"discounts\", \"test_clock\"] });\n\n        var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);\n\n        var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>\n        {\n            var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);\n            var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type);\n            var price = await stripeAdapter.GetPriceAsync(priceId);\n\n            var unitAmount = price.UnitAmountDecimal.HasValue\n                ? price.UnitAmountDecimal.Value / 100M\n                : plan.PasswordManager.ProviderPortalSeatPrice;\n\n            return new ConfiguredProviderPlan(\n                providerPlan.Id,\n                providerPlan.ProviderId,\n                plan,\n                unitAmount,\n                providerPlan.SeatMinimum ?? 0,\n                providerPlan.PurchasedSeats ?? 0,\n                providerPlan.AllocatedSeats ?? 0);\n        }));\n\n        var taxInformation = GetTaxInformation(subscription.Customer);\n\n        var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);\n\n        var paymentSource = await subscriberService.GetPaymentSource(provider);\n\n        var response = ProviderSubscriptionResponse.From(\n            subscription,\n            configuredProviderPlans,\n            taxInformation,\n            subscriptionSuspension,\n            provider,\n            paymentSource);\n\n        return TypedResults.Ok(response);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/StripeController.cs",
    "content": "﻿using Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing Microsoft.AspNetCore.Mvc;\nusing Stripe;\n\nnamespace Bit.Api.Billing.Controllers;\n\n[Authorize(\"Application\")]\npublic class StripeController(\n    IStripeAdapter stripeAdapter) : Controller\n{\n    [HttpPost]\n    [Route(\"~/setup-intent/bank-account\")]\n    public async Task<Ok<string>> CreateSetupIntentForBankAccountAsync()\n    {\n        var options = new SetupIntentCreateOptions\n        {\n            PaymentMethodOptions = new SetupIntentPaymentMethodOptionsOptions\n            {\n                UsBankAccount = new SetupIntentPaymentMethodOptionsUsBankAccountOptions\n                {\n                    VerificationMethod = \"microdeposits\"\n                }\n            },\n            PaymentMethodTypes = [\"us_bank_account\"],\n            Usage = \"off_session\"\n        };\n\n        var setupIntent = await stripeAdapter.CreateSetupIntentAsync(options);\n\n        return TypedResults.Ok(setupIntent.ClientSecret);\n    }\n\n    [HttpPost]\n    [Route(\"~/setup-intent/card\")]\n    public async Task<Ok<string>> CreateSetupIntentForCardAsync()\n    {\n        var options = new SetupIntentCreateOptions\n        {\n            PaymentMethodTypes = [\"card\"],\n            Usage = \"off_session\"\n        };\n\n        var setupIntent = await stripeAdapter.CreateSetupIntentAsync(options);\n\n        return TypedResults.Ok(setupIntent.ClientSecret);\n    }\n\n    [HttpGet]\n    [Route(\"~/tax/is-country-supported\")]\n    public IResult IsCountrySupported(\n        [FromQuery] string country,\n        [FromServices] ITaxService taxService)\n    {\n        var isSupported = taxService.IsSupported(country);\n\n        return TypedResults.Ok(isSupported);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs",
    "content": "﻿using Bit.Api.Billing.Attributes;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Api.Billing.Models.Requests.Premium;\nusing Bit.Api.Billing.Models.Requests.Storage;\nusing Bit.Api.Billing.Models.Responses.Portal;\nusing Bit.Core;\nusing Bit.Core.Billing.Licenses.Queries;\nusing Bit.Core.Billing.Payment.Commands;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Portal.Commands;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Billing.Subscriptions.Commands;\nusing Bit.Core.Billing.Subscriptions.Queries;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Api.Billing.Controllers.VNext;\n\n[Authorize(\"Application\")]\n[Route(\"account/billing/vnext\")]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class AccountBillingVNextController(\n    ICreateBillingPortalSessionCommand createBillingPortalSessionCommand,\n    ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,\n    ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand,\n    ICurrentContext currentContext,\n    IGetApplicableDiscountsQuery getApplicableDiscountsQuery,\n    IGetBitwardenSubscriptionQuery getBitwardenSubscriptionQuery,\n    IGetCreditQuery getCreditQuery,\n    IGetPaymentMethodQuery getPaymentMethodQuery,\n    IGetUserLicenseQuery getUserLicenseQuery,\n    IReinstateSubscriptionCommand reinstateSubscriptionCommand,\n    IUpdatePaymentMethodCommand updatePaymentMethodCommand,\n    IUpdatePremiumStorageCommand updatePremiumStorageCommand,\n    IUpgradePremiumToOrganizationCommand upgradePremiumToOrganizationCommand) : BaseBillingController\n{\n    [HttpGet(\"credit\")]\n    [InjectUser]\n    public async Task<IResult> GetCreditAsync(\n        [BindNever] User user)\n    {\n        var credit = await getCreditQuery.Run(user);\n        return TypedResults.Ok(credit);\n    }\n\n    [HttpPost(\"credit/bitpay\")]\n    [InjectUser]\n    public async Task<IResult> AddCreditViaBitPayAsync(\n        [BindNever] User user,\n        [FromBody] BitPayCreditRequest request)\n    {\n        var result = await createBitPayInvoiceForCreditCommand.Run(\n            user,\n            request.Amount,\n            request.RedirectUrl);\n        return Handle(result);\n    }\n\n    [HttpGet(\"payment-method\")]\n    [InjectUser]\n    public async Task<IResult> GetPaymentMethodAsync(\n        [BindNever] User user)\n    {\n        var paymentMethod = await getPaymentMethodQuery.Run(user);\n        return TypedResults.Ok(paymentMethod);\n    }\n\n    [HttpPut(\"payment-method\")]\n    [InjectUser]\n    public async Task<IResult> UpdatePaymentMethodAsync(\n        [BindNever] User user,\n        [FromBody] TokenizedPaymentMethodRequest request)\n    {\n        var (paymentMethod, billingAddress) = request.ToDomain();\n        var result = await updatePaymentMethodCommand.Run(user, paymentMethod, billingAddress);\n        return Handle(result);\n    }\n\n    [HttpPost(\"subscription\")]\n    [InjectUser]\n    public async Task<IResult> CreateSubscriptionAsync(\n        [BindNever] User user,\n        [FromBody] PremiumCloudHostedSubscriptionRequest request)\n    {\n        var subscriptionPurchase = request.ToDomain();\n        var result = await createPremiumCloudHostedSubscriptionCommand.Run(user, subscriptionPurchase);\n        return Handle(result);\n    }\n\n    [HttpGet(\"license\")]\n    [InjectUser]\n    public async Task<IResult> GetLicenseAsync(\n        [BindNever] User user)\n    {\n        var response = await getUserLicenseQuery.Run(user);\n        return TypedResults.Ok(response);\n    }\n\n    [HttpGet(\"subscription\")]\n    [RequireFeature(FeatureFlagKeys.PM29594_UpdateIndividualSubscriptionPage)]\n    [InjectUser]\n    public async Task<IResult> GetSubscriptionAsync(\n        [BindNever] User user)\n    {\n        var subscription = await getBitwardenSubscriptionQuery.Run(user);\n        return subscription == null ? TypedResults.NotFound() : TypedResults.Ok(subscription);\n    }\n\n    [HttpPost(\"subscription/reinstate\")]\n    [RequireFeature(FeatureFlagKeys.PM29594_UpdateIndividualSubscriptionPage)]\n    [InjectUser]\n    public async Task<IResult> ReinstateSubscriptionAsync(\n        [BindNever] User user)\n    {\n        var result = await reinstateSubscriptionCommand.Run(user);\n        return Handle(result);\n    }\n\n    [HttpPut(\"subscription/storage\")]\n    [RequireFeature(FeatureFlagKeys.PM29594_UpdateIndividualSubscriptionPage)]\n    [InjectUser]\n    public async Task<IResult> UpdateSubscriptionStorageAsync(\n        [BindNever] User user,\n        [FromBody] StorageUpdateRequest request)\n    {\n        var result = await updatePremiumStorageCommand.Run(user, request.AdditionalStorageGb);\n        return Handle(result);\n    }\n\n    [HttpPost(\"upgrade\")]\n    [InjectUser]\n    public async Task<IResult> UpgradePremiumToOrganizationAsync(\n        [BindNever] User user,\n        [FromBody] UpgradePremiumToOrganizationRequest request)\n    {\n        var (organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress) = request.ToDomain();\n        var result = await upgradePremiumToOrganizationCommand.Run(user, organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress);\n        return Handle(result);\n    }\n\n    [HttpGet(\"discounts\")]\n    [RequireFeature(FeatureFlagKeys.PM29108_EnablePersonalDiscounts)]\n    [InjectUser]\n    public async Task<IResult> GetApplicableDiscountsAsync(\n        [BindNever] User user)\n    {\n        var result = await getApplicableDiscountsQuery.Run(user);\n        return Handle(result);\n    }\n\n    [HttpPost(\"portal-session\")]\n    [InjectUser]\n    public async Task<IResult> CreatePortalSessionAsync([BindNever] User user)\n    {\n        if (DeviceTypes.ToClientType(currentContext.DeviceType) != ClientType.Mobile)\n        {\n            return TypedResults.NotFound();\n        }\n\n        var returnUrl = \"bitwarden://premium-upgrade-callback\";\n\n        var result = await createBillingPortalSessionCommand.Run(user, returnUrl);\n        return Handle(result.Map(url => new PortalSessionResponse { Url = url }));\n    }\n\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs",
    "content": "﻿using Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Api.Billing.Attributes;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Api.Billing.Models.Requests.Subscriptions;\nusing Bit.Api.Billing.Models.Requirements;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Billing.Payment.Commands;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Subscriptions.Commands;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n// ReSharper disable RouteTemplates.MethodMissingRouteParameters\n\nnamespace Bit.Api.Billing.Controllers.VNext;\n\n[Authorize(\"Application\")]\n[Route(\"organizations/{organizationId:guid}/billing/vnext\")]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class OrganizationBillingVNextController(\n    ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,\n    IGetBillingAddressQuery getBillingAddressQuery,\n    IGetCreditQuery getCreditQuery,\n    IGetOrganizationMetadataQuery getOrganizationMetadataQuery,\n    IGetOrganizationWarningsQuery getOrganizationWarningsQuery,\n    IGetPaymentMethodQuery getPaymentMethodQuery,\n    IRestartSubscriptionCommand restartSubscriptionCommand,\n    IUpdateBillingAddressCommand updateBillingAddressCommand,\n    IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController\n{\n    [Authorize<ManageOrganizationBillingRequirement>]\n    [HttpGet(\"address\")]\n    [InjectOrganization]\n    public async Task<IResult> GetBillingAddressAsync(\n        [BindNever] Organization organization)\n    {\n        var billingAddress = await getBillingAddressQuery.Run(organization);\n        return TypedResults.Ok(billingAddress);\n    }\n\n    [Authorize<ManageOrganizationBillingRequirement>]\n    [HttpPut(\"address\")]\n    [InjectOrganization]\n    public async Task<IResult> UpdateBillingAddressAsync(\n        [BindNever] Organization organization,\n        [FromBody] BillingAddressRequest request)\n    {\n        var billingAddress = request.ToDomain();\n        var result = await updateBillingAddressCommand.Run(organization, billingAddress);\n        return Handle(result);\n    }\n\n    [Authorize<ManageOrganizationBillingRequirement>]\n    [HttpGet(\"credit\")]\n    [InjectOrganization]\n    public async Task<IResult> GetCreditAsync(\n        [BindNever] Organization organization)\n    {\n        var credit = await getCreditQuery.Run(organization);\n        return TypedResults.Ok(credit);\n    }\n\n    [Authorize<ManageOrganizationBillingRequirement>]\n    [HttpPost(\"credit/bitpay\")]\n    [InjectOrganization]\n    public async Task<IResult> AddCreditViaBitPayAsync(\n        [BindNever] Organization organization,\n        [FromBody] BitPayCreditRequest request)\n    {\n        var result = await createBitPayInvoiceForCreditCommand.Run(\n            organization,\n            request.Amount,\n            request.RedirectUrl);\n        return Handle(result);\n    }\n\n    [Authorize<ManageOrganizationBillingRequirement>]\n    [HttpGet(\"payment-method\")]\n    [InjectOrganization]\n    public async Task<IResult> GetPaymentMethodAsync(\n        [BindNever] Organization organization)\n    {\n        var paymentMethod = await getPaymentMethodQuery.Run(organization);\n        return TypedResults.Ok(paymentMethod);\n    }\n\n    [Authorize<ManageOrganizationBillingRequirement>]\n    [HttpPut(\"payment-method\")]\n    [InjectOrganization]\n    public async Task<IResult> UpdatePaymentMethodAsync(\n        [BindNever] Organization organization,\n        [FromBody] TokenizedPaymentMethodRequest request)\n    {\n        var (paymentMethod, billingAddress) = request.ToDomain();\n        var result = await updatePaymentMethodCommand.Run(organization, paymentMethod, billingAddress);\n        return Handle(result);\n    }\n\n    [Authorize<ManageOrganizationBillingRequirement>]\n    [HttpPost(\"subscription/restart\")]\n    [InjectOrganization]\n    public async Task<IResult> RestartSubscriptionAsync(\n        [BindNever] Organization organization,\n        [FromBody] RestartSubscriptionRequest request)\n    {\n        var (paymentMethod, billingAddress) = request.ToDomain();\n        var result = await updatePaymentMethodCommand.Run(organization, paymentMethod, null)\n            .AndThenAsync(_ => updateBillingAddressCommand.Run(organization, billingAddress))\n            .AndThenAsync(_ => restartSubscriptionCommand.Run(organization));\n        return Handle(result);\n    }\n\n    [Authorize<MemberOrProviderRequirement>]\n    [HttpGet(\"metadata\")]\n    [InjectOrganization]\n    public async Task<IResult> GetMetadataAsync(\n        [BindNever] Organization organization)\n    {\n        var metadata = await getOrganizationMetadataQuery.Run(organization);\n\n        if (metadata == null)\n        {\n            return TypedResults.NotFound();\n        }\n\n        return TypedResults.Ok(metadata);\n    }\n\n    [Authorize<MemberOrProviderRequirement>]\n    [HttpGet(\"warnings\")]\n    [InjectOrganization]\n    public async Task<IResult> GetWarningsAsync(\n        [BindNever] Organization organization)\n    {\n        var warnings = await getOrganizationWarningsQuery.Run(organization);\n        return TypedResults.Ok(warnings);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs",
    "content": "﻿using Bit.Api.Billing.Attributes;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Payment.Commands;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Providers.Queries;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n// ReSharper disable RouteTemplates.MethodMissingRouteParameters\n\nnamespace Bit.Api.Billing.Controllers.VNext;\n\n[Route(\"providers/{providerId:guid}/billing/vnext\")]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class ProviderBillingVNextController(\n    ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,\n    IGetBillingAddressQuery getBillingAddressQuery,\n    IGetCreditQuery getCreditQuery,\n    IGetPaymentMethodQuery getPaymentMethodQuery,\n    IGetProviderWarningsQuery getProviderWarningsQuery,\n    IProviderService providerService,\n    IUpdateBillingAddressCommand updateBillingAddressCommand,\n    IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController\n{\n    [HttpGet(\"address\")]\n    [InjectProvider(ProviderUserType.ProviderAdmin)]\n    public async Task<IResult> GetBillingAddressAsync(\n        [BindNever] Provider provider)\n    {\n        var billingAddress = await getBillingAddressQuery.Run(provider);\n        return TypedResults.Ok(billingAddress);\n    }\n\n    [HttpPut(\"address\")]\n    [InjectProvider(ProviderUserType.ProviderAdmin)]\n    public async Task<IResult> UpdateBillingAddressAsync(\n        [BindNever] Provider provider,\n        [FromBody] BillingAddressRequest request)\n    {\n        var billingAddress = request.ToDomain();\n        var result = await updateBillingAddressCommand.Run(provider, billingAddress);\n        return Handle(result);\n    }\n\n    [HttpGet(\"credit\")]\n    [InjectProvider(ProviderUserType.ProviderAdmin)]\n    public async Task<IResult> GetCreditAsync(\n        [BindNever] Provider provider)\n    {\n        var credit = await getCreditQuery.Run(provider);\n        return TypedResults.Ok(credit);\n    }\n\n    [HttpPost(\"credit/bitpay\")]\n    [InjectProvider(ProviderUserType.ProviderAdmin)]\n    public async Task<IResult> AddCreditViaBitPayAsync(\n        [BindNever] Provider provider,\n        [FromBody] BitPayCreditRequest request)\n    {\n        var result = await createBitPayInvoiceForCreditCommand.Run(\n            provider,\n            request.Amount,\n            request.RedirectUrl);\n        return Handle(result);\n    }\n\n    [HttpGet(\"payment-method\")]\n    [InjectProvider(ProviderUserType.ProviderAdmin)]\n    public async Task<IResult> GetPaymentMethodAsync(\n        [BindNever] Provider provider)\n    {\n        var paymentMethod = await getPaymentMethodQuery.Run(provider);\n        return TypedResults.Ok(paymentMethod);\n    }\n\n    [HttpPut(\"payment-method\")]\n    [InjectProvider(ProviderUserType.ProviderAdmin)]\n    public async Task<IResult> UpdatePaymentMethodAsync(\n        [BindNever] Provider provider,\n        [FromBody] TokenizedPaymentMethodRequest request)\n    {\n        var (paymentMethod, billingAddress) = request.ToDomain();\n        var result = await updatePaymentMethodCommand.Run(provider, paymentMethod, billingAddress);\n        // TODO: Temporary until we can send Provider notifications from the Billing API\n        if (!provider.Enabled)\n        {\n            await result.TapAsync(async _ =>\n            {\n                provider.Enabled = true;\n                await providerService.UpdateAsync(provider);\n            });\n        }\n        return Handle(result);\n    }\n\n    [HttpGet(\"warnings\")]\n    [InjectProvider(ProviderUserType.ServiceUser)]\n    public async Task<IResult> GetWarningsAsync(\n        [BindNever] Provider provider)\n    {\n        var warnings = await getProviderWarningsQuery.Run(provider);\n        return TypedResults.Ok(warnings);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingVNextController.cs",
    "content": "﻿using Bit.Api.Billing.Attributes;\nusing Bit.Api.Billing.Models.Requests.Premium;\nusing Bit.Api.Utilities;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Api.Billing.Controllers.VNext;\n\n[Authorize(\"Application\")]\n[Route(\"account/billing/vnext/self-host\")]\n[SelfHosted(SelfHostedOnly = true)]\npublic class SelfHostedAccountBillingVNextController(\n    ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController\n{\n    [HttpPost(\"license\")]\n    [InjectUser]\n    public async Task<IResult> UploadLicenseAsync(\n        [BindNever] User user,\n        PremiumSelfHostedSubscriptionRequest request)\n    {\n        var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, request.License);\n        if (license == null)\n        {\n            throw new BadRequestException(\"Invalid license.\");\n        }\n        var result = await createPremiumSelfHostedSubscriptionCommand.Run(user, license);\n        return Handle(result);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Controllers/VNext/SelfHostedOrganizationBillingVNextController.cs",
    "content": "﻿using Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Api.Billing.Attributes;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Api.Billing.Controllers.VNext;\n\n[Authorize(\"Application\")]\n[Route(\"organizations/{organizationId:guid}/billing/vnext/self-host\")]\n[SelfHosted(SelfHostedOnly = true)]\npublic class SelfHostedOrganizationBillingVNextController(\n    IGetOrganizationMetadataQuery getOrganizationMetadataQuery) : BaseBillingController\n{\n    [Authorize<MemberOrProviderRequirement>]\n    [HttpGet(\"metadata\")]\n    [InjectOrganization]\n    public async Task<IResult> GetMetadataAsync([BindNever] Organization organization)\n    {\n        var metadata = await getOrganizationMetadataQuery.Run(organization);\n\n        if (metadata == null)\n        {\n            return TypedResults.NotFound();\n        }\n\n        return TypedResults.Ok(metadata);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Billing.Models.Requests;\n\npublic class AddExistingOrganizationRequestBody\n{\n    [Required(ErrorMessage = \"'key' must be provided\")]\n    public string Key { get; set; }\n\n    [Required(ErrorMessage = \"'organizationId' must be provided\")]\n    public Guid OrganizationId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/ChangePlanFrequencyRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Api.Billing.Models.Requests;\n\npublic class ChangePlanFrequencyRequest\n{\n    [Required]\n    public PlanType NewPlanType { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/CreateClientOrganizationRequestBody.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.Utilities;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Api.Billing.Models.Requests;\n\npublic class CreateClientOrganizationRequestBody\n{\n    [Required(ErrorMessage = \"'name' must be provided\")]\n    public string Name { get; set; }\n\n    [Required(ErrorMessage = \"'ownerEmail' must be provided\")]\n    public string OwnerEmail { get; set; }\n\n    [EnumMatches<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly, PlanType.EnterpriseAnnually, ErrorMessage = \"'planType' must be Teams (Monthly), Enterprise (Monthly) or Enterprise (Annually)\")]\n    public PlanType PlanType { get; set; }\n\n    [Range(1, int.MaxValue, ErrorMessage = \"'seats' must be greater than 0\")]\n    public int Seats { get; set; }\n\n    [Required(ErrorMessage = \"'key' must be provided\")]\n    public string Key { get; set; }\n\n    [Required(ErrorMessage = \"'keyPair' must be provided\")]\n    public KeyPairRequestBody KeyPair { get; set; }\n\n    [Required(ErrorMessage = \"'collectionName' must be provided\")]\n    public string CollectionName { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/KeyPairRequestBody.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Api.Billing.Models.Requests;\n\n// ReSharper disable once ClassNeverInstantiated.Global\npublic class KeyPairRequestBody\n{\n    [Required(ErrorMessage = \"'publicKey' must be provided\")]\n    public string PublicKey { get; set; }\n    [Required(ErrorMessage = \"'encryptedPrivateKey' must be provided\")]\n    public string EncryptedPrivateKey { get; set; }\n\n    public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData()\n    {\n        return new PublicKeyEncryptionKeyPairData(\n            wrappedPrivateKey: EncryptedPrivateKey,\n            publicKey: PublicKey);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPlanChangeRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Organizations;\n\npublic record OrganizationSubscriptionPlanChangeRequest : IValidatableObject\n{\n    [Required]\n    [JsonConverter(typeof(JsonStringEnumConverter))]\n    public ProductTierType Tier { get; set; }\n\n    [Required]\n    [JsonConverter(typeof(JsonStringEnumConverter))]\n    public PlanCadenceType Cadence { get; set; }\n\n    public OrganizationSubscriptionPlanChange ToDomain() => new()\n    {\n        Tier = Tier,\n        Cadence = Cadence\n    };\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (Tier == ProductTierType.Families && Cadence == PlanCadenceType.Monthly)\n        {\n            yield return new ValidationResult(\"Monthly billing cadence is not available for the Families plan.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPurchaseRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Organizations;\n\npublic record OrganizationSubscriptionPurchaseRequest : IValidatableObject\n{\n    [Required]\n    [JsonConverter(typeof(JsonStringEnumConverter))]\n    public ProductTierType Tier { get; set; }\n\n    [Required]\n    [JsonConverter(typeof(JsonStringEnumConverter))]\n    public PlanCadenceType Cadence { get; set; }\n\n    [Required]\n    public required PasswordManagerPurchaseSelections PasswordManager { get; set; }\n\n    public SecretsManagerPurchaseSelections? SecretsManager { get; set; }\n\n    public string[]? Coupons { get; set; }\n\n    public OrganizationSubscriptionPurchase ToDomain() => new()\n    {\n        Tier = Tier,\n        Cadence = Cadence,\n        PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n        {\n            Seats = PasswordManager.Seats,\n            AdditionalStorage = PasswordManager.AdditionalStorage,\n            Sponsored = PasswordManager.Sponsored\n        },\n        SecretsManager = SecretsManager != null ? new OrganizationSubscriptionPurchase.SecretsManagerSelections\n        {\n            Seats = SecretsManager.Seats,\n            AdditionalServiceAccounts = SecretsManager.AdditionalServiceAccounts,\n            Standalone = SecretsManager.Standalone\n        } : null,\n        Coupons = Coupons\n    };\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (Tier != ProductTierType.Families)\n        {\n            yield break;\n        }\n\n        if (Cadence == PlanCadenceType.Monthly)\n        {\n            yield return new ValidationResult(\"Monthly cadence is not available on the Families plan.\");\n        }\n\n        if (SecretsManager != null)\n        {\n            yield return new ValidationResult(\"Secrets Manager is not available on the Families plan.\");\n        }\n    }\n\n    public record PasswordManagerPurchaseSelections\n    {\n        [Required]\n        [Range(1, 100000, ErrorMessage = \"Password Manager seats must be between 1 and 100,000\")]\n        public int Seats { get; set; }\n\n        [Required]\n        [Range(0, 99, ErrorMessage = \"Additional storage must be between 0 and 99 GB\")]\n        public int AdditionalStorage { get; set; }\n\n        public bool Sponsored { get; set; } = false;\n    }\n\n    public record SecretsManagerPurchaseSelections\n    {\n        [Required]\n        [Range(1, 100000, ErrorMessage = \"Secrets Manager seats must be between 1 and 100,000\")]\n        public int Seats { get; set; }\n\n        [Required]\n        [Range(0, 100000, ErrorMessage = \"Additional service accounts must be between 0 and 100,000\")]\n        public int AdditionalServiceAccounts { get; set; }\n\n        public bool Standalone { get; set; } = false;\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionUpdateRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Organizations.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Organizations;\n\npublic record OrganizationSubscriptionUpdateRequest\n{\n    public PasswordManagerUpdateSelections? PasswordManager { get; set; }\n    public SecretsManagerUpdateSelections? SecretsManager { get; set; }\n\n    public OrganizationSubscriptionUpdate ToDomain() => new()\n    {\n        PasswordManager =\n            PasswordManager != null\n                ? new OrganizationSubscriptionUpdate.PasswordManagerSelections\n                {\n                    Seats = PasswordManager.Seats,\n                    AdditionalStorage = PasswordManager.AdditionalStorage\n                }\n                : null,\n        SecretsManager =\n            SecretsManager != null\n                ? new OrganizationSubscriptionUpdate.SecretsManagerSelections\n                {\n                    Seats = SecretsManager.Seats,\n                    AdditionalServiceAccounts = SecretsManager.AdditionalServiceAccounts\n                }\n                : null\n    };\n\n    public record PasswordManagerUpdateSelections\n    {\n        [Range(1, 100000, ErrorMessage = \"Password Manager seats must be between 1 and 100,000\")]\n        public int? Seats { get; set; }\n\n        [Range(0, 99, ErrorMessage = \"Additional storage must be between 0 and 99 GB\")]\n        public int? AdditionalStorage { get; set; }\n    }\n\n    public record SecretsManagerUpdateSelections\n    {\n        [Range(0, 100000, ErrorMessage = \"Secrets Manager seats must be between 0 and 100,000\")]\n        public int? Seats { get; set; }\n\n        [Range(0, 100000, ErrorMessage = \"Additional service accounts must be between 0 and 100,000\")]\n        public int? AdditionalServiceAccounts { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Payment/BillingAddressRequest.cs",
    "content": "﻿using Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Payment;\n\npublic record BillingAddressRequest : CheckoutBillingAddressRequest\n{\n    public string? Line1 { get; set; }\n    public string? Line2 { get; set; }\n    public string? City { get; set; }\n    public string? State { get; set; }\n\n    public override BillingAddress ToDomain() => base.ToDomain() with\n    {\n        Line1 = Line1,\n        Line2 = Line2,\n        City = City,\n        State = State,\n    };\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Payment/BitPayCreditRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Billing.Models.Requests.Payment;\n\npublic record BitPayCreditRequest\n{\n    [Required]\n    public required decimal Amount { get; set; }\n\n    [Required]\n    public required string RedirectUrl { get; set; } = null!;\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Payment/CheckoutBillingAddressRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Payment;\n\npublic record CheckoutBillingAddressRequest : MinimalBillingAddressRequest\n{\n    public TaxIdRequest? TaxId { get; set; }\n\n    public override BillingAddress ToDomain() => base.ToDomain() with\n    {\n        TaxId = TaxId != null ? new TaxID(TaxId.Code, TaxId.Value) : null\n    };\n\n    public class TaxIdRequest\n    {\n        [Required]\n        public string Code { get; set; } = null!;\n\n        [Required]\n        public string Value { get; set; } = null!;\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Payment/MinimalBillingAddressRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Payment;\n\npublic record MinimalBillingAddressRequest\n{\n    [Required]\n    [StringLength(2, MinimumLength = 2, ErrorMessage = \"Country code must be 2 characters long.\")]\n    public required string Country { get; set; } = null!;\n    [Required]\n    public required string PostalCode { get; set; } = null!;\n\n    public virtual BillingAddress ToDomain() => new() { Country = Country, PostalCode = PostalCode, };\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Billing.Attributes;\nusing Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Payment;\n\npublic class MinimalTokenizedPaymentMethodRequest\n{\n    [Required]\n    [TokenizedPaymentMethodTypeValidation]\n    public required string Type { get; set; }\n\n    [Required]\n    public required string Token { get; set; }\n\n    public TokenizedPaymentMethod ToDomain() => new()\n    {\n        Type = TokenizablePaymentMethodTypeExtensions.From(Type),\n        Token = Token\n    };\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Payment/NonTokenizedPaymentMethodRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Billing.Attributes;\nusing Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Payment;\n\npublic class NonTokenizedPaymentMethodRequest\n{\n    [Required]\n    [NonTokenizedPaymentMethodTypeValidation]\n    public required string Type { get; set; }\n\n    public NonTokenizedPaymentMethod ToDomain()\n    {\n        return Type switch\n        {\n            \"accountCredit\" => new NonTokenizedPaymentMethod { Type = NonTokenizablePaymentMethodType.AccountCredit },\n            _ => throw new InvalidOperationException($\"Invalid value for {nameof(NonTokenizedPaymentMethod)}.{nameof(NonTokenizedPaymentMethod.Type)}\")\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs",
    "content": "﻿using Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Payment;\n\npublic class TokenizedPaymentMethodRequest : MinimalTokenizedPaymentMethodRequest\n{\n    public MinimalBillingAddressRequest? BillingAddress { get; set; }\n\n    public new (TokenizedPaymentMethod, BillingAddress?) ToDomain()\n    {\n        var paymentMethod = base.ToDomain();\n        var billingAddress = BillingAddress?.ToDomain();\n        return (paymentMethod, billingAddress);\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Payment/VerifyBankAccountRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Billing.Models.Requests.Payment;\n\npublic class VerifyBankAccountRequest\n{\n    [Required]\n    public required string DescriptorCode { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Premium.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Premium;\n\npublic class PremiumCloudHostedSubscriptionRequest : IValidatableObject\n{\n    public MinimalTokenizedPaymentMethodRequest? TokenizedPaymentMethod { get; set; }\n    public NonTokenizedPaymentMethodRequest? NonTokenizedPaymentMethod { get; set; }\n\n    [Required]\n    public required MinimalBillingAddressRequest BillingAddress { get; set; }\n\n    [Range(0, 99)]\n    public short AdditionalStorageGb { get; set; } = 0;\n\n    public string[]? Coupons { get; set; }\n\n    public PremiumSubscriptionPurchase ToDomain()\n    {\n        // Check if TokenizedPaymentMethod or NonTokenizedPaymentMethod is provided.\n        var tokenizedPaymentMethod = TokenizedPaymentMethod?.ToDomain();\n        var nonTokenizedPaymentMethod = NonTokenizedPaymentMethod?.ToDomain();\n\n        PaymentMethod paymentMethod = tokenizedPaymentMethod != null\n            ? tokenizedPaymentMethod\n            : nonTokenizedPaymentMethod!;\n\n        var billingAddress = BillingAddress.ToDomain();\n\n        return new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = AdditionalStorageGb,\n            Coupons = Coupons\n        };\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (TokenizedPaymentMethod == null && NonTokenizedPaymentMethod == null)\n        {\n            yield return new ValidationResult(\n                \"Either TokenizedPaymentMethod or NonTokenizedPaymentMethod must be provided.\",\n                new[] { nameof(TokenizedPaymentMethod), nameof(NonTokenizedPaymentMethod) }\n            );\n        }\n\n        if (TokenizedPaymentMethod != null && NonTokenizedPaymentMethod != null)\n        {\n            yield return new ValidationResult(\n                \"Only one of TokenizedPaymentMethod or NonTokenizedPaymentMethod can be provided.\",\n                new[] { nameof(TokenizedPaymentMethod), nameof(NonTokenizedPaymentMethod) }\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Premium/PremiumSelfHostedSubscriptionRequest.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Billing.Models.Requests.Premium;\n\npublic class PremiumSelfHostedSubscriptionRequest\n{\n    [Required]\n    public required IFormFile License { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Premium/UpgradePremiumToOrganizationRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Api.Billing.Models.Requests.Premium;\n\npublic class UpgradePremiumToOrganizationRequest\n{\n    [Required]\n    public string OrganizationName { get; set; } = null!;\n\n    [Required]\n    public string Key { get; set; } = null!;\n\n    [Required]\n    public string PublicKey { get; set; } = null!;\n\n    [Required]\n    public string EncryptedPrivateKey { get; set; } = null!;\n\n    public string? CollectionName { get; set; }\n\n    [Required]\n    [JsonConverter(typeof(JsonStringEnumConverter))]\n    public required ProductTierType TargetProductTierType { get; set; }\n\n    [Required]\n    public required CheckoutBillingAddressRequest BillingAddress { get; set; }\n\n    private PlanType PlanType\n    {\n        get\n        {\n            if (TargetProductTierType is not (ProductTierType.Families or ProductTierType.Teams or ProductTierType.Enterprise))\n            {\n                throw new InvalidOperationException($\"Cannot upgrade Premium subscription to {TargetProductTierType} plan.\");\n            }\n\n            return TargetProductTierType switch\n            {\n                ProductTierType.Families => PlanType.FamiliesAnnually,\n                ProductTierType.Teams => PlanType.TeamsAnnually,\n                ProductTierType.Enterprise => PlanType.EnterpriseAnnually,\n                _ => throw new InvalidOperationException($\"Unexpected ProductTierType: {TargetProductTierType}\")\n            };\n        }\n    }\n\n    public (string OrganizationName, string Key, string PublicKey, string EncryptedPrivateKey, string? CollectionName, PlanType PlanType, Core.Billing.Payment.Models.BillingAddress BillingAddress) ToDomain() =>\n        (OrganizationName, Key, PublicKey, EncryptedPrivateKey, CollectionName, PlanType, BillingAddress.ToDomain());\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/PreviewInvoice/PreviewOrganizationSubscriptionPlanChangeTaxRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Billing.Models.Requests.Organizations;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.PreviewInvoice;\n\npublic record PreviewOrganizationSubscriptionPlanChangeTaxRequest\n{\n    [Required]\n    public required OrganizationSubscriptionPlanChangeRequest Plan { get; set; }\n\n    [Required]\n    public required CheckoutBillingAddressRequest BillingAddress { get; set; }\n\n    public (OrganizationSubscriptionPlanChange, BillingAddress) ToDomain() =>\n        (Plan.ToDomain(), BillingAddress.ToDomain());\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/PreviewInvoice/PreviewOrganizationSubscriptionPurchaseTaxRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Billing.Models.Requests.Organizations;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.PreviewInvoice;\n\npublic record PreviewOrganizationSubscriptionPurchaseTaxRequest\n{\n    [Required]\n    public required OrganizationSubscriptionPurchaseRequest Purchase { get; set; }\n\n    [Required]\n    public required CheckoutBillingAddressRequest BillingAddress { get; set; }\n\n    public (OrganizationSubscriptionPurchase, BillingAddress) ToDomain() =>\n        (Purchase.ToDomain(), BillingAddress.ToDomain());\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/PreviewInvoice/PreviewOrganizationSubscriptionUpdateTaxRequest.cs",
    "content": "﻿using Bit.Api.Billing.Models.Requests.Organizations;\nusing Bit.Core.Billing.Organizations.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.PreviewInvoice;\n\npublic class PreviewOrganizationSubscriptionUpdateTaxRequest\n{\n    public required OrganizationSubscriptionUpdateRequest Update { get; set; }\n\n    public OrganizationSubscriptionUpdate ToDomain() => Update.ToDomain();\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/PreviewInvoice/PreviewPremiumSubscriptionPurchaseTaxRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Premium.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.PreviewInvoice;\n\npublic record PreviewPremiumSubscriptionPurchaseTaxRequest\n{\n    [Required]\n    [Range(0, 99, ErrorMessage = \"Additional storage must be between 0 and 99 GB.\")]\n    public short AdditionalStorage { get; set; }\n\n    [Required]\n    public required MinimalBillingAddressRequest BillingAddress { get; set; }\n\n    public string[]? Coupons { get; set; }\n\n    public (PremiumPurchasePreview, BillingAddress) ToDomain() => (\n        new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = AdditionalStorage,\n            Coupons = Coupons\n        },\n        BillingAddress.ToDomain());\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/PreviewInvoice/PreviewPremiumUpgradeProrationRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.PreviewInvoice;\n\npublic record PreviewPremiumUpgradeProrationRequest\n{\n    [Required]\n    [JsonConverter(typeof(JsonStringEnumConverter))]\n    public required ProductTierType TargetProductTierType { get; set; }\n\n    [Required]\n    public required MinimalBillingAddressRequest BillingAddress { get; set; }\n\n    private PlanType PlanType\n    {\n        get\n        {\n            if (TargetProductTierType is not (ProductTierType.Families or ProductTierType.Teams or ProductTierType.Enterprise))\n            {\n                throw new InvalidOperationException($\"Cannot upgrade Premium subscription to {TargetProductTierType} plan.\");\n            }\n\n            return TargetProductTierType switch\n            {\n                ProductTierType.Families => PlanType.FamiliesAnnually,\n                ProductTierType.Teams => PlanType.TeamsAnnually,\n                ProductTierType.Enterprise => PlanType.EnterpriseAnnually,\n                _ => throw new InvalidOperationException($\"Unexpected ProductTierType: {TargetProductTierType}\")\n            };\n        }\n    }\n\n    public (PlanType, BillingAddress) ToDomain() =>\n        (PlanType, BillingAddress.ToDomain());\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/SetupBusinessUnitRequestBody.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Billing.Models.Requests;\n\npublic class SetupBusinessUnitRequestBody\n{\n    [Required]\n    public Guid UserId { get; set; }\n\n    [Required]\n    public string Token { get; set; }\n\n    [Required]\n    public string ProviderKey { get; set; }\n\n    [Required]\n    public string OrganizationKey { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Storage/StorageUpdateRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Billing.Models.Requests.Storage;\n\n/// <summary>\n/// Request model for updating storage allocation on a user's premium subscription.\n/// Allows for both increasing and decreasing storage in an idempotent manner.\n/// </summary>\npublic class StorageUpdateRequest : IValidatableObject\n{\n    /// <summary>\n    /// The additional storage in GB beyond the base storage.\n    /// Must be between 0 and the maximum allowed (minus base storage).\n    /// </summary>\n    [Required]\n    public short AdditionalStorageGb { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (AdditionalStorageGb < 0)\n        {\n            yield return new ValidationResult(\n                \"Additional storage cannot be negative.\",\n                [nameof(AdditionalStorageGb)]);\n        }\n\n        if (AdditionalStorageGb > 99)\n        {\n            yield return new ValidationResult(\n                \"Maximum additional storage is 99 GB.\",\n                [nameof(AdditionalStorageGb)]);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/Subscriptions/RestartSubscriptionRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Api.Billing.Models.Requests.Subscriptions;\n\npublic class RestartSubscriptionRequest\n{\n    [Required]\n    public required MinimalTokenizedPaymentMethodRequest PaymentMethod { get; set; }\n    [Required]\n    public required CheckoutBillingAddressRequest BillingAddress { get; set; }\n\n    public (TokenizedPaymentMethod, BillingAddress) ToDomain()\n        => (PaymentMethod.ToDomain(), BillingAddress.ToDomain());\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requests/UpdateClientOrganizationRequestBody.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Billing.Models.Requests;\n\npublic class UpdateClientOrganizationRequestBody\n{\n    [Required]\n    [Range(0, int.MaxValue, ErrorMessage = \"You cannot assign negative seats to a client organization.\")]\n    public int AssignedSeats { get; set; }\n\n    [Required]\n    public string Name { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Requirements/ManageOrganizationBillingRequirement.cs",
    "content": "﻿using Bit.Api.AdminConsole.Authorization;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.Billing.Models.Requirements;\n\npublic class ManageOrganizationBillingRequirement : IOrganizationRequirement\n{\n    public async Task<bool> AuthorizeAsync(\n        CurrentContextOrganization? organizationClaims,\n        Func<Task<bool>> isProviderUserForOrg)\n        => organizationClaims switch\n        {\n            { Type: OrganizationUserType.Owner } => true,\n            _ => await isProviderUserForOrg()\n        };\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Responses/BillingHistoryResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Billing.Models.Responses;\n\npublic class BillingHistoryResponseModel : ResponseModel\n{\n    public BillingHistoryResponseModel(BillingHistoryInfo billing)\n        : base(\"billingHistory\")\n    {\n        Transactions = billing.Transactions?.Select(t => new BillingTransaction(t));\n        Invoices = billing.Invoices?.Select(i => new BillingInvoice(i));\n    }\n    public IEnumerable<BillingInvoice> Invoices { get; set; }\n    public IEnumerable<BillingTransaction> Transactions { get; set; }\n}\n\npublic class BillingInvoice\n{\n    public BillingInvoice(BillingHistoryInfo.BillingInvoice inv)\n    {\n        Amount = inv.Amount;\n        Date = inv.Date;\n        Url = inv.Url;\n        PdfUrl = inv.PdfUrl;\n        Number = inv.Number;\n        Paid = inv.Paid;\n    }\n\n    public decimal Amount { get; set; }\n    public DateTime? Date { get; set; }\n    public string Url { get; set; }\n    public string PdfUrl { get; set; }\n    public string Number { get; set; }\n    public bool Paid { get; set; }\n}\n\npublic class BillingTransaction\n{\n    public BillingTransaction(BillingHistoryInfo.BillingTransaction transaction)\n    {\n        CreatedDate = transaction.CreatedDate;\n        Amount = transaction.Amount;\n        Refunded = transaction.Refunded;\n        RefundedAmount = transaction.RefundedAmount;\n        PartiallyRefunded = transaction.PartiallyRefunded;\n        Type = transaction.Type;\n        PaymentMethodType = transaction.PaymentMethodType;\n        Details = transaction.Details;\n    }\n\n    public DateTime CreatedDate { get; set; }\n    public decimal Amount { get; set; }\n    public bool? Refunded { get; set; }\n    public bool? PartiallyRefunded { get; set; }\n    public decimal? RefundedAmount { get; set; }\n    public TransactionType Type { get; set; }\n    public PaymentMethodType? PaymentMethodType { get; set; }\n    public string Details { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Responses/BillingResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Billing.Models.Responses;\n\npublic class BillingResponseModel : ResponseModel\n{\n    public BillingResponseModel(BillingInfo billing)\n        : base(\"billing\")\n    {\n        Balance = billing.Balance;\n        PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;\n    }\n\n    public decimal Balance { get; set; }\n    public BillingSource PaymentSource { get; set; }\n}\n\npublic class BillingSource\n{\n    public BillingSource(BillingInfo.BillingSource source)\n    {\n        Type = source.Type;\n        CardBrand = source.CardBrand;\n        Description = source.Description;\n        NeedsVerification = source.NeedsVerification;\n    }\n\n    public PaymentMethodType Type { get; set; }\n    public string CardBrand { get; set; }\n    public string Description { get; set; }\n    public bool NeedsVerification { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Responses/InvoicesResponse.cs",
    "content": "﻿using Stripe;\n\nnamespace Bit.Api.Billing.Models.Responses;\n\npublic record InvoicesResponse(\n    List<InvoiceResponse> Invoices)\n{\n    public static InvoicesResponse From(IEnumerable<Invoice> invoices) => new(\n        invoices\n            .Where(i => i.Status is \"open\" or \"paid\" or \"uncollectible\")\n            .OrderByDescending(i => i.Created)\n            .Select(InvoiceResponse.From).ToList());\n}\n\npublic record InvoiceResponse(\n    string Id,\n    DateTime Date,\n    string Number,\n    decimal Total,\n    string Status,\n    DateTime? DueDate,\n    string Url)\n{\n    public static InvoiceResponse From(Invoice invoice) => new(\n        invoice.Id,\n        invoice.Created,\n        invoice.Number,\n        invoice.Total / 100M,\n        invoice.Status,\n        invoice.DueDate,\n        invoice.HostedInvoiceUrl);\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Responses/Portal/PortalSessionResponse.cs",
    "content": "﻿namespace Bit.Api.Billing.Models.Responses.Portal;\n\n/// <summary>\n/// Response model containing the Stripe billing portal session URL.\n/// </summary>\npublic class PortalSessionResponse\n{\n    /// <summary>\n    /// The URL to redirect the user to for accessing the Stripe billing portal.\n    /// </summary>\n    public required string Url { get; init; }\n}\n"
  },
  {
    "path": "src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Providers.Models;\nusing Bit.Core.Billing.Tax.Models;\nusing Stripe;\n\nnamespace Bit.Api.Billing.Models.Responses;\n\npublic record ProviderSubscriptionResponse(\n    string Status,\n    DateTime? CurrentPeriodEndDate,\n    decimal? DiscountPercentage,\n    string CollectionMethod,\n    IEnumerable<ProviderPlanResponse> Plans,\n    decimal AccountCredit,\n    TaxInformation TaxInformation,\n    DateTime? CancelAt,\n    SubscriptionSuspension Suspension,\n    ProviderType ProviderType,\n    PaymentSource PaymentSource)\n{\n    private const string _annualCadence = \"Annual\";\n    private const string _monthlyCadence = \"Monthly\";\n\n    public static ProviderSubscriptionResponse From(\n        Subscription subscription,\n        ICollection<ConfiguredProviderPlan> providerPlans,\n        TaxInformation taxInformation,\n        SubscriptionSuspension subscriptionSuspension,\n        Provider provider,\n        PaymentSource paymentSource)\n    {\n        var providerPlanResponses = providerPlans\n            .Select(providerPlan =>\n            {\n                var plan = providerPlan.Plan;\n                var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * providerPlan.Price;\n                var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;\n                return new ProviderPlanResponse(\n                    plan.Name,\n                    plan.Type,\n                    plan.ProductTier,\n                    providerPlan.SeatMinimum,\n                    providerPlan.PurchasedSeats,\n                    providerPlan.AssignedSeats,\n                    cost,\n                    cadence);\n            });\n\n        var accountCredit = Convert.ToDecimal(subscription.Customer?.Balance) * -1 / 100;\n\n        var discount = subscription.Customer?.Discount ?? subscription.Discounts?.FirstOrDefault();\n\n        return new ProviderSubscriptionResponse(\n            subscription.Status,\n            subscription.GetCurrentPeriodEnd(),\n            discount?.Coupon?.PercentOff,\n            subscription.CollectionMethod,\n            providerPlanResponses,\n            accountCredit,\n            taxInformation,\n            subscription.CancelAt,\n            subscriptionSuspension,\n            provider.Type,\n            paymentSource);\n    }\n}\n\npublic record ProviderPlanResponse(\n    string PlanName,\n    PlanType Type,\n    ProductTierType ProductTier,\n    int SeatMinimum,\n    int PurchasedSeats,\n    int AssignedSeats,\n    decimal Cost,\n    string Cadence);\n"
  },
  {
    "path": "src/Api/Billing/Public/Controllers/OrganizationController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing Bit.Api.Billing.Public.Models;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Context;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing OrganizationSubscriptionUpdateRequestModel = Bit.Api.Billing.Public.Models.OrganizationSubscriptionUpdateRequestModel;\n\nnamespace Bit.Api.Billing.Public.Controllers;\n\n[Route(\"public/organization\")]\n[Authorize(\"Organization\")]\npublic class OrganizationController : Controller\n{\n    private readonly IOrganizationService _organizationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;\n    private readonly ILogger<OrganizationController> _logger;\n    private readonly IPricingClient _pricingClient;\n\n    public OrganizationController(\n        IOrganizationService organizationService,\n        ICurrentContext currentContext,\n        IOrganizationRepository organizationRepository,\n        IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,\n        ILogger<OrganizationController> logger,\n        IPricingClient pricingClient)\n    {\n        _organizationService = organizationService;\n        _currentContext = currentContext;\n        _organizationRepository = organizationRepository;\n        _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;\n        _logger = logger;\n        _pricingClient = pricingClient;\n    }\n\n    /// <summary>\n    /// Retrieves the subscription details for the current organization.\n    /// </summary>\n    /// <returns>\n    /// Returns an object containing the subscription details if successful.\n    /// </returns>\n    [HttpGet(\"subscription\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    [ProducesResponseType(typeof(OrganizationSubscriptionDetailsResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> GetSubscriptionAsync()\n    {\n        try\n        {\n            var organizationId = _currentContext.OrganizationId.Value;\n            var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n            var subscriptionDetails = new OrganizationSubscriptionDetailsResponseModel\n            {\n                PasswordManager = new PasswordManagerSubscriptionDetails\n                {\n                    Seats = organization.Seats,\n                    MaxAutoScaleSeats = organization.MaxAutoscaleSeats,\n                    Storage = organization.MaxStorageGb\n                },\n                SecretsManager = new SecretsManagerSubscriptionDetails\n                {\n                    Seats = organization.SmSeats,\n                    MaxAutoScaleSeats = organization.MaxAutoscaleSmSeats,\n                    ServiceAccounts = organization.SmServiceAccounts,\n                    MaxAutoScaleServiceAccounts = organization.MaxAutoscaleSmServiceAccounts\n                }\n            };\n\n            return Ok(subscriptionDetails);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Unhandled error while retrieving the subscription details\");\n            return StatusCode(500, new { Message = \"An error occurred while retrieving the subscription details.\" });\n        }\n    }\n\n    /// <summary>\n    /// Update the organization's current subscription for Password Manager and/or Secrets Manager.\n    /// </summary>\n    /// <param name=\"model\">The request model containing the updated subscription information.</param>\n    [HttpPut(\"subscription\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    [ProducesResponseType((int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> PostSubscriptionAsync([FromBody] OrganizationSubscriptionUpdateRequestModel model)\n    {\n        try\n        {\n            await UpdatePasswordManagerAsync(model, _currentContext.OrganizationId.Value);\n\n            var secretsManagerResult = await UpdateSecretsManagerAsync(model, _currentContext.OrganizationId.Value);\n\n            if (!string.IsNullOrEmpty(secretsManagerResult))\n            {\n                return Ok(new { Message = secretsManagerResult });\n            }\n\n            return Ok(new { Message = \"Subscription updated successfully.\" });\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Unhandled error while updating the subscription\");\n            return StatusCode(500, new { Message = \"An error occurred while updating the subscription.\" });\n        }\n    }\n\n    private async Task UpdatePasswordManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId)\n    {\n        if (model.PasswordManager != null)\n        {\n            var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n            model.PasswordManager.ToPasswordManagerSubscriptionUpdate(organization);\n            await _organizationService.UpdateSubscription(organization.Id, (int)model.PasswordManager.Seats,\n                model.PasswordManager.MaxAutoScaleSeats);\n            if (model.PasswordManager.Storage.HasValue)\n            {\n                await _organizationService.AdjustStorageAsync(organization.Id, (short)model.PasswordManager.Storage);\n            }\n        }\n    }\n\n    private async Task<string> UpdateSecretsManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId)\n    {\n        if (model.SecretsManager == null)\n        {\n            return string.Empty;\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n        if (!organization.UseSecretsManager)\n        {\n            return \"Organization has no access to Secrets Manager.\";\n        }\n\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n        var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization, plan);\n        await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(secretsManagerUpdate);\n\n        return string.Empty;\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Api.Billing.Public.Models;\n\npublic class OrganizationSubscriptionUpdateRequestModel : IValidatableObject\n{\n    public PasswordManagerSubscriptionUpdateModel PasswordManager { get; set; }\n    public SecretsManagerSubscriptionUpdateModel SecretsManager { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (PasswordManager == null && SecretsManager == null)\n        {\n            yield return new ValidationResult(\"At least one of PasswordManager or SecretsManager must be provided.\");\n        }\n\n        yield return ValidationResult.Success;\n    }\n}\n\npublic class PasswordManagerSubscriptionUpdateModel\n{\n    public int? Seats { get; set; }\n    public int? Storage { get; set; }\n    private int? _maxAutoScaleSeats;\n    public int? MaxAutoScaleSeats\n    {\n        get { return _maxAutoScaleSeats; }\n        set { _maxAutoScaleSeats = value < 0 ? null : value; }\n    }\n\n    public virtual void ToPasswordManagerSubscriptionUpdate(Organization organization)\n    {\n        UpdateMaxAutoScaleSeats(organization);\n\n        UpdateSeats(organization);\n\n        UpdateStorage(organization);\n    }\n\n    private void UpdateMaxAutoScaleSeats(Organization organization)\n    {\n        MaxAutoScaleSeats ??= organization.MaxAutoscaleSeats;\n    }\n\n    private void UpdateSeats(Organization organization)\n    {\n        if (Seats is > 0)\n        {\n            if (organization.Seats.HasValue)\n            {\n                Seats = Seats.Value - organization.Seats.Value;\n            }\n        }\n        else\n        {\n            Seats = 0;\n        }\n    }\n\n    private void UpdateStorage(Organization organization)\n    {\n        if (Storage is > 0)\n        {\n            if (organization.MaxStorageGb.HasValue)\n            {\n                Storage = (short?)(Storage - organization.MaxStorageGb.Value);\n            }\n        }\n        else\n        {\n            Storage = null;\n        }\n    }\n}\n\npublic class SecretsManagerSubscriptionUpdateModel\n{\n    public int? Seats { get; set; }\n    private int? _maxAutoScaleSeats;\n    public int? MaxAutoScaleSeats\n    {\n        get { return _maxAutoScaleSeats; }\n        set { _maxAutoScaleSeats = value < 0 ? null : value; }\n    }\n    public int? ServiceAccounts { get; set; }\n    private int? _maxAutoScaleServiceAccounts;\n    public int? MaxAutoScaleServiceAccounts\n    {\n        get { return _maxAutoScaleServiceAccounts; }\n        set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; }\n    }\n\n    public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)\n    {\n        var update = UpdateUpdateMaxAutoScale(organization, plan);\n        UpdateSeats(organization, update);\n        UpdateServiceAccounts(organization, update);\n        return update;\n    }\n\n    private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization, Plan plan)\n    {\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats,\n            MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts\n        };\n        return update;\n    }\n\n    private void UpdateSeats(Organization organization, SecretsManagerSubscriptionUpdate update)\n    {\n        if (Seats is > 0)\n        {\n            if (organization.SmSeats.HasValue)\n            {\n                Seats = Seats.Value - organization.SmSeats.Value;\n\n            }\n            update.AdjustSeats(Seats.Value);\n        }\n    }\n\n    private void UpdateServiceAccounts(Organization organization, SecretsManagerSubscriptionUpdate update)\n    {\n        if (ServiceAccounts is > 0)\n        {\n            if (organization.SmServiceAccounts.HasValue)\n            {\n                ServiceAccounts = ServiceAccounts.Value - organization.SmServiceAccounts.Value;\n            }\n            update.AdjustServiceAccounts(ServiceAccounts.Value);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Billing/Public/Models/Response/OrganizationSubscriptionDetailsResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Billing.Public.Models;\n\npublic class OrganizationSubscriptionDetailsResponseModel : IValidatableObject\n{\n    public PasswordManagerSubscriptionDetails PasswordManager { get; set; }\n    public SecretsManagerSubscriptionDetails SecretsManager { get; set; }\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (PasswordManager == null && SecretsManager == null)\n        {\n            yield return new ValidationResult(\"At least one of PasswordManager or SecretsManager must be provided.\");\n        }\n\n        yield return ValidationResult.Success;\n    }\n}\npublic class PasswordManagerSubscriptionDetails\n{\n    public int? Seats { get; set; }\n    public int? MaxAutoScaleSeats { get; set; }\n    public short? Storage { get; set; }\n}\n\npublic class SecretsManagerSubscriptionDetails\n{\n    public int? Seats { get; set; }\n    public int? MaxAutoScaleSeats { get; set; }\n    public int? ServiceAccounts { get; set; }\n    public int? MaxAutoScaleServiceAccounts { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Controllers/CollectionsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Request;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Controllers;\n\n[Route(\"organizations/{orgId}/collections\")]\n[Authorize(\"Application\")]\npublic class CollectionsController : Controller\n{\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly ICreateCollectionCommand _createCollectionCommand;\n    private readonly IUpdateCollectionCommand _updateCollectionCommand;\n    private readonly IDeleteCollectionCommand _deleteCollectionCommand;\n    private readonly IUserService _userService;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly IBulkAddCollectionAccessCommand _bulkAddCollectionAccessCommand;\n\n    public CollectionsController(\n        ICollectionRepository collectionRepository,\n        ICreateCollectionCommand createCollectionCommand,\n        IUpdateCollectionCommand updateCollectionCommand,\n        IDeleteCollectionCommand deleteCollectionCommand,\n        IUserService userService,\n        IAuthorizationService authorizationService,\n        ICurrentContext currentContext,\n        IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand)\n    {\n        _collectionRepository = collectionRepository;\n        _createCollectionCommand = createCollectionCommand;\n        _updateCollectionCommand = updateCollectionCommand;\n        _deleteCollectionCommand = deleteCollectionCommand;\n        _userService = userService;\n        _authorizationService = authorizationService;\n        _currentContext = currentContext;\n        _bulkAddCollectionAccessCommand = bulkAddCollectionAccessCommand;\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<CollectionResponseModel> Get(Guid orgId, Guid id)\n    {\n        var collection = await _collectionRepository.GetByIdAsync(id);\n        var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Read)).Succeeded;\n        if (!authorized)\n        {\n            throw new NotFoundException();\n        }\n\n        return new CollectionResponseModel(collection);\n    }\n\n    [HttpGet(\"{id}/details\")]\n    public async Task<CollectionAccessDetailsResponseModel> GetDetails(Guid orgId, Guid id)\n    {\n        var collectionAdminDetails =\n            await _collectionRepository.GetByIdWithPermissionsAsync(id, _currentContext.UserId, true);\n\n        var authorized = (await _authorizationService.AuthorizeAsync(User, collectionAdminDetails, BulkCollectionOperations.ReadWithAccess)).Succeeded;\n        if (!authorized)\n        {\n            throw new NotFoundException();\n        }\n\n        return new CollectionAccessDetailsResponseModel(collectionAdminDetails);\n    }\n\n    [HttpGet(\"details\")]\n    public async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails(Guid orgId)\n    {\n        var allOrgCollections = await _collectionRepository.GetManySharedByOrganizationIdWithPermissionsAsync(\n            orgId, _currentContext.UserId.Value, true);\n\n        var readAllAuthorized =\n            (await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAllWithAccess(orgId))).Succeeded;\n        if (readAllAuthorized)\n        {\n            return new ListResponseModel<CollectionAccessDetailsResponseModel>(\n                allOrgCollections.Select(c => new CollectionAccessDetailsResponseModel(c))\n            );\n        }\n\n        // Filter collections to only return those where the user has Manage permission\n        var manageableOrgCollections = allOrgCollections.Where(c => c.Manage).ToList();\n\n        return new ListResponseModel<CollectionAccessDetailsResponseModel>(manageableOrgCollections.Select(c =>\n            new CollectionAccessDetailsResponseModel(c)\n        ));\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<CollectionResponseModel>> GetAll(Guid orgId)\n    {\n        IEnumerable<Collection> orgCollections;\n\n        var readAllAuthorized = (await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAll(orgId))).Succeeded;\n        if (readAllAuthorized)\n        {\n            orgCollections = await _collectionRepository.GetManySharedCollectionsByOrganizationIdAsync(orgId);\n        }\n        else\n        {\n            var assignedCollections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value);\n            orgCollections = assignedCollections.Where(c => c.OrganizationId == orgId && c.Manage).ToList();\n        }\n\n        var responses = orgCollections.Select(c => new CollectionResponseModel(c));\n        return new ListResponseModel<CollectionResponseModel>(responses);\n    }\n\n    [HttpGet(\"~/collections\")]\n    public async Task<ListResponseModel<CollectionDetailsResponseModel>> GetUser()\n    {\n        var collections = await _collectionRepository.GetManyByUserIdAsync(\n            _userService.GetProperUserId(User).Value);\n        var responses = collections.Select(c => new CollectionDetailsResponseModel(c));\n        return new ListResponseModel<CollectionDetailsResponseModel>(responses);\n    }\n\n    [HttpGet(\"{id}/users\")]\n    public async Task<IEnumerable<SelectionReadOnlyResponseModel>> GetUsers(Guid orgId, Guid id)\n    {\n        var collection = await _collectionRepository.GetByIdAsync(id);\n        var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ReadAccess)).Succeeded;\n        if (!authorized)\n        {\n            throw new NotFoundException();\n        }\n\n        var collectionUsers = await _collectionRepository.GetManyUsersByIdAsync(collection.Id);\n        var responses = collectionUsers.Select(cu => new SelectionReadOnlyResponseModel(cu));\n        return responses;\n    }\n\n    [HttpPost(\"\")]\n    public async Task<CollectionResponseModel> Post(Guid orgId, [FromBody] CreateCollectionRequestModel model)\n    {\n        var collection = model.ToCollection(orgId);\n\n        var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Create)).Succeeded;\n        if (!authorized)\n        {\n            throw new NotFoundException();\n        }\n\n        var groups = model.Groups?.Select(g => g.ToSelectionReadOnly());\n        var users = model.Users?.Select(g => g.ToSelectionReadOnly()).ToList() ?? new List<CollectionAccessSelection>();\n\n        await _createCollectionCommand.CreateAsync(collection, groups, users);\n\n        if (!_currentContext.UserId.HasValue || (_currentContext.GetOrganization(orgId) == null && await _currentContext.ProviderUserForOrgAsync(orgId)))\n        {\n            return new CollectionAccessDetailsResponseModel(collection);\n        }\n\n        // If we have a user, fetch the latest collection permission details\n        var collectionWithPermissions = await _collectionRepository.GetByIdWithPermissionsAsync(collection.Id, _currentContext.UserId.Value, false);\n\n        return new CollectionAccessDetailsResponseModel(collectionWithPermissions);\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model)\n    {\n        var collection = await _collectionRepository.GetByIdAsync(id);\n        var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded;\n        if (!authorized)\n        {\n            throw new NotFoundException();\n        }\n\n        var groups = model.Groups?.Select(g => g.ToSelectionReadOnly());\n        var users = model.Users?.Select(g => g.ToSelectionReadOnly());\n        await _updateCollectionCommand.UpdateAsync(model.ToCollection(collection), groups, users);\n\n        if (!_currentContext.UserId.HasValue || (_currentContext.GetOrganization(collection.OrganizationId) == null && await _currentContext.ProviderUserForOrgAsync(collection.OrganizationId)))\n        {\n            return new CollectionAccessDetailsResponseModel(collection);\n        }\n\n        // If we have a user, fetch the latest collection permission details\n        var collectionWithPermissions = await _collectionRepository.GetByIdWithPermissionsAsync(collection.Id, _currentContext.UserId.Value, false);\n\n        return new CollectionAccessDetailsResponseModel(collectionWithPermissions);\n    }\n\n    [HttpPost(\"{id}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /{id} instead.\")]\n    public async Task<CollectionResponseModel> PostPut(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model)\n    {\n        return await Put(orgId, id, model);\n    }\n\n    [HttpPost(\"bulk-access\")]\n    public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model)\n    {\n        var collections = await _collectionRepository.GetManyByManyIdsAsync(model.CollectionIds);\n        if (collections.Count(c => c.OrganizationId == orgId) != model.CollectionIds.Count())\n        {\n            throw new NotFoundException(\"One or more collections not found.\");\n        }\n\n        var result = await _authorizationService.AuthorizeAsync(User, collections,\n            new[] { BulkCollectionOperations.ModifyUserAccess, BulkCollectionOperations.ModifyGroupAccess });\n\n        if (!result.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        await _bulkAddCollectionAccessCommand.AddAccessAsync(\n            collections,\n            model.Users?.Select(u => u.ToSelectionReadOnly()).ToList(),\n            model.Groups?.Select(g => g.ToSelectionReadOnly()).ToList());\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task Delete(Guid orgId, Guid id)\n    {\n        var collection = await _collectionRepository.GetByIdAsync(id);\n        var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Delete)).Succeeded;\n        if (!authorized)\n        {\n            throw new NotFoundException();\n        }\n\n        await _deleteCollectionCommand.DeleteAsync(collection);\n    }\n\n    [HttpPost(\"{id}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE /{id} instead.\")]\n    public async Task PostDelete(Guid orgId, Guid id)\n    {\n        await Delete(orgId, id);\n    }\n\n    [HttpDelete(\"\")]\n    public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)\n    {\n        var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids);\n        var result = await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Delete);\n        if (!result.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        await _deleteCollectionCommand.DeleteManyAsync(collections);\n    }\n\n    [HttpPost(\"delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE / instead.\")]\n    public async Task PostDeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)\n    {\n        await DeleteMany(orgId, model);\n    }\n}\n"
  },
  {
    "path": "src/Api/Controllers/ConfigController.cs",
    "content": "﻿using Bit.Api.Models.Response;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\n\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Controllers;\n\n[Route(\"config\")]\npublic class ConfigController : Controller\n{\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IFeatureService _featureService;\n\n    public ConfigController(\n        IGlobalSettings globalSettings,\n        IFeatureService featureService)\n    {\n        _globalSettings = globalSettings;\n        _featureService = featureService;\n    }\n\n    [HttpGet(\"\")]\n    public ConfigResponseModel GetConfigs()\n    {\n        return new ConfigResponseModel(_featureService, _globalSettings);\n    }\n}\n"
  },
  {
    "path": "src/Api/Controllers/DevicesController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.Auth.Models.Request;\nusing Bit.Api.Models.Request;\nusing Bit.Api.Models.Response;\nusing Bit.Core.Auth.Models.Api.Request;\nusing Bit.Core.Auth.Models.Api.Response;\nusing Bit.Core.Auth.UserFeatures.DeviceTrust;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Controllers;\n\n[Route(\"devices\")]\n[Authorize(\"Application\")]\npublic class DevicesController : Controller\n{\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly IDeviceService _deviceService;\n    private readonly IUserService _userService;\n    private readonly IUntrustDevicesCommand _untrustDevicesCommand;\n    private readonly IUserRepository _userRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly ILogger<DevicesController> _logger;\n\n    public DevicesController(\n        IDeviceRepository deviceRepository,\n        IDeviceService deviceService,\n        IUserService userService,\n        IUntrustDevicesCommand untrustDevicesCommand,\n        IUserRepository userRepository,\n        ICurrentContext currentContext,\n        ILogger<DevicesController> logger)\n    {\n        _deviceRepository = deviceRepository;\n        _deviceService = deviceService;\n        _userService = userService;\n        _untrustDevicesCommand = untrustDevicesCommand;\n        _userRepository = userRepository;\n        _currentContext = currentContext;\n        _logger = logger;\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<DeviceResponseModel> Get(string id)\n    {\n        var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);\n        if (device == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var response = new DeviceResponseModel(device);\n        return response;\n    }\n\n    [HttpGet(\"identifier/{identifier}\")]\n    public async Task<DeviceResponseModel> GetByIdentifier(string identifier)\n    {\n        var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);\n        if (device == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var response = new DeviceResponseModel(device);\n        return response;\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> GetAll()\n    {\n        var devicesWithPendingAuthData = await _deviceRepository.GetManyByUserIdWithDeviceAuth(_userService.GetProperUserId(User).Value);\n\n        // Convert from DeviceAuthDetails to DeviceAuthRequestResponseModel\n        var deviceAuthRequestResponseList = devicesWithPendingAuthData\n            .Select(DeviceAuthRequestResponseModel.From)\n            .ToList();\n\n        var response = new ListResponseModel<DeviceAuthRequestResponseModel>(deviceAuthRequestResponseList);\n        return response;\n    }\n\n    [HttpPost(\"\")]\n    public async Task<DeviceResponseModel> Post([FromBody] DeviceRequestModel model)\n    {\n        var device = model.ToDevice(_userService.GetProperUserId(User));\n        await _deviceService.SaveAsync(device);\n\n        var response = new DeviceResponseModel(device);\n        return response;\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task<DeviceResponseModel> Put(string id, [FromBody] DeviceRequestModel model)\n    {\n        var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);\n        if (device == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _deviceService.SaveAsync(model.ToDevice(device));\n\n        var response = new DeviceResponseModel(device);\n        return response;\n    }\n\n    [HttpPost(\"{id}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /{id} instead.\")]\n    public async Task<DeviceResponseModel> PostPut(string id, [FromBody] DeviceRequestModel model)\n    {\n        return await Put(id, model);\n    }\n\n    [HttpPut(\"{identifier}/keys\")]\n    public async Task<DeviceResponseModel> PutKeys(string identifier, [FromBody] DeviceKeysRequestModel model)\n    {\n        var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);\n        if (device == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _deviceService.SaveAsync(model.ToDevice(device));\n\n        var response = new DeviceResponseModel(device);\n        return response;\n    }\n\n    [HttpPost(\"{identifier}/keys\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /{identifier}/keys instead.\")]\n    public async Task<DeviceResponseModel> PostKeys(string identifier, [FromBody] DeviceKeysRequestModel model)\n    {\n        return await PutKeys(identifier, model);\n    }\n\n    [HttpPost(\"{identifier}/retrieve-keys\")]\n    [Obsolete(\"This endpoint is deprecated. The keys are on the regular device GET endpoints now.\")]\n    public async Task<ProtectedDeviceResponseModel> GetDeviceKeys(string identifier)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var device = await _deviceRepository.GetByIdentifierAsync(identifier, user.Id);\n        if (device == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new ProtectedDeviceResponseModel(device);\n    }\n\n    [HttpPost(\"update-trust\")]\n    public async Task PostUpdateTrust([FromBody] UpdateDevicesTrustRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!await _userService.VerifySecretAsync(user, model.Secret))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(string.Empty, \"User verification failed.\");\n        }\n\n        await _deviceService.UpdateDevicesTrustAsync(\n            _currentContext.DeviceIdentifier,\n            user.Id,\n            model.CurrentDevice,\n            model.OtherDevices ?? Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>());\n    }\n\n    [HttpPost(\"untrust\")]\n    public async Task PostUntrust([FromBody] UntrustDevicesRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        await _untrustDevicesCommand.UntrustDevices(user, model.Devices);\n    }\n\n    [HttpPut(\"identifier/{identifier}/token\")]\n    public async Task PutToken(string identifier, [FromBody] DeviceTokenRequestModel model)\n    {\n        var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);\n        if (device == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _deviceService.SaveAsync(model.ToDevice(device));\n    }\n\n    [HttpPost(\"identifier/{identifier}/token\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /identifier/{identifier}/token instead.\")]\n    public async Task PostToken(string identifier, [FromBody] DeviceTokenRequestModel model)\n    {\n        await PutToken(identifier, model);\n    }\n\n    [HttpPut(\"identifier/{identifier}/web-push-auth\")]\n    public async Task PutWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model)\n    {\n        var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);\n        if (device == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _deviceService.SaveAsync(\n            model.ToData(),\n            device,\n            _currentContext.Organizations.Select(org => org.Id.ToString())\n        );\n    }\n\n    [HttpPost(\"identifier/{identifier}/web-push-auth\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /identifier/{identifier}/web-push-auth instead.\")]\n    public async Task PostWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model)\n    {\n        await PutWebPushAuth(identifier, model);\n    }\n\n    [AllowAnonymous]\n    [HttpPut(\"identifier/{identifier}/clear-token\")]\n    public async Task PutClearToken(string identifier)\n    {\n        var device = await _deviceRepository.GetByIdentifierAsync(identifier);\n        if (device == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _deviceService.ClearTokenAsync(device);\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"identifier/{identifier}/clear-token\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /identifier/{identifier}/clear-token instead.\")]\n    public async Task PostClearToken(string identifier)\n    {\n        await PutClearToken(identifier);\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task Deactivate(string id)\n    {\n        var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);\n        if (device == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _deviceService.DeactivateAsync(device);\n    }\n\n    [HttpPost(\"{id}/deactivate\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE /{id} instead.\")]\n    public async Task PostDeactivate(string id)\n    {\n        await Deactivate(id);\n    }\n\n    [AllowAnonymous]\n    [HttpGet(\"knowndevice\")]\n    public async Task<bool> GetByIdentifierQuery(\n            [Required][FromHeader(Name = \"X-Request-Email\")] string Email,\n            [Required][FromHeader(Name = \"X-Device-Identifier\")] string DeviceIdentifier)\n        => await GetByEmailAndIdentifier(CoreHelpers.Base64UrlDecodeString(Email), DeviceIdentifier);\n\n    [Obsolete(\"Path is deprecated due to encoding issues, use /knowndevice instead.\")]\n    [AllowAnonymous]\n    [HttpGet(\"knowndevice/{email}/{identifier}\")]\n    public async Task<bool> GetByEmailAndIdentifier(string email, string identifier)\n    {\n        if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(identifier))\n        {\n            throw new BadRequestException(\"Please provide an email and device identifier\");\n        }\n\n        var user = await _userRepository.GetByEmailAsync(email);\n        if (user == null)\n        {\n            return false;\n        }\n\n        var device = await _deviceRepository.GetByIdentifierAsync(identifier, user.Id);\n        return device != null;\n    }\n\n    [HttpPost(\"lost-trust\")]\n    public void PostLostTrust()\n    {\n        var userId = _currentContext.UserId.GetValueOrDefault();\n        if (userId == default)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var deviceId = _currentContext.DeviceIdentifier;\n        if (deviceId == null)\n        {\n            throw new BadRequestException(\"Please provide a device identifier\");\n        }\n\n        var deviceType = _currentContext.DeviceType;\n        if (deviceType == null)\n        {\n            throw new BadRequestException(\"Please provide a device type\");\n        }\n\n        _logger.LogError(\"User {id} has a device key, but didn't receive decryption keys for device {device} of type {deviceType}\", userId,\n            deviceId, deviceType);\n    }\n\n}\n"
  },
  {
    "path": "src/Api/Controllers/InfoController.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Controllers;\n\npublic class InfoController : Controller\n{\n    [HttpGet(\"~/alive\")]\n    public DateTime GetAlive()\n    {\n        return DateTime.UtcNow;\n    }\n\n    [HttpGet(\"~/now\")]\n    [Obsolete(\"This endpoint is deprecated. Use GET /alive instead.\")]\n    public DateTime GetNow()\n    {\n        return GetAlive();\n    }\n\n    [HttpGet(\"~/version\")]\n    public JsonResult GetVersion()\n    {\n        return Json(AssemblyHelpers.GetVersion());\n    }\n}\n"
  },
  {
    "path": "src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Models.Request;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Api.Utilities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Controllers.SelfHosted;\n\n[Route(\"organizations/licenses/self-hosted\")]\n[Authorize(\"Application\")]\n[SelfHosted(SelfHostedOnly = true)]\npublic class SelfHostedOrganizationLicensesController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IGetSelfHostedOrganizationLicenseQuery _getSelfHostedOrganizationLicenseQuery;\n    private readonly IOrganizationConnectionRepository _organizationConnectionRepository;\n    private readonly ISelfHostedOrganizationSignUpCommand _selfHostedOrganizationSignUpCommand;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IUserService _userService;\n    private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;\n\n    public SelfHostedOrganizationLicensesController(\n        ICurrentContext currentContext,\n        IGetSelfHostedOrganizationLicenseQuery getSelfHostedOrganizationLicenseQuery,\n        IOrganizationConnectionRepository organizationConnectionRepository,\n        ISelfHostedOrganizationSignUpCommand selfHostedOrganizationSignUpCommand,\n        IOrganizationRepository organizationRepository,\n        IUserService userService,\n        IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand)\n    {\n        _currentContext = currentContext;\n        _getSelfHostedOrganizationLicenseQuery = getSelfHostedOrganizationLicenseQuery;\n        _organizationConnectionRepository = organizationConnectionRepository;\n        _selfHostedOrganizationSignUpCommand = selfHostedOrganizationSignUpCommand;\n        _organizationRepository = organizationRepository;\n        _userService = userService;\n        _updateOrganizationLicenseCommand = updateOrganizationLicenseCommand;\n    }\n\n    [HttpPost(\"\")]\n    public async Task<OrganizationResponseModel> CreateLicenseAsync(OrganizationCreateLicenseRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var license = await ApiHelpers.ReadJsonFileFromBody<OrganizationLicense>(HttpContext, model.License);\n        if (license == null)\n        {\n            throw new BadRequestException(\"Invalid license\");\n        }\n\n        var result = await _selfHostedOrganizationSignUpCommand.SignUpAsync(license, user, model.Key,\n            model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey);\n\n        return new OrganizationResponseModel(result.Item1, null);\n    }\n\n    [HttpPost(\"{id}\")]\n    public async Task UpdateLicenseAsync(string id, LicenseRequestModel model)\n    {\n        var orgIdGuid = new Guid(id);\n        if (!await _currentContext.OrganizationOwner(orgIdGuid))\n        {\n            throw new NotFoundException();\n        }\n\n        var license = await ApiHelpers.ReadJsonFileFromBody<OrganizationLicense>(HttpContext, model.License);\n        if (license == null)\n        {\n            throw new BadRequestException(\"Invalid license\");\n        }\n\n        var selfHostedOrganizationDetails = await _organizationRepository.GetSelfHostedOrganizationDetailsById(orgIdGuid);\n        if (selfHostedOrganizationDetails == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var currentOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey);\n\n        await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, currentOrganization);\n    }\n\n    [HttpPost(\"{id}/sync\")]\n    public async Task SyncLicenseAsync(string id)\n    {\n        var selfHostedOrganizationDetails = await _organizationRepository.GetSelfHostedOrganizationDetailsById(new Guid(id));\n        if (selfHostedOrganizationDetails == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!await _currentContext.OrganizationOwner(selfHostedOrganizationDetails.Id))\n        {\n            throw new NotFoundException();\n        }\n\n        var billingSyncConnection =\n            (await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(selfHostedOrganizationDetails.Id,\n                OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();\n        if (billingSyncConnection == null)\n        {\n            throw new NotFoundException(\"Unable to get Cloud Billing Sync connection\");\n        }\n\n        var license =\n            await _getSelfHostedOrganizationLicenseQuery.GetLicenseAsync(selfHostedOrganizationDetails, billingSyncConnection);\n        var currentOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey);\n\n        await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, currentOrganization);\n\n        var config = billingSyncConnection.GetConfig<BillingSyncConfig>();\n        config.LastLicenseSync = DateTime.Now;\n        billingSyncConnection.SetConfig(config);\n        await _organizationConnectionRepository.ReplaceAsync(billingSyncConnection);\n    }\n}\n"
  },
  {
    "path": "src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Api.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api.Response.OrganizationSponsorships;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Controllers.SelfHosted;\n\n[Route(\"organization/sponsorship/self-hosted\")]\n[Authorize(\"Application\")]\n[SelfHosted(SelfHostedOnly = true)]\npublic class SelfHostedOrganizationSponsorshipsController : Controller\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;\n    private readonly ICreateSponsorshipCommand _offerSponsorshipCommand;\n    private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand;\n    private readonly ICurrentContext _currentContext;\n    private readonly IFeatureService _featureService;\n    private readonly IAuthorizationService _authorizationService;\n\n    public SelfHostedOrganizationSponsorshipsController(\n        ICreateSponsorshipCommand offerSponsorshipCommand,\n        IRevokeSponsorshipCommand revokeSponsorshipCommand,\n        IOrganizationRepository organizationRepository,\n        IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        ICurrentContext currentContext,\n        IFeatureService featureService,\n        IAuthorizationService authorizationService\n    )\n    {\n        _offerSponsorshipCommand = offerSponsorshipCommand;\n        _revokeSponsorshipCommand = revokeSponsorshipCommand;\n        _organizationRepository = organizationRepository;\n        _organizationSponsorshipRepository = organizationSponsorshipRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _currentContext = currentContext;\n        _featureService = featureService;\n        _authorizationService = authorizationService;\n    }\n\n    [HttpPost(\"{sponsoringOrgId}/families-for-enterprise\")]\n    public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)\n    {\n        await _offerSponsorshipCommand.CreateSponsorshipAsync(\n            await _organizationRepository.GetByIdAsync(sponsoringOrgId),\n            await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),\n            model.PlanSponsorshipType,\n            model.SponsoredEmail,\n            model.FriendlyName,\n            model.IsAdminInitiated.GetValueOrDefault(),\n            model.Notes);\n    }\n\n    [HttpDelete(\"{sponsoringOrgId}\")]\n    public async Task RevokeSponsorship(Guid sponsoringOrgId)\n    {\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);\n\n        if (orgUser == null)\n        {\n            throw new BadRequestException(\"Unknown Organization User\");\n        }\n\n        var existingOrgSponsorship = await _organizationSponsorshipRepository\n            .GetBySponsoringOrganizationUserIdAsync(orgUser.Id);\n\n        await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);\n    }\n\n    [HttpPost(\"{sponsoringOrgId}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE /{sponsoringOrgId} instead.\")]\n    public async Task PostRevokeSponsorship(Guid sponsoringOrgId)\n    {\n        await RevokeSponsorship(sponsoringOrgId);\n    }\n\n    [Authorize<ManageUsersRequirement>]\n    [HttpDelete(\"{organizationId}/{sponsoredFriendlyName}/revoke\")]\n    public async Task AdminInitiatedRevokeSponsorshipAsync([FromRoute(Name = \"organizationId\")] Guid sponsoringOrgId, string sponsoredFriendlyName)\n    {\n        var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);\n        var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase));\n        if (existingOrgSponsorship == null)\n        {\n            throw new BadRequestException(\"The specified sponsored organization could not be found under the given sponsoring organization.\");\n        }\n        await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);\n    }\n\n    [Authorize(\"Application\")]\n    [HttpGet(\"{orgId}/sponsored\")]\n    public async Task<ListResponseModel<OrganizationSponsorshipInvitesResponseModel>> GetSponsoredOrganizations(Guid orgId)\n    {\n        var sponsoringOrg = await _organizationRepository.GetByIdAsync(orgId);\n        if (sponsoringOrg == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, orgId, new ManageUsersRequirement());\n        if (!authorizationResult.Succeeded)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(orgId);\n        return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(\n            sponsorships\n                .Where(s => s.IsAdminInitiated)\n                .Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))\n        );\n\n    }\n}\n"
  },
  {
    "path": "src/Api/Controllers/SettingsController.cs",
    "content": "﻿using Bit.Api.Models.Request;\nusing Bit.Api.Models.Response;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Controllers;\n\n[Route(\"settings\")]\n[Authorize(\"Application\")]\npublic class SettingsController : Controller\n{\n    private readonly IUserService _userService;\n\n    public SettingsController(\n        IUserService userService)\n    {\n        _userService = userService;\n    }\n\n    [HttpGet(\"domains\")]\n    public async Task<DomainsResponseModel> GetDomains(bool excluded = true)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var response = new DomainsResponseModel(user, excluded);\n        return response;\n    }\n\n    [HttpPut(\"domains\")]\n    public async Task<DomainsResponseModel> PutDomains([FromBody] UpdateDomainsRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        await _userService.SaveUserAsync(model.ToUser(user), true);\n\n        var response = new DomainsResponseModel(user);\n        return response;\n    }\n\n    [HttpPost(\"domains\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT /domains instead.\")]\n    public async Task<DomainsResponseModel> PostDomains([FromBody] UpdateDomainsRequestModel model)\n    {\n        return await PutDomains(model);\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Controllers/EventsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Utilities;\nusing Bit.Api.Utilities.DiagnosticTools;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Dirt.Controllers;\n\n[Route(\"events\")]\n[Authorize(\"Application\")]\npublic class EventsController : Controller\n{\n    private readonly IUserService _userService;\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IEventRepository _eventRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly ILogger<EventsController> _logger;\n    private readonly IFeatureService _featureService;\n\n\n    public EventsController(IUserService userService,\n        ICipherRepository cipherRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IProviderUserRepository providerUserRepository,\n        IEventRepository eventRepository,\n        ICurrentContext currentContext,\n        ISecretRepository secretRepository,\n        IProjectRepository projectRepository,\n        IServiceAccountRepository serviceAccountRepository,\n        ILogger<EventsController> logger,\n        IFeatureService featureService)\n    {\n        _userService = userService;\n        _cipherRepository = cipherRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _providerUserRepository = providerUserRepository;\n        _eventRepository = eventRepository;\n        _currentContext = currentContext;\n        _secretRepository = secretRepository;\n        _projectRepository = projectRepository;\n        _serviceAccountRepository = serviceAccountRepository;\n        _logger = logger;\n        _featureService = featureService;\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetUser(\n        [FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)\n    {\n        var dateRange = ApiHelpers.GetDateRange(start, end);\n        var userId = _userService.GetProperUserId(User).Value;\n        var result = await _eventRepository.GetManyByUserAsync(userId, dateRange.Item1, dateRange.Item2,\n            new PageOptions { ContinuationToken = continuationToken });\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n\n    [HttpGet(\"~/ciphers/{id}/events\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetCipher(string id,\n        [FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)\n    {\n        var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var canView = false;\n        if (cipher.OrganizationId.HasValue)\n        {\n            canView = await _currentContext.AccessEventLogs(cipher.OrganizationId.Value);\n        }\n        else if (cipher.UserId.HasValue)\n        {\n            var userId = _userService.GetProperUserId(User).Value;\n            canView = userId == cipher.UserId.Value;\n        }\n\n        if (!canView)\n        {\n            throw new NotFoundException();\n        }\n\n        var dateRange = ApiHelpers.GetDateRange(start, end);\n        var result = await _eventRepository.GetManyByCipherAsync(cipher, dateRange.Item1, dateRange.Item2,\n            new PageOptions { ContinuationToken = continuationToken });\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n\n    [HttpGet(\"~/organizations/{id}/events\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetOrganization(string id,\n        [FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)\n    {\n        var orgId = new Guid(id);\n        if (!await _currentContext.AccessEventLogs(orgId))\n        {\n            throw new NotFoundException();\n        }\n\n        var dateRange = ApiHelpers.GetDateRange(start, end);\n        var result = await _eventRepository.GetManyByOrganizationAsync(orgId, dateRange.Item1, dateRange.Item2,\n            new PageOptions { ContinuationToken = continuationToken });\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n\n        _logger.LogAggregateData(_featureService, orgId, responses, continuationToken, start, end);\n\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n\n    [HttpGet(\"~/organization/{orgId}/secrets/{id}/events\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetSecrets(\n        Guid id, Guid orgId,\n        [FromQuery] DateTime? start = null,\n        [FromQuery] DateTime? end = null,\n        [FromQuery] string continuationToken = null)\n    {\n        if (id == Guid.Empty || orgId == Guid.Empty)\n        {\n            throw new NotFoundException();\n        }\n\n        var secret = await _secretRepository.GetByIdAsync(id);\n        var orgIdForVerification = secret?.OrganizationId ?? orgId;\n        var secretOrg = _currentContext.GetOrganization(orgIdForVerification);\n\n        if (secretOrg == null || !await _currentContext.AccessEventLogs(secretOrg.Id))\n        {\n            throw new NotFoundException();\n        }\n\n        bool canViewLogs = false;\n\n        if (secret == null)\n        {\n            secret = new Core.SecretsManager.Entities.Secret { Id = id, OrganizationId = orgId };\n            canViewLogs = secretOrg.Type is Core.Enums.OrganizationUserType.Admin or Core.Enums.OrganizationUserType.Owner;\n        }\n        else\n        {\n            canViewLogs = await CanViewSecretsLogs(secret);\n        }\n\n        if (!canViewLogs)\n        {\n            throw new NotFoundException();\n        }\n\n        var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);\n        var result = await _eventRepository.GetManyBySecretAsync(secret, fromDate, toDate, new PageOptions { ContinuationToken = continuationToken });\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n\n    [HttpGet(\"~/organization/{orgId}/projects/{id}/events\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetProjects(\n        Guid id,\n        Guid orgId,\n        [FromQuery] DateTime? start = null,\n        [FromQuery] DateTime? end = null,\n        [FromQuery] string continuationToken = null)\n    {\n        if (id == Guid.Empty || orgId == Guid.Empty)\n        {\n            throw new NotFoundException();\n        }\n\n        var project = await GetProject(id, orgId);\n        await ValidateOrganization(project);\n\n        var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);\n        var result = await _eventRepository.GetManyByProjectAsync(\n            project,\n            fromDate,\n            toDate,\n            new PageOptions { ContinuationToken = continuationToken });\n\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n\n    [HttpGet(\"~/organization/{orgId}/service-account/{id}/events\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetServiceAccounts(\n       Guid orgId,\n       Guid id,\n       [FromQuery] DateTime? start = null,\n       [FromQuery] DateTime? end = null,\n       [FromQuery] string continuationToken = null)\n    {\n        if (id == Guid.Empty || orgId == Guid.Empty)\n        {\n            throw new NotFoundException();\n        }\n\n        var serviceAccount = await GetServiceAccount(id, orgId);\n        var org = _currentContext.GetOrganization(serviceAccount.OrganizationId);\n\n        if (org == null || !await _currentContext.AccessEventLogs(org.Id))\n        {\n            throw new NotFoundException();\n        }\n\n        var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);\n        var result = await _eventRepository.GetManyByOrganizationServiceAccountAsync(\n            serviceAccount.OrganizationId,\n            serviceAccount.Id,\n            fromDate,\n            toDate,\n            new PageOptions { ContinuationToken = continuationToken });\n\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n\n    [ApiExplorerSettings(IgnoreApi = true)]\n    private async Task<ServiceAccount> GetServiceAccount(Guid serviceAccountId, Guid orgId)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);\n        if (serviceAccount != null)\n        {\n            return serviceAccount;\n        }\n\n        var fallbackServiceAccount = new ServiceAccount\n        {\n            Id = serviceAccountId,\n            OrganizationId = orgId\n        };\n\n        return fallbackServiceAccount;\n    }\n\n    [HttpGet(\"~/organizations/{orgId}/users/{id}/events\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetOrganizationUser(string orgId, string id,\n        [FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)\n    {\n        var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));\n        if (organizationUser == null || !organizationUser.UserId.HasValue ||\n            !await _currentContext.AccessEventLogs(organizationUser.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var dateRange = ApiHelpers.GetDateRange(start, end);\n        var result = await _eventRepository.GetManyByOrganizationActingUserAsync(organizationUser.OrganizationId,\n            organizationUser.UserId.Value, dateRange.Item1, dateRange.Item2,\n            new PageOptions { ContinuationToken = continuationToken });\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n\n    [HttpGet(\"~/providers/{providerId:guid}/events\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetProvider(Guid providerId,\n        [FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)\n    {\n        if (!_currentContext.ProviderAccessEventLogs(providerId))\n        {\n            throw new NotFoundException();\n        }\n\n        var dateRange = ApiHelpers.GetDateRange(start, end);\n        var result = await _eventRepository.GetManyByProviderAsync(providerId, dateRange.Item1, dateRange.Item2,\n            new PageOptions { ContinuationToken = continuationToken });\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n\n    [HttpGet(\"~/providers/{providerId:guid}/users/{id:guid}/events\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetProviderUser(Guid providerId, Guid id,\n        [FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)\n    {\n        var providerUser = await _providerUserRepository.GetByIdAsync(id);\n        if (providerUser == null || !providerUser.UserId.HasValue ||\n            !_currentContext.ProviderAccessEventLogs(providerUser.ProviderId))\n        {\n            throw new NotFoundException();\n        }\n\n        var dateRange = ApiHelpers.GetDateRange(start, end);\n        var result = await _eventRepository.GetManyByProviderActingUserAsync(providerUser.ProviderId,\n            providerUser.UserId.Value, dateRange.Item1, dateRange.Item2,\n            new PageOptions { ContinuationToken = continuationToken });\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n\n    [ApiExplorerSettings(IgnoreApi = true)]\n    private async Task ValidateOrganization(Project project)\n    {\n        var org = _currentContext.GetOrganization(project.OrganizationId);\n\n        if (org == null || !await _currentContext.AccessEventLogs(org.Id))\n        {\n            throw new NotFoundException();\n        }\n    }\n\n    [ApiExplorerSettings(IgnoreApi = true)]\n    private async Task<Project> GetProject(Guid projectGuid, Guid orgGuid)\n    {\n        var project = await _projectRepository.GetByIdAsync(projectGuid);\n        if (project != null)\n        {\n            return project;\n        }\n\n        var fallbackProject = new Project\n        {\n            Id = projectGuid,\n            OrganizationId = orgGuid\n        };\n\n        return fallbackProject;\n    }\n\n    [ApiExplorerSettings(IgnoreApi = true)]\n    private async Task<bool> CanViewSecretsLogs(Secret secret)\n    {\n        if (!_currentContext.AccessSecretsManager(secret.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User)!.Value;\n        var isAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, isAdmin);\n        var access = await _secretRepository.AccessToSecretAsync(secret.Id, userId, accessClient);\n        return access.Read;\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Controllers/HibpController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing System.Security.Cryptography;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Dirt.Controllers;\n\n[Route(\"hibp\")]\n[Authorize(\"Application\")]\npublic class HibpController : Controller\n{\n    private const string HibpBreachApi = \"https://haveibeenpwned.com/api/v3/breachedaccount/{0}\" +\n        \"?truncateResponse=false&includeUnverified=false\";\n    private static HttpClient _httpClient;\n\n    private readonly IUserService _userService;\n    private readonly ICurrentContext _currentContext;\n    private readonly GlobalSettings _globalSettings;\n    private readonly string _userAgent;\n\n    static HibpController()\n    {\n        _httpClient = new HttpClient();\n    }\n\n    public HibpController(\n        IUserService userService,\n        ICurrentContext currentContext,\n        GlobalSettings globalSettings)\n    {\n        _userService = userService;\n        _currentContext = currentContext;\n        _globalSettings = globalSettings;\n        _userAgent = _globalSettings.SelfHosted ? \"Bitwarden Self-Hosted\" : \"Bitwarden\";\n    }\n\n    [HttpGet(\"breach\")]\n    public async Task<IActionResult> Get(string username)\n    {\n        return await SendAsync(WebUtility.UrlEncode(username), true);\n    }\n\n    private async Task<IActionResult> SendAsync(string username, bool retry)\n    {\n        if (!CoreHelpers.SettingHasValue(_globalSettings.HibpApiKey))\n        {\n            throw new BadRequestException(\"HaveIBeenPwned API key not set.\");\n        }\n        var request = new HttpRequestMessage(HttpMethod.Get, string.Format(HibpBreachApi, username));\n        request.Headers.Add(\"hibp-api-key\", _globalSettings.HibpApiKey);\n        request.Headers.Add(\"hibp-client-id\", GetClientId());\n        request.Headers.Add(\"User-Agent\", _userAgent);\n        var response = await _httpClient.SendAsync(request);\n        if (response.IsSuccessStatusCode)\n        {\n            var data = await response.Content.ReadAsStringAsync();\n            return Content(data, \"application/json\");\n        }\n        else if (response.StatusCode == HttpStatusCode.NotFound)\n        {\n            /* 12/1/2025 - Per the HIBP API, If the domain does not have any email addresses in any breaches, \n               an HTTP 404 response will be returned. API also specifies that \"404 Not found is the account could \n               not be found and has therefore not been pwned\". Per REST semantics we will return 200 OK with empty array. */\n            return Content(\"[]\", \"application/json\");\n        }\n        else if (response.StatusCode == HttpStatusCode.TooManyRequests && retry)\n        {\n            var delay = 2000;\n            if (response.Headers.Contains(\"retry-after\"))\n            {\n                var vals = response.Headers.GetValues(\"retry-after\");\n                if (vals.Any() && int.TryParse(vals.FirstOrDefault(), out var secDelay))\n                {\n                    delay = (secDelay * 1000) + 200;\n                }\n            }\n            await Task.Delay(delay);\n            return await SendAsync(username, false);\n        }\n        else\n        {\n            throw new BadRequestException(\"Request failed. Status code: \" + response.StatusCode);\n        }\n    }\n\n    private string GetClientId()\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        using (var sha256 = SHA256.Create())\n        {\n            var hash = sha256.ComputeHash(userId.ToByteArray());\n            return Convert.ToBase64String(hash);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Controllers/OrganizationIntegrationConfigurationController.cs",
    "content": "﻿using Bit.Api.Dirt.Models.Request;\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Dirt.Controllers;\n\n[Route(\"organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations\")]\n[Authorize(\"Application\")]\npublic class OrganizationIntegrationConfigurationController(\n    ICurrentContext currentContext,\n    ICreateOrganizationIntegrationConfigurationCommand createCommand,\n    IUpdateOrganizationIntegrationConfigurationCommand updateCommand,\n    IDeleteOrganizationIntegrationConfigurationCommand deleteCommand,\n    IGetOrganizationIntegrationConfigurationsQuery getQuery) : Controller\n{\n    [HttpGet(\"\")]\n    public async Task<List<OrganizationIntegrationConfigurationResponseModel>> GetAsync(\n        Guid organizationId,\n        Guid integrationId)\n    {\n        if (!await HasPermission(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var configurations = await getQuery.GetManyByIntegrationAsync(organizationId, integrationId);\n        return configurations\n            .Select(configuration => new OrganizationIntegrationConfigurationResponseModel(configuration))\n            .ToList();\n    }\n\n    [HttpPost(\"\")]\n    public async Task<OrganizationIntegrationConfigurationResponseModel> CreateAsync(\n        Guid organizationId,\n        Guid integrationId,\n        [FromBody] OrganizationIntegrationConfigurationRequestModel model)\n    {\n        if (!await HasPermission(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var configuration = model.ToOrganizationIntegrationConfiguration(integrationId);\n        var created = await createCommand.CreateAsync(organizationId, integrationId, configuration);\n\n        return new OrganizationIntegrationConfigurationResponseModel(created);\n    }\n\n    [HttpPut(\"{configurationId:guid}\")]\n    public async Task<OrganizationIntegrationConfigurationResponseModel> UpdateAsync(\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        [FromBody] OrganizationIntegrationConfigurationRequestModel model)\n    {\n        if (!await HasPermission(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var configuration = model.ToOrganizationIntegrationConfiguration(integrationId);\n        var updated = await updateCommand.UpdateAsync(organizationId, integrationId, configurationId, configuration);\n\n        return new OrganizationIntegrationConfigurationResponseModel(updated);\n    }\n\n    [HttpDelete(\"{configurationId:guid}\")]\n    public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)\n    {\n        if (!await HasPermission(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        await deleteCommand.DeleteAsync(organizationId, integrationId, configurationId);\n    }\n\n    [HttpPost(\"{configurationId:guid}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)\n    {\n        await DeleteAsync(organizationId, integrationId, configurationId);\n    }\n\n    private async Task<bool> HasPermission(Guid organizationId)\n    {\n        return await currentContext.OrganizationOwner(organizationId);\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Controllers/OrganizationIntegrationController.cs",
    "content": "﻿using Bit.Api.Dirt.Models.Request;\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Dirt.Controllers;\n\n[Route(\"organizations/{organizationId:guid}/integrations\")]\n[Authorize(\"Application\")]\npublic class OrganizationIntegrationController(\n    ICurrentContext currentContext,\n    ICreateOrganizationIntegrationCommand createCommand,\n    IUpdateOrganizationIntegrationCommand updateCommand,\n    IDeleteOrganizationIntegrationCommand deleteCommand,\n    IGetOrganizationIntegrationsQuery getQuery) : Controller\n{\n    [HttpGet(\"\")]\n    public async Task<List<OrganizationIntegrationResponseModel>> GetAsync(Guid organizationId)\n    {\n        if (!await HasPermission(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var integrations = await getQuery.GetManyByOrganizationAsync(organizationId);\n        return integrations\n            .Select(integration => new OrganizationIntegrationResponseModel(integration))\n            .ToList();\n    }\n\n    [HttpPost(\"\")]\n    public async Task<OrganizationIntegrationResponseModel> CreateAsync(Guid organizationId, [FromBody] OrganizationIntegrationRequestModel model)\n    {\n        if (!await HasPermission(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var integration = model.ToOrganizationIntegration(organizationId);\n        var created = await createCommand.CreateAsync(integration);\n\n        return new OrganizationIntegrationResponseModel(created);\n    }\n\n    [HttpPut(\"{integrationId:guid}\")]\n    public async Task<OrganizationIntegrationResponseModel> UpdateAsync(Guid organizationId, Guid integrationId, [FromBody] OrganizationIntegrationRequestModel model)\n    {\n        if (!await HasPermission(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var integration = model.ToOrganizationIntegration(organizationId);\n        var updated = await updateCommand.UpdateAsync(organizationId, integrationId, integration);\n\n        return new OrganizationIntegrationResponseModel(updated);\n    }\n\n    [HttpDelete(\"{integrationId:guid}\")]\n    public async Task DeleteAsync(Guid organizationId, Guid integrationId)\n    {\n        if (!await HasPermission(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        await deleteCommand.DeleteAsync(organizationId, integrationId);\n    }\n\n    [HttpPost(\"{integrationId:guid}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDeleteAsync(Guid organizationId, Guid integrationId)\n    {\n        await DeleteAsync(organizationId, integrationId);\n    }\n\n    private async Task<bool> HasPermission(Guid organizationId)\n    {\n        return await currentContext.OrganizationOwner(organizationId);\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Controllers/OrganizationReportsController.cs",
    "content": "﻿using Bit.Api.Dirt.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Dirt.Controllers;\n\n[Route(\"reports/organizations\")]\n[Authorize(\"Application\")]\npublic class OrganizationReportsController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IGetOrganizationReportQuery _getOrganizationReportQuery;\n    private readonly IAddOrganizationReportCommand _addOrganizationReportCommand;\n    private readonly IUpdateOrganizationReportCommand _updateOrganizationReportCommand;\n    private readonly IUpdateOrganizationReportSummaryCommand _updateOrganizationReportSummaryCommand;\n    private readonly IGetOrganizationReportSummaryDataQuery _getOrganizationReportSummaryDataQuery;\n    private readonly IGetOrganizationReportSummaryDataByDateRangeQuery _getOrganizationReportSummaryDataByDateRangeQuery;\n    private readonly IGetOrganizationReportDataQuery _getOrganizationReportDataQuery;\n    private readonly IUpdateOrganizationReportDataCommand _updateOrganizationReportDataCommand;\n    private readonly IGetOrganizationReportApplicationDataQuery _getOrganizationReportApplicationDataQuery;\n    private readonly IUpdateOrganizationReportApplicationDataCommand _updateOrganizationReportApplicationDataCommand;\n\n    public OrganizationReportsController(\n        ICurrentContext currentContext,\n        IGetOrganizationReportQuery getOrganizationReportQuery,\n        IAddOrganizationReportCommand addOrganizationReportCommand,\n        IUpdateOrganizationReportCommand updateOrganizationReportCommand,\n        IUpdateOrganizationReportSummaryCommand updateOrganizationReportSummaryCommand,\n        IGetOrganizationReportSummaryDataQuery getOrganizationReportSummaryDataQuery,\n        IGetOrganizationReportSummaryDataByDateRangeQuery getOrganizationReportSummaryDataByDateRangeQuery,\n        IGetOrganizationReportDataQuery getOrganizationReportDataQuery,\n        IUpdateOrganizationReportDataCommand updateOrganizationReportDataCommand,\n        IGetOrganizationReportApplicationDataQuery getOrganizationReportApplicationDataQuery,\n        IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicationDataCommand\n    )\n    {\n        _currentContext = currentContext;\n        _getOrganizationReportQuery = getOrganizationReportQuery;\n        _addOrganizationReportCommand = addOrganizationReportCommand;\n        _updateOrganizationReportCommand = updateOrganizationReportCommand;\n        _updateOrganizationReportSummaryCommand = updateOrganizationReportSummaryCommand;\n        _getOrganizationReportSummaryDataQuery = getOrganizationReportSummaryDataQuery;\n        _getOrganizationReportSummaryDataByDateRangeQuery = getOrganizationReportSummaryDataByDateRangeQuery;\n        _getOrganizationReportDataQuery = getOrganizationReportDataQuery;\n        _updateOrganizationReportDataCommand = updateOrganizationReportDataCommand;\n        _getOrganizationReportApplicationDataQuery = getOrganizationReportApplicationDataQuery;\n        _updateOrganizationReportApplicationDataCommand = updateOrganizationReportApplicationDataCommand;\n    }\n\n    #region Whole OrganizationReport Endpoints\n\n    [HttpGet(\"{organizationId}/latest\")]\n    public async Task<IActionResult> GetLatestOrganizationReportAsync(Guid organizationId)\n    {\n        if (!await _currentContext.AccessReports(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var latestReport = await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(organizationId);\n        var response = latestReport == null ? null : new OrganizationReportResponseModel(latestReport);\n\n        return Ok(response);\n    }\n\n    [HttpGet(\"{organizationId}/{reportId}\")]\n    public async Task<IActionResult> GetOrganizationReportAsync(Guid organizationId, Guid reportId)\n    {\n        if (!await _currentContext.AccessReports(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);\n\n        if (report == null)\n        {\n            throw new NotFoundException(\"Report not found for the specified organization.\");\n        }\n\n        if (report.OrganizationId != organizationId)\n        {\n            throw new BadRequestException(\"Invalid report ID\");\n        }\n\n        return Ok(report);\n    }\n\n    [HttpPost(\"{organizationId}\")]\n    public async Task<IActionResult> CreateOrganizationReportAsync(Guid organizationId, [FromBody] AddOrganizationReportRequest request)\n    {\n        if (!await _currentContext.AccessReports(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (request.OrganizationId != organizationId)\n        {\n            throw new BadRequestException(\"Organization ID in the request body must match the route parameter\");\n        }\n\n        var report = await _addOrganizationReportCommand.AddOrganizationReportAsync(request);\n        var response = report == null ? null : new OrganizationReportResponseModel(report);\n        return Ok(response);\n    }\n\n    [HttpPatch(\"{organizationId}/{reportId}\")]\n    public async Task<IActionResult> UpdateOrganizationReportAsync(Guid organizationId, [FromBody] UpdateOrganizationReportRequest request)\n    {\n        if (!await _currentContext.AccessReports(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (request.OrganizationId != organizationId)\n        {\n            throw new BadRequestException(\"Organization ID in the request body must match the route parameter\");\n        }\n\n        var updatedReport = await _updateOrganizationReportCommand.UpdateOrganizationReportAsync(request);\n        var response = new OrganizationReportResponseModel(updatedReport);\n        return Ok(response);\n    }\n\n    #endregion\n\n    # region SummaryData Field Endpoints\n\n    /// <summary>\n    /// Gets summary data for organization reports within a specified date range. \n    /// The response is optimized for widget display by returning up to 6 entries that are \n    /// evenly spaced across the date range, including the most recent entry. \n    /// This allows the widget to show trends over time while ensuring the latest data point is always included.\n    /// </summary>\n    /// <param name=\"organizationId\"></param>\n    /// <param name=\"startDate\"></param>\n    /// <param name=\"endDate\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"NotFoundException\"></exception>\n    /// <exception cref=\"BadRequestException\"></exception>\n    [HttpGet(\"{organizationId}/data/summary\")]\n    [ProducesResponseType<IEnumerable<OrganizationReportSummaryDataResponse>>(StatusCodes.Status200OK)]\n    [ProducesResponseType(StatusCodes.Status400BadRequest)]\n    [ProducesResponseType(StatusCodes.Status404NotFound)]\n    public async Task<IActionResult> GetOrganizationReportSummaryDataByDateRangeAsync(\n        Guid organizationId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate)\n    {\n        if (!await _currentContext.AccessReports(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (organizationId == Guid.Empty)\n        {\n            throw new BadRequestException(\"Organization ID is required.\");\n        }\n\n        var summaryDataList = await _getOrganizationReportSummaryDataByDateRangeQuery\n            .GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);\n\n        return Ok(summaryDataList);\n    }\n\n    [HttpGet(\"{organizationId}/data/summary/{reportId}\")]\n    public async Task<IActionResult> GetOrganizationReportSummaryAsync(Guid organizationId, Guid reportId)\n    {\n        if (!await _currentContext.AccessReports(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var summaryData =\n            await _getOrganizationReportSummaryDataQuery.GetOrganizationReportSummaryDataAsync(organizationId, reportId);\n\n        if (summaryData == null)\n        {\n            throw new NotFoundException(\"Report not found for the specified organization.\");\n        }\n\n        return Ok(summaryData);\n    }\n\n    [HttpPatch(\"{organizationId}/data/summary/{reportId}\")]\n    public async Task<IActionResult> UpdateOrganizationReportSummaryAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportSummaryRequest request)\n    {\n        if (!await _currentContext.AccessReports(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (request.OrganizationId != organizationId)\n        {\n            throw new BadRequestException(\"Organization ID in the request body must match the route parameter\");\n        }\n\n        if (request.ReportId != reportId)\n        {\n            throw new BadRequestException(\"Report ID in the request body must match the route parameter\");\n        }\n        var updatedReport = await _updateOrganizationReportSummaryCommand.UpdateOrganizationReportSummaryAsync(request);\n        var response = new OrganizationReportResponseModel(updatedReport);\n\n        return Ok(response);\n    }\n    #endregion\n\n    #region ReportData Field Endpoints\n\n    [HttpGet(\"{organizationId}/data/report/{reportId}\")]\n    public async Task<IActionResult> GetOrganizationReportDataAsync(Guid organizationId, Guid reportId)\n    {\n        if (!await _currentContext.AccessReports(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var reportData = await _getOrganizationReportDataQuery.GetOrganizationReportDataAsync(organizationId, reportId);\n\n        if (reportData == null)\n        {\n            throw new NotFoundException(\"Organization report data not found.\");\n        }\n\n        return Ok(reportData);\n    }\n\n    [HttpPatch(\"{organizationId}/data/report/{reportId}\")]\n    public async Task<IActionResult> UpdateOrganizationReportDataAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportDataRequest request)\n    {\n        if (!await _currentContext.AccessReports(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (request.OrganizationId != organizationId)\n        {\n            throw new BadRequestException(\"Organization ID in the request body must match the route parameter\");\n        }\n\n        if (request.ReportId != reportId)\n        {\n            throw new BadRequestException(\"Report ID in the request body must match the route parameter\");\n        }\n\n        var updatedReport = await _updateOrganizationReportDataCommand.UpdateOrganizationReportDataAsync(request);\n        var response = new OrganizationReportResponseModel(updatedReport);\n\n        return Ok(response);\n    }\n\n    #endregion\n\n    #region ApplicationData Field Endpoints\n\n    [HttpGet(\"{organizationId}/data/application/{reportId}\")]\n    public async Task<IActionResult> GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId)\n    {\n        try\n        {\n            if (!await _currentContext.AccessReports(organizationId))\n            {\n                throw new NotFoundException();\n            }\n\n            var applicationData = await _getOrganizationReportApplicationDataQuery.GetOrganizationReportApplicationDataAsync(organizationId, reportId);\n\n            if (applicationData == null)\n            {\n                throw new NotFoundException(\"Organization report application data not found.\");\n            }\n\n            return Ok(applicationData);\n        }\n        catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))\n        {\n            throw;\n        }\n    }\n\n    [HttpPatch(\"{organizationId}/data/application/{reportId}\")]\n    public async Task<IActionResult> UpdateOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportApplicationDataRequest request)\n    {\n        try\n        {\n            if (!await _currentContext.AccessReports(organizationId))\n            {\n                throw new NotFoundException();\n            }\n\n            if (request.OrganizationId != organizationId)\n            {\n                throw new BadRequestException(\"Organization ID in the request body must match the route parameter\");\n            }\n\n            if (request.Id != reportId)\n            {\n                throw new BadRequestException(\"Report ID in the request body must match the route parameter\");\n            }\n\n            var updatedReport = await _updateOrganizationReportApplicationDataCommand.UpdateOrganizationReportApplicationDataAsync(request);\n            var response = new OrganizationReportResponseModel(updatedReport);\n\n            return Ok(response);\n        }\n        catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))\n        {\n            throw;\n        }\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Api/Dirt/Controllers/ReportsController.cs",
    "content": "﻿using Bit.Api.Dirt.Models;\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Api.Tools.Models.Response;\nusing Bit.Core;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Dirt.Controllers;\n\n[Route(\"reports\")]\n[Authorize(\"Application\")]\npublic class ReportsController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IMemberAccessReportQuery _memberAccessReportQuery;\n    private readonly IRiskInsightsReportQuery _riskInsightsReportQuery;\n    private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand;\n    private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;\n    private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;\n    private readonly IAddOrganizationReportCommand _addOrganizationReportCommand;\n    private readonly IGetOrganizationReportQuery _getOrganizationReportQuery;\n    private readonly ILogger<ReportsController> _logger;\n\n    public ReportsController(\n        ICurrentContext currentContext,\n        IMemberAccessReportQuery memberAccessReportQuery,\n        IRiskInsightsReportQuery riskInsightsReportQuery,\n        IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand,\n        IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery,\n        IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand,\n        IGetOrganizationReportQuery getOrganizationReportQuery,\n        IAddOrganizationReportCommand addOrganizationReportCommand,\n        ILogger<ReportsController> logger\n    )\n    {\n        _currentContext = currentContext;\n        _memberAccessReportQuery = memberAccessReportQuery;\n        _riskInsightsReportQuery = riskInsightsReportQuery;\n        _addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand;\n        _getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery;\n        _dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;\n        _getOrganizationReportQuery = getOrganizationReportQuery;\n        _addOrganizationReportCommand = addOrganizationReportCommand;\n        _logger = logger;\n    }\n\n    /// <summary>\n    /// Organization member information containing a list of cipher ids\n    /// assigned\n    /// </summary>\n    /// <param name=\"orgId\">Organzation Id</param>\n    /// <returns>IEnumerable of MemberCipherDetailsResponseModel</returns>\n    /// <exception cref=\"NotFoundException\">If Access reports permission is not assigned</exception>\n    [HttpGet(\"member-cipher-details/{orgId}\")]\n    public async Task<IEnumerable<MemberCipherDetailsResponseModel>> GetMemberCipherDetails(Guid orgId)\n    {\n        // Using the AccessReports permission here until new permissions\n        // are needed for more control over reports\n        if (!await _currentContext.AccessReports(orgId))\n        {\n            throw new NotFoundException();\n        }\n\n        var riskDetails = await GetRiskInsightsReportDetails(new RiskInsightsReportRequest { OrganizationId = orgId });\n\n        var responses = riskDetails.Select(x => new MemberCipherDetailsResponseModel(x));\n\n        return responses;\n    }\n\n    /// <summary>\n    /// Access details for an organization member. Includes the member information,\n    /// group collection assignment, and item counts\n    /// </summary>\n    /// <param name=\"orgId\">Organization Id</param>\n    /// <returns>IEnumerable of MemberAccessReportResponseModel</returns>\n    /// <exception cref=\"NotFoundException\">If Access reports permission is not assigned</exception>\n    [HttpGet(\"member-access/{orgId}\")]\n    public async Task<IEnumerable<MemberAccessDetailReportResponseModel>> GetMemberAccessReport(Guid orgId)\n    {\n        if (!await _currentContext.AccessReports(orgId))\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId,\n                \"AccessReports Check - UserId: {userId} OrgId: {orgId} DeviceType: {deviceType}\",\n                _currentContext.UserId, orgId, _currentContext.DeviceType);\n            throw new NotFoundException();\n        }\n\n        _logger.LogInformation(Constants.BypassFiltersEventId,\n            \"MemberAccessReportQuery starts - UserId: {userId} OrgId: {orgId} DeviceType: {deviceType}\",\n            _currentContext.UserId, orgId, _currentContext.DeviceType);\n\n        var accessDetails = await _memberAccessReportQuery\n            .GetMemberAccessReportsAsync(new MemberAccessReportRequest { OrganizationId = orgId });\n\n        var responses = accessDetails.Select(x => new MemberAccessDetailReportResponseModel(x));\n\n        return responses;\n    }\n\n    /// <summary>\n    /// Gets the risk insights report details from the risk insights query. Associates a user to their cipher ids\n    /// </summary>\n    /// <param name=\"request\">Request parameters</param>\n    /// <returns>A list of risk insights data associating the user to cipher ids</returns>\n    private async Task<IEnumerable<RiskInsightsReportDetail>> GetRiskInsightsReportDetails(\n        RiskInsightsReportRequest request)\n    {\n        var riskDetails = await _riskInsightsReportQuery.GetRiskInsightsReportDetails(request);\n        return riskDetails;\n    }\n\n    /// <summary>\n    /// Get the password health report applications for an organization\n    /// </summary>\n    /// <param name=\"orgId\">A valid Organization Id</param>\n    /// <returns>An Enumerable of PasswordHealthReportApplication </returns>\n    /// <exception cref=\"NotFoundException\">If the user lacks access</exception>\n    /// <exception cref=\"BadRequestException\">If the organization Id is not valid</exception>\n    [HttpGet(\"password-health-report-applications/{orgId}\")]\n    public async Task<IEnumerable<PasswordHealthReportApplication>> GetPasswordHealthReportApplications(Guid orgId)\n    {\n        if (!await _currentContext.AccessReports(orgId))\n        {\n            throw new NotFoundException();\n        }\n\n        return await _getPwdHealthReportAppQuery.GetPasswordHealthReportApplicationAsync(orgId);\n    }\n\n    /// <summary>\n    /// Adds a new record into PasswordHealthReportApplication\n    /// </summary>\n    /// <param name=\"request\">A single instance of PasswordHealthReportApplication Model</param>\n    /// <returns>A single instance of PasswordHealthReportApplication</returns>\n    /// <exception cref=\"BadRequestException\">If the organization Id is not valid</exception>\n    /// <exception cref=\"NotFoundException\">If the user lacks access</exception>\n    [HttpPost(\"password-health-report-application\")]\n    public async Task<PasswordHealthReportApplication> AddPasswordHealthReportApplication(\n        [FromBody] PasswordHealthReportApplicationModel request)\n    {\n        if (!await _currentContext.AccessReports(request.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var commandRequest = new AddPasswordHealthReportApplicationRequest\n        {\n            OrganizationId = request.OrganizationId,\n            Url = request.Url\n        };\n\n        return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequest);\n    }\n\n    /// <summary>\n    /// Adds multiple records into PasswordHealthReportApplication\n    /// </summary>\n    /// <param name=\"request\">A enumerable of PasswordHealthReportApplicationModel</param>\n    /// <returns>An Enumerable of PasswordHealthReportApplication</returns>\n    /// <exception cref=\"NotFoundException\">If user does not have access to the OrganizationId</exception>\n    /// <exception cref=\"BadRequestException\">If the organization Id is not valid</exception>\n    [HttpPost(\"password-health-report-applications\")]\n    public async Task<IEnumerable<PasswordHealthReportApplication>> AddPasswordHealthReportApplications(\n        [FromBody] IEnumerable<PasswordHealthReportApplicationModel> request)\n    {\n        if (request.Any(_ => _currentContext.AccessReports(_.OrganizationId).Result == false))\n        {\n            throw new NotFoundException();\n        }\n\n        var commandRequests = request.Select(request => new AddPasswordHealthReportApplicationRequest\n        {\n            OrganizationId = request.OrganizationId,\n            Url = request.Url\n        }).ToList();\n\n        return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequests);\n    }\n\n    /// <summary>\n    /// Drops a record from PasswordHealthReportApplication\n    /// </summary>\n    /// <param name=\"request\">\n    ///     A single instance of DropPasswordHealthReportApplicationRequest\n    ///     { OrganizationId, array of PasswordHealthReportApplicationIds }\n    /// </param>\n    /// <returns></returns>\n    /// <exception cref=\"NotFoundException\">If user does not have access to the organization</exception>\n    /// <exception cref=\"BadRequestException\">If the organization does not have any records</exception>\n    [HttpDelete(\"password-health-report-application\")]\n    public async Task DropPasswordHealthReportApplication(\n        [FromBody] DropPasswordHealthReportApplicationRequest request)\n    {\n        if (!await _currentContext.AccessReports(request.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        await _dropPwdHealthReportAppCommand.DropPasswordHealthReportApplicationAsync(request);\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Controllers/SlackIntegrationController.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Dirt.Controllers;\n\n[Route(\"organizations\")]\n[Authorize(\"Application\")]\npublic class SlackIntegrationController(\n    ICurrentContext currentContext,\n    IOrganizationIntegrationRepository integrationRepository,\n    ISlackService slackService,\n    TimeProvider timeProvider) : Controller\n{\n    [HttpGet(\"{organizationId:guid}/integrations/slack/redirect\")]\n    public async Task<IActionResult> RedirectAsync(Guid organizationId)\n    {\n        if (!await currentContext.OrganizationOwner(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        string? callbackUrl = Url.RouteUrl(\n            routeName: \"SlackIntegration_Create\",\n            values: null,\n            protocol: currentContext.HttpContext.Request.Scheme,\n            host: currentContext.HttpContext.Request.Host.ToUriComponent()\n        );\n        if (string.IsNullOrEmpty(callbackUrl))\n        {\n            throw new BadRequestException(\"Unable to build callback Url\");\n        }\n\n        var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);\n        var integration = integrations.FirstOrDefault(i => i.Type == IntegrationType.Slack);\n\n        if (integration is null)\n        {\n            // No slack integration exists, create Initiated version\n            integration = await integrationRepository.CreateAsync(new OrganizationIntegration\n            {\n                OrganizationId = organizationId,\n                Type = IntegrationType.Slack,\n                Configuration = null,\n            });\n        }\n        else if (integration.Configuration is not null)\n        {\n            // A Completed (fully configured) Slack integration already exists, throw to prevent overriding\n            throw new BadRequestException(\"There already exists a Slack integration for this organization\");\n\n        } // An Initiated slack integration exits, re-use it and kick off a new OAuth flow\n\n        var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);\n        var redirectUrl = slackService.GetRedirectUrl(\n            callbackUrl: callbackUrl,\n            state: state.ToString()\n        );\n\n        if (string.IsNullOrEmpty(redirectUrl))\n        {\n            throw new NotFoundException();\n        }\n\n        return Redirect(redirectUrl);\n    }\n\n    [HttpGet(\"integrations/slack/create\", Name = \"SlackIntegration_Create\")]\n    [AllowAnonymous]\n    public async Task<IActionResult> CreateAsync([FromQuery] string code, [FromQuery] string state)\n    {\n        var oAuthState = IntegrationOAuthState.FromString(state: state, timeProvider: timeProvider);\n        if (oAuthState is null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Fetch existing Initiated record\n        var integration = await integrationRepository.GetByIdAsync(oAuthState.IntegrationId);\n        if (integration is null ||\n            integration.Type != IntegrationType.Slack ||\n            integration.Configuration is not null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Verify Organization matches hash\n        if (!oAuthState.ValidateOrg(integration.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        // Fetch token from Slack and store to DB\n        string? callbackUrl = Url.RouteUrl(\n            routeName: \"SlackIntegration_Create\",\n            values: null,\n            protocol: currentContext.HttpContext.Request.Scheme,\n            host: currentContext.HttpContext.Request.Host.ToUriComponent()\n        );\n        if (string.IsNullOrEmpty(callbackUrl))\n        {\n            throw new BadRequestException(\"Unable to build callback Url\");\n        }\n        var token = await slackService.ObtainTokenViaOAuth(code, callbackUrl);\n\n        if (string.IsNullOrEmpty(token))\n        {\n            throw new BadRequestException(\"Invalid response from Slack.\");\n        }\n\n        integration.Configuration = JsonSerializer.Serialize(new SlackIntegration(token));\n        await integrationRepository.UpsertAsync(integration);\n\n        var location = $\"/organizations/{integration.OrganizationId}/integrations/{integration.Id}\";\n        return Created(location, new OrganizationIntegrationResponseModel(integration));\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Controllers/TeamsIntegrationController.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Bot.Builder;\nusing Microsoft.Bot.Builder.Integration.AspNet.Core;\n\nnamespace Bit.Api.Dirt.Controllers;\n\n[Route(\"organizations\")]\n[Authorize(\"Application\")]\npublic class TeamsIntegrationController(\n    ICurrentContext currentContext,\n    IOrganizationIntegrationRepository integrationRepository,\n    IBot bot,\n    IBotFrameworkHttpAdapter adapter,\n    ITeamsService teamsService,\n    TimeProvider timeProvider) : Controller\n{\n    [HttpGet(\"{organizationId:guid}/integrations/teams/redirect\")]\n    public async Task<IActionResult> RedirectAsync(Guid organizationId)\n    {\n        if (!await currentContext.OrganizationOwner(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var callbackUrl = Url.RouteUrl(\n            routeName: \"TeamsIntegration_Create\",\n            values: null,\n            protocol: currentContext.HttpContext.Request.Scheme,\n            host: currentContext.HttpContext.Request.Host.ToUriComponent()\n        );\n        if (string.IsNullOrEmpty(callbackUrl))\n        {\n            throw new BadRequestException(\"Unable to build callback Url\");\n        }\n\n        var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);\n        var integration = integrations.FirstOrDefault(i => i.Type == IntegrationType.Teams);\n\n        if (integration is null)\n        {\n            // No teams integration exists, create Initiated version\n            integration = await integrationRepository.CreateAsync(new OrganizationIntegration\n            {\n                OrganizationId = organizationId,\n                Type = IntegrationType.Teams,\n                Configuration = null,\n            });\n        }\n        else if (integration.Configuration is not null)\n        {\n            // A Completed (fully configured) Teams integration already exists, throw to prevent overriding\n            throw new BadRequestException(\"There already exists a Teams integration for this organization\");\n\n        } // An Initiated teams integration exits, re-use it and kick off a new OAuth flow\n\n        var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);\n        var redirectUrl = teamsService.GetRedirectUrl(\n            callbackUrl: callbackUrl,\n            state: state.ToString()\n        );\n\n        if (string.IsNullOrEmpty(redirectUrl))\n        {\n            throw new NotFoundException();\n        }\n\n        return Redirect(redirectUrl);\n    }\n\n    [HttpGet(\"integrations/teams/create\", Name = \"TeamsIntegration_Create\")]\n    [AllowAnonymous]\n    public async Task<IActionResult> CreateAsync([FromQuery] string code, [FromQuery] string state)\n    {\n        var oAuthState = IntegrationOAuthState.FromString(state: state, timeProvider: timeProvider);\n        if (oAuthState is null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Fetch existing Initiated record\n        var integration = await integrationRepository.GetByIdAsync(oAuthState.IntegrationId);\n        if (integration is null ||\n            integration.Type != IntegrationType.Teams ||\n            integration.Configuration is not null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Verify Organization matches hash\n        if (!oAuthState.ValidateOrg(integration.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var callbackUrl = Url.RouteUrl(\n            routeName: \"TeamsIntegration_Create\",\n            values: null,\n            protocol: currentContext.HttpContext.Request.Scheme,\n            host: currentContext.HttpContext.Request.Host.ToUriComponent()\n        );\n        if (string.IsNullOrEmpty(callbackUrl))\n        {\n            throw new BadRequestException(\"Unable to build callback Url\");\n        }\n\n        var token = await teamsService.ObtainTokenViaOAuth(code, callbackUrl);\n        if (string.IsNullOrEmpty(token))\n        {\n            throw new BadRequestException(\"Invalid response from Teams.\");\n        }\n\n        var teams = await teamsService.GetJoinedTeamsAsync(token);\n\n        if (!teams.Any())\n        {\n            throw new BadRequestException(\"No teams were found.\");\n        }\n\n        var teamsIntegration = new TeamsIntegration(TenantId: teams[0].TenantId, Teams: teams);\n        integration.Configuration = JsonSerializer.Serialize(teamsIntegration);\n        await integrationRepository.UpsertAsync(integration);\n\n        var location = $\"/organizations/{integration.OrganizationId}/integrations/{integration.Id}\";\n        return Created(location, new OrganizationIntegrationResponseModel(integration));\n    }\n\n    [Route(\"integrations/teams/incoming\")]\n    [AllowAnonymous]\n    [HttpPost]\n    public async Task IncomingPostAsync()\n    {\n        await adapter.ProcessAsync(Request, Response, bot);\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/PasswordHealthReportApplicationModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Api.Dirt.Models;\n\npublic class PasswordHealthReportApplicationModel\n{\n    public Guid OrganizationId { get; set; }\n    public string Url { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Request/OrganizationIntegrationConfigurationRequestModel.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.Dirt.Models.Request;\n\npublic class OrganizationIntegrationConfigurationRequestModel\n{\n    public string? Configuration { get; set; }\n\n    public EventType? EventType { get; set; }\n\n    public string? Filters { get; set; }\n\n    public string? Template { get; set; }\n\n    public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(Guid organizationIntegrationId)\n    {\n        return new OrganizationIntegrationConfiguration()\n        {\n            OrganizationIntegrationId = organizationIntegrationId,\n            Configuration = Configuration,\n            Filters = Filters,\n            EventType = EventType,\n            Template = Template\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Request/OrganizationIntegrationRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\n\nnamespace Bit.Api.Dirt.Models.Request;\n\npublic class OrganizationIntegrationRequestModel : IValidatableObject\n{\n    public string? Configuration { get; init; }\n\n    public IntegrationType Type { get; init; }\n\n    public OrganizationIntegration ToOrganizationIntegration(Guid organizationId)\n    {\n        return new OrganizationIntegration()\n        {\n            OrganizationId = organizationId,\n            Configuration = Configuration,\n            Type = Type,\n        };\n    }\n\n    public OrganizationIntegration ToOrganizationIntegration(OrganizationIntegration currentIntegration)\n    {\n        currentIntegration.Configuration = Configuration;\n        return currentIntegration;\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        switch (Type)\n        {\n            case IntegrationType.CloudBillingSync or IntegrationType.Scim:\n                yield return new ValidationResult($\"{nameof(Type)} integrations are not yet supported.\", [nameof(Type)]);\n                break;\n            case IntegrationType.Slack or IntegrationType.Teams:\n                yield return new ValidationResult($\"{nameof(Type)} integrations cannot be created directly.\", [nameof(Type)]);\n                break;\n            case IntegrationType.Webhook:\n                foreach (var r in ValidateConfiguration<WebhookIntegration>(allowNullOrEmpty: true))\n                    yield return r;\n                break;\n            case IntegrationType.Hec:\n                foreach (var r in ValidateConfiguration<HecIntegration>(allowNullOrEmpty: false))\n                    yield return r;\n                break;\n            case IntegrationType.Datadog:\n                foreach (var r in ValidateConfiguration<DatadogIntegration>(allowNullOrEmpty: false))\n                    yield return r;\n                break;\n            default:\n                yield return new ValidationResult(\n                    $\"Integration type '{Type}' is not recognized.\",\n                    [nameof(Type)]);\n                break;\n        }\n    }\n\n    private List<ValidationResult> ValidateConfiguration<T>(bool allowNullOrEmpty)\n    {\n        var results = new List<ValidationResult>();\n\n        if (string.IsNullOrWhiteSpace(Configuration))\n        {\n            if (!allowNullOrEmpty)\n                results.Add(InvalidConfig<T>());\n            return results;\n        }\n\n        try\n        {\n            if (JsonSerializer.Deserialize<T>(Configuration) is null)\n                results.Add(InvalidConfig<T>());\n        }\n        catch\n        {\n            results.Add(InvalidConfig<T>());\n        }\n\n        return results;\n    }\n\n    private static ValidationResult InvalidConfig<T>() =>\n        new(errorMessage: $\"Must include valid {typeof(T).Name} configuration.\", memberNames: [nameof(Configuration)]);\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Response/EventResponseModel.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.Dirt.Models.Response;\n\npublic class EventResponseModel : ResponseModel\n{\n    public EventResponseModel(IEvent ev)\n        : base(\"event\")\n    {\n        if (ev == null)\n        {\n            throw new ArgumentNullException(nameof(ev));\n        }\n\n        Type = ev.Type;\n        UserId = ev.UserId;\n        OrganizationId = ev.OrganizationId;\n        ProviderId = ev.ProviderId;\n        CipherId = ev.CipherId;\n        CollectionId = ev.CollectionId;\n        GroupId = ev.GroupId;\n        PolicyId = ev.PolicyId;\n        OrganizationUserId = ev.OrganizationUserId;\n        ProviderUserId = ev.ProviderUserId;\n        ProviderOrganizationId = ev.ProviderOrganizationId;\n        ActingUserId = ev.ActingUserId;\n        Date = ev.Date;\n        DeviceType = ev.DeviceType;\n        IpAddress = ev.IpAddress;\n        InstallationId = ev.InstallationId;\n        SystemUser = ev.SystemUser;\n        DomainName = ev.DomainName;\n        SecretId = ev.SecretId;\n        ProjectId = ev.ProjectId;\n        ServiceAccountId = ev.ServiceAccountId;\n        GrantedServiceAccountId = ev.GrantedServiceAccountId;\n    }\n\n    public EventType Type { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Guid? ProviderId { get; set; }\n    public Guid? CipherId { get; set; }\n    public Guid? CollectionId { get; set; }\n    public Guid? GroupId { get; set; }\n    public Guid? PolicyId { get; set; }\n    public Guid? OrganizationUserId { get; set; }\n    public Guid? ProviderUserId { get; set; }\n    public Guid? ProviderOrganizationId { get; set; }\n    public Guid? ActingUserId { get; set; }\n    public Guid? InstallationId { get; set; }\n    public DateTime Date { get; set; }\n    public DeviceType? DeviceType { get; set; }\n    public string IpAddress { get; set; }\n    public EventSystemUser? SystemUser { get; set; }\n    public string DomainName { get; set; }\n    public Guid? SecretId { get; set; }\n    public Guid? ProjectId { get; set; }\n    public Guid? ServiceAccountId { get; set; }\n    public Guid? GrantedServiceAccountId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Response/MemberAccessDetailReportResponseModel.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.Models.Data;\n\nnamespace Bit.Api.Tools.Models.Response;\n\npublic class MemberAccessDetailReportResponseModel\n{\n    public Guid? UserGuid { get; set; }\n    public string UserName { get; set; }\n    public string Email { get; set; }\n    public bool TwoFactorEnabled { get; set; }\n    public bool AccountRecoveryEnabled { get; set; }\n    public bool UsesKeyConnector { get; set; }\n    public Guid? CollectionId { get; set; }\n    public Guid? GroupId { get; set; }\n    public string GroupName { get; set; }\n    public string CollectionName { get; set; }\n    public bool? ReadOnly { get; set; }\n    public bool? HidePasswords { get; set; }\n    public bool? Manage { get; set; }\n    public IEnumerable<Guid> CipherIds { get; set; }\n\n    public MemberAccessDetailReportResponseModel(MemberAccessReportDetail reportDetail)\n    {\n        UserGuid = reportDetail.UserGuid;\n        UserName = reportDetail.UserName;\n        Email = reportDetail.Email;\n        TwoFactorEnabled = reportDetail.TwoFactorEnabled;\n        AccountRecoveryEnabled = reportDetail.AccountRecoveryEnabled;\n        UsesKeyConnector = reportDetail.UsesKeyConnector;\n        CollectionId = reportDetail.CollectionId;\n        GroupId = reportDetail.GroupId;\n        GroupName = reportDetail.GroupName;\n        CollectionName = reportDetail.CollectionName;\n        ReadOnly = reportDetail.ReadOnly;\n        HidePasswords = reportDetail.HidePasswords;\n        Manage = reportDetail.Manage;\n        CipherIds = reportDetail.CipherIds;\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Response/MemberAccessReportModel.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data;\n\nnamespace Bit.Api.Dirt.Models.Response;\n\n/// <summary>\n/// Contains the collections and group collections a user has access to including\n/// the permission level for the collection and group collection.\n/// </summary>\npublic class MemberAccessReportResponseModel\n{\n    public string UserName { get; set; }\n    public string Email { get; set; }\n    public bool TwoFactorEnabled { get; set; }\n    public bool AccountRecoveryEnabled { get; set; }\n    public int GroupsCount { get; set; }\n    public int CollectionsCount { get; set; }\n    public int TotalItemCount { get; set; }\n    public Guid? UserGuid { get; set; }\n    public bool UsesKeyConnector { get; set; }\n    public IEnumerable<MemberAccessDetails> AccessDetails { get; set; }\n\n    public MemberAccessReportResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)\n    {\n        this.UserName = memberAccessCipherDetails.UserName;\n        this.Email = memberAccessCipherDetails.Email;\n        this.TwoFactorEnabled = memberAccessCipherDetails.TwoFactorEnabled;\n        this.AccountRecoveryEnabled = memberAccessCipherDetails.AccountRecoveryEnabled;\n        this.GroupsCount = memberAccessCipherDetails.GroupsCount;\n        this.CollectionsCount = memberAccessCipherDetails.CollectionsCount;\n        this.TotalItemCount = memberAccessCipherDetails.TotalItemCount;\n        this.UserGuid = memberAccessCipherDetails.UserGuid;\n        this.AccessDetails = memberAccessCipherDetails.AccessDetails;\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.Models.Data;\nnamespace Bit.Api.Dirt.Models.Response;\n\npublic class MemberCipherDetailsResponseModel\n{\n    public Guid? UserGuid { get; set; }\n    public string UserName { get; set; }\n    public string Email { get; set; }\n    public bool UsesKeyConnector { get; set; }\n\n    /// <summary>\n    /// A distinct list of the cipher ids associated with\n    /// the organization member\n    /// </summary>\n    public IEnumerable<string> CipherIds { get; set; }\n\n    public MemberCipherDetailsResponseModel(RiskInsightsReportDetail reportDetail)\n    {\n        this.UserGuid = reportDetail.UserGuid;\n        this.UserName = reportDetail.UserName;\n        this.Email = reportDetail.Email;\n        this.UsesKeyConnector = reportDetail.UsesKeyConnector;\n        this.CipherIds = reportDetail.CipherIds;\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Response/OrganizationIntegrationConfigurationResponseModel.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Dirt.Models.Response;\n\npublic class OrganizationIntegrationConfigurationResponseModel : ResponseModel\n{\n    public OrganizationIntegrationConfigurationResponseModel(OrganizationIntegrationConfiguration organizationIntegrationConfiguration, string obj = \"organizationIntegrationConfiguration\")\n        : base(obj)\n    {\n        Id = organizationIntegrationConfiguration.Id;\n        Configuration = organizationIntegrationConfiguration.Configuration;\n        CreationDate = organizationIntegrationConfiguration.CreationDate;\n        EventType = organizationIntegrationConfiguration.EventType;\n        Filters = organizationIntegrationConfiguration.Filters;\n        Template = organizationIntegrationConfiguration.Template;\n    }\n\n    public Guid Id { get; set; }\n    public string? Configuration { get; set; }\n    public string? Filters { get; set; }\n    public DateTime CreationDate { get; set; }\n    public EventType? EventType { get; set; }\n    public string? Template { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Response/OrganizationIntegrationResponseModel.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Dirt.Models.Response;\n\npublic class OrganizationIntegrationResponseModel : ResponseModel\n{\n    public OrganizationIntegrationResponseModel(OrganizationIntegration organizationIntegration, string obj = \"organizationIntegration\")\n        : base(obj)\n    {\n        ArgumentNullException.ThrowIfNull(organizationIntegration);\n\n        Id = organizationIntegration.Id;\n        Type = organizationIntegration.Type;\n        Configuration = organizationIntegration.Configuration;\n    }\n\n    public Guid Id { get; set; }\n    public IntegrationType Type { get; set; }\n    public string? Configuration { get; set; }\n\n    public OrganizationIntegrationStatus Status => Type switch\n    {\n        // Not yet implemented, shouldn't be present, NotApplicable\n        IntegrationType.CloudBillingSync => OrganizationIntegrationStatus.NotApplicable,\n        IntegrationType.Scim => OrganizationIntegrationStatus.NotApplicable,\n\n        // Webhook is allowed to be null. If it's present, it's Completed\n        IntegrationType.Webhook => OrganizationIntegrationStatus.Completed,\n\n        // If present and the configuration is null, OAuth has been initiated, and we are\n        // waiting on the return call\n        IntegrationType.Slack => string.IsNullOrWhiteSpace(Configuration)\n            ? OrganizationIntegrationStatus.Initiated\n            : OrganizationIntegrationStatus.Completed,\n\n        // If present and the configuration is null, OAuth has been initiated, and we are\n        // waiting on the return OAuth call. If Configuration is not null and IsCompleted is true,\n        // then we've received the app install bot callback, and it's Completed. Otherwise,\n        // it is In Progress while we await the app install bot callback.\n        IntegrationType.Teams => string.IsNullOrWhiteSpace(Configuration)\n            ? OrganizationIntegrationStatus.Initiated\n            : (JsonSerializer.Deserialize<TeamsIntegration>(Configuration)?.IsCompleted ?? false)\n                ? OrganizationIntegrationStatus.Completed\n                : OrganizationIntegrationStatus.InProgress,\n\n        // HEC and Datadog should only be allowed to be created non-null.\n        // If they are null, they are Invalid\n        IntegrationType.Hec => string.IsNullOrWhiteSpace(Configuration)\n            ? OrganizationIntegrationStatus.Invalid\n            : OrganizationIntegrationStatus.Completed,\n        IntegrationType.Datadog => string.IsNullOrWhiteSpace(Configuration)\n            ? OrganizationIntegrationStatus.Invalid\n            : OrganizationIntegrationStatus.Completed,\n    };\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Response/OrganizationReportResponseModel.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\n\nnamespace Bit.Api.Dirt.Models.Response;\n\npublic class OrganizationReportResponseModel\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string? ReportData { get; set; }\n    public string? ContentEncryptionKey { get; set; }\n    public string? SummaryData { get; set; }\n    public string? ApplicationData { get; set; }\n    public int? PasswordCount { get; set; }\n    public int? PasswordAtRiskCount { get; set; }\n    public int? MemberCount { get; set; }\n    public DateTime? CreationDate { get; set; } = null;\n    public DateTime? RevisionDate { get; set; } = null;\n\n    public OrganizationReportResponseModel(OrganizationReport organizationReport)\n    {\n        if (organizationReport == null)\n        {\n            return;\n        }\n\n        Id = organizationReport.Id;\n        OrganizationId = organizationReport.OrganizationId;\n        ReportData = organizationReport.ReportData;\n        ContentEncryptionKey = organizationReport.ContentEncryptionKey;\n        SummaryData = organizationReport.SummaryData;\n        ApplicationData = organizationReport.ApplicationData;\n        PasswordCount = organizationReport.PasswordCount;\n        PasswordAtRiskCount = organizationReport.PasswordAtRiskCount;\n        MemberCount = organizationReport.MemberCount;\n        CreationDate = organizationReport.CreationDate;\n        RevisionDate = organizationReport.RevisionDate;\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs",
    "content": "﻿namespace Bit.Api.Dirt.Models.Response;\n\npublic class OrganizationReportSummaryModel\n{\n    public Guid OrganizationId { get; set; }\n    public required string EncryptedData { get; set; }\n    public required string EncryptionKey { get; set; }\n    public DateTime Date { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Public/Controllers/EventsController.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.Dirt.Public.Models;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Api.Utilities.DiagnosticTools;\nusing Bit.Core.Context;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Dirt.Public.Controllers;\n\n[Route(\"public/events\")]\n[Authorize(\"Organization\")]\npublic class EventsController : Controller\n{\n    private readonly IEventRepository _eventRepository;\n    private readonly ICipherRepository _cipherRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IUserService _userService;\n    private readonly ILogger<EventsController> _logger;\n    private readonly IFeatureService _featureService;\n\n    public EventsController(\n        IEventRepository eventRepository,\n        ICipherRepository cipherRepository,\n        ICurrentContext currentContext,\n        ISecretRepository secretRepository,\n        IProjectRepository projectRepository,\n        IUserService userService,\n        ILogger<EventsController> logger,\n        IFeatureService featureService)\n    {\n        _eventRepository = eventRepository;\n        _cipherRepository = cipherRepository;\n        _currentContext = currentContext;\n        _secretRepository = secretRepository;\n        _projectRepository = projectRepository;\n        _userService = userService;\n        _logger = logger;\n        _featureService = featureService;\n    }\n\n    /// <summary>\n    /// List all events.\n    /// </summary>\n    /// <remarks>\n    /// Returns a filtered list of your organization's event logs, paged by a continuation token.\n    /// If no filters are provided, it will return the last 30 days of event for the organization.\n    /// </remarks>\n    [HttpGet]\n    [ProducesResponseType(typeof(PagedListResponseModel<EventResponseModel>), (int)HttpStatusCode.OK)]\n    public async Task<IActionResult> List([FromQuery] EventFilterRequestModel request)\n    {\n        if (!_currentContext.OrganizationId.HasValue)\n        {\n            return new JsonResult(new PagedListResponseModel<EventResponseModel>([], null));\n        }\n\n        var organizationId = _currentContext.OrganizationId.Value;\n        var dateRange = request.ToDateRange();\n        var result = new PagedResult<IEvent>();\n        if (request.ActingUserId.HasValue)\n        {\n            result = await _eventRepository.GetManyByOrganizationActingUserAsync(\n                organizationId, request.ActingUserId.Value, dateRange.Item1, dateRange.Item2,\n                new PageOptions { ContinuationToken = request.ContinuationToken });\n        }\n        else if (request.ItemId.HasValue)\n        {\n            var cipher = await _cipherRepository.GetByIdAsync(request.ItemId.Value);\n            if (cipher != null && cipher.OrganizationId == organizationId)\n            {\n                result = await _eventRepository.GetManyByCipherAsync(\n                    cipher, dateRange.Item1, dateRange.Item2,\n                    new PageOptions { ContinuationToken = request.ContinuationToken });\n            }\n        }\n        else if (request.SecretId.HasValue)\n        {\n            var secret = await _secretRepository.GetByIdAsync(request.SecretId.Value);\n\n            if (secret == null)\n            {\n                secret = new Core.SecretsManager.Entities.Secret { Id = request.SecretId.Value, OrganizationId = organizationId };\n            }\n\n            if (secret.OrganizationId == organizationId)\n            {\n                result = await _eventRepository.GetManyBySecretAsync(\n                    secret, dateRange.Item1, dateRange.Item2,\n                    new PageOptions { ContinuationToken = request.ContinuationToken });\n            }\n            else\n            {\n                return new JsonResult(new PagedListResponseModel<EventResponseModel>([], null));\n            }\n        }\n        else if (request.ProjectId.HasValue)\n        {\n            var project = await _projectRepository.GetByIdAsync(request.ProjectId.Value);\n            if (project != null && project.OrganizationId == organizationId)\n            {\n                result = await _eventRepository.GetManyByProjectAsync(\n                    project, dateRange.Item1, dateRange.Item2,\n                    new PageOptions { ContinuationToken = request.ContinuationToken });\n            }\n            else\n            {\n                return new JsonResult(new PagedListResponseModel<EventResponseModel>([], null));\n            }\n        }\n        else\n        {\n            result = await _eventRepository.GetManyByOrganizationAsync(\n                organizationId, dateRange.Item1, dateRange.Item2,\n                new PageOptions { ContinuationToken = request.ContinuationToken });\n        }\n\n        var eventResponses = result.Data.Select(e => new EventResponseModel(e));\n        var response = new PagedListResponseModel<EventResponseModel>(eventResponses, result.ContinuationToken ?? null);\n\n        _logger.LogAggregateData(_featureService, organizationId, response, request);\n\n        return new JsonResult(response);\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Public/Models/EventFilterRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Api.Dirt.Public.Models;\n\npublic class EventFilterRequestModel\n{\n    /// <summary>\n    /// The start date. Must be less than the end date.\n    /// </summary>\n    public DateTime? Start { get; set; }\n    /// <summary>\n    /// The end date. Must be greater than the start date.\n    /// </summary>\n    public DateTime? End { get; set; }\n    /// <summary>\n    /// The unique identifier of the user that performed the event.\n    /// </summary>\n    public Guid? ActingUserId { get; set; }\n    /// <summary>\n    /// The unique identifier of the related item that the event describes.\n    /// </summary>\n    public Guid? ItemId { get; set; }\n    /// <summary>\n    /// The unique identifier of the related secret that the event describes.\n    /// </summary>\n    public Guid? SecretId { get; set; }\n    /// <summary>\n    /// The unique identifier of the related project that the event describes.\n    /// </summary>\n    public Guid? ProjectId { get; set; }\n    /// <summary>\n    /// A cursor for use in pagination.\n    /// </summary>\n    public string ContinuationToken { get; set; }\n\n    public Tuple<DateTime, DateTime> ToDateRange()\n    {\n        if (!End.HasValue || !Start.HasValue)\n        {\n            End = DateTime.UtcNow.Date.AddDays(1).AddMilliseconds(-1);\n            Start = DateTime.UtcNow.Date.AddDays(-30);\n        }\n        else if (Start.Value > End.Value)\n        {\n            var newEnd = Start;\n            Start = End;\n            End = newEnd;\n        }\n\n        if ((End.Value - Start.Value) > TimeSpan.FromDays(367))\n        {\n            throw new BadRequestException(\"Date range must be < 367 days.\");\n        }\n\n        return new Tuple<DateTime, DateTime>(Start.Value, End.Value);\n    }\n}\n"
  },
  {
    "path": "src/Api/Dirt/Public/Models/EventResponseModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.Dirt.Public.Models;\n\n/// <summary>\n/// An event log.\n/// </summary>\npublic class EventResponseModel : IResponseModel\n{\n    public EventResponseModel(IEvent ev)\n    {\n        if (ev == null)\n        {\n            throw new ArgumentNullException(nameof(ev));\n        }\n\n        Type = ev.Type;\n        ItemId = ev.CipherId;\n        CollectionId = ev.CollectionId;\n        GroupId = ev.GroupId;\n        PolicyId = ev.PolicyId;\n        MemberId = ev.OrganizationUserId;\n        ActingUserId = ev.ActingUserId;\n        Date = ev.Date;\n        Device = ev.DeviceType;\n        IpAddress = ev.IpAddress;\n        InstallationId = ev.InstallationId;\n        SecretId = ev.SecretId;\n        ProjectId = ev.ProjectId;\n        ServiceAccountId = ev.ServiceAccountId;\n    }\n\n    /// <summary>\n    /// String representing the object's type. Objects of the same type share the same properties.\n    /// </summary>\n    /// <example>event</example>\n    [Required]\n    public string Object => \"event\";\n    /// <summary>\n    /// The type of event.\n    /// </summary>\n    [Required]\n    public EventType Type { get; set; }\n    /// <summary>\n    /// The unique identifier of the related item that the event describes.\n    /// </summary>\n    /// <example>3767a302-8208-4dc6-b842-030428a1cfad</example>\n    public Guid? ItemId { get; set; }\n    /// <summary>\n    /// The unique identifier of the related collection that the event describes.\n    /// </summary>\n    /// <example>bce212a4-25f3-4888-8a0a-4c5736d851e0</example>\n    public Guid? CollectionId { get; set; }\n    /// <summary>\n    /// The unique identifier of the related group that the event describes.\n    /// </summary>\n    /// <example>f29a2515-91d2-4452-b49b-5e8040e6b0f4</example>\n    public Guid? GroupId { get; set; }\n    /// <summary>\n    /// The unique identifier of the related policy that the event describes.\n    /// </summary>\n    /// <example>f29a2515-91d2-4452-b49b-5e8040e6b0f4</example>\n    public Guid? PolicyId { get; set; }\n    /// <summary>\n    /// The unique identifier of the related member that the event describes.\n    /// </summary>\n    /// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>\n    public Guid? MemberId { get; set; }\n    /// <summary>\n    /// The unique identifier of the user that performed the event.\n    /// </summary>\n    /// <example>a2549f79-a71f-4eb9-9234-eb7247333f94</example>\n    public Guid? ActingUserId { get; set; }\n    /// <summary>\n    /// The Unique identifier of the Installation that performed the event.\n    /// </summary>\n    /// <value></value>\n    public Guid? InstallationId { get; set; }\n    /// <summary>\n    /// The date/timestamp when the event occurred.\n    /// </summary>\n    [Required]\n    public DateTime Date { get; set; }\n    /// <summary>\n    /// The type of device used by the acting user when the event occurred.\n    /// </summary>\n    public DeviceType? Device { get; set; }\n    /// <summary>\n    /// The IP address of the acting user.\n    /// </summary>\n    /// <example>172.16.254.1</example>\n    public string IpAddress { get; set; }\n    /// <summary>\n    /// The unique identifier of the related secret that the event describes.\n    /// </summary>\n    /// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>\n    public Guid? SecretId { get; set; }\n    /// <summary>\n    /// The unique identifier of the related project that the event describes.\n    /// </summary>\n    /// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>\n    public Guid? ProjectId { get; set; }\n    /// <summary>\n    /// The unique identifier of the related service account that the event describes.\n    /// </summary>\n    /// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>\n    public Guid? ServiceAccountId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Dockerfile",
    "content": "###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\n# We put the value in a file to be read by later layers.\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/src/Api\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    krb5 \\\n    icu-libs \\\n    tzdata \\\n    shadow \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu\n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/src/Api/out /app\nCOPY ./src/Api/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\nHEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "src/Api/Jobs/AliveJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Jobs;\nusing Quartz;\n\nnamespace Bit.Api.Jobs;\n\npublic class AliveJob : BaseJob\n{\n    public AliveJob(ILogger<AliveJob> logger)\n        : base(logger) { }\n\n    protected override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, null, \"It's alive!\");\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Api/Jobs/JobsHostedService.cs",
    "content": "﻿using Bit.Api.AdminConsole.Jobs;\nusing Bit.Api.Auth.Jobs;\nusing Bit.Core.Jobs;\nusing Bit.Core.Settings;\nusing Quartz;\n\nnamespace Bit.Api.Jobs;\n\npublic class JobsHostedService : BaseJobsHostedService\n{\n    public JobsHostedService(\n        GlobalSettings globalSettings,\n        IServiceProvider serviceProvider,\n        ILogger<JobsHostedService> logger,\n        ILogger<JobListener> listenerLogger)\n        : base(globalSettings, serviceProvider, logger, listenerLogger) { }\n\n    public override async Task StartAsync(CancellationToken cancellationToken)\n    {\n        var everyTopOfTheHourTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EveryTopOfTheHourTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 * * * ?\")\n            .Build();\n        var emergencyAccessNotificationTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EmergencyAccessNotificationTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 * * * ?\")\n            .Build();\n        var emergencyAccessTimeoutTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EmergencyAccessTimeoutTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 * * * ?\")\n            .Build();\n        var everyTopOfTheSixthHourTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EveryTopOfTheSixthHourTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 */6 * * ?\")\n            .Build();\n        var everyTwelfthHourAndThirtyMinutesTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EveryTwelfthHourAndThirtyMinutesTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 30 */12 * * ?\")\n            .Build();\n        var smTrashCleanupTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"SMTrashCleanupTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 22 * * ?\")\n            .Build();\n        var randomDailySponsorshipSyncTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"RandomDailySponsorshipSyncTrigger\")\n            .StartAt(DateBuilder.FutureDate(new Random().Next(24), IntervalUnit.Hour))\n            .WithSimpleSchedule(x => x\n                .WithIntervalInHours(24)\n                .RepeatForever())\n            .Build();\n        var validateOrganizationDomainTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"ValidateOrganizationDomainTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 * * * ?\")\n            .Build();\n        var updateOrgSubscriptionsTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"UpdateOrgSubscriptionsTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 */3 * * ?\") // top of every 3rd hour\n            .Build();\n\n\n        var jobs = new List<Tuple<Type, ITrigger>>\n        {\n            new Tuple<Type, ITrigger>(typeof(AliveJob), everyTopOfTheHourTrigger),\n            new Tuple<Type, ITrigger>(typeof(EmergencyAccessNotificationJob), emergencyAccessNotificationTrigger),\n            new Tuple<Type, ITrigger>(typeof(EmergencyAccessTimeoutJob), emergencyAccessTimeoutTrigger),\n            new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger),\n            new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),\n            new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),\n            new (typeof(OrganizationSubscriptionUpdateJob), updateOrgSubscriptionsTrigger),\n        };\n\n        if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)\n        {\n            jobs.Add(new Tuple<Type, ITrigger>(typeof(SelfHostedSponsorshipSyncJob), randomDailySponsorshipSyncTrigger));\n        }\n\n#if !OSS\n        jobs.Add(new Tuple<Type, ITrigger>(typeof(EmptySecretsManagerTrashJob), smTrashCleanupTrigger));\n#endif\n\n        Jobs = jobs;\n\n        await base.StartAsync(cancellationToken);\n    }\n\n    public static void AddJobsServices(IServiceCollection services, bool selfHosted)\n    {\n        if (selfHosted)\n        {\n            services.AddTransient<SelfHostedSponsorshipSyncJob>();\n        }\n        services.AddTransient<AliveJob>();\n        services.AddTransient<EmergencyAccessNotificationJob>();\n        services.AddTransient<EmergencyAccessTimeoutJob>();\n        services.AddTransient<ValidateUsersJob>();\n        services.AddTransient<ValidateOrganizationsJob>();\n        services.AddTransient<ValidateOrganizationDomainJob>();\n        services.AddTransient<OrganizationSubscriptionUpdateJob>();\n    }\n\n    public static void AddCommercialSecretsManagerJobServices(IServiceCollection services)\n    {\n        services.AddTransient<EmptySecretsManagerTrashJob>();\n    }\n}\n"
  },
  {
    "path": "src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Jobs;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Quartz;\n\nnamespace Bit.Api.Jobs;\n\npublic class SelfHostedSponsorshipSyncJob : BaseJob\n{\n    private readonly IServiceProvider _serviceProvider;\n    private IOrganizationRepository _organizationRepository;\n    private IOrganizationConnectionRepository _organizationConnectionRepository;\n    private readonly ILicensingService _licensingService;\n    private GlobalSettings _globalSettings;\n\n    public SelfHostedSponsorshipSyncJob(\n        IServiceProvider serviceProvider,\n        IOrganizationRepository organizationRepository,\n        IOrganizationConnectionRepository organizationConnectionRepository,\n        ILicensingService licensingService,\n        ILogger<SelfHostedSponsorshipSyncJob> logger,\n        GlobalSettings globalSettings)\n        : base(logger)\n    {\n        _serviceProvider = serviceProvider;\n        _organizationRepository = organizationRepository;\n        _organizationConnectionRepository = organizationConnectionRepository;\n        _licensingService = licensingService;\n        _globalSettings = globalSettings;\n    }\n\n    protected override async Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        if (!_globalSettings.EnableCloudCommunication)\n        {\n            _logger.LogInformation(\"Skipping Organization sync with cloud - Cloud communication is disabled in global settings\");\n            return;\n        }\n\n        var organizations = await _organizationRepository.GetManyByEnabledAsync();\n\n        using (var scope = _serviceProvider.CreateScope())\n        {\n            var syncCommand = scope.ServiceProvider.GetRequiredService<ISelfHostedSyncSponsorshipsCommand>();\n            foreach (var org in organizations)\n            {\n                var connection = (await _organizationConnectionRepository.GetEnabledByOrganizationIdTypeAsync(org.Id, OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();\n                if (connection != null)\n                {\n                    try\n                    {\n                        var config = connection.GetConfig<BillingSyncConfig>();\n                        await syncCommand.SyncOrganization(org.Id, config.CloudOrganizationId, connection);\n                    }\n                    catch (Exception ex)\n                    {\n                        _logger.LogError(ex, \"Sponsorship sync for organization {OrganizationName} Failed\", org.DisplayName());\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Jobs/ValidateOrganizationDomainJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Jobs;\nusing Quartz;\n\nnamespace Bit.Api.Jobs;\n\npublic class ValidateOrganizationDomainJob : BaseJob\n{\n    private readonly IServiceProvider _serviceProvider;\n    public ValidateOrganizationDomainJob(\n        IServiceProvider serviceProvider,\n        ILogger<ValidateOrganizationDomainJob> logger)\n        : base(logger)\n    {\n        _serviceProvider = serviceProvider;\n    }\n\n    protected override async Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: ValidateOrganizationDomainJob: Start\");\n        using (var serviceScope = _serviceProvider.CreateScope())\n        {\n            var organizationDomainService =\n                serviceScope.ServiceProvider.GetRequiredService<IOrganizationDomainService>();\n            await organizationDomainService.ValidateOrganizationsDomainAsync();\n        }\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Execute job task: ValidateOrganizationDomainJob: End\");\n    }\n}\n"
  },
  {
    "path": "src/Api/Jobs/ValidateOrganizationsJob.cs",
    "content": "﻿using Bit.Core.Billing.Services;\nusing Bit.Core.Jobs;\nusing Quartz;\n\nnamespace Bit.Api.Jobs;\n\npublic class ValidateOrganizationsJob : BaseJob\n{\n    private readonly ILicensingService _licensingService;\n\n    public ValidateOrganizationsJob(\n        ILicensingService licensingService,\n        ILogger<ValidateOrganizationsJob> logger)\n        : base(logger)\n    {\n        _licensingService = licensingService;\n    }\n\n    protected async override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        await _licensingService.ValidateOrganizationsAsync();\n    }\n}\n"
  },
  {
    "path": "src/Api/Jobs/ValidateUsersJob.cs",
    "content": "﻿using Bit.Core.Billing.Services;\nusing Bit.Core.Jobs;\nusing Quartz;\n\nnamespace Bit.Api.Jobs;\n\npublic class ValidateUsersJob : BaseJob\n{\n    private readonly ILicensingService _licensingService;\n\n    public ValidateUsersJob(\n        ILicensingService licensingService,\n        ILogger<ValidateUsersJob> logger)\n        : base(logger)\n    {\n        _licensingService = licensingService;\n    }\n\n    protected async override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        await _licensingService.ValidateUsersAsync();\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.Auth.Models.Request;\nusing Bit.Api.Auth.Models.Request.WebAuthn;\nusing Bit.Api.KeyManagement.Models.Requests;\nusing Bit.Api.KeyManagement.Models.Responses;\nusing Bit.Api.KeyManagement.Validators;\nusing Bit.Api.Tools.Models.Request;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Api.Request;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Commands.Interfaces;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.KeyManagement.Controllers;\n\n[Route(\"accounts\")]\n[Authorize(\"Application\")]\npublic class AccountsKeyManagementController : Controller\n{\n    private readonly IEmergencyAccessRepository _emergencyAccessRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand;\n    private readonly IUserService _userService;\n    private readonly IRotateUserAccountKeysCommand _rotateUserAccountKeysCommand;\n    private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;\n    private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;\n    private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;\n    private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>\n        _emergencyAccessValidator;\n    private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,\n            IReadOnlyList<OrganizationUser>>\n        _organizationUserValidator;\n    private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>\n        _webauthnKeyValidator;\n    private readonly IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> _deviceValidator;\n    private readonly IKeyConnectorConfirmationDetailsQuery _keyConnectorConfirmationDetailsQuery;\n    private readonly ISetKeyConnectorKeyCommand _setKeyConnectorKeyCommand;\n\n    public AccountsKeyManagementController(IUserService userService,\n        IOrganizationUserRepository organizationUserRepository,\n        IEmergencyAccessRepository emergencyAccessRepository,\n        IKeyConnectorConfirmationDetailsQuery keyConnectorConfirmationDetailsQuery,\n        IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand,\n        IRotateUserAccountKeysCommand rotateUserKeyCommandV2,\n        IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,\n        IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,\n        IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator,\n        IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>\n            emergencyAccessValidator,\n        IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>\n            organizationUserValidator,\n        IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>\n            webAuthnKeyValidator,\n        IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> deviceValidator,\n        ISetKeyConnectorKeyCommand setKeyConnectorKeyCommand)\n    {\n        _userService = userService;\n        _regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand;\n        _organizationUserRepository = organizationUserRepository;\n        _emergencyAccessRepository = emergencyAccessRepository;\n        _rotateUserAccountKeysCommand = rotateUserKeyCommandV2;\n        _cipherValidator = cipherValidator;\n        _folderValidator = folderValidator;\n        _sendValidator = sendValidator;\n        _emergencyAccessValidator = emergencyAccessValidator;\n        _organizationUserValidator = organizationUserValidator;\n        _webauthnKeyValidator = webAuthnKeyValidator;\n        _deviceValidator = deviceValidator;\n        _keyConnectorConfirmationDetailsQuery = keyConnectorConfirmationDetailsQuery;\n        _setKeyConnectorKeyCommand = setKeyConnectorKeyCommand;\n    }\n\n    [HttpPost(\"key-management/regenerate-keys\")]\n    public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();\n        var usersOrganizationAccounts = await _organizationUserRepository.GetManyByUserAsync(user.Id);\n        var designatedEmergencyAccess = await _emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(user.Id);\n        await _regenerateUserAsymmetricKeysCommand.RegenerateKeysAsync(request.ToUserAsymmetricKeys(user.Id),\n            usersOrganizationAccounts, designatedEmergencyAccess);\n    }\n\n\n    [HttpPost(\"key-management/rotate-user-account-keys\")]\n    public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var dataModel = new RotateUserAccountKeysData\n        {\n            OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,\n\n            AccountKeys = model.AccountKeys.ToAccountKeysData(),\n\n            MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),\n            EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),\n            OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.AccountUnlockData.OrganizationAccountRecoveryUnlockData),\n            WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.AccountUnlockData.PasskeyUnlockData),\n            DeviceKeys = await _deviceValidator.ValidateAsync(user, model.AccountUnlockData.DeviceKeyUnlockData),\n            V2UpgradeToken = model.AccountUnlockData.V2UpgradeToken?.ToData(),\n\n            Ciphers = await _cipherValidator.ValidateAsync(user, model.AccountData.Ciphers),\n            Folders = await _folderValidator.ValidateAsync(user, model.AccountData.Folders),\n            Sends = await _sendValidator.ValidateAsync(user, model.AccountData.Sends),\n        };\n\n        var result = await _rotateUserAccountKeysCommand.RotateUserAccountKeysAsync(user, dataModel);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"set-key-connector-key\")]\n    public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (model.IsV2Request())\n        {\n            // V2 account registration\n            await _setKeyConnectorKeyCommand.SetKeyConnectorKeyForUserAsync(user, model.ToKeyConnectorKeysData());\n        }\n        else\n        {\n            // V1 account registration\n            // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328\n            var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key!, model.OrgIdentifier);\n            if (result.Succeeded)\n            {\n                return;\n            }\n\n            foreach (var error in result.Errors)\n            {\n                ModelState.AddModelError(string.Empty, error.Description);\n            }\n\n            throw new BadRequestException(ModelState);\n        }\n    }\n\n    [HttpPost(\"convert-to-key-connector\")]\n    public async Task PostConvertToKeyConnectorAsync()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await _userService.ConvertToKeyConnectorAsync(user, null);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"key-connector/enroll\")]\n    public async Task PostEnrollToKeyConnectorAsync([FromBody] KeyConnectorEnrollmentRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var result = await _userService.ConvertToKeyConnectorAsync(user, model.KeyConnectorKeyWrappedUserKey);\n        if (result.Succeeded)\n        {\n            return;\n        }\n\n        foreach (var error in result.Errors)\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpGet(\"key-connector/confirmation-details/{orgSsoIdentifier}\")]\n    public async Task<KeyConnectorConfirmationDetailsResponseModel> GetKeyConnectorConfirmationDetailsAsync(string orgSsoIdentifier)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var details = await _keyConnectorConfirmationDetailsQuery.Run(orgSsoIdentifier, user.Id);\n        return new KeyConnectorConfirmationDetailsResponseModel(details);\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Controllers/UsersController.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Api.Response;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\nusing UserKeyResponseModel = Bit.Api.Models.Response.UserKeyResponseModel;\n\n\nnamespace Bit.Api.KeyManagement.Controllers;\n\n[Route(\"users\")]\n[Authorize(\"Application\")]\npublic class UsersController : Controller\n{\n    private readonly IUserRepository _userRepository;\n    private readonly IUserAccountKeysQuery _userAccountKeysQuery;\n\n    public UsersController(IUserRepository userRepository, IUserAccountKeysQuery userAccountKeysQuery)\n    {\n        _userRepository = userRepository;\n        _userAccountKeysQuery = userAccountKeysQuery;\n    }\n\n    [HttpGet(\"{id}/public-key\")]\n    public async Task<UserKeyResponseModel> GetPublicKeyAsync([FromRoute] Guid id)\n    {\n        var key = await _userRepository.GetPublicKeyAsync(id) ?? throw new NotFoundException();\n        return new UserKeyResponseModel(id, key);\n    }\n\n    [HttpGet(\"{id}/keys\")]\n    public async Task<PublicKeysResponseModel> GetAccountKeysAsync([FromRoute] Guid id)\n    {\n        var user = await _userRepository.GetByIdAsync(id) ?? throw new NotFoundException();\n        var accountKeys = await _userAccountKeysQuery.Run(user) ?? throw new NotFoundException(\"User account keys not found.\");\n        return new PublicKeysResponseModel(accountKeys);\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Models/Requests/KeyConnectorEnrollmentRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.KeyManagement.Models.Requests;\n\npublic class KeyConnectorEnrollmentRequestModel : IValidatableObject\n{\n    [EncryptedString]\n    public required string KeyConnectorKeyWrappedUserKey { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (string.IsNullOrWhiteSpace(KeyConnectorKeyWrappedUserKey))\n        {\n            yield return new ValidationResult(\n                \"KeyConnectorKeyWrappedUserKey must be supplied when request body is provided.\",\n                [nameof(KeyConnectorKeyWrappedUserKey)]);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.KeyManagement.Models.Requests;\n\npublic class KeyRegenerationRequestModel\n{\n    public required string UserPublicKey { get; set; }\n\n    [EncryptedString]\n    public required string UserKeyEncryptedUserPrivateKey { get; set; }\n\n    public UserAsymmetricKeys ToUserAsymmetricKeys(Guid userId)\n    {\n        return new UserAsymmetricKeys\n        {\n            UserId = userId,\n            PublicKey = UserPublicKey,\n            UserKeyEncryptedPrivateKey = UserKeyEncryptedUserPrivateKey,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.KeyManagement.Models.Api.Request;\n\nnamespace Bit.Api.KeyManagement.Models.Requests;\n\npublic class RotateUserAccountKeysAndDataRequestModel\n{\n    [StringLength(300)]\n    public required string OldMasterKeyAuthenticationHash { get; set; }\n    public required UnlockDataRequestModel AccountUnlockData { get; set; }\n    public required AccountKeysRequestModel AccountKeys { get; set; }\n    public required AccountDataRequestModel AccountData { get; set; }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Models/Requests/SetKeyConnectorKeyRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.KeyManagement.Models.Requests;\n\npublic class SetKeyConnectorKeyRequestModel : IValidatableObject\n{\n    // TODO will be removed with https://bitwarden.atlassian.net/browse/PM-27328\n    [Obsolete(\"Use KeyConnectorKeyWrappedUserKey instead\")]\n    public string? Key { get; set; }\n\n    [Obsolete(\"Use AccountKeys instead\")]\n    public KeysRequestModel? Keys { get; set; }\n    [Obsolete(\"Not used anymore\")]\n    public KdfType? Kdf { get; set; }\n    [Obsolete(\"Not used anymore\")]\n    public int? KdfIterations { get; set; }\n    [Obsolete(\"Not used anymore\")]\n    public int? KdfMemory { get; set; }\n    [Obsolete(\"Not used anymore\")]\n    public int? KdfParallelism { get; set; }\n\n    [EncryptedString]\n    public string? KeyConnectorKeyWrappedUserKey { get; set; }\n    public AccountKeysRequestModel? AccountKeys { get; set; }\n\n    [Required]\n    public required string OrgIdentifier { get; init; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (IsV2Request())\n        {\n            // V2 registration\n            yield break;\n        }\n\n        // V1 registration\n        // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328\n        if (string.IsNullOrEmpty(Key))\n        {\n            yield return new ValidationResult(\"Key must be supplied.\");\n        }\n\n        if (Keys == null)\n        {\n            yield return new ValidationResult(\"Keys must be supplied.\");\n        }\n\n        if (Kdf == null)\n        {\n            yield return new ValidationResult(\"Kdf must be supplied.\");\n        }\n\n        if (KdfIterations == null)\n        {\n            yield return new ValidationResult(\"KdfIterations must be supplied.\");\n        }\n\n        if (Kdf == KdfType.Argon2id)\n        {\n            if (KdfMemory == null)\n            {\n                yield return new ValidationResult(\"KdfMemory must be supplied when Kdf is Argon2id.\");\n            }\n\n            if (KdfParallelism == null)\n            {\n                yield return new ValidationResult(\"KdfParallelism must be supplied when Kdf is Argon2id.\");\n            }\n        }\n    }\n\n    public bool IsV2Request()\n    {\n        return !string.IsNullOrEmpty(KeyConnectorKeyWrappedUserKey) && AccountKeys != null;\n    }\n\n    // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328\n    public User ToUser(User existingUser)\n    {\n        existingUser.Kdf = Kdf!.Value;\n        existingUser.KdfIterations = KdfIterations!.Value;\n        existingUser.KdfMemory = KdfMemory;\n        existingUser.KdfParallelism = KdfParallelism;\n        existingUser.Key = Key;\n        Keys!.ToUser(existingUser);\n        return existingUser;\n    }\n\n    public KeyConnectorKeysData ToKeyConnectorKeysData()\n    {\n        // TODO remove validation with https://bitwarden.atlassian.net/browse/PM-27328\n        if (string.IsNullOrEmpty(KeyConnectorKeyWrappedUserKey) || AccountKeys == null)\n        {\n            throw new BadRequestException(\"KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.\");\n        }\n\n        return new KeyConnectorKeysData\n        {\n            KeyConnectorKeyWrappedUserKey = KeyConnectorKeyWrappedUserKey,\n            AccountKeys = AccountKeys,\n            OrgIdentifier = OrgIdentifier\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.Auth.Models.Request;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Auth.Models.Request.WebAuthn;\nusing Bit.Core.Auth.Models.Api.Request;\n\nnamespace Bit.Api.KeyManagement.Models.Requests;\n\npublic class UnlockDataRequestModel\n{\n    // All methods to get to the userkey\n    public required MasterPasswordUnlockAndAuthenticationDataModel MasterPasswordUnlockData { get; set; }\n    public required IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessUnlockData { get; set; }\n    public required IEnumerable<ResetPasswordWithOrgIdRequestModel> OrganizationAccountRecoveryUnlockData { get; set; }\n    public required IEnumerable<WebAuthnLoginRotateKeyRequestModel> PasskeyUnlockData { get; set; }\n    public required IEnumerable<OtherDeviceKeysUpdateRequestModel> DeviceKeyUnlockData { get; set; }\n    public V2UpgradeTokenRequestModel? V2UpgradeToken { get; set; }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs",
    "content": "﻿using Bit.Api.Tools.Models.Request;\nusing Bit.Api.Vault.Models.Request;\n\nnamespace Bit.Api.KeyManagement.Models.Requests;\n\npublic class AccountDataRequestModel\n{\n    public required IEnumerable<CipherWithIdRequestModel> Ciphers { get; set; }\n    public required IEnumerable<FolderWithIdRequestModel> Folders { get; set; }\n    public required IEnumerable<SendWithIdRequestModel> Sends { get; set; }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Models/Requests/V2UpgradeTokenRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.KeyManagement.Models.Requests;\n\n/// <summary>\n/// Request model for V2 upgrade token submitted during key rotation.\n/// Contains wrapped user keys allowing clients to unlock after V1→V2 upgrade.\n/// </summary>\npublic class V2UpgradeTokenRequestModel\n{\n    /// <summary>\n    /// User Key V2 Wrapped User Key V1.\n    /// </summary>\n    [Required]\n    [EncryptedString]\n    public required string WrappedUserKey1 { get; init; }\n\n    /// <summary>\n    /// User Key V1 Wrapped User Key V2.\n    /// </summary>\n    [Required]\n    [EncryptedString]\n    public required string WrappedUserKey2 { get; init; }\n\n    public V2UpgradeTokenData ToData()\n    {\n        return new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = WrappedUserKey1,\n            WrappedUserKey2 = WrappedUserKey2\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Models/Responses/KeyConnectorConfirmationDetailsResponseModel.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.KeyManagement.Models.Responses;\n\npublic class KeyConnectorConfirmationDetailsResponseModel : ResponseModel\n{\n    private const string _objectName = \"keyConnectorConfirmationDetails\";\n\n    public KeyConnectorConfirmationDetailsResponseModel(KeyConnectorConfirmationDetails details,\n        string obj = _objectName) : base(obj)\n    {\n        ArgumentNullException.ThrowIfNull(details);\n\n        OrganizationName = details.OrganizationName;\n    }\n\n    public KeyConnectorConfirmationDetailsResponseModel() : base(_objectName)\n    {\n        OrganizationName = string.Empty;\n    }\n\n    public string OrganizationName { get; set; }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Validators/CipherRotationValidator.cs",
    "content": "﻿using Bit.Api.Vault.Models.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Repositories;\n\nnamespace Bit.Api.KeyManagement.Validators;\n\npublic class CipherRotationValidator : IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>\n{\n    private readonly ICipherRepository _cipherRepository;\n\n    public CipherRotationValidator(ICipherRepository cipherRepository)\n    {\n        _cipherRepository = cipherRepository;\n    }\n\n    public async Task<IEnumerable<Cipher>> ValidateAsync(User user, IEnumerable<CipherWithIdRequestModel> ciphers)\n    {\n        var result = new List<Cipher>();\n\n        var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id);\n        if (existingCiphers == null)\n        {\n            return result;\n        }\n\n        var existingUserCiphers = existingCiphers.Where(c => c.OrganizationId == null);\n        if (existingUserCiphers.Count() == 0)\n        {\n            return result;\n        }\n\n        foreach (var existing in existingUserCiphers)\n        {\n            var cipher = ciphers.FirstOrDefault(c => c.Id == existing.Id);\n            if (cipher == null)\n            {\n                throw new BadRequestException(\"All existing ciphers must be included in the rotation.\");\n            }\n            result.Add(cipher.ToCipher(existing));\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Validators/DeviceRotationValidator.cs",
    "content": "﻿using Bit.Core.Auth.Models.Api.Request;\nusing Bit.Core.Auth.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Api.KeyManagement.Validators;\n\n/// <summary>\n/// Device implementation for <see cref=\"IRotationValidator{T,R}\"/>\n/// </summary>\npublic class DeviceRotationValidator : IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>>\n{\n    private readonly IDeviceRepository _deviceRepository;\n\n    /// <summary>\n    /// Instantiates a new <see cref=\"DeviceRotationValidator\"/>\n    /// </summary>\n    /// <param name=\"deviceRepository\">Retrieves all user <see cref=\"Device\"/>s</param>\n    public DeviceRotationValidator(IDeviceRepository deviceRepository)\n    {\n        _deviceRepository = deviceRepository;\n    }\n\n    public async Task<IEnumerable<Device>> ValidateAsync(User user, IEnumerable<OtherDeviceKeysUpdateRequestModel> devices)\n    {\n        var result = new List<Device>();\n\n        var existingTrustedDevices = (await _deviceRepository.GetManyByUserIdAsync(user.Id)).Where(d => d.IsTrusted()).ToList();\n        if (existingTrustedDevices.Count == 0)\n        {\n            return result;\n        }\n\n        foreach (var existing in existingTrustedDevices)\n        {\n            var device = devices.FirstOrDefault(c => c.DeviceId == existing.Id);\n            if (device == null)\n            {\n                throw new BadRequestException(\"All existing trusted devices must be included in the rotation.\");\n            }\n\n            if (device.EncryptedUserKey == null || device.EncryptedPublicKey == null)\n            {\n                throw new BadRequestException(\"Rotated encryption keys must be provided for all devices that are trusted.\");\n            }\n\n            result.Add(device.ToDevice(existing));\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Validators/EmergencyAccessRotationValidator.cs",
    "content": "﻿using Bit.Api.Auth.Models.Request;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Api.KeyManagement.Validators;\n\npublic class EmergencyAccessRotationValidator : IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>,\n    IEnumerable<EmergencyAccess>>\n{\n    private readonly IEmergencyAccessRepository _emergencyAccessRepository;\n    private readonly IUserService _userService;\n\n    public EmergencyAccessRotationValidator(IEmergencyAccessRepository emergencyAccessRepository,\n        IUserService userService)\n    {\n        _emergencyAccessRepository = emergencyAccessRepository;\n        _userService = userService;\n    }\n\n    public async Task<IEnumerable<EmergencyAccess>> ValidateAsync(User user,\n        IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)\n    {\n        var result = new List<EmergencyAccess>();\n\n        var existing = await _emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(user.Id);\n        if (existing == null || existing.Count == 0)\n        {\n            return result;\n        }\n        // Exclude any emergency access that has not been confirmed yet.\n        existing = existing.Where(ea => ea.KeyEncrypted != null).ToList();\n\n        foreach (var ea in existing)\n        {\n            var emergencyAccess = emergencyAccessKeys.FirstOrDefault(c => c.Id == ea.Id);\n            if (emergencyAccess == null)\n            {\n                throw new BadRequestException(\"All existing emergency access keys must be included in the rotation.\");\n            }\n\n            if (emergencyAccess.KeyEncrypted == null)\n            {\n                throw new BadRequestException(\"Emergency access keys cannot be set to null during rotation.\");\n            }\n\n            result.Add(emergencyAccess.ToEmergencyAccess(ea));\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Validators/FolderRotationValidator.cs",
    "content": "﻿using Bit.Api.Vault.Models.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Repositories;\n\nnamespace Bit.Api.KeyManagement.Validators;\n\npublic class FolderRotationValidator : IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>\n{\n    private readonly IFolderRepository _folderRepository;\n\n    public FolderRotationValidator(IFolderRepository folderRepository)\n    {\n        _folderRepository = folderRepository;\n    }\n\n    public async Task<IEnumerable<Folder>> ValidateAsync(User user, IEnumerable<FolderWithIdRequestModel> folders)\n    {\n        var result = new List<Folder>();\n\n        var existingFolders = await _folderRepository.GetManyByUserIdAsync(user.Id);\n        if (existingFolders == null || existingFolders.Count == 0)\n        {\n            return result;\n        }\n\n        foreach (var existing in existingFolders)\n        {\n            var folder = folders.FirstOrDefault(c => c.Id == existing.Id);\n            if (folder == null)\n            {\n                throw new BadRequestException(\"All existing folders must be included in the rotation.\");\n            }\n            result.Add(folder.ToFolder(existing));\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Validators/IRotationValidator.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Api.KeyManagement.Validators;\n\n/// <summary>\n/// A consistent interface for domains to validate re-encrypted data before saved to database. Some examples are:<br/>\n/// - All available encrypted data is accounted for<br/>\n/// - All provided encrypted data belongs to the user\n/// </summary>\n/// <typeparam name=\"T\">Request model</typeparam>\n/// <typeparam name=\"R\">Domain model</typeparam>\npublic interface IRotationValidator<T, R>\n{\n    /// <summary>\n    /// Validates re-encrypted data before being saved to database.\n    /// </summary>\n    /// <param name=\"user\">Request model</param>\n    /// <param name=\"data\">Domain model</param>\n    /// <exception cref=\"BadRequestException\">Throws if data fails validation</exception>\n    Task<R> ValidateAsync(User user, T data);\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Validators/OrganizationUserRotationValidator.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Api.KeyManagement.Validators;\n\n/// <summary>\n/// Organization user implementation for <see cref=\"IRotationValidator{T,R}\"/>\n/// Currently responsible for validation of user reset password keys (used by admins to perform account recovery) during user key rotation\n/// </summary>\npublic class OrganizationUserRotationValidator : IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,\n    IReadOnlyList<OrganizationUser>>\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public OrganizationUserRotationValidator(IOrganizationUserRepository organizationUserRepository) =>\n        _organizationUserRepository = organizationUserRepository;\n\n    public async Task<IReadOnlyList<OrganizationUser>> ValidateAsync(User user,\n        IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        var result = new List<OrganizationUser>();\n\n        var existing = await _organizationUserRepository.GetManyByUserAsync(user.Id);\n        if (existing == null || existing.Count == 0)\n        {\n            return result;\n        }\n\n        // Exclude any account recovery that do not have a key.\n        existing = existing.Where(o => OrganizationUser.IsValidResetPasswordKey(o.ResetPasswordKey)).ToList();\n\n        foreach (var ou in existing)\n        {\n            var organizationUser = resetPasswordKeys.FirstOrDefault(a => a.OrganizationId == ou.OrganizationId);\n            if (organizationUser == null)\n            {\n                throw new BadRequestException(\"All existing reset password keys must be included in the rotation.\");\n            }\n\n            // Should be migrated to: if (!OrganizationUser.IsValidResetPasswordKey(organizationUser.ResetPasswordKey))\n            // after https://bitwarden.atlassian.net/browse/PM-31001 is resolved\n            if (organizationUser.ResetPasswordKey == null)\n            {\n                throw new BadRequestException(\"Reset Password keys cannot be set to null during rotation.\");\n            }\n\n            ou.ResetPasswordKey = organizationUser.ResetPasswordKey;\n            result.Add(ou);\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Validators/SendRotationValidator.cs",
    "content": "﻿using Bit.Api.Tools.Models.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.Services;\n\nnamespace Bit.Api.KeyManagement.Validators;\n\n/// <summary>\n/// Send implementation for <see cref=\"IRotationValidator{T,R}\"/>\n/// </summary>\npublic class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>\n{\n    private readonly ISendAuthorizationService _sendAuthorizationService;\n    private readonly ISendRepository _sendRepository;\n\n    /// <summary>\n    /// Instantiates a new <see cref=\"SendRotationValidator\"/>\n    /// </summary>\n    /// <param name=\"sendAuthorizationService\">Enables conversion of <see cref=\"SendWithIdRequestModel\"/> to <see cref=\"Send\"/></param>\n    /// <param name=\"sendRepository\">Retrieves all user <see cref=\"Send\"/>s</param>\n    public SendRotationValidator(ISendAuthorizationService sendAuthorizationService, ISendRepository sendRepository)\n    {\n        _sendAuthorizationService = sendAuthorizationService;\n        _sendRepository = sendRepository;\n    }\n\n    public async Task<IReadOnlyList<Send>> ValidateAsync(User user, IEnumerable<SendWithIdRequestModel> sends)\n    {\n        var result = new List<Send>();\n\n        var existingSends = await _sendRepository.GetManyByUserIdAsync(user.Id);\n        if (existingSends == null || existingSends.Count == 0)\n        {\n            return result;\n        }\n\n        foreach (var existing in existingSends)\n        {\n            var send = sends.FirstOrDefault(c => c.Id == existing.Id);\n            if (send == null)\n            {\n                throw new BadRequestException(\"All existing sends must be included in the rotation.\");\n            }\n\n            result.Add(send.UpdateSend(existing, _sendAuthorizationService));\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs",
    "content": "﻿using Bit.Api.Auth.Models.Request.WebAuthn;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Api.KeyManagement.Validators;\n\n/// <summary>\n/// Validates WebAuthn credentials during key rotation. Only processes credentials that have PRF enabled\n/// and have encrypted user, public, and private keys. Ensures all such credentials are included\n/// in the rotation request with the required encrypted keys.\n/// </summary>\npublic class WebAuthnLoginKeyRotationValidator : IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>,\n    IEnumerable<WebAuthnLoginRotateKeyData>>\n{\n    private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;\n\n    public WebAuthnLoginKeyRotationValidator(IWebAuthnCredentialRepository webAuthnCredentialRepository)\n    {\n        _webAuthnCredentialRepository = webAuthnCredentialRepository;\n    }\n\n    public async Task<IEnumerable<WebAuthnLoginRotateKeyData>> ValidateAsync(User user,\n        IEnumerable<WebAuthnLoginRotateKeyRequestModel> keysToRotate)\n    {\n        var result = new List<WebAuthnLoginRotateKeyData>();\n        var validCredentials = (await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id))\n            .Where(credential => credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled).ToList();\n        if (validCredentials.Count == 0)\n        {\n            return result;\n        }\n\n        foreach (var webAuthnCredential in validCredentials)\n        {\n            var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == webAuthnCredential.Id);\n            if (keyToRotate == null)\n            {\n                throw new BadRequestException(\"All existing webauthn prf keys must be included in the rotation.\");\n            }\n\n            if (keyToRotate.EncryptedUserKey == null)\n            {\n                throw new BadRequestException(\"WebAuthn prf keys must have user-key during rotation.\");\n            }\n\n            if (keyToRotate.EncryptedPublicKey == null)\n            {\n                throw new BadRequestException(\"WebAuthn prf keys must have public-key during rotation.\");\n            }\n\n            result.Add(keyToRotate.ToWebAuthnRotateKeyData());\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Public/CollectionBaseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Models.Public;\n\npublic abstract class CollectionBaseModel\n{\n    /// <summary>\n    /// External identifier for reference or linking this collection to another system.\n    /// </summary>\n    /// <example>external_id_123456</example>\n    [StringLength(300)]\n    public string ExternalId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Public/Request/CollectionUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Core.Entities;\n\nnamespace Bit.Api.Models.Public.Request;\n\npublic class CollectionUpdateRequestModel : CollectionBaseModel\n{\n    /// <summary>\n    /// The associated groups that this collection is assigned to.\n    /// </summary>\n    public IEnumerable<AssociationWithPermissionsRequestModel> Groups { get; set; }\n\n    public Collection ToCollection(Collection existingCollection)\n    {\n        existingCollection.ExternalId = ExternalId;\n        return existingCollection;\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Public/Response/CollectionResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Api.AdminConsole.Public.Models.Response;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.Models.Public.Response;\n\n/// <summary>\n/// A collection.\n/// </summary>\npublic class CollectionResponseModel : CollectionBaseModel, IResponseModel\n{\n    [JsonConstructor]\n    public CollectionResponseModel()\n    {\n\n    }\n\n    public CollectionResponseModel(Collection collection, IEnumerable<CollectionAccessSelection> groups)\n    {\n        if (collection == null)\n        {\n            throw new ArgumentNullException(nameof(collection));\n        }\n\n        Id = collection.Id;\n        ExternalId = collection.ExternalId;\n        Groups = groups?.Select(c => new AssociationWithPermissionsResponseModel(c));\n    }\n\n    /// <summary>\n    /// String representing the object's type. Objects of the same type share the same properties.\n    /// </summary>\n    /// <example>collection</example>\n    [Required]\n    public string Object => \"collection\";\n    /// <summary>\n    /// The collection's unique identifier.\n    /// </summary>\n    /// <example>539a36c5-e0d2-4cf9-979e-51ecf5cf6593</example>\n    [Required]\n    public Guid Id { get; set; }\n    /// <summary>\n    /// The associated groups that this collection is assigned to.\n    /// </summary>\n    public IEnumerable<AssociationWithPermissionsResponseModel> Groups { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Public/Response/ErrorResponseModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Api.Models.Public.Response;\n\npublic class ErrorResponseModel : IResponseModel\n{\n    public ErrorResponseModel(string message)\n    {\n        Message = message;\n    }\n\n    public ErrorResponseModel(ModelStateDictionary modelState)\n    {\n        Message = \"The request's model state is invalid.\";\n        Errors = new Dictionary<string, IEnumerable<string>>();\n\n        var keys = modelState.Keys.ToList();\n        var values = modelState.Values.ToList();\n\n        for (var i = 0; i < values.Count; i++)\n        {\n            var value = values[i];\n            if (keys.Count <= i)\n            {\n                // Keys not available for some reason.\n                break;\n            }\n\n            var key = keys[i];\n            if (value.ValidationState != ModelValidationState.Invalid || value.Errors.Count == 0)\n            {\n                continue;\n            }\n\n            var errors = value.Errors.Select(e => e.ErrorMessage);\n            Errors.Add(key, errors);\n        }\n    }\n\n    public ErrorResponseModel(Dictionary<string, IEnumerable<string>> errors)\n        : this(\"Errors have occurred.\", errors)\n    { }\n\n    public ErrorResponseModel(string errorKey, string errorValue)\n        : this(errorKey, [errorValue])\n    { }\n\n    public ErrorResponseModel(string errorKey, IEnumerable<string> errorValues)\n        : this(new Dictionary<string, IEnumerable<string>> { { errorKey, errorValues } })\n    { }\n\n    [JsonConstructor]\n    public ErrorResponseModel(string message, Dictionary<string, IEnumerable<string>> errors)\n    {\n        Message = message;\n        Errors = errors;\n    }\n\n    /// <summary>\n    /// String representing the object's type. Objects of the same type share the same properties.\n    /// </summary>\n    /// <example>error</example>\n    [Required]\n    public string Object => \"error\";\n    /// <summary>\n    /// A human-readable message providing details about the error.\n    /// </summary>\n    /// <example>The request model is invalid.</example>\n    [Required]\n    public string Message { get; init; }\n    /// <summary>\n    /// If multiple errors occurred, they are listed in dictionary. Errors related to a specific\n    /// request parameter will include a dictionary key describing that parameter.\n    /// </summary>\n    public Dictionary<string, IEnumerable<string>>? Errors { get; }\n}\n"
  },
  {
    "path": "src/Api/Models/Public/Response/IResponseModel.cs",
    "content": "﻿namespace Bit.Api.Models.Public.Response;\n\npublic interface IResponseModel\n{\n    string Object { get; }\n}\n"
  },
  {
    "path": "src/Api/Models/Public/Response/ListResponseModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Models.Public.Response;\n\npublic class ListResponseModel<T> : IResponseModel where T : IResponseModel\n{\n    public ListResponseModel(IEnumerable<T> data)\n    {\n        Data = data;\n    }\n\n    /// <summary>\n    /// String representing the object's type. Objects of the same type share the same properties.\n    /// </summary>\n    /// <example>list</example>\n    [Required]\n    public string Object => \"list\";\n    /// <summary>\n    /// An array containing the actual response elements, paginated by any request parameters.\n    /// </summary>\n    [Required]\n    public IEnumerable<T> Data { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Public/Response/PagedListResponseModel.cs",
    "content": "﻿namespace Bit.Api.Models.Public.Response;\n\npublic class PagedListResponseModel<T>(IEnumerable<T> data, string? continuationToken) : ListResponseModel<T>(data)\n    where T : IResponseModel\n{\n    /// <summary>\n    /// A cursor for use in pagination.\n    /// </summary>\n    public string? ContinuationToken { get; set; } = string.IsNullOrEmpty(continuationToken) ? null : continuationToken;\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Accounts/PremiumRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core;\nusing Bit.Core.Settings;\nusing Enums = Bit.Core.Enums;\n\nnamespace Bit.Api.Models.Request.Accounts;\n\npublic class PremiumRequestModel : IValidatableObject\n{\n    [Required]\n    public Enums.PaymentMethodType? PaymentMethodType { get; set; }\n    public string PaymentToken { get; set; }\n    [Range(0, 99)]\n    public short? AdditionalStorageGb { get; set; }\n    public IFormFile License { get; set; }\n    public string Country { get; set; }\n    public string PostalCode { get; set; }\n\n    public bool Validate(GlobalSettings globalSettings)\n    {\n        if (!(License == null && !globalSettings.SelfHosted) ||\n            (License != null && globalSettings.SelfHosted))\n        {\n            return false;\n        }\n        return globalSettings.SelfHosted || !string.IsNullOrWhiteSpace(Country);\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        var creditType = PaymentMethodType.HasValue && PaymentMethodType.Value == Enums.PaymentMethodType.Credit;\n        if (string.IsNullOrWhiteSpace(PaymentToken) && !creditType && License == null)\n        {\n            yield return new ValidationResult(\"Payment token or license is required.\");\n        }\n        if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode))\n        {\n            yield return new ValidationResult(\"Zip / postal code is required.\",\n                new string[] { nameof(PostalCode) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Accounts/StorageRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Models.Request.Accounts;\n\npublic class StorageRequestModel : IValidatableObject\n{\n    [Required]\n    public short? StorageGbAdjustment { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (StorageGbAdjustment == 0)\n        {\n            yield return new ValidationResult(\"Storage adjustment cannot be 0.\",\n                new string[] { nameof(StorageGbAdjustment) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core;\n\nnamespace Bit.Api.Models.Request.Accounts;\n\npublic class TaxInfoUpdateRequestModel : IValidatableObject\n{\n    [Required]\n    public string Country { get; set; }\n    public string PostalCode { get; set; }\n\n    public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode))\n        {\n            yield return new ValidationResult(\"Zip / postal code is required.\",\n                new string[] { nameof(PostalCode) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Accounts/UpdateAvatarRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\n\nnamespace Bit.Api.Models.Request.Accounts;\n\npublic class UpdateAvatarRequestModel\n{\n    [StringLength(7)]\n    public string AvatarColor { get; set; }\n\n    public User ToUser(User existingUser)\n    {\n        existingUser.AvatarColor = AvatarColor;\n        return existingUser;\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/BulkCollectionAccessRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Api.Models.Request;\n\npublic class BulkCollectionAccessRequestModel\n{\n    public IEnumerable<Guid> CollectionIds { get; set; }\n    public IEnumerable<SelectionReadOnlyRequestModel> Groups { get; set; }\n    public IEnumerable<SelectionReadOnlyRequestModel> Users { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/CollectionRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Models.Request;\n\npublic class CreateCollectionRequestModel\n{\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Name { get; set; }\n    [StringLength(300)]\n    public string ExternalId { get; set; }\n    public IEnumerable<SelectionReadOnlyRequestModel> Groups { get; set; }\n    public IEnumerable<SelectionReadOnlyRequestModel> Users { get; set; }\n\n    public Collection ToCollection(Guid orgId)\n    {\n        return ToCollection(new Collection\n        {\n            OrganizationId = orgId\n        });\n    }\n\n    public virtual Collection ToCollection(Collection existingCollection)\n    {\n        existingCollection.Name = Name;\n        existingCollection.ExternalId = ExternalId;\n        return existingCollection;\n    }\n}\n\npublic class CollectionBulkDeleteRequestModel\n{\n    [Required]\n    public IEnumerable<Guid> Ids { get; set; }\n}\n\npublic class CollectionWithIdRequestModel : CreateCollectionRequestModel\n{\n    public Guid? Id { get; set; }\n\n    public override Collection ToCollection(Collection existingCollection)\n    {\n        existingCollection.Id = Id ?? Guid.Empty;\n        return base.ToCollection(existingCollection);\n    }\n}\n\npublic class UpdateCollectionRequestModel : CreateCollectionRequestModel\n{\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public new string Name { get; set; }\n\n    public override Collection ToCollection(Collection existingCollection)\n    {\n        if (string.IsNullOrEmpty(existingCollection.DefaultUserCollectionEmail) && !string.IsNullOrWhiteSpace(Name))\n        {\n            existingCollection.Name = Name;\n        }\n        existingCollection.ExternalId = ExternalId;\n        return existingCollection;\n    }\n\n}\n"
  },
  {
    "path": "src/Api/Models/Request/DeviceRequestModels.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.PushRegistration;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Models.Request;\n\npublic class DeviceRequestModel\n{\n    [Required]\n    public DeviceType? Type { get; set; }\n    [Required]\n    [StringLength(50)]\n    public string Name { get; set; }\n    [Required]\n    [StringLength(50)]\n    public string Identifier { get; set; }\n    [StringLength(255)]\n    public string PushToken { get; set; }\n\n    public Device ToDevice(Guid? userId = null)\n    {\n        return ToDevice(new Device\n        {\n            UserId = userId == null ? default(Guid) : userId.Value\n        });\n    }\n\n    public Device ToDevice(Device existingDevice)\n    {\n        existingDevice.Name = Name;\n        existingDevice.Identifier = Identifier;\n        existingDevice.PushToken = PushToken;\n        existingDevice.Type = Type.Value;\n\n        return existingDevice;\n    }\n}\n\npublic class WebPushAuthRequestModel\n{\n    [Required]\n    public string Endpoint { get; set; }\n    [Required]\n    public string P256dh { get; set; }\n    [Required]\n    public string Auth { get; set; }\n\n    public WebPushRegistrationData ToData()\n    {\n        return new WebPushRegistrationData\n        {\n            Endpoint = Endpoint,\n            P256dh = P256dh,\n            Auth = Auth\n        };\n    }\n}\n\npublic class DeviceTokenRequestModel\n{\n    [StringLength(255)]\n    public string PushToken { get; set; }\n\n    public Device ToDevice(Device existingDevice)\n    {\n        existingDevice.PushToken = PushToken;\n        return existingDevice;\n    }\n}\n\npublic class DeviceKeysRequestModel\n{\n    /// <inheritdoc cref=\"Device.EncryptedUserKey\" />\n    [Required]\n    [EncryptedString]\n    public string EncryptedUserKey { get; set; }\n\n    /// <inheritdoc cref=\"Device.EncryptedPublicKey\" />\n    [Required]\n    [EncryptedString]\n    public string EncryptedPublicKey { get; set; }\n\n    /// <inheritdoc cref=\"Device.EncryptedPrivateKey\" />\n    [Required]\n    [EncryptedString]\n    public string EncryptedPrivateKey { get; set; }\n\n    public Device ToDevice(Device existingDevice)\n    {\n        existingDevice.EncryptedUserKey = EncryptedUserKey;\n        existingDevice.EncryptedPublicKey = EncryptedPublicKey;\n        existingDevice.EncryptedPrivateKey = EncryptedPrivateKey;\n\n        return existingDevice;\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/DeviceVerificationRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Models.Request;\n\npublic class DeviceVerificationRequestModel\n{\n    [Obsolete(\"Leaving this for backwards compatibility on clients\")]\n    [Required]\n    public bool UnknownDeviceVerificationEnabled { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/ExpandedTaxInfoUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Request.Accounts;\n\nnamespace Bit.Api.Models.Request;\n\npublic class ExpandedTaxInfoUpdateRequestModel : TaxInfoUpdateRequestModel\n{\n    public string TaxId { get; set; }\n    public string Line1 { get; set; }\n    public string Line2 { get; set; }\n    public string City { get; set; }\n    public string State { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/LicenseRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Models.Request;\n\npublic class LicenseRequestModel\n{\n    [Required]\n    public IFormFile License { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Business;\n\nnamespace Bit.Api.Models.Request.Organizations;\n\npublic class OrganizationCollectionManagementUpdateRequestModel\n{\n    public bool LimitCollectionCreation { get; set; }\n    public bool LimitCollectionDeletion { get; set; }\n    public bool LimitItemDeletion { get; set; }\n    public bool AllowAdminAccessToAllCollectionItems { get; set; }\n\n    public OrganizationCollectionManagementSettings ToSettings() => new()\n    {\n        LimitCollectionCreation = LimitCollectionCreation,\n        LimitCollectionDeletion = LimitCollectionDeletion,\n        LimitItemDeletion = LimitItemDeletion,\n        AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems\n    };\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Organizations/OrganizationCreateLicenseRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Models.Request.Organizations;\n\npublic class OrganizationCreateLicenseRequestModel : LicenseRequestModel\n{\n    [Required]\n    public string Key { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string CollectionName { get; set; }\n    public OrganizationKeysRequestModel Keys { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Organizations/OrganizationSponsorshipCreateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Models.Request.Organizations;\n\npublic class OrganizationSponsorshipCreateRequestModel\n{\n    [Required]\n    public PlanSponsorshipType PlanSponsorshipType { get; set; }\n\n    [Required]\n    [StringLength(256)]\n    [StrictEmailAddress]\n    public string SponsoredEmail { get; set; }\n\n    [StringLength(256)]\n    public string FriendlyName { get; set; }\n\n    public bool? IsAdminInitiated { get; set; }\n\n    [EncryptedString]\n    [EncryptedStringLength(512)]\n    public string Notes { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.Models.Request.Organizations;\n\npublic class OrganizationSponsorshipRedeemRequestModel\n{\n    [Required]\n    public PlanSponsorshipType PlanSponsorshipType { get; set; }\n    [Required]\n    public Guid SponsoredOrganizationId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Organizations/OrganizationSubscriptionUpdateRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Models.Request.Organizations;\n\npublic class OrganizationSubscriptionUpdateRequestModel\n{\n    [Required]\n    public int SeatAdjustment { get; set; }\n    public int? MaxAutoscaleSeats { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Organizations/OrganizationUserResetPasswordRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Models.Request.Organizations;\n\npublic class OrganizationUserResetPasswordRequestModel\n{\n    [Required]\n    [StringLength(300)]\n    public string NewMasterPasswordHash { get; set; }\n    [Required]\n    public string Key { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Organizations/SecretsManagerSubscribeRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Models.Request.Organizations;\n\npublic class SecretsManagerSubscribeRequestModel\n{\n    [Required]\n    [Range(0, int.MaxValue)]\n    public int AdditionalSmSeats { get; set; }\n\n    [Required]\n    [Range(0, int.MaxValue)]\n    public int AdditionalServiceAccounts { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/Organizations/SecretsManagerSubscriptionUpdateRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Api.Models.Request.Organizations;\n\npublic class SecretsManagerSubscriptionUpdateRequestModel\n{\n    [Required]\n    public int SeatAdjustment { get; set; }\n    public int? MaxAutoscaleSeats { get; set; }\n    public int ServiceAccountAdjustment { get; set; }\n    public int? MaxAutoscaleServiceAccounts { get; set; }\n\n    public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)\n    {\n        return new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            MaxAutoscaleSmSeats = MaxAutoscaleSeats,\n            MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts\n        }\n        .AdjustSeats(SeatAdjustment)\n        .AdjustServiceAccounts(ServiceAccountAdjustment);\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/PaymentRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.Models.Request;\n\npublic class PaymentRequestModel : ExpandedTaxInfoUpdateRequestModel\n{\n    [Required]\n    public PaymentMethodType? PaymentMethodType { get; set; }\n    [Required]\n    public string PaymentToken { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/SelectionReadOnlyRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.Models.Request;\n\npublic class SelectionReadOnlyRequestModel\n{\n    [Required]\n    public Guid Id { get; set; }\n    public bool ReadOnly { get; set; }\n    public bool HidePasswords { get; set; }\n    public bool Manage { get; set; }\n\n    public CollectionAccessSelection ToSelectionReadOnly()\n    {\n        return new CollectionAccessSelection\n        {\n            Id = Id,\n            ReadOnly = ReadOnly,\n            HidePasswords = HidePasswords,\n            Manage = Manage,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/SubscriptionCancellationRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Api.Models.Request;\n\npublic class SubscriptionCancellationRequestModel\n{\n    public string Reason { get; set; }\n    public string Feedback { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Request/UpdateDomainsRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Api.Models.Request;\n\npublic class UpdateDomainsRequestModel\n{\n    public IEnumerable<IEnumerable<string>> EquivalentDomains { get; set; }\n    public IEnumerable<GlobalEquivalentDomainsType> ExcludedGlobalEquivalentDomains { get; set; }\n\n    public User ToUser(User existingUser)\n    {\n        existingUser.EquivalentDomains = EquivalentDomains != null ? JsonSerializer.Serialize(EquivalentDomains) : null;\n        existingUser.ExcludedGlobalEquivalentDomains = ExcludedGlobalEquivalentDomains != null ?\n            JsonSerializer.Serialize(ExcludedGlobalEquivalentDomains) : null;\n        return existingUser;\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/ApiKeyResponseModel.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Models.Response;\n\npublic class ApiKeyResponseModel : ResponseModel\n{\n    public ApiKeyResponseModel(OrganizationApiKey organizationApiKey, string obj = \"apiKey\")\n        : base(obj)\n    {\n        if (organizationApiKey == null)\n        {\n            throw new ArgumentNullException(nameof(organizationApiKey));\n        }\n        ApiKey = organizationApiKey.ApiKey;\n        RevisionDate = organizationApiKey.RevisionDate;\n    }\n\n    public ApiKeyResponseModel(User user, string obj = \"apiKey\")\n        : base(obj)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n        ApiKey = user.ApiKey;\n        RevisionDate = user.RevisionDate;\n    }\n\n    public string ApiKey { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/CollectionResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Api.Models.Response;\n\npublic class CollectionResponseModel : ResponseModel\n{\n    public CollectionResponseModel(Collection collection, string obj = \"collection\")\n        : base(obj)\n    {\n        if (collection == null)\n        {\n            throw new ArgumentNullException(nameof(collection));\n        }\n\n        Id = collection.Id;\n        OrganizationId = collection.OrganizationId;\n        Name = collection.Name;\n        ExternalId = collection.ExternalId;\n        Type = collection.Type;\n        DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail;\n    }\n\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string Name { get; set; }\n    public string ExternalId { get; set; }\n    public CollectionType Type { get; set; }\n    public string DefaultUserCollectionEmail { get; set; }\n}\n\n/// <summary>\n/// Response model for a collection that is always assigned to the requesting user, including permissions.\n/// </summary>\npublic class CollectionDetailsResponseModel : CollectionResponseModel\n{\n    /// <summary>\n    /// Create a response model for when the user is assumed to be assigned to the collection with permissions.\n    /// e.g. The collection details comes from a repository method that only returns collections the user is assigned to.\n    /// </summary>\n    public CollectionDetailsResponseModel(CollectionDetails collectionDetails)\n        : base(collectionDetails, \"collectionDetails\")\n    {\n        ReadOnly = collectionDetails.ReadOnly;\n        HidePasswords = collectionDetails.HidePasswords;\n        Manage = collectionDetails.Manage;\n        DefaultUserCollectionEmail = collectionDetails.DefaultUserCollectionEmail;\n    }\n\n    public bool ReadOnly { get; set; }\n    public bool HidePasswords { get; set; }\n    public bool Manage { get; set; }\n}\n\npublic class CollectionAccessDetailsResponseModel : CollectionResponseModel\n{\n    /// <summary>\n    /// Create a response model for when the requesting user is assumed not assigned to the collection.\n    /// No user permissions are included.\n    ///\n    /// Ideally, the CollectionAdminDetails constructor should be used instead wherever possible. This is only\n    /// used in the case of MSPs where the Provider user will likely never be assigned to the collection.\n    /// </summary>\n    /// <param name=\"collection\"></param>\n    public CollectionAccessDetailsResponseModel(Collection collection)\n        : base(collection, \"collectionAccessDetails\")\n    { }\n\n    /// <summary>\n    /// Create a response model for when the requesting user is assumed not assigned to the collection. Includes\n    /// the other groups and user relationships for the collection.\n    /// No user permissions are included.\n    /// </summary>\n    /// <param name=\"collection\"></param>\n    /// <param name=\"groups\"></param>\n    /// <param name=\"users\"></param>\n    [Obsolete(\"Use the CollectionAdminDetails constructor instead.\")]\n    public CollectionAccessDetailsResponseModel(Collection collection, IEnumerable<CollectionAccessSelection> groups, IEnumerable<CollectionAccessSelection> users)\n        : base(collection, \"collectionAccessDetails\")\n    {\n        Groups = groups.Select(g => new SelectionReadOnlyResponseModel(g));\n        Users = users.Select(g => new SelectionReadOnlyResponseModel(g));\n    }\n\n    /// <summary>\n    /// Create a response model for when the requesting user's assignment is available via CollectionAdminDetails.\n    /// </summary>\n    /// <param name=\"collection\"></param>\n    public CollectionAccessDetailsResponseModel(CollectionAdminDetails collection)\n        : base(collection, \"collectionAccessDetails\")\n    {\n        Assigned = collection.Assigned;\n        ReadOnly = collection.ReadOnly;\n        HidePasswords = collection.HidePasswords;\n        Manage = collection.Manage;\n        Unmanaged = collection.Unmanaged;\n        Groups = collection.Groups?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty<SelectionReadOnlyResponseModel>();\n        Users = collection.Users?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty<SelectionReadOnlyResponseModel>();\n    }\n\n    public IEnumerable<SelectionReadOnlyResponseModel> Groups { get; set; }\n    public IEnumerable<SelectionReadOnlyResponseModel> Users { get; set; }\n\n    /// <summary>\n    /// True if the acting user is explicitly assigned to the collection\n    /// </summary>\n    public bool Assigned { get; set; }\n\n    public bool ReadOnly { get; set; }\n    public bool HidePasswords { get; set; }\n    public bool Manage { get; set; }\n    public bool Unmanaged { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/ConfigResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\n\nusing Bit.Core;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Models.Response;\n\npublic class ConfigResponseModel : ResponseModel\n{\n    public string Version { get; set; }\n    public string GitHash { get; set; }\n    public ServerConfigResponseModel Server { get; set; }\n    public EnvironmentConfigResponseModel Environment { get; set; }\n    public IDictionary<string, object> FeatureStates { get; set; }\n    public PushSettings Push { get; set; }\n    public CommunicationSettings Communication { get; set; }\n    public ServerSettingsResponseModel Settings { get; set; }\n\n    public ConfigResponseModel() : base(\"config\")\n    {\n        Version = AssemblyHelpers.GetVersion();\n        GitHash = AssemblyHelpers.GetGitHash();\n        Environment = new EnvironmentConfigResponseModel();\n        FeatureStates = new Dictionary<string, object>();\n        Settings = new ServerSettingsResponseModel();\n    }\n\n    public ConfigResponseModel(\n        IFeatureService featureService,\n        IGlobalSettings globalSettings\n        ) : base(\"config\")\n    {\n        Version = AssemblyHelpers.GetVersion();\n        GitHash = AssemblyHelpers.GetGitHash();\n        Environment = new EnvironmentConfigResponseModel\n        {\n            CloudRegion = globalSettings.BaseServiceUri.CloudRegion,\n            Vault = globalSettings.BaseServiceUri.Vault,\n            Api = globalSettings.BaseServiceUri.Api,\n            Identity = globalSettings.BaseServiceUri.Identity,\n            Notifications = globalSettings.BaseServiceUri.Notifications,\n            Sso = globalSettings.BaseServiceUri.Sso,\n            FillAssistRules = globalSettings.BaseServiceUri.FillAssistRules\n        };\n        FeatureStates = featureService.GetAll();\n        var webPushEnabled = FeatureStates.TryGetValue(FeatureFlagKeys.WebPush, out var webPushEnabledValue) ? (bool)webPushEnabledValue : false;\n        Push = PushSettings.Build(webPushEnabled, globalSettings);\n        Communication = CommunicationSettings.Build(globalSettings);\n        Settings = new ServerSettingsResponseModel\n        {\n            DisableUserRegistration = globalSettings.DisableUserRegistration\n        };\n    }\n}\n\npublic class ServerConfigResponseModel\n{\n    public string Name { get; set; }\n    public string Url { get; set; }\n}\n\npublic class EnvironmentConfigResponseModel\n{\n    public string CloudRegion { get; set; }\n    public string Vault { get; set; }\n    public string Api { get; set; }\n    public string Identity { get; set; }\n    public string Notifications { get; set; }\n    public string Sso { get; set; }\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string FillAssistRules { get; set; }\n}\n\npublic class PushSettings\n{\n    public PushTechnologyType PushTechnology { get; private init; }\n    public string VapidPublicKey { get; private init; }\n\n    public static PushSettings Build(bool webPushEnabled, IGlobalSettings globalSettings)\n    {\n        var vapidPublicKey = webPushEnabled ? globalSettings.WebPush.VapidPublicKey : null;\n        var pushTechnology = vapidPublicKey != null ? PushTechnologyType.WebPush : PushTechnologyType.SignalR;\n        return new()\n        {\n            VapidPublicKey = vapidPublicKey,\n            PushTechnology = pushTechnology\n        };\n    }\n}\n\npublic class CommunicationSettings\n{\n    public CommunicationBootstrapSettings Bootstrap { get; private init; }\n\n    public static CommunicationSettings Build(IGlobalSettings globalSettings)\n    {\n        var bootstrap = CommunicationBootstrapSettings.Build(globalSettings);\n        return bootstrap == null ? null : new() { Bootstrap = bootstrap };\n    }\n}\n\npublic class CommunicationBootstrapSettings\n{\n    public string Type { get; private init; }\n    public string IdpLoginUrl { get; private init; }\n    public string CookieName { get; private init; }\n    public string CookieDomain { get; private init; }\n\n    public static CommunicationBootstrapSettings Build(IGlobalSettings globalSettings)\n    {\n        return globalSettings.Communication?.Bootstrap?.ToLowerInvariant() switch\n        {\n            \"ssocookievendor\" => new()\n            {\n                Type = \"ssoCookieVendor\",\n                IdpLoginUrl = globalSettings.Communication?.SsoCookieVendor?.IdpLoginUrl,\n                CookieName = globalSettings.Communication?.SsoCookieVendor?.CookieName,\n                CookieDomain = globalSettings.Communication?.SsoCookieVendor?.CookieDomain\n            },\n            _ => null\n        };\n    }\n}\n\npublic class ServerSettingsResponseModel\n{\n    public bool DisableUserRegistration { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/DeviceResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Models.Response;\n\npublic class DeviceResponseModel : ResponseModel\n{\n    public DeviceResponseModel(Device device)\n        : base(\"device\")\n    {\n        if (device == null)\n        {\n            throw new ArgumentNullException(nameof(device));\n        }\n\n        Id = device.Id;\n        Name = device.Name;\n        Type = device.Type;\n        Identifier = device.Identifier;\n        CreationDate = device.CreationDate;\n        IsTrusted = device.IsTrusted();\n        EncryptedUserKey = device.EncryptedUserKey;\n        EncryptedPublicKey = device.EncryptedPublicKey;\n    }\n\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public DeviceType Type { get; set; }\n    public string Identifier { get; set; }\n    public DateTime CreationDate { get; set; }\n    public bool IsTrusted { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedUserKey { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedPublicKey { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/DeviceVerificationResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\n\nnamespace Bit.Api.Models.Response;\n\n[Obsolete(\"Leaving this for backwards compatibility on clients\")]\npublic class DeviceVerificationResponseModel : ResponseModel\n{\n    public DeviceVerificationResponseModel(bool isDeviceVerificationSectionEnabled, bool unknownDeviceVerificationEnabled)\n        : base(\"deviceVerificationSettings\")\n    {\n        IsDeviceVerificationSectionEnabled = isDeviceVerificationSectionEnabled;\n        UnknownDeviceVerificationEnabled = unknownDeviceVerificationEnabled;\n    }\n\n    public bool IsDeviceVerificationSectionEnabled { get; }\n    public bool UnknownDeviceVerificationEnabled { get; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/DomainsResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Models.Response;\n\npublic class DomainsResponseModel() : ResponseModel(\"domains\")\n{\n    public DomainsResponseModel(User user, bool excluded = true)\n        : this()\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        EquivalentDomains = user.EquivalentDomains != null ?\n            JsonSerializer.Deserialize<List<List<string>>>(user.EquivalentDomains) : null;\n\n        var excludedGlobalEquivalentDomains = user.ExcludedGlobalEquivalentDomains != null ?\n            JsonSerializer.Deserialize<List<GlobalEquivalentDomainsType>>(user.ExcludedGlobalEquivalentDomains) :\n            new List<GlobalEquivalentDomainsType>();\n        var globalDomains = new List<GlobalDomains>();\n        var domainsToInclude = excluded ? Core.Utilities.StaticStore.GlobalDomains :\n            Core.Utilities.StaticStore.GlobalDomains.Where(d => !excludedGlobalEquivalentDomains.Contains(d.Key));\n        foreach (var domain in domainsToInclude)\n        {\n            globalDomains.Add(new GlobalDomains(domain.Key, domain.Value, excludedGlobalEquivalentDomains, excluded));\n        }\n        GlobalEquivalentDomains = !globalDomains.Any() ? null : globalDomains;\n    }\n\n    public IEnumerable<IEnumerable<string>> EquivalentDomains { get; set; }\n    public IEnumerable<GlobalDomains> GlobalEquivalentDomains { get; set; }\n\n\n    public class GlobalDomains()\n    {\n        public GlobalDomains(\n            GlobalEquivalentDomainsType globalDomain,\n            IEnumerable<string> domains,\n            IEnumerable<GlobalEquivalentDomainsType> excludedDomains,\n            bool excluded) : this()\n        {\n            Type = (byte)globalDomain;\n            Domains = domains;\n            Excluded = excluded && (excludedDomains?.Contains(globalDomain) ?? false);\n        }\n\n        public byte Type { get; set; }\n        public IEnumerable<string> Domains { get; set; }\n        public bool Excluded { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/KeysResponseModel.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Api.Response;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Models.Response;\n\npublic class KeysResponseModel : ResponseModel\n{\n    public KeysResponseModel(UserAccountKeysData accountKeys, string? masterKeyWrappedUserKey)\n        : base(\"keys\")\n    {\n        if (masterKeyWrappedUserKey != null)\n        {\n            Key = masterKeyWrappedUserKey;\n        }\n\n        PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;\n        PrivateKey = accountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey;\n        AccountKeys = new PrivateKeysResponseModel(accountKeys);\n    }\n\n    /// <summary>\n    /// The master key wrapped user key. The master key can either be a master-password master key or a\n    /// key-connector master key.\n    /// </summary>\n    public string? Key { get; set; }\n    [Obsolete(\"Use AccountKeys.PublicKeyEncryptionKeyPair.PublicKey instead\")]\n    public string PublicKey { get; set; }\n    [Obsolete(\"Use AccountKeys.PublicKeyEncryptionKeyPair.WrappedPrivateKey instead\")]\n    public string PrivateKey { get; set; }\n    public PrivateKeysResponseModel AccountKeys { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/ListResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Models.Response;\n\npublic class ListResponseModel<T> : ResponseModel where T : ResponseModel\n{\n    public ListResponseModel(IEnumerable<T> data, string continuationToken = null)\n        : base(\"list\")\n    {\n        Data = data;\n        ContinuationToken = continuationToken;\n    }\n\n    public IEnumerable<T> Data { get; set; }\n    public string ContinuationToken { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/Organizations/OrganizationSponsorshipSyncStatusResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\n\nnamespace Bit.Api.Models.Response.Organizations;\n\npublic class OrganizationSponsorshipSyncStatusResponseModel : ResponseModel\n{\n    public OrganizationSponsorshipSyncStatusResponseModel(DateTime? lastSyncDate)\n        : base(\"syncStatus\")\n    {\n        LastSyncDate = lastSyncDate;\n    }\n\n    public DateTime? LastSyncDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/PaymentResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Models.Response;\n\npublic class PaymentResponseModel : ResponseModel\n{\n    public PaymentResponseModel()\n        : base(\"payment\")\n    { }\n\n    public ProfileResponseModel UserProfile { get; set; }\n    public string PaymentIntentClientSecret { get; set; }\n    public bool Success { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/PlanResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Api.Models.Response;\n\npublic class PlanResponseModel : ResponseModel\n{\n    public PlanResponseModel(Plan plan, string obj = \"plan\")\n        : base(obj)\n    {\n        if (plan == null)\n        {\n            throw new ArgumentNullException(nameof(plan));\n        }\n\n        Type = plan.Type;\n        ProductTier = plan.ProductTier;\n        Name = plan.Name;\n        IsAnnual = plan.IsAnnual;\n        NameLocalizationKey = plan.NameLocalizationKey;\n        DescriptionLocalizationKey = plan.DescriptionLocalizationKey;\n        CanBeUsedByBusiness = plan.CanBeUsedByBusiness;\n        TrialPeriodDays = plan.TrialPeriodDays;\n        HasSelfHost = plan.HasSelfHost;\n        HasPolicies = plan.HasPolicies;\n        HasGroups = plan.HasGroups;\n        HasDirectory = plan.HasDirectory;\n        HasEvents = plan.HasEvents;\n        HasTotp = plan.HasTotp;\n        Has2fa = plan.Has2fa;\n        HasSso = plan.HasSso;\n        HasOrganizationDomains = plan.HasOrganizationDomains;\n        HasResetPassword = plan.HasResetPassword;\n        UsersGetPremium = plan.UsersGetPremium;\n        HasMyItems = plan.HasMyItems;\n        UpgradeSortOrder = plan.UpgradeSortOrder;\n        DisplaySortOrder = plan.DisplaySortOrder;\n        LegacyYear = plan.LegacyYear;\n        Disabled = plan.Disabled;\n        if (plan.SecretsManager != null)\n        {\n            SecretsManager = new SecretsManagerPlanFeaturesResponseModel(plan.SecretsManager);\n        }\n\n        PasswordManager = new PasswordManagerPlanFeaturesResponseModel(plan.PasswordManager);\n    }\n\n    public PlanResponseModel(Organization organization, string obj = \"plan\") : base(obj)\n    {\n        Type = organization.PlanType;\n        ProductTier = organization.PlanType.GetProductTier();\n        Name = organization.Plan;\n    }\n\n    public PlanType Type { get; set; }\n    public ProductTierType ProductTier { get; set; }\n    public string Name { get; set; }\n    public bool IsAnnual { get; set; }\n    public string NameLocalizationKey { get; set; }\n    public string DescriptionLocalizationKey { get; set; }\n    public bool CanBeUsedByBusiness { get; set; }\n    public int? TrialPeriodDays { get; set; }\n\n    public bool HasSelfHost { get; set; }\n    public bool HasPolicies { get; set; }\n    public bool HasGroups { get; set; }\n    public bool HasDirectory { get; set; }\n    public bool HasEvents { get; set; }\n    public bool HasTotp { get; set; }\n    public bool Has2fa { get; set; }\n    public bool HasApi { get; set; }\n    public bool HasSso { get; set; }\n    public bool HasOrganizationDomains { get; set; }\n    public bool HasResetPassword { get; set; }\n    public bool UsersGetPremium { get; set; }\n    public bool HasMyItems { get; set; }\n\n    public int UpgradeSortOrder { get; set; }\n    public int DisplaySortOrder { get; set; }\n    public int? LegacyYear { get; set; }\n    public bool Disabled { get; set; }\n    public SecretsManagerPlanFeaturesResponseModel SecretsManager { get; protected init; }\n    public PasswordManagerPlanFeaturesResponseModel PasswordManager { get; protected init; }\n\n    public class SecretsManagerPlanFeaturesResponseModel\n    {\n        public SecretsManagerPlanFeaturesResponseModel(Plan.SecretsManagerPlanFeatures plan)\n        {\n            MaxServiceAccounts = plan.MaxServiceAccounts;\n            AllowServiceAccountsAutoscale = plan is { AllowServiceAccountsAutoscale: true };\n            StripeServiceAccountPlanId = plan.StripeServiceAccountPlanId;\n            AdditionalPricePerServiceAccount = plan.AdditionalPricePerServiceAccount;\n            BaseServiceAccount = plan.BaseServiceAccount;\n            MaxAdditionalServiceAccount = plan.MaxAdditionalServiceAccount;\n            HasAdditionalServiceAccountOption = plan is { HasAdditionalServiceAccountOption: true };\n            StripeSeatPlanId = plan.StripeSeatPlanId;\n            HasAdditionalSeatsOption = plan is { HasAdditionalSeatsOption: true };\n            BasePrice = plan.BasePrice;\n            SeatPrice = plan.SeatPrice;\n            BaseSeats = plan.BaseSeats;\n            MaxSeats = plan.MaxSeats;\n            MaxAdditionalSeats = plan.MaxAdditionalSeats;\n            AllowSeatAutoscale = plan.AllowSeatAutoscale;\n            MaxProjects = plan.MaxProjects;\n        }\n        // Service accounts\n        public short? MaxServiceAccounts { get; init; }\n        public bool AllowServiceAccountsAutoscale { get; init; }\n        public string StripeServiceAccountPlanId { get; init; }\n        public decimal? AdditionalPricePerServiceAccount { get; init; }\n        public short? BaseServiceAccount { get; init; }\n        public short? MaxAdditionalServiceAccount { get; init; }\n        public bool HasAdditionalServiceAccountOption { get; init; }\n        // Seats\n        public string StripeSeatPlanId { get; init; }\n        public bool HasAdditionalSeatsOption { get; init; }\n        public decimal BasePrice { get; init; }\n        public decimal SeatPrice { get; init; }\n        public int BaseSeats { get; init; }\n        public short? MaxSeats { get; init; }\n        public int? MaxAdditionalSeats { get; init; }\n        public bool AllowSeatAutoscale { get; init; }\n\n        // Features\n        public int MaxProjects { get; init; }\n    }\n\n    public record PasswordManagerPlanFeaturesResponseModel\n    {\n        public PasswordManagerPlanFeaturesResponseModel(Plan.PasswordManagerPlanFeatures plan)\n        {\n            StripePlanId = plan.StripePlanId;\n            StripeSeatPlanId = plan.StripeSeatPlanId;\n            StripeProviderPortalSeatPlanId = plan.StripeProviderPortalSeatPlanId;\n            BasePrice = plan.BasePrice;\n            SeatPrice = plan.SeatPrice;\n            ProviderPortalSeatPrice = plan.ProviderPortalSeatPrice;\n            AllowSeatAutoscale = plan.AllowSeatAutoscale;\n            HasAdditionalSeatsOption = plan.HasAdditionalSeatsOption;\n            MaxAdditionalSeats = plan.MaxAdditionalSeats;\n            BaseSeats = plan.BaseSeats;\n            HasPremiumAccessOption = plan.HasPremiumAccessOption;\n            StripePremiumAccessPlanId = plan.StripePremiumAccessPlanId;\n            PremiumAccessOptionPrice = plan.PremiumAccessOptionPrice;\n            MaxSeats = plan.MaxSeats;\n            BaseStorageGb = plan.BaseStorageGb;\n            HasAdditionalStorageOption = plan.HasAdditionalStorageOption;\n            AdditionalStoragePricePerGb = plan.AdditionalStoragePricePerGb;\n            StripeStoragePlanId = plan.StripeStoragePlanId;\n            MaxAdditionalStorage = plan.MaxAdditionalStorage;\n            MaxCollections = plan.MaxCollections;\n        }\n        // Seats\n        public string StripePlanId { get; init; }\n        public string StripeSeatPlanId { get; init; }\n        public string StripeProviderPortalSeatPlanId { get; init; }\n        public decimal BasePrice { get; init; }\n        public decimal SeatPrice { get; init; }\n        public decimal ProviderPortalSeatPrice { get; init; }\n        public bool AllowSeatAutoscale { get; init; }\n        public bool HasAdditionalSeatsOption { get; init; }\n        public int? MaxAdditionalSeats { get; init; }\n        public int BaseSeats { get; init; }\n        public bool HasPremiumAccessOption { get; init; }\n        public string StripePremiumAccessPlanId { get; init; }\n        public decimal PremiumAccessOptionPrice { get; init; }\n        public short? MaxSeats { get; init; }\n        // Storage\n        public short? BaseStorageGb { get; init; }\n        public bool HasAdditionalStorageOption { get; init; }\n        public decimal AdditionalStoragePricePerGb { get; init; }\n        public string StripeStoragePlanId { get; init; }\n        public short? MaxAdditionalStorage { get; init; }\n        // Feature\n        public short? MaxCollections { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/ProfileResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Response;\nusing Bit.Api.AdminConsole.Models.Response.Providers;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Api.Response;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Api.Models.Response;\n\npublic class ProfileResponseModel : ResponseModel\n{\n    public ProfileResponseModel(User user,\n        UserAccountKeysData userAccountKeysData,\n        IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,\n        IEnumerable<ProviderUserProviderDetails> providerUserDetails,\n        IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,\n        bool twoFactorEnabled,\n        bool premiumFromOrganization,\n        IEnumerable<Guid> organizationIdsClaimingUser) : base(\"profile\")\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        Id = user.Id;\n        Name = user.Name;\n        Email = user.Email;\n        EmailVerified = user.EmailVerified;\n        Premium = user.Premium;\n        PremiumFromOrganization = premiumFromOrganization;\n        Culture = user.Culture;\n        TwoFactorEnabled = twoFactorEnabled;\n        Key = user.Key;\n        PrivateKey = user.PrivateKey;\n        AccountKeys = userAccountKeysData != null ? new PrivateKeysResponseModel(userAccountKeysData) : null;\n        SecurityStamp = user.SecurityStamp;\n        ForcePasswordReset = user.ForcePasswordReset;\n        UsesKeyConnector = user.UsesKeyConnector;\n        AvatarColor = user.AvatarColor;\n        CreationDate = user.CreationDate;\n        VerifyDevices = user.VerifyDevices;\n        Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsClaimingUser));\n        Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));\n        ProviderOrganizations =\n            providerUserOrganizationDetails?.Select(po => new ProfileProviderOrganizationResponseModel(po));\n    }\n\n    public ProfileResponseModel() : base(\"profile\")\n    {\n    }\n\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public string Email { get; set; }\n    public bool EmailVerified { get; set; }\n    public bool Premium { get; set; }\n    public bool PremiumFromOrganization { get; set; }\n    public string Culture { get; set; }\n    public bool TwoFactorEnabled { get; set; }\n    public string Key { get; set; }\n    [Obsolete(\"Use AccountKeys instead.\")]\n    public string PrivateKey { get; set; }\n    public PrivateKeysResponseModel AccountKeys { get; set; }\n    public string SecurityStamp { get; set; }\n    public bool ForcePasswordReset { get; set; }\n    public bool UsesKeyConnector { get; set; }\n    public string AvatarColor { get; set; }\n    public DateTime CreationDate { get; set; }\n    public bool VerifyDevices { get; set; }\n    public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }\n    public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }\n    public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/SelectionReadOnlyResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Data;\n\nnamespace Bit.Api.Models.Response;\n\npublic class SelectionReadOnlyResponseModel\n{\n    public SelectionReadOnlyResponseModel(CollectionAccessSelection selection)\n    {\n        if (selection == null)\n        {\n            throw new ArgumentNullException(nameof(selection));\n        }\n\n        Id = selection.Id;\n        ReadOnly = selection.ReadOnly;\n        HidePasswords = selection.HidePasswords;\n        Manage = selection.Manage;\n    }\n\n    public Guid Id { get; set; }\n    public bool ReadOnly { get; set; }\n    public bool HidePasswords { get; set; }\n    public bool Manage { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/SubscriptionResponseModel.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Licenses;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Models.Response;\n\n// TODO: Remove with deletion of pm-29594-update-individual-subscription-page\npublic class SubscriptionResponseModel : ResponseModel\n{\n\n    /// <param name=\"user\">The user entity containing storage and premium subscription information</param>\n    /// <param name=\"subscription\">Subscription information retrieved from the payment provider (Stripe/Braintree)</param>\n    /// <param name=\"license\">The user's license containing expiration and feature entitlements</param>\n    /// <param name=\"includeMilestone2Discount\">\n    /// Whether to include discount information in the response.\n    /// Set to true when the PM23341_Milestone_2 feature flag is enabled AND\n    /// you want to expose Milestone 2 discount information to the client.\n    /// The discount will only be included if it matches the specific Milestone 2 coupon ID.\n    /// </param>\n    public SubscriptionResponseModel(User user, SubscriptionInfo subscription, UserLicense license, bool includeMilestone2Discount = false)\n        : base(\"subscription\")\n    {\n        Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;\n        UpcomingInvoice = subscription.UpcomingInvoice != null ?\n            new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;\n        StorageName = user.Storage.HasValue ? CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;\n        StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB\n        MaxStorageGb = user.MaxStorageGb;\n        License = license;\n        Expiration = License.Expires;\n\n        // Only display the Milestone 2 subscription discount on the subscription page.\n        CustomerDiscount = ShouldIncludeMilestone2Discount(includeMilestone2Discount, subscription.CustomerDiscount)\n            ? new BillingCustomerDiscount(subscription.CustomerDiscount!)\n            : null;\n    }\n\n    /// <param name=\"user\">The user entity containing storage and premium subscription information</param>\n    /// <param name=\"subscription\">Subscription information retrieved from the payment provider (Stripe/Braintree)</param>\n    /// <param name=\"license\">The user's license containing expiration and feature entitlements</param>\n    /// <param name=\"claimsPrincipal\">The claims principal containing cryptographically secure token claims</param>\n    /// <param name=\"includeMilestone2Discount\">\n    /// Whether to include discount information in the response.\n    /// Set to true when the PM23341_Milestone_2 feature flag is enabled AND\n    /// you want to expose Milestone 2 discount information to the client.\n    /// The discount will only be included if it matches the specific Milestone 2 coupon ID.\n    /// </param>\n    public SubscriptionResponseModel(User user, SubscriptionInfo? subscription, UserLicense license, ClaimsPrincipal? claimsPrincipal, bool includeMilestone2Discount = false)\n        : base(\"subscription\")\n    {\n        Subscription = subscription?.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;\n        UpcomingInvoice = subscription?.UpcomingInvoice != null ?\n            new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;\n        StorageName = user.Storage.HasValue ? CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;\n        StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB\n        MaxStorageGb = user.MaxStorageGb;\n        License = license;\n\n        // CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim\n        // The token's expiration is cryptographically secured and cannot be tampered with\n        // The file's Expires property can be manually edited and should NOT be trusted for display\n        if (claimsPrincipal != null)\n        {\n            Expiration = claimsPrincipal.GetValue<DateTime?>(UserLicenseConstants.Expires);\n        }\n        else\n        {\n            // No token - use the license file expiration (for older licenses without tokens)\n            Expiration = License.Expires;\n        }\n\n        // Only display the Milestone 2 subscription discount on the subscription page.\n        CustomerDiscount = ShouldIncludeMilestone2Discount(includeMilestone2Discount, subscription?.CustomerDiscount)\n            ? new BillingCustomerDiscount(subscription!.CustomerDiscount!)\n            : null;\n    }\n\n    public SubscriptionResponseModel(User user, UserLicense? license = null)\n        : base(\"subscription\")\n    {\n        StorageName = user.Storage.HasValue ? CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;\n        StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB\n        MaxStorageGb = user.MaxStorageGb;\n        Expiration = user.PremiumExpirationDate;\n\n        if (license != null)\n        {\n            License = license;\n        }\n    }\n\n    public string? StorageName { get; set; }\n    public double? StorageGb { get; set; }\n    public short? MaxStorageGb { get; set; }\n    public BillingSubscriptionUpcomingInvoice? UpcomingInvoice { get; set; }\n    public BillingSubscription? Subscription { get; set; }\n    /// <summary>\n    /// Customer discount information from Stripe for the Milestone 2 subscription discount.\n    /// Only includes the specific Milestone 2 coupon (cm3nHfO1) when it's a perpetual discount (no expiration).\n    /// This is for display purposes only and does not affect Stripe's automatic discount application.\n    /// Other discounts may still apply in Stripe billing but are not included in this response.\n    /// <para>\n    /// Null when:\n    /// - The PM23341_Milestone_2 feature flag is disabled\n    /// - There is no active discount\n    /// - The discount coupon ID doesn't match the Milestone 2 coupon (cm3nHfO1)\n    /// - The instance is self-hosted\n    /// </para>\n    /// </summary>\n    public BillingCustomerDiscount? CustomerDiscount { get; set; }\n    public UserLicense? License { get; set; }\n    public DateTime? Expiration { get; set; }\n\n    /// <summary>\n    /// Determines whether the Milestone 2 discount should be included in the response.\n    /// </summary>\n    /// <param name=\"includeMilestone2Discount\">Whether the feature flag is enabled and discount should be considered.</param>\n    /// <param name=\"customerDiscount\">The customer discount from subscription info, if any.</param>\n    /// <returns>True if the discount should be included; false otherwise.</returns>\n    private static bool ShouldIncludeMilestone2Discount(\n        bool includeMilestone2Discount,\n        SubscriptionInfo.BillingCustomerDiscount? customerDiscount)\n    {\n        return includeMilestone2Discount &&\n               customerDiscount != null &&\n               customerDiscount.Id == StripeConstants.CouponIDs.Milestone2SubscriptionDiscount &&\n               customerDiscount.Active;\n    }\n}\n\n/// <summary>\n/// Customer discount information from Stripe billing.\n/// </summary>\npublic class BillingCustomerDiscount\n{\n    /// <summary>\n    /// The Stripe coupon ID (e.g., \"cm3nHfO1\").\n    /// </summary>\n    public string? Id { get; }\n\n    /// <summary>\n    /// Whether the discount is a recurring/perpetual discount with no expiration date.\n    /// <para>\n    /// This property is true only when the discount has no end date, meaning it applies\n    /// indefinitely to all future renewals. This is a product decision for Milestone 2\n    /// to only display perpetual discounts in the UI.\n    /// </para>\n    /// <para>\n    /// Note: This does NOT indicate whether the discount is \"currently active\" in the billing sense.\n    /// A discount with a future end date is functionally active and will be applied by Stripe,\n    /// but this property will be false because it has an expiration date.\n    /// </para>\n    /// </summary>\n    public bool Active { get; }\n\n    /// <summary>\n    /// Percentage discount applied to the subscription (e.g., 20.0 for 20% off).\n    /// Null if this is an amount-based discount.\n    /// </summary>\n    public decimal? PercentOff { get; }\n\n    /// <summary>\n    /// Fixed amount discount in USD (e.g., 14.00 for $14 off).\n    /// Converted from Stripe's cent-based values (1400 cents → $14.00).\n    /// Null if this is a percentage-based discount.\n    /// Note: Stripe stores amounts in the smallest currency unit. This value is always in USD.\n    /// </summary>\n    public decimal? AmountOff { get; }\n\n    /// <summary>\n    /// List of Stripe product IDs that this discount applies to (e.g., [\"prod_premium\", \"prod_families\"]).\n    /// <para>\n    /// Null: discount applies to all products with no restrictions (AppliesTo not specified in Stripe).\n    /// Empty list: discount restricted to zero products (edge case - AppliesTo.Products = [] in Stripe).\n    /// Non-empty list: discount applies only to the specified product IDs.\n    /// </para>\n    /// </summary>\n    public IReadOnlyList<string>? AppliesTo { get; }\n\n    /// <summary>\n    /// Creates a BillingCustomerDiscount from a SubscriptionInfo.BillingCustomerDiscount.\n    /// </summary>\n    /// <param name=\"discount\">The discount to convert. Must not be null.</param>\n    /// <exception cref=\"ArgumentNullException\">Thrown when discount is null.</exception>\n    public BillingCustomerDiscount(SubscriptionInfo.BillingCustomerDiscount discount)\n    {\n        ArgumentNullException.ThrowIfNull(discount);\n\n        Id = discount.Id;\n        Active = discount.Active;\n        PercentOff = discount.PercentOff;\n        AmountOff = discount.AmountOff;\n        AppliesTo = discount.AppliesTo;\n    }\n}\n\npublic class BillingSubscription\n{\n    public BillingSubscription(SubscriptionInfo.BillingSubscription sub)\n    {\n        Status = sub.Status;\n        TrialStartDate = sub.TrialStartDate;\n        TrialEndDate = sub.TrialEndDate;\n        PeriodStartDate = sub.PeriodStartDate;\n        PeriodEndDate = sub.PeriodEndDate;\n        CancelledDate = sub.CancelledDate;\n        CancelAtEndDate = sub.CancelAtEndDate;\n        Cancelled = sub.Cancelled;\n        if (sub.Items != null)\n        {\n            Items = sub.Items.Select(i => new BillingSubscriptionItem(i));\n        }\n        CollectionMethod = sub.CollectionMethod;\n        SuspensionDate = sub.SuspensionDate;\n        UnpaidPeriodEndDate = sub.UnpaidPeriodEndDate;\n        GracePeriod = sub.GracePeriod;\n    }\n\n    public DateTime? TrialStartDate { get; set; }\n    public DateTime? TrialEndDate { get; set; }\n    public DateTime? PeriodStartDate { get; set; }\n    public DateTime? PeriodEndDate { get; set; }\n    public DateTime? CancelledDate { get; set; }\n    public bool CancelAtEndDate { get; set; }\n    public string? Status { get; set; }\n    public bool Cancelled { get; set; }\n    public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();\n    public string? CollectionMethod { get; set; }\n    public DateTime? SuspensionDate { get; set; }\n    public DateTime? UnpaidPeriodEndDate { get; set; }\n    public int? GracePeriod { get; set; }\n\n    public class BillingSubscriptionItem\n    {\n        public BillingSubscriptionItem(SubscriptionInfo.BillingSubscription.BillingSubscriptionItem item)\n        {\n            ProductId = item.ProductId;\n            Name = item.Name;\n            Amount = item.Amount;\n            Interval = item.Interval;\n            Quantity = item.Quantity;\n            SponsoredSubscriptionItem = item.SponsoredSubscriptionItem;\n            AddonSubscriptionItem = item.AddonSubscriptionItem;\n        }\n\n        public string? ProductId { get; set; }\n        public string? Name { get; set; }\n        public decimal Amount { get; set; }\n        public int Quantity { get; set; }\n        public string? Interval { get; set; }\n        public bool SponsoredSubscriptionItem { get; set; }\n        public bool AddonSubscriptionItem { get; set; }\n    }\n}\n\npublic class BillingSubscriptionUpcomingInvoice\n{\n    public BillingSubscriptionUpcomingInvoice(SubscriptionInfo.BillingUpcomingInvoice inv)\n    {\n        Amount = inv.Amount;\n        Date = inv.Date;\n    }\n\n    public decimal? Amount { get; set; }\n    public DateTime? Date { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/TaxInfoResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Api.Models.Response;\n\npublic class TaxInfoResponseModel\n{\n    public TaxInfoResponseModel() { }\n\n    public TaxInfoResponseModel(TaxInfo taxInfo)\n    {\n        if (taxInfo == null)\n        {\n            return;\n        }\n\n        TaxIdNumber = taxInfo.TaxIdNumber;\n        TaxIdType = taxInfo.TaxIdType;\n        Line1 = taxInfo.BillingAddressLine1;\n        Line2 = taxInfo.BillingAddressLine2;\n        City = taxInfo.BillingAddressCity;\n        State = taxInfo.BillingAddressState;\n        PostalCode = taxInfo.BillingAddressPostalCode;\n        Country = taxInfo.BillingAddressCountry;\n    }\n\n    public string TaxIdNumber { get; set; }\n    public string TaxIdType { get; set; }\n    public string Line1 { get; set; }\n    public string Line2 { get; set; }\n    public string City { get; set; }\n    public string State { get; set; }\n    public string PostalCode { get; set; }\n    public string Country { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Models/Response/UserKeyResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\n\nnamespace Bit.Api.Models.Response;\n\npublic class UserKeyResponseModel : ResponseModel\n{\n    public UserKeyResponseModel(Guid id, string key)\n        : base(\"userKey\")\n    {\n        UserId = id;\n        PublicKey = key;\n    }\n\n    public Guid UserId { get; set; }\n    public string PublicKey { get; set; }\n}\n"
  },
  {
    "path": "src/Api/NotificationCenter/Controllers/NotificationsController.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.Models.Response;\nusing Bit.Api.NotificationCenter.Models.Request;\nusing Bit.Api.NotificationCenter.Models.Response;\nusing Bit.Core.Models.Data;\nusing Bit.Core.NotificationCenter.Commands.Interfaces;\nusing Bit.Core.NotificationCenter.Models.Filter;\nusing Bit.Core.NotificationCenter.Queries.Interfaces;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.NotificationCenter.Controllers;\n\n[Route(\"notifications\")]\n[Authorize(\"Application\")]\npublic class NotificationsController : Controller\n{\n    private readonly IGetNotificationStatusDetailsForUserQuery _getNotificationStatusDetailsForUserQuery;\n    private readonly IMarkNotificationDeletedCommand _markNotificationDeletedCommand;\n    private readonly IMarkNotificationReadCommand _markNotificationReadCommand;\n\n    public NotificationsController(\n        IGetNotificationStatusDetailsForUserQuery getNotificationStatusDetailsForUserQuery,\n        IMarkNotificationDeletedCommand markNotificationDeletedCommand,\n        IMarkNotificationReadCommand markNotificationReadCommand)\n    {\n        _getNotificationStatusDetailsForUserQuery = getNotificationStatusDetailsForUserQuery;\n        _markNotificationDeletedCommand = markNotificationDeletedCommand;\n        _markNotificationReadCommand = markNotificationReadCommand;\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<NotificationResponseModel>> ListAsync(\n        [FromQuery] NotificationFilterRequestModel filter)\n    {\n        var pageOptions = new PageOptions\n        {\n            ContinuationToken = filter.ContinuationToken,\n            PageSize = filter.PageSize\n        };\n\n        var notificationStatusFilter = new NotificationStatusFilter\n        {\n            Read = filter.ReadStatusFilter,\n            Deleted = filter.DeletedStatusFilter\n        };\n\n        var notificationStatusDetailsPagedResult =\n            await _getNotificationStatusDetailsForUserQuery.GetByUserIdStatusFilterAsync(notificationStatusFilter,\n                pageOptions);\n\n        var responses = notificationStatusDetailsPagedResult.Data\n            .Select(n => new NotificationResponseModel(n))\n            .ToList();\n\n        return new ListResponseModel<NotificationResponseModel>(responses,\n            notificationStatusDetailsPagedResult.ContinuationToken);\n    }\n\n    [HttpPatch(\"{id}/delete\")]\n    public async Task MarkAsDeletedAsync([FromRoute] Guid id)\n    {\n        await _markNotificationDeletedCommand.MarkDeletedAsync(id);\n    }\n\n    [HttpPatch(\"{id}/read\")]\n    public async Task MarkAsReadAsync([FromRoute] Guid id)\n    {\n        await _markNotificationReadCommand.MarkReadAsync(id);\n    }\n}\n"
  },
  {
    "path": "src/Api/NotificationCenter/Models/Request/NotificationFilterRequestModel.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.NotificationCenter.Models.Request;\n\npublic class NotificationFilterRequestModel : IValidatableObject\n{\n    /// <summary>\n    /// Filters notifications by read status. When not set, includes notifications without a status.\n    /// </summary>\n    public bool? ReadStatusFilter { get; set; }\n\n    /// <summary>\n    /// Filters notifications by deleted status. When not set, includes notifications without a status.\n    /// </summary>\n    public bool? DeletedStatusFilter { get; set; }\n\n    /// <summary>\n    /// A cursor for use in pagination.\n    /// </summary>\n    [StringLength(9)]\n    public string? ContinuationToken { get; set; }\n\n    /// <summary>\n    /// The number of items to return in a single page.\n    /// Default 10. Minimum 10, maximum 1000.\n    /// </summary>\n    [Range(10, 1000)]\n    public int PageSize { get; set; } = 10;\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (!string.IsNullOrWhiteSpace(ContinuationToken) &&\n            (!int.TryParse(ContinuationToken, out var pageNumber) || pageNumber <= 0))\n        {\n            yield return new ValidationResult(\n                \"Continuation token must be a positive, non zero integer.\",\n                [nameof(ContinuationToken)]);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/NotificationCenter/Models/Response/NotificationResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\nusing Bit.Core.NotificationCenter.Enums;\nusing Bit.Core.NotificationCenter.Models.Data;\n\nnamespace Bit.Api.NotificationCenter.Models.Response;\n\npublic class NotificationResponseModel : ResponseModel\n{\n    private const string _objectName = \"notification\";\n\n    public NotificationResponseModel(NotificationStatusDetails notificationStatusDetails, string obj = _objectName)\n        : base(obj)\n    {\n        if (notificationStatusDetails == null)\n        {\n            throw new ArgumentNullException(nameof(notificationStatusDetails));\n        }\n\n        Id = notificationStatusDetails.Id;\n        Priority = notificationStatusDetails.Priority;\n        Title = notificationStatusDetails.Title;\n        Body = notificationStatusDetails.Body;\n        Date = notificationStatusDetails.RevisionDate;\n        TaskId = notificationStatusDetails.TaskId;\n        ReadDate = notificationStatusDetails.ReadDate;\n        DeletedDate = notificationStatusDetails.DeletedDate;\n    }\n\n    public NotificationResponseModel() : base(_objectName)\n    {\n    }\n\n    public Guid Id { get; set; }\n\n    public Priority Priority { get; set; }\n\n    public string? Title { get; set; }\n\n    public string? Body { get; set; }\n\n    public DateTime Date { get; set; }\n\n    public Guid? TaskId { get; set; }\n\n    public DateTime? ReadDate { get; set; }\n\n    public DateTime? DeletedDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Platform/Installations/Controllers/InstallationsController.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.Platform.Installations;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Platform.Installations;\n\n/// <summary>\n/// Routes used to manipulate `Installation` objects: a type used to manage\n/// a record of a self hosted installation.\n/// </summary>\n/// <remarks>\n/// This controller is not called from any clients. It's primarily referenced\n/// in the `Setup` project for creating a new self hosted installation.\n/// </remarks>\n/// <seealso>Bit.Setup.Program</seealso>\n[Route(\"installations\")]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class InstallationsController : Controller\n{\n    private readonly IInstallationRepository _installationRepository;\n\n    public InstallationsController(\n        IInstallationRepository installationRepository)\n    {\n        _installationRepository = installationRepository;\n    }\n\n    [HttpGet(\"{id}\")]\n    [AllowAnonymous]\n    public async Task<InstallationResponseModel> Get(Guid id)\n    {\n        var installation = await _installationRepository.GetByIdAsync(id);\n        if (installation == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new InstallationResponseModel(installation, false);\n    }\n\n    [HttpPost(\"\")]\n    [AllowAnonymous]\n    public async Task<InstallationResponseModel> Post([FromBody] InstallationRequestModel model)\n    {\n        var installation = model.ToInstallation();\n        await _installationRepository.CreateAsync(installation);\n        return new InstallationResponseModel(installation, true);\n    }\n}\n"
  },
  {
    "path": "src/Api/Platform/Installations/Models/InstallationRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Platform.Installations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Platform.Installations;\n\npublic class InstallationRequestModel\n{\n    [Required]\n    [EmailAddress]\n    [StringLength(256)]\n    public string Email { get; set; }\n\n    public Installation ToInstallation()\n    {\n        return new Installation\n        {\n            Key = CoreHelpers.SecureRandomString(20),\n            Email = Email,\n            Enabled = true\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/Platform/Installations/Models/InstallationResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.Platform.Installations;\n\nnamespace Bit.Api.Platform.Installations;\n\npublic class InstallationResponseModel : ResponseModel\n{\n    public InstallationResponseModel(Installation installation, bool withKey)\n        : base(\"installation\")\n    {\n        Id = installation.Id;\n        Key = withKey ? installation.Key : null;\n        Enabled = installation.Enabled;\n    }\n\n    public Guid Id { get; set; }\n    public string Key { get; set; }\n    public bool Enabled { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Platform/Push/Controllers/PushController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Diagnostics;\nusing System.Text.Json;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Platform.PushRegistration;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Platform.Push;\n\n/// <summary>\n/// Routes for push relay: functionality that facilitates communication\n/// between self hosted organizations and Bitwarden cloud.\n/// </summary>\n[Route(\"push\")]\n[Authorize(\"Push\")]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class PushController : Controller\n{\n    private readonly IPushRegistrationService _pushRegistrationService;\n    private readonly IPushRelayer _pushRelayer;\n    private readonly IWebHostEnvironment _environment;\n    private readonly ICurrentContext _currentContext;\n    private readonly IGlobalSettings _globalSettings;\n\n    public PushController(\n        IPushRegistrationService pushRegistrationService,\n        IPushRelayer pushRelayer,\n        IWebHostEnvironment environment,\n        ICurrentContext currentContext,\n        IGlobalSettings globalSettings)\n    {\n        _currentContext = currentContext;\n        _environment = environment;\n        _pushRegistrationService = pushRegistrationService;\n        _pushRelayer = pushRelayer;\n        _globalSettings = globalSettings;\n    }\n\n    [HttpPost(\"register\")]\n    public async Task RegisterAsync([FromBody] PushRegistrationRequestModel model)\n    {\n        CheckUsage();\n        await _pushRegistrationService.CreateOrUpdateRegistrationAsync(new PushRegistrationData(model.PushToken),\n            Prefix(model.DeviceId), Prefix(model.UserId), Prefix(model.Identifier), model.Type,\n            model.OrganizationIds?.Select(Prefix) ?? [], model.InstallationId);\n    }\n\n    [HttpPost(\"delete\")]\n    public async Task DeleteAsync([FromBody] PushDeviceRequestModel model)\n    {\n        CheckUsage();\n        await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id));\n    }\n\n    [HttpPut(\"add-organization\")]\n    public async Task AddOrganizationAsync([FromBody] PushUpdateRequestModel model)\n    {\n        CheckUsage();\n        await _pushRegistrationService.AddUserRegistrationOrganizationAsync(\n            model.Devices.Select(d => Prefix(d.Id)),\n            Prefix(model.OrganizationId));\n    }\n\n    [HttpPut(\"delete-organization\")]\n    public async Task DeleteOrganizationAsync([FromBody] PushUpdateRequestModel model)\n    {\n        CheckUsage();\n        await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(\n            model.Devices.Select(d => Prefix(d.Id)),\n            Prefix(model.OrganizationId));\n    }\n\n    [HttpPost(\"send\")]\n    public async Task SendAsync([FromBody] PushSendRequestModel<JsonElement> model)\n    {\n        CheckUsage();\n\n        NotificationTarget target;\n        Guid targetId;\n\n        if (model.InstallationId.HasValue)\n        {\n            if (_currentContext.InstallationId!.Value != model.InstallationId.Value)\n            {\n                throw new BadRequestException(\"InstallationId does not match current context.\");\n            }\n\n            target = NotificationTarget.Installation;\n            targetId = _currentContext.InstallationId.Value;\n        }\n        else if (model.UserId.HasValue)\n        {\n            target = NotificationTarget.User;\n            targetId = model.UserId.Value;\n        }\n        else if (model.OrganizationId.HasValue)\n        {\n            target = NotificationTarget.Organization;\n            targetId = model.OrganizationId.Value;\n        }\n        else\n        {\n            throw new UnreachableException(\"Model validation should have prevented getting here.\");\n        }\n\n        var notification = new RelayedNotification\n        {\n            Type = model.Type,\n            Target = target,\n            TargetId = targetId,\n            Payload = model.Payload,\n            Identifier = model.Identifier,\n            DeviceId = model.DeviceId,\n            ClientType = model.ClientType,\n        };\n\n        await _pushRelayer.RelayAsync(_currentContext.InstallationId.Value, notification);\n    }\n\n    private string Prefix(string value)\n    {\n        if (string.IsNullOrWhiteSpace(value))\n        {\n            return null;\n        }\n\n        return $\"{_currentContext.InstallationId!.Value}_{value}\";\n    }\n\n    private void CheckUsage()\n    {\n        if (CanUse())\n        {\n            return;\n        }\n\n        throw new BadRequestException(\"Not correctly configured for push relays.\");\n    }\n\n    private bool CanUse()\n    {\n        if (_environment.IsDevelopment())\n        {\n            return true;\n        }\n\n        return _currentContext.InstallationId.HasValue && !_globalSettings.SelfHosted;\n    }\n}\n"
  },
  {
    "path": "src/Api/Platform/Push/PushTechnologyType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Enums;\n\npublic enum PushTechnologyType\n{\n    [Display(Name = \"SignalR\")]\n    SignalR = 0,\n    [Display(Name = \"WebPush\")]\n    WebPush = 1,\n}\n"
  },
  {
    "path": "src/Api/Platform/SsoCookieVendor/Controllers/SsoCookieVendorController.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Platform.SsoCookieVendor;\n\n/// <summary>\n/// Provides an endpoint to read an SSO cookie and redirect to a custom URI\n/// scheme. The load balancer/reverse proxy must be configured such that\n/// requests to this endpoint do not have the auth cookie stripped.\n/// </summary>\n[Route(\"sso-cookie-vendor\")]\n[SelfHosted(SelfHostedOnly = true)]\npublic class SsoCookieVendorController(IGlobalSettings globalSettings) : Controller\n{\n    private readonly IGlobalSettings _globalSettings = globalSettings;\n    private const int _maxShardCount = 20;\n    private const int _maxUriLength = 8192;\n\n    /// <summary>\n    /// Reads SSO cookie (shards supported) and redirects to the bitwarden://\n    /// URI with cookie value(s).\n    /// </summary>\n    /// <returns>\n    /// 302 redirect on success, 404 if no cookies found, 400 if URI too long,\n    /// 500 if misconfigured\n    /// </returns>\n    [HttpGet]\n    [AllowAnonymous]\n    public IActionResult Get()\n    {\n        var bootstrap = _globalSettings.Communication?.Bootstrap;\n        if (string.IsNullOrEmpty(bootstrap) || !bootstrap.Equals(\"ssoCookieVendor\", StringComparison.OrdinalIgnoreCase))\n        {\n            return NotFound();\n        }\n\n        var cookieName = _globalSettings.Communication?.SsoCookieVendor?.CookieName;\n        if (string.IsNullOrWhiteSpace(cookieName))\n        {\n            return StatusCode(500, \"SSO cookie vendor is not properly configured\");\n        }\n\n        var uri = string.Empty;\n        if (TryGetCookie(cookieName, out var cookie))\n        {\n            uri = BuildRedirectUri(cookie);\n        }\n        else if (TryGetShardedCookie(cookieName, out var shardedCookie))\n        {\n            uri = BuildRedirectUri(shardedCookie);\n        }\n\n        if (uri == string.Empty)\n        {\n            return NotFound(\"No SSO cookies found\");\n        }\n\n        if (uri.Length > _maxUriLength)\n        {\n            return BadRequest();\n        }\n\n        return Redirect(uri);\n    }\n\n    private bool TryGetCookie(string cookieName, out Dictionary<string, string> cookie)\n    {\n        cookie = [];\n\n        if (Request.Cookies.TryGetValue(cookieName, out var value) && !string.IsNullOrEmpty(value))\n        {\n            cookie[cookieName] = value;\n            return true;\n        }\n\n        return false;\n    }\n\n    private bool TryGetShardedCookie(string cookieName, out Dictionary<string, string> cookies)\n    {\n        var shardedCookies = new Dictionary<string, string>();\n\n        for (var i = 0; i < _maxShardCount; i++)\n        {\n            var shardName = $\"{cookieName}-{i}\";\n            if (Request.Cookies.TryGetValue(shardName, out var value) && !string.IsNullOrEmpty(value))\n            {\n                shardedCookies[shardName] = value;\n            }\n            else\n            {\n                // Stop at first missing shard to maintain order integrity\n                break;\n            }\n        }\n\n        cookies = shardedCookies;\n        return shardedCookies.Count > 0;\n    }\n\n    private static string BuildRedirectUri(Dictionary<string, string> cookies)\n    {\n        var queryParams = new List<string>();\n\n        foreach (var kvp in cookies)\n        {\n            var encodedValue = Uri.EscapeDataString(kvp.Value);\n            queryParams.Add($\"{kvp.Key}={encodedValue}\");\n        }\n\n        // Add a sentinel value so clients can detect a truncated URI, in the\n        // event a user agent decides the URI is too long.\n        queryParams.Add(\"d=1\");\n\n        return $\"bitwarden://sso-cookie-vendor?{string.Join(\"&\", queryParams)}\";\n    }\n}\n"
  },
  {
    "path": "src/Api/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Api;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        Host\n            .CreateDefaultBuilder(args)\n            .UseBitwardenSdk()\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging()\n            .Build()\n            .Run();\n    }\n}\n"
  },
  {
    "path": "src/Api/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:4000\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Api\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:4000\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Api-SelfHost\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:4001\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"developSelfHosted\": \"true\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Api/Public/Controllers/CollectionsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing Bit.Api.Models.Public.Request;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\nusing Bit.Core.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Public.Controllers;\n\n[Route(\"public/collections\")]\n[Authorize(\"Organization\")]\npublic class CollectionsController : Controller\n{\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IUpdateCollectionCommand _updateCollectionCommand;\n    private readonly ICurrentContext _currentContext;\n\n    public CollectionsController(\n        ICollectionRepository collectionRepository,\n        IUpdateCollectionCommand updateCollectionCommand,\n        ICurrentContext currentContext)\n    {\n        _collectionRepository = collectionRepository;\n        _updateCollectionCommand = updateCollectionCommand;\n        _currentContext = currentContext;\n    }\n\n    /// <summary>\n    /// Retrieve a collection.\n    /// </summary>\n    /// <remarks>\n    /// Retrieves the details of an existing collection. You need only supply the unique collection identifier\n    /// that was returned upon collection creation.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the collection to be retrieved.</param>\n    [HttpGet(\"{id}\")]\n    [ProducesResponseType(typeof(CollectionResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Get(Guid id)\n    {\n        (var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id);\n        if (collection == null || collection.OrganizationId != _currentContext.OrganizationId ||\n            collection.Type == CollectionType.DefaultUserCollection)\n        {\n            return new NotFoundResult();\n        }\n        var response = new CollectionResponseModel(collection, access.Groups);\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// List all collections.\n    /// </summary>\n    /// <remarks>\n    /// Returns a list of your organization's collections.\n    /// Collection objects listed in this call do not include information about their associated groups.\n    /// </remarks>\n    [HttpGet]\n    [ProducesResponseType(typeof(ListResponseModel<CollectionResponseModel>), (int)HttpStatusCode.OK)]\n    public async Task<IActionResult> List()\n    {\n        var collections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(_currentContext.OrganizationId.Value);\n\n        var collectionResponses = collections\n            .Where(c => c.Item1.Type != CollectionType.DefaultUserCollection)\n            .Select(c => new CollectionResponseModel(c.Item1, c.Item2.Groups));\n\n        var response = new ListResponseModel<CollectionResponseModel>(collectionResponses);\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// Update a collection.\n    /// </summary>\n    /// <remarks>\n    /// Updates the specified collection object. If a property is not provided,\n    /// the value of the existing property will be reset.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the collection to be updated.</param>\n    /// <param name=\"model\">The request model.</param>\n    [HttpPut(\"{id}\")]\n    [ProducesResponseType(typeof(CollectionResponseModel), (int)HttpStatusCode.OK)]\n    [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Put(Guid id, [FromBody] CollectionUpdateRequestModel model)\n    {\n        var existingCollection = await _collectionRepository.GetByIdAsync(id);\n        if (existingCollection == null || existingCollection.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n        var updatedCollection = model.ToCollection(existingCollection);\n        var associations = model.Groups?.Select(c => c.ToCollectionAccessSelection()).ToList();\n        await _updateCollectionCommand.UpdateAsync(updatedCollection, associations, null);\n        var response = new CollectionResponseModel(updatedCollection, associations);\n        return new JsonResult(response);\n    }\n\n    /// <summary>\n    /// Delete a collection.\n    /// </summary>\n    /// <remarks>\n    /// Permanently deletes a collection. This cannot be undone.\n    /// </remarks>\n    /// <param name=\"id\">The identifier of the collection to be deleted.</param>\n    [HttpDelete(\"{id}\")]\n    [ProducesResponseType((int)HttpStatusCode.OK)]\n    [ProducesResponseType((int)HttpStatusCode.NotFound)]\n    public async Task<IActionResult> Delete(Guid id)\n    {\n        var collection = await _collectionRepository.GetByIdAsync(id);\n        if (collection == null || collection.OrganizationId != _currentContext.OrganizationId)\n        {\n            return new NotFoundResult();\n        }\n\n        if (collection.Type == CollectionType.DefaultUserCollection)\n        {\n            return new BadRequestObjectResult(new ErrorResponseModel(\"You cannot delete a collection with the type as DefaultUserCollection.\"));\n        }\n\n        await _collectionRepository.DeleteAsync(collection);\n        return new OkResult();\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/AccessPoliciesController.cs",
    "content": "﻿using Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Authorize(\"secrets\")]\npublic class AccessPoliciesController : Controller\n{\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly IProjectRepository _projectRepository;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IServiceAccountGrantedPolicyUpdatesQuery _serviceAccountGrantedPolicyUpdatesQuery;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly IUpdateServiceAccountGrantedPoliciesCommand _updateServiceAccountGrantedPoliciesCommand;\n    private readonly IUserService _userService;\n    private readonly IEventService _eventService;\n    private readonly IProjectServiceAccountsAccessPoliciesUpdatesQuery\n        _projectServiceAccountsAccessPoliciesUpdatesQuery;\n    private readonly IUpdateProjectServiceAccountsAccessPoliciesCommand\n        _updateProjectServiceAccountsAccessPoliciesCommand;\n\n\n    public AccessPoliciesController(\n        IAuthorizationService authorizationService,\n        IUserService userService,\n        ICurrentContext currentContext,\n        IAccessPolicyRepository accessPolicyRepository,\n        IServiceAccountRepository serviceAccountRepository,\n        IProjectRepository projectRepository,\n        ISecretRepository secretRepository,\n        IAccessClientQuery accessClientQuery,\n        IServiceAccountGrantedPolicyUpdatesQuery serviceAccountGrantedPolicyUpdatesQuery,\n        IProjectServiceAccountsAccessPoliciesUpdatesQuery projectServiceAccountsAccessPoliciesUpdatesQuery,\n        IUpdateServiceAccountGrantedPoliciesCommand updateServiceAccountGrantedPoliciesCommand,\n        IUpdateProjectServiceAccountsAccessPoliciesCommand updateProjectServiceAccountsAccessPoliciesCommand,\n        IEventService eventService)\n    {\n        _authorizationService = authorizationService;\n        _userService = userService;\n        _currentContext = currentContext;\n        _serviceAccountRepository = serviceAccountRepository;\n        _projectRepository = projectRepository;\n        _secretRepository = secretRepository;\n        _accessPolicyRepository = accessPolicyRepository;\n        _updateServiceAccountGrantedPoliciesCommand = updateServiceAccountGrantedPoliciesCommand;\n        _accessClientQuery = accessClientQuery;\n        _serviceAccountGrantedPolicyUpdatesQuery = serviceAccountGrantedPolicyUpdatesQuery;\n        _projectServiceAccountsAccessPoliciesUpdatesQuery = projectServiceAccountsAccessPoliciesUpdatesQuery;\n        _updateProjectServiceAccountsAccessPoliciesCommand = updateProjectServiceAccountsAccessPoliciesCommand;\n        _eventService = eventService;\n    }\n\n    [HttpGet(\"/organizations/{id}/access-policies/people/potential-grantees\")]\n    public async Task<ListResponseModel<PotentialGranteeResponseModel>> GetPeoplePotentialGranteesAsync(\n        [FromRoute] Guid id)\n    {\n        if (!_currentContext.AccessSecretsManager(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User)!.Value;\n        var peopleGrantees = await _accessPolicyRepository.GetPeopleGranteesAsync(id, userId);\n\n        var userResponses = peopleGrantees.UserGrantees.Select(ug => new PotentialGranteeResponseModel(ug));\n        var groupResponses = peopleGrantees.GroupGrantees.Select(g => new PotentialGranteeResponseModel(g));\n        return new ListResponseModel<PotentialGranteeResponseModel>(userResponses.Concat(groupResponses));\n    }\n\n    [HttpGet(\"/organizations/{id}/access-policies/service-accounts/potential-grantees\")]\n    public async Task<ListResponseModel<PotentialGranteeResponseModel>> GetServiceAccountsPotentialGranteesAsync(\n        [FromRoute] Guid id)\n    {\n        if (!_currentContext.AccessSecretsManager(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(User, id);\n\n        var serviceAccounts =\n            await _serviceAccountRepository.GetManyByOrganizationIdWriteAccessAsync(id,\n                userId,\n                accessClient);\n        var serviceAccountResponses =\n            serviceAccounts.Select(serviceAccount => new PotentialGranteeResponseModel(serviceAccount));\n\n        return new ListResponseModel<PotentialGranteeResponseModel>(serviceAccountResponses);\n    }\n\n    [HttpGet(\"/organizations/{id}/access-policies/projects/potential-grantees\")]\n    public async Task<ListResponseModel<PotentialGranteeResponseModel>> GetProjectPotentialGranteesAsync(\n        [FromRoute] Guid id)\n    {\n        if (!_currentContext.AccessSecretsManager(id))\n        {\n            throw new NotFoundException();\n        }\n\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(User, id);\n\n        var projects =\n            await _projectRepository.GetManyByOrganizationIdWriteAccessAsync(id,\n                userId,\n                accessClient);\n        var projectResponses =\n            projects.Select(project => new PotentialGranteeResponseModel(project));\n\n        return new ListResponseModel<PotentialGranteeResponseModel>(projectResponses);\n    }\n\n    [HttpGet(\"/projects/{id}/access-policies/people\")]\n    public async Task<ProjectPeopleAccessPoliciesResponseModel> GetProjectPeopleAccessPoliciesAsync([FromRoute] Guid id)\n    {\n        var project = await _projectRepository.GetByIdAsync(id);\n        var (_, userId) = await CheckUserHasWriteAccessToProjectAsync(project);\n        var results = await _accessPolicyRepository.GetPeoplePoliciesByGrantedProjectIdAsync(id, userId);\n        return new ProjectPeopleAccessPoliciesResponseModel(results, userId);\n    }\n\n    [HttpPut(\"/projects/{id}/access-policies/people\")]\n    public async Task<ProjectPeopleAccessPoliciesResponseModel> PutProjectPeopleAccessPoliciesAsync([FromRoute] Guid id,\n        [FromBody] PeopleAccessPoliciesRequestModel request)\n    {\n        var project = await _projectRepository.GetByIdAsync(id);\n        if (project == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var peopleAccessPolicies = request.ToProjectPeopleAccessPolicies(id, project.OrganizationId);\n\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, peopleAccessPolicies,\n            ProjectPeopleAccessPoliciesOperations.Replace);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User)!.Value;\n        var results = await _accessPolicyRepository.ReplaceProjectPeopleAsync(peopleAccessPolicies, userId);\n        return new ProjectPeopleAccessPoliciesResponseModel(results, userId);\n    }\n\n    [HttpGet(\"/service-accounts/{id}/access-policies/people\")]\n    public async Task<ServiceAccountPeopleAccessPoliciesResponseModel> GetServiceAccountPeopleAccessPoliciesAsync(\n        [FromRoute] Guid id)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);\n        var (_, userId) = await CheckUserHasWriteAccessToServiceAccountAsync(serviceAccount);\n        var results = await _accessPolicyRepository.GetPeoplePoliciesByGrantedServiceAccountIdAsync(id, userId);\n        return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId);\n    }\n\n    [HttpPut(\"/service-accounts/{id}/access-policies/people\")]\n    public async Task<ServiceAccountPeopleAccessPoliciesResponseModel> PutServiceAccountPeopleAccessPoliciesAsync(\n        [FromRoute] Guid id,\n        [FromBody] PeopleAccessPoliciesRequestModel request)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);\n        if (serviceAccount == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var peopleAccessPolicies = request.ToServiceAccountPeopleAccessPolicies(id, serviceAccount.OrganizationId);\n\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, peopleAccessPolicies,\n            ServiceAccountPeopleAccessPoliciesOperations.Replace);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User)!.Value;\n        var currentPolicies = await _accessPolicyRepository.GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId);\n        var results = await _accessPolicyRepository.ReplaceServiceAccountPeopleAsync(peopleAccessPolicies, userId);\n        await LogAccessPolicyServiceAccountChanges(currentPolicies, results, userId);\n        return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId);\n    }\n\n    [HttpGet(\"/service-accounts/{id}/granted-policies\")]\n    public async Task<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>\n        GetServiceAccountGrantedPoliciesAsync([FromRoute] Guid id)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Update);\n\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        return await GetServiceAccountGrantedPoliciesAsync(serviceAccount);\n    }\n\n\n    [HttpPut(\"/service-accounts/{id}/granted-policies\")]\n    public async Task<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>\n        PutServiceAccountGrantedPoliciesAsync([FromRoute] Guid id,\n            [FromBody] ServiceAccountGrantedPoliciesRequestModel request)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id) ?? throw new NotFoundException();\n        var grantedPoliciesUpdates =\n            await _serviceAccountGrantedPolicyUpdatesQuery.GetAsync(request.ToGrantedPolicies(serviceAccount));\n\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, grantedPoliciesUpdates,\n            ServiceAccountGrantedPoliciesOperations.Updates);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        await _updateServiceAccountGrantedPoliciesCommand.UpdateAsync(grantedPoliciesUpdates);\n        return await GetServiceAccountGrantedPoliciesAsync(serviceAccount);\n    }\n\n    [HttpGet(\"/projects/{id}/access-policies/service-accounts\")]\n    public async Task<ProjectServiceAccountsAccessPoliciesResponseModel>\n        GetProjectServiceAccountsAccessPoliciesAsync(\n            [FromRoute] Guid id)\n    {\n        var project = await _projectRepository.GetByIdAsync(id);\n        await CheckUserHasWriteAccessToProjectAsync(project);\n        var results =\n            await _accessPolicyRepository.GetProjectServiceAccountsAccessPoliciesAsync(id);\n        return new ProjectServiceAccountsAccessPoliciesResponseModel(results);\n    }\n\n    [HttpPut(\"/projects/{id}/access-policies/service-accounts\")]\n    public async Task<ProjectServiceAccountsAccessPoliciesResponseModel>\n        PutProjectServiceAccountsAccessPoliciesAsync([FromRoute] Guid id,\n            [FromBody] ProjectServiceAccountsAccessPoliciesRequestModel request)\n    {\n        var project = await _projectRepository.GetByIdAsync(id) ?? throw new NotFoundException();\n        var accessPoliciesUpdates =\n            await _projectServiceAccountsAccessPoliciesUpdatesQuery.GetAsync(\n                request.ToProjectServiceAccountsAccessPolicies(project));\n\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, accessPoliciesUpdates,\n            ProjectServiceAccountsAccessPoliciesOperations.Updates);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        await _updateProjectServiceAccountsAccessPoliciesCommand.UpdateAsync(accessPoliciesUpdates);\n\n        var results = await _accessPolicyRepository.GetProjectServiceAccountsAccessPoliciesAsync(id);\n        return new ProjectServiceAccountsAccessPoliciesResponseModel(results);\n    }\n\n    [HttpGet(\"/secrets/{secretId}/access-policies\")]\n    public async Task<SecretAccessPoliciesResponseModel> GetSecretAccessPoliciesAsync(Guid secretId)\n    {\n        var secret = await _secretRepository.GetByIdAsync(secretId);\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.ReadAccessPolicies);\n\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User)!.Value;\n        var accessPolicies = await _accessPolicyRepository.GetSecretAccessPoliciesAsync(secretId, userId);\n        return new SecretAccessPoliciesResponseModel(accessPolicies, userId);\n    }\n\n    private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(\n        Project project)\n    {\n        if (project == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!_currentContext.AccessSecretsManager(project.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(User, project.OrganizationId);\n\n        var access = await _projectRepository.AccessToProjectAsync(project.Id, userId, accessClient);\n        if (!access.Write || accessClient == AccessClientType.ServiceAccount)\n        {\n            throw new NotFoundException();\n        }\n\n        return (accessClient, userId);\n    }\n\n    private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToServiceAccountAsync(\n        ServiceAccount serviceAccount)\n    {\n        if (serviceAccount == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(User, serviceAccount.OrganizationId);\n\n        var access =\n            await _serviceAccountRepository.AccessToServiceAccountAsync(serviceAccount.Id, userId, accessClient);\n        if (!access.Write)\n        {\n            throw new NotFoundException();\n        }\n\n        return (accessClient, userId);\n    }\n\n    private async Task<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>\n        GetServiceAccountGrantedPoliciesAsync(ServiceAccount serviceAccount)\n    {\n        var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(User, serviceAccount.OrganizationId);\n        var results =\n            await _accessPolicyRepository.GetServiceAccountGrantedPoliciesPermissionDetailsAsync(serviceAccount.Id,\n                userId, accessClient);\n        return new ServiceAccountGrantedPoliciesPermissionDetailsResponseModel(results);\n    }\n\n    public async Task LogAccessPolicyServiceAccountChanges(IEnumerable<BaseAccessPolicy> currentPolicies, IEnumerable<BaseAccessPolicy> updatedPolicies, Guid userId)\n    {\n        foreach (var current in currentPolicies.OfType<GroupServiceAccountAccessPolicy>())\n        {\n            if (!updatedPolicies.Any(r => r.Id == current.Id))\n            {\n                await _eventService.LogServiceAccountGroupEventAsync(userId, current, EventType.ServiceAccount_GroupRemoved, _currentContext.IdentityClientType);\n            }\n        }\n\n        foreach (var policy in updatedPolicies.OfType<GroupServiceAccountAccessPolicy>())\n        {\n            if (!currentPolicies.Any(e => e.Id == policy.Id))\n            {\n                await _eventService.LogServiceAccountGroupEventAsync(userId, policy, EventType.ServiceAccount_GroupAdded, _currentContext.IdentityClientType);\n            }\n        }\n\n        foreach (var current in currentPolicies.OfType<UserServiceAccountAccessPolicy>())\n        {\n            if (!updatedPolicies.Any(r => r.Id == current.Id))\n            {\n                await _eventService.LogServiceAccountPeopleEventAsync(userId, current, EventType.ServiceAccount_UserRemoved, _currentContext.IdentityClientType);\n            }\n        }\n\n        foreach (var policy in updatedPolicies.OfType<UserServiceAccountAccessPolicy>())\n        {\n            if (!currentPolicies.Any(e => e.Id == policy.Id))\n            {\n                await _eventService.LogServiceAccountPeopleEventAsync(userId, policy, EventType.ServiceAccount_UserAdded, _currentContext.IdentityClientType);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/CountsController.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Authorize(\"secrets\")]\npublic class CountsController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly IProjectRepository _projectRepository;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public CountsController(\n        ICurrentContext currentContext,\n        IAccessClientQuery accessClientQuery,\n        IProjectRepository projectRepository,\n        ISecretRepository secretRepository,\n        IServiceAccountRepository serviceAccountRepository)\n    {\n        _currentContext = currentContext;\n        _accessClientQuery = accessClientQuery;\n        _projectRepository = projectRepository;\n        _secretRepository = secretRepository;\n        _serviceAccountRepository = serviceAccountRepository;\n    }\n\n    [HttpGet(\"organizations/{organizationId}/sm-counts\")]\n    public async Task<OrganizationCountsResponseModel> GetByOrganizationAsync([FromRoute] Guid organizationId)\n    {\n        var (accessType, userId) = await GetAccessClientAsync(organizationId);\n\n        var projectsCountTask = _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId,\n            userId, accessType);\n\n        var secretsCountTask = _secretRepository.GetSecretsCountByOrganizationIdAsync(organizationId,\n            userId, accessType);\n\n        var serviceAccountsCountsTask = _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(\n            organizationId, userId, accessType);\n\n        var counts = await Task.WhenAll(projectsCountTask, secretsCountTask, serviceAccountsCountsTask);\n\n        return new OrganizationCountsResponseModel\n        {\n            Projects = counts[0],\n            Secrets = counts[1],\n            ServiceAccounts = counts[2]\n        };\n    }\n\n\n    [HttpGet(\"projects/{projectId}/sm-counts\")]\n    public async Task<ProjectCountsResponseModel> GetByProjectAsync([FromRoute] Guid projectId)\n    {\n        var project = await _projectRepository.GetByIdAsync(projectId);\n        if (project == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var (accessType, userId) = await GetAccessClientAsync(project.OrganizationId);\n\n        var projectsCounts = await _projectRepository.GetProjectCountsByIdAsync(projectId, userId, accessType);\n\n        return new ProjectCountsResponseModel\n        {\n            Secrets = projectsCounts.Secrets,\n            People = projectsCounts.People,\n            ServiceAccounts = projectsCounts.ServiceAccounts\n        };\n    }\n\n    [HttpGet(\"service-accounts/{serviceAccountId}/sm-counts\")]\n    public async Task<ServiceAccountCountsResponseModel> GetByServiceAccountAsync([FromRoute] Guid serviceAccountId)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);\n        if (serviceAccount == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var (accessType, userId) = await GetAccessClientAsync(serviceAccount.OrganizationId);\n\n        var serviceAccountCounts =\n            await _serviceAccountRepository.GetServiceAccountCountsByIdAsync(serviceAccountId, userId, accessType);\n\n        return new ServiceAccountCountsResponseModel\n        {\n            Projects = serviceAccountCounts.Projects,\n            People = serviceAccountCounts.People,\n            AccessTokens = serviceAccountCounts.AccessTokens\n        };\n    }\n\n    private async Task<(AccessClientType, Guid)> GetAccessClientAsync(Guid organizationId)\n    {\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var (accessType, userId) = await _accessClientQuery.GetAccessClientAsync(User, organizationId);\n        if (accessType == AccessClientType.ServiceAccount)\n        {\n            throw new NotFoundException();\n        }\n\n        return (accessType, userId);\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/ProjectsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Commands.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Authorize(\"secrets\")]\npublic class ProjectsController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IUserService _userService;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IMaxProjectsQuery _maxProjectsQuery;\n    private readonly ICreateProjectCommand _createProjectCommand;\n    private readonly IUpdateProjectCommand _updateProjectCommand;\n    private readonly IDeleteProjectCommand _deleteProjectCommand;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly IEventService _eventService;\n\n    public ProjectsController(\n        ICurrentContext currentContext,\n        IUserService userService,\n        IProjectRepository projectRepository,\n        IMaxProjectsQuery maxProjectsQuery,\n        ICreateProjectCommand createProjectCommand,\n        IUpdateProjectCommand updateProjectCommand,\n        IDeleteProjectCommand deleteProjectCommand,\n        IAuthorizationService authorizationService,\n        IEventService eventService)\n    {\n        _currentContext = currentContext;\n        _userService = userService;\n        _projectRepository = projectRepository;\n        _maxProjectsQuery = maxProjectsQuery;\n        _createProjectCommand = createProjectCommand;\n        _updateProjectCommand = updateProjectCommand;\n        _deleteProjectCommand = deleteProjectCommand;\n        _authorizationService = authorizationService;\n        _eventService = eventService;\n    }\n\n    [HttpGet(\"organizations/{organizationId}/projects\")]\n    public async Task<ListResponseModel<ProjectResponseModel>> ListByOrganizationAsync([FromRoute] Guid organizationId)\n    {\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);\n\n        var responses = projects.Select(project => new ProjectResponseModel(project));\n        return new ListResponseModel<ProjectResponseModel>(responses);\n    }\n\n    [HttpPost(\"organizations/{organizationId}/projects\")]\n    public async Task<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId,\n        [FromBody] ProjectCreateRequestModel createRequest)\n    {\n        var project = createRequest.ToProject(organizationId);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, project, ProjectOperations.Create);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var (max, overMax) = await _maxProjectsQuery.GetByOrgIdAsync(organizationId, 1);\n        if (overMax != null && overMax.Value)\n        {\n            throw new BadRequestException($\"You have reached the maximum number of projects ({max}) for this plan.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var result = await _createProjectCommand.CreateAsync(project, userId, _currentContext.IdentityClientType);\n\n        if (result != null)\n        {\n            await LogProjectEventAsync(project, EventType.Project_Created);\n        }\n\n        // Creating a project means you have read & write permission.\n        return new ProjectResponseModel(result, true, true);\n    }\n\n    [HttpPut(\"projects/{id}\")]\n    public async Task<ProjectResponseModel> UpdateAsync([FromRoute] Guid id,\n        [FromBody] ProjectUpdateRequestModel updateRequest)\n    {\n        var project = await _projectRepository.GetByIdAsync(id);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, project, ProjectOperations.Update);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id));\n        if (result != null)\n        {\n            await LogProjectEventAsync(project, EventType.Project_Edited);\n        }\n\n        // Updating a project means you have read & write permission.\n        return new ProjectResponseModel(result, true, true);\n    }\n\n    [HttpGet(\"projects/{id}\")]\n    public async Task<ProjectResponseModel> GetAsync([FromRoute] Guid id)\n    {\n        var project = await _projectRepository.GetByIdAsync(id);\n        if (project == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!_currentContext.AccessSecretsManager(project.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        var access = await _projectRepository.AccessToProjectAsync(id, userId, accessClient);\n\n        if (!access.Read)\n        {\n            throw new NotFoundException();\n        }\n\n        await LogProjectEventAsync(project, EventType.Project_Retrieved);\n\n        return new ProjectResponseModel(project, access.Read, access.Write);\n    }\n\n    [HttpPost(\"projects/delete\")]\n    public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync(\n        [FromBody] List<Guid> ids)\n    {\n        var projects = (await _projectRepository.GetManyWithSecretsByIds(ids)).ToList();\n        if (!projects.Any() || projects.Count != ids.Count)\n        {\n            throw new NotFoundException();\n        }\n\n        // Ensure all projects belongs to the same organization\n        var organizationId = projects.First().OrganizationId;\n        if (projects.Any(p => p.OrganizationId != organizationId) ||\n            !_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var projectsToDelete = new List<Project>();\n        var results = new List<(Project Project, string Error)>();\n\n        foreach (var project in projects)\n        {\n            var authorizationResult =\n                await _authorizationService.AuthorizeAsync(User, project, ProjectOperations.Delete);\n            if (authorizationResult.Succeeded)\n            {\n                projectsToDelete.Add(project);\n                results.Add((project, \"\"));\n            }\n            else\n            {\n                results.Add((project, \"access denied\"));\n            }\n        }\n\n        if (projectsToDelete.Count > 0)\n        {\n            await _deleteProjectCommand.DeleteProjects(projectsToDelete);\n            await LogProjectsEventAsync(projectsToDelete, EventType.Project_Deleted);\n        }\n\n        var responses = results.Select(r => new BulkDeleteResponseModel(r.Project.Id, r.Error));\n        return new ListResponseModel<BulkDeleteResponseModel>(responses);\n    }\n\n\n    private async Task LogProjectsEventAsync(IEnumerable<Project> projects, EventType eventType)\n    {\n        var userId = _userService.GetProperUserId(User)!.Value;\n\n        switch (_currentContext.IdentityClientType)\n        {\n            case IdentityClientType.ServiceAccount:\n                await _eventService.LogServiceAccountProjectsEventAsync(userId, projects, eventType);\n                break;\n            case IdentityClientType.User:\n                await _eventService.LogUserProjectsEventAsync(userId, projects, eventType);\n                break;\n        }\n    }\n\n    private Task LogProjectEventAsync(Project project, EventType eventType) =>\n       LogProjectsEventAsync(new[] { project }, eventType);\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/RequestSMAccessController.cs",
    "content": "﻿using Bit.Api.SecretsManager.Models.Request;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Commands.Requests.Interfaces;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Route(\"request-access\")]\n[Authorize(\"Web\")]\npublic class RequestSMAccessController : Controller\n{\n    private readonly IRequestSMAccessCommand _requestSMAccessCommand;\n    private readonly IUserService _userService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ICurrentContext _currentContext;\n\n    public RequestSMAccessController(\n        IRequestSMAccessCommand requestSMAccessCommand, IUserService userService, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, ICurrentContext currentContext)\n    {\n        _requestSMAccessCommand = requestSMAccessCommand;\n        _userService = userService;\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _currentContext = currentContext;\n    }\n\n    [HttpPost(\"request-sm-access\")]\n    public async Task RequestSMAccessFromAdmins([FromBody] RequestSMAccessRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!await _currentContext.OrganizationUser(model.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(model.OrganizationId);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organization.Id);\n        await _requestSMAccessCommand.SendRequestAccessToSM(organization, orgUsers, user, model.EmailContent);\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/SecretVersionsController.cs",
    "content": "﻿using Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Authorize(\"secrets\")]\npublic class SecretVersionsController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly ISecretVersionRepository _secretVersionRepository;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IUserService _userService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public SecretVersionsController(\n        ICurrentContext currentContext,\n        ISecretVersionRepository secretVersionRepository,\n        ISecretRepository secretRepository,\n        IUserService userService,\n        IOrganizationUserRepository organizationUserRepository)\n    {\n        _currentContext = currentContext;\n        _secretVersionRepository = secretVersionRepository;\n        _secretRepository = secretRepository;\n        _userService = userService;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    [HttpGet(\"secrets/{secretId}/versions\")]\n    public async Task<ListResponseModel<SecretVersionResponseModel>> GetVersionsBySecretIdAsync([FromRoute] Guid secretId)\n    {\n        var secret = await _secretRepository.GetByIdAsync(secretId);\n        if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        // For service accounts and organization API, skip user-level access checks\n        if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||\n            _currentContext.IdentityClientType == IdentityClientType.Organization)\n        {\n            // Already verified Secrets Manager access above\n            var versionList = await _secretVersionRepository.GetManyBySecretIdAsync(secretId);\n            var responseList = versionList.Select(v => new SecretVersionResponseModel(v));\n            return new ListResponseModel<SecretVersionResponseModel>(responseList);\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        if (!userId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient);\n        if (!access.Read)\n        {\n            throw new NotFoundException();\n        }\n\n        var versions = await _secretVersionRepository.GetManyBySecretIdAsync(secretId);\n        var responses = versions.Select(v => new SecretVersionResponseModel(v));\n\n        return new ListResponseModel<SecretVersionResponseModel>(responses);\n    }\n\n    [HttpGet(\"secret-versions/{id}\")]\n    public async Task<SecretVersionResponseModel> GetByIdAsync([FromRoute] Guid id)\n    {\n        var secretVersion = await _secretVersionRepository.GetByIdAsync(id);\n        if (secretVersion == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var secret = await _secretRepository.GetByIdAsync(secretVersion.SecretId);\n        if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        // For service accounts and organization API, skip user-level access checks\n        if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||\n            _currentContext.IdentityClientType == IdentityClientType.Organization)\n        {\n            // Already verified Secrets Manager access above\n            return new SecretVersionResponseModel(secretVersion);\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        if (!userId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        var access = await _secretRepository.AccessToSecretAsync(secretVersion.SecretId, userId.Value, accessClient);\n        if (!access.Read)\n        {\n            throw new NotFoundException();\n        }\n\n        return new SecretVersionResponseModel(secretVersion);\n    }\n\n    [HttpPost(\"secret-versions/get-by-ids\")]\n    public async Task<ListResponseModel<SecretVersionResponseModel>> GetManyByIdsAsync([FromBody] List<Guid> ids)\n    {\n        if (!ids.Any())\n        {\n            throw new BadRequestException(\"No version IDs provided.\");\n        }\n\n        // Get all versions\n        var versions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList();\n        if (!versions.Any())\n        {\n            throw new NotFoundException();\n        }\n\n        // Get all associated secrets and check permissions\n        var secretIds = versions.Select(v => v.SecretId).Distinct().ToList();\n        var secrets = (await _secretRepository.GetManyByIds(secretIds)).ToList();\n\n        if (!secrets.Any())\n        {\n            throw new NotFoundException();\n        }\n\n        // Ensure all secrets belong to the same organization\n        var organizationId = secrets.First().OrganizationId;\n        if (secrets.Any(s => s.OrganizationId != organizationId) ||\n            !_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        // For service accounts and organization API, skip user-level access checks\n        if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||\n            _currentContext.IdentityClientType == IdentityClientType.Organization)\n        {\n            // Already verified Secrets Manager access and organization ownership above\n            var serviceAccountResponses = versions.Select(v => new SecretVersionResponseModel(v));\n            return new ListResponseModel<SecretVersionResponseModel>(serviceAccountResponses);\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        if (!userId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var isAdmin = await _currentContext.OrganizationAdmin(organizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, isAdmin);\n\n        // Verify read access to all associated secrets\n        var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient);\n        if (accessResults.Values.Any(access => !access.Read))\n        {\n            throw new NotFoundException();\n        }\n\n        var responses = versions.Select(v => new SecretVersionResponseModel(v));\n        return new ListResponseModel<SecretVersionResponseModel>(responses);\n    }\n\n    [HttpPut(\"secrets/{secretId}/versions/restore\")]\n    public async Task<SecretResponseModel> RestoreVersionAsync([FromRoute] Guid secretId, [FromBody] RestoreSecretVersionRequestModel request)\n    {\n        if (!(_currentContext.IdentityClientType == IdentityClientType.User || _currentContext.IdentityClientType == IdentityClientType.ServiceAccount))\n        {\n            throw new NotFoundException();\n        }\n\n        var secret = await _secretRepository.GetByIdAsync(secretId);\n        if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        // Get the version first to validate it belongs to this secret\n        var version = await _secretVersionRepository.GetByIdAsync(request.VersionId);\n        if (version == null || version.SecretId != secretId)\n        {\n            throw new NotFoundException();\n        }\n\n        // Store the current value before restoration\n        var currentValue = secret.Value;\n\n        // For service accounts and organization API, skip user-level access checks\n        if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)\n        {\n            // Save current value as a version before restoring\n            if (currentValue != version.Value)\n            {\n                var editorUserId = _userService.GetProperUserId(User);\n                if (editorUserId.HasValue)\n                {\n                    var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion\n                    {\n                        SecretId = secretId,\n                        Value = currentValue!,\n                        VersionDate = DateTime.UtcNow,\n                        EditorServiceAccountId = editorUserId.Value\n                    };\n\n                    await _secretVersionRepository.CreateAsync(currentVersionSnapshot);\n                }\n            }\n\n            // Already verified Secrets Manager access above\n            secret.Value = version.Value;\n            secret.RevisionDate = DateTime.UtcNow;\n            var updatedSec = await _secretRepository.UpdateAsync(secret);\n            return new SecretResponseModel(updatedSec, true, true);\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        if (!userId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient);\n        if (!access.Write)\n        {\n            throw new NotFoundException();\n        }\n\n        // Save current value as a version before restoring\n        if (currentValue != version.Value)\n        {\n            var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId.Value);\n            if (orgUser == null)\n            {\n                throw new NotFoundException();\n            }\n\n            var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion\n            {\n                SecretId = secretId,\n                Value = currentValue!,\n                VersionDate = DateTime.UtcNow,\n                EditorOrganizationUserId = orgUser.Id\n            };\n\n            await _secretVersionRepository.CreateAsync(currentVersionSnapshot);\n        }\n\n        // Update the secret with the version's value\n        secret.Value = version.Value;\n        secret.RevisionDate = DateTime.UtcNow;\n\n        var updatedSecret = await _secretRepository.UpdateAsync(secret);\n\n        return new SecretResponseModel(updatedSecret, true, true);\n    }\n\n    [HttpPost(\"secret-versions/delete\")]\n    public async Task<IActionResult> BulkDeleteAsync([FromBody] List<Guid> ids)\n    {\n        if (!ids.Any())\n        {\n            throw new BadRequestException(\"No version IDs provided.\");\n        }\n\n        var secretVersions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList();\n        if (secretVersions.Count != ids.Count)\n        {\n            throw new NotFoundException();\n        }\n\n        // Ensure all versions belong to secrets in the same organization\n        var secretIds = secretVersions.Select(v => v.SecretId).Distinct().ToList();\n        var secrets = await _secretRepository.GetManyByIds(secretIds);\n        var secretsList = secrets.ToList();\n\n        if (!secretsList.Any())\n        {\n            throw new NotFoundException();\n        }\n\n        var organizationId = secretsList.First().OrganizationId;\n        if (secretsList.Any(s => s.OrganizationId != organizationId) ||\n            !_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        // For service accounts and organization API, skip user-level access checks\n        if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount ||\n            _currentContext.IdentityClientType == IdentityClientType.Organization)\n        {\n            // Already verified Secrets Manager access and organization ownership above\n            await _secretVersionRepository.DeleteManyByIdAsync(ids);\n            return Ok();\n        }\n\n        var userId = _userService.GetProperUserId(User);\n        if (!userId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        // Verify write access to all associated secrets\n        var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient);\n        if (accessResults.Values.Any(access => !access.Write))\n        {\n            throw new NotFoundException();\n        }\n\n        await _secretVersionRepository.DeleteManyByIdAsync(ids);\n\n        return Ok();\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/SecretsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Commands.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Authorize(\"secrets\")]\npublic class SecretsController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IProjectRepository _projectRepository;\n    private readonly ISecretRepository _secretRepository;\n    private readonly ISecretVersionRepository _secretVersionRepository;\n    private readonly ICreateSecretCommand _createSecretCommand;\n    private readonly IUpdateSecretCommand _updateSecretCommand;\n    private readonly IDeleteSecretCommand _deleteSecretCommand;\n    private readonly IAccessClientQuery _accessClientQuery;\n    private readonly ISecretsSyncQuery _secretsSyncQuery;\n    private readonly ISecretAccessPoliciesUpdatesQuery _secretAccessPoliciesUpdatesQuery;\n    private readonly IUserService _userService;\n    private readonly IEventService _eventService;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public SecretsController(\n        ICurrentContext currentContext,\n        IProjectRepository projectRepository,\n        ISecretRepository secretRepository,\n        ISecretVersionRepository secretVersionRepository,\n        ICreateSecretCommand createSecretCommand,\n        IUpdateSecretCommand updateSecretCommand,\n        IDeleteSecretCommand deleteSecretCommand,\n        IAccessClientQuery accessClientQuery,\n        ISecretsSyncQuery secretsSyncQuery,\n        ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery,\n        IUserService userService,\n        IEventService eventService,\n        IAuthorizationService authorizationService,\n        IOrganizationUserRepository organizationUserRepository)\n    {\n        _currentContext = currentContext;\n        _projectRepository = projectRepository;\n        _secretRepository = secretRepository;\n        _secretVersionRepository = secretVersionRepository;\n        _createSecretCommand = createSecretCommand;\n        _updateSecretCommand = updateSecretCommand;\n        _deleteSecretCommand = deleteSecretCommand;\n        _accessClientQuery = accessClientQuery;\n        _secretsSyncQuery = secretsSyncQuery;\n        _secretAccessPoliciesUpdatesQuery = secretAccessPoliciesUpdatesQuery;\n        _userService = userService;\n        _eventService = eventService;\n        _authorizationService = authorizationService;\n        _organizationUserRepository = organizationUserRepository;\n\n    }\n\n    [HttpGet(\"organizations/{organizationId}/secrets\")]\n    public async Task<SecretWithProjectsListResponseModel> ListByOrganizationAsync([FromRoute] Guid organizationId)\n    {\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        var secrets = await _secretRepository.GetManyDetailsByOrganizationIdAsync(organizationId, userId, accessClient);\n\n        return new SecretWithProjectsListResponseModel(secrets);\n    }\n\n    [HttpPost(\"organizations/{organizationId}/secrets\")]\n    public async Task<SecretResponseModel> CreateAsync([FromRoute] Guid organizationId,\n        [FromBody] SecretCreateRequestModel createRequest)\n    {\n        var secret = createRequest.ToSecret(organizationId);\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Create);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        SecretAccessPoliciesUpdates accessPoliciesUpdates = null;\n        if (createRequest.AccessPoliciesRequests != null)\n        {\n            secret.SetNewId();\n            accessPoliciesUpdates =\n                new SecretAccessPoliciesUpdates(\n                    createRequest.AccessPoliciesRequests.ToSecretAccessPolicies(secret.Id, organizationId));\n            var accessPolicyAuthorizationResult = await _authorizationService.AuthorizeAsync(User,\n                accessPoliciesUpdates, SecretAccessPoliciesOperations.Create);\n            if (!accessPolicyAuthorizationResult.Succeeded)\n            {\n                throw new NotFoundException();\n            }\n        }\n\n        var result = await _createSecretCommand.CreateAsync(secret, accessPoliciesUpdates);\n        await LogSecretEventAsync(secret, EventType.Secret_Created);\n        // Creating a secret means you have read & write permission.\n        return new SecretResponseModel(result, true, true);\n    }\n\n    [HttpGet(\"secrets/{id}\")]\n    public async Task<SecretResponseModel> GetAsync([FromRoute] Guid id)\n    {\n        var secret = await _secretRepository.GetByIdAsync(id);\n\n        if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        var access = await _secretRepository.AccessToSecretAsync(id, userId, accessClient);\n\n        if (!access.Read)\n        {\n            throw new NotFoundException();\n        }\n\n        await LogSecretEventAsync(secret, EventType.Secret_Retrieved);\n\n        return new SecretResponseModel(secret, access.Read, access.Write);\n    }\n\n    [HttpGet(\"projects/{projectId}/secrets\")]\n    public async Task<SecretWithProjectsListResponseModel> GetSecretsByProjectAsync([FromRoute] Guid projectId)\n    {\n        var project = await _projectRepository.GetByIdAsync(projectId);\n        if (project == null || !_currentContext.AccessSecretsManager(project.OrganizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        var secrets = await _secretRepository.GetManyDetailsByProjectIdAsync(projectId, userId, accessClient);\n\n        return new SecretWithProjectsListResponseModel(secrets);\n    }\n\n    [HttpPut(\"secrets/{id}\")]\n    public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)\n    {\n        var secret = await _secretRepository.GetByIdAsync(id);\n        if (secret == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var updatedSecret = updateRequest.ToSecret(secret);\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, updatedSecret, SecretOperations.Update);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        SecretAccessPoliciesUpdates accessPoliciesUpdates = null;\n        if (updateRequest.AccessPoliciesRequests != null)\n        {\n            var userId = _userService.GetProperUserId(User)!.Value;\n            accessPoliciesUpdates = await _secretAccessPoliciesUpdatesQuery.GetAsync(updateRequest.AccessPoliciesRequests.ToSecretAccessPolicies(id, secret.OrganizationId), userId);\n\n            var accessPolicyAuthorizationResult = await _authorizationService.AuthorizeAsync(User, accessPoliciesUpdates, SecretAccessPoliciesOperations.Updates);\n            if (!accessPolicyAuthorizationResult.Succeeded)\n            {\n                throw new NotFoundException();\n            }\n        }\n\n        // Create a version record if the value changed\n        if (updateRequest.ValueChanged)\n        {\n            // Store the old value before updating\n            var oldValue = secret.Value;\n            var userId = _userService.GetProperUserId(User)!.Value;\n            Guid? editorServiceAccountId = null;\n            Guid? editorOrganizationUserId = null;\n\n            if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount)\n            {\n                editorServiceAccountId = userId;\n            }\n            else if (_currentContext.IdentityClientType == IdentityClientType.User)\n            {\n                var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId);\n                if (orgUser != null)\n                {\n                    editorOrganizationUserId = orgUser.Id;\n                }\n                else\n                {\n                    throw new NotFoundException();\n                }\n            }\n\n            var secretVersion = new SecretVersion\n            {\n                SecretId = id,\n                Value = oldValue,\n                VersionDate = DateTime.UtcNow,\n                EditorServiceAccountId = editorServiceAccountId,\n                EditorOrganizationUserId = editorOrganizationUserId\n            };\n\n            await _secretVersionRepository.CreateAsync(secretVersion);\n        }\n\n        var result = await _updateSecretCommand.UpdateAsync(updatedSecret, accessPoliciesUpdates);\n        await LogSecretEventAsync(secret, EventType.Secret_Edited);\n\n        // Updating a secret means you have read & write permission.\n        return new SecretResponseModel(result, true, true);\n    }\n\n    [HttpPost(\"secrets/delete\")]\n    public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids)\n    {\n        var secrets = (await _secretRepository.GetManyByIds(ids)).ToList();\n        if (!secrets.Any() || secrets.Count != ids.Count)\n        {\n            throw new NotFoundException();\n        }\n\n        // Ensure all secrets belong to the same organization.\n        var organizationId = secrets.First().OrganizationId;\n        if (secrets.Any(secret => secret.OrganizationId != organizationId) ||\n            !_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var secretsToDelete = new List<Secret>();\n        var results = new List<(Secret Secret, string Error)>();\n\n        foreach (var secret in secrets)\n        {\n            var authorizationResult =\n                await _authorizationService.AuthorizeAsync(User, secret, SecretOperations.Delete);\n            if (authorizationResult.Succeeded)\n            {\n                secretsToDelete.Add(secret);\n                results.Add((secret, \"\"));\n            }\n            else\n            {\n                results.Add((secret, \"access denied\"));\n            }\n        }\n\n        await _deleteSecretCommand.DeleteSecrets(secretsToDelete);\n        var responses = results.Select(r => new BulkDeleteResponseModel(r.Secret.Id, r.Error));\n        await LogSecretsEventAsync(secretsToDelete, EventType.Secret_Deleted);\n        return new ListResponseModel<BulkDeleteResponseModel>(responses);\n    }\n\n    [HttpPost(\"secrets/get-by-ids\")]\n    public async Task<ListResponseModel<BaseSecretResponseModel>> GetSecretsByIdsAsync(\n        [FromBody] GetSecretsRequestModel request)\n    {\n        var secrets = (await _secretRepository.GetManyByIds(request.Ids)).ToList();\n        if (!secrets.Any() || secrets.Count != request.Ids.Count())\n        {\n            throw new NotFoundException();\n        }\n\n        var authorizationResult = await _authorizationService.AuthorizeAsync(User, secrets, BulkSecretOperations.ReadAll);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        await LogSecretsEventAsync(secrets, EventType.Secret_Retrieved);\n\n        var responses = secrets.Select(s => new BaseSecretResponseModel(s));\n        return new ListResponseModel<BaseSecretResponseModel>(responses);\n    }\n\n    [HttpGet(\"/organizations/{organizationId}/secrets/sync\")]\n    public async Task<SecretsSyncResponseModel> GetSecretsSyncAsync([FromRoute] Guid organizationId,\n        [FromQuery] DateTime? lastSyncedDate = null)\n    {\n        if (lastSyncedDate.HasValue && lastSyncedDate.Value > DateTime.UtcNow)\n        {\n            throw new BadRequestException(\"Last synced date must be in the past.\");\n        }\n\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var (accessClient, serviceAccountId) = await _accessClientQuery.GetAccessClientAsync(User, organizationId);\n        if (accessClient != AccessClientType.ServiceAccount)\n        {\n            throw new BadRequestException(\"Only service accounts can sync secrets.\");\n        }\n\n        var syncRequest = new SecretsSyncRequest\n        {\n            AccessClientType = accessClient,\n            OrganizationId = organizationId,\n            ServiceAccountId = serviceAccountId,\n            LastSyncedDate = lastSyncedDate\n        };\n        var syncResult = await _secretsSyncQuery.GetAsync(syncRequest);\n\n        if (syncResult.HasChanges)\n        {\n            await LogSecretsEventAsync(syncResult.Secrets, EventType.Secret_Retrieved);\n        }\n\n        return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets);\n    }\n\n    private async Task LogSecretsEventAsync(IEnumerable<Secret> secrets, EventType eventType)\n    {\n        var userId = _userService.GetProperUserId(User)!.Value;\n\n        switch (_currentContext.IdentityClientType)\n        {\n            case IdentityClientType.ServiceAccount:\n                await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, eventType);\n                break;\n            case IdentityClientType.User:\n                await _eventService.LogUserSecretsEventAsync(userId, secrets, eventType);\n                break;\n        }\n    }\n\n    private Task LogSecretEventAsync(Secret secret, EventType eventType) =>\n       LogSecretsEventAsync(new[] { secret }, eventType);\n\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Utilities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Authorize(\"secrets\")]\npublic class SecretsManagerEventsController : Controller\n{\n    private readonly IAuthorizationService _authorizationService;\n    private readonly IEventRepository _eventRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    public SecretsManagerEventsController(\n        IEventRepository eventRepository,\n        IServiceAccountRepository serviceAccountRepository,\n        IAuthorizationService authorizationService)\n    {\n        _authorizationService = authorizationService;\n        _serviceAccountRepository = serviceAccountRepository;\n        _eventRepository = eventRepository;\n    }\n\n    [HttpGet(\"sm/events/service-accounts/{serviceAccountId}\")]\n    public async Task<ListResponseModel<EventResponseModel>> GetServiceAccountEventsAsync(Guid serviceAccountId,\n        [FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null,\n        [FromQuery] string continuationToken = null)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.ReadEvents);\n\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var dateRange = ApiHelpers.GetDateRange(start, end);\n\n        var result = await _eventRepository.GetManyByOrganizationServiceAccountAsync(serviceAccount.OrganizationId,\n            serviceAccount.Id, dateRange.Item1, dateRange.Item2,\n            new PageOptions { ContinuationToken = continuationToken });\n        var responses = result.Data.Select(e => new EventResponseModel(e));\n        return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.Porting.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Authorize(\"secrets\")]\npublic class SecretsManagerPortingController : Controller\n{\n    private readonly ISecretRepository _secretRepository;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IUserService _userService;\n    private readonly IMaxProjectsQuery _maxProjectsQuery;\n    private readonly IImportCommand _importCommand;\n    private readonly ICurrentContext _currentContext;\n\n    public SecretsManagerPortingController(ISecretRepository secretRepository, IProjectRepository projectRepository,\n        IUserService userService, IMaxProjectsQuery maxProjectsQuery, IImportCommand importCommand,\n        ICurrentContext currentContext)\n    {\n        _secretRepository = secretRepository;\n        _projectRepository = projectRepository;\n        _userService = userService;\n        _maxProjectsQuery = maxProjectsQuery;\n        _importCommand = importCommand;\n        _currentContext = currentContext;\n    }\n\n    [HttpGet(\"sm/{organizationId}/export\")]\n    public async Task<SMExportResponseModel> Export([FromRoute] Guid organizationId)\n    {\n        if (!await _currentContext.OrganizationAdmin(organizationId) || !_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);\n        var secrets = await _secretRepository.GetManyDetailsByOrganizationIdAsync(organizationId, userId, AccessClientType.NoAccessCheck);\n\n        if (projects == null && secrets == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new SMExportResponseModel(projects.Select(p => p.Project), secrets.Select(s => s.Secret));\n    }\n\n    [HttpPost(\"sm/{organizationId}/import\")]\n    public async Task Import([FromRoute] Guid organizationId, [FromBody] SMImportRequestModel importRequest)\n    {\n        if (!await _currentContext.OrganizationAdmin(organizationId) || !_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (importRequest.Projects?.Count() > 1000 || importRequest.Secrets?.Count() > 6000)\n        {\n            throw new BadRequestException(\"You cannot import this much data at once, the limit is 1000 projects and 6000 secrets.\");\n        }\n\n        if (importRequest.Secrets.Any(s => s.ProjectIds.Count() > 1))\n        {\n            throw new BadRequestException(\"A secret can only be in one project at a time.\");\n        }\n\n        var projectsToAdd = importRequest.Projects?.Count();\n        if (projectsToAdd is > 0)\n        {\n            var (max, overMax) = await _maxProjectsQuery.GetByOrgIdAsync(organizationId, projectsToAdd.Value);\n            if (overMax != null && overMax.Value)\n            {\n                throw new BadRequestException($\"The maximum number of projects for this plan is ({max}).\");\n            }\n        }\n\n        await _importCommand.ImportAsync(organizationId, importRequest.ToSMImport());\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/SecretsTrashController.cs",
    "content": "﻿using Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.Trash.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Authorize(\"secrets\")]\npublic class TrashController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IEmptyTrashCommand _emptyTrashCommand;\n    private readonly IRestoreTrashCommand _restoreTrashCommand;\n    private readonly IUserService _userService;\n    private readonly IEventService _eventService;\n\n    public TrashController(\n        ICurrentContext currentContext,\n        ISecretRepository secretRepository,\n        IEmptyTrashCommand emptyTrashCommand,\n        IRestoreTrashCommand restoreTrashCommand,\n        IUserService userService,\n        IEventService eventService)\n    {\n        _currentContext = currentContext;\n        _secretRepository = secretRepository;\n        _emptyTrashCommand = emptyTrashCommand;\n        _restoreTrashCommand = restoreTrashCommand;\n        _userService = userService;\n        _eventService = eventService;\n    }\n\n    [HttpGet(\"secrets/{organizationId}/trash\")]\n    public async Task<SecretWithProjectsListResponseModel> ListByOrganizationAsync(Guid organizationId)\n    {\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (!await _currentContext.OrganizationAdmin(organizationId))\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var secrets = await _secretRepository.GetManyDetailsByOrganizationIdInTrashAsync(organizationId);\n        return new SecretWithProjectsListResponseModel(secrets);\n    }\n\n    [HttpPost(\"secrets/{organizationId}/trash/empty\")]\n    public async Task EmptyTrashAsync(Guid organizationId, [FromBody] List<Guid> ids)\n    {\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (!await _currentContext.OrganizationAdmin(organizationId))\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var deletedSecrets = await _secretRepository.GetManyTrashedSecretsByIds(ids);\n        await _emptyTrashCommand.EmptyTrash(organizationId, ids);\n        await LogSecretsTrashEventAsync(deletedSecrets, EventType.Secret_Permanently_Deleted);\n    }\n\n    [HttpPost(\"secrets/{organizationId}/trash/restore\")]\n    public async Task RestoreTrashAsync(Guid organizationId, [FromBody] List<Guid> ids)\n    {\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (!await _currentContext.OrganizationAdmin(organizationId))\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        await _restoreTrashCommand.RestoreTrash(organizationId, ids);\n        await LogSecretsTrashEventAsync(ids, EventType.Secret_Restored);\n    }\n\n    private async Task LogSecretsTrashEventAsync(IEnumerable<Guid> secretIds, EventType eventType)\n    {\n        var secrets = await _secretRepository.GetManyByIds(secretIds);\n        await LogSecretsTrashEventAsync(secrets, eventType);\n    }\n\n    private async Task LogSecretsTrashEventAsync(IEnumerable<Secret> secrets, EventType eventType)\n    {\n        var userId = _userService.GetProperUserId(User)!.Value;\n\n        switch (_currentContext.IdentityClientType)\n        {\n            case IdentityClientType.ServiceAccount:\n                await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, eventType);\n                break;\n            case IdentityClientType.User:\n                await _eventService.LogUserSecretsEventAsync(userId, secrets, eventType);\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Controllers/ServiceAccountsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.AuthorizationRequirements;\nusing Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;\nusing Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.SecretsManager.Controllers;\n\n[Authorize(\"secrets\")]\n[Route(\"service-accounts\")]\npublic class ServiceAccountsController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IUserService _userService;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly IApiKeyRepository _apiKeyRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ICountNewServiceAccountSlotsRequiredQuery _countNewServiceAccountSlotsRequiredQuery;\n    private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;\n    private readonly IServiceAccountSecretsDetailsQuery _serviceAccountSecretsDetailsQuery;\n    private readonly ICreateAccessTokenCommand _createAccessTokenCommand;\n    private readonly ICreateServiceAccountCommand _createServiceAccountCommand;\n    private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;\n    private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand;\n    private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand;\n    private readonly IPricingClient _pricingClient;\n    private readonly IEventService _eventService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public ServiceAccountsController(\n        ICurrentContext currentContext,\n        IUserService userService,\n        IAuthorizationService authorizationService,\n        IServiceAccountRepository serviceAccountRepository,\n        IApiKeyRepository apiKeyRepository,\n        IOrganizationRepository organizationRepository,\n        ICountNewServiceAccountSlotsRequiredQuery countNewServiceAccountSlotsRequiredQuery,\n        IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,\n        IServiceAccountSecretsDetailsQuery serviceAccountSecretsDetailsQuery,\n        ICreateAccessTokenCommand createAccessTokenCommand,\n        ICreateServiceAccountCommand createServiceAccountCommand,\n        IUpdateServiceAccountCommand updateServiceAccountCommand,\n        IDeleteServiceAccountsCommand deleteServiceAccountsCommand,\n        IRevokeAccessTokensCommand revokeAccessTokensCommand,\n        IPricingClient pricingClient,\n        IEventService eventService,\n        IOrganizationUserRepository organizationUserRepository)\n    {\n        _currentContext = currentContext;\n        _userService = userService;\n        _authorizationService = authorizationService;\n        _serviceAccountRepository = serviceAccountRepository;\n        _apiKeyRepository = apiKeyRepository;\n        _organizationRepository = organizationRepository;\n        _countNewServiceAccountSlotsRequiredQuery = countNewServiceAccountSlotsRequiredQuery;\n        _serviceAccountSecretsDetailsQuery = serviceAccountSecretsDetailsQuery;\n        _createServiceAccountCommand = createServiceAccountCommand;\n        _updateServiceAccountCommand = updateServiceAccountCommand;\n        _deleteServiceAccountsCommand = deleteServiceAccountsCommand;\n        _revokeAccessTokensCommand = revokeAccessTokensCommand;\n        _pricingClient = pricingClient;\n        _createAccessTokenCommand = createAccessTokenCommand;\n        _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;\n        _eventService = eventService;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    [HttpGet(\"/organizations/{organizationId}/service-accounts\")]\n    public async Task<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>> ListByOrganizationAsync(\n        [FromRoute] Guid organizationId, [FromQuery] bool includeAccessToSecrets = false)\n    {\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);\n        var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin);\n\n        var results =\n            await _serviceAccountSecretsDetailsQuery.GetManyByOrganizationIdAsync(organizationId, userId, accessClient,\n                includeAccessToSecrets);\n        var responses = results.Select(r => new ServiceAccountSecretsDetailsResponseModel(r));\n        return new ListResponseModel<ServiceAccountSecretsDetailsResponseModel>(responses);\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<ServiceAccountResponseModel> GetByServiceAccountIdAsync(\n        [FromRoute] Guid id)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Read);\n\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        return new ServiceAccountResponseModel(serviceAccount);\n    }\n\n    [HttpPost(\"/organizations/{organizationId}/service-accounts\")]\n    public async Task<ServiceAccountResponseModel> CreateAsync([FromRoute] Guid organizationId,\n        [FromBody] ServiceAccountCreateRequestModel createRequest)\n    {\n        var serviceAccount = createRequest.ToServiceAccount(organizationId);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Create);\n\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var newServiceAccountSlotsRequired = await _countNewServiceAccountSlotsRequiredQuery\n            .CountNewServiceAccountSlotsRequiredAsync(organizationId, 1);\n        if (newServiceAccountSlotsRequired > 0)\n        {\n            var org = await _organizationRepository.GetByIdAsync(organizationId);\n            // TODO: https://bitwarden.atlassian.net/browse/PM-17002\n            var plan = await _pricingClient.GetPlanOrThrow(org!.PlanType);\n            var update = new SecretsManagerSubscriptionUpdate(org, plan, true)\n                .AdjustServiceAccounts(newServiceAccountSlotsRequired);\n            await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n\n        var result =\n            await _createServiceAccountCommand.CreateAsync(serviceAccount, userId);\n\n        if (result != null)\n        {\n            await _eventService.LogServiceAccountEventAsync(userId, [serviceAccount], EventType.ServiceAccount_Created, _currentContext.IdentityClientType);\n        }\n\n        return new ServiceAccountResponseModel(result);\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task<ServiceAccountResponseModel> UpdateAsync([FromRoute] Guid id,\n        [FromBody] ServiceAccountUpdateRequestModel updateRequest)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Update);\n\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await _updateServiceAccountCommand.UpdateAsync(updateRequest.ToServiceAccount(id));\n        return new ServiceAccountResponseModel(result);\n    }\n\n    [HttpPost(\"delete\")]\n    public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids)\n    {\n        var serviceAccounts = (await _serviceAccountRepository.GetManyByIds(ids)).ToList();\n        if (!serviceAccounts.Any() || serviceAccounts.Count != ids.Count)\n        {\n            throw new NotFoundException();\n        }\n\n        // Ensure all service accounts belong to the same organization\n        var organizationId = serviceAccounts.First().OrganizationId;\n        if (serviceAccounts.Any(sa => sa.OrganizationId != organizationId) ||\n            !_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var serviceAccountsToDelete = new List<ServiceAccount>();\n        var results = new List<(ServiceAccount ServiceAccount, string Error)>();\n\n        foreach (var serviceAccount in serviceAccounts)\n        {\n            var authorizationResult =\n                await _authorizationService.AuthorizeAsync(User, serviceAccount, ServiceAccountOperations.Delete);\n            if (authorizationResult.Succeeded)\n            {\n                serviceAccountsToDelete.Add(serviceAccount);\n                results.Add((serviceAccount, \"\"));\n            }\n            else\n            {\n                results.Add((serviceAccount, \"access denied\"));\n            }\n        }\n\n        await _deleteServiceAccountsCommand.DeleteServiceAccounts(serviceAccountsToDelete);\n        var userId = _userService.GetProperUserId(User)!.Value;\n        await _eventService.LogServiceAccountEventAsync(userId, serviceAccountsToDelete, EventType.ServiceAccount_Deleted, _currentContext.IdentityClientType);\n\n        var responses = results.Select(r => new BulkDeleteResponseModel(r.ServiceAccount.Id, r.Error));\n        return new ListResponseModel<BulkDeleteResponseModel>(responses);\n    }\n\n    [HttpGet(\"{id}/access-tokens\")]\n    public async Task<ListResponseModel<AccessTokenResponseModel>> GetAccessTokens([FromRoute] Guid id)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, serviceAccount,\n                ServiceAccountOperations.ReadAccessTokens);\n\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var accessTokens = await _apiKeyRepository.GetManyByServiceAccountIdAsync(id);\n        var responses = accessTokens.Select(token => new AccessTokenResponseModel(token));\n        return new ListResponseModel<AccessTokenResponseModel>(responses);\n    }\n\n    [HttpPost(\"{id}/access-tokens\")]\n    public async Task<AccessTokenCreationResponseModel> CreateAccessTokenAsync([FromRoute] Guid id,\n        [FromBody] AccessTokenCreateRequestModel request)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, serviceAccount,\n                ServiceAccountOperations.CreateAccessToken);\n\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await _createAccessTokenCommand.CreateAsync(request.ToApiKey(id));\n        return new AccessTokenCreationResponseModel(result);\n    }\n\n    [HttpPost(\"{id}/access-tokens/revoke\")]\n    public async Task RevokeAccessTokensAsync(Guid id, [FromBody] RevokeAccessTokensRequest request)\n    {\n        var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);\n        var authorizationResult =\n            await _authorizationService.AuthorizeAsync(User, serviceAccount,\n                ServiceAccountOperations.RevokeAccessTokens);\n\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n\n        await _revokeAccessTokensCommand.RevokeAsync(serviceAccount, request.Ids);\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Jobs/EmptySecretsManagerTrashJob.cs",
    "content": "﻿using Bit.Core.Jobs;\nusing Bit.Core.SecretsManager.Repositories;\nusing Quartz;\n\nnamespace Bit.Api.Jobs;\n\npublic class EmptySecretsManagerTrashJob : BaseJob\n{\n    private ISecretRepository _secretRepository;\n    private const uint DeleteAfterThisNumberOfDays = 30;\n\n    public EmptySecretsManagerTrashJob(ISecretRepository secretRepository, ILogger<EmptySecretsManagerTrashJob> logger) : base(logger)\n    {\n        _secretRepository = secretRepository;\n    }\n\n    protected override async Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(\"Execute job task: EmptySecretsManagerTrashJob: Start\");\n        await _secretRepository.EmptyTrash(DateTime.UtcNow, DeleteAfterThisNumberOfDays);\n        _logger.LogInformation(\"Execute job task: EmptySecretsManagerTrashJob: End\");\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/AccessPolicyRequest.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class AccessPolicyRequest\n{\n    [Required]\n    public Guid GranteeId { get; set; }\n\n    [Required]\n    public bool Read { get; set; }\n\n    [Required]\n    public bool Write { get; set; }\n\n    public UserProjectAccessPolicy ToUserProjectAccessPolicy(Guid projectId, Guid organizationId) =>\n        new()\n        {\n            OrganizationUserId = GranteeId,\n            GrantedProjectId = projectId,\n            GrantedProject = new Project { OrganizationId = organizationId, Id = projectId },\n            Read = Read,\n            Write = Write\n        };\n\n    public UserSecretAccessPolicy ToUserSecretAccessPolicy(Guid secretId, Guid organizationId) =>\n        new()\n        {\n            OrganizationUserId = GranteeId,\n            GrantedSecretId = secretId,\n            GrantedSecret = new Secret { OrganizationId = organizationId, Id = secretId },\n            Read = Read,\n            Write = Write\n        };\n\n    public GroupProjectAccessPolicy ToGroupProjectAccessPolicy(Guid projectId, Guid organizationId) =>\n        new()\n        {\n            GroupId = GranteeId,\n            GrantedProjectId = projectId,\n            GrantedProject = new Project { OrganizationId = organizationId, Id = projectId },\n            Read = Read,\n            Write = Write\n        };\n\n    public GroupSecretAccessPolicy ToGroupSecretAccessPolicy(Guid secretId, Guid organizationId) =>\n        new()\n        {\n            GroupId = GranteeId,\n            GrantedSecretId = secretId,\n            GrantedSecret = new Secret { OrganizationId = organizationId, Id = secretId },\n            Read = Read,\n            Write = Write\n        };\n\n    public ServiceAccountProjectAccessPolicy ToServiceAccountProjectAccessPolicy(Guid projectId, Guid organizationId) =>\n        new()\n        {\n            ServiceAccountId = GranteeId,\n            GrantedProjectId = projectId,\n            GrantedProject = new Project { OrganizationId = organizationId, Id = projectId },\n            Read = Read,\n            Write = Write\n        };\n\n    public ServiceAccountSecretAccessPolicy ToServiceAccountSecretAccessPolicy(Guid secretId, Guid organizationId) =>\n        new()\n        {\n            ServiceAccountId = GranteeId,\n            GrantedSecretId = secretId,\n            GrantedSecret = new Secret { OrganizationId = organizationId, Id = secretId },\n            Read = Read,\n            Write = Write\n        };\n\n    public UserServiceAccountAccessPolicy ToUserServiceAccountAccessPolicy(Guid id, Guid organizationId) =>\n        new()\n        {\n            OrganizationUserId = GranteeId,\n            GrantedServiceAccountId = id,\n            GrantedServiceAccount = new ServiceAccount() { OrganizationId = organizationId, Id = id },\n            Read = Read,\n            Write = Write\n        };\n\n    public GroupServiceAccountAccessPolicy ToGroupServiceAccountAccessPolicy(Guid id, Guid organizationId) =>\n        new()\n        {\n            GroupId = GranteeId,\n            GrantedServiceAccountId = id,\n            GrantedServiceAccount = new ServiceAccount() { OrganizationId = organizationId, Id = id },\n            Read = Read,\n            Write = Write\n        };\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/AccessTokenCreateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class AccessTokenCreateRequestModel : IValidatableObject\n{\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(200)]\n    public string Name { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(4000)]\n    public string EncryptedPayload { get; set; }\n\n    [Required]\n    [EncryptedString]\n    public string Key { get; set; }\n\n    public DateTime? ExpireAt { get; set; }\n\n    public ApiKey ToApiKey(Guid serviceAccountId)\n    {\n        return new ApiKey()\n        {\n            ServiceAccountId = serviceAccountId,\n            Name = Name,\n            Key = Key,\n            ExpireAt = ExpireAt,\n            Scope = \"[\\\"api.secrets\\\"]\",\n            EncryptedPayload = EncryptedPayload,\n        };\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (ExpireAt != null && ExpireAt <= DateTime.UtcNow)\n        {\n            yield return new ValidationResult(\n               $\"Please select an expiration date that is in the future.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/GetSecretsRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class GetSecretsRequestModel : IValidatableObject\n{\n    [Required]\n    public IEnumerable<Guid> Ids { get; set; }\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        var isDistinct = Ids.Distinct().Count() == Ids.Count();\n        if (!isDistinct)\n        {\n            var duplicateGuids = Ids.GroupBy(x => x)\n                         .Where(g => g.Count() > 1)\n                         .Select(g => g.Key);\n\n            yield return new ValidationResult(\n                $\"The following GUIDs were duplicated {string.Join(\", \", duplicateGuids)} \",\n                new[] { nameof(GetSecretsRequestModel) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/GrantedAccessPolicyRequest.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing ServiceAccountProjectAccessPolicy = Bit.Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class GrantedAccessPolicyRequest\n{\n    [Required]\n    public Guid GrantedId { get; set; }\n\n    [Required]\n    public bool Read { get; set; }\n\n    [Required]\n    public bool Write { get; set; }\n\n    public ServiceAccountProjectAccessPolicy ToServiceAccountProjectAccessPolicy(Guid serviceAccountId, Guid organizationId) =>\n        new()\n        {\n            ServiceAccountId = serviceAccountId,\n            ServiceAccount = new ServiceAccount() { Id = serviceAccountId, OrganizationId = organizationId },\n            GrantedProjectId = GrantedId,\n            Read = Read,\n            Write = Write,\n        };\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.SecretsManager.Utilities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class PeopleAccessPoliciesRequestModel\n{\n    public IEnumerable<AccessPolicyRequest> UserAccessPolicyRequests { get; set; }\n\n    public IEnumerable<AccessPolicyRequest> GroupAccessPolicyRequests { get; set; }\n\n    public ProjectPeopleAccessPolicies ToProjectPeopleAccessPolicies(Guid grantedProjectId, Guid organizationId)\n    {\n        var userAccessPolicies = UserAccessPolicyRequests?\n            .Select(x => x.ToUserProjectAccessPolicy(grantedProjectId, organizationId)).ToList();\n\n        var groupAccessPolicies = GroupAccessPolicyRequests?\n            .Select(x => x.ToGroupProjectAccessPolicy(grantedProjectId, organizationId)).ToList();\n        var policies = new List<BaseAccessPolicy>();\n        if (userAccessPolicies != null)\n        {\n            policies.AddRange(userAccessPolicies);\n        }\n\n        if (groupAccessPolicies != null)\n        {\n            policies.AddRange(groupAccessPolicies);\n        }\n\n        AccessPolicyHelpers.CheckForDistinctAccessPolicies(policies);\n        AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(policies);\n\n        return new ProjectPeopleAccessPolicies\n        {\n            Id = grantedProjectId,\n            OrganizationId = organizationId,\n            UserAccessPolicies = userAccessPolicies,\n            GroupAccessPolicies = groupAccessPolicies\n        };\n    }\n\n    public ServiceAccountPeopleAccessPolicies ToServiceAccountPeopleAccessPolicies(Guid grantedServiceAccountId,\n        Guid organizationId)\n    {\n        var userAccessPolicies = UserAccessPolicyRequests?\n            .Select(x => x.ToUserServiceAccountAccessPolicy(grantedServiceAccountId, organizationId)).ToList();\n\n        var groupAccessPolicies = GroupAccessPolicyRequests?\n            .Select(x => x.ToGroupServiceAccountAccessPolicy(grantedServiceAccountId, organizationId)).ToList();\n\n        var policies = new List<BaseAccessPolicy>();\n        if (userAccessPolicies != null)\n        {\n            policies.AddRange(userAccessPolicies);\n        }\n\n        if (groupAccessPolicies != null)\n        {\n            policies.AddRange(groupAccessPolicies);\n        }\n\n        AccessPolicyHelpers.CheckForDistinctAccessPolicies(policies);\n\n        if (!policies.All(ap => ap.Read && ap.Write))\n        {\n            throw new BadRequestException(\"Machine account access must be Can read, write\");\n        }\n\n        return new ServiceAccountPeopleAccessPolicies\n        {\n            Id = grantedServiceAccountId,\n            OrganizationId = organizationId,\n            UserAccessPolicies = userAccessPolicies,\n            GroupAccessPolicies = groupAccessPolicies\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/ProjectCreateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class ProjectCreateRequestModel\n{\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Name { get; set; }\n\n    public Project ToProject(Guid organizationId)\n    {\n        return new Project\n        {\n            OrganizationId = organizationId,\n            Name = Name,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/ProjectServiceAccountsAccessPoliciesRequestModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.SecretsManager.Utilities;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class ProjectServiceAccountsAccessPoliciesRequestModel\n{\n    public required IEnumerable<AccessPolicyRequest> ServiceAccountAccessPolicyRequests { get; set; }\n\n    public ProjectServiceAccountsAccessPolicies ToProjectServiceAccountsAccessPolicies(Project project)\n    {\n        var serviceAccountAccessPolicies = ServiceAccountAccessPolicyRequests\n            .Select(x => x.ToServiceAccountProjectAccessPolicy(project.Id, project.OrganizationId))\n            .ToList();\n\n        AccessPolicyHelpers.CheckForDistinctAccessPolicies(serviceAccountAccessPolicies);\n        AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(serviceAccountAccessPolicies);\n\n        return new ProjectServiceAccountsAccessPolicies\n        {\n            ProjectId = project.Id,\n            OrganizationId = project.OrganizationId,\n            ServiceAccountAccessPolicies = serviceAccountAccessPolicies\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/ProjectUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class ProjectUpdateRequestModel\n{\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Name { get; set; }\n\n    public Project ToProject(Guid id)\n    {\n        return new Project\n        {\n            Id = id,\n            Name = Name,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/RequestSMAccessRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class RequestSMAccessRequestModel\n{\n    [Required]\n    public Guid OrganizationId { get; set; }\n    [Required(ErrorMessage = \"Add a note is a required field\")]\n    public string EmailContent { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/RestoreSecretVersionRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class RestoreSecretVersionRequestModel\n{\n    [Required]\n    public Guid VersionId { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/RevokeAccessTokensRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\npublic class RevokeAccessTokensRequest\n{\n    [Required]\n    public Guid[] Ids { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/SMImportRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.SecretsManager.Commands.Porting;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class SMImportRequestModel\n{\n    public IEnumerable<InnerProjectImportRequestModel> Projects { get; set; }\n    public IEnumerable<InnerSecretImportRequestModel> Secrets { get; set; }\n\n    public class InnerProjectImportRequestModel\n    {\n        public InnerProjectImportRequestModel() { }\n\n        [Required]\n        public Guid Id { get; set; }\n\n        [Required]\n        [EncryptedString]\n        [EncryptedStringLength(1000)]\n        public string Name { get; set; }\n    }\n\n    public class InnerSecretImportRequestModel\n    {\n        public InnerSecretImportRequestModel() { }\n\n        [Required]\n        public Guid Id { get; set; }\n\n        [Required]\n        [EncryptedString]\n        [EncryptedStringLength(1000)]\n        public string Key { get; set; }\n\n        [Required]\n        [EncryptedString]\n        [EncryptedStringLength(35000)]\n        public string Value { get; set; }\n\n        [Required]\n        [EncryptedString]\n        [EncryptedStringLength(10000)]\n        public string Note { get; set; }\n\n        [Required]\n        public IEnumerable<Guid> ProjectIds { get; set; }\n    }\n\n    public SMImport ToSMImport()\n    {\n        return new SMImport\n        {\n            Projects = Projects?.Select(p => new SMImport.InnerProject\n            {\n                Id = p.Id,\n                Name = p.Name,\n            }),\n            Secrets = Secrets?.Select(s => new SMImport.InnerSecret\n            {\n                Id = s.Id,\n                Key = s.Key,\n                Value = s.Value,\n                Note = s.Note,\n                ProjectIds = s.ProjectIds,\n            }),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/SecretAccessPoliciesRequestsModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.SecretsManager.Utilities;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class SecretAccessPoliciesRequestsModel\n{\n    public required IEnumerable<AccessPolicyRequest> UserAccessPolicyRequests { get; set; }\n\n    public required IEnumerable<AccessPolicyRequest> GroupAccessPolicyRequests { get; set; }\n\n    public required IEnumerable<AccessPolicyRequest> ServiceAccountAccessPolicyRequests { get; set; }\n\n    public SecretAccessPolicies ToSecretAccessPolicies(Guid secretId, Guid organizationId)\n    {\n        var userAccessPolicies = UserAccessPolicyRequests\n            .Select(x => x.ToUserSecretAccessPolicy(secretId, organizationId)).ToList();\n        var groupAccessPolicies = GroupAccessPolicyRequests\n            .Select(x => x.ToGroupSecretAccessPolicy(secretId, organizationId)).ToList();\n        var serviceAccountAccessPolicies = ServiceAccountAccessPolicyRequests\n            .Select(x => x.ToServiceAccountSecretAccessPolicy(secretId, organizationId)).ToList();\n\n        var policies = new List<BaseAccessPolicy>();\n        policies.AddRange(userAccessPolicies);\n        policies.AddRange(groupAccessPolicies);\n        policies.AddRange(serviceAccountAccessPolicies);\n\n        AccessPolicyHelpers.CheckForDistinctAccessPolicies(policies);\n        AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(policies);\n\n        return new SecretAccessPolicies\n        {\n            SecretId = secretId,\n            OrganizationId = organizationId,\n            UserAccessPolicies = userAccessPolicies,\n            GroupAccessPolicies = groupAccessPolicies,\n            ServiceAccountAccessPolicies = serviceAccountAccessPolicies\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class SecretCreateRequestModel : IValidatableObject\n{\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Key { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(35000)]\n    public string Value { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(10000)]\n    public string Note { get; set; }\n\n    public Guid[] ProjectIds { get; set; }\n\n    public SecretAccessPoliciesRequestsModel AccessPoliciesRequests { get; set; }\n\n    public Secret ToSecret(Guid organizationId)\n    {\n        return new Secret()\n        {\n            OrganizationId = organizationId,\n            Key = Key,\n            Value = Value,\n            Note = Note,\n            DeletedDate = null,\n            Projects = ProjectIds != null && ProjectIds.Any() ? ProjectIds.Select(x => new Project() { Id = x }).ToList() : null,\n        };\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (ProjectIds is { Length: > 1 })\n        {\n            yield return new ValidationResult(\n                $\"Only one project assignment is supported.\",\n                new[] { nameof(ProjectIds) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class SecretUpdateRequestModel : IValidatableObject\n{\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Key { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(35000)]\n    public string Value { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(10000)]\n    public string Note { get; set; }\n\n    public Guid[] ProjectIds { get; set; }\n\n    public SecretAccessPoliciesRequestsModel AccessPoliciesRequests { get; set; }\n\n    public bool ValueChanged { get; set; } = false;\n\n    public Secret ToSecret(Secret secret)\n    {\n        secret.Key = Key;\n        secret.Value = Value;\n        secret.Note = Note;\n        secret.RevisionDate = DateTime.UtcNow;\n\n        if (secret.Projects?.FirstOrDefault()?.Id == ProjectIds?.FirstOrDefault())\n        {\n            secret.Projects = null;\n        }\n        else\n        {\n            secret.Projects = ProjectIds != null && ProjectIds.Length != 0\n                ? ProjectIds.Select(x => new Project() { Id = x }).ToList()\n                : [];\n        }\n\n        return secret;\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (ProjectIds is { Length: > 1 })\n        {\n            yield return new ValidationResult(\n                $\"Only one project assignment is supported.\",\n                new[] { nameof(ProjectIds) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/SerivceAccountUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class ServiceAccountUpdateRequestModel\n{\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Name { get; set; }\n\n    public ServiceAccount ToServiceAccount(Guid id)\n    {\n        return new ServiceAccount()\n        {\n            Id = id,\n            Name = Name,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/ServiceAccountCreateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class ServiceAccountCreateRequestModel\n{\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Name { get; set; }\n\n    public ServiceAccount ToServiceAccount(Guid organizationId)\n    {\n        return new ServiceAccount()\n        {\n            OrganizationId = organizationId,\n            Name = Name,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Request/ServiceAccountGrantedPoliciesRequestModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.SecretsManager.Utilities;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Request;\n\npublic class ServiceAccountGrantedPoliciesRequestModel\n{\n    public required IEnumerable<GrantedAccessPolicyRequest> ProjectGrantedPolicyRequests { get; set; }\n\n    public ServiceAccountGrantedPolicies ToGrantedPolicies(ServiceAccount serviceAccount)\n    {\n        var projectGrantedPolicies = ProjectGrantedPolicyRequests\n            .Select(x => x.ToServiceAccountProjectAccessPolicy(serviceAccount.Id, serviceAccount.OrganizationId))\n            .ToList();\n\n        AccessPolicyHelpers.CheckForDistinctAccessPolicies(projectGrantedPolicies);\n        AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(projectGrantedPolicies);\n\n        return new ServiceAccountGrantedPolicies\n        {\n            ServiceAccountId = serviceAccount.Id,\n            OrganizationId = serviceAccount.OrganizationId,\n            ProjectGrantedPolicies = projectGrantedPolicies\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic abstract class BaseAccessPolicyResponseModel : ResponseModel\n{\n    protected BaseAccessPolicyResponseModel(BaseAccessPolicy baseAccessPolicy, string obj) : base(obj)\n    {\n        Read = baseAccessPolicy.Read;\n        Write = baseAccessPolicy.Write;\n    }\n\n    public bool Read { get; set; }\n    public bool Write { get; set; }\n\n    protected static string? GetUserDisplayName(User? user)\n    {\n        return string.IsNullOrWhiteSpace(user?.Name) ? user?.Email : user?.Name;\n    }\n}\n\npublic class UserAccessPolicyResponseModel : BaseAccessPolicyResponseModel\n{\n    private const string _objectName = \"userAccessPolicy\";\n\n    public UserAccessPolicyResponseModel(UserProjectAccessPolicy accessPolicy, Guid currentUserId) : base(accessPolicy, _objectName)\n    {\n        CurrentUser = currentUserId == accessPolicy.User?.Id;\n        OrganizationUserId = accessPolicy.OrganizationUserId;\n        OrganizationUserName = GetUserDisplayName(accessPolicy.User);\n    }\n\n    public UserAccessPolicyResponseModel(UserServiceAccountAccessPolicy accessPolicy, Guid currentUserId) : base(accessPolicy, _objectName)\n    {\n        CurrentUser = currentUserId == accessPolicy.User?.Id;\n        OrganizationUserId = accessPolicy.OrganizationUserId;\n        OrganizationUserName = GetUserDisplayName(accessPolicy.User);\n    }\n\n    public UserAccessPolicyResponseModel(UserSecretAccessPolicy accessPolicy, Guid currentUserId) : base(accessPolicy, _objectName)\n    {\n        CurrentUser = currentUserId == accessPolicy.User?.Id;\n        OrganizationUserId = accessPolicy.OrganizationUserId;\n        OrganizationUserName = GetUserDisplayName(accessPolicy.User);\n    }\n\n    public UserAccessPolicyResponseModel() : base(new UserProjectAccessPolicy(), _objectName)\n    {\n    }\n\n    public Guid? OrganizationUserId { get; set; }\n    public string? OrganizationUserName { get; set; }\n    public bool? CurrentUser { get; set; }\n}\n\npublic class GroupAccessPolicyResponseModel : BaseAccessPolicyResponseModel\n{\n    private const string _objectName = \"groupAccessPolicy\";\n\n    public GroupAccessPolicyResponseModel(GroupProjectAccessPolicy accessPolicy)\n        : base(accessPolicy, _objectName)\n    {\n        GroupId = accessPolicy.GroupId;\n        GroupName = accessPolicy.Group?.Name;\n        CurrentUserInGroup = accessPolicy.CurrentUserInGroup;\n    }\n\n    public GroupAccessPolicyResponseModel(GroupServiceAccountAccessPolicy accessPolicy)\n        : base(accessPolicy, _objectName)\n    {\n        GroupId = accessPolicy.GroupId;\n        GroupName = accessPolicy.Group?.Name;\n        CurrentUserInGroup = accessPolicy.CurrentUserInGroup;\n    }\n\n    public GroupAccessPolicyResponseModel(GroupSecretAccessPolicy accessPolicy)\n        : base(accessPolicy, _objectName)\n    {\n        GroupId = accessPolicy.GroupId;\n        GroupName = accessPolicy.Group?.Name;\n        CurrentUserInGroup = accessPolicy.CurrentUserInGroup;\n    }\n\n    public GroupAccessPolicyResponseModel() : base(new GroupProjectAccessPolicy(), _objectName)\n    {\n    }\n\n    public Guid? GroupId { get; set; }\n    public string? GroupName { get; set; }\n    public bool? CurrentUserInGroup { get; set; }\n}\n\npublic class ServiceAccountAccessPolicyResponseModel : BaseAccessPolicyResponseModel\n{\n    private const string _objectName = \"serviceAccountProjectAccessPolicy\";\n\n    public ServiceAccountAccessPolicyResponseModel(ServiceAccountProjectAccessPolicy accessPolicy)\n        : base(accessPolicy, _objectName)\n    {\n        ServiceAccountId = accessPolicy.ServiceAccountId;\n        ServiceAccountName = accessPolicy.ServiceAccount?.Name;\n    }\n\n    public ServiceAccountAccessPolicyResponseModel(ServiceAccountSecretAccessPolicy accessPolicy)\n        : base(accessPolicy, _objectName)\n    {\n        ServiceAccountId = accessPolicy.ServiceAccountId;\n        ServiceAccountName = accessPolicy.ServiceAccount?.Name;\n    }\n\n    public ServiceAccountAccessPolicyResponseModel()\n        : base(new ServiceAccountProjectAccessPolicy(), _objectName)\n    {\n    }\n\n    public Guid? ServiceAccountId { get; set; }\n    public string? ServiceAccountName { get; set; }\n}\n\npublic class GrantedProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel\n{\n    private const string _objectName = \"grantedProjectAccessPolicy\";\n\n    public GrantedProjectAccessPolicyResponseModel(ServiceAccountProjectAccessPolicy accessPolicy)\n        : base(accessPolicy, _objectName)\n    {\n        GrantedProjectId = accessPolicy.GrantedProjectId;\n        GrantedProjectName = accessPolicy.GrantedProject?.Name;\n    }\n\n    public GrantedProjectAccessPolicyResponseModel()\n        : base(new ServiceAccountProjectAccessPolicy(), _objectName)\n    {\n    }\n\n    public Guid? GrantedProjectId { get; set; }\n    public string? GrantedProjectName { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/AccessTokenCreationResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class AccessTokenCreationResponseModel : ResponseModel\n{\n    private const string _objectName = \"accessTokenCreation\";\n\n    public AccessTokenCreationResponseModel(ApiKeyClientSecretDetails details) : base(_objectName)\n    {\n        Id = details.ApiKey.Id;\n        Name = details.ApiKey.Name;\n        ExpireAt = details.ApiKey.ExpireAt;\n        CreationDate = details.ApiKey.CreationDate;\n        RevisionDate = details.ApiKey.RevisionDate;\n        ClientSecret = details.ClientSecret;\n    }\n\n    public AccessTokenCreationResponseModel() : base(_objectName)\n    {\n    }\n\n    public Guid Id { get; set; }\n    public string? Name { get; set; }\n    public string? ClientSecret { get; set; }\n    public DateTime? ExpireAt { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/AccessTokenResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class AccessTokenResponseModel : ResponseModel\n{\n    private const string _objectName = \"accessToken\";\n\n    public AccessTokenResponseModel(ApiKey apiKey, string obj = _objectName)\n        : base(obj)\n    {\n        Id = apiKey.Id;\n        Name = apiKey.Name;\n        Scopes = apiKey.GetScopes();\n\n        ExpireAt = apiKey.ExpireAt;\n        CreationDate = apiKey.CreationDate;\n        RevisionDate = apiKey.RevisionDate;\n    }\n\n    public AccessTokenResponseModel() : base(_objectName)\n    {\n    }\n\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public ICollection<string> Scopes { get; set; }\n\n    public DateTime? ExpireAt { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/BaseSecretResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class BaseSecretResponseModel : ResponseModel\n{\n    private const string _objectName = \"baseSecret\";\n\n    public BaseSecretResponseModel(Secret secret, string objectName = _objectName) : base(objectName)\n    {\n        if (secret == null)\n        {\n            throw new ArgumentNullException(nameof(secret));\n        }\n\n        Id = secret.Id;\n        OrganizationId = secret.OrganizationId;\n        Key = secret.Key;\n        Value = secret.Value;\n        Note = secret.Note;\n        CreationDate = secret.CreationDate;\n        RevisionDate = secret.RevisionDate;\n        Projects = secret.Projects?.Select(p => new SecretResponseInnerProject(p));\n    }\n\n    public BaseSecretResponseModel(string objectName = _objectName) : base(objectName)\n    {\n    }\n\n    public BaseSecretResponseModel() : base(_objectName)\n    {\n    }\n\n    public Guid Id { get; set; }\n\n    public Guid OrganizationId { get; set; }\n\n    public string Key { get; set; }\n\n    public string Value { get; set; }\n\n    public string Note { get; set; }\n\n    public DateTime CreationDate { get; set; }\n\n    public DateTime RevisionDate { get; set; }\n\n    public IEnumerable<SecretResponseInnerProject> Projects { get; set; }\n\n    public class SecretResponseInnerProject\n    {\n        public SecretResponseInnerProject(Project project)\n        {\n            Id = project.Id;\n            Name = project.Name;\n        }\n\n        public SecretResponseInnerProject()\n        {\n        }\n\n        public Guid Id { get; set; }\n        public string Name { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/BulkDeleteResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class BulkDeleteResponseModel : ResponseModel\n{\n    private const string _objectName = \"BulkDeleteResponseModel\";\n\n    public BulkDeleteResponseModel(Guid id, string error) : base(_objectName)\n    {\n        Id = id;\n\n        if (string.IsNullOrWhiteSpace(error))\n        {\n            Error = null;\n        }\n        else\n        {\n            Error = error;\n        }\n    }\n\n    public BulkDeleteResponseModel() : base(_objectName)\n    {\n    }\n\n    public Guid Id { get; set; }\n\n    public string? Error { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/GrantedProjectAccessPolicyPermissionDetailsResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class GrantedProjectAccessPolicyPermissionDetailsResponseModel : ResponseModel\n{\n    private const string _objectName = \"grantedProjectAccessPolicyPermissionDetails\";\n\n    public GrantedProjectAccessPolicyPermissionDetailsResponseModel(\n        ServiceAccountProjectAccessPolicyPermissionDetails apPermissionDetails, string obj = _objectName) : base(obj)\n    {\n        AccessPolicy = new GrantedProjectAccessPolicyResponseModel(apPermissionDetails.AccessPolicy);\n        HasPermission = apPermissionDetails.HasPermission;\n    }\n\n    public GrantedProjectAccessPolicyPermissionDetailsResponseModel()\n        : base(_objectName)\n    {\n    }\n\n    public GrantedProjectAccessPolicyResponseModel AccessPolicy { get; set; } = new();\n    public bool HasPermission { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/OrganizationCountsResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class OrganizationCountsResponseModel() : ResponseModel(_objectName)\n{\n    private const string _objectName = \"organizationCounts\";\n\n    public int Projects { get; set; }\n\n    public int Secrets { get; set; }\n\n    public int ServiceAccounts { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class PotentialGranteeResponseModel : ResponseModel\n{\n    private const string _objectName = \"potentialGrantee\";\n\n    public PotentialGranteeResponseModel(GroupGrantee grantee)\n        : base(_objectName)\n    {\n        if (grantee == null)\n        {\n            throw new ArgumentNullException(nameof(grantee));\n        }\n\n        Type = \"group\";\n        Id = grantee.GroupId;\n        Name = grantee.Name;\n        CurrentUserInGroup = grantee.CurrentUserInGroup;\n    }\n\n    public PotentialGranteeResponseModel(UserGrantee grantee)\n        : base(_objectName)\n    {\n        if (grantee == null)\n        {\n            throw new ArgumentNullException(nameof(grantee));\n        }\n\n        Type = \"user\";\n        Id = grantee.OrganizationUserId;\n        Name = grantee.Name;\n        Email = grantee.Email;\n        CurrentUser = grantee.CurrentUser;\n    }\n\n    public PotentialGranteeResponseModel(ServiceAccount serviceAccount)\n        : base(_objectName)\n    {\n        if (serviceAccount == null)\n        {\n            throw new ArgumentNullException(nameof(serviceAccount));\n        }\n\n        Id = serviceAccount.Id;\n        Name = serviceAccount.Name;\n        Type = \"serviceAccount\";\n    }\n\n    public PotentialGranteeResponseModel(Project project)\n        : base(_objectName)\n    {\n        if (project == null)\n        {\n            throw new ArgumentNullException(nameof(project));\n        }\n\n        Id = project.Id;\n        Name = project.Name;\n        Type = \"project\";\n    }\n\n    public PotentialGranteeResponseModel() : base(_objectName)\n    {\n    }\n\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public string Type { get; set; }\n    public string Email { get; set; }\n    public bool CurrentUserInGroup { get; set; }\n    public bool CurrentUser { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/ProjectCountsResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class ProjectCountsResponseModel() : ResponseModel(_objectName)\n{\n    private const string _objectName = \"projectCounts\";\n\n    public int Secrets { get; set; }\n\n    public int People { get; set; }\n\n    public int ServiceAccounts { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/ProjectPeopleAccessPoliciesResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class ProjectPeopleAccessPoliciesResponseModel : ResponseModel\n{\n    private const string _objectName = \"projectPeopleAccessPolicies\";\n\n    public ProjectPeopleAccessPoliciesResponseModel(IEnumerable<BaseAccessPolicy> baseAccessPolicies, Guid userId)\n        : base(_objectName)\n    {\n        foreach (var baseAccessPolicy in baseAccessPolicies)\n        {\n            switch (baseAccessPolicy)\n            {\n                case UserProjectAccessPolicy accessPolicy:\n                    UserAccessPolicies.Add(new UserAccessPolicyResponseModel(accessPolicy, userId));\n                    break;\n                case GroupProjectAccessPolicy accessPolicy:\n                    GroupAccessPolicies.Add(new GroupAccessPolicyResponseModel(accessPolicy));\n                    break;\n            }\n        }\n    }\n\n    public ProjectPeopleAccessPoliciesResponseModel() : base(_objectName)\n    {\n    }\n\n    public List<UserAccessPolicyResponseModel> UserAccessPolicies { get; set; } = new();\n\n    public List<GroupAccessPolicyResponseModel> GroupAccessPolicies { get; set; } = new();\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/ProjectResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class ProjectResponseModel : ResponseModel\n{\n    private const string _objectName = \"project\";\n\n    public ProjectResponseModel(Project project, bool read, bool write, string obj = _objectName)\n        : base(obj)\n    {\n        if (project == null)\n        {\n            throw new ArgumentNullException(nameof(project));\n        }\n\n        Id = project.Id;\n        OrganizationId = project.OrganizationId;\n        Name = project.Name;\n        CreationDate = project.CreationDate;\n        RevisionDate = project.RevisionDate;\n        Read = read;\n        Write = write;\n    }\n\n    public ProjectResponseModel(ProjectPermissionDetails projectDetails, string obj = _objectName)\n        : base(obj)\n    {\n        if (projectDetails == null)\n        {\n            throw new ArgumentNullException(nameof(projectDetails));\n        }\n\n        Id = projectDetails.Project.Id;\n        OrganizationId = projectDetails.Project.OrganizationId;\n        Name = projectDetails.Project.Name;\n        CreationDate = projectDetails.Project.CreationDate;\n        RevisionDate = projectDetails.Project.RevisionDate;\n        Read = projectDetails.Read;\n        Write = projectDetails.Write;\n    }\n\n    public ProjectResponseModel() : base(_objectName)\n    {\n    }\n\n    public Guid Id { get; set; }\n\n    public Guid OrganizationId { get; set; }\n\n    public string Name { get; set; }\n\n    public DateTime CreationDate { get; set; }\n\n    public DateTime RevisionDate { get; set; }\n\n    public bool Read { get; set; }\n\n    public bool Write { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/ProjectServiceAccountsAccessPoliciesResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class ProjectServiceAccountsAccessPoliciesResponseModel : ResponseModel\n{\n    private const string _objectName = \"ProjectServiceAccountsAccessPolicies\";\n\n    public ProjectServiceAccountsAccessPoliciesResponseModel(\n        ProjectServiceAccountsAccessPolicies? projectServiceAccountsAccessPolicies)\n        : base(_objectName)\n    {\n        if (projectServiceAccountsAccessPolicies == null)\n        {\n            return;\n        }\n\n        ServiceAccountAccessPolicies = projectServiceAccountsAccessPolicies.ServiceAccountAccessPolicies\n            .Select(x => new ServiceAccountAccessPolicyResponseModel(x)).ToList();\n    }\n\n    public ProjectServiceAccountsAccessPoliciesResponseModel() : base(_objectName)\n    {\n    }\n\n    public List<ServiceAccountAccessPolicyResponseModel> ServiceAccountAccessPolicies { get; set; } = [];\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/SMExportResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class SMExportResponseModel : ResponseModel\n{\n    public SMExportResponseModel(IEnumerable<Project> projects, IEnumerable<Secret> secrets, string obj = \"SecretsManagerExportResponseModel\") : base(obj)\n    {\n        Secrets = secrets?.Select(s => new InnerSecretExportResponseModel(s));\n        Projects = projects?.Select(p => new InnerProjectExportResponseModel(p));\n    }\n\n    public IEnumerable<InnerProjectExportResponseModel> Projects { get; set; }\n    public IEnumerable<InnerSecretExportResponseModel> Secrets { get; set; }\n\n    public class InnerProjectExportResponseModel\n    {\n        public InnerProjectExportResponseModel(Project project)\n        {\n            Id = project.Id;\n            Name = project.Name;\n        }\n\n        public Guid Id { get; set; }\n        public string Name { get; set; }\n    }\n\n    public class InnerSecretExportResponseModel\n    {\n        public InnerSecretExportResponseModel(Secret secret)\n        {\n            Id = secret.Id;\n            Key = secret.Key;\n            Value = secret.Value;\n            Note = secret.Note;\n            ProjectIds = secret.Projects?.Select(p => p.Id);\n        }\n\n        public Guid Id { get; set; }\n        public string Key { get; set; }\n        public string Value { get; set; }\n        public string Note { get; set; }\n        public IEnumerable<Guid> ProjectIds { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/SMImportResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Commands.Porting;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class SMImportResponseModel : ResponseModel\n{\n    public SMImportResponseModel(SMImport import, string obj = \"SecretsManagerImportResponseModel\") : base(obj)\n    {\n        Projects = import.Projects?.Select(p => new InnerProjectImportResponseModel(p));\n        Secrets = import.Secrets?.Select(s => new InnerSecretImportResponseModel(s));\n    }\n\n    public IEnumerable<InnerProjectImportResponseModel> Projects { get; set; }\n    public IEnumerable<InnerSecretImportResponseModel> Secrets { get; set; }\n\n    public class InnerProjectImportResponseModel\n    {\n        public InnerProjectImportResponseModel() { }\n\n        public InnerProjectImportResponseModel(SMImport.InnerProject project)\n        {\n            Id = project.Id;\n            Name = project.Name;\n        }\n\n        public Guid Id { get; set; }\n        public string Name { get; set; }\n    }\n\n    public class InnerSecretImportResponseModel\n    {\n        public InnerSecretImportResponseModel() { }\n\n        public InnerSecretImportResponseModel(SMImport.InnerSecret secret)\n        {\n            Id = secret.Id;\n            Key = secret.Key;\n            Value = secret.Value;\n            Note = secret.Note;\n            ProjectIds = secret.ProjectIds;\n        }\n\n        public Guid Id { get; set; }\n        public string Key { get; set; }\n        public string Value { get; set; }\n        public string Note { get; set; }\n        public IEnumerable<Guid> ProjectIds { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/SecretAccessPoliciesResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class SecretAccessPoliciesResponseModel : ResponseModel\n{\n    private const string _objectName = \"secretAccessPolicies\";\n\n    public SecretAccessPoliciesResponseModel(SecretAccessPolicies? accessPolicies, Guid userId) :\n        base(_objectName)\n    {\n        if (accessPolicies == null)\n        {\n            return;\n        }\n\n        UserAccessPolicies = accessPolicies.UserAccessPolicies.Select(x => new UserAccessPolicyResponseModel(x, userId)).ToList();\n        GroupAccessPolicies = accessPolicies.GroupAccessPolicies.Select(x => new GroupAccessPolicyResponseModel(x)).ToList();\n        ServiceAccountAccessPolicies = accessPolicies.ServiceAccountAccessPolicies.Select(x => new ServiceAccountAccessPolicyResponseModel(x)).ToList();\n    }\n\n    public SecretAccessPoliciesResponseModel() : base(_objectName)\n    {\n    }\n\n\n    public List<UserAccessPolicyResponseModel> UserAccessPolicies { get; set; } = [];\n    public List<GroupAccessPolicyResponseModel> GroupAccessPolicies { get; set; } = [];\n    public List<ServiceAccountAccessPolicyResponseModel> ServiceAccountAccessPolicies { get; set; } = [];\n\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/SecretResponseModel.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class SecretResponseModel : BaseSecretResponseModel\n{\n    private const string _objectName = \"secret\";\n\n    public SecretResponseModel(Secret secret, bool read, bool write) : base(secret, _objectName)\n    {\n        Read = read;\n        Write = write;\n    }\n\n    public SecretResponseModel() : base(_objectName)\n    {\n    }\n\n    public bool Read { get; set; }\n\n    public bool Write { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class SecretVersionResponseModel : ResponseModel\n{\n    private const string _objectName = \"secretVersion\";\n\n    public Guid Id { get; set; }\n    public Guid SecretId { get; set; }\n    public string Value { get; set; } = string.Empty;\n    public DateTime VersionDate { get; set; }\n    public Guid? EditorServiceAccountId { get; set; }\n    public Guid? EditorOrganizationUserId { get; set; }\n\n    public SecretVersionResponseModel() : base(_objectName) { }\n\n    public SecretVersionResponseModel(SecretVersion secretVersion) : base(_objectName)\n    {\n        Id = secretVersion.Id;\n        SecretId = secretVersion.SecretId;\n        Value = secretVersion.Value;\n        VersionDate = secretVersion.VersionDate;\n        EditorServiceAccountId = secretVersion.EditorServiceAccountId;\n        EditorOrganizationUserId = secretVersion.EditorOrganizationUserId;\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/SecretWithProjectsListResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class SecretWithProjectsListResponseModel : ResponseModel\n{\n    private const string _objectName = \"SecretsWithProjectsList\";\n\n    public SecretWithProjectsListResponseModel(IEnumerable<SecretPermissionDetails> secrets) : base(_objectName)\n    {\n        Secrets = secrets.Select(s => new SecretsWithProjectsInnerSecret(s));\n        Projects = secrets.SelectMany(s => s.Secret.Projects).DistinctBy(p => p.Id).Select(p => new SecretWithProjectsInnerProject(p));\n    }\n\n    public SecretWithProjectsListResponseModel() : base(_objectName)\n    {\n    }\n\n    public IEnumerable<SecretsWithProjectsInnerSecret> Secrets { get; set; }\n    public IEnumerable<SecretWithProjectsInnerProject> Projects { get; set; }\n\n    public class SecretWithProjectsInnerProject\n    {\n        public SecretWithProjectsInnerProject(Project project)\n        {\n            Id = project.Id;\n            Name = project.Name;\n        }\n\n        public SecretWithProjectsInnerProject()\n        {\n        }\n\n        public Guid Id { get; set; }\n        public string Name { get; set; }\n    }\n\n    public class SecretsWithProjectsInnerSecret\n    {\n        public SecretsWithProjectsInnerSecret(SecretPermissionDetails secret)\n        {\n            Id = secret.Secret.Id;\n            OrganizationId = secret.Secret.OrganizationId;\n            Key = secret.Secret.Key;\n            CreationDate = secret.Secret.CreationDate;\n            RevisionDate = secret.Secret.RevisionDate;\n            Projects = secret.Secret.Projects?.Select(p => new SecretWithProjectsInnerProject(p));\n            Read = secret.Read;\n            Write = secret.Write;\n        }\n\n        public SecretsWithProjectsInnerSecret()\n        {\n        }\n\n        public Guid Id { get; set; }\n\n        public Guid OrganizationId { get; set; }\n\n        public string Key { get; set; }\n\n        public DateTime CreationDate { get; set; }\n\n        public DateTime RevisionDate { get; set; }\n\n        public IEnumerable<SecretWithProjectsInnerProject> Projects { get; set; }\n        public bool Read { get; set; }\n        public bool Write { get; set; }\n    }\n}\n\n\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/SecretsSyncResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.Models.Response;\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class SecretsSyncResponseModel : ResponseModel\n{\n    private const string _objectName = \"secretsSync\";\n\n    public bool HasChanges { get; set; }\n    public ListResponseModel<BaseSecretResponseModel>? Secrets { get; set; }\n\n    public SecretsSyncResponseModel(bool hasChanges, IEnumerable<Secret>? secrets, string obj = _objectName)\n        : base(obj)\n    {\n        Secrets = secrets != null\n            ? new ListResponseModel<BaseSecretResponseModel>(secrets.Select(s => new BaseSecretResponseModel(s)))\n            : null;\n        HasChanges = hasChanges;\n    }\n\n    public SecretsSyncResponseModel() : base(_objectName)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/ServiceAccountCountsResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class ServiceAccountCountsResponseModel() : ResponseModel(_objectName)\n{\n    private const string _objectName = \"serviceAccountCounts\";\n\n    public int Projects { get; set; }\n\n    public int People { get; set; }\n\n    public int AccessTokens { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/ServiceAccountGrantedPoliciesPermissionDetailsResponseModel.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class ServiceAccountGrantedPoliciesPermissionDetailsResponseModel : ResponseModel\n{\n    private const string _objectName = \"ServiceAccountGrantedPoliciesPermissionDetails\";\n\n    public ServiceAccountGrantedPoliciesPermissionDetailsResponseModel(\n        ServiceAccountGrantedPoliciesPermissionDetails? grantedPoliciesPermissionDetails)\n        : base(_objectName)\n    {\n        if (grantedPoliciesPermissionDetails == null)\n        {\n            return;\n        }\n\n        GrantedProjectPolicies = grantedPoliciesPermissionDetails.ProjectGrantedPolicies\n            .Select(x => new GrantedProjectAccessPolicyPermissionDetailsResponseModel(x)).ToList();\n    }\n\n    public ServiceAccountGrantedPoliciesPermissionDetailsResponseModel() : base(_objectName)\n    {\n    }\n\n    public List<GrantedProjectAccessPolicyPermissionDetailsResponseModel> GrantedProjectPolicies { get; set; } =\n        [];\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/ServiceAccountPeopleAccessPoliciesResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class ServiceAccountPeopleAccessPoliciesResponseModel : ResponseModel\n{\n    private const string _objectName = \"serviceAccountAccessPolicies\";\n\n    public ServiceAccountPeopleAccessPoliciesResponseModel(IEnumerable<BaseAccessPolicy> baseAccessPolicies, Guid userId)\n        : base(_objectName)\n    {\n        if (baseAccessPolicies == null)\n        {\n            return;\n        }\n\n        foreach (var baseAccessPolicy in baseAccessPolicies)\n        {\n            switch (baseAccessPolicy)\n            {\n                case UserServiceAccountAccessPolicy accessPolicy:\n                    UserAccessPolicies.Add(new UserAccessPolicyResponseModel(accessPolicy, userId));\n                    break;\n                case GroupServiceAccountAccessPolicy accessPolicy:\n                    GroupAccessPolicies.Add(new GroupAccessPolicyResponseModel(accessPolicy));\n                    break;\n            }\n        }\n    }\n\n    public ServiceAccountPeopleAccessPoliciesResponseModel() : base(_objectName)\n    {\n    }\n\n    public List<UserAccessPolicyResponseModel> UserAccessPolicies { get; set; } = new();\n\n    public List<GroupAccessPolicyResponseModel> GroupAccessPolicies { get; set; } = new();\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Models/Response/ServiceAccountResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.SecretsManager.Models.Response;\n\npublic class ServiceAccountResponseModel : ResponseModel\n{\n    private const string _objectName = \"serviceAccount\";\n\n    public ServiceAccountResponseModel(ServiceAccount serviceAccount) : base(_objectName)\n    {\n        if (serviceAccount == null)\n        {\n            throw new ArgumentNullException(nameof(serviceAccount));\n        }\n\n        Id = serviceAccount.Id;\n        OrganizationId = serviceAccount.OrganizationId;\n        Name = serviceAccount.Name;\n        CreationDate = serviceAccount.CreationDate;\n        RevisionDate = serviceAccount.RevisionDate;\n    }\n\n    public ServiceAccountResponseModel() : base(_objectName)\n    {\n    }\n\n    public Guid Id { get; set; }\n\n    public Guid OrganizationId { get; set; }\n\n    public string Name { get; set; }\n\n    public DateTime CreationDate { get; set; }\n\n    public DateTime RevisionDate { get; set; }\n}\n\npublic class ServiceAccountSecretsDetailsResponseModel : ServiceAccountResponseModel\n{\n    public ServiceAccountSecretsDetailsResponseModel(ServiceAccountSecretsDetails serviceAccountDetails) : base(serviceAccountDetails.ServiceAccount)\n    {\n        if (serviceAccountDetails == null)\n        {\n            throw new ArgumentNullException(nameof(serviceAccountDetails));\n        }\n\n        AccessToSecrets = serviceAccountDetails.AccessToSecrets;\n    }\n\n    public ServiceAccountSecretsDetailsResponseModel() : base(new ServiceAccount())\n    {\n    }\n\n    public int AccessToSecrets { get; set; }\n}\n"
  },
  {
    "path": "src/Api/SecretsManager/Utilities/AccessPolicyHelpers.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Api.SecretsManager.Utilities;\n\npublic static class AccessPolicyHelpers\n{\n    public static void CheckForDistinctAccessPolicies(IReadOnlyCollection<BaseAccessPolicy> accessPolicies)\n    {\n        var distinctAccessPolicies = accessPolicies.DistinctBy(baseAccessPolicy =>\n        {\n            return baseAccessPolicy switch\n            {\n                UserProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.OrganizationUserId, ap.GrantedProjectId),\n                UserSecretAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.OrganizationUserId, ap.GrantedSecretId),\n                UserServiceAccountAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.OrganizationUserId,\n                    ap.GrantedServiceAccountId),\n                GroupProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedProjectId),\n                GroupSecretAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedSecretId),\n                GroupServiceAccountAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.GroupId, ap.GrantedServiceAccountId),\n                ServiceAccountProjectAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.ServiceAccountId,\n                    ap.GrantedProjectId),\n                ServiceAccountSecretAccessPolicy ap => new Tuple<Guid?, Guid?>(ap.ServiceAccountId,\n                    ap.GrantedSecretId),\n                _ => throw new ArgumentException(\"Unsupported access policy type provided.\", nameof(baseAccessPolicy)),\n            };\n        }).ToList();\n\n        if (accessPolicies.Count != distinctAccessPolicies.Count)\n        {\n            throw new BadRequestException(\"Resources must be unique\");\n        }\n    }\n\n    public static void CheckAccessPoliciesHaveReadPermission(IEnumerable<BaseAccessPolicy> accessPolicies)\n    {\n        var accessPoliciesPermission = accessPolicies.All(policy => policy.Read);\n        if (!accessPoliciesPermission)\n        {\n            throw new BadRequestException(\"Resources must be Read = true\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Startup.cs",
    "content": "﻿using Bit.Api.Utilities;\nusing Bit.Core;\nusing Bit.Core.Context;\nusing Bit.Core.Settings;\nusing AspNetCoreRateLimit;\nusing Stripe;\nusing Bit.Core.Utilities;\nusing Duende.IdentityModel;\nusing System.Globalization;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.Auth.Models.Request;\nusing Bit.Api.KeyManagement.Validators;\nusing Bit.Api.Tools.Models.Request;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Core.Auth.Entities;\nusing Bit.SharedWeb.Health;\nusing Microsoft.OpenApi;\nusing Bit.SharedWeb.Utilities;\nusing Microsoft.AspNetCore.Diagnostics.HealthChecks;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Bit.Core.Auth.UserFeatures;\nusing Bit.Core.Entities;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Api.Auth.Models.Request.WebAuthn;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Tools.ImportFeatures;\nusing Bit.Core.Auth.Models.Api.Request;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Tools.SendFeatures;\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Enums;\n\n\n#if !OSS\nusing Bit.Commercial.Core.SecretsManager;\nusing Bit.Commercial.Core.Utilities;\nusing Bit.Commercial.Infrastructure.EntityFramework.SecretsManager;\n#endif\n\nnamespace Bit.Api;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(\"en-US\");\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; private set; }\n    public IWebHostEnvironment Environment { get; set; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n        if (!globalSettings.SelfHosted)\n        {\n            services.Configure<IpRateLimitOptions>(Configuration.GetSection(\"IpRateLimitOptions\"));\n            services.Configure<IpRateLimitPolicies>(Configuration.GetSection(\"IpRateLimitPolicies\"));\n        }\n\n        // Data Protection\n        services.AddCustomDataProtectionServices(Environment, globalSettings);\n\n        // Event Grid\n        if (!string.IsNullOrWhiteSpace(globalSettings.EventGridKey))\n        {\n            ApiHelpers.EventGridKey = globalSettings.EventGridKey;\n        }\n\n        // Stripe Billing\n        StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;\n        StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;\n\n        // Repositories\n        services.AddDatabaseRepositories(globalSettings);\n        services.AddTestPlayIdTracking(globalSettings);\n\n        // Context\n        services.AddScoped<ICurrentContext, CurrentContext>();\n        services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();\n\n        // Caching\n        services.AddMemoryCache();\n        services.AddDistributedCache(globalSettings);\n\n        if (!globalSettings.SelfHosted)\n        {\n            services.AddIpRateLimiting(globalSettings);\n        }\n\n        // Identity\n        services.AddCustomIdentityServices(globalSettings);\n        services.AddIdentityAuthenticationServices(globalSettings, Environment, config =>\n        {\n            config.AddPolicy(Policies.Application, policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, \"Application\", \"external\");\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);\n            });\n            config.AddPolicy(Policies.Web, policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, \"Application\", \"external\");\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);\n                policy.RequireClaim(JwtClaimTypes.ClientId, BitwardenClient.Web);\n            });\n            config.AddPolicy(Policies.Push, policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiPush);\n            });\n            config.AddPolicy(Policies.Licensing, policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiLicensing);\n            });\n            config.AddPolicy(Policies.Organization, policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiOrganization);\n            });\n            config.AddPolicy(Policies.Installation, policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiInstallation);\n            });\n            config.AddPolicy(Policies.Secrets, policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireAssertion(ctx => ctx.User.HasClaim(c =>\n                    c.Type == JwtClaimTypes.Scope &&\n                    (c.Value.Contains(ApiScopes.Api) || c.Value.Contains(ApiScopes.ApiSecrets))\n                ));\n            });\n            config.AddPolicy(Policies.Send, configurePolicy: policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiSendAccess);\n                policy.RequireClaim(Claims.SendAccessClaims.SendId);\n            });\n        });\n\n        services.AddScoped<AuthenticatorTokenProvider>();\n\n        // Key Rotation\n        services.AddUserKeyCommands(globalSettings);\n        services\n            .AddScoped<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>,\n                CipherRotationValidator>();\n        services\n            .AddScoped<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>,\n                FolderRotationValidator>();\n        services\n            .AddScoped<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>,\n                SendRotationValidator>();\n        services\n            .AddScoped<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>,\n                EmergencyAccessRotationValidator>();\n        services\n            .AddScoped<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,\n                    IReadOnlyList<OrganizationUser>>\n                , OrganizationUserRotationValidator>();\n        services\n            .AddScoped<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>,\n                WebAuthnLoginKeyRotationValidator>();\n        services\n            .AddScoped<IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>>,\n                DeviceRotationValidator>();\n\n        // Services\n        services.AddBaseServices(globalSettings);\n        services.AddDefaultServices(globalSettings);\n        services.AddOrganizationSubscriptionServices();\n        services.AddCoreLocalizationServices();\n        services.AddBillingOperations();\n        services.AddReportingServices(globalSettings);\n        services.AddImportServices();\n\n        services.AddSendServices();\n\n        // Authorization Handlers\n        services.AddAuthorizationHandlers();\n\n        //health check\n        if (!globalSettings.SelfHosted)\n        {\n            services.AddHealthChecks(globalSettings);\n        }\n\n#if OSS\n        services.AddOosServices();\n#else\n        services.AddCommercialCoreServices();\n        services.AddCommercialSecretsManagerServices();\n        services.AddSecretsManagerEfRepositories();\n        Jobs.JobsHostedService.AddCommercialSecretsManagerJobServices(services);\n#endif\n\n        // MVC\n        services.AddMvc(config =>\n        {\n            config.Conventions.Add(new ApiExplorerGroupConvention());\n            config.Conventions.Add(new PublicApiControllersModelConvention());\n        });\n\n        services.AddSwaggerGen(globalSettings, Environment);\n        Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);\n        services.AddHostedService<Jobs.JobsHostedService>();\n\n        if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&\n            CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName))\n        {\n            services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();\n        }\n\n        // Add Event Integrations services\n        services.AddEventIntegrationsCommandsQueries(globalSettings);\n        services.AddSlackService(globalSettings);\n        services.AddTeamsService(globalSettings);\n    }\n\n    public void Configure(\n        IApplicationBuilder app,\n        IWebHostEnvironment env,\n        GlobalSettings globalSettings,\n        ILogger<Startup> logger)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n\n        // Default Middleware\n        app.UseDefaultMiddleware(env, globalSettings);\n\n        if (!globalSettings.SelfHosted)\n        {\n            // Rate limiting\n            app.UseMiddleware<CustomIpRateLimitMiddleware>();\n        }\n        else\n        {\n            app.UseForwardedHeaders(globalSettings);\n        }\n\n        // Add localization\n        app.UseCoreLocalization();\n\n        // Add static files to the request pipeline.\n        app.UseStaticFiles();\n\n        // Add routing\n        app.UseRouting();\n\n        // Add Cors\n        app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))\n            .AllowAnyMethod().AllowAnyHeader().AllowCredentials());\n\n        // Add authentication and authorization to the request pipeline.\n        app.UseAuthentication();\n        app.UseAuthorization();\n\n        // Add current context\n        app.UseMiddleware<CurrentContextMiddleware>();\n\n        // Add endpoints to the request pipeline.\n        app.UseEndpoints(endpoints =>\n        {\n            endpoints.MapDefaultControllerRoute();\n\n            if (!globalSettings.SelfHosted)\n            {\n                endpoints.MapHealthChecks(\"/healthz\");\n\n                endpoints.MapHealthChecks(\"/healthz/extended\", new HealthCheckOptions\n                {\n                    ResponseWriter = HealthCheckServiceExtensions.WriteResponse\n                });\n            }\n        });\n\n        // Add Swagger\n        // Note that the swagger.json generation is configured in the call to AddSwaggerGen above.\n        if (Environment.IsDevelopment() || globalSettings.SelfHosted)\n        {\n            // adds the middleware to serve the swagger.json while the server is running\n            app.UseSwagger(config =>\n            {\n                config.RouteTemplate = \"specs/{documentName}/swagger.json\";\n\n                // Remove all Bitwarden cloud servers and only register the local server\n                config.PreSerializeFilters.Add((swaggerDoc, httpReq) =>\n                {\n                    swaggerDoc.Servers =\n                    [\n                        new()\n                        {\n                            Url = globalSettings.BaseServiceUri.Api,\n                        }\n                    ];\n\n                    swaggerDoc.Components ??= new OpenApiComponents();\n                    swaggerDoc.Components.SecuritySchemes = new Dictionary<string, IOpenApiSecurityScheme>\n                    {\n                        {\n                            \"oauth2-client-credentials\",\n                            new OpenApiSecurityScheme\n                            {\n                                Type = SecuritySchemeType.OAuth2,\n                                Flows = new OpenApiOAuthFlows\n                                {\n                                    ClientCredentials = new OpenApiOAuthFlow\n                                    {\n                                        TokenUrl = new Uri($\"{globalSettings.BaseServiceUri.Identity}/connect/token\"),\n                                        Scopes = new Dictionary<string, string>\n                                {\n                                    { ApiScopes.ApiOrganization, \"Organization APIs\" }\n                                }\n                                    }\n                                }\n                            }\n                        }\n                    };\n\n                    swaggerDoc.Security =\n                    [\n                        new OpenApiSecurityRequirement\n                        {\n                            [new OpenApiSecuritySchemeReference(\"oauth2-client-credentials\", swaggerDoc)] = [ApiScopes.ApiOrganization]\n                        },\n                    ];\n\n                    swaggerDoc.Workspace = new OpenApiWorkspace();\n                    swaggerDoc.RegisterComponents();\n                });\n            });\n\n            // adds the middleware to display the web UI\n            app.UseSwaggerUI(config =>\n            {\n                config.DocumentTitle = \"Bitwarden API Documentation\";\n                config.RoutePrefix = \"docs\";\n                config.SwaggerEndpoint($\"{globalSettings.BaseServiceUri.Api}/specs/public/swagger.json\",\n                    \"Bitwarden Public API\");\n                config.OAuthClientId(\"accountType.id\");\n                config.OAuthClientSecret(\"secretKey\");\n\n                // Persist authorization on page refresh - for development use only\n                if (Environment.IsDevelopment())\n                {\n                    config.EnablePersistAuthorization();\n                }\n            });\n        }\n\n        // Log startup\n        logger.LogInformation(Constants.BypassFiltersEventId, \"{Project} started.\", globalSettings.ProjectName);\n    }\n}\n"
  },
  {
    "path": "src/Api/Tools/Authorization/VaultExportAuthorizationHandler.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Api.Tools.Authorization;\n\npublic class VaultExportAuthorizationHandler(ICurrentContext currentContext)\n    : AuthorizationHandler<VaultExportOperationRequirement, OrganizationScope>\n{\n    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        VaultExportOperationRequirement requirement, OrganizationScope organizationScope)\n    {\n        var org = currentContext.GetOrganization(organizationScope);\n\n        var authorized = requirement switch\n        {\n            not null when requirement == VaultExportOperations.ExportWholeVault =>\n                CanExportWholeVault(org),\n            not null when requirement == VaultExportOperations.ExportManagedCollections =>\n                CanExportManagedCollections(org),\n            _ => false\n        };\n\n        if (authorized)\n        {\n            context.Succeed(requirement);\n        }\n\n        return Task.FromResult(0);\n    }\n\n    private bool CanExportWholeVault(CurrentContextOrganization organization) => organization is\n    { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n    { Type: OrganizationUserType.Custom, Permissions.AccessImportExport: true };\n\n    private bool CanExportManagedCollections(CurrentContextOrganization organization) => organization is not null;\n}\n"
  },
  {
    "path": "src/Api/Tools/Authorization/VaultExportOperations.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Api.Tools.Authorization;\n\npublic class VaultExportOperationRequirement : OperationAuthorizationRequirement;\n\npublic static class VaultExportOperations\n{\n    /// <summary>\n    /// Exporting the entire organization vault.\n    /// </summary>\n    public static readonly VaultExportOperationRequirement ExportWholeVault =\n        new() { Name = nameof(ExportWholeVault) };\n\n    /// <summary>\n    /// Exporting only the organization items that the user has Can Manage permissions for\n    /// </summary>\n    public static readonly VaultExportOperationRequirement ExportManagedCollections =\n        new() { Name = nameof(ExportManagedCollections) };\n}\n"
  },
  {
    "path": "src/Api/Tools/Controllers/ImportCiphersController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Tools.Models.Request.Accounts;\nusing Bit.Api.Tools.Models.Request.Organizations;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.ImportFeatures.Interfaces;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Tools.Controllers;\n\n[Route(\"ciphers\")]\n[Authorize(\"Application\")]\npublic class ImportCiphersController : Controller\n{\n    private readonly IUserService _userService;\n    private readonly ICurrentContext _currentContext;\n    private readonly ILogger<ImportCiphersController> _logger;\n    private readonly GlobalSettings _globalSettings;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly IImportCiphersCommand _importCiphersCommand;\n\n    public ImportCiphersController(\n        IUserService userService,\n        ICurrentContext currentContext,\n        ILogger<ImportCiphersController> logger,\n        GlobalSettings globalSettings,\n        ICollectionRepository collectionRepository,\n        IAuthorizationService authorizationService,\n        IImportCiphersCommand importCiphersCommand)\n    {\n        _userService = userService;\n        _currentContext = currentContext;\n        _logger = logger;\n        _globalSettings = globalSettings;\n        _collectionRepository = collectionRepository;\n        _authorizationService = authorizationService;\n        _importCiphersCommand = importCiphersCommand;\n    }\n\n    [HttpPost(\"import\")]\n    public async Task PostImport([FromBody] ImportCiphersRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted &&\n            (model.Ciphers.Count() > 7000 || model.FolderRelationships.Count() > 7000 ||\n                model.Folders.Count() > 2000))\n        {\n            throw new BadRequestException(\"You cannot import this much data at once.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();\n        var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList();\n        await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships, userId);\n    }\n\n    [HttpPost(\"import-organization\")]\n    public async Task PostImportOrganization([FromQuery] string organizationId,\n        [FromBody] ImportOrganizationCiphersRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted &&\n            (model.Ciphers.Count() > _globalSettings.ImportCiphersLimitation.CiphersLimit ||\n             model.CollectionRelationships.Count() > _globalSettings.ImportCiphersLimitation.CollectionRelationshipsLimit ||\n             model.Collections.Count() > _globalSettings.ImportCiphersLimitation.CollectionsLimit))\n        {\n            throw new BadRequestException(\"You cannot import this much data at once.\");\n        }\n\n        var orgId = new Guid(organizationId);\n        var collections = model.Collections.Select(c => c.ToCollection(orgId)).ToList();\n\n        //An User is allowed to import if CanCreate Collections or has AccessToImportExport\n        var authorized = await CheckOrgImportPermission(collections, orgId);\n        if (!authorized)\n        {\n            throw new BadRequestException(\"Not enough privileges to import into this organization.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList();\n        await _importCiphersCommand.ImportIntoOrganizationalVaultAsync(collections, ciphers, model.CollectionRelationships, userId);\n    }\n\n    private async Task<bool> CheckOrgImportPermission(List<Collection> collections, Guid orgId)\n    {\n        //Users are allowed to import if they have the AccessToImportExport permission\n        if (await _currentContext.AccessImportExport(orgId))\n        {\n            return true;\n        }\n\n        //Calling Repository instead of Service as we want to get all the collections, regardless of permission\n        //Permissions check will be done later on AuthorizationService\n        var orgCollectionIds =\n            (await _collectionRepository.GetManyByOrganizationIdAsync(orgId))\n            .Select(c => c.Id)\n            .ToHashSet();\n\n        // when there are no collections, then we can import\n        if (collections.Count == 0)\n        {\n            return true;\n        }\n\n        // are we trying to import into existing collections?\n        var existingCollections = collections.Where(tc => orgCollectionIds.Contains(tc.Id));\n\n        // are we trying to create new collections?\n        var hasNewCollections = collections.Any(tc => !orgCollectionIds.Contains(tc.Id));\n\n        // suppose we have both new and existing collections\n        if (hasNewCollections && existingCollections.Any())\n        {\n            // since we are creating new collection, user must have import/manage and create collection permission\n            if ((await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Create)).Succeeded\n                && (await _authorizationService.AuthorizeAsync(User, existingCollections, BulkCollectionOperations.ImportCiphers)).Succeeded)\n            {\n                // can import collections and create new ones\n                return true;\n            }\n            else\n            {\n                // user does not have permission to import\n                return false;\n            }\n        }\n\n        // suppose we have new collections and none of our collections exist\n        if (hasNewCollections && !existingCollections.Any())\n        {\n            // user is trying to create new collections\n            // we need to check if the user has permission to create collections\n            if ((await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Create)).Succeeded)\n            {\n                return true;\n            }\n            else\n            {\n                // user does not have permission to create new collections\n                return false;\n            }\n        }\n\n        // in many import formats, we don't create collections, we just import ciphers into an existing collection\n\n        // When importing, we need to verify if the user has ImportCiphers permission\n        if (existingCollections.Any() && (await _authorizationService.AuthorizeAsync(User, existingCollections, BulkCollectionOperations.ImportCiphers)).Succeeded)\n        {\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Api/Tools/Controllers/OrganizationExportController.cs",
    "content": "﻿using Bit.Api.Tools.Authorization;\nusing Bit.Api.Tools.Models.Response;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Queries;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Tools.Controllers;\n\n[Route(\"organizations/{organizationId}\")]\n[Authorize(\"Application\")]\npublic class OrganizationExportController : Controller\n{\n    private readonly IUserService _userService;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly IOrganizationCiphersQuery _organizationCiphersQuery;\n    private readonly ICollectionRepository _collectionRepository;\n\n    public OrganizationExportController(\n        IUserService userService,\n        GlobalSettings globalSettings,\n        IAuthorizationService authorizationService,\n        IOrganizationCiphersQuery organizationCiphersQuery,\n        ICollectionRepository collectionRepository,\n        IFeatureService featureService)\n    {\n        _userService = userService;\n        _globalSettings = globalSettings;\n        _authorizationService = authorizationService;\n        _organizationCiphersQuery = organizationCiphersQuery;\n        _collectionRepository = collectionRepository;\n    }\n\n    [HttpGet(\"export\")]\n    public async Task<IActionResult> Export(Guid organizationId)\n    {\n        var canExportAll = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId),\n            VaultExportOperations.ExportWholeVault);\n        var canExportManaged = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId),\n            VaultExportOperations.ExportManagedCollections);\n\n        if (canExportAll.Succeeded)\n        {\n            var allOrganizationCiphers =\n                await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(\n                    organizationId);\n\n            var allCollections = await _collectionRepository\n                .GetManySharedCollectionsByOrganizationIdAsync(\n                    organizationId);\n\n\n            return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections,\n                _globalSettings));\n        }\n\n        if (canExportManaged.Succeeded)\n        {\n            var userId = _userService.GetProperUserId(User)!.Value;\n\n            var allUserCollections = await _collectionRepository.GetManyByUserIdAsync(userId);\n            var managedOrgCollections =\n                allUserCollections.Where(c => c.OrganizationId == organizationId && c.Manage).ToList();\n\n            var managedCiphers = await _organizationCiphersQuery.GetOrganizationCiphersByCollectionIds(organizationId,\n                managedOrgCollections.Select(c => c.Id));\n\n            return Ok(new OrganizationExportResponseModel(managedCiphers, managedOrgCollections, _globalSettings));\n        }\n\n        // Unauthorized\n        throw new NotFoundException();\n    }\n}\n"
  },
  {
    "path": "src/Api/Tools/Controllers/SendsController.cs",
    "content": "﻿using System.Text.Json;\nusing Azure.Messaging.EventGrid;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Tools.Models.Request;\nusing Bit.Api.Tools.Models.Response;\nusing Bit.Api.Utilities;\nusing Bit.Core;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.UserFeatures.SendAccess;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures;\nusing Bit.Core.Tools.SendFeatures.Commands.Interfaces;\nusing Bit.Core.Tools.SendFeatures.Queries.Interfaces;\nusing Bit.Core.Tools.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Tools.Controllers;\n\n[Route(\"sends\")]\npublic class SendsController : Controller\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly IUserService _userService;\n    private readonly ISendAuthorizationService _sendAuthorizationService;\n    private readonly ISendFileStorageService _sendFileStorageService;\n    private readonly IAnonymousSendCommand _anonymousSendCommand;\n    private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;\n    private readonly ISendOwnerQuery _sendOwnerQuery;\n    private readonly ILogger<SendsController> _logger;\n    private readonly IFeatureService _featureService;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;\n\n    public SendsController(\n        ISendRepository sendRepository,\n        IUserService userService,\n        ISendAuthorizationService sendAuthorizationService,\n        IAnonymousSendCommand anonymousSendCommand,\n        INonAnonymousSendCommand nonAnonymousSendCommand,\n        ISendOwnerQuery sendOwnerQuery,\n        ISendFileStorageService sendFileStorageService,\n        ILogger<SendsController> logger,\n        IFeatureService featureService,\n        IPushNotificationService pushNotificationService,\n        IHasPremiumAccessQuery hasPremiumAccessQuery\n    )\n    {\n        _sendRepository = sendRepository;\n        _userService = userService;\n        _sendAuthorizationService = sendAuthorizationService;\n        _anonymousSendCommand = anonymousSendCommand;\n        _nonAnonymousSendCommand = nonAnonymousSendCommand;\n        _sendOwnerQuery = sendOwnerQuery;\n        _sendFileStorageService = sendFileStorageService;\n        _logger = logger;\n        _featureService = featureService;\n        _pushNotificationService = pushNotificationService;\n        _hasPremiumAccessQuery = hasPremiumAccessQuery;\n    }\n\n    #region Anonymous endpoints\n\n    [AllowAnonymous]\n    [HttpPost(\"access/{id}\")]\n    public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestModel model)\n    {\n        // Uncomment whenever we want to require the `send-id` header\n        //if (!_currentContext.HttpContext.Request.Headers.ContainsKey(\"Send-Id\") ||\n        //    _currentContext.HttpContext.Request.Headers[\"Send-Id\"] != id)\n        //{\n        //    throw new BadRequestException(\"Invalid Send-Id header.\");\n        //}\n\n        var guid = new Guid(CoreHelpers.Base64UrlDecode(id));\n        var send = await _sendRepository.GetByIdAsync(guid);\n\n        if (send == null)\n        {\n            throw new BadRequestException(\"Could not locate send\");\n        }\n\n        if (send.AuthType == AuthType.Email && send.Emails is not null)\n        {\n            throw new NotFoundException();\n        }\n\n        var sendAuthResult =\n            await _sendAuthorizationService.AccessAsync(send, model.Password);\n        if (sendAuthResult.Equals(SendAccessResult.PasswordRequired))\n        {\n            return new UnauthorizedResult();\n        }\n\n        if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"Invalid password.\");\n        }\n\n        if (sendAuthResult.Equals(SendAccessResult.Denied))\n        {\n            throw new NotFoundException();\n        }\n\n        var sendResponse = new SendAccessResponseModel(send);\n        if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault())\n        {\n            var creator = await _userService.GetUserByIdAsync(send.UserId.Value);\n            sendResponse.CreatorIdentifier = creator.Email;\n        }\n\n        return new ObjectResult(sendResponse);\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"{encodedSendId}/access/file/{fileId}\")]\n    public async Task<IActionResult> GetSendFileDownloadData(string encodedSendId,\n        string fileId, [FromBody] SendAccessRequestModel model)\n    {\n        // Uncomment whenever we want to require the `send-id` header\n        //if (!_currentContext.HttpContext.Request.Headers.ContainsKey(\"Send-Id\") ||\n        //    _currentContext.HttpContext.Request.Headers[\"Send-Id\"] != encodedSendId)\n        //{\n        //    throw new BadRequestException(\"Invalid Send-Id header.\");\n        //}\n\n        var sendId = new Guid(CoreHelpers.Base64UrlDecode(encodedSendId));\n        var send = await _sendRepository.GetByIdAsync(sendId);\n\n        if (send == null)\n        {\n            throw new BadRequestException(\"Could not locate send\");\n        }\n\n        if (send.AuthType == AuthType.Email && send.Emails is not null)\n        {\n            throw new NotFoundException();\n        }\n\n        var (url, result) = await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId,\n            model.Password);\n\n        if (result.Equals(SendAccessResult.PasswordRequired))\n        {\n            return new UnauthorizedResult();\n        }\n\n        if (result.Equals(SendAccessResult.PasswordInvalid))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"Invalid password.\");\n        }\n\n        if (result.Equals(SendAccessResult.Denied))\n        {\n            throw new NotFoundException();\n        }\n\n        return new ObjectResult(new SendFileDownloadDataResponseModel() { Id = fileId, Url = url, });\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"file/validate/azure\")]\n    public async Task<ObjectResult> AzureValidateFile()\n    {\n        return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>\n        {\n            {\n                \"Microsoft.Storage.BlobCreated\", async (eventGridEvent) =>\n                {\n                    try\n                    {\n                        var blobName =\n                            eventGridEvent.Subject.Split($\"{AzureSendFileStorageService.FilesContainerName}/blobs/\")[1];\n                        var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);\n                        var send = await _sendRepository.GetByIdAsync(new Guid(sendId));\n                        if (send == null)\n                        {\n                            if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService)\n                            {\n                                await azureSendFileStorageService.DeleteBlobAsync(blobName);\n                            }\n\n                            return;\n                        }\n\n                        await _nonAnonymousSendCommand.ConfirmFileSize(send);\n                    }\n                    catch (Exception e)\n                    {\n                        _logger.LogError(e, \"Uncaught exception occurred while handling event grid event: {Event}\",\n                            JsonSerializer.Serialize(eventGridEvent));\n                        return;\n                    }\n                }\n            }\n        });\n    }\n\n    #endregion\n\n    #region Non-anonymous endpoints\n\n    [Authorize(Policies.Application)]\n    [HttpGet(\"{id}\")]\n    public async Task<SendResponseModel> Get(string id)\n    {\n        var sendId = new Guid(id);\n        var send = await _sendOwnerQuery.Get(sendId, User);\n        return new SendResponseModel(send);\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<SendResponseModel>> GetAll()\n    {\n        var sends = await _sendOwnerQuery.GetOwned(User);\n        var responses = sends.Select(s => new SendResponseModel(s));\n        var result = new ListResponseModel<SendResponseModel>(responses);\n\n        return result;\n    }\n\n    [Authorize(Policy = Policies.Send)]\n    [HttpPost(\"access/\")]\n    public async Task<IActionResult> AccessUsingAuth()\n    {\n        var guid = User.GetSendId();\n        var send = await _sendRepository.GetByIdAsync(guid);\n        if (send == null)\n        {\n            throw new BadRequestException(\"Could not locate send\");\n        }\n\n        /* This guard can be removed once feature flag is retired*/\n        var sendEmailOtpEnabled = _featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP);\n        if (!sendEmailOtpEnabled && send.AuthType == AuthType.Email && send.Emails is not null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!INonAnonymousSendCommand.SendCanBeAccessed(send))\n        {\n            throw new NotFoundException();\n        }\n\n        var sendResponse = new SendAccessResponseModel(send);\n        if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault())\n        {\n            var creator = await _userService.GetUserByIdAsync(send.UserId.Value);\n            sendResponse.CreatorIdentifier = creator.Email;\n        }\n\n        /*\n         * AccessCount is incremented differently for File and Text Send types:\n         * - Text Sends are incremented at every access\n         * - File Sends are incremented only when the file is downloaded\n         *\n         * Note that this endpoint is initially called for all Send types\n         */\n        if (send.Type == SendType.Text)\n        {\n            send.AccessCount++;\n            await _sendRepository.ReplaceAsync(send);\n            await _pushNotificationService.PushSyncSendUpdateAsync(send);\n        }\n\n        return new ObjectResult(sendResponse);\n    }\n\n    [Authorize(Policy = Policies.Send)]\n    [HttpPost(\"access/file/{fileId}\")]\n    public async Task<IActionResult> GetSendFileDownloadDataUsingAuth(string fileId)\n    {\n        var sendId = User.GetSendId();\n        var send = await _sendRepository.GetByIdAsync(sendId);\n\n        if (send == null)\n        {\n            throw new BadRequestException(\"Could not locate send\");\n        }\n\n        /* This guard can be removed once feature flag is retired*/\n        var sendEmailOtpEnabled = _featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP);\n        if (!sendEmailOtpEnabled && send.AuthType == AuthType.Email && send.Emails is not null)\n        {\n            throw new NotFoundException();\n        }\n\n        var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);\n\n        if (result.Equals(SendAccessResult.Denied))\n        {\n            throw new NotFoundException();\n        }\n\n        return new ObjectResult(new SendFileDownloadDataResponseModel() { Id = fileId, Url = url });\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpPost(\"\")]\n    public async Task<SendResponseModel> Post([FromBody] SendRequestModel model)\n    {\n        model.ValidateCreation();\n        var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException(\"User ID not found\");\n        var hasPremium = await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId);\n\n        if (!hasPremium && !string.IsNullOrWhiteSpace(model.Emails))\n        {\n            throw new BadRequestException(\"Email verified Sends require a premium membership\");\n        }\n\n        var send = model.ToSend(userId, _sendAuthorizationService);\n        await _nonAnonymousSendCommand.SaveSendAsync(send);\n        return new SendResponseModel(send);\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpPost(\"file/v2\")]\n    public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendRequestModel model)\n    {\n        if (model.Type != SendType.File)\n        {\n            throw new BadRequestException(\"Invalid content.\");\n        }\n\n        if (!model.FileLength.HasValue)\n        {\n            throw new BadRequestException(\"Invalid content. File size hint is required.\");\n        }\n\n        if (model.FileLength.Value > Constants.FileSize501mb)\n        {\n            throw new BadRequestException($\"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}.\");\n        }\n\n        model.ValidateCreation();\n        var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException(\"User ID not found\");\n        var hasPremium = await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId);\n\n        if (!hasPremium && !string.IsNullOrWhiteSpace(model.Emails))\n        {\n            throw new BadRequestException(\"Email verified Sends require a premium membership\");\n        }\n\n        var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService);\n        var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value);\n        return new SendFileUploadDataResponseModel\n        {\n            Url = uploadUrl,\n            FileUploadType = _sendFileStorageService.FileUploadType,\n            SendResponse = new SendResponseModel(send)\n        };\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpGet(\"{id}/file/{fileId}\")]\n    public async Task<SendFileUploadDataResponseModel> RenewFileUpload(string id, string fileId)\n    {\n        var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException(\"User ID not found\");\n        var sendId = new Guid(id);\n        var send = await _sendRepository.GetByIdAsync(sendId);\n        var fileData = JsonSerializer.Deserialize<SendFileData>(send?.Data ?? string.Empty);\n\n        if (send == null || send.Type != SendType.File || (send.UserId.HasValue && send.UserId.Value != userId) ||\n            !send.UserId.HasValue || fileData?.Id != fileId || fileData.Validated)\n        {\n            // Not found if Send isn't found, user doesn't have access, request is faulty,\n            // or we've already validated the file. This last is to emulate create-only blob permissions for Azure\n            throw new NotFoundException();\n        }\n\n        return new SendFileUploadDataResponseModel\n        {\n            Url = await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId),\n            FileUploadType = _sendFileStorageService.FileUploadType,\n            SendResponse = new SendResponseModel(send),\n        };\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpPost(\"{id}/file/{fileId}\")]\n    [SelfHosted(SelfHostedOnly = true)]\n    [RequestSizeLimit(Constants.FileSize501mb)]\n    [DisableFormValueModelBinding]\n    public async Task PostFileForExistingSend(string id, string fileId)\n    {\n        var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException(\"User ID not found\");\n        if (!Request?.ContentType?.Contains(\"multipart/\") ?? true)\n        {\n            throw new BadRequestException(\"Invalid content.\");\n        }\n\n        var send = await _sendRepository.GetByIdAsync(new Guid(id));\n        if (send == null || send.UserId != userId)\n        {\n            throw new NotFoundException();\n        }\n\n        await Request.GetFileAsync(async (stream) =>\n        {\n            await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send);\n        });\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpPut(\"{id}\")]\n    public async Task<SendResponseModel> Put(string id, [FromBody] SendRequestModel model)\n    {\n        model.ValidateEdit();\n        var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException(\"User ID not found\");\n        var hasPremium = await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId);\n\n        if (!hasPremium && !string.IsNullOrWhiteSpace(model.Emails))\n        {\n            throw new BadRequestException(\"Email verified Sends require a premium membership\");\n        }\n\n        var send = await _sendRepository.GetByIdAsync(new Guid(id));\n        if (send == null || send.UserId != userId)\n        {\n            throw new NotFoundException();\n        }\n\n        await _nonAnonymousSendCommand.SaveSendAsync(model.UpdateSend(send, _sendAuthorizationService));\n        return new SendResponseModel(send);\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpPut(\"{id}/remove-password\")]\n    public async Task<SendResponseModel> PutRemovePassword(string id)\n    {\n        return await this.PutRemoveAuth(id);\n    }\n\n    // Removes ALL authentication (email or password) if any is present\n    [Authorize(Policies.Application)]\n    [HttpPut(\"{id}/remove-auth\")]\n    public async Task<SendResponseModel> PutRemoveAuth(string id)\n    {\n        var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException(\"User ID not found\");\n        var send = await _sendRepository.GetByIdAsync(new Guid(id));\n        if (send == null || send.UserId != userId)\n        {\n            throw new NotFoundException();\n        }\n\n        send.Password = null;\n        send.Emails = null;\n        send.AuthType = AuthType.None;\n        await _nonAnonymousSendCommand.SaveSendAsync(send);\n        return new SendResponseModel(send);\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpDelete(\"{id}\")]\n    public async Task Delete(string id)\n    {\n        var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException(\"User ID not found\");\n        var send = await _sendRepository.GetByIdAsync(new Guid(id));\n        if (send == null || send.UserId != userId)\n        {\n            throw new NotFoundException();\n        }\n\n        await _nonAnonymousSendCommand.DeleteSendAsync(send);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/Request/Accounts/ImportCiphersRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Vault.Models.Request;\n\nnamespace Bit.Api.Tools.Models.Request.Accounts;\n\npublic class ImportCiphersRequestModel\n{\n    public FolderWithIdRequestModel[] Folders { get; set; }\n    public CipherRequestModel[] Ciphers { get; set; }\n    public KeyValuePair<int, int>[] FolderRelationships { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/Request/Organizations/ImportOrganizationCiphersRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Request;\nusing Bit.Api.Vault.Models.Request;\n\nnamespace Bit.Api.Tools.Models.Request.Organizations;\n\npublic class ImportOrganizationCiphersRequestModel\n{\n    public CollectionWithIdRequestModel[] Collections { get; set; }\n    public CipherRequestModel[] Ciphers { get; set; }\n    public KeyValuePair<int, int>[] CollectionRelationships { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/Request/SendAccessRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Tools.Models.Request;\n\npublic class SendAccessRequestModel\n{\n    [StringLength(300)]\n    public string Password { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/Request/SendRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json;\nusing Bit.Api.Tools.Utilities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Services;\nusing Bit.Core.Utilities;\n\nusing static System.StringSplitOptions;\n\nnamespace Bit.Api.Tools.Models.Request;\n\n/// <summary>\n/// A send request issued by a Bitwarden client\n/// </summary>\npublic class SendRequestModel\n{\n    /// <summary>\n    /// Indicates whether the send contains text or file data.\n    /// </summary>\n    public SendType Type { get; set; }\n\n    /// <summary>\n    /// Specifies the authentication method required to access this Send.\n    /// </summary>\n    public AuthType? AuthType { get; set; }\n\n    /// <summary>\n    /// Estimated length of the file accompanying the send. <see langword=\"null\"/> when\n    /// <see cref=\"Type\"/> is <see cref=\"SendType.Text\"/>.\n    /// </summary>\n    public long? FileLength { get; set; } = null;\n\n    /// <summary>\n    /// Label for the send.\n    /// </summary>\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Name { get; set; }\n\n    /// <summary>\n    /// Notes for the send. This is only visible to the owner of the send.\n    /// </summary>\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Notes { get; set; }\n\n    /// <summary>\n    /// A base64-encoded byte array containing the Send's encryption key. This key is\n    /// also provided to send recipients in the Send's URL.\n    /// </summary>\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Key { get; set; }\n\n    /// <summary>\n    /// The maximum number of times a send can be accessed before it expires.\n    /// When this value is <see langword=\"null\" />, there is no limit.\n    /// </summary>\n    [Range(1, int.MaxValue)]\n    public int? MaxAccessCount { get; set; }\n\n    /// <summary>\n    /// The date after which a send cannot be accessed. When this value is\n    /// <see langword=\"null\"/>, there is no expiration date.\n    /// </summary>\n    public DateTime? ExpirationDate { get; set; }\n\n    /// <summary>\n    /// The date after which a send may be automatically deleted from the server.\n    /// When this is <see langword=\"null\" />, the send may be deleted after it has\n    /// exceeded the global send timeout limit.\n    /// </summary>\n    [Required]\n    public DateTime? DeletionDate { get; set; }\n\n    /// <summary>\n    /// Contains file metadata uploaded with the send.\n    /// The file content is uploaded separately.\n    /// </summary>\n    public SendFileModel File { get; set; }\n\n    /// <summary>\n    /// Contains text data uploaded with the send.\n    /// </summary>\n    public SendTextModel Text { get; set; }\n\n    /// <summary>\n    /// Base64-encoded byte array of a password hash that grants access to the send.\n    /// Mutually exclusive with <see cref=\"Emails\"/>.\n    /// </summary>\n    [StringLength(1000)]\n    public string Password { get; set; }\n\n    /// <summary>\n    /// Comma-separated list of emails that may access the send using OTP\n    /// authentication. Mutually exclusive with <see cref=\"Password\"/>.\n    /// </summary>\n    [StringLength(4000)]\n    public string Emails { get; set; }\n\n    /// <summary>\n    /// When <see langword=\"true\"/>, send access is disabled.\n    /// Defaults to <see langword=\"false\"/>.\n    /// </summary>\n    [Required]\n    public bool? Disabled { get; set; }\n\n    /// <summary>\n    /// When <see langword=\"true\"/> send access hides the user's email address\n    /// and displays a confirmation message instead. Defaults to <see langword=\"false\"/>.\n    /// </summary>\n    public bool? HideEmail { get; set; }\n\n    /// <summary>\n    /// Transforms the request into a send object.\n    /// </summary>\n    /// <param name=\"userId\">The user that owns the send.</param>\n    /// <param name=\"sendAuthorizationService\">Hashes the send password.</param>\n    /// <returns>The send object</returns>\n    public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService)\n    {\n        var send = new Send\n        {\n            Type = Type,\n            UserId = (Guid?)userId\n        };\n        send = UpdateSend(send, sendAuthorizationService);\n        return send;\n    }\n\n    /// <summary>\n    /// Transforms the request into a send object and file data.\n    /// </summary>\n    /// <param name=\"userId\">The user that owns the send.</param>\n    /// <param name=\"fileName\">Name of the file uploaded with the send.</param>\n    /// <param name=\"sendAuthorizationService\">Hashes the send password.</param>\n    /// <returns>The send object and file data.</returns>\n    public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendAuthorizationService sendAuthorizationService)\n    {\n        // FIXME: This method does two things: creates a send and a send file data.\n        //        It should only do one thing.\n        var send = ToSendBase(new Send\n        {\n            Type = Type,\n            UserId = (Guid?)userId\n        }, sendAuthorizationService);\n        var data = new SendFileData(Name, Notes, fileName);\n        return (send, data);\n    }\n\n    /// <summary>\n    /// Update a send object with request content\n    /// </summary>\n    /// <param name=\"existingSend\">The send to update</param>\n    /// <param name=\"sendAuthorizationService\">Hashes the send password.</param>\n    /// <returns>The send object</returns>\n    public Send UpdateSend(Send existingSend, ISendAuthorizationService sendAuthorizationService)\n    {\n        existingSend = ToSendBase(existingSend, sendAuthorizationService);\n        switch (existingSend.Type)\n        {\n            case SendType.File:\n                var fileData = JsonSerializer.Deserialize<SendFileData>(existingSend.Data);\n                fileData.Name = Name;\n                fileData.Notes = Notes;\n                existingSend.Data = JsonSerializer.Serialize(fileData, JsonHelpers.IgnoreWritingNull);\n                break;\n            case SendType.Text:\n                existingSend.Data = JsonSerializer.Serialize(ToSendTextData(), JsonHelpers.IgnoreWritingNull);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported type: \" + nameof(Type) + \".\");\n        }\n        return existingSend;\n    }\n\n    /// <summary>\n    /// Validates that the request is internally consistent for send creation.\n    /// </summary>\n    /// <exception cref=\"BadRequestException\">\n    /// Thrown when the send's expiration date has already expired.\n    /// </exception>\n    public void ValidateCreation()\n    {\n        var now = DateTime.UtcNow;\n        // Add 1 minute for a sane buffer and client clock float\n        var nowPlus1Minute = now.AddMinutes(1);\n        if (ExpirationDate.HasValue && ExpirationDate.Value <= nowPlus1Minute)\n        {\n            throw new BadRequestException(\"You cannot create a Send that is already expired. \" +\n                \"Adjust the expiration date and try again.\");\n        }\n        ValidateEdit();\n    }\n\n    /// <summary>\n    /// Validates that the request is internally consistent for send administration.\n    /// </summary>\n    /// <exception cref=\"BadRequestException\">\n    /// Thrown when the send's deletion date has already expired or when its\n    /// expiration occurs after its deletion.\n    /// </exception>\n    public void ValidateEdit()\n    {\n        var now = DateTime.UtcNow;\n        // Add 1 minute for a sane buffer and client clock float\n        var nowPlus1Minute = now.AddMinutes(1);\n        if (DeletionDate.HasValue)\n        {\n            if (DeletionDate.Value <= nowPlus1Minute)\n            {\n                throw new BadRequestException(\"You cannot have a Send with a deletion date in the past. \" +\n                    \"Adjust the deletion date and try again.\");\n            }\n            if (DeletionDate.Value > now.AddDays(31))\n            {\n                throw new BadRequestException(\"You cannot have a Send with a deletion date that far \" +\n                    \"into the future. Adjust the Deletion Date to a value less than 31 days from now \" +\n                    \"and try again.\");\n            }\n        }\n        if (ExpirationDate.HasValue)\n        {\n            if (ExpirationDate.Value <= nowPlus1Minute)\n            {\n                throw new BadRequestException(\"You cannot have a Send with an expiration date in the past. \" +\n                    \"Adjust the expiration date and try again.\");\n            }\n            if (ExpirationDate.Value > DeletionDate.Value)\n            {\n                throw new BadRequestException(\"You cannot have a Send with an expiration date greater than the deletion date. \" +\n                    \"Adjust the expiration date and try again.\");\n            }\n        }\n    }\n\n    private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizationService)\n    {\n        existingSend.Key = Key;\n        existingSend.ExpirationDate = ExpirationDate;\n        existingSend.DeletionDate = DeletionDate.Value;\n        existingSend.MaxAccessCount = MaxAccessCount;\n        existingSend.Disabled = Disabled.GetValueOrDefault();\n        existingSend.HideEmail = HideEmail.GetValueOrDefault();\n\n        if (existingSend.AuthType == Core.Tools.Enums.AuthType.Password &&\n            AuthType == Core.Tools.Enums.AuthType.Password)\n        {\n            // when password protected Sends are edited, DO NOT re-hash the existing password hash\n            return existingSend;\n        }\n\n        if (AuthType != null)\n        {\n            existingSend.AuthType = AuthType;\n            switch (AuthType)\n            {\n                case Core.Tools.Enums.AuthType.Email:\n                    var emails = string.IsNullOrWhiteSpace(Emails) ? [] : Emails.Split(',', RemoveEmptyEntries | TrimEntries);\n                    existingSend.Emails = string.Join(\",\", emails);\n                    existingSend.Password = null;\n                    break;\n                case Core.Tools.Enums.AuthType.Password:\n                    existingSend.Password = authorizationService.HashPassword(Password);\n                    existingSend.Emails = null;\n                    break;\n                case Core.Tools.Enums.AuthType.None:\n                case null:\n                    existingSend.Emails = null;\n                    existingSend.Password = null;\n                    break;\n                default:\n                    throw new BadRequestException(\"You cannot save a Send having an invalid AuthType\");\n            }\n        }\n        /* FIXME: Remove after two releases of clients\n        // This supports clients that do not send an AuthType in the request,\n        // but does not fully support a user changing the AuthType in the UI.\n        // Specifically a password protected Send can't directly change AuthType to None using this logic.\n        // They can change to AuthType.Email, and then AuthType.None.\n        */\n        else\n        {\n            if (!string.IsNullOrWhiteSpace(Emails))\n            {\n                // normalize encoding\n                var emails = Emails.Split(',', RemoveEmptyEntries | TrimEntries);\n                existingSend.Emails = string.Join(\",\", emails);\n                existingSend.Password = null;\n            }\n            else if (!string.IsNullOrWhiteSpace(Password))\n            {\n                existingSend.Password = authorizationService.HashPassword(Password);\n                existingSend.Emails = null;\n            }\n            else if (existingSend.AuthType == Core.Tools.Enums.AuthType.Email)\n            {\n                existingSend.Emails = null;\n                existingSend.Password = null;\n            }\n            existingSend.AuthType = SendUtilities.InferAuthType(existingSend);\n        }\n\n\n        return existingSend;\n    }\n\n    private SendTextData ToSendTextData()\n    {\n        return new SendTextData(Name, Notes, Text.Text, Text.Hidden);\n    }\n}\n\n/// <summary>\n/// A send request issued by a Bitwarden client\n/// </summary>\npublic class SendWithIdRequestModel : SendRequestModel\n{\n    /// <summary>\n    /// Identifies the send. When this is <see langword=\"null\" />, the client is requesting\n    /// a new send.\n    /// </summary>\n    [Required]\n    public Guid? Id { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Response;\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Tools.Models.Response;\n\npublic class OrganizationExportResponseModel : ResponseModel\n{\n    public OrganizationExportResponseModel() : base(\"organizationExport\")\n    {\n    }\n\n    public OrganizationExportResponseModel(IEnumerable<CipherOrganizationDetailsWithCollections> ciphers,\n        IEnumerable<Collection> collections, GlobalSettings globalSettings) : this()\n    {\n        Ciphers = ciphers.Select(c => new CipherMiniDetailsResponseModel(c, globalSettings));\n        Collections = collections.Select(c => new CollectionResponseModel(c));\n    }\n\n    public IEnumerable<CollectionResponseModel> Collections { get; set; }\n\n    public IEnumerable<CipherMiniDetailsResponseModel> Ciphers { get; set; }\n}\n\n[Obsolete(\"This version is for backwards compatibility for client version 2022.9.0\")]\npublic class OrganizationExportListResponseModel\n{\n    public ListResponseModel<CollectionResponseModel> Collections { get; set; }\n\n    public ListResponseModel<CipherMiniDetailsResponseModel> Ciphers { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/Response/SendAccessResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Tools.Models.Response;\n\n/// <summary>\n/// A response issued to a Bitwarden client in response to access operations.\n/// </summary>\npublic class SendAccessResponseModel : ResponseModel\n{\n    /// <summary>\n    /// Instantiates a send access response model\n    /// </summary>\n    /// <param name=\"send\">Content to transmit to the client.</param>\n    /// <exception cref=\"ArgumentNullException\">\n    /// Thrown when <paramref name=\"send\"/> is <see langword=\"null\" />\n    /// </exception>\n    /// <exception cref=\"ArgumentException\">\n    /// Thrown when <paramref name=\"send\" /> has an invalid <see cref=\"Send.Type\"/>.\n    /// </exception>\n    public SendAccessResponseModel(Send send)\n        : base(\"send-access\")\n    {\n        if (send == null)\n        {\n            throw new ArgumentNullException(nameof(send));\n        }\n\n        Id = CoreHelpers.Base64UrlEncode(send.Id.ToByteArray());\n        Type = send.Type;\n        AuthType = send.AuthType;\n\n        SendData sendData;\n        switch (send.Type)\n        {\n            case SendType.File:\n                var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);\n                sendData = fileData;\n                File = new SendFileModel(fileData);\n                break;\n            case SendType.Text:\n                var textData = JsonSerializer.Deserialize<SendTextData>(send.Data);\n                sendData = textData;\n                Text = new SendTextModel(textData);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported \" + nameof(Type) + \".\");\n        }\n\n        Name = sendData.Name;\n        ExpirationDate = send.ExpirationDate;\n    }\n\n    /// <summary>\n    /// Identifies the send in a send URL\n    /// </summary>\n    public string Id { get; set; }\n\n    /// <summary>\n    /// Indicates whether the send contains text or file data.\n    /// </summary>\n    public SendType Type { get; set; }\n\n    /// <summary>\n    /// Specifies the authentication method required to access this Send.\n    /// </summary>\n    public AuthType? AuthType { get; set; }\n\n    /// <summary>\n    /// Label for the send. This is only visible to the owner of the send.\n    /// </summary>\n    /// <remarks>\n    /// This field contains a base64-encoded byte array. The array contains\n    /// the E2E-encrypted encrypted content.\n    /// </remarks>\n    public string Name { get; set; }\n\n    /// <summary>\n    /// Describes the file attached to the send.\n    /// </summary>\n    /// <remarks>\n    /// File content is downloaded separately using\n    /// <see cref=\"Bit.Api.Tools.Controllers.SendsController.GetSendFileDownloadData\" />\n    /// </remarks>\n    public SendFileModel File { get; set; }\n\n    /// <summary>\n    /// Contains text data uploaded with the send.\n    /// </summary>\n    public SendTextModel Text { get; set; }\n\n    /// <summary>\n    /// The date after which a send cannot be accessed. When this value is\n    /// <see langword=\"null\"/>, there is no expiration date.\n    /// </summary>\n    public DateTime? ExpirationDate { get; set; }\n\n    /// <summary>\n    /// Indicates the person that created the send to the accessor.\n    /// </summary>\n    public string CreatorIdentifier { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/Response/SendFileDownloadDataResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Tools.Models.Response;\n\npublic class SendFileDownloadDataResponseModel : ResponseModel\n{\n    public string Id { get; set; }\n    public string Url { get; set; }\n\n    public SendFileDownloadDataResponseModel() : base(\"send-fileDownload\") { }\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/Response/SendFileUploadDataResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Tools.Models.Response;\n\npublic class SendFileUploadDataResponseModel : ResponseModel\n{\n    public SendFileUploadDataResponseModel() : base(\"send-fileUpload\") { }\n\n    public string Url { get; set; }\n    public FileUploadType FileUploadType { get; set; }\n    public SendResponseModel SendResponse { get; set; }\n\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/Response/SendResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Api.Tools.Utilities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Tools.Models.Response;\n\n/// <summary>\n/// A response issued to a Bitwarden client in response to ownership operations.\n/// </summary>\n/// <seealso cref=\"SendAccessResponseModel\" />\npublic class SendResponseModel : ResponseModel\n{\n    /// <summary>\n    /// Instantiates a send response model\n    /// </summary>\n    /// <param name=\"send\">Content to transmit to the client.</param>\n    /// <exception cref=\"ArgumentNullException\">\n    /// Thrown when <paramref name=\"send\"/> is <see langword=\"null\" />\n    /// </exception>\n    /// <exception cref=\"ArgumentException\">\n    /// Thrown when <paramref name=\"send\" /> has an invalid <see cref=\"Send.Type\"/>.\n    /// </exception>\n    public SendResponseModel(Send send)\n        : base(\"send\")\n    {\n        if (send == null)\n        {\n            throw new ArgumentNullException(nameof(send));\n        }\n\n        Id = send.Id;\n        AccessId = CoreHelpers.Base64UrlEncode(send.Id.ToByteArray());\n        Type = send.Type;\n        AuthType = send.AuthType ?? SendUtilities.InferAuthType(send);\n        Key = send.Key;\n        MaxAccessCount = send.MaxAccessCount;\n        AccessCount = send.AccessCount;\n        RevisionDate = send.RevisionDate;\n        ExpirationDate = send.ExpirationDate;\n        DeletionDate = send.DeletionDate;\n        Password = send.Password;\n        Emails = send.Emails;\n        Disabled = send.Disabled;\n        HideEmail = send.HideEmail.GetValueOrDefault();\n\n        SendData sendData;\n        switch (send.Type)\n        {\n            case SendType.File:\n                var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);\n                sendData = fileData;\n                File = new SendFileModel(fileData);\n                break;\n            case SendType.Text:\n                var textData = JsonSerializer.Deserialize<SendTextData>(send.Data);\n                sendData = textData;\n                Text = new SendTextModel(textData);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported \" + nameof(Type) + \".\");\n        }\n\n        Name = sendData.Name;\n        Notes = sendData.Notes;\n    }\n\n    /// <summary>\n    /// Identifies the send to its owner\n    /// </summary>\n    public Guid Id { get; set; }\n\n    /// <summary>\n    /// Identifies the send in a send URL\n    /// </summary>\n    public string AccessId { get; set; }\n\n    /// <summary>\n    /// Indicates whether the send contains text or file data.\n    /// </summary>\n    public SendType Type { get; set; }\n\n    /// <summary>\n    /// Specifies the authentication method required to access this Send.\n    /// </summary>\n    public AuthType? AuthType { get; set; }\n\n    /// <summary>\n    /// Label for the send.\n    /// </summary>\n    /// <remarks>\n    /// This field contains a base64-encoded byte array. The array contains\n    /// the E2E-encrypted encrypted content.\n    /// </remarks>\n    public string Name { get; set; }\n\n    /// <summary>\n    /// Notes for the send. This is only visible to the owner of the send.\n    /// This field is encrypted.\n    /// </summary>\n    /// <remarks>\n    /// This field contains a base64-encoded byte array. The array contains\n    /// the E2E-encrypted  encrypted content.\n    /// </remarks>\n    public string Notes { get; set; }\n\n    /// <summary>\n    /// Contains file metadata uploaded with the send.\n    /// The file content is uploaded separately.\n    /// </summary>\n    public SendFileModel File { get; set; }\n\n    /// <summary>\n    /// Contains text data uploaded with the send.\n    /// </summary>\n    public SendTextModel Text { get; set; }\n\n    /// <summary>\n    /// A base64-encoded byte array containing the Send's encryption key.\n    /// It's also provided to send recipients in the Send's URL.\n    /// </summary>\n    /// <remarks>\n    /// This field contains a base64-encoded byte array. The array contains\n    /// the E2E-encrypted content.\n    /// </remarks>\n    public string Key { get; set; }\n\n    /// <summary>\n    /// The maximum number of times a send can be accessed before it expires.\n    /// When this value is <see langword=\"null\" />, there is no limit.\n    /// </summary>\n    public int? MaxAccessCount { get; set; }\n\n    /// <summary>\n    /// The number of times a send has been accessed since it was created.\n    /// </summary>\n    public int AccessCount { get; set; }\n\n    /// <summary>\n    /// Base64-encoded byte array of a password hash that grants access to the send.\n    /// Mutually exclusive with <see cref=\"Emails\"/>.\n    /// </summary>\n    public string Password { get; set; }\n\n    /// <summary>\n    /// Comma-separated list of emails that may access the send using OTP\n    /// authentication. Mutually exclusive with <see cref=\"Password\"/>.\n    /// </summary>\n    public string Emails { get; set; }\n\n    /// <summary>\n    /// When <see langword=\"true\"/>, send access is disabled.\n    /// </summary>\n    public bool Disabled { get; set; }\n\n    /// <summary>\n    /// The last time this send's data changed.\n    /// </summary>\n    public DateTime RevisionDate { get; set; }\n\n    /// <summary>\n    /// The date after which a send cannot be accessed. When this value is\n    /// <see langword=\"null\"/>, there is no expiration date.\n    /// </summary>\n    public DateTime? ExpirationDate { get; set; }\n\n    /// <summary>\n    /// The date after which a send may be automatically deleted from the server.\n    /// </summary>\n    public DateTime DeletionDate { get; set; }\n\n    /// <summary>\n    /// When <see langword=\"true\"/> send access hides the user's email address\n    /// and displays a confirmation message instead.\n    /// </summary>\n    public bool HideEmail { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/SendFileModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Tools.Models;\n\npublic class SendFileModel\n{\n    public SendFileModel() { }\n\n    public SendFileModel(SendFileData data)\n    {\n        Id = data.Id;\n        FileName = data.FileName;\n        Size = data.Size;\n        SizeName = CoreHelpers.ReadableBytesSize(data.Size);\n    }\n\n    public string Id { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string FileName { get; set; }\n    [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]\n    public long? Size { get; set; }\n    public string SizeName { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Tools/Models/SendTextModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Api.Tools.Models;\n\npublic class SendTextModel\n{\n    public SendTextModel() { }\n\n    public SendTextModel(SendTextData data)\n    {\n        Text = data.Text;\n        Hidden = data.Hidden;\n    }\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Text { get; set; }\n    public bool Hidden { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Tools/Utilities/InferAuthType.cs",
    "content": "﻿namespace Bit.Api.Tools.Utilities;\n\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\n\npublic class SendUtilities\n{\n    public static AuthType InferAuthType(Send send)\n    {\n        if (!string.IsNullOrWhiteSpace(send.Password))\n        {\n            return AuthType.Password;\n        }\n\n        if (!string.IsNullOrWhiteSpace(send.Emails))\n        {\n            return AuthType.Email;\n        }\n\n        return AuthType.None;\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/ApiExplorerGroupConvention.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Mvc.ApplicationModels;\n\nnamespace Bit.Api.Utilities;\n\npublic class ApiExplorerGroupConvention : IControllerModelConvention\n{\n    public void Apply(ControllerModel controller)\n    {\n        var controllerNamespace = controller.ControllerType.Namespace;\n        controller.ApiExplorer.GroupName = controllerNamespace.Contains(\".Public.\") ? \"public\" : \"internal\";\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/ApiHelpers.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Azure.Messaging.EventGrid;\nusing Azure.Messaging.EventGrid.SystemEvents;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Utilities;\n\npublic static class ApiHelpers\n{\n    public static string EventGridKey { get; set; }\n    public async static Task<T> ReadJsonFileFromBody<T>(HttpContext httpContext, IFormFile file, long maxSize = 51200)\n    {\n        T obj = default(T);\n        if (file != null && httpContext.Request.ContentLength.HasValue && httpContext.Request.ContentLength.Value <= maxSize)\n        {\n            try\n            {\n                using var stream = file.OpenReadStream();\n                obj = await JsonSerializer.DeserializeAsync<T>(stream, JsonHelpers.IgnoreCase);\n            }\n            catch { }\n        }\n\n        return obj;\n    }\n\n    /// <summary>\n    /// Validates Azure event subscription and calls the appropriate event handler. Responds HttpOk.\n    /// </summary>\n    /// <param name=\"request\">HttpRequest received from Azure</param>\n    /// <param name=\"eventTypeHandlers\">Dictionary of eventType strings and their associated handlers.</param>\n    /// <returns>OkObjectResult</returns>\n    /// <remarks>Reference https://docs.microsoft.com/en-us/azure/event-grid/receive-events</remarks>\n    public async static Task<ObjectResult> HandleAzureEvents(HttpRequest request,\n        Dictionary<string, Func<EventGridEvent, Task>> eventTypeHandlers)\n    {\n        var queryKey = request.Query[\"key\"];\n\n        if (!CoreHelpers.FixedTimeEquals(queryKey, EventGridKey))\n        {\n            return new UnauthorizedObjectResult(\"Authentication failed. Please use a valid key.\");\n        }\n\n        var response = string.Empty;\n        var requestData = await BinaryData.FromStreamAsync(request.Body);\n        var eventGridEvents = EventGridEvent.ParseMany(requestData);\n        foreach (var eventGridEvent in eventGridEvents)\n        {\n            if (eventGridEvent.TryGetSystemEventData(out object systemEvent))\n            {\n                if (systemEvent is SubscriptionValidationEventData eventData)\n                {\n                    // Might want to enable additional validation: subject, topic etc.\n                    var responseData = new SubscriptionValidationResponse()\n                    {\n                        ValidationResponse = eventData.ValidationCode\n                    };\n\n                    return new OkObjectResult(responseData);\n                }\n            }\n\n            if (eventTypeHandlers.TryGetValue(eventGridEvent.EventType, out var eventTypeHandler))\n            {\n                await eventTypeHandler(eventGridEvent);\n            }\n        }\n\n        return new OkObjectResult(response);\n    }\n\n    /// <summary>\n    /// Validates and returns a date range. Currently used for fetching events.\n    /// </summary>\n    /// <param name=\"start\">start date and time</param>\n    /// <param name=\"end\">end date and time</param>\n    /// <remarks>\n    /// If start or end are null, will return a range of the last 30 days.\n    /// If a time span greater than 367 days is passed will throw BadRequestException.\n    /// </remarks>\n    public static Tuple<DateTime, DateTime> GetDateRange(DateTime? start, DateTime? end)\n    {\n        if (!end.HasValue || !start.HasValue)\n        {\n            end = DateTime.UtcNow.Date.AddDays(1).AddMilliseconds(-1);\n            start = DateTime.UtcNow.Date.AddDays(-30);\n        }\n        else if (start.Value > end.Value)\n        {\n            var newEnd = start;\n            start = end;\n            end = newEnd;\n        }\n\n        if ((end.Value - start.Value) > TimeSpan.FromDays(367))\n        {\n            throw new BadRequestException(\"Range too large.\");\n        }\n\n        return new Tuple<DateTime, DateTime>(start.Value, end.Value);\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/DiagnosticTools/EventDiagnosticLogger.cs",
    "content": "﻿using Bit.Api.Dirt.Public.Models;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core;\nusing Bit.Core.Services;\n\nnamespace Bit.Api.Utilities.DiagnosticTools;\n\npublic static class EventDiagnosticLogger\n{\n    public static void LogAggregateData(\n        this ILogger logger,\n        IFeatureService featureService,\n        Guid organizationId,\n        PagedListResponseModel<EventResponseModel> data, EventFilterRequestModel request)\n    {\n        try\n        {\n            if (!featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging))\n            {\n                return;\n            }\n\n            var orderedRecords = data.Data.OrderBy(e => e.Date).ToList();\n            var recordCount = orderedRecords.Count;\n            var newestRecordDate = orderedRecords.LastOrDefault()?.Date.ToString(\"o\");\n            var oldestRecordDate = orderedRecords.FirstOrDefault()?.Date.ToString(\"o\"); ;\n            var hasMore = !string.IsNullOrEmpty(data.ContinuationToken);\n\n            logger.LogInformation(\n                \"Events query for Organization:{OrgId}. Event count:{Count} newest record:{newestRecord}  oldest record:{oldestRecord} HasMore:{HasMore} \" +\n                \"Request Filters Start:{QueryStart} End:{QueryEnd} ActingUserId:{ActingUserId} ItemId:{ItemId},\",\n                organizationId,\n                recordCount,\n                newestRecordDate,\n                oldestRecordDate,\n                hasMore,\n                request.Start?.ToString(\"o\"),\n                request.End?.ToString(\"o\"),\n                request.ActingUserId,\n                request.ItemId);\n        }\n        catch (Exception exception)\n        {\n            logger.LogWarning(exception, \"Unexpected exception from EventDiagnosticLogger.LogAggregateData\");\n        }\n    }\n\n    public static void LogAggregateData(\n        this ILogger logger,\n        IFeatureService featureService,\n        Guid organizationId,\n        IEnumerable<Dirt.Models.Response.EventResponseModel> data,\n        string? continuationToken,\n        DateTime? queryStart = null,\n        DateTime? queryEnd = null)\n    {\n\n        try\n        {\n            if (!featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging))\n            {\n                return;\n            }\n\n            var orderedRecords = data.OrderBy(e => e.Date).ToList();\n            var recordCount = orderedRecords.Count;\n            var newestRecordDate = orderedRecords.LastOrDefault()?.Date.ToString(\"o\");\n            var oldestRecordDate = orderedRecords.FirstOrDefault()?.Date.ToString(\"o\"); ;\n            var hasMore = !string.IsNullOrEmpty(continuationToken);\n\n            logger.LogInformation(\n                \"Events query for Organization:{OrgId}. Event count:{Count} newest record:{newestRecord}  oldest record:{oldestRecord} HasMore:{HasMore} \" +\n                \"Request Filters Start:{QueryStart} End:{QueryEnd}\",\n                organizationId,\n                recordCount,\n                newestRecordDate,\n                oldestRecordDate,\n                hasMore,\n                queryStart?.ToString(\"o\"),\n                queryEnd?.ToString(\"o\"));\n        }\n        catch (Exception exception)\n        {\n            logger.LogWarning(exception, \"Unexpected exception from EventDiagnosticLogger.LogAggregateData\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/DisableFormValueModelBindingAttribute.cs",
    "content": "﻿using Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Api.Utilities;\n\n[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]\npublic class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter\n{\n    public void OnResourceExecuting(ResourceExecutingContext context)\n    {\n        var factories = context.ValueProviderFactories;\n        factories.RemoveType<FormValueProviderFactory>();\n        factories.RemoveType<FormFileValueProviderFactory>();\n        factories.RemoveType<JQueryFormValueProviderFactory>();\n    }\n\n    public void OnResourceExecuted(ResourceExecutedContext context)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/EnumMatchesAttribute.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Utilities;\n\npublic class EnumMatchesAttribute<T>(params T[] accepted) : ValidationAttribute\n    where T : Enum\n{\n    public override bool IsValid(object value)\n    {\n        if (value == null || accepted == null || accepted.Length == 0)\n        {\n            return false;\n        }\n\n        var success = Enum.TryParse(typeof(T), value.ToString(), out var result);\n\n        if (!success)\n        {\n            return false;\n        }\n\n        var typed = (T)result;\n\n        return accepted.Contains(typed);\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/ExceptionHandlerFilterAttribute.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.Billing;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.IdentityModel.Tokens;\nusing Stripe;\nusing InternalApi = Bit.Core.Models.Api;\n\nnamespace Bit.Api.Utilities;\n\npublic class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute\n{\n    private readonly bool _publicApi;\n\n    public ExceptionHandlerFilterAttribute(bool publicApi)\n    {\n        _publicApi = publicApi;\n    }\n\n    public override void OnException(ExceptionContext context)\n    {\n        var errorMessage = \"An error has occurred.\";\n\n        var exception = context.Exception;\n        if (exception == null)\n        {\n            // Should never happen.\n            return;\n        }\n\n        ErrorResponseModel publicErrorModel = null;\n        InternalApi.ErrorResponseModel internalErrorModel = null;\n        if (exception is BadRequestException badRequestException)\n        {\n            context.HttpContext.Response.StatusCode = 400;\n            if (badRequestException.ModelState != null)\n            {\n                if (_publicApi)\n                {\n                    publicErrorModel = new ErrorResponseModel(badRequestException.ModelState);\n                }\n                else\n                {\n                    internalErrorModel = new InternalApi.ErrorResponseModel(badRequestException.ModelState);\n                }\n            }\n            else\n            {\n                errorMessage = badRequestException.Message;\n            }\n        }\n        else if (exception is StripeException { StripeError.Type: \"card_error\" } stripeCardErrorException)\n        {\n            context.HttpContext.Response.StatusCode = 400;\n            if (_publicApi)\n            {\n                publicErrorModel = new ErrorResponseModel(stripeCardErrorException.StripeError.Param,\n                    stripeCardErrorException.Message);\n            }\n            else\n            {\n                internalErrorModel = new InternalApi.ErrorResponseModel(stripeCardErrorException.StripeError.Param,\n                    stripeCardErrorException.Message);\n            }\n        }\n        else if (exception is GatewayException)\n        {\n            errorMessage = exception.Message;\n            context.HttpContext.Response.StatusCode = 400;\n        }\n        else if (exception is BillingException billingException)\n        {\n            errorMessage = billingException.Response;\n            context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;\n        }\n        else if (exception is StripeException stripeException)\n        {\n            var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();\n\n            var error = stripeException.Message;\n\n            if (stripeException.StripeError != null)\n            {\n                var stringBuilder = new StringBuilder();\n\n                if (!string.IsNullOrEmpty(stripeException.StripeError.Code))\n                {\n                    stringBuilder.Append($\"{stripeException.StripeError.Code} | \");\n                }\n\n                stringBuilder.Append(stripeException.StripeError.Message);\n\n                if (!string.IsNullOrEmpty(stripeException.StripeError.DocUrl))\n                {\n                    stringBuilder.Append($\" > {stripeException.StripeError.DocUrl}\");\n                }\n\n                error = stringBuilder.ToString();\n            }\n\n            logger.LogError(\"An unhandled error occurred while communicating with Stripe: {Error}\", error);\n            errorMessage = \"Something went wrong with your request. Please contact support.\";\n            context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;\n        }\n        else if (exception is NotSupportedException && !string.IsNullOrWhiteSpace(exception.Message))\n        {\n            errorMessage = exception.Message;\n            context.HttpContext.Response.StatusCode = 400;\n        }\n        else if (exception is ApplicationException)\n        {\n            context.HttpContext.Response.StatusCode = 402;\n        }\n        else if (exception is NotFoundException)\n        {\n            errorMessage = \"Resource not found.\";\n            context.HttpContext.Response.StatusCode = 404;\n        }\n        else if (exception is SecurityTokenValidationException)\n        {\n            errorMessage = \"Invalid token.\";\n            context.HttpContext.Response.StatusCode = 403;\n        }\n        else if (exception is UnauthorizedAccessException)\n        {\n            errorMessage = \"Unauthorized.\";\n            context.HttpContext.Response.StatusCode = 401;\n        }\n        else if (exception is ConflictException)\n        {\n            errorMessage = exception.Message;\n            context.HttpContext.Response.StatusCode = 409;\n        }\n        else if (exception is AggregateException aggregateException)\n        {\n            context.HttpContext.Response.StatusCode = 400;\n            var errorValues = aggregateException.InnerExceptions.Select(ex => ex.Message);\n            if (_publicApi)\n            {\n                publicErrorModel = new ErrorResponseModel(errorMessage, errorValues);\n            }\n            else\n            {\n                internalErrorModel = new InternalApi.ErrorResponseModel(errorMessage, errorValues);\n            }\n        }\n        else\n        {\n            var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();\n            logger.LogError(0, exception, \"Unhandled exception\");\n            errorMessage = \"An unhandled server error has occurred.\";\n            context.HttpContext.Response.StatusCode = 500;\n        }\n\n        if (_publicApi)\n        {\n            var errorModel = publicErrorModel ?? new ErrorResponseModel(errorMessage);\n            context.Result = new ObjectResult(errorModel);\n        }\n        else\n        {\n            var errorModel = internalErrorModel ?? new InternalApi.ErrorResponseModel(errorMessage);\n            var env = context.HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>();\n            if (env.IsDevelopment())\n            {\n                errorModel.ExceptionMessage = exception.Message;\n                errorModel.ExceptionStackTrace = exception.StackTrace;\n                errorModel.InnerExceptionMessage = exception?.InnerException?.Message;\n            }\n            context.Result = new ObjectResult(errorModel);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/ModelStateValidationFilterAttribute.cs",
    "content": "﻿using Bit.Api.Models.Public.Response;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing InternalApi = Bit.Core.Models.Api;\n\nnamespace Bit.Api.Utilities;\n\npublic class ModelStateValidationFilterAttribute : SharedWeb.Utilities.ModelStateValidationFilterAttribute\n{\n    private readonly bool _publicApi;\n\n    public ModelStateValidationFilterAttribute(bool publicApi)\n    {\n        _publicApi = publicApi;\n    }\n\n    protected override void OnModelStateInvalid(ActionExecutingContext context)\n    {\n        if (_publicApi)\n        {\n            context.Result = new BadRequestObjectResult(new ErrorResponseModel(context.ModelState));\n        }\n        else\n        {\n            context.Result = new BadRequestObjectResult(new InternalApi.ErrorResponseModel(context.ModelState));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/MultipartFormDataHelper.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Api.Tools.Models.Request;\nusing Microsoft.AspNetCore.Http.Features;\nusing Microsoft.AspNetCore.WebUtilities;\nusing Microsoft.Extensions.Primitives;\nusing Microsoft.Net.Http.Headers;\n\nnamespace Bit.Api.Utilities;\n\npublic static class MultipartFormDataHelper\n{\n    private static readonly FormOptions _defaultFormOptions = new FormOptions();\n\n    public static async Task GetFileAsync(this HttpRequest request, Func<Stream, string, string, Task> callback)\n    {\n        var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),\n            _defaultFormOptions.MultipartBoundaryLengthLimit);\n        var reader = new MultipartReader(boundary, request.Body);\n\n        var firstSection = await reader.ReadNextSectionAsync();\n        if (firstSection != null)\n        {\n            if (ContentDispositionHeaderValue.TryParse(firstSection.ContentDisposition, out var firstContent))\n            {\n                if (HasFileContentDisposition(firstContent))\n                {\n                    // Old style with just data\n                    var fileName = HeaderUtilities.RemoveQuotes(firstContent.FileName).ToString();\n                    using (firstSection.Body)\n                    {\n                        await callback(firstSection.Body, fileName, null);\n                    }\n                }\n                else if (HasDispositionName(firstContent, \"key\"))\n                {\n                    // New style with key, then data\n                    string key = null;\n                    using (var sr = new StreamReader(firstSection.Body))\n                    {\n                        key = await sr.ReadToEndAsync();\n                    }\n\n                    var secondSection = await reader.ReadNextSectionAsync();\n                    if (secondSection != null)\n                    {\n                        if (ContentDispositionHeaderValue.TryParse(secondSection.ContentDisposition,\n                            out var secondContent) && HasFileContentDisposition(secondContent))\n                        {\n                            var fileName = HeaderUtilities.RemoveQuotes(secondContent.FileName).ToString();\n                            using (secondSection.Body)\n                            {\n                                await callback(secondSection.Body, fileName, key);\n                            }\n                        }\n\n                        secondSection = null;\n                    }\n                }\n            }\n\n            firstSection = null;\n        }\n    }\n\n    public static async Task GetSendFileAsync(this HttpRequest request, Func<Stream, string,\n        SendRequestModel, Task> callback)\n    {\n        var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),\n            _defaultFormOptions.MultipartBoundaryLengthLimit);\n        var reader = new MultipartReader(boundary, request.Body);\n\n        var firstSection = await reader.ReadNextSectionAsync();\n        if (firstSection != null)\n        {\n            if (ContentDispositionHeaderValue.TryParse(firstSection.ContentDisposition, out _))\n            {\n                var secondSection = await reader.ReadNextSectionAsync();\n                if (secondSection != null)\n                {\n                    if (ContentDispositionHeaderValue.TryParse(secondSection.ContentDisposition,\n                        out var secondContent) && HasFileContentDisposition(secondContent))\n                    {\n                        var fileName = HeaderUtilities.RemoveQuotes(secondContent.FileName).ToString();\n                        using (secondSection.Body)\n                        {\n                            var model = await JsonSerializer.DeserializeAsync<SendRequestModel>(firstSection.Body);\n                            await callback(secondSection.Body, fileName, model);\n                        }\n                    }\n\n                    secondSection = null;\n                }\n\n            }\n\n            firstSection = null;\n        }\n    }\n\n    public static async Task GetFileAsync(this HttpRequest request, Func<Stream, Task> callback)\n    {\n        var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),\n            _defaultFormOptions.MultipartBoundaryLengthLimit);\n        var reader = new MultipartReader(boundary, request.Body);\n\n        var dataSection = await reader.ReadNextSectionAsync();\n        if (dataSection != null)\n        {\n            if (ContentDispositionHeaderValue.TryParse(dataSection.ContentDisposition, out var dataContent)\n                && HasFileContentDisposition(dataContent))\n            {\n                using (dataSection.Body)\n                {\n                    await callback(dataSection.Body);\n                }\n            }\n            dataSection = null;\n        }\n    }\n\n\n    private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)\n    {\n        var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);\n        if (StringSegment.IsNullOrEmpty(boundary))\n        {\n            throw new InvalidDataException(\"Missing content-type boundary.\");\n        }\n\n        if (boundary.Length > lengthLimit)\n        {\n            throw new InvalidDataException($\"Multipart boundary length limit {lengthLimit} exceeded.\");\n        }\n\n        return boundary.ToString();\n    }\n\n    private static bool HasFileContentDisposition(ContentDispositionHeaderValue content)\n    {\n        // Content-Disposition: form-data; name=\"data\"; filename=\"Misc 002.jpg\"\n        return content != null && content.DispositionType.Equals(\"form-data\") &&\n            (!StringSegment.IsNullOrEmpty(content.FileName) || !StringSegment.IsNullOrEmpty(content.FileNameStar));\n    }\n\n    private static bool HasDispositionName(ContentDispositionHeaderValue content, string name)\n    {\n        // Content-Disposition: form-data; name=\"key\";\n        return content != null && content.DispositionType.Equals(\"form-data\") && content.Name == name;\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/PublicApiControllersModelConvention.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Mvc.ApplicationModels;\n\nnamespace Bit.Api.Utilities;\n\npublic class PublicApiControllersModelConvention : IControllerModelConvention\n{\n    public void Apply(ControllerModel controller)\n    {\n        var controllerNamespace = controller.ControllerType.Namespace;\n        var publicApi = controllerNamespace.Contains(\".Public.\");\n        controller.Filters.Add(new ExceptionHandlerFilterAttribute(publicApi));\n        controller.Filters.Add(new ModelStateValidationFilterAttribute(publicApi));\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/ServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.Tools.Authorization;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Authorization.SecurityTasks;\nusing Bit.SharedWeb.Health;\nusing Bit.SharedWeb.Swagger;\nusing Bit.SharedWeb.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.OpenApi;\n\nnamespace Bit.Api.Utilities;\n\npublic static class ServiceCollectionExtensions\n{\n    /// <summary>\n    /// Configures the generation of swagger.json OpenAPI spec.\n    /// </summary>\n    public static void AddSwaggerGen(this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment)\n    {\n        services.AddSwaggerGen(config =>\n        {\n            config.SwaggerDoc(\"public\", new OpenApiInfo\n            {\n                Title = \"Bitwarden Public API\",\n                Version = \"latest\",\n                Contact = new OpenApiContact\n                {\n                    Name = \"Bitwarden Support\",\n                    Url = new Uri(\"https://bitwarden.com\"),\n                    Email = \"support@bitwarden.com\"\n                },\n                Description = \"\"\"\n                              This schema documents the endpoints available to the Public API, which provides\n                              organizations tools for managing members, collections, groups, event logs, and policies.\n                              If you are looking for the Vault Management API, refer instead to\n                              [this document](https://bitwarden.com/help/vault-management-api/).\n\n                              **Note:** your authorization must match the server you have selected.\n                              \"\"\",\n                License = new OpenApiLicense\n                {\n                    Name = \"GNU Affero General Public License v3.0\",\n                    Url = new Uri(\"https://github.com/bitwarden/server/blob/master/LICENSE.txt\")\n                }\n            });\n\n            config.SwaggerDoc(\"internal\", new OpenApiInfo { Title = \"Bitwarden Internal API\", Version = \"latest\" });\n\n            // Configure Bitwarden cloud US and EU servers. These will appear in the swagger.json build artifact\n            // used for our help center. These are overwritten with the local server when running in self-hosted\n            // or dev mode (see Api Startup.cs).\n            config.AddSwaggerServerWithSecurity(\n                serverId: \"US_server\",\n                serverUrl: \"https://api.bitwarden.com\",\n                identityTokenUrl: \"https://identity.bitwarden.com/connect/token\",\n                serverDescription: \"US server\");\n\n            config.AddSwaggerServerWithSecurity(\n                serverId: \"EU_server\",\n                serverUrl: \"https://api.bitwarden.eu\",\n                identityTokenUrl: \"https://identity.bitwarden.eu/connect/token\",\n                serverDescription: \"EU server\");\n\n            config.DescribeAllParametersInCamelCase();\n            // config.UseReferencedDefinitionsForEnums();\n\n            config.InitializeSwaggerFilters(environment);\n\n            var apiFilePath = Path.Combine(AppContext.BaseDirectory, \"Api.xml\");\n            config.IncludeXmlComments(apiFilePath, true);\n            var coreFilePath = Path.Combine(AppContext.BaseDirectory, \"Core.xml\");\n            config.IncludeXmlComments(coreFilePath);\n        });\n    }\n\n    public static void AddHealthChecks(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        services.AddHealthCheckServices(globalSettings, builder =>\n        {\n            var identityUri = new Uri(globalSettings.BaseServiceUri.Identity\n                                      + \"/.well-known/openid-configuration\");\n\n            builder.AddUrlGroup(identityUri, \"identity\");\n\n            if (CoreHelpers.SettingHasValue(globalSettings.SqlServer.ConnectionString))\n            {\n                builder.AddSqlServer(globalSettings.SqlServer.ConnectionString);\n            }\n        });\n    }\n\n    public static void AddAuthorizationHandlers(this IServiceCollection services)\n    {\n        services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();\n\n        // Admin Console authorization handlers\n        services.AddAdminConsoleAuthorizationHandlers();\n    }\n}\n"
  },
  {
    "path": "src/Api/Utilities/StringMatchesAttribute.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Utilities;\n\npublic class StringMatchesAttribute(params string[]? accepted) : ValidationAttribute\n{\n    public override bool IsValid(object? value)\n    {\n        if (value is not string str ||\n            accepted == null ||\n            accepted.Length == 0)\n        {\n            return false;\n        }\n\n        return accepted.Contains(str);\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing System.Diagnostics;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Api.Vault.AuthorizationHandlers.Collections;\n\n/// <summary>\n/// Handles authorization logic for Collection objects, including access permissions for users and groups.\n/// This uses new logic implemented in the Flexible Collections initiative.\n/// </summary>\npublic class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkCollectionOperationRequirement, Collection>\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IFeatureService _featureService;\n    private Guid _targetOrganizationId;\n    private HashSet<Guid>? _managedCollectionsIds;\n\n    private HashSet<Guid>? _orphanedCollectionsIds;\n\n    public BulkCollectionAuthorizationHandler(\n        ICurrentContext currentContext,\n        ICollectionRepository collectionRepository,\n        IApplicationCacheService applicationCacheService,\n        IFeatureService featureService)\n    {\n        _currentContext = currentContext;\n        _collectionRepository = collectionRepository;\n        _applicationCacheService = applicationCacheService;\n        _featureService = featureService;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        BulkCollectionOperationRequirement requirement, ICollection<Collection>? resources)\n    {\n        // Establish pattern of authorization handler null checking passed resources\n        if (resources == null || !resources.Any())\n        {\n            context.Fail();\n            return;\n        }\n\n        // Acting user is not authenticated, fail\n        if (!_currentContext.UserId.HasValue)\n        {\n            context.Fail();\n            return;\n        }\n\n        _targetOrganizationId = resources.First().OrganizationId;\n\n        // Ensure all target collections belong to the same organization\n        if (resources.Any(tc => tc.OrganizationId != _targetOrganizationId))\n        {\n            throw new BadRequestException(\"Requested collections must belong to the same organization.\");\n        }\n\n        var org = _currentContext.GetOrganization(_targetOrganizationId);\n\n        var authorized = false;\n\n        switch (requirement)\n        {\n            case not null when requirement == BulkCollectionOperations.Create:\n                authorized = await CanCreateAsync(org);\n                break;\n\n            case not null when requirement == BulkCollectionOperations.Read:\n            case not null when requirement == BulkCollectionOperations.ReadAccess:\n                authorized = await CanReadAsync(resources, org);\n                break;\n\n            case not null when requirement == BulkCollectionOperations.ReadWithAccess:\n                authorized = await CanReadWithAccessAsync(resources, org);\n                break;\n\n            case not null when requirement == BulkCollectionOperations.Update:\n            case not null when requirement == BulkCollectionOperations.ImportCiphers:\n                authorized = await CanUpdateCollectionAsync(resources, org);\n                break;\n\n            case not null when requirement == BulkCollectionOperations.ModifyUserAccess:\n                authorized = await CanUpdateUserAccessAsync(resources, org);\n                break;\n\n            case not null when requirement == BulkCollectionOperations.ModifyGroupAccess:\n                authorized = await CanUpdateGroupAccessAsync(resources, org);\n                break;\n\n            case not null when requirement == BulkCollectionOperations.Delete:\n                authorized = await CanDeleteAsync(resources, org);\n                break;\n\n            case null:\n                // requirement isn't actually nullable but since we use the\n                // not null when trick it makes the compiler think that requirement\n                // could actually be nullable.\n                throw new UnreachableException();\n        }\n\n        if (authorized)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task<bool> CanCreateAsync(CurrentContextOrganization? org)\n    {\n        // Owners, Admins, and users with CreateNewCollections permission can always create collections\n        if (org is\n        { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n        { Permissions.CreateNewCollections: true })\n        {\n            return true;\n        }\n\n        var organizationAbility = await GetOrganizationAbilityAsync(org);\n\n        var userIsMemberOfOrg = org is not null;\n        var limitCollectionCreationEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionCreation: true };\n        var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };\n        // If the limit collection management setting is disabled, allow any user to create collections\n        if (userIsMemberOfOrg && (!limitCollectionCreationEnabled || userIsOrgOwnerOrAdmin))\n        {\n            return true;\n        }\n\n        // Allow provider users to create collections if they are a provider for the target organization\n        return await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId);\n    }\n\n    private async Task<bool> CanReadAsync(ICollection<Collection> resources, CurrentContextOrganization? org)\n    {\n        // Owners, Admins, and users with EditAnyCollection or DeleteAnyCollection permission can always read a collection\n        if (org is\n        { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n        { Permissions.EditAnyCollection: true } or\n        { Permissions.DeleteAnyCollection: true })\n        {\n            return true;\n        }\n\n        // The acting user is a member of the target organization,\n        // ensure they have access for the collection being read\n        if (org is not null)\n        {\n            var canManageCollections = await CanManageCollectionsAsync(resources, org);\n            if (canManageCollections)\n            {\n                return true;\n            }\n        }\n\n        // Allow provider users to read collections if they are a provider for the target organization\n        return await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId);\n    }\n\n    private async Task<bool> CanReadWithAccessAsync(ICollection<Collection> resources, CurrentContextOrganization? org)\n    {\n        // Owners, Admins, and users with EditAnyCollection, DeleteAnyCollection or ManageUsers permission can always read a collection\n        if (org is\n        { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n        { Permissions.EditAnyCollection: true } or\n        { Permissions.DeleteAnyCollection: true } or\n        { Permissions.ManageUsers: true })\n        {\n            return true;\n        }\n\n        // The acting user is a member of the target organization,\n        // ensure they have access with manage permission for the collection being read\n        if (org is not null)\n        {\n            var canManageCollections = await CanManageCollectionsAsync(resources, org);\n            if (canManageCollections)\n            {\n                return true;\n            }\n        }\n\n        // Allow provider users to read collections if they are a provider for the target organization\n        return await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId);\n    }\n\n    /// <summary>\n    /// Ensures the acting user is allowed to update the target collections or manage access permissions for them.\n    /// </summary>\n    private async Task<bool> CanUpdateCollectionAsync(ICollection<Collection> resources, CurrentContextOrganization? org)\n    {\n        // Users with EditAnyCollection permission can always update a collection\n        if (org is { Permissions.EditAnyCollection: true })\n        {\n            return true;\n        }\n\n        // Owners and Admins can update any collection only if permitted by collection management settings\n        if (await AllowAdminAccessToAllCollectionItems(org) && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })\n        {\n            return true;\n        }\n\n        // The acting user is a member of the target organization,\n        // ensure they have manage permission for the collection being managed\n        if (org is not null)\n        {\n            var canManageCollections = await CanManageCollectionsAsync(resources, org);\n            if (canManageCollections)\n            {\n                return true;\n            }\n        }\n\n        // Allow providers to manage collections if they are a provider for the target organization\n        return await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId);\n    }\n\n    private async Task<bool> CanUpdateUserAccessAsync(ICollection<Collection> resources, CurrentContextOrganization? org)\n    {\n        if (await AllowAdminAccessToAllCollectionItems(org) && org?.Permissions.ManageUsers == true)\n        {\n            return true;\n        }\n\n        return await CanUpdateCollectionAsync(resources, org);\n    }\n\n    private async Task<bool> CanUpdateGroupAccessAsync(ICollection<Collection> resources, CurrentContextOrganization? org)\n    {\n        if (await AllowAdminAccessToAllCollectionItems(org) && org?.Permissions.ManageGroups == true)\n        {\n            return true;\n        }\n\n        return await CanUpdateCollectionAsync(resources, org);\n    }\n\n    private async Task<bool> CanDeleteAsync(ICollection<Collection> resources, CurrentContextOrganization? org)\n    {\n        // Users with DeleteAnyCollection permission can always delete collections\n        if (org is { Permissions.DeleteAnyCollection: true })\n        {\n            return true;\n        }\n\n        // If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionDeletion setting\n        if (await AllowAdminAccessToAllCollectionItems(org) && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })\n        {\n            return true;\n        }\n\n        var userIsMemberOfOrg = org is not null;\n        var limitCollectionDeletionEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionDeletion: true };\n        var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };\n        // If the limit collection management setting is disabled, allow any user to delete collections\n        if (userIsMemberOfOrg && (!limitCollectionDeletionEnabled || userIsOrgOwnerOrAdmin) && await CanManageCollectionsAsync(resources, org))\n        {\n            return true;\n        }\n\n        // Allow providers to delete collections if they are a provider for the target organization\n        return await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId);\n    }\n\n    private async Task<bool> CanManageCollectionsAsync(ICollection<Collection> targetCollections,\n        CurrentContextOrganization? org)\n    {\n        if (_managedCollectionsIds == null)\n        {\n            var allUserCollections = await _collectionRepository\n                .GetManyByUserIdAsync(_currentContext.UserId!.Value);\n\n            var managedCollectionIds = allUserCollections\n                .Where(c => c.Manage)\n                .Select(c => c.Id);\n\n            _managedCollectionsIds = managedCollectionIds\n                .ToHashSet();\n        }\n\n        var canManageTargetCollections = targetCollections.All(tc => _managedCollectionsIds.Contains(tc.Id));\n\n        // The user can manage all target collections, stop here, return true.\n        if (canManageTargetCollections)\n        {\n            return true;\n        }\n\n        // The user is not assigned to manage all target collections\n        // If the user is an Owner/Admin/Custom user with edit, check if any targets are orphaned collections\n        if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }))\n        {\n            // User is not allowed to manage orphaned collections\n            return false;\n        }\n\n        if (_orphanedCollectionsIds == null)\n        {\n            var orgCollections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(_targetOrganizationId);\n\n            // Orphaned collections are collections that have no users or groups with manage permissions\n            _orphanedCollectionsIds = orgCollections.Where(c =>\n                    !c.Item2.Users.Any(u => u.Manage) && !c.Item2.Groups.Any(g => g.Manage))\n                .Select(c => c.Item1.Id)\n                .ToHashSet();\n        }\n\n        return targetCollections.All(tc => _orphanedCollectionsIds.Contains(tc.Id) || _managedCollectionsIds.Contains(tc.Id));\n    }\n\n    private async Task<OrganizationAbility?> GetOrganizationAbilityAsync(CurrentContextOrganization? organization)\n    {\n        // If the CurrentContextOrganization is null, then the user isn't a member of the org so the setting is\n        // irrelevant\n        if (organization == null)\n        {\n            return null;\n        }\n\n        return await _applicationCacheService.GetOrganizationAbilityAsync(organization.Id);\n    }\n\n    private async Task<bool> AllowAdminAccessToAllCollectionItems(CurrentContextOrganization? org)\n    {\n        return await GetOrganizationAbilityAsync(org) is { AllowAdminAccessToAllCollectionItems: true };\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/AuthorizationHandlers/Collections/BulkCollectionOperations.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Api.Vault.AuthorizationHandlers.Collections;\n\npublic class BulkCollectionOperationRequirement : OperationAuthorizationRequirement { }\n\npublic static class BulkCollectionOperations\n{\n    /// <summary>\n    /// Create a new collection\n    /// </summary>\n    public static readonly BulkCollectionOperationRequirement Create = new() { Name = nameof(Create) };\n    public static readonly BulkCollectionOperationRequirement Read = new() { Name = nameof(Read) };\n    public static readonly BulkCollectionOperationRequirement ReadAccess = new() { Name = nameof(ReadAccess) };\n    public static readonly BulkCollectionOperationRequirement ReadWithAccess = new() { Name = nameof(ReadWithAccess) };\n    /// <summary>\n    /// Update a collection, including user and group access\n    /// </summary>\n    public static readonly BulkCollectionOperationRequirement Update = new() { Name = nameof(Update) };\n    /// <summary>\n    /// Delete a collection\n    /// </summary>\n    public static readonly BulkCollectionOperationRequirement Delete = new() { Name = nameof(Delete) };\n    /// <summary>\n    /// Import ciphers into a collection\n    /// </summary>\n    public static readonly BulkCollectionOperationRequirement ImportCiphers = new() { Name = nameof(ImportCiphers) };\n    /// <summary>\n    /// Create, update or delete user access (CollectionUser)\n    /// </summary>\n    public static readonly BulkCollectionOperationRequirement ModifyUserAccess = new() { Name = nameof(ModifyUserAccess) };\n    /// <summary>\n    /// Create, update or delete group access (CollectionGroup)\n    /// </summary>\n    public static readonly BulkCollectionOperationRequirement ModifyGroupAccess = new() { Name = nameof(ModifyGroupAccess) };\n}\n"
  },
  {
    "path": "src/Api/Vault/AuthorizationHandlers/Collections/CollectionAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Api.Vault.AuthorizationHandlers.Collections;\n\n/// <summary>\n/// Handles authorization logic for Collection operations.\n/// This uses new logic implemented in the Flexible Collections initiative.\n/// </summary>\npublic class CollectionAuthorizationHandler : AuthorizationHandler<CollectionOperationRequirement>\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IFeatureService _featureService;\n\n    public CollectionAuthorizationHandler(\n        ICurrentContext currentContext,\n        IFeatureService featureService)\n    {\n        _currentContext = currentContext;\n        _featureService = featureService;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        CollectionOperationRequirement requirement)\n    {\n        // Acting user is not authenticated, fail\n        if (!_currentContext.UserId.HasValue)\n        {\n            context.Fail();\n            return;\n        }\n\n        if (requirement.OrganizationId == default)\n        {\n            context.Fail();\n            return;\n        }\n\n        var org = _currentContext.GetOrganization(requirement.OrganizationId);\n\n        switch (requirement)\n        {\n            case not null when requirement.Name == nameof(CollectionOperations.ReadAll):\n                await CanReadAllAsync(context, requirement, org);\n                break;\n\n            case not null when requirement.Name == nameof(CollectionOperations.ReadAllWithAccess):\n                await CanReadAllWithAccessAsync(context, requirement, org);\n                break;\n        }\n    }\n\n    private async Task CanReadAllAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,\n        CurrentContextOrganization? org)\n    {\n        // Owners, Admins, and users with EditAnyCollection, DeleteAnyCollection,\n        // or AccessImportExport permission can always read a collection\n        if (org is\n        { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n        { Permissions.EditAnyCollection: true } or\n        { Permissions.DeleteAnyCollection: true } or\n        { Permissions.AccessImportExport: true } or\n        { Permissions.ManageGroups: true })\n        {\n            context.Succeed(requirement);\n            return;\n        }\n\n        // Allow provider users to read collections if they are a provider for the target organization\n        if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task CanReadAllWithAccessAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,\n        CurrentContextOrganization? org)\n    {\n        // Owners, Admins, and users with EditAnyCollection or DeleteAnyCollection\n        // permission can always read a collection\n        if (org is\n        { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n        { Permissions.EditAnyCollection: true } or\n        { Permissions.DeleteAnyCollection: true } or\n        { Permissions.ManageUsers: true } or\n        { Permissions.ManageGroups: true })\n        {\n            context.Succeed(requirement);\n            return;\n        }\n\n        // Allow provider users to read collections if they are a provider for the target organization\n        if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))\n        {\n            context.Succeed(requirement);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/AuthorizationHandlers/Collections/CollectionOperations.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Api.Vault.AuthorizationHandlers.Collections;\n\npublic class CollectionOperationRequirement : OperationAuthorizationRequirement\n{\n    public Guid OrganizationId { get; init; }\n\n    public CollectionOperationRequirement(string name, Guid organizationId)\n    {\n        Name = name;\n        OrganizationId = organizationId;\n    }\n}\n\npublic static class CollectionOperations\n{\n    public static CollectionOperationRequirement ReadAll(Guid organizationId)\n    {\n        return new CollectionOperationRequirement(nameof(ReadAll), organizationId);\n    }\n    public static CollectionOperationRequirement ReadAllWithAccess(Guid organizationId)\n    {\n        return new CollectionOperationRequirement(nameof(ReadAllWithAccess), organizationId);\n    }\n}\n\n"
  },
  {
    "path": "src/Api/Vault/Controllers/CiphersController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Globalization;\nusing System.Text.Json;\nusing Azure.Messaging.EventGrid;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Utilities;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Services;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Authorization.Permissions;\nusing Bit.Core.Vault.Commands.Interfaces;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Queries;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Core.Vault.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Vault.Controllers;\n\n[Route(\"ciphers\")]\n[Authorize(\"Application\")]\npublic class CiphersController : Controller\n{\n    private static readonly Version _fido2KeyCipherMinimumVersion = new Version(Constants.Fido2KeyCipherMinimumVersion);\n\n    private readonly ICipherRepository _cipherRepository;\n    private readonly ICollectionCipherRepository _collectionCipherRepository;\n    private readonly ICipherService _cipherService;\n    private readonly IUserService _userService;\n    private readonly IAttachmentStorageService _attachmentStorageService;\n    private readonly ICurrentContext _currentContext;\n    private readonly ILogger<CiphersController> _logger;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IOrganizationCiphersQuery _organizationCiphersQuery;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IArchiveCiphersCommand _archiveCiphersCommand;\n    private readonly IUnarchiveCiphersCommand _unarchiveCiphersCommand;\n\n    public CiphersController(\n        ICipherRepository cipherRepository,\n        ICollectionCipherRepository collectionCipherRepository,\n        ICipherService cipherService,\n        IUserService userService,\n        IAttachmentStorageService attachmentStorageService,\n        ICurrentContext currentContext,\n        ILogger<CiphersController> logger,\n        GlobalSettings globalSettings,\n        IOrganizationCiphersQuery organizationCiphersQuery,\n        IApplicationCacheService applicationCacheService,\n        ICollectionRepository collectionRepository,\n        IArchiveCiphersCommand archiveCiphersCommand,\n        IUnarchiveCiphersCommand unarchiveCiphersCommand)\n    {\n        _cipherRepository = cipherRepository;\n        _collectionCipherRepository = collectionCipherRepository;\n        _cipherService = cipherService;\n        _userService = userService;\n        _attachmentStorageService = attachmentStorageService;\n        _currentContext = currentContext;\n        _logger = logger;\n        _globalSettings = globalSettings;\n        _organizationCiphersQuery = organizationCiphersQuery;\n        _applicationCacheService = applicationCacheService;\n        _collectionRepository = collectionRepository;\n        _archiveCiphersCommand = archiveCiphersCommand;\n        _unarchiveCiphersCommand = unarchiveCiphersCommand;\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<CipherResponseModel> Get(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = await GetByIdAsync(id, user.Id);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n\n        return new CipherResponseModel(cipher, user, organizationAbilities, _globalSettings);\n    }\n\n    [HttpGet(\"{id}/admin\")]\n    public async Task<CipherMiniResponseModel> GetAdmin(string id)\n    {\n        var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await _currentContext.ViewAllCollections(cipher.OrganizationId.Value))\n        {\n            throw new NotFoundException();\n        }\n\n        var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value);\n        var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);\n\n        return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp);\n    }\n\n    [HttpGet(\"{id}/details\")]\n    public async Task<CipherDetailsResponseModel> GetDetails(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = await GetByIdAsync(id, user.Id);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id);\n        return new CipherDetailsResponseModel(cipher, user, organizationAbilities, _globalSettings, collectionCiphers);\n    }\n\n    [HttpGet(\"{id}/full-details\")]\n    [Obsolete(\"This endpoint is deprecated. Use GET details method instead.\")]\n    public async Task<CipherDetailsResponseModel> GetFullDetails(Guid id)\n    {\n        return await GetDetails(id);\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<CipherDetailsResponseModel>> GetAll()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var hasOrgs = _currentContext.Organizations.Count != 0;\n        // TODO: Use hasOrgs proper for cipher listing here?\n        var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: true);\n        Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null;\n        if (hasOrgs)\n        {\n            var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id);\n            collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);\n        }\n        var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var responses = ciphers.Select(cipher => new CipherDetailsResponseModel(\n            cipher,\n            user,\n            organizationAbilities,\n            _globalSettings,\n            collectionCiphersGroupDict)).ToList();\n        return new ListResponseModel<CipherDetailsResponseModel>(responses);\n    }\n\n    [HttpPost(\"\")]\n    public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        // Validate the model was encrypted for the posting user\n        if (model.EncryptedFor != null)\n        {\n            if (model.EncryptedFor != user.Id)\n            {\n                _logger.LogError(\"Cipher was not encrypted for the current user. CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}\", user.Id, model.EncryptedFor);\n                throw new BadRequestException(\"Cipher was not encrypted for the current user. Please try again.\");\n            }\n        }\n\n        var cipher = model.ToCipherDetails(user.Id);\n        if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.SaveDetailsAsync(cipher, user.Id, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue);\n        var response = new CipherResponseModel(\n            cipher,\n            user,\n            await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n            _globalSettings);\n        return response;\n    }\n\n    [HttpPost(\"create\")]\n    public async Task<CipherResponseModel> PostCreate([FromBody] CipherCreateRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        // Validate the model was encrypted for the posting user\n        if (model.Cipher.EncryptedFor != null)\n        {\n            if (model.Cipher.EncryptedFor != user.Id)\n            {\n                _logger.LogError(\"Cipher was not encrypted for the current user. CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}\", user.Id, model.Cipher.EncryptedFor);\n                throw new BadRequestException(\"Cipher was not encrypted for the current user. Please try again.\");\n            }\n        }\n\n        var cipher = model.Cipher.ToCipherDetails(user.Id);\n        if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.SaveDetailsAsync(cipher, user.Id, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue);\n        return await Get(cipher.Id);\n    }\n\n    [HttpPost(\"admin\")]\n    public async Task<CipherMiniResponseModel> PostAdmin([FromBody] CipherCreateRequestModel model)\n    {\n        var cipher = model.Cipher.ToOrganizationCipher();\n        // Only users that can edit all ciphers can create new ciphers via the admin endpoint\n        // Other users should use the regular POST/create endpoint\n        if (!await CanEditAllCiphersAsync(cipher.OrganizationId.Value))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n\n        // Validate the model was encrypted for the posting user\n        if (model.Cipher.EncryptedFor != null)\n        {\n            if (model.Cipher.EncryptedFor != userId)\n            {\n                _logger.LogError(\"Cipher was not encrypted for the current user. CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}\", userId, model.Cipher.EncryptedFor);\n                throw new BadRequestException(\"Cipher was not encrypted for the current user. Please try again.\");\n            }\n        }\n\n        await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false);\n\n        var response = new CipherMiniResponseModel(cipher, _globalSettings, false);\n        return response;\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task<CipherResponseModel> Put(Guid id, [FromBody] CipherRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = await GetByIdAsync(id, user.Id);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Validate the model was encrypted for the posting user\n        if (model.EncryptedFor != null)\n        {\n            if (model.EncryptedFor != user.Id)\n            {\n                _logger.LogError(\"Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}\", id, user.Id, model.EncryptedFor);\n                throw new BadRequestException(\"Cipher was not encrypted for the current user. Please try again.\");\n            }\n        }\n\n        ValidateClientVersionForFido2CredentialSupport(cipher);\n\n        var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList();\n        var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ?\n            (Guid?)null : new Guid(model.OrganizationId);\n        if (cipher.OrganizationId != modelOrgId)\n        {\n            throw new BadRequestException(\"Organization mismatch. Re-sync if you recently moved this item, \" +\n                \"then try again.\");\n        }\n\n        await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), user.Id, model.LastKnownRevisionDate, collectionIds);\n\n        var response = new CipherResponseModel(\n            cipher,\n            user,\n            await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n            _globalSettings);\n        return response;\n    }\n\n    [HttpPost(\"{id}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task<CipherResponseModel> PostPut(Guid id, [FromBody] CipherRequestModel model)\n    {\n        return await Put(id, model);\n    }\n\n    [HttpPut(\"{id}/admin\")]\n    public async Task<CipherMiniResponseModel> PutAdmin(Guid id, [FromBody] CipherRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);\n\n        // Validate the model was encrypted for the posting user\n        if (model.EncryptedFor != null)\n        {\n            if (model.EncryptedFor != userId)\n            {\n                _logger.LogError(\"Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}\", id, userId, model.EncryptedFor);\n                throw new BadRequestException(\"Cipher was not encrypted for the current user. Please try again.\");\n            }\n        }\n\n        ValidateClientVersionForFido2CredentialSupport(cipher);\n\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            throw new NotFoundException();\n        }\n\n        var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList();\n        // object cannot be a descendant of CipherDetails, so let's clone it.\n        var cipherClone = model.ToCipher(cipher).Clone();\n        await _cipherService.SaveAsync(cipherClone, userId, model.LastKnownRevisionDate, collectionIds, true, false);\n\n        var response = new CipherMiniResponseModel(cipherClone, _globalSettings, cipher.OrganizationUseTotp);\n        return response;\n    }\n\n    [HttpPost(\"{id}/admin\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task<CipherMiniResponseModel> PostPutAdmin(Guid id, [FromBody] CipherRequestModel model)\n    {\n        return await PutAdmin(id, model);\n    }\n\n    [HttpGet(\"organization-details\")]\n    public async Task<ListResponseModel<CipherMiniDetailsResponseModel>> GetOrganizationCiphers(Guid organizationId, bool includeMemberItems = false)\n    {\n        if (!await CanAccessAllCiphersAsync(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var allOrganizationCiphers = !includeMemberItems\n        ?\n            await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId)\n        :\n            await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId);\n\n        var allOrganizationCipherResponses =\n            allOrganizationCiphers.Select(c =>\n                new CipherMiniDetailsResponseModel(c, _globalSettings, c.OrganizationUseTotp)\n            );\n\n        return new ListResponseModel<CipherMiniDetailsResponseModel>(allOrganizationCipherResponses);\n    }\n\n    [HttpGet(\"organization-details/assigned\")]\n    public async Task<ListResponseModel<CipherDetailsResponseModel>> GetAssignedOrganizationCiphers(Guid organizationId)\n    {\n        if (!await CanAccessOrganizationCiphersAsync(organizationId) || !_currentContext.UserId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var ciphers = await _organizationCiphersQuery.GetOrganizationCiphersForUser(organizationId, _currentContext.UserId.Value);\n\n        if (await CanAccessUnassignedCiphersAsync(organizationId))\n        {\n            var unassignedCiphers = await _organizationCiphersQuery.GetUnassignedOrganizationCiphers(organizationId);\n            ciphers = ciphers.Concat(unassignedCiphers.Select(c => new CipherDetailsWithCollections(c, null)\n            {\n                // Users that can access unassigned ciphers can also edit them\n                Edit = true,\n                ViewPassword = true,\n            }));\n        }\n\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var responses = ciphers.Select(cipher =>\n            new CipherDetailsResponseModel(\n                cipher,\n                user,\n                organizationAbilities,\n                _globalSettings));\n\n        return new ListResponseModel<CipherDetailsResponseModel>(responses);\n    }\n\n    /// <summary>\n    /// Permission helper to determine if the current user can use the \"/admin\" variants of the cipher endpoints.\n    /// Allowed for custom users with EditAnyCollection, providers, unrestricted owners and admins (allowAdminAccess setting is ON).\n    /// Falls back to original EditAnyCollection permission check for when V1 flag is disabled.\n    /// TODO: Move this to its own authorization handler or equivalent service - AC-2062\n    /// </summary>\n    private async Task<bool> CanEditCipherAsAdminAsync(Guid organizationId, IEnumerable<Guid> cipherIds)\n    {\n        var org = _currentContext.GetOrganization(organizationId);\n\n        // If we're not an \"admin\" we don't need to check the ciphers\n        if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n            { Permissions.EditAnyCollection: true }))\n        {\n            return false;\n        }\n\n        // We know we're an \"admin\", now check the ciphers explicitly (in case admins are restricted)\n        return await CanEditCiphersAsync(organizationId, cipherIds);\n    }\n\n    private async Task<bool> CanDeleteOrRestoreCipherAsAdminAsync(Guid organizationId, IEnumerable<Guid> cipherIds)\n    {\n        var org = _currentContext.GetOrganization(organizationId);\n\n        // If we're not an \"admin\" we don't need to check the ciphers\n        if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n            { Permissions.EditAnyCollection: true }))\n        {\n            return false;\n        }\n\n        // If the user can edit all ciphers for the organization, just check they all belong to the org\n        if (await CanEditAllCiphersAsync(organizationId))\n        {\n            // TODO: This can likely be optimized to only query the requested ciphers and then checking they belong to the org\n            var orgCiphers = (await _cipherRepository.GetManyByOrganizationIdAsync(organizationId)).ToDictionary(c => c.Id);\n\n            // Ensure all requested ciphers are in orgCiphers\n            return cipherIds.All(c => orgCiphers.ContainsKey(c));\n        }\n\n        // The user cannot access any ciphers for the organization, we're done\n        if (!await CanAccessOrganizationCiphersAsync(organizationId))\n        {\n            return false;\n        }\n\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        // Select all deletable ciphers for this user belonging to the organization\n        var deletableOrgCipherList = (await _cipherRepository.GetManyByUserIdAsync(user.Id, true))\n            .Where(c => c.OrganizationId == organizationId && c.UserId == null).ToList();\n\n        // Special case for unassigned ciphers\n        if (await CanAccessUnassignedCiphersAsync(organizationId))\n        {\n            var unassignedCiphers =\n                (await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(\n                    organizationId));\n\n            // Users that can access unassigned ciphers can also delete them\n            deletableOrgCipherList.AddRange(unassignedCiphers.Select(c => new CipherDetails(c) { Manage = true }));\n        }\n\n        var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);\n        var deletableOrgCiphers = deletableOrgCipherList\n            .Where(c => NormalCipherPermissions.CanDelete(user, c, organizationAbility))\n            .ToDictionary(c => c.Id);\n\n        return cipherIds.All(c => deletableOrgCiphers.ContainsKey(c));\n    }\n\n    /// <summary>\n    /// TODO: Move this to its own authorization handler or equivalent service - AC-2062\n    /// </summary>\n    private async Task<bool> CanAccessAllCiphersAsync(Guid organizationId)\n    {\n        var org = _currentContext.GetOrganization(organizationId);\n\n        // We do NOT need to check the organization collection management setting here because Owners/Admins can\n        // ALWAYS access all ciphers in order to export them. Additionally, custom users with AccessImportExport,\n        // EditAnyCollection, or AccessReports permissions can also always access all ciphers.\n        if (org is\n        { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n        { Permissions.AccessImportExport: true } or\n        { Permissions.EditAnyCollection: true } or\n        { Permissions.AccessReports: true })\n        {\n            return true;\n        }\n\n        // Provider users can access all ciphers.\n        if (await _currentContext.ProviderUserForOrgAsync(organizationId))\n        {\n            return true;\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    /// TODO: Move this to its own authorization handler or equivalent service - AC-2062\n    /// </summary>\n    private async Task<bool> CanEditAllCiphersAsync(Guid organizationId)\n    {\n        var org = _currentContext.GetOrganization(organizationId);\n\n        // Custom users with EditAnyCollection permissions can always edit all ciphers\n        if (org is { Type: OrganizationUserType.Custom, Permissions.EditAnyCollection: true })\n        {\n            return true;\n        }\n\n        var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);\n\n        // Owners/Admins can only edit all ciphers if the organization has the setting enabled\n        if (orgAbility is { AllowAdminAccessToAllCollectionItems: true } && org is\n            { Type: OrganizationUserType.Admin or OrganizationUserType.Owner })\n        {\n            return true;\n        }\n\n        // Provider users cannot edit ciphers\n        if (await _currentContext.ProviderUserForOrgAsync(organizationId))\n        {\n            return false;\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    /// TODO: Move this to its own authorization handler or equivalent service - AC-2062\n    /// </summary>\n    private async Task<bool> CanAccessOrganizationCiphersAsync(Guid organizationId)\n    {\n        var org = _currentContext.GetOrganization(organizationId);\n\n        // The user has a relationship with the organization;\n        // they can access its ciphers in collections they've been assigned\n        if (org is not null)\n        {\n            return true;\n        }\n\n        // Provider users cannot access organization ciphers\n        if (await _currentContext.ProviderUserForOrgAsync(organizationId))\n        {\n            return false;\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    /// TODO: Move this to its own authorization handler or equivalent service - AC-2062\n    /// </summary>\n    private async Task<bool> CanAccessUnassignedCiphersAsync(Guid organizationId)\n    {\n        var org = _currentContext.GetOrganization(organizationId);\n\n        if (org is\n        { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n        { Permissions.EditAnyCollection: true })\n        {\n            return true;\n        }\n\n        // Provider users cannot access ciphers\n        if (await _currentContext.ProviderUserForOrgAsync(organizationId))\n        {\n            return false;\n        }\n\n        return false;\n    }\n\n    /// <summary>\n    /// TODO: Move this to its own authorization handler or equivalent service - AC-2062\n    /// </summary>\n    private async Task<bool> CanModifyCipherCollectionsAsync(Guid organizationId, IEnumerable<Guid> cipherIds)\n    {\n        // If the user can edit all ciphers for the organization, just check they all belong to the org\n        if (await CanEditAllCiphersAsync(organizationId))\n        {\n            // TODO: This can likely be optimized to only query the requested ciphers and then checking they belong to the org\n            var orgCiphers = (await _cipherRepository.GetManyByOrganizationIdAsync(organizationId)).ToDictionary(c => c.Id);\n\n            // Ensure all requested ciphers are in orgCiphers\n            if (cipherIds.Any(c => !orgCiphers.ContainsKey(c)))\n            {\n                return false;\n            }\n\n            return true;\n        }\n\n        // The user cannot access any ciphers for the organization, we're done\n        if (!await CanAccessOrganizationCiphersAsync(organizationId))\n        {\n            return false;\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        // Select all editable ciphers for this user belonging to the organization\n        var editableOrgCipherList = (await _cipherRepository.GetManyByUserIdAsync(userId, true))\n            .Where(c => c.OrganizationId == organizationId && c.UserId == null && c.Edit && c.ViewPassword).ToList();\n\n        // Special case for unassigned ciphers\n        if (await CanAccessUnassignedCiphersAsync(organizationId))\n        {\n            var unassignedCiphers =\n                (await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(\n                    organizationId));\n\n            // Users that can access unassigned ciphers can also edit them\n            editableOrgCipherList.AddRange(unassignedCiphers.Select(c => new CipherDetails(c) { Edit = true }));\n        }\n\n        var editableOrgCiphers = editableOrgCipherList\n            .ToDictionary(c => c.Id);\n\n        if (cipherIds.Any(c => !editableOrgCiphers.ContainsKey(c)))\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    /// <summary>\n    /// TODO: Move this to its own authorization handler or equivalent service - AC-2062\n    /// </summary>\n    private async Task<bool> CanEditCiphersAsync(Guid organizationId, IEnumerable<Guid> cipherIds)\n    {\n        // If the user can edit all ciphers for the organization, just check they all belong to the org\n        if (await CanEditAllCiphersAsync(organizationId))\n        {\n            // TODO: This can likely be optimized to only query the requested ciphers and then checking they belong to the org\n            var orgCiphers = (await _cipherRepository.GetManyByOrganizationIdAsync(organizationId)).ToDictionary(c => c.Id);\n\n            // Ensure all requested ciphers are in orgCiphers\n            if (cipherIds.Any(c => !orgCiphers.ContainsKey(c)))\n            {\n                return false;\n            }\n\n            return true;\n        }\n\n        // The user cannot access any ciphers for the organization, we're done\n        if (!await CanAccessOrganizationCiphersAsync(organizationId))\n        {\n            return false;\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        // Select all editable ciphers for this user belonging to the organization\n        var editableOrgCipherList = (await _cipherRepository.GetManyByUserIdAsync(userId, true))\n            .Where(c => c.OrganizationId == organizationId && c.UserId == null && c.Edit).ToList();\n\n        // Special case for unassigned ciphers\n        if (await CanAccessUnassignedCiphersAsync(organizationId))\n        {\n            var unassignedCiphers =\n                (await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(\n                    organizationId));\n\n            // Users that can access unassigned ciphers can also edit them\n            editableOrgCipherList.AddRange(unassignedCiphers.Select(c => new CipherDetails(c) { Edit = true }));\n        }\n\n        var editableOrgCiphers = editableOrgCipherList\n            .ToDictionary(c => c.Id);\n\n        if (cipherIds.Any(c => !editableOrgCiphers.ContainsKey(c)))\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    /// <summary>\n    /// TODO: Move this to its own authorization handler or equivalent service - AC-2062\n    /// This likely belongs to the BulkCollectionAuthorizationHandler\n    /// </summary>\n    private async Task<bool> CanEditItemsInCollections(Guid organizationId, IEnumerable<Guid> collectionIds)\n    {\n        if (await CanEditAllCiphersAsync(organizationId))\n        {\n            // TODO: This can likely be optimized to only query the requested ciphers and then checking they belong to the org\n            var orgCollections = (await _collectionRepository.GetManyByOrganizationIdAsync(organizationId)).ToDictionary(c => c.Id);\n\n            // Ensure all requested collections are in orgCollections\n            if (collectionIds.Any(c => !orgCollections.ContainsKey(c)))\n            {\n                return false;\n            }\n\n            return true;\n        }\n\n        if (!await CanAccessOrganizationCiphersAsync(organizationId))\n        {\n            return false;\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var editableCollections = (await _collectionRepository.GetManyByUserIdAsync(userId))\n            .Where(c => c.OrganizationId == organizationId && !c.ReadOnly)\n            .ToDictionary(c => c.Id);\n\n        if (collectionIds.Any(c => !editableCollections.ContainsKey(c)))\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    [HttpPut(\"{id}/partial\")]\n    public async Task<CipherResponseModel> PutPartial(Guid id, [FromBody] CipherPartialRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = await GetByIdAsync(id, user.Id);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId);\n        await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite);\n\n        var updatedCipher = await GetByIdAsync(id, user.Id);\n        var response = new CipherResponseModel(\n            updatedCipher,\n            user,\n            await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n            _globalSettings);\n        return response;\n    }\n\n    [HttpPost(\"{id}/partial\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task<CipherResponseModel> PostPartial(Guid id, [FromBody] CipherPartialRequestModel model)\n    {\n        return await PutPartial(id, model);\n    }\n\n    [HttpPut(\"{id}/share\")]\n    public async Task<CipherResponseModel> PutShare(Guid id, [FromBody] CipherShareRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = await _cipherRepository.GetByIdAsync(id);\n        if (cipher == null || cipher.UserId != user.Id ||\n            !await _currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId)))\n        {\n            throw new NotFoundException();\n        }\n\n        // Validate the model was encrypted for the posting user\n        if (model.Cipher.EncryptedFor != null)\n        {\n            if (model.Cipher.EncryptedFor != user.Id)\n            {\n                _logger.LogError(\"Cipher was not encrypted for the current user. CipherId: {CipherId} CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}\", id, user.Id, model.Cipher.EncryptedFor);\n                throw new BadRequestException(\"Cipher was not encrypted for the current user. Please try again.\");\n            }\n        }\n\n        ValidateClientVersionForFido2CredentialSupport(cipher);\n\n        var original = cipher.Clone();\n        await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher, user.Id), new Guid(model.Cipher.OrganizationId),\n            model.CollectionIds.Select(c => new Guid(c)), user.Id, model.Cipher.LastKnownRevisionDate);\n\n        var sharedCipher = await GetByIdAsync(id, user.Id);\n        var response = new CipherResponseModel(\n            sharedCipher,\n            user,\n            await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n            _globalSettings);\n        return response;\n    }\n\n    [HttpPost(\"{id}/share\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task<CipherResponseModel> PostShare(Guid id, [FromBody] CipherShareRequestModel model)\n    {\n        return await PutShare(id, model);\n    }\n\n    [HttpPut(\"{id}/collections\")]\n    public async Task<CipherDetailsResponseModel> PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = await GetByIdAsync(id, user.Id);\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.SaveCollectionsAsync(cipher,\n            model.CollectionIds.Select(c => new Guid(c)), user.Id, false);\n\n        var updatedCipher = await GetByIdAsync(id, user.Id);\n        var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id);\n\n        return new CipherDetailsResponseModel(\n            updatedCipher,\n            user,\n            await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n            _globalSettings,\n            collectionCiphers);\n    }\n\n    [HttpPost(\"{id}/collections\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task<CipherDetailsResponseModel> PostCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)\n    {\n        return await PutCollections(id, model);\n    }\n\n    [HttpPut(\"{id}/collections_v2\")]\n    public async Task<OptionalCipherDetailsResponseModel> PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = await GetByIdAsync(id, user.Id);\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await _currentContext.OrganizationUser(cipher.OrganizationId.Value) || !cipher.ViewPassword)\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.SaveCollectionsAsync(cipher,\n            model.CollectionIds.Select(c => new Guid(c)), user.Id, false);\n\n        var updatedCipher = await GetByIdAsync(id, user.Id);\n        var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id);\n        // If a user removes the last Can Manage access of a cipher, the \"updatedCipher\" will return null\n        // We will be returning an \"Unavailable\" property so the client knows the user can no longer access this\n        var response = new OptionalCipherDetailsResponseModel()\n        {\n            Unavailable = updatedCipher is null,\n            Cipher = updatedCipher is null\n                ? null\n                : new CipherDetailsResponseModel(\n                    updatedCipher,\n                    user,\n                    await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n                    _globalSettings,\n                    collectionCiphers)\n        };\n        return response;\n    }\n\n    [HttpPost(\"{id}/collections_v2\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task<OptionalCipherDetailsResponseModel> PostCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model)\n    {\n        return await PutCollections_vNext(id, model);\n    }\n\n    [HttpPut(\"{id}/collections-admin\")]\n    public async Task<CipherMiniDetailsResponseModel> PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));\n\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            throw new NotFoundException();\n        }\n\n        var collectionIds = model.CollectionIds.Select(c => new Guid(c)).ToList();\n\n        // In V1, we still need to check if the user can edit the collections they're submitting\n        // This should only happen for unassigned ciphers (otherwise restricted admins would use the normal collections endpoint)\n        if (!await CanEditItemsInCollections(cipher.OrganizationId.Value, collectionIds))\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.SaveCollectionsAsync(cipher, collectionIds, userId, true);\n\n        var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value);\n        var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);\n\n        return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp);\n    }\n\n    [HttpPost(\"{id}/collections-admin\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task<CipherMiniDetailsResponseModel> PostCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)\n    {\n        return await PutCollectionsAdmin(id, model);\n    }\n\n    [HttpPost(\"bulk-collections\")]\n    public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        await _cipherService.ValidateBulkCollectionAssignmentAsync(model.CollectionIds, model.CipherIds, userId);\n\n        if (!await CanModifyCipherCollectionsAsync(model.OrganizationId, model.CipherIds) ||\n            !await CanEditItemsInCollections(model.OrganizationId, model.CollectionIds))\n        {\n            throw new NotFoundException();\n        }\n\n        if (model.RemoveCollections)\n        {\n            await _collectionCipherRepository.RemoveCollectionsForManyCiphersAsync(model.OrganizationId, model.CipherIds, model.CollectionIds);\n        }\n        else\n        {\n            await _collectionCipherRepository.AddCollectionsForManyCiphersAsync(model.OrganizationId, model.CipherIds, model.CollectionIds);\n        }\n    }\n\n    [HttpPut(\"{id}/archive\")]\n    [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]\n    public async Task<CipherResponseModel> PutArchive(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n\n        var archivedCipherOrganizationDetails = await _archiveCiphersCommand.ArchiveManyAsync([id], userId);\n\n        if (archivedCipherOrganizationDetails.Count == 0)\n        {\n            throw new BadRequestException(\"Cipher was not archived. Ensure the provided ID is correct and you have permission to archive it.\");\n        }\n\n        return new CipherResponseModel(archivedCipherOrganizationDetails.First(),\n            await _userService.GetUserByPrincipalAsync(User),\n            await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n            _globalSettings\n        );\n    }\n\n    [HttpPut(\"archive\")]\n    [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]\n    public async Task<ListResponseModel<CipherResponseModel>> PutArchiveMany([FromBody] CipherBulkArchiveRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)\n        {\n            throw new BadRequestException(\"You can only archive up to 500 items at a time.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        var cipherIdsToArchive = new HashSet<Guid>(model.Ids);\n\n        var archivedCiphers = await _archiveCiphersCommand.ArchiveManyAsync(cipherIdsToArchive, userId);\n\n        if (archivedCiphers.Count == 0)\n        {\n            throw new BadRequestException(\"No ciphers were archived. Ensure the provided IDs are correct and you have permission to archive them.\");\n        }\n\n        var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var responses = archivedCiphers.Select(c => new CipherResponseModel(c,\n            user,\n            organizationAbilities,\n            _globalSettings\n        ));\n\n        return new ListResponseModel<CipherResponseModel>(responses);\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task Delete(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await GetByIdAsync(id, userId);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.DeleteAsync(cipher, userId);\n    }\n\n    [HttpPost(\"{id}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead.\")]\n    public async Task PostDelete(Guid id)\n    {\n        await Delete(id);\n    }\n\n    [HttpDelete(\"{id}/admin\")]\n    public async Task DeleteAdmin(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await GetByIdAsyncAdmin(id);\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await CanDeleteOrRestoreCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.DeleteAsync(new CipherDetails(cipher), userId, true);\n    }\n\n    [HttpPost(\"{id}/delete-admin\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead.\")]\n    public async Task PostDeleteAdmin(Guid id)\n    {\n        await DeleteAdmin(id);\n    }\n\n    [HttpDelete(\"\")]\n    public async Task DeleteMany([FromBody] CipherBulkDeleteRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)\n        {\n            throw new BadRequestException(\"You can only delete up to 500 items at a time. \" +\n                \"Consider using the \\\"Purge Vault\\\" option instead.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        await _cipherService.DeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId);\n    }\n\n    [HttpPost(\"delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead.\")]\n    public async Task PostDeleteMany([FromBody] CipherBulkDeleteRequestModel model)\n    {\n        await DeleteMany(model);\n    }\n\n    [HttpDelete(\"admin\")]\n    public async Task DeleteManyAdmin([FromBody] CipherBulkDeleteRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)\n        {\n            throw new BadRequestException(\"You can only delete up to 500 items at a time. \" +\n                \"Consider using the \\\"Purge Vault\\\" option instead.\");\n        }\n\n        if (model == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var cipherIds = model.Ids.Select(i => new Guid(i)).ToList();\n\n        if (string.IsNullOrWhiteSpace(model.OrganizationId) ||\n            !await CanDeleteOrRestoreCipherAsAdminAsync(new Guid(model.OrganizationId), cipherIds))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        await _cipherService.DeleteManyAsync(cipherIds, userId, new Guid(model.OrganizationId), true);\n    }\n\n    [HttpPost(\"delete-admin\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead.\")]\n    public async Task PostDeleteManyAdmin([FromBody] CipherBulkDeleteRequestModel model)\n    {\n        await DeleteManyAdmin(model);\n    }\n\n    [HttpPut(\"{id}/delete\")]\n    public async Task PutDelete(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await GetByIdAsync(id, userId);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n        await _cipherService.SoftDeleteAsync(cipher, userId);\n    }\n\n    [HttpPut(\"{id}/delete-admin\")]\n    public async Task PutDeleteAdmin(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await GetByIdAsyncAdmin(id);\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await CanDeleteOrRestoreCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.SoftDeleteAsync(new CipherDetails(cipher), userId, true);\n    }\n\n    [HttpPut(\"delete\")]\n    public async Task PutDeleteMany([FromBody] CipherBulkDeleteRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)\n        {\n            throw new BadRequestException(\"You can only delete up to 500 items at a time.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        await _cipherService.SoftDeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId);\n    }\n\n    [HttpPut(\"delete-admin\")]\n    public async Task PutDeleteManyAdmin([FromBody] CipherBulkDeleteRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)\n        {\n            throw new BadRequestException(\"You can only delete up to 500 items at a time.\");\n        }\n\n        if (model == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var cipherIds = model.Ids.Select(i => new Guid(i)).ToList();\n\n        if (string.IsNullOrWhiteSpace(model.OrganizationId) ||\n            !await CanDeleteOrRestoreCipherAsAdminAsync(new Guid(model.OrganizationId), cipherIds))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        await _cipherService.SoftDeleteManyAsync(cipherIds, userId, new Guid(model.OrganizationId), true);\n    }\n\n    [HttpPut(\"{id}/unarchive\")]\n    [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]\n    public async Task<CipherResponseModel> PutUnarchive(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n\n        var unarchivedCipherDetails = await _unarchiveCiphersCommand.UnarchiveManyAsync([id], userId);\n\n        if (unarchivedCipherDetails.Count == 0)\n        {\n            throw new BadRequestException(\"Cipher was not unarchived. Ensure the provided ID is correct and you have permission to archive it.\");\n        }\n\n        return new CipherResponseModel(unarchivedCipherDetails.First(),\n            await _userService.GetUserByPrincipalAsync(User),\n            await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n            _globalSettings\n        );\n    }\n\n    [HttpPut(\"unarchive\")]\n    [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]\n    public async Task<ListResponseModel<CipherResponseModel>> PutUnarchiveMany([FromBody] CipherBulkUnarchiveRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)\n        {\n            throw new BadRequestException(\"You can only unarchive up to 500 items at a time.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n\n        var cipherIdsToUnarchive = new HashSet<Guid>(model.Ids);\n\n        var unarchivedCipherOrganizationDetails = await _unarchiveCiphersCommand.UnarchiveManyAsync(cipherIdsToUnarchive, userId);\n\n        if (unarchivedCipherOrganizationDetails.Count == 0)\n        {\n            throw new BadRequestException(\"Ciphers were not unarchived. Ensure the provided ID is correct and you have permission to archive it.\");\n        }\n\n        var responses = unarchivedCipherOrganizationDetails.Select(c => new CipherResponseModel(c, user, organizationAbilities, _globalSettings));\n\n        return new ListResponseModel<CipherResponseModel>(responses);\n    }\n\n    [HttpPut(\"{id}/restore\")]\n    public async Task<CipherResponseModel> PutRestore(Guid id)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = await GetByIdAsync(id, user.Id);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.RestoreAsync(cipher, user.Id);\n        return new CipherResponseModel(\n            cipher,\n            user,\n            await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n            _globalSettings);\n    }\n\n    [HttpPut(\"{id}/restore-admin\")]\n    public async Task<CipherMiniResponseModel> PutRestoreAdmin(Guid id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await GetByIdAsyncAdmin(id);\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await CanDeleteOrRestoreCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.RestoreAsync(new CipherDetails(cipher), userId, true);\n        return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);\n    }\n\n    [HttpPut(\"restore\")]\n    public async Task<ListResponseModel<CipherMiniResponseModel>> PutRestoreMany([FromBody] CipherBulkRestoreRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)\n        {\n            throw new BadRequestException(\"You can only restore up to 500 items at a time.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipherIdsToRestore = new HashSet<Guid>(model.Ids.Select(i => new Guid(i)));\n\n        var restoredCiphers = await _cipherService.RestoreManyAsync(cipherIdsToRestore, userId);\n        var responses = restoredCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));\n        return new ListResponseModel<CipherMiniResponseModel>(responses);\n    }\n\n    [HttpPut(\"restore-admin\")]\n    public async Task<ListResponseModel<CipherMiniResponseModel>> PutRestoreManyAdmin([FromBody] CipherBulkRestoreRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)\n        {\n            throw new BadRequestException(\"You can only restore up to 500 items at a time.\");\n        }\n\n        if (model == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var cipherIdsToRestore = new HashSet<Guid>(model.Ids.Select(i => new Guid(i)));\n\n        if (model.OrganizationId == default || !await CanDeleteOrRestoreCipherAsAdminAsync(model.OrganizationId, cipherIdsToRestore))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n\n        var restoredCiphers = await _cipherService.RestoreManyAsync(cipherIdsToRestore, userId, model.OrganizationId, true);\n        var responses = restoredCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));\n        return new ListResponseModel<CipherMiniResponseModel>(responses);\n    }\n\n    [HttpPut(\"move\")]\n    public async Task MoveMany([FromBody] CipherBulkMoveRequestModel model)\n    {\n        if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)\n        {\n            throw new BadRequestException(\"You can only move up to 500 items at a time.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        await _cipherService.MoveManyAsync(model.Ids.Select(i => new Guid(i)),\n            string.IsNullOrWhiteSpace(model.FolderId) ? (Guid?)null : new Guid(model.FolderId), userId);\n    }\n\n    [HttpPost(\"move\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task PostMoveMany([FromBody] CipherBulkMoveRequestModel model)\n    {\n        await MoveMany(model);\n    }\n\n    [HttpPut(\"share\")]\n    public async Task<ListResponseModel<CipherMiniResponseModel>> PutShareMany([FromBody] CipherBulkShareRequestModel model)\n    {\n        var organizationId = new Guid(model.Ciphers.First().OrganizationId);\n        if (!await _currentContext.OrganizationUser(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n\n        var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);\n        var ciphersDict = ciphers.ToDictionary(c => c.Id);\n\n        // Validate the model was encrypted for the posting user\n        foreach (var cipher in model.Ciphers)\n        {\n            if (cipher.EncryptedFor.HasValue && cipher.EncryptedFor.Value != userId)\n            {\n                _logger.LogError(\"Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}\", cipher.Id, userId, cipher.EncryptedFor);\n                throw new BadRequestException(\"Cipher was not encrypted for the current user. Please try again.\");\n            }\n        }\n\n        var shareCiphers = new List<(CipherDetails, DateTime?)>();\n        foreach (var cipher in model.Ciphers)\n        {\n            if (!ciphersDict.TryGetValue(cipher.Id.Value, out var existingCipher))\n            {\n                throw new BadRequestException(\"Trying to share ciphers that you do not own.\");\n            }\n\n            ValidateClientVersionForFido2CredentialSupport(existingCipher);\n\n            shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate));\n        }\n\n        var updated = await _cipherService.ShareManyAsync(\n            shareCiphers,\n            organizationId,\n            model.CollectionIds.Select(Guid.Parse),\n            userId\n        );\n\n        var response = updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));\n        return new ListResponseModel<CipherMiniResponseModel>(response);\n    }\n\n    [HttpPost(\"share\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task<ListResponseModel<CipherMiniResponseModel>> PostShareMany([FromBody] CipherBulkShareRequestModel model)\n    {\n        return await PutShareMany(model);\n    }\n\n    [HttpPost(\"purge\")]\n    public async Task PostPurge([FromBody] SecretVerificationRequestModel model, Guid? organizationId = null)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!await _userService.VerifySecretAsync(user, model.Secret))\n        {\n            ModelState.AddModelError(string.Empty, \"User verification failed.\");\n            await Task.Delay(2000);\n            throw new BadRequestException(ModelState);\n        }\n\n        if (organizationId == null)\n        {\n            // Check if the user is claimed by any organization.\n            if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))\n            {\n                throw new BadRequestException(\"Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.\");\n            }\n            await _cipherRepository.DeleteByUserIdAsync(user.Id);\n        }\n        else\n        {\n            if (!await _currentContext.EditAnyCollection(organizationId!.Value))\n            {\n                throw new NotFoundException();\n            }\n            await _cipherService.PurgeAsync(organizationId!.Value);\n        }\n    }\n\n    [HttpPost(\"{id}/attachment/v2\")]\n    public async Task<AttachmentUploadDataResponseModel> PostAttachment(Guid id, [FromBody] AttachmentRequestModel request)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = request.AdminRequest ?\n            await _cipherRepository.GetOrganizationDetailsByIdAsync(id) :\n            await GetByIdAsync(id, user.Id);\n\n        if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue ||\n            !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))))\n        {\n            throw new NotFoundException();\n        }\n\n        if (request.FileSize > CipherService.MAX_FILE_SIZE)\n        {\n            throw new BadRequestException($\"Max file size is {CipherService.MAX_FILE_SIZE_READABLE}.\");\n        }\n\n        var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher,\n            request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id, request.LastKnownRevisionDate);\n        return new AttachmentUploadDataResponseModel\n        {\n            AttachmentId = attachmentId,\n            Url = uploadUrl,\n            FileUploadType = _attachmentStorageService.FileUploadType,\n            CipherResponse = request.AdminRequest ? null : new CipherResponseModel(\n                (CipherDetails)cipher,\n                user,\n                await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n                _globalSettings),\n            CipherMiniResponse = request.AdminRequest ? new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp) : null,\n        };\n    }\n\n    [HttpGet(\"{id}/attachment/{attachmentId}/renew\")]\n    public async Task<AttachmentUploadDataResponseModel> RenewFileUploadUrl(Guid id, string attachmentId)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await GetByIdAsync(id, userId);\n\n        var orgAdmin = false;\n        if (cipher.OrganizationId.HasValue &&\n            await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            orgAdmin = true;\n        }\n\n        var attachments = cipher?.GetAttachments();\n\n        if (attachments == null || !attachments.TryGetValue(attachmentId, out var attachment) || attachment.Validated)\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.ValidateCipherEditForAttachmentAsync(cipher, userId, orgAdmin, attachment.Size);\n\n        return new AttachmentUploadDataResponseModel\n        {\n            Url = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, attachment),\n            FileUploadType = _attachmentStorageService.FileUploadType,\n        };\n    }\n\n    [HttpPost(\"{id}/attachment/{attachmentId}\")]\n    [SelfHosted(SelfHostedOnly = true)]\n    [RequestSizeLimit(Constants.FileSize501mb)]\n    [DisableFormValueModelBinding]\n    public async Task PostFileForExistingAttachment(Guid id, string attachmentId)\n    {\n        if (!Request?.ContentType.Contains(\"multipart/\") ?? true)\n        {\n            throw new BadRequestException(\"Invalid content.\");\n        }\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await GetByIdAsync(id, userId);\n\n        var orgAdmin = false;\n        if (cipher.OrganizationId.HasValue &&\n            await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            orgAdmin = true;\n        }\n        var attachments = cipher?.GetAttachments();\n        if (attachments == null || !attachments.TryGetValue(attachmentId, out var attachmentData))\n        {\n            throw new NotFoundException();\n        }\n\n        await Request.GetFileAsync(async (stream) =>\n        {\n            await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData, userId, orgAdmin);\n        });\n    }\n\n    [HttpPost(\"{id}/attachment\")]\n    [Obsolete(\"Deprecated Attachments API\", false)]\n    [RequestSizeLimit(Constants.FileSize101mb)]\n    [DisableFormValueModelBinding]\n    public async Task<CipherResponseModel> PostAttachmentV1(Guid id)\n    {\n        ValidateAttachment();\n\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        var cipher = await GetByIdAsync(id, user.Id);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Extract lastKnownRevisionDate from form data if present\n        DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();\n        await Request.GetFileAsync(async (stream, fileName, key) =>\n        {\n            await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,\n                    Request.ContentLength.GetValueOrDefault(0), user.Id, false, lastKnownRevisionDate);\n        });\n\n        return new CipherResponseModel(\n            cipher,\n            user,\n            await _applicationCacheService.GetOrganizationAbilitiesAsync(),\n            _globalSettings);\n    }\n\n    [HttpPost(\"{id}/attachment-admin\")]\n    [RequestSizeLimit(Constants.FileSize101mb)]\n    [DisableFormValueModelBinding]\n    public async Task<CipherMiniResponseModel> PostAttachmentAdmin(string id)\n    {\n        ValidateAttachment();\n\n        var idGuid = new Guid(id);\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(idGuid);\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            throw new NotFoundException();\n        }\n\n        // Extract lastKnownRevisionDate from form data if present\n        DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();\n\n        await Request.GetFileAsync(async (stream, fileName, key) =>\n        {\n            await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,\n                    Request.ContentLength.GetValueOrDefault(0), userId, true, lastKnownRevisionDate);\n        });\n\n        return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);\n    }\n\n    [HttpGet(\"{id}/attachment/{attachmentId}/admin\")]\n    public async Task<AttachmentResponseModel> GetAttachmentDataAdmin(Guid id, string attachmentId)\n    {\n        var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId);\n        return new AttachmentResponseModel(result);\n    }\n\n    [HttpGet(\"{id}/attachment/{attachmentId}\")]\n    public async Task<AttachmentResponseModel> GetAttachmentData(Guid id, string attachmentId)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await GetByIdAsync(id, userId);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId);\n        return new AttachmentResponseModel(result);\n    }\n\n    /// <summary>\n    /// Serves a locally stored attachment file using a time-limited, signed token.\n    /// This endpoint replaces direct static file access for self-hosted environments\n    /// to ensure that only authorized users can download attachment files.\n    /// </summary>\n    [AllowAnonymous]\n    [HttpGet(\"attachment/download\")]\n    public async Task<IActionResult> DownloadAttachmentAsync([FromQuery] string token)\n    {\n        if (string.IsNullOrEmpty(token))\n        {\n            throw new NotFoundException();\n        }\n\n        (Guid cipherId, string attachmentId) = _attachmentStorageService.ParseAttachmentDownloadToken(token);\n\n        var cipher = await _cipherRepository.GetByIdAsync(cipherId);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var attachments = cipher.GetAttachments();\n        if (attachments == null || !attachments.TryGetValue(attachmentId, out var attachmentData))\n        {\n            throw new NotFoundException();\n        }\n\n        var stream = await _attachmentStorageService.GetAttachmentReadStreamAsync(cipher, attachmentData);\n        if (stream == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return File(stream, \"application/octet-stream\", attachmentData.FileName);\n    }\n\n    [HttpPost(\"{id}/attachment/{attachmentId}/share\")]\n    [RequestSizeLimit(Constants.FileSize101mb)]\n    [DisableFormValueModelBinding]\n    public async Task PostAttachmentShare(string id, string attachmentId, Guid organizationId)\n    {\n        ValidateAttachment();\n\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));\n        if (cipher == null || cipher.UserId != userId || !await _currentContext.OrganizationUser(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        await Request.GetFileAsync(async (stream, fileName, key) =>\n        {\n            await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key,\n                Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);\n        });\n    }\n\n    [HttpDelete(\"{id}/attachment/{attachmentId}\")]\n    public async Task<DeleteAttachmentResponseModel> DeleteAttachment(Guid id, string attachmentId)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await GetByIdAsync(id, userId);\n        if (cipher == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false);\n        return new DeleteAttachmentResponseModel(result, _globalSettings);\n    }\n\n    [HttpPost(\"{id}/attachment/{attachmentId}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead.\")]\n    public async Task<DeleteAttachmentResponseModel> PostDeleteAttachment(Guid id, string attachmentId)\n    {\n        return await DeleteAttachment(id, attachmentId);\n    }\n\n    [HttpDelete(\"{id}/attachment/{attachmentId}/admin\")]\n    public async Task<DeleteAttachmentResponseModel> DeleteAttachmentAdmin(Guid id, string attachmentId)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var cipher = await _cipherRepository.GetByIdAsync(id);\n        if (cipher == null || !cipher.OrganizationId.HasValue ||\n            !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))\n        {\n            throw new NotFoundException();\n        }\n\n        var result = await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true);\n        return new DeleteAttachmentResponseModel(result, _globalSettings);\n    }\n\n    [HttpPost(\"{id}/attachment/{attachmentId}/delete-admin\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead.\")]\n    public async Task<DeleteAttachmentResponseModel> PostDeleteAttachmentAdmin(Guid id, string attachmentId)\n    {\n        return await DeleteAttachmentAdmin(id, attachmentId);\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"attachment/validate/azure\")]\n    public async Task<ObjectResult> AzureValidateFile()\n    {\n        return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>\n        {\n            {\n                \"Microsoft.Storage.BlobCreated\", async (eventGridEvent) =>\n                {\n                    try\n                    {\n                        var blobName = eventGridEvent.Subject.Split($\"{AzureAttachmentStorageService.EventGridEnabledContainerName}/blobs/\")[1];\n                        var (cipherId, organizationId, attachmentId) = AzureAttachmentStorageService.IdentifiersFromBlobName(blobName);\n                        var cipher = await _cipherRepository.GetByIdAsync(new Guid(cipherId));\n                        var attachments = cipher?.GetAttachments() ?? new Dictionary<string, CipherAttachment.MetaData>();\n\n                        if (cipher == null || !attachments.TryGetValue(attachmentId, out var attachment) || attachment.Validated)\n                        {\n                            if (_attachmentStorageService is AzureSendFileStorageService azureFileStorageService)\n                            {\n                                await azureFileStorageService.DeleteBlobAsync(blobName);\n                            }\n\n                            return;\n                        }\n\n                        await _cipherService.ValidateCipherAttachmentFile(cipher, attachment);\n                    }\n                    catch (Exception e)\n                    {\n                        _logger.LogError(e, \"Uncaught exception occurred while handling event grid event: {Event}\", JsonSerializer.Serialize(eventGridEvent));\n                        return;\n                    }\n                }\n            }\n        });\n    }\n\n    private void ValidateAttachment()\n    {\n        if (!Request?.ContentType.Contains(\"multipart/\") ?? true)\n        {\n            throw new BadRequestException(\"Invalid content.\");\n        }\n    }\n\n    private void ValidateClientVersionForFido2CredentialSupport(Cipher cipher)\n    {\n        if (cipher.Type == Core.Vault.Enums.CipherType.Login)\n        {\n            var loginData = JsonSerializer.Deserialize<CipherLoginData>(cipher.Data);\n            if (loginData?.Fido2Credentials != null && _currentContext.ClientVersion < _fido2KeyCipherMinimumVersion)\n            {\n                throw new BadRequestException(\"Cannot edit item. Update to the latest version of Bitwarden and try again.\");\n            }\n        }\n    }\n\n    private async Task<CipherOrganizationDetails> GetByIdAsyncAdmin(Guid cipherId)\n    {\n        return await _cipherRepository.GetOrganizationDetailsByIdAsync(cipherId);\n    }\n\n    private async Task<CipherDetails> GetByIdAsync(Guid cipherId, Guid userId)\n    {\n        return await _cipherRepository.GetByIdAsync(cipherId, userId);\n    }\n\n    private DateTime? GetLastKnownRevisionDateFromForm()\n    {\n        DateTime? lastKnownRevisionDate = null;\n        if (Request.Form.TryGetValue(\"lastKnownRevisionDate\", out var dateValue))\n        {\n            if (!DateTime.TryParse(dateValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsedDate))\n            {\n                throw new BadRequestException(\"Invalid lastKnownRevisionDate format.\");\n            }\n            lastKnownRevisionDate = parsedDate;\n        }\n\n        return lastKnownRevisionDate;\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Controllers/FoldersController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Response;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Core.Vault.Services;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Vault.Controllers;\n\n[Route(\"folders\")]\n[Authorize(\"Application\")]\npublic class FoldersController : Controller\n{\n    private readonly IFolderRepository _folderRepository;\n    private readonly ICipherService _cipherService;\n    private readonly IUserService _userService;\n\n    public FoldersController(\n        IFolderRepository folderRepository,\n        ICipherService cipherService,\n        IUserService userService)\n    {\n        _folderRepository = folderRepository;\n        _cipherService = cipherService;\n        _userService = userService;\n    }\n\n    [HttpGet(\"{id}\")]\n    public async Task<FolderResponseModel> Get(string id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var folder = await _folderRepository.GetByIdAsync(new Guid(id), userId);\n        if (folder == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new FolderResponseModel(folder);\n    }\n\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<FolderResponseModel>> GetAll()\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var folders = await _folderRepository.GetManyByUserIdAsync(userId);\n        var responses = folders.Select(f => new FolderResponseModel(f));\n        return new ListResponseModel<FolderResponseModel>(responses);\n    }\n\n    [HttpPost(\"\")]\n    public async Task<FolderResponseModel> Post([FromBody] FolderRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var folder = model.ToFolder(_userService.GetProperUserId(User).Value);\n        await _cipherService.SaveFolderAsync(folder);\n        return new FolderResponseModel(folder);\n    }\n\n    [HttpPut(\"{id}\")]\n    public async Task<FolderResponseModel> Put(string id, [FromBody] FolderRequestModel model)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var folder = await _folderRepository.GetByIdAsync(new Guid(id), userId);\n        if (folder == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.SaveFolderAsync(model.ToFolder(folder));\n        return new FolderResponseModel(folder);\n    }\n\n    [HttpPost(\"{id}\")]\n    [Obsolete(\"This endpoint is deprecated. Use PUT method instead.\")]\n    public async Task<FolderResponseModel> PostPut(string id, [FromBody] FolderRequestModel model)\n    {\n        return await Put(id, model);\n    }\n\n    [HttpDelete(\"{id}\")]\n    public async Task Delete(string id)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var folder = await _folderRepository.GetByIdAsync(new Guid(id), userId);\n        if (folder == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _cipherService.DeleteFolderAsync(folder);\n    }\n\n    [HttpPost(\"{id}/delete\")]\n    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead.\")]\n    public async Task PostDelete(string id)\n    {\n        await Delete(id);\n    }\n\n    [HttpDelete(\"all\")]\n    public async Task DeleteAll()\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var allFolders = await _folderRepository.GetManyByUserIdAsync(userId);\n\n        foreach (var folder in allFolders)\n        {\n            await _cipherService.DeleteFolderAsync(folder);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Controllers/SecurityTaskController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Models.Response;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Commands.Interfaces;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Queries;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Vault.Controllers;\n\n[Route(\"tasks\")]\n[Authorize(\"Application\")]\npublic class SecurityTaskController : Controller\n{\n    private readonly IUserService _userService;\n    private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery;\n    private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand;\n    private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery;\n    private readonly ICreateManyTasksCommand _createManyTasksCommand;\n    private readonly ICreateManyTaskNotificationsCommand _createManyTaskNotificationsCommand;\n    private readonly IGetTaskMetricsForOrganizationQuery _getTaskMetricsForOrganizationQuery;\n\n    public SecurityTaskController(\n        IUserService userService,\n        IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery,\n        IMarkTaskAsCompleteCommand markTaskAsCompleteCommand,\n        IGetTasksForOrganizationQuery getTasksForOrganizationQuery,\n        ICreateManyTasksCommand createManyTasksCommand,\n        ICreateManyTaskNotificationsCommand createManyTaskNotificationsCommand,\n        IGetTaskMetricsForOrganizationQuery getTaskMetricsForOrganizationQuery)\n    {\n        _userService = userService;\n        _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;\n        _markTaskAsCompleteCommand = markTaskAsCompleteCommand;\n        _getTasksForOrganizationQuery = getTasksForOrganizationQuery;\n        _createManyTasksCommand = createManyTasksCommand;\n        _createManyTaskNotificationsCommand = createManyTaskNotificationsCommand;\n        _getTaskMetricsForOrganizationQuery = getTaskMetricsForOrganizationQuery;\n    }\n\n    /// <summary>\n    /// Retrieves security tasks for the current user.\n    /// </summary>\n    /// <param name=\"status\">Optional filter for task status. If not provided returns tasks of all statuses.</param>\n    /// <returns>A list response model containing the security tasks for the user.</returns>\n    [HttpGet(\"\")]\n    public async Task<ListResponseModel<SecurityTasksResponseModel>> Get([FromQuery] SecurityTaskStatus? status)\n    {\n        var userId = _userService.GetProperUserId(User).Value;\n        var securityTasks = await _getTaskDetailsForUserQuery.GetTaskDetailsForUserAsync(userId, status);\n        var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();\n        return new ListResponseModel<SecurityTasksResponseModel>(response);\n    }\n\n    /// <summary>\n    /// Marks a task as complete. The user must have edit permission on the cipher associated with the task.\n    /// </summary>\n    /// <param name=\"taskId\">The unique identifier of the task to complete</param>\n    [HttpPatch(\"{taskId:guid}/complete\")]\n    public async Task<IActionResult> Complete(Guid taskId)\n    {\n        await _markTaskAsCompleteCommand.CompleteAsync(taskId);\n        return NoContent();\n    }\n\n    /// <summary>\n    /// Retrieves security tasks for an organization. Restricted to organization administrators.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization Id</param>\n    /// <param name=\"status\">Optional filter for task status. If not provided, returns tasks of all statuses.</param>\n    [HttpGet(\"organization\")]\n    public async Task<ListResponseModel<SecurityTasksResponseModel>> ListForOrganization(\n        [FromQuery] Guid organizationId, [FromQuery] SecurityTaskStatus? status)\n    {\n        var securityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(organizationId, status);\n        var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();\n        return new ListResponseModel<SecurityTasksResponseModel>(response);\n    }\n\n    /// <summary>\n    /// Retrieves security task metrics for an organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization Id</param>\n    [HttpGet(\"{organizationId:guid}/metrics\")]\n    public async Task<SecurityTaskMetricsResponseModel> GetTaskMetricsForOrganization([FromRoute] Guid organizationId)\n    {\n        var metrics = await _getTaskMetricsForOrganizationQuery.GetTaskMetrics(organizationId);\n\n        return new SecurityTaskMetricsResponseModel(metrics.CompletedTasks, metrics.TotalTasks);\n    }\n\n    /// <summary>\n    /// Bulk create security tasks for an organization.\n    /// </summary>\n    /// <param name=\"orgId\"></param>\n    /// <param name=\"model\"></param>\n    /// <returns>A list response model containing the security tasks created for the organization.</returns>\n    [HttpPost(\"{orgId:guid}/bulk-create\")]\n    public async Task<ListResponseModel<SecurityTasksResponseModel>> BulkCreateTasks(Guid orgId,\n        [FromBody] BulkCreateSecurityTasksRequestModel model)\n    {\n        // Retrieve existing pending security tasks for the organization\n        var pendingSecurityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(orgId, SecurityTaskStatus.Pending);\n\n        // Get the security tasks that are already associated with a cipher within the submitted model\n        var existingTasks = pendingSecurityTasks.Where(x => model.Tasks.Any(y => y.CipherId == x.CipherId)).ToList();\n\n        // Get tasks that need to be created\n        var tasksToCreateFromModel = model.Tasks.Where(x => !existingTasks.Any(y => y.CipherId == x.CipherId)).ToList();\n\n        ICollection<SecurityTask> newSecurityTasks = new List<SecurityTask>();\n\n        if (tasksToCreateFromModel.Count != 0)\n        {\n            newSecurityTasks = await _createManyTasksCommand.CreateAsync(orgId, tasksToCreateFromModel);\n        }\n\n        // Combine existing tasks and newly created tasks\n        var allTasks = existingTasks.Concat(newSecurityTasks);\n\n        await _createManyTaskNotificationsCommand.CreateAsync(orgId, allTasks);\n\n        var response = allTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();\n        return new ListResponseModel<SecurityTasksResponseModel>(response);\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Controllers/SyncController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Api.Vault.Controllers;\n\n[Route(\"sync\")]\n[Authorize(\"Application\")]\npublic class SyncController : Controller\n{\n    private readonly IUserService _userService;\n    private readonly IFolderRepository _folderRepository;\n    private readonly ICipherRepository _cipherRepository;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly ICollectionCipherRepository _collectionCipherRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IPolicyRepository _policyRepository;\n    private readonly ISendRepository _sendRepository;\n    private readonly GlobalSettings _globalSettings;\n    private readonly ICurrentContext _currentContext;\n    private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);\n    private readonly IFeatureService _featureService;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;\n    private readonly IUserAccountKeysQuery _userAccountKeysQuery;\n\n    public SyncController(\n        IUserService userService,\n        IFolderRepository folderRepository,\n        ICipherRepository cipherRepository,\n        ICollectionRepository collectionRepository,\n        ICollectionCipherRepository collectionCipherRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IProviderUserRepository providerUserRepository,\n        IPolicyRepository policyRepository,\n        ISendRepository sendRepository,\n        GlobalSettings globalSettings,\n        ICurrentContext currentContext,\n        IFeatureService featureService,\n        IApplicationCacheService applicationCacheService,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IWebAuthnCredentialRepository webAuthnCredentialRepository,\n        IUserAccountKeysQuery userAccountKeysQuery)\n    {\n        _userService = userService;\n        _folderRepository = folderRepository;\n        _cipherRepository = cipherRepository;\n        _collectionRepository = collectionRepository;\n        _collectionCipherRepository = collectionCipherRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _providerUserRepository = providerUserRepository;\n        _policyRepository = policyRepository;\n        _sendRepository = sendRepository;\n        _globalSettings = globalSettings;\n        _currentContext = currentContext;\n        _featureService = featureService;\n        _applicationCacheService = applicationCacheService;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _webAuthnCredentialRepository = webAuthnCredentialRepository;\n        _userAccountKeysQuery = userAccountKeysQuery;\n    }\n\n    [HttpGet(\"\")]\n    public async Task<SyncResponseModel> Get([FromQuery] bool excludeDomains = false)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new BadRequestException(\"User not found.\");\n        }\n\n        var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id,\n            OrganizationUserStatusType.Confirmed);\n        var providerUserDetails = await _providerUserRepository.GetManyDetailsByUserAsync(user.Id,\n            ProviderUserStatusType.Confirmed);\n        var providerUserOrganizationDetails =\n            await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,\n                ProviderUserStatusType.Confirmed);\n        var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);\n\n        var folders = await _folderRepository.GetManyByUserIdAsync(user.Id);\n        var allCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: hasEnabledOrgs);\n        var ciphers = FilterSSHKeys(allCiphers);\n        var sends = await _sendRepository.GetManyByUserIdAsync(user.Id);\n\n        IEnumerable<CollectionDetails> collections = null;\n        IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null;\n        IEnumerable<Policy> policies = await _policyRepository.GetManyByUserIdAsync(user.Id);\n\n        if (hasEnabledOrgs)\n        {\n            collections = await _collectionRepository.GetManyByUserIdAsync(user.Id);\n            var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id);\n            collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);\n        }\n\n        var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);\n        var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);\n        var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);\n        var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);\n\n        var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var webAuthnCredentials = _featureService.IsEnabled(FeatureFlagKeys.PM2035PasskeyUnlock)\n            ? await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id)\n            : [];\n\n        UserAccountKeysData userAccountKeys = null;\n        // JIT TDE users and some broken/old users may not have a private key.\n        if (!string.IsNullOrWhiteSpace(user.PrivateKey))\n        {\n            userAccountKeys = await _userAccountKeysQuery.Run(user);\n        }\n\n        var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,\n            organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,\n            folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends, webAuthnCredentials);\n        return response;\n    }\n\n    private ICollection<CipherDetails> FilterSSHKeys(ICollection<CipherDetails> ciphers)\n    {\n        if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion || _featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride))\n        {\n            return ciphers;\n        }\n        else\n        {\n            return ciphers.Where(c => c.Type != Core.Vault.Enums.CipherType.SSHKey).ToList();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/CipherAttachmentModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models;\n\npublic class CipherAttachmentModel\n{\n    public CipherAttachmentModel() { }\n\n    public CipherAttachmentModel(CipherAttachment.MetaData data)\n    {\n        FileName = data.FileName;\n        Key = data.Key;\n    }\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string FileName { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Key { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/CipherCardModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models;\n\npublic class CipherCardModel\n{\n    public CipherCardModel() { }\n\n    public CipherCardModel(CipherCardData data)\n    {\n        CardholderName = data.CardholderName;\n        Brand = data.Brand;\n        Number = data.Number;\n        ExpMonth = data.ExpMonth;\n        ExpYear = data.ExpYear;\n        Code = data.Code;\n    }\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string CardholderName { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Brand { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Number { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string ExpMonth { get; set; }\n    [EncryptedString]\n    [StringLength(1000)]\n    public string ExpYear { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Code { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/CipherFido2CredentialModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models;\n\npublic class CipherFido2CredentialModel\n{\n    public CipherFido2CredentialModel() { }\n\n    public CipherFido2CredentialModel(CipherLoginFido2CredentialData data)\n    {\n        CredentialId = data.CredentialId;\n        KeyType = data.KeyType;\n        KeyAlgorithm = data.KeyAlgorithm;\n        KeyCurve = data.KeyCurve;\n        KeyValue = data.KeyValue;\n        RpId = data.RpId;\n        RpName = data.RpName;\n        UserHandle = data.UserHandle;\n        UserName = data.UserName;\n        UserDisplayName = data.UserDisplayName;\n        Counter = data.Counter;\n        Discoverable = data.Discoverable;\n        CreationDate = data.CreationDate;\n    }\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string CredentialId { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string KeyType { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string KeyAlgorithm { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string KeyCurve { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string KeyValue { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string RpId { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string RpName { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string UserHandle { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string UserName { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string UserDisplayName { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Counter { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Discoverable { get; set; }\n    [Required]\n    public DateTime CreationDate { get; set; }\n\n    public CipherLoginFido2CredentialData ToCipherLoginFido2CredentialData()\n    {\n        return new CipherLoginFido2CredentialData\n        {\n            CredentialId = CredentialId,\n            KeyType = KeyType,\n            KeyAlgorithm = KeyAlgorithm,\n            KeyCurve = KeyCurve,\n            KeyValue = KeyValue,\n            RpId = RpId,\n            RpName = RpName,\n            UserHandle = UserHandle,\n            UserName = UserName,\n            UserDisplayName = UserDisplayName,\n            Counter = Counter,\n            Discoverable = Discoverable,\n            CreationDate = CreationDate\n        };\n    }\n}\n\nstatic class CipherFido2CredentialModelExtensions\n{\n    public static CipherLoginFido2CredentialData[] ToCipherLoginFido2CredentialData(this CipherFido2CredentialModel[] models)\n    {\n        return models.Select(m => m.ToCipherLoginFido2CredentialData()).ToArray();\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/CipherFieldModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models;\n\npublic class CipherFieldModel\n{\n    public CipherFieldModel() { }\n\n    public CipherFieldModel(CipherFieldData data)\n    {\n        Type = data.Type;\n        Name = data.Name;\n        Value = data.Value;\n        LinkedId = data.LinkedId ?? null;\n    }\n\n    public FieldType Type { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Name { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(5000)]\n    public string Value { get; set; }\n    public int? LinkedId { get; set; }\n\n    public CipherFieldData ToCipherFieldData()\n    {\n        return new CipherFieldData\n        {\n            Type = Type,\n            Name = Name,\n            Value = Value,\n            LinkedId = LinkedId ?? null,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/CipherIdentityModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models;\n\npublic class CipherIdentityModel\n{\n    public CipherIdentityModel() { }\n\n    public CipherIdentityModel(CipherIdentityData data)\n    {\n        Title = data.Title;\n        FirstName = data.FirstName;\n        MiddleName = data.MiddleName;\n        LastName = data.LastName;\n        Address1 = data.Address1;\n        Address2 = data.Address2;\n        Address3 = data.Address3;\n        City = data.City;\n        State = data.State;\n        PostalCode = data.PostalCode;\n        Country = data.Country;\n        Company = data.Company;\n        Email = data.Email;\n        Phone = data.Phone;\n        SSN = data.SSN;\n        Username = data.Username;\n        PassportNumber = data.PassportNumber;\n        LicenseNumber = data.LicenseNumber;\n    }\n\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Title { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string FirstName { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string MiddleName { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string LastName { get; set; }\n    [EncryptedString]\n    [StringLength(1000)]\n    public string Address1 { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Address2 { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Address3 { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string City { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string State { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string PostalCode { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Country { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Company { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Email { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Phone { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string SSN { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Username { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string PassportNumber { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string LicenseNumber { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/CipherLoginModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models;\n\npublic class CipherLoginModel\n{\n    public CipherLoginModel() { }\n\n    public CipherLoginModel(CipherLoginData data)\n    {\n        Uris = data.Uris?.Select(u => new CipherLoginUriModel(u))?.ToList();\n        if (!Uris?.Any() ?? true)\n        {\n            Uri = data.Uri;\n        }\n\n        if (data.Fido2Credentials != null)\n        {\n            Fido2Credentials = data.Fido2Credentials.Select(key => new CipherFido2CredentialModel(key)).ToArray();\n        }\n\n        Username = data.Username;\n        Password = data.Password;\n        PasswordRevisionDate = data.PasswordRevisionDate;\n        Totp = data.Totp;\n        AutofillOnPageLoad = data.AutofillOnPageLoad;\n    }\n\n    [EncryptedString]\n    [EncryptedStringLength(10000)]\n    public string Uri\n    {\n        get => Uris?.FirstOrDefault()?.Uri;\n        set\n        {\n            if (string.IsNullOrWhiteSpace(value))\n            {\n                return;\n            }\n\n            if (Uris == null)\n            {\n                Uris = new List<CipherLoginUriModel>();\n            }\n\n            Uris.Add(new CipherLoginUriModel(value));\n        }\n    }\n    public List<CipherLoginUriModel> Uris { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Username { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(5000)]\n    public string Password { get; set; }\n    public DateTime? PasswordRevisionDate { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Totp { get; set; }\n    public bool? AutofillOnPageLoad { get; set; }\n    public CipherFido2CredentialModel[] Fido2Credentials { get; set; }\n\n    public class CipherLoginUriModel\n    {\n        public CipherLoginUriModel() { }\n\n        public CipherLoginUriModel(string uri)\n        {\n            Uri = uri;\n        }\n\n        public CipherLoginUriModel(CipherLoginData.CipherLoginUriData uri)\n        {\n            Uri = uri.Uri;\n            UriChecksum = uri.UriChecksum;\n            Match = uri.Match;\n        }\n\n        [EncryptedString]\n        [EncryptedStringLength(10000)]\n        public string Uri { get; set; }\n        [EncryptedString]\n        [EncryptedStringLength(10000)]\n        public string UriChecksum { get; set; }\n        public UriMatchType? Match { get; set; } = null;\n\n        public CipherLoginData.CipherLoginUriData ToCipherLoginUriData()\n        {\n            return new CipherLoginData.CipherLoginUriData { Uri = Uri, UriChecksum = UriChecksum, Match = Match, };\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/CipherPasswordHistoryModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models;\n\npublic class CipherPasswordHistoryModel\n{\n    public CipherPasswordHistoryModel() { }\n\n    public CipherPasswordHistoryModel(CipherPasswordHistoryData data)\n    {\n        Password = data.Password;\n        LastUsedDate = data.LastUsedDate;\n    }\n\n    [EncryptedString]\n    [EncryptedStringLength(5000)]\n    [Required]\n    public string Password { get; set; }\n    [Required]\n    public DateTime? LastUsedDate { get; set; }\n\n    public CipherPasswordHistoryData ToCipherPasswordHistoryData()\n    {\n        return new CipherPasswordHistoryData { Password = Password, LastUsedDate = LastUsedDate.Value, };\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/CipherSSHKeyModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models;\n\npublic class CipherSSHKeyModel\n{\n    public CipherSSHKeyModel() { }\n\n    public CipherSSHKeyModel(CipherSSHKeyData data)\n    {\n        PrivateKey = data.PrivateKey;\n        PublicKey = data.PublicKey;\n        KeyFingerprint = data.KeyFingerprint;\n    }\n\n    [EncryptedString]\n    [EncryptedStringLength(5000)]\n    public string PrivateKey { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(5000)]\n    public string PublicKey { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string KeyFingerprint { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/CipherSecureNoteModel.cs",
    "content": "﻿using Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models;\n\npublic class CipherSecureNoteModel\n{\n    public CipherSecureNoteModel() { }\n\n    public CipherSecureNoteModel(CipherSecureNoteData data)\n    {\n        Type = data.Type;\n    }\n\n    public SecureNoteType Type { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Request/AttachmentRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Api.Vault.Models.Request;\n\npublic class AttachmentRequestModel\n{\n    public string Key { get; set; }\n    public string FileName { get; set; }\n    public long FileSize { get; set; }\n    public bool AdminRequest { get; set; } = false;\n\n    /// <summary>\n    /// The last known revision date of the Cipher that this attachment belongs to.\n    /// </summary>\n    public DateTime? LastKnownRevisionDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Vault.Models.Api;\n\nnamespace Bit.Api.Vault.Models.Request;\n\npublic class BulkCreateSecurityTasksRequestModel\n{\n    public IEnumerable<SecurityTaskCreateRequest> Tasks { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Request/CipherBulkUpdateCollectionsRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Api.Vault.Models.Request;\n\npublic class CipherBulkUpdateCollectionsRequestModel\n{\n    public Guid OrganizationId { get; set; }\n\n    public IEnumerable<Guid> CipherIds { get; set; }\n\n    public IEnumerable<Guid> CollectionIds { get; set; }\n\n    /// <summary>\n    /// If true, the collections will be removed from the ciphers. Otherwise, they will be added.\n    /// </summary>\n    public bool RemoveCollections { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Request/CipherPartialRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Api.Vault.Models.Request;\n\npublic class CipherPartialRequestModel\n{\n    [StringLength(36)]\n    public string FolderId { get; set; }\n    public bool Favorite { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Request/CipherRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models.Request;\n\npublic class CipherRequestModel\n{\n    /// <summary>\n    /// The Id of the user that encrypted the cipher. It should always represent a UserId.\n    /// </summary>\n    public Guid? EncryptedFor { get; set; }\n    public CipherType Type { get; set; }\n\n    [StringLength(36)]\n    public string OrganizationId { get; set; }\n    public string FolderId { get; set; }\n    public bool Favorite { get; set; }\n    public CipherRepromptType Reprompt { get; set; }\n    public string Key { get; set; }\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Name { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(10000)]\n    public string Notes { get; set; }\n    public IEnumerable<CipherFieldModel> Fields { get; set; }\n    public IEnumerable<CipherPasswordHistoryModel> PasswordHistory { get; set; }\n    [Obsolete]\n    public Dictionary<string, string> Attachments { get; set; }\n    // TODO: Rename to Attachments whenever the above is finally removed.\n    public Dictionary<string, CipherAttachmentModel> Attachments2 { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherLoginModel Login { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherCardModel Card { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherIdentityModel Identity { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherSecureNoteModel SecureNote { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherSSHKeyModel SSHKey { get; set; }\n\n    /// <summary>\n    /// JSON string containing cipher-specific data\n    /// </summary>\n    [StringLength(500000)]\n    public string Data { get; set; }\n    public DateTime? LastKnownRevisionDate { get; set; } = null;\n    public DateTime? ArchivedDate { get; set; }\n\n    public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true)\n    {\n        var hasOrgId = !string.IsNullOrWhiteSpace(OrganizationId);\n        var cipher = new CipherDetails\n        {\n            Type = Type,\n            UserId = !hasOrgId ? (Guid?)userId : null,\n            OrganizationId = allowOrgIdSet && hasOrgId ? new Guid(OrganizationId) : (Guid?)null,\n            Edit = true,\n            ViewPassword = true,\n        };\n        ToCipherDetails(cipher);\n        return cipher;\n    }\n\n    public CipherDetails ToCipherDetails(CipherDetails existingCipher)\n    {\n        existingCipher.FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : (Guid?)new Guid(FolderId);\n        existingCipher.Favorite = Favorite;\n        existingCipher.ArchivedDate = ArchivedDate;\n        ToCipher(existingCipher);\n        return existingCipher;\n    }\n\n    public Cipher ToCipher(Cipher existingCipher, Guid? userId = null)\n    {\n        // If Data field is provided, use it directly\n        if (!string.IsNullOrWhiteSpace(Data))\n        {\n            existingCipher.Data = Data;\n        }\n        else\n        {\n            // Fallback to structured fields\n            switch (existingCipher.Type)\n            {\n                case CipherType.Login:\n                    var loginData = ToCipherLoginData();\n                    var loginJson = JsonSerializer.Serialize(loginData, JsonHelpers.IgnoreWritingNull);\n                    var loginObj = JsonDocument.Parse(loginJson);\n                    var loginDict = JsonSerializer.Deserialize<Dictionary<string, object>>(loginJson);\n                    loginDict?.Remove(nameof(CipherLoginData.Uri));\n\n                    existingCipher.Data = JsonSerializer.Serialize(loginDict, JsonHelpers.IgnoreWritingNull);\n                    break;\n                case CipherType.Card:\n                    existingCipher.Data = JsonSerializer.Serialize(ToCipherCardData(), JsonHelpers.IgnoreWritingNull);\n                    break;\n                case CipherType.Identity:\n                    existingCipher.Data =\n                        JsonSerializer.Serialize(ToCipherIdentityData(), JsonHelpers.IgnoreWritingNull);\n                    break;\n                case CipherType.SecureNote:\n                    existingCipher.Data =\n                        JsonSerializer.Serialize(ToCipherSecureNoteData(), JsonHelpers.IgnoreWritingNull);\n                    break;\n                case CipherType.SSHKey:\n                    existingCipher.Data = JsonSerializer.Serialize(ToCipherSSHKeyData(), JsonHelpers.IgnoreWritingNull);\n                    break;\n                default:\n                    throw new ArgumentException(\"Unsupported type: \" + nameof(Type) + \".\");\n            }\n        }\n\n        var userIdKey = userId.HasValue ? userId.ToString().ToUpperInvariant() : null;\n        existingCipher.Reprompt = Reprompt;\n        existingCipher.Key = Key;\n        existingCipher.Folders = UpdateUserSpecificJsonField(existingCipher.Folders, userIdKey, FolderId);\n        existingCipher.Favorites = UpdateUserSpecificJsonField(existingCipher.Favorites, userIdKey, Favorite);\n        existingCipher.Archives = UpdateUserSpecificJsonField(existingCipher.Archives, userIdKey, ArchivedDate);\n\n        var hasAttachments2 = (Attachments2?.Count ?? 0) > 0;\n        var hasAttachments = (Attachments?.Count ?? 0) > 0;\n\n        if (!hasAttachments2 && !hasAttachments)\n        {\n            return existingCipher;\n        }\n\n        var attachments = existingCipher.GetAttachments();\n        if ((attachments?.Count ?? 0) == 0)\n        {\n            return existingCipher;\n        }\n\n        if (hasAttachments2)\n        {\n            foreach (var attachment in attachments)\n            {\n                if (!Attachments2.TryGetValue(attachment.Key, out var attachment2))\n                {\n                    continue;\n                }\n                attachment.Value.FileName = attachment2.FileName;\n                attachment.Value.Key = attachment2.Key;\n            }\n        }\n        else if (hasAttachments)\n        {\n            foreach (var attachment in attachments)\n            {\n                if (!Attachments.TryGetValue(attachment.Key, out var attachmentForKey))\n                {\n                    continue;\n                }\n                attachment.Value.FileName = attachmentForKey;\n                attachment.Value.Key = null;\n            }\n        }\n\n        existingCipher.SetAttachments(attachments);\n        return existingCipher;\n    }\n\n    public Cipher ToOrganizationCipher()\n    {\n        if (string.IsNullOrWhiteSpace(OrganizationId))\n        {\n            throw new ArgumentNullException(nameof(OrganizationId));\n        }\n\n        return ToCipher(new Cipher\n        {\n            Type = Type,\n            OrganizationId = new Guid(OrganizationId)\n        });\n    }\n\n    public CipherDetails ToOrganizationCipherDetails(Guid orgId)\n    {\n        return ToCipherDetails(new CipherDetails\n        {\n            Type = Type,\n            OrganizationId = orgId,\n            Edit = true\n        });\n    }\n\n    private CipherLoginData ToCipherLoginData()\n    {\n        return new CipherLoginData\n        {\n            Name = Name,\n            Notes = Notes,\n            Fields = Fields?.Select(f => f.ToCipherFieldData()),\n            PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()),\n\n            Uris =\n                Login.Uris?.Where(u => u != null)\n                    .Select(u => u.ToCipherLoginUriData()),\n            Username = Login.Username,\n            Password = Login.Password,\n            PasswordRevisionDate = Login.PasswordRevisionDate,\n            Totp = Login.Totp,\n            AutofillOnPageLoad = Login.AutofillOnPageLoad,\n            Fido2Credentials = Login.Fido2Credentials == null ? null : Login.Fido2Credentials.ToCipherLoginFido2CredentialData(),\n        };\n    }\n\n    private CipherIdentityData ToCipherIdentityData()\n    {\n        return new CipherIdentityData\n        {\n            Name = Name,\n            Notes = Notes,\n            Fields = Fields?.Select(f => f.ToCipherFieldData()),\n            PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()),\n\n            Title = Identity.Title,\n            FirstName = Identity.FirstName,\n            MiddleName = Identity.MiddleName,\n            LastName = Identity.LastName,\n            Address1 = Identity.Address1,\n            Address2 = Identity.Address2,\n            Address3 = Identity.Address3,\n            City = Identity.City,\n            State = Identity.State,\n            PostalCode = Identity.PostalCode,\n            Country = Identity.Country,\n            Company = Identity.Company,\n            Email = Identity.Email,\n            Phone = Identity.Phone,\n            SSN = Identity.SSN,\n            Username = Identity.Username,\n            PassportNumber = Identity.PassportNumber,\n            LicenseNumber = Identity.LicenseNumber,\n        };\n    }\n\n    private CipherCardData ToCipherCardData()\n    {\n        return new CipherCardData\n        {\n            Name = Name,\n            Notes = Notes,\n            Fields = Fields?.Select(f => f.ToCipherFieldData()),\n            PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()),\n\n            CardholderName = Card.CardholderName,\n            Brand = Card.Brand,\n            Number = Card.Number,\n            ExpMonth = Card.ExpMonth,\n            ExpYear = Card.ExpYear,\n            Code = Card.Code,\n        };\n    }\n\n    private CipherSecureNoteData ToCipherSecureNoteData()\n    {\n        return new CipherSecureNoteData\n        {\n            Name = Name,\n            Notes = Notes,\n            Fields = Fields?.Select(f => f.ToCipherFieldData()),\n            PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()),\n\n            Type = SecureNote.Type,\n        };\n    }\n\n    private CipherSSHKeyData ToCipherSSHKeyData()\n    {\n        return new CipherSSHKeyData\n        {\n            Name = Name,\n            Notes = Notes,\n            Fields = Fields?.Select(f => f.ToCipherFieldData()),\n            PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()),\n\n            PrivateKey = SSHKey.PrivateKey,\n            PublicKey = SSHKey.PublicKey,\n            KeyFingerprint = SSHKey.KeyFingerprint,\n        };\n    }\n\n    /// <summary>\n    /// Updates a JSON string representing a dictionary by adding, updating, or removing a key-value pair\n    /// based on the provided userIdKey and newValue.\n    /// </summary>\n    private static string UpdateUserSpecificJsonField(string existingJson, string userIdKey, object newValue)\n    {\n        if (userIdKey == null)\n        {\n            return existingJson;\n        }\n\n        var jsonDict = string.IsNullOrWhiteSpace(existingJson)\n            ? new Dictionary<string, object>()\n            : JsonSerializer.Deserialize<Dictionary<string, object>>(existingJson) ?? new Dictionary<string, object>();\n\n        var shouldRemove = newValue == null ||\n                          (newValue is string strValue && string.IsNullOrWhiteSpace(strValue)) ||\n                          (newValue is bool boolValue && !boolValue);\n\n        if (shouldRemove)\n        {\n            jsonDict.Remove(userIdKey);\n        }\n        else\n        {\n            jsonDict[userIdKey] = newValue is string str ? str.ToUpperInvariant() : newValue;\n        }\n\n        return jsonDict.Count == 0 ? null : JsonSerializer.Serialize(jsonDict);\n    }\n}\n\npublic class CipherWithIdRequestModel : CipherRequestModel\n{\n    [Required]\n    public Guid? Id { get; set; }\n}\n\npublic class CipherCreateRequestModel : IValidatableObject\n{\n    public IEnumerable<Guid> CollectionIds { get; set; }\n    [Required]\n    public CipherRequestModel Cipher { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (!string.IsNullOrWhiteSpace(Cipher.OrganizationId) && (!CollectionIds?.Any() ?? true))\n        {\n            yield return new ValidationResult(\"You must select at least one collection.\",\n               new string[] { nameof(CollectionIds) });\n        }\n    }\n}\n\npublic class CipherShareRequestModel : IValidatableObject\n{\n    [Required]\n    public IEnumerable<string> CollectionIds { get; set; }\n    [Required]\n    public CipherRequestModel Cipher { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (string.IsNullOrWhiteSpace(Cipher.OrganizationId))\n        {\n            yield return new ValidationResult(\"Cipher OrganizationId is required.\",\n                new string[] { nameof(Cipher.OrganizationId) });\n        }\n\n        if (!CollectionIds?.Any() ?? true)\n        {\n            yield return new ValidationResult(\"You must select at least one collection.\",\n                new string[] { nameof(CollectionIds) });\n        }\n    }\n}\n\npublic class CipherCollectionsRequestModel\n{\n    [Required]\n    public IEnumerable<string> CollectionIds { get; set; }\n}\n\npublic class CipherBulkArchiveRequestModel\n{\n    [Required]\n    public IEnumerable<Guid> Ids { get; set; }\n}\n\npublic class CipherBulkDeleteRequestModel\n{\n    [Required]\n    public IEnumerable<string> Ids { get; set; }\n    public string OrganizationId { get; set; }\n}\n\npublic class CipherBulkUnarchiveRequestModel\n{\n    [Required]\n    public IEnumerable<Guid> Ids { get; set; }\n}\n\npublic class CipherBulkRestoreRequestModel\n{\n    [Required]\n    public IEnumerable<string> Ids { get; set; }\n    public Guid OrganizationId { get; set; }\n}\n\npublic class CipherBulkMoveRequestModel\n{\n    [Required]\n    public IEnumerable<string> Ids { get; set; }\n    public string FolderId { get; set; }\n}\n\npublic class CipherBulkShareRequestModel : IValidatableObject\n{\n    [Required]\n    public IEnumerable<string> CollectionIds { get; set; }\n    [Required]\n    public IEnumerable<CipherWithIdRequestModel> Ciphers { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (!Ciphers?.Any() ?? true)\n        {\n            yield return new ValidationResult(\"You must select at least one cipher.\",\n                new string[] { nameof(Ciphers) });\n        }\n        else\n        {\n            var allHaveIds = true;\n            var organizationIds = new HashSet<string>();\n            foreach (var c in Ciphers)\n            {\n                organizationIds.Add(c.OrganizationId);\n                if (allHaveIds)\n                {\n                    allHaveIds = !(!c.Id.HasValue || string.IsNullOrWhiteSpace(c.OrganizationId));\n                }\n            }\n\n            if (!allHaveIds)\n            {\n                yield return new ValidationResult(\"All Ciphers must have an Id and OrganizationId.\",\n                    new string[] { nameof(Ciphers) });\n            }\n            else if (organizationIds.Count != 1)\n            {\n                yield return new ValidationResult(\"All ciphers must be for the same organization.\");\n            }\n        }\n\n        if (!CollectionIds?.Any() ?? true)\n        {\n            yield return new ValidationResult(\"You must select at least one collection.\",\n                new string[] { nameof(CollectionIds) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Request/FolderRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Api.Vault.Models.Request;\n\npublic class FolderRequestModel\n{\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(1000)]\n    public string Name { get; set; }\n\n    public Folder ToFolder(Guid userId)\n    {\n        return ToFolder(new Folder\n        {\n            UserId = userId\n        });\n    }\n\n    public virtual Folder ToFolder(Folder existingFolder)\n    {\n        existingFolder.Name = Name;\n        return existingFolder;\n    }\n}\n\npublic class FolderWithIdRequestModel : FolderRequestModel\n{\n    public Guid? Id { get; set; }\n\n    public override Folder ToFolder(Folder existingFolder)\n    {\n        existingFolder.Id = Id ?? Guid.Empty;\n        return base.ToFolder(existingFolder);\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/AttachmentResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models.Response;\n\npublic class AttachmentResponseModel : ResponseModel\n{\n    public AttachmentResponseModel(AttachmentResponseData data) : base(\"attachment\")\n    {\n        Id = data.Id;\n        Url = data.Url;\n        FileName = data.Data.FileName;\n        Key = data.Data.Key;\n        Size = data.Data.Size.ToString();\n        SizeName = CoreHelpers.ReadableBytesSize(data.Data.Size);\n    }\n\n    public AttachmentResponseModel(string id, CipherAttachment.MetaData data, Cipher cipher,\n        IGlobalSettings globalSettings)\n        : base(\"attachment\")\n    {\n        Id = id;\n        Url = $\"{globalSettings.Attachment.BaseUrl}/{cipher.Id}/{id}\";\n        FileName = data.FileName;\n        Key = data.Key;\n        Size = data.Size.ToString();\n        SizeName = CoreHelpers.ReadableBytesSize(data.Size);\n    }\n\n    public string Id { get; set; }\n    public string Url { get; set; }\n    public string FileName { get; set; }\n    public string Key { get; set; }\n    public string Size { get; set; }\n    public string SizeName { get; set; }\n\n    public static IEnumerable<AttachmentResponseModel> FromCipher(Cipher cipher, IGlobalSettings globalSettings)\n    {\n        var attachments = cipher.GetAttachments();\n        if (attachments == null)\n        {\n            return null;\n        }\n\n        return attachments.Select(a => new AttachmentResponseModel(a.Key, a.Value, cipher, globalSettings));\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/AttachmentUploadDataResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Api.Vault.Models.Response;\n\npublic class AttachmentUploadDataResponseModel : ResponseModel\n{\n    public string AttachmentId { get; set; }\n    public string Url { get; set; }\n    public FileUploadType FileUploadType { get; set; }\n    public CipherResponseModel CipherResponse { get; set; }\n    public CipherMiniResponseModel CipherMiniResponse { get; set; }\n\n    public AttachmentUploadDataResponseModel() : base(\"attachment-fileUpload\") { }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Vault.Authorization.Permissions;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models.Response;\n\npublic record CipherPermissionsResponseModel\n{\n    public bool Delete { get; init; }\n    public bool Restore { get; init; }\n\n    public CipherPermissionsResponseModel(\n        User user,\n        CipherDetails cipherDetails,\n        IDictionary<Guid, OrganizationAbility> organizationAbilities)\n    {\n        OrganizationAbility organizationAbility = null;\n        if (cipherDetails.OrganizationId.HasValue && !organizationAbilities.TryGetValue(cipherDetails.OrganizationId.Value, out organizationAbility))\n        {\n            throw new Exception(\"OrganizationAbility not found for organization cipher.\");\n        }\n\n        Delete = NormalCipherPermissions.CanDelete(user, cipherDetails, organizationAbility);\n        Restore = NormalCipherPermissions.CanRestore(user, cipherDetails, organizationAbility);\n    }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/CipherResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models.Response;\n\npublic class CipherMiniResponseModel : ResponseModel\n{\n    public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bool orgUseTotp, string obj = \"cipherMini\")\n        : base(obj)\n    {\n        if (cipher == null)\n        {\n            throw new ArgumentNullException(nameof(cipher));\n        }\n\n        Id = cipher.Id;\n        Type = cipher.Type;\n        Data = cipher.Data;\n\n        CipherData cipherData;\n        switch (cipher.Type)\n        {\n            case CipherType.Login:\n                var loginData = JsonSerializer.Deserialize<CipherLoginData>(cipher.Data);\n                cipherData = loginData;\n                Login = new CipherLoginModel(loginData);\n                break;\n            case CipherType.SecureNote:\n                var secureNoteData = JsonSerializer.Deserialize<CipherSecureNoteData>(cipher.Data);\n                cipherData = secureNoteData;\n                SecureNote = new CipherSecureNoteModel(secureNoteData);\n                break;\n            case CipherType.Card:\n                var cardData = JsonSerializer.Deserialize<CipherCardData>(cipher.Data);\n                cipherData = cardData;\n                Card = new CipherCardModel(cardData);\n                break;\n            case CipherType.Identity:\n                var identityData = JsonSerializer.Deserialize<CipherIdentityData>(cipher.Data);\n                cipherData = identityData;\n                Identity = new CipherIdentityModel(identityData);\n                break;\n            case CipherType.SSHKey:\n                var sshKeyData = JsonSerializer.Deserialize<CipherSSHKeyData>(cipher.Data);\n                cipherData = sshKeyData;\n                SSHKey = new CipherSSHKeyModel(sshKeyData);\n                break;\n            default:\n                throw new ArgumentException(\"Unsupported \" + nameof(Type) + \".\");\n        }\n\n        Name = cipherData.Name;\n        Notes = cipherData.Notes;\n        Fields = cipherData.Fields?.Select(f => new CipherFieldModel(f));\n        PasswordHistory = cipherData.PasswordHistory?.Select(ph => new CipherPasswordHistoryModel(ph));\n        RevisionDate = cipher.RevisionDate;\n        OrganizationId = cipher.OrganizationId;\n        Attachments = AttachmentResponseModel.FromCipher(cipher, globalSettings);\n        OrganizationUseTotp = orgUseTotp;\n        CreationDate = cipher.CreationDate;\n        DeletedDate = cipher.DeletedDate;\n        Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None);\n        Key = cipher.Key;\n    }\n\n    public Guid Id { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public CipherType Type { get; set; }\n    public string Data { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public string Name { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public string Notes { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherLoginModel Login { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherCardModel Card { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherIdentityModel Identity { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherSecureNoteModel SecureNote { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public CipherSSHKeyModel SSHKey { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public IEnumerable<CipherFieldModel> Fields { get; set; }\n\n    [Obsolete(\"Use Data instead.\")]\n    public IEnumerable<CipherPasswordHistoryModel> PasswordHistory { get; set; }\n    public IEnumerable<AttachmentResponseModel> Attachments { get; set; }\n    public bool OrganizationUseTotp { get; set; }\n    public DateTime RevisionDate { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime? DeletedDate { get; set; }\n    public CipherRepromptType Reprompt { get; set; }\n    public string Key { get; set; }\n}\n\npublic class CipherResponseModel : CipherMiniResponseModel\n{\n    public CipherResponseModel(\n        CipherDetails cipher,\n        User user,\n        IDictionary<Guid, OrganizationAbility> organizationAbilities,\n        IGlobalSettings globalSettings,\n        string obj = \"cipher\")\n        : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj)\n    {\n        FolderId = cipher.FolderId;\n        Favorite = cipher.Favorite;\n        Edit = cipher.Edit;\n        ArchivedDate = cipher.ArchivedDate;\n        ViewPassword = cipher.ViewPassword;\n        Permissions = new CipherPermissionsResponseModel(user, cipher, organizationAbilities);\n    }\n\n    public Guid? FolderId { get; set; }\n    public bool Favorite { get; set; }\n    public bool Edit { get; set; }\n    public bool ViewPassword { get; set; }\n    public DateTime? ArchivedDate { get; set; }\n    public CipherPermissionsResponseModel Permissions { get; set; }\n}\n\npublic class CipherDetailsResponseModel : CipherResponseModel\n{\n    public CipherDetailsResponseModel(\n        CipherDetails cipher,\n        User user,\n        IDictionary<Guid, OrganizationAbility> organizationAbilities,\n        GlobalSettings globalSettings,\n        IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, string obj = \"cipherDetails\")\n        : base(cipher, user, organizationAbilities, globalSettings, obj)\n    {\n        if (collectionCiphers?.TryGetValue(cipher.Id, out var collectionCipher) ?? false)\n        {\n            CollectionIds = collectionCipher.Select(c => c.CollectionId);\n        }\n        else\n        {\n            CollectionIds = [];\n        }\n    }\n\n    public CipherDetailsResponseModel(\n        CipherDetails cipher,\n        User user,\n        IDictionary<Guid, OrganizationAbility> organizationAbilities,\n        GlobalSettings globalSettings,\n        IEnumerable<CollectionCipher> collectionCiphers, string obj = \"cipherDetails\")\n        : base(cipher, user, organizationAbilities, globalSettings, obj)\n    {\n        CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? [];\n    }\n\n    public CipherDetailsResponseModel(\n        CipherDetailsWithCollections cipher,\n        User user,\n        IDictionary<Guid, OrganizationAbility> organizationAbilities,\n        GlobalSettings globalSettings,\n        string obj = \"cipherDetails\")\n        : base(cipher, user, organizationAbilities, globalSettings, obj)\n    {\n        CollectionIds = cipher.CollectionIds ?? [];\n    }\n\n    public IEnumerable<Guid> CollectionIds { get; set; }\n}\n\npublic class CipherMiniDetailsResponseModel : CipherMiniResponseModel\n{\n    public CipherMiniDetailsResponseModel(Cipher cipher, GlobalSettings globalSettings,\n        IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, bool orgUseTotp, string obj = \"cipherMiniDetails\")\n        : base(cipher, globalSettings, orgUseTotp, obj)\n    {\n        if (collectionCiphers?.TryGetValue(cipher.Id, out var collectionCipher) ?? false)\n        {\n            CollectionIds = collectionCipher.Select(c => c.CollectionId);\n        }\n        else\n        {\n            CollectionIds = [];\n        }\n    }\n\n    public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher,\n        GlobalSettings globalSettings, bool orgUseTotp, string obj = \"cipherMiniDetails\")\n        : base(cipher, globalSettings, orgUseTotp, obj)\n    {\n        CollectionIds = cipher.CollectionIds ?? [];\n    }\n\n    public CipherMiniDetailsResponseModel(CipherOrganizationDetailsWithCollections cipher,\n        GlobalSettings globalSettings, string obj = \"cipherMiniDetails\")\n        : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj)\n    {\n        CollectionIds = cipher.CollectionIds ?? new List<Guid>();\n    }\n\n    public IEnumerable<Guid> CollectionIds { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/DeleteAttachmentResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models.Response;\n\npublic class DeleteAttachmentResponseModel(DeleteAttachmentResponseData data, IGlobalSettings globalSettings)\n    : ResponseModel(\"deleteAttachment\")\n{\n    public CipherMiniResponseModel Cipher { get; set; } = new(data.Cipher, globalSettings, false);\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/FolderResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Api;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Api.Vault.Models.Response;\n\npublic class FolderResponseModel : ResponseModel\n{\n    public FolderResponseModel(Folder folder)\n        : base(\"folder\")\n    {\n        if (folder == null)\n        {\n            throw new ArgumentNullException(nameof(folder));\n        }\n\n        Id = folder.Id;\n        Name = folder.Name;\n        RevisionDate = folder.RevisionDate;\n    }\n\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/OptionalCipherDetailsResponseModel.cs",
    "content": "﻿using Bit.Api.Vault.Models.Response;\nusing Bit.Core.Models.Api;\n\n#nullable enable\n\npublic class OptionalCipherDetailsResponseModel : ResponseModel\n{\n    public bool Unavailable { get; set; }\n\n    public CipherDetailsResponseModel? Cipher { get; set; }\n\n    public OptionalCipherDetailsResponseModel()\n        : base(\"optionalCipherDetails\")\n    { }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/SecurityTaskMetricsResponseModel.cs",
    "content": "﻿namespace Bit.Api.Vault.Models.Response;\n\npublic class SecurityTaskMetricsResponseModel\n{\n\n    public SecurityTaskMetricsResponseModel(int completedTasks, int totalTasks)\n    {\n        CompletedTasks = completedTasks;\n        TotalTasks = totalTasks;\n    }\n\n    /// <summary>\n    /// Number of tasks that have been completed in the organization.\n    /// </summary>\n    public int CompletedTasks { get; set; }\n\n    /// <summary>\n    /// Total number of tasks in the organization, regardless of their status.\n    /// </summary>\n    public int TotalTasks { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/SecurityTasksResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\n\nnamespace Bit.Api.Vault.Models.Response;\n\npublic class SecurityTasksResponseModel : ResponseModel\n{\n    public SecurityTasksResponseModel(SecurityTask securityTask, string obj = \"securityTask\")\n        : base(obj)\n    {\n        ArgumentNullException.ThrowIfNull(securityTask);\n\n        Id = securityTask.Id;\n        OrganizationId = securityTask.OrganizationId;\n        CipherId = securityTask.CipherId;\n        Type = securityTask.Type;\n        Status = securityTask.Status;\n        CreationDate = securityTask.CreationDate;\n        RevisionDate = securityTask.RevisionDate;\n    }\n\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public Guid? CipherId { get; set; }\n    public SecurityTaskType Type { get; set; }\n    public SecurityTaskStatus Status { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n"
  },
  {
    "path": "src/Api/Vault/Models/Response/SyncResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Tools.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Api.Response;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Api.Response;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Api.Vault.Models.Response;\n\npublic class SyncResponseModel() : ResponseModel(\"sync\")\n{\n    public SyncResponseModel(\n        GlobalSettings globalSettings,\n        User user,\n        UserAccountKeysData userAccountKeysData,\n        bool userTwoFactorEnabled,\n        bool userHasPremiumFromOrganization,\n        IDictionary<Guid, OrganizationAbility> organizationAbilities,\n        IEnumerable<Guid> organizationIdsClaimingingUser,\n        IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails,\n        IEnumerable<ProviderUserProviderDetails> providerUserDetails,\n        IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,\n        IEnumerable<Folder> folders,\n        IEnumerable<CollectionDetails> collections,\n        IEnumerable<CipherDetails> ciphers,\n        IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersDict,\n        bool excludeDomains,\n        IEnumerable<Policy> policies,\n        IEnumerable<Send> sends,\n        IEnumerable<WebAuthnCredential> webAuthnCredentials)\n        : this()\n    {\n        Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,\n            providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);\n        Folders = folders.Select(f => new FolderResponseModel(f));\n        Ciphers = ciphers.Select(cipher =>\n            new CipherDetailsResponseModel(\n                cipher,\n                user,\n                organizationAbilities,\n                globalSettings,\n                collectionCiphersDict));\n        Collections = collections?.Select(\n            c => new CollectionDetailsResponseModel(c)) ?? new List<CollectionDetailsResponseModel>();\n        Domains = excludeDomains ? null : new DomainsResponseModel(user, false);\n        Policies = policies?.Select(p => new PolicyResponseModel(p)) ?? new List<PolicyResponseModel>();\n        Sends = sends.Select(s => new SendResponseModel(s));\n        var webAuthnPrfOptions = webAuthnCredentials\n            .Where(c => c.GetPrfStatus() == WebAuthnPrfStatus.Enabled)\n            .Select(c => new WebAuthnPrfDecryptionOption(\n                c.EncryptedPrivateKey,\n                c.EncryptedUserKey,\n                c.CredentialId,\n                [] // transports as empty array\n            ))\n            .ToArray();\n\n        UserDecryption = new UserDecryptionResponseModel\n        {\n            MasterPasswordUnlock = user.HasMasterPassword()\n                ? new MasterPasswordUnlockResponseModel\n                {\n                    Kdf = new MasterPasswordUnlockKdfResponseModel\n                    {\n                        KdfType = user.Kdf,\n                        Iterations = user.KdfIterations,\n                        Memory = user.KdfMemory,\n                        Parallelism = user.KdfParallelism\n                    },\n                    MasterKeyEncryptedUserKey = user.Key!,\n                    Salt = user.GetMasterPasswordSalt()\n                }\n                : null,\n            WebAuthnPrfOptions = webAuthnPrfOptions.Length > 0 ? webAuthnPrfOptions : null,\n            V2UpgradeToken = V2UpgradeTokenData.FromJson(user.V2UpgradeToken) is { } data\n                ? new V2UpgradeTokenResponseModel\n                {\n                    WrappedUserKey1 = data.WrappedUserKey1,\n                    WrappedUserKey2 = data.WrappedUserKey2\n                }\n                : null\n        };\n    }\n\n    public ProfileResponseModel Profile { get; set; }\n    public IEnumerable<FolderResponseModel> Folders { get; set; }\n    public IEnumerable<CollectionDetailsResponseModel> Collections { get; set; }\n    public IEnumerable<CipherDetailsResponseModel> Ciphers { get; set; }\n    public DomainsResponseModel Domains { get; set; }\n    public IEnumerable<PolicyResponseModel> Policies { get; set; }\n    public IEnumerable<SendResponseModel> Sends { get; set; }\n    public UserDecryptionResponseModel UserDecryption { get; set; }\n}\n"
  },
  {
    "path": "src/Api/appsettings.Development.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"fillAssistRules\": \"http://localhost:1495\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\",\n      \"internalScim\": \"http://localhost:44559\"\n    },\n    \"mail\": {\n      \"smtp\": {\n        \"host\": \"localhost\",\n        \"port\": 10250\n      }\n    },\n    \"attachment\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\",\n      \"baseUrl\": \"http://localhost:4000/attachments/\"\n    },\n    \"events\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"send\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\",\n      \"baseUrl\": \"http://localhost:4000/sendfiles/\"\n    },\n    \"notifications\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"storage\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"pricingUri\": \"https://billingpricing.qa.bitwarden.pw\"\n  }\n}\n"
  },
  {
    "path": "src/Api/appsettings.Production.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.bitwarden.com\",\n      \"api\": \"https://api.bitwarden.com\",\n      \"identity\": \"https://identity.bitwarden.com\",\n      \"admin\": \"https://admin.bitwarden.com\",\n      \"notifications\": \"https://notifications.bitwarden.com\",\n      \"sso\": \"https://sso.bitwarden.com\",\n      \"internalNotifications\": \"https://notifications.bitwarden.com\",\n      \"internalAdmin\": \"https://admin.bitwarden.com\",\n      \"internalIdentity\": \"https://identity.bitwarden.com\",\n      \"internalApi\": \"https://api.bitwarden.com\",\n      \"internalVault\": \"https://vault.bitwarden.com\",\n      \"internalSso\": \"https://sso.bitwarden.com\",\n      \"internalScim\": \"https://scim.bitwarden.com\"\n    },\n    \"braintree\": {\n      \"production\": true\n    },\n    \"bitPay\": {\n      \"production\": true\n    }\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Warning\",\n          \"System\": \"Warning\",\n          \"Microsoft\": \"Warning\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Api/appsettings.QA.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.qa.bitwarden.pw\",\n      \"api\": \"https://api.qa.bitwarden.pw\",\n      \"identity\": \"https://identity.qa.bitwarden.pw\",\n      \"admin\": \"https://admin.qa.bitwarden.pw\",\n      \"notifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"sso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalNotifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"internalAdmin\": \"https://admin.qa.bitwarden.pw\",\n      \"internalIdentity\": \"https://identity.qa.bitwarden.pw\",\n      \"internalApi\": \"https://api.qa.bitwarden.pw\",\n      \"internalVault\": \"https://vault.qa.bitwarden.pw\",\n      \"internalSso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalScim\": \"https://scim.qa.bitwarden.pw\"\n    },\n    \"braintree\": {\n      \"production\": false\n    },\n    \"bitPay\": {\n      \"production\": false\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Debug\",\n          \"System\": \"Debug\",\n          \"Microsoft\": \"Debug\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Api/appsettings.SelfHosted.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": null,\n      \"api\": null,\n      \"identity\": null,\n      \"admin\": null,\n      \"notifications\": null,\n      \"sso\": null,\n      \"internalNotifications\": null,\n      \"internalAdmin\": null,\n      \"internalIdentity\": null,\n      \"internalApi\": null,\n      \"internalVault\": null,\n      \"internalSso\": null,\n      \"internalScim\": null\n    }\n  }\n}\n"
  },
  {
    "path": "src/Api/appsettings.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"selfHosted\": false,\n    \"siteName\": \"Bitwarden\",\n    \"projectName\": \"Api\",\n    \"stripe\": {\n      \"apiKey\": \"SECRET\"\n    },\n    \"sqlServer\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"mail\": {\n      \"sendGridApiKey\": \"SECRET\",\n      \"amazonConfigSetName\": \"Email\",\n      \"replyToEmail\": \"no-reply@bitwarden.com\"\n    },\n    \"identityServer\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"dataProtection\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"storage\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"events\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"attachment\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"send\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"notificationHub\": {\n      \"connectionString\": \"SECRET\",\n      \"hubName\": \"SECRET\"\n    },\n    \"serviceBus\": {\n      \"connectionString\": \"SECRET\",\n      \"applicationCacheTopicName\": \"SECRET\"\n    },\n    \"yubico\": {\n      \"clientid\": \"SECRET\",\n      \"key\": \"SECRET\"\n    },\n    \"duo\": {\n      \"aKey\": \"SECRET\"\n    },\n    \"braintree\": {\n      \"production\": false,\n      \"merchantId\": \"SECRET\",\n      \"publicKey\": \"SECRET\",\n      \"privateKey\": \"SECRET\"\n    },\n    \"importCiphersLimitation\": {\n      \"ciphersLimit\": 40000,\n      \"collectionRelationshipsLimit\": 80000,\n      \"collectionsLimit\": 2000\n    },\n    \"bitPay\": {\n      \"production\": false,\n      \"token\": \"SECRET\",\n      \"notificationUrl\": \"https://bitwarden.com/SECRET\",\n      \"webhookKey\": \"SECRET\"\n    },\n    \"amazon\": {\n      \"accessKeyId\": \"SECRET\",\n      \"accessKeySecret\": \"SECRET\",\n      \"region\": \"SECRET\"\n    },\n    \"distributedIpRateLimiting\": {\n      \"enabled\": true,\n      \"maxRedisTimeoutsThreshold\": 10,\n      \"slidingWindowSeconds\": 120\n    }\n  },\n  \"IpRateLimitOptions\": {\n    \"EnableEndpointRateLimiting\": true,\n    \"StackBlockedRequests\": false,\n    \"RealIpHeader\": \"X-Connecting-IP\",\n    \"ClientIdHeader\": \"X-ClientId\",\n    \"HttpStatusCode\": 429,\n    \"IpWhitelist\": [],\n    \"EndpointWhitelist\": [],\n    \"ClientWhitelist\": [],\n    \"GeneralRules\": [\n      {\n        \"Endpoint\": \"post:*\",\n        \"Period\": \"1m\",\n        \"Limit\": 60\n      },\n      {\n        \"Endpoint\": \"post:*\",\n        \"Period\": \"1s\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"put:*\",\n        \"Period\": \"1m\",\n        \"Limit\": 60\n      },\n      {\n        \"Endpoint\": \"put:*\",\n        \"Period\": \"1s\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"delete:*\",\n        \"Period\": \"1m\",\n        \"Limit\": 60\n      },\n      {\n        \"Endpoint\": \"delete:*\",\n        \"Period\": \"1s\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"get:*\",\n        \"Period\": \"1m\",\n        \"Limit\": 200\n      },\n      {\n        \"Endpoint\": \"post:/accounts/password-hint\",\n        \"Period\": \"60m\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"post:/accounts/email-token\",\n        \"Period\": \"1m\",\n        \"Limit\": 2\n      },\n      {\n        \"Endpoint\": \"post:/accounts/email\",\n        \"Period\": \"60m\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"post:/accounts/verify-email-token\",\n        \"Period\": \"1m\",\n        \"Limit\": 2\n      },\n      {\n        \"Endpoint\": \"post:/accounts/verify-email\",\n        \"Period\": \"60m\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"post:/accounts/delete-recover-token\",\n        \"Period\": \"1m\",\n        \"Limit\": 2\n      },\n      {\n        \"Endpoint\": \"post:/accounts/delete-recover\",\n        \"Period\": \"60m\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"post:/two-factor/send-email\",\n        \"Period\": \"10m\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"post:/two-factor/send-email-login\",\n        \"Period\": \"10m\",\n        \"Limit\": 10\n      },\n      {\n        \"Endpoint\": \"post:/two-factor/authenticator\",\n        \"Period\": \"1m\",\n        \"Limit\": 3\n      },\n      {\n        \"Endpoint\": \"post:/two-factor/email\",\n        \"Period\": \"1m\",\n        \"Limit\": 3\n      },\n      {\n        \"Endpoint\": \"get:/alive\",\n        \"Period\": \"1m\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"get:/hibp/breach\",\n        \"Period\": \"2s\",\n        \"Limit\": 1\n      },\n      {\n        \"Endpoint\": \"post:/installations\",\n        \"Period\": \"2m\",\n        \"Limit\": 2\n      },\n      {\n        \"Endpoint\": \"post:/organizations/*/users/invite\",\n        \"Period\": \"1m\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"post:/organizations/*/users/invite\",\n        \"Period\": \"1d\",\n        \"Limit\": 300\n      },\n      {\n        \"Endpoint\": \"post:/organizations/*/users/*/reinvite\",\n        \"Period\": \"1m\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"post:/organizations/*/users/*/reinvite\",\n        \"Period\": \"1d\",\n        \"Limit\": 300\n      },\n      {\n        \"Endpoint\": \"post:/accounts/prelogin\",\n        \"Period\": \"1m\",\n        \"Limit\": 10\n      }\n    ]\n  },\n  \"IpRateLimitPolicies\": {\n    \"IpRules\": []\n  }\n}\n"
  },
  {
    "path": "src/Api/build.ps1",
    "content": "$dir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\necho \"`n## Building API\"\n\necho \"`nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore $dir\\Api.csproj\necho \"Clean\"\ndotnet clean $dir\\Api.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\necho \"Publish\"\ndotnet publish $dir\\Api.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\n"
  },
  {
    "path": "src/Api/build.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && \"pwd\" )\"\n\necho -e \"\\n## Building API\"\n\necho -e \"\\nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore \"$DIR/Api.csproj\"\necho \"Clean\"\ndotnet clean \"$DIR/Api.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\necho \"Publish\"\ndotnet publish \"$DIR/Api.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\n"
  },
  {
    "path": "src/Api/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/core\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    if [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n        chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos\n    fi\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nif [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n    cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf\n    $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab\nfi\n\nexec $gosu_cmd /app/Api\n"
  },
  {
    "path": "src/Api/runtimeconfig.template.json",
    "content": "{\n  \"gcServer\": false,\n  \"gcConcurrent\": true\n}"
  },
  {
    "path": "src/Billing/Billing.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-Billing</UserSecretsId>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <PropertyGroup Label=\"Server SDK settings\">\n    <!-- These features will be gradually turned on -->\n    <BitIncludeFeatures>false</BitIncludeFeatures>\n    <BitIncludeAuthentication>false</BitIncludeAuthentication>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Billing' \" />\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\bitwarden_license\\src\\Commercial.Core\\Commercial.Core.csproj\" />\n    <ProjectReference Include=\"..\\SharedWeb\\SharedWeb.csproj\" />\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"MarkDig\" Version=\"1.1.0\" />\n    <PackageReference Include=\"Swashbuckle.AspNetCore\" Version=\"10.1.0\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Billing/BillingSettings.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Billing;\n\npublic class BillingSettings\n{\n    public virtual string JobsKey { get; set; }\n    public virtual string StripeWebhookKey { get; set; }\n    public virtual string StripeWebhookSecret20250827Basil { get; set; }\n    public virtual string AppleWebhookKey { get; set; }\n    public virtual PayPalSettings PayPal { get; set; } = new PayPalSettings();\n\n    public class PayPalSettings\n    {\n        public virtual bool Production { get; set; }\n        public virtual string BusinessId { get; set; }\n        public virtual string WebhookKey { get; set; }\n    }\n\n}\n"
  },
  {
    "path": "src/Billing/Constants/BitPayNotificationCode.cs",
    "content": "﻿namespace Bit.Billing.Constants;\n\npublic static class BitPayNotificationCode\n{\n    public const string InvoiceConfirmed = \"invoice_confirmed\";\n}\n"
  },
  {
    "path": "src/Billing/Constants/HandledStripeWebhook.cs",
    "content": "﻿namespace Bit.Billing.Constants;\n\npublic static class HandledStripeWebhook\n{\n    public const string SubscriptionDeleted = \"customer.subscription.deleted\";\n    public const string SubscriptionUpdated = \"customer.subscription.updated\";\n    public const string UpcomingInvoice = \"invoice.upcoming\";\n    public const string ChargeSucceeded = \"charge.succeeded\";\n    public const string ChargeRefunded = \"charge.refunded\";\n    public const string PaymentSucceeded = \"invoice.payment_succeeded\";\n    public const string PaymentFailed = \"invoice.payment_failed\";\n    public const string InvoiceCreated = \"invoice.created\";\n    public const string PaymentMethodAttached = \"payment_method.attached\";\n    public const string CustomerUpdated = \"customer.updated\";\n    public const string InvoiceFinalized = \"invoice.finalized\";\n    public const string SetupIntentSucceeded = \"setup_intent.succeeded\";\n    public const string CouponDeleted = \"coupon.deleted\";\n}\n"
  },
  {
    "path": "src/Billing/Constants/StripeInvoiceStatus.cs",
    "content": "﻿namespace Bit.Billing.Constants;\n\npublic static class StripeInvoiceStatus\n{\n    public const string Draft = \"draft\";\n    public const string Open = \"open\";\n    public const string Paid = \"paid\";\n    public const string Void = \"void\";\n    public const string Uncollectible = \"uncollectible\";\n}\n"
  },
  {
    "path": "src/Billing/Constants/StripeSubscriptionStatus.cs",
    "content": "﻿namespace Bit.Billing.Constants;\n\npublic static class StripeSubscriptionStatus\n{\n    public const string Trialing = \"trialing\";\n    public const string Active = \"active\";\n    public const string Incomplete = \"incomplete\";\n    public const string IncompleteExpired = \"incomplete_expired\";\n    public const string PastDue = \"past_due\";\n    public const string Canceled = \"canceled\";\n    public const string Unpaid = \"unpaid\";\n    public const string Paused = \"paused\";\n}\n"
  },
  {
    "path": "src/Billing/Controllers/AppleController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text;\nusing System.Text.Json;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Billing.Controllers;\n\n[Route(\"apple\")]\npublic class AppleController : Controller\n{\n    private readonly BillingSettings _billingSettings;\n    private readonly ILogger<AppleController> _logger;\n\n    public AppleController(\n        IOptions<BillingSettings> billingSettings,\n        ILogger<AppleController> logger)\n    {\n        _billingSettings = billingSettings?.Value;\n        _logger = logger;\n    }\n\n    [HttpPost(\"iap\")]\n    public async Task<IActionResult> PostIap()\n    {\n        if (HttpContext?.Request?.Query == null)\n        {\n            return new BadRequestResult();\n        }\n\n        var key = HttpContext.Request.Query.TryGetValue(\"key\", out var keyValue) ?\n            keyValue.ToString() : null;\n        if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey))\n        {\n            return new BadRequestResult();\n        }\n\n        string body = null;\n        using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))\n        {\n            body = await reader.ReadToEndAsync();\n        }\n\n        if (string.IsNullOrWhiteSpace(body))\n        {\n            return new BadRequestResult();\n        }\n\n        try\n        {\n            var json = JsonSerializer.Serialize(JsonSerializer.Deserialize<JsonDocument>(body), JsonHelpers.Indented);\n            _logger.LogInformation(Bit.Core.Constants.BypassFiltersEventId, \"Apple IAP Notification:\\n\\n{0}\", json);\n            return new OkResult();\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(e, \"Error processing IAP status notification.\");\n            return new BadRequestResult();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Controllers/BitPayController.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Billing.Models;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Payment.Clients;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing BitPayLight.Models.Invoice;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Billing.Controllers;\n\nusing static BitPayConstants;\nusing static StripeConstants;\n\n[Route(\"bitpay\")]\n[ApiExplorerSettings(IgnoreApi = true)]\npublic class BitPayController(\n    GlobalSettings globalSettings,\n    IBitPayClient bitPayClient,\n    ITransactionRepository transactionRepository,\n    IOrganizationRepository organizationRepository,\n    IUserRepository userRepository,\n    IProviderRepository providerRepository,\n    IMailService mailService,\n    IStripePaymentService paymentService,\n    ILogger<BitPayController> logger,\n    IPremiumUserBillingService premiumUserBillingService)\n    : Controller\n{\n    [HttpPost(\"ipn\")]\n    public async Task<IActionResult> PostIpn([FromBody] BitPayEventModel model, [FromQuery] string key)\n    {\n        if (!CoreHelpers.FixedTimeEquals(key, globalSettings.BitPay.WebhookKey))\n        {\n            return new BadRequestObjectResult(\"Invalid key\");\n        }\n\n        var invoice = await bitPayClient.GetInvoice(model.Data.Id);\n\n        if (invoice.Currency != \"USD\")\n        {\n            logger.LogWarning(\"Received BitPay invoice webhook for invoice ({InvoiceID}) with non-USD currency: {Currency}\", invoice.Id, invoice.Currency);\n            return new BadRequestObjectResult(\"Cannot process non-USD payments\");\n        }\n\n        var (organizationId, userId, providerId) = GetIdsFromPosData(invoice);\n        if ((!organizationId.HasValue && !userId.HasValue && !providerId.HasValue) || !invoice.PosData.Contains(PosDataKeys.AccountCredit))\n        {\n            logger.LogWarning(\"Received BitPay invoice webhook for invoice ({InvoiceID}) that had invalid POS data: {PosData}\", invoice.Id, invoice.PosData);\n            return new BadRequestObjectResult(\"Invalid POS data\");\n        }\n\n        if (invoice.Status != InvoiceStatuses.Complete)\n        {\n            logger.LogInformation(\"Received valid BitPay invoice webhook for invoice ({InvoiceID}) that is not yet complete: {Status}\",\n                invoice.Id, invoice.Status);\n            return new OkObjectResult(\"Waiting for invoice to be completed\");\n        }\n\n        var existingTransaction = await transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id);\n        if (existingTransaction != null)\n        {\n            logger.LogWarning(\"Already processed BitPay invoice webhook for invoice ({InvoiceID})\", invoice.Id);\n            return new OkObjectResult(\"Invoice already processed\");\n        }\n\n        try\n        {\n            var transaction = new Transaction\n            {\n                Amount = Convert.ToDecimal(invoice.Price),\n                CreationDate = GetTransactionDate(invoice),\n                OrganizationId = organizationId,\n                UserId = userId,\n                ProviderId = providerId,\n                Type = TransactionType.Credit,\n                Gateway = GatewayType.BitPay,\n                GatewayId = invoice.Id,\n                PaymentMethodType = PaymentMethodType.BitPay,\n                Details = $\"{invoice.Currency}, BitPay {invoice.Id}\"\n            };\n\n            await transactionRepository.CreateAsync(transaction);\n\n            var billingEmail = \"\";\n            if (transaction.OrganizationId.HasValue)\n            {\n                var organization = await organizationRepository.GetByIdAsync(transaction.OrganizationId.Value);\n                if (organization != null)\n                {\n                    billingEmail = organization.BillingEmailAddress();\n                    if (await paymentService.CreditAccountAsync(organization, transaction.Amount))\n                    {\n                        await organizationRepository.ReplaceAsync(organization);\n                    }\n                }\n            }\n            else if (transaction.UserId.HasValue)\n            {\n                var user = await userRepository.GetByIdAsync(transaction.UserId.Value);\n                if (user != null)\n                {\n                    billingEmail = user.BillingEmailAddress();\n                    await premiumUserBillingService.Credit(user, transaction.Amount);\n                }\n            }\n            else if (transaction.ProviderId.HasValue)\n            {\n                var provider = await providerRepository.GetByIdAsync(transaction.ProviderId.Value);\n                if (provider != null)\n                {\n                    billingEmail = provider.BillingEmailAddress();\n                    if (await paymentService.CreditAccountAsync(provider, transaction.Amount))\n                    {\n                        await providerRepository.ReplaceAsync(provider);\n                    }\n                }\n            }\n\n            if (!string.IsNullOrWhiteSpace(billingEmail))\n            {\n                await mailService.SendAddedCreditAsync(billingEmail, transaction.Amount);\n            }\n        }\n        // Catch foreign key violations because user/org could have been deleted.\n        catch (SqlException e) when (e.Number == 547)\n        {\n        }\n\n        return new OkResult();\n    }\n\n    private static DateTime GetTransactionDate(Invoice invoice)\n    {\n        var transactions = invoice.Transactions?.Where(transaction =>\n            transaction.Type == null && !string.IsNullOrWhiteSpace(transaction.Confirmations) &&\n            transaction.Confirmations != \"0\").ToList();\n\n        return transactions?.Count == 1\n            ? DateTime.Parse(transactions.First().ReceivedTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind)\n            : CoreHelpers.FromEpocMilliseconds(invoice.CurrentTime);\n    }\n\n    public (Guid? OrganizationId, Guid? UserId, Guid? ProviderId) GetIdsFromPosData(Invoice invoice)\n    {\n        if (invoice.PosData is null or { Length: 0 } || !invoice.PosData.Contains(':'))\n        {\n            return new ValueTuple<Guid?, Guid?, Guid?>(null, null, null);\n        }\n\n        var ids = invoice.PosData\n            .Split(',')\n            .Select(part => part.Split(':'))\n            .Where(parts => parts.Length == 2 && Guid.TryParse(parts[1], out _))\n            .ToDictionary(parts => parts[0], parts => Guid.Parse(parts[1]));\n\n        return new ValueTuple<Guid?, Guid?, Guid?>(\n            ids.TryGetValue(MetadataKeys.OrganizationId, out var id) ? id : null,\n            ids.TryGetValue(MetadataKeys.UserId, out id) ? id : null,\n            ids.TryGetValue(MetadataKeys.ProviderId, out id) ? id : null\n        );\n    }\n}\n"
  },
  {
    "path": "src/Billing/Controllers/InfoController.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Billing.Controllers;\n\npublic class InfoController : Controller\n{\n    [HttpGet(\"~/alive\")]\n    [HttpGet(\"~/now\")]\n    public DateTime GetAlive()\n    {\n        return DateTime.UtcNow;\n    }\n\n    [HttpGet(\"~/version\")]\n    public JsonResult GetVersion()\n    {\n        return Json(AssemblyHelpers.GetVersion());\n    }\n}\n"
  },
  {
    "path": "src/Billing/Controllers/JobsController.cs",
    "content": "﻿using Bit.Billing.Jobs;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Billing.Controllers;\n\n[Route(\"jobs\")]\n[SelfHosted(NotSelfHostedOnly = true)]\n[RequireLowerEnvironment]\npublic class JobsController(\n    JobsHostedService jobsHostedService) : Controller\n{\n    [HttpPost(\"run/{jobName}\")]\n    public async Task<IActionResult> RunJobAsync(string jobName)\n    {\n        if (jobName == nameof(ReconcileAdditionalStorageJob))\n        {\n            await jobsHostedService.RunJobAdHocAsync<ReconcileAdditionalStorageJob>();\n            return Ok(new { message = $\"Job {jobName} scheduled successfully\" });\n        }\n\n        return BadRequest(new { error = $\"Unknown job name: {jobName}\" });\n    }\n\n    [HttpPost(\"stop/{jobName}\")]\n    public async Task<IActionResult> StopJobAsync(string jobName)\n    {\n        if (jobName == nameof(ReconcileAdditionalStorageJob))\n        {\n            await jobsHostedService.InterruptAdHocJobAsync<ReconcileAdditionalStorageJob>();\n            return Ok(new { message = $\"Job {jobName} queued for cancellation\" });\n        }\n\n        return BadRequest(new { error = $\"Unknown job name: {jobName}\" });\n    }\n}\n"
  },
  {
    "path": "src/Billing/Controllers/PayPalController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text;\nusing Bit.Billing.Models;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Data.SqlClient;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Billing.Controllers;\n\n[Route(\"paypal\")]\npublic class PayPalController : Controller\n{\n    private readonly BillingSettings _billingSettings;\n    private readonly ILogger<PayPalController> _logger;\n    private readonly IMailService _mailService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly ITransactionRepository _transactionRepository;\n    private readonly IUserRepository _userRepository;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IPremiumUserBillingService _premiumUserBillingService;\n\n    public PayPalController(\n        IOptions<BillingSettings> billingSettings,\n        ILogger<PayPalController> logger,\n        IMailService mailService,\n        IOrganizationRepository organizationRepository,\n        IStripePaymentService paymentService,\n        ITransactionRepository transactionRepository,\n        IUserRepository userRepository,\n        IProviderRepository providerRepository,\n        IPremiumUserBillingService premiumUserBillingService)\n    {\n        _billingSettings = billingSettings?.Value;\n        _logger = logger;\n        _mailService = mailService;\n        _organizationRepository = organizationRepository;\n        _paymentService = paymentService;\n        _transactionRepository = transactionRepository;\n        _userRepository = userRepository;\n        _providerRepository = providerRepository;\n        _premiumUserBillingService = premiumUserBillingService;\n    }\n\n    [HttpPost(\"ipn\")]\n    public async Task<IActionResult> PostIpn()\n    {\n        var key = HttpContext.Request.Query.TryGetValue(\"key\", out var keyValue)\n            ? keyValue.ToString()\n            : null;\n\n        if (string.IsNullOrEmpty(key))\n        {\n            _logger.LogError(\"PayPal IPN: Key is missing\");\n            return BadRequest();\n        }\n\n        if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.PayPal.WebhookKey))\n        {\n            _logger.LogError(\"PayPal IPN: Key is incorrect\");\n            return BadRequest();\n        }\n\n        using var streamReader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8);\n\n        var requestContent = await streamReader.ReadToEndAsync();\n\n        if (string.IsNullOrEmpty(requestContent))\n        {\n            _logger.LogError(\"PayPal IPN: Request body is null or empty\");\n            return BadRequest();\n        }\n\n        var transactionModel = new PayPalIPNTransactionModel(requestContent);\n\n        _logger.LogInformation(\"PayPal IPN: Transaction Type = {Type}\", transactionModel.TransactionType);\n\n        if (string.IsNullOrEmpty(transactionModel.TransactionId))\n        {\n            _logger.LogWarning(\"PayPal IPN: Transaction ID is missing\");\n            return Ok();\n        }\n\n        var entityId = transactionModel.UserId ?? transactionModel.OrganizationId ?? transactionModel.ProviderId;\n\n        if (!entityId.HasValue)\n        {\n            _logger.LogError(\"PayPal IPN ({Id}): 'custom' did not contain a User ID or Organization ID or provider ID\", transactionModel.TransactionId);\n            return BadRequest();\n        }\n\n        if (transactionModel.TransactionType != \"web_accept\" &&\n            transactionModel.TransactionType != \"merch_pmt\" &&\n            transactionModel.PaymentStatus != \"Refunded\")\n        {\n            _logger.LogWarning(\"PayPal IPN ({Id}): Transaction type ({Type}) not supported for payments\",\n                transactionModel.TransactionId,\n                transactionModel.TransactionType);\n\n            return Ok();\n        }\n\n        if (transactionModel.ReceiverId != _billingSettings.PayPal.BusinessId)\n        {\n            _logger.LogWarning(\n                \"PayPal IPN ({Id}): Receiver ID ({ReceiverId}) does not match Bitwarden business ID ({BusinessId})\",\n                transactionModel.TransactionId,\n                transactionModel.ReceiverId,\n                _billingSettings.PayPal.BusinessId);\n\n            return Ok();\n        }\n\n        if (transactionModel.PaymentStatus == \"Refunded\" && string.IsNullOrEmpty(transactionModel.ParentTransactionId))\n        {\n            _logger.LogWarning(\"PayPal IPN ({Id}): Parent transaction ID is required for refund\", transactionModel.TransactionId);\n            return Ok();\n        }\n\n        if (transactionModel.PaymentType == \"echeck\" && transactionModel.PaymentStatus != \"Refunded\")\n        {\n            _logger.LogWarning(\"PayPal IPN ({Id}): Transaction was an eCheck payment\", transactionModel.TransactionId);\n            return Ok();\n        }\n\n        if (transactionModel.MerchantCurrency != \"USD\")\n        {\n            _logger.LogWarning(\"PayPal IPN ({Id}): Transaction was not in USD ({Currency})\",\n                transactionModel.TransactionId,\n                transactionModel.MerchantCurrency);\n\n            return Ok();\n        }\n\n        switch (transactionModel.PaymentStatus)\n        {\n            case \"Completed\":\n                {\n                    var existingTransaction = await _transactionRepository.GetByGatewayIdAsync(\n                        GatewayType.PayPal,\n                        transactionModel.TransactionId);\n\n                    if (existingTransaction != null)\n                    {\n                        _logger.LogWarning(\"PayPal IPN ({Id}): Already processed this completed transaction\", transactionModel.TransactionId);\n                        return Ok();\n                    }\n\n                    try\n                    {\n                        var transaction = new Transaction\n                        {\n                            Amount = transactionModel.MerchantGross,\n                            CreationDate = transactionModel.PaymentDate,\n                            OrganizationId = transactionModel.OrganizationId,\n                            UserId = transactionModel.UserId,\n                            ProviderId = transactionModel.ProviderId,\n                            Type = transactionModel.IsAccountCredit ? TransactionType.Credit : TransactionType.Charge,\n                            Gateway = GatewayType.PayPal,\n                            GatewayId = transactionModel.TransactionId,\n                            PaymentMethodType = PaymentMethodType.PayPal,\n                            Details = transactionModel.TransactionId\n                        };\n\n                        await _transactionRepository.CreateAsync(transaction);\n\n                        if (transactionModel.IsAccountCredit)\n                        {\n                            await ApplyCreditAsync(transaction);\n                        }\n                    }\n                    // Catch foreign key violations because user/org could have been deleted.\n                    catch (SqlException sqlException) when (sqlException.Number == 547)\n                    {\n                        _logger.LogError(\"PayPal IPN ({Id}): SQL Exception | {Message}\", transactionModel.TransactionId, sqlException.Message);\n                    }\n\n                    break;\n                }\n            case \"Refunded\" or \"Reversed\":\n                {\n                    var existingTransaction = await _transactionRepository.GetByGatewayIdAsync(\n                        GatewayType.PayPal,\n                        transactionModel.TransactionId);\n\n                    if (existingTransaction != null)\n                    {\n                        _logger.LogWarning(\"PayPal IPN ({Id}): Already processed this refunded transaction\", transactionModel.TransactionId);\n                        return Ok();\n                    }\n\n                    var parentTransaction = await _transactionRepository.GetByGatewayIdAsync(\n                        GatewayType.PayPal,\n                        transactionModel.ParentTransactionId);\n\n                    if (parentTransaction == null)\n                    {\n                        _logger.LogWarning(\"PayPal IPN ({Id}): Could not find parent transaction\", transactionModel.TransactionId);\n                        return Ok();\n                    }\n\n                    var refundAmount = Math.Abs(transactionModel.MerchantGross);\n\n                    var remainingAmount = parentTransaction.Amount - parentTransaction.RefundedAmount.GetValueOrDefault();\n\n                    if (refundAmount > 0 && !parentTransaction.Refunded.GetValueOrDefault() && remainingAmount >= refundAmount)\n                    {\n                        parentTransaction.RefundedAmount = parentTransaction.RefundedAmount.GetValueOrDefault() + refundAmount;\n\n                        if (parentTransaction.RefundedAmount == parentTransaction.Amount)\n                        {\n                            parentTransaction.Refunded = true;\n                        }\n\n                        await _transactionRepository.ReplaceAsync(parentTransaction);\n\n                        await _transactionRepository.CreateAsync(new Transaction\n                        {\n                            Amount = refundAmount,\n                            CreationDate = transactionModel.PaymentDate,\n                            OrganizationId = transactionModel.OrganizationId,\n                            UserId = transactionModel.UserId,\n                            ProviderId = transactionModel.ProviderId,\n                            Type = TransactionType.Refund,\n                            Gateway = GatewayType.PayPal,\n                            GatewayId = transactionModel.TransactionId,\n                            PaymentMethodType = PaymentMethodType.PayPal,\n                            Details = transactionModel.TransactionId\n                        });\n                    }\n\n                    break;\n                }\n        }\n\n        return Ok();\n    }\n\n    private async Task ApplyCreditAsync(Transaction transaction)\n    {\n        string billingEmail = null;\n\n        if (transaction.OrganizationId.HasValue)\n        {\n            var organization = await _organizationRepository.GetByIdAsync(transaction.OrganizationId.Value);\n\n            if (await _paymentService.CreditAccountAsync(organization, transaction.Amount))\n            {\n                await _organizationRepository.ReplaceAsync(organization);\n\n                billingEmail = organization.BillingEmailAddress();\n            }\n        }\n        else if (transaction.UserId.HasValue)\n        {\n            var user = await _userRepository.GetByIdAsync(transaction.UserId.Value);\n\n            if (user != null)\n            {\n                await _premiumUserBillingService.Credit(user, transaction.Amount);\n                billingEmail = user.BillingEmailAddress();\n            }\n        }\n        else if (transaction.ProviderId.HasValue)\n        {\n            var provider = await _providerRepository.GetByIdAsync(transaction.ProviderId.Value);\n\n            if (await _paymentService.CreditAccountAsync(provider, transaction.Amount))\n            {\n                await _providerRepository.ReplaceAsync(provider);\n\n                billingEmail = provider.BillingEmailAddress();\n            }\n        }\n\n        if (!string.IsNullOrEmpty(billingEmail))\n        {\n            await _mailService.SendAddedCreditAsync(billingEmail, transaction.Amount);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Controllers/RecoveryController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Billing.Models.Recovery;\nusing Bit.Billing.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing Microsoft.AspNetCore.Mvc;\nusing Stripe;\n\nnamespace Bit.Billing.Controllers;\n\n[Route(\"stripe/recovery\")]\n[SelfHosted(NotSelfHostedOnly = true)]\npublic class RecoveryController(\n    IStripeEventProcessor stripeEventProcessor,\n    IStripeFacade stripeFacade,\n    IWebHostEnvironment webHostEnvironment) : Controller\n{\n    private readonly string _stripeURL = webHostEnvironment.IsDevelopment() || webHostEnvironment.IsEnvironment(\"QA\")\n        ? \"https://dashboard.stripe.com/test\"\n        : \"https://dashboard.stripe.com\";\n\n    // ReSharper disable once RouteTemplates.ActionRoutePrefixCanBeExtractedToControllerRoute\n    [HttpPost(\"events/inspect\")]\n    public async Task<Ok<EventsResponseBody>> InspectEventsAsync([FromBody] EventsRequestBody requestBody)\n    {\n        var inspected = await Task.WhenAll(requestBody.EventIds.Select(async eventId =>\n        {\n            var @event = await stripeFacade.GetEvent(eventId);\n            return Map(@event);\n        }));\n\n        var response = new EventsResponseBody { Events = inspected.ToList() };\n\n        return TypedResults.Ok(response);\n    }\n\n    // ReSharper disable once RouteTemplates.ActionRoutePrefixCanBeExtractedToControllerRoute\n    [HttpPost(\"events/process\")]\n    public async Task<Ok<EventsResponseBody>> ProcessEventsAsync([FromBody] EventsRequestBody requestBody)\n    {\n        var processed = await Task.WhenAll(requestBody.EventIds.Select(async eventId =>\n        {\n            var @event = await stripeFacade.GetEvent(eventId);\n            try\n            {\n                await stripeEventProcessor.ProcessEventAsync(@event);\n                return Map(@event);\n            }\n            catch (Exception exception)\n            {\n                return Map(@event, exception.Message);\n            }\n        }));\n\n        var response = new EventsResponseBody { Events = processed.ToList() };\n\n        return TypedResults.Ok(response);\n    }\n\n    private EventResponseBody Map(Event @event, string processingError = null) => new()\n    {\n        Id = @event.Id,\n        URL = $\"{_stripeURL}/workbench/events/{@event.Id}\",\n        APIVersion = @event.ApiVersion,\n        Type = @event.Type,\n        CreatedUTC = @event.Created,\n        ProcessingError = processingError\n    };\n}\n"
  },
  {
    "path": "src/Billing/Controllers/StripeController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Billing.Models;\nusing Bit.Billing.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Options;\nusing Stripe;\nusing Event = Stripe.Event;\nusing JsonSerializer = System.Text.Json.JsonSerializer;\n\nnamespace Bit.Billing.Controllers;\n\n[Route(\"stripe\")]\npublic class StripeController : Controller\n{\n    private readonly BillingSettings _billingSettings;\n    private readonly IWebHostEnvironment _hostingEnvironment;\n    private readonly ILogger<StripeController> _logger;\n    private readonly IStripeEventService _stripeEventService;\n    private readonly IStripeEventProcessor _stripeEventProcessor;\n\n    public StripeController(\n        IOptions<BillingSettings> billingSettings,\n        IWebHostEnvironment hostingEnvironment,\n        ILogger<StripeController> logger,\n        IStripeEventService stripeEventService,\n        IStripeEventProcessor stripeEventProcessor)\n    {\n        _billingSettings = billingSettings?.Value;\n        _hostingEnvironment = hostingEnvironment;\n        _logger = logger;\n        _stripeEventService = stripeEventService;\n        _stripeEventProcessor = stripeEventProcessor;\n    }\n\n    [HttpPost(\"webhook\")]\n    public async Task<IActionResult> PostWebhook([FromQuery] string key)\n    {\n        if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.StripeWebhookKey))\n        {\n            _logger.LogError(\"Stripe webhook key does not match configured webhook key\");\n            return new BadRequestResult();\n        }\n\n        var parsedEvent = await TryParseEventFromRequestBodyAsync();\n        if (parsedEvent is null)\n        {\n            return Ok(new\n            {\n                Processed = false,\n                Message = \"Could not find a configured webhook secret to process this event with\"\n            });\n        }\n\n        if (StripeConfiguration.ApiVersion != parsedEvent.ApiVersion)\n        {\n            _logger.LogWarning(\n                \"Stripe {WebhookType} webhook's API version ({WebhookAPIVersion}) does not match SDK API Version ({SDKAPIVersion})\",\n                parsedEvent.Type,\n                parsedEvent.ApiVersion,\n                StripeConfiguration.ApiVersion);\n\n            return Ok(new\n            {\n                Processed = false,\n                Message = \"SDK API version does not match the event's API version\"\n            });\n        }\n\n        if (string.IsNullOrWhiteSpace(parsedEvent?.Id))\n        {\n            _logger.LogWarning(\"No event id.\");\n            return new BadRequestResult();\n        }\n\n        if (_hostingEnvironment.IsProduction() && !parsedEvent.Livemode)\n        {\n            _logger.LogWarning(\"Getting test events in production.\");\n            return new BadRequestResult();\n        }\n\n        // If the customer and server cloud regions don't match, early return 200 to avoid unnecessary errors\n        if (!await _stripeEventService.ValidateCloudRegion(parsedEvent))\n        {\n            return Ok(new\n            {\n                Processed = false,\n                Message = \"Event is not for this cloud region\"\n            });\n        }\n\n        await _stripeEventProcessor.ProcessEventAsync(parsedEvent);\n        return Ok(new\n        {\n            Processed = true,\n            Message = \"Processed\"\n        });\n    }\n\n    /// <summary>\n    /// Selects the appropriate Stripe webhook secret based on the API version specified in the webhook body.\n    /// </summary>\n    /// <param name=\"webhookBody\">The body of the webhook request received from Stripe.</param>\n    /// <returns>\n    /// The Stripe webhook secret corresponding to the API version found in the webhook body.\n    /// Returns null if the API version is unrecognized.\n    /// </returns>\n    private string PickStripeWebhookSecret(string webhookBody)\n    {\n        var deliveryContainer = JsonSerializer.Deserialize<StripeWebhookDeliveryContainer>(webhookBody);\n\n        _logger.LogInformation(\n            \"Picking secret for Stripe webhook | {EventID}: {EventType} | Version: {APIVersion} | Initiating Request ID: {RequestID}\",\n            deliveryContainer.Id,\n            deliveryContainer.Type,\n            deliveryContainer.ApiVersion,\n            deliveryContainer.Request?.Id);\n\n        return deliveryContainer.ApiVersion switch\n        {\n            \"2025-08-27.basil\" => HandleVersionWith(_billingSettings.StripeWebhookSecret20250827Basil),\n            _ => HandleDefault(deliveryContainer.ApiVersion)\n        };\n\n        string HandleVersionWith(string secret)\n        {\n            if (string.IsNullOrEmpty(secret))\n            {\n                _logger.LogError(\"No webhook secret is configured for API version {APIVersion}\", deliveryContainer.ApiVersion);\n                return null;\n            }\n\n            if (!secret.StartsWith(\"whsec_\"))\n            {\n                _logger.LogError(\"Webhook secret configured for API version {APIVersion} does not start with whsec_\",\n                    deliveryContainer.ApiVersion);\n                return null;\n            }\n\n            var truncatedSecret = secret[..10];\n\n            _logger.LogInformation(\"Picked webhook secret {TruncatedSecret}... for API version {APIVersion}\", truncatedSecret, deliveryContainer.ApiVersion);\n\n            return secret;\n        }\n\n        string HandleDefault(string version)\n        {\n            _logger.LogWarning(\n                \"Stripe webhook contained an API version ({APIVersion}) we do not process\",\n                version);\n\n            return null;\n        }\n    }\n\n    /// <summary>\n    /// Attempts to pick the Stripe webhook secret from the JSON payload.\n    /// </summary>\n    /// <returns>Returns the event if the event was parsed, otherwise, null</returns>\n    private async Task<Event> TryParseEventFromRequestBodyAsync()\n    {\n        using var sr = new StreamReader(HttpContext.Request.Body);\n\n        var json = await sr.ReadToEndAsync();\n        var webhookSecret = PickStripeWebhookSecret(json);\n\n        if (string.IsNullOrEmpty(webhookSecret))\n        {\n            return null;\n        }\n\n        return EventUtility.ConstructEvent(\n            json,\n            Request.Headers[\"Stripe-Signature\"],\n            webhookSecret,\n            throwOnApiVersionMismatch: false);\n    }\n}\n"
  },
  {
    "path": "src/Billing/Dockerfile",
    "content": "###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\n# We put the value in a file to be read by later layers.\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/src/Billing\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    icu-libs \\\n    shadow \\\n    tzdata \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu\n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/src/Billing/out /app\nCOPY ./src/Billing/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\nHEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "src/Billing/Jobs/AliveJob.cs",
    "content": "﻿using Bit.Core.Jobs;\nusing Quartz;\n\nnamespace Bit.Billing.Jobs;\n\npublic class AliveJob(ILogger<AliveJob> logger) : BaseJob(logger)\n{\n    protected override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Core.Constants.BypassFiltersEventId, null, \"Billing service is alive!\");\n        return Task.FromResult(0);\n    }\n\n    public static ITrigger GetTrigger()\n    {\n        return TriggerBuilder.Create()\n            .WithIdentity(\"EveryTopOfTheHourTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 * * * ?\")\n            .Build();\n    }\n}\n"
  },
  {
    "path": "src/Billing/Jobs/JobsHostedService.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.Jobs;\nusing Bit.Core.Settings;\nusing Quartz;\n\nnamespace Bit.Billing.Jobs;\n\npublic class JobsHostedService(\n    GlobalSettings globalSettings,\n    IServiceProvider serviceProvider,\n    ILogger<JobsHostedService> logger,\n    ILogger<JobListener> listenerLogger,\n    ISchedulerFactory schedulerFactory)\n    : BaseJobsHostedService(globalSettings, serviceProvider, logger, listenerLogger)\n{\n    private List<JobKey> AdHocJobKeys { get; } = [];\n    private IScheduler? _adHocScheduler;\n\n    public override async Task StartAsync(CancellationToken cancellationToken)\n    {\n        Jobs = new List<Tuple<Type, ITrigger>>\n        {\n            new(typeof(AliveJob), AliveJob.GetTrigger()),\n            new(typeof(ReconcileAdditionalStorageJob), ReconcileAdditionalStorageJob.GetTrigger())\n        };\n\n        await base.StartAsync(cancellationToken);\n    }\n\n    public static void AddJobsServices(IServiceCollection services)\n    {\n        services.AddTransient<AliveJob>();\n        services.AddTransient<SubscriptionCancellationJob>();\n        services.AddTransient<ReconcileAdditionalStorageJob>();\n        // add this service as a singleton so we can inject it where needed\n        services.AddSingleton<JobsHostedService>();\n        services.AddHostedService(sp => sp.GetRequiredService<JobsHostedService>());\n    }\n\n    public async Task InterruptAdHocJobAsync<T>(CancellationToken cancellationToken = default) where T : class, IJob\n    {\n        if (_adHocScheduler == null)\n        {\n            throw new InvalidOperationException(\"AdHocScheduler is null, cannot interrupt ad-hoc job.\");\n        }\n\n        var jobKey = AdHocJobKeys.FirstOrDefault(j => j.Name == typeof(T).ToString());\n        if (jobKey == null)\n        {\n            throw new NotFoundException($\"Cannot find job key: {typeof(T)}, not running?\");\n        }\n        logger.LogInformation(\"CANCELLING ad-hoc job with key: {JobKey}\", jobKey);\n        AdHocJobKeys.Remove(jobKey);\n        await _adHocScheduler.Interrupt(jobKey, cancellationToken);\n    }\n\n    public async Task RunJobAdHocAsync<T>(CancellationToken cancellationToken = default) where T : class, IJob\n    {\n        _adHocScheduler ??= await schedulerFactory.GetScheduler(cancellationToken);\n\n        var jobKey = new JobKey(typeof(T).ToString());\n\n        var currentlyExecuting = await _adHocScheduler.GetCurrentlyExecutingJobs(cancellationToken);\n        if (currentlyExecuting.Any(j => j.JobDetail.Key.Equals(jobKey)))\n        {\n            throw new InvalidOperationException($\"Job {jobKey} is already running\");\n        }\n\n        AdHocJobKeys.Add(jobKey);\n\n        var job = JobBuilder.Create<T>()\n            .WithIdentity(jobKey)\n            .Build();\n\n        var trigger = TriggerBuilder.Create()\n            .WithIdentity(typeof(T).ToString())\n            .StartNow()\n            .Build();\n\n        logger.LogInformation(\"Scheduling ad-hoc job with key: {JobKey}\", jobKey);\n\n        await _adHocScheduler.ScheduleJob(job, trigger, cancellationToken);\n    }\n}\n"
  },
  {
    "path": "src/Billing/Jobs/ProviderOrganizationDisableJob.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Quartz;\n\nnamespace Bit.Billing.Jobs;\n\npublic class ProviderOrganizationDisableJob(\n    IProviderOrganizationRepository providerOrganizationRepository,\n    IOrganizationDisableCommand organizationDisableCommand,\n    ILogger<ProviderOrganizationDisableJob> logger)\n    : IJob\n{\n    private const int MaxConcurrency = 5;\n    private const int MaxTimeoutMinutes = 10;\n\n    public async Task Execute(IJobExecutionContext context)\n    {\n        var providerId = new Guid(context.MergedJobDataMap.GetString(\"providerId\") ?? string.Empty);\n        var expirationDateString = context.MergedJobDataMap.GetString(\"expirationDate\");\n        DateTime? expirationDate = string.IsNullOrEmpty(expirationDateString)\n            ? null\n            : DateTime.Parse(expirationDateString);\n\n        logger.LogInformation(\"Starting to disable organizations for provider {ProviderId}\", providerId);\n\n        var startTime = DateTime.UtcNow;\n        var totalProcessed = 0;\n        var totalErrors = 0;\n\n        try\n        {\n            var providerOrganizations = await providerOrganizationRepository\n                .GetManyDetailsByProviderAsync(providerId);\n\n            if (providerOrganizations == null || !providerOrganizations.Any())\n            {\n                logger.LogInformation(\"No organizations found for provider {ProviderId}\", providerId);\n                return;\n            }\n\n            logger.LogInformation(\"Disabling {OrganizationCount} organizations for provider {ProviderId}\",\n                providerOrganizations.Count, providerId);\n\n            var semaphore = new SemaphoreSlim(MaxConcurrency, MaxConcurrency);\n            var tasks = providerOrganizations.Select(async po =>\n            {\n                if (DateTime.UtcNow.Subtract(startTime).TotalMinutes > MaxTimeoutMinutes)\n                {\n                    logger.LogWarning(\"Timeout reached while disabling organizations for provider {ProviderId}\", providerId);\n                    return false;\n                }\n\n                await semaphore.WaitAsync();\n                try\n                {\n                    await organizationDisableCommand.DisableAsync(po.OrganizationId, expirationDate);\n                    Interlocked.Increment(ref totalProcessed);\n                    return true;\n                }\n                catch (Exception ex)\n                {\n                    logger.LogError(ex, \"Failed to disable organization {OrganizationId} for provider {ProviderId}\",\n                        po.OrganizationId, providerId);\n                    Interlocked.Increment(ref totalErrors);\n                    return false;\n                }\n                finally\n                {\n                    semaphore.Release();\n                }\n            });\n\n            await Task.WhenAll(tasks);\n\n            logger.LogInformation(\"Completed disabling organizations for provider {ProviderId}. Processed: {TotalProcessed}, Errors: {TotalErrors}\",\n                providerId, totalProcessed, totalErrors);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Error disabling organizations for provider {ProviderId}. Processed: {TotalProcessed}, Errors: {TotalErrors}\",\n                providerId, totalProcessed, totalErrors);\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Jobs/ReconcileAdditionalStorageJob.cs",
    "content": "﻿using System.Globalization;\nusing System.Text.Json;\nusing Bit.Billing.Services;\nusing Bit.Core;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Jobs;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Quartz;\nusing Stripe;\n\nnamespace Bit.Billing.Jobs;\n\npublic class ReconcileAdditionalStorageJob(\n    IStripeFacade stripeFacade,\n    ILogger<ReconcileAdditionalStorageJob> logger,\n    IFeatureService featureService,\n    IUserRepository userRepository,\n    IOrganizationRepository organizationRepository,\n    IStripeEventUtilityService stripeEventUtilityService) : BaseJob(logger)\n{\n    private const string _storageGbMonthlyPriceId = \"storage-gb-monthly\";\n    private const string _storageGbAnnuallyPriceId = \"storage-gb-annually\";\n    private const string _personalStorageGbAnnuallyPriceId = \"personal-storage-gb-annually\";\n    private const int _storageGbToRemove = 4;\n    private const short _includedStorageGb = 5;\n\n    public enum SubscriptionPlanTier\n    {\n        Personal,\n        Organization,\n        Unknown\n    }\n\n    protected override async Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        if (!featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob))\n        {\n            logger.LogInformation(\"Skipping ReconcileAdditionalStorageJob, feature flag off.\");\n            return;\n        }\n\n        var liveMode = featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode);\n\n        // Execution tracking\n        var subscriptionsFound = 0;\n        var subscriptionsUpdated = 0;\n        var subscriptionsWithErrors = 0;\n        var databaseUpdatesFailed = 0;\n        var failures = new List<string>();\n\n        logger.LogInformation(\"Starting ReconcileAdditionalStorageJob (live mode: {LiveMode})\", liveMode);\n\n        var priceIds = new[] { _storageGbMonthlyPriceId, _storageGbAnnuallyPriceId, _personalStorageGbAnnuallyPriceId };\n        var stripeStatusesToProcess = new[] { StripeConstants.SubscriptionStatus.Active, StripeConstants.SubscriptionStatus.Trialing, StripeConstants.SubscriptionStatus.PastDue };\n\n        foreach (var priceId in priceIds)\n        {\n            var options = new SubscriptionListOptions { Limit = 100, Price = priceId };\n\n            await foreach (var subscription in stripeFacade.ListSubscriptionsAutoPagingAsync(options))\n            {\n                if (context.CancellationToken.IsCancellationRequested)\n                {\n                    logger.LogWarning(\n                        \"Job cancelled!! Exiting. Progress at time of cancellation: Subscriptions found: {SubscriptionsFound}, \" +\n                        \"Stripe updates: {StripeUpdates}, Database updates: {DatabaseFailed} failed, \" +\n                        \"Errors: {SubscriptionsWithErrors}{Failures}\",\n                        subscriptionsFound,\n                        liveMode\n                            ? subscriptionsUpdated\n                            : $\"(In live mode, would have updated) {subscriptionsUpdated}\",\n                        databaseUpdatesFailed,\n                        subscriptionsWithErrors,\n                        failures.Count > 0\n                            ? $\", Failures: {Environment.NewLine}{string.Join(Environment.NewLine, failures)}\"\n                            : string.Empty\n                    );\n                    return;\n                }\n\n                if (subscription == null)\n                {\n                    continue;\n                }\n\n                if (!stripeStatusesToProcess.Contains(subscription.Status))\n                {\n                    logger.LogInformation(\"Skipping subscription with unsupported status: {SubscriptionId} - {Status}\", subscription.Id, subscription.Status);\n                    continue;\n                }\n\n                logger.LogInformation(\"Processing subscription: {SubscriptionId}\", subscription.Id);\n                subscriptionsFound++;\n\n                if (subscription.Metadata?.TryGetValue(StripeConstants.MetadataKeys.StorageReconciled2025, out var dateString) == true)\n                {\n                    if (DateTime.TryParse(dateString, null, DateTimeStyles.RoundtripKind, out var dateProcessed))\n                    {\n                        logger.LogInformation(\"Skipping subscription {SubscriptionId} - already processed on {Date}\",\n                            subscription.Id,\n                            dateProcessed.ToString(\"f\"));\n                        continue;\n                    }\n                }\n\n                var updateOptions = BuildSubscriptionUpdateOptions(subscription, priceId);\n\n                if (updateOptions == null)\n                {\n                    logger.LogInformation(\"Skipping subscription {SubscriptionId} - no updates needed\", subscription.Id);\n                    continue;\n                }\n\n                subscriptionsUpdated++;\n\n                // Now, prepare the database update so we can log details out if not in live mode\n                var (organizationId, userId, _) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata ?? new Dictionary<string, string>());\n                var subscriptionPlanTier = DetermineSubscriptionPlanTier(userId, organizationId);\n\n                if (subscriptionPlanTier == SubscriptionPlanTier.Unknown)\n                {\n                    logger.LogError(\n                        \"Cannot determine subscription plan tier for {SubscriptionId}. Skipping subscription. \",\n                        subscription.Id);\n                    subscriptionsWithErrors++;\n                    continue;\n                }\n\n                var entityId =\n                    subscriptionPlanTier switch\n                    {\n                        SubscriptionPlanTier.Personal => userId!.Value,\n                        SubscriptionPlanTier.Organization => organizationId!.Value,\n                        _ => throw new ArgumentOutOfRangeException(nameof(subscriptionPlanTier), subscriptionPlanTier, null)\n                    };\n\n                // Calculate new MaxStorageGb\n                var currentStorageQuantity = GetCurrentStorageQuantityFromSubscription(subscription, priceId);\n                var newMaxStorageGb = CalculateNewMaxStorageGb(currentStorageQuantity, updateOptions);\n\n                if (!liveMode)\n                {\n                    logger.LogInformation(\n                        \"Not live mode (dry-run): Would have updated subscription {SubscriptionId} with item changes: {NewLine}{UpdateOptions}\" +\n                        \"{NewLine2}And would have updated database record tier: {Tier} to new MaxStorageGb: {MaxStorageGb}\",\n                        subscription.Id,\n                        Environment.NewLine,\n                        JsonSerializer.Serialize(updateOptions),\n                        Environment.NewLine,\n                        subscriptionPlanTier,\n                        newMaxStorageGb);\n                    continue;\n                }\n\n                // Live mode enabled - continue with updates to stripe and database\n                try\n                {\n                    await stripeFacade.UpdateSubscription(subscription.Id, updateOptions);\n                    logger.LogInformation(\"Successfully updated Stripe subscription: {SubscriptionId}\", subscription.Id);\n\n                    logger.LogInformation(\n                        \"Updating MaxStorageGb in database for subscription {SubscriptionId} ({Type}): New MaxStorageGb: {MaxStorage}\",\n                        subscription.Id,\n                        subscriptionPlanTier,\n                        newMaxStorageGb);\n\n                    var dbUpdateSuccess = await UpdateDatabaseMaxStorageAsync(\n                        subscriptionPlanTier,\n                        entityId,\n                        newMaxStorageGb,\n                        subscription.Id);\n\n                    if (!dbUpdateSuccess)\n                    {\n                        databaseUpdatesFailed++;\n                        failures.Add($\"Subscription {subscription.Id}: Database update failed\");\n                    }\n                }\n                catch (Exception ex)\n                {\n                    subscriptionsWithErrors++;\n                    failures.Add($\"Subscription {subscription.Id}: {ex.Message}\");\n                    logger.LogError(ex, \"Failed to update subscription {SubscriptionId}: {ErrorMessage}\",\n                        subscription.Id, ex.Message);\n                }\n            }\n        }\n\n        logger.LogInformation(\n            \"ReconcileAdditionalStorageJob FINISHED. Subscriptions found: {SubscriptionsFound}, \" +\n            \"Subscriptions updated: {SubscriptionsUpdated}, Database failures: {DatabaseFailed}, \" +\n            \"Total Subscriptions With Errors: {SubscriptionsWithErrors}{Failures}\",\n            subscriptionsFound,\n            liveMode\n                ? subscriptionsUpdated\n                : $\"(In live mode, would have updated) {subscriptionsUpdated}\",\n            databaseUpdatesFailed,\n            subscriptionsWithErrors,\n            failures.Count > 0\n                ? $\", Failures: {Environment.NewLine}{string.Join(Environment.NewLine, failures)}\"\n                : string.Empty\n        );\n    }\n\n    private SubscriptionUpdateOptions? BuildSubscriptionUpdateOptions(\n        Subscription subscription,\n        string targetPriceId)\n    {\n        if (subscription.Items?.Data == null)\n        {\n            return null;\n        }\n\n        var updateOptions = new SubscriptionUpdateOptions { ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, Metadata = new Dictionary<string, string> { [StripeConstants.MetadataKeys.StorageReconciled2025] = DateTime.UtcNow.ToString(\"o\") }, Items = [] };\n\n        var hasUpdates = false;\n\n        foreach (var item in subscription.Items.Data.Where(item => item?.Price?.Id == targetPriceId))\n        {\n            hasUpdates = true;\n            var currentQuantity = item.Quantity;\n\n            if (currentQuantity > _storageGbToRemove)\n            {\n                var newQuantity = currentQuantity - _storageGbToRemove;\n                logger.LogInformation(\n                    \"Subscription {SubscriptionId}: reducing quantity from {CurrentQuantity} to {NewQuantity} for price {PriceId}\",\n                    subscription.Id,\n                    currentQuantity,\n                    newQuantity,\n                    item.Price.Id);\n\n                updateOptions.Items.Add(new SubscriptionItemOptions { Id = item.Id, Quantity = newQuantity });\n            }\n            else\n            {\n                logger.LogInformation(\"Subscription {SubscriptionId}: deleting storage item with quantity {CurrentQuantity} for price {PriceId}\",\n                    subscription.Id,\n                    currentQuantity,\n                    item.Price.Id);\n\n                updateOptions.Items.Add(new SubscriptionItemOptions { Id = item.Id, Deleted = true });\n            }\n        }\n\n        return hasUpdates ? updateOptions : null;\n    }\n\n    public SubscriptionPlanTier DetermineSubscriptionPlanTier(\n        Guid? userId,\n        Guid? organizationId)\n    {\n        return userId.HasValue\n            ? SubscriptionPlanTier.Personal\n            : organizationId.HasValue\n                ? SubscriptionPlanTier.Organization\n                : SubscriptionPlanTier.Unknown;\n    }\n\n    public long GetCurrentStorageQuantityFromSubscription(\n        Subscription subscription,\n        string storagePriceId)\n    {\n        return subscription.Items?.Data?.FirstOrDefault(item => item?.Price?.Id == storagePriceId)?.Quantity ?? 0;\n    }\n\n    public short CalculateNewMaxStorageGb(\n        long currentQuantity,\n        SubscriptionUpdateOptions? updateOptions)\n    {\n        if (updateOptions?.Items == null)\n        {\n            return (short)(_includedStorageGb + currentQuantity);\n        }\n\n        // If the update marks item as deleted, new quantity is whatever the base storage gb\n        if (updateOptions.Items.Any(i => i.Deleted == true))\n        {\n            return _includedStorageGb;\n        }\n\n        // If the update has a new quantity, use it to calculate the new max\n        var updatedItem = updateOptions.Items.FirstOrDefault(i => i.Quantity.HasValue);\n        if (updatedItem?.Quantity != null)\n        {\n            return (short)(_includedStorageGb + updatedItem.Quantity.Value);\n        }\n\n        // Otherwise, no change\n        return (short)(_includedStorageGb + currentQuantity);\n    }\n\n    public async Task<bool> UpdateDatabaseMaxStorageAsync(\n        SubscriptionPlanTier subscriptionPlanTier,\n        Guid entityId,\n        short newMaxStorageGb,\n        string subscriptionId)\n    {\n        try\n        {\n            switch (subscriptionPlanTier)\n            {\n                case SubscriptionPlanTier.Personal:\n                    {\n                        var user = await userRepository.GetByIdAsync(entityId);\n                        if (user == null)\n                        {\n                            logger.LogError(\n                                \"User not found for subscription {SubscriptionId}. Database not updated.\",\n                                subscriptionId);\n                            return false;\n                        }\n\n                        user.MaxStorageGb = newMaxStorageGb;\n                        await userRepository.ReplaceAsync(user);\n\n                        logger.LogInformation(\n                            \"Successfully updated User {UserId} MaxStorageGb to {MaxStorageGb} for subscription {SubscriptionId}\",\n                            user.Id,\n                            newMaxStorageGb,\n                            subscriptionId);\n                        return true;\n                    }\n                case SubscriptionPlanTier.Organization:\n                    {\n                        var organization = await organizationRepository.GetByIdAsync(entityId);\n                        if (organization == null)\n                        {\n                            logger.LogError(\n                                \"Organization not found for subscription {SubscriptionId}. Database not updated.\",\n                                subscriptionId);\n                            return false;\n                        }\n\n                        organization.MaxStorageGb = newMaxStorageGb;\n                        await organizationRepository.ReplaceAsync(organization);\n\n                        logger.LogInformation(\n                            \"Successfully updated Organization {OrganizationId} MaxStorageGb to {MaxStorageGb} for subscription {SubscriptionId}\",\n                            organization.Id,\n                            newMaxStorageGb,\n                            subscriptionId);\n                        return true;\n                    }\n                case SubscriptionPlanTier.Unknown:\n                default:\n                    return false;\n            }\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex,\n                \"Failed to update database MaxStorageGb for subscription {SubscriptionId} (Plan Tier: {SubscriptionType})\",\n                subscriptionId,\n                subscriptionPlanTier);\n            return false;\n        }\n    }\n\n    public static ITrigger GetTrigger()\n    {\n        return TriggerBuilder.Create()\n            .WithIdentity(\"EveryMorningTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 0 16 * * ?\") // 10am CST daily; the pods execute in UTC time\n            .Build();\n    }\n}\n"
  },
  {
    "path": "src/Billing/Jobs/SubscriptionCancellationJob.cs",
    "content": "﻿using Bit.Billing.Services;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Repositories;\nusing Quartz;\nusing Stripe;\n\nnamespace Bit.Billing.Jobs;\n\nusing static StripeConstants;\n\npublic class SubscriptionCancellationJob(\n    IStripeFacade stripeFacade,\n    IOrganizationRepository organizationRepository,\n    ILogger<SubscriptionCancellationJob> logger)\n    : IJob\n{\n    public async Task Execute(IJobExecutionContext context)\n    {\n        var subscriptionId = context.MergedJobDataMap.GetString(\"subscriptionId\");\n        var organizationId = new Guid(context.MergedJobDataMap.GetString(\"organizationId\") ?? string.Empty);\n\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n        if (organization == null || organization.Enabled)\n        {\n            logger.LogWarning(\"{Job} skipped for subscription ({SubscriptionID}) because organization is either null or enabled\", nameof(SubscriptionCancellationJob), subscriptionId);\n            // Organization was deleted or re-enabled by CS, skip cancellation\n            return;\n        }\n\n        var subscription = await stripeFacade.GetSubscription(subscriptionId, new SubscriptionGetOptions\n        {\n            Expand = [\"latest_invoice\"]\n        });\n\n        if (subscription is not\n            {\n                Status: SubscriptionStatus.Unpaid,\n                LatestInvoice: { BillingReason: BillingReasons.SubscriptionCreate or BillingReasons.SubscriptionCycle }\n            })\n        {\n            logger.LogWarning(\"{Job} skipped for subscription ({SubscriptionID}) because subscription is not unpaid or does not have a cancellable billing reason\", nameof(SubscriptionCancellationJob), subscriptionId);\n            return;\n        }\n\n        // Cancel the subscription\n        await stripeFacade.CancelSubscription(subscriptionId, new SubscriptionCancelOptions());\n\n        logger.LogInformation(\"{Job} cancelled subscription ({SubscriptionID})\", nameof(SubscriptionCancellationJob), subscriptionId);\n\n        // Void any open invoices\n        var options = new InvoiceListOptions\n        {\n            Status = \"open\",\n            Subscription = subscriptionId,\n            Limit = 100\n        };\n        var invoices = await stripeFacade.ListInvoices(options);\n        foreach (var invoice in invoices)\n        {\n            await stripeFacade.VoidInvoice(invoice.Id);\n            logger.LogInformation(\"{Job} voided invoice ({InvoiceID}) for subscription ({SubscriptionID})\", nameof(SubscriptionCancellationJob), invoice.Id, subscriptionId);\n        }\n\n        while (invoices.HasMore)\n        {\n            options.StartingAfter = invoices.Data.Last().Id;\n            invoices = await stripeFacade.ListInvoices(options);\n            foreach (var invoice in invoices)\n            {\n                await stripeFacade.VoidInvoice(invoice.Id);\n                logger.LogInformation(\"{Job} voided invoice ({InvoiceID}) for subscription ({SubscriptionID})\", nameof(SubscriptionCancellationJob), invoice.Id, subscriptionId);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Models/BitPayEventModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Billing.Models;\n\npublic class BitPayEventModel\n{\n    public EventModel Event { get; set; }\n    public InvoiceDataModel Data { get; set; }\n\n    public class EventModel\n    {\n        public int Code { get; set; }\n        public string Name { get; set; }\n    }\n\n    public class InvoiceDataModel\n    {\n        public string Id { get; set; }\n        public string Url { get; set; }\n        public string Status { get; set; }\n        public string Currency { get; set; }\n        public decimal Price { get; set; }\n        public string PosData { get; set; }\n        public bool ExceptionStatus { get; set; }\n        public long CurrentTime { get; set; }\n        public long AmountPaid { get; set; }\n        public string TransactionCurrency { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Models/LoginModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Billing.Models;\n\npublic class LoginModel\n{\n    [Required]\n    [EmailAddress]\n    public string Email { get; set; }\n}\n"
  },
  {
    "path": "src/Billing/Models/PayPalIPNTransactionModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Globalization;\nusing System.Runtime.InteropServices;\nusing System.Web;\n\nnamespace Bit.Billing.Models;\n\npublic class PayPalIPNTransactionModel\n{\n    public string TransactionId { get; }\n    public string TransactionType { get; }\n    public string ParentTransactionId { get; }\n    public string PaymentStatus { get; }\n    public string PaymentType { get; }\n    public decimal MerchantGross { get; }\n    public string MerchantCurrency { get; }\n    public string ReceiverId { get; }\n    public DateTime PaymentDate { get; }\n    public Guid? UserId { get; }\n    public Guid? OrganizationId { get; }\n    public Guid? ProviderId { get; }\n    public bool IsAccountCredit { get; }\n\n    public PayPalIPNTransactionModel(string formData)\n    {\n        var queryString = HttpUtility.ParseQueryString(formData);\n\n        var data = queryString\n            .AllKeys\n            .Where(key => !string.IsNullOrWhiteSpace(key))\n            .ToDictionary(key =>\n                key.Trim('\\r'),\n                key => queryString[key]?.Trim('\\r'));\n\n        TransactionId = Extract(data, \"txn_id\");\n        TransactionType = Extract(data, \"txn_type\");\n        ParentTransactionId = Extract(data, \"parent_txn_id\");\n        PaymentStatus = Extract(data, \"payment_status\");\n        PaymentType = Extract(data, \"payment_type\");\n\n        var merchantGross = Extract(data, \"mc_gross\");\n        if (!string.IsNullOrEmpty(merchantGross))\n        {\n            MerchantGross = decimal.Parse(merchantGross, CultureInfo.InvariantCulture);\n        }\n\n        MerchantCurrency = Extract(data, \"mc_currency\");\n        ReceiverId = Extract(data, \"receiver_id\");\n\n        var paymentDate = Extract(data, \"payment_date\");\n        PaymentDate = ToUTCDateTime(paymentDate);\n\n        var custom = Extract(data, \"custom\");\n\n        if (string.IsNullOrEmpty(custom))\n        {\n            return;\n        }\n\n        var metadata = custom.Split(',')\n            .Where(field => !string.IsNullOrEmpty(field) && field.Contains(':'))\n            .Select(field => field.Split(':'))\n            .ToDictionary(parts => parts[0], parts => parts[1]);\n\n        if (metadata.TryGetValue(\"user_id\", out var userIdStr) &&\n            Guid.TryParse(userIdStr, out var userId))\n        {\n            UserId = userId;\n        }\n\n        if (metadata.TryGetValue(\"organization_id\", out var organizationIdStr) &&\n            Guid.TryParse(organizationIdStr, out var organizationId))\n        {\n            OrganizationId = organizationId;\n        }\n\n        if (metadata.TryGetValue(\"provider_id\", out var providerIdStr) &&\n            Guid.TryParse(providerIdStr, out var providerId))\n        {\n            ProviderId = providerId;\n        }\n\n        IsAccountCredit = custom.Contains(\"account_credit:1\");\n    }\n\n    private static string Extract(IReadOnlyDictionary<string, string> data, string key)\n    {\n        var success = data.TryGetValue(key, out var value);\n        return success ? value : null;\n    }\n\n    private static DateTime ToUTCDateTime(string input)\n    {\n        if (string.IsNullOrEmpty(input))\n        {\n            return default;\n        }\n\n        var success = DateTime.TryParseExact(input,\n            new[]\n            {\n                \"HH:mm:ss dd MMM yyyy PDT\",\n                \"HH:mm:ss dd MMM yyyy PST\",\n                \"HH:mm:ss dd MMM, yyyy PST\",\n                \"HH:mm:ss dd MMM, yyyy PDT\",\n                \"HH:mm:ss MMM dd, yyyy PST\",\n                \"HH:mm:ss MMM dd, yyyy PDT\"\n            }, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTime);\n\n        if (!success)\n        {\n            return default;\n        }\n\n        var pacificTime = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)\n            ? TimeZoneInfo.FindSystemTimeZoneById(\"Pacific Standard Time\")\n            : TimeZoneInfo.FindSystemTimeZoneById(\"America/Los_Angeles\");\n\n        return TimeZoneInfo.ConvertTimeToUtc(dateTime, pacificTime);\n    }\n}\n"
  },
  {
    "path": "src/Billing/Models/Recovery/EventsRequestBody.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Billing.Models.Recovery;\n\npublic class EventsRequestBody\n{\n    [JsonPropertyName(\"eventIds\")]\n    public List<string> EventIds { get; set; }\n}\n"
  },
  {
    "path": "src/Billing/Models/Recovery/EventsResponseBody.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Billing.Models.Recovery;\n\npublic class EventsResponseBody\n{\n    [JsonPropertyName(\"events\")]\n    public List<EventResponseBody> Events { get; set; }\n}\n\npublic class EventResponseBody\n{\n    [JsonPropertyName(\"id\")]\n    public string Id { get; set; }\n\n    [JsonPropertyName(\"url\")]\n    public string URL { get; set; }\n\n    [JsonPropertyName(\"apiVersion\")]\n    public string APIVersion { get; set; }\n\n    [JsonPropertyName(\"type\")]\n    public string Type { get; set; }\n\n    [JsonPropertyName(\"createdUTC\")]\n    public DateTime CreatedUTC { get; set; }\n\n    [JsonPropertyName(\"processingError\")]\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public string ProcessingError { get; set; }\n}\n"
  },
  {
    "path": "src/Billing/Models/StripeWebhookDeliveryContainer.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Billing.Models;\n\npublic class StripeWebhookDeliveryContainer\n{\n    [JsonPropertyName(\"id\")]\n    public string Id { get; set; }\n    [JsonPropertyName(\"api_version\")]\n    public string ApiVersion { get; set; }\n    [JsonPropertyName(\"request\")]\n    public StripeWebhookRequestData Request { get; set; }\n    [JsonPropertyName(\"type\")]\n    public string Type { get; set; }\n}\n\npublic class StripeWebhookRequestData\n{\n    [JsonPropertyName(\"id\")]\n    public string Id { get; set; }\n}\n"
  },
  {
    "path": "src/Billing/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Billing;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        Host\n            .CreateDefaultBuilder(args)\n            .UseBitwardenSdk()\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging()\n            .Build()\n            .Run();\n    }\n}\n"
  },
  {
    "path": "src/Billing/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:44518/\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchUrl\": \"http://localhost:44518\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Billing\": {\n      \"commandName\": \"Project\",\n      \"launchUrl\": \"http://localhost:44518\",\n      \"applicationUrl\": \"http://localhost:44519\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Billing/Services/IPayPalIPNClient.cs",
    "content": "﻿namespace Bit.Billing.Services;\n\npublic interface IPayPalIPNClient\n{\n    Task<bool> VerifyIPN(string transactionId, string formData);\n}\n"
  },
  {
    "path": "src/Billing/Services/IProviderEventService.cs",
    "content": "﻿using Stripe;\n\nnamespace Bit.Billing.Services;\n\npublic interface IProviderEventService\n{\n    Task TryRecordInvoiceLineItems(Event parsedEvent);\n}\n"
  },
  {
    "path": "src/Billing/Services/IPushNotificationAdapter.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Entities;\n\nnamespace Bit.Billing.Services;\n\npublic interface IPushNotificationAdapter\n{\n    Task NotifyBankAccountVerifiedAsync(Organization organization);\n    Task NotifyBankAccountVerifiedAsync(Provider provider);\n    Task NotifyEnabledChangedAsync(Organization organization);\n    Task NotifyPremiumStatusChangedAsync(User user);\n}\n"
  },
  {
    "path": "src/Billing/Services/IStripeEventProcessor.cs",
    "content": "﻿using Event = Stripe.Event;\nnamespace Bit.Billing.Services;\n\npublic interface IStripeEventProcessor\n{\n    /// <summary>\n    /// Processes the specified Stripe event asynchronously.\n    /// </summary>\n    /// <param name=\"parsedEvent\">The Stripe event to be processed.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    Task ProcessEventAsync(Event parsedEvent);\n}\n"
  },
  {
    "path": "src/Billing/Services/IStripeEventService.cs",
    "content": "﻿using Stripe;\n\nnamespace Bit.Billing.Services;\n\npublic interface IStripeEventService\n{\n    /// <summary>\n    /// Extracts the <see cref=\"Charge\"/> object from the Stripe <see cref=\"Event\"/>. When <paramref name=\"fresh\"/> is true,\n    /// uses the charge ID extracted from the event to retrieve the most up-to-update charge from Stripe's API\n    /// and optionally expands it with the provided <see cref=\"expand\"/> options.\n    /// </summary>\n    /// <param name=\"stripeEvent\">The Stripe webhook event.</param>\n    /// <param name=\"fresh\">Determines whether to retrieve a fresh copy of the charge object from Stripe.</param>\n    /// <param name=\"expand\">Optionally provided to expand the fresh charge object retrieved from Stripe.</param>\n    /// <returns>A Stripe <see cref=\"Charge\"/>.</returns>\n    Task<Charge> GetCharge(Event stripeEvent, bool fresh = false, List<string>? expand = null);\n\n    /// <summary>\n    /// Extracts the <see cref=\"Customer\"/> object from the Stripe <see cref=\"Event\"/>. When <paramref name=\"fresh\"/> is true,\n    /// uses the customer ID extracted from the event to retrieve the most up-to-update customer from Stripe's API\n    /// and optionally expands it with the provided <see cref=\"expand\"/> options.\n    /// </summary>\n    /// <param name=\"stripeEvent\">The Stripe webhook event.</param>\n    /// <param name=\"fresh\">Determines whether to retrieve a fresh copy of the customer object from Stripe.</param>\n    /// <param name=\"expand\">Optionally provided to expand the fresh customer object retrieved from Stripe.</param>\n    /// <returns>A Stripe <see cref=\"Customer\"/>.</returns>\n    Task<Customer> GetCustomer(Event stripeEvent, bool fresh = false, List<string>? expand = null);\n\n    /// <summary>\n    /// Extracts the <see cref=\"Invoice\"/> object from the Stripe <see cref=\"Event\"/>. When <paramref name=\"fresh\"/> is true,\n    /// uses the invoice ID extracted from the event to retrieve the most up-to-update invoice from Stripe's API\n    /// and optionally expands it with the provided <see cref=\"expand\"/> options.\n    /// </summary>\n    /// <param name=\"stripeEvent\">The Stripe webhook event.</param>\n    /// <param name=\"fresh\">Determines whether to retrieve a fresh copy of the invoice object from Stripe.</param>\n    /// <param name=\"expand\">Optionally provided to expand the fresh invoice object retrieved from Stripe.</param>\n    /// <returns>A Stripe <see cref=\"Invoice\"/>.</returns>\n    Task<Invoice> GetInvoice(Event stripeEvent, bool fresh = false, List<string>? expand = null);\n\n    /// <summary>\n    /// Extracts the <see cref=\"PaymentMethod\"/> object from the Stripe <see cref=\"Event\"/>. When <paramref name=\"fresh\"/> is true,\n    /// uses the payment method ID extracted from the event to retrieve the most up-to-update payment method from Stripe's API\n    /// and optionally expands it with the provided <see cref=\"expand\"/> options.\n    /// </summary>\n    /// <param name=\"stripeEvent\">The Stripe webhook event.</param>\n    /// <param name=\"fresh\">Determines whether to retrieve a fresh copy of the payment method object from Stripe.</param>\n    /// <param name=\"expand\">Optionally provided to expand the fresh payment method object retrieved from Stripe.</param>\n    /// <returns>A Stripe <see cref=\"PaymentMethod\"/>.</returns>\n    Task<PaymentMethod> GetPaymentMethod(Event stripeEvent, bool fresh = false, List<string>? expand = null);\n\n    /// <summary>\n    /// Extracts the <see cref=\"SetupIntent\"/> object from the Stripe <see cref=\"Event\"/>. When <paramref name=\"fresh\"/> is true,\n    /// uses the setup intent ID extracted from the event to retrieve the most up-to-update setup intent from Stripe's API\n    /// and optionally expands it with the provided <see cref=\"expand\"/> options.\n    /// </summary>\n    /// <param name=\"stripeEvent\">The Stripe webhook event.</param>\n    /// <param name=\"fresh\">Determines whether to retrieve a fresh copy of the setup intent object from Stripe.</param>\n    /// <param name=\"expand\">Optionally provided to expand the fresh setup intent object retrieved from Stripe.</param>\n    /// <returns>A Stripe <see cref=\"SetupIntent\"/>.</returns>\n    Task<SetupIntent> GetSetupIntent(Event stripeEvent, bool fresh = false, List<string>? expand = null);\n\n    /// <summary>\n    /// Extracts the <see cref=\"Subscription\"/> object from the Stripe <see cref=\"Event\"/>. When <paramref name=\"fresh\"/> is true,\n    /// uses the subscription ID extracted from the event to retrieve the most up-to-update subscription from Stripe's API\n    /// and optionally expands it with the provided <see cref=\"expand\"/> options.\n    /// </summary>\n    /// <param name=\"stripeEvent\">The Stripe webhook event.</param>\n    /// <param name=\"fresh\">Determines whether to retrieve a fresh copy of the subscription object from Stripe.</param>\n    /// <param name=\"expand\">Optionally provided to expand the fresh subscription object retrieved from Stripe.</param>\n    /// <returns>A Stripe <see cref=\"Subscription\"/>.</returns>\n    Task<Subscription> GetSubscription(Event stripeEvent, bool fresh = false, List<string>? expand = null);\n\n    /// <summary>\n    /// Ensures that the customer associated with the Stripe <see cref=\"Event\"/> is in the correct region for this server.\n    /// We use the customer instead of the subscription given that all subscriptions have customers, but not all\n    /// customers have subscriptions.\n    /// </summary>\n    /// <param name=\"stripeEvent\">The Stripe webhook event.</param>\n    /// <returns>True if the customer's region and the server's region match, otherwise false.</returns>\n    Task<bool> ValidateCloudRegion(Event stripeEvent);\n}\n"
  },
  {
    "path": "src/Billing/Services/IStripeEventUtilityService.cs",
    "content": "﻿using Stripe;\nusing Transaction = Bit.Core.Entities.Transaction;\nnamespace Bit.Billing.Services;\n\npublic interface IStripeEventUtilityService\n{\n    /// <summary>\n    /// Gets the organization or user ID from the metadata of a Stripe Charge object.\n    /// </summary>\n    /// <param name=\"charge\"></param>\n    /// <returns></returns>\n    Task<(Guid?, Guid?, Guid?)> GetEntityIdsFromChargeAsync(Charge charge);\n\n    /// <summary>\n    /// Gets the organizationId, userId, or providerId from the metadata of a Stripe Subscription object.\n    /// </summary>\n    /// <param name=\"metadata\"></param>\n    /// <returns></returns>\n    Tuple<Guid?, Guid?, Guid?> GetIdsFromMetadata(Dictionary<string, string> metadata);\n\n    /// <summary>\n    /// Determines whether the specified subscription is a sponsored subscription.\n    /// </summary>\n    /// <param name=\"subscription\">The subscription to be evaluated.</param>\n    /// <returns>\n    /// A boolean value indicating whether the subscription is a sponsored subscription.\n    /// Returns <c>true</c> if the subscription matches any of the sponsored plans; otherwise, <c>false</c>.\n    /// </returns>\n    bool IsSponsoredSubscription(Subscription subscription);\n\n    /// <summary>\n    /// Converts a Stripe Charge object to a Bitwarden Transaction object.\n    /// </summary>\n    /// <param name=\"charge\"></param>\n    /// <param name=\"organizationId\"></param>\n    /// <param name=\"userId\"></param>\n    /// /// <param name=\"providerId\"></param>\n    /// <returns></returns>\n    Task<Transaction> FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId);\n\n    /// <summary>\n    /// Attempts to pay the specified invoice. If a customer is eligible, the invoice is paid using Braintree or Stripe.\n    /// </summary>\n    /// <param name=\"invoice\">The invoice to be paid.</param>\n    /// <param name=\"attemptToPayWithStripe\">Indicates whether to attempt payment with Stripe. Defaults to false.</param>\n    /// <returns>A task representing the asynchronous operation. The task result contains a boolean value indicating whether the invoice payment attempt was successful.</returns>\n    Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false);\n\n\n    /// <summary>\n    /// Determines whether an invoice should be attempted to be paid based on certain criteria.\n    /// </summary>\n    /// <param name=\"invoice\">The invoice to be evaluated.</param>\n    /// <returns>A boolean value indicating whether the invoice should be attempted to be paid.</returns>\n    bool ShouldAttemptToPayInvoice(Invoice invoice);\n\n    /// <summary>\n    /// The ID for the premium annual plan.\n    /// </summary>\n    const string PremiumPlanId = \"premium-annually\";\n\n    /// <summary>\n    /// The ID for the premium annual plan via the App Store.\n    /// </summary>\n    const string PremiumPlanIdAppStore = \"premium-annually-app\";\n\n}\n"
  },
  {
    "path": "src/Billing/Services/IStripeFacade.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Stripe;\nusing Stripe.TestHelpers;\n\nnamespace Bit.Billing.Services;\n\npublic interface IStripeFacade\n{\n    Task<Charge> GetCharge(\n        string chargeId,\n        ChargeGetOptions chargeGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Customer> GetCustomer(\n        string customerId,\n        CustomerGetOptions customerGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    IAsyncEnumerable<CustomerCashBalanceTransaction> GetCustomerCashBalanceTransactions(\n        string customerId,\n        CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Customer> UpdateCustomer(\n        string customerId,\n        CustomerUpdateOptions customerUpdateOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Event> GetEvent(\n        string eventId,\n        EventGetOptions eventGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Invoice> GetInvoice(\n        string invoiceId,\n        InvoiceGetOptions invoiceGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<SetupIntent> GetSetupIntent(\n        string setupIntentId,\n        SetupIntentGetOptions setupIntentGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<StripeList<Invoice>> ListInvoices(\n        InvoiceListOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Invoice> UpdateInvoice(\n        string invoiceId,\n        InvoiceUpdateOptions invoiceGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Invoice> PayInvoice(\n        string invoiceId,\n        InvoicePayOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Invoice> VoidInvoice(\n        string invoiceId,\n        InvoiceVoidOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<PaymentMethod> GetPaymentMethod(\n        string paymentMethodId,\n        PaymentMethodGetOptions paymentMethodGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<StripeList<Subscription>> ListSubscriptions(\n        SubscriptionListOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    IAsyncEnumerable<Subscription> ListSubscriptionsAutoPagingAsync(\n        SubscriptionListOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Subscription> GetSubscription(\n        string subscriptionId,\n        SubscriptionGetOptions subscriptionGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Subscription> UpdateSubscription(\n        string subscriptionId,\n        SubscriptionUpdateOptions subscriptionGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Subscription> CancelSubscription(\n        string subscriptionId,\n        SubscriptionCancelOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Discount> DeleteCustomerDiscount(\n        string customerId,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Discount> DeleteSubscriptionDiscount(\n        string subscriptionId,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<TestClock> GetTestClock(\n        string testClockId,\n        TestClockGetOptions testClockGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n\n    Task<Coupon> GetCoupon(\n        string couponId,\n        CouponGetOptions couponGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "src/Billing/Services/IStripeWebhookHandler.cs",
    "content": "﻿using Event = Stripe.Event;\nnamespace Bit.Billing.Services;\n\npublic interface IStripeWebhookHandler\n{\n    /// <summary>\n    /// Handles the specified Stripe event asynchronously.\n    /// </summary>\n    /// <param name=\"parsedEvent\">The Stripe event to be handled.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    Task HandleAsync(Event parsedEvent);\n}\n\n/// <summary>\n/// Defines the contract for handling Stripe subscription deleted events.\n/// </summary>\npublic interface ISubscriptionDeletedHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe subscription updated events.\n/// </summary>\npublic interface ISubscriptionUpdatedHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe upcoming invoice events.\n/// </summary>\npublic interface IUpcomingInvoiceHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe charge succeeded events.\n/// </summary>\npublic interface IChargeSucceededHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe charge refunded events.\n/// </summary>\npublic interface IChargeRefundedHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe payment succeeded events.\n/// </summary>\npublic interface IPaymentSucceededHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe payment failed events.\n/// </summary>\npublic interface IPaymentFailedHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe invoice created events.\n/// </summary>\npublic interface IInvoiceCreatedHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe payment method attached events.\n/// </summary>\npublic interface IPaymentMethodAttachedHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe customer updated events.\n/// </summary>\npublic interface ICustomerUpdatedHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe Invoice Finalized events.\n/// </summary>\npublic interface IInvoiceFinalizedHandler : IStripeWebhookHandler;\n\npublic interface ISetupIntentSucceededHandler : IStripeWebhookHandler;\n\n/// <summary>\n/// Defines the contract for handling Stripe coupon deleted events.\n/// </summary>\npublic interface ICouponDeletedHandler : IStripeWebhookHandler;\n"
  },
  {
    "path": "src/Billing/Services/Implementations/ChargeRefundedHandler.cs",
    "content": "﻿using Bit.Billing.Constants;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Microsoft.Data.SqlClient;\nusing Event = Stripe.Event;\nusing Transaction = Bit.Core.Entities.Transaction;\nusing TransactionType = Bit.Core.Enums.TransactionType;\nnamespace Bit.Billing.Services.Implementations;\n\npublic class ChargeRefundedHandler : IChargeRefundedHandler\n{\n    private readonly ILogger<ChargeRefundedHandler> _logger;\n    private readonly IStripeEventService _stripeEventService;\n    private readonly ITransactionRepository _transactionRepository;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n\n    public ChargeRefundedHandler(\n        ILogger<ChargeRefundedHandler> logger,\n        IStripeEventService stripeEventService,\n        ITransactionRepository transactionRepository,\n        IStripeEventUtilityService stripeEventUtilityService)\n    {\n        _logger = logger;\n        _stripeEventService = stripeEventService;\n        _transactionRepository = transactionRepository;\n        _stripeEventUtilityService = stripeEventUtilityService;\n    }\n\n    /// <summary>\n    /// Handles the <see cref=\"HandledStripeWebhook.ChargeRefunded\"/> event type from Stripe.\n    /// </summary>\n    /// <param name=\"parsedEvent\"></param>\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        var charge = await _stripeEventService.GetCharge(parsedEvent, true, [\"refunds\"]);\n        var parentTransaction = await _transactionRepository.GetByGatewayIdAsync(GatewayType.Stripe, charge.Id);\n        if (parentTransaction == null)\n        {\n            // Attempt to create a transaction for the charge if it doesn't exist\n            var (organizationId, userId, providerId) = await _stripeEventUtilityService.GetEntityIdsFromChargeAsync(charge);\n            var tx = await _stripeEventUtilityService.FromChargeToTransactionAsync(charge, organizationId, userId, providerId);\n            try\n            {\n                parentTransaction = await _transactionRepository.CreateAsync(tx);\n            }\n            catch (SqlException e) when (e.Number == 547) // FK constraint violation\n            {\n                _logger.LogWarning(\n                    \"Charge refund could not create transaction as entity may have been deleted. {ChargeId}\",\n                    charge.Id);\n                return;\n            }\n        }\n\n        var amountRefunded = charge.AmountRefunded / 100M;\n\n        if (parentTransaction.Refunded.GetValueOrDefault() ||\n            parentTransaction.RefundedAmount.GetValueOrDefault() >= amountRefunded)\n        {\n            _logger.LogWarning(\n                \"Charge refund amount doesn't match parent transaction's amount or parent has already been refunded. {ChargeId}\",\n                charge.Id);\n            return;\n        }\n\n        parentTransaction.RefundedAmount = amountRefunded;\n        if (charge.Refunded)\n        {\n            parentTransaction.Refunded = true;\n        }\n\n        await _transactionRepository.ReplaceAsync(parentTransaction);\n\n        foreach (var refund in charge.Refunds)\n        {\n            var refundTransaction = await _transactionRepository.GetByGatewayIdAsync(\n                GatewayType.Stripe, refund.Id);\n            if (refundTransaction != null)\n            {\n                continue;\n            }\n\n            await _transactionRepository.CreateAsync(new Transaction\n            {\n                Amount = refund.Amount / 100M,\n                CreationDate = refund.Created,\n                OrganizationId = parentTransaction.OrganizationId,\n                UserId = parentTransaction.UserId,\n                ProviderId = parentTransaction.ProviderId,\n                Type = TransactionType.Refund,\n                Gateway = GatewayType.Stripe,\n                GatewayId = refund.Id,\n                PaymentMethodType = parentTransaction.PaymentMethodType,\n                Details = parentTransaction.Details\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/ChargeSucceededHandler.cs",
    "content": "﻿using Bit.Billing.Constants;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Microsoft.Data.SqlClient;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class ChargeSucceededHandler : IChargeSucceededHandler\n{\n    private readonly ILogger<ChargeSucceededHandler> _logger;\n    private readonly IStripeEventService _stripeEventService;\n    private readonly ITransactionRepository _transactionRepository;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n\n    public ChargeSucceededHandler(\n        ILogger<ChargeSucceededHandler> logger,\n        IStripeEventService stripeEventService,\n        ITransactionRepository transactionRepository,\n        IStripeEventUtilityService stripeEventUtilityService)\n    {\n        _logger = logger;\n        _stripeEventService = stripeEventService;\n        _transactionRepository = transactionRepository;\n        _stripeEventUtilityService = stripeEventUtilityService;\n    }\n\n    /// <summary>\n    /// Handles the <see cref=\"HandledStripeWebhook.ChargeSucceeded\"/> event type from Stripe.\n    /// </summary>\n    /// <param name=\"parsedEvent\"></param>\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        var charge = await _stripeEventService.GetCharge(parsedEvent);\n        var existingTransaction = await _transactionRepository.GetByGatewayIdAsync(GatewayType.Stripe, charge.Id);\n        if (existingTransaction is not null)\n        {\n            _logger.LogInformation(\"Charge success already processed. {ChargeId}\", charge.Id);\n            return;\n        }\n\n        var (organizationId, userId, providerId) = await _stripeEventUtilityService.GetEntityIdsFromChargeAsync(charge);\n        if (!organizationId.HasValue && !userId.HasValue && !providerId.HasValue)\n        {\n            _logger.LogWarning(\"Charge success has no subscriber ids. {ChargeId}\", charge.Id);\n            return;\n        }\n\n        var transaction = await _stripeEventUtilityService.FromChargeToTransactionAsync(charge, organizationId, userId, providerId);\n        if (!transaction.PaymentMethodType.HasValue)\n        {\n            _logger.LogWarning(\"Charge success from unsupported source/method. {ChargeId}\", charge.Id);\n            return;\n        }\n\n        try\n        {\n            await _transactionRepository.CreateAsync(transaction);\n        }\n        catch (SqlException e) when (e.Number == 547)\n        {\n            _logger.LogWarning(\n                \"Charge success could not create transaction as entity may have been deleted. {ChargeId}\",\n                charge.Id);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/CouponDeletedHandler.cs",
    "content": "﻿using Bit.Core.Billing.Subscriptions.Repositories;\nusing Stripe;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class CouponDeletedHandler(\n    ILogger<CouponDeletedHandler> logger,\n    ISubscriptionDiscountRepository subscriptionDiscountRepository) : ICouponDeletedHandler\n{\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        if (parsedEvent.Data.Object is not Coupon coupon)\n        {\n            logger.LogWarning(\"Received coupon.deleted event with unexpected object type. Event ID: {EventId}\", parsedEvent.Id);\n            return;\n        }\n\n        var discount = await subscriptionDiscountRepository.GetByStripeCouponIdAsync(coupon.Id);\n\n        if (discount is null)\n        {\n            logger.LogInformation(\"Received coupon.deleted event for coupon {CouponId} not found in database. Ignoring.\", coupon.Id);\n            return;\n        }\n\n        await subscriptionDiscountRepository.DeleteAsync(discount);\n        logger.LogInformation(\"Deleted subscription discount for Stripe coupon {CouponId}.\", coupon.Id);\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/CustomerUpdatedHandler.cs",
    "content": "﻿using Bit.Core.Repositories;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class CustomerUpdatedHandler : ICustomerUpdatedHandler\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IStripeEventService _stripeEventService;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n    private readonly ILogger<CustomerUpdatedHandler> _logger;\n\n    public CustomerUpdatedHandler(\n        IOrganizationRepository organizationRepository,\n        IStripeEventService stripeEventService,\n        IStripeEventUtilityService stripeEventUtilityService,\n        ILogger<CustomerUpdatedHandler> logger)\n    {\n        _organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository));\n        _stripeEventService = stripeEventService;\n        _stripeEventUtilityService = stripeEventUtilityService;\n        _logger = logger;\n    }\n\n    /// <summary>\n    /// Handles the <see cref=\"HandledStripeWebhook.CustomerUpdated\"/> event type from Stripe.\n    /// </summary>\n    /// <param name=\"parsedEvent\"></param>\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        if (parsedEvent == null)\n        {\n            _logger.LogError(\"Parsed event was null in CustomerUpdatedHandler\");\n            throw new ArgumentNullException(nameof(parsedEvent));\n        }\n\n        if (_stripeEventService == null)\n        {\n            _logger.LogError(\"StripeEventService was not initialized in CustomerUpdatedHandler\");\n            throw new InvalidOperationException($\"{nameof(_stripeEventService)} is not initialized\");\n        }\n\n        var customer = await _stripeEventService.GetCustomer(parsedEvent, true, [\"subscriptions\"]);\n        if (customer?.Subscriptions == null || !customer.Subscriptions.Any())\n        {\n            _logger.LogWarning(\"Customer or subscriptions were null or empty in CustomerUpdatedHandler. Customer ID: {CustomerId}\", customer?.Id);\n            return;\n        }\n\n        var subscription = customer.Subscriptions.First();\n\n        if (subscription.Metadata == null)\n        {\n            _logger.LogWarning(\"Subscription metadata was null in CustomerUpdatedHandler. Subscription ID: {SubscriptionId}\", subscription.Id);\n            return;\n        }\n\n        if (_stripeEventUtilityService == null)\n        {\n            _logger.LogError(\"StripeEventUtilityService was not initialized in CustomerUpdatedHandler\");\n            throw new InvalidOperationException($\"{nameof(_stripeEventUtilityService)} is not initialized\");\n        }\n\n        var (organizationId, _, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);\n\n        if (!organizationId.HasValue)\n        {\n            _logger.LogWarning(\"Organization ID was not found in subscription metadata. Subscription ID: {SubscriptionId}\", subscription.Id);\n            return;\n        }\n\n        if (_organizationRepository == null)\n        {\n            _logger.LogError(\"OrganizationRepository was not initialized in CustomerUpdatedHandler\");\n            throw new InvalidOperationException($\"{nameof(_organizationRepository)} is not initialized\");\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);\n\n        if (organization == null)\n        {\n            _logger.LogWarning(\"Organization not found. Organization ID: {OrganizationId}\", organizationId.Value);\n            return;\n        }\n\n        organization.BillingEmail = customer.Email;\n        await _organizationRepository.ReplaceAsync(organization);\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/InvoiceCreatedHandler.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Services;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class InvoiceCreatedHandler(\n    IBraintreeService braintreeService,\n    ILogger<InvoiceCreatedHandler> logger,\n    IStripeEventService stripeEventService,\n    IProviderEventService providerEventService)\n    : IInvoiceCreatedHandler\n{\n\n    /// <summary>\n    /// <para>\n    /// This handler processes the `invoice.created` event in <see href=\"https://docs.stripe.com/api/events/types#event_types-invoice.created\">Stripe</see>. It has\n    /// two primary responsibilities.\n    /// </para>\n    /// <para>\n    /// 1. Checks to see if the newly created invoice belongs to a PayPal customer. If it does, and the invoice is ready to be paid, it will attempt to pay the invoice\n    /// with Braintree and then let Stripe know the invoice can be marked as paid.\n    /// </para>\n    /// <para>\n    /// 2. If the invoice is for a provider, it records a point-in-time snapshot of the invoice broken down by the provider's client organizations. This is later used in\n    /// the provider invoice export.\n    /// </para>\n    /// </summary>\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        try\n        {\n            var invoice = await stripeEventService.GetInvoice(parsedEvent, true, [\"customer\", \"parent.subscription_details.subscription\"]);\n\n            var usingPayPal = invoice.Customer.Metadata.ContainsKey(\"btCustomerId\");\n\n            if (usingPayPal && invoice is\n                {\n                    AmountDue: > 0,\n                    Status: not StripeConstants.InvoiceStatus.Paid,\n                    CollectionMethod: \"charge_automatically\",\n                    BillingReason:\n                    \"subscription_cycle\" or\n                    \"automatic_pending_invoice_item_invoice\",\n                    Parent.SubscriptionDetails.Subscription: not null\n                })\n            {\n                await braintreeService.PayInvoice(invoice.Parent.SubscriptionDetails.Subscription, invoice);\n            }\n        }\n        catch (Exception exception)\n        {\n            logger.LogError(exception, \"Failed to attempt paying for invoice while handling 'invoice.created' event ({EventID})\", parsedEvent.Id);\n        }\n\n        try\n        {\n            await providerEventService.TryRecordInvoiceLineItems(parsedEvent);\n        }\n        catch (Exception exception)\n        {\n            logger.LogError(exception, \"Failed to record provider invoice line items while handling 'invoice.created' event ({EventID})\", parsedEvent.Id);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/InvoiceFinalizedHandler.cs",
    "content": "﻿using Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class InvoiceFinalizedHandler : IInvoiceFinalizedHandler\n{\n\n    private readonly IProviderEventService _providerEventService;\n\n    public InvoiceFinalizedHandler(IProviderEventService providerEventService)\n    {\n        _providerEventService = providerEventService;\n    }\n\n    /// <summary>\n    /// Handles the <see cref=\"HandledStripeWebhook.InvoiceFinalized\"/> event type from Stripe.\n    /// </summary>\n    /// <param name=\"parsedEvent\"></param>\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        await _providerEventService.TryRecordInvoiceLineItems(parsedEvent);\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/PayPalIPNClient.cs",
    "content": "﻿using System.Text;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class PayPalIPNClient : IPayPalIPNClient\n{\n    private readonly HttpClient _httpClient;\n    private readonly Uri _ipnEndpoint;\n    private readonly ILogger<PayPalIPNClient> _logger;\n\n    public PayPalIPNClient(\n        IOptions<BillingSettings> billingSettings,\n        HttpClient httpClient,\n        ILogger<PayPalIPNClient> logger)\n    {\n        _httpClient = httpClient;\n        _ipnEndpoint = new Uri(billingSettings.Value.PayPal.Production\n            ? \"https://www.paypal.com/cgi-bin/webscr\"\n            : \"https://www.sandbox.paypal.com/cgi-bin/webscr\");\n        _logger = logger;\n    }\n\n    public async Task<bool> VerifyIPN(string transactionId, string formData)\n    {\n        LogInfo(transactionId, $\"Verifying IPN against {_ipnEndpoint}\");\n\n        if (string.IsNullOrEmpty(formData))\n        {\n            throw new ArgumentNullException(nameof(formData));\n        }\n\n        var requestMessage = new HttpRequestMessage { Method = HttpMethod.Post, RequestUri = _ipnEndpoint };\n\n        var requestContent = string.Concat(\"cmd=_notify-validate&\", formData);\n\n        requestMessage.Content = new StringContent(requestContent, Encoding.UTF8, \"application/x-www-form-urlencoded\");\n\n        var response = await _httpClient.SendAsync(requestMessage);\n\n        var responseContent = await response.Content.ReadAsStringAsync();\n\n        if (response.IsSuccessStatusCode)\n        {\n            return responseContent switch\n            {\n                \"VERIFIED\" => Verified(),\n                \"INVALID\" => Invalid(),\n                _ => Unhandled(responseContent)\n            };\n        }\n\n        LogError(transactionId, $\"Unsuccessful Response | Status Code: {response.StatusCode} | Content: {responseContent}\");\n\n        return false;\n\n        bool Verified()\n        {\n            LogInfo(transactionId, \"Verified\");\n            return true;\n        }\n\n        bool Invalid()\n        {\n            LogError(transactionId, \"Verification Invalid\");\n            return false;\n        }\n\n        bool Unhandled(string content)\n        {\n            LogWarning(transactionId, $\"Unhandled Response Content: {content}\");\n            return false;\n        }\n    }\n\n    private void LogInfo(string transactionId, string message)\n        => _logger.LogInformation(\"Verify PayPal IPN ({Id}) | {Message}\", transactionId, message);\n\n    private void LogWarning(string transactionId, string message)\n        => _logger.LogWarning(\"Verify PayPal IPN ({Id}) | {Message}\", transactionId, message);\n\n    private void LogError(string transactionId, string message)\n        => _logger.LogError(\"Verify PayPal IPN ({Id}) | {Message}\", transactionId, message);\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/PaymentFailedHandler.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Stripe;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class PaymentFailedHandler : IPaymentFailedHandler\n{\n    private readonly IStripeEventService _stripeEventService;\n    private readonly IStripeFacade _stripeFacade;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n\n    public PaymentFailedHandler(\n        IStripeEventService stripeEventService,\n        IStripeFacade stripeFacade,\n        IStripeEventUtilityService stripeEventUtilityService)\n    {\n        _stripeEventService = stripeEventService;\n        _stripeFacade = stripeFacade;\n        _stripeEventUtilityService = stripeEventUtilityService;\n    }\n\n    /// <summary>\n    /// Handles the <see cref=\"HandledStripeWebhook.PaymentFailed\"/> event type from Stripe.\n    /// </summary>\n    /// <param name=\"parsedEvent\"></param>\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        var invoice = await _stripeEventService.GetInvoice(parsedEvent, true);\n        if (invoice.Status == StripeConstants.InvoiceStatus.Paid || invoice.AttemptCount <= 1 || !ShouldAttemptToPayInvoice(invoice))\n        {\n            return;\n        }\n\n        if (invoice.Parent?.SubscriptionDetails != null)\n        {\n            var subscription = await _stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId);\n            // attempt count 4 = 11 days after initial failure\n            if (invoice.AttemptCount <= 3 ||\n                !subscription.Items.Any(i => i.Price.Id is IStripeEventUtilityService.PremiumPlanId or IStripeEventUtilityService.PremiumPlanIdAppStore))\n            {\n                await _stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice);\n            }\n        }\n    }\n\n    private static bool ShouldAttemptToPayInvoice(Invoice invoice) =>\n        invoice is\n        {\n            AmountDue: > 0,\n            Status: not StripeConstants.InvoiceStatus.Paid,\n            CollectionMethod: \"charge_automatically\",\n            BillingReason: \"subscription_cycle\" or \"automatic_pending_invoice_item_invoice\",\n            Parent.SubscriptionDetails: not null\n        };\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Billing.Constants;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Extensions;\nusing Stripe;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler\n{\n    private readonly ILogger<PaymentMethodAttachedHandler> _logger;\n    private readonly IStripeEventService _stripeEventService;\n    private readonly IStripeFacade _stripeFacade;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n    private readonly IProviderRepository _providerRepository;\n\n    public PaymentMethodAttachedHandler(ILogger<PaymentMethodAttachedHandler> logger,\n        IStripeEventService stripeEventService,\n        IStripeFacade stripeFacade,\n        IStripeEventUtilityService stripeEventUtilityService,\n        IProviderRepository providerRepository)\n    {\n        _logger = logger;\n        _stripeEventService = stripeEventService;\n        _stripeFacade = stripeFacade;\n        _stripeEventUtilityService = stripeEventUtilityService;\n        _providerRepository = providerRepository;\n    }\n\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent, true, [\"customer.subscriptions.data.latest_invoice\"]);\n\n        if (paymentMethod == null)\n        {\n            _logger.LogWarning(\"Attempted to handle the event payment_method.attached but paymentMethod was null\");\n            return;\n        }\n\n        var customer = paymentMethod.Customer;\n        var subscriptions = customer?.Subscriptions;\n\n        // This represents a provider subscription set to \"send_invoice\" that was paid using a Stripe hosted invoice payment page.\n        var invoicedProviderSubscription = subscriptions?.Data.FirstOrDefault(subscription =>\n            subscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.ProviderId) &&\n            subscription.Status != StripeConstants.SubscriptionStatus.Canceled &&\n            subscription.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice);\n\n        /*\n         * If we have an invoiced provider subscription where the customer hasn't been marked as invoice-approved,\n         * we need to try and set the default payment method and update the collection method to be \"charge_automatically\".\n         */\n        if (invoicedProviderSubscription != null &&\n            !customer.ApprovedToPayByInvoice() &&\n            Guid.TryParse(invoicedProviderSubscription.Metadata[StripeConstants.MetadataKeys.ProviderId], out var providerId))\n        {\n            var provider = await _providerRepository.GetByIdAsync(providerId);\n\n            if (provider is { Type: ProviderType.Msp })\n            {\n                if (customer.InvoiceSettings.DefaultPaymentMethodId != paymentMethod.Id)\n                {\n                    try\n                    {\n                        await _stripeFacade.UpdateCustomer(customer.Id,\n                            new CustomerUpdateOptions\n                            {\n                                InvoiceSettings = new CustomerInvoiceSettingsOptions\n                                {\n                                    DefaultPaymentMethod = paymentMethod.Id\n                                }\n                            });\n                    }\n                    catch (Exception exception)\n                    {\n                        _logger.LogWarning(exception,\n                            \"Failed to set customer's ({CustomerID}) default payment method during 'payment_method.attached' webhook\",\n                            customer.Id);\n                    }\n                }\n\n                try\n                {\n                    await _stripeFacade.UpdateSubscription(invoicedProviderSubscription.Id,\n                        new SubscriptionUpdateOptions\n                        {\n                            CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically\n                        });\n                }\n                catch (Exception exception)\n                {\n                    _logger.LogWarning(exception,\n                        \"Failed to set subscription's ({SubscriptionID}) collection method to 'charge_automatically' during 'payment_method.attached' webhook\",\n                        customer.Id);\n                }\n            }\n        }\n\n        var unpaidSubscriptions = subscriptions?.Data.Where(subscription =>\n            subscription.Status == StripeConstants.SubscriptionStatus.Unpaid).ToList();\n\n        var incompleteSubscriptions = subscriptions?.Data.Where(subscription =>\n            subscription.Status == StripeConstants.SubscriptionStatus.Incomplete).ToList();\n\n        // Process unpaid subscriptions\n        if (unpaidSubscriptions != null && unpaidSubscriptions.Count > 0)\n        {\n            foreach (var subscription in unpaidSubscriptions)\n            {\n                await AttemptToPayOpenSubscriptionAsync(subscription);\n            }\n        }\n\n        // Process incomplete subscriptions - only if there's exactly one to avoid overcharging\n        if (incompleteSubscriptions == null || incompleteSubscriptions.Count == 0)\n        {\n            return;\n        }\n\n        if (incompleteSubscriptions.Count > 1)\n        {\n            _logger.LogWarning(\n                \"Customer {CustomerId} has {Count} incomplete subscriptions. Skipping automatic payment retry to avoid overcharging. Subscription IDs: {SubscriptionIds}\",\n                customer.Id,\n                incompleteSubscriptions.Count,\n                string.Join(\", \", incompleteSubscriptions.Select(s => s.Id)));\n            return;\n        }\n\n        // Exactly one incomplete subscription - safe to retry\n        await AttemptToPayOpenSubscriptionAsync(incompleteSubscriptions.First());\n    }\n\n    private async Task AttemptToPayOpenSubscriptionAsync(Subscription subscription)\n    {\n        var latestInvoice = subscription.LatestInvoice;\n\n        if (subscription.LatestInvoice is null)\n        {\n            _logger.LogWarning(\n                \"Attempted to pay subscription {SubscriptionId} with status {Status} but latest invoice didn't exist\",\n                subscription.Id, subscription.Status);\n\n            return;\n        }\n\n        if (latestInvoice.Status != StripeInvoiceStatus.Open)\n        {\n            _logger.LogWarning(\n                \"Attempted to pay subscription {SubscriptionId} with status {Status} but latest invoice wasn't \\\"open\\\"\",\n                subscription.Id, subscription.Status);\n\n            return;\n        }\n\n        try\n        {\n            await _stripeEventUtilityService.AttemptToPayInvoiceAsync(latestInvoice, true);\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(e,\n                \"Attempted to pay open invoice {InvoiceId} on subscription {SubscriptionId} with status {Status} but encountered an error\",\n                latestInvoice.Id, subscription.Id, subscription.Status);\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/PaymentSucceededHandler.cs",
    "content": "﻿using Bit.Billing.Constants;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class PaymentSucceededHandler(\n    ILogger<PaymentSucceededHandler> logger,\n    IStripeEventService stripeEventService,\n    IStripeFacade stripeFacade,\n    IProviderRepository providerRepository,\n    IOrganizationRepository organizationRepository,\n    IStripeEventUtilityService stripeEventUtilityService,\n    IUserService userService,\n    IUserRepository userRepository,\n    IOrganizationEnableCommand organizationEnableCommand,\n    IPricingClient pricingClient,\n    IPushNotificationAdapter pushNotificationAdapter)\n    : IPaymentSucceededHandler\n{\n    /// <summary>\n    /// Handles the <see cref=\"HandledStripeWebhook.PaymentSucceeded\"/> event type from Stripe.\n    /// </summary>\n    /// <param name=\"parsedEvent\"></param>\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        var invoice = await stripeEventService.GetInvoice(parsedEvent, true);\n        if (invoice.Status != StripeConstants.InvoiceStatus.Paid || invoice.BillingReason != \"subscription_create\")\n        {\n            return;\n        }\n\n        if (invoice.Parent?.SubscriptionDetails == null)\n        {\n            return;\n        }\n\n        var subscription = await stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId);\n        if (subscription?.Status != StripeSubscriptionStatus.Active)\n        {\n            return;\n        }\n\n        if (DateTime.UtcNow - invoice.Created < TimeSpan.FromMinutes(1))\n        {\n            await Task.Delay(5000);\n        }\n\n        var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);\n\n        if (providerId.HasValue)\n        {\n            var provider = await providerRepository.GetByIdAsync(providerId.Value);\n\n            if (provider == null)\n            {\n                logger.LogError(\n                    \"Received invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) that does not exist\",\n                    parsedEvent.Id,\n                    providerId.Value);\n\n                return;\n            }\n\n            var teamsMonthly = await pricingClient.GetPlanOrThrow(PlanType.TeamsMonthly);\n\n            var enterpriseMonthly = await pricingClient.GetPlanOrThrow(PlanType.EnterpriseMonthly);\n\n            var teamsMonthlyLineItem =\n                subscription.Items.Data.FirstOrDefault(item =>\n                    item.Plan.Id == teamsMonthly.PasswordManager.StripeSeatPlanId);\n\n            var enterpriseMonthlyLineItem =\n                subscription.Items.Data.FirstOrDefault(item =>\n                    item.Plan.Id == enterpriseMonthly.PasswordManager.StripeSeatPlanId);\n\n            if (teamsMonthlyLineItem == null || enterpriseMonthlyLineItem == null)\n            {\n                logger.LogError(\"invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items\",\n                    parsedEvent.Id,\n                    provider.Id);\n            }\n        }\n        else if (organizationId.HasValue)\n        {\n            var organization = await organizationRepository.GetByIdAsync(organizationId.Value);\n\n            if (organization == null)\n            {\n                return;\n            }\n\n            var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n            if (subscription.Items.All(item => plan.PasswordManager.StripePlanId != item.Plan.Id))\n            {\n                return;\n            }\n\n            await organizationEnableCommand.EnableAsync(organizationId.Value, subscription.GetCurrentPeriodEnd());\n            organization = await organizationRepository.GetByIdAsync(organization.Id);\n            await pushNotificationAdapter.NotifyEnabledChangedAsync(organization!);\n        }\n        else if (userId.HasValue)\n        {\n            if (subscription.Items.All(i => i.Price.Id is not IStripeEventUtilityService.PremiumPlanId and not IStripeEventUtilityService.PremiumPlanIdAppStore))\n            {\n                return;\n            }\n\n            await userService.EnablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd());\n            var user = await userRepository.GetByIdAsync(userId.Value);\n            if (user != null)\n            {\n                await pushNotificationAdapter.NotifyPremiumStatusChangedAsync(user);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/ProviderEventService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Billing.Constants;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Stripe;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class ProviderEventService(\n    IOrganizationRepository organizationRepository,\n    IPricingClient pricingClient,\n    IProviderInvoiceItemRepository providerInvoiceItemRepository,\n    IProviderOrganizationRepository providerOrganizationRepository,\n    IProviderPlanRepository providerPlanRepository,\n    IStripeEventService stripeEventService,\n    IStripeFacade stripeFacade) : IProviderEventService\n{\n    public async Task TryRecordInvoiceLineItems(Event parsedEvent)\n    {\n        if (parsedEvent.Type is not HandledStripeWebhook.InvoiceCreated and not HandledStripeWebhook.InvoiceFinalized)\n        {\n            return;\n        }\n\n        var invoice = await stripeEventService.GetInvoice(parsedEvent, true, [\"discounts\"]);\n\n        if (invoice.Parent is not { Type: \"subscription_details\" })\n        {\n            return;\n        }\n\n        var metadata = (await stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId)).Metadata ?? new Dictionary<string, string>();\n\n        var hasProviderId = metadata.TryGetValue(\"providerId\", out var providerId);\n\n        if (!hasProviderId)\n        {\n            return;\n        }\n\n        var parsedProviderId = Guid.Parse(providerId);\n\n        switch (parsedEvent.Type)\n        {\n            case HandledStripeWebhook.InvoiceCreated:\n                {\n                    var clients =\n                        await providerOrganizationRepository.GetManyDetailsByProviderAsync(parsedProviderId);\n\n                    var providerPlans = await providerPlanRepository.GetByProviderId(parsedProviderId);\n\n                    var invoiceItems = new List<ProviderInvoiceItem>();\n\n                    foreach (var client in clients)\n                    {\n                        if (client.Status != OrganizationStatusType.Managed)\n                        {\n                            continue;\n                        }\n\n                        var organization = await organizationRepository.GetByIdAsync(client.OrganizationId);\n\n                        if (organization == null)\n                        {\n                            return;\n                        }\n\n                        var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n                        var totalPercentOff = invoice.Discounts?.Sum(discount => discount?.Coupon?.PercentOff ?? 0) ?? 0;\n\n                        var discountedPercentage = (100 - totalPercentOff) / 100;\n\n                        var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;\n\n                        invoiceItems.Add(new ProviderInvoiceItem\n                        {\n                            ProviderId = parsedProviderId,\n                            InvoiceId = invoice.Id,\n                            InvoiceNumber = invoice.Number,\n                            ClientId = client.OrganizationId,\n                            ClientName = client.OrganizationName,\n                            PlanName = client.Plan,\n                            AssignedSeats = client.Seats ?? 0,\n                            UsedSeats = client.OccupiedSeats ?? 0,\n                            Total = (client.Seats ?? 0) * discountedSeatPrice\n                        });\n                    }\n\n                    foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0))\n                    {\n                        var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);\n\n                        var clientSeats = invoiceItems\n                            .Where(item => item.PlanName == plan.Name)\n                            .Sum(item => item.AssignedSeats);\n\n                        var unassignedSeats = providerPlan.SeatMinimum - clientSeats ?? 0;\n\n                        var totalPercentOff = invoice.Discounts?.Sum(discount => discount?.Coupon?.PercentOff ?? 0) ?? 0;\n\n                        var discountedPercentage = (100 - totalPercentOff) / 100;\n\n                        var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage;\n\n                        invoiceItems.Add(new ProviderInvoiceItem\n                        {\n                            ProviderId = parsedProviderId,\n                            InvoiceId = invoice.Id,\n                            InvoiceNumber = invoice.Number,\n                            ClientName = \"Unassigned seats\",\n                            PlanName = plan.Name,\n                            AssignedSeats = unassignedSeats,\n                            UsedSeats = 0,\n                            Total = unassignedSeats * discountedSeatPrice\n                        });\n                    }\n\n                    await Task.WhenAll(invoiceItems.Select(providerInvoiceItemRepository.CreateAsync));\n\n                    break;\n                }\n            case HandledStripeWebhook.InvoiceFinalized:\n                {\n                    var invoiceItems = await providerInvoiceItemRepository.GetByInvoiceId(invoice.Id);\n\n                    if (invoiceItems.Count != 0)\n                    {\n                        await Task.WhenAll(invoiceItems.Select(invoiceItem =>\n                        {\n                            invoiceItem.InvoiceNumber = invoice.Number;\n                            return providerInvoiceItemRepository.ReplaceAsync(invoiceItem);\n                        }));\n                    }\n\n                    break;\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/PushNotificationAdapter.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.Platform.Push;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class PushNotificationAdapter(\n    IProviderUserRepository providerUserRepository,\n    IPushNotificationService pushNotificationService) : IPushNotificationAdapter\n{\n    public Task NotifyBankAccountVerifiedAsync(Organization organization) =>\n        pushNotificationService.PushAsync(new PushNotification<OrganizationBankAccountVerifiedPushNotification>\n        {\n            Type = PushType.OrganizationBankAccountVerified,\n            Target = NotificationTarget.Organization,\n            TargetId = organization.Id,\n            Payload = new OrganizationBankAccountVerifiedPushNotification\n            {\n                OrganizationId = organization.Id\n            },\n            ExcludeCurrentContext = false\n        });\n\n    public async Task NotifyBankAccountVerifiedAsync(Provider provider)\n    {\n        var providerUsers = await providerUserRepository.GetManyByProviderAsync(provider.Id);\n        var providerAdmins = providerUsers.Where(providerUser => providerUser is\n        {\n            Type: ProviderUserType.ProviderAdmin,\n            Status: ProviderUserStatusType.Confirmed,\n            UserId: not null\n        }).ToList();\n\n        if (providerAdmins.Count > 0)\n        {\n            var tasks = providerAdmins.Select(providerAdmin => pushNotificationService.PushAsync(\n                new PushNotification<ProviderBankAccountVerifiedPushNotification>\n                {\n                    Type = PushType.ProviderBankAccountVerified,\n                    Target = NotificationTarget.User,\n                    TargetId = providerAdmin.UserId!.Value,\n                    Payload = new ProviderBankAccountVerifiedPushNotification\n                    {\n                        ProviderId = provider.Id,\n                        AdminId = providerAdmin.UserId!.Value\n                    },\n                    ExcludeCurrentContext = false\n                }));\n\n            await Task.WhenAll(tasks);\n        }\n    }\n\n    public Task NotifyEnabledChangedAsync(Organization organization) =>\n        pushNotificationService.PushAsync(new PushNotification<OrganizationStatusPushNotification>\n        {\n            Type = PushType.SyncOrganizationStatusChanged,\n            Target = NotificationTarget.Organization,\n            TargetId = organization.Id,\n            Payload = new OrganizationStatusPushNotification\n            {\n                OrganizationId = organization.Id,\n                Enabled = organization.Enabled,\n            },\n            ExcludeCurrentContext = false,\n        });\n\n    public Task NotifyPremiumStatusChangedAsync(User user) =>\n        pushNotificationService.PushAsync(new PushNotification<PremiumStatusPushNotification>\n        {\n            Type = PushType.PremiumStatusChanged,\n            Target = NotificationTarget.User,\n            TargetId = user.Id,\n            Payload = new PremiumStatusPushNotification\n            {\n                UserId = user.Id,\n                Premium = user.Premium\n            },\n            ExcludeCurrentContext = false,\n        });\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/SetupIntentSucceededHandler.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Repositories;\nusing OneOf;\nusing Stripe;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class SetupIntentSucceededHandler(\n    ILogger<SetupIntentSucceededHandler> logger,\n    IOrganizationRepository organizationRepository,\n    IProviderRepository providerRepository,\n    IPushNotificationAdapter pushNotificationAdapter,\n    IStripeAdapter stripeAdapter,\n    IStripeEventService stripeEventService) : ISetupIntentSucceededHandler\n{\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        var setupIntent = await stripeEventService.GetSetupIntent(\n            parsedEvent,\n            true,\n            [\"payment_method\"]);\n\n        if (setupIntent is not\n            {\n                CustomerId: not null,\n                PaymentMethod.UsBankAccount: not null\n            })\n        {\n            logger.LogWarning(\"SetupIntent {SetupIntentId} has no customer ID or is not a US bank account\", setupIntent.Id);\n            return;\n        }\n\n        var organization = await organizationRepository.GetByGatewayCustomerIdAsync(setupIntent.CustomerId);\n        if (organization != null)\n        {\n            await SetPaymentMethodAsync(organization, setupIntent.PaymentMethod);\n            return;\n        }\n\n        var provider = await providerRepository.GetByGatewayCustomerIdAsync(setupIntent.CustomerId);\n        if (provider != null)\n        {\n            await SetPaymentMethodAsync(provider, setupIntent.PaymentMethod);\n            return;\n        }\n\n        logger.LogError(\"No organization or provider found for customer {CustomerId}\", setupIntent.CustomerId);\n    }\n\n    private async Task SetPaymentMethodAsync(\n        OneOf<Organization, Provider> subscriber,\n        PaymentMethod paymentMethod)\n    {\n        var customerId = subscriber.Match(\n            organization => organization.GatewayCustomerId,\n            provider => provider.GatewayCustomerId);\n\n        if (string.IsNullOrEmpty(customerId))\n        {\n            return;\n        }\n\n        await stripeAdapter.AttachPaymentMethodAsync(paymentMethod.Id,\n            new PaymentMethodAttachOptions { Customer = customerId });\n\n        await stripeAdapter.UpdateCustomerAsync(customerId, new CustomerUpdateOptions\n        {\n            InvoiceSettings = new CustomerInvoiceSettingsOptions\n            {\n                DefaultPaymentMethod = paymentMethod.Id\n            }\n        });\n\n        await subscriber.Match(\n            async organization => await pushNotificationAdapter.NotifyBankAccountVerifiedAsync(organization),\n            async provider => await pushNotificationAdapter.NotifyBankAccountVerifiedAsync(provider));\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/StripeEventProcessor.cs",
    "content": "﻿using Bit.Billing.Constants;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class StripeEventProcessor(\n    ILogger<StripeEventProcessor> logger,\n    ISubscriptionDeletedHandler subscriptionDeletedHandler,\n    ISubscriptionUpdatedHandler subscriptionUpdatedHandler,\n    IUpcomingInvoiceHandler upcomingInvoiceHandler,\n    IChargeSucceededHandler chargeSucceededHandler,\n    IChargeRefundedHandler chargeRefundedHandler,\n    IPaymentSucceededHandler paymentSucceededHandler,\n    IPaymentFailedHandler paymentFailedHandler,\n    IInvoiceCreatedHandler invoiceCreatedHandler,\n    IPaymentMethodAttachedHandler paymentMethodAttachedHandler,\n    ICustomerUpdatedHandler customerUpdatedHandler,\n    IInvoiceFinalizedHandler invoiceFinalizedHandler,\n    ISetupIntentSucceededHandler setupIntentSucceededHandler,\n    ICouponDeletedHandler couponDeletedHandler)\n    : IStripeEventProcessor\n{\n    public async Task ProcessEventAsync(Event parsedEvent)\n    {\n        switch (parsedEvent.Type)\n        {\n            case HandledStripeWebhook.SubscriptionDeleted:\n                await subscriptionDeletedHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.SubscriptionUpdated:\n                await subscriptionUpdatedHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.UpcomingInvoice:\n                await upcomingInvoiceHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.ChargeSucceeded:\n                await chargeSucceededHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.ChargeRefunded:\n                await chargeRefundedHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.PaymentSucceeded:\n                await paymentSucceededHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.PaymentFailed:\n                await paymentFailedHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.InvoiceCreated:\n                await invoiceCreatedHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.PaymentMethodAttached:\n                await paymentMethodAttachedHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.CustomerUpdated:\n                await customerUpdatedHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.InvoiceFinalized:\n                await invoiceFinalizedHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.SetupIntentSucceeded:\n                await setupIntentSucceededHandler.HandleAsync(parsedEvent);\n                break;\n            case HandledStripeWebhook.CouponDeleted:\n                await couponDeletedHandler.HandleAsync(parsedEvent);\n                break;\n            default:\n                logger.LogWarning(\"Unsupported event received. {EventType}\", parsedEvent.Type);\n                break;\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/StripeEventService.cs",
    "content": "﻿using Bit.Billing.Constants;\nusing Bit.Core.Settings;\nusing Stripe;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class StripeEventService(\n    GlobalSettings globalSettings,\n    IStripeFacade stripeFacade)\n    : IStripeEventService\n{\n    public async Task<Charge> GetCharge(Event stripeEvent, bool fresh = false, List<string>? expand = null)\n    {\n        var charge = Extract<Charge>(stripeEvent);\n\n        if (!fresh)\n        {\n            return charge;\n        }\n\n        return await stripeFacade.GetCharge(charge.Id, new ChargeGetOptions { Expand = expand });\n    }\n\n    public async Task<Customer> GetCustomer(Event stripeEvent, bool fresh = false, List<string>? expand = null)\n    {\n        var customer = Extract<Customer>(stripeEvent);\n\n        if (!fresh)\n        {\n            return customer;\n        }\n\n        return await stripeFacade.GetCustomer(customer.Id, new CustomerGetOptions { Expand = expand });\n    }\n\n    public async Task<Invoice> GetInvoice(Event stripeEvent, bool fresh = false, List<string>? expand = null)\n    {\n        var invoice = Extract<Invoice>(stripeEvent);\n\n        if (!fresh)\n        {\n            return invoice;\n        }\n\n        return await stripeFacade.GetInvoice(invoice.Id, new InvoiceGetOptions { Expand = expand });\n    }\n\n    public async Task<PaymentMethod> GetPaymentMethod(Event stripeEvent, bool fresh = false,\n        List<string>? expand = null)\n    {\n        var paymentMethod = Extract<PaymentMethod>(stripeEvent);\n\n        if (!fresh)\n        {\n            return paymentMethod;\n        }\n\n        return await stripeFacade.GetPaymentMethod(paymentMethod.Id, new PaymentMethodGetOptions { Expand = expand });\n    }\n\n    public async Task<SetupIntent> GetSetupIntent(Event stripeEvent, bool fresh = false, List<string>? expand = null)\n    {\n        var setupIntent = Extract<SetupIntent>(stripeEvent);\n\n        if (!fresh)\n        {\n            return setupIntent;\n        }\n\n        return await stripeFacade.GetSetupIntent(setupIntent.Id, new SetupIntentGetOptions { Expand = expand });\n    }\n\n    public async Task<Subscription> GetSubscription(Event stripeEvent, bool fresh = false, List<string>? expand = null)\n    {\n        var subscription = Extract<Subscription>(stripeEvent);\n\n        if (!fresh)\n        {\n            return subscription;\n        }\n\n        return await stripeFacade.GetSubscription(subscription.Id, new SubscriptionGetOptions { Expand = expand });\n    }\n\n    public async Task<bool> ValidateCloudRegion(Event stripeEvent)\n    {\n        if (EventTypeAppliesToAllRegions(stripeEvent.Type))\n        {\n            return true;\n        }\n\n        var serverRegion = globalSettings.BaseServiceUri.CloudRegion;\n\n        var customerExpansion = new List<string> { \"customer\" };\n\n        var customerMetadata = stripeEvent.Type switch\n        {\n            HandledStripeWebhook.SubscriptionDeleted or HandledStripeWebhook.SubscriptionUpdated =>\n                (await GetSubscription(stripeEvent, true, customerExpansion)).Customer?.Metadata,\n\n            HandledStripeWebhook.ChargeSucceeded or HandledStripeWebhook.ChargeRefunded =>\n                (await GetCharge(stripeEvent, true, customerExpansion)).Customer?.Metadata,\n\n            HandledStripeWebhook.UpcomingInvoice =>\n                await GetCustomerMetadataFromUpcomingInvoiceEvent(stripeEvent),\n\n            HandledStripeWebhook.PaymentSucceeded or HandledStripeWebhook.PaymentFailed\n                or HandledStripeWebhook.InvoiceCreated or HandledStripeWebhook.InvoiceFinalized =>\n                (await GetInvoice(stripeEvent, true, customerExpansion)).Customer?.Metadata,\n\n            HandledStripeWebhook.PaymentMethodAttached =>\n                (await GetPaymentMethod(stripeEvent, true, customerExpansion)).Customer?.Metadata,\n\n            HandledStripeWebhook.CustomerUpdated =>\n                (await GetCustomer(stripeEvent, true)).Metadata,\n\n            HandledStripeWebhook.SetupIntentSucceeded =>\n                (await GetSetupIntent(stripeEvent, true, customerExpansion)).Customer?.Metadata,\n\n            _ => null\n        };\n\n        if (customerMetadata == null)\n        {\n            return false;\n        }\n\n        var customerRegion = GetCustomerRegion(customerMetadata);\n\n        return customerRegion == serverRegion;\n\n        /* In Stripe, when we receive an invoice.upcoming event, the event does not include an Invoice ID because\n           the invoice hasn't been created yet. Therefore, rather than retrieving the fresh Invoice with a 'customer'\n           expansion, we need to use the Customer ID on the event to retrieve the metadata. */\n        async Task<Dictionary<string, string>?> GetCustomerMetadataFromUpcomingInvoiceEvent(Event localStripeEvent)\n        {\n            var invoice = await GetInvoice(localStripeEvent);\n\n            var customer = !string.IsNullOrEmpty(invoice.CustomerId)\n                ? await stripeFacade.GetCustomer(invoice.CustomerId)\n                : null;\n\n            return customer?.Metadata;\n        }\n    }\n\n    /// <summary>\n    /// Returns true for event types that should be processed by all cloud regions.\n    /// </summary>\n    private static bool EventTypeAppliesToAllRegions(string eventType) => eventType switch\n    {\n        // Business rules say that coupons are allowed to be imported into multiple regions, so coupon deleted events are not region-segmented\n        HandledStripeWebhook.CouponDeleted => true,\n        _ => false\n    };\n\n    private static T Extract<T>(Event stripeEvent)\n        => stripeEvent.Data.Object is not T type\n            ? throw new Exception(\n                $\"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{typeof(T).Name}'\")\n            : type;\n\n    private static string GetCustomerRegion(IDictionary<string, string> customerMetadata)\n    {\n        const string defaultRegion = Core.Constants.CountryAbbreviations.UnitedStates;\n\n        if (customerMetadata.TryGetValue(\"region\", out var value))\n        {\n            return value;\n        }\n\n        var incorrectlyCasedRegionKey = customerMetadata.Keys\n            .FirstOrDefault(key => key.Equals(\"region\", StringComparison.OrdinalIgnoreCase));\n\n        if (incorrectlyCasedRegionKey is null)\n        {\n            return defaultRegion;\n        }\n\n        _ = customerMetadata.TryGetValue(incorrectlyCasedRegionKey, out var regionValue);\n\n        return !string.IsNullOrWhiteSpace(regionValue)\n            ? regionValue\n            : defaultRegion;\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/StripeEventUtilityService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Billing.Constants;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Braintree;\nusing Stripe;\nusing Customer = Stripe.Customer;\nusing Subscription = Stripe.Subscription;\nusing Transaction = Bit.Core.Entities.Transaction;\nusing TransactionType = Bit.Core.Enums.TransactionType;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class StripeEventUtilityService : IStripeEventUtilityService\n{\n    private readonly IStripeFacade _stripeFacade;\n    private readonly ILogger<StripeEventUtilityService> _logger;\n    private readonly ITransactionRepository _transactionRepository;\n    private readonly IMailService _mailService;\n    private readonly BraintreeGateway _btGateway;\n    private readonly GlobalSettings _globalSettings;\n\n    public StripeEventUtilityService(\n        IStripeFacade stripeFacade,\n        ILogger<StripeEventUtilityService> logger,\n        ITransactionRepository transactionRepository,\n        IMailService mailService,\n        GlobalSettings globalSettings)\n    {\n        _stripeFacade = stripeFacade;\n        _logger = logger;\n        _transactionRepository = transactionRepository;\n        _mailService = mailService;\n        _btGateway = new BraintreeGateway\n        {\n            Environment = globalSettings.Braintree.Production ?\n                Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,\n            MerchantId = globalSettings.Braintree.MerchantId,\n            PublicKey = globalSettings.Braintree.PublicKey,\n            PrivateKey = globalSettings.Braintree.PrivateKey\n        };\n        _globalSettings = globalSettings;\n    }\n\n    /// <summary>\n    /// Gets the organizationId, userId, or providerId from the metadata of a Stripe Subscription object.\n    /// </summary>\n    /// <param name=\"metadata\"></param>\n    /// <returns></returns>\n    public Tuple<Guid?, Guid?, Guid?> GetIdsFromMetadata(Dictionary<string, string> metadata)\n    {\n        if (metadata == null || metadata.Count == 0)\n        {\n            return new Tuple<Guid?, Guid?, Guid?>(null, null, null);\n        }\n\n        metadata.TryGetValue(\"organizationId\", out var orgIdString);\n        metadata.TryGetValue(\"userId\", out var userIdString);\n        metadata.TryGetValue(\"providerId\", out var providerIdString);\n\n        orgIdString ??= metadata.FirstOrDefault(x =>\n            x.Key.Equals(\"organizationId\", StringComparison.OrdinalIgnoreCase)).Value;\n\n        userIdString ??= metadata.FirstOrDefault(x =>\n            x.Key.Equals(\"userId\", StringComparison.OrdinalIgnoreCase)).Value;\n\n        providerIdString ??= metadata.FirstOrDefault(x =>\n            x.Key.Equals(\"providerId\", StringComparison.OrdinalIgnoreCase)).Value;\n\n        Guid? organizationId = string.IsNullOrWhiteSpace(orgIdString) ? null : new Guid(orgIdString);\n        Guid? userId = string.IsNullOrWhiteSpace(userIdString) ? null : new Guid(userIdString);\n        Guid? providerId = string.IsNullOrWhiteSpace(providerIdString) ? null : new Guid(providerIdString);\n\n        return new Tuple<Guid?, Guid?, Guid?>(organizationId, userId, providerId);\n    }\n\n    /// <summary>\n    /// Gets the organization or user ID from the metadata of a Stripe Charge object.\n    /// </summary>\n    /// <param name=\"charge\"></param>\n    /// <returns></returns>\n    public async Task<(Guid?, Guid?, Guid?)> GetEntityIdsFromChargeAsync(Charge charge)\n    {\n        var subscriptions = await _stripeFacade.ListSubscriptions(new SubscriptionListOptions\n        {\n            Customer = charge.CustomerId\n        });\n\n        foreach (var subscription in subscriptions)\n        {\n            if (subscription.Status is StripeSubscriptionStatus.Canceled or StripeSubscriptionStatus.IncompleteExpired)\n            {\n                continue;\n            }\n\n            var (organizationId, userId, providerId) = GetIdsFromMetadata(subscription.Metadata);\n\n            if (organizationId.HasValue || userId.HasValue || providerId.HasValue)\n            {\n                return (organizationId, userId, providerId);\n            }\n        }\n\n        return (null, null, null);\n    }\n\n    public bool IsSponsoredSubscription(Subscription subscription) =>\n        SponsoredPlans.All\n            .Any(p => subscription.Items\n                .Any(i => i.Plan.Id == p.StripePlanId));\n\n    /// <summary>\n    /// Converts a Stripe Charge object to a Bitwarden Transaction object.\n    /// </summary>\n    /// <param name=\"charge\"></param>\n    /// <param name=\"organizationId\"></param>\n    /// <param name=\"userId\"></param>\n    /// /// <param name=\"providerId\"></param>\n    /// <returns></returns>\n    public async Task<Transaction> FromChargeToTransactionAsync(Charge charge, Guid? organizationId, Guid? userId, Guid? providerId)\n    {\n        var transaction = new Transaction\n        {\n            Amount = charge.Amount / 100M,\n            CreationDate = charge.Created,\n            OrganizationId = organizationId,\n            UserId = userId,\n            ProviderId = providerId,\n            Type = TransactionType.Charge,\n            Gateway = GatewayType.Stripe,\n            GatewayId = charge.Id\n        };\n\n        switch (charge.Source)\n        {\n            case Card card:\n                {\n                    transaction.PaymentMethodType = PaymentMethodType.Card;\n                    transaction.Details = $\"{card.Brand}, *{card.Last4}\";\n                    break;\n                }\n            case BankAccount bankAccount:\n                {\n                    transaction.PaymentMethodType = PaymentMethodType.BankAccount;\n                    transaction.Details = $\"{bankAccount.BankName}, *{bankAccount.Last4}\";\n                    break;\n                }\n            case Source { Card: not null } source:\n                {\n                    transaction.PaymentMethodType = PaymentMethodType.Card;\n                    transaction.Details = $\"{source.Card.Brand}, *{source.Card.Last4}\";\n                    break;\n                }\n            case Source { AchDebit: not null } source:\n                {\n                    transaction.PaymentMethodType = PaymentMethodType.BankAccount;\n                    transaction.Details = $\"{source.AchDebit.BankName}, *{source.AchDebit.Last4}\";\n                    break;\n                }\n            case Source source:\n                {\n                    if (source.AchCreditTransfer == null)\n                    {\n                        break;\n                    }\n\n                    var achCreditTransfer = source.AchCreditTransfer;\n\n                    transaction.PaymentMethodType = PaymentMethodType.BankAccount;\n                    transaction.Details = $\"ACH => {achCreditTransfer.BankName}, {achCreditTransfer.AccountNumber}\";\n\n                    break;\n                }\n            default:\n                {\n                    if (charge.PaymentMethodDetails == null)\n                    {\n                        break;\n                    }\n\n                    if (charge.PaymentMethodDetails.Card != null)\n                    {\n                        var card = charge.PaymentMethodDetails.Card;\n                        transaction.PaymentMethodType = PaymentMethodType.Card;\n                        transaction.Details = $\"{card.Brand?.ToUpperInvariant()}, *{card.Last4}\";\n                    }\n                    else if (charge.PaymentMethodDetails.UsBankAccount != null)\n                    {\n                        var usBankAccount = charge.PaymentMethodDetails.UsBankAccount;\n                        transaction.PaymentMethodType = PaymentMethodType.BankAccount;\n                        transaction.Details = $\"{usBankAccount.BankName}, *{usBankAccount.Last4}\";\n                    }\n                    else if (charge.PaymentMethodDetails.AchDebit != null)\n                    {\n                        var achDebit = charge.PaymentMethodDetails.AchDebit;\n                        transaction.PaymentMethodType = PaymentMethodType.BankAccount;\n                        transaction.Details = $\"{achDebit.BankName}, *{achDebit.Last4}\";\n                    }\n                    else if (charge.PaymentMethodDetails.AchCreditTransfer != null)\n                    {\n                        var achCreditTransfer = charge.PaymentMethodDetails.AchCreditTransfer;\n                        transaction.PaymentMethodType = PaymentMethodType.BankAccount;\n                        transaction.Details = $\"ACH => {achCreditTransfer.BankName}, {achCreditTransfer.AccountNumber}\";\n                    }\n                    else if (charge.PaymentMethodDetails.CustomerBalance != null)\n                    {\n                        var bankTransferType = await GetFundingBankTransferTypeAsync(charge);\n\n                        if (!string.IsNullOrEmpty(bankTransferType))\n                        {\n                            transaction.PaymentMethodType = PaymentMethodType.BankAccount;\n                            transaction.Details = bankTransferType switch\n                            {\n                                \"eu_bank_transfer\" => \"EU Bank Transfer\",\n                                \"gb_bank_transfer\" => \"GB Bank Transfer\",\n                                \"jp_bank_transfer\" => \"JP Bank Transfer\",\n                                \"mx_bank_transfer\" => \"MX Bank Transfer\",\n                                \"us_bank_transfer\" => \"US Bank Transfer\",\n                                _ => \"Bank Transfer\"\n                            };\n                        }\n                    }\n\n                    break;\n                }\n        }\n\n        return transaction;\n    }\n\n    public async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false)\n    {\n        var customer = await _stripeFacade.GetCustomer(invoice.CustomerId);\n\n        if (customer?.Metadata?.ContainsKey(\"btCustomerId\") ?? false)\n        {\n            return await AttemptToPayInvoiceWithBraintreeAsync(invoice, customer);\n        }\n\n        if (attemptToPayWithStripe)\n        {\n            return await AttemptToPayInvoiceWithStripeAsync(invoice);\n        }\n\n        return false;\n    }\n\n    public bool ShouldAttemptToPayInvoice(Invoice invoice) =>\n        invoice is\n        {\n            AmountDue: > 0,\n            Status: not StripeConstants.InvoiceStatus.Paid,\n            CollectionMethod: \"charge_automatically\",\n            BillingReason: \"subscription_cycle\" or \"automatic_pending_invoice_item_invoice\",\n            Parent.SubscriptionDetails: not null\n        };\n\n    private async Task<bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer)\n    {\n        _logger.LogDebug(\"Attempting to pay invoice with Braintree\");\n        if (!customer?.Metadata?.ContainsKey(\"btCustomerId\") ?? true)\n        {\n            _logger.LogWarning(\n                \"Attempted to pay invoice with Braintree but btCustomerId wasn't on Stripe customer metadata\");\n            return false;\n        }\n\n        if (invoice.Parent?.SubscriptionDetails == null)\n        {\n            _logger.LogWarning(\"Invoice parent was not a subscription.\");\n            return false;\n        }\n\n        var subscription = await _stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId);\n        var (organizationId, userId, providerId) = GetIdsFromMetadata(subscription?.Metadata);\n        if (!organizationId.HasValue && !userId.HasValue && !providerId.HasValue)\n        {\n            _logger.LogWarning(\n                \"Attempted to pay invoice with Braintree but Stripe subscription metadata didn't contain either a organizationId or userId or \");\n            return false;\n        }\n\n        var orgTransaction = organizationId.HasValue;\n        string btObjIdField;\n        Guid btObjId;\n        if (organizationId.HasValue)\n        {\n            btObjIdField = \"organization_id\";\n            btObjId = organizationId.Value;\n        }\n        else if (userId.HasValue)\n        {\n            btObjIdField = \"user_id\";\n            btObjId = userId.Value;\n        }\n        else\n        {\n            btObjIdField = \"provider_id\";\n            btObjId = providerId.Value;\n        }\n        var btInvoiceAmount = Math.Round(invoice.AmountDue / 100M, 2);\n\n        // Check if this invoice already has a Braintree transaction ID to prevent duplicate charges\n        if (invoice.Metadata?.ContainsKey(\"btTransactionId\") ?? false)\n        {\n            _logger.LogWarning(\"Invoice {InvoiceId} already has a Braintree transaction ({TransactionId}). \" +\n                \"Do not charge again to prevent duplicate.\",\n                invoice.Id,\n                invoice.Metadata[\"btTransactionId\"]);\n            return false;\n        }\n\n        Result<Braintree.Transaction> transactionResult;\n        try\n        {\n            var transactionRequest = new Braintree.TransactionRequest\n            {\n                Amount = btInvoiceAmount,\n                CustomerId = customer.Metadata[\"btCustomerId\"],\n                Options = new Braintree.TransactionOptionsRequest\n                {\n                    SubmitForSettlement = true,\n                    PayPal = new Braintree.TransactionOptionsPayPalRequest\n                    {\n                        CustomField =\n                            $\"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}\"\n                    }\n                },\n                CustomFields = new Dictionary<string, string>\n                {\n                    [btObjIdField] = btObjId.ToString(),\n                    [\"region\"] = _globalSettings.BaseServiceUri.CloudRegion\n                }\n            };\n\n            _logger.LogInformation(\"Creating Braintree transaction with Amount: {Amount}, CustomerId: {CustomerId}, \" +\n                \"CustomField: {CustomField}, CustomFields: {@CustomFields}\",\n                transactionRequest.Amount,\n                transactionRequest.CustomerId,\n                transactionRequest.Options.PayPal.CustomField,\n                transactionRequest.CustomFields);\n\n            transactionResult = await _btGateway.Transaction.SaleAsync(transactionRequest);\n        }\n        catch (NotFoundException e)\n        {\n            _logger.LogError(e,\n                \"Attempted to make a payment with Braintree, but customer did not exist for the given btCustomerId present on the Stripe metadata\");\n            throw;\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(e, \"Exception occurred while trying to pay invoice with Braintree\");\n            throw;\n        }\n\n        if (!transactionResult.IsSuccess())\n        {\n            _logger.LogWarning(\"Braintree transaction failed. Error: {ErrorMessage}, Transaction Status: {Status}, Validation Errors: {ValidationErrors}\",\n                transactionResult.Message,\n                transactionResult.Target?.Status,\n                string.Join(\", \", transactionResult.Errors.DeepAll().Select(e => $\"Code: {e.Code}, Message: {e.Message}, Attribute: {e.Attribute}\")));\n\n            if (invoice.AttemptCount < 4)\n            {\n                await _mailService.SendPaymentFailedAsync(customer.Email, btInvoiceAmount, true);\n            }\n            return false;\n        }\n\n        try\n        {\n            await _stripeFacade.UpdateInvoice(invoice.Id, new InvoiceUpdateOptions\n            {\n                Metadata = new Dictionary<string, string>\n                {\n                    [\"btTransactionId\"] = transactionResult.Target.Id,\n                    [\"btPayPalTransactionId\"] =\n                        transactionResult.Target.PayPalDetails?.AuthorizationId\n                }\n            });\n            await _stripeFacade.PayInvoice(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true });\n        }\n        catch (Exception e)\n        {\n            await _btGateway.Transaction.RefundAsync(transactionResult.Target.Id);\n            if (e.Message.Contains(\"Invoice is already paid\"))\n            {\n                await _stripeFacade.UpdateInvoice(invoice.Id, new InvoiceUpdateOptions\n                {\n                    Metadata = invoice.Metadata\n                });\n            }\n            else\n            {\n                throw;\n            }\n        }\n\n        return true;\n    }\n\n    private async Task<bool> AttemptToPayInvoiceWithStripeAsync(Invoice invoice)\n    {\n        try\n        {\n            await _stripeFacade.PayInvoice(invoice.Id);\n            return true;\n        }\n        catch (Exception e)\n        {\n            _logger.LogWarning(\n                e,\n                \"Exception occurred while trying to pay Stripe invoice with Id: {InvoiceId}\",\n                invoice.Id);\n\n            throw;\n        }\n    }\n\n    /// <summary>\n    /// Retrieves the bank transfer type that funded a charge paid via customer balance.\n    /// </summary>\n    /// <param name=\"charge\">The charge to analyze.</param>\n    /// <returns>\n    /// The bank transfer type (e.g., \"us_bank_transfer\", \"eu_bank_transfer\") if the charge was funded\n    /// by a bank transfer via customer balance, otherwise null.\n    /// </returns>\n    private async Task<string> GetFundingBankTransferTypeAsync(Charge charge)\n    {\n        if (charge is not\n            {\n                CustomerId: not null,\n                PaymentIntentId: not null,\n                PaymentMethodDetails: { Type: \"customer_balance\" }\n            })\n        {\n            return null;\n        }\n\n        var cashBalanceTransactions = _stripeFacade.GetCustomerCashBalanceTransactions(charge.CustomerId);\n\n        string bankTransferType = null;\n        var matchingPaymentIntentFound = false;\n\n        await foreach (var cashBalanceTransaction in cashBalanceTransactions)\n        {\n            switch (cashBalanceTransaction)\n            {\n                case { Type: \"funded\", Funded: not null }:\n                    {\n                        bankTransferType = cashBalanceTransaction.Funded.BankTransfer.Type;\n                        break;\n                    }\n                case { Type: \"applied_to_payment\", AppliedToPayment: not null }\n                    when cashBalanceTransaction.AppliedToPayment.PaymentIntentId == charge.PaymentIntentId:\n                    {\n                        matchingPaymentIntentFound = true;\n                        break;\n                    }\n            }\n\n            if (matchingPaymentIntentFound && !string.IsNullOrEmpty(bankTransferType))\n            {\n                return bankTransferType;\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/StripeFacade.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Stripe;\nusing Stripe.TestHelpers;\nusing CustomerService = Stripe.CustomerService;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class StripeFacade : IStripeFacade\n{\n    private readonly ChargeService _chargeService = new();\n    private readonly CustomerService _customerService = new();\n    private readonly CustomerCashBalanceTransactionService _customerCashBalanceTransactionService = new();\n    private readonly EventService _eventService = new();\n    private readonly InvoiceService _invoiceService = new();\n    private readonly PaymentMethodService _paymentMethodService = new();\n    private readonly SubscriptionService _subscriptionService = new();\n    private readonly DiscountService _discountService = new();\n    private readonly SetupIntentService _setupIntentService = new();\n    private readonly TestClockService _testClockService = new();\n    private readonly CouponService _couponService = new();\n\n    public async Task<Charge> GetCharge(\n        string chargeId,\n        ChargeGetOptions chargeGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _chargeService.GetAsync(chargeId, chargeGetOptions, requestOptions, cancellationToken);\n\n    public async Task<Event> GetEvent(\n        string eventId,\n        EventGetOptions eventGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _eventService.GetAsync(eventId, eventGetOptions, requestOptions, cancellationToken);\n\n    public async Task<Customer> GetCustomer(\n        string customerId,\n        CustomerGetOptions customerGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _customerService.GetAsync(customerId, customerGetOptions, requestOptions, cancellationToken);\n\n    public IAsyncEnumerable<CustomerCashBalanceTransaction> GetCustomerCashBalanceTransactions(\n        string customerId,\n        CustomerCashBalanceTransactionListOptions customerCashBalanceTransactionListOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default)\n        => _customerCashBalanceTransactionService.ListAutoPagingAsync(customerId, customerCashBalanceTransactionListOptions, requestOptions, cancellationToken);\n\n    public async Task<Customer> UpdateCustomer(\n        string customerId,\n        CustomerUpdateOptions customerUpdateOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _customerService.UpdateAsync(customerId, customerUpdateOptions, requestOptions, cancellationToken);\n\n    public async Task<Invoice> GetInvoice(\n        string invoiceId,\n        InvoiceGetOptions invoiceGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _invoiceService.GetAsync(invoiceId, invoiceGetOptions, requestOptions, cancellationToken);\n\n    public async Task<SetupIntent> GetSetupIntent(\n        string setupIntentId,\n        SetupIntentGetOptions setupIntentGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _setupIntentService.GetAsync(setupIntentId, setupIntentGetOptions, requestOptions, cancellationToken);\n\n    public async Task<StripeList<Invoice>> ListInvoices(\n        InvoiceListOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _invoiceService.ListAsync(options, requestOptions, cancellationToken);\n\n    public async Task<Invoice> UpdateInvoice(\n        string invoiceId,\n        InvoiceUpdateOptions invoiceGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _invoiceService.UpdateAsync(invoiceId, invoiceGetOptions, requestOptions, cancellationToken);\n\n    public async Task<Invoice> PayInvoice(string invoiceId, InvoicePayOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _invoiceService.PayAsync(invoiceId, options, requestOptions, cancellationToken);\n\n    public async Task<Invoice> VoidInvoice(\n        string invoiceId,\n        InvoiceVoidOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _invoiceService.VoidInvoiceAsync(invoiceId, options, requestOptions, cancellationToken);\n\n    public async Task<PaymentMethod> GetPaymentMethod(\n        string paymentMethodId,\n        PaymentMethodGetOptions paymentMethodGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _paymentMethodService.GetAsync(paymentMethodId, paymentMethodGetOptions, requestOptions, cancellationToken);\n\n    public async Task<StripeList<Subscription>> ListSubscriptions(SubscriptionListOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _subscriptionService.ListAsync(options, requestOptions, cancellationToken);\n\n    public IAsyncEnumerable<Subscription> ListSubscriptionsAutoPagingAsync(\n        SubscriptionListOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        _subscriptionService.ListAutoPagingAsync(options, requestOptions, cancellationToken);\n\n    public async Task<Subscription> GetSubscription(\n        string subscriptionId,\n        SubscriptionGetOptions subscriptionGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _subscriptionService.GetAsync(subscriptionId, subscriptionGetOptions, requestOptions, cancellationToken);\n\n    public async Task<Subscription> UpdateSubscription(\n        string subscriptionId,\n        SubscriptionUpdateOptions subscriptionUpdateOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _subscriptionService.UpdateAsync(subscriptionId, subscriptionUpdateOptions, requestOptions, cancellationToken);\n\n    public async Task<Subscription> CancelSubscription(\n        string subscriptionId,\n        SubscriptionCancelOptions options = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _subscriptionService.CancelAsync(subscriptionId, options, requestOptions, cancellationToken);\n\n    public async Task<Discount> DeleteCustomerDiscount(\n        string customerId,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _discountService.DeleteCustomerDiscountAsync(customerId, requestOptions, cancellationToken);\n\n    public async Task<Discount> DeleteSubscriptionDiscount(\n        string subscriptionId,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        await _discountService.DeleteSubscriptionDiscountAsync(subscriptionId, requestOptions, cancellationToken);\n\n    public Task<TestClock> GetTestClock(\n        string testClockId,\n        TestClockGetOptions testClockGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        _testClockService.GetAsync(testClockId, testClockGetOptions, requestOptions, cancellationToken);\n\n    public Task<Coupon> GetCoupon(\n        string couponId,\n        CouponGetOptions couponGetOptions = null,\n        RequestOptions requestOptions = null,\n        CancellationToken cancellationToken = default) =>\n        _couponService.GetAsync(couponId, couponGetOptions, requestOptions, cancellationToken);\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs",
    "content": "﻿using Bit.Billing.Constants;\nusing Bit.Billing.Jobs;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Quartz;\nusing Event = Stripe.Event;\nnamespace Bit.Billing.Services.Implementations;\n\npublic class SubscriptionDeletedHandler : ISubscriptionDeletedHandler\n{\n    private readonly IStripeEventService _stripeEventService;\n    private readonly IUserService _userService;\n    private readonly IUserRepository _userRepository;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n    private readonly IOrganizationDisableCommand _organizationDisableCommand;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderService _providerService;\n    private readonly ISchedulerFactory _schedulerFactory;\n    private readonly IPushNotificationAdapter _pushNotificationAdapter;\n\n    public SubscriptionDeletedHandler(\n        IStripeEventService stripeEventService,\n        IUserService userService,\n        IUserRepository userRepository,\n        IStripeEventUtilityService stripeEventUtilityService,\n        IOrganizationDisableCommand organizationDisableCommand,\n        IProviderRepository providerRepository,\n        IProviderService providerService,\n        ISchedulerFactory schedulerFactory,\n        IPushNotificationAdapter pushNotificationAdapter)\n    {\n        _stripeEventService = stripeEventService;\n        _userService = userService;\n        _userRepository = userRepository;\n        _stripeEventUtilityService = stripeEventUtilityService;\n        _organizationDisableCommand = organizationDisableCommand;\n        _providerRepository = providerRepository;\n        _providerService = providerService;\n        _schedulerFactory = schedulerFactory;\n        _pushNotificationAdapter = pushNotificationAdapter;\n    }\n\n    /// <summary>\n    /// Handles the <see cref=\"HandledStripeWebhook.SubscriptionDeleted\"/> event type from Stripe.\n    /// </summary>\n    /// <param name=\"parsedEvent\"></param>\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        var subscription = await _stripeEventService.GetSubscription(parsedEvent, true);\n        var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);\n        var subCanceled = subscription.Status == StripeSubscriptionStatus.Canceled;\n\n        const string providerMigrationCancellationComment = \"Cancelled as part of provider migration to Consolidated Billing\";\n        const string addedToProviderCancellationComment = \"Organization was added to Provider\";\n\n        if (!subCanceled)\n        {\n            return;\n        }\n\n        if (organizationId.HasValue)\n        {\n            if (!string.IsNullOrEmpty(subscription.CancellationDetails?.Comment) &&\n                (subscription.CancellationDetails.Comment == providerMigrationCancellationComment ||\n                 subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment)))\n            {\n                return;\n            }\n\n            await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.GetCurrentPeriodEnd());\n        }\n        else if (providerId.HasValue)\n        {\n            var provider = await _providerRepository.GetByIdAsync(providerId.Value);\n            if (provider != null)\n            {\n                provider.Enabled = false;\n                await _providerService.UpdateAsync(provider);\n\n                await QueueProviderOrganizationDisableJobAsync(providerId.Value, subscription.GetCurrentPeriodEnd());\n            }\n        }\n        else if (userId.HasValue)\n        {\n            await _userService.DisablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd());\n            var user = await _userRepository.GetByIdAsync(userId.Value);\n            if (user != null)\n            {\n                await _pushNotificationAdapter.NotifyPremiumStatusChangedAsync(user!);\n            }\n        }\n    }\n\n    private async Task QueueProviderOrganizationDisableJobAsync(Guid providerId, DateTime? expirationDate)\n    {\n        var scheduler = await _schedulerFactory.GetScheduler();\n\n        var job = JobBuilder.Create<ProviderOrganizationDisableJob>()\n            .WithIdentity($\"disable-provider-orgs-{providerId}\", \"provider-management\")\n            .UsingJobData(\"providerId\", providerId.ToString())\n            .UsingJobData(\"expirationDate\", expirationDate?.ToString(\"O\"))\n            .Build();\n\n        var trigger = TriggerBuilder.Create()\n            .WithIdentity($\"disable-trigger-{providerId}\", \"provider-management\")\n            .StartNow()\n            .Build();\n\n        await scheduler.ScheduleJob(job, trigger);\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Subscriptions.Models;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Stripe;\nusing Stripe.TestHelpers;\nusing static Bit.Core.Billing.Constants.StripeConstants;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Services.Implementations;\n\npublic class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler\n{\n    private readonly IStripeEventService _stripeEventService;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n    private readonly IOrganizationService _organizationService;\n    private readonly IStripeFacade _stripeFacade;\n    private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand;\n    private readonly IUserService _userService;\n    private readonly IUserRepository _userRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationEnableCommand _organizationEnableCommand;\n    private readonly IOrganizationDisableCommand _organizationDisableCommand;\n    private readonly IPricingClient _pricingClient;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderService _providerService;\n    private readonly IPushNotificationAdapter _pushNotificationAdapter;\n\n    public SubscriptionUpdatedHandler(\n        IStripeEventService stripeEventService,\n        IStripeEventUtilityService stripeEventUtilityService,\n        IOrganizationService organizationService,\n        IStripeFacade stripeFacade,\n        IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand,\n        IUserService userService,\n        IUserRepository userRepository,\n        IOrganizationRepository organizationRepository,\n        IOrganizationEnableCommand organizationEnableCommand,\n        IOrganizationDisableCommand organizationDisableCommand,\n        IPricingClient pricingClient,\n        IProviderRepository providerRepository,\n        IProviderService providerService,\n        IPushNotificationAdapter pushNotificationAdapter)\n    {\n        _stripeEventService = stripeEventService;\n        _stripeEventUtilityService = stripeEventUtilityService;\n        _organizationService = organizationService;\n        _providerService = providerService;\n        _stripeFacade = stripeFacade;\n        _organizationSponsorshipRenewCommand = organizationSponsorshipRenewCommand;\n        _userService = userService;\n        _userRepository = userRepository;\n        _organizationRepository = organizationRepository;\n        _providerRepository = providerRepository;\n        _organizationEnableCommand = organizationEnableCommand;\n        _organizationDisableCommand = organizationDisableCommand;\n        _pricingClient = pricingClient;\n        _providerRepository = providerRepository;\n        _providerService = providerService;\n        _pushNotificationAdapter = pushNotificationAdapter;\n    }\n\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, [\"customer\", \"discounts\", \"latest_invoice\", \"test_clock\"]);\n        SubscriberId subscriberId = subscription;\n\n        var currentPeriodEnd = subscription.GetCurrentPeriodEnd();\n\n        if (SubscriptionWentUnpaid(parsedEvent, subscription) ||\n            SubscriptionWentIncompleteExpired(parsedEvent, subscription))\n        {\n            await DisableSubscriberAsync(subscriberId, currentPeriodEnd);\n            await SetSubscriptionToCancelAsync(subscription);\n        }\n        else if (SubscriptionBecameActive(parsedEvent, subscription))\n        {\n            await EnableSubscriberAsync(subscriberId, currentPeriodEnd);\n            await RemovePendingCancellationAsync(subscription);\n        }\n\n        await subscriberId.Match(\n            userId => _userService.UpdatePremiumExpirationAsync(userId.Value, currentPeriodEnd),\n            async organizationId =>\n            {\n                await _organizationService.UpdateExpirationDateAsync(organizationId.Value, currentPeriodEnd);\n\n                if (_stripeEventUtilityService.IsSponsoredSubscription(subscription) && currentPeriodEnd.HasValue)\n                {\n                    await _organizationSponsorshipRenewCommand.UpdateExpirationDateAsync(organizationId.Value, currentPeriodEnd.Value);\n                }\n\n                await RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(parsedEvent, subscription);\n            },\n            _ => Task.CompletedTask);\n    }\n\n    private static bool SubscriptionWentUnpaid(\n        Event parsedEvent,\n        Subscription currentSubscription) =>\n        parsedEvent.Data.PreviousAttributes.ToObject<Subscription>() is Subscription\n        {\n            Status:\n            SubscriptionStatus.Trialing or\n            SubscriptionStatus.Active or\n            SubscriptionStatus.PastDue\n        } && currentSubscription is\n        {\n            Status: SubscriptionStatus.Unpaid,\n            LatestInvoice.BillingReason: BillingReasons.SubscriptionCreate or BillingReasons.SubscriptionCycle\n        };\n\n    private static bool SubscriptionWentIncompleteExpired(\n        Event parsedEvent,\n        Subscription currentSubscription) =>\n        parsedEvent.Data.PreviousAttributes.ToObject<Subscription>() is Subscription\n        {\n            Status: SubscriptionStatus.Incomplete\n        } && currentSubscription is\n        {\n            Status: SubscriptionStatus.IncompleteExpired,\n            LatestInvoice.BillingReason: BillingReasons.SubscriptionCreate or BillingReasons.SubscriptionCycle\n        };\n\n    private static bool SubscriptionBecameActive(\n        Event parsedEvent,\n        Subscription currentSubscription) =>\n        parsedEvent.Data.PreviousAttributes.ToObject<Subscription>() is Subscription\n        {\n            Status:\n            SubscriptionStatus.Incomplete or\n            SubscriptionStatus.Unpaid\n        } && currentSubscription is\n        {\n            Status: SubscriptionStatus.Active,\n            LatestInvoice.BillingReason: BillingReasons.SubscriptionCreate or BillingReasons.SubscriptionCycle\n        };\n\n    private Task DisableSubscriberAsync(SubscriberId subscriberId, DateTime? currentPeriodEnd) =>\n        subscriberId.Match(\n            async userId =>\n            {\n                await _userService.DisablePremiumAsync(userId.Value, currentPeriodEnd);\n                var user = await _userRepository.GetByIdAsync(userId.Value);\n                if (user != null)\n                {\n                    await _pushNotificationAdapter.NotifyPremiumStatusChangedAsync(user);\n                }\n            },\n            async organizationId =>\n            {\n                await _organizationDisableCommand.DisableAsync(organizationId.Value, currentPeriodEnd);\n                var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);\n                if (organization != null)\n                {\n                    await _pushNotificationAdapter.NotifyEnabledChangedAsync(organization);\n                }\n            },\n            async providerId =>\n            {\n                var provider = await _providerRepository.GetByIdAsync(providerId.Value);\n                if (provider != null)\n                {\n                    provider.Enabled = false;\n                    await _providerService.UpdateAsync(provider);\n                }\n            });\n\n    private Task EnableSubscriberAsync(SubscriberId subscriberId, DateTime? currentPeriodEnd) =>\n        subscriberId.Match(\n            async userId =>\n            {\n                await _userService.EnablePremiumAsync(userId.Value, currentPeriodEnd);\n                var user = await _userRepository.GetByIdAsync(userId.Value);\n                if (user != null)\n                {\n                    await _pushNotificationAdapter.NotifyPremiumStatusChangedAsync(user!);\n                }\n            },\n            async organizationId =>\n            {\n                await _organizationEnableCommand.EnableAsync(organizationId.Value, currentPeriodEnd);\n                var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);\n                if (organization != null)\n                {\n                    await _pushNotificationAdapter.NotifyEnabledChangedAsync(organization);\n                }\n            },\n            async providerId =>\n            {\n                var provider = await _providerRepository.GetByIdAsync(providerId.Value);\n                if (provider != null)\n                {\n                    provider.Enabled = true;\n                    await _providerService.UpdateAsync(provider);\n                }\n            });\n\n    private async Task SetSubscriptionToCancelAsync(Subscription subscription)\n    {\n        if (subscription.TestClock != null)\n        {\n            await WaitForTestClockToAdvanceAsync(subscription.TestClock);\n        }\n\n        var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;\n\n        await _stripeFacade.UpdateSubscription(subscription.Id, new SubscriptionUpdateOptions\n        {\n            CancelAt = now.AddDays(7),\n            ProrationBehavior = ProrationBehavior.None,\n            CancellationDetails = new SubscriptionCancellationDetailsOptions\n            {\n                Comment = $\"Automation: Setting unpaid subscription to cancel 7 days from {now:yyyy-MM-dd}.\"\n            }\n        });\n    }\n\n    private async Task RemovePendingCancellationAsync(Subscription subscription)\n        => await _stripeFacade.UpdateSubscription(subscription.Id, new SubscriptionUpdateOptions\n        {\n            CancelAtPeriodEnd = false,\n            ProrationBehavior = ProrationBehavior.None\n        });\n\n    /// <summary>\n    /// Removes the Password Manager coupon if the organization is removing the Secrets Manager trial.\n    /// Only applies to organizations that have a subscription from the Secrets Manager trial.\n    /// </summary>\n    /// <param name=\"parsedEvent\"></param>\n    /// <param name=\"subscription\"></param>\n    private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(\n        Event parsedEvent,\n        Subscription subscription)\n    {\n        if (parsedEvent.Data.PreviousAttributes?.items is null)\n        {\n            return;\n        }\n\n        var organization = subscription.Metadata.TryGetValue(\"organizationId\", out var organizationId)\n            ? await _organizationRepository.GetByIdAsync(Guid.Parse(organizationId))\n            : null;\n\n        if (organization == null)\n        {\n            return;\n        }\n\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        if (!plan.SupportsSecretsManager)\n        {\n            return;\n        }\n\n        var previousSubscription = parsedEvent.Data\n            .PreviousAttributes\n            .ToObject<Subscription>() as Subscription;\n\n        // Get all plan IDs that include Secrets Manager support to check if the organization has secret manager in the\n        // previous and/or current subscriptions.\n        var planIdsOfPlansWithSecretManager = (await _pricingClient.ListPlans())\n            .Where(orgPlan => orgPlan.SupportsSecretsManager && orgPlan.SecretsManager.StripeSeatPlanId != null)\n            .Select(orgPlan => orgPlan.SecretsManager.StripeSeatPlanId)\n            .ToHashSet();\n\n        // This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager.\n        // If there are changes to any subscription item, Stripe sends every item in the subscription, both\n        // changed and unchanged.\n        var previousSubscriptionHasSecretsManager =\n            previousSubscription?.Items is not null &&\n            previousSubscription.Items.Any(\n                previousSubscriptionItem => planIdsOfPlansWithSecretManager.Contains(previousSubscriptionItem.Plan.Id));\n\n        var currentSubscriptionHasSecretsManager =\n            subscription.Items.Any(\n                currentSubscriptionItem => planIdsOfPlansWithSecretManager.Contains(currentSubscriptionItem.Plan.Id));\n\n        if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager)\n        {\n            return;\n        }\n\n        var customerHasSecretsManagerTrial = subscription.Customer\n            ?.Discount\n            ?.Coupon\n            ?.Id == \"sm-standalone\";\n\n        var subscriptionHasSecretsManagerTrial = subscription.Discounts.Select(discount => discount.Coupon.Id)\n            .Contains(CouponIDs.SecretsManagerStandalone);\n\n        if (customerHasSecretsManagerTrial)\n        {\n            await _stripeFacade.DeleteCustomerDiscount(subscription.CustomerId);\n        }\n\n        if (subscriptionHasSecretsManagerTrial)\n        {\n            await _stripeFacade.DeleteSubscriptionDiscount(subscription.Id);\n        }\n    }\n\n    private async Task WaitForTestClockToAdvanceAsync(TestClock testClock)\n    {\n        while (testClock.Status != \"ready\")\n        {\n            await Task.Delay(TimeSpan.FromSeconds(2));\n            testClock = await _stripeFacade.GetTestClock(testClock.Id);\n            if (testClock.Status == \"internal_failure\")\n            {\n                throw new Exception(\"Stripe Test Clock encountered an internal failure\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;\nusing Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;\nusing Bit.Core.Models.Mail.Billing.Renewal.Premium;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Platform.Mail.Mailer;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Stripe;\nusing Event = Stripe.Event;\nusing Plan = Bit.Core.Models.StaticStore.Plan;\nusing PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;\n\nnamespace Bit.Billing.Services.Implementations;\n\nusing static StripeConstants;\n\npublic class UpcomingInvoiceHandler(\n    IGetPaymentMethodQuery getPaymentMethodQuery,\n    ILogger<StripeEventProcessor> logger,\n    IMailService mailService,\n    IOrganizationRepository organizationRepository,\n    IPricingClient pricingClient,\n    IProviderRepository providerRepository,\n    IStripeFacade stripeFacade,\n    IStripeEventService stripeEventService,\n    IStripeEventUtilityService stripeEventUtilityService,\n    IUserRepository userRepository,\n    IValidateSponsorshipCommand validateSponsorshipCommand,\n    IMailer mailer,\n    IFeatureService featureService)\n    : IUpcomingInvoiceHandler\n{\n    public async Task HandleAsync(Event parsedEvent)\n    {\n        var invoice = await stripeEventService.GetInvoice(parsedEvent);\n\n        var customer =\n            await stripeFacade.GetCustomer(invoice.CustomerId,\n                new CustomerGetOptions { Expand = [\"subscriptions\", \"tax\", \"tax_ids\"] });\n\n        var subscription = customer.Subscriptions.FirstOrDefault();\n\n        if (subscription == null)\n        {\n            return;\n        }\n\n        var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);\n\n        if (organizationId.HasValue)\n        {\n            await HandleOrganizationUpcomingInvoiceAsync(\n                organizationId.Value,\n                parsedEvent,\n                invoice,\n                customer,\n                subscription);\n        }\n        else if (userId.HasValue)\n        {\n            await HandlePremiumUsersUpcomingInvoiceAsync(\n                userId.Value,\n                parsedEvent,\n                invoice,\n                customer,\n                subscription);\n        }\n        else if (providerId.HasValue)\n        {\n            await HandleProviderUpcomingInvoiceAsync(\n                providerId.Value,\n                parsedEvent,\n                invoice,\n                customer,\n                subscription);\n        }\n    }\n\n    #region Organizations\n\n    private async Task HandleOrganizationUpcomingInvoiceAsync(\n        Guid organizationId,\n        Event @event,\n        Invoice invoice,\n        Customer customer,\n        Subscription subscription)\n    {\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            logger.LogWarning(\"Could not find Organization ({OrganizationID}) for '{EventType}' event ({EventID})\",\n                organizationId, @event.Type, @event.Id);\n            return;\n        }\n\n        await AlignOrganizationTaxConcernsAsync(organization, subscription, customer, @event.Id);\n\n        var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        var milestone3 = featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3);\n\n        var subscriptionAligned = await AlignOrganizationSubscriptionConcernsAsync(\n            organization,\n            @event,\n            subscription,\n            plan,\n            milestone3);\n\n        /*\n         * Subscription alignment sends out a different version of our Upcoming Invoice email, so we don't need to continue\n         * with processing.\n         */\n        if (subscriptionAligned)\n        {\n            return;\n        }\n\n        // Don't send the upcoming invoice email unless the organization's on an annual plan.\n        if (!plan.IsAnnual)\n        {\n            return;\n        }\n\n        if (stripeEventUtilityService.IsSponsoredSubscription(subscription))\n        {\n            var sponsorshipIsValid =\n                await validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId);\n\n            if (!sponsorshipIsValid)\n            {\n                /*\n                 * If the sponsorship is invalid, then the subscription was updated to use the regular families plan\n                 * price. Given that this is the case, we need the new invoice amount\n                 */\n                invoice = await stripeFacade.GetInvoice(subscription.LatestInvoiceId);\n            }\n        }\n\n        await SendUpcomingInvoiceEmailsAsync([organization.BillingEmail], invoice);\n    }\n\n    private async Task AlignOrganizationTaxConcernsAsync(\n        Organization organization,\n        Subscription subscription,\n        Customer customer,\n        string eventId)\n    {\n        var isBusinessUse = organization.PlanType.GetProductTier() != ProductTierType.Families;\n\n        if (isBusinessUse)\n        {\n            var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt);\n            switch (customer)\n            {\n                case { Address.Country: not null and not \"\", TaxExempt: var customerTaxExemptStatus }\n                    when determinedTaxExemptStatus != customerTaxExemptStatus:\n                    try\n                    {\n                        await stripeFacade.UpdateCustomer(subscription.CustomerId,\n                            new CustomerUpdateOptions { TaxExempt = determinedTaxExemptStatus });\n                    }\n                    catch (Exception exception)\n                    {\n                        logger.LogError(\n                            exception,\n                            \"Failed to set organization's ({OrganizationID}) to the required tax exemption while processing event with ID {EventID}\",\n                            organization.Id,\n                            eventId);\n                    }\n                    break;\n            }\n        }\n\n        if (!subscription.AutomaticTax.Enabled)\n        {\n            try\n            {\n                await stripeFacade.UpdateSubscription(subscription.Id,\n                    new SubscriptionUpdateOptions\n                    {\n                        AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }\n                    });\n            }\n            catch (Exception exception)\n            {\n                logger.LogError(\n                    exception,\n                    \"Failed to set organization's ({OrganizationID}) subscription to automatic tax while processing event with ID {EventID}\",\n                    organization.Id,\n                    eventId);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Aligns the organization's subscription details with the specified plan and milestone requirements.\n    /// </summary>\n    /// <param name=\"organization\">The organization whose subscription is being updated.</param>\n    /// <param name=\"event\">The Stripe event associated with this operation.</param>\n    /// <param name=\"subscription\">The organization's subscription.</param>\n    /// <param name=\"plan\">The organization's current plan.</param>\n    /// <param name=\"milestone3\">A flag indicating whether the third milestone is enabled.</param>\n    /// <returns>Whether the operation resulted in an updated subscription.</returns>\n    private async Task<bool> AlignOrganizationSubscriptionConcernsAsync(\n        Organization organization,\n        Event @event,\n        Subscription subscription,\n        Plan plan,\n        bool milestone3)\n    {\n        // currently these are the only plans that need aligned and both require the same flag and share most of the logic\n        if (!milestone3 || plan.Type is not (PlanType.FamiliesAnnually2019 or PlanType.FamiliesAnnually2025))\n        {\n            return false;\n        }\n\n        var passwordManagerItem =\n            subscription.Items.FirstOrDefault(item => item.Price.Id == plan.PasswordManager.StripePlanId);\n\n        if (passwordManagerItem == null)\n        {\n            logger.LogWarning(\"Could not find Organization's ({OrganizationId}) password manager item while processing '{EventType}' event ({EventID})\",\n                organization.Id, @event.Type, @event.Id);\n            return false;\n        }\n\n        var familiesPlan = await pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually);\n\n        organization.PlanType = familiesPlan.Type;\n        organization.Plan = familiesPlan.Name;\n        organization.UsersGetPremium = familiesPlan.UsersGetPremium;\n        organization.Seats = familiesPlan.PasswordManager.BaseSeats;\n\n        var options = new SubscriptionUpdateOptions\n        {\n            Items =\n            [\n                new SubscriptionItemOptions\n                {\n                    Id = passwordManagerItem.Id,\n                    Price = familiesPlan.PasswordManager.StripePlanId\n                }\n            ],\n            ProrationBehavior = ProrationBehavior.None\n        };\n\n        if (plan.Type == PlanType.FamiliesAnnually2019)\n        {\n            options.Discounts =\n            [\n                new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone3SubscriptionDiscount }\n            ];\n\n            var premiumAccessAddOnItem = subscription.Items.FirstOrDefault(item =>\n                item.Price.Id == plan.PasswordManager.StripePremiumAccessPlanId);\n\n            if (premiumAccessAddOnItem != null)\n            {\n                options.Items.Add(new SubscriptionItemOptions\n                {\n                    Id = premiumAccessAddOnItem.Id,\n                    Deleted = true\n                });\n            }\n\n            var seatAddOnItem = subscription.Items.FirstOrDefault(item => item.Price.Id == \"personal-org-seat-annually\");\n\n            if (seatAddOnItem != null)\n            {\n                options.Items.Add(new SubscriptionItemOptions\n                {\n                    Id = seatAddOnItem.Id,\n                    Deleted = true\n                });\n            }\n        }\n\n        try\n        {\n            await organizationRepository.ReplaceAsync(organization);\n            await stripeFacade.UpdateSubscription(subscription.Id, options);\n            await SendFamiliesRenewalEmailAsync(organization, familiesPlan, plan);\n            return true;\n        }\n        catch (Exception exception)\n        {\n            logger.LogError(\n                exception,\n                \"Failed to align subscription concerns for Organization ({OrganizationID}) while processing '{EventType}' event ({EventID})\",\n                organization.Id,\n                @event.Type,\n                @event.Id);\n            return false;\n        }\n    }\n\n    #endregion\n\n    #region Premium Users\n\n    private async Task HandlePremiumUsersUpcomingInvoiceAsync(\n        Guid userId,\n        Event @event,\n        Invoice invoice,\n        Customer customer,\n        Subscription subscription)\n    {\n        var user = await userRepository.GetByIdAsync(userId);\n\n        if (user == null)\n        {\n            logger.LogWarning(\"Could not find User ({UserID}) for '{EventType}' event ({EventID})\",\n                userId, @event.Type, @event.Id);\n            return;\n        }\n\n        await AlignPremiumUsersTaxConcernsAsync(user, @event, customer, subscription);\n\n        var milestone2Feature = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);\n        if (milestone2Feature)\n        {\n            var subscriptionAligned = await AlignPremiumUsersSubscriptionConcernsAsync(user, @event, subscription);\n\n            /*\n             * Subscription alignment sends out a different version of our Upcoming Invoice email, so we don't need to continue\n             * with processing.\n             */\n            if (subscriptionAligned)\n            {\n                return;\n            }\n        }\n\n        if (user.Premium)\n        {\n            await SendUpcomingInvoiceEmailsAsync(new List<string> { user.Email }, invoice);\n        }\n    }\n\n    private async Task AlignPremiumUsersTaxConcernsAsync(\n        User user,\n        Event @event,\n        Customer customer,\n        Subscription subscription)\n    {\n        if (!subscription.AutomaticTax.Enabled && customer.HasRecognizedTaxLocation())\n        {\n            try\n            {\n                await stripeFacade.UpdateSubscription(subscription.Id,\n                    new SubscriptionUpdateOptions\n                    {\n                        AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }\n                    });\n            }\n            catch (Exception exception)\n            {\n                logger.LogError(\n                    exception,\n                    \"Failed to set user's ({UserID}) subscription to automatic tax while processing event with ID {EventID}\",\n                    user.Id,\n                    @event.Id);\n            }\n        }\n    }\n\n    private async Task<bool> AlignPremiumUsersSubscriptionConcernsAsync(\n        User user,\n        Event @event,\n        Subscription subscription)\n    {\n        var premiumItem = subscription.Items.FirstOrDefault(i => i.Price.Id == Prices.PremiumAnnually);\n\n        if (premiumItem == null)\n        {\n            logger.LogWarning(\"Could not find User's ({UserID}) premium subscription item while processing '{EventType}' event ({EventID})\",\n                user.Id, @event.Type, @event.Id);\n            return false;\n        }\n\n        try\n        {\n            var plan = await pricingClient.GetAvailablePremiumPlan();\n            await stripeFacade.UpdateSubscription(subscription.Id,\n                new SubscriptionUpdateOptions\n                {\n                    Items =\n                    [\n                        new SubscriptionItemOptions { Id = premiumItem.Id, Price = plan.Seat.StripePriceId }\n                    ],\n                    Discounts =\n                    [\n                        new SubscriptionDiscountOptions { Coupon = CouponIDs.Milestone2SubscriptionDiscount }\n                    ],\n                    ProrationBehavior = ProrationBehavior.None\n                });\n            await SendPremiumRenewalEmailAsync(user, plan);\n            return true;\n        }\n        catch (Exception exception)\n        {\n            logger.LogError(\n                exception,\n                \"Failed to update user's ({UserID}) subscription price id while processing event with ID {EventID}\",\n                user.Id,\n                @event.Id);\n            return false;\n        }\n    }\n\n    #endregion\n\n    #region Providers\n\n    private async Task HandleProviderUpcomingInvoiceAsync(\n        Guid providerId,\n        Event @event,\n        Invoice invoice,\n        Customer customer,\n        Subscription subscription)\n    {\n        var provider = await providerRepository.GetByIdAsync(providerId);\n\n        if (provider == null)\n        {\n            logger.LogWarning(\"Could not find Provider ({ProviderID}) for '{EventType}' event ({EventID})\",\n                providerId, @event.Type, @event.Id);\n            return;\n        }\n\n        await AlignProviderTaxConcernsAsync(provider, subscription, customer, @event.Id);\n\n        if (!string.IsNullOrEmpty(provider.BillingEmail))\n        {\n            await SendProviderUpcomingInvoiceEmailsAsync(new List<string> { provider.BillingEmail }, invoice, subscription, providerId);\n        }\n    }\n\n    private async Task AlignProviderTaxConcernsAsync(\n        Provider provider,\n        Subscription subscription,\n        Customer customer,\n        string eventId)\n    {\n        var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt);\n        switch (customer)\n        {\n            case { Address.Country: not null and not \"\", TaxExempt: var customerTaxExemptStatus }\n                when determinedTaxExemptStatus != customerTaxExemptStatus:\n                try\n                {\n                    await stripeFacade.UpdateCustomer(subscription.CustomerId,\n                        new CustomerUpdateOptions { TaxExempt = determinedTaxExemptStatus });\n                }\n                catch (Exception exception)\n                {\n                    logger.LogError(\n                        exception,\n                        \"Failed to set provider's ({ProviderID}) to the required tax exemption while processing event with ID {EventID}\",\n                        provider.Id,\n                        eventId);\n                }\n                break;\n        }\n\n        if (!subscription.AutomaticTax.Enabled)\n        {\n            try\n            {\n                await stripeFacade.UpdateSubscription(subscription.Id,\n                    new SubscriptionUpdateOptions\n                    {\n                        AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }\n                    });\n            }\n            catch (Exception exception)\n            {\n                logger.LogError(\n                    exception,\n                    \"Failed to set provider's ({ProviderID}) subscription to automatic tax while processing event with ID {EventID}\",\n                    provider.Id,\n                    eventId);\n            }\n        }\n    }\n\n    private async Task SendProviderUpcomingInvoiceEmailsAsync(IEnumerable<string> emails, Invoice invoice,\n        Subscription subscription, Guid providerId)\n    {\n        var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));\n\n        var items = invoice.FormatForProvider(subscription);\n\n        if (invoice.NextPaymentAttempt.HasValue && invoice.AmountDue > 0)\n        {\n            var provider = await providerRepository.GetByIdAsync(providerId);\n            if (provider == null)\n            {\n                logger.LogWarning(\"Provider {ProviderId} not found for invoice upcoming email\", providerId);\n                return;\n            }\n\n            var collectionMethod = subscription.CollectionMethod;\n            var paymentMethod = await getPaymentMethodQuery.Run(provider);\n\n            var hasPaymentMethod = paymentMethod != null;\n            var paymentMethodDescription = paymentMethod?.Match(\n                bankAccount => $\"Bank account ending in {bankAccount.Last4}\",\n                card => $\"{card.Brand} ending in {card.Last4}\",\n                payPal => $\"PayPal account {payPal.Email}\"\n            );\n\n            await mailService.SendProviderInvoiceUpcoming(\n                validEmails,\n                invoice.AmountDue / 100M,\n                invoice.NextPaymentAttempt.Value,\n                items,\n                collectionMethod,\n                hasPaymentMethod,\n                paymentMethodDescription);\n        }\n    }\n\n    #endregion\n\n    #region Shared\n\n    private async Task SendUpcomingInvoiceEmailsAsync(IEnumerable<string> emails, Invoice invoice)\n    {\n        var validEmails = emails.Where(e => !string.IsNullOrEmpty(e));\n\n        var items = invoice.Lines.Select(i => i.Description).ToList();\n\n        if (invoice is { NextPaymentAttempt: not null, AmountDue: > 0 })\n        {\n            await mailService.SendInvoiceUpcoming(\n                validEmails,\n                invoice.AmountDue / 100M,\n                invoice.NextPaymentAttempt.Value,\n                items,\n                true);\n        }\n    }\n\n    private async Task SendFamiliesRenewalEmailAsync(\n        Organization organization,\n        Plan familiesPlan,\n        Plan planBeforeAlignment)\n    {\n        await (planBeforeAlignment switch\n        {\n            { Type: PlanType.FamiliesAnnually2025 } => SendFamilies2020RenewalEmailAsync(organization, familiesPlan),\n            { Type: PlanType.FamiliesAnnually2019 } => SendFamilies2019RenewalEmailAsync(organization, familiesPlan),\n            _ => throw new InvalidOperationException(\"Unsupported families plan in SendFamiliesRenewalEmailAsync().\")\n        });\n    }\n\n    private async Task SendFamilies2020RenewalEmailAsync(Organization organization, Plan familiesPlan)\n    {\n        var email = new Families2020RenewalMail\n        {\n            ToEmails = [organization.BillingEmail],\n            View = new Families2020RenewalMailView\n            {\n                MonthlyRenewalPrice = (familiesPlan.PasswordManager.BasePrice / 12).ToString(\"C\", new CultureInfo(\"en-US\"))\n            }\n        };\n\n        await mailer.SendEmail(email);\n    }\n\n    private async Task SendFamilies2019RenewalEmailAsync(Organization organization, Plan familiesPlan)\n    {\n        var coupon = await stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);\n        if (coupon == null)\n        {\n            throw new InvalidOperationException($\"Coupon for sending families 2019 email id:{CouponIDs.Milestone3SubscriptionDiscount} not found\");\n        }\n\n        if (coupon.PercentOff == null)\n        {\n            throw new InvalidOperationException($\"coupon.PercentOff for sending families 2019 email id:{CouponIDs.Milestone3SubscriptionDiscount} is null\");\n        }\n\n        var discountedAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice * (100 - coupon.PercentOff.Value) / 100;\n\n        var email = new Families2019RenewalMail\n        {\n            ToEmails = [organization.BillingEmail],\n            View = new Families2019RenewalMailView\n            {\n                BaseMonthlyRenewalPrice = (familiesPlan.PasswordManager.BasePrice / 12).ToString(\"C\", new CultureInfo(\"en-US\")),\n                BaseAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice.ToString(\"C\", new CultureInfo(\"en-US\")),\n                DiscountAmount = $\"{coupon.PercentOff}%\",\n                DiscountedAnnualRenewalPrice = discountedAnnualRenewalPrice.ToString(\"C\", new CultureInfo(\"en-US\"))\n            }\n        };\n\n        await mailer.SendEmail(email);\n    }\n\n    private async Task SendPremiumRenewalEmailAsync(\n        User user,\n        PremiumPlan premiumPlan)\n    {\n        var coupon = await stripeFacade.GetCoupon(CouponIDs.Milestone2SubscriptionDiscount);\n        if (coupon == null)\n        {\n            throw new InvalidOperationException($\"Coupon for sending premium renewal email id:{CouponIDs.Milestone2SubscriptionDiscount} not found\");\n        }\n\n        if (coupon.PercentOff == null)\n        {\n            throw new InvalidOperationException($\"coupon.PercentOff for sending premium renewal email id:{CouponIDs.Milestone2SubscriptionDiscount} is null\");\n        }\n\n        var discountedAnnualRenewalPrice = premiumPlan.Seat.Price * (100 - coupon.PercentOff.Value) / 100;\n\n        var email = new PremiumRenewalMail\n        {\n            ToEmails = [user.Email],\n            View = new PremiumRenewalMailView\n            {\n                BaseMonthlyRenewalPrice = (premiumPlan.Seat.Price / 12).ToString(\"C\", new CultureInfo(\"en-US\")),\n                DiscountAmount = $\"{coupon.PercentOff}%\",\n                DiscountedAnnualRenewalPrice = discountedAnnualRenewalPrice.ToString(\"C\", new CultureInfo(\"en-US\"))\n            }\n        };\n\n        await mailer.SendEmail(email);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Billing/Startup.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Globalization;\nusing Bit.Billing.Services;\nusing Bit.Billing.Services.Implementations;\nusing Bit.Commercial.Core.Utilities;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Context;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.SecretsManager.Repositories.Noop;\nusing Bit.Core.Utilities;\nusing Bit.SharedWeb.Utilities;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Quartz;\nusing Stripe;\n\nnamespace Bit.Billing;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(\"en-US\");\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; }\n    public IWebHostEnvironment Environment { get; set; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n        services.Configure<BillingSettings>(Configuration.GetSection(\"BillingSettings\"));\n        var billingSettings = Configuration.GetSection(\"BillingSettings\").Get<BillingSettings>();\n\n        // Stripe Billing\n        StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;\n        StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;\n\n        // Data Protection\n        services.AddCustomDataProtectionServices(Environment, globalSettings);\n\n        // Repositories\n        services.AddDatabaseRepositories(globalSettings);\n        services.AddTestPlayIdTracking(globalSettings);\n\n        // PayPal IPN Client\n        services.AddHttpClient<IPayPalIPNClient, PayPalIPNClient>();\n\n        // Context\n        services.AddScoped<ICurrentContext, CurrentContext>();\n\n        //Handlers\n        services.AddScoped<IStripeEventUtilityService, StripeEventUtilityService>();\n        services.AddScoped<ISubscriptionDeletedHandler, SubscriptionDeletedHandler>();\n        services.AddScoped<ISubscriptionUpdatedHandler, SubscriptionUpdatedHandler>();\n        services.AddScoped<IUpcomingInvoiceHandler, UpcomingInvoiceHandler>();\n        services.AddScoped<IChargeSucceededHandler, ChargeSucceededHandler>();\n        services.AddScoped<IChargeRefundedHandler, ChargeRefundedHandler>();\n        services.AddScoped<ICustomerUpdatedHandler, CustomerUpdatedHandler>();\n        services.AddScoped<IInvoiceCreatedHandler, InvoiceCreatedHandler>();\n        services.AddScoped<IPaymentFailedHandler, PaymentFailedHandler>();\n        services.AddScoped<IPaymentMethodAttachedHandler, PaymentMethodAttachedHandler>();\n        services.AddScoped<IPaymentSucceededHandler, PaymentSucceededHandler>();\n        services.AddScoped<IInvoiceFinalizedHandler, InvoiceFinalizedHandler>();\n        services.AddScoped<ISetupIntentSucceededHandler, SetupIntentSucceededHandler>();\n        services.AddScoped<ICouponDeletedHandler, CouponDeletedHandler>();\n        services.AddScoped<IStripeEventProcessor, StripeEventProcessor>();\n\n        // Identity\n        services.AddCustomIdentityServices(globalSettings);\n        //services.AddPasswordlessIdentityServices<ReadOnlyDatabaseIdentityUserStore>(globalSettings);\n\n        // Services\n        services.AddBaseServices(globalSettings);\n        services.AddDefaultServices(globalSettings);\n        services.AddDistributedCache(globalSettings);\n        services.AddBillingOperations();\n        services.AddCommercialCoreServices();\n\n        services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();\n\n        // TODO: Remove when OrganizationUser methods are moved out of OrganizationService, this noop dependency should\n        // TODO: no longer be required - see PM-1880\n        services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();\n\n        services.AddControllers(config =>\n        {\n            config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());\n        });\n        services.Configure<RouteOptions>(options => options.LowercaseUrls = true);\n\n        // Authentication\n        services.AddAuthentication();\n\n        services.AddScoped<IStripeFacade, StripeFacade>();\n        services.AddScoped<IStripeEventService, StripeEventService>();\n        services.AddScoped<IProviderEventService, ProviderEventService>();\n        services.AddScoped<IPushNotificationAdapter, PushNotificationAdapter>();\n\n        // Add Quartz services first\n        services.AddQuartz(q =>\n        {\n            q.UseMicrosoftDependencyInjectionJobFactory();\n        });\n        services.AddQuartzHostedService();\n\n        // Jobs service\n        Jobs.JobsHostedService.AddJobsServices(services);\n        services.AddHostedService<Jobs.JobsHostedService>();\n\n        // Swagger\n        services.AddEndpointsApiExplorer();\n        services.AddSwaggerGen();\n    }\n\n    public void Configure(\n        IApplicationBuilder app,\n        IWebHostEnvironment env)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n\n        if (env.IsDevelopment())\n        {\n            app.UseDeveloperExceptionPage();\n            app.UseSwagger();\n            app.UseSwaggerUI(c =>\n            {\n                c.SwaggerEndpoint(\"/swagger/v1/swagger.json\", \"Billing API V1\");\n            });\n        }\n\n        app.UseStaticFiles();\n        app.UseRouting();\n        app.UseAuthentication();\n        app.UseAuthorization();\n        app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());\n    }\n}\n"
  },
  {
    "path": "src/Billing/appsettings.Development.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\",\n      \"internalScim\": \"http://localhost:44559\"\n    },\n    \"mail\": {\n      \"smtp\": {\n        \"host\": \"localhost\",\n        \"port\": 10250\n      }\n    },\n    \"attachment\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\",\n      \"baseUrl\": \"http://localhost:4000/attachments/\"\n    },\n    \"events\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"storage\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    }\n  },\n  \"pricingUri\": \"https://billingpricing.qa.bitwarden.pw\"\n}\n"
  },
  {
    "path": "src/Billing/appsettings.Production.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.bitwarden.com\",\n      \"api\": \"https://api.bitwarden.com\",\n      \"identity\": \"https://identity.bitwarden.com\",\n      \"admin\": \"https://admin.bitwarden.com\",\n      \"notifications\": \"https://notifications.bitwarden.com\",\n      \"sso\": \"https://sso.bitwarden.com\",\n      \"internalNotifications\": \"https://notifications.bitwarden.com\",\n      \"internalAdmin\": \"https://admin.bitwarden.com\",\n      \"internalIdentity\": \"https://identity.bitwarden.com\",\n      \"internalApi\": \"https://api.bitwarden.com\",\n      \"internalVault\": \"https://vault.bitwarden.com\",\n      \"internalSso\": \"https://sso.bitwarden.com\",\n      \"internalScim\": \"https://scim.bitwarden.com\"\n    },\n    \"braintree\": {\n      \"production\": true\n    },\n    \"bitPay\": {\n      \"production\": true\n    }\n  },\n  \"billingSettings\": {\n    \"payPal\": {\n      \"production\": true,\n      \"businessId\": \"4ZDA7DLUUJGMN\"\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Warning\",\n          \"System\": \"Warning\",\n          \"Microsoft\": \"Warning\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Billing/appsettings.QA.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.qa.bitwarden.pw\",\n      \"api\": \"https://api.qa.bitwarden.pw\",\n      \"identity\": \"https://identity.qa.bitwarden.pw\",\n      \"admin\": \"https://admin.qa.bitwarden.pw\",\n      \"notifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"sso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalNotifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"internalAdmin\": \"https://admin.qa.bitwarden.pw\",\n      \"internalIdentity\": \"https://identity.qa.bitwarden.pw\",\n      \"internalApi\": \"https://api.qa.bitwarden.pw\",\n      \"internalVault\": \"https://vault.qa.bitwarden.pw\",\n      \"internalSso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalScim\": \"https://scim.qa.bitwarden.pw\"\n    },\n    \"braintree\": {\n      \"production\": false\n    },\n    \"bitPay\": {\n      \"production\": false\n    }\n  },\n  \"billingSettings\": {\n    \"payPal\": {\n      \"production\": false,\n      \"businessId\": \"AD3LAUZSNVPJY\"\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Debug\",\n          \"System\": \"Debug\",\n          \"Microsoft\": \"Debug\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Billing/appsettings.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"selfHosted\": false,\n    \"siteName\": \"Bitwarden\",\n    \"projectName\": \"Billing\",\n    \"stripe\": {\n      \"apiKey\": \"SECRET\"\n    },\n    \"sqlServer\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"mail\": {\n      \"sendGridApiKey\": \"SECRET\",\n      \"amazonConfigSetName\": \"Email\",\n      \"replyToEmail\": \"no-reply@bitwarden.com\"\n    },\n    \"identityServer\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"dataProtection\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"storage\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"events\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"serviceBus\": {\n      \"connectionString\": \"SECRET\",\n      \"applicationCacheTopicName\": \"SECRET\"\n    },\n    \"notificationHub\": {\n      \"connectionString\": \"SECRET\",\n      \"hubName\": \"SECRET\"\n    },\n    \"braintree\": {\n      \"production\": false,\n      \"merchantId\": \"SECRET\",\n      \"publicKey\": \"SECRET\",\n      \"privateKey\": \"SECRET\"\n    },\n    \"bitPay\": {\n      \"production\": false,\n      \"token\": \"SECRET\",\n      \"notificationUrl\": \"https://bitwarden.com/SECRET\"\n    },\n    \"amazon\": {\n      \"accessKeyId\": \"SECRET\",\n      \"accessKeySecret\": \"SECRET\",\n      \"region\": \"SECRET\"\n    }\n  },\n  \"billingSettings\": {\n    \"jobsKey\": \"SECRET\",\n    \"stripeWebhookKey\": \"SECRET\",\n    \"stripeWebhookSecret20250827Basil\": \"SECRET\",\n    \"bitPayWebhookKey\": \"SECRET\",\n    \"appleWebhookKey\": \"SECRET\",\n    \"payPal\": {\n      \"production\": false,\n      \"businessId\": \"AD3LAUZSNVPJY\",\n      \"webhookKey\": \"SECRET\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Billing/build.ps1",
    "content": "$dir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\necho \"`n## Building Billing\"\n\necho \"`nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore $dir\\Billing.csproj\necho \"Clean\"\ndotnet clean $dir\\Billing.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\necho \"Publish\"\ndotnet publish $dir\\Billing.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\n"
  },
  {
    "path": "src/Billing/build.sh",
    "content": "#!/usr/bin/env bash\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\necho -e \"\\n## Building Billing\"\n\necho -e \"\\nBuilding app\"\necho -e \".NET Core version $(dotnet --version)\"\necho -e \"Restore\"\ndotnet restore $DIR/Billing.csproj\necho -e \"Clean\"\ndotnet clean $DIR/Billing.csproj -c \"Release\" -o $DIR/obj/build-output/publish\necho -e \"Publish\"\ndotnet publish $DIR/Billing.csproj -c \"Release\" -o $DIR/obj/build-output/publish\n"
  },
  {
    "path": "src/Billing/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/core\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nexec $gosu_cmd /app/Billing\n"
  },
  {
    "path": "src/Core/AdminConsole/AbilitiesCache/IApplicationCacheServiceBusMessaging.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.AdminConsole.AbilitiesCache;\n\npublic interface IApplicationCacheServiceBusMessaging\n{\n    Task NotifyOrganizationAbilityUpsertedAsync(Organization organization);\n    Task NotifyOrganizationAbilityDeletedAsync(Guid organizationId);\n    Task NotifyProviderAbilityDeletedAsync(Guid providerId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/AbilitiesCache/IVCurrentInMemoryApplicationCacheService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Models.Data.Organizations;\n\nnamespace Bit.Core.AdminConsole.AbilitiesCache;\n\npublic interface IVCurrentInMemoryApplicationCacheService\n{\n    Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync();\n#nullable enable\n    Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId);\n#nullable disable\n    Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync();\n    Task UpsertOrganizationAbilityAsync(Organization organization);\n    Task UpsertProviderAbilityAsync(Provider provider);\n    Task DeleteOrganizationAbilityAsync(Guid organizationId);\n    Task DeleteProviderAbilityAsync(Guid providerId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/AbilitiesCache/NoOpApplicationCacheMessaging.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.AdminConsole.AbilitiesCache;\n\npublic class NoOpApplicationCacheMessaging : IApplicationCacheServiceBusMessaging\n{\n    public Task NotifyOrganizationAbilityUpsertedAsync(Organization organization)\n    {\n        return Task.CompletedTask;\n    }\n\n    public Task NotifyOrganizationAbilityDeletedAsync(Guid organizationId)\n    {\n        return Task.CompletedTask;\n    }\n\n    public Task NotifyProviderAbilityDeletedAsync(Guid providerId)\n    {\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/AbilitiesCache/ServiceBusApplicationCacheMessaging.cs",
    "content": "﻿using Azure.Messaging.ServiceBus;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.AbilitiesCache;\n\npublic class ServiceBusApplicationCacheMessaging : IApplicationCacheServiceBusMessaging\n{\n    private readonly ServiceBusSender _topicMessageSender;\n    private readonly string _subName;\n\n    public ServiceBusApplicationCacheMessaging(\n        GlobalSettings globalSettings)\n    {\n        _subName = CoreHelpers.GetApplicationCacheServiceBusSubscriptionName(globalSettings);\n        var serviceBusClient = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString);\n        _topicMessageSender = serviceBusClient.CreateSender(globalSettings.ServiceBus.ApplicationCacheTopicName);\n    }\n\n    public async Task NotifyOrganizationAbilityUpsertedAsync(Organization organization)\n    {\n        var message = new ServiceBusMessage\n        {\n            Subject = _subName,\n            ApplicationProperties =\n            {\n                { \"type\", (byte)ApplicationCacheMessageType.UpsertOrganizationAbility },\n                { \"id\", organization.Id },\n            }\n        };\n        await _topicMessageSender.SendMessageAsync(message);\n    }\n\n    public async Task NotifyOrganizationAbilityDeletedAsync(Guid organizationId)\n    {\n        var message = new ServiceBusMessage\n        {\n            Subject = _subName,\n            ApplicationProperties =\n            {\n                { \"type\", (byte)ApplicationCacheMessageType.DeleteOrganizationAbility },\n                { \"id\", organizationId },\n            }\n        };\n        await _topicMessageSender.SendMessageAsync(message);\n    }\n\n    public async Task NotifyProviderAbilityDeletedAsync(Guid providerId)\n    {\n        var message = new ServiceBusMessage\n        {\n            Subject = _subName,\n            ApplicationProperties =\n            {\n                { \"type\", (byte)ApplicationCacheMessageType.DeleteProviderAbility },\n                { \"id\", providerId },\n            }\n        };\n        await _topicMessageSender.SendMessageAsync(message);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Context/CurrentContextOrganization.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Context;\n\n/// <summary>\n/// Represents the claims for a user in relation to a particular organization.\n/// These claims will only be present for users in the <see cref=\"OrganizationUserStatusType.Confirmed\"/> status.\n/// </summary>\npublic class CurrentContextOrganization\n{\n    public CurrentContextOrganization() { }\n\n    public CurrentContextOrganization(OrganizationUserOrganizationDetails orgUser)\n    {\n        Id = orgUser.OrganizationId;\n        Type = orgUser.Type;\n        Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(orgUser.Permissions);\n        AccessSecretsManager = orgUser.AccessSecretsManager && orgUser.UseSecretsManager && orgUser.Enabled;\n    }\n\n    public Guid Id { get; set; }\n    public OrganizationUserType Type { get; set; }\n    public Permissions Permissions { get; set; } = new();\n    public bool AccessSecretsManager { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Context/CurrentContextProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.Context;\n\npublic class CurrentContextProvider\n{\n    public CurrentContextProvider() { }\n\n    public CurrentContextProvider(ProviderUser providerUser)\n    {\n        Id = providerUser.ProviderId;\n        Type = providerUser.Type;\n        Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(providerUser.Permissions);\n    }\n\n    public Guid Id { get; set; }\n    public ProviderUserType Type { get; set; }\n    public Permissions Permissions { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Entities/Group.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Models;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Entities;\n\npublic class Group : ITableObject<Guid>, IExternal\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    [MaxLength(100)]\n    public string Name { get; set; } = null!;\n    [MaxLength(300)]\n    public string? ExternalId { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Entities/GroupUser.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Entities;\n\n#nullable enable\n\npublic class GroupUser\n{\n    public Guid GroupId { get; set; }\n    public Guid OrganizationUserId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Entities/Organization.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Net;\nusing System.Text.Json;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Entities;\n\npublic class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable\n{\n    private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders;\n\n    public Guid Id { get; set; }\n    [MaxLength(50)]\n    public string? Identifier { get; set; }\n    /// <summary>\n    /// This value is HTML encoded. For display purposes use the method DisplayName() instead.\n    /// </summary>\n    [MaxLength(50)]\n    public string Name { get; set; } = null!;\n    /// <summary>\n    /// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead.\n    /// </summary>\n    [MaxLength(50)]\n    [Obsolete(\"This property has been deprecated. Use the 'Name' property instead.\")]\n    public string? BusinessName { get; set; }\n    [MaxLength(50)]\n    public string? BusinessAddress1 { get; set; }\n    [MaxLength(50)]\n    public string? BusinessAddress2 { get; set; }\n    [MaxLength(50)]\n    public string? BusinessAddress3 { get; set; }\n    [MaxLength(2)]\n    public string? BusinessCountry { get; set; }\n    [MaxLength(30)]\n    public string? BusinessTaxNumber { get; set; }\n    [MaxLength(256)]\n    public string BillingEmail { get; set; } = null!;\n    [MaxLength(50)]\n    public string Plan { get; set; } = null!;\n    public PlanType PlanType { get; set; }\n    public int? Seats { get; set; }\n    public short? MaxCollections { get; set; }\n    public bool UsePolicies { get; set; }\n    public bool UseSso { get; set; }\n    public bool UseKeyConnector { get; set; }\n    public bool UseScim { get; set; }\n    public bool UseGroups { get; set; }\n    public bool UseDirectory { get; set; }\n    public bool UseEvents { get; set; }\n    public bool UseTotp { get; set; }\n    public bool Use2fa { get; set; }\n    public bool UseApi { get; set; }\n    public bool UseResetPassword { get; set; }\n    public bool UseSecretsManager { get; set; }\n    public bool SelfHost { get; set; }\n    public bool UsersGetPremium { get; set; }\n    public bool UseCustomPermissions { get; set; }\n    public long? Storage { get; set; }\n    public short? MaxStorageGb { get; set; }\n    public GatewayType? Gateway { get; set; }\n    [MaxLength(50)]\n    public string? GatewayCustomerId { get; set; }\n    [MaxLength(50)]\n    public string? GatewaySubscriptionId { get; set; }\n    public string? ReferenceData { get; set; }\n    public bool Enabled { get; set; } = true;\n    [MaxLength(100)]\n    public string? LicenseKey { get; set; }\n    public string? PublicKey { get; set; }\n    public string? PrivateKey { get; set; }\n    public string? TwoFactorProviders { get; set; }\n    public DateTime? ExpirationDate { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n    public int? MaxAutoscaleSeats { get; set; } = null;\n    public DateTime? OwnersNotifiedOfAutoscaling { get; set; } = null;\n    public OrganizationStatusType Status { get; set; }\n    public bool UsePasswordManager { get; set; }\n    public int? SmSeats { get; set; }\n    public int? SmServiceAccounts { get; set; }\n    public int? MaxAutoscaleSmSeats { get; set; }\n    public int? MaxAutoscaleSmServiceAccounts { get; set; }\n    /// <summary>\n    /// If set to true, only owners, admins, and some custom users can create and delete collections.\n    /// If set to false, any organization member can create a collection, and any member can delete a collection that\n    /// they have Can Manage permissions for.\n    /// </summary>\n    public bool LimitCollectionCreation { get; set; }\n    public bool LimitCollectionDeletion { get; set; }\n\n    /// <summary>\n    /// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console.\n    /// If set to false, users generally need collection-level permissions to read/write a collection or its items.\n    /// </summary>\n    public bool AllowAdminAccessToAllCollectionItems { get; set; }\n\n    /// <summary>\n    /// If set to true, members can only delete items when they have a Can Manage permission over the collection.\n    /// If set to false, members can delete items when they have a Can Manage OR Can Edit permission over the collection.\n    /// </summary>\n    public bool LimitItemDeletion { get; set; }\n\n    /// <summary>\n    /// Risk Insights is a reporting feature that provides insights into the security of an organization's vault.\n    /// </summary>\n    public bool UseRiskInsights { get; set; }\n\n    /// <summary>\n    /// If true, the organization can claim domains, which unlocks additional enterprise features\n    /// </summary>\n    public bool UseOrganizationDomains { get; set; }\n\n    /// <summary>\n    /// If set to true, admins can initiate organization-issued sponsorships.\n    /// </summary>\n    public bool UseAdminSponsoredFamilies { get; set; }\n\n    /// <summary>\n    /// If set to true, organization needs their seat count synced with their subscription\n    /// </summary>\n    public bool SyncSeats { get; set; }\n\n    /// <summary>\n    /// If set to true,  user accounts created within the organization are automatically confirmed without requiring additional verification steps.\n    /// </summary>\n    public bool UseAutomaticUserConfirmation { get; set; }\n\n    /// <summary>\n    /// If set to true, disables Secrets Manager ads for users in the organization\n    /// </summary>\n    public bool UseDisableSmAdsForUsers { get; set; }\n\n    /// <summary>\n    /// If set to true, the organization has phishing protection enabled.\n    /// </summary>\n    public bool UsePhishingBlocker { get; set; }\n\n    /// <summary>\n    /// If set to true, My Items collections will be created automatically when the Organization Data Ownership\n    /// policy is enabled.\n    /// </summary>\n    public bool UseMyItems { get; set; }\n\n    public void SetNewId()\n    {\n        if (Id == default(Guid))\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n\n    /// <summary>\n    /// Returns the name of the organization, HTML decoded ready for display.\n    /// </summary>\n    public string DisplayName()\n    {\n        return WebUtility.HtmlDecode(Name);\n    }\n\n    /// <summary>\n    /// Returns the business name of the organization, HTML decoded ready for display.\n    /// </summary>\n    ///\n    [Obsolete(\"This method has been deprecated. Use the 'DisplayName()' method instead.\")]\n    public string? DisplayBusinessName()\n    {\n        return WebUtility.HtmlDecode(BusinessName);\n    }\n\n    public string? BillingEmailAddress()\n    {\n        return BillingEmail?.ToLowerInvariant()?.Trim();\n    }\n\n    public string? BillingName()\n    {\n        return DisplayBusinessName();\n    }\n\n    public string? SubscriberName()\n    {\n        return DisplayName();\n    }\n\n    public string BraintreeCustomerIdPrefix()\n    {\n        return \"o\";\n    }\n\n    public string BraintreeIdField()\n    {\n        return \"organization_id\";\n    }\n\n    public string BraintreeCloudRegionField()\n    {\n        return \"region\";\n    }\n\n    public string GatewayIdField()\n    {\n        return \"organizationId\";\n    }\n\n    public bool IsOrganization() => true;\n\n    public bool IsUser()\n    {\n        return false;\n    }\n\n    public string SubscriberType()\n    {\n        return \"Organization\";\n    }\n\n    public bool IsExpired() => ExpirationDate.HasValue && ExpirationDate.Value <= DateTime.UtcNow;\n\n    public long StorageBytesRemaining()\n    {\n        if (!MaxStorageGb.HasValue)\n        {\n            return 0;\n        }\n\n        return StorageBytesRemaining(MaxStorageGb.Value);\n    }\n\n    public long StorageBytesRemaining(short maxStorageGb)\n    {\n        var maxStorageBytes = maxStorageGb * 1073741824L;\n        if (!Storage.HasValue)\n        {\n            return maxStorageBytes;\n        }\n\n        return maxStorageBytes - Storage.Value;\n    }\n\n    public Dictionary<TwoFactorProviderType, TwoFactorProvider>? GetTwoFactorProviders()\n    {\n        if (string.IsNullOrWhiteSpace(TwoFactorProviders))\n        {\n            return null;\n        }\n\n        try\n        {\n            if (_twoFactorProviders == null)\n            {\n                _twoFactorProviders =\n                    JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(\n                        TwoFactorProviders);\n            }\n\n            return _twoFactorProviders;\n        }\n        catch (JsonException)\n        {\n            return null;\n        }\n    }\n\n    public void SetTwoFactorProviders(Dictionary<TwoFactorProviderType, TwoFactorProvider> providers)\n    {\n        if (!providers.Any())\n        {\n            TwoFactorProviders = null;\n            _twoFactorProviders = null;\n            return;\n        }\n\n        TwoFactorProviders = JsonHelpers.LegacySerialize(providers, JsonHelpers.LegacyEnumKeyResolver);\n        _twoFactorProviders = providers;\n    }\n\n    public bool TwoFactorProviderIsEnabled(TwoFactorProviderType provider)\n    {\n        var providers = GetTwoFactorProviders();\n        if (providers == null || !providers.TryGetValue(provider, out var twoFactorProvider))\n        {\n            return false;\n        }\n\n        return twoFactorProvider.Enabled && Use2fa;\n    }\n\n    public bool TwoFactorIsEnabled()\n    {\n        var providers = GetTwoFactorProviders();\n        if (providers == null)\n        {\n            return false;\n        }\n\n        return providers.Any(p => (p.Value?.Enabled ?? false) && Use2fa);\n    }\n\n    public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)\n    {\n        var providers = GetTwoFactorProviders();\n        return providers?.GetValueOrDefault(provider);\n    }\n\n    public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService)\n    {\n        // The following properties are intentionally excluded from being updated:\n        // - Id - self-hosted org will have its own unique Guid\n        // - MaxStorageGb - not enforced for self-hosted because we're not providing the storage\n\n        Name = license.Name;\n        BusinessName = license.BusinessName;\n        BillingEmail = license.BillingEmail;\n        PlanType = license.PlanType;\n        Seats = license.Seats;\n        MaxCollections = license.MaxCollections;\n        UseGroups = license.UseGroups;\n        UseDirectory = license.UseDirectory;\n        UseEvents = license.UseEvents;\n        UseTotp = license.UseTotp;\n        Use2fa = license.Use2fa;\n        UseApi = license.UseApi;\n        UsePolicies = license.UsePolicies;\n        UseMyItems = license.UseMyItems;\n        UseSso = license.UseSso;\n        UseKeyConnector = license.UseKeyConnector;\n        UseScim = license.UseScim;\n        UseResetPassword = license.UseResetPassword;\n        SelfHost = license.SelfHost;\n        UsersGetPremium = license.UsersGetPremium;\n        UseCustomPermissions = license.UseCustomPermissions;\n        Plan = license.Plan;\n        Enabled = license.Enabled;\n        ExpirationDate = license.Expires;\n        LicenseKey = license.LicenseKey;\n        RevisionDate = DateTime.UtcNow;\n        UsePasswordManager = license.UsePasswordManager;\n        UseSecretsManager = license.UseSecretsManager;\n        SmSeats = license.SmSeats;\n        SmServiceAccounts = license.SmServiceAccounts;\n        UseRiskInsights = license.UseRiskInsights;\n        UseOrganizationDomains = license.UseOrganizationDomains;\n        UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies;\n        UseDisableSmAdsForUsers = license.UseDisableSmAdsForUsers;\n        UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation;\n        UsePhishingBlocker = license.UsePhishingBlocker;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Entities/OrganizationUser.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Interfaces;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\n/// <summary>\n/// An association table between one <see cref=\"User\"/> and one <see cref=\"Organization\"/>, representing that user's\n/// membership in the organization. \"Member\" refers to the OrganizationUser object.\n/// </summary>\npublic class OrganizationUser : ITableObject<Guid>, IExternal, IOrganizationUser\n{\n    /// <summary>\n    /// A unique random identifier.\n    /// </summary>\n    public Guid Id { get; set; }\n    /// <summary>\n    /// The ID of the Organization that the user is a member of.\n    /// </summary>\n    public Guid OrganizationId { get; set; }\n    /// <summary>\n    /// The ID of the User that is the member. This is NULL if the Status is Invited (or Invited and then Revoked), because\n    /// it is not linked to a specific User yet.\n    /// </summary>\n    public Guid? UserId { get; set; }\n    /// <summary>\n    /// The email address of the user invited to the organization. This is NULL if the Status is not Invited (or\n    /// Invited and then Revoked), because in that case the OrganizationUser is linked to a User\n    /// and the email is stored on the User object.\n    /// </summary>\n    [MaxLength(256)]\n    public string? Email { get; set; }\n    /// <summary>\n    /// The Organization symmetric key encrypted with the User's public key. NULL if the user is not in a Confirmed\n    /// (or Confirmed and then Revoked) status.\n    /// </summary>\n    public string? Key { get; set; }\n    /// <summary>\n    /// The User's symmetric key encrypted with the Organization's public key. NULL if the OrganizationUser\n    /// is not enrolled in account recovery.\n    /// </summary>\n    public string? ResetPasswordKey { get; set; }\n    /// <inheritdoc cref=\"OrganizationUserStatusType\"/>\n    public OrganizationUserStatusType Status { get; set; }\n    /// <summary>\n    /// The User's role in the Organization.\n    /// </summary>\n    public OrganizationUserType Type { get; set; }\n    /// <summary>\n    /// An ID used to identify the OrganizationUser with an external directory service. Used by Directory Connector\n    /// and SCIM.\n    /// </summary>\n    [MaxLength(300)]\n    public string? ExternalId { get; set; }\n    /// <summary>\n    /// The date the OrganizationUser was created, i.e. when the User was first invited to the Organization.\n    /// </summary>\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    /// <summary>\n    /// The last date the OrganizationUser entry was updated.\n    /// </summary>\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n    /// <summary>\n    /// A json blob representing the <see cref=\"Bit.Core.Models.Data.Permissions\"/> of the OrganizationUser if they\n    /// are a Custom user role (i.e. the <see cref=\"OrganizationUserType\"/> is Custom). MAY be NULL if they are not\n    /// a custom user, but this is not guaranteed; do not use this to determine their role.\n    /// </summary>\n    /// <remarks>\n    /// Avoid using this property directly - instead use the <see cref=\"GetPermissions\"/> and <see cref=\"SetPermissions\"/>\n    /// helper methods.\n    /// </remarks>\n    public string? Permissions { get; set; }\n    /// <summary>\n    /// True if the User has access to Secrets Manager for this Organization, false otherwise.\n    /// </summary>\n    public bool AccessSecretsManager { get; set; }\n\n    /// <summary>\n    /// Checks whether the given reset password key is non-null and non-whitespace.\n    /// </summary>\n    public static bool IsValidResetPasswordKey(string? resetPasswordKey)\n        => !string.IsNullOrWhiteSpace(resetPasswordKey);\n\n    /// <summary>\n    /// Whether this organization user is enrolled in account recovery.\n    /// </summary>\n    public bool IsEnrolledInAccountRecovery() => IsValidResetPasswordKey(ResetPasswordKey);\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public Permissions? GetPermissions()\n    {\n        return string.IsNullOrWhiteSpace(Permissions) ? null\n            : CoreHelpers.LoadClassFromJsonData<Permissions>(Permissions);\n    }\n\n    public void SetPermissions(Permissions permissions)\n    {\n        Permissions = CoreHelpers.ClassToJsonData(permissions);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Entities/Policy.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Entities;\n\npublic class Policy : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public PolicyType Type { get; set; }\n    public string? Data { get; set; }\n    public bool Enabled { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public T GetDataModel<T>() where T : IPolicyDataModel, new()\n    {\n        return CoreHelpers.LoadClassFromJsonData<T>(Data);\n    }\n\n    public void SetDataModel<T>(T dataModel) where T : IPolicyDataModel, new()\n    {\n        Data = CoreHelpers.ClassToJsonData(dataModel);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Entities/Provider/Provider.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Net;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Entities.Provider;\n\npublic class Provider : ITableObject<Guid>, ISubscriber\n{\n    public Guid Id { get; set; }\n    /// <summary>\n    /// This value is HTML encoded. For display purposes use the method DisplayName() instead.\n    /// </summary>\n    public string? Name { get; set; }\n    /// <summary>\n    /// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead.\n    /// </summary>\n    public string? BusinessName { get; set; }\n    public string? BusinessAddress1 { get; set; }\n    public string? BusinessAddress2 { get; set; }\n    public string? BusinessAddress3 { get; set; }\n    public string? BusinessCountry { get; set; }\n    public string? BusinessTaxNumber { get; set; }\n    public string? BillingEmail { get; set; }\n    public string? BillingPhone { get; set; }\n    public ProviderStatusType Status { get; set; }\n    public bool UseEvents { get; set; }\n    public ProviderType Type { get; set; }\n    public bool Enabled { get; set; } = true;\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n    public GatewayType? Gateway { get; set; }\n    [MaxLength(50)]\n    public string? GatewayCustomerId { get; set; }\n    [MaxLength(50)]\n    public string? GatewaySubscriptionId { get; set; }\n    public string? DiscountId { get; set; }\n\n    public string? BillingEmailAddress() => BillingEmail?.ToLowerInvariant().Trim();\n\n    public string? BillingName() => DisplayBusinessName();\n\n    public string? SubscriberName() => DisplayName();\n\n    public string BraintreeCustomerIdPrefix() => \"p\";\n\n    public string BraintreeIdField() => \"provider_id\";\n\n    public string BraintreeCloudRegionField() => \"region\";\n\n    public bool IsOrganization() => false;\n\n    public bool IsUser() => false;\n\n    public string SubscriberType() => \"Provider\";\n\n    public bool IsExpired() => false;\n\n    public void SetNewId()\n    {\n        if (Id == default)\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n\n    /// <summary>\n    /// Returns the name of the provider, HTML decoded ready for display.\n    /// </summary>\n    public string? DisplayName()\n    {\n        return WebUtility.HtmlDecode(Name);\n    }\n\n    /// <summary>\n    /// Returns the business name of the provider, HTML decoded ready for display.\n    /// </summary>\n    public string? DisplayBusinessName()\n    {\n        return WebUtility.HtmlDecode(BusinessName);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Entities/Provider/ProviderOrganization.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Entities.Provider;\n\npublic class ProviderOrganization : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid ProviderId { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string? Key { get; set; }\n    public string? Settings { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        if (Id == default)\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Entities/Provider/ProviderUser.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Entities.Provider;\n\npublic class ProviderUser : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid ProviderId { get; set; }\n    public Guid? UserId { get; set; }\n    public string? Email { get; set; }\n    public string? Key { get; set; }\n    public ProviderUserStatusType Status { get; set; }\n    public ProviderUserType Type { get; set; }\n    public string? Permissions { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        if (Id == default)\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Enums/OrganizationStatusType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum OrganizationStatusType : byte\n{\n    Pending = 0,\n    Created = 1,\n    Managed = 2,\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Enums/OrganizationUserStatusType.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.Enums;\n\n/// <summary>\n/// Represents the different stages of a member's lifecycle in an organization.\n/// The <see cref=\"OrganizationUser\"/> object is populated differently depending on their Status.\n/// </summary>\npublic enum OrganizationUserStatusType : short\n{\n    /// <summary>\n    /// The OrganizationUser entry only represents an invitation to join the organization. It is not linked to a\n    /// specific User yet.\n    /// </summary>\n    Invited = 0,\n    /// <summary>\n    /// The User has accepted the invitation and linked their User account to the OrganizationUser entry.\n    /// </summary>\n    Accepted = 1,\n    /// <summary>\n    /// An administrator has granted the User access to the organization. This is the final step in the User becoming\n    /// a \"full\" member of the organization, including a key exchange so that they can decrypt organization data.\n    /// </summary>\n    Confirmed = 2,\n    /// <summary>\n    /// The OrganizationUser has been revoked from the organization and cannot access organization data while in this state.\n    /// </summary>\n    /// <remarks>\n    /// An OrganizationUser may move into this status from any other status, and will move back to their original status\n    /// if restored. This allows an administrator to easily suspend and restore access without going through the\n    /// Invite flow again.\n    /// </remarks>\n    Revoked = -1,\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Enums/OrganizationUserType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum OrganizationUserType : byte\n{\n    Owner = 0,\n    Admin = 1,\n    User = 2,\n    // Manager = 3 has been intentionally permanently deleted\n    Custom = 4,\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Enums/PolicyType.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Enums;\n\npublic enum PolicyType : byte\n{\n    TwoFactorAuthentication = 0,\n    MasterPassword = 1,\n    PasswordGenerator = 2,\n    SingleOrg = 3,\n    RequireSso = 4,\n    OrganizationDataOwnership = 5,\n    DisableSend = 6,\n    SendOptions = 7,\n    ResetPassword = 8,\n    MaximumVaultTimeout = 9,\n    DisablePersonalVaultExport = 10,\n    ActivateAutofill = 11,\n    AutomaticAppLogIn = 12,\n    FreeFamiliesSponsorshipPolicy = 13,\n    RemoveUnlockWithPin = 14,\n    RestrictedItemTypesPolicy = 15,\n    UriMatchDefaults = 16,\n    AutotypeDefaultSetting = 17,\n    AutomaticUserConfirmation = 18,\n    BlockClaimedDomainAccountCreation = 19,\n}\n\npublic static class PolicyTypeExtensions\n{\n    /// <summary>\n    /// Returns the name of the policy for display to the user.\n    /// Do not include the word \"policy\" in the return value.\n    /// </summary>\n    public static string GetName(this PolicyType type)\n    {\n        return type switch\n        {\n            PolicyType.TwoFactorAuthentication => \"Require two-step login\",\n            PolicyType.MasterPassword => \"Master password requirements\",\n            PolicyType.PasswordGenerator => \"Password generator\",\n            PolicyType.SingleOrg => \"Single organization\",\n            PolicyType.RequireSso => \"Require single sign-on authentication\",\n            PolicyType.OrganizationDataOwnership => \"Enforce organization data ownership\",\n            PolicyType.DisableSend => \"Remove Send\",\n            PolicyType.SendOptions => \"Send options\",\n            PolicyType.ResetPassword => \"Account recovery administration\",\n            PolicyType.MaximumVaultTimeout => \"Vault timeout\",\n            PolicyType.DisablePersonalVaultExport => \"Remove individual vault export\",\n            PolicyType.ActivateAutofill => \"Active auto-fill\",\n            PolicyType.AutomaticAppLogIn => \"Automatic login with SSO\",\n            PolicyType.FreeFamiliesSponsorshipPolicy => \"Remove Free Bitwarden Families sponsorship\",\n            PolicyType.RemoveUnlockWithPin => \"Remove unlock with PIN\",\n            PolicyType.RestrictedItemTypesPolicy => \"Restricted item types\",\n            PolicyType.UriMatchDefaults => \"URI match defaults\",\n            PolicyType.AutotypeDefaultSetting => \"Autotype default setting\",\n            PolicyType.AutomaticUserConfirmation => \"Automatically confirm invited users\",\n            PolicyType.BlockClaimedDomainAccountCreation => \"Block account creation for claimed domains\",\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Enums/Provider/ProviderStatusType.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Enums.Provider;\n\npublic enum ProviderStatusType : byte\n{\n    Pending = 0,\n    Created = 1,\n    Billable = 2\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Enums/Provider/ProviderType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.AdminConsole.Enums.Provider;\n\npublic enum ProviderType : byte\n{\n    [Display(ShortName = \"MSP\", Name = \"Managed Service Provider\", Description = \"Creates provider portal for client organization management\", Order = 0)]\n    Msp = 0,\n    [Display(ShortName = \"Reseller\", Name = \"Reseller\", Description = \"Creates Bitwarden Portal page for client organization billing management\", Order = 1000)]\n    Reseller = 1,\n    [Display(ShortName = \"Business Unit\", Name = \"Business Unit\", Description = \"Creates provider portal for business unit management\", Order = 1)]\n    BusinessUnit = 2,\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Enums/Provider/ProviderUserStatusType.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Enums.Provider;\n\npublic enum ProviderUserStatusType : byte\n{\n    Invited = 0,\n    Accepted = 1,\n    Confirmed = 2,\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Enums/Provider/ProviderUserType.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Enums.Provider;\n\npublic enum ProviderUserType : byte\n{\n    ProviderAdmin = 0,\n    ServiceUser = 1,\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Enums/ScimProviderType.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Enums;\n\npublic enum ScimProviderType : byte\n{\n    Default = 0,\n    AzureAd = 1,\n    Okta = 2,\n    OneLogin = 3,\n    JumpCloud = 4,\n    GoogleWorkspace = 5,\n    Rippling = 6,\n    Ping = 7,\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Interfaces/IOrganizationUser.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Interfaces;\n\npublic interface IOrganizationUser\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public Guid? UserId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Business/ImportedGroup.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.AdminConsole.Models.Business;\n\npublic class ImportedGroup\n{\n    public Group Group { get; set; }\n    public HashSet<string> ExternalUserIds { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Business/ImportedOrganizationUser.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Business;\n\npublic class ImportedOrganizationUser\n{\n    public string Email { get; set; }\n    public string ExternalId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Business/InviteOrganization.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.AdminConsole.Models.Business;\n\npublic record InviteOrganization\n{\n    public Guid OrganizationId { get; init; }\n    public int? Seats { get; init; }\n    public int? MaxAutoScaleSeats { get; init; }\n    public int? SmSeats { get; init; }\n    public int? SmMaxAutoScaleSeats { get; init; }\n    public Plan Plan { get; init; }\n    public string GatewayCustomerId { get; init; }\n    public string GatewaySubscriptionId { get; init; }\n    public bool UseSecretsManager { get; init; }\n\n    public InviteOrganization()\n    {\n\n    }\n\n    public InviteOrganization(Organization organization, Plan plan)\n    {\n        OrganizationId = organization.Id;\n        Seats = organization.Seats;\n        MaxAutoScaleSeats = organization.MaxAutoscaleSeats;\n        SmSeats = organization.SmSeats;\n        SmMaxAutoScaleSeats = organization.MaxAutoscaleSmSeats;\n        Plan = plan;\n        GatewayCustomerId = organization.GatewayCustomerId;\n        GatewaySubscriptionId = organization.GatewaySubscriptionId;\n        UseSecretsManager = organization.UseSecretsManager;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Business/OrganizationCollectionManagementSettings.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Models.Business;\n\npublic record OrganizationCollectionManagementSettings\n{\n    public bool LimitCollectionCreation { get; set; }\n    public bool LimitCollectionDeletion { get; set; }\n    public bool LimitItemDeletion { get; set; }\n    public bool AllowAdminAccessToAllCollectionItems { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Business/OrganizationUserInvite.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Core.Models.Business;\n\npublic class OrganizationUserInvite\n{\n    public IEnumerable<string> Emails { get; set; }\n    public Enums.OrganizationUserType? Type { get; set; }\n    public bool AccessSecretsManager { get; set; }\n    public Permissions Permissions { get; set; }\n    public IEnumerable<CollectionAccessSelection> Collections { get; set; }\n    public IEnumerable<Guid> Groups { get; set; }\n\n    public OrganizationUserInvite() { }\n\n    public OrganizationUserInvite(OrganizationUserInviteData requestModel)\n    {\n        Emails = requestModel.Emails;\n        Type = requestModel.Type;\n        AccessSecretsManager = requestModel.AccessSecretsManager;\n        Collections = requestModel.Collections;\n        Groups = requestModel.Groups;\n        Permissions = requestModel.Permissions;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Business/Provider/ProviderUserInvite.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Enums.Provider;\n\nnamespace Bit.Core.AdminConsole.Models.Business.Provider;\n\npublic class ProviderUserInvite<T>\n{\n    public IEnumerable<T> UserIdentifiers { get; set; }\n    public ProviderUserType Type { get; set; }\n    public Guid InvitingUserId { get; set; }\n    public Guid ProviderId { get; set; }\n}\n\npublic static class ProviderUserInviteFactory\n{\n    public static ProviderUserInvite<string> CreateInitialInvite(IEnumerable<string> inviteeEmails, ProviderUserType type, Guid invitingUserId, Guid providerId)\n    {\n        return new ProviderUserInvite<string>\n        {\n            UserIdentifiers = inviteeEmails,\n            Type = type,\n            InvitingUserId = invitingUserId,\n            ProviderId = providerId\n        };\n    }\n\n    public static ProviderUserInvite<Guid> CreateReinvite(IEnumerable<Guid> inviteeUserIds, Guid invitingUserId, Guid providerId)\n    {\n        return new ProviderUserInvite<Guid>\n        {\n            UserIdentifiers = inviteeUserIds,\n            InvitingUserId = invitingUserId,\n            ProviderId = providerId\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Business/Tokenables/OrgDeleteTokenable.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.AdminConsole.Models.Business.Tokenables;\n\npublic class OrgDeleteTokenable : Tokens.ExpiringTokenable\n{\n    public const string ClearTextPrefix = \"\";\n    public const string DataProtectorPurpose = \"OrgDeleteDataProtector\";\n    public const string TokenIdentifier = \"OrgDelete\";\n    public string Identifier { get; set; } = TokenIdentifier;\n    public Guid Id { get; set; }\n\n    [JsonConstructor]\n    public OrgDeleteTokenable(DateTime expirationDate)\n    {\n        ExpirationDate = expirationDate;\n    }\n\n    public OrgDeleteTokenable(Organization organization, int hoursTillExpiration)\n    {\n        Id = organization.Id;\n        ExpirationDate = DateTime.UtcNow.AddHours(hoursTillExpiration);\n    }\n\n    public bool IsValid(Organization organization)\n    {\n        return Id == organization.Id;\n    }\n\n    protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Business/Tokenables/ProviderDeleteTokenable.cs",
    "content": "﻿using Newtonsoft.Json;\n\nnamespace Bit.Core.AdminConsole.Models.Business.Tokenables;\n\npublic class ProviderDeleteTokenable : Tokens.ExpiringTokenable\n{\n    public const string ClearTextPrefix = \"BwProviderId\";\n    public const string DataProtectorPurpose = \"ProviderDeleteDataProtector\";\n    public const string TokenIdentifier = \"ProviderDelete\";\n    public string Identifier { get; set; } = TokenIdentifier;\n    public Guid Id { get; set; }\n\n    [JsonConstructor]\n    public ProviderDeleteTokenable()\n    {\n\n    }\n\n    [JsonConstructor]\n    public ProviderDeleteTokenable(DateTime expirationDate)\n    {\n        ExpirationDate = expirationDate;\n    }\n\n    public ProviderDeleteTokenable(Entities.Provider.Provider provider, int hoursTillExpiration)\n    {\n        Id = provider.Id;\n        ExpirationDate = DateTime.UtcNow.AddHours(hoursTillExpiration);\n    }\n\n    public bool IsValid(Entities.Provider.Provider provider)\n    {\n        return Id == provider.Id;\n    }\n\n    protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/GroupWithCollections.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.AdminConsole.Models.Data;\n\npublic class GroupWithCollections : Group\n{\n    public DataTable Collections { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/IActingUser.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.Models.Data;\n\npublic interface IActingUser\n{\n    Guid? UserId { get; }\n    bool IsOrganizationOwnerOrProvider { get; }\n    EventSystemUser? SystemUserType { get; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/IProfileOrganizationDetails.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.AdminConsole.Models.Data;\n\n/// <summary>\n/// Interface defining common organization details properties shared between\n/// regular organization users and provider organization users for profile endpoints.\n/// </summary>\npublic interface IProfileOrganizationDetails\n{\n    Guid? UserId { get; set; }\n    Guid OrganizationId { get; set; }\n    string Name { get; set; }\n    bool Enabled { get; set; }\n    PlanType PlanType { get; set; }\n    bool UsePolicies { get; set; }\n    bool UseSso { get; set; }\n    bool UseKeyConnector { get; set; }\n    bool UseScim { get; set; }\n    bool UseGroups { get; set; }\n    bool UseDirectory { get; set; }\n    bool UseEvents { get; set; }\n    bool UseTotp { get; set; }\n    bool Use2fa { get; set; }\n    bool UseApi { get; set; }\n    bool UseResetPassword { get; set; }\n    bool SelfHost { get; set; }\n    bool UsersGetPremium { get; set; }\n    bool UseCustomPermissions { get; set; }\n    bool UseSecretsManager { get; set; }\n    int? Seats { get; set; }\n    short? MaxCollections { get; set; }\n    short? MaxStorageGb { get; set; }\n    string? Identifier { get; set; }\n    string? Key { get; set; }\n    string? ResetPasswordKey { get; set; }\n    string? PublicKey { get; set; }\n    string? PrivateKey { get; set; }\n    string? SsoExternalId { get; set; }\n    string? Permissions { get; set; }\n    Guid? ProviderId { get; set; }\n    string? ProviderName { get; set; }\n    ProviderType? ProviderType { get; set; }\n    bool? SsoEnabled { get; set; }\n    string? SsoConfig { get; set; }\n    bool UsePasswordManager { get; set; }\n    bool LimitCollectionCreation { get; set; }\n    bool LimitCollectionDeletion { get; set; }\n    bool AllowAdminAccessToAllCollectionItems { get; set; }\n    bool UseRiskInsights { get; set; }\n    bool LimitItemDeletion { get; set; }\n    bool UseAdminSponsoredFamilies { get; set; }\n    bool UseOrganizationDomains { get; set; }\n    bool UseAutomaticUserConfirmation { get; set; }\n    bool UseDisableSMAdsForUsers { get; set; }\n\n    bool UsePhishingBlocker { get; set; }\n    bool UseMyItems { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/OrganizationUsers/AcceptedOrganizationUserToConfirm.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Models.Data.OrganizationUsers;\n\npublic record AcceptedOrganizationUserToConfirm\n{\n    public required Guid OrganizationUserId { get; init; }\n    public required Guid UserId { get; init; }\n    public required string Key { get; init; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/OrganizationSubscriptionUpdate.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Organizations;\n\npublic record OrganizationSubscriptionUpdate\n{\n    public required Organization Organization { get; set; }\n    public int Seats => Organization.Seats ?? 0;\n    public Plan? Plan { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\npublic class OrganizationUserInviteData\n{\n    public IEnumerable<string> Emails { get; set; }\n    public OrganizationUserType? Type { get; set; }\n    public bool AccessSecretsManager { get; set; }\n    public IEnumerable<CollectionAccessSelection> Collections { get; set; }\n    public IEnumerable<Guid> Groups { get; set; }\n    public Permissions Permissions { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\npublic class OrganizationUserOrganizationDetails : IProfileOrganizationDetails\n{\n    public Guid OrganizationId { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid OrganizationUserId { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; } = null!;\n    public bool UsePolicies { get; set; }\n    public bool UseSso { get; set; }\n    public bool UseKeyConnector { get; set; }\n    public bool UseScim { get; set; }\n    public bool UseGroups { get; set; }\n    public bool UseDirectory { get; set; }\n    public bool UseEvents { get; set; }\n    public bool UseTotp { get; set; }\n    public bool Use2fa { get; set; }\n    public bool UseApi { get; set; }\n    public bool UseResetPassword { get; set; }\n    public bool UseSecretsManager { get; set; }\n    public bool SelfHost { get; set; }\n    public bool UsersGetPremium { get; set; }\n    public bool UseCustomPermissions { get; set; }\n    public int? Seats { get; set; }\n    public short? MaxCollections { get; set; }\n    public short? MaxStorageGb { get; set; }\n    public string? Key { get; set; }\n    public Enums.OrganizationUserStatusType Status { get; set; }\n    public Enums.OrganizationUserType Type { get; set; }\n    public bool Enabled { get; set; }\n    public PlanType PlanType { get; set; }\n    public string? SsoExternalId { get; set; }\n    public string? Identifier { get; set; }\n    public string? Permissions { get; set; }\n    public string? ResetPasswordKey { get; set; }\n    public string? PublicKey { get; set; }\n    public string? PrivateKey { get; set; }\n    public Guid? ProviderId { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string? ProviderName { get; set; }\n    public ProviderType? ProviderType { get; set; }\n    public string? FamilySponsorshipFriendlyName { get; set; }\n    public bool? SsoEnabled { get; set; }\n    public string? SsoConfig { get; set; }\n    public DateTime? FamilySponsorshipLastSyncDate { get; set; }\n    public DateTime? FamilySponsorshipValidUntil { get; set; }\n    public bool? FamilySponsorshipToDelete { get; set; }\n    public bool AccessSecretsManager { get; set; }\n    public bool UsePasswordManager { get; set; }\n    public int? SmSeats { get; set; }\n    public int? SmServiceAccounts { get; set; }\n    public bool LimitCollectionCreation { get; set; }\n    public bool LimitCollectionDeletion { get; set; }\n    public bool LimitItemDeletion { get; set; }\n    public bool AllowAdminAccessToAllCollectionItems { get; set; }\n    public bool UseRiskInsights { get; set; }\n    public bool UseOrganizationDomains { get; set; }\n    public bool UseAdminSponsoredFamilies { get; set; }\n    public bool? IsAdminInitiated { get; set; }\n    public bool UseAutomaticUserConfirmation { get; set; }\n    public bool UseDisableSMAdsForUsers { get; set; }\n    public bool UsePhishingBlocker { get; set; }\n    public bool UseMyItems { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\npublic class OrganizationUserPolicyDetails\n{\n    public Guid OrganizationUserId { get; set; }\n\n    public Guid OrganizationId { get; set; }\n\n    public PolicyType PolicyType { get; set; }\n\n    public bool PolicyEnabled { get; set; }\n\n    public string PolicyData { get; set; }\n\n    public OrganizationUserType OrganizationUserType { get; set; }\n\n    public OrganizationUserStatusType OrganizationUserStatus { get; set; }\n\n    public string OrganizationUserPermissionsData { get; set; }\n\n    public bool IsProvider { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPublicKey.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\npublic class OrganizationUserPublicKey\n{\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public string PublicKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserResetPasswordDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\npublic class OrganizationUserResetPasswordDetails\n{\n    public OrganizationUserResetPasswordDetails() { }\n\n    public OrganizationUserResetPasswordDetails(OrganizationUser orgUser, User user, Organization org)\n    {\n        if (orgUser == null)\n        {\n            throw new ArgumentNullException(nameof(orgUser));\n        }\n\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        if (org == null)\n        {\n            throw new ArgumentNullException(nameof(org));\n        }\n\n        OrganizationUserId = orgUser.Id;\n        Kdf = user.Kdf;\n        KdfIterations = user.KdfIterations;\n        KdfMemory = user.KdfMemory;\n        KdfParallelism = user.KdfParallelism;\n        ResetPasswordKey = orgUser.ResetPasswordKey;\n        EncryptedPrivateKey = org.PrivateKey;\n    }\n    public Guid OrganizationUserId { get; set; }\n    public KdfType Kdf { get; set; }\n    public int KdfIterations { get; set; }\n    public int? KdfMemory { get; set; }\n    public int? KdfParallelism { get; set; }\n    public string ResetPasswordKey { get; set; }\n    public string EncryptedPrivateKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Interfaces;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\npublic class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser, IOrganizationUser\n{\n    private Dictionary<TwoFactorProviderType, TwoFactorProvider> _twoFactorProviders;\n\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public Guid? UserId { get; set; }\n    public string Name { get; set; }\n    public string Email { get; set; }\n    public string AvatarColor { get; set; }\n    public string TwoFactorProviders { get; set; }\n    /// <summary>\n    /// Indicates whether the user has a personal premium subscription.\n    /// Does not include premium access from organizations -\n    /// do not use this to check whether the user can access premium features.\n    /// Null when the organization user is in Invited status (UserId is null).\n    /// </summary>\n    public bool? Premium { get; set; }\n    public OrganizationUserStatusType Status { get; set; }\n    public OrganizationUserType Type { get; set; }\n    public bool AccessSecretsManager { get; set; }\n    public string ExternalId { get; set; }\n    public string SsoExternalId { get; set; }\n    public string Permissions { get; set; }\n    public string ResetPasswordKey { get; set; }\n    public bool UsesKeyConnector { get; set; }\n    public bool HasMasterPassword { get; set; }\n\n    public ICollection<Guid> Groups { get; set; } = new List<Guid>();\n    public ICollection<CollectionAccessSelection> Collections { get; set; } = new List<CollectionAccessSelection>();\n\n    public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()\n    {\n        if (string.IsNullOrWhiteSpace(TwoFactorProviders))\n        {\n            return null;\n        }\n\n        try\n        {\n            if (_twoFactorProviders == null)\n            {\n                _twoFactorProviders =\n                    JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(\n                        TwoFactorProviders);\n            }\n\n            return _twoFactorProviders;\n        }\n        catch (Newtonsoft.Json.JsonException)\n        {\n            return null;\n        }\n    }\n\n    public Guid? GetUserId()\n    {\n        return UserId;\n    }\n\n    public Permissions GetPermissions()\n    {\n        return string.IsNullOrWhiteSpace(Permissions) ? null\n            : CoreHelpers.LoadClassFromJsonData<Permissions>(Permissions);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserWithCollections.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\npublic class OrganizationUserWithCollections : OrganizationUser\n{\n    public DataTable Collections { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/Policies/IPolicyDataModel.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\npublic interface IPolicyDataModel\n{\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nnamespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\npublic class MasterPasswordPolicyData : IPolicyDataModel\n{\n    /// <summary>\n    /// Minimum password complexity score (0-4). Null indicates no complexity requirement.\n    /// </summary>\n    [JsonPropertyName(\"minComplexity\")]\n    [Range(0, 4)]\n    public int? MinComplexity { get; set; }\n\n    /// <summary>\n    /// Minimum password length (12-128). Null indicates no minimum length requirement.\n    /// </summary>\n    [JsonPropertyName(\"minLength\")]\n    [Range(12, 128)]\n    public int? MinLength { get; set; }\n    [JsonPropertyName(\"requireLower\")]\n    public bool? RequireLower { get; set; }\n    [JsonPropertyName(\"requireUpper\")]\n    public bool? RequireUpper { get; set; }\n    [JsonPropertyName(\"requireNumbers\")]\n    public bool? RequireNumbers { get; set; }\n    [JsonPropertyName(\"requireSpecial\")]\n    public bool? RequireSpecial { get; set; }\n    [JsonPropertyName(\"enforceOnLogin\")]\n    public bool? EnforceOnLogin { get; set; }\n\n    /// <summary>\n    /// Combine the other policy data with this instance, taking the most secure options\n    /// </summary>\n    /// <param name=\"other\">The other policy instance to combine with this</param>\n    public void CombineWith(MasterPasswordPolicyData? other)\n    {\n        if (other == null)\n        {\n            return;\n        }\n\n        if (other.MinComplexity.HasValue && (!MinComplexity.HasValue || other.MinComplexity > MinComplexity))\n        {\n            MinComplexity = other.MinComplexity;\n        }\n\n        if (other.MinLength.HasValue && (!MinLength.HasValue || other.MinLength > MinLength))\n        {\n            MinLength = other.MinLength;\n        }\n\n        RequireLower = (other.RequireLower ?? false) || (RequireLower ?? false);\n        RequireUpper = (other.RequireUpper ?? false) || (RequireUpper ?? false);\n        RequireNumbers = (other.RequireNumbers ?? false) || (RequireNumbers ?? false);\n        RequireSpecial = (other.RequireSpecial ?? false) || (RequireSpecial ?? false);\n        EnforceOnLogin = (other.EnforceOnLogin ?? false) || (EnforceOnLogin ?? false);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/Policies/OrganizationPolicyDetails.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\npublic class OrganizationPolicyDetails : PolicyDetails\n{\n    public Guid UserId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyDetails.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\n/// <summary>\n/// Represents an OrganizationUser and a Policy which *may* be enforced against them.\n/// You may assume that the Policy is enabled and that the organization's plan supports policies.\n/// This is consumed by <see cref=\"IPolicyRequirement\"/> to create requirements for specific policy types.\n/// </summary>\npublic class PolicyDetails\n{\n    public Guid OrganizationUserId { get; set; }\n    public Guid OrganizationId { get; set; }\n    public PolicyType PolicyType { get; set; }\n    public string? PolicyData { get; set; }\n    public OrganizationUserType OrganizationUserType { get; set; }\n    public OrganizationUserStatusType OrganizationUserStatus { get; set; }\n    /// <summary>\n    /// Custom permissions for the organization user, if any. Use <see cref=\"GetOrganizationUserCustomPermissions\"/>\n    /// to deserialize.\n    /// </summary>\n    public string? OrganizationUserPermissionsData { get; set; }\n    /// <summary>\n    /// True if the user is also a ProviderUser for the organization, false otherwise.\n    /// </summary>\n    public bool IsProvider { get; set; }\n\n    public T GetDataModel<T>() where T : IPolicyDataModel, new()\n        => CoreHelpers.LoadClassFromJsonData<T>(PolicyData);\n\n    public Permissions GetOrganizationUserCustomPermissions()\n        => CoreHelpers.LoadClassFromJsonData<Permissions>(OrganizationUserPermissionsData);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/Policies/PolicyStatus.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\npublic class PolicyStatus\n{\n    public PolicyStatus(Guid organizationId, PolicyType policyType, Policy? policy = null)\n    {\n        OrganizationId = policy?.OrganizationId ?? organizationId;\n        Data = policy?.Data;\n        Type = policy?.Type ?? policyType;\n        Enabled = policy?.Enabled ?? false;\n    }\n\n    public Guid OrganizationId { get; set; }\n    public PolicyType Type { get; set; }\n    public bool Enabled { get; set; }\n    public string? Data { get; set; }\n\n    public T GetDataModel<T>() where T : IPolicyDataModel, new()\n    {\n        return CoreHelpers.LoadClassFromJsonData<T>(Data);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\npublic class ResetPasswordDataModel : IPolicyDataModel\n{\n    [Display(Name = \"ResetPasswordAutoEnrollCheckbox\")]\n    public bool AutoEnrollEnabled { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\npublic class SendOptionsPolicyData : IPolicyDataModel\n{\n    [Display(Name = \"DisableHideEmail\")]\n    public bool DisableHideEmail { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Data.Organizations;\n\npublic class SelfHostedOrganizationDetails : Organization\n{\n    public int OccupiedSeatCount { get; set; }\n    public int CollectionCount { get; set; }\n    public int GroupCount { get; set; }\n    public IEnumerable<OrganizationUser> OrganizationUsers { get; set; }\n    public IEnumerable<Policy> Policies { get; set; }\n    public SsoConfig SsoConfig { get; set; }\n    public IEnumerable<OrganizationConnection> ScimConnections { get; set; }\n\n    public bool CanUseLicense(OrganizationLicense license, out string exception)\n    {\n        if (license.Seats.HasValue && OccupiedSeatCount > license.Seats.Value)\n        {\n            exception = $\"Your organization currently has {OccupiedSeatCount} seats filled. \" +\n                $\"Your new license only has ({license.Seats.Value}) seats. Remove some users.\";\n            return false;\n        }\n\n        if (license.MaxCollections.HasValue && CollectionCount > license.MaxCollections.Value)\n        {\n            exception = $\"Your organization currently has {CollectionCount} collections. \" +\n                $\"Your new license allows for a maximum of ({license.MaxCollections.Value}) collections. \" +\n                \"Remove some collections.\";\n            return false;\n        }\n\n        if (!license.UseGroups && UseGroups && GroupCount > 1)\n        {\n            exception = $\"Your organization currently has {GroupCount} groups. \" +\n                $\"Your new license does not allow for the use of groups. Remove all groups.\";\n            return false;\n        }\n\n        var enabledPolicyCount = Policies.Count(p => p.Enabled);\n        if (!license.UsePolicies && UsePolicies && enabledPolicyCount > 0)\n        {\n            exception = $\"Your organization currently has {enabledPolicyCount} enabled \" +\n                $\"policies. Your new license does not allow for the use of policies. Disable all policies.\";\n            return false;\n        }\n\n        if (!license.UseSso && UseSso && SsoConfig is { Enabled: true })\n        {\n            exception = $\"Your organization currently has a SSO configuration. \" +\n                $\"Your new license does not allow for the use of SSO. Disable your SSO configuration.\";\n            return false;\n        }\n\n        if (!license.UseKeyConnector && UseKeyConnector && SsoConfig?.Data != null &&\n            SsoConfig.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector)\n        {\n            exception = $\"Your organization currently has Key Connector enabled. \" +\n                $\"Your new license does not allow for the use of Key Connector. Disable your Key Connector.\";\n            return false;\n        }\n\n        if (!license.UseScim && UseScim && ScimConnections != null &&\n            ScimConnections.Any(c => c.GetConfig<ScimConfig>() is { Enabled: true }))\n        {\n            exception = \"Your new plan does not allow the SCIM feature. \" +\n                \"Disable your SCIM configuration.\";\n            return false;\n        }\n\n        if (!license.UseCustomPermissions && UseCustomPermissions &&\n            OrganizationUsers.Any(ou => ou.Type == OrganizationUserType.Custom))\n        {\n            exception = \"Your new plan does not allow the Custom Permissions feature. \" +\n                \"Disable your Custom Permissions configuration.\";\n            return false;\n        }\n\n        if (!license.UseResetPassword && UseResetPassword &&\n            Policies.Any(p => p.Type == PolicyType.ResetPassword && p.Enabled))\n        {\n            exception = \"Your new license does not allow the Password Reset feature. \"\n                + \"Disable your Password Reset policy.\";\n            return false;\n        }\n\n        exception = \"\";\n        return true;\n    }\n\n    public Organization ToOrganization()\n    {\n        // Any new Organization properties must be added here for them to flow through to self-hosted organizations\n        return new Organization\n        {\n            Id = Id,\n            Identifier = Identifier,\n            Name = Name,\n            BusinessName = BusinessName,\n            BusinessAddress1 = BusinessAddress1,\n            BusinessAddress2 = BusinessAddress2,\n            BusinessAddress3 = BusinessAddress3,\n            BusinessCountry = BusinessCountry,\n            BusinessTaxNumber = BusinessTaxNumber,\n            BillingEmail = BillingEmail,\n            Plan = Plan,\n            PlanType = PlanType,\n            Seats = Seats,\n            MaxCollections = MaxCollections,\n            UsePolicies = UsePolicies,\n            UseSso = UseSso,\n            UseKeyConnector = UseKeyConnector,\n            UseScim = UseScim,\n            UseGroups = UseGroups,\n            UseDirectory = UseDirectory,\n            UseEvents = UseEvents,\n            UseTotp = UseTotp,\n            Use2fa = Use2fa,\n            UseApi = UseApi,\n            UseResetPassword = UseResetPassword,\n            UseSecretsManager = UseSecretsManager,\n            UsePasswordManager = UsePasswordManager,\n            SelfHost = SelfHost,\n            UsersGetPremium = UsersGetPremium,\n            UseCustomPermissions = UseCustomPermissions,\n            Storage = Storage,\n            MaxStorageGb = MaxStorageGb,\n            Gateway = Gateway,\n            GatewayCustomerId = GatewayCustomerId,\n            GatewaySubscriptionId = GatewaySubscriptionId,\n            ReferenceData = ReferenceData,\n            Enabled = Enabled,\n            LicenseKey = LicenseKey,\n            PublicKey = PublicKey,\n            PrivateKey = PrivateKey,\n            TwoFactorProviders = TwoFactorProviders,\n            ExpirationDate = ExpirationDate,\n            CreationDate = CreationDate,\n            RevisionDate = RevisionDate,\n            MaxAutoscaleSeats = MaxAutoscaleSeats,\n            OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,\n            LimitCollectionCreation = LimitCollectionCreation,\n            LimitCollectionDeletion = LimitCollectionDeletion,\n            LimitItemDeletion = LimitItemDeletion,\n            AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,\n            Status = Status,\n            UseRiskInsights = UseRiskInsights,\n            UseAdminSponsoredFamilies = UseAdminSponsoredFamilies,\n            UseDisableSmAdsForUsers = UseDisableSmAdsForUsers,\n            UsePhishingBlocker = UsePhishingBlocker,\n            UseOrganizationDomains = UseOrganizationDomains,\n            UseAutomaticUserConfirmation = UseAutomaticUserConfirmation,\n            UseMyItems = UseMyItems,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Permissions.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.Auth.Identity;\n\nnamespace Bit.Core.Models.Data;\n\npublic class Permissions\n{\n    public bool AccessEventLogs { get; set; }\n    public bool AccessImportExport { get; set; }\n    public bool AccessReports { get; set; }\n    public bool CreateNewCollections { get; set; }\n    public bool EditAnyCollection { get; set; }\n    public bool DeleteAnyCollection { get; set; }\n    public bool ManageGroups { get; set; }\n    public bool ManagePolicies { get; set; }\n    public bool ManageSso { get; set; }\n    public bool ManageUsers { get; set; }\n    public bool ManageResetPassword { get; set; }\n    public bool ManageScim { get; set; }\n\n    [JsonIgnore]\n    public List<(bool Permission, string ClaimName)> ClaimsMap => new()\n    {\n        (AccessEventLogs, Claims.CustomPermissions.AccessEventLogs),\n        (AccessImportExport, Claims.CustomPermissions.AccessImportExport),\n        (AccessReports, Claims.CustomPermissions.AccessReports),\n        (CreateNewCollections, Claims.CustomPermissions.CreateNewCollections),\n        (EditAnyCollection, Claims.CustomPermissions.EditAnyCollection),\n        (DeleteAnyCollection, Claims.CustomPermissions.DeleteAnyCollection),\n        (ManageGroups, Claims.CustomPermissions.ManageGroups),\n        (ManagePolicies, Claims.CustomPermissions.ManagePolicies),\n        (ManageSso, Claims.CustomPermissions.ManageSso),\n        (ManageUsers, Claims.CustomPermissions.ManageUsers),\n        (ManageResetPassword, Claims.CustomPermissions.ManageResetPassword),\n        (ManageScim, Claims.CustomPermissions.ManageScim),\n    };\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Provider/ProviderAbility.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Models.Data.Provider;\n\npublic class ProviderAbility\n{\n    public ProviderAbility() { }\n\n    public ProviderAbility(Entities.Provider.Provider provider)\n    {\n        Id = provider.Id;\n        UseEvents = provider.UseEvents;\n        Enabled = provider.Enabled;\n    }\n\n    public Guid Id { get; set; }\n    public bool UseEvents { get; set; }\n    public bool Enabled { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Provider;\n\npublic class ProviderOrganizationOrganizationDetails\n{\n    public Guid Id { get; set; }\n    public Guid ProviderId { get; set; }\n    public Guid OrganizationId { get; set; }\n    /// <summary>\n    /// This value is HTML encoded. For display purposes use the method DisplayName() instead.\n    /// </summary>\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string OrganizationName { get; set; }\n    public string Key { get; set; }\n    public string Settings { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime RevisionDate { get; set; }\n    public int UserCount { get; set; }\n    public int? OccupiedSeats { get; set; }\n    public int? Seats { get; set; }\n    public string Plan { get; set; }\n    public PlanType PlanType { get; set; }\n    public OrganizationStatusType Status { get; set; }\n\n    /// <summary>\n    /// Returns the name of the organization, HTML decoded ready for display.\n    /// </summary>\n    public string DisplayName()\n    {\n        return WebUtility.HtmlDecode(OrganizationName);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationProviderDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Provider;\n\npublic class ProviderOrganizationProviderDetails\n{\n    public Guid Id { get; set; }\n    public Guid ProviderId { get; set; }\n    public Guid OrganizationId { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string ProviderName { get; set; }\n    public ProviderType ProviderType { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Provider;\n\npublic class ProviderUserOrganizationDetails : IProfileOrganizationDetails\n{\n    public Guid OrganizationId { get; set; }\n    public Guid? UserId { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; } = null!;\n    public bool UsePolicies { get; set; }\n    public bool UseSso { get; set; }\n    public bool UseKeyConnector { get; set; }\n    public bool UseScim { get; set; }\n    public bool UseGroups { get; set; }\n    public bool UseDirectory { get; set; }\n    public bool UseEvents { get; set; }\n    public bool UseTotp { get; set; }\n    public bool Use2fa { get; set; }\n    public bool UseApi { get; set; }\n    public bool UseResetPassword { get; set; }\n    public bool SelfHost { get; set; }\n    public bool UsersGetPremium { get; set; }\n    public bool UseCustomPermissions { get; set; }\n    public bool UseSecretsManager { get; set; }\n    public bool UsePasswordManager { get; set; }\n    public int? Seats { get; set; }\n    public short? MaxCollections { get; set; }\n    public short? MaxStorageGb { get; set; }\n    public string? Key { get; set; }\n    public ProviderUserStatusType Status { get; set; }\n    public ProviderUserType Type { get; set; }\n    public bool Enabled { get; set; }\n    public string? Identifier { get; set; }\n    public string? PublicKey { get; set; }\n    public string? PrivateKey { get; set; }\n    public Guid? ProviderId { get; set; }\n    public Guid? ProviderUserId { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string? ProviderName { get; set; }\n    public PlanType PlanType { get; set; }\n    public bool LimitCollectionCreation { get; set; }\n    public bool LimitCollectionDeletion { get; set; }\n    public bool LimitItemDeletion { get; set; }\n    public bool AllowAdminAccessToAllCollectionItems { get; set; }\n    public bool UseRiskInsights { get; set; }\n    public bool UseOrganizationDomains { get; set; }\n    public bool UseAdminSponsoredFamilies { get; set; }\n    public ProviderType? ProviderType { get; set; }\n    public bool UseAutomaticUserConfirmation { get; set; }\n    public bool? SsoEnabled { get; set; }\n    public string? SsoConfig { get; set; }\n    public string? SsoExternalId { get; set; }\n    public string? Permissions { get; set; }\n    public string? ResetPasswordKey { get; set; }\n    public bool UseDisableSMAdsForUsers { get; set; }\n    public bool UsePhishingBlocker { get; set; }\n    public bool UseMyItems { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Provider/ProviderUserProviderDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Provider;\n\npublic class ProviderUserProviderDetails\n{\n    public Guid ProviderId { get; set; }\n    public Guid? UserId { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; }\n    public string Key { get; set; }\n    public ProviderUserStatusType Status { get; set; }\n    public ProviderUserType Type { get; set; }\n    public bool Enabled { get; set; }\n    public string Permissions { get; set; }\n    public bool UseEvents { get; set; }\n    public ProviderStatusType ProviderStatus { get; set; }\n    public ProviderType ProviderType { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Provider/ProviderUserPublicKey.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.AdminConsole.Models.Data.Provider;\n\npublic class ProviderUserPublicKey\n{\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public string PublicKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/Provider/ProviderUserUserDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.Models.Data.Provider;\n\npublic class ProviderUserUserDetails\n{\n    public Guid Id { get; set; }\n    public Guid ProviderId { get; set; }\n    public Guid? UserId { get; set; }\n    [JsonConverter(typeof(HtmlEncodingStringConverter))]\n    public string Name { get; set; }\n    public string Email { get; set; }\n    public ProviderUserStatusType Status { get; set; }\n    public ProviderUserType Type { get; set; }\n    public string Permissions { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/StandardUser.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.Models.Data;\n\npublic class StandardUser : IActingUser\n{\n    public StandardUser(Guid userId, bool isOrganizationOwner)\n    {\n        UserId = userId;\n        IsOrganizationOwnerOrProvider = isOrganizationOwner;\n    }\n\n    public Guid? UserId { get; }\n    public bool IsOrganizationOwnerOrProvider { get; }\n    public EventSystemUser? SystemUserType => throw new Exception($\"{nameof(StandardUser)} does not have a {nameof(SystemUserType)}\");\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Data/SystemUser.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.Models.Data;\n\npublic class SystemUser : IActingUser\n{\n    public SystemUser(EventSystemUser systemUser)\n    {\n        SystemUserType = systemUser;\n    }\n\n    public Guid? UserId => throw new Exception($\"{nameof(SystemUserType)} does not have a {nameof(UserId)}.\");\n\n    public bool IsOrganizationOwnerOrProvider => false;\n    public EventSystemUser? SystemUserType { get; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/DeviceApprovalRequestedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Mail;\n\nnamespace Bit.Core.AdminConsole.Models.Mail;\n\npublic class DeviceApprovalRequestedViewModel : BaseMailModel\n{\n    public Guid OrganizationId { get; set; }\n    public string UserNameRequestingAccess { get; set; }\n\n    public string Url => string.Format(\"{0}/organizations/{1}/settings/device-approvals\",\n        WebVaultUrl,\n        OrganizationId);\n}\n\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationBaseView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;\n\npublic abstract class OrganizationConfirmationBaseView : BaseMailView\n{\n    public required string OrganizationName { get; set; }\n    public required string TitleFirst { get; set; }\n    public required string TitleSecondBold { get; set; }\n    public required string TitleThird { get; set; }\n    public required string WebVaultUrl { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;\n\npublic class OrganizationConfirmationEnterpriseTeamsView : OrganizationConfirmationBaseView\n{\n}\n\npublic class OrganizationConfirmationEnterpriseTeams : BaseMail<OrganizationConfirmationEnterpriseTeamsView>\n{\n    public override required string Subject { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    <!-- Include shared head styles -->\n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              You can now share passwords with members of <b>{{OrganizationName}}!</b>\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{WebVaultUrl}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>Log in</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/ac-spot-enterprise.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">As a member of <b>{{ OrganizationName }}</b>:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Organization Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-enterprise.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Your account is owned by {{OrganizationName}} and is subject to their security and management policies.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">You can easily access and share passwords with your team.</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/help/sharing\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Share passwords in Bitwarden\n              </a></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n    You may now access logins and other items {{OrganizationName}} has shared with you from your Bitwarden vault.\n    Tip: Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the App Store or Google Play.\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationConfirmation;\n\npublic class OrganizationConfirmationFamilyFreeView : OrganizationConfirmationBaseView\n{\n}\n\npublic class OrganizationConfirmationFamilyFree : BaseMail<OrganizationConfirmationFamilyFreeView>\n{\n    public override required string Subject { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n.mj-column-px-159 { width:159px !important; max-width: 159px; }\n.mj-column-px-140 { width:140px !important; max-width: 140px; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n.moz-text-html .mj-column-px-159 { width:159px !important; max-width: 159px; }\n.moz-text-html .mj-column-px-140 { width:140px !important; max-width: 140px; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n@media only screen and (max-width: 480px) {\n    .hide-mobile {\n      display: none !important;\n    }\n  }\n    </style>\n    <!-- Include shared head styles -->\n<!-- Include admin console shared styles -->\n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              You can now share passwords with members of <b>{{OrganizationName}}!</b>\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{WebVaultUrl}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>Log in</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/ac-spot-family.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">As a member of <b>{{ OrganizationName }}</b>:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Group Users Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">You can access passwords {{OrganizationName}} has shared with you.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">You can easily share passwords with friends, family, or coworkers.</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/help/sharing\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Share passwords in Bitwarden\n              </a></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Download Mobile Apps Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:32px 10px 0px 25px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:585px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0 0 16px 0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:18px;font-weight:500;line-height:24px;text-align:left;color:#1B2029;\">Download Bitwarden on all devices</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0 0 24px 0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Already using the\n            <a href=\"https://bitwarden.com/download/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">browser extension</a>? Download the Bitwarden mobile app from the\n            <a href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">App Store</a>\n            or\n            <a href=\"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">Google Play</a>\n            to quickly save logins and autofill forms on the go.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0 10px 32px 25px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"width:585px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:159px;\" ><![endif]-->\n                \n      <div class=\"mj-column-px-159 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:27.17948717948718%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"hide-mobile\" style=\"font-size:0px;padding:0 24px 0 0;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:135px;\">\n              \n        <a href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\" target=\"_blank\">\n          \n      <img alt=\"Download on the App Store\" src=\"https://assets.bitwarden.com/email/v1/ac-apple-store.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:40px;width:100%;font-size:16px;\" width=\"135\" height=\"40\">\n    \n        </a>\n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:140px;\" ><![endif]-->\n                \n      <div class=\"mj-column-px-140 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:23.931623931623932%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"hide-mobile\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:140px;\">\n              \n        <a href=\"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden\" target=\"_blank\">\n          \n      <img alt=\"Get it on Google Play\" src=\"https://assets.bitwarden.com/email/v1/ac-google-play.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:40px;width:100%;font-size:16px;\" width=\"140\" height=\"40\">\n    \n        </a>\n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n    You may now access logins and other items {{OrganizationName}} has shared with you from your Bitwarden vault.\n    Tip: Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the App Store or Google Play.\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteBaseView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite;\n\npublic abstract class OrganizationInviteBaseView : BaseMailView\n{\n    public required string OrganizationName { get; set; }\n    public required string Email { get; set; }\n    public required string ExpirationDate { get; set; }\n    public required string Url { get; set; }\n    public required string ButtonText { get; set; }\n    public string? InviterEmail { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite;\n\npublic class OrganizationInviteEnterpriseTeamsExistingUserView : OrganizationInviteBaseView\n{\n}\n\npublic class OrganizationInviteEnterpriseTeamsExistingUser : BaseMail<OrganizationInviteEnterpriseTeamsExistingUserView>\n{\n    public override required string Subject { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              <b>{{OrganizationName}}</b> invited you to join them on Bitwarden\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-enterprise.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><b>{{OrganizationName}}</b> is rolling out Bitwarden to increase security and protect your sensitive data. Once you accept this invitation, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Store logins securely so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Store logins securely so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your team.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your team.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Policy Warning Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 8px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:18px;font-weight:500;line-height:28px;text-align:left;color:#1B2029;\">Your existing account will be owned by {{OrganizationName}}</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">By accepting this invitation, your account ({{Email}}) will be owned by <b>{{OrganizationName}}</b> and will be subject to their security and management policies. Contact your administrator with any questions or concerns.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you accept this invitation, you can:\n\n- Store logins securely so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your team.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nYour existing account will be owned by {{OrganizationName}}\n\nBy accepting this invitation, your account ({{Email}}) will be owned by {{OrganizationName}} and will be subject to their security and management policies. Contact your administrator with any questions or concerns.\n\nAccept invitation: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite;\n\npublic class OrganizationInviteEnterpriseTeamsNewUserView : OrganizationInviteBaseView\n{\n}\n\npublic class OrganizationInviteEnterpriseTeamsNewUser : BaseMail<OrganizationInviteEnterpriseTeamsNewUserView>\n{\n    public override required string Subject { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              <b>{{OrganizationName}}</b> set up a Bitwarden password manager account for you.\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-enterprise.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><b>{{OrganizationName}}</b> is rolling out Bitwarden to increase security and protect your sensitive data. Once you finish account setup, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Store logins securely so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Store logins securely so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your team.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your team.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you finish account setup, you can:\n\n- Store logins securely so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your team.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nFinish account setup: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite;\n\npublic class OrganizationInviteFamiliesExistingUserView : OrganizationInviteBaseView\n{\n}\n\npublic class OrganizationInviteFamiliesExistingUser : BaseMail<OrganizationInviteFamiliesExistingUserView>\n{\n    public override required string Subject { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              <b>{{OrganizationName}}</b> invited you to join them on Bitwarden\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-family-homes.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><b>{{OrganizationName}}</b> is using Bitwarden to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Store logins securely so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Store logins securely so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your friends, family, or coworkers.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your friends, family, or coworkers.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:\n\n- Store logins securely so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your friends, family, or coworkers.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nAccept invitation: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite;\n\npublic class OrganizationInviteFamiliesNewUserView : OrganizationInviteBaseView\n{\n}\n\npublic class OrganizationInviteFamiliesNewUser : BaseMail<OrganizationInviteFamiliesNewUserView>\n{\n    public override required string Subject { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              <b>{{OrganizationName}}</b> set up a Bitwarden password manager account for you.\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-family-homes.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><b>{{OrganizationName}}</b> is using Bitwarden to simplify password sharing and protect your sensitive data. Once you finish account setup, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Store logins securely so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Store logins securely so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your friends, family, or coworkers.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your friends, family, or coworkers.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFamiliesNewUserView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you finish account setup, you can:\n\n- Store logins securely so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your friends, family, or coworkers.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nFinish account setup: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationInvite;\n\npublic class OrganizationInviteFreeView : OrganizationInviteBaseView\n{\n}\n\npublic class OrganizationInviteFree : BaseMail<OrganizationInviteFreeView>\n{\n    public override required string Subject { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              You have been invited to Bitwarden Password Manager\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-family-homes.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">Bitwarden is a password manager used to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Securely store logins so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Securely store logins so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your friends, family, or coworkers.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your friends, family, or coworkers.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationInvite/OrganizationInviteFreeView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\nBitwarden is a password manager used to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:\n\n- Securely store logins so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your friends, family, or coworkers.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nAccept invitation: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationUserAutoConfirmation;\n\npublic class OrganizationAutoConfirmationEnabledView : BaseMailView\n{\n    public required string WebVaultUrl { get; set; }\n}\n\npublic class OrganizationAutoConfirmationEnabled : BaseMail<OrganizationAutoConfirmationEnabledView>\n{\n    public override required string Subject { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n        #outlook a { padding:0; }\n        body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n        table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n        img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n        p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n        <xml>\n            <o:OfficeDocumentSettings>\n                <o:AllowPNG/>\n                <o:PixelsPerInch>96</o:PixelsPerInch>\n            </o:OfficeDocumentSettings>\n        </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n        .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n\n    <!--[if !mso]><!-->\n    <link href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700\" rel=\"stylesheet\" type=\"text/css\">\n    <style type=\"text/css\">\n        @import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500,700);\n    </style>\n    <!--<![endif]-->\n\n\n\n    <style type=\"text/css\">\n        @media only screen and (min-width:480px) {\n            .mj-column-per-100 { width:100% !important; max-width: 100%; }\n        }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n        .moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n    </style>\n\n\n\n\n    <style type=\"text/css\">\n\n        @media only screen and (max-width:479px) {\n            table.mj-full-width-mobile { width: 100% !important; }\n            td.mj-full-width-mobile { width: auto !important; }\n        }\n\n    </style>\n\n    <style type=\"text/css\">\n        .border-fix > table {\n            border-collapse: separate !important;\n        }\n        .border-fix > table > tbody > tr > td {\n            border-radius: 3px;\n        }\n    </style>\n\n</head>\n<body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n\n\n<div style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n    <!-- Blue Header Section -->\n\n    <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n\n\n    <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n\n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n            <tbody>\n            <tr>\n                <td style=\"direction:ltr;font-size:0px;padding:20px 24px 0px 24px;text-align:center;\">\n                    <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n\n                    <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n                        <tbody>\n                        <tr>\n                            <td>\n\n\n                                <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:612px;\" width=\"612\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n\n\n                                <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:612px;\">\n\n                                    <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n                                        <tbody>\n                                        <tr>\n                                            <td style=\"direction:ltr;font-size:0px;padding:20px 20px;text-align:center;\">\n                                                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:572px;\" ><![endif]-->\n\n                                                <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n\n                                                    <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n                                                        <tbody>\n\n                                                        <tr>\n                                                            <td align=\"left\" style=\"font-size:0px;padding:10px 5px;word-break:break-word;\">\n\n                                                                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n                                                                    <tbody>\n                                                                    <tr>\n                                                                        <td style=\"width:150px;\">\n\n                                                                            <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n\n                                                                        </td>\n                                                                    </tr>\n                                                                    </tbody>\n                                                                </table>\n\n                                                            </td>\n                                                        </tr>\n\n                                                        </tbody>\n                                                    </table>\n\n                                                </div>\n\n                                                <!--[if mso | IE]></td></tr></table><![endif]-->\n                                            </td>\n                                        </tr>\n                                        </tbody>\n                                    </table>\n\n                                </div>\n\n\n                                <!--[if mso | IE]></td></tr></table><![endif]-->\n\n\n                            </td>\n                        </tr>\n                        </tbody>\n                    </table>\n\n                    <!--[if mso | IE]></td></tr></table><![endif]-->\n                </td>\n            </tr>\n            </tbody>\n        </table>\n\n    </div>\n\n\n    <!--[if mso | IE]></td></tr></table><![endif]-->\n\n    <!-- Main Content -->\n\n    <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n\n\n    <div style=\"margin:0px auto;max-width:660px;\">\n\n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n            <tbody>\n            <tr>\n                <td style=\"direction:ltr;font-size:0px;padding:0px 25px;text-align:center;\">\n                    <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:610px;\" width=\"610\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n\n\n                    <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:610px;\">\n\n                        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n                            <tbody>\n                            <tr>\n                                <td style=\"direction:ltr;font-size:0px;padding:24px 0px;text-align:center;\">\n                                    <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:610px;\" ><![endif]-->\n\n                                    <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n\n                                        <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n                                            <tbody>\n                                            <tr>\n                                                <td style=\"vertical-align:top;padding:0px 25px;\">\n\n                                                    <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n                                                        <tbody>\n\n                                                        <tr>\n                                                            <td align=\"left\" style=\"font-size:0px;padding:0px 0px 24px 0px;word-break:break-word;\">\n\n                                                                <div style=\"font-family:roboto;font-size:16px;font-weight:700;line-height:24px;text-align:left;color:#1B2029;\">Automatic user confirmation is now available!</div>\n\n                                                            </td>\n                                                        </tr>\n\n                                                        <tr>\n                                                            <td align=\"left\" style=\"font-size:0px;padding:0px 0px 24px 0px;word-break:break-word;\">\n\n                                                                <div style=\"font-family:roboto;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">A new policy is available for your organization. It allows new users\n                                                                    to be automatically confirmed while an admin’s device is unlocked.\n                                                                    Log in to the web app to turn on the policy.</div>\n\n                                                            </td>\n                                                        </tr>\n\n                                                        <tr>\n                                                            <td align=\"left\" style=\"font-size:0px;padding:0px 0px 24px 0px;word-break:break-word;\">\n\n                                                                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n                                                                    <tbody>\n                                                                    <tr>\n                                                                        <td align=\"center\" bgcolor=\"#175ddc\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#175ddc;\" valign=\"middle\">\n                                                                            <a href=\"{{WebVaultUrl}}\" style=\"display:inline-block;background:#175ddc;color:#ffffff;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:600;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                                                                                Log in\n                                                                            </a>\n                                                                        </td>\n                                                                    </tr>\n                                                                    </tbody>\n                                                                </table>\n\n                                                            </td>\n                                                        </tr>\n\n                                                        <tr>\n                                                            <td align=\"left\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n\n                                                                <div style=\"font-family:roboto;font-size:13px;font-weight:700;line-height:16px;text-align:left;color:#1B2029;\"><a class=\"link\" href=\"https://bitwarden.com/help/automatic-confirmation/\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">Learn more about this policy</a></div>\n\n                                                            </td>\n                                                        </tr>\n\n                                                        </tbody>\n                                                    </table>\n\n                                                </td>\n                                            </tr>\n                                            </tbody>\n                                        </table>\n\n                                    </div>\n\n                                    <!--[if mso | IE]></td></tr></table><![endif]-->\n                                </td>\n                            </tr>\n                            </tbody>\n                        </table>\n\n                    </div>\n\n\n                    <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n                </td>\n            </tr>\n            </tbody>\n        </table>\n\n    </div>\n\n\n    <!--[if mso | IE]></td></tr></table><![endif]-->\n\n    <!-- Footer -->\n\n    <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n\n\n    <div style=\"margin:0px auto;max-width:660px;\">\n\n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n            <tbody>\n            <tr>\n                <td style=\"direction:ltr;font-size:0px;padding:20px 0;padding-top:10px;text-align:center;\">\n                    <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n\n\n                    <div style=\"margin:0px auto;max-width:660px;\">\n\n                        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n                            <tbody>\n                            <tr>\n                                <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                                    <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n\n                                    <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n\n                                        <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n                                            <tbody>\n\n                                            <tr>\n                                                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n\n\n                                                    <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n                                                    <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                                                        <tbody>\n\n                                                        <tr>\n                                                            <td style=\"padding:8px;vertical-align:middle;\">\n                                                                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n                                                                    <tbody>\n                                                                    <tr>\n                                                                        <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                                                                            <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                                                                                <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                                                                            </a>\n                                                                        </td>\n                                                                    </tr>\n                                                                    </tbody>\n                                                                </table>\n                                                            </td>\n\n                                                        </tr>\n\n                                                        </tbody>\n                                                    </table>\n                                                    <!--[if mso | IE]></td><td><![endif]-->\n                                                    <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                                                        <tbody>\n\n                                                        <tr>\n                                                            <td style=\"padding:8px;vertical-align:middle;\">\n                                                                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n                                                                    <tbody>\n                                                                    <tr>\n                                                                        <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                                                                            <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                                                                                <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                                                                            </a>\n                                                                        </td>\n                                                                    </tr>\n                                                                    </tbody>\n                                                                </table>\n                                                            </td>\n\n                                                        </tr>\n\n                                                        </tbody>\n                                                    </table>\n                                                    <!--[if mso | IE]></td><td><![endif]-->\n                                                    <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                                                        <tbody>\n\n                                                        <tr>\n                                                            <td style=\"padding:8px;vertical-align:middle;\">\n                                                                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n                                                                    <tbody>\n                                                                    <tr>\n                                                                        <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                                                                            <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                                                                                <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                                                                            </a>\n                                                                        </td>\n                                                                    </tr>\n                                                                    </tbody>\n                                                                </table>\n                                                            </td>\n\n                                                        </tr>\n\n                                                        </tbody>\n                                                    </table>\n                                                    <!--[if mso | IE]></td><td><![endif]-->\n                                                    <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                                                        <tbody>\n\n                                                        <tr>\n                                                            <td style=\"padding:8px;vertical-align:middle;\">\n                                                                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n                                                                    <tbody>\n                                                                    <tr>\n                                                                        <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                                                                            <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                                                                                <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                                                                            </a>\n                                                                        </td>\n                                                                    </tr>\n                                                                    </tbody>\n                                                                </table>\n                                                            </td>\n\n                                                        </tr>\n\n                                                        </tbody>\n                                                    </table>\n                                                    <!--[if mso | IE]></td><td><![endif]-->\n                                                    <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                                                        <tbody>\n\n                                                        <tr>\n                                                            <td style=\"padding:8px;vertical-align:middle;\">\n                                                                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n                                                                    <tbody>\n                                                                    <tr>\n                                                                        <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                                                                            <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                                                                                <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                                                                            </a>\n                                                                        </td>\n                                                                    </tr>\n                                                                    </tbody>\n                                                                </table>\n                                                            </td>\n\n                                                        </tr>\n\n                                                        </tbody>\n                                                    </table>\n                                                    <!--[if mso | IE]></td><td><![endif]-->\n                                                    <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                                                        <tbody>\n\n                                                        <tr>\n                                                            <td style=\"padding:8px;vertical-align:middle;\">\n                                                                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n                                                                    <tbody>\n                                                                    <tr>\n                                                                        <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                                                                            <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                                                                                <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                                                                            </a>\n                                                                        </td>\n                                                                    </tr>\n                                                                    </tbody>\n                                                                </table>\n                                                            </td>\n\n                                                        </tr>\n\n                                                        </tbody>\n                                                    </table>\n                                                    <!--[if mso | IE]></td><td><![endif]-->\n                                                    <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                                                        <tbody>\n\n                                                        <tr>\n                                                            <td style=\"padding:8px;vertical-align:middle;\">\n                                                                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n                                                                    <tbody>\n                                                                    <tr>\n                                                                        <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                                                                            <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                                                                                <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                                                                            </a>\n                                                                        </td>\n                                                                    </tr>\n                                                                    </tbody>\n                                                                </table>\n                                                            </td>\n\n                                                        </tr>\n\n                                                        </tbody>\n                                                    </table>\n                                                    <!--[if mso | IE]></td></tr></table><![endif]-->\n\n\n                                                </td>\n                                            </tr>\n\n                                            <tr>\n                                                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n\n                                                    <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n                                                        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n                                                        Barbara, CA, USA\n                                                    </p>\n                                                        <p style=\"margin-top: 5px\">\n                                                            Always confirm you are on a trusted Bitwarden domain before logging\n                                                            in:<br>\n                                                            <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n                                                            <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n                                                        </p></div>\n\n                                                </td>\n                                            </tr>\n\n                                            </tbody>\n                                        </table>\n\n                                    </div>\n\n                                    <!--[if mso | IE]></td></tr></table><![endif]-->\n                                </td>\n                            </tr>\n                            </tbody>\n                        </table>\n\n                    </div>\n\n\n                    <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n                </td>\n            </tr>\n            </tbody>\n        </table>\n\n    </div>\n\n\n    <!--[if mso | IE]></td></tr></table><![endif]-->\n\n\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/Mail/Mailer/OrganizationUserAutoConfirmation/OrganizationAutoConfirmationEnabledView.text.hbs",
    "content": "{{#>BasicTextLayout}}\nAutomatic user confirmation is now available!\n\nA new policy is available for your organization. It allows new users to be automatically confirmed while an\nadmin’s device is unlocked. Log in to the web app to turn on the policy.\n\nLearn more about this policy here: https://bitwarden.com/help/automatic-confirmation/\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/AdminConsole/Models/OrganizationConnectionConfigs/ScimConfig.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\n\nnamespace Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\n\npublic class ScimConfig : IConnectionConfig\n{\n    public bool Enabled { get; set; }\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public ScimProviderType? ScimProvider { get; set; }\n\n    public bool Validate(out string exception)\n    {\n        if (!Enabled)\n        {\n            exception = \"Scim Config is disabled\";\n            return false;\n        }\n\n        exception = \"\";\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationAuth/Interfaces/IUpdateOrganizationAuthRequestCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationAuth.Models;\n\nnamespace Bit.Core.AdminConsole.OrganizationAuth.Interfaces;\n\npublic interface IUpdateOrganizationAuthRequestCommand\n{\n    Task UpdateAsync(Guid requestId, Guid userId, bool requestApproved, string encryptedUserKey);\n    Task UpdateAsync(Guid organizationId, IEnumerable<OrganizationAuthRequestUpdate> authRequestUpdates);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationAuth/Models/ApprovedAuthRequestIsMissingKeyException.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationAuth.Models;\n\npublic class ApprovedAuthRequestIsMissingKeyException : AuthRequestUpdateProcessingException\n{\n    public ApprovedAuthRequestIsMissingKeyException(Guid id)\n        : base($\"An auth request with id {id} was approved, but no key was provided. This auth request can not be approved.\")\n    {\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationAuth/Models/AuthRequestUpdateCouldNotBeProcessedException.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationAuth.Models;\n\npublic class AuthRequestUpdateCouldNotBeProcessedException : AuthRequestUpdateProcessingException\n{\n    public AuthRequestUpdateCouldNotBeProcessedException()\n        : base($\"An auth request could not be processed.\")\n    {\n    }\n\n    public AuthRequestUpdateCouldNotBeProcessedException(Guid id)\n        : base($\"An auth request with id {id} could not be processed.\")\n    {\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationAuth/Models/AuthRequestUpdateProcessingException.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationAuth.Models;\n\npublic class AuthRequestUpdateProcessingException : Exception\n{\n    public AuthRequestUpdateProcessingException() { }\n\n    public AuthRequestUpdateProcessingException(string message)\n        : base(message) { }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationAuth/Models/AuthRequestUpdateProcessor.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Reflection;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationAuth.Models;\n\npublic class AuthRequestUpdateProcessor\n{\n    public OrganizationAdminAuthRequest ProcessedAuthRequest { get; private set; }\n\n    private OrganizationAdminAuthRequest _unprocessedAuthRequest { get; }\n    private OrganizationAuthRequestUpdate _update { get; }\n    private AuthRequestUpdateProcessorConfiguration _configuration { get; }\n\n    public EventType OrganizationEventType => ProcessedAuthRequest?.Approved.Value ?? false\n        ? EventType.OrganizationUser_ApprovedAuthRequest\n        : EventType.OrganizationUser_RejectedAuthRequest;\n\n    public AuthRequestUpdateProcessor(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration configuration\n    )\n    {\n        _unprocessedAuthRequest = authRequest;\n        _update = update;\n        _configuration = configuration;\n    }\n\n    public void Process()\n    {\n        if (_unprocessedAuthRequest == null)\n        {\n            throw new AuthRequestUpdateCouldNotBeProcessedException();\n        }\n        var isExpired = DateTime.UtcNow >\n            _unprocessedAuthRequest.CreationDate\n            .Add(_configuration.AuthRequestExpiresAfter);\n        var isSpent = _unprocessedAuthRequest.Approved != null ||\n            _unprocessedAuthRequest.ResponseDate.HasValue ||\n            _unprocessedAuthRequest.AuthenticationDate.HasValue;\n        var canBeProcessed = !isExpired &&\n            !isSpent &&\n            _unprocessedAuthRequest.Id == _update.Id &&\n            _unprocessedAuthRequest.OrganizationId == _configuration.OrganizationId;\n        if (!canBeProcessed)\n        {\n            throw new AuthRequestUpdateCouldNotBeProcessedException(_unprocessedAuthRequest.Id);\n        }\n        if (_update.Approved)\n        {\n            Approve();\n            return;\n        }\n        Deny();\n    }\n\n    public async Task SendPushNotification(Func<OrganizationAdminAuthRequest, Task> callback)\n    {\n        if (!ProcessedAuthRequest?.Approved ?? false)\n        {\n            return;\n        }\n        await callback(ProcessedAuthRequest);\n    }\n\n    public async Task SendApprovalEmail(Func<OrganizationAdminAuthRequest, string, Task> callback)\n    {\n        if (!ProcessedAuthRequest?.Approved ?? false)\n        {\n            return;\n        }\n        var deviceTypeDisplayName = _unprocessedAuthRequest.RequestDeviceType.GetType()\n            .GetMember(_unprocessedAuthRequest.RequestDeviceType.ToString())\n            .FirstOrDefault()?\n            // This unknown case can't be unit tested without adding an enum\n            // with no display attribute. Faith and trust are required!\n            .GetCustomAttribute<DisplayAttribute>()?.Name ?? \"Unknown Device Type\";\n        var deviceTypeAndIdentifierDisplayString =\n            string.IsNullOrWhiteSpace(_unprocessedAuthRequest.RequestDeviceIdentifier)\n                ? deviceTypeDisplayName\n                : $\"{deviceTypeDisplayName} - {_unprocessedAuthRequest.RequestDeviceIdentifier}\";\n        await callback(ProcessedAuthRequest, deviceTypeAndIdentifierDisplayString);\n    }\n\n    private void Approve()\n    {\n        if (string.IsNullOrWhiteSpace(_update.Key))\n        {\n            throw new ApprovedAuthRequestIsMissingKeyException(_update.Id);\n        }\n        ProcessedAuthRequest = _unprocessedAuthRequest;\n        ProcessedAuthRequest.Key = _update.Key;\n        ProcessedAuthRequest.Approved = true;\n        ProcessedAuthRequest.ResponseDate = DateTime.UtcNow;\n    }\n\n    private void Deny()\n    {\n        ProcessedAuthRequest = _unprocessedAuthRequest;\n        ProcessedAuthRequest.Approved = false;\n        ProcessedAuthRequest.ResponseDate = DateTime.UtcNow;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationAuth/Models/AuthRequestUpdateProcessorConfiguration.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationAuth.Models;\n\npublic class AuthRequestUpdateProcessorConfiguration\n{\n    public Guid OrganizationId { get; set; }\n    public TimeSpan AuthRequestExpiresAfter { get; set; }\n}\n\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationAuth/Models/BatchAuthRequestUpdateProcessor.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationAuth.Models;\n\npublic class BatchAuthRequestUpdateProcessor\n{\n    public List<AuthRequestUpdateProcessor> Processors { get; } = new List<AuthRequestUpdateProcessor>();\n    private List<AuthRequestUpdateProcessor> _processed => Processors\n        .Where(p => p.ProcessedAuthRequest != null)\n        .ToList();\n\n    public BatchAuthRequestUpdateProcessor(\n        ICollection<OrganizationAdminAuthRequest> authRequests,\n        IEnumerable<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration\n    )\n    {\n        Processors = authRequests?.Select(ar =>\n        {\n            return new AuthRequestUpdateProcessor(\n                ar,\n                updates.FirstOrDefault(u => u.Id == ar.Id),\n                configuration\n            );\n        }).ToList() ?? Processors;\n    }\n\n    public BatchAuthRequestUpdateProcessor Process(Action<Exception> errorHandlerCallback)\n    {\n        foreach (var processor in Processors)\n        {\n            try\n            {\n                processor.Process();\n            }\n            catch (AuthRequestUpdateProcessingException e)\n            {\n                errorHandlerCallback(e);\n            }\n        }\n        return this;\n    }\n\n    public async Task Save(Func<IEnumerable<OrganizationAdminAuthRequest>, Task> callback)\n    {\n        if (_processed.Any())\n        {\n            await callback(_processed.Select(p => p.ProcessedAuthRequest));\n        }\n    }\n\n    // Currently push notifications and emails are still done per-request in\n    // a loop, which is different than saving updates to the database and\n    // raising organization events. These can be done in bulk all the way\n    // through to the repository.\n    //\n    // Adding bulk notification and email methods is being tracked as tech\n    // debt on https://bitwarden.atlassian.net/browse/AC-2629\n    public async Task SendPushNotifications(Func<OrganizationAdminAuthRequest, Task> callback)\n    {\n        foreach (var processor in _processed)\n        {\n            await processor.SendPushNotification(callback);\n        }\n    }\n\n    public async Task SendApprovalEmailsForProcessedRequests(Func<OrganizationAdminAuthRequest, string, Task> callback)\n    {\n        foreach (var processor in _processed)\n        {\n            await processor.SendApprovalEmail(callback);\n        }\n    }\n\n    public async Task LogOrganizationEventsForProcessedRequests(Func<IEnumerable<(OrganizationAdminAuthRequest, EventType)>, Task> callback)\n    {\n        if (_processed.Any())\n        {\n            await callback(_processed.Select(p =>\n            {\n                return (p.ProcessedAuthRequest, p.OrganizationEventType);\n            }));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationAuth/Models/OrganizationAuthRequestUpdate.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.AdminConsole.OrganizationAuth.Models;\n\npublic class OrganizationAuthRequestUpdate\n{\n    public Guid Id { get; set; }\n    public bool Approved { get; set; }\n    public string Key { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationAuth/UpdateOrganizationAuthRequestCommand.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Reflection;\nusing Bit.Core.AdminConsole.OrganizationAuth.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationAuth.Models;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Api.Request.AuthRequest;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.AdminConsole.OrganizationAuth;\n\npublic class UpdateOrganizationAuthRequestCommand : IUpdateOrganizationAuthRequestCommand\n{\n    private readonly IAuthRequestService _authRequestService;\n    private readonly IMailService _mailService;\n    private readonly IUserRepository _userRepository;\n    private readonly ILogger<UpdateOrganizationAuthRequestCommand> _logger;\n    private readonly IAuthRequestRepository _authRequestRepository;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IEventService _eventService;\n\n    public UpdateOrganizationAuthRequestCommand(\n        IAuthRequestService authRequestService,\n        IMailService mailService,\n        IUserRepository userRepository,\n        ILogger<UpdateOrganizationAuthRequestCommand> logger,\n        IAuthRequestRepository authRequestRepository,\n        IGlobalSettings globalSettings,\n        IPushNotificationService pushNotificationService,\n        IOrganizationUserRepository organizationUserRepository,\n        IEventService eventService)\n    {\n        _authRequestService = authRequestService;\n        _mailService = mailService;\n        _userRepository = userRepository;\n        _logger = logger;\n        _authRequestRepository = authRequestRepository;\n        _globalSettings = globalSettings;\n        _pushNotificationService = pushNotificationService;\n        _organizationUserRepository = organizationUserRepository;\n        _eventService = eventService;\n    }\n\n    // TODO: When refactoring this method as a part of Bulk Device Approval\n    // post-release cleanup we should be able to construct a single\n    // AuthRequestProcessor and run its Process() Save() methods, and the\n    // various calls to send notifications.\n    public async Task UpdateAsync(Guid requestId, Guid userId, bool requestApproved, string encryptedUserKey)\n    {\n        var updatedAuthRequest = await _authRequestService.UpdateAuthRequestAsync(requestId, userId,\n            new AuthRequestUpdateRequestModel { RequestApproved = requestApproved, Key = encryptedUserKey });\n\n        if (updatedAuthRequest.Approved is true)\n        {\n            var user = await _userRepository.GetByIdAsync(userId);\n            if (user == null)\n            {\n                _logger.LogError(\"User ({id}) not found. Trusted device admin approval email not sent.\", userId);\n                return;\n            }\n            var approvalDateTime = updatedAuthRequest.ResponseDate ?? DateTime.UtcNow;\n            var deviceTypeDisplayName = updatedAuthRequest.RequestDeviceType.GetType()\n                .GetMember(updatedAuthRequest.RequestDeviceType.ToString())\n                .FirstOrDefault()?\n                .GetCustomAttribute<DisplayAttribute>()?.Name ?? \"Unknown\";\n            var deviceTypeAndIdentifier = $\"{deviceTypeDisplayName} - {updatedAuthRequest.RequestDeviceIdentifier}\";\n            await _mailService.SendTrustedDeviceAdminApprovalEmailAsync(user.Email, approvalDateTime,\n                updatedAuthRequest.RequestIpAddress, deviceTypeAndIdentifier);\n        }\n    }\n\n    public async Task UpdateAsync(Guid organizationId, IEnumerable<OrganizationAuthRequestUpdate> authRequestUpdates)\n    {\n        var authRequestEntities = await FetchManyOrganizationAuthRequestsFromTheDatabase(organizationId, authRequestUpdates.Select(aru => aru.Id));\n        var processor = new BatchAuthRequestUpdateProcessor(\n            authRequestEntities,\n            authRequestUpdates,\n            new AuthRequestUpdateProcessorConfiguration()\n            {\n                OrganizationId = organizationId,\n                AuthRequestExpiresAfter = _globalSettings.PasswordlessAuth.AdminRequestExpiration\n            }\n        );\n        processor.Process((Exception e) => _logger.LogError(\"Error processing organization auth request: {Message}\", e.Message));\n        await processor.Save((IEnumerable<OrganizationAdminAuthRequest> authRequests) => _authRequestRepository.UpdateManyAsync(authRequests));\n        await processor.SendPushNotifications((ar) => _pushNotificationService.PushAuthRequestResponseAsync(ar));\n        await processor.SendApprovalEmailsForProcessedRequests(SendApprovalEmail);\n        await processor.LogOrganizationEventsForProcessedRequests(LogOrganizationEvents);\n    }\n\n    async Task<ICollection<OrganizationAdminAuthRequest>> FetchManyOrganizationAuthRequestsFromTheDatabase(Guid organizationId, IEnumerable<Guid> authRequestIds)\n    {\n        return authRequestIds != null && authRequestIds.Any()\n            ? await _authRequestRepository\n            .GetManyAdminApprovalRequestsByManyIdsAsync(\n                organizationId,\n                authRequestIds\n            )\n            : new List<OrganizationAdminAuthRequest>();\n    }\n\n    async Task SendApprovalEmail<T>(T authRequest, string identifier) where T : AuthRequest\n    {\n        var user = await _userRepository.GetByIdAsync(authRequest.UserId);\n\n        // This should be impossible\n        if (user == null)\n        {\n            _logger.LogError(\"User {UserId} not found. Trusted device admin approval email not sent.\", authRequest.UserId);\n            return;\n        }\n\n        await _mailService.SendTrustedDeviceAdminApprovalEmailAsync(\n            user.Email,\n            authRequest.ResponseDate ?? DateTime.UtcNow,\n            authRequest.RequestIpAddress,\n            identifier\n        );\n    }\n\n    async Task LogOrganizationEvents(IEnumerable<(OrganizationAdminAuthRequest AuthRequest, EventType EventType)> events)\n    {\n        var organizationUsers = await _organizationUserRepository.GetManyAsync(events.Select(e => e.AuthRequest.OrganizationUserId));\n        await _eventService.LogOrganizationUserEventsAsync(\n            organizationUsers.Select(ou =>\n            {\n                var e = events.FirstOrDefault(e => e.AuthRequest.OrganizationUserId == ou.Id);\n                return (ou, e.EventType, e.AuthRequest.ResponseDate);\n            })\n        );\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/AccountRecovery/AdminRecoverAccountCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;\n\npublic class AdminRecoverAccountCommand(IOrganizationRepository organizationRepository,\n    IPolicyQuery policyQuery,\n    IUserRepository userRepository,\n    IMailService mailService,\n    IEventService eventService,\n    IPushNotificationService pushNotificationService,\n    IUserService userService,\n    TimeProvider timeProvider) : IAdminRecoverAccountCommand\n{\n    public async Task<IdentityResult> RecoverAccountAsync(Guid orgId,\n        OrganizationUser organizationUser, string newMasterPassword, string key)\n    {\n        // Org must be able to use reset password\n        var org = await organizationRepository.GetByIdAsync(orgId);\n        if (org == null || !org.UseResetPassword)\n        {\n            throw new BadRequestException(\"Organization does not allow password reset.\");\n        }\n\n        // Enterprise policy must be enabled\n        var resetPasswordPolicy = await policyQuery.RunAsync(orgId, PolicyType.ResetPassword);\n        if (!resetPasswordPolicy.Enabled)\n        {\n            throw new BadRequestException(\"Organization does not have the password reset policy enabled.\");\n        }\n\n        // Org User must be confirmed and have a ResetPasswordKey\n        if (organizationUser == null ||\n            organizationUser.Status != OrganizationUserStatusType.Confirmed ||\n            organizationUser.OrganizationId != orgId ||\n            !organizationUser.IsEnrolledInAccountRecovery() ||\n            !organizationUser.UserId.HasValue)\n        {\n            throw new BadRequestException(\"Organization User not valid\");\n        }\n\n        var user = await userService.GetUserByIdAsync(organizationUser.UserId.Value);\n        if (user == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (user.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"Cannot reset password of a user with Key Connector.\");\n        }\n\n        var result = await userService.UpdatePasswordHash(user, newMasterPassword);\n        if (!result.Succeeded)\n        {\n            return result;\n        }\n\n        user.RevisionDate = user.AccountRevisionDate = timeProvider.GetUtcNow().UtcDateTime;\n        user.LastPasswordChangeDate = user.RevisionDate;\n        user.ForcePasswordReset = true;\n        user.Key = key;\n\n        await userRepository.ReplaceAsync(user);\n        await mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.DisplayName());\n        await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_AdminResetPassword);\n        await pushNotificationService.PushLogOutAsync(user.Id);\n\n        return IdentityResult.Success;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/AccountRecovery/IAdminRecoverAccountCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;\n\n/// <summary>\n/// A command used to recover an organization user's account by an organization admin.\n/// </summary>\npublic interface IAdminRecoverAccountCommand\n{\n    /// <summary>\n    /// Recovers an organization user's account by resetting their master password.\n    /// </summary>\n    /// <param name=\"orgId\">The organization the user belongs to.</param>\n    /// <param name=\"organizationUser\">The organization user being recovered.</param>\n    /// <param name=\"newMasterPassword\">The user's new master password hash.</param>\n    /// <param name=\"key\">The user's new master-password-sealed user key.</param>\n    /// <returns>An IdentityResult indicating success or failure.</returns>\n    /// <exception cref=\"BadRequestException\">When organization settings, policy, or user state is invalid.</exception>\n    /// <exception cref=\"NotFoundException\">When the user does not exist.</exception>\n    Task<IdentityResult> RecoverAccountAsync(Guid orgId, OrganizationUser organizationUser,\n        string newMasterPassword, string key);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Collections/CollectionUtils.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Collections;\n\npublic static class CollectionUtils\n{\n    /// <summary>\n    /// Arranges Collection and CollectionUser objects to create default user collections.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization ID.</param>\n    /// <param name=\"organizationUserIds\">The IDs for organization users who need default collections.</param>\n    /// <param name=\"defaultCollectionName\">The encrypted string to use as the default collection name.</param>\n    /// <returns>A tuple containing the collections and collection users.</returns>\n    public static (ICollection<Collection> collections, ICollection<CollectionUser> collectionUsers)\n        BuildDefaultUserCollections(Guid organizationId, IEnumerable<Guid> organizationUserIds,\n            string defaultCollectionName)\n    {\n        var now = DateTime.UtcNow;\n\n        var collectionUsers = new List<CollectionUser>();\n        var collections = new List<Collection>();\n\n        foreach (var orgUserId in organizationUserIds)\n        {\n            var collectionId = CoreHelpers.GenerateComb();\n\n            collections.Add(new Collection\n            {\n                Id = collectionId,\n                OrganizationId = organizationId,\n                Name = defaultCollectionName,\n                CreationDate = now,\n                RevisionDate = now,\n                Type = CollectionType.DefaultUserCollection,\n                DefaultUserCollectionEmail = null\n\n            });\n\n            collectionUsers.Add(new CollectionUser\n            {\n                CollectionId = collectionId,\n                OrganizationUserId = orgUserId,\n                ReadOnly = false,\n                HidePasswords = false,\n                Manage = true,\n            });\n        }\n\n        return (collections, collectionUsers);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;\n\npublic class GroupAuthorizationHandler(ICurrentContext currentContext)\n    : AuthorizationHandler<GroupOperationRequirement, OrganizationScope>\n{\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        GroupOperationRequirement requirement, OrganizationScope organizationScope)\n    {\n        var authorized = requirement switch\n        {\n            not null when requirement.Name == nameof(GroupOperations.ReadAll) =>\n                await CanReadAllAsync(organizationScope),\n            not null when requirement.Name == nameof(GroupOperations.ReadAllDetails) =>\n                await CanViewGroupDetailsAsync(organizationScope),\n            _ => false\n        };\n\n        if (requirement is not null && authorized)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task<bool> CanReadAllAsync(OrganizationScope organizationScope) =>\n        currentContext.GetOrganization(organizationScope) is not null\n        || await currentContext.ProviderUserForOrgAsync(organizationScope);\n\n    private async Task<bool> CanViewGroupDetailsAsync(OrganizationScope organizationScope) =>\n        currentContext.GetOrganization(organizationScope) is\n    { Type: OrganizationUserType.Owner } or\n    { Type: OrganizationUserType.Admin } or\n    {\n        Permissions: { ManageGroups: true } or\n        { ManageUsers: true }\n    } ||\n        await currentContext.ProviderUserForOrgAsync(organizationScope);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Groups/Authorization/GroupOperations.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;\n\npublic class GroupOperationRequirement : OperationAuthorizationRequirement\n{\n    public GroupOperationRequirement(string name)\n    {\n        Name = name;\n    }\n}\n\npublic static class GroupOperations\n{\n    public static readonly GroupOperationRequirement ReadAll = new(nameof(ReadAll));\n    public static readonly GroupOperationRequirement ReadAllDetails = new(nameof(ReadAllDetails));\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Groups;\n\npublic class CreateGroupCommand : ICreateGroupCommand\n{\n    private readonly IEventService _eventService;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public CreateGroupCommand(\n        IEventService eventService,\n        IGroupRepository groupRepository,\n        IOrganizationUserRepository organizationUserRepository\n        )\n    {\n        _eventService = eventService;\n        _groupRepository = groupRepository;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    public async Task CreateGroupAsync(Group group, Organization organization,\n        ICollection<CollectionAccessSelection> collections = null,\n        IEnumerable<Guid> users = null)\n    {\n        Validate(organization, group, collections);\n        await GroupRepositoryCreateGroupAsync(group, organization, collections);\n\n        if (users != null)\n        {\n            await GroupRepositoryUpdateUsersAsync(group, users);\n        }\n\n        await _eventService.LogGroupEventAsync(group, Core.Enums.EventType.Group_Created);\n    }\n\n    public async Task CreateGroupAsync(Group group, Organization organization, EventSystemUser systemUser,\n        ICollection<CollectionAccessSelection> collections = null,\n        IEnumerable<Guid> users = null)\n    {\n        Validate(organization, group, collections);\n        await GroupRepositoryCreateGroupAsync(group, organization, collections);\n\n        if (users != null)\n        {\n            await GroupRepositoryUpdateUsersAsync(group, users, systemUser);\n        }\n\n        await _eventService.LogGroupEventAsync(group, Core.Enums.EventType.Group_Created, systemUser);\n    }\n\n    private async Task GroupRepositoryCreateGroupAsync(Group group, Organization organization, IEnumerable<CollectionAccessSelection> collections = null)\n    {\n        group.CreationDate = group.RevisionDate = DateTime.UtcNow;\n\n        if (collections == null)\n        {\n            await _groupRepository.CreateAsync(group);\n        }\n        else\n        {\n            await _groupRepository.CreateAsync(group, collections);\n        }\n    }\n\n    private async Task GroupRepositoryUpdateUsersAsync(Group group, IEnumerable<Guid> userIds,\n        EventSystemUser? systemUser = null)\n    {\n        var usersToAddToGroup = userIds as Guid[] ?? userIds.ToArray();\n\n        await _groupRepository.UpdateUsersAsync(group.Id, usersToAddToGroup);\n\n        var users = await _organizationUserRepository.GetManyAsync(usersToAddToGroup);\n        var eventDate = DateTime.UtcNow;\n\n        if (systemUser.HasValue)\n        {\n            await _eventService.LogOrganizationUserEventsAsync(users.Select(u =>\n                (u, EventType.OrganizationUser_UpdatedGroups, systemUser.Value, (DateTime?)eventDate)));\n        }\n        else\n        {\n            await _eventService.LogOrganizationUserEventsAsync(users.Select(u =>\n                (u, EventType.OrganizationUser_UpdatedGroups, (DateTime?)eventDate)));\n        }\n    }\n\n    private static void Validate(Organization organization, Group group, IEnumerable<CollectionAccessSelection> collections)\n    {\n        if (organization == null)\n        {\n            throw new BadRequestException(\"Organization not found\");\n        }\n\n        if (!organization.UseGroups)\n        {\n            throw new BadRequestException(\"This organization cannot use groups.\");\n        }\n\n        var invalidAssociations = collections?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));\n        if (invalidAssociations?.Any() ?? false)\n        {\n            throw new BadRequestException(\"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Groups/DeleteGroupCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Groups;\n\npublic class DeleteGroupCommand : IDeleteGroupCommand\n{\n    private readonly IGroupRepository _groupRepository;\n    private readonly IEventService _eventService;\n\n    public DeleteGroupCommand(IGroupRepository groupRepository, IEventService eventService)\n    {\n        _groupRepository = groupRepository;\n        _eventService = eventService;\n    }\n\n    public async Task DeleteGroupAsync(Guid organizationId, Guid id)\n    {\n        var group = await GroupRepositoryDeleteGroupAsync(organizationId, id);\n        await _eventService.LogGroupEventAsync(group, EventType.Group_Deleted);\n    }\n\n    public async Task DeleteGroupAsync(Guid organizationId, Guid id, EventSystemUser eventSystemUser)\n    {\n        var group = await GroupRepositoryDeleteGroupAsync(organizationId, id);\n        await _eventService.LogGroupEventAsync(group, EventType.Group_Deleted, eventSystemUser);\n    }\n\n    public async Task DeleteAsync(Group group)\n    {\n        await _groupRepository.DeleteAsync(group);\n        await _eventService.LogGroupEventAsync(group, EventType.Group_Deleted);\n    }\n\n    public async Task DeleteManyAsync(ICollection<Group> groups)\n    {\n        await _eventService.LogGroupEventsAsync(\n            groups.Select(g =>\n                (g, EventType.Group_Deleted, (EventSystemUser?)null, (DateTime?)DateTime.UtcNow)\n            ));\n\n        await _groupRepository.DeleteManyAsync(\n            groups.Select(g => g.Id)\n            );\n    }\n\n    private async Task<Group> GroupRepositoryDeleteGroupAsync(Guid organizationId, Guid id)\n    {\n        var group = await _groupRepository.GetByIdAsync(id);\n        if (group == null || group.OrganizationId != organizationId)\n        {\n            throw new NotFoundException(\"Group not found.\");\n        }\n\n        await _groupRepository.DeleteAsync(group);\n\n        return group;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/ICreateGroupCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\n\npublic interface ICreateGroupCommand\n{\n    Task CreateGroupAsync(Group group, Organization organization,\n        ICollection<CollectionAccessSelection> collections = null,\n        IEnumerable<Guid> users = null);\n\n    Task CreateGroupAsync(Group group, Organization organization, EventSystemUser systemUser,\n        ICollection<CollectionAccessSelection> collections = null,\n        IEnumerable<Guid> users = null);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/IDeleteGroupCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\n\npublic interface IDeleteGroupCommand\n{\n    Task DeleteGroupAsync(Guid organizationId, Guid id);\n    Task DeleteGroupAsync(Guid organizationId, Guid id, EventSystemUser eventSystemUser);\n    Task DeleteAsync(Group group);\n    Task DeleteManyAsync(ICollection<Group> groups);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/IUpdateGroupCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\n\npublic interface IUpdateGroupCommand\n{\n    Task UpdateGroupAsync(Group group, Organization organization,\n        ICollection<CollectionAccessSelection> collections = null,\n        IEnumerable<Guid> users = null);\n\n    Task UpdateGroupAsync(Group group, Organization organization, EventSystemUser systemUser,\n        ICollection<CollectionAccessSelection> collections = null,\n        IEnumerable<Guid> users = null);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommand.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Groups;\n\npublic class UpdateGroupCommand : IUpdateGroupCommand\n{\n    private readonly IEventService _eventService;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ICollectionRepository _collectionRepository;\n\n    public UpdateGroupCommand(\n        IEventService eventService,\n        IGroupRepository groupRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        ICollectionRepository collectionRepository)\n    {\n        _eventService = eventService;\n        _groupRepository = groupRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _collectionRepository = collectionRepository;\n    }\n\n    public async Task UpdateGroupAsync(Group group, Organization organization,\n        ICollection<CollectionAccessSelection>? collections = null,\n        IEnumerable<Guid>? userIds = null)\n    {\n        await ValidateAsync(organization, group, collections, userIds);\n\n        await SaveGroupWithCollectionsAsync(group, collections);\n\n        if (userIds != null)\n        {\n            await SaveGroupUsersAsync(group, userIds);\n        }\n\n        await _eventService.LogGroupEventAsync(group, Core.Enums.EventType.Group_Updated);\n    }\n\n    public async Task UpdateGroupAsync(Group group, Organization organization, EventSystemUser systemUser,\n        ICollection<CollectionAccessSelection>? collections = null,\n        IEnumerable<Guid>? userIds = null)\n    {\n        await ValidateAsync(organization, group, collections, userIds);\n\n        await SaveGroupWithCollectionsAsync(group, collections);\n\n        if (userIds != null)\n        {\n            await SaveGroupUsersAsync(group, userIds, systemUser);\n        }\n\n        await _eventService.LogGroupEventAsync(group, Core.Enums.EventType.Group_Updated, systemUser);\n    }\n\n    private async Task SaveGroupWithCollectionsAsync(Group group, IEnumerable<CollectionAccessSelection>? collections = null)\n    {\n        group.RevisionDate = DateTime.UtcNow;\n\n        if (collections == null)\n        {\n            await _groupRepository.ReplaceAsync(group);\n        }\n        else\n        {\n            await _groupRepository.ReplaceAsync(group, collections);\n        }\n    }\n\n    private async Task SaveGroupUsersAsync(Group group, IEnumerable<Guid> userIds, EventSystemUser? systemUser = null)\n    {\n        var newUserIds = userIds as Guid[] ?? userIds.ToArray();\n        var originalUserIds = await _groupRepository.GetManyUserIdsByIdAsync(group.Id);\n\n        await _groupRepository.UpdateUsersAsync(group.Id, newUserIds);\n\n        // We only want to create events OrganizationUserEvents for those that were actually modified.\n        // HashSet.SymmetricExceptWith is a convenient method of finding the difference between lists\n        var changedUserIds = new HashSet<Guid>(originalUserIds);\n        changedUserIds.SymmetricExceptWith(newUserIds);\n\n        // Fetch all changed users for logging the event\n        var users = await _organizationUserRepository.GetManyAsync(changedUserIds);\n        var eventDate = DateTime.UtcNow;\n\n        if (systemUser.HasValue)\n        {\n            await _eventService.LogOrganizationUserEventsAsync(users.Select(u =>\n                (u, EventType.OrganizationUser_UpdatedGroups, systemUser.Value, (DateTime?)eventDate)));\n        }\n        else\n        {\n            await _eventService.LogOrganizationUserEventsAsync(users.Select(u =>\n                (u, EventType.OrganizationUser_UpdatedGroups, (DateTime?)eventDate)));\n        }\n    }\n\n    private async Task ValidateAsync(Organization organization, Group group, ICollection<CollectionAccessSelection>? collectionAccess,\n        IEnumerable<Guid>? memberAccess)\n    {\n        // Avoid multiple enumeration\n        memberAccess = memberAccess?.ToList();\n\n        if (organization == null || organization.Id != group.OrganizationId)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!organization.UseGroups)\n        {\n            throw new BadRequestException(\"This organization cannot use groups.\");\n        }\n\n        var originalGroup = await _groupRepository.GetByIdAsync(group.Id);\n        if (originalGroup == null || originalGroup.OrganizationId != group.OrganizationId)\n        {\n            throw new NotFoundException();\n        }\n\n        if (collectionAccess?.Any() == true)\n        {\n            await ValidateCollectionAccessAsync(originalGroup, collectionAccess);\n        }\n\n        if (memberAccess?.Any() == true)\n        {\n            await ValidateMemberAccessAsync(originalGroup, memberAccess.ToList());\n        }\n\n        var invalidAssociations = collectionAccess?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));\n        if (invalidAssociations?.Any() ?? false)\n        {\n            throw new BadRequestException(\"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.\");\n        }\n    }\n\n    private async Task ValidateCollectionAccessAsync(Group originalGroup,\n        ICollection<CollectionAccessSelection> collectionAccess)\n    {\n        var collections = await _collectionRepository\n            .GetManyByManyIdsAsync(collectionAccess.Select(c => c.Id));\n        var collectionIds = collections.Select(c => c.Id);\n\n        var missingCollection = collectionAccess\n            .FirstOrDefault(cas => !collectionIds.Contains(cas.Id));\n        if (missingCollection != default)\n        {\n            throw new NotFoundException();\n        }\n\n        var invalidCollection = collections.FirstOrDefault(c => c.OrganizationId != originalGroup.OrganizationId);\n        if (invalidCollection != default)\n        {\n            // Use generic error message to avoid enumeration\n            throw new NotFoundException();\n        }\n\n        if (collections.Any(c => c.Type == CollectionType.DefaultUserCollection))\n        {\n            throw new BadRequestException(\"You cannot modify group access for collections with the type as DefaultUserCollection.\");\n        }\n    }\n\n    private async Task ValidateMemberAccessAsync(Group originalGroup,\n        ICollection<Guid> memberAccess)\n    {\n        var members = await _organizationUserRepository.GetManyAsync(memberAccess);\n        var memberIds = members.Select(g => g.Id);\n\n        var missingMemberId = memberAccess.FirstOrDefault(mId => !memberIds.Contains(mId));\n        if (missingMemberId != default)\n        {\n            throw new NotFoundException();\n        }\n\n        var invalidMember = members.FirstOrDefault(m => m.OrganizationId != originalGroup.OrganizationId);\n        if (invalidMember != default)\n        {\n            // Use generic error message to avoid enumeration\n            throw new NotFoundException();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Import/IImportOrganizationUserAndGroupsCommand.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IImportOrganizationUsersAndGroupsCommand\n{\n    Task ImportAsync(Guid organizationId,\n        IEnumerable<ImportedGroup> groups,\n        IEnumerable<ImportedOrganizationUser> newUsers,\n        IEnumerable<string> removeUserExternalIds,\n        bool overwriteExisting\n    );\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Import;\n\npublic class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersAndGroupsCommand\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IEventService _eventService;\n    private readonly IOrganizationService _organizationService;\n\n    private readonly EventSystemUser _EventSystemUser = EventSystemUser.PublicApi;\n\n    public ImportOrganizationUsersAndGroupsCommand(IOrganizationRepository organizationRepository,\n            IOrganizationUserRepository organizationUserRepository,\n            IStripePaymentService paymentService,\n            IGroupRepository groupRepository,\n            IEventService eventService,\n            IOrganizationService organizationService)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _paymentService = paymentService;\n        _groupRepository = groupRepository;\n        _eventService = eventService;\n        _organizationService = organizationService;\n    }\n\n    /// <summary>\n    /// Imports and synchronizes organization users and groups.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <param name=\"importedGroups\">List of groups to import.</param>\n    /// <param name=\"importedUsers\">List of users to import.</param>\n    /// <param name=\"removeUserExternalIds\">A collection of ExternalUserIds to be removed from the organization.</param>\n    /// <param name=\"overwriteExisting\">Indicates whether to delete existing external users from the organization\n    /// who are not included in the current import.</param>\n    /// <exception cref=\"NotFoundException\">Thrown if the organization does not exist.</exception>\n    /// <exception cref=\"BadRequestException\">Thrown if the organization is not configured to use directory syncing.</exception>\n    public async Task ImportAsync(Guid organizationId,\n        IEnumerable<ImportedGroup> importedGroups,\n        IEnumerable<ImportedOrganizationUser> importedUsers,\n        IEnumerable<string> removeUserExternalIds,\n        bool overwriteExisting)\n    {\n        var organization = await GetOrgById(organizationId);\n        if (organization is null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!organization.UseDirectory)\n        {\n            throw new BadRequestException(\"Organization cannot use directory syncing.\");\n        }\n\n        var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);\n        var importUserData = new OrganizationUserImportData(existingUsers, importedUsers);\n        var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>();\n\n        await RemoveExistingExternalUsers(removeUserExternalIds, events, importUserData);\n\n        if (overwriteExisting)\n        {\n            await OverwriteExisting(events, importUserData);\n        }\n\n        await UpdateExistingUsers(importedUsers, importUserData);\n\n        await AddNewUsers(organization, importedUsers, importUserData);\n\n        await ImportGroups(organization, importedGroups, importUserData);\n\n        await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, _EventSystemUser, e.d)));\n    }\n\n    /// <summary>\n    /// Deletes external users based on provided set of ExternalIds.\n    /// </summary>\n    /// <param name=\"removeUserExternalIds\">A collection of external user IDs to be deleted.</param>\n    /// <param name=\"events\">A list to which user removal events will be added.</param>\n    /// <param name=\"importUserData\">Data containing imported and existing external users.</param>\n\n    private async Task RemoveExistingExternalUsers(IEnumerable<string> removeUserExternalIds,\n            List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events,\n            OrganizationUserImportData importUserData)\n    {\n        if (!removeUserExternalIds.Any())\n        {\n            return;\n        }\n\n        var existingUsersDict = importUserData.ExistingExternalUsers.ToDictionary(u => u.ExternalId);\n        // Determine which ids in removeUserExternalIds to delete based on:\n        // They are not in ImportedExternalIds, they are in existingUsersDict, and they are not an owner.\n        var removeUsersSet = new HashSet<string>(removeUserExternalIds)\n            .Except(importUserData.ImportedExternalIds)\n            .Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner)\n            .Select(u => existingUsersDict[u]);\n\n        await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id));\n        events.AddRange(removeUsersSet.Select(u => (\n          u,\n          EventType.OrganizationUser_Removed,\n          (DateTime?)DateTime.UtcNow\n          ))\n        );\n    }\n\n    /// <summary>\n    /// Updates existing organization users by assigning each an ExternalId from the imported user data\n    /// where a match is found by email and the existing user lacks an ExternalId. Saves the updated\n    /// users and updates the ExistingExternalUsersIdDict mapping.\n    /// </summary>\n    /// <param name=\"importedUsers\">List of imported organization users.</param>\n    /// <param name=\"importUserData\">Data containing existing and imported users, along with mapping dictionaries.</param>\n    private async Task UpdateExistingUsers(IEnumerable<ImportedOrganizationUser> importedUsers, OrganizationUserImportData importUserData)\n    {\n        if (!importedUsers.Any())\n        {\n            return;\n        }\n\n        var updateUsers = new List<OrganizationUser>();\n\n        // Map existing and imported users to dicts keyed by Email\n        var existingUsersEmailsDict = importUserData.ExistingUsers\n            .Where(u => string.IsNullOrWhiteSpace(u.ExternalId))\n            .ToDictionary(u => u.Email);\n        var importedUsersEmailsDict = importedUsers.ToDictionary(u => u.Email);\n\n        // Determine which users to update.\n        var userEmailsToUpdate = existingUsersEmailsDict.Keys.Intersect(importedUsersEmailsDict.Keys).ToList();\n        var userIdsToUpdate = userEmailsToUpdate.Select(e => existingUsersEmailsDict[e].Id).ToList();\n\n        var organizationUsers = (await _organizationUserRepository.GetManyAsync(userIdsToUpdate)).ToDictionary(u => u.Id);\n\n        foreach (var userEmail in userEmailsToUpdate)\n        {\n            // verify userEmail has an associated OrganizationUser\n            existingUsersEmailsDict.TryGetValue(userEmail, out var existingUser);\n            organizationUsers.TryGetValue(existingUser!.Id, out var organizationUser);\n            importedUsersEmailsDict.TryGetValue(userEmail, out var importedUser);\n\n            if (organizationUser is null || importedUser is null)\n            {\n                continue;\n            }\n\n            organizationUser.ExternalId = importedUser.ExternalId;\n            updateUsers.Add(organizationUser);\n            importUserData.ExistingExternalUsersIdDict.Add(organizationUser.ExternalId, organizationUser.Id);\n        }\n        await _organizationUserRepository.UpsertManyAsync(updateUsers);\n    }\n\n    /// <summary>\n    /// Adds new external users to the organization by inviting users who are present in the imported data\n    /// but not already part of the organization. Sends invitations, updates the user Id mapping on success,\n    /// and throws exceptions on failure.\n    /// </summary>\n    /// <param name=\"organization\">The target organization to which users are being added.</param>\n    /// <param name=\"importedUsers\">A collection of imported users to consider for addition.</param>\n    /// <param name=\"importUserData\">Data containing imported user info and existing user mappings.</param>\n    private async Task AddNewUsers(Organization organization,\n            IEnumerable<ImportedOrganizationUser> importedUsers,\n            OrganizationUserImportData importUserData)\n    {\n        // Determine which users are already in the organization\n        var existingUsersSet = new HashSet<string>(importUserData.ExistingExternalUsersIdDict.Keys);\n        var usersToAdd = importUserData.ImportedExternalIds.Except(existingUsersSet).ToList();\n        var userInvites = new List<(OrganizationUserInvite, string)>();\n        var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization);\n\n        foreach (var user in importedUsers)\n        {\n            if (!usersToAdd.Contains(user.ExternalId) || string.IsNullOrWhiteSpace(user.Email))\n            {\n                continue;\n            }\n\n            try\n            {\n                var invite = new OrganizationUserInvite\n                {\n                    Emails = new List<string> { user.Email },\n                    Type = OrganizationUserType.User,\n                    Collections = new List<CollectionAccessSelection>(),\n                    AccessSecretsManager = hasStandaloneSecretsManager\n                };\n                userInvites.Add((invite, user.ExternalId));\n            }\n            catch (BadRequestException)\n            {\n                // Thrown when the user is already invited to the organization\n                continue;\n            }\n        }\n\n        var invitedUsers = await _organizationService.InviteUsersAsync(organization.Id, Guid.Empty, _EventSystemUser, userInvites);\n        foreach (var invitedUser in invitedUsers)\n        {\n            importUserData.ExistingExternalUsersIdDict.TryAdd(invitedUser.ExternalId!, invitedUser.Id);\n        }\n    }\n\n    /// <summary>\n    /// Deletes existing external users from the organization who are not included in the current import and are not owners.\n    /// Records corresponding removal events and updates the internal mapping by removing deleted users.\n    /// </summary>\n    /// <param name=\"events\">A list to which user removal events will be added.</param>\n    /// <param name=\"importUserData\">Data containing existing and imported external users along with their Id mappings.</param>\n    private async Task OverwriteExisting(\n            List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events,\n            OrganizationUserImportData importUserData)\n    {\n        var usersToDelete = importUserData.ExistingExternalUsers\n            .Where(u =>\n                u.Type != OrganizationUserType.Owner &&\n                !importUserData.ImportedExternalIds.Contains(u.ExternalId) &&\n                importUserData.ExistingExternalUsersIdDict.ContainsKey(u.ExternalId))\n            .ToList();\n\n        if (usersToDelete.Any(u => !u.HasMasterPassword))\n        {\n            // Removing users without an MP will put their account in an unrecoverable state.\n            // We allow this during normal syncs for offboarding, but overwriteExisting risks bricking every user in\n            // the organization, so you don't get to do it here.\n            throw new BadRequestException(\n                \"Sync failed. To proceed, disable the 'Remove and re-add users during next sync' setting and try again.\");\n        }\n\n        await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id));\n        events.AddRange(usersToDelete.Select(u => (\n          u,\n          EventType.OrganizationUser_Removed,\n          (DateTime?)DateTime.UtcNow\n          ))\n        );\n        foreach (var deletedUser in usersToDelete)\n        {\n            importUserData.ExistingExternalUsersIdDict.Remove(deletedUser.ExternalId);\n        }\n    }\n\n    /// <summary>\n    /// Imports group data into the organization by saving new groups and updating existing ones.\n    /// </summary>\n    /// <param name=\"organization\">The organization into which groups are being imported.</param>\n    /// <param name=\"importedGroups\">A collection of groups to be imported.</param>\n    /// <param name=\"importUserData\">Data containing information about existing and imported users.</param>\n    private async Task ImportGroups(Organization organization, IEnumerable<ImportedGroup> importedGroups, OrganizationUserImportData importUserData)\n    {\n        if (!importedGroups.Any())\n        {\n            return;\n        }\n\n        if (!organization.UseGroups)\n        {\n            throw new BadRequestException(\"Organization cannot use groups.\");\n        }\n\n        var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);\n        var importGroupData = new OrganizationGroupImportData(importedGroups, existingGroups);\n\n        await SaveNewGroups(importGroupData, importUserData);\n        await UpdateExistingGroups(importGroupData, importUserData, organization);\n    }\n\n    /// <summary>\n    /// Saves newly imported groups that do not already exist in the organization.\n    /// Sets their creation and revision dates, associates users with each group.\n    /// </summary>\n    /// <param name=\"importGroupData\">Data containing both imported and existing groups.</param>\n    /// <param name=\"importUserData\">Data containing information about existing and imported users.</param>\n    private async Task SaveNewGroups(OrganizationGroupImportData importGroupData, OrganizationUserImportData importUserData)\n    {\n        var existingExternalGroupsDict = importGroupData.ExistingExternalGroups.ToDictionary(g => g.ExternalId!);\n        var newGroups = importGroupData.Groups\n            .Where(g => !existingExternalGroupsDict.ContainsKey(g.Group.ExternalId!))\n            .Select(g => g.Group)\n            .ToList()!;\n\n        var savedGroups = new List<Group>();\n        foreach (var group in newGroups)\n        {\n            group.CreationDate = group.RevisionDate = DateTime.UtcNow;\n\n            savedGroups.Add(await _groupRepository.CreateAsync(group));\n            await UpdateUsersAsync(group, importGroupData.GroupsDict[group.ExternalId!].ExternalUserIds,\n                importUserData.ExistingExternalUsersIdDict);\n        }\n\n        await _eventService.LogGroupEventsAsync(\n            savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)_EventSystemUser, (DateTime?)DateTime.UtcNow)));\n    }\n\n    /// <summary>\n    /// Updates existing groups in the organization based on imported group data.\n    /// If a group's name has changed, it updates the name and revision date in the repository.\n    /// Also updates group-user associations.\n    /// </summary>\n    /// <param name=\"importGroupData\">Data containing imported groups and their user associations.</param>\n    /// <param name=\"importUserData\">Data containing imported and existing organization users.</param>\n    /// <param name=\"organization\">The organization to which the groups belong.</param>\n    private async Task UpdateExistingGroups(OrganizationGroupImportData importGroupData,\n            OrganizationUserImportData importUserData,\n            Organization organization)\n    {\n        var updateGroups = importGroupData.ExistingExternalGroups\n            .Where(g => importGroupData.GroupsDict.ContainsKey(g.ExternalId!))\n            .ToList();\n\n        if (updateGroups.Any())\n        {\n            // get existing group users\n            var groupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organization.Id);\n            var existingGroupUsers = groupUsers\n                .GroupBy(gu => gu.GroupId)\n                .ToDictionary(g => g.Key, g => new HashSet<Guid>(g.Select(gr => gr.OrganizationUserId)));\n\n            foreach (var group in updateGroups)\n            {\n                // Check for changes to the group, update if changed.\n                var updatedGroup = importGroupData.GroupsDict[group.ExternalId!].Group;\n                if (group.Name != updatedGroup.Name)\n                {\n                    group.RevisionDate = DateTime.UtcNow;\n                    group.Name = updatedGroup.Name;\n\n                    await _groupRepository.ReplaceAsync(group);\n                }\n\n                // compare and update user group associations\n                await UpdateUsersAsync(group, importGroupData.GroupsDict[group.ExternalId!].ExternalUserIds,\n                    importUserData.ExistingExternalUsersIdDict,\n                    existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null);\n\n            }\n\n            await _eventService.LogGroupEventsAsync(\n                updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)_EventSystemUser, (DateTime?)DateTime.UtcNow)));\n        }\n\n    }\n\n    /// <summary>\n    /// Updates the user associations for a given group.\n    /// Only updates if the set of associated users differs from the current group membership.\n    /// Filters users based on those present in the existing user Id dictionary.\n    /// </summary>\n    /// <param name=\"group\">The group whose user associations are being updated.</param>\n    /// <param name=\"groupUsers\">A set of ExternalUserIds to be associated with the group.</param>\n    /// <param name=\"existingUsersIdDict\">A dictionary mapping ExternalUserIds to internal user Ids.</param>\n    /// <param name=\"existingUsers\">Optional set of currently associated user Ids for comparison.</param>\n    private async Task UpdateUsersAsync(Group group, HashSet<string> groupUsers,\n        Dictionary<string, Guid> existingUsersIdDict, HashSet<Guid>? existingUsers = null)\n    {\n        var availableUsers = groupUsers.Intersect(existingUsersIdDict.Keys);\n        var users = new HashSet<Guid>(availableUsers.Select(u => existingUsersIdDict[u]));\n        if (existingUsers is not null && existingUsers.Count == users.Count && users.SetEquals(existingUsers))\n        {\n            return;\n        }\n\n        await _groupRepository.UpdateUsersAsync(group.Id, users);\n    }\n\n    private async Task<Organization?> GetOrgById(Guid id)\n    {\n        return await _organizationRepository.GetByIdAsync(id);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationGroupImportData.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\n\nnamespace Bit.Core.Models.Data.Organizations;\n\n/// <summary>\n/// Represents the data required to import organization groups,\n/// including newly imported groups and existing groups within the organization.\n/// </summary>\npublic class OrganizationGroupImportData\n{\n    /// <summary>\n    /// The collection of groups that are being imported.\n    /// </summary>\n    public readonly IEnumerable<ImportedGroup> Groups;\n\n    /// <summary>\n    /// Collection of groups that already exist in the organization.\n    /// </summary>\n    public readonly ICollection<Group> ExistingGroups;\n\n    /// <summary>\n    /// Existing groups with ExternalId set.\n    /// </summary>\n    public readonly IEnumerable<Group> ExistingExternalGroups;\n\n    /// <summary>\n    /// Mapping of imported groups keyed by their ExternalId.\n    /// </summary>\n    public readonly IDictionary<string, ImportedGroup> GroupsDict;\n\n    public OrganizationGroupImportData(IEnumerable<ImportedGroup> groups, ICollection<Group> existingGroups)\n    {\n        Groups = groups;\n        GroupsDict = groups.ToDictionary(g => g.Group.ExternalId!);\n        ExistingGroups = existingGroups;\n        ExistingExternalGroups = existingGroups.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationUserImportData.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Models.Business;\nnamespace Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\npublic class OrganizationUserImportData\n{\n    /// <summary>\n    /// Set of user ExternalIds that are being imported\n    /// </summary>\n    public readonly HashSet<string> ImportedExternalIds;\n    /// <summary>\n    /// All existing OrganizationUsers for the organization\n    /// </summary>\n    public readonly ICollection<OrganizationUserUserDetails> ExistingUsers;\n    /// <summary>\n    /// Existing OrganizationUsers with ExternalIds set.\n    /// </summary>\n    public readonly IEnumerable<OrganizationUserUserDetails> ExistingExternalUsers;\n    /// <summary>\n    /// Mapping of an existing users's ExternalId to their Id\n    /// </summary>\n    public readonly Dictionary<string, Guid> ExistingExternalUsersIdDict;\n\n    public OrganizationUserImportData(ICollection<OrganizationUserUserDetails> existingUsers, IEnumerable<ImportedOrganizationUser> importedUsers)\n    {\n        ImportedExternalIds = new HashSet<string>(importedUsers?.Select(u => u.ExternalId) ?? new List<string>());\n        ExistingUsers = existingUsers;\n        ExistingExternalUsers = ExistingUsers.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();\n        ExistingExternalUsersIdDict = ExistingExternalUsers.ToDictionary(u => u.ExternalId, u => u.Id);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.Models.Data.Organizations;\n\npublic class OrganizationAbility\n{\n    public OrganizationAbility() { }\n\n    public OrganizationAbility(Organization organization)\n    {\n        Id = organization.Id;\n        UseEvents = organization.UseEvents;\n        Use2fa = organization.Use2fa;\n        Using2fa = organization.Use2fa && organization.TwoFactorProviders != null &&\n            organization.TwoFactorProviders != \"{}\";\n        UsersGetPremium = organization.UsersGetPremium;\n        Enabled = organization.Enabled;\n        UseSso = organization.UseSso;\n        UseKeyConnector = organization.UseKeyConnector;\n        UseScim = organization.UseScim;\n        UseResetPassword = organization.UseResetPassword;\n        UseCustomPermissions = organization.UseCustomPermissions;\n        UsePolicies = organization.UsePolicies;\n        LimitCollectionCreation = organization.LimitCollectionCreation;\n        LimitCollectionDeletion = organization.LimitCollectionDeletion;\n        LimitItemDeletion = organization.LimitItemDeletion;\n        AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;\n        UseRiskInsights = organization.UseRiskInsights;\n        UseOrganizationDomains = organization.UseOrganizationDomains;\n        UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;\n        UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;\n        UseDisableSmAdsForUsers = organization.UseDisableSmAdsForUsers;\n        UsePhishingBlocker = organization.UsePhishingBlocker;\n        UseMyItems = organization.UseMyItems;\n    }\n\n    public Guid Id { get; set; }\n    public bool UseEvents { get; set; }\n    public bool Use2fa { get; set; }\n    public bool Using2fa { get; set; }\n    public bool UsersGetPremium { get; set; }\n    public bool Enabled { get; set; }\n    public bool UseSso { get; set; }\n    public bool UseKeyConnector { get; set; }\n    public bool UseScim { get; set; }\n    public bool UseResetPassword { get; set; }\n    public bool UseCustomPermissions { get; set; }\n    public bool UsePolicies { get; set; }\n    public bool LimitCollectionCreation { get; set; }\n    public bool LimitCollectionDeletion { get; set; }\n    public bool LimitItemDeletion { get; set; }\n    public bool AllowAdminAccessToAllCollectionItems { get; set; }\n    public bool UseRiskInsights { get; set; }\n    public bool UseOrganizationDomains { get; set; }\n    public bool UseAdminSponsoredFamilies { get; set; }\n    public bool UseAutomaticUserConfirmation { get; set; }\n    public bool UseDisableSmAdsForUsers { get; set; }\n    public bool UsePhishingBlocker { get; set; }\n    public bool UseMyItems { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md",
    "content": "# Organization ability flags\n\n## Overview\n\nMany Bitwarden features are tied to specific subscription plans. For example, SCIM and SSO are Enterprise features,\nwhile Event Logs are available to Teams and Enterprise plans. When developing features that require plan-based access\ncontrol, we use **Organization Ability Flags** (or simply _abilities_) — explicit boolean properties on the Organization\nentity that indicate whether an organization can use a specific feature.\n\n## The rule\n\n**Never check plan types to control feature access.** Always use a dedicated ability flag on the Organization entity.\n\n### ❌ Don't do this\n\n```csharp\n// Checking plan type directly\nif (organization.PlanType == PlanType.Enterprise ||\n    organization.PlanType == PlanType.Teams ||\n    organization.PlanType == PlanType.Family)\n{\n    // allow feature...\n}\n```\n\n### ❌ Don't do this\n\n```csharp\n// Piggybacking off another feature's ability\nif (organization.PlanType == PlanType.Enterprise && organization.UseEvents)\n{\n    // assume they can use some other feature...\n}\n```\n\n### ✅ Do this instead\n\n```csharp\n// Check the explicit ability flag\nif (!organization.UseEvents)\n{\n    throw new BadRequestException(\"Your organization does not have access to this feature.\");\n}\n// proceed with feature logic...\n```\n\n## Why this pattern matters\n\nUsing explicit ability flags instead of plan type checks provides several benefits:\n\n1. **Simplicity** — A single boolean check is cleaner and less error-prone than maintaining lists of plan types.\n\n2. **Centralized Control** — Feature access is managed in one place: the ability assignment during organization\n   creation/upgrade. No need to hunt through the codebase for scattered plan type checks.\n\n3. **Flexibility** — Abilities can be set independently of plan type, enabling:\n    - Early access programs for features not yet tied to a plan\n    - Trial access to help customers evaluate a feature before upgrading\n    - Custom arrangements for specific customers (can be manually toggled in Bitwarden Portal)\n    - A/B testing of features across different cohorts\n    - Gating high-risk features behind internal support teams (e.g., Key Connector)\n\n4. **Safe Refactoring** — When plans change (e.g., adding a new plan tier, renaming plans, or moving features between\n   tiers), we only update the ability assignment logic—not every place the feature is used.\n\n5. **Graceful Downgrades** — When an organization downgrades, we update their abilities. All feature checks\n   automatically respect the new access level.\n\n6. **Semantic Code** — The code clearly expresses what capability is being checked, making it more maintainable.\n\n## Organization abilities and other features\n\nOrganization abilities work alongside other access control mechanisms. Understanding the differences helps you choose the right tool:\n\n|                   | **Organization abilities** (this document)                                                         | **Feature flags**                                                | **Enterprise policies**                                                 |\n|-------------------|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------|-------------------------------------------------------------------------|\n| **Purpose**       | Control whether an organization has **access** to a feature                                        | Control feature **rollout** and act as a killswitch if necessary | Control **behavior** of features the organization already has access to |\n| **Set by**        | Subscription plan (automatically) or internal support teams (manual override via Bitwarden Portal) | Engineering teams                                                | Organization admins and owners                                          |\n| **Lifecycle**     | Permanent - part of the core product                                                               | Temporary - removed once feature is stable                       | Permanent - part of the core product                                    |\n| **Scope**         | Per organization                                                                                   | Global or targeted                                               | Per organization                                                        |\n| **Toggle method** | Bitwarden Portal (single) or data migration (bulk)                                                 | LaunchDarkly                                                     | In-product via Admin Console                                            |\n| **Examples**      | Can the org use SSO? Can they use SCIM? Can they use Events?                                       | Is the new API available? Is the redesigned UI enabled?          | Require 2FA for all users, enforce password complexity                  |\n\n### When to use which?\n\n**Use an organization ability** when the feature will be permanently gated behind a subscription tier or our support teams.\n\n**Use a feature flag** when you need to control the release of a new feature.\n\n**Use a policy** when you're adding configurable rules to a feature the organization can already access.\n\n**Use multiple together** when appropriate. For example, a new enterprise feature might use all three: a feature flag to control initial rollout, an organization ability to restrict it to Enterprise plans, and a policy to let admins configure enforcement rules.\n\n## How it works\n\n### Ability assignment at signup/upgrade\n\nWhen an organization is created or changes plans, the ability flags are set based on the plan's capabilities:\n\n```csharp\n// During organization creation or plan change\norganization.UseGroups = plan.HasGroups;\norganization.UseSso = plan.HasSso;\norganization.UseScim = plan.HasScim;\norganization.UsePolicies = plan.HasPolicies;\norganization.UseEvents = plan.HasEvents;\n// ... etc\n```\n\n### Accessing abilities in code\n\n**Server-side:**\n\n- If you already have the full `Organization` object in scope, use it directly: `organization.UseMyFeature`\n- If not, use the in-memory cache to avoid hitting the database:\n  `IApplicationCacheService.GetOrganizationAbilityAsync(orgId)`\n    - This returns an `OrganizationAbility` object - a simplified, cached representation of the ability flags\n    - Note: some older flags may be missing from `OrganizationAbility` but can be added if needed\n\n**Client-side:**\n\n- Get the organization object from `OrganizationService`, then use it directly: `organization.useMyFeature`\n\n### Manual override via Bitwarden Portal\n\nOrganization abilities can be manually toggled for specific customers via the Bitwarden Portal → Organizations page.\nThis is useful for custom arrangements, early access, or internal testing.\n\n## Adding a new ability\n\nWhen developing a new plan-gated feature, follow these steps. We use `MyFeature` as a placeholder for your feature name\n(e.g., `UseEvents`).\n\n### 1. Update core entities\n\n- `src/Core/AdminConsole/Entities/Organization.cs` — Add `UseMyFeature` boolean property\n- `src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs` — Add to ability object\n\n### 2. Database changes (MSSQL)\n\nAdd a new `UseMyFeature` column to the Organization table:\n\n**Files to modify:**\n\n- `src/Sql/dbo/Tables/Organization.sql` — Add column with `NOT NULL` constraint and default of `0` (false) for EDD\n  backward compatibility\n\n**Stored procedures to update:**\n\n- `src/Sql/dbo/Stored Procedures/Organization_Create.sql`\n- `src/Sql/dbo/Stored Procedures/Organization_Update.sql`\n- `src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql`\n\n**Views to update (add the new column):**\n\n- `src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql`\n- `src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql`\n- `src/Sql/dbo/Views/OrganizationView.sql`\n\n**Views to refresh (use `sp_refreshview`):**\n\nAfter schema changes, the following views may need to be refreshed even though they don't explicitly include the new\ncolumn:\n\n- `src/Sql/dbo/Views/OrganizationCipherDetailsCollectionsView.sql`\n- `src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql`\n\n**Create a migration script** for these database changes.\n\n### 3. Entity Framework changes\n\nEF is primarily used for self-host. Implementations must be kept consistent.\n\n**Generate EF migrations** for the new column.\n\n**Update queries and initialization code:**\n\n- `src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs`\n    - Update `GetManyAbilitiesAsync()` to initialize the new property\n- `src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs`\n    - Update the integration test:\n      `test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs`\n- `src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs`\n\n### 4. Data migrations for existing organizations\n\nIf your feature should be enabled for existing organizations on certain plan types, create data migrations to set the\nability flag:\n\n**MSSQL migration:**\n\n```sql\n-- Example: Enable UseMyFeature for all Enterprise organizations\n-- Check src/Core/Billing/Enums/PlanType.cs for current values\nUPDATE [dbo].[Organization]\nSET UseMyFeature = 1\nWHERE PlanType IN (4, 5, 10, 11, 14, 15, 19, 20) -- All Enterprise plan types (2019, 2020, 2023, current)\n```\n\n**EF migration:**\n\nCreate a corresponding data migration for EF databases used by self-hosted instances.\n\n### 5. Server code changes\n\nUpdate related models and mapping code so models receive the new value.\n\n**Response models:**\n\n- `src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs`\n- `src/Api/AdminConsole/Models/Response/BaseProfileOrganizationResponseModel.cs`\n\n**Data models:**\n\n- `src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs`\n- `src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs`\n- `src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs`\n- `src/Core/AdminConsole/Models/Data/IProfileOrganizationDetails.cs`\n\n**Plan definition and signup logic:**\n\nIf your feature should be automatically enabled based on plan type at signup (e.g., SSO for Enterprise plans), you'll\nneed to:\n\n1. Work with the Billing Team to add a `HasMyFeature` property to the Plan model and configure which plans include it\n2. Update the following files to map `plan.HasMyFeature` to `organization.UseMyFeature`:\n    - `src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs` — Used during\n      organization creation and plan upgrades\n    - `src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs` — Used when restarting a canceled\n      subscription with a potentially different plan\n\n**Note:** This step is not required if your feature is enabled manually via the Admin Portal.\n\n### 6. Client changes\n\n**TypeScript models to update:**\n\n- `libs/common/src/admin-console/models/response/profile-organization.response.ts`\n- `libs/common/src/admin-console/models/response/organization.response.ts`\n- `libs/common/src/admin-console/models/domain/organization.ts`\n- `libs/common/src/admin-console/models/data/organization.data.ts`\n    - Update tests: `libs/common/src/admin-console/models/data/organization.data.spec.ts`\n\n### 7. Bitwarden Portal changes\n\nFor manual override capability in the admin portal:\n\n- `src/Admin/AdminConsole/Models/OrganizationEditModel.cs` — Map the ability from the organization entity\n- `src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml` — Add checkbox for the new ability\n- `src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml` — Add the new ability to the\n  `togglePlanFeatures()` function so it's automatically set when a plan type is selected\n- `src/Admin/AdminConsole/Controllers/OrganizationsController.cs` — Update `UpdateOrganization()` method mapping\n\n### 8. Self-host licensing\n\n> ⚠️ **WARNING:** Mistakes in organization license changes can disable the entire organization for self-hosted\n> customers!\n> Double-check your work and ask for help if unsure.\n>\n> **Note:** New properties must be added to both the `OrganizationLicense` class and the claims-based system.\n\n**Update OrganizationLicense:**\n\n- `src/Core/Billing/Organizations/Models/OrganizationLicense.cs`\n    - Add the new property to the class\n    - `VerifyData()` — Add claims validation\n    - `GetDataBytes()` — Add the new property to the ignored fields section (below the comment\n      `// any new fields added need to be added here so that they're ignored`)\n\n**Add property to Organization entity mapper:**\n\n- `src/Core/AdminConsole/Entities/Organization.cs` — Add the new property to the `UpdateFromLicense()` method\n\n**Add claims for the new feature:**\n\n- `src/Core/Billing/Licenses/LicenseConstants.cs` — Add constant for the new ability in `OrganizationLicenseConstants`\n- `src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs`\n\n**Update license command:**\n\nMap your feature property from the claim to the organization when creating or updating from the license file:\n\n- `src/Core/AdminConsole/Services/OrganizationFactory.cs`\n- `src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs`\n\n**Update tests:**\n\n- `test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs` - add the new property to\n  `UpdateLicenseAsync_WithClaimsPrincipal_ExtractsAllPropertiesFromClaims` test\n\n> **Tip:** Running tests in `UpdateOrganizationLicenseCommandTests.cs` will help identify any missing changes.\n> Test failures will guide you to all areas that need updates.\n\n### 9. Implement business logic checks\n\nIn your feature's business logic, check the ability flag:\n\n```csharp\n// Retrieve the organization ability (uses cache, avoids DB hit)\nvar orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);\n\nif (!orgAbility.UseMyFeature)\n{\n    throw new BadRequestException(\"Your organization's plan does not support this feature.\");\n}\n\n// Proceed with feature logic...\n```\n\nAs explained above, organization abilities work alongside feature flags — they don't replace them.\nFor new features, you'll typically want both:\n\n```csharp\n// Check feature flag first (controls rollout)\nif (!_featureService.IsEnabled(FeatureFlagKeys.MyFeature))\n{\n    throw new BadRequestException(\"This feature is not available.\");\n}\n\n// Then check organization ability (controls plan-based access)\nif (!orgAbility.UseMyFeature)\n{\n    throw new BadRequestException(\"Your organization's plan does not support this feature.\");\n}\n```\n\n## Existing abilities\n\nFor reference, here are some current organization ability flags (not a complete list):\n\n| Ability                  | Description                   | Typical Plans     |\n|--------------------------|-------------------------------|-------------------|\n| `UseGroups`              | Group-based collection access | Teams, Enterprise |\n| `UseDirectory`           | Directory Connector sync      | Teams, Enterprise |\n| `UseEvents`              | Event logging                 | Teams, Enterprise |\n| `UseTotp`                | Authenticator (TOTP)          | Teams, Enterprise |\n| `UseSso`                 | Single Sign-On                | Enterprise        |\n| `UseScim`                | SCIM provisioning             | Teams, Enterprise |\n| `UsePolicies`            | Enterprise policies           | Enterprise        |\n| `UseResetPassword`       | Admin password reset          | Enterprise        |\n| `UseOrganizationDomains` | Domain verification/claiming  | Enterprise        |\n\n## Questions?\n\nIf you're unsure whether your feature needs a new ability or which existing ability to use, reach out to your team lead\nor members of the Admin Console or Architecture teams. When in doubt, adding an explicit ability is almost always the\nright choice—it's easy to do and keeps our access control clean and maintainable.\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/CreateOrganizationApiKeyCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\n\npublic class CreateOrganizationApiKeyCommand : ICreateOrganizationApiKeyCommand\n{\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;\n\n    public CreateOrganizationApiKeyCommand(IOrganizationApiKeyRepository organizationApiKeyRepository)\n    {\n        _organizationApiKeyRepository = organizationApiKeyRepository;\n    }\n\n    public async Task<OrganizationApiKey> CreateAsync(Guid organizationId,\n        OrganizationApiKeyType organizationApiKeyType)\n    {\n        var apiKey = new OrganizationApiKey\n        {\n            OrganizationId = organizationId,\n            Type = organizationApiKeyType,\n            ApiKey = CoreHelpers.SecureRandomString(30),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await _organizationApiKeyRepository.CreateAsync(apiKey);\n        return apiKey;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\n\npublic class GetOrganizationApiKeyQuery : IGetOrganizationApiKeyQuery\n{\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;\n\n    public GetOrganizationApiKeyQuery(IOrganizationApiKeyRepository organizationApiKeyRepository)\n    {\n        _organizationApiKeyRepository = organizationApiKeyRepository;\n    }\n\n    public async Task<OrganizationApiKey> GetOrganizationApiKeyAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType)\n    {\n        if (!Enum.IsDefined(organizationApiKeyType))\n        {\n            throw new ArgumentOutOfRangeException(nameof(organizationApiKeyType), $\"Invalid value for enum {nameof(OrganizationApiKeyType)}\");\n        }\n\n        var apiKeys = await _organizationApiKeyRepository\n            .GetManyByOrganizationIdTypeAsync(organizationId, organizationApiKeyType);\n\n        // NOTE: Currently we only allow one type of api key per organization\n        return apiKeys.SingleOrDefault();\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/Interfaces/ICreateOrganizationApiKeyCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;\n\npublic interface ICreateOrganizationApiKeyCommand\n{\n    Task<OrganizationApiKey> CreateAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyQuery.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;\n\npublic interface IGetOrganizationApiKeyQuery\n{\n    Task<OrganizationApiKey> GetOrganizationApiKeyAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/Interfaces/IRotateOrganizationApiKeyCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;\n\npublic interface IRotateOrganizationApiKeyCommand\n{\n    Task<OrganizationApiKey> RotateApiKeyAsync(OrganizationApiKey organizationApiKey);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/RotateOrganizationApiKeyCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\n\npublic class RotateOrganizationApiKeyCommand : IRotateOrganizationApiKeyCommand\n{\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;\n\n    public RotateOrganizationApiKeyCommand(IOrganizationApiKeyRepository organizationApiKeyRepository)\n    {\n        _organizationApiKeyRepository = organizationApiKeyRepository;\n    }\n\n    public async Task<OrganizationApiKey> RotateApiKeyAsync(OrganizationApiKey organizationApiKey)\n    {\n        organizationApiKey.ApiKey = CoreHelpers.SecureRandomString(30);\n        organizationApiKey.RevisionDate = DateTime.UtcNow;\n        await _organizationApiKeyRepository.UpsertAsync(organizationApiKey);\n        return organizationApiKey;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations.OrganizationConnections;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;\n\npublic class CreateOrganizationConnectionCommand : ICreateOrganizationConnectionCommand\n{\n    private readonly IOrganizationConnectionRepository _organizationConnectionRepository;\n\n    public CreateOrganizationConnectionCommand(IOrganizationConnectionRepository organizationConnectionRepository)\n    {\n        _organizationConnectionRepository = organizationConnectionRepository;\n    }\n\n    public async Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig\n    {\n        return await _organizationConnectionRepository.CreateAsync(connectionData.ToEntity());\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/DeleteOrganizationConnectionCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;\n\npublic class DeleteOrganizationConnectionCommand : IDeleteOrganizationConnectionCommand\n{\n    private readonly IOrganizationConnectionRepository _organizationConnectionRepository;\n\n    public DeleteOrganizationConnectionCommand(IOrganizationConnectionRepository organizationConnectionRepository)\n    {\n        _organizationConnectionRepository = organizationConnectionRepository;\n    }\n\n    public async Task DeleteAsync(OrganizationConnection connection)\n    {\n        await _organizationConnectionRepository.DeleteAsync(connection);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations.OrganizationConnections;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\n\npublic interface ICreateOrganizationConnectionCommand\n{\n    Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/Interfaces/IDeleteOrganizationConnectionCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\n\npublic interface IDeleteOrganizationConnectionCommand\n{\n    Task DeleteAsync(OrganizationConnection connection);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations.OrganizationConnections;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\n\npublic interface IUpdateOrganizationConnectionCommand\n{\n    Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/Interfaces/IValidateBillingSyncKeyCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\n\npublic interface IValidateBillingSyncKeyCommand\n{\n    Task<bool> ValidateBillingSyncKeyAsync(Organization organization, string billingSyncKey);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationConnections;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;\n\npublic class UpdateOrganizationConnectionCommand : IUpdateOrganizationConnectionCommand\n{\n    private readonly IOrganizationConnectionRepository _organizationConnectionRepository;\n\n    public UpdateOrganizationConnectionCommand(IOrganizationConnectionRepository organizationConnectionRepository)\n    {\n        _organizationConnectionRepository = organizationConnectionRepository;\n    }\n\n    public async Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig\n    {\n        if (!connectionData.Id.HasValue)\n        {\n            throw new Exception(\"Cannot update connection, Connection does not exist.\");\n        }\n\n        var connection = await _organizationConnectionRepository.GetByIdAsync(connectionData.Id.Value);\n\n        if (connection == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var entity = connectionData.ToEntity();\n        await _organizationConnectionRepository.UpsertAsync(entity);\n        return entity;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;\n\npublic class ValidateBillingSyncKeyCommand : IValidateBillingSyncKeyCommand\n{\n    private readonly IOrganizationApiKeyRepository _apiKeyRepository;\n\n    public ValidateBillingSyncKeyCommand(\n        IOrganizationApiKeyRepository organizationApiKeyRepository)\n    {\n        _apiKeyRepository = organizationApiKeyRepository;\n    }\n\n    public async Task<bool> ValidateBillingSyncKeyAsync(Organization organization, string billingSyncKey)\n    {\n        if (organization == null)\n        {\n            throw new BadRequestException(\"Invalid organization\");\n        }\n        if (string.IsNullOrWhiteSpace(billingSyncKey))\n        {\n            return false;\n        }\n\n        var orgApiKey = (await _apiKeyRepository.GetManyByOrganizationIdTypeAsync(organization.Id, Core.Enums.OrganizationApiKeyType.BillingSync)).FirstOrDefault();\n        if (string.Equals(orgApiKey.ApiKey, billingSyncKey))\n        {\n            return true;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/CreateOrganizationDomainCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\npublic class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand\n{\n    private readonly IOrganizationDomainRepository _organizationDomainRepository;\n    private readonly IEventService _eventService;\n    private readonly IGlobalSettings _globalSettings;\n\n    public CreateOrganizationDomainCommand(\n        IOrganizationDomainRepository organizationDomainRepository,\n        IEventService eventService,\n        IGlobalSettings globalSettings)\n    {\n        _organizationDomainRepository = organizationDomainRepository;\n        _eventService = eventService;\n        _globalSettings = globalSettings;\n    }\n\n    public async Task<OrganizationDomain> CreateAsync(OrganizationDomain organizationDomain)\n    {\n        //Domains claimed and verified by an organization cannot be claimed\n        var claimedDomain =\n            await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(organizationDomain.DomainName);\n        if (claimedDomain.Any())\n        {\n            throw new ConflictException(\"The domain is not available to be claimed.\");\n        }\n\n        //check for duplicate domain entry for an organization\n        var duplicateOrgDomain =\n            await _organizationDomainRepository.GetDomainByOrgIdAndDomainNameAsync(organizationDomain.OrganizationId,\n                organizationDomain.DomainName);\n        if (duplicateOrgDomain is not null)\n        {\n            throw new ConflictException(\"A domain already exists for this organization.\");\n        }\n\n        // Generate and set DNS TXT Record\n        // DNS-Based Service Discovery RFC: https://www.ietf.org/rfc/rfc6763.txt; see section 6.1\n        // Google uses 43 chars for their TXT record value: https://support.google.com/a/answer/2716802\n        // A random 44 character string was used here to keep parity with prior client-side generation of 47 characters\n        organizationDomain.Txt = string.Join(\"=\", \"bw\", CoreHelpers.RandomString(44));\n        organizationDomain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);\n\n        var orgDomain = await _organizationDomainRepository.CreateAsync(organizationDomain);\n\n        await _eventService.LogOrganizationDomainEventAsync(orgDomain, EventType.OrganizationDomain_Added);\n\n        return orgDomain;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\npublic class DeleteOrganizationDomainCommand : IDeleteOrganizationDomainCommand\n{\n    private readonly IOrganizationDomainRepository _organizationDomainRepository;\n    private readonly IEventService _eventService;\n\n    public DeleteOrganizationDomainCommand(IOrganizationDomainRepository organizationDomainRepository,\n        IEventService eventService)\n    {\n        _organizationDomainRepository = organizationDomainRepository;\n        _eventService = eventService;\n    }\n\n    public async Task DeleteAsync(OrganizationDomain organizationDomain)\n    {\n        await _organizationDomainRepository.DeleteAsync(organizationDomain);\n        await _eventService.LogOrganizationDomainEventAsync(organizationDomain, EventType.OrganizationDomain_Removed);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\npublic class GetOrganizationDomainByIdOrganizationIdQuery : IGetOrganizationDomainByIdOrganizationIdQuery\n{\n    private readonly IOrganizationDomainRepository _organizationDomainRepository;\n\n    public GetOrganizationDomainByIdOrganizationIdQuery(IOrganizationDomainRepository organizationDomainRepository)\n    {\n        _organizationDomainRepository = organizationDomainRepository;\n    }\n\n    public async Task<OrganizationDomain> GetOrganizationDomainByIdOrganizationIdAsync(Guid id, Guid organizationId)\n        => await _organizationDomainRepository.GetDomainByIdOrganizationIdAsync(id, organizationId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\npublic class GetOrganizationDomainByOrganizationIdQuery : IGetOrganizationDomainByOrganizationIdQuery\n{\n    private readonly IOrganizationDomainRepository _organizationDomainRepository;\n\n    public GetOrganizationDomainByOrganizationIdQuery(IOrganizationDomainRepository organizationDomainRepository)\n    {\n        _organizationDomainRepository = organizationDomainRepository;\n    }\n\n    public async Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId)\n        => await _organizationDomainRepository.GetDomainsByOrganizationIdAsync(orgId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/ICreateOrganizationDomainCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\n\npublic interface ICreateOrganizationDomainCommand\n{\n    Task<OrganizationDomain> CreateAsync(OrganizationDomain organizationDomain);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IDeleteOrganizationDomainCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\n\npublic interface IDeleteOrganizationDomainCommand\n{\n    Task DeleteAsync(OrganizationDomain organizationDomain);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByIdOrganizationIdQuery.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\n\npublic interface IGetOrganizationDomainByIdOrganizationIdQuery\n{\n    Task<OrganizationDomain> GetOrganizationDomainByIdOrganizationIdAsync(Guid id, Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IGetOrganizationDomainByOrganizationIdQuery.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\n\npublic interface IGetOrganizationDomainByOrganizationIdQuery\n{\n    Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IOrganizationHasVerifiedDomainsQuery.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\n\npublic interface IOrganizationHasVerifiedDomainsQuery\n{\n    Task<bool> HasVerifiedDomainsAsync(Guid orgId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/Interfaces/IVerifyOrganizationDomainCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\n\npublic interface IVerifyOrganizationDomainCommand\n{\n    Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);\n    Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\npublic class OrganizationHasVerifiedDomainsQuery(IOrganizationDomainRepository domainRepository) : IOrganizationHasVerifiedDomainsQuery\n{\n    public async Task<bool> HasVerifiedDomainsAsync(Guid orgId) =>\n        (await domainRepository.GetDomainsByOrganizationIdAsync(orgId)).Any(od => od.VerifiedDate is not null);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\npublic class VerifyOrganizationDomainCommand(\n    IOrganizationDomainRepository organizationDomainRepository,\n    IDnsResolverService dnsResolverService,\n    IEventService eventService,\n    IGlobalSettings globalSettings,\n    ICurrentContext currentContext,\n    IVNextSavePolicyCommand vNextSavePolicyCommand,\n    IMailService mailService,\n    IOrganizationUserRepository organizationUserRepository,\n    IOrganizationRepository organizationRepository,\n    ILogger<VerifyOrganizationDomainCommand> logger)\n    : IVerifyOrganizationDomainCommand\n{\n    public async Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)\n    {\n        if (currentContext.UserId is null)\n        {\n            throw new InvalidOperationException(\n                $\"{nameof(UserVerifyOrganizationDomainAsync)} can only be called by a user. \" +\n                $\"Please call {nameof(SystemVerifyOrganizationDomainAsync)} for system users.\");\n        }\n\n        var actingUser = new StandardUser(currentContext.UserId.Value, await currentContext.OrganizationOwner(organizationDomain.OrganizationId));\n\n        var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain, actingUser);\n\n        await eventService.LogOrganizationDomainEventAsync(domainVerificationResult,\n            domainVerificationResult.VerifiedDate != null\n                ? EventType.OrganizationDomain_Verified\n                : EventType.OrganizationDomain_NotVerified);\n\n        await organizationDomainRepository.ReplaceAsync(domainVerificationResult);\n\n        return domainVerificationResult;\n    }\n\n    public async Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)\n    {\n        var actingUser = new SystemUser(EventSystemUser.DomainVerification);\n\n        organizationDomain.SetJobRunCount();\n\n        var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain, actingUser);\n\n        if (domainVerificationResult.VerifiedDate is not null)\n        {\n            logger.LogInformation(Constants.BypassFiltersEventId, \"Successfully validated domain\");\n\n            await eventService.LogOrganizationDomainEventAsync(domainVerificationResult,\n                EventType.OrganizationDomain_Verified,\n                EventSystemUser.DomainVerification);\n        }\n        else\n        {\n            domainVerificationResult.SetNextRunDate(globalSettings.DomainVerification.VerificationInterval);\n\n            await eventService.LogOrganizationDomainEventAsync(domainVerificationResult,\n                EventType.OrganizationDomain_NotVerified,\n                EventSystemUser.DomainVerification);\n\n            logger.LogInformation(Constants.BypassFiltersEventId,\n                \"Verification for organization {OrgId} with domain {Domain} failed\",\n                domainVerificationResult.OrganizationId, domainVerificationResult.DomainName);\n        }\n\n        await organizationDomainRepository.ReplaceAsync(domainVerificationResult);\n\n        return domainVerificationResult;\n    }\n\n    private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain, IActingUser actingUser)\n    {\n        domain.SetLastCheckedDate();\n\n        if (domain.VerifiedDate is not null)\n        {\n            await organizationDomainRepository.ReplaceAsync(domain);\n            throw new ConflictException(\"Domain has already been verified.\");\n        }\n\n        var claimedDomain =\n            await organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);\n\n        if (claimedDomain.Count > 0)\n        {\n            await organizationDomainRepository.ReplaceAsync(domain);\n            throw new ConflictException(\"The domain is not available to be claimed.\");\n        }\n\n        try\n        {\n            if (await dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))\n            {\n                domain.SetVerifiedDate();\n\n                await DomainVerificationSideEffectsAsync(domain, actingUser);\n            }\n        }\n        catch (Exception e)\n        {\n            logger.LogError(\"Error verifying Organization domain: {domain}. {errorMessage}\",\n                domain.DomainName, e.Message);\n        }\n\n        return domain;\n    }\n\n    private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)\n    {\n        await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);\n        await SendVerifiedDomainUserEmailAsync(domain);\n    }\n\n    private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser)\n    {\n        var policyUpdate = new PolicyUpdate\n        {\n            OrganizationId = organizationId,\n            Type = PolicyType.SingleOrg,\n            Enabled = true,\n            PerformedBy = actingUser\n        };\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate, actingUser);\n        await vNextSavePolicyCommand.SaveAsync(savePolicyModel);\n    }\n\n    private async Task SendVerifiedDomainUserEmailAsync(OrganizationDomain domain)\n    {\n        var orgUserUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(domain.OrganizationId);\n\n        var domainUserEmails = orgUserUsers\n            .Where(ou => ou.Email.ToLower().EndsWith($\"@{domain.DomainName.ToLower()}\") &&\n                         ou.Status != OrganizationUserStatusType.Revoked &&\n                         ou.Status != OrganizationUserStatusType.Invited)\n            .Select(ou => ou.Email);\n\n        var organization = await organizationRepository.GetByIdAsync(domain.OrganizationId);\n\n        await mailService.SendClaimedDomainUserEmailAsync(new ClaimedUserDomainClaimedEmails(domainUserEmails, organization, domain.DomainName));\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements.Errors;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationUsers;\n\npublic class AcceptOrgUserCommand : IAcceptOrgUserCommand\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IPolicyService _policyService;\n    private readonly IMailService _mailService;\n    private readonly IUserRepository _userRepository;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;\n    private readonly IFeatureService _featureService;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IAutomaticUserConfirmationPolicyEnforcementValidator _automaticUserConfirmationPolicyEnforcementValidator;\n    private readonly IPushAutoConfirmNotificationCommand _pushAutoConfirmNotificationCommand;\n    private readonly IDeleteEmergencyAccessCommand _deleteEmergencyAccessCommand;\n\n    public AcceptOrgUserCommand(\n        IOrganizationUserRepository organizationUserRepository,\n        IOrganizationRepository organizationRepository,\n        IPolicyService policyService,\n        IMailService mailService,\n        IUserRepository userRepository,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,\n        IFeatureService featureService,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator,\n        IPushAutoConfirmNotificationCommand pushAutoConfirmNotificationCommand,\n        IDeleteEmergencyAccessCommand deleteEmergencyAccessCommand)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _organizationRepository = organizationRepository;\n        _policyService = policyService;\n        _mailService = mailService;\n        _userRepository = userRepository;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;\n        _featureService = featureService;\n        _policyRequirementQuery = policyRequirementQuery;\n        _automaticUserConfirmationPolicyEnforcementValidator = automaticUserConfirmationPolicyEnforcementValidator;\n        _pushAutoConfirmNotificationCommand = pushAutoConfirmNotificationCommand;\n        _deleteEmergencyAccessCommand = deleteEmergencyAccessCommand;\n    }\n\n    public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,\n        IUserService userService)\n    {\n        var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);\n        if (orgUser == null)\n        {\n            throw new BadRequestException(\"User invalid.\");\n        }\n\n        var tokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(\n            _orgUserInviteTokenDataFactory, emailToken, orgUser);\n\n        if (!tokenValid)\n        {\n            throw new BadRequestException(\"Invalid token.\");\n        }\n\n        var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(\n            orgUser.OrganizationId, user.Email, true);\n        if (existingOrgUserCount > 0)\n        {\n            if (orgUser.Status == OrganizationUserStatusType.Accepted)\n            {\n                throw new BadRequestException(\"Invitation already accepted. You will receive an email when your organization membership is confirmed.\");\n            }\n            throw new BadRequestException(\"You are already part of this organization.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(orgUser.Email) ||\n            !orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))\n        {\n            throw new BadRequestException(\"User email does not match invite.\");\n        }\n\n        var organizationUser = await AcceptOrgUserAsync(orgUser, user, userService);\n\n        // Verify user email if they accept org invite via email link\n        if (user.EmailVerified == false)\n        {\n            user.EmailVerified = true;\n            await _userRepository.ReplaceAsync(user);\n        }\n\n        return organizationUser;\n    }\n\n    public async Task<OrganizationUser> AcceptOrgUserByOrgSsoIdAsync(string orgSsoIdentifier, User user, IUserService userService)\n    {\n        var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier);\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization invalid.\");\n        }\n\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);\n        if (orgUser == null)\n        {\n            throw new BadRequestException(\"User not found within organization.\");\n        }\n\n        return await AcceptOrgUserAsync(orgUser, user, userService);\n    }\n\n    public async Task<OrganizationUser> AcceptOrgUserByOrgIdAsync(Guid organizationId, User user, IUserService userService)\n    {\n        var org = await _organizationRepository.GetByIdAsync(organizationId);\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization invalid.\");\n        }\n\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);\n        if (orgUser == null)\n        {\n            throw new BadRequestException(\"User not found within organization.\");\n        }\n\n        return await AcceptOrgUserAsync(orgUser, user, userService);\n    }\n\n    public async Task<OrganizationUser> AcceptOrgUserAsync(OrganizationUser orgUser, User user,\n        IUserService userService)\n    {\n        if (orgUser.Status == OrganizationUserStatusType.Revoked)\n        {\n            throw new BadRequestException(\"Your organization access has been revoked.\");\n        }\n\n        if (orgUser.Status != OrganizationUserStatusType.Invited)\n        {\n            throw new BadRequestException(\"Already accepted.\");\n        }\n\n        if (orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin)\n        {\n            var org = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId);\n            if (org.PlanType == PlanType.Free)\n            {\n                var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(\n                    user.Id);\n                if (adminCount > 0)\n                {\n                    throw new BadRequestException(\"You can only be an admin of one free organization.\");\n                }\n            }\n        }\n\n        var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id);\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            await HandleAutomaticUserConfirmationPolicyAsync(orgUser, allOrgUsers, user);\n        }\n\n        await ValidateSingleOrganizationPolicyAsync(orgUser, allOrgUsers, user);\n\n        // Enforce Two Factor Authentication Policy of organization user is trying to join\n        await ValidateTwoFactorAuthenticationPolicyAsync(user, orgUser.OrganizationId);\n\n        orgUser.Status = OrganizationUserStatusType.Accepted;\n        orgUser.UserId = user.Id;\n        orgUser.Email = null;\n\n        await _organizationUserRepository.ReplaceAsync(orgUser);\n\n        var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin);\n        var adminEmails = admins.Select(a => a.Email).Distinct().ToList();\n\n        if (adminEmails.Count > 0)\n        {\n            var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId);\n            await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails);\n        }\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            await _pushAutoConfirmNotificationCommand.PushAsync(user.Id, orgUser.OrganizationId);\n        }\n\n        return orgUser;\n    }\n\n    private async Task ValidateSingleOrganizationPolicyAsync(OrganizationUser orgUser, ICollection<OrganizationUser> allOrgUsers, User user)\n    {\n        var singleOrgRequirement = await _policyRequirementQuery.GetAsync<SingleOrganizationPolicyRequirement>(user.Id);\n        var error = singleOrgRequirement.CanJoinOrganization(orgUser.OrganizationId, allOrgUsers);\n        if (error is not null)\n        {\n            var singleOrgErrorMessage = error switch\n            {\n                UserIsAMemberOfAnotherOrganization => \"You cannot accept this invite until you leave or remove all other organizations.\",\n                UserIsAMemberOfAnOrganizationThatHasSingleOrgPolicy => \"You cannot accept this invite because you are in another organization which forbids it.\",\n                _ => error.Message\n            };\n\n            throw new BadRequestException(singleOrgErrorMessage);\n        }\n    }\n\n    private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId)\n    {\n        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))\n        {\n            if (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))\n            {\n                // If the user has two-step login enabled, we skip checking the 2FA policy\n                return;\n            }\n\n            var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);\n            if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))\n            {\n                throw new BadRequestException(\"You cannot join this organization until you enable two-step login on your user account.\");\n            }\n\n            return;\n        }\n\n        if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))\n        {\n            var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,\n                PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);\n            if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId))\n            {\n                throw new BadRequestException(\"You cannot join this organization until you enable two-step login on your user account.\");\n            }\n        }\n    }\n\n    private async Task HandleAutomaticUserConfirmationPolicyAsync(OrganizationUser orgUser,\n        ICollection<OrganizationUser> allOrgUsers,\n        User user)\n    {\n        var policyRequirement = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(\n            user.Id);\n\n        var error = (await _automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.OrganizationId,\n                    allOrgUsers.Append(orgUser),\n                    user),\n                policyRequirement))\n            .Match(\n                error => error.Message,\n                _ => string.Empty\n            );\n\n        if (!string.IsNullOrEmpty(error))\n        {\n            throw new BadRequestException(error);\n        }\n\n        if (policyRequirement.IsEnabled(orgUser.OrganizationId))\n        {\n            await _deleteEmergencyAccessCommand.DeleteAllByUserIdAsync(user.Id);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;\n\npublic class OrganizationUserUserDetailsAuthorizationHandler\n    : AuthorizationHandler<OrganizationUserUserDetailsOperationRequirement, OrganizationScope>\n{\n    private readonly ICurrentContext _currentContext;\n\n    public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext)\n    {\n        _currentContext = currentContext;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        OrganizationUserUserDetailsOperationRequirement requirement, OrganizationScope organizationScope)\n    {\n        var authorized = false;\n\n        switch (requirement)\n        {\n            case not null when requirement.Name == nameof(OrganizationUserUserDetailsOperations.ReadAll):\n                authorized = await CanReadAllAsync(organizationScope);\n                break;\n        }\n\n        if (authorized)\n        {\n            context.Succeed(requirement!);\n        }\n    }\n\n    private async Task<bool> CanReadAllAsync(Guid organizationId)\n    {\n        // Admins can access this for general user management\n        var organization = _currentContext.GetOrganization(organizationId);\n        if (organization is\n        { Type: OrganizationUserType.Owner } or\n        { Type: OrganizationUserType.Admin } or\n        { Permissions.ManageUsers: true })\n        {\n            return true;\n        }\n\n        // Allow provider users to read all organization users if they are a provider for the target organization\n        return await _currentContext.ProviderUserForOrgAsync(organizationId);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserDetailsOperations.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;\n\npublic class OrganizationUserUserDetailsOperationRequirement : OperationAuthorizationRequirement;\n\npublic static class OrganizationUserUserDetailsOperations\n{\n    public static OrganizationUserUserDetailsOperationRequirement ReadAll = new() { Name = nameof(ReadAll) };\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsOperations.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;\n\npublic class OrganizationUserUserMiniDetailsOperationRequirement : OperationAuthorizationRequirement;\n\npublic static class OrganizationUserUserMiniDetailsOperations\n{\n    public static readonly OrganizationUserUserMiniDetailsOperationRequirement ReadAll = new() { Name = nameof(ReadAll) };\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Data.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\nusing CommandResult = Bit.Core.AdminConsole.Utilities.v2.Results.CommandResult;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\n\npublic class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository,\n    IOrganizationRepository organizationRepository,\n    IAutomaticallyConfirmOrganizationUsersValidator validator,\n    IEventService eventService,\n    IUserRepository userRepository,\n    IPushRegistrationService pushRegistrationService,\n    IDeviceRepository deviceRepository,\n    IPushNotificationService pushNotificationService,\n    IPolicyRequirementQuery policyRequirementQuery,\n    ICollectionRepository collectionRepository,\n    ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand,\n    TimeProvider timeProvider,\n    ILogger<AutomaticallyConfirmOrganizationUserCommand> logger) : IAutomaticallyConfirmOrganizationUserCommand\n{\n    public async Task<CommandResult> AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request)\n    {\n        var validatorRequest = await RetrieveDataAsync(request);\n\n        var validatedData = await validator.ValidateAsync(validatorRequest);\n\n        return await validatedData.Match<Task<CommandResult>>(\n            error => Task.FromResult(new CommandResult(error)),\n            async _ =>\n            {\n                var userToConfirm = new AcceptedOrganizationUserToConfirm\n                {\n                    OrganizationUserId = validatedData.Request.OrganizationUser!.Id,\n                    UserId = validatedData.Request.OrganizationUser.UserId!.Value,\n                    Key = validatedData.Request.Key\n                };\n\n                // This operation is idempotent. If false, the user is already confirmed and no additional side effects are required.\n                if (!await organizationUserRepository.ConfirmOrganizationUserAsync(userToConfirm))\n                {\n                    return new None();\n                }\n\n                await CreateDefaultCollectionsAsync(validatedData.Request);\n\n                await Task.WhenAll(\n                    LogOrganizationUserConfirmedEventAsync(validatedData.Request),\n                    SendConfirmedOrganizationUserEmailAsync(validatedData.Request),\n                    SyncOrganizationKeysAsync(validatedData.Request)\n                );\n\n                return new None();\n            }\n        );\n    }\n\n    private async Task SyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)\n    {\n        await DeleteDeviceRegistrationAsync(request);\n        await PushSyncOrganizationKeysAsync(request);\n    }\n\n    private async Task CreateDefaultCollectionsAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)\n    {\n        try\n        {\n            if (!await ShouldCreateDefaultCollectionAsync(request))\n            {\n                return;\n            }\n\n            await collectionRepository.CreateDefaultCollectionsAsync(\n                request.Organization!.Id,\n                [request.OrganizationUser!.Id],\n                request.DefaultUserCollectionName);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Failed to create default collection for user.\");\n        }\n    }\n\n    /// <summary>\n    /// Determines whether a default collection should be created for an organization user during the confirmation process.\n    /// </summary>\n    /// <param name=\"request\">\n    /// The validation request containing information about the user, organization, and collection settings.\n    /// </param>\n    /// <returns>The result is a boolean value indicating whether a default collection should be created.</returns>\n    private async Task<bool> ShouldCreateDefaultCollectionAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>\n        !string.IsNullOrWhiteSpace(request.DefaultUserCollectionName)\n        && request.Organization!.UseMyItems\n        && (await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(request.OrganizationUser!.UserId!.Value))\n            .GetDefaultCollectionRequestOnConfirm(request.Organization!.Id).ShouldCreateDefaultCollection;\n\n    private async Task PushSyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)\n    {\n        try\n        {\n            await pushNotificationService.PushSyncOrgKeysAsync(request.OrganizationUser!.UserId!.Value);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Failed to push organization keys.\");\n        }\n    }\n\n    private async Task LogOrganizationUserConfirmedEventAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)\n    {\n        try\n        {\n            await eventService.LogOrganizationUserEventAsync(request.OrganizationUser,\n                EventType.OrganizationUser_AutomaticallyConfirmed,\n                timeProvider.GetUtcNow().UtcDateTime);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Failed to log OrganizationUser_AutomaticallyConfirmed event.\");\n        }\n    }\n\n    private async Task SendConfirmedOrganizationUserEmailAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)\n    {\n        try\n        {\n            var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);\n\n            await SendOrganizationConfirmedEmailAsync(request.Organization!, user!.Email, request.OrganizationUser.AccessSecretsManager);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Failed to send OrganizationUserConfirmed.\");\n        }\n    }\n\n    private async Task DeleteDeviceRegistrationAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)\n    {\n        try\n        {\n            var devices = (await deviceRepository.GetManyByUserIdAsync(request.OrganizationUser!.UserId!.Value))\n                    .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))\n                    .Select(d => d.Id.ToString());\n\n            await pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, request.Organization!.Id.ToString());\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Failed to delete device registration.\");\n        }\n    }\n\n    private async Task<AutomaticallyConfirmOrganizationUserValidationRequest> RetrieveDataAsync(\n        AutomaticallyConfirmOrganizationUserRequest request)\n    {\n        return new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            OrganizationUserId = request.OrganizationUserId,\n            OrganizationId = request.OrganizationId,\n            Key = request.Key,\n            DefaultUserCollectionName = request.DefaultUserCollectionName,\n            PerformedBy = request.PerformedBy,\n            OrganizationUser = await organizationUserRepository.GetByIdAsync(request.OrganizationUserId),\n            Organization = await organizationRepository.GetByIdAsync(request.OrganizationId)\n        };\n    }\n\n    /// <summary>\n    /// Sends the organization confirmed email using the new mailer pattern.\n    /// </summary>\n    /// <param name=\"organization\">The organization the user was confirmed to.</param>\n    /// <param name=\"userEmail\">The email address of the confirmed user.</param>\n    /// <param name=\"accessSecretsManager\">Whether the user has access to Secrets Manager.</param>\n    internal async Task SendOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager)\n    {\n        await sendOrganizationConfirmationCommand.SendConfirmationAsync(organization, userEmail, accessSecretsManager);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserRequest.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\n\n/// <summary>\n/// Automatically Confirm User Command Request\n/// </summary>\npublic record AutomaticallyConfirmOrganizationUserRequest\n{\n    public required Guid OrganizationUserId { get; init; }\n    public required Guid OrganizationId { get; init; }\n    public required string Key { get; init; }\n    public required string DefaultUserCollectionName { get; init; }\n    public required IActingUser PerformedBy { get; init; }\n}\n\n/// <summary>\n/// Automatically Confirm User Validation Request\n/// </summary>\n/// <remarks>\n/// This is used to hold retrieved data and pass it to the validator\n/// </remarks>\npublic record AutomaticallyConfirmOrganizationUserValidationRequest : AutomaticallyConfirmOrganizationUserRequest\n{\n    public OrganizationUser? OrganizationUser { get; set; }\n    public Organization? Organization { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUsersValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Utilities.v2;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\n\npublic class AutomaticallyConfirmOrganizationUsersValidator(\n    IOrganizationUserRepository organizationUserRepository,\n    ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n    IPolicyRequirementQuery policyRequirementQuery,\n    IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator,\n    IUserService userService,\n    IPolicyQuery policyQuery) : IAutomaticallyConfirmOrganizationUsersValidator\n{\n    public async Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(\n        AutomaticallyConfirmOrganizationUserValidationRequest request)\n    {\n        // User must exist\n        if (request is { OrganizationUser: null } || request.OrganizationUser is { UserId: null })\n        {\n            return Invalid(request, new UserNotFoundError());\n        }\n\n        // Organization must exist\n        if (request is { Organization: null })\n        {\n            return Invalid(request, new OrganizationNotFound());\n        }\n\n        // User must belong to the organization\n        if (request.OrganizationUser.OrganizationId != request.Organization.Id)\n        {\n            return Invalid(request, new OrganizationUserIdIsInvalid());\n        }\n\n        // User must be accepted\n        if (request is { OrganizationUser.Status: not OrganizationUserStatusType.Accepted })\n        {\n            return Invalid(request, new UserIsNotAccepted());\n        }\n\n        // User must be of type User\n        if (request is { OrganizationUser.Type: not OrganizationUserType.User })\n        {\n            return Invalid(request, new UserIsNotUserType());\n        }\n\n        if (!await OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(request))\n        {\n            return Invalid(request, new AutomaticallyConfirmUsersPolicyIsNotEnabled());\n        }\n\n        if (!await OrganizationUserConformsToTwoFactorRequiredPolicyAsync(request))\n        {\n            return Invalid(request, new UserDoesNotHaveTwoFactorEnabled());\n        }\n\n        if (await OrganizationUserConformsToAutomaticUserConfirmationPolicyAsync(request) is { } error)\n        {\n            return Invalid(request, error);\n        }\n\n        return Valid(request);\n    }\n\n    private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>\n        (await policyQuery.RunAsync(request.OrganizationId, PolicyType.AutomaticUserConfirmation)).Enabled\n        && request.Organization is { UseAutomaticUserConfirmation: true };\n\n    private async Task<bool> OrganizationUserConformsToTwoFactorRequiredPolicyAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)\n    {\n        if ((await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync([request.OrganizationUser!.UserId!.Value]))\n            .Any(x => x.userId == request.OrganizationUser.UserId && x.twoFactorIsEnabled))\n        {\n            return true;\n        }\n\n        return !(await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(request.OrganizationUser.UserId!.Value))\n            .IsTwoFactorRequiredForOrganization(request.Organization!.Id);\n    }\n\n    /// <summary>\n    /// Validates whether the specified organization user complies with the automatic user confirmation policy.\n    /// This includes checks across all organizations the user is associated with to ensure they meet the compliance criteria.\n    ///\n    /// We are not checking single organization policy compliance here because automatically confirm users policy enforces\n    /// a stricter version and applies to all users. If you are compliant with Auto Confirm, you'll be in compliance with\n    /// Single Org.\n    /// </summary>\n    /// <param name=\"request\">\n    /// The request model encapsulates the current organization, the user being validated, and all organization users associated\n    /// with that user.\n    /// </param>\n    /// <returns>\n    /// An <see cref=\"Error\"/> if the user fails to meet the automatic user confirmation policy, or null if the validation succeeds.\n    /// </returns>\n    private async Task<Error?> OrganizationUserConformsToAutomaticUserConfirmationPolicyAsync(\n        AutomaticallyConfirmOrganizationUserValidationRequest request)\n    {\n        var allOrganizationUsersForUser = await organizationUserRepository\n            .GetManyByUserAsync(request.OrganizationUser!.UserId!.Value);\n\n        var user = await userService.GetUserByIdAsync(request.OrganizationUser!.UserId!.Value);\n\n        return (await automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(\n                    request.OrganizationId,\n                    allOrganizationUsersForUser,\n                    user)))\n            .Match<Error?>(\n                error => error,\n                _ => null\n            );\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\n\npublic record OrganizationNotFound() : NotFoundError(\"Invalid organization\");\npublic record FailedToWriteToEventLog() : InternalError(\"Failed to write to event log\");\npublic record UserIsNotUserType() : BadRequestError(\"Only organization users with the User role can be automatically confirmed\");\npublic record UserIsNotAccepted() : BadRequestError(\"Cannot confirm user that has not accepted the invitation.\");\npublic record OrganizationUserIdIsInvalid() : BadRequestError(\"Invalid organization user id.\");\npublic record UserDoesNotHaveTwoFactorEnabled() : BadRequestError(\"User does not have two-step login enabled.\");\npublic record UserCannotBelongToAnotherOrganization() : BadRequestError(\"Cannot confirm this member to the organization until they leave or remove all other organizations\");\npublic record OtherOrganizationDoesNotAllowOtherMembership() : BadRequestError(\"Cannot confirm this member to the organization because they are in another organization which forbids it.\");\npublic record AutomaticallyConfirmUsersPolicyIsNotEnabled() : BadRequestError(\"Cannot confirm this member because the Automatically Confirm Users policy is not enabled.\");\npublic record ProviderUsersCannotJoin() : BadRequestError(\"An organization the user is a part of has enabled Automatic User Confirmation policy, and it does not support provider users joining.\");\npublic record UserCannotJoinProvider() : BadRequestError(\"An organization the user is a part of has enabled Automatic User Confirmation policy, and it does not support the user joining a provider.\");\npublic record CurrentOrganizationUserIsNotPresentInRequest() : BadRequestError(\"The current organization user does not exist in the request.\");\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/IAutomaticallyConfirmOrganizationUsersValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2.Validation;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\n\npublic interface IAutomaticallyConfirmOrganizationUsersValidator\n{\n    Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(\n        AutomaticallyConfirmOrganizationUserValidationRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommand.cs",
    "content": "﻿using System.Net;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationUserAutoConfirmation;\nusing Bit.Core.Platform.Mail.Mailer;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\nusing CommandResult = Bit.Core.AdminConsole.Utilities.v2.Results.CommandResult;\nusing Error = Bit.Core.AdminConsole.Utilities.v2.Error;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\n\npublic record OrganizationAutoConfirmEnabledNotificationRequest(Organization Organization, ICollection<string> Emails);\n\npublic record NoEmailsWereProvided() : Error(\"No emails were provided\");\n\npublic record EmailSendingFailed() : Error(\"Failed to send email to organization admins\");\n\npublic interface IOrganizationAutoConfirmEnabledNotificationCommand\n{\n    Task<CommandResult> SendEmailAsync(OrganizationAutoConfirmEnabledNotificationRequest request);\n}\n\npublic class OrganizationAutoConfirmEnabledNotificationCommand(\n    IMailer mailer,\n    ILogger<OrganizationAutoConfirmEnabledNotificationCommand> logger,\n    GlobalSettings globalSettings) : IOrganizationAutoConfirmEnabledNotificationCommand\n{\n    public async Task<CommandResult> SendEmailAsync(OrganizationAutoConfirmEnabledNotificationRequest request)\n    {\n        if (request.Emails.Count == 0)\n        {\n            return new NoEmailsWereProvided();\n        }\n\n        var mail = new OrganizationAutoConfirmationEnabled\n        {\n            ToEmails = request.Emails,\n            View = new OrganizationAutoConfirmationEnabledView\n            {\n                WebVaultUrl = globalSettings.BaseServiceUri.Vault + \"#/organizations/\" + request.Organization.Id + \"/settings/policies\"\n            },\n            Subject = $\"Automatic user confirmation is available for {WebUtility.HtmlEncode(request.Organization.Name)}\"\n        };\n\n        try\n        {\n            await mailer.SendEmail(mail);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex,\n                \"Failed to send email to organization admins for Auto Confirm feature enablement. Organization: {OrganizationId}\",\n                request.Organization.Id);\n\n            return new EmailSendingFailed();\n        }\n\n        return new None();\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/README.md",
    "content": "# Automatic User Confirmation\n\nOwned by: admin-console\n\nAutomatic confirmation requests are server driven events that are sent to the admin's client where via a background service the confirmation will occur. The basic model\nfor the workflow is as follows:\n\n- The Api server sends an invite email to a user.\n- The user accepts the invite request, which is sent back to the Api server\n- The Api server sends a push-notification with the OrganizationId and UserId to a client admin session.\n- The Client performs the key exchange in the background and POSTs the ConfirmRequest back to the Api server\n- The Api server runs the OrgUser_Confirm sproc to confirm the user in the DB\n\nThis Feature has the following security measures in place in order to achieve our security goals:\n\n- The single organization exemption for admins/owners is removed for this policy.\n  - This is enforced by preventing enabling the policy and organization plan feature if there are non-compliant users\n- Emergency access is removed for all organization users\n- Automatic confirmation will only apply to the User role (You cannot auto confirm admins/owners to an organization)\n- The organization has no members with the Provider user type.\n  - This will also prevent the policy and organization plan feature from being enabled\n  - This will prevent sending organization invites to provider users\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements.Errors;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IUserRepository _userRepository;\n    private readonly IEventService _eventService;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IPushRegistrationService _pushRegistrationService;\n    private readonly IPolicyService _policyService;\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IFeatureService _featureService;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IAutomaticUserConfirmationPolicyEnforcementValidator _automaticUserConfirmationPolicyEnforcementValidator;\n    private readonly ISendOrganizationConfirmationCommand _sendOrganizationConfirmationCommand;\n    private readonly IDeleteEmergencyAccessCommand _deleteEmergencyAccessCommand;\n\n    public ConfirmOrganizationUserCommand(\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IUserRepository userRepository,\n        IEventService eventService,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IPushNotificationService pushNotificationService,\n        IPushRegistrationService pushRegistrationService,\n        IPolicyService policyService,\n        IDeviceRepository deviceRepository,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IFeatureService featureService,\n        ICollectionRepository collectionRepository,\n        IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator,\n        ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand,\n        IDeleteEmergencyAccessCommand deleteEmergencyAccessCommand)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _userRepository = userRepository;\n        _eventService = eventService;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _pushNotificationService = pushNotificationService;\n        _pushRegistrationService = pushRegistrationService;\n        _policyService = policyService;\n        _deviceRepository = deviceRepository;\n        _policyRequirementQuery = policyRequirementQuery;\n        _featureService = featureService;\n        _collectionRepository = collectionRepository;\n        _automaticUserConfirmationPolicyEnforcementValidator = automaticUserConfirmationPolicyEnforcementValidator;\n        _sendOrganizationConfirmationCommand = sendOrganizationConfirmationCommand;\n        _deleteEmergencyAccessCommand = deleteEmergencyAccessCommand;\n    }\n    public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,\n        Guid confirmingUserId, string defaultUserCollectionName = null)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n        var result = await SaveChangesToDatabaseAsync(\n            organizationId,\n            new Dictionary<Guid, string>() { { organizationUserId, key } },\n            confirmingUserId,\n            organization);\n\n        if (!result.Any())\n        {\n            throw new BadRequestException(\"User not valid.\");\n        }\n\n        var (orgUser, error) = result[0];\n        if (error != \"\")\n        {\n            throw new BadRequestException(error);\n        }\n\n        await CreateManyDefaultCollectionsAsync(organization, [orgUser], defaultUserCollectionName);\n\n        return orgUser;\n    }\n\n    public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,\n        Guid confirmingUserId, string defaultUserCollectionName = null)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n        var result = await SaveChangesToDatabaseAsync(organizationId, keys, confirmingUserId, organization);\n\n        var confirmedOrganizationUsers = result\n            .Where(r => string.IsNullOrEmpty(r.Item2))\n            .Select(r => r.Item1)\n            .ToList();\n\n        await CreateManyDefaultCollectionsAsync(organization, confirmedOrganizationUsers, defaultUserCollectionName);\n\n        return result;\n    }\n\n    private async Task<List<Tuple<OrganizationUser, string>>> SaveChangesToDatabaseAsync(Guid organizationId, Dictionary<Guid, string> keys,\n        Guid confirmingUserId, Organization organization)\n    {\n        var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);\n        var validSelectedOrganizationUsers = selectedOrganizationUsers\n            .Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)\n            .ToList();\n\n        if (!validSelectedOrganizationUsers.Any())\n        {\n            return new List<Tuple<OrganizationUser, string>>();\n        }\n\n        var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList();\n        var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);\n\n        var users = await _userRepository.GetManyAsync(validSelectedUserIds);\n        var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);\n\n        var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);\n        var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value)\n            .ToDictionary(u => u.Key, u => u.ToList());\n\n        var succeededUsers = new List<OrganizationUser>();\n        var result = new List<Tuple<OrganizationUser, string>>();\n\n        foreach (var user in users)\n        {\n            if (!keyedFilteredUsers.ContainsKey(user.Id))\n            {\n                continue;\n            }\n            var orgUser = keyedFilteredUsers[user.Id];\n            var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>());\n            try\n            {\n                if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin\n                    || orgUser.Type == OrganizationUserType.Owner))\n                {\n                    // Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.\n                    var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);\n                    if (adminCount > 0)\n                    {\n                        throw new BadRequestException(\"User can only be an admin of one free organization.\");\n                    }\n                }\n\n                var userTwoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;\n                await CheckPoliciesAsync(organizationId, user, orgUsers, userTwoFactorEnabled);\n                orgUser.Status = OrganizationUserStatusType.Confirmed;\n                orgUser.Key = keys[orgUser.Id];\n                orgUser.Email = null;\n\n                await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);\n                await SendOrganizationConfirmedEmailAsync(organization, user.Email, orgUser.AccessSecretsManager);\n                succeededUsers.Add(orgUser);\n                result.Add(Tuple.Create(orgUser, \"\"));\n            }\n            catch (BadRequestException e)\n            {\n                result.Add(Tuple.Create(orgUser, e.Message));\n            }\n        }\n\n        await _organizationUserRepository.ReplaceManyAsync(succeededUsers);\n        await DeleteAndPushUserRegistrationAsync(organizationId, succeededUsers.Select(u => u.UserId!.Value));\n\n        return result;\n    }\n\n    private async Task CheckPoliciesAsync(Guid organizationId, User user,\n        ICollection<OrganizationUser> orgUsers, bool userTwoFactorEnabled)\n    {\n        // Enforce Two Factor Authentication Policy for this organization\n        await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled);\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var policyRequirement = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(\n                user.Id);\n\n            var error = (await _automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(\n                    new AutomaticUserConfirmationPolicyEnforcementRequest(\n                        organizationId,\n                        orgUsers,\n                        user),\n                    policyRequirement))\n                .Match(\n                    error => new BadRequestException(error.Message),\n                    _ => null\n                );\n\n            if (error is not null)\n            {\n                throw error;\n            }\n\n            if (policyRequirement.IsEnabled(organizationId))\n            {\n                await _deleteEmergencyAccessCommand.DeleteAllByUserIdAsync(user.Id);\n            }\n        }\n\n        var singleOrgRequirement = await _policyRequirementQuery.GetAsync<SingleOrganizationPolicyRequirement>(user.Id);\n        var singleOrgError = singleOrgRequirement.CanJoinOrganization(organizationId, orgUsers);\n        if (singleOrgError is not null)\n        {\n            var singleOrgErrorMessage = singleOrgError switch\n            {\n                UserIsAMemberOfAnotherOrganization => $\"{user.Email} cannot be confirmed until they leave or remove all other organizations.\",\n                UserIsAMemberOfAnOrganizationThatHasSingleOrgPolicy => $\"{user.Email} cannot be confirmed because they are in another organization which forbids it.\",\n                _ => singleOrgError.Message\n            };\n\n            throw new BadRequestException(singleOrgErrorMessage);\n        }\n    }\n\n    private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled)\n    {\n        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))\n        {\n            if (userTwoFactorEnabled)\n            {\n                // If the user has two-step login enabled, we skip checking the 2FA policy\n                return;\n            }\n\n            var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);\n            if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))\n            {\n                throw new BadRequestException(\"User does not have two-step login enabled.\");\n            }\n\n            return;\n        }\n\n        var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))\n            .Any(p => p.OrganizationId == organizationId);\n        if (orgRequiresTwoFactor && !userTwoFactorEnabled)\n        {\n            throw new BadRequestException(\"User does not have two-step login enabled.\");\n        }\n    }\n\n    private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, IEnumerable<Guid> userIds)\n    {\n        foreach (var userId in userIds)\n        {\n            var devices = await GetUserDeviceIdsAsync(userId);\n            await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,\n                organizationId.ToString());\n            await _pushNotificationService.PushSyncOrgKeysAsync(userId);\n        }\n    }\n\n    private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)\n    {\n        var devices = await _deviceRepository.GetManyByUserIdAsync(userId);\n        return devices\n            .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))\n            .Select(d => d.Id.ToString());\n    }\n\n    /// <summary>\n    /// Creates default collections for multiple users if required by the Organization Data Ownership policy.\n    /// </summary>\n    /// <param name=\"organization\">The organization.</param>\n    /// <param name=\"confirmedOrganizationUsers\">The confirmed organization users.</param>\n    /// <param name=\"defaultUserCollectionName\">The encrypted default user collection name.</param>\n    private async Task CreateManyDefaultCollectionsAsync(Organization organization,\n        IEnumerable<OrganizationUser> confirmedOrganizationUsers, string defaultUserCollectionName)\n    {\n        // Skip if no collection name provided (backwards compatibility)\n        if (string.IsNullOrWhiteSpace(defaultUserCollectionName))\n        {\n            return;\n        }\n\n        // Skip if organization has disabled My Items\n        if (!organization.UseMyItems)\n        {\n            return;\n        }\n\n        var confirmedUserIds = confirmedOrganizationUsers\n            .Select(s => s.UserId!.Value)\n            .ToList();\n\n        var policiesForUsers = await _policyRequirementQuery\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(confirmedUserIds);\n\n        var eligibleOrganizationUserIds = policiesForUsers\n            .Select(x => x.Requirement.GetDefaultCollectionRequestOnConfirm(organization.Id))\n            .Where(w => w.ShouldCreateDefaultCollection)\n            .Select(s => s.OrganizationUserId)\n            .ToList();\n\n        if (eligibleOrganizationUserIds.Count == 0)\n        {\n            return;\n        }\n\n        await _collectionRepository.CreateDefaultCollectionsAsync(organization.Id, eligibleOrganizationUserIds, defaultUserCollectionName);\n    }\n\n    /// <summary>\n    /// Sends the organization confirmed email using the new mailer pattern.\n    /// </summary>\n    /// <param name=\"organization\">The organization the user was confirmed to.</param>\n    /// <param name=\"userEmail\">The email address of the confirmed user.</param>\n    /// <param name=\"accessSecretsManager\">Whether the user has access to Secrets Manager.</param>\n    internal async Task SendOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager)\n    {\n        await _sendOrganizationConfirmationCommand.SendConfirmationAsync(organization, userEmail, accessSecretsManager);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/CountNewSmSeatsRequiredQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class CountNewSmSeatsRequiredQuery : ICountNewSmSeatsRequiredQuery\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n\n    public CountNewSmSeatsRequiredQuery(IOrganizationUserRepository organizationUserRepository,\n        IOrganizationRepository organizationRepository)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _organizationRepository = organizationRepository;\n    }\n\n    public async Task<int> CountNewSmSeatsRequiredAsync(Guid organizationId, int usersToAdd)\n    {\n        if (usersToAdd == 0)\n        {\n            return 0;\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!organization.UseSecretsManager)\n        {\n            throw new BadRequestException(\"Organization does not use Secrets Manager\");\n        }\n\n        if (!organization.SmSeats.HasValue)\n        {\n            return 0;\n        }\n\n        var occupiedSmSeats =\n            await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);\n\n        var availableSmSeats = organization.SmSeats.Value - occupiedSmSeats;\n\n        if (availableSmSeats >= usersToAdd)\n        {\n            return 0;\n        }\n\n        return usersToAdd - availableSmSeats;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Utilities.v2.Results;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\n\npublic class DeleteClaimedOrganizationUserAccountCommand(\n    IUserService userService,\n    IEventService eventService,\n    IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery,\n    IOrganizationUserRepository organizationUserRepository,\n    IUserRepository userRepository,\n    IPushNotificationService pushService,\n    ILogger<DeleteClaimedOrganizationUserAccountCommand> logger,\n    IDeleteClaimedOrganizationUserAccountValidator deleteClaimedOrganizationUserAccountValidator)\n    : IDeleteClaimedOrganizationUserAccountCommand\n{\n    public async Task<BulkCommandResult> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId)\n    {\n        var result = await DeleteManyUsersAsync(organizationId, [organizationUserId], deletingUserId);\n        return result.Single();\n    }\n\n    public async Task<IEnumerable<BulkCommandResult>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid deletingUserId)\n    {\n        orgUserIds = orgUserIds.ToList();\n        var orgUsers = await organizationUserRepository.GetManyAsync(orgUserIds);\n        var users = await GetUsersAsync(orgUsers);\n        var claimedStatuses = await getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, orgUserIds);\n\n        var internalRequests = CreateInternalRequests(organizationId, deletingUserId, orgUserIds, orgUsers, users, claimedStatuses);\n        var validationResults = (await deleteClaimedOrganizationUserAccountValidator.ValidateAsync(internalRequests)).ToList();\n\n        var validRequests = validationResults.ValidRequests();\n        await CancelPremiumsAsync(validRequests);\n        await HandleUserDeletionsAsync(validRequests);\n        await LogDeletedOrganizationUsersAsync(validRequests);\n\n        return validationResults.Select(v => v.Match(\n            error => new BulkCommandResult(v.Request.OrganizationUserId, error),\n            _ => new BulkCommandResult(v.Request.OrganizationUserId, new None())\n        ));\n    }\n\n    private static IEnumerable<DeleteUserValidationRequest> CreateInternalRequests(\n        Guid organizationId,\n        Guid deletingUserId,\n        IEnumerable<Guid> orgUserIds,\n        ICollection<OrganizationUser> orgUsers,\n        IEnumerable<User> users,\n        IDictionary<Guid, bool> claimedStatuses)\n    {\n        foreach (var orgUserId in orgUserIds)\n        {\n            var orgUser = orgUsers.FirstOrDefault(orgUser => orgUser.Id == orgUserId);\n            var user = users.FirstOrDefault(user => user.Id == orgUser?.UserId);\n            claimedStatuses.TryGetValue(orgUserId, out var isClaimed);\n\n            yield return new DeleteUserValidationRequest\n            {\n                User = user,\n                OrganizationUserId = orgUserId,\n                OrganizationUser = orgUser,\n                IsClaimed = isClaimed,\n                OrganizationId = organizationId,\n                DeletingUserId = deletingUserId,\n            };\n        }\n    }\n\n    private async Task<IEnumerable<User>> GetUsersAsync(ICollection<OrganizationUser> orgUsers)\n    {\n        var userIds = orgUsers\n         .Where(orgUser => orgUser.UserId.HasValue)\n         .Select(orgUser => orgUser.UserId!.Value)\n         .ToList();\n\n        return await userRepository.GetManyAsync(userIds);\n    }\n\n    private async Task LogDeletedOrganizationUsersAsync(IEnumerable<DeleteUserValidationRequest> requests)\n    {\n        var eventDate = DateTime.UtcNow;\n\n        var events = requests\n            .Select(request => (request.OrganizationUser!, EventType.OrganizationUser_Deleted, (DateTime?)eventDate))\n            .ToList();\n\n        if (events.Count != 0)\n        {\n            await eventService.LogOrganizationUserEventsAsync(events);\n        }\n    }\n\n    private async Task HandleUserDeletionsAsync(IEnumerable<DeleteUserValidationRequest> requests)\n    {\n        var users = requests\n            .Select(request => request.User!)\n            .ToList();\n\n        if (users.Count == 0)\n        {\n            return;\n        }\n\n        await userRepository.DeleteManyAsync(users);\n\n        foreach (var user in users)\n        {\n            await pushService.PushLogOutAsync(user.Id);\n        }\n    }\n\n    private async Task CancelPremiumsAsync(IEnumerable<DeleteUserValidationRequest> requests)\n    {\n        var users = requests.Select(request => request.User!);\n\n        foreach (var user in users)\n        {\n            try\n            {\n                await userService.CancelPremiumAsync(user);\n            }\n            catch (GatewayException exception)\n            {\n                logger.LogWarning(exception, \"Failed to cancel premium subscription for {userId}.\", user.Id);\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\n\npublic class DeleteClaimedOrganizationUserAccountValidator(\n    ICurrentContext currentContext,\n    IOrganizationUserRepository organizationUserRepository,\n    IProviderUserRepository providerUserRepository) : IDeleteClaimedOrganizationUserAccountValidator\n{\n    public async Task<IEnumerable<ValidationResult<DeleteUserValidationRequest>>> ValidateAsync(IEnumerable<DeleteUserValidationRequest> requests)\n    {\n        var tasks = requests.Select(ValidateAsync);\n        var results = await Task.WhenAll(tasks);\n        return results;\n    }\n\n    private async Task<ValidationResult<DeleteUserValidationRequest>> ValidateAsync(DeleteUserValidationRequest request)\n    {\n        // Ensure user exists\n        if (request.User == null || request.OrganizationUser == null)\n        {\n            return Invalid(request, new UserNotFoundError());\n        }\n\n        // Cannot delete invited users\n        if (request.OrganizationUser.Status == OrganizationUserStatusType.Invited)\n        {\n            return Invalid(request, new InvalidUserStatusError());\n        }\n\n        // Cannot delete yourself\n        if (request.OrganizationUser.UserId == request.DeletingUserId)\n        {\n            return Invalid(request, new CannotDeleteYourselfError());\n        }\n\n        // Can only delete a claimed user\n        if (!request.IsClaimed)\n        {\n            return Invalid(request, new UserNotClaimedError());\n        }\n\n        // Cannot delete an owner unless you are an owner or provider\n        if (request.OrganizationUser.Type == OrganizationUserType.Owner &&\n            !await currentContext.OrganizationOwner(request.OrganizationId))\n        {\n            return Invalid(request, new CannotDeleteOwnersError());\n        }\n\n        // Cannot delete a user who is the sole owner of an organization\n        var onlyOwnerCount = await organizationUserRepository.GetCountByOnlyOwnerAsync(request.User.Id);\n        if (onlyOwnerCount > 0)\n        {\n            return Invalid(request, new SoleOwnerError());\n        }\n\n        // Cannot delete a user who is the sole member of a provider\n        var onlyOwnerProviderCount = await providerUserRepository.GetCountByOnlyOwnerAsync(request.User.Id);\n        if (onlyOwnerProviderCount > 0)\n        {\n            return Invalid(request, new SoleProviderError());\n        }\n\n        // Custom users cannot delete admins\n        if (request.OrganizationUser.Type == OrganizationUserType.Admin && await currentContext.OrganizationCustom(request.OrganizationId))\n        {\n            return Invalid(request, new CannotDeleteAdminsError());\n        }\n\n        return Valid(request);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteUserValidationRequest.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\n\npublic class DeleteUserValidationRequest\n{\n    public Guid OrganizationId { get; init; }\n    public Guid OrganizationUserId { get; init; }\n    public OrganizationUser? OrganizationUser { get; init; }\n    public User? User { get; init; }\n    public Guid DeletingUserId { get; init; }\n    public bool IsClaimed { get; init; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\n\npublic record UserNotFoundError() : NotFoundError(\"Invalid user.\");\npublic record UserNotClaimedError() : Error(\"Member is not claimed by the organization.\");\npublic record InvalidUserStatusError() : Error(\"You cannot delete a member with Invited status.\");\npublic record CannotDeleteYourselfError() : Error(\"You cannot delete yourself.\");\npublic record CannotDeleteOwnersError() : Error(\"Only owners can delete other owners.\");\npublic record SoleOwnerError() : Error(\"Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.\");\npublic record SoleProviderError() : Error(\"Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.\");\npublic record CannotDeleteAdminsError() : Error(\"Custom users can not delete admins.\");\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2.Results;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\n\npublic interface IDeleteClaimedOrganizationUserAccountCommand\n{\n    /// <summary>\n    /// Removes a user from an organization and deletes all of their associated user data.\n    /// </summary>\n    Task<BulkCommandResult> DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId);\n\n    /// <summary>\n    /// Removes multiple users from an organization and deletes all of their associated user data.\n    /// </summary>\n    /// <returns>\n    /// An error message for each user that could not be removed, otherwise null.\n    /// </returns>\n    Task<IEnumerable<BulkCommandResult>> DeleteManyUsersAsync(Guid organizationId, IEnumerable<Guid> orgUserIds, Guid deletingUserId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2.Validation;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\n\npublic interface IDeleteClaimedOrganizationUserAccountValidator\n{\n    Task<IEnumerable<ValidationResult<DeleteUserValidationRequest>>> ValidateAsync(IEnumerable<DeleteUserValidationRequest> requests);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class GetOrganizationUsersClaimedStatusQuery : IGetOrganizationUsersClaimedStatusQuery\n{\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public GetOrganizationUsersClaimedStatusQuery(\n        IApplicationCacheService applicationCacheService,\n        IOrganizationUserRepository organizationUserRepository)\n    {\n        _applicationCacheService = applicationCacheService;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    public async Task<IDictionary<Guid, bool>> GetUsersOrganizationClaimedStatusAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds)\n    {\n        if (organizationUserIds.Any())\n        {\n            // Users can only be claimed by an Organization that is enabled and can have organization domains\n            var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);\n\n            if (organizationAbility is { Enabled: true, UseOrganizationDomains: true })\n            {\n                // Get all organization users with claimed domains by the organization\n                var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId);\n\n                // Create a dictionary with the OrganizationUserId and a boolean indicating if the user is claimed by the organization\n                return organizationUserIds.ToDictionary(ouId => ouId, ouId => organizationUsersWithClaimedDomain.Any(ou => ou.Id == ouId));\n            }\n        }\n\n        return organizationUserIds.ToDictionary(ouId => ouId, _ => false);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class HasConfirmedOwnersExceptQuery : IHasConfirmedOwnersExceptQuery\n{\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public HasConfirmedOwnersExceptQuery(\n        IProviderUserRepository providerUserRepository,\n        IOrganizationUserRepository organizationUserRepository)\n    {\n        _providerUserRepository = providerUserRepository;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    public async Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId, bool includeProvider = true)\n    {\n        var confirmedOwners = await GetConfirmedOwnersAsync(organizationId);\n        var confirmedOwnersIds = confirmedOwners.Select(u => u.Id);\n        bool hasOtherOwner = confirmedOwnersIds.Except(organizationUsersId).Any();\n        if (!hasOtherOwner && includeProvider)\n        {\n            return (await _providerUserRepository.GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)).Any();\n        }\n        return hasOtherOwner;\n    }\n\n    private async Task<IEnumerable<OrganizationUser>> GetConfirmedOwnersAsync(Guid organizationId)\n    {\n        var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId,\n            OrganizationUserType.Owner);\n        return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IAcceptOrgUserCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IAcceptOrgUserCommand\n{\n    /// <summary>\n    /// Moves an OrganizationUser into the Accepted status and marks their email as verified.\n    /// This method is used where the user has clicked the invitation link sent by email.\n    /// </summary>\n    /// <param name=\"emailToken\">The token embedded in the email invitation link</param>\n    /// <returns>The accepted OrganizationUser.</returns>\n    Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken, IUserService userService);\n    Task<OrganizationUser> AcceptOrgUserByOrgSsoIdAsync(string orgIdentifier, User user, IUserService userService);\n    Task<OrganizationUser> AcceptOrgUserByOrgIdAsync(Guid organizationId, User user, IUserService userService);\n    Task<OrganizationUser> AcceptOrgUserAsync(OrganizationUser orgUser, User user, IUserService userService);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IAutomaticallyConfirmOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.Utilities.v2.Results;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\n/// <summary>\n/// Command to automatically confirm an organization user.\n/// </summary>\n/// <remarks>\n/// The auto-confirm feature enables eligible client apps to confirm OrganizationUsers\n/// automatically via push notifications, eliminating the need for manual administrator\n/// intervention. Client apps receive a push notification, perform the required key exchange,\n/// and submit an auto-confirm request to the server. This command processes those\n/// client-initiated requests and should only be used in that specific context.\n/// </remarks>\npublic interface IAutomaticallyConfirmOrganizationUserCommand\n{\n    /// <summary>\n    /// Automatically confirms the organization user based on the provided request data.\n    /// </summary>\n    /// <param name=\"request\">The request containing necessary information to confirm the organization user.</param>\n    /// <remarks>\n    /// This action has side effects. The side effects are\n    /// <ul>\n    ///   <li>Creating an event log entry.</li>\n    ///   <li>Syncing organization keys with the user.</li>\n    ///   <li>Deleting any registered user devices for the organization.</li>\n    ///   <li>Sending an email to the confirmed user.</li>\n    ///   <li>Creating the default collection if applicable.</li>\n    /// </ul>\n    ///\n    /// Each of these actions is performed independently of each other and not guaranteed to be performed in any order.\n    /// Errors will be reported back for the actions that failed in a consolidated error message.\n    /// </remarks>\n    /// <returns>\n    /// The result of the command. If there was an error, the result will contain a typed error describing the problem\n    /// that occurred.\n    /// </returns>\n    Task<CommandResult> AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\n/// <summary>\n/// Command to confirm organization users who have accepted their invitations.\n/// </summary>\npublic interface IConfirmOrganizationUserCommand\n{\n    /// <summary>\n    /// Confirms a single organization user who has accepted their invitation.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization.</param>\n    /// <param name=\"organizationUserId\">The ID of the organization user to confirm.</param>\n    /// <param name=\"key\">The encrypted organization key for the user.</param>\n    /// <param name=\"confirmingUserId\">The ID of the user performing the confirmation.</param>\n    /// <param name=\"defaultUserCollectionName\">Optional encrypted collection name for creating a default collection.</param>\n    /// <returns>The confirmed organization user.</returns>\n    /// <exception cref=\"BadRequestException\">Thrown when the user is not valid or cannot be confirmed.</exception>\n    Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId, string defaultUserCollectionName = null);\n\n    /// <summary>\n    /// Confirms multiple organization users who have accepted their invitations.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization.</param>\n    /// <param name=\"keys\">A dictionary mapping organization user IDs to their encrypted organization keys.</param>\n    /// <param name=\"confirmingUserId\">The ID of the user performing the confirmation.</param>\n    /// <param name=\"defaultUserCollectionName\">Optional encrypted collection name for creating default collections.</param>\n    /// <returns>A list of tuples containing the organization user and an error message (if any).</returns>\n    Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,\n        Guid confirmingUserId, string defaultUserCollectionName = null);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/ICountNewSmSeatsRequiredQuery.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface ICountNewSmSeatsRequiredQuery\n{\n    public Task<int> CountNewSmSeatsRequiredAsync(Guid organizationId, int usersToAdd);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IGetOrganizationUsersClaimedStatusQuery.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IGetOrganizationUsersClaimedStatusQuery\n{\n    /// <summary>\n    /// Checks whether each user in the provided list of organization user IDs is claimed by the specified organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization to check against.</param>\n    /// <param name=\"organizationUserIds\">A list of OrganizationUserIds to be checked.</param>\n    /// <remarks>\n    /// A claimed user is a user whose email domain matches one of the Organization's verified domains.\n    /// The organization must be enabled and be on an Enterprise plan.\n    /// </remarks>\n    /// <returns>\n    /// A dictionary containing the OrganizationUserId and a boolean indicating if the user is claimed by the organization.\n    /// </returns>\n    Task<IDictionary<Guid, bool>> GetUsersOrganizationClaimedStatusAsync(Guid organizationId,\n        IEnumerable<Guid> organizationUserIds);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IHasConfirmedOwnersExceptQuery.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IHasConfirmedOwnersExceptQuery\n{\n    /// <summary>\n    /// Checks if an organization has any confirmed owners except for the ones in the <paramref name=\"organizationUsersId\"/> list.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization ID.</param>\n    /// <param name=\"organizationUsersId\">The organization user IDs to exclude.</param>\n    /// <param name=\"includeProvider\">Whether to include the provider users in the count.</param>\n    Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId, bool includeProvider = true);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs",
    "content": "﻿using Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\n\nnamespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IOrganizationUserUserDetailsQuery\n{\n    Task<IEnumerable<OrganizationUserUserDetails>> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request);\n\n    Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> Get(OrganizationUserUserDetailsQueryRequest request);\n\n    Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IPushAutoConfirmNotificationCommand.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IPushAutoConfirmNotificationCommand\n{\n    Task PushAsync(Guid userId, Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRemoveOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IRemoveOrganizationUserCommand\n{\n    /// <summary>\n    /// Removes a user from an organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization.</param>\n    /// <param name=\"userId\">The ID of the user to remove.</param>\n    Task RemoveUserAsync(Guid organizationId, Guid userId);\n\n    /// <summary>\n    /// Removes a user from an organization with a specified deleting user.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization.</param>\n    /// <param name=\"organizationUserId\">The ID of the organization user to remove.</param>\n    /// <param name=\"deletingUserId\">The ID of the user performing the removal operation.</param>\n    Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);\n\n    /// <summary>\n    /// Removes a user from an organization using a system user.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization.</param>\n    /// <param name=\"organizationUserId\">The ID of the organization user to remove.</param>\n    /// <param name=\"eventSystemUser\">The system user performing the removal operation.</param>\n    Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser);\n\n    /// <summary>\n    /// Removes multiple users from an organization with a specified deleting user.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization.</param>\n    /// <param name=\"organizationUserIds\">The collection of organization user IDs to remove.</param>\n    /// <param name=\"deletingUserId\">The ID of the user performing the removal operation.</param>\n    /// <returns>\n    /// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty.\n    /// </returns>\n    Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(\n        Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);\n\n    /// <summary>\n    /// Removes multiple users from an organization using a system user.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization.</param>\n    /// <param name=\"organizationUserIds\">The collection of organization user IDs to remove.</param>\n    /// <param name=\"eventSystemUser\">The system user performing the removal operation.</param>\n    /// <returns>\n    /// A list of tuples containing the organization user id and the error message for each user that could not be removed, otherwise empty.\n    /// </returns>\n    Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(\n        Guid organizationId, IEnumerable<Guid> organizationUserIds, EventSystemUser eventSystemUser);\n\n    /// <summary>\n    /// Removes a user from an organization when they have left voluntarily. This should only be called by the same user who is being removed.\n    /// </summary>\n    /// <param name=\"organizationId\">Organization to leave.</param>\n    /// <param name=\"userId\">User to leave.</param>\n    Task UserLeaveAsync(Guid organizationId, Guid userId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Bit.Core.AdminConsole.Utilities.Commands;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IRevokeNonCompliantOrganizationUserCommand\n{\n    Task<CommandResult> RevokeNonCompliantOrganizationUsersAsync(RevokeOrganizationUsersRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IUpdateOrganizationUserCommand\n{\n    Task UpdateUserAsync(OrganizationUser organizationUser, OrganizationUserType existingUserType, Guid? savingUserId,\n        List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IUpdateOrganizationUserGroupsCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IUpdateOrganizationUserGroupsCommand\n{\n    Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/BulkResendOrganizationInvitesCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.Utilities.DebuggingInstruments;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\npublic class BulkResendOrganizationInvitesCommand : IBulkResendOrganizationInvitesCommand\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;\n    private readonly ILogger<BulkResendOrganizationInvitesCommand> _logger;\n\n    public BulkResendOrganizationInvitesCommand(\n        IOrganizationUserRepository organizationUserRepository,\n        IOrganizationRepository organizationRepository,\n        ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,\n        ILogger<BulkResendOrganizationInvitesCommand> logger)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _organizationRepository = organizationRepository;\n        _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;\n        _logger = logger;\n    }\n\n    public async Task<IEnumerable<Tuple<OrganizationUser, string>>> BulkResendInvitesAsync(\n        Guid organizationId,\n        Guid? invitingUserId,\n        IEnumerable<Guid> organizationUsersId)\n    {\n        var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);\n        _logger.LogUserInviteStateDiagnostics(orgUsers);\n\n        var org = await _organizationRepository.GetByIdAsync(organizationId);\n        if (org == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var validUsers = new List<OrganizationUser>();\n        var result = new List<Tuple<OrganizationUser, string>>();\n\n        foreach (var orgUser in orgUsers)\n        {\n            if (orgUser.Status != OrganizationUserStatusType.Invited || orgUser.OrganizationId != organizationId)\n            {\n                result.Add(Tuple.Create(orgUser, \"User invalid.\"));\n            }\n            else\n            {\n                validUsers.Add(orgUser);\n            }\n        }\n\n        if (validUsers.Any())\n        {\n            await _sendOrganizationInvitesCommand.SendInvitesAsync(\n                new SendInvitesRequest(validUsers, org, invitingUserId: invitingUserId));\n\n            result.AddRange(validUsers.Select(u => Tuple.Create(u, \"\")));\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.Errors;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;\n\npublic static class ErrorMapper\n{\n\n    /// <summary>\n    /// Maps the ErrorT to a Bit.Exception class.\n    /// </summary>\n    /// <param name=\"error\"></param>\n    /// <typeparam name=\"T\"></typeparam>\n    /// <returns></returns>\n    public static Exception MapToBitException<T>(Error<T> error) =>\n        error switch\n        {\n            UserAlreadyExistsError alreadyExistsError => new ConflictException(alreadyExistsError.Message),\n            _ => new BadRequestException(error.Message)\n        };\n\n    /// <summary>\n    /// This maps the ErrorT object to the Bit.Exception class.\n    ///\n    /// This should be replaced by an IActionResult mapper when possible.\n    /// </summary>\n    /// <param name=\"errors\"></param>\n    /// <typeparam name=\"T\"></typeparam>\n    /// <returns></returns>\n    public static Exception MapToBitException<T>(ICollection<Error<T>> errors) =>\n        errors switch\n        {\n            not null when errors.Count == 1 => MapToBitException(errors.First()),\n            not null when errors.Count > 1 => new BadRequestException(string.Join(' ', errors.Select(e => e.Message))),\n            _ => new BadRequestException()\n        };\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.Utilities.Errors;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;\n\npublic record FailedToInviteUsersError(InviteOrganizationUsersResponse Response) : Error<InviteOrganizationUsersResponse>(Code, Response)\n{\n    public const string Code = \"Failed to invite users\";\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.Utilities.Errors;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;\n\npublic record NoUsersToInviteError(InviteOrganizationUsersResponse Response) : Error<InviteOrganizationUsersResponse>(Code, Response)\n{\n    public const string Code = \"No users to invite\";\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.Utilities.Errors;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;\n\npublic record UserAlreadyExistsError(ScimInviteOrganizationUsersResponse Response) : Error<ScimInviteOrganizationUsersResponse>(Code, Response)\n{\n    public const string Code = \"User already exists\";\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IBulkResendOrganizationInvitesCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\npublic interface IBulkResendOrganizationInvitesCommand\n{\n    /// <summary>\n    /// Resend invites to multiple organization users in bulk.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization.</param>\n    /// <param name=\"invitingUserId\">The ID of the user who is resending the invites.</param>\n    /// <param name=\"organizationUsersId\">The IDs of the organization users to resend invites to.</param>\n    /// <returns>A tuple containing the OrganizationUser and an error message (empty string if successful)</returns>\n    Task<IEnumerable<Tuple<OrganizationUser, string>>> BulkResendInvitesAsync(\n        Guid organizationId,\n        Guid? invitingUserId,\n        IEnumerable<Guid> organizationUsersId);\n}\n\n\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.Utilities.Commands;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\n/// <summary>\n/// Defines the contract for inviting organization users via SCIM (System for Cross-domain Identity Management).\n/// Provides functionality for handling single email invitation requests within an organization context.\n/// </summary>\npublic interface IInviteOrganizationUsersCommand\n{\n    /// <summary>\n    /// Sends an invitation to add an organization user via SCIM (System for Cross-domain Identity Management) system.\n    /// This can be a Success or a Failure. Failure will contain the Error along with a representation of the errored value.\n    /// Success will be the successful return object.\n    /// </summary>\n    /// <param name=\"request\">\n    /// Contains the details for inviting a single organization user via email.\n    /// </param>\n    /// <returns>Response from InviteScimOrganiation<see cref=\"ScimInviteOrganizationUsersResponse\"/></returns>\n    Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(InviteOrganizationUsersRequest request);\n    /// <summary>\n    /// Sends invitations to add imported organization users via the public API.\n    /// This can be a Success or a Failure. Failure will contain the Error along with a representation of the errored value.\n    /// Success will be the successful return object.\n    /// </summary>\n    /// <param name=\"request\">\n    /// Contains the details for inviting the imported organization users.\n    /// </param>\n    /// <returns>Response from InviteOrganiationUsersAsync<see cref=\"InviteOrganizationUsersResponse\"/></returns>\n    Task<CommandResult<InviteOrganizationUsersResponse>> InviteImportedOrganizationUsersAsync(InviteOrganizationUsersRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IResendOrganizationInviteCommand.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\npublic interface IResendOrganizationInviteCommand\n{\n    /// <summary>\n    /// Resend an invite to an organization user.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization.</param>\n    /// <param name=\"invitingUserId\">The ID of the user who is inviting the organization user.</param>\n    /// <param name=\"organizationUserId\">The ID of the organization user to resend the invite to.</param>\n    /// <param name=\"initOrganization\">Whether to initialize the organization. \n    /// This is should only be true when inviting the owner of a new organization.</param>\n    Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ISendOrganizationInvitesCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\n/// <summary>\n/// This is for sending the invite to an organization user.\n/// </summary>\npublic interface ISendOrganizationInvitesCommand\n{\n    /// <summary>\n    /// This sends emails out to organization users for a given organization.\n    /// </summary>\n    /// <param name=\"request\"><see cref=\"SendInvitesRequest\"/></param>\n    /// <returns></returns>\n    Task SendInvitesAsync(SendInvitesRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Interfaces;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities.Commands;\nusing Bit.Core.AdminConsole.Utilities.Errors;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\npublic class InviteOrganizationUsersCommand(IEventService eventService,\n    IOrganizationUserRepository organizationUserRepository,\n    IInviteUsersValidator inviteUsersValidator,\n    IOrganizationRepository organizationRepository,\n    IApplicationCacheService applicationCacheService,\n    IMailService mailService,\n    ILogger<InviteOrganizationUsersCommand> logger,\n    IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,\n    ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,\n    IProviderOrganizationRepository providerOrganizationRepository,\n    IProviderUserRepository providerUserRepository\n    ) : IInviteOrganizationUsersCommand\n{\n\n    public const string IssueNotifyingOwnersOfSeatLimitReached = \"Error encountered notifying organization owners of seat limit reached.\";\n\n    public async Task<CommandResult<ScimInviteOrganizationUsersResponse>> InviteScimOrganizationUserAsync(InviteOrganizationUsersRequest request)\n    {\n        var result = await InviteOrganizationUsersAsync(request);\n\n        switch (result)\n        {\n            case Failure<InviteOrganizationUsersResponse> failure:\n                return new Failure<ScimInviteOrganizationUsersResponse>(\n                    new Error<ScimInviteOrganizationUsersResponse>(failure.Error.Message,\n                        new ScimInviteOrganizationUsersResponse\n                        {\n                            InvitedUser = failure.Error.ErroredValue.InvitedUsers.FirstOrDefault()\n                        }));\n\n            case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():\n                var user = success.Value.InvitedUsers.First();\n\n                await eventService.LogOrganizationUserEventAsync<IOrganizationUser>(\n                    organizationUser: user,\n                    type: EventType.OrganizationUser_Invited,\n                    systemUser: EventSystemUser.SCIM,\n                    date: request.PerformedAt.UtcDateTime);\n\n                return new Success<ScimInviteOrganizationUsersResponse>(new ScimInviteOrganizationUsersResponse\n                {\n                    InvitedUser = user\n                });\n\n            default:\n                return new Failure<ScimInviteOrganizationUsersResponse>(\n                    new InvalidResultTypeError<ScimInviteOrganizationUsersResponse>(\n                        new ScimInviteOrganizationUsersResponse()));\n        }\n    }\n\n    public async Task<CommandResult<InviteOrganizationUsersResponse>> InviteImportedOrganizationUsersAsync(InviteOrganizationUsersRequest request)\n    {\n        var result = await InviteOrganizationUsersAsync(request);\n\n        switch (result)\n        {\n            case Failure<InviteOrganizationUsersResponse> failure:\n                return new Failure<InviteOrganizationUsersResponse>(\n                        new Error<InviteOrganizationUsersResponse>(\n                            failure.Error.Message,\n                            new InviteOrganizationUsersResponse(failure.Error.ErroredValue.InvitedUsers, request.InviteOrganization.OrganizationId)\n                            )\n                        );\n\n            case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():\n\n                List<(OrganizationUser, EventType, EventSystemUser, DateTime?)> events = new List<(OrganizationUser, EventType, EventSystemUser, DateTime?)>();\n                foreach (var user in success.Value.InvitedUsers)\n                {\n                    events.Add((user, EventType.OrganizationUser_Invited, EventSystemUser.PublicApi, request.PerformedAt.UtcDateTime));\n                }\n\n                await eventService.LogOrganizationUserEventsAsync(events);\n\n                return new Success<InviteOrganizationUsersResponse>(new InviteOrganizationUsersResponse(success.Value.InvitedUsers, request.InviteOrganization.OrganizationId)\n                );\n\n            default:\n                return new Failure<InviteOrganizationUsersResponse>(\n                    new InvalidResultTypeError<InviteOrganizationUsersResponse>(\n                        new InviteOrganizationUsersResponse(request.InviteOrganization.OrganizationId)));\n        }\n    }\n\n    private async Task<CommandResult<InviteOrganizationUsersResponse>> InviteOrganizationUsersAsync(InviteOrganizationUsersRequest request)\n    {\n        var invitesToSend = (await FilterExistingUsersAsync(request)).ToArray();\n\n        if (invitesToSend.Length == 0)\n        {\n            return new Failure<InviteOrganizationUsersResponse>(new NoUsersToInviteError(\n                new InviteOrganizationUsersResponse(request.InviteOrganization.OrganizationId)));\n        }\n\n        var validationResult = await inviteUsersValidator.ValidateAsync(new InviteOrganizationUsersValidationRequest\n        {\n            Invites = invitesToSend.ToArray(),\n            InviteOrganization = request.InviteOrganization,\n            PerformedBy = request.PerformedBy,\n            PerformedAt = request.PerformedAt,\n            OccupiedPmSeats = (await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)).Total,\n            OccupiedSmSeats = await organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(request.InviteOrganization.OrganizationId)\n        });\n\n        if (validationResult is Invalid<InviteOrganizationUsersValidationRequest> invalid)\n        {\n            return invalid.MapToFailure(r => new InviteOrganizationUsersResponse(r));\n        }\n\n        var validatedRequest = validationResult as Valid<InviteOrganizationUsersValidationRequest>;\n\n        var organizationUserToInviteEntities = invitesToSend\n            .Select(x => x.MapToDataModel(request.PerformedAt, validatedRequest!.Value.InviteOrganization))\n            .ToArray();\n\n        var organization = await organizationRepository.GetByIdAsync(validatedRequest!.Value.InviteOrganization.OrganizationId);\n\n        try\n        {\n            await organizationUserRepository.CreateManyAsync(organizationUserToInviteEntities);\n\n            await AdjustPasswordManagerSeatsAsync(validatedRequest, organization);\n\n            await AdjustSecretsManagerSeatsAsync(validatedRequest);\n\n            await SendAdditionalEmailsAsync(validatedRequest, organization);\n\n            await SendInvitesAsync(organizationUserToInviteEntities, organization, request.PerformedBy);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, FailedToInviteUsersError.Code);\n\n            await organizationUserRepository.DeleteManyAsync(organizationUserToInviteEntities.Select(x => x.OrganizationUser.Id));\n\n            // Do this first so that SmSeats never exceed PM seats (due to current billing requirements)\n            await RevertSecretsManagerChangesAsync(validatedRequest, organization, validatedRequest.Value.InviteOrganization.SmSeats);\n\n            await RevertPasswordManagerChangesAsync(validatedRequest, organization);\n\n            return new Failure<InviteOrganizationUsersResponse>(\n                new FailedToInviteUsersError(\n                    new InviteOrganizationUsersResponse(validatedRequest.Value)));\n        }\n\n        return new Success<InviteOrganizationUsersResponse>(\n            new InviteOrganizationUsersResponse(\n                invitedOrganizationUsers: organizationUserToInviteEntities.Select(x => x.OrganizationUser).ToArray(),\n                organizationId: organization!.Id));\n    }\n\n    private async Task<IEnumerable<OrganizationUserInviteCommandModel>> FilterExistingUsersAsync(InviteOrganizationUsersRequest request)\n    {\n        var existingEmails = new HashSet<string>(await organizationUserRepository.SelectKnownEmailsAsync(\n                request.InviteOrganization.OrganizationId, request.Invites.Select(i => i.Email), false),\n            StringComparer.OrdinalIgnoreCase);\n\n        return request.Invites\n            .Where(invite => !existingEmails.Contains(invite.Email))\n            .ToArray();\n    }\n\n    private async Task RevertPasswordManagerChangesAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)\n    {\n        if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { Seats: > 0, SeatsRequiredToAdd: > 0 })\n        {\n            organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats;\n\n            await organizationRepository.ReplaceAsync(organization);\n            await applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n        }\n    }\n\n    private async Task RevertSecretsManagerChangesAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization, int? initialSmSeats)\n    {\n        if (validatedResult.Value.SecretsManagerSubscriptionUpdate?.SmSeatsChanged is true)\n        {\n            var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(\n                organization: organization,\n                plan: validatedResult.Value.InviteOrganization.Plan,\n                autoscaling: false)\n            {\n                SmSeats = initialSmSeats\n            };\n\n            await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdateRevert);\n        }\n    }\n\n    private async Task SendInvitesAsync(IEnumerable<CreateOrganizationUser> users, Organization organization, Guid invitingUserId) =>\n        await sendOrganizationInvitesCommand.SendInvitesAsync(\n            new SendInvitesRequest(\n                users.Select(x => x.OrganizationUser),\n                organization,\n                initOrganization: false,\n                invitingUserId: invitingUserId));\n\n    private async Task SendAdditionalEmailsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)\n    {\n        await NotifyOwnersIfAutoscaleOccursAsync(validatedResult, organization);\n        await NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(validatedResult, organization);\n    }\n\n    private async Task NotifyOwnersIfAutoscaleOccursAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)\n    {\n        if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0\n            && !organization.OwnersNotifiedOfAutoscaling.HasValue)\n        {\n            await mailService.SendOrganizationAutoscaledEmailAsync(\n                organization,\n                validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats!.Value,\n                await GetOwnerEmailAddressesAsync(validatedResult.Value.InviteOrganization));\n\n            organization.OwnersNotifiedOfAutoscaling = validatedResult.Value.PerformedAt.UtcDateTime;\n            await organizationRepository.UpsertAsync(organization);\n        }\n    }\n\n    private async Task NotifyOwnersIfPasswordManagerMaxSeatLimitReachedAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)\n    {\n        if (!validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxSeatsReached)\n        {\n            return;\n        }\n\n        try\n        {\n            var ownerEmails = await GetOwnerEmailAddressesAsync(validatedResult.Value.InviteOrganization);\n\n            await mailService.SendOrganizationMaxSeatLimitReachedEmailAsync(organization,\n                validatedResult.Value.PasswordManagerSubscriptionUpdate.MaxAutoScaleSeats!.Value, ownerEmails);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, IssueNotifyingOwnersOfSeatLimitReached);\n        }\n    }\n\n    private async Task<IEnumerable<string>> GetOwnerEmailAddressesAsync(InviteOrganization organization)\n    {\n        var providerOrganization = await providerOrganizationRepository\n            .GetByOrganizationId(organization.OrganizationId);\n\n        if (providerOrganization == null)\n        {\n            return (await organizationUserRepository\n                    .GetManyByMinimumRoleAsync(organization.OrganizationId, OrganizationUserType.Owner))\n                .Select(x => x.Email)\n                .Distinct();\n        }\n\n        return (await providerUserRepository\n                .GetManyDetailsByProviderAsync(providerOrganization.ProviderId, ProviderUserStatusType.Confirmed))\n            .Select(u => u.Email).Distinct();\n    }\n\n    private async Task AdjustSecretsManagerSeatsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult)\n    {\n        if (validatedResult.Value.SecretsManagerSubscriptionUpdate?.SmSeatsChanged is true)\n        {\n            await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(validatedResult.Value.SecretsManagerSubscriptionUpdate);\n        }\n\n    }\n\n    private async Task AdjustPasswordManagerSeatsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)\n    {\n        if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { SeatsRequiredToAdd: > 0, UpdatedSeatTotal: > 0 })\n        {\n            await organizationRepository.IncrementSeatCountAsync(\n                organization.Id,\n                validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd,\n                validatedResult.Value.PerformedAt.UtcDateTime);\n\n            organization.Seats = validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal;\n            organization.SyncSeats = true;\n\n            await applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUser.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\n\n/// <summary>\n/// Object for associating the <see cref=\"OrganizationUser\"/> with their assigned collections\n/// <see cref=\"CollectionAccessSelection\"/> and Group Ids.\n/// </summary>\npublic class CreateOrganizationUser\n{\n    public OrganizationUser OrganizationUser { get; set; }\n    public CollectionAccessSelection[] Collections { get; set; } = [];\n    public Guid[] Groups { get; set; } = [];\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUserExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\n\npublic static class CreateOrganizationUserExtensions\n{\n    public static CreateOrganizationUser MapToDataModel(this OrganizationUserInviteCommandModel organizationUserInvite,\n        DateTimeOffset performedAt,\n        InviteOrganization organization) =>\n        new()\n        {\n            OrganizationUser = new OrganizationUser\n            {\n                Id = CoreHelpers.GenerateComb(),\n                OrganizationId = organization.OrganizationId,\n                Email = organizationUserInvite.Email.ToLowerInvariant(),\n                Type = organizationUserInvite.Type,\n                Status = OrganizationUserStatusType.Invited,\n                AccessSecretsManager = organizationUserInvite.AccessSecretsManager,\n                ExternalId = string.IsNullOrWhiteSpace(organizationUserInvite.ExternalId) ? null : organizationUserInvite.ExternalId,\n                CreationDate = performedAt.UtcDateTime,\n                RevisionDate = performedAt.UtcDateTime\n            },\n            Collections = organizationUserInvite.AssignedCollections,\n            Groups = organizationUserInvite.Groups\n        };\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUserErrorMessages.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\n\npublic static class InviteOrganizationUserErrorMessages\n{\n    public const string InvalidEmailErrorMessage = \"The email address is not valid.\";\n    public const string InvalidCollectionConfigurationErrorMessage = \"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.\";\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersRequest.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Business;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\n\npublic class InviteOrganizationUsersRequest\n{\n    public OrganizationUserInviteCommandModel[] Invites { get; } = [];\n    public InviteOrganization InviteOrganization { get; }\n    public Guid PerformedBy { get; }\n    public DateTimeOffset PerformedAt { get; }\n\n    public InviteOrganizationUsersRequest(OrganizationUserInviteCommandModel[] invites,\n        InviteOrganization inviteOrganization,\n        Guid performedBy,\n        DateTimeOffset performedAt)\n    {\n        Invites = invites;\n        InviteOrganization = inviteOrganization;\n        PerformedBy = performedBy;\n        PerformedAt = performedAt;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\n\npublic class InviteOrganizationUsersResponse(Guid organizationId)\n{\n    public IEnumerable<OrganizationUser> InvitedUsers { get; } = [];\n    public Guid OrganizationId { get; } = organizationId;\n\n    public InviteOrganizationUsersResponse(InviteOrganizationUsersValidationRequest usersValidationRequest)\n        : this(usersValidationRequest.InviteOrganization.OrganizationId)\n    {\n        InvitedUsers = usersValidationRequest.Invites.Select(x => new OrganizationUser { Email = x.Email });\n    }\n\n    public InviteOrganizationUsersResponse(IEnumerable<OrganizationUser> invitedOrganizationUsers, Guid organizationId)\n        : this(organizationId)\n    {\n        InvitedUsers = invitedOrganizationUsers;\n    }\n}\n\npublic class ScimInviteOrganizationUsersResponse\n{\n    public OrganizationUser InvitedUser { get; init; }\n\n    public ScimInviteOrganizationUsersResponse()\n    {\n\n    }\n\n    public ScimInviteOrganizationUsersResponse(InviteOrganizationUsersRequest request)\n    {\n        var userToInvite = request.Invites.First();\n\n        InvitedUser = new OrganizationUser\n        {\n            Email = userToInvite.Email,\n            ExternalId = userToInvite.ExternalId\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersValidationRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\n\npublic class InviteOrganizationUsersValidationRequest\n{\n    public InviteOrganizationUsersValidationRequest()\n    {\n    }\n\n    public InviteOrganizationUsersValidationRequest(InviteOrganizationUsersValidationRequest request)\n    {\n        Invites = request.Invites;\n        InviteOrganization = request.InviteOrganization;\n        PerformedBy = request.PerformedBy;\n        PerformedAt = request.PerformedAt;\n        OccupiedPmSeats = request.OccupiedPmSeats;\n        OccupiedSmSeats = request.OccupiedSmSeats;\n    }\n\n    public InviteOrganizationUsersValidationRequest(InviteOrganizationUsersValidationRequest request,\n        PasswordManagerSubscriptionUpdate subscriptionUpdate,\n        SecretsManagerSubscriptionUpdate smSubscriptionUpdate)\n        : this(request)\n    {\n        PasswordManagerSubscriptionUpdate = subscriptionUpdate;\n        SecretsManagerSubscriptionUpdate = smSubscriptionUpdate;\n    }\n\n    public OrganizationUserInviteCommandModel[] Invites { get; init; } = [];\n    public InviteOrganization InviteOrganization { get; init; }\n    public Guid PerformedBy { get; init; }\n    public DateTimeOffset PerformedAt { get; init; }\n    public int OccupiedPmSeats { get; init; }\n    public int OccupiedSmSeats { get; init; }\n    public PasswordManagerSubscriptionUpdate PasswordManagerSubscriptionUpdate { get; set; }\n    public SecretsManagerSubscriptionUpdate SecretsManagerSubscriptionUpdate { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInviteCommandModel.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\nusing static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.InviteOrganizationUserErrorMessages;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\n\npublic class OrganizationUserInviteCommandModel\n{\n    public string Email { get; private init; }\n    public CollectionAccessSelection[] AssignedCollections { get; private init; }\n    public OrganizationUserType Type { get; private init; }\n    public Permissions Permissions { get; private init; }\n    public string ExternalId { get; private init; }\n    public bool AccessSecretsManager { get; private init; }\n    public Guid[] Groups { get; private init; }\n\n    public OrganizationUserInviteCommandModel(string email, string externalId) :\n        this(\n            email: email,\n            assignedCollections: [],\n            groups: [],\n            type: OrganizationUserType.User,\n            permissions: new Permissions(),\n            externalId: externalId,\n            false)\n    {\n    }\n\n    public OrganizationUserInviteCommandModel(OrganizationUserInviteCommandModel invite, bool accessSecretsManager) :\n        this(invite.Email,\n            invite.AssignedCollections,\n            invite.Groups,\n            invite.Type,\n            invite.Permissions,\n            invite.ExternalId,\n            accessSecretsManager)\n    {\n\n    }\n\n    public OrganizationUserInviteCommandModel(string email,\n        IEnumerable<CollectionAccessSelection> assignedCollections,\n        IEnumerable<Guid> groups,\n        OrganizationUserType type,\n        Permissions permissions,\n        string externalId,\n        bool accessSecretsManager)\n    {\n        ValidateEmailAddress(email);\n\n        var collections = assignedCollections?.ToArray() ?? [];\n\n        if (collections.Any(x => x.IsValidCollectionAccessConfiguration()))\n        {\n            throw new BadRequestException(InvalidCollectionConfigurationErrorMessage);\n        }\n\n        Email = email;\n        AssignedCollections = collections;\n        Groups = groups.ToArray();\n        Type = type;\n        Permissions = permissions ?? new Permissions();\n        ExternalId = externalId;\n        AccessSecretsManager = accessSecretsManager;\n    }\n\n    private static void ValidateEmailAddress(string email)\n    {\n        if (!email.IsValidEmail())\n        {\n            throw new BadRequestException($\"{email} {InvalidEmailErrorMessage}\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/SendInvitesRequest.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\n\n/// <summary>\n/// Represents a request to send invitations to a group of organization users.\n/// </summary>\npublic class SendInvitesRequest\n{\n    public SendInvitesRequest(IEnumerable<OrganizationUser> users, Organization organization, bool initOrganization = false, Guid? invitingUserId = null) =>\n        (Users, Organization, InitOrganization, InvitingUserId) = (users.ToArray(), organization, initOrganization, invitingUserId);\n\n    /// <summary>\n    /// Organization Users to send emails to.\n    /// </summary>\n    public OrganizationUser[] Users { get; set; } = [];\n\n    /// <summary>\n    /// The organization to invite the users to.\n    /// </summary>\n    public Organization Organization { get; init; }\n\n    /// <summary>\n    /// This is for when the organization is being created and this is the owners initial invite\n    /// </summary>\n    public bool InitOrganization { get; init; }\n\n    /// <summary>\n    /// The user ID of the person sending the invitation (null for SCIM/automated invitations)\n    /// </summary>\n    public Guid? InvitingUserId { get; init; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.Utilities.DebuggingInstruments;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\npublic class ResendOrganizationInviteCommand : IResendOrganizationInviteCommand\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;\n    private readonly ILogger<ResendOrganizationInviteCommand> _logger;\n\n    public ResendOrganizationInviteCommand(\n        IOrganizationUserRepository organizationUserRepository,\n        IOrganizationRepository organizationRepository,\n        ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,\n        ILogger<ResendOrganizationInviteCommand> logger)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _organizationRepository = organizationRepository;\n        _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;\n        _logger = logger;\n    }\n\n    public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId,\n        bool initOrganization = false)\n    {\n        var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);\n        if (organizationUser == null || organizationUser.OrganizationId != organizationId ||\n            organizationUser.Status != OrganizationUserStatusType.Invited)\n        {\n            throw new BadRequestException(\"User invalid.\");\n        }\n\n        _logger.LogUserInviteStateDiagnostics(organizationUser);\n\n        var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);\n        if (organization == null)\n        {\n            throw new BadRequestException(\"Organization invalid.\");\n        }\n        await SendInviteAsync(organizationUser, organization, initOrganization, invitingUserId);\n    }\n\n    private async Task SendInviteAsync(OrganizationUser organizationUser, Organization organization,\n        bool initOrganization, Guid? invitingUserId) =>\n        await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(\n            users: [organizationUser],\n            organization: organization,\n            initOrganization: initOrganization,\n            invitingUserId: invitingUserId));\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Auth.Models.Business;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\npublic class SendOrganizationInvitesCommand(\n    IUserRepository userRepository,\n    ISsoConfigRepository ssoConfigurationRepository,\n    IPolicyQuery policyQuery,\n    IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,\n    IDataProtectorTokenFactory<OrgUserInviteTokenable> dataProtectorTokenFactory,\n    IMailService mailService) : ISendOrganizationInvitesCommand\n{\n    public async Task SendInvitesAsync(SendInvitesRequest request)\n    {\n        var inviterEmail = await GetInviterEmailAsync(request.InvitingUserId);\n        var orgInvitesInfo = await BuildOrganizationInvitesInfoAsync(\n            request.Users, request.Organization, request.InitOrganization, inviterEmail);\n        await mailService.SendUpdatedOrganizationInviteEmailsAsync(orgInvitesInfo);\n    }\n\n    private async Task<OrganizationInvitesInfo> BuildOrganizationInvitesInfoAsync(IEnumerable<OrganizationUser> orgUsers,\n        Organization organization, bool initOrganization = false, string inviterEmail = null)\n    {\n        // Materialize the sequence into a list to avoid multiple enumeration warnings\n        var orgUsersList = orgUsers.ToList();\n\n        // Email links must include information about the org and user for us to make routing decisions client side\n        // Given an org user, determine if existing BW user exists\n        var orgUserEmails = orgUsersList.Select(ou => ou.Email).ToList();\n        var existingUsers = await userRepository.GetManyByEmailsAsync(orgUserEmails);\n\n        // hash existing users emails list for O(1) lookups\n        var existingUserEmailsHashSet = new HashSet<string>(existingUsers.Select(u => u.Email));\n\n        // Create a dictionary of org user guids and bools for whether or not they have an existing BW user\n        var orgUserHasExistingUserDict = orgUsersList.ToDictionary(\n            ou => ou.Id,\n            ou => existingUserEmailsHashSet.Contains(ou.Email)\n        );\n\n        // Determine if org has SSO enabled and if user is required to login with SSO\n        // Note: we only want to call the DB after checking if the org can use SSO per plan and if they have any policies enabled.\n        var orgSsoEnabled = organization.UseSso && (await ssoConfigurationRepository.GetByOrganizationIdAsync(organization.Id))?.Enabled == true;\n        // Even though the require SSO policy can be turned on regardless of SSO being enabled, for this logic, we only\n        // need to check the policy if the org has SSO enabled.\n        var orgSsoLoginRequiredPolicyEnabled = orgSsoEnabled &&\n                                               organization.UsePolicies &&\n                                               (await policyQuery.RunAsync(organization.Id, PolicyType.RequireSso)).Enabled;\n\n        // Generate the list of org users and expiring tokens\n        // create helper function to create expiring tokens\n        (OrganizationUser, ExpiringToken) MakeOrgUserExpiringTokenPair(OrganizationUser orgUser)\n        {\n            var orgUserInviteTokenable = orgUserInviteTokenableFactory.CreateToken(orgUser);\n            var protectedToken = dataProtectorTokenFactory.Protect(orgUserInviteTokenable);\n            return (orgUser, new ExpiringToken(protectedToken, orgUserInviteTokenable.ExpirationDate));\n        }\n\n        var orgUsersWithExpTokens = orgUsers.Select(MakeOrgUserExpiringTokenPair);\n\n        return new OrganizationInvitesInfo(\n            organization,\n            orgSsoEnabled,\n            orgSsoLoginRequiredPolicyEnabled,\n            orgUsersWithExpTokens,\n            orgUserHasExistingUserDict,\n            initOrganization,\n            inviterEmail\n        );\n    }\n\n    private async Task<string> GetInviterEmailAsync(Guid? invitingUserId)\n    {\n        if (!invitingUserId.HasValue || invitingUserId.Value == Guid.Empty)\n        {\n            return null;\n        }\n\n        var invitingUser = await userRepository.GetByIdAsync(invitingUserId.Value);\n        return invitingUser?.Email;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/CollectionAccessSelectionExtensions.cs",
    "content": "﻿using Bit.Core.Models.Data;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\n\npublic static class CollectionAccessSelectionExtensions\n{\n    /// <summary>\n    /// This validates the permissions on the given assigned collection\n    /// </summary>\n    public static bool IsValidCollectionAccessConfiguration(this CollectionAccessSelection collectionAccessSelection) =>\n        collectionAccessSelection.Manage && (collectionAccessSelection.ReadOnly || collectionAccessSelection.HidePasswords);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.Errors;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;\n\npublic record CannotAutoScaleOnSelfHostError(EnvironmentRequest Invalid) : Error<EnvironmentRequest>(Code, Invalid)\n{\n    public const string Code = \"Cannot auto scale self-host.\";\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/EnvironmentRequest.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;\n\npublic class EnvironmentRequest\n{\n    public bool IsSelfHosted { get; init; }\n    public PasswordManagerSubscriptionUpdate PasswordManagerSubscriptionUpdate { get; init; }\n\n    public EnvironmentRequest(IGlobalSettings globalSettings, PasswordManagerSubscriptionUpdate passwordManagerSubscriptionUpdate)\n    {\n        IsSelfHosted = globalSettings.SelfHosted;\n        PasswordManagerSubscriptionUpdate = passwordManagerSubscriptionUpdate;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.Validation;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;\n\npublic interface IInviteUsersEnvironmentValidator : IValidator<EnvironmentRequest>;\n\npublic class InviteUsersEnvironmentValidator : IInviteUsersEnvironmentValidator\n{\n    public Task<ValidationResult<EnvironmentRequest>> ValidateAsync(EnvironmentRequest value) =>\n        Task.FromResult<ValidationResult<EnvironmentRequest>>(\n            value.IsSelfHosted && value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0 ?\n                new Invalid<EnvironmentRequest>(new CannotAutoScaleOnSelfHostError(value)) :\n                new Valid<EnvironmentRequest>(value));\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\nusing Bit.Core.AdminConsole.Utilities.Errors;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Models.Business;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\n\npublic interface IInviteUsersValidator : IValidator<InviteOrganizationUsersValidationRequest>;\n\npublic class InviteOrganizationUsersValidator(\n    IOrganizationRepository organizationRepository,\n    IInviteUsersPasswordManagerValidator inviteUsersPasswordManagerValidator,\n    IUpdateSecretsManagerSubscriptionCommand secretsManagerSubscriptionCommand,\n    IStripePaymentService paymentService) : IInviteUsersValidator\n{\n    public async Task<ValidationResult<InviteOrganizationUsersValidationRequest>> ValidateAsync(\n        InviteOrganizationUsersValidationRequest request)\n    {\n        var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(request);\n\n        var passwordManagerValidationResult =\n            await inviteUsersPasswordManagerValidator.ValidateAsync(subscriptionUpdate);\n\n        if (passwordManagerValidationResult is Invalid<PasswordManagerSubscriptionUpdate> invalidSubscriptionUpdate)\n        {\n            return invalidSubscriptionUpdate.Map(request);\n        }\n\n        // If the organization has the Secrets Manager Standalone Discount, all users are added to secrets manager.\n        // This is an expensive call, so we're doing it now to delay the check as long as possible.\n        if (await paymentService.HasSecretsManagerStandalone(request.InviteOrganization))\n        {\n            request = new InviteOrganizationUsersValidationRequest(request)\n            {\n                Invites = request.Invites\n                    .Select(x => new OrganizationUserInviteCommandModel(x, accessSecretsManager: true))\n                    .ToArray()\n            };\n        }\n\n        if (request.InviteOrganization.UseSecretsManager && request.Invites.Any(x => x.AccessSecretsManager))\n        {\n            return await ValidateSecretsManagerSubscriptionUpdateAsync(request, subscriptionUpdate);\n        }\n\n        return new Valid<InviteOrganizationUsersValidationRequest>(new InviteOrganizationUsersValidationRequest(\n            request,\n            subscriptionUpdate,\n            null));\n    }\n\n    private async Task<ValidationResult<InviteOrganizationUsersValidationRequest>> ValidateSecretsManagerSubscriptionUpdateAsync(\n            InviteOrganizationUsersValidationRequest request,\n            PasswordManagerSubscriptionUpdate subscriptionUpdate)\n    {\n        try\n        {\n            var organization = await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId);\n\n            organization!.Seats = subscriptionUpdate.UpdatedSeatTotal;\n\n            var smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(\n                organization: organization,\n                plan: request.InviteOrganization.Plan,\n                autoscaling: true);\n\n            var seatsToAdd = GetSecretManagerSeatAdjustment(request);\n\n            if (seatsToAdd > 0)\n            {\n                smSubscriptionUpdate.AdjustSeats(seatsToAdd);\n\n                await secretsManagerSubscriptionCommand.ValidateUpdateAsync(smSubscriptionUpdate);\n            }\n\n            return new Valid<InviteOrganizationUsersValidationRequest>(new InviteOrganizationUsersValidationRequest(\n                request,\n                subscriptionUpdate,\n                smSubscriptionUpdate));\n        }\n        catch (Exception ex)\n        {\n            return new Invalid<InviteOrganizationUsersValidationRequest>(\n                new Error<InviteOrganizationUsersValidationRequest>(ex.Message, request));\n        }\n    }\n\n    /// <summary>\n    /// This calculates the number of SM seats to add to the organization seat total.\n    ///\n    /// If they have a current seat limit (it can be null), we want to figure out how many are available (seats -\n    /// occupied seats). Then, we'll subtract the available seats from the number of users we're trying to invite.\n    ///\n    /// If it's negative, we have available seats and do not need to increase, so we go with 0.\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    private static int GetSecretManagerSeatAdjustment(InviteOrganizationUsersValidationRequest request) =>\n        request.InviteOrganization.SmSeats.HasValue\n            ? Math.Max(\n                request.Invites.Count(x => x.AccessSecretsManager) -\n                (request.InviteOrganization.SmSeats.Value -\n                 request.OccupiedSmSeats),\n                0)\n            : 0;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.Utilities.Errors;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;\n\npublic record OrganizationNoPaymentMethodFoundError(InviteOrganization InvalidRequest)\n    : Error<InviteOrganization>(Code, InvalidRequest)\n{\n    public const string Code = \"No payment method found.\";\n}\n\npublic record OrganizationNoSubscriptionFoundError(InviteOrganization InvalidRequest)\n    : Error<InviteOrganization>(Code, InvalidRequest)\n{\n    public const string Code = \"No subscription found.\";\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.Utilities.Validation;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;\n\npublic interface IInviteUsersOrganizationValidator : IValidator<InviteOrganization>;\n\npublic class InviteUsersOrganizationValidator : IInviteUsersOrganizationValidator\n{\n    public Task<ValidationResult<InviteOrganization>> ValidateAsync(InviteOrganization inviteOrganization)\n    {\n        if (inviteOrganization.Seats is null)\n        {\n            return Task.FromResult<ValidationResult<InviteOrganization>>(\n                new Valid<InviteOrganization>(inviteOrganization));\n        }\n\n        if (string.IsNullOrWhiteSpace(inviteOrganization.GatewayCustomerId))\n        {\n            return Task.FromResult<ValidationResult<InviteOrganization>>(\n                new Invalid<InviteOrganization>(new OrganizationNoPaymentMethodFoundError(inviteOrganization)));\n        }\n\n        if (string.IsNullOrWhiteSpace(inviteOrganization.GatewaySubscriptionId))\n        {\n            return Task.FromResult<ValidationResult<InviteOrganization>>(\n                new Invalid<InviteOrganization>(new OrganizationNoSubscriptionFoundError(inviteOrganization)));\n        }\n\n        return Task.FromResult<ValidationResult<InviteOrganization>>(new Valid<InviteOrganization>(inviteOrganization));\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.Errors;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\n\npublic record PasswordManagerSeatLimitHasBeenReachedError(PasswordManagerSubscriptionUpdate InvalidRequest)\n    : Error<PasswordManagerSubscriptionUpdate>(Code, InvalidRequest)\n{\n    public const string Code = \"Seat limit has been reached.\";\n}\n\npublic record PasswordManagerPlanDoesNotAllowAdditionalSeatsError(PasswordManagerSubscriptionUpdate InvalidRequest)\n    : Error<PasswordManagerSubscriptionUpdate>(Code, InvalidRequest)\n{\n    public const string Code = \"Plan does not allow additional seats.\";\n}\n\npublic record PasswordManagerPlanOnlyAllowsMaxAdditionalSeatsError(PasswordManagerSubscriptionUpdate InvalidRequest)\n    : Error<PasswordManagerSubscriptionUpdate>(GetErrorMessage(InvalidRequest), InvalidRequest)\n{\n    private static string GetErrorMessage(PasswordManagerSubscriptionUpdate invalidRequest) =>\n        string.Format(Code, invalidRequest.PasswordManagerPlan.MaxAdditionalSeats);\n\n    public const string Code = \"Organization plan allows a maximum of {0} additional seats.\";\n}\n\npublic record PasswordManagerMustHaveSeatsError(PasswordManagerSubscriptionUpdate InvalidRequest)\n    : Error<PasswordManagerSubscriptionUpdate>(Code, InvalidRequest)\n{\n    public const string Code = \"You do not have any Password Manager seats!\";\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\n\npublic interface IInviteUsersPasswordManagerValidator : IValidator<PasswordManagerSubscriptionUpdate>;\n\npublic class InviteUsersPasswordManagerValidator(\n    IGlobalSettings globalSettings,\n    IInviteUsersEnvironmentValidator inviteUsersEnvironmentValidator,\n    IInviteUsersOrganizationValidator inviteUsersOrganizationValidator,\n    IProviderRepository providerRepository,\n    IStripePaymentService paymentService,\n    IOrganizationRepository organizationRepository\n    ) : IInviteUsersPasswordManagerValidator\n{\n    /// <summary>\n    /// This is for validating if the organization can add additional users.\n    /// </summary>\n    /// <param name=\"subscriptionUpdate\"></param>\n    /// <returns></returns>\n    public static ValidationResult<PasswordManagerSubscriptionUpdate> ValidatePasswordManager(PasswordManagerSubscriptionUpdate subscriptionUpdate)\n    {\n        if (subscriptionUpdate.Seats is null)\n        {\n            return new Valid<PasswordManagerSubscriptionUpdate>(subscriptionUpdate);\n        }\n\n        if (subscriptionUpdate.SeatsRequiredToAdd == 0)\n        {\n            return new Valid<PasswordManagerSubscriptionUpdate>(subscriptionUpdate);\n        }\n\n        if (subscriptionUpdate.PasswordManagerPlan.BaseSeats + subscriptionUpdate.SeatsRequiredToAdd <= 0)\n        {\n            return new Invalid<PasswordManagerSubscriptionUpdate>(new PasswordManagerMustHaveSeatsError(subscriptionUpdate));\n        }\n\n        if (subscriptionUpdate.MaxSeatsExceeded)\n        {\n            return new Invalid<PasswordManagerSubscriptionUpdate>(\n                new PasswordManagerSeatLimitHasBeenReachedError(subscriptionUpdate));\n        }\n\n        if (subscriptionUpdate.PasswordManagerPlan.HasAdditionalSeatsOption is false)\n        {\n            return new Invalid<PasswordManagerSubscriptionUpdate>(\n                new PasswordManagerPlanDoesNotAllowAdditionalSeatsError(subscriptionUpdate));\n        }\n\n        // Apparently MaxAdditionalSeats is never set. Can probably be removed.\n        if (subscriptionUpdate.UpdatedSeatTotal - subscriptionUpdate.PasswordManagerPlan.BaseSeats > subscriptionUpdate.PasswordManagerPlan.MaxAdditionalSeats)\n        {\n            return new Invalid<PasswordManagerSubscriptionUpdate>(\n                new PasswordManagerPlanOnlyAllowsMaxAdditionalSeatsError(subscriptionUpdate));\n        }\n\n        return new Valid<PasswordManagerSubscriptionUpdate>(subscriptionUpdate);\n    }\n\n    public async Task<ValidationResult<PasswordManagerSubscriptionUpdate>> ValidateAsync(PasswordManagerSubscriptionUpdate request)\n    {\n        switch (ValidatePasswordManager(request))\n        {\n            case Valid<PasswordManagerSubscriptionUpdate> valid\n                when valid.Value.SeatsRequiredToAdd is 0:\n                return new Valid<PasswordManagerSubscriptionUpdate>(request);\n\n            case Invalid<PasswordManagerSubscriptionUpdate> invalid:\n                return invalid;\n        }\n\n        if (await inviteUsersEnvironmentValidator.ValidateAsync(new EnvironmentRequest(globalSettings, request)) is Invalid<EnvironmentRequest> invalidEnvironment)\n        {\n            return invalidEnvironment.Map(request);\n        }\n\n        // Organizations managed by a provider need to be scaled by the provider. This needs to be checked in the event seats are increasing.\n        var provider = await providerRepository.GetByOrganizationIdAsync(request.InviteOrganization.OrganizationId);\n\n        if (provider is not null)\n        {\n            var providerValidationResult = InvitingUserOrganizationProviderValidator.Validate(new InviteOrganizationProvider(provider));\n\n            if (providerValidationResult is Invalid<InviteOrganizationProvider> invalidProviderValidation)\n            {\n                return invalidProviderValidation.Map(request);\n            }\n        }\n\n        var organizationValidationResult = await inviteUsersOrganizationValidator.ValidateAsync(request.InviteOrganization);\n\n        if (organizationValidationResult is Invalid<InviteOrganization> organizationValidation)\n        {\n            return organizationValidation.Map(request);\n        }\n\n        var paymentSubscription = await paymentService.GetSubscriptionAsync(\n            await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId));\n\n        var paymentValidationResult = InviteUserPaymentValidation.Validate(\n            new PaymentsSubscription(paymentSubscription, request.InviteOrganization));\n\n        if (paymentValidationResult is Invalid<PaymentsSubscription> invalidPaymentValidation)\n        {\n            return invalidPaymentValidation.Map(request);\n        }\n\n        return new Valid<PasswordManagerSubscriptionUpdate>(request);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/PasswordManagerSubscriptionUpdate.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\n\npublic class PasswordManagerSubscriptionUpdate\n{\n    /// <summary>\n    /// Seats the organization has\n    /// </summary>\n    public int? Seats { get; }\n\n    /// <summary>\n    /// Max number of seats that the organization can have\n    /// </summary>\n    public int? MaxAutoScaleSeats { get; }\n\n    /// <summary>\n    /// Seats currently occupied by current users\n    /// </summary>\n    public int OccupiedSeats { get; }\n\n    /// <summary>\n    /// Users to add to the organization seats\n    /// </summary>\n    public int NewUsersToAdd { get; }\n\n    /// <summary>\n    /// Number of seats available for users\n    /// </summary>\n    public int? AvailableSeats => Seats - OccupiedSeats;\n\n    /// <summary>\n    /// Number of seats to scale the organization by.\n    ///\n    /// If Organization has no seat limit (Seats is null), then there are no new seats to add.\n    /// </summary>\n    public int SeatsRequiredToAdd => AvailableSeats.HasValue ? Math.Max(NewUsersToAdd - AvailableSeats.Value, 0) : 0;\n\n    /// <summary>\n    /// New total of seats for the organization\n    /// </summary>\n    public int? UpdatedSeatTotal => Seats + SeatsRequiredToAdd;\n\n    /// <summary>\n    /// If the new seat total is equal to the organization's auto-scale seat count\n    /// </summary>\n    public bool MaxSeatsReached => UpdatedSeatTotal.HasValue && MaxAutoScaleSeats.HasValue && UpdatedSeatTotal.Value >= MaxAutoScaleSeats.Value;\n\n    /// <summary>\n    /// If the new seat total exceeds the organization's auto-scale seat limit\n    /// </summary>\n    public bool MaxSeatsExceeded => UpdatedSeatTotal.HasValue && MaxAutoScaleSeats.HasValue && UpdatedSeatTotal.Value > MaxAutoScaleSeats.Value;\n\n    public Plan.PasswordManagerPlanFeatures PasswordManagerPlan { get; }\n\n    public InviteOrganization InviteOrganization { get; }\n\n    private PasswordManagerSubscriptionUpdate(int? organizationSeats,\n        int? organizationAutoScaleSeatLimit,\n        int currentSeats,\n        int newUsersToAdd,\n        Plan.PasswordManagerPlanFeatures plan,\n        InviteOrganization inviteOrganization)\n    {\n        Seats = organizationSeats;\n        MaxAutoScaleSeats = organizationAutoScaleSeatLimit;\n        OccupiedSeats = currentSeats;\n        NewUsersToAdd = newUsersToAdd;\n        PasswordManagerPlan = plan;\n        InviteOrganization = inviteOrganization;\n    }\n\n    public PasswordManagerSubscriptionUpdate(InviteOrganization inviteOrganization, int occupiedSeats, int newUsersToAdd) :\n        this(\n            organizationSeats: inviteOrganization.Seats,\n            organizationAutoScaleSeatLimit: inviteOrganization.MaxAutoScaleSeats,\n            currentSeats: occupiedSeats,\n            newUsersToAdd: newUsersToAdd,\n            plan: inviteOrganization.Plan.PasswordManager,\n            inviteOrganization: inviteOrganization)\n    { }\n\n    public PasswordManagerSubscriptionUpdate(InviteOrganizationUsersValidationRequest usersValidationRequest) :\n        this(\n            organizationSeats: usersValidationRequest.InviteOrganization.Seats,\n            organizationAutoScaleSeatLimit: usersValidationRequest.InviteOrganization.MaxAutoScaleSeats,\n            currentSeats: usersValidationRequest.OccupiedPmSeats,\n            newUsersToAdd: usersValidationRequest.Invites.Length,\n            plan: usersValidationRequest.InviteOrganization.Plan.PasswordManager,\n            inviteOrganization: usersValidationRequest.InviteOrganization)\n    { }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;\nusing Bit.Core.AdminConsole.Utilities.Errors;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;\n\npublic record PaymentCancelledSubscriptionError(PaymentsSubscription InvalidRequest)\n    : Error<PaymentsSubscription>(Code, InvalidRequest)\n{\n    public const string Code = \"You do not have an active subscription. Reinstate your subscription to make changes.\";\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;\n\npublic static class InviteUserPaymentValidation\n{\n    public static ValidationResult<PaymentsSubscription> Validate(PaymentsSubscription subscription)\n    {\n        if (subscription.ProductTierType is ProductTierType.Free)\n        {\n            return new Valid<PaymentsSubscription>(subscription);\n        }\n\n        if (subscription.SubscriptionStatus == StripeConstants.SubscriptionStatus.Canceled)\n        {\n            return new Invalid<PaymentsSubscription>(new PaymentCancelledSubscriptionError(subscription));\n        }\n\n        return new Valid<PaymentsSubscription>(subscription);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/PaymentsSubscription.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;\n\npublic class PaymentsSubscription\n{\n    public ProductTierType ProductTierType { get; init; }\n    public string SubscriptionStatus { get; init; }\n\n    public PaymentsSubscription() { }\n\n    public PaymentsSubscription(SubscriptionInfo subscriptionInfo, InviteOrganization inviteOrganization)\n    {\n        SubscriptionStatus = subscriptionInfo?.Subscription?.Status ?? string.Empty;\n        ProductTierType = inviteOrganization.Plan.ProductTier;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.Errors;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;\n\npublic record ProviderBillableSeatLimitError(InviteOrganizationProvider InvalidRequest) : Error<InviteOrganizationProvider>(Code, InvalidRequest)\n{\n    public const string Code = \"Seat limit has been reached. Please contact your provider to add more seats.\";\n}\n\npublic record ProviderResellerSeatLimitError(InviteOrganizationProvider InvalidRequest) : Error<InviteOrganizationProvider>(Code, InvalidRequest)\n{\n    public const string Code = \"Seat limit has been reached. Contact your provider to purchase additional seats.\";\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InviteOrganizationProvider.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums.Provider;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;\n\npublic class InviteOrganizationProvider\n{\n    public Guid ProviderId { get; init; }\n    public ProviderType Type { get; init; }\n    public ProviderStatusType Status { get; init; }\n    public bool Enabled { get; init; }\n\n    public InviteOrganizationProvider(Entities.Provider.Provider provider)\n    {\n        ProviderId = provider.Id;\n        Type = provider.Type;\n        Status = provider.Status;\n        Enabled = provider.Enabled;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Billing.Extensions;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;\n\npublic static class InvitingUserOrganizationProviderValidator\n{\n    public static ValidationResult<InviteOrganizationProvider> Validate(InviteOrganizationProvider inviteOrganizationProvider)\n    {\n        if (inviteOrganizationProvider is not { Enabled: true })\n        {\n            return new Valid<InviteOrganizationProvider>(inviteOrganizationProvider);\n        }\n\n        if (inviteOrganizationProvider.IsBillable())\n        {\n            return new Invalid<InviteOrganizationProvider>(new ProviderBillableSeatLimitError(inviteOrganizationProvider));\n        }\n\n        if (inviteOrganizationProvider.Type == ProviderType.Reseller)\n        {\n            return new Invalid<InviteOrganizationProvider>(new ProviderResellerSeatLimitError(inviteOrganizationProvider));\n        }\n\n        return new Valid<InviteOrganizationProvider>(inviteOrganizationProvider);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/ISendOrganizationConfirmationCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\n\npublic interface ISendOrganizationConfirmationCommand\n{\n    /// <summary>\n    /// Sends an organization confirmation email to the specified user.\n    /// </summary>\n    /// <param name=\"organization\">The organization to send the confirmation email for.</param>\n    /// <param name=\"userEmail\">The email address of the user to send the confirmation to.</param>\n    /// <param name=\"accessSecretsManager\">Whether the user has access to Secrets Manager.</param>\n    Task SendConfirmationAsync(Organization organization, string userEmail, bool accessSecretsManager);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\n\npublic class SendOrganizationConfirmationCommand(IMailService mailService)\n    : ISendOrganizationConfirmationCommand\n{\n    public async Task SendConfirmationAsync(Organization organization, string userEmail, bool accessSecretsManager = false)\n    {\n        await mailService.SendUpdatedOrganizationConfirmedEmailAsync(organization, userEmail, accessSecretsManager);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\n\nnamespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuery\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery;\n\n    public OrganizationUserUserDetailsQuery(\n        IOrganizationUserRepository organizationUserRepository,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery\n    )\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery;\n    }\n\n    /// <summary>\n    /// Gets the organization user user details for the provided request\n    /// </summary>\n    /// <param name=\"request\">Request details for the query</param>\n    /// <returns>List of OrganizationUserUserDetails</returns>\n    public async Task<IEnumerable<OrganizationUserUserDetails>> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request)\n    {\n        var organizationUsers = await _organizationUserRepository\n            .GetManyDetailsByOrganizationAsync(request.OrganizationId, request.IncludeGroups, request.IncludeCollections);\n\n        return organizationUsers\n            .Select(o =>\n            {\n                // Only set permissions for Custom user types for performance optimization\n                if (o.Type == OrganizationUserType.Custom)\n                {\n                    var userPermissions = o.GetPermissions();\n                    o.Permissions = CoreHelpers.ClassToJsonData(userPermissions);\n                }\n\n                return o;\n            });\n    }\n\n    /// <summary>\n    /// Get the organization user user details, two factor enabled status, and\n    /// claimed status for the provided request.\n    /// </summary>\n    /// <param name=\"request\">Request details for the query</param>\n    /// <returns>List of OrganizationUserUserDetails</returns>\n    public async Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> Get(OrganizationUserUserDetailsQueryRequest request)\n    {\n        var organizationUsers = await _organizationUserRepository\n            .GetManyDetailsByOrganizationAsync_vNext(request.OrganizationId, request.IncludeGroups, request.IncludeCollections);\n\n        var twoFactorTask = _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);\n        var claimedStatusTask = _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id));\n\n        await Task.WhenAll(twoFactorTask, claimedStatusTask);\n\n        var organizationUsersTwoFactorEnabled = twoFactorTask.Result.ToDictionary(u => u.user.Id, u => u.twoFactorIsEnabled);\n        var organizationUsersClaimedStatus = claimedStatusTask.Result;\n        var responses = organizationUsers.Select(organizationUserDetails =>\n        {\n            // Only set permissions for Custom user types for performance optimization\n            if (organizationUserDetails.Type == OrganizationUserType.Custom)\n            {\n                var organizationUserPermissions = organizationUserDetails.GetPermissions();\n                organizationUserDetails.Permissions = CoreHelpers.ClassToJsonData(organizationUserPermissions);\n            }\n\n            var userHasTwoFactorEnabled = organizationUsersTwoFactorEnabled[organizationUserDetails.Id];\n            var userIsClaimedByOrganization = organizationUsersClaimedStatus[organizationUserDetails.Id];\n\n            return (organizationUserDetails, userHasTwoFactorEnabled, userIsClaimedByOrganization);\n        });\n\n        return responses;\n    }\n\n    /// <summary>\n    /// Get the organization users user details, two factor enabled status, and\n    /// claimed status for confirmed users that are enrolled in account recovery\n    /// </summary>\n    /// <param name=\"request\">Request details for the query</param>\n    /// <returns>List of OrganizationUserUserDetails</returns>\n    public async Task<IEnumerable<(OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization)>> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request)\n    {\n        var organizationUsers = (await _organizationUserRepository\n            .GetManyDetailsByOrganizationAsync_vNext(request.OrganizationId, request.IncludeGroups, request.IncludeCollections))\n            .Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false &&\n                OrganizationUser.IsValidResetPasswordKey(o.ResetPasswordKey))\n            .ToArray();\n\n        var twoFactorTask = _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);\n        var claimedStatusTask = _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id));\n\n        await Task.WhenAll(twoFactorTask, claimedStatusTask);\n\n        var organizationUsersTwoFactorEnabled = twoFactorTask.Result.ToDictionary(u => u.user.Id, u => u.twoFactorIsEnabled);\n        var organizationUsersClaimedStatus = claimedStatusTask.Result;\n        var responses = organizationUsers.Select(organizationUserDetails =>\n        {\n            // Only set permissions for Custom user types for performance optimization\n            if (organizationUserDetails.Type == OrganizationUserType.Custom)\n            {\n                var organizationUserPermissions = organizationUserDetails.GetPermissions();\n                organizationUserDetails.Permissions = CoreHelpers.ClassToJsonData(organizationUserPermissions);\n            }\n\n            var userHasTwoFactorEnabled = organizationUsersTwoFactorEnabled[organizationUserDetails.Id];\n            var userIsClaimedByOrganization = organizationUsersClaimedStatus[organizationUserDetails.Id];\n\n            return (organizationUserDetails, userHasTwoFactorEnabled, userIsClaimedByOrganization);\n        });\n\n        return responses;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/PushAutoConfirmNotificationCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class PushAutoConfirmNotificationCommand : IPushAutoConfirmNotificationCommand\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    public PushAutoConfirmNotificationCommand(\n        IOrganizationUserRepository organizationUserRepository,\n        IPushNotificationService pushNotificationService)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _pushNotificationService = pushNotificationService;\n    }\n\n    public async Task PushAsync(Guid userId, Guid organizationId)\n    {\n        var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);\n        if (organizationUser == null)\n        {\n            throw new Exception(\"Organization user not found\");\n        }\n\n        var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(\n            organizationId,\n            OrganizationUserType.Admin);\n\n        var customUsersWithManagePermission = (await _organizationUserRepository.GetManyDetailsByRoleAsync(\n                organizationId,\n                OrganizationUserType.Custom))\n            .Where(c => c.GetPermissions()?.ManageUsers == true)\n            .Select(c => c.UserId);\n\n        var userIds = admins\n            .Select(a => a.UserId)\n            .Concat(customUsersWithManagePermission)\n            .Where(id => id.HasValue)\n            .Select(id => id!.Value)\n            .Distinct();\n\n        foreach (var adminUserId in userIds)\n        {\n            await _pushNotificationService.PushAsync(\n                new PushNotification<AutoConfirmPushNotification>\n                {\n                    Target = NotificationTarget.User,\n                    TargetId = adminUserId,\n                    Type = PushType.AutoConfirm,\n                    Payload = new AutoConfirmPushNotification\n                    {\n                        UserId = adminUserId,\n                        OrganizationId = organizationId,\n                        TargetUserId = userId,\n                        TargetOrganizationUserId = organizationUser.Id\n                    },\n                    ExcludeCurrentContext = false,\n                });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand\n{\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IEventService _eventService;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IPushRegistrationService _pushRegistrationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;\n    private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery;\n    private readonly IFeatureService _featureService;\n    private readonly TimeProvider _timeProvider;\n\n    public const string UserNotFoundErrorMessage = \"User not found.\";\n    public const string UsersInvalidErrorMessage = \"Users invalid.\";\n    public const string RemoveYourselfErrorMessage = \"You cannot remove yourself.\";\n    public const string RemoveOwnerByNonOwnerErrorMessage = \"Only owners can remove other owners.\";\n    public const string RemoveAdminByCustomUserErrorMessage = \"Custom users can not remove admins.\";\n    public const string RemoveLastConfirmedOwnerErrorMessage = \"Organization must have at least one confirmed owner.\";\n    public const string RemoveClaimedAccountErrorMessage = \"Cannot remove member accounts claimed by the organization. To offboard a member, revoke or delete the account.\";\n\n    public RemoveOrganizationUserCommand(\n        IDeviceRepository deviceRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IEventService eventService,\n        IPushNotificationService pushNotificationService,\n        IPushRegistrationService pushRegistrationService,\n        ICurrentContext currentContext,\n        IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,\n        IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery,\n        IFeatureService featureService,\n        TimeProvider timeProvider)\n    {\n        _deviceRepository = deviceRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _eventService = eventService;\n        _pushNotificationService = pushNotificationService;\n        _pushRegistrationService = pushRegistrationService;\n        _currentContext = currentContext;\n        _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;\n        _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery;\n        _featureService = featureService;\n        _timeProvider = timeProvider;\n    }\n\n    public async Task RemoveUserAsync(Guid organizationId, Guid userId)\n    {\n        var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);\n        ValidateRemoveUser(organizationId, organizationUser);\n\n        await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser: null);\n\n        await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);\n    }\n\n    public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)\n    {\n        var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);\n        ValidateRemoveUser(organizationId, organizationUser);\n\n        await RepositoryRemoveUserAsync(organizationUser, deletingUserId, eventSystemUser: null);\n\n        await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);\n    }\n\n    public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)\n    {\n        var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);\n        ValidateRemoveUser(organizationId, organizationUser);\n\n        await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser);\n\n        await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);\n    }\n\n    public async Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(\n        Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? deletingUserId)\n    {\n        var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId, eventSystemUser: null);\n\n        var removedUsers = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList();\n        if (removedUsers.Any())\n        {\n            DateTime? eventDate = _timeProvider.GetUtcNow().UtcDateTime;\n            await _eventService.LogOrganizationUserEventsAsync(\n                removedUsers.Select(ou => (ou, EventType.OrganizationUser_Removed, eventDate)));\n        }\n\n        return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage));\n    }\n\n    public async Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(\n        Guid organizationId, IEnumerable<Guid> organizationUserIds, EventSystemUser eventSystemUser)\n    {\n        var result = await RemoveUsersInternalAsync(organizationId, organizationUserIds, deletingUserId: null, eventSystemUser);\n\n        var removedUsers = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList();\n        if (removedUsers.Any())\n        {\n            DateTime? eventDate = _timeProvider.GetUtcNow().UtcDateTime;\n            await _eventService.LogOrganizationUserEventsAsync(\n                removedUsers.Select(ou => (ou, EventType.OrganizationUser_Removed, eventSystemUser, eventDate)));\n        }\n\n        return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage));\n    }\n\n    public async Task UserLeaveAsync(Guid organizationId, Guid userId)\n    {\n        var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);\n        ValidateRemoveUser(organizationId, organizationUser);\n\n        await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser: null);\n\n        await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Left);\n    }\n\n    private void ValidateRemoveUser(Guid organizationId, OrganizationUser orgUser)\n    {\n        if (orgUser == null || orgUser.OrganizationId != organizationId)\n        {\n            throw new NotFoundException(UserNotFoundErrorMessage);\n        }\n    }\n\n    private async Task RepositoryRemoveUserAsync(OrganizationUser orgUser, Guid? deletingUserId, EventSystemUser? eventSystemUser)\n    {\n        if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value)\n        {\n            throw new BadRequestException(RemoveYourselfErrorMessage);\n        }\n\n        if (orgUser.Type == OrganizationUserType.Owner)\n        {\n            if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(orgUser.OrganizationId))\n            {\n                throw new BadRequestException(RemoveOwnerByNonOwnerErrorMessage);\n            }\n\n            if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, new[] { orgUser.Id }, includeProvider: true))\n            {\n                throw new BadRequestException(RemoveLastConfirmedOwnerErrorMessage);\n            }\n        }\n\n        if (orgUser.Type == OrganizationUserType.Admin && await _currentContext.OrganizationCustom(orgUser.OrganizationId))\n        {\n            throw new BadRequestException(RemoveAdminByCustomUserErrorMessage);\n        }\n\n        if (deletingUserId.HasValue && eventSystemUser == null)\n        {\n            var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id });\n            if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)\n            {\n                throw new BadRequestException(RemoveClaimedAccountErrorMessage);\n            }\n        }\n\n        await _organizationUserRepository.DeleteAsync(orgUser);\n\n        if (orgUser.UserId.HasValue)\n        {\n            await DeleteAndPushUserRegistrationAsync(orgUser.OrganizationId, orgUser.UserId.Value);\n        }\n    }\n\n    private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)\n    {\n        var devices = await _deviceRepository.GetManyByUserIdAsync(userId);\n        return devices\n            .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))\n            .Select(d => d.Id.ToString());\n    }\n\n    private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)\n    {\n        var devices = await GetUserDeviceIdsAsync(userId);\n        await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,\n            organizationId.ToString());\n        await _pushNotificationService.PushSyncOrgKeysAsync(userId);\n    }\n\n    private async Task<IEnumerable<(OrganizationUser OrganizationUser, string ErrorMessage)>> RemoveUsersInternalAsync(\n        Guid organizationId, IEnumerable<Guid> organizationUsersId, Guid? deletingUserId, EventSystemUser? eventSystemUser)\n    {\n        var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);\n        var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId).ToList();\n\n        if (!filteredUsers.Any())\n        {\n            throw new BadRequestException(UsersInvalidErrorMessage);\n        }\n\n        if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))\n        {\n            throw new BadRequestException(RemoveLastConfirmedOwnerErrorMessage);\n        }\n\n        var deletingUserIsOwner = false;\n        if (deletingUserId.HasValue)\n        {\n            deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);\n        }\n\n        var claimedStatus = deletingUserId.HasValue && eventSystemUser == null\n            ? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id))\n            : filteredUsers.ToDictionary(u => u.Id, u => false);\n        var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();\n        foreach (var orgUser in filteredUsers)\n        {\n            try\n            {\n                if (deletingUserId.HasValue && orgUser.UserId == deletingUserId)\n                {\n                    throw new BadRequestException(RemoveYourselfErrorMessage);\n                }\n\n                if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner)\n                {\n                    throw new BadRequestException(RemoveOwnerByNonOwnerErrorMessage);\n                }\n\n                if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)\n                {\n                    throw new BadRequestException(RemoveClaimedAccountErrorMessage);\n                }\n\n                result.Add((orgUser, string.Empty));\n            }\n            catch (BadRequestException e)\n            {\n                result.Add((orgUser, e.Message));\n            }\n        }\n\n        var organizationUsersToRemove = result.Where(r => string.IsNullOrEmpty(r.ErrorMessage)).Select(r => r.OrganizationUser).ToList();\n        if (organizationUsersToRemove.Any())\n        {\n            await _organizationUserRepository.DeleteManyAsync(organizationUsersToRemove.Select(ou => ou.Id));\n            foreach (var orgUser in organizationUsersToRemove.Where(ou => ou.UserId.HasValue))\n            {\n                await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId!.Value);\n            }\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Requests/OrganizationUserUserDetailsQueryRequest.cs",
    "content": "﻿namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\n\npublic class OrganizationUserUserDetailsQueryRequest\n{\n    public Guid OrganizationId { get; set; }\n    public bool IncludeGroups { get; set; } = false;\n    public bool IncludeCollections { get; set; } = false;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Requests/RevokeOrganizationUserRequest.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\n\npublic record RevokeOrganizationUsersRequest(\n    Guid OrganizationId,\n    IEnumerable<OrganizationUserUserDetails> OrganizationUsers,\n    IActingUser ActionPerformedBy)\n{\n    public RevokeOrganizationUsersRequest(Guid organizationId, OrganizationUserUserDetails organizationUser, IActingUser actionPerformedBy)\n        : this(organizationId, [organizationUser], actionPerformedBy) { }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/IRestoreOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;\n\n/// <summary>\n/// Restores a user back to their previous status.\n/// </summary>\npublic interface IRestoreOrganizationUserCommand\n{\n    /// <summary>\n    /// Validates that the requesting user can perform the action. There is also a check done to ensure the organization\n    /// can re-add this user based on their current occupied seats.\n    ///\n    /// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as\n    /// other organizations the user may belong to.\n    ///\n    /// Reference Events and Push Notifications are fired off for this as well.\n    /// </summary>\n    /// <param name=\"organizationUser\">Revoked user to be restored.</param>\n    /// <param name=\"restoringUserId\">UserId of the user performing the action.</param>\n    Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, string? defaultCollectionName);\n\n    /// <summary>\n    /// Validates that the requesting user can perform the action. There is also a check done to ensure the organization\n    /// can re-add this user based on their current occupied seats.\n    ///\n    /// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as\n    /// other organizations the user may belong to.\n    ///\n    /// Reference Events and Push Notifications are fired off for this as well.\n    /// </summary>\n    /// <param name=\"organizationUser\">Revoked user to be restored.</param>\n    /// <param name=\"systemUser\">System that is performing the action on behalf of the organization (Public API, SCIM, etc.)</param>\n    Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);\n\n    /// <summary>\n    /// Validates that the requesting user can perform the action. There is also a check done to ensure the organization\n    /// can re-add this user based on their current occupied seats.\n    ///\n    /// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as\n    /// other organizations the user may belong to.\n    ///\n    /// Reference Events and Push Notifications are fired off for this as well.\n    /// </summary>\n    /// <param name=\"organizationId\">Organization the users should be restored to.</param>\n    /// <param name=\"organizationUserIds\">List of organization user ids to restore to previous status.</param>\n    /// <param name=\"restoringUserId\">UserId of the user performing the action.</param>\n    /// <param name=\"userService\">Passed in from caller to avoid circular dependency</param>\n    /// <returns>List of organization user Ids and strings. A successful restoration will have an empty string.\n    /// If an error occurs, the error message will be provided.</returns>\n    Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService, string? defaultCollectionName);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements.Errors;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;\n\npublic class RestoreOrganizationUserCommand(\n    ICurrentContext currentContext,\n    IEventService eventService,\n    IPushNotificationService pushNotificationService,\n    IOrganizationUserRepository organizationUserRepository,\n    IOrganizationRepository organizationRepository,\n    ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n    IPolicyService policyService,\n    IUserRepository userRepository,\n    IOrganizationService organizationService,\n    IFeatureService featureService,\n    IPolicyRequirementQuery policyRequirementQuery,\n    ICollectionRepository collectionRepository,\n    IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator,\n    IDeleteEmergencyAccessCommand deleteEmergencyAccessCommand) : IRestoreOrganizationUserCommand\n{\n    public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, string defaultCollectionName)\n    {\n        if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)\n        {\n            throw new BadRequestException(\"You cannot restore yourself.\");\n        }\n\n        if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&\n            !await currentContext.OrganizationOwner(organizationUser.OrganizationId))\n        {\n            throw new BadRequestException(\"Only owners can restore other owners.\");\n        }\n\n        await RepositoryRestoreUserAsync(organizationUser, defaultCollectionName);\n        await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);\n\n        if (organizationUser.UserId.HasValue)\n        {\n            await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);\n        }\n    }\n\n    public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)\n    {\n        await RepositoryRestoreUserAsync(organizationUser, null); // users stored by a system user will not get a default collection at this point.\n        await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored,\n            systemUser);\n\n        if (organizationUser.UserId.HasValue)\n        {\n            await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);\n        }\n    }\n\n    private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser, string defaultCollectionName)\n    {\n        if (organizationUser.Status != OrganizationUserStatusType.Revoked)\n        {\n            throw new BadRequestException(\"Already active.\");\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);\n        var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);\n        var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;\n\n        if (availableSeats < 1)\n        {\n            await organizationService.AutoAddSeatsAsync(organization, 1); // Hooray\n        }\n\n        var userTwoFactorIsEnabled = false;\n        // Only check 2FA status if the user is linked to a user account\n        if (organizationUser.UserId.HasValue)\n        {\n            userTwoFactorIsEnabled =\n                (await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync([organizationUser.UserId.Value]))\n                .FirstOrDefault()\n                .twoFactorIsEnabled;\n        }\n\n        if (organization.PlanType == PlanType.Free && organizationUser.UserId.HasValue)\n        {\n            await CheckUserForOtherFreeOrganizationOwnershipAsync(organizationUser);\n        }\n\n        await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled);\n\n        var status = OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser);\n\n        await organizationUserRepository.RestoreAsync(organizationUser.Id, status);\n\n        if (organizationUser.UserId.HasValue\n           && organization.UseMyItems\n           && (await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(organizationUser.UserId.Value)).State == OrganizationDataOwnershipState.Enabled\n           && status == OrganizationUserStatusType.Confirmed\n           && !string.IsNullOrWhiteSpace(defaultCollectionName))\n        {\n            await collectionRepository.CreateDefaultCollectionsAsync(organizationUser.OrganizationId,\n                [organizationUser.Id],\n                defaultCollectionName);\n        }\n    }\n\n    private async Task CheckUserForOtherFreeOrganizationOwnershipAsync(OrganizationUser organizationUser)\n    {\n        var relatedOrgUsersFromOtherOrgs = await organizationUserRepository.GetManyByUserAsync(organizationUser.UserId!.Value);\n        var otherOrgs = await organizationRepository.GetManyByUserIdAsync(organizationUser.UserId.Value);\n\n        var orgOrgUserDict = relatedOrgUsersFromOtherOrgs\n            .Where(x => x.Id != organizationUser.Id)\n            .ToDictionary(x => x, x => otherOrgs.FirstOrDefault(y => y.Id == x.OrganizationId));\n\n        CheckForOtherFreeOrganizationOwnership(organizationUser, orgOrgUserDict);\n    }\n\n    private async Task<Dictionary<OrganizationUser, Organization>> GetRelatedOrganizationUsersAndOrganizationsAsync(\n        List<OrganizationUser> organizationUsers)\n    {\n        var allUserIds = organizationUsers\n            .Where(x => x.UserId.HasValue)\n            .Select(x => x.UserId.Value);\n\n        var otherOrganizationUsers = (await organizationUserRepository.GetManyByManyUsersAsync(allUserIds))\n            .Where(x => organizationUsers.Any(y => y.Id == x.Id) == false)\n            .ToArray();\n\n        var otherOrgs = await organizationRepository.GetManyByIdsAsync(otherOrganizationUsers\n                .Select(x => x.OrganizationId)\n                .Distinct());\n\n        return otherOrganizationUsers\n            .ToDictionary(x => x, x => otherOrgs.FirstOrDefault(y => y.Id == x.OrganizationId));\n    }\n\n    private static void CheckForOtherFreeOrganizationOwnership(OrganizationUser organizationUser,\n        Dictionary<OrganizationUser, Organization> otherOrgUsersAndOrgs)\n    {\n        var ownerOrAdminList = new[] { OrganizationUserType.Owner, OrganizationUserType.Admin };\n\n        if (ownerOrAdminList.Any(x => organizationUser.Type == x) &&\n            otherOrgUsersAndOrgs.Any(x =>\n                x.Key.UserId == organizationUser.UserId &&\n                ownerOrAdminList.Any(userType => userType == x.Key.Type) &&\n                x.Key.Status == OrganizationUserStatusType.Confirmed &&\n                x.Value.PlanType == PlanType.Free))\n        {\n            throw new BadRequestException(\n                \"User is an owner/admin of another free organization. Please have them upgrade to a paid plan to restore their account.\");\n        }\n    }\n\n    public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,\n        IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService,\n        string defaultCollectionName)\n    {\n        var orgUsers = await organizationUserRepository.GetManyAsync(organizationUserIds);\n        var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)\n            .ToList();\n\n        if (filteredUsers.Count == 0)\n        {\n            throw new BadRequestException(\"Users invalid.\");\n        }\n\n        var organization = await organizationRepository.GetByIdAsync(organizationId);\n        var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);\n        var availableSeats = organization.Seats.GetValueOrDefault(0) - seatCounts.Total;\n        var newSeatsRequired = organizationUserIds.Count() - availableSeats;\n        await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);\n\n        var deletingUserIsOwner = false;\n        if (restoringUserId.HasValue)\n        {\n            deletingUserIsOwner = await currentContext.OrganizationOwner(organizationId);\n        }\n\n        // Query Two Factor Authentication status for all users in the organization\n        // This is an optimization to avoid querying the Two Factor Authentication status for each user individually\n        var organizationUsersTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(\n            filteredUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId.Value));\n\n        var orgUsersAndOrgs = await GetRelatedOrganizationUsersAndOrganizationsAsync(filteredUsers);\n\n        var result = new List<Tuple<OrganizationUser, string>>();\n\n        foreach (var organizationUser in filteredUsers)\n        {\n            try\n            {\n                if (organizationUser.Status != OrganizationUserStatusType.Revoked)\n                {\n                    throw new BadRequestException(\"Already active.\");\n                }\n\n                if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId)\n                {\n                    throw new BadRequestException(\"You cannot restore yourself.\");\n                }\n\n                if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&\n                    !deletingUserIsOwner)\n                {\n                    throw new BadRequestException(\"Only owners can restore other owners.\");\n                }\n\n                var twoFactorIsEnabled = organizationUser.UserId.HasValue\n                                         && organizationUsersTwoFactorEnabled\n                                             .FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value)\n                                             .twoFactorIsEnabled;\n\n                await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled);\n\n                if (organization.PlanType == PlanType.Free)\n                {\n                    CheckForOtherFreeOrganizationOwnership(organizationUser, orgUsersAndOrgs);\n                }\n\n                var status = OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser);\n\n                await organizationUserRepository.RestoreAsync(organizationUser.Id, status);\n                organizationUser.Status = status;\n\n                if (organizationUser.UserId.HasValue)\n                {\n                    await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);\n                }\n\n                await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);\n\n                result.Add(Tuple.Create(organizationUser, \"\"));\n            }\n            catch (BadRequestException e)\n            {\n                result.Add(Tuple.Create(organizationUser, e.Message));\n            }\n        }\n\n        await CreateDefaultCollectionsForConfirmedUsersAsync(organization, defaultCollectionName,\n            result.Where(r => r.Item2 == \"\").Select(x => x.Item1).ToList());\n\n\n        return result;\n    }\n\n    private async Task CreateDefaultCollectionsForConfirmedUsersAsync(Organization organization, string defaultCollectionName,\n        ICollection<OrganizationUser> restoredUsers)\n    {\n        if (string.IsNullOrWhiteSpace(defaultCollectionName))\n        {\n            return;\n        }\n\n        if (!organization.UseMyItems)\n        {\n            return;\n        }\n\n        var restoredConfirmedUsers = restoredUsers\n            .Where(w => w.Status == OrganizationUserStatusType.Confirmed)\n            .Where(w => w.UserId != null)\n            .Select(s => s.UserId.Value)\n            .ToList();\n\n        if (restoredConfirmedUsers.Count == 0)\n        {\n            return;\n        }\n\n        var restoredUserPolicyRequirements = await\n            policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(restoredConfirmedUsers);\n\n        var orgUserIdsToCreateDefaultCollectionsFor = restoredUserPolicyRequirements\n            .Select(s => s.Requirement.GetDefaultCollectionRequestOnConfirm(organization.Id))\n            .Where(w => w.ShouldCreateDefaultCollection)\n            .Select(s => s.OrganizationUserId)\n            .ToList();\n\n        if (orgUserIdsToCreateDefaultCollectionsFor.Count != 0)\n        {\n            await collectionRepository.CreateDefaultCollectionsAsync(organization.Id,\n                orgUserIdsToCreateDefaultCollectionsFor,\n                defaultCollectionName);\n        }\n    }\n\n    private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)\n    {\n        // An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant\n        // The user will be subject to the same checks when they try to accept the invite\n        if (OrganizationService.GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited)\n        {\n            return;\n        }\n\n        var userId = orgUser.UserId.Value;\n\n        var allOrgUsers = await organizationUserRepository.GetManyByUserAsync(userId);\n        var user = await userRepository.GetByIdAsync(userId);\n\n        var singleOrgRequirement = await policyRequirementQuery.GetAsync<SingleOrganizationPolicyRequirement>(userId);\n        var singleOrgError = singleOrgRequirement.CanJoinOrganization(orgUser.OrganizationId, allOrgUsers);\n\n        var twoFactorCompliant = userHasTwoFactorEnabled || !await IsTwoFactorRequiredForOrganizationAsync(userId, orgUser.OrganizationId);\n\n        if (singleOrgError is not null && !twoFactorCompliant)\n        {\n            throw new BadRequestException(user.Email +\n                                          \" is not compliant with the single organization and two-step login policy\");\n        }\n\n        if (singleOrgError is not null)\n        {\n            var singleOrgErrorMessage = singleOrgError switch\n            {\n                UserIsAMemberOfAnotherOrganization => $\"{user.Email} cannot be restored until they leave or remove all other organizations.\",\n                UserIsAMemberOfAnOrganizationThatHasSingleOrgPolicy => $\"{user.Email} cannot be restored because they are in another organization which forbids it.\",\n                _ => singleOrgError.Message\n            };\n\n            throw new BadRequestException(singleOrgErrorMessage);\n        }\n\n        if (!twoFactorCompliant)\n        {\n            throw new BadRequestException(user.Email + \" is not compliant with the two-step login policy\");\n        }\n\n        if (featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var policyRequirement = await policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(\n                user.Id);\n\n            var validationResult = await automaticUserConfirmationPolicyEnforcementValidator.IsCompliantAsync(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.OrganizationId, allOrgUsers, user!),\n                policyRequirement);\n\n            var badRequestException = validationResult.Match(\n                error => new BadRequestException(user.Email +\n                                                 \" is not compliant with the automatic user confirmation policy: \" +\n                                                 error.Message),\n                _ => null);\n\n            if (badRequestException is not null)\n            {\n                throw badRequestException;\n            }\n\n            if (policyRequirement.IsEnabled(orgUser.OrganizationId))\n            {\n                await deleteEmergencyAccessCommand.DeleteAllByUserIdAsync(user.Id);\n            }\n        }\n    }\n\n    private async Task<bool> IsTwoFactorRequiredForOrganizationAsync(Guid userId, Guid organizationId)\n    {\n        if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))\n        {\n            var requirement = await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(userId);\n            return requirement.IsTwoFactorRequiredForOrganization(organizationId);\n        }\n\n        var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,\n            PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);\n        return invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Bit.Core.AdminConsole.Utilities.Commands;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class RevokeNonCompliantOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository,\n    IEventService eventService,\n    IHasConfirmedOwnersExceptQuery confirmedOwnersExceptQuery,\n    TimeProvider timeProvider) : IRevokeNonCompliantOrganizationUserCommand\n{\n    public const string ErrorCannotRevokeSelf = \"You cannot revoke yourself.\";\n    public const string ErrorOnlyOwnersCanRevokeOtherOwners = \"Only owners can revoke other owners.\";\n    public const string ErrorUserAlreadyRevoked = \"User is already revoked.\";\n    public const string ErrorOrgMustHaveAtLeastOneOwner = \"Organization must have at least one confirmed owner.\";\n    public const string ErrorInvalidUsers = \"Invalid users.\";\n    public const string ErrorRequestedByWasNotValid = \"Action was performed by an unexpected type.\";\n\n    public async Task<CommandResult> RevokeNonCompliantOrganizationUsersAsync(RevokeOrganizationUsersRequest request)\n    {\n        var validationResult = await ValidateAsync(request);\n\n        if (validationResult.HasErrors)\n        {\n            return validationResult;\n        }\n\n        await organizationUserRepository.RevokeManyByIdAsync(request.OrganizationUsers.Select(x => x.Id));\n\n        var now = timeProvider.GetUtcNow();\n\n        switch (request.ActionPerformedBy)\n        {\n            case StandardUser:\n                await eventService.LogOrganizationUserEventsAsync(\n                    request.OrganizationUsers.Select(x => GetRevokedUserEventTuple(x, now)));\n                break;\n            case SystemUser { SystemUserType: not null } loggableSystem:\n                await eventService.LogOrganizationUserEventsAsync(\n                    request.OrganizationUsers.Select(x =>\n                        GetRevokedUserEventBySystemUserTuple(x, loggableSystem.SystemUserType.Value, now)));\n                break;\n        }\n\n        return validationResult;\n    }\n\n    private static (OrganizationUserUserDetails organizationUser, EventType eventType, DateTime? time) GetRevokedUserEventTuple(\n        OrganizationUserUserDetails organizationUser, DateTimeOffset dateTimeOffset) =>\n        new(organizationUser, EventType.OrganizationUser_Revoked, dateTimeOffset.UtcDateTime);\n\n    private static (OrganizationUserUserDetails organizationUser, EventType eventType, EventSystemUser eventSystemUser, DateTime? time) GetRevokedUserEventBySystemUserTuple(\n        OrganizationUserUserDetails organizationUser, EventSystemUser systemUser, DateTimeOffset dateTimeOffset) => new(organizationUser,\n        EventType.OrganizationUser_Revoked, systemUser, dateTimeOffset.UtcDateTime);\n\n    private async Task<CommandResult> ValidateAsync(RevokeOrganizationUsersRequest request)\n    {\n        if (!PerformedByIsAnExpectedType(request.ActionPerformedBy))\n        {\n            return new CommandResult(ErrorRequestedByWasNotValid);\n        }\n\n        if (request.ActionPerformedBy is StandardUser user\n            && request.OrganizationUsers.Any(x => x.UserId == user.UserId))\n        {\n            return new CommandResult(ErrorCannotRevokeSelf);\n        }\n\n        if (request.OrganizationUsers.Any(x => x.OrganizationId != request.OrganizationId))\n        {\n            return new CommandResult(ErrorInvalidUsers);\n        }\n\n        if (!await confirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(\n                    request.OrganizationId,\n                    request.OrganizationUsers.Select(x => x.Id)))\n        {\n            return new CommandResult(ErrorOrgMustHaveAtLeastOneOwner);\n        }\n\n        return request.OrganizationUsers.Aggregate(new CommandResult(), (result, userToRevoke) =>\n        {\n            if (IsAlreadyRevoked(userToRevoke))\n            {\n                result.ErrorMessages.Add($\"{ErrorUserAlreadyRevoked} Id: {userToRevoke.Id}\");\n                return result;\n            }\n\n            if (NonOwnersCannotRevokeOwners(userToRevoke, request.ActionPerformedBy))\n            {\n                result.ErrorMessages.Add($\"{ErrorOnlyOwnersCanRevokeOtherOwners}\");\n                return result;\n            }\n\n            return result;\n        });\n    }\n\n    private static bool PerformedByIsAnExpectedType(IActingUser entity) => entity is SystemUser or StandardUser;\n\n    private static bool IsAlreadyRevoked(OrganizationUserUserDetails organizationUser) =>\n        organizationUser is { Status: OrganizationUserStatusType.Revoked };\n\n    private static bool NonOwnersCannotRevokeOwners(OrganizationUserUserDetails organizationUser,\n        IActingUser actingUser) =>\n        actingUser is StandardUser { IsOrganizationOwnerOrProvider: false } && organizationUser.Type == OrganizationUserType.Owner;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/IRevokeOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;\n\npublic interface IRevokeOrganizationUserCommand\n{\n    Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId);\n    Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v1/RevokeOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;\n\npublic class RevokeOrganizationUserCommand(\n    IEventService eventService,\n    IPushNotificationService pushNotificationService,\n    IOrganizationUserRepository organizationUserRepository,\n    ICurrentContext currentContext,\n    IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)\n    : IRevokeOrganizationUserCommand\n{\n    public async Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId)\n    {\n        if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId.Value)\n        {\n            throw new BadRequestException(\"You cannot revoke yourself.\");\n        }\n\n        if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue &&\n            !await currentContext.OrganizationOwner(organizationUser.OrganizationId))\n        {\n            throw new BadRequestException(\"Only owners can revoke other owners.\");\n        }\n\n        await RepositoryRevokeUserAsync(organizationUser);\n        await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);\n\n        if (organizationUser.UserId.HasValue)\n        {\n            await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);\n        }\n    }\n\n    public async Task RevokeUserAsync(OrganizationUser organizationUser,\n        EventSystemUser systemUser)\n    {\n        await RepositoryRevokeUserAsync(organizationUser);\n        await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked,\n            systemUser);\n\n        if (organizationUser.UserId.HasValue)\n        {\n            await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);\n        }\n    }\n\n    private async Task RepositoryRevokeUserAsync(OrganizationUser organizationUser)\n    {\n        if (organizationUser.Status == OrganizationUserStatusType.Revoked)\n        {\n            throw new BadRequestException(\"Already revoked.\");\n        }\n\n        if (!await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId,\n                new[] { organizationUser.Id }, includeProvider: true))\n        {\n            throw new BadRequestException(\"Organization must have at least one confirmed owner.\");\n        }\n\n        await organizationUserRepository.RevokeAsync(organizationUser.Id);\n        organizationUser.Status = OrganizationUserStatusType.Revoked;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\npublic record UserAlreadyRevoked() : BadRequestError(\"Already revoked.\");\npublic record CannotRevokeYourself() : BadRequestError(\"You cannot revoke yourself.\");\npublic record OnlyOwnersCanRevokeOwners() : BadRequestError(\"Only owners can revoke other owners.\");\npublic record MustHaveConfirmedOwner() : BadRequestError(\"Organization must have at least one confirmed owner.\");\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/IRevokeOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2.Results;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\npublic interface IRevokeOrganizationUserCommand\n{\n    Task<IEnumerable<BulkCommandResult>> RevokeUsersAsync(RevokeOrganizationUsersRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/IRevokeOrganizationUserValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\npublic interface IRevokeOrganizationUserValidator\n{\n    Task<ICollection<ValidationResult<OrganizationUser>>> ValidateAsync(RevokeOrganizationUsersValidationRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/RevokeOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.Utilities.v2.Results;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\npublic class RevokeOrganizationUserCommand(\n    IOrganizationUserRepository organizationUserRepository,\n    IEventService eventService,\n    IPushNotificationService pushNotificationService,\n    IRevokeOrganizationUserValidator validator,\n    TimeProvider timeProvider,\n    ILogger<RevokeOrganizationUserCommand> logger)\n    : IRevokeOrganizationUserCommand\n{\n    public async Task<IEnumerable<BulkCommandResult>> RevokeUsersAsync(RevokeOrganizationUsersRequest request)\n    {\n        var validationRequest = await CreateValidationRequestsAsync(request);\n\n        var results = await validator.ValidateAsync(validationRequest);\n\n        var validUsers = results.Where(r => r.IsValid).Select(r => r.Request).ToList();\n\n        await RevokeValidUsersAsync(validUsers);\n\n        await Task.WhenAll(\n            LogRevokedOrganizationUsersAsync(validUsers, request.PerformedBy),\n            SendPushNotificationsAsync(validUsers)\n        );\n\n        return results.Select(r => r.Match(\n            error => new BulkCommandResult(r.Request.Id, error),\n            _ => new BulkCommandResult(r.Request.Id, new None())\n        ));\n    }\n\n    private async Task<RevokeOrganizationUsersValidationRequest> CreateValidationRequestsAsync(\n        RevokeOrganizationUsersRequest request)\n    {\n        var organizationUser = await organizationUserRepository\n            .GetManyAsync(request.OrganizationUserIdsToRevoke);\n\n        var organizationUserToRevoke = organizationUser\n            .Where(x => x.OrganizationId == request.OrganizationId)\n            .ToArray();\n\n        return new RevokeOrganizationUsersValidationRequest(\n            request.OrganizationId,\n            organizationUserToRevoke,\n            request.PerformedBy);\n    }\n\n    private async Task RevokeValidUsersAsync(ICollection<OrganizationUser> validUsers)\n    {\n        if (validUsers.Count == 0)\n        {\n            return;\n        }\n\n        await organizationUserRepository.RevokeManyByIdAsync(validUsers.Select(u => u.Id));\n    }\n\n    private async Task LogRevokedOrganizationUsersAsync(\n        ICollection<OrganizationUser> revokedUsers,\n        IActingUser actingUser)\n    {\n        if (revokedUsers.Count == 0)\n        {\n            return;\n        }\n\n        var eventDate = timeProvider.GetUtcNow().UtcDateTime;\n\n        if (actingUser is SystemUser { SystemUserType: not null })\n        {\n            var revokeEventsWithSystem = revokedUsers\n                .Select(user => (user, EventType.OrganizationUser_Revoked, actingUser.SystemUserType!.Value,\n                    (DateTime?)eventDate))\n                .ToList();\n            await eventService.LogOrganizationUserEventsAsync(revokeEventsWithSystem);\n        }\n        else\n        {\n            var revokeEvents = revokedUsers\n                .Select(user => (user, EventType.OrganizationUser_Revoked, (DateTime?)eventDate))\n                .ToList();\n            await eventService.LogOrganizationUserEventsAsync(revokeEvents);\n        }\n    }\n\n    private async Task SendPushNotificationsAsync(ICollection<OrganizationUser> revokedUsers)\n    {\n        var userIdsToNotify = revokedUsers\n            .Where(user => user.UserId.HasValue)\n            .Select(user => user.UserId!.Value)\n            .Distinct()\n            .ToList();\n\n        foreach (var userId in userIdsToNotify)\n        {\n            try\n            {\n                await pushNotificationService.PushSyncOrgKeysAsync(userId);\n            }\n            catch (Exception ex)\n            {\n                logger.LogWarning(ex, \"Failed to send push notification for user {UserId}.\", userId);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/RevokeOrganizationUsersRequest.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\npublic record RevokeOrganizationUsersRequest(\n    Guid OrganizationId,\n    ICollection<Guid> OrganizationUserIdsToRevoke,\n    IActingUser PerformedBy\n);\n\npublic record RevokeOrganizationUsersValidationRequest(\n    Guid OrganizationId,\n    ICollection<OrganizationUser> OrganizationUsersToRevoke,\n    IActingUser PerformedBy\n)\n{\n    public ICollection<Guid> OrganizationUserIdsToRevoke => OrganizationUsersToRevoke.Select(x => x.Id).ToArray();\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/RevokeOrganizationUsersValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\npublic class RevokeOrganizationUsersValidator(IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)\n    : IRevokeOrganizationUserValidator\n{\n    public async Task<ICollection<ValidationResult<OrganizationUser>>> ValidateAsync(\n        RevokeOrganizationUsersValidationRequest request)\n    {\n        var hasRemainingOwner = await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(request.OrganizationId,\n            request.OrganizationUsersToRevoke.Select(x => x.Id) // users excluded because they are going to be revoked\n            );\n\n        return request.OrganizationUsersToRevoke.Select(x =>\n        {\n            return x switch\n            {\n                _ when request.PerformedBy is not SystemUser\n                       && x.UserId is not null\n                       && x.UserId == request.PerformedBy.UserId =>\n                    Invalid(x, new CannotRevokeYourself()),\n                { Status: OrganizationUserStatusType.Revoked } =>\n                    Invalid(x, new UserAlreadyRevoked()),\n                { Type: OrganizationUserType.Owner } when !hasRemainingOwner =>\n                    Invalid(x, new MustHaveConfirmedOwner()),\n                { Type: OrganizationUserType.Owner } when request.PerformedBy is not SystemUser\n                                                        && !request.PerformedBy.IsOrganizationOwnerOrProvider =>\n                    Invalid(x, new OnlyOwnersCanRevokeOwners()),\n\n                _ => Valid(x)\n            };\n        }).ToList();\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;\n\npublic record OrganizationUserNotFound() : NotFoundError(\"Organization user not found.\");\npublic record NotEligibleForSelfRevoke() : BadRequestError(\"User is not eligible for self-revocation. The organization data ownership policy must be enabled and the user must be a confirmed member.\");\npublic record LastOwnerCannotSelfRevoke() : BadRequestError(\"The last owner cannot revoke themselves.\");\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/ISelfRevokeOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2.Results;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;\n\n/// <summary>\n/// Allows users to revoke themselves from an organization when declining to migrate personal items\n/// under the OrganizationDataOwnership policy.\n/// </summary>\npublic interface ISelfRevokeOrganizationUserCommand\n{\n    /// <summary>\n    /// Revokes a user from an organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization ID.</param>\n    /// <param name=\"userId\">The user ID to revoke.</param>\n    /// <returns>A <see cref=\"CommandResult\"/> indicating success or containing an error.</returns>\n    /// <remarks>\n    /// Validates the OrganizationDataOwnership policy is enabled and applies to the user (currently Owners/Admins are exempt),\n    /// the user is a confirmed member, and prevents the last owner from revoking themselves.\n    /// </remarks>\n    Task<CommandResult> SelfRevokeUserAsync(Guid organizationId, Guid userId);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/SelfRevokeOrganizationUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Utilities.v2.Results;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing OneOf.Types;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;\n\npublic class SelfRevokeOrganizationUserCommand(\n    IOrganizationUserRepository organizationUserRepository,\n    IPolicyRequirementQuery policyRequirementQuery,\n    IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,\n    IEventService eventService,\n    IPushNotificationService pushNotificationService)\n    : ISelfRevokeOrganizationUserCommand\n{\n    public async Task<CommandResult> SelfRevokeUserAsync(Guid organizationId, Guid userId)\n    {\n        var organizationUser = await organizationUserRepository.GetByOrganizationAsync(organizationId, userId);\n        if (organizationUser == null)\n        {\n            return new OrganizationUserNotFound();\n        }\n\n        var policyRequirement = await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId);\n\n        if (!policyRequirement.EligibleForSelfRevoke(organizationId))\n        {\n            return new NotEligibleForSelfRevoke();\n        }\n\n        // Prevent the last owner from revoking themselves, which would brick the organization\n        if (organizationUser.Type == OrganizationUserType.Owner)\n        {\n            var hasOtherOwner = await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(\n                organizationId,\n                [organizationUser.Id],\n                includeProvider: true);\n\n            if (!hasOtherOwner)\n            {\n                return new LastOwnerCannotSelfRevoke();\n            }\n        }\n\n        await organizationUserRepository.RevokeAsync(organizationUser.Id);\n        await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_SelfRevoked);\n        await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);\n\n        return new None();\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand\n{\n    private readonly IEventService _eventService;\n    private readonly IOrganizationService _organizationService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;\n    private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;\n    private readonly IPricingClient _pricingClient;\n\n    public UpdateOrganizationUserCommand(\n        IEventService eventService,\n        IOrganizationService organizationService,\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,\n        IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,\n        ICollectionRepository collectionRepository,\n        IGroupRepository groupRepository,\n        IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,\n        IPricingClient pricingClient)\n    {\n        _eventService = eventService;\n        _organizationService = organizationService;\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;\n        _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;\n        _collectionRepository = collectionRepository;\n        _groupRepository = groupRepository;\n        _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;\n        _pricingClient = pricingClient;\n    }\n\n    /// <summary>\n    /// Update an organization user.\n    /// </summary>\n    /// <param name=\"organizationUser\">The modified organization user to save.</param>\n    /// <param name=\"existingUserType\">The current type (member role) of the user.</param>\n    /// <param name=\"savingUserId\">The userId of the currently logged in user who is making the change.</param>\n    /// <param name=\"collectionAccess\">The user's updated collection access. If set to null, this removes all collection access.</param>\n    /// <param name=\"groupAccess\">The user's updated group access. If set to null, groups are not updated.</param>\n    /// <exception cref=\"BadRequestException\"></exception>\n    public async Task UpdateUserAsync(OrganizationUser organizationUser, OrganizationUserType existingUserType,\n        Guid? savingUserId,\n        List<CollectionAccessSelection>? collectionAccess, IEnumerable<Guid>? groupAccess)\n    {\n        // Avoid multiple enumeration\n        var collectionAccessList = collectionAccess?.ToList() ?? [];\n        groupAccess = groupAccess?.ToList();\n\n        if (organizationUser.Id.Equals(Guid.Empty))\n        {\n            throw new BadRequestException(\"Invite the user first.\");\n        }\n\n        var originalOrganizationUser = await _organizationUserRepository.GetByIdAsync(organizationUser.Id);\n        if (originalOrganizationUser == null || organizationUser.OrganizationId != originalOrganizationUser.OrganizationId)\n        {\n            throw new NotFoundException();\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await EnsureUserCannotBeAdminOrOwnerForMultipleFreeOrganizationAsync(organizationUser, existingUserType, organization);\n\n        if (collectionAccessList.Count != 0)\n        {\n            collectionAccessList = await ValidateAccessAndFilterDefaultUserCollectionsAsync(originalOrganizationUser, collectionAccessList);\n        }\n\n        if (groupAccess?.Any() == true)\n        {\n            await ValidateGroupAccessAsync(originalOrganizationUser, groupAccess.ToList());\n        }\n\n        if (savingUserId.HasValue)\n        {\n            await _organizationService.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, originalOrganizationUser.Type, organizationUser.GetPermissions());\n        }\n\n        await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(organizationUser.OrganizationId, organizationUser.Type);\n\n        if (organizationUser.Type != OrganizationUserType.Owner &&\n            !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId,\n                [organizationUser.Id]))\n        {\n            throw new BadRequestException(\"Organization must have at least one confirmed owner.\");\n        }\n\n        if (collectionAccessList.Count > 0)\n        {\n            var invalidAssociations = collectionAccessList.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));\n            if (invalidAssociations.Any())\n            {\n                throw new BadRequestException(\"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.\");\n            }\n        }\n\n        // Only autoscale (if required) after all validation has passed so that we know it's a valid request before\n        // updating Stripe\n        if (!originalOrganizationUser.AccessSecretsManager && organizationUser.AccessSecretsManager)\n        {\n            var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1);\n            if (additionalSmSeatsRequired > 0)\n            {\n                // TODO: https://bitwarden.atlassian.net/browse/PM-17012\n                var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n                var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)\n                    .AdjustSeats(additionalSmSeatsRequired);\n                await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);\n            }\n        }\n\n        await _organizationUserRepository.ReplaceAsync(organizationUser, collectionAccessList);\n\n        if (groupAccess != null)\n        {\n            await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupAccess);\n        }\n\n        await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Updated);\n    }\n\n    private async Task EnsureUserCannotBeAdminOrOwnerForMultipleFreeOrganizationAsync(OrganizationUser updatedOrgUser, OrganizationUserType existingUserType, Entities.Organization organization)\n    {\n\n        if (organization.PlanType != PlanType.Free)\n        {\n            return;\n        }\n        if (!updatedOrgUser.UserId.HasValue)\n        {\n            return;\n        }\n        if (updatedOrgUser.Type is not (OrganizationUserType.Admin or OrganizationUserType.Owner))\n        {\n            return;\n        }\n\n        // Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.\n        var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(updatedOrgUser.UserId!.Value);\n\n        var isCurrentAdminOrOwner = existingUserType is OrganizationUserType.Admin or OrganizationUserType.Owner;\n\n        if (isCurrentAdminOrOwner && adminCount <= 1)\n        {\n            return;\n        }\n\n        if (!isCurrentAdminOrOwner && adminCount == 0)\n        {\n            return;\n        }\n\n        throw new BadRequestException(\"User can only be an admin of one free organization.\");\n    }\n\n    private async Task<List<CollectionAccessSelection>> ValidateAccessAndFilterDefaultUserCollectionsAsync(\n        OrganizationUser originalUser, List<CollectionAccessSelection> collectionAccess)\n    {\n        var collections = await _collectionRepository\n            .GetManyByManyIdsAsync(collectionAccess.Select(c => c.Id));\n\n        ValidateCollections(originalUser, collectionAccess, collections);\n\n        return ExcludeDefaultUserCollections(collectionAccess, collections);\n    }\n\n    private static void ValidateCollections(OrganizationUser originalUser, List<CollectionAccessSelection> collectionAccess, ICollection<Collection> collections)\n    {\n        var collectionIds = collections.Select(c => c.Id);\n\n        var missingCollection = collectionAccess\n            .FirstOrDefault(cas => !collectionIds.Contains(cas.Id));\n        if (missingCollection != default)\n        {\n            throw new NotFoundException();\n        }\n\n        var invalidCollection = collections.FirstOrDefault(c => c.OrganizationId != originalUser.OrganizationId);\n        if (invalidCollection != default)\n        {\n            // Use generic error message to avoid enumeration\n            throw new NotFoundException();\n        }\n    }\n\n    private static List<CollectionAccessSelection> ExcludeDefaultUserCollections(\n        List<CollectionAccessSelection> collectionAccess, ICollection<Collection> collections) =>\n            collectionAccess\n                .Where(cas => collections.Any(c => c.Id == cas.Id && c.Type != CollectionType.DefaultUserCollection))\n                .ToList();\n\n    private async Task ValidateGroupAccessAsync(OrganizationUser originalUser,\n        ICollection<Guid> groupAccess)\n    {\n        var groups = await _groupRepository.GetManyByManyIds(groupAccess);\n        var groupIds = groups.Select(g => g.Id);\n\n        var missingGroupId = groupAccess.FirstOrDefault(gId => !groupIds.Contains(gId));\n        if (missingGroupId != default)\n        {\n            throw new NotFoundException();\n        }\n\n        var invalidGroup = groups.FirstOrDefault(g => g.OrganizationId != originalUser.OrganizationId);\n        if (invalidGroup != default)\n        {\n            // Use generic error message to avoid enumeration\n            throw new NotFoundException();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class UpdateOrganizationUserGroupsCommand : IUpdateOrganizationUserGroupsCommand\n{\n    private readonly IEventService _eventService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public UpdateOrganizationUserGroupsCommand(\n        IEventService eventService,\n        IOrganizationUserRepository organizationUserRepository)\n    {\n        _eventService = eventService;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds)\n    {\n        await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);\n        await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/BulkUpdateOrganizationSubscriptionsCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\nusing OrganizationSubscriptionUpdate = Bit.Core.AdminConsole.Models.Data.Organizations.OrganizationSubscriptionUpdate;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic class BulkUpdateOrganizationSubscriptionsCommand(\n    IStripePaymentService paymentService,\n    IOrganizationRepository repository,\n    TimeProvider timeProvider,\n    ILogger<BulkUpdateOrganizationSubscriptionsCommand> logger,\n    IFeatureService featureService,\n    IUpdateOrganizationSubscriptionCommand updateOrganizationSubscriptionCommand) : IBulkUpdateOrganizationSubscriptionsCommand\n{\n    public async Task BulkUpdateOrganizationSubscriptionsAsync(IEnumerable<OrganizationSubscriptionUpdate> subscriptionsToUpdate)\n    {\n        var successfulSyncs = new List<Guid>();\n        var useUpdateOrganizationSubscriptionCommand =\n            featureService.IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand);\n\n        foreach (var subscriptionUpdate in subscriptionsToUpdate)\n        {\n            if (useUpdateOrganizationSubscriptionCommand)\n            {\n                var changeSet = OrganizationSubscriptionChangeSet.UpdatePasswordManagerSeats(\n                    subscriptionUpdate.Plan!,\n                    subscriptionUpdate.Seats);\n\n                var result =\n                    await updateOrganizationSubscriptionCommand.Run(subscriptionUpdate.Organization, changeSet);\n\n                if (result.Success)\n                {\n                    successfulSyncs.Add(subscriptionUpdate.Organization.Id);\n                }\n                else\n                {\n                    logger.LogError(\"Failed to update organization {OrganizationId} subscription.\", subscriptionUpdate.Organization.Id);\n                }\n            }\n            else\n            {\n                try\n                {\n                    await paymentService.AdjustSeatsAsync(subscriptionUpdate.Organization,\n                        subscriptionUpdate.Plan,\n                        subscriptionUpdate.Seats);\n\n                    successfulSyncs.Add(subscriptionUpdate.Organization.Id);\n                }\n                catch (Exception ex)\n                {\n                    logger.LogError(ex,\n                        \"Failed to update organization {organizationId} subscription.\",\n                        subscriptionUpdate.Organization.Id);\n                }\n            }\n        }\n\n        if (successfulSyncs.Count == 0)\n        {\n            return;\n        }\n\n        await repository.UpdateSuccessfulOrganizationSyncStatusAsync(successfulSyncs, timeProvider.GetUtcNow().UtcDateTime);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic record SignUpOrganizationResponse(\n    Organization Organization,\n    OrganizationUser OrganizationUser);\n\npublic interface ICloudOrganizationSignUpCommand\n{\n    Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup);\n}\n\npublic class CloudOrganizationSignUpCommand(\n    IOrganizationUserRepository organizationUserRepository,\n    IOrganizationBillingService organizationBillingService,\n    IStripePaymentService paymentService,\n    IOrganizationRepository organizationRepository,\n    IOrganizationApiKeyRepository organizationApiKeyRepository,\n    IApplicationCacheService applicationCacheService,\n    IPushRegistrationService pushRegistrationService,\n    IPushNotificationService pushNotificationService,\n    ICollectionRepository collectionRepository,\n    IDeviceRepository deviceRepository,\n    IPricingClient pricingClient,\n    IPolicyRequirementQuery policyRequirementQuery,\n    IFeatureService featureService) : ICloudOrganizationSignUpCommand\n{\n    public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)\n    {\n        var plan = await pricingClient.GetPlanOrThrow(signup.Plan);\n\n        ValidatePasswordManagerPlan(plan, signup);\n\n        if (signup.UseSecretsManager)\n        {\n            if (signup.IsFromProvider)\n            {\n                throw new BadRequestException(\n                    \"Organizations with a Managed Service Provider do not support Secrets Manager.\");\n            }\n            ValidateSecretsManagerPlan(plan, signup);\n        }\n\n        if (!signup.IsFromProvider)\n        {\n            await ValidateSignUpPoliciesAsync(signup.Owner.Id);\n        }\n\n        var organization = new Organization\n        {\n            // Pre-generate the org id so that we can save it with the Stripe subscription\n            Id = CoreHelpers.GenerateComb(),\n            Name = signup.Name,\n            BillingEmail = signup.BillingEmail,\n            BusinessName = signup.BusinessName,\n            PlanType = plan!.Type,\n            Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats),\n            MaxCollections = plan.PasswordManager.MaxCollections,\n            MaxStorageGb = (short)(plan.PasswordManager.BaseStorageGb + signup.AdditionalStorageGb),\n            UsePolicies = plan.HasPolicies,\n            UseMyItems = plan.HasMyItems,\n            UseSso = plan.HasSso,\n            UseGroups = plan.HasGroups,\n            UseEvents = plan.HasEvents,\n            UseDirectory = plan.HasDirectory,\n            UseTotp = plan.HasTotp,\n            Use2fa = plan.Has2fa,\n            UseApi = plan.HasApi,\n            UseResetPassword = plan.HasResetPassword,\n            SelfHost = plan.HasSelfHost,\n            UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,\n            UseCustomPermissions = plan.HasCustomPermissions,\n            UseScim = plan.HasScim,\n            Plan = plan.Name,\n            Gateway = null,\n            ReferenceData = signup.Owner.ReferenceData,\n            Enabled = true,\n            LicenseKey = CoreHelpers.SecureRandomString(20),\n            PublicKey = signup.Keys?.PublicKey,\n            PrivateKey = signup.Keys?.WrappedPrivateKey,\n            CreationDate = DateTime.UtcNow,\n            RevisionDate = DateTime.UtcNow,\n            Status = OrganizationStatusType.Created,\n            UsePasswordManager = true,\n            UseSecretsManager = signup.UseSecretsManager,\n            UseOrganizationDomains = plan.HasOrganizationDomains,\n        };\n\n        if (signup.UseSecretsManager)\n        {\n            organization.SmSeats = plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats.GetValueOrDefault();\n            organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount +\n                                             signup.AdditionalServiceAccounts.GetValueOrDefault();\n        }\n\n        if (plan.Type == PlanType.Free && !signup.IsFromProvider)\n        {\n            var adminCount =\n                await organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);\n            if (adminCount > 0)\n            {\n                throw new BadRequestException(\"You can only be an admin of one free organization.\");\n            }\n        }\n        else if (plan.Type != PlanType.Free)\n        {\n            var sale = OrganizationSale.From(organization, signup);\n            await organizationBillingService.Finalize(sale);\n        }\n\n        var ownerId = signup.IsFromProvider ? default : signup.Owner.Id;\n        var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);\n        return new SignUpOrganizationResponse(returnValue.organization, returnValue.organizationUser);\n    }\n\n    public void ValidatePasswordManagerPlan(Plan plan, OrganizationUpgrade upgrade)\n    {\n        ValidatePlan(plan, upgrade.AdditionalSeats, \"Password Manager\");\n\n        if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0)\n        {\n            throw new BadRequestException($\"You do not have any Password Manager seats!\");\n        }\n\n        if (upgrade.AdditionalSeats < 0)\n        {\n            throw new BadRequestException($\"You can't subtract Password Manager seats!\");\n        }\n\n        if (!plan.PasswordManager.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)\n        {\n            throw new BadRequestException(\"Plan does not allow additional storage.\");\n        }\n\n        if (upgrade.AdditionalStorageGb < 0)\n        {\n            throw new BadRequestException(\"You can't subtract storage!\");\n        }\n\n        if (!plan.PasswordManager.HasPremiumAccessOption && upgrade.PremiumAccessAddon)\n        {\n            throw new BadRequestException(\"This plan does not allow you to buy the premium access addon.\");\n        }\n\n        if (!plan.PasswordManager.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)\n        {\n            throw new BadRequestException(\"Plan does not allow additional users.\");\n        }\n\n        if (plan.PasswordManager.HasAdditionalSeatsOption && plan.PasswordManager.MaxAdditionalSeats.HasValue &&\n            upgrade.AdditionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)\n        {\n            throw new BadRequestException($\"Selected plan allows a maximum of \" +\n                                          $\"{plan.PasswordManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.\");\n        }\n    }\n\n    public void ValidateSecretsManagerPlan(Plan plan, OrganizationUpgrade upgrade)\n    {\n        if (plan.SupportsSecretsManager == false)\n        {\n            throw new BadRequestException(\"Invalid Secrets Manager plan selected.\");\n        }\n\n        ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), \"Secrets Manager\");\n\n        if (plan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats <= 0)\n        {\n            throw new BadRequestException($\"You do not have any Secrets Manager seats!\");\n        }\n\n        if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)\n        {\n            throw new BadRequestException(\"Plan does not allow additional Machine Accounts.\");\n        }\n\n        if ((plan.ProductTier == ProductTierType.TeamsStarter &&\n            upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) ||\n            (plan.ProductTier != ProductTierType.TeamsStarter &&\n             upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats))\n        {\n            throw new BadRequestException(\"You cannot have more Secrets Manager seats than Password Manager seats.\");\n        }\n\n        if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)\n        {\n            throw new BadRequestException(\"You can't subtract Machine Accounts!\");\n        }\n\n        switch (plan.SecretsManager.HasAdditionalSeatsOption)\n        {\n            case false when upgrade.AdditionalSmSeats > 0:\n                throw new BadRequestException(\"Plan does not allow additional users.\");\n            case true when plan.SecretsManager.MaxAdditionalSeats.HasValue &&\n                           upgrade.AdditionalSmSeats > plan.SecretsManager.MaxAdditionalSeats.Value:\n                throw new BadRequestException($\"Selected plan allows a maximum of \" +\n                                              $\"{plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.\");\n        }\n    }\n\n    private static void ValidatePlan(Plan plan, int additionalSeats, string productType)\n    {\n        if (plan is null)\n        {\n            throw new BadRequestException($\"{productType} Plan was null.\");\n        }\n\n        if (plan.Disabled)\n        {\n            throw new BadRequestException($\"{productType} Plan not found.\");\n        }\n\n        if (additionalSeats < 0)\n        {\n            throw new BadRequestException($\"You can't subtract {productType} seats!\");\n        }\n    }\n\n    private async Task ValidateSignUpPoliciesAsync(Guid ownerId)\n    {\n        if (featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var requirement = await policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerId);\n\n            if (requirement.CannotCreateNewOrganization())\n            {\n                throw new BadRequestException(\"You may not create an organization. You belong to an organization \" +\n                                              \"which has a policy that prohibits you from being a member of any other organization.\");\n            }\n        }\n\n        var singleOrgRequirement = await policyRequirementQuery.GetAsync<SingleOrganizationPolicyRequirement>(ownerId);\n        var error = singleOrgRequirement.CanCreateOrganization();\n        if (error is not null)\n        {\n            throw new BadRequestException(error.Message);\n        }\n    }\n\n    private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization,\n    Guid ownerId, string ownerKey, string collectionName, bool withPayment)\n    {\n        try\n        {\n            await organizationRepository.CreateAsync(organization);\n            await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey\n            {\n                OrganizationId = organization.Id,\n                ApiKey = CoreHelpers.SecureRandomString(30),\n                Type = OrganizationApiKeyType.Default,\n                RevisionDate = DateTime.UtcNow,\n            });\n            await applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n\n            // ownerId == default if the org is created by a provider - in this case it's created without an\n            // owner and the first owner is immediately invited afterwards\n            OrganizationUser orgUser = null;\n            if (ownerId != default)\n            {\n                orgUser = new OrganizationUser\n                {\n                    OrganizationId = organization.Id,\n                    UserId = ownerId,\n                    Key = ownerKey,\n                    AccessSecretsManager = organization.UseSecretsManager,\n                    Type = OrganizationUserType.Owner,\n                    Status = OrganizationUserStatusType.Confirmed,\n                    CreationDate = organization.CreationDate,\n                    RevisionDate = organization.CreationDate\n                };\n                orgUser.SetNewId();\n\n                await organizationUserRepository.CreateAsync(orgUser);\n\n                var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);\n                await pushRegistrationService.AddUserRegistrationOrganizationAsync(devices, organization.Id.ToString());\n                await pushNotificationService.PushSyncOrgKeysAsync(ownerId);\n            }\n\n            Collection defaultCollection = null;\n            if (!string.IsNullOrWhiteSpace(collectionName))\n            {\n                defaultCollection = new Collection\n                {\n                    Name = collectionName,\n                    OrganizationId = organization.Id,\n                    CreationDate = organization.CreationDate,\n                    RevisionDate = organization.CreationDate\n                };\n\n                // Give the owner Can Manage access over the default collection\n                List<CollectionAccessSelection> defaultOwnerAccess = null;\n                if (orgUser != null)\n                {\n                    defaultOwnerAccess =\n                        [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];\n                }\n\n                await collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);\n            }\n\n            return (organization, orgUser, defaultCollection);\n        }\n        catch\n        {\n            if (withPayment)\n            {\n                await paymentService.CancelAndRecoverChargesAsync(organization);\n            }\n\n            if (organization.Id != default(Guid))\n            {\n                await organizationRepository.DeleteAsync(organization);\n                await applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);\n            }\n\n            throw;\n        }\n    }\n\n    private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)\n    {\n        var devices = await deviceRepository.GetManyByUserIdAsync(userId);\n        return devices\n            .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))\n            .Select(d => d.Id.ToString());\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic record InvalidTokenError() : BadRequestError(\"Invalid token.\");\npublic record OrganizationAlreadyEnabledError() : BadRequestError(\"Organization is already enabled.\");\npublic record OrganizationNotPendingError() : BadRequestError(\"Organization is not on a Pending status.\");\npublic record OrganizationHasKeysError() : BadRequestError(\"Organization already has encryption keys.\");\npublic record EmailMismatchError() : BadRequestError(\"User email does not match invite.\");\npublic record FreeOrgAdminLimitError() : BadRequestError(\"You can only be an admin of one free organization.\");\npublic record SingleOrgPolicyViolationError() : BadRequestError(\"You cannot join this organization because you are a member of another organization which forbids it.\");\npublic record TwoFactorRequiredError() : BadRequestError(\"You cannot join this organization until you enable two-step login on your user account.\");\npublic record OrganizationUserNotFoundError() : NotFoundError(\"User invalid.\");\npublic record OrganizationNotFoundError() : NotFoundError(\"Organization invalid.\");\npublic record OrganizationMismatchError() : BadRequestError(\"User does not belong to this organization.\");\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic class GetOrganizationSubscriptionsToUpdateQuery(IOrganizationRepository organizationRepository,\n    IPricingClient pricingClient) : IGetOrganizationSubscriptionsToUpdateQuery\n{\n    public async Task<IEnumerable<OrganizationSubscriptionUpdate>> GetOrganizationSubscriptionsToUpdateAsync()\n    {\n        var organizationsToUpdateTask = organizationRepository.GetOrganizationsForSubscriptionSyncAsync();\n        var plansTask = pricingClient.ListPlans();\n\n        await Task.WhenAll(organizationsToUpdateTask, plansTask);\n\n        return organizationsToUpdateTask.Result.Select(o => new OrganizationSubscriptionUpdate\n        {\n            Organization = o,\n            Plan = plansTask.Result.FirstOrDefault(plan => plan.Type == o.PlanType)\n        });\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Utilities.v2.Results;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing OneOf.Types;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic class InitPendingOrganizationCommand : IInitPendingOrganizationCommand\n{\n    private readonly IOrganizationService _organizationService;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IFeatureService _featureService;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IEventService _eventService;\n    private readonly IUserRepository _userRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IPushRegistrationService _pushRegistrationService;\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly IInitPendingOrganizationValidator _validator;\n    private readonly TimeProvider _timeProvider;\n    private readonly ISendOrganizationConfirmationCommand _sendOrganizationConfirmationCommand;\n\n    public InitPendingOrganizationCommand(\n            IOrganizationService organizationService,\n            ICollectionRepository collectionRepository,\n            IOrganizationRepository organizationRepository,\n            IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,\n            IOrganizationUserRepository organizationUserRepository,\n            IFeatureService featureService,\n            IPolicyRequirementQuery policyRequirementQuery,\n            IEventService eventService,\n            IUserRepository userRepository,\n            IPushNotificationService pushNotificationService,\n            IPushRegistrationService pushRegistrationService,\n            IDeviceRepository deviceRepository,\n            IInitPendingOrganizationValidator validator,\n            TimeProvider timeProvider,\n            ISendOrganizationConfirmationCommand sendOrganizationConfirmationCommand)\n    {\n        _organizationService = organizationService;\n        _collectionRepository = collectionRepository;\n        _organizationRepository = organizationRepository;\n        _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;\n        _organizationUserRepository = organizationUserRepository;\n        _featureService = featureService;\n        _policyRequirementQuery = policyRequirementQuery;\n        _eventService = eventService;\n        _userRepository = userRepository;\n        _pushNotificationService = pushNotificationService;\n        _pushRegistrationService = pushRegistrationService;\n        _deviceRepository = deviceRepository;\n        _validator = validator;\n        _timeProvider = timeProvider;\n        _sendOrganizationConfirmationCommand = sendOrganizationConfirmationCommand;\n    }\n\n    public async Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken)\n    {\n        await ValidateSignUpPoliciesAsync(user.Id);\n\n        var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);\n        if (orgUser == null)\n        {\n            throw new BadRequestException(\"User invalid.\");\n        }\n\n        var tokenValid = ValidateInviteToken(orgUser, user, emailToken);\n\n        if (!tokenValid)\n        {\n            throw new BadRequestException(\"Invalid token\");\n        }\n\n        var org = await _organizationRepository.GetByIdAsync(organizationId);\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization not found.\");\n        }\n\n        if (org.Enabled)\n        {\n            throw new BadRequestException(\"Organization is already enabled.\");\n        }\n\n        if (org.Status != OrganizationStatusType.Pending)\n        {\n            throw new BadRequestException(\"Organization is not on a Pending status.\");\n        }\n\n        if (!string.IsNullOrEmpty(org.PublicKey))\n        {\n            throw new BadRequestException(\"Organization already has a Public Key.\");\n        }\n\n        if (!string.IsNullOrEmpty(org.PrivateKey))\n        {\n            throw new BadRequestException(\"Organization already has a Private Key.\");\n        }\n\n        org.Enabled = true;\n        org.Status = OrganizationStatusType.Created;\n        org.PublicKey = publicKey;\n        org.PrivateKey = privateKey;\n\n        await _organizationService.UpdateAsync(org);\n\n        if (!string.IsNullOrWhiteSpace(collectionName))\n        {\n            // give the owner Can Manage access over the default collection\n            List<CollectionAccessSelection> defaultOwnerAccess =\n                [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];\n\n            var defaultCollection = new Collection\n            {\n                Name = collectionName,\n                OrganizationId = org.Id\n            };\n            await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);\n        }\n    }\n\n    private async Task ValidateSignUpPoliciesAsync(Guid ownerId)\n    {\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var requirement = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerId);\n\n            if (requirement.CannotCreateNewOrganization())\n            {\n                throw new BadRequestException(\"You may not create an organization. You belong to an organization \" +\n                                              \"which has a policy that prohibits you from being a member of any other organization.\");\n            }\n        }\n\n        var singleOrgRequirement = await _policyRequirementQuery.GetAsync<SingleOrganizationPolicyRequirement>(ownerId);\n        var error = singleOrgRequirement.CanCreateOrganization();\n        if (error is not null)\n        {\n            throw new BadRequestException(error.Message);\n        }\n    }\n\n    private bool ValidateInviteToken(OrganizationUser orgUser, User user, string emailToken)\n    {\n        var tokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(\n            _orgUserInviteTokenDataFactory, emailToken, orgUser);\n\n        return tokenValid;\n    }\n\n    public async Task<CommandResult> InitPendingOrganizationVNextAsync(InitPendingOrganizationRequest request)\n    {\n        var orgUser = await _organizationUserRepository.GetByIdAsync(request.OrganizationUserId);\n        if (orgUser is null)\n        {\n            return new OrganizationUserNotFoundError();\n        }\n\n        var org = await _organizationRepository.GetByIdAsync(request.OrganizationId);\n        if (org is null)\n        {\n            return new OrganizationNotFoundError();\n        }\n\n        var validationRequest = new InitPendingOrganizationValidationRequest\n        {\n            User = request.User,\n            OrganizationId = request.OrganizationId,\n            OrganizationUserId = request.OrganizationUserId,\n            OrganizationKeys = request.OrganizationKeys,\n            CollectionName = request.CollectionName,\n            EmailToken = request.EmailToken,\n            EncryptedOrganizationSymmetricKey = request.EncryptedOrganizationSymmetricKey,\n            Organization = org,\n            OrganizationUser = orgUser,\n        };\n\n        var validationResult = await _validator.ValidateAsync(validationRequest);\n        if (validationResult.IsError)\n        {\n            return validationResult.AsError;\n        }\n\n        PrepareOrganizationForInitialization(org, request);\n        PrepareOrganizationUserForConfirmation(orgUser, request);\n\n        var confirmOwnerAction = _organizationUserRepository.BuildConfirmOwnerAction(orgUser);\n        await _organizationRepository.InitializeOrganizationAsync(org, confirmOwnerAction);\n\n        await VerifyUserEmailAsync(request.User);\n        await CreateDefaultCollectionAsync(orgUser, request);\n\n        await SendNotificationsAsync(org, orgUser, request.User);\n\n        return new None();\n    }\n\n    private void PrepareOrganizationForInitialization(Organization org, InitPendingOrganizationRequest request)\n    {\n        org.Enabled = true;\n        org.Status = OrganizationStatusType.Created;\n        org.PublicKey = request.OrganizationKeys.PublicKey;\n        org.PrivateKey = request.OrganizationKeys.WrappedPrivateKey;\n        org.RevisionDate = _timeProvider.GetUtcNow().UtcDateTime;\n    }\n\n    private static void PrepareOrganizationUserForConfirmation(OrganizationUser orgUser, InitPendingOrganizationRequest request)\n    {\n        orgUser.Status = OrganizationUserStatusType.Confirmed;\n        orgUser.UserId = request.User.Id;\n        orgUser.Key = request.EncryptedOrganizationSymmetricKey;\n        orgUser.Email = null;\n    }\n\n    private async Task VerifyUserEmailAsync(User user)\n    {\n        if (!user.EmailVerified)\n        {\n            user.EmailVerified = true;\n            await _userRepository.ReplaceAsync(user);\n        }\n    }\n\n    private async Task CreateDefaultCollectionAsync(OrganizationUser orgUser, InitPendingOrganizationRequest request)\n    {\n        if (string.IsNullOrWhiteSpace(request.CollectionName))\n        {\n            return;\n        }\n\n        List<CollectionAccessSelection> defaultOwnerAccess =\n        [\n            new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }\n        ];\n\n        var defaultCollection = new Collection\n        {\n            Name = request.CollectionName,\n            OrganizationId = request.OrganizationId\n        };\n\n        await _collectionRepository.CreateAsync(\n            obj: defaultCollection,\n            groups: null,\n            users: defaultOwnerAccess);\n    }\n\n    private async Task SendNotificationsAsync(Organization org, OrganizationUser orgUser, User user)\n    {\n        await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);\n\n        await _sendOrganizationConfirmationCommand.SendConfirmationAsync(org, user.Email, orgUser.AccessSecretsManager);\n\n        await _pushNotificationService.PushSyncOrgKeysAsync(user.Id);\n\n        var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);\n        var deviceIds = devices\n            .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))\n            .Select(d => d.Id.ToString());\n        await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds, org.Id.ToString());\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationRequest.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\n/// <summary>\n/// Request model for initializing a pending organization.\n/// </summary>\npublic record InitPendingOrganizationRequest\n{\n    /// <summary>\n    /// The user who is accepting the organization invitation.\n    /// </summary>\n    public required User User { get; init; }\n\n    /// <summary>\n    /// The ID of the organization to initialize.\n    /// </summary>\n    public required Guid OrganizationId { get; init; }\n\n    /// <summary>\n    /// The ID of the organization user record.\n    /// </summary>\n    public required Guid OrganizationUserId { get; init; }\n\n    /// <summary>\n    /// The organization's encryption key pair (public key and wrapped private key).\n    /// </summary>\n    public required PublicKeyEncryptionKeyPairData OrganizationKeys { get; init; }\n\n    /// <summary>\n    /// The name of the default collection to create. Optional - if null or empty, no collection is created.\n    /// </summary>\n    public string? CollectionName { get; init; }\n\n    /// <summary>\n    /// The email token for validating the invitation.\n    /// </summary>\n    public required string EmailToken { get; init; }\n\n    /// <summary>\n    /// The Organization symmetric key encrypted with the User's public key.\n    /// </summary>\n    public required string EncryptedOrganizationSymmetricKey { get; init; }\n}\n\n/// <summary>\n/// Enriched validation request that includes fetched entities so the validator doesn't\n/// need to perform its own data access.\n/// </summary>\npublic record InitPendingOrganizationValidationRequest : InitPendingOrganizationRequest\n{\n    /// <summary>\n    /// The organization entity fetched from the database.\n    /// </summary>\n    public required Organization Organization { get; init; }\n\n    /// <summary>\n    /// The organization user entity fetched from the database.\n    /// </summary>\n    public required OrganizationUser OrganizationUser { get; init; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\nusing Error = Bit.Core.AdminConsole.Utilities.v2.Error;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\npublic interface IInitPendingOrganizationValidator\n{\n    /// <summary>\n    /// Validates all preconditions for initializing a pending organization.\n    /// </summary>\n    Task<ValidationResult<InitPendingOrganizationValidationRequest>> ValidateAsync(\n        InitPendingOrganizationValidationRequest request);\n}\n\npublic class InitPendingOrganizationValidator : IInitPendingOrganizationValidator\n{\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;\n    private readonly IFeatureService _featureService;\n    private readonly IPolicyService _policyService;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public InitPendingOrganizationValidator(\n        IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,\n        IFeatureService featureService,\n        IPolicyService policyService,\n        IPolicyRequirementQuery policyRequirementQuery,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IOrganizationUserRepository organizationUserRepository)\n    {\n        _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;\n        _featureService = featureService;\n        _policyService = policyService;\n        _policyRequirementQuery = policyRequirementQuery;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    public async Task<ValidationResult<InitPendingOrganizationValidationRequest>> ValidateAsync(\n        InitPendingOrganizationValidationRequest request)\n    {\n        if (!ValidateInviteToken(request.OrganizationUser, request.User, request.EmailToken))\n        {\n            return Invalid(request, new InvalidTokenError());\n        }\n\n        var emailError = ValidateUserEmail(request.OrganizationUser, request.User);\n        if (emailError != null)\n        {\n            return Invalid(request, emailError);\n        }\n\n        var matchError = ValidateOrganizationMatch(request.OrganizationUser, request.OrganizationId);\n        if (matchError != null)\n        {\n            return Invalid(request, matchError);\n        }\n\n        var stateError = ValidateOrganizationState(request.Organization);\n        if (stateError != null)\n        {\n            return Invalid(request, stateError);\n        }\n\n        var policyError = await ValidatePoliciesAsync(request.User, request.OrganizationId);\n        if (policyError != null)\n        {\n            return Invalid(request, policyError);\n        }\n\n        var limitError = await ValidateFreeOrganizationLimitAsync(\n            request.User, request.Organization, request.OrganizationUser);\n        if (limitError != null)\n        {\n            return Invalid(request, limitError);\n        }\n\n        return Valid(request);\n    }\n\n    private bool ValidateInviteToken(OrganizationUser orgUser, User user, string emailToken)\n    {\n        return OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(\n            _orgUserInviteTokenDataFactory, emailToken, orgUser);\n    }\n\n    private static Error? ValidateUserEmail(OrganizationUser orgUser, User user)\n    {\n        if (string.IsNullOrWhiteSpace(orgUser.Email) ||\n            !orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))\n        {\n            return new EmailMismatchError();\n        }\n\n        return null;\n    }\n\n    private static Error? ValidateOrganizationState(Organization org)\n    {\n        if (org.Enabled)\n        {\n            return new OrganizationAlreadyEnabledError();\n        }\n\n        if (org.Status != OrganizationStatusType.Pending)\n        {\n            return new OrganizationNotPendingError();\n        }\n\n        if (!string.IsNullOrEmpty(org.PublicKey) || !string.IsNullOrEmpty(org.PrivateKey))\n        {\n            return new OrganizationHasKeysError();\n        }\n\n        return null;\n    }\n\n    private static Error? ValidateOrganizationMatch(OrganizationUser orgUser, Guid organizationId)\n    {\n        if (orgUser.OrganizationId != organizationId)\n        {\n            return new OrganizationMismatchError();\n        }\n\n        return null;\n    }\n\n    private async Task<Error?> ValidatePoliciesAsync(User user, Guid organizationId)\n    {\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var autoConfirmReq = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id);\n            if (autoConfirmReq.CannotCreateNewOrganization())\n            {\n                return new SingleOrgPolicyViolationError();\n            }\n        }\n\n        var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);\n        if (anySingleOrgPolicies)\n        {\n            return new SingleOrgPolicyViolationError();\n        }\n\n        var twoFactorReq = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);\n        if (twoFactorReq.IsTwoFactorRequiredForOrganization(organizationId) &&\n            !await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))\n        {\n            return new TwoFactorRequiredError();\n        }\n\n        return null;\n    }\n\n    private async Task<Error?> ValidateFreeOrganizationLimitAsync(User user, Organization org, OrganizationUser orgUser)\n    {\n        if (org.PlanType == PlanType.Free &&\n            (orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin))\n        {\n            var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);\n            if (adminCount > 0)\n            {\n                return new FreeOrgAdminLimitError();\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IBulkUpdateOrganizationSubscriptionsCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Organizations;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\n\npublic interface IBulkUpdateOrganizationSubscriptionsCommand\n{\n    /// <summary>\n    /// Attempts to update the subscription of all organizations that have had a subscription update.\n    ///\n    /// If successful, the Organization.SyncSeats flag will be set to false and Organization.RevisionDate will be set.\n    ///\n    /// In the event of a failure, it will log the failure and maybe be picked up in later runs.\n    /// </summary>\n    /// <param name=\"subscriptionsToUpdate\">The collection of organization subscriptions to update.</param>\n    Task BulkUpdateOrganizationSubscriptionsAsync(IEnumerable<OrganizationSubscriptionUpdate> subscriptionsToUpdate);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IGetOrganizationSubscriptionsToUpdateQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Organizations;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\n\npublic interface IGetOrganizationSubscriptionsToUpdateQuery\n{\n    /// <summary>\n    /// Retrieves a collection of organization subscriptions that need to be updated. This is based on if the\n    /// Organization.SyncSeats flag is true and Organization.Seats has a value.\n    /// </summary>\n    /// <returns>\n    /// A collection of <see cref=\"OrganizationSubscriptionUpdate\"/> instances, each representing an organization\n    /// subscription to be updated with their associated plan.\n    /// </returns>\n    Task<IEnumerable<OrganizationSubscriptionUpdate>> GetOrganizationSubscriptionsToUpdateAsync();\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.Utilities.v2.Results;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\n\npublic interface IInitPendingOrganizationCommand\n{\n    /// <summary>\n    /// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.\n    /// </summary>\n    /// <remarks>\n    /// This method must target a disabled Organization that has null keys and status as 'Pending'.\n    /// </remarks>\n    [Obsolete(\"Use InitPendingOrganizationVNextAsync for consolidated flow with upfront validation. This method will be removed.\")]\n    Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken);\n\n    /// <summary>\n    /// Initializes a pending organization created via the Bitwarden Portal on behalf of a Reseller.\n    /// See <see cref=\"ResellerClientOrganizationSignUpCommand\"/>.\n    /// It also confirms the first owner.\n    /// </summary>\n    /// <remarks>\n    /// The user initializing the organization is the first user to access it - there is no existing \n    /// owner or provider who can change its settings. Therefore, validation in this command assumes \n    /// a default state. For example, it does not enforce policies for this organization because none \n    /// will be enabled yet.\n    /// </remarks>\n    /// <returns>A CommandResult indicating success or specific validation errors.</returns>\n    Task<CommandResult> InitPendingOrganizationVNextAsync(InitPendingOrganizationRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDeleteCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\n\npublic interface IOrganizationDeleteCommand\n{\n    /// <summary>\n    /// Permanently deletes an organization and performs necessary cleanup.\n    /// </summary>\n    /// <param name=\"organization\">The organization to delete.</param>\n    /// <exception cref=\"BadRequestException\">Thrown when the organization cannot be deleted due to configuration constraints.</exception>\n    Task DeleteAsync(Organization organization);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDisableCommand.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\n\n/// <summary>\n/// Command interface for disabling organizations.\n/// </summary>\npublic interface IOrganizationDisableCommand\n{\n    /// <summary>\n    /// Disables an organization with an optional expiration date.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization to disable.</param>\n    /// <param name=\"expirationDate\">Optional date when the disable status should expire.</param>\n    Task DisableAsync(Guid organizationId, DateTime? expirationDate);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationEnableCommand.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\n\npublic interface IOrganizationEnableCommand\n{\n    /// <summary>\n    /// Enables an organization that is currently disabled and has a gateway configured.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization to enable.</param>\n    /// <param name=\"expirationDate\">When provided, sets the date the organization's subscription will expire. If not provided, no expiration date will be set.</param>\n    Task EnableAsync(Guid organizationId, DateTime? expirationDate = null);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationInitiateDeleteCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\n\npublic interface IOrganizationInitiateDeleteCommand\n{\n    /// <summary>\n    /// Initiates a secure deletion process for an organization by requesting confirmation from an organization admin.\n    /// </summary>\n    /// <param name=\"organization\">The organization to be deleted.</param>\n    /// <param name=\"orgAdminEmail\">The email address of the organization admin who will confirm the deletion.</param>\n    /// <exception cref=\"BadRequestException\">Thrown when the specified admin email is invalid or lacks sufficient permissions.</exception>\n    Task InitiateDeleteAsync(Organization organization, string orgAdminEmail);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationUpdateCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\n\npublic interface IOrganizationUpdateCommand\n{\n    /// <summary>\n    /// Updates an organization's information in the Bitwarden database and Stripe (if required).\n    /// Also optionally updates an organization's public-private keypair if it was not created with one.\n    /// On self-host, only the public-private keys will be updated because all other properties are fixed by the license file.\n    /// </summary>\n    /// <param name=\"request\">The update request containing the details to be updated.</param>\n    Task<Organization> UpdateAsync(OrganizationUpdateRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationUpdateKeysCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\n\npublic interface IOrganizationUpdateKeysCommand\n{\n    /// <summary>\n    /// Update the keys for an organization.\n    /// </summary>\n    /// <param name=\"orgId\">The ID of the organization to update.</param>\n    /// <param name=\"publicKey\">The public key for the organization.</param>\n    /// <param name=\"privateKey\">The private key for the organization.</param>\n    /// <returns>The updated organization.</returns>\n    Task<Organization> UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/ISelfHostedOrganizationSignUpCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\n\npublic interface ISelfHostedOrganizationSignUpCommand\n{\n    /// <summary>\n    /// Create a new organization on a self-hosted instance\n    /// </summary>\n    Task<(Organization organization, OrganizationUser? organizationUser)> SignUpAsync(\n        OrganizationLicense license, User owner, string ownerKey,\n        string? collectionName, string publicKey, string privateKey);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic class OrganizationDeleteCommand : IOrganizationDeleteCommand\n{\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n\n    public OrganizationDeleteCommand(\n        IApplicationCacheService applicationCacheService,\n        IOrganizationRepository organizationRepository,\n        IStripePaymentService paymentService,\n        ISsoConfigRepository ssoConfigRepository)\n    {\n        _applicationCacheService = applicationCacheService;\n        _organizationRepository = organizationRepository;\n        _paymentService = paymentService;\n        _ssoConfigRepository = ssoConfigRepository;\n    }\n\n    public async Task DeleteAsync(Organization organization)\n    {\n        await ValidateDeleteOrganizationAsync(organization);\n\n        if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))\n        {\n            try\n            {\n                var eop = !organization.ExpirationDate.HasValue ||\n                          organization.ExpirationDate.Value >= DateTime.UtcNow;\n                await _paymentService.CancelSubscriptionAsync(organization, eop);\n            }\n            catch (GatewayException) { }\n        }\n\n        await _organizationRepository.DeleteAsync(organization);\n        await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);\n    }\n\n    private async Task ValidateDeleteOrganizationAsync(Organization organization)\n    {\n        var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);\n        if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector)\n        {\n            throw new BadRequestException(\"You cannot delete an Organization that is using Key Connector.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic class OrganizationDisableCommand : IOrganizationDisableCommand\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IApplicationCacheService _applicationCacheService;\n\n    public OrganizationDisableCommand(\n        IOrganizationRepository organizationRepository,\n        IApplicationCacheService applicationCacheService)\n    {\n        _organizationRepository = organizationRepository;\n        _applicationCacheService = applicationCacheService;\n    }\n\n    public async Task DisableAsync(Guid organizationId, DateTime? expirationDate)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n        if (organization is { Enabled: true })\n        {\n            organization.Enabled = false;\n            organization.ExpirationDate = expirationDate;\n            organization.RevisionDate = DateTime.UtcNow;\n\n            await _organizationRepository.ReplaceAsync(organization);\n            await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic class OrganizationEnableCommand : IOrganizationEnableCommand\n{\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IOrganizationRepository _organizationRepository;\n\n    public OrganizationEnableCommand(\n        IApplicationCacheService applicationCacheService,\n        IOrganizationRepository organizationRepository)\n    {\n        _applicationCacheService = applicationCacheService;\n        _organizationRepository = organizationRepository;\n    }\n\n    public async Task EnableAsync(Guid organizationId, DateTime? expirationDate = null)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n        if (organization is null || organization.Enabled || expirationDate is not null && organization.Gateway is null)\n        {\n            return;\n        }\n\n        organization.Enabled = true;\n\n        if (expirationDate is not null && organization.Gateway is not null)\n        {\n            organization.ExpirationDate = expirationDate;\n            organization.RevisionDate = DateTime.UtcNow;\n        }\n\n        await _organizationRepository.ReplaceAsync(organization);\n        await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic static class OrganizationExtensions\n{\n    /// <summary>\n    /// Updates the organization public and private keys if provided and not already set.\n    /// This is legacy code for old organizations that were not created with a public/private keypair.\n    /// It is a soft migration that will silently migrate organizations when they perform certain actions,\n    /// e.g. change their details or upgrade their plan.\n    /// </summary>\n    public static void BackfillPublicPrivateKeys(this Organization organization, PublicKeyEncryptionKeyPairData? keyPair)\n    {\n        // Only backfill if both new keys are provided and both old keys are missing.\n        if (string.IsNullOrWhiteSpace(keyPair?.PublicKey) ||\n            string.IsNullOrWhiteSpace(keyPair.WrappedPrivateKey) ||\n            !string.IsNullOrWhiteSpace(organization.PublicKey) ||\n            !string.IsNullOrWhiteSpace(organization.PrivateKey))\n        {\n            return;\n        }\n\n        organization.PublicKey = keyPair.PublicKey;\n        organization.PrivateKey = keyPair.WrappedPrivateKey;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business.Tokenables;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic class OrganizationInitiateDeleteCommand : IOrganizationInitiateDeleteCommand\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IUserRepository _userRepository;\n    private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;\n    private readonly IMailService _mailService;\n\n    public const string OrganizationAdminNotFoundErrorMessage = \"Org admin not found.\";\n\n    public OrganizationInitiateDeleteCommand(\n        IOrganizationUserRepository organizationUserRepository,\n        IUserRepository userRepository,\n        IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,\n        IMailService mailService)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _userRepository = userRepository;\n        _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;\n        _mailService = mailService;\n    }\n\n    public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail)\n    {\n        var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail);\n        if (orgAdmin == null)\n        {\n            throw new BadRequestException(OrganizationAdminNotFoundErrorMessage);\n        }\n        var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id);\n        if (orgAdminOrgUser == null || orgAdminOrgUser.Status is not OrganizationUserStatusType.Confirmed ||\n            (orgAdminOrgUser.Type is not OrganizationUserType.Admin and not OrganizationUserType.Owner))\n        {\n            throw new BadRequestException(OrganizationAdminNotFoundErrorMessage);\n        }\n        var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1));\n        await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\npublic class OrganizationUpdateKeysCommand : IOrganizationUpdateKeysCommand\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationService _organizationService;\n\n    public const string OrganizationKeysAlreadyExistErrorMessage = \"Organization Keys already exist.\";\n\n    public OrganizationUpdateKeysCommand(\n        ICurrentContext currentContext,\n        IOrganizationRepository organizationRepository,\n        IOrganizationService organizationService)\n    {\n        _currentContext = currentContext;\n        _organizationRepository = organizationRepository;\n        _organizationService = organizationService;\n    }\n\n    public async Task<Organization> UpdateOrganizationKeysAsync(Guid organizationId, string publicKey, string privateKey)\n    {\n        if (!await _currentContext.ManageResetPassword(organizationId))\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        // If the keys already exist, error out\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n        if (organization.PublicKey != null && organization.PrivateKey != null)\n        {\n            throw new BadRequestException(OrganizationKeysAlreadyExistErrorMessage);\n        }\n\n        // Update org with generated public/private key\n        organization.PublicKey = publicKey;\n        organization.PrivateKey = privateKey;\n\n        await _organizationService.UpdateAsync(organization);\n\n        return organization;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic record ProviderClientOrganizationSignUpResponse(\n    Organization Organization,\n    Collection DefaultCollection);\n\npublic interface IProviderClientOrganizationSignUpCommand\n{\n    /// <summary>\n    /// Sign up a new client organization for a provider.\n    /// </summary>\n    /// <param name=\"signup\">The signup information.</param>\n    /// <returns>A tuple containing the new organization and its default collection.</returns>\n    Task<ProviderClientOrganizationSignUpResponse> SignUpClientOrganizationAsync(OrganizationSignup signup);\n}\n\npublic class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizationSignUpCommand\n{\n    public const string PlanNullErrorMessage = \"Password Manager Plan was null.\";\n    public const string PlanDisabledErrorMessage = \"Password Manager Plan is disabled.\";\n    public const string AdditionalSeatsNegativeErrorMessage = \"You can't subtract Password Manager seats!\";\n\n    private readonly ICurrentContext _currentContext;\n    private readonly IPricingClient _pricingClient;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly ICollectionRepository _collectionRepository;\n\n    public ProviderClientOrganizationSignUpCommand(\n        ICurrentContext currentContext,\n        IPricingClient pricingClient,\n        IOrganizationRepository organizationRepository,\n        IOrganizationApiKeyRepository organizationApiKeyRepository,\n        IApplicationCacheService applicationCacheService,\n        ICollectionRepository collectionRepository)\n    {\n        _currentContext = currentContext;\n        _pricingClient = pricingClient;\n        _organizationRepository = organizationRepository;\n        _organizationApiKeyRepository = organizationApiKeyRepository;\n        _applicationCacheService = applicationCacheService;\n        _collectionRepository = collectionRepository;\n    }\n\n    public async Task<ProviderClientOrganizationSignUpResponse> SignUpClientOrganizationAsync(OrganizationSignup signup)\n    {\n        var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);\n\n        ValidatePlan(plan, signup.AdditionalSeats);\n\n        var organization = new Organization\n        {\n            // Pre-generate the org id so that we can save it with the Stripe subscription.\n            Id = CoreHelpers.GenerateComb(),\n            Name = signup.Name,\n            BillingEmail = signup.BillingEmail,\n            PlanType = plan!.Type,\n            Seats = signup.AdditionalSeats,\n            MaxCollections = plan.PasswordManager.MaxCollections,\n            MaxStorageGb = plan.PasswordManager.BaseStorageGb,\n            UsePolicies = plan.HasPolicies,\n            UseMyItems = plan.HasMyItems,\n            UseSso = plan.HasSso,\n            UseOrganizationDomains = plan.HasOrganizationDomains,\n            UseGroups = plan.HasGroups,\n            UseEvents = plan.HasEvents,\n            UseDirectory = plan.HasDirectory,\n            UseTotp = plan.HasTotp,\n            Use2fa = plan.Has2fa,\n            UseApi = plan.HasApi,\n            UseResetPassword = plan.HasResetPassword,\n            SelfHost = plan.HasSelfHost,\n            UsersGetPremium = plan.UsersGetPremium,\n            UseCustomPermissions = plan.HasCustomPermissions,\n            UseScim = plan.HasScim,\n            Plan = plan.Name,\n            Gateway = GatewayType.Stripe,\n            ReferenceData = signup.Owner.ReferenceData,\n            Enabled = true,\n            LicenseKey = CoreHelpers.SecureRandomString(20),\n            PublicKey = signup.Keys?.PublicKey,\n            PrivateKey = signup.Keys?.WrappedPrivateKey,\n            CreationDate = DateTime.UtcNow,\n            RevisionDate = DateTime.UtcNow,\n            Status = OrganizationStatusType.Created,\n            UsePasswordManager = true,\n            // Secrets Manager not available for purchase with Consolidated Billing.\n            UseSecretsManager = false,\n        };\n\n        var returnValue = await SignUpAsync(organization, signup.CollectionName);\n\n        return returnValue;\n    }\n\n    private static void ValidatePlan(Plan plan, int additionalSeats)\n    {\n        if (plan is null)\n        {\n            throw new BadRequestException(PlanNullErrorMessage);\n        }\n\n        if (plan.Disabled)\n        {\n            throw new BadRequestException(PlanDisabledErrorMessage);\n        }\n\n        if (additionalSeats < 0)\n        {\n            throw new BadRequestException(AdditionalSeatsNegativeErrorMessage);\n        }\n    }\n\n    /// <summary>\n    /// Private helper method to create a new organization.\n    /// </summary>\n    private async Task<ProviderClientOrganizationSignUpResponse> SignUpAsync(\n        Organization organization, string collectionName)\n    {\n        try\n        {\n            await _organizationRepository.CreateAsync(organization);\n            await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey\n            {\n                OrganizationId = organization.Id,\n                ApiKey = CoreHelpers.SecureRandomString(30),\n                Type = OrganizationApiKeyType.Default,\n                RevisionDate = DateTime.UtcNow,\n            });\n            await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n\n            Collection defaultCollection = null;\n            if (!string.IsNullOrWhiteSpace(collectionName))\n            {\n                defaultCollection = new Collection\n                {\n                    Name = collectionName,\n                    OrganizationId = organization.Id,\n                    CreationDate = organization.CreationDate,\n                    RevisionDate = organization.CreationDate\n                };\n\n                await _collectionRepository.CreateAsync(defaultCollection, null, null);\n            }\n\n            return new ProviderClientOrganizationSignUpResponse(organization, defaultCollection);\n        }\n        catch\n        {\n            if (organization.Id != default)\n            {\n                await _organizationRepository.DeleteAsync(organization);\n                await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);\n            }\n\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic record ResellerClientOrganizationSignUpResponse(\n    Organization Organization,\n    OrganizationUser OwnerOrganizationUser);\n\n/// <summary>\n/// Command for signing up reseller client organizations in a pending state.\n/// </summary>\npublic interface IResellerClientOrganizationSignUpCommand\n{\n    /// <summary>\n    /// Sign up a reseller client organization. The organization will be created in a pending state \n    /// (disabled and with Pending status) and the owner will be invited via email. The organization \n    /// will become active once the owner accepts the invitation.\n    /// </summary>\n    /// <param name=\"organization\">The organization to create.</param>\n    /// <param name=\"ownerEmail\">The email of the organization owner who will be invited.</param>\n    /// <returns>A response containing the created pending organization and invited owner user.</returns>\n    Task<ResellerClientOrganizationSignUpResponse> SignUpResellerClientAsync(\n        Organization organization,\n        string ownerEmail);\n}\n\npublic class ResellerClientOrganizationSignUpCommand : IResellerClientOrganizationSignUpCommand\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IEventService _eventService;\n    private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;\n    private readonly IStripePaymentService _paymentService;\n\n    public ResellerClientOrganizationSignUpCommand(\n        IOrganizationRepository organizationRepository,\n        IOrganizationApiKeyRepository organizationApiKeyRepository,\n        IApplicationCacheService applicationCacheService,\n        IOrganizationUserRepository organizationUserRepository,\n        IEventService eventService,\n        ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,\n        IStripePaymentService paymentService)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationApiKeyRepository = organizationApiKeyRepository;\n        _applicationCacheService = applicationCacheService;\n        _organizationUserRepository = organizationUserRepository;\n        _eventService = eventService;\n        _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;\n        _paymentService = paymentService;\n    }\n\n    public async Task<ResellerClientOrganizationSignUpResponse> SignUpResellerClientAsync(\n        Organization organization,\n        string ownerEmail)\n    {\n        try\n        {\n            var createdOrganization = await CreateOrganizationAsync(organization);\n            var ownerOrganizationUser = await CreateAndInviteOwnerAsync(createdOrganization, ownerEmail);\n\n            await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited);\n\n            return new ResellerClientOrganizationSignUpResponse(organization, ownerOrganizationUser);\n        }\n        catch\n        {\n            await _paymentService.CancelAndRecoverChargesAsync(organization);\n\n            if (organization.Id != default)\n            {\n                // Deletes the organization and all related data, including its owner user\n                await _organizationRepository.DeleteAsync(organization);\n                await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);\n            }\n\n            throw;\n        }\n    }\n\n    private async Task<Organization> CreateOrganizationAsync(Organization organization)\n    {\n        organization.Id = CoreHelpers.GenerateComb();\n        organization.Enabled = false;\n        organization.Status = OrganizationStatusType.Pending;\n\n        await _organizationRepository.CreateAsync(organization);\n        await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey\n        {\n            OrganizationId = organization.Id,\n            ApiKey = CoreHelpers.SecureRandomString(30),\n            Type = OrganizationApiKeyType.Default,\n            RevisionDate = DateTime.UtcNow,\n        });\n        await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n\n        return organization;\n    }\n\n    private async Task<OrganizationUser> CreateAndInviteOwnerAsync(Organization organization, string ownerEmail)\n    {\n        var ownerOrganizationUser = new OrganizationUser\n        {\n            OrganizationId = organization.Id,\n            UserId = null,\n            Email = ownerEmail,\n            Key = null,\n            Type = OrganizationUserType.Owner,\n            Status = OrganizationUserStatusType.Invited,\n        };\n\n        await _organizationUserRepository.CreateAsync(ownerOrganizationUser);\n\n        await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(\n            users: [ownerOrganizationUser],\n            organization: organization,\n            initOrganization: true));\n\n        return ownerOrganizationUser;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommand.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\n\npublic class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUpCommand\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IPushRegistrationService _pushRegistrationService;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly ILicensingService _licensingService;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IStripePaymentService _paymentService;\n    private readonly IFeatureService _featureService;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n\n    public SelfHostedOrganizationSignUpCommand(\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IOrganizationApiKeyRepository organizationApiKeyRepository,\n        IApplicationCacheService applicationCacheService,\n        ICollectionRepository collectionRepository,\n        IPushRegistrationService pushRegistrationService,\n        IPushNotificationService pushNotificationService,\n        IDeviceRepository deviceRepository,\n        ILicensingService licensingService,\n        IGlobalSettings globalSettings,\n        IStripePaymentService paymentService,\n        IFeatureService featureService,\n        IPolicyRequirementQuery policyRequirementQuery)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationApiKeyRepository = organizationApiKeyRepository;\n        _applicationCacheService = applicationCacheService;\n        _collectionRepository = collectionRepository;\n        _pushRegistrationService = pushRegistrationService;\n        _pushNotificationService = pushNotificationService;\n        _deviceRepository = deviceRepository;\n        _licensingService = licensingService;\n        _globalSettings = globalSettings;\n        _paymentService = paymentService;\n        _featureService = featureService;\n        _policyRequirementQuery = policyRequirementQuery;\n    }\n\n    public async Task<(Organization organization, OrganizationUser? organizationUser)> SignUpAsync(\n        OrganizationLicense license, User owner, string ownerKey, string? collectionName, string publicKey,\n        string privateKey)\n    {\n        if (license.LicenseType != LicenseType.Organization)\n        {\n            throw new BadRequestException(\"Premium licenses cannot be applied to an organization. \" +\n                                          \"Upload this license from your personal account settings page.\");\n        }\n\n        var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);\n        var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception);\n\n        if (!canUse)\n        {\n            throw new BadRequestException(exception);\n        }\n\n        var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();\n        if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey)))\n        {\n            throw new BadRequestException(\"License is already in use by another organization.\");\n        }\n\n        await ValidateSignUpPoliciesAsync(owner.Id);\n\n        var organization = claimsPrincipal != null\n            // If the ClaimsPrincipal exists (there's a token on the license), use it to build the organization.\n            ? OrganizationFactory.Create(owner, claimsPrincipal, publicKey, privateKey)\n            // If there's no ClaimsPrincipal (there's no token on the license), use the license to build the organization.\n            : OrganizationFactory.Create(owner, license, publicKey, privateKey);\n\n        var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);\n\n        var dir = $\"{_globalSettings.LicenseDirectory}/organization\";\n        Directory.CreateDirectory(dir);\n        await using var fs = new FileStream(Path.Combine(dir, $\"{organization.Id}.json\"), FileMode.Create);\n        await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);\n        return (result.organization, result.organizationUser);\n    }\n\n    private async Task ValidateSignUpPoliciesAsync(Guid ownerId)\n    {\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var requirement = await _policyRequirementQuery.GetAsync<AutomaticUserConfirmationPolicyRequirement>(ownerId);\n\n            if (requirement.CannotCreateNewOrganization())\n            {\n                throw new BadRequestException(\"You may not create an organization. You belong to an organization \" +\n                                              \"which has a policy that prohibits you from being a member of any other organization.\");\n            }\n        }\n\n        var singleOrgRequirement = await _policyRequirementQuery.GetAsync<SingleOrganizationPolicyRequirement>(ownerId);\n        var error = singleOrgRequirement.CanCreateOrganization();\n        if (error is not null)\n        {\n            throw new BadRequestException(error.Message);\n        }\n    }\n\n    /// <summary>\n    /// Private helper method to create a new organization.\n    /// This is common code used by both the cloud and self-hosted methods.\n    /// </summary>\n    private async Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)>\n        SignUpAsync(Organization organization,\n            Guid ownerId, string ownerKey, string? collectionName, bool withPayment)\n    {\n        try\n        {\n            await _organizationRepository.CreateAsync(organization);\n            await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey\n            {\n                OrganizationId = organization.Id,\n                ApiKey = CoreHelpers.SecureRandomString(30),\n                Type = OrganizationApiKeyType.Default,\n                RevisionDate = DateTime.UtcNow,\n            });\n            await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n\n            // ownerId == default if the org is created by a provider - in this case it's created without an\n            // owner and the first owner is immediately invited afterwards\n            OrganizationUser? orgUser = null;\n            if (ownerId != default)\n            {\n                orgUser = new OrganizationUser\n                {\n                    OrganizationId = organization.Id,\n                    UserId = ownerId,\n                    Key = ownerKey,\n                    AccessSecretsManager = organization.UseSecretsManager,\n                    Type = OrganizationUserType.Owner,\n                    Status = OrganizationUserStatusType.Confirmed,\n                    CreationDate = organization.CreationDate,\n                    RevisionDate = organization.CreationDate\n                };\n                orgUser.SetNewId();\n\n                await _organizationUserRepository.CreateAsync(orgUser);\n\n                var devices = await GetUserDeviceIdsAsync(orgUser.UserId!.Value);\n                await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices,\n                    organization.Id.ToString());\n                await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);\n            }\n\n            Collection? defaultCollection = null;\n            if (!string.IsNullOrWhiteSpace(collectionName))\n            {\n                defaultCollection = new Collection\n                {\n                    Name = collectionName,\n                    OrganizationId = organization.Id,\n                    CreationDate = organization.CreationDate,\n                    RevisionDate = organization.CreationDate\n                };\n\n                // Give the owner Can Manage access over the default collection\n                List<CollectionAccessSelection>? defaultOwnerAccess = null;\n                if (orgUser != null)\n                {\n                    defaultOwnerAccess =\n                    [\n                        new CollectionAccessSelection\n                        {\n                            Id = orgUser.Id,\n                            HidePasswords = false,\n                            ReadOnly = false,\n                            Manage = true\n                        }\n                    ];\n                }\n\n                await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);\n            }\n\n            return (organization, orgUser, defaultCollection);\n        }\n        catch\n        {\n            if (withPayment)\n            {\n                await _paymentService.CancelAndRecoverChargesAsync(organization);\n            }\n\n            if (organization.Id != default(Guid))\n            {\n                await _organizationRepository.DeleteAsync(organization);\n                await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);\n            }\n\n            throw;\n        }\n    }\n\n    private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)\n    {\n        var devices = await _deviceRepository.GetManyByUserIdAsync(userId);\n        return devices\n            .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))\n            .Select(d => d.Id.ToString());\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;\n\npublic class OrganizationUpdateCommand(\n    IOrganizationService organizationService,\n    IOrganizationRepository organizationRepository,\n    IGlobalSettings globalSettings,\n    IOrganizationBillingService organizationBillingService\n) : IOrganizationUpdateCommand\n{\n    public async Task<Organization> UpdateAsync(OrganizationUpdateRequest request)\n    {\n        var organization = await organizationRepository.GetByIdAsync(request.OrganizationId);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (globalSettings.SelfHosted)\n        {\n            return await UpdateSelfHostedAsync(organization, request);\n        }\n\n        return await UpdateCloudAsync(organization, request);\n    }\n\n    private async Task<Organization> UpdateCloudAsync(Organization organization, OrganizationUpdateRequest request)\n    {\n        // Store original values for comparison\n        var originalName = organization.Name;\n        var originalBillingEmail = organization.BillingEmail;\n\n        // Apply updates to organization\n        // These values may or may not be sent by the client depending on the operation being performed.\n        // Skip any values not provided.\n        if (request.Name is not null)\n        {\n            organization.Name = request.Name;\n        }\n\n        if (request.BillingEmail is not null)\n        {\n            organization.BillingEmail = request.BillingEmail.ToLowerInvariant().Trim();\n        }\n\n        organization.BackfillPublicPrivateKeys(request.Keys);\n\n        await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated);\n\n        // Update billing information in Stripe if required\n        await UpdateBillingAsync(organization, originalName, originalBillingEmail);\n\n        return organization;\n    }\n\n    /// <summary>\n    /// Self-host cannot update the organization details because they are set by the license file.\n    /// However, this command does offer a soft migration pathway for organizations without public and private keys.\n    /// If we remove this migration code in the future, this command and endpoint can become cloud only.\n    /// </summary>\n    private async Task<Organization> UpdateSelfHostedAsync(Organization organization, OrganizationUpdateRequest request)\n    {\n        organization.BackfillPublicPrivateKeys(request.Keys);\n        await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated);\n        return organization;\n    }\n\n    private async Task UpdateBillingAsync(Organization organization, string originalName, string? originalBillingEmail)\n    {\n        // Update Stripe if name or billing email changed\n        var shouldUpdateBilling = originalName != organization.Name ||\n                                  originalBillingEmail != organization.BillingEmail;\n\n        if (!shouldUpdateBilling)\n        {\n            return;\n        }\n\n        await organizationBillingService.UpdateOrganizationNameAndEmail(organization);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;\n\n/// <summary>\n/// Request model for updating the name, billing email, and/or public-private keys for an organization (legacy migration code).\n/// Any combination of these properties can be updated, so they are optional. If none are specified it will not update anything.\n/// </summary>\npublic record OrganizationUpdateRequest\n{\n    /// <summary>\n    /// The ID of the organization to update.\n    /// </summary>\n    public required Guid OrganizationId { get; init; }\n\n    /// <summary>\n    /// The new organization name to apply (optional, this is skipped if not provided).\n    /// </summary>\n    public string? Name { get; init; }\n\n    /// <summary>\n    /// The new billing email address to apply (optional, this is skipped if not provided).\n    /// </summary>\n    public string? BillingEmail { get; init; }\n\n    /// <summary>\n    /// The organization's public/private key pair to set (optional, only set if not already present on the organization).\n    /// </summary>\n    public PublicKeyEncryptionKeyPairData? Keys { get; init; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities.v2;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\n\npublic class AutomaticUserConfirmationOrganizationPolicyComplianceValidator(\n    IOrganizationUserRepository organizationUserRepository,\n    IProviderUserRepository providerUserRepository)\n    : IAutomaticUserConfirmationOrganizationPolicyComplianceValidator\n{\n    public async Task<ValidationResult<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>>\n        IsOrganizationCompliantAsync(AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest request)\n    {\n        var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(request.OrganizationId);\n\n        if (await ValidateUserComplianceWithSingleOrgAsync(request, organizationUsers) is { } singleOrgNonCompliant)\n        {\n            return Invalid(request, singleOrgNonCompliant);\n        }\n\n        if (await ValidateNoProviderUsersAsync(organizationUsers) is { } orgHasProviderMember)\n        {\n            return Invalid(request, orgHasProviderMember);\n        }\n\n        return Valid(request);\n    }\n\n    private async Task<Error?> ValidateUserComplianceWithSingleOrgAsync(\n        AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest request,\n        ICollection<OrganizationUserUserDetails> organizationUsers)\n    {\n        var userIds = organizationUsers\n            .Where(u => u.UserId is not null && u.Status != OrganizationUserStatusType.Invited)\n            .Select(u => u.UserId!.Value);\n\n        var hasNonCompliantUser = (await organizationUserRepository.GetManyByManyUsersAsync(userIds))\n            .Any(uo => uo.OrganizationId != request.OrganizationId\n                       && uo.Status != OrganizationUserStatusType.Invited);\n\n        return hasNonCompliantUser ? new UserNotCompliantWithSingleOrganization() : null;\n    }\n\n    private async Task<Error?> ValidateNoProviderUsersAsync(ICollection<OrganizationUserUserDetails> organizationUsers)\n    {\n        var userIds = organizationUsers.Where(x => x.UserId is not null)\n            .Select(x => x.UserId!.Value);\n\n        return (await providerUserRepository.GetManyByManyUsersAsync(userIds)).Count != 0\n            ? new ProviderExistsInOrganization()\n            : null;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\n\npublic record AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(Guid OrganizationId);\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementRequest.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\n\n/// <summary>\n/// Request object for <see cref=\"AutomaticUserConfirmationPolicyEnforcementValidator\"/>\n/// </summary>\npublic record AutomaticUserConfirmationPolicyEnforcementRequest\n{\n    /// <summary>\n    /// Organization to be validated\n    /// </summary>\n    public Guid OrganizationId { get; }\n\n    /// <summary>\n    /// All organization users that match the provided user.\n    /// </summary>\n    public ICollection<OrganizationUser> AllOrganizationUsers { get; }\n\n    /// <summary>\n    /// User associated with the organization user to be confirmed\n    /// </summary>\n    public User User { get; }\n\n    /// <summary>\n    /// Request object for <see cref=\"AutomaticUserConfirmationPolicyEnforcementValidator\"/>.\n    /// </summary>\n    /// <remarks>\n    /// This record is used to encapsulate the data required for handling the automatic confirmation policy enforcement.\n    /// </remarks>\n    /// <param name=\"organizationId\">The organization to be validated.</param>\n    /// <param name=\"organizationUsers\">All organization users that match the provided user.</param>\n    /// <param name=\"user\">The user entity connecting all org users provided.</param>\n    public AutomaticUserConfirmationPolicyEnforcementRequest(\n        Guid organizationId,\n        IEnumerable<OrganizationUser> organizationUsers,\n        User user)\n    {\n        OrganizationId = organizationId;\n        AllOrganizationUsers = organizationUsers.ToArray();\n        User = user;\n    }\n}\n\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\n\npublic class AutomaticUserConfirmationPolicyEnforcementValidator(\n    IPolicyRequirementQuery policyRequirementQuery,\n    IProviderUserRepository providerUserRepository)\n    : IAutomaticUserConfirmationPolicyEnforcementValidator\n{\n    public async Task<ValidationResult<AutomaticUserConfirmationPolicyEnforcementRequest>> IsCompliantAsync(\n        AutomaticUserConfirmationPolicyEnforcementRequest request)\n    {\n        var automaticUserConfirmationPolicyRequirement = await policyRequirementQuery\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(request.User.Id);\n\n        return await IsCompliantAsync(request, automaticUserConfirmationPolicyRequirement);\n    }\n\n    public async Task<ValidationResult<AutomaticUserConfirmationPolicyEnforcementRequest>> IsCompliantAsync(\n        AutomaticUserConfirmationPolicyEnforcementRequest request,\n        AutomaticUserConfirmationPolicyRequirement policyRequirement)\n    {\n        var currentOrganizationUser = request.AllOrganizationUsers\n            .FirstOrDefault(x => x.OrganizationId == request.OrganizationId\n                                 // invited users do not have a userId but will have email\n                                 && (x.UserId == request.User.Id || x.Email == request.User.Email));\n\n        if (currentOrganizationUser is null)\n        {\n            return Invalid(request, new CurrentOrganizationUserIsNotPresentInRequest());\n        }\n\n        if (policyRequirement.IsEnabled(request.OrganizationId))\n        {\n            if ((await providerUserRepository.GetManyByUserAsync(request.User.Id)).Count != 0)\n            {\n                return Invalid(request, new ProviderUsersCannotJoin());\n            }\n\n            if (request.AllOrganizationUsers.Count > 1)\n            {\n                return Invalid(request, new UserCannotBelongToAnotherOrganization());\n            }\n        }\n\n        if (policyRequirement.IsEnabledForOrganizationsOtherThan(currentOrganizationUser.OrganizationId))\n        {\n            return Invalid(request, new OtherOrganizationDoesNotAllowOtherMembership());\n        }\n\n        return Valid(request);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/Errors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\n\npublic record UserNotCompliantWithSingleOrganization() : BadRequestError(\"All organization users must be compliant with the Single organization policy before enabling the Automatically confirm invited users policy. Please remove users who are members of multiple organizations.\");\n\npublic record ProviderExistsInOrganization() : BadRequestError(\"The organization has users with the Provider user type. Please remove provider users before enabling the Automatically confirm invited users policy.\");\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/IAutomaticUserConfirmationOrganizationPolicyComplianceValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2.Validation;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\n\n/// <summary>\n/// Validates that an organization meets the prerequisites for enabling the Automatic User Confirmation policy.\n/// </summary>\n/// <remarks>\n/// The following conditions must be met:\n/// <list type=\"bullet\">\n///     <item>All non-invited organization users belong only to this organization (Single Organization compliance)</item>\n///     <item>No organization users are provider members</item>\n/// </list>\n/// </remarks>\npublic interface IAutomaticUserConfirmationOrganizationPolicyComplianceValidator\n{\n    /// <summary>\n    /// Checks whether the organization is compliant with the Automatic User Confirmation policy prerequisites.\n    /// </summary>\n    /// <param name=\"request\">The request containing the organization ID to validate.</param>\n    /// <returns>\n    /// A <see cref=\"ValidationResult{TRequest}\"/> that is valid if the organization is compliant,\n    /// or contains a <see cref=\"UserNotCompliantWithSingleOrganization\"/> or <see cref=\"ProviderExistsInOrganization\"/>\n    /// error if validation fails.\n    /// </returns>\n    Task<ValidationResult<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>>\n        IsOrganizationCompliantAsync(AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest request);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/IAutomaticUserConfirmationPolicyEnforcementValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\n\n/// <summary>\n/// Used to enforce the Automatic User Confirmation policy. It uses the <see cref=\"IPolicyRequirementQuery\"/> to retrieve\n/// the <see cref=\"AutomaticUserConfirmationPolicyRequirement\"/>. It is used to check to make sure the given user is\n/// valid for the Automatic User Confirmation policy. It also validates that the given user is not a provider\n/// or a member of another organization regardless of status or type.\n/// </summary>\npublic interface IAutomaticUserConfirmationPolicyEnforcementValidator\n{\n\n    /// <summary>\n    /// Checks if the given user is compliant with the Automatic User Confirmation policy.\n    ///\n    /// To be compliant, a user must\n    /// - not be a member of a provider\n    /// - not be a member of another organization\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <remarks>\n    /// This uses the validation result pattern to avoid throwing exceptions.\n    /// </remarks>\n    /// <returns>A validation result with the error message if applicable.</returns>\n    Task<ValidationResult<AutomaticUserConfirmationPolicyEnforcementRequest>> IsCompliantAsync(AutomaticUserConfirmationPolicyEnforcementRequest request);\n\n    /// <summary>\n    /// Checks if the given user is compliant with the Automatic User Confirmation policy, using the passed-in\n    /// <see cref=\"AutomaticUserConfirmationPolicyRequirement\"/> as the source of truth for the validation request.\n    ///\n    /// To be compliant, a user must\n    /// - not be a member of a provider\n    /// - not be a member of another organization\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <remarks>\n    /// This uses the validation result pattern to avoid throwing exceptions.\n    /// </remarks>\n    /// <returns>A validation result with the error message if applicable.</returns>\n    Task<ValidationResult<AutomaticUserConfirmationPolicyEnforcementRequest>> IsCompliantAsync(\n        AutomaticUserConfirmationPolicyEnforcementRequest request,\n        AutomaticUserConfirmationPolicyRequirement policyRequirement);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;\n\npublic interface IPolicyQuery\n{\n    /// <summary>\n    /// Retrieves a summary view of an organization's usage of a policy specified by the <paramref name=\"policyType\"/>.\n    /// </summary>\n    /// <remarks>\n    /// This query is the entrypoint for consumers interested in understanding how a particular <see cref=\"PolicyType\"/>\n    /// has been applied to an organization; the resultant <see cref=\"PolicyStatus\"/> is not indicative of explicit\n    /// policy configuration. \n    /// </remarks>\n    Task<PolicyStatus> RunAsync(Guid organizationId, PolicyType policyType);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;\n\npublic interface IPolicyRequirementQuery\n{\n    /// <summary>\n    /// Get a policy requirement for a specific user.\n    /// The policy requirement represents how one or more policy types should be enforced against the user.\n    /// It will always return a value even if there are no policies that should be enforced.\n    /// This should be used for all policy checks.\n    /// </summary>\n    /// <param name=\"userId\">The user that you need to enforce the policy against.</param>\n    /// <typeparam name=\"T\">The IPolicyRequirement that corresponds to the policy you want to enforce.</typeparam>\n    Task<T> GetAsync<T>(Guid userId) where T : IPolicyRequirement;\n\n    /// <summary>\n    /// Get a policy requirement for a specific user using the optimized single-user query.\n    /// The policy requirement represents how one or more policy types should be enforced against the user.\n    /// It will always return a value even if there are no policies that should be enforced.\n    /// This is the vNext version that uses the optimized GetPolicyDetailsByUserIdAndPolicyTypeAsync method.\n    /// </summary>\n    /// <param name=\"userId\">The user that you need to enforce the policy against.</param>\n    /// <typeparam name=\"T\">The IPolicyRequirement that corresponds to the policy you want to enforce.</typeparam>\n    Task<T> GetAsyncVNext<T>(Guid userId) where T : IPolicyRequirement;\n\n    /// <summary>\n    /// Get a policy requirement for a list of users.\n    /// The policy requirement represents how one or more policy types should be enforced against the users.\n    /// </summary>\n    /// <returns>\n    /// A collection of tuples pairing each user ID with their corresponding policy requirement.\n    /// </returns>\n    /// <param name=\"userIds\">The users that you need to enforce the policy against.</param>\n    /// <typeparam name=\"T\">The IPolicyRequirement that corresponds to the policy you want to enforce.</typeparam>\n    Task<IEnumerable<(Guid UserId, T Requirement)>> GetAsync<T>(IEnumerable<Guid> userIds) where T : IPolicyRequirement;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyValidator.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;\n\n/// <summary>\n/// Defines behavior and functionality for a given PolicyType.\n/// </summary>\n/// <remarks>\n/// All methods defined in this interface are for the PolicyService#SavePolicy method. This needs to be supported until\n/// we successfully refactor policy validators over to policy validation handlers\n/// </remarks>\npublic interface IPolicyValidator\n{\n    /// <summary>\n    /// The PolicyType that this definition relates to.\n    /// </summary>\n    public PolicyType Type { get; }\n\n    /// <summary>\n    /// PolicyTypes that must be enabled before this policy can be enabled, if any.\n    /// These dependencies will be checked when this policy is enabled and when any required policy is disabled.\n    /// </summary>\n    public IEnumerable<PolicyType> RequiredPolicies { get; }\n\n    /// <summary>\n    /// Validates a policy before saving it.\n    /// Do not use this for simple dependencies between different policies - see <see cref=\"RequiredPolicies\"/> instead.\n    /// Implementation is optional; by default it will not perform any validation.\n    /// </summary>\n    /// <param name=\"policyUpdate\">The policy update request</param>\n    /// <param name=\"currentPolicy\">The current policy, if any</param>\n    /// <returns>A validation error if validation was unsuccessful, otherwise an empty string</returns>\n    public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy);\n\n    /// <summary>\n    /// Performs side effects after a policy is validated but before it is saved.\n    /// For example, this can be used to remove non-compliant users from the organization.\n    /// Implementation is optional; by default it will not perform any side effects.\n    /// </summary>\n    /// <param name=\"policyUpdate\">The policy update request</param>\n    /// <param name=\"currentPolicy\">The current policy, if any</param>\n    public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/IPostSavePolicySideEffect.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;\n\npublic interface IPostSavePolicySideEffect\n{\n    public Task ExecuteSideEffectsAsync(SavePolicyModel policyRequest, Policy postUpdatedPolicy,\n        Policy? previousPolicyState);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;\n\npublic interface ISavePolicyCommand\n{\n    Task<Policy> SaveAsync(PolicyUpdate policy);\n\n    /// <summary>\n    /// FIXME: this is a first pass at implementing side effects after the policy has been saved, which was not supported by the validator pattern.\n    /// However, this needs to be implemented in a policy-agnostic way rather than building out switch statements in the command itself.\n    /// </summary>\n    Task<Policy> VNextSaveAsync(SavePolicyModel policyRequest);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;\n\npublic class PolicyQuery(IPolicyRepository policyRepository) : IPolicyQuery\n{\n    public async Task<PolicyStatus> RunAsync(Guid organizationId, PolicyType policyType)\n    {\n        var dbPolicy = await policyRepository.GetByOrganizationIdTypeAsync(organizationId, policyType);\n        return new PolicyStatus(organizationId, policyType, dbPolicy);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;\n\npublic class PolicyRequirementQuery(\n    IPolicyRepository policyRepository,\n    IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories)\n    : IPolicyRequirementQuery\n{\n    public async Task<T> GetAsync<T>(Guid userId) where T : IPolicyRequirement\n        => (await GetAsync<T>([userId])).Single().Requirement;\n\n    public async Task<T> GetAsyncVNext<T>(Guid userId) where T : IPolicyRequirement\n    {\n        var factory = factories.OfType<IPolicyRequirementFactory<T>>().SingleOrDefault();\n        if (factory is null)\n        {\n            throw new NotImplementedException(\"No Requirement Factory found for \" + typeof(T));\n        }\n\n        var policyDetails = await policyRepository.GetPolicyDetailsByUserIdAndPolicyTypeAsync(userId, factory.PolicyType);\n        var enforcedPolicyDetails = policyDetails.Where(factory.Enforce);\n\n        return factory.Create(enforcedPolicyDetails);\n    }\n\n    public async Task<IEnumerable<(Guid UserId, T Requirement)>> GetAsync<T>(IEnumerable<Guid> userIds) where T : IPolicyRequirement\n    {\n        var factory = factories.OfType<IPolicyRequirementFactory<T>>().SingleOrDefault();\n        if (factory is null)\n        {\n            throw new NotImplementedException(\"No Requirement Factory found for \" + typeof(T));\n        }\n\n        var userIdList = userIds.ToList();\n\n        var policyDetailsByUser = (await GetPolicyDetails(userIdList, factory.PolicyType))\n            .Where(factory.Enforce)\n            .ToLookup(l => l.UserId);\n\n        var policyRequirements = userIdList.Select(u => (u, factory.Create(policyDetailsByUser[u])));\n\n        return policyRequirements;\n    }\n\n    private async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetails(IEnumerable<Guid> userIds, PolicyType policyType)\n        => await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(userIds, policyType);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;\n\npublic class SavePolicyCommand : ISavePolicyCommand\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IEventService _eventService;\n    private readonly IPolicyRepository _policyRepository;\n    private readonly IReadOnlyDictionary<PolicyType, IPolicyValidator> _policyValidators;\n    private readonly TimeProvider _timeProvider;\n    private readonly IPostSavePolicySideEffect _postSavePolicySideEffect;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    public SavePolicyCommand(\n        IOrganizationRepository organizationRepository,\n        IEventService eventService,\n        IPolicyRepository policyRepository,\n        IEnumerable<IPolicyValidator> policyValidators,\n        TimeProvider timeProvider,\n        IPostSavePolicySideEffect postSavePolicySideEffect,\n        IPushNotificationService pushNotificationService)\n    {\n        _organizationRepository = organizationRepository;\n        _eventService = eventService;\n        _policyRepository = policyRepository;\n        _timeProvider = timeProvider;\n        _postSavePolicySideEffect = postSavePolicySideEffect;\n        _pushNotificationService = pushNotificationService;\n\n        var policyValidatorsDict = new Dictionary<PolicyType, IPolicyValidator>();\n        foreach (var policyValidator in policyValidators)\n        {\n            if (!policyValidatorsDict.TryAdd(policyValidator.Type, policyValidator))\n            {\n                throw new Exception($\"Duplicate PolicyValidator for {policyValidator.Type} policy.\");\n            }\n        }\n\n        _policyValidators = policyValidatorsDict;\n    }\n\n    public async Task<Policy> SaveAsync(PolicyUpdate policyUpdate)\n    {\n        var org = await _organizationRepository.GetByIdAsync(policyUpdate.OrganizationId);\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization not found\");\n        }\n\n        if (!org.UsePolicies)\n        {\n            throw new BadRequestException(\"This organization cannot use policies.\");\n        }\n\n        if (_policyValidators.TryGetValue(policyUpdate.Type, out var validator))\n        {\n            await RunValidatorAsync(validator, policyUpdate);\n        }\n\n        var policy = await _policyRepository.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)\n                     ?? new Policy\n                     {\n                         OrganizationId = policyUpdate.OrganizationId,\n                         Type = policyUpdate.Type,\n                         CreationDate = _timeProvider.GetUtcNow().UtcDateTime\n                     };\n\n        policy.Enabled = policyUpdate.Enabled;\n        policy.Data = policyUpdate.Data;\n        policy.RevisionDate = _timeProvider.GetUtcNow().UtcDateTime;\n\n        await _policyRepository.UpsertAsync(policy);\n        await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);\n\n        await PushPolicyUpdateToClients(policy.OrganizationId, policy);\n\n        return policy;\n    }\n\n    public async Task<Policy> VNextSaveAsync(SavePolicyModel policyRequest)\n    {\n        var (_, currentPolicy) = await GetCurrentPolicyStateAsync(policyRequest.PolicyUpdate);\n\n        var policy = await SaveAsync(policyRequest.PolicyUpdate);\n\n        await ExecutePostPolicySaveSideEffectsForSupportedPoliciesAsync(policyRequest, policy, currentPolicy);\n\n        return policy;\n    }\n\n    private async Task ExecutePostPolicySaveSideEffectsForSupportedPoliciesAsync(SavePolicyModel policyRequest, Policy postUpdatedPolicy, Policy? previousPolicyState)\n    {\n        if (postUpdatedPolicy.Type == PolicyType.OrganizationDataOwnership)\n        {\n            await _postSavePolicySideEffect.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n        }\n    }\n\n    private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate)\n    {\n        var (savedPoliciesDict, currentPolicy) = await GetCurrentPolicyStateAsync(policyUpdate);\n\n        // If enabling this policy - check that all policy requirements are satisfied\n        if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled)\n        {\n            var missingRequiredPolicyTypes = validator.RequiredPolicies\n                .Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true })\n                .ToList();\n\n            if (missingRequiredPolicyTypes.Count != 0)\n            {\n                throw new BadRequestException($\"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {validator.Type.GetName()} policy.\");\n            }\n        }\n\n        // If disabling this policy - ensure it's not required by any other policy\n        if (currentPolicy is { Enabled: true } && !policyUpdate.Enabled)\n        {\n            var dependentPolicyTypes = _policyValidators.Values\n                .Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type))\n                .Select(otherValidator => otherValidator.Type)\n                .Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) &&\n                                          savedPolicy.Enabled)\n                .ToList();\n\n            switch (dependentPolicyTypes)\n            {\n                case { Count: 1 }:\n                    throw new BadRequestException($\"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {validator.Type.GetName()} policy.\");\n                case { Count: > 1 }:\n                    throw new BadRequestException($\"Turn off all of the policies that require the {validator.Type.GetName()} policy.\");\n            }\n        }\n\n        // Run other validation\n        var validationError = await validator.ValidateAsync(policyUpdate, currentPolicy);\n        if (!string.IsNullOrEmpty(validationError))\n        {\n            throw new BadRequestException(validationError);\n        }\n\n        // Run side effects\n        await validator.OnSaveSideEffectsAsync(policyUpdate, currentPolicy);\n    }\n\n    private async Task<(Dictionary<PolicyType, Policy> savedPoliciesDict, Policy? currentPolicy)> GetCurrentPolicyStateAsync(PolicyUpdate policyUpdate)\n    {\n        var savedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId);\n        // Note: policies may be missing from this dict if they have never been enabled\n        var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type);\n        var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type);\n        return (savedPoliciesDict, currentPolicy);\n    }\n\n    Task PushPolicyUpdateToClients(Guid organizationId, Policy policy) => this._pushNotificationService.PushAsync(new PushNotification<SyncPolicyPushNotification>\n    {\n        Type = PushType.PolicyChanged,\n        Target = NotificationTarget.Organization,\n        TargetId = organizationId,\n        ExcludeCurrentContext = false,\n        Payload = new SyncPolicyPushNotification\n        {\n            Policy = policy,\n            OrganizationId = organizationId\n        }\n    });\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/VNextSavePolicyCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;\n\npublic class VNextSavePolicyCommand(\n    IOrganizationRepository organizationRepository,\n    IEventService eventService,\n    IPolicyRepository policyRepository,\n    IEnumerable<IPolicyUpdateEvent> policyUpdateEventHandlers,\n    TimeProvider timeProvider,\n    IPolicyEventHandlerFactory policyEventHandlerFactory,\n    IPushNotificationService pushNotificationService)\n    : IVNextSavePolicyCommand\n{\n\n    public async Task<Policy> SaveAsync(SavePolicyModel policyRequest)\n    {\n        var policyUpdateRequest = policyRequest.PolicyUpdate;\n        var organizationId = policyUpdateRequest.OrganizationId;\n\n        await EnsureOrganizationCanUsePolicyAsync(organizationId);\n\n        var savedPoliciesDict = await GetCurrentPolicyStateAsync(organizationId);\n\n        var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdateRequest.Type);\n\n        ValidatePolicyDependencies(policyUpdateRequest, currentPolicy, savedPoliciesDict);\n\n        await ValidateTargetedPolicyAsync(policyRequest, currentPolicy);\n\n        await ExecutePreUpsertSideEffectAsync(policyRequest, currentPolicy);\n\n        var upsertedPolicy = await UpsertPolicyAsync(policyUpdateRequest);\n\n        await eventService.LogPolicyEventAsync(upsertedPolicy, EventType.Policy_Updated);\n\n        await ExecutePostUpsertSideEffectAsync(policyRequest, upsertedPolicy, currentPolicy);\n\n        return upsertedPolicy;\n    }\n\n    private async Task EnsureOrganizationCanUsePolicyAsync(Guid organizationId)\n    {\n        var org = await organizationRepository.GetByIdAsync(organizationId);\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization not found\");\n        }\n\n        if (!org.UsePolicies)\n        {\n            throw new BadRequestException(\"This organization cannot use policies.\");\n        }\n    }\n\n    private async Task<Policy> UpsertPolicyAsync(PolicyUpdate policyUpdateRequest)\n    {\n        var policy = await policyRepository.GetByOrganizationIdTypeAsync(policyUpdateRequest.OrganizationId, policyUpdateRequest.Type)\n                     ?? new Policy\n                     {\n                         OrganizationId = policyUpdateRequest.OrganizationId,\n                         Type = policyUpdateRequest.Type,\n                         CreationDate = timeProvider.GetUtcNow().UtcDateTime\n                     };\n\n        policy.Enabled = policyUpdateRequest.Enabled;\n        policy.Data = policyUpdateRequest.Data;\n        policy.RevisionDate = timeProvider.GetUtcNow().UtcDateTime;\n\n        await policyRepository.UpsertAsync(policy);\n        await PushPolicyUpdateToClients(policyUpdateRequest.OrganizationId, policy);\n        return policy;\n    }\n\n    private async Task ValidateTargetedPolicyAsync(SavePolicyModel policyRequest,\n        Policy? currentPolicy)\n    {\n        await ExecutePolicyEventAsync<IPolicyValidationEvent>(\n            policyRequest.PolicyUpdate.Type,\n            async validator =>\n            {\n                var validationError = await validator.ValidateAsync(policyRequest, currentPolicy);\n                if (!string.IsNullOrEmpty(validationError))\n                {\n                    throw new BadRequestException(validationError);\n                }\n            });\n    }\n\n    private void ValidatePolicyDependencies(\n        PolicyUpdate policyUpdateRequest,\n        Policy? currentPolicy,\n        Dictionary<PolicyType, Policy> savedPoliciesDict)\n    {\n        var isCurrentlyEnabled = currentPolicy?.Enabled == true;\n        var isBeingEnabled = policyUpdateRequest.Enabled && !isCurrentlyEnabled;\n        var isBeingDisabled = !policyUpdateRequest.Enabled && isCurrentlyEnabled;\n\n        if (isBeingEnabled)\n        {\n            ValidateEnablingRequirements(policyUpdateRequest.Type, savedPoliciesDict);\n        }\n        else if (isBeingDisabled)\n        {\n            ValidateDisablingRequirements(policyUpdateRequest.Type, savedPoliciesDict);\n        }\n    }\n\n    private void ValidateDisablingRequirements(\n        PolicyType policyType,\n        Dictionary<PolicyType, Policy> savedPoliciesDict)\n    {\n        var dependentPolicyTypes = policyUpdateEventHandlers\n            .OfType<IEnforceDependentPoliciesEvent>()\n            .Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyType))\n            .Select(otherValidator => otherValidator.Type)\n            .Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) &&\n                                      savedPolicy.Enabled)\n            .ToList();\n\n        switch (dependentPolicyTypes)\n        {\n            case { Count: 1 }:\n                throw new BadRequestException($\"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {policyType.GetName()} policy.\");\n            case { Count: > 1 }:\n                throw new BadRequestException($\"Turn off all of the policies that require the {policyType.GetName()} policy.\");\n        }\n    }\n\n    private void ValidateEnablingRequirements(\n        PolicyType policyType,\n        Dictionary<PolicyType, Policy> savedPoliciesDict)\n    {\n        var result = policyEventHandlerFactory.GetHandler<IEnforceDependentPoliciesEvent>(policyType);\n\n        result.Switch(\n            validator =>\n            {\n                var missingRequiredPolicyTypes = validator.RequiredPolicies\n                    .Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true })\n                    .ToList();\n\n                if (missingRequiredPolicyTypes.Count != 0)\n                {\n                    throw new BadRequestException($\"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {policyType.GetName()} policy.\");\n                }\n            },\n            _ => { /* Policy has no required dependencies */ });\n    }\n\n    private async Task ExecutePreUpsertSideEffectAsync(\n        SavePolicyModel policyRequest,\n        Policy? currentPolicy)\n    {\n        await ExecutePolicyEventAsync<IOnPolicyPreUpdateEvent>(\n            policyRequest.PolicyUpdate.Type,\n            handler => handler.ExecutePreUpsertSideEffectAsync(policyRequest, currentPolicy));\n    }\n    private async Task ExecutePostUpsertSideEffectAsync(\n        SavePolicyModel policyRequest,\n        Policy postUpsertedPolicyState,\n        Policy? previousPolicyState)\n    {\n        await ExecutePolicyEventAsync<IOnPolicyPostUpdateEvent>(\n            policyRequest.PolicyUpdate.Type,\n            handler => handler.ExecutePostUpsertSideEffectAsync(\n                policyRequest,\n                postUpsertedPolicyState,\n                previousPolicyState));\n    }\n\n    private async Task ExecutePolicyEventAsync<T>(PolicyType type, Func<T, Task> func) where T : IPolicyUpdateEvent\n    {\n        var handler = policyEventHandlerFactory.GetHandler<T>(type);\n\n        await handler.Match(\n            async h => await func(h),\n            _ => Task.CompletedTask\n        );\n    }\n\n    private async Task<Dictionary<PolicyType, Policy>> GetCurrentPolicyStateAsync(Guid organizationId)\n    {\n        var savedPolicies = await policyRepository.GetManyByOrganizationIdAsync(organizationId);\n        // Note: policies may be missing from this dict if they have never been enabled\n        var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type);\n        return savedPoliciesDict;\n    }\n\n    Task PushPolicyUpdateToClients(Guid organizationId, Policy policy) => pushNotificationService.PushAsync(new PushNotification<SyncPolicyPushNotification>\n    {\n        Type = PushType.PolicyChanged,\n        Target = NotificationTarget.Organization,\n        TargetId = organizationId,\n        ExcludeCurrentContext = false,\n        Payload = new SyncPolicyPushNotification\n        {\n            Policy = policy,\n            OrganizationId = organizationId\n        }\n    });\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Models/EmptyMetadataModel.cs",
    "content": "﻿\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\npublic record EmptyMetadataModel : IPolicyMetadataModel\n{\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Models/IPolicyMetadataModel.cs",
    "content": "﻿\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\npublic interface IPolicyMetadataModel\n{\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Models/OrganizationModelOwnershipPolicyModel.cs",
    "content": "﻿\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\npublic class OrganizationModelOwnershipPolicyModel : IPolicyMetadataModel\n{\n    public OrganizationModelOwnershipPolicyModel()\n    {\n    }\n\n    public OrganizationModelOwnershipPolicyModel(string? defaultUserCollectionName)\n    {\n        DefaultUserCollectionName = defaultUserCollectionName;\n    }\n\n    public string? DefaultUserCollectionName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\n/// <summary>\n/// A request for SavePolicyCommand to update a policy\n/// </summary>\npublic record PolicyUpdate\n{\n    public Guid OrganizationId { get; set; }\n    public PolicyType Type { get; set; }\n    public string? Data { get; set; }\n    public bool Enabled { get; set; }\n\n    [Obsolete(\"Please use SavePolicyModel.PerformedBy instead.\")]\n    public IActingUser? PerformedBy { get; set; }\n\n    public T GetDataModel<T>() where T : IPolicyDataModel, new()\n    {\n        return CoreHelpers.LoadClassFromJsonData<T>(Data);\n    }\n\n    public void SetDataModel<T>(T dataModel) where T : IPolicyDataModel, new()\n    {\n        Data = CoreHelpers.ClassToJsonData(dataModel);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/Models/SavePolicyModel.cs",
    "content": "﻿\nusing Bit.Core.AdminConsole.Models.Data;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\npublic record SavePolicyModel(PolicyUpdate PolicyUpdate, IActingUser? PerformedBy, IPolicyMetadataModel Metadata)\n{\n    public SavePolicyModel(PolicyUpdate PolicyUpdate)\n        : this(PolicyUpdate, null, new EmptyMetadataModel())\n    {\n    }\n\n    public SavePolicyModel(PolicyUpdate PolicyUpdate, IActingUser performedBy)\n        : this(PolicyUpdate, performedBy, new EmptyMetadataModel())\n    {\n    }\n\n    public SavePolicyModel(PolicyUpdate PolicyUpdate, IPolicyMetadataModel metadata)\n        : this(PolicyUpdate, null, metadata)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/AutomaticUserConfirmationPolicyRequirement.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n/// <summary>\n/// Represents the enforcement status of the Automatic User Confirmation policy.\n/// </summary>\n/// <remarks>\n/// The Automatic User Confirmation policy is enforced against all types of users regardless of status or type.\n///\n/// Users cannot:\n/// <ul>\n/// <li>Be a member of another organization (similar to Single Organization Policy)</li>\n/// <li>Cannot be a provider</li>\n/// </ul>\n/// </remarks>\n/// <param name=\"policyDetails\">Collection of policy details that apply to this user id</param>\npublic class AutomaticUserConfirmationPolicyRequirement(IEnumerable<PolicyDetails> policyDetails) : IPolicyRequirement\n{\n    /// <summary>\n    /// Returns true if the user cannot invite to emergency access because they are in an\n    /// auto-confirm organization with status Accepted, Confirmed, or Revoked.\n    /// </summary>\n    public bool GrantorCannotInviteToEmergencyAccess() => policyDetails.Any(p =>\n        p.OrganizationUserStatus is\n            OrganizationUserStatusType.Accepted or\n            OrganizationUserStatusType.Confirmed or\n            OrganizationUserStatusType.Revoked);\n\n    /// <summary>\n    /// Returns true if the user cannot accept emergency access because they are in an\n    /// auto-confirm organization with status Accepted, Confirmed, or Revoked.\n    /// </summary>\n    public bool GranteeCannotAcceptEmergencyAccess() => policyDetails.Any(p =>\n        p.OrganizationUserStatus is\n            OrganizationUserStatusType.Accepted or\n            OrganizationUserStatusType.Confirmed or\n            OrganizationUserStatusType.Revoked);\n\n    public bool CannotJoinProvider() => policyDetails.Any();\n\n    public bool CannotCreateProvider() => policyDetails.Any();\n\n    public bool CannotCreateNewOrganization() => policyDetails.Any();\n\n    public bool IsEnabled(Guid organizationId) => policyDetails.Any(p => p.OrganizationId == organizationId);\n\n    public bool IsEnabledForOrganizationsOtherThan(Guid organizationId) =>\n        policyDetails.Any(p => p.OrganizationId != organizationId);\n}\n\npublic class AutomaticUserConfirmationPolicyRequirementFactory : BasePolicyRequirementFactory<AutomaticUserConfirmationPolicyRequirement>\n{\n    public override PolicyType PolicyType => PolicyType.AutomaticUserConfirmation;\n\n    protected override IEnumerable<OrganizationUserType> ExemptRoles => [];\n\n    protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => [];\n\n    protected override bool ExemptProviders => false;\n\n    public override AutomaticUserConfirmationPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) =>\n        new(policyDetails);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n/// <summary>\n/// A simple base implementation of <see cref=\"IPolicyRequirementFactory{T}\"/> which will be suitable for most policies.\n/// It provides sensible defaults to help teams to implement their own Policy Requirements.\n/// </summary>\n/// <typeparam name=\"T\"></typeparam>\npublic abstract class BasePolicyRequirementFactory<T> : IPolicyRequirementFactory<T> where T : IPolicyRequirement\n{\n    /// <summary>\n    /// User roles that are exempt from policy enforcement.\n    /// Owners and Admins are exempt by default but this may be overridden.\n    /// </summary>\n    protected virtual IEnumerable<OrganizationUserType> ExemptRoles { get; } =\n        [OrganizationUserType.Owner, OrganizationUserType.Admin];\n\n    /// <summary>\n    /// User statuses that are exempt from policy enforcement.\n    /// Invited and Revoked users are exempt by default, which is appropriate in the majority of cases.\n    /// </summary>\n    protected virtual IEnumerable<OrganizationUserStatusType> ExemptStatuses { get; } =\n        [OrganizationUserStatusType.Invited, OrganizationUserStatusType.Revoked];\n\n    /// <summary>\n    /// Whether a Provider User for the organization is exempt from policy enforcement.\n    /// Provider Users are exempt by default, which is appropriate in the majority of cases.\n    /// </summary>\n    protected virtual bool ExemptProviders { get; } = true;\n\n    /// <inheritdoc />\n    public abstract PolicyType PolicyType { get; }\n\n    public bool Enforce(PolicyDetails policyDetails)\n        => !policyDetails.HasRole(ExemptRoles) &&\n            !policyDetails.HasStatus(ExemptStatuses) &&\n            (!policyDetails.IsProvider || !ExemptProviders);\n\n    /// <inheritdoc />\n    public abstract T Create(IEnumerable<PolicyDetails> policyDetails);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirement.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n/// <summary>\n/// Policy requirements for the Disable Send policy.\n/// </summary>\npublic class DisableSendPolicyRequirement : IPolicyRequirement\n{\n    /// <summary>\n    /// Indicates whether Send is disabled for the user. If true, the user should not be able to create or edit Sends.\n    /// They may still delete existing Sends.\n    /// </summary>\n    public bool DisableSend { get; init; }\n}\n\npublic class DisableSendPolicyRequirementFactory : BasePolicyRequirementFactory<DisableSendPolicyRequirement>\n{\n    public override PolicyType PolicyType => PolicyType.DisableSend;\n\n    public override DisableSendPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)\n    {\n        var result = new DisableSendPolicyRequirement { DisableSend = policyDetails.Any() };\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/Errors/SingleOrganizationPolicyErrors.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.v2;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements.Errors;\n\npublic record UserIsAMemberOfAnOrganizationThatHasSingleOrgPolicy()\n    : BadRequestError(\"Member cannot join the organization because they are in another organization which forbids it.\");\n\npublic record UserIsAMemberOfAnotherOrganization()\n    : BadRequestError(\"Member cannot join the organization until they leave or remove all other organizations.\");\n\npublic record UserCannotCreateOrg()\n    : BadRequestError(\"You may not create an organization. You belong to an organization \" +\n                      \"which has a policy that prohibits you from being a member of any other organization.\");\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirement.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n/// <summary>\n/// An object that represents how a <see cref=\"PolicyType\"/> will be enforced against a user.\n/// This acts as a bridge between the <see cref=\"Policy\"/> entity saved to the database and the domain that the policy\n/// affects. You may represent the impact of the policy in any way that makes sense for the domain.\n/// </summary>\npublic interface IPolicyRequirement;\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/IPolicyRequirementFactory.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n/// <summary>\n/// An interface that defines how to create a single <see cref=\"IPolicyRequirement\"/> from a sequence of\n/// <see cref=\"PolicyDetails\"/>.\n/// </summary>\n/// <typeparam name=\"T\">The <see cref=\"IPolicyRequirement\"/> that the factory produces.</typeparam>\n/// <remarks>\n/// See <see cref=\"BasePolicyRequirementFactory{T}\"/> for a simple base implementation suitable for most policies.\n/// </remarks>\npublic interface IPolicyRequirementFactory<out T> where T : IPolicyRequirement\n{\n    /// <summary>\n    /// The <see cref=\"PolicyType\"/> that the requirement relates to.\n    /// </summary>\n    PolicyType PolicyType { get; }\n\n    /// <summary>\n    /// A predicate that determines whether a policy should be enforced against the user.\n    /// </summary>\n    /// <remarks>Use this to exempt users based on their role, status or other attributes.</remarks>\n    /// <param name=\"policyDetails\">Policy details for the defined PolicyType.</param>\n    /// <returns>True if the policy should be enforced against the user, false otherwise.</returns>\n    bool Enforce(PolicyDetails policyDetails);\n\n    /// <summary>\n    /// A reducer method that creates a single <see cref=\"IPolicyRequirement\"/> from a set of PolicyDetails.\n    /// </summary>\n    /// <param name=\"policyDetails\">\n    /// PolicyDetails for the specified PolicyType, after they have been filtered by the Enforce predicate. That is,\n    /// this is the final interface to be called.\n    /// </param>\n    T Create(IEnumerable<PolicyDetails> policyDetails);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n/// <summary>\n/// Represents the Organization Data Ownership policy state.\n/// </summary>\npublic enum OrganizationDataOwnershipState\n{\n    /// <summary>\n    /// Organization Data Ownership is enforced- members are required to save items to an organization.\n    /// </summary>\n    Enabled = 1,\n\n    /// <summary>\n    /// Organization Data Ownership is not enforced- users can save items to their personal vault.\n    /// </summary>\n    Disabled = 2\n}\n\n/// <summary>\n/// Policy requirements for the Organization data ownership policy\n/// </summary>\npublic class OrganizationDataOwnershipPolicyRequirement : IPolicyRequirement\n{\n    private readonly IEnumerable<PolicyDetails> _policyDetails;\n\n    /// <param name=\"organizationDataOwnershipState\">\n    /// The organization data ownership state for the user.\n    /// </param>\n    /// <param name=\"policyDetails\">\n    /// An enumerable collection of PolicyDetails for the organizations.\n    /// </param>\n    public OrganizationDataOwnershipPolicyRequirement(\n        OrganizationDataOwnershipState organizationDataOwnershipState,\n        IEnumerable<PolicyDetails> policyDetails)\n    {\n        _policyDetails = policyDetails;\n        State = organizationDataOwnershipState;\n    }\n\n    /// <summary>\n    /// The Organization data ownership policy state for the user.\n    /// </summary>\n    public OrganizationDataOwnershipState State { get; }\n\n    /// <summary>\n    /// Gets a default collection request for enforcing the Organization Data Ownership policy.\n    /// Only confirmed users are applicable.\n    /// This indicates whether the user should have a default collection created for them when the policy is enabled,\n    /// and if so, the relevant OrganizationUserId to create the collection for.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization ID to create the request for.</param>\n    /// <returns>A DefaultCollectionRequest containing the OrganizationUserId and a flag indicating whether to create a default collection.</returns>\n    public DefaultCollectionRequest GetDefaultCollectionRequestOnPolicyEnable(Guid organizationId)\n    {\n        var policyDetail = _policyDetails\n            .FirstOrDefault(p => p.OrganizationId == organizationId);\n\n        if (policyDetail != null && policyDetail.HasStatus([OrganizationUserStatusType.Confirmed]))\n        {\n            return new DefaultCollectionRequest(policyDetail.OrganizationUserId, true);\n        }\n\n        var noCollectionNeeded = new DefaultCollectionRequest(Guid.Empty, false);\n        return noCollectionNeeded;\n    }\n\n    public DefaultCollectionRequest GetDefaultCollectionRequestOnConfirm(Guid organizationId)\n    {\n        var matchingOrgUserId =\n            _policyDetails.FirstOrDefault(p => p.OrganizationId == organizationId)?.OrganizationUserId;\n\n        return new DefaultCollectionRequest(\n            OrganizationUserId: matchingOrgUserId.GetValueOrDefault(Guid.Empty),\n            ShouldCreateDefaultCollection: matchingOrgUserId.HasValue);\n    }\n\n    /// <summary>\n    /// Ignore storage limits if the organization has data ownership policy enabled.\n    /// Allows users to seamlessly migrate their data into the organization without being blocked by storage limits.\n    /// Organization admins will need to manage storage after migration should overages occur.\n    /// </summary>\n    public bool IgnoreStorageLimitsOnMigration(Guid organizationId)\n    {\n        return _policyDetails.Any(p => p.OrganizationId == organizationId &&\n                                       p.OrganizationUserStatus == OrganizationUserStatusType.Confirmed);\n    }\n\n    /// <summary>\n    /// Determines if a user is eligible for self-revocation under the Organization Data Ownership policy.\n    /// A user is eligible if they are a confirmed member of the organization and the policy is enabled.\n    /// This also handles exempt roles (Owner/Admin) and policy disabled state via the factory's Enforce predicate.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization ID to check.</param>\n    /// <returns>True if the user is eligible for self-revocation (policy applies to them), false otherwise.</returns>\n    /// <remarks>\n    /// Self-revoke is used to opt out of migrating the user's personal vault to the organization as required by this policy.\n    /// </remarks>\n    public bool EligibleForSelfRevoke(Guid organizationId)\n    {\n        var policyDetail = _policyDetails\n            .FirstOrDefault(p => p.OrganizationId == organizationId);\n\n        return policyDetail?.HasStatus([OrganizationUserStatusType.Confirmed]) ?? false;\n    }\n}\n\npublic record DefaultCollectionRequest(Guid OrganizationUserId, bool ShouldCreateDefaultCollection)\n{\n    public readonly bool ShouldCreateDefaultCollection = ShouldCreateDefaultCollection;\n    public readonly Guid OrganizationUserId = OrganizationUserId;\n}\n\npublic class OrganizationDataOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory<OrganizationDataOwnershipPolicyRequirement>\n{\n    public override PolicyType PolicyType => PolicyType.OrganizationDataOwnership;\n\n    public override OrganizationDataOwnershipPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)\n    {\n        var organizationDataOwnershipState = policyDetails.Any()\n            ? OrganizationDataOwnershipState.Enabled\n            : OrganizationDataOwnershipState.Disabled;\n\n        return new OrganizationDataOwnershipPolicyRequirement(\n            organizationDataOwnershipState,\n            policyDetails);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyRequirementHelpers.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\npublic static class PolicyRequirementHelpers\n{\n    /// <summary>\n    /// Returns true if the <see cref=\"PolicyDetails\"/> is for one of the specified roles, false otherwise.\n    /// </summary>\n    public static bool HasRole(\n        this PolicyDetails policyDetails,\n        IEnumerable<OrganizationUserType> roles)\n        => roles.Contains(policyDetails.OrganizationUserType);\n\n    /// <summary>\n    /// Returns true if the <see cref=\"PolicyDetails\"/> relates to one of the specified statuses, false otherwise.\n    /// </summary>\n    public static bool HasStatus(this PolicyDetails policyDetails, IEnumerable<OrganizationUserStatusType> status)\n        => status.Contains(policyDetails.OrganizationUserStatus);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireSsoPolicyRequirement.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Enums;\nusing Bit.Core.Settings;\n\n/// <summary>\n/// Policy requirements for the Require SSO policy.\n/// </summary>\npublic class RequireSsoPolicyRequirement : IPolicyRequirement\n{\n    /// <summary>\n    /// Indicates whether the user can use passkey login.\n    /// </summary>\n    /// <remarks>\n    /// The user can use passkey login if they are not a member (Accepted/Confirmed) of an organization\n    /// that has the Require SSO policy enabled.\n    /// </remarks>\n    public bool CanUsePasskeyLogin { get; init; }\n\n    /// <summary>\n    /// Indicates whether SSO requirement is enforced for the user.\n    /// </summary>\n    /// <remarks>\n    /// The user is required to login with SSO if they are a confirmed member of an organization\n    /// that has the Require SSO policy enabled.\n    /// </remarks>\n    public bool SsoRequired { get; init; }\n}\n\n\npublic class RequireSsoPolicyRequirementFactory : BasePolicyRequirementFactory<RequireSsoPolicyRequirement>\n{\n    private readonly GlobalSettings _globalSettings;\n\n    public RequireSsoPolicyRequirementFactory(GlobalSettings globalSettings)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public override PolicyType PolicyType => PolicyType.RequireSso;\n\n    protected override IEnumerable<OrganizationUserType> ExemptRoles =>\n        _globalSettings.Sso.EnforceSsoPolicyForAllUsers\n            ? Array.Empty<OrganizationUserType>()\n            : [OrganizationUserType.Owner, OrganizationUserType.Admin];\n\n    public override RequireSsoPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)\n    {\n        policyDetails = policyDetails.ToList();\n        var result = new RequireSsoPolicyRequirement\n        {\n            CanUsePasskeyLogin = !policyDetails.Any(p =>\n                p.OrganizationUserStatus is OrganizationUserStatusType.Accepted or OrganizationUserStatusType.Confirmed),\n\n            SsoRequired = policyDetails.Any(p =>\n                p.OrganizationUserStatus == OrganizationUserStatusType.Confirmed)\n        };\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n/// <summary>\n/// Policy requirements for the Require Two-Factor Authentication policy.\n/// </summary>\npublic class RequireTwoFactorPolicyRequirement : IPolicyRequirement\n{\n    private readonly IEnumerable<PolicyDetails> _policyDetails;\n\n    public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)\n    {\n        _policyDetails = policyDetails;\n    }\n\n    /// <summary>\n    /// Checks if two-factor authentication is required for the organization due to an active policy.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization to check.</param>\n    /// <returns>True if two-factor authentication is required for the organization, false otherwise.</returns>\n    /// <remarks>\n    /// This should be used to check whether the member needs to have 2FA enabled before being\n    /// accepted, confirmed, or restored to the organization.\n    /// </remarks>\n    public bool IsTwoFactorRequiredForOrganization(Guid organizationId) =>\n        _policyDetails.Any(p => p.OrganizationId == organizationId);\n\n    /// <summary>\n    /// Returns tuples of (OrganizationId, OrganizationUserId) for active memberships where two-factor authentication is required.\n    /// Users should be revoked from these organizations if they disable all 2FA methods.\n    /// </summary>\n    public IEnumerable<(Guid OrganizationId, Guid OrganizationUserId)> OrganizationsRequiringTwoFactor =>\n        _policyDetails\n            .Where(p => p.OrganizationUserStatus is\n                OrganizationUserStatusType.Accepted or\n                OrganizationUserStatusType.Confirmed)\n            .Select(p => (p.OrganizationId, p.OrganizationUserId));\n}\n\npublic class RequireTwoFactorPolicyRequirementFactory : BasePolicyRequirementFactory<RequireTwoFactorPolicyRequirement>\n{\n    public override PolicyType PolicyType => PolicyType.TwoFactorAuthentication;\n    protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => [];\n\n    public override RequireTwoFactorPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)\n    {\n        return new RequireTwoFactorPolicyRequirement(policyDetails);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirement.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n/// <summary>\n/// Policy requirements for the Account recovery administration policy.\n/// </summary>\npublic class ResetPasswordPolicyRequirement : IPolicyRequirement\n{\n    /// <summary>\n    /// List of Organization Ids that require automatic enrollment in password recovery.\n    /// </summary>\n    private IEnumerable<Guid> _autoEnrollOrganizations;\n    public IEnumerable<Guid> AutoEnrollOrganizations { init => _autoEnrollOrganizations = value; }\n\n    /// <summary>\n    /// Returns true if provided organizationId requires automatic enrollment in password recovery.\n    /// </summary>\n    public bool AutoEnrollEnabled(Guid organizationId)\n    {\n        return _autoEnrollOrganizations.Contains(organizationId);\n    }\n\n\n}\n\npublic class ResetPasswordPolicyRequirementFactory : BasePolicyRequirementFactory<ResetPasswordPolicyRequirement>\n{\n    public override PolicyType PolicyType => PolicyType.ResetPassword;\n\n    protected override bool ExemptProviders => false;\n\n    protected override IEnumerable<OrganizationUserType> ExemptRoles => [];\n\n    protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => [OrganizationUserStatusType.Revoked];\n\n    public override ResetPasswordPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)\n    {\n        var result = policyDetails\n        .Where(p => p.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled)\n        .Select(p => p.OrganizationId)\n        .ToHashSet();\n\n        return new ResetPasswordPolicyRequirement() { AutoEnrollOrganizations = result };\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirement.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n/// <summary>\n/// Policy requirements for the Send Options policy.\n/// </summary>\npublic class SendOptionsPolicyRequirement : IPolicyRequirement\n{\n    /// <summary>\n    /// Indicates whether the user is prohibited from hiding their email from the recipient of a Send.\n    /// </summary>\n    public bool DisableHideEmail { get; init; }\n}\n\npublic class SendOptionsPolicyRequirementFactory : BasePolicyRequirementFactory<SendOptionsPolicyRequirement>\n{\n    public override PolicyType PolicyType => PolicyType.SendOptions;\n\n    public override SendOptionsPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)\n    {\n        var result = policyDetails\n            .Select(p => p.GetDataModel<SendOptionsPolicyData>())\n            .Aggregate(\n                new SendOptionsPolicyRequirement(),\n                (result, data) => new SendOptionsPolicyRequirement\n                {\n                    DisableHideEmail = result.DisableHideEmail || data.DisableHideEmail\n                });\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirement.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements.Errors;\nusing Bit.Core.AdminConsole.Utilities.v2;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\npublic class SingleOrganizationPolicyRequirement(IEnumerable<PolicyDetails> policyDetails) : IPolicyRequirement\n{\n    /// <summary>\n    /// Returns an error if the user cannot create an organization due to being a part of another organization.\n    /// </summary>\n    /// <returns>UserCannotCreateOrg error if the user cannot create an organization, otherwise null.</returns>\n    public Error? CanCreateOrganization() => policyDetails\n        .Any(p => p.HasStatus([OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed]))\n        ? new UserCannotCreateOrg()\n        : null;\n\n    /// <summary>\n    /// Returns an error if the user cannot join the organization.\n    /// </summary>\n    /// <param name=\"organizationId\">Organization the user is attempting to join.</param>\n    /// <param name=\"allOrgUsers\">All organization users that a given user is linked to.</param>\n    /// <returns>\n    /// UserIsAMemberOfAnotherOrganization or UserIsAMemberOfAnOrganizationThatHasSingleOrgPolicy if the user cannot\n    /// join the organization, otherwise null.\n    /// </returns>\n    public Error? CanJoinOrganization(Guid organizationId, ICollection<OrganizationUser> allOrgUsers) =>\n        IsCompliantWithTargetOrganization(organizationId, allOrgUsers)\n        ?? IsEnforcedForOtherOrganizationsUserIsAPartOf(organizationId);\n\n    /// <summary>\n    /// Returns true if the policy is enabled for the target organization.\n    /// </summary>\n    /// <param name=\"targetOrganizationId\">Organization Id the user is attempting to join</param>\n    /// <returns></returns>\n    private bool IsEnabledForTargetOrganization(Guid targetOrganizationId) =>\n        policyDetails.Any(p => p.OrganizationId == targetOrganizationId);\n\n    /// <summary>\n    /// Will return an error if the user is a member of another organization and Single Organization is enabled for the\n    /// target organization.\n    /// </summary>\n    /// <param name=\"targetOrganizationId\">Organization Id the user is attempting to join</param>\n    /// <param name=\"allOrgUsers\">All organization users associated with the user id</param>\n    /// <returns>\n    /// UserIsAMemberOfAnotherOrganization if the user cannot join the target organization, otherwise null.\n    /// </returns>\n    private Error? IsCompliantWithTargetOrganization(Guid targetOrganizationId,\n        ICollection<OrganizationUser> allOrgUsers) =>\n        IsEnabledForTargetOrganization(targetOrganizationId)\n        && allOrgUsers.Any(ou => ou.OrganizationId != targetOrganizationId)\n            ? new UserIsAMemberOfAnotherOrganization()\n            : null;\n\n    /// <summary>\n    /// Returns an error if the user is a member of another organization that has enabled the Single Organization policy.\n    /// </summary>\n    /// <param name=\"targetOrganizationId\">Organization Id the user is attempting to join</param>\n    /// <returns>\n    /// UserIsAMemberOfAnOrganizationThatHasSingleOrgPolicy if the user is a member of another organization that has\n    /// enabled the Single Organization policy, otherwise null.\n    /// </returns>\n    private Error? IsEnforcedForOtherOrganizationsUserIsAPartOf(Guid targetOrganizationId) =>\n        policyDetails.Any(p => p.OrganizationId != targetOrganizationId\n            && p.HasStatus([OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed]))\n            ? new UserIsAMemberOfAnOrganizationThatHasSingleOrgPolicy()\n            : null;\n}\n\npublic class SingleOrganizationPolicyRequirementFactory : BasePolicyRequirementFactory<SingleOrganizationPolicyRequirement>\n{\n    public override PolicyType PolicyType => PolicyType.SingleOrg;\n\n    protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses { get; } = [];\n\n    public override SingleOrganizationPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) =>\n        new(policyDetails);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.AdminConsole.Services.Implementations;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;\n\npublic static class PolicyServiceCollectionExtensions\n{\n    public static void AddPolicyServices(this IServiceCollection services)\n    {\n        services.AddScoped<IPolicyService, PolicyService>();\n        services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();\n        services.AddScoped<IVNextSavePolicyCommand, VNextSavePolicyCommand>();\n        services.AddScoped<IPolicyRequirementQuery, PolicyRequirementQuery>();\n        services.AddScoped<IPolicyQuery, PolicyQuery>();\n        services.AddScoped<IPolicyEventHandlerFactory, PolicyEventHandlerHandlerFactory>();\n\n        services.AddScoped<IAutomaticUserConfirmationPolicyEnforcementValidator, AutomaticUserConfirmationPolicyEnforcementValidator>();\n        services.AddScoped<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator, AutomaticUserConfirmationOrganizationPolicyComplianceValidator>();\n\n        services.AddPolicyValidators();\n        services.AddPolicyRequirements();\n        services.AddPolicySideEffects();\n        services.AddPolicyUpdateEvents();\n\n    }\n\n    [Obsolete(\"Use AddPolicyUpdateEvents instead.\")]\n    private static void AddPolicyValidators(this IServiceCollection services)\n    {\n        services.AddScoped<IPolicyValidator, TwoFactorAuthenticationPolicyValidator>();\n        services.AddScoped<IPolicyValidator, SingleOrgPolicyValidator>();\n        services.AddScoped<IPolicyValidator, RequireSsoPolicyValidator>();\n        services.AddScoped<IPolicyValidator, ResetPasswordPolicyValidator>();\n        services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();\n        services.AddScoped<IPolicyValidator, UriMatchDefaultPolicyValidator>();\n        services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();\n        services.AddScoped<IPolicyValidator, BlockClaimedDomainAccountCreationPolicyValidator>();\n        services.AddScoped<IPolicyValidator, AutomaticUserConfirmationPolicyEventHandler>();\n    }\n\n    [Obsolete(\"Use AddPolicyUpdateEvents instead.\")]\n    private static void AddPolicySideEffects(this IServiceCollection services)\n    {\n        services.AddScoped<IPostSavePolicySideEffect, OrganizationDataOwnershipPolicyValidator>();\n    }\n\n    private static void AddPolicyUpdateEvents(this IServiceCollection services)\n    {\n        services.AddScoped<IPolicyUpdateEvent, RequireSsoPolicyValidator>();\n        services.AddScoped<IPolicyUpdateEvent, TwoFactorAuthenticationPolicyValidator>();\n        services.AddScoped<IPolicyUpdateEvent, SingleOrgPolicyValidator>();\n        services.AddScoped<IPolicyUpdateEvent, ResetPasswordPolicyValidator>();\n        services.AddScoped<IPolicyUpdateEvent, MaximumVaultTimeoutPolicyValidator>();\n        services.AddScoped<IPolicyUpdateEvent, FreeFamiliesForEnterprisePolicyValidator>();\n        services.AddScoped<IPolicyUpdateEvent, OrganizationDataOwnershipPolicyValidator>();\n        services.AddScoped<IPolicyUpdateEvent, UriMatchDefaultPolicyValidator>();\n        services.AddScoped<IPolicyUpdateEvent, BlockClaimedDomainAccountCreationPolicyValidator>();\n        services.AddScoped<IPolicyUpdateEvent, AutomaticUserConfirmationPolicyEventHandler>();\n    }\n\n    private static void AddPolicyRequirements(this IServiceCollection services)\n    {\n        services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();\n        services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();\n        services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();\n        services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, OrganizationDataOwnershipPolicyRequirementFactory>();\n        services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();\n        services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();\n        services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SingleOrganizationPolicyRequirementFactory>();\n        services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, AutomaticUserConfirmationPolicyRequirementFactory>();\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IEnforceDependentPoliciesEvent.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\n\n/// <summary>\n/// Represents all policies required to be enabled before the given policy can be enabled.\n/// </summary>\n/// <remarks>\n/// This interface is intended for policy event handlers that mandate the activation of other policies\n/// as prerequisites for enabling the associated policy.\n/// </remarks>\npublic interface IEnforceDependentPoliciesEvent : IPolicyUpdateEvent\n{\n    /// <summary>\n    /// PolicyTypes that must be enabled before this policy can be enabled, if any.\n    /// These dependencies will be checked when this policy is enabled and when any required policy is disabled.\n    /// </summary>\n    public IEnumerable<PolicyType> RequiredPolicies { get; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPostUpdateEvent.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\npublic interface IOnPolicyPostUpdateEvent : IPolicyUpdateEvent\n{\n    /// <summary>\n    /// Performs side effects after a policy has been upserted.\n    /// For example, this can be used for cleanup tasks or notifications.\n    /// </summary>\n    /// <param name=\"policyRequest\">The policy save request</param>\n    /// <param name=\"postUpsertedPolicyState\">The policy after it was upserted</param>\n    /// <param name=\"previousPolicyState\">The policy state before it was updated, if any</param>\n    public Task ExecutePostUpsertSideEffectAsync(\n        SavePolicyModel policyRequest,\n        Policy postUpsertedPolicyState,\n        Policy? previousPolicyState);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPreUpdateEvent.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\n\n/// <summary>\n/// Represents all side effects that should be executed before a policy is upserted.\n/// </summary>\n/// <remarks>\n/// This should be added to policy handlers that need to perform side effects before policy upserts.\n/// </remarks>\npublic interface IOnPolicyPreUpdateEvent : IPolicyUpdateEvent\n{\n    /// <summary>\n    /// Performs side effects before a policy is upserted.\n    /// For example, this can be used to remove non-compliant users from the organization.\n    /// </summary>\n    /// <param name=\"policyRequest\">The policy save request containing the policy update and metadata</param>\n    /// <param name=\"currentPolicy\">The current policy, if any</param>\n    public Task ExecutePreUpsertSideEffectAsync(\n        SavePolicyModel policyRequest,\n        Policy? currentPolicy);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyEventHandlerFactory.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Enums;\nusing OneOf;\nusing OneOf.Types;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\n\n/// <summary>\n/// Provides policy-specific event handlers used during the save workflow in <see cref=\"IVNextSavePolicyCommand\"/>.\n/// </summary>\n/// <remarks>\n/// Supported handlers:  \n/// - <see cref=\"IEnforceDependentPoliciesEvent\"/> for dependency checks  \n/// - <see cref=\"IPolicyValidationEvent\"/> for custom validation  \n/// - <see cref=\"IOnPolicyPreUpdateEvent\"/> for pre-save logic  \n/// - <see cref=\"IOnPolicyPostUpdateEvent\"/> for post-save logic  \n/// </remarks>\npublic interface IPolicyEventHandlerFactory\n{\n    /// <summary>\n    /// Gets the event handler for the given policy type and handler interface.\n    /// </summary>\n    /// <typeparam name=\"T\">Handler type implementing <see cref=\"IPolicyUpdateEvent\"/>.</typeparam>\n    /// <param name=\"policyType\">The policy type to resolve.</param>\n    /// <returns>\n    /// <see cref=\"OneOf{T, None}\"/> — the handler if available, or None if not implemented.\n    /// </returns>\n    OneOf<T, None> GetHandler<T>(PolicyType policyType) where T : IPolicyUpdateEvent;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyUpdateEvent.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\n\n/// <summary>\n/// Represents the policy to be upserted.\n/// </summary>\n/// <remarks>\n/// This is used for the VNextSavePolicyCommand. All policy handlers should implement this interface.\n/// </remarks>\npublic interface IPolicyUpdateEvent\n{\n    /// <summary>\n    /// The policy type that the associated handler will handle.\n    /// </summary>\n    public PolicyType Type { get; }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyValidationEvent.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\n\n/// <summary>\n/// Represents all validations that need to be run to enable or disable the given policy.\n/// </summary>\n/// <remarks>\n/// This is used for the VNextSavePolicyCommand. This optional but should be implemented for all policies that have\n/// certain requirements for the given organization.\n/// </remarks>\npublic interface IPolicyValidationEvent : IPolicyUpdateEvent\n{\n    /// <summary>\n    /// Performs any validations required to enable or disable the policy.\n    /// </summary>\n    /// <param name=\"policyRequest\">The policy save request containing the policy update and metadata</param>\n    /// <param name=\"currentPolicy\">The current policy, if any</param>\n    public Task<string> ValidateAsync(\n        SavePolicyModel policyRequest,\n        Policy? currentPolicy);\n\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IVNextSavePolicyCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Microsoft.Azure.NotificationHubs.Messaging;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\n\n/// <summary>\n/// Handles creating or updating organization policies with validation and side effect execution.\n/// </summary>\n/// <remarks>\n/// Workflow:\n/// 1. Validates organization can use policies\n/// 2. Validates required and dependent policies\n/// 3. Runs policy-specific validation (<see cref=\"IPolicyValidationEvent\"/>)\n/// 4. Executes pre-save logic (<see cref=\"IOnPolicyPreUpdateEvent\"/>)\n/// 5. Saves the policy\n/// 6. Logs the event\n/// 7. Executes post-save logic (<see cref=\"IOnPolicyPostUpdateEvent\"/>)\n/// </remarks>\npublic interface IVNextSavePolicyCommand\n{\n    /// <summary>\n    /// Performs the necessary validations, saves the policy and any side effects\n    /// </summary>\n    /// <param name=\"policyRequest\">Policy data, acting user, and metadata.</param>\n    /// <returns>The saved policy with updated revision and applied changes.</returns>\n    /// <exception cref=\"BadRequestException\">\n    /// Thrown if:\n    /// - The organization can’t use policies\n    /// - Dependent policies are missing or block changes\n    /// - Custom validation fails\n    /// </exception>\n    Task<Policy> SaveAsync(SavePolicyModel policyRequest);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/PolicyEventHandlerHandlerFactory.cs",
    "content": "﻿\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing OneOf;\nusing OneOf.Types;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents;\n\npublic class PolicyEventHandlerHandlerFactory(\n    IEnumerable<IPolicyUpdateEvent> allEventHandlers) : IPolicyEventHandlerFactory\n{\n    public OneOf<T, None> GetHandler<T>(PolicyType policyType) where T : IPolicyUpdateEvent\n    {\n        var tEventHandlers = allEventHandlers.OfType<T>().ToList();\n\n        var matchingHandlers = tEventHandlers.Where(h => h.Type == policyType).ToList();\n\n        if (matchingHandlers.Count > 1)\n        {\n            throw new InvalidOperationException(\n                $\"Multiple {nameof(IPolicyUpdateEvent)} handlers of type {typeof(T).Name} found for {nameof(PolicyType)} {policyType}. \" +\n                $\"Expected one {typeof(T).Name} handler per {nameof(PolicyType)}.\");\n        }\n\n        var policyTEventHandler = matchingHandlers.SingleOrDefault();\n        if (policyTEventHandler is null)\n        {\n            return new None();\n        }\n\n        return policyTEventHandler;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandler.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\n/// <summary>\n/// Represents an event handler for the Automatic User Confirmation policy.\n///\n/// This class validates that the following conditions are met:\n/// <ul>\n///     <li>The Single organization policy is enabled</li>\n///     <li>All organization users are compliant with the Single organization policy</li>\n///     <li>No provider users exist</li>\n/// </ul>\n/// </summary>\npublic class AutomaticUserConfirmationPolicyEventHandler(\n    IAutomaticUserConfirmationOrganizationPolicyComplianceValidator validator,\n    IOrganizationUserRepository organizationUserRepository,\n    IDeleteEmergencyAccessCommand deleteEmergencyAccessCommand)\n    : IPolicyValidator, IPolicyValidationEvent, IEnforceDependentPoliciesEvent, IOnPolicyPreUpdateEvent\n{\n    public PolicyType Type => PolicyType.AutomaticUserConfirmation;\n\n    public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];\n\n    public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        var isNotEnablingPolicy = policyUpdate is not { Enabled: true };\n        var policyAlreadyEnabled = currentPolicy is { Enabled: true };\n        if (isNotEnablingPolicy || policyAlreadyEnabled)\n        {\n            return string.Empty;\n        }\n\n        return (await validator.IsOrganizationCompliantAsync(\n            new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId)))\n            .Match(\n                error => error.Message,\n                _ => string.Empty);\n    }\n\n    public async Task<string> ValidateAsync(SavePolicyModel savePolicyModel, Policy? currentPolicy) =>\n        await ValidateAsync(savePolicyModel.PolicyUpdate, currentPolicy);\n\n    public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy)\n    {\n        await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy);\n    }\n\n    public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        var isNotEnablingPolicy = policyUpdate is not { Enabled: true };\n        var policyAlreadyEnabled = currentPolicy is { Enabled: true };\n        if (isNotEnablingPolicy || policyAlreadyEnabled)\n        {\n            return;\n        }\n\n        var orgUsers = await organizationUserRepository.GetManyByOrganizationAsync(policyUpdate.OrganizationId, null);\n        var orgUserIds = orgUsers.Where(w => w.UserId != null).Select(s => s.UserId!.Value).ToList();\n\n        await deleteEmergencyAccessCommand.DeleteAllByUserIdsAsync(orgUserIds);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/BlockClaimedDomainAccountCreationPolicyValidator.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class BlockClaimedDomainAccountCreationPolicyValidator : IPolicyValidator, IPolicyValidationEvent\n{\n    private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;\n\n    public BlockClaimedDomainAccountCreationPolicyValidator(\n        IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)\n    {\n        _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;\n    }\n\n    public PolicyType Type => PolicyType.BlockClaimedDomainAccountCreation;\n\n    // No prerequisites - this policy stands alone\n    public IEnumerable<PolicyType> RequiredPolicies => [];\n\n    public async Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)\n    {\n        return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy);\n    }\n\n    public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        // Only validate when trying to ENABLE the policy\n        if (policyUpdate is { Enabled: true })\n        {\n            // Check if organization has at least one verified domain\n            if (!await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))\n            {\n                return \"You must claim at least one domain to turn on this policy\";\n            }\n        }\n\n        // Disabling the policy is always allowed\n        return string.Empty;\n    }\n\n    public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n        => Task.CompletedTask;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidator.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class FreeFamiliesForEnterprisePolicyValidator(\n    IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n    IMailService mailService,\n    IOrganizationRepository organizationRepository)\n    : IPolicyValidator, IOnPolicyPreUpdateEvent\n{\n    public PolicyType Type => PolicyType.FreeFamiliesSponsorshipPolicy;\n    public IEnumerable<PolicyType> RequiredPolicies => [];\n\n    public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy)\n    {\n        await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy);\n    }\n\n    public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })\n        {\n            await NotifiesUserWithApplicablePoliciesAsync(policyUpdate);\n        }\n    }\n\n    private async Task NotifiesUserWithApplicablePoliciesAsync(PolicyUpdate policy)\n    {\n        var organizationSponsorships = (await organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(policy.OrganizationId))\n            .Where(p => p.SponsoredOrganizationId is not null)\n            .ToList();\n\n        var organization = await organizationRepository.GetByIdAsync(policy.OrganizationId);\n        var organizationName = organization?.Name;\n\n        foreach (var org in organizationSponsorships)\n        {\n            var offerAcceptanceDate = org.ValidUntil!.Value.AddDays(-7).ToString(\"MM/dd/yyyy\");\n            await mailService.SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(org.FriendlyName, offerAcceptanceDate,\n                org.SponsoredOrganizationId.ToString(), organizationName);\n        }\n    }\n\n    public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(\"\");\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/MaximumVaultTimeoutPolicyValidator.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class MaximumVaultTimeoutPolicyValidator : IPolicyValidator, IEnforceDependentPoliciesEvent\n{\n    public PolicyType Type => PolicyType.MaximumVaultTimeout;\n    public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];\n    public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(\"\");\n    public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs",
    "content": "﻿\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class OrganizationDataOwnershipPolicyValidator(\n    IPolicyRepository policyRepository,\n    ICollectionRepository collectionRepository,\n    IOrganizationRepository organizationRepository,\n    IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories)\n    : OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect, IOnPolicyPostUpdateEvent\n{\n    public PolicyType Type => PolicyType.OrganizationDataOwnership;\n\n    public async Task ExecutePostUpsertSideEffectAsync(\n        SavePolicyModel policyRequest,\n        Policy postUpsertedPolicyState,\n        Policy? previousPolicyState)\n    {\n        await ExecuteSideEffectsAsync(policyRequest, postUpsertedPolicyState, previousPolicyState);\n    }\n\n    public async Task ExecuteSideEffectsAsync(\n        SavePolicyModel policyRequest,\n        Policy postUpdatedPolicy,\n        Policy? previousPolicyState)\n    {\n        if (policyRequest.Metadata is not OrganizationModelOwnershipPolicyModel metadata)\n        {\n            return;\n        }\n\n        if (string.IsNullOrWhiteSpace(metadata.DefaultUserCollectionName))\n        {\n            return;\n        }\n\n        var isFirstTimeEnabled = postUpdatedPolicy.Enabled && previousPolicyState == null;\n        var reEnabled = previousPolicyState?.Enabled == false\n                        && postUpdatedPolicy.Enabled;\n\n        if (isFirstTimeEnabled || reEnabled)\n        {\n            await UpsertDefaultCollectionsForUsersAsync(policyRequest.PolicyUpdate, metadata.DefaultUserCollectionName);\n        }\n    }\n\n    private async Task UpsertDefaultCollectionsForUsersAsync(PolicyUpdate policyUpdate, string defaultCollectionName)\n    {\n        // FIXME: we should use the organizationAbility cache here, but it is currently flaky\n        // and it's not obvious how to handle a cache failure.\n        // https://bitwarden.atlassian.net/browse/PM-32699\n        var organization = await organizationRepository.GetByIdAsync(policyUpdate.OrganizationId);\n        if (organization == null)\n        {\n            throw new InvalidOperationException($\"Organization with ID {policyUpdate.OrganizationId} not found.\");\n        }\n\n        if (!organization.UseMyItems)\n        {\n            return;\n        }\n\n        var requirements = await GetUserPolicyRequirementsByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(policyUpdate.OrganizationId, policyUpdate.Type);\n\n        var userOrgIds = requirements\n            .Select(requirement => requirement.GetDefaultCollectionRequestOnPolicyEnable(policyUpdate.OrganizationId))\n            .Where(request => request.ShouldCreateDefaultCollection)\n            .Select(request => request.OrganizationUserId)\n            .ToList();\n\n        if (!userOrgIds.Any())\n        {\n            return;\n        }\n\n        await collectionRepository.CreateDefaultCollectionsBulkAsync(\n            policyUpdate.OrganizationId,\n            userOrgIds,\n            defaultCollectionName);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\n\n/// <summary>\n/// Please do not use this validator. We're currently in the process of refactoring our policy validator pattern.\n/// This is a stop-gap solution for post-policy-save side effects, but it is not the long-term solution.\n/// </summary>\npublic abstract class OrganizationPolicyValidator(IPolicyRepository policyRepository, IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>> factories)\n{\n    protected async Task<IEnumerable<T>> GetUserPolicyRequirementsByOrganizationIdAsync<T>(Guid organizationId, PolicyType policyType) where T : IPolicyRequirement\n    {\n        var factory = factories.OfType<IPolicyRequirementFactory<T>>().SingleOrDefault();\n        if (factory is null)\n        {\n            throw new NotImplementedException(\"No Requirement Factory found for \" + typeof(T));\n        }\n\n        var policyDetails = await policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, policyType);\n        var policyDetailGroups = policyDetails.GroupBy(policyDetail => policyDetail.UserId);\n        var requirements = new List<T>();\n\n        foreach (var policyDetailGroup in policyDetailGroups)\n        {\n            var filteredPolicies = policyDetailGroup\n                .Where(factory.Enforce)\n                // Prevent deferred execution from causing inconsistent tests.\n                .ToList();\n\n            requirements.Add(factory.Create(filteredPolicies));\n        }\n\n        return requirements;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/PolicyValidatorHelpers.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic static class PolicyValidatorHelpers\n{\n    /// <summary>\n    /// Validate that given Member Decryption Options are not enabled.\n    /// Used for validation when disabling a policy that is required by certain Member Decryption Options.\n    /// </summary>\n    /// <param name=\"decryptionOptions\">The Member Decryption Options that require the policy to be enabled.</param>\n    /// <returns>A validation error if validation was unsuccessful, otherwise an empty string</returns>\n    public static string ValidateDecryptionOptionsNotEnabled(this SsoConfig? ssoConfig,\n        MemberDecryptionType[] decryptionOptions)\n    {\n        if (ssoConfig is not { Enabled: true })\n        {\n            return \"\";\n        }\n\n        return ssoConfig.GetData().MemberDecryptionType switch\n        {\n            MemberDecryptionType.KeyConnector when decryptionOptions.Contains(MemberDecryptionType.KeyConnector)\n                => \"Key Connector is enabled and requires this policy.\",\n            MemberDecryptionType.TrustedDeviceEncryption when decryptionOptions.Contains(MemberDecryptionType\n                .TrustedDeviceEncryption) => \"Trusted device encryption is on and requires this policy.\",\n            _ => \"\"\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidator.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class RequireSsoPolicyValidator : IPolicyValidator, IPolicyValidationEvent, IEnforceDependentPoliciesEvent\n{\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n\n    public RequireSsoPolicyValidator(ISsoConfigRepository ssoConfigRepository)\n    {\n        _ssoConfigRepository = ssoConfigRepository;\n    }\n\n    public PolicyType Type => PolicyType.RequireSso;\n    public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];\n\n    public async Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)\n    {\n        return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy);\n    }\n\n    public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        if (policyUpdate is not { Enabled: true })\n        {\n            var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);\n            return ssoConfig.ValidateDecryptionOptionsNotEnabled([\n                MemberDecryptionType.KeyConnector,\n                MemberDecryptionType.TrustedDeviceEncryption\n            ]);\n        }\n\n        return \"\";\n    }\n\n    public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidator.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class ResetPasswordPolicyValidator : IPolicyValidator, IPolicyValidationEvent, IEnforceDependentPoliciesEvent\n{\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    public PolicyType Type => PolicyType.ResetPassword;\n    public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];\n\n    public ResetPasswordPolicyValidator(ISsoConfigRepository ssoConfigRepository)\n    {\n        _ssoConfigRepository = ssoConfigRepository;\n    }\n\n    public async Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)\n    {\n        return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy);\n    }\n\n    public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        if (policyUpdate is not { Enabled: true } ||\n            policyUpdate.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled == false)\n        {\n            var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);\n            return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);\n        }\n\n        return \"\";\n    }\n\n    public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent, IOnPolicyPreUpdateEvent\n{\n    public PolicyType Type => PolicyType.SingleOrg;\n    private const string OrganizationNotFoundErrorMessage = \"Organization not found.\";\n    private const string ClaimedDomainSingleOrganizationRequiredErrorMessage = \"The Single organization policy is required for organizations that have enabled domain verification.\";\n\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IMailService _mailService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;\n    private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;\n\n    public SingleOrgPolicyValidator(\n        IOrganizationUserRepository organizationUserRepository,\n        IMailService mailService,\n        IOrganizationRepository organizationRepository,\n        ISsoConfigRepository ssoConfigRepository,\n        ICurrentContext currentContext,\n        IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,\n        IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _mailService = mailService;\n        _organizationRepository = organizationRepository;\n        _ssoConfigRepository = ssoConfigRepository;\n        _currentContext = currentContext;\n        _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;\n        _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;\n    }\n\n    public IEnumerable<PolicyType> RequiredPolicies => [];\n\n    public async Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)\n    {\n        return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy);\n    }\n\n    public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy)\n    {\n        await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy);\n    }\n\n    public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })\n        {\n            var currentUser = _currentContext.UserId ?? Guid.Empty;\n            var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);\n            await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));\n        }\n    }\n\n    private async Task RevokeNonCompliantUsersAsync(Guid organizationId, IActingUser performedBy)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization is null)\n        {\n            throw new NotFoundException(OrganizationNotFoundErrorMessage);\n        }\n\n        var currentActiveRevocableOrganizationUsers =\n            (await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId))\n            .Where(ou => ou.Status != OrganizationUserStatusType.Invited &&\n                         ou.Status != OrganizationUserStatusType.Revoked &&\n                         ou.Type != OrganizationUserType.Owner &&\n                         ou.Type != OrganizationUserType.Admin &&\n                         !(performedBy is StandardUser stdUser && stdUser.UserId == ou.UserId))\n            .ToList();\n\n        if (currentActiveRevocableOrganizationUsers.Count == 0)\n        {\n            return;\n        }\n\n        var allRevocableUserOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(\n            currentActiveRevocableOrganizationUsers.Select(ou => ou.UserId!.Value));\n        var usersToRevoke = currentActiveRevocableOrganizationUsers.Where(ou =>\n            allRevocableUserOrgs.Any(uo => uo.UserId == ou.UserId &&\n                uo.OrganizationId != organizationId &&\n                uo.Status != OrganizationUserStatusType.Invited)).ToList();\n\n        var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(\n            new RevokeOrganizationUsersRequest(organizationId, usersToRevoke, performedBy));\n\n        if (commandResult.HasErrors)\n        {\n            throw new BadRequestException(string.Join(\", \", commandResult.ErrorMessages));\n        }\n\n        await Task.WhenAll(usersToRevoke.Select(x =>\n            _mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));\n    }\n\n    public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        if (policyUpdate is not { Enabled: true })\n        {\n            var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);\n\n            var validateDecryptionErrorMessage = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);\n\n            if (!string.IsNullOrWhiteSpace(validateDecryptionErrorMessage))\n            {\n                return validateDecryptionErrorMessage;\n            }\n\n            if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))\n            {\n                return ClaimedDomainSingleOrganizationRequiredErrorMessage;\n            }\n        }\n\n        return string.Empty;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class TwoFactorAuthenticationPolicyValidator : IPolicyValidator, IOnPolicyPreUpdateEvent\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IMailService _mailService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;\n\n    public const string NonCompliantMembersWillLoseAccessMessage = \"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.\";\n\n    public PolicyType Type => PolicyType.TwoFactorAuthentication;\n    public IEnumerable<PolicyType> RequiredPolicies => [];\n\n    public TwoFactorAuthenticationPolicyValidator(\n        IOrganizationUserRepository organizationUserRepository,\n        IMailService mailService,\n        IOrganizationRepository organizationRepository,\n        ICurrentContext currentContext,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _mailService = mailService;\n        _organizationRepository = organizationRepository;\n        _currentContext = currentContext;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;\n    }\n\n    public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy)\n    {\n        await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy);\n    }\n\n    public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })\n        {\n            var currentUser = _currentContext.UserId ?? Guid.Empty;\n            var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);\n            await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));\n        }\n    }\n\n    private async Task RevokeNonCompliantUsersAsync(Guid organizationId, IActingUser performedBy)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization is null)\n        {\n            return;\n        }\n\n        var currentActiveRevocableOrganizationUsers =\n            (await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId))\n            .Where(ou => ou.Status != OrganizationUserStatusType.Invited &&\n                         ou.Status != OrganizationUserStatusType.Revoked &&\n                         ou.Type != OrganizationUserType.Owner &&\n                         ou.Type != OrganizationUserType.Admin &&\n                         !(performedBy is StandardUser stdUser && stdUser.UserId == ou.UserId))\n            .ToList();\n\n        if (currentActiveRevocableOrganizationUsers.Count == 0)\n        {\n            return;\n        }\n\n        var revocableUsersWithTwoFactorStatus =\n            await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(currentActiveRevocableOrganizationUsers);\n\n        var nonCompliantUsers = revocableUsersWithTwoFactorStatus\n            .Where(x => !x.twoFactorIsEnabled)\n            .ToArray();\n\n        if (nonCompliantUsers.Length == 0)\n        {\n            return;\n        }\n\n        if (MembersWithNoMasterPasswordWillLoseAccess(currentActiveRevocableOrganizationUsers, nonCompliantUsers))\n        {\n            throw new BadRequestException(NonCompliantMembersWillLoseAccessMessage);\n        }\n\n        var commandResult = await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(\n            new RevokeOrganizationUsersRequest(organizationId, nonCompliantUsers.Select(x => x.user), performedBy));\n\n        if (commandResult.HasErrors)\n        {\n            throw new BadRequestException(string.Join(\", \", commandResult.ErrorMessages));\n        }\n\n        await Task.WhenAll(nonCompliantUsers.Select(nonCompliantUser =>\n            _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), nonCompliantUser.user.Email)));\n    }\n\n    private static bool MembersWithNoMasterPasswordWillLoseAccess(\n        IEnumerable<OrganizationUserUserDetails> orgUserDetails,\n        IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>\n            orgUserDetails.Any(x =>\n                !x.HasMasterPassword && !organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == x.Id)\n                    .isTwoFactorEnabled);\n\n    public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(\"\");\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/UriMatchDefaultPolicyValidator.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class UriMatchDefaultPolicyValidator : IPolicyValidator, IEnforceDependentPoliciesEvent\n{\n    public PolicyType Type => PolicyType.UriMatchDefaults;\n    public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];\n    public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(\"\");\n    public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.CompletedTask;\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/OrganizationFeatures/Shared/Authorization/OrganizationScope.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;\n\n/// <summary>\n/// A typed wrapper for an organization Guid. This is used for authorization checks\n/// scoped to an organization's resources (e.g. all users for an organization).\n/// In these cases, AuthorizationService needs more than just a Guid, but we also don't want to fetch the\n/// Organization object from the database each time when it's usually not needed.\n/// This should not be used for operations on the organization itself.\n/// It implicitly converts to a regular Guid.\n/// </summary>\npublic record OrganizationScope\n{\n    public OrganizationScope(Guid id)\n    {\n        Id = id;\n    }\n    private Guid Id { get; }\n    public static implicit operator Guid(OrganizationScope organizationScope) =>\n        organizationScope.Id;\n    public override string ToString() => Id.ToString();\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Providers/Interfaces/ICreateProviderCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.AdminConsole.Providers.Interfaces;\n\npublic interface ICreateProviderCommand\n{\n    Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);\n    Task CreateResellerAsync(Provider provider);\n    Task CreateBusinessUnitAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Providers/Interfaces/IRemoveOrganizationFromProviderCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\n\nnamespace Bit.Core.AdminConsole.Providers.Interfaces;\n\npublic interface IRemoveOrganizationFromProviderCommand\n{\n    Task RemoveOrganizationFromProvider(\n        Provider provider,\n        ProviderOrganization providerOrganization,\n        Organization organization);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Repositories/IGroupRepository.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Repositories;\n\npublic interface IGroupRepository : IRepository<Group, Guid>\n{\n    Task<Tuple<Group?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);\n    Task<ICollection<Group>> GetManyByOrganizationIdAsync(Guid organizationId);\n    Task<ICollection<Tuple<Group, ICollection<CollectionAccessSelection>>>> GetManyWithCollectionsByOrganizationIdAsync(\n        Guid organizationId);\n    Task<ICollection<Group>> GetManyByManyIds(IEnumerable<Guid> groupIds);\n    Task<ICollection<Guid>> GetManyIdsByUserIdAsync(Guid organizationUserId);\n    /// <summary>\n    /// Query all OrganizationUserIds who are a member of the specified group.\n    /// </summary>\n    /// <param name=\"id\">The group id.</param>\n    /// <param name=\"useReadOnlyReplica\">\n    /// Whether to use the high-availability database replica. This is for paths with high traffic where immediate data\n    /// consistency is not required. You generally do not want this.\n    /// </param>\n    /// <returns></returns>\n    Task<ICollection<Guid>> GetManyUserIdsByIdAsync(Guid id, bool useReadOnlyReplica = false);\n    Task<ICollection<GroupUser>> GetManyGroupUsersByOrganizationIdAsync(Guid organizationId);\n    Task CreateAsync(Group obj, IEnumerable<CollectionAccessSelection> collections);\n    Task ReplaceAsync(Group obj, IEnumerable<CollectionAccessSelection> collections);\n    Task DeleteUserAsync(Guid groupId, Guid organizationUserId);\n    /// <summary>\n    /// Update a group's members. Replaces all members currently in the group.\n    /// Ignores members that do not belong to the same organization as the group.\n    /// </summary>\n    Task UpdateUsersAsync(Guid groupId, IEnumerable<Guid> organizationUserIds);\n    /// <summary>\n    /// Add members to a group. Gracefully ignores members that are already in the group,\n    /// duplicate organizationUserIds, and organizationUsers who are not part of the organization.\n    /// </summary>\n    Task AddGroupUsersByIdAsync(Guid groupId, IEnumerable<Guid> organizationUserIds);\n    Task DeleteManyAsync(IEnumerable<Guid> groupIds);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Repositories/IOrganizationRepository.cs",
    "content": "﻿using System.Data.Common;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IOrganizationRepository : IRepository<Organization, Guid>\n{\n    Task<Organization?> GetByGatewayCustomerIdAsync(string gatewayCustomerId);\n    Task<Organization?> GetByGatewaySubscriptionIdAsync(string gatewaySubscriptionId);\n    Task<Organization?> GetByIdentifierAsync(string identifier);\n    Task<ICollection<Organization>> GetManyByEnabledAsync();\n    Task<ICollection<Organization>> GetManyByUserIdAsync(Guid userId);\n    Task<ICollection<Organization>> SearchAsync(string name, string userEmail, bool? paid, int skip, int take);\n    Task UpdateStorageAsync(Guid id);\n    Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync();\n    Task<Organization?> GetByLicenseKeyAsync(string licenseKey);\n    Task<SelfHostedOrganizationDetails?> GetSelfHostedOrganizationDetailsById(Guid id);\n    Task<ICollection<Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take);\n    Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);\n\n    /// <summary>\n    /// Gets the organizations that have claimed the user's account. Currently, only one organization may claim a user.\n    /// This requires that the organization has claimed the user's domain and the user is an organization member.\n    /// It excludes invited members.\n    /// </summary>\n    Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);\n\n    Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);\n    Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids);\n\n    /// <summary>\n    /// Returns the number of occupied seats for an organization.\n    /// OrganizationUsers occupy a seat, unless they are revoked.\n    /// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an\n    /// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The ID of the organization to get the occupied seat count for.</param>\n    /// <returns>The number of occupied seats for the organization.</returns>\n    Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);\n\n    /// <summary>\n    /// Get all organizations that need to have their seat count updated to their Stripe subscription.\n    /// </summary>\n    /// <returns>Organizations to sync to Stripe</returns>\n    Task<IEnumerable<Organization>> GetOrganizationsForSubscriptionSyncAsync();\n\n    /// <summary>\n    /// Updates the organization SeatSync property to signify the organization's subscription has been updated in stripe\n    /// to match the password manager seats for the organization.\n    /// </summary>\n    /// <param name=\"successfulOrganizations\"></param>\n    /// <param name=\"syncDate\"></param>\n    /// <returns></returns>\n    Task UpdateSuccessfulOrganizationSyncStatusAsync(IEnumerable<Guid> successfulOrganizations, DateTime syncDate);\n\n    /// <summary>\n    /// This increments the password manager seat count on the organization by the provided amount and sets SyncSeats to true.\n    /// It also sets the revision date using the request date.\n    /// </summary>\n    /// <param name=\"organizationId\">Organization to update</param>\n    /// <param name=\"increaseAmount\">Amount to increase password manager seats by</param>\n    /// <param name=\"requestDate\">When the action was performed</param>\n    /// <returns></returns>\n    Task IncrementSeatCountAsync(Guid organizationId, int increaseAmount, DateTime requestDate);\n\n    /// <summary>\n    /// Atomically initializes a pending organization and confirms its first owner user\n    /// within a single transaction. Both updates succeed or fail together.\n    /// </summary>\n    /// <param name=\"organization\">The organization entity with updated properties (enabled, keys, status)</param>\n    /// <param name=\"confirmOwnerAction\">Action to confirm the organization owner, obtained from\n    /// <see cref=\"IOrganizationUserRepository.BuildConfirmOwnerAction\"/></param>\n    Task InitializeOrganizationAsync(Organization organization, Func<DbConnection, DbTransaction, Task> confirmOwnerAction);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs",
    "content": "﻿using System.Data.Common;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IOrganizationUserRepository : IRepository<OrganizationUser, Guid>\n{\n    Task<int> GetCountByOrganizationIdAsync(Guid organizationId);\n    Task<int> GetCountByFreeOrganizationAdminUserAsync(Guid userId);\n    Task<int> GetCountByOnlyOwnerAsync(Guid userId);\n    Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);\n    Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);\n    Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);\n    Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);\n    Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);\n    Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);\n    Task<OrganizationUserUserDetails?> GetDetailsByIdAsync(Guid id);\n    /// <summary>\n    /// Returns the OrganizationUser and its associated collections (excluding DefaultUserCollections).\n    /// </summary>\n    /// <param name=\"id\">The id of the OrganizationUser</param>\n    /// <returns>A tuple containing the OrganizationUser and its associated collections</returns>\n    Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithSharedCollectionsAsync(Guid id);\n    /// <summary>\n    /// Returns the OrganizationUsers and their associated collections (excluding DefaultUserCollections).\n    /// </summary>\n    /// <param name=\"organizationId\">The id of the organization</param>\n    /// <param name=\"includeGroups\">Whether to include groups</param>\n    /// <param name=\"includeSharedCollections\">Whether to include shared collections</param>\n    /// <returns>A list of OrganizationUserUserDetails</returns>\n    Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeSharedCollections = false);\n    /// <inheritdoc cref=\"GetManyDetailsByOrganizationAsync\"/>\n    /// <remarks>\n    /// This method is optimized for performance.\n    /// Reduces database round trips by fetching all data in fewer queries.\n    /// </remarks>\n    Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups = false, bool includeSharedCollections = false);\n    Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,\n        OrganizationUserStatusType? status = null);\n    Task<OrganizationUserOrganizationDetails?> GetDetailsByUserAsync(Guid userId, Guid organizationId,\n        OrganizationUserStatusType? status = null);\n    Task UpdateGroupsAsync(Guid orgUserId, IEnumerable<Guid> groupIds);\n    Task UpsertManyAsync(IEnumerable<OrganizationUser> organizationUsers);\n    Task<Guid> CreateAsync(OrganizationUser obj, IEnumerable<CollectionAccessSelection> collections);\n    Task<ICollection<Guid>?> CreateManyAsync(IEnumerable<OrganizationUser> organizationIdUsers);\n    Task ReplaceAsync(OrganizationUser obj, IEnumerable<CollectionAccessSelection> collections);\n    Task ReplaceManyAsync(IEnumerable<OrganizationUser> organizationUsers);\n    Task<ICollection<OrganizationUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds);\n    Task<ICollection<OrganizationUser>> GetManyAsync(IEnumerable<Guid> Ids);\n    Task DeleteManyAsync(IEnumerable<Guid> userIds);\n    Task<OrganizationUser?> GetByOrganizationEmailAsync(Guid organizationId, string email);\n    Task<IEnumerable<OrganizationUserPublicKey>> GetManyPublicKeysByOrganizationUserAsync(Guid organizationId, IEnumerable<Guid> Ids);\n    Task<IEnumerable<OrganizationUserUserDetails>> GetManyByMinimumRoleAsync(Guid organizationId, OrganizationUserType minRole);\n    Task RevokeAsync(Guid id);\n    Task RestoreAsync(Guid id, OrganizationUserStatusType status);\n    Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType);\n    Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId);\n    Task<IEnumerable<OrganizationUserResetPasswordDetails>> GetManyAccountRecoveryDetailsByOrganizationUserAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds);\n\n    /// <summary>\n    /// Updates encrypted data for organization users during a key rotation\n    /// </summary>\n    /// <param name=\"userId\">The user that initiated the key rotation</param>\n    /// <param name=\"resetPasswordKeys\">A list of organization users with updated reset password keys</param>\n    UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,\n        IEnumerable<OrganizationUser> resetPasswordKeys);\n\n    /// <summary>\n    /// Returns a list of OrganizationUsers with email domains that match one of the Organization's claimed domains.\n    /// </summary>\n    Task<ICollection<OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId);\n    Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds);\n\n    /// <summary>\n    /// Returns a list of OrganizationUsersUserDetails with the specified role.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization to search within</param>\n    /// <param name=\"role\">The role to search for</param>\n    /// <returns>A list of OrganizationUsersUserDetails with the specified role</returns>\n    Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role);\n\n    Task CreateManyAsync(IEnumerable<CreateOrganizationUser> organizationUserCollection);\n\n    /// <summary>\n    /// It will only confirm if the user is in the `Accepted` state.\n    ///\n    /// This is an idempotent operation.\n    /// </summary>\n    /// <param name=\"organizationUserToConfirm\">Accepted OrganizationUser to confirm</param>\n    /// <returns>True, if the user was updated. False, if not performed.</returns>\n    Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm);\n\n    /// <summary>\n    /// Returns the OrganizationUserUserDetails if found.\n    /// </summary>\n    /// <param name=\"organizationId\">The id of the organization</param>\n    /// <param name=\"userId\">The id of the User to fetch</param>\n    /// <returns>OrganizationUserUserDetails of the specified user or null if not found</returns>\n    /// <remarks>\n    /// Similar to GetByOrganizationAsync, but returns the user details.\n    /// </remarks>\n    Task<OrganizationUserUserDetails?> GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId);\n\n    /// <summary>\n    /// Builds an action that confirms the organization owner within a shared transaction.\n    /// The returned action is intended to be passed to\n    /// <see cref=\"IOrganizationRepository.InitializeOrganizationAsync\"/> to execute atomically\n    /// alongside the organization update.\n    /// </summary>\n    /// <param name=\"organizationUser\">The organization user entity with updated properties (status, userId, key)</param>\n    /// <returns>An action that can be executed within a transaction</returns>\n    Func<DbConnection, DbTransaction, Task> BuildConfirmOwnerAction(OrganizationUser organizationUser);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Repositories/IPolicyRepository.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Repositories;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Repositories;\n\npublic interface IPolicyRepository : IRepository<Policy, Guid>\n{\n    /// <summary>\n    /// Gets all policies of a given type for an organization.\n    /// </summary>\n    /// <remarks>\n    /// WARNING: do not use this to enforce policies against a user! It returns raw data and does not take into account\n    /// various business rules. Use <see cref=\"IPolicyRequirementQuery\"/> instead.\n    /// </remarks>\n    Task<Policy?> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type);\n    Task<ICollection<Policy>> GetManyByOrganizationIdAsync(Guid organizationId);\n    Task<ICollection<Policy>> GetManyByUserIdAsync(Guid userId);\n\n    /// <summary>\n    /// Retrieves <see cref=\"OrganizationPolicyDetails\"/> of the specified <paramref name=\"policyType\"/>\n    /// for users in the given organization and for any other organizations those users belong to.\n    /// </summary>\n    /// <remarks>\n    /// Each PolicyDetail represents an OrganizationUser and a Policy which *may* be enforced\n    /// against them. It only returns PolicyDetails for policies that are enabled and where the organization's plan\n    /// supports policies. It also excludes \"revoked invited\" users who are not subject to policy enforcement.\n    /// This is consumed by <see cref=\"IPolicyRequirementQuery\"/> to create requirements for specific policy types.\n    /// You probably do not want to call it directly.\n    /// </remarks>\n    Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType);\n\n    /// <summary>\n    /// Retrieves policy details for a list of users filtered by the specified policy type.\n    /// </summary>\n    /// <param name=\"userIds\">A collection of user identifiers for which the policy details are to be fetched.</param>\n    /// <param name=\"policyType\">The type of policy for which the details are required.</param>\n    /// <returns>\n    /// An asynchronous task that returns a collection of <see cref=\"OrganizationPolicyDetails\"/> objects containing the policy information\n    /// associated with the specified users and policy type.\n    /// </returns>\n    Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByUserIdsAndPolicyType(IEnumerable<Guid> userIds, PolicyType policyType);\n\n    /// <summary>\n    /// Retrieves policy details for a single user filtered by the specified policy type.\n    /// </summary>\n    /// <remarks>\n    /// Returns policy details only for enabled policies from enabled organizations that support policies.\n    /// This includes both confirmed users (matched by UserId) and invited users (matched by email).\n    /// Provider users are identified via the IsProvider flag.\n    /// </remarks>\n    /// <param name=\"userId\">The user identifier for which policy details are to be fetched.</param>\n    /// <param name=\"policyType\">The type of policy for which the details are required.</param>\n    /// <returns>\n    /// An asynchronous task that returns a collection of <see cref=\"PolicyDetails\"/> objects containing\n    /// the policy information associated with the specified user and policy type.\n    /// </returns>\n    Task<IEnumerable<PolicyDetails>> GetPolicyDetailsByUserIdAndPolicyTypeAsync(Guid userId, PolicyType policyType);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Repositories/IProviderOrganizationRepository.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Repositories;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Repositories;\n\npublic interface IProviderOrganizationRepository : IRepository<ProviderOrganization, Guid>\n{\n    Task<ICollection<ProviderOrganization>?> CreateManyAsync(IEnumerable<ProviderOrganization> providerOrganizations);\n    Task<ICollection<ProviderOrganizationOrganizationDetails>> GetManyDetailsByProviderAsync(Guid providerId);\n    Task<ProviderOrganization?> GetByOrganizationId(Guid organizationId);\n    Task<IEnumerable<ProviderOrganizationProviderDetails>> GetManyByUserAsync(Guid userId);\n    Task<int> GetCountByOrganizationIdsAsync(IEnumerable<Guid> organizationIds);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Repositories/IProviderRepository.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Repositories;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Repositories;\n\npublic interface IProviderRepository : IRepository<Provider, Guid>\n{\n    Task<Provider?> GetByGatewayCustomerIdAsync(string gatewayCustomerId);\n    Task<Provider?> GetByGatewaySubscriptionIdAsync(string gatewaySubscriptionId);\n    Task<Provider?> GetByOrganizationIdAsync(Guid organizationId);\n    Task<ICollection<Provider>> SearchAsync(string name, string userEmail, int skip, int take);\n    Task<ICollection<ProviderAbility>> GetManyAbilitiesAsync();\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Repositories/IProviderUserRepository.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Repositories;\n\n#nullable enable\n\nnamespace Bit.Core.AdminConsole.Repositories;\n\npublic interface IProviderUserRepository : IRepository<ProviderUser, Guid>\n{\n    Task<int> GetCountByProviderAsync(Guid providerId, string email, bool onlyRegisteredUsers);\n    Task<ICollection<ProviderUser>> GetManyAsync(IEnumerable<Guid> ids);\n    Task<ICollection<ProviderUser>> GetManyByUserAsync(Guid userId);\n    Task<ICollection<ProviderUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds);\n    Task<ProviderUser?> GetByProviderUserAsync(Guid providerId, Guid userId);\n    Task<ICollection<ProviderUser>> GetManyByProviderAsync(Guid providerId, ProviderUserType? type = null);\n    Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status = null);\n    Task<ICollection<ProviderUserProviderDetails>> GetManyDetailsByUserAsync(Guid userId,\n        ProviderUserStatusType? status = null);\n    Task<IEnumerable<ProviderUserOrganizationDetails>> GetManyOrganizationDetailsByUserAsync(Guid userId, ProviderUserStatusType? status = null);\n    Task DeleteManyAsync(IEnumerable<Guid> userIds);\n    Task<IEnumerable<ProviderUserPublicKey>> GetManyPublicKeysByProviderUserAsync(Guid providerId, IEnumerable<Guid> Ids);\n    Task<int> GetCountByOnlyOwnerAsync(Guid userId);\n    Task<ICollection<ProviderUser>> GetManyByOrganizationAsync(Guid organizationId, ProviderUserStatusType? status = null);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/IEventService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Interfaces;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Services;\n\npublic interface IEventService\n{\n    Task LogUserEventAsync(Guid userId, EventType type, DateTime? date = null);\n    Task LogCipherEventAsync(Cipher cipher, EventType type, DateTime? date = null);\n    Task LogCipherEventsAsync(IEnumerable<Tuple<Cipher, EventType, DateTime?>> events);\n    Task LogCollectionEventAsync(Collection collection, EventType type, DateTime? date = null);\n    Task LogCollectionEventsAsync(IEnumerable<(Collection collection, EventType type, DateTime? date)> events);\n    Task LogGroupEventAsync(Group group, EventType type, DateTime? date = null);\n    Task LogGroupEventAsync(Group group, EventType type, EventSystemUser systemUser, DateTime? date = null);\n    Task LogGroupEventsAsync(IEnumerable<(Group group, EventType type, EventSystemUser? systemUser, DateTime? date)> events);\n    Task LogPolicyEventAsync(Policy policy, EventType type, DateTime? date = null);\n    Task LogOrganizationUserEventAsync<T>(T organizationUser, EventType type, DateTime? date = null) where T : IOrganizationUser;\n    Task LogOrganizationUserEventAsync<T>(T organizationUser, EventType type, EventSystemUser systemUser, DateTime? date = null) where T : IOrganizationUser;\n    Task LogOrganizationUserEventsAsync<T>(IEnumerable<(T, EventType, DateTime?)> events) where T : IOrganizationUser;\n    Task LogOrganizationUserEventsAsync<T>(IEnumerable<(T, EventType, EventSystemUser, DateTime?)> events) where T : IOrganizationUser;\n    Task LogOrganizationEventAsync(Organization organization, EventType type, DateTime? date = null);\n    Task LogOrganizationEventAsync(Organization organization, EventType type, EventSystemUser systemUser, DateTime? date = null);\n    Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null);\n    Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events);\n    Task LogProviderOrganizationEventAsync(ProviderOrganization providerOrganization, EventType type, DateTime? date = null);\n    Task LogProviderOrganizationEventsAsync(IEnumerable<(ProviderOrganization, EventType, DateTime?)> events);\n    Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, DateTime? date = null);\n    Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null);\n    Task LogUserSecretsEventAsync(Guid userId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null);\n    Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null);\n    Task LogUserProjectsEventAsync(Guid userId, IEnumerable<Project> projects, EventType type, DateTime? date = null);\n    Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable<Project> projects, EventType type, DateTime? date = null);\n    Task LogServiceAccountPeopleEventAsync(Guid userId, UserServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null);\n    Task LogServiceAccountGroupEventAsync(Guid userId, GroupServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null);\n    Task LogServiceAccountEventAsync(Guid userId, List<ServiceAccount> serviceAccount, EventType type, IdentityClientType identityClientType, DateTime? date = null);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/IGroupService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.Services;\n\npublic interface IGroupService\n{\n    [Obsolete(\"IDeleteGroupCommand should be used instead. To be removed by EC-608.\")]\n    Task DeleteAsync(Group group);\n    [Obsolete(\"IDeleteGroupCommand should be used instead. To be removed by EC-608.\")]\n    Task DeleteAsync(Group group, EventSystemUser systemUser);\n    Task DeleteUserAsync(Group group, Guid organizationUserId);\n    Task DeleteUserAsync(Group group, Guid organizationUserId, EventSystemUser systemUser);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/IOrganizationDomainService.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Services;\n\npublic interface IOrganizationDomainService\n{\n    Task ValidateOrganizationsDomainAsync();\n    Task OrganizationDomainMaintenanceAsync();\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/IOrganizationService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.Services;\n\npublic interface IOrganizationService\n{\n    Task ReinstateSubscriptionAsync(Guid organizationId);\n    Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb);\n    Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats);\n    Task AutoAddSeatsAsync(Organization organization, int seatsToAdd);\n    Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment);\n    Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate);\n    Task UpdateAsync(Organization organization, bool updateBilling = false);\n    Task<Organization> UpdateCollectionManagementSettingsAsync(Guid organizationId, OrganizationCollectionManagementSettings settings);\n    Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);\n    Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);\n    Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,\n        OrganizationUserInvite invite, string externalId);\n    Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,\n        IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);\n    Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);\n    Task DeleteSsoUserAsync(Guid userId, Guid? organizationId);\n    Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);\n    Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd);\n\n    void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);\n    void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);\n    Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType,\n        OrganizationUserType? oldType, Permissions permissions);\n    Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId, OrganizationUserType newType);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/IPolicyService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Core.AdminConsole.Services;\n\npublic interface IPolicyService\n{\n    /// <summary>\n    /// Get the combined master password policy options for the specified user.\n    /// </summary>\n    Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user);\n    Task<ICollection<OrganizationUserPolicyDetails>> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted);\n    Task<bool> AnyPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/IProviderService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Business.Provider;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.AdminConsole.Services;\n\npublic interface IProviderService\n{\n    Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress);\n    Task UpdateAsync(Provider provider, bool updateBilling = false);\n\n    Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite);\n    Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(ProviderUserInvite<Guid> invite);\n    Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token);\n    Task<List<Tuple<ProviderUser, string>>> ConfirmUsersAsync(Guid providerId, Dictionary<Guid, string> keys, Guid confirmingUserId);\n\n    Task SaveUserAsync(ProviderUser user, Guid savingUserId);\n    Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds,\n        Guid deletingUserId);\n\n    Task AddOrganization(Guid providerId, Guid organizationId, string key);\n    Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds);\n    Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId, OrganizationSignup organizationSignup,\n        string clientOwnerEmail, User user);\n    Task LogProviderAccessToOrganizationAsync(Guid organizationId);\n    Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId);\n    Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail);\n    Task InitiateDeleteAsync(Provider provider, string providerAdminEmail);\n    Task DeleteAsync(Provider provider, string token);\n    Task DeleteAsync(Provider provider);\n}\n\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/Implementations/GroupService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.AdminConsole.Services.Implementations;\n\npublic class GroupService : IGroupService\n{\n    private readonly IEventService _eventService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IGroupRepository _groupRepository;\n\n    public GroupService(\n        IEventService eventService,\n        IOrganizationUserRepository organizationUserRepository,\n        IGroupRepository groupRepository)\n    {\n        _eventService = eventService;\n        _organizationUserRepository = organizationUserRepository;\n        _groupRepository = groupRepository;\n    }\n\n    [Obsolete(\"IDeleteGroupCommand should be used instead. To be removed by EC-608.\")]\n    public async Task DeleteAsync(Group group)\n    {\n        await _groupRepository.DeleteAsync(group);\n        await _eventService.LogGroupEventAsync(group, EventType.Group_Deleted);\n    }\n\n    [Obsolete(\"IDeleteGroupCommand should be used instead. To be removed by EC-608.\")]\n    public async Task DeleteAsync(Group group, EventSystemUser systemUser)\n    {\n        await _groupRepository.DeleteAsync(group);\n        await _eventService.LogGroupEventAsync(group, EventType.Group_Deleted, systemUser);\n    }\n\n    public async Task DeleteUserAsync(Group group, Guid organizationUserId)\n    {\n        var orgUser = await GroupRepositoryDeleteUserAsync(group, organizationUserId, systemUser: null);\n        await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_UpdatedGroups);\n    }\n\n    public async Task DeleteUserAsync(Group group, Guid organizationUserId, EventSystemUser systemUser)\n    {\n        var orgUser = await GroupRepositoryDeleteUserAsync(group, organizationUserId, systemUser);\n        await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_UpdatedGroups, systemUser);\n    }\n\n    private async Task<OrganizationUser> GroupRepositoryDeleteUserAsync(Group group, Guid organizationUserId, EventSystemUser? systemUser)\n    {\n        var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);\n        if (orgUser == null || orgUser.OrganizationId != group.OrganizationId)\n        {\n            throw new NotFoundException();\n        }\n\n        await _groupRepository.DeleteUserAsync(group.Id, organizationUserId);\n\n        return orgUser;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.AdminConsole.Services.Implementations;\n\npublic class OrganizationDomainService : IOrganizationDomainService\n{\n    private readonly IOrganizationDomainRepository _domainRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IEventService _eventService;\n    private readonly IMailService _mailService;\n    private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand;\n    private readonly TimeProvider _timeProvider;\n    private readonly ILogger<OrganizationDomainService> _logger;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IFeatureService _featureService;\n\n    public OrganizationDomainService(\n        IOrganizationDomainRepository domainRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IEventService eventService,\n        IMailService mailService,\n        IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,\n        TimeProvider timeProvider,\n        ILogger<OrganizationDomainService> logger,\n        IGlobalSettings globalSettings,\n        IFeatureService featureService)\n    {\n        _domainRepository = domainRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _eventService = eventService;\n        _mailService = mailService;\n        _verifyOrganizationDomainCommand = verifyOrganizationDomainCommand;\n        _timeProvider = timeProvider;\n        _logger = logger;\n        _globalSettings = globalSettings;\n        _featureService = featureService;\n    }\n\n    public async Task ValidateOrganizationsDomainAsync()\n    {\n        //Date should be set 1 hour behind to ensure it selects all domains that should be verified\n        var runDate = _timeProvider.GetUtcNow().UtcDateTime.AddHours(-1);\n\n        var verifiableDomains = await _domainRepository.GetManyByNextRunDateAsync(runDate);\n\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Validating {verifiableDomainsCount} domains.\", verifiableDomains.Count);\n\n        foreach (var domain in verifiableDomains)\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId,\n                \"Attempting verification for organization {OrgId} with domain {Domain}\",\n                domain.OrganizationId,\n                domain.DomainName);\n\n            try\n            {\n                _ = await _verifyOrganizationDomainCommand.SystemVerifyOrganizationDomainAsync(domain);\n            }\n            catch (Exception ex)\n            {\n                domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);\n                await _domainRepository.ReplaceAsync(domain);\n\n                await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_NotVerified,\n                    EventSystemUser.DomainVerification);\n\n                _logger.LogError(ex, \"Verification for organization {OrgId} with domain {Domain} threw an exception: {errorMessage}\",\n                    domain.OrganizationId, domain.DomainName, ex.Message);\n            }\n        }\n    }\n\n    public async Task OrganizationDomainMaintenanceAsync()\n    {\n        try\n        {\n            //Get domains that have not been verified within 72 hours\n            var expiredDomains = await _domainRepository.GetExpiredOrganizationDomainsAsync();\n\n            _logger.LogInformation(Constants.BypassFiltersEventId,\n                \"Attempting email reminder for {expiredDomainCount} expired domains.\", expiredDomains.Count);\n\n            foreach (var domain in expiredDomains)\n            {\n                //get admin emails of organization\n                var adminEmails = await GetAdminEmailsAsync(domain.OrganizationId);\n\n                //Send email to administrators\n                if (adminEmails.Count > 0)\n                {\n                    await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,\n                        domain.OrganizationId.ToString(), domain.DomainName);\n                }\n\n                _logger.LogInformation(Constants.BypassFiltersEventId, \"Expired domain: {domainName}\", domain.DomainName);\n            }\n            // Delete domains that have not been verified within 7 days\n            var status = await _domainRepository.DeleteExpiredAsync(_globalSettings.DomainVerification.ExpirationPeriod);\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Delete status {status}\", status);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Organization domain maintenance failed\");\n        }\n    }\n\n    private async Task<List<string>> GetAdminEmailsAsync(Guid organizationId)\n    {\n        var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);\n        var emailList = orgUsers.Where(o => o.Type <= OrganizationUserType.Admin\n                                        || o.GetPermissions()?.ManageSso == true)\n            .Select(a => a.Email).Distinct().ToList();\n\n        return emailList;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/Implementations/OrganizationService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\nusing OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;\n\nnamespace Bit.Core.Services;\n\npublic class OrganizationService : IOrganizationService\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IMailService _mailService;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IEventService _eventService;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IStripePaymentService _paymentService;\n    private readonly IPolicyQuery _policyQuery;\n    private readonly IPolicyService _policyService;\n    private readonly ISsoUserRepository _ssoUserRepository;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly ICurrentContext _currentContext;\n    private readonly ILogger<OrganizationService> _logger;\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;\n    private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IFeatureService _featureService;\n    private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;\n    private readonly IPricingClient _pricingClient;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;\n    private readonly IStripeAdapter _stripeAdapter;\n    private readonly IUpdateOrganizationSubscriptionCommand _updateOrganizationSubscriptionCommand;\n\n    public OrganizationService(\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IGroupRepository groupRepository,\n        IMailService mailService,\n        IPushNotificationService pushNotificationService,\n        IEventService eventService,\n        IApplicationCacheService applicationCacheService,\n        IStripePaymentService paymentService,\n        IPolicyQuery policyQuery,\n        IPolicyService policyService,\n        ISsoUserRepository ssoUserRepository,\n        IGlobalSettings globalSettings,\n        ICurrentContext currentContext,\n        ILogger<OrganizationService> logger,\n        IProviderOrganizationRepository providerOrganizationRepository,\n        IProviderUserRepository providerUserRepository,\n        ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,\n        IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,\n        IProviderRepository providerRepository,\n        IFeatureService featureService,\n        IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,\n        IPricingClient pricingClient,\n        IPolicyRequirementQuery policyRequirementQuery,\n        ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,\n        IStripeAdapter stripeAdapter, IUpdateOrganizationSubscriptionCommand updateOrganizationSubscriptionCommand)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _groupRepository = groupRepository;\n        _mailService = mailService;\n        _pushNotificationService = pushNotificationService;\n        _eventService = eventService;\n        _applicationCacheService = applicationCacheService;\n        _paymentService = paymentService;\n        _policyQuery = policyQuery;\n        _policyService = policyService;\n        _ssoUserRepository = ssoUserRepository;\n        _globalSettings = globalSettings;\n        _currentContext = currentContext;\n        _logger = logger;\n        _providerOrganizationRepository = providerOrganizationRepository;\n        _providerUserRepository = providerUserRepository;\n        _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;\n        _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;\n        _providerRepository = providerRepository;\n        _featureService = featureService;\n        _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;\n        _pricingClient = pricingClient;\n        _policyRequirementQuery = policyRequirementQuery;\n        _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;\n        _stripeAdapter = stripeAdapter;\n        _updateOrganizationSubscriptionCommand = updateOrganizationSubscriptionCommand;\n    }\n\n    public async Task ReinstateSubscriptionAsync(Guid organizationId)\n    {\n        var organization = await GetOrgById(organizationId);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _paymentService.ReinstateSubscriptionAsync(organization);\n    }\n\n    public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)\n    {\n        var organization = await GetOrgById(organizationId);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        if (!plan.PasswordManager.HasAdditionalStorageOption)\n        {\n            throw new BadRequestException(\"Plan does not allow additional storage.\");\n        }\n\n        var secret = await BillingHelpers.AdjustStorageAsync(\n            _paymentService,\n            _updateOrganizationSubscriptionCommand,\n            _featureService,\n            organization,\n            storageAdjustmentGb,\n            plan.PasswordManager.StripeStoragePlanId,\n            plan.PasswordManager.BaseStorageGb);\n        await ReplaceAndUpdateCacheAsync(organization);\n        return secret;\n    }\n\n    public async Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats)\n    {\n        var organization = await GetOrgById(organizationId);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var newSeatCount = organization.Seats + seatAdjustment;\n        if (maxAutoscaleSeats.HasValue && newSeatCount > maxAutoscaleSeats.Value)\n        {\n            throw new BadRequestException(\"Cannot set max seat autoscaling below seat count.\");\n        }\n\n        if (seatAdjustment != 0)\n        {\n            await AdjustSeatsAsync(organization, seatAdjustment);\n        }\n\n        if (maxAutoscaleSeats != organization.MaxAutoscaleSeats)\n        {\n            await UpdateAutoscalingAsync(organization, maxAutoscaleSeats);\n        }\n    }\n\n    private async Task UpdateAutoscalingAsync(Organization organization, int? maxAutoscaleSeats)\n    {\n        if (maxAutoscaleSeats.HasValue &&\n            organization.Seats.HasValue &&\n            maxAutoscaleSeats.Value < organization.Seats.Value)\n        {\n            throw new BadRequestException($\"Cannot set max seat autoscaling below current seat count.\");\n        }\n\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n        if (plan == null)\n        {\n            throw new BadRequestException(\"Existing plan not found.\");\n        }\n\n        if (!plan.PasswordManager.AllowSeatAutoscale)\n        {\n            throw new BadRequestException(\"Your plan does not allow seat autoscaling.\");\n        }\n\n        if (plan.PasswordManager.MaxSeats.HasValue && maxAutoscaleSeats.HasValue &&\n            maxAutoscaleSeats > plan.PasswordManager.MaxSeats)\n        {\n            throw new BadRequestException(string.Concat(\n                $\"Your plan has a seat limit of {plan.PasswordManager.MaxSeats}, \",\n                $\"but you have specified a max autoscale count of {maxAutoscaleSeats}.\",\n                \"Reduce your max autoscale seat count.\"));\n        }\n\n        organization.MaxAutoscaleSeats = maxAutoscaleSeats;\n\n        await ReplaceAndUpdateCacheAsync(organization);\n    }\n\n    public async Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment)\n    {\n        var organization = await GetOrgById(organizationId);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return await AdjustSeatsAsync(organization, seatAdjustment);\n    }\n\n    private async Task<string> AdjustSeatsAsync(Organization organization, int seatAdjustment,\n        IEnumerable<string> ownerEmails = null)\n    {\n        if (organization.Seats == null)\n        {\n            throw new BadRequestException(\"Organization has no seat limit, no need to adjust seats\");\n        }\n\n        if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))\n        {\n            throw new BadRequestException(\"No payment method found.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))\n        {\n            throw new BadRequestException(\"No subscription found.\");\n        }\n\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        if (!plan.PasswordManager.HasAdditionalSeatsOption)\n        {\n            throw new BadRequestException(\"Plan does not allow additional seats.\");\n        }\n\n        var newSeatTotal = organization.Seats.Value + seatAdjustment;\n        if (plan.PasswordManager.BaseSeats > newSeatTotal)\n        {\n            throw new BadRequestException($\"Plan has a minimum of {plan.PasswordManager.BaseSeats} seats.\");\n        }\n\n        if (newSeatTotal <= 0)\n        {\n            throw new BadRequestException(\"You must have at least 1 seat.\");\n        }\n\n        var additionalSeats = newSeatTotal - plan.PasswordManager.BaseSeats;\n        if (plan.PasswordManager.MaxAdditionalSeats.HasValue &&\n            additionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)\n        {\n            throw new BadRequestException($\"Organization plan allows a maximum of \" +\n                                          $\"{plan.PasswordManager.MaxAdditionalSeats.Value} additional seats.\");\n        }\n\n        if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)\n        {\n            var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);\n\n            if (seatCounts.Total > newSeatTotal)\n            {\n                if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)\n                {\n                    throw new BadRequestException(\n                        $\"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. \" +\n                        $\"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.\");\n                }\n                else\n                {\n                    throw new BadRequestException($\"Your organization currently has {seatCounts.Total} seats filled. \" +\n                                                  $\"Your new plan only has ({newSeatTotal}) seats. Remove some users.\");\n                }\n            }\n        }\n\n        if (organization.UseSecretsManager && organization.Seats + seatAdjustment < organization.SmSeats)\n        {\n            throw new BadRequestException(\"You cannot have more Secrets Manager seats than Password Manager seats.\");\n        }\n\n        _logger.LogInformation(\"{Method}: Invoking _paymentService.AdjustSeatsAsync with {AdditionalSeats} additional seats for Organization ({OrganizationID})\",\n            nameof(AdjustSeatsAsync), additionalSeats, organization.Id);\n\n        string paymentIntentClientSecret = null;\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand))\n        {\n            var changeSet = OrganizationSubscriptionChangeSet.UpdatePasswordManagerSeats(plan, additionalSeats);\n            var result = await _updateOrganizationSubscriptionCommand.Run(organization, changeSet);\n            result.GetValueOrThrow();\n        }\n        else\n        {\n            paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats);\n        }\n\n        organization.Seats = (short?)newSeatTotal;\n\n        _logger.LogInformation(\"{Method}: Invoking _organizationRepository.ReplaceAsync with {Seats} seats for Organization ({OrganizationID})\", nameof(AdjustSeatsAsync), organization.Seats, organization.Id); ;\n\n        await ReplaceAndUpdateCacheAsync(organization);\n\n        if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue &&\n            organization.Seats == organization.MaxAutoscaleSeats)\n        {\n            try\n            {\n                if (ownerEmails == null)\n                {\n                    ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,\n                        OrganizationUserType.Owner)).Select(u => u.Email).Distinct();\n                }\n\n                await _mailService.SendOrganizationMaxSeatLimitReachedEmailAsync(organization,\n                    organization.MaxAutoscaleSeats.Value, ownerEmails);\n            }\n            catch (Exception e)\n            {\n                _logger.LogError(e, \"Error encountered notifying organization owners of seat limit reached.\");\n            }\n        }\n\n        return paymentIntentClientSecret;\n    }\n\n    public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate)\n    {\n        var org = await GetOrgById(organizationId);\n        if (org != null)\n        {\n            org.ExpirationDate = expirationDate;\n            org.RevisionDate = DateTime.UtcNow;\n            await ReplaceAndUpdateCacheAsync(org);\n        }\n    }\n\n    public async Task UpdateAsync(Organization organization, bool updateBilling = false)\n    {\n        if (organization.Id == default(Guid))\n        {\n            throw new ApplicationException(\"Cannot create org this way. Call SignUpAsync.\");\n        }\n\n        if (!string.IsNullOrWhiteSpace(organization.Identifier))\n        {\n            var orgById = await _organizationRepository.GetByIdentifierAsync(organization.Identifier);\n            if (orgById != null && orgById.Id != organization.Id)\n            {\n                throw new BadRequestException(\"Identifier already in use by another organization.\");\n            }\n        }\n\n        await ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated);\n\n        if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))\n        {\n            var newDisplayName = organization.DisplayName();\n\n            await _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId,\n                new CustomerUpdateOptions\n                {\n                    Email = organization.BillingEmail,\n                    Description = organization.DisplayBusinessName(),\n                    InvoiceSettings = new CustomerInvoiceSettingsOptions\n                    {\n                        // This overwrites the existing custom fields for this organization\n                        CustomFields = [\n                            new CustomerInvoiceSettingsCustomFieldOptions\n                            {\n                                Name = organization.SubscriberType(),\n                                Value = newDisplayName.Length <= 30\n                                    ? newDisplayName\n                                    : newDisplayName[..30]\n                            }]\n                    },\n                });\n        }\n    }\n\n    public async Task<Organization> UpdateCollectionManagementSettingsAsync(Guid organizationId, OrganizationCollectionManagementSettings settings)\n    {\n        var existingOrganization = await _organizationRepository.GetByIdAsync(organizationId);\n        if (existingOrganization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        // Create logging actions based on what will change\n        var loggingActions = CreateCollectionManagementLoggingActions(existingOrganization, settings);\n\n        existingOrganization.LimitCollectionCreation = settings.LimitCollectionCreation;\n        existingOrganization.LimitCollectionDeletion = settings.LimitCollectionDeletion;\n        existingOrganization.LimitItemDeletion = settings.LimitItemDeletion;\n        existingOrganization.AllowAdminAccessToAllCollectionItems = settings.AllowAdminAccessToAllCollectionItems;\n        existingOrganization.RevisionDate = DateTime.UtcNow;\n\n        await ReplaceAndUpdateCacheAsync(existingOrganization);\n\n        if (loggingActions.Any())\n        {\n            await Task.WhenAll(loggingActions.Select(action => action()));\n        }\n\n        await _pushNotificationService.PushSyncOrganizationCollectionManagementSettingsAsync(existingOrganization);\n\n        return existingOrganization;\n    }\n\n    public async Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type)\n    {\n        if (!type.ToString().Contains(\"Organization\"))\n        {\n            throw new ArgumentException(\"Not an organization provider type.\");\n        }\n\n        if (!organization.Use2fa)\n        {\n            throw new BadRequestException(\"Organization cannot use 2FA.\");\n        }\n\n        var providers = organization.GetTwoFactorProviders();\n        if (providers is null || !providers.TryGetValue(type, out var provider))\n        {\n            return;\n        }\n\n        provider.Enabled = true;\n        organization.SetTwoFactorProviders(providers);\n        await UpdateAsync(organization);\n    }\n\n    public async Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type)\n    {\n        if (!type.ToString().Contains(\"Organization\"))\n        {\n            throw new ArgumentException(\"Not an organization provider type.\");\n        }\n\n        var providers = organization.GetTwoFactorProviders();\n        if (!providers?.ContainsKey(type) ?? true)\n        {\n            return;\n        }\n\n        providers.Remove(type);\n        organization.SetTwoFactorProviders(providers);\n        await UpdateAsync(organization);\n    }\n\n    public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId,\n        EventSystemUser? systemUser,\n        OrganizationUserInvite invite, string externalId)\n    {\n        // Ideally OrganizationUserInvite should represent a single user so that this doesn't have to be a runtime check\n        if (invite.Emails.Count() > 1)\n        {\n            throw new BadRequestException(\"This method can only be used to invite a single user.\");\n        }\n\n        // Validate Collection associations\n        var invalidAssociations = invite.Collections?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));\n        if (invalidAssociations?.Any() ?? false)\n        {\n            throw new BadRequestException(\n                \"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.\");\n        }\n\n        var results = await InviteUsersAsync(organizationId, invitingUserId, systemUser,\n            new (OrganizationUserInvite, string)[] { (invite, externalId) });\n\n        var result = results.FirstOrDefault();\n        if (result == null)\n        {\n            throw new BadRequestException(\"This user has already been invited.\");\n        }\n\n        return result;\n    }\n\n    /// <summary>\n    /// Invite users to an organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization Id</param>\n    /// <param name=\"invitingUserId\">The current authenticated user who is sending the invite. Only used when inviting via a client app; null if using SCIM or Public API.</param>\n    /// <param name=\"systemUser\">The system user which is sending the invite. Only used when inviting via SCIM; null if using a client app or Public API</param>\n    /// <param name=\"invites\">Details about the users being invited</param>\n    /// <returns></returns>\n    public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,\n        EventSystemUser? systemUser,\n        IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)\n    {\n        var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)\n            .Select(i => i.invite.Type.Value));\n\n        // If authenticating via a client app, verify the inviting user has permissions\n        // cf. SCIM and Public API have superuser permissions here\n        if (invitingUserId.HasValue && inviteTypes.Count > 0)\n        {\n            foreach (var (invite, _) in invites)\n            {\n                await ValidateOrganizationUserUpdatePermissions(organizationId, invite.Type.Value, null,\n                    invite.Permissions);\n                await ValidateOrganizationCustomPermissionsEnabledAsync(organizationId, invite.Type.Value);\n            }\n        }\n\n        var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites, invitingUserId);\n\n        if (systemUser.HasValue)\n        {\n            // Log SCIM event\n            await _eventService.LogOrganizationUserEventsAsync(events.Select(e =>\n                (e.Item1, e.Item2, systemUser.Value, e.Item3)));\n        }\n        else\n        {\n            // Log client app or Public Api event\n            await _eventService.LogOrganizationUserEventsAsync(events);\n        }\n\n        return organizationUsers;\n    }\n\n    private async\n        Task<(List<OrganizationUser> organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)>\n        SaveUsersSendInvitesAsync(Guid organizationId,\n            IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,\n            Guid? invitingUserId)\n    {\n        var organization = await GetOrgById(organizationId);\n        var initialSeatCount = organization.Seats;\n        if (organization == null || invites.Any(i => i.invite.Emails == null))\n        {\n            throw new NotFoundException();\n        }\n\n        var existingEmails = new HashSet<string>(await _organizationUserRepository.SelectKnownEmailsAsync(\n                organizationId, invites.SelectMany(i => i.invite.Emails), false),\n            StringComparer.InvariantCultureIgnoreCase);\n\n        // Seat autoscaling\n        var initialSmSeatCount = organization.SmSeats;\n        var newSeatsRequired = 0;\n        if (organization.Seats.HasValue)\n        {\n            var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);\n            var availableSeats = organization.Seats.Value - seatCounts.Total;\n            newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;\n        }\n\n        if (newSeatsRequired > 0)\n        {\n            var (canScale, failureReason) = await CanScaleAsync(organization, newSeatsRequired);\n            if (!canScale)\n            {\n                throw new BadRequestException(failureReason);\n            }\n        }\n\n        // Secrets Manager seat autoscaling\n        SecretsManagerSubscriptionUpdate smSubscriptionUpdate = null;\n        var inviteWithSmAccessCount = invites\n            .Where(i => i.invite.AccessSecretsManager)\n            .SelectMany(i => i.invite.Emails)\n            .Count(email => !existingEmails.Contains(email));\n\n        var additionalSmSeatsRequired =\n            await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount);\n        if (additionalSmSeatsRequired > 0)\n        {\n            var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n            smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, plan, true)\n                .AdjustSeats(additionalSmSeatsRequired);\n        }\n\n        var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner);\n        if (!invitedAreAllOwners &&\n            !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { },\n                includeProvider: true))\n        {\n            throw new BadRequestException(\"Organization must have at least one confirmed owner.\");\n        }\n\n        var orgUsersWithoutCollections = new List<OrganizationUser>();\n        var orgUsersWithCollections = new List<(OrganizationUser, IEnumerable<CollectionAccessSelection>)>();\n        var orgUserGroups = new List<(OrganizationUser, IEnumerable<Guid>)>();\n        var orgUserInvitedCount = 0;\n        var exceptions = new List<Exception>();\n        var events = new List<(OrganizationUser, EventType, DateTime?)>();\n        foreach (var (invite, externalId) in invites)\n        {\n            // Prevent duplicate invitations\n            foreach (var email in invite.Emails.Distinct())\n            {\n                try\n                {\n                    // Make sure user is not already invited\n                    if (existingEmails.Contains(email))\n                    {\n                        continue;\n                    }\n\n                    var orgUser = new OrganizationUser\n                    {\n                        OrganizationId = organizationId,\n                        UserId = null,\n                        Email = email.ToLowerInvariant(),\n                        Key = null,\n                        Type = invite.Type.Value,\n                        Status = OrganizationUserStatusType.Invited,\n                        AccessSecretsManager = invite.AccessSecretsManager,\n                        ExternalId = externalId,\n                        CreationDate = DateTime.UtcNow,\n                        RevisionDate = DateTime.UtcNow,\n                    };\n\n                    if (invite.Type == OrganizationUserType.Custom)\n                    {\n                        orgUser.SetPermissions(invite.Permissions ?? new Permissions());\n                    }\n\n                    if (invite.Collections.Any())\n                    {\n                        orgUsersWithCollections.Add((orgUser, invite.Collections));\n                    }\n                    else\n                    {\n                        orgUsersWithoutCollections.Add(orgUser);\n                    }\n\n                    if (invite.Groups != null && invite.Groups.Any())\n                    {\n                        orgUserGroups.Add((orgUser, invite.Groups));\n                    }\n\n                    events.Add((orgUser, EventType.OrganizationUser_Invited, DateTime.UtcNow));\n                    orgUserInvitedCount++;\n                }\n                catch (Exception e)\n                {\n                    exceptions.Add(e);\n                }\n            }\n        }\n\n        if (exceptions.Any())\n        {\n            throw new AggregateException(\"One or more errors occurred while inviting users.\", exceptions);\n        }\n\n        var allOrgUsers = orgUsersWithoutCollections\n            .Concat(orgUsersWithCollections.Select(u => u.Item1))\n            .ToList();\n\n        try\n        {\n            await _organizationUserRepository.CreateManyAsync(orgUsersWithoutCollections);\n            foreach (var (orgUser, collections) in orgUsersWithCollections)\n            {\n                await _organizationUserRepository.CreateAsync(orgUser, collections);\n            }\n\n            foreach (var (orgUser, groups) in orgUserGroups)\n            {\n                await _organizationUserRepository.UpdateGroupsAsync(orgUser.Id, groups);\n            }\n\n            if (!await _currentContext.ManageUsers(organization.Id))\n            {\n                throw new BadRequestException(\"Cannot add seats. Cannot manage organization users.\");\n            }\n\n            await AutoAddSeatsAsync(organization, newSeatsRequired);\n\n            if (additionalSmSeatsRequired > 0)\n            {\n                await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdate);\n            }\n\n            await SendInvitesAsync(allOrgUsers, organization, invitingUserId);\n        }\n        catch (Exception e)\n        {\n            // Revert any added users.\n            var invitedOrgUserIds = allOrgUsers.Select(ou => ou.Id);\n            await _organizationUserRepository.DeleteManyAsync(invitedOrgUserIds);\n            var currentOrganization = await _organizationRepository.GetByIdAsync(organization.Id);\n\n            // Revert autoscaling\n            // Do this first so that SmSeats never exceed PM seats (due to current billing requirements)\n            if (initialSmSeatCount.HasValue && currentOrganization.SmSeats.HasValue &&\n                currentOrganization.SmSeats.Value != initialSmSeatCount.Value)\n            {\n                var plan = await _pricingClient.GetPlanOrThrow(currentOrganization.PlanType);\n                var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(currentOrganization, plan, false)\n                {\n                    SmSeats = initialSmSeatCount.Value\n                };\n                await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdateRevert);\n            }\n\n            if (initialSeatCount.HasValue && currentOrganization.Seats.HasValue &&\n                currentOrganization.Seats.Value != initialSeatCount.Value)\n            {\n                await AdjustSeatsAsync(organization, initialSeatCount.Value - currentOrganization.Seats.Value);\n            }\n\n            exceptions.Add(e);\n        }\n\n        if (exceptions.Any())\n        {\n            throw new AggregateException(\"One or more errors occurred while inviting users.\", exceptions);\n        }\n\n        return (allOrgUsers, events);\n    }\n\n    private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization, Guid? invitingUserId = null) =>\n        await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(orgUsers, organization, initOrganization: false, invitingUserId: invitingUserId));\n\n    private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization, Guid? invitingUserId = null) =>\n        await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(\n            users: [orgUser],\n            organization: organization,\n            initOrganization: initOrganization,\n            invitingUserId: invitingUserId));\n\n    public async Task<(bool canScale, string failureReason)> CanScaleAsync(\n        Organization organization,\n        int seatsToAdd)\n    {\n        var failureReason = \"\";\n        if (_globalSettings.SelfHosted)\n        {\n            failureReason = \"Cannot autoscale on self-hosted instance.\";\n            return (false, failureReason);\n        }\n\n        if (seatsToAdd < 1)\n        {\n            return (true, failureReason);\n        }\n\n        var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);\n\n        if (provider is { Enabled: true })\n        {\n            if (provider.IsBillable())\n            {\n                return (false, \"Seat limit has been reached. Please contact your provider to add more seats.\");\n            }\n\n            if (provider.Type == ProviderType.Reseller)\n            {\n                return (false, \"Seat limit has been reached. Contact your provider to purchase additional seats.\");\n            }\n        }\n\n        var subscription = await _paymentService.GetSubscriptionAsync(organization);\n        if (subscription?.Subscription?.Status == StripeConstants.SubscriptionStatus.Canceled)\n        {\n            return (false, \"You do not have an active subscription. Reinstate your subscription to make changes\");\n        }\n\n        if (organization.Seats.HasValue &&\n            organization.MaxAutoscaleSeats.HasValue &&\n            organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd)\n        {\n            return (false, $\"Seat limit has been reached.\");\n        }\n\n        return (true, failureReason);\n    }\n\n    public async Task AutoAddSeatsAsync(Organization organization, int seatsToAdd)\n    {\n        if (seatsToAdd < 1 || !organization.Seats.HasValue)\n        {\n            return;\n        }\n\n        var (canScale, failureMessage) = await CanScaleAsync(organization, seatsToAdd);\n        if (!canScale)\n        {\n            throw new BadRequestException(failureMessage);\n        }\n\n        var providerOrg = await this._providerOrganizationRepository.GetByOrganizationId(organization.Id);\n\n        IEnumerable<string> ownerEmails;\n        if (providerOrg != null)\n        {\n            ownerEmails =\n                (await _providerUserRepository.GetManyDetailsByProviderAsync(providerOrg.ProviderId,\n                    ProviderUserStatusType.Confirmed))\n                .Select(u => u.Email).Distinct();\n        }\n        else\n        {\n            ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,\n                OrganizationUserType.Owner)).Select(u => u.Email).Distinct();\n        }\n\n        var initialSeatCount = organization.Seats.Value;\n\n        await AdjustSeatsAsync(organization, seatsToAdd, ownerEmails);\n\n        if (!organization.OwnersNotifiedOfAutoscaling.HasValue)\n        {\n            await _mailService.SendOrganizationAutoscaledEmailAsync(organization, initialSeatCount,\n                ownerEmails);\n            organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;\n            await _organizationRepository.UpsertAsync(organization);\n        }\n    }\n\n\n    public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey,\n        Guid? callingUserId)\n    {\n        // Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);\n        if (!callingUserId.HasValue || orgUser == null || orgUser.UserId != callingUserId.Value ||\n            orgUser.OrganizationId != organizationId)\n        {\n            throw new BadRequestException(\"User not valid.\");\n        }\n\n        // Make sure the organization has the ability to use password reset\n        var org = await _organizationRepository.GetByIdAsync(organizationId);\n        if (org == null || !org.UseResetPassword)\n        {\n            throw new BadRequestException(\"Organization does not allow password reset enrollment.\");\n        }\n\n        // Make sure the organization has the policy enabled\n        var resetPasswordPolicy = await _policyQuery.RunAsync(organizationId, PolicyType.ResetPassword);\n        if (!resetPasswordPolicy.Enabled)\n        {\n            throw new BadRequestException(\"Organization does not have the password reset policy enabled.\");\n        }\n\n        // Block the user from withdrawal if auto enrollment is enabled\n        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))\n        {\n            var resetPasswordPolicyRequirement =\n                await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(userId);\n            if (resetPasswordKey == null && resetPasswordPolicyRequirement.AutoEnrollEnabled(organizationId))\n            {\n                throw new BadRequestException(\n                    \"Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.\");\n            }\n        }\n        else\n        {\n            if (resetPasswordKey == null && resetPasswordPolicy.Data != null)\n            {\n                var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data,\n                    JsonHelpers.IgnoreCase);\n\n                if (data?.AutoEnrollEnabled ?? false)\n                {\n                    throw new BadRequestException(\n                        \"Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.\");\n                }\n            }\n        }\n\n        orgUser.ResetPasswordKey = resetPasswordKey;\n        await _organizationUserRepository.ReplaceAsync(orgUser);\n        await _eventService.LogOrganizationUserEventAsync(orgUser,\n            resetPasswordKey != null\n                ? EventType.OrganizationUser_ResetPassword_Enroll\n                : EventType.OrganizationUser_ResetPassword_Withdraw);\n    }\n\n\n    public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)\n    {\n        await _ssoUserRepository.DeleteAsync(userId, organizationId);\n        if (organizationId.HasValue)\n        {\n            var organizationUser =\n                await _organizationUserRepository.GetByOrganizationAsync(organizationId.Value, userId);\n            if (organizationUser != null)\n            {\n                await _eventService.LogOrganizationUserEventAsync(organizationUser,\n                    EventType.OrganizationUser_UnlinkedSso);\n            }\n        }\n    }\n\n\n    public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)\n    {\n        try\n        {\n            await _organizationRepository.ReplaceAsync(org);\n            await _applicationCacheService.UpsertOrganizationAbilityAsync(org);\n\n            if (orgEvent.HasValue)\n            {\n                await _eventService.LogOrganizationEventAsync(org, orgEvent.Value);\n            }\n        }\n        catch (Exception exception)\n        {\n            _logger.LogError(exception, \"An error occurred while calling {Method} for Organization ({OrganizationID})\", nameof(ReplaceAndUpdateCacheAsync), org.Id);\n            throw;\n        }\n    }\n\n    private async Task<Organization> GetOrgById(Guid id)\n    {\n        return await _organizationRepository.GetByIdAsync(id);\n    }\n\n    private static void ValidatePlan(Models.StaticStore.Plan plan, int additionalSeats, string productType)\n    {\n        if (plan is null)\n        {\n            throw new BadRequestException($\"{productType} Plan was null.\");\n        }\n\n        if (plan.Disabled)\n        {\n            throw new BadRequestException($\"{productType} Plan not found.\");\n        }\n\n        if (additionalSeats < 0)\n        {\n            throw new BadRequestException($\"You can't subtract {productType} seats!\");\n        }\n    }\n\n    public void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)\n    {\n        ValidatePlan(plan, upgrade.AdditionalSeats, \"Password Manager\");\n\n        if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0)\n        {\n            throw new BadRequestException($\"You do not have any Password Manager seats!\");\n        }\n\n        if (upgrade.AdditionalSeats < 0)\n        {\n            throw new BadRequestException($\"You can't subtract Password Manager seats!\");\n        }\n\n        if (!plan.PasswordManager.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)\n        {\n            throw new BadRequestException(\"Plan does not allow additional storage.\");\n        }\n\n        if (upgrade.AdditionalStorageGb < 0)\n        {\n            throw new BadRequestException(\"You can't subtract storage!\");\n        }\n\n        if (!plan.PasswordManager.HasPremiumAccessOption && upgrade.PremiumAccessAddon)\n        {\n            throw new BadRequestException(\"This plan does not allow you to buy the premium access addon.\");\n        }\n\n        if (!plan.PasswordManager.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)\n        {\n            throw new BadRequestException(\"Plan does not allow additional users.\");\n        }\n\n        if (plan.PasswordManager.HasAdditionalSeatsOption && plan.PasswordManager.MaxAdditionalSeats.HasValue &&\n            upgrade.AdditionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)\n        {\n            throw new BadRequestException($\"Selected plan allows a maximum of \" +\n                                          $\"{plan.PasswordManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.\");\n        }\n    }\n\n    public void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)\n    {\n        if (plan.SupportsSecretsManager == false)\n        {\n            throw new BadRequestException(\"Invalid Secrets Manager plan selected.\");\n        }\n\n        ValidatePlan(plan, upgrade.AdditionalSmSeats.GetValueOrDefault(), \"Secrets Manager\");\n\n        if (plan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats <= 0)\n        {\n            throw new BadRequestException($\"You do not have any Secrets Manager seats!\");\n        }\n\n        if (!plan.SecretsManager.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccounts > 0)\n        {\n            throw new BadRequestException(\"Plan does not allow additional Machine Accounts.\");\n        }\n\n        if ((plan.ProductTier == ProductTierType.TeamsStarter &&\n             upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) ||\n            (plan.ProductTier != ProductTierType.TeamsStarter &&\n             upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats))\n        {\n            throw new BadRequestException(\"You cannot have more Secrets Manager seats than Password Manager seats.\");\n        }\n\n        if (upgrade.AdditionalServiceAccounts.GetValueOrDefault() < 0)\n        {\n            throw new BadRequestException(\"You can't subtract Machine Accounts!\");\n        }\n\n        switch (plan.SecretsManager.HasAdditionalSeatsOption)\n        {\n            case false when upgrade.AdditionalSmSeats > 0:\n                throw new BadRequestException(\"Plan does not allow additional users.\");\n            case true when plan.SecretsManager.MaxAdditionalSeats.HasValue &&\n                           upgrade.AdditionalSmSeats > plan.SecretsManager.MaxAdditionalSeats.Value:\n                throw new BadRequestException($\"Selected plan allows a maximum of \" +\n                                              $\"{plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.\");\n        }\n    }\n\n    public async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType,\n        OrganizationUserType? oldType, Permissions permissions)\n    {\n        if (await _currentContext.OrganizationOwner(organizationId))\n        {\n            return;\n        }\n\n        if (oldType == OrganizationUserType.Owner || newType == OrganizationUserType.Owner)\n        {\n            throw new BadRequestException(\"Only an Owner can configure another Owner's account.\");\n        }\n\n        if (await _currentContext.OrganizationAdmin(organizationId))\n        {\n            return;\n        }\n\n        if (!await _currentContext.ManageUsers(organizationId))\n        {\n            throw new BadRequestException(\"Your account does not have permission to manage users.\");\n        }\n\n        if (oldType == OrganizationUserType.Admin || newType == OrganizationUserType.Admin)\n        {\n            throw new BadRequestException(\"Custom users can not manage Admins or Owners.\");\n        }\n\n        if (newType == OrganizationUserType.Custom &&\n            !await ValidateCustomPermissionsGrant(organizationId, permissions))\n        {\n            throw new BadRequestException(\"Custom users can only grant the same custom permissions that they have.\");\n        }\n    }\n\n    public async Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId,\n        OrganizationUserType newType)\n    {\n        if (newType != OrganizationUserType.Custom)\n        {\n            return;\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!organization.UseCustomPermissions)\n        {\n            throw new BadRequestException(\n                \"To enable custom permissions the organization must be on an Enterprise plan.\");\n        }\n    }\n\n    private async Task<bool> ValidateCustomPermissionsGrant(Guid organizationId, Permissions permissions)\n    {\n        if (permissions == null || await _currentContext.OrganizationAdmin(organizationId))\n        {\n            return true;\n        }\n\n        if (permissions.ManageUsers && !await _currentContext.ManageUsers(organizationId))\n        {\n            return false;\n        }\n\n        if (permissions.AccessReports && !await _currentContext.AccessReports(organizationId))\n        {\n            return false;\n        }\n\n        if (permissions.ManageGroups && !await _currentContext.ManageGroups(organizationId))\n        {\n            return false;\n        }\n\n        if (permissions.ManagePolicies && !await _currentContext.ManagePolicies(organizationId))\n        {\n            return false;\n        }\n\n        if (permissions.ManageScim && !await _currentContext.ManageScim(organizationId))\n        {\n            return false;\n        }\n\n        if (permissions.ManageSso && !await _currentContext.ManageSso(organizationId))\n        {\n            return false;\n        }\n\n        if (permissions.AccessEventLogs && !await _currentContext.AccessEventLogs(organizationId))\n        {\n            return false;\n        }\n\n        if (permissions.AccessImportExport && !await _currentContext.AccessImportExport(organizationId))\n        {\n            return false;\n        }\n\n        if (permissions.EditAnyCollection && !await _currentContext.EditAnyCollection(organizationId))\n        {\n            return false;\n        }\n\n        if (permissions.ManageResetPassword && !await _currentContext.ManageResetPassword(organizationId))\n        {\n            return false;\n        }\n\n        var org = _currentContext.GetOrganization(organizationId);\n        if (org == null)\n        {\n            return false;\n        }\n\n        if (permissions.CreateNewCollections && !org.Permissions.CreateNewCollections)\n        {\n            return false;\n        }\n\n        if (permissions.DeleteAnyCollection && !org.Permissions.DeleteAnyCollection)\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)\n    {\n        // Determine status to revert back to\n        var status = OrganizationUserStatusType.Invited;\n        if (organizationUser.UserId.HasValue && string.IsNullOrWhiteSpace(organizationUser.Email))\n        {\n            // Has UserId & Email is null, then Accepted\n            status = OrganizationUserStatusType.Accepted;\n            if (!string.IsNullOrWhiteSpace(organizationUser.Key))\n            {\n                // We have an org key for this user, user was confirmed\n                status = OrganizationUserStatusType.Confirmed;\n            }\n        }\n\n        return status;\n    }\n\n    private List<Func<Task>> CreateCollectionManagementLoggingActions(\n        Organization existingOrganization, OrganizationCollectionManagementSettings settings)\n    {\n        var loggingActions = new List<Func<Task>>();\n\n        if (existingOrganization.LimitCollectionCreation != settings.LimitCollectionCreation)\n        {\n            var eventType = settings.LimitCollectionCreation\n                ? EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled\n                : EventType.Organization_CollectionManagement_LimitCollectionCreationDisabled;\n            loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType));\n        }\n\n        if (existingOrganization.LimitCollectionDeletion != settings.LimitCollectionDeletion)\n        {\n            var eventType = settings.LimitCollectionDeletion\n                ? EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled\n                : EventType.Organization_CollectionManagement_LimitCollectionDeletionDisabled;\n            loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType));\n        }\n\n        if (existingOrganization.LimitItemDeletion != settings.LimitItemDeletion)\n        {\n            var eventType = settings.LimitItemDeletion\n                ? EventType.Organization_CollectionManagement_LimitItemDeletionEnabled\n                : EventType.Organization_CollectionManagement_LimitItemDeletionDisabled;\n            loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType));\n        }\n\n        if (existingOrganization.AllowAdminAccessToAllCollectionItems != settings.AllowAdminAccessToAllCollectionItems)\n        {\n            var eventType = settings.AllowAdminAccessToAllCollectionItems\n                ? EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled\n                : EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled;\n            loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType));\n        }\n\n        return loggingActions;\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/Implementations/PolicyService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.AdminConsole.Services.Implementations;\n\npublic class PolicyService : IPolicyService\n{\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IPolicyRepository _policyRepository;\n    private readonly GlobalSettings _globalSettings;\n\n    public PolicyService(\n        IApplicationCacheService applicationCacheService,\n        IOrganizationUserRepository organizationUserRepository,\n        IPolicyRepository policyRepository,\n        GlobalSettings globalSettings)\n    {\n        _applicationCacheService = applicationCacheService;\n        _organizationUserRepository = organizationUserRepository;\n        _policyRepository = policyRepository;\n        _globalSettings = globalSettings;\n    }\n\n    public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)\n    {\n        var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id))\n            .Where(p => p.Type == PolicyType.MasterPassword && p.Enabled)\n            .ToList();\n\n        if (!policies.Any())\n        {\n            return null;\n        }\n\n        var enforcedOptions = new MasterPasswordPolicyData();\n\n        foreach (var policy in policies)\n        {\n            enforcedOptions.CombineWith(policy.GetDataModel<MasterPasswordPolicyData>());\n        }\n\n        return enforcedOptions;\n\n    }\n\n    public async Task<ICollection<OrganizationUserPolicyDetails>> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)\n    {\n        var result = await QueryOrganizationUserPolicyDetailsAsync(userId, policyType, minStatus);\n        return result.ToList();\n    }\n\n    public async Task<bool> AnyPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)\n    {\n        var result = await QueryOrganizationUserPolicyDetailsAsync(userId, policyType, minStatus);\n        return result.Any();\n    }\n\n    private async Task<IEnumerable<OrganizationUserPolicyDetails>> QueryOrganizationUserPolicyDetailsAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)\n    {\n        var organizationUserPolicyDetails = await _organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(userId, policyType);\n        var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType);\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        return organizationUserPolicyDetails.Where(o =>\n            (!orgAbilities.TryGetValue(o.OrganizationId, out var orgAbility) || orgAbility.UsePolicies) &&\n            o.PolicyEnabled &&\n            !excludedUserTypes.Contains(o.OrganizationUserType) &&\n            o.OrganizationUserStatus >= minStatus &&\n            !o.IsProvider);\n    }\n\n    private OrganizationUserType[] GetUserTypesExcludedFromPolicy(PolicyType policyType)\n    {\n        switch (policyType)\n        {\n            case PolicyType.MasterPassword:\n                return Array.Empty<OrganizationUserType>();\n            case PolicyType.RequireSso:\n                // If 'EnforceSsoPolicyForAllUsers' is set to true then SSO policy applies to all user types otherwise it does not apply to Owner or Admin\n                if (_globalSettings.Sso.EnforceSsoPolicyForAllUsers)\n                {\n                    return Array.Empty<OrganizationUserType>();\n                }\n                break;\n        }\n\n        return new[] { OrganizationUserType.Owner, OrganizationUserType.Admin };\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Business.Provider;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.AdminConsole.Services.NoopImplementations;\n\npublic class NoopProviderService : IProviderService\n{\n    public Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) => throw new NotImplementedException();\n\n    public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException();\n\n    public Task<List<ProviderUser>> InviteUserAsync(ProviderUserInvite<string> invite) => throw new NotImplementedException();\n\n    public Task<List<Tuple<ProviderUser, string>>> ResendInvitesAsync(ProviderUserInvite<Guid> invite) => throw new NotImplementedException();\n\n    public Task<ProviderUser> AcceptUserAsync(Guid providerUserId, User user, string token) => throw new NotImplementedException();\n\n    public Task<List<Tuple<ProviderUser, string>>> ConfirmUsersAsync(Guid providerId, Dictionary<Guid, string> keys, Guid confirmingUserId) => throw new NotImplementedException();\n\n    public Task SaveUserAsync(ProviderUser user, Guid savingUserId) => throw new NotImplementedException();\n\n    public Task<List<Tuple<ProviderUser, string>>> DeleteUsersAsync(Guid providerId, IEnumerable<Guid> providerUserIds, Guid deletingUserId) => throw new NotImplementedException();\n\n    public Task AddOrganization(Guid providerId, Guid organizationId, string key) => throw new NotImplementedException();\n\n    public Task AddOrganizationsToReseller(Guid providerId, IEnumerable<Guid> organizationIds) => throw new NotImplementedException();\n\n    public Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId, OrganizationSignup organizationSignup, string clientOwnerEmail, User user) => throw new NotImplementedException();\n\n    public Task RemoveOrganizationAsync(Guid providerId, Guid providerOrganizationId, Guid removingUserId) => throw new NotImplementedException();\n\n    public Task LogProviderAccessToOrganizationAsync(Guid organizationId) => throw new NotImplementedException();\n\n    public Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid userId) => throw new NotImplementedException();\n    public Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail) => throw new NotImplementedException();\n    public Task InitiateDeleteAsync(Provider provider, string providerAdminEmail) => throw new NotImplementedException();\n    public Task DeleteAsync(Provider provider, string token) => throw new NotImplementedException();\n    public Task DeleteAsync(Provider provider) => throw new NotImplementedException();\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Services/OrganizationFactory.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Licenses;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.AdminConsole.Services;\n\npublic static class OrganizationFactory\n{\n    public static Organization Create(\n        User owner,\n        ClaimsPrincipal claimsPrincipal,\n        string publicKey,\n        string privateKey) => new()\n        {\n            Name = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.Name),\n            BillingEmail = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.BillingEmail),\n            BusinessName = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.BusinessName),\n            PlanType = claimsPrincipal.GetValue<PlanType>(OrganizationLicenseConstants.PlanType),\n            Seats = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.Seats),\n            MaxCollections = claimsPrincipal.GetValue<short?>(OrganizationLicenseConstants.MaxCollections),\n            MaxStorageGb = Constants.SelfHostedMaxStorageGb,\n            UsePolicies = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePolicies),\n            UseSso = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseSso),\n            UseKeyConnector = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseKeyConnector),\n            UseScim = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseScim),\n            UseGroups = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseGroups),\n            UseDirectory = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseDirectory),\n            UseEvents = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseEvents),\n            UseTotp = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseTotp),\n            Use2fa = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.Use2fa),\n            UseApi = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseApi),\n            UseResetPassword = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseResetPassword),\n            Plan = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.Plan),\n            SelfHost = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.SelfHost),\n            UsersGetPremium = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsersGetPremium),\n            UseCustomPermissions =\n            claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseCustomPermissions),\n            Gateway = null,\n            GatewayCustomerId = null,\n            GatewaySubscriptionId = null,\n            ReferenceData = owner.ReferenceData,\n            Enabled = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.Enabled),\n            ExpirationDate = claimsPrincipal.GetValue<DateTime?>(OrganizationLicenseConstants.Expires),\n            LicenseKey = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.LicenseKey),\n            PublicKey = publicKey,\n            PrivateKey = privateKey,\n            CreationDate = DateTime.UtcNow,\n            RevisionDate = DateTime.UtcNow,\n            Status = OrganizationStatusType.Created,\n            UsePasswordManager = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePasswordManager),\n            UseSecretsManager = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseSecretsManager),\n            SmSeats = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.SmSeats),\n            SmServiceAccounts = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.SmServiceAccounts),\n            UseRiskInsights = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseRiskInsights),\n            UseOrganizationDomains =\n            claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseOrganizationDomains),\n            UseAdminSponsoredFamilies =\n            claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAdminSponsoredFamilies),\n            UseAutomaticUserConfirmation = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAutomaticUserConfirmation),\n            UseDisableSmAdsForUsers =\n                claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseDisableSmAdsForUsers),\n            UsePhishingBlocker = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePhishingBlocker),\n            UseMyItems = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseMyItems),\n        };\n\n    public static Organization Create(\n        User owner,\n        OrganizationLicense license,\n        string publicKey,\n        string privateKey) => new()\n        {\n            Name = license.Name,\n            BillingEmail = license.BillingEmail,\n            BusinessName = license.BusinessName,\n            PlanType = license.PlanType,\n            Seats = license.Seats,\n            MaxCollections = license.MaxCollections,\n            MaxStorageGb = Constants.SelfHostedMaxStorageGb,\n            UsePolicies = license.UsePolicies,\n            UseSso = license.UseSso,\n            UseKeyConnector = license.UseKeyConnector,\n            UseScim = license.UseScim,\n            UseGroups = license.UseGroups,\n            UseDirectory = license.UseDirectory,\n            UseEvents = license.UseEvents,\n            UseTotp = license.UseTotp,\n            Use2fa = license.Use2fa,\n            UseApi = license.UseApi,\n            UseResetPassword = license.UseResetPassword,\n            Plan = license.Plan,\n            SelfHost = license.SelfHost,\n            UsersGetPremium = license.UsersGetPremium,\n            UseCustomPermissions = license.UseCustomPermissions,\n            Gateway = null,\n            GatewayCustomerId = null,\n            GatewaySubscriptionId = null,\n            ReferenceData = owner.ReferenceData,\n            Enabled = license.Enabled,\n            ExpirationDate = license.Expires,\n            LicenseKey = license.LicenseKey,\n            PublicKey = publicKey,\n            PrivateKey = privateKey,\n            CreationDate = DateTime.UtcNow,\n            RevisionDate = DateTime.UtcNow,\n            Status = OrganizationStatusType.Created,\n            UsePasswordManager = license.UsePasswordManager,\n            UseSecretsManager = license.UseSecretsManager,\n            SmSeats = license.SmSeats,\n            SmServiceAccounts = license.SmServiceAccounts,\n            UseRiskInsights = license.UseRiskInsights,\n            UseOrganizationDomains = license.UseOrganizationDomains,\n            UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies,\n            UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation,\n            UseDisableSmAdsForUsers = license.UseDisableSmAdsForUsers,\n            UsePhishingBlocker = license.UsePhishingBlocker,\n            UseMyItems = license.UseMyItems,\n        };\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/Commands/CommandResult.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Utilities.Errors;\nusing Bit.Core.AdminConsole.Utilities.Validation;\n\nnamespace Bit.Core.AdminConsole.Utilities.Commands;\n\npublic abstract class CommandResult<T>;\n\npublic class Success<T>(T value) : CommandResult<T>\n{\n    public T Value { get; } = value;\n}\n\npublic class Failure<T>(Error<T> error) : CommandResult<T>\n{\n    public Error<T> Error { get; } = error;\n}\n\npublic class Partial<T>(IEnumerable<T> successfulItems, IEnumerable<Error<T>> failedItems)\n    : CommandResult<T>\n{\n    public IEnumerable<T> Successes { get; } = successfulItems;\n    public IEnumerable<Error<T>> Failures { get; } = failedItems;\n}\n\npublic static class CommandResultExtensions\n{\n    /// <summary>\n    /// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types.\n    ///\n    /// </summary>\n    /// <param name=\"invalidResult\">This is the invalid type from validating the object.</param>\n    /// <param name=\"mappingFunction\">This function will map between the two types for the inner ErrorT</param>\n    /// <typeparam name=\"A\">Invalid object's type</typeparam>\n    /// <typeparam name=\"B\">Failure object's type</typeparam>\n    /// <returns></returns>\n    public static CommandResult<B> MapToFailure<A, B>(this Invalid<A> invalidResult, Func<A, B> mappingFunction) =>\n        new Failure<B>(invalidResult.Error.ToError(mappingFunction(invalidResult.Error.ErroredValue)));\n}\n\n[Obsolete(\"Use CommandResult<T> instead. This will be removed once old code is updated.\")]\npublic class CommandResult(IEnumerable<string> errors)\n{\n    public CommandResult(string error) : this([error]) { }\n\n    public bool Success => ErrorMessages.Count == 0;\n    public bool HasErrors => ErrorMessages.Count > 0;\n    public List<string> ErrorMessages { get; } = errors.ToList();\n    public CommandResult() : this(Array.Empty<string>()) { }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLogger.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Microsoft.Extensions.Logging;\nusing Quartz.Util;\n\nnamespace Bit.Core.AdminConsole.Utilities.DebuggingInstruments;\n\n/// <summary>\n/// Temporary code: Log warning when OrganizationUser is in an invalid state,\n/// so we can identify which flow is causing the issue through Datadog.\n/// </summary>\npublic static class UserInviteDebuggingLogger\n{\n    public static void LogUserInviteStateDiagnostics(this ILogger logger, OrganizationUser orgUser)\n    {\n        LogUserInviteStateDiagnostics(logger, [orgUser]);\n    }\n\n    public static void LogUserInviteStateDiagnostics(this ILogger logger, IEnumerable<OrganizationUser> allOrgUsers)\n    {\n        try\n        {\n            var invalidInviteState = allOrgUsers.Any(user => user.Status == OrganizationUserStatusType.Invited && user.Email.IsNullOrWhiteSpace());\n\n            if (invalidInviteState)\n            {\n                var logData = MapObjectDataToLog(allOrgUsers);\n                logger.LogWarning(\"Warning invalid invited state. {logData}\", logData);\n            }\n\n            var invalidConfirmedOrAcceptedState = allOrgUsers.Any(user => (user.Status == OrganizationUserStatusType.Confirmed || user.Status == OrganizationUserStatusType.Accepted) && !user.Email.IsNullOrWhiteSpace());\n\n            if (invalidConfirmedOrAcceptedState)\n            {\n                var logData = MapObjectDataToLog(allOrgUsers);\n                logger.LogWarning(\"Warning invalid confirmed or accepted state. {logData}\", logData);\n            }\n        }\n        catch (Exception exception)\n        {\n\n            // Ensure that this debugging instrument does not interfere with the current flow.\n            logger.LogWarning(exception, \"Unexpected exception from UserInviteDebuggingLogger\");\n        }\n    }\n\n    private static string MapObjectDataToLog(IEnumerable<OrganizationUser> allOrgUsers)\n    {\n        var log = allOrgUsers.Select(allOrgUser => new\n        {\n            allOrgUser.OrganizationId,\n            allOrgUser.Status,\n            hasEmail = !allOrgUser.Email.IsNullOrWhiteSpace(),\n            userId = allOrgUser.UserId,\n            allOrgUserId = allOrgUser.Id\n        });\n\n        var options = new JsonSerializerOptions\n        {\n            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n            WriteIndented = true\n        };\n\n        return JsonSerializer.Serialize(log, options);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/Errors/Error.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Utilities.Errors;\n\npublic record Error<T>(string Message, T ErroredValue);\n\npublic static class ErrorMappers\n{\n    public static Error<B> ToError<A, B>(this Error<A> errorA, B erroredValue) => new(errorA.Message, erroredValue);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Utilities.Errors;\n\npublic record InsufficientPermissionsError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)\n{\n    public const string Code = \"Insufficient Permissions\";\n\n    public InsufficientPermissionsError(T ErroredValue) : this(Code, ErroredValue)\n    {\n\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Utilities.Errors;\n\npublic record InvalidResultTypeError<T>(T Value) : Error<T>(Code, Value)\n{\n    public const string Code = \"Invalid result type.\";\n};\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Utilities.Errors;\n\npublic record RecordNotFoundError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)\n{\n    public const string Code = \"Record Not Found\";\n\n    public RecordNotFoundError(T ErroredValue) : this(Code, ErroredValue)\n    {\n\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs",
    "content": "﻿using System.Text.RegularExpressions;\n\nnamespace Bit.Core.AdminConsole.Utilities;\n\npublic static partial class IntegrationTemplateProcessor\n{\n    [GeneratedRegex(@\"#(\\w+)#\")]\n    private static partial Regex TokenRegex();\n\n    public static string ReplaceTokens(string template, object values)\n    {\n        if (string.IsNullOrEmpty(template))\n        {\n            return template;\n        }\n        var type = values.GetType();\n        return TokenRegex().Replace(template, match =>\n        {\n            var propertyName = match.Groups[1].Value;\n            var property = type.GetProperty(propertyName);\n\n            if (property == null)\n            {\n                return match.Value;  // Return unknown keys as keys - i.e. #Key#\n            }\n\n            return property.GetValue(values)?.ToString() ?? string.Empty;\n        });\n    }\n\n    public static bool TemplateRequiresUser(string template)\n    {\n        if (string.IsNullOrEmpty(template))\n        {\n            return false;\n        }\n\n        return template.Contains(\"#UserName#\", StringComparison.Ordinal)\n               || template.Contains(\"#UserEmail#\", StringComparison.Ordinal)\n               || template.Contains(\"#UserType#\", StringComparison.Ordinal);\n    }\n\n    public static bool TemplateRequiresActingUser(string template)\n    {\n        if (string.IsNullOrEmpty(template))\n        {\n            return false;\n        }\n\n        return template.Contains(\"#ActingUserName#\", StringComparison.Ordinal)\n               || template.Contains(\"#ActingUserEmail#\", StringComparison.Ordinal)\n               || template.Contains(\"#ActingUserType#\", StringComparison.Ordinal);\n    }\n\n    public static bool TemplateRequiresGroup(string template)\n    {\n        if (string.IsNullOrEmpty(template))\n        {\n            return false;\n        }\n\n        return template.Contains(\"#GroupName#\", StringComparison.Ordinal);\n    }\n\n    public static bool TemplateRequiresOrganization(string template)\n    {\n        if (string.IsNullOrEmpty(template))\n        {\n            return false;\n        }\n\n        return template.Contains(\"#OrganizationName#\", StringComparison.Ordinal);\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/PolicyDataValidator.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.AdminConsole.Utilities;\n\npublic static class PolicyDataValidator\n{\n    /// <summary>\n    /// Validates and serializes policy data based on the policy type.\n    /// </summary>\n    /// <param name=\"data\">The policy data to validate</param>\n    /// <param name=\"policyType\">The type of policy</param>\n    /// <returns>Serialized JSON string if data is valid, null if data is null or empty</returns>\n    /// <exception cref=\"BadRequestException\">Thrown when data validation fails</exception>\n    public static string? ValidateAndSerialize(Dictionary<string, object>? data, PolicyType policyType)\n    {\n        if (data == null || data.Count == 0)\n        {\n            return null;\n        }\n\n        try\n        {\n            var json = JsonSerializer.Serialize(data);\n\n            switch (policyType)\n            {\n                case PolicyType.MasterPassword:\n                    var masterPasswordData = CoreHelpers.LoadClassFromJsonData<MasterPasswordPolicyData>(json);\n                    ValidateModel(masterPasswordData, policyType);\n                    break;\n                case PolicyType.SendOptions:\n                    CoreHelpers.LoadClassFromJsonData<SendOptionsPolicyData>(json);\n                    break;\n                case PolicyType.ResetPassword:\n                    CoreHelpers.LoadClassFromJsonData<ResetPasswordDataModel>(json);\n                    break;\n            }\n\n            return json;\n        }\n        catch (JsonException ex)\n        {\n            var fieldName = !string.IsNullOrEmpty(ex.Path) ? ex.Path.TrimStart('$', '.') : null;\n            var fieldInfo = !string.IsNullOrEmpty(fieldName) ? $\": {fieldName} has an invalid value\" : \"\";\n            throw new BadRequestException($\"Invalid data for {policyType} policy{fieldInfo}.\");\n        }\n    }\n\n    private static void ValidateModel(object model, PolicyType policyType)\n    {\n        var validationContext = new ValidationContext(model);\n        var validationResults = new List<ValidationResult>();\n\n        if (!Validator.TryValidateObject(model, validationContext, validationResults, true))\n        {\n            var errors = string.Join(\", \", validationResults.Select(r => r.ErrorMessage));\n            throw new BadRequestException($\"Invalid data for {policyType} policy: {errors}\");\n        }\n    }\n\n    /// <summary>\n    /// Validates and deserializes policy metadata based on the policy type.\n    /// </summary>\n    /// <param name=\"metadata\">The policy metadata to validate</param>\n    /// <param name=\"policyType\">The type of policy</param>\n    /// <returns>Deserialized metadata model, or EmptyMetadataModel if metadata is null, empty, or validation fails</returns>\n    public static IPolicyMetadataModel ValidateAndDeserializeMetadata(Dictionary<string, object>? metadata, PolicyType policyType)\n    {\n        if (metadata == null || metadata.Count == 0)\n        {\n            return new EmptyMetadataModel();\n        }\n\n        try\n        {\n            var json = JsonSerializer.Serialize(metadata);\n\n            return policyType switch\n            {\n                PolicyType.OrganizationDataOwnership =>\n                    CoreHelpers.LoadClassFromJsonData<OrganizationModelOwnershipPolicyModel>(json),\n                _ => new EmptyMetadataModel()\n            };\n        }\n        catch (JsonException)\n        {\n            return new EmptyMetadataModel();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/Validation/IValidator.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Utilities.Validation;\n\npublic interface IValidator<T>\n{\n    public Task<ValidationResult<T>> ValidateAsync(T value);\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.Errors;\n\nnamespace Bit.Core.AdminConsole.Utilities.Validation;\n\npublic abstract record ValidationResult<T>;\n\npublic record Valid<T>(T Value) : ValidationResult<T>;\n\npublic record Invalid<T>(Error<T> Error) : ValidationResult<T>;\n\npublic static class ValidationResultMappers\n{\n    public static ValidationResult<B> Map<A, B>(this ValidationResult<A> validationResult, B invalidValue) =>\n        validationResult switch\n        {\n            Valid<A> => new Valid<B>(invalidValue),\n            Invalid<A> invalid => new Invalid<B>(invalid.Error.ToError(invalidValue)),\n            _ => throw new ArgumentOutOfRangeException(nameof(validationResult), \"Unhandled validation result type\")\n        };\n}\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/v2/Errors.cs",
    "content": "﻿namespace Bit.Core.AdminConsole.Utilities.v2;\n\n/// <summary>\n/// A strongly typed error containing a reason that an action failed.\n/// This is used for business logic validation and other expected errors, not exceptions.\n/// </summary>\npublic abstract record Error(string Message);\n/// <summary>\n/// An <see cref=\"Error\"/> type that maps to a NotFoundResult at the api layer.\n/// </summary>\n/// <param name=\"Message\"></param>\npublic abstract record NotFoundError(string Message) : Error(Message);\n\npublic abstract record BadRequestError(string Message) : Error(Message);\npublic abstract record InternalError(string Message) : Error(Message);\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/v2/Results/CommandResult.cs",
    "content": "﻿using OneOf;\nusing OneOf.Types;\n\nnamespace Bit.Core.AdminConsole.Utilities.v2.Results;\n\n/// <summary>\n/// Represents the result of a command.\n/// This is a <see cref=\"OneOf{Error, T}\"/> type that contains an Error if the command execution failed, or the result of the command if it succeeded.\n/// </summary>\n/// <typeparam name=\"T\">The type of the successful result. If there is no successful result (void), use <see cref=\"BulkCommandResult\"/>.</typeparam>\n\npublic class CommandResult<T>(OneOf<Error, T> result) : OneOfBase<Error, T>(result)\n{\n    public bool IsError => IsT0;\n    public bool IsSuccess => IsT1;\n    public Error AsError => AsT0;\n    public T AsSuccess => AsT1;\n\n    public static implicit operator CommandResult<T>(T value) => new(value);\n    public static implicit operator CommandResult<T>(Error error) => new(error);\n}\n\n/// <summary>\n/// Represents the result of a command where successful execution returns no value (void).\n/// See <see cref=\"CommandResult{T}\"/> for more information.\n/// </summary>\npublic class CommandResult(OneOf<Error, None> result) : CommandResult<None>(result)\n{\n    public static implicit operator CommandResult(None none) => new(none);\n    public static implicit operator CommandResult(Error error) => new(error);\n}\n\n/// <summary>\n/// A wrapper for <see cref=\"CommandResult{T}\"/> with an ID, to identify the result in bulk operations.\n/// </summary>\npublic record BulkCommandResult<T>(Guid Id, CommandResult<T> Result);\n\n/// <summary>\n/// A wrapper for <see cref=\"CommandResult\"/> with an ID, to identify the result in bulk operations.\n/// </summary>\npublic record BulkCommandResult(Guid Id, CommandResult Result);\n"
  },
  {
    "path": "src/Core/AdminConsole/Utilities/v2/Validation/ValidationResult.cs",
    "content": "﻿using OneOf;\nusing OneOf.Types;\n\nnamespace Bit.Core.AdminConsole.Utilities.v2.Validation;\n\n/// <summary>\n/// Represents the result of validating a request.\n/// This is for use within the Core layer, e.g. validating a command request.\n/// </summary>\n/// <param name=\"request\">The request that has been validated.</param>\n/// <param name=\"error\">A <see cref=\"OneOf{Error, None}\"/> type that contains an Error if validation failed.</param>\n/// <typeparam name=\"TRequest\">The request type.</typeparam>\npublic class ValidationResult<TRequest>(TRequest request, OneOf<Error, None> error) : OneOfBase<Error, None>(error)\n{\n    public TRequest Request { get; } = request;\n\n    public bool IsError => IsT0;\n    public bool IsValid => IsT1;\n    public Error AsError => AsT0;\n}\n\npublic static class ValidationResultHelpers\n{\n    /// <summary>\n    /// Creates a successful <see cref=\"ValidationResult{TRequest}\"/> with no error set.\n    /// </summary>\n    public static ValidationResult<T> Valid<T>(T request) => new(request, new None());\n    /// <summary>\n    /// Creates a failed <see cref=\"ValidationResult{TRequest}\"/> with the specified error.\n    /// </summary>\n    public static ValidationResult<T> Invalid<T>(T request, Error error) => new(request, error);\n\n    /// <summary>\n    /// Extracts successfully validated requests from a sequence of <see cref=\"ValidationResult{TRequest}\"/>.\n    /// </summary>\n    public static List<T> ValidRequests<T>(this IEnumerable<ValidationResult<T>> results) =>\n        results\n            .Where(r => r.IsValid)\n            .Select(r => r.Request)\n            .ToList();\n}\n"
  },
  {
    "path": "src/Core/AssemblyInfo.cs",
    "content": "﻿using System.Runtime.CompilerServices;\n\n[assembly: InternalsVisibleTo(\"Core.Test\")]\n[assembly: InternalsVisibleTo(\"Identity.IntegrationTest\")]\n"
  },
  {
    "path": "src/Core/Auth/Attributes/MarketingInitiativeValidationAttribute.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\n\nnamespace Bit.Core.Auth.Attributes;\n\npublic class MarketingInitiativeValidationAttribute : ValidationAttribute\n{\n    private static readonly string[] _acceptedValues = [MarketingInitiativeConstants.Premium];\n\n    public MarketingInitiativeValidationAttribute()\n    {\n        ErrorMessage = $\"Marketing initiative type must be one of: {string.Join(\", \", _acceptedValues)}\";\n    }\n\n    public override bool IsValid(object? value)\n    {\n        if (value == null)\n        {\n            return true;\n        }\n\n        if (value is not string str)\n        {\n            return false;\n        }\n\n        return _acceptedValues.Contains(str);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Entities/AuthRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Entities;\n\npublic class AuthRequest : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Enums.AuthRequestType Type { get; set; }\n    [MaxLength(50)]\n    public string RequestDeviceIdentifier { get; set; }\n    public DeviceType RequestDeviceType { get; set; }\n    [MaxLength(50)]\n    public string RequestIpAddress { get; set; }\n    /// <summary>\n    /// This country name is populated through a header value fetched from the ISO-3166 country code.\n    /// It will always be the English short form of the country name. The length should never be over 200 characters.\n    /// </summary>\n    [MaxLength(200)]\n    public string RequestCountryName { get; set; }\n    public Guid? ResponseDeviceId { get; set; }\n    [MaxLength(25)]\n    public string AccessCode { get; set; }\n    public string PublicKey { get; set; }\n    public string Key { get; set; }\n    public string MasterPasswordHash { get; set; }\n    public bool? Approved { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime? ResponseDate { get; set; }\n    public DateTime? AuthenticationDate { get; set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public bool IsSpent()\n    {\n        return ResponseDate.HasValue || AuthenticationDate.HasValue || IsExpired();\n    }\n\n    public bool IsExpired()\n    {\n        return GetExpirationDate() < DateTime.UtcNow;\n    }\n\n    public bool IsValidForAuthentication(Guid userId,\n        string password)\n    {\n        return ResponseDate.HasValue // it’s been responded to\n               && Approved == true // it was approved\n               && !IsExpired() // it's not expired\n               && Type == AuthRequestType.AuthenticateAndUnlock // it’s an authN request\n               && !AuthenticationDate.HasValue // it was not already used for authN\n               && UserId == userId // it belongs to the user\n               && CoreHelpers.FixedTimeEquals(AccessCode, password);  // the access code matches the password\n    }\n\n    public DateTime GetExpirationDate()\n    {\n        // TODO: PM-24252 - this should reference PasswordlessAuthSettings.UserRequestExpiration\n        return CreationDate.AddMinutes(15);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Entities/EmergencyAccess.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Entities;\n\npublic class EmergencyAccess : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid GrantorId { get; set; }\n    public Guid? GranteeId { get; set; }\n    [MaxLength(256)]\n    public string? Email { get; set; }\n    public string? KeyEncrypted { get; set; }\n    public EmergencyAccessType Type { get; set; }\n    public EmergencyAccessStatusType Status { get; set; }\n    public short WaitTimeDays { get; set; }\n    public DateTime? RecoveryInitiatedDate { get; set; }\n    public DateTime? LastNotificationDate { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public EmergencyAccess ToEmergencyAccess()\n    {\n        return new EmergencyAccess\n        {\n            Id = Id,\n            GrantorId = GrantorId,\n            GranteeId = GranteeId,\n            Email = Email,\n            KeyEncrypted = KeyEncrypted,\n            Type = Type,\n            Status = Status,\n            WaitTimeDays = WaitTimeDays,\n            RecoveryInitiatedDate = RecoveryInitiatedDate,\n            LastNotificationDate = LastNotificationDate,\n            CreationDate = CreationDate,\n            RevisionDate = RevisionDate,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Entities/Grant.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Models.Data;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Core.Auth.Entities;\n\npublic class Grant : IGrant\n{\n    public Grant() { }\n\n    public Grant(PersistedGrant pGrant)\n    {\n        Key = pGrant.Key;\n        Type = pGrant.Type;\n        SubjectId = pGrant.SubjectId;\n        SessionId = pGrant.SessionId;\n        ClientId = pGrant.ClientId;\n        Description = pGrant.Description;\n        CreationDate = pGrant.CreationTime;\n        ExpirationDate = pGrant.Expiration;\n        ConsumedDate = pGrant.ConsumedTime;\n        Data = pGrant.Data;\n    }\n\n    public int Id { get; set; }\n    [MaxLength(200)]\n    public string Key { get; set; } = null!;\n    [MaxLength(50)]\n    public string Type { get; set; } = null!;\n    [MaxLength(200)]\n    public string? SubjectId { get; set; }\n    [MaxLength(100)]\n    public string? SessionId { get; set; }\n    [MaxLength(200)]\n    public string ClientId { get; set; } = null!;\n    [MaxLength(200)]\n    public string? Description { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime? ExpirationDate { get; set; }\n    public DateTime? ConsumedDate { get; set; }\n    public string Data { get; set; } = null!;\n}\n"
  },
  {
    "path": "src/Core/Auth/Entities/SsoConfig.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Auth.Entities;\n\npublic class SsoConfig : ITableObject<long>\n{\n    public long Id { get; set; }\n    public bool Enabled { get; set; } = true;\n    public Guid OrganizationId { get; set; }\n    public string Data { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        // int will be auto-populated\n        Id = 0;\n    }\n\n    public SsoConfigurationData GetData()\n    {\n        return SsoConfigurationData.Deserialize(Data);\n    }\n\n    public void SetData(SsoConfigurationData data)\n    {\n        Data = data.Serialize();\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Entities/SsoUser.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Auth.Entities;\n\npublic class SsoUser : ITableObject<long>\n{\n    public long Id { get; set; }\n    public Guid UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    [MaxLength(300)]\n    public string ExternalId { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        // int will be auto-populated\n        Id = 0;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Entities/WebAuthnCredential.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Entities;\n\npublic class WebAuthnCredential : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    [MaxLength(50)]\n    public string Name { get; set; }\n    [MaxLength(256)]\n    public string PublicKey { get; set; }\n    [MaxLength(256)]\n    public string CredentialId { get; set; }\n    public int Counter { get; set; }\n    [MaxLength(20)]\n    public string Type { get; set; }\n    public Guid AaGuid { get; set; }\n\n    /// <summary>\n    /// User key encrypted with this WebAuthn credential's public key (EncryptedPublicKey field).\n    /// </summary>\n    [MaxLength(2000)]\n    public string EncryptedUserKey { get; set; }\n\n    /// <summary>\n    /// Private key encrypted with an external key for secure storage.\n    /// </summary>\n    [MaxLength(2000)]\n    public string EncryptedPrivateKey { get; set; }\n\n    /// <summary>\n    /// Public key encrypted with the user key for key rotation.\n    /// </summary>\n    [MaxLength(2000)]\n    public string EncryptedPublicKey { get; set; }\n\n    /// <summary>\n    /// Indicates whether this credential supports PRF (Pseudo-Random Function) extension.\n    /// </summary>\n    public bool SupportsPrf { get; set; }\n\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public WebAuthnPrfStatus GetPrfStatus()\n    {\n        if (!SupportsPrf)\n        {\n            return WebAuthnPrfStatus.Unsupported;\n        }\n\n        if (EncryptedUserKey != null && EncryptedPrivateKey != null && EncryptedPublicKey != null)\n        {\n            return WebAuthnPrfStatus.Enabled;\n        }\n\n        return WebAuthnPrfStatus.Supported;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/AuthRequestType.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\n/**\n * The type of auth request.\n *\n * Note:\n * Used by the Device_ReadActiveWithPendingAuthRequestsByUserId.sql stored procedure.\n *  If the enum changes be aware of this reference.\n */\npublic enum AuthRequestType : byte\n{\n    AuthenticateAndUnlock = 0,\n    Unlock = 1,\n    AdminApproval = 2,\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/EmergencyAccessStatusType.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum EmergencyAccessStatusType : byte\n{\n    /// <summary>\n    /// The user has been invited to be an emergency contact.\n    /// </summary>\n    Invited = 0,\n    /// <summary>\n    /// The invited user, \"grantee\", has accepted the request to be an emergency contact.\n    /// </summary>\n    Accepted = 1,\n    /// <summary>\n    /// The inviting user, \"grantor\", has approved the grantee's acceptance.\n    /// </summary>\n    Confirmed = 2,\n    /// <summary>\n    /// The grantee has initiated the recovery process.\n    /// </summary>\n    RecoveryInitiated = 3,\n    /// <summary>\n    /// The grantee has exercised their emergency access.\n    /// </summary>\n    RecoveryApproved = 4,\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/EmergencyAccessType.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum EmergencyAccessType : byte\n{\n    /// <summary>\n    /// Allows emergency contact to view the Grantor's data.\n    /// </summary>\n    View = 0,\n    /// <summary>\n    /// Allows emergency contact to take over the Grantor's account by overwriting the Grantor's password.\n    /// </summary>\n    Takeover = 1,\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/MemberDecryptionType.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum MemberDecryptionType\n{\n    MasterPassword = 0,\n    KeyConnector = 1,\n    TrustedDeviceEncryption = 2\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/Saml2BindingType.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum Saml2BindingType : byte\n{\n    HttpRedirect = 1,\n    HttpPost = 2,\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/Saml2NameIdFormat.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum Saml2NameIdFormat : byte\n{\n    NotConfigured = 0,\n    Unspecified = 1,\n    EmailAddress = 2,\n    X509SubjectName = 3,\n    WindowsDomainQualifiedName = 4,\n    KerberosPrincipalName = 5,\n    EntityIdentifier = 6,\n    Persistent = 7,\n    Transient = 8,\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/Saml2SigningBehavior.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum Saml2SigningBehavior : byte\n{\n    IfIdpWantAuthnRequestsSigned = 0,\n    Always = 1,\n    Never = 3\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/SsoType.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum SsoType : byte\n{\n    OpenIdConnect = 1,\n    Saml2 = 2,\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/TwoFactorEmailPurpose.cs",
    "content": "﻿namespace Core.Auth.Enums;\n\npublic enum TwoFactorEmailPurpose\n{\n    Login,\n    Setup,\n    NewDeviceVerification,\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/TwoFactorProviderType.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum TwoFactorProviderType : byte\n{\n    Authenticator = 0,\n    Email = 1,\n    Duo = 2,\n    YubiKey = 3,\n    [Obsolete(\"Deprecated in favor of WebAuthn.\")]\n    U2f = 4,\n    Remember = 5,\n    OrganizationDuo = 6,\n    WebAuthn = 7,\n    RecoveryCode = 8,\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/WebAuthnLoginAssertionOptionsScope.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum WebAuthnLoginAssertionOptionsScope\n{\n    /*\n        Authentication is used when a user is trying to login in with a credential.\n    */\n    Authentication = 0,\n    /*\n        PrfRegistration is used when a user is trying to register a new credential.\n    */\n    PrfRegistration = 1,\n    /*\n        UpdateKeySet is used when a user is enabling a credential for passwordless login\n        This is done by adding rotatable keys to the credential.\n    */\n    UpdateKeySet = 2\n}\n"
  },
  {
    "path": "src/Core/Auth/Enums/WebAuthnPrfStatus.cs",
    "content": "﻿namespace Bit.Core.Auth.Enums;\n\npublic enum WebAuthnPrfStatus\n{\n    Enabled = 0,\n    Supported = 1,\n    Unsupported = 2\n}\n"
  },
  {
    "path": "src/Core/Auth/Exceptions/DuplicateAuthRequestException.cs",
    "content": "﻿using Bit.Core.Exceptions;\n\nnamespace Bit.Core.Auth.Exceptions;\n\npublic class DuplicateAuthRequestException : BadRequestException\n{\n    public DuplicateAuthRequestException()\n        : base(\"An authentication request with the same device already exists.\")\n    {\n\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/Claims.cs",
    "content": "﻿namespace Bit.Core.Auth.Identity;\n\npublic static class Claims\n{\n    // User\n    public const string SecurityStamp = \"sstamp\";\n    public const string Premium = \"premium\";\n    public const string Device = \"device\";\n    public const string DeviceType = \"devicetype\";\n\n    public const string OrganizationOwner = \"orgowner\";\n    public const string OrganizationAdmin = \"orgadmin\";\n    public const string OrganizationUser = \"orguser\";\n    public const string OrganizationCustom = \"orgcustom\";\n    public const string ProviderAdmin = \"providerprovideradmin\";\n    public const string ProviderServiceUser = \"providerserviceuser\";\n\n    public const string SecretsManagerAccess = \"accesssecretsmanager\";\n\n    // Service Account\n    public const string Organization = \"organization\";\n\n    // General\n    public const string Type = \"type\";\n\n    // Organization custom permissions\n    public static class CustomPermissions\n    {\n        public const string AccessEventLogs = \"accesseventlogs\";\n        public const string AccessImportExport = \"accessimportexport\";\n        public const string AccessReports = \"accessreports\";\n        public const string CreateNewCollections = \"createnewcollections\";\n        public const string EditAnyCollection = \"editanycollection\";\n        public const string DeleteAnyCollection = \"deleteanycollection\";\n        public const string ManageGroups = \"managegroups\";\n        public const string ManagePolicies = \"managepolicies\";\n        public const string ManageSso = \"managesso\";\n        public const string ManageUsers = \"manageusers\";\n        public const string ManageResetPassword = \"manageresetpassword\";\n        public const string ManageScim = \"managescim\";\n    }\n    public static class SendAccessClaims\n    {\n        public const string SendId = \"send_id\";\n        public const string Email = \"send_email\";\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/CustomIdentityServiceCollectionExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Identity;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\n// ref: https://github.com/aspnet/Identity/blob/dev/src/Microsoft.AspNetCore.Identity/IdentityServiceCollectionExtensions.cs\npublic static class CustomIdentityServiceCollectionExtensions\n{\n    public static IdentityBuilder AddIdentityWithoutCookieAuth<TUser, TRole>(\n        this IServiceCollection services)\n        where TUser : class\n        where TRole : class\n    {\n        return services.AddIdentityWithoutCookieAuth<TUser, TRole>(setupAction: null);\n    }\n\n    public static IdentityBuilder AddIdentityWithoutCookieAuth<TUser, TRole>(\n        this IServiceCollection services,\n        Action<IdentityOptions> setupAction)\n        where TUser : class\n        where TRole : class\n    {\n        // Hosting doesn't add IHttpContextAccessor by default\n        services.AddHttpContextAccessor();\n        // Identity services\n        services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();\n        services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();\n        services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();\n        services.TryAddScoped<ILookupNormalizer, LowerInvariantLookupNormalizer>();\n        services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();\n        // No interface for the error describer so we can add errors without rev'ing the interface\n        services.TryAddScoped<IdentityErrorDescriber>();\n        services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();\n        services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();\n        services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();\n        services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();\n        services.TryAddScoped<UserManager<TUser>>();\n        services.TryAddScoped<SignInManager<TUser>>();\n        services.TryAddScoped<RoleManager<TRole>>();\n\n        if (setupAction != null)\n        {\n            services.Configure(setupAction);\n        }\n\n        return new IdentityBuilder(typeof(TUser), typeof(TRole), services);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/IdentityClientType.cs",
    "content": "﻿namespace Bit.Core.Auth.Identity;\n\npublic enum IdentityClientType : byte\n{\n    User = 0,\n    Organization = 1,\n    ServiceAccount = 2,\n    Send = 3\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/LowerInvariantLookupNormalizer.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Auth.Identity;\n\npublic class LowerInvariantLookupNormalizer : ILookupNormalizer\n{\n    public string NormalizeEmail(string email)\n    {\n        return Normalize(email);\n    }\n\n    public string NormalizeName(string name)\n    {\n        return Normalize(name);\n    }\n\n    private string Normalize(string key)\n    {\n        return key?.Normalize().ToLowerInvariant();\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/Policies.cs",
    "content": "﻿namespace Bit.Core.Auth.Identity;\n\npublic static class Policies\n{\n    /// <summary>\n    /// Policy for managing access to the Send feature.\n    /// </summary>\n    /// <remarks>\n    /// <example>\n    /// Can be used with the <c>Authorize</c> attribute, for example:\n    /// <code>\n    /// [Authorize(Policy = Policies.Send)]\n    /// </code>\n    /// </example>\n    /// </remarks>\n    public const string Send = \"Send\";\n\n    /// <summary>\n    /// Policy to manage access to general API endpoints.\n    /// </summary>\n    /// <remarks>\n    /// <example>\n    /// Can be used with the <c>Authorize</c> attribute, for example:\n    /// <code>\n    /// [Authorize(Policy = Policies.Application)]\n    /// </code>\n    /// </example>\n    /// </remarks>\n    public const string Application = \"Application\";\n\n    /// <summary>\n    /// Policy to manage access to API endpoints intended for use by the Web Vault and browser extension only.\n    /// </summary>\n    /// <remarks>\n    /// <example>\n    /// Can be used with the <c>Authorize</c> attribute, for example:\n    /// <code>\n    /// [Authorize(Policy = Policies.Web)]\n    /// </code>\n    /// </example>\n    /// </remarks>\n    public const string Web = \"Web\";\n\n    /// <summary>\n    /// Policy to restrict access to API endpoints for the Push feature.\n    /// </summary>\n    /// <remarks>\n    /// <example>\n    /// Can be used with the <c>Authorize</c> attribute, for example:\n    /// <code>\n    /// [Authorize(Policy = Policies.Push)]\n    /// </code>\n    /// </example>\n    /// </remarks>\n    public const string Push = \"Push\";\n\n    // TODO: This is unused\n    public const string Licensing = \"Licensing\"; // [Authorize(Policy = Policies.Licensing)]\n\n    /// <summary>\n    /// Policy to restrict access to API endpoints related to the Organization features.\n    /// </summary>\n    /// <remarks>\n    /// <example>\n    /// Can be used with the <c>Authorize</c> attribute, for example:\n    /// <code>\n    /// [Authorize(Policy = Policies.Licensing)]\n    /// </code>\n    /// </example>\n    /// </remarks>\n    public const string Organization = \"Organization\";\n\n    /// <summary>\n    /// Policy to restrict access to API endpoints related to the setting up new installations.\n    /// </summary>\n    /// <remarks>\n    /// <example>\n    /// Can be used with the <c>Authorize</c> attribute, for example:\n    /// <code>\n    /// [Authorize(Policy = Policies.Installation)]\n    /// </code>\n    /// </example>\n    /// </remarks>\n    public const string Installation = \"Installation\";\n\n    /// <summary>\n    /// Policy to restrict access to API endpoints for Secrets Manager features.\n    /// </summary>\n    /// <remarks>\n    /// <example>\n    /// Can be used with the <c>Authorize</c> attribute, for example:\n    /// <code>\n    /// [Authorize(Policy = Policies.Secrets)]\n    /// </code>\n    /// </example>\n    /// </remarks>\n    public const string Secrets = \"Secrets\";\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/RoleStore.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Auth.Identity;\n\npublic class RoleStore : IRoleStore<Role>\n{\n    public void Dispose() { }\n\n    public Task<IdentityResult> CreateAsync(Role role, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task<IdentityResult> DeleteAsync(Role role, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task<Role> FindByIdAsync(string roleId, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task<Role> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task<string> GetNormalizedRoleNameAsync(Role role, CancellationToken cancellationToken)\n    {\n        return Task.FromResult(role.Name);\n    }\n\n    public Task<string> GetRoleIdAsync(Role role, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task<string> GetRoleNameAsync(Role role, CancellationToken cancellationToken)\n    {\n        return Task.FromResult(role.Name);\n    }\n\n    public Task SetNormalizedRoleNameAsync(Role role, string normalizedName, CancellationToken cancellationToken)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SetRoleNameAsync(Role role, string roleName, CancellationToken cancellationToken)\n    {\n        role.Name = roleName;\n        return Task.FromResult(0);\n    }\n\n    public Task<IdentityResult> UpdateAsync(Role role, CancellationToken cancellationToken)\n    {\n        throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.DependencyInjection;\nusing OtpNet;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\npublic class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>\n{\n    private const string CacheKeyFormat = \"Authenticator_TOTP_{0}_{1}\";\n\n    private readonly IDistributedCache _distributedCache;\n    private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions;\n\n    public AuthenticatorTokenProvider(\n        [FromKeyedServices(\"persistent\")]\n        IDistributedCache distributedCache)\n    {\n        _distributedCache = distributedCache;\n        _distributedCacheEntryOptions = new DistributedCacheEntryOptions\n        {\n            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)\n        };\n    }\n\n    public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)\n    {\n        var authenticatorProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);\n        if (string.IsNullOrWhiteSpace((string)authenticatorProvider?.MetaData[\"Key\"]))\n        {\n            return Task.FromResult(false);\n        }\n        return Task.FromResult(authenticatorProvider.Enabled);\n    }\n\n    public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)\n    {\n        return Task.FromResult<string>(null);\n    }\n\n    public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)\n    {\n        var cacheKey = string.Format(CacheKeyFormat, user.Id, token);\n        var cachedValue = await _distributedCache.GetAsync(cacheKey);\n        if (cachedValue != null)\n        {\n            return false;\n        }\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);\n        var otp = new Totp(Base32Encoding.ToBytes((string)provider.MetaData[\"Key\"]));\n        var valid = otp.VerifyTotp(token, out _, new VerificationWindow(1, 1));\n\n        if (valid)\n        {\n            await _distributedCache.SetAsync(cacheKey, [1], _distributedCacheEntryOptions);\n        }\n\n        return valid;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.DependencyInjection;\nusing Duo = DuoUniversal;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\npublic class DuoUniversalTokenProvider(\n    IServiceProvider serviceProvider,\n    IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,\n    IDuoUniversalTokenService duoUniversalTokenService) : IUserTwoFactorTokenProvider<User>\n{\n    /// <summary>\n    /// We need the IServiceProvider to resolve the <see cref=\"IUserService\"/>. There is a complex dependency dance\n    /// occurring between <see cref=\"IUserService\"/>, which extends the <see cref=\"UserManager{User}\"/>, and the usage\n    /// of the <see cref=\"UserManager{User}\"/> within this class. Trying to resolve the <see cref=\"IUserService\"/> using\n    /// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the\n    /// problem.\n    /// </summary>\n    private readonly IServiceProvider _serviceProvider = serviceProvider;\n    private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = tokenDataFactory;\n    private readonly IDuoUniversalTokenService _duoUniversalTokenService = duoUniversalTokenService;\n\n    public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)\n    {\n        var userService = _serviceProvider.GetRequiredService<IUserService>();\n        var duoUniversalTokenProvider = await GetDuoTwoFactorProvider(user, userService);\n        if (duoUniversalTokenProvider == null)\n        {\n            return false;\n        }\n\n        return duoUniversalTokenProvider.Enabled;\n    }\n\n    public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)\n    {\n        var duoClient = await GetDuoClientAsync(user);\n        if (duoClient == null)\n        {\n            return null;\n        }\n        return _duoUniversalTokenService.GenerateAuthUrl(duoClient, _tokenDataFactory, user);\n    }\n\n    public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)\n    {\n        var duoClient = await GetDuoClientAsync(user);\n        if (duoClient == null)\n        {\n            return false;\n        }\n        return await _duoUniversalTokenService.RequestDuoValidationAsync(duoClient, _tokenDataFactory, user, token);\n    }\n\n    /// <summary>\n    /// Get the Duo Two Factor Provider for the user if they have premium access to Duo\n    /// </summary>\n    /// <param name=\"user\">Active User</param>\n    /// <returns>null or Duo TwoFactorProvider</returns>\n    private async Task<TwoFactorProvider> GetDuoTwoFactorProvider(User user, IUserService userService)\n    {\n        if (!await userService.CanAccessPremium(user))\n        {\n            return null;\n        }\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);\n        if (!_duoUniversalTokenService.HasProperDuoMetadata(provider))\n        {\n            return null;\n        }\n\n        return provider;\n    }\n\n    /// <summary>\n    /// Uses the User to fetch a valid TwoFactorProvider and use it to create a Duo.Client\n    /// </summary>\n    /// <param name=\"user\">active user</param>\n    /// <returns>null or Duo TwoFactorProvider</returns>\n    private async Task<Duo.Client> GetDuoClientAsync(User user)\n    {\n        var userService = _serviceProvider.GetRequiredService<IUserService>();\n        var provider = await GetDuoTwoFactorProvider(user, userService);\n        if (provider == null)\n        {\n            return null;\n        }\n\n        var duoClient = await _duoUniversalTokenService.BuildDuoTwoFactorClientAsync(provider);\n        if (duoClient == null)\n        {\n            return null;\n        }\n\n        return duoClient;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Duo = DuoUniversal;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\n/// <summary>\n/// OrganizationDuo and Duo TwoFactorProviderTypes both use the same flows so both of those Token Providers will\n/// have this class injected to utilize these methods\n/// </summary>\npublic interface IDuoUniversalTokenService\n{\n    /// <summary>\n    /// Generates the Duo Auth URL for the user to be redirected to Duo for 2FA. This\n    /// Auth URL also lets the Duo Service know where to redirect the user back to after\n    /// the 2FA process is complete.\n    /// </summary>\n    /// <param name=\"duoClient\">A not null valid Duo.Client</param>\n    /// <param name=\"tokenDataFactory\">This service creates the state token for added security</param>\n    /// <param name=\"user\">currently active user</param>\n    /// <returns>a URL in string format</returns>\n    string GenerateAuthUrl(\n        Duo.Client duoClient,\n        IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,\n        User user);\n\n    /// <summary>\n    /// Makes the request to Duo to validate the authCode and state token\n    /// </summary>\n    /// <param name=\"duoClient\">A not null valid Duo.Client</param>\n    /// <param name=\"tokenDataFactory\">Factory for decrypting the state</param>\n    /// <param name=\"user\">self</param>\n    /// <param name=\"token\">token received from the client</param>\n    /// <returns>boolean based on result from Duo</returns>\n    Task<bool> RequestDuoValidationAsync(\n        Duo.Client duoClient,\n        IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,\n        User user,\n        string token);\n\n    /// <summary>\n    /// Generates a Duo.Client object for use with Duo SDK v4. This method is to validate a Duo configuration\n    /// when adding or updating the configuration. This method makes a web request to Duo to verify the configuration.\n    /// Throws exception if configuration is invalid.\n    /// </summary>\n    /// <param name=\"clientSecret\">Duo client Secret</param>\n    /// <param name=\"clientId\">Duo client Id</param>\n    /// <param name=\"host\">Duo host</param>\n    /// <returns>Boolean</returns>\n    Task<bool> ValidateDuoConfiguration(string clientSecret, string clientId, string host);\n\n    /// <summary>\n    /// Checks provider for the correct Duo metadata: ClientId, ClientSecret, and Host. Does no validation on the data.\n    /// it is assumed to be correct. The only way to have the data written to the Database is after verification\n    /// occurs.\n    /// </summary>\n    /// <param name=\"provider\">Host being checked for proper data</param>\n    /// <returns>true if all three are present; false if one is missing or the host is incorrect</returns>\n    bool HasProperDuoMetadata(TwoFactorProvider provider);\n\n    /// <summary>\n    /// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation.\n    /// This method is made public so that it is easier to test. If the method was private then there would not be an\n    /// easy way to mock the response. Since this makes a web request it is difficult to mock.\n    /// </summary>\n    /// <param name=\"provider\">TwoFactorProvider Duo or OrganizationDuo</param>\n    /// <returns>Duo.Client object or null</returns>\n    Task<Duo.Client> BuildDuoTwoFactorClientAsync(TwoFactorProvider provider);\n}\n\npublic class DuoUniversalTokenService(\n    ICurrentContext currentContext,\n    GlobalSettings globalSettings) : IDuoUniversalTokenService\n{\n    private readonly ICurrentContext _currentContext = currentContext;\n    private readonly GlobalSettings _globalSettings = globalSettings;\n\n    public string GenerateAuthUrl(\n        Duo.Client duoClient,\n        IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,\n        User user)\n    {\n        var state = tokenDataFactory.Protect(new DuoUserStateTokenable(user));\n        var authUrl = duoClient.GenerateAuthUri(user.Email, state);\n\n        return authUrl;\n    }\n\n    public async Task<bool> RequestDuoValidationAsync(\n        Duo.Client duoClient,\n        IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,\n        User user,\n        string token)\n    {\n        var parts = token.Split(\"|\");\n        var authCode = parts[0];\n        var state = parts[1];\n        tokenDataFactory.TryUnprotect(state, out var tokenable);\n        if (!tokenable.Valid || !tokenable.TokenIsValid(user))\n        {\n            return false;\n        }\n\n        // duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used\n        // their authCode with a victims credentials\n        var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email);\n        // If the result of the exchange doesn't throw an exception and it's not null, then it's valid\n        return res.AuthResult.Result == \"allow\";\n    }\n\n    public async Task<bool> ValidateDuoConfiguration(string clientSecret, string clientId, string host)\n    {\n        // Do some simple checks to ensure data integrity\n        if (!ValidDuoHost(host) ||\n            string.IsNullOrWhiteSpace(clientSecret) ||\n            string.IsNullOrWhiteSpace(clientId))\n        {\n            return false;\n        }\n        // The AuthURI is not important for this health check so we pass in a non-empty string\n        var client = new Duo.ClientBuilder(clientId, clientSecret, host, \"non-empty\").Build();\n\n        // This could throw an exception, the false flag will allow the exception to bubble up\n        return await client.DoHealthCheck(false);\n    }\n\n    public bool HasProperDuoMetadata(TwoFactorProvider provider)\n    {\n        return provider?.MetaData != null &&\n               provider.MetaData.ContainsKey(\"ClientId\") &&\n               provider.MetaData.ContainsKey(\"ClientSecret\") &&\n               provider.MetaData.ContainsKey(\"Host\") &&\n               ValidDuoHost((string)provider.MetaData[\"Host\"]);\n    }\n\n\n    /// <summary>\n    /// Checks the host string to make sure it meets Duo's Guidelines before attempting to create a Duo.Client.\n    /// </summary>\n    /// <param name=\"host\">string representing the Duo Host</param>\n    /// <returns>true if the host is valid false otherwise</returns>\n    public static bool ValidDuoHost(string host)\n    {\n        if (Uri.TryCreate($\"https://{host}\", UriKind.Absolute, out var uri))\n        {\n            return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == \"/\") &&\n                uri.Host.StartsWith(\"api-\") &&\n                (uri.Host.EndsWith(\".duosecurity.com\") || uri.Host.EndsWith(\".duofederal.com\"));\n        }\n        return false;\n    }\n\n    public async Task<Duo.Client> BuildDuoTwoFactorClientAsync(TwoFactorProvider provider)\n    {\n        // Fetch Client name from header value since duo auth can be initiated from multiple clients and we want\n        // to redirect back to the initiating client\n        _currentContext.HttpContext.Request.Headers.TryGetValue(\"Bitwarden-Client-Name\", out var bitwardenClientName);\n        var redirectUri = string.Format(\"{0}/duo-redirect-connector.html?client={1}\",\n            _globalSettings.BaseServiceUri.Vault, bitwardenClientName.FirstOrDefault() ?? \"web\");\n\n        var client = new Duo.ClientBuilder(\n            (string)provider.MetaData[\"ClientId\"],\n            (string)provider.MetaData[\"ClientSecret\"],\n            (string)provider.MetaData[\"Host\"],\n            redirectUri).Build();\n\n        if (!await client.DoHealthCheck(false))\n        {\n            return null;\n        }\n        return client;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs",
    "content": "﻿using System.Text;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\n/// <summary>\n/// Generates and validates tokens for email OTPs.\n/// </summary>\npublic class EmailTokenProvider : IUserTwoFactorTokenProvider<User>\n{\n    private const string CacheKeyFormat = \"EmailToken_{0}_{1}_{2}\";\n\n    private readonly IDistributedCache _distributedCache;\n    private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions;\n\n    public EmailTokenProvider(\n        [FromKeyedServices(\"persistent\")]\n        IDistributedCache distributedCache,\n        IFeatureService featureService)\n    {\n        _distributedCache = distributedCache;\n        _distributedCacheEntryOptions = new DistributedCacheEntryOptions\n        {\n            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)\n        };\n        if (featureService.IsEnabled(FeatureFlagKeys.Otp6Digits))\n        {\n            TokenLength = 6;\n        }\n        else\n        {\n            TokenLength = 8;\n        }\n    }\n\n    public int TokenLength { get; protected set; }\n    public bool TokenAlpha { get; protected set; } = false;\n    public bool TokenNumeric { get; protected set; } = true;\n\n    public virtual Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)\n    {\n        return Task.FromResult(!string.IsNullOrEmpty(user.Email));\n    }\n\n    public virtual async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)\n    {\n        var code = CoreHelpers.SecureRandomString(TokenLength, TokenAlpha, true, false, TokenNumeric, false);\n        var cacheKey = string.Format(CacheKeyFormat, user.Id, user.SecurityStamp, purpose);\n        await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(code), _distributedCacheEntryOptions);\n        return code;\n    }\n\n    public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)\n    {\n        var cacheKey = string.Format(CacheKeyFormat, user.Id, user.SecurityStamp, purpose);\n        var cachedValue = await _distributedCache.GetAsync(cacheKey);\n        if (cachedValue == null)\n        {\n            return false;\n        }\n\n        var code = Encoding.UTF8.GetString(cachedValue);\n        var valid = CoreHelpers.FixedTimeEquals(token, code);\n        if (valid)\n        {\n            await _distributedCache.RemoveAsync(cacheKey);\n        }\n\n        return valid;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\n/// <summary>\n/// Generates tokens for email two-factor authentication.\n/// It inherits from the EmailTokenProvider class, which manages the persistence and validation of tokens, \n/// and adds additional validation to ensure that 2FA is enabled for the user.\n/// </summary>\npublic class EmailTwoFactorTokenProvider : EmailTokenProvider\n{\n    public EmailTwoFactorTokenProvider(\n        [FromKeyedServices(\"persistent\")]\n        IDistributedCache distributedCache,\n        IFeatureService featureService) :\n        base(distributedCache, featureService)\n    {\n        // This can be removed when the pm-18612-otp-6-digits feature flag is removed because the base implementation will match.\n        TokenAlpha = false;\n        TokenNumeric = true;\n        TokenLength = 6;\n    }\n\n    public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)\n    {\n        var emailTokenProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);\n        if (!HasProperMetaData(emailTokenProvider))\n        {\n            return Task.FromResult(false);\n        }\n\n        return Task.FromResult(emailTokenProvider.Enabled);\n    }\n\n    public override Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)\n    {\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);\n        if (!HasProperMetaData(provider))\n        {\n            return null;\n        }\n\n        return base.GenerateAsync(purpose, manager, user);\n    }\n\n    private static bool HasProperMetaData(TwoFactorProvider provider)\n    {\n        return provider?.MetaData != null && provider.MetaData.TryGetValue(\"Email\", out var emailValue) &&\n            !string.IsNullOrWhiteSpace((string)emailValue);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/IOrganizationTwoFactorTokenProvider.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\npublic interface IOrganizationTwoFactorTokenProvider\n{\n    Task<bool> CanGenerateTwoFactorTokenAsync(Organization organization);\n    Task<string> GenerateAsync(Organization organization, User user);\n    Task<bool> ValidateAsync(string token, Organization organization, User user);\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\nusing Duo = DuoUniversal;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\npublic interface IOrganizationDuoUniversalTokenProvider : IOrganizationTwoFactorTokenProvider { }\n\npublic class OrganizationDuoUniversalTokenProvider(\n    IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,\n    IDuoUniversalTokenService duoUniversalTokenService) : IOrganizationDuoUniversalTokenProvider\n{\n    private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = tokenDataFactory;\n    private readonly IDuoUniversalTokenService _duoUniversalTokenService = duoUniversalTokenService;\n\n    public Task<bool> CanGenerateTwoFactorTokenAsync(Organization organization)\n    {\n        var provider = GetDuoTwoFactorProvider(organization);\n        if (provider != null && provider.Enabled)\n        {\n            return Task.FromResult(true);\n        }\n        return Task.FromResult(false);\n    }\n\n    public async Task<string> GenerateAsync(Organization organization, User user)\n    {\n        var duoClient = await GetDuoClientAsync(organization);\n        if (duoClient == null)\n        {\n            return null;\n        }\n        return _duoUniversalTokenService.GenerateAuthUrl(duoClient, _tokenDataFactory, user);\n    }\n\n    public async Task<bool> ValidateAsync(string token, Organization organization, User user)\n    {\n        var duoClient = await GetDuoClientAsync(organization);\n        if (duoClient == null)\n        {\n            return false;\n        }\n        return await _duoUniversalTokenService.RequestDuoValidationAsync(duoClient, _tokenDataFactory, user, token);\n    }\n\n    private TwoFactorProvider GetDuoTwoFactorProvider(Organization organization)\n    {\n        if (organization == null || !organization.Enabled || !organization.Use2fa)\n        {\n            return null;\n        }\n\n        var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);\n        if (!_duoUniversalTokenService.HasProperDuoMetadata(provider))\n        {\n            return null;\n        }\n        return provider;\n    }\n\n    private async Task<Duo.Client> GetDuoClientAsync(Organization organization)\n    {\n        var provider = GetDuoTwoFactorProvider(organization);\n        if (provider == null)\n        {\n            return null;\n        }\n\n        var duoClient = await _duoUniversalTokenService.BuildDuoTwoFactorClientAsync(provider);\n        if (duoClient == null)\n        {\n            return null;\n        }\n\n        return duoClient;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/IOtpTokenProvider.cs",
    "content": "﻿namespace Bit.Core.Auth.Identity.TokenProviders;\n\n/// <summary>\n/// A generic interface for a one-time password (OTP) token provider.\n/// </summary>\npublic interface IOtpTokenProvider<TOptions>\n    where TOptions : DefaultOtpTokenProviderOptions\n{\n    /// <summary>\n    /// Generates a new one-time password (OTP) based on the configured parameters.\n    /// The generated OTP is stored in the distributed cache with a key based on the unique identifier and purpose. If the\n    /// key is already in use, it will overwrite and generate a new OTP with a refreshed TTL.\n    /// </summary>\n    /// <param name=\"tokenProviderName\">Name of the token provider, used to distinguish different token providers that may inject this class</param>\n    /// <param name=\"purpose\">Purpose of the OTP token, used to distinguish different types of tokens.</param>\n    /// <param name=\"uniqueIdentifier\">Unique identifier to distinguish one request from another</param>\n    /// <returns>generated token | null</returns>\n    Task<string?> GenerateTokenAsync(string tokenProviderName, string purpose, string uniqueIdentifier);\n\n    /// <summary>\n    /// Validates the provided token against the stored value in the distributed cache.\n    /// </summary>\n    /// <param name=\"token\">string value matched against the unique identifier in the cache if found</param>\n    /// <param name=\"tokenProviderName\">Name of the token provider, used to distinguish different token providers that may inject this class</param>\n    /// <param name=\"purpose\">Purpose of the OTP token, used to distinguish different types of tokens.</param>\n    /// <param name=\"uniqueIdentifier\">Unique identifier to distinguish one request from another</param>\n    /// <returns>true if the token matches what is fetched from the cache, false if not.</returns>\n    Task<bool> ValidateTokenAsync(string token, string tokenProviderName, string purpose, string uniqueIdentifier);\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProvider.cs",
    "content": "﻿using System.Text;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\npublic class OtpTokenProvider<TOptions>(\n    [FromKeyedServices(\"persistent\")]\n    IDistributedCache distributedCache,\n    IOptions<TOptions> options) : IOtpTokenProvider<TOptions>\n        where TOptions : DefaultOtpTokenProviderOptions\n{\n    private readonly TOptions _otpTokenProviderOptions = options.Value;\n\n    /// <summary>\n    /// This is where the OTP tokens are stored.\n    /// </summary>\n    private readonly IDistributedCache _distributedCache = distributedCache;\n\n    /// <summary>\n    /// Used to store and fetch the OTP tokens from the distributed cache.\n    /// The format is \"{tokenProviderName}_{purpose}_{uniqueIdentifier}\".\n    /// </summary>\n    private readonly string _cacheKeyFormat = \"{0}_{1}_{2}\";\n\n    public async Task<string?> GenerateTokenAsync(string tokenProviderName, string purpose, string uniqueIdentifier)\n    {\n        if (string.IsNullOrEmpty(tokenProviderName)\n            || string.IsNullOrEmpty(purpose)\n            || string.IsNullOrEmpty(uniqueIdentifier))\n        {\n            return null;\n        }\n\n        var cacheKey = string.Format(_cacheKeyFormat, tokenProviderName, purpose, uniqueIdentifier);\n        var token = CoreHelpers.SecureRandomString(\n            _otpTokenProviderOptions.TokenLength,\n            _otpTokenProviderOptions.TokenAlpha,\n            true,\n            false,\n            _otpTokenProviderOptions.TokenNumeric,\n            false);\n        await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(token), _otpTokenProviderOptions.DistributedCacheEntryOptions);\n        return token;\n    }\n\n    public async Task<bool> ValidateTokenAsync(string token, string tokenProviderName, string purpose, string uniqueIdentifier)\n    {\n        if (string.IsNullOrEmpty(token)\n            || string.IsNullOrEmpty(tokenProviderName)\n            || string.IsNullOrEmpty(purpose)\n            || string.IsNullOrEmpty(uniqueIdentifier))\n        {\n            return false;\n        }\n\n        var cacheKey = string.Format(_cacheKeyFormat, tokenProviderName, purpose, uniqueIdentifier);\n        var cachedValue = await _distributedCache.GetAsync(cacheKey);\n        if (cachedValue == null)\n        {\n            return false;\n        }\n\n        var code = Encoding.UTF8.GetString(cachedValue);\n        var valid = CoreHelpers.FixedTimeEquals(token, code);\n        if (valid)\n        {\n            await _distributedCache.RemoveAsync(cacheKey);\n        }\n\n        return valid;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProviderOptions.cs",
    "content": "﻿using Microsoft.Extensions.Caching.Distributed;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\n/// <summary>\n/// Options for configuring the OTP token provider.\n/// </summary>\npublic class DefaultOtpTokenProviderOptions\n{\n    /// <summary>\n    /// Gets or sets the length of the generated token.\n    /// Default is 6 characters.\n    /// </summary>\n    public int TokenLength { get; set; } = 6;\n\n    /// <summary>\n    /// Gets or sets whether the token should contain alphabetic characters.\n    /// Default is false.\n    /// </summary>\n    public bool TokenAlpha { get; set; } = false;\n\n    /// <summary>\n    /// Gets or sets whether the token should contain numeric characters.\n    /// Default is true.\n    /// </summary>\n    public bool TokenNumeric { get; set; } = true;\n\n    /// <summary>\n    /// Cache entry options for Otp Token provider.\n    /// Default is 5 minutes expiration.\n    /// </summary>\n    public DistributedCacheEntryOptions DistributedCacheEntryOptions { get; set; } = new DistributedCacheEntryOptions\n    {\n        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)\n    };\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/readme.md",
    "content": "# OtpTokenProvider\n\nThe `OtpTokenProvider` is a token provider service for generating and validating Time-Based one-time passwords (TOTP). It provides a secure way to create temporary tokens for various authentication and verification scenarios. The provider can be configured to generate tokens specific to your use case by using the options pattern in the DI pipeline.\n\n## Overview\n\nThe OTP Token Provider generates secure, time-limited tokens that can be used for:\n\n- Two-factor authentication\n- Temporary access tokens for Sends\n- Any scenario requiring short-lived verification codes\n\n## Features\n\n- **Configurable Token Length**: Default 6 characters, customizable\n- **Character Set Options**: Numeric (default), alphabetic, or mixed\n- **Distributed Caching**: Uses CosmosDb for cloud, or the configured database otherwise.\n- **TTL Management**: Configurable expiration (default 5 minutes)\n- **Secure Generation**: Uses cryptographically secure random generation\n- **One-Time Use**: Tokens are automatically deleted from the cache after successful validation\n\n## Architecture\n\n### Interface: `IOtpTokenProvider<TOptions>`\n\n```csharp\npublic interface IOtpTokenProvider<TOptions>\n    where TOptions : DefaultOtpTokenProviderOptions\n{\n    Task<string?> GenerateTokenAsync(string tokenProviderName, string purpose, string uniqueIdentifier);\n    Task<bool> ValidateTokenAsync(string token, string tokenProviderName, string purpose, string uniqueIdentifier);\n}\n```\n\n### Implementation: `OtpTokenProvider`\n\nThe provider is initialized with:\n\n- **Distributed Cache**: Storage backend for tokens (using \"persistent\" keyed service)\n- **IOptions<TOptions>**: Configuration options for token generation and caching\n\n## Usage\n\n### Basic Setup\n\nIf your class needs the use the `IOtpTokenProvider` you can inject it like any other injectable class from the DI.\n\n### Generating a Token\n\n```csharp\n// Generate a new OTP with token provider name, purpose and unique identifier\nstring token = await otpProvider.GenerateTokenAsync(\"EmailToken\", \"email_verification\", $\"{userId}_{securityStamp}\");\n// Returns: \"123456\" (6-digit numeric by default)\n```\n\n### Validating a Token\n\n```csharp\n// Validate user-provided token with same parameters used for generation\nbool isValid = await otpProvider.ValidateTokenAsync(\"123456\", \"EmailToken\", \"email_verification\", $\"{userId}_{securityStamp}\");\n// Returns: true if valid, false otherwise\n// Note: Valid tokens are automatically removed from cache\n```\n\n### Custom Configurations\n\nIf you need to modify the default options you can do so by creating an extension of the `DefaultOtpTokenProviderOptions` and using that class as the TOptions when injecting another IOtpTokenProvider service.\n\n#### OtpTokenProviderOptions\n\n```csharp\npublic class DefaultOtpTokenProviderOptions\n{ ... }\n\npublic class UserEmailOtpTokenOptions : DefaultOtpTokenProviderOptions { }\n```\n\n#### Service Collection\n\n```csharp\npublic static IdentityBuilder AddCustomIdentityServices(\n    this IServiceCollection services, GlobalSettings globalSettings)\n{\n    // possible customization\n    services.Configure<UserEmailOtpTokenOptions>(options =>\n    {\n        options.TokenLength = 8;\n        // The other options are left default\n    });\n\n    // TryAddTransient open generics -> this allows us to inject IOtpTokenProvider<T> without having to specify the specific type here.\n    services.TryAddTransient(typeof(IOtpTokenProvider<>), typeof(OtpTokenProvider<>);\n}\n```\n\n#### Usage\n\n```csharp\npublic class UserEmailTokenProvider(\n    IOtpTokenProvider<UserEmailOtpTokenOptions> otpTokenProvider\n)\n{\n    private readonly IOtpTokenProvider<UserEmailOtpTokenOptions> _otpTokenProvider = otpTokenProvider;\n    ...\n}\n```\n\n## Configuration Options\n\n### Token Properties\n\n| Property       | Default | Description                              |\n| -------------- | ------- | ---------------------------------------- |\n| `TokenLength`  | 6       | Number of characters in generated token  |\n| `TokenAlpha`   | false   | Include alphabetic characters (a-z, A-Z) |\n| `TokenNumeric` | true    | Include numeric characters (0-9)         |\n\n### Cache Options\n\nSee `DistributedCacheEntryOptions` documentation for a complete list of configuration options.\n\n| Property                          | Default   | Description                  |\n| --------------------------------- | --------- | ---------------------------- |\n| `AbsoluteExpirationRelativeToNow` | 5 minutes | How long tokens remain valid |\n\n## Cache Key Format\n\nThe cache key format uses three components: `{tokenProviderName}_{purpose}_{uniqueIdentifier}`\n\n### Examples:\n\n#### Possible Email Token Provider Example\n\nEmail token provider uses:\n\n- **Token Provider Name**: `\"EmailToken\"` (identifies the specific use case)\n- **Purpose**: `\"EmailTwoFactorAuthentication\"` (specific action being verified)\n- **Unique Identifier**: `\"{user.Id}_{securityStamp}\"` (user-specific data)\n\nThese are passed into the OTP Token Provider which creates a cache record:\n\n- Cache Key: `EmailToken_EmailTwoFactorAuthentication_guid_guid`\n\n## Security Considerations\n\n### Token Generation\n\n- Uses `CoreHelpers.SecureRandomString()` for cryptographically secure randomness\n- No predictable patterns in generated tokens\n- Configurable character sets for different security requirements\n\n### Storage\n\n- Tokens are stored in distributed cache. The cache depends on the specific deployment, for cloud it is CosmosDb.\n- Automatic expiration prevents indefinite token validity\n- One-time use prevents replay attacks\n\n### Validation\n\n- Exact string matching for validation\n- Automatic removal after successful validation\n- Returns `false` for expired or non-existent tokens\n\n## Dependency Injection\n\nThe provider is registered in `ServiceCollectionExtensions.cs`:\n\n```csharp\nservices.TryAddScoped<IOtpTokenProvider<TOptions>, OtpTokenProvider<TOptions>>();\n```\n\n## Error Handling\n\n### Common Scenarios\n\n- **Token Not Found**: `ValidateTokenAsync()` returns `false`\n- **Token Expired**: Automatically cleaned up by cache, validation returns `false`\n- **Invalid Input**:\n  - `GenerateTokenAsync` returns `null` for empty/null tokenProviderName, purpose, or uniqueIdentifier\n  - `ValidateTokenAsync` returns `false` for empty/null token, tokenProviderName, purpose, or uniqueIdentifier\n  - No cache operations are performed for invalid inputs\n\n### Best Practices\n\n- Always check validation results\n- Handle token expiration gracefully\n- Provide clear user feedback for invalid tokens\n- Implement rate limiting for token generation\n\n## Related Components\n\n- **`CoreHelpers.SecureRandomString()`**: Secure token generation\n- **`IDistributedCache`**: Token storage backend\n- **Two-Factor Authentication Providers**: Integration with 2FA flows\n- **Email Services**: A Token delivery mechanism\n\n## Testing\n\nWhen testing components that use `OtpTokenProvider`:\n\n```csharp\n// Mock the interface for unit tests\nvar mockOtpProvider = Substitute.For<IOtpTokenProvider<DefaultOtpTokenProviderOptions>>();\nmockOtpProvider.GenerateTokenAsync(\"EmailToken\", \"email_verification\", \"user_123\").Returns(\"123456\");\nmockOtpProvider.ValidateTokenAsync(\"123456\", \"EmailToken\", \"email_verification\", \"user_123\").Returns(true);\n```\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/TwoFactorRememberTokenProvider.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\npublic class TwoFactorRememberTokenProvider : DataProtectorTokenProvider<User>\n{\n    public TwoFactorRememberTokenProvider(\n        IDataProtectionProvider dataProtectionProvider,\n        IOptions<TwoFactorRememberTokenProviderOptions> options,\n        ILogger<DataProtectorTokenProvider<User>> logger)\n        : base(dataProtectionProvider, options, logger)\n    { }\n}\n\npublic class TwoFactorRememberTokenProviderOptions : DataProtectionTokenProviderOptions\n{ }\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Fido2NetLib;\nusing Fido2NetLib.Objects;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\npublic class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>\n{\n    private readonly IServiceProvider _serviceProvider;\n    private readonly IFido2 _fido2;\n    private readonly GlobalSettings _globalSettings;\n\n    public WebAuthnTokenProvider(IServiceProvider serviceProvider, IFido2 fido2, GlobalSettings globalSettings)\n    {\n        _serviceProvider = serviceProvider;\n        _fido2 = fido2;\n        _globalSettings = globalSettings;\n    }\n\n    public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)\n    {\n        var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);\n        // null check happens in this method\n        if (!HasProperMetaData(webAuthnProvider))\n        {\n            return Task.FromResult(false);\n        }\n\n        return Task.FromResult(webAuthnProvider.Enabled);\n    }\n\n    public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)\n    {\n        var userService = _serviceProvider.GetRequiredService<IUserService>();\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);\n        var keys = LoadKeys(provider);\n        var existingCredentials = keys.Select(key => key.Item2.Descriptor).ToList();\n\n        if (existingCredentials.Count == 0)\n        {\n            return null;\n        }\n\n        var exts = new AuthenticationExtensionsClientInputs()\n        {\n            UserVerificationMethod = true,\n            AppID = CoreHelpers.U2fAppIdUrl(_globalSettings),\n        };\n\n        var options = _fido2.GetAssertionOptions(existingCredentials, UserVerificationRequirement.Discouraged, exts);\n\n        // TODO: Remove this when newtonsoft legacy converters are gone\n        provider.MetaData[\"login\"] = JsonSerializer.Serialize(options);\n\n        var providers = user.GetTwoFactorProviders();\n        providers[TwoFactorProviderType.WebAuthn] = provider;\n        user.SetTwoFactorProviders(providers);\n        await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, logEvent: false);\n\n        return options.ToJson();\n    }\n\n    public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)\n    {\n        var userService = _serviceProvider.GetRequiredService<IUserService>();\n        if (string.IsNullOrWhiteSpace(token))\n        {\n            return false;\n        }\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);\n        var keys = LoadKeys(provider);\n\n        if (!provider.MetaData.TryGetValue(\"login\", out var login))\n        {\n            return false;\n        }\n\n        var clientResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(token,\n            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });\n\n        var jsonOptions = login.ToString();\n        var options = AssertionOptions.FromJson(jsonOptions);\n\n        var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id));\n\n        if (webAuthCred == null)\n        {\n            return false;\n        }\n\n        // Callback to check user ownership of credential. Always return true since we have already\n        // established ownership in this context.\n        IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true);\n\n        try\n        {\n            var res = await _fido2.MakeAssertionAsync(clientResponse, options, webAuthCred.Item2.PublicKey, webAuthCred.Item2.SignatureCounter, callback);\n\n            provider.MetaData.Remove(\"login\");\n\n            // Update SignatureCounter\n            webAuthCred.Item2.SignatureCounter = res.Counter;\n\n            var providers = user.GetTwoFactorProviders();\n            providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2;\n            user.SetTwoFactorProviders(providers);\n            await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, logEvent: false);\n\n            return res.Status == \"ok\";\n        }\n        catch (Fido2VerificationException)\n        {\n            return false;\n        }\n\n    }\n\n    /// <summary>\n    /// Checks if the provider has proper metadata.\n    /// This is used to determine if the provider has been properly configured.\n    /// </summary>\n    /// <param name=\"provider\"></param>\n    /// <returns>true if metadata is present; false if empty or null</returns>\n    private bool HasProperMetaData(TwoFactorProvider provider)\n    {\n        return provider?.MetaData?.Any() ?? false;\n    }\n\n    private List<Tuple<string, TwoFactorProvider.WebAuthnData>> LoadKeys(TwoFactorProvider provider)\n    {\n        var keys = new List<Tuple<string, TwoFactorProvider.WebAuthnData>>();\n        if (!HasProperMetaData(provider))\n        {\n            return keys;\n        }\n\n        // Load all WebAuthn credentials stored in metadata. The number of allowed credentials\n        // is controlled by credential registration.\n        foreach (var kvp in provider.MetaData.Where(k => k.Key.StartsWith(\"Key\")))\n        {\n            var key = new TwoFactorProvider.WebAuthnData((dynamic)kvp.Value);\n            keys.Add(new Tuple<string, TwoFactorProvider.WebAuthnData>(kvp.Key, key));\n        }\n\n        return keys;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.DependencyInjection;\nusing YubicoDotNetClient;\n\nnamespace Bit.Core.Auth.Identity.TokenProviders;\n\npublic class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>\n{\n    private readonly IServiceProvider _serviceProvider;\n    private readonly GlobalSettings _globalSettings;\n\n    public YubicoOtpTokenProvider(\n        IServiceProvider serviceProvider,\n        GlobalSettings globalSettings)\n    {\n        _serviceProvider = serviceProvider;\n        _globalSettings = globalSettings;\n    }\n\n    public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)\n    {\n        // Ensure the user has access to premium\n        var userService = _serviceProvider.GetRequiredService<IUserService>();\n        if (!await userService.CanAccessPremium(user))\n        {\n            return false;\n        }\n\n        // Check if the user has a YubiKey provider configured\n        var yubicoProvider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey);\n        if (!yubicoProvider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true)\n        {\n            return false;\n        }\n\n        return yubicoProvider.Enabled;\n    }\n\n    public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)\n    {\n        return Task.FromResult<string>(null);\n    }\n\n    public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)\n    {\n        var userService = _serviceProvider.GetRequiredService<IUserService>();\n        if (!await userService.CanAccessPremium(user))\n        {\n            return false;\n        }\n\n        if (string.IsNullOrWhiteSpace(token) || token.Length < 32 || token.Length > 48)\n        {\n            return false;\n        }\n\n        var id = token.Substring(0, 12);\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey);\n        if (!provider.MetaData.ContainsValue(id))\n        {\n            return false;\n        }\n\n        var client = new YubicoClient(_globalSettings.Yubico.ClientId, _globalSettings.Yubico.Key);\n        if (_globalSettings.Yubico.ValidationUrls != null && _globalSettings.Yubico.ValidationUrls.Length > 0)\n        {\n            client.SetUrls(_globalSettings.Yubico.ValidationUrls);\n        }\n        var response = await client.VerifyAsync(token);\n        return response.Status == YubicoResponseStatus.Ok;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Identity/UserStore.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Auth.Identity;\n\npublic class UserStore :\n    IUserStore<User>,\n    IUserPasswordStore<User>,\n    IUserEmailStore<User>,\n    IUserTwoFactorStore<User>,\n    IUserSecurityStampStore<User>\n{\n    private readonly IServiceProvider _serviceProvider;\n    private readonly IUserRepository _userRepository;\n    private readonly ICurrentContext _currentContext;\n\n    public UserStore(\n        IServiceProvider serviceProvider,\n        IUserRepository userRepository,\n        ICurrentContext currentContext)\n    {\n        _serviceProvider = serviceProvider;\n        _userRepository = userRepository;\n        _currentContext = currentContext;\n    }\n\n    public void Dispose() { }\n\n    public async Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        await _userRepository.CreateAsync(user);\n        return IdentityResult.Success;\n    }\n\n    public async Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        await _userRepository.DeleteAsync(user);\n        return IdentityResult.Success;\n    }\n\n    public async Task<User> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        if (_currentContext?.User != null && _currentContext.User.Email == normalizedEmail)\n        {\n            return _currentContext.User;\n        }\n\n        _currentContext.User = await _userRepository.GetByEmailAsync(normalizedEmail);\n        return _currentContext.User;\n    }\n\n    public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        if (_currentContext?.User != null &&\n            string.Equals(_currentContext.User.Id.ToString(), userId, StringComparison.InvariantCultureIgnoreCase))\n        {\n            return _currentContext.User;\n        }\n\n        Guid userIdGuid;\n        if (!Guid.TryParse(userId, out userIdGuid))\n        {\n            return null;\n        }\n\n        _currentContext.User = await _userRepository.GetByIdAsync(userIdGuid);\n        return _currentContext.User;\n    }\n\n    public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return await FindByEmailAsync(normalizedUserName, cancellationToken);\n    }\n\n    public Task<string> GetEmailAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return Task.FromResult(user.Email);\n    }\n\n    public Task<bool> GetEmailConfirmedAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return Task.FromResult(user.EmailVerified);\n    }\n\n    public Task<string> GetNormalizedEmailAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return Task.FromResult(user.Email);\n    }\n\n    public Task<string> GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return Task.FromResult(user.Email);\n    }\n\n    public Task<string> GetPasswordHashAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return Task.FromResult(user.MasterPassword);\n    }\n\n    public Task<string> GetUserIdAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return Task.FromResult(user.Id.ToString());\n    }\n\n    public Task<string> GetUserNameAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return Task.FromResult(user.Email);\n    }\n\n    public Task<bool> HasPasswordAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return Task.FromResult(!string.IsNullOrWhiteSpace(user.MasterPassword));\n    }\n\n    public Task SetEmailAsync(User user, string email, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        user.Email = email;\n        return Task.FromResult(0);\n    }\n\n    public Task SetEmailConfirmedAsync(User user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        user.EmailVerified = confirmed;\n        return Task.FromResult(0);\n    }\n\n    public Task SetNormalizedEmailAsync(User user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        user.Email = normalizedEmail;\n        return Task.FromResult(0);\n    }\n\n    public Task SetNormalizedUserNameAsync(User user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        user.Email = normalizedName;\n        return Task.FromResult(0);\n    }\n\n    public Task SetPasswordHashAsync(User user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        user.MasterPassword = passwordHash;\n        return Task.FromResult(0);\n    }\n\n    public Task SetUserNameAsync(User user, string userName, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        user.Email = userName;\n        return Task.FromResult(0);\n    }\n\n    public async Task<IdentityResult> UpdateAsync(User user, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;\n        await _userRepository.ReplaceAsync(user);\n        return IdentityResult.Success;\n    }\n\n    public Task SetTwoFactorEnabledAsync(User user, bool enabled, CancellationToken cancellationToken)\n    {\n        // Do nothing...\n        return Task.FromResult(0);\n    }\n\n    public async Task<bool> GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken)\n    {\n        return await _serviceProvider.GetRequiredService<ITwoFactorIsEnabledQuery>().TwoFactorIsEnabledAsync(user);\n    }\n\n    public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken)\n    {\n        user.SecurityStamp = stamp;\n        return Task.FromResult(0);\n    }\n\n    public Task<string> GetSecurityStampAsync(User user, CancellationToken cancellationToken)\n    {\n        return Task.FromResult(user.SecurityStamp);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/IdentityServer/ApiScopes.cs",
    "content": "﻿using Duende.IdentityServer.Models;\n\nnamespace Bit.Core.Auth.IdentityServer;\n\npublic static class ApiScopes\n{\n    public const string Api = \"api\";\n    public const string ApiInstallation = \"api.installation\";\n    public const string ApiLicensing = \"api.licensing\";\n    public const string ApiOrganization = \"api.organization\";\n    public const string ApiPush = \"api.push\";\n    public const string ApiSecrets = \"api.secrets\";\n    public const string Internal = \"internal\";\n    public const string ApiSendAccess = \"api.send.access\";\n\n    public static IEnumerable<ApiScope> GetApiScopes()\n    {\n        return new List<ApiScope>\n        {\n            new(Api, \"API Access\"),\n            new(ApiPush, \"API Push Access\"),\n            new(ApiLicensing, \"API Licensing Access\"),\n            new(ApiOrganization, \"API Organization Access\"),\n            new(ApiInstallation, \"API Installation Access\"),\n            new(Internal, \"Internal Access\"),\n            new(ApiSecrets, \"Secrets Manager Access\"),\n            new(ApiSendAccess, \"API Send Access\"),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Duende.IdentityServer.Configuration;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Core.Auth.IdentityServer;\n\npublic class ConfigureOpenIdConnectDistributedOptions : IPostConfigureOptions<CookieAuthenticationOptions>\n{\n    private readonly IdentityServerOptions _idsrv;\n    private readonly IDistributedCache _distributedCache;\n    private readonly IDataProtectionProvider _dataProtectionProvider;\n\n    public ConfigureOpenIdConnectDistributedOptions(\n        [FromKeyedServices(\"persistent\")]\n        IDistributedCache distributedCache,\n        IDataProtectionProvider dataProtectionProvider,\n        IdentityServerOptions idsrv)\n    {\n        _idsrv = idsrv;\n        _distributedCache = distributedCache;\n        _dataProtectionProvider = dataProtectionProvider;\n    }\n\n    public void PostConfigure(string name, CookieAuthenticationOptions options)\n    {\n        options.CookieManager = new DistributedCacheCookieManager();\n\n        if (name != AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)\n        {\n            // Ignore\n            return;\n        }\n\n        options.Cookie.Name = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme;\n        options.Cookie.IsEssential = true;\n        options.Cookie.SameSite = _idsrv.Authentication.CookieSameSiteMode;\n        options.TicketDataFormat = new DistributedCacheTicketDataFormatter(_distributedCache, _dataProtectionProvider, name);\n        options.SessionStore = new DistributedCacheTicketStore(_distributedCache);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/IdentityServer/DistributedCacheCookieManager.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Auth.IdentityServer;\n\npublic class DistributedCacheCookieManager : ICookieManager\n{\n    private readonly ChunkingCookieManager _cookieManager;\n\n    public DistributedCacheCookieManager()\n    {\n        _cookieManager = new ChunkingCookieManager();\n    }\n\n    private string CacheKeyPrefix => \"cookie-data\";\n\n    public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)\n    {\n        var id = Guid.NewGuid().ToString();\n        var cacheKey = GetKey(key, id);\n\n        var expiresUtc = options.Expires ?? DateTimeOffset.UtcNow.AddMinutes(15);\n        var cacheOptions = new DistributedCacheEntryOptions()\n            .SetAbsoluteExpiration(expiresUtc);\n\n        var data = Encoding.UTF8.GetBytes(value);\n\n        var cache = GetCache(context);\n        cache.Set(cacheKey, data, cacheOptions);\n\n        // Write the cookie with the identifier as the body\n        _cookieManager.AppendResponseCookie(context, key, id, options);\n    }\n\n    public void DeleteCookie(HttpContext context, string key, CookieOptions options)\n    {\n        _cookieManager.DeleteCookie(context, key, options);\n        var id = GetId(context, key);\n        if (!string.IsNullOrWhiteSpace(id))\n        {\n            var cacheKey = GetKey(key, id);\n            GetCache(context).Remove(cacheKey);\n        }\n    }\n\n    public string GetRequestCookie(HttpContext context, string key)\n    {\n        var id = GetId(context, key);\n        if (string.IsNullOrWhiteSpace(id))\n        {\n            return null;\n        }\n        var cacheKey = GetKey(key, id);\n        return GetCache(context).GetString(cacheKey);\n    }\n\n    private IDistributedCache GetCache(HttpContext context) =>\n        context.RequestServices.GetRequiredKeyedService<IDistributedCache>(\"persistent\");\n\n    private string GetKey(string key, string id) => $\"{CacheKeyPrefix}-{key}-{id}\";\n\n    private string GetId(HttpContext context, string key) =>\n        context.Request.Cookies.TryGetValue(key, out var cookie) ?\n        cookie : null;\n}\n"
  },
  {
    "path": "src/Core/Auth/IdentityServer/DistributedCacheTicketDataFormatter.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.Caching.Distributed;\n\nnamespace Bit.Core.Auth.IdentityServer;\n\npublic class DistributedCacheTicketDataFormatter : ISecureDataFormat<AuthenticationTicket>\n{\n    private const string CacheKeyPrefix = \"ticket-data\";\n\n    private readonly IDistributedCache _distributedCache;\n    private readonly IDataProtector _dataProtector;\n    private readonly string _prefix;\n\n    public DistributedCacheTicketDataFormatter(\n        IDistributedCache distributedCache,\n        IDataProtectionProvider dataProtectionProvider,\n        string name)\n    {\n        _distributedCache = distributedCache;\n        _dataProtector = dataProtectionProvider.CreateProtector(CacheKeyPrefix, name);\n        _prefix = $\"{CacheKeyPrefix}-{name}\";\n    }\n\n    public string Protect(AuthenticationTicket data) => Protect(data, null);\n    public string Protect(AuthenticationTicket data, string purpose)\n    {\n        var key = Guid.NewGuid().ToString();\n        var cacheKey = $\"{_prefix}-{purpose}-{key}\";\n\n        var expiresUtc = data.Properties.ExpiresUtc ??\n            DateTimeOffset.UtcNow.AddMinutes(15);\n\n        var options = new DistributedCacheEntryOptions();\n        options.SetAbsoluteExpiration(expiresUtc);\n\n        var ticket = TicketSerializer.Default.Serialize(data);\n        _distributedCache.Set(cacheKey, ticket, options);\n\n        return _dataProtector.Protect(key);\n    }\n\n    public AuthenticationTicket Unprotect(string protectedText) => Unprotect(protectedText, null);\n    public AuthenticationTicket Unprotect(string protectedText, string purpose)\n    {\n        if (string.IsNullOrWhiteSpace(protectedText))\n        {\n            return null;\n        }\n\n        // Decrypt the key and retrieve the data from the cache.\n        var key = _dataProtector.Unprotect(protectedText);\n        var cacheKey = $\"{_prefix}-{purpose}-{key}\";\n        var ticket = _distributedCache.Get(cacheKey);\n\n        if (ticket == null)\n        {\n            return null;\n        }\n\n        var data = TicketSerializer.Default.Deserialize(ticket);\n        return data;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/IdentityServer/DistributedCacheTicketStore.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.Extensions.Caching.Distributed;\n\nnamespace Bit.Core.Auth.IdentityServer;\n\npublic class DistributedCacheTicketStore : ITicketStore\n{\n    private const string KeyPrefix = \"auth-\";\n    private readonly IDistributedCache _cache;\n\n    public DistributedCacheTicketStore(IDistributedCache distributedCache)\n    {\n        _cache = distributedCache;\n    }\n\n    public async Task<string> StoreAsync(AuthenticationTicket ticket)\n    {\n        var key = $\"{KeyPrefix}{Guid.NewGuid()}\";\n        await RenewAsync(key, ticket);\n\n        return key;\n    }\n\n    public Task RenewAsync(string key, AuthenticationTicket ticket)\n    {\n        var options = new DistributedCacheEntryOptions();\n        var expiresUtc = ticket.Properties.ExpiresUtc ??\n            DateTimeOffset.UtcNow.AddMinutes(15);\n        options.SetAbsoluteExpiration(expiresUtc);\n\n        var val = SerializeToBytes(ticket);\n        _cache.Set(key, val, options);\n\n        return Task.FromResult(0);\n    }\n\n    public Task<AuthenticationTicket> RetrieveAsync(string key)\n    {\n        AuthenticationTicket ticket;\n        var bytes = _cache.Get(key);\n        ticket = DeserializeFromBytes(bytes);\n\n        return Task.FromResult(ticket);\n    }\n\n    public Task RemoveAsync(string key)\n    {\n        _cache.Remove(key);\n\n        return Task.FromResult(0);\n    }\n\n    private static byte[] SerializeToBytes(AuthenticationTicket source)\n    {\n        return TicketSerializer.Default.Serialize(source);\n    }\n\n    private static AuthenticationTicket DeserializeFromBytes(byte[] source)\n    {\n        return source == null ? null : TicketSerializer.Default.Deserialize(source);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/IdentityServer/TokenRetrieval.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Http;\n\nnamespace Bit.Core.Auth.IdentityServer;\n\npublic static class TokenRetrieval\n{\n    private static string _headerScheme = \"Bearer \";\n    private static string _queryScheme = \"access_token\";\n    private static string _authHeader = \"Authorization\";\n\n    public static Func<HttpRequest, string> FromAuthorizationHeaderOrQueryString()\n    {\n        return (request) =>\n        {\n            var authorization = request.Headers[_authHeader].FirstOrDefault();\n            if (string.IsNullOrWhiteSpace(authorization))\n            {\n                return request.Query[_queryScheme].FirstOrDefault();\n            }\n\n            if (authorization.StartsWith(_headerScheme, StringComparison.OrdinalIgnoreCase))\n            {\n                return authorization.Substring(_headerScheme.Length).Trim();\n            }\n\n            return null;\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Models.Api.Request.Accounts;\n\npublic class KeysRequestModel\n{\n    [Obsolete(\"Use AccountKeys.AccountPublicKey instead\")]\n    [Required]\n    public string PublicKey { get; set; }\n    [Obsolete(\"Use AccountKeys.UserKeyEncryptedAccountPrivateKey instead\")]\n    [Required]\n    public string EncryptedPrivateKey { get; set; }\n    public AccountKeysRequestModel AccountKeys { get; set; }\n\n    [Obsolete(\"Use SetAccountKeysForUserCommand instead\")]\n    public User ToUser(User existingUser)\n    {\n        if (string.IsNullOrWhiteSpace(PublicKey) || string.IsNullOrWhiteSpace(EncryptedPrivateKey))\n        {\n            throw new InvalidOperationException(\"Public and private keys are required.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(existingUser.PublicKey) && string.IsNullOrWhiteSpace(existingUser.PrivateKey))\n        {\n            existingUser.PublicKey = PublicKey;\n            existingUser.PrivateKey = EncryptedPrivateKey;\n            return existingUser;\n        }\n        else if (PublicKey == existingUser.PublicKey && CoreHelpers.FixedTimeEquals(EncryptedPrivateKey, existingUser.PrivateKey))\n        {\n            return existingUser;\n        }\n        else\n        {\n            throw new InvalidOperationException(\"Cannot replace existing key(s) with new key(s).\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstants.cs",
    "content": "﻿namespace Bit.Core.Auth.Models.Api.Request.Accounts;\n\npublic static class MarketingInitiativeConstants\n{\n    /// <summary>\n    /// Indicates that the user began the registration process on a marketing page designed\n    /// to streamline users who intend to setup a premium subscription after registration.\n    /// </summary>\n    public const string Premium = \"premium\";\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Models.Api.Request.Accounts;\nusing System.ComponentModel.DataAnnotations;\n\npublic enum RegisterFinishTokenType : byte\n{\n    EmailVerification = 1,\n    OrganizationInvite = 2,\n    OrgSponsoredFreeFamilyPlan = 3,\n    EmergencyAccessInvite = 4,\n    ProviderInvite = 5,\n}\n\npublic class RegisterFinishRequestModel : IValidatableObject\n{\n    [StrictEmailAddress, StringLength(256)]\n    public required string Email { get; set; }\n    public string? EmailVerificationToken { get; set; }\n\n    public MasterPasswordAuthenticationDataRequestModel? MasterPasswordAuthentication { get; set; }\n    public MasterPasswordUnlockDataRequestModel? MasterPasswordUnlock { get; set; }\n\n    // PM-28143 - Remove property below (made optional during migration to MasterPasswordUnlockData)\n    [StringLength(1000)]\n    // Made optional but there will still be a thrown error if it does not exist either here or\n    // in the MasterPasswordAuthenticationData.\n    public string? MasterPasswordHash { get; set; }\n\n    [StringLength(50)]\n    public string? MasterPasswordHint { get; set; }\n\n    // PM-28143 - Remove property below (made optional during migration to MasterPasswordUnlockData)\n    // Made optional but there will still be a thrown error if it does not exist either here or\n    // in the MasterPasswordAuthenticationData.\n    public string? UserSymmetricKey { get; set; }\n\n    public required KeysRequestModel UserAsymmetricKeys { get; set; }\n\n    // PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData)\n    public KdfType? Kdf { get; set; }\n    // PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData)\n    public int? KdfIterations { get; set; }\n    // PM-28143 - Remove line below\n    public int? KdfMemory { get; set; }\n    // PM-28143 - Remove line below\n    public int? KdfParallelism { get; set; }\n\n    public Guid? OrganizationUserId { get; set; }\n    public string? OrgInviteToken { get; set; }\n\n    public string? OrgSponsoredFreeFamilyPlanToken { get; set; }\n\n    public string? AcceptEmergencyAccessInviteToken { get; set; }\n    public Guid? AcceptEmergencyAccessId { get; set; }\n\n    public string? ProviderInviteToken { get; set; }\n\n    public Guid? ProviderUserId { get; set; }\n\n    public User ToUser()\n    {\n        var user = new User\n        {\n            Email = Email,\n            MasterPasswordHint = MasterPasswordHint,\n            Kdf = (KdfType)(MasterPasswordUnlock?.Kdf.KdfType ?? Kdf)!,\n            KdfIterations = (int)(MasterPasswordUnlock?.Kdf.Iterations ?? KdfIterations)!,\n            // KdfMemory and KdfParallelism are optional (only used for Argon2id)\n            KdfMemory = MasterPasswordUnlock?.Kdf.Memory ?? KdfMemory,\n            KdfParallelism = MasterPasswordUnlock?.Kdf.Parallelism ?? KdfParallelism,\n            // PM-28827 To be added when MasterPasswordSalt is added to the user column\n            // MasterPasswordSalt = MasterPasswordUnlock?.Salt ?? Email.ToLower().Trim(),\n            Key = MasterPasswordUnlock?.MasterKeyWrappedUserKey ?? UserSymmetricKey\n        };\n\n        UserAsymmetricKeys.ToUser(user);\n\n        return user;\n    }\n\n    public RegisterFinishTokenType GetTokenType()\n    {\n        if (!string.IsNullOrWhiteSpace(EmailVerificationToken))\n        {\n            return RegisterFinishTokenType.EmailVerification;\n        }\n        if (!string.IsNullOrEmpty(OrgInviteToken)\n            && OrganizationUserId.HasValue\n            && OrganizationUserId.Value != Guid.Empty)\n        {\n            return RegisterFinishTokenType.OrganizationInvite;\n        }\n        if (!string.IsNullOrWhiteSpace(OrgSponsoredFreeFamilyPlanToken))\n        {\n            return RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan;\n        }\n        if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken)\n            && AcceptEmergencyAccessId.HasValue\n            && AcceptEmergencyAccessId.Value != Guid.Empty)\n        {\n            return RegisterFinishTokenType.EmergencyAccessInvite;\n        }\n        if (!string.IsNullOrWhiteSpace(ProviderInviteToken)\n            && ProviderUserId.HasValue\n            && ProviderUserId.Value != Guid.Empty)\n        {\n            return RegisterFinishTokenType.ProviderInvite;\n        }\n\n        throw new InvalidOperationException(\"Invalid token type.\");\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        // 1. Authentication data containing hash and hash at root level check\n        if (MasterPasswordAuthentication != null && MasterPasswordHash != null)\n        {\n            if (MasterPasswordAuthentication.MasterPasswordAuthenticationHash != MasterPasswordHash)\n            {\n                yield return new ValidationResult(\n                    $\"{nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash)} and root level {nameof(MasterPasswordHash)} provided and are not equal. Only provide one.\",\n                    [nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash), nameof(MasterPasswordHash)]);\n            }\n        } // 1.5 if there is no master password hash that is unacceptable even though they are both optional in the model\n        else if (MasterPasswordAuthentication == null && MasterPasswordHash == null)\n        {\n            yield return new ValidationResult(\n                $\"{nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash)} and {nameof(MasterPasswordHash)} not found on request, one needs to be defined.\",\n                [nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash), nameof(MasterPasswordHash)]);\n        }\n\n        // 2. Validate kdf settings.\n        if (MasterPasswordUnlock != null)\n        {\n            foreach (var validationResult in KdfSettingsValidator.Validate(MasterPasswordUnlock.ToData().Kdf))\n            {\n                yield return validationResult;\n            }\n        }\n\n        if (MasterPasswordAuthentication != null)\n        {\n            foreach (var validationResult in KdfSettingsValidator.Validate(MasterPasswordAuthentication.ToData().Kdf))\n            {\n                yield return validationResult;\n            }\n        }\n\n        // 3. Validate root kdf values if kdf values are not in the unlock and authentication.\n        if (MasterPasswordUnlock == null && MasterPasswordAuthentication == null)\n        {\n            var hasMissingRequiredKdfInputs = false;\n            if (Kdf == null)\n            {\n                yield return new ValidationResult($\"{nameof(Kdf)} not found on RequestModel\", [nameof(Kdf)]);\n                hasMissingRequiredKdfInputs = true;\n            }\n            if (KdfIterations == null)\n            {\n                yield return new ValidationResult($\"{nameof(KdfIterations)} not found on RequestModel\", [nameof(KdfIterations)]);\n                hasMissingRequiredKdfInputs = true;\n            }\n\n            if (!hasMissingRequiredKdfInputs)\n            {\n                foreach (var validationResult in KdfSettingsValidator.Validate(\n                             Kdf!.Value,\n                             KdfIterations!.Value,\n                             KdfMemory,\n                             KdfParallelism))\n                {\n                    yield return validationResult;\n                }\n            }\n        }\n        else if (MasterPasswordUnlock == null && MasterPasswordAuthentication != null)\n        {\n            // Authentication provided but Unlock missing\n            yield return new ValidationResult($\"{nameof(MasterPasswordUnlock)} not found on RequestModel\", [nameof(MasterPasswordUnlock)]);\n        }\n        else if (MasterPasswordUnlock != null && MasterPasswordAuthentication == null)\n        {\n            // Unlock provided but Authentication missing\n            yield return new ValidationResult($\"{nameof(MasterPasswordAuthentication)} not found on RequestModel\", [nameof(MasterPasswordAuthentication)]);\n        }\n\n        // 3. Lastly, validate access token type and presence. Must be done last because of yield break.\n        RegisterFinishTokenType tokenType;\n        var tokenTypeResolved = true;\n        try\n        {\n            tokenType = GetTokenType();\n        }\n        catch (InvalidOperationException)\n        {\n            tokenTypeResolved = false;\n            tokenType = default;\n        }\n\n        if (!tokenTypeResolved)\n        {\n            yield return new ValidationResult(\"No valid registration token provided\");\n            yield break;\n        }\n\n        switch (tokenType)\n        {\n            case RegisterFinishTokenType.EmailVerification:\n                if (string.IsNullOrEmpty(EmailVerificationToken))\n                {\n                    yield return new ValidationResult(\n                        $\"{nameof(EmailVerificationToken)} absent when processing register/finish.\",\n                        [nameof(EmailVerificationToken)]);\n                }\n                break;\n            case RegisterFinishTokenType.OrganizationInvite:\n                if (string.IsNullOrEmpty(OrgInviteToken))\n                {\n                    yield return new ValidationResult(\n                        $\"{nameof(OrgInviteToken)} absent when processing register/finish.\",\n                        [nameof(OrgInviteToken)]);\n                }\n                break;\n            case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:\n                if (string.IsNullOrEmpty(OrgSponsoredFreeFamilyPlanToken))\n                {\n                    yield return new ValidationResult(\n                        $\"{nameof(OrgSponsoredFreeFamilyPlanToken)} absent when processing register/finish.\",\n                        [nameof(OrgSponsoredFreeFamilyPlanToken)]);\n                }\n                break;\n            case RegisterFinishTokenType.EmergencyAccessInvite:\n                if (string.IsNullOrEmpty(AcceptEmergencyAccessInviteToken))\n                {\n                    yield return new ValidationResult(\n                        $\"{nameof(AcceptEmergencyAccessInviteToken)} absent when processing register/finish.\",\n                        [nameof(AcceptEmergencyAccessInviteToken)]);\n                }\n                if (!AcceptEmergencyAccessId.HasValue || AcceptEmergencyAccessId.Value == Guid.Empty)\n                {\n                    yield return new ValidationResult(\n                        $\"{nameof(AcceptEmergencyAccessId)} absent when processing register/finish.\",\n                        [nameof(AcceptEmergencyAccessId)]);\n                }\n                break;\n            case RegisterFinishTokenType.ProviderInvite:\n                if (string.IsNullOrEmpty(ProviderInviteToken))\n                {\n                    yield return new ValidationResult(\n                        $\"{nameof(ProviderInviteToken)} absent when processing register/finish.\",\n                        [nameof(ProviderInviteToken)]);\n                }\n                if (!ProviderUserId.HasValue || ProviderUserId.Value == Guid.Empty)\n                {\n                    yield return new ValidationResult(\n                        $\"{nameof(ProviderUserId)} absent when processing register/finish.\",\n                        [nameof(ProviderUserId)]);\n                }\n                break;\n            default:\n                yield return new ValidationResult(\"Invalid registration finish request\");\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Request/Accounts/RegisterSendVerificationEmailRequestModel.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Attributes;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Models.Api.Request.Accounts;\n\npublic class RegisterSendVerificationEmailRequestModel\n{\n    [StringLength(50)] public string? Name { get; set; }\n    [StrictEmailAddress]\n    [StringLength(256)]\n    public required string Email { get; set; }\n    public bool ReceiveMarketingEmails { get; set; }\n    [MarketingInitiativeValidation]\n    public string? FromMarketing { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Request/Accounts/RegisterVerificationEmailClickedRequestModel.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Models.Api.Request.Accounts;\n\npublic class RegisterVerificationEmailClickedRequestModel\n{\n    [StrictEmailAddress]\n    [StringLength(256)]\n    public required string Email { get; set; }\n\n    public required string EmailVerificationToken { get; set; }\n\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestCreateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Auth.Enums;\n\nnamespace Bit.Core.Auth.Models.Api.Request.AuthRequest;\n\npublic class AuthRequestCreateRequestModel\n{\n    [Required]\n    public string Email { get; set; }\n    [Required]\n    public string PublicKey { get; set; }\n    [Required]\n    public string DeviceIdentifier { get; set; }\n    [Required]\n    [StringLength(25)]\n    public string AccessCode { get; set; }\n    [Required]\n    public AuthRequestType? Type { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Auth.Models.Api.Request.AuthRequest;\n\npublic class AuthRequestUpdateRequestModel\n{\n    public string Key { get; set; }\n    public string MasterPasswordHash { get; set; }\n    [Required]\n    public string DeviceIdentifier { get; set; }\n    [Required]\n    public bool RequestApproved { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Request/DeviceKeysUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Models.Api.Request;\n\npublic class OtherDeviceKeysUpdateRequestModel : DeviceKeysUpdateRequestModel\n{\n    [Required]\n    public Guid DeviceId { get; set; }\n\n    public Device ToDevice(Device existingDevice)\n    {\n        existingDevice.EncryptedPublicKey = EncryptedPublicKey;\n        existingDevice.EncryptedUserKey = EncryptedUserKey;\n        return existingDevice;\n    }\n}\n\npublic class DeviceKeysUpdateRequestModel\n{\n    [Required]\n    [EncryptedString]\n    public string EncryptedPublicKey { get; set; }\n\n    [Required]\n    [EncryptedString]\n    public string EncryptedUserKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Response/Accounts/WebAuthnLoginAssertionOptionsResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Models.Api;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.Models.Api.Response.Accounts;\n\npublic class WebAuthnLoginAssertionOptionsResponseModel : ResponseModel\n{\n    private const string ResponseObj = \"webAuthnLoginAssertionOptions\";\n\n    public WebAuthnLoginAssertionOptionsResponseModel() : base(ResponseObj)\n    {\n    }\n\n    [Required]\n    public AssertionOptions Options { get; set; }\n\n    [Required]\n    public string Token { get; set; }\n}\n\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Response/DeviceAuthRequestResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Models.Api.Response;\n\npublic class DeviceAuthRequestResponseModel : ResponseModel\n{\n    public DeviceAuthRequestResponseModel()\n        : base(\"device\") { }\n\n    public static DeviceAuthRequestResponseModel From(DeviceAuthDetails deviceAuthDetails)\n    {\n        var converted = new DeviceAuthRequestResponseModel\n        {\n            Id = deviceAuthDetails.Id,\n            Name = deviceAuthDetails.Name,\n            Type = deviceAuthDetails.Type,\n            Identifier = deviceAuthDetails.Identifier,\n            CreationDate = deviceAuthDetails.CreationDate,\n            IsTrusted = deviceAuthDetails.IsTrusted,\n            EncryptedPublicKey = deviceAuthDetails.EncryptedPublicKey,\n            EncryptedUserKey = deviceAuthDetails.EncryptedUserKey\n        };\n\n        if (deviceAuthDetails.AuthRequestId != null && deviceAuthDetails.AuthRequestCreatedAt != null)\n        {\n            converted.DevicePendingAuthRequest = new PendingAuthRequest\n            {\n                Id = (Guid)deviceAuthDetails.AuthRequestId,\n                CreationDate = (DateTime)deviceAuthDetails.AuthRequestCreatedAt\n            };\n        }\n\n        return converted;\n    }\n\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public DeviceType Type { get; set; }\n    public string Identifier { get; set; }\n    public DateTime CreationDate { get; set; }\n    public bool IsTrusted { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedUserKey { get; set; }\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedPublicKey { get; set; }\n\n    public PendingAuthRequest DevicePendingAuthRequest { get; set; }\n\n    public class PendingAuthRequest\n    {\n        public Guid Id { get; set; }\n        public DateTime CreationDate { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Response/ProtectedDeviceResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Core.Auth.Models.Api.Response;\n\npublic class ProtectedDeviceResponseModel : ResponseModel\n{\n    public ProtectedDeviceResponseModel(Device device)\n        : base(\"protectedDevice\")\n    {\n        ArgumentNullException.ThrowIfNull(device);\n\n        Id = device.Id;\n        Name = device.Name;\n        Type = device.Type;\n        Identifier = device.Identifier;\n        CreationDate = device.CreationDate;\n        EncryptedUserKey = device.EncryptedUserKey;\n        EncryptedPublicKey = device.EncryptedPublicKey;\n    }\n\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public DeviceType Type { get; set; }\n    public string Identifier { get; set; }\n    public DateTime CreationDate { get; set; }\n    public string EncryptedUserKey { get; set; }\n    public string EncryptedPublicKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.KeyManagement.Models.Api.Response;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Core.Auth.Models.Api.Response;\n\npublic class UserDecryptionOptions : ResponseModel\n{\n    public UserDecryptionOptions() : base(\"userDecryptionOptions\")\n    {\n    }\n\n    /// <summary>\n    /// Gets or sets whether the current user has a master password that can be used to decrypt their vault.\n    /// </summary>\n    [Obsolete(\"Use MasterPasswordUnlock instead. This will be removed in a future version.\")]\n    public bool HasMasterPassword { get; set; }\n\n    /// <summary>\n    /// Gets or sets whether the current user has master password unlock data available.\n    /// </summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; }\n\n    /// <summary>\n    /// Gets or sets the WebAuthn PRF decryption keys.\n    /// </summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public WebAuthnPrfDecryptionOption? WebAuthnPrfOption { get; set; }\n\n    /// <summary>\n    /// Gets or sets information regarding this users trusted device decryption setup.\n    /// </summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public TrustedDeviceUserDecryptionOption? TrustedDeviceOption { get; set; }\n\n    /// <summary>\n    /// Gets or set information about the current users KeyConnector setup.\n    /// </summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public KeyConnectorUserDecryptionOption? KeyConnectorOption { get; set; }\n}\n\npublic class WebAuthnPrfDecryptionOption\n{\n    public string EncryptedPrivateKey { get; }\n    public string EncryptedUserKey { get; }\n    public string CredentialId { get; }\n    public string[] Transports { get; }\n\n    public WebAuthnPrfDecryptionOption(\n        string encryptedPrivateKey,\n        string encryptedUserKey,\n        string credentialId,\n        string[]? transports = null)\n    {\n        EncryptedPrivateKey = encryptedPrivateKey;\n        EncryptedUserKey = encryptedUserKey;\n        CredentialId = credentialId;\n        Transports = transports ?? [];\n    }\n}\n\npublic class TrustedDeviceUserDecryptionOption\n{\n    public bool HasAdminApproval { get; }\n    public bool HasLoginApprovingDevice { get; }\n    public bool HasManageResetPasswordPermission { get; }\n    public bool IsTdeOffboarding { get; }\n    public string? EncryptedPrivateKey { get; }\n    public string? EncryptedUserKey { get; }\n\n    public TrustedDeviceUserDecryptionOption(bool hasAdminApproval,\n        bool hasLoginApprovingDevice,\n        bool hasManageResetPasswordPermission,\n        bool isTdeOffboarding,\n        string? encryptedPrivateKey,\n        string? encryptedUserKey)\n    {\n        HasAdminApproval = hasAdminApproval;\n        HasLoginApprovingDevice = hasLoginApprovingDevice;\n        HasManageResetPasswordPermission = hasManageResetPasswordPermission;\n        IsTdeOffboarding = isTdeOffboarding;\n        EncryptedPrivateKey = encryptedPrivateKey;\n        EncryptedUserKey = encryptedUserKey;\n    }\n}\n\npublic class KeyConnectorUserDecryptionOption\n{\n    public string KeyConnectorUrl { get; }\n\n    public KeyConnectorUserDecryptionOption(string keyConnectorUrl)\n    {\n        KeyConnectorUrl = keyConnectorUrl;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/ExpiringToken.cs",
    "content": "﻿namespace Bit.Core.Auth.Models.Business;\n\npublic class ExpiringToken\n{\n    public readonly string Token;\n    public readonly DateTime ExpirationDate;\n\n    public ExpiringToken(string token, DateTime expirationDate)\n    {\n        Token = token;\n        ExpirationDate = expirationDate;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/DuoUserStateTokenable.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Tokens;\nusing Newtonsoft.Json;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\npublic class DuoUserStateTokenable : Tokenable\n{\n    public const string ClearTextPrefix = \"BwDuoUserId\";\n    public const string DataProtectorPurpose = \"DuoUserIdTokenDataProtector\";\n    public const string TokenIdentifier = \"DuoUserIdToken\";\n    public string Identifier { get; set; } = TokenIdentifier;\n    public Guid UserId { get; set; }\n\n    public override bool Valid => Identifier == TokenIdentifier &&\n                                  UserId != default;\n\n    [JsonConstructor]\n    public DuoUserStateTokenable()\n    {\n    }\n\n    public DuoUserStateTokenable(User user)\n    {\n        UserId = user?.Id ?? default;\n    }\n\n    public bool TokenIsValid(User user)\n    {\n        if (UserId == default || user == null)\n        {\n            return false;\n        }\n\n        return UserId == user.Id;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/EmergencyAccessInviteTokenable.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Auth.Entities;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\npublic class EmergencyAccessInviteTokenable : Tokens.ExpiringTokenable\n{\n    public const string ClearTextPrefix = \"\";\n    public const string DataProtectorPurpose = \"EmergencyAccessServiceDataProtector\";\n    public const string TokenIdentifier = \"EmergencyAccessInvite\";\n    public string Identifier { get; set; } = TokenIdentifier;\n    public Guid Id { get; set; }\n    public string Email { get; set; }\n\n    [JsonConstructor]\n    public EmergencyAccessInviteTokenable(DateTime expirationDate)\n    {\n        ExpirationDate = expirationDate;\n    }\n\n    public EmergencyAccessInviteTokenable(EmergencyAccess user, int hoursTillExpiration)\n    {\n        Id = user.Id;\n        Email = user.Email;\n        ExpirationDate = DateTime.UtcNow.AddHours(hoursTillExpiration);\n    }\n\n    public bool IsValid(Guid id, string email)\n    {\n        return Id == id &&\n            Email.Equals(email, StringComparison.InvariantCultureIgnoreCase);\n    }\n\n    protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email);\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/IOrgUserInviteTokenableFactory.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\npublic interface IOrgUserInviteTokenableFactory\n{\n    OrgUserInviteTokenable CreateToken(OrganizationUser orgUser);\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\npublic class OrgUserInviteTokenable : ExpiringTokenable\n{\n    // TODO: PM-4317 - Ideally this would be internal and only visible to the test project.\n    // but configuring that is out of scope for these changes.\n    public static TimeSpan GetTokenLifetime() => TimeSpan.FromDays(5);\n\n    public const string ClearTextPrefix = \"BwOrgUserInviteToken_\";\n\n    // Backwards compatibility Note:\n    // Previously, tokens were manually created in the OrganizationService using a data protector\n    // initialized with purpose: \"OrganizationServiceDataProtector\"\n    // So, we must continue to use the existing purpose to be able to decrypt tokens\n    // in emailed invites that have not yet been accepted.\n    public const string DataProtectorPurpose = \"OrganizationServiceDataProtector\";\n\n    public const string TokenIdentifier = \"OrgUserInviteToken\";\n\n    public string Identifier { get; set; } = TokenIdentifier;\n    public Guid OrgUserId { get; set; }\n    public string? OrgUserEmail { get; set; }\n\n    [JsonConstructor]\n    public OrgUserInviteTokenable()\n    {\n        ExpirationDate = DateTime.UtcNow.Add(GetTokenLifetime());\n    }\n\n    public OrgUserInviteTokenable(OrganizationUser orgUser) : this()\n    {\n        OrgUserId = orgUser?.Id ?? default;\n        OrgUserEmail = orgUser?.Email;\n    }\n\n    public bool TokenIsValid(OrganizationUser orgUser)\n    {\n        if (OrgUserId == default || OrgUserEmail == default || orgUser == null)\n        {\n            return false;\n        }\n\n        return OrgUserId == orgUser.Id &&\n               OrgUserEmail.Equals(orgUser.Email, StringComparison.InvariantCultureIgnoreCase);\n    }\n\n    public bool TokenIsValid(Guid orgUserId, string orgUserEmail)\n    {\n        if (OrgUserId == default || OrgUserEmail == default || orgUserId == default || orgUserEmail == default)\n        {\n            return false;\n        }\n\n        return OrgUserId == orgUserId &&\n               OrgUserEmail.Equals(orgUserEmail, StringComparison.InvariantCultureIgnoreCase);\n    }\n\n    // Validates deserialized\n    protected override bool TokenIsValid() =>\n        Identifier == TokenIdentifier && OrgUserId != default && !string.IsNullOrWhiteSpace(OrgUserEmail);\n\n\n    public static bool ValidateOrgUserInviteStringToken(\n        IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,\n        string orgUserInviteToken, OrganizationUser orgUser)\n    {\n        return orgUserInviteTokenDataFactory.TryUnprotect(orgUserInviteToken, out var decryptedToken)\n               && decryptedToken.Valid\n               && decryptedToken.TokenIsValid(orgUser);\n    }\n\n    public static bool ValidateOrgUserInviteStringToken(\n        IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,\n        string orgUserInviteToken, Guid orgUserId, string orgUserEmail)\n    {\n        return orgUserInviteTokenDataFactory.TryUnprotect(orgUserInviteToken, out var decryptedToken)\n               && decryptedToken.Valid\n               && decryptedToken.TokenIsValid(orgUserId, orgUserEmail);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenableFactory.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\npublic class OrgUserInviteTokenableFactory : IOrgUserInviteTokenableFactory\n{\n    private readonly IGlobalSettings _globalSettings;\n\n    public OrgUserInviteTokenableFactory(IGlobalSettings globalSettings)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public OrgUserInviteTokenable CreateToken(OrganizationUser orgUser)\n    {\n        var token = new OrgUserInviteTokenable(orgUser)\n        {\n            ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromHours(_globalSettings.OrganizationInviteExpirationHours))\n        };\n        return token;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/RegistrationEmailVerificationTokenable.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\n// <summary>\n// This token contains encrypted registration information for new users. The token is sent via email for verification as\n// part of a link to complete the registration process.\n// </summary>\npublic class RegistrationEmailVerificationTokenable : ExpiringTokenable\n{\n    public static TimeSpan GetTokenLifetime() => TimeSpan.FromMinutes(15);\n\n    public const string ClearTextPrefix = \"BwRegistrationEmailVerificationToken_\";\n    public const string DataProtectorPurpose = \"RegistrationEmailVerificationTokenDataProtector\";\n    public const string TokenIdentifier = \"RegistrationEmailVerificationToken\";\n\n    public string Identifier { get; set; } = TokenIdentifier;\n\n    public string Name { get; set; }\n    public string Email { get; set; }\n    public bool ReceiveMarketingEmails { get; set; }\n\n    [JsonConstructor]\n    public RegistrationEmailVerificationTokenable()\n    {\n        ExpirationDate = DateTime.UtcNow.Add(GetTokenLifetime());\n    }\n\n    public RegistrationEmailVerificationTokenable(string email, string name = default, bool receiveMarketingEmails = default) : this()\n    {\n        if (string.IsNullOrEmpty(email))\n        {\n            throw new ArgumentNullException(nameof(email));\n        }\n\n        Email = email;\n        Name = name;\n        ReceiveMarketingEmails = receiveMarketingEmails;\n    }\n\n    public bool TokenIsValid(string email)\n    {\n        if (Email == default || email == default)\n        {\n            return false;\n        }\n\n        return Email.Equals(email, StringComparison.InvariantCultureIgnoreCase);\n    }\n\n    // Validates deserialized\n    protected override bool TokenIsValid() =>\n        Identifier == TokenIdentifier\n        && !string.IsNullOrWhiteSpace(Email);\n\n\n    public static bool ValidateToken(IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> dataProtectorTokenFactory, string token, string userEmail)\n    {\n        return dataProtectorTokenFactory.TryUnprotect(token, out var tokenable)\n               && tokenable.Valid\n               && tokenable.TokenIsValid(userEmail);\n    }\n\n\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\n/// <summary>\n/// This token provides a verifiable authN mechanism for the TwoFactorController.SendEmailLoginAsync\n/// anonymous endpoint so it cannot used maliciously.\n/// </summary>\npublic class SsoEmail2faSessionTokenable : ExpiringTokenable\n{\n    // Just over 2 min expiration (client expires session after 2 min)\n    public static TimeSpan GetTokenLifetime() => TimeSpan.FromMinutes(2.05);\n\n    public const string ClearTextPrefix = \"BwSsoEmail2FaSessionToken_\";\n    public const string DataProtectorPurpose = \"SsoEmail2faSessionTokenDataProtector\";\n\n    public const string TokenIdentifier = \"SsoEmail2faSessionToken\";\n    public string Identifier { get; set; } = TokenIdentifier;\n    public Guid Id { get; set; }\n    public string Email { get; set; }\n    [JsonConstructor]\n    public SsoEmail2faSessionTokenable()\n    {\n        ExpirationDate = DateTime.UtcNow.Add(GetTokenLifetime());\n    }\n\n    public SsoEmail2faSessionTokenable(User user) : this()\n    {\n        Id = user?.Id ?? default;\n        Email = user?.Email;\n    }\n    public bool TokenIsValid(User user)\n    {\n        if (Id == default || Email == default || user == null)\n        {\n            return false;\n        }\n        return Id == user.Id &&\n               Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);\n    }\n\n    // Validates deserialized\n    protected override bool TokenIsValid() =>\n        Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email);\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/SsoTokenable.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\npublic class SsoTokenable : ExpiringTokenable\n{\n    public const string ClearTextPrefix = \"BWUserPrefix_\";\n    public const string DataProtectorPurpose = \"SsoTokenDataProtector\";\n    public const string TokenIdentifier = \"ssoToken\";\n\n    public Guid OrganizationId { get; set; }\n    public string DomainHint { get; set; }\n    public string Identifier { get; set; } = TokenIdentifier;\n\n    [JsonConstructor]\n    public SsoTokenable() { }\n\n    public SsoTokenable(Organization organization, double tokenLifetimeInSeconds) : this()\n    {\n        OrganizationId = organization?.Id ?? default;\n        DomainHint = organization?.Identifier;\n        ExpirationDate = DateTime.UtcNow.AddSeconds(tokenLifetimeInSeconds);\n    }\n\n    public bool TokenIsValid(Organization organization)\n    {\n        if (OrganizationId == default || DomainHint == default || organization == null || !Valid)\n        {\n            return false;\n        }\n\n        return organization.Identifier.Equals(DomainHint, StringComparison.InvariantCultureIgnoreCase)\n            && organization.Id.Equals(OrganizationId);\n    }\n\n    // Validates deserialized\n    protected override bool TokenIsValid() =>\n        Identifier == TokenIdentifier\n        && OrganizationId != default\n        && !string.IsNullOrWhiteSpace(DomainHint);\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/TwoFactorAuthenticatorUserVerificationTokenable.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\nusing Newtonsoft.Json;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\n/// <summary>\n/// A tokenable object that gives a user the ability to update their authenticator two factor settings.\n/// </summary>\n/// <remarks>\n/// We protect two factor updates behind user verification (re-authentication) to protect against attacks of opportunity\n/// (e.g. a user leaves their web vault unlocked). Most two factor options only require user verification (UV) when\n/// enabling or disabling the option, retrieving the current status usually isn't a sensitive operation. However,\n/// the status of authenticator two factor is sensitive because it reveals the user's secret key, which means both\n/// operations must be protected by UV.\n///\n/// TOTP as a UV option is only allowed to be used once, so we return this tokenable when retrieving the current status\n/// (and secret key) of authenticator two factor to give the user a means of passing UV when updating (enabling/disabling).\n/// </remarks>\npublic class TwoFactorAuthenticatorUserVerificationTokenable : ExpiringTokenable\n{\n    private static readonly TimeSpan _tokenLifetime = TimeSpan.FromMinutes(30);\n\n    public const string ClearTextPrefix = \"TwoFactorAuthenticatorUserVerification\";\n    public const string DataProtectorPurpose = \"TwoFactorAuthenticatorUserVerificationTokenDataProtector\";\n    public const string TokenIdentifier = \"TwoFactorAuthenticatorUserVerificationToken\";\n    public string Identifier { get; set; } = TokenIdentifier;\n    public Guid UserId { get; set; }\n    public string Key { get; set; }\n\n    public override bool Valid => Identifier == TokenIdentifier &&\n                                  UserId != default;\n\n    [JsonConstructor]\n    public TwoFactorAuthenticatorUserVerificationTokenable()\n    {\n        ExpirationDate = DateTime.UtcNow.Add(_tokenLifetime);\n    }\n\n    public TwoFactorAuthenticatorUserVerificationTokenable(User user, string key) : this()\n    {\n        UserId = user?.Id ?? default;\n        Key = key;\n    }\n\n    public bool TokenIsValid(User user, string key)\n    {\n        if (UserId == default\n            || user == null\n            || string.IsNullOrWhiteSpace(key))\n        {\n            return false;\n        }\n\n        return UserId == user.Id && Key == key;\n    }\n\n    protected override bool TokenIsValid() =>\n        Identifier == TokenIdentifier\n        && UserId != default\n        && !string.IsNullOrWhiteSpace(Key);\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/WebAuthnCredentialCreateOptionsTokenable.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\npublic class WebAuthnCredentialCreateOptionsTokenable : ExpiringTokenable\n{\n    // 7 minutes = max webauthn timeout (6 minutes) + slack for miscellaneous delays\n    private const double _tokenLifetimeInHours = (double)7 / 60;\n    public const string ClearTextPrefix = \"BWWebAuthnCredentialCreateOptions_\";\n    public const string DataProtectorPurpose = \"WebAuthnCredentialCreateDataProtector\";\n    public const string TokenIdentifier = \"WebAuthnCredentialCreateOptionsToken\";\n\n    public string Identifier { get; set; } = TokenIdentifier;\n    public Guid? UserId { get; set; }\n    public CredentialCreateOptions Options { get; set; }\n\n    [JsonConstructor]\n    public WebAuthnCredentialCreateOptionsTokenable()\n    {\n        ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours);\n    }\n\n    public WebAuthnCredentialCreateOptionsTokenable(User user, CredentialCreateOptions options) : this()\n    {\n        UserId = user?.Id;\n        Options = options;\n    }\n\n    public bool TokenIsValid(User user)\n    {\n        if (!Valid || user == null)\n        {\n            return false;\n        }\n\n        return UserId == user.Id;\n    }\n\n    protected override bool TokenIsValid() => Identifier == TokenIdentifier && UserId != null && Options != null;\n}\n\n"
  },
  {
    "path": "src/Core/Auth/Models/Business/Tokenables/WebAuthnLoginAssertionOptionsTokenable.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Tokens;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.Models.Business.Tokenables;\n\npublic class WebAuthnLoginAssertionOptionsTokenable : ExpiringTokenable\n{\n    // Lifetime 17 minutes =\n    //  - 6 Minutes for Attestation (max webauthn timeout)\n    //  - 6 Minutes for PRF Assertion (max webauthn timeout)\n    //  - 5 minutes for user to complete the process (name their passkey, etc)\n    private static readonly TimeSpan _tokenLifetime = TimeSpan.FromMinutes(17);\n    public const string ClearTextPrefix = \"BWWebAuthnLoginAssertionOptions_\";\n    public const string DataProtectorPurpose = \"WebAuthnLoginAssertionOptionsDataProtector\";\n    public const string TokenIdentifier = \"WebAuthnLoginAssertionOptionsToken\";\n\n    public string Identifier { get; set; } = TokenIdentifier;\n    public AssertionOptions Options { get; set; }\n    public WebAuthnLoginAssertionOptionsScope Scope { get; set; }\n\n    [JsonConstructor]\n    public WebAuthnLoginAssertionOptionsTokenable()\n    {\n        ExpirationDate = DateTime.UtcNow.Add(_tokenLifetime);\n    }\n\n    public WebAuthnLoginAssertionOptionsTokenable(WebAuthnLoginAssertionOptionsScope scope, AssertionOptions options) : this()\n    {\n        Scope = scope;\n        Options = options;\n    }\n\n    public bool TokenIsValid(WebAuthnLoginAssertionOptionsScope scope)\n    {\n        if (!Valid)\n        {\n            return false;\n        }\n\n        return Scope == scope;\n    }\n\n    protected override bool TokenIsValid() => Identifier == TokenIdentifier && Options != null;\n}\n\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/DeviceAuthDetails.cs",
    "content": "﻿using Bit.Core.Auth.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Auth.Models.Data;\n\npublic class DeviceAuthDetails : Device\n{\n    public bool IsTrusted { get; set; }\n    public Guid? AuthRequestId { get; set; }\n    public DateTime? AuthRequestCreatedAt { get; set; }\n\n    /**\n     * Constructor for EF response.\n     */\n    public DeviceAuthDetails(\n        Device device,\n        Guid? authRequestId,\n        DateTime? authRequestCreationDate)\n    {\n        if (device == null)\n        {\n            throw new ArgumentNullException(nameof(device));\n        }\n\n        Id = device.Id;\n        Name = device.Name;\n        Type = device.Type;\n        Identifier = device.Identifier;\n        CreationDate = device.CreationDate;\n        IsTrusted = device.IsTrusted();\n        EncryptedPublicKey = device.EncryptedPublicKey;\n        EncryptedUserKey = device.EncryptedUserKey;\n        AuthRequestId = authRequestId;\n        AuthRequestCreatedAt = authRequestCreationDate;\n    }\n\n    /**\n     * Constructor for dapper response.\n     * Note: if the authRequestId or authRequestCreationDate is null it comes back as\n     * an empty guid and a min value for datetime. That could change if the stored\n     * procedure runs on a different kind of db.\n     */\n    public DeviceAuthDetails(\n        Guid id,\n        Guid userId,\n        string name,\n        short type,\n        string identifier,\n        string pushToken,\n        DateTime creationDate,\n        DateTime revisionDate,\n        string encryptedUserKey,\n        string encryptedPublicKey,\n        string encryptedPrivateKey,\n        bool active,\n        Guid authRequestId,\n        DateTime authRequestCreationDate)\n    {\n        Id = id;\n        Name = name;\n        Type = (DeviceType)type;\n        Identifier = identifier;\n        CreationDate = creationDate;\n        IsTrusted = new Device\n        {\n            Id = id,\n            UserId = userId,\n            Name = name,\n            Type = (DeviceType)type,\n            Identifier = identifier,\n            PushToken = pushToken,\n            RevisionDate = revisionDate,\n            EncryptedUserKey = encryptedUserKey,\n            EncryptedPublicKey = encryptedPublicKey,\n            EncryptedPrivateKey = encryptedPrivateKey,\n            Active = active\n        }.IsTrusted();\n        EncryptedPublicKey = encryptedPublicKey;\n        EncryptedUserKey = encryptedUserKey;\n        AuthRequestId = authRequestId != Guid.Empty ? authRequestId : null;\n        AuthRequestCreatedAt =\n            authRequestCreationDate != DateTime.MinValue ? authRequestCreationDate : null;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/EmergencyAccessDetails.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\n\nnamespace Bit.Core.Auth.Models.Data;\n\npublic class EmergencyAccessDetails : EmergencyAccess\n{\n    public string? GranteeName { get; set; }\n    public string? GranteeEmail { get; set; }\n    public string? GranteeAvatarColor { get; set; }\n    public string? GrantorName { get; set; }\n    public string? GrantorEmail { get; set; }\n    public string? GrantorAvatarColor { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/EmergencyAccessNotify.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\n\nnamespace Bit.Core.Auth.Models.Data;\n\npublic class EmergencyAccessNotify : EmergencyAccess\n{\n    public string? GrantorEmail { get; set; }\n    public string? GranteeName { get; set; }\n    public string? GranteeEmail { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/EmergencyAccessViewData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Auth.Models.Data;\n\npublic class EmergencyAccessViewData\n{\n    public EmergencyAccess EmergencyAccess { get; set; }\n    public IEnumerable<CipherDetails> Ciphers { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/GrantItem.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Auth.Repositories.Cosmos;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Core.Auth.Models.Data;\n\npublic class GrantItem : IGrant\n{\n    public GrantItem() { }\n\n    public GrantItem(PersistedGrant pGrant)\n    {\n        Key = pGrant.Key;\n        Type = pGrant.Type;\n        SubjectId = pGrant.SubjectId;\n        SessionId = pGrant.SessionId;\n        ClientId = pGrant.ClientId;\n        Description = pGrant.Description;\n        CreationDate = pGrant.CreationTime;\n        ExpirationDate = pGrant.Expiration;\n        ConsumedDate = pGrant.ConsumedTime;\n        Data = pGrant.Data;\n        SetTtl();\n    }\n\n    public GrantItem(IGrant g)\n    {\n        Key = g.Key;\n        Type = g.Type;\n        SubjectId = g.SubjectId;\n        SessionId = g.SessionId;\n        ClientId = g.ClientId;\n        Description = g.Description;\n        CreationDate = g.CreationDate;\n        ExpirationDate = g.ExpirationDate;\n        ConsumedDate = g.ConsumedDate;\n        Data = g.Data;\n        SetTtl();\n    }\n\n    [JsonPropertyName(\"id\")]\n    [JsonConverter(typeof(Base64IdStringConverter))]\n    public string Key { get; set; }\n    [JsonPropertyName(\"typ\")]\n    public string Type { get; set; }\n    [JsonPropertyName(\"sub\")]\n    public string SubjectId { get; set; }\n    [JsonPropertyName(\"sid\")]\n    public string SessionId { get; set; }\n    [JsonPropertyName(\"cid\")]\n    public string ClientId { get; set; }\n    [JsonPropertyName(\"des\")]\n    public string Description { get; set; }\n    [JsonPropertyName(\"cre\")]\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    [JsonPropertyName(\"exp\")]\n    public DateTime? ExpirationDate { get; set; }\n    [JsonPropertyName(\"con\")]\n    public DateTime? ConsumedDate { get; set; }\n    [JsonPropertyName(\"data\")]\n    public string Data { get; set; }\n    // https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-time-to-live?tabs=dotnet-sdk-v3#set-time-to-live-on-an-item-using-an-sdk\n    [JsonPropertyName(\"ttl\")]\n    public int Ttl { get; set; } = -1;\n\n    public void SetTtl()\n    {\n        if (ExpirationDate != null)\n        {\n            var sec = (ExpirationDate.Value - DateTime.UtcNow).TotalSeconds;\n            if (sec > 0)\n            {\n                Ttl = (int)sec;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/IGrant.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Auth.Models.Data;\n\npublic interface IGrant\n{\n    string Key { get; set; }\n    string Type { get; set; }\n    string SubjectId { get; set; }\n    string SessionId { get; set; }\n    string ClientId { get; set; }\n    string Description { get; set; }\n    DateTime CreationDate { get; set; }\n    DateTime? ExpirationDate { get; set; }\n    DateTime? ConsumedDate { get; set; }\n    string Data { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/OrganizationAdminAuthRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\n\nnamespace Bit.Core.Auth.Models.Data;\n\n/// <summary>\n/// Represents an <see cref=\"AuthRequestType.AdminApproval\"/> AuthRequest.\n/// Includes additional user and organization information.\n/// </summary>\npublic class OrganizationAdminAuthRequest : AuthRequest\n{\n    /// <summary>\n    /// Email address of the requesting user\n    /// </summary>\n    public string Email { get; set; }\n\n    public Guid OrganizationUserId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs",
    "content": "﻿\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Auth.Models.Data;\n\npublic class PendingAuthRequestDetails : AuthRequest\n{\n    public Guid? RequestDeviceId { get; set; }\n\n    /**\n     * Constructor for EF response.\n     */\n    public PendingAuthRequestDetails(\n        AuthRequest authRequest,\n        Guid? deviceId)\n    {\n        ArgumentNullException.ThrowIfNull(authRequest);\n\n        Id = authRequest.Id;\n        UserId = authRequest.UserId;\n        OrganizationId = authRequest.OrganizationId;\n        Type = authRequest.Type;\n        RequestDeviceIdentifier = authRequest.RequestDeviceIdentifier;\n        RequestDeviceType = authRequest.RequestDeviceType;\n        RequestIpAddress = authRequest.RequestIpAddress;\n        RequestCountryName = authRequest.RequestCountryName;\n        ResponseDeviceId = authRequest.ResponseDeviceId;\n        AccessCode = authRequest.AccessCode;\n        PublicKey = authRequest.PublicKey;\n        Key = authRequest.Key;\n        MasterPasswordHash = authRequest.MasterPasswordHash;\n        Approved = authRequest.Approved;\n        CreationDate = authRequest.CreationDate;\n        ResponseDate = authRequest.ResponseDate;\n        AuthenticationDate = authRequest.AuthenticationDate;\n        RequestDeviceId = deviceId;\n    }\n\n    /**\n     * Constructor for dapper response.\n     */\n    public PendingAuthRequestDetails(\n        Guid id,\n        Guid userId,\n        Guid organizationId,\n        short type,\n        string requestDeviceIdentifier,\n        short requestDeviceType,\n        string requestIpAddress,\n        string requestCountryName,\n        Guid? responseDeviceId,\n        string accessCode,\n        string publicKey,\n        string key,\n        string masterPasswordHash,\n        bool? approved,\n        DateTime creationDate,\n        DateTime? responseDate,\n        DateTime? authenticationDate,\n        Guid deviceId)\n    {\n        Id = id;\n        UserId = userId;\n        OrganizationId = organizationId;\n        Type = (AuthRequestType)type;\n        RequestDeviceIdentifier = requestDeviceIdentifier;\n        RequestDeviceType = (DeviceType)requestDeviceType;\n        RequestIpAddress = requestIpAddress;\n        RequestCountryName = requestCountryName;\n        ResponseDeviceId = responseDeviceId;\n        AccessCode = accessCode;\n        PublicKey = publicKey;\n        Key = key;\n        MasterPasswordHash = masterPasswordHash;\n        Approved = approved;\n        CreationDate = creationDate;\n        ResponseDate = responseDate;\n        AuthenticationDate = authenticationDate;\n        RequestDeviceId = deviceId;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/SetInitialMasterPasswordDataModel.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.Auth.Models.Data;\n\n/// <summary>\n/// Data model for setting an initial master password for a user.\n/// </summary>\npublic class SetInitialMasterPasswordDataModel\n{\n    public required MasterPasswordAuthenticationData MasterPasswordAuthentication { get; set; }\n    public required MasterPasswordUnlockData MasterPasswordUnlock { get; set; }\n\n    /// <summary>\n    /// Organization SSO identifier.\n    /// </summary>\n    public required string OrgSsoIdentifier { get; set; }\n\n    /// <summary>\n    /// User account keys. Required for Master Password decryption user.\n    /// </summary>\n    public required UserAccountKeysData? AccountKeys { get; set; }\n    public string? MasterPasswordHint { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/SsoConfigurationData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\n\nnamespace Bit.Core.Auth.Models.Data;\n\npublic class SsoConfigurationData\n{\n    private static string _oidcSigninPath = \"/oidc-signin\";\n    private static string _oidcSignedOutPath = \"/oidc-signedout\";\n    private static string _saml2ModulePath = \"/saml2\";\n\n    public static SsoConfigurationData Deserialize(string data)\n    {\n        return CoreHelpers.LoadClassFromJsonData<SsoConfigurationData>(data);\n    }\n\n    public string Serialize()\n    {\n        return CoreHelpers.ClassToJsonData(this);\n    }\n\n    public SsoType ConfigType { get; set; }\n\n    public MemberDecryptionType MemberDecryptionType { get; set; }\n\n    /// <summary>\n    /// Legacy property to determine if KeyConnector was enabled.\n    /// Kept for backwards compatibility with old configs that will not have\n    /// the new <see cref=\"MemberDecryptionType\"/> when deserialized from the database.\n    /// </summary>\n    [Obsolete(\"Use MemberDecryptionType instead\")]\n    public bool KeyConnectorEnabled\n    {\n        get => MemberDecryptionType == MemberDecryptionType.KeyConnector;\n        set\n        {\n            if (value)\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector;\n            }\n        }\n    }\n    public string KeyConnectorUrl { get; set; }\n\n    // OIDC\n    public string Authority { get; set; }\n    public string ClientId { get; set; }\n    public string ClientSecret { get; set; }\n    public string MetadataAddress { get; set; }\n    public OpenIdConnectRedirectBehavior RedirectBehavior { get; set; }\n    public bool GetClaimsFromUserInfoEndpoint { get; set; }\n    public string AdditionalScopes { get; set; }\n    public string AdditionalUserIdClaimTypes { get; set; }\n    public string AdditionalEmailClaimTypes { get; set; }\n    public string AdditionalNameClaimTypes { get; set; }\n    public string AcrValues { get; set; }\n    public string ExpectedReturnAcrValue { get; set; }\n\n    // SAML2 IDP\n    public string IdpEntityId { get; set; }\n    public string IdpSingleSignOnServiceUrl { get; set; }\n    public string IdpSingleLogoutServiceUrl { get; set; }\n    public string IdpX509PublicCert { get; set; }\n    public Saml2BindingType IdpBindingType { get; set; }\n    public bool IdpAllowUnsolicitedAuthnResponse { get; set; }\n    public string IdpArtifactResolutionServiceUrl { get => null; set { /*IGNORE*/ } }\n    public bool IdpDisableOutboundLogoutRequests { get; set; }\n    public string IdpOutboundSigningAlgorithm { get; set; }\n    public bool IdpWantAuthnRequestsSigned { get; set; }\n\n    // SAML2 SP\n    public bool SpUniqueEntityId { get; set; }\n    public Saml2NameIdFormat SpNameIdFormat { get; set; }\n    public string SpOutboundSigningAlgorithm { get; set; }\n    public Saml2SigningBehavior SpSigningBehavior { get; set; }\n    public bool SpWantAssertionsSigned { get; set; }\n    public bool SpValidateCertificates { get; set; }\n    public string SpMinIncomingSigningAlgorithm { get; set; }\n\n    public static string BuildCallbackPath(string ssoUri = null)\n    {\n        return BuildSsoUrl(_oidcSigninPath, ssoUri);\n    }\n\n    public static string BuildSignedOutCallbackPath(string ssoUri = null)\n    {\n        return BuildSsoUrl(_oidcSignedOutPath, ssoUri);\n    }\n\n    public static string BuildSaml2ModulePath(string ssoUri = null, string scheme = null)\n    {\n        return string.Concat(BuildSsoUrl(_saml2ModulePath, ssoUri),\n            string.IsNullOrWhiteSpace(scheme) ? string.Empty : $\"/{scheme}\");\n    }\n\n    public static string BuildSaml2AcsUrl(string ssoUri = null, string scheme = null)\n    {\n        return string.Concat(BuildSaml2ModulePath(ssoUri, scheme), \"/Acs\");\n    }\n\n    public static string BuildSaml2MetadataUrl(string ssoUri = null, string scheme = null)\n    {\n        return BuildSaml2ModulePath(ssoUri, scheme);\n    }\n\n    public IEnumerable<string> GetAdditionalScopes() => AdditionalScopes?\n        .Split(',')?\n        .Where(c => !string.IsNullOrWhiteSpace(c))?\n        .Select(c => c.Trim()) ??\n        Array.Empty<string>();\n\n    public IEnumerable<string> GetAdditionalUserIdClaimTypes() => AdditionalUserIdClaimTypes?\n        .Split(',')?\n        .Where(c => !string.IsNullOrWhiteSpace(c))?\n        .Select(c => c.Trim()) ??\n        Array.Empty<string>();\n\n    public IEnumerable<string> GetAdditionalEmailClaimTypes() => AdditionalEmailClaimTypes?\n        .Split(',')?\n        .Where(c => !string.IsNullOrWhiteSpace(c))?\n        .Select(c => c.Trim()) ??\n        Array.Empty<string>();\n\n    public IEnumerable<string> GetAdditionalNameClaimTypes() => AdditionalNameClaimTypes?\n        .Split(',')?\n        .Where(c => !string.IsNullOrWhiteSpace(c))?\n        .Select(c => c.Trim()) ??\n        Array.Empty<string>();\n\n    private static string BuildSsoUrl(string relativePath, string ssoUri)\n    {\n        if (string.IsNullOrWhiteSpace(ssoUri) ||\n            !Uri.IsWellFormedUriString(ssoUri, UriKind.Absolute))\n        {\n            return relativePath;\n        }\n        if (Uri.TryCreate(string.Concat(ssoUri.TrimEnd('/'), relativePath), UriKind.Absolute, out var newUri))\n        {\n            return newUri.ToString();\n        }\n        return relativePath;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Auth.Models.Data;\n\npublic class WebAuthnLoginRotateKeyData\n{\n    [Required]\n    public Guid Id { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedUserKey { get; set; }\n\n    [Required]\n    [EncryptedString]\n    [EncryptedStringLength(2000)]\n    public string EncryptedPublicKey { get; set; }\n\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/ITwoFactorProvidersUser.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.Auth.Models;\n\n/// <summary>\n/// An interface representing a user entity that supports two-factor providers\n/// </summary>\npublic interface ITwoFactorProvidersUser\n{\n    string? TwoFactorProviders { get; }\n    /// <summary>\n    /// Get the two factor providers for the user. Currently it can be assumed providers are enabled\n    /// if they exists in the dictionary. When two factor providers are disabled they are removed\n    /// from the dictionary. <see cref=\"IUserService.DisableTwoFactorProviderAsync\"/>\n    /// <see cref=\"IOrganizationService.DisableTwoFactorProviderAsync\"/>\n    /// </summary>\n    /// <returns>Dictionary of providers with the type enum as the key</returns>\n    Dictionary<TwoFactorProviderType, TwoFactorProvider>? GetTwoFactorProviders();\n    /// <summary>\n    /// The unique `UserId` of the user entity for which there are two-factor providers configured.\n    /// </summary>\n    /// <returns>The unique identifier for the user</returns>\n    Guid? GetUserId();\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/CannotDeleteClaimedAccountViewModel.cs",
    "content": "﻿using Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Auth.Models.Mail;\n\npublic class CannotDeleteClaimedAccountViewModel : BaseMailModel\n{\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/EmergencyAccessAcceptedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class EmergencyAccessAcceptedViewModel : BaseMailModel\n{\n    public string GranteeEmail { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/EmergencyAccessApprovedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class EmergencyAccessApprovedViewModel : BaseMailModel\n{\n    public string Name { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/EmergencyAccessConfirmedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class EmergencyAccessConfirmedViewModel : BaseMailModel\n{\n    public string Name { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/EmergencyAccessInvitedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class EmergencyAccessInvitedViewModel : BaseMailModel\n{\n    public string Name { get; set; }\n    public string Id { get; set; }\n    public string Email { get; set; }\n    public string Token { get; set; }\n    public string Url => $\"{WebVaultUrl}/accept-emergency?id={Id}&name={Name}&email={Email}&token={Token}\";\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/EmergencyAccessRecoveryTimedOutViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class EmergencyAccessRecoveryTimedOutViewModel : BaseMailModel\n{\n    public string Name { get; set; }\n    public string Action { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/EmergencyAccessRecoveryViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class EmergencyAccessRecoveryViewModel : BaseMailModel\n{\n    public string Name { get; set; }\n    public string Action { get; set; }\n    public int DaysLeft { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/EmergencyAccessRejectedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class EmergencyAccessRejectedViewModel : BaseMailModel\n{\n    public string Name { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/FailedAuthAttemptModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Auth.Models.Mail;\n\npublic class FailedAuthAttemptModel : NewDeviceLoggedInModel\n{\n    public string AffectedEmail { get; set; }\n    public TwoFactorProviderType TwoFactorType { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/MasterPasswordHintViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Auth.Models.Mail;\n\npublic class MasterPasswordHintViewModel : BaseMailModel\n{\n    public string Hint { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/PasswordlessSignInModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Auth.Models.Mail;\n\npublic class PasswordlessSignInModel\n{\n    public string Url { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/RecoverTwoFactorModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Auth.Models.Mail;\n\npublic class RecoverTwoFactorModel : BaseMailModel\n{\n    public string TheDate { get; set; }\n    public string TheTime { get; set; }\n    public string TimeZone { get; set; }\n    public string IpAddress { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/RegisterVerifyEmail.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Auth.Models.Mail;\n\npublic class RegisterVerifyEmail : BaseMailModel\n{\n    // Note 1: We must include email in the URL even though it is already in the token so that the\n    // client can use it to create the master key when they set their password.\n    // We also have to include the fromEmail flag so that the client knows the user\n    // is coming to the finish signup page from an email link and not directly from another route in the app.\n    // Note 2: we cannot use a web vault url which contains a # as that is a reserved wild character on Android\n    // so we must land on a redirect connector which will redirect to the finish signup page.\n    // Note 3: The use of a fragment to indicate the redirect url is to prevent the query string from being logged by\n    // proxies and servers. It also helps reduce open redirect vulnerabilities.\n    public string Url => string.Format(\"{0}/redirect-connector.html#finish-signup?token={1}&email={2}&fromEmail=true{3}\",\n        WebVaultUrl,\n        Token,\n        Email,\n        !string.IsNullOrEmpty(FromMarketing) ? $\"&fromMarketing={FromMarketing}\" : string.Empty);\n\n    public string Token { get; set; }\n    public string Email { get; set; }\n    public string FromMarketing { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/VerifyDeleteModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Auth.Models.Mail;\n\npublic class VerifyDeleteModel : BaseMailModel\n{\n    public string Url => string.Format(\"{0}/verify-recover-delete?userId={1}&token={2}&email={3}\",\n        WebVaultUrl,\n        UserId,\n        Token,\n        EmailEncoded);\n\n    public Guid UserId { get; set; }\n    public string Email { get; set; }\n    public string EmailEncoded { get; set; }\n    public string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/Mail/VerifyEmailModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Auth.Models.Mail;\n\npublic class VerifyEmailModel : BaseMailModel\n{\n    public string Url => string.Format(\"{0}/verify-email?userId={1}&token={2}\",\n        WebVaultUrl,\n        UserId,\n        Token);\n\n    public Guid UserId { get; set; }\n    public string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Models/TwoFactorProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Auth.Enums;\nusing Fido2NetLib.Objects;\n\nnamespace Bit.Core.Auth.Models;\n\npublic class TwoFactorProvider\n{\n    public bool Enabled { get; set; }\n    public Dictionary<string, object> MetaData { get; set; } = new Dictionary<string, object>();\n\n    public class WebAuthnData\n    {\n        public WebAuthnData() { }\n\n        public WebAuthnData(dynamic o)\n        {\n            Name = o.Name;\n            try\n            {\n                Descriptor = o.Descriptor;\n            }\n            catch\n            {\n                // Fallback for older newtonsoft serialized tokens.\n                if (o.Descriptor.Type == 0)\n                {\n                    o.Descriptor.Type = \"public-key\";\n                }\n                Descriptor = JsonSerializer.Deserialize<PublicKeyCredentialDescriptor>(o.Descriptor.ToString(),\n                    new JsonSerializerOptions { PropertyNameCaseInsensitive = true });\n            }\n            PublicKey = o.PublicKey;\n            UserHandle = o.UserHandle;\n            SignatureCounter = o.SignatureCounter;\n            CredType = o.CredType;\n            RegDate = o.RegDate;\n            AaGuid = o.AaGuid;\n            Migrated = o.Migrated;\n        }\n\n        public string Name { get; set; }\n        public PublicKeyCredentialDescriptor Descriptor { get; internal set; }\n        public byte[] PublicKey { get; internal set; }\n        public byte[] UserHandle { get; internal set; }\n        public uint SignatureCounter { get; set; }\n        public string CredType { get; internal set; }\n        public DateTime RegDate { get; internal set; }\n        public Guid AaGuid { get; internal set; }\n        public bool Migrated { get; internal set; }\n    }\n\n    public static bool RequiresPremium(TwoFactorProviderType type)\n    {\n        switch (type)\n        {\n            case TwoFactorProviderType.Duo:\n            case TwoFactorProviderType.YubiKey:\n                return true;\n            default:\n                return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Repositories/Cosmos/Base64IdStringConverter.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Auth.Repositories.Cosmos;\n\npublic class Base64IdStringConverter : JsonConverter<string?>\n{\n    public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>\n        ToKey(reader.GetString());\n\n    public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) =>\n        writer.WriteStringValue(ToId(value));\n\n    public static string? ToId(string? key)\n    {\n        if (key == null)\n        {\n            return null;\n        }\n        return CoreHelpers.TransformToBase64Url(key);\n    }\n\n    public static string? ToKey(string? id)\n    {\n        if (id == null)\n        {\n            return null;\n        }\n        return CoreHelpers.TransformFromBase64Url(id);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Repositories/Cosmos/GrantRepository.cs",
    "content": "﻿using System.Net;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Azure.Cosmos;\n\n#nullable enable\n\nnamespace Bit.Core.Auth.Repositories.Cosmos;\n\npublic class GrantRepository : IGrantRepository\n{\n    private readonly CosmosClient _client;\n    private readonly Database _database;\n    private readonly Container _container;\n\n    public GrantRepository(GlobalSettings globalSettings)\n        : this(globalSettings.IdentityServer.CosmosConnectionString)\n    { }\n\n    public GrantRepository(string cosmosConnectionString)\n    {\n        var options = new CosmosClientOptions\n        {\n            Serializer = new SystemTextJsonCosmosSerializer(new JsonSerializerOptions\n            {\n                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,\n                WriteIndented = false\n            })\n        };\n        // TODO: Perhaps we want to evaluate moving this to DI as a keyed service singleton in .NET 8\n        _client = new CosmosClient(cosmosConnectionString, options);\n        _database = _client.GetDatabase(\"identity\");\n        _container = _database.GetContainer(\"grant\");\n    }\n\n    public async Task<IGrant?> GetByKeyAsync(string key)\n    {\n        var id = Base64IdStringConverter.ToId(key);\n        try\n        {\n            var response = await _container.ReadItemAsync<GrantItem>(id, new PartitionKey(id));\n            return response.Resource;\n        }\n        catch (CosmosException e)\n        {\n            if (e.StatusCode == HttpStatusCode.NotFound)\n            {\n                return null;\n            }\n            throw;\n        }\n    }\n\n    public Task<ICollection<IGrant>> GetManyAsync(string subjectId, string sessionId, string clientId, string type)\n        => throw new NotImplementedException();\n\n    public async Task SaveAsync(IGrant obj)\n    {\n        if (obj is not GrantItem item)\n        {\n            item = new GrantItem(obj);\n        }\n        item.SetTtl();\n        var id = Base64IdStringConverter.ToId(item.Key);\n        await _container.UpsertItemAsync(item, new PartitionKey(id), new ItemRequestOptions\n        {\n            // ref: https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/best-practice-dotnet#best-practices-for-write-heavy-workloads\n            EnableContentResponseOnWrite = false\n        });\n    }\n\n    public async Task DeleteByKeyAsync(string key)\n    {\n        var id = Base64IdStringConverter.ToId(key);\n        await _container.DeleteItemAsync<IGrant>(id, new PartitionKey(id));\n    }\n\n    public Task DeleteManyAsync(string subjectId, string sessionId, string clientId, string type)\n        => throw new NotImplementedException();\n}\n"
  },
  {
    "path": "src/Core/Auth/Repositories/IAuthRequestRepository.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IAuthRequestRepository : IRepository<AuthRequest, Guid>\n{\n    Task<int> DeleteExpiredAsync(TimeSpan userRequestExpiration, TimeSpan adminRequestExpiration, TimeSpan afterAdminApprovalExpiration);\n    Task<ICollection<AuthRequest>> GetManyByUserIdAsync(Guid userId);\n    /// <summary>\n    /// Gets all active pending auth requests for a user. Each auth request in the collection will be associated with a different\n    /// device. It will be the most current request for the device.\n    /// </summary>\n    /// <param name=\"userId\">UserId of the owner of the AuthRequests</param>\n    /// <returns>a collection Auth request details or empty</returns>\n    Task<IEnumerable<PendingAuthRequestDetails>> GetManyPendingAuthRequestByUserId(Guid userId);\n    Task<ICollection<OrganizationAdminAuthRequest>> GetManyPendingByOrganizationIdAsync(Guid organizationId);\n    Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable<Guid> ids);\n    Task UpdateManyAsync(IEnumerable<AuthRequest> authRequests);\n}\n"
  },
  {
    "path": "src/Core/Auth/Repositories/IEmergencyAccessRepository.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.KeyManagement.UserKey;\n\nnamespace Bit.Core.Repositories;\n\npublic interface IEmergencyAccessRepository : IRepository<EmergencyAccess, Guid>\n{\n    Task<int> GetCountByGrantorIdEmailAsync(Guid grantorId, string email, bool onlyRegisteredUsers);\n    Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGrantorIdAsync(Guid grantorId);\n    Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGranteeIdAsync(Guid granteeId);\n    /// <summary>\n    /// Gets all emergency access details where the user IDs are either grantors or grantees\n    /// </summary>\n    /// <param name=\"userIds\">Collection of user IDs to query</param>\n    /// <returns>All emergency access details matching the user IDs</returns>\n    Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByUserIdsAsync(ICollection<Guid> userIds);\n    /// <summary>\n    /// Fetches emergency access details by EmergencyAccess id and grantor id\n    /// </summary>\n    /// <param name=\"id\">Emergency Access Id</param>\n    /// <param name=\"grantorId\">Grantor Id</param>\n    /// <returns>EmergencyAccessDetails or null</returns>\n    Task<EmergencyAccessDetails?> GetDetailsByIdGrantorIdAsync(Guid id, Guid grantorId);\n    /// <summary>\n    /// Fetches emergency access details by EmergencyAccess id\n    /// </summary>\n    /// <param name=\"id\">Emergency Access Id</param>\n    /// <returns>EmergencyAccessDetails or null</returns>\n    Task<EmergencyAccessDetails?> GetDetailsByIdAsync(Guid id);\n    /// <summary>\n    /// Database call to fetch emergency accesses that need notification emails sent through a Job\n    /// </summary>\n    /// <returns>collection of EmergencyAccessNotify objects that require notification</returns>\n    Task<ICollection<EmergencyAccessNotify>> GetManyToNotifyAsync();\n    Task<ICollection<EmergencyAccessDetails>> GetExpiredRecoveriesAsync();\n\n    /// <summary>\n    /// Updates encrypted data for emergency access during a key rotation\n    /// </summary>\n    /// <param name=\"grantorId\">The grantor that initiated the key rotation</param>\n    /// <param name=\"emergencyAccessKeys\">A list of emergency access with updated keys</param>\n    UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId,\n        IEnumerable<EmergencyAccess> emergencyAccessKeys);\n\n    /// <summary>\n    /// Deletes multiple emergency access records by their IDs\n    /// </summary>\n    /// <param name=\"emergencyAccessIds\">Ids of records to be deleted</param>\n    /// <returns>void</returns>\n    Task DeleteManyAsync(ICollection<Guid> emergencyAccessIds);\n}\n"
  },
  {
    "path": "src/Core/Auth/Repositories/IGrantRepository.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\n\n#nullable enable\n\nnamespace Bit.Core.Auth.Repositories;\n\npublic interface IGrantRepository\n{\n    Task<IGrant?> GetByKeyAsync(string key);\n    Task<ICollection<IGrant>> GetManyAsync(string subjectId, string sessionId, string clientId, string type);\n    Task SaveAsync(IGrant obj);\n    Task DeleteByKeyAsync(string key);\n    Task DeleteManyAsync(string subjectId, string sessionId, string clientId, string type);\n}\n"
  },
  {
    "path": "src/Core/Auth/Repositories/ISsoConfigRepository.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Repositories;\n\n#nullable enable\n\nnamespace Bit.Core.Auth.Repositories;\n\npublic interface ISsoConfigRepository : IRepository<SsoConfig, long>\n{\n    Task<SsoConfig?> GetByOrganizationIdAsync(Guid organizationId);\n    Task<SsoConfig?> GetByIdentifierAsync(string identifier);\n    Task<ICollection<SsoConfig>> GetManyByRevisionNotBeforeDate(DateTime? notBefore);\n}\n"
  },
  {
    "path": "src/Core/Auth/Repositories/ISsoUserRepository.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Repositories;\n\n#nullable enable\n\nnamespace Bit.Core.Auth.Repositories;\n\npublic interface ISsoUserRepository : IRepository<SsoUser, long>\n{\n    Task DeleteAsync(Guid userId, Guid? organizationId);\n    Task<SsoUser?> GetByUserIdOrganizationIdAsync(Guid organizationId, Guid userId);\n}\n"
  },
  {
    "path": "src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\n\n#nullable enable\n\nnamespace Bit.Core.Auth.Repositories;\n\npublic interface IWebAuthnCredentialRepository : IRepository<WebAuthnCredential, Guid>\n{\n    Task<WebAuthnCredential?> GetByIdAsync(Guid id, Guid userId);\n    Task<ICollection<WebAuthnCredential>> GetManyByUserIdAsync(Guid userId);\n    Task<bool> UpdateAsync(WebAuthnCredential credential);\n    UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<WebAuthnLoginRotateKeyData> credentials);\n}\n"
  },
  {
    "path": "src/Core/Auth/Services/IAuthRequestService.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Exceptions;\nusing Bit.Core.Auth.Models.Api.Request.AuthRequest;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Settings;\n\n#nullable enable\n\nnamespace Bit.Core.Auth.Services;\n\npublic interface IAuthRequestService\n{\n    /// <summary>\n    /// Fetches an authRequest by Id. Returns AuthRequest if AuthRequest.UserId mateches\n    /// userId. Returns null if the user doesn't match or if the AuthRequest is not found.\n    /// </summary>\n    /// <param name=\"authRequestId\">Authrequest Id being fetched</param>\n    /// <param name=\"userId\">user who owns AuthRequest</param>\n    /// <returns>An AuthRequest or null</returns>\n    Task<AuthRequest?> GetAuthRequestAsync(Guid authRequestId, Guid userId);\n    /// <summary>\n    /// Fetches the authrequest from the database with the id provided. Then checks\n    /// the accessCode against the AuthRequest.AccessCode from the database. accessCodes\n    /// must match the found authRequest, and the AuthRequest must not be expired. Expiration\n    /// is configured in <see cref=\"GlobalSettings\"/>\n    /// </summary>\n    /// <param name=\"authRequestId\">AuthRequest being acted on</param>\n    /// <param name=\"accessCode\">Access code of the authrequest, must match saved database value</param>\n    /// <returns>A valid AuthRequest or null</returns>\n    Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid authRequestId, string accessCode);\n    /// <summary>\n    /// Validates and Creates an <see cref=\"AuthRequest\" /> in the database, as well as pushes it through notifications services\n    /// </summary>\n    /// <remarks>\n    /// This method can only be called inside of an HTTP call because of it's reliance on <see cref=\"ICurrentContext\" />\n    /// </remarks>\n    Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model);\n    /// <summary>\n    /// Updates the AuthRequest per the AuthRequestUpdateRequestModel context. This approves\n    /// or rejects the login request.\n    /// </summary>\n    /// <param name=\"authRequestId\">AuthRequest being acted on.</param>\n    /// <param name=\"userId\">User acting on AuthRequest</param>\n    /// <param name=\"model\">Update context for the AuthRequest</param>\n    /// <returns>retuns an AuthRequest or throws an exception</returns>\n    /// <exception cref=\"DuplicateAuthRequestException\">Thows if the AuthRequest has already been Approved/Rejected</exception>\n    /// <exception cref=\"NotFoundException\">Throws if the AuthRequest as expired or the userId doesn't match</exception>\n    /// <exception cref=\"BadRequestException\">Throws if the device isn't associated with the UserId</exception>\n    Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid userId, AuthRequestUpdateRequestModel model);\n}\n"
  },
  {
    "path": "src/Core/Auth/Services/ISsoConfigService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\n\nnamespace Bit.Core.Auth.Services;\n\npublic interface ISsoConfigService\n{\n    Task SaveAsync(SsoConfig config, Organization organization);\n}\n"
  },
  {
    "path": "src/Core/Auth/Services/ITwoFactorEmailService.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.Auth.Services;\n\npublic interface ITwoFactorEmailService\n{\n    Task SendTwoFactorEmailAsync(User user);\n    Task SendTwoFactorSetupEmailAsync(User user);\n    Task SendNewDeviceVerificationEmailAsync(User user);\n    Task<bool> VerifyTwoFactorTokenAsync(User user, string token);\n}\n"
  },
  {
    "path": "src/Core/Auth/Services/Implementations/AuthRequestService.cs",
    "content": "﻿using System.Diagnostics;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Exceptions;\nusing Bit.Core.Auth.Models.Api.Request.AuthRequest;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Logging;\n\n#nullable enable\n\nnamespace Bit.Core.Auth.Services.Implementations;\n\npublic class AuthRequestService : IAuthRequestService\n{\n    private readonly IAuthRequestRepository _authRequestRepository;\n    private readonly IUserRepository _userRepository;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IEventService _eventService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IMailService _mailService;\n    private readonly IFeatureService _featureService;\n    private readonly ILogger<AuthRequestService> _logger;\n\n    public AuthRequestService(\n        IAuthRequestRepository authRequestRepository,\n        IUserRepository userRepository,\n        IGlobalSettings globalSettings,\n        IDeviceRepository deviceRepository,\n        ICurrentContext currentContext,\n        IPushNotificationService pushNotificationService,\n        IEventService eventService,\n        IOrganizationUserRepository organizationRepository,\n        IMailService mailService,\n        IFeatureService featureService,\n        ILogger<AuthRequestService> logger)\n    {\n        _authRequestRepository = authRequestRepository;\n        _userRepository = userRepository;\n        _globalSettings = globalSettings;\n        _deviceRepository = deviceRepository;\n        _currentContext = currentContext;\n        _pushNotificationService = pushNotificationService;\n        _eventService = eventService;\n        _organizationUserRepository = organizationRepository;\n        _mailService = mailService;\n        _featureService = featureService;\n        _logger = logger;\n    }\n\n    public async Task<AuthRequest?> GetAuthRequestAsync(Guid authRequestId, Guid userId)\n    {\n        var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);\n        if (authRequest == null || authRequest.UserId != userId)\n        {\n            return null;\n        }\n\n        return authRequest;\n    }\n\n    public async Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid authRequestId, string accessCode)\n    {\n        var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);\n        if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, accessCode))\n        {\n            return null;\n        }\n\n        if (!IsAuthRequestValid(authRequest))\n        {\n            return null;\n        }\n\n        return authRequest;\n    }\n\n    public async Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model)\n    {\n        if (!_currentContext.DeviceType.HasValue)\n        {\n            throw new BadRequestException(\"Device type not provided.\");\n        }\n\n        var userNotFound = false;\n        var user = await _userRepository.GetByEmailAsync(model.Email);\n        if (user == null)\n        {\n            userNotFound = true;\n        }\n        else if (_globalSettings.PasswordlessAuth.KnownDevicesOnly)\n        {\n            var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);\n            if (devices == null || !devices.Any(d => d.Identifier == model.DeviceIdentifier))\n            {\n                userNotFound = true;\n            }\n        }\n\n        // Anonymous endpoints must not leak that a user exists or not\n        if (userNotFound)\n        {\n            throw new BadRequestException(\"User or known device not found.\");\n        }\n\n        // AdminApproval requests require correlating the user and their organization\n        if (model.Type == AuthRequestType.AdminApproval)\n        {\n            // TODO: When single org policy is turned on we should query for only a single organization from the current user\n            // and create only an AuthRequest for that organization and return only that one\n\n            // This will send out the request to all organizations this user belongs to\n            var organizationUsers = await _organizationUserRepository.GetManyByUserAsync(_currentContext.UserId!.Value);\n\n            if (organizationUsers.Count == 0)\n            {\n                throw new BadRequestException(\"User does not belong to any organizations.\");\n            }\n\n            Debug.Assert(user is not null, \"user should have been validated to be non-null and thrown if it's not.\");\n            // A user event will automatically create logs for each organization/provider this user belongs to.\n            await _eventService.LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);\n\n            AuthRequest? firstAuthRequest = null;\n            foreach (var organizationUser in organizationUsers)\n            {\n                var createdAuthRequest = await CreateAuthRequestAsync(model, user, organizationUser.OrganizationId);\n                firstAuthRequest ??= createdAuthRequest;\n\n                await NotifyAdminsOfDeviceApprovalRequestAsync(organizationUser, user);\n            }\n\n            // I know this won't be null because I have already validated that at least one organization exists\n            return firstAuthRequest!;\n        }\n\n        Debug.Assert(user is not null, \"user should have been validated to be non-null and thrown if it's not.\");\n        var authRequest = await CreateAuthRequestAsync(model, user, organizationId: null);\n        await _pushNotificationService.PushAuthRequestAsync(authRequest);\n        return authRequest;\n    }\n\n    private async Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model, User user, Guid? organizationId)\n    {\n        Debug.Assert(_currentContext.DeviceType.HasValue, \"DeviceType should have already been validated to have a value.\");\n        var authRequest = new AuthRequest\n        {\n            RequestDeviceIdentifier = model.DeviceIdentifier,\n            RequestDeviceType = _currentContext.DeviceType.Value,\n            RequestIpAddress = _currentContext.IpAddress,\n            RequestCountryName = _currentContext.CountryName,\n            AccessCode = model.AccessCode,\n            PublicKey = model.PublicKey,\n            UserId = user.Id,\n            Type = model.Type.GetValueOrDefault(),\n            OrganizationId = organizationId,\n        };\n        authRequest = await _authRequestRepository.CreateAsync(authRequest);\n        return authRequest;\n    }\n\n    public async Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid currentUserId, AuthRequestUpdateRequestModel model)\n    {\n        var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId) ?? throw new NotFoundException();\n\n        // Once Approval/Disapproval has been set, this AuthRequest should not be updated again.\n        if (authRequest.Approved is not null)\n        {\n            throw new DuplicateAuthRequestException();\n        }\n\n        // Do type specific validation\n        switch (authRequest.Type)\n        {\n            case AuthRequestType.AdminApproval:\n                // AdminApproval has a different expiration time, by default is 7 days compared to\n                // non-AdminApproval ones having a default of 15 minutes.\n                if (IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.AdminRequestExpiration))\n                {\n                    throw new NotFoundException();\n                }\n                break;\n            case AuthRequestType.AuthenticateAndUnlock:\n            case AuthRequestType.Unlock:\n                if (IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.UserRequestExpiration))\n                {\n                    throw new NotFoundException();\n                }\n\n                if (authRequest.UserId != currentUserId)\n                {\n                    throw new NotFoundException();\n                }\n\n                // Admin approval responses are not tied to a specific device, but these types are so we need to validate them\n                var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier, currentUserId);\n                if (device == null)\n                {\n                    throw new BadRequestException(\"Invalid device.\");\n                }\n                authRequest.ResponseDeviceId = device.Id;\n                break;\n        }\n\n        authRequest.ResponseDate = DateTime.UtcNow;\n        authRequest.Approved = model.RequestApproved;\n\n        if (model.RequestApproved)\n        {\n            authRequest.Key = model.Key;\n            authRequest.MasterPasswordHash = model.MasterPasswordHash;\n        }\n\n        await _authRequestRepository.ReplaceAsync(authRequest);\n\n        // We only want to send an approval notification if the request is approved (or null),\n        // to not leak that it was denied to the originating client if it was originated by a malicious actor.\n        if (authRequest.Approved ?? true)\n        {\n            if (authRequest.OrganizationId.HasValue)\n            {\n                var organizationUser = await _organizationUserRepository\n                    .GetByOrganizationAsync(authRequest.OrganizationId.Value, authRequest.UserId);\n                await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_ApprovedAuthRequest);\n            }\n\n            // No matter what we want to push out the success notification\n            await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);\n        }\n        // If the request is rejected by an organization admin then we want to log an event of that action\n        else if (authRequest.Approved.HasValue && !authRequest.Approved.Value && authRequest.OrganizationId.HasValue)\n        {\n            var organizationUser = await _organizationUserRepository\n                    .GetByOrganizationAsync(authRequest.OrganizationId.Value, authRequest.UserId);\n            await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_RejectedAuthRequest);\n        }\n\n        return authRequest;\n    }\n\n    private bool IsAuthRequestValid(AuthRequest authRequest)\n    {\n        return authRequest.Type switch\n        {\n            AuthRequestType.AuthenticateAndUnlock or AuthRequestType.Unlock\n                => !IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.UserRequestExpiration),\n            AuthRequestType.AdminApproval => IsAdminApprovalAuthRequestValid(authRequest),\n            _ => false,\n        };\n    }\n\n    private bool IsAdminApprovalAuthRequestValid(AuthRequest authRequest)\n    {\n        Debug.Assert(authRequest.Type == AuthRequestType.AdminApproval, \"This method should only be called on AdminApproval type\");\n        // If an AdminApproval type has been approved it's expiration time is based on how long it's been since approved.\n        if (authRequest.Approved is true)\n        {\n            Debug.Assert(authRequest.ResponseDate.HasValue, \"The response date should have been set when the request was updated.\");\n            return !IsDateExpired(authRequest.ResponseDate.Value, _globalSettings.PasswordlessAuth.AfterAdminApprovalExpiration);\n        }\n        else\n        {\n            return !IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.AdminRequestExpiration);\n        }\n    }\n\n    private static bool IsDateExpired(DateTime savedDate, TimeSpan allowedLifetime)\n    {\n        return DateTime.UtcNow > savedDate.Add(allowedLifetime);\n    }\n\n    private async Task NotifyAdminsOfDeviceApprovalRequestAsync(OrganizationUser organizationUser, User user)\n    {\n        var adminEmails = await GetAdminAndAccountRecoveryEmailsAsync(organizationUser.OrganizationId);\n\n        if (adminEmails.Count == 0)\n        {\n            _logger.LogWarning(\"There are no admin emails to send to.\");\n            return;\n        }\n\n        await _mailService.SendDeviceApprovalRequestedNotificationEmailAsync(\n            adminEmails,\n            organizationUser.OrganizationId,\n            user.Email,\n            user.Name);\n    }\n\n    /// <summary>\n    /// Returns a list of emails for admins and custom users with the ManageResetPassword permission.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization to search within</param>\n    private async Task<List<string>> GetAdminAndAccountRecoveryEmailsAsync(Guid organizationId)\n    {\n        var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(\n            organizationId,\n            OrganizationUserType.Admin);\n\n        var customUsers = await _organizationUserRepository.GetManyDetailsByRoleAsync(\n            organizationId,\n            OrganizationUserType.Custom);\n\n        return admins.Select(a => a.Email)\n            .Concat(customUsers\n                .Where(a => a.GetPermissions().ManageResetPassword)\n                .Select(a => a.Email))\n            .Distinct()\n            .ToList();\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Services/Implementations/SsoConfigService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.Auth.Services;\n\npublic class SsoConfigService : ISsoConfigService\n{\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly IPolicyQuery _policyQuery;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IEventService _eventService;\n    private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;\n\n    public SsoConfigService(\n        ISsoConfigRepository ssoConfigRepository,\n        IPolicyQuery policyQuery,\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IEventService eventService,\n        IVNextSavePolicyCommand vNextSavePolicyCommand)\n    {\n        _ssoConfigRepository = ssoConfigRepository;\n        _policyQuery = policyQuery;\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _eventService = eventService;\n        _vNextSavePolicyCommand = vNextSavePolicyCommand;\n    }\n\n    public async Task SaveAsync(SsoConfig config, Organization organization)\n    {\n        var now = DateTime.UtcNow;\n        config.RevisionDate = now;\n        if (config.Id == default)\n        {\n            config.CreationDate = now;\n        }\n\n        var useKeyConnector = config.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector;\n        if (useKeyConnector)\n        {\n            await VerifyDependenciesAsync(config, organization);\n        }\n\n        var oldConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(config.OrganizationId);\n        var disabledKeyConnector = oldConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector && !useKeyConnector;\n        if (disabledKeyConnector && await AnyOrgUserHasKeyConnectorEnabledAsync(config.OrganizationId))\n        {\n            throw new BadRequestException(\"Key Connector cannot be disabled at this moment.\");\n        }\n\n        // Automatically enable account recovery, SSO required, and single org policies if trusted device encryption is selected\n        if (config.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption)\n        {\n            var singleOrgPolicy = new PolicyUpdate\n            {\n                OrganizationId = config.OrganizationId,\n                Type = PolicyType.SingleOrg,\n                Enabled = true\n            };\n\n            var resetPasswordPolicy = new PolicyUpdate\n            {\n                OrganizationId = config.OrganizationId,\n                Type = PolicyType.ResetPassword,\n                Enabled = true,\n            };\n            resetPasswordPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });\n\n            var requireSsoPolicy = new PolicyUpdate\n            {\n                OrganizationId = config.OrganizationId,\n                Type = PolicyType.RequireSso,\n                Enabled = true\n            };\n\n            var performedBy = new SystemUser(EventSystemUser.Unknown);\n            await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(singleOrgPolicy, performedBy));\n            await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(resetPasswordPolicy, performedBy));\n            await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(requireSsoPolicy, performedBy));\n        }\n\n        await LogEventsAsync(config, oldConfig);\n        await _ssoConfigRepository.UpsertAsync(config);\n    }\n\n    private async Task<bool> AnyOrgUserHasKeyConnectorEnabledAsync(Guid organizationId)\n    {\n        var userDetails =\n            await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);\n        return userDetails.Any(u => u.UsesKeyConnector);\n    }\n\n    private async Task VerifyDependenciesAsync(SsoConfig config, Organization organization)\n    {\n        if (!organization.UseKeyConnector)\n        {\n            throw new BadRequestException(\"Organization cannot use Key Connector.\");\n        }\n\n        var singleOrgPolicy = await _policyQuery.RunAsync(config.OrganizationId, PolicyType.SingleOrg);\n        if (!singleOrgPolicy.Enabled)\n        {\n            throw new BadRequestException(\"Key Connector requires the Single Organization policy to be enabled.\");\n        }\n\n        var ssoPolicy = await _policyQuery.RunAsync(config.OrganizationId, PolicyType.RequireSso);\n        if (!ssoPolicy.Enabled)\n        {\n            throw new BadRequestException(\"Key Connector requires the Single Sign-On Authentication policy to be enabled.\");\n        }\n\n        if (!config.Enabled)\n        {\n            throw new BadRequestException(\"You must enable SSO to use Key Connector.\");\n        }\n    }\n\n    private async Task LogEventsAsync(SsoConfig config, SsoConfig oldConfig)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(config.OrganizationId);\n        if (oldConfig?.Enabled != config.Enabled)\n        {\n            var e = config.Enabled ? EventType.Organization_EnabledSso : EventType.Organization_DisabledSso;\n            await _eventService.LogOrganizationEventAsync(organization, e);\n        }\n\n        var keyConnectorEnabled = config.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector;\n        var oldKeyConnectorEnabled = oldConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector;\n        if (oldKeyConnectorEnabled != keyConnectorEnabled)\n        {\n            var e = keyConnectorEnabled\n                ? EventType.Organization_EnabledKeyConnector\n                : EventType.Organization_DisabledKeyConnector;\n            await _eventService.LogOrganizationEventAsync(organization, e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Reflection;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Core.Auth.Enums;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Auth.Services;\n\npublic class TwoFactorEmailService : ITwoFactorEmailService\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly UserManager<User> _userManager;\n    private readonly IMailService _mailService;\n\n    public TwoFactorEmailService(\n        ICurrentContext currentContext,\n        IMailService mailService,\n        UserManager<User> userManager\n    )\n    {\n        _currentContext = currentContext;\n        _userManager = userManager;\n        _mailService = mailService;\n    }\n\n    /// <summary>\n    /// Sends a two-factor email to the user with an OTP token for login\n    /// </summary>\n    /// <param name=\"user\">The user to whom the email should be sent</param>\n    /// <exception cref=\"ArgumentNullException\">Thrown if the user does not have an email for email 2FA</exception>\n    public async Task SendTwoFactorEmailAsync(User user)\n    {\n        await VerifyAndSendTwoFactorEmailAsync(user, TwoFactorEmailPurpose.Login);\n    }\n\n    /// <summary>\n    /// Sends a two-factor email to the user with an OTP for setting up 2FA\n    /// </summary>\n    /// <param name=\"user\">The user to whom the email should be sent</param>\n    /// <exception cref=\"ArgumentNullException\">Thrown if the user does not have an email for email 2FA</exception>\n    public async Task SendTwoFactorSetupEmailAsync(User user)\n    {\n        await VerifyAndSendTwoFactorEmailAsync(user, TwoFactorEmailPurpose.Setup);\n    }\n\n    /// <summary>\n    /// Sends a new device verification email to the user with an OTP token\n    /// </summary>\n    /// <param name=\"user\">The user to whom the email should be sent</param>\n    /// <exception cref=\"ArgumentNullException\">Thrown if the user is not provided</exception>\n    public async Task SendNewDeviceVerificationEmailAsync(User user)\n    {\n        ArgumentNullException.ThrowIfNull(user);\n\n        var token = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,\n            \"otp:\" + user.Email);\n\n        var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())\n            .FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? \"Unknown Browser\";\n\n        await _mailService.SendTwoFactorEmailAsync(\n            user.Email, user.Email, token, _currentContext.IpAddress, deviceType, TwoFactorEmailPurpose.NewDeviceVerification);\n    }\n\n    /// <summary>\n    /// Verifies the two-factor token for the specified user\n    /// </summary>\n    /// <param name=\"user\">The user for whom the token should be verified</param>\n    /// <param name=\"token\">The token to verify</param>\n    /// <exception cref=\"ArgumentNullException\">Thrown if the user does not have an email for email 2FA</exception>\n    public async Task<bool> VerifyTwoFactorTokenAsync(User user, string token)\n    {\n        var email = GetUserTwoFactorEmail(user);\n        return await _userManager.VerifyTwoFactorTokenAsync(user,\n            CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token);\n    }\n\n    /// <summary>\n    /// Sends a two-factor email with the specified purpose to the user only if they have 2FA email set up\n    /// </summary>\n    /// <param name=\"user\">The user to whom the email should be sent</param>\n    /// <param name=\"purpose\">The purpose of the email</param>\n    /// <exception cref=\"ArgumentNullException\">Thrown if the user does not have an email set up for 2FA</exception>\n    private async Task VerifyAndSendTwoFactorEmailAsync(User user, TwoFactorEmailPurpose purpose)\n    {\n        var email = GetUserTwoFactorEmail(user);\n        var token = await _userManager.GenerateTwoFactorTokenAsync(user,\n            CoreHelpers.CustomProviderName(TwoFactorProviderType.Email));\n\n        var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())\n            .FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? \"Unknown Browser\";\n\n        await _mailService.SendTwoFactorEmailAsync(\n            email, user.Email, token, _currentContext.IpAddress, deviceType, purpose);\n    }\n\n    /// <summary>\n    ///  Verifies the user has email 2FA and will return the email if present and throw otherwise.\n    /// </summary>\n    /// <param name=\"user\">The user to check</param>\n    /// <returns>The user's 2FA email address</returns>\n    /// <exception cref=\"ArgumentNullException\"></exception>\n    private string GetUserTwoFactorEmail(User user)\n    {\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);\n        if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue(\"Email\", out var emailValue))\n        {\n            throw new ArgumentNullException(\"No email.\");\n        }\n        return ((string)emailValue).ToLowerInvariant();\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Settings/IPasswordlessAuthSettings.cs",
    "content": "﻿namespace Bit.Core.Auth.Settings;\n\npublic interface IPasswordlessAuthSettings\n{\n    bool KnownDevicesOnly { get; set; }\n    TimeSpan UserRequestExpiration { get; set; }\n    TimeSpan AdminRequestExpiration { get; set; }\n    TimeSpan AfterAdminApprovalExpiration { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Settings/ISsoSettings.cs",
    "content": "﻿namespace Bit.Core.Settings;\n\npublic interface ISsoSettings\n{\n    int CacheLifetimeInSeconds { get; set; }\n    double SsoTokenLifetimeInSeconds { get; set; }\n    bool EnforceSsoPolicyForAllUsers { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Auth/Sso/IUserSsoOrganizationIdentifierQuery.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.Auth.Sso;\n\n/// <summary>\n/// Query to retrieve the SSO organization identifier that a user is a confirmed member of.\n/// </summary>\npublic interface IUserSsoOrganizationIdentifierQuery\n{\n    /// <summary>\n    /// Retrieves the SSO organization identifier for a confirmed organization user.\n    /// If there is more than one organization a User is associated with, we return null. If there are more than one\n    /// organization there is no way to know which organization the user wishes to authenticate with.\n    /// Owners and Admins who are not subject to the SSO required policy cannot utilize this flow, since they may have\n    /// multiple organizations with different SSO configurations.\n    /// </summary>\n    /// <param name=\"userId\">The ID of the <see cref=\"User\"/> to retrieve the SSO organization for. _Not_ an <see cref=\"OrganizationUser\"/>.</param>\n    /// <returns>\n    /// The organization identifier if the user is a confirmed member of an organization with SSO configured,\n    /// otherwise null\n    /// </returns>\n    Task<string?> GetSsoOrganizationIdentifierAsync(Guid userId);\n}\n"
  },
  {
    "path": "src/Core/Auth/Sso/SamlSigningAlgorithms.cs",
    "content": "﻿namespace Bit.Core.Sso;\n\npublic static class SamlSigningAlgorithms\n{\n    public const string Default = Sha256;\n    public const string Sha256 = \"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\";\n    public const string Sha384 = \"http://www.w3.org/2000/09/xmldsig#rsa-sha384\";\n    public const string Sha512 = \"http://www.w3.org/2000/09/xmldsig#rsa-sha512\";\n\n    public static IEnumerable<string> GetEnumerable()\n    {\n        yield return Sha256;\n        yield return Sha384;\n        yield return Sha512;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Sso/UserSsoOrganizationIdentifierQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Auth.Sso;\n\n/// <summary>\n/// TODO : PM-28846 review data structures as they relate to this query\n/// Query to retrieve the SSO organization identifier that a user is a confirmed member of.\n/// </summary>\npublic class UserSsoOrganizationIdentifierQuery(\n    IOrganizationUserRepository _organizationUserRepository,\n    IOrganizationRepository _organizationRepository) : IUserSsoOrganizationIdentifierQuery\n{\n    /// <inheritdoc />\n    public async Task<string?> GetSsoOrganizationIdentifierAsync(Guid userId)\n    {\n        // Get all confirmed organization memberships for the user\n        var organizationUsers = await _organizationUserRepository.GetManyByUserAsync(userId);\n\n        // we can only confidently return the correct SsoOrganizationIdentifier if there is exactly one Organization.\n        // The user must also be in the Confirmed status.\n        var confirmedOrgUsers = organizationUsers.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed);\n        if (confirmedOrgUsers.Count() != 1)\n        {\n            return null;\n        }\n\n        var confirmedOrgUser = confirmedOrgUsers.Single();\n        var organization = await _organizationRepository.GetByIdAsync(confirmedOrgUser.OrganizationId);\n\n        if (organization == null)\n        {\n            return null;\n        }\n\n        return organization.Identifier;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/DeviceTrust/Interfaces/IUntrustDevicesCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.Auth.UserFeatures.DeviceTrust;\n\npublic interface IUntrustDevicesCommand\n{\n    public Task UntrustDevices(User user, IEnumerable<Guid> devicesToUntrust);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/DeviceTrust/UntrustDevicesCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Auth.UserFeatures.DeviceTrust;\n\npublic class UntrustDevicesCommand : IUntrustDevicesCommand\n{\n    private readonly IDeviceRepository _deviceRepository;\n\n    public UntrustDevicesCommand(\n        IDeviceRepository deviceRepository)\n    {\n        _deviceRepository = deviceRepository;\n    }\n\n    public async Task UntrustDevices(User user, IEnumerable<Guid> devicesToUntrust)\n    {\n        var userDevices = await _deviceRepository.GetManyByUserIdAsync(user.Id);\n        var deviceIdDict = userDevices.ToDictionary(device => device.Id);\n\n        // Validate that the user owns all devices that they passed in\n        foreach (var deviceId in devicesToUntrust)\n        {\n            if (!deviceIdDict.ContainsKey(deviceId))\n            {\n                throw new UnauthorizedAccessException($\"User {user.Id} does not have access to device {deviceId}\");\n            }\n        }\n\n        foreach (var deviceId in devicesToUntrust)\n        {\n            var device = deviceIdDict[deviceId];\n            device.EncryptedPrivateKey = null;\n            device.EncryptedPublicKey = null;\n            device.EncryptedUserKey = null;\n            await _deviceRepository.UpsertAsync(device);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/EmergencyAccess/Commands/DeleteEmergencyAccessCommand.cs",
    "content": "﻿using Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Mail.Mailer;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Auth.UserFeatures.EmergencyAccess.Commands;\n\npublic class DeleteEmergencyAccessCommand(\n    IEmergencyAccessRepository _emergencyAccessRepository,\n    IMailer mailer,\n    ILogger<DeleteEmergencyAccessCommand> _logger) : IDeleteEmergencyAccessCommand\n{\n    /// <inheritdoc />\n    public async Task DeleteByIdAndUserIdAsync(Guid emergencyAccessId, Guid userId)\n    {\n        var emergencyAccessDetails = await _emergencyAccessRepository.GetDetailsByIdAsync(emergencyAccessId);\n\n        // Error if the emergency access doesn't exist or the user trying to delete is neither the grantor nor the grantee\n        if (emergencyAccessDetails == null || (emergencyAccessDetails.GrantorId != userId && emergencyAccessDetails.GranteeId != userId))\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        await _emergencyAccessRepository.DeleteAsync(emergencyAccessDetails);\n\n        // Emails may be null if the grantor or grantee user account has since been deleted\n        // so ensure we have both emails we need.\n        if (!string.IsNullOrEmpty(emergencyAccessDetails.GrantorEmail) &&\n            !string.IsNullOrEmpty(emergencyAccessDetails.GranteeEmail))\n        {\n            await SendGranteesRemovalNotificationToGrantorAsync(\n                emergencyAccessDetails.GrantorEmail,\n                [emergencyAccessDetails.GranteeEmail]);\n        }\n        else\n        {\n            // If we are missing the emails needed to send a notification, log this occurrence.\n            _logger.LogWarning(\n                \"Emergency access deletion notification skipped for grantor {GrantorId} and grantee {GranteeId}: GrantorEmail missing: {GrantorEmailMissing}, GranteeEmail missing: {GranteeEmailMissing}.\",\n                emergencyAccessDetails.GrantorId,\n                emergencyAccessDetails.GranteeId,\n                string.IsNullOrEmpty(emergencyAccessDetails.GrantorEmail),\n                string.IsNullOrEmpty(emergencyAccessDetails.GranteeEmail));\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task DeleteAllByUserIdAsync(Guid userId)\n    {\n        await DeleteAllByUserIdsAsync([userId]);\n    }\n\n\n    /// <inheritdoc />\n    public async Task DeleteAllByUserIdsAsync(ICollection<Guid> userIds)\n    {\n        var emergencyAccessDetails = await _emergencyAccessRepository.GetManyDetailsByUserIdsAsync(userIds);\n\n        if (emergencyAccessDetails.Count == 0)\n        {\n            // No records found, so nothing to delete or notify\n            // However, don't throw an error since the end state of \"no records for these user IDs\" \n            // is already achieved\n            return;\n        }\n\n        // Delete all records using existing DeleteManyAsync (batching already implemented)\n        var emergencyAccessIds = emergencyAccessDetails.Select(ea => ea.Id).ToList();\n        await _emergencyAccessRepository.DeleteManyAsync(emergencyAccessIds);\n\n        // After deletion, send notifications to grantors about their removed grantees.\n        // GrantorEmail may be null when a grantor's account has been deleted, since it is sourced\n        // entirely from a LEFT JOIN on the User table with no fallback column. Log any affected\n        // GrantorIds up front for traceability — the grantor's account is already gone so the ID\n        // cannot be used to look up the user, but it can be correlated with audit logs generated\n        // at the time of that account's deletion to understand why the notification was skipped.\n        var grantorIdsWithNullEmail = emergencyAccessDetails\n            .Where(ea => string.IsNullOrEmpty(ea.GrantorEmail))\n            .Select(ea => ea.GrantorId)\n            .Distinct()\n            .ToList();\n\n        if (grantorIdsWithNullEmail.Count > 0)\n        {\n            _logger.LogWarning(\n                \"Emergency access deletion notification skipped for {Count} grantor(s) with missing GrantorEmail. GrantorIds: {GrantorIds}.\",\n                grantorIdsWithNullEmail.Count,\n                grantorIdsWithNullEmail);\n        }\n\n        // Group by grantor email to send each grantor a single email listing all their removed grantees.\n        // Records with null GrantorEmail are excluded above and will not receive a notification.\n        var grantorEmergencyAccessDetailGroups = emergencyAccessDetails\n            .Where(ea => !string.IsNullOrEmpty(ea.GrantorEmail))\n            .GroupBy(ea => ea.GrantorEmail!); // .GrantorEmail is safe here due to the Where above\n\n        foreach (var grantorGroup in grantorEmergencyAccessDetailGroups)\n        {\n            var grantorEmail = grantorGroup.Key;\n            var granteeEmails = grantorGroup\n                .Select(ea => ea.GranteeEmail)\n                // Filter out null grantee emails, which may occur if a grantee's account has been deleted\n                .Where(e => !string.IsNullOrEmpty(e))\n                .Cast<string>() // Cast is safe here due to the Where above\n                .Distinct();\n\n            var granteeIdsWithNullEmail = grantorGroup\n                .Where(ea => string.IsNullOrEmpty(ea.GranteeEmail))\n                .Select(ea => ea.GranteeId)\n                .Distinct()\n                .ToList();\n\n            if (granteeIdsWithNullEmail.Count > 0)\n            {\n                _logger.LogWarning(\n                    \"Emergency access deletion notification skipped for {Count} grantee(s) with missing GranteeEmail. GranteeIds: {GranteeIds}.\",\n                    granteeIdsWithNullEmail.Count,\n                    granteeIdsWithNullEmail);\n            }\n\n            if (granteeEmails.Any())\n            {\n                await SendGranteesRemovalNotificationToGrantorAsync(grantorEmail, granteeEmails);\n            }\n        }\n    }\n\n    /// <summary>\n    /// Sends an email notification to a grantor about their removed grantees.\n    /// </summary>\n    /// <param name=\"grantorEmail\">The email address of the grantor to notify</param>\n    /// <param name=\"granteeEmails\">The email addresses of the removed grantees</param>\n    private async Task SendGranteesRemovalNotificationToGrantorAsync(string grantorEmail, IEnumerable<string> granteeEmails)\n    {\n        var emailViewModel = new EmergencyAccessRemoveGranteesMail\n        {\n            ToEmails = [grantorEmail],\n            View = new EmergencyAccessRemoveGranteesMailView\n            {\n                RemovedGranteeEmails = granteeEmails\n            }\n        };\n\n        await mailer.SendEmail(emailViewModel);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/EmergencyAccess/EmergencyAccessService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Core.Vault.Services;\n\nnamespace Bit.Core.Auth.UserFeatures.EmergencyAccess;\n\npublic class EmergencyAccessService : IEmergencyAccessService\n{\n    private readonly IEmergencyAccessRepository _emergencyAccessRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IUserRepository _userRepository;\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IPolicyRepository _policyRepository;\n    private readonly ICipherService _cipherService;\n    private readonly IMailService _mailService;\n    private readonly IUserService _userService;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _dataProtectorTokenizer;\n    private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;\n    private readonly IFeatureService _featureService;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n\n    public EmergencyAccessService(\n        IEmergencyAccessRepository emergencyAccessRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IUserRepository userRepository,\n        ICipherRepository cipherRepository,\n        IPolicyRepository policyRepository,\n        ICipherService cipherService,\n        IMailService mailService,\n        IUserService userService,\n        GlobalSettings globalSettings,\n        IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer,\n        IRemoveOrganizationUserCommand removeOrganizationUserCommand,\n        IFeatureService featureService,\n        IPolicyRequirementQuery policyRequirementQuery)\n    {\n        _emergencyAccessRepository = emergencyAccessRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _userRepository = userRepository;\n        _cipherRepository = cipherRepository;\n        _policyRepository = policyRepository;\n        _cipherService = cipherService;\n        _mailService = mailService;\n        _userService = userService;\n        _globalSettings = globalSettings;\n        _dataProtectorTokenizer = dataProtectorTokenizer;\n        _removeOrganizationUserCommand = removeOrganizationUserCommand;\n        _featureService = featureService;\n        _policyRequirementQuery = policyRequirementQuery;\n    }\n\n    public async Task<Entities.EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime)\n    {\n        if (!await _userService.CanAccessPremium(grantorUser))\n        {\n            throw new BadRequestException(\"Not a premium user.\");\n        }\n\n        if (grantorUser.Email.Equals(emergencyContactEmail, StringComparison.OrdinalIgnoreCase))\n        {\n            throw new BadRequestException(\"You cannot add yourself as an emergency access contact.\");\n        }\n\n        if (accessType == EmergencyAccessType.Takeover && grantorUser.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"You cannot use Emergency Access Takeover because you are using Key Connector.\");\n        }\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var requirement = await _policyRequirementQuery\n                .GetAsync<AutomaticUserConfirmationPolicyRequirement>(grantorUser.Id);\n\n            if (requirement.GrantorCannotInviteToEmergencyAccess())\n            {\n                throw new BadRequestException(\"You cannot invite emergency contacts because you are a member of an organization that uses Automatic User Confirmation.\");\n            }\n        }\n\n        var emergencyAccess = new Entities.EmergencyAccess\n        {\n            GrantorId = grantorUser.Id,\n            Email = emergencyContactEmail.ToLowerInvariant(),\n            Status = EmergencyAccessStatusType.Invited,\n            Type = accessType,\n            WaitTimeDays = (short)waitTime,\n            CreationDate = DateTime.UtcNow,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await _emergencyAccessRepository.CreateAsync(emergencyAccess);\n        await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser));\n\n        return emergencyAccess;\n    }\n\n    public async Task<EmergencyAccessDetails> GetAsync(Guid emergencyAccessId, Guid grantorId)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, grantorId);\n        if (emergencyAccess == null)\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        return emergencyAccess;\n    }\n\n    public async Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n        if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id ||\n            emergencyAccess.Status != EmergencyAccessStatusType.Invited)\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser));\n    }\n\n    public async Task<Entities.EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n        if (emergencyAccess == null)\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        if (!_dataProtectorTokenizer.TryUnprotect(token, out var data))\n        {\n            throw new BadRequestException(\"Invalid token.\");\n        }\n\n        if (!data.IsValid(emergencyAccessId, granteeUser.Email))\n        {\n            throw new BadRequestException(\"Invalid token.\");\n        }\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))\n        {\n            var requirement = await _policyRequirementQuery\n                .GetAsync<AutomaticUserConfirmationPolicyRequirement>(granteeUser.Id);\n\n            if (requirement.GranteeCannotAcceptEmergencyAccess())\n            {\n                throw new BadRequestException(\"You cannot accept emergency access invitations because you are a member of an organization that uses Automatic User Confirmation.\");\n            }\n        }\n\n        if (emergencyAccess.Status == EmergencyAccessStatusType.Accepted)\n        {\n            throw new BadRequestException(\"Invitation already accepted. You will receive an email when the grantor confirms you as an emergency access contact.\");\n        }\n        else if (emergencyAccess.Status != EmergencyAccessStatusType.Invited)\n        {\n            throw new BadRequestException(\"Invitation already accepted.\");\n        }\n\n        // TODO PM-21687\n        // Might not be reachable since the Tokenable.IsValid() does an email comparison\n        if (string.IsNullOrWhiteSpace(emergencyAccess.Email) ||\n            !emergencyAccess.Email.Equals(granteeUser.Email, StringComparison.InvariantCultureIgnoreCase))\n        {\n            throw new BadRequestException(\"User email does not match invite.\");\n        }\n\n        var granteeEmail = emergencyAccess.Email;\n\n        emergencyAccess.Status = EmergencyAccessStatusType.Accepted;\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Email = null;\n\n        var grantor = await userService.GetUserByIdAsync(emergencyAccess.GrantorId);\n\n        await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);\n        await _mailService.SendEmergencyAccessAcceptedEmailAsync(granteeEmail, grantor.Email);\n\n        return emergencyAccess;\n    }\n\n    // TODO: remove with PM-31327 when we migrate to the command.\n    public async Task DeleteAsync(Guid emergencyAccessId, Guid userId)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n\n        // Error if the emergency access doesn't exist or the user trying to delete is neither the grantor nor the grantee\n        if (emergencyAccess == null || (emergencyAccess.GrantorId != userId && emergencyAccess.GranteeId != userId))\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        await _emergencyAccessRepository.DeleteAsync(emergencyAccess);\n    }\n\n    public async Task<Entities.EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n        if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted ||\n            emergencyAccess.GrantorId != grantorId)\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        var grantor = await _userRepository.GetByIdAsync(grantorId);\n        if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"You cannot use Emergency Access Takeover because you are using Key Connector.\");\n        }\n\n        var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);\n\n        emergencyAccess.Status = EmergencyAccessStatusType.Confirmed;\n        emergencyAccess.KeyEncrypted = key;\n        emergencyAccess.Email = null;\n        await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);\n        await _mailService.SendEmergencyAccessConfirmedEmailAsync(NameOrEmail(grantor), grantee.Email);\n\n        return emergencyAccess;\n    }\n\n    public async Task SaveAsync(Entities.EmergencyAccess emergencyAccess, User grantorUser)\n    {\n        if (!await _userService.CanAccessPremium(grantorUser))\n        {\n            throw new BadRequestException(\"Not a premium user.\");\n        }\n\n        if (emergencyAccess.GrantorId != grantorUser.Id)\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        if (emergencyAccess.Type == EmergencyAccessType.Takeover)\n        {\n            var grantor = await _userService.GetUserByIdAsync(emergencyAccess.GrantorId);\n            if (grantor.UsesKeyConnector)\n            {\n                throw new BadRequestException(\"You cannot use Emergency Access Takeover because you are using Key Connector.\");\n            }\n        }\n\n        await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);\n    }\n\n    // TODO PM-21687: rename this to something like InitiateRecoveryAsync, and something similar for Approve and Reject\n    public async Task InitiateAsync(Guid emergencyAccessId, User granteeUser)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n        if (emergencyAccess == null || emergencyAccess.GranteeId != granteeUser.Id ||\n            emergencyAccess.Status != EmergencyAccessStatusType.Confirmed)\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);\n\n        if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"You cannot takeover an account that is using Key Connector.\");\n        }\n\n        var now = DateTime.UtcNow;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;\n        emergencyAccess.RevisionDate = now;\n        emergencyAccess.RecoveryInitiatedDate = now;\n        emergencyAccess.LastNotificationDate = now;\n        await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);\n\n        await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(granteeUser), grantor.Email);\n    }\n\n    public async Task ApproveAsync(Guid emergencyAccessId, User grantorUser)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n\n        if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id ||\n            emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated)\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);\n\n        var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);\n        await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, NameOrEmail(grantorUser), grantee.Email);\n    }\n\n    public async Task RejectAsync(Guid emergencyAccessId, User grantorUser)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n\n        if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id ||\n            (emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated &&\n             emergencyAccess.Status != EmergencyAccessStatusType.RecoveryApproved))\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        emergencyAccess.Status = EmergencyAccessStatusType.Confirmed;\n        await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);\n\n        var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);\n        await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, NameOrEmail(grantorUser), grantee.Email);\n    }\n\n    public async Task<ICollection<Policy>> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser)\n    {\n        // TODO PM-21687\n        // Should we look up policies here or just verify the EmergencyAccess is correct\n        // and handle policy logic else where? Should this be a query/Command?\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n\n        if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover))\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);\n\n        var grantorOrganizations = await _organizationUserRepository.GetManyByUserAsync(grantor.Id);\n        var isOrganizationOwner = grantorOrganizations\n            .Any(organization => organization.Type == OrganizationUserType.Owner);\n\n        var policies = isOrganizationOwner ? await _policyRepository.GetManyByUserIdAsync(grantor.Id) : null;\n\n        return policies;\n    }\n\n    // TODO PM-21687: rename this to something like InitiateRecoveryTakeoverAsync\n    public async Task<(Entities.EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n\n        if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover))\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);\n        // TODO PM-21687\n        // Redundant check of the EmergencyAccessType -> checked in IsValidRequest() ln 308\n        if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"You cannot takeover an account that is using Key Connector.\");\n        }\n\n        return (emergencyAccess, grantor);\n    }\n\n    // TODO PM-21687: rename this to something like FinishRecoveryTakeoverAsync\n    public async Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n\n        if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover))\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);\n\n        await _userService.UpdatePasswordHash(grantor, newMasterPasswordHash);\n        grantor.RevisionDate = DateTime.UtcNow;\n        grantor.LastPasswordChangeDate = grantor.RevisionDate;\n        grantor.Key = key;\n        // Disable TwoFactor providers since they will otherwise block logins\n        grantor.SetTwoFactorProviders([]);\n        // Disable New Device Verification since it will otherwise block logins\n        grantor.VerifyDevices = false;\n        await _userRepository.ReplaceAsync(grantor);\n\n        // Remove grantor from all organizations unless Owner\n        var orgUser = await _organizationUserRepository.GetManyByUserAsync(grantor.Id);\n        foreach (var o in orgUser)\n        {\n            if (o.Type != OrganizationUserType.Owner)\n            {\n                await _removeOrganizationUserCommand.RemoveUserAsync(o.OrganizationId, grantor.Id);\n            }\n        }\n    }\n\n    public async Task SendNotificationsAsync()\n    {\n        var toNotify = await _emergencyAccessRepository.GetManyToNotifyAsync();\n\n        foreach (var notify in toNotify)\n        {\n            var ea = notify.ToEmergencyAccess();\n            ea.LastNotificationDate = DateTime.UtcNow;\n            await _emergencyAccessRepository.ReplaceAsync(ea);\n\n            var granteeNameOrEmail = string.IsNullOrWhiteSpace(notify.GranteeName) ? notify.GranteeEmail : notify.GranteeName;\n\n            await _mailService.SendEmergencyAccessRecoveryReminder(ea, granteeNameOrEmail, notify.GrantorEmail);\n        }\n    }\n\n    public async Task HandleTimedOutRequestsAsync()\n    {\n        var expired = await _emergencyAccessRepository.GetExpiredRecoveriesAsync();\n\n        foreach (var details in expired)\n        {\n            var ea = details.ToEmergencyAccess();\n            ea.Status = EmergencyAccessStatusType.RecoveryApproved;\n            await _emergencyAccessRepository.ReplaceAsync(ea);\n\n            var grantorNameOrEmail = string.IsNullOrWhiteSpace(details.GrantorName) ? details.GrantorEmail : details.GrantorName;\n            var granteeNameOrEmail = string.IsNullOrWhiteSpace(details.GranteeName) ? details.GranteeEmail : details.GranteeName;\n\n            await _mailService.SendEmergencyAccessRecoveryApproved(ea, grantorNameOrEmail, details.GranteeEmail);\n            await _mailService.SendEmergencyAccessRecoveryTimedOut(ea, granteeNameOrEmail, details.GrantorEmail);\n        }\n    }\n\n    public async Task<EmergencyAccessViewData> ViewAsync(Guid emergencyAccessId, User granteeUser)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n\n        if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View))\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        var ciphers = await _cipherRepository.GetManyByUserIdAsync(emergencyAccess.GrantorId, withOrganizations: false);\n\n        return new EmergencyAccessViewData\n        {\n            EmergencyAccess = emergencyAccess,\n            Ciphers = ciphers,\n        };\n    }\n\n    public async Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser)\n    {\n        var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);\n\n        if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View))\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        var cipher = await _cipherRepository.GetByIdAsync(cipherId, emergencyAccess.GrantorId);\n        return await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId);\n    }\n\n    private async Task SendInviteAsync(Entities.EmergencyAccess emergencyAccess, string invitingUsersName)\n    {\n        var token = _dataProtectorTokenizer.Protect(new EmergencyAccessInviteTokenable(emergencyAccess, _globalSettings.OrganizationInviteExpirationHours));\n        await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token);\n    }\n\n    // TODO PM-21687: move this to the user entity -> User.GetNameOrEmail()?\n    private static string NameOrEmail(User user)\n    {\n        return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name;\n    }\n\n    /*\n     * Checks if EmergencyAccess Object is null\n     * Checks the requesting user is the same as the granteeUser (So we are checking for proper grantee action)\n     * Status _must_ equal RecoveryApproved (This means the grantor has invited, the grantee has accepted, and the grantor has approved so the shared key exists but hasn't been exercised yet)\n     * request type must equal the type of access requested (View or Takeover)\n     */\n    //TODO PM-21687: this IsValidRequest() checks the validity based on the granteeUser. There should be a complementary method for the grantorUser\n    private static bool IsValidRequest(\n        Entities.EmergencyAccess availableAccess,\n        User requestingUser,\n        EmergencyAccessType requestedAccessType)\n    {\n        return availableAccess != null &&\n           availableAccess.GranteeId == requestingUser.Id &&\n           availableAccess.Status == EmergencyAccessStatusType.RecoveryApproved &&\n           availableAccess.Type == requestedAccessType;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/EmergencyAccess/IEmergencyAccessService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Auth.UserFeatures.EmergencyAccess;\n\npublic interface IEmergencyAccessService\n{\n    /// <summary>\n    /// Invites a user via email to become an emergency contact for the Grantor user. The Grantor must have a premium subscription.\n    /// the grantor user must not be a member of the organization that uses KeyConnector.\n    /// </summary>\n    /// <param name=\"grantorUser\">The user initiating the emergency contact request</param>\n    /// <param name=\"emergencyContactEmail\">Emergency contact</param>\n    /// <param name=\"accessType\">Type of emergency access allowed to the emergency contact</param>\n    /// <param name=\"waitTime\">The amount of time to pass before the invite is auto confirmed</param>\n    /// <returns>a new Emergency Access object</returns>\n    Task<Entities.EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);\n    /// <summary>\n    /// Sends an invite to the emergency contact associated with the emergency access id.\n    /// </summary>\n    /// <param name=\"grantorUser\">The grantor. This must be the owner of the Emergency Access object</param>\n    /// <param name=\"emergencyAccessId\">The Id of the emergency access being requested.</param>\n    /// <returns>void</returns>\n    Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId);\n    /// <summary>\n    /// A grantee user accepts the emergency contact request. This updates the emergency access status to be\n    /// \"Accepted\", this is the middle step before the grantor user confirms the request.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">Id of the emergency access object being acted on.</param>\n    /// <param name=\"granteeUser\">User being invited to be an emergency contact</param>\n    /// <param name=\"token\">the tokenable that was sent via email</param>\n    /// <param name=\"userService\">service dependency</param>\n    /// <returns>void</returns>\n    Task<Entities.EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);\n    /// <summary>\n    /// Either the grantor OR the grantee can delete the emergency access.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">Id of the emergency access being acted on</param>\n    /// <param name=\"userId\">Id of the user (needs to be the grantor or grantee) trying to delete the emergency access</param>\n    /// <returns>void</returns>\n    /// TODO: Remove this deprecated method with PM-31327\n    [Obsolete(\"Use IDeleteEmergencyAccessCommand.DeleteByIdAndUserIdAsync instead.\")]\n    Task DeleteAsync(Guid emergencyAccessId, Guid userId);\n    /// <summary>\n    /// The grantor user confirms the acceptance of the emergency contact request. This stores the encrypted key allowing the grantee\n    /// access based on the emergency access type.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">Id of the emergency access being acted on.</param>\n    /// <param name=\"key\">The grantor user key encrypted by the grantee public key; grantee.PubicKey(grantor.User.Key)</param>\n    /// <param name=\"grantorId\">Id of grantor user</param>\n    /// <returns>emergency access object associated with the Id passed in</returns>\n    Task<Entities.EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);\n    /// <summary>\n    /// Fetches an emergency access object. The grantor user must own the object being fetched.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">Id of emergency access object</param>\n    /// <param name=\"grantorId\">Id of the owner of the emergency access object.</param>\n    /// <returns>Details of the emergency access object</returns>\n    Task<EmergencyAccessDetails> GetAsync(Guid emergencyAccessId, Guid grantorId);\n    /// <summary>\n    /// Updates the emergency access object.\n    /// </summary>\n    /// <param name=\"emergencyAccess\">emergency access entity being updated</param>\n    /// <param name=\"grantorUser\">grantor user</param>\n    /// <returns>void</returns>\n    Task SaveAsync(Entities.EmergencyAccess emergencyAccess, User grantorUser);\n    /// <summary>\n    /// Initiates the recovery process. For either Takeover or view. Will send an email to the Grantor User notifying of the initiation.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">EmergencyAccess Id</param>\n    /// <param name=\"granteeUser\">grantee user</param>\n    /// <returns>void</returns>\n    Task InitiateAsync(Guid emergencyAccessId, User granteeUser);\n    /// <summary>\n    /// Approves a recovery request. Sets the EmergencyAccess.Status to RecoveryApproved.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">emergency access id</param>\n    /// <param name=\"grantorUser\">grantor user</param>\n    /// <returns>void</returns>\n    Task ApproveAsync(Guid emergencyAccessId, User grantorUser);\n    /// <summary>\n    /// Rejects a recovery request. Sets the EmergencyAccess.Status to Confirmed. This does not remove the emergency access entity. The\n    /// Grantee user can still initiate another recovery request.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">emergency access id</param>\n    /// <param name=\"grantorUser\">grantor user</param>\n    /// <returns>void</returns>\n    Task RejectAsync(Guid emergencyAccessId, User grantorUser);\n    /// <summary>\n    /// This request is made by the Grantee user to fetch the policies <see cref=\"Policy\"/> for the Grantor User.\n    /// The Grantor User has to be the owner of the organization. <see cref=\"OrganizationUserType\"/>\n    /// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user\n    /// are returned. This is used to ensure the password is of the proper complexity for the organization.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">EmergencyAccess.Id being acted on</param>\n    /// <param name=\"granteeUser\">User making the request, this is the Grantee</param>\n    /// <returns>null if the GrantorUser is not an organization owner; A list of policies otherwise.</returns>\n    Task<ICollection<Policy>> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser);\n    /// <summary>\n    /// Fetches the emergency access entity and grantor user. The grantor user is returned so the correct KDF configuration is\n    /// used for the new password.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">Id of entity being accessed</param>\n    /// <param name=\"granteeUser\">grantee user of the emergency access entity</param>\n    /// <returns>emergency access entity and the grantorUser</returns>\n    Task<(Entities.EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);\n    /// <summary>\n    /// Updates the grantor's password hash and updates the key for the EmergencyAccess entity.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">Emergency Access Id being acted on</param>\n    /// <param name=\"granteeUser\">user making the request</param>\n    /// <param name=\"newMasterPasswordHash\">new password hash set by grantee user</param>\n    /// <param name=\"key\">new encrypted user key</param>\n    /// <returns>void</returns>\n    Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key);\n    /// <summary>\n    /// sends a reminder email that there is a pending request for recovery.\n    /// </summary>\n    /// <returns>void</returns>\n    Task SendNotificationsAsync();\n    /// <summary>\n    /// This handles the auto approval of recovery requests started in the <see cref=\"InitiateAsync\"/> method.\n    /// An email will be sent to the Grantee and the Grantor notifying each the recovery has been approved.\n    /// </summary>\n    /// <returns>void</returns>\n    Task HandleTimedOutRequestsAsync();\n    /// <summary>\n    /// Fetched ciphers from the grantors account for the grantee to view.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">Emergency access entity being acted on</param>\n    /// <param name=\"granteeUser\">user requesting cipher items</param>\n    /// <returns>ciphers associated with the emergency access request</returns>\n    Task<EmergencyAccessViewData> ViewAsync(Guid emergencyAccessId, User granteeUser);\n    /// <summary>\n    /// Returns attachment if the grantee user has access to the cipher through the emergency access entity.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">EmergencyAccess entity being acted on</param>\n    /// <param name=\"cipherId\">cipher entity containing the attachment</param>\n    /// <param name=\"attachmentId\">Attachment entity</param>\n    /// <param name=\"granteeUser\">user making the request</param>\n    /// <returns>attachment response </returns>\n    Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/EmergencyAccess/Interfaces/IDeleteEmergencyAccessCommand.cs",
    "content": "﻿using Bit.Core.Exceptions;\n\nnamespace Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\n\n/// <summary>\n/// Command for deleting emergency access records.\n/// </summary>\npublic interface IDeleteEmergencyAccessCommand\n{\n    /// <summary>\n    /// Deletes a single emergency access record if the requesting user is the grantor or grantee.\n    /// Sends an email notification to the grantor when a grantee is removed.\n    /// </summary>\n    /// <param name=\"emergencyAccessId\">The ID of the emergency access record to delete.</param>\n    /// <param name=\"userId\">The ID of the requesting user; must be either the grantor or grantee of the record.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    /// <remarks>\n    /// Only records in <c>Accepted</c> or <c>Confirmed</c> status have a <c>GranteeId</c> foreign key set.\n    /// <c>Invited</c> records store only an email address, so a user cannot be matched as the grantee on\n    /// such records. When the user ID matches the <c>GrantorId</c>, records in any status are found.\n    /// </remarks>\n    /// <exception cref=\"BadRequestException\">\n    /// Thrown when the emergency access record is not found or does not belong to the specified user.\n    /// </exception>\n    Task DeleteByIdAndUserIdAsync(Guid emergencyAccessId, Guid userId);\n\n    /// <summary>\n    /// Deletes all emergency access records where the user IDs are either grantors or grantees.\n    /// Sends email notifications only to grantors when their grantees are removed.\n    /// </summary>\n    /// <param name=\"userIds\">The IDs of users whose emergency access records — as grantor or grantee — will be deleted.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    /// <remarks>\n    /// Only records in <c>Accepted</c> or <c>Confirmed</c> status have a <c>GranteeId</c> foreign key set.\n    /// <c>Invited</c> records store only an email address, so a user cannot be matched as the grantee on\n    /// such records. When a user ID matches the <c>GrantorId</c>, records in any status are found.\n    /// If no records are found for the provided user IDs, the method returns.\n    /// </remarks>\n    Task DeleteAllByUserIdsAsync(ICollection<Guid> userIds);\n\n    /// <summary>\n    /// Deletes all emergency access records where the user ID is either a grantor or a grantee.\n    /// Sends email notifications only to grantors when their grantees are removed.\n    /// </summary>\n    /// <param name=\"userId\">The ID of the user whose emergency access records — as grantor or grantee — will be deleted.</param>\n    /// <returns>A task representing the asynchronous operation.</returns>\n    /// <remarks>\n    /// Only records in <c>Accepted</c> or <c>Confirmed</c> status have a <c>GranteeId</c> foreign key set.\n    /// <c>Invited</c> records store only an email address, so a user cannot be matched as the grantee on\n    /// such records. When the user ID matches the <c>GrantorId</c>, records in any status are found.\n    /// If no records are found for the provided user ID, the method returns.\n    /// </remarks>\n    Task DeleteAllByUserIdAsync(Guid userId);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/EmergencyAccess/Mail/EmergencyAccessRemoveGranteesMailView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail;\n\npublic class EmergencyAccessRemoveGranteesMailView : BaseMailView\n{\n\n    public required IEnumerable<string> RemovedGranteeEmails { get; set; }\n    // ReSharper disable once MemberCanBeMadeStatic.Global\n#pragma warning disable CA1822\n    // Handlebars needs it to be an instance variable to work properly.\n    public string EmergencyAccessHelpPageUrl => \"https://bitwarden.com/help/emergency-access/\";\n#pragma warning restore CA1822\n}\n\npublic class EmergencyAccessRemoveGranteesMail : BaseMail<EmergencyAccessRemoveGranteesMailView>\n{\n    public override string Subject { get; set; } = \"Emergency contacts removed\";\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/EmergencyAccess/Mail/EmergencyAccessRemoveGranteesMailView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 0px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: normal; font-size: 24px; line-height: 32px\">\n              \n            </h1></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-hero-responsive-img\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"undefined\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 20px 0px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 0px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">The following emergency contacts have been removed from your\n            account:\n            <ul>\n              {{#each RemovedGranteeEmails}}\n              <li>{{ this }}</li>\n              {{/each}}\n            </ul>\n            Learn more about\n            <a href=\"{{EmergencyAccessHelpPageUrl}}\">emergency access</a>.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/Auth/UserFeatures/EmergencyAccess/Mail/EmergencyAccessRemoveGranteesMailView.text.hbs",
    "content": "The following emergency contacts have been removed from your account:\n\n{{#each RemovedGranteeEmails}}\n    {{this}}\n{{/each}}\n\nLearn more about emergency access at {{EmergencyAccessHelpPageUrl}}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/EmergencyAccess/readme.md",
    "content": "# Emergency Access System\n\nThis system allows users to share their `User.Key` with other users using public key exchange. An emergency contact (a grantee user) can view or takeover (reset the password) of the grantor user.\n\nWhen an account is taken over all two factor methods are turned off and device verification is disabled.\n\nThis system is affected by the Key Rotation feature. The `EmergencyAccess.KeyEncrypted` is the `Grantor.UserKey` encrypted by the `Grantee.PublicKey`. So if the `User.Key` is rotated then all `EmergencyAccess` entities will need to be updated.\n\n## Special Cases\n\nUsers who use `KeyConnector` are not able to allow `Takeover` of their accounts. However, they can allow `View`.\n\nWhen a grantee user _takes over_ a grantor user's account, the grantor user will be removed from all organizations where the grantor user is not the `OrganizationUserType.Owner`. A grantor user will not be removed from organizations if the `EmergencyAccessType` is `View`. The grantee user will only be able to `View` the grantor user's ciphers, and not any of the organization ciphers, if any exist.\n\n## Step 1. Invitation\n\nA grantor user invites another user to be their emergency contact, the grantee. This will create a new `EmergencyAccess` entity in the database with the `EmergencyAccessStatusType` set to `Invited`.\nThe `EmergencyAccess.KeyEncrypted` field is empty, and the `GranteeId` is `null` since the user being invited via email may not have an account yet.\n\n### code\n\n```csharp\n// creates entity.\nTask<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);\n// resend email to the EmergencyAccess.Email.\nTask ResendInviteAsync(User grantorUser, Guid emergencyAccessId);\n```\n\n## Step 2. Acceptance\n\nThe grantee user receives an email they have been invited to be an emergency contact for a grantor user.\n\nAt this point the grantee user can accept the request. This will set the `EmergencyAccess.GranteeId` to the `User.Id` of the grantee user. The `EmergencyAccess.Status` is set to `Accepted`.\n\nIf the grantee user does not have an account then they can create an account and accept the invitation.\n\n### Code\n\n```csharp\n// accepts the request to be an emergency contact.\nTask<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);\n```\n\n## Step 3. Confirmation\n\nOnce the grantee user has accepted, the `EmergencyAccess.GranteeId` allows the grantor user the ability to query for the `GranteeUser.PublicKey`. With the `Grantee.PublicKey`, the grantor on the client is able to safely encrypt their `User.Key` and save the encrypted string to the database.\n\nThe `EmergencyAccess.Status` is set to `Confirmed`, and the `EmergencyAccess.KeyEncrypted` is set.\n\n### Code\n\n```csharp\nTask<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);\n```\n\n## Step 4. Recovery Approval\n\nThe grantee user can now exercise the ability to view or takeover the account. This is done by initiating the recovery. Initiating recovery has a time delay specified by `EmergencyAccess.WaitTime`. `WaitTime` is set in the initial invite. The grantor user can approve the request before the `WaitTime`, but they _cannot_ reject the request _after_ the `WaitTime` has completed. If the recovery request is not rejected then once the `WaitTime` has passed the grantee user will be able to access the emergency access entity.\n\n### Code\n\n```csharp\n// Initiates the recovery process; Will set EmergencyAccess.Status to RecoveryInitiated.\nTask InitiateAsync(Guid id, User granteeUser);\n// Approved the recovery request; Will set EmergencyAccess.Status to RecoveryApproved.\nTask ApproveAsync(Guid id, User approvingUser);\n// Rejects the recovery request; Will set EmergencyAccess.Status to Confirmed.\nTask RejectAsync(Guid id, User rejectingUser);\n// Automatically set the EmergencyAccess.Status to RecoveryApproved after WaitTime has passed.\nTask HandleTimedOutRequestsAsync();\n```\n\n## Step 5. Recovering the account\n\nOnce the `EmergencyAccess.Status` is `RecoveryApproved` the grantee user is able to exercise their ability to view or takeover the grantor account. Viewing allows the grantee user to view the vault data of the grantor user. Takeover allows the grantee to change the password of the grantor user.\n\n### Takeover\n\n`TakeoverAsync(Guid, User)` returns the grantor user object along with the `EmergencyAccess` entity. The grantor user object is required since to update the password the client needs access to the grantor kdf configuration. Once the password has been set in the `PasswordAsync(Guid, User, string, string)` the account has been successfully recovered.\n\nTaking over the account will change the password of the grantor user, empty the two factor array on the grantor user, and disable device verification.\n\n```csharp\n// Takeover returns the grantor user and the emergency access entity.\nTask<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);\n// Password sets the password for the grantor user.\nTask PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key);\n// Returns Ciphers the Grantee is allowed to view based on the EmergencyAccess status.\nTask<EmergencyAccessViewData> ViewAsync(Guid emergencyAccessId, User granteeUser);\n// Returns downloadable cipher attachments based on the EmergencyAccess status.\nTask<AttachmentResponseData> GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser);\n```\n\n## Optional steps\n\nEither the grantor or grantee is able to delete an emergency access record at any time, at any point in the recovery process.\n\n### Code\n\n```csharp\n// Deletes the associated EmergencyAccess entity. The requesting user must be the grantor or grantee.\nTask DeleteByIdAndUserIdAsync(Guid emergencyAccessId, Guid userId);\n```\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/PasswordValidation/PasswordValidationConstants.cs",
    "content": "﻿namespace Bit.Core.Auth.UserFeatures.PasswordValidation;\n\npublic static class PasswordValidationConstants\n{\n    public const int PasswordHasherKdfIterations = 100000;\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Auth.UserFeatures.Registration;\n\npublic interface IRegisterUserCommand\n{\n\n    /// <summary>\n    /// Creates a new user, sends a welcome email, and raises the signup reference event.\n    /// This method is used for JIT of organization Users.\n    /// </summary>\n    /// <param name=\"user\">The <see cref=\"User\"/> to create</param>\n    /// <returns><see cref=\"IdentityResult\"/></returns>\n    public Task<IdentityResult> RegisterUser(User user);\n\n    /// <summary>\n    /// Creates a new user, sends a welcome email, and raises the signup reference event.\n    /// This method is used by SSO auto-provisioned organization Users.\n    /// </summary>\n    /// <param name=\"user\">The <see cref=\"User\"/> to create</param>\n    /// <param name=\"organization\">The <see cref=\"Organization\"/> associated with the user</param>\n    /// <returns><see cref=\"IdentityResult\"/></returns>\n    Task<IdentityResult> RegisterSSOAutoProvisionedUserAsync(User user, Organization organization);\n\n    /// <summary>\n    /// Creates a new user with a given master password hash, sends a welcome email (differs based on initiation path),\n    /// and raises the signup reference event. Optionally accepts an org invite token and org user id to associate\n    /// the user with an organization upon registration and login. Both are required if either is provided or validation will fail.\n    /// If the organization has a 2FA required policy enabled, email verification will be enabled for the user.\n    /// </summary>\n    /// <param name=\"user\">The <see cref=\"User\"/> to create</param>\n    /// <param name=\"masterPasswordHash\">The hashed master password the user entered</param>\n    /// <param name=\"orgInviteToken\">The org invite token sent to the user via email</param>\n    /// <param name=\"orgUserId\">The associated org user guid that was created at the time of invite</param>\n    /// <returns><see cref=\"IdentityResult\"/></returns>\n    public Task<IdentityResult> RegisterUserViaOrganizationInviteToken(User user, string masterPasswordHash, string orgInviteToken, Guid? orgUserId);\n\n    /// <summary>\n    /// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.\n    /// If a valid email verification token is provided, the user will be created with their email verified.\n    /// An error will be thrown if the token is invalid or expired.\n    /// </summary>\n    /// <param name=\"user\">The <see cref=\"User\"/> to create</param>\n    /// <param name=\"masterPasswordHash\">The hashed master password the user entered</param>\n    /// <param name=\"emailVerificationToken\">The email verification token sent to the user via email</param>\n    /// <returns><see cref=\"IdentityResult\"/></returns>\n    public Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash, string emailVerificationToken);\n\n    /// <summary>\n    /// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.\n    /// If a valid org sponsored free family plan invite token is provided, the user will be created with their email verified.\n    /// If the token is invalid or expired, an error will be thrown.\n    /// </summary>\n    /// <param name=\"user\">The <see cref=\"User\"/> to create</param>\n    /// <param name=\"masterPasswordHash\">The hashed master password the user entered</param>\n    /// <param name=\"orgSponsoredFreeFamilyPlanInviteToken\">The org sponsored free family plan invite token sent to the user via email</param>\n    /// <returns><see cref=\"IdentityResult\"/></returns>\n    public Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken);\n\n    /// <summary>\n    /// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.\n    /// If a valid token is provided, the user will be created with their email verified.\n    /// If the token is invalid or expired, an error will be thrown.\n    /// </summary>\n    /// <param name=\"user\">The <see cref=\"User\"/> to create</param>\n    /// <param name=\"masterPasswordHash\">The hashed master password the user entered</param>\n    /// <param name=\"acceptEmergencyAccessInviteToken\">The emergency access invite token sent to the user via email</param>\n    /// <param name=\"acceptEmergencyAccessId\">The emergency access id (used to validate the token)</param>\n    /// <returns><see cref=\"IdentityResult\"/></returns>\n    public Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash,\n        string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId);\n\n    /// <summary>\n    /// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.\n    /// If a valid token is provided, the user will be created with their email verified.\n    /// If the token is invalid or expired, an error will be thrown.\n    /// </summary>\n    /// <param name=\"user\">The <see cref=\"User\"/> to create</param>\n    /// <param name=\"masterPasswordHash\">The hashed master password the user entered</param>\n    /// <param name=\"providerInviteToken\">The provider invite token sent to the user via email</param>\n    /// <param name=\"providerUserId\">The provider user id which is used to validate the invite token</param>\n    /// <returns><see cref=\"IdentityResult\"/></returns>\n    public Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, string masterPasswordHash, string providerInviteToken, Guid providerUserId);\n\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/Registration/ISendVerificationEmailForRegistrationCommand.cs",
    "content": "﻿#nullable enable\nnamespace Bit.Core.Auth.UserFeatures.Registration;\n\npublic interface ISendVerificationEmailForRegistrationCommand\n{\n    public Task<string?> Run(string email, string? name, bool receiveMarketingEmails, string? fromMarketing);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Logging;\nusing Newtonsoft.Json;\n\nnamespace Bit.Core.Auth.UserFeatures.Registration.Implementations;\n\npublic class RegisterUserCommand : IRegisterUserCommand\n{\n    private readonly ILogger<RegisterUserCommand> _logger;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IPolicyQuery _policyQuery;\n    private readonly IOrganizationDomainRepository _organizationDomainRepository;\n    private readonly IFeatureService _featureService;\n\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;\n    private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;\n    private readonly IDataProtector _providerServiceDataProtector;\n\n    private readonly IUserService _userService;\n    private readonly IMailService _mailService;\n\n    private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand;\n\n    private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _emergencyAccessInviteTokenDataFactory;\n\n    private readonly string _disabledUserRegistrationExceptionMsg = \"Open registration has been disabled by the system administrator.\";\n\n    public RegisterUserCommand(\n            ILogger<RegisterUserCommand> logger,\n            IGlobalSettings globalSettings,\n            IOrganizationUserRepository organizationUserRepository,\n            IOrganizationRepository organizationRepository,\n            IPolicyQuery policyQuery,\n            IOrganizationDomainRepository organizationDomainRepository,\n            IFeatureService featureService,\n            IDataProtectionProvider dataProtectionProvider,\n            IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,\n            IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,\n            IUserService userService,\n            IMailService mailService,\n            IValidateRedemptionTokenCommand validateRedemptionTokenCommand,\n            IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> emergencyAccessInviteTokenDataFactory)\n    {\n        _logger = logger;\n        _globalSettings = globalSettings;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationRepository = organizationRepository;\n        _policyQuery = policyQuery;\n        _organizationDomainRepository = organizationDomainRepository;\n        _featureService = featureService;\n\n        _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;\n        _registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory;\n\n        _userService = userService;\n        _mailService = mailService;\n\n        _validateRedemptionTokenCommand = validateRedemptionTokenCommand;\n        _emergencyAccessInviteTokenDataFactory = emergencyAccessInviteTokenDataFactory;\n\n        _providerServiceDataProtector = dataProtectionProvider.CreateProtector(\"ProviderServiceDataProtector\");\n    }\n\n    public async Task<IdentityResult> RegisterUser(User user)\n    {\n        await ValidateEmailDomainNotBlockedAsync(user.Email);\n\n        var result = await _userService.CreateUserAsync(user);\n        if (result == IdentityResult.Success)\n        {\n            await _mailService.SendWelcomeEmailAsync(user);\n        }\n\n        return result;\n    }\n\n    public async Task<IdentityResult> RegisterSSOAutoProvisionedUserAsync(User user, Organization organization)\n    {\n        // Validate that the email domain is not blocked by another organization's policy\n        await ValidateEmailDomainNotBlockedAsync(user.Email, organization.Id);\n\n        var result = await _userService.CreateUserAsync(user);\n        if (result == IdentityResult.Success)\n        {\n            await SendWelcomeEmailAsync(user, organization);\n        }\n\n        return result;\n    }\n\n    public async Task<IdentityResult> RegisterUserViaOrganizationInviteToken(User user, string masterPasswordHash,\n        string orgInviteToken, Guid? orgUserId)\n    {\n        TryValidateOrgInviteToken(orgInviteToken, orgUserId, user);\n        var orgUser = await SetUserEmail2FaIfOrgPolicyEnabledAsync(orgUserId, user);\n        if (orgUser == null && orgUserId.HasValue)\n        {\n            throw new BadRequestException(\"Invalid organization user invitation.\");\n        }\n        await ValidateEmailDomainNotBlockedAsync(user.Email, orgUser?.OrganizationId);\n\n        user.ApiKey = CoreHelpers.SecureRandomString(30);\n\n        if (!string.IsNullOrEmpty(orgInviteToken) && orgUserId.HasValue)\n        {\n            user.EmailVerified = true;\n        }\n\n        var result = await _userService.CreateUserAsync(user, masterPasswordHash);\n        var organization = await GetOrganizationUserOrganization(orgUserId ?? Guid.Empty, orgUser);\n        if (result == IdentityResult.Success)\n        {\n            var sentWelcomeEmail = false;\n            if (!string.IsNullOrEmpty(user.ReferenceData))\n            {\n                var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(user.ReferenceData) ?? [];\n                if (referenceData.TryGetValue(\"initiationPath\", out var value))\n                {\n                    var initiationPath = value.ToString() ?? string.Empty;\n                    await SendAppropriateWelcomeEmailAsync(user, initiationPath, organization);\n                    sentWelcomeEmail = true;\n                    if (!string.IsNullOrEmpty(initiationPath))\n                    {\n                        return result;\n                    }\n                }\n            }\n\n            if (!sentWelcomeEmail)\n            {\n                await SendWelcomeEmailAsync(user, organization);\n            }\n        }\n\n        return result;\n    }\n\n    /// <summary>\n    /// This method attempts to validate the org invite token if provided. If the token is invalid an exception is thrown.\n    /// If there is no exception it is assumed the token is valid or not provided and open registration is allowed.\n    /// </summary>\n    /// <param name=\"orgInviteToken\">The organization invite token.</param>\n    /// <param name=\"orgUserId\">The organization user ID.</param>\n    /// <param name=\"user\">The user being registered.</param>\n    /// <exception cref=\"BadRequestException\">If validation fails then an exception is thrown.</exception>\n    private void TryValidateOrgInviteToken(string orgInviteToken, Guid? orgUserId, User user)\n    {\n        var orgInviteTokenProvided = !string.IsNullOrWhiteSpace(orgInviteToken);\n\n        if (orgInviteTokenProvided && orgUserId.HasValue)\n        {\n            // We have token data so validate it\n            if (OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(\n                    _orgUserInviteTokenDataFactory, orgInviteToken, orgUserId.Value, user.Email))\n            {\n                return;\n            }\n\n            // Token data is invalid\n            if (_globalSettings.DisableUserRegistration)\n            {\n                throw new BadRequestException(_disabledUserRegistrationExceptionMsg);\n            }\n\n            throw new BadRequestException(\"Organization invite token is invalid.\");\n        }\n\n        // no token data or missing token data\n        // Throw if open registration is disabled and there isn't an org invite token or an org user id\n        // as you can't register without them.\n        if (_globalSettings.DisableUserRegistration)\n        {\n            throw new BadRequestException(_disabledUserRegistrationExceptionMsg);\n        }\n\n        // Open registration is allowed\n        // if we have an org invite token but no org user id, then throw an exception as we can't validate the token\n        if (orgInviteTokenProvided && !orgUserId.HasValue)\n        {\n            throw new BadRequestException(\"Organization invite token cannot be validated without an organization user id.\");\n        }\n\n        // if we have an org user id but no org invite token, then throw an exception as that isn't a supported flow\n        if (orgUserId.HasValue && string.IsNullOrWhiteSpace(orgInviteToken))\n        {\n            throw new BadRequestException(\"Organization user id cannot be provided without an organization invite token.\");\n        }\n\n        // If both orgInviteToken && orgUserId are missing, then proceed with open registration\n    }\n\n    /// <summary>\n    /// Handles initializing the user with Email 2FA enabled if they are subject to an enabled 2FA organizational policy.\n    /// </summary>\n    /// <param name=\"orgUserId\">The optional org user id</param>\n    /// <param name=\"user\">The newly created user object which could be modified</param>\n    /// <returns>The organization user if one exists for the provided org user id, null otherwise</returns>\n    private async Task<OrganizationUser?> SetUserEmail2FaIfOrgPolicyEnabledAsync(Guid? orgUserId, User user)\n    {\n        if (!orgUserId.HasValue)\n        {\n            return null;\n        }\n\n        var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserId.Value);\n        if (orgUser != null)\n        {\n            var twoFactorPolicy = await _policyQuery.RunAsync(orgUser.OrganizationId,\n                PolicyType.TwoFactorAuthentication);\n            if (twoFactorPolicy.Enabled)\n            {\n                user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n                {\n\n                    [TwoFactorProviderType.Email] = new TwoFactorProvider\n                    {\n                        MetaData = new Dictionary<string, object> { [\"Email\"] = user.Email.ToLowerInvariant() },\n                        Enabled = true\n                    }\n                });\n                _userService.SetTwoFactorProvider(user, TwoFactorProviderType.Email);\n            }\n        }\n        return orgUser;\n    }\n\n    private async Task SendAppropriateWelcomeEmailAsync(User user, string initiationPath, Organization? organization)\n    {\n        var isFromMarketingWebsite = initiationPath.Contains(\"Secrets Manager trial\");\n\n        if (isFromMarketingWebsite)\n        {\n            await _mailService.SendTrialInitiationEmailAsync(user.Email);\n        }\n        else\n        {\n            await SendWelcomeEmailAsync(user, organization);\n        }\n    }\n\n    public async Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash,\n        string emailVerificationToken)\n    {\n        ValidateOpenRegistrationAllowed();\n        await ValidateEmailDomainNotBlockedAsync(user.Email);\n\n        var tokenable = ValidateRegistrationEmailVerificationTokenable(emailVerificationToken, user.Email);\n\n        user.EmailVerified = true;\n        user.Name = tokenable.Name;\n        user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.\n\n        var result = await _userService.CreateUserAsync(user, masterPasswordHash);\n        if (result == IdentityResult.Success)\n        {\n            await SendWelcomeEmailAsync(user);\n        }\n\n        return result;\n    }\n\n    public async Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, string masterPasswordHash,\n        string orgSponsoredFreeFamilyPlanInviteToken)\n    {\n        ValidateOpenRegistrationAllowed();\n        await ValidateEmailDomainNotBlockedAsync(user.Email);\n        await ValidateOrgSponsoredFreeFamilyPlanInviteToken(orgSponsoredFreeFamilyPlanInviteToken, user.Email);\n\n        user.EmailVerified = true;\n        user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.\n\n        var result = await _userService.CreateUserAsync(user, masterPasswordHash);\n        if (result == IdentityResult.Success)\n        {\n            await SendWelcomeEmailAsync(user);\n        }\n\n        return result;\n    }\n\n\n    // TODO: in future, consider how we can consolidate base registration logic to reduce code duplication\n    public async Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash,\n        string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)\n    {\n        ValidateOpenRegistrationAllowed();\n        await ValidateEmailDomainNotBlockedAsync(user.Email);\n        ValidateAcceptEmergencyAccessInviteToken(acceptEmergencyAccessInviteToken, acceptEmergencyAccessId, user.Email);\n\n        user.EmailVerified = true;\n        user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.\n\n        var result = await _userService.CreateUserAsync(user, masterPasswordHash);\n        if (result == IdentityResult.Success)\n        {\n            await SendWelcomeEmailAsync(user);\n        }\n\n        return result;\n    }\n\n    public async Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, string masterPasswordHash,\n        string providerInviteToken, Guid providerUserId)\n    {\n        ValidateOpenRegistrationAllowed();\n        await ValidateEmailDomainNotBlockedAsync(user.Email);\n        ValidateProviderInviteToken(providerInviteToken, providerUserId, user.Email);\n\n        user.EmailVerified = true;\n        user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.\n\n        var result = await _userService.CreateUserAsync(user, masterPasswordHash);\n        if (result == IdentityResult.Success)\n        {\n            await SendWelcomeEmailAsync(user);\n        }\n\n        return result;\n    }\n\n    private void ValidateOpenRegistrationAllowed()\n    {\n        // We validate open registration on send of initial email and here b/c a user could technically start the\n        // account creation process while open registration is enabled and then finish it after it has been\n        // disabled by the self hosted admin.\n        if (_globalSettings.DisableUserRegistration)\n        {\n            throw new BadRequestException(_disabledUserRegistrationExceptionMsg);\n        }\n    }\n\n    private async Task ValidateOrgSponsoredFreeFamilyPlanInviteToken(string orgSponsoredFreeFamilyPlanInviteToken, string userEmail)\n    {\n        var (valid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, userEmail);\n\n        if (!valid)\n        {\n            throw new BadRequestException(\"Invalid org sponsored free family plan token.\");\n        }\n    }\n\n    private void ValidateAcceptEmergencyAccessInviteToken(string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId, string userEmail)\n    {\n        _emergencyAccessInviteTokenDataFactory.TryUnprotect(acceptEmergencyAccessInviteToken, out var tokenable);\n        if (tokenable == null || !tokenable.Valid || !tokenable.IsValid(acceptEmergencyAccessId, userEmail))\n        {\n            throw new BadRequestException(\"Invalid accept emergency access invite token.\");\n        }\n    }\n\n    private void ValidateProviderInviteToken(string providerInviteToken, Guid providerUserId, string userEmail)\n    {\n        if (!CoreHelpers.TokenIsValid(\"ProviderUserInvite\", _providerServiceDataProtector, providerInviteToken, userEmail, providerUserId,\n                _globalSettings.OrganizationInviteExpirationHours))\n        {\n            throw new BadRequestException(\"Invalid provider invite token.\");\n        }\n    }\n\n\n    private RegistrationEmailVerificationTokenable ValidateRegistrationEmailVerificationTokenable(string emailVerificationToken, string userEmail)\n    {\n        _registrationEmailVerificationTokenDataFactory.TryUnprotect(emailVerificationToken, out var tokenable);\n        if (tokenable == null || !tokenable.Valid || !tokenable.TokenIsValid(userEmail))\n        {\n            throw new BadRequestException(\"Invalid email verification token.\");\n        }\n\n        return tokenable;\n    }\n\n    private async Task ValidateEmailDomainNotBlockedAsync(string email, Guid? excludeOrganizationId = null)\n    {\n        var emailDomain = EmailValidation.GetDomain(email);\n\n        var isDomainBlocked = await _organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\n            emailDomain, excludeOrganizationId);\n        if (isDomainBlocked)\n        {\n            _logger.LogInformation(\n                \"User registration blocked by domain claim policy. Domain: {Domain}, ExcludedOrgId: {ExcludedOrgId}\",\n                emailDomain,\n                excludeOrganizationId);\n            throw new BadRequestException(\"This email address is claimed by an organization using Bitwarden.\");\n        }\n    }\n\n    /// <summary>\n    /// We send different welcome emails depending on whether the user is joining a free/family or an enterprise organization. If information to populate the\n    /// email isn't present we send the standard individual welcome email.\n    /// </summary>\n    /// <param name=\"user\">Target user for the email</param>\n    /// <param name=\"organization\">this value is nullable</param>\n    /// <returns></returns>\n    private async Task SendWelcomeEmailAsync(User user, Organization? organization = null)\n    {\n        // Check if feature is enabled\n        // TODO: Remove Feature flag: PM-28221\n        if (!_featureService.IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates))\n        {\n            await _mailService.SendWelcomeEmailAsync(user);\n            return;\n        }\n\n        // Most emails are probably for non organization users so we default to that experience\n        if (organization == null)\n        {\n            await _mailService.SendIndividualUserWelcomeEmailAsync(user);\n        }\n        // We need to make sure that the organization email has the correct data to display otherwise we just send the standard welcome email\n        else if (!string.IsNullOrEmpty(organization.DisplayName()))\n        {\n            // If the organization is Free or Families plan, send families welcome email\n            if (organization.PlanType.GetProductTier() is ProductTierType.Free or ProductTierType.Families)\n            {\n                await _mailService.SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, organization.DisplayName());\n            }\n            else\n            {\n                await _mailService.SendOrganizationUserWelcomeEmailAsync(user, organization.DisplayName());\n            }\n        }\n        // If the organization data isn't present send the standard welcome email\n        else\n        {\n            await _mailService.SendIndividualUserWelcomeEmailAsync(user);\n        }\n    }\n\n    private async Task<Organization?> GetOrganizationUserOrganization(Guid orgUserId, OrganizationUser? orgUser = null)\n    {\n        var organizationUser = orgUser ?? await _organizationUserRepository.GetByIdAsync(orgUserId);\n        if (organizationUser == null)\n        {\n            return null;\n        }\n\n        return await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/Registration/Implementations/SendVerificationEmailForRegistrationCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Auth.UserFeatures.Registration.Implementations;\n\n/// <summary>\n/// If email verification is enabled, this command will send a verification email to the user which will\n///  contain a link to complete the registration process.\n/// If email verification is disabled, this command will return a token that can be used to complete the registration process directly.\n/// </summary>\npublic class SendVerificationEmailForRegistrationCommand : ISendVerificationEmailForRegistrationCommand\n{\n    private readonly ILogger<SendVerificationEmailForRegistrationCommand> _logger;\n    private readonly IUserRepository _userRepository;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IMailService _mailService;\n    private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _tokenDataFactory;\n    private readonly IOrganizationDomainRepository _organizationDomainRepository;\n\n    public SendVerificationEmailForRegistrationCommand(\n        ILogger<SendVerificationEmailForRegistrationCommand> logger,\n        IUserRepository userRepository,\n        GlobalSettings globalSettings,\n        IMailService mailService,\n        IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> tokenDataFactory,\n        IOrganizationDomainRepository organizationDomainRepository)\n    {\n        _logger = logger;\n        _userRepository = userRepository;\n        _globalSettings = globalSettings;\n        _mailService = mailService;\n        _tokenDataFactory = tokenDataFactory;\n        _organizationDomainRepository = organizationDomainRepository;\n\n    }\n\n    public async Task<string?> Run(string email, string? name, bool receiveMarketingEmails, string? fromMarketing)\n    {\n        if (_globalSettings.DisableUserRegistration)\n        {\n            throw new BadRequestException(\"Open registration has been disabled by the system administrator.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(email))\n        {\n            throw new ArgumentNullException(nameof(email));\n        }\n\n        // Check if the email domain is blocked by an organization policy\n        var emailDomain = EmailValidation.GetDomain(email);\n\n        if (await _organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(emailDomain))\n        {\n            _logger.LogInformation(\n                \"User registration email verification blocked by domain claim policy. Domain: {Domain}\",\n                emailDomain);\n            throw new BadRequestException(\"This email address is claimed by an organization using Bitwarden.\");\n        }\n\n        // Check to see if the user already exists\n        var user = await _userRepository.GetByEmailAsync(email);\n        var userExists = user != null;\n\n        if (!_globalSettings.EnableEmailVerification)\n        {\n            if (userExists)\n            {\n                throw new BadRequestException($\"Email {email} is already taken\");\n            }\n\n            // if user doesn't exist, return a EmailVerificationTokenable in the response body.\n            var token = GenerateToken(email, name, receiveMarketingEmails);\n\n            return token;\n        }\n\n        if (!userExists)\n        {\n            // If the user doesn't exist, create a new EmailVerificationTokenable and send the user\n            // an email with a link to verify their email address\n            var token = GenerateToken(email, name, receiveMarketingEmails);\n            await _mailService.SendRegistrationVerificationEmailAsync(email, token, fromMarketing);\n        }\n\n        // User exists but we will return a 200 regardless of whether the email was sent or not; so return null\n        return null;\n    }\n\n    private string GenerateToken(string email, string? name, bool receiveMarketingEmails)\n    {\n        var registrationEmailVerificationTokenable = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);\n        return _tokenDataFactory.Protect(registrationEmailVerificationTokenable);\n    }\n}\n\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Auth.Identity;\n\nnamespace Bit.Core.Auth.UserFeatures.SendAccess;\n\npublic static class SendAccessClaimsPrincipalExtensions\n{\n    public static Guid GetSendId(this ClaimsPrincipal user)\n    {\n        ArgumentNullException.ThrowIfNull(user);\n\n        var sendIdClaim = user.FindFirst(Claims.SendAccessClaims.SendId)\n            ?? throw new InvalidOperationException(\"send_id claim not found.\");\n\n        if (!Guid.TryParse(sendIdClaim.Value, out var sendGuid))\n        {\n            throw new InvalidOperationException(\"Invalid send_id claim value.\");\n        }\n\n        return sendGuid;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TdeOffboardingPassword/Interfaces/ITdeOffboardingPasswordCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;\n\n/// <summary>\n/// <para>Manages the setting of the master password for JIT provisioned TDE <see cref=\"User\"/> in an organization, after the organization disabled TDE.</para>\n/// <para>This command is invoked, when the user first logs in after the organization has switched from TDE to master password based decryption.</para>\n/// </summary>\npublic interface ITdeOffboardingPasswordCommand\n{\n    public Task<IdentityResult> UpdateTdeOffboardingPasswordAsync(User user, string masterPassword, string key,\n        string orgSsoIdentifier);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TdeOffboardingPassword/TdeOffboardingPasswordCommand.cs",
    "content": "﻿using Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Auth.UserFeatures.UserMasterPassword;\n\npublic class TdeOffboardingPasswordCommand : ITdeOffboardingPasswordCommand\n{\n    private readonly IUserService _userService;\n    private readonly IUserRepository _userRepository;\n    private readonly IEventService _eventService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ISsoUserRepository _ssoUserRepository;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly IPushNotificationService _pushService;\n\n\n    public TdeOffboardingPasswordCommand(\n        IUserService userService,\n        IUserRepository userRepository,\n        IEventService eventService,\n        IOrganizationUserRepository organizationUserRepository,\n        ISsoUserRepository ssoUserRepository,\n        ISsoConfigRepository ssoConfigRepository,\n        IPushNotificationService pushService)\n    {\n        _userService = userService;\n        _userRepository = userRepository;\n        _eventService = eventService;\n        _organizationUserRepository = organizationUserRepository;\n        _ssoUserRepository = ssoUserRepository;\n        _ssoConfigRepository = ssoConfigRepository;\n        _pushService = pushService;\n    }\n\n    public async Task<IdentityResult> UpdateTdeOffboardingPasswordAsync(User user, string newMasterPassword, string key, string hint)\n    {\n        if (string.IsNullOrWhiteSpace(newMasterPassword))\n        {\n            throw new BadRequestException(\"Master password is required.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(key))\n        {\n            throw new BadRequestException(\"Key is required.\");\n        }\n\n        if (user.HasMasterPassword())\n        {\n            throw new BadRequestException(\"User already has a master password.\");\n        }\n        var orgUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id);\n        orgUserDetails = orgUserDetails.Where(x => x.UseSso).ToList();\n        if (orgUserDetails.Count == 0)\n        {\n            throw new BadRequestException(\"User is not part of any organization that has SSO enabled.\");\n        }\n\n        var orgSSOUsers = await Task.WhenAll(orgUserDetails.Select(async x => await _ssoUserRepository.GetByUserIdOrganizationIdAsync(x.OrganizationId, user.Id)));\n        if (orgSSOUsers.Length != 1)\n        {\n            throw new BadRequestException(\"User is part of no or multiple SSO configurations.\");\n        }\n\n        var orgUser = orgUserDetails.First();\n        var orgSSOConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgUser.OrganizationId);\n        if (orgSSOConfig == null)\n        {\n            throw new BadRequestException(\"Organization SSO configuration not found.\");\n        }\n        else if (orgSSOConfig.GetData().MemberDecryptionType != Enums.MemberDecryptionType.MasterPassword)\n        {\n            throw new BadRequestException(\"Organization SSO Member Decryption Type is not Master Password.\");\n        }\n\n        var result = await _userService.UpdatePasswordHash(user, newMasterPassword);\n        if (!result.Succeeded)\n        {\n            return result;\n        }\n\n        user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;\n        user.ForcePasswordReset = false;\n        user.Key = key;\n        user.MasterPasswordHint = hint;\n\n        await _userRepository.ReplaceAsync(user);\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_TdeOffboardingPasswordSet);\n        await _pushService.PushLogOutAsync(user.Id);\n\n        return IdentityResult.Success;\n    }\n\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TwoFactorAuth/ICompleteTwoFactorWebAuthnRegistrationCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.UserFeatures.TwoFactorAuth;\n\npublic interface ICompleteTwoFactorWebAuthnRegistrationCommand\n{\n    /// <summary>\n    /// Enshrines WebAuthn 2FA credential registration after a successful challenge.\n    /// </summary>\n    /// <param name=\"user\">The current user.</param>\n    /// <param name=\"id\">ID for the Key credential to complete.</param>\n    /// <param name=\"name\">Name for the Key credential to complete.</param>\n    /// <param name=\"attestationResponse\">WebAuthn attestation response.</param>\n    /// <returns>Whether persisting the credential was successful.</returns>\n    Task<bool> CompleteTwoFactorWebAuthnRegistrationAsync(User user, int id, string name,\n        AuthenticatorAttestationRawResponse attestationResponse);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TwoFactorAuth/IDeleteTwoFactorWebAuthnCredentialCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.Auth.UserFeatures.TwoFactorAuth;\n\npublic interface IDeleteTwoFactorWebAuthnCredentialCommand\n{\n    /// <summary>\n    /// Deletes a single Two-factor WebAuthn credential by ID (\"Key{id}\").\n    /// </summary>\n    /// <param name=\"user\">The current user.</param>\n    /// <param name=\"id\">ID of the credential to delete (\"Key{id}\").</param>\n    /// <returns>Whether deletion was successful.</returns>\n    /// <remarks>Will not delete the last registered credential for a user. To delete the last (or single)\n    /// registered credential, use <see cref=\"IUserService.DisableTwoFactorProviderAsync\"/></remarks>\n    Task<bool> DeleteTwoFactorWebAuthnCredentialAsync(User user, int id);\n}\n\n\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TwoFactorAuth/IStartTwoFactorWebAuthnRegistrationCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.UserFeatures.TwoFactorAuth;\n\npublic interface IStartTwoFactorWebAuthnRegistrationCommand\n{\n    /// <summary>\n    /// Initiates WebAuthn 2FA credential registration and generates a challenge for adding a new security key.\n    /// </summary>\n    /// <param name=\"user\">The current user.</param>\n    /// <returns>Options for creating a new WebAuthn 2FA credential</returns>\n    /// <exception cref=\"BadRequestException\">Maximum allowed number of credentials already registered.</exception>\n    Task<CredentialCreateOptions> StartTwoFactorWebAuthnRegistrationAsync(User user);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TwoFactorAuth/ITwoFactorIsEnabledQuery.cs",
    "content": "﻿using Bit.Core.Auth.Models;\n\nnamespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\n\n\npublic interface ITwoFactorIsEnabledQuery\n{\n    /// <summary>\n    /// Returns a list of user IDs and whether two factor is enabled for each user.\n    /// </summary>\n    /// <param name=\"userIds\">The list of user IDs to check.</param>\n    Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds);\n    /// <summary>\n    /// Returns a list of users and whether two factor is enabled for each user.\n    /// </summary>\n    /// <param name=\"users\">The list of users to check.</param>\n    /// <typeparam name=\"T\">The type of user in the list. Must implement <see cref=\"ITwoFactorProvidersUser\"/>.</typeparam>\n    Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser;\n    /// <summary>\n    /// Returns whether two factor is enabled for the user. A user is able to have a TwoFactorProvider that is enabled but requires Premium.\n    /// If the user does not have premium then the TwoFactorProvider is considered _not_ enabled.\n    /// </summary>\n    /// <param name=\"user\">The user to check.</param>\n    Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/CompleteTwoFactorWebAuthnRegistrationCommand.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Fido2NetLib;\nusing Fido2NetLib.Objects;\n\nnamespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;\n\npublic class CompleteTwoFactorWebAuthnRegistrationCommand : ICompleteTwoFactorWebAuthnRegistrationCommand\n{\n    private readonly IFido2 _fido2;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;\n    private readonly IUserService _userService;\n\n    public CompleteTwoFactorWebAuthnRegistrationCommand(IFido2 fido2,\n        IGlobalSettings globalSettings,\n        IHasPremiumAccessQuery hasPremiumAccessQuery,\n        IUserService userService)\n    {\n        _fido2 = fido2;\n        _globalSettings = globalSettings;\n        _hasPremiumAccessQuery = hasPremiumAccessQuery;\n        _userService = userService;\n    }\n\n    public async Task<bool> CompleteTwoFactorWebAuthnRegistrationAsync(User user, int id, string name,\n        AuthenticatorAttestationRawResponse attestationResponse)\n    {\n        var keyId = $\"Key{id}\";\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);\n        if (provider?.MetaData is null || !provider.MetaData.TryGetValue(\"pending\", out var pendingValue))\n        {\n            return false;\n        }\n\n        // Persistence-time validation for comprehensive enforcement. There is also boundary validation for best-possible UX.\n        var maximumAllowedCredentialCount = await _hasPremiumAccessQuery.HasPremiumAccessAsync(user.Id)\n            ? _globalSettings.WebAuthn.PremiumMaximumAllowedCredentials\n            : _globalSettings.WebAuthn.NonPremiumMaximumAllowedCredentials;\n        // Count only saved credentials (\"Key{id}\") toward the limit.\n        if (provider.MetaData.Count(k => k.Key.StartsWith(\"Key\")) >=\n            maximumAllowedCredentialCount)\n        {\n            throw new BadRequestException(\"Maximum allowed WebAuthn credential count exceeded.\");\n        }\n\n        var options = CredentialCreateOptions.FromJson((string)pendingValue);\n\n        // Callback to ensure credential ID is unique. Always return true since we don't care if another\n        // account uses the same 2FA key.\n        IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) => Task.FromResult(true);\n\n        var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);\n        if (success.Result == null)\n        {\n            throw new BadRequestException(\"WebAuthn credential creation failed.\");\n        }\n\n        provider.MetaData.Remove(\"pending\");\n        provider.MetaData[keyId] = new TwoFactorProvider.WebAuthnData\n        {\n            Name = name,\n            Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId),\n            PublicKey = success.Result.PublicKey,\n            UserHandle = success.Result.User.Id,\n            SignatureCounter = success.Result.Counter,\n            CredType = success.Result.CredType,\n            RegDate = DateTime.Now,\n            AaGuid = success.Result.Aaguid\n        };\n\n        var providers = user.GetTwoFactorProviders();\n        if (providers == null)\n        {\n            throw new BadRequestException(\"No two-factor provider found.\");\n        }\n        providers[TwoFactorProviderType.WebAuthn] = provider;\n        user.SetTwoFactorProviders(providers);\n        await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/DeleteTwoFactorWebAuthnCredentialCommand.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;\n\npublic class DeleteTwoFactorWebAuthnCredentialCommand : IDeleteTwoFactorWebAuthnCredentialCommand\n{\n    private readonly IUserService _userService;\n\n    public DeleteTwoFactorWebAuthnCredentialCommand(IUserService userService)\n    {\n        _userService = userService;\n    }\n    public async Task<bool> DeleteTwoFactorWebAuthnCredentialAsync(User user, int id)\n    {\n        var providers = user.GetTwoFactorProviders();\n        if (providers == null)\n        {\n            return false;\n        }\n\n        var keyName = $\"Key{id}\";\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);\n        if (provider?.MetaData == null || !provider.MetaData.ContainsKey(keyName))\n        {\n            return false;\n        }\n\n        // Do not delete the last registered key credential.\n        // This prevents accidental account lockout (factor enabled, no credentials registered).\n        // To remove the last (or single) registered credential, disable the WebAuthn 2fa provider.\n        if (provider.MetaData.Count(k => k.Key.StartsWith(\"Key\")) < 2)\n        {\n            return false;\n        }\n\n        provider.MetaData.Remove(keyName);\n        providers[TwoFactorProviderType.WebAuthn] = provider;\n        user.SetTwoFactorProviders(providers);\n        await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/StartTwoFactorWebAuthnRegistrationCommand.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Fido2NetLib;\nusing Fido2NetLib.Objects;\n\nnamespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;\n\npublic class StartTwoFactorWebAuthnRegistrationCommand : IStartTwoFactorWebAuthnRegistrationCommand\n{\n    private readonly IFido2 _fido2;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;\n    private readonly IUserService _userService;\n\n    public StartTwoFactorWebAuthnRegistrationCommand(\n        IFido2 fido2,\n        IGlobalSettings globalSettings,\n        IHasPremiumAccessQuery hasPremiumAccessQuery,\n        IUserService userService)\n    {\n        _fido2 = fido2;\n        _globalSettings = globalSettings;\n        _hasPremiumAccessQuery = hasPremiumAccessQuery;\n        _userService = userService;\n    }\n\n    public async Task<CredentialCreateOptions> StartTwoFactorWebAuthnRegistrationAsync(User user)\n    {\n        var providers = user.GetTwoFactorProviders() ?? new Dictionary<TwoFactorProviderType, TwoFactorProvider>();\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn) ??\n                       new TwoFactorProvider { Enabled = false };\n        provider.MetaData ??= new Dictionary<string, object>();\n\n        // Boundary validation to provide a better UX. There is also second-level enforcement at persistence time.\n        var userHasPremiumAccess = await _hasPremiumAccessQuery.HasPremiumAccessAsync(user.Id);\n        var maximumAllowedCredentialCount = userHasPremiumAccess\n            ? _globalSettings.WebAuthn.PremiumMaximumAllowedCredentials\n            : _globalSettings.WebAuthn.NonPremiumMaximumAllowedCredentials;\n\n        // Count only saved credentials (\"Key{id}\") toward the limit.\n        if (provider.MetaData.Count(k => k.Key.StartsWith(\"Key\")) >=\n            maximumAllowedCredentialCount)\n        {\n            throw new BadRequestException(\"Maximum allowed WebAuthn credential count exceeded.\");\n        }\n\n        var fidoUser = new Fido2User { DisplayName = user.Name, Name = user.Email, Id = user.Id.ToByteArray(), };\n\n        var excludeCredentials = provider.MetaData\n            .Where(k => k.Key.StartsWith(\"Key\"))\n            .Select(k => new TwoFactorProvider.WebAuthnData((dynamic)k.Value).Descriptor)\n            .ToList();\n\n        var authenticatorSelection = new AuthenticatorSelection\n        {\n            AuthenticatorAttachment = null,\n            RequireResidentKey = false,\n            UserVerification = UserVerificationRequirement.Discouraged\n        };\n        var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, authenticatorSelection,\n            AttestationConveyancePreference.None);\n\n        provider.MetaData[\"pending\"] = options.ToJson();\n        providers[TwoFactorProviderType.WebAuthn] = provider;\n        user.SetTwoFactorProviders(providers);\n        await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false);\n\n        return options;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/TwoFactorIsEnabledQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;\n\npublic class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery\n{\n    private readonly IUserRepository _userRepository;\n    private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;\n\n    public TwoFactorIsEnabledQuery(\n        IUserRepository userRepository,\n        IHasPremiumAccessQuery hasPremiumAccessQuery)\n    {\n        _userRepository = userRepository;\n        _hasPremiumAccessQuery = hasPremiumAccessQuery;\n    }\n\n    public async Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds)\n    {\n        var result = new List<(Guid userId, bool hasTwoFactor)>();\n        if (userIds == null || !userIds.Any())\n        {\n            return result;\n        }\n\n        var users = await _userRepository.GetManyAsync([.. userIds]);\n\n        // Get enabled providers for each user\n        var usersTwoFactorProvidersMap = users.ToDictionary(u => u.Id, GetEnabledTwoFactorProviders);\n\n        // Bulk fetch premium status only for users who need it (those with only premium providers)\n        var userIdsNeedingPremium = usersTwoFactorProvidersMap\n            .Where(kvp => kvp.Value.Any() && kvp.Value.All(TwoFactorProvider.RequiresPremium))\n            .Select(kvp => kvp.Key)\n            .ToList();\n\n        var premiumStatusMap = userIdsNeedingPremium.Count > 0\n            ? await _hasPremiumAccessQuery.HasPremiumAccessAsync(userIdsNeedingPremium)\n            : new Dictionary<Guid, bool>();\n\n        foreach (var user in users)\n        {\n            var userTwoFactorProviders = usersTwoFactorProvidersMap[user.Id];\n\n            if (!userTwoFactorProviders.Any())\n            {\n                result.Add((user.Id, false));\n                continue;\n            }\n\n            // User has providers. If they're in the premium check map, verify premium status\n            var twoFactorIsEnabled = !premiumStatusMap.TryGetValue(user.Id, out var hasPremium) || hasPremium;\n            result.Add((user.Id, twoFactorIsEnabled));\n        }\n\n        return result;\n    }\n\n    public async Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser\n    {\n        var userIds = users\n            .Select(u => u.GetUserId())\n            .Where(u => u.HasValue)\n            .Select(u => u.Value)\n            .ToList();\n\n        var twoFactorResults = await TwoFactorIsEnabledAsync(userIds);\n\n        var result = new List<(T user, bool twoFactorIsEnabled)>();\n\n        foreach (var user in users)\n        {\n            var userId = user.GetUserId();\n            if (userId.HasValue)\n            {\n                var hasTwoFactor = twoFactorResults.FirstOrDefault(res => res.userId == userId.Value).twoFactorIsEnabled;\n                result.Add((user, hasTwoFactor));\n            }\n            else\n            {\n                result.Add((user, false));\n            }\n        }\n\n        return result;\n    }\n\n    public async Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user)\n    {\n        var userId = user.GetUserId();\n        if (!userId.HasValue)\n        {\n            return false;\n        }\n\n        var userEntity = user as User ?? await _userRepository.GetByIdAsync(userId.Value);\n        if (userEntity == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var enabledProviders = GetEnabledTwoFactorProviders(userEntity);\n        if (!enabledProviders.Any())\n        {\n            return false;\n        }\n\n        // If all providers require premium, check if user has premium access\n        if (enabledProviders.All(TwoFactorProvider.RequiresPremium))\n        {\n            return await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId.Value);\n        }\n\n        // User has at least one non-premium provider\n        return true;\n    }\n\n    /// <summary>\n    /// Gets all enabled two-factor provider types for a user.\n    /// </summary>\n    /// <param name=\"user\">user with two factor providers</param>\n    /// <returns>list of enabled provider types</returns>\n    private static IList<TwoFactorProviderType> GetEnabledTwoFactorProviders(User user)\n    {\n        var providers = user.GetTwoFactorProviders();\n\n        if (providers == null || providers.Count == 0)\n        {\n            return Array.Empty<TwoFactorProviderType>();\n        }\n\n        // TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into.\n        return (from provider in providers\n                where provider.Value?.Enabled ?? false\n                select provider.Key).ToList();\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ISetInitialMasterPasswordCommand.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;\n\n/// <summary>\n/// <para>Manages the setting of the initial master password for a <see cref=\"User\"/> in an organization.</para>\n/// <para>In organizations configured with Single Sign-On (SSO) and master password decryption:\n/// just in time (JIT) provisioned users logging in via SSO are required to set a master password.</para>\n/// </summary>\npublic interface ISetInitialMasterPasswordCommand\n{\n    /// <summary>\n    /// Sets the initial master password and account keys for the specified user.\n    /// </summary>\n    /// <param name=\"user\">User to set the master password for</param>\n    /// <param name=\"masterPasswordDataModel\">Initial master password setup data</param>\n    /// <returns>A task that completes when the operation succeeds</returns>\n    /// <exception cref=\"BadRequestException\">\n    /// Thrown if the user's master password is already set, the organization is not found,\n    /// the user is not a member of the organization, or the account keys are missing.\n    /// </exception>\n    public Task SetInitialMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ISetInitialMasterPasswordCommandV1.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;\n\n/// <summary>\n/// <para>Manages the setting of the initial master password for a <see cref=\"User\"/> in an organization.</para>\n/// <para>This class is primarily invoked in two scenarios:</para>\n/// <para>1) In organizations configured with Single Sign-On (SSO) and master password decryption:\n/// just in time (JIT) provisioned users logging in via SSO are required to set a master password.</para>\n/// <para>2) In organizations configured with SSO and trusted devices decryption:\n/// Users who are upgraded to have admin account recovery permissions must set a master password\n/// to ensure their ability to reset other users' accounts.</para>\n/// </summary>\n// TODO removed with https://bitwarden.atlassian.net/browse/PM-27327\n[Obsolete(\"Use ISetInitialMasterPasswordCommand instead\")]\npublic interface ISetInitialMasterPasswordCommandV1\n{\n    public Task<IdentityResult> SetInitialMasterPasswordAsync(User user, string masterPassword, string key,\n        string orgSsoIdentifier);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/UserMasterPassword/Interfaces/ITdeSetPasswordCommand.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;\n\n/// <summary>\n/// <para>Manages the setting of the master password for a TDE <see cref=\"User\"/> in an organization.</para>\n/// <para>In organizations configured with SSO and trusted devices decryption:\n/// Users who are upgraded to have admin account recovery permissions must set a master password\n/// to ensure their ability to reset other users' accounts.</para>\n/// </summary>\npublic interface ITdeSetPasswordCommand\n{\n    /// <summary>\n    /// Sets the master password for the specified TDE user.\n    /// </summary>\n    /// <param name=\"user\">User to set the master password for</param>\n    /// <param name=\"masterPasswordDataModel\">Master password setup data</param>\n    /// <returns>A task that completes when the operation succeeds</returns>\n    /// <exception cref=\"BadRequestException\">\n    /// Thrown if the user's master password is already set, the organization is not found,\n    /// the user is not a member of the organization, or the user is a TDE user without account keys set.\n    /// </exception>\n    Task SetMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommand.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Auth.UserFeatures.UserMasterPassword;\n\npublic class SetInitialMasterPasswordCommand : ISetInitialMasterPasswordCommand\n{\n    private readonly IUserService _userService;\n    private readonly IUserRepository _userRepository;\n    private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IPasswordHasher<User> _passwordHasher;\n    private readonly IEventService _eventService;\n\n    public SetInitialMasterPasswordCommand(IUserService userService, IUserRepository userRepository,\n        IAcceptOrgUserCommand acceptOrgUserCommand, IOrganizationUserRepository organizationUserRepository,\n        IOrganizationRepository organizationRepository, IPasswordHasher<User> passwordHasher,\n        IEventService eventService)\n    {\n        _userService = userService;\n        _userRepository = userRepository;\n        _acceptOrgUserCommand = acceptOrgUserCommand;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationRepository = organizationRepository;\n        _passwordHasher = passwordHasher;\n        _eventService = eventService;\n    }\n\n    public async Task SetInitialMasterPasswordAsync(User user,\n        SetInitialMasterPasswordDataModel masterPasswordDataModel)\n    {\n        if (user.Key != null)\n        {\n            throw new BadRequestException(\"User already has a master password set.\");\n        }\n\n        if (masterPasswordDataModel.AccountKeys == null)\n        {\n            throw new BadRequestException(\"Account keys are required.\");\n        }\n\n        // Prevent a de-synced salt value from creating an un-decryptable unlock method\n        masterPasswordDataModel.MasterPasswordAuthentication.ValidateSaltUnchangedForUser(user);\n        masterPasswordDataModel.MasterPasswordUnlock.ValidateSaltUnchangedForUser(user);\n\n        var org = await _organizationRepository.GetByIdentifierAsync(masterPasswordDataModel.OrgSsoIdentifier);\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization SSO identifier is invalid.\");\n        }\n\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);\n        if (orgUser == null)\n        {\n            throw new BadRequestException(\"User not found within organization.\");\n        }\n\n        // Hash the provided user master password authentication hash on the server side\n        var serverSideHashedMasterPasswordAuthenticationHash = _passwordHasher.HashPassword(user,\n            masterPasswordDataModel.MasterPasswordAuthentication.MasterPasswordAuthenticationHash);\n\n        var setMasterPasswordTask = _userRepository.SetMasterPassword(user.Id,\n            masterPasswordDataModel.MasterPasswordUnlock, serverSideHashedMasterPasswordAuthenticationHash,\n            masterPasswordDataModel.MasterPasswordHint);\n        await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, masterPasswordDataModel.AccountKeys,\n            [setMasterPasswordTask]);\n\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);\n\n        await _acceptOrgUserCommand.AcceptOrgUserAsync(orgUser, user, _userService);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommandV1.cs",
    "content": "﻿using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Auth.UserFeatures.UserMasterPassword;\n\npublic class SetInitialMasterPasswordCommandV1 : ISetInitialMasterPasswordCommandV1\n{\n    private readonly ILogger<SetInitialMasterPasswordCommandV1> _logger;\n    private readonly IdentityErrorDescriber _identityErrorDescriber;\n    private readonly IUserService _userService;\n    private readonly IUserRepository _userRepository;\n    private readonly IEventService _eventService;\n    private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n\n\n    public SetInitialMasterPasswordCommandV1(\n        ILogger<SetInitialMasterPasswordCommandV1> logger,\n        IdentityErrorDescriber identityErrorDescriber,\n        IUserService userService,\n        IUserRepository userRepository,\n        IEventService eventService,\n        IAcceptOrgUserCommand acceptOrgUserCommand,\n        IOrganizationUserRepository organizationUserRepository,\n        IOrganizationRepository organizationRepository)\n    {\n        _logger = logger;\n        _identityErrorDescriber = identityErrorDescriber;\n        _userService = userService;\n        _userRepository = userRepository;\n        _eventService = eventService;\n        _acceptOrgUserCommand = acceptOrgUserCommand;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationRepository = organizationRepository;\n    }\n\n    public async Task<IdentityResult> SetInitialMasterPasswordAsync(User user, string masterPassword, string key,\n        string orgSsoIdentifier)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        if (!string.IsNullOrWhiteSpace(user.MasterPassword))\n        {\n            _logger.LogWarning(\"Change password failed for user {userId} - already has password.\", user.Id);\n            return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());\n        }\n\n        var result = await _userService.UpdatePasswordHash(user, masterPassword, validatePassword: true, refreshStamp: false);\n        if (!result.Succeeded)\n        {\n            return result;\n        }\n\n        user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;\n        user.Key = key;\n\n        await _userRepository.ReplaceAsync(user);\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);\n\n\n        if (string.IsNullOrWhiteSpace(orgSsoIdentifier))\n        {\n            throw new BadRequestException(\"Organization SSO Identifier required.\");\n        }\n\n        var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier);\n\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization invalid.\");\n        }\n\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);\n\n        if (orgUser == null)\n        {\n            throw new BadRequestException(\"User not found within organization.\");\n        }\n\n        // TDE users who go from a user without admin acct recovery permission to having it will be\n        // required to set a MP for the first time and we don't want to re-execute the accept logic\n        // as they are already confirmed.\n        // TLDR: only accept post SSO user if they are invited\n        if (orgUser.Status == OrganizationUserStatusType.Invited)\n        {\n            await _acceptOrgUserCommand.AcceptOrgUserAsync(orgUser, user, _userService);\n        }\n\n        return IdentityResult.Success;\n    }\n\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/UserMasterPassword/TdeSetPasswordCommand.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Auth.UserFeatures.UserMasterPassword;\n\npublic class TdeSetPasswordCommand : ITdeSetPasswordCommand\n{\n    private readonly IUserRepository _userRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IPasswordHasher<User> _passwordHasher;\n    private readonly IEventService _eventService;\n\n    public TdeSetPasswordCommand(IUserRepository userRepository,\n        IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository,\n        IPasswordHasher<User> passwordHasher, IEventService eventService)\n    {\n        _userRepository = userRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationRepository = organizationRepository;\n        _passwordHasher = passwordHasher;\n        _eventService = eventService;\n    }\n\n    public async Task SetMasterPasswordAsync(User user, SetInitialMasterPasswordDataModel masterPasswordDataModel)\n    {\n        if (user.Key != null)\n        {\n            throw new BadRequestException(\"User already has a master password set.\");\n        }\n\n        if (user.PublicKey == null || user.PrivateKey == null)\n        {\n            throw new BadRequestException(\"TDE user account keys must be set before setting initial master password.\");\n        }\n\n        // Prevent a de-synced salt value from creating an un-decryptable unlock method\n        masterPasswordDataModel.MasterPasswordAuthentication.ValidateSaltUnchangedForUser(user);\n        masterPasswordDataModel.MasterPasswordUnlock.ValidateSaltUnchangedForUser(user);\n\n        var org = await _organizationRepository.GetByIdentifierAsync(masterPasswordDataModel.OrgSsoIdentifier);\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization SSO identifier is invalid.\");\n        }\n\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);\n        if (orgUser == null)\n        {\n            throw new BadRequestException(\"User not found within organization.\");\n        }\n\n        // Hash the provided user master password authentication hash on the server side\n        var serverSideHashedMasterPasswordAuthenticationHash = _passwordHasher.HashPassword(user,\n            masterPasswordDataModel.MasterPasswordAuthentication.MasterPasswordAuthenticationHash);\n\n        var setMasterPasswordTask = _userRepository.SetMasterPassword(user.Id,\n            masterPasswordDataModel.MasterPasswordUnlock, serverSideHashedMasterPasswordAuthenticationHash,\n            masterPasswordDataModel.MasterPasswordHint);\n        await _userRepository.UpdateUserDataAsync([setMasterPasswordTask]);\n\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Auth.Sso;\nusing Bit.Core.Auth.UserFeatures.DeviceTrust;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Commands;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Auth.UserFeatures.Registration;\nusing Bit.Core.Auth.UserFeatures.Registration.Implementations;\nusing Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;\nusing Bit.Core.Auth.UserFeatures.WebAuthnLogin;\nusing Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.KeyManagement.UserKey.Implementations;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Auth.UserFeatures;\n\npublic static class UserServiceCollectionExtensions\n{\n    public static void AddUserServices(this IServiceCollection services, IGlobalSettings globalSettings)\n    {\n        services.AddScoped<IUserService, UserService>();\n        services.AddDeviceTrustCommands();\n        services.AddEmergencyAccessCommands();\n        services.AddUserPasswordCommands();\n        services.AddUserRegistrationCommands();\n        services.AddWebAuthnLoginCommands();\n        services.AddTdeOffboardingPasswordCommands();\n        services.AddTwoFactorCommandsQueries();\n        services.AddSsoQueries();\n    }\n\n    public static void AddDeviceTrustCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IUntrustDevicesCommand, UntrustDevicesCommand>();\n    }\n\n    private static void AddEmergencyAccessCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IDeleteEmergencyAccessCommand, DeleteEmergencyAccessCommand>();\n    }\n\n    public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)\n    {\n        services.AddScoped<IRotateUserAccountKeysCommand, RotateUserAccountKeysCommand>();\n    }\n\n    private static void AddUserPasswordCommands(this IServiceCollection services)\n    {\n        services.AddScoped<ISetInitialMasterPasswordCommand, SetInitialMasterPasswordCommand>();\n        services.AddScoped<ISetInitialMasterPasswordCommandV1, SetInitialMasterPasswordCommandV1>();\n        services.AddScoped<ITdeSetPasswordCommand, TdeSetPasswordCommand>();\n    }\n\n    private static void AddTdeOffboardingPasswordCommands(this IServiceCollection services)\n    {\n        services.AddScoped<ITdeOffboardingPasswordCommand, TdeOffboardingPasswordCommand>();\n    }\n\n    private static void AddUserRegistrationCommands(this IServiceCollection services)\n    {\n        services.AddScoped<ISendVerificationEmailForRegistrationCommand, SendVerificationEmailForRegistrationCommand>();\n        services.AddScoped<IRegisterUserCommand, RegisterUserCommand>();\n    }\n\n    private static void AddWebAuthnLoginCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IGetWebAuthnLoginCredentialCreateOptionsCommand, GetWebAuthnLoginCredentialCreateOptionsCommand>();\n        services.AddScoped<ICreateWebAuthnLoginCredentialCommand, CreateWebAuthnLoginCredentialCommand>();\n        services.AddScoped<IGetWebAuthnLoginCredentialAssertionOptionsCommand, GetWebAuthnLoginCredentialAssertionOptionsCommand>();\n        services.AddScoped<IAssertWebAuthnLoginCredentialCommand, AssertWebAuthnLoginCredentialCommand>();\n    }\n\n    private static void AddTwoFactorCommandsQueries(this IServiceCollection services)\n    {\n        services\n            .AddScoped<ICompleteTwoFactorWebAuthnRegistrationCommand, CompleteTwoFactorWebAuthnRegistrationCommand>();\n        services\n            .AddScoped<IStartTwoFactorWebAuthnRegistrationCommand,\n                StartTwoFactorWebAuthnRegistrationCommand>();\n        services.AddScoped<IDeleteTwoFactorWebAuthnCredentialCommand, DeleteTwoFactorWebAuthnCredentialCommand>();\n        services.AddScoped<ITwoFactorIsEnabledQuery, TwoFactorIsEnabledQuery>();\n    }\n\n    private static void AddSsoQueries(this IServiceCollection services)\n    {\n        services.AddScoped<IUserSsoOrganizationIdentifierQuery, UserSsoOrganizationIdentifierQuery>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/WebAuthnLogin/IAssertWebAuthnLoginCredentialCommand.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Entities;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;\n\npublic interface IAssertWebAuthnLoginCredentialCommand\n{\n    public Task<(User, WebAuthnCredential)> AssertWebAuthnLoginCredential(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/WebAuthnLogin/ICreateWebAuthnLoginCredentialCommand.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Entities;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;\n\npublic interface ICreateWebAuthnLoginCredentialCommand\n{\n    public Task<WebAuthnCredential?> CreateWebAuthnLoginCredentialAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string? encryptedUserKey = null, string? encryptedPublicKey = null, string? encryptedPrivateKey = null);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/WebAuthnLogin/IGetWebAuthnLoginCredentialAssertionOptionsCommand.cs",
    "content": "﻿using Fido2NetLib;\n\nnamespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;\n\npublic interface IGetWebAuthnLoginCredentialAssertionOptionsCommand\n{\n    public AssertionOptions GetWebAuthnLoginCredentialAssertionOptions();\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/WebAuthnLogin/IGetWebAuthnLoginCredentialCreateOptionsCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.UserFeatures.WebAuthnLogin;\n\n/// <summary>\n/// Get the options required to create a Passkey for login.\n/// </summary>\npublic interface IGetWebAuthnLoginCredentialCreateOptionsCommand\n{\n    public Task<CredentialCreateOptions> GetWebAuthnLoginCredentialCreateOptionsAsync(User user);\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/AssertWebAuthnLoginCredentialCommand.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;\n\ninternal class AssertWebAuthnLoginCredentialCommand : IAssertWebAuthnLoginCredentialCommand\n{\n    private readonly IFido2 _fido2;\n    private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;\n    private readonly IUserRepository _userRepository;\n\n    public AssertWebAuthnLoginCredentialCommand(IFido2 fido2, IWebAuthnCredentialRepository webAuthnCredentialRepository, IUserRepository userRepository)\n    {\n        _fido2 = fido2;\n        _webAuthnCredentialRepository = webAuthnCredentialRepository;\n        _userRepository = userRepository;\n    }\n\n    public async Task<(User, WebAuthnCredential)> AssertWebAuthnLoginCredential(AssertionOptions options, AuthenticatorAssertionRawResponse assertionResponse)\n    {\n        if (!GuidUtilities.TryParseBytes(assertionResponse.Response.UserHandle, out var userId))\n        {\n            throw new BadRequestException(\"Invalid credential.\");\n        }\n\n        var user = await _userRepository.GetByIdAsync(userId);\n        if (user == null)\n        {\n            throw new BadRequestException(\"Invalid credential.\");\n        }\n\n        var userCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);\n        var assertedCredentialId = CoreHelpers.Base64UrlEncode(assertionResponse.Id);\n        var credential = userCredentials.FirstOrDefault(c => c.CredentialId == assertedCredentialId);\n        if (credential == null)\n        {\n            throw new BadRequestException(\"Invalid credential.\");\n        }\n\n        // Always return true, since we've already filtered the credentials after user id\n        IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true);\n        var credentialPublicKey = CoreHelpers.Base64UrlDecode(credential.PublicKey);\n        var assertionVerificationResult = await _fido2.MakeAssertionAsync(\n            assertionResponse, options, credentialPublicKey, (uint)credential.Counter, callback);\n\n        // Update SignatureCounter\n        credential.Counter = (int)assertionVerificationResult.Counter;\n        await _webAuthnCredentialRepository.ReplaceAsync(credential);\n\n        if (assertionVerificationResult.Status != \"ok\")\n        {\n            throw new BadRequestException(\"Invalid credential.\");\n        }\n\n        return (user, credential);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/CreateWebAuthnLoginCredentialCommand.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\nusing Fido2NetLib;\n\nnamespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;\n\ninternal class CreateWebAuthnLoginCredentialCommand : ICreateWebAuthnLoginCredentialCommand\n{\n    public const int MaxCredentialsPerUser = 5;\n\n    private readonly IFido2 _fido2;\n    private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;\n\n    public CreateWebAuthnLoginCredentialCommand(IFido2 fido2, IWebAuthnCredentialRepository webAuthnCredentialRepository)\n    {\n        _fido2 = fido2;\n        _webAuthnCredentialRepository = webAuthnCredentialRepository;\n    }\n\n    public async Task<WebAuthnCredential?> CreateWebAuthnLoginCredentialAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string? encryptedUserKey = null, string? encryptedPublicKey = null, string? encryptedPrivateKey = null)\n    {\n        var existingCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);\n        if (existingCredentials.Count >= MaxCredentialsPerUser)\n        {\n            return null;\n        }\n\n        var existingCredentialIds = existingCredentials.Select(c => c.CredentialId);\n        IsCredentialIdUniqueToUserAsyncDelegate callback = (args, cancellationToken) => Task.FromResult(!existingCredentialIds.Contains(CoreHelpers.Base64UrlEncode(args.CredentialId)));\n\n        var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, callback);\n        if (success.Result == null)\n        {\n            return null;\n        }\n\n        var credential = new WebAuthnCredential\n        {\n            Name = name,\n            CredentialId = CoreHelpers.Base64UrlEncode(success.Result.CredentialId),\n            PublicKey = CoreHelpers.Base64UrlEncode(success.Result.PublicKey),\n            Type = success.Result.CredType,\n            AaGuid = success.Result.Aaguid,\n            Counter = (int)success.Result.Counter,\n            UserId = user.Id,\n            SupportsPrf = supportsPrf,\n            EncryptedUserKey = encryptedUserKey,\n            EncryptedPublicKey = encryptedPublicKey,\n            EncryptedPrivateKey = encryptedPrivateKey\n        };\n\n        await _webAuthnCredentialRepository.CreateAsync(credential);\n        return credential;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/GetWebAuthnLoginCredentialAssertionOptionsCommand.cs",
    "content": "﻿using Fido2NetLib;\nusing Fido2NetLib.Objects;\n\nnamespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;\n\ninternal class GetWebAuthnLoginCredentialAssertionOptionsCommand : IGetWebAuthnLoginCredentialAssertionOptionsCommand\n{\n    private readonly IFido2 _fido2;\n\n    public GetWebAuthnLoginCredentialAssertionOptionsCommand(IFido2 fido2)\n    {\n        _fido2 = fido2;\n    }\n\n    public AssertionOptions GetWebAuthnLoginCredentialAssertionOptions()\n    {\n        return _fido2.GetAssertionOptions(Enumerable.Empty<PublicKeyCredentialDescriptor>(), UserVerificationRequirement.Required);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/GetWebAuthnLoginCredentialCreateOptionsCommand.cs",
    "content": "﻿using Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\nusing Fido2NetLib;\nusing Fido2NetLib.Objects;\n\nnamespace Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;\n\ninternal class GetWebAuthnLoginCredentialCreateOptionsCommand : IGetWebAuthnLoginCredentialCreateOptionsCommand\n{\n    private readonly IFido2 _fido2;\n    private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;\n\n    public GetWebAuthnLoginCredentialCreateOptionsCommand(IFido2 fido2, IWebAuthnCredentialRepository webAuthnCredentialRepository)\n    {\n        _fido2 = fido2;\n        _webAuthnCredentialRepository = webAuthnCredentialRepository;\n    }\n\n    public async Task<CredentialCreateOptions> GetWebAuthnLoginCredentialCreateOptionsAsync(User user)\n    {\n        var fidoUser = new Fido2User\n        {\n            DisplayName = user.Name ?? \"\",\n            Name = user.Email,\n            Id = user.Id.ToByteArray(),\n        };\n\n        // Get existing keys to exclude\n        var existingKeys = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id);\n        var excludeCredentials = existingKeys\n            .Select(k => new PublicKeyCredentialDescriptor(CoreHelpers.Base64UrlDecode(k.CredentialId)))\n            .ToList();\n\n        var authenticatorSelection = new AuthenticatorSelection\n        {\n            AuthenticatorAttachment = null,\n            RequireResidentKey = true,\n            UserVerification = UserVerificationRequirement.Required\n        };\n\n        var extensions = new AuthenticationExtensionsClientInputs { };\n\n        var options = _fido2.RequestNewCredential(fidoUser, excludeCredentials, authenticatorSelection,\n            AttestationConveyancePreference.None, extensions);\n\n        return options;\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Utilities/DeviceExtensions.cs",
    "content": "﻿using Bit.Core.Entities;\n\n#nullable enable\n\nnamespace Bit.Core.Auth.Utilities;\n\npublic static class DeviceExtensions\n{\n    /// <summary>\n    /// Gets a boolean representing if the device has enough information on it to determine whether or not it is trusted.\n    /// </summary>\n    /// <remarks>\n    /// It is possible for a device to be un-trusted client side and not notify the server side. This should not be\n    /// the source of truth for whether a device is fully trusted and should just be considered that, to the server,\n    /// a device has the necessary information to be \"trusted\".\n    /// </remarks>\n    public static bool IsTrusted(this Device device)\n    {\n        return !string.IsNullOrEmpty(device.EncryptedUserKey) &&\n            !string.IsNullOrEmpty(device.EncryptedPublicKey) &&\n            !string.IsNullOrEmpty(device.EncryptedPrivateKey);\n    }\n}\n"
  },
  {
    "path": "src/Core/Auth/Utilities/GuidUtilities.cs",
    "content": "﻿namespace Bit.Core.Auth.Utilities;\n\npublic static class GuidUtilities\n{\n    public static bool TryParseBytes(ReadOnlySpan<byte> bytes, out Guid guid)\n    {\n        try\n        {\n            guid = new Guid(bytes);\n            return true;\n        }\n        catch\n        {\n            guid = Guid.Empty;\n            return false;\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/Core/Billing/BillingException.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Billing;\n\npublic class BillingException(\n    string response = null,\n    string message = null,\n    Exception innerException = null) : Exception(message, innerException)\n{\n    public const string DefaultMessage = \"Something went wrong with your request. Please contact support.\";\n\n    public string Response { get; } = response ?? DefaultMessage;\n}\n"
  },
  {
    "path": "src/Core/Billing/Commands/BaseBillingCommand.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Exceptions;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Commands;\n\nusing static StripeConstants;\n\npublic abstract class BaseBillingCommand<T>(\n    ILogger<T> logger)\n{\n    protected string CommandName => GetType().Name;\n\n    /// <summary>\n    /// Override this property to set a client-facing conflict response in the case a <see cref=\"ConflictException\"/> is thrown\n    /// during the command's execution.\n    /// </summary>\n    protected virtual Conflict? DefaultConflict => null;\n\n    /// <summary>\n    /// Executes the provided function within a predefined execution context, handling any exceptions that occur during the process.\n    /// </summary>\n    /// <typeparam name=\"TSuccess\">The type of the successful result expected from the provided function.</typeparam>\n    /// <param name=\"function\">A function that performs an operation and returns a <see cref=\"BillingCommandResult{TSuccess}\"/>.</param>\n    /// <returns>A task that represents the operation. The result provides a <see cref=\"BillingCommandResult{TSuccess}\"/> which may indicate success or an error outcome.</returns>\n    protected async Task<BillingCommandResult<TSuccess>> HandleAsync<TSuccess>(\n        Func<Task<BillingCommandResult<TSuccess>>> function)\n    {\n        try\n        {\n            return await function();\n        }\n        catch (StripeException stripeException) when (ErrorCodes.InputErrors().Contains(stripeException.StripeError.Code))\n        {\n            return stripeException.StripeError.Code switch\n            {\n                ErrorCodes.CustomerTaxLocationInvalid =>\n                    new BadRequest(\n                        \"Your location wasn't recognized. Please ensure your country and postal code are valid and try again.\"),\n\n                ErrorCodes.PaymentMethodMicroDepositVerificationAttemptsExceeded =>\n                    new BadRequest(\n                        \"You have exceeded the number of allowed verification attempts. Please contact support for assistance.\"),\n\n                ErrorCodes.PaymentMethodMicroDepositVerificationDescriptorCodeMismatch =>\n                    new BadRequest(\n                        \"The verification code you provided does not match the one sent to your bank account. Please try again.\"),\n\n                ErrorCodes.PaymentMethodMicroDepositVerificationTimeout =>\n                    new BadRequest(\n                        \"Your bank account was not verified within the required time period. Please contact support for assistance.\"),\n\n                ErrorCodes.TaxIdInvalid =>\n                    new BadRequest(\n                        \"The tax ID number you provided was invalid. Please try again or contact support for assistance.\"),\n\n                _ => new Unhandled(stripeException)\n            };\n        }\n        catch (ConflictException conflictException)\n        {\n            logger.LogError(\"{Command}: {Message}\", CommandName, conflictException.Message);\n            return DefaultConflict != null ?\n                DefaultConflict :\n                new Unhandled(conflictException);\n        }\n        catch (StripeException stripeException)\n        {\n            logger.LogError(stripeException,\n                \"{Command}: An error occurred while communicating with Stripe | Code = {Code}\", CommandName,\n                stripeException.StripeError.Code);\n            return new Unhandled(stripeException);\n        }\n        catch (Exception exception)\n        {\n            logger.LogError(exception, \"{Command}: An unknown error occurred during execution\", CommandName);\n            return new Unhandled(exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Commands/BillingCommandResult.cs",
    "content": "﻿using OneOf;\n\nnamespace Bit.Core.Billing.Commands;\n\npublic record BadRequest(string Response);\npublic record Conflict(string Response);\npublic record Unhandled(Exception? Exception = null, string Response = \"Something went wrong with your request. Please contact support for assistance.\");\n\n/// <summary>\n/// A <see cref=\"OneOf\"/> union type representing the result of a billing command.\n/// <remarks>\n/// Choices include:\n/// <list type=\"bullet\">\n/// <item><description><typeparamref name=\"T\"/>: Success</description></item>\n/// <item><description><see cref=\"BadRequest\"/>: Invalid input</description></item>\n/// <item><description><see cref=\"Conflict\"/>: A known, but unresolvable issue</description></item>\n/// <item><description><see cref=\"Unhandled\"/>: An unknown issue</description></item>\n/// </list>\n/// </remarks>\n/// </summary>\n/// <typeparam name=\"T\">The successful result type of the operation.</typeparam>\npublic class BillingCommandResult<T>(OneOf<T, BadRequest, Conflict, Unhandled> input)\n    : OneOfBase<T, BadRequest, Conflict, Unhandled>(input)\n{\n    public static implicit operator BillingCommandResult<T>(T output) => new(output);\n    public static implicit operator BillingCommandResult<T>(BadRequest badRequest) => new(badRequest);\n    public static implicit operator BillingCommandResult<T>(Conflict conflict) => new(conflict);\n    public static implicit operator BillingCommandResult<T>(Unhandled unhandled) => new(unhandled);\n\n    public bool Success => IsT0;\n\n    public BillingCommandResult<TResult> Map<TResult>(Func<T, TResult> f)\n        => Match(\n            value => new BillingCommandResult<TResult>(f(value)),\n            badRequest => new BillingCommandResult<TResult>(badRequest),\n            conflict => new BillingCommandResult<TResult>(conflict),\n            unhandled => new BillingCommandResult<TResult>(unhandled));\n\n    public Task TapAsync(Func<T, Task> f) => Match(\n        f,\n        _ => Task.CompletedTask,\n        _ => Task.CompletedTask,\n        _ => Task.CompletedTask);\n\n    public T GetValueOrThrow() => Match(\n        value => value,\n        badRequest => throw new BillingException(badRequest.Response),\n        conflict => throw new BillingException(message: conflict.Response),\n        unhandled => throw new BillingException(message: unhandled.Response, innerException: unhandled.Exception));\n}\n\npublic static class BillingCommandResultExtensions\n{\n    public static async Task<BillingCommandResult<TResult>> AndThenAsync<T, TResult>(\n        this Task<BillingCommandResult<T>> task, Func<T, Task<BillingCommandResult<TResult>>> binder)\n    {\n        var result = await task;\n        return await result.Match(\n            binder,\n            badRequest => Task.FromResult(new BillingCommandResult<TResult>(badRequest)),\n            conflict => Task.FromResult(new BillingCommandResult<TResult>(conflict)),\n            unhandled => Task.FromResult(new BillingCommandResult<TResult>(unhandled)));\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Constants/BitPayConstants.cs",
    "content": "﻿namespace Bit.Core.Billing.Constants;\n\npublic static class BitPayConstants\n{\n    public static class InvoiceStatuses\n    {\n        public const string Complete = \"complete\";\n    }\n\n    public static class PosDataKeys\n    {\n        public const string AccountCredit = \"accountCredit:1\";\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Constants/PlanConstants.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Constants;\n\npublic static class PlanConstants\n{\n    public static List<PlanType> EnterprisePlanTypes =>\n    [\n        PlanType.EnterpriseAnnually2019,\n        PlanType.EnterpriseAnnually2020,\n        PlanType.EnterpriseAnnually2023,\n        PlanType.EnterpriseAnnually,\n        PlanType.EnterpriseMonthly2019,\n        PlanType.EnterpriseMonthly2020,\n        PlanType.EnterpriseMonthly2023,\n        PlanType.EnterpriseMonthly\n    ];\n\n    public static List<PlanType> TeamsPlanTypes =>\n    [\n        PlanType.TeamsAnnually2019,\n        PlanType.TeamsAnnually2020,\n        PlanType.TeamsAnnually2023,\n        PlanType.TeamsAnnually,\n        PlanType.TeamsMonthly2019,\n        PlanType.TeamsMonthly2020,\n        PlanType.TeamsMonthly2023,\n        PlanType.TeamsMonthly\n    ];\n\n    public static List<PlanType> FamiliesPlanTypes =>\n    [\n        PlanType.FamiliesAnnually2019,\n        PlanType.FamiliesAnnually2025,\n        PlanType.FamiliesAnnually\n    ];\n}\n"
  },
  {
    "path": "src/Core/Billing/Constants/StripeConstants.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Constants;\n\npublic static class StripeConstants\n{\n    public static class AutomaticTaxStatus\n    {\n        public const string Failed = \"failed\";\n        public const string NotCollecting = \"not_collecting\";\n        public const string Supported = \"supported\";\n        public const string UnrecognizedLocation = \"unrecognized_location\";\n    }\n\n    public static class BillingReasons\n    {\n        public const string SubscriptionCreate = \"subscription_create\";\n        public const string SubscriptionCycle = \"subscription_cycle\";\n    }\n\n    public static class CollectionMethod\n    {\n        public const string ChargeAutomatically = \"charge_automatically\";\n        public const string SendInvoice = \"send_invoice\";\n    }\n\n    public static class CouponIDs\n    {\n        public const string LegacyMSPDiscount = \"msp-discount-35\";\n        public const string SecretsManagerStandalone = \"sm-standalone\";\n        public const string Milestone2SubscriptionDiscount = \"milestone-2c\";\n        public const string Milestone3SubscriptionDiscount = \"milestone-3\";\n\n        public static class MSPDiscounts\n        {\n            public const string Open = \"msp-open-discount\";\n            public const string Silver = \"msp-silver-discount\";\n            public const string Gold = \"msp-gold-discount\";\n        }\n    }\n\n    public static class CouponExpandablePropertyNames\n    {\n        public const string AppliesTo = \"applies_to\";\n    }\n\n    public static class ErrorCodes\n    {\n        public const string CustomerTaxLocationInvalid = \"customer_tax_location_invalid\";\n        public const string InvoiceUpcomingNone = \"invoice_upcoming_none\";\n        public const string PaymentMethodMicroDepositVerificationAttemptsExceeded = \"payment_method_microdeposit_verification_attempts_exceeded\";\n        public const string PaymentMethodMicroDepositVerificationDescriptorCodeMismatch = \"payment_method_microdeposit_verification_descriptor_code_mismatch\";\n        public const string PaymentMethodMicroDepositVerificationTimeout = \"payment_method_microdeposit_verification_timeout\";\n        public const string ResourceMissing = \"resource_missing\";\n        public const string TaxIdInvalid = \"tax_id_invalid\";\n\n        public static string[] InputErrors() =>\n        [\n            CustomerTaxLocationInvalid,\n            InvoiceUpcomingNone,\n            PaymentMethodMicroDepositVerificationAttemptsExceeded,\n            PaymentMethodMicroDepositVerificationDescriptorCodeMismatch,\n            PaymentMethodMicroDepositVerificationTimeout,\n            TaxIdInvalid\n        ];\n    }\n\n    public static class Intervals\n    {\n        public const string Month = \"month\";\n        public const string Year = \"year\";\n    }\n\n    public static class InvoiceStatus\n    {\n        public const string Draft = \"draft\";\n        public const string Open = \"open\";\n        public const string Paid = \"paid\";\n    }\n\n    public static class MetadataKeys\n    {\n        public const string BraintreeCustomerId = \"btCustomerId\";\n        public const string BraintreeTransactionId = \"btTransactionId\";\n        public const string InvoiceApproved = \"invoice_approved\";\n        public const string OrganizationId = \"organizationId\";\n        public const string PayPalTransactionId = \"btPayPalTransactionId\";\n        public const string ProviderId = \"providerId\";\n        public const string Region = \"region\";\n        public const string RetiredBraintreeCustomerId = \"btCustomerId_old\";\n        public const string UserId = \"userId\";\n        public const string StorageReconciled2025 = \"storage_reconciled_2025\";\n    }\n\n    public static class PaymentBehavior\n    {\n        public const string DefaultIncomplete = \"default_incomplete\";\n        public const string PendingIfIncomplete = \"pending_if_incomplete\";\n    }\n\n    public static class PaymentMethodTypes\n    {\n        public const string Card = \"card\";\n        public const string USBankAccount = \"us_bank_account\";\n    }\n\n    public static class Prices\n    {\n        public const string StoragePlanPersonal = \"personal-storage-gb-annually\";\n        public const string PremiumAnnually = \"premium-annually\";\n    }\n\n    public static class ProrationBehavior\n    {\n        public const string AlwaysInvoice = \"always_invoice\";\n        public const string CreateProrations = \"create_prorations\";\n        public const string None = \"none\";\n    }\n\n    public static class SubscriptionStatus\n    {\n        public const string Trialing = \"trialing\";\n        public const string Active = \"active\";\n        public const string Incomplete = \"incomplete\";\n        public const string IncompleteExpired = \"incomplete_expired\";\n        public const string PastDue = \"past_due\";\n        public const string Canceled = \"canceled\";\n        public const string Unpaid = \"unpaid\";\n        public const string Paused = \"paused\";\n    }\n\n    public static class TaxExempt\n    {\n        public const string Exempt = \"exempt\";\n        public const string None = \"none\";\n        public const string Reverse = \"reverse\";\n    }\n\n    public static class TaxIdType\n    {\n        public const string EUVAT = \"eu_vat\";\n        public const string SpanishNIF = \"es_cif\";\n    }\n\n    public static class TaxIdVerificationStatus\n    {\n        public const string Pending = \"pending\";\n        public const string Unavailable = \"unavailable\";\n        public const string Unverified = \"unverified\";\n        public const string Verified = \"verified\";\n    }\n\n    public static class TaxRegistrationStatus\n    {\n        public const string Active = \"active\";\n        public const string Expired = \"expired\";\n        public const string Scheduled = \"scheduled\";\n    }\n\n    public static class ValidateTaxLocationTiming\n    {\n        public const string Deferred = \"deferred\";\n        public const string Immediately = \"immediately\";\n    }\n\n    public static class MissingPaymentMethodBehaviorOptions\n    {\n        public const string CreateInvoice = \"create_invoice\";\n        public const string Cancel = \"cancel\";\n        public const string Pause = \"pause\";\n    }\n    /// <summary>\n    /// Product Ids in Stripe that are used to identify password manager products in subscriptions\n    /// These should be kept up to date with the products created in Stripe dashboard.\n    /// </summary>\n    public static class ProductIDs\n    {\n        public const string Premium = \"prod_BUqgYr48VzDuCg\";\n        public const string Families = \"prod_HgOroKDcpTzJgn\";\n\n        /// <summary>\n        /// Gets the product tier for a given Stripe product ID.\n        /// </summary>\n        /// <param name=\"productId\">The Stripe product ID.</param>\n        /// <returns>The corresponding <see cref=\"DiscountTierType\"/>, or <see langword=\"null\"/> if not found.</returns>\n        public static DiscountTierType? GetProductTier(string productId) => productId switch\n        {\n            Premium => DiscountTierType.Premium,\n            Families => DiscountTierType.Families,\n            _ => null\n        };\n    }\n\n\n}\n"
  },
  {
    "path": "src/Core/Billing/Enums/DiscountAudienceType.cs",
    "content": "﻿namespace Bit.Core.Billing.Enums;\n\n/// <summary>\n/// Defines the target audience for subscription discounts using an extensible strategy pattern.\n/// Each audience type maps to specific eligibility rules implemented via IDiscountAudienceFilter.\n/// </summary>\npublic enum DiscountAudienceType\n{\n    /// <summary>\n    /// Discount applies to all users regardless of subscription history.\n    /// This is the default value (0) when audience restrictions are not applied.\n    /// </summary>\n    AllUsers = 0,\n\n    /// <summary>\n    /// Discount applies to users who have never had a subscription before.\n    /// </summary>\n    UserHasNoPreviousSubscriptions = 1\n}\n"
  },
  {
    "path": "src/Core/Billing/Enums/DiscountTierType.cs",
    "content": "﻿namespace Bit.Core.Billing.Enums;\n\n/// <summary>\n/// Represents the product tiers that a subscription discount can target,\n/// including both personal premium and organization-level tiers.\n/// </summary>\npublic enum DiscountTierType : byte\n{\n    Premium = 0,\n    Families = 1\n}\n"
  },
  {
    "path": "src/Core/Billing/Enums/PlanCadenceType.cs",
    "content": "﻿using System.Runtime.Serialization;\n\nnamespace Bit.Core.Billing.Enums;\n\npublic enum PlanCadenceType\n{\n    [EnumMember(Value = \"annually\")]\n    Annually,\n    [EnumMember(Value = \"monthly\")]\n    Monthly\n}\n"
  },
  {
    "path": "src/Core/Billing/Enums/PlanType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Billing.Enums;\n\npublic enum PlanType : byte\n{\n    [Display(Name = \"Free\")]\n    Free = 0,\n    [Display(Name = \"Families 2019\")]\n    FamiliesAnnually2019 = 1,\n    [Display(Name = \"Teams (Monthly) 2019\")]\n    TeamsMonthly2019 = 2,\n    [Display(Name = \"Teams (Annually) 2019\")]\n    TeamsAnnually2019 = 3,\n    [Display(Name = \"Enterprise (Monthly) 2019\")]\n    EnterpriseMonthly2019 = 4,\n    [Display(Name = \"Enterprise (Annually) 2019\")]\n    EnterpriseAnnually2019 = 5,\n    [Display(Name = \"Custom\")]\n    Custom = 6,\n    [Display(Name = \"Families 2025\")]\n    FamiliesAnnually2025 = 7,\n    [Display(Name = \"Teams (Monthly) 2020\")]\n    TeamsMonthly2020 = 8,\n    [Display(Name = \"Teams (Annually) 2020\")]\n    TeamsAnnually2020 = 9,\n    [Display(Name = \"Enterprise (Monthly) 2020\")]\n    EnterpriseMonthly2020 = 10,\n    [Display(Name = \"Enterprise (Annually) 2020\")]\n    EnterpriseAnnually2020 = 11,\n    [Display(Name = \"Teams (Monthly) 2023\")]\n    TeamsMonthly2023 = 12,\n    [Display(Name = \"Teams (Annually) 2023\")]\n    TeamsAnnually2023 = 13,\n    [Display(Name = \"Enterprise (Monthly) 2023\")]\n    EnterpriseMonthly2023 = 14,\n    [Display(Name = \"Enterprise (Annually) 2023\")]\n    EnterpriseAnnually2023 = 15,\n    [Display(Name = \"Teams Starter 2023\")]\n    TeamsStarter2023 = 16,\n    [Display(Name = \"Teams (Monthly)\")]\n    TeamsMonthly = 17,\n    [Display(Name = \"Teams (Annually)\")]\n    TeamsAnnually = 18,\n    [Display(Name = \"Enterprise (Monthly)\")]\n    EnterpriseMonthly = 19,\n    [Display(Name = \"Enterprise (Annually)\")]\n    EnterpriseAnnually = 20,\n    [Display(Name = \"Teams Starter\")]\n    TeamsStarter = 21,\n    [Display(Name = \"Families\")]\n    FamiliesAnnually = 22,\n}\n"
  },
  {
    "path": "src/Core/Billing/Enums/ProductTierType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Billing.Enums;\n\npublic enum ProductTierType : byte\n{\n    [Display(Name = \"Free\")]\n    Free = 0,\n    [Display(Name = \"Families\")]\n    Families = 1,\n    [Display(Name = \"Teams\")]\n    Teams = 2,\n    [Display(Name = \"Enterprise\")]\n    Enterprise = 3,\n    [Display(Name = \"Teams Starter\")]\n    TeamsStarter = 4,\n}\n"
  },
  {
    "path": "src/Core/Billing/Enums/ProductType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Billing.Enums;\n\npublic enum ProductType\n{\n    [Display(Name = \"Password Manager\")]\n    PasswordManager = 0,\n    [Display(Name = \"Secrets Manager\")]\n    SecretsManager = 1,\n}\n"
  },
  {
    "path": "src/Core/Billing/Extensions/BillingExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Extensions;\n\npublic static class BillingExtensions\n{\n    public static ProductTierType GetProductTier(this PlanType planType)\n        => planType switch\n        {\n            PlanType.Custom or PlanType.Free => ProductTierType.Free,\n            PlanType.FamiliesAnnually or PlanType.FamiliesAnnually2025 or PlanType.FamiliesAnnually2019 => ProductTierType.Families,\n            PlanType.TeamsStarter or PlanType.TeamsStarter2023 => ProductTierType.TeamsStarter,\n            _ when planType.ToString().Contains(\"Teams\") => ProductTierType.Teams,\n            _ when planType.ToString().Contains(\"Enterprise\") => ProductTierType.Enterprise,\n            _ => throw new BillingException($\"PlanType {planType} could not be matched to a ProductTierType\")\n        };\n\n    public static bool IsBusinessProductTierType(this PlanType planType)\n        => IsBusinessProductTierType(planType.GetProductTier());\n\n    public static bool IsBusinessProductTierType(this ProductTierType productTierType)\n        => productTierType switch\n        {\n            ProductTierType.Free => false,\n            ProductTierType.Families => false,\n            ProductTierType.Enterprise => true,\n            ProductTierType.Teams => true,\n            ProductTierType.TeamsStarter => true\n        };\n\n    public static bool IsBillable(this Provider provider) =>\n        provider is\n        {\n            Type: ProviderType.Msp or ProviderType.BusinessUnit,\n            Status: ProviderStatusType.Billable\n        };\n\n    public static bool IsBillable(this InviteOrganizationProvider inviteOrganizationProvider) =>\n        inviteOrganizationProvider is\n        {\n            Type: ProviderType.Msp or ProviderType.BusinessUnit,\n            Status: ProviderStatusType.Billable\n        };\n\n    // Reseller types do not have Stripe entities\n    public static bool IsStripeSupported(this ProviderType providerType) =>\n        providerType is ProviderType.Msp or ProviderType.BusinessUnit;\n\n    public static bool SupportsConsolidatedBilling(this ProviderType providerType)\n        => providerType is ProviderType.Msp or ProviderType.BusinessUnit;\n\n    public static bool IsValidClient(this Organization organization)\n        => organization is\n        {\n            Seats: not null,\n            Status: OrganizationStatusType.Managed,\n            PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually\n        };\n\n    public static bool IsStripeEnabled(this ISubscriber subscriber)\n        => subscriber is\n        {\n            GatewayCustomerId: not null and not \"\",\n            GatewaySubscriptionId: not null and not \"\"\n        };\n\n    public static bool IsUnverifiedBankAccount(this SetupIntent setupIntent) =>\n        setupIntent is\n        {\n            Status: \"requires_action\",\n            NextAction:\n            {\n                VerifyWithMicrodeposits: not null\n            },\n            PaymentMethod:\n            {\n                UsBankAccount: not null\n            }\n        };\n\n    public static bool SupportsConsolidatedBilling(this PlanType planType)\n        => planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually;\n}\n"
  },
  {
    "path": "src/Core/Billing/Extensions/CurrencyExtensions.cs",
    "content": "﻿namespace Bit.Core.Billing.Extensions;\n\npublic static class CurrencyExtensions\n{\n    /// <summary>\n    /// Converts a currency amount in major units to minor units.\n    /// </summary>\n    /// <example>123.99 USD returns 12399 in minor units.</example>\n    public static long ToMinor(this decimal amount)\n    {\n        return Convert.ToInt64(amount * 100);\n    }\n\n    /// <summary>\n    /// Converts a currency amount in minor units to major units.\n    /// </summary>\n    /// <param name=\"amount\"></param>\n    /// <example>12399 in minor units returns 123.99 USD.</example>\n    public static decimal? ToMajor(this long? amount)\n    {\n        return amount?.ToMajor();\n    }\n\n    /// <summary>\n    /// Converts a currency amount in minor units to major units.\n    /// </summary>\n    /// <param name=\"amount\"></param>\n    /// <example>12399 in minor units returns 123.99 USD.</example>\n    public static decimal ToMajor(this long amount)\n    {\n        return Convert.ToDecimal(amount) / 100;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Extensions/CustomerExtensions.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Extensions;\n\npublic static class CustomerExtensions\n{\n    public static bool HasBillingLocation(this Customer customer)\n        => customer is\n        {\n            Address:\n            {\n                Country: not null and not \"\",\n                PostalCode: not null and not \"\"\n            }\n        };\n\n    public static bool HasRecognizedTaxLocation(this Customer customer) =>\n        customer?.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.UnrecognizedLocation;\n\n    public static decimal GetBillingBalance(this Customer customer)\n    {\n        return customer != null ? customer.Balance / 100M : default;\n    }\n\n    public static bool ApprovedToPayByInvoice(this Customer customer)\n        => customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.InvoiceApproved, out var value) &&\n           int.TryParse(value, out var invoiceApproved) && invoiceApproved == 1;\n}\n"
  },
  {
    "path": "src/Core/Billing/Extensions/DiscountExtensions.cs",
    "content": "﻿using Stripe;\n\nnamespace Bit.Core.Billing.Extensions;\n\npublic static class DiscountExtensions\n{\n    public static bool AppliesTo(this Discount discount, SubscriptionItem subscriptionItem)\n        => discount.Coupon.AppliesTo.Products.Contains(subscriptionItem.Price.Product.Id);\n\n    public static bool IsValid(this Discount? discount)\n        => discount?.Coupon?.Valid ?? false;\n}\n"
  },
  {
    "path": "src/Core/Billing/Extensions/InvoiceExtensions.cs",
    "content": "﻿using System.Globalization;\nusing System.Text.RegularExpressions;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Extensions;\n\npublic static class InvoiceExtensions\n{\n    /// <summary>\n    /// Formats invoice line items specifically for provider invoices, standardizing product descriptions\n    /// and ensuring consistent tax representation.\n    /// </summary>\n    /// <param name=\"invoice\">The Stripe invoice containing line items</param>\n    /// <param name=\"subscription\">The associated subscription (for future extensibility)</param>\n    /// <returns>A list of formatted invoice item descriptions</returns>\n    public static List<string> FormatForProvider(this Invoice invoice, Subscription subscription)\n    {\n        var items = new List<string>();\n\n        // Return empty list if no line items\n        if (invoice.Lines == null)\n        {\n            return items;\n        }\n\n        foreach (var line in invoice.Lines.Data ?? new List<InvoiceLineItem>())\n        {\n            // Skip null lines or lines without description\n            if (line?.Description == null)\n            {\n                continue;\n            }\n\n            var description = line.Description;\n\n            // Handle Provider Portal and Business Unit Portal service lines\n            if (description.Contains(\"Provider Portal\") || description.Contains(\"Business Unit\"))\n            {\n                var priceMatch = Regex.Match(description, @\"\\(at \\$[\\d,]+\\.?\\d* / month\\)\");\n                var priceInfo = priceMatch.Success ? priceMatch.Value : \"\";\n\n                var standardizedDescription = $\"{line.Quantity} × Manage service provider {priceInfo}\";\n                items.Add(standardizedDescription);\n            }\n            // Handle tax lines\n            else if (description.ToLower().Contains(\"tax\"))\n            {\n                var priceMatch = Regex.Match(description, @\"\\(at \\$[\\d,]+\\.?\\d* / month\\)\");\n                var priceInfo = priceMatch.Success ? priceMatch.Value : \"\";\n\n                // If no price info found in description, calculate from amount\n                if (string.IsNullOrEmpty(priceInfo) && line.Quantity > 0)\n                {\n                    var pricePerItem = (line.Amount / 100m) / line.Quantity;\n                    priceInfo = string.Format(CultureInfo.InvariantCulture, \"(at ${0:F2} / month)\", pricePerItem);\n                }\n\n                var taxDescription = $\"{line.Quantity} × Tax {priceInfo}\";\n                items.Add(taxDescription);\n            }\n            // Handle other line items as-is\n            else\n            {\n                items.Add(description);\n            }\n        }\n\n        var tax = invoice.TotalTaxes?.Sum(invoiceTotalTax => invoiceTotalTax.Amount) ?? 0;\n\n        // Add fallback tax from invoice-level tax if present and not already included\n        if (tax > 0)\n        {\n            var taxAmount = tax / 100m;\n            items.Add(string.Format(CultureInfo.InvariantCulture, \"1 × Tax (at ${0:F2} / month)\", taxAmount));\n        }\n\n        return items;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Extensions/ServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Billing.Licenses;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Billing.Payment;\nusing Bit.Core.Billing.Portal.Commands;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Services.Implementations;\nusing Bit.Core.Billing.Subscriptions.Commands;\nusing Bit.Core.Billing.Subscriptions.Queries;\nusing Bit.Core.Billing.Tax.Services;\nusing Bit.Core.Billing.Tax.Services.Implementations;\nusing Bit.Core.Services;\nusing Bit.Core.Services.Implementations;\n\nnamespace Bit.Core.Billing.Extensions;\n\nusing Microsoft.Extensions.DependencyInjection;\n\npublic static class ServiceCollectionExtensions\n{\n    public static void AddBillingOperations(this IServiceCollection services)\n    {\n        services.AddSingleton<ITaxService, TaxService>();\n        services.AddTransient<IOrganizationBillingService, OrganizationBillingService>();\n        services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();\n        services.AddTransient<ISubscriberService, SubscriberService>();\n        services.AddLicenseServices();\n        services.AddLicenseOperations();\n        services.AddPricingClient();\n        services.AddPaymentOperations();\n        services.AddOrganizationLicenseCommandsQueries();\n        services.AddPremiumCommands();\n        services.AddPremiumQueries();\n        services.AddTransient<IGetOrganizationMetadataQuery, GetOrganizationMetadataQuery>();\n        services.AddTransient<IGetOrganizationWarningsQuery, GetOrganizationWarningsQuery>();\n        services.AddTransient<IRestartSubscriptionCommand, RestartSubscriptionCommand>();\n        services.AddTransient<IPreviewOrganizationTaxCommand, PreviewOrganizationTaxCommand>();\n        services.AddTransient<IGetBitwardenSubscriptionQuery, GetBitwardenSubscriptionQuery>();\n        services.AddTransient<IReinstateSubscriptionCommand, ReinstateSubscriptionCommand>();\n        services.AddTransient<IBraintreeService, BraintreeService>();\n        services.AddTransient<IUpdateOrganizationSubscriptionCommand, UpdateOrganizationSubscriptionCommand>();\n        services.AddTransient<IUpgradeOrganizationPlanVNextCommand, UpgradeOrganizationPlanVNextCommand>();\n        services.AddTransient<ICreateBillingPortalSessionCommand, CreateBillingPortalSessionCommand>();\n    }\n\n    private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services)\n    {\n        services.AddScoped<IGetCloudOrganizationLicenseQuery, GetCloudOrganizationLicenseQuery>();\n        services.AddScoped<IGetSelfHostedOrganizationLicenseQuery, GetSelfHostedOrganizationLicenseQuery>();\n        services.AddScoped<IUpdateOrganizationLicenseCommand, UpdateOrganizationLicenseCommand>();\n    }\n\n    private static void AddPremiumCommands(this IServiceCollection services)\n    {\n        services.AddScoped<ICreatePremiumCloudHostedSubscriptionCommand, CreatePremiumCloudHostedSubscriptionCommand>();\n        services.AddScoped<ICreatePremiumSelfHostedSubscriptionCommand, CreatePremiumSelfHostedSubscriptionCommand>();\n        services.AddTransient<IPreviewPremiumTaxCommand, PreviewPremiumTaxCommand>();\n        services.AddScoped<IPreviewPremiumUpgradeProrationCommand, PreviewPremiumUpgradeProrationCommand>();\n        services.AddScoped<IUpdatePremiumStorageCommand, UpdatePremiumStorageCommand>();\n        services.AddScoped<IUpgradePremiumToOrganizationCommand, UpgradePremiumToOrganizationCommand>();\n    }\n\n    private static void AddPremiumQueries(this IServiceCollection services)\n    {\n        services.AddScoped<IHasPremiumAccessQuery, HasPremiumAccessQuery>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Extensions/SubscriberExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Extensions;\n\npublic static class SubscriberExtensions\n{\n    /// <summary>\n    /// We are taking only first 30 characters of the SubscriberName because stripe provide for 30 characters  for\n    /// custom_fields,see the link: https://stripe.com/docs/api/invoices/create\n    /// </summary>\n    /// <param name=\"subscriber\"></param>\n    /// <returns></returns>\n    public static string GetFormattedInvoiceName(this ISubscriber subscriber)\n    {\n        var subscriberName = subscriber.SubscriberName();\n\n        if (string.IsNullOrWhiteSpace(subscriberName))\n        {\n            return string.Empty;\n        }\n\n        return subscriberName.Length <= 30\n            ? subscriberName\n            : subscriberName[..30];\n    }\n\n    public static ProductUsageType GetProductUsageType(this ISubscriber subscriber)\n        => subscriber switch\n        {\n            User => ProductUsageType.Personal,\n            Organization organization when organization.PlanType.GetProductTier() is ProductTierType.Free or ProductTierType.Families => ProductUsageType.Personal,\n            Organization => ProductUsageType.Business,\n            Provider => ProductUsageType.Business,\n            _ => throw new ArgumentOutOfRangeException(nameof(subscriber))\n        };\n}\n"
  },
  {
    "path": "src/Core/Billing/Extensions/SubscriptionExtensions.cs",
    "content": "﻿using Stripe;\n\nnamespace Bit.Core.Billing.Extensions;\n\npublic static class SubscriptionExtensions\n{\n    /*\n     * For the time being, this is the simplest migration approach from v45 to v48 as\n     * we do not support multi-cadence subscriptions. Each subscription item should be on the\n     * same billing cycle. If this changes, we'll need a significantly more robust approach.\n     *\n     * Because we can't guarantee a subscription will have items, this has to be nullable.\n     */\n    public static (DateTime? Start, DateTime? End)? GetCurrentPeriod(this Subscription subscription)\n    {\n        var item = subscription.Items?.FirstOrDefault();\n        return item is null ? null : (item.CurrentPeriodStart, item.CurrentPeriodEnd);\n    }\n\n    public static DateTime? GetCurrentPeriodStart(this Subscription subscription) =>\n        subscription.Items?.FirstOrDefault()?.CurrentPeriodStart;\n\n    public static DateTime? GetCurrentPeriodEnd(this Subscription subscription) =>\n        subscription.Items?.FirstOrDefault()?.CurrentPeriodEnd;\n}\n"
  },
  {
    "path": "src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs",
    "content": "﻿using Stripe;\n\nnamespace Bit.Core.Billing.Extensions;\n\npublic static class SubscriptionUpdateOptionsExtensions\n{\n    /// <summary>\n    /// Attempts to enable automatic tax for given subscription options.\n    /// </summary>\n    /// <param name=\"options\"></param>\n    /// <param name=\"customer\">The existing customer to which the subscription belongs.</param>\n    /// <param name=\"subscription\">The existing subscription.</param>\n    /// <returns>Returns true when successful, false when conditions are not met.</returns>\n    public static bool EnableAutomaticTax(\n        this SubscriptionUpdateOptions options,\n        Customer customer,\n        Subscription subscription)\n    {\n        if (subscription.AutomaticTax.Enabled)\n        {\n            return false;\n        }\n\n        // We might only need to check the automatic tax status.\n        if (!customer.HasRecognizedTaxLocation() && string.IsNullOrWhiteSpace(customer.Address?.Country))\n        {\n            return false;\n        }\n\n        options.DefaultTaxRates = [];\n        options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.Billing.Licenses.Extensions;\n\npublic static class LicenseExtensions\n{\n    public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime issued)\n    {\n\n        if (subscriptionInfo?.Subscription == null)\n        {\n            // Subscription isn't setup yet, so fallback to the organization's expiration date\n            // If there isn't an expiration date on the org, treat it as a free trial\n            return org.ExpirationDate ?? issued.AddDays(7);\n        }\n\n        var subscription = subscriptionInfo.Subscription;\n\n        if (subscription.TrialEndDate > DateTime.UtcNow)\n        {\n            // Still trialing, use trial's end date\n            return subscription.TrialEndDate.Value;\n        }\n\n        if (org.ExpirationDate < DateTime.UtcNow)\n        {\n            // Organization is expired\n            return org.ExpirationDate.Value;\n        }\n\n        if (subscription.PeriodDuration > TimeSpan.FromDays(180))\n        {\n            // Annual subscription - include grace period to give the administrators time to upload a new license\n            return subscription.PeriodEndDate\n                !.Value\n                .AddDays(Core.Constants.OrganizationSelfHostSubscriptionGracePeriodDays);\n        }\n\n        // Monthly subscription - giving an annual expiration to not burnden admins to upload fresh licenses each month\n        return org.ExpirationDate?.AddMonths(11) ?? issued.AddYears(1);\n    }\n\n    public static DateTime CalculateFreshRefreshDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime issued)\n    {\n\n        if (subscriptionInfo?.Subscription == null)\n        {\n            // Subscription isn't setup yet, so fallback to the organization's expiration date\n            // If there isn't an expiration date on the org, treat it as a free trial\n            return org.ExpirationDate ?? issued.AddDays(7);\n        }\n\n        var subscription = subscriptionInfo.Subscription;\n\n        if (subscription.TrialEndDate > DateTime.UtcNow)\n        {\n            // Still trialing, use trial's end date\n            return subscription.TrialEndDate.Value;\n        }\n\n        if (org.ExpirationDate < DateTime.UtcNow)\n        {\n            // Organization is expired\n            return org.ExpirationDate.Value;\n        }\n\n        if (subscription.PeriodDuration > TimeSpan.FromDays(180))\n        {\n            // Annual subscription - refresh every 30 days to check for plan changes, cancellations, and payment issues\n            return issued.AddDays(30);\n        }\n\n        var expires = org.ExpirationDate?.AddMonths(11) ?? issued.AddYears(1);\n\n        // If expiration is more than 30 days in the past, refresh in 30 days instead of using the stale date to give\n        // them a chance to refresh. Otherwise, uses the expiration date\n        return issued - expires > TimeSpan.FromDays(30)\n            ? issued.AddDays(30)\n            : expires;\n    }\n\n    public static DateTime? CalculateFreshExpirationDateWithoutGracePeriod(this Organization org, SubscriptionInfo subscriptionInfo)\n    {\n        // It doesn't make sense that this returns null sometimes. If the expiration date doesn't include a grace period\n        // then we should just return the expiration date instead of null. This is currently forcing the single consumer\n        // to check for nulls.\n\n        // At some point in the future, we should update this. We can't easily, though, without breaking the signatures\n        // since `ExpirationWithoutGracePeriod` is included on them. So for now, I'll shake my fist and then move on.\n\n        // Only set expiration without grace period for active, non-trial, annual subscriptions\n        if (subscriptionInfo?.Subscription != null &&\n            subscriptionInfo.Subscription.TrialEndDate <= DateTime.UtcNow &&\n            org.ExpirationDate >= DateTime.UtcNow &&\n            subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180))\n        {\n            return subscriptionInfo.Subscription.PeriodEndDate;\n        }\n\n        // Otherwise, return null.\n        return null;\n    }\n\n    public static bool CalculateIsTrialing(this Organization org, SubscriptionInfo subscriptionInfo) =>\n        subscriptionInfo?.Subscription is null\n            ? !org.ExpirationDate.HasValue\n            : subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow;\n\n    public static T GetValue<T>(this ClaimsPrincipal principal, string claimType)\n    {\n        var claim = principal.FindFirst(claimType);\n\n        if (claim is null)\n        {\n            return default;\n        }\n\n        // Handle Guid\n        if (typeof(T) == typeof(Guid))\n        {\n            return Guid.TryParse(claim.Value, out var guid)\n                ? (T)(object)guid\n                : default;\n        }\n\n        // Handle DateTime\n        if (typeof(T) == typeof(DateTime))\n        {\n            return DateTime.TryParse(claim.Value, out var dateTime)\n                ? (T)(object)dateTime\n                : default;\n        }\n\n        // Handle TimeSpan\n        if (typeof(T) == typeof(TimeSpan))\n        {\n            return TimeSpan.TryParse(claim.Value, out var timeSpan)\n                ? (T)(object)timeSpan\n                : default;\n        }\n\n        // Check for Nullable Types\n        var underlyingType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);\n\n        // Handle Enums\n        if (underlyingType.IsEnum)\n        {\n            if (Enum.TryParse(underlyingType, claim.Value, true, out var enumValue))\n            {\n                return (T)enumValue; // Cast back to T\n            }\n\n            return default; // Return default value for non-nullable enums or null for nullable enums\n        }\n\n        // Handle other Nullable Types (e.g., int?, bool?)\n        if (underlyingType == typeof(int))\n        {\n            return int.TryParse(claim.Value, out var intValue)\n                ? (T)(object)intValue\n                : default;\n        }\n\n        if (underlyingType == typeof(bool))\n        {\n            return bool.TryParse(claim.Value, out var boolValue)\n                ? (T)(object)boolValue\n                : default;\n        }\n\n        if (underlyingType == typeof(double))\n        {\n            return double.TryParse(claim.Value, out var doubleValue)\n                ? (T)(object)doubleValue\n                : default;\n        }\n\n        // Fallback to Convert.ChangeType for other types including strings\n        return (T)Convert.ChangeType(claim.Value, underlyingType);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Licenses.Services;\nusing Bit.Core.Billing.Licenses.Services.Implementations;\nusing Bit.Core.Entities;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Billing.Licenses.Extensions;\n\npublic static class LicenseServiceCollectionExtensions\n{\n    public static void AddLicenseServices(this IServiceCollection services)\n    {\n        services.AddTransient<ILicenseClaimsFactory<Organization>, OrganizationLicenseClaimsFactory>();\n        services.AddTransient<ILicenseClaimsFactory<User>, UserLicenseClaimsFactory>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/LicenseConstants.cs",
    "content": "﻿namespace Bit.Core.Billing.Licenses;\n\npublic static class OrganizationLicenseConstants\n{\n    public const string LicenseType = nameof(LicenseType);\n    public const string LicenseKey = nameof(LicenseKey);\n    public const string InstallationId = nameof(InstallationId);\n    public const string Id = nameof(Id);\n    public const string Name = nameof(Name);\n    public const string BusinessName = nameof(BusinessName);\n    public const string BillingEmail = nameof(BillingEmail);\n    public const string Enabled = nameof(Enabled);\n    public const string Plan = nameof(Plan);\n    public const string PlanType = nameof(PlanType);\n    public const string Seats = nameof(Seats);\n    public const string MaxCollections = nameof(MaxCollections);\n    public const string UsePolicies = nameof(UsePolicies);\n    public const string UseSso = nameof(UseSso);\n    public const string UseKeyConnector = nameof(UseKeyConnector);\n    public const string UseScim = nameof(UseScim);\n    public const string UseGroups = nameof(UseGroups);\n    public const string UseEvents = nameof(UseEvents);\n    public const string UseDirectory = nameof(UseDirectory);\n    public const string UseTotp = nameof(UseTotp);\n    public const string Use2fa = nameof(Use2fa);\n    public const string UseApi = nameof(UseApi);\n    public const string UseResetPassword = nameof(UseResetPassword);\n    public const string MaxStorageGb = nameof(MaxStorageGb);\n    public const string SelfHost = nameof(SelfHost);\n    public const string UsersGetPremium = nameof(UsersGetPremium);\n    public const string UseCustomPermissions = nameof(UseCustomPermissions);\n    public const string Issued = nameof(Issued);\n    public const string UsePasswordManager = nameof(UsePasswordManager);\n    public const string UseSecretsManager = nameof(UseSecretsManager);\n    public const string SmSeats = nameof(SmSeats);\n    public const string SmServiceAccounts = nameof(SmServiceAccounts);\n    public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion);\n    public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems);\n    public const string UseRiskInsights = nameof(UseRiskInsights);\n    public const string Expires = nameof(Expires);\n    public const string Refresh = nameof(Refresh);\n    public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod);\n    public const string Trial = nameof(Trial);\n    public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);\n    public const string UseOrganizationDomains = nameof(UseOrganizationDomains);\n    public const string UseAutomaticUserConfirmation = nameof(UseAutomaticUserConfirmation);\n    public const string UseDisableSmAdsForUsers = nameof(UseDisableSmAdsForUsers);\n    public const string UsePhishingBlocker = nameof(UsePhishingBlocker);\n    public const string UseMyItems = nameof(UseMyItems);\n}\n\npublic static class UserLicenseConstants\n{\n    public const string LicenseType = nameof(LicenseType);\n    public const string LicenseKey = nameof(LicenseKey);\n    public const string Id = nameof(Id);\n    public const string Name = nameof(Name);\n    public const string Email = nameof(Email);\n    public const string Premium = nameof(Premium);\n    public const string MaxStorageGb = nameof(MaxStorageGb);\n    public const string Issued = nameof(Issued);\n    public const string Expires = nameof(Expires);\n    public const string Refresh = nameof(Refresh);\n    public const string Trial = nameof(Trial);\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/Models/Api/Response/LicenseResponseModel.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Core.Billing.Licenses.Models.Api.Response;\n\n/// <summary>\n/// Response model containing user license information.\n/// Separated from subscription data to maintain separation of concerns.\n/// </summary>\npublic class LicenseResponseModel : ResponseModel\n{\n    public LicenseResponseModel(UserLicense license, ClaimsPrincipal? claimsPrincipal)\n        : base(\"license\")\n    {\n        License = license;\n\n        // CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim\n        // The token's expiration is cryptographically secured and cannot be tampered with\n        // The file's Expires property can be manually edited and should NOT be trusted for display\n        if (claimsPrincipal != null)\n        {\n            Expiration = claimsPrincipal.GetValue<DateTime?>(UserLicenseConstants.Expires);\n        }\n        else\n        {\n            // No token - use the license file expiration (for older licenses without tokens)\n            Expiration = license.Expires;\n        }\n    }\n\n    /// <summary>\n    /// The user's license containing feature entitlements and metadata.\n    /// </summary>\n    public UserLicense License { get; set; }\n\n    /// <summary>\n    /// The license expiration date.\n    /// Extracted from the cryptographically secured JWT token when available,\n    /// otherwise falls back to the license file's expiration date.\n    /// </summary>\n    public DateTime? Expiration { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/Models/LicenseContext.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.Billing.Licenses.Models;\n\npublic class LicenseContext\n{\n    public Guid? InstallationId { get; init; }\n    public required SubscriptionInfo SubscriptionInfo { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/Queries/GetUserLicenseQuery.cs",
    "content": "﻿using Bit.Core.Billing.Models.Business;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.Billing.Licenses.Queries;\n\npublic interface IGetUserLicenseQuery\n{\n    Task<UserLicense> Run(User user);\n}\n\npublic class GetUserLicenseQuery(\n    IUserService userService) : IGetUserLicenseQuery\n{\n    public async Task<UserLicense> Run(User user)\n    {\n        return await userService.GenerateLicenseAsync(user);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/Registrations.cs",
    "content": "﻿using Bit.Core.Billing.Licenses.Queries;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Billing.Licenses;\n\npublic static class Registrations\n{\n    public static void AddLicenseOperations(this IServiceCollection services)\n    {\n        // Queries\n        services.AddTransient<IGetUserLicenseQuery, GetUserLicenseQuery>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/Services/ILicenseClaimsFactory.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Billing.Licenses.Models;\n\nnamespace Bit.Core.Billing.Licenses.Services;\n\npublic interface ILicenseClaimsFactory<in T>\n{\n    Task<List<Claim>> GenerateClaims(T entity, LicenseContext licenseContext);\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Globalization;\nusing System.Security.Claims;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Licenses.Models;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Billing.Licenses.Services.Implementations;\n\npublic class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organization>\n{\n    public Task<List<Claim>> GenerateClaims(Organization entity, LicenseContext licenseContext)\n    {\n        var issued = DateTime.UtcNow;\n        var subscriptionInfo = licenseContext.SubscriptionInfo;\n        var expires = entity.CalculateFreshExpirationDate(subscriptionInfo, issued);\n        var refresh = entity.CalculateFreshRefreshDate(subscriptionInfo, issued);\n        var expirationWithoutGracePeriod = entity.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo);\n        var trial = entity.CalculateIsTrialing(subscriptionInfo);\n\n        var claims = new List<Claim>\n        {\n            new(nameof(OrganizationLicenseConstants.LicenseType), LicenseType.Organization.ToString()),\n            new(nameof(OrganizationLicenseConstants.Id), entity.Id.ToString()),\n            new(nameof(OrganizationLicenseConstants.Enabled), entity.Enabled.ToString()),\n            new(nameof(OrganizationLicenseConstants.PlanType), ((int)entity.PlanType).ToString()),\n            new(nameof(OrganizationLicenseConstants.UsePolicies), entity.UsePolicies.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseSso), entity.UseSso.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseKeyConnector), entity.UseKeyConnector.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseScim), entity.UseScim.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseGroups), entity.UseGroups.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseEvents), entity.UseEvents.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseDirectory), entity.UseDirectory.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseTotp), entity.UseTotp.ToString()),\n            new(nameof(OrganizationLicenseConstants.Use2fa), entity.Use2fa.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseApi), entity.UseApi.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseResetPassword), entity.UseResetPassword.ToString()),\n            new(nameof(OrganizationLicenseConstants.SelfHost), entity.SelfHost.ToString()),\n            new(nameof(OrganizationLicenseConstants.UsersGetPremium), entity.UsersGetPremium.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseCustomPermissions), entity.UseCustomPermissions.ToString()),\n            new(nameof(OrganizationLicenseConstants.UsePasswordManager), entity.UsePasswordManager.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseSecretsManager), entity.UseSecretsManager.ToString()),\n            // LimitCollectionCreationDeletion was split and removed from the\n            // license. Left here with an assignment from the new values for\n            // backwards compatibility.\n            new(nameof(OrganizationLicenseConstants.LimitCollectionCreationDeletion),\n                (entity.LimitCollectionCreation || entity.LimitCollectionDeletion).ToString()),\n            new(nameof(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems), entity.AllowAdminAccessToAllCollectionItems.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseRiskInsights), entity.UseRiskInsights.ToString()),\n            new(nameof(OrganizationLicenseConstants.Issued), issued.ToString(CultureInfo.InvariantCulture)),\n            new(nameof(OrganizationLicenseConstants.Expires), expires.ToString(CultureInfo.InvariantCulture)),\n            new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)),\n            new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseOrganizationDomains), entity.UseOrganizationDomains.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseAutomaticUserConfirmation), entity.UseAutomaticUserConfirmation.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseDisableSmAdsForUsers), entity.UseDisableSmAdsForUsers.ToString()),\n            new(nameof(OrganizationLicenseConstants.UsePhishingBlocker), entity.UsePhishingBlocker.ToString()),\n            new(nameof(OrganizationLicenseConstants.UseMyItems), entity.UseMyItems.ToString()),\n        };\n\n        if (entity.Name is not null)\n        {\n            claims.Add(new(nameof(OrganizationLicenseConstants.Name), entity.Name));\n        }\n\n        if (entity.BillingEmail is not null)\n        {\n            claims.Add(new(nameof(OrganizationLicenseConstants.BillingEmail), entity.BillingEmail));\n        }\n\n        if (entity.Plan is not null)\n        {\n            claims.Add(new(nameof(OrganizationLicenseConstants.Plan), entity.Plan));\n        }\n\n        if (entity.BusinessName is not null)\n        {\n            claims.Add(new Claim(nameof(OrganizationLicenseConstants.BusinessName), entity.BusinessName));\n        }\n\n        if (entity.LicenseKey is not null)\n        {\n            claims.Add(new Claim(nameof(OrganizationLicenseConstants.LicenseKey), entity.LicenseKey));\n        }\n\n        if (licenseContext.InstallationId.HasValue)\n        {\n            claims.Add(new Claim(nameof(OrganizationLicenseConstants.InstallationId), licenseContext.InstallationId.ToString()));\n        }\n\n        if (entity.Seats.HasValue)\n        {\n            claims.Add(new Claim(nameof(OrganizationLicenseConstants.Seats), entity.Seats.ToString()));\n        }\n\n        if (entity.MaxCollections.HasValue)\n        {\n            claims.Add(new Claim(nameof(OrganizationLicenseConstants.MaxCollections), entity.MaxCollections.ToString()));\n        }\n\n        if (entity.MaxStorageGb.HasValue)\n        {\n            claims.Add(new Claim(nameof(OrganizationLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()));\n        }\n\n        if (entity.SmSeats.HasValue)\n        {\n            claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmSeats), entity.SmSeats.ToString()));\n        }\n\n        if (entity.SmServiceAccounts.HasValue)\n        {\n            claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()));\n        }\n\n        if (expirationWithoutGracePeriod is not null)\n        {\n            claims.Add(new Claim(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod),\n                expirationWithoutGracePeriod.Value.ToString(CultureInfo.InvariantCulture)));\n        }\n\n        return Task.FromResult(claims);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Globalization;\nusing System.Security.Claims;\nusing Bit.Core.Billing.Licenses.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Billing.Licenses.Services.Implementations;\n\npublic class UserLicenseClaimsFactory : ILicenseClaimsFactory<User>\n{\n    public Task<List<Claim>> GenerateClaims(User entity, LicenseContext licenseContext)\n    {\n        var subscriptionInfo = licenseContext.SubscriptionInfo;\n\n        var expires = subscriptionInfo?.UpcomingInvoice?.Date?.AddDays(7) ?? entity.PremiumExpirationDate?.AddDays(7);\n        var refresh = subscriptionInfo?.UpcomingInvoice?.Date ?? entity.PremiumExpirationDate;\n        var trial = (subscriptionInfo?.Subscription?.TrialEndDate.HasValue ?? false) &&\n                    subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow;\n\n        var claims = new List<Claim>\n        {\n            new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()),\n            new(nameof(UserLicenseConstants.Id), entity.Id.ToString()),\n            new(nameof(UserLicenseConstants.Premium), entity.Premium.ToString()),\n            new(nameof(UserLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),\n            new(nameof(UserLicenseConstants.Trial), trial.ToString()),\n        };\n\n        if (entity.Email is not null)\n        {\n            claims.Add(new(nameof(UserLicenseConstants.Email), entity.Email));\n        }\n\n        if (entity.Name is not null)\n        {\n            claims.Add(new(nameof(UserLicenseConstants.Name), entity.Name));\n        }\n\n        if (entity.LicenseKey is not null)\n        {\n            claims.Add(new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey));\n        }\n\n        if (entity.MaxStorageGb.HasValue)\n        {\n            claims.Add(new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()));\n        }\n\n        if (expires.HasValue)\n        {\n            claims.Add(new(nameof(UserLicenseConstants.Expires), expires.Value.ToString(CultureInfo.InvariantCulture)));\n        }\n\n        if (refresh.HasValue)\n        {\n            claims.Add(new(nameof(UserLicenseConstants.Refresh), refresh.Value.ToString(CultureInfo.InvariantCulture)));\n        }\n\n        return Task.FromResult(claims);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Models.Api.Requests.Accounts;\n\npublic class TrialSendVerificationEmailRequestModel : RegisterSendVerificationEmailRequestModel\n{\n    public ProductTierType ProductTier { get; set; }\n    public IEnumerable<ProductType> Products { get; set; }\n    public int? TrialLength { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/Api/Response/SubscriptionDiscountResponseModel.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Subscriptions.Entities;\n\nnamespace Bit.Core.Billing.Models.Api.Response;\n\npublic class SubscriptionDiscountResponseModel\n{\n    public string StripeCouponId { get; init; } = null!;\n    public decimal? PercentOff { get; init; }\n    public long? AmountOff { get; init; }\n    public string? Currency { get; init; }\n    public string Duration { get; init; } = null!;\n    public int? DurationInMonths { get; init; }\n    public string? Name { get; init; }\n    public DateTime StartDate { get; init; }\n    public DateTime EndDate { get; init; }\n    public IDictionary<DiscountTierType, bool>? TierEligibility { get; init; }\n\n    public static SubscriptionDiscountResponseModel From(\n        SubscriptionDiscount discount,\n        IDictionary<DiscountTierType, bool> tierEligibility) => new()\n        {\n            StripeCouponId = discount.StripeCouponId,\n            PercentOff = discount.PercentOff,\n            AmountOff = discount.AmountOff,\n            Currency = discount.Currency,\n            Duration = discount.Duration,\n            DurationInMonths = discount.DurationInMonths,\n            Name = discount.Name,\n            StartDate = discount.StartDate,\n            EndDate = discount.EndDate,\n            TierEligibility = tierEligibility\n        };\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/BillingHistoryInfo.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Models;\n\npublic class BillingHistoryInfo\n{\n    public IEnumerable<BillingInvoice> Invoices { get; set; } = new List<BillingInvoice>();\n    public IEnumerable<BillingTransaction> Transactions { get; set; } = new List<BillingTransaction>();\n\n    public class BillingTransaction\n    {\n        public BillingTransaction(Transaction transaction)\n        {\n            Id = transaction.Id;\n            CreatedDate = transaction.CreationDate;\n            Refunded = transaction.Refunded;\n            Type = transaction.Type;\n            PaymentMethodType = transaction.PaymentMethodType;\n            Details = transaction.Details;\n            Amount = transaction.Amount;\n            RefundedAmount = transaction.RefundedAmount;\n        }\n\n        public Guid Id { get; set; }\n        public DateTime CreatedDate { get; set; }\n        public decimal Amount { get; set; }\n        public bool? Refunded { get; set; }\n        public bool? PartiallyRefunded => !Refunded.GetValueOrDefault() && RefundedAmount.GetValueOrDefault() > 0;\n        public decimal? RefundedAmount { get; set; }\n        public TransactionType Type { get; set; }\n        public PaymentMethodType? PaymentMethodType { get; set; }\n        public string Details { get; set; }\n    }\n\n    public class BillingInvoice\n    {\n        public BillingInvoice(Invoice inv)\n        {\n            Id = inv.Id;\n            Date = inv.Created;\n            Url = inv.HostedInvoiceUrl;\n            PdfUrl = inv.InvoicePdf;\n            Number = inv.Number;\n            Paid = inv.Status == StripeConstants.InvoiceStatus.Paid;\n            Amount = inv.Total / 100M;\n        }\n\n        public string Id { get; set; }\n        public decimal Amount { get; set; }\n        public DateTime? Date { get; set; }\n        public string Url { get; set; }\n        public string PdfUrl { get; set; }\n        public string Number { get; set; }\n        public bool Paid { get; set; }\n    }\n\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/BillingInfo.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Models;\n\npublic class BillingInfo\n{\n    public decimal Balance { get; set; }\n    public BillingSource PaymentSource { get; set; }\n\n    public class BillingSource\n    {\n        public BillingSource() { }\n\n        public BillingSource(Stripe.PaymentMethod method)\n        {\n            if (method.Card == null)\n            {\n                return;\n            }\n\n            Type = PaymentMethodType.Card;\n            var card = method.Card;\n            Description = $\"{card.Brand?.ToUpperInvariant()}, *{card.Last4}, {card.ExpMonth:00}/{card.ExpYear}\";\n            CardBrand = card.Brand;\n        }\n\n        public BillingSource(IPaymentSource source)\n        {\n            switch (source)\n            {\n                case BankAccount bankAccount:\n                    var bankStatus = bankAccount.Status switch\n                    {\n                        \"verified\" => \"verified\",\n                        \"errored\" => \"invalid\",\n                        \"verification_failed\" => \"verification failed\",\n                        _ => \"unverified\"\n                    };\n                    Type = PaymentMethodType.BankAccount;\n                    Description = $\"{bankAccount.BankName}, *{bankAccount.Last4} - {bankStatus}\";\n                    NeedsVerification = bankAccount.Status is \"new\" or \"validated\";\n                    break;\n                case Card card:\n                    Type = PaymentMethodType.Card;\n                    Description = $\"{card.Brand}, *{card.Last4}, {card.ExpMonth:00}/{card.ExpYear}\";\n                    CardBrand = card.Brand;\n                    break;\n                case Source { Card: not null } src:\n                    Type = PaymentMethodType.Card;\n                    Description = $\"{src.Card.Brand}, *{src.Card.Last4}, {src.Card.ExpMonth:00}/{src.Card.ExpYear}\";\n                    CardBrand = src.Card.Brand;\n                    break;\n            }\n        }\n\n        public BillingSource(Braintree.PaymentMethod method)\n        {\n            switch (method)\n            {\n                case Braintree.PayPalAccount paypal:\n                    Type = PaymentMethodType.PayPal;\n                    Description = paypal.Email;\n                    break;\n                case Braintree.CreditCard card:\n                    Type = PaymentMethodType.Card;\n                    Description = $\"{card.CardType.ToString()}, *{card.LastFour}, \" +\n                                  $\"{card.ExpirationMonth.PadLeft(2, '0')}/{card.ExpirationYear}\";\n                    CardBrand = card.CardType.ToString();\n                    break;\n                case Braintree.UsBankAccount bank:\n                    Type = PaymentMethodType.BankAccount;\n                    Description = $\"{bank.BankName}, *{bank.Last4}\";\n                    break;\n                default:\n                    throw new NotSupportedException(\"Method not supported.\");\n            }\n        }\n\n        public BillingSource(Braintree.UsBankAccountDetails bank)\n        {\n            Type = PaymentMethodType.BankAccount;\n            Description = $\"{bank.BankName}, *{bank.Last4}\";\n        }\n\n        public BillingSource(Braintree.PayPalDetails paypal)\n        {\n            Type = PaymentMethodType.PayPal;\n            Description = paypal.PayerEmail;\n        }\n\n        public PaymentMethodType Type { get; set; }\n        public string CardBrand { get; set; }\n        public string Description { get; set; }\n        public bool NeedsVerification { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/Business/ILicense.cs",
    "content": "﻿using System.Security.Cryptography.X509Certificates;\n\nnamespace Bit.Core.Billing.Models.Business;\n\npublic interface ILicense\n{\n    string LicenseKey { get; set; }\n    int Version { get; set; }\n    DateTime Issued { get; set; }\n    DateTime? Refresh { get; set; }\n    DateTime? Expires { get; set; }\n    bool Trial { get; set; }\n    string Hash { get; set; }\n    string Signature { get; set; }\n    string Token { get; set; }\n    byte[] SignatureBytes { get; }\n    byte[] GetDataBytes(bool forHash = false);\n    byte[] ComputeHash();\n    bool VerifySignature(X509Certificate2 certificate);\n    byte[] Sign(X509Certificate2 certificate);\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/Business/UserLicense.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Reflection;\nusing System.Security.Claims;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.Billing.Models.Business;\n\npublic class UserLicense : ILicense\n{\n    public UserLicense()\n    { }\n\n    public UserLicense(User user, SubscriptionInfo subscriptionInfo, ILicensingService licenseService,\n        int? version = null)\n    {\n        LicenseType = Core.Enums.LicenseType.User;\n        LicenseKey = user.LicenseKey;\n        Id = user.Id;\n        Name = user.Name;\n        Email = user.Email;\n        Version = version.GetValueOrDefault(1);\n        Premium = user.Premium;\n        MaxStorageGb = user.MaxStorageGb;\n        Issued = DateTime.UtcNow;\n        Expires = subscriptionInfo?.UpcomingInvoice?.Date != null ?\n            subscriptionInfo.UpcomingInvoice.Date.Value.AddDays(7) :\n            user.PremiumExpirationDate?.AddDays(7);\n        Refresh = subscriptionInfo?.UpcomingInvoice?.Date;\n        Trial = (subscriptionInfo?.Subscription?.TrialEndDate.HasValue ?? false) &&\n            subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow;\n\n        Hash = Convert.ToBase64String(ComputeHash());\n        Signature = Convert.ToBase64String(licenseService.SignLicense(this));\n    }\n\n    public UserLicense(User user, ILicensingService licenseService, int? version = null)\n    {\n        LicenseType = Core.Enums.LicenseType.User;\n        LicenseKey = user.LicenseKey;\n        Id = user.Id;\n        Name = user.Name;\n        Email = user.Email;\n        Version = version.GetValueOrDefault(1);\n        Premium = user.Premium;\n        MaxStorageGb = user.MaxStorageGb;\n        Issued = DateTime.UtcNow;\n        Expires = user.PremiumExpirationDate?.AddDays(7);\n        Refresh = user.PremiumExpirationDate?.Date;\n        Trial = false;\n\n        Hash = Convert.ToBase64String(ComputeHash());\n        Signature = Convert.ToBase64String(licenseService.SignLicense(this));\n    }\n\n    public string LicenseKey { get; set; }\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public string Email { get; set; }\n    public bool Premium { get; set; }\n    public short? MaxStorageGb { get; set; }\n    public int Version { get; set; }\n    public DateTime Issued { get; set; }\n    public DateTime? Refresh { get; set; }\n    public DateTime? Expires { get; set; }\n    public bool Trial { get; set; }\n    public LicenseType? LicenseType { get; set; }\n    public string Hash { get; set; }\n    public string Signature { get; set; }\n    public string Token { get; set; }\n    [JsonIgnore]\n    public byte[] SignatureBytes => Convert.FromBase64String(Signature);\n\n    public byte[] GetDataBytes(bool forHash = false)\n    {\n        string data = null;\n        if (Version == 1)\n        {\n            var props = typeof(UserLicense)\n                .GetProperties(BindingFlags.Public | BindingFlags.Instance)\n                .Where(p =>\n                    !p.Name.Equals(nameof(Signature)) &&\n                    !p.Name.Equals(nameof(SignatureBytes)) &&\n                    !p.Name.Equals(nameof(LicenseType)) &&\n                    !p.Name.Equals(nameof(Token)) &&\n                    (\n                        !forHash ||\n                        (\n                            !p.Name.Equals(nameof(Hash)) &&\n                            !p.Name.Equals(nameof(Issued)) &&\n                            !p.Name.Equals(nameof(Refresh))\n                        )\n                    ))\n                .OrderBy(p => p.Name)\n                .Select(p => $\"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}\")\n                .Aggregate((c, n) => $\"{c}|{n}\");\n            data = $\"license:user|{props}\";\n        }\n        else\n        {\n            throw new NotSupportedException($\"Version {Version} is not supported.\");\n        }\n\n        return Encoding.UTF8.GetBytes(data);\n    }\n\n    public byte[] ComputeHash()\n    {\n        using (var alg = SHA256.Create())\n        {\n            return alg.ComputeHash(GetDataBytes(true));\n        }\n    }\n\n    public bool CanUse(User user, ClaimsPrincipal claimsPrincipal, out string exception)\n    {\n        if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)\n        {\n            return ObsoleteCanUse(user, out exception);\n        }\n\n        var errorMessages = new StringBuilder();\n\n        if (!user.EmailVerified)\n        {\n            errorMessages.AppendLine(\"The user's email is not verified.\");\n        }\n\n        var email = claimsPrincipal.GetValue<string>(nameof(Email));\n        if (!email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))\n        {\n            errorMessages.AppendLine(\"The user's email does not match the license email.\");\n        }\n\n        if (errorMessages.Length > 0)\n        {\n            exception = $\"Invalid license. {errorMessages.ToString().TrimEnd()}\";\n            return false;\n        }\n\n        exception = \"\";\n        return true;\n    }\n\n    /// <summary>\n    /// Do not extend this method. It is only here for backwards compatibility with old licenses.\n    /// Instead, extend the CanUse method using the ClaimsPrincipal.\n    /// </summary>\n    /// <param name=\"user\"></param>\n    /// <param name=\"exception\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"NotSupportedException\"></exception>\n    private bool ObsoleteCanUse(User user, out string exception)\n    {\n        // Do not extend this method. It is only here for backwards compatibility with old licenses.\n        var errorMessages = new StringBuilder();\n\n        if (Issued > DateTime.UtcNow)\n        {\n            errorMessages.AppendLine(\"The license hasn't been issued yet.\");\n        }\n\n        if (Expires < DateTime.UtcNow)\n        {\n            errorMessages.AppendLine(\"The license has expired.\");\n        }\n\n        if (Version != 1)\n        {\n            throw new NotSupportedException($\"Version {Version} is not supported.\");\n        }\n\n        if (!user.EmailVerified)\n        {\n            errorMessages.AppendLine(\"The user's email is not verified.\");\n        }\n\n        if (!user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase))\n        {\n            errorMessages.AppendLine(\"The user's email does not match the license email.\");\n        }\n\n        if (errorMessages.Length > 0)\n        {\n            exception = $\"Invalid license. {errorMessages.ToString().TrimEnd()}\";\n            return false;\n        }\n\n        exception = \"\";\n        return true;\n    }\n\n    public bool VerifyData(User user, ClaimsPrincipal claimsPrincipal)\n    {\n        if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)\n        {\n            return ObsoleteVerifyData(user);\n        }\n\n        var licenseKey = claimsPrincipal.GetValue<string>(nameof(LicenseKey));\n        var premium = claimsPrincipal.GetValue<bool>(nameof(Premium));\n        var email = claimsPrincipal.GetValue<string>(nameof(Email));\n\n        return licenseKey == user.LicenseKey &&\n               premium == user.Premium &&\n               email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);\n    }\n\n    /// <summary>\n    /// Do not extend this method. It is only here for backwards compatibility with old licenses.\n    /// Instead, extend the VerifyData method using the ClaimsPrincipal.\n    /// </summary>\n    /// <param name=\"user\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"NotSupportedException\"></exception>\n    private bool ObsoleteVerifyData(User user)\n    {\n        // Do not extend this method. It is only here for backwards compatibility with old licenses.\n        if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)\n        {\n            return false;\n        }\n\n        if (Version != 1)\n        {\n            throw new NotSupportedException($\"Version {Version} is not supported.\");\n        }\n\n        return\n            user.LicenseKey != null && user.LicenseKey.Equals(LicenseKey) &&\n            user.Premium == Premium &&\n            user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase);\n    }\n\n    public bool VerifySignature(X509Certificate2 certificate)\n    {\n        using (var rsa = certificate.GetRSAPublicKey())\n        {\n            return rsa.VerifyData(GetDataBytes(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n        }\n    }\n\n    public byte[] Sign(X509Certificate2 certificate)\n    {\n        if (!certificate.HasPrivateKey)\n        {\n            throw new InvalidOperationException(\"You don't have the private key!\");\n        }\n\n        using (var rsa = certificate.GetRSAPrivateKey())\n        {\n            return rsa.SignData(GetDataBytes(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/DiscountEligibility.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Subscriptions.Entities;\n\nnamespace Bit.Core.Billing.Models;\n\n/// <summary>\n/// Pairs a <see cref=\"SubscriptionDiscount\"/> with its per-tier eligibility matrix.\n/// </summary>\npublic record DiscountEligibility(\n    SubscriptionDiscount Discount,\n    IDictionary<DiscountTierType, bool> TierEligibility);\n"
  },
  {
    "path": "src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Models.Mail;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Billing.Models.Mail;\n\npublic class TrialInitiationVerifyEmail : RegisterVerifyEmail\n{\n    public bool IsExistingUser { get; set; }\n    /// <summary>\n    /// See comment on <see cref=\"RegisterVerifyEmail\"/>.<see cref=\"RegisterVerifyEmail.Url\"/>\n    /// </summary>\n    public new string Url\n    {\n        get => $\"{WebVaultUrl}/{Route}\" +\n               $\"?token={Token}\" +\n               $\"&email={Email}\" +\n               $\"&fromEmail=true\" +\n               $\"&productTier={(int)ProductTier}\" +\n               $\"&product={string.Join(\",\", Product.Select(p => (int)p))}\" +\n               $\"&trialLength={TrialLength}\";\n    }\n\n    public string VerifyYourEmailHTMLCopy =>\n        TrialLength == 7\n            ? \"Verify your email address below to finish signing up for your free trial.\"\n            : $\"Verify your email address below to finish signing up for your {ProductTier.GetDisplayName()} plan.\";\n\n    public string VerifyYourEmailTextCopy =>\n        TrialLength == 7\n            ? \"Verify your email address using the link below and start your free trial of Bitwarden.\"\n            : $\"Verify your email address using the link below and start your {ProductTier.GetDisplayName()} Bitwarden plan.\";\n\n    public ProductTierType ProductTier { get; set; }\n\n    public IEnumerable<ProductType> Product { get; set; }\n\n    public int TrialLength { get; set; }\n\n    /// <summary>\n    /// Currently we only support one product type at a time, despite Product being a collection.\n    /// If we receive both PasswordManager and SecretsManager, we'll send the user to the PM trial route\n    /// </summary>\n    private string Route\n    {\n        get\n        {\n            if (IsExistingUser)\n            {\n                return \"create-organization\";\n            }\n\n            return Product.Any(p => p == ProductType.PasswordManager)\n                ? \"trial-initiation\"\n                : \"secrets-manager-trial-initiation\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/OffboardingSurveyResponse.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Billing.Models;\n\npublic class OffboardingSurveyResponse\n{\n    public Guid UserId { get; set; }\n    public string Reason { get; set; }\n    public string Feedback { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/PaymentMethod.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Tax.Models;\n\nnamespace Bit.Core.Billing.Models;\n\npublic record PaymentMethod(\n    decimal AccountCredit,\n    PaymentSource PaymentSource,\n    string SubscriptionStatus,\n    TaxInformation TaxInformation)\n{\n    public static PaymentMethod Empty => new(0, null, null, null);\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/PaymentSource.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Billing.Models;\n\npublic record PaymentSource(\n    PaymentMethodType Type,\n    string Description,\n    bool NeedsVerification)\n{\n    public static PaymentSource From(Stripe.Customer customer)\n    {\n        var defaultPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod;\n\n        if (defaultPaymentMethod == null)\n        {\n            return customer.DefaultSource != null ? FromStripeLegacyPaymentSource(customer.DefaultSource) : null;\n        }\n\n        return defaultPaymentMethod.Type switch\n        {\n            \"card\" => FromStripeCardPaymentMethod(defaultPaymentMethod.Card),\n            \"us_bank_account\" => FromStripeBankAccountPaymentMethod(defaultPaymentMethod.UsBankAccount),\n            _ => null\n        };\n    }\n\n    public static PaymentSource From(Stripe.SetupIntent setupIntent)\n    {\n        if (!setupIntent.IsUnverifiedBankAccount())\n        {\n            return null;\n        }\n\n        var bankAccount = setupIntent.PaymentMethod.UsBankAccount;\n\n        var description = $\"{bankAccount.BankName}, *{bankAccount.Last4}\";\n\n        return new PaymentSource(\n            PaymentMethodType.BankAccount,\n            description,\n            true);\n    }\n\n    public static PaymentSource From(Braintree.Customer customer)\n    {\n        var defaultPaymentMethod = customer.DefaultPaymentMethod;\n\n        if (defaultPaymentMethod == null)\n        {\n            return null;\n        }\n\n        switch (defaultPaymentMethod)\n        {\n            case Braintree.PayPalAccount payPalAccount:\n                {\n                    return new PaymentSource(\n                        PaymentMethodType.PayPal,\n                        payPalAccount.Email,\n                        false);\n                }\n            case Braintree.CreditCard creditCard:\n                {\n                    var paddedExpirationMonth = creditCard.ExpirationMonth.PadLeft(2, '0');\n\n                    var description =\n                        $\"{creditCard.CardType}, *{creditCard.LastFour}, {paddedExpirationMonth}/{creditCard.ExpirationYear}\";\n\n                    return new PaymentSource(\n                        PaymentMethodType.Card,\n                        description,\n                        false);\n                }\n            case Braintree.UsBankAccount bankAccount:\n                {\n                    return new PaymentSource(\n                        PaymentMethodType.BankAccount,\n                        $\"{bankAccount.BankName}, *{bankAccount.Last4}\",\n                        false);\n                }\n            default:\n                {\n                    return null;\n                }\n        }\n    }\n\n    private static PaymentSource FromStripeBankAccountPaymentMethod(\n        Stripe.PaymentMethodUsBankAccount bankAccount)\n    {\n        var description = $\"{bankAccount.BankName}, *{bankAccount.Last4}\";\n\n        return new PaymentSource(\n            PaymentMethodType.BankAccount,\n            description,\n            false);\n    }\n\n    private static PaymentSource FromStripeCardPaymentMethod(Stripe.PaymentMethodCard card)\n        => new(\n            PaymentMethodType.Card,\n            GetCardDescription(card.Brand, card.Last4, card.ExpMonth, card.ExpYear),\n            false);\n\n    #region Legacy Source Payments\n\n    private static PaymentSource FromStripeLegacyPaymentSource(Stripe.IPaymentSource paymentSource)\n        => paymentSource switch\n        {\n            Stripe.BankAccount bankAccount => FromStripeBankAccountLegacySource(bankAccount),\n            Stripe.Card card => FromStripeCardLegacySource(card),\n            Stripe.Source { Card: not null } source => FromStripeSourceCardLegacySource(source.Card),\n            _ => null\n        };\n\n    private static PaymentSource FromStripeBankAccountLegacySource(Stripe.BankAccount bankAccount)\n    {\n        var status = bankAccount.Status switch\n        {\n            \"verified\" => \"Verified\",\n            \"errored\" => \"Invalid\",\n            \"verification_failed\" => \"Verification failed\",\n            _ => \"Unverified\"\n        };\n\n        var description = $\"{bankAccount.BankName}, *{bankAccount.Last4} - {status}\";\n\n        var needsVerification = bankAccount.Status is \"new\" or \"validated\";\n\n        return new PaymentSource(\n            PaymentMethodType.BankAccount,\n            description,\n            needsVerification);\n    }\n\n    private static PaymentSource FromStripeCardLegacySource(Stripe.Card card)\n        => new(\n            PaymentMethodType.Card,\n            GetCardDescription(card.Brand, card.Last4, card.ExpMonth, card.ExpYear),\n            false);\n\n    private static PaymentSource FromStripeSourceCardLegacySource(Stripe.SourceCard card)\n        => new(\n            PaymentMethodType.Card,\n            GetCardDescription(card.Brand, card.Last4, card.ExpMonth, card.ExpYear),\n            false);\n\n    #endregion\n\n    private static string GetCardDescription(\n        string brand,\n        string last4,\n        long expirationMonth,\n        long expirationYear) => $\"{brand.ToUpperInvariant()}, *{last4}, {expirationMonth:00}/{expirationYear}\";\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/PremiumStatusPushNotification.cs",
    "content": "﻿namespace Bit.Core.Billing.Models;\n\npublic class PremiumStatusPushNotification\n{\n    public Guid UserId { get; set; }\n    public bool Premium { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/PreviewInvoiceInfo.cs",
    "content": "﻿namespace Bit.Core.Billing.Models;\n\npublic record PreviewInvoiceInfo(\n    decimal EffectiveTaxRate,\n    decimal TaxableBaseAmount,\n    decimal TaxAmount,\n    decimal TotalAmount);\n"
  },
  {
    "path": "src/Core/Billing/Models/Sales/CustomerSetup.cs",
    "content": "﻿using Bit.Core.Billing.Tax.Models;\n\nnamespace Bit.Core.Billing.Models.Sales;\n\n#nullable enable\n\npublic class CustomerSetup\n{\n    public TokenizedPaymentSource? TokenizedPaymentSource { get; set; }\n    public TaxInformation? TaxInformation { get; set; }\n    public string[]? Coupons { get; set; }\n\n    public bool IsBillable => TokenizedPaymentSource != null && TaxInformation != null;\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/Sales/PremiumUserSale.cs",
    "content": "﻿using Bit.Core.Billing.Tax.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.Billing.Models.Sales;\n\n#nullable enable\n\npublic class PremiumUserSale\n{\n    private PremiumUserSale() { }\n\n    public required User User { get; set; }\n    public required CustomerSetup CustomerSetup { get; set; }\n    public short? Storage { get; set; }\n\n    public void Deconstruct(\n        out User user,\n        out CustomerSetup customerSetup,\n        out short? storage)\n    {\n        user = User;\n        customerSetup = CustomerSetup;\n        storage = Storage;\n    }\n\n    public static PremiumUserSale From(\n        User user,\n        PaymentMethodType paymentMethodType,\n        string paymentMethodToken,\n        TaxInfo taxInfo,\n        short? storage)\n    {\n        var tokenizedPaymentSource = new TokenizedPaymentSource(paymentMethodType, paymentMethodToken);\n\n        var taxInformation = TaxInformation.From(taxInfo);\n\n        return new PremiumUserSale\n        {\n            User = user,\n            CustomerSetup = new CustomerSetup\n            {\n                TokenizedPaymentSource = tokenizedPaymentSource,\n                TaxInformation = taxInformation\n            },\n            Storage = storage\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/Sales/SubscriptionSetup.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Models.Sales;\n\n#nullable enable\n\npublic class SubscriptionSetup\n{\n    public required PlanType PlanType { get; set; }\n    public required PasswordManager PasswordManagerOptions { get; set; }\n    public SecretsManager? SecretsManagerOptions { get; set; }\n    public bool SkipTrial = false;\n    public string? InitiationPath { get; set; }\n\n    public class PasswordManager\n    {\n        public required int Seats { get; set; }\n        public short? Storage { get; set; }\n        public bool? PremiumAccess { get; set; }\n    }\n\n    public class SecretsManager\n    {\n        public required int Seats { get; set; }\n        public int? ServiceAccounts { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/SponsoredPlans.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Billing.Models;\n\npublic class SponsoredPlans\n{\n    public static IEnumerable<SponsoredPlan> All { get; set; } =\n    [\n        new()\n        {\n            PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,\n            SponsoredProductTierType = ProductTierType.Families,\n            SponsoringProductTierType = ProductTierType.Enterprise,\n            StripePlanId = \"2021-family-for-enterprise-annually\",\n            UsersCanSponsor = org =>\n                org.PlanType.GetProductTier() == ProductTierType.Enterprise,\n        }\n    ];\n\n    public static SponsoredPlan Get(PlanSponsorshipType planSponsorshipType) =>\n        All.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType)!;\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/StaticStore/Plan.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Models.StaticStore;\n\npublic abstract record Plan\n{\n    public PlanType Type { get; protected init; }\n    public ProductTierType ProductTier { get; protected init; }\n    public string Name { get; protected init; }\n    public bool IsAnnual { get; protected init; }\n    // TODO: Move to the client\n    public string NameLocalizationKey { get; protected init; }\n    // TODO: Move to the client\n    public string DescriptionLocalizationKey { get; protected init; }\n    // TODO: Remove\n    public bool CanBeUsedByBusiness { get; protected init; }\n    public int? TrialPeriodDays { get; protected init; }\n    public bool HasSelfHost { get; protected init; }\n    public bool HasPolicies { get; protected init; }\n    public bool HasGroups { get; protected init; }\n    public bool HasDirectory { get; protected init; }\n    public bool HasEvents { get; protected init; }\n    public bool HasTotp { get; protected init; }\n    public bool Has2fa { get; protected init; }\n    public bool HasApi { get; protected init; }\n    public bool HasSso { get; protected init; }\n    public bool HasOrganizationDomains { get; protected init; }\n    public bool HasKeyConnector { get; protected init; }\n    public bool HasScim { get; protected init; }\n    public bool HasResetPassword { get; protected init; }\n    public bool UsersGetPremium { get; protected init; }\n    public bool HasCustomPermissions { get; protected init; }\n    public bool HasMyItems { get; protected init; }\n    public int UpgradeSortOrder { get; protected init; }\n    // TODO: Move to the client\n    public int DisplaySortOrder { get; protected init; }\n    // TODO: Remove\n    public int? LegacyYear { get; protected init; }\n    public bool Disabled { get; protected init; }\n    public PasswordManagerPlanFeatures PasswordManager { get; protected init; }\n    public SecretsManagerPlanFeatures SecretsManager { get; protected init; }\n    public bool SupportsSecretsManager => SecretsManager != null;\n\n    public bool AutomaticUserConfirmation { get; init; }\n\n    public bool HasNonSeatBasedPasswordManagerPlan() =>\n        PasswordManager is { StripePlanId: not null and not \"\", StripeSeatPlanId: null or \"\" };\n\n    public record SecretsManagerPlanFeatures\n    {\n        // Service accounts\n        public short? MaxServiceAccounts { get; init; }\n        public bool AllowServiceAccountsAutoscale { get; init; }\n        public string StripeServiceAccountPlanId { get; init; }\n        public decimal? AdditionalPricePerServiceAccount { get; init; }\n        public short BaseServiceAccount { get; init; }\n        // TODO: Unused, remove\n        public short? MaxAdditionalServiceAccount { get; init; }\n        public bool HasAdditionalServiceAccountOption { get; init; }\n        // Seats\n        public string StripeSeatPlanId { get; init; }\n        public bool HasAdditionalSeatsOption { get; init; }\n        // TODO: Remove, SM is never packaged\n        public decimal BasePrice { get; init; }\n        public decimal SeatPrice { get; init; }\n        // TODO: Remove, SM is never packaged\n        public int BaseSeats { get; init; }\n        public short? MaxSeats { get; init; }\n        // TODO: Unused, remove\n        public int? MaxAdditionalSeats { get; init; }\n        public bool AllowSeatAutoscale { get; init; }\n\n        // Features\n        public int MaxProjects { get; init; }\n    }\n\n    public record PasswordManagerPlanFeatures\n    {\n        // Seats\n        public string StripePlanId { get; init; }\n        public string StripeSeatPlanId { get; init; }\n        [Obsolete(\"No longer used to retrieve a provider's price ID. Use ProviderPriceAdapter instead.\")]\n        public string StripeProviderPortalSeatPlanId { get; init; }\n        public decimal BasePrice { get; init; }\n        public decimal SeatPrice { get; init; }\n        public decimal ProviderPortalSeatPrice { get; init; }\n        public bool AllowSeatAutoscale { get; init; }\n        public bool HasAdditionalSeatsOption { get; init; }\n        // TODO: Remove, never set.\n        public int? MaxAdditionalSeats { get; init; }\n        public int BaseSeats { get; init; }\n        // TODO: Remove premium access as it's deprecated\n        public bool HasPremiumAccessOption { get; init; }\n        public string StripePremiumAccessPlanId { get; init; }\n        public decimal PremiumAccessOptionPrice { get; init; }\n        public short? MaxSeats { get; init; }\n        // Storage\n        public short BaseStorageGb { get; init; }\n        public bool HasAdditionalStorageOption { get; init; }\n        public decimal AdditionalStoragePricePerGb { get; init; }\n        public string StripeStoragePlanId { get; init; }\n        // TODO: Remove\n        public short? MaxAdditionalStorage { get; init; }\n        // Feature\n        public short? MaxCollections { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/StaticStore/SponsoredPlan.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Core.Models.StaticStore;\n\npublic class SponsoredPlan\n{\n    public PlanSponsorshipType PlanSponsorshipType { get; set; }\n    public ProductTierType SponsoredProductTierType { get; set; }\n    public ProductTierType SponsoringProductTierType { get; set; }\n    public string StripePlanId { get; set; }\n    public Func<OrganizationUserOrganizationDetails, bool> UsersCanSponsor { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Models/SubscriptionSuspension.cs",
    "content": "﻿namespace Bit.Core.Billing.Models;\n\npublic record SubscriptionSuspension(\n    DateTime SuspensionDate,\n    DateTime UnpaidPeriodEndDate,\n    int GracePeriod);\n"
  },
  {
    "path": "src/Core/Billing/Models/TokenizedPaymentSource.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.Billing.Models;\n\npublic record TokenizedPaymentSource(\n    PaymentMethodType Type,\n    string Token);\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Microsoft.Extensions.Logging;\nusing OneOf;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Organizations.Commands;\n\nusing static StripeConstants;\n\npublic interface IPreviewOrganizationTaxCommand\n{\n    Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(\n        User user,\n        OrganizationSubscriptionPurchase purchase,\n        BillingAddress billingAddress);\n\n    Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(\n        Organization organization,\n        OrganizationSubscriptionPlanChange planChange,\n        BillingAddress billingAddress);\n\n    Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(\n        Organization organization,\n        OrganizationSubscriptionUpdate update);\n}\n\npublic class PreviewOrganizationTaxCommand(\n    ILogger<PreviewOrganizationTaxCommand> logger,\n    IPricingClient pricingClient,\n    IStripeAdapter stripeAdapter,\n    ISubscriptionDiscountService subscriptionDiscountService)\n    : BaseBillingCommand<PreviewOrganizationTaxCommand>(logger), IPreviewOrganizationTaxCommand\n{\n    public Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(\n        User user,\n        OrganizationSubscriptionPurchase purchase,\n        BillingAddress billingAddress)\n        => HandleAsync<(decimal, decimal)>(async () =>\n        {\n            var plan = await pricingClient.GetPlanOrThrow(purchase.PlanType);\n\n            var options = GetBaseOptions(billingAddress, purchase.Tier != ProductTierType.Families);\n\n            var items = new List<InvoiceSubscriptionDetailsItemOptions>();\n\n            switch (purchase)\n            {\n                case { PasswordManager.Sponsored: true }:\n                    var sponsoredPlan = SponsoredPlans.Get(PlanSponsorshipType.FamiliesForEnterprise);\n                    items.Add(new InvoiceSubscriptionDetailsItemOptions\n                    {\n                        Price = sponsoredPlan.StripePlanId,\n                        Quantity = 1\n                    });\n                    break;\n\n                case { SecretsManager.Standalone: true }:\n                    items.AddRange([\n                        new InvoiceSubscriptionDetailsItemOptions\n                        {\n                            Price = plan.PasswordManager.StripeSeatPlanId,\n                            Quantity = purchase.PasswordManager.Seats\n                        },\n                        new InvoiceSubscriptionDetailsItemOptions\n                        {\n                            Price = plan.SecretsManager.StripeSeatPlanId,\n                            Quantity = purchase.SecretsManager.Seats\n                        }\n                    ]);\n                    // System coupon takes precedence for standalone Secrets Manager purchases.\n                    // Any user-provided coupons are ignored in this scenario.\n                    options.Discounts =\n                    [\n                        new InvoiceDiscountOptions\n                        {\n                            Coupon = CouponIDs.SecretsManagerStandalone\n                        }\n                    ];\n                    break;\n\n                default:\n                    items.Add(new InvoiceSubscriptionDetailsItemOptions\n                    {\n                        Price = plan.HasNonSeatBasedPasswordManagerPlan()\n                            ? plan.PasswordManager.StripePlanId\n                            : plan.PasswordManager.StripeSeatPlanId,\n                        Quantity = purchase.PasswordManager.Seats\n                    });\n\n                    if (purchase.PasswordManager.AdditionalStorage > 0)\n                    {\n                        items.Add(new InvoiceSubscriptionDetailsItemOptions\n                        {\n                            Price = plan.PasswordManager.StripeStoragePlanId,\n                            Quantity = purchase.PasswordManager.AdditionalStorage\n                        });\n                    }\n\n                    if (purchase.SecretsManager is { Seats: > 0 })\n                    {\n                        items.Add(new InvoiceSubscriptionDetailsItemOptions\n                        {\n                            Price = plan.SecretsManager.StripeSeatPlanId,\n                            Quantity = purchase.SecretsManager.Seats\n                        });\n\n                        if (purchase.SecretsManager.AdditionalServiceAccounts > 0)\n                        {\n                            items.Add(new InvoiceSubscriptionDetailsItemOptions\n                            {\n                                Price = plan.SecretsManager.StripeServiceAccountPlanId,\n                                Quantity = purchase.SecretsManager.AdditionalServiceAccounts\n                            });\n                        }\n                    }\n\n                    // Validate all coupons at once. If all are eligible, apply them; otherwise skip gracefully.\n                    // Only Families plans support user-provided coupons.\n                    if (purchase is { Coupons.Length: > 0, Tier: ProductTierType.Families })\n                    {\n                        var trimmedCoupons = purchase.Coupons\n                            .Where(c => !string.IsNullOrWhiteSpace(c))\n                            .Select(c => c.Trim())\n                            .ToArray();\n\n                        if (trimmedCoupons.Length > 0)\n                        {\n                            var allValid = await subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n                                user, trimmedCoupons, DiscountTierType.Families);\n\n                            if (allValid)\n                            {\n                                options.Discounts = trimmedCoupons\n                                    .Select(c => new InvoiceDiscountOptions { Coupon = c })\n                                    .ToList();\n                            }\n                        }\n                    }\n\n                    break;\n            }\n\n            options.SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = items };\n\n            var invoice = await stripeAdapter.CreateInvoicePreviewAsync(options);\n            return GetAmounts(invoice);\n        });\n\n    public Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(\n        Organization organization,\n        OrganizationSubscriptionPlanChange planChange,\n        BillingAddress billingAddress)\n        => HandleAsync<(decimal, decimal)>(async () =>\n        {\n            if (organization.PlanType.GetProductTier() == ProductTierType.Free)\n            {\n                var options = GetBaseOptions(billingAddress, planChange.Tier != ProductTierType.Families);\n\n                var newPlan = await pricingClient.GetPlanOrThrow(planChange.PlanType);\n\n                var quantity = newPlan.HasNonSeatBasedPasswordManagerPlan() ? 1 : 2;\n\n                var items = new List<InvoiceSubscriptionDetailsItemOptions>\n                {\n                    new ()\n                    {\n                        Price = newPlan.HasNonSeatBasedPasswordManagerPlan()\n                            ? newPlan.PasswordManager.StripePlanId\n                            : newPlan.PasswordManager.StripeSeatPlanId,\n                        Quantity = quantity\n                    }\n                };\n\n                if (organization.UseSecretsManager && planChange.Tier != ProductTierType.Families)\n                {\n                    items.Add(new InvoiceSubscriptionDetailsItemOptions\n                    {\n                        Price = newPlan.SecretsManager.StripeSeatPlanId,\n                        Quantity = 2\n                    });\n                }\n\n                options.SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = items };\n\n                var invoice = await stripeAdapter.CreateInvoicePreviewAsync(options);\n                return GetAmounts(invoice);\n            }\n            else\n            {\n                if (organization is not\n                    {\n                        GatewayCustomerId: not null,\n                        GatewaySubscriptionId: not null\n                    })\n                {\n                    return new BadRequest(\"Organization does not have a subscription.\");\n                }\n\n                var options = GetBaseOptions(billingAddress, planChange.Tier != ProductTierType.Families);\n\n                var subscription = await stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId,\n                    new SubscriptionGetOptions { Expand = [\"customer\"] });\n\n                if (subscription.Customer.Discount != null)\n                {\n                    options.Discounts =\n                    [\n                        new InvoiceDiscountOptions { Coupon = subscription.Customer.Discount.Coupon.Id }\n                    ];\n                }\n\n                var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n                var newPlan = await pricingClient.GetPlanOrThrow(planChange.PlanType);\n\n                var subscriptionItemsByPriceId =\n                    subscription.Items.ToDictionary(subscriptionItem => subscriptionItem.Price.Id);\n\n                var items = new List<InvoiceSubscriptionDetailsItemOptions>();\n\n                var passwordManagerSeats = subscriptionItemsByPriceId[\n                    currentPlan.HasNonSeatBasedPasswordManagerPlan()\n                        ? currentPlan.PasswordManager.StripePlanId\n                        : currentPlan.PasswordManager.StripeSeatPlanId];\n\n                var quantity = currentPlan.HasNonSeatBasedPasswordManagerPlan() &&\n                               !newPlan.HasNonSeatBasedPasswordManagerPlan()\n                    ? (long)organization.Seats!\n                    : passwordManagerSeats.Quantity;\n\n                items.Add(new InvoiceSubscriptionDetailsItemOptions\n                {\n                    Price = newPlan.HasNonSeatBasedPasswordManagerPlan()\n                        ? newPlan.PasswordManager.StripePlanId\n                        : newPlan.PasswordManager.StripeSeatPlanId,\n                    Quantity = quantity\n                });\n\n                var hasStorage =\n                    subscriptionItemsByPriceId.TryGetValue(newPlan.PasswordManager.StripeStoragePlanId,\n                        out var storage);\n\n                if (hasStorage && storage != null)\n                {\n                    items.Add(new InvoiceSubscriptionDetailsItemOptions\n                    {\n                        Price = newPlan.PasswordManager.StripeStoragePlanId,\n                        Quantity = storage.Quantity\n                    });\n                }\n\n                var hasSecretsManagerSeats = subscriptionItemsByPriceId.TryGetValue(\n                    newPlan.SecretsManager.StripeSeatPlanId,\n                    out var secretsManagerSeats);\n\n                if (hasSecretsManagerSeats && secretsManagerSeats != null)\n                {\n                    items.Add(new InvoiceSubscriptionDetailsItemOptions\n                    {\n                        Price = newPlan.SecretsManager.StripeSeatPlanId,\n                        Quantity = secretsManagerSeats.Quantity\n                    });\n\n                    var hasServiceAccounts =\n                        subscriptionItemsByPriceId.TryGetValue(newPlan.SecretsManager.StripeServiceAccountPlanId,\n                            out var serviceAccounts);\n\n                    if (hasServiceAccounts && serviceAccounts != null)\n                    {\n                        items.Add(new InvoiceSubscriptionDetailsItemOptions\n                        {\n                            Price = newPlan.SecretsManager.StripeServiceAccountPlanId,\n                            Quantity = serviceAccounts.Quantity\n                        });\n                    }\n                }\n\n                options.SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = items };\n\n                var invoice = await stripeAdapter.CreateInvoicePreviewAsync(options);\n                return GetAmounts(invoice);\n            }\n        });\n\n    public Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(\n        Organization organization,\n        OrganizationSubscriptionUpdate update)\n        => HandleAsync<(decimal, decimal)>(async () =>\n        {\n            if (organization is not\n                {\n                    GatewayCustomerId: not null,\n                    GatewaySubscriptionId: not null\n                })\n            {\n                return new BadRequest(\"Organization does not have a subscription.\");\n            }\n\n            var subscription = await stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId,\n                new SubscriptionGetOptions { Expand = [\"customer.tax_ids\"] });\n\n            var options = GetBaseOptions(subscription.Customer,\n                organization.GetProductUsageType() == ProductUsageType.Business);\n\n            if (subscription.Customer.Discount != null)\n            {\n                options.Discounts =\n                [\n                    new InvoiceDiscountOptions { Coupon = subscription.Customer.Discount.Coupon.Id }\n                ];\n            }\n\n            var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n            var items = new List<InvoiceSubscriptionDetailsItemOptions>();\n\n            if (update.PasswordManager?.Seats != null)\n            {\n                items.Add(new InvoiceSubscriptionDetailsItemOptions\n                {\n                    Price = currentPlan.HasNonSeatBasedPasswordManagerPlan()\n                        ? currentPlan.PasswordManager.StripePlanId\n                        : currentPlan.PasswordManager.StripeSeatPlanId,\n                    Quantity = update.PasswordManager.Seats\n                });\n            }\n\n            if (update.PasswordManager?.AdditionalStorage is > 0)\n            {\n                items.Add(new InvoiceSubscriptionDetailsItemOptions\n                {\n                    Price = currentPlan.PasswordManager.StripeStoragePlanId,\n                    Quantity = update.PasswordManager.AdditionalStorage\n                });\n            }\n\n            if (update.SecretsManager?.Seats is > 0)\n            {\n                items.Add(new InvoiceSubscriptionDetailsItemOptions\n                {\n                    Price = currentPlan.SecretsManager.StripeSeatPlanId,\n                    Quantity = update.SecretsManager.Seats\n                });\n\n                if (update.SecretsManager.AdditionalServiceAccounts is > 0)\n                {\n                    items.Add(new InvoiceSubscriptionDetailsItemOptions\n                    {\n                        Price = currentPlan.SecretsManager.StripeServiceAccountPlanId,\n                        Quantity = update.SecretsManager.AdditionalServiceAccounts\n                    });\n                }\n            }\n\n            options.SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = items };\n\n            var invoice = await stripeAdapter.CreateInvoicePreviewAsync(options);\n            return GetAmounts(invoice);\n        });\n\n    private static (decimal, decimal) GetAmounts(Invoice invoice) => (\n        Convert.ToDecimal(invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount)) / 100,\n        Convert.ToDecimal(invoice.Total) / 100);\n\n    private static InvoiceCreatePreviewOptions GetBaseOptions(\n        OneOf<Customer, BillingAddress> addressChoice,\n        bool businessUse)\n    {\n        var country = addressChoice.Match(\n            customer => customer.Address.Country,\n            billingAddress => billingAddress.Country\n        );\n\n        var postalCode = addressChoice.Match(\n            customer => customer.Address.PostalCode,\n            billingAddress => billingAddress.PostalCode);\n\n        var options = new InvoiceCreatePreviewOptions\n        {\n            AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true },\n            Currency = \"usd\",\n            CustomerDetails = new InvoiceCustomerDetailsOptions\n            {\n                Address = new AddressOptions { Country = country, PostalCode = postalCode },\n            }\n        };\n\n        switch (businessUse)\n        {\n            case true:\n                var existingTaxExemptStatus = addressChoice.Match(\n                    customer => customer.TaxExempt,\n                    _ => null!);\n\n                var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(country, existingTaxExemptStatus);\n                options.CustomerDetails.TaxExempt = determinedTaxExemptStatus;\n                break;\n            default:\n                options.CustomerDetails.TaxExempt = TaxExempt.None;\n                break;\n        }\n\n        var taxId = addressChoice.Match(\n            customer =>\n            {\n                var taxId = customer.TaxIds?.FirstOrDefault();\n                return taxId != null ? new TaxID(taxId.Type, taxId.Value) : null;\n            },\n            billingAddress => billingAddress.TaxId);\n\n        if (taxId == null)\n        {\n            return options;\n        }\n\n        options.CustomerDetails.TaxIds =\n        [\n            new InvoiceCustomerDetailsTaxIdOptions { Type = taxId.Code, Value = taxId.Value }\n        ];\n\n        if (taxId.Code == TaxIdType.SpanishNIF)\n        {\n            options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions\n            {\n                Type = TaxIdType.EUVAT,\n                Value = $\"ES{taxId.Value}\"\n            });\n        }\n\n        return options;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Licenses;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Billing.Organizations.Commands;\n\npublic interface IUpdateOrganizationLicenseCommand\n{\n    Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,\n        OrganizationLicense license, Organization? currentOrganizationUsingLicenseKey);\n}\n\npublic class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseCommand\n{\n    private readonly ILicensingService _licensingService;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IOrganizationService _organizationService;\n    private readonly IFeatureService _featureService;\n\n    public UpdateOrganizationLicenseCommand(\n        ILicensingService licensingService,\n        IGlobalSettings globalSettings,\n        IOrganizationService organizationService,\n        IFeatureService featureService)\n    {\n        _licensingService = licensingService;\n        _globalSettings = globalSettings;\n        _organizationService = organizationService;\n        _featureService = featureService;\n    }\n\n    public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,\n        OrganizationLicense license, Organization? currentOrganizationUsingLicenseKey)\n    {\n        if (currentOrganizationUsingLicenseKey != null && currentOrganizationUsingLicenseKey.Id != selfHostedOrganization.Id)\n        {\n            throw new BadRequestException(\"License is already in use by another organization.\");\n        }\n\n        var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);\n\n        // If the license has a Token (claims-based), extract all properties from claims BEFORE validation\n        // This ensures that CanUseLicense validation has access to the correct values from claims\n        // Otherwise, fall back to using the properties already on the license object (backward compatibility)\n        if (claimsPrincipal != null)\n        {\n            license.Name = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.Name);\n            license.BillingEmail = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.BillingEmail);\n            license.BusinessName = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.BusinessName);\n            license.PlanType = claimsPrincipal.GetValue<PlanType>(OrganizationLicenseConstants.PlanType);\n            license.Seats = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.Seats);\n            license.MaxCollections = claimsPrincipal.GetValue<short?>(OrganizationLicenseConstants.MaxCollections);\n            license.UsePolicies = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePolicies);\n            license.UseSso = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseSso);\n            license.UseKeyConnector = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseKeyConnector);\n            license.UseScim = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseScim);\n            license.UseGroups = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseGroups);\n            license.UseDirectory = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseDirectory);\n            license.UseEvents = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseEvents);\n            license.UseTotp = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseTotp);\n            license.Use2fa = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.Use2fa);\n            license.UseApi = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseApi);\n            license.UseResetPassword = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseResetPassword);\n            license.Plan = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.Plan);\n            license.SelfHost = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.SelfHost);\n            license.UsersGetPremium = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsersGetPremium);\n            license.UseCustomPermissions = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseCustomPermissions);\n            license.Enabled = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.Enabled);\n            license.Expires = claimsPrincipal.GetValue<DateTime?>(OrganizationLicenseConstants.Expires);\n            license.LicenseKey = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.LicenseKey);\n            license.UsePasswordManager = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePasswordManager);\n            license.UseSecretsManager = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseSecretsManager);\n            license.SmSeats = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.SmSeats);\n            license.SmServiceAccounts = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.SmServiceAccounts);\n            license.UseRiskInsights = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseRiskInsights);\n            license.UseOrganizationDomains = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseOrganizationDomains);\n            license.UseAdminSponsoredFamilies = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAdminSponsoredFamilies);\n            license.UseAutomaticUserConfirmation = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAutomaticUserConfirmation);\n            license.UseDisableSmAdsForUsers = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseDisableSmAdsForUsers);\n            license.UsePhishingBlocker = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePhishingBlocker);\n            license.UseMyItems = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseMyItems);\n            license.MaxStorageGb = claimsPrincipal.GetValue<short?>(OrganizationLicenseConstants.MaxStorageGb);\n            license.InstallationId = claimsPrincipal.GetValue<Guid>(OrganizationLicenseConstants.InstallationId);\n            license.LicenseType = claimsPrincipal.GetValue<LicenseType>(OrganizationLicenseConstants.LicenseType);\n            license.Issued = claimsPrincipal.GetValue<DateTime>(OrganizationLicenseConstants.Issued);\n            license.Refresh = claimsPrincipal.GetValue<DateTime?>(OrganizationLicenseConstants.Refresh);\n            license.ExpirationWithoutGracePeriod = claimsPrincipal.GetValue<DateTime?>(OrganizationLicenseConstants.ExpirationWithoutGracePeriod);\n            license.Trial = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.Trial);\n            license.LimitCollectionCreationDeletion = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.LimitCollectionCreationDeletion);\n            license.AllowAdminAccessToAllCollectionItems = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems);\n        }\n\n        var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception) &&\n            selfHostedOrganization.CanUseLicense(license, out exception);\n\n        if (!canUse)\n        {\n            throw new BadRequestException(exception);\n        }\n\n        await WriteLicenseFileAsync(selfHostedOrganization, license);\n        await UpdateOrganizationAsync(selfHostedOrganization, license);\n    }\n\n    private async Task WriteLicenseFileAsync(Organization organization, OrganizationLicense license)\n    {\n        var dir = $\"{_globalSettings.LicenseDirectory}/organization\";\n        Directory.CreateDirectory(dir);\n        await using var fs = new FileStream(Path.Combine(dir, $\"{organization.Id}.json\"), FileMode.Create);\n        await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);\n    }\n\n    private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)\n    {\n        var organization = selfHostedOrganizationDetails.ToOrganization();\n\n        organization.UpdateFromLicense(license, _featureService);\n\n        await _organizationService.ReplaceAndUpdateCacheAsync(organization);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Commands/UpdateOrganizationSubscriptionCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Microsoft.Extensions.Logging;\nusing OneOf;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Organizations.Commands;\n\nusing static StripeConstants;\n\n/// <summary>\n/// Updates an organization's Stripe subscription based on a set of changes described by an\n/// <see cref=\"OrganizationSubscriptionChangeSet\"/>. Handles adding, removing, and updating\n/// subscription items as well as proration, invoice finalization, and tax exemption reconciliation.\n/// </summary>\npublic interface IUpdateOrganizationSubscriptionCommand\n{\n    /// <summary>\n    /// Applies the provided <paramref name=\"changeSet\"/> to the organization's Stripe subscription.\n    /// </summary>\n    /// <param name=\"organization\">The organization whose subscription will be updated.</param>\n    /// <param name=\"changeSet\">The set of changes to apply to the subscription.</param>\n    /// <returns>\n    /// A <see cref=\"BillingCommandResult{T}\"/> containing the updated <see cref=\"Subscription\"/>\n    /// on success, or an error result if validation or the Stripe operation fails.\n    /// </returns>\n    Task<BillingCommandResult<Subscription>> Run(\n        Organization organization,\n        OrganizationSubscriptionChangeSet changeSet);\n}\n\npublic class UpdateOrganizationSubscriptionCommand(\n    ILogger<UpdateOrganizationSubscriptionCommand> logger,\n    IStripeAdapter stripeAdapter) : BaseBillingCommand<UpdateOrganizationSubscriptionCommand>(logger), IUpdateOrganizationSubscriptionCommand\n{\n    private static readonly List<string> _validSubscriptionStatusesForUpdate =\n    [\n        SubscriptionStatus.Trialing,\n        SubscriptionStatus.Active,\n        SubscriptionStatus.PastDue\n    ];\n\n    private readonly ILogger<UpdateOrganizationSubscriptionCommand> _logger = logger;\n\n    protected override Conflict DefaultConflict =>\n        new(\"We had a problem updating your subscription. Please contact support for assistance.\");\n\n    public Task<BillingCommandResult<Subscription>> Run(\n        Organization organization,\n        OrganizationSubscriptionChangeSet changeSet) => HandleAsync<Subscription>(async () =>\n    {\n        var subscription = await FetchSubscriptionAsync(organization);\n\n        if (subscription is null)\n        {\n            return new BadRequest(\"We couldn't find your subscription.\");\n        }\n\n        if (!_validSubscriptionStatusesForUpdate.Contains(subscription.Status))\n        {\n            _logger.LogWarning(\n                \"{Command}: Tried to update organization ({OrganizationId}) subscription ({SubscriptionId}) with status ({SubscriptionStatus})\",\n                CommandName, organization.Id, subscription.Id, subscription.Status);\n            return new BadRequest(\"Your subscription cannot be updated in its current status.\");\n        }\n\n        if (changeSet.Changes.Count == 0)\n        {\n            _logger.LogWarning(\n                \"{Command}: Change set for organization ({OrganizationId}) subscription ({SubscriptionId}) contained zero changes\",\n                CommandName, organization.Id, subscription.Id);\n            return new Conflict(\"No changes were provided for the organization subscription update\");\n        }\n\n        await ReconcileTaxExemptionAsync(subscription.Customer);\n\n        var hasStructuralChanges = changeSet.Changes.Any(change => change.IsStructural);\n        var isChargedAutomatically = subscription.CollectionMethod == CollectionMethod.ChargeAutomatically;\n        var isBilledAnnually = subscription.Items.FirstOrDefault()?.Price.Recurring?.Interval == Intervals.Year;\n\n        var prorationBehavior =\n            hasStructuralChanges ? ProrationBehavior.AlwaysInvoice : ProrationBehavior.CreateProrations;\n        var paymentBehavior =\n            hasStructuralChanges && isChargedAutomatically ? PaymentBehavior.PendingIfIncomplete : null;\n\n        var items = new List<SubscriptionItemOptions>();\n        foreach (var change in changeSet.Changes)\n        {\n            var validationResult = change.Match(\n                addItem => ValidateItemAddition(addItem, subscription),\n                changeItemPrice => ValidateItemPriceChange(changeItemPrice, subscription),\n                removeItem => ValidateItemRemoval(removeItem, subscription),\n                updateItemQuantity => ValidateItemQuantityUpdate(updateItemQuantity, subscription));\n\n            if (validationResult.IsT1)\n            {\n                return validationResult.AsT1;\n            }\n\n            items.Add(validationResult.AsT0);\n        }\n\n        var options = new SubscriptionUpdateOptions { Items = items, ProrationBehavior = prorationBehavior };\n\n        if (paymentBehavior is not null)\n        {\n            options.PaymentBehavior = paymentBehavior;\n        }\n\n        if (isBilledAnnually && !hasStructuralChanges && subscription.Status != SubscriptionStatus.Trialing)\n        {\n            options.PendingInvoiceItemInterval = new SubscriptionPendingInvoiceItemIntervalOptions\n            {\n                Interval = Intervals.Month\n            };\n        }\n\n        var updatedSubscription = await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, options);\n\n        // ReSharper disable once InvertIf\n        if (!isChargedAutomatically && hasStructuralChanges && updatedSubscription.LatestInvoiceId is not null)\n        {\n            var invoice = await stripeAdapter.GetInvoiceAsync(updatedSubscription.LatestInvoiceId);\n\n            if (invoice is { Status: InvoiceStatus.Draft })\n            {\n                var finalizedInvoice = await stripeAdapter.FinalizeInvoiceAsync(invoice.Id,\n                    new InvoiceFinalizeOptions { AutoAdvance = false });\n\n                await stripeAdapter.SendInvoiceAsync(finalizedInvoice.Id);\n            }\n            else\n            {\n                _logger.LogWarning(\n                    \"{Command}: Latest invoice ({InvoiceId}) after subscription ({SubscriptionId}) update for organization ({OrganizationId}) was in '{Status}' status\",\n                    CommandName, invoice.Id, subscription.Id, organization.Id, invoice.Status);\n            }\n        }\n\n        return updatedSubscription;\n    });\n\n    private async Task<Subscription?> FetchSubscriptionAsync(Organization organization)\n    {\n        try\n        {\n            return await stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId, new SubscriptionGetOptions\n            {\n                Expand = [\"customer\"]\n            });\n        }\n        catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.ResourceMissing)\n        {\n            _logger.LogError(\"{Command}: Subscription ({SubscriptionId}) for Organization ({OrganizationId}) was not found\",\n                CommandName, organization.GatewaySubscriptionId, organization.Id);\n            return null;\n        }\n    }\n\n    private async Task ReconcileTaxExemptionAsync(Customer customer)\n    {\n        var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt);\n        switch (customer)\n        {\n            case { Address.Country: not null and not \"\", TaxExempt: var customerTaxExemptStatus }\n                when determinedTaxExemptStatus != customerTaxExemptStatus:\n                await stripeAdapter.UpdateCustomerAsync(customer.Id,\n                    new CustomerUpdateOptions { TaxExempt = determinedTaxExemptStatus });\n                break;\n        }\n\n    }\n\n    private static OneOf<SubscriptionItemOptions, BadRequest> ValidateItemAddition(\n        AddItem addItem, Subscription subscription)\n    {\n        var duplicate = subscription.Items.Data\n            .FirstOrDefault(i => i.Price.Id == addItem.PriceId);\n\n        if (duplicate is not null)\n        {\n            return new BadRequest($\"Subscription already contains an item with price '{addItem.PriceId}'.\");\n        }\n\n        return new SubscriptionItemOptions\n        {\n            Price = addItem.PriceId,\n            Quantity = addItem.Quantity\n        };\n    }\n\n    private static OneOf<SubscriptionItemOptions, BadRequest> ValidateItemPriceChange(\n        ChangeItemPrice priceChange, Subscription subscription)\n    {\n        var currentItem = subscription.Items.Data\n            .FirstOrDefault(i => i.Price.Id == priceChange.CurrentPriceId);\n\n        if (currentItem is null)\n        {\n            return new BadRequest($\"Subscription does not contain an item with price '{priceChange.CurrentPriceId}'.\");\n        }\n\n        return new SubscriptionItemOptions\n        {\n            Id = currentItem.Id,\n            Price = priceChange.UpdatedPriceId,\n            Quantity = priceChange.Quantity ?? currentItem.Quantity\n        };\n    }\n\n    private static OneOf<SubscriptionItemOptions, BadRequest> ValidateItemQuantityUpdate(\n        UpdateItemQuantity updateItemQuantity, Subscription subscription)\n    {\n        var existingItem = subscription.Items.Data\n            .FirstOrDefault(i => i.Price.Id == updateItemQuantity.PriceId);\n\n        if (existingItem is null)\n        {\n            return new BadRequest($\"Subscription does not contain an item with price '{updateItemQuantity.PriceId}'.\");\n        }\n\n        return updateItemQuantity.Quantity == 0\n            ? new SubscriptionItemOptions { Id = existingItem.Id, Deleted = true }\n            : new SubscriptionItemOptions { Id = existingItem.Id, Price = updateItemQuantity.PriceId, Quantity = updateItemQuantity.Quantity };\n    }\n\n    private static OneOf<SubscriptionItemOptions, BadRequest> ValidateItemRemoval(\n        RemoveItem removeItem, Subscription subscription)\n    {\n        var existingItem = subscription.Items.Data\n            .FirstOrDefault(i => i.Price.Id == removeItem.PriceId);\n\n        if (existingItem is null)\n        {\n            return new BadRequest($\"Subscription does not contain an item with price '{removeItem.PriceId}'.\");\n        }\n\n        return new SubscriptionItemOptions\n        {\n            Id = existingItem.Id,\n            Deleted = true\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Commands/UpgradeOrganizationPlanVNextCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\n\nnamespace Bit.Core.Billing.Organizations.Commands;\n\n/// <summary>\n/// Upgrades an organization's subscription plan by updating its Stripe subscription\n/// and persisting the corresponding feature and configuration changes to the database.\n/// </summary>\npublic interface IUpgradeOrganizationPlanVNextCommand\n{\n    /// <summary>\n    /// Upgrades the <paramref name=\"organization\"/> to the specified <paramref name=\"plan\"/>\n    /// by applying subscription changes through <see cref=\"IUpdateOrganizationSubscriptionCommand\"/>\n    /// and updating the organization's features, limits, and encryption keys.\n    /// </summary>\n    /// <param name=\"organization\">The organization to upgrade.</param>\n    /// <param name=\"plan\">The target plan to upgrade to.</param>\n    /// <param name=\"keys\">Optional public key encryption key pair data to set during the upgrade.</param>\n    /// <returns>\n    /// A <see cref=\"BillingCommandResult{T}\"/> containing <see cref=\"None\"/> on success,\n    /// or an error result if the subscription update or feature persistence fails.\n    /// </returns>\n    Task<BillingCommandResult<None>> Run(\n        Organization organization,\n        Plan plan,\n        PublicKeyEncryptionKeyPairData? keys);\n}\n\npublic class UpgradeOrganizationPlanVNextCommand(\n    ILogger<UpgradeOrganizationPlanVNextCommand> logger,\n    IOrganizationBillingService organizationBillingService,\n    IOrganizationService organizationService,\n    IPricingClient pricingClient,\n    IUpdateOrganizationSubscriptionCommand updateOrganizationSubscriptionCommand) : BaseBillingCommand<UpgradeOrganizationPlanVNextCommand>(logger), IUpgradeOrganizationPlanVNextCommand\n{\n    protected override Conflict DefaultConflict => new(\"We had a problem upgrading your plan. Please contact support for assistance.\");\n\n    public Task<BillingCommandResult<None>> Run(\n        Organization organization,\n        Plan plan,\n        PublicKeyEncryptionKeyPairData? keys) => HandleAsync(async () =>\n    {\n        var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        if (currentPlan.UpgradeSortOrder == plan.UpgradeSortOrder)\n        {\n            return new BadRequest(\"Your organization is already on this plan.\");\n        }\n\n        if (currentPlan.UpgradeSortOrder > plan.UpgradeSortOrder)\n        {\n            return new BadRequest(\"You can't downgrade your organization's plan.\");\n        }\n\n        if (string.IsNullOrEmpty(organization.GatewayCustomerId))\n        {\n            return new Conflict($\"Organization's ({organization.Id}) Stripe customer should already have been created\");\n        }\n\n        // Upgrade from Free\n        if (currentPlan.Type == PlanType.Free && organization is\n            {\n                GatewaySubscriptionId: null,\n                Seats: not null\n            })\n        {\n            var sale = OrganizationSale.From(organization, new OrganizationUpgrade\n            {\n                Plan = plan.Type,\n                AdditionalSeats = organization.Seats ?? 0,\n                UseSecretsManager = organization.UseSecretsManager,\n                AdditionalSmSeats = organization.UseSecretsManager ? organization.SmSeats : null,\n            }, null);\n\n            await organizationBillingService.Finalize(sale);\n\n            if (plan.HasNonSeatBasedPasswordManagerPlan())\n            {\n                organization.Seats = plan.PasswordManager.BaseSeats;\n            }\n\n            organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;\n\n            if (organization.UseSecretsManager)\n            {\n                organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount;\n            }\n\n            await UpdateOrganizationFeaturesAsync(organization, plan, keys);\n\n            return new None();\n        }\n\n        var builder = OrganizationSubscriptionChangeSet.Builder();\n\n        builder.ChangeItemPrice(\n            GetPasswordManagerPriceId(currentPlan),\n            GetPasswordManagerPriceId(plan));\n\n        if (organization.MaxStorageGb > currentPlan.PasswordManager.BaseStorageGb)\n        {\n            builder.ChangeItemPrice(\n                currentPlan.PasswordManager.StripeStoragePlanId,\n                plan.PasswordManager.StripeStoragePlanId);\n        }\n\n        if (organization.UseSecretsManager)\n        {\n            builder.ChangeItemPrice(\n                currentPlan.SecretsManager.StripeSeatPlanId,\n                plan.SecretsManager.StripeSeatPlanId);\n\n            if (organization.SmServiceAccounts > currentPlan.SecretsManager.BaseServiceAccount)\n            {\n                builder.ChangeItemPrice(\n                    currentPlan.SecretsManager.StripeServiceAccountPlanId,\n                    plan.SecretsManager.StripeServiceAccountPlanId);\n            }\n        }\n\n        var changeSet = builder.Build();\n        var result = await updateOrganizationSubscriptionCommand.Run(organization, changeSet);\n\n        if (!result.Success)\n        {\n            return result.Map(_ => new None());\n        }\n\n        await UpdateOrganizationFeaturesAsync(organization, plan, keys);\n\n        return result.Map(_ => new None());\n    });\n\n    private static string GetPasswordManagerPriceId(Plan plan) =>\n        plan.HasNonSeatBasedPasswordManagerPlan()\n            ? plan.PasswordManager.StripePlanId\n            : plan.PasswordManager.StripeSeatPlanId;\n\n    private async Task UpdateOrganizationFeaturesAsync(\n        Organization organization,\n        Plan plan,\n        PublicKeyEncryptionKeyPairData? keys)\n    {\n        organization.Plan = plan.Name;\n        organization.PlanType = plan.Type;\n        organization.MaxCollections = plan.PasswordManager.MaxCollections;\n        organization.UsePolicies = plan.HasPolicies;\n        organization.UseSso = plan.HasSso;\n        organization.UseKeyConnector = plan.HasKeyConnector;\n        organization.UseScim = plan.HasScim;\n        organization.UseGroups = plan.HasGroups;\n        organization.UseDirectory = plan.HasDirectory;\n        organization.UseEvents = plan.HasEvents;\n        organization.UseTotp = plan.HasTotp;\n        organization.Use2fa = plan.Has2fa;\n        organization.UseApi = plan.HasApi;\n        organization.UseResetPassword = plan.HasResetPassword;\n        organization.SelfHost = plan.HasSelfHost;\n        organization.UsersGetPremium = plan.UsersGetPremium;\n        organization.UseCustomPermissions = plan.HasCustomPermissions;\n        organization.UseOrganizationDomains = plan.HasOrganizationDomains;\n        organization.UseAutomaticUserConfirmation = plan.AutomaticUserConfirmation;\n        organization.UseMyItems = plan.HasMyItems;\n\n        if (keys != null)\n        {\n            organization.BackfillPublicPrivateKeys(keys);\n        }\n\n        await organizationService.ReplaceAndUpdateCacheAsync(organization);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Entities/OrganizationInstallation.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Billing.Organizations.Entities;\n\npublic class OrganizationInstallation : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n\n    public Guid OrganizationId { get; set; }\n    public Guid InstallationId { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime? RevisionDate { get; set; }\n\n    public void SetNewId()\n    {\n        if (Id == default)\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Models/OrganizationLicense.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Reflection;\nusing System.Security.Claims;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Text.Json.Serialization;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.Billing.Organizations.Models;\n\npublic class OrganizationLicense : ILicense\n{\n    public OrganizationLicense()\n    {\n    }\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"OrganizationLicense\"/> class.\n    /// </summary>\n    /// <remarks>\n    /// <para>\n    /// ⚠️ DEPRECATED: This constructor and the entire property-based licensing system is deprecated.\n    /// Do not add new properties to this constructor or extend its functionality.\n    /// </para>\n    /// <para>\n    /// This implementation has been replaced by a new claims-based licensing system that provides better security\n    /// and flexibility. The new system uses JWT claims to store and validate license information, making it more\n    /// secure and easier to extend without requiring changes to the license format.\n    /// </para>\n    /// <para>\n    /// For new license-related features or modifications:\n    /// 1. Use the claims-based system instead of adding properties here\n    /// 2. Add new claims to the license token\n    /// 3. Validate claims in the <see cref=\"CanUse\"/> and <see cref=\"VerifyData\"/> methods\n    /// </para>\n    /// <para>\n    /// This constructor is maintained only for backward compatibility with existing licenses.\n    /// </para>\n    /// </remarks>\n    /// <param name=\"org\">The organization to create the license for.</param>\n    /// <param name=\"subscriptionInfo\">Information about the organization's subscription.</param>\n    /// <param name=\"installationId\">The ID of the current installation.</param>\n    /// <param name=\"licenseService\">The service used to sign the license.</param>\n    /// <param name=\"version\">Optional version number for the license format.</param>\n    public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId,\n        ILicensingService licenseService, int? version = null)\n    {\n        Version = version.GetValueOrDefault(CurrentLicenseFileVersion); // TODO: Remember to change the constant\n        LicenseType = Core.Enums.LicenseType.Organization;\n        LicenseKey = org.LicenseKey;\n        InstallationId = installationId;\n        Id = org.Id;\n        Name = org.Name;\n        BillingEmail = org.BillingEmail;\n        BusinessName = org.BusinessName;\n        Enabled = org.Enabled;\n        Plan = org.Plan;\n        PlanType = org.PlanType;\n        Seats = org.Seats;\n        MaxCollections = org.MaxCollections;\n        UsePolicies = org.UsePolicies;\n        UseSso = org.UseSso;\n        UseKeyConnector = org.UseKeyConnector;\n        UseScim = org.UseScim;\n        UseGroups = org.UseGroups;\n        UseEvents = org.UseEvents;\n        UseDirectory = org.UseDirectory;\n        UseTotp = org.UseTotp;\n        Use2fa = org.Use2fa;\n        UseApi = org.UseApi;\n        UseResetPassword = org.UseResetPassword;\n        MaxStorageGb = org.MaxStorageGb;\n        SelfHost = org.SelfHost;\n        UsersGetPremium = org.UsersGetPremium;\n        UseCustomPermissions = org.UseCustomPermissions;\n        Issued = DateTime.UtcNow;\n        UsePasswordManager = org.UsePasswordManager;\n        UseSecretsManager = org.UseSecretsManager;\n        SmSeats = org.SmSeats;\n        SmServiceAccounts = org.SmServiceAccounts;\n        UseRiskInsights = org.UseRiskInsights;\n        UseOrganizationDomains = org.UseOrganizationDomains;\n\n        // Deprecated. Left for backwards compatibility with old license versions.\n        LimitCollectionCreationDeletion = org.LimitCollectionCreation || org.LimitCollectionDeletion;\n        AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;\n        //\n\n        UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies;\n\n        Expires = org.CalculateFreshExpirationDate(subscriptionInfo, Issued);\n        Refresh = org.CalculateFreshRefreshDate(subscriptionInfo, Issued);\n        ExpirationWithoutGracePeriod = org.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo);\n        Trial = org.CalculateIsTrialing(subscriptionInfo);\n\n        Hash = Convert.ToBase64String(ComputeHash());\n        Signature = Convert.ToBase64String(licenseService.SignLicense(this));\n    }\n\n    public string LicenseKey { get; set; }\n    public Guid InstallationId { get; set; }\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public string BillingEmail { get; set; }\n    public string BusinessName { get; set; }\n    public bool Enabled { get; set; }\n    public string Plan { get; set; }\n    public PlanType PlanType { get; set; }\n    public int? Seats { get; set; }\n    public short? MaxCollections { get; set; }\n    public bool UsePolicies { get; set; }\n    public bool UseSso { get; set; }\n    public bool UseKeyConnector { get; set; }\n    public bool UseScim { get; set; }\n    public bool UseGroups { get; set; }\n    public bool UseEvents { get; set; }\n    public bool UseDirectory { get; set; }\n    public bool UseTotp { get; set; }\n    public bool Use2fa { get; set; }\n    public bool UseApi { get; set; }\n    public bool UseResetPassword { get; set; }\n    public short? MaxStorageGb { get; set; }\n    public bool SelfHost { get; set; }\n    public bool UsersGetPremium { get; set; }\n    public bool UseCustomPermissions { get; set; }\n    public int Version { get; set; }\n    public DateTime Issued { get; set; }\n    public DateTime? Refresh { get; set; }\n    public DateTime? Expires { get; set; }\n    public DateTime? ExpirationWithoutGracePeriod { get; set; }\n    public bool UsePasswordManager { get; set; }\n    public bool UseSecretsManager { get; set; }\n    public int? SmSeats { get; set; }\n    public int? SmServiceAccounts { get; set; }\n    public bool UseRiskInsights { get; set; }\n    public bool UsePhishingBlocker { get; set; }\n\n    // Deprecated. Left for backwards compatibility with old license versions.\n    public bool LimitCollectionCreationDeletion { get; set; } = true;\n    public bool AllowAdminAccessToAllCollectionItems { get; set; } = true;\n    //\n\n    public bool Trial { get; set; }\n    public LicenseType? LicenseType { get; set; }\n    public bool UseOrganizationDomains { get; set; }\n    public bool UseAdminSponsoredFamilies { get; set; }\n    public bool UseAutomaticUserConfirmation { get; set; }\n    public bool UseDisableSmAdsForUsers { get; set; }\n    public bool UseMyItems { get; set; }\n    public string Hash { get; set; }\n    public string Signature { get; set; }\n    public string Token { get; set; }\n    [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature);\n\n    /// <summary>\n    /// Represents the current version of the license format. Should be updated whenever new fields are added.\n    /// </summary>\n    /// <remarks>Intentionally set one version behind to allow self hosted users some time to update before\n    /// getting out of date license errors\n    /// </remarks>\n    public const int CurrentLicenseFileVersion = 15;\n    private bool ValidLicenseVersion\n    {\n        get => Version is >= 1 and <= 16;\n    }\n\n    public byte[] GetDataBytes(bool forHash = false)\n    {\n        string data = null;\n        if (ValidLicenseVersion)\n        {\n            var props = typeof(OrganizationLicense)\n                .GetProperties(BindingFlags.Public | BindingFlags.Instance)\n                .Where(p =>\n                    !p.Name.Equals(nameof(Signature)) &&\n                    !p.Name.Equals(nameof(SignatureBytes)) &&\n                    !p.Name.Equals(nameof(LicenseType)) &&\n                    !p.Name.Equals(nameof(Token)) &&\n                    // UsersGetPremium was added in Version 2\n                    (Version >= 2 || !p.Name.Equals(nameof(UsersGetPremium))) &&\n                    // UseEvents was added in Version 3\n                    (Version >= 3 || !p.Name.Equals(nameof(UseEvents))) &&\n                    // Use2fa was added in Version 4\n                    (Version >= 4 || !p.Name.Equals(nameof(Use2fa))) &&\n                    // UseApi was added in Version 5\n                    (Version >= 5 || !p.Name.Equals(nameof(UseApi))) &&\n                    // UsePolicies was added in Version 6\n                    (Version >= 6 || !p.Name.Equals(nameof(UsePolicies))) &&\n                    // UseSso was added in Version 7\n                    (Version >= 7 || !p.Name.Equals(nameof(UseSso))) &&\n                    // UseResetPassword was added in Version 8\n                    (Version >= 8 || !p.Name.Equals(nameof(UseResetPassword))) &&\n                    // UseKeyConnector was added in Version 9\n                    (Version >= 9 || !p.Name.Equals(nameof(UseKeyConnector))) &&\n                    // UseScim was added in Version 10\n                    (Version >= 10 || !p.Name.Equals(nameof(UseScim))) &&\n                    // UseCustomPermissions was added in Version 11\n                    (Version >= 11 || !p.Name.Equals(nameof(UseCustomPermissions))) &&\n                    // ExpirationWithoutGracePeriod was added in Version 12\n                    (Version >= 12 || !p.Name.Equals(nameof(ExpirationWithoutGracePeriod))) &&\n                    // UseSecretsManager, UsePasswordManager, SmSeats, and SmServiceAccounts were added in Version 13\n                    (Version >= 13 || !p.Name.Equals(nameof(UseSecretsManager))) &&\n                    (Version >= 13 || !p.Name.Equals(nameof(UsePasswordManager))) &&\n                    (Version >= 13 || !p.Name.Equals(nameof(SmSeats))) &&\n                    (Version >= 13 || !p.Name.Equals(nameof(SmServiceAccounts))) &&\n                    // LimitCollectionCreationDeletion was added in Version 14\n                    (Version >= 14 || !p.Name.Equals(nameof(LimitCollectionCreationDeletion))) &&\n                    // AllowAdminAccessToAllCollectionItems was added in Version 15\n                    (Version >= 15 || !p.Name.Equals(nameof(AllowAdminAccessToAllCollectionItems))) &&\n                    // UseOrganizationDomains was added in Version 16\n                    (Version >= 16 || !p.Name.Equals(nameof(UseOrganizationDomains))) &&\n                    (\n                        !forHash ||\n                        (\n                            !p.Name.Equals(nameof(Hash)) &&\n                            !p.Name.Equals(nameof(Issued)) &&\n                            !p.Name.Equals(nameof(Refresh))\n                        )\n                    ) &&\n                    // any new fields added need to be added here so that they're ignored\n                    !p.Name.Equals(nameof(UseRiskInsights)) &&\n                    !p.Name.Equals(nameof(UseAdminSponsoredFamilies)) &&\n                    !p.Name.Equals(nameof(UseOrganizationDomains)) &&\n                    !p.Name.Equals(nameof(UseAutomaticUserConfirmation)) &&\n                    !p.Name.Equals(nameof(UseDisableSmAdsForUsers)) &&\n                    !p.Name.Equals(nameof(UsePhishingBlocker)) &&\n                    !p.Name.Equals(nameof(UseMyItems)))\n                .OrderBy(p => p.Name)\n                .Select(p => $\"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}\")\n                .Aggregate((c, n) => $\"{c}|{n}\");\n            data = $\"license:organization|{props}\";\n        }\n        else\n        {\n            throw new NotSupportedException($\"Version {Version} is not supported.\");\n        }\n\n        return Encoding.UTF8.GetBytes(data);\n    }\n\n    public byte[] ComputeHash()\n    {\n        using (var alg = SHA256.Create())\n        {\n            return alg.ComputeHash(GetDataBytes(true));\n        }\n    }\n\n    public bool CanUse(\n        IGlobalSettings globalSettings,\n        ILicensingService licensingService,\n        ClaimsPrincipal claimsPrincipal,\n        out string exception)\n    {\n        if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)\n        {\n            return ObsoleteCanUse(globalSettings, licensingService, out exception);\n        }\n\n        var errorMessages = new StringBuilder();\n\n        var enabled = claimsPrincipal.GetValue<bool>(nameof(Enabled));\n        if (!enabled)\n        {\n            errorMessages.AppendLine(\"Your cloud-hosted organization is currently disabled.\");\n        }\n\n        var installationId = claimsPrincipal.GetValue<Guid>(nameof(InstallationId));\n        if (installationId != globalSettings.Installation.Id)\n        {\n            errorMessages.AppendLine(\"The installation ID does not match the current installation.\");\n        }\n\n        var selfHost = claimsPrincipal.GetValue<bool>(nameof(SelfHost));\n        if (!selfHost)\n        {\n            errorMessages.AppendLine(\"The license does not allow for on-premise hosting of organizations.\");\n        }\n\n        var licenseType = claimsPrincipal.GetValue<LicenseType>(nameof(LicenseType));\n        if (licenseType != Core.Enums.LicenseType.Organization)\n        {\n            errorMessages.AppendLine(\"Premium licenses cannot be applied to an organization. \" +\n                                     \"Upload this license from your personal account settings page.\");\n        }\n\n        if (errorMessages.Length > 0)\n        {\n            exception = $\"Invalid license. {errorMessages.ToString().TrimEnd()}\";\n            return false;\n        }\n\n        exception = \"\";\n        return true;\n    }\n\n    /// <summary>\n    /// Validates an obsolete license format using property-based validation.\n    /// </summary>\n    /// <remarks>\n    /// <para>\n    /// ⚠️ DEPRECATED: This method is deprecated and should not be extended or modified.\n    /// It is maintained only for backward compatibility with old license formats.\n    /// </para>\n    /// <para>\n    /// This method has been replaced by a new claims-based validation system that provides:\n    /// - Better security through JWT claims\n    /// - More flexible validation rules\n    /// - Easier extensibility without changing the license format\n    /// - Better separation of concerns\n    /// </para>\n    /// <para>\n    /// To add new license validation rules:\n    /// 1. Add new claims to the license token in the claims-based system\n    /// 2. Extend the <see cref=\"CanUse(IGlobalSettings, ILicensingService, ClaimsPrincipal, out string)\"/> method\n    /// 3. Validate the new claims using the ClaimsPrincipal parameter\n    /// </para>\n    /// <para>\n    /// This method will be removed in a future version once all old licenses have been migrated\n    /// to the new claims-based system.\n    /// </para>\n    /// </remarks>\n    /// <param name=\"globalSettings\">The global settings containing installation information.</param>\n    /// <param name=\"licensingService\">The service used to verify the license signature.</param>\n    /// <param name=\"exception\">When the method returns false, contains the error message explaining why the license is invalid.</param>\n    /// <returns>True if the license is valid, false otherwise.</returns>\n    private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception)\n    {\n        // Do not extend this method. It is only here for backwards compatibility with old licenses.\n        var errorMessages = new StringBuilder();\n\n        if (!Enabled)\n        {\n            errorMessages.AppendLine(\"Your cloud-hosted organization is currently disabled.\");\n        }\n\n        if (Issued > DateTime.UtcNow)\n        {\n            errorMessages.AppendLine(\"The license hasn't been issued yet.\");\n        }\n\n        if (Expires < DateTime.UtcNow)\n        {\n            errorMessages.AppendLine(\"The license has expired.\");\n        }\n\n        if (!ValidLicenseVersion)\n        {\n            errorMessages.AppendLine($\"Version {Version} is not supported.\");\n        }\n\n        if (InstallationId != globalSettings.Installation.Id)\n        {\n            errorMessages.AppendLine(\"The installation ID does not match the current installation.\");\n        }\n\n        if (!SelfHost)\n        {\n            errorMessages.AppendLine(\"The license does not allow for on-premise hosting of organizations.\");\n        }\n\n        if (LicenseType != null && LicenseType != Core.Enums.LicenseType.Organization)\n        {\n            errorMessages.AppendLine(\"Premium licenses cannot be applied to an organization. \" +\n                                     \"Upload this license from your personal account settings page.\");\n        }\n\n        if (!licensingService.VerifyLicense(this))\n        {\n            errorMessages.AppendLine(\"The license verification failed.\");\n        }\n\n        if (errorMessages.Length > 0)\n        {\n            exception = $\"Invalid license. {errorMessages.ToString().TrimEnd()}\";\n            return false;\n        }\n\n        exception = \"\";\n        return true;\n    }\n\n    public bool VerifyData(\n        Organization organization,\n        ClaimsPrincipal claimsPrincipal,\n        IGlobalSettings globalSettings)\n    {\n        if (string.IsNullOrWhiteSpace(Token))\n        {\n            return ObsoleteVerifyData(organization, globalSettings);\n        }\n\n        var issued = claimsPrincipal.GetValue<DateTime>(nameof(Issued));\n        var expires = claimsPrincipal.GetValue<DateTime>(nameof(Expires));\n        var installationId = claimsPrincipal.GetValue<Guid>(nameof(InstallationId));\n        var licenseKey = claimsPrincipal.GetValue<string>(nameof(LicenseKey));\n        var enabled = claimsPrincipal.GetValue<bool>(nameof(Enabled));\n        var seats = claimsPrincipal.GetValue<int?>(nameof(Seats));\n        var maxCollections = claimsPrincipal.GetValue<short?>(nameof(MaxCollections));\n        var useGroups = claimsPrincipal.GetValue<bool>(nameof(UseGroups));\n        var useDirectory = claimsPrincipal.GetValue<bool>(nameof(UseDirectory));\n        var useTotp = claimsPrincipal.GetValue<bool>(nameof(UseTotp));\n        var selfHost = claimsPrincipal.GetValue<bool>(nameof(SelfHost));\n        var name = claimsPrincipal.GetValue<string>(nameof(Name));\n        var usersGetPremium = claimsPrincipal.GetValue<bool>(nameof(UsersGetPremium));\n        var useEvents = claimsPrincipal.GetValue<bool>(nameof(UseEvents));\n        var use2fa = claimsPrincipal.GetValue<bool>(nameof(Use2fa));\n        var useApi = claimsPrincipal.GetValue<bool>(nameof(UseApi));\n        var usePolicies = claimsPrincipal.GetValue<bool>(nameof(UsePolicies));\n        var useSso = claimsPrincipal.GetValue<bool>(nameof(UseSso));\n        var useResetPassword = claimsPrincipal.GetValue<bool>(nameof(UseResetPassword));\n        var useKeyConnector = claimsPrincipal.GetValue<bool>(nameof(UseKeyConnector));\n        var useScim = claimsPrincipal.GetValue<bool>(nameof(UseScim));\n        var useCustomPermissions = claimsPrincipal.GetValue<bool>(nameof(UseCustomPermissions));\n        var useSecretsManager = claimsPrincipal.GetValue<bool>(nameof(UseSecretsManager));\n        var usePasswordManager = claimsPrincipal.GetValue<bool>(nameof(UsePasswordManager));\n        var smSeats = claimsPrincipal.GetValue<int?>(nameof(SmSeats));\n        var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts));\n        var useAdminSponsoredFamilies = claimsPrincipal.GetValue<bool>(nameof(UseAdminSponsoredFamilies));\n        var useOrganizationDomains = claimsPrincipal.GetValue<bool>(nameof(UseOrganizationDomains));\n        var useAutomaticUserConfirmation = claimsPrincipal.GetValue<bool>(nameof(UseAutomaticUserConfirmation));\n        var useDisableSmAdsForUsers = claimsPrincipal.GetValue<bool>(nameof(UseDisableSmAdsForUsers));\n        var useMyItems = claimsPrincipal.GetValue<bool>(nameof(UseMyItems));\n\n        var claimedPlanType = claimsPrincipal.GetValue<PlanType>(nameof(PlanType));\n\n        var planTypesMatch = claimedPlanType == PlanType.FamiliesAnnually\n            ? organization.PlanType is PlanType.FamiliesAnnually or PlanType.FamiliesAnnually2025\n            : organization.PlanType == claimedPlanType;\n\n        return issued <= DateTime.UtcNow &&\n               expires >= DateTime.UtcNow &&\n               installationId == globalSettings.Installation.Id &&\n               licenseKey == organization.LicenseKey &&\n               enabled == organization.Enabled &&\n               planTypesMatch &&\n               seats == organization.Seats &&\n               maxCollections == organization.MaxCollections &&\n               useGroups == organization.UseGroups &&\n               useDirectory == organization.UseDirectory &&\n               useTotp == organization.UseTotp &&\n               selfHost == organization.SelfHost &&\n               name == organization.Name &&\n               usersGetPremium == organization.UsersGetPremium &&\n               useEvents == organization.UseEvents &&\n               use2fa == organization.Use2fa &&\n               useApi == organization.UseApi &&\n               usePolicies == organization.UsePolicies &&\n               useSso == organization.UseSso &&\n               useResetPassword == organization.UseResetPassword &&\n               useKeyConnector == organization.UseKeyConnector &&\n               useScim == organization.UseScim &&\n               useCustomPermissions == organization.UseCustomPermissions &&\n               useSecretsManager == organization.UseSecretsManager &&\n               usePasswordManager == organization.UsePasswordManager &&\n               smSeats == organization.SmSeats &&\n               smServiceAccounts == organization.SmServiceAccounts &&\n               useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies &&\n               useOrganizationDomains == organization.UseOrganizationDomains &&\n               useAutomaticUserConfirmation == organization.UseAutomaticUserConfirmation &&\n               useDisableSmAdsForUsers == organization.UseDisableSmAdsForUsers &&\n               useMyItems == organization.UseMyItems;\n\n    }\n\n    /// <summary>\n    /// Do not extend this method. It is only here for backwards compatibility with old licenses.\n    /// Instead, extend the VerifyData method using the ClaimsPrincipal.\n    /// </summary>\n    /// <param name=\"organization\"></param>\n    /// <param name=\"globalSettings\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"NotSupportedException\"></exception>\n    private bool ObsoleteVerifyData(Organization organization, IGlobalSettings globalSettings)\n    {\n        // Do not extend this method. It is only here for backwards compatibility with old licenses.\n        if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)\n        {\n            return false;\n        }\n\n        if (!ValidLicenseVersion)\n        {\n            throw new NotSupportedException($\"Version {Version} is not supported.\");\n        }\n\n        var valid =\n            globalSettings.Installation.Id == InstallationId &&\n            organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) &&\n            organization.Enabled == Enabled &&\n            organization.PlanType == PlanType &&\n            organization.Seats == Seats &&\n            organization.MaxCollections == MaxCollections &&\n            organization.UseGroups == UseGroups &&\n            organization.UseDirectory == UseDirectory &&\n            organization.UseTotp == UseTotp &&\n            organization.SelfHost == SelfHost &&\n            organization.Name.Equals(Name);\n\n        if (valid && Version >= 2)\n        {\n            valid = organization.UsersGetPremium == UsersGetPremium;\n        }\n\n        if (valid && Version >= 3)\n        {\n            valid = organization.UseEvents == UseEvents;\n        }\n\n        if (valid && Version >= 4)\n        {\n            valid = organization.Use2fa == Use2fa;\n        }\n\n        if (valid && Version >= 5)\n        {\n            valid = organization.UseApi == UseApi;\n        }\n\n        if (valid && Version >= 6)\n        {\n            valid = organization.UsePolicies == UsePolicies;\n        }\n\n        if (valid && Version >= 7)\n        {\n            valid = organization.UseSso == UseSso;\n        }\n\n        if (valid && Version >= 8)\n        {\n            valid = organization.UseResetPassword == UseResetPassword;\n        }\n\n        if (valid && Version >= 9)\n        {\n            valid = organization.UseKeyConnector == UseKeyConnector;\n        }\n\n        if (valid && Version >= 10)\n        {\n            valid = organization.UseScim == UseScim;\n        }\n\n        if (valid && Version >= 11)\n        {\n            valid = organization.UseCustomPermissions == UseCustomPermissions;\n        }\n\n        /*Version 12 added ExpirationWithoutDatePeriod, but that property is informational only and is not saved\n            to the Organization object. It's validated as part of the hash but does not need to be validated here.\n            */\n\n        if (valid && Version >= 13)\n        {\n            valid = organization.UseSecretsManager == UseSecretsManager &&\n                    organization.UsePasswordManager == UsePasswordManager &&\n                    organization.SmSeats == SmSeats &&\n                    organization.SmServiceAccounts == SmServiceAccounts;\n        }\n\n        /*\n            * Version 14 added LimitCollectionCreationDeletion and Version\n            * 15 added AllowAdminAccessToAllCollectionItems, however they\n            * are no longer used and are intentionally excluded from\n            * validation.\n            */\n\n        if (valid && Version >= 16)\n        {\n            valid = organization.UseOrganizationDomains;\n        }\n\n        return valid;\n    }\n\n    public bool VerifySignature(X509Certificate2 certificate)\n    {\n        using (var rsa = certificate.GetRSAPublicKey())\n        {\n            return rsa.VerifyData(GetDataBytes(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n        }\n    }\n\n    public byte[] Sign(X509Certificate2 certificate)\n    {\n        if (!certificate.HasPrivateKey)\n        {\n            throw new InvalidOperationException(\"You don't have the private key!\");\n        }\n\n        using (var rsa = certificate.GetRSAPrivateKey())\n        {\n            return rsa.SignData(GetDataBytes(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Models/OrganizationMetadata.cs",
    "content": "﻿namespace Bit.Core.Billing.Organizations.Models;\n\npublic record OrganizationMetadata(\n    bool IsOnSecretsManagerStandalone,\n    int OrganizationOccupiedSeats)\n{\n    public static OrganizationMetadata Default => new OrganizationMetadata(\n        false,\n        0);\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Models/OrganizationSale.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Models.Sales;\nusing Bit.Core.Billing.Tax.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.Billing.Organizations.Models;\n\npublic class OrganizationSale\n{\n    internal OrganizationSale() { }\n\n    public void Deconstruct(\n        out Organization organization,\n        out CustomerSetup? customerSetup,\n        out SubscriptionSetup subscriptionSetup,\n        out User? owner)\n    {\n        organization = Organization;\n        customerSetup = CustomerSetup;\n        subscriptionSetup = SubscriptionSetup;\n        owner = Owner;\n    }\n\n    public required Organization Organization { get; init; }\n    public CustomerSetup? CustomerSetup { get; init; }\n    public required SubscriptionSetup SubscriptionSetup { get; init; }\n    public User? Owner { get; init; }\n\n    public static OrganizationSale From(\n        Organization organization,\n        OrganizationSignup signup)\n    {\n        var customerSetup = string.IsNullOrEmpty(organization.GatewayCustomerId) ? GetCustomerSetup(signup) : null;\n\n        var subscriptionSetup = GetSubscriptionSetup(signup);\n\n        subscriptionSetup.SkipTrial = signup.SkipTrial;\n        subscriptionSetup.InitiationPath = signup.InitiationPath;\n\n        return new OrganizationSale\n        {\n            Organization = organization,\n            CustomerSetup = customerSetup,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = signup.Owner\n        };\n    }\n\n    public static OrganizationSale From(\n        Organization organization,\n        OrganizationUpgrade upgrade,\n        User? owner) => new()\n        {\n            Organization = organization,\n            SubscriptionSetup = GetSubscriptionSetup(upgrade),\n            Owner = owner\n        };\n\n    private static CustomerSetup GetCustomerSetup(OrganizationSignup signup)\n    {\n        var customerSetup = new CustomerSetup\n        {\n            Coupons = signup.IsFromProvider\n            // TODO: Remove when last of the legacy providers has been migrated.\n            ? [StripeConstants.CouponIDs.LegacyMSPDiscount]\n            : signup.IsFromSecretsManagerTrial\n                ? [StripeConstants.CouponIDs.SecretsManagerStandalone]\n                : signup.Coupons\n        };\n\n        if (!signup.PaymentMethodType.HasValue)\n        {\n            return customerSetup;\n        }\n\n        customerSetup.TokenizedPaymentSource = new TokenizedPaymentSource(\n            signup.PaymentMethodType!.Value,\n            signup.PaymentToken);\n\n        customerSetup.TaxInformation = new TaxInformation(\n            signup.TaxInfo.BillingAddressCountry,\n            signup.TaxInfo.BillingAddressPostalCode,\n            signup.TaxInfo.TaxIdNumber,\n            signup.TaxInfo.TaxIdType,\n            signup.TaxInfo.BillingAddressLine1,\n            signup.TaxInfo.BillingAddressLine2,\n            signup.TaxInfo.BillingAddressCity,\n            signup.TaxInfo.BillingAddressState);\n\n        return customerSetup;\n    }\n\n    private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade)\n    {\n        var passwordManagerOptions = new SubscriptionSetup.PasswordManager\n        {\n            Seats = upgrade.AdditionalSeats,\n            Storage = upgrade.AdditionalStorageGb,\n            PremiumAccess = upgrade.PremiumAccessAddon\n        };\n\n        var secretsManagerOptions = upgrade.UseSecretsManager\n            ? new SubscriptionSetup.SecretsManager\n            {\n                Seats = upgrade.AdditionalSmSeats ?? 0,\n                ServiceAccounts = upgrade.AdditionalServiceAccounts\n            }\n            : null;\n\n        return new SubscriptionSetup\n        {\n            PlanType = upgrade.Plan,\n            PasswordManagerOptions = passwordManagerOptions,\n            SecretsManagerOptions = secretsManagerOptions\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Models/OrganizationSubscriptionChangeSet.cs",
    "content": "﻿using Bit.Core.Models.StaticStore;\nusing OneOf;\n\nnamespace Bit.Core.Billing.Organizations.Models;\n\n/// <summary>\n/// Adds a new line item to the subscription.\n/// </summary>\npublic record AddItem(string PriceId, int Quantity);\n\n/// <summary>\n/// Replaces an existing line item's price (e.g. switching from monthly to annual billing).\n/// Optionally updates the quantity; if <c>null</c>, the current quantity is preserved.\n/// </summary>\npublic record ChangeItemPrice(string CurrentPriceId, string UpdatedPriceId, int? Quantity);\n\n/// <summary>\n/// Removes a line item from the subscription.\n/// </summary>\npublic record RemoveItem(string PriceId);\n\n/// <summary>\n/// Updates the quantity of an existing line item. Setting quantity to 0 deletes the item.\n/// </summary>\npublic record UpdateItemQuantity(string PriceId, int Quantity);\n\n/// <summary>\n/// A union type representing a single change to apply to an organization's Stripe subscription.\n/// A change is considered \"structural\" (triggering immediate invoicing) if it adds, removes,\n/// or re-prices a line item, or sets a quantity to 0. Non-structural quantity updates use prorations.\n/// </summary>\npublic class OrganizationSubscriptionChange(OneOf<AddItem, ChangeItemPrice, RemoveItem, UpdateItemQuantity> input)\n    : OneOfBase<AddItem, ChangeItemPrice, RemoveItem, UpdateItemQuantity>(input)\n{\n    public static implicit operator OrganizationSubscriptionChange(AddItem addItem) =>\n        new(addItem);\n\n    public static implicit operator OrganizationSubscriptionChange(ChangeItemPrice changeItemPrice) =>\n        new(changeItemPrice);\n\n    public static implicit operator OrganizationSubscriptionChange(RemoveItem removeItem) =>\n        new(removeItem);\n\n    public static implicit operator OrganizationSubscriptionChange(UpdateItemQuantity updateItemQuantity) =>\n        new(updateItemQuantity);\n\n    public bool IsItemAddition => IsT0;\n    public bool IsItemPriceChange => IsT1;\n    public bool IsItemRemoval => IsT2;\n    public bool IsItemQuantityUpdate => IsT3;\n    public bool IsStructural => !IsItemQuantityUpdate || AsT3.Quantity == 0;\n}\n\n/// <summary>\n/// A collection of <see cref=\"OrganizationSubscriptionChange\"/> items to apply atomically to\n/// an organization's Stripe subscription. Use the static factory methods for common single-change\n/// operations, or <see cref=\"Builder\"/> for composing multiple changes.\n/// </summary>\npublic record OrganizationSubscriptionChangeSet\n{\n    public required IReadOnlyList<OrganizationSubscriptionChange> Changes { get; init; } = [];\n\n    public static OrganizationSubscriptionChangeSet UpdatePasswordManagerSeats(Plan plan, int seats) =>\n        new()\n        {\n            Changes =\n            [\n                new UpdateItemQuantity(plan.PasswordManager.StripeSeatPlanId, seats)\n            ]\n        };\n\n    public static OrganizationSubscriptionChangeSet UpdateStorage(Plan plan, int storage) =>\n        new()\n        {\n            Changes =\n            [\n                new UpdateItemQuantity(plan.PasswordManager.StripeStoragePlanId, storage)\n            ]\n        };\n\n    public static OrganizationSubscriptionChangeSet UpdateSecretsManagerSeats(Plan plan, int seats) =>\n        new()\n        {\n            Changes =\n            [\n                new UpdateItemQuantity(plan.SecretsManager.StripeSeatPlanId, seats)\n            ]\n        };\n\n    public static OrganizationSubscriptionChangeSet UpdateSecretsManagerServiceAccounts(Plan plan, int serviceAccounts) =>\n        new()\n        {\n            Changes =\n            [\n                new UpdateItemQuantity(plan.SecretsManager.StripeServiceAccountPlanId, serviceAccounts)\n            ]\n        };\n\n    public static OrganizationSubscriptionChangeSetBuilder Builder() => new();\n}\n\npublic class OrganizationSubscriptionChangeSetBuilder\n{\n    private readonly List<OrganizationSubscriptionChange> _changes = [];\n\n    public OrganizationSubscriptionChangeSetBuilder AddItem(string priceId, int quantity)\n    {\n        _changes.Add(new AddItem(priceId, quantity));\n        return this;\n    }\n\n    public OrganizationSubscriptionChangeSetBuilder ChangeItemPrice(string currentPriceId, string updatedPriceId, int? quantity = null)\n    {\n        _changes.Add(new ChangeItemPrice(currentPriceId, updatedPriceId, quantity));\n        return this;\n    }\n\n    public OrganizationSubscriptionChangeSetBuilder RemoveItem(string priceId)\n    {\n        _changes.Add(new RemoveItem(priceId));\n        return this;\n    }\n\n    public OrganizationSubscriptionChangeSetBuilder UpdateItemQuantity(string priceId, int quantity)\n    {\n        _changes.Add(new UpdateItemQuantity(priceId, quantity));\n        return this;\n    }\n\n    public OrganizationSubscriptionChangeSet Build() =>\n        new() { Changes = _changes.AsReadOnly() };\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Models/OrganizationSubscriptionPlanChange.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Organizations.Models;\n\npublic record OrganizationSubscriptionPlanChange\n{\n    public ProductTierType Tier { get; init; }\n    public PlanCadenceType Cadence { get; init; }\n\n    public PlanType PlanType =>\n        // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault\n        Tier switch\n        {\n            ProductTierType.Families => PlanType.FamiliesAnnually,\n            ProductTierType.Teams => Cadence == PlanCadenceType.Monthly\n                ? PlanType.TeamsMonthly\n                : PlanType.TeamsAnnually,\n            ProductTierType.Enterprise => Cadence == PlanCadenceType.Monthly\n                ? PlanType.EnterpriseMonthly\n                : PlanType.EnterpriseAnnually,\n            _ => throw new InvalidOperationException(\"Cannot change an Organization subscription to a tier that isn't Families, Teams or Enterprise.\")\n        };\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Models/OrganizationSubscriptionPurchase.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Organizations.Models;\n\npublic record OrganizationSubscriptionPurchase\n{\n    public ProductTierType Tier { get; init; }\n    public PlanCadenceType Cadence { get; init; }\n    public required PasswordManagerSelections PasswordManager { get; init; }\n    public SecretsManagerSelections? SecretsManager { get; init; }\n    public string[]? Coupons { get; init; }\n\n    public PlanType PlanType =>\n        // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault\n        Tier switch\n        {\n            ProductTierType.Families => PlanType.FamiliesAnnually,\n            ProductTierType.Teams => Cadence == PlanCadenceType.Monthly\n                ? PlanType.TeamsMonthly\n                : PlanType.TeamsAnnually,\n            ProductTierType.Enterprise => Cadence == PlanCadenceType.Monthly\n                ? PlanType.EnterpriseMonthly\n                : PlanType.EnterpriseAnnually,\n            _ => throw new InvalidOperationException(\"Cannot purchase an Organization subscription that isn't Families, Teams or Enterprise.\")\n        };\n\n    public record PasswordManagerSelections\n    {\n        public int Seats { get; init; }\n        public int AdditionalStorage { get; init; }\n        public bool Sponsored { get; init; }\n    }\n\n    public record SecretsManagerSelections\n    {\n        public int Seats { get; init; }\n        public int AdditionalServiceAccounts { get; init; }\n        public bool Standalone { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Models/OrganizationSubscriptionUpdate.cs",
    "content": "﻿namespace Bit.Core.Billing.Organizations.Models;\n\npublic record OrganizationSubscriptionUpdate\n{\n    public PasswordManagerSelections? PasswordManager { get; init; }\n    public SecretsManagerSelections? SecretsManager { get; init; }\n\n    public record PasswordManagerSelections\n    {\n        public int? Seats { get; init; }\n        public int? AdditionalStorage { get; init; }\n    }\n\n    public record SecretsManagerSelections\n    {\n        public int? Seats { get; init; }\n        public int? AdditionalServiceAccounts { get; init; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Models/OrganizationWarnings.cs",
    "content": "﻿namespace Bit.Core.Billing.Organizations.Models;\n\npublic record OrganizationWarnings\n{\n    public FreeTrialWarning? FreeTrial { get; set; }\n    public InactiveSubscriptionWarning? InactiveSubscription { get; set; }\n    public ResellerRenewalWarning? ResellerRenewal { get; set; }\n    public TaxIdWarning? TaxId { get; set; }\n\n    public record FreeTrialWarning\n    {\n        public int RemainingTrialDays { get; set; }\n    }\n\n    public record InactiveSubscriptionWarning\n    {\n        public required string Resolution { get; set; }\n    }\n\n    public record ResellerRenewalWarning\n    {\n        public required string Type { get; set; }\n        public UpcomingRenewal? Upcoming { get; set; }\n        public IssuedRenewal? Issued { get; set; }\n        public PastDueRenewal? PastDue { get; set; }\n\n        public record UpcomingRenewal\n        {\n            public required DateTime RenewalDate { get; set; }\n        }\n\n        public record IssuedRenewal\n        {\n            public required DateTime IssuedDate { get; set; }\n            public required DateTime DueDate { get; set; }\n        }\n\n        public record PastDueRenewal\n        {\n            public required DateTime SuspensionDate { get; set; }\n        }\n    }\n\n    public record TaxIdWarning\n    {\n        public required string Type { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Models/SponsorOrganizationSubscriptionUpdate.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Models.Business;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Organizations.Models;\n\npublic class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate\n{\n    private readonly string _existingPlanStripeId;\n    private readonly string _sponsoredPlanStripeId;\n    private readonly bool _applySponsorship;\n    protected override List<string> PlanIds => new() { _existingPlanStripeId, _sponsoredPlanStripeId };\n\n    public SponsorOrganizationSubscriptionUpdate(Core.Models.StaticStore.Plan existingPlan, Core.Models.StaticStore.SponsoredPlan sponsoredPlan, bool applySponsorship)\n    {\n        _existingPlanStripeId = existingPlan.PasswordManager.StripePlanId;\n        _sponsoredPlanStripeId = sponsoredPlan?.StripePlanId\n                                 ?? SponsoredPlans.All.FirstOrDefault()?.StripePlanId;\n        _applySponsorship = applySponsorship;\n    }\n\n    public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)\n    {\n        var result = new List<SubscriptionItemOptions>();\n        if (!string.IsNullOrWhiteSpace(AddStripePlanId))\n        {\n            result.Add(new SubscriptionItemOptions\n            {\n                Id = AddStripeItem(subscription)?.Id,\n                Plan = AddStripePlanId,\n                Quantity = 0,\n                Deleted = true,\n            });\n        }\n\n        if (!string.IsNullOrWhiteSpace(RemoveStripePlanId))\n        {\n            result.Add(new SubscriptionItemOptions\n            {\n                Id = RemoveStripeItem(subscription)?.Id,\n                Plan = RemoveStripePlanId,\n                Quantity = 1,\n                Deleted = false,\n            });\n        }\n        return result;\n    }\n\n    public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)\n    {\n        var result = new List<SubscriptionItemOptions>();\n        if (RemoveStripeItem(subscription) != null)\n        {\n            result.Add(new SubscriptionItemOptions\n            {\n                Id = RemoveStripeItem(subscription)?.Id,\n                Plan = RemoveStripePlanId,\n                Quantity = 0,\n                Deleted = true,\n            });\n        }\n\n        if (!string.IsNullOrWhiteSpace(AddStripePlanId))\n        {\n            result.Add(new SubscriptionItemOptions\n            {\n                Id = AddStripeItem(subscription)?.Id,\n                Plan = AddStripePlanId,\n                Quantity = 1,\n                Deleted = false,\n            });\n        }\n        return result;\n    }\n\n    private string RemoveStripePlanId => _applySponsorship ? _existingPlanStripeId : _sponsoredPlanStripeId;\n    private string AddStripePlanId => _applySponsorship ? _sponsoredPlanStripeId : _existingPlanStripeId;\n    private Stripe.SubscriptionItem RemoveStripeItem(Subscription subscription) =>\n        _applySponsorship ?\n            FindSubscriptionItem(subscription, _existingPlanStripeId) :\n            FindSubscriptionItem(subscription, _sponsoredPlanStripeId);\n    private Stripe.SubscriptionItem AddStripeItem(Subscription subscription) =>\n        _applySponsorship ?\n            FindSubscriptionItem(subscription, _sponsoredPlanStripeId) :\n            FindSubscriptionItem(subscription, _existingPlanStripeId);\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Queries/GetCloudOrganizationLicenseQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Platform.Installations;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.Billing.Organizations.Queries;\n\npublic interface IGetCloudOrganizationLicenseQuery\n{\n    Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,\n        int? version = null);\n}\n\npublic class GetCloudOrganizationLicenseQuery : IGetCloudOrganizationLicenseQuery\n{\n    private readonly IInstallationRepository _installationRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly ILicensingService _licensingService;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IFeatureService _featureService;\n\n    public GetCloudOrganizationLicenseQuery(\n        IInstallationRepository installationRepository,\n        IStripePaymentService paymentService,\n        ILicensingService licensingService,\n        IProviderRepository providerRepository,\n        IFeatureService featureService)\n    {\n        _installationRepository = installationRepository;\n        _paymentService = paymentService;\n        _licensingService = licensingService;\n        _providerRepository = providerRepository;\n        _featureService = featureService;\n    }\n\n    public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,\n        int? version = null)\n    {\n        var installation = await _installationRepository.GetByIdAsync(installationId);\n        if (installation is not { Enabled: true })\n        {\n            throw new BadRequestException(\"Invalid installation id\");\n        }\n\n        var subscriptionInfo = await GetSubscriptionAsync(organization);\n        var license = new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);\n        license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo);\n\n        return license;\n    }\n\n    private async Task<SubscriptionInfo> GetSubscriptionAsync(Organization organization)\n    {\n        if (organization is not { Status: OrganizationStatusType.Managed })\n        {\n            return await _paymentService.GetSubscriptionAsync(organization);\n        }\n\n        var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);\n        return await _paymentService.GetSubscriptionAsync(provider);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Organizations.Queries;\n\npublic interface IGetOrganizationMetadataQuery\n{\n    Task<OrganizationMetadata?> Run(Organization organization);\n}\n\npublic class GetOrganizationMetadataQuery(\n    IGlobalSettings globalSettings,\n    IOrganizationRepository organizationRepository,\n    IPricingClient pricingClient,\n    ISubscriberService subscriberService) : IGetOrganizationMetadataQuery\n{\n    public async Task<OrganizationMetadata?> Run(Organization organization)\n    {\n        if (globalSettings.SelfHosted)\n        {\n            return OrganizationMetadata.Default;\n        }\n\n        var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);\n\n        if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))\n        {\n            return OrganizationMetadata.Default with\n            {\n                OrganizationOccupiedSeats = orgOccupiedSeats.Total\n            };\n        }\n\n        var customer = await subscriberService.GetCustomer(organization);\n\n        var subscription = await subscriberService.GetSubscription(organization, new SubscriptionGetOptions\n        {\n            Expand = [\"discounts.coupon.applies_to\"]\n        });\n\n        if (customer == null || subscription == null)\n        {\n            return OrganizationMetadata.Default with\n            {\n                OrganizationOccupiedSeats = orgOccupiedSeats.Total\n            };\n        }\n\n        var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription);\n\n        return new OrganizationMetadata(\n            isOnSecretsManagerStandalone,\n            orgOccupiedSeats.Total);\n    }\n\n    private async Task<bool> IsOnSecretsManagerStandalone(\n        Organization organization,\n        Customer? customer,\n        Subscription? subscription)\n    {\n        if (customer == null || subscription == null)\n        {\n            return false;\n        }\n\n        var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        if (!plan.SupportsSecretsManager)\n        {\n            return false;\n        }\n\n        var coupon = subscription.Discounts?.FirstOrDefault(discount =>\n            discount.Coupon?.Id == StripeConstants.CouponIDs.SecretsManagerStandalone)?.Coupon;\n\n        if (coupon == null)\n        {\n            return false;\n        }\n\n        var subscriptionProductIds = subscription.Items.Data.Select(item => item.Plan.ProductId);\n\n        var couponAppliesTo = coupon.AppliesTo?.Products;\n\n        return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Context;\nusing Stripe;\nusing Stripe.Tax;\n\nnamespace Bit.Core.Billing.Organizations.Queries;\n\nusing static StripeConstants;\nusing FreeTrialWarning = OrganizationWarnings.FreeTrialWarning;\nusing InactiveSubscriptionWarning = OrganizationWarnings.InactiveSubscriptionWarning;\nusing ResellerRenewalWarning = OrganizationWarnings.ResellerRenewalWarning;\nusing TaxIdWarning = OrganizationWarnings.TaxIdWarning;\n\npublic interface IGetOrganizationWarningsQuery\n{\n    Task<OrganizationWarnings> Run(\n        Organization organization);\n}\n\npublic class GetOrganizationWarningsQuery(\n    ICurrentContext currentContext,\n    IHasPaymentMethodQuery hasPaymentMethodQuery,\n    IProviderRepository providerRepository,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService) : IGetOrganizationWarningsQuery\n{\n    public async Task<OrganizationWarnings> Run(\n        Organization organization)\n    {\n        var warnings = new OrganizationWarnings();\n\n        var subscription =\n            await subscriberService.GetSubscription(organization,\n                new SubscriptionGetOptions { Expand = [\"customer.tax_ids\", \"latest_invoice\", \"test_clock\"] });\n\n        if (subscription == null)\n        {\n            return warnings;\n        }\n\n        warnings.FreeTrial = await GetFreeTrialWarningAsync(organization, subscription);\n\n        var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);\n\n        warnings.InactiveSubscription = await GetInactiveSubscriptionWarningAsync(organization, provider, subscription);\n\n        warnings.ResellerRenewal = await GetResellerRenewalWarningAsync(provider, subscription);\n\n        warnings.TaxId = await GetTaxIdWarningAsync(organization, subscription.Customer, provider);\n\n        return warnings;\n    }\n\n    private async Task<FreeTrialWarning?> GetFreeTrialWarningAsync(\n        Organization organization,\n        Subscription subscription)\n    {\n        if (!await currentContext.EditSubscription(organization.Id))\n        {\n            return null;\n        }\n\n        if (subscription is not\n            {\n                Status: SubscriptionStatus.Trialing,\n                TrialEnd: not null,\n                Customer: not null\n            })\n        {\n            return null;\n        }\n\n        var hasPaymentMethod = await hasPaymentMethodQuery.Run(organization);\n\n        if (hasPaymentMethod)\n        {\n            return null;\n        }\n\n        var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;\n\n        var remainingTrialDays = (int)Math.Ceiling((subscription.TrialEnd.Value - now).TotalDays);\n\n        return new FreeTrialWarning { RemainingTrialDays = remainingTrialDays };\n    }\n\n    private async Task<InactiveSubscriptionWarning?> GetInactiveSubscriptionWarningAsync(\n        Organization organization,\n        Provider? provider,\n        Subscription subscription)\n    {\n        // If the organization is enabled or the subscription is active, don't return a warning.\n        if (organization.Enabled || subscription is not\n            {\n                Status: SubscriptionStatus.Unpaid or SubscriptionStatus.Canceled\n            })\n        {\n            return null;\n        }\n\n        // If the organization is managed by a provider, return a warning asking them to contact the provider.\n        if (provider != null)\n        {\n            return new InactiveSubscriptionWarning { Resolution = \"contact_provider\" };\n        }\n\n        var isOrganizationOwner = await currentContext.OrganizationOwner(organization.Id);\n\n        /* If the organization is not managed by a provider and this user is the owner, return a warning based\n           on the subscription status. */\n        if (isOrganizationOwner)\n        {\n            return subscription.Status switch\n            {\n                SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning\n                {\n                    Resolution = \"add_payment_method\"\n                },\n                SubscriptionStatus.Canceled => new InactiveSubscriptionWarning\n                {\n                    Resolution = \"resubscribe\"\n                },\n                _ => null\n            };\n        }\n\n        // Otherwise, return a warning asking them to contact the owner.\n        return new InactiveSubscriptionWarning { Resolution = \"contact_owner\" };\n    }\n\n    private async Task<ResellerRenewalWarning?> GetResellerRenewalWarningAsync(\n        Provider? provider,\n        Subscription subscription)\n    {\n        if (provider is not\n            {\n                Type: ProviderType.Reseller\n            })\n        {\n            return null;\n        }\n\n        if (subscription.CollectionMethod != CollectionMethod.SendInvoice)\n        {\n            return null;\n        }\n\n        var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;\n\n        // ReSharper disable once ConvertIfStatementToSwitchStatement\n        if (subscription is\n            {\n                Status: SubscriptionStatus.Trialing or SubscriptionStatus.Active,\n                LatestInvoice: null or { Status: InvoiceStatus.Paid },\n                Items.Data.Count: > 0\n            })\n        {\n            var currentPeriodEnd = subscription.GetCurrentPeriodEnd();\n\n            if (currentPeriodEnd != null && (currentPeriodEnd.Value - now).TotalDays <= 14)\n            {\n                return new ResellerRenewalWarning\n                {\n                    Type = \"upcoming\",\n                    Upcoming = new ResellerRenewalWarning.UpcomingRenewal\n                    {\n                        RenewalDate = currentPeriodEnd.Value\n                    }\n                };\n            }\n        }\n\n        if (subscription is\n            {\n                Status: SubscriptionStatus.Active,\n                LatestInvoice: { Status: InvoiceStatus.Open, DueDate: not null }\n            } && subscription.LatestInvoice.DueDate > now)\n        {\n            return new ResellerRenewalWarning\n            {\n                Type = \"issued\",\n                Issued = new ResellerRenewalWarning.IssuedRenewal\n                {\n                    IssuedDate = subscription.LatestInvoice.Created,\n                    DueDate = subscription.LatestInvoice.DueDate.Value\n                }\n            };\n        }\n\n        // ReSharper disable once InvertIf\n        if (subscription.Status == SubscriptionStatus.PastDue)\n        {\n            var openInvoices = await stripeAdapter.SearchInvoiceAsync(new InvoiceSearchOptions\n            {\n                Query = $\"subscription:'{subscription.Id}' status:'open'\"\n            });\n\n            var earliestOverdueInvoice = openInvoices\n                .Where(invoice => invoice.DueDate != null && invoice.DueDate < now)\n                .MinBy(invoice => invoice.Created);\n\n            if (earliestOverdueInvoice != null)\n            {\n                return new ResellerRenewalWarning\n                {\n                    Type = \"past_due\",\n                    PastDue = new ResellerRenewalWarning.PastDueRenewal\n                    {\n                        SuspensionDate = earliestOverdueInvoice.DueDate!.Value.AddDays(30)\n                    }\n                };\n            }\n        }\n\n        return null;\n    }\n\n    private async Task<TaxIdWarning?> GetTaxIdWarningAsync(\n        Organization organization,\n        Customer customer,\n        Provider? provider)\n    {\n        if (TaxHelpers.IsDirectTaxCountry(customer.Address?.Country))\n        {\n            return null;\n        }\n\n        var productTier = organization.PlanType.GetProductTier();\n\n        // Only business tier customers can have tax IDs\n        if (productTier is not ProductTierType.Teams and not ProductTierType.Enterprise)\n        {\n            return null;\n        }\n\n        // Only an organization owner can update a tax ID\n        if (!await currentContext.OrganizationOwner(organization.Id))\n        {\n            return null;\n        }\n\n        if (provider != null)\n        {\n            return null;\n        }\n\n        // Get active and scheduled registrations\n        var registrations = (await Task.WhenAll(\n                stripeAdapter.ListTaxRegistrationsAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }),\n                stripeAdapter.ListTaxRegistrationsAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled })))\n            .SelectMany(registrations => registrations.Data);\n\n        // Find the matching registration for the customer\n        var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address?.Country);\n\n        // If we're not registered in their country, we don't need a warning\n        if (registration == null)\n        {\n            return null;\n        }\n\n        var taxId = customer.TaxIds.FirstOrDefault();\n\n        return taxId switch\n        {\n            // Customer's tax ID is missing\n            null => new TaxIdWarning { Type = \"tax_id_missing\" },\n            // Not sure if this case is valid, but Stripe says this property is nullable\n            not null when taxId.Verification == null => null,\n            // Customer's tax ID is pending verification\n            not null when taxId.Verification.Status == TaxIdVerificationStatus.Pending => new TaxIdWarning { Type = \"tax_id_pending_verification\" },\n            // Customer's tax ID failed verification\n            not null when taxId.Verification.Status == TaxIdVerificationStatus.Unverified => new TaxIdWarning { Type = \"tax_id_failed_verification\" },\n            _ => null\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api.OrganizationLicenses;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Billing.Organizations.Queries;\n\npublic interface IGetSelfHostedOrganizationLicenseQuery\n{\n    Task<OrganizationLicense> GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection);\n}\n\npublic class GetSelfHostedOrganizationLicenseQuery : BaseIdentityClientService, IGetSelfHostedOrganizationLicenseQuery\n{\n    private readonly IGlobalSettings _globalSettings;\n\n    public GetSelfHostedOrganizationLicenseQuery(IHttpClientFactory httpFactory, IGlobalSettings globalSettings, ILogger<GetSelfHostedOrganizationLicenseQuery> logger, ICurrentContext currentContext)\n        : base(\n            httpFactory,\n            globalSettings.Installation.ApiUri,\n            globalSettings.Installation.IdentityUri,\n            \"api.licensing\",\n            $\"installation.{globalSettings.Installation.Id}\",\n            globalSettings.Installation.Key,\n            logger)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection)\n    {\n        if (!_globalSettings.SelfHosted)\n        {\n            throw new BadRequestException(\"This action is only available for self-hosted.\");\n        }\n\n        if (!_globalSettings.EnableCloudCommunication)\n        {\n            throw new BadRequestException(\"Cloud communication is disabled in global settings\");\n        }\n\n        if (!billingSyncConnection.Validate<BillingSyncConfig>(out var exception))\n        {\n            throw new BadRequestException(exception);\n        }\n\n        var billingSyncConfig = billingSyncConnection.GetConfig<BillingSyncConfig>();\n        var cloudOrganizationId = billingSyncConfig.CloudOrganizationId;\n\n        var response = await SendAsync<SelfHostedOrganizationLicenseRequestModel, OrganizationLicense>(\n            HttpMethod.Get, $\"licenses/organization/{cloudOrganizationId}\", new SelfHostedOrganizationLicenseRequestModel()\n            {\n                BillingSyncKey = billingSyncConfig.BillingSyncKey,\n                LicenseKey = organization.LicenseKey,\n            }, true);\n\n        if (response == null)\n        {\n            _logger.LogDebug(\"Organization License sync failed for '{OrgId}'\", organization.Id);\n            throw new BadRequestException(\"An error has occurred. Check your internet connection and ensure the billing token is correct.\");\n        }\n\n        return response;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Repositories/IOrganizationInstallationRepository.cs",
    "content": "﻿using Bit.Core.Billing.Organizations.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Billing.Organizations.Repositories;\n\npublic interface IOrganizationInstallationRepository : IRepository<OrganizationInstallation, Guid>\n{\n    Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId);\n    Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Services/IOrganizationBillingService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\n\nnamespace Bit.Core.Billing.Organizations.Services;\n\npublic interface IOrganizationBillingService\n{\n    /// <summary>\n    /// <para>Establishes the Stripe entities necessary for a Bitwarden <see cref=\"Organization\"/> using the provided <paramref name=\"sale\"/>.</para>\n    /// <para>\n    /// The method first checks to see if the\n    /// provided <see cref=\"OrganizationSale.Organization\"/> already has a Stripe <see cref=\"Stripe.Customer\"/> using the <see cref=\"Organization.GatewayCustomerId\"/>.\n    /// If it doesn't, the method creates one using the <paramref name=\"sale\"/>'s <see cref=\"OrganizationSale.CustomerSetup\"/>. The method then creates a Stripe <see cref=\"Stripe.Subscription\"/>\n    /// for the created or existing customer using the provided <see cref=\"OrganizationSale.SubscriptionSetup\"/>.\n    /// </para>\n    /// </summary>\n    /// <param name=\"sale\">The data required to establish the Stripe entities responsible for billing the organization.</param>\n    /// <example>\n    /// <code>\n    /// var sale = OrganizationSale.From(organization, organizationSignup);\n    /// await organizationBillingService.Finalize(sale);\n    /// </code>\n    /// </example>\n    Task Finalize(OrganizationSale sale);\n\n    /// <summary>\n    /// Updates the subscription with new plan frequencies and changes the collection method to charge_automatically if a valid payment method exists.\n    /// Validates that the customer has a payment method attached before switching to automatic charging.\n    /// Handles both Password Manager and Secrets Manager subscription items separately to ensure billing interval compatibility.\n    /// </summary>\n    /// <param name=\"organization\">The Organization whose subscription to update.</param>\n    /// <param name=\"newPlanType\">The Stripe price/plan for the new Password Manager and secrets manager.</param>\n    /// <exception cref=\"ArgumentNullException\">Thrown when the <paramref name=\"organization\"/> is <see langword=\"null\"/>.</exception>\n    /// <exception cref=\"BillingException\">Thrown when no payment method is found for the customer, no plan IDs are provided, or subscription update fails.</exception>\n    Task UpdateSubscriptionPlanFrequency(Organization organization, PlanType newPlanType);\n\n    /// <summary>\n    /// Updates the organization name and email on the Stripe customer entry.\n    /// This only updates Stripe, not the Bitwarden database.\n    /// </summary>\n    /// <param name=\"organization\">The organization to update in Stripe.</param>\n    Task UpdateOrganizationNameAndEmail(Organization organization);\n}\n"
  },
  {
    "path": "src/Core/Billing/Organizations/Services/OrganizationBillingService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models.Sales;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Services;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Braintree;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\nusing static Bit.Core.Billing.Utilities;\nusing Customer = Stripe.Customer;\nusing StripeConstants = Bit.Core.Billing.Constants.StripeConstants;\nusing Subscription = Stripe.Subscription;\n\n\nnamespace Bit.Core.Billing.Organizations.Services;\n\npublic class OrganizationBillingService(\n    IBraintreeGateway braintreeGateway,\n    IGlobalSettings globalSettings,\n    IHasPaymentMethodQuery hasPaymentMethodQuery,\n    ILogger<OrganizationBillingService> logger,\n    IOrganizationRepository organizationRepository,\n    IPricingClient pricingClient,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService,\n    ISubscriptionDiscountService subscriptionDiscountService,\n    ITaxService taxService) : IOrganizationBillingService\n{\n    public async Task Finalize(OrganizationSale sale)\n    {\n        var (organization, customerSetup, subscriptionSetup, owner) = sale;\n\n        // Validate all provided coupons. Fail fast if any coupon is invalid.\n        // Validation includes user-specific eligibility checks to ensure the owner has never had premium\n        // and that this is for a Families subscription.\n        // Only validate discounts if owner is provided (i.e., the user performing the upgrade is an owner).\n        var validatedCoupons = new List<string>();\n        if (customerSetup?.Coupons is { Length: > 0 } && owner != null)\n        {\n            // Only Families plans support user-provided coupons\n            if (subscriptionSetup.PlanType.GetProductTier() == ProductTierType.Families)\n            {\n                validatedCoupons = customerSetup.Coupons\n                    .Where(c => !string.IsNullOrWhiteSpace(c))\n                    .Select(c => c.Trim())\n                    .ToList();\n\n                if (validatedCoupons.Count > 0)\n                {\n                    var allValid = await subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n                        owner, validatedCoupons, DiscountTierType.Families);\n\n                    if (!allValid)\n                    {\n                        throw new BadRequestException(\"Discount expired. Please review your cart total and try again\");\n                    }\n                }\n            }\n        }\n\n        var customer = string.IsNullOrEmpty(organization.GatewayCustomerId) && customerSetup != null\n            ? await CreateCustomerAsync(organization, customerSetup, subscriptionSetup.PlanType)\n            : await GetCustomerWhileEnsuringCorrectTaxExemptionAsync(organization, subscriptionSetup);\n\n        var subscription = await CreateSubscriptionAsync(organization, customer, subscriptionSetup, validatedCoupons);\n\n        if (subscription.Status is StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active)\n        {\n            organization.Enabled = true;\n            organization.ExpirationDate = subscription.GetCurrentPeriodEnd();\n            await organizationRepository.ReplaceAsync(organization);\n        }\n    }\n\n    public async Task UpdateSubscriptionPlanFrequency(\n        Organization organization, PlanType newPlanType)\n    {\n        ArgumentNullException.ThrowIfNull(organization);\n\n        var subscription = await subscriberService.GetSubscriptionOrThrow(organization);\n        var subscriptionItems = subscription.Items.Data;\n\n        var newPlan = await pricingClient.GetPlanOrThrow(newPlanType);\n        var oldPlan = await pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        // Build the subscription update options\n        var subscriptionItemOptions = new List<SubscriptionItemOptions>();\n        foreach (var item in subscriptionItems)\n        {\n            var subscriptionItemOption = new SubscriptionItemOptions\n            {\n                Id = item.Id,\n                Quantity = item.Quantity,\n                Price = item.Price.Id == oldPlan.SecretsManager.StripeSeatPlanId ? newPlan.SecretsManager.StripeSeatPlanId : newPlan.PasswordManager.StripeSeatPlanId\n            };\n\n            subscriptionItemOptions.Add(subscriptionItemOption);\n        }\n        var updateOptions = new SubscriptionUpdateOptions\n        {\n            Items = subscriptionItemOptions,\n            ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations\n        };\n\n        try\n        {\n            // Update the subscription in Stripe\n            await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, updateOptions);\n            organization.PlanType = newPlan.Type;\n            await organizationRepository.ReplaceAsync(organization);\n        }\n        catch (StripeException stripeException)\n        {\n            logger.LogError(stripeException, \"Failed to update subscription plan for subscriber ({SubscriberID}): {Error}\",\n                organization.Id, stripeException.Message);\n\n            throw new BillingException(\n                message: \"An error occurred while updating the subscription plan\",\n                innerException: stripeException);\n        }\n    }\n\n    public async Task UpdateOrganizationNameAndEmail(Organization organization)\n    {\n        if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))\n        {\n            logger.LogWarning(\n                \"Organization ({OrganizationId}) has no Stripe customer to update\",\n                organization.Id);\n            return;\n        }\n\n        var newDisplayName = organization.DisplayName();\n\n        // Organization.DisplayName() can return null - handle gracefully\n        if (string.IsNullOrWhiteSpace(newDisplayName))\n        {\n            logger.LogWarning(\n                \"Organization ({OrganizationId}) has no name to update in Stripe\",\n                organization.Id);\n            return;\n        }\n\n        await stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId,\n            new CustomerUpdateOptions\n            {\n                Email = organization.BillingEmail,\n                Description = newDisplayName,\n                InvoiceSettings = new CustomerInvoiceSettingsOptions\n                {\n                    // This overwrites the existing custom fields for this organization\n                    CustomFields = [\n                        new CustomerInvoiceSettingsCustomFieldOptions\n                        {\n                            Name = organization.SubscriberType(),\n                            Value = newDisplayName\n                        }]\n                },\n            });\n    }\n\n    #region Utilities\n\n    private async Task<Customer> CreateCustomerAsync(\n        Organization organization,\n        CustomerSetup customerSetup,\n        PlanType? updatedPlanType = null)\n    {\n        var planType = updatedPlanType ?? organization.PlanType;\n\n        var displayName = organization.DisplayName();\n\n        var customerCreateOptions = new CustomerCreateOptions\n        {\n            Description = organization.DisplayBusinessName(),\n            Email = organization.BillingEmail,\n            Expand = [\"tax\", \"tax_ids\"],\n            InvoiceSettings = new CustomerInvoiceSettingsOptions\n            {\n                CustomFields = [\n                    new CustomerInvoiceSettingsCustomFieldOptions\n                    {\n                        Name = organization.SubscriberType(),\n                        Value = displayName.Length <= 30\n                            ? displayName\n                            : displayName[..30]\n                    }]\n            },\n            Metadata = new Dictionary<string, string>\n            {\n                [\"organizationId\"] = organization.Id.ToString(),\n                [\"region\"] = globalSettings.BaseServiceUri.CloudRegion\n            }\n        };\n\n        var braintreeCustomerId = \"\";\n        var setupIntentId = \"\";\n\n        if (customerSetup.IsBillable)\n        {\n            if (customerSetup.TokenizedPaymentSource is not\n                {\n                    Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,\n                    Token: not null and not \"\"\n                })\n            {\n                logger.LogError(\n                    \"Cannot create customer for organization ({OrganizationID}) without a valid payment source\",\n                    organization.Id);\n\n                throw new BillingException();\n            }\n\n            if (customerSetup.TaxInformation is not { Country: not null and not \"\", PostalCode: not null and not \"\" })\n            {\n                logger.LogError(\n                    \"Cannot create customer for organization ({OrganizationID}) without valid tax information\",\n                    organization.Id);\n\n                throw new BillingException();\n            }\n\n            customerCreateOptions.Address = new AddressOptions\n            {\n                Line1 = customerSetup.TaxInformation.Line1,\n                Line2 = customerSetup.TaxInformation.Line2,\n                City = customerSetup.TaxInformation.City,\n                PostalCode = customerSetup.TaxInformation.PostalCode,\n                State = customerSetup.TaxInformation.State,\n                Country = customerSetup.TaxInformation.Country\n            };\n\n            customerCreateOptions.Tax = new CustomerTaxOptions\n            {\n                ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately\n            };\n\n            if (planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families &&\n                !TaxHelpers.IsDirectTaxCountry(customerSetup.TaxInformation.Country))\n            {\n                customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;\n            }\n\n            if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId))\n            {\n                var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country,\n                    customerSetup.TaxInformation.TaxId);\n\n                if (taxIdType == null)\n                {\n                    logger.LogWarning(\"Could not determine tax ID type for organization '{OrganizationID}' in country '{Country}' with tax ID '{TaxID}'.\",\n                        organization.Id,\n                        customerSetup.TaxInformation.Country,\n                        customerSetup.TaxInformation.TaxId);\n\n                    throw new BadRequestException(\"billingTaxIdTypeInferenceError\");\n                }\n\n                customerCreateOptions.TaxIdData =\n                [\n                    new CustomerTaxIdDataOptions { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId }\n                ];\n\n                if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)\n                {\n                    customerCreateOptions.TaxIdData.Add(new CustomerTaxIdDataOptions\n                    {\n                        Type = StripeConstants.TaxIdType.EUVAT,\n                        Value = $\"ES{customerSetup.TaxInformation.TaxId}\"\n                    });\n                }\n            }\n\n            var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource;\n\n            // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault\n            switch (paymentMethodType)\n            {\n                case PaymentMethodType.BankAccount:\n                    {\n                        var setupIntent =\n                            (await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions { PaymentMethod = paymentMethodToken }))\n                            .FirstOrDefault();\n\n                        if (setupIntent == null)\n                        {\n                            logger.LogError(\"Cannot create customer for organization ({OrganizationID}) without a setup intent for their bank account\", organization.Id);\n                            throw new BillingException();\n                        }\n\n                        setupIntentId = setupIntent.Id;\n                        break;\n                    }\n                case PaymentMethodType.Card:\n                    {\n                        customerCreateOptions.PaymentMethod = paymentMethodToken;\n                        customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethodToken;\n                        break;\n                    }\n                case PaymentMethodType.PayPal:\n                    {\n                        braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(organization, paymentMethodToken);\n                        customerCreateOptions.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;\n                        break;\n                    }\n                default:\n                    {\n                        logger.LogError(\"Cannot create customer for organization ({OrganizationID}) using payment method type ({PaymentMethodType}) as it is not supported\", organization.Id, paymentMethodType.ToString());\n                        throw new BillingException();\n                    }\n            }\n        }\n\n        try\n        {\n            var customer = await stripeAdapter.CreateCustomerAsync(customerCreateOptions);\n\n            if (!string.IsNullOrEmpty(setupIntentId))\n            {\n                await stripeAdapter.UpdateSetupIntentAsync(setupIntentId,\n                    new SetupIntentUpdateOptions { Customer = customer.Id });\n            }\n\n            organization.Gateway = GatewayType.Stripe;\n            organization.GatewayCustomerId = customer.Id;\n            await organizationRepository.ReplaceAsync(organization);\n\n            return customer;\n        }\n        catch (StripeException stripeException) when (stripeException.StripeError?.Code ==\n                                                      StripeConstants.ErrorCodes.CustomerTaxLocationInvalid)\n        {\n            await Revert();\n            throw new BadRequestException(\n                \"Your location wasn't recognized. Please ensure your country and postal code are valid.\");\n        }\n        catch (StripeException stripeException) when (stripeException.StripeError?.Code ==\n                                                      StripeConstants.ErrorCodes.TaxIdInvalid)\n        {\n            await Revert();\n            throw new BadRequestException(\n                \"Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.\");\n        }\n        catch\n        {\n            await Revert();\n            throw;\n        }\n\n        async Task Revert()\n        {\n            if (customerSetup.IsBillable)\n            {\n                // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault\n                switch (customerSetup.TokenizedPaymentSource!.Type)\n                {\n                    case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):\n                        {\n                            await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);\n                            break;\n                        }\n                }\n            }\n        }\n    }\n\n    private async Task<Subscription> CreateSubscriptionAsync(\n        Organization organization,\n        Customer customer,\n        SubscriptionSetup subscriptionSetup,\n        IReadOnlyList<string> coupons)\n    {\n        var plan = await pricingClient.GetPlanOrThrow(subscriptionSetup.PlanType);\n\n        var passwordManagerOptions = subscriptionSetup.PasswordManagerOptions;\n\n        var subscriptionItemOptionsList = new List<SubscriptionItemOptions>\n        {\n            plan.HasNonSeatBasedPasswordManagerPlan()\n                ? new SubscriptionItemOptions\n                {\n                    Price = plan.PasswordManager.StripePlanId,\n                    Quantity = 1\n                }\n                : new SubscriptionItemOptions\n                {\n                    Price = plan.PasswordManager.StripeSeatPlanId,\n                    Quantity = passwordManagerOptions.Seats\n                }\n        };\n\n        if (passwordManagerOptions.PremiumAccess is true)\n        {\n            subscriptionItemOptionsList.Add(new SubscriptionItemOptions\n            {\n                Price = plan.PasswordManager.StripePremiumAccessPlanId,\n                Quantity = 1\n            });\n        }\n\n        if (passwordManagerOptions.Storage is > 0)\n        {\n            subscriptionItemOptionsList.Add(new SubscriptionItemOptions\n            {\n                Price = plan.PasswordManager.StripeStoragePlanId,\n                Quantity = passwordManagerOptions.Storage\n            });\n        }\n\n        var secretsManagerOptions = subscriptionSetup.SecretsManagerOptions;\n\n        if (secretsManagerOptions != null)\n        {\n            subscriptionItemOptionsList.Add(new SubscriptionItemOptions\n            {\n                Price = plan.SecretsManager.StripeSeatPlanId,\n                Quantity = secretsManagerOptions.Seats\n            });\n\n            if (secretsManagerOptions.ServiceAccounts is > 0)\n            {\n                subscriptionItemOptionsList.Add(new SubscriptionItemOptions\n                {\n                    Price = plan.SecretsManager.StripeServiceAccountPlanId,\n                    Quantity = secretsManagerOptions.ServiceAccounts\n                });\n            }\n        }\n\n        var subscriptionCreateOptions = new SubscriptionCreateOptions\n        {\n            CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,\n            Customer = customer.Id,\n            Discounts = coupons.Count > 0 ? coupons.Select(c => new SubscriptionDiscountOptions { Coupon = c }).ToList() : null,\n            Items = subscriptionItemOptionsList,\n            Metadata = new Dictionary<string, string>\n            {\n                [\"organizationId\"] = organization.Id.ToString(),\n                [\"trialInitiationPath\"] = !string.IsNullOrEmpty(subscriptionSetup.InitiationPath) &&\n                    subscriptionSetup.InitiationPath.Contains(\"trial from marketing website\")\n                    ? \"marketing-initiated\"\n                    : \"product-initiated\"\n            },\n            OffSession = true,\n            TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays\n        };\n\n        var hasPaymentMethod = await hasPaymentMethodQuery.Run(organization);\n\n        // Only set trial_settings.end_behavior.missing_payment_method to \"cancel\"\n        // if there is no payment method AND there's an actual trial period\n        if (!hasPaymentMethod && subscriptionCreateOptions.TrialPeriodDays > 0)\n        {\n            subscriptionCreateOptions.TrialSettings = new SubscriptionTrialSettingsOptions\n            {\n                EndBehavior = new SubscriptionTrialSettingsEndBehaviorOptions\n                {\n                    MissingPaymentMethod = \"cancel\"\n                }\n            };\n        }\n\n        if (customer.HasBillingLocation())\n        {\n            subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };\n        }\n\n        var subscription = await stripeAdapter.CreateSubscriptionAsync(subscriptionCreateOptions);\n\n        organization.GatewaySubscriptionId = subscription.Id;\n        await organizationRepository.ReplaceAsync(organization);\n\n        return subscription;\n    }\n\n    private async Task<Customer> GetCustomerWhileEnsuringCorrectTaxExemptionAsync(\n        Organization organization,\n        SubscriptionSetup subscriptionSetup)\n    {\n        var customer = await subscriberService.GetCustomerOrThrow(organization,\n            new CustomerGetOptions { Expand = [\"tax\", \"tax_ids\"] });\n\n        if (subscriptionSetup.PlanType.GetProductTier() is\n                not (ProductTierType.Teams or\n                ProductTierType.TeamsStarter or\n                ProductTierType.Enterprise))\n        {\n            return customer;\n        }\n\n        List<string> expansions = [\"tax\", \"tax_ids\"];\n        var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt);\n        customer = customer switch\n        {\n            { Address.Country: not null and not \"\", TaxExempt: var customerTaxExemptStatus }\n                when determinedTaxExemptStatus != customerTaxExemptStatus =>\n                await stripeAdapter.UpdateCustomerAsync(customer.Id,\n                    new CustomerUpdateOptions { Expand = expansions, TaxExempt = determinedTaxExemptStatus }),\n            _ => customer\n        };\n\n        return customer;\n    }\n\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Clients/BitPayClient.cs",
    "content": "﻿using Bit.Core.Settings;\nusing BitPayLight;\nusing BitPayLight.Models.Invoice;\n\nnamespace Bit.Core.Billing.Payment.Clients;\n\npublic interface IBitPayClient\n{\n    Task<Invoice> GetInvoice(string invoiceId);\n    Task<Invoice> CreateInvoice(Invoice invoice);\n}\n\npublic class BitPayClient(\n    GlobalSettings globalSettings) : IBitPayClient\n{\n    private readonly BitPay _bitPay = new(\n        globalSettings.BitPay.Token, globalSettings.BitPay.Production ? Env.Prod : Env.Test);\n\n    public Task<Invoice> GetInvoice(string invoiceId)\n        => _bitPay.GetInvoice(invoiceId);\n\n    public Task<Invoice> CreateInvoice(Invoice invoice)\n        => _bitPay.CreateInvoice(invoice);\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Payment.Clients;\nusing Bit.Core.Entities;\nusing Bit.Core.Settings;\nusing BitPayLight.Models.Invoice;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Billing.Payment.Commands;\n\nusing static BitPayConstants;\n\npublic interface ICreateBitPayInvoiceForCreditCommand\n{\n    Task<BillingCommandResult<string>> Run(\n        ISubscriber subscriber,\n        decimal amount,\n        string redirectUrl);\n}\n\npublic class CreateBitPayInvoiceForCreditCommand(\n    IBitPayClient bitPayClient,\n    GlobalSettings globalSettings,\n    ILogger<CreateBitPayInvoiceForCreditCommand> logger) : BaseBillingCommand<CreateBitPayInvoiceForCreditCommand>(logger), ICreateBitPayInvoiceForCreditCommand\n{\n    protected override Conflict DefaultConflict => new(\"We had a problem applying your account credit. Please contact support for assistance.\");\n\n    public Task<BillingCommandResult<string>> Run(\n        ISubscriber subscriber,\n        decimal amount,\n        string redirectUrl) => HandleAsync<string>(async () =>\n    {\n        var (name, email, posData) = GetSubscriberInformation(subscriber);\n\n        var notificationUrl = $\"{globalSettings.BitPay.NotificationUrl}?key={globalSettings.BitPay.WebhookKey}\";\n\n        var invoice = new Invoice\n        {\n            Buyer = new Buyer { Email = email, Name = name },\n            Currency = \"USD\",\n            ExtendedNotifications = true,\n            FullNotifications = true,\n            ItemDesc = \"Bitwarden\",\n            NotificationUrl = notificationUrl,\n            PosData = posData,\n            Price = Convert.ToDouble(amount),\n            RedirectUrl = redirectUrl\n        };\n\n        var created = await bitPayClient.CreateInvoice(invoice);\n        return created.Url;\n    });\n\n    private static (string? Name, string? Email, string POSData) GetSubscriberInformation(\n        ISubscriber subscriber) => subscriber switch\n        {\n            User user => (user.Email, user.Email, $\"userId:{user.Id},{PosDataKeys.AccountCredit}\"),\n            Organization organization => (organization.Name, organization.BillingEmail,\n                $\"organizationId:{organization.Id},{PosDataKeys.AccountCredit}\"),\n            Provider provider => (provider.Name, provider.BillingEmail, $\"providerId:{provider.Id},{PosDataKeys.AccountCredit}\"),\n            _ => throw new ArgumentOutOfRangeException(nameof(subscriber))\n        };\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs",
    "content": "﻿using Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Entities;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Payment.Commands;\n\npublic interface IUpdateBillingAddressCommand\n{\n    Task<BillingCommandResult<BillingAddress>> Run(\n        ISubscriber subscriber,\n        BillingAddress billingAddress);\n}\n\npublic class UpdateBillingAddressCommand(\n    ILogger<UpdateBillingAddressCommand> logger,\n    ISubscriberService subscriberService,\n    IStripeAdapter stripeAdapter) : BaseBillingCommand<UpdateBillingAddressCommand>(logger), IUpdateBillingAddressCommand\n{\n    protected override Conflict DefaultConflict =>\n        new(\"We had a problem updating your billing address. Please contact support for assistance.\");\n\n    public Task<BillingCommandResult<BillingAddress>> Run(\n        ISubscriber subscriber,\n        BillingAddress billingAddress) => HandleAsync(async () =>\n    {\n        if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))\n        {\n            await subscriberService.CreateStripeCustomer(subscriber);\n        }\n\n        return subscriber.GetProductUsageType() switch\n        {\n            ProductUsageType.Personal => await UpdatePersonalBillingAddressAsync(subscriber, billingAddress),\n            ProductUsageType.Business => await UpdateBusinessBillingAddressAsync(subscriber, billingAddress)\n        };\n    });\n\n    private async Task<BillingCommandResult<BillingAddress>> UpdatePersonalBillingAddressAsync(\n        ISubscriber subscriber,\n        BillingAddress billingAddress)\n    {\n        var customer =\n            await stripeAdapter.UpdateCustomerAsync(subscriber.GatewayCustomerId,\n                new CustomerUpdateOptions\n                {\n                    Address = new AddressOptions\n                    {\n                        Country = billingAddress.Country,\n                        PostalCode = billingAddress.PostalCode,\n                        Line1 = billingAddress.Line1,\n                        Line2 = billingAddress.Line2,\n                        City = billingAddress.City,\n                        State = billingAddress.State\n                    },\n                    Expand = [\"subscriptions\"]\n                });\n\n        await EnableAutomaticTaxAsync(subscriber, customer);\n\n        return BillingAddress.From(customer.Address);\n    }\n\n    private async Task<BillingCommandResult<BillingAddress>> UpdateBusinessBillingAddressAsync(\n        ISubscriber subscriber,\n        BillingAddress billingAddress)\n    {\n        var determinedTaxExemptStatus = await GetDeterminedTaxExemptStatusAsync(subscriber.GatewayCustomerId!, billingAddress.Country);\n\n        var customer = await stripeAdapter.UpdateCustomerAsync(subscriber.GatewayCustomerId,\n            new CustomerUpdateOptions\n            {\n                Address = new AddressOptions\n                {\n                    Country = billingAddress.Country,\n                    PostalCode = billingAddress.PostalCode,\n                    Line1 = billingAddress.Line1,\n                    Line2 = billingAddress.Line2,\n                    City = billingAddress.City,\n                    State = billingAddress.State\n                },\n                Expand = [\"subscriptions\", \"tax_ids\"],\n                TaxExempt = determinedTaxExemptStatus\n            });\n\n        await EnableAutomaticTaxAsync(subscriber, customer);\n\n        var deleteExistingTaxIds = customer.TaxIds?.Any() ?? false\n            ? customer.TaxIds.Select(taxId => stripeAdapter.DeleteTaxIdAsync(customer.Id, taxId.Id)).ToList()\n            : [];\n\n        if (billingAddress.TaxId == null)\n        {\n            await Task.WhenAll(deleteExistingTaxIds);\n            return BillingAddress.From(customer.Address);\n        }\n\n        var updatedTaxId = await stripeAdapter.CreateTaxIdAsync(customer.Id,\n            new TaxIdCreateOptions { Type = billingAddress.TaxId.Code, Value = billingAddress.TaxId.Value });\n\n        if (billingAddress.TaxId.Code == StripeConstants.TaxIdType.SpanishNIF)\n        {\n            updatedTaxId = await stripeAdapter.CreateTaxIdAsync(customer.Id,\n                new TaxIdCreateOptions\n                {\n                    Type = StripeConstants.TaxIdType.EUVAT,\n                    Value = $\"ES{billingAddress.TaxId.Value}\"\n                });\n        }\n\n        await Task.WhenAll(deleteExistingTaxIds);\n\n        return BillingAddress.From(customer.Address, updatedTaxId);\n    }\n\n\n    private async Task<string> GetDeterminedTaxExemptStatusAsync(string customerId, string? billingCountry)\n    {\n        var existingCustomer = await stripeAdapter.GetCustomerAsync(customerId);\n        return TaxHelpers.DetermineTaxExemptStatus(billingCountry, existingCustomer.TaxExempt);\n    }\n\n    private async Task EnableAutomaticTaxAsync(\n        ISubscriber subscriber,\n        Customer customer)\n    {\n        if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))\n        {\n            var subscription = customer.Subscriptions.FirstOrDefault(subscription =>\n                subscription.Id == subscriber.GatewaySubscriptionId);\n\n            if (subscription is { AutomaticTax.Enabled: false })\n            {\n                await stripeAdapter.UpdateSubscriptionAsync(subscriber.GatewaySubscriptionId,\n                    new SubscriptionUpdateOptions\n                    {\n                        AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }\n                    });\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Commands/UpdatePaymentMethodCommand.cs",
    "content": "﻿using Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Braintree;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\nusing Customer = Stripe.Customer;\n\nnamespace Bit.Core.Billing.Payment.Commands;\n\npublic interface IUpdatePaymentMethodCommand\n{\n    Task<BillingCommandResult<MaskedPaymentMethod>> Run(\n        ISubscriber subscriber,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress? billingAddress);\n}\n\npublic class UpdatePaymentMethodCommand(\n    IBraintreeGateway braintreeGateway,\n    IBraintreeService braintreeService,\n    IGlobalSettings globalSettings,\n    ILogger<UpdatePaymentMethodCommand> logger,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService) : BaseBillingCommand<UpdatePaymentMethodCommand>(logger), IUpdatePaymentMethodCommand\n{\n    private readonly ILogger<UpdatePaymentMethodCommand> _logger = logger;\n    protected override Conflict DefaultConflict\n        => new(\"We had a problem updating your payment method. Please contact support for assistance.\");\n\n    public Task<BillingCommandResult<MaskedPaymentMethod>> Run(\n        ISubscriber subscriber,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress? billingAddress) => HandleAsync(async () =>\n    {\n        if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))\n        {\n            await subscriberService.CreateStripeCustomer(subscriber);\n        }\n\n        var customer = await subscriberService.GetCustomer(subscriber);\n\n        var result = paymentMethod.Type switch\n        {\n            TokenizablePaymentMethodType.BankAccount => await AddBankAccountAsync(subscriber, customer, paymentMethod.Token),\n            TokenizablePaymentMethodType.Card => await AddCardAsync(customer, paymentMethod.Token),\n            TokenizablePaymentMethodType.PayPal => await AddPayPalAsync(subscriber, customer, paymentMethod.Token),\n            _ => new BadRequest($\"Payment method type '{paymentMethod.Type}' is not supported.\")\n        };\n\n        if (billingAddress != null && customer.Address is not { Country: not null, PostalCode: not null })\n        {\n            await stripeAdapter.UpdateCustomerAsync(customer.Id,\n                new CustomerUpdateOptions\n                {\n                    Address = new AddressOptions\n                    {\n                        Country = billingAddress.Country,\n                        PostalCode = billingAddress.PostalCode\n                    }\n                });\n        }\n\n        return result;\n    });\n\n    private async Task<BillingCommandResult<MaskedPaymentMethod>> AddBankAccountAsync(\n        ISubscriber subscriber,\n        Customer customer,\n        string token)\n    {\n        var setupIntents = await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions\n        {\n            Expand = [\"data.payment_method\"],\n            PaymentMethod = token\n        });\n\n        switch (setupIntents.Count)\n        {\n            case 0:\n                _logger.LogError(\"{Command}: Could not find setup intent for subscriber's ({SubscriberID}) bank account\", CommandName, subscriber.Id);\n                return DefaultConflict;\n            case > 1:\n                _logger.LogError(\"{Command}: Found more than one set up intent for subscriber's ({SubscriberID}) bank account\", CommandName, subscriber.Id);\n                return DefaultConflict;\n        }\n\n        var setupIntent = setupIntents.First();\n\n        await stripeAdapter.UpdateSetupIntentAsync(setupIntent.Id,\n            new SetupIntentUpdateOptions { Customer = customer.Id });\n\n        _logger.LogInformation(\"{Command}: Successfully linked Setup Intent ({SetupIntentId}) to customer ({CustomerId}) for subscriber ({SubscriberID})\", CommandName, setupIntent.Id, customer.Id, subscriber.Id);\n\n        await UnlinkBraintreeCustomerAsync(customer);\n\n        return MaskedPaymentMethod.From(setupIntent);\n    }\n\n    private async Task<BillingCommandResult<MaskedPaymentMethod>> AddCardAsync(\n        Customer customer,\n        string token)\n    {\n        var paymentMethod = await stripeAdapter.AttachPaymentMethodAsync(token, new PaymentMethodAttachOptions { Customer = customer.Id });\n\n        await stripeAdapter.UpdateCustomerAsync(customer.Id,\n            new CustomerUpdateOptions\n            {\n                InvoiceSettings = new CustomerInvoiceSettingsOptions { DefaultPaymentMethod = token }\n            });\n\n        await UnlinkBraintreeCustomerAsync(customer);\n\n        return MaskedPaymentMethod.From(paymentMethod.Card);\n    }\n\n    private async Task<BillingCommandResult<MaskedPaymentMethod>> AddPayPalAsync(\n        ISubscriber subscriber,\n        Customer customer,\n        string token)\n    {\n        var braintreeCustomer = await braintreeService.GetCustomer(customer);\n\n        if (braintreeCustomer != null)\n        {\n            await ReplaceBraintreePaymentMethodAsync(braintreeCustomer, token);\n        }\n        else\n        {\n            braintreeCustomer = await CreateBraintreeCustomerAsync(subscriber, token);\n\n            var metadata = new Dictionary<string, string>\n            {\n                [StripeConstants.MetadataKeys.BraintreeCustomerId] = braintreeCustomer.Id\n            };\n\n            await stripeAdapter.UpdateCustomerAsync(customer.Id, new CustomerUpdateOptions { Metadata = metadata });\n        }\n\n        // If the subscriber has an incomplete subscription, pay the invoice with the new PayPal payment method\n        if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))\n        {\n            var subscription = await stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId);\n\n            if (subscription.Status == StripeConstants.SubscriptionStatus.Incomplete)\n            {\n                var invoice = await stripeAdapter.UpdateInvoiceAsync(subscription.LatestInvoiceId,\n                    new InvoiceUpdateOptions\n                    {\n                        AutoAdvance = false,\n                        Expand = [\"customer\"]\n                    });\n\n                await braintreeService.PayInvoice(new UserId(subscriber.Id), invoice);\n            }\n        }\n\n        var payPalAccount = braintreeCustomer.DefaultPaymentMethod as PayPalAccount;\n\n        return MaskedPaymentMethod.From(payPalAccount!);\n    }\n\n    private async Task<Braintree.Customer> CreateBraintreeCustomerAsync(\n        ISubscriber subscriber,\n        string token)\n    {\n        var braintreeCustomerId =\n            subscriber.BraintreeCustomerIdPrefix() +\n            subscriber.Id.ToString(\"N\").ToLower() +\n            CoreHelpers.RandomString(3, upper: false, numeric: false);\n\n        var result = await braintreeGateway.Customer.CreateAsync(new CustomerRequest\n        {\n            Id = braintreeCustomerId,\n            CustomFields = new Dictionary<string, string>\n            {\n                [subscriber.BraintreeIdField()] = subscriber.Id.ToString(),\n                [subscriber.BraintreeCloudRegionField()] = globalSettings.BaseServiceUri.CloudRegion\n            },\n            Email = subscriber.BillingEmailAddress(),\n            PaymentMethodNonce = token\n        });\n\n        return result.Target;\n    }\n\n    private async Task ReplaceBraintreePaymentMethodAsync(\n        Braintree.Customer customer,\n        string token)\n    {\n        var existing = customer.DefaultPaymentMethod;\n\n        var result = await braintreeGateway.PaymentMethod.CreateAsync(new PaymentMethodRequest\n        {\n            CustomerId = customer.Id,\n            PaymentMethodNonce = token\n        });\n\n        await braintreeGateway.Customer.UpdateAsync(\n            customer.Id,\n            new CustomerRequest { DefaultPaymentMethodToken = result.Target.Token });\n\n        if (existing != null)\n        {\n            await braintreeGateway.PaymentMethod.DeleteAsync(existing.Token);\n        }\n    }\n\n    private async Task UnlinkBraintreeCustomerAsync(\n        Customer customer)\n    {\n        if (customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId))\n        {\n            var metadata = new Dictionary<string, string>\n            {\n                [StripeConstants.MetadataKeys.RetiredBraintreeCustomerId] = braintreeCustomerId,\n                [StripeConstants.MetadataKeys.BraintreeCustomerId] = string.Empty\n            };\n\n            await stripeAdapter.UpdateCustomerAsync(customer.Id, new CustomerUpdateOptions { Metadata = metadata });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Models/BillingAddress.cs",
    "content": "﻿using Stripe;\n\nnamespace Bit.Core.Billing.Payment.Models;\n\npublic record TaxID(string Code, string Value);\n\npublic record BillingAddress\n{\n    public required string Country { get; set; }\n    public required string PostalCode { get; set; }\n    public string? Line1 { get; set; }\n    public string? Line2 { get; set; }\n    public string? City { get; set; }\n    public string? State { get; set; }\n    public TaxID? TaxId { get; set; }\n\n    public static BillingAddress From(Address address) => new()\n    {\n        Country = address.Country,\n        PostalCode = address.PostalCode,\n        Line1 = address.Line1,\n        Line2 = address.Line2,\n        City = address.City,\n        State = address.State\n    };\n\n    public static BillingAddress From(Address address, TaxId? taxId) =>\n        From(address) with { TaxId = taxId != null ? new TaxID(taxId.Type, taxId.Value) : null };\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Braintree;\nusing OneOf;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Payment.Models;\n\npublic record MaskedBankAccount\n{\n    public required string BankName { get; init; }\n    public required string Last4 { get; init; }\n    public string? HostedVerificationUrl { get; init; }\n    public string Type => \"bankAccount\";\n}\n\npublic record MaskedCard\n{\n    public required string Brand { get; init; }\n    public required string Last4 { get; init; }\n    public required string Expiration { get; init; }\n    public string Type => \"card\";\n}\n\npublic record MaskedPayPalAccount\n{\n    public required string Email { get; init; }\n    public string Type => \"payPal\";\n}\n\n[JsonConverter(typeof(MaskedPaymentMethodJsonConverter))]\npublic class MaskedPaymentMethod(OneOf<MaskedBankAccount, MaskedCard, MaskedPayPalAccount> input)\n    : OneOfBase<MaskedBankAccount, MaskedCard, MaskedPayPalAccount>(input)\n{\n    public static implicit operator MaskedPaymentMethod(MaskedBankAccount bankAccount) => new(bankAccount);\n    public static implicit operator MaskedPaymentMethod(MaskedCard card) => new(card);\n    public static implicit operator MaskedPaymentMethod(MaskedPayPalAccount payPal) => new(payPal);\n\n    public static MaskedPaymentMethod From(BankAccount bankAccount) => new MaskedBankAccount\n    {\n        BankName = bankAccount.BankName,\n        Last4 = bankAccount.Last4\n    };\n\n    public static MaskedPaymentMethod From(Card card) => new MaskedCard\n    {\n        Brand = card.Brand.ToLower(),\n        Last4 = card.Last4,\n        Expiration = $\"{card.ExpMonth:00}/{card.ExpYear}\"\n    };\n\n    public static MaskedPaymentMethod From(PaymentMethodCard card) => new MaskedCard\n    {\n        Brand = card.Brand.ToLower(),\n        Last4 = card.Last4,\n        Expiration = $\"{card.ExpMonth:00}/{card.ExpYear}\"\n    };\n\n    public static MaskedPaymentMethod From(SetupIntent setupIntent) => new MaskedBankAccount\n    {\n        BankName = setupIntent.PaymentMethod.UsBankAccount.BankName,\n        Last4 = setupIntent.PaymentMethod.UsBankAccount.Last4,\n        HostedVerificationUrl = setupIntent.NextAction?.VerifyWithMicrodeposits?.HostedVerificationUrl\n    };\n\n    public static MaskedPaymentMethod From(SourceCard sourceCard) => new MaskedCard\n    {\n        Brand = sourceCard.Brand.ToLower(),\n        Last4 = sourceCard.Last4,\n        Expiration = $\"{sourceCard.ExpMonth:00}/{sourceCard.ExpYear}\"\n    };\n\n    public static MaskedPaymentMethod From(PaymentMethodUsBankAccount bankAccount) => new MaskedBankAccount\n    {\n        BankName = bankAccount.BankName,\n        Last4 = bankAccount.Last4\n    };\n\n    public static MaskedPaymentMethod From(PayPalAccount payPalAccount) => new MaskedPayPalAccount { Email = payPalAccount.Email };\n}\n\npublic class MaskedPaymentMethodJsonConverter : JsonConverter<MaskedPaymentMethod>\n{\n    private const string _typePropertyName = nameof(MaskedBankAccount.Type);\n\n    public override MaskedPaymentMethod Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        var element = JsonElement.ParseValue(ref reader);\n\n        if (!element.TryGetProperty(options.PropertyNamingPolicy?.ConvertName(_typePropertyName) ?? _typePropertyName, out var typeProperty))\n        {\n            throw new JsonException(\n                $\"Failed to deserialize {nameof(MaskedPaymentMethod)}: missing '{_typePropertyName}' property\");\n        }\n\n        var type = typeProperty.GetString();\n\n        return type switch\n        {\n            \"bankAccount\" => element.Deserialize<MaskedBankAccount>(options)!,\n            \"card\" => element.Deserialize<MaskedCard>(options)!,\n            \"payPal\" => element.Deserialize<MaskedPayPalAccount>(options)!,\n            _ => throw new JsonException($\"Failed to deserialize {nameof(MaskedPaymentMethod)}: invalid '{_typePropertyName}' value - '{type}'\")\n        };\n    }\n\n    public override void Write(Utf8JsonWriter writer, MaskedPaymentMethod value, JsonSerializerOptions options)\n        => value.Switch(\n            bankAccount => JsonSerializer.Serialize(writer, bankAccount, options),\n            card => JsonSerializer.Serialize(writer, card, options),\n            payPal => JsonSerializer.Serialize(writer, payPal, options));\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Models/NonTokenizedPaymentMethod.cs",
    "content": "﻿namespace Bit.Core.Billing.Payment.Models;\n\npublic record NonTokenizedPaymentMethod\n{\n    public NonTokenizablePaymentMethodType Type { get; set; }\n}\n\npublic enum NonTokenizablePaymentMethodType\n{\n    AccountCredit,\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Models/PaymentMethod.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing OneOf;\n\nnamespace Bit.Core.Billing.Payment.Models;\n\n[JsonConverter(typeof(PaymentMethodJsonConverter))]\npublic class PaymentMethod(OneOf<TokenizedPaymentMethod, NonTokenizedPaymentMethod> input)\n    : OneOfBase<TokenizedPaymentMethod, NonTokenizedPaymentMethod>(input)\n{\n    public static implicit operator PaymentMethod(TokenizedPaymentMethod tokenized) => new(tokenized);\n    public static implicit operator PaymentMethod(NonTokenizedPaymentMethod nonTokenized) => new(nonTokenized);\n    public bool IsTokenized => IsT0;\n    public TokenizedPaymentMethod AsTokenized => AsT0;\n    public bool IsNonTokenized => IsT1;\n    public NonTokenizedPaymentMethod AsNonTokenized => AsT1;\n}\n\ninternal class PaymentMethodJsonConverter : JsonConverter<PaymentMethod>\n{\n    public override PaymentMethod Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        var element = JsonElement.ParseValue(ref reader);\n\n        if (!element.TryGetProperty(\"type\", out var typeProperty))\n        {\n            throw new JsonException(\"PaymentMethod requires a 'type' property\");\n        }\n\n        var type = typeProperty.GetString();\n\n\n        if (Enum.TryParse<TokenizablePaymentMethodType>(type, true, out var tokenizedType) &&\n            Enum.IsDefined(typeof(TokenizablePaymentMethodType), tokenizedType))\n        {\n            var token = element.TryGetProperty(\"token\", out var tokenProperty) ? tokenProperty.GetString() : null;\n            if (string.IsNullOrEmpty(token))\n            {\n                throw new JsonException(\"TokenizedPaymentMethod requires a 'token' property\");\n            }\n\n            return new TokenizedPaymentMethod { Type = tokenizedType, Token = token };\n        }\n\n        if (Enum.TryParse<NonTokenizablePaymentMethodType>(type, true, out var nonTokenizedType) &&\n            Enum.IsDefined(typeof(NonTokenizablePaymentMethodType), nonTokenizedType))\n        {\n            return new NonTokenizedPaymentMethod { Type = nonTokenizedType };\n        }\n\n        throw new JsonException($\"Unknown payment method type: {type}\");\n    }\n\n    public override void Write(Utf8JsonWriter writer, PaymentMethod value, JsonSerializerOptions options)\n    {\n        writer.WriteStartObject();\n\n        value.Switch(\n            tokenized =>\n            {\n                writer.WriteString(\"type\",\n                    tokenized.Type.ToString().ToLowerInvariant()\n                );\n                writer.WriteString(\"token\", tokenized.Token);\n            },\n            nonTokenized => { writer.WriteString(\"type\", nonTokenized.Type.ToString().ToLowerInvariant()); }\n        );\n\n        writer.WriteEndObject();\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Models/ProductUsageType.cs",
    "content": "﻿namespace Bit.Core.Billing.Payment.Models;\n\npublic enum ProductUsageType\n{\n    Personal,\n    Business\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Models/TokenizablePaymentMethodType.cs",
    "content": "﻿namespace Bit.Core.Billing.Payment.Models;\n\npublic enum TokenizablePaymentMethodType\n{\n    BankAccount,\n    Card,\n    PayPal\n}\n\npublic static class TokenizablePaymentMethodTypeExtensions\n{\n    public static TokenizablePaymentMethodType From(string type)\n    {\n        return type switch\n        {\n            \"bankAccount\" => TokenizablePaymentMethodType.BankAccount,\n            \"card\" => TokenizablePaymentMethodType.Card,\n            \"payPal\" => TokenizablePaymentMethodType.PayPal,\n            _ => throw new InvalidOperationException($\"Invalid value for {nameof(TokenizedPaymentMethod)}.{nameof(TokenizedPaymentMethod.Type)}\")\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Models/TokenizedPaymentMethod.cs",
    "content": "﻿namespace Bit.Core.Billing.Payment.Models;\n\npublic record TokenizedPaymentMethod\n{\n    public required TokenizablePaymentMethodType Type { get; set; }\n    public required string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Queries/GetApplicableDiscountsQuery.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Models.Api.Response;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Payment.Queries;\n\npublic interface IGetApplicableDiscountsQuery\n{\n    /// <summary>\n    /// Returns all discounts the user is eligible for, mapped to <see cref=\"SubscriptionDiscountResponseModel\"/>.\n    /// </summary>\n    Task<BillingCommandResult<SubscriptionDiscountResponseModel[]>> Run(User user);\n}\n\npublic class GetApplicableDiscountsQuery(\n    ISubscriptionDiscountService subscriptionDiscountService) : IGetApplicableDiscountsQuery\n{\n    public async Task<BillingCommandResult<SubscriptionDiscountResponseModel[]>> Run(User user)\n    {\n        var eligibleDiscounts = await subscriptionDiscountService.GetEligibleDiscountsAsync(user);\n        return eligibleDiscounts\n            .Select(e => SubscriptionDiscountResponseModel.From(e.Discount, e.TierEligibility))\n            .ToArray();\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Queries/GetBillingAddressQuery.cs",
    "content": "﻿using Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Payment.Queries;\n\npublic interface IGetBillingAddressQuery\n{\n    Task<BillingAddress?> Run(ISubscriber subscriber);\n}\n\npublic class GetBillingAddressQuery(\n    ISubscriberService subscriberService) : IGetBillingAddressQuery\n{\n    public async Task<BillingAddress?> Run(ISubscriber subscriber)\n    {\n        var productUsageType = subscriber.GetProductUsageType();\n\n        var options = productUsageType switch\n        {\n            ProductUsageType.Business => new CustomerGetOptions { Expand = [\"tax_ids\"] },\n            _ => new CustomerGetOptions()\n        };\n\n        var customer = await subscriberService.GetCustomer(subscriber, options);\n\n        if (customer is not { Address: { Country: not null, PostalCode: not null } })\n        {\n            return null;\n        }\n\n        var taxId = productUsageType == ProductUsageType.Business ? customer.TaxIds?.FirstOrDefault() : null;\n\n        return taxId != null\n            ? BillingAddress.From(customer.Address, taxId)\n            : BillingAddress.From(customer.Address);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Queries/GetCreditQuery.cs",
    "content": "﻿using Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Payment.Queries;\n\npublic interface IGetCreditQuery\n{\n    Task<decimal?> Run(ISubscriber subscriber);\n}\n\npublic class GetCreditQuery(\n    ISubscriberService subscriberService) : IGetCreditQuery\n{\n    public async Task<decimal?> Run(ISubscriber subscriber)\n    {\n        var customer = await subscriberService.GetCustomer(subscriber);\n\n        if (customer == null)\n        {\n            return null;\n        }\n\n        return Convert.ToDecimal(customer.Balance) * -1 / 100;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs",
    "content": "﻿using Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Braintree;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Payment.Queries;\n\npublic interface IGetPaymentMethodQuery\n{\n    Task<MaskedPaymentMethod?> Run(ISubscriber subscriber);\n}\n\npublic class GetPaymentMethodQuery(\n    IBraintreeService braintreeService,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService) : IGetPaymentMethodQuery\n{\n    public async Task<MaskedPaymentMethod?> Run(ISubscriber subscriber)\n    {\n        var customer = await subscriberService.GetCustomer(subscriber,\n            new CustomerGetOptions { Expand = [\"default_source\", \"invoice_settings.default_payment_method\"] });\n\n        if (customer == null)\n        {\n            return null;\n        }\n\n        // First check for a PayPal account\n        var braintreeCustomer = await braintreeService.GetCustomer(customer);\n\n        if (braintreeCustomer is { DefaultPaymentMethod: PayPalAccount payPalAccount })\n        {\n            return new MaskedPayPalAccount { Email = payPalAccount.Email };\n        }\n\n        // Then check for a bank account pending verification\n        var setupIntents = await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions\n        {\n            Customer = customer.Id,\n            Expand = [\"data.payment_method\"]\n        });\n\n        var unverifiedBankAccount = setupIntents?.FirstOrDefault(si => si.IsUnverifiedBankAccount());\n\n        if (unverifiedBankAccount != null)\n        {\n            return MaskedPaymentMethod.From(unverifiedBankAccount);\n        }\n\n        // Then check the default payment method\n        var paymentMethod = customer.InvoiceSettings.DefaultPaymentMethod != null\n            ? customer.InvoiceSettings.DefaultPaymentMethod.Type switch\n            {\n                \"card\" => MaskedPaymentMethod.From(customer.InvoiceSettings.DefaultPaymentMethod.Card),\n                \"us_bank_account\" => MaskedPaymentMethod.From(customer.InvoiceSettings.DefaultPaymentMethod.UsBankAccount),\n                _ => null\n            }\n            : null;\n\n        if (paymentMethod != null)\n        {\n            return paymentMethod;\n        }\n\n        return customer.DefaultSource switch\n        {\n            Card card => MaskedPaymentMethod.From(card),\n            BankAccount bankAccount => MaskedPaymentMethod.From(bankAccount),\n            Source { Card: not null } source => MaskedPaymentMethod.From(source.Card),\n            _ => null\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Queries/HasPaymentMethodQuery.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Payment.Queries;\n\nusing static StripeConstants;\n\npublic interface IHasPaymentMethodQuery\n{\n    Task<bool> Run(ISubscriber subscriber);\n}\n\npublic class HasPaymentMethodQuery(\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService) : IHasPaymentMethodQuery\n{\n    public async Task<bool> Run(ISubscriber subscriber)\n    {\n        var customer = await subscriberService.GetCustomer(subscriber);\n\n        if (customer == null)\n        {\n            return false;\n        }\n\n        var hasUnverifiedBankAccount = await HasUnverifiedBankAccountAsync(customer.Id);\n\n        return\n            !string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||\n            !string.IsNullOrEmpty(customer.DefaultSourceId) ||\n            hasUnverifiedBankAccount ||\n            customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId);\n    }\n\n    private async Task<bool> HasUnverifiedBankAccountAsync(string customerId)\n    {\n        var setupIntents = await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions\n        {\n            Customer = customerId,\n            Expand = [\"data.payment_method\"]\n        });\n\n        return setupIntents?.Any(si => si.IsUnverifiedBankAccount()) ?? false;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Payment/Registrations.cs",
    "content": "﻿using Bit.Core.Billing.Payment.Clients;\nusing Bit.Core.Billing.Payment.Commands;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Services.DiscountAudienceFilters;\nusing Bit.Core.Billing.Services.Implementations;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Billing.Payment;\n\npublic static class Registrations\n{\n    public static void AddPaymentOperations(this IServiceCollection services)\n    {\n        // Commands\n        services.AddTransient<IBitPayClient, BitPayClient>();\n        services.AddTransient<ICreateBitPayInvoiceForCreditCommand, CreateBitPayInvoiceForCreditCommand>();\n        services.AddTransient<IUpdateBillingAddressCommand, UpdateBillingAddressCommand>();\n        services.AddTransient<IUpdatePaymentMethodCommand, UpdatePaymentMethodCommand>();\n\n        // Discount services\n        services.AddScoped<IDiscountAudienceFilter, AllUsersFilter>();\n        services.AddScoped<IDiscountAudienceFilter, UserHasNoPreviousSubscriptionsFilter>();\n        services.AddScoped<IDiscountAudienceFilterFactory, DiscountAudienceFilterFactory>();\n        services.AddTransient<ISubscriptionDiscountService, SubscriptionDiscountService>();\n\n        // Queries\n        services.AddTransient<IGetApplicableDiscountsQuery, GetApplicableDiscountsQuery>();\n        services.AddTransient<IGetBillingAddressQuery, GetBillingAddressQuery>();\n        services.AddTransient<IGetCreditQuery, GetCreditQuery>();\n        services.AddTransient<IGetPaymentMethodQuery, GetPaymentMethodQuery>();\n        services.AddTransient<IHasPaymentMethodQuery, HasPaymentMethodQuery>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Portal/Commands/CreateBillingPortalSessionCommand.cs",
    "content": "﻿using Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\nusing Stripe.BillingPortal;\n\nnamespace Bit.Core.Billing.Portal.Commands;\n\nusing static StripeConstants;\n\npublic interface ICreateBillingPortalSessionCommand\n{\n    Task<BillingCommandResult<string>> Run(User user, string returnUrl);\n}\n\npublic class CreateBillingPortalSessionCommand(\n    ILogger<CreateBillingPortalSessionCommand> logger,\n    IStripeAdapter stripeAdapter)\n    : BaseBillingCommand<CreateBillingPortalSessionCommand>(logger), ICreateBillingPortalSessionCommand\n{\n    private readonly ILogger<CreateBillingPortalSessionCommand> _logger = logger;\n\n    protected override Conflict DefaultConflict =>\n        new(\"Unable to create billing portal session. Please contact support for assistance.\");\n\n    public Task<BillingCommandResult<string>> Run(User user, string returnUrl) =>\n        HandleAsync<string>(async () =>\n        {\n            if (string.IsNullOrEmpty(user.GatewayCustomerId))\n            {\n                _logger.LogWarning(\"{Command}: User ({UserId}) does not have a Stripe customer ID\",\n                    CommandName, user.Id);\n                return DefaultConflict;\n            }\n\n            if (string.IsNullOrEmpty(user.GatewaySubscriptionId))\n            {\n                _logger.LogWarning(\"{Command}: User ({UserId}) does not have a subscription\",\n                    CommandName, user.Id);\n                return DefaultConflict;\n            }\n\n            // Fetch the subscription to validate its status\n            Subscription subscription;\n            try\n            {\n                subscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId);\n            }\n            catch (StripeException stripeException)\n            {\n                _logger.LogError(stripeException,\n                    \"{Command}: Failed to fetch subscription ({SubscriptionId}) for user ({UserId})\",\n                    CommandName, user.GatewaySubscriptionId, user.Id);\n                return DefaultConflict;\n            }\n\n            // Only allow portal access for active or past_due subscriptions\n            if (subscription.Status != SubscriptionStatus.Active && subscription.Status != SubscriptionStatus.PastDue)\n            {\n                _logger.LogWarning(\n                    \"{Command}: User ({UserId}) subscription ({SubscriptionId}) has status '{Status}' which is not eligible for portal access\",\n                    CommandName, user.Id, user.GatewaySubscriptionId, subscription.Status);\n                return new BadRequest(\"Your subscription cannot be managed in its current status.\");\n            }\n\n            var options = new SessionCreateOptions\n            {\n                Customer = user.GatewayCustomerId,\n                ReturnUrl = returnUrl\n            };\n\n            var session = await stripeAdapter.CreateBillingPortalSessionAsync(options);\n\n            return session.Url;\n        });\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs",
    "content": "﻿using Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Payment.Commands;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Braintree;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\nusing Stripe;\nusing Customer = Stripe.Customer;\nusing PaymentMethod = Bit.Core.Billing.Payment.Models.PaymentMethod;\nusing Subscription = Stripe.Subscription;\n\nnamespace Bit.Core.Billing.Premium.Commands;\n\nusing static StripeConstants;\nusing static Utilities;\n\n/// <summary>\n/// Creates a premium subscription for a cloud-hosted user with Stripe payment processing.\n/// Handles customer creation, payment method setup, and subscription creation.\n/// </summary>\npublic interface ICreatePremiumCloudHostedSubscriptionCommand\n{\n    /// <summary>\n    /// Creates a premium cloud-hosted subscription for the specified user.\n    /// </summary>\n    /// <param name=\"user\">The user to create the premium subscription for. Must not yet be a premium user.</param>\n    /// <param name=\"subscriptionPurchase\">The subscription purchase details including payment method, billing address, storage, and optional coupon.</param>\n    /// <returns>A billing command result indicating success or failure with appropriate error details.</returns>\n    Task<BillingCommandResult<None>> Run(\n        User user,\n        PremiumSubscriptionPurchase subscriptionPurchase);\n}\n\npublic class CreatePremiumCloudHostedSubscriptionCommand(\n    IBraintreeGateway braintreeGateway,\n    IBraintreeService braintreeService,\n    IGlobalSettings globalSettings,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService,\n    IUserService userService,\n    IPushNotificationService pushNotificationService,\n    ILogger<CreatePremiumCloudHostedSubscriptionCommand> logger,\n    IPricingClient pricingClient,\n    IHasPaymentMethodQuery hasPaymentMethodQuery,\n    IUpdatePaymentMethodCommand updatePaymentMethodCommand,\n    ISubscriptionDiscountService subscriptionDiscountService)\n    : BaseBillingCommand<CreatePremiumCloudHostedSubscriptionCommand>(logger), ICreatePremiumCloudHostedSubscriptionCommand\n{\n    private static readonly List<string> _expand = [\"tax\"];\n    private readonly ILogger<CreatePremiumCloudHostedSubscriptionCommand> _logger = logger;\n\n    public Task<BillingCommandResult<None>> Run(\n        User user,\n        PremiumSubscriptionPurchase subscriptionPurchase) => HandleAsync<None>(async () =>\n    {\n        // A \"terminal\" subscription is one that has ended and cannot be renewed/reactivated.\n        // These are: 'canceled' (user canceled) and 'incomplete_expired' (payment failed and time expired).\n        // We allow users with terminal subscriptions to create a new subscription even if user.Premium is still true,\n        // enabling the resubscribe workflow without requiring Premium status to be cleared first.\n        var hasTerminalSubscription = await HasTerminalSubscriptionAsync(user);\n\n        if (user.Premium && !hasTerminalSubscription)\n        {\n            return new BadRequest(\"Already a premium user.\");\n        }\n\n        if (subscriptionPurchase.AdditionalStorageGb is < 0)\n        {\n            return new BadRequest(\"Additional storage must be greater than 0.\");\n        }\n\n        // Validate all provided coupons. Fail fast if any coupon is invalid to prevent charging more than expected.\n        var validatedCoupons = (subscriptionPurchase.Coupons ?? [])\n            .Where(c => !string.IsNullOrWhiteSpace(c))\n            .Select(c => c.Trim())\n            .ToList();\n\n        if (validatedCoupons.Count > 0)\n        {\n            var allValid = await subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n                user, validatedCoupons, DiscountTierType.Premium);\n\n            if (!allValid)\n            {\n                return new BadRequest(\"Discount expired. Please review your cart total and try again\");\n            }\n        }\n\n        var premiumPlan = await pricingClient.GetAvailablePremiumPlan();\n\n        Customer? customer;\n\n        /*\n         * For a new customer purchasing a new subscription, we attach the payment method while creating the customer.\n         */\n        if (string.IsNullOrEmpty(user.GatewayCustomerId))\n        {\n            customer = await CreateCustomerAsync(user, subscriptionPurchase.PaymentMethod, subscriptionPurchase.BillingAddress);\n        }\n        /*\n         * An existing customer without a payment method starting a new subscription indicates a user who previously\n         * purchased account credit but chose to use a tokenizable payment method to pay for the subscription. In this case,\n         * we need to add the payment method to their customer first. If the incoming payment method is account credit,\n         * we can just go straight to fetching the customer since there's no payment method to apply.\n         *\n         * Additionally, if this is a resubscribe scenario with a tokenized payment method, we should update the payment method\n         * to ensure the new payment method is used instead of the old one.\n         */\n        else if (subscriptionPurchase.PaymentMethod.IsTokenized && (!await hasPaymentMethodQuery.Run(user) || hasTerminalSubscription))\n        {\n            await updatePaymentMethodCommand.Run(user, subscriptionPurchase.PaymentMethod.AsTokenized, subscriptionPurchase.BillingAddress);\n            customer = await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = _expand });\n        }\n        else\n        {\n            customer = await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = _expand });\n        }\n\n        customer = await ReconcileBillingLocationAsync(customer, subscriptionPurchase.BillingAddress);\n\n        var subscription = await CreateSubscriptionAsync(user.Id, customer, premiumPlan, subscriptionPurchase.AdditionalStorageGb > 0 ? subscriptionPurchase.AdditionalStorageGb : null, validatedCoupons);\n\n        subscriptionPurchase.PaymentMethod.Switch(\n            tokenized =>\n            {\n                // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault\n                switch (tokenized)\n                {\n                    case { Type: TokenizablePaymentMethodType.PayPal }\n                        when subscription.Status == SubscriptionStatus.Incomplete:\n                    case { Type: not TokenizablePaymentMethodType.PayPal }\n                        when subscription.Status is SubscriptionStatus.Active or SubscriptionStatus.Incomplete:\n                        {\n                            user.Premium = true;\n                            user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();\n                            break;\n                        }\n                }\n            },\n            _ =>\n            {\n                if (subscription.Status != SubscriptionStatus.Active)\n                {\n                    return;\n                }\n\n                user.Premium = true;\n                user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();\n            });\n\n        user.Gateway = GatewayType.Stripe;\n        user.GatewayCustomerId = customer.Id;\n        user.GatewaySubscriptionId = subscription.Id;\n        user.MaxStorageGb = (short)(premiumPlan.Storage.Provided + subscriptionPurchase.AdditionalStorageGb.GetValueOrDefault(0));\n        user.LicenseKey = CoreHelpers.SecureRandomString(20);\n        user.RevisionDate = DateTime.UtcNow;\n\n        await userService.SaveUserAsync(user);\n        await pushNotificationService.PushSyncVaultAsync(user.Id);\n\n        return new None();\n    });\n\n    private async Task<Customer> CreateCustomerAsync(User user,\n        PaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        if (paymentMethod.IsNonTokenized)\n        {\n            _logger.LogError(\"Cannot create customer for user ({UserID}) using non-tokenized payment method. The customer should already exist\", user.Id);\n            throw new BillingException();\n        }\n\n        var subscriberName = user.SubscriberName();\n        var customerCreateOptions = new CustomerCreateOptions\n        {\n            Address = new AddressOptions\n            {\n                Line1 = billingAddress.Line1,\n                Line2 = billingAddress.Line2,\n                City = billingAddress.City,\n                PostalCode = billingAddress.PostalCode,\n                State = billingAddress.State,\n                Country = billingAddress.Country\n            },\n            Description = user.Name,\n            Email = user.Email,\n            Expand = _expand,\n            InvoiceSettings = new CustomerInvoiceSettingsOptions\n            {\n                CustomFields =\n                [\n                    new CustomerInvoiceSettingsCustomFieldOptions\n                    {\n                        Name = user.SubscriberType(),\n                        Value = subscriberName.Length <= 30\n                            ? subscriberName\n                            : subscriberName[..30]\n                    }\n                ]\n            },\n            Metadata = new Dictionary<string, string>\n            {\n                [MetadataKeys.Region] = globalSettings.BaseServiceUri.CloudRegion,\n                [MetadataKeys.UserId] = user.Id.ToString()\n            },\n            Tax = new CustomerTaxOptions\n            {\n                ValidateLocation = ValidateTaxLocationTiming.Immediately\n            }\n        };\n\n        var braintreeCustomerId = \"\";\n\n        // We have checked that the payment method is tokenized, so we can safely cast it.\n        var tokenizedPaymentMethod = paymentMethod.AsTokenized;\n        switch (tokenizedPaymentMethod.Type)\n        {\n            case TokenizablePaymentMethodType.Card:\n                {\n                    customerCreateOptions.PaymentMethod = tokenizedPaymentMethod.Token;\n                    customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = tokenizedPaymentMethod.Token;\n                    break;\n                }\n            case TokenizablePaymentMethodType.PayPal:\n                {\n                    braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, tokenizedPaymentMethod.Token);\n                    customerCreateOptions.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;\n                    break;\n                }\n            default:\n                {\n                    _logger.LogError(\"Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported\", user.Id, tokenizedPaymentMethod.Type.ToString());\n                    throw new BillingException();\n                }\n        }\n\n        try\n        {\n            return await stripeAdapter.CreateCustomerAsync(customerCreateOptions);\n        }\n        catch\n        {\n            await Revert();\n            throw;\n        }\n\n        async Task Revert()\n        {\n            // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault\n            switch (tokenizedPaymentMethod.Type)\n            {\n                case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):\n                    {\n                        await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);\n                        break;\n                    }\n            }\n        }\n    }\n\n    private async Task<Customer> ReconcileBillingLocationAsync(\n        Customer customer,\n        BillingAddress billingAddress)\n    {\n        /*\n         * If the customer was previously set up with credit, which does not require a billing location,\n         * we need to update the customer on the fly before we start the subscription.\n         */\n        if (customer is { Address: { Country: not null and not \"\", PostalCode: not null and not \"\" } })\n        {\n            return customer;\n        }\n\n        var options = new CustomerUpdateOptions\n        {\n            Address = new AddressOptions\n            {\n                Line1 = billingAddress.Line1,\n                Line2 = billingAddress.Line2,\n                City = billingAddress.City,\n                PostalCode = billingAddress.PostalCode,\n                State = billingAddress.State,\n                Country = billingAddress.Country\n            },\n            Expand = _expand,\n            Tax = new CustomerTaxOptions\n            {\n                ValidateLocation = ValidateTaxLocationTiming.Immediately\n            }\n        };\n\n        return await stripeAdapter.UpdateCustomerAsync(customer.Id, options);\n    }\n\n    private async Task<Subscription> CreateSubscriptionAsync(\n        Guid userId,\n        Customer customer,\n        Pricing.Premium.Plan premiumPlan,\n        int? storage,\n        IReadOnlyList<string> validatedCoupons)\n    {\n\n        var subscriptionItemOptionsList = new List<SubscriptionItemOptions>\n        {\n            new ()\n            {\n                Price = premiumPlan.Seat.StripePriceId,\n                Quantity = 1\n            }\n        };\n\n        if (storage is > 0)\n        {\n            subscriptionItemOptionsList.Add(new SubscriptionItemOptions\n            {\n                Price = premiumPlan.Storage.StripePriceId,\n                Quantity = storage\n            });\n        }\n\n        var usingPayPal = customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) ?? false;\n\n        var subscriptionCreateOptions = new SubscriptionCreateOptions\n        {\n            AutomaticTax = new SubscriptionAutomaticTaxOptions\n            {\n                Enabled = true\n            },\n            CollectionMethod = CollectionMethod.ChargeAutomatically,\n            Customer = customer.Id,\n            Items = subscriptionItemOptionsList,\n            Metadata = new Dictionary<string, string>\n            {\n                [MetadataKeys.UserId] = userId.ToString()\n            },\n            PaymentBehavior = usingPayPal\n                ? PaymentBehavior.DefaultIncomplete\n                : null,\n            OffSession = true\n        };\n\n        if (validatedCoupons.Count > 0)\n        {\n            subscriptionCreateOptions.Discounts = validatedCoupons\n                .Select(c => new SubscriptionDiscountOptions { Coupon = c })\n                .ToList();\n        }\n\n        var subscription = await stripeAdapter.CreateSubscriptionAsync(subscriptionCreateOptions);\n\n        if (!usingPayPal)\n        {\n            return subscription;\n        }\n\n        var invoice = await stripeAdapter.UpdateInvoiceAsync(subscription.LatestInvoiceId, new InvoiceUpdateOptions\n        {\n            AutoAdvance = false,\n            Expand = [\"customer\"]\n        });\n\n        await braintreeService.PayInvoice(new UserId(userId), invoice);\n\n        return subscription;\n    }\n\n    private async Task<bool> HasTerminalSubscriptionAsync(User user)\n    {\n        if (string.IsNullOrEmpty(user.GatewaySubscriptionId))\n        {\n            return false;\n        }\n\n        try\n        {\n            var existingSubscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId);\n            return existingSubscription.Status is\n                SubscriptionStatus.Canceled or\n                SubscriptionStatus.IncompleteExpired;\n        }\n        catch (Exception ex)\n        {\n            // Subscription doesn't exist in Stripe or can't be fetched (e.g., network issues, invalid ID)\n            // Log the issue but proceed with subscription creation to avoid blocking legitimate resubscribe attempts\n            _logger.LogWarning(ex, \"Unable to fetch existing subscription {SubscriptionId} for user {UserId}. Proceeding with subscription creation\",\n                user.GatewaySubscriptionId, user.Id);\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommand.cs",
    "content": "﻿using Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\n\nnamespace Bit.Core.Billing.Premium.Commands;\n\n/// <summary>\n/// Creates a premium subscription for a self-hosted user.\n/// Validates the license and applies premium benefits including storage limits based on the license terms.\n/// </summary>\npublic interface ICreatePremiumSelfHostedSubscriptionCommand\n{\n    /// <summary>\n    /// Creates a premium self-hosted subscription for the specified user using the provided license.\n    /// </summary>\n    /// <param name=\"user\">The user to create the premium subscription for. Must not already be a premium user.</param>\n    /// <param name=\"license\">The user license containing the premium subscription details and verification data. Must be valid and usable by the specified user.</param>\n    /// <returns>A billing command result indicating success or failure with appropriate error details.</returns>\n    Task<BillingCommandResult<None>> Run(User user, UserLicense license);\n}\n\npublic class CreatePremiumSelfHostedSubscriptionCommand(\n    ILicensingService licensingService,\n    IUserService userService,\n    IPushNotificationService pushNotificationService,\n    ILogger<CreatePremiumSelfHostedSubscriptionCommand> logger)\n    : BaseBillingCommand<CreatePremiumSelfHostedSubscriptionCommand>(logger), ICreatePremiumSelfHostedSubscriptionCommand\n{\n    public Task<BillingCommandResult<None>> Run(\n        User user,\n        UserLicense license) => HandleAsync<None>(async () =>\n    {\n        if (user.Premium)\n        {\n            return new BadRequest(\"Already a premium user.\");\n        }\n\n        if (!licensingService.VerifyLicense(license))\n        {\n            return new BadRequest(\"Invalid license.\");\n        }\n\n        var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);\n        if (!license.CanUse(user, claimsPrincipal, out var exceptionMessage))\n        {\n            return new BadRequest(exceptionMessage);\n        }\n\n        await licensingService.WriteUserLicenseAsync(user, license);\n\n        user.Premium = true;\n        user.RevisionDate = DateTime.UtcNow;\n        user.MaxStorageGb = Core.Constants.SelfHostedMaxStorageGb;\n        user.LicenseKey = license.LicenseKey;\n        user.PremiumExpirationDate = license.Expires;\n\n        await userService.SaveUserAsync(user);\n        await pushNotificationService.PushSyncVaultAsync(user.Id);\n\n        return new None();\n    });\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs",
    "content": "﻿using Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Premium.Commands;\n\npublic interface IPreviewPremiumTaxCommand\n{\n    Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(\n        User user,\n        PremiumPurchasePreview preview,\n        BillingAddress billingAddress);\n}\n\npublic class PreviewPremiumTaxCommand(\n    ILogger<PreviewPremiumTaxCommand> logger,\n    IPricingClient pricingClient,\n    IStripeAdapter stripeAdapter,\n    ISubscriptionDiscountService subscriptionDiscountService) : BaseBillingCommand<PreviewPremiumTaxCommand>(logger), IPreviewPremiumTaxCommand\n{\n    public Task<BillingCommandResult<(decimal Tax, decimal Total)>> Run(\n        User user,\n        PremiumPurchasePreview preview,\n        BillingAddress billingAddress)\n        => HandleAsync<(decimal, decimal)>(async () =>\n        {\n            var premiumPlan = await pricingClient.GetAvailablePremiumPlan();\n\n            var options = new InvoiceCreatePreviewOptions\n            {\n                AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true },\n                CustomerDetails = new InvoiceCustomerDetailsOptions\n                {\n                    Address = new AddressOptions\n                    {\n                        Country = billingAddress.Country,\n                        PostalCode = billingAddress.PostalCode\n                    }\n                },\n                Currency = \"usd\",\n                SubscriptionDetails = new InvoiceSubscriptionDetailsOptions\n                {\n                    Items =\n                    [\n                        new InvoiceSubscriptionDetailsItemOptions { Price = premiumPlan.Seat.StripePriceId, Quantity = 1 }\n                    ]\n                }\n            };\n\n            if (preview.AdditionalStorageGb > 0)\n            {\n                options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions\n                {\n                    Price = premiumPlan.Storage.StripePriceId,\n                    Quantity = preview.AdditionalStorageGb\n                });\n            }\n\n            // Validate all coupons at once. If all are eligible, apply them; otherwise skip gracefully.\n            if (preview.Coupons is { Length: > 0 })\n            {\n                var trimmedCoupons = preview.Coupons\n                    .Where(c => !string.IsNullOrWhiteSpace(c))\n                    .Select(c => c.Trim())\n                    .ToArray();\n\n                if (trimmedCoupons.Length > 0)\n                {\n                    var allValid = await subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n                        user, trimmedCoupons, DiscountTierType.Premium);\n\n                    if (allValid)\n                    {\n                        options.Discounts = trimmedCoupons\n                            .Select(c => new InvoiceDiscountOptions { Coupon = c })\n                            .ToList();\n                    }\n                }\n            }\n\n            var invoice = await stripeAdapter.CreateInvoicePreviewAsync(options);\n            return GetAmounts(invoice);\n        });\n\n    private static (decimal, decimal) GetAmounts(Invoice invoice) => (\n        Convert.ToDecimal(invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount)) / 100,\n        Convert.ToDecimal(invoice.Total) / 100);\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Commands/PreviewPremiumUpgradeProrationCommand.cs",
    "content": "﻿using Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Premium.Commands;\n\n/// <summary>\n/// Previews the proration details for upgrading a Premium user subscription to an Organization\n/// plan by using the Stripe API to create an invoice preview, prorated, for the upgrade.\n/// </summary>\npublic interface IPreviewPremiumUpgradeProrationCommand\n{\n    /// <summary>\n    /// Calculates the tax, total cost, and proration credit for upgrading a Premium subscription to an Organization plan.\n    /// </summary>\n    /// <param name=\"user\">The user with an active Premium subscription.</param>\n    /// <param name=\"targetPlanType\">The target organization plan type.</param>\n    /// <param name=\"billingAddress\">The billing address for tax calculation.</param>\n    /// <returns>The proration details for the upgrade including costs, credits, tax, and time remaining.</returns>\n    Task<BillingCommandResult<PremiumUpgradeProration>> Run(\n        User user,\n        PlanType targetPlanType,\n        BillingAddress billingAddress);\n}\n\npublic class PreviewPremiumUpgradeProrationCommand(\n    ILogger<PreviewPremiumUpgradeProrationCommand> logger,\n    IPricingClient pricingClient,\n    IStripeAdapter stripeAdapter)\n    : BaseBillingCommand<PreviewPremiumUpgradeProrationCommand>(logger),\n      IPreviewPremiumUpgradeProrationCommand\n{\n    public Task<BillingCommandResult<PremiumUpgradeProration>> Run(\n        User user,\n        PlanType targetPlanType,\n        BillingAddress billingAddress) => HandleAsync<PremiumUpgradeProration>(async () =>\n    {\n        if (user is not { Premium: true, GatewaySubscriptionId: not null and not \"\" })\n        {\n            return new BadRequest(\"User does not have an active Premium subscription.\");\n        }\n\n        var currentSubscription = await stripeAdapter.GetSubscriptionAsync(\n            user.GatewaySubscriptionId,\n            new SubscriptionGetOptions { Expand = [\"customer\"] });\n        var premiumPlans = await pricingClient.ListPremiumPlans();\n        var passwordManagerItem = currentSubscription.Items.Data.FirstOrDefault(i =>\n            premiumPlans.Any(p => p.Seat.StripePriceId == i.Price.Id));\n\n        if (passwordManagerItem == null)\n        {\n            return new BadRequest(\"Premium subscription password manager item not found.\");\n        }\n\n        var usersPremiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id);\n        var targetPlan = await pricingClient.GetPlanOrThrow(targetPlanType);\n        var subscriptionItems = new List<InvoiceSubscriptionDetailsItemOptions>();\n        var storageItem = currentSubscription.Items.Data.FirstOrDefault(i =>\n            i.Price.Id == usersPremiumPlan.Storage.StripePriceId);\n\n        // Delete the storage item if it exists for this user's plan\n        if (storageItem != null)\n        {\n            subscriptionItems.Add(new InvoiceSubscriptionDetailsItemOptions\n            {\n                Id = storageItem.Id,\n                Deleted = true\n            });\n        }\n\n        // Hardcode seats to 1 for upgrade flow\n        if (targetPlan.HasNonSeatBasedPasswordManagerPlan())\n        {\n            subscriptionItems.Add(new InvoiceSubscriptionDetailsItemOptions\n            {\n                Id = passwordManagerItem.Id,\n                Price = targetPlan.PasswordManager.StripePlanId,\n                Quantity = 1\n            });\n        }\n        else\n        {\n            subscriptionItems.Add(new InvoiceSubscriptionDetailsItemOptions\n            {\n                Id = passwordManagerItem.Id,\n                Price = targetPlan.PasswordManager.StripeSeatPlanId,\n                Quantity = 1\n            });\n        }\n\n        var options = new InvoiceCreatePreviewOptions\n        {\n            AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true },\n            Customer = user.GatewayCustomerId,\n            Subscription = user.GatewaySubscriptionId,\n            CustomerDetails = new InvoiceCustomerDetailsOptions\n            {\n                Address = new AddressOptions\n                {\n                    Country = billingAddress.Country,\n                    PostalCode = billingAddress.PostalCode\n                }\n            },\n            SubscriptionDetails = new InvoiceSubscriptionDetailsOptions\n            {\n                Items = subscriptionItems,\n                ProrationBehavior = StripeConstants.ProrationBehavior.AlwaysInvoice\n            }\n        };\n\n        var invoicePreview = await stripeAdapter.CreateInvoicePreviewAsync(options);\n        var proration = GetProration(invoicePreview, passwordManagerItem);\n\n        return proration;\n    });\n\n    private static PremiumUpgradeProration GetProration(Invoice invoicePreview, SubscriptionItem passwordManagerItem) => new()\n    {\n        NewPlanProratedAmount = GetNewPlanProratedAmountFromInvoice(invoicePreview),\n        Credit = GetProrationCreditFromInvoice(invoicePreview),\n        Tax = Convert.ToDecimal(invoicePreview.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount)) / 100,\n        Total = Convert.ToDecimal(invoicePreview.Total) / 100,\n        // Use invoice periodEnd here instead of UtcNow so that testing with Stripe time clocks works correctly. And if there is no test clock,\n        // (like in production), the previewInvoice's periodEnd is the same as UtcNow anyway because of the proration behavior (always_invoice)\n        NewPlanProratedMonths = CalculateNewPlanProratedMonths(invoicePreview.PeriodEnd, passwordManagerItem.CurrentPeriodEnd)\n    };\n\n    private static decimal GetProrationCreditFromInvoice(Invoice invoicePreview)\n    {\n        // Extract proration credit from negative line items (credits are negative in Stripe)\n        var prorationCredit = invoicePreview.Lines?.Data?\n            .Where(line => line.Amount < 0)\n            .Sum(line => Math.Abs(line.Amount)) ?? 0; // Return the credit as positive number\n\n        return Convert.ToDecimal(prorationCredit) / 100;\n    }\n\n    private static decimal GetNewPlanProratedAmountFromInvoice(Invoice invoicePreview)\n    {\n        // The target plan's prorated upgrade amount should be the only positive-valued line item\n        var proratedTotal = invoicePreview.Lines?.Data?\n            .Where(line => line.Amount > 0)\n            .Sum(line => line.Amount) ?? 0;\n\n        return Convert.ToDecimal(proratedTotal) / 100;\n    }\n\n    private static int CalculateNewPlanProratedMonths(DateTime invoicePeriodEnd, DateTime currentPeriodEnd)\n    {\n        var daysInProratedPeriod = (currentPeriodEnd - invoicePeriodEnd).TotalDays;\n\n        // Round to nearest month (30-day periods)\n        // 1-14 days = 1 month, 15-44 days = 1 month, 45-74 days = 2 months, etc.\n        // Minimum is always 1 month (never returns 0)\n        // Use MidpointRounding.AwayFromZero to round 0.5 up to 1\n        var months = (int)Math.Round(daysInProratedPeriod / 30, MidpointRounding.AwayFromZero);\n        return Math.Max(1, months);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Commands/UpdatePremiumStorageCommand.cs",
    "content": "﻿using Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Premium.Commands;\n\nusing static StripeConstants;\n\n/// <summary>\n/// Updates the storage allocation for a premium user's subscription.\n/// Handles both increases and decreases in storage in an idempotent manner.\n/// </summary>\npublic interface IUpdatePremiumStorageCommand\n{\n    /// <summary>\n    /// Updates the user's storage by the specified additional amount.\n    /// </summary>\n    /// <param name=\"user\">The premium user whose storage should be updated.</param>\n    /// <param name=\"additionalStorageGb\">The additional storage amount in GB beyond base storage.</param>\n    /// <returns>A billing command result indicating success or failure.</returns>\n    Task<BillingCommandResult<None>> Run(User user, short additionalStorageGb);\n}\n\npublic class UpdatePremiumStorageCommand(\n    IBraintreeService braintreeService,\n    IStripeAdapter stripeAdapter,\n    IUserService userService,\n    IPricingClient pricingClient,\n    ILogger<UpdatePremiumStorageCommand> logger)\n    : BaseBillingCommand<UpdatePremiumStorageCommand>(logger), IUpdatePremiumStorageCommand\n{\n    public Task<BillingCommandResult<None>> Run(User user, short additionalStorageGb) => HandleAsync<None>(async () =>\n    {\n        if (user is not { Premium: true, GatewaySubscriptionId: not null and not \"\" })\n        {\n            return new BadRequest(\"User does not have a premium subscription.\");\n        }\n\n        if (!user.MaxStorageGb.HasValue)\n        {\n            return new BadRequest(\"User has no access to storage.\");\n        }\n\n        // Fetch all premium plans and the user's subscription to find which plan they're on\n        var premiumPlans = await pricingClient.ListPremiumPlans();\n        var subscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, new SubscriptionGetOptions\n        {\n            Expand = [\"customer\"]\n        });\n\n        // Find the password manager subscription item (seat, not storage) and match it to a plan\n        var passwordManagerItem = subscription.Items.Data.FirstOrDefault(i =>\n            premiumPlans.Any(p => p.Seat.StripePriceId == i.Price.Id));\n\n        if (passwordManagerItem == null)\n        {\n            return new Conflict(\"Premium subscription does not have a Password Manager line item.\");\n        }\n\n        var premiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id);\n\n        var baseStorageGb = (short)premiumPlan.Storage.Provided;\n\n        if (additionalStorageGb < 0)\n        {\n            return new BadRequest(\"Additional storage cannot be negative.\");\n        }\n\n        var maxStorageGb = (short)(baseStorageGb + additionalStorageGb);\n\n        if (maxStorageGb > 100)\n        {\n            return new BadRequest(\"Maximum storage is 100 GB.\");\n        }\n\n        // Idempotency check: if user already has the requested storage, return success\n        if (user.MaxStorageGb == maxStorageGb)\n        {\n            return new None();\n        }\n\n        var remainingStorage = user.StorageBytesRemaining(maxStorageGb);\n        if (remainingStorage < 0)\n        {\n            return new BadRequest(\n                $\"You are currently using {CoreHelpers.ReadableBytesSize(user.Storage.GetValueOrDefault(0))} of storage. \" +\n                \"Delete some stored data first.\");\n        }\n\n        // Find the storage line item in the subscription\n        var storageItem = subscription.Items.Data.FirstOrDefault(i => i.Price.Id == premiumPlan.Storage.StripePriceId);\n\n        var subscriptionItemOptions = new List<SubscriptionItemOptions>();\n\n        if (additionalStorageGb > 0)\n        {\n            if (storageItem != null)\n            {\n                // Update existing storage item\n                subscriptionItemOptions.Add(new SubscriptionItemOptions\n                {\n                    Id = storageItem.Id,\n                    Price = premiumPlan.Storage.StripePriceId,\n                    Quantity = additionalStorageGb\n                });\n            }\n            else\n            {\n                // Add new storage item\n                subscriptionItemOptions.Add(new SubscriptionItemOptions\n                {\n                    Price = premiumPlan.Storage.StripePriceId,\n                    Quantity = additionalStorageGb\n                });\n            }\n        }\n        else if (storageItem != null)\n        {\n            // Remove storage item if setting to 0\n            subscriptionItemOptions.Add(new SubscriptionItemOptions\n            {\n                Id = storageItem.Id,\n                Deleted = true\n            });\n        }\n\n        var usingPayPal = subscription.Customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId);\n\n        if (usingPayPal)\n        {\n            var options = new SubscriptionUpdateOptions\n            {\n                Items = subscriptionItemOptions,\n                ProrationBehavior = ProrationBehavior.CreateProrations\n            };\n\n            await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, options);\n\n            var draftInvoice = await stripeAdapter.CreateInvoiceAsync(new InvoiceCreateOptions\n            {\n                Customer = subscription.CustomerId,\n                Subscription = subscription.Id,\n                AutoAdvance = false,\n                CollectionMethod = CollectionMethod.ChargeAutomatically\n            });\n\n            var finalizedInvoice = await stripeAdapter.FinalizeInvoiceAsync(draftInvoice.Id,\n                new InvoiceFinalizeOptions { AutoAdvance = false, Expand = [\"customer\"] });\n\n            await braintreeService.PayInvoice(new UserId(user.Id), finalizedInvoice);\n        }\n        else\n        {\n            var options = new SubscriptionUpdateOptions\n            {\n                Items = subscriptionItemOptions,\n                ProrationBehavior = ProrationBehavior.AlwaysInvoice\n            };\n\n            await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, options);\n        }\n\n        // Update the user's max storage\n        user.MaxStorageGb = maxStorageGb;\n        await userService.SaveUserAsync(user);\n\n        return new None();\n    });\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Premium.Commands;\n/// <summary>\n/// Upgrades a user's Premium subscription to an Organization plan by creating a new Organization\n/// and transferring the subscription from the User to the Organization.\n/// </summary>\npublic interface IUpgradePremiumToOrganizationCommand\n{\n    /// <summary>\n    /// Upgrades a Premium subscription to an Organization subscription.\n    /// </summary>\n    /// <param name=\"user\">The user with an active Premium subscription to upgrade.</param>\n    /// <param name=\"organizationName\">The name for the new organization.</param>\n    /// <param name=\"key\">The encrypted organization key for the owner.</param>\n    /// <param name=\"publicKey\">The organization's public key.</param>\n    /// <param name=\"encryptedPrivateKey\">The organization's encrypted private key.</param>\n    /// <param name=\"collectionName\">Optional name for the default collection.</param>\n    /// <param name=\"targetPlanType\">The target organization plan type to upgrade to.</param>\n    /// <param name=\"billingAddress\">The billing address for tax calculation.</param>\n    /// <returns>A billing command result containing the new organization ID on success, or error details on failure.</returns>\n    Task<BillingCommandResult<Guid>> Run(\n        User user,\n        string organizationName,\n        string key,\n        string publicKey,\n        string encryptedPrivateKey,\n        string? collectionName,\n        PlanType targetPlanType,\n        BillingAddress billingAddress);\n}\n\npublic class UpgradePremiumToOrganizationCommand(\n    ILogger<UpgradePremiumToOrganizationCommand> logger,\n    IPricingClient pricingClient,\n    IStripeAdapter stripeAdapter,\n    IUserService userService,\n    IOrganizationRepository organizationRepository,\n    IOrganizationUserRepository organizationUserRepository,\n    IOrganizationApiKeyRepository organizationApiKeyRepository,\n    ICollectionRepository collectionRepository,\n    IApplicationCacheService applicationCacheService,\n    IPushNotificationService pushNotificationService)\n    : BaseBillingCommand<UpgradePremiumToOrganizationCommand>(logger), IUpgradePremiumToOrganizationCommand\n{\n    private readonly ILogger<UpgradePremiumToOrganizationCommand> _logger = logger;\n\n    public Task<BillingCommandResult<Guid>> Run(\n        User user,\n        string organizationName,\n        string key,\n        string publicKey,\n        string encryptedPrivateKey,\n        string? collectionName,\n        PlanType targetPlanType,\n        BillingAddress billingAddress) => HandleAsync<Guid>(async () =>\n    {\n        // Validate that the user has an active Premium subscription\n        if (user is not { Premium: true, GatewaySubscriptionId: not null and not \"\" })\n        {\n            return new BadRequest(\"User does not have an active Premium subscription.\");\n        }\n\n        // Fetch the current Premium subscription from Stripe\n        var currentSubscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId);\n\n        // Fetch all premium plans to find which specific plan the user is on\n        var premiumPlans = await pricingClient.ListPremiumPlans();\n\n        // Find the password manager subscription item (seat, not storage) and match it to a plan\n        var passwordManagerItem = currentSubscription.Items.Data.FirstOrDefault(i =>\n            premiumPlans.Any(p => p.Seat.StripePriceId == i.Price.Id));\n\n        if (passwordManagerItem == null)\n        {\n            return new BadRequest(\"Premium subscription password manager item not found.\");\n        }\n\n        var usersPremiumPlan = premiumPlans.First(p => p.Seat.StripePriceId == passwordManagerItem.Price.Id);\n\n        // Get the target organization plan\n        var targetPlan = await pricingClient.GetPlanOrThrow(targetPlanType);\n\n        var isNonSeatBasedPmPlan = targetPlan.HasNonSeatBasedPasswordManagerPlan();\n\n        // if the target plan is non-seat-based, set seats to the base seats of the target plan, otherwise set to 1\n        var initialSeats = isNonSeatBasedPmPlan ? targetPlan.PasswordManager.BaseSeats : 1;\n\n        // Build the list of subscription item updates\n        var subscriptionItemOptions = new List<SubscriptionItemOptions>();\n\n        // Delete the storage item if it exists for this user's plan\n        var storageItem = currentSubscription.Items.Data.FirstOrDefault(i =>\n            i.Price.Id == usersPremiumPlan.Storage.StripePriceId);\n\n        if (storageItem != null)\n        {\n            subscriptionItemOptions.Add(new SubscriptionItemOptions\n            {\n                Id = storageItem.Id,\n                Deleted = true\n            });\n        }\n\n        // Add new organization subscription items\n        if (isNonSeatBasedPmPlan)\n        {\n            subscriptionItemOptions.Add(new SubscriptionItemOptions\n            {\n                Id = passwordManagerItem.Id,\n                Price = targetPlan.PasswordManager.StripePlanId,\n                Quantity = 1\n            });\n        }\n        else\n        {\n            subscriptionItemOptions.Add(new SubscriptionItemOptions\n            {\n                Id = passwordManagerItem.Id,\n                Price = targetPlan.PasswordManager.StripeSeatPlanId,\n                Quantity = initialSeats\n            });\n        }\n\n        // Generate organization ID early to include in metadata\n        var organizationId = CoreHelpers.GenerateComb();\n\n        // Build the subscription update options\n        var subscriptionUpdateOptions = new SubscriptionUpdateOptions\n        {\n            Items = subscriptionItemOptions,\n            ProrationBehavior = StripeConstants.ProrationBehavior.AlwaysInvoice,\n            BillingCycleAnchor = SubscriptionBillingCycleAnchor.Unchanged,\n            AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },\n            Metadata = new Dictionary<string, string>\n            {\n                [StripeConstants.MetadataKeys.OrganizationId] = organizationId.ToString(),\n                [StripeConstants.MetadataKeys.UserId] = string.Empty // Remove userId to unlink subscription from User\n            }\n        };\n\n        // Create the Organization entity\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Name = organizationName,\n            BillingEmail = user.Email,\n            PlanType = targetPlan.Type,\n            Seats = initialSeats,\n            MaxCollections = targetPlan.PasswordManager.MaxCollections,\n            MaxStorageGb = targetPlan.PasswordManager.BaseStorageGb,\n            UsePolicies = targetPlan.HasPolicies,\n            UseMyItems = targetPlan.HasMyItems,\n            UseSso = targetPlan.HasSso,\n            UseGroups = targetPlan.HasGroups,\n            UseEvents = targetPlan.HasEvents,\n            UseDirectory = targetPlan.HasDirectory,\n            UseTotp = targetPlan.HasTotp,\n            Use2fa = targetPlan.Has2fa,\n            UseApi = targetPlan.HasApi,\n            UseResetPassword = targetPlan.HasResetPassword,\n            SelfHost = targetPlan.HasSelfHost,\n            UsersGetPremium = targetPlan.UsersGetPremium,\n            UseCustomPermissions = targetPlan.HasCustomPermissions,\n            UseScim = targetPlan.HasScim,\n            Plan = targetPlan.Name,\n            Gateway = GatewayType.Stripe,\n            Enabled = true,\n            LicenseKey = CoreHelpers.SecureRandomString(20),\n            PublicKey = publicKey,\n            PrivateKey = encryptedPrivateKey,\n            CreationDate = DateTime.UtcNow,\n            RevisionDate = DateTime.UtcNow,\n            Status = OrganizationStatusType.Created,\n            UsePasswordManager = true,\n            UseSecretsManager = false,\n            UseOrganizationDomains = targetPlan.HasOrganizationDomains,\n            GatewayCustomerId = user.GatewayCustomerId,\n            GatewaySubscriptionId = currentSubscription.Id\n        };\n\n        // Update customer billing address for tax calculation\n        await stripeAdapter.UpdateCustomerAsync(user.GatewayCustomerId, new CustomerUpdateOptions\n        {\n            Address = new AddressOptions\n            {\n                Country = billingAddress.Country,\n                PostalCode = billingAddress.PostalCode\n            },\n            TaxExempt = TaxHelpers.DetermineTaxExemptStatus(billingAddress.Country)\n        });\n\n        // Add tax ID to customer for accurate tax calculation if provided\n        if (billingAddress.TaxId != null)\n        {\n            await AddTaxIdToCustomerAsync(user, billingAddress.TaxId);\n        }\n\n        // Update the subscription in Stripe\n        await stripeAdapter.UpdateSubscriptionAsync(currentSubscription.Id, subscriptionUpdateOptions);\n\n        // Save the organization\n        await organizationRepository.CreateAsync(organization);\n\n        // Create organization API key\n        await organizationApiKeyRepository.CreateAsync(new OrganizationApiKey\n        {\n            OrganizationId = organization.Id,\n            ApiKey = CoreHelpers.SecureRandomString(30),\n            Type = OrganizationApiKeyType.Default,\n            RevisionDate = DateTime.UtcNow,\n        });\n\n        // Update cache\n        await applicationCacheService.UpsertOrganizationAbilityAsync(organization);\n\n        // Create OrganizationUser for the upgrading user as owner\n        var organizationUser = new OrganizationUser\n        {\n            OrganizationId = organization.Id,\n            UserId = user.Id,\n            Key = key,\n            AccessSecretsManager = false,\n            Type = OrganizationUserType.Owner,\n            Status = OrganizationUserStatusType.Confirmed,\n            CreationDate = organization.CreationDate,\n            RevisionDate = organization.CreationDate\n        };\n        organizationUser.SetNewId();\n        await organizationUserRepository.CreateAsync(organizationUser);\n\n        // Create default collection if collection name is provided\n        if (!string.IsNullOrWhiteSpace(collectionName))\n        {\n            try\n            {\n                // Give the owner Can Manage access over the default collection\n                List<CollectionAccessSelection> defaultOwnerAccess =\n                    [new CollectionAccessSelection { Id = organizationUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];\n\n                var defaultCollection = new Collection\n                {\n                    Name = collectionName,\n                    OrganizationId = organization.Id,\n                    CreationDate = organization.CreationDate,\n                    RevisionDate = organization.CreationDate\n                };\n                await collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);\n            }\n            catch (Exception ex)\n            {\n                _logger.LogWarning(ex,\n                    \"{Command}: Failed to create default collection for organization {OrganizationId}. Organization upgrade will continue.\",\n                    CommandName, organization.Id);\n                // Continue - organization is fully functional without default collection\n            }\n        }\n\n        // Remove subscription from user\n        user.Premium = false;\n        user.PremiumExpirationDate = null;\n        user.GatewaySubscriptionId = null;\n        user.GatewayCustomerId = null;\n        user.RevisionDate = DateTime.UtcNow;\n        await userService.SaveUserAsync(user);\n\n        await pushNotificationService.PushAsync(new PushNotification<PremiumStatusPushNotification>\n        {\n            Type = PushType.PremiumStatusChanged,\n            Target = NotificationTarget.User,\n            TargetId = user.Id,\n            Payload = new PremiumStatusPushNotification\n            {\n                UserId = user.Id,\n                Premium = user.Premium,\n            },\n            ExcludeCurrentContext = false,\n        });\n\n        return organization.Id;\n    });\n\n    /// <summary>\n    /// Adds a tax ID to the Stripe customer for accurate tax calculation.\n    /// If the tax ID is a Spanish NIF, also adds the corresponding EU VAT ID.\n    /// </summary>\n    /// <param name=\"user\"> The user whose Stripe customer will be updated with the tax ID.</param>\n    /// <param name=\"taxId\"> The tax ID to add, including the type and value.</param>\n    private async Task AddTaxIdToCustomerAsync(User user, TaxID taxId)\n    {\n        await stripeAdapter.CreateTaxIdAsync(user.GatewayCustomerId,\n            new TaxIdCreateOptions { Type = taxId.Code, Value = taxId.Value });\n\n        if (taxId.Code == StripeConstants.TaxIdType.SpanishNIF)\n        {\n            await stripeAdapter.CreateTaxIdAsync(user.GatewayCustomerId,\n                new TaxIdCreateOptions\n                {\n                    Type = StripeConstants.TaxIdType.EUVAT,\n                    Value = $\"ES{taxId.Value}\"\n                });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Models/PremiumPurchasePreview.cs",
    "content": "﻿namespace Bit.Core.Billing.Premium.Models;\n\npublic record PremiumPurchasePreview\n{\n    public short? AdditionalStorageGb { get; init; }\n    public string[]? Coupons { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Models/PremiumSubscriptionPurchase.cs",
    "content": "﻿using Bit.Core.Billing.Payment.Models;\n\nnamespace Bit.Core.Billing.Premium.Models;\n\npublic record PremiumSubscriptionPurchase\n{\n    public required PaymentMethod PaymentMethod { get; init; }\n    public required BillingAddress BillingAddress { get; init; }\n    public short? AdditionalStorageGb { get; init; }\n    public string[]? Coupons { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Models/PremiumUpgradeProration.cs",
    "content": "﻿namespace Bit.Core.Billing.Premium.Models;\n\n/// <summary>\n/// Represents the proration details for upgrading a Premium user subscription to an Organization plan.\n/// </summary>\npublic class PremiumUpgradeProration\n{\n    /// <summary>\n    /// The prorated cost for the new organization plan, calculated from now until the end of the current billing period.\n    /// This represents what the user will pay for the upgraded plan for the remainder of the period.\n    /// </summary>\n    public decimal NewPlanProratedAmount { get; set; }\n\n    /// <summary>\n    /// The credit amount for the unused portion of the current Premium subscription.\n    /// This credit is applied against the cost of the new organization plan.\n    /// </summary>\n    public decimal Credit { get; set; }\n\n    /// <summary>\n    /// The tax amount calculated for the upgrade transaction.\n    /// </summary>\n    public decimal Tax { get; set; }\n\n    /// <summary>\n    /// The total amount due for the upgrade after applying the credit and adding tax.\n    /// </summary>\n    public decimal Total { get; set; }\n\n    /// <summary>\n    /// The number of months the user will be charged for the new organization plan in the prorated billing period.\n    /// Calculated by rounding the days remaining in the current billing cycle to the nearest month.\n    /// Minimum value is 1 month (never returns 0).\n    /// </summary>\n    public int NewPlanProratedMonths { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Models/UserPremiumAccess.cs",
    "content": "﻿namespace Bit.Core.Billing.Premium.Models;\n\n/// <summary>\n/// Represents user premium access status from personal subscriptions and organization memberships.\n/// </summary>\npublic class UserPremiumAccess\n{\n    /// <summary>\n    /// The unique identifier for the user.\n    /// </summary>\n    public Guid Id { get; set; }\n\n    /// <summary>\n    /// Indicates whether the user has a personal premium subscription.\n    /// This does NOT include premium access from organizations.\n    /// </summary>\n    public bool PersonalPremium { get; set; }\n\n    /// <summary>\n    /// Indicates whether the user has premium access through any organization membership.\n    /// This is true if the user is a member of at least one enabled organization that grants premium access to users.\n    /// </summary>\n    public bool OrganizationPremium { get; set; }\n\n    /// <summary>\n    /// Indicates whether the user has premium access from any source (personal subscription or organization).\n    /// </summary>\n    public bool HasPremiumAccess => PersonalPremium || OrganizationPremium;\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Queries/HasPremiumAccessQuery.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Billing.Premium.Queries;\n\npublic class HasPremiumAccessQuery : IHasPremiumAccessQuery\n{\n    private readonly IUserRepository _userRepository;\n\n    public HasPremiumAccessQuery(IUserRepository userRepository)\n    {\n        _userRepository = userRepository;\n    }\n\n    public async Task<bool> HasPremiumAccessAsync(Guid userId)\n    {\n        var user = await _userRepository.GetPremiumAccessAsync(userId);\n        if (user == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return user.HasPremiumAccess;\n    }\n\n    public async Task<Dictionary<Guid, bool>> HasPremiumAccessAsync(IEnumerable<Guid> userIds)\n    {\n        var distinctUserIds = userIds.Distinct().ToList();\n        var usersWithPremium = await _userRepository.GetPremiumAccessByIdsAsync(distinctUserIds);\n\n        if (usersWithPremium.Count() != distinctUserIds.Count)\n        {\n            throw new NotFoundException();\n        }\n\n        return usersWithPremium.ToDictionary(u => u.Id, u => u.HasPremiumAccess);\n    }\n\n    public async Task<bool> HasPremiumFromOrganizationAsync(Guid userId)\n    {\n        var user = await _userRepository.GetPremiumAccessAsync(userId);\n        if (user == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return user.OrganizationPremium;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Premium/Queries/IHasPremiumAccessQuery.cs",
    "content": "﻿namespace Bit.Core.Billing.Premium.Queries;\n\n/// <summary>\n/// Centralized query for checking if users have premium access through personal subscriptions or organizations.\n/// Note: Different from User.Premium which only checks personal subscriptions.\n/// </summary>\npublic interface IHasPremiumAccessQuery\n{\n    /// <summary>\n    /// Checks if a user has premium access (personal or organization).\n    /// </summary>\n    /// <param name=\"userId\">The user ID to check</param>\n    /// <returns>True if user can access premium features</returns>\n    Task<bool> HasPremiumAccessAsync(Guid userId);\n\n    /// <summary>\n    /// Checks premium access for multiple users.\n    /// </summary>\n    /// <param name=\"userIds\">The user IDs to check</param>\n    /// <returns>Dictionary mapping user IDs to their premium access status</returns>\n    Task<Dictionary<Guid, bool>> HasPremiumAccessAsync(IEnumerable<Guid> userIds);\n\n    /// <summary>\n    /// Checks if a user belongs to any organization that grants premium (enabled org with UsersGetPremium).\n    /// Returns true regardless of personal subscription. Useful for UI decisions like showing subscription options.\n    /// </summary>\n    /// <param name=\"userId\">The user ID to check</param>\n    /// <returns>True if user is in any organization that grants premium</returns>\n    Task<bool> HasPremiumFromOrganizationAsync(Guid userId);\n}\n"
  },
  {
    "path": "src/Core/Billing/Pricing/IPricingClient.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Billing.Pricing;\n\nusing OrganizationPlan = Plan;\nusing PremiumPlan = Premium.Plan;\n\npublic interface IPricingClient\n{\n    // TODO: Rename with Organization focus.\n    /// <summary>\n    /// Retrieve a Bitwarden plan by its <paramref name=\"planType\"/> from the Bitwarden Pricing Service.\n    /// </summary>\n    /// <param name=\"planType\">The type of plan to retrieve.</param>\n    /// <returns>A Bitwarden <see cref=\"Plan\"/> record or null in the case the plan could not be found or the method was executed from a self-hosted instance.</returns>\n    /// <exception cref=\"BillingException\">Thrown when the request to the Pricing Service fails unexpectedly.</exception>\n    Task<OrganizationPlan?> GetPlan(PlanType planType);\n\n    // TODO: Rename with Organization focus.\n    /// <summary>\n    /// Retrieve a Bitwarden plan by its <paramref name=\"planType\"/> from the Bitwarden Pricing Service.\n    /// </summary>\n    /// <param name=\"planType\">The type of plan to retrieve.</param>\n    /// <returns>A Bitwarden <see cref=\"Plan\"/> record.</returns>\n    /// <exception cref=\"NotFoundException\">Thrown when the <see cref=\"Plan\"/> for the provided <paramref name=\"planType\"/> could not be found or the method was executed from a self-hosted instance.</exception>\n    /// <exception cref=\"BillingException\">Thrown when the request to the Pricing Service fails unexpectedly.</exception>\n    Task<OrganizationPlan> GetPlanOrThrow(PlanType planType);\n\n    // TODO: Rename with Organization focus.\n    /// <summary>\n    /// Retrieve all Bitwarden plans from the Pricing Service.\n    /// </summary>\n    /// <returns>A list of Bitwarden <see cref=\"Plan\"/> records or an empty list in the case the method is executed from a self-hosted instance.</returns>\n    /// <exception cref=\"BillingException\">Thrown when the request to the Pricing Service fails unexpectedly.</exception>\n    Task<List<OrganizationPlan>> ListPlans();\n\n    Task<PremiumPlan> GetAvailablePremiumPlan();\n    Task<List<PremiumPlan>> ListPremiumPlans();\n}\n"
  },
  {
    "path": "src/Core/Billing/Pricing/Organizations/Feature.cs",
    "content": "﻿namespace Bit.Core.Billing.Pricing.Organizations;\n\npublic class Feature\n{\n    public required string Name { get; set; }\n    public required string LookupKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Pricing/Organizations/Plan.cs",
    "content": "﻿namespace Bit.Core.Billing.Pricing.Organizations;\n\npublic class Plan\n{\n    public required string LookupKey { get; set; }\n    public required string Name { get; set; }\n    public required string Tier { get; set; }\n    public string? Cadence { get; set; }\n    public int? LegacyYear { get; set; }\n    public bool Available { get; set; }\n    public required Feature[] Features { get; set; }\n    public required Purchasable Seats { get; set; }\n    public Scalable? ManagedSeats { get; set; }\n    public Scalable? Storage { get; set; }\n    public SecretsManagerPurchasables? SecretsManager { get; set; }\n    public int? TrialPeriodDays { get; set; }\n    public required string[] CanUpgradeTo { get; set; }\n    public required Dictionary<string, string> AdditionalData { get; set; }\n}\n\npublic class SecretsManagerPurchasables\n{\n    public required FreeOrScalable Seats { get; set; }\n    public required FreeOrScalable ServiceAccounts { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Pricing/Organizations/PlanAdapter.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Pricing.Organizations;\n\npublic record PlanAdapter : Core.Models.StaticStore.Plan\n{\n    public PlanAdapter(Plan plan)\n    {\n        Type = ToPlanType(plan.LookupKey);\n        ProductTier = ToProductTierType(Type);\n        Name = plan.Name;\n        IsAnnual = plan.Cadence is \"annually\";\n        NameLocalizationKey = plan.AdditionalData[\"nameLocalizationKey\"];\n        DescriptionLocalizationKey = plan.AdditionalData[\"descriptionLocalizationKey\"];\n        TrialPeriodDays = plan.TrialPeriodDays;\n        HasSelfHost = HasFeature(\"selfHost\");\n        HasPolicies = HasFeature(\"policies\");\n        HasGroups = HasFeature(\"groups\");\n        HasDirectory = HasFeature(\"directory\");\n        HasEvents = HasFeature(\"events\");\n        HasTotp = HasFeature(\"totp\");\n        Has2fa = HasFeature(\"2fa\");\n        HasApi = HasFeature(\"api\");\n        HasSso = HasFeature(\"sso\");\n        HasOrganizationDomains = HasFeature(\"organizationDomains\");\n        HasKeyConnector = HasFeature(\"keyConnector\");\n        HasScim = HasFeature(\"scim\");\n        HasResetPassword = HasFeature(\"resetPassword\");\n        UsersGetPremium = HasFeature(\"usersGetPremium\");\n        HasCustomPermissions = HasFeature(\"customPermissions\");\n        HasMyItems = HasFeature(\"myItems\");\n        UpgradeSortOrder = plan.AdditionalData.TryGetValue(\"upgradeSortOrder\", out var upgradeSortOrder)\n            ? int.Parse(upgradeSortOrder)\n            : 0;\n        DisplaySortOrder = plan.AdditionalData.TryGetValue(\"displaySortOrder\", out var displaySortOrder)\n            ? int.Parse(displaySortOrder)\n            : 0;\n        Disabled = !plan.Available;\n        LegacyYear = plan.LegacyYear;\n        PasswordManager = ToPasswordManagerPlanFeatures(plan);\n        SecretsManager = plan.SecretsManager != null ? ToSecretsManagerPlanFeatures(plan) : null;\n\n        return;\n\n        bool HasFeature(string lookupKey) => plan.Features.Any(feature => feature.LookupKey == lookupKey);\n    }\n\n    #region Mappings\n\n    private static PlanType ToPlanType(string lookupKey)\n        => lookupKey switch\n        {\n            \"enterprise-annually\" => PlanType.EnterpriseAnnually,\n            \"enterprise-annually-2019\" => PlanType.EnterpriseAnnually2019,\n            \"enterprise-annually-2020\" => PlanType.EnterpriseAnnually2020,\n            \"enterprise-annually-2023\" => PlanType.EnterpriseAnnually2023,\n            \"enterprise-monthly\" => PlanType.EnterpriseMonthly,\n            \"enterprise-monthly-2019\" => PlanType.EnterpriseMonthly2019,\n            \"enterprise-monthly-2020\" => PlanType.EnterpriseMonthly2020,\n            \"enterprise-monthly-2023\" => PlanType.EnterpriseMonthly2023,\n            \"families\" => PlanType.FamiliesAnnually,\n            \"families-2025\" => PlanType.FamiliesAnnually2025,\n            \"families-2019\" => PlanType.FamiliesAnnually2019,\n            \"free\" => PlanType.Free,\n            \"teams-annually\" => PlanType.TeamsAnnually,\n            \"teams-annually-2019\" => PlanType.TeamsAnnually2019,\n            \"teams-annually-2020\" => PlanType.TeamsAnnually2020,\n            \"teams-annually-2023\" => PlanType.TeamsAnnually2023,\n            \"teams-monthly\" => PlanType.TeamsMonthly,\n            \"teams-monthly-2019\" => PlanType.TeamsMonthly2019,\n            \"teams-monthly-2020\" => PlanType.TeamsMonthly2020,\n            \"teams-monthly-2023\" => PlanType.TeamsMonthly2023,\n            \"teams-starter\" => PlanType.TeamsStarter,\n            \"teams-starter-2023\" => PlanType.TeamsStarter2023,\n            _ => throw new BillingException() // TODO: Flesh out\n        };\n\n    private static ProductTierType ToProductTierType(PlanType planType)\n        => planType switch\n        {\n            PlanType.Free => ProductTierType.Free,\n            PlanType.FamiliesAnnually or PlanType.FamiliesAnnually2025 or PlanType.FamiliesAnnually2019 => ProductTierType.Families,\n            PlanType.TeamsStarter or PlanType.TeamsStarter2023 => ProductTierType.TeamsStarter,\n            _ when planType.ToString().Contains(\"Teams\") => ProductTierType.Teams,\n            _ when planType.ToString().Contains(\"Enterprise\") => ProductTierType.Enterprise,\n            _ => throw new BillingException() // TODO: Flesh out\n        };\n\n    private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(Plan plan)\n    {\n        var stripePlanId = GetStripePlanId(plan.Seats);\n        var stripeSeatPlanId = GetStripeSeatPlanId(plan.Seats);\n        var stripeProviderPortalSeatPlanId = plan.ManagedSeats?.StripePriceId;\n        var basePrice = GetBasePrice(plan.Seats);\n        var seatPrice = GetSeatPrice(plan.Seats);\n        var providerPortalSeatPrice = plan.ManagedSeats?.Price ?? 0;\n        var scales = plan.Seats.Match(\n            _ => false,\n            packaged => packaged.Additional != null,\n            _ => true);\n        var baseSeats = GetBaseSeats(plan.Seats);\n        var maxSeats = GetMaxSeats(plan.Seats);\n        var baseStorageGb = (short)(plan.Storage?.Provided ?? 0);\n        var hasAdditionalStorageOption = plan.Storage != null;\n        var additionalStoragePricePerGb = plan.Storage?.Price ?? 0;\n        var stripeStoragePlanId = plan.Storage?.StripePriceId;\n        short? maxCollections = plan.AdditionalData.TryGetValue(\"passwordManager.maxCollections\", out var value) ? short.Parse(value) : null;\n        var stripePremiumAccessPlanId =\n            plan.AdditionalData.TryGetValue(\"premiumAccessAddOnPriceId\", out var premiumAccessAddOnPriceIdValue)\n                ? premiumAccessAddOnPriceIdValue\n                : null;\n        var premiumAccessOptionPrice =\n            plan.AdditionalData.TryGetValue(\"premiumAccessAddOnPriceAmount\", out var premiumAccessAddOnPriceAmountValue)\n                ? decimal.Parse(premiumAccessAddOnPriceAmountValue)\n                : 0;\n\n        return new PasswordManagerPlanFeatures\n        {\n            StripePlanId = stripePlanId,\n            StripeSeatPlanId = stripeSeatPlanId,\n            StripeProviderPortalSeatPlanId = stripeProviderPortalSeatPlanId,\n            BasePrice = basePrice,\n            SeatPrice = seatPrice,\n            ProviderPortalSeatPrice = providerPortalSeatPrice,\n            AllowSeatAutoscale = scales,\n            HasAdditionalSeatsOption = scales,\n            BaseSeats = baseSeats,\n            MaxSeats = maxSeats,\n            BaseStorageGb = baseStorageGb,\n            HasAdditionalStorageOption = hasAdditionalStorageOption,\n            AdditionalStoragePricePerGb = additionalStoragePricePerGb,\n            StripeStoragePlanId = stripeStoragePlanId,\n            MaxCollections = maxCollections,\n            StripePremiumAccessPlanId = stripePremiumAccessPlanId,\n            PremiumAccessOptionPrice = premiumAccessOptionPrice\n        };\n    }\n\n    private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(Plan plan)\n    {\n        var seats = plan.SecretsManager!.Seats;\n        var serviceAccounts = plan.SecretsManager.ServiceAccounts;\n\n        var maxServiceAccounts = GetMaxServiceAccounts(serviceAccounts);\n        var allowServiceAccountsAutoscale = serviceAccounts.IsScalable;\n        var stripeServiceAccountPlanId = GetStripeServiceAccountPlanId(serviceAccounts);\n        var additionalPricePerServiceAccount = GetAdditionalPricePerServiceAccount(serviceAccounts);\n        var baseServiceAccount = GetBaseServiceAccount(serviceAccounts);\n        var hasAdditionalServiceAccountOption = serviceAccounts.IsScalable;\n        var stripeSeatPlanId = GetStripeSeatPlanId(seats);\n        var hasAdditionalSeatsOption = seats.IsScalable;\n        var seatPrice = GetSeatPrice(seats);\n        var baseSeats = GetBaseSeats(seats);\n        var maxSeats = GetMaxSeats(seats);\n        var allowSeatAutoscale = seats.IsScalable;\n        var maxProjects = plan.AdditionalData.TryGetValue(\"secretsManager.maxProjects\", out var value) ? short.Parse(value) : 0;\n\n        return new SecretsManagerPlanFeatures\n        {\n            MaxServiceAccounts = maxServiceAccounts,\n            AllowServiceAccountsAutoscale = allowServiceAccountsAutoscale,\n            StripeServiceAccountPlanId = stripeServiceAccountPlanId,\n            AdditionalPricePerServiceAccount = additionalPricePerServiceAccount,\n            BaseServiceAccount = baseServiceAccount,\n            HasAdditionalServiceAccountOption = hasAdditionalServiceAccountOption,\n            StripeSeatPlanId = stripeSeatPlanId,\n            HasAdditionalSeatsOption = hasAdditionalSeatsOption,\n            SeatPrice = seatPrice,\n            BaseSeats = baseSeats,\n            MaxSeats = maxSeats,\n            AllowSeatAutoscale = allowSeatAutoscale,\n            MaxProjects = maxProjects\n        };\n    }\n\n    private static decimal? GetAdditionalPricePerServiceAccount(FreeOrScalable freeOrScalable)\n        => freeOrScalable.FromScalable(x => x.Price);\n\n    private static decimal GetBasePrice(Purchasable purchasable)\n        => purchasable.FromPackaged(x => x.Price);\n\n    private static int GetBaseSeats(FreeOrScalable freeOrScalable)\n        => freeOrScalable.Match(\n            free => free.Quantity,\n            scalable => scalable.Provided);\n\n    private static int GetBaseSeats(Purchasable purchasable)\n        => purchasable.Match(\n            free => free.Quantity,\n            packaged => packaged.Quantity,\n            scalable => scalable.Provided);\n\n    private static short GetBaseServiceAccount(FreeOrScalable freeOrScalable)\n        => freeOrScalable.Match(\n            free => (short)free.Quantity,\n            scalable => (short)scalable.Provided);\n\n    private static short? GetMaxSeats(Purchasable purchasable)\n        => purchasable.Match<short?>(\n            free => (short)free.Quantity,\n            packaged => (short)packaged.Quantity,\n            _ => null);\n\n    private static short? GetMaxSeats(FreeOrScalable freeOrScalable)\n        => freeOrScalable.FromFree(x => (short)x.Quantity);\n\n    private static short? GetMaxServiceAccounts(FreeOrScalable freeOrScalable)\n        => freeOrScalable.FromFree(x => (short)x.Quantity);\n\n    private static decimal GetSeatPrice(Purchasable purchasable)\n        => purchasable.Match(\n            _ => 0,\n            packaged => packaged.Additional?.Price ?? 0,\n            scalable => scalable.Price);\n\n    private static decimal GetSeatPrice(FreeOrScalable freeOrScalable)\n        => freeOrScalable.FromScalable(x => x.Price);\n\n    private static string? GetStripePlanId(Purchasable purchasable)\n        => purchasable.FromPackaged(x => x.StripePriceId);\n\n    private static string? GetStripeSeatPlanId(Purchasable purchasable)\n        => purchasable.Match(\n            _ => null,\n            packaged => packaged.Additional?.StripePriceId,\n            scalable => scalable.StripePriceId);\n\n    private static string? GetStripeSeatPlanId(FreeOrScalable freeOrScalable)\n        => freeOrScalable.FromScalable(x => x.StripePriceId);\n\n    private static string? GetStripeServiceAccountPlanId(FreeOrScalable freeOrScalable)\n        => freeOrScalable.FromScalable(x => x.StripePriceId);\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Core/Billing/Pricing/Organizations/Purchasable.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing OneOf;\n\nnamespace Bit.Core.Billing.Pricing.Organizations;\n\n[JsonConverter(typeof(PurchasableJsonConverter))]\npublic class Purchasable(OneOf<Free, Packaged, Scalable> input) : OneOfBase<Free, Packaged, Scalable>(input)\n{\n    public static implicit operator Purchasable(Free free) => new(free);\n    public static implicit operator Purchasable(Packaged packaged) => new(packaged);\n    public static implicit operator Purchasable(Scalable scalable) => new(scalable);\n\n    public T? FromFree<T>(Func<Free, T> select, Func<Purchasable, T>? fallback = null) =>\n        IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default;\n\n    public T? FromPackaged<T>(Func<Packaged, T> select, Func<Purchasable, T>? fallback = null) =>\n        IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default;\n\n    public T? FromScalable<T>(Func<Scalable, T> select, Func<Purchasable, T>? fallback = null) =>\n        IsT2 ? select(AsT2) : fallback != null ? fallback(this) : default;\n\n    public bool IsFree => IsT0;\n    public bool IsPackaged => IsT1;\n    public bool IsScalable => IsT2;\n}\n\ninternal class PurchasableJsonConverter : JsonConverter<Purchasable>\n{\n    private static readonly string _typePropertyName = nameof(Free.Type).ToLower();\n\n    public override Purchasable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        var element = JsonElement.ParseValue(ref reader);\n\n        if (!element.TryGetProperty(options.PropertyNamingPolicy?.ConvertName(_typePropertyName) ?? _typePropertyName, out var typeProperty))\n        {\n            throw new JsonException(\n                $\"Failed to deserialize {nameof(Purchasable)}: missing '{_typePropertyName}' property\");\n        }\n\n        var type = typeProperty.GetString();\n\n        return type switch\n        {\n            \"free\" => element.Deserialize<Free>(options)!,\n            \"packaged\" => element.Deserialize<Packaged>(options)!,\n            \"scalable\" => element.Deserialize<Scalable>(options)!,\n            _ => throw new JsonException($\"Failed to deserialize {nameof(Purchasable)}: invalid '{_typePropertyName}' value - '{type}'\"),\n        };\n    }\n\n    public override void Write(Utf8JsonWriter writer, Purchasable value, JsonSerializerOptions options)\n        => value.Switch(\n            free => JsonSerializer.Serialize(writer, free, options),\n            packaged => JsonSerializer.Serialize(writer, packaged, options),\n            scalable => JsonSerializer.Serialize(writer, scalable, options)\n        );\n}\n\n[JsonConverter(typeof(FreeOrScalableJsonConverter))]\npublic class FreeOrScalable(OneOf<Free, Scalable> input) : OneOfBase<Free, Scalable>(input)\n{\n    public static implicit operator FreeOrScalable(Free free) => new(free);\n    public static implicit operator FreeOrScalable(Scalable scalable) => new(scalable);\n\n    public T? FromFree<T>(Func<Free, T> select, Func<FreeOrScalable, T>? fallback = null) =>\n        IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default;\n\n    public T? FromScalable<T>(Func<Scalable, T> select, Func<FreeOrScalable, T>? fallback = null) =>\n        IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default;\n\n    public bool IsFree => IsT0;\n    public bool IsScalable => IsT1;\n}\n\npublic class FreeOrScalableJsonConverter : JsonConverter<FreeOrScalable>\n{\n    private static readonly string _typePropertyName = nameof(Free.Type).ToLower();\n\n    public override FreeOrScalable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        var element = JsonElement.ParseValue(ref reader);\n\n        if (!element.TryGetProperty(options.PropertyNamingPolicy?.ConvertName(_typePropertyName) ?? _typePropertyName, out var typeProperty))\n        {\n            throw new JsonException(\n                $\"Failed to deserialize {nameof(FreeOrScalable)}: missing '{_typePropertyName}' property\");\n        }\n\n        var type = typeProperty.GetString();\n\n        return type switch\n        {\n            \"free\" => element.Deserialize<Free>(options)!,\n            \"scalable\" => element.Deserialize<Scalable>(options)!,\n            _ => throw new JsonException($\"Failed to deserialize {nameof(FreeOrScalable)}: invalid '{_typePropertyName}' value - '{type}'\"),\n        };\n    }\n\n    public override void Write(Utf8JsonWriter writer, FreeOrScalable value, JsonSerializerOptions options)\n        => value.Switch(\n            free => JsonSerializer.Serialize(writer, free, options),\n            scalable => JsonSerializer.Serialize(writer, scalable, options)\n        );\n}\n\npublic class Free\n{\n    public int Quantity { get; set; }\n    public string Type => \"free\";\n}\n\npublic class Packaged\n{\n    public int Quantity { get; set; }\n    public string StripePriceId { get; set; } = null!;\n    public decimal Price { get; set; }\n    public AdditionalSeats? Additional { get; set; }\n    public string Type => \"packaged\";\n\n    public class AdditionalSeats\n    {\n        public string StripePriceId { get; set; } = null!;\n        public decimal Price { get; set; }\n    }\n}\n\npublic class Scalable\n{\n    public int Provided { get; set; }\n    public string StripePriceId { get; set; } = null!;\n    public decimal Price { get; set; }\n    public string Type => \"scalable\";\n}\n"
  },
  {
    "path": "src/Core/Billing/Pricing/Premium/Plan.cs",
    "content": "﻿namespace Bit.Core.Billing.Pricing.Premium;\n\npublic class Plan\n{\n    public string Name { get; init; } = null!;\n    public int? LegacyYear { get; init; }\n    public bool Available { get; init; }\n    public Purchasable Seat { get; init; } = null!;\n    public Purchasable Storage { get; init; } = null!;\n}\n"
  },
  {
    "path": "src/Core/Billing/Pricing/Premium/Purchasable.cs",
    "content": "﻿namespace Bit.Core.Billing.Pricing.Premium;\n\npublic class Purchasable\n{\n    public string StripePriceId { get; init; } = null!;\n    public decimal Price { get; init; }\n    public int Provided { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Pricing/PricingClient.cs",
    "content": "﻿using System.Net;\nusing System.Net.Http.Json;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing.Organizations;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Billing.Pricing;\n\nusing OrganizationPlan = Bit.Core.Models.StaticStore.Plan;\nusing PremiumPlan = Premium.Plan;\n\npublic class PricingClient(\n    IFeatureService featureService,\n    GlobalSettings globalSettings,\n    HttpClient httpClient,\n    ILogger<PricingClient> logger) : IPricingClient\n{\n    public async Task<OrganizationPlan?> GetPlan(PlanType planType)\n    {\n        if (globalSettings.SelfHosted)\n        {\n            return null;\n        }\n\n        var lookupKey = GetLookupKey(planType);\n\n        if (lookupKey == null)\n        {\n            logger.LogError(\"Could not find Pricing Service lookup key for PlanType {PlanType}\", planType);\n            return null;\n        }\n\n        var response = await httpClient.GetAsync($\"plans/organization/{lookupKey}\");\n\n        if (response.IsSuccessStatusCode)\n        {\n            var plan = await response.Content.ReadFromJsonAsync<Plan>();\n            return plan == null\n                ? throw new BillingException(message: \"Deserialization of Pricing Service response resulted in null\")\n                : new PlanAdapter(PreProcessFamiliesPreMigrationPlan(plan));\n        }\n\n        if (response.StatusCode == HttpStatusCode.NotFound)\n        {\n            logger.LogError(\"Pricing Service plan for PlanType {PlanType} was not found\", planType);\n            return null;\n        }\n\n        throw new BillingException(\n            message: $\"Request to the Pricing Service failed with status code {response.StatusCode}\");\n    }\n\n    public async Task<OrganizationPlan> GetPlanOrThrow(PlanType planType)\n    {\n        var plan = await GetPlan(planType);\n\n        return plan ?? throw new NotFoundException($\"Could not find plan for type {planType}\");\n    }\n\n    public async Task<List<OrganizationPlan>> ListPlans()\n    {\n        if (globalSettings.SelfHosted)\n        {\n            return [];\n        }\n\n        var response = await httpClient.GetAsync(\"plans/organization\");\n\n        if (response.IsSuccessStatusCode)\n        {\n            var plans = await response.Content.ReadFromJsonAsync<List<Plan>>();\n            return plans == null\n                ? throw new BillingException(message: \"Deserialization of Pricing Service response resulted in null\")\n                : plans.Select(OrganizationPlan (plan) => new PlanAdapter(PreProcessFamiliesPreMigrationPlan(plan))).ToList();\n        }\n\n        throw new BillingException(\n            message: $\"Request to the Pricing Service failed with status {response.StatusCode}\");\n    }\n\n    public async Task<PremiumPlan> GetAvailablePremiumPlan()\n    {\n        var premiumPlans = await ListPremiumPlans();\n\n        var availablePlan = premiumPlans.FirstOrDefault(premiumPlan => premiumPlan.Available);\n\n        return availablePlan ?? throw new NotFoundException(\"Could not find available premium plan\");\n    }\n\n    public async Task<List<PremiumPlan>> ListPremiumPlans()\n    {\n        if (globalSettings.SelfHosted)\n        {\n            return [];\n        }\n\n        var response = await httpClient.GetAsync(\"plans/premium\");\n\n        if (response.IsSuccessStatusCode)\n        {\n            var plans = await response.Content.ReadFromJsonAsync<List<PremiumPlan>>();\n            return plans ?? throw new BillingException(message: \"Deserialization of Pricing Service response resulted in null\");\n        }\n\n        throw new BillingException(\n            message: $\"Request to the Pricing Service failed with status {response.StatusCode}\");\n    }\n\n    private string? GetLookupKey(PlanType planType)\n        => planType switch\n        {\n            PlanType.EnterpriseAnnually => \"enterprise-annually\",\n            PlanType.EnterpriseAnnually2019 => \"enterprise-annually-2019\",\n            PlanType.EnterpriseAnnually2020 => \"enterprise-annually-2020\",\n            PlanType.EnterpriseAnnually2023 => \"enterprise-annually-2023\",\n            PlanType.EnterpriseMonthly => \"enterprise-monthly\",\n            PlanType.EnterpriseMonthly2019 => \"enterprise-monthly-2019\",\n            PlanType.EnterpriseMonthly2020 => \"enterprise-monthly-2020\",\n            PlanType.EnterpriseMonthly2023 => \"enterprise-monthly-2023\",\n            PlanType.FamiliesAnnually => \"families\",\n            PlanType.FamiliesAnnually2025 =>\n                featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3)\n                    ? \"families-2025\"\n                    : \"families\",\n            PlanType.FamiliesAnnually2019 => \"families-2019\",\n            PlanType.Free => \"free\",\n            PlanType.TeamsAnnually => \"teams-annually\",\n            PlanType.TeamsAnnually2019 => \"teams-annually-2019\",\n            PlanType.TeamsAnnually2020 => \"teams-annually-2020\",\n            PlanType.TeamsAnnually2023 => \"teams-annually-2023\",\n            PlanType.TeamsMonthly => \"teams-monthly\",\n            PlanType.TeamsMonthly2019 => \"teams-monthly-2019\",\n            PlanType.TeamsMonthly2020 => \"teams-monthly-2020\",\n            PlanType.TeamsMonthly2023 => \"teams-monthly-2023\",\n            PlanType.TeamsStarter => \"teams-starter\",\n            PlanType.TeamsStarter2023 => \"teams-starter-2023\",\n            _ => null\n        };\n\n    /// <summary>\n    /// Safeguard used until the feature flag is enabled. Pricing service will return the\n    /// 2025PreMigration plan with \"families\" lookup key. When that is detected and the FF\n    /// is still disabled, set the lookup key to families-2025 so PlanAdapter will assign\n    /// the correct plan.\n    /// </summary>\n    /// <param name=\"plan\">The plan to preprocess</param>\n    private Plan PreProcessFamiliesPreMigrationPlan(Plan plan)\n    {\n        if (plan.LookupKey == \"families\" && !featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3))\n            plan.LookupKey = \"families-2025\";\n        return plan;\n    }\n\n}\n"
  },
  {
    "path": "src/Core/Billing/Pricing/ServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Billing.Pricing;\n\npublic static class ServiceCollectionExtensions\n{\n    public static void AddPricingClient(this IServiceCollection services)\n    {\n        services.AddHttpClient<IPricingClient, PricingClient>((serviceProvider, httpClient) =>\n        {\n            var globalSettings = serviceProvider.GetRequiredService<GlobalSettings>();\n            if (string.IsNullOrEmpty(globalSettings.PricingUri))\n            {\n                return;\n            }\n            httpClient.BaseAddress = new Uri(globalSettings.PricingUri);\n            httpClient.DefaultRequestHeaders.Add(\"Accept\", \"application/json\");\n        });\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Entities/ClientOrganizationMigrationRecord.cs",
    "content": "﻿#nullable enable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Billing.Providers.Entities;\n\npublic class ClientOrganizationMigrationRecord : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public Guid ProviderId { get; set; }\n    public PlanType PlanType { get; set; }\n    public int Seats { get; set; }\n    public short? MaxStorageGb { get; set; }\n    [MaxLength(50)] public string GatewayCustomerId { get; set; } = null!;\n    [MaxLength(50)] public string GatewaySubscriptionId { get; set; } = null!;\n    public DateTime? ExpirationDate { get; set; }\n    public int? MaxAutoscaleSeats { get; set; }\n    public OrganizationStatusType Status { get; set; }\n\n    public void SetNewId()\n    {\n        if (Id == default)\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Entities/ProviderInvoiceItem.cs",
    "content": "﻿#nullable enable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Billing.Providers.Entities;\n\npublic class ProviderInvoiceItem : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid ProviderId { get; set; }\n    [MaxLength(50)]\n    public string InvoiceId { get; set; } = null!;\n    [MaxLength(50)]\n    public string? InvoiceNumber { get; set; }\n    public Guid? ClientId { get; set; }\n    [MaxLength(50)]\n    public string ClientName { get; set; } = null!;\n    [MaxLength(50)]\n    public string PlanName { get; set; } = null!;\n    public int AssignedSeats { get; set; }\n    public int UsedSeats { get; set; }\n    public decimal Total { get; set; }\n    public DateTime Created { get; set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        if (Id == default)\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Entities/ProviderPlan.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Billing.Providers.Entities;\n\npublic class ProviderPlan : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid ProviderId { get; set; }\n    public PlanType PlanType { get; set; }\n    public int? SeatMinimum { get; set; }\n    public int? PurchasedSeats { get; set; }\n    public int? AllocatedSeats { get; set; }\n\n    public void SetNewId()\n    {\n        if (Id == default)\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n\n    public bool IsConfigured() => SeatMinimum.HasValue && PurchasedSeats.HasValue && AllocatedSeats.HasValue;\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Models/AddableOrganization.cs",
    "content": "﻿namespace Bit.Core.Billing.Providers.Models;\n\npublic record AddableOrganization(\n    Guid Id,\n    string Name,\n    string Plan,\n    int Seats,\n    bool Disabled = false);\n"
  },
  {
    "path": "src/Core/Billing/Providers/Models/ChangeProviderPlansCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Providers.Models;\n\npublic record ChangeProviderPlanCommand(\n    Provider Provider,\n    Guid ProviderPlanId,\n    PlanType NewPlan);\n"
  },
  {
    "path": "src/Core/Billing/Providers/Models/ConfiguredProviderPlan.cs",
    "content": "﻿using Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Billing.Providers.Models;\n\npublic record ConfiguredProviderPlan(\n    Guid Id,\n    Guid ProviderId,\n    Plan Plan,\n    decimal Price,\n    int SeatMinimum,\n    int PurchasedSeats,\n    int AssignedSeats);\n"
  },
  {
    "path": "src/Core/Billing/Providers/Models/ProviderWarnings.cs",
    "content": "﻿namespace Bit.Core.Billing.Providers.Models;\n\npublic class ProviderWarnings\n{\n    public SuspensionWarning? Suspension { get; set; }\n    public TaxIdWarning? TaxId { get; set; }\n\n    public record SuspensionWarning\n    {\n        public required string Resolution { get; set; }\n        public DateTime? SubscriptionCancelsAt { get; set; }\n    }\n\n    public record TaxIdWarning\n    {\n        public required string Type { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Models/UpdateProviderSeatMinimumsCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Providers.Models;\n\n/// <param name=\"Provider\">The provider to update the seat minimums for.</param>\n/// <param name=\"Configuration\">The new seat minimums for the provider.</param>\npublic record UpdateProviderSeatMinimumsCommand(\n    Provider Provider,\n    IReadOnlyCollection<(PlanType Plan, int SeatsMinimum)> Configuration);\n"
  },
  {
    "path": "src/Core/Billing/Providers/Queries/IGetProviderWarningsQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Providers.Models;\n\nnamespace Bit.Core.Billing.Providers.Queries;\n\npublic interface IGetProviderWarningsQuery\n{\n    Task<ProviderWarnings?> Run(Provider provider);\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Repositories/IClientOrganizationMigrationRecordRepository.cs",
    "content": "﻿using Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Billing.Providers.Repositories;\n\npublic interface IClientOrganizationMigrationRecordRepository : IRepository<ClientOrganizationMigrationRecord, Guid>\n{\n    Task<ClientOrganizationMigrationRecord> GetByOrganizationId(Guid organizationId);\n    Task<ICollection<ClientOrganizationMigrationRecord>> GetByProviderId(Guid providerId);\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Repositories/IProviderInvoiceItemRepository.cs",
    "content": "﻿using Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Billing.Providers.Repositories;\n\npublic interface IProviderInvoiceItemRepository : IRepository<ProviderInvoiceItem, Guid>\n{\n    Task<ICollection<ProviderInvoiceItem>> GetByInvoiceId(string invoiceId);\n    Task<ICollection<ProviderInvoiceItem>> GetByProviderId(Guid providerId);\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Repositories/IProviderPlanRepository.cs",
    "content": "﻿using Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Billing.Providers.Repositories;\n\npublic interface IProviderPlanRepository : IRepository<ProviderPlan, Guid>\n{\n    Task<ICollection<ProviderPlan>> GetByProviderId(Guid providerId);\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Services/IBusinessUnitConverter.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing OneOf;\n\nnamespace Bit.Core.Billing.Providers.Services;\n\npublic interface IBusinessUnitConverter\n{\n    /// <summary>\n    /// Finalizes the process of converting the <paramref name=\"organization\"/> to a <see cref=\"ProviderType.BusinessUnit\"/> by\n    /// saving all the necessary key provided by the client and updating the <paramref name=\"organization\"/>'s subscription to a\n    /// provider subscription.\n    /// </summary>\n    /// <param name=\"organization\">The organization to convert to a business unit.</param>\n    /// <param name=\"userId\">The ID of the organization member who will be the provider admin.</param>\n    /// <param name=\"token\">The token sent to the client as part of the <see cref=\"InitiateConversion\"/> process.</param>\n    /// <param name=\"providerKey\">The encrypted provider key used to enable the <see cref=\"ProviderUser\"/>.</param>\n    /// <param name=\"organizationKey\">The encrypted organization key used to enable the <see cref=\"ProviderOrganization\"/>.</param>\n    /// <returns>The provider ID</returns>\n    Task<Guid> FinalizeConversion(\n        Organization organization,\n        Guid userId,\n        string token,\n        string providerKey,\n        string organizationKey);\n\n    /// <summary>\n    /// Begins the process of converting the <paramref name=\"organization\"/> to a <see cref=\"ProviderType.BusinessUnit\"/> by\n    /// creating all the necessary database entities and sending a setup invitation to the <paramref name=\"providerAdminEmail\"/>.\n    /// </summary>\n    /// <param name=\"organization\">The organization to convert to a business unit.</param>\n    /// <param name=\"providerAdminEmail\">The email address of the organization member who will be the provider admin.</param>\n    /// <returns>Either the newly created provider ID or a list of validation failures.</returns>\n    Task<OneOf<Guid, List<string>>> InitiateConversion(\n        Organization organization,\n        string providerAdminEmail);\n\n    /// <summary>\n    /// Checks if the <paramref name=\"organization\"/> has a business unit conversion in progress and, if it does, resends the\n    /// setup invitation to the provider admin.\n    /// </summary>\n    /// <param name=\"organization\">The organization to convert to a business unit.</param>\n    /// <param name=\"providerAdminEmail\">The email address of the organization member who will be the provider admin.</param>\n    Task ResendConversionInvite(\n        Organization organization,\n        string providerAdminEmail);\n\n    /// <summary>\n    /// Checks if the <paramref name=\"organization\"/> has a business unit conversion in progress and, if it does, resets that conversion\n    /// by deleting all the database entities created as part of <see cref=\"InitiateConversion\"/>.\n    /// </summary>\n    /// <param name=\"organization\">The organization to convert to a business unit.</param>\n    /// <param name=\"providerAdminEmail\">The email address of the organization member who will be the provider admin.</param>\n    Task ResetConversion(\n        Organization organization,\n        string providerAdminEmail);\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Services/IProviderBillingService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Models;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Providers.Services;\n\npublic interface IProviderBillingService\n{\n    Task AddExistingOrganization(\n        Provider provider,\n        Organization organization,\n        string key);\n\n    /// <summary>\n    /// Changes the assigned provider plan for the provider.\n    /// </summary>\n    /// <param name=\"command\">The command to change the provider plan.</param>\n    Task ChangePlan(ChangeProviderPlanCommand command);\n\n    /// <summary>\n    /// Create a Stripe <see cref=\"Stripe.Customer\"/> for the provided client <paramref name=\"organization\"/> utilizing\n    /// the address and tax information of its <paramref name=\"provider\"/>.\n    /// </summary>\n    /// <param name=\"provider\">The MSP that owns the client organization.</param>\n    /// <param name=\"organization\">The client organization to create a Stripe <see cref=\"Stripe.Customer\"/> for.</param>\n    Task CreateCustomerForClientOrganization(\n        Provider provider,\n        Organization organization);\n\n    /// <summary>\n    /// Generate a provider's client invoice report in CSV format for the specified <paramref name=\"invoiceId\"/>. Utilizes the <see cref=\"ProviderInvoiceItem\"/>\n    /// records saved for the <paramref name=\"invoiceId\"/> as part of our webhook processing for the <b>\"invoice.created\"</b> and <b>\"invoice.finalized\"</b> Stripe events.\n    /// </summary>\n    /// <param name=\"invoiceId\">The ID of the Stripe <see cref=\"Stripe.Invoice\"/> to generate the report for.</param>\n    /// <returns>The provider's client invoice report as a byte array.</returns>\n    Task<byte[]> GenerateClientInvoiceReport(\n        string invoiceId);\n\n    Task<IEnumerable<AddableOrganization>> GetAddableOrganizations(\n        Provider provider,\n        Guid userId);\n\n    /// <summary>\n    /// Scales the <paramref name=\"provider\"/>'s seats for the specified <paramref name=\"planType\"/> using the provided <paramref name=\"seatAdjustment\"/>.\n    /// This operation may autoscale the provider's Stripe <see cref=\"Stripe.Subscription\"/> depending on the <paramref name=\"provider\"/>'s seat minimum for the\n    /// specified <paramref name=\"planType\"/>.\n    /// </summary>\n    /// <param name=\"provider\">The <see cref=\"Provider\"/> to scale seats for.</param>\n    /// <param name=\"planType\">The <see cref=\"PlanType\"/> to scale seats for.</param>\n    /// <param name=\"seatAdjustment\">The change in the number of seats you'd like to apply to the <paramref name=\"provider\"/>.</param>\n    Task ScaleSeats(\n        Provider provider,\n        PlanType planType,\n        int seatAdjustment);\n\n    /// <summary>\n    /// Determines whether the provided <paramref name=\"seatAdjustment\"/> will result in a purchase for the <paramref name=\"provider\"/>'s <see cref=\"PlanType\"/>.\n    /// Seat adjustments that result in purchases include:\n    /// <list type=\"bullet\">\n    /// <item>The <paramref name=\"provider\"/> going from below the seat minimum to above the seat minimum for the provided <paramref name=\"planType\"/></item>\n    /// <item>The <paramref name=\"provider\"/> going from above the seat minimum to further above the seat minimum for the provided <paramref name=\"planType\"/></item>\n    /// </list>\n    /// </summary>\n    /// <param name=\"provider\">The provider to check seat adjustments for.</param>\n    /// <param name=\"planType\">The plan type to check seat adjustments for.</param>\n    /// <param name=\"seatAdjustment\">The change in seats for the <paramref name=\"provider\"/>'s <paramref name=\"planType\"/>.</param>\n    Task<bool> SeatAdjustmentResultsInPurchase(\n        Provider provider,\n        PlanType planType,\n        int seatAdjustment);\n\n    /// <summary>\n    /// For use during the provider setup process, this method creates a Stripe <see cref=\"Stripe.Customer\"/> for the specified <paramref name=\"provider\"/> utilizing the provided <paramref name=\"paymentMethod\"/> and <paramref name=\"billingAddress\"/>.\n    /// </summary>\n    /// <param name=\"provider\">The <see cref=\"Provider\"/> to create a Stripe customer for.</param>\n    /// <param name=\"paymentMethod\">The <see cref=\"TokenizedPaymentMethod\"/> (e.g., Credit Card, Bank Account, or PayPal) to attach to the customer.</param>\n    /// <param name=\"billingAddress\">The <see cref=\"BillingAddress\"/> containing the customer's billing information including address and tax ID details.</param>\n    /// <returns>The newly created <see cref=\"Stripe.Customer\"/> for the <paramref name=\"provider\"/>.</returns>\n    Task<Customer> SetupCustomer(\n        Provider provider,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress);\n\n    /// <summary>\n    /// For use during the provider setup process, this method starts a Stripe <see cref=\"Stripe.Subscription\"/> for the given <paramref name=\"provider\"/>.\n    /// <see cref=\"Provider\"/> subscriptions will always be started with a <see cref=\"Stripe.SubscriptionItem\"/> for both the <see cref=\"PlanType.TeamsMonthly\"/>\n    /// and <see cref=\"PlanType.EnterpriseMonthly\"/> plan, and the quantity for each item will be equal the provider's seat minimum for each respective plan.\n    /// </summary>\n    /// <param name=\"provider\">The provider to create the <see cref=\"Stripe.Subscription\"/> for.</param>\n    /// <returns>The newly created <see cref=\"Stripe.Subscription\"/> for the <paramref name=\"provider\"/>.</returns>\n    /// <remarks>This method requires the <paramref name=\"provider\"/> to already have a linked Stripe <see cref=\"Stripe.Customer\"/> via its <see cref=\"Provider.GatewayCustomerId\"/> field.</remarks>\n    Task<Subscription> SetupSubscription(\n        Provider provider);\n\n    Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);\n\n    /// <summary>\n    /// Updates the provider name and email on the Stripe customer entry.\n    /// This only updates Stripe, not the Bitwarden database.\n    /// </summary>\n    /// <param name=\"provider\">The provider to update in Stripe.</param>\n    Task UpdateProviderNameAndEmail(Provider provider);\n}\n"
  },
  {
    "path": "src/Core/Billing/Providers/Services/ProviderPriceAdapter.cs",
    "content": "﻿// ReSharper disable SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Billing.Enums;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Providers.Services;\n\npublic static class ProviderPriceAdapter\n{\n    public static class MSP\n    {\n        public static class Active\n        {\n            public const string Enterprise = \"provider-portal-enterprise-monthly-2025\";\n            public const string Teams = \"provider-portal-teams-monthly-2025\";\n        }\n\n        public static class Legacy\n        {\n            public const string Enterprise = \"password-manager-provider-portal-enterprise-monthly-2024\";\n            public const string Teams = \"password-manager-provider-portal-teams-monthly-2024\";\n            public static readonly List<string> List = [Enterprise, Teams];\n        }\n    }\n\n    public static class BusinessUnit\n    {\n        public static class Active\n        {\n            public const string Annually = \"business-unit-portal-enterprise-annually-2025\";\n            public const string Monthly = \"business-unit-portal-enterprise-monthly-2025\";\n        }\n\n        public static class Legacy\n        {\n            public const string Annually = \"password-manager-provider-portal-enterprise-annually-2024\";\n            public const string Monthly = \"password-manager-provider-portal-enterprise-monthly-2024\";\n            public static readonly List<string> List = [Annually, Monthly];\n        }\n    }\n\n    /// <summary>\n    /// Uses the <paramref name=\"provider\"/>'s <see cref=\"Provider.Type\"/> and <paramref name=\"subscription\"/> to determine\n    /// whether the <paramref name=\"provider\"/> is on active or legacy pricing and then returns a Stripe price ID for the provided\n    /// <paramref name=\"planType\"/> based on that determination.\n    /// </summary>\n    /// <param name=\"provider\">The provider to get the Stripe price ID for.</param>\n    /// <param name=\"subscription\">The provider's subscription.</param>\n    /// <param name=\"planType\">The plan type correlating to the desired Stripe price ID.</param>\n    /// <returns>A Stripe <see cref=\"Stripe.Price\"/> ID.</returns>\n    /// <exception cref=\"BillingException\">Thrown when the provider's type is not <see cref=\"ProviderType.Msp\"/> or <see cref=\"ProviderType.BusinessUnit\"/>.</exception>\n    /// <exception cref=\"BillingException\">Thrown when the provided <paramref name=\"planType\"/> does not relate to a Stripe price ID.</exception>\n    public static string GetPriceId(\n        Provider provider,\n        Subscription subscription,\n        PlanType planType)\n    {\n        var priceIds = subscription.Items.Select(item => item.Price.Id);\n\n        var invalidPlanType =\n            new BillingException(message: $\"PlanType {planType} does not have an associated provider price in Stripe\");\n\n        return provider.Type switch\n        {\n            ProviderType.Msp => MSP.Legacy.List.Intersect(priceIds).Any()\n                ? planType switch\n                {\n                    PlanType.TeamsMonthly => MSP.Legacy.Teams,\n                    PlanType.EnterpriseMonthly => MSP.Legacy.Enterprise,\n                    _ => throw invalidPlanType\n                }\n                : planType switch\n                {\n                    PlanType.TeamsMonthly => MSP.Active.Teams,\n                    PlanType.EnterpriseMonthly => MSP.Active.Enterprise,\n                    _ => throw invalidPlanType\n                },\n            ProviderType.BusinessUnit => BusinessUnit.Legacy.List.Intersect(priceIds).Any()\n                ? planType switch\n                {\n                    PlanType.EnterpriseAnnually => BusinessUnit.Legacy.Annually,\n                    PlanType.EnterpriseMonthly => BusinessUnit.Legacy.Monthly,\n                    _ => throw invalidPlanType\n                }\n                : planType switch\n                {\n                    PlanType.EnterpriseAnnually => BusinessUnit.Active.Annually,\n                    PlanType.EnterpriseMonthly => BusinessUnit.Active.Monthly,\n                    _ => throw invalidPlanType\n                },\n            _ => throw new BillingException(\n                $\"ProviderType {provider.Type} does not have any associated provider price IDs\")\n        };\n    }\n\n    /// <summary>\n    /// Uses the <paramref name=\"provider\"/>'s <see cref=\"Provider.Type\"/> to return the active Stripe price ID for the provided\n    /// <paramref name=\"planType\"/>.\n    /// </summary>\n    /// <param name=\"provider\">The provider to get the Stripe price ID for.</param>\n    /// <param name=\"planType\">The plan type correlating to the desired Stripe price ID.</param>\n    /// <returns>A Stripe <see cref=\"Stripe.Price\"/> ID.</returns>\n    /// <exception cref=\"BillingException\">Thrown when the provider's type is not <see cref=\"ProviderType.Msp\"/> or <see cref=\"ProviderType.BusinessUnit\"/>.</exception>\n    /// <exception cref=\"BillingException\">Thrown when the provided <paramref name=\"planType\"/> does not relate to a Stripe price ID.</exception>\n    public static string GetActivePriceId(\n        Provider provider,\n        PlanType planType)\n    {\n        var invalidPlanType =\n            new BillingException(message: $\"PlanType {planType} does not have an associated provider price in Stripe\");\n\n        return provider.Type switch\n        {\n            ProviderType.Msp => planType switch\n            {\n                PlanType.TeamsMonthly => MSP.Active.Teams,\n                PlanType.EnterpriseMonthly => MSP.Active.Enterprise,\n                _ => throw invalidPlanType\n            },\n            ProviderType.BusinessUnit => planType switch\n            {\n                PlanType.EnterpriseAnnually => BusinessUnit.Active.Annually,\n                PlanType.EnterpriseMonthly => BusinessUnit.Active.Monthly,\n                _ => throw invalidPlanType\n            },\n            _ => throw new BillingException(\n                $\"ProviderType {provider.Type} does not have any associated provider price IDs\")\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/DiscountAudienceFilters/AllUsersFilter.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Services.DiscountAudienceFilters;\n\npublic class AllUsersFilter : IDiscountAudienceFilter\n{\n    public DiscountAudienceType SupportedType => DiscountAudienceType.AllUsers;\n\n    public Task<IDictionary<DiscountTierType, bool>> IsUserEligible(User user, SubscriptionDiscount discount)\n    {\n        var eligibleTiers = Utilities.GetTierEligibilityDictionary();\n\n        if (discount.StripeProductIds == null || !discount.StripeProductIds.Any())\n        {\n            // If no product IDs are specified, the discount applies to all tiers\n            foreach (var tier in eligibleTiers.Keys.ToList())\n            {\n                eligibleTiers[tier] = true;\n            }\n            return Task.FromResult(eligibleTiers);\n        }\n\n        foreach (var tier in discount.StripeProductIds)\n        {\n            var discountTier = StripeConstants.ProductIDs.GetProductTier(tier);\n            if (discountTier != null)\n            {\n                eligibleTiers[discountTier.Value] = true;\n            }\n        }\n\n        return Task.FromResult(eligibleTiers);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/DiscountAudienceFilters/DiscountAudienceFilterFactory.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Services.DiscountAudienceFilters;\n\n/// <inheritdoc />\n/// <remarks>\n/// To add support for a new audience type: add an enum value, create a filter class,\n/// implement <see cref=\"IDiscountAudienceFilter.SupportedType\"/>, and register it in DI.\n/// </remarks>\npublic class DiscountAudienceFilterFactory(\n    IEnumerable<IDiscountAudienceFilter> filters) : IDiscountAudienceFilterFactory\n{\n    private readonly Dictionary<DiscountAudienceType, IDiscountAudienceFilter> _filters =\n        filters.ToDictionary(f => f.SupportedType);\n\n    public IDiscountAudienceFilter? GetFilter(DiscountAudienceType audienceType)\n        => _filters.GetValueOrDefault(audienceType);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/DiscountAudienceFilters/IDiscountAudienceFilter.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Services.DiscountAudienceFilters;\n\n/// <summary>\n/// Defines an eligibility check for a specific <see cref=\"Enums.DiscountAudienceType\"/>.\n/// Implementations are instantiated by <see cref=\"IDiscountAudienceFilterFactory\"/> and\n/// represent a single audience targeting rule.\n/// </summary>\npublic interface IDiscountAudienceFilter\n{\n    /// <summary>\n    /// The <see cref=\"DiscountAudienceType\"/> this filter handles.\n    /// </summary>\n    DiscountAudienceType SupportedType { get; }\n\n    /// <summary>\n    /// Determines whether the given <paramref name=\"user\"/> is eligible for the specified <paramref name=\"discount\"/>\n    /// </summary>\n    /// <param name=\"user\">The user to evaluate.</param>\n    /// <param name=\"discount\">The discount being evaluated for eligibility.</param>\n    /// <returns>A per-tier eligibility matrix mapping each <see cref=\"DiscountTierType\"/> to whether the user is eligible.</returns>\n    Task<IDictionary<DiscountTierType, bool>> IsUserEligible(User user, SubscriptionDiscount discount);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/DiscountAudienceFilters/IDiscountAudienceFilterFactory.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.Services.DiscountAudienceFilters;\n\n/// <summary>\n/// Creates <see cref=\"IDiscountAudienceFilter\"/> instances for a given <see cref=\"DiscountAudienceType\"/>.\n/// </summary>\npublic interface IDiscountAudienceFilterFactory\n{\n    /// <summary>\n    /// Returns the <see cref=\"IDiscountAudienceFilter\"/> for the specified <paramref name=\"audienceType\"/>,\n    /// or <see langword=\"null\"/> if no filter is registered for that type.\n    /// </summary>\n    /// <param name=\"audienceType\">The audience type to retrieve a filter for.</param>\n    IDiscountAudienceFilter? GetFilter(DiscountAudienceType audienceType);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/DiscountAudienceFilters/UserHasNoPreviousSubscriptionsFilter.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Stripe;\nusing StripeProductIDs = Bit.Core.Billing.Constants.StripeConstants.ProductIDs;\n\nnamespace Bit.Core.Billing.Services.DiscountAudienceFilters;\n\n/// <summary>\n/// Restricts a discount to users who have never held a Bitwarden subscription.\n/// </summary>\npublic class UserHasNoPreviousSubscriptionsFilter : IDiscountAudienceFilter\n{\n    private readonly IOrganizationUserRepository organizationUserRepository;\n    private readonly IStripeAdapter stripeAdapter;\n    private readonly IPricingClient pricingClient;\n\n    // Caches to avoid redundant checks for users during the same request.\n    private readonly Dictionary<Guid, bool> _premiumEligibilityByUser = new();\n    private readonly Dictionary<Guid, bool> _familiesOrgOwnershipByUser = new();\n\n    public UserHasNoPreviousSubscriptionsFilter(\n        IStripeAdapter stripeAdapter,\n        IOrganizationUserRepository organizationUserRepository,\n        IPricingClient pricingClient)\n    {\n        this.organizationUserRepository = organizationUserRepository;\n        this.stripeAdapter = stripeAdapter;\n        this.pricingClient = pricingClient;\n    }\n\n    public DiscountAudienceType SupportedType => DiscountAudienceType.UserHasNoPreviousSubscriptions;\n\n    public async Task<IDictionary<DiscountTierType, bool>> IsUserEligible(User user, SubscriptionDiscount discount)\n    {\n        var eligibleTiers = Utilities.GetTierEligibilityDictionary();\n\n        if (IsApplicableToProduct(discount, StripeProductIDs.Premium))\n        {\n            eligibleTiers[DiscountTierType.Premium] = await IsUserEligibleForPremiumDiscount(user);\n        }\n\n        if (IsApplicableToProduct(discount, StripeProductIDs.Families))\n        {\n            eligibleTiers[DiscountTierType.Families] = !await IsUserOwnerOfFamiliesOrgAsync(user);\n        }\n\n        return eligibleTiers;\n    }\n\n    /**\n     * Determines if the discount is applicable to the given product based on the discount's configured Stripe product IDs.\n     * If the discount does not specify any product IDs, it is considered applicable to all products.\n     */\n    private bool IsApplicableToProduct(SubscriptionDiscount discount, string productId) =>\n        discount.StripeProductIds?.Contains(productId) ?? true;\n\n    private async Task<bool> IsUserEligibleForPremiumDiscount(User user)\n    {\n        if (_premiumEligibilityByUser.TryGetValue(user.Id, out var cached))\n        {\n            return cached;\n        }\n\n        bool result;\n\n        if (user.Premium)\n        {\n            result = false;\n        }\n        else if (string.IsNullOrWhiteSpace(user.GatewayCustomerId))\n        {\n            result = true;\n        }\n        else\n        {\n            result = !await UserHasPreviousPremiumSubscriptionAsync(user);\n        }\n\n        return _premiumEligibilityByUser[user.Id] = result;\n    }\n\n    private async Task<bool> UserHasPreviousPremiumSubscriptionAsync(User user)\n    {\n        var premiumPlans = await pricingClient.ListPremiumPlans();\n        var premiumPriceIds = premiumPlans.Select(p => p.Seat.StripePriceId).ToHashSet();\n        try\n        {\n            var subscriptions = await stripeAdapter.ListSubscriptionsAsync(new SubscriptionListOptions\n            {\n                Customer = user.GatewayCustomerId,\n                Expand = [\"data.items.data.price\"]\n            });\n            return subscriptions.Data.Any(subscription =>\n                subscription.Items.Data.Any(item => premiumPriceIds.Contains(item.Price.Id)));\n        }\n        catch (StripeException ex) when (ex.StripeError.Code == StripeConstants.ErrorCodes.ResourceMissing)\n        {\n            // If the customer ID does not exist in Stripe, treat as no previous subscriptions\n            return false;\n        }\n    }\n\n    private async Task<bool> IsUserOwnerOfFamiliesOrgAsync(User user)\n    {\n        if (_familiesOrgOwnershipByUser.TryGetValue(user.Id, out var cached))\n        {\n            return cached;\n        }\n\n        var orgDetails = await organizationUserRepository.GetManyDetailsByUserAsync(\n            user.Id,\n            OrganizationUserStatusType.Confirmed);\n\n        var result = orgDetails.Any(o =>\n            o.Type == OrganizationUserType.Owner &&\n            IsFamiliesPlanType(o.PlanType));\n\n        return _familiesOrgOwnershipByUser[user.Id] = result;\n    }\n\n    private static bool IsFamiliesPlanType(PlanType planType) =>\n        planType is PlanType.FamiliesAnnually\n            or PlanType.FamiliesAnnually2019\n            or PlanType.FamiliesAnnually2025;\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/ILicensingService.cs",
    "content": "﻿#nullable enable\n\nusing System.Security.Claims;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.Billing.Services;\n\npublic interface ILicensingService\n{\n    Task ValidateOrganizationsAsync();\n    Task ValidateUsersAsync();\n    Task<bool> ValidateUserPremiumAsync(User user);\n    bool VerifyLicense(ILicense license);\n    byte[] SignLicense(ILicense license);\n    Task<OrganizationLicense?> ReadOrganizationLicenseAsync(Organization organization);\n    Task<OrganizationLicense?> ReadOrganizationLicenseAsync(Guid organizationId);\n    ClaimsPrincipal? GetClaimsPrincipalFromLicense(ILicense license);\n\n    Task<string?> CreateOrganizationTokenAsync(\n        Organization organization,\n        Guid installationId,\n        SubscriptionInfo subscriptionInfo);\n\n    Task<string?> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo);\n    Task WriteUserLicenseAsync(User user, UserLicense license);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/IPaymentHistoryService.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Services;\n\npublic interface IPaymentHistoryService\n{\n    Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetInvoiceHistoryAsync(\n        ISubscriber subscriber,\n        int pageSize = 5,\n        string? status = null,\n        string? startAfter = null);\n\n    Task<IEnumerable<BillingHistoryInfo.BillingTransaction>> GetTransactionHistoryAsync(\n        ISubscriber subscriber,\n        int pageSize = 5,\n        DateTime? startAfter = null);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/IPremiumUserBillingService.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Services;\n\npublic interface IPremiumUserBillingService\n{\n    Task Credit(User user, decimal amount);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/IStripeAdapter.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.BitStripe;\nusing Stripe;\nusing Stripe.BillingPortal;\nusing Stripe.Tax;\n\nnamespace Bit.Core.Billing.Services;\n\npublic interface IStripeAdapter\n{\n    Task<Customer> CreateCustomerAsync(CustomerCreateOptions customerCreateOptions);\n    Task<Customer> GetCustomerAsync(string id, CustomerGetOptions options = null);\n    Task<Customer> UpdateCustomerAsync(string id, CustomerUpdateOptions options = null);\n    Task<Customer> DeleteCustomerAsync(string id);\n    Task<List<PaymentMethod>> ListCustomerPaymentMethodsAsync(string id, CustomerPaymentMethodListOptions options = null);\n    Task<CustomerBalanceTransaction> CreateCustomerBalanceTransactionAsync(string customerId,\n        CustomerBalanceTransactionCreateOptions options);\n    Task<Subscription> CreateSubscriptionAsync(SubscriptionCreateOptions subscriptionCreateOptions);\n    Task<Subscription> GetSubscriptionAsync(string id, SubscriptionGetOptions options = null);\n    Task<StripeList<Registration>> ListTaxRegistrationsAsync(RegistrationListOptions options = null);\n    Task DeleteCustomerDiscountAsync(string customerId, CustomerDeleteDiscountOptions options = null);\n    Task<Subscription> UpdateSubscriptionAsync(string id, SubscriptionUpdateOptions options = null);\n    Task<Subscription> CancelSubscriptionAsync(string id, SubscriptionCancelOptions options = null);\n    Task<Invoice> GetInvoiceAsync(string id, InvoiceGetOptions options = null);\n    Task<List<Invoice>> ListInvoicesAsync(StripeInvoiceListOptions options);\n    Task<Invoice> CreateInvoiceAsync(InvoiceCreateOptions options);\n    Task<Invoice> CreateInvoicePreviewAsync(InvoiceCreatePreviewOptions options);\n    Task<List<Invoice>> SearchInvoiceAsync(InvoiceSearchOptions options);\n    Task<Invoice> UpdateInvoiceAsync(string id, InvoiceUpdateOptions options);\n    Task<Invoice> FinalizeInvoiceAsync(string id, InvoiceFinalizeOptions options = null);\n    Task<Invoice> SendInvoiceAsync(string id, InvoiceSendOptions options = null);\n    Task<Invoice> PayInvoiceAsync(string id, InvoicePayOptions options = null);\n    Task<Invoice> DeleteInvoiceAsync(string id, InvoiceDeleteOptions options = null);\n    Task<Invoice> VoidInvoiceAsync(string id, InvoiceVoidOptions options = null);\n    IEnumerable<PaymentMethod> ListPaymentMethodsAutoPaging(PaymentMethodListOptions options);\n    IAsyncEnumerable<PaymentMethod> ListPaymentMethodsAutoPagingAsync(PaymentMethodListOptions options);\n    Task<PaymentMethod> AttachPaymentMethodAsync(string id, PaymentMethodAttachOptions options = null);\n    Task<PaymentMethod> DetachPaymentMethodAsync(string id, PaymentMethodDetachOptions options = null);\n    Task<TaxId> CreateTaxIdAsync(string id, TaxIdCreateOptions options);\n    Task<TaxId> DeleteTaxIdAsync(string customerId, string taxIdId, TaxIdDeleteOptions options = null);\n    Task<StripeList<Charge>> ListChargesAsync(ChargeListOptions options);\n    Task<Refund> CreateRefundAsync(RefundCreateOptions options);\n    Task<Card> DeleteCardAsync(string customerId, string cardId, CardDeleteOptions options = null);\n    Task<BankAccount> DeleteBankAccountAsync(string customerId, string bankAccount, BankAccountDeleteOptions options = null);\n    Task<SetupIntent> CreateSetupIntentAsync(SetupIntentCreateOptions options);\n    Task<List<SetupIntent>> ListSetupIntentsAsync(SetupIntentListOptions options);\n    Task CancelSetupIntentAsync(string id, SetupIntentCancelOptions options = null);\n    Task<SetupIntent> GetSetupIntentAsync(string id, SetupIntentGetOptions options = null);\n    Task<SetupIntent> UpdateSetupIntentAsync(string id, SetupIntentUpdateOptions options = null);\n    Task<Price> GetPriceAsync(string id, PriceGetOptions options = null);\n    Task<Coupon> GetCouponAsync(string couponId, CouponGetOptions options = null);\n    Task<List<Product>> ListProductsAsync(ProductListOptions options = null);\n    Task<StripeList<Subscription>> ListSubscriptionsAsync(SubscriptionListOptions options = null);\n    Task<Session> CreateBillingPortalSessionAsync(SessionCreateOptions options);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/IStripePaymentService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Billing.Services;\n\npublic interface IStripePaymentService\n{\n    Task CancelAndRecoverChargesAsync(ISubscriber subscriber);\n    Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);\n    Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);\n    Task<string> AdjustSubscription(\n        Organization organization,\n        Plan updatedPlan,\n        int newlyPurchasedPasswordManagerSeats,\n        bool subscribedToSecretsManager,\n        int? newlyPurchasedSecretsManagerSeats,\n        int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,\n        int newlyPurchasedAdditionalStorage);\n\n    /// <summary>\n    /// Used to update the organization's password manager subscription\n    /// </summary>\n    /// <param name=\"organization\"></param>\n    /// <param name=\"plan\"></param>\n    /// <param name=\"additionalSeats\">New seat total</param>\n    /// <returns></returns>\n    Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats);\n    Task<string> AdjustSmSeatsAsync(Organization organization, Plan plan, int additionalSeats);\n    Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId);\n\n    Task<string> AdjustServiceAccountsAsync(Organization organization, Plan plan, int additionalServiceAccounts);\n    Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false);\n    Task ReinstateSubscriptionAsync(ISubscriber subscriber);\n    Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount);\n    Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);\n    Task<BillingHistoryInfo> GetBillingHistoryAsync(ISubscriber subscriber);\n    Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber);\n    Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats, int additionalServiceAccount);\n    /// <summary>\n    /// Secrets Manager Standalone is a discount in Stripe that is used to give an organization access to Secrets Manager.\n    /// Usually, this also implies that when they invite a user to their organization, they are doing so for both Password\n    /// Manager and Secrets Manger.\n    ///\n    /// This will not call out to Stripe if they don't have a GatewayId or if they don't have Secrets Manager.\n    /// </summary>\n    /// <param name=\"organization\">Organization Entity</param>\n    /// <returns>If the organization has Secrets Manager and has the Standalone Stripe Discount</returns>\n    Task<bool> HasSecretsManagerStandalone(Organization organization);\n\n    /// <summary>\n    /// Secrets Manager Standalone is a discount in Stripe that is used to give an organization access to Secrets Manager.\n    /// Usually, this also implies that when they invite a user to their organization, they are doing so for both Password\n    /// Manager and Secrets Manger.\n    ///\n    /// This will not call out to Stripe if they don't have a GatewayId or if they don't have Secrets Manager.\n    /// </summary>\n    /// <param name=\"organization\">Organization Representation used for Inviting Organization Users</param>\n    /// <returns>If the organization has Secrets Manager and has the Standalone Stripe Discount</returns>\n    Task<bool> HasSecretsManagerStandalone(InviteOrganization organization);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/IStripeSyncService.cs",
    "content": "﻿namespace Bit.Core.Billing.Services;\n\npublic interface IStripeSyncService\n{\n    Task UpdateCustomerEmailAddressAsync(string gatewayCustomerId, string emailAddress);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/ISubscriberService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Tax.Models;\nusing Bit.Core.Entities;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Services;\n\npublic interface ISubscriberService\n{\n    /// <summary>\n    /// Cancels a subscriber's subscription while including user-provided feedback via the <paramref name=\"offboardingSurveyResponse\"/>.\n    /// If the <paramref name=\"cancelImmediately\"/> flag is <see langword=\"false\"/>,\n    /// this command sets the subscription's <b>\"cancel_at_end_of_period\"</b> property to <see langword=\"true\"/>.\n    /// Otherwise, this command cancels the subscription immediately.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber with the subscription to cancel.</param>\n    /// <param name=\"offboardingSurveyResponse\">An <see cref=\"OffboardingSurveyResponse\"/> DTO containing user-provided feedback on why they are cancelling the subscription.</param>\n    /// <param name=\"cancelImmediately\">A flag indicating whether to cancel the subscription immediately or at the end of the subscription period.</param>\n    Task CancelSubscription(\n        ISubscriber subscriber,\n        OffboardingSurveyResponse offboardingSurveyResponse,\n        bool cancelImmediately);\n\n    /// <summary>\n    /// Creates a Braintree <see cref=\"Braintree.Customer\"/> for the provided <paramref name=\"subscriber\"/> while attaching the provided <paramref name=\"paymentMethodNonce\"/>.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber to create a Braintree customer for.</param>\n    /// <param name=\"paymentMethodNonce\">A nonce representing the PayPal payment method the customer will use for payments.</param>\n    /// <returns>The <see cref=\"Braintree.Customer.Id\"/> of the created Braintree customer.</returns>\n    Task<string> CreateBraintreeCustomer(\n        ISubscriber subscriber,\n        string paymentMethodNonce);\n\n    Task<Customer> CreateStripeCustomer(\n        ISubscriber subscriber);\n\n    /// <summary>\n    /// Retrieves a Stripe <see cref=\"Customer\"/> using the <paramref name=\"subscriber\"/>'s <see cref=\"ISubscriber.GatewayCustomerId\"/> property.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber to retrieve the Stripe customer for.</param>\n    /// <param name=\"customerGetOptions\">Optional parameters that can be passed to Stripe to expand or modify the customer.</param>\n    /// <returns>A Stripe <see cref=\"Customer\"/>.</returns>\n    /// <exception cref=\"ArgumentNullException\">Thrown when the <paramref name=\"subscriber\"/> is <see langword=\"null\"/>.</exception>\n    /// <remarks>This method opts for returning <see langword=\"null\"/> rather than throwing exceptions, making it ideal for surfacing data from API endpoints.</remarks>\n    Task<Customer> GetCustomer(\n        ISubscriber subscriber,\n        CustomerGetOptions customerGetOptions = null);\n\n    /// <summary>\n    /// Retrieves a Stripe <see cref=\"Customer\"/> using the <paramref name=\"subscriber\"/>'s <see cref=\"ISubscriber.GatewayCustomerId\"/> property.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber to retrieve the Stripe customer for.</param>\n    /// <param name=\"customerGetOptions\">Optional parameters that can be passed to Stripe to expand or modify the customer.</param>\n    /// <returns>A Stripe <see cref=\"Customer\"/>.</returns>\n    /// <exception cref=\"ArgumentNullException\">Thrown when the <paramref name=\"subscriber\"/> is <see langword=\"null\"/>.</exception>\n    /// <exception cref=\"BillingException\">Thrown when the subscriber's <see cref=\"ISubscriber.GatewayCustomerId\"/> is <see langword=\"null\"/> or empty.</exception>\n    /// <exception cref=\"BillingException\">Thrown when the <see cref=\"Customer\"/> returned from Stripe's API is null.</exception>\n    Task<Customer> GetCustomerOrThrow(\n        ISubscriber subscriber,\n        CustomerGetOptions customerGetOptions = null);\n\n    /// <summary>\n    /// Retrieves a masked representation of the subscriber's payment source for presentation to a client.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber to retrieve the payment source for.</param>\n    /// <returns>A <see cref=\"PaymentSource\"/> containing a non-identifiable description of the subscriber's payment source. Example: VISA, *4242, 10/2026</returns>\n    Task<PaymentSource> GetPaymentSource(\n        ISubscriber subscriber);\n\n    /// <summary>\n    /// Retrieves a Stripe <see cref=\"Subscription\"/> using the <paramref name=\"subscriber\"/>'s <see cref=\"ISubscriber.GatewaySubscriptionId\"/> property.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber to retrieve the Stripe subscription for.</param>\n    /// <param name=\"subscriptionGetOptions\">Optional parameters that can be passed to Stripe to expand or modify the subscription.</param>\n    /// <returns>A Stripe <see cref=\"Subscription\"/>.</returns>\n    /// <exception cref=\"ArgumentNullException\">Thrown when the <paramref name=\"subscriber\"/> is <see langword=\"null\"/>.</exception>\n    /// <remarks>This method opts for returning <see langword=\"null\"/> rather than throwing exceptions, making it ideal for surfacing data from API endpoints.</remarks>\n    Task<Subscription> GetSubscription(\n        ISubscriber subscriber,\n        SubscriptionGetOptions subscriptionGetOptions = null);\n\n    /// <summary>\n    /// Retrieves a Stripe <see cref=\"Subscription\"/> using the <paramref name=\"subscriber\"/>'s <see cref=\"ISubscriber.GatewaySubscriptionId\"/> property.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber to retrieve the Stripe subscription for.</param>\n    /// <param name=\"subscriptionGetOptions\">Optional parameters that can be passed to Stripe to expand or modify the subscription.</param>\n    /// <returns>A Stripe <see cref=\"Subscription\"/>.</returns>\n    /// <exception cref=\"ArgumentNullException\">Thrown when the <paramref name=\"subscriber\"/> is <see langword=\"null\"/>.</exception>\n    /// <exception cref=\"BillingException\">Thrown when the subscriber's <see cref=\"ISubscriber.GatewaySubscriptionId\"/> is <see langword=\"null\"/> or empty.</exception>\n    /// <exception cref=\"BillingException\">Thrown when the <see cref=\"Subscription\"/> returned from Stripe's API is null.</exception>\n    Task<Subscription> GetSubscriptionOrThrow(\n        ISubscriber subscriber,\n        SubscriptionGetOptions subscriptionGetOptions = null);\n\n    /// <summary>\n    /// Attempts to remove a subscriber's saved payment source. If the Stripe <see cref=\"Stripe.Customer\"/> representing the\n    /// <paramref name=\"subscriber\"/> contains a valid <b>\"btCustomerId\"</b> key in its <see cref=\"Stripe.Customer.Metadata\"/> property,\n    /// this command will attempt to remove the Braintree <see cref=\"Braintree.PaymentMethod\"/>. Otherwise, it will attempt to remove the\n    /// Stripe <see cref=\"Stripe.PaymentMethod\"/>.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber to remove the saved payment source for.</param>\n    Task RemovePaymentSource(ISubscriber subscriber);\n\n    /// <summary>\n    /// Updates the tax information for the provided <paramref name=\"subscriber\"/>.\n    /// </summary>\n    /// <param name=\"subscriber\">The <paramref name=\"subscriber\"/> to update the tax information for.</param>\n    /// <param name=\"taxInformation\">A <see cref=\"TaxInformation\"/> representing the <paramref name=\"subscriber\"/>'s updated tax information.</param>\n    Task UpdateTaxInformation(\n        ISubscriber subscriber,\n        TaxInformation taxInformation);\n\n    /// <summary>\n    /// Validates whether the <paramref name=\"subscriber\"/>'s <see cref=\"ISubscriber.GatewayCustomerId\"/> exists in the gateway.\n    /// If the <paramref name=\"subscriber\"/>'s <see cref=\"ISubscriber.GatewayCustomerId\"/> is <see langword=\"null\"/> or empty, returns <see langword=\"true\"/>.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber whose gateway customer ID should be validated.</param>\n    /// <returns><see langword=\"true\"/> if the gateway customer ID is valid or empty; <see langword=\"false\"/> if the customer doesn't exist in the gateway.</returns>\n    /// <exception cref=\"ArgumentNullException\">Thrown when the <paramref name=\"subscriber\"/> is <see langword=\"null\"/>.</exception>\n    Task<bool> IsValidGatewayCustomerIdAsync(ISubscriber subscriber);\n\n    /// <summary>\n    /// Validates whether the <paramref name=\"subscriber\"/>'s <see cref=\"ISubscriber.GatewaySubscriptionId\"/> exists in the gateway.\n    /// If the <paramref name=\"subscriber\"/>'s <see cref=\"ISubscriber.GatewaySubscriptionId\"/> is <see langword=\"null\"/> or empty, returns <see langword=\"true\"/>.\n    /// </summary>\n    /// <param name=\"subscriber\">The subscriber whose gateway subscription ID should be validated.</param>\n    /// <returns><see langword=\"true\"/> if the gateway subscription ID is valid or empty; <see langword=\"false\"/> if the subscription doesn't exist in the gateway.</returns>\n    /// <exception cref=\"ArgumentNullException\">Thrown when the <paramref name=\"subscriber\"/> is <see langword=\"null\"/>.</exception>\n    Task<bool> IsValidGatewaySubscriptionIdAsync(ISubscriber subscriber);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/ISubscriptionDiscountService.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Services;\n\n/// <summary>\n/// Manages eligibility evaluation for subscription discounts.\n/// </summary>\npublic interface ISubscriptionDiscountService\n{\n    /// <summary>\n    /// Retrieves all active discounts the user is eligible for.\n    /// </summary>\n    /// <param name=\"user\">The user to evaluate discount eligibility for.</param>\n    /// <returns>The collection of <see cref=\"DiscountEligibility\"/> records pairing each eligible discount with its tier eligibility matrix.</returns>\n    Task<IEnumerable<DiscountEligibility>> GetEligibleDiscountsAsync(User user);\n\n    /// <summary>\n    /// Performs a server-side eligibility recheck for the provided coupon IDs before subscription creation,\n    /// confirming every coupon exists, is active, and the user qualifies for each on the specified tier.\n    /// </summary>\n    /// <param name=\"user\">The user to validate eligibility for.</param>\n    /// <param name=\"couponIds\">The Stripe coupon IDs to validate.</param>\n    /// <param name=\"tierType\">The product tier the user intends to subscribe to.</param>\n    /// <returns><see langword=\"true\"/> if all coupons are found in the user's eligible discounts and tier eligibility is <see langword=\"true\"/> for <paramref name=\"tierType\"/>; otherwise <see langword=\"false\"/>.</returns>\n    Task<bool> ValidateDiscountEligibilityForUserAsync(User user, IReadOnlyList<string> couponIds, DiscountTierType tierType);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/Implementations/LicensingService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.IdentityModel.Tokens.Jwt;\nusing System.Security.Claims;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Licenses.Models;\nusing Bit.Core.Billing.Licenses.Services;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Duende.IdentityModel;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.IdentityModel.Tokens;\n\nnamespace Bit.Core.Billing.Services;\n\npublic class LicensingService : ILicensingService\n{\n    private readonly X509Certificate2 _certificate;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IUserRepository _userRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IMailService _mailService;\n    private readonly ILogger<LicensingService> _logger;\n    private readonly ILicenseClaimsFactory<Organization> _organizationLicenseClaimsFactory;\n    private readonly ILicenseClaimsFactory<User> _userLicenseClaimsFactory;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    private IDictionary<Guid, DateTime> _userCheckCache = new Dictionary<Guid, DateTime>();\n\n    public LicensingService(\n        IUserRepository userRepository,\n        IOrganizationRepository organizationRepository,\n        IMailService mailService,\n        IWebHostEnvironment environment,\n        ILogger<LicensingService> logger,\n        IGlobalSettings globalSettings,\n        ILicenseClaimsFactory<Organization> organizationLicenseClaimsFactory,\n        ILicenseClaimsFactory<User> userLicenseClaimsFactory,\n        IPushNotificationService pushNotificationService)\n    {\n        _userRepository = userRepository;\n        _organizationRepository = organizationRepository;\n        _mailService = mailService;\n        _logger = logger;\n        _globalSettings = globalSettings;\n        _organizationLicenseClaimsFactory = organizationLicenseClaimsFactory;\n        _userLicenseClaimsFactory = userLicenseClaimsFactory;\n        _pushNotificationService = pushNotificationService;\n\n        var certThumbprint = environment.IsDevelopment() ?\n            \"207E64A231E8AA32AAF68A61037C075EBEBD553F\" :\n            \"‎B34876439FCDA2846505B2EFBBA6C4A951313EBE\";\n        if (_globalSettings.SelfHosted)\n        {\n            _certificate = CoreHelpers.GetEmbeddedCertificateAsync(environment.IsDevelopment() ? \"licensing_dev.cer\" : \"licensing.cer\", null)\n                .GetAwaiter().GetResult();\n        }\n        else if (CoreHelpers.SettingHasValue(_globalSettings.Storage?.ConnectionString) &&\n            CoreHelpers.SettingHasValue(_globalSettings.LicenseCertificatePassword))\n        {\n            _certificate = CoreHelpers.GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, \"certificates\",\n                \"licensing.pfx\", _globalSettings.LicenseCertificatePassword)\n                .GetAwaiter().GetResult();\n        }\n        else\n        {\n            _certificate = CoreHelpers.GetCertificate(certThumbprint);\n        }\n\n        if (_certificate == null || !_certificate.Thumbprint.Equals(CoreHelpers.CleanCertificateThumbprint(certThumbprint),\n            StringComparison.InvariantCultureIgnoreCase))\n        {\n            throw new Exception(\"Invalid licensing certificate.\");\n        }\n\n        if (_globalSettings.SelfHosted && !CoreHelpers.SettingHasValue(_globalSettings.LicenseDirectory))\n        {\n            throw new InvalidOperationException(\"No license directory.\");\n        }\n    }\n\n    public async Task ValidateOrganizationsAsync()\n    {\n        if (!_globalSettings.SelfHosted)\n        {\n            return;\n        }\n\n        var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();\n        _logger.LogInformation(Core.Constants.BypassFiltersEventId, null,\n            \"Validating licenses for {NumberOfOrganizations} organizations.\", enabledOrgs.Count);\n\n        var exceptions = new List<Exception>();\n\n        foreach (var org in enabledOrgs)\n        {\n            try\n            {\n                var license = await ReadOrganizationLicenseAsync(org);\n                if (license == null)\n                {\n                    await DisableOrganizationAsync(org, null, \"No license file.\");\n                    continue;\n                }\n\n                var totalLicensedOrgs = enabledOrgs.Count(o => string.Equals(o.LicenseKey, license.LicenseKey));\n                if (totalLicensedOrgs > 1)\n                {\n                    await DisableOrganizationAsync(org, license, \"Multiple organizations.\");\n                    continue;\n                }\n\n                if (!license.VerifyData(org, GetClaimsPrincipalFromLicense(license), _globalSettings))\n                {\n                    await DisableOrganizationAsync(org, license, \"Invalid data.\");\n                    continue;\n                }\n\n                if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate))\n                {\n                    await DisableOrganizationAsync(org, license, \"Invalid signature.\");\n                    continue;\n                }\n            }\n            catch (Exception ex)\n            {\n                exceptions.Add(ex);\n            }\n        }\n\n        if (exceptions.Any())\n        {\n            throw new AggregateException(\"There were one or more exceptions while validating organizations.\", exceptions);\n        }\n    }\n\n    private async Task DisableOrganizationAsync(Organization org, ILicense license, string reason)\n    {\n        _logger.LogInformation(Core.Constants.BypassFiltersEventId, null,\n            \"Organization {0} ({1}) has an invalid license and is being disabled. Reason: {2}\",\n            org.Id, org.DisplayName(), reason);\n        org.Enabled = false;\n        org.ExpirationDate = license?.Expires ?? DateTime.UtcNow;\n        org.RevisionDate = DateTime.UtcNow;\n        await _organizationRepository.ReplaceAsync(org);\n\n        await _mailService.SendLicenseExpiredAsync(new List<string> { org.BillingEmail }, org.DisplayName());\n    }\n\n    public async Task ValidateUsersAsync()\n    {\n        if (!_globalSettings.SelfHosted)\n        {\n            return;\n        }\n\n        var premiumUsers = await _userRepository.GetManyByPremiumAsync(true);\n        _logger.LogInformation(Core.Constants.BypassFiltersEventId, null,\n            \"Validating premium for {0} users.\", premiumUsers.Count);\n\n        foreach (var user in premiumUsers)\n        {\n            await ProcessUserValidationAsync(user);\n        }\n    }\n\n    public async Task<bool> ValidateUserPremiumAsync(User user)\n    {\n        if (!_globalSettings.SelfHosted)\n        {\n            return user.Premium;\n        }\n\n        if (!user.Premium)\n        {\n            return false;\n        }\n\n        // Only check once per day\n        var now = DateTime.UtcNow;\n        if (_userCheckCache.TryGetValue(user.Id, out var lastCheck))\n        {\n            if (lastCheck < now && now - lastCheck < TimeSpan.FromDays(1))\n            {\n                return user.Premium;\n            }\n            else\n            {\n                _userCheckCache[user.Id] = now;\n            }\n        }\n        else\n        {\n            _userCheckCache.Add(user.Id, now);\n        }\n\n        _logger.LogInformation(Core.Constants.BypassFiltersEventId, null,\n            \"Validating premium license for user {0}({1}).\", user.Id, user.Email);\n        return await ProcessUserValidationAsync(user);\n    }\n\n    private async Task<bool> ProcessUserValidationAsync(User user)\n    {\n        var license = ReadUserLicense(user);\n        if (license == null)\n        {\n            await DisablePremiumAsync(user, null, \"No license file.\");\n            return false;\n        }\n\n        var claimsPrincipal = GetClaimsPrincipalFromLicense(license);\n        if (!license.VerifyData(user, claimsPrincipal))\n        {\n            await DisablePremiumAsync(user, license, \"Invalid data.\");\n            return false;\n        }\n\n        if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate))\n        {\n            await DisablePremiumAsync(user, license, \"Invalid signature.\");\n            return false;\n        }\n\n        return true;\n    }\n\n    private async Task DisablePremiumAsync(User user, ILicense license, string reason)\n    {\n        _logger.LogInformation(Core.Constants.BypassFiltersEventId, null,\n            \"User {0}({1}) has an invalid license and premium is being disabled. Reason: {2}\",\n            user.Id, user.Email, reason);\n\n        user.Premium = false;\n        user.PremiumExpirationDate = license?.Expires ?? DateTime.UtcNow;\n        user.RevisionDate = DateTime.UtcNow;\n        await _userRepository.ReplaceAsync(user);\n\n        await _mailService.SendLicenseExpiredAsync(new List<string> { user.Email });\n\n        await _pushNotificationService.PushAsync(new PushNotification<PremiumStatusPushNotification>\n        {\n            Type = PushType.PremiumStatusChanged,\n            Target = NotificationTarget.User,\n            TargetId = user.Id,\n            Payload = new PremiumStatusPushNotification\n            {\n                UserId = user.Id,\n                Premium = user.Premium,\n            },\n            ExcludeCurrentContext = false,\n        });\n    }\n\n    public bool VerifyLicense(ILicense license)\n    {\n        if (string.IsNullOrWhiteSpace(license.Token))\n        {\n            return license.VerifySignature(_certificate);\n        }\n\n        try\n        {\n            _ = GetClaimsPrincipalFromLicense(license);\n            return true;\n        }\n        catch (Exception e)\n        {\n            _logger.LogWarning(e, \"Invalid token.\");\n            return false;\n        }\n    }\n\n    public byte[] SignLicense(ILicense license)\n    {\n        if (_globalSettings.SelfHosted || !_certificate.HasPrivateKey)\n        {\n            throw new InvalidOperationException(\"Cannot sign licenses.\");\n        }\n\n        return license.Sign(_certificate);\n    }\n\n    private UserLicense ReadUserLicense(User user)\n    {\n        var filePath = $\"{_globalSettings.LicenseDirectory}/user/{user.Id}.json\";\n        if (!File.Exists(filePath))\n        {\n            return null;\n        }\n\n        var data = File.ReadAllText(filePath, Encoding.UTF8);\n        return JsonSerializer.Deserialize<UserLicense>(data);\n    }\n\n    public Task<OrganizationLicense> ReadOrganizationLicenseAsync(Organization organization) =>\n        ReadOrganizationLicenseAsync(organization.Id);\n    public async Task<OrganizationLicense> ReadOrganizationLicenseAsync(Guid organizationId)\n    {\n        var filePath = Path.Combine(_globalSettings.LicenseDirectory, \"organization\", $\"{organizationId}.json\");\n        if (!File.Exists(filePath))\n        {\n            return null;\n        }\n\n        using var fs = File.OpenRead(filePath);\n        return await JsonSerializer.DeserializeAsync<OrganizationLicense>(fs);\n    }\n\n    public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license)\n    {\n        if (string.IsNullOrWhiteSpace(license.Token))\n        {\n            return null;\n        }\n\n        var audience = license switch\n        {\n            OrganizationLicense orgLicense => $\"organization:{orgLicense.Id}\",\n            UserLicense userLicense => $\"user:{userLicense.Id}\",\n            _ => throw new ArgumentException(\"Unsupported license type.\", nameof(license)),\n        };\n\n        var token = license.Token;\n        var tokenHandler = new JwtSecurityTokenHandler();\n        var validationParameters = new TokenValidationParameters\n        {\n            ValidateIssuerSigningKey = true,\n            IssuerSigningKey = new X509SecurityKey(_certificate),\n            ValidateIssuer = true,\n            ValidIssuer = \"bitwarden\",\n            ValidateAudience = true,\n            ValidAudience = audience,\n            ValidateLifetime = true,\n            ClockSkew = TimeSpan.Zero,\n            RequireExpirationTime = true\n        };\n\n        try\n        {\n            return tokenHandler.ValidateToken(token, validationParameters, out _);\n        }\n        catch (Exception ex)\n        {\n            // Token exceptions thrown are interpreted by the client as Identity errors and cause the user to logout\n            // Mask them by rethrowing as BadRequestException\n            throw new BadRequestException($\"Invalid license. {ex.Message}\");\n        }\n    }\n\n    public async Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)\n    {\n        var licenseContext = new LicenseContext\n        {\n            InstallationId = installationId,\n            SubscriptionInfo = subscriptionInfo,\n        };\n\n        var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext);\n        var audience = $\"organization:{organization.Id}\";\n\n        return GenerateToken(claims, audience);\n    }\n\n    public async Task<string> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo)\n    {\n        var licenseContext = new LicenseContext { SubscriptionInfo = subscriptionInfo };\n        var claims = await _userLicenseClaimsFactory.GenerateClaims(user, licenseContext);\n        var audience = $\"user:{user.Id}\";\n\n        return GenerateToken(claims, audience);\n    }\n\n    private string GenerateToken(List<Claim> claims, string audience)\n    {\n        if (claims.All(claim => claim.Type != JwtClaimTypes.JwtId))\n        {\n            claims.Add(new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()));\n        }\n\n        var securityKey = new RsaSecurityKey(_certificate.GetRSAPrivateKey());\n        var tokenDescriptor = new SecurityTokenDescriptor\n        {\n            Subject = new ClaimsIdentity(claims),\n            Issuer = \"bitwarden\",\n            Audience = audience,\n            NotBefore = DateTime.UtcNow,\n            Expires = DateTime.UtcNow.AddYears(1), // Org expiration is a claim\n            SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256Signature)\n        };\n\n        var tokenHandler = new JwtSecurityTokenHandler();\n        var token = tokenHandler.CreateToken(tokenDescriptor);\n        return tokenHandler.WriteToken(token);\n    }\n\n    public async Task WriteUserLicenseAsync(User user, UserLicense license)\n    {\n        var dir = $\"{_globalSettings.LicenseDirectory}/user\";\n        Directory.CreateDirectory(dir);\n        await using var fs = File.OpenWrite(Path.Combine(dir, $\"{user.Id}.json\"));\n        await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/Implementations/PaymentHistoryService.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.BitStripe;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Billing.Services.Implementations;\n\npublic class PaymentHistoryService(\n    IStripeAdapter stripeAdapter,\n    ITransactionRepository transactionRepository) : IPaymentHistoryService\n{\n    public async Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetInvoiceHistoryAsync(\n        ISubscriber subscriber,\n        int pageSize = 5,\n        string? status = null,\n        string? startAfter = null)\n    {\n        if (subscriber is not { GatewayCustomerId: not null, GatewaySubscriptionId: not null })\n        {\n            return Array.Empty<BillingHistoryInfo.BillingInvoice>();\n        }\n\n        var invoices = await stripeAdapter.ListInvoicesAsync(new StripeInvoiceListOptions\n        {\n            Customer = subscriber.GatewayCustomerId,\n            Limit = pageSize,\n            Status = status,\n            StartingAfter = startAfter\n        });\n\n        return invoices.Select(invoice => new BillingHistoryInfo.BillingInvoice(invoice));\n\n    }\n\n    public async Task<IEnumerable<BillingHistoryInfo.BillingTransaction>> GetTransactionHistoryAsync(\n        ISubscriber subscriber,\n        int pageSize = 5,\n        DateTime? startAfter = null)\n    {\n        var transactions = subscriber switch\n        {\n            User => await transactionRepository.GetManyByUserIdAsync(subscriber.Id, pageSize, startAfter),\n            Organization => await transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id, pageSize, startAfter),\n            _ => null\n        };\n\n        return transactions?.OrderByDescending(i => i.CreationDate)\n            .Select(t => new BillingHistoryInfo.BillingTransaction(t))\n            ?? Array.Empty<BillingHistoryInfo.BillingTransaction>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Services.Implementations;\n\npublic class PremiumUserBillingService(\n    IGlobalSettings globalSettings,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService,\n    IUserRepository userRepository) : IPremiumUserBillingService\n{\n    public async Task Credit(User user, decimal amount)\n    {\n        var customer = await subscriberService.GetCustomer(user);\n\n        // Negative credit represents a balance, and all Stripe denomination is in cents.\n        var credit = (long)(amount * -100);\n\n        if (customer == null)\n        {\n            var options = new CustomerCreateOptions\n            {\n                Balance = credit,\n                Description = user.Name,\n                Email = user.Email,\n                InvoiceSettings = new CustomerInvoiceSettingsOptions\n                {\n                    CustomFields =\n                    [\n                        new CustomerInvoiceSettingsCustomFieldOptions\n                        {\n                            Name = user.SubscriberType(),\n                            Value = user.SubscriberName().Length <= 30\n                                ? user.SubscriberName()\n                                : user.SubscriberName()[..30]\n                        }\n                    ]\n                },\n                Metadata = new Dictionary<string, string>\n                {\n                    [\"region\"] = globalSettings.BaseServiceUri.CloudRegion,\n                    [\"userId\"] = user.Id.ToString()\n                }\n            };\n\n            customer = await stripeAdapter.CreateCustomerAsync(options);\n\n            user.Gateway = GatewayType.Stripe;\n            user.GatewayCustomerId = customer.Id;\n            await userRepository.ReplaceAsync(user);\n        }\n        else\n        {\n            var options = new CustomerUpdateOptions\n            {\n                Balance = customer.Balance + credit\n            };\n\n            await stripeAdapter.UpdateCustomerAsync(customer.Id, options);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/Implementations/StripeAdapter.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n\n#nullable disable\n\nusing Bit.Core.Models.BitStripe;\nusing Stripe;\nusing Stripe.BillingPortal;\nusing Stripe.Tax;\nusing Stripe.TestHelpers;\nusing CustomerService = Stripe.CustomerService;\nusing RefundService = Stripe.RefundService;\n\nnamespace Bit.Core.Billing.Services.Implementations;\n\npublic class StripeAdapter : IStripeAdapter\n{\n    private readonly CustomerService _customerService;\n    private readonly SubscriptionService _subscriptionService;\n    private readonly InvoiceService _invoiceService;\n    private readonly PaymentMethodService _paymentMethodService;\n    private readonly TaxIdService _taxIdService;\n    private readonly ChargeService _chargeService;\n    private readonly RefundService _refundService;\n    private readonly CardService _cardService;\n    private readonly BankAccountService _bankAccountService;\n    private readonly PriceService _priceService;\n    private readonly SetupIntentService _setupIntentService;\n    private readonly TestClockService _testClockService;\n    private readonly CustomerBalanceTransactionService _customerBalanceTransactionService;\n    private readonly RegistrationService _taxRegistrationService;\n    private readonly CouponService _couponService;\n    private readonly ProductService _productService;\n    private readonly SessionService _billingPortalSessionService;\n\n    public StripeAdapter()\n    {\n        _customerService = new CustomerService();\n        _subscriptionService = new SubscriptionService();\n        _invoiceService = new InvoiceService();\n        _paymentMethodService = new PaymentMethodService();\n        _taxIdService = new TaxIdService();\n        _chargeService = new ChargeService();\n        _refundService = new RefundService();\n        _cardService = new CardService();\n        _bankAccountService = new BankAccountService();\n        _priceService = new PriceService();\n        _setupIntentService = new SetupIntentService();\n        _testClockService = new TestClockService();\n        _customerBalanceTransactionService = new CustomerBalanceTransactionService();\n        _taxRegistrationService = new RegistrationService();\n        _couponService = new CouponService();\n        _productService = new ProductService();\n        _billingPortalSessionService = new SessionService();\n    }\n\n    /**************\n     ** CUSTOMER **\n     **************/\n    public Task<Customer> CreateCustomerAsync(CustomerCreateOptions options) =>\n        _customerService.CreateAsync(options);\n\n    public Task DeleteCustomerDiscountAsync(string customerId, CustomerDeleteDiscountOptions options = null) =>\n        _customerService.DeleteDiscountAsync(customerId, options);\n\n    public Task<Customer> GetCustomerAsync(string id, CustomerGetOptions options = null) =>\n        _customerService.GetAsync(id, options);\n\n    public Task<Customer> UpdateCustomerAsync(string id, CustomerUpdateOptions options = null) =>\n        _customerService.UpdateAsync(id, options);\n\n    public Task<Customer> DeleteCustomerAsync(string id) =>\n        _customerService.DeleteAsync(id);\n\n    public async Task<List<PaymentMethod>> ListCustomerPaymentMethodsAsync(string id,\n        CustomerPaymentMethodListOptions options = null)\n    {\n        var paymentMethods = await _customerService.ListPaymentMethodsAsync(id, options);\n        return paymentMethods.Data;\n    }\n\n    public Task<CustomerBalanceTransaction> CreateCustomerBalanceTransactionAsync(string customerId,\n        CustomerBalanceTransactionCreateOptions options) =>\n        _customerBalanceTransactionService.CreateAsync(customerId, options);\n\n    /******************\n     ** SUBSCRIPTION **\n     ******************/\n    public Task<Subscription> CreateSubscriptionAsync(SubscriptionCreateOptions options) =>\n        _subscriptionService.CreateAsync(options);\n\n    public Task<Subscription> GetSubscriptionAsync(string id, SubscriptionGetOptions options = null) =>\n        _subscriptionService.GetAsync(id, options);\n\n    public Task<Subscription> UpdateSubscriptionAsync(string id,\n        SubscriptionUpdateOptions options = null) =>\n        _subscriptionService.UpdateAsync(id, options);\n\n    public Task<Subscription> CancelSubscriptionAsync(string id, SubscriptionCancelOptions options = null) =>\n        _subscriptionService.CancelAsync(id, options);\n\n    /*************\n     ** INVOICE **\n     *************/\n    public Task<Invoice> GetInvoiceAsync(string id, InvoiceGetOptions options = null) =>\n        _invoiceService.GetAsync(id, options);\n\n    public async Task<List<Invoice>> ListInvoicesAsync(StripeInvoiceListOptions options)\n    {\n        if (!options.SelectAll)\n        {\n            return (await _invoiceService.ListAsync(options.ToInvoiceListOptions())).Data;\n        }\n\n        options.Limit = 100;\n\n        var invoices = new List<Invoice>();\n\n        await foreach (var invoice in _invoiceService.ListAutoPagingAsync(options.ToInvoiceListOptions()))\n        {\n            invoices.Add(invoice);\n        }\n\n        return invoices;\n    }\n\n    public Task<Invoice> CreateInvoiceAsync(InvoiceCreateOptions options) =>\n        _invoiceService.CreateAsync(options);\n\n    public Task<Invoice> CreateInvoicePreviewAsync(InvoiceCreatePreviewOptions options) =>\n        _invoiceService.CreatePreviewAsync(options);\n\n    public async Task<List<Invoice>> SearchInvoiceAsync(InvoiceSearchOptions options) =>\n        (await _invoiceService.SearchAsync(options)).Data;\n\n    public Task<Invoice> UpdateInvoiceAsync(string id, InvoiceUpdateOptions options) =>\n        _invoiceService.UpdateAsync(id, options);\n\n    public Task<Invoice> FinalizeInvoiceAsync(string id, InvoiceFinalizeOptions options = null) =>\n        _invoiceService.FinalizeInvoiceAsync(id, options);\n\n    public Task<Invoice> SendInvoiceAsync(string id, InvoiceSendOptions options = null) =>\n        _invoiceService.SendInvoiceAsync(id, options);\n\n    public Task<Invoice> PayInvoiceAsync(string id, InvoicePayOptions options = null) =>\n        _invoiceService.PayAsync(id, options);\n\n    public Task<Invoice> DeleteInvoiceAsync(string id, InvoiceDeleteOptions options = null) =>\n        _invoiceService.DeleteAsync(id, options);\n\n    public Task<Invoice> VoidInvoiceAsync(string id, InvoiceVoidOptions options = null) =>\n        _invoiceService.VoidInvoiceAsync(id, options);\n\n    /********************\n     ** PAYMENT METHOD **\n     ********************/\n    public IEnumerable<PaymentMethod> ListPaymentMethodsAutoPaging(PaymentMethodListOptions options) =>\n        _paymentMethodService.ListAutoPaging(options);\n\n    public IAsyncEnumerable<PaymentMethod> ListPaymentMethodsAutoPagingAsync(PaymentMethodListOptions options)\n        => _paymentMethodService.ListAutoPagingAsync(options);\n\n    public Task<PaymentMethod> AttachPaymentMethodAsync(string id, PaymentMethodAttachOptions options = null) =>\n        _paymentMethodService.AttachAsync(id, options);\n\n    public Task<PaymentMethod> DetachPaymentMethodAsync(string id, PaymentMethodDetachOptions options = null) =>\n        _paymentMethodService.DetachAsync(id, options);\n\n    /************\n     ** TAX ID **\n     ************/\n    public Task<TaxId> CreateTaxIdAsync(string id, TaxIdCreateOptions options) =>\n        _taxIdService.CreateAsync(id, options);\n\n    public Task<TaxId> DeleteTaxIdAsync(string customerId, string taxIdId,\n        TaxIdDeleteOptions options = null) =>\n        _taxIdService.DeleteAsync(customerId, taxIdId, options);\n\n    /******************\n     ** BANK ACCOUNT **\n     ******************/\n    public Task<BankAccount> DeleteBankAccountAsync(string customerId, string bankAccount, BankAccountDeleteOptions options = null) =>\n        _bankAccountService.DeleteAsync(customerId, bankAccount, options);\n\n    /***********\n     ** PRICE **\n     ***********/\n    public Task<Price> GetPriceAsync(string id, PriceGetOptions options = null) =>\n        _priceService.GetAsync(id, options);\n\n    /******************\n     ** SETUP INTENT **\n     ******************/\n    public Task<SetupIntent> CreateSetupIntentAsync(SetupIntentCreateOptions options) =>\n        _setupIntentService.CreateAsync(options);\n\n    public async Task<List<SetupIntent>> ListSetupIntentsAsync(SetupIntentListOptions options) =>\n        (await _setupIntentService.ListAsync(options)).Data;\n\n    public Task CancelSetupIntentAsync(string id, SetupIntentCancelOptions options = null) =>\n        _setupIntentService.CancelAsync(id, options);\n\n    public Task<SetupIntent> GetSetupIntentAsync(string id, SetupIntentGetOptions options = null) =>\n        _setupIntentService.GetAsync(id, options);\n\n    public Task<SetupIntent> UpdateSetupIntentAsync(string id, SetupIntentUpdateOptions options = null) =>\n        _setupIntentService.UpdateAsync(id, options);\n\n    /*******************\n     ** MISCELLANEOUS **\n     *******************/\n    public Task<StripeList<Charge>> ListChargesAsync(ChargeListOptions options) =>\n        _chargeService.ListAsync(options);\n\n    public Task<StripeList<Registration>> ListTaxRegistrationsAsync(RegistrationListOptions options = null) =>\n        _taxRegistrationService.ListAsync(options);\n\n    public Task<Refund> CreateRefundAsync(RefundCreateOptions options) =>\n        _refundService.CreateAsync(options);\n\n    public Task<Card> DeleteCardAsync(string customerId, string cardId, CardDeleteOptions options = null) =>\n        _cardService.DeleteAsync(customerId, cardId, options);\n\n    /************\n     ** COUPON **\n     ************/\n    public Task<Coupon> GetCouponAsync(string couponId, CouponGetOptions options = null) =>\n        _couponService.GetAsync(couponId, options);\n\n    /*************\n     ** PRODUCT **\n     *************/\n    public async Task<List<Product>> ListProductsAsync(ProductListOptions options = null) =>\n        (await _productService.ListAsync(options)).Data;\n\n    /****************\n     ** SUBSCRIPTION **\n     ****************/\n    public Task<StripeList<Subscription>> ListSubscriptionsAsync(SubscriptionListOptions options = null) =>\n        _subscriptionService.ListAsync(options);\n\n    /**********************\n     ** BILLING PORTAL **\n     **********************/\n    public Task<Session> CreateBillingPortalSessionAsync(SessionCreateOptions options) =>\n        _billingPortalSessionService.CreateAsync(options);\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/Implementations/StripePaymentService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.BitStripe;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\nusing PaymentMethod = Stripe.PaymentMethod;\nusing StaticStore = Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Billing.Services.Implementations;\n\npublic class StripePaymentService : IStripePaymentService\n{\n    private const string SecretsManagerStandaloneDiscountId = \"sm-standalone\";\n\n    private readonly ITransactionRepository _transactionRepository;\n    private readonly ILogger<StripePaymentService> _logger;\n    private readonly Braintree.IBraintreeGateway _btGateway;\n    private readonly IStripeAdapter _stripeAdapter;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IPricingClient _pricingClient;\n\n    public StripePaymentService(\n        ITransactionRepository transactionRepository,\n        ILogger<StripePaymentService> logger,\n        IStripeAdapter stripeAdapter,\n        Braintree.IBraintreeGateway braintreeGateway,\n        IGlobalSettings globalSettings,\n        IPricingClient pricingClient)\n    {\n        _transactionRepository = transactionRepository;\n        _logger = logger;\n        _stripeAdapter = stripeAdapter;\n        _btGateway = braintreeGateway;\n        _globalSettings = globalSettings;\n        _pricingClient = pricingClient;\n    }\n\n    // TODO: Remove with FF: pm-32581-use-update-organization-subscription-command -> Updated SetUpSponsorshipCommand\n    private async Task ChangeOrganizationSponsorship(\n        Organization org,\n        OrganizationSponsorship sponsorship,\n        bool applySponsorship)\n    {\n        var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType);\n        var sponsoredPlan = sponsorship?.PlanSponsorshipType != null\n            ? SponsoredPlans.Get(sponsorship.PlanSponsorshipType.Value)\n            : null;\n        var subscriptionUpdate =\n            new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship);\n\n        await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, true);\n\n        var sub = await _stripeAdapter.GetSubscriptionAsync(org.GatewaySubscriptionId);\n        org.ExpirationDate = sub.GetCurrentPeriodEnd();\n\n        if (sponsorship is not null)\n        {\n            sponsorship.ValidUntil = sub.GetCurrentPeriodEnd();\n        }\n    }\n\n    // TODO: Remove with FF: pm-32581-use-update-organization-subscription-command -> Updated SetUpSponsorshipCommand\n    public Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) =>\n        ChangeOrganizationSponsorship(org, sponsorship, true);\n\n    // TODO: Remove -> Unused\n    public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>\n        ChangeOrganizationSponsorship(org, sponsorship, false);\n\n    private async Task<string> FinalizeSubscriptionChangeAsync(ISubscriber subscriber,\n        SubscriptionUpdate subscriptionUpdate, bool invoiceNow = false)\n    {\n        // remember, when in doubt, throw\n        var subGetOptions = new SubscriptionGetOptions { Expand = [\"customer.tax\", \"customer.tax_ids\"] };\n        var sub = await _stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId, subGetOptions);\n        if (sub == null)\n        {\n            throw new GatewayException(\"Subscription not found.\");\n        }\n\n        if (sub.Status == SubscriptionStatuses.Canceled)\n        {\n            throw new BadRequestException(\n                \"You do not have an active subscription. Reinstate your subscription to make changes.\");\n        }\n\n        var existingCoupon = sub.Customer.Discount?.Coupon?.Id;\n\n        var collectionMethod = sub.CollectionMethod;\n        var daysUntilDue = sub.DaysUntilDue;\n        var chargeNow = collectionMethod == \"charge_automatically\";\n        var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);\n        var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == \"year\";\n\n        var subUpdateOptions = new SubscriptionUpdateOptions\n        {\n            Items = updatedItemOptions,\n            ProrationBehavior = invoiceNow ? Core.Constants.AlwaysInvoice : Core.Constants.CreateProrations,\n            DaysUntilDue = daysUntilDue ?? 1,\n            CollectionMethod = \"send_invoice\"\n        };\n        if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != \"trialing\")\n        {\n            subUpdateOptions.PendingInvoiceItemInterval =\n                new SubscriptionPendingInvoiceItemIntervalOptions { Interval = \"month\" };\n        }\n\n        if (subscriptionUpdate is CompleteSubscriptionUpdate)\n        {\n            var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(sub.Customer.Address?.Country, sub.Customer.TaxExempt);\n            switch (sub.Customer)\n            {\n                case { Address.Country: not null and not \"\", TaxExempt: var customerTaxExemptStatus }\n                    when determinedTaxExemptStatus != customerTaxExemptStatus:\n                    await _stripeAdapter.UpdateCustomerAsync(sub.Customer.Id,\n                        new CustomerUpdateOptions { TaxExempt = determinedTaxExemptStatus });\n                    break;\n            }\n\n            subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };\n        }\n\n        if (!subscriptionUpdate.UpdateNeeded(sub))\n        {\n            // No need to update subscription, quantity matches\n            return null;\n        }\n\n        string paymentIntentClientSecret = null;\n        try\n        {\n            var subResponse = await _stripeAdapter.UpdateSubscriptionAsync(sub.Id, subUpdateOptions);\n\n            var invoice = await _stripeAdapter.GetInvoiceAsync(subResponse?.LatestInvoiceId, new InvoiceGetOptions());\n            if (invoice == null)\n            {\n                throw new BadRequestException(\"Unable to locate draft invoice for subscription update.\");\n            }\n\n            if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0))\n            {\n                try\n                {\n                    if (invoiceNow)\n                    {\n                        if (chargeNow)\n                        {\n                            paymentIntentClientSecret =\n                                await PayInvoiceAfterSubscriptionChangeAsync(subscriber, invoice);\n                        }\n                        else\n                        {\n                            invoice = await _stripeAdapter.FinalizeInvoiceAsync(subResponse.LatestInvoiceId,\n                                new InvoiceFinalizeOptions { AutoAdvance = false, });\n                            await _stripeAdapter.SendInvoiceAsync(invoice.Id, new InvoiceSendOptions());\n                            paymentIntentClientSecret = null;\n                        }\n                    }\n                }\n                catch\n                {\n                    // Need to revert the subscription\n                    await _stripeAdapter.UpdateSubscriptionAsync(sub.Id, new SubscriptionUpdateOptions\n                    {\n                        Items = subscriptionUpdate.RevertItemsOptions(sub),\n                        // This proration behavior prevents a false \"credit\" from\n                        //  being applied forward to the next month's invoice\n                        ProrationBehavior = \"none\",\n                        CollectionMethod = collectionMethod,\n                        DaysUntilDue = daysUntilDue,\n                    });\n                    throw;\n                }\n            }\n            else if (invoice.Status != StripeConstants.InvoiceStatus.Paid)\n            {\n                // Pay invoice with no charge to the customer this completes the invoice immediately without waiting the scheduled 1h\n                invoice = await _stripeAdapter.PayInvoiceAsync(subResponse.LatestInvoiceId);\n                paymentIntentClientSecret = null;\n            }\n        }\n        finally\n        {\n            // Change back the subscription collection method and/or days until due\n            if (collectionMethod != \"send_invoice\" || daysUntilDue == null)\n            {\n                await _stripeAdapter.UpdateSubscriptionAsync(sub.Id,\n                    new SubscriptionUpdateOptions\n                    {\n                        CollectionMethod = collectionMethod,\n                        DaysUntilDue = daysUntilDue,\n                    });\n            }\n\n            var customer = await _stripeAdapter.GetCustomerAsync(sub.CustomerId);\n\n            var newCoupon = customer.Discount?.Coupon?.Id;\n\n            if (!string.IsNullOrEmpty(existingCoupon) && string.IsNullOrEmpty(newCoupon))\n            {\n                // Re-add the lost coupon due to the update.\n                await _stripeAdapter.UpdateSubscriptionAsync(sub.Id, new SubscriptionUpdateOptions\n                {\n                    Discounts =\n                    [\n                        new SubscriptionDiscountOptions\n                        {\n                            Coupon = existingCoupon\n                        }\n                    ]\n                });\n            }\n        }\n\n        return paymentIntentClientSecret;\n    }\n\n    // TODO: Remove with FF: pm-32581-use-update-organization-subscription-command -> Updated UpgradeOrganizationPlanCommand\n    public async Task<string> AdjustSubscription(\n        Organization organization,\n        StaticStore.Plan updatedPlan,\n        int newlyPurchasedPasswordManagerSeats,\n        bool subscribedToSecretsManager,\n        int? newlyPurchasedSecretsManagerSeats,\n        int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,\n        int newlyPurchasedAdditionalStorage)\n    {\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n        return await FinalizeSubscriptionChangeAsync(\n            organization,\n            new CompleteSubscriptionUpdate(\n                organization,\n                plan,\n                new SubscriptionData\n                {\n                    Plan = updatedPlan,\n                    PurchasedPasswordManagerSeats = newlyPurchasedPasswordManagerSeats,\n                    SubscribedToSecretsManager = subscribedToSecretsManager,\n                    PurchasedSecretsManagerSeats = newlyPurchasedSecretsManagerSeats,\n                    PurchasedAdditionalSecretsManagerServiceAccounts =\n                        newlyPurchasedAdditionalSecretsManagerServiceAccounts,\n                    PurchasedAdditionalStorage = newlyPurchasedAdditionalStorage\n                }), true);\n    }\n\n    // TODO: Remove with FF: pm-32581-use-update-organization-subscription-command -> Updated OrganizationService.AdjustSeatsAsync\n    public Task<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats) =>\n        FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats));\n\n    // TODO: Remove with FF: pm-32581-use-update-organization-subscription-command -> Updated UpdateSecretsManagerSubscriptionCommand\n    public Task<string> AdjustSmSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats) =>\n        FinalizeSubscriptionChangeAsync(\n            organization,\n            new SmSeatSubscriptionUpdate(organization, plan, additionalSeats));\n\n    // TODO: Remove with FF: pm-32581-use-update-organization-subscription-command -> Updated UpdateSecretsManagerSubscriptionCommand\n    public Task<string> AdjustServiceAccountsAsync(\n        Organization organization,\n        StaticStore.Plan plan,\n        int additionalServiceAccounts) =>\n        FinalizeSubscriptionChangeAsync(\n            organization,\n            new ServiceAccountSubscriptionUpdate(organization, plan, additionalServiceAccounts));\n\n    public Task<string> AdjustStorageAsync(\n        IStorableSubscriber storableSubscriber,\n        int additionalStorage,\n        string storagePlanId)\n    {\n        return FinalizeSubscriptionChangeAsync(\n            storableSubscriber,\n            new StorageSubscriptionUpdate(storagePlanId, additionalStorage));\n    }\n\n    public async Task CancelAndRecoverChargesAsync(ISubscriber subscriber)\n    {\n        if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))\n        {\n            await _stripeAdapter.CancelSubscriptionAsync(subscriber.GatewaySubscriptionId,\n                new SubscriptionCancelOptions());\n        }\n\n        if (string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))\n        {\n            return;\n        }\n\n        var customer = await _stripeAdapter.GetCustomerAsync(subscriber.GatewayCustomerId);\n        if (customer == null)\n        {\n            return;\n        }\n\n        if (customer.Metadata.ContainsKey(\"btCustomerId\"))\n        {\n            var transactionRequest = new Braintree.TransactionSearchRequest()\n                .CustomerId.Is(customer.Metadata[\"btCustomerId\"]);\n            var transactions = _btGateway.Transaction.Search(transactionRequest);\n\n            if ((transactions?.MaximumCount ?? 0) > 0)\n            {\n                var txs = transactions.Cast<Braintree.Transaction>().Where(c => c.RefundedTransactionId == null);\n                foreach (var transaction in txs)\n                {\n                    await _btGateway.Transaction.RefundAsync(transaction.Id);\n                }\n            }\n\n            await _btGateway.Customer.DeleteAsync(customer.Metadata[\"btCustomerId\"]);\n        }\n        else\n        {\n            var charges = await _stripeAdapter.ListChargesAsync(new ChargeListOptions\n            {\n                Customer = subscriber.GatewayCustomerId\n            });\n\n            if (charges?.Data != null)\n            {\n                foreach (var charge in charges.Data.Where(c => c.Captured && !c.Refunded))\n                {\n                    await _stripeAdapter.CreateRefundAsync(new RefundCreateOptions { Charge = charge.Id });\n                }\n            }\n        }\n\n        await _stripeAdapter.DeleteCustomerAsync(subscriber.GatewayCustomerId);\n    }\n\n    public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Invoice invoice)\n    {\n        var customerOptions = new CustomerGetOptions();\n        customerOptions.AddExpand(\"default_source\");\n        customerOptions.AddExpand(\"invoice_settings.default_payment_method\");\n        var customer = await _stripeAdapter.GetCustomerAsync(subscriber.GatewayCustomerId, customerOptions);\n\n        string paymentIntentClientSecret = null;\n\n        // Invoice them and pay now instead of waiting until Stripe does this automatically.\n\n        string cardPaymentMethodId = null;\n        if (!customer.Metadata.ContainsKey(\"btCustomerId\"))\n        {\n            var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == \"card\";\n            var hasDefaultValidSource = customer.DefaultSource != null &&\n                                        (customer.DefaultSource is Card || customer.DefaultSource is BankAccount);\n            if (!hasDefaultCardPaymentMethod && !hasDefaultValidSource)\n            {\n                cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;\n                if (cardPaymentMethodId == null)\n                {\n                    // We're going to delete this draft invoice, it can't be paid\n                    try\n                    {\n                        await _stripeAdapter.DeleteInvoiceAsync(invoice.Id);\n                    }\n                    catch\n                    {\n                        await _stripeAdapter.FinalizeInvoiceAsync(invoice.Id,\n                            new InvoiceFinalizeOptions { AutoAdvance = false });\n                        await _stripeAdapter.VoidInvoiceAsync(invoice.Id);\n                    }\n\n                    throw new BadRequestException(\"No payment method is available.\");\n                }\n            }\n        }\n\n        Braintree.Transaction braintreeTransaction = null;\n        try\n        {\n            // Finalize the invoice (from Draft) w/o auto-advance so we\n            //  can attempt payment manually.\n            invoice = await _stripeAdapter.FinalizeInvoiceAsync(invoice.Id,\n                new InvoiceFinalizeOptions { AutoAdvance = false, });\n            var invoicePayOptions = new InvoicePayOptions { PaymentMethod = cardPaymentMethodId, };\n            if (customer?.Metadata?.ContainsKey(\"btCustomerId\") ?? false)\n            {\n                invoicePayOptions.PaidOutOfBand = true;\n                var btInvoiceAmount = (invoice.AmountDue / 100M);\n                var transactionResult = await _btGateway.Transaction.SaleAsync(\n                    new Braintree.TransactionRequest\n                    {\n                        Amount = btInvoiceAmount,\n                        CustomerId = customer.Metadata[\"btCustomerId\"],\n                        Options = new Braintree.TransactionOptionsRequest\n                        {\n                            SubmitForSettlement = true,\n                            PayPal = new Braintree.TransactionOptionsPayPalRequest\n                            {\n                                CustomField =\n                                    $\"{subscriber.BraintreeIdField()}:{subscriber.Id},{subscriber.BraintreeCloudRegionField()}:{_globalSettings.BaseServiceUri.CloudRegion}\"\n                            }\n                        },\n                        CustomFields = new Dictionary<string, string>\n                        {\n                            [subscriber.BraintreeIdField()] = subscriber.Id.ToString(),\n                            [subscriber.BraintreeCloudRegionField()] =\n                                _globalSettings.BaseServiceUri.CloudRegion\n                        }\n                    });\n\n                if (!transactionResult.IsSuccess())\n                {\n                    throw new GatewayException(\"Failed to charge PayPal customer.\");\n                }\n\n                braintreeTransaction = transactionResult.Target;\n                invoice = await _stripeAdapter.UpdateInvoiceAsync(invoice.Id, new InvoiceUpdateOptions\n                {\n                    Metadata = new Dictionary<string, string>\n                    {\n                        [\"btTransactionId\"] = braintreeTransaction.Id,\n                        [\"btPayPalTransactionId\"] =\n                            braintreeTransaction.PayPalDetails.AuthorizationId\n                    },\n                });\n                invoicePayOptions.PaidOutOfBand = true;\n            }\n\n            try\n            {\n                invoice = await _stripeAdapter.PayInvoiceAsync(invoice.Id, invoicePayOptions);\n            }\n            catch (StripeException e)\n            {\n                if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&\n                    e.StripeError?.Code == \"invoice_payment_intent_requires_action\")\n                {\n                    // SCA required, get intent client secret\n                    var invoiceGetOptions = new InvoiceGetOptions();\n                    invoiceGetOptions.AddExpand(\"confirmation_secret\");\n                    invoice = await _stripeAdapter.GetInvoiceAsync(invoice.Id, invoiceGetOptions);\n                    paymentIntentClientSecret = invoice?.ConfirmationSecret?.ClientSecret;\n                }\n                else\n                {\n                    throw new GatewayException(\"Unable to pay invoice.\");\n                }\n            }\n        }\n        catch (Exception e)\n        {\n            if (braintreeTransaction != null)\n            {\n                await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id);\n            }\n\n            if (invoice != null)\n            {\n                if (invoice.Status == \"paid\")\n                {\n                    // It's apparently paid, so we need to return w/o throwing an exception\n                    return paymentIntentClientSecret;\n                }\n\n                invoice = await _stripeAdapter.VoidInvoiceAsync(invoice.Id, new InvoiceVoidOptions());\n\n                // HACK: Workaround for customer balance credit\n                if (invoice.StartingBalance < 0)\n                {\n                    // Customer had a balance applied to this invoice. Since we can't fully trust Stripe to\n                    //  credit it back to the customer (even though their docs claim they will), we need to\n                    //  check that balance against the current customer balance and determine if it needs to be re-applied\n                    customer = await _stripeAdapter.GetCustomerAsync(subscriber.GatewayCustomerId, customerOptions);\n\n                    // Assumption: Customer balance should now be $0, otherwise payment would not have failed.\n                    if (customer.Balance == 0)\n                    {\n                        await _stripeAdapter.UpdateCustomerAsync(customer.Id,\n                            new CustomerUpdateOptions { Balance = invoice.StartingBalance });\n                    }\n                }\n            }\n\n            if (e is StripeException strEx &&\n                (strEx.StripeError?.Message?.Contains(\"cannot be used because it is not verified\") ?? false))\n            {\n                throw new GatewayException(\"Bank account is not yet verified.\");\n            }\n\n            // Let the caller perform any subscription change cleanup\n            throw;\n        }\n\n        return paymentIntentClientSecret;\n    }\n\n    public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false)\n    {\n        if (subscriber == null)\n        {\n            throw new ArgumentNullException(nameof(subscriber));\n        }\n\n        if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))\n        {\n            throw new GatewayException(\"No subscription.\");\n        }\n\n        var sub = await _stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId);\n        if (sub == null)\n        {\n            throw new GatewayException(\"Subscription was not found.\");\n        }\n\n        if (sub.CanceledAt.HasValue || sub.Status == \"canceled\" || sub.Status == \"unpaid\" ||\n            sub.Status == \"incomplete_expired\")\n        {\n            // Already canceled\n            return;\n        }\n\n        try\n        {\n            var canceledSub = endOfPeriod\n                ? await _stripeAdapter.UpdateSubscriptionAsync(sub.Id,\n                    new SubscriptionUpdateOptions { CancelAtPeriodEnd = true })\n                : await _stripeAdapter.CancelSubscriptionAsync(sub.Id, new SubscriptionCancelOptions());\n            if (!canceledSub.CanceledAt.HasValue)\n            {\n                throw new GatewayException(\"Unable to cancel subscription.\");\n            }\n        }\n        catch (StripeException e)\n        {\n            if (e.Message != $\"No such subscription: {subscriber.GatewaySubscriptionId}\")\n            {\n                throw;\n            }\n        }\n    }\n\n    public async Task ReinstateSubscriptionAsync(ISubscriber subscriber)\n    {\n        if (subscriber == null)\n        {\n            throw new ArgumentNullException(nameof(subscriber));\n        }\n\n        if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))\n        {\n            throw new GatewayException(\"No subscription.\");\n        }\n\n        var sub = await _stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId);\n        if (sub == null)\n        {\n            throw new GatewayException(\"Subscription was not found.\");\n        }\n\n        if ((sub.Status != \"active\" && sub.Status != \"trialing\" && !sub.Status.StartsWith(\"incomplete\")) ||\n            !sub.CanceledAt.HasValue)\n        {\n            throw new GatewayException(\"Subscription is not marked for cancellation.\");\n        }\n\n        var updatedSub = await _stripeAdapter.UpdateSubscriptionAsync(sub.Id,\n            new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });\n        if (updatedSub.CanceledAt.HasValue)\n        {\n            throw new GatewayException(\"Unable to reinstate subscription.\");\n        }\n    }\n\n    public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount)\n    {\n        Customer customer = null;\n        var customerExists = subscriber.Gateway == GatewayType.Stripe &&\n                             !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId);\n        if (customerExists)\n        {\n            customer = await _stripeAdapter.GetCustomerAsync(subscriber.GatewayCustomerId);\n        }\n        else\n        {\n            customer = await _stripeAdapter.CreateCustomerAsync(new CustomerCreateOptions\n            {\n                Email = subscriber.BillingEmailAddress(),\n                Description = subscriber.BillingName(),\n            });\n            subscriber.Gateway = GatewayType.Stripe;\n            subscriber.GatewayCustomerId = customer.Id;\n        }\n\n        await _stripeAdapter.UpdateCustomerAsync(customer.Id,\n            new CustomerUpdateOptions { Balance = customer.Balance - (long)(creditAmount * 100) });\n        return !customerExists;\n    }\n\n    public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber)\n    {\n        var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions());\n        var billingInfo = new BillingInfo\n        {\n            Balance = customer.GetBillingBalance(),\n            PaymentSource = await GetBillingPaymentSourceAsync(customer)\n        };\n\n        return billingInfo;\n    }\n\n    public async Task<BillingHistoryInfo> GetBillingHistoryAsync(ISubscriber subscriber)\n    {\n        var customer = await GetCustomerAsync(subscriber.GatewayCustomerId);\n        var billingInfo = new BillingHistoryInfo\n        {\n            Transactions = await GetBillingTransactionsAsync(subscriber, 20),\n            Invoices = await GetBillingInvoicesAsync(customer, 20)\n        };\n\n        return billingInfo;\n    }\n\n    public async Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber)\n    {\n        var subscriptionInfo = new SubscriptionInfo();\n\n        if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))\n        {\n            return subscriptionInfo;\n        }\n\n        var subscription = await _stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId,\n            new SubscriptionGetOptions { Expand = [\"customer.discount.coupon.applies_to\", \"discounts.coupon.applies_to\", \"test_clock\"] });\n\n        if (subscription == null)\n        {\n            return subscriptionInfo;\n        }\n\n        subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(subscription);\n\n        // Discount selection priority:\n        // 1. Customer-level discount (applies to all subscriptions for the customer)\n        // 2. First subscription-level discount (if multiple exist, FirstOrDefault() selects the first one)\n        // Note: When multiple subscription-level discounts exist, only the first one is used.\n        // This matches Stripe's behavior where the first discount in the list is applied.\n        // Defensive null checks: Even though we expand \"customer\" and \"discounts\", external APIs\n        // may not always return the expected data structure, so we use null-safe operators.\n        var discount = subscription.Customer?.Discount ?? subscription.Discounts?.FirstOrDefault();\n\n        if (discount != null)\n        {\n            subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(discount);\n        }\n\n        var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(subscription);\n\n        if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue)\n        {\n            subscriptionInfo.Subscription.SuspensionDate = suspensionDate;\n            subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate;\n        }\n\n        if (subscription is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))\n        {\n            return subscriptionInfo;\n        }\n\n        try\n        {\n            var invoiceCreatePreviewOptions = new InvoiceCreatePreviewOptions\n            {\n                Customer = subscriber.GatewayCustomerId,\n                Subscription = subscriber.GatewaySubscriptionId\n            };\n\n            var upcomingInvoice = await _stripeAdapter.CreateInvoicePreviewAsync(invoiceCreatePreviewOptions);\n\n            if (upcomingInvoice != null)\n            {\n                subscriptionInfo.UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(upcomingInvoice);\n            }\n        }\n        catch (StripeException ex)\n        {\n            _logger.LogWarning(\n                ex,\n                \"Failed to retrieve upcoming invoice for customer {CustomerId}, subscription {SubscriptionId}. Error Code: {ErrorCode}\",\n                subscriber.GatewayCustomerId,\n                subscriber.GatewaySubscriptionId,\n                ex.StripeError?.Code);\n        }\n\n        return subscriptionInfo;\n    }\n\n    public async Task<string> AddSecretsManagerToSubscription(\n        Organization org,\n        StaticStore.Plan plan,\n        int additionalSmSeats,\n        int additionalServiceAccount) =>\n        await FinalizeSubscriptionChangeAsync(\n            org,\n            new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount),\n            true);\n\n    public async Task<bool> HasSecretsManagerStandalone(Organization organization) =>\n        await HasSecretsManagerStandaloneAsync(gatewayCustomerId: organization.GatewayCustomerId,\n            organizationHasSecretsManager: organization.UseSecretsManager);\n\n    public async Task<bool> HasSecretsManagerStandalone(InviteOrganization organization) =>\n        await HasSecretsManagerStandaloneAsync(gatewayCustomerId: organization.GatewayCustomerId,\n            organizationHasSecretsManager: organization.UseSecretsManager);\n\n    private async Task<bool> HasSecretsManagerStandaloneAsync(string gatewayCustomerId,\n        bool organizationHasSecretsManager)\n    {\n        if (string.IsNullOrEmpty(gatewayCustomerId))\n        {\n            return false;\n        }\n\n        if (organizationHasSecretsManager is false)\n        {\n            return false;\n        }\n\n        var customer = await _stripeAdapter.GetCustomerAsync(gatewayCustomerId);\n\n        return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId;\n    }\n\n    private async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)\n    {\n        if (subscription.Status is not \"past_due\" && subscription.Status is not \"unpaid\")\n        {\n            return (null, null);\n        }\n\n        var openInvoices = await _stripeAdapter.SearchInvoiceAsync(new InvoiceSearchOptions\n        {\n            Query = $\"subscription:'{subscription.Id}' status:'open'\"\n        });\n\n        if (openInvoices.Count == 0)\n        {\n            return (null, null);\n        }\n\n        var currentDate = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;\n\n        switch (subscription.CollectionMethod)\n        {\n            case \"charge_automatically\":\n                {\n                    var firstOverdueInvoice = openInvoices\n                        .Where(invoice => invoice.PeriodEnd < currentDate && invoice.Attempted)\n                        .MinBy(invoice => invoice.Created);\n\n                    return (firstOverdueInvoice?.Created.AddDays(14), firstOverdueInvoice?.PeriodEnd);\n                }\n            case \"send_invoice\":\n                {\n                    var firstOverdueInvoice = openInvoices\n                        .Where(invoice => invoice.DueDate < currentDate)\n                        .MinBy(invoice => invoice.Created);\n\n                    return (firstOverdueInvoice?.DueDate?.AddDays(30), firstOverdueInvoice?.PeriodEnd);\n                }\n            default: return (null, null);\n        }\n    }\n\n    private PaymentMethod GetLatestCardPaymentMethod(string customerId)\n    {\n        var cardPaymentMethods = _stripeAdapter.ListPaymentMethodsAutoPaging(\n            new PaymentMethodListOptions { Customer = customerId, Type = \"card\" });\n        return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();\n    }\n\n    private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Customer customer)\n    {\n        if (customer == null)\n        {\n            return null;\n        }\n\n        if (customer.Metadata?.ContainsKey(\"btCustomerId\") ?? false)\n        {\n            try\n            {\n                var braintreeCustomer = await _btGateway.Customer.FindAsync(\n                    customer.Metadata[\"btCustomerId\"]);\n                if (braintreeCustomer?.DefaultPaymentMethod != null)\n                {\n                    return new BillingInfo.BillingSource(\n                        braintreeCustomer.DefaultPaymentMethod);\n                }\n            }\n            catch (Braintree.Exceptions.NotFoundException)\n            {\n            }\n        }\n\n        if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == \"card\")\n        {\n            return new BillingInfo.BillingSource(\n                customer.InvoiceSettings.DefaultPaymentMethod);\n        }\n\n        if (customer.DefaultSource != null &&\n            (customer.DefaultSource is Card || customer.DefaultSource is BankAccount))\n        {\n            return new BillingInfo.BillingSource(customer.DefaultSource);\n        }\n\n        var paymentMethod = GetLatestCardPaymentMethod(customer.Id);\n        return paymentMethod != null ? new BillingInfo.BillingSource(paymentMethod) : null;\n    }\n\n    private CustomerGetOptions GetCustomerPaymentOptions()\n    {\n        var customerOptions = new CustomerGetOptions();\n        customerOptions.AddExpand(\"default_source\");\n        customerOptions.AddExpand(\"invoice_settings.default_payment_method\");\n        return customerOptions;\n    }\n\n    private async Task<Customer> GetCustomerAsync(string gatewayCustomerId, CustomerGetOptions options = null)\n    {\n        if (string.IsNullOrWhiteSpace(gatewayCustomerId))\n        {\n            return null;\n        }\n\n        Customer customer = null;\n        try\n        {\n            customer = await _stripeAdapter.GetCustomerAsync(gatewayCustomerId, options);\n        }\n        catch (StripeException)\n        {\n        }\n\n        return customer;\n    }\n\n    private async Task<IEnumerable<BillingHistoryInfo.BillingTransaction>> GetBillingTransactionsAsync(\n        ISubscriber subscriber, int? limit = null)\n    {\n        var transactions = subscriber switch\n        {\n            User => await _transactionRepository.GetManyByUserIdAsync(subscriber.Id, limit),\n            Organization => await _transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id, limit),\n            _ => null\n        };\n\n        return transactions?.OrderByDescending(i => i.CreationDate)\n            .Select(t => new BillingHistoryInfo.BillingTransaction(t));\n    }\n\n    private async Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetBillingInvoicesAsync(Customer customer,\n        int? limit = null)\n    {\n        if (customer == null)\n        {\n            return null;\n        }\n\n        try\n        {\n            var paidInvoicesTask = _stripeAdapter.ListInvoicesAsync(new StripeInvoiceListOptions\n            {\n                Customer = customer.Id,\n                SelectAll = !limit.HasValue,\n                Limit = limit,\n                Status = \"paid\"\n            });\n            var openInvoicesTask = _stripeAdapter.ListInvoicesAsync(new StripeInvoiceListOptions\n            {\n                Customer = customer.Id,\n                SelectAll = !limit.HasValue,\n                Limit = limit,\n                Status = \"open\"\n            });\n            var uncollectibleInvoicesTask = _stripeAdapter.ListInvoicesAsync(new StripeInvoiceListOptions\n            {\n                Customer = customer.Id,\n                SelectAll = !limit.HasValue,\n                Limit = limit,\n                Status = \"uncollectible\"\n            });\n\n            var paidInvoices = await paidInvoicesTask;\n            var openInvoices = await openInvoicesTask;\n            var uncollectibleInvoices = await uncollectibleInvoicesTask;\n\n            var invoices = paidInvoices\n                .Concat(openInvoices)\n                .Concat(uncollectibleInvoices);\n\n            var result = invoices\n                .OrderByDescending(invoice => invoice.Created)\n                .Select(invoice => new BillingHistoryInfo.BillingInvoice(invoice));\n\n            return limit.HasValue\n                ? result.Take(limit.Value)\n                : result;\n        }\n        catch (StripeException exception)\n        {\n            _logger.LogError(exception, \"An error occurred while listing Stripe invoices\");\n            throw new GatewayException(\"Failed to retrieve current invoices\", exception);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/Implementations/StripeSyncService.cs",
    "content": "﻿using Bit.Core.Exceptions;\n\nnamespace Bit.Core.Billing.Services.Implementations;\n\npublic class StripeSyncService : IStripeSyncService\n{\n    private readonly IStripeAdapter _stripeAdapter;\n\n    public StripeSyncService(IStripeAdapter stripeAdapter)\n    {\n        _stripeAdapter = stripeAdapter;\n    }\n\n    public async Task UpdateCustomerEmailAddressAsync(string gatewayCustomerId, string emailAddress)\n    {\n        if (string.IsNullOrWhiteSpace(gatewayCustomerId))\n        {\n            throw new InvalidGatewayCustomerIdException();\n        }\n\n        if (string.IsNullOrWhiteSpace(emailAddress))\n        {\n            throw new InvalidEmailException();\n        }\n\n        var customer = await _stripeAdapter.GetCustomerAsync(gatewayCustomerId);\n\n        await _stripeAdapter.UpdateCustomerAsync(customer.Id,\n            new Stripe.CustomerUpdateOptions { Email = emailAddress });\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/Implementations/SubscriberService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Tax.Models;\nusing Bit.Core.Billing.Tax.Services;\nusing Bit.Core.Billing.Tax.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Braintree;\nusing Microsoft.Extensions.Logging;\nusing Stripe;\nusing static Bit.Core.Billing.Utilities;\nusing Customer = Stripe.Customer;\nusing Subscription = Stripe.Subscription;\n\nnamespace Bit.Core.Billing.Services.Implementations;\n\nusing static StripeConstants;\n\npublic class SubscriberService(\n    IBraintreeGateway braintreeGateway,\n    IGlobalSettings globalSettings,\n    ILogger<SubscriberService> logger,\n    IOrganizationRepository organizationRepository,\n    IProviderRepository providerRepository,\n    IStripeAdapter stripeAdapter,\n    ITaxService taxService,\n    IUserRepository userRepository) : ISubscriberService\n{\n    public async Task CancelSubscription(\n        ISubscriber subscriber,\n        OffboardingSurveyResponse offboardingSurveyResponse,\n        bool cancelImmediately)\n    {\n        var subscription = await GetSubscriptionOrThrow(subscriber);\n\n        if (subscription.CanceledAt.HasValue ||\n            subscription.Status == \"canceled\" ||\n            subscription.Status == \"unpaid\" ||\n            subscription.Status == \"incomplete_expired\")\n        {\n            logger.LogWarning(\"Cannot cancel subscription ({ID}) that's already inactive\", subscription.Id);\n\n            throw new BillingException();\n        }\n\n        var metadata = new Dictionary<string, string>\n        {\n            { \"cancellingUserId\", offboardingSurveyResponse.UserId.ToString() }\n        };\n\n        List<string> validCancellationReasons = [\n            \"customer_service\",\n            \"low_quality\",\n            \"missing_features\",\n            \"other\",\n            \"switched_service\",\n            \"too_complex\",\n            \"too_expensive\",\n            \"unused\"\n        ];\n\n        if (cancelImmediately)\n        {\n            if (subscription.Metadata != null && subscription.Metadata.ContainsKey(\"organizationId\"))\n            {\n                await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, new SubscriptionUpdateOptions\n                {\n                    Metadata = metadata\n                });\n            }\n\n            var options = new SubscriptionCancelOptions\n            {\n                CancellationDetails = new SubscriptionCancellationDetailsOptions\n                {\n                    Comment = offboardingSurveyResponse.Feedback\n                }\n            };\n\n            if (validCancellationReasons.Contains(offboardingSurveyResponse.Reason))\n            {\n                options.CancellationDetails.Feedback = offboardingSurveyResponse.Reason;\n            }\n\n            await stripeAdapter.CancelSubscriptionAsync(subscription.Id, options);\n        }\n        else\n        {\n            var options = new SubscriptionUpdateOptions\n            {\n                CancelAtPeriodEnd = true,\n                CancellationDetails = new SubscriptionCancellationDetailsOptions\n                {\n                    Comment = offboardingSurveyResponse.Feedback\n                },\n                Metadata = metadata\n            };\n\n            if (validCancellationReasons.Contains(offboardingSurveyResponse.Reason))\n            {\n                options.CancellationDetails.Feedback = offboardingSurveyResponse.Reason;\n            }\n\n            await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, options);\n        }\n    }\n\n    public async Task<string> CreateBraintreeCustomer(\n        ISubscriber subscriber,\n        string paymentMethodNonce)\n    {\n        var braintreeCustomerId =\n            subscriber.BraintreeCustomerIdPrefix() +\n            subscriber.Id.ToString(\"N\").ToLower() +\n            CoreHelpers.RandomString(3, upper: false, numeric: false);\n\n        var customerResult = await braintreeGateway.Customer.CreateAsync(new CustomerRequest\n        {\n            Id = braintreeCustomerId,\n            CustomFields = new Dictionary<string, string>\n            {\n                [subscriber.BraintreeIdField()] = subscriber.Id.ToString(),\n                [subscriber.BraintreeCloudRegionField()] = globalSettings.BaseServiceUri.CloudRegion\n            },\n            Email = subscriber.BillingEmailAddress(),\n            PaymentMethodNonce = paymentMethodNonce\n        });\n\n        if (customerResult.IsSuccess())\n        {\n            return customerResult.Target.Id;\n        }\n\n        logger.LogError(\"Failed to create Braintree customer for subscriber ({ID})\", subscriber.Id);\n\n        throw new BillingException();\n    }\n\n#nullable enable\n    public async Task<Customer> CreateStripeCustomer(ISubscriber subscriber)\n    {\n        if (!string.IsNullOrEmpty(subscriber.GatewayCustomerId))\n        {\n            throw new ConflictException(\"Subscriber already has a linked Stripe Customer\");\n        }\n\n        var options = subscriber switch\n        {\n            Organization organization => new CustomerCreateOptions\n            {\n                Description = organization.DisplayBusinessName(),\n                Email = organization.BillingEmail,\n                InvoiceSettings = new CustomerInvoiceSettingsOptions\n                {\n                    CustomFields =\n                    [\n                        new CustomerInvoiceSettingsCustomFieldOptions\n                        {\n                            Name = organization.SubscriberType(),\n                            Value = Max30Characters(organization.DisplayName())\n                        }\n                    ]\n                },\n                Metadata = new Dictionary<string, string>\n                {\n                    [MetadataKeys.OrganizationId] = organization.Id.ToString(),\n                    [MetadataKeys.Region] = globalSettings.BaseServiceUri.CloudRegion\n                }\n            },\n            Provider provider => new CustomerCreateOptions\n            {\n                Description = provider.DisplayBusinessName(),\n                Email = provider.BillingEmail,\n                InvoiceSettings = new CustomerInvoiceSettingsOptions\n                {\n                    CustomFields =\n                    [\n                        new CustomerInvoiceSettingsCustomFieldOptions\n                        {\n                            Name = provider.SubscriberType(),\n                            Value = Max30Characters(provider.DisplayName())\n                        }\n                    ]\n                },\n                Metadata = new Dictionary<string, string>\n                {\n                    [MetadataKeys.ProviderId] = provider.Id.ToString(),\n                    [MetadataKeys.Region] = globalSettings.BaseServiceUri.CloudRegion\n                }\n            },\n            User user => new CustomerCreateOptions\n            {\n                Description = user.Name,\n                Email = user.Email,\n                InvoiceSettings = new CustomerInvoiceSettingsOptions\n                {\n                    CustomFields =\n                    [\n                        new CustomerInvoiceSettingsCustomFieldOptions\n                        {\n                            Name = user.SubscriberType(),\n                            Value = Max30Characters(user.SubscriberName())\n                        }\n                    ]\n                },\n                Metadata = new Dictionary<string, string>\n                {\n                    [MetadataKeys.Region] = globalSettings.BaseServiceUri.CloudRegion,\n                    [MetadataKeys.UserId] = user.Id.ToString()\n                }\n            },\n            _ => throw new ArgumentOutOfRangeException(nameof(subscriber))\n        };\n\n        var customer = await stripeAdapter.CreateCustomerAsync(options);\n\n        switch (subscriber)\n        {\n            case Organization organization:\n                organization.Gateway = GatewayType.Stripe;\n                organization.GatewayCustomerId = customer.Id;\n                await organizationRepository.ReplaceAsync(organization);\n                break;\n            case Provider provider:\n                provider.Gateway = GatewayType.Stripe;\n                provider.GatewayCustomerId = customer.Id;\n                await providerRepository.ReplaceAsync(provider);\n                break;\n            case User user:\n                user.Gateway = GatewayType.Stripe;\n                user.GatewayCustomerId = customer.Id;\n                await userRepository.ReplaceAsync(user);\n                break;\n        }\n\n        return customer;\n\n        string? Max30Characters(string? input)\n            => input?.Length <= 30 ? input : input?[..30];\n    }\n#nullable disable\n\n    public async Task<Customer> GetCustomer(\n        ISubscriber subscriber,\n        CustomerGetOptions customerGetOptions = null)\n    {\n        ArgumentNullException.ThrowIfNull(subscriber);\n\n        if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))\n        {\n            logger.LogError(\"Cannot retrieve customer for subscriber ({SubscriberID}) with no {FieldName}\", subscriber.Id, nameof(subscriber.GatewayCustomerId));\n\n            return null;\n        }\n\n        try\n        {\n            var customer = await stripeAdapter.GetCustomerAsync(subscriber.GatewayCustomerId, customerGetOptions);\n\n            if (customer != null)\n            {\n                return customer;\n            }\n\n            logger.LogError(\"Could not find Stripe customer ({CustomerID}) for subscriber ({SubscriberID})\",\n                subscriber.GatewayCustomerId, subscriber.Id);\n\n            return null;\n        }\n        catch (StripeException exception)\n        {\n            logger.LogError(\"An error occurred while trying to retrieve Stripe customer ({CustomerID}) for subscriber ({SubscriberID}): {Error}\",\n                subscriber.GatewayCustomerId, subscriber.Id, exception.Message);\n\n            return null;\n        }\n    }\n\n    public async Task<Customer> GetCustomerOrThrow(\n        ISubscriber subscriber,\n        CustomerGetOptions customerGetOptions = null)\n    {\n        ArgumentNullException.ThrowIfNull(subscriber);\n\n        if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))\n        {\n            logger.LogError(\"Cannot retrieve customer for subscriber ({SubscriberID}) with no {FieldName}\", subscriber.Id, nameof(subscriber.GatewayCustomerId));\n\n            throw new BillingException();\n        }\n\n        try\n        {\n            var customer = await stripeAdapter.GetCustomerAsync(subscriber.GatewayCustomerId, customerGetOptions);\n\n            if (customer != null)\n            {\n                return customer;\n            }\n\n            logger.LogError(\"Could not find Stripe customer ({CustomerID}) for subscriber ({SubscriberID})\",\n                subscriber.GatewayCustomerId, subscriber.Id);\n\n            throw new BillingException();\n        }\n        catch (StripeException stripeException)\n        {\n            logger.LogError(\"An error occurred while trying to retrieve Stripe customer ({CustomerID}) for subscriber ({SubscriberID}): {Error}\",\n                subscriber.GatewayCustomerId, subscriber.Id, stripeException.Message);\n\n            throw new BillingException(\n                message: \"An error occurred while trying to retrieve a Stripe customer\",\n                innerException: stripeException);\n        }\n    }\n\n    public async Task<PaymentSource> GetPaymentSource(\n        ISubscriber subscriber)\n    {\n        ArgumentNullException.ThrowIfNull(subscriber);\n\n        var customer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions\n        {\n            Expand = [\"default_source\", \"invoice_settings.default_payment_method\"]\n        });\n\n        return await GetPaymentSourceAsync(customer);\n    }\n\n    public async Task<Subscription> GetSubscription(\n        ISubscriber subscriber,\n        SubscriptionGetOptions subscriptionGetOptions = null)\n    {\n        ArgumentNullException.ThrowIfNull(subscriber);\n\n        if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))\n        {\n            logger.LogError(\"Cannot retrieve subscription for subscriber ({SubscriberID}) with no {FieldName}\", subscriber.Id, nameof(subscriber.GatewaySubscriptionId));\n\n            return null;\n        }\n\n        try\n        {\n            var subscription = await stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions);\n\n            if (subscription != null)\n            {\n                return subscription;\n            }\n\n            logger.LogError(\"Could not find Stripe subscription ({SubscriptionID}) for subscriber ({SubscriberID})\",\n                subscriber.GatewaySubscriptionId, subscriber.Id);\n\n            return null;\n        }\n        catch (StripeException exception)\n        {\n            logger.LogError(\"An error occurred while trying to retrieve Stripe subscription ({SubscriptionID}) for subscriber ({SubscriberID}): {Error}\",\n                subscriber.GatewaySubscriptionId, subscriber.Id, exception.Message);\n\n            return null;\n        }\n    }\n\n    public async Task<Subscription> GetSubscriptionOrThrow(\n        ISubscriber subscriber,\n        SubscriptionGetOptions subscriptionGetOptions = null)\n    {\n        ArgumentNullException.ThrowIfNull(subscriber);\n\n        if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))\n        {\n            logger.LogError(\"Cannot retrieve subscription for subscriber ({SubscriberID}) with no {FieldName}\", subscriber.Id, nameof(subscriber.GatewaySubscriptionId));\n\n            throw new BillingException();\n        }\n\n        try\n        {\n            var subscription = await stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions);\n\n            if (subscription != null)\n            {\n                return subscription;\n            }\n\n            logger.LogError(\"Could not find Stripe subscription ({SubscriptionID}) for subscriber ({SubscriberID})\",\n                subscriber.GatewaySubscriptionId, subscriber.Id);\n\n            throw new BillingException();\n        }\n        catch (StripeException stripeException)\n        {\n            logger.LogError(\"An error occurred while trying to retrieve Stripe subscription ({SubscriptionID}) for subscriber ({SubscriberID}): {Error}\",\n                subscriber.GatewaySubscriptionId, subscriber.Id, stripeException.Message);\n\n            throw new BillingException(\n                message: \"An error occurred while trying to retrieve a Stripe subscription\",\n                innerException: stripeException);\n        }\n    }\n\n    public async Task RemovePaymentSource(\n        ISubscriber subscriber)\n    {\n        ArgumentNullException.ThrowIfNull(subscriber);\n\n        if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))\n        {\n            throw new BillingException();\n        }\n\n        var stripeCustomer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions\n        {\n            Expand = [\"invoice_settings.default_payment_method\", \"sources\"]\n        });\n\n        if (stripeCustomer.Metadata?.TryGetValue(BraintreeCustomerIdKey, out var braintreeCustomerId) ?? false)\n        {\n            var braintreeCustomer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId);\n\n            if (braintreeCustomer == null)\n            {\n                logger.LogError(\"Failed to retrieve Braintree customer ({ID}) when removing payment method\", braintreeCustomerId);\n\n                throw new BillingException();\n            }\n\n            if (braintreeCustomer.DefaultPaymentMethod != null)\n            {\n                var existingDefaultPaymentMethod = braintreeCustomer.DefaultPaymentMethod;\n\n                var updateCustomerResult = await braintreeGateway.Customer.UpdateAsync(\n                    braintreeCustomerId,\n                    new CustomerRequest { DefaultPaymentMethodToken = null });\n\n                if (!updateCustomerResult.IsSuccess())\n                {\n                    logger.LogError(\"Failed to update payment method for Braintree customer ({ID}) | Message: {Message}\",\n                        braintreeCustomerId, updateCustomerResult.Message);\n\n                    throw new BillingException();\n                }\n\n                var deletePaymentMethodResult = await braintreeGateway.PaymentMethod.DeleteAsync(existingDefaultPaymentMethod.Token);\n\n                if (!deletePaymentMethodResult.IsSuccess())\n                {\n                    await braintreeGateway.Customer.UpdateAsync(\n                        braintreeCustomerId,\n                        new CustomerRequest { DefaultPaymentMethodToken = existingDefaultPaymentMethod.Token });\n\n                    logger.LogError(\n                        \"Failed to delete Braintree payment method for Customer ({ID}), re-linked payment method. Message: {Message}\",\n                        braintreeCustomerId, deletePaymentMethodResult.Message);\n\n                    throw new BillingException();\n                }\n            }\n            else\n            {\n                logger.LogWarning(\"Tried to remove non-existent Braintree payment method for Customer ({ID})\", braintreeCustomerId);\n            }\n        }\n        else\n        {\n            if (stripeCustomer.Sources != null && stripeCustomer.Sources.Any())\n            {\n                foreach (var source in stripeCustomer.Sources)\n                {\n                    switch (source)\n                    {\n                        case BankAccount:\n                            await stripeAdapter.DeleteBankAccountAsync(stripeCustomer.Id, source.Id);\n                            break;\n                        case Card:\n                            await stripeAdapter.DeleteCardAsync(stripeCustomer.Id, source.Id);\n                            break;\n                    }\n                }\n            }\n\n            var paymentMethods = stripeAdapter.ListPaymentMethodsAutoPagingAsync(new PaymentMethodListOptions\n            {\n                Customer = stripeCustomer.Id\n            });\n\n            await foreach (var paymentMethod in paymentMethods)\n            {\n                await stripeAdapter.DetachPaymentMethodAsync(paymentMethod.Id);\n            }\n        }\n    }\n\n    public async Task UpdateTaxInformation(\n        ISubscriber subscriber,\n        TaxInformation taxInformation)\n    {\n        ArgumentNullException.ThrowIfNull(subscriber);\n        ArgumentNullException.ThrowIfNull(taxInformation);\n\n        var customer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions\n        {\n            Expand = [\"subscriptions\", \"tax\", \"tax_ids\"]\n        });\n\n        customer = await stripeAdapter.UpdateCustomerAsync(customer.Id, new CustomerUpdateOptions\n        {\n            Address = new AddressOptions\n            {\n                Country = taxInformation.Country,\n                PostalCode = taxInformation.PostalCode,\n                Line1 = taxInformation.Line1 ?? string.Empty,\n                Line2 = taxInformation.Line2,\n                City = taxInformation.City,\n                State = taxInformation.State\n            },\n            Expand = [\"subscriptions\", \"tax\", \"tax_ids\"]\n        });\n\n        var taxId = customer.TaxIds?.FirstOrDefault();\n\n        if (taxId != null)\n        {\n            await stripeAdapter.DeleteTaxIdAsync(customer.Id, taxId.Id);\n        }\n\n        if (!string.IsNullOrWhiteSpace(taxInformation.TaxId))\n        {\n            var taxIdType = taxInformation.TaxIdType;\n            if (string.IsNullOrWhiteSpace(taxIdType))\n            {\n                taxIdType = taxService.GetStripeTaxCode(taxInformation.Country,\n                    taxInformation.TaxId);\n\n                if (taxIdType == null)\n                {\n                    logger.LogWarning(\"Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.\",\n                        taxInformation.Country,\n                        taxInformation.TaxId);\n\n                    throw new BadRequestException(\"billingTaxIdTypeInferenceError\");\n                }\n            }\n\n            try\n            {\n                await stripeAdapter.CreateTaxIdAsync(customer.Id,\n                    new TaxIdCreateOptions { Type = taxIdType, Value = taxInformation.TaxId });\n\n                if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)\n                {\n                    await stripeAdapter.CreateTaxIdAsync(customer.Id,\n                        new TaxIdCreateOptions { Type = StripeConstants.TaxIdType.EUVAT, Value = $\"ES{taxInformation.TaxId}\" });\n                }\n            }\n            catch (StripeException e)\n            {\n                switch (e.StripeError.Code)\n                {\n                    case StripeConstants.ErrorCodes.TaxIdInvalid:\n                        logger.LogWarning(\"Invalid tax ID '{TaxID}' for country '{Country}'.\",\n                            taxInformation.TaxId,\n                            taxInformation.Country);\n\n                        throw new BadRequestException(\"billingInvalidTaxIdError\");\n\n                    default:\n                        logger.LogError(e,\n                            \"Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.\",\n                            taxInformation.TaxId,\n                            taxInformation.Country,\n                            customer.Id);\n\n                        throw new BadRequestException(\"billingTaxIdCreationError\");\n                }\n            }\n        }\n\n        var subscription =\n            customer.Subscriptions.First(subscription => subscription.Id == subscriber.GatewaySubscriptionId);\n\n        var isBusinessUseSubscriber = subscriber switch\n        {\n            Organization organization => organization.PlanType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families,\n            Provider => true,\n            _ => false\n        };\n\n        if (isBusinessUseSubscriber)\n        {\n            var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt);\n            switch (customer)\n            {\n                case { Address.Country: not null and not \"\", TaxExempt: var customerTaxExemptStatus }\n                    when determinedTaxExemptStatus != customerTaxExemptStatus:\n                    await stripeAdapter.UpdateCustomerAsync(customer.Id,\n                        new CustomerUpdateOptions { TaxExempt = determinedTaxExemptStatus });\n                    break;\n            }\n\n            if (!subscription.AutomaticTax.Enabled)\n            {\n                await stripeAdapter.UpdateSubscriptionAsync(subscription.Id,\n                    new SubscriptionUpdateOptions\n                    {\n                        AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }\n                    });\n            }\n        }\n        else\n        {\n            var automaticTaxShouldBeEnabled = subscriber switch\n            {\n                User => true,\n                Organization organization => organization.PlanType.GetProductTier() == ProductTierType.Families ||\n                                             TaxHelpers.IsDirectTaxCountry(customer.Address?.Country) || (customer.TaxIds?.Any() ?? false),\n                Provider provider => TaxHelpers.IsDirectTaxCountry(customer.Address?.Country) || (customer.TaxIds?.Any() ?? false),\n                _ => false\n            };\n\n            if (automaticTaxShouldBeEnabled && !subscription.AutomaticTax.Enabled)\n            {\n                await stripeAdapter.UpdateSubscriptionAsync(subscription.Id,\n                    new SubscriptionUpdateOptions\n                    {\n                        AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }\n                    });\n            }\n        }\n    }\n\n    public async Task<bool> IsValidGatewayCustomerIdAsync(ISubscriber subscriber)\n    {\n        ArgumentNullException.ThrowIfNull(subscriber);\n        if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))\n        {\n            // subscribers are allowed to have no customer id as a business rule\n            return true;\n        }\n        try\n        {\n            await stripeAdapter.GetCustomerAsync(subscriber.GatewayCustomerId);\n            return true;\n        }\n        catch (StripeException e) when (e.StripeError.Code == \"resource_missing\")\n        {\n            return false;\n        }\n    }\n\n    public async Task<bool> IsValidGatewaySubscriptionIdAsync(ISubscriber subscriber)\n    {\n        ArgumentNullException.ThrowIfNull(subscriber);\n        if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))\n        {\n            // subscribers are allowed to have no subscription id as a business rule\n            return true;\n        }\n        try\n        {\n            await stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId);\n            return true;\n        }\n        catch (StripeException e) when (e.StripeError.Code == \"resource_missing\")\n        {\n            return false;\n        }\n    }\n\n    #region Shared Utilities\n\n    private async Task<PaymentSource> GetPaymentSourceAsync(Customer customer)\n    {\n        if (customer.Metadata != null)\n        {\n            var hasBraintreeCustomerId = customer.Metadata.TryGetValue(BraintreeCustomerIdKey, out var braintreeCustomerId);\n\n            if (hasBraintreeCustomerId)\n            {\n                var braintreeCustomer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId);\n\n                return PaymentSource.From(braintreeCustomer);\n            }\n        }\n\n        var attachedPaymentMethodDTO = PaymentSource.From(customer);\n\n        if (attachedPaymentMethodDTO != null)\n        {\n            return attachedPaymentMethodDTO;\n        }\n\n        /*\n         * attachedPaymentMethodDTO being null represents a case where we could be looking for the SetupIntent for an unverified \"us_bank_account\".\n         * Query Stripe for SetupIntents associated with this customer.\n         */\n        var setupIntents = await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions\n        {\n            Customer = customer.Id,\n            Expand = [\"data.payment_method\"]\n        });\n\n        var unverifiedBankAccount = setupIntents?.FirstOrDefault(si => si.IsUnverifiedBankAccount());\n\n        return unverifiedBankAccount != null ? PaymentSource.From(unverifiedBankAccount) : null;\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/Implementations/SubscriptionDiscountService.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Services.DiscountAudienceFilters;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Billing.Subscriptions.Repositories;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Services.Implementations;\n\n/// <inheritdoc />\npublic class SubscriptionDiscountService(\n    ISubscriptionDiscountRepository subscriptionDiscountRepository,\n    IDiscountAudienceFilterFactory discountAudienceFilterFactory) : ISubscriptionDiscountService\n{\n    /// <inheritdoc />\n    public async Task<IEnumerable<DiscountEligibility>> GetEligibleDiscountsAsync(User user)\n    {\n        var activeDiscounts = await subscriptionDiscountRepository.GetActiveDiscountsAsync();\n        var eligibleDiscounts = new List<DiscountEligibility>();\n\n        foreach (var discount in activeDiscounts)\n        {\n            var tierEligibility = await GetTierEligibilityAsync(user, discount);\n            // If tierEligibility is null, it means no filter is configured for the discount's audience type,\n            // so we skip it since we can't determine eligibility. If it's not null, we check\n            // if the user is eligible for at least one tier before adding it to the results.\n            if (tierEligibility is not null && tierEligibility.Values.Any(isEligible => isEligible))\n            {\n                eligibleDiscounts.Add(new DiscountEligibility(discount, tierEligibility));\n            }\n        }\n\n        return eligibleDiscounts;\n    }\n\n    /// <inheritdoc />\n    public async Task<bool> ValidateDiscountEligibilityForUserAsync(User user, IReadOnlyList<string> couponIds, DiscountTierType tierType)\n    {\n        var eligibleDiscounts = await GetEligibleDiscountsAsync(user);\n        var eligibilityByStripeCouponId = eligibleDiscounts.ToDictionary(d => d.Discount.StripeCouponId);\n        return couponIds.All(id =>\n            eligibilityByStripeCouponId.TryGetValue(id, out var eligibility) &&\n            eligibility.TierEligibility[tierType]);\n    }\n\n    /// <summary>\n    /// Returns the per-tier eligibility matrix for the given <paramref name=\"user\"/> and <paramref name=\"discount\"/>,\n    /// or <see langword=\"null\"/> if no filter is configured for the discount's audience type.\n    /// </summary>\n    private async Task<IDictionary<DiscountTierType, bool>?> GetTierEligibilityAsync(\n        User user, SubscriptionDiscount discount)\n    {\n        var filter = discountAudienceFilterFactory.GetFilter(discount.AudienceType);\n        return filter is not null ? await filter.IsUserEligible(user, discount) : null;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs",
    "content": "﻿#nullable enable\n\nusing System.Security.Claims;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Bit.Core.Billing.Services;\n\npublic class NoopLicensingService : ILicensingService\n{\n    public NoopLicensingService(\n        IWebHostEnvironment environment,\n        GlobalSettings globalSettings)\n    {\n        if (!environment.IsDevelopment() && globalSettings.SelfHosted)\n        {\n            throw new Exception($\"{nameof(NoopLicensingService)} cannot be used for self hosted instances.\");\n        }\n    }\n\n    public Task ValidateOrganizationsAsync()\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task ValidateUsersAsync()\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<bool> ValidateUserPremiumAsync(User user)\n    {\n        return Task.FromResult(user.Premium);\n    }\n\n    public bool VerifyLicense(ILicense license)\n    {\n        return true;\n    }\n\n    public byte[] SignLicense(ILicense license)\n    {\n        return new byte[0];\n    }\n\n    public Task<OrganizationLicense?> ReadOrganizationLicenseAsync(Organization organization)\n    {\n        return Task.FromResult<OrganizationLicense?>(null);\n    }\n\n    public Task<OrganizationLicense?> ReadOrganizationLicenseAsync(Guid organizationId)\n    {\n        return Task.FromResult<OrganizationLicense?>(null);\n    }\n\n    public ClaimsPrincipal? GetClaimsPrincipalFromLicense(ILicense license)\n    {\n        return null;\n    }\n\n    public Task<string?> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)\n    {\n        return Task.FromResult<string?>(null);\n    }\n\n    public Task<string?> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo)\n    {\n        return Task.FromResult<string?>(null);\n    }\n\n    public Task WriteUserLicenseAsync(User user, UserLicense license)\n    {\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Commands/ReinstateSubscriptionCommand.cs",
    "content": "﻿using Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Subscriptions.Commands;\n\nusing static StripeConstants;\n\npublic interface IReinstateSubscriptionCommand\n{\n    Task<BillingCommandResult<None>> Run(ISubscriber subscriber);\n}\n\npublic class ReinstateSubscriptionCommand(\n    ILogger<ReinstateSubscriptionCommand> logger,\n    IStripeAdapter stripeAdapter) : BaseBillingCommand<ReinstateSubscriptionCommand>(logger), IReinstateSubscriptionCommand\n{\n    public Task<BillingCommandResult<None>> Run(ISubscriber subscriber) => HandleAsync<None>(async () =>\n    {\n        var subscription = await stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId);\n\n        if (subscription is not\n            {\n                Status: SubscriptionStatus.Trialing or SubscriptionStatus.Active,\n                CancelAt: not null\n            })\n        {\n            return new BadRequest(\"Subscription is not pending cancellation.\");\n        }\n\n        await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, new SubscriptionUpdateOptions\n        {\n            CancelAtPeriodEnd = false\n        });\n\n        return new None();\n    });\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.Logging;\nusing OneOf.Types;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Subscriptions.Commands;\n\nusing static StripeConstants;\n\npublic interface IRestartSubscriptionCommand\n{\n    Task<BillingCommandResult<None>> Run(\n        ISubscriber subscriber);\n}\n\npublic class RestartSubscriptionCommand(\n    ILogger<RestartSubscriptionCommand> logger,\n    IOrganizationRepository organizationRepository,\n    IPricingClient pricingClient,\n    IStripeAdapter stripeAdapter,\n    ISubscriberService subscriberService) : BaseBillingCommand<RestartSubscriptionCommand>(logger), IRestartSubscriptionCommand\n{\n    public Task<BillingCommandResult<None>> Run(\n        ISubscriber subscriber) => HandleAsync<None>(async () =>\n    {\n        var existingSubscription = await subscriberService.GetSubscription(subscriber);\n\n        if (existingSubscription is not { Status: SubscriptionStatus.Canceled })\n        {\n            return new BadRequest(\"Cannot restart a subscription that is not canceled.\");\n        }\n\n        await RestartSubscriptionAsync(subscriber, existingSubscription);\n\n        return new None();\n    });\n\n    private Task RestartSubscriptionAsync(\n        ISubscriber subscriber,\n        Subscription canceledSubscription) => subscriber switch\n        {\n            Organization organization => RestartOrganizationSubscriptionAsync(organization, canceledSubscription),\n            _ => throw new NotSupportedException(\"Only organization subscriptions can be restarted\")\n        };\n\n    private async Task RestartOrganizationSubscriptionAsync(\n        Organization organization,\n        Subscription canceledSubscription)\n    {\n        var plans = await pricingClient.ListPlans();\n\n        var oldPlan = plans.FirstOrDefault(plan => plan.Type == organization.PlanType);\n\n        if (oldPlan == null)\n        {\n            throw new ConflictException(\"Could not find plan for organization's plan type\");\n        }\n\n        var newPlan = oldPlan.Disabled\n            ? plans.FirstOrDefault(plan =>\n                plan.ProductTier == oldPlan.ProductTier &&\n                plan.IsAnnual == oldPlan.IsAnnual &&\n                !plan.Disabled)\n            : oldPlan;\n\n        if (newPlan == null)\n        {\n            throw new ConflictException(\"Could not find the current, enabled plan for organization's tier and cadence\");\n        }\n\n        if (newPlan.Type != oldPlan.Type)\n        {\n            organization.PlanType = newPlan.Type;\n            organization.Plan = newPlan.Name;\n            organization.SelfHost = newPlan.HasSelfHost;\n            organization.UsePolicies = newPlan.HasPolicies;\n            organization.UseMyItems = newPlan.HasMyItems;\n            organization.UseGroups = newPlan.HasGroups;\n            organization.UseDirectory = newPlan.HasDirectory;\n            organization.UseEvents = newPlan.HasEvents;\n            organization.UseTotp = newPlan.HasTotp;\n            organization.Use2fa = newPlan.Has2fa;\n            organization.UseApi = newPlan.HasApi;\n            organization.UseSso = newPlan.HasSso;\n            organization.UseOrganizationDomains = newPlan.HasOrganizationDomains;\n            organization.UseKeyConnector = newPlan.HasKeyConnector;\n            organization.UseScim = newPlan.HasScim;\n            organization.UseResetPassword = newPlan.HasResetPassword;\n            organization.UsersGetPremium = newPlan.UsersGetPremium;\n            organization.UseCustomPermissions = newPlan.HasCustomPermissions;\n        }\n\n        var items = new List<SubscriptionItemOptions>();\n\n        // Password Manager\n        var passwordManagerItem = canceledSubscription.Items.FirstOrDefault(item =>\n            item.Price.Id == (oldPlan.HasNonSeatBasedPasswordManagerPlan()\n                ? oldPlan.PasswordManager.StripePlanId\n                : oldPlan.PasswordManager.StripeSeatPlanId));\n\n        if (passwordManagerItem == null)\n        {\n            throw new ConflictException(\"Organization's subscription does not have a Password Manager subscription item.\");\n        }\n\n        items.Add(new SubscriptionItemOptions\n        {\n            Price = newPlan.HasNonSeatBasedPasswordManagerPlan() ? newPlan.PasswordManager.StripePlanId : newPlan.PasswordManager.StripeSeatPlanId,\n            Quantity = passwordManagerItem.Quantity\n        });\n\n        // Storage\n        var storageItem = canceledSubscription.Items.FirstOrDefault(\n            item => item.Price.Id == oldPlan.PasswordManager.StripeStoragePlanId);\n\n        if (storageItem != null)\n        {\n            items.Add(new SubscriptionItemOptions\n            {\n                Price = newPlan.PasswordManager.StripeStoragePlanId,\n                Quantity = storageItem.Quantity\n            });\n        }\n\n        // Secrets Manager & Service Accounts\n        var secretsManagerItem = oldPlan.SecretsManager != null\n            ? canceledSubscription.Items.FirstOrDefault(item =>\n                item.Price.Id == oldPlan.SecretsManager.StripeSeatPlanId)\n            : null;\n\n        var serviceAccountsItem = oldPlan.SecretsManager != null\n            ? canceledSubscription.Items.FirstOrDefault(item =>\n                item.Price.Id == oldPlan.SecretsManager.StripeServiceAccountPlanId)\n            : null;\n\n        if (newPlan.SecretsManager != null)\n        {\n            if (secretsManagerItem != null)\n            {\n                items.Add(new SubscriptionItemOptions\n                {\n                    Price = newPlan.SecretsManager.StripeSeatPlanId,\n                    Quantity = secretsManagerItem.Quantity\n                });\n            }\n\n            if (serviceAccountsItem != null)\n            {\n                items.Add(new SubscriptionItemOptions\n                {\n                    Price = newPlan.SecretsManager.StripeServiceAccountPlanId,\n                    Quantity = serviceAccountsItem.Quantity\n                });\n            }\n        }\n\n        var options = new SubscriptionCreateOptions\n        {\n            AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },\n            CollectionMethod = CollectionMethod.ChargeAutomatically,\n            Customer = canceledSubscription.CustomerId,\n            Items = items,\n            Metadata = canceledSubscription.Metadata,\n            OffSession = true,\n            TrialPeriodDays = 0\n        };\n\n        var subscription = await stripeAdapter.CreateSubscriptionAsync(options);\n\n        organization.GatewaySubscriptionId = subscription.Id;\n        organization.Enabled = true;\n        organization.ExpirationDate = subscription.GetCurrentPeriodEnd();\n        organization.RevisionDate = DateTime.UtcNow;\n\n        await organizationRepository.ReplaceAsync(organization);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Entities/SubscriptionDiscount.cs",
    "content": "﻿#nullable enable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Billing.Subscriptions.Entities;\n\npublic class SubscriptionDiscount : ITableObject<Guid>, IRevisable, IValidatableObject\n{\n    public Guid Id { get; set; }\n    [MaxLength(50)]\n    public string StripeCouponId { get; set; } = null!;\n    public ICollection<string>? StripeProductIds { get; set; }\n    public decimal? PercentOff { get; set; }\n    public long? AmountOff { get; set; }\n    [MaxLength(10)]\n    public string? Currency { get; set; }\n    [MaxLength(20)]\n    public string Duration { get; set; } = null!;\n    public int? DurationInMonths { get; set; }\n    [MaxLength(100)]\n    public string? Name { get; set; }\n    public DateTime StartDate { get; set; }\n    public DateTime EndDate { get; set; }\n    public DiscountAudienceType AudienceType { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        if (Id == default)\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (EndDate < StartDate)\n        {\n            yield return new ValidationResult(\n                \"EndDate must be greater than or equal to StartDate.\",\n                new[] { nameof(EndDate) });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Models/BitwardenDiscount.cs",
    "content": "﻿using System.Runtime.Serialization;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Utilities;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Subscriptions.Models;\n\n/// <summary>\n/// The type of discounts Bitwarden supports.\n/// </summary>\npublic enum BitwardenDiscountType\n{\n    [EnumMember(Value = \"amount-off\")]\n    AmountOff,\n\n    [EnumMember(Value = \"percent-off\")]\n    PercentOff\n}\n\n/// <summary>\n/// A record representing a discount applied to a Bitwarden subscription.\n/// </summary>\npublic record BitwardenDiscount\n{\n    /// <summary>\n    /// The type of the discount.\n    /// </summary>\n    [JsonConverter(typeof(EnumMemberJsonConverter<BitwardenDiscountType>))]\n    public required BitwardenDiscountType Type { get; init; }\n\n    /// <summary>\n    /// The value of the discount.\n    /// </summary>\n    public required decimal Value { get; init; }\n\n    public static implicit operator BitwardenDiscount(Discount? discount)\n    {\n        if (discount is not\n            {\n                Coupon.Valid: true\n            })\n        {\n            return null!;\n        }\n\n        return discount.Coupon switch\n        {\n            { AmountOff: > 0 } => new BitwardenDiscount\n            {\n                Type = BitwardenDiscountType.AmountOff,\n                Value = discount.Coupon.AmountOff.Value\n            },\n            { PercentOff: > 0 } => new BitwardenDiscount\n            {\n                Type = BitwardenDiscountType.PercentOff,\n                Value = discount.Coupon.PercentOff.Value\n            },\n            _ => null!\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Models/BitwardenSubscription.cs",
    "content": "﻿namespace Bit.Core.Billing.Subscriptions.Models;\n\npublic record BitwardenSubscription\n{\n    /// <summary>\n    /// The status of the subscription.\n    /// </summary>\n    public required string Status { get; init; }\n\n    /// <summary>\n    /// The subscription's cart, including line items, any discounts, and estimated tax.\n    /// </summary>\n    public required Cart Cart { get; init; }\n\n    /// <summary>\n    /// The amount of storage available and used for the subscription.\n    /// <remarks>Allowed Subscribers: User, Organization</remarks>\n    /// </summary>\n    public Storage? Storage { get; init; }\n\n    /// <summary>\n    /// If the subscription is pending cancellation, the date at which the\n    /// subscription will be canceled.\n    /// <remarks>Allowed Statuses: 'trialing', 'active'</remarks>\n    /// </summary>\n    public DateTime? CancelAt { get; init; }\n\n    /// <summary>\n    /// The date the subscription was canceled.\n    /// <remarks>Allowed Statuses: 'canceled'</remarks>\n    /// </summary>\n    public DateTime? Canceled { get; init; }\n\n    /// <summary>\n    /// The date of the next charge for the subscription.\n    /// <remarks>Allowed Statuses: 'trialing', 'active'</remarks>\n    /// </summary>\n    public DateTime? NextCharge { get; init; }\n\n    /// <summary>\n    /// The date the subscription will be or was suspended due to lack of payment.\n    /// <remarks>Allowed Statuses: 'incomplete', 'incomplete_expired', 'past_due', 'unpaid'</remarks>\n    /// </summary>\n    public DateTime? Suspension { get; init; }\n\n    /// <summary>\n    /// The number of days after the subscription goes 'past_due' the subscriber has to resolve their\n    /// open invoices before the subscription is suspended.\n    /// <remarks>Allowed Statuses: 'past_due'</remarks>\n    /// </summary>\n    public int? GracePeriod { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Models/Cart.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Billing.Subscriptions.Models;\n\npublic record CartItem\n{\n    /// <summary>\n    /// The client-side translation key for the name of the cart item.\n    /// </summary>\n    public required string TranslationKey { get; init; }\n\n    /// <summary>\n    /// The quantity of the cart item.\n    /// </summary>\n    public required long Quantity { get; init; }\n\n    /// <summary>\n    /// The unit-cost of the cart item.\n    /// </summary>\n    public required decimal Cost { get; init; }\n\n    /// <summary>\n    /// An optional discount applied specifically to this cart item.\n    /// </summary>\n    public BitwardenDiscount? Discount { get; init; }\n}\n\npublic record PasswordManagerCartItems\n{\n    /// <summary>\n    /// The Password Manager seats in the cart.\n    /// </summary>\n    public required CartItem Seats { get; init; }\n\n    /// <summary>\n    /// The additional storage in the cart.\n    /// </summary>\n    public CartItem? AdditionalStorage { get; init; }\n}\n\npublic record SecretsManagerCartItems\n{\n    /// <summary>\n    /// The Secrets Manager seats in the cart.\n    /// </summary>\n    public required CartItem Seats { get; init; }\n\n    /// <summary>\n    /// The additional service accounts in the cart.\n    /// </summary>\n    public CartItem? AdditionalServiceAccounts { get; init; }\n}\n\npublic record Cart\n{\n    /// <summary>\n    /// The Password Manager items in the cart.\n    /// </summary>\n    public required PasswordManagerCartItems PasswordManager { get; init; }\n\n    /// <summary>\n    /// The Secrets Manager items in the cart.\n    /// </summary>\n    public SecretsManagerCartItems? SecretsManager { get; init; }\n\n    /// <summary>\n    /// The cart's billing cadence.\n    /// </summary>\n    [JsonConverter(typeof(EnumMemberJsonConverter<PlanCadenceType>))]\n    public PlanCadenceType Cadence { get; init; }\n\n    /// <summary>\n    /// An optional discount applied to the entire cart.\n    /// </summary>\n    public BitwardenDiscount? Discount { get; init; }\n\n    /// <summary>\n    /// The estimated tax for the cart.\n    /// </summary>\n    public required decimal EstimatedTax { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Models/Storage.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\nusing OneOf;\n\nnamespace Bit.Core.Billing.Subscriptions.Models;\n\npublic record Storage\n{\n    private const double _bytesPerGibibyte = 1073741824D;\n\n    /// <summary>\n    /// The amount of storage the subscriber has available.\n    /// </summary>\n    public required short Available { get; init; }\n\n    /// <summary>\n    /// The amount of storage the subscriber has used.\n    /// </summary>\n    public required double Used { get; init; }\n\n    /// <summary>\n    /// The amount of storage the subscriber has used, formatted as a human-readable string.\n    /// </summary>\n    public required string ReadableUsed { get; init; }\n\n    public static implicit operator Storage(User user) => From(user);\n    public static implicit operator Storage(Organization organization) => From(organization);\n\n    private static Storage From(OneOf<User, Organization> subscriber)\n    {\n        var maxStorageGB = subscriber.Match(\n            user => user.MaxStorageGb,\n            organization => organization.MaxStorageGb);\n\n        if (maxStorageGB == null)\n        {\n            return null!;\n        }\n\n        var storage = subscriber.Match(\n            user => user.Storage,\n            organization => organization.Storage);\n\n        return new Storage\n        {\n            Available = maxStorageGB.Value,\n            Used = Math.Round((storage ?? 0) / _bytesPerGibibyte, 2),\n            ReadableUsed = CoreHelpers.ReadableBytesSize(storage ?? 0)\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Models/SubscriberId.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Exceptions;\nusing OneOf;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Subscriptions.Models;\n\nusing static StripeConstants;\n\npublic record UserId(Guid Value);\n\npublic record OrganizationId(Guid Value);\n\npublic record ProviderId(Guid Value);\n\npublic class SubscriberId : OneOfBase<UserId, OrganizationId, ProviderId>\n{\n    private SubscriberId(OneOf<UserId, OrganizationId, ProviderId> input) : base(input) { }\n\n    public static implicit operator SubscriberId(UserId value) => new(value);\n    public static implicit operator SubscriberId(OrganizationId value) => new(value);\n    public static implicit operator SubscriberId(ProviderId value) => new(value);\n\n    public static implicit operator SubscriberId(Subscription subscription)\n    {\n        if (subscription.Metadata.TryGetValue(MetadataKeys.UserId, out var userIdValue)\n            && Guid.TryParse(userIdValue, out var userId))\n        {\n            return new UserId(userId);\n        }\n\n        if (subscription.Metadata.TryGetValue(MetadataKeys.OrganizationId, out var organizationIdValue)\n            && Guid.TryParse(organizationIdValue, out var organizationId))\n        {\n            return new OrganizationId(organizationId);\n        }\n\n        return subscription.Metadata.TryGetValue(MetadataKeys.ProviderId, out var providerIdValue) &&\n               Guid.TryParse(providerIdValue, out var providerId)\n            ? new ProviderId(providerId)\n            : throw new ConflictException(\"Subscription does not have a valid subscriber ID\");\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Queries/GetBitwardenSubscriptionQuery.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Microsoft.Extensions.Logging;\nusing OneOf;\nusing Stripe;\n\nnamespace Bit.Core.Billing.Subscriptions.Queries;\n\nusing static StripeConstants;\nusing static Utilities;\nusing PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;\n\npublic interface IGetBitwardenSubscriptionQuery\n{\n    /// <summary>\n    /// Retrieves detailed subscription information for a user, including subscription status,\n    /// cart items, discounts, and billing details.\n    /// </summary>\n    /// <param name=\"user\">The user whose subscription information to retrieve.</param>\n    /// <returns>\n    /// A <see cref=\"BitwardenSubscription\"/> containing the subscription details, or null if no\n    /// subscription is found or the subscription status is not recognized.\n    /// </returns>\n    /// <remarks>\n    /// Currently only supports <see cref=\"User\"/> subscribers. Future versions will support all\n    /// <see cref=\"ISubscriber\"/> types (User and Organization).\n    /// </remarks>\n    Task<BitwardenSubscription?> Run(User user);\n}\n\npublic class GetBitwardenSubscriptionQuery(\n    ILogger<GetBitwardenSubscriptionQuery> logger,\n    IPricingClient pricingClient,\n    IStripeAdapter stripeAdapter) : IGetBitwardenSubscriptionQuery\n{\n    public async Task<BitwardenSubscription?> Run(User user)\n    {\n        if (string.IsNullOrEmpty(user.GatewaySubscriptionId))\n        {\n            return null;\n        }\n\n        var subscription = await FetchSubscriptionAsync(user);\n\n        if (subscription == null)\n        {\n            return null;\n        }\n\n        var cart = await GetPremiumCartAsync(subscription);\n\n        var baseSubscription = new BitwardenSubscription { Status = subscription.Status, Cart = cart, Storage = user };\n\n        switch (subscription.Status)\n        {\n            case SubscriptionStatus.Incomplete:\n            case SubscriptionStatus.IncompleteExpired:\n                return baseSubscription with { Suspension = subscription.Created.AddHours(23), GracePeriod = 1 };\n\n            case SubscriptionStatus.Trialing:\n            case SubscriptionStatus.Active:\n                return baseSubscription with\n                {\n                    NextCharge = subscription.GetCurrentPeriodEnd(),\n                    CancelAt = subscription.CancelAt\n                };\n\n            case SubscriptionStatus.PastDue:\n            case SubscriptionStatus.Unpaid:\n                var suspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);\n                if (suspension == null)\n                {\n                    return baseSubscription;\n                }\n                return baseSubscription with { Suspension = suspension.SuspensionDate, GracePeriod = suspension.GracePeriod };\n\n            case SubscriptionStatus.Canceled:\n                return baseSubscription with { Canceled = subscription.CanceledAt };\n\n            default:\n                {\n                    logger.LogError(\"Subscription ({SubscriptionID}) has an unmanaged status ({Status})\", subscription.Id, subscription.Status);\n                    throw new ConflictException(\"Subscription is in an invalid state. Please contact support for assistance.\");\n                }\n        }\n    }\n\n    private async Task<Cart> GetPremiumCartAsync(\n        Subscription subscription)\n    {\n        var plans = await pricingClient.ListPremiumPlans();\n\n        var passwordManagerSeatsItem = subscription.Items.FirstOrDefault(item =>\n            plans.Any(plan => plan.Seat.StripePriceId == item.Price.Id));\n\n        if (passwordManagerSeatsItem == null)\n        {\n            throw new ConflictException(\"Premium subscription does not have a Password Manager line item.\");\n        }\n\n        var additionalStorageItem = subscription.Items.FirstOrDefault(item =>\n            plans.Any(plan => plan.Storage.StripePriceId == item.Price.Id));\n\n        var (cartLevelDiscount, productLevelDiscounts) = GetStripeDiscounts(subscription);\n\n        var availablePlan = plans.First(plan => plan.Available);\n        var onCurrentPricing = passwordManagerSeatsItem.Price.Id == availablePlan.Seat.StripePriceId;\n\n        decimal seatCost;\n        decimal estimatedTax;\n\n        if (onCurrentPricing)\n        {\n            seatCost = GetCost(passwordManagerSeatsItem);\n            estimatedTax = await EstimatePremiumTaxAsync(subscription);\n        }\n        else\n        {\n            seatCost = availablePlan.Seat.Price;\n            estimatedTax = await EstimatePremiumTaxAsync(subscription, plans, availablePlan);\n        }\n\n        var passwordManagerSeats = new CartItem\n        {\n            TranslationKey = \"premiumMembership\",\n            Quantity = passwordManagerSeatsItem.Quantity,\n            Cost = seatCost,\n            Discount = productLevelDiscounts.FirstOrDefault(discount => discount.AppliesTo(passwordManagerSeatsItem))\n        };\n\n        var additionalStorage = additionalStorageItem != null\n            ? new CartItem\n            {\n                TranslationKey = \"additionalStorageGB\",\n                Quantity = additionalStorageItem.Quantity,\n                Cost = GetCost(additionalStorageItem),\n                Discount = productLevelDiscounts.FirstOrDefault(discount => discount.AppliesTo(additionalStorageItem))\n            }\n            : null;\n\n        return new Cart\n        {\n            PasswordManager = new PasswordManagerCartItems\n            {\n                Seats = passwordManagerSeats,\n                AdditionalStorage = additionalStorage\n            },\n            Cadence = PlanCadenceType.Annually,\n            Discount = cartLevelDiscount,\n            EstimatedTax = estimatedTax\n        };\n    }\n\n    #region Utilities\n\n    private async Task<decimal> EstimatePremiumTaxAsync(\n        Subscription subscription,\n        List<PremiumPlan>? plans = null,\n        PremiumPlan? availablePlan = null)\n    {\n        try\n        {\n            var options = new InvoiceCreatePreviewOptions\n            {\n                Customer = subscription.Customer.Id\n            };\n\n            if (plans != null && availablePlan != null)\n            {\n                options.AutomaticTax = new InvoiceAutomaticTaxOptions\n                {\n                    Enabled = subscription.AutomaticTax?.Enabled ?? false\n                };\n\n                options.SubscriptionDetails = new InvoiceSubscriptionDetailsOptions\n                {\n                    Items = subscription.Items.Select(item =>\n                    {\n                        var isSeatItem = plans.Any(plan => plan.Seat.StripePriceId == item.Price.Id);\n\n                        return new InvoiceSubscriptionDetailsItemOptions\n                        {\n                            Price = isSeatItem ? availablePlan.Seat.StripePriceId : item.Price.Id,\n                            Quantity = item.Quantity\n                        };\n                    }).ToList()\n                };\n            }\n            else\n            {\n                options.Subscription = subscription.Id;\n            }\n\n            var invoice = await stripeAdapter.CreateInvoicePreviewAsync(options);\n\n            return GetCost(invoice.TotalTaxes);\n        }\n        catch (StripeException stripeException) when\n            (stripeException.StripeError.Code == ErrorCodes.InvoiceUpcomingNone)\n        {\n            return 0;\n        }\n    }\n\n    private static decimal GetCost(OneOf<SubscriptionItem, List<InvoiceTotalTax>> value) =>\n        value.Match(\n            item => (item.Price.UnitAmountDecimal ?? 0) / 100M,\n            taxes => taxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount) / 100M);\n\n    private static (Discount? CartLevel, List<Discount> ProductLevel) GetStripeDiscounts(\n        Subscription subscription)\n    {\n        var discounts = new List<Discount>();\n\n        if (subscription.Customer.Discount.IsValid())\n        {\n            discounts.Add(subscription.Customer.Discount);\n        }\n\n        discounts.AddRange(subscription.Discounts.Where(discount => discount.IsValid()));\n\n        var cartLevel = new List<Discount>();\n        var productLevel = new List<Discount>();\n\n        foreach (var discount in discounts)\n        {\n            switch (discount)\n            {\n                case { Coupon.AppliesTo.Products: null or { Count: 0 } }:\n                    cartLevel.Add(discount);\n                    break;\n                case { Coupon.AppliesTo.Products.Count: > 0 }:\n                    productLevel.Add(discount);\n                    break;\n            }\n        }\n\n        return (cartLevel.FirstOrDefault(), productLevel);\n    }\n\n    private async Task<Subscription?> FetchSubscriptionAsync(User user)\n    {\n        try\n        {\n            return await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, new SubscriptionGetOptions\n            {\n                Expand =\n                [\n                    \"customer.discount.coupon.applies_to\",\n                    \"discounts.coupon.applies_to\",\n                    \"items.data.price.product\",\n                    \"test_clock\"\n                ]\n            });\n        }\n        catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.ResourceMissing)\n        {\n            logger.LogError(\"Subscription ({SubscriptionID}) for User ({UserID}) was not found\", user.GatewaySubscriptionId, user.Id);\n            return null;\n        }\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "src/Core/Billing/Subscriptions/Repositories/ISubscriptionDiscountRepository.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Billing.Subscriptions.Repositories;\n\npublic interface ISubscriptionDiscountRepository : IRepository<SubscriptionDiscount, Guid>\n{\n    /// <summary>\n    /// Retrieves all active subscription discounts that are currently within their valid date range.\n    /// A discount is considered active if the current UTC date falls between StartDate (inclusive) and EndDate (inclusive).\n    /// </summary>\n    /// <returns>A collection of active subscription discounts.</returns>\n    Task<ICollection<SubscriptionDiscount>> GetActiveDiscountsAsync();\n\n    /// <summary>\n    /// Retrieves a subscription discount by its Stripe coupon ID.\n    /// </summary>\n    /// <param name=\"stripeCouponId\">The Stripe coupon ID to search for.</param>\n    /// <returns>The subscription discount if found; otherwise, null.</returns>\n    Task<SubscriptionDiscount?> GetByStripeCouponIdAsync(string stripeCouponId);\n\n    /// <summary>\n    /// Lists subscription discounts with pagination support.\n    /// Returns discounts ordered by creation date descending (newest first).\n    /// </summary>\n    /// <param name=\"skip\">Number of records to skip (for pagination).</param>\n    /// <param name=\"take\">Number of records to take (page size).</param>\n    /// <returns>A collection of subscription discounts for the requested page.</returns>\n    Task<ICollection<SubscriptionDiscount>> ListAsync(int skip, int take);\n}\n"
  },
  {
    "path": "src/Core/Billing/Tax/Models/AutomaticTaxFactoryParameters.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Billing.Tax.Models;\n\npublic class AutomaticTaxFactoryParameters\n{\n    public AutomaticTaxFactoryParameters(PlanType planType)\n    {\n        PlanType = planType;\n    }\n\n    public AutomaticTaxFactoryParameters(ISubscriber subscriber, IEnumerable<string> prices)\n    {\n        Subscriber = subscriber;\n        Prices = prices;\n    }\n\n    public AutomaticTaxFactoryParameters(IEnumerable<string> prices)\n    {\n        Prices = prices;\n    }\n\n    public ISubscriber? Subscriber { get; init; }\n\n    public PlanType? PlanType { get; init; }\n\n    public IEnumerable<string>? Prices { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Tax/Models/TaxIdType.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.RegularExpressions;\n\nnamespace Bit.Core.Billing.Tax.Models;\n\npublic class TaxIdType\n{\n    /// <summary>\n    /// ISO-3166-2 code for the country.\n    /// </summary>\n    public string Country { get; set; }\n\n    /// <summary>\n    /// The identifier in Stripe for the tax ID type.\n    /// </summary>\n    public string Code { get; set; }\n\n    public Regex ValidationExpression { get; set; }\n\n    public string Description { get; set; }\n\n    public string Example { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Tax/Models/TaxInformation.cs",
    "content": "﻿using Bit.Core.Models.Business;\n\nnamespace Bit.Core.Billing.Tax.Models;\n\npublic record TaxInformation(\n    string Country,\n    string PostalCode,\n    string TaxId,\n    string TaxIdType,\n    string Line1,\n    string Line2,\n    string City,\n    string State)\n{\n    public static TaxInformation From(TaxInfo taxInfo) => new(\n        taxInfo.BillingAddressCountry,\n        taxInfo.BillingAddressPostalCode,\n        taxInfo.TaxIdNumber,\n        taxInfo.TaxIdType,\n        taxInfo.BillingAddressLine1,\n        taxInfo.BillingAddressLine2,\n        taxInfo.BillingAddressCity,\n        taxInfo.BillingAddressState);\n}\n"
  },
  {
    "path": "src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Billing.Tax.Requests;\n\npublic class PreviewIndividualInvoiceRequestBody\n{\n    [Required]\n    public IndividualPasswordManagerRequestModel PasswordManager { get; set; }\n\n    [Required]\n    public TaxInformationRequestModel TaxInformation { get; set; }\n}\n\npublic class IndividualPasswordManagerRequestModel\n{\n    [Range(0, int.MaxValue)]\n    public int AdditionalStorage { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Billing.Tax.Requests;\n\npublic class PreviewOrganizationInvoiceRequestBody\n{\n    public Guid OrganizationId { get; set; }\n\n    [Required]\n    public OrganizationPasswordManagerRequestModel PasswordManager { get; set; }\n\n    public SecretsManagerRequestModel SecretsManager { get; set; }\n\n    [Required]\n    public TaxInformationRequestModel TaxInformation { get; set; }\n}\n\npublic class OrganizationPasswordManagerRequestModel\n{\n    public PlanType Plan { get; set; }\n\n    public PlanSponsorshipType? SponsoredPlan { get; set; }\n\n    [Range(0, int.MaxValue)]\n    public int Seats { get; set; }\n\n    [Range(0, int.MaxValue)]\n    public int AdditionalStorage { get; set; }\n}\n\npublic class SecretsManagerRequestModel\n{\n    [Range(0, int.MaxValue)]\n    public int Seats { get; set; }\n\n    [Range(0, int.MaxValue)]\n    public int AdditionalMachineAccounts { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Billing.Tax.Requests;\n\npublic class TaxInformationRequestModel\n{\n    [Length(2, 2), Required]\n    public string Country { get; set; }\n\n    [Required]\n    public string PostalCode { get; set; }\n\n    public string TaxId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Billing/Tax/Responses/PreviewInvoiceResponseModel.cs",
    "content": "﻿namespace Bit.Core.Billing.Tax.Responses;\n\npublic record PreviewInvoiceResponseModel(\n    decimal EffectiveTaxRate,\n    decimal TaxableBaseAmount,\n    decimal TaxAmount,\n    decimal TotalAmount);\n"
  },
  {
    "path": "src/Core/Billing/Tax/Services/ITaxService.cs",
    "content": "﻿namespace Bit.Core.Billing.Tax.Services;\n\npublic interface ITaxService\n{\n    /// <summary>\n    /// Retrieves the Stripe tax code for a given country and tax ID.\n    /// </summary>\n    /// <param name=\"country\"></param>\n    /// <param name=\"taxId\"></param>\n    /// <returns>\n    /// Returns the Stripe tax code if the tax ID is valid for the country.\n    /// Returns null if the tax ID is invalid or the country is not supported.\n    /// </returns>\n    string GetStripeTaxCode(string country, string taxId);\n\n    /// <summary>\n    /// Returns true or false whether charging or storing tax is supported for the given country.\n    /// </summary>\n    /// <param name=\"country\"></param>\n    /// <returns></returns>\n    bool IsSupported(string country);\n}\n"
  },
  {
    "path": "src/Core/Billing/Tax/Services/Implementations/TaxService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.RegularExpressions;\nusing Bit.Core.Billing.Tax.Models;\n\nnamespace Bit.Core.Billing.Tax.Services.Implementations;\n\npublic class TaxService : ITaxService\n{\n    /// <summary>\n    /// Retrieves a list of supported tax ID types for customers.\n    /// </summary>\n    /// <remarks>Compiled list from <see href=\"https://docs.stripe.com/billing/customer/tax-ids\">Stripe</see></remarks>\n    private static readonly IEnumerable<TaxIdType> _taxIdTypes =\n    [\n        new()\n        {\n            Country = \"AD\",\n            Code = \"ad_nrt\",\n            Description = \"Andorran NRT number\",\n            Example = \"A-123456-Z\",\n            ValidationExpression = new Regex(\"^([A-Z]{1})-?([0-9]{6})-?([A-Z]{1})$\")\n        },\n        new()\n        {\n            Country = \"AR\",\n            Code = \"ar_cuit\",\n            Description = \"Argentinian tax ID number\",\n            Example = \"12-34567890-1\",\n            ValidationExpression = new Regex(\"^([0-9]{2})-?([0-9]{8})-?([0-9]{1})$\")\n        },\n        new()\n        {\n            Country = \"AU\",\n            Code = \"au_abn\",\n            Description = \"Australian Business Number (AU ABN)\",\n            Example = \"123456789012\",\n            ValidationExpression = new Regex(\"^[0-9]{11}$\")\n        },\n        new()\n        {\n            Country = \"AU\",\n            Code = \"au_arn\",\n            Description = \"Australian Taxation Office Reference Number\",\n            Example = \"123456789123\",\n            ValidationExpression = new Regex(\"^[0-9]{12}$\")\n        },\n        new()\n        {\n            Country = \"AT\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Austria)\",\n            Example = \"ATU12345678\",\n            ValidationExpression = new Regex(\"^ATU[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"BH\",\n            Code = \"bh_vat\",\n            Description = \"Bahraini VAT Number\",\n            Example = \"123456789012345\",\n            ValidationExpression = new Regex(\"^[0-9]{15}$\")\n        },\n        new()\n        {\n            Country = \"BY\",\n            Code = \"by_tin\",\n            Description = \"Belarus TIN Number\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"BE\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Belgium)\",\n            Example = \"BE0123456789\",\n            ValidationExpression = new Regex(\"^BE[0-9]{10}$\")\n        },\n        new()\n        {\n            Country = \"BO\",\n            Code = \"bo_tin\",\n            Description = \"Bolivian tax ID\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"BR\",\n            Code = \"br_cnpj\",\n            Description = \"Brazilian CNPJ number\",\n            Example = \"01.234.456/5432-10\",\n            ValidationExpression = new Regex(\"^[0-9]{2}.?[0-9]{3}.?[0-9]{3}/?[0-9]{4}-?[0-9]{2}$\")\n        },\n        new()\n        {\n            Country = \"BR\",\n            Code = \"br_cpf\",\n            Description = \"Brazilian CPF number\",\n            Example = \"123.456.789-87\",\n            ValidationExpression = new Regex(\"^[0-9]{3}.?[0-9]{3}.?[0-9]{3}-?[0-9]{2}$\")\n        },\n        new()\n        {\n            Country = \"BG\",\n            Code = \"bg_uic\",\n            Description = \"Bulgaria Unified Identification Code\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"BG\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Bulgaria)\",\n            Example = \"BG0123456789\",\n            ValidationExpression = new Regex(\"^BG[0-9]{9,10}$\")\n        },\n        new()\n        {\n            Country = \"CA\",\n            Code = \"ca_bn\",\n            Description = \"Canadian BN\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"CA\",\n            Code = \"ca_gst_hst\",\n            Description = \"Canadian GST/HST number\",\n            Example = \"123456789RT0002\",\n            ValidationExpression = new Regex(\"^[0-9]{9}RT[0-9]{4}$\")\n        },\n        new()\n        {\n            Country = \"CA\",\n            Code = \"ca_pst_bc\",\n            Description = \"Canadian PST number (British Columbia)\",\n            Example = \"PST-1234-5678\",\n            ValidationExpression = new Regex(\"^PST-[0-9]{4}-[0-9]{4}$\")\n        },\n        new()\n        {\n            Country = \"CA\",\n            Code = \"ca_pst_mb\",\n            Description = \"Canadian PST number (Manitoba)\",\n            Example = \"123456-7\",\n            ValidationExpression = new Regex(\"^[0-9]{6}-[0-9]{1}$\")\n        },\n        new()\n        {\n            Country = \"CA\",\n            Code = \"ca_pst_sk\",\n            Description = \"Canadian PST number (Saskatchewan)\",\n            Example = \"1234567\",\n            ValidationExpression = new Regex(\"^[0-9]{7}$\")\n        },\n        new()\n        {\n            Country = \"CA\",\n            Code = \"ca_qst\",\n            Description = \"Canadian QST number (Québec)\",\n            Example = \"1234567890TQ1234\",\n            ValidationExpression = new Regex(\"^[0-9]{10}TQ[0-9]{4}$\")\n        },\n        new()\n        {\n            Country = \"CL\",\n            Code = \"cl_tin\",\n            Description = \"Chilean TIN\",\n            Example = \"12.345.678-K\",\n            ValidationExpression = new Regex(\"^[0-9]{2}.?[0-9]{3}.?[0-9]{3}-?[0-9A-Z]{1}$\")\n        },\n        new()\n        {\n            Country = \"CN\",\n            Code = \"cn_tin\",\n            Description = \"Chinese tax ID\",\n            Example = \"123456789012345678\",\n            ValidationExpression = new Regex(\"^[0-9]{15,18}$\")\n        },\n        new()\n        {\n            Country = \"CO\",\n            Code = \"co_nit\",\n            Description = \"Colombian NIT number\",\n            Example = \"123.456.789-0\",\n            ValidationExpression = new Regex(\"^[0-9]{3}.?[0-9]{3}.?[0-9]{3}-?[0-9]{1}$\")\n        },\n        new()\n        {\n            Country = \"CR\",\n            Code = \"cr_tin\",\n            Description = \"Costa Rican tax ID\",\n            Example = \"1-234-567890\",\n            ValidationExpression = new Regex(\"^[0-9]{1}-?[0-9]{3}-?[0-9]{6}$\")\n        },\n        new()\n        {\n            Country = \"HR\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Croatia)\",\n            Example = \"HR12345678912\",\n            ValidationExpression = new Regex(\"^HR[0-9]{11}$\")\n        },\n        new()\n        {\n            Country = \"HR\",\n            Code = \"hr_oib\",\n            Description = \"Croatian Personal Identification Number\",\n            Example = \"12345678901\",\n            ValidationExpression = new Regex(\"^[0-9]{11}$\")\n        },\n        new()\n        {\n            Country = \"CY\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Cyprus)\",\n            Example = \"CY12345678X\",\n            ValidationExpression = new Regex(\"^CY[0-9]{8}[A-Z]{1}$\")\n        },\n        new()\n        {\n            Country = \"CZ\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Czech Republic)\",\n            Example = \"CZ12345678\",\n            ValidationExpression = new Regex(\"^CZ[0-9]{8,10}$\")\n        },\n        new()\n        {\n            Country = \"DK\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Denmark)\",\n            Example = \"DK12345678\",\n            ValidationExpression = new Regex(\"^DK[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"DO\",\n            Code = \"do_rcn\",\n            Description = \"Dominican RCN number\",\n            Example = \"123-4567890-1\",\n            ValidationExpression = new Regex(\"^[0-9]{3}-?[0-9]{7}-?[0-9]{1}$\")\n        },\n        new()\n        {\n            Country = \"EC\",\n            Code = \"ec_ruc\",\n            Description = \"Ecuadorian RUC number\",\n            Example = \"1234567890001\",\n            ValidationExpression = new Regex(\"^[0-9]{13}$\")\n        },\n        new()\n        {\n            Country = \"EG\",\n            Code = \"eg_tin\",\n            Description = \"Egyptian Tax Identification Number\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n\n        new()\n        {\n            Country = \"SV\",\n            Code = \"sv_nit\",\n            Description = \"El Salvadorian NIT number\",\n            Example = \"1234-567890-123-4\",\n            ValidationExpression = new Regex(\"^[0-9]{4}-?[0-9]{6}-?[0-9]{3}-?[0-9]{1}$\")\n        },\n\n        new()\n        {\n            Country = \"EE\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Estonia)\",\n            Example = \"EE123456789\",\n            ValidationExpression = new Regex(\"^EE[0-9]{9}$\")\n        },\n\n        new()\n        {\n            Country = \"EU\",\n            Code = \"eu_oss_vat\",\n            Description = \"European One Stop Shop VAT number for non-Union scheme\",\n            Example = \"EU123456789\",\n            ValidationExpression = new Regex(\"^EU[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"FI\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Finland)\",\n            Example = \"FI12345678\",\n            ValidationExpression = new Regex(\"^FI[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"FR\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (France)\",\n            Example = \"FR12345678901\",\n            ValidationExpression = new Regex(\"^FR[0-9A-Z]{2}[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"GE\",\n            Code = \"ge_vat\",\n            Description = \"Georgian VAT\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"DE\",\n            Code = \"de_stn\",\n            Description = \"German Tax Number (Steuernummer)\",\n            Example = \"1234567890\",\n            ValidationExpression = new Regex(\"^[0-9]{10}$\")\n        },\n        new()\n        {\n            Country = \"DE\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Germany)\",\n            Example = \"DE123456789\",\n            ValidationExpression = new Regex(\"^DE[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"GR\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Greece)\",\n            Example = \"EL123456789\",\n            ValidationExpression = new Regex(\"^EL[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"HK\",\n            Code = \"hk_br\",\n            Description = \"Hong Kong BR number\",\n            Example = \"12345678\",\n            ValidationExpression = new Regex(\"^[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"HU\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Hungaria)\",\n            Example = \"HU12345678\",\n            ValidationExpression = new Regex(\"^HU[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"HU\",\n            Code = \"hu_tin\",\n            Description = \"Hungary tax number (adószám)\",\n            Example = \"12345678-1-23\",\n            ValidationExpression = new Regex(\"^[0-9]{8}-?[0-9]-?[0-9]{2}$\")\n        },\n        new()\n        {\n            Country = \"IS\",\n            Code = \"is_vat\",\n            Description = \"Icelandic VAT\",\n            Example = \"123456\",\n            ValidationExpression = new Regex(\"^[0-9]{6}$\")\n        },\n        new()\n        {\n            Country = \"IN\",\n            Code = \"in_gst\",\n            Description = \"Indian GST number\",\n            Example = \"12ABCDE3456FGZH\",\n            ValidationExpression = new Regex(\"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$\")\n        },\n        new()\n        {\n            Country = \"ID\",\n            Code = \"id_npwp\",\n            Description = \"Indonesian NPWP number\",\n            Example = \"012.345.678.9-012.345\",\n            ValidationExpression = new Regex(\"^[0-9]{3}.?[0-9]{3}.?[0-9]{3}.?[0-9]{1}-?[0-9]{3}.?[0-9]{3}$\")\n        },\n        new()\n        {\n            Country = \"IE\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Ireland)\",\n            Example = \"IE1234567AB\",\n            ValidationExpression = new Regex(\"^IE[0-9]{7}[A-Z]{1,2}$\")\n        },\n        new()\n        {\n            Country = \"IL\",\n            Code = \"il_vat\",\n            Description = \"Israel VAT\",\n            Example = \"000012345\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"IT\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Italy)\",\n            Example = \"IT12345678912\",\n            ValidationExpression = new Regex(\"^IT[0-9]{11}$\")\n        },\n        new()\n        {\n            Country = \"JP\",\n            Code = \"jp_cn\",\n            Description = \"Japanese Corporate Number (*Hōjin Bangō*)\",\n            Example = \"1234567891234\",\n            ValidationExpression = new Regex(\"^[0-9]{13}$\")\n        },\n        new()\n        {\n            Country = \"JP\",\n            Code = \"jp_rn\",\n            Description =\n                \"Japanese Registered Foreign Businesses' Registration Number (*Tōroku Kokugai Jigyōsha no Tōroku Bangō*)\",\n            Example = \"12345\",\n            ValidationExpression = new Regex(\"^[0-9]{5}$\")\n        },\n        new()\n        {\n            Country = \"JP\",\n            Code = \"jp_trn\",\n            Description = \"Japanese Tax Registration Number (*Tōroku Bangō*)\",\n            Example = \"T1234567891234\",\n            ValidationExpression = new Regex(\"^T[0-9]{13}$\")\n        },\n        new()\n        {\n            Country = \"KZ\",\n            Code = \"kz_bin\",\n            Description = \"Kazakhstani Business Identification Number\",\n            Example = \"123456789012\",\n            ValidationExpression = new Regex(\"^[0-9]{12}$\")\n        },\n        new()\n        {\n            Country = \"KE\",\n            Code = \"ke_pin\",\n            Description = \"Kenya Revenue Authority Personal Identification Number\",\n            Example = \"P000111111A\",\n            ValidationExpression = new Regex(\"^[A-Z]{1}[0-9]{9}[A-Z]{1}$\")\n        },\n        new()\n        {\n            Country = \"LV\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number\",\n            Example = \"LV12345678912\",\n            ValidationExpression = new Regex(\"^LV[0-9]{11}$\")\n        },\n        new()\n        {\n            Country = \"LI\",\n            Code = \"li_uid\",\n            Description = \"Liechtensteinian UID number\",\n            Example = \"CHE123456789\",\n            ValidationExpression = new Regex(\"^CHE[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"LI\",\n            Code = \"li_vat\",\n            Description = \"Liechtensteinian VAT number\",\n            Example = \"12345\",\n            ValidationExpression = new Regex(\"^[0-9]{5}$\")\n        },\n        new()\n        {\n            Country = \"LT\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Lithuania)\",\n            Example = \"LT123456789123\",\n            ValidationExpression = new Regex(\"^LT[0-9]{9,12}$\")\n        },\n        new()\n        {\n            Country = \"LU\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Luxembourg)\",\n            Example = \"LU12345678\",\n            ValidationExpression = new Regex(\"^LU[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"MY\",\n            Code = \"my_frp\",\n            Description = \"Malaysian FRP number\",\n            Example = \"12345678\",\n            ValidationExpression = new Regex(\"^[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"MY\",\n            Code = \"my_itn\",\n            Description = \"Malaysian ITN\",\n            Example = \"C 1234567890\",\n            ValidationExpression = new Regex(\"^[A-Z]{1} ?[0-9]{10}$\")\n        },\n        new()\n        {\n            Country = \"MY\",\n            Code = \"my_sst\",\n            Description = \"Malaysian SST number\",\n            Example = \"A12-3456-78912345\",\n            ValidationExpression = new Regex(\"^[A-Z]{1}[0-9]{2}-?[0-9]{4}-?[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"MT\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Malta)\",\n            Example = \"MT12345678\",\n            ValidationExpression = new Regex(\"^MT[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"MX\",\n            Code = \"mx_rfc\",\n            Description = \"Mexican RFC number\",\n            Example = \"ABC010203AB9\",\n            ValidationExpression = new Regex(\"^[A-Z]{3}[0-9]{6}[A-Z0-9]{3}$\")\n        },\n        new()\n        {\n            Country = \"MD\",\n            Code = \"md_vat\",\n            Description = \"Moldova VAT Number\",\n            Example = \"1234567\",\n            ValidationExpression = new Regex(\"^[0-9]{7}$\")\n        },\n        new()\n        {\n            Country = \"MA\",\n            Code = \"ma_vat\",\n            Description = \"Morocco VAT Number\",\n            Example = \"12345678\",\n            ValidationExpression = new Regex(\"^[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"NL\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Netherlands)\",\n            Example = \"NL123456789B12\",\n            ValidationExpression = new Regex(\"^NL[0-9]{9}B[0-9]{2}$\")\n        },\n        new()\n        {\n            Country = \"NZ\",\n            Code = \"nz_gst\",\n            Description = \"New Zealand GST number\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"NG\",\n            Code = \"ng_tin\",\n            Description = \"Nigerian TIN Number\",\n            Example = \"12345678-0001\",\n            ValidationExpression = new Regex(\"^[0-9]{8}-[0-9]{4}$\")\n        },\n        new()\n        {\n            Country = \"NO\",\n            Code = \"no_vat\",\n            Description = \"Norwegian VAT number\",\n            Example = \"123456789MVA\",\n            ValidationExpression = new Regex(\"^[0-9]{9}MVA$\")\n        },\n        new()\n        {\n            Country = \"NO\",\n            Code = \"no_voec\",\n            Description = \"Norwegian VAT on e-commerce number\",\n            Example = \"1234567\",\n            ValidationExpression = new Regex(\"^[0-9]{7}$\")\n        },\n        new()\n        {\n            Country = \"OM\",\n            Code = \"om_vat\",\n            Description = \"Omani VAT Number\",\n            Example = \"OM1234567890\",\n            ValidationExpression = new Regex(\"^OM[0-9]{10}$\")\n        },\n        new()\n        {\n            Country = \"PE\",\n            Code = \"pe_ruc\",\n            Description = \"Peruvian RUC number\",\n            Example = \"12345678901\",\n            ValidationExpression = new Regex(\"^[0-9]{11}$\")\n        },\n        new()\n        {\n            Country = \"PH\",\n            Code = \"ph_tin\",\n            Description = \"Philippines Tax Identification Number\",\n            Example = \"123456789012\",\n            ValidationExpression = new Regex(\"^[0-9]{12}$\")\n        },\n        new()\n        {\n            Country = \"PL\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Poland)\",\n            Example = \"PL1234567890\",\n            ValidationExpression = new Regex(\"^PL[0-9]{10}$\")\n        },\n        new()\n        {\n            Country = \"PT\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Portugal)\",\n            Example = \"PT123456789\",\n            ValidationExpression = new Regex(\"^PT[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"RO\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Romania)\",\n            Example = \"RO1234567891\",\n            ValidationExpression = new Regex(\"^RO[0-9]{2,10}$\")\n        },\n        new()\n        {\n            Country = \"RO\",\n            Code = \"ro_tin\",\n            Description = \"Romanian tax ID number\",\n            Example = \"1234567890123\",\n            ValidationExpression = new Regex(\"^[0-9]{13}$\")\n        },\n        new()\n        {\n            Country = \"RU\",\n            Code = \"ru_inn\",\n            Description = \"Russian INN\",\n            Example = \"1234567891\",\n            ValidationExpression = new Regex(\"^[0-9]{10,12}$\")\n        },\n        new()\n        {\n            Country = \"RU\",\n            Code = \"ru_kpp\",\n            Description = \"Russian KPP\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"SA\",\n            Code = \"sa_vat\",\n            Description = \"Saudi Arabia VAT\",\n            Example = \"123456789012345\",\n            ValidationExpression = new Regex(\"^[0-9]{15}$\")\n        },\n        new()\n        {\n            Country = \"RS\",\n            Code = \"rs_pib\",\n            Description = \"Serbian PIB number\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"SG\",\n            Code = \"sg_gst\",\n            Description = \"Singaporean GST\",\n            Example = \"M12345678X\",\n            ValidationExpression = new Regex(\"^[A-Z]{1}[0-9]{8}[A-Z]{1}$\")\n        },\n        new()\n        {\n            Country = \"SG\",\n            Code = \"sg_uen\",\n            Description = \"Singaporean UEN\",\n            Example = \"123456789F\",\n            ValidationExpression = new Regex(\"^[0-9]{9}[A-Z]{1}$\")\n        },\n        new()\n        {\n            Country = \"SK\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Slovakia)\",\n            Example = \"SK1234567891\",\n            ValidationExpression = new Regex(\"^SK[0-9]{10}$\")\n        },\n        new()\n        {\n            Country = \"SI\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Slovenia)\",\n            Example = \"SI12345678\",\n            ValidationExpression = new Regex(\"^SI[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"SI\",\n            Code = \"si_tin\",\n            Description = \"Slovenia tax number (davčna številka)\",\n            Example = \"12345678\",\n            ValidationExpression = new Regex(\"^[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"ZA\",\n            Code = \"za_vat\",\n            Description = \"South African VAT number\",\n            Example = \"4123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{10}$\")\n        },\n        new()\n        {\n            Country = \"KR\",\n            Code = \"kr_brn\",\n            Description = \"Korean BRN\",\n            Example = \"123-45-67890\",\n            ValidationExpression = new Regex(\"^[0-9]{3}-?[0-9]{2}-?[0-9]{5}$\")\n        },\n        new()\n        {\n            Country = \"ES\",\n            Code = \"es_cif\",\n            Description = \"Spanish NIF/CIF number\",\n            Example = \"A12345678\",\n            ValidationExpression = new Regex(\"^[A-Z]{1}[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"ES\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Spain)\",\n            Example = \"ESA1234567Z\",\n            ValidationExpression = new Regex(\"^ES[A-Z]{1}[0-9]{7}[A-Z]{1}$\")\n        },\n        new()\n        {\n            Country = \"SE\",\n            Code = \"eu_vat\",\n            Description = \"European VAT number (Sweden)\",\n            Example = \"SE123456789123\",\n            ValidationExpression = new Regex(\"^SE[0-9]{12}$\")\n        },\n        new()\n        {\n            Country = \"CH\",\n            Code = \"ch_uid\",\n            Description = \"Switzerland UID number\",\n            Example = \"CHE-123.456.789 HR\",\n            ValidationExpression = new Regex(\"^CHE-?[0-9]{3}.?[0-9]{3}.?[0-9]{3} ?HR$\")\n        },\n        new()\n        {\n            Country = \"CH\",\n            Code = \"ch_vat\",\n            Description = \"Switzerland VAT number\",\n            Example = \"CHE-123.456.789 MWST\",\n            ValidationExpression = new Regex(\"^CHE-?[0-9]{3}.?[0-9]{3}.?[0-9]{3} ?MWST$\")\n        },\n        new()\n        {\n            Country = \"TW\",\n            Code = \"tw_vat\",\n            Description = \"Taiwanese VAT\",\n            Example = \"12345678\",\n            ValidationExpression = new Regex(\"^[0-9]{8}$\")\n        },\n        new()\n        {\n            Country = \"TZ\",\n            Code = \"tz_vat\",\n            Description = \"Tanzania VAT Number\",\n            Example = \"12345678A\",\n            ValidationExpression = new Regex(\"^[0-9]{8}[A-Z]{1}$\")\n        },\n        new()\n        {\n            Country = \"TH\",\n            Code = \"th_vat\",\n            Description = \"Thai VAT\",\n            Example = \"1234567891234\",\n            ValidationExpression = new Regex(\"^[0-9]{13}$\")\n        },\n        new()\n        {\n            Country = \"TR\",\n            Code = \"tr_tin\",\n            Description = \"Turkish TIN Number\",\n            Example = \"0123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{10}$\")\n        },\n        new()\n        {\n            Country = \"UA\",\n            Code = \"ua_vat\",\n            Description = \"Ukrainian VAT\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"AE\",\n            Code = \"ae_trn\",\n            Description = \"United Arab Emirates TRN\",\n            Example = \"123456789012345\",\n            ValidationExpression = new Regex(\"^[0-9]{15}$\")\n        },\n        new()\n        {\n            Country = \"GB\",\n            Code = \"eu_vat\",\n            Description = \"Northern Ireland VAT number\",\n            Example = \"XI123456789\",\n            ValidationExpression = new Regex(\"^XI[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"GB\",\n            Code = \"gb_vat\",\n            Description = \"United Kingdom VAT number\",\n            Example = \"GB123456789\",\n            ValidationExpression = new Regex(\"^GB[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"US\",\n            Code = \"us_ein\",\n            Description = \"United States EIN\",\n            Example = \"12-3456789\",\n            ValidationExpression = new Regex(\"^[0-9]{2}-?[0-9]{7}$\")\n        },\n        new()\n        {\n            Country = \"UY\",\n            Code = \"uy_ruc\",\n            Description = \"Uruguayan RUC number\",\n            Example = \"123456789012\",\n            ValidationExpression = new Regex(\"^[0-9]{12}$\")\n        },\n        new()\n        {\n            Country = \"UZ\",\n            Code = \"uz_tin\",\n            Description = \"Uzbekistan TIN Number\",\n            Example = \"123456789\",\n            ValidationExpression = new Regex(\"^[0-9]{9}$\")\n        },\n        new()\n        {\n            Country = \"UZ\",\n            Code = \"uz_vat\",\n            Description = \"Uzbekistan VAT Number\",\n            Example = \"123456789012\",\n            ValidationExpression = new Regex(\"^[0-9]{12}$\")\n        },\n        new()\n        {\n            Country = \"VE\",\n            Code = \"ve_rif\",\n            Description = \"Venezuelan RIF number\",\n            Example = \"A-12345678-9\",\n            ValidationExpression = new Regex(\"^[A-Z]{1}-?[0-9]{8}-?[0-9]{1}$\")\n        },\n        new()\n        {\n            Country = \"VN\",\n            Code = \"vn_tin\",\n            Description = \"Vietnamese tax ID number\",\n            Example = \"1234567890\",\n            ValidationExpression = new Regex(\"^[0-9]{10}$\")\n        }\n    ];\n\n    public string GetStripeTaxCode(string country, string taxId)\n    {\n        foreach (var taxIdType in _taxIdTypes.Where(x => x.Country == country))\n        {\n            if (taxIdType.ValidationExpression.IsMatch(taxId))\n            {\n                return taxIdType.Code;\n            }\n        }\n\n        return null;\n    }\n\n    public bool IsSupported(string country)\n    {\n        return _taxIdTypes.Any(x => x.Country == country);\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Tax/Utilities/TaxHelpers.cs",
    "content": "﻿using CountryAbbreviations = Bit.Core.Constants.CountryAbbreviations;\nusing TaxExempt = Bit.Core.Billing.Constants.StripeConstants.TaxExempt;\nnamespace Bit.Core.Billing.Tax.Utilities;\n\npublic static class TaxHelpers\n{\n    /// <summary>\n    /// Countries where tax is collected directly from customers, rather than through VAT ID reverse charge.\n    /// To add a new country, add its ISO 3166 code to <see cref=\"Bit.Core.Constants.CountryAbbreviations\"/>\n    /// and then add it to this set.\n    /// </summary>\n    private static readonly HashSet<string> DirectTaxCountries =\n    [\n        CountryAbbreviations.UnitedStates,\n        CountryAbbreviations.Switzerland\n    ];\n\n    /// <summary>\n    /// Returns <see langword=\"true\"/> if <paramref name=\"country\"/> is in <see cref=\"DirectTaxCountries\"/>,\n    /// meaning tax is collected directly and Stripe's <c>tax_exempt</c> should default to <c>\"none\"</c>.\n    /// Returns <see langword=\"false\"/> for all other countries, where VAT reverse charge applies.\n    /// </summary>\n    public static bool IsDirectTaxCountry(string? country) =>\n       country is not null and not \"\" && DirectTaxCountries.Contains(country);\n\n    /// <summary>\n    /// Returns the Stripe <c>tax_exempt</c> value appropriate for <paramref name=\"country\"/>.<br/>\n    /// If <paramref name=\"currentTaxExempt\"/> is already <c>\"exempt\"</c>, that status is always preserved.<br/>\n    /// For direct-tax countries, returns <c>\"none\"</c>.<br/>\n    /// For all other countries, returns <c>\"reverse\"</c>.\n    /// </summary>\n    public static string DetermineTaxExemptStatus(string? country, string? currentTaxExempt = null) =>\n        currentTaxExempt == TaxExempt.Exempt\n            ? TaxExempt.Exempt\n            : IsDirectTaxCountry(country) ? TaxExempt.None : TaxExempt.Reverse;\n}\n"
  },
  {
    "path": "src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Billing.Enums;\n\nnamespace Bit.Core.Billing.TrialInitiation.Registration;\n\npublic interface ISendTrialInitiationEmailForRegistrationCommand\n{\n    public Task<string?> Handle(\n        string email,\n        string? name,\n        bool receiveMarketingEmails,\n        ProductTierType productTier,\n        IEnumerable<ProductType> products,\n        int trialLength);\n}\n"
  },
  {
    "path": "src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Billing.TrialInitiation.Registration.Implementations;\n\npublic class SendTrialInitiationEmailForRegistrationCommand(\n    IUserRepository userRepository,\n    GlobalSettings globalSettings,\n    IMailService mailService,\n    IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> tokenDataFactory)\n    : ISendTrialInitiationEmailForRegistrationCommand\n{\n    public async Task<string?> Handle(\n        string email,\n        string? name,\n        bool receiveMarketingEmails,\n        ProductTierType productTier,\n        IEnumerable<ProductType> products,\n        int trialLength)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(email, nameof(email));\n\n        var userExists = await CheckUserExistsConstantTimeAsync(email);\n        var token = GenerateToken(email, name, receiveMarketingEmails);\n\n        if (!globalSettings.EnableEmailVerification)\n        {\n            await PerformConstantTimeOperationsAsync();\n\n            if (userExists)\n            {\n                throw new BadRequestException($\"Email {email} is already taken\");\n            }\n\n            return token;\n        }\n\n        await PerformConstantTimeOperationsAsync();\n\n        if (trialLength != 0 && trialLength != 7)\n        {\n            trialLength = 7;\n        }\n\n        await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products, trialLength);\n\n        return null;\n    }\n\n    /// <summary>\n    /// Perform constant time operations to prevent timing attacks\n    /// </summary>\n    private static async Task PerformConstantTimeOperationsAsync()\n    {\n        await Task.Delay(130);\n    }\n\n    private string GenerateToken(string email, string? name, bool receiveMarketingEmails)\n    {\n        var tokenable = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);\n        return tokenDataFactory.Protect(tokenable);\n    }\n\n    private async Task<bool> CheckUserExistsConstantTimeAsync(string email)\n    {\n        var user = await userRepository.GetByEmailAsync(email);\n\n        return CoreHelpers.FixedTimeEquals(user?.Email.ToLowerInvariant() ?? string.Empty, email.ToLowerInvariant());\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/TrialInitiation/TrialInitiationCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Billing.TrialInitiation.Registration;\nusing Bit.Core.Billing.TrialInitiation.Registration.Implementations;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Billing.TrialInitiation;\n\npublic static class TrialInitiationCollectionExtensions\n{\n    public static void AddTrialInitiationServices(this IServiceCollection services)\n    {\n        services.AddSingleton<ISendTrialInitiationEmailForRegistrationCommand, SendTrialInitiationEmailForRegistrationCommand>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Billing/Utilities.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Tax.Models;\nusing Stripe;\n\nnamespace Bit.Core.Billing;\n\npublic static class Utilities\n{\n    public const string BraintreeCustomerIdKey = \"btCustomerId\";\n    public const string BraintreeCustomerIdOldKey = \"btCustomerId_old\";\n\n    public static async Task<SubscriptionSuspension> GetSubscriptionSuspensionAsync(\n        IStripeAdapter stripeAdapter,\n        Subscription subscription)\n    {\n        if (subscription.Status is not \"past_due\" && subscription.Status is not \"unpaid\")\n        {\n            return null;\n        }\n\n        var openInvoices = await stripeAdapter.SearchInvoiceAsync(new InvoiceSearchOptions\n        {\n            Query = $\"subscription:'{subscription.Id}' status:'open'\"\n        });\n\n        if (openInvoices.Count == 0)\n        {\n            return null;\n        }\n\n        var currentDate = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;\n\n        switch (subscription.CollectionMethod)\n        {\n            case \"charge_automatically\":\n                {\n                    var firstOverdueInvoice = openInvoices\n                        .Where(invoice => invoice.PeriodEnd < currentDate && invoice.Attempted)\n                        .MinBy(invoice => invoice.Created);\n\n                    if (firstOverdueInvoice == null)\n                    {\n                        return null;\n                    }\n\n                    const int gracePeriod = 14;\n\n                    return new SubscriptionSuspension(\n                        firstOverdueInvoice.Created.AddDays(gracePeriod),\n                        firstOverdueInvoice.PeriodEnd,\n                        gracePeriod);\n                }\n            case \"send_invoice\":\n                {\n                    var firstOverdueInvoice = openInvoices\n                        .Where(invoice => invoice.DueDate < currentDate)\n                        .MinBy(invoice => invoice.Created);\n\n                    if (firstOverdueInvoice?.DueDate == null)\n                    {\n                        return null;\n                    }\n\n                    const int gracePeriod = 30;\n\n                    return new SubscriptionSuspension(\n                        firstOverdueInvoice.DueDate.Value.AddDays(gracePeriod),\n                        firstOverdueInvoice.PeriodEnd,\n                        gracePeriod);\n                }\n            default: return null;\n        }\n    }\n\n    public static TaxInformation GetTaxInformation(Customer customer)\n    {\n        if (customer.Address == null)\n        {\n            return null;\n        }\n\n        return new TaxInformation(\n            customer.Address.Country,\n            customer.Address.PostalCode,\n            customer.TaxIds?.FirstOrDefault()?.Value,\n            customer.TaxIds?.FirstOrDefault()?.Type,\n            customer.Address.Line1,\n            customer.Address.Line2,\n            customer.Address.City,\n            customer.Address.State);\n    }\n\n    /**\n     * Returns a dictionary with all DiscountTierTypes as keys and false as values,\n     * indicating that by default, no tiers are eligible for a discount.\n     */\n    public static IDictionary<DiscountTierType, bool> GetTierEligibilityDictionary()\n        => Enum.GetValues<DiscountTierType>().ToDictionary(t => t, _ => false);\n}\n"
  },
  {
    "path": "src/Core/Constants.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Reflection;\n\nnamespace Bit.Core;\n\npublic static class Constants\n{\n    public const int BypassFiltersEventId = 12482444;\n    public const int FailedSecretVerificationDelay = 2000;\n\n    /// <summary>\n    /// Self-hosted max storage limit in GB (10 TB).\n    /// </summary>\n    public const short SelfHostedMaxStorageGb = 10240;\n\n    // File size limits - give 1 MB extra for cushion.\n    // Note: if request size limits are changed, 'client_max_body_size'\n    // in nginx/proxy.conf may also need to be updated accordingly.\n    public const long FileSize101mb = 101L * 1024L * 1024L;\n    public const long FileSize501mb = 501L * 1024L * 1024L;\n    public const string DatabaseFieldProtectorPurpose = \"DatabaseFieldProtection\";\n    public const string DatabaseFieldProtectedPrefix = \"P|\";\n\n    /// <summary>\n    /// Default number of days an organization has to apply an updated license to their self-hosted installation after\n    /// their subscription has expired.\n    /// </summary>\n    public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60;\n\n    public const string Fido2KeyCipherMinimumVersion = \"2023.10.0\";\n    public const string SSHKeyCipherMinimumVersion = \"2024.12.0\";\n    public const string DenyLegacyUserMinimumVersion = \"2025.6.0\";\n\n    /// <summary>\n    /// Used by IdentityServer to identify our own provider.\n    /// </summary>\n    public const string IdentityProvider = \"bitwarden\";\n\n    /// <summary>\n    /// Date identifier used in ProviderService to determine if a provider was created before Nov 6, 2023.\n    /// If true, the organization plan assigned to that provider is updated to a 2020 plan.\n    /// </summary>\n    public static readonly DateTime ProviderCreatedPriorNov62023 = new DateTime(2023, 11, 6);\n\n    /// <summary>\n    /// When you set the ProrationBehavior to create_prorations,\n    /// Stripe will automatically create prorations for any changes made to the subscription,\n    /// such as changing the plan, adding or removing quantities, or applying discounts.\n    /// </summary>\n    public const string CreateProrations = \"create_prorations\";\n\n    /// <summary>\n    /// When you set the ProrationBehavior to always_invoice,\n    /// Stripe will always generate an invoice when a subscription update occurs,\n    /// regardless of whether there is a proration or not.\n    /// </summary>\n    public const string AlwaysInvoice = \"always_invoice\";\n\n    /// <summary>\n    /// Used primarily to determine whether a customer's business is inside or outside the United States\n    /// for billing purposes.\n    /// </summary>\n    public static class CountryAbbreviations\n    {\n        /// <summary>\n        /// Abbreviation for The United States.\n        /// This value must match what Stripe uses for the `Country` field value for the United States.\n        /// </summary>\n        public const string UnitedStates = \"US\";\n\n        /// <summary>\n        /// Abbreviation for Switzerland.\n        /// This value must match what Stripe uses for the `Country` field value for Switzerland.\n        /// </summary>\n        public const string Switzerland = \"CH\";\n    }\n\n    /// <summary>\n    /// Constants for our browser extensions IDs\n    /// </summary>\n    public static class BrowserExtensions\n    {\n        public const string ChromeId = \"chrome-extension://nngceckbapebfimnlniiiahkandclblb/\";\n        public const string EdgeId = \"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/\";\n        public const string OperaId = \"chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/\";\n    }\n}\n\npublic static class AuthConstants\n{\n    public static readonly RangeConstant PBKDF2_ITERATIONS = new(600_000, 2_000_000, 600_000);\n\n    public static readonly RangeConstant ARGON2_ITERATIONS = new(2, 10, 3);\n    public static readonly RangeConstant ARGON2_MEMORY = new(15, 1024, 64);\n    public static readonly RangeConstant ARGON2_PARALLELISM = new(1, 16, 4);\n    public static readonly string NewDeviceVerificationExceptionCacheKeyFormat = \"NewDeviceVerificationException_{0}\";\n}\n\npublic class RangeConstant\n{\n    public int Default { get; }\n    public int Min { get; }\n    public int Max { get; }\n\n    public RangeConstant(int min, int max, int defaultValue)\n    {\n        Default = defaultValue;\n        Min = min;\n        Max = max;\n\n        if (Min > Max)\n        {\n            throw new ArgumentOutOfRangeException($\"{Min} is larger than {Max}.\");\n        }\n\n        if (!InsideRange(defaultValue))\n        {\n            throw new ArgumentOutOfRangeException($\"{Default} is outside allowed range of {Min}-{Max}.\");\n        }\n    }\n\n    public bool InsideRange(int number)\n    {\n        return Min <= number && number <= Max;\n    }\n}\n\npublic static class TokenPurposes\n{\n    public const string LinkSso = \"LinkSso\";\n}\n\npublic static class AuthenticationSchemes\n{\n    public const string BitwardenExternalCookieAuthenticationScheme = \"bw.external\";\n}\n\npublic static class FeatureFlagKeys\n{\n    /* Admin Console Team */\n    public const string PolicyRequirements = \"pm-14439-policy-requirements\";\n    public const string ScimInviteUserOptimization = \"pm-16811-optimize-invite-user-flow-to-fail-fast\";\n    public const string CreateDefaultLocation = \"pm-19467-create-default-location\";\n    public const string AutomaticConfirmUsers = \"pm-19934-auto-confirm-organization-users\";\n    public const string ScimRevokeV2 = \"pm-32394-scim-revoke-put-v2\";\n    public const string RefactorMembersComponent = \"pm-29503-refactor-members-inheritance\";\n    public const string BulkReinviteUI = \"pm-28416-bulk-reinvite-ux-improvements\";\n    public const string RefactorOrgAcceptInit = \"pm-33082-refactor-org-accept-init\";\n    public const string PublicMembersInviteRefactor = \"pm-33398-refactor-members-invite-org-users-command\";\n\n    /* Architecture */\n    public const string DesktopMigrationMilestone1 = \"desktop-ui-migration-milestone-1\";\n    public const string DesktopMigrationMilestone2 = \"desktop-ui-migration-milestone-2\";\n    public const string DesktopMigrationMilestone3 = \"desktop-ui-migration-milestone-3\";\n    public const string DesktopMigrationMilestone4 = \"desktop-ui-migration-milestone-4\";\n\n    /* Auth Team */\n    public const string Otp6Digits = \"pm-18612-otp-6-digits\";\n    public const string DisableAlternateLoginMethods = \"pm-22110-disable-alternate-login-methods\";\n    public const string PM2035PasskeyUnlock = \"pm-2035-passkey-unlock\";\n    public const string MjmlWelcomeEmailTemplates = \"pm-21741-mjml-welcome-email\";\n    public const string MarketingInitiatedPremiumFlow = \"pm-26140-marketing-initiated-premium-flow\";\n    public const string PrefetchPasswordPrelogin = \"pm-23801-prefetch-password-prelogin\";\n    public const string SafariAccountSwitching = \"pm-5594-safari-account-switching\";\n    public const string PM27086_UpdateAuthenticationApisForInputPassword = \"pm-27086-update-authentication-apis-for-input-password\";\n    public const string ChangeEmailNewAuthenticationApis = \"pm-30811-change-email-new-authentication-apis\";\n    public const string PM31088_MasterPasswordServiceEmitSalt = \"pm-31088-master-password-service-emit-salt\";\n    public const string PM32413_MultiClientPasswordManagement = \"pm-32413-multi-client-password-management\";\n\n    /* Autofill Team */\n    public const string SSHAgentV2 = \"ssh-agent-v2\";\n    public const string SSHVersionCheckQAOverride = \"ssh-version-check-qa-override\";\n    public const string NotificationRefresh = \"notification-refresh\";\n    public const string MacOsNativeCredentialSync = \"macos-native-credential-sync\";\n    public const string WindowsDesktopAutotype = \"windows-desktop-autotype\";\n    public const string WindowsDesktopAutotypeGA = \"windows-desktop-autotype-ga\";\n    public const string FillAssistTargetingRules = \"fill-assist-targeting-rules\";\n    public const string NotificationUndeterminedCipherScenarioLogic = \"undetermined-cipher-scenario-logic\";\n\n    /* Billing Team */\n    public const string TrialPayment = \"PM-8163-trial-payment\";\n    public const string PM24032_NewNavigationPremiumUpgradeButton = \"pm-24032-new-navigation-premium-upgrade-button\";\n    public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = \"pm-23713-premium-badge-opens-new-premium-upgrade-dialog\";\n    public const string PM26793_FetchPremiumPriceFromPricingService = \"pm-26793-fetch-premium-price-from-pricing-service\";\n    public const string PM23341_Milestone_2 = \"pm-23341-milestone-2\";\n    public const string PM26462_Milestone_3 = \"pm-26462-milestone-3\";\n    public const string PM28265_EnableReconcileAdditionalStorageJob = \"pm-28265-enable-reconcile-additional-storage-job\";\n    public const string PM28265_ReconcileAdditionalStorageJobEnableLiveMode = \"pm-28265-reconcile-additional-storage-job-enable-live-mode\";\n    public const string PM29594_UpdateIndividualSubscriptionPage = \"pm-29594-update-individual-subscription-page\";\n    public const string PM29108_EnablePersonalDiscounts = \"pm-29108-enable-personal-discounts\";\n    public const string PM29593_PremiumToOrganizationUpgrade = \"pm-29593-premium-to-organization-upgrade\";\n    public const string PM32581_UseUpdateOrganizationSubscriptionCommand = \"pm-32581-use-update-organization-subscription-command\";\n\n    /* Key Management Team */\n    public const string PrivateKeyRegeneration = \"pm-12241-private-key-regeneration\";\n    public const string Argon2Default = \"argon2-default\";\n    public const string EnrollAeadOnKeyRotation = \"enroll-aead-on-key-rotation\";\n    public const string ForceUpdateKDFSettings = \"pm-18021-force-update-kdf-settings\";\n    public const string UnlockWithMasterPasswordUnlockData = \"pm-23246-unlock-with-master-password-unlock-data\";\n    public const string LinuxBiometricsV2 = \"pm-26340-linux-biometrics-v2\";\n    public const string NoLogoutOnKdfChange = \"pm-23995-no-logout-on-kdf-change\";\n    public const string DisableType0Decryption = \"pm-25174-disable-type-0-decryption\";\n    public const string ConsolidatedSessionTimeoutComponent = \"pm-26056-consolidated-session-timeout-component\";\n    public const string V2RegistrationTDEJIT = \"pm-27279-v2-registration-tde-jit\";\n    public const string EnableAccountEncryptionV2KeyConnectorRegistration = \"enable-account-encryption-v2-key-connector-registration\";\n    public const string SdkKeyRotation = \"pm-30144-sdk-key-rotation\";\n    public const string UnlockViaSdk = \"unlock-via-sdk\";\n    public const string UseSdkForKeyConnectorMigration = \"use-sdk-for-key-connector-migration\";\n    public const string UseUnlockServiceForPasswordLogin = \"use-unlock-service-for-password-login\";\n    public const string UseUnlockServiceForKeyConnectorLogin = \"use-unlock-service-for-key-connector-login\";\n    public const string NoLogoutOnKeyUpgradeRotation = \"pm-31050-no-logout-key-upgrade-rotation\";\n    public const string EnableAccountEncryptionV2JitPasswordRegistration = \"enable-account-encryption-v2-jit-password-registration\";\n\n    /* Mobile Team */\n    public const string AndroidImportLoginsFlow = \"import-logins-flow\";\n    public const string AndroidMutualTls = \"mutual-tls\";\n    public const string SingleTapPasskeyCreation = \"single-tap-passkey-creation\";\n    public const string SingleTapPasskeyAuthentication = \"single-tap-passkey-authentication\";\n    public const string PM3503_MobileAnonAddySelfHostAlias = \"anon-addy-self-host-alias\";\n    public const string PM3553_MobileSimpleLoginSelfHostAlias = \"simple-login-self-host-alias\";\n    public const string MobileErrorReporting = \"mobile-error-reporting\";\n    public const string AndroidChromeAutofill = \"android-chrome-autofill\";\n    public const string UserManagedPrivilegedApps = \"pm-18970-user-managed-privileged-apps\";\n    public const string CxpImportMobile = \"cxp-import-mobile\";\n    public const string CxpExportMobile = \"cxp-export-mobile\";\n    public const string DeviceAuthKey = \"pm-27581-device-auth-key\";\n    public const string PremiumUpgradePath = \"pm-31697-premium-upgrade-path\";\n\n    /* Platform Team */\n    public const string WebPush = \"web-push\";\n    public const string ContentScriptIpcFramework = \"content-script-ipc-channel-framework\";\n    public const string WebAuthnRelatedOrigins = \"pm-30529-webauthn-related-origins\";\n\n    /* Tools Team */\n    /// <summary>\n    /// Enable this flag to share the send view used by the web and browser clients\n    /// on the desktop client.\n    /// </summary>\n    public const string UseSdkPasswordGenerators = \"pm-19976-use-sdk-password-generators\";\n    public const string UseChromiumImporter = \"pm-23982-chromium-importer\";\n    public const string ChromiumImporterWithABE = \"pm-25855-chromium-importer-abe\";\n    public const string SendUIRefresh = \"pm-28175-send-ui-refresh\";\n    public const string SendEmailOTP = \"pm-19051-send-email-verification\";\n    public const string SendControls = \"pm-31885-send-controls\";\n\n    /* Vault Team */\n    public const string CipherKeyEncryption = \"cipher-key-encryption\";\n    public const string PM19941MigrateCipherDomainToSdk = \"pm-19941-migrate-cipher-domain-to-sdk\";\n    public const string PhishingDetection = \"phishing-detection\";\n    public const string PM22134SdkCipherListView = \"pm-22134-sdk-cipher-list-view\";\n    public const string PM22136_SdkCipherEncryption = \"pm-22136-sdk-cipher-encryption\";\n    public const string PM23904_RiskInsightsForPremium = \"pm-23904-risk-insights-for-premium\";\n    public const string PM25083_AutofillConfirmFromSearch = \"pm-25083-autofill-confirm-from-search\";\n    public const string VaultLoadingSkeletons = \"pm-25081-vault-skeleton-loaders\";\n    public const string BrowserPremiumSpotlight = \"pm-23384-browser-premium-spotlight\";\n    public const string MigrateMyVaultToMyItems = \"pm-20558-migrate-myvault-to-myitems\";\n    public const string PM27632_CipherCrudOperationsToSdk = \"pm-27632-cipher-crud-operations-to-sdk\";\n    public const string PM30521_AutofillButtonViewLoginScreen = \"pm-30521-autofill-button-view-login-screen\";\n    public const string PM32180_PremiumUpsellAccountAge = \"pm-32180-premium-upsell-account-age\";\n    public const string PM29438_WelcomeDialogWithExtensionPrompt = \"pm-29438-welcome-dialog-with-extension-prompt\";\n    public const string PM29438_DialogWithExtensionPromptAccountAge = \"pm-29438-dialog-with-extension-prompt-account-age\";\n    public const string PM31039_ItemActionInExtension = \"pm-31039-item-action-in-extension\";\n    public const string PM29437_WelcomeDialogNoExtPrompt = \"pm-29437-welcome-dialog-no-ext-prompt\";\n    public const string PM31948_OrgUserNotificationBanner = \"pm-31948-org-user-notification-banner\";\n\n    /* Innovation Team */\n    public const string ArchiveVaultItems = \"pm-19148-innovation-archive\";\n\n    /* DIRT Team */\n    public const string EventManagementForDataDogAndCrowdStrike = \"event-management-for-datadog-and-crowdstrike\";\n    public const string EventDiagnosticLogging = \"pm-27666-siem-event-log-debugging\";\n    public const string EventManagementForHuntress = \"event-management-for-huntress\";\n    public const string Milestone11AppPageImprovements = \"pm-30538-dirt-milestone-11-app-page-improvements\";\n\n    /* UIF Team */\n    public const string RouterFocusManagement = \"router-focus-management\";\n\n    public static List<string> GetAllKeys()\n    {\n        return typeof(FeatureFlagKeys).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)\n            .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(string))\n            .Select(x => (string)x.GetRawConstantValue())\n            .ToList();\n    }\n\n    public static Dictionary<string, string> GetLocalOverrideFlagValues()\n    {\n        // place overriding values when needed locally (offline), or return null\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Core/Context/CurrentContext.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing Bit.Core.AdminConsole.Context;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Bit.Core.Context;\n\npublic class CurrentContext(\n    IProviderOrganizationRepository _providerOrganizationRepository,\n    IProviderUserRepository _providerUserRepository) : ICurrentContext\n{\n    private bool _builtHttpContext;\n    private bool _builtClaimsPrincipal;\n    private IEnumerable<ProviderOrganizationProviderDetails> _providerOrganizationProviderDetails;\n    private IEnumerable<ProviderUserOrganizationDetails> _providerUserOrganizations;\n\n    public virtual HttpContext HttpContext { get; set; }\n    public virtual Guid? UserId { get; set; }\n    public virtual User User { get; set; }\n    public virtual string DeviceIdentifier { get; set; }\n    public virtual DeviceType? DeviceType { get; set; }\n    public virtual string IpAddress { get; set; }\n    public virtual string CountryName { get; set; }\n    public virtual List<CurrentContextOrganization> Organizations { get; set; }\n    public virtual List<CurrentContextProvider> Providers { get; set; }\n    public virtual Guid? InstallationId { get; set; }\n    public virtual Guid? OrganizationId { get; set; }\n    public virtual string ClientId { get; set; }\n    public virtual Version ClientVersion { get; set; }\n    public virtual bool ClientVersionIsPrerelease { get; set; }\n    public virtual IdentityClientType IdentityClientType { get; set; }\n    public virtual Guid? ServiceAccountOrganizationId { get; set; }\n\n    public async virtual Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings)\n    {\n        if (_builtHttpContext)\n        {\n            return;\n        }\n\n        _builtHttpContext = true;\n        HttpContext = httpContext;\n        await BuildAsync(httpContext.User, globalSettings);\n\n        if (DeviceIdentifier == null && httpContext.Request.Headers.TryGetValue(\"Device-Identifier\", out var deviceIdentifier))\n        {\n            DeviceIdentifier = deviceIdentifier;\n        }\n\n        if (httpContext.Request.Headers.TryGetValue(\"Device-Type\", out var deviceType) &&\n            Enum.TryParse(deviceType.ToString(), out DeviceType dType))\n        {\n            DeviceType = dType;\n        }\n\n        if (httpContext.Request.Headers.TryGetValue(\"Bitwarden-Client-Version\", out var bitWardenClientVersion) && Version.TryParse(bitWardenClientVersion, out var cVersion))\n        {\n            ClientVersion = cVersion;\n        }\n\n        if (httpContext.Request.Headers.TryGetValue(\"Is-Prerelease\", out var clientVersionIsPrerelease))\n        {\n            ClientVersionIsPrerelease = clientVersionIsPrerelease == \"1\";\n        }\n\n        if (httpContext.Request.Headers.TryGetValue(\"country-name\", out var countryName))\n        {\n            CountryName = countryName;\n        }\n\n    }\n\n    public async virtual Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings)\n    {\n        if (_builtClaimsPrincipal)\n        {\n            return;\n        }\n\n        _builtClaimsPrincipal = true;\n        IpAddress = HttpContext.GetIpAddress(globalSettings);\n        await SetContextAsync(user);\n    }\n\n    public virtual Task SetContextAsync(ClaimsPrincipal user)\n    {\n        if (user == null || !user.Claims.Any())\n        {\n            return Task.FromResult(0);\n        }\n\n        var claimsDict = user.Claims.GroupBy(c => c.Type).ToDictionary(c => c.Key, c => c.Select(v => v));\n\n        ClientId = GetClaimValue(claimsDict, \"client_id\");\n\n        var clientType = GetClaimValue(claimsDict, Claims.Type);\n        if (clientType != null)\n        {\n            if (Enum.TryParse(clientType, out IdentityClientType c))\n            {\n                IdentityClientType = c;\n            }\n        }\n\n        if (IdentityClientType == IdentityClientType.Send)\n        {\n            // For the Send client, we don't need to set any User specific properties on the context\n            // so just short circuit and return here.\n            return Task.FromResult(0);\n        }\n\n        var subject = GetClaimValue(claimsDict, \"sub\");\n        if (Guid.TryParse(subject, out var subIdGuid))\n        {\n            UserId = subIdGuid;\n        }\n\n        ClientId = GetClaimValue(claimsDict, \"client_id\");\n        var clientSubject = GetClaimValue(claimsDict, \"client_sub\");\n        var orgApi = false;\n        if (clientSubject != null)\n        {\n            if (ClientId?.StartsWith(\"installation.\") ?? false)\n            {\n                if (Guid.TryParse(clientSubject, out var idGuid))\n                {\n                    InstallationId = idGuid;\n                }\n            }\n            else if (ClientId?.StartsWith(\"organization.\") ?? false)\n            {\n                if (Guid.TryParse(clientSubject, out var idGuid))\n                {\n                    OrganizationId = idGuid;\n                    orgApi = true;\n                }\n            }\n        }\n\n        if (IdentityClientType == IdentityClientType.ServiceAccount)\n        {\n            ServiceAccountOrganizationId = new Guid(GetClaimValue(claimsDict, Claims.Organization));\n        }\n\n        DeviceIdentifier = GetClaimValue(claimsDict, Claims.Device);\n\n        if (Enum.TryParse(GetClaimValue(claimsDict, Claims.DeviceType), out DeviceType deviceType))\n        {\n            DeviceType = deviceType;\n        }\n\n        Organizations = GetOrganizations(claimsDict, orgApi);\n\n        Providers = GetProviders(claimsDict);\n\n        return Task.FromResult(0);\n    }\n\n    private List<CurrentContextOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi)\n    {\n        var accessSecretsManager = claimsDict.TryGetValue(Claims.SecretsManagerAccess, out var secretsManagerAccessClaim)\n            ? secretsManagerAccessClaim.ToDictionary(s => s.Value, _ => true)\n            : new Dictionary<string, bool>();\n\n        var organizations = new List<CurrentContextOrganization>();\n        if (claimsDict.TryGetValue(Claims.OrganizationOwner, out var organizationOwnerClaim))\n        {\n            organizations.AddRange(organizationOwnerClaim.Select(c =>\n                new CurrentContextOrganization\n                {\n                    Id = new Guid(c.Value),\n                    Type = OrganizationUserType.Owner,\n                    AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),\n                }));\n        }\n        else if (orgApi && OrganizationId.HasValue)\n        {\n            organizations.Add(new CurrentContextOrganization\n            {\n                Id = OrganizationId.Value,\n                Type = OrganizationUserType.Owner,\n            });\n        }\n\n        if (claimsDict.TryGetValue(Claims.OrganizationAdmin, out var organizationAdminClaim))\n        {\n            organizations.AddRange(organizationAdminClaim.Select(c =>\n                new CurrentContextOrganization\n                {\n                    Id = new Guid(c.Value),\n                    Type = OrganizationUserType.Admin,\n                    AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),\n                }));\n        }\n\n        if (claimsDict.TryGetValue(Claims.OrganizationUser, out var organizationUserClaim))\n        {\n            organizations.AddRange(organizationUserClaim.Select(c =>\n                new CurrentContextOrganization\n                {\n                    Id = new Guid(c.Value),\n                    Type = OrganizationUserType.User,\n                    AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),\n                }));\n        }\n\n        if (claimsDict.TryGetValue(Claims.OrganizationCustom, out var organizationCustomClaim))\n        {\n            organizations.AddRange(organizationCustomClaim.Select(c =>\n                new CurrentContextOrganization\n                {\n                    Id = new Guid(c.Value),\n                    Type = OrganizationUserType.Custom,\n                    Permissions = SetOrganizationPermissionsFromClaims(c.Value, claimsDict),\n                    AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),\n                }));\n        }\n\n        return organizations;\n    }\n\n    private List<CurrentContextProvider> GetProviders(Dictionary<string, IEnumerable<Claim>> claimsDict)\n    {\n        var providers = new List<CurrentContextProvider>();\n        if (claimsDict.TryGetValue(Claims.ProviderAdmin, out var providerAdminClaim))\n        {\n            providers.AddRange(providerAdminClaim.Select(c =>\n                new CurrentContextProvider\n                {\n                    Id = new Guid(c.Value),\n                    Type = ProviderUserType.ProviderAdmin\n                }));\n        }\n\n        if (claimsDict.TryGetValue(Claims.ProviderServiceUser, out var providerServiceUserClaim))\n        {\n            providers.AddRange(providerServiceUserClaim.Select(c =>\n                new CurrentContextProvider\n                {\n                    Id = new Guid(c.Value),\n                    Type = ProviderUserType.ServiceUser\n                }));\n        }\n\n        return providers;\n    }\n\n    public async Task<bool> OrganizationUser(Guid orgId)\n    {\n        return (Organizations?.Any(o => o.Id == orgId) ?? false) || await OrganizationOwner(orgId);\n    }\n\n    public async Task<bool> OrganizationAdmin(Guid orgId)\n    {\n        return await OrganizationOwner(orgId) ||\n               (Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Admin) ?? false);\n    }\n\n    public async Task<bool> OrganizationOwner(Guid orgId)\n    {\n        if (Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Owner) ?? false)\n        {\n            return true;\n        }\n\n        if (Providers.Any())\n        {\n            return await ProviderUserForOrgAsync(orgId);\n        }\n\n        return false;\n    }\n\n    public Task<bool> OrganizationCustom(Guid orgId)\n    {\n        return Task.FromResult(Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Custom) ?? false);\n    }\n\n    public async Task<bool> AccessEventLogs(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.AccessEventLogs ?? false)) ?? false);\n    }\n\n    public async Task<bool> AccessImportExport(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.AccessImportExport ?? false)) ?? false);\n    }\n\n    public async Task<bool> AccessReports(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.AccessReports ?? false)) ?? false);\n    }\n\n    public async Task<bool> EditAnyCollection(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.EditAnyCollection ?? false)) ?? false);\n    }\n\n    public async Task<bool> ViewAllCollections(Guid orgId)\n    {\n        var org = GetOrganization(orgId);\n        return await EditAnyCollection(orgId) || (org != null && org.Permissions.DeleteAnyCollection);\n    }\n\n    public async Task<bool> ManageGroups(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.ManageGroups ?? false)) ?? false);\n    }\n\n    public async Task<bool> ManagePolicies(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.ManagePolicies ?? false)) ?? false);\n    }\n\n    public async Task<bool> ManageSso(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.ManageSso ?? false)) ?? false);\n    }\n\n    public async Task<bool> ManageScim(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.ManageScim ?? false)) ?? false);\n    }\n\n    public async Task<bool> ManageUsers(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.ManageUsers ?? false)) ?? false);\n    }\n\n    public async Task<bool> ManageResetPassword(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId\n                    && (o.Permissions?.ManageResetPassword ?? false)) ?? false);\n    }\n\n    public async Task<bool> ViewSubscription(Guid orgId)\n    {\n        var isManagedByBillableProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId && po.ProviderType.SupportsConsolidatedBilling());\n\n        return isManagedByBillableProvider\n            ? await ProviderUserForOrgAsync(orgId)\n            : await OrganizationOwner(orgId);\n    }\n\n    public async Task<bool> EditSubscription(Guid orgId)\n    {\n        var orgManagedByProvider = (await GetOrganizationProviderDetails()).Any(po => po.OrganizationId == orgId);\n\n        return orgManagedByProvider\n            ? await ProviderUserForOrgAsync(orgId)\n            : await OrganizationOwner(orgId);\n    }\n\n    public async Task<bool> EditPaymentMethods(Guid orgId)\n    {\n        return await EditSubscription(orgId);\n    }\n\n    public async Task<bool> ViewBillingHistory(Guid orgId)\n    {\n        return await EditSubscription(orgId);\n    }\n\n    public async Task<bool> AccessMembersTab(Guid orgId)\n    {\n        return await OrganizationAdmin(orgId) || await ManageUsers(orgId) || await ManageResetPassword(orgId);\n    }\n\n    public bool ProviderProviderAdmin(Guid providerId)\n    {\n        return Providers?.Any(o => o.Id == providerId && o.Type == ProviderUserType.ProviderAdmin) ?? false;\n    }\n\n    public bool ProviderManageUsers(Guid providerId)\n    {\n        return ProviderProviderAdmin(providerId);\n    }\n\n    public bool ProviderAccessEventLogs(Guid providerId)\n    {\n        return ProviderProviderAdmin(providerId);\n    }\n\n    public bool AccessProviderOrganizations(Guid providerId)\n    {\n        return ProviderUser(providerId);\n    }\n\n    public bool ManageProviderOrganizations(Guid providerId)\n    {\n        return ProviderProviderAdmin(providerId);\n    }\n\n    public bool ProviderUser(Guid providerId)\n    {\n        return Providers?.Any(o => o.Id == providerId) ?? false;\n    }\n\n    public async Task<bool> ProviderUserForOrgAsync(Guid orgId)\n    {\n        return (await GetProviderUserOrganizations()).Any(po => po.OrganizationId == orgId);\n    }\n\n    public async Task<Guid?> ProviderIdForOrg(Guid orgId)\n    {\n        if (Organizations?.Any(org => org.Id == orgId) ?? false)\n        {\n            return null;\n        }\n\n        var po = (await GetProviderUserOrganizations())\n            ?.FirstOrDefault(po => po.OrganizationId == orgId);\n\n        return po?.ProviderId;\n    }\n\n    public bool AccessSecretsManager(Guid orgId)\n    {\n        if (ServiceAccountOrganizationId.HasValue && ServiceAccountOrganizationId.Value == orgId)\n        {\n            return true;\n        }\n\n        return Organizations?.Any(o => o.Id == orgId && o.AccessSecretsManager) ?? false;\n    }\n\n    public async Task<ICollection<CurrentContextOrganization>> OrganizationMembershipAsync(\n        IOrganizationUserRepository organizationUserRepository, Guid userId)\n    {\n        if (Organizations == null)\n        {\n            // If we haven't had our user id set, take the one passed in since we are about to get information\n            // for them anyways.\n            UserId ??= userId;\n\n            var userOrgs = await organizationUserRepository.GetManyDetailsByUserAsync(userId);\n            Organizations = userOrgs.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed)\n                .Select(ou => new CurrentContextOrganization(ou)).ToList();\n        }\n        return Organizations;\n    }\n\n    public async Task<ICollection<CurrentContextProvider>> ProviderMembershipAsync(\n        IProviderUserRepository providerUserRepository, Guid userId)\n    {\n        if (Providers == null)\n        {\n            // If we haven't had our user id set, take the one passed in since we are about to get information\n            // for them anyways.\n            UserId ??= userId;\n\n            var userProviders = await providerUserRepository.GetManyByUserAsync(userId);\n            Providers = userProviders.Where(ou => ou.Status == ProviderUserStatusType.Confirmed)\n                .Select(ou => new CurrentContextProvider(ou)).ToList();\n        }\n        return Providers;\n    }\n\n    public CurrentContextOrganization GetOrganization(Guid orgId)\n    {\n        return Organizations?.Find(o => o.Id == orgId);\n    }\n\n    private string GetClaimValue(Dictionary<string, IEnumerable<Claim>> claims, string type)\n    {\n        if (!claims.TryGetValue(type, out var claim))\n        {\n            return null;\n        }\n\n        return claim.FirstOrDefault()?.Value;\n    }\n\n    private Permissions SetOrganizationPermissionsFromClaims(string organizationId, Dictionary<string, IEnumerable<Claim>> claimsDict)\n    {\n        bool hasClaim(string claimKey)\n        {\n            return claimsDict.TryGetValue(claimKey, out var claim) ?\n                claim.Any(x => x.Value == organizationId) : false;\n        }\n\n        return new Permissions\n        {\n            AccessEventLogs = hasClaim(\"accesseventlogs\"),\n            AccessImportExport = hasClaim(\"accessimportexport\"),\n            AccessReports = hasClaim(\"accessreports\"),\n            CreateNewCollections = hasClaim(\"createnewcollections\"),\n            EditAnyCollection = hasClaim(\"editanycollection\"),\n            DeleteAnyCollection = hasClaim(\"deleteanycollection\"),\n            ManageGroups = hasClaim(\"managegroups\"),\n            ManagePolicies = hasClaim(\"managepolicies\"),\n            ManageSso = hasClaim(\"managesso\"),\n            ManageUsers = hasClaim(\"manageusers\"),\n            ManageResetPassword = hasClaim(\"manageresetpassword\"),\n            ManageScim = hasClaim(\"managescim\"),\n        };\n    }\n\n    protected async Task<IEnumerable<ProviderUserOrganizationDetails>> GetProviderUserOrganizations()\n    {\n        if (_providerUserOrganizations == null && UserId.HasValue)\n        {\n            _providerUserOrganizations = await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(UserId.Value, ProviderUserStatusType.Confirmed);\n        }\n\n        return _providerUserOrganizations;\n    }\n\n    protected async Task<IEnumerable<ProviderOrganizationProviderDetails>> GetOrganizationProviderDetails()\n    {\n        if (_providerOrganizationProviderDetails == null && UserId.HasValue)\n        {\n            _providerOrganizationProviderDetails = await _providerOrganizationRepository.GetManyByUserAsync(UserId.Value);\n        }\n\n        return _providerOrganizationProviderDetails;\n    }\n}\n"
  },
  {
    "path": "src/Core/Context/ICurrentContext.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.Context;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Bit.Core.Context;\n\n/// <summary>\n/// Provides information about the current HTTP request and the currently authenticated user (if any).\n/// This is often (but not exclusively) parsed from the JWT in the current request.\n/// </summary>\n/// <remarks>\n/// This interface suffers from having too much responsibility; consider whether any new code can go in a more\n/// specific class rather than adding it here.\n/// </remarks>\npublic interface ICurrentContext\n{\n    HttpContext HttpContext { get; set; }\n    Guid? UserId { get; set; }\n    User User { get; set; }\n    string DeviceIdentifier { get; set; }\n    DeviceType? DeviceType { get; set; }\n    string IpAddress { get; set; }\n    string CountryName { get; set; }\n    List<CurrentContextOrganization> Organizations { get; set; }\n    Guid? InstallationId { get; set; }\n    Guid? OrganizationId { get; set; }\n    IdentityClientType IdentityClientType { get; set; }\n    string ClientId { get; set; }\n    Version? ClientVersion { get; set; }\n    bool ClientVersionIsPrerelease { get; set; }\n\n    Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings);\n    Task BuildAsync(ClaimsPrincipal user, GlobalSettings globalSettings);\n\n    Task SetContextAsync(ClaimsPrincipal user);\n\n    Task<bool> OrganizationUser(Guid orgId);\n    Task<bool> OrganizationAdmin(Guid orgId);\n    Task<bool> OrganizationOwner(Guid orgId);\n    Task<bool> OrganizationCustom(Guid orgId);\n    Task<bool> AccessEventLogs(Guid orgId);\n    Task<bool> AccessImportExport(Guid orgId);\n    Task<bool> AccessReports(Guid orgId);\n    [Obsolete(\"Deprecated. Use an authorization handler checking the specific permissions required instead.\")]\n    Task<bool> EditAnyCollection(Guid orgId);\n    [Obsolete(\"Deprecated. Use an authorization handler checking the specific permissions required instead.\")]\n    Task<bool> ViewAllCollections(Guid orgId);\n    Task<bool> ManageGroups(Guid orgId);\n    Task<bool> ManagePolicies(Guid orgId);\n    Task<bool> ManageSso(Guid orgId);\n    Task<bool> ManageUsers(Guid orgId);\n    Task<bool> AccessMembersTab(Guid orgId);\n    Task<bool> ManageScim(Guid orgId);\n    Task<bool> ManageResetPassword(Guid orgId);\n    Task<bool> ViewSubscription(Guid orgId);\n    Task<bool> EditSubscription(Guid orgId);\n    Task<bool> EditPaymentMethods(Guid orgId);\n    Task<bool> ViewBillingHistory(Guid orgId);\n    /// <summary>\n    /// Returns true if the current user is a member of a provider that manages the specified organization.\n    /// This generally gives the user administrative privileges for the organization.\n    /// </summary>\n    /// <param name=\"orgId\"></param>\n    /// <returns></returns>\n    Task<bool> ProviderUserForOrgAsync(Guid orgId);\n    /// <summary>\n    /// Returns true if the current user is a Provider Admin of the specified provider.\n    /// </summary>\n    bool ProviderProviderAdmin(Guid providerId);\n    /// <summary>\n    /// Returns true if the current user is a member of the specified provider (with any role).\n    /// </summary>\n    bool ProviderUser(Guid providerId);\n    bool ProviderManageUsers(Guid providerId);\n    bool ProviderAccessEventLogs(Guid providerId);\n    bool AccessProviderOrganizations(Guid providerId);\n    bool ManageProviderOrganizations(Guid providerId);\n\n    Task<ICollection<CurrentContextOrganization>> OrganizationMembershipAsync(\n        IOrganizationUserRepository organizationUserRepository, Guid userId);\n\n    Task<ICollection<CurrentContextProvider>> ProviderMembershipAsync(\n        IProviderUserRepository providerUserRepository, Guid userId);\n\n    Task<Guid?> ProviderIdForOrg(Guid orgId);\n    bool AccessSecretsManager(Guid organizationId);\n    CurrentContextOrganization? GetOrganization(Guid orgId);\n}\n"
  },
  {
    "path": "src/Core/Core.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>\n    <DocumentationFile>bin\\$(Configuration)\\$(TargetFramework)\\$(AssemblyName).xml</DocumentationFile>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304;CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|AnyCPU'\">\n    <NoWarn>1701;1702;1591;1573</NoWarn>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|AnyCPU'\">\n    <NoWarn>1701;1702;1591;1573</NoWarn>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <EmbeddedResource Include=\"licensing.cer\" />\n    <EmbeddedResource Include=\"licensing_dev.cer\" />\n\n    <!-- Email templates uses .hbs extension, they must be included for emails to work -->\n    <EmbeddedResource Include=\"**\\*.hbs\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"AspNetCoreRateLimit.Redis\" Version=\"2.0.0\" />\n    <PackageReference Include=\"AWSSDK.SimpleEmail\" Version=\"4.0.2.5\" />\n    <PackageReference Include=\"AWSSDK.SQS\" Version=\"4.0.2.5\" />\n    <PackageReference Include=\"Azure.Data.Tables\" Version=\"12.11.0\" />\n    <PackageReference Include=\"Azure.Extensions.AspNetCore.DataProtection.Blobs\" Version=\"1.3.4\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.DataProtection\" Version=\"8.0.10\" />\n    <PackageReference Include=\"Azure.Messaging.ServiceBus\" Version=\"7.20.1\" />\n    <PackageReference Include=\"Azure.Storage.Blobs\" Version=\"12.26.0\" />\n    <PackageReference Include=\"Azure.Storage.Queues\" Version=\"12.24.0\" />\n    <PackageReference Include=\"BitPay.Light\" Version=\"1.0.1907\" />\n    <PackageReference Include=\"DuoUniversal\" Version=\"1.3.1\" />\n    <PackageReference Include=\"DnsClient\" Version=\"1.8.0\" />\n    <PackageReference Include=\"Fido2.AspNet\" Version=\"3.0.1\" />\n    <PackageReference Include=\"Handlebars.Net\" Version=\"2.1.6\" />\n    <PackageReference Include=\"MailKit\" Version=\"4.15.0\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.Authentication.JwtBearer\" Version=\"8.0.10\" />\n    <PackageReference Include=\"Microsoft.Azure.Cosmos\" Version=\"3.52.0\" />\n    <PackageReference Include=\"Microsoft.Azure.NotificationHubs\" Version=\"4.2.0\" />\n    <PackageReference Include=\"Microsoft.Bot.Builder\" Version=\"4.23.0\" />\n    <PackageReference Include=\"Microsoft.Bot.Builder.Integration.AspNet.Core\" Version=\"4.23.0\" />\n    <PackageReference Include=\"Microsoft.Bot.Connector\" Version=\"4.23.0\" />\n    <PackageReference Include=\"Microsoft.Data.SqlClient\" Version=\"5.2.2\" />\n    <PackageReference Include=\"Microsoft.Extensions.Caching.Cosmos\" Version=\"1.8.0\" />\n    <PackageReference Include=\"Microsoft.Extensions.Caching.SqlServer\" Version=\"8.0.10\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.EnvironmentVariables\" Version=\"8.0.0\" />\n    <PackageReference Include=\"Microsoft.Extensions.Configuration.UserSecrets\" Version=\"8.0.0\" />\n    <PackageReference Include=\"Microsoft.Extensions.Identity.Stores\" Version=\"8.0.10\" />\n    <PackageReference Include=\"OneOf\" Version=\"3.0.271\" />\n    <PackageReference Include=\"SendGrid\" Version=\"9.29.3\" />\n    <PackageReference Include=\"Serilog.Extensions.Logging.File\" Version=\"3.0.0\" />\n    <PackageReference Include=\"Duende.IdentityServer\" Version=\"7.2.4\" />\n    <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.3\" />\n    <PackageReference Include=\"AspNetCoreRateLimit\" Version=\"5.0.0\" />\n    <PackageReference Include=\"Braintree\" Version=\"5.36.0\" />\n    <PackageReference Include=\"Stripe.net\" Version=\"48.5.0\" />\n    <PackageReference Include=\"Otp.NET\" Version=\"1.4.0\" />\n    <PackageReference Include=\"YubicoDotNetClient\" Version=\"1.2.0\" />\n    <PackageReference Include=\"Microsoft.Extensions.Caching.StackExchangeRedis\" Version=\"8.0.10\" />\n    <PackageReference Include=\"LaunchDarkly.ServerSdk\" Version=\"8.11.0\" />\n    <PackageReference Include=\"Quartz\" Version=\"3.15.1\" />\n    <PackageReference Include=\"Quartz.Extensions.Hosting\" Version=\"3.15.1\" />\n    <PackageReference Include=\"Quartz.Extensions.DependencyInjection\" Version=\"3.15.1\" />\n    <PackageReference Include=\"RabbitMQ.Client\" Version=\"7.1.2\" />\n    <PackageReference Include=\"ZiggyCreatures.FusionCache\" Version=\"2.0.2\" />\n    <PackageReference Include=\"ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis\" Version=\"2.0.2\" />\n    <PackageReference Include=\"ZiggyCreatures.FusionCache.Serialization.SystemTextJson\" Version=\"2.0.2\" />\n  </ItemGroup>\n\n  <ItemGroup Label=\"Pinned transitive dependencies\">\n    <PackageReference Include=\"System.Text.Json\" Version=\"8.0.5\" />\n    <PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"8.0.1\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Folder Include=\"Resources\\\" />\n    <Folder Include=\"Properties\\\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <InternalsVisibleTo Include=\"Infrastructure.IntegrationTest\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Core/Dirt/Entities/Event.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic class Event : ITableObject<Guid>, IEvent\n{\n    public Event() { }\n\n    public Event(IEvent e)\n    {\n        Date = e.Date;\n        Type = e.Type;\n        UserId = e.UserId;\n        OrganizationId = e.OrganizationId;\n        ProviderId = e.ProviderId;\n        CipherId = e.CipherId;\n        CollectionId = e.CollectionId;\n        PolicyId = e.PolicyId;\n        GroupId = e.GroupId;\n        OrganizationUserId = e.OrganizationUserId;\n        InstallationId = e.InstallationId;\n        ProviderUserId = e.ProviderUserId;\n        ProviderOrganizationId = e.ProviderOrganizationId;\n        DeviceType = e.DeviceType;\n        IpAddress = e.IpAddress;\n        ActingUserId = e.ActingUserId;\n        SystemUser = e.SystemUser;\n        DomainName = e.DomainName;\n        SecretId = e.SecretId;\n        ProjectId = e.ProjectId;\n        ServiceAccountId = e.ServiceAccountId;\n        GrantedServiceAccountId = e.GrantedServiceAccountId;\n    }\n\n    public Guid Id { get; set; }\n    public DateTime Date { get; set; }\n    public EventType Type { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Guid? InstallationId { get; set; }\n    public Guid? ProviderId { get; set; }\n    public Guid? CipherId { get; set; }\n    public Guid? CollectionId { get; set; }\n    public Guid? PolicyId { get; set; }\n    public Guid? GroupId { get; set; }\n    public Guid? OrganizationUserId { get; set; }\n    public Guid? ProviderUserId { get; set; }\n    public Guid? ProviderOrganizationId { get; set; }\n    public DeviceType? DeviceType { get; set; }\n    [MaxLength(50)]\n    public string? IpAddress { get; set; }\n    public Guid? ActingUserId { get; set; }\n    public EventSystemUser? SystemUser { get; set; }\n    public string? DomainName { get; set; }\n    public Guid? SecretId { get; set; }\n    public Guid? ProjectId { get; set; }\n    public Guid? ServiceAccountId { get; set; }\n    public Guid? GrantedServiceAccountId { get; set; }\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Entities/OrganizationApplication.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Dirt.Entities;\n\npublic class OrganizationApplication : ITableObject<Guid>, IRevisable\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string Applications { get; set; } = string.Empty;\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n    public string ContentEncryptionKey { get; set; } = string.Empty;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Entities/OrganizationIntegration.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Dirt.Entities;\n\npublic class OrganizationIntegration : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public IntegrationType Type { get; set; }\n    public string? Configuration { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n    public void SetNewId() => Id = CoreHelpers.GenerateComb();\n}\n"
  },
  {
    "path": "src/Core/Dirt/Entities/OrganizationIntegrationConfiguration.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Dirt.Entities;\n\npublic class OrganizationIntegrationConfiguration : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationIntegrationId { get; set; }\n    public EventType? EventType { get; set; }\n    public string? Configuration { get; set; }\n    public string? Template { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n    public string? Filters { get; set; }\n    public void SetNewId() => Id = CoreHelpers.GenerateComb();\n}\n"
  },
  {
    "path": "src/Core/Dirt/Entities/OrganizationReport.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Dirt.Entities;\n\npublic class OrganizationReport : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string ReportData { get; set; } = string.Empty;\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public string ContentEncryptionKey { get; set; } = string.Empty;\n    public string? SummaryData { get; set; }\n    public string? ApplicationData { get; set; }\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n    public int? ApplicationCount { get; set; }\n    public int? ApplicationAtRiskCount { get; set; }\n    public int? CriticalApplicationCount { get; set; }\n    public int? CriticalApplicationAtRiskCount { get; set; }\n    public int? MemberCount { get; set; }\n    public int? MemberAtRiskCount { get; set; }\n    public int? CriticalMemberCount { get; set; }\n    public int? CriticalMemberAtRiskCount { get; set; }\n    public int? PasswordCount { get; set; }\n    public int? PasswordAtRiskCount { get; set; }\n    public int? CriticalPasswordCount { get; set; }\n    public int? CriticalPasswordAtRiskCount { get; set; }\n    public string? ReportFile { get; set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Entities/PasswordHealthReportApplication.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Dirt.Entities;\n\npublic class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string? Uri { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Enums/EventSystemUser.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum EventSystemUser : byte\n{\n    Unknown = 0,\n    SCIM = 1,\n    DomainVerification = 2,\n    PublicApi = 3,\n    TwoFactorDisabled = 4,\n    BitwardenPortal = 5,\n}\n"
  },
  {
    "path": "src/Core/Dirt/Enums/EventType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\n// Increment by 100 for each new set of events\npublic enum EventType : int\n{\n    User_LoggedIn = 1000,\n    User_ChangedPassword = 1001,\n    User_Updated2fa = 1002,\n    User_Disabled2fa = 1003,\n    User_Recovered2fa = 1004,\n    User_FailedLogIn = 1005,\n    User_FailedLogIn2fa = 1006,\n    User_ClientExportedVault = 1007,\n    User_UpdatedTempPassword = 1008,\n    User_MigratedKeyToKeyConnector = 1009,\n    User_RequestedDeviceApproval = 1010,\n    User_TdeOffboardingPasswordSet = 1011,\n\n    Cipher_Created = 1100,\n    Cipher_Updated = 1101,\n    Cipher_Deleted = 1102,\n    Cipher_AttachmentCreated = 1103,\n    Cipher_AttachmentDeleted = 1104,\n    Cipher_Shared = 1105,\n    Cipher_UpdatedCollections = 1106,\n    Cipher_ClientViewed = 1107,\n    Cipher_ClientToggledPasswordVisible = 1108,\n    Cipher_ClientToggledHiddenFieldVisible = 1109,\n    Cipher_ClientToggledCardCodeVisible = 1110,\n    Cipher_ClientCopiedPassword = 1111,\n    Cipher_ClientCopiedHiddenField = 1112,\n    Cipher_ClientCopiedCardCode = 1113,\n    Cipher_ClientAutofilled = 1114,\n    Cipher_SoftDeleted = 1115,\n    Cipher_Restored = 1116,\n    Cipher_ClientToggledCardNumberVisible = 1117,\n\n    Collection_Created = 1300,\n    Collection_Updated = 1301,\n    Collection_Deleted = 1302,\n\n    Group_Created = 1400,\n    Group_Updated = 1401,\n    Group_Deleted = 1402,\n\n    OrganizationUser_Invited = 1500,\n    OrganizationUser_Confirmed = 1501,\n    OrganizationUser_Updated = 1502,\n    OrganizationUser_Removed = 1503, // Organization user data was deleted\n    OrganizationUser_UpdatedGroups = 1504,\n    OrganizationUser_UnlinkedSso = 1505,\n    OrganizationUser_ResetPassword_Enroll = 1506,\n    OrganizationUser_ResetPassword_Withdraw = 1507,\n    OrganizationUser_AdminResetPassword = 1508,\n    OrganizationUser_ResetSsoLink = 1509,\n    OrganizationUser_FirstSsoLogin = 1510,\n    OrganizationUser_Revoked = 1511,\n    OrganizationUser_Restored = 1512,\n    OrganizationUser_ApprovedAuthRequest = 1513,\n    OrganizationUser_RejectedAuthRequest = 1514,\n    OrganizationUser_Deleted = 1515, // Both user and organization user data were deleted\n    OrganizationUser_Left = 1516,    // User voluntarily left the organization\n    OrganizationUser_AutomaticallyConfirmed = 1517,\n    OrganizationUser_SelfRevoked = 1518, // User self-revoked due to declining organization data ownership policy\n\n    Organization_Updated = 1600,\n    Organization_PurgedVault = 1601,\n    Organization_ClientExportedVault = 1602,\n    Organization_VaultAccessed = 1603,\n    Organization_EnabledSso = 1604,\n    Organization_DisabledSso = 1605,\n    Organization_EnabledKeyConnector = 1606,\n    Organization_DisabledKeyConnector = 1607,\n    Organization_SponsorshipsSynced = 1608,\n    [Obsolete(\"Kept for historical data. Use specific Organization_CollectionManagement events instead.\")]\n    Organization_CollectionManagement_Updated = 1609,\n    Organization_CollectionManagement_LimitCollectionCreationEnabled = 1610,\n    Organization_CollectionManagement_LimitCollectionCreationDisabled = 1611,\n    Organization_CollectionManagement_LimitCollectionDeletionEnabled = 1612,\n    Organization_CollectionManagement_LimitCollectionDeletionDisabled = 1613,\n    Organization_CollectionManagement_LimitItemDeletionEnabled = 1614,\n    Organization_CollectionManagement_LimitItemDeletionDisabled = 1615,\n    Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled = 1616,\n    Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled = 1617,\n    Organization_ItemOrganization_Accepted = 1618,\n    Organization_ItemOrganization_Declined = 1619,\n    Organization_AutoConfirmEnabled_Admin = 1620,\n    Organization_AutoConfirmDisabled_Admin = 1621,\n    Organization_AutoConfirmEnabled_Portal = 1622,\n    Organization_AutoConfirmDisabled_Portal = 1623,\n\n    Policy_Updated = 1700,\n\n    ProviderUser_Invited = 1800,\n    ProviderUser_Confirmed = 1801,\n    ProviderUser_Updated = 1802,\n    ProviderUser_Removed = 1803,\n\n    ProviderOrganization_Created = 1900,\n    ProviderOrganization_Added = 1901,\n    ProviderOrganization_Removed = 1902,\n    ProviderOrganization_VaultAccessed = 1903,\n\n    OrganizationDomain_Added = 2000,\n    OrganizationDomain_Removed = 2001,\n    OrganizationDomain_Verified = 2002,\n    OrganizationDomain_NotVerified = 2003,\n\n    Secret_Retrieved = 2100,\n    Secret_Created = 2101,\n    Secret_Edited = 2102,\n    Secret_Deleted = 2103,\n    Secret_Permanently_Deleted = 2104,\n    Secret_Restored = 2105,\n\n    Project_Retrieved = 2200,\n    Project_Created = 2201,\n    Project_Edited = 2202,\n    Project_Deleted = 2203,\n\n    ServiceAccount_UserAdded = 2300,\n    ServiceAccount_UserRemoved = 2301,\n    ServiceAccount_GroupAdded = 2302,\n    ServiceAccount_GroupRemoved = 2303,\n    ServiceAccount_Created = 2304,\n    ServiceAccount_Deleted = 2305,\n}\n"
  },
  {
    "path": "src/Core/Dirt/Enums/IntegrationType.cs",
    "content": "﻿namespace Bit.Core.Dirt.Enums;\n\npublic enum IntegrationType : int\n{\n    CloudBillingSync = 1,\n    Scim = 2,\n    Slack = 3,\n    Webhook = 4,\n    Hec = 5,\n    Datadog = 6,\n    Teams = 7\n}\n\npublic static class IntegrationTypeExtensions\n{\n    public static string ToRoutingKey(this IntegrationType type)\n    {\n        switch (type)\n        {\n            case IntegrationType.Slack:\n                return \"slack\";\n            case IntegrationType.Webhook:\n                return \"webhook\";\n            case IntegrationType.Hec:\n                return \"hec\";\n            case IntegrationType.Datadog:\n                return \"datadog\";\n            case IntegrationType.Teams:\n                return \"teams\";\n            default:\n                throw new ArgumentOutOfRangeException(nameof(type), $\"Unsupported integration type: {type}\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Enums/OrganizationIntegrationStatus.cs",
    "content": "﻿namespace Bit.Core.Dirt.Enums;\n\npublic enum OrganizationIntegrationStatus : int\n{\n    NotApplicable,\n    Invalid,\n    Initiated,\n    InProgress,\n    Completed\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/EventIntegrationsServiceCollectionExtensions.cs",
    "content": "﻿using Azure.Messaging.ServiceBus;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Models.Data.Teams;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Dirt.Services.NoopImplementations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Bot.Builder;\nusing Microsoft.Bot.Builder.Integration.AspNet.Core;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing ZiggyCreatures.Caching.Fusion;\nusing TableStorageRepos = Bit.Core.Repositories.TableStorage;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\npublic static class EventIntegrationsServiceCollectionExtensions\n{\n    /// <summary>\n    /// Adds all event integrations commands, queries, and required cache infrastructure.\n    /// This method is idempotent and can be called multiple times safely.\n    /// </summary>\n    public static IServiceCollection AddEventIntegrationsCommandsQueries(\n        this IServiceCollection services,\n        GlobalSettings globalSettings)\n    {\n        // Ensure cache is registered first - commands depend on this keyed cache.\n        // This is idempotent for the same named cache, so it's safe to call.\n        services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);\n\n        // Add Validator\n        services.TryAddSingleton<IOrganizationIntegrationConfigurationValidator, OrganizationIntegrationConfigurationValidator>();\n\n        // Add all commands/queries\n        services.AddOrganizationIntegrationCommandsQueries();\n        services.AddOrganizationIntegrationConfigurationCommandsQueries();\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers event write services based on available configuration.\n    /// </summary>\n    /// <param name=\"services\">The service collection to add services to.</param>\n    /// <param name=\"globalSettings\">The global settings containing event logging configuration.</param>\n    /// <returns>The service collection for chaining.</returns>\n    /// <remarks>\n    /// <para>\n    /// This method registers the appropriate IEventWriteService implementation based on the available\n    /// configuration, checking in the following priority order:\n    /// </para>\n    /// <para>\n    /// 1. Azure Service Bus - If all Azure Service Bus settings are present, registers\n    /// EventIntegrationEventWriteService with AzureServiceBusService as the publisher\n    /// </para>\n    /// <para>\n    /// 2. RabbitMQ - If all RabbitMQ settings are present, registers EventIntegrationEventWriteService with\n    /// RabbitMqService as the publisher\n    /// </para>\n    /// <para>\n    /// 3. Azure Queue Storage - If Events.ConnectionString is present, registers AzureQueueEventWriteService\n    /// </para>\n    /// <para>\n    /// 4. Repository (Self-Hosted) - If SelfHosted is true, registers RepositoryEventWriteService\n    /// </para>\n    /// <para>\n    /// 5. Noop - If none of the above are configured, registers NoopEventWriteService (no-op implementation)\n    /// </para>\n    /// </remarks>\n    public static IServiceCollection AddEventWriteServices(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        if (IsAzureServiceBusEnabled(globalSettings))\n        {\n            services.TryAddSingleton<IEventIntegrationPublisher, AzureServiceBusService>();\n            services.TryAddSingleton<IEventWriteService, EventIntegrationEventWriteService>();\n            return services;\n        }\n\n        if (IsRabbitMqEnabled(globalSettings))\n        {\n            services.TryAddSingleton<IEventIntegrationPublisher, RabbitMqService>();\n            services.TryAddSingleton<IEventWriteService, EventIntegrationEventWriteService>();\n            return services;\n        }\n\n        if (CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString) &&\n            CoreHelpers.SettingHasValue(globalSettings.Events.QueueName))\n        {\n            services.TryAddSingleton<IEventWriteService, AzureQueueEventWriteService>();\n            return services;\n        }\n\n        if (globalSettings.SelfHosted)\n        {\n            services.TryAddSingleton<IEventWriteService, RepositoryEventWriteService>();\n            return services;\n        }\n\n        services.TryAddSingleton<IEventWriteService, NoopEventWriteService>();\n        return services;\n    }\n\n    /// <summary>\n    /// Registers Azure Service Bus-based event integration listeners and supporting infrastructure.\n    /// </summary>\n    /// <param name=\"services\">The service collection to add services to.</param>\n    /// <param name=\"globalSettings\">The global settings containing Azure Service Bus configuration.</param>\n    /// <returns>The service collection for chaining.</returns>\n    /// <remarks>\n    /// <para>\n    /// If Azure Service Bus is not enabled (missing required settings), this method returns immediately\n    /// without registering any services.\n    /// </para>\n    /// <para>\n    /// When Azure Service Bus is enabled, this method registers:\n    /// - IAzureServiceBusService and IEventIntegrationPublisher implementations\n    /// - Table Storage event repository\n    /// - Azure Table Storage event handler\n    /// - All event integration services via AddEventIntegrationServices\n    /// </para>\n    /// <para>\n    /// PREREQUISITE: Callers must ensure AddDistributedCache has been called before this method,\n    /// as it is required to create the event integrations extended cache.\n    /// </para>\n    /// </remarks>\n    public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        if (!IsAzureServiceBusEnabled(globalSettings))\n        {\n            return services;\n        }\n\n        services.TryAddSingleton<IAzureServiceBusService, AzureServiceBusService>();\n        services.TryAddSingleton<IEventIntegrationPublisher, AzureServiceBusService>();\n        services.TryAddSingleton<IEventRepository, TableStorageRepos.EventRepository>();\n        services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>(\"persistent\");\n        services.TryAddSingleton<AzureTableStorageEventHandler>();\n\n        services.AddEventIntegrationServices(globalSettings);\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers RabbitMQ-based event integration listeners and supporting infrastructure.\n    /// </summary>\n    /// <param name=\"services\">The service collection to add services to.</param>\n    /// <param name=\"globalSettings\">The global settings containing RabbitMQ configuration.</param>\n    /// <returns>The service collection for chaining.</returns>\n    /// <remarks>\n    /// <para>\n    /// If RabbitMQ is not enabled (missing required settings), this method returns immediately\n    /// without registering any services.\n    /// </para>\n    /// <para>\n    /// When RabbitMQ is enabled, this method registers:\n    /// - IRabbitMqService and IEventIntegrationPublisher implementations\n    /// - Event repository handler\n    /// - All event integration services via AddEventIntegrationServices\n    /// </para>\n    /// <para>\n    /// PREREQUISITE: Callers must ensure AddDistributedCache has been called before this method,\n    /// as it is required to create the event integrations extended cache.\n    /// </para>\n    /// </remarks>\n    public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        if (!IsRabbitMqEnabled(globalSettings))\n        {\n            return services;\n        }\n\n        services.TryAddSingleton<IRabbitMqService, RabbitMqService>();\n        services.TryAddSingleton<IEventIntegrationPublisher, RabbitMqService>();\n        services.TryAddSingleton<EventRepositoryHandler>();\n\n        services.AddEventIntegrationServices(globalSettings);\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers Slack integration services based on configuration settings.\n    /// </summary>\n    /// <param name=\"services\">The service collection to add services to.</param>\n    /// <param name=\"globalSettings\">The global settings containing Slack configuration.</param>\n    /// <returns>The service collection for chaining.</returns>\n    /// <remarks>\n    /// If all required Slack settings are configured (ClientId, ClientSecret, Scopes), registers the full SlackService,\n    /// including an HttpClient for Slack API calls. Otherwise, registers a NoopSlackService that performs no operations.\n    /// </remarks>\n    public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&\n            CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&\n            CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))\n        {\n            services.AddHttpClient(SlackService.HttpClientName);\n            services.TryAddSingleton<ISlackService, SlackService>();\n        }\n        else\n        {\n            services.TryAddSingleton<ISlackService, NoopSlackService>();\n        }\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers Microsoft Teams integration services based on configuration settings.\n    /// </summary>\n    /// <param name=\"services\">The service collection to add services to.</param>\n    /// <param name=\"globalSettings\">The global settings containing Teams configuration.</param>\n    /// <returns>The service collection for chaining.</returns>\n    /// <remarks>\n    /// If all required Teams settings are configured (ClientId, ClientSecret, Scopes), registers:\n    /// - TeamsService and its interfaces (IBot, ITeamsService)\n    /// - IBotFrameworkHttpAdapter with Teams credentials\n    /// - HttpClient for Teams API calls\n    /// Otherwise, registers a NoopTeamsService that performs no operations.\n    /// </remarks>\n    public static IServiceCollection AddTeamsService(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        if (CoreHelpers.SettingHasValue(globalSettings.Teams.ClientId) &&\n            CoreHelpers.SettingHasValue(globalSettings.Teams.ClientSecret) &&\n            CoreHelpers.SettingHasValue(globalSettings.Teams.Scopes))\n        {\n            services.AddHttpClient(TeamsService.HttpClientName);\n            services.TryAddSingleton<TeamsService>();\n            services.TryAddSingleton<IBot>(sp => sp.GetRequiredService<TeamsService>());\n            services.TryAddSingleton<ITeamsService>(sp => sp.GetRequiredService<TeamsService>());\n            services.TryAddSingleton<IBotFrameworkHttpAdapter>(_ =>\n                new BotFrameworkHttpAdapter(\n                    new TeamsBotCredentialProvider(\n                        clientId: globalSettings.Teams.ClientId,\n                        clientSecret: globalSettings.Teams.ClientSecret\n                    )\n                )\n            );\n        }\n        else\n        {\n            services.TryAddSingleton<ITeamsService, NoopTeamsService>();\n        }\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers event integration services including handlers, listeners, and supporting infrastructure.\n    /// </summary>\n    /// <param name=\"services\">The service collection to add services to.</param>\n    /// <param name=\"globalSettings\">The global settings containing integration configuration.</param>\n    /// <returns>The service collection for chaining.</returns>\n    /// <remarks>\n    /// <para>\n    /// This method orchestrates the registration of all event integration components based on the enabled\n    /// message broker (Azure Service Bus or RabbitMQ). It is an internal method called by the public\n    /// entry points AddAzureServiceBusListeners and AddRabbitMqListeners.\n    /// </para>\n    /// <para>\n    /// NOTE: If both Azure Service Bus and RabbitMQ are configured, Azure Service Bus takes precedence. This means that\n    /// Azure Service Bus listeners will be registered (and RabbitMQ listeners will NOT) even if this event is called\n    /// from AddRabbitMqListeners when Azure Service Bus settings are configured.\n    /// </para>\n    /// <para>\n    /// PREREQUISITE: Callers must ensure AddDistributedCache has been called before invoking this method.\n    /// This method depends on distributed cache infrastructure being available for the keyed extended\n    /// cache registration.\n    /// </para>\n    /// <para>\n    /// Registered Services:\n    /// - Keyed ExtendedCache for event integrations\n    /// - Integration filter service\n    /// - Integration handlers for Slack, Webhook, Hec, Datadog, and Teams\n    /// - Hosted services for event and integration listeners (based on enabled message broker)\n    /// </para>\n    /// </remarks>\n    internal static IServiceCollection AddEventIntegrationServices(this IServiceCollection services,\n        GlobalSettings globalSettings)\n    {\n        // Add common services\n        // NOTE: AddDistributedCache must be called by the caller before this method\n        services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);\n        services.TryAddSingleton<IIntegrationFilterService, IntegrationFilterService>();\n        services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>(\"persistent\");\n\n        // Add services in support of handlers\n        services.AddSlackService(globalSettings);\n        services.AddTeamsService(globalSettings);\n        services.TryAddSingleton(TimeProvider.System);\n        services.AddHttpClient(WebhookIntegrationHandler.HttpClientName);\n        services.AddHttpClient(DatadogIntegrationHandler.HttpClientName);\n\n        // Add integration handlers\n        services.TryAddSingleton<IIntegrationHandler<SlackIntegrationConfigurationDetails>, SlackIntegrationHandler>();\n        services.TryAddSingleton<IIntegrationHandler<WebhookIntegrationConfigurationDetails>, WebhookIntegrationHandler>();\n        services.TryAddSingleton<IIntegrationHandler<DatadogIntegrationConfigurationDetails>, DatadogIntegrationHandler>();\n        services.TryAddSingleton<IIntegrationHandler<TeamsIntegrationConfigurationDetails>, TeamsIntegrationHandler>();\n\n        var repositoryConfiguration = new RepositoryListenerConfiguration(globalSettings);\n        var slackConfiguration = new SlackListenerConfiguration(globalSettings);\n        var webhookConfiguration = new WebhookListenerConfiguration(globalSettings);\n        var hecConfiguration = new HecListenerConfiguration(globalSettings);\n        var datadogConfiguration = new DatadogListenerConfiguration(globalSettings);\n        var teamsConfiguration = new TeamsListenerConfiguration(globalSettings);\n\n        if (IsAzureServiceBusEnabled(globalSettings))\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,\n                    AzureServiceBusEventListenerService<RepositoryListenerConfiguration>>(provider =>\n                    new AzureServiceBusEventListenerService<RepositoryListenerConfiguration>(\n                        configuration: repositoryConfiguration,\n                        handler: provider.GetRequiredService<AzureTableStorageEventHandler>(),\n                        serviceBusService: provider.GetRequiredService<IAzureServiceBusService>(),\n                        serviceBusOptions: new ServiceBusProcessorOptions()\n                        {\n                            PrefetchCount = repositoryConfiguration.EventPrefetchCount,\n                            MaxConcurrentCalls = repositoryConfiguration.EventMaxConcurrentCalls\n                        },\n                        loggerFactory: provider.GetRequiredService<ILoggerFactory>()\n                    )\n                )\n            );\n            services.AddAzureServiceBusIntegration<SlackIntegrationConfigurationDetails, SlackListenerConfiguration>(slackConfiguration);\n            services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, WebhookListenerConfiguration>(webhookConfiguration);\n            services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, HecListenerConfiguration>(hecConfiguration);\n            services.AddAzureServiceBusIntegration<DatadogIntegrationConfigurationDetails, DatadogListenerConfiguration>(datadogConfiguration);\n            services.AddAzureServiceBusIntegration<TeamsIntegrationConfigurationDetails, TeamsListenerConfiguration>(teamsConfiguration);\n\n            return services;\n        }\n\n        if (IsRabbitMqEnabled(globalSettings))\n        {\n            services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,\n                    RabbitMqEventListenerService<RepositoryListenerConfiguration>>(provider =>\n                    new RabbitMqEventListenerService<RepositoryListenerConfiguration>(\n                        handler: provider.GetRequiredService<EventRepositoryHandler>(),\n                        configuration: repositoryConfiguration,\n                        rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),\n                        loggerFactory: provider.GetRequiredService<ILoggerFactory>()\n                    )\n                )\n            );\n            services.AddRabbitMqIntegration<SlackIntegrationConfigurationDetails, SlackListenerConfiguration>(slackConfiguration);\n            services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, WebhookListenerConfiguration>(webhookConfiguration);\n            services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, HecListenerConfiguration>(hecConfiguration);\n            services.AddRabbitMqIntegration<DatadogIntegrationConfigurationDetails, DatadogListenerConfiguration>(datadogConfiguration);\n            services.AddRabbitMqIntegration<TeamsIntegrationConfigurationDetails, TeamsListenerConfiguration>(teamsConfiguration);\n        }\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers Azure Service Bus-based event integration listeners for a specific integration type.\n    /// </summary>\n    /// <typeparam name=\"TConfig\">The integration configuration details type (e.g., SlackIntegrationConfigurationDetails).</typeparam>\n    /// <typeparam name=\"TListenerConfig\">The listener configuration type implementing IIntegrationListenerConfiguration.</typeparam>\n    /// <param name=\"services\">The service collection to add services to.</param>\n    /// <param name=\"listenerConfiguration\">The listener configuration containing routing keys and message processing settings.</param>\n    /// <returns>The service collection for chaining.</returns>\n    /// <remarks>\n    /// <para>\n    /// This method registers three key components:\n    /// 1. EventIntegrationHandler - Keyed singleton for processing integration events\n    /// 2. AzureServiceBusEventListenerService - Hosted service for listening to event messages from Azure Service Bus\n    ///    for this integration type\n    /// 3. AzureServiceBusIntegrationListenerService - Hosted service for listening to integration messages from\n    ///    Azure Service Bus for this integration type\n    /// </para>\n    /// <para>\n    /// The handler uses the listener configuration's routing key as its service key, allowing multiple\n    /// handlers to be registered for different integration types.\n    /// </para>\n    /// <para>\n    /// Service Bus processor options (PrefetchCount and MaxConcurrentCalls) are configured from the listener\n    /// configuration to optimize message throughput and concurrency.\n    /// </para>\n    /// </remarks>\n    internal static IServiceCollection AddAzureServiceBusIntegration<TConfig, TListenerConfig>(this IServiceCollection services,\n        TListenerConfig listenerConfiguration)\n        where TConfig : class\n        where TListenerConfig : IIntegrationListenerConfiguration\n    {\n        services.TryAddKeyedSingleton<IEventMessageHandler>(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>\n            new EventIntegrationHandler<TConfig>(\n                integrationType: listenerConfiguration.IntegrationType,\n                eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),\n                integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),\n                cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),\n                configurationRepository: provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),\n                groupRepository: provider.GetRequiredService<IGroupRepository>(),\n                organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),\n                organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(), logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>())\n        );\n        services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,\n            AzureServiceBusEventListenerService<TListenerConfig>>(provider =>\n                new AzureServiceBusEventListenerService<TListenerConfig>(\n                    configuration: listenerConfiguration,\n                    handler: provider.GetRequiredKeyedService<IEventMessageHandler>(serviceKey: listenerConfiguration.RoutingKey),\n                    serviceBusService: provider.GetRequiredService<IAzureServiceBusService>(),\n                    serviceBusOptions: new ServiceBusProcessorOptions()\n                    {\n                        PrefetchCount = listenerConfiguration.EventPrefetchCount,\n                        MaxConcurrentCalls = listenerConfiguration.EventMaxConcurrentCalls\n                    },\n                    loggerFactory: provider.GetRequiredService<ILoggerFactory>()\n                )\n            )\n        );\n        services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,\n            AzureServiceBusIntegrationListenerService<TListenerConfig>>(provider =>\n                new AzureServiceBusIntegrationListenerService<TListenerConfig>(\n                    configuration: listenerConfiguration,\n                    handler: provider.GetRequiredService<IIntegrationHandler<TConfig>>(),\n                    serviceBusService: provider.GetRequiredService<IAzureServiceBusService>(),\n                    serviceBusOptions: new ServiceBusProcessorOptions()\n                    {\n                        PrefetchCount = listenerConfiguration.IntegrationPrefetchCount,\n                        MaxConcurrentCalls = listenerConfiguration.IntegrationMaxConcurrentCalls\n                    },\n                    loggerFactory: provider.GetRequiredService<ILoggerFactory>()\n                )\n            )\n        );\n\n        return services;\n    }\n\n    /// <summary>\n    /// Registers RabbitMQ-based event integration listeners for a specific integration type.\n    /// </summary>\n    /// <typeparam name=\"TConfig\">The integration configuration details type (e.g., SlackIntegrationConfigurationDetails).</typeparam>\n    /// <typeparam name=\"TListenerConfig\">The listener configuration type implementing IIntegrationListenerConfiguration.</typeparam>\n    /// <param name=\"services\">The service collection to add services to.</param>\n    /// <param name=\"listenerConfiguration\">The listener configuration containing routing keys and message processing settings.</param>\n    /// <returns>The service collection for chaining.</returns>\n    /// <remarks>\n    /// <para>\n    /// This method registers three key components:\n    /// 1. EventIntegrationHandler - Keyed singleton for processing integration events\n    /// 2. RabbitMqEventListenerService - Hosted service for listening to event messages from RabbitMQ for\n    ///    this integration type\n    /// 3. RabbitMqIntegrationListenerService - Hosted service for listening to integration messages from RabbitMQ for\n    ///    this integration type\n    /// </para>\n    ///\n    /// <para>\n    /// The handler uses the listener configuration's routing key as its service key, allowing multiple\n    /// handlers to be registered for different integration types.\n    /// </para>\n    /// </remarks>\n    internal static IServiceCollection AddRabbitMqIntegration<TConfig, TListenerConfig>(this IServiceCollection services,\n        TListenerConfig listenerConfiguration)\n        where TConfig : class\n        where TListenerConfig : IIntegrationListenerConfiguration\n    {\n        services.TryAddKeyedSingleton<IEventMessageHandler>(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>\n            new EventIntegrationHandler<TConfig>(\n                integrationType: listenerConfiguration.IntegrationType,\n                eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),\n                integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),\n                cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),\n                configurationRepository: provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),\n                groupRepository: provider.GetRequiredService<IGroupRepository>(),\n                organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),\n                organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(), logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>())\n        );\n        services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,\n            RabbitMqEventListenerService<TListenerConfig>>(provider =>\n                new RabbitMqEventListenerService<TListenerConfig>(\n                    handler: provider.GetRequiredKeyedService<IEventMessageHandler>(serviceKey: listenerConfiguration.RoutingKey),\n                    configuration: listenerConfiguration,\n                    rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),\n                    loggerFactory: provider.GetRequiredService<ILoggerFactory>()\n                )\n            )\n        );\n        services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,\n            RabbitMqIntegrationListenerService<TListenerConfig>>(provider =>\n                new RabbitMqIntegrationListenerService<TListenerConfig>(\n                    handler: provider.GetRequiredService<IIntegrationHandler<TConfig>>(),\n                    configuration: listenerConfiguration,\n                    rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),\n                    loggerFactory: provider.GetRequiredService<ILoggerFactory>(),\n                    timeProvider: provider.GetRequiredService<TimeProvider>()\n                )\n            )\n        );\n\n        return services;\n    }\n\n    internal static IServiceCollection AddOrganizationIntegrationCommandsQueries(this IServiceCollection services)\n    {\n        services.TryAddScoped<ICreateOrganizationIntegrationCommand, CreateOrganizationIntegrationCommand>();\n        services.TryAddScoped<IUpdateOrganizationIntegrationCommand, UpdateOrganizationIntegrationCommand>();\n        services.TryAddScoped<IDeleteOrganizationIntegrationCommand, DeleteOrganizationIntegrationCommand>();\n        services.TryAddScoped<IGetOrganizationIntegrationsQuery, GetOrganizationIntegrationsQuery>();\n\n        return services;\n    }\n\n    internal static IServiceCollection AddOrganizationIntegrationConfigurationCommandsQueries(this IServiceCollection services)\n    {\n        services.TryAddScoped<ICreateOrganizationIntegrationConfigurationCommand, CreateOrganizationIntegrationConfigurationCommand>();\n        services.TryAddScoped<IUpdateOrganizationIntegrationConfigurationCommand, UpdateOrganizationIntegrationConfigurationCommand>();\n        services.TryAddScoped<IDeleteOrganizationIntegrationConfigurationCommand, DeleteOrganizationIntegrationConfigurationCommand>();\n        services.TryAddScoped<IGetOrganizationIntegrationConfigurationsQuery, GetOrganizationIntegrationConfigurationsQuery>();\n\n        return services;\n    }\n\n    /// <summary>\n    /// Determines if RabbitMQ is enabled for event integrations based on configuration settings.\n    /// </summary>\n    /// <param name=\"settings\">The global settings containing RabbitMQ configuration.</param>\n    /// <returns>True if all required RabbitMQ settings are present; otherwise, false.</returns>\n    /// <remarks>\n    /// Requires all the following settings to be configured:\n    /// <list type=\"bullet\">\n    ///   <item><description>EventLogging.RabbitMq.HostName</description></item>\n    ///   <item><description>EventLogging.RabbitMq.Username</description></item>\n    ///   <item><description>EventLogging.RabbitMq.Password</description></item>\n    ///   <item><description>EventLogging.RabbitMq.EventExchangeName</description></item>\n    ///   <item><description>EventLogging.RabbitMq.IntegrationExchangeName</description></item>\n    /// </list>\n    /// </remarks>\n    internal static bool IsRabbitMqEnabled(GlobalSettings settings)\n    {\n        return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) &&\n               CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) &&\n               CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) &&\n               CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName) &&\n               CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.IntegrationExchangeName);\n    }\n\n    /// <summary>\n    /// Determines if Azure Service Bus is enabled for event integrations based on configuration settings.\n    /// </summary>\n    /// <param name=\"settings\">The global settings containing Azure Service Bus configuration.</param>\n    /// <returns>True if all required Azure Service Bus settings are present; otherwise, false.</returns>\n    /// <remarks>\n    /// Requires all of the following settings to be configured:\n    /// <list type=\"bullet\">\n    ///   <item><description>EventLogging.AzureServiceBus.ConnectionString</description></item>\n    ///   <item><description>EventLogging.AzureServiceBus.EventTopicName</description></item>\n    ///   <item><description>EventLogging.AzureServiceBus.IntegrationTopicName</description></item>\n    /// </list>\n    /// </remarks>\n    internal static bool IsAzureServiceBusEnabled(GlobalSettings settings)\n    {\n        return CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.ConnectionString) &&\n               CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.EventTopicName) &&\n               CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.IntegrationTopicName);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\n\n/// <summary>\n/// Command implementation for creating organization integration configurations with validation and cache invalidation support.\n/// </summary>\npublic class CreateOrganizationIntegrationConfigurationCommand(\n    IOrganizationIntegrationRepository integrationRepository,\n    IOrganizationIntegrationConfigurationRepository configurationRepository,\n    [FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache,\n    IOrganizationIntegrationConfigurationValidator validator)\n    : ICreateOrganizationIntegrationConfigurationCommand\n{\n    public async Task<OrganizationIntegrationConfiguration> CreateAsync(\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        var integration = await integrationRepository.GetByIdAsync(integrationId);\n        if (integration == null || integration.OrganizationId != organizationId)\n        {\n            throw new NotFoundException();\n        }\n        if (!validator.ValidateConfiguration(integration.Type, configuration))\n        {\n            throw new BadRequestException(\n                $\"Invalid Configuration and/or Filters for integration type {integration.Type}\");\n        }\n\n        var created = await configurationRepository.CreateAsync(configuration);\n\n        // Invalidate the cached configuration details\n        // Even though this is a new record, the cache could hold a stale empty list for this\n        if (created.EventType == null)\n        {\n            // Wildcard configuration - invalidate all cached results for this org/integration\n            await cache.RemoveByTagAsync(\n                EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                    organizationId: organizationId,\n                    integrationType: integration.Type\n                ));\n        }\n        else\n        {\n            // Specific event type - only invalidate that specific cache entry\n            await cache.RemoveAsync(\n                EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                    organizationId: organizationId,\n                    integrationType: integration.Type,\n                    eventType: created.EventType.Value\n                ));\n        }\n\n        return created;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\n\n/// <summary>\n/// Command implementation for deleting organization integration configurations with cache invalidation support.\n/// </summary>\npublic class DeleteOrganizationIntegrationConfigurationCommand(\n    IOrganizationIntegrationRepository integrationRepository,\n    IOrganizationIntegrationConfigurationRepository configurationRepository,\n    [FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache)\n    : IDeleteOrganizationIntegrationConfigurationCommand\n{\n    public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)\n    {\n        var integration = await integrationRepository.GetByIdAsync(integrationId);\n        if (integration == null || integration.OrganizationId != organizationId)\n        {\n            throw new NotFoundException();\n        }\n        var configuration = await configurationRepository.GetByIdAsync(configurationId);\n        if (configuration is null || configuration.OrganizationIntegrationId != integrationId)\n        {\n            throw new NotFoundException();\n        }\n\n        await configurationRepository.DeleteAsync(configuration);\n\n        if (configuration.EventType == null)\n        {\n            // Wildcard configuration - invalidate all cached results for this org/integration\n            await cache.RemoveByTagAsync(\n                EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                    organizationId: organizationId,\n                    integrationType: integration.Type\n                ));\n        }\n        else\n        {\n            // Specific event type - only invalidate that specific cache entry\n            await cache.RemoveAsync(\n                EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                    organizationId: organizationId,\n                    integrationType: integration.Type,\n                    eventType: configuration.EventType.Value\n                ));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\n\n/// <summary>\n/// Query implementation for retrieving organization integration configurations.\n/// </summary>\npublic class GetOrganizationIntegrationConfigurationsQuery(\n    IOrganizationIntegrationRepository integrationRepository,\n    IOrganizationIntegrationConfigurationRepository configurationRepository)\n    : IGetOrganizationIntegrationConfigurationsQuery\n{\n    public async Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(\n        Guid organizationId,\n        Guid integrationId)\n    {\n        var integration = await integrationRepository.GetByIdAsync(integrationId);\n        if (integration == null || integration.OrganizationId != organizationId)\n        {\n            throw new NotFoundException();\n        }\n\n        var configurations = await configurationRepository.GetManyByIntegrationAsync(integrationId);\n        return configurations.ToList();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/ICreateOrganizationIntegrationConfigurationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\n\n/// <summary>\n/// Command interface for creating organization integration configurations.\n/// </summary>\npublic interface ICreateOrganizationIntegrationConfigurationCommand\n{\n    /// <summary>\n    /// Creates a new configuration for an organization integration.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <param name=\"integrationId\">The unique identifier of the integration.</param>\n    /// <param name=\"configuration\">The configuration to create.</param>\n    /// <returns>The created configuration.</returns>\n    /// <exception cref=\"Exceptions.NotFoundException\">Thrown when the integration does not exist\n    /// or does not belong to the specified organization.</exception>\n    /// <exception cref=\"Exceptions.BadRequestException\">Thrown when the configuration or filters\n    /// are invalid for the integration type.</exception>\n    Task<OrganizationIntegrationConfiguration> CreateAsync(Guid organizationId, Guid integrationId, OrganizationIntegrationConfiguration configuration);\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IDeleteOrganizationIntegrationConfigurationCommand.cs",
    "content": "﻿namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\n\n/// <summary>\n/// Command interface for deleting organization integration configurations.\n/// </summary>\npublic interface IDeleteOrganizationIntegrationConfigurationCommand\n{\n    /// <summary>\n    /// Deletes a configuration from an organization integration.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <param name=\"integrationId\">The unique identifier of the integration.</param>\n    /// <param name=\"configurationId\">The unique identifier of the configuration to delete.</param>\n    /// <exception cref=\"Exceptions.NotFoundException\">\n    /// Thrown when the integration or configuration does not exist,\n    /// or the integration does not belong to the specified organization,\n    /// or the configuration does not belong to the specified integration.</exception>\n    Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IGetOrganizationIntegrationConfigurationsQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\n\n/// <summary>\n/// Query interface for retrieving organization integration configurations.\n/// </summary>\npublic interface IGetOrganizationIntegrationConfigurationsQuery\n{\n    /// <summary>\n    /// Retrieves all configurations for a specific organization integration.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <param name=\"integrationId\">The unique identifier of the integration.</param>\n    /// <returns>A list of configurations associated with the integration.</returns>\n    /// <exception cref=\"Exceptions.NotFoundException\">Thrown when the integration does not exist\n    /// or does not belong to the specified organization.</exception>\n    Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(Guid organizationId, Guid integrationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/Interfaces/IUpdateOrganizationIntegrationConfigurationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\n\n/// <summary>\n/// Command interface for updating organization integration configurations.\n/// </summary>\npublic interface IUpdateOrganizationIntegrationConfigurationCommand\n{\n    /// <summary>\n    /// Updates an existing configuration for an organization integration.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <param name=\"integrationId\">The unique identifier of the integration.</param>\n    /// <param name=\"configurationId\">The unique identifier of the configuration to update.</param>\n    /// <param name=\"updatedConfiguration\">The updated configuration data.</param>\n    /// <returns>The updated configuration.</returns>\n    /// <exception cref=\"Exceptions.NotFoundException\">\n    /// Thrown when the integration or the configuration does not exist,\n    /// or the integration does not belong to the specified organization,\n    /// or the configuration does not belong to the specified integration.</exception>\n    /// <exception cref=\"Exceptions.BadRequestException\">Thrown when the configuration or filters\n    /// are invalid for the integration type.</exception>\n    Task<OrganizationIntegrationConfiguration> UpdateAsync(Guid organizationId, Guid integrationId, Guid configurationId, OrganizationIntegrationConfiguration updatedConfiguration);\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\n\n/// <summary>\n/// Command implementation for updating organization integration configurations with validation and cache invalidation support.\n/// </summary>\npublic class UpdateOrganizationIntegrationConfigurationCommand(\n    IOrganizationIntegrationRepository integrationRepository,\n    IOrganizationIntegrationConfigurationRepository configurationRepository,\n    [FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache,\n    IOrganizationIntegrationConfigurationValidator validator)\n    : IUpdateOrganizationIntegrationConfigurationCommand\n{\n    public async Task<OrganizationIntegrationConfiguration> UpdateAsync(\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegrationConfiguration updatedConfiguration)\n    {\n        var integration = await integrationRepository.GetByIdAsync(integrationId);\n        if (integration == null || integration.OrganizationId != organizationId)\n        {\n            throw new NotFoundException();\n        }\n        var configuration = await configurationRepository.GetByIdAsync(configurationId);\n        if (configuration is null || configuration.OrganizationIntegrationId != integrationId)\n        {\n            throw new NotFoundException();\n        }\n        if (!validator.ValidateConfiguration(integration.Type, updatedConfiguration))\n        {\n            throw new BadRequestException($\"Invalid Configuration and/or Filters for integration type {integration.Type}\");\n        }\n\n        updatedConfiguration.Id = configuration.Id;\n        updatedConfiguration.CreationDate = configuration.CreationDate;\n        await configurationRepository.ReplaceAsync(updatedConfiguration);\n\n        // If either old or new EventType is null (wildcard), invalidate all cached results\n        // for the specific integration\n        if (configuration.EventType == null || updatedConfiguration.EventType == null)\n        {\n            // Wildcard involved - invalidate all cached results for this org/integration\n            await cache.RemoveByTagAsync(\n                EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                    organizationId: organizationId,\n                    integrationType: integration.Type\n                ));\n\n            return updatedConfiguration;\n        }\n\n        // Both are specific event types - invalidate specific cache entries\n        await cache.RemoveAsync(\n            EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                organizationId: organizationId,\n                integrationType: integration.Type,\n                eventType: configuration.EventType.Value\n            ));\n\n        // If event type changed, also clear the new event type's cache\n        if (configuration.EventType != updatedConfiguration.EventType)\n        {\n            await cache.RemoveAsync(\n                EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                    organizationId: organizationId,\n                    integrationType: integration.Type,\n                    eventType: updatedConfiguration.EventType.Value\n                ));\n        }\n\n        return updatedConfiguration;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;\n\n/// <summary>\n/// Command implementation for creating organization integrations with cache invalidation support.\n/// </summary>\npublic class CreateOrganizationIntegrationCommand(\n    IOrganizationIntegrationRepository integrationRepository,\n    [FromKeyedServices(EventIntegrationsCacheConstants.CacheName)]\n    IFusionCache cache)\n    : ICreateOrganizationIntegrationCommand\n{\n    public async Task<OrganizationIntegration> CreateAsync(OrganizationIntegration integration)\n    {\n        var existingIntegrations = await integrationRepository\n            .GetManyByOrganizationAsync(integration.OrganizationId);\n        if (existingIntegrations.Any(i => i.Type == integration.Type))\n        {\n            throw new BadRequestException(\"An integration of this type already exists for this organization.\");\n        }\n\n        var created = await integrationRepository.CreateAsync(integration);\n        await cache.RemoveByTagAsync(\n            EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId: integration.OrganizationId,\n                integrationType: integration.Type\n            ));\n\n        return created;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;\n\n/// <summary>\n/// Command implementation for deleting organization integrations with cache invalidation support.\n/// </summary>\npublic class DeleteOrganizationIntegrationCommand(\n    IOrganizationIntegrationRepository integrationRepository,\n    [FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache)\n    : IDeleteOrganizationIntegrationCommand\n{\n    public async Task DeleteAsync(Guid organizationId, Guid integrationId)\n    {\n        var integration = await integrationRepository.GetByIdAsync(integrationId);\n        if (integration is null || integration.OrganizationId != organizationId)\n        {\n            throw new NotFoundException();\n        }\n\n        await integrationRepository.DeleteAsync(integration);\n        await cache.RemoveByTagAsync(\n            EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId: organizationId,\n                integrationType: integration.Type\n            ));\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\nusing Bit.Core.Dirt.Repositories;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;\n\n/// <summary>\n/// Query implementation for retrieving organization integrations.\n/// </summary>\npublic class GetOrganizationIntegrationsQuery(IOrganizationIntegrationRepository integrationRepository)\n    : IGetOrganizationIntegrationsQuery\n{\n    public async Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId)\n    {\n        var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);\n        return integrations.ToList();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/ICreateOrganizationIntegrationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\n\n/// <summary>\n/// Command interface for creating an OrganizationIntegration.\n/// </summary>\npublic interface ICreateOrganizationIntegrationCommand\n{\n    /// <summary>\n    /// Creates a new organization integration.\n    /// </summary>\n    /// <param name=\"integration\">The OrganizationIntegration to create.</param>\n    /// <returns>The created OrganizationIntegration.</returns>\n    /// <exception cref=\"Exceptions.BadRequestException\">Thrown when an integration\n    /// of the same type already exists for the organization.</exception>\n    Task<OrganizationIntegration> CreateAsync(OrganizationIntegration integration);\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IDeleteOrganizationIntegrationCommand.cs",
    "content": "﻿namespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\n\n/// <summary>\n/// Command interface for deleting organization integrations.\n/// </summary>\npublic interface IDeleteOrganizationIntegrationCommand\n{\n    /// <summary>\n    /// Deletes an organization integration.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <param name=\"integrationId\">The unique identifier of the integration to delete.</param>\n    /// <exception cref=\"Exceptions.NotFoundException\">Thrown when the integration does not exist\n    /// or does not belong to the specified organization.</exception>\n    Task DeleteAsync(Guid organizationId, Guid integrationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IGetOrganizationIntegrationsQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\n\n/// <summary>\n/// Query interface for retrieving organization integrations.\n/// </summary>\npublic interface IGetOrganizationIntegrationsQuery\n{\n    /// <summary>\n    /// Retrieves all organization integrations for a specific organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <returns>A list of organization integrations associated with the organization.</returns>\n    Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrations/Interfaces/IUpdateOrganizationIntegrationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\n\n/// <summary>\n/// Command interface for updating organization integrations.\n/// </summary>\npublic interface IUpdateOrganizationIntegrationCommand\n{\n    /// <summary>\n    /// Updates an existing organization integration.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <param name=\"integrationId\">The unique identifier of the integration to update.</param>\n    /// <param name=\"updatedIntegration\">The updated organization integration data.</param>\n    /// <returns>The updated organization integration.</returns>\n    /// <exception cref=\"Exceptions.NotFoundException\">Thrown when the integration does not exist,\n    /// does not belong to the specified organization, or the integration type does not match.</exception>\n    Task<OrganizationIntegration> UpdateAsync(Guid organizationId, Guid integrationId, OrganizationIntegration updatedIntegration);\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;\n\n/// <summary>\n/// Command implementation for updating organization integrations with cache invalidation support.\n/// </summary>\npublic class UpdateOrganizationIntegrationCommand(\n    IOrganizationIntegrationRepository integrationRepository,\n    [FromKeyedServices(EventIntegrationsCacheConstants.CacheName)]\n    IFusionCache cache)\n    : IUpdateOrganizationIntegrationCommand\n{\n    public async Task<OrganizationIntegration> UpdateAsync(\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration updatedIntegration)\n    {\n        var integration = await integrationRepository.GetByIdAsync(integrationId);\n        if (integration is null ||\n            integration.OrganizationId != organizationId ||\n            integration.Type != updatedIntegration.Type)\n        {\n            throw new NotFoundException();\n        }\n\n        updatedIntegration.Id = integration.Id;\n        updatedIntegration.OrganizationId = integration.OrganizationId;\n        updatedIntegration.CreationDate = integration.CreationDate;\n        await integrationRepository.ReplaceAsync(updatedIntegration);\n        await cache.RemoveByTagAsync(\n            EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId: organizationId,\n                integrationType: integration.Type\n            ));\n\n        return updatedIntegration;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/EventIntegrations/README.md",
    "content": "# Design goals\n\nThe main goal of event integrations is to easily enable adding new integrations over time without the need\nfor a lot of custom work to expose events to a new integration. The ability of fan-out offered by AMQP\n(either in RabbitMQ or in Azure Service Bus) gives us a way to attach any number of new integrations to the\nexisting event system without needing to add special handling. By adding a new listener to the existing\npipeline, it gains an independent stream of events without the need for additional broadcast code.\n\nWe want to enable robust handling of failures and retries. By utilizing the two-tier approach\n([described below](#two-tier-exchange)), we build in support at the service level for retries. When we add\nnew integrations, they can focus solely on the integration-specific logic and reporting status, with all the\nprocess of retries and delays managed by the messaging system.\n\nAnother goal is to not only support this functionality in the cloud version, but offer it as well to\nself-hosted instances. RabbitMQ provides a lightweight way for self-hosted instances to tie into the event system\nusing the same robust architecture for integrations without the need for Azure Service Bus.\n\nFinally, we want to offer organization admins flexibility and control over what events are significant, where\nto send events, and the data to be included in the message. The configuration architecture allows Organizations\nto customize details of a specific integration; see [Integrations and integration\nconfigurations](#integrations-and-integration-configurations) below for more details on the configuration piece.\n\n# Architecture\n\nThe entry point for the event integrations is the `IEventWriteService`. By configuring the\n`EventIntegrationEventWriteService` as the `EventWriteService`, all events sent to the\nservice are broadcast on the RabbitMQ or Azure Service Bus message exchange. To abstract away\nthe specifics of publishing to a specific AMQP provider, an `IEventIntegrationPublisher`\nis injected into `EventIntegrationEventWriteService` to handle the publishing of events to the\nRabbitMQ or Azure Service Bus service.\n\n## Two-tier exchange\n\nWhen `EventIntegrationEventWriteService` publishes, it posts to the first tier of our two-tier\napproach to handling messages. Each tier is represented in the AMQP stack by a separate exchange\n(in RabbitMQ terminology) or topic (in Azure Service Bus).\n\n``` mermaid\nflowchart TD\n    B1[EventService]\n    B2[EventIntegrationEventWriteService]\n    B3[Event Exchange / Topic]\n    B4[EventRepositoryHandler]\n    B5[WebhookIntegrationHandler]\n    B6[Events in Database / Azure Tables]\n    B7[HTTP Server]\n    B8[SlackIntegrationHandler]\n    B9[Slack]\n    B10[EventIntegrationHandler]\n    B12[Integration Exchange / Topic]\n\n    B1 -->|IEventWriteService| B2 --> B3\n    B3-->|EventListenerService| B4 --> B6\n    B3-->|EventListenerService| B10\n    B3-->|EventListenerService| B10\n    B10 --> B12\n    B12 -->|IntegrationListenerService| B5\n    B12 -->|IntegrationListenerService| B8\n    B5 -->|HTTP POST| B7\n    B8 -->|HTTP POST| B9\n```\n\n### Event tier\n\nIn the first tier, events are broadcast in a fan-out to a series of listeners. The message body\nis a JSON representation of an individual `EventMessage` or an array of `EventMessage`. Handlers at\nthis level are responsible for handling each event or array of events. There are currently two handlers\nat this level:\n  - `EventRepositoryHandler`\n    - The `EventRepositoryHandler` is responsible for long term storage of events. It receives all events\n      and  stores them via an injected `IEventRepository` into the database.\n    - This mirrors the behavior of when event integrations are turned off - cloud stores to Azure Tables\n      and self-hosted is stored to the database.\n  - `EventIntegrationHandler`\n    - The `EventIntegrationHandler` is a generic class that is customized to each integration (via the\n      configuration details of the integration) and is responsible for determining if there's a configuration\n      for this event / organization / integration, fetching that configuration, and parsing the details of the\n      event into a template string.\n    - The `EventIntegrationHandler` uses the injected `IOrganizationIntegrationConfigurationRepository` to pull\n      the specific set of configuration and template based on the event type, organization, and integration type.\n      This configuration is what determines if an integration should be sent, what details are necessary for sending\n      it, and the actual message to send.\n    - The output of `EventIntegrationHandler` is a new `IntegrationMessage`, with the details of this\n      the configuration necessary to interact with the integration and the message to send (with all the event\n      details incorporated), published to the integration level of the message bus.\n\n### Integration tier\n\nAt the integration level, messages are JSON representations of `IIntegrationMessage` - specifically they\nwill be concrete types of the generic `IntegrationMessage<T>` where `<T>` is the configuration details of the\nspecific integration for which they've been sent. These messages represent the details required for\nsending a specific event to a specific integration, including handling retries and delays.\n\nHandlers at the integration level are tied directly to the integration (e.g. `SlackIntegrationHandler`,\n`WebhookIntegrationHandler`). These handlers take in `IntegrationMessage<T>` and output\n`IntegrationHandlerResult`, which tells the listener the outcome of the integration (e.g. success / fail,\nif it can be retried and any minimum delay that should occur). This makes them easy to unit test in isolation\nwithout any of the concerns of AMQP or messaging.\n\nThe listeners at this level are responsible for firing off the handler when a new message comes in and then\ntaking the correct action based on the result. Successful results simply acknowledge the message and resolve.\nFailures will either be sent to the dead letter queue (DLQ) or re-published for retry after the correct amount of delay.\n\n### Retries\n\nOne of the goals of introducing the integration level is to simplify and enable the process of multiple retries\nfor a specific event integration. For instance, if a service is temporarily down, we don't want one of our handlers\nblocking the rest of the queue while it waits to retry. In addition, we don't want to retry _all_ integrations for a\nspecific event if only one integration fails nor do we want to re-lookup the configuration details. By splitting\nout the `IntegrationMessage<T>` with the configuration, message, and details around retries, we can process each\nevent / integration individually and retry easily.\n\nWhen the `IntegrationHandlerResult.Success` is set to `false` (indicating that the integration attempt failed) the\n`Retryable` flag tells the listener whether this failure is temporary or final. If the `Retryable` is `false`, then\nthe message is immediately sent to the DLQ. If it is `true`, the listener uses the `ApplyRetry(DateTime)` method\nin `IntegrationMessage` which handles both incrementing the `RetryCount` and updating the `DelayUntilDate` using\nthe provided DateTime, but also adding exponential backoff (based on `RetryCount`) and jitter. The listener compares\nthe `RetryCount` in the `IntegrationMessage` to see if it's over the `MaxRetries` defined in Global Settings. If it\nis over the `MaxRetries`, the message is sent to the DLQ. Otherwise, it is scheduled for retry.\n\n``` mermaid\nflowchart TD\nA[Success == false] --> B{Retryable?}\n    B -- No --> C[Send to Dead Letter Queue DLQ]\n    B -- Yes --> D[Check RetryCount vs MaxRetries]\n    D -->|RetryCount >= MaxRetries| E[Send to Dead Letter Queue DLQ]\n    D -->|RetryCount < MaxRetries| F[Schedule for Retry]\n```\n\nAzure Service Bus supports scheduling messages as part of its core functionality. Retries are scheduled to a specific\ntime and then ASB holds the message and publishes it at the correct time.\n\n#### RabbitMQ retry options\n\nFor RabbitMQ (which will be used by self-host only), we have two different options. The `useDelayPlugin` flag in\n`GlobalSettings.RabbitMqSettings` determines which one is used. If it is set to `true`, we use the delay plugin. It\ndefaults to `false` which indicates we should use retry queues with a timing check.\n\n1. Delay plugin\n   - [Delay plugin GitHub repo](https://github.com/rabbitmq/rabbitmq-delayed-message-exchange)\n   - This plugin enables a delayed message exchange in RabbitMQ that supports delaying a message for an amount\n     of time specified in a special header.\n   - This allows us to forego using any retry queues and rely instead on the delay exchange. When a message is\n     marked with the header it gets published to the exchange and the exchange handles all the functionality of\n     holding it until the appropriate time (similar to ASB's built-in support).\n   - The plugin must be setup and enabled before turning this option on (which is why it defaults to off).\n\n2. Retry queues + timing check\n    - If the delay plugin setting is off, we push the message to a retry queue which has a fixed amount of time before\n      it gets re-published back to the main queue.\n    - When a message comes off the queue, we check to see if the `DelayUntilDate` has already passed.\n      - If it has passed, we then handle the integration normally and retry the request.\n      - If it is still in the future, we put the message back on the retry queue for an additional wait.\n    - While this does use extra processing, it gives us better support for honoring the delays even if the delay plugin\n      isn't enabled. Since this solution is only intended for self-host, it should be a pretty minimal impact with short\n      delays and a small number of retries.\n\n## Listener / Handler pattern\n\nTo make it easy to support multiple AMQP services (RabbitMQ and Azure Service Bus), the act\nof listening to the stream of messages is decoupled from the act of responding to a message.\n\n### Listeners\n\n- Listeners handle the details of the communication platform (i.e. RabbitMQ and Azure Service Bus).\n- There is one listener for each platform (RabbitMQ / ASB) for each of the two levels - i.e. one event listener\n  and one integration listener.\n- Perform all the aspects of setup / teardown, subscription, message acknowledgement, etc. for the messaging platform,\n  but do not directly process any events themselves. Instead, they delegate to the handler with which they\n  are configured.\n- Multiple instances can be configured to run independently, each with its own handler and\n  subscription / queue.\n\n### Handlers\n\n- One handler per queue / subscription (e.g. per integration at the integration level).\n- Completely isolated from and know nothing of the messaging platform in use. This allows them to be\n  freely reused across different communication platforms.\n- Perform all aspects of handling an event.\n- Allows them to be highly testable as they are isolated and decoupled from the more complicated\n  aspects of messaging.\n\nThis combination allows for a configuration inside of `ServiceCollectionExtensions.cs` that pairs\ninstances of the listener service for the currently running messaging platform with any number of\nhandlers. It also allows for quick development of new handlers as they are focused only on the\ntask of handling a specific event.\n\n## Publishers and Services\n\nListeners (and `EventIntegrationHandler`) interact with the messaging system via the `IEventPublisher` interface,\nwhich is backed by a RabbitMQ and ASB specific service. By placing most of the messaging platform details in the\nservice layer, we are able to handle common things like configuring the connection, binding or creating a specific\nqueue, etc. in one place. The `IRabbitMqService` and `IAzureServiceBusService` implement the `IEventPublisher`\ninterface and therefore can also handle directly all the message publishing functionality.\n\n## Integrations and integration configurations\n\nOrganizations can configure integration configurations to send events to different endpoints -- each\nhandler maps to a specific integration and checks for the configuration when it receives an event.\nCurrently, there are integrations / handlers for Slack, webhooks, and HTTP Event Collector (HEC).\n\n### `OrganizationIntegration`\n\n- The top-level object that enables a specific integration for the organization.\n- Includes any properties that apply to the entire integration across all events.\n  - For example, Slack stores the token in the `Configuration` which applies to every event, but stores the\nchannel id in the `Configuration` of the `OrganizationIntegrationConfiguration`. The token applies to the entire Slack\nintegration, but the channel could be configured differently depending on event type.\n  - See the table below for more examples / details on what is stored at which level.\n\n### `OrganizationIntegrationConfiguration`\n\n- This contains the configurations specific to each `EventType` for the integration.\n- `Configuration` contains the event-specific configuration.\n    - Any properties at this level override the `Configuration` form the `OrganizationIntegration`.\n    - See the table below for examples of specific integrations.\n- `Template` contains a template string that is expected to be filled in with the contents of the actual event.\n    - The tokens in the string are wrapped in `#` characters. For instance, the UserId would be `#UserId#`.\n    - The `IntegrationTemplateProcessor` does the actual work of replacing these tokens with introspected values from\n      the provided `EventMessage`.\n    - The template does not enforce any structure — it could be a freeform text message to send via Slack, or a\n      JSON body to send via webhook; it is simply stored and used as a string for the most flexibility.\n\n### `OrganizationIntegrationConfigurationDetails`\n\n- This is the combination of both the `OrganizationIntegration` and `OrganizationIntegrationConfiguration` into\n  a single object. The combined contents tell the integration's handler all the details needed to send to an\n  external service.\n- `OrganizationIntegrationConfiguration` takes precedence over `OrganizationIntegration` - any keys present in\n  both will receive the value declared in `OrganizationIntegrationConfiguration`.\n- An array of `OrganizationIntegrationConfigurationDetails` is what the `EventIntegrationHandler` fetches from\n  the database to determine what to publish at the integration level.\n\n### Existing integrations and the configurations at each level\n\nThe following table illustrates how each integration is configured and what exactly is stored in the `Configuration`\nproperty at each level (`OrganizationIntegration` or `OrganizationIntegrationConfiguration`). Under\n`OrganizationIntegration` the valid `OrganizationIntegrationStatus` are in bold, with an example of what would be\nstored at each status.\n\n| **Integration**  | **OrganizationIntegration**                                                                                                                                                                                                                                                                 | **OrganizationIntegrationConfiguration**                                                                                                           |\n|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|\n| CloudBillingSync | **Not Applicable** (not yet used)                                                                                                                                                                                                                                                           | **Not Applicable** (not yet used)                                                                                                                  |\n| Scim             | **Not Applicable** (not yet used)                                                                                                                                                                                                                                                           | **Not Applicable** (not yet used)                                                                                                                  |\n| Slack            | **Initiated**: `null`<br/>**Completed**:<br/>`{ \"Token\": \"xoxb-token-from-slack\" }`                                                                                                                                                                                                         | `{ \"channelId\": \"C123456\" }`                                                                                                                       |\n| Webhook          | `null` or `{ \"Scheme\": \"Bearer\", \"Token\": \"AUTH-TOKEN\", \"Uri\": \"https://example.com\" }`                                                                                                                                                                                                     | `null` or `{ \"Scheme\": \"Bearer\", \"Token\":\"AUTH-TOKEN\", \"Uri\": \"https://example.com\" }`<br/><br/>Whatever is defined at this level takes precedence |\n| Hec              | `{ \"Scheme\": \"Bearer\", \"Token\": \"AUTH-TOKEN\", \"Uri\": \"https://example.com\" }`                                                                                                                                                                                                               | Always `null`                                                                                                                                      |\n| Datadog          | `{ \"ApiKey\": \"TheKey12345\", \"Uri\": \"https://api.us5.datadoghq.com/api/v1/events\"}`                                                                                                                                                                                                          | Always `null`                                                                                                                                      |\n| Teams            | **Initiated**: `null`<br/>**In Progress**: <br/> `{ \"TenantID\": \"tenant\", \"Teams\": [\"Id\": \"team\", DisplayName: \"MyTeam\"]}`<br/>**Completed**: <br/>`{ \"TenantID\": \"tenant\", \"Teams\": [\"Id\": \"team\", DisplayName: \"MyTeam\"], \"ServiceUrl\":\"https://example.com\", ChannelId: \"channel-1234\"}` | Always `null`                                                                                                                                      |\n\n## Filtering\n\nIn addition to the ability to configure integrations mentioned above, organization admins can\nalso add `Filters` stored in the `OrganizationIntegrationConfiguration`. Filters are completely\noptional and as simple or complex as organization admins want to make them. These are stored in\nthe database as JSON and serialized into an `IntegrationFilterGroup`. This is then passed to\nthe `IntegrationFilterService`, which evaluates it to a `bool`. If it's `true`, the integration\nproceeds as above. If it's `false`, we ignore this event and do not route it to the integration\nlevel.\n\n### `IntegrationFilterGroup`\n\nLogical AND / OR grouping of a number of rules and other subgroups.\n\n| Property      | Description                                                                                                                                                                                                                                                                                                                                                      |\n|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `AndOperator` | Indicates whether **all** (`true`) or **any** (`false`) of the `Rules` and `Groups` must be true. This applies to _both_ the inner group and the list of rules; for instance, if this group contained Rule1 and Rule2 as well as Group1 and Group2:<br/><br/>`true`: `Rule1 && Rule2 && Group1 && Group2`<br>`false`: `Rule1 \\|\\| Rule2 \\|\\| Group1 \\|\\| Group2` |\n| `Rules`       | A list of `IntegrationFilterRule`. Can be null or empty, in which case it will return `true`.                                                                                                                                                                                                                                                                    |\n| `Groups`      | A list of nested `IntegrationFilterGroup`. Can be null or empty, in which case it will return `true`.                                                                                                                                                                                                                                                            |\n\n### `IntegrationFilterRule`\n\nThe core of the filtering framework to determine if the data in this specific EventMessage\nmatches the data for which the filter is searching.\n\n| Property    | Description                                                                                                                                                                                                                                                 |\n|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `Property`  | The property on `EventMessage` to evaluate (e.g., `CollectionId`).                                                                                                                                                                                          |\n| `Operation` | The comparison to perform between the property and `Value`. <br><br>**Supported operations:**<br>• `Equals`: `Guid` equals `Value`<br>• `NotEquals`: logical inverse of `Equals`<br>• `In`: `Guid` is in `Value` list<br>• `NotIn`: logical inverse of `In` |\n| `Value`     | The comparison value. Type depends on `Operation`: <br>• `Equals`, `NotEquals`: `Guid`<br>• `In`, `NotIn`: list of `Guid`                                                                                                                                   |\n\n```mermaid\ngraph TD\n    A[IntegrationFilterGroup]\n    A -->|Has 0..many| B1[IntegrationFilterRule]\n    A --> D1[And Operator]\n    A -->|Has 0..many| C1[Nested IntegrationFilterGroup]\n\n    B1 --> B2[Property: string]\n    B1 --> B3[Operation: Equals/In/DateBefore/DateAfter]\n    B1 --> B4[Value: object?]\n\n    C1 -->|Has many| B1_2[IntegrationFilterRule]\n    C1 -->|Can contain| C2[IntegrationFilterGroup...]\n```\n## Caching\n\nTo reduce database load and improve performance, event integrations uses its own named extended cache (see\n[CACHING in Utilities](https://github.com/bitwarden/server/blob/main/src/Core/Utilities/CACHING.md)\nfor more information). Without caching, for instance, each incoming `EventMessage` would trigger a database\nquery to retrieve the relevant `OrganizationIntegrationConfigurationDetails`.\n\n### `EventIntegrationsCacheConstants`\n\n`EventIntegrationsCacheConstants` allows the code to have strongly typed references to a number of cache-related\ndetails when working with the extended cache. The cache name and all cache keys and tags are programmatically accessed\nfrom `EventIntegrationsCacheConstants` rather than simple strings. For instance,\n`EventIntegrationsCacheConstants.CacheName` is used in the cache setup, keyed services, dependency injection, etc.,\nrather than using a string literal (i.e. \"EventIntegrations\") in code.\n\n### `OrganizationIntegrationConfigurationDetails`\n\n- This is one of the most actively used portions of the architecture because any event that has an associated\n  organization requires a check of the configurations to determine if we need to fire off an integration.\n- By using the extended cache, all reads are hitting the L1 or L2 cache before needing to access the database.\n- Reads return a `List<OrganizationIntegrationConfigurationDetails>` for a given key or an empty list if no\n  match exists.\n- The TTL is set very high on these records (1 day). This is because when the admin API makes any changes, it\n  tells the cache to remove that key. This propagates to the event listening code via the extended cache backplane,\n  which means that the cache is then expired and the next read will fetch the new values. This allows us to have\n  a high TTL and avoid needing to refresh values except when necessary.\n\n#### Tagging per integration\n\n- Each entry in the cache (which again, returns `List<OrganizationIntegrationConfigurationDetails>`) is tagged with\n  the organization id and the integration type.\n- This allows us to remove all of a given organization's configuration details for an integration when the admin\n  makes changes at the integration level.\n    - For instance, if there were 5 events configured for a given organization's webhook and the admin changed the URL\n      at the integration level, the updates would need to be propagated or else the cache will continue returning the\n      stale URL.\n    - By tagging each of the entries, the API can ask the extended cache to remove all the entries for a given\n      organization integration in one call. The cache will handle dropping / refreshing these entries in a\n      performant way.\n- There are two places in the code that are both aware of the tagging functionality\n    - The `EventIntegrationHandler` must use the tag when fetching relevant configuration details. This tells the cache\n      to store the entry with the tag when it successfully loads from the repository.\n    - The `CreateOrganizationIntegrationCommand`, `UpdateOrganizationIntegrationCommand`, and\n      `DeleteOrganizationIntegrationCommand` commands need to use the tag to remove all the tagged entries when an admin\n      creates, updates, or deletes an integration.\n    - To ensure both places are synchronized on how to tag entries, they both use\n      `EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration` to build the tag.\n\n### Template Properties\n\n- The `IntegrationTemplateProcessor` supports some properties that require an additional lookup. For instance,\n  the `UserId` is provided as part of the `EventMessage`, but `UserName` means an additional lookup to map the user\n  id to the actual name.\n- The properties for a `User` (which includes `ActingUser`), `Group`, and `Organization` are cached via the\n  extended cache with a default TTL of 30 minutes.\n- This is cached in both the L1 (Memory) and L2 (Redis) and will be automatically refreshed as needed.\n\n# Building a new integration\n\nThese are all the pieces required in the process of building out a new integration. For\nclarity in naming, these assume a new integration called \"Example\". To see a complete example\nin context, view [the PR for adding the Datadog integration](https://github.com/bitwarden/server/pull/6289).\n\n## IntegrationType\n\nAdd a new type to `IntegrationType` for the new integration.\n\n## Configuration Models\n\nThe configuration models are the classes that will determine what is stored in the database for\n`OrganizationIntegration` and `OrganizationIntegrationConfiguration`. The `Configuration` columns are the\nserialized version of the corresponding objects and represent the coonfiguration details for this integration\nand event type.\n\n1. `ExampleIntegration`\n    - Configuration details for the whole integration (e.g. a token in Slack).\n    - Applies to every event type configuration defined for this integration.\n    - Maps to the JSON structure stored in `Configuration` in ``OrganizationIntegration`.\n2. `ExampleIntegrationConfiguration`\n    - Configuration details that could change from event to event (e.g. channelId in Slack).\n    - Maps to the JSON structure stored in `Configuration` in `OrganizationIntegrationConfiguration`.\n3. `ExampleIntegrationConfigurationDetails`\n    - Combined configuration of both Integration _and_ IntegrationConfiguration.\n    - This will be the deserialized version of the `MergedConfiguration` in\n      `OrganizationIntegrationConfigurationDetails`.\n\nA new row with the new integration should be added to this doc in the table above [Existing integrations\nand the configurations at each level](#existing-integrations-and-the-configurations-at-each-level).\n\n## Request Models\n\n1. Add a new case to the switch method in `OrganizationIntegrationRequestModel.Validate`.\n   - Additionally, add tests in `OrganizationIntegrationRequestModelTests`\n2. Add a new case to the switch method in `OrganizationIntegrationConfigurationRequestModel.IsValidForType`.\n    - Additionally, add / update tests in `OrganizationIntegrationConfigurationRequestModelTests`\n\n## Response Model\n\n1. Add a new case to the switch method in `OrganizationIntegrationResponseModel.Status`.\n    - Additionally, add / update tests in `OrganizationIntegrationResponseModelTests`\n\n## Integration Handler\n\ne.g. `ExampleIntegrationHandler`\n- This is where the actual code will go to perform the integration (i.e. send an HTTP request, etc.).\n- Handlers receive an `IntegrationMessage<T>` where `<T>` is the `ExampleIntegrationConfigurationDetails`\n  defined above. This has the Configuration as well as the rendered template message to be sent.\n- Handlers return an `IntegrationHandlerResult` with details about if the request - success / failure,\n  if it can be retried, when it should be delayed until, etc.\n- The scope of the handler is simply to do the integration and report the result.\n  Everything else (such as how many times to retry, when to retry, what to do with failures)\n  is done in the Listener.\n\n## GlobalSettings\n\n### RabbitMQ\nAdd the queue names for the integration. These are typically set with a default value so\nthat they will be created when first accessed in code by RabbitMQ.\n\n1. `ExampleEventQueueName`\n2. `ExampleIntegrationQueueName`\n3. `ExampleIntegrationRetryQueueName`\n\n### Azure Service Bus\nAdd the subscription names to use for ASB for this integration. Similar to RabbitMQ a\ndefault value is provided so that we don't require configuring it in secrets but allow\nit to be overridden. **However**, unlike RabbitMQ these subscriptions must exist prior\nto the code accessing them. They will not be created on the fly. See [Deploying a new\nintegration](#deploying-a-new-integration) below\n\n1. `ExmpleEventSubscriptionName`\n2. `ExmpleIntegrationSubscriptionName`\n\n#### Service Bus Emulator, local config\nIn order to create ASB resources locally, we need to also update the `servicebusemulator_config.json` file\nto include any new subscriptions.\n- Under the existing event topic (`event-logging`) add a subscription for the event level for this\n  new integration (`events-example-subscription`).\n- Under the existing integration topic (`event-integrations`) add a new subscription for the integration\n  level messages (`integration-example-subscription`).\n  - Copy the correlation filter from the other integration level subscriptions. It should filter based on\n    the `IntegrationType.ToRoutingKey`, or in this example `example`.\n\nThese names added here are what must match the values provided in the secrets or the defaults provided\nin Global Settings. This must be in place (and the local ASB emulator restarted) before you can use any\ncode locally that accesses ASB resources.\n\n## ListenerConfiguration\n\nNew integrations will need their own subclass of `ListenerConfiguration` which also conforms to\n`IIntegrationListenerConfiguration`. This class provides a way of accessing the previously configured\nRabbitMQ queues and ASB subscriptions by referring to the values created in `GlobalSettings`. This new\nlistener configuration will be used to type the listener and provide the means to access the necessary\nconfigurations for the integration.\n\n## ServiceCollectionExtensions\n\nIn our `ServiceCollectionExtensions`, we pull all the above pieces together to start listeners on each message\ntier with handlers to process the integration.\n\nThe core method for all event integration setup is `AddEventIntegrationServices`. This method is called by\nboth of the add listeners methods, which ensures that we have one common place to set up cross-messaging-platform\ndependencies and integrations. For instance, `SlackIntegrationHandler` needs a `SlackService`, so\n`AddEventIntegrationServices` has a call to `AddSlackService`. Same thing for webhooks when it\ncomes to defining a custom HttpClient by name.\n\nIn `AddEventIntegrationServices`:\n\n1.  Create the singleton for the handler:\n\n``` csharp\n        services.TryAddSingleton<IIntegrationHandler<ExampleIntegrationConfigurationDetails>, ExampleIntegrationHandler>();\n```\n\n2. Create the listener configuration:\n\n``` csharp\n        var exampleConfiguration = new ExampleListenerConfiguration(globalSettings);\n```\n\n3. Add the integration to both the RabbitMQ and ASB specific declarations:\n\n``` csharp\n        services.AddRabbitMqIntegration<ExampleIntegrationConfigurationDetails, ExampleListenerConfiguration>(exampleConfiguration);\n```\n\nand\n\n``` csharp\n        services.AddAzureServiceBusIntegration<ExampleIntegrationConfigurationDetails, ExampleListenerConfiguration>(exampleConfiguration);\n```\n\n\n# Deploying a new integration\n\n## RabbitMQ\n\nRabbitMQ dynamically creates queues and exchanges when they are first accessed in code.\nTherefore, there is no need to manually create queues when deploying a new integration.\nThey can be created and configured ahead of time, but it's not required. Note that once\nthey are created, if any configurations need to be changed, the queue or exchange must be\ndeleted and recreated.\n\n## Azure Service Bus\n\nUnlike RabbitMQ, ASB resources **must** be allocated before the code accesses them and\nwill not be created on the fly. This means that any subscriptions needed for a new\nintegration must be created in ASB before that code is deployed.\n\nThe two subscriptions created above in Global Settings and `servicebusemulator_config.json`\nneed to be created in the Azure portal or CLI for the environment before deploying the\ncode.\n\n1. `ExmpleEventSubscriptionName`\n    - This subscription is a fan-out subscription from the main event topic.\n    - As such, it will start receiving all the events as soon as it is declared.\n    - This can create a backlog before the integration-specific handler is declared and deployed.\n    - One strategy to avoid this is to create the subscription with a false filter (e.g. `1 = 0`).\n        - This will create the subscription, but the filter will ensure that no messages\n          actually land in the subscription.\n        - Code can be deployed that references the subscription, because the subscription\n          legitimately exists (it is simply empty).\n        - When the code is in place, and we're ready to start receiving messages on the new\n          integration, we simply remove the filter to return the subscription to receiving\n          all messages via fan-out.\n2. `ExmpleIntegrationSubscriptionName`\n    - This subscription must be created before the new integration code can be deployed.\n    - However, it is not fan-out, but rather a filter based on the `IntegrationType.ToRoutingKey`.\n    - Therefore, it won't start receiving messages until organizations have active configurations.\n      This means there's no risk of building up a backlog by declaring it ahead of time.\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegration.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record DatadogIntegration(string ApiKey, Uri Uri);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record DatadogIntegrationConfigurationDetails(string ApiKey, Uri Uri);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class DatadogListenerConfiguration(GlobalSettings globalSettings)\n    : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration\n{\n    public IntegrationType IntegrationType\n    {\n        get => IntegrationType.Datadog;\n    }\n\n    public string EventQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.DatadogEventsQueueName;\n    }\n\n    public string IntegrationQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.DatadogIntegrationQueueName;\n    }\n\n    public string IntegrationRetryQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.DatadogIntegrationRetryQueueName;\n    }\n\n    public string EventSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.DatadogEventSubscriptionName;\n    }\n\n    public string IntegrationSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.DatadogIntegrationSubscriptionName;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/HecIntegration.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record HecIntegration(Uri Uri, string Scheme, string Token, string? Service = null);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/HecListenerConfiguration.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class HecListenerConfiguration(GlobalSettings globalSettings)\n    : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration\n{\n    public IntegrationType IntegrationType\n    {\n        get => IntegrationType.Hec;\n    }\n\n    public string EventQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.HecEventsQueueName;\n    }\n\n    public string IntegrationQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.HecIntegrationQueueName;\n    }\n\n    public string IntegrationRetryQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.HecIntegrationRetryQueueName;\n    }\n\n    public string EventSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.HecEventSubscriptionName;\n    }\n\n    public string IntegrationSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.HecIntegrationSubscriptionName;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IEventListenerConfiguration.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic interface IEventListenerConfiguration\n{\n    public string EventQueueName { get; }\n    public string EventSubscriptionName { get; }\n    public string EventTopicName { get; }\n    public int EventPrefetchCount { get; }\n    public int EventMaxConcurrentCalls { get; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic interface IIntegrationListenerConfiguration : IEventListenerConfiguration\n{\n    public IntegrationType IntegrationType { get; }\n    public string IntegrationQueueName { get; }\n    public string IntegrationRetryQueueName { get; }\n    public string IntegrationSubscriptionName { get; }\n    public string IntegrationTopicName { get; }\n    public int MaxRetries { get; }\n    public int IntegrationPrefetchCount { get; }\n    public int IntegrationMaxConcurrentCalls { get; }\n\n    public string RoutingKey\n    {\n        get => IntegrationType.ToRoutingKey();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IIntegrationMessage.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic interface IIntegrationMessage\n{\n    IntegrationType IntegrationType { get; }\n    string MessageId { get; set; }\n    string? OrganizationId { get; set; }\n    int RetryCount { get; }\n    DateTime? DelayUntilDate { get; }\n    void ApplyRetry(DateTime? handlerDelayUntilDate);\n    string ToJson();\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFailureCategory.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\n/// <summary>\n/// Categories of event integration failures used for classification and retry logic.\n/// </summary>\npublic enum IntegrationFailureCategory\n{\n    /// <summary>\n    /// Service is temporarily unavailable (503, upstream outage, maintenance).\n    /// </summary>\n    ServiceUnavailable,\n\n    /// <summary>\n    /// Authentication failed (401, 403, invalid_auth, token issues).\n    /// </summary>\n    AuthenticationFailed,\n\n    /// <summary>\n    /// Configuration error (invalid config, channel_not_found, etc.).\n    /// </summary>\n    ConfigurationError,\n\n    /// <summary>\n    /// Rate limited (429, rate_limited).\n    /// </summary>\n    RateLimited,\n\n    /// <summary>\n    /// Transient error (timeouts, 500, network errors).\n    /// </summary>\n    TransientError,\n\n    /// <summary>\n    /// Permanent failure unrelated to authentication/config (e.g., unrecoverable payload/format issue).\n    /// </summary>\n    PermanentFailure\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterGroup.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class IntegrationFilterGroup\n{\n    public bool AndOperator { get; init; } = true;\n    public List<IntegrationFilterRule>? Rules { get; init; }\n    public List<IntegrationFilterGroup>? Groups { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterOperation.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic enum IntegrationFilterOperation\n{\n    Equals = 0,\n    NotEquals = 1,\n    In = 2,\n    NotIn = 3\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IntegrationFilterRule.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class IntegrationFilterRule\n{\n    public required string Property { get; set; }\n    public required IntegrationFilterOperation Operation { get; set; }\n    public required object? Value { get; set; }\n}\n\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResult.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\n/// <summary>\n/// Represents the result of an integration handler operation, including success status,\n/// failure categorization, and retry metadata. Use the <see cref=\"Succeed\"/> factory method\n/// for successful operations or <see cref=\"Fail\"/> for failures with automatic retry-ability\n/// determination based on the failure category.\n/// </summary>\npublic class IntegrationHandlerResult\n{\n    /// <summary>\n    /// True if the integration send succeeded, false otherwise.\n    /// </summary>\n    public bool Success { get; }\n\n    /// <summary>\n    /// The integration message that was processed.\n    /// </summary>\n    public IIntegrationMessage Message { get; }\n\n    /// <summary>\n    /// Optional UTC date/time indicating when a failed operation should be retried.\n    /// Will be used by the retry queue to delay re-sending the message.\n    /// Usually set based on the Retry-After header from rate-limited responses.\n    /// </summary>\n    public DateTime? DelayUntilDate { get; private init; }\n\n    /// <summary>\n    /// Category of the failure. Null for successful results.\n    /// </summary>\n    public IntegrationFailureCategory? Category { get; private init; }\n\n    /// <summary>\n    /// Detailed failure reason or error message. Empty for successful results.\n    /// </summary>\n    public string? FailureReason { get; private init; }\n\n    /// <summary>\n    /// Indicates whether the operation is retryable.\n    /// Computed from the failure category.\n    /// </summary>\n    public bool Retryable => Category switch\n    {\n        IntegrationFailureCategory.RateLimited => true,\n        IntegrationFailureCategory.TransientError => true,\n        IntegrationFailureCategory.ServiceUnavailable => true,\n        IntegrationFailureCategory.AuthenticationFailed => false,\n        IntegrationFailureCategory.ConfigurationError => false,\n        IntegrationFailureCategory.PermanentFailure => false,\n        null => false,\n        _ => false\n    };\n\n    /// <summary>\n    /// Creates a successful result.\n    /// </summary>\n    public static IntegrationHandlerResult Succeed(IIntegrationMessage message)\n    {\n        return new IntegrationHandlerResult(success: true, message: message);\n    }\n\n    /// <summary>\n    /// Creates a failed result with a failure category and reason.\n    /// </summary>\n    public static IntegrationHandlerResult Fail(\n        IIntegrationMessage message,\n        IntegrationFailureCategory category,\n        string failureReason,\n        DateTime? delayUntil = null)\n    {\n        return new IntegrationHandlerResult(success: false, message: message)\n        {\n            Category = category,\n            FailureReason = failureReason,\n            DelayUntilDate = delayUntil\n        };\n    }\n\n    private IntegrationHandlerResult(bool success, IIntegrationMessage message)\n    {\n        Success = success;\n        Message = message;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IntegrationMessage.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Enums;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class IntegrationMessage : IIntegrationMessage\n{\n    public IntegrationType IntegrationType { get; set; }\n    public required string MessageId { get; set; }\n    public string? OrganizationId { get; set; }\n    public required string RenderedTemplate { get; set; }\n    public int RetryCount { get; set; } = 0;\n    public DateTime? DelayUntilDate { get; set; }\n\n    public void ApplyRetry(DateTime? handlerDelayUntilDate)\n    {\n        RetryCount++;\n\n        var baseTime = handlerDelayUntilDate ?? DateTime.UtcNow;\n        var backoffSeconds = Math.Pow(2, RetryCount);\n        var jitterSeconds = Random.Shared.Next(0, 3);\n\n        DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds);\n    }\n\n    public virtual string ToJson()\n    {\n        return JsonSerializer.Serialize(this);\n    }\n}\n\npublic class IntegrationMessage<T> : IntegrationMessage\n{\n    public required T Configuration { get; set; }\n\n    public override string ToJson()\n    {\n        return JsonSerializer.Serialize(this);\n    }\n\n    public static IntegrationMessage<T>? FromJson(string json)\n    {\n        return JsonSerializer.Deserialize<IntegrationMessage<T>>(json);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IntegrationOAuthState.cs",
    "content": "﻿using System.Security.Cryptography;\nusing System.Text;\nusing Bit.Core.Dirt.Entities;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class IntegrationOAuthState\n{\n    private const int _orgHashLength = 12;\n    private static readonly TimeSpan _maxAge = TimeSpan.FromMinutes(20);\n\n    public Guid IntegrationId { get; }\n    private DateTimeOffset Issued { get; }\n    private string OrganizationIdHash { get; }\n\n    private IntegrationOAuthState(Guid integrationId, string organizationIdHash, DateTimeOffset issued)\n    {\n        IntegrationId = integrationId;\n        OrganizationIdHash = organizationIdHash;\n        Issued = issued;\n    }\n\n    public static IntegrationOAuthState FromIntegration(OrganizationIntegration integration, TimeProvider timeProvider)\n    {\n        var integrationId = integration.Id;\n        var issuedUtc = timeProvider.GetUtcNow();\n        var organizationIdHash = ComputeOrgHash(integration.OrganizationId, issuedUtc.ToUnixTimeSeconds());\n\n        return new IntegrationOAuthState(integrationId, organizationIdHash, issuedUtc);\n    }\n\n    public static IntegrationOAuthState? FromString(string state, TimeProvider timeProvider)\n    {\n        if (string.IsNullOrWhiteSpace(state)) return null;\n\n        var parts = state.Split('.');\n        if (parts.Length != 3) return null;\n\n        // Verify timestamp\n        if (!long.TryParse(parts[2], out var unixSeconds)) return null;\n\n        var issuedUtc = DateTimeOffset.FromUnixTimeSeconds(unixSeconds);\n        var now = timeProvider.GetUtcNow();\n        var age = now - issuedUtc;\n\n        if (age > _maxAge) return null;\n\n        // Parse integration id and store org\n        if (!Guid.TryParse(parts[0], out var integrationId)) return null;\n        var organizationIdHash = parts[1];\n\n        return new IntegrationOAuthState(integrationId, organizationIdHash, issuedUtc);\n    }\n\n    public bool ValidateOrg(Guid orgId)\n    {\n        var expected = ComputeOrgHash(orgId, Issued.ToUnixTimeSeconds());\n        return expected == OrganizationIdHash;\n    }\n\n    public override string ToString()\n    {\n        return $\"{IntegrationId}.{OrganizationIdHash}.{Issued.ToUnixTimeSeconds()}\";\n    }\n\n    private static string ComputeOrgHash(Guid orgId, long timestamp)\n    {\n        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes($\"{orgId:N}:{timestamp}\"));\n        return Convert.ToHexString(bytes)[.._orgHashLength];\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContext.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class IntegrationTemplateContext(EventMessage eventMessage)\n{\n    public EventMessage Event { get; } = eventMessage;\n\n    public string DomainName => Event.DomainName;\n    public string IpAddress => Event.IpAddress;\n    public DeviceType? DeviceType => Event.DeviceType;\n    public int? DeviceTypeId => Event.DeviceType is not null ? (int)Event.DeviceType : null;\n    public Guid? ActingUserId => Event.ActingUserId;\n    public Guid? OrganizationUserId => Event.OrganizationUserId;\n    public DateTime Date => Event.Date;\n    public EventType Type => Event.Type;\n    public int TypeId => (int)Event.Type;\n    public Guid? UserId => Event.UserId;\n    public Guid? OrganizationId => Event.OrganizationId;\n    public Guid? CipherId => Event.CipherId;\n    public Guid? CollectionId => Event.CollectionId;\n    public Guid? GroupId => Event.GroupId;\n    public Guid? PolicyId => Event.PolicyId;\n    public Guid? IdempotencyId => Event.IdempotencyId;\n    public Guid? ProviderId => Event.ProviderId;\n    public Guid? ProviderUserId => Event.ProviderUserId;\n    public Guid? ProviderOrganizationId => Event.ProviderOrganizationId;\n    public Guid? InstallationId => Event.InstallationId;\n    public Guid? SecretId => Event.SecretId;\n    public Guid? ProjectId => Event.ProjectId;\n    public Guid? ServiceAccountId => Event.ServiceAccountId;\n    public Guid? GrantedServiceAccountId => Event.GrantedServiceAccountId;\n\n    public string DateIso8601 => Date.ToString(\"o\");\n    public string EventMessage => JsonSerializer.Serialize(Event);\n\n    public OrganizationUserUserDetails? User { get; set; }\n    public string? UserName => User?.Name;\n    public string? UserEmail => User?.Email;\n    public OrganizationUserType? UserType => User?.Type;\n\n    public OrganizationUserUserDetails? ActingUser { get; set; }\n    public string? ActingUserName => ActingUser?.Name;\n    public string? ActingUserEmail => ActingUser?.Email;\n    public OrganizationUserType? ActingUserType => ActingUser?.Type;\n\n    public Group? Group { get; set; }\n    public string? GroupName => Group?.Name;\n\n    public Organization? Organization { get; set; }\n    public string? OrganizationName => Organization?.DisplayName();\n\n    public int? SystemUser => Event.SystemUser is not null ? (int)Event.SystemUser : null;\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/ListenerConfiguration.cs",
    "content": "﻿using Bit.Core.Settings;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic abstract class ListenerConfiguration\n{\n    protected GlobalSettings _globalSettings;\n\n    public ListenerConfiguration(GlobalSettings globalSettings)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public int MaxRetries\n    {\n        get => _globalSettings.EventLogging.MaxRetries;\n    }\n\n    public string EventTopicName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.EventTopicName;\n    }\n\n    public string IntegrationTopicName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName;\n    }\n\n    public int EventPrefetchCount\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.DefaultPrefetchCount;\n    }\n\n    public int EventMaxConcurrentCalls\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.DefaultMaxConcurrentCalls;\n    }\n\n    public int IntegrationPrefetchCount\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.DefaultPrefetchCount;\n    }\n\n    public int IntegrationMaxConcurrentCalls\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.DefaultMaxConcurrentCalls;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetails.cs",
    "content": "﻿using System.Text.Json.Nodes;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class OrganizationIntegrationConfigurationDetails\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public Guid OrganizationIntegrationId { get; set; }\n    public IntegrationType IntegrationType { get; set; }\n    public EventType? EventType { get; set; }\n    public string? Configuration { get; set; }\n    public string? Filters { get; set; }\n    public string? IntegrationConfiguration { get; set; }\n    public string? Template { get; set; }\n\n    public JsonObject MergedConfiguration\n    {\n        get\n        {\n            var integrationJson = IntegrationConfigurationJson;\n\n            foreach (var kvp in ConfigurationJson)\n            {\n                integrationJson[kvp.Key] = kvp.Value?.DeepClone();\n            }\n\n            return integrationJson;\n        }\n    }\n\n    private JsonObject ConfigurationJson\n    {\n        get\n        {\n            try\n            {\n                var configuration = Configuration ?? string.Empty;\n                return JsonNode.Parse(configuration) as JsonObject ?? new JsonObject();\n            }\n            catch\n            {\n                return new JsonObject();\n            }\n        }\n    }\n\n    private JsonObject IntegrationConfigurationJson\n    {\n        get\n        {\n            try\n            {\n                var integration = IntegrationConfiguration ?? string.Empty;\n                return JsonNode.Parse(integration) as JsonObject ?? new JsonObject();\n            }\n            catch\n            {\n                return new JsonObject();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs",
    "content": "﻿using Bit.Core.Settings;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class RepositoryListenerConfiguration(GlobalSettings globalSettings)\n    : ListenerConfiguration(globalSettings), IEventListenerConfiguration\n{\n    public string EventQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName;\n    }\n\n    public string EventSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegration.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record SlackIntegration(string Token);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record SlackIntegrationConfiguration(string ChannelId);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record SlackIntegrationConfigurationDetails(string ChannelId, string Token);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/SlackListenerConfiguration.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class SlackListenerConfiguration(GlobalSettings globalSettings) :\n    ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration\n{\n    public IntegrationType IntegrationType\n    {\n        get => IntegrationType.Slack;\n    }\n\n    public string EventQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.SlackEventsQueueName;\n    }\n\n    public string IntegrationQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName;\n    }\n\n    public string IntegrationRetryQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName;\n    }\n\n    public string EventSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.SlackEventSubscriptionName;\n    }\n\n    public string IntegrationSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.SlackIntegrationSubscriptionName;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegration.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.Teams;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record TeamsIntegration(\n    string TenantId,\n    IReadOnlyList<TeamInfo> Teams,\n    string? ChannelId = null,\n    Uri? ServiceUrl = null)\n{\n    public bool IsCompleted => !string.IsNullOrEmpty(ChannelId) && ServiceUrl is not null;\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record TeamsIntegrationConfigurationDetails(string ChannelId, Uri ServiceUrl);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class TeamsListenerConfiguration(GlobalSettings globalSettings) :\n    ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration\n{\n    public IntegrationType IntegrationType\n    {\n        get => IntegrationType.Teams;\n    }\n\n    public string EventQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.TeamsEventsQueueName;\n    }\n\n    public string IntegrationQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.TeamsIntegrationQueueName;\n    }\n\n    public string IntegrationRetryQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.TeamsIntegrationRetryQueueName;\n    }\n\n    public string EventSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.TeamsEventSubscriptionName;\n    }\n\n    public string IntegrationSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.TeamsIntegrationSubscriptionName;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegration.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record WebhookIntegration(Uri Uri, string? Scheme = null, string? Token = null);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record WebhookIntegrationConfiguration(Uri Uri, string? Scheme = null, string? Token = null);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic record WebhookIntegrationConfigurationDetails(Uri Uri, string? Scheme = null, string? Token = null);\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.Dirt.Models.Data.EventIntegrations;\n\npublic class WebhookListenerConfiguration(GlobalSettings globalSettings)\n    : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration\n{\n    public IntegrationType IntegrationType\n    {\n        get => IntegrationType.Webhook;\n    }\n\n    public string EventQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName;\n    }\n\n    public string IntegrationQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName;\n    }\n\n    public string IntegrationRetryQueueName\n    {\n        get => _globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName;\n    }\n\n    public string EventSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.WebhookEventSubscriptionName;\n    }\n\n    public string IntegrationSubscriptionName\n    {\n        get => _globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventMessage.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Data;\n\npublic class EventMessage : IEvent\n{\n    public EventMessage() { }\n\n    public EventMessage(ICurrentContext currentContext)\n        : base()\n    {\n        IpAddress = currentContext.IpAddress;\n        DeviceType = currentContext.DeviceType;\n    }\n\n    public DateTime Date { get; set; }\n    public EventType Type { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Guid? InstallationId { get; set; }\n    public Guid? ProviderId { get; set; }\n    public Guid? CipherId { get; set; }\n    public Guid? CollectionId { get; set; }\n    public Guid? GroupId { get; set; }\n    public Guid? PolicyId { get; set; }\n    public Guid? OrganizationUserId { get; set; }\n    public Guid? ProviderUserId { get; set; }\n    public Guid? ProviderOrganizationId { get; set; }\n    public Guid? ActingUserId { get; set; }\n    public DeviceType? DeviceType { get; set; }\n    public string IpAddress { get; set; }\n    public Guid? IdempotencyId { get; private set; } = Guid.NewGuid();\n    public EventSystemUser? SystemUser { get; set; }\n    public string DomainName { get; set; }\n    public Guid? SecretId { get; set; }\n    public Guid? ProjectId { get; set; }\n    public Guid? ServiceAccountId { get; set; }\n    public Guid? GrantedServiceAccountId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/EventTableEntity.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Azure;\nusing Azure.Data.Tables;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Models.Data;\n\n// used solely for interaction with Azure Table Storage\npublic class AzureEvent : ITableEntity\n{\n    public string PartitionKey { get; set; }\n    public string RowKey { get; set; }\n    public DateTimeOffset? Timestamp { get; set; }\n    public ETag ETag { get; set; }\n\n    public DateTime Date { get; set; }\n    public int Type { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Guid? InstallationId { get; set; }\n    public Guid? ProviderId { get; set; }\n    public Guid? CipherId { get; set; }\n    public Guid? CollectionId { get; set; }\n    public Guid? PolicyId { get; set; }\n    public Guid? GroupId { get; set; }\n    public Guid? OrganizationUserId { get; set; }\n    public Guid? ProviderUserId { get; set; }\n    public Guid? ProviderOrganizationId { get; set; }\n    public int? DeviceType { get; set; }\n    public string IpAddress { get; set; }\n    public Guid? ActingUserId { get; set; }\n    public int? SystemUser { get; set; }\n    public string DomainName { get; set; }\n    public Guid? SecretId { get; set; }\n    public Guid? ProjectId { get; set; }\n    public Guid? ServiceAccountId { get; set; }\n    public Guid? GrantedServiceAccountId { get; set; }\n\n    public EventTableEntity ToEventTableEntity()\n    {\n        return new EventTableEntity\n        {\n            PartitionKey = PartitionKey,\n            RowKey = RowKey,\n            Timestamp = Timestamp,\n            ETag = ETag,\n\n            Date = Date,\n            Type = (EventType)Type,\n            UserId = UserId,\n            OrganizationId = OrganizationId,\n            InstallationId = InstallationId,\n            ProviderId = ProviderId,\n            CipherId = CipherId,\n            CollectionId = CollectionId,\n            PolicyId = PolicyId,\n            GroupId = GroupId,\n            OrganizationUserId = OrganizationUserId,\n            ProviderUserId = ProviderUserId,\n            ProviderOrganizationId = ProviderOrganizationId,\n            DeviceType = DeviceType.HasValue ? (DeviceType)DeviceType.Value : null,\n            IpAddress = IpAddress,\n            ActingUserId = ActingUserId,\n            SystemUser = SystemUser.HasValue ? (EventSystemUser)SystemUser.Value : null,\n            DomainName = DomainName,\n            SecretId = SecretId,\n            ServiceAccountId = ServiceAccountId,\n            ProjectId = ProjectId,\n            GrantedServiceAccountId = GrantedServiceAccountId\n        };\n    }\n}\n\npublic class EventTableEntity : IEvent\n{\n    public EventTableEntity() { }\n\n    private EventTableEntity(IEvent e)\n    {\n        Date = e.Date;\n        Type = e.Type;\n        UserId = e.UserId;\n        OrganizationId = e.OrganizationId;\n        InstallationId = e.InstallationId;\n        ProviderId = e.ProviderId;\n        CipherId = e.CipherId;\n        CollectionId = e.CollectionId;\n        PolicyId = e.PolicyId;\n        GroupId = e.GroupId;\n        OrganizationUserId = e.OrganizationUserId;\n        ProviderUserId = e.ProviderUserId;\n        ProviderOrganizationId = e.ProviderOrganizationId;\n        DeviceType = e.DeviceType;\n        IpAddress = e.IpAddress;\n        ActingUserId = e.ActingUserId;\n        SystemUser = e.SystemUser;\n        DomainName = e.DomainName;\n        SecretId = e.SecretId;\n        ProjectId = e.ProjectId;\n        ServiceAccountId = e.ServiceAccountId;\n        GrantedServiceAccountId = e.GrantedServiceAccountId;\n    }\n\n    public string PartitionKey { get; set; }\n    public string RowKey { get; set; }\n    public DateTimeOffset? Timestamp { get; set; }\n    public ETag ETag { get; set; }\n\n    public DateTime Date { get; set; }\n    public EventType Type { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Guid? InstallationId { get; set; }\n    public Guid? ProviderId { get; set; }\n    public Guid? CipherId { get; set; }\n    public Guid? CollectionId { get; set; }\n    public Guid? PolicyId { get; set; }\n    public Guid? GroupId { get; set; }\n    public Guid? OrganizationUserId { get; set; }\n    public Guid? ProviderUserId { get; set; }\n    public Guid? ProviderOrganizationId { get; set; }\n    public DeviceType? DeviceType { get; set; }\n    public string IpAddress { get; set; }\n    public Guid? ActingUserId { get; set; }\n    public EventSystemUser? SystemUser { get; set; }\n    public string DomainName { get; set; }\n    public Guid? SecretId { get; set; }\n    public Guid? ProjectId { get; set; }\n    public Guid? ServiceAccountId { get; set; }\n    public Guid? GrantedServiceAccountId { get; set; }\n\n    public AzureEvent ToAzureEvent()\n    {\n        return new AzureEvent\n        {\n            PartitionKey = PartitionKey,\n            RowKey = RowKey,\n            Timestamp = Timestamp,\n            ETag = ETag,\n\n            Date = Date,\n            Type = (int)Type,\n            UserId = UserId,\n            OrganizationId = OrganizationId,\n            InstallationId = InstallationId,\n            ProviderId = ProviderId,\n            CipherId = CipherId,\n            CollectionId = CollectionId,\n            PolicyId = PolicyId,\n            GroupId = GroupId,\n            OrganizationUserId = OrganizationUserId,\n            ProviderUserId = ProviderUserId,\n            ProviderOrganizationId = ProviderOrganizationId,\n            DeviceType = DeviceType.HasValue ? (int)DeviceType.Value : null,\n            IpAddress = IpAddress,\n            ActingUserId = ActingUserId,\n            SystemUser = SystemUser.HasValue ? (int)SystemUser.Value : null,\n            DomainName = DomainName,\n            SecretId = SecretId,\n            ProjectId = ProjectId,\n            ServiceAccountId = ServiceAccountId,\n            GrantedServiceAccountId = GrantedServiceAccountId\n        };\n    }\n\n    public static List<EventTableEntity> IndexEvent(EventMessage e)\n    {\n        var uniquifier = e.IdempotencyId.GetValueOrDefault(Guid.NewGuid());\n\n        var pKey = GetPartitionKey(e);\n\n        var dateKey = CoreHelpers.DateTimeToTableStorageKey(e.Date);\n\n        var entities = new List<EventTableEntity>\n        {\n            new EventTableEntity(e)\n            {\n                PartitionKey = pKey,\n                RowKey = $\"Date={dateKey}__Uniquifier={uniquifier}\"\n            }\n        };\n\n        if (e.OrganizationId.HasValue && e.ActingUserId.HasValue)\n        {\n            entities.Add(new EventTableEntity(e)\n            {\n                PartitionKey = pKey,\n                RowKey = $\"ActingUserId={e.ActingUserId}__Date={dateKey}__Uniquifier={uniquifier}\"\n            });\n        }\n\n        if (!e.OrganizationId.HasValue && e.ProviderId.HasValue && e.ActingUserId.HasValue)\n        {\n            entities.Add(new EventTableEntity(e)\n            {\n                PartitionKey = pKey,\n                RowKey = $\"ActingUserId={e.ActingUserId}__Date={dateKey}__Uniquifier={uniquifier}\"\n            });\n        }\n\n        if (e.CipherId.HasValue)\n        {\n            entities.Add(new EventTableEntity(e)\n            {\n                PartitionKey = pKey,\n                RowKey = $\"CipherId={e.CipherId}__Date={dateKey}__Uniquifier={uniquifier}\"\n            });\n        }\n\n        if (e.OrganizationId.HasValue && e.ServiceAccountId.HasValue)\n        {\n            entities.Add(new EventTableEntity(e)\n            {\n                PartitionKey = pKey,\n                RowKey = $\"ServiceAccountId={e.ServiceAccountId}__Date={dateKey}__Uniquifier={uniquifier}\"\n            });\n        }\n\n        if (e.SecretId.HasValue)\n        {\n            entities.Add(new EventTableEntity(e)\n            {\n                PartitionKey = pKey,\n                RowKey = $\"SecretId={e.SecretId}__Date={dateKey}__Uniquifier={uniquifier}\"\n            });\n        }\n\n        if (e.ProjectId.HasValue)\n        {\n            entities.Add(new EventTableEntity(e)\n            {\n                PartitionKey = pKey,\n                RowKey = $\"ProjectId={e.ProjectId}__Date={dateKey}__Uniquifier={uniquifier}\"\n            });\n        }\n\n        if (e.GrantedServiceAccountId.HasValue)\n        {\n            entities.Add(new EventTableEntity(e)\n            {\n                PartitionKey = pKey,\n                RowKey = $\"GrantedServiceAccountId={e.GrantedServiceAccountId}__Date={dateKey}__Uniquifier={uniquifier}\"\n            });\n        }\n\n        return entities;\n    }\n\n    private static string GetPartitionKey(EventMessage e)\n    {\n        if (e.OrganizationId.HasValue)\n        {\n            return $\"OrganizationId={e.OrganizationId}\";\n        }\n\n        if (e.ProviderId.HasValue)\n        {\n            return $\"ProviderId={e.ProviderId}\";\n        }\n\n        return $\"UserId={e.UserId}\";\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/IEvent.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Data;\n\npublic interface IEvent\n{\n    EventType Type { get; set; }\n    Guid? UserId { get; set; }\n    Guid? OrganizationId { get; set; }\n    Guid? InstallationId { get; set; }\n    Guid? ProviderId { get; set; }\n    Guid? CipherId { get; set; }\n    Guid? CollectionId { get; set; }\n    Guid? GroupId { get; set; }\n    Guid? PolicyId { get; set; }\n    Guid? OrganizationUserId { get; set; }\n    Guid? ProviderUserId { get; set; }\n    Guid? ProviderOrganizationId { get; set; }\n    Guid? ActingUserId { get; set; }\n    DeviceType? DeviceType { get; set; }\n    string IpAddress { get; set; }\n    DateTime Date { get; set; }\n    EventSystemUser? SystemUser { get; set; }\n    string DomainName { get; set; }\n    Guid? SecretId { get; set; }\n    Guid? ProjectId { get; set; }\n    Guid? ServiceAccountId { get; set; }\n    Guid? GrantedServiceAccountId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/MemberAccessCipherDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Models.Data;\n\npublic class MemberAccessDetails\n{\n    public Guid? CollectionId { get; set; }\n    public Guid? GroupId { get; set; }\n    public string GroupName { get; set; }\n    public string CollectionName { get; set; }\n    public int ItemCount { get; set; }\n    public bool? ReadOnly { get; set; }\n    public bool? HidePasswords { get; set; }\n    public bool? Manage { get; set; }\n\n    /// <summary>\n    /// The CipherIds associated with the group/collection access\n    /// </summary>\n    public IEnumerable<string> CollectionCipherIds { get; set; }\n}\n\npublic class MemberAccessCipherDetails\n{\n    public string UserName { get; set; }\n    public string Email { get; set; }\n    public bool TwoFactorEnabled { get; set; }\n    public bool AccountRecoveryEnabled { get; set; }\n    public int GroupsCount { get; set; }\n    public int CollectionsCount { get; set; }\n    public int TotalItemCount { get; set; }\n    public Guid? UserGuid { get; set; }\n    public bool UsesKeyConnector { get; set; }\n\n    /// <summary>\n    /// The details for the member's collection access depending\n    /// on the collections and groups they are assigned to\n    /// </summary>\n    public IEnumerable<MemberAccessDetails> AccessDetails { get; set; }\n\n    /// <summary>\n    /// A distinct list of the cipher ids associated with\n    /// the organization member\n    /// </summary>\n    public IEnumerable<string> CipherIds { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/MemberAccessReportDetail.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Reports.Models.Data;\n\npublic class MemberAccessReportDetail\n{\n    public Guid? UserGuid { get; set; }\n    public string UserName { get; set; }\n    public string Email { get; set; }\n    public bool TwoFactorEnabled { get; set; }\n    public bool AccountRecoveryEnabled { get; set; }\n    public bool UsesKeyConnector { get; set; }\n    public Guid? CollectionId { get; set; }\n    public Guid? GroupId { get; set; }\n    public string GroupName { get; set; }\n    public string CollectionName { get; set; }\n    public bool? ReadOnly { get; set; }\n    public bool? HidePasswords { get; set; }\n    public bool? Manage { get; set; }\n    public IEnumerable<Guid> CipherIds { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/OrganizationMemberBaseDetail.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Reports.Models.Data;\n\npublic class OrganizationMemberBaseDetail\n{\n    public Guid? UserGuid { get; set; }\n    public string UserName { get; set; }\n    public string Email { get; set; }\n    public string TwoFactorProviders { get; set; }\n    public bool UsesKeyConnector { get; set; }\n    public string ResetPasswordKey { get; set; }\n    public Guid? CollectionId { get; set; }\n    public Guid? GroupId { get; set; }\n    public string GroupName { get; set; }\n    public string CollectionName { get; set; }\n    public bool? ReadOnly { get; set; }\n    public bool? HidePasswords { get; set; }\n    public bool? Manage { get; set; }\n    public Guid CipherId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/OrganizationReportApplicationDataResponse.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data;\n\npublic class OrganizationReportApplicationDataResponse\n{\n    public string? ApplicationData { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/OrganizationReportDataResponse.cs",
    "content": "﻿namespace Bit.Core.Dirt.Models.Data;\n\npublic class OrganizationReportDataResponse\n{\n    public string? ReportData { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/OrganizationReportMetricsData.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.Models.Data;\n\npublic class OrganizationReportMetricsData\n{\n    public Guid OrganizationId { get; set; }\n    public int? ApplicationCount { get; set; }\n    public int? ApplicationAtRiskCount { get; set; }\n    public int? CriticalApplicationCount { get; set; }\n    public int? CriticalApplicationAtRiskCount { get; set; }\n    public int? MemberCount { get; set; }\n    public int? MemberAtRiskCount { get; set; }\n    public int? CriticalMemberCount { get; set; }\n    public int? CriticalMemberAtRiskCount { get; set; }\n    public int? PasswordCount { get; set; }\n    public int? PasswordAtRiskCount { get; set; }\n    public int? CriticalPasswordCount { get; set; }\n    public int? CriticalPasswordAtRiskCount { get; set; }\n\n    public static OrganizationReportMetricsData From(Guid organizationId, OrganizationReportMetricsRequest? request)\n    {\n        if (request == null)\n        {\n            return new OrganizationReportMetricsData\n            {\n                OrganizationId = organizationId\n            };\n        }\n\n        return new OrganizationReportMetricsData\n        {\n            OrganizationId = organizationId,\n            ApplicationCount = request.ApplicationCount,\n            ApplicationAtRiskCount = request.ApplicationAtRiskCount,\n            CriticalApplicationCount = request.CriticalApplicationCount,\n            CriticalApplicationAtRiskCount = request.CriticalApplicationAtRiskCount,\n            MemberCount = request.MemberCount,\n            MemberAtRiskCount = request.MemberAtRiskCount,\n            CriticalMemberCount = request.CriticalMemberCount,\n            CriticalMemberAtRiskCount = request.CriticalMemberAtRiskCount,\n            PasswordCount = request.PasswordCount,\n            PasswordAtRiskCount = request.PasswordAtRiskCount,\n            CriticalPasswordCount = request.CriticalPasswordCount,\n            CriticalPasswordAtRiskCount = request.CriticalPasswordAtRiskCount\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/OrganizationReportSummaryDataResponse.cs",
    "content": "﻿using System.Text.Json.Serialization;\n\nnamespace Bit.Core.Dirt.Models.Data;\n\npublic class OrganizationReportSummaryDataResponse\n{\n    public required Guid OrganizationId { get; set; }\n    [JsonPropertyName(\"encryptedData\")]\n    public required string SummaryData { get; set; }\n    [JsonPropertyName(\"encryptionKey\")]\n    public required string ContentEncryptionKey { get; set; }\n    [JsonPropertyName(\"date\")]\n    public required DateTime RevisionDate { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/ReportFile.cs",
    "content": "﻿#nullable enable\n\nusing System.Diagnostics.CodeAnalysis;\nusing System.Text.Json.Serialization;\nusing static System.Text.Json.Serialization.JsonNumberHandling;\n\nnamespace Bit.Core.Dirt.Models.Data;\n\npublic class ReportFile\n{\n    /// <summary>\n    /// Uniquely identifies an uploaded file.\n    /// </summary>\n    [DisallowNull]\n    public string? Id { get; set; }\n\n    /// <summary>\n    /// Attached file name.\n    /// </summary>\n    public string FileName { get; set; } = string.Empty;\n\n    /// <summary>\n    /// Size of the attached file in bytes.\n    /// </summary>\n    [JsonNumberHandling(WriteAsString | AllowReadingFromString)]\n    public long Size { get; set; }\n\n    /// <summary>\n    /// When true the uploaded file's length has been validated.\n    /// </summary>\n    public bool Validated { get; set; } = true;\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/RiskInsightsReportDetail.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Reports.Models.Data;\n\npublic class RiskInsightsReportDetail\n{\n    public Guid? UserGuid { get; set; }\n    public string UserName { get; set; }\n    public string Email { get; set; }\n    public bool UsesKeyConnector { get; set; }\n    public IEnumerable<string> CipherIds { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/Slack/SlackApiResponse.cs",
    "content": "﻿using System.Text.Json.Serialization;\n\nnamespace Bit.Core.Dirt.Models.Data.Slack;\n\npublic abstract class SlackApiResponse\n{\n    public bool Ok { get; set; }\n    [JsonPropertyName(\"response_metadata\")]\n    public SlackResponseMetadata ResponseMetadata { get; set; } = new();\n    public string Error { get; set; } = string.Empty;\n}\n\npublic class SlackResponseMetadata\n{\n    [JsonPropertyName(\"next_cursor\")]\n    public string NextCursor { get; set; } = string.Empty;\n}\n\npublic class SlackChannelListResponse : SlackApiResponse\n{\n    public List<SlackChannel> Channels { get; set; } = new();\n}\n\npublic class SlackUserResponse : SlackApiResponse\n{\n    public SlackUser User { get; set; } = new();\n}\n\npublic class SlackOAuthResponse : SlackApiResponse\n{\n    [JsonPropertyName(\"access_token\")]\n    public string AccessToken { get; set; } = string.Empty;\n    public SlackTeam Team { get; set; } = new();\n}\n\npublic class SlackSendMessageResponse : SlackApiResponse\n{\n    [JsonPropertyName(\"channel\")]\n    public string Channel { get; set; } = string.Empty;\n}\n\npublic class SlackTeam\n{\n    public string Id { get; set; } = string.Empty;\n}\n\npublic class SlackChannel\n{\n    public string Id { get; set; } = string.Empty;\n    public string Name { get; set; } = string.Empty;\n}\n\npublic class SlackUser\n{\n    public string Id { get; set; } = string.Empty;\n    public string Name { get; set; } = string.Empty;\n}\n\npublic class SlackDmResponse : SlackApiResponse\n{\n    public SlackChannel Channel { get; set; } = new();\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/Teams/TeamsApiResponse.cs",
    "content": "﻿using System.Text.Json.Serialization;\n\nnamespace Bit.Core.Dirt.Models.Data.Teams;\n\n/// <summary>Represents the response returned by the Microsoft OAuth 2.0 token endpoint.\n/// See <see href=\"https://learn.microsoft.com/graph/auth-v2-user\">Microsoft identity platform and OAuth 2.0\n/// authorization code flow</see>.</summary>\npublic class TeamsOAuthResponse\n{\n    /// <summary>The access token issued by Microsoft, used to call the Microsoft Graph API.</summary>\n    [JsonPropertyName(\"access_token\")]\n    public string AccessToken { get; set; } = string.Empty;\n}\n\n/// <summary>Represents the response from the <c>/me/joinedTeams</c> Microsoft Graph API call.\n/// See <see href=\"https://learn.microsoft.com/graph/api/user-list-joinedteams\">List joined teams -\n/// Microsoft Graph v1.0</see>.</summary>\npublic class JoinedTeamsResponse\n{\n    /// <summary>The collection of teams that the user has joined.</summary>\n    [JsonPropertyName(\"value\")]\n    public List<TeamInfo> Value { get; set; } = [];\n}\n\n/// <summary>Represents a Microsoft Teams team returned by the Graph API.\n/// See <see href=\"https://learn.microsoft.com/graph/api/resources/team\">Team resource type -\n/// Microsoft Graph v1.0</see>.</summary>\npublic class TeamInfo\n{\n    /// <summary>The unique identifier of the team.</summary>\n    [JsonPropertyName(\"id\")]\n    public string Id { get; set; } = string.Empty;\n\n    /// <summary>The name of the team.</summary>\n    [JsonPropertyName(\"displayName\")]\n    public string DisplayName { get; set; } = string.Empty;\n\n    /// <summary>The ID of the Microsoft Entra tenant for this team.</summary>\n    [JsonPropertyName(\"tenantId\")]\n    public string TenantId { get; set; } = string.Empty;\n}\n"
  },
  {
    "path": "src/Core/Dirt/Models/Data/Teams/TeamsBotCredentialProvider.cs",
    "content": "﻿using Microsoft.Bot.Connector.Authentication;\n\nnamespace Bit.Core.Dirt.Models.Data.Teams;\n\npublic class TeamsBotCredentialProvider(string clientId, string clientSecret) : ICredentialProvider\n{\n    private const string _microsoftBotFrameworkIssuer = AuthenticationConstants.ToBotFromChannelTokenIssuer;\n\n    public Task<bool> IsValidAppIdAsync(string appId)\n    {\n        return Task.FromResult(appId == clientId);\n    }\n\n    public Task<string?> GetAppPasswordAsync(string appId)\n    {\n        return Task.FromResult(appId == clientId ? clientSecret : null);\n    }\n\n    public Task<bool> IsAuthenticationDisabledAsync()\n    {\n        return Task.FromResult(false);\n    }\n\n    public Task<bool> ValidateIssuerAsync(string issuer)\n    {\n        return Task.FromResult(issuer == _microsoftBotFrameworkIssuer);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class AddOrganizationReportCommand : IAddOrganizationReportCommand\n{\n    private readonly IOrganizationRepository _organizationRepo;\n    private readonly IOrganizationReportRepository _organizationReportRepo;\n    private readonly IFusionCache _cache;\n    private ILogger<AddOrganizationReportCommand> _logger;\n\n    public AddOrganizationReportCommand(\n        IOrganizationRepository organizationRepository,\n        IOrganizationReportRepository organizationReportRepository,\n        [FromKeyedServices(OrganizationReportCacheConstants.CacheName)] IFusionCache cache,\n        ILogger<AddOrganizationReportCommand> logger)\n    {\n        _organizationRepo = organizationRepository;\n        _organizationReportRepo = organizationReportRepository;\n        _cache = cache;\n        _logger = logger;\n    }\n\n    public async Task<OrganizationReport> AddOrganizationReportAsync(AddOrganizationReportRequest request)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Adding organization report for organization {organizationId}\", request.OrganizationId);\n\n        var (isValid, errorMessage) = await ValidateRequestAsync(request);\n        if (!isValid)\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Failed to add organization {organizationId} report: {errorMessage}\", request.OrganizationId, errorMessage);\n            throw new BadRequestException(errorMessage);\n        }\n\n        var requestMetrics = request.Metrics ?? new OrganizationReportMetricsRequest();\n\n        var organizationReport = new OrganizationReport\n        {\n            OrganizationId = request.OrganizationId,\n            ReportData = request.ReportData ?? string.Empty,\n            CreationDate = DateTime.UtcNow,\n            ContentEncryptionKey = request.ContentEncryptionKey ?? string.Empty,\n            SummaryData = request.SummaryData,\n            ApplicationData = request.ApplicationData,\n            ApplicationCount = requestMetrics.ApplicationCount,\n            ApplicationAtRiskCount = requestMetrics.ApplicationAtRiskCount,\n            CriticalApplicationCount = requestMetrics.CriticalApplicationCount,\n            CriticalApplicationAtRiskCount = requestMetrics.CriticalApplicationAtRiskCount,\n            MemberCount = requestMetrics.MemberCount,\n            MemberAtRiskCount = requestMetrics.MemberAtRiskCount,\n            CriticalMemberCount = requestMetrics.CriticalMemberCount,\n            CriticalMemberAtRiskCount = requestMetrics.CriticalMemberAtRiskCount,\n            PasswordCount = requestMetrics.PasswordCount,\n            PasswordAtRiskCount = requestMetrics.PasswordAtRiskCount,\n            CriticalPasswordCount = requestMetrics.CriticalPasswordCount,\n            CriticalPasswordAtRiskCount = requestMetrics.CriticalPasswordAtRiskCount,\n            RevisionDate = DateTime.UtcNow\n        };\n\n        organizationReport.SetNewId();\n\n        var data = await _organizationReportRepo.CreateAsync(organizationReport);\n\n        await _cache.RemoveByTagAsync(OrganizationReportCacheConstants.BuildCacheTagForOrganizationReports(request.OrganizationId));\n\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Successfully added organization report for organization {organizationId}, {organizationReportId}\",\n                request.OrganizationId, data.Id);\n\n        return data;\n    }\n\n    private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(\n        AddOrganizationReportRequest request)\n    {\n        // verify that the organization exists\n        var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);\n        if (organization == null)\n        {\n            return (false, \"Invalid Organization\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.ContentEncryptionKey))\n        {\n            return (false, \"Content Encryption Key is required\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.ReportData))\n        {\n            return (false, \"Report Data is required\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.SummaryData))\n        {\n            return (false, \"Summary Data is required\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.ApplicationData))\n        {\n            return (false, \"Application Data is required\");\n        }\n\n        return (true, string.Empty);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class AddPasswordHealthReportApplicationCommand : IAddPasswordHealthReportApplicationCommand\n{\n    private IOrganizationRepository _organizationRepo;\n    private IPasswordHealthReportApplicationRepository _passwordHealthReportApplicationRepo;\n\n    public AddPasswordHealthReportApplicationCommand(\n        IOrganizationRepository organizationRepository,\n        IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepository)\n    {\n        _organizationRepo = organizationRepository;\n        _passwordHealthReportApplicationRepo = passwordHealthReportApplicationRepository;\n    }\n\n    public async Task<PasswordHealthReportApplication> AddPasswordHealthReportApplicationAsync(AddPasswordHealthReportApplicationRequest request)\n    {\n        var (req, IsValid, errorMessage) = await ValidateRequestAsync(request);\n        if (!IsValid)\n        {\n            throw new BadRequestException(errorMessage);\n        }\n\n        var passwordHealthReportApplication = new PasswordHealthReportApplication\n        {\n            OrganizationId = request.OrganizationId,\n            Uri = request.Url,\n        };\n\n        passwordHealthReportApplication.SetNewId();\n\n        var data = await _passwordHealthReportApplicationRepo.CreateAsync(passwordHealthReportApplication);\n        return data;\n    }\n\n    public async Task<IEnumerable<PasswordHealthReportApplication>> AddPasswordHealthReportApplicationAsync(IEnumerable<AddPasswordHealthReportApplicationRequest> requests)\n    {\n        var requestsList = requests.ToList();\n\n        // create tasks to validate each request\n        var tasks = requestsList.Select(async request =>\n        {\n            var (req, IsValid, errorMessage) = await ValidateRequestAsync(request);\n            if (!IsValid)\n            {\n                throw new BadRequestException(errorMessage);\n            }\n        });\n\n        // run validations and allow exceptions to bubble\n        await Task.WhenAll(tasks);\n\n        // create PasswordHealthReportApplication entities\n        var passwordHealthReportApplications = requestsList.Select(request =>\n            {\n                var pwdHealthReportApplication = new PasswordHealthReportApplication\n                {\n                    OrganizationId = request.OrganizationId,\n                    Uri = request.Url,\n                };\n                pwdHealthReportApplication.SetNewId();\n                return pwdHealthReportApplication;\n            });\n\n        // create and return the entities\n        var response = new List<PasswordHealthReportApplication>();\n        foreach (var record in passwordHealthReportApplications)\n        {\n            var data = await _passwordHealthReportApplicationRepo.CreateAsync(record);\n            response.Add(data);\n        }\n\n        return response;\n    }\n\n    private async Task<Tuple<AddPasswordHealthReportApplicationRequest, bool, string>> ValidateRequestAsync(\n        AddPasswordHealthReportApplicationRequest request)\n    {\n        // verify that the organization exists\n        var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);\n        if (organization == null)\n        {\n            return new Tuple<AddPasswordHealthReportApplicationRequest, bool, string>(request, false, \"Invalid Organization\");\n        }\n\n        // ensure that we have a URL\n        if (string.IsNullOrWhiteSpace(request.Url))\n        {\n            return new Tuple<AddPasswordHealthReportApplicationRequest, bool, string>(request, false, \"URL is required\");\n        }\n\n        return new Tuple<AddPasswordHealthReportApplicationRequest, bool, string>(request, true, string.Empty);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class DropPasswordHealthReportApplicationCommand : IDropPasswordHealthReportApplicationCommand\n{\n    private IPasswordHealthReportApplicationRepository _passwordHealthReportApplicationRepo;\n\n    public DropPasswordHealthReportApplicationCommand(\n        IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepository)\n    {\n        _passwordHealthReportApplicationRepo = passwordHealthReportApplicationRepository;\n    }\n\n    public async Task DropPasswordHealthReportApplicationAsync(DropPasswordHealthReportApplicationRequest request)\n    {\n        var data = await _passwordHealthReportApplicationRepo.GetByOrganizationIdAsync(request.OrganizationId);\n        if (data == null)\n        {\n            throw new BadRequestException(\"Organization does not have any records.\");\n        }\n\n        data.Where(_ => request.PasswordHealthReportApplicationIds.Contains(_.Id)).ToList().ForEach(async _ =>\n        {\n            await _passwordHealthReportApplicationRepo.DeleteAsync(_);\n        });\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportApplicationDataQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class GetOrganizationReportApplicationDataQuery : IGetOrganizationReportApplicationDataQuery\n{\n    private readonly IOrganizationReportRepository _organizationReportRepo;\n    private readonly ILogger<GetOrganizationReportApplicationDataQuery> _logger;\n\n    public GetOrganizationReportApplicationDataQuery(\n        IOrganizationReportRepository organizationReportRepo,\n        ILogger<GetOrganizationReportApplicationDataQuery> logger)\n    {\n        _organizationReportRepo = organizationReportRepo;\n        _logger = logger;\n    }\n\n    public async Task<OrganizationReportApplicationDataResponse> GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId)\n    {\n        try\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Fetching organization report application data for organization {organizationId} and report {reportId}\",\n                organizationId, reportId);\n\n            if (organizationId == Guid.Empty)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"GetOrganizationReportApplicationDataAsync called with empty OrganizationId\");\n                throw new BadRequestException(\"OrganizationId is required.\");\n            }\n\n            if (reportId == Guid.Empty)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"GetOrganizationReportApplicationDataAsync called with empty ReportId\");\n                throw new BadRequestException(\"ReportId is required.\");\n            }\n\n            var applicationDataResponse = await _organizationReportRepo.GetApplicationDataAsync(reportId);\n\n            if (applicationDataResponse == null)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"No application data found for organization {organizationId} and report {reportId}\",\n                    organizationId, reportId);\n                throw new NotFoundException(\"Organization report application data not found.\");\n            }\n\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Successfully retrieved organization report application data for organization {organizationId} and report {reportId}\",\n                organizationId, reportId);\n\n            return applicationDataResponse;\n        }\n        catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))\n        {\n            _logger.LogError(ex, \"Error fetching organization report application data for organization {organizationId} and report {reportId}\",\n                organizationId, reportId);\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportDataQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class GetOrganizationReportDataQuery : IGetOrganizationReportDataQuery\n{\n    private readonly IOrganizationReportRepository _organizationReportRepo;\n    private readonly ILogger<GetOrganizationReportDataQuery> _logger;\n\n    public GetOrganizationReportDataQuery(\n        IOrganizationReportRepository organizationReportRepo,\n        ILogger<GetOrganizationReportDataQuery> logger)\n    {\n        _organizationReportRepo = organizationReportRepo;\n        _logger = logger;\n    }\n\n    public async Task<OrganizationReportDataResponse> GetOrganizationReportDataAsync(Guid organizationId, Guid reportId)\n    {\n        try\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Fetching organization report data for organization {organizationId} and report {reportId}\",\n                organizationId, reportId);\n\n            if (organizationId == Guid.Empty)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"GetOrganizationReportDataAsync called with empty OrganizationId\");\n                throw new BadRequestException(\"OrganizationId is required.\");\n            }\n\n            if (reportId == Guid.Empty)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"GetOrganizationReportDataAsync called with empty ReportId\");\n                throw new BadRequestException(\"ReportId is required.\");\n            }\n\n            var reportDataResponse = await _organizationReportRepo.GetReportDataAsync(reportId);\n\n            if (reportDataResponse == null)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"No report data found for organization {organizationId} and report {reportId}\",\n                    organizationId, reportId);\n                throw new NotFoundException(\"Organization report data not found.\");\n            }\n\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Successfully retrieved organization report data for organization {organizationId} and report {reportId}\",\n                organizationId, reportId);\n\n            return reportDataResponse;\n        }\n        catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))\n        {\n            _logger.LogError(ex, \"Error fetching organization report data for organization {organizationId} and report {reportId}\",\n                organizationId, reportId);\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class GetOrganizationReportQuery : IGetOrganizationReportQuery\n{\n    private IOrganizationReportRepository _organizationReportRepo;\n    private ILogger<GetOrganizationReportQuery> _logger;\n\n    public GetOrganizationReportQuery(\n        IOrganizationReportRepository organizationReportRepo,\n        ILogger<GetOrganizationReportQuery> logger)\n    {\n        _organizationReportRepo = organizationReportRepo;\n        _logger = logger;\n    }\n\n    public async Task<OrganizationReport> GetOrganizationReportAsync(Guid reportId)\n    {\n        if (reportId == Guid.Empty)\n        {\n            throw new BadRequestException(\"Id of report is required.\");\n        }\n\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Fetching organization reports for organization by Id: {reportId}\", reportId);\n\n        var results = await _organizationReportRepo.GetByIdAsync(reportId);\n\n        if (results == null)\n        {\n            throw new NotFoundException($\"No report found for Id: {reportId}\");\n        }\n\n        return results;\n    }\n\n    public async Task<OrganizationReport> GetLatestOrganizationReportAsync(Guid organizationId)\n    {\n        if (organizationId == Guid.Empty)\n        {\n            throw new BadRequestException(\"OrganizationId is required.\");\n        }\n\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Fetching latest organization report for organization {organizationId}\", organizationId);\n        return await _organizationReportRepo.GetLatestByOrganizationIdAsync(organizationId);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class GetOrganizationReportSummaryDataByDateRangeQuery : IGetOrganizationReportSummaryDataByDateRangeQuery\n{\n    private const int MaxRecordsForWidget = 6;\n    private readonly IOrganizationReportRepository _organizationReportRepo;\n    private readonly ILogger<GetOrganizationReportSummaryDataByDateRangeQuery> _logger;\n    private readonly IFusionCache _cache;\n\n    public GetOrganizationReportSummaryDataByDateRangeQuery(\n        IOrganizationReportRepository organizationReportRepo,\n        [FromKeyedServices(OrganizationReportCacheConstants.CacheName)] IFusionCache cache,\n        ILogger<GetOrganizationReportSummaryDataByDateRangeQuery> logger)\n    {\n        _organizationReportRepo = organizationReportRepo;\n        _cache = cache;\n        _logger = logger;\n    }\n\n    public async Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetOrganizationReportSummaryDataByDateRangeAsync(Guid organizationId, DateTime startDate, DateTime endDate)\n    {\n        try\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Fetching organization report summary data by date range for organization {OrganizationId}, from {StartDate} to {EndDate}\",\n                organizationId, startDate, endDate);\n\n            var (isValid, errorMessage) = ValidateRequest(organizationId, startDate, endDate);\n            if (!isValid)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"GetOrganizationReportSummaryDataByDateRangeAsync validation failed: {errorMessage}\", errorMessage);\n                throw new BadRequestException(errorMessage);\n            }\n\n            // update start and end date to include the entire day\n            startDate = startDate.Date;\n            endDate = endDate.Date.AddDays(1).AddTicks(-1);\n\n            // cache key and tag\n            var cacheKey = OrganizationReportCacheConstants.BuildCacheKeyForSummaryDataByDateRange(organizationId, startDate, endDate);\n            var cacheTag = OrganizationReportCacheConstants.BuildCacheTagForOrganizationReports(organizationId);\n\n            var summaryDataList = await _cache.GetOrSetAsync(\n                key: cacheKey,\n                factory: async _ =>\n                    {\n                        var data = await _organizationReportRepo.GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate);\n                        return GetMostRecentEntries(data);\n                    },\n                options: new FusionCacheEntryOptions(duration: OrganizationReportCacheConstants.DurationForSummaryData),\n                tags: [cacheTag]\n            );\n\n            var resultList = summaryDataList?.ToList() ?? Enumerable.Empty<OrganizationReportSummaryDataResponse>().ToList();\n\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Fetched {Count} organization report summary data entries for organization {OrganizationId}, from {StartDate} to {EndDate}\",\n                resultList.Count, organizationId, startDate, endDate);\n\n            return resultList;\n        }\n        catch (Exception ex) when (!(ex is BadRequestException))\n        {\n            _logger.LogError(ex, \"Error fetching organization report summary data by date range for organization {OrganizationId}, from {StartDate} to {EndDate}\",\n                organizationId, startDate, endDate);\n            throw;\n        }\n    }\n\n    private static (bool IsValid, string errorMessage) ValidateRequest(Guid organizationId, DateTime startDate, DateTime endDate)\n    {\n        if (organizationId == Guid.Empty)\n        {\n            return (false, \"OrganizationId is required\");\n        }\n\n        if (startDate == default)\n        {\n            return (false, \"StartDate is required\");\n        }\n\n        if (endDate == default)\n        {\n            return (false, \"EndDate is required\");\n        }\n\n        if (startDate > endDate)\n        {\n            return (false, \"StartDate must be earlier than or equal to EndDate\");\n        }\n\n        return (true, string.Empty);\n    }\n\n    private static IEnumerable<OrganizationReportSummaryDataResponse> GetMostRecentEntries(IEnumerable<OrganizationReportSummaryDataResponse> data, int maxEntries = MaxRecordsForWidget)\n    {\n        if (data.Count() <= maxEntries)\n        {\n            return data;\n        }\n\n        // here we need to take 10 records, evenly spaced by RevisionDate, \n        // to cover the entire date range, \n        // and ensure we include the most recent record as well\n        var sortedData = data.OrderByDescending(d => d.RevisionDate).ToList();\n        var totalRecords = sortedData.Count;\n        var interval = (double)(totalRecords - 1) / (maxEntries - 1); // -1 the most recent record will be included by default\n        var result = new List<OrganizationReportSummaryDataResponse>();\n\n        for (int i = 0; i <= maxEntries - 1; i++)\n        {\n            result.Add(sortedData[(int)Math.Round(i * interval)]);\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class GetOrganizationReportSummaryDataQuery : IGetOrganizationReportSummaryDataQuery\n{\n    private readonly IOrganizationReportRepository _organizationReportRepo;\n    private readonly ILogger<GetOrganizationReportSummaryDataQuery> _logger;\n\n    public GetOrganizationReportSummaryDataQuery(\n        IOrganizationReportRepository organizationReportRepo,\n        ILogger<GetOrganizationReportSummaryDataQuery> logger)\n    {\n        _organizationReportRepo = organizationReportRepo;\n        _logger = logger;\n    }\n\n    public async Task<OrganizationReportSummaryDataResponse> GetOrganizationReportSummaryDataAsync(Guid organizationId, Guid reportId)\n    {\n        try\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Fetching organization report summary data for organization {organizationId} and report {reportId}\",\n                organizationId, reportId);\n\n            if (organizationId == Guid.Empty)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"GetOrganizationReportSummaryDataAsync called with empty OrganizationId\");\n                throw new BadRequestException(\"OrganizationId is required.\");\n            }\n\n            if (reportId == Guid.Empty)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"GetOrganizationReportSummaryDataAsync called with empty ReportId\");\n                throw new BadRequestException(\"ReportId is required.\");\n            }\n\n            var summaryDataResponse = await _organizationReportRepo.GetSummaryDataAsync(reportId);\n\n            if (summaryDataResponse == null)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"No summary data found for organization {organizationId} and report {reportId}\",\n                    organizationId, reportId);\n                throw new NotFoundException(\"Organization report summary data not found.\");\n            }\n\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Successfully retrieved organization report summary data for organization {organizationId} and report {reportId}\",\n                organizationId, reportId);\n\n            return summaryDataResponse;\n        }\n        catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))\n        {\n            _logger.LogError(ex, \"Error fetching organization report summary data for organization {organizationId} and report {reportId}\",\n                organizationId, reportId);\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class GetPasswordHealthReportApplicationQuery : IGetPasswordHealthReportApplicationQuery\n{\n    private IPasswordHealthReportApplicationRepository _passwordHealthReportApplicationRepo;\n\n    public GetPasswordHealthReportApplicationQuery(\n        IPasswordHealthReportApplicationRepository passwordHealthReportApplicationRepo)\n    {\n        _passwordHealthReportApplicationRepo = passwordHealthReportApplicationRepo;\n    }\n\n    public async Task<IEnumerable<PasswordHealthReportApplication>> GetPasswordHealthReportApplicationAsync(Guid organizationId)\n    {\n        if (organizationId == Guid.Empty)\n        {\n            throw new BadRequestException(\"OrganizationId is required.\");\n        }\n\n        return await _passwordHealthReportApplicationRepo.GetByOrganizationIdAsync(organizationId);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddOrganizationReportCommand.cs",
    "content": "﻿\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IAddOrganizationReportCommand\n{\n    Task<OrganizationReport> AddOrganizationReportAsync(AddOrganizationReportRequest request);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IAddPasswordHealthReportApplicationCommand\n{\n    Task<PasswordHealthReportApplication> AddPasswordHealthReportApplicationAsync(AddPasswordHealthReportApplicationRequest request);\n    Task<IEnumerable<PasswordHealthReportApplication>> AddPasswordHealthReportApplicationAsync(IEnumerable<AddPasswordHealthReportApplicationRequest> requests);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IDropPasswordHealthReportApplicationCommand\n{\n    Task DropPasswordHealthReportApplicationAsync(DropPasswordHealthReportApplicationRequest request);\n}\n\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportApplicationDataQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IGetOrganizationReportApplicationDataQuery\n{\n    Task<OrganizationReportApplicationDataResponse> GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportDataQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IGetOrganizationReportDataQuery\n{\n    Task<OrganizationReportDataResponse> GetOrganizationReportDataAsync(Guid organizationId, Guid reportId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IGetOrganizationReportQuery\n{\n    Task<OrganizationReport> GetOrganizationReportAsync(Guid organizationId);\n    Task<OrganizationReport> GetLatestOrganizationReportAsync(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataByDateRangeQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IGetOrganizationReportSummaryDataByDateRangeQuery\n{\n    Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetOrganizationReportSummaryDataByDateRangeAsync(\n        Guid organizationId, DateTime startDate, DateTime endDate);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IGetOrganizationReportSummaryDataQuery\n{\n    Task<OrganizationReportSummaryDataResponse> GetOrganizationReportSummaryDataAsync(Guid organizationId, Guid reportId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IGetPasswordHealthReportApplicationQuery\n{\n    Task<IEnumerable<PasswordHealthReportApplication>> GetPasswordHealthReportApplicationAsync(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportApplicationDataCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IUpdateOrganizationReportApplicationDataCommand\n{\n    Task<OrganizationReport> UpdateOrganizationReportApplicationDataAsync(UpdateOrganizationReportApplicationDataRequest request);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IUpdateOrganizationReportCommand\n{\n    Task<OrganizationReport> UpdateOrganizationReportAsync(UpdateOrganizationReportRequest request);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportDataCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IUpdateOrganizationReportDataCommand\n{\n    Task<OrganizationReport> UpdateOrganizationReportDataAsync(UpdateOrganizationReportDataRequest request);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportSummaryCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\n\npublic interface IUpdateOrganizationReportSummaryCommand\n{\n    Task<OrganizationReport> UpdateOrganizationReportSummaryAsync(UpdateOrganizationReportSummaryRequest request);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Reports.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class MemberAccessReportQuery(\n    IOrganizationMemberBaseDetailRepository organizationMemberBaseDetailRepository,\n    ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n    IApplicationCacheService applicationCacheService,\n    ILogger<MemberAccessReportQuery> logger) : IMemberAccessReportQuery\n{\n    public async Task<IEnumerable<MemberAccessReportDetail>> GetMemberAccessReportsAsync(\n        MemberAccessReportRequest request)\n    {\n        logger.LogInformation(Constants.BypassFiltersEventId, \"Starting MemberAccessReport generation for OrganizationId: {OrganizationId}\", request.OrganizationId);\n\n        var baseDetails =\n            await organizationMemberBaseDetailRepository.GetOrganizationMemberBaseDetailsByOrganizationId(\n                request.OrganizationId);\n\n        logger.LogInformation(Constants.BypassFiltersEventId, \"Retrieved {BaseDetailsCount} base details for OrganizationId: {OrganizationId}\",\n            baseDetails.Count(), request.OrganizationId);\n\n        var orgUsers = baseDetails.Select(x => x.UserGuid.GetValueOrDefault()).Distinct();\n        var orgUsersCount = orgUsers.Count();\n        logger.LogInformation(Constants.BypassFiltersEventId, \"Found {UniqueUsersCount} unique users for OrganizationId: {OrganizationId}\",\n            orgUsersCount, request.OrganizationId);\n\n        var orgUsersTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);\n        logger.LogInformation(Constants.BypassFiltersEventId, \"Retrieved two-factor status for {UsersCount} users for OrganizationId: {OrganizationId}\",\n            orgUsersTwoFactorEnabled.Count(), request.OrganizationId);\n\n        var orgAbility = await applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId);\n        logger.LogInformation(Constants.BypassFiltersEventId, \"Retrieved organization ability (UseResetPassword: {UseResetPassword}) for OrganizationId: {OrganizationId}\",\n            orgAbility?.UseResetPassword, request.OrganizationId);\n\n        var accessDetails = baseDetails\n            .GroupBy(b => new\n            {\n                b.UserGuid,\n                b.UserName,\n                b.Email,\n                b.TwoFactorProviders,\n                b.ResetPasswordKey,\n                b.UsesKeyConnector,\n                b.GroupId,\n                b.GroupName,\n                b.CollectionId,\n                b.CollectionName,\n                b.ReadOnly,\n                b.HidePasswords,\n                b.Manage\n            })\n            .Select(g => new MemberAccessReportDetail\n            {\n                UserGuid = g.Key.UserGuid,\n                UserName = g.Key.UserName,\n                Email = g.Key.Email,\n                TwoFactorEnabled = orgUsersTwoFactorEnabled.FirstOrDefault(x => x.userId == g.Key.UserGuid).twoFactorIsEnabled,\n                AccountRecoveryEnabled = OrganizationUser.IsValidResetPasswordKey(g.Key.ResetPasswordKey) && orgAbility.UseResetPassword,\n                UsesKeyConnector = g.Key.UsesKeyConnector,\n                GroupId = g.Key.GroupId,\n                GroupName = g.Key.GroupName,\n                CollectionId = g.Key.CollectionId,\n                CollectionName = g.Key.CollectionName,\n                ReadOnly = g.Key.ReadOnly,\n                HidePasswords = g.Key.HidePasswords,\n                Manage = g.Key.Manage,\n                CipherIds = g.Select(c => c.CipherId)\n            });\n\n        var accessDetailsCount = accessDetails.Count();\n        logger.LogInformation(Constants.BypassFiltersEventId, \"Completed MemberAccessReport generation for OrganizationId: {OrganizationId}. Generated {AccessDetailsCount} access detail records\",\n            request.OrganizationId, accessDetailsCount);\n\n        return accessDetails;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessReportQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;\n\npublic interface IMemberAccessReportQuery\n{\n    Task<IEnumerable<MemberAccessReportDetail>> GetMemberAccessReportsAsync(MemberAccessReportRequest request);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IRiskInsightsReportQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;\n\npublic interface IRiskInsightsReportQuery\n{\n    Task<IEnumerable<RiskInsightsReportDetail>> GetRiskInsightsReportDetails(RiskInsightsReportRequest request);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic static class ReportingServiceCollectionExtensions\n{\n    public static void AddReportingServices(this IServiceCollection services, IGlobalSettings globalSettings)\n    {\n        services.AddExtendedCache(OrganizationReportCacheConstants.CacheName, (GlobalSettings)globalSettings);\n\n        services.AddScoped<IRiskInsightsReportQuery, RiskInsightsReportQuery>();\n        services.AddScoped<IMemberAccessReportQuery, MemberAccessReportQuery>();\n        services.AddScoped<IAddPasswordHealthReportApplicationCommand, AddPasswordHealthReportApplicationCommand>();\n        services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>();\n        services.AddScoped<IDropPasswordHealthReportApplicationCommand, DropPasswordHealthReportApplicationCommand>();\n        services.AddScoped<IAddOrganizationReportCommand, AddOrganizationReportCommand>();\n        services.AddScoped<IGetOrganizationReportQuery, GetOrganizationReportQuery>();\n        services.AddScoped<IUpdateOrganizationReportCommand, UpdateOrganizationReportCommand>();\n        services.AddScoped<IUpdateOrganizationReportSummaryCommand, UpdateOrganizationReportSummaryCommand>();\n        services.AddScoped<IGetOrganizationReportSummaryDataQuery, GetOrganizationReportSummaryDataQuery>();\n        services.AddScoped<IGetOrganizationReportSummaryDataByDateRangeQuery, GetOrganizationReportSummaryDataByDateRangeQuery>();\n        services.AddScoped<IGetOrganizationReportDataQuery, GetOrganizationReportDataQuery>();\n        services.AddScoped<IUpdateOrganizationReportDataCommand, UpdateOrganizationReportDataCommand>();\n        services.AddScoped<IGetOrganizationReportApplicationDataQuery, GetOrganizationReportApplicationDataQuery>();\n        services.AddScoped<IUpdateOrganizationReportApplicationDataCommand, UpdateOrganizationReportApplicationDataCommand>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs",
    "content": "﻿namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class AddOrganizationReportRequest\n{\n    public Guid OrganizationId { get; set; }\n    public string? ReportData { get; set; }\n\n    public string? ContentEncryptionKey { get; set; }\n\n    public string? SummaryData { get; set; }\n\n    public string? ApplicationData { get; set; }\n\n    public OrganizationReportMetricsRequest? Metrics { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class AddPasswordHealthReportApplicationRequest\n{\n    public Guid OrganizationId { get; set; }\n    public string Url { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class DropOrganizationReportRequest\n{\n    public Guid OrganizationId { get; set; }\n    public IEnumerable<Guid> OrganizationReportIds { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class DropPasswordHealthReportApplicationRequest\n{\n    public Guid OrganizationId { get; set; }\n    public IEnumerable<Guid> PasswordHealthReportApplicationIds { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/GetOrganizationReportSummaryDataByDateRangeRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class GetOrganizationReportSummaryDataByDateRangeRequest\n{\n    public Guid OrganizationId { get; set; }\n    public DateTime StartDate { get; set; }\n    public DateTime EndDate { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessReportRequest.cs",
    "content": "﻿namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class MemberAccessReportRequest\n{\n    public Guid OrganizationId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/OrganizationReportMetricsRequest.cs",
    "content": "﻿using System.Text.Json.Serialization;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class OrganizationReportMetricsRequest\n{\n    [JsonPropertyName(\"totalApplicationCount\")]\n    public int? ApplicationCount { get; set; } = null;\n    [JsonPropertyName(\"totalAtRiskApplicationCount\")]\n    public int? ApplicationAtRiskCount { get; set; } = null;\n    [JsonPropertyName(\"totalCriticalApplicationCount\")]\n    public int? CriticalApplicationCount { get; set; } = null;\n    [JsonPropertyName(\"totalCriticalAtRiskApplicationCount\")]\n    public int? CriticalApplicationAtRiskCount { get; set; } = null;\n    [JsonPropertyName(\"totalMemberCount\")]\n    public int? MemberCount { get; set; } = null;\n    [JsonPropertyName(\"totalAtRiskMemberCount\")]\n    public int? MemberAtRiskCount { get; set; } = null;\n    [JsonPropertyName(\"totalCriticalMemberCount\")]\n    public int? CriticalMemberCount { get; set; } = null;\n    [JsonPropertyName(\"totalCriticalAtRiskMemberCount\")]\n    public int? CriticalMemberAtRiskCount { get; set; } = null;\n    [JsonPropertyName(\"totalPasswordCount\")]\n    public int? PasswordCount { get; set; } = null;\n    [JsonPropertyName(\"totalAtRiskPasswordCount\")]\n    public int? PasswordAtRiskCount { get; set; } = null;\n    [JsonPropertyName(\"totalCriticalPasswordCount\")]\n    public int? CriticalPasswordCount { get; set; } = null;\n    [JsonPropertyName(\"totalCriticalAtRiskPasswordCount\")]\n    public int? CriticalPasswordAtRiskCount { get; set; } = null;\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/RiskInsightsReportRequest.cs",
    "content": "﻿namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class RiskInsightsReportRequest\n{\n    public Guid OrganizationId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportApplicationDataRequest.cs",
    "content": "﻿namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class UpdateOrganizationReportApplicationDataRequest\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string? ApplicationData { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportDataRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class UpdateOrganizationReportDataRequest\n{\n    public Guid OrganizationId { get; set; }\n    public Guid ReportId { get; set; }\n    public string ReportData { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class UpdateOrganizationReportRequest\n{\n    public Guid ReportId { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string ReportData { get; set; }\n    public string ContentEncryptionKey { get; set; }\n    public string SummaryData { get; set; } = null;\n    public string ApplicationData { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportSummaryRequest.cs",
    "content": "﻿namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;\n\npublic class UpdateOrganizationReportSummaryRequest\n{\n    public Guid OrganizationId { get; set; }\n    public Guid ReportId { get; set; }\n    public string? SummaryData { get; set; }\n    public OrganizationReportMetricsRequest? Metrics { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/RiskInsightsReportQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Reports.Repositories;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class RiskInsightsReportQuery : IRiskInsightsReportQuery\n{\n    private readonly IOrganizationMemberBaseDetailRepository _organizationMemberBaseDetailRepository;\n\n    public RiskInsightsReportQuery(IOrganizationMemberBaseDetailRepository repository)\n    {\n        _organizationMemberBaseDetailRepository = repository;\n    }\n\n    public async Task<IEnumerable<RiskInsightsReportDetail>> GetRiskInsightsReportDetails(\n        RiskInsightsReportRequest request)\n    {\n        var baseDetails =\n            await _organizationMemberBaseDetailRepository.GetOrganizationMemberBaseDetailsByOrganizationId(\n                request.OrganizationId);\n\n        var insightsDetails = baseDetails\n            .GroupBy(b => new { b.UserGuid, b.UserName, b.Email, b.UsesKeyConnector })\n            .Select(g => new RiskInsightsReportDetail\n            {\n                UserGuid = g.Key.UserGuid,\n                UserName = g.Key.UserName,\n                Email = g.Key.Email,\n                UsesKeyConnector = g.Key.UsesKeyConnector,\n                CipherIds = g\n                    .Select(x => x.CipherId.ToString())\n                    .Distinct()\n            });\n\n        return insightsDetails;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportApplicationDataCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class UpdateOrganizationReportApplicationDataCommand : IUpdateOrganizationReportApplicationDataCommand\n{\n    private readonly IOrganizationRepository _organizationRepo;\n    private readonly IOrganizationReportRepository _organizationReportRepo;\n    private readonly ILogger<UpdateOrganizationReportApplicationDataCommand> _logger;\n\n    public UpdateOrganizationReportApplicationDataCommand(\n        IOrganizationRepository organizationRepository,\n        IOrganizationReportRepository organizationReportRepository,\n        ILogger<UpdateOrganizationReportApplicationDataCommand> logger)\n    {\n        _organizationRepo = organizationRepository;\n        _organizationReportRepo = organizationReportRepository;\n        _logger = logger;\n    }\n\n    public async Task<OrganizationReport> UpdateOrganizationReportApplicationDataAsync(UpdateOrganizationReportApplicationDataRequest request)\n    {\n        try\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Updating organization report application data {reportId} for organization {organizationId}\",\n                request.Id, request.OrganizationId);\n\n            var (isValid, errorMessage) = await ValidateRequestAsync(request);\n            if (!isValid)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Failed to update organization report application data {reportId} for organization {organizationId}: {errorMessage}\",\n                    request.Id, request.OrganizationId, errorMessage);\n                throw new BadRequestException(errorMessage);\n            }\n\n            var existingReport = await _organizationReportRepo.GetByIdAsync(request.Id);\n            if (existingReport == null)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Organization report {reportId} not found\", request.Id);\n                throw new NotFoundException(\"Organization report not found\");\n            }\n\n            if (existingReport.OrganizationId != request.OrganizationId)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Organization report {reportId} does not belong to organization {organizationId}\",\n                    request.Id, request.OrganizationId);\n                throw new BadRequestException(\"Organization report does not belong to the specified organization\");\n            }\n\n            var updatedReport = await _organizationReportRepo.UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData ?? string.Empty);\n\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Successfully updated organization report application data {reportId} for organization {organizationId}\",\n                request.Id, request.OrganizationId);\n\n            return updatedReport;\n        }\n        catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))\n        {\n            _logger.LogError(ex, \"Error updating organization report application data {reportId} for organization {organizationId}\",\n                request.Id, request.OrganizationId);\n            throw;\n        }\n    }\n\n    private async Task<(bool isValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportApplicationDataRequest request)\n    {\n        if (request.OrganizationId == Guid.Empty)\n        {\n            return (false, \"OrganizationId is required\");\n        }\n\n        if (request.Id == Guid.Empty)\n        {\n            return (false, \"Id is required\");\n        }\n\n        var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);\n        if (organization == null)\n        {\n            return (false, \"Invalid Organization\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.ApplicationData))\n        {\n            return (false, \"Application Data is required\");\n        }\n\n        return (true, string.Empty);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class UpdateOrganizationReportCommand : IUpdateOrganizationReportCommand\n{\n    private readonly IOrganizationRepository _organizationRepo;\n    private readonly IOrganizationReportRepository _organizationReportRepo;\n    private readonly ILogger<UpdateOrganizationReportCommand> _logger;\n    private readonly IFusionCache _cache;\n    public UpdateOrganizationReportCommand(\n        IOrganizationRepository organizationRepository,\n        IOrganizationReportRepository organizationReportRepository,\n        ILogger<UpdateOrganizationReportCommand> logger,\n        [FromKeyedServices(OrganizationReportCacheConstants.CacheName)] IFusionCache cache)\n    {\n        _organizationRepo = organizationRepository;\n        _organizationReportRepo = organizationReportRepository;\n        _logger = logger;\n        _cache = cache;\n    }\n\n    public async Task<OrganizationReport> UpdateOrganizationReportAsync(UpdateOrganizationReportRequest request)\n    {\n        try\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Updating organization report {reportId} for organization {organizationId}\",\n                request.ReportId, request.OrganizationId);\n\n            var (isValid, errorMessage) = await ValidateRequestAsync(request);\n            if (!isValid)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Failed to update organization report {reportId} for organization {organizationId}: {errorMessage}\",\n                    request.ReportId, request.OrganizationId, errorMessage);\n                throw new BadRequestException(errorMessage);\n            }\n\n            var existingReport = await _organizationReportRepo.GetByIdAsync(request.ReportId);\n            if (existingReport == null)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Organization report {reportId} not found\", request.ReportId);\n                throw new NotFoundException(\"Organization report not found\");\n            }\n\n            if (existingReport.OrganizationId != request.OrganizationId)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Organization report {reportId} does not belong to organization {organizationId}\",\n                    request.ReportId, request.OrganizationId);\n                throw new BadRequestException(\"Organization report does not belong to the specified organization\");\n            }\n\n            existingReport.ContentEncryptionKey = request.ContentEncryptionKey;\n            existingReport.SummaryData = request.SummaryData;\n            existingReport.ReportData = request.ReportData;\n            existingReport.ApplicationData = request.ApplicationData;\n            existingReport.RevisionDate = DateTime.UtcNow;\n\n            await _organizationReportRepo.UpsertAsync(existingReport);\n\n            // Invalidate cache\n            await _cache.RemoveByTagAsync(OrganizationReportCacheConstants.BuildCacheTagForOrganizationReports(request.OrganizationId));\n\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Successfully updated organization report {reportId} for organization {organizationId}\",\n                request.ReportId, request.OrganizationId);\n\n            var response = await _organizationReportRepo.GetByIdAsync(request.ReportId);\n\n            if (response == null)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Organization report {reportId} not found after update\", request.ReportId);\n                throw new NotFoundException(\"Organization report not found after update\");\n            }\n            return response;\n        }\n        catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))\n        {\n            _logger.LogError(ex, \"Error updating organization report {reportId} for organization {organizationId}\",\n                request.ReportId, request.OrganizationId);\n            throw;\n        }\n    }\n\n    private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportRequest request)\n    {\n        if (request.OrganizationId == Guid.Empty)\n        {\n            return (false, \"OrganizationId is required\");\n        }\n\n        if (request.ReportId == Guid.Empty)\n        {\n            return (false, \"ReportId is required\");\n        }\n\n        var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);\n        if (organization == null)\n        {\n            return (false, \"Invalid Organization\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.ContentEncryptionKey))\n        {\n            return (false, \"ContentEncryptionKey is required\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.ReportData))\n        {\n            return (false, \"Report Data is required\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.SummaryData))\n        {\n            return (false, \"Summary Data is required\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.ApplicationData))\n        {\n            return (false, \"Application Data is required\");\n        }\n\n        return (true, string.Empty);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportDataCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class UpdateOrganizationReportDataCommand : IUpdateOrganizationReportDataCommand\n{\n    private readonly IOrganizationRepository _organizationRepo;\n    private readonly IOrganizationReportRepository _organizationReportRepo;\n    private readonly ILogger<UpdateOrganizationReportDataCommand> _logger;\n\n    public UpdateOrganizationReportDataCommand(\n        IOrganizationRepository organizationRepository,\n        IOrganizationReportRepository organizationReportRepository,\n        ILogger<UpdateOrganizationReportDataCommand> logger)\n    {\n        _organizationRepo = organizationRepository;\n        _organizationReportRepo = organizationReportRepository;\n        _logger = logger;\n    }\n\n    public async Task<OrganizationReport> UpdateOrganizationReportDataAsync(UpdateOrganizationReportDataRequest request)\n    {\n        try\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Updating organization report data {reportId} for organization {organizationId}\",\n                request.ReportId, request.OrganizationId);\n\n            var (isValid, errorMessage) = await ValidateRequestAsync(request);\n            if (!isValid)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Failed to update organization report data {reportId} for organization {organizationId}: {errorMessage}\",\n                    request.ReportId, request.OrganizationId, errorMessage);\n                throw new BadRequestException(errorMessage);\n            }\n\n            var existingReport = await _organizationReportRepo.GetByIdAsync(request.ReportId);\n            if (existingReport == null)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Organization report {reportId} not found\", request.ReportId);\n                throw new NotFoundException(\"Organization report not found\");\n            }\n\n            if (existingReport.OrganizationId != request.OrganizationId)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Organization report {reportId} does not belong to organization {organizationId}\",\n                    request.ReportId, request.OrganizationId);\n                throw new BadRequestException(\"Organization report does not belong to the specified organization\");\n            }\n\n            var updatedReport = await _organizationReportRepo.UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData);\n\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Successfully updated organization report data {reportId} for organization {organizationId}\",\n                request.ReportId, request.OrganizationId);\n\n            return updatedReport;\n        }\n        catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))\n        {\n            _logger.LogError(ex, \"Error updating organization report data {reportId} for organization {organizationId}\",\n                request.ReportId, request.OrganizationId);\n            throw;\n        }\n    }\n\n    private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportDataRequest request)\n    {\n        if (request.OrganizationId == Guid.Empty)\n        {\n            return (false, \"OrganizationId is required\");\n        }\n\n        if (request.ReportId == Guid.Empty)\n        {\n            return (false, \"ReportId is required\");\n        }\n\n        var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);\n        if (organization == null)\n        {\n            return (false, \"Invalid Organization\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.ReportData))\n        {\n            return (false, \"Report Data is required\");\n        }\n\n        return (true, string.Empty);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportSummaryCommand.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.Reports.ReportFeatures;\n\npublic class UpdateOrganizationReportSummaryCommand : IUpdateOrganizationReportSummaryCommand\n{\n    private readonly IOrganizationRepository _organizationRepo;\n    private readonly IOrganizationReportRepository _organizationReportRepo;\n    private readonly ILogger<UpdateOrganizationReportSummaryCommand> _logger;\n    private readonly IFusionCache _cache;\n    public UpdateOrganizationReportSummaryCommand(\n        IOrganizationRepository organizationRepository,\n        IOrganizationReportRepository organizationReportRepository,\n        ILogger<UpdateOrganizationReportSummaryCommand> logger,\n        [FromKeyedServices(OrganizationReportCacheConstants.CacheName)] IFusionCache cache)\n    {\n        _organizationRepo = organizationRepository;\n        _organizationReportRepo = organizationReportRepository;\n        _logger = logger;\n        _cache = cache;\n    }\n\n    public async Task<OrganizationReport> UpdateOrganizationReportSummaryAsync(UpdateOrganizationReportSummaryRequest request)\n    {\n        try\n        {\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Updating organization report summary {reportId} for organization {organizationId}\",\n                request.ReportId, request.OrganizationId);\n\n            var (isValid, errorMessage) = await ValidateRequestAsync(request);\n            if (!isValid)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Failed to update organization report summary {reportId} for organization {organizationId}: {errorMessage}\",\n                    request.ReportId, request.OrganizationId, errorMessage);\n                throw new BadRequestException(errorMessage);\n            }\n\n            var existingReport = await _organizationReportRepo.GetByIdAsync(request.ReportId);\n            if (existingReport == null)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Organization report {reportId} not found\", request.ReportId);\n                throw new NotFoundException(\"Organization report not found\");\n            }\n\n            if (existingReport.OrganizationId != request.OrganizationId)\n            {\n                _logger.LogWarning(Constants.BypassFiltersEventId, \"Organization report {reportId} does not belong to organization {organizationId}\",\n                    request.ReportId, request.OrganizationId);\n                throw new BadRequestException(\"Organization report does not belong to the specified organization\");\n            }\n\n            await _organizationReportRepo.UpdateMetricsAsync(request.ReportId, OrganizationReportMetricsData.From(request.OrganizationId, request.Metrics));\n            var updatedReport = await _organizationReportRepo.UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData ?? string.Empty);\n\n            // Invalidate cache\n            await _cache.RemoveByTagAsync(OrganizationReportCacheConstants.BuildCacheTagForOrganizationReports(request.OrganizationId));\n\n            _logger.LogInformation(Constants.BypassFiltersEventId, \"Successfully updated organization report summary {reportId} for organization {organizationId}\",\n                request.ReportId, request.OrganizationId);\n\n            return updatedReport;\n        }\n        catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))\n        {\n            _logger.LogError(ex, \"Error updating organization report summary {reportId} for organization {organizationId}\",\n                request.ReportId, request.OrganizationId);\n            throw;\n        }\n    }\n\n    private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportSummaryRequest request)\n    {\n        if (request.OrganizationId == Guid.Empty)\n        {\n            return (false, \"OrganizationId is required\");\n        }\n\n        if (request.ReportId == Guid.Empty)\n        {\n            return (false, \"ReportId is required\");\n        }\n\n        var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId);\n        if (organization == null)\n        {\n            return (false, \"Invalid Organization\");\n        }\n\n        if (string.IsNullOrWhiteSpace(request.SummaryData))\n        {\n            return (false, \"Summary Data is required\");\n        }\n\n        return (true, string.Empty);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Repositories/IEventRepository.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Vault.Entities;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IEventRepository\n{\n    Task<PagedResult<IEvent>> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions);\n    Task<PagedResult<IEvent>> GetManyByOrganizationAsync(Guid organizationId, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions);\n\n    Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions);\n\n    Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions);\n\n    Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions);\n    Task<PagedResult<IEvent>> GetManyByProviderAsync(Guid providerId, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions);\n    Task<PagedResult<IEvent>> GetManyByProviderActingUserAsync(Guid providerId, Guid actingUserId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions);\n    Task<PagedResult<IEvent>> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions);\n\n    Task CreateAsync(IEvent e);\n    Task CreateManyAsync(IEnumerable<IEvent> e);\n    Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId, Guid serviceAccountId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Repositories/IOrganizationApplicationRepository.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Dirt.Repositories;\n\npublic interface IOrganizationApplicationRepository : IRepository<OrganizationApplication, Guid>\n{\n    Task<ICollection<OrganizationApplication>> GetByOrganizationIdAsync(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Repositories/IOrganizationIntegrationConfigurationRepository.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Dirt.Repositories;\n\npublic interface IOrganizationIntegrationConfigurationRepository : IRepository<OrganizationIntegrationConfiguration, Guid>\n{\n    /// <summary>\n    /// Retrieve the list of available configuration details for a specific event for the organization and\n    /// integration type.<br/>\n    /// <br/>\n    /// <b>Note:</b> This returns all configurations that match the event type explicitly <b>and</b>\n    /// all the configurations that have a null event type - null event type is considered a\n    /// wildcard that matches all events.\n    ///\n    /// </summary>\n    /// <param name=\"eventType\">The specific event type</param>\n    /// <param name=\"organizationId\">The id of the organization</param>\n    /// <param name=\"integrationType\">The integration type</param>\n    /// <returns>A List of <see cref=\"OrganizationIntegrationConfigurationDetails\"/> that match</returns>\n    Task<List<OrganizationIntegrationConfigurationDetails>> GetManyByEventTypeOrganizationIdIntegrationType(\n        EventType eventType,\n        Guid organizationId,\n        IntegrationType integrationType);\n\n    Task<List<OrganizationIntegrationConfigurationDetails>> GetAllConfigurationDetailsAsync();\n\n    Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(Guid organizationIntegrationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Repositories/IOrganizationIntegrationRepository.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Dirt.Repositories;\n\npublic interface IOrganizationIntegrationRepository : IRepository<OrganizationIntegration, Guid>\n{\n    Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId);\n\n    Task<OrganizationIntegration?> GetByTeamsConfigurationTenantIdTeamId(string tenantId, string teamId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Repositories/IOrganizationMemberBaseDetailRepository.cs",
    "content": "﻿using Bit.Core.Dirt.Reports.Models.Data;\n\nnamespace Bit.Core.Dirt.Reports.Repositories;\n\npublic interface IOrganizationMemberBaseDetailRepository\n{\n    Task<IEnumerable<OrganizationMemberBaseDetail>> GetOrganizationMemberBaseDetailsByOrganizationId(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Repositories/IOrganizationReportRepository.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Dirt.Repositories;\n\npublic interface IOrganizationReportRepository : IRepository<OrganizationReport, Guid>\n{\n    // Whole OrganizationReport methods\n    Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId);\n\n    // SummaryData methods\n    Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetSummaryDataByDateRangeAsync(Guid organizationId, DateTime startDate, DateTime endDate);\n    Task<OrganizationReportSummaryDataResponse> GetSummaryDataAsync(Guid reportId);\n    Task<OrganizationReport> UpdateSummaryDataAsync(Guid orgId, Guid reportId, string summaryData);\n\n    // ReportData methods\n    Task<OrganizationReportDataResponse> GetReportDataAsync(Guid reportId);\n    Task<OrganizationReport> UpdateReportDataAsync(Guid orgId, Guid reportId, string reportData);\n\n    // ApplicationData methods\n    Task<OrganizationReportApplicationDataResponse> GetApplicationDataAsync(Guid reportId);\n    Task<OrganizationReport> UpdateApplicationDataAsync(Guid orgId, Guid reportId, string applicationData);\n\n    // Metrics methods\n    Task UpdateMetricsAsync(Guid reportId, OrganizationReportMetricsData metrics);\n}\n\n"
  },
  {
    "path": "src/Core/Dirt/Repositories/IPasswordHealthReportApplicationRepository.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Dirt.Repositories;\n\npublic interface IPasswordHealthReportApplicationRepository : IRepository<PasswordHealthReportApplication, Guid>\n{\n    Task<ICollection<PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Repositories/TableStorage/EventRepository.cs",
    "content": "﻿using Azure.Data.Tables;\nusing Bit.Core.Models.Data;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Entities;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories.TableStorage;\n\npublic class EventRepository : IEventRepository\n{\n    private readonly TableClient _tableClient;\n\n    public EventRepository(GlobalSettings globalSettings)\n        : this(globalSettings.Events.ConnectionString)\n    { }\n\n    public EventRepository(string storageConnectionString)\n    {\n        var tableClient = new TableServiceClient(storageConnectionString);\n        _tableClient = tableClient.GetTableClient(\"event\");\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"UserId={userId}\", \"Date={{0}}\", startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByOrganizationAsync(Guid organizationId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"OrganizationId={organizationId}\", \"Date={0}\", startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"OrganizationId={secret.OrganizationId}\",\n            $\"SecretId={secret.Id}__Date={{0}}\", startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"OrganizationId={project.OrganizationId}\",\n            $\"ProjectId={project.Id}__Date={{0}}\", startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"OrganizationId={organizationId}\",\n            $\"ActingUserId={actingUserId}__Date={{0}}\", startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByProviderAsync(Guid providerId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"ProviderId={providerId}\", \"Date={0}\", startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByProviderActingUserAsync(Guid providerId, Guid actingUserId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"ProviderId={providerId}\",\n            $\"ActingUserId={actingUserId}__Date={{0}}\", startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions)\n    {\n        var partitionKey = cipher.OrganizationId.HasValue ?\n            $\"OrganizationId={cipher.OrganizationId}\" : $\"UserId={cipher.UserId}\";\n        return await GetManyAsync(partitionKey, $\"CipherId={cipher.Id}__Date={{0}}\", startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(\n        Guid organizationId,\n        Guid serviceAccountId,\n        DateTime startDate,\n        DateTime endDate,\n        PageOptions pageOptions)\n    {\n        return await GetManyServiceAccountAsync(\n               $\"OrganizationId={organizationId}\",\n               serviceAccountId.ToString(),\n               startDate, endDate, pageOptions);\n\n    }\n\n    public async Task CreateAsync(IEvent e)\n    {\n        if (!(e is EventTableEntity entity))\n        {\n            throw new ArgumentException(nameof(e));\n        }\n\n        await CreateEventAsync(entity);\n    }\n\n    public async Task CreateManyAsync(IEnumerable<IEvent>? e)\n    {\n        if (e is null || !e.Any())\n        {\n            return;\n        }\n\n        if (!e.Skip(1).Any())\n        {\n            await CreateAsync(e.First());\n            return;\n        }\n\n        var entities = e.OfType<EventTableEntity>();\n        var entityGroups = entities.GroupBy(ent => ent.PartitionKey);\n        foreach (var group in entityGroups)\n        {\n            var groupEntities = group.ToList();\n            if (groupEntities.Count == 1)\n            {\n                await CreateEventAsync(groupEntities.First());\n                continue;\n            }\n\n            // A batch insert can only contain 100 entities at a time\n            var iterations = groupEntities.Count / 100;\n            for (var i = 0; i <= iterations; i++)\n            {\n                var batch = new List<TableTransactionAction>();\n                var batchEntities = groupEntities.Skip(i * 100).Take(100);\n                if (!batchEntities.Any())\n                {\n                    break;\n                }\n\n                foreach (var entity in batchEntities)\n                {\n                    batch.Add(new TableTransactionAction(TableTransactionActionType.Add,\n                        entity.ToAzureEvent()));\n                }\n\n                await _tableClient.SubmitTransactionAsync(batch);\n            }\n        }\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyServiceAccountAsync(\n        string partitionKey,\n        string serviceAccountId,\n        DateTime startDate,\n        DateTime endDate,\n        PageOptions pageOptions)\n    {\n        var start = CoreHelpers.DateTimeToTableStorageKey(startDate);\n        var end = CoreHelpers.DateTimeToTableStorageKey(endDate);\n        var filter = MakeFilterForServiceAccount(partitionKey, serviceAccountId, startDate, endDate);\n\n        var result = new PagedResult<IEvent>();\n        var query = _tableClient.QueryAsync<AzureEvent>(filter, pageOptions.PageSize);\n\n        await using (var enumerator = query.AsPages(pageOptions.ContinuationToken,\n            pageOptions.PageSize).GetAsyncEnumerator())\n        {\n            if (await enumerator.MoveNextAsync())\n            {\n                result.ContinuationToken = enumerator.Current.ContinuationToken;\n\n                var events = enumerator.Current.Values\n                    .Select(e => e.ToEventTableEntity())\n                    .ToList();\n\n                events = events.OrderByDescending(e => e.Date).ToList();\n\n                result.Data.AddRange(events);\n            }\n        }\n\n        return result;\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyAsync(string partitionKey, string rowKey,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        var start = CoreHelpers.DateTimeToTableStorageKey(startDate);\n        var end = CoreHelpers.DateTimeToTableStorageKey(endDate);\n        var filter = MakeFilter(partitionKey, string.Format(rowKey, start), string.Format(rowKey, end));\n\n        var result = new PagedResult<IEvent>();\n        var query = _tableClient.QueryAsync<AzureEvent>(filter, pageOptions.PageSize);\n\n        await using (var enumerator = query.AsPages(pageOptions.ContinuationToken,\n            pageOptions.PageSize).GetAsyncEnumerator())\n        {\n            await enumerator.MoveNextAsync();\n\n            result.ContinuationToken = enumerator.Current.ContinuationToken;\n            result.Data.AddRange(enumerator.Current.Values.Select(e => e.ToEventTableEntity()));\n        }\n\n        return result;\n    }\n\n    private async Task CreateEventAsync(EventTableEntity entity)\n    {\n        await _tableClient.UpsertEntityAsync(entity.ToAzureEvent());\n    }\n\n    private string MakeFilter(string partitionKey, string rowStart, string rowEnd)\n    {\n        return $\"PartitionKey eq '{partitionKey}' and RowKey le '{rowStart}' and RowKey ge '{rowEnd}'\";\n    }\n\n    private string MakeFilterForServiceAccount(\n        string partitionKey,\n        string machineAccountId,\n        DateTime startDate,\n        DateTime endDate)\n    {\n        var start = CoreHelpers.DateTimeToTableStorageKey(startDate);\n        var end = CoreHelpers.DateTimeToTableStorageKey(endDate);\n\n        var rowKey1Start = $\"ServiceAccountId={machineAccountId}__Date={start}\";\n        var rowKey1End = $\"ServiceAccountId={machineAccountId}__Date={end}\";\n\n        var rowKey2Start = $\"GrantedServiceAccountId={machineAccountId}__Date={start}\";\n        var rowKey2End = $\"GrantedServiceAccountId={machineAccountId}__Date={end}\";\n\n        var left = $\"PartitionKey eq '{partitionKey}' and RowKey le '{rowKey1Start}' and RowKey ge '{rowKey1End}'\";\n        var right = $\"PartitionKey eq '{partitionKey}' and RowKey le '{rowKey2Start}' and RowKey ge '{rowKey2End}'\";\n\n        return $\"({left}) or ({right})\";\n    }\n\n\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/IAzureServiceBusService.cs",
    "content": "﻿using Azure.Messaging.ServiceBus;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\n\nnamespace Bit.Core.Dirt.Services;\n\npublic interface IAzureServiceBusService : IEventIntegrationPublisher, IAsyncDisposable\n{\n    ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName, ServiceBusProcessorOptions options);\n    Task PublishToRetryAsync(IIntegrationMessage message);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/IEventIntegrationPublisher.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.EventIntegrations;\n\nnamespace Bit.Core.Dirt.Services;\n\npublic interface IEventIntegrationPublisher : IAsyncDisposable\n{\n    Task PublishAsync(IIntegrationMessage message);\n    Task PublishEventAsync(string body, string? organizationId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/IEventMessageHandler.cs",
    "content": "﻿using Bit.Core.Models.Data;\n\nnamespace Bit.Core.Dirt.Services;\n\npublic interface IEventMessageHandler\n{\n    Task HandleEventAsync(EventMessage eventMessage);\n\n    Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/IEventWriteService.cs",
    "content": "﻿using Bit.Core.Models.Data;\n\nnamespace Bit.Core.Services;\n\npublic interface IEventWriteService\n{\n    Task CreateAsync(IEvent e);\n    Task CreateManyAsync(IEnumerable<IEvent> e);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/IIntegrationFilterService.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.Dirt.Services;\n\npublic interface IIntegrationFilterService\n{\n    bool EvaluateFilterGroup(IntegrationFilterGroup group, EventMessage message);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/IIntegrationHandler.cs",
    "content": "﻿using System.Globalization;\nusing System.Net;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\n\nnamespace Bit.Core.Dirt.Services;\n\npublic interface IIntegrationHandler\n{\n    Task<IntegrationHandlerResult> HandleAsync(string json);\n}\n\npublic interface IIntegrationHandler<T> : IIntegrationHandler\n{\n    Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message);\n}\n\npublic abstract class IntegrationHandlerBase<T> : IIntegrationHandler<T>\n{\n    public async Task<IntegrationHandlerResult> HandleAsync(string json)\n    {\n        var message = IntegrationMessage<T>.FromJson(json);\n        return await HandleAsync(message ?? throw new ArgumentException(\"IntegrationMessage was null when created from the provided JSON\"));\n    }\n\n    public abstract Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message);\n\n    protected IntegrationHandlerResult ResultFromHttpResponse(\n        HttpResponseMessage response,\n        IntegrationMessage<T> message,\n        TimeProvider timeProvider)\n    {\n        if (response.IsSuccessStatusCode)\n        {\n            return IntegrationHandlerResult.Succeed(message);\n        }\n\n        var category = ClassifyHttpStatusCode(response.StatusCode);\n        var failureReason = response.ReasonPhrase ?? $\"Failure with status code {(int)response.StatusCode}\";\n\n        if (category is not (IntegrationFailureCategory.RateLimited\n                or IntegrationFailureCategory.TransientError\n                or IntegrationFailureCategory.ServiceUnavailable) ||\n            !response.Headers.TryGetValues(\"Retry-After\", out var values)\n           )\n        {\n            return IntegrationHandlerResult.Fail(message: message, category: category, failureReason: failureReason);\n        }\n\n        // Handle Retry-After header for rate-limited and retryable errors\n        DateTime? delayUntil = null;\n        var value = values.FirstOrDefault();\n        if (int.TryParse(value, out var seconds))\n        {\n            // Retry-after was specified in seconds\n            delayUntil = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime;\n        }\n        else if (DateTimeOffset.TryParseExact(value,\n                     \"r\", // \"r\" is the round-trip format: RFC1123\n                     CultureInfo.InvariantCulture,\n                     DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,\n                     out var retryDate))\n        {\n            // Retry-after was specified as a date\n            delayUntil = retryDate.UtcDateTime;\n        }\n\n        return IntegrationHandlerResult.Fail(\n            message,\n            category,\n            failureReason,\n            delayUntil\n        );\n    }\n\n    /// <summary>\n    /// Classifies an <see cref=\"HttpStatusCode\"/> as an <see cref=\"IntegrationFailureCategory\"/> to drive\n    /// retry behavior and operator-facing failure reporting.\n    /// </summary>\n    /// <param name=\"statusCode\">The HTTP status code.</param>\n    /// <returns>The corresponding <see cref=\"IntegrationFailureCategory\"/>.</returns>\n    protected static IntegrationFailureCategory ClassifyHttpStatusCode(HttpStatusCode statusCode)\n    {\n        var explicitCategory = statusCode switch\n        {\n            HttpStatusCode.Unauthorized => IntegrationFailureCategory.AuthenticationFailed,\n            HttpStatusCode.Forbidden => IntegrationFailureCategory.AuthenticationFailed,\n            HttpStatusCode.NotFound => IntegrationFailureCategory.ConfigurationError,\n            HttpStatusCode.Gone => IntegrationFailureCategory.ConfigurationError,\n            HttpStatusCode.MovedPermanently => IntegrationFailureCategory.ConfigurationError,\n            HttpStatusCode.TemporaryRedirect => IntegrationFailureCategory.ConfigurationError,\n            HttpStatusCode.PermanentRedirect => IntegrationFailureCategory.ConfigurationError,\n            HttpStatusCode.TooManyRequests => IntegrationFailureCategory.RateLimited,\n            HttpStatusCode.RequestTimeout => IntegrationFailureCategory.TransientError,\n            HttpStatusCode.InternalServerError => IntegrationFailureCategory.TransientError,\n            HttpStatusCode.BadGateway => IntegrationFailureCategory.TransientError,\n            HttpStatusCode.GatewayTimeout => IntegrationFailureCategory.TransientError,\n            HttpStatusCode.ServiceUnavailable => IntegrationFailureCategory.ServiceUnavailable,\n            HttpStatusCode.NotImplemented => IntegrationFailureCategory.PermanentFailure,\n            _ => (IntegrationFailureCategory?)null\n        };\n\n        if (explicitCategory is not null)\n        {\n            return explicitCategory.Value;\n        }\n\n        return (int)statusCode switch\n        {\n            >= 300 and <= 399 => IntegrationFailureCategory.ConfigurationError,\n            >= 400 and <= 499 => IntegrationFailureCategory.ConfigurationError,\n            >= 500 and <= 599 => IntegrationFailureCategory.ServiceUnavailable,\n            _ => IntegrationFailureCategory.ServiceUnavailable\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/IOrganizationIntegrationConfigurationValidator.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\n\nnamespace Bit.Core.Dirt.Services;\n\npublic interface IOrganizationIntegrationConfigurationValidator\n{\n    /// <summary>\n    /// Validates that the configuration is valid for the given integration type. The configuration must\n    /// include a Configuration that is valid for the type, valid Filters, and a non-empty Template\n    /// to pass validation.\n    /// </summary>\n    /// <param name=\"integrationType\">The type of integration</param>\n    /// <param name=\"configuration\">The OrganizationIntegrationConfiguration to validate</param>\n    /// <returns>True if valid, false otherwise</returns>\n    bool ValidateConfiguration(IntegrationType integrationType, OrganizationIntegrationConfiguration configuration);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/IRabbitMqService.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing RabbitMQ.Client;\nusing RabbitMQ.Client.Events;\n\nnamespace Bit.Core.Dirt.Services;\n\npublic interface IRabbitMqService : IEventIntegrationPublisher\n{\n    Task<IChannel> CreateChannelAsync(CancellationToken cancellationToken = default);\n    Task CreateEventQueueAsync(string queueName, CancellationToken cancellationToken = default);\n    Task CreateIntegrationQueuesAsync(\n        string queueName,\n        string retryQueueName,\n        string routingKey,\n        CancellationToken cancellationToken = default);\n    Task PublishToRetryAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken);\n    Task PublishToDeadLetterAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken);\n    Task RepublishToRetryQueueAsync(IChannel channel, BasicDeliverEventArgs eventArgs);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/ISlackService.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.Slack;\nusing Bit.Core.Dirt.Services.Implementations;\n\nnamespace Bit.Core.Dirt.Services;\n\n/// <summary>Defines operations for interacting with Slack, including OAuth authentication, channel discovery,\n/// and sending messages.</summary>\npublic interface ISlackService\n{\n    /// <remarks>Note: This API is not currently used (yet) by any server code. It is here to provide functionality if\n    /// the UI needs to be able to look up channels for a user.</remarks>\n    /// <summary>Retrieves the ID of a Slack channel by name.\n    /// See <see href=\"https://api.slack.com/methods/conversations.list\">conversations.list API</see>.</summary>\n    /// <param name=\"token\">A valid Slack OAuth access token.</param>\n    /// <param name=\"channelName\">The name of the channel to look up.</param>\n    /// <returns>The channel ID if found; otherwise, an empty string.</returns>\n    Task<string> GetChannelIdAsync(string token, string channelName);\n\n    /// <remarks>Note: This API is not currently used (yet) by any server code. It is here to provide functionality if\n    /// the UI needs to be able to look up channels for a user.</remarks>\n    /// <summary>Retrieves the IDs of multiple Slack channels by name.\n    /// See <see href=\"https://api.slack.com/methods/conversations.list\">conversations.list API</see>.</summary>\n    /// <param name=\"token\">A valid Slack OAuth access token.</param>\n    /// <param name=\"channelNames\">A list of channel names to look up.</param>\n    /// <returns>A list of matching channel IDs. Channels that cannot be found are omitted.</returns>\n    Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames);\n\n    /// <remarks>Note: This API is not currently used (yet) by any server code. It is here to provide functionality if\n    /// the UI needs to be able to look up a user by their email address.</remarks>\n    /// <summary>Retrieves the DM channel ID for a Slack user by email.\n    /// See <see href=\"https://api.slack.com/methods/users.lookupByEmail\">users.lookupByEmail API</see> and\n    /// <see href=\"https://api.slack.com/methods/conversations.open\">conversations.open API</see>.</summary>\n    /// <param name=\"token\">A valid Slack OAuth access token.</param>\n    /// <param name=\"email\">The email address of the user to open a DM with.</param>\n    /// <returns>The DM channel ID if successful; otherwise, an empty string.</returns>\n    Task<string> GetDmChannelByEmailAsync(string token, string email);\n\n    /// <summary>Builds the Slack OAuth 2.0 authorization URL for the app.\n    /// See <see href=\"https://api.slack.com/authentication/oauth-v2\">Slack OAuth v2 documentation</see>.</summary>\n    /// <param name=\"callbackUrl\">The absolute redirect URI that Slack will call after user authorization.\n    /// Must match the URI registered with the app configuration.</param>\n    /// <param name=\"state\">A state token used to correlate the request and callback and prevent CSRF attacks.</param>\n    /// <returns>The full authorization URL to which the user should be redirected to begin the sign-in process.</returns>\n    string GetRedirectUrl(string callbackUrl, string state);\n\n    /// <summary>Exchanges a Slack OAuth code for an access token.\n    /// See <see href=\"https://api.slack.com/methods/oauth.v2.access\">oauth.v2.access API</see>.</summary>\n    /// <param name=\"code\">The authorization code returned by Slack via the callback URL after user authorization.</param>\n    /// <param name=\"redirectUrl\">The redirect URI that was used in the authorization request.</param>\n    /// <returns>A valid Slack access token if successful; otherwise, an empty string.</returns>\n    Task<string> ObtainTokenViaOAuth(string code, string redirectUrl);\n\n    /// <summary>Sends a message to a Slack channel by ID.\n    /// See <see href=\"https://api.slack.com/methods/chat.postMessage\">chat.postMessage API</see>.</summary>\n    /// <remarks>This is used primarily by the <see cref=\"SlackIntegrationHandler\"/> to send events to the\n    /// Slack channel.</remarks>\n    /// <param name=\"token\">A valid Slack OAuth access token.</param>\n    /// <param name=\"message\">The message text to send.</param>\n    /// <param name=\"channelId\">The channel ID to send the message to.</param>\n    /// <returns>The response from Slack after sending the message.</returns>\n    Task<SlackSendMessageResponse?> SendSlackMessageByChannelIdAsync(string token, string message, string channelId);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/ITeamsService.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.Teams;\nusing Bit.Core.Dirt.Services.Implementations;\n\nnamespace Bit.Core.Dirt.Services;\n\n/// <summary>\n/// Service that provides functionality relating to the Microsoft Teams integration including OAuth,\n/// team discovery and sending a message to a channel in Teams.\n/// </summary>\npublic interface ITeamsService\n{\n    /// <summary>\n    /// Generate the Microsoft Teams OAuth 2.0 authorization URL used to begin the sign-in flow.\n    /// </summary>\n    /// <param name=\"callbackUrl\">The absolute redirect URI that Microsoft will call after user authorization.\n    /// Must match the URI registered with the app configuration.</param>\n    /// <param name=\"state\">A state token used to correlate the request and callback and prevent CSRF attacks.</param>\n    /// <returns>The full authorization URL to which the user should be redirected to begin the sign-in process.</returns>\n    string GetRedirectUrl(string callbackUrl, string state);\n\n    /// <summary>\n    /// Exchange the OAuth code for a Microsoft Graph API access token.\n    /// </summary>\n    /// <param name=\"code\">The code returned from Microsoft via the OAuth callback Url.</param>\n    /// <param name=\"redirectUrl\">The same redirect URI that was passed to the authorization request.</param>\n    /// <returns>A valid Microsoft Graph access token if the exchange succeeds; otherwise, an empty string.</returns>\n    Task<string> ObtainTokenViaOAuth(string code, string redirectUrl);\n\n    /// <summary>\n    /// Get the Teams to which the authenticated user belongs via Microsoft Graph API.\n    /// </summary>\n    /// <param name=\"accessToken\">A valid Microsoft Graph access token for the user (obtained via OAuth).</param>\n    /// <returns>A read-only list of <see cref=\"TeamInfo\"/> objects representing the user’s joined teams.\n    /// Returns an empty list if the request fails or if the token is invalid.</returns>\n    Task<IReadOnlyList<TeamInfo>> GetJoinedTeamsAsync(string accessToken);\n\n    /// <summary>\n    /// Send a message to a specific channel in Teams.\n    /// </summary>\n    /// <remarks>This is used primarily by the <see cref=\"TeamsIntegrationHandler\"/> to send events to the\n    /// Teams channel.</remarks>\n    /// <param name=\"serviceUri\">The service URI associated with the Microsoft Bot Framework connector for the target\n    /// team. Obtained via the bot framework callback.</param>\n    /// <param name=\"channelId\"> The conversation or channel ID where the message should be delivered. Obtained via\n    /// the bot framework callback.</param>\n    /// <param name=\"message\">The message text to post to the channel.</param>\n    /// <returns>A task that completes when the message has been sent. Errors during message delivery are surfaced\n    /// as exceptions from the underlying connector client.</returns>\n    Task SendMessageToChannelAsync(Uri serviceUri, string channelId, string message);\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/AzureQueueEventWriteService.cs",
    "content": "﻿using Azure.Storage.Queues;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Services;\n\npublic class AzureQueueEventWriteService : AzureQueueService<IEvent>, IEventWriteService\n{\n    public AzureQueueEventWriteService(GlobalSettings globalSettings) : base(\n        new QueueClient(globalSettings.Events.ConnectionString, globalSettings.Events.QueueName),\n        JsonHelpers.IgnoreWritingNull)\n    { }\n\n    public Task CreateAsync(IEvent e) => CreateManyAsync(new[] { e });\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/AzureServiceBusEventListenerService.cs",
    "content": "﻿using System.Text;\nusing Azure.Messaging.ServiceBus;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class AzureServiceBusEventListenerService<TConfiguration> : EventLoggingListenerService\n    where TConfiguration : IEventListenerConfiguration\n{\n    private readonly ServiceBusProcessor _processor;\n\n    public AzureServiceBusEventListenerService(\n        TConfiguration configuration,\n        IEventMessageHandler handler,\n        IAzureServiceBusService serviceBusService,\n        ServiceBusProcessorOptions serviceBusOptions,\n        ILoggerFactory loggerFactory)\n        : base(handler, CreateLogger(loggerFactory, configuration))\n    {\n        _processor = serviceBusService.CreateProcessor(\n            topicName: configuration.EventTopicName,\n            subscriptionName: configuration.EventSubscriptionName,\n            options: serviceBusOptions);\n    }\n\n    protected override async Task ExecuteAsync(CancellationToken cancellationToken)\n    {\n        _processor.ProcessMessageAsync += ProcessReceivedMessageAsync;\n        _processor.ProcessErrorAsync += ProcessErrorAsync;\n\n        await _processor.StartProcessingAsync(cancellationToken);\n    }\n\n    public override async Task StopAsync(CancellationToken cancellationToken)\n    {\n        await _processor.StopProcessingAsync(cancellationToken);\n        await _processor.DisposeAsync();\n        await base.StopAsync(cancellationToken);\n    }\n\n    private static ILogger CreateLogger(ILoggerFactory loggerFactory, TConfiguration configuration)\n    {\n        return loggerFactory.CreateLogger(\n            categoryName: $\"Bit.Core.Dirt.Services.Implementations.AzureServiceBusEventListenerService.{configuration.EventSubscriptionName}\");\n    }\n\n    internal Task ProcessErrorAsync(ProcessErrorEventArgs args)\n    {\n        _logger.LogError(\n            args.Exception,\n            \"An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}\",\n            args.EntityPath,\n            args.ErrorSource\n        );\n        return Task.CompletedTask;\n    }\n\n    private async Task ProcessReceivedMessageAsync(ProcessMessageEventArgs args)\n    {\n        await ProcessReceivedMessageAsync(Encoding.UTF8.GetString(args.Message.Body), args.Message.MessageId);\n        await args.CompleteMessageAsync(args.Message);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/AzureServiceBusIntegrationListenerService.cs",
    "content": "﻿using Azure.Messaging.ServiceBus;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class AzureServiceBusIntegrationListenerService<TConfiguration> : BackgroundService\n    where TConfiguration : IIntegrationListenerConfiguration\n{\n    private readonly int _maxRetries;\n    private readonly IAzureServiceBusService _serviceBusService;\n    private readonly IIntegrationHandler _handler;\n    private readonly ServiceBusProcessor _processor;\n    private readonly ILogger _logger;\n\n    public AzureServiceBusIntegrationListenerService(\n        TConfiguration configuration,\n        IIntegrationHandler handler,\n        IAzureServiceBusService serviceBusService,\n        ServiceBusProcessorOptions serviceBusOptions,\n        ILoggerFactory loggerFactory)\n    {\n        _handler = handler;\n        _logger = loggerFactory.CreateLogger(\n            categoryName: $\"Bit.Core.Dirt.Services.Implementations.AzureServiceBusIntegrationListenerService.{configuration.IntegrationSubscriptionName}\");\n        _maxRetries = configuration.MaxRetries;\n        _serviceBusService = serviceBusService;\n\n        _processor = _serviceBusService.CreateProcessor(\n            topicName: configuration.IntegrationTopicName,\n            subscriptionName: configuration.IntegrationSubscriptionName,\n            options: serviceBusOptions);\n    }\n\n    protected override async Task ExecuteAsync(CancellationToken cancellationToken)\n    {\n        _processor.ProcessMessageAsync += HandleMessageAsync;\n        _processor.ProcessErrorAsync += ProcessErrorAsync;\n\n        await _processor.StartProcessingAsync(cancellationToken);\n    }\n\n    public override async Task StopAsync(CancellationToken cancellationToken)\n    {\n        await _processor.StopProcessingAsync(cancellationToken);\n        await _processor.DisposeAsync();\n        await base.StopAsync(cancellationToken);\n    }\n\n    internal Task ProcessErrorAsync(ProcessErrorEventArgs args)\n    {\n        _logger.LogError(\n            args.Exception,\n            \"An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}\",\n            args.EntityPath,\n            args.ErrorSource\n        );\n        return Task.CompletedTask;\n    }\n\n    internal async Task<bool> HandleMessageAsync(string body)\n    {\n        try\n        {\n            var result = await _handler.HandleAsync(body);\n            var message = result.Message;\n\n            if (result.Success)\n            {\n                // Successful integration. Return true to indicate the message has been handled\n                return true;\n            }\n\n            message.ApplyRetry(result.DelayUntilDate);\n\n            if (result.Retryable && message.RetryCount < _maxRetries)\n            {\n                // Publish message to the retry queue. It will be re-published for retry after a delay\n                // Return true to indicate the message has been handled\n                await _serviceBusService.PublishToRetryAsync(message);\n                return true;\n            }\n            else\n            {\n                // Non-recoverable failure or exceeded the max number of retries\n                // Return false to indicate this message should be dead-lettered\n                _logger.LogWarning(\n                    \"Integration failure - non-recoverable error or max retries exceeded. \" +\n                    \"MessageId: {MessageId}, IntegrationType: {IntegrationType}, OrganizationId: {OrgId}, \" +\n                    \"FailureCategory: {Category}, Reason: {Reason}, RetryCount: {RetryCount}, MaxRetries: {MaxRetries}\",\n                    message.MessageId,\n                    message.IntegrationType,\n                    message.OrganizationId,\n                    result.Category,\n                    result.FailureReason,\n                    message.RetryCount,\n                    _maxRetries);\n                return false;\n            }\n        }\n        catch (Exception ex)\n        {\n            // Unknown exception - log error, return true so the message will be acknowledged and not resent\n            _logger.LogError(ex, \"Unhandled error processing ASB message\");\n            return true;\n        }\n    }\n\n    private async Task HandleMessageAsync(ProcessMessageEventArgs args)\n    {\n        var json = args.Message.Body.ToString();\n        if (await HandleMessageAsync(json))\n        {\n            await args.CompleteMessageAsync(args.Message);\n        }\n        else\n        {\n            await args.DeadLetterMessageAsync(args.Message, \"Retry limit exceeded or non-retryable\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/AzureServiceBusService.cs",
    "content": "﻿using Azure.Messaging.ServiceBus;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class AzureServiceBusService : IAzureServiceBusService\n{\n    private readonly ServiceBusClient _client;\n    private readonly ServiceBusSender _eventSender;\n    private readonly ServiceBusSender _integrationSender;\n\n    public AzureServiceBusService(GlobalSettings globalSettings)\n    {\n        _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);\n        _eventSender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName);\n        _integrationSender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName);\n    }\n\n    public ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName, ServiceBusProcessorOptions options)\n    {\n        return _client.CreateProcessor(topicName, subscriptionName, options);\n    }\n\n    public async Task PublishAsync(IIntegrationMessage message)\n    {\n        var json = message.ToJson();\n\n        var serviceBusMessage = new ServiceBusMessage(json)\n        {\n            Subject = message.IntegrationType.ToRoutingKey(),\n            MessageId = message.MessageId,\n            PartitionKey = message.OrganizationId\n        };\n\n        await _integrationSender.SendMessageAsync(serviceBusMessage);\n    }\n\n    public async Task PublishToRetryAsync(IIntegrationMessage message)\n    {\n        var json = message.ToJson();\n\n        var serviceBusMessage = new ServiceBusMessage(json)\n        {\n            Subject = message.IntegrationType.ToRoutingKey(),\n            ScheduledEnqueueTime = message.DelayUntilDate ?? DateTime.UtcNow,\n            MessageId = message.MessageId,\n            PartitionKey = message.OrganizationId\n        };\n\n        await _integrationSender.SendMessageAsync(serviceBusMessage);\n    }\n\n    public async Task PublishEventAsync(string body, string? organizationId)\n    {\n        var message = new ServiceBusMessage(body)\n        {\n            ContentType = \"application/json\",\n            MessageId = Guid.NewGuid().ToString(),\n            PartitionKey = organizationId\n        };\n\n        await _eventSender.SendMessageAsync(message);\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        await _eventSender.DisposeAsync();\n        await _integrationSender.DisposeAsync();\n        await _client.DisposeAsync();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/AzureTableStorageEventHandler.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class AzureTableStorageEventHandler(\n    [FromKeyedServices(\"persistent\")] IEventWriteService eventWriteService)\n    : IEventMessageHandler\n{\n    public Task HandleEventAsync(EventMessage eventMessage)\n    {\n        return eventWriteService.CreateManyAsync(EventTableEntity.IndexEvent(eventMessage));\n    }\n\n    public Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)\n    {\n        return eventWriteService.CreateManyAsync(eventMessages.SelectMany(EventTableEntity.IndexEvent));\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/DatadogIntegrationHandler.cs",
    "content": "﻿using System.Text;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class DatadogIntegrationHandler(\n    IHttpClientFactory httpClientFactory,\n    TimeProvider timeProvider)\n    : IntegrationHandlerBase<DatadogIntegrationConfigurationDetails>\n{\n    private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);\n\n    public const string HttpClientName = \"DatadogIntegrationHandlerHttpClient\";\n\n    public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)\n    {\n        var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Uri);\n        request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, \"application/json\");\n        request.Headers.Add(\"DD-API-KEY\", message.Configuration.ApiKey);\n\n        var response = await _httpClient.SendAsync(request);\n\n        return ResultFromHttpResponse(response, message, timeProvider);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/EventIntegrationEventWriteService.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\npublic class EventIntegrationEventWriteService : IEventWriteService, IAsyncDisposable\n{\n    private readonly IEventIntegrationPublisher _eventIntegrationPublisher;\n\n    public EventIntegrationEventWriteService(IEventIntegrationPublisher eventIntegrationPublisher)\n    {\n        _eventIntegrationPublisher = eventIntegrationPublisher;\n    }\n\n    public async Task CreateAsync(IEvent e)\n    {\n        var body = JsonSerializer.Serialize(e);\n        await _eventIntegrationPublisher.PublishEventAsync(body: body, organizationId: e.OrganizationId?.ToString());\n    }\n\n    public async Task CreateManyAsync(IEnumerable<IEvent> events)\n    {\n        var eventList = events as IList<IEvent> ?? events.ToList();\n        if (eventList.Count == 0)\n        {\n            return;\n        }\n\n        var organizationId = eventList[0].OrganizationId?.ToString();\n        var body = JsonSerializer.Serialize(eventList);\n        await _eventIntegrationPublisher.PublishEventAsync(body: body, organizationId: organizationId);\n    }\n    public async ValueTask DisposeAsync()\n    {\n        await _eventIntegrationPublisher.DisposeAsync();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/EventIntegrationHandler.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Logging;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class EventIntegrationHandler<T>(\n    IntegrationType integrationType,\n    IEventIntegrationPublisher eventIntegrationPublisher,\n    IIntegrationFilterService integrationFilterService,\n    IFusionCache cache,\n    IOrganizationIntegrationConfigurationRepository configurationRepository,\n    IGroupRepository groupRepository,\n    IOrganizationRepository organizationRepository,\n    IOrganizationUserRepository organizationUserRepository,\n    ILogger<EventIntegrationHandler<T>> logger)\n    : IEventMessageHandler\n{\n    public async Task HandleEventAsync(EventMessage eventMessage)\n    {\n        foreach (var configuration in await GetConfigurationDetailsListAsync(eventMessage))\n        {\n            try\n            {\n                if (configuration.Filters is string filterJson)\n                {\n                    // Evaluate filters - if false, then discard and do not process\n                    var filters = JsonSerializer.Deserialize<IntegrationFilterGroup>(filterJson)\n                        ?? throw new InvalidOperationException($\"Failed to deserialize Filters to FilterGroup\");\n                    if (!integrationFilterService.EvaluateFilterGroup(filters, eventMessage))\n                    {\n                        continue;\n                    }\n                }\n\n                // Valid filter - assemble message and publish to Integration topic/exchange\n                var template = configuration.Template ?? string.Empty;\n                var context = await BuildContextAsync(eventMessage, template);\n                var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context);\n                var messageId = eventMessage.IdempotencyId ?? Guid.NewGuid();\n                var config = configuration.MergedConfiguration.Deserialize<T>()\n                    ?? throw new InvalidOperationException($\"Failed to deserialize to {typeof(T).Name} - bad Configuration\");\n\n                var message = new IntegrationMessage<T>\n                {\n                    IntegrationType = integrationType,\n                    MessageId = messageId.ToString(),\n                    OrganizationId = eventMessage.OrganizationId?.ToString(),\n                    Configuration = config,\n                    RenderedTemplate = renderedTemplate,\n                    RetryCount = 0,\n                    DelayUntilDate = null\n                };\n\n                await eventIntegrationPublisher.PublishAsync(message);\n            }\n            catch (Exception exception)\n            {\n                logger.LogError(\n                    exception,\n                    \"Failed to publish Integration Message for {Type}, check Id {RecordId} for error in Configuration or Filters\",\n                    typeof(T).Name,\n                    configuration.Id);\n            }\n        }\n    }\n\n    public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)\n    {\n        foreach (var eventMessage in eventMessages)\n        {\n            await HandleEventAsync(eventMessage);\n        }\n    }\n\n    internal async Task<IntegrationTemplateContext> BuildContextAsync(EventMessage eventMessage, string template)\n    {\n        // Note: All of these cache calls use the default options, including TTL of 30 minutes\n\n        var context = new IntegrationTemplateContext(eventMessage);\n\n        if (IntegrationTemplateProcessor.TemplateRequiresGroup(template) && eventMessage.GroupId.HasValue)\n        {\n            context.Group = await cache.GetOrSetAsync<Group?>(\n                key: EventIntegrationsCacheConstants.BuildCacheKeyForGroup(eventMessage.GroupId.Value),\n                factory: async _ => await groupRepository.GetByIdAsync(eventMessage.GroupId.Value)\n            );\n        }\n\n        if (eventMessage.OrganizationId is not Guid organizationId)\n        {\n            return context;\n        }\n\n        if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue)\n        {\n            context.User = await GetUserFromCacheAsync(organizationId, eventMessage.UserId.Value);\n        }\n\n        if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue)\n        {\n            context.ActingUser = await GetUserFromCacheAsync(organizationId, eventMessage.ActingUserId.Value);\n        }\n\n        if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template))\n        {\n            context.Organization = await cache.GetOrSetAsync<Organization?>(\n                key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(organizationId),\n                factory: async _ => await organizationRepository.GetByIdAsync(organizationId)\n            );\n        }\n\n        return context;\n    }\n\n    private async Task<List<OrganizationIntegrationConfigurationDetails>> GetConfigurationDetailsListAsync(EventMessage eventMessage)\n    {\n        if (eventMessage.OrganizationId is not Guid organizationId)\n        {\n            return [];\n        }\n\n        List<OrganizationIntegrationConfigurationDetails> configurations = [];\n\n        var integrationTag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n            organizationId,\n            integrationType\n        );\n\n        configurations.AddRange(await cache.GetOrSetAsync<List<OrganizationIntegrationConfigurationDetails>>(\n            key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                organizationId: organizationId,\n                integrationType: integrationType,\n                eventType: eventMessage.Type),\n            factory: async _ => await configurationRepository.GetManyByEventTypeOrganizationIdIntegrationType(\n                eventType: eventMessage.Type,\n                organizationId: organizationId,\n                integrationType: integrationType),\n            options: new FusionCacheEntryOptions(\n                duration: EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails),\n            tags: [integrationTag]\n        ));\n\n        return configurations;\n    }\n\n    private async Task<OrganizationUserUserDetails?> GetUserFromCacheAsync(Guid organizationId, Guid userId) =>\n        await cache.GetOrSetAsync<OrganizationUserUserDetails?>(\n            key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(organizationId, userId),\n            factory: async _ => await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(\n                organizationId: organizationId,\n                userId: userId\n            )\n        );\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/EventLoggingListenerService.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Models.Data;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic abstract class EventLoggingListenerService : BackgroundService\n{\n    protected readonly IEventMessageHandler _handler;\n    protected ILogger _logger;\n\n    protected EventLoggingListenerService(IEventMessageHandler handler, ILogger logger)\n    {\n        _handler = handler;\n        _logger = logger;\n    }\n\n    internal async Task ProcessReceivedMessageAsync(string body, string? messageId)\n    {\n        try\n        {\n            using var jsonDocument = JsonDocument.Parse(body);\n            var root = jsonDocument.RootElement;\n\n            if (root.ValueKind == JsonValueKind.Array)\n            {\n                var eventMessages = root.Deserialize<IEnumerable<EventMessage>>();\n                await _handler.HandleManyEventsAsync(eventMessages ?? throw new JsonException(\"Deserialize returned null\"));\n            }\n            else if (root.ValueKind == JsonValueKind.Object)\n            {\n                var eventMessage = root.Deserialize<EventMessage>();\n                await _handler.HandleEventAsync(eventMessage ?? throw new JsonException(\"Deserialize returned null\"));\n            }\n            else\n            {\n                if (!string.IsNullOrEmpty(messageId))\n                {\n                    _logger.LogError(\"An error occurred while processing message: {MessageId} - Invalid JSON\", messageId);\n                }\n                else\n                {\n                    _logger.LogError(\"An Invalid JSON error occurred while processing a message with an empty message id\");\n                }\n            }\n        }\n        catch (JsonException exception)\n        {\n            if (!string.IsNullOrEmpty(messageId))\n            {\n                _logger.LogError(\n                    exception,\n                    \"An error occurred while processing message: {MessageId} - Invalid JSON\",\n                    messageId\n                );\n            }\n            else\n            {\n                _logger.LogError(\n                    exception,\n                    \"An Invalid JSON error occurred while processing a message with an empty message id\"\n                );\n            }\n        }\n        catch (Exception exception)\n        {\n            if (!string.IsNullOrEmpty(messageId))\n            {\n                _logger.LogError(\n                    exception,\n                    \"An error occurred while processing message: {MessageId}\",\n                    messageId\n                );\n            }\n            else\n            {\n                _logger.LogError(\n                    exception,\n                    \"An error occurred while processing a message with an empty message id\"\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/EventRepositoryHandler.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class EventRepositoryHandler(\n    [FromKeyedServices(\"persistent\")] IEventWriteService eventWriteService)\n    : IEventMessageHandler\n{\n    public Task HandleEventAsync(EventMessage eventMessage)\n    {\n        return eventWriteService.CreateAsync(eventMessage);\n    }\n\n    public Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)\n    {\n        return eventWriteService.CreateManyAsync(eventMessages);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/EventService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Interfaces;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Services;\n\npublic class EventService : IEventService\n{\n    private readonly IEventWriteService _eventWriteService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly ICurrentContext _currentContext;\n    private readonly GlobalSettings _globalSettings;\n\n    public EventService(\n        IEventWriteService eventWriteService,\n        IOrganizationUserRepository organizationUserRepository,\n        IProviderUserRepository providerUserRepository,\n        IApplicationCacheService applicationCacheService,\n        ICurrentContext currentContext,\n        GlobalSettings globalSettings)\n    {\n        _eventWriteService = eventWriteService;\n        _organizationUserRepository = organizationUserRepository;\n        _providerUserRepository = providerUserRepository;\n        _applicationCacheService = applicationCacheService;\n        _currentContext = currentContext;\n        _globalSettings = globalSettings;\n    }\n\n    public async Task LogUserEventAsync(Guid userId, EventType type, DateTime? date = null)\n    {\n        var events = new List<IEvent>\n        {\n            new EventMessage(_currentContext)\n            {\n                UserId = userId,\n                ActingUserId = userId,\n                Type = type,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            }\n        };\n\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, userId);\n        var orgEvents = orgs.Where(o => CanUseEvents(orgAbilities, o.Id))\n            .Select(o => new EventMessage(_currentContext)\n            {\n                OrganizationId = o.Id,\n                UserId = userId,\n                ActingUserId = userId,\n                Type = type,\n                Date = DateTime.UtcNow\n            });\n\n        var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync();\n        var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, userId);\n        var providerEvents = providers.Where(o => CanUseProviderEvents(providerAbilities, o.Id))\n            .Select(p => new EventMessage(_currentContext)\n            {\n                ProviderId = p.Id,\n                UserId = userId,\n                ActingUserId = userId,\n                Type = type,\n                Date = DateTime.UtcNow\n            });\n\n        if (orgEvents.Any() || providerEvents.Any())\n        {\n            events.AddRange(orgEvents);\n            events.AddRange(providerEvents);\n            await _eventWriteService.CreateManyAsync(events);\n        }\n        else\n        {\n            await _eventWriteService.CreateAsync(events.First());\n        }\n    }\n\n    public async Task LogCipherEventAsync(Cipher cipher, EventType type, DateTime? date = null)\n    {\n        var e = await BuildCipherEventMessageAsync(cipher, type, date);\n        if (e != null)\n        {\n            await _eventWriteService.CreateAsync(e);\n        }\n    }\n\n    public async Task LogCipherEventsAsync(IEnumerable<Tuple<Cipher, EventType, DateTime?>> events)\n    {\n        var cipherEvents = new List<IEvent>();\n        foreach (var ev in events)\n        {\n            var e = await BuildCipherEventMessageAsync(ev.Item1, ev.Item2, ev.Item3);\n            if (e != null)\n            {\n                cipherEvents.Add(e);\n            }\n        }\n        await _eventWriteService.CreateManyAsync(cipherEvents);\n    }\n\n    private async Task<EventMessage> BuildCipherEventMessageAsync(Cipher cipher, EventType type, DateTime? date = null)\n    {\n        // Only logging organization cipher events for now.\n        if (!cipher.OrganizationId.HasValue || (!_currentContext?.UserId.HasValue ?? true))\n        {\n            return null;\n        }\n\n        if (cipher.OrganizationId.HasValue)\n        {\n            var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n            if (!CanUseEvents(orgAbilities, cipher.OrganizationId.Value))\n            {\n                return null;\n            }\n        }\n\n        return new EventMessage(_currentContext)\n        {\n            OrganizationId = cipher.OrganizationId,\n            UserId = cipher.OrganizationId.HasValue ? null : cipher.UserId,\n            CipherId = cipher.Id,\n            Type = type,\n            ActingUserId = _currentContext?.UserId,\n            ProviderId = await GetProviderIdAsync(cipher.OrganizationId),\n            Date = date.GetValueOrDefault(DateTime.UtcNow)\n        };\n    }\n\n    public async Task LogCollectionEventAsync(Collection collection, EventType type, DateTime? date = null) =>\n        await LogCollectionEventsAsync(new[] { (collection, type, date) });\n\n\n    public async Task LogCollectionEventsAsync(IEnumerable<(Collection collection, EventType type, DateTime? date)> events)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n\n        // Batch lookup provider IDs for all unique organization IDs upfront\n        var materializedEvents = events.ToList();\n        var uniqueOrgIds = materializedEvents\n            .Select(e => e.collection.OrganizationId)\n            .Distinct()\n            .Where(orgId => CanUseEvents(orgAbilities, orgId))\n            .ToList();\n\n        var providerIds = new Dictionary<Guid, Guid?>();\n        foreach (var orgId in uniqueOrgIds)\n        {\n            providerIds[orgId] = await GetProviderIdAsync(orgId);\n        }\n\n        var eventMessages = new List<IEvent>();\n        foreach (var (collection, type, date) in materializedEvents)\n        {\n            if (!CanUseEvents(orgAbilities, collection.OrganizationId))\n            {\n                continue;\n            }\n\n            eventMessages.Add(new EventMessage(_currentContext)\n            {\n                OrganizationId = collection.OrganizationId,\n                CollectionId = collection.Id,\n                Type = type,\n                ActingUserId = _currentContext?.UserId,\n                ProviderId = providerIds.GetValueOrDefault(collection.OrganizationId),\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            });\n        }\n\n        await _eventWriteService.CreateManyAsync(eventMessages);\n    }\n\n    public async Task LogGroupEventAsync(Group group, EventType type, DateTime? date = null) =>\n        await LogGroupEventsAsync(new[] { (group, type, (EventSystemUser?)null, date) });\n\n    public async Task LogGroupEventAsync(Group group, EventType type, EventSystemUser systemUser, DateTime? date = null) =>\n        await LogGroupEventsAsync(new[] { (group, type, (EventSystemUser?)systemUser, date) });\n\n    public async Task LogGroupEventsAsync(IEnumerable<(Group group, EventType type, EventSystemUser? systemUser, DateTime? date)> events)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n\n        // Batch lookup provider IDs for all unique organization IDs upfront\n        var materializedEvents = events.ToList();\n        var uniqueOrgIds = materializedEvents\n            .Select(e => e.group.OrganizationId)\n            .Distinct()\n            .Where(orgId => CanUseEvents(orgAbilities, orgId))\n            .ToList();\n\n        var providerIds = new Dictionary<Guid, Guid?>();\n        foreach (var orgId in uniqueOrgIds)\n        {\n            providerIds[orgId] = await GetProviderIdAsync(orgId);\n        }\n\n        var eventMessages = new List<IEvent>();\n        foreach (var (group, type, systemUser, date) in materializedEvents)\n        {\n            if (!CanUseEvents(orgAbilities, group.OrganizationId))\n            {\n                continue;\n            }\n\n            var e = new EventMessage(_currentContext)\n            {\n                OrganizationId = group.OrganizationId,\n                GroupId = group.Id,\n                Type = type,\n                ActingUserId = _currentContext?.UserId,\n                ProviderId = providerIds.GetValueOrDefault(group.OrganizationId),\n                SystemUser = systemUser,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            };\n\n            if (systemUser is EventSystemUser.SCIM)\n            {\n                // System user only used for SCIM logs in this method\n                // and we want event logs to report server instead of unknown\n                e.DeviceType = DeviceType.Server;\n            }\n\n            eventMessages.Add(e);\n        }\n        await _eventWriteService.CreateManyAsync(eventMessages);\n    }\n\n    public async Task LogPolicyEventAsync(Policy policy, EventType type, DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        if (!CanUseEvents(orgAbilities, policy.OrganizationId))\n        {\n            return;\n        }\n\n        var e = new EventMessage(_currentContext)\n        {\n            OrganizationId = policy.OrganizationId,\n            PolicyId = policy.Id,\n            Type = type,\n            ActingUserId = _currentContext?.UserId,\n            ProviderId = await GetProviderIdAsync(policy.OrganizationId),\n            Date = date.GetValueOrDefault(DateTime.UtcNow)\n        };\n        await _eventWriteService.CreateAsync(e);\n    }\n\n    public async Task LogOrganizationUserEventAsync<T>(T organizationUser, EventType type,\n        DateTime? date = null) where T : IOrganizationUser =>\n        await CreateLogOrganizationUserEventsAsync(new (T, EventType, EventSystemUser?, DateTime?)[] { (organizationUser, type, null, date) });\n\n    public async Task LogOrganizationUserEventAsync<T>(T organizationUser, EventType type,\n        EventSystemUser systemUser, DateTime? date = null) where T : IOrganizationUser =>\n        await CreateLogOrganizationUserEventsAsync(new (T, EventType, EventSystemUser?, DateTime?)[] { (organizationUser, type, systemUser, date) });\n\n    public async Task LogOrganizationUserEventsAsync<T>(\n        IEnumerable<(T, EventType, DateTime?)> events) where T : IOrganizationUser\n    {\n        await CreateLogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, (EventSystemUser?)null, e.Item3)));\n    }\n\n    public async Task LogOrganizationUserEventsAsync<T>(\n        IEnumerable<(T, EventType, EventSystemUser, DateTime?)> events) where T : IOrganizationUser\n    {\n        await CreateLogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, (EventSystemUser?)e.Item3, e.Item4)));\n    }\n\n    private async Task CreateLogOrganizationUserEventsAsync<T>(IEnumerable<(T, EventType, EventSystemUser?, DateTime?)> events) where T : IOrganizationUser\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n        foreach (var (organizationUser, type, systemUser, date) in events)\n        {\n            if (!CanUseEvents(orgAbilities, organizationUser.OrganizationId))\n            {\n                continue;\n            }\n\n            var e = new EventMessage(_currentContext)\n            {\n                OrganizationId = organizationUser.OrganizationId,\n                UserId = organizationUser.UserId,\n                OrganizationUserId = organizationUser.Id,\n                ProviderId = await GetProviderIdAsync(organizationUser.OrganizationId),\n                Type = type,\n                ActingUserId = _currentContext?.UserId,\n                Date = date.GetValueOrDefault(DateTime.UtcNow),\n                SystemUser = systemUser\n            };\n\n            if (systemUser is EventSystemUser.SCIM)\n            {\n                // System user only used for SCIM logs in this method\n                // and we want event logs to report server instead of unknown\n                e.DeviceType = DeviceType.Server;\n            }\n\n            eventMessages.Add(e);\n        }\n\n        await _eventWriteService.CreateManyAsync(eventMessages);\n    }\n\n    public async Task LogOrganizationEventAsync(Organization organization, EventType type, DateTime? date = null)\n    {\n        if (!organization.Enabled || !organization.UseEvents)\n        {\n            return;\n        }\n\n        var e = new EventMessage(_currentContext)\n        {\n            OrganizationId = organization.Id,\n            ProviderId = await GetProviderIdAsync(organization.Id),\n            Type = type,\n            ActingUserId = _currentContext?.UserId,\n            Date = date.GetValueOrDefault(DateTime.UtcNow),\n            InstallationId = GetInstallationId(),\n        };\n        await _eventWriteService.CreateAsync(e);\n    }\n\n    public async Task LogOrganizationEventAsync(Organization organization, EventType type, EventSystemUser systemUser, DateTime? date = null)\n    {\n        if (!organization.Enabled || !organization.UseEvents)\n        {\n            return;\n        }\n\n        var EventMessage = new EventMessage\n        {\n            OrganizationId = organization.Id,\n            ProviderId = await GetProviderIdAsync(organization.Id),\n            Type = type,\n            SystemUser = systemUser,\n            Date = date.GetValueOrDefault(DateTime.UtcNow),\n            DeviceType = DeviceType.Server\n        };\n        await _eventWriteService.CreateAsync(EventMessage);\n    }\n\n    public async Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null)\n    {\n        await LogProviderUsersEventAsync(new[] { (providerUser, type, date) });\n    }\n\n    public async Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events)\n    {\n        var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n        foreach (var (providerUser, type, date) in events)\n        {\n            if (!CanUseProviderEvents(providerAbilities, providerUser.ProviderId))\n            {\n                continue;\n            }\n            eventMessages.Add(new EventMessage(_currentContext)\n            {\n                ProviderId = providerUser.ProviderId,\n                UserId = providerUser.UserId,\n                ProviderUserId = providerUser.Id,\n                Type = type,\n                ActingUserId = _currentContext?.UserId,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            });\n        }\n\n        await _eventWriteService.CreateManyAsync(eventMessages);\n    }\n\n    public async Task LogProviderOrganizationEventAsync(ProviderOrganization providerOrganization, EventType type,\n        DateTime? date = null)\n    {\n        await LogProviderOrganizationEventsAsync(new[] { (providerOrganization, type, date) });\n    }\n\n    public async Task LogProviderOrganizationEventsAsync(IEnumerable<(ProviderOrganization, EventType, DateTime?)> events)\n    {\n        var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n        foreach (var (providerOrganization, type, date) in events)\n        {\n            if (!CanUseProviderEvents(providerAbilities, providerOrganization.ProviderId))\n            {\n                continue;\n            }\n\n            var e = new EventMessage(_currentContext)\n            {\n                ProviderId = providerOrganization.ProviderId,\n                ProviderOrganizationId = providerOrganization.Id,\n                Type = type,\n                ActingUserId = _currentContext?.UserId,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            };\n\n            eventMessages.Add(e);\n        }\n\n        await _eventWriteService.CreateManyAsync(eventMessages);\n    }\n\n    public async Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type,\n            DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        if (!CanUseEvents(orgAbilities, organizationDomain.OrganizationId))\n        {\n            return;\n        }\n\n        var e = new EventMessage(_currentContext)\n        {\n            OrganizationId = organizationDomain.OrganizationId,\n            Type = type,\n            ActingUserId = _currentContext?.UserId,\n            DomainName = organizationDomain.DomainName,\n            Date = date.GetValueOrDefault(DateTime.UtcNow)\n        };\n        await _eventWriteService.CreateAsync(e);\n    }\n\n    public async Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type,\n        EventSystemUser systemUser,\n        DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        if (!CanUseEvents(orgAbilities, organizationDomain.OrganizationId))\n        {\n            return;\n        }\n\n        var e = new EventMessage(_currentContext)\n        {\n            OrganizationId = organizationDomain.OrganizationId,\n            Type = type,\n            ActingUserId = _currentContext?.UserId,\n            DomainName = organizationDomain.DomainName,\n            SystemUser = systemUser,\n            Date = date.GetValueOrDefault(DateTime.UtcNow),\n            DeviceType = DeviceType.Server\n        };\n        await _eventWriteService.CreateAsync(e);\n    }\n\n    public async Task LogUserSecretsEventAsync(Guid userId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n\n        foreach (var secret in secrets)\n        {\n            if (!CanUseEvents(orgAbilities, secret.OrganizationId))\n            {\n                continue;\n            }\n\n            var e = new EventMessage(_currentContext)\n            {\n                OrganizationId = secret.OrganizationId,\n                Type = type,\n                SecretId = secret.Id,\n                UserId = userId,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            };\n            eventMessages.Add(e);\n        }\n\n        await _eventWriteService.CreateManyAsync(eventMessages);\n    }\n\n    public async Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n\n        foreach (var secret in secrets)\n        {\n            if (!CanUseEvents(orgAbilities, secret.OrganizationId))\n            {\n                continue;\n            }\n\n            var e = new EventMessage(_currentContext)\n            {\n                OrganizationId = secret.OrganizationId,\n                Type = type,\n                SecretId = secret.Id,\n                ServiceAccountId = serviceAccountId,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            };\n            eventMessages.Add(e);\n        }\n\n        await _eventWriteService.CreateManyAsync(eventMessages);\n    }\n\n    public async Task LogUserProjectsEventAsync(Guid userId, IEnumerable<Project> projects, EventType type, DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n\n        foreach (var project in projects)\n        {\n            if (!CanUseEvents(orgAbilities, project.OrganizationId))\n            {\n                continue;\n            }\n\n            var e = new EventMessage(_currentContext)\n            {\n                OrganizationId = project.OrganizationId,\n                Type = type,\n                ProjectId = project.Id,\n                UserId = userId,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            };\n            eventMessages.Add(e);\n        }\n\n        await _eventWriteService.CreateManyAsync(eventMessages);\n    }\n\n    public async Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable<Project> projects, EventType type, DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n\n        foreach (var project in projects)\n        {\n            if (!CanUseEvents(orgAbilities, project.OrganizationId))\n            {\n                continue;\n            }\n\n            var e = new EventMessage(_currentContext)\n            {\n                OrganizationId = project.OrganizationId,\n                Type = type,\n                ProjectId = project.Id,\n                ServiceAccountId = serviceAccountId,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            };\n            eventMessages.Add(e);\n        }\n\n        await _eventWriteService.CreateManyAsync(eventMessages);\n    }\n\n\n    public async Task LogServiceAccountPeopleEventAsync(Guid userId, UserServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n        var orgUser = await _organizationUserRepository.GetByIdAsync((Guid)policy.OrganizationUserId);\n\n        if (!CanUseEvents(orgAbilities, orgUser.OrganizationId))\n        {\n            return;\n        }\n\n        var (actingUserId, serviceAccountId) = MapIdentityClientType(userId, identityClientType);\n\n        if (actingUserId is null && serviceAccountId is null)\n        {\n            return;\n        }\n\n        if (policy.OrganizationUserId != null)\n        {\n            var e = new EventMessage(_currentContext)\n            {\n                OrganizationId = orgUser.OrganizationId,\n                Type = type,\n                GrantedServiceAccountId = policy.GrantedServiceAccountId,\n                ServiceAccountId = serviceAccountId,\n                UserId = policy.OrganizationUserId,\n                ActingUserId = actingUserId,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            };\n            eventMessages.Add(e);\n\n            await _eventWriteService.CreateManyAsync(eventMessages);\n        }\n    }\n\n    public async Task LogServiceAccountGroupEventAsync(Guid userId, GroupServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n\n        if (!CanUseEvents(orgAbilities, policy.Group.OrganizationId))\n        {\n            return;\n        }\n\n        var (actingUserId, serviceAccountId) = MapIdentityClientType(userId, identityClientType);\n\n        if (actingUserId is null && serviceAccountId is null)\n        {\n            return;\n        }\n\n        if (policy.GroupId != null)\n        {\n            var e = new EventMessage(_currentContext)\n            {\n                OrganizationId = policy.Group.OrganizationId,\n                Type = type,\n                GrantedServiceAccountId = policy.GrantedServiceAccountId,\n                ServiceAccountId = serviceAccountId,\n                GroupId = policy.GroupId,\n                ActingUserId = actingUserId,\n                Date = date.GetValueOrDefault(DateTime.UtcNow)\n            };\n            eventMessages.Add(e);\n\n            await _eventWriteService.CreateManyAsync(eventMessages);\n        }\n    }\n\n    public async Task LogServiceAccountEventAsync(Guid userId, List<ServiceAccount> serviceAccounts, EventType type, IdentityClientType identityClientType, DateTime? date = null)\n    {\n        var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n        var eventMessages = new List<IEvent>();\n\n        foreach (var serviceAccount in serviceAccounts)\n        {\n            if (!CanUseEvents(orgAbilities, serviceAccount.OrganizationId))\n            {\n                continue;\n            }\n\n            var (actingUserId, serviceAccountId) = MapIdentityClientType(userId, identityClientType);\n\n            if (actingUserId is null && serviceAccountId is null)\n            {\n                continue;\n            }\n\n            if (serviceAccount != null)\n            {\n                var e = new EventMessage(_currentContext)\n                {\n                    OrganizationId = serviceAccount.OrganizationId,\n                    Type = type,\n                    GrantedServiceAccountId = serviceAccount.Id,\n                    ServiceAccountId = serviceAccountId,\n                    ActingUserId = actingUserId,\n                    Date = date.GetValueOrDefault(DateTime.UtcNow)\n                };\n                eventMessages.Add(e);\n            }\n        }\n\n        if (eventMessages.Any())\n        {\n            await _eventWriteService.CreateManyAsync(eventMessages);\n        }\n    }\n\n    private (Guid? actingUserId, Guid? serviceAccountId) MapIdentityClientType(\n           Guid userId, IdentityClientType identityClientType)\n    {\n        if (identityClientType == IdentityClientType.Organization)\n        {\n            return (null, null);\n        }\n\n        return identityClientType switch\n        {\n            IdentityClientType.User => (userId, null),\n            IdentityClientType.ServiceAccount => (null, userId),\n            _ => throw new InvalidOperationException(\"Unknown identity client type.\")\n        };\n    }\n\n\n    private async Task<Guid?> GetProviderIdAsync(Guid? orgId)\n    {\n        if (_currentContext == null || !orgId.HasValue)\n        {\n            return null;\n        }\n\n        return await _currentContext.ProviderIdForOrg(orgId.Value);\n    }\n\n    private Guid? GetInstallationId()\n    {\n        if (_currentContext == null)\n        {\n            return null;\n        }\n\n        return _currentContext.InstallationId;\n    }\n\n    private bool CanUseEvents(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)\n    {\n        return orgAbilities != null && orgAbilities.TryGetValue(orgId, out var orgAbility) &&\n               orgAbility.Enabled && orgAbility.UseEvents;\n    }\n\n    private bool CanUseProviderEvents(IDictionary<Guid, ProviderAbility> providerAbilities, Guid providerId)\n    {\n        return providerAbilities != null && providerAbilities.TryGetValue(providerId, out var providerAbility) &&\n               providerAbility.Enabled && providerAbility.UseEvents;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/IntegrationFilterFactory.cs",
    "content": "﻿using System.Linq.Expressions;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic delegate bool IntegrationFilter(EventMessage message, object? value);\n\npublic static class IntegrationFilterFactory\n{\n    public static IntegrationFilter BuildEqualityFilter<T>(string propertyName)\n    {\n        var param = Expression.Parameter(typeof(EventMessage), \"m\");\n        var valueParam = Expression.Parameter(typeof(object), \"val\");\n\n        var property = Expression.PropertyOrField(param, propertyName);\n        var typedVal = Expression.Convert(valueParam, typeof(T));\n        var body = Expression.Equal(property, typedVal);\n\n        var lambda = Expression.Lambda<Func<EventMessage, object?, bool>>(body, param, valueParam);\n        return new IntegrationFilter(lambda.Compile());\n    }\n\n    public static IntegrationFilter BuildInFilter<T>(string propertyName)\n    {\n        var param = Expression.Parameter(typeof(EventMessage), \"m\");\n        var valueParam = Expression.Parameter(typeof(object), \"val\");\n\n        var property = Expression.PropertyOrField(param, propertyName);\n\n        var method = typeof(Enumerable)\n            .GetMethods()\n            .FirstOrDefault(m =>\n                m.Name == \"Contains\"\n                && m.GetParameters().Length == 2)\n            ?.MakeGenericMethod(typeof(T));\n        if (method is null)\n        {\n            throw new InvalidOperationException(\"Could not find Contains method.\");\n        }\n\n        var listType = typeof(IEnumerable<T>);\n        var castedList = Expression.Convert(valueParam, listType);\n\n        var containsCall = Expression.Call(method, castedList, property);\n\n        var lambda = Expression.Lambda<Func<EventMessage, object?, bool>>(containsCall, param, valueParam);\n        return new IntegrationFilter(lambda.Compile());\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/IntegrationFilterService.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class IntegrationFilterService : IIntegrationFilterService\n{\n    private readonly Dictionary<string, IntegrationFilter> _equalsFilters = new();\n    private readonly Dictionary<string, IntegrationFilter> _inFilters = new();\n    private static readonly string[] _filterableProperties = new[]\n    {\n        \"UserId\",\n        \"InstallationId\",\n        \"ProviderId\",\n        \"CipherId\",\n        \"CollectionId\",\n        \"GroupId\",\n        \"PolicyId\",\n        \"OrganizationUserId\",\n        \"ProviderUserId\",\n        \"ProviderOrganizationId\",\n        \"ActingUserId\",\n        \"SecretId\",\n        \"ServiceAccountId\"\n    };\n\n    public IntegrationFilterService()\n    {\n        BuildFilters();\n    }\n\n    public bool EvaluateFilterGroup(IntegrationFilterGroup group, EventMessage message)\n    {\n        var ruleResults = group.Rules?.Select(\n            rule => EvaluateRule(rule, message)\n        ) ?? Enumerable.Empty<bool>();\n        var groupResults = group.Groups?.Select(\n            innerGroup => EvaluateFilterGroup(innerGroup, message)\n        ) ?? Enumerable.Empty<bool>();\n\n        var results = ruleResults.Concat(groupResults);\n        return group.AndOperator ? results.All(r => r) : results.Any(r => r);\n    }\n\n    private bool EvaluateRule(IntegrationFilterRule rule, EventMessage message)\n    {\n        var key = rule.Property;\n        return rule.Operation switch\n        {\n            IntegrationFilterOperation.Equals => _equalsFilters.TryGetValue(key, out var equals) &&\n                                                 equals(message, ToGuid(rule.Value)),\n            IntegrationFilterOperation.NotEquals => !(_equalsFilters.TryGetValue(key, out var equals) &&\n                                                 equals(message, ToGuid(rule.Value))),\n            IntegrationFilterOperation.In => _inFilters.TryGetValue(key, out var inList) &&\n                                             inList(message, ToGuidList(rule.Value)),\n            IntegrationFilterOperation.NotIn => !(_inFilters.TryGetValue(key, out var inList) &&\n                                                inList(message, ToGuidList(rule.Value))),\n            _ => false\n        };\n    }\n\n    private void BuildFilters()\n    {\n        foreach (var property in _filterableProperties)\n        {\n            _equalsFilters[property] = IntegrationFilterFactory.BuildEqualityFilter<Guid?>(property);\n            _inFilters[property] = IntegrationFilterFactory.BuildInFilter<Guid?>(property);\n        }\n    }\n\n    private static Guid? ToGuid(object? value)\n    {\n        if (value is Guid guid)\n        {\n            return guid;\n        }\n        if (value is string stringValue)\n        {\n            return Guid.Parse(stringValue);\n        }\n        if (value is JsonElement jsonElement)\n        {\n            return jsonElement.GetGuid();\n        }\n\n        throw new InvalidCastException(\"Could not convert value to Guid\");\n    }\n\n    private static IEnumerable<Guid?> ToGuidList(object? value)\n    {\n        if (value is IEnumerable<Guid?> guidList)\n        {\n            return guidList;\n        }\n        if (value is JsonElement { ValueKind: JsonValueKind.Array } jsonElement)\n        {\n            var list = new List<Guid?>();\n            foreach (var item in jsonElement.EnumerateArray())\n            {\n                list.Add(ToGuid(item));\n            }\n            return list;\n        }\n\n        throw new InvalidCastException(\"Could not convert value to Guid[]\");\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/OrganizationIntegrationConfigurationValidator.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class OrganizationIntegrationConfigurationValidator : IOrganizationIntegrationConfigurationValidator\n{\n    public bool ValidateConfiguration(IntegrationType integrationType,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        // Validate template is present\n        if (string.IsNullOrWhiteSpace(configuration.Template))\n        {\n            return false;\n        }\n        // If Filters are present, they must be valid\n        if (!IsFiltersValid(configuration.Filters))\n        {\n            return false;\n        }\n\n        switch (integrationType)\n        {\n            case IntegrationType.CloudBillingSync or IntegrationType.Scim:\n                return false;\n            case IntegrationType.Slack:\n                return IsConfigurationValid<SlackIntegrationConfiguration>(configuration.Configuration);\n            case IntegrationType.Webhook:\n                return IsConfigurationValid<WebhookIntegrationConfiguration>(configuration.Configuration);\n            case IntegrationType.Hec:\n            case IntegrationType.Datadog:\n            case IntegrationType.Teams:\n                return configuration.Configuration is null;\n            default:\n                return false;\n        }\n    }\n\n    private static bool IsConfigurationValid<T>(string? configuration)\n    {\n        if (string.IsNullOrWhiteSpace(configuration))\n        {\n            return false;\n        }\n\n        try\n        {\n            var config = JsonSerializer.Deserialize<T>(configuration);\n            return config is not null;\n        }\n        catch\n        {\n            return false;\n        }\n    }\n\n    private static bool IsFiltersValid(string? filters)\n    {\n        if (filters is null)\n        {\n            return true;\n        }\n\n        try\n        {\n            var filterGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(filters);\n            return filterGroup is not null;\n        }\n        catch\n        {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/RabbitMqEventListenerService.cs",
    "content": "﻿using System.Text;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Microsoft.Extensions.Logging;\nusing RabbitMQ.Client;\nusing RabbitMQ.Client.Events;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class RabbitMqEventListenerService<TConfiguration> : EventLoggingListenerService\n    where TConfiguration : IEventListenerConfiguration\n{\n    private readonly Lazy<Task<IChannel>> _lazyChannel;\n    private readonly string _queueName;\n    private readonly IRabbitMqService _rabbitMqService;\n\n    public RabbitMqEventListenerService(\n        IEventMessageHandler handler,\n        TConfiguration configuration,\n        IRabbitMqService rabbitMqService,\n        ILoggerFactory loggerFactory)\n        : base(handler, CreateLogger(loggerFactory, configuration))\n    {\n        _queueName = configuration.EventQueueName;\n        _rabbitMqService = rabbitMqService;\n        _lazyChannel = new Lazy<Task<IChannel>>(() => _rabbitMqService.CreateChannelAsync());\n    }\n\n    public override async Task StartAsync(CancellationToken cancellationToken)\n    {\n        await _rabbitMqService.CreateEventQueueAsync(_queueName, cancellationToken);\n        await base.StartAsync(cancellationToken);\n    }\n\n    protected override async Task ExecuteAsync(CancellationToken cancellationToken)\n    {\n        var channel = await _lazyChannel.Value;\n        var consumer = new AsyncEventingBasicConsumer(channel);\n        consumer.ReceivedAsync += async (_, eventArgs) => { await ProcessReceivedMessageAsync(eventArgs); };\n\n        await channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken);\n    }\n\n    internal async Task ProcessReceivedMessageAsync(BasicDeliverEventArgs eventArgs)\n    {\n        await ProcessReceivedMessageAsync(\n            Encoding.UTF8.GetString(eventArgs.Body.Span),\n            eventArgs.BasicProperties.MessageId);\n    }\n\n    public override async Task StopAsync(CancellationToken cancellationToken)\n    {\n        if (_lazyChannel.IsValueCreated)\n        {\n            var channel = await _lazyChannel.Value;\n            await channel.CloseAsync(cancellationToken);\n        }\n        await base.StopAsync(cancellationToken);\n    }\n\n    public override void Dispose()\n    {\n        if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully)\n        {\n            _lazyChannel.Value.Result.Dispose();\n        }\n        base.Dispose();\n    }\n\n    private static ILogger CreateLogger(ILoggerFactory loggerFactory, TConfiguration configuration)\n    {\n        return loggerFactory.CreateLogger(\n            categoryName: $\"Bit.Core.Dirt.Services.Implementations.RabbitMqEventListenerService.{configuration.EventQueueName}\");\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/RabbitMqIntegrationListenerService.cs",
    "content": "﻿using System.Text;\nusing System.Text.Json;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing RabbitMQ.Client;\nusing RabbitMQ.Client.Events;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class RabbitMqIntegrationListenerService<TConfiguration> : BackgroundService\n    where TConfiguration : IIntegrationListenerConfiguration\n{\n    private readonly int _maxRetries;\n    private readonly string _queueName;\n    private readonly string _routingKey;\n    private readonly string _retryQueueName;\n    private readonly IIntegrationHandler _handler;\n    private readonly Lazy<Task<IChannel>> _lazyChannel;\n    private readonly IRabbitMqService _rabbitMqService;\n    private readonly ILogger _logger;\n    private readonly TimeProvider _timeProvider;\n\n    public RabbitMqIntegrationListenerService(\n        IIntegrationHandler handler,\n        TConfiguration configuration,\n        IRabbitMqService rabbitMqService,\n        ILoggerFactory loggerFactory,\n        TimeProvider timeProvider)\n    {\n        _handler = handler;\n        _maxRetries = configuration.MaxRetries;\n        _routingKey = configuration.RoutingKey;\n        _retryQueueName = configuration.IntegrationRetryQueueName;\n        _queueName = configuration.IntegrationQueueName;\n        _rabbitMqService = rabbitMqService;\n        _timeProvider = timeProvider;\n        _lazyChannel = new Lazy<Task<IChannel>>(() => _rabbitMqService.CreateChannelAsync());\n        _logger = loggerFactory.CreateLogger(\n            categoryName: $\"Bit.Core.Dirt.Services.Implementations.RabbitMqIntegrationListenerService.{configuration.IntegrationQueueName}\"); ;\n    }\n\n    public override async Task StartAsync(CancellationToken cancellationToken)\n    {\n        await _rabbitMqService.CreateIntegrationQueuesAsync(\n            _queueName,\n            _retryQueueName,\n            _routingKey,\n            cancellationToken: cancellationToken);\n\n        await base.StartAsync(cancellationToken);\n    }\n\n    protected override async Task ExecuteAsync(CancellationToken cancellationToken)\n    {\n        var channel = await _lazyChannel.Value;\n        var consumer = new AsyncEventingBasicConsumer(channel);\n        consumer.ReceivedAsync += async (_, ea) =>\n        {\n            await ProcessReceivedMessageAsync(ea, cancellationToken);\n        };\n\n        await channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken);\n    }\n\n    internal async Task ProcessReceivedMessageAsync(BasicDeliverEventArgs ea, CancellationToken cancellationToken)\n    {\n        var channel = await _lazyChannel.Value;\n        try\n        {\n            var json = Encoding.UTF8.GetString(ea.Body.Span);\n\n            // Determine if the message came off of the retry queue too soon\n            // If so, place it back on the retry queue\n            var integrationMessage = JsonSerializer.Deserialize<IntegrationMessage>(json);\n            if (integrationMessage is not null &&\n                integrationMessage.DelayUntilDate.HasValue &&\n                integrationMessage.DelayUntilDate.Value > _timeProvider.GetUtcNow().UtcDateTime)\n            {\n                await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea);\n                await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);\n                return;\n            }\n\n            var result = await _handler.HandleAsync(json);\n            var message = result.Message;\n\n            if (result.Success)\n            {\n                // Successful integration send. Acknowledge message delivery and return\n                await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);\n                return;\n            }\n\n            if (result.Retryable)\n            {\n                // Integration failed, but is retryable - apply delay and check max retries\n                message.ApplyRetry(result.DelayUntilDate);\n\n                if (message.RetryCount < _maxRetries)\n                {\n                    // Publish message to the retry queue. It will be re-published for retry after a delay\n                    await _rabbitMqService.PublishToRetryAsync(channel, message, cancellationToken);\n                }\n                else\n                {\n                    // Exceeded the max number of retries; fail and send to dead letter queue\n                    await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken);\n                    _logger.LogWarning(\n                        \"Integration failure - max retries exceeded. \" +\n                        \"MessageId: {MessageId}, IntegrationType: {IntegrationType}, OrganizationId: {OrgId}, \" +\n                        \"FailureCategory: {Category}, Reason: {Reason}, RetryCount: {RetryCount}, MaxRetries: {MaxRetries}\",\n                        message.MessageId,\n                        message.IntegrationType,\n                        message.OrganizationId,\n                        result.Category,\n                        result.FailureReason,\n                        message.RetryCount,\n                        _maxRetries);\n                }\n            }\n            else\n            {\n                // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries\n                await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken);\n                _logger.LogWarning(\n                    \"Integration failure - non-retryable. \" +\n                    \"MessageId: {MessageId}, IntegrationType: {IntegrationType}, OrganizationId: {OrgId}, \" +\n                    \"FailureCategory: {Category}, Reason: {Reason}\",\n                    message.MessageId,\n                    message.IntegrationType,\n                    message.OrganizationId,\n                    result.Category,\n                    result.FailureReason);\n            }\n\n            // Message has been sent to retry or dead letter queues.\n            // Acknowledge receipt so Rabbit knows it's been processed\n            await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);\n        }\n        catch (Exception ex)\n        {\n            // Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error\n            _logger.LogError(ex, \"Unhandled error processing integration message.\");\n            await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);\n        }\n    }\n\n    public override async Task StopAsync(CancellationToken cancellationToken)\n    {\n        if (_lazyChannel.IsValueCreated)\n        {\n            var channel = await _lazyChannel.Value;\n            await channel.CloseAsync(cancellationToken);\n        }\n        await base.StopAsync(cancellationToken);\n    }\n\n    public override void Dispose()\n    {\n        if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully)\n        {\n            _lazyChannel.Value.Result.Dispose();\n        }\n        base.Dispose();\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/RabbitMqService.cs",
    "content": "﻿using System.Text;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Settings;\nusing RabbitMQ.Client;\nusing RabbitMQ.Client.Events;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class RabbitMqService : IRabbitMqService\n{\n    private const string _deadLetterRoutingKey = \"dead-letter\";\n\n    private readonly ConnectionFactory _factory;\n    private readonly Lazy<Task<IConnection>> _lazyConnection;\n    private readonly string _deadLetterQueueName;\n    private readonly string _eventExchangeName;\n    private readonly string _integrationExchangeName;\n    private readonly int _retryTiming;\n    private readonly bool _useDelayPlugin;\n\n    public RabbitMqService(GlobalSettings globalSettings)\n    {\n        _factory = new ConnectionFactory\n        {\n            HostName = globalSettings.EventLogging.RabbitMq.HostName,\n            UserName = globalSettings.EventLogging.RabbitMq.Username,\n            Password = globalSettings.EventLogging.RabbitMq.Password\n        };\n        _deadLetterQueueName = globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName;\n        _eventExchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName;\n        _integrationExchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName;\n        _retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming;\n        _useDelayPlugin = globalSettings.EventLogging.RabbitMq.UseDelayPlugin;\n\n        _lazyConnection = new Lazy<Task<IConnection>>(CreateConnectionAsync);\n    }\n\n    public async Task<IChannel> CreateChannelAsync(CancellationToken cancellationToken = default)\n    {\n        var connection = await _lazyConnection.Value;\n        return await connection.CreateChannelAsync(cancellationToken: cancellationToken);\n    }\n\n    public async Task CreateEventQueueAsync(string queueName, CancellationToken cancellationToken = default)\n    {\n        using var channel = await CreateChannelAsync(cancellationToken);\n        await channel.QueueDeclareAsync(queue: queueName,\n            durable: true,\n            exclusive: false,\n            autoDelete: false,\n            arguments: null,\n            cancellationToken: cancellationToken);\n        await channel.QueueBindAsync(queue: queueName,\n            exchange: _eventExchangeName,\n            routingKey: string.Empty,\n            cancellationToken: cancellationToken);\n    }\n\n    public async Task CreateIntegrationQueuesAsync(\n        string queueName,\n        string retryQueueName,\n        string routingKey,\n        CancellationToken cancellationToken = default)\n    {\n        using var channel = await CreateChannelAsync(cancellationToken);\n        var retryRoutingKey = $\"{routingKey}-retry\";\n\n        // Declare main integration queue\n        await channel.QueueDeclareAsync(\n            queue: queueName,\n            durable: true,\n            exclusive: false,\n            autoDelete: false,\n            arguments: null,\n            cancellationToken: cancellationToken);\n        await channel.QueueBindAsync(\n            queue: queueName,\n            exchange: _integrationExchangeName,\n            routingKey: routingKey,\n            cancellationToken: cancellationToken);\n\n        if (!_useDelayPlugin)\n        {\n            // Declare retry queue (Configurable TTL, dead-letters back to main queue)\n            // Only needed if NOT using delay plugin\n            await channel.QueueDeclareAsync(queue: retryQueueName,\n                durable: true,\n                exclusive: false,\n                autoDelete: false,\n                arguments: new Dictionary<string, object?>\n                {\n                    { \"x-dead-letter-exchange\", _integrationExchangeName },\n                    { \"x-dead-letter-routing-key\", routingKey },\n                    { \"x-message-ttl\", _retryTiming }\n                },\n                cancellationToken: cancellationToken);\n            await channel.QueueBindAsync(queue: retryQueueName,\n                exchange: _integrationExchangeName,\n                routingKey: retryRoutingKey,\n                cancellationToken: cancellationToken);\n        }\n    }\n\n    public async Task PublishAsync(IIntegrationMessage message)\n    {\n        var routingKey = message.IntegrationType.ToRoutingKey();\n        await using var channel = await CreateChannelAsync();\n\n        var body = Encoding.UTF8.GetBytes(message.ToJson());\n        var properties = new BasicProperties\n        {\n            MessageId = message.MessageId,\n            Persistent = true\n        };\n\n        await channel.BasicPublishAsync(\n            exchange: _integrationExchangeName,\n            mandatory: true,\n            basicProperties: properties,\n            routingKey: routingKey,\n            body: body);\n    }\n\n    public async Task PublishEventAsync(string body, string? organizationId)\n    {\n        await using var channel = await CreateChannelAsync();\n        var properties = new BasicProperties\n        {\n            MessageId = Guid.NewGuid().ToString(),\n            Persistent = true\n        };\n\n        await channel.BasicPublishAsync(\n            exchange: _eventExchangeName,\n            mandatory: true,\n            basicProperties: properties,\n            routingKey: string.Empty,\n            body: Encoding.UTF8.GetBytes(body));\n    }\n\n    public async Task PublishToRetryAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken)\n    {\n        var routingKey = message.IntegrationType.ToRoutingKey();\n        var retryRoutingKey = $\"{routingKey}-retry\";\n        var properties = new BasicProperties\n        {\n            Persistent = true,\n            MessageId = message.MessageId,\n            Headers = _useDelayPlugin && message.DelayUntilDate.HasValue ?\n                new Dictionary<string, object?>\n                {\n                    [\"x-delay\"] = Math.Max((int)(message.DelayUntilDate.Value - DateTime.UtcNow).TotalMilliseconds, 0)\n                } :\n                null\n        };\n\n        await channel.BasicPublishAsync(\n            exchange: _integrationExchangeName,\n            routingKey: _useDelayPlugin ? routingKey : retryRoutingKey,\n            mandatory: true,\n            basicProperties: properties,\n            body: Encoding.UTF8.GetBytes(message.ToJson()),\n            cancellationToken: cancellationToken);\n    }\n\n    public async Task PublishToDeadLetterAsync(\n        IChannel channel,\n        IIntegrationMessage message,\n        CancellationToken cancellationToken)\n    {\n        var properties = new BasicProperties\n        {\n            MessageId = message.MessageId,\n            Persistent = true\n        };\n\n        await channel.BasicPublishAsync(\n            exchange: _integrationExchangeName,\n            mandatory: true,\n            basicProperties: properties,\n            routingKey: _deadLetterRoutingKey,\n            body: Encoding.UTF8.GetBytes(message.ToJson()),\n            cancellationToken: cancellationToken);\n    }\n\n    public async Task RepublishToRetryQueueAsync(IChannel channel, BasicDeliverEventArgs eventArgs)\n    {\n        await channel.BasicPublishAsync(\n            exchange: _integrationExchangeName,\n            routingKey: eventArgs.RoutingKey,\n            mandatory: true,\n            basicProperties: new BasicProperties(eventArgs.BasicProperties),\n            body: eventArgs.Body);\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        if (_lazyConnection.IsValueCreated)\n        {\n            var connection = await _lazyConnection.Value;\n            await connection.DisposeAsync();\n        }\n    }\n\n    private async Task<IConnection> CreateConnectionAsync()\n    {\n        var connection = await _factory.CreateConnectionAsync();\n        using var channel = await connection.CreateChannelAsync();\n\n        // Declare Exchanges\n        await channel.ExchangeDeclareAsync(exchange: _eventExchangeName, type: ExchangeType.Fanout, durable: true);\n        if (_useDelayPlugin)\n        {\n            await channel.ExchangeDeclareAsync(\n                exchange: _integrationExchangeName,\n                type: \"x-delayed-message\",\n                durable: true,\n                arguments: new Dictionary<string, object?>\n                {\n                    { \"x-delayed-type\", \"direct\" }\n                }\n            );\n        }\n        else\n        {\n            await channel.ExchangeDeclareAsync(exchange: _integrationExchangeName, type: ExchangeType.Direct, durable: true);\n        }\n\n        // Declare dead letter queue for Integration exchange\n        await channel.QueueDeclareAsync(queue: _deadLetterQueueName,\n            durable: true,\n            exclusive: false,\n            autoDelete: false,\n            arguments: null);\n        await channel.QueueBindAsync(queue: _deadLetterQueueName,\n            exchange: _integrationExchangeName,\n            routingKey: _deadLetterRoutingKey);\n\n        return connection;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/RepositoryEventWriteService.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Services;\n\npublic class RepositoryEventWriteService : IEventWriteService\n{\n    private readonly IEventRepository _eventRepository;\n\n    public RepositoryEventWriteService(\n        IEventRepository eventRepository)\n    {\n        _eventRepository = eventRepository;\n    }\n\n    public async Task CreateAsync(IEvent e)\n    {\n        await _eventRepository.CreateAsync(e);\n    }\n\n    public async Task CreateManyAsync(IEnumerable<IEvent> e)\n    {\n        await _eventRepository.CreateManyAsync(e);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/SlackIntegrationHandler.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.EventIntegrations;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class SlackIntegrationHandler(\n    ISlackService slackService)\n    : IntegrationHandlerBase<SlackIntegrationConfigurationDetails>\n{\n    public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<SlackIntegrationConfigurationDetails> message)\n    {\n        var slackResponse = await slackService.SendSlackMessageByChannelIdAsync(\n            message.Configuration.Token,\n            message.RenderedTemplate,\n            message.Configuration.ChannelId\n        );\n\n        if (slackResponse is null)\n        {\n            return IntegrationHandlerResult.Fail(\n                message,\n                IntegrationFailureCategory.TransientError,\n                \"Slack response was null\"\n            );\n        }\n\n        if (slackResponse.Ok)\n        {\n            return IntegrationHandlerResult.Succeed(message);\n        }\n\n        var category = ClassifySlackError(slackResponse.Error);\n        return IntegrationHandlerResult.Fail(\n            message,\n            category,\n            slackResponse.Error\n        );\n    }\n\n    /// <summary>\n    /// Classifies a Slack API error code string as an <see cref=\"IntegrationFailureCategory\"/> to drive\n    /// retry behavior and operator-facing failure reporting.\n    /// </summary>\n    /// <remarks>\n    /// <para>\n    /// Slack responses commonly return an <c>error</c> string when <c>ok</c> is false. This method maps\n    /// known Slack error codes to failure categories.\n    /// </para>\n    /// <para>\n    /// Any unrecognized error codes default to <see cref=\"IntegrationFailureCategory.TransientError\"/> to avoid\n    /// incorrectly marking new/unknown Slack failures as non-retryable.\n    /// </para>\n    /// </remarks>\n    /// <param name=\"error\">The Slack error code string (e.g. <c>invalid_auth</c>, <c>rate_limited</c>).</param>\n    /// <returns>The corresponding <see cref=\"IntegrationFailureCategory\"/>.</returns>\n    private static IntegrationFailureCategory ClassifySlackError(string error)\n    {\n        return error switch\n        {\n            \"invalid_auth\" => IntegrationFailureCategory.AuthenticationFailed,\n            \"access_denied\" => IntegrationFailureCategory.AuthenticationFailed,\n            \"token_expired\" => IntegrationFailureCategory.AuthenticationFailed,\n            \"token_revoked\" => IntegrationFailureCategory.AuthenticationFailed,\n            \"account_inactive\" => IntegrationFailureCategory.AuthenticationFailed,\n            \"not_authed\" => IntegrationFailureCategory.AuthenticationFailed,\n            \"channel_not_found\" => IntegrationFailureCategory.ConfigurationError,\n            \"is_archived\" => IntegrationFailureCategory.ConfigurationError,\n            \"rate_limited\" => IntegrationFailureCategory.RateLimited,\n            \"ratelimited\" => IntegrationFailureCategory.RateLimited,\n            \"message_limit_exceeded\" => IntegrationFailureCategory.RateLimited,\n            \"internal_error\" => IntegrationFailureCategory.TransientError,\n            \"service_unavailable\" => IntegrationFailureCategory.ServiceUnavailable,\n            \"fatal_error\" => IntegrationFailureCategory.ServiceUnavailable,\n            _ => IntegrationFailureCategory.TransientError\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/SlackService.cs",
    "content": "﻿using System.Net.Http.Headers;\nusing System.Net.Http.Json;\nusing System.Text.Json;\nusing System.Web;\nusing Bit.Core.Dirt.Models.Data.Slack;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class SlackService(\n    IHttpClientFactory httpClientFactory,\n    GlobalSettings globalSettings,\n    ILogger<SlackService> logger) : ISlackService\n{\n    private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);\n    private readonly string _clientId = globalSettings.Slack.ClientId;\n    private readonly string _clientSecret = globalSettings.Slack.ClientSecret;\n    private readonly string _scopes = globalSettings.Slack.Scopes;\n    private readonly string _slackApiBaseUrl = globalSettings.Slack.ApiBaseUrl;\n\n    public const string HttpClientName = \"SlackServiceHttpClient\";\n    private const string _slackOAuthBaseUri = \"https://slack.com/oauth/v2/authorize\";\n\n    public async Task<string> GetChannelIdAsync(string token, string channelName)\n    {\n        return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault() ?? string.Empty;\n    }\n\n    public async Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames)\n    {\n        var matchingChannelIds = new List<string>();\n        var baseUrl = $\"{_slackApiBaseUrl}/conversations.list\";\n        var nextCursor = string.Empty;\n\n        do\n        {\n            var uriBuilder = new UriBuilder(baseUrl);\n            var queryParameters = HttpUtility.ParseQueryString(uriBuilder.Query);\n            queryParameters[\"types\"] = \"public_channel,private_channel\";\n            queryParameters[\"limit\"] = \"1000\";\n            if (!string.IsNullOrEmpty(nextCursor))\n            {\n                queryParameters[\"cursor\"] = nextCursor;\n            }\n            uriBuilder.Query = queryParameters.ToString();\n\n            var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.Uri);\n            request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n\n            var response = await _httpClient.SendAsync(request);\n            var result = await response.Content.ReadFromJsonAsync<SlackChannelListResponse>();\n\n            if (result is { Ok: true })\n            {\n                matchingChannelIds.AddRange(result.Channels\n                    .Where(channel => channelNames.Contains(channel.Name))\n                    .Select(channel => channel.Id));\n                nextCursor = result.ResponseMetadata.NextCursor;\n            }\n            else\n            {\n                logger.LogError(\"Error getting Channel Ids: {Error}\", result?.Error ?? \"Unknown Error\");\n                nextCursor = string.Empty;\n            }\n\n        } while (!string.IsNullOrEmpty(nextCursor));\n\n        return matchingChannelIds;\n    }\n\n    public async Task<string> GetDmChannelByEmailAsync(string token, string email)\n    {\n        var userId = await GetUserIdByEmailAsync(token, email);\n        return await OpenDmChannelAsync(token, userId);\n    }\n\n    public string GetRedirectUrl(string callbackUrl, string state)\n    {\n        var builder = new UriBuilder(_slackOAuthBaseUri);\n        var query = HttpUtility.ParseQueryString(builder.Query);\n\n        query[\"client_id\"] = _clientId;\n        query[\"scope\"] = _scopes;\n        query[\"redirect_uri\"] = callbackUrl;\n        query[\"state\"] = state;\n\n        builder.Query = query.ToString();\n        return builder.ToString();\n    }\n\n    public async Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)\n    {\n        if (string.IsNullOrEmpty(code) || string.IsNullOrWhiteSpace(redirectUrl))\n        {\n            logger.LogError(\"Error obtaining token via OAuth: Code and/or RedirectUrl were empty\");\n            return string.Empty;\n        }\n\n        var tokenResponse = await _httpClient.PostAsync($\"{_slackApiBaseUrl}/oauth.v2.access\",\n            new FormUrlEncodedContent([\n                new KeyValuePair<string, string>(\"client_id\", _clientId),\n                new KeyValuePair<string, string>(\"client_secret\", _clientSecret),\n                new KeyValuePair<string, string>(\"code\", code),\n                new KeyValuePair<string, string>(\"redirect_uri\", redirectUrl)\n            ]));\n\n        SlackOAuthResponse? result;\n        try\n        {\n            result = await tokenResponse.Content.ReadFromJsonAsync<SlackOAuthResponse>();\n        }\n        catch (JsonException ex)\n        {\n            logger.LogError(ex, \"Error parsing SlackOAuthResponse: invalid JSON\");\n            result = null;\n        }\n\n        if (result is null)\n        {\n            logger.LogError(\"Error obtaining token via OAuth: Unknown error\");\n            return string.Empty;\n        }\n        if (!result.Ok)\n        {\n            logger.LogError(\"Error obtaining token via OAuth: {Error}\", result.Error);\n            return string.Empty;\n        }\n\n        return result.AccessToken;\n    }\n\n    public async Task<SlackSendMessageResponse?> SendSlackMessageByChannelIdAsync(string token, string message,\n        string channelId)\n    {\n        var payload = JsonContent.Create(new { channel = channelId, text = message });\n        var request = new HttpRequestMessage(HttpMethod.Post, $\"{_slackApiBaseUrl}/chat.postMessage\");\n        request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n        request.Content = payload;\n\n        var response = await _httpClient.SendAsync(request);\n\n        try\n        {\n            return await response.Content.ReadFromJsonAsync<SlackSendMessageResponse>();\n        }\n        catch (JsonException ex)\n        {\n            logger.LogError(ex, \"Error parsing Slack message response: invalid JSON\");\n            return null;\n        }\n    }\n\n    private async Task<string> GetUserIdByEmailAsync(string token, string email)\n    {\n        var request = new HttpRequestMessage(HttpMethod.Get, $\"{_slackApiBaseUrl}/users.lookupByEmail?email={email}\");\n        request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n        var response = await _httpClient.SendAsync(request);\n        SlackUserResponse? result;\n        try\n        {\n            result = await response.Content.ReadFromJsonAsync<SlackUserResponse>();\n        }\n        catch (JsonException ex)\n        {\n            logger.LogError(ex, \"Error parsing SlackUserResponse: invalid JSON\");\n            result = null;\n        }\n\n        if (result is null)\n        {\n            logger.LogError(\"Error retrieving Slack user ID: Unknown error\");\n            return string.Empty;\n        }\n        if (!result.Ok)\n        {\n            logger.LogError(\"Error retrieving Slack user ID: {Error}\", result.Error);\n            return string.Empty;\n        }\n\n        return result.User.Id;\n    }\n\n    private async Task<string> OpenDmChannelAsync(string token, string userId)\n    {\n        if (string.IsNullOrEmpty(userId))\n            return string.Empty;\n\n        var payload = JsonContent.Create(new { users = userId });\n        var request = new HttpRequestMessage(HttpMethod.Post, $\"{_slackApiBaseUrl}/conversations.open\");\n        request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n        request.Content = payload;\n        var response = await _httpClient.SendAsync(request);\n        SlackDmResponse? result;\n        try\n        {\n            result = await response.Content.ReadFromJsonAsync<SlackDmResponse>();\n        }\n        catch (JsonException ex)\n        {\n            logger.LogError(ex, \"Error parsing SlackDmResponse: invalid JSON\");\n            result = null;\n        }\n\n        if (result is null)\n        {\n            logger.LogError(\"Error opening DM channel: Unknown error\");\n            return string.Empty;\n        }\n        if (!result.Ok)\n        {\n            logger.LogError(\"Error opening DM channel: {Error}\", result.Error);\n            return string.Empty;\n        }\n\n        return result.Channel.Id;\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/TeamsIntegrationHandler.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Microsoft.Rest;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class TeamsIntegrationHandler(\n    ITeamsService teamsService)\n    : IntegrationHandlerBase<TeamsIntegrationConfigurationDetails>\n{\n    public override async Task<IntegrationHandlerResult> HandleAsync(\n        IntegrationMessage<TeamsIntegrationConfigurationDetails> message)\n    {\n        try\n        {\n            await teamsService.SendMessageToChannelAsync(\n                serviceUri: message.Configuration.ServiceUrl,\n                message: message.RenderedTemplate,\n                channelId: message.Configuration.ChannelId\n            );\n\n            return IntegrationHandlerResult.Succeed(message);\n        }\n        catch (HttpOperationException ex)\n        {\n            var category = ClassifyHttpStatusCode(ex.Response.StatusCode);\n            return IntegrationHandlerResult.Fail(\n                message,\n                category,\n                ex.Message\n            );\n        }\n        catch (ArgumentException ex)\n        {\n            return IntegrationHandlerResult.Fail(\n                message,\n                IntegrationFailureCategory.ConfigurationError,\n                ex.Message\n            );\n        }\n        catch (UriFormatException ex)\n        {\n            return IntegrationHandlerResult.Fail(\n                message,\n                IntegrationFailureCategory.ConfigurationError,\n                ex.Message\n            );\n        }\n        catch (JsonException ex)\n        {\n            return IntegrationHandlerResult.Fail(\n                message,\n                IntegrationFailureCategory.PermanentFailure,\n                ex.Message\n            );\n        }\n        catch (Exception ex)\n        {\n            return IntegrationHandlerResult.Fail(\n                message,\n                IntegrationFailureCategory.TransientError,\n                ex.Message\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/TeamsService.cs",
    "content": "﻿using System.Net.Http.Headers;\nusing System.Net.Http.Json;\nusing System.Text.Json;\nusing System.Web;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Models.Data.Teams;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Settings;\nusing Microsoft.Bot.Builder;\nusing Microsoft.Bot.Builder.Teams;\nusing Microsoft.Bot.Connector;\nusing Microsoft.Bot.Connector.Authentication;\nusing Microsoft.Bot.Schema;\nusing Microsoft.Extensions.Logging;\nusing TeamInfo = Bit.Core.Dirt.Models.Data.Teams.TeamInfo;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class TeamsService(\n    IHttpClientFactory httpClientFactory,\n    IOrganizationIntegrationRepository integrationRepository,\n    GlobalSettings globalSettings,\n    ILogger<TeamsService> logger) : ActivityHandler, ITeamsService\n{\n    private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);\n    private readonly string _clientId = globalSettings.Teams.ClientId;\n    private readonly string _clientSecret = globalSettings.Teams.ClientSecret;\n    private readonly string _scopes = globalSettings.Teams.Scopes;\n    private readonly string _graphBaseUrl = globalSettings.Teams.GraphBaseUrl;\n    private readonly string _loginBaseUrl = globalSettings.Teams.LoginBaseUrl;\n\n    public const string HttpClientName = \"TeamsServiceHttpClient\";\n\n    public string GetRedirectUrl(string redirectUrl, string state)\n    {\n        var query = HttpUtility.ParseQueryString(string.Empty);\n        query[\"client_id\"] = _clientId;\n        query[\"response_type\"] = \"code\";\n        query[\"redirect_uri\"] = redirectUrl;\n        query[\"response_mode\"] = \"query\";\n        query[\"scope\"] = string.Join(\" \", _scopes);\n        query[\"state\"] = state;\n\n        return $\"{_loginBaseUrl}/common/oauth2/v2.0/authorize?{query}\";\n    }\n\n    public async Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)\n    {\n        if (string.IsNullOrEmpty(code) || string.IsNullOrWhiteSpace(redirectUrl))\n        {\n            logger.LogError(\"Error obtaining token via OAuth: Code and/or RedirectUrl were empty\");\n            return string.Empty;\n        }\n\n        var request = new HttpRequestMessage(HttpMethod.Post,\n            $\"{_loginBaseUrl}/common/oauth2/v2.0/token\");\n\n        request.Content = new FormUrlEncodedContent(new Dictionary<string, string>\n        {\n            { \"client_id\", _clientId },\n            { \"client_secret\", _clientSecret },\n            { \"code\", code },\n            { \"redirect_uri\", redirectUrl },\n            { \"grant_type\", \"authorization_code\" }\n        });\n\n        using var response = await _httpClient.SendAsync(request);\n        if (!response.IsSuccessStatusCode)\n        {\n            var errorText = await response.Content.ReadAsStringAsync();\n            logger.LogError(\"Teams OAuth token exchange failed: {errorText}\", errorText);\n            return string.Empty;\n        }\n\n        TeamsOAuthResponse? result;\n        try\n        {\n            result = await response.Content.ReadFromJsonAsync<TeamsOAuthResponse>();\n        }\n        catch\n        {\n            result = null;\n        }\n\n        if (result is null)\n        {\n            logger.LogError(\"Error obtaining token via OAuth: Unknown error\");\n            return string.Empty;\n        }\n\n        return result.AccessToken;\n    }\n\n    public async Task<IReadOnlyList<TeamInfo>> GetJoinedTeamsAsync(string accessToken)\n    {\n        using var request = new HttpRequestMessage(\n            HttpMethod.Get,\n            $\"{_graphBaseUrl}/me/joinedTeams\");\n        request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", accessToken);\n\n        using var response = await _httpClient.SendAsync(request);\n        if (!response.IsSuccessStatusCode)\n        {\n            var errorText = await response.Content.ReadAsStringAsync();\n            logger.LogError(\"Get Teams request failed: {errorText}\", errorText);\n            return new List<TeamInfo>();\n        }\n\n        var result = await response.Content.ReadFromJsonAsync<JoinedTeamsResponse>();\n\n        return result?.Value ?? [];\n    }\n\n    public async Task SendMessageToChannelAsync(Uri serviceUri, string channelId, string message)\n    {\n        var credentials = new MicrosoftAppCredentials(_clientId, _clientSecret);\n        using var connectorClient = new ConnectorClient(serviceUri, credentials);\n\n        var activity = new Activity\n        {\n            Type = ActivityTypes.Message,\n            Text = message\n        };\n\n        await connectorClient.Conversations.SendToConversationAsync(channelId, activity);\n    }\n\n    protected override async Task OnInstallationUpdateAddAsync(ITurnContext<IInstallationUpdateActivity> turnContext,\n        CancellationToken cancellationToken)\n    {\n        var conversationId = turnContext.Activity.Conversation.Id;\n        var serviceUrl = turnContext.Activity.ServiceUrl;\n        var teamId = turnContext.Activity.TeamsGetTeamInfo().AadGroupId;\n        var tenantId = turnContext.Activity.Conversation.TenantId;\n\n        if (!string.IsNullOrWhiteSpace(conversationId) &&\n            !string.IsNullOrWhiteSpace(serviceUrl) &&\n            Uri.TryCreate(serviceUrl, UriKind.Absolute, out var parsedUri) &&\n            !string.IsNullOrWhiteSpace(teamId) &&\n            !string.IsNullOrWhiteSpace(tenantId))\n        {\n            await HandleIncomingAppInstallAsync(\n                conversationId: conversationId,\n                serviceUrl: parsedUri,\n                teamId: teamId,\n                tenantId: tenantId\n            );\n        }\n\n        await base.OnInstallationUpdateAddAsync(turnContext, cancellationToken);\n    }\n\n    internal async Task HandleIncomingAppInstallAsync(\n        string conversationId,\n        Uri serviceUrl,\n        string teamId,\n        string tenantId)\n    {\n        var integration = await integrationRepository.GetByTeamsConfigurationTenantIdTeamId(\n            tenantId: tenantId,\n            teamId: teamId);\n\n        if (integration?.Configuration is null)\n        {\n            return;\n        }\n\n        var teamsConfig = JsonSerializer.Deserialize<TeamsIntegration>(integration.Configuration);\n        if (teamsConfig is null || teamsConfig.IsCompleted)\n        {\n            return;\n        }\n\n        integration.Configuration = JsonSerializer.Serialize(teamsConfig with\n        {\n            ChannelId = conversationId,\n            ServiceUrl = serviceUrl\n        });\n\n        await integrationRepository.UpsertAsync(integration);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/Implementations/WebhookIntegrationHandler.cs",
    "content": "﻿using System.Net.Http.Headers;\nusing System.Text;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\n\nnamespace Bit.Core.Dirt.Services.Implementations;\n\npublic class WebhookIntegrationHandler(\n    IHttpClientFactory httpClientFactory,\n    TimeProvider timeProvider)\n    : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>\n{\n    private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);\n\n    public const string HttpClientName = \"WebhookIntegrationHandlerHttpClient\";\n\n    public override async Task<IntegrationHandlerResult> HandleAsync(\n        IntegrationMessage<WebhookIntegrationConfigurationDetails> message)\n    {\n        var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Uri);\n        request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, \"application/json\");\n        if (!string.IsNullOrEmpty(message.Configuration.Scheme))\n        {\n            request.Headers.Authorization = new AuthenticationHeaderValue(\n                scheme: message.Configuration.Scheme,\n                parameter: message.Configuration.Token\n            );\n        }\n\n        var response = await _httpClient.SendAsync(request);\n        return ResultFromHttpResponse(response, message, timeProvider);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/NoopImplementations/NoopEventService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Interfaces;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Services;\n\npublic class NoopEventService : IEventService\n{\n    public Task LogCipherEventAsync(Cipher cipher, EventType type, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogCipherEventsAsync(IEnumerable<Tuple<Cipher, EventType, DateTime?>> events)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogCollectionEventAsync(Collection collection, EventType type, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    Task IEventService.LogCollectionEventsAsync(IEnumerable<(Collection collection, EventType type, DateTime? date)> events)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogGroupEventsAsync(\n        IEnumerable<(Group group, EventType type, EventSystemUser? systemUser, DateTime? date)> events)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogPolicyEventAsync(Policy policy, EventType type, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogGroupEventAsync(Group group, EventType type, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogGroupEventAsync(Group group, EventType type, EventSystemUser systemUser, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogOrganizationEventAsync(Organization organization, EventType type, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogOrganizationEventAsync(Organization organization, EventType type, EventSystemUser systemUser, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogProviderOrganizationEventAsync(ProviderOrganization providerOrganization, EventType type,\n        DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogProviderOrganizationEventsAsync(IEnumerable<(ProviderOrganization, EventType, DateTime?)> events)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type,\n            DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type,\n        EventSystemUser systemUser,\n        DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogOrganizationUserEventAsync<T>(T organizationUser, EventType type, DateTime? date = null) where T : IOrganizationUser\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogOrganizationUserEventAsync<T>(T organizationUser, EventType type,\n        EventSystemUser systemUser, DateTime? date = null) where T : IOrganizationUser\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogOrganizationUserEventsAsync<T>(IEnumerable<(T, EventType, DateTime?)> events) where T : IOrganizationUser\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogOrganizationUserEventsAsync<T>(IEnumerable<(T, EventType, EventSystemUser, DateTime?)> events) where T : IOrganizationUser\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogUserEventAsync(Guid userId, EventType type, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogUserSecretsEventAsync(Guid userId, IEnumerable<Secret> secrets, EventType type,\n        DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type,\n        DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogUserProjectsEventAsync(Guid userId, IEnumerable<Project> projects, EventType type,\n        DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable<Project> projects, EventType type,\n        DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogServiceAccountPeopleEventAsync(Guid userId, UserServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogServiceAccountGroupEventAsync(Guid userId, GroupServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task LogServiceAccountEventAsync(Guid userId, List<ServiceAccount> serviceAccount, EventType type, IdentityClientType identityClientType, DateTime? date = null)\n    {\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/NoopImplementations/NoopEventWriteService.cs",
    "content": "﻿using Bit.Core.Models.Data;\n\nnamespace Bit.Core.Services;\n\npublic class NoopEventWriteService : IEventWriteService\n{\n    public Task CreateAsync(IEvent e)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task CreateManyAsync(IEnumerable<IEvent> e)\n    {\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/NoopImplementations/NoopSlackService.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.Slack;\n\nnamespace Bit.Core.Dirt.Services.NoopImplementations;\n\npublic class NoopSlackService : ISlackService\n{\n    public Task<string> GetChannelIdAsync(string token, string channelName)\n    {\n        return Task.FromResult(string.Empty);\n    }\n\n    public Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames)\n    {\n        return Task.FromResult(new List<string>());\n    }\n\n    public Task<string> GetDmChannelByEmailAsync(string token, string email)\n    {\n        return Task.FromResult(string.Empty);\n    }\n\n    public string GetRedirectUrl(string callbackUrl, string state)\n    {\n        return string.Empty;\n    }\n\n    public Task<SlackSendMessageResponse?> SendSlackMessageByChannelIdAsync(string token, string message,\n        string channelId)\n    {\n        return Task.FromResult<SlackSendMessageResponse?>(null);\n    }\n\n    public Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)\n    {\n        return Task.FromResult(string.Empty);\n    }\n}\n"
  },
  {
    "path": "src/Core/Dirt/Services/NoopImplementations/NoopTeamsService.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.Teams;\n\nnamespace Bit.Core.Dirt.Services.NoopImplementations;\n\npublic class NoopTeamsService : ITeamsService\n{\n    public string GetRedirectUrl(string callbackUrl, string state)\n    {\n        return string.Empty;\n    }\n\n    public Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)\n    {\n        return Task.FromResult(string.Empty);\n    }\n\n    public Task<IReadOnlyList<TeamInfo>> GetJoinedTeamsAsync(string accessToken)\n    {\n        return Task.FromResult<IReadOnlyList<TeamInfo>>(Array.Empty<TeamInfo>());\n    }\n\n    public Task SendMessageToChannelAsync(Uri serviceUri, string channelId, string message)\n    {\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/Collection.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic class Collection : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string Name { get; set; } = null!;\n    [MaxLength(300)]\n    public string? ExternalId { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n    public CollectionType Type { get; set; } = CollectionType.SharedCollection;\n    public string? DefaultUserCollectionEmail { get; set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/CollectionCipher.cs",
    "content": "﻿namespace Bit.Core.Entities;\n\n#nullable enable\n\npublic class CollectionCipher\n{\n    public Guid CollectionId { get; set; }\n    public Guid CipherId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Entities/CollectionGroup.cs",
    "content": "﻿namespace Bit.Core.Entities;\n\n#nullable enable\n\npublic class CollectionGroup\n{\n    public Guid CollectionId { get; set; }\n    public Guid GroupId { get; set; }\n    public bool ReadOnly { get; set; }\n    public bool HidePasswords { get; set; }\n    public bool Manage { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Entities/CollectionUser.cs",
    "content": "﻿namespace Bit.Core.Entities;\n\n#nullable enable\n\npublic class CollectionUser\n{\n    public Guid CollectionId { get; set; }\n    public Guid OrganizationUserId { get; set; }\n    public bool ReadOnly { get; set; }\n    public bool HidePasswords { get; set; }\n    public bool Manage { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Entities/Device.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic class Device : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    [MaxLength(50)]\n    public string Name { get; set; } = null!;\n    public Enums.DeviceType Type { get; set; }\n    [MaxLength(50)]\n    public string Identifier { get; set; } = null!;\n    [MaxLength(255)]\n    public string? PushToken { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n\n    /// <summary>\n    /// Intended to be the users symmetric key that is encrypted in some form, the current way to encrypt this is with\n    /// the devices public key.\n    /// </summary>\n    public string? EncryptedUserKey { get; set; }\n\n    /// <summary>\n    /// Intended to be the public key that was generated for a device upon trust and encrypted. Currenly encrypted using\n    /// a users symmetric key so that when trusted and unlocked a user can decrypt the public key for all their devices.\n    /// This enabled a user to rotate the keys for all of their devices.\n    /// </summary>\n    public string? EncryptedPublicKey { get; set; }\n\n    /// <summary>\n    /// Intended to be the private key that was generated for a device upon trust and encrypted. Currenly encrypted with\n    /// the devices key, that upon successful login a user can decrypt this value and therefor decrypt their vault.\n    /// </summary>\n    public string? EncryptedPrivateKey { get; set; }\n\n    /// <summary>\n    /// Whether the device is active for the user.\n    /// </summary>\n    public bool Active { get; set; } = true;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/Folder.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Vault.Entities;\n\npublic class Folder : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public string? Name { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/IRevisable.cs",
    "content": "﻿namespace Bit.Core.Entities;\n\n#nullable enable\n\npublic interface IRevisable\n{\n    DateTime CreationDate { get; }\n    DateTime RevisionDate { get; }\n}\n"
  },
  {
    "path": "src/Core/Entities/IStorable.cs",
    "content": "﻿namespace Bit.Core.Entities;\n\n#nullable enable\n\npublic interface IStorable\n{\n    long? Storage { get; set; }\n    short? MaxStorageGb { get; set; }\n    long StorageBytesRemaining();\n    long StorageBytesRemaining(short maxStorageGb);\n}\n"
  },
  {
    "path": "src/Core/Entities/IStorableSubscriber.cs",
    "content": "﻿namespace Bit.Core.Entities;\n\n#nullable enable\n\npublic interface IStorableSubscriber : IStorable, ISubscriber\n{ }\n"
  },
  {
    "path": "src/Core/Entities/ISubscriber.cs",
    "content": "﻿using Bit.Core.Enums;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic interface ISubscriber\n{\n    Guid Id { get; }\n    GatewayType? Gateway { get; set; }\n    string? GatewayCustomerId { get; set; }\n    string? GatewaySubscriptionId { get; set; }\n    string? BillingEmailAddress();\n    string? BillingName();\n    string? SubscriberName();\n    string BraintreeCustomerIdPrefix();\n    string BraintreeIdField();\n    string BraintreeCloudRegionField();\n    bool IsOrganization();\n    bool IsUser();\n    string SubscriberType();\n    bool IsExpired();\n}\n"
  },
  {
    "path": "src/Core/Entities/ITableObject.cs",
    "content": "﻿namespace Bit.Core.Entities;\n\n#nullable enable\n\npublic interface ITableObject<T> where T : IEquatable<T>\n{\n    T Id { get; set; }\n    void SetNewId();\n}\n"
  },
  {
    "path": "src/Core/Entities/OrganizationApiKey.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic class OrganizationApiKey : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public OrganizationApiKeyType Type { get; set; }\n    [MaxLength(30)]\n    public string ApiKey { get; set; } = null!;\n    public DateTime RevisionDate { get; set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/OrganizationConnection.cs",
    "content": "﻿using System.Diagnostics.CodeAnalysis;\nusing System.Text.Json;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic class OrganizationConnection<T> : OrganizationConnection where T : IConnectionConfig\n{\n    [DisallowNull]\n    public new T? Config\n    {\n        get => base.GetConfig<T>();\n        set => base.SetConfig(value);\n    }\n}\n\npublic class OrganizationConnection : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public OrganizationConnectionType Type { get; set; }\n    public Guid OrganizationId { get; set; }\n    public bool Enabled { get; set; }\n    public string? Config { get; set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public T? GetConfig<T>() where T : IConnectionConfig\n    {\n        try\n        {\n            if (Config is null)\n            {\n                return default;\n            }\n\n            return JsonSerializer.Deserialize<T>(Config);\n        }\n        catch (JsonException)\n        {\n            return default;\n        }\n    }\n\n    public void SetConfig<T>(T config) where T : IConnectionConfig\n    {\n        Config = JsonSerializer.Serialize(config);\n    }\n\n    public bool Validate<T>(out string exception) where T : IConnectionConfig\n    {\n        if (!Enabled)\n        {\n            exception = $\"Connection disabled for organization {OrganizationId}\";\n            return false;\n        }\n\n        if (string.IsNullOrWhiteSpace(Config))\n        {\n            exception = $\"No saved Connection config for organization {OrganizationId}\";\n            return false;\n        }\n\n        var config = GetConfig<T>();\n        if (config == null)\n        {\n            exception = $\"Error parsing Connection config for organization {OrganizationId}\";\n            return false;\n        }\n\n        return config.Validate(out exception);\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/OrganizationDomain.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic class OrganizationDomain : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string Txt { get; set; } = null!;\n    [MaxLength(255)]\n    public string DomainName { get; set; } = null!;\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime? VerifiedDate { get; private set; }\n    public DateTime NextRunDate { get; private set; }\n    public DateTime? LastCheckedDate { get; private set; }\n    public int JobRunCount { get; private set; }\n    public void SetNewId() => Id = CoreHelpers.GenerateComb();\n\n    public void SetNextRunDate(int interval)\n    {\n        //verification can take up to 72 hours\n        //1st job runs after 12hrs, 2nd after 24hrs and 3rd after 36hrs\n        NextRunDate = JobRunCount == 0\n            ? CreationDate.AddHours(interval)\n            : NextRunDate.AddHours((JobRunCount + 1) * interval);\n    }\n\n    public void SetJobRunCount()\n    {\n        if (JobRunCount == 3)\n        {\n            return;\n        }\n\n        JobRunCount++;\n    }\n\n    public void SetVerifiedDate()\n    {\n        VerifiedDate = DateTime.UtcNow;\n    }\n\n    public void SetLastCheckedDate()\n    {\n        LastCheckedDate = DateTime.UtcNow;\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/OrganizationSponsorship.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic class OrganizationSponsorship : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid? SponsoringOrganizationId { get; set; }\n    public Guid SponsoringOrganizationUserId { get; set; }\n    public Guid? SponsoredOrganizationId { get; set; }\n    [MaxLength(256)]\n    public string? FriendlyName { get; set; }\n    [MaxLength(256)]\n    public string? OfferedToEmail { get; set; }\n    public PlanSponsorshipType? PlanSponsorshipType { get; set; }\n    public DateTime? LastSyncDate { get; set; }\n    public DateTime? ValidUntil { get; set; }\n    public bool ToDelete { get; set; }\n    public bool IsAdminInitiated { get; set; }\n    public string? Notes { get; set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/PlayItem.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Entities;\n\n/// <summary>\n/// PlayItem is a join table tracking entities created during automated testing.\n/// A `PlayId` is supplied by the clients in the `x-play-id` header to inform the server\n/// that any data created should be associated with the play, and therefore cleaned up with it.\n/// </summary>\npublic class PlayItem : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    [MaxLength(256)]\n    public required string PlayId { get; init; }\n    public Guid? UserId { get; init; }\n    public Guid? OrganizationId { get; init; }\n    public DateTime CreationDate { get; init; }\n\n    /// <summary>\n    /// Generates and sets a new COMB GUID for the Id property.\n    /// </summary>\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    /// <summary>\n    /// Creates a new PlayItem record associated with a User.\n    /// </summary>\n    /// <param name=\"user\">The user entity created during the play.</param>\n    /// <param name=\"playId\">The play identifier from the x-play-id header.</param>\n    /// <returns>A new PlayItem instance tracking the user.</returns>\n    public static PlayItem Create(User user, string playId)\n    {\n        return new PlayItem\n        {\n            PlayId = playId,\n            UserId = user.Id,\n            CreationDate = DateTime.UtcNow\n        };\n    }\n\n    /// <summary>\n    /// Creates a new PlayItem record associated with an Organization.\n    /// </summary>\n    /// <param name=\"organization\">The organization entity created during the play.</param>\n    /// <param name=\"playId\">The play identifier from the x-play-id header.</param>\n    /// <returns>A new PlayItem instance tracking the organization.</returns>\n    public static PlayItem Create(Organization organization, string playId)\n    {\n        return new PlayItem\n        {\n            PlayId = playId,\n            OrganizationId = organization.Id,\n            CreationDate = DateTime.UtcNow\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/Role.cs",
    "content": "﻿namespace Bit.Core.Entities;\n\n#nullable enable\n\n/// <summary>\n/// This class is not used. It is implemented to make the Identity provider happy.\n/// </summary>\npublic class Role\n{\n    public string Name { get; set; } = null!;\n}\n"
  },
  {
    "path": "src/Core/Entities/TaxRate.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic class TaxRate : ITableObject<string>\n{\n    [MaxLength(40)]\n    public string Id { get; set; } = null!;\n    [MaxLength(50)]\n    public string Country { get; set; } = null!;\n    [MaxLength(2)]\n    public string? State { get; set; }\n    [MaxLength(10)]\n    public string PostalCode { get; set; } = null!;\n    public decimal Rate { get; set; }\n    public bool Active { get; set; }\n\n    public void SetNewId()\n    {\n        // Id is created by Stripe, should exist before this gets called\n        return;\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/Transaction.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Entities;\n\npublic class Transaction : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public TransactionType Type { get; set; }\n    public decimal Amount { get; set; }\n    public bool? Refunded { get; set; }\n    public decimal? RefundedAmount { get; set; }\n    [MaxLength(100)]\n    public string? Details { get; set; }\n    public PaymentMethodType? PaymentMethodType { get; set; }\n    public GatewayType? Gateway { get; set; }\n    [MaxLength(50)]\n    public string? GatewayId { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public Guid? ProviderId { get; set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Entities/User.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Utilities;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Entities;\n\npublic class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser\n{\n    private Dictionary<TwoFactorProviderType, TwoFactorProvider>? _twoFactorProviders;\n\n    public Guid Id { get; set; }\n    [MaxLength(50)]\n    public string? Name { get; set; }\n    [Required]\n    [MaxLength(256)]\n    public string Email { get; set; } = null!;\n    public bool EmailVerified { get; set; }\n    /// <summary>\n    /// The server-side master-password hash\n    /// </summary>\n    [MaxLength(300)]\n    public string? MasterPassword { get; set; }\n    [MaxLength(50)]\n    public string? MasterPasswordHint { get; set; }\n    [MaxLength(10)]\n    public string Culture { get; set; } = \"en-US\";\n    [Required]\n    [MaxLength(50)]\n    public string SecurityStamp { get; set; } = null!;\n    public string? TwoFactorProviders { get; set; }\n    [MaxLength(32)]\n    public string? TwoFactorRecoveryCode { get; set; }\n    public string? EquivalentDomains { get; set; }\n    public string? ExcludedGlobalEquivalentDomains { get; set; }\n    /// <summary>\n    /// The Account Revision Date is used to check if new sync needs to occur. It should be updated\n    /// whenever a change is made that affects a client's sync data; for example, updating their vault or\n    /// organization membership.\n    /// </summary>\n    public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;\n    /// <summary>\n    /// The master-password-sealed user key.\n    /// </summary>\n    public string? Key { get; set; }\n    /// <summary>\n    /// The raw public key, without a signature from the user's signature key.\n    /// </summary>\n    public string? PublicKey { get; set; }\n    /// <summary>\n    /// User key wrapped private key.\n    /// </summary>\n    public string? PrivateKey { get; set; }\n    /// <summary>\n    /// The public key, signed by the user's signature key.\n    /// </summary>\n    public string? SignedPublicKey { get; set; }\n    /// <summary>\n    /// The security version is included in the security state, but needs COSE parsing\n    /// </summary>\n    public int? SecurityVersion { get; set; }\n    /// <summary>\n    /// The security state is a signed object attesting to the version of the user's account.\n    /// </summary>\n    public string? SecurityState { get; set; }\n    /// <summary>\n    /// Indicates whether the user has a personal premium subscription.\n    /// Does not include premium access from organizations -\n    /// do not use this to check whether the user can access premium features.\n    /// </summary>\n    public bool Premium { get; set; }\n    public DateTime? PremiumExpirationDate { get; set; }\n    public DateTime? RenewalReminderDate { get; set; }\n    public long? Storage { get; set; }\n    public short? MaxStorageGb { get; set; }\n    public GatewayType? Gateway { get; set; }\n    [MaxLength(50)]\n    public string? GatewayCustomerId { get; set; }\n    [MaxLength(50)]\n    public string? GatewaySubscriptionId { get; set; }\n    public string? ReferenceData { get; set; }\n    [MaxLength(100)]\n    public string? LicenseKey { get; set; }\n    [Required]\n    [MaxLength(30)]\n    public string ApiKey { get; set; } = null!;\n    public KdfType Kdf { get; set; } = KdfType.PBKDF2_SHA256;\n    public int KdfIterations { get; set; } = AuthConstants.PBKDF2_ITERATIONS.Default;\n    public int? KdfMemory { get; set; }\n    public int? KdfParallelism { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n    public bool ForcePasswordReset { get; set; }\n    public bool UsesKeyConnector { get; set; }\n    public int FailedLoginCount { get; set; }\n    public DateTime? LastFailedLoginDate { get; set; }\n    [MaxLength(7)]\n    public string? AvatarColor { get; set; }\n    public DateTime? LastPasswordChangeDate { get; set; }\n    public DateTime? LastKdfChangeDate { get; set; }\n    public DateTime? LastKeyRotationDate { get; set; }\n    public DateTime? LastEmailChangeDate { get; set; }\n    public bool VerifyDevices { get; set; } = true;\n    /// <summary>\n    /// V2 upgrade token stored as JSON containing two wrapped user keys.\n    /// Allows clients to unlock vault after V1 to V2 key rotation without logout.\n    /// </summary>\n    public string? V2UpgradeToken { get; set; }\n    [MaxLength(256)]\n    public string? MasterPasswordSalt { get; set; }\n\n    public string GetMasterPasswordSalt()\n    {\n        return MasterPasswordSalt ?? Email.ToLowerInvariant().Trim();\n    }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public string? BillingEmailAddress()\n    {\n        return Email?.ToLowerInvariant()?.Trim();\n    }\n\n    public string? BillingName()\n    {\n        return Name;\n    }\n\n    public string SubscriberName()\n    {\n        return string.IsNullOrWhiteSpace(Name) ? Email : Name;\n    }\n\n    public string BraintreeCustomerIdPrefix()\n    {\n        return \"u\";\n    }\n\n    public string BraintreeIdField()\n    {\n        return \"user_id\";\n    }\n\n    public string BraintreeCloudRegionField()\n    {\n        return \"region\";\n    }\n\n    public string GatewayIdField()\n    {\n        return \"userId\";\n    }\n\n    public bool IsOrganization() => false;\n\n    public bool IsUser()\n    {\n        return true;\n    }\n\n    public string SubscriberType()\n    {\n        return \"Subscriber\";\n    }\n\n    public bool IsExpired() => PremiumExpirationDate.HasValue && PremiumExpirationDate.Value <= DateTime.UtcNow;\n\n    /// <summary>\n    /// Deserializes the User.TwoFactorProviders property from JSON to the appropriate C# dictionary.\n    /// </summary>\n    /// <returns>Dictionary of TwoFactor providers</returns>\n    public Dictionary<TwoFactorProviderType, TwoFactorProvider>? GetTwoFactorProviders()\n    {\n        if (string.IsNullOrWhiteSpace(TwoFactorProviders))\n        {\n            return null;\n        }\n\n        try\n        {\n            _twoFactorProviders ??=\n                JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(\n                    TwoFactorProviders);\n\n            /*\n                U2F is no longer supported, and all users keys should have been migrated to WebAuthn.\n                To prevent issues with accounts being prompted for unsupported U2F we remove them.\n                This will probably exist in perpetuity since there is no way to know for sure if any\n                given user does or doesn't have this enabled. It is a non-zero chance.\n            */\n            _twoFactorProviders?.Remove(TwoFactorProviderType.U2f);\n\n            return _twoFactorProviders;\n        }\n        catch (JsonException)\n        {\n            return null;\n        }\n    }\n\n    public Guid? GetUserId()\n    {\n        return Id;\n    }\n\n    public int GetSecurityVersion()\n    {\n        // If no security version is set, it is version 1. The minimum initialized version is 2.\n        return SecurityVersion ?? 1;\n    }\n\n    /// <summary>\n    /// Evaluates user state to determine if they are currently in a v2 encryption state.\n    /// </summary>\n    /// <returns>If the shape of their private key is v2 as well as has the proper security version then true, otherwise false</returns>\n    public bool HasV2Encryption()\n    {\n        return HasV2KeyShape() && IsSecurityVersionTwo();\n    }\n\n    private bool HasV2KeyShape()\n    {\n        if (string.IsNullOrEmpty(PrivateKey))\n        {\n            return false;\n        }\n\n        try\n        {\n            return EncryptionParsing.GetEncryptionType(PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;\n        }\n        catch (ArgumentException)\n        {\n            // Invalid encryption string format - treat as not v2\n            return false;\n        }\n    }\n\n    /// <summary>\n    /// This technically is correct but all versions after 1 are considered v2 encryption. Leaving for now with\n    /// KM's blessing that when a new version comes along they will handle migration.\n    /// </summary>\n    private bool IsSecurityVersionTwo()\n    {\n        return SecurityVersion == 2;\n    }\n\n    /// <summary>\n    /// Serializes the C# object to the User.TwoFactorProviders property in JSON format.\n    /// </summary>\n    /// <param name=\"providers\">Dictionary of Two Factor providers</param>\n    public void SetTwoFactorProviders(Dictionary<TwoFactorProviderType, TwoFactorProvider> providers)\n    {\n        // When replacing with system.text remember to remove the extra serialization in WebAuthnTokenProvider.\n        TwoFactorProviders = JsonHelpers.LegacySerialize(providers, JsonHelpers.LegacyEnumKeyResolver);\n        _twoFactorProviders = providers;\n    }\n\n    /// <summary>\n    /// Checks if the user has a specific TwoFactorProvider configured. If a user has a premium TwoFactor\n    /// configured it will still be found, even if the user's premium subscription has ended.\n    /// </summary>\n    /// <param name=\"provider\">TwoFactor provider being searched for</param>\n    /// <returns>TwoFactorProvider if found; null otherwise.</returns>\n    public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)\n    {\n        var providers = GetTwoFactorProviders();\n        return providers?.GetValueOrDefault(provider);\n    }\n\n    public long StorageBytesRemaining()\n    {\n        if (!MaxStorageGb.HasValue)\n        {\n            return 0;\n        }\n\n        return StorageBytesRemaining(MaxStorageGb.Value);\n    }\n\n    public long StorageBytesRemaining(short maxStorageGb)\n    {\n        var maxStorageBytes = maxStorageGb * 1073741824L;\n        if (!Storage.HasValue)\n        {\n            return maxStorageBytes;\n        }\n\n        return maxStorageBytes - Storage.Value;\n    }\n\n    public IdentityUser ToIdentityUser(bool twoFactorEnabled)\n    {\n        return new IdentityUser\n        {\n            Id = Id.ToString(),\n            Email = Email,\n            NormalizedEmail = Email,\n            EmailConfirmed = EmailVerified,\n            UserName = Email,\n            NormalizedUserName = Email,\n            TwoFactorEnabled = twoFactorEnabled,\n            SecurityStamp = SecurityStamp\n        };\n    }\n\n    public bool HasMasterPassword()\n    {\n        return MasterPassword != null;\n    }\n\n    public PublicKeyEncryptionKeyPairData GetPublicKeyEncryptionKeyPair()\n    {\n        if (string.IsNullOrWhiteSpace(PrivateKey) || string.IsNullOrWhiteSpace(PublicKey))\n        {\n            throw new InvalidOperationException(\"User public key encryption key pair is not fully initialized.\");\n        }\n\n        return new PublicKeyEncryptionKeyPairData(PrivateKey, PublicKey, SignedPublicKey);\n    }\n}\n"
  },
  {
    "path": "src/Core/Enums/AccessClientType.cs",
    "content": "﻿using Bit.Core.Auth.Identity;\n\nnamespace Bit.Core.Enums;\n\npublic enum AccessClientType\n{\n    NoAccessCheck = -1,\n    User = 0,\n    Organization = 1,\n    ServiceAccount = 2,\n}\n\npublic static class AccessClientHelper\n{\n    public static AccessClientType ToAccessClient(IdentityClientType identityClientType, bool bypassAccessCheck = false)\n    {\n        if (bypassAccessCheck)\n        {\n            return AccessClientType.NoAccessCheck;\n        }\n\n        return identityClientType switch\n        {\n            IdentityClientType.User => AccessClientType.User,\n            IdentityClientType.Organization => AccessClientType.Organization,\n            IdentityClientType.ServiceAccount => AccessClientType.ServiceAccount,\n            _ => throw new ArgumentOutOfRangeException(nameof(identityClientType), identityClientType, null),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Enums/ApplicationCacheMessageType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum ApplicationCacheMessageType : byte\n{\n    UpsertOrganizationAbility = 0,\n    DeleteOrganizationAbility = 1,\n    DeleteProviderAbility = 2,\n}\n"
  },
  {
    "path": "src/Core/Enums/BitwardenClient.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic static class BitwardenClient\n{\n    public const string\n        Web = \"web\",\n        Browser = \"browser\",\n        Desktop = \"desktop\",\n        Mobile = \"mobile\",\n        Cli = \"cli\",\n        DirectoryConnector = \"connector\",\n        Send = \"send\";\n}\n"
  },
  {
    "path": "src/Core/Enums/ClientType.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Enums;\n\npublic enum ClientType : byte\n{\n    [Display(Name = \"All\")]\n    All = 0,\n    [Display(Name = \"Web Vault\")]\n    Web = 1,\n    [Display(Name = \"Browser Extension\")]\n    Browser = 2,\n    [Display(Name = \"Desktop App\")]\n    Desktop = 3,\n    [Display(Name = \"Mobile App\")]\n    Mobile = 4,\n    [Display(Name = \"CLI\")]\n    Cli = 5\n}\n"
  },
  {
    "path": "src/Core/Enums/CollectionType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum CollectionType\n{\n    SharedCollection = 0,\n    DefaultUserCollection = 1,\n}\n"
  },
  {
    "path": "src/Core/Enums/DeviceType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Enums;\n\npublic enum DeviceType : byte\n{\n    [Display(Name = \"Android\")]\n    Android = 0,\n    [Display(Name = \"iOS\")]\n    iOS = 1,\n    [Display(Name = \"Chrome Extension\")]\n    ChromeExtension = 2,\n    [Display(Name = \"Firefox Extension\")]\n    FirefoxExtension = 3,\n    [Display(Name = \"Opera Extension\")]\n    OperaExtension = 4,\n    [Display(Name = \"Edge Extension\")]\n    EdgeExtension = 5,\n    [Display(Name = \"Windows\")]\n    WindowsDesktop = 6,\n    [Display(Name = \"macOS\")]\n    MacOsDesktop = 7,\n    [Display(Name = \"Linux\")]\n    LinuxDesktop = 8,\n    [Display(Name = \"Chrome\")]\n    ChromeBrowser = 9,\n    [Display(Name = \"Firefox\")]\n    FirefoxBrowser = 10,\n    [Display(Name = \"Opera\")]\n    OperaBrowser = 11,\n    [Display(Name = \"Edge\")]\n    EdgeBrowser = 12,\n    [Display(Name = \"Internet Explorer\")]\n    IEBrowser = 13,\n    [Display(Name = \"Unknown Browser\")]\n    UnknownBrowser = 14,\n    [Display(Name = \"Android\")]\n    AndroidAmazon = 15,\n    [Display(Name = \"UWP\")]\n    UWP = 16,\n    [Display(Name = \"Safari\")]\n    SafariBrowser = 17,\n    [Display(Name = \"Vivaldi\")]\n    VivaldiBrowser = 18,\n    [Display(Name = \"Vivaldi Extension\")]\n    VivaldiExtension = 19,\n    [Display(Name = \"Safari Extension\")]\n    SafariExtension = 20,\n    [Display(Name = \"SDK\")]\n    SDK = 21,\n    [Display(Name = \"Server\")]\n    Server = 22,\n    [Display(Name = \"Windows CLI\")]\n    WindowsCLI = 23,\n    [Display(Name = \"MacOs CLI\")]\n    MacOsCLI = 24,\n    [Display(Name = \"Linux CLI\")]\n    LinuxCLI = 25,\n    [Display(Name = \"DuckDuckGo\")]\n    DuckDuckGoBrowser = 26,\n}\n"
  },
  {
    "path": "src/Core/Enums/EncryptionType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\n// If the backing type here changes to a different type you will likely also need to change the value used in\n// EncryptedStringAttribute\npublic enum EncryptionType : byte\n{\n    // symmetric\n    AesCbc256_B64 = 0,\n    AesCbc128_HmacSha256_B64 = 1,\n    AesCbc256_HmacSha256_B64 = 2,\n    XChaCha20Poly1305_B64 = 7,\n\n    // asymmetric\n    [Obsolete(\"PM-29656 - Should probably be removed as it is not known to exist in the real world\")]\n    Rsa2048_OaepSha256_B64 = 3,\n    Rsa2048_OaepSha1_B64 = 4,\n    [Obsolete(\"PM-29656 - Should probably be removed as it is not known to exist in the real world\")]\n    Rsa2048_OaepSha256_HmacSha256_B64 = 5,\n    [Obsolete(\"PM-29656 - Should probably be removed as it is not known to exist in the real world\")]\n    Rsa2048_OaepSha1_HmacSha256_B64 = 6\n}\n"
  },
  {
    "path": "src/Core/Enums/EnumExtensions.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Reflection;\n\nnamespace Bit.Core.Enums;\n\npublic static class EnumExtensions\n{\n    public static string GetDisplayName(this Enum value)\n    {\n        var field = value.GetType().GetField(value.ToString());\n        if (field?.GetCustomAttribute<DisplayAttribute>() is { } attribute)\n        {\n            return attribute.Name ?? value.ToString();\n        }\n\n        return value.ToString();\n    }\n}\n"
  },
  {
    "path": "src/Core/Enums/FileUploadType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum FileUploadType\n{\n    Direct = 0,\n    Azure = 1,\n}\n"
  },
  {
    "path": "src/Core/Enums/GatewayType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Enums;\n\npublic enum GatewayType : byte\n{\n    [Display(Name = \"Stripe\")]\n    Stripe = 0,\n    [Display(Name = \"Braintree\")]\n    Braintree = 1,\n    [Display(Name = \"Apple App Store\")]\n    AppStore = 2,\n    [Display(Name = \"Google Play Store\")]\n    PlayStore = 3,\n    [Display(Name = \"BitPay\")]\n    BitPay = 4,\n    [Display(Name = \"PayPal\")]\n    PayPal = 5,\n    [Display(Name = \"Bank\")]\n    Bank = 6,\n}\n"
  },
  {
    "path": "src/Core/Enums/GlobalEquivalentDomainsType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum GlobalEquivalentDomainsType : byte\n{\n    Google = 0,\n    Apple = 1,\n    Ameritrade = 2,\n    BoA = 3,\n    Sprint = 4,\n    WellsFargo = 5,\n    Merrill = 6,\n    Citi = 7,\n    Cnet = 8,\n    Gap = 9,\n    Microsoft = 10,\n    United = 11,\n    Yahoo = 12,\n    Zonelabs = 13,\n    PayPal = 14,\n    Avon = 15,\n    Diapers = 16,\n    Contacts = 17,\n    Amazon = 18,\n    Cox = 19,\n    Norton = 20,\n    Verizon = 21,\n    Buy = 22,\n    Sirius = 23,\n    Ea = 24,\n    Basecamp = 25,\n    Steam = 26,\n    Chart = 27,\n    Gotomeeting = 28,\n    Gogo = 29,\n    Oracle = 30,\n    Discover = 31,\n    Dcu = 32,\n    Healthcare = 33,\n    Pepco = 34,\n    Century21 = 35,\n    Comcast = 36,\n    Cricket = 37,\n    Mtb = 38,\n    Dropbox = 39,\n    Snapfish = 40,\n    Alibaba = 41,\n    Playstation = 42,\n    Mercado = 43,\n    Zendesk = 44,\n    Autodesk = 45,\n    RailNation = 46,\n    Wpcu = 47,\n    Mathletics = 48,\n    Discountbank = 49,\n    Mi = 50,\n    Facebook = 51,\n    Postepay = 52,\n    Skysports = 53,\n    Disney = 54,\n    Pokemon = 55,\n    Uv = 56,\n    Yahavo = 57,\n    Mdsol = 58,\n    Sears = 59,\n    Xiami = 60,\n    Belkin = 61,\n    Turbotax = 62,\n    Shopify = 63,\n    Ebay = 64,\n    Techdata = 65,\n    Schwab = 66,\n    Mozilla = 67, // deprecated\n    Tesla = 68,\n    MorganStanley = 69,\n    TaxAct = 70,\n    Wikimedia = 71,\n    Airbnb = 72,\n    Eventbrite = 73,\n    StackExchange = 74,\n    Docusign = 75,\n    Envato = 76,\n    X10Hosting = 77,\n    Cisco = 78,\n    CedarFair = 79,\n    Ubiquiti = 80,\n    Discord = 81,\n    Netcup = 82,\n    Yandex = 83,\n    Sony = 84,\n    Proton = 85,\n    Ubisoft = 86,\n    TransferWise = 87,\n    TakeawayEU = 88,\n    Atlassian = 89,\n    Pinterest = 90,\n    Twitter = 91\n}\n"
  },
  {
    "path": "src/Core/Enums/KdfType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum KdfType : byte\n{\n    PBKDF2_SHA256 = 0,\n    Argon2id = 1\n}\n"
  },
  {
    "path": "src/Core/Enums/LicenseType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum LicenseType : byte\n{\n    User = 0,\n    Organization = 1,\n}\n"
  },
  {
    "path": "src/Core/Enums/NotificationHubType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum NotificationHubType\n{\n    General = 0,\n    Android = 1,\n    iOS = 2,\n    GeneralWeb = 3,\n    GeneralBrowserExtension = 4,\n    GeneralDesktop = 5\n}\n"
  },
  {
    "path": "src/Core/Enums/OrganizationApiKeyType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum OrganizationApiKeyType : byte\n{\n    Default = 0,\n    BillingSync = 1,\n    Scim = 2,\n}\n"
  },
  {
    "path": "src/Core/Enums/OrganizationConnectionType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum OrganizationConnectionType : byte\n{\n    CloudBillingSync = 1,\n    Scim = 2,\n}\n"
  },
  {
    "path": "src/Core/Enums/PaymentMethodType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Enums;\n\npublic enum PaymentMethodType : byte\n{\n    [Display(Name = \"Card\")]\n    Card = 0,\n    [Display(Name = \"Bank Account\")]\n    BankAccount = 1,\n    [Display(Name = \"PayPal\")]\n    PayPal = 2,\n    [Display(Name = \"BitPay\")]\n    BitPay = 3,\n    [Display(Name = \"Credit\")]\n    Credit = 4,\n    [Display(Name = \"Wire Transfer\")]\n    WireTransfer = 5,\n    [Display(Name = \"Check\")]\n    Check = 8,\n    [Display(Name = \"None\")]\n    None = 255,\n}\n"
  },
  {
    "path": "src/Core/Enums/PlanSponsorshipType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Enums;\n\npublic enum PlanSponsorshipType : byte\n{\n    [Display(Name = \"Families For Enterprise\")]\n    FamiliesForEnterprise = 0,\n}\n"
  },
  {
    "path": "src/Core/Enums/PushNotificationLogOutReason.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum PushNotificationLogOutReason : byte\n{\n    KdfChange = 0,\n    KeyRotation = 1\n}\n"
  },
  {
    "path": "src/Core/Enums/SupportedDatabaseProviders.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum SupportedDatabaseProviders\n{\n    SqlServer,\n    MySql,\n    Postgres,\n    Sqlite,\n}\n\n"
  },
  {
    "path": "src/Core/Enums/TransactionType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Enums;\n\npublic enum TransactionType : byte\n{\n    [Display(Name = \"Charge\")]\n    Charge = 0,\n    [Display(Name = \"Credit\")]\n    Credit = 1,\n    [Display(Name = \"Promotional Credit\")]\n    PromotionalCredit = 2,\n    [Display(Name = \"Referral Credit\")]\n    ReferralCredit = 3,\n    [Display(Name = \"Refund\")]\n    Refund = 4,\n}\n"
  },
  {
    "path": "src/Core/Enums/UriMatchType.cs",
    "content": "﻿namespace Bit.Core.Enums;\n\npublic enum UriMatchType : byte\n{\n    Domain = 0,\n    Host = 1,\n    StartsWith = 2,\n    Exact = 3,\n    RegularExpression = 4,\n    Never = 5\n}\n"
  },
  {
    "path": "src/Core/Exceptions/BadRequestException.cs",
    "content": "﻿using Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class BadRequestException : Exception\n{\n    public BadRequestException() : base()\n    { }\n\n    public BadRequestException(string message)\n        : base(message)\n    { }\n\n    public BadRequestException(string key, string errorMessage)\n        : base(\"The model state is invalid.\")\n    {\n        ModelState = new ModelStateDictionary();\n        ModelState.AddModelError(key, errorMessage);\n    }\n\n    public BadRequestException(ModelStateDictionary modelState)\n        : base(\"The model state is invalid.\")\n    {\n        if (modelState.IsValid || modelState.ErrorCount == 0)\n        {\n            return;\n        }\n\n        ModelState = modelState;\n    }\n\n    public BadRequestException(IEnumerable<IdentityError> identityErrors)\n    : base(\"The model state is invalid.\")\n    {\n        ModelState = new ModelStateDictionary();\n\n        foreach (var error in identityErrors)\n        {\n            ModelState.AddModelError(error.Code, error.Description);\n        }\n    }\n\n    public ModelStateDictionary? ModelState { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/ConflictException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class ConflictException : Exception\n{\n    public ConflictException() : base(\"Conflict.\") { }\n    public ConflictException(string message) : base(message) { }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/DnsQueryException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class DnsQueryException : Exception\n{\n    public DnsQueryException(string message)\n        : base(message) { }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/DomainClaimedException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class DomainClaimedException : Exception\n{\n    public DomainClaimedException()\n        : base(\"The domain is not available to be claimed.\")\n    {\n\n    }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/DomainVerifiedException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class DomainVerifiedException : Exception\n{\n    public DomainVerifiedException()\n        : base(\"Domain has already been verified.\")\n    {\n\n    }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/DuplicateDomainException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class DuplicateDomainException : Exception\n{\n    public DuplicateDomainException()\n        : base(\"A domain already exists for this organization.\")\n    {\n\n    }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/FeatureUnavailableException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\n/// <summary>\n/// Exception to throw when a requested feature is not yet enabled/available for the requesting context.\n/// </summary>\npublic class FeatureUnavailableException : NotFoundException\n{\n    public FeatureUnavailableException()\n    { }\n\n    public FeatureUnavailableException(string message)\n        : base(message)\n    { }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/GatewayException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class GatewayException : Exception\n{\n    public GatewayException(string message, Exception? innerException = null)\n        : base(message, innerException)\n    { }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/InvalidEmailException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class InvalidEmailException : Exception\n{\n    public InvalidEmailException()\n        : base(\"Invalid email.\")\n    {\n\n    }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/InvalidGatewayCustomerIdException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class InvalidGatewayCustomerIdException : Exception\n{\n    public InvalidGatewayCustomerIdException()\n        : base(\"Invalid gateway customerId.\")\n    {\n\n    }\n}\n"
  },
  {
    "path": "src/Core/Exceptions/NotFoundException.cs",
    "content": "﻿namespace Bit.Core.Exceptions;\n\n#nullable enable\n\npublic class NotFoundException : Exception\n{\n    public NotFoundException() : base()\n    { }\n\n    public NotFoundException(string message)\n        : base(message)\n    { }\n}\n"
  },
  {
    "path": "src/Core/HostedServices/ApplicationCacheHostedService.cs",
    "content": "﻿using Azure.Messaging.ServiceBus;\nusing Azure.Messaging.ServiceBus.Administration;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Services.Implementations;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.HostedServices;\n\n#nullable enable\n\npublic class ApplicationCacheHostedService : IHostedService, IDisposable\n{\n    private readonly FeatureRoutedCacheService? _applicationCacheService;\n    private readonly IOrganizationRepository _organizationRepository;\n    protected readonly ILogger<ApplicationCacheHostedService> _logger;\n    private readonly ServiceBusClient _serviceBusClient;\n    private readonly ServiceBusReceiver _subscriptionReceiver;\n    private readonly ServiceBusAdministrationClient _serviceBusAdministrationClient;\n    private readonly string _subName;\n    private readonly string _topicName;\n    private CancellationTokenSource? _cts;\n    private Task? _executingTask;\n\n\n    public ApplicationCacheHostedService(\n        IApplicationCacheService applicationCacheService,\n        IOrganizationRepository organizationRepository,\n        ILogger<ApplicationCacheHostedService> logger,\n        GlobalSettings globalSettings)\n    {\n        _topicName = globalSettings.ServiceBus.ApplicationCacheTopicName;\n        _subName = CoreHelpers.GetApplicationCacheServiceBusSubscriptionName(globalSettings);\n        _applicationCacheService = applicationCacheService as FeatureRoutedCacheService;\n        _organizationRepository = organizationRepository;\n        _logger = logger;\n        _serviceBusClient = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString);\n        _subscriptionReceiver = _serviceBusClient.CreateReceiver(_topicName, _subName);\n        _serviceBusAdministrationClient = new ServiceBusAdministrationClient(globalSettings.ServiceBus.ConnectionString);\n    }\n\n    public virtual async Task StartAsync(CancellationToken cancellationToken)\n    {\n        try\n        {\n            await _serviceBusAdministrationClient.CreateSubscriptionAsync(new CreateSubscriptionOptions(_topicName, _subName)\n            {\n                DefaultMessageTimeToLive = TimeSpan.FromDays(14),\n                LockDuration = TimeSpan.FromSeconds(30),\n                EnableDeadLetteringOnFilterEvaluationExceptions = true,\n                DeadLetteringOnMessageExpiration = true,\n            }, new CreateRuleOptions\n            {\n                Filter = new SqlRuleFilter($\"sys.label != '{_subName}'\")\n            }, cancellationToken);\n        }\n        catch (ServiceBusException e)\n        when (e.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists)\n        { }\n\n        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        _executingTask = ExecuteAsync(_cts.Token);\n    }\n\n    public virtual async Task StopAsync(CancellationToken cancellationToken)\n    {\n        // Step 1: Signal ExecuteAsync to stop gracefully\n        _cts?.Cancel();\n\n        // Step 2: Wait for ExecuteAsync to finish cleanly\n        if (_executingTask != null)\n        {\n            await _executingTask;\n        }\n\n        // Step 3: Now safely dispose resources (ExecuteAsync is done)\n        await _subscriptionReceiver.CloseAsync(cancellationToken);\n        await _serviceBusClient.DisposeAsync();\n\n        // Step 4: Clean up subscription\n        try\n        {\n            await _serviceBusAdministrationClient.DeleteSubscriptionAsync(_topicName, _subName, cancellationToken);\n        }\n        catch { }\n    }\n\n    public virtual void Dispose()\n    { }\n\n    private async Task ExecuteAsync(CancellationToken cancellationToken)\n    {\n        while (!cancellationToken.IsCancellationRequested)\n        {\n            try\n            {\n                var messages = await _subscriptionReceiver.ReceiveMessagesAsync(\n                    maxMessages: 1,\n                    maxWaitTime: TimeSpan.FromSeconds(30),\n                    cancellationToken);\n\n                if (messages?.Any() == true)\n                {\n                    foreach (var message in messages)\n                    {\n                        try\n                        {\n                            await ProcessMessageAsync(message, cancellationToken);\n                        }\n                        catch (Exception e)\n                        {\n                            _logger.LogError(e, \"Error processing messages in ApplicationCacheHostedService\");\n                        }\n                    }\n                }\n            }\n            catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)\n            {\n                _logger.LogDebug(\"ServiceBus receiver disposed during Alpine container shutdown\");\n                break;\n            }\n            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)\n            {\n                _logger.LogDebug(\"ServiceBus operation cancelled during Alpine container shutdown\");\n                break;\n            }\n        }\n    }\n\n    private async Task ProcessMessageAsync(ServiceBusReceivedMessage message, CancellationToken cancellationToken)\n    {\n        if (message.Subject != _subName && _applicationCacheService != null)\n        {\n            switch ((ApplicationCacheMessageType)message.ApplicationProperties[\"type\"])\n            {\n                case ApplicationCacheMessageType.UpsertOrganizationAbility:\n                    var upsertedOrgId = (Guid)message.ApplicationProperties[\"id\"];\n                    var upsertedOrg = await _organizationRepository.GetByIdAsync(upsertedOrgId);\n                    if (upsertedOrg != null)\n                    {\n                        await _applicationCacheService.BaseUpsertOrganizationAbilityAsync(upsertedOrg);\n                    }\n                    break;\n                case ApplicationCacheMessageType.DeleteOrganizationAbility:\n                    await _applicationCacheService.BaseDeleteOrganizationAbilityAsync(\n                        (Guid)message.ApplicationProperties[\"id\"]);\n                    break;\n                default:\n                    break;\n            }\n        }\n        if (!cancellationToken.IsCancellationRequested)\n        {\n            await _subscriptionReceiver.CompleteMessageAsync(message, cancellationToken);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/HostedServices/IpRateLimitSeedStartupService.cs",
    "content": "﻿using AspNetCoreRateLimit;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Bit.Core.HostedServices;\n\n#nullable enable\n\n/// <summary>\n/// A startup service that will seed the IP rate limiting stores with any values in the\n/// GlobalSettings configuration.\n/// </summary>\n/// <remarks>\n/// <para>Using an <see cref=\"IHostedService\"/> here because it runs before the request processing pipeline\n/// is configured, so that any rate limiting configuration is seeded/applied before any requests come in.\n/// </para>\n/// <para>\n/// This is a cleaner alternative to modifying Program.cs in every project that requires rate limiting as\n/// described/suggested here:\n/// https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/Version-3.0.0-Breaking-Changes\n/// </para>\n/// </remarks>\npublic class IpRateLimitSeedStartupService : IHostedService\n{\n    private readonly IIpPolicyStore _ipPolicyStore;\n    private readonly IClientPolicyStore _clientPolicyStore;\n\n    public IpRateLimitSeedStartupService(IIpPolicyStore ipPolicyStore, IClientPolicyStore clientPolicyStore)\n    {\n        _ipPolicyStore = ipPolicyStore;\n        _clientPolicyStore = clientPolicyStore;\n    }\n\n    public async Task StartAsync(CancellationToken cancellationToken)\n    {\n        // Seed the policies from GlobalSettings\n        await _ipPolicyStore.SeedAsync();\n        await _clientPolicyStore.SeedAsync();\n    }\n\n    // noop\n    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;\n}\n"
  },
  {
    "path": "src/Core/Jobs/BaseJob.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Quartz;\n\nnamespace Bit.Core.Jobs;\n\n#nullable enable\n\npublic abstract class BaseJob : IJob\n{\n    protected readonly ILogger _logger;\n\n    public BaseJob(ILogger logger)\n    {\n        _logger = logger;\n    }\n\n    public async Task Execute(IJobExecutionContext context)\n    {\n        try\n        {\n            await ExecuteJobAsync(context);\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(2, e, \"Error performing {0}.\", GetType().Name);\n        }\n    }\n\n    protected abstract Task ExecuteJobAsync(IJobExecutionContext context);\n}\n"
  },
  {
    "path": "src/Core/Jobs/BaseJobsHostedService.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Quartz;\nusing Quartz.Impl.Matchers;\n\nnamespace Bit.Core.Jobs;\n\n#nullable enable\n\npublic abstract class BaseJobsHostedService : IHostedService, IDisposable\n{\n    private const int MaximumJobRetries = 10;\n\n    private readonly IServiceProvider _serviceProvider;\n    private readonly ILogger<JobListener> _listenerLogger;\n    protected readonly ILogger _logger;\n\n    private IScheduler? _scheduler;\n    protected GlobalSettings _globalSettings;\n\n    public BaseJobsHostedService(\n        GlobalSettings globalSettings,\n        IServiceProvider serviceProvider,\n        ILogger logger,\n        ILogger<JobListener> listenerLogger)\n    {\n        _serviceProvider = serviceProvider;\n        _logger = logger;\n        _listenerLogger = listenerLogger;\n        _globalSettings = globalSettings;\n    }\n\n    public IEnumerable<Tuple<Type, ITrigger>>? Jobs { get; protected set; }\n\n    public virtual async Task StartAsync(CancellationToken cancellationToken)\n    {\n        var fullName = GetType().FullName;\n        if (fullName == null)\n        {\n            throw new InvalidOperationException(\"Hosted service must have a valid type name.\");\n        }\n        var schedulerBuilder = SchedulerBuilder.Create()\n            .WithName(fullName) // Ensure each project has a unique instanceName\n            .WithId(\"AUTO\");\n\n        if (!string.IsNullOrEmpty(_globalSettings.SqlServer.JobSchedulerConnectionString))\n        {\n            schedulerBuilder = schedulerBuilder.UsePersistentStore(options =>\n            {\n                options.UseProperties = true;\n                options.UseClustering();\n                options.UseBinarySerializer();\n                options.UseSqlServer(connectionString: _globalSettings.SqlServer.JobSchedulerConnectionString);\n            });\n        }\n\n        var factory = schedulerBuilder.Build();\n        _scheduler = await factory.GetScheduler(cancellationToken);\n        _scheduler.JobFactory = new JobFactory(_serviceProvider);\n\n        _scheduler.ListenerManager.AddJobListener(new JobListener(_listenerLogger), GroupMatcher<JobKey>.AnyGroup());\n\n        await _scheduler.Start(cancellationToken);\n\n        var jobKeys = new List<JobKey>();\n        var triggerKeys = new List<TriggerKey>();\n\n        if (Jobs != null)\n        {\n            foreach (var (job, trigger) in Jobs)\n            {\n                jobKeys.Add(JobBuilder.Create(job)\n                    .WithIdentity(job.FullName!)\n                    .Build().Key);\n                triggerKeys.Add(trigger.Key);\n\n                for (var retry = 0; retry < MaximumJobRetries; retry++)\n                {\n                    // There's a race condition when starting multiple containers simultaneously, retry until it succeeds..\n                    try\n                    {\n                        var dupeT = await _scheduler.GetTrigger(trigger.Key, cancellationToken);\n                        if (dupeT != null)\n                        {\n                            await _scheduler.RescheduleJob(trigger.Key, trigger, cancellationToken);\n                        }\n\n                        var jobDetail = JobBuilder.Create(job)\n                            .WithIdentity(job.FullName!)\n                            .Build();\n\n                        var dupeJ = await _scheduler.GetJobDetail(jobDetail.Key, cancellationToken);\n                        if (dupeJ != null)\n                        {\n                            await _scheduler.DeleteJob(jobDetail.Key, cancellationToken);\n                        }\n\n                        await _scheduler.ScheduleJob(jobDetail, trigger, cancellationToken);\n                        break;\n                    }\n                    catch (Exception e)\n                    {\n                        if (retry == MaximumJobRetries - 1)\n                        {\n                            throw new Exception(\"Job failed to start after 10 retries.\");\n                        }\n\n                        _logger.LogWarning(e, \"Exception while trying to schedule job: {JobName}\", job.FullName);\n                        var random = new Random();\n                        await Task.Delay(random.Next(50, 250));\n                    }\n                }\n            }\n        }\n\n        // Delete old Jobs and Triggers\n        var existingJobKeys = await _scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup(), cancellationToken);\n\n        foreach (var key in existingJobKeys)\n        {\n            if (jobKeys.Contains(key))\n            {\n                continue;\n            }\n\n            _logger.LogInformation(\"Deleting old job with key {Key}\", key);\n            await _scheduler.DeleteJob(key, cancellationToken);\n        }\n\n        var existingTriggerKeys = await _scheduler.GetTriggerKeys(GroupMatcher<TriggerKey>.AnyGroup(), cancellationToken);\n\n        foreach (var key in existingTriggerKeys)\n        {\n            if (triggerKeys.Contains(key))\n            {\n                continue;\n            }\n\n            _logger.LogInformation(\"Unscheduling old trigger with key {Key}\", key);\n            await _scheduler.UnscheduleJob(key, cancellationToken);\n        }\n    }\n\n    public virtual async Task StopAsync(CancellationToken cancellationToken)\n    {\n        if (_scheduler is not null)\n        {\n            await _scheduler.Shutdown(cancellationToken);\n        }\n    }\n\n    public virtual void Dispose()\n    { }\n}\n"
  },
  {
    "path": "src/Core/Jobs/JobFactory.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Quartz;\nusing Quartz.Spi;\n\nnamespace Bit.Core.Jobs;\n\n#nullable enable\n\npublic class JobFactory : IJobFactory\n{\n    private readonly IServiceProvider _container;\n\n    public JobFactory(IServiceProvider container)\n    {\n        _container = container;\n    }\n\n    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)\n    {\n        var scope = _container.CreateScope();\n        return (scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob)!;\n    }\n\n    public void ReturnJob(IJob job)\n    {\n        var disposable = job as IDisposable;\n        disposable?.Dispose();\n    }\n}\n"
  },
  {
    "path": "src/Core/Jobs/JobListener.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Quartz;\n\nnamespace Bit.Core.Jobs;\n\n#nullable enable\n\npublic class JobListener : IJobListener\n{\n    private readonly ILogger<JobListener> _logger;\n\n    public JobListener(ILogger<JobListener> logger)\n    {\n        _logger = logger;\n    }\n\n    public string Name => \"JobListener\";\n\n    public Task JobExecutionVetoed(IJobExecutionContext context,\n        CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task JobToBeExecuted(IJobExecutionContext context,\n        CancellationToken cancellationToken = default(CancellationToken))\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, null, \"Starting job {0} at {1}.\",\n            context.JobDetail.JobType.Name, DateTime.UtcNow);\n        return Task.FromResult(0);\n    }\n\n    public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException,\n        CancellationToken cancellationToken = default(CancellationToken))\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, null, \"Finished job {0} at {1}.\",\n            context.JobDetail.JobType.Name, DateTime.UtcNow);\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Authorization/KeyConnectorAuthorizationHandler.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.KeyManagement.Authorization;\n\npublic class KeyConnectorAuthorizationHandler : AuthorizationHandler<KeyConnectorOperationsRequirement, User>\n{\n    private readonly ICurrentContext _currentContext;\n\n    public KeyConnectorAuthorizationHandler(ICurrentContext currentContext)\n    {\n        _currentContext = currentContext;\n    }\n\n    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        KeyConnectorOperationsRequirement requirement,\n        User user)\n    {\n        var authorized = requirement switch\n        {\n            not null when requirement == KeyConnectorOperations.Use => CanUse(user),\n            _ => throw new ArgumentException(\"Unsupported operation requirement type provided.\", nameof(requirement))\n        };\n\n        if (authorized)\n        {\n            context.Succeed(requirement);\n        }\n\n        return Task.CompletedTask;\n    }\n\n    private bool CanUse(User user)\n    {\n        // User cannot use Key Connector if they already use it\n        if (user.UsesKeyConnector)\n        {\n            return false;\n        }\n\n        // User cannot use Key Connector if they are an owner or admin of any organization\n        if (_currentContext.Organizations.Any(u =>\n                u.Type is OrganizationUserType.Owner or OrganizationUserType.Admin))\n        {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Authorization/KeyConnectorOperations.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.KeyManagement.Authorization;\n\npublic class KeyConnectorOperationsRequirement : OperationAuthorizationRequirement\n{\n    public KeyConnectorOperationsRequirement(string name)\n    {\n        Name = name;\n    }\n}\n\npublic static class KeyConnectorOperations\n{\n    public static readonly KeyConnectorOperationsRequirement Use = new(nameof(Use));\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Commands/Interfaces/IRegenerateUserAsymmetricKeysCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.KeyManagement.Commands.Interfaces;\n\npublic interface IRegenerateUserAsymmetricKeysCommand\n{\n    Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys,\n        ICollection<OrganizationUser> usersOrganizationAccounts,\n        ICollection<EmergencyAccessDetails> designatedEmergencyAccess);\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Commands/Interfaces/ISetKeyConnectorKeyCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.KeyManagement.Commands.Interfaces;\n\n/// <summary>\n/// Creates the user key and account cryptographic state for a new user registering\n/// with Key Connector SSO configuration.\n/// </summary>\npublic interface ISetKeyConnectorKeyCommand\n{\n    Task SetKeyConnectorKeyForUserAsync(User user, KeyConnectorKeysData keyConnectorKeysData);\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Commands.Interfaces;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.Platform.Push;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.KeyManagement.Commands;\n\npublic class RegenerateUserAsymmetricKeysCommand : IRegenerateUserAsymmetricKeysCommand\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly ILogger<RegenerateUserAsymmetricKeysCommand> _logger;\n    private readonly IUserAsymmetricKeysRepository _userAsymmetricKeysRepository;\n    private readonly IPushNotificationService _pushService;\n\n    public RegenerateUserAsymmetricKeysCommand(\n        ICurrentContext currentContext,\n        IUserAsymmetricKeysRepository userAsymmetricKeysRepository,\n        IPushNotificationService pushService,\n        ILogger<RegenerateUserAsymmetricKeysCommand> logger)\n    {\n        _currentContext = currentContext;\n        _logger = logger;\n        _userAsymmetricKeysRepository = userAsymmetricKeysRepository;\n        _pushService = pushService;\n    }\n\n    public async Task RegenerateKeysAsync(UserAsymmetricKeys userAsymmetricKeys,\n        ICollection<OrganizationUser> usersOrganizationAccounts,\n        ICollection<EmergencyAccessDetails> designatedEmergencyAccess)\n    {\n        var userId = _currentContext.UserId;\n        if (!userId.HasValue ||\n            userAsymmetricKeys.UserId != userId.Value ||\n            usersOrganizationAccounts.Any(ou => ou.UserId != userId) ||\n            designatedEmergencyAccess.Any(dea => dea.GranteeId != userId))\n        {\n            throw new NotFoundException();\n        }\n\n        var inOrganizations = usersOrganizationAccounts.Any(ou =>\n            ou.Status is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked);\n        var hasDesignatedEmergencyAccess = designatedEmergencyAccess.Any(x =>\n            x.Status is EmergencyAccessStatusType.Confirmed or EmergencyAccessStatusType.RecoveryApproved\n                or EmergencyAccessStatusType.RecoveryInitiated);\n\n        _logger.LogInformation(\n            \"User asymmetric keys regeneration requested. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}\",\n            userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType);\n\n        // For now, don't regenerate asymmetric keys for user's with organization membership and designated emergency access.\n        if (inOrganizations || hasDesignatedEmergencyAccess)\n        {\n            throw new BadRequestException(\"Key regeneration not supported for this user.\");\n        }\n\n        await _userAsymmetricKeysRepository.RegenerateUserAsymmetricKeysAsync(userAsymmetricKeys);\n        _logger.LogInformation(\n            \"User's asymmetric keys regenerated. UserId: {userId} OrganizationMembership: {inOrganizations} DesignatedEmergencyAccess: {hasDesignatedEmergencyAccess} DeviceType: {deviceType}\",\n            userAsymmetricKeys.UserId, inOrganizations, hasDesignatedEmergencyAccess, _currentContext.DeviceType);\n\n        await _pushService.PushSyncSettingsAsync(userId.Value);\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Commands/SetKeyConnectorKeyCommand.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Authorization;\nusing Bit.Core.KeyManagement.Commands.Interfaces;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.KeyManagement.Commands;\n\npublic class SetKeyConnectorKeyCommand : ISetKeyConnectorKeyCommand\n{\n    private readonly IAuthorizationService _authorizationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly IEventService _eventService;\n    private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;\n    private readonly IUserService _userService;\n    private readonly IUserRepository _userRepository;\n\n    public SetKeyConnectorKeyCommand(\n        IAuthorizationService authorizationService,\n        ICurrentContext currentContext,\n        IEventService eventService,\n        IAcceptOrgUserCommand acceptOrgUserCommand,\n        IUserService userService,\n        IUserRepository userRepository)\n    {\n        _authorizationService = authorizationService;\n        _currentContext = currentContext;\n        _eventService = eventService;\n        _acceptOrgUserCommand = acceptOrgUserCommand;\n        _userService = userService;\n        _userRepository = userRepository;\n    }\n\n    public async Task SetKeyConnectorKeyForUserAsync(User user, KeyConnectorKeysData keyConnectorKeysData)\n    {\n        var authorizationResult = await _authorizationService.AuthorizeAsync(_currentContext.HttpContext.User, user,\n            KeyConnectorOperations.Use);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new BadRequestException(\"Cannot use Key Connector\");\n        }\n\n        var setKeyConnectorUserKeyTask =\n            _userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorKeysData.KeyConnectorKeyWrappedUserKey);\n\n        await _userRepository.SetV2AccountCryptographicStateAsync(user.Id,\n            keyConnectorKeysData.AccountKeys.ToAccountKeysData(), [setKeyConnectorUserKeyTask]);\n\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);\n\n        await _acceptOrgUserCommand.AcceptOrgUserByOrgSsoIdAsync(keyConnectorKeysData.OrgIdentifier, user,\n            _userService);\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Constants.cs",
    "content": "﻿namespace Bit.Core.KeyManagement;\n\npublic static class Constants\n{\n    public static readonly Version MinimumClientVersionForV2Encryption = new(\"2025.11.0\");\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\n\nnamespace Bit.Core.KeyManagement.Entities;\n\npublic class UserSignatureKeyPair : ITableObject<Guid>, IRevisable\n{\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public SignatureAlgorithm SignatureAlgorithm { get; set; }\n\n    public required string VerifyingKey { get; set; }\n    public required string SigningKey { get; set; }\n\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public SignatureKeyPairData ToSignatureKeyPairData()\n    {\n        return new SignatureKeyPairData(SignatureAlgorithm, SigningKey, VerifyingKey);\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Enums/SignatureAlgorithm.cs",
    "content": "﻿namespace Bit.Core.KeyManagement.Enums;\n\n// <summary>\n// Represents the algorithm / digital signature scheme used for a signature key pair.\n// </summary>\npublic enum SignatureAlgorithm : byte\n{\n    Ed25519 = 0\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Kdf/IChangeKdfCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.KeyManagement.Kdf;\n\n/// <summary>\n/// Command to change the Key Derivation Function (KDF) settings for a user. This includes \n/// changing the masterpassword authentication hash, and the masterkey encrypted userkey.\n/// The salt must not change during the KDF change.\n/// </summary>\npublic interface IChangeKdfCommand\n{\n    public Task<IdentityResult> ChangeKdfAsync(User user, string masterPasswordAuthenticationHash, MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData);\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.KeyManagement.Kdf.Implementations;\n\n/// <inheritdoc />\npublic class ChangeKdfCommand : IChangeKdfCommand\n{\n    private readonly IUserService _userService;\n    private readonly IPushNotificationService _pushService;\n    private readonly IUserRepository _userRepository;\n    private readonly IdentityErrorDescriber _identityErrorDescriber;\n    private readonly ILogger<ChangeKdfCommand> _logger;\n    private readonly IFeatureService _featureService;\n\n    public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService,\n        IUserRepository userRepository, IdentityErrorDescriber describer, ILogger<ChangeKdfCommand> logger,\n        IFeatureService featureService)\n    {\n        _userService = userService;\n        _pushService = pushService;\n        _userRepository = userRepository;\n        _identityErrorDescriber = describer;\n        _logger = logger;\n        _featureService = featureService;\n    }\n\n    public async Task<IdentityResult> ChangeKdfAsync(User user, string masterPasswordAuthenticationHash,\n        MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData)\n    {\n        ArgumentNullException.ThrowIfNull(user);\n        if (!await _userService.CheckPasswordAsync(user, masterPasswordAuthenticationHash))\n        {\n            return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());\n        }\n\n        // Validate to prevent user account from becoming un-decryptable from invalid parameters\n        //\n        // Prevent a de-synced salt value from creating an un-decryptable unlock method\n        authenticationData.ValidateSaltUnchangedForUser(user);\n        unlockData.ValidateSaltUnchangedForUser(user);\n\n        // Currently KDF settings are not saved separately for authentication and unlock and must therefore be equal\n        if (!authenticationData.Kdf.Equals(unlockData.Kdf))\n        {\n            throw new BadRequestException(\"KDF settings must be equal for authentication and unlock.\");\n        }\n\n        var validationErrors = KdfSettingsValidator.Validate(unlockData.Kdf);\n        if (validationErrors.Any())\n        {\n            throw new BadRequestException(\"KDF settings are invalid.\");\n        }\n\n        var logoutOnKdfChange = !_featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange);\n\n        // Update the user with the new KDF settings\n        // This updates the authentication data and unlock data for the user separately. Currently these still\n        // use shared values for KDF settings and salt.\n        // The authentication hash, and the unlock data each are dependent on:\n        // - The master password (entered by the user every time)\n        // - The KDF settings (iterations, memory, parallelism)\n        // - The salt\n        // These combinations - (password, authentication hash, KDF settings, salt) and (password, unlock data, KDF settings, salt)\n        // must remain consistent to unlock correctly.\n\n        // Authentication\n        // Note: This mutates the user but does not yet save it to DB. That is done atomically, later.\n        // This entire operation MUST be atomic to prevent a user from being locked out of their account.\n        // Salt is ensured to be the same as unlock data, and the value stored in the account and not updated.\n        // KDF is ensured to be the same as unlock data above and updated below.\n        var result = await _userService.UpdatePasswordHash(user, authenticationData.MasterPasswordAuthenticationHash,\n            refreshStamp: logoutOnKdfChange);\n        if (!result.Succeeded)\n        {\n            _logger.LogWarning(\"Change KDF failed for user {userId}.\", user.Id);\n            return result;\n        }\n\n        // Salt is ensured to be the same as authentication data, and the value stored in the account, and is not updated.\n        // Kdf - These will be seperated in the future, but for now are ensured to be the same as authentication data above.\n        user.Key = unlockData.MasterKeyWrappedUserKey;\n        user.Kdf = unlockData.Kdf.KdfType;\n        user.KdfIterations = unlockData.Kdf.Iterations;\n        user.KdfMemory = unlockData.Kdf.Memory;\n        user.KdfParallelism = unlockData.Kdf.Parallelism;\n\n        var now = DateTime.UtcNow;\n        user.RevisionDate = user.AccountRevisionDate = now;\n        user.LastKdfChangeDate = now;\n\n        await _userRepository.ReplaceAsync(user);\n        if (logoutOnKdfChange)\n        {\n            await _pushService.PushLogOutAsync(user.Id);\n        }\n        else\n        {\n            // Clients that support the new feature flag will ignore the logout when it matches the reason and the feature flag is enabled.\n            await _pushService.PushLogOutAsync(user.Id, reason: PushNotificationLogOutReason.KdfChange);\n            await _pushService.PushSyncSettingsAsync(user.Id);\n        }\n\n        return IdentityResult.Success;\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.KeyManagement.Authorization;\nusing Bit.Core.KeyManagement.Commands;\nusing Bit.Core.KeyManagement.Commands.Interfaces;\nusing Bit.Core.KeyManagement.Kdf;\nusing Bit.Core.KeyManagement.Kdf.Implementations;\nusing Bit.Core.KeyManagement.Queries;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.KeyManagement;\n\npublic static class KeyManagementServiceCollectionExtensions\n{\n    public static void AddKeyManagementServices(this IServiceCollection services)\n    {\n        services.AddKeyManagementAuthorizationHandlers();\n        services.AddKeyManagementCommands();\n        services.AddKeyManagementQueries();\n        services.AddSendPasswordServices();\n    }\n\n    private static void AddKeyManagementAuthorizationHandlers(this IServiceCollection services)\n    {\n        services.AddScoped<IAuthorizationHandler, KeyConnectorAuthorizationHandler>();\n    }\n\n    private static void AddKeyManagementCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();\n        services.AddScoped<IChangeKdfCommand, ChangeKdfCommand>();\n        services.AddScoped<ISetKeyConnectorKeyCommand, SetKeyConnectorKeyCommand>();\n    }\n\n    private static void AddKeyManagementQueries(this IServiceCollection services)\n    {\n        services.AddScoped<IUserAccountKeysQuery, UserAccountKeysQuery>();\n        services.AddScoped<IKeyConnectorConfirmationDetailsQuery, KeyConnectorConfirmationDetailsQuery>();\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Request/AccountKeysRequestModel.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Request;\n\npublic class AccountKeysRequestModel\n{\n    [EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }\n    public required string AccountPublicKey { get; set; }\n\n    public PublicKeyEncryptionKeyPairRequestModel? PublicKeyEncryptionKeyPair { get; set; }\n    public SignatureKeyPairRequestModel? SignatureKeyPair { get; set; }\n    public SecurityStateModel? SecurityState { get; set; }\n\n    public UserAccountKeysData ToAccountKeysData()\n    {\n        // This will be cleaned up, after a compatibility period, at which point PublicKeyEncryptionKeyPair and SignatureKeyPair will be required.\n        // TODO: https://bitwarden.atlassian.net/browse/PM-23751\n        if (PublicKeyEncryptionKeyPair == null)\n        {\n            return new UserAccountKeysData\n            {\n                PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData\n                (\n                    UserKeyEncryptedAccountPrivateKey,\n                    AccountPublicKey\n                ),\n            };\n        }\n        else\n        {\n            if (SignatureKeyPair == null || SecurityState == null)\n            {\n                return new UserAccountKeysData\n                {\n                    PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),\n                };\n            }\n            else\n            {\n                return new UserAccountKeysData\n                {\n                    PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),\n                    SignatureKeyPairData = SignatureKeyPair.ToSignatureKeyPairData(),\n                    SecurityStateData = SecurityState.ToSecurityState()\n                };\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Request/KdfRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Request;\n\npublic class KdfRequestModel : IValidatableObject\n{\n    [Required]\n    public required KdfType KdfType { get; init; }\n    [Required]\n    public required int Iterations { get; init; }\n    public int? Memory { get; init; }\n    public int? Parallelism { get; init; }\n\n    public KdfSettings ToData()\n    {\n        return new KdfSettings\n        {\n            KdfType = KdfType,\n            Iterations = Iterations,\n            Memory = Memory,\n            Parallelism = Parallelism\n        };\n    }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        // Generic per-request KDF validation for any request model embedding KdfRequestModel\n        return KdfSettingsValidator.Validate(ToData());\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Request/MasterPasswordAuthenticationDataRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Request;\n\n/// <summary>\n/// Use this datatype when interfacing with requests to create a separation of concern.\n/// See <see cref=\"MasterPasswordAuthenticationData\"/> to use for commands, queries, services.\n/// </summary>\npublic class MasterPasswordAuthenticationDataRequestModel\n{\n    public required KdfRequestModel Kdf { get; init; }\n    [Required]\n    public required string MasterPasswordAuthenticationHash { get; init; }\n    [Required]\n    [StringLength(256)]\n    public required string Salt { get; init; }\n\n    public MasterPasswordAuthenticationData ToData()\n    {\n        return new MasterPasswordAuthenticationData\n        {\n            Kdf = Kdf.ToData(),\n            MasterPasswordAuthenticationHash = MasterPasswordAuthenticationHash,\n            Salt = Salt\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Request/MasterPasswordUnlockDataRequestModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Request;\n\n/// <summary>\n/// Use this datatype when interfacing with requests to create a separation of concern.\n/// See <see cref=\"MasterPasswordUnlockData\"/> to use for commands, queries, services.\n/// </summary>\npublic class MasterPasswordUnlockDataRequestModel\n{\n    public required KdfRequestModel Kdf { get; init; }\n    [Required]\n    [EncryptedString]\n    public required string MasterKeyWrappedUserKey { get; init; }\n    [Required]\n    [StringLength(256)]\n    public required string Salt { get; init; }\n\n    public MasterPasswordUnlockData ToData()\n    {\n        return new MasterPasswordUnlockData\n        {\n            Kdf = Kdf.ToData(),\n            MasterKeyWrappedUserKey = MasterKeyWrappedUserKey,\n            Salt = Salt\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Request/PublicKeyEncryptionKeyPairRequestModel.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Request;\n\npublic class PublicKeyEncryptionKeyPairRequestModel\n{\n    [EncryptedString] public required string WrappedPrivateKey { get; set; }\n    public required string PublicKey { get; set; }\n    public string? SignedPublicKey { get; set; }\n\n    public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData()\n    {\n        return new PublicKeyEncryptionKeyPairData(\n            WrappedPrivateKey,\n            PublicKey,\n            SignedPublicKey\n        );\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Request/SecurityStateModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json.Serialization;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Request;\n\npublic class SecurityStateModel\n{\n    [StringLength(1000)]\n    [JsonPropertyName(\"securityState\")]\n    public required string SecurityState { get; set; }\n    [JsonPropertyName(\"securityVersion\")]\n    public required int SecurityVersion { get; set; }\n\n    public SecurityStateData ToSecurityState()\n    {\n        return new SecurityStateData\n        {\n            SecurityState = SecurityState,\n            SecurityVersion = SecurityVersion\n        };\n    }\n\n    public static SecurityStateModel FromSecurityStateData(SecurityStateData data)\n    {\n        return new SecurityStateModel\n        {\n            SecurityState = data.SecurityState,\n            SecurityVersion = data.SecurityVersion\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Request/SignatureKeyPairRequestModel.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Request;\n\npublic class SignatureKeyPairRequestModel\n{\n    public required string SignatureAlgorithm { get; set; }\n    [EncryptedString] public required string WrappedSigningKey { get; set; }\n    public required string VerifyingKey { get; set; }\n\n    public SignatureKeyPairData ToSignatureKeyPairData()\n    {\n        if (SignatureAlgorithm != \"ed25519\")\n        {\n            throw new ArgumentException(\n                $\"Unsupported signature algorithm: {SignatureAlgorithm}\"\n            );\n        }\n        var algorithm = Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519;\n\n        return new SignatureKeyPairData(\n            algorithm,\n            WrappedSigningKey,\n            VerifyingKey\n        );\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Response/MasterPasswordUnlockResponseModel.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Response;\n\npublic class MasterPasswordUnlockResponseModel\n{\n    public required MasterPasswordUnlockKdfResponseModel Kdf { get; init; }\n    /// <summary>\n    /// The user's symmetric key encrypted with their master key.\n    /// Also known as \"MasterKeyWrappedUserKey\"\n    /// </summary>\n    [EncryptedString] public required string MasterKeyEncryptedUserKey { get; init; }\n    [StringLength(256)] public required string Salt { get; init; }\n}\n\npublic class MasterPasswordUnlockKdfResponseModel\n{\n    public required KdfType KdfType { get; init; }\n    public required int Iterations { get; init; }\n    public int? Memory { get; init; }\n    public int? Parallelism { get; init; }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Response/PrivateKeysResponseModel.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Response;\n\n\n/// <summary>\n/// This response model is used to return the asymmetric encryption keys,\n/// and signature keys of an entity. This includes the private keys of the key pairs,\n/// (private key, signing key), and the public keys of the key pairs (unsigned public key,\n/// signed public key, verification key). \n/// </summary>\npublic class PrivateKeysResponseModel : ResponseModel\n{\n    // Not all accounts have signature keys, but all accounts have public encryption keys.\n    [JsonPropertyName(\"signatureKeyPair\")]\n    public SignatureKeyPairResponseModel? SignatureKeyPair { get; set; }\n\n    [JsonPropertyName(\"publicKeyEncryptionKeyPair\")]\n    public required PublicKeyEncryptionKeyPairResponseModel PublicKeyEncryptionKeyPair { get; set; }\n\n    [JsonPropertyName(\"securityState\")]\n    public SecurityStateModel? SecurityState { get; set; }\n\n    [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]\n    public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base(\"privateKeys\")\n    {\n        ArgumentNullException.ThrowIfNull(accountKeys);\n        PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairResponseModel(accountKeys.PublicKeyEncryptionKeyPairData);\n\n        if (accountKeys.SignatureKeyPairData != null && accountKeys.SecurityStateData != null)\n        {\n            SignatureKeyPair = new SignatureKeyPairResponseModel(accountKeys.SignatureKeyPairData);\n            SecurityState = SecurityStateModel.FromSecurityStateData(accountKeys.SecurityStateData!);\n        }\n    }\n\n    [JsonConstructor]\n    public PrivateKeysResponseModel(SignatureKeyPairResponseModel? signatureKeyPair, PublicKeyEncryptionKeyPairResponseModel publicKeyEncryptionKeyPair, SecurityStateModel? securityState)\n        : base(\"privateKeys\")\n    {\n        SignatureKeyPair = signatureKeyPair;\n        PublicKeyEncryptionKeyPair = publicKeyEncryptionKeyPair ?? throw new ArgumentNullException(nameof(publicKeyEncryptionKeyPair));\n        SecurityState = securityState;\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Response/PublicKeyEncryptionKeyPairResponseModel.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Response;\n\n\npublic class PublicKeyEncryptionKeyPairResponseModel : ResponseModel\n{\n    [JsonPropertyName(\"wrappedPrivateKey\")]\n    public required string WrappedPrivateKey { get; set; }\n    [JsonPropertyName(\"publicKey\")]\n    public required string PublicKey { get; set; }\n    [JsonPropertyName(\"signedPublicKey\")]\n    public string? SignedPublicKey { get; set; }\n\n    [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]\n    public PublicKeyEncryptionKeyPairResponseModel(PublicKeyEncryptionKeyPairData keyPair)\n        : base(\"publicKeyEncryptionKeyPair\")\n    {\n        WrappedPrivateKey = keyPair.WrappedPrivateKey;\n        PublicKey = keyPair.PublicKey;\n        SignedPublicKey = keyPair.SignedPublicKey;\n    }\n\n    [JsonConstructor]\n    public PublicKeyEncryptionKeyPairResponseModel(string wrappedPrivateKey, string publicKey, string? signedPublicKey)\n        : base(\"publicKeyEncryptionKeyPair\")\n    {\n        WrappedPrivateKey = wrappedPrivateKey ?? throw new ArgumentNullException(nameof(wrappedPrivateKey));\n        PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey));\n        SignedPublicKey = signedPublicKey;\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Response/PublicKeysResponseModel.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Response;\n\n\n/// <summary>\n/// This response model is used to return the public keys of a user, to any other registered user or entity on the server.\n/// It can contain public keys (signature/encryption), and proofs between the two. It does not contain (encrypted) private keys.\n/// </summary>\npublic class PublicKeysResponseModel : ResponseModel\n{\n    [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]\n    public PublicKeysResponseModel(UserAccountKeysData accountKeys)\n        : base(\"publicKeys\")\n    {\n        ArgumentNullException.ThrowIfNull(accountKeys);\n        PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;\n\n        if (accountKeys.SignatureKeyPairData != null)\n        {\n            SignedPublicKey = accountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;\n            VerifyingKey = accountKeys.SignatureKeyPairData.VerifyingKey;\n        }\n    }\n\n    public string? VerifyingKey { get; set; }\n    public string? SignedPublicKey { get; set; }\n    public required string PublicKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Response/SignatureKeyPairResponseModel.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Api;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Response;\n\n\npublic class SignatureKeyPairResponseModel : ResponseModel\n{\n    [JsonPropertyName(\"wrappedSigningKey\")]\n    public required string WrappedSigningKey { get; set; }\n    [JsonPropertyName(\"verifyingKey\")]\n    public required string VerifyingKey { get; set; }\n\n    [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]\n    public SignatureKeyPairResponseModel(SignatureKeyPairData signatureKeyPair)\n        : base(\"signatureKeyPair\")\n    {\n        ArgumentNullException.ThrowIfNull(signatureKeyPair);\n        WrappedSigningKey = signatureKeyPair.WrappedSigningKey;\n        VerifyingKey = signatureKeyPair.VerifyingKey;\n    }\n\n\n    [JsonConstructor]\n    public SignatureKeyPairResponseModel(string wrappedSigningKey, string verifyingKey)\n        : base(\"signatureKeyPair\")\n    {\n        WrappedSigningKey = wrappedSigningKey ?? throw new ArgumentNullException(nameof(wrappedSigningKey));\n        VerifyingKey = verifyingKey ?? throw new ArgumentNullException(nameof(verifyingKey));\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Response/UserDecryptionResponseModel.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.Auth.Models.Api.Response;\n\nnamespace Bit.Core.KeyManagement.Models.Api.Response;\n\npublic class UserDecryptionResponseModel\n{\n    /// <summary>\n    /// Returns the unlock data when the user has a master password that can be used to decrypt their vault.\n    /// </summary>\n    public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; }\n\n    /// <summary>\n    /// Gets or sets the WebAuthn PRF decryption keys.\n    /// </summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public WebAuthnPrfDecryptionOption[]? WebAuthnPrfOptions { get; set; }\n\n    /// <summary>\n    /// V2 upgrade token returned when available, allowing unlock after V1→V2 upgrade.\n    /// </summary>\n    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n    public V2UpgradeTokenResponseModel? V2UpgradeToken { get; set; }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Api/Response/V2UpgradeTokenResponseModel.cs",
    "content": "﻿namespace Bit.Core.KeyManagement.Models.Api.Response;\n\npublic class V2UpgradeTokenResponseModel\n{\n    public required string WrappedUserKey1 { get; set; }\n    public required string WrappedUserKey2 { get; set; }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/KdfSettings.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.KeyManagement.Models.Data;\n\npublic class KdfSettings\n{\n    public required KdfType KdfType { get; init; }\n    public required int Iterations { get; init; }\n    public int? Memory { get; init; }\n    public int? Parallelism { get; init; }\n\n    public void ValidateUnchangedForUser(User user)\n    {\n        if (user.Kdf != KdfType || user.KdfIterations != Iterations || user.KdfMemory != Memory || user.KdfParallelism != Parallelism)\n        {\n            throw new ArgumentException(\"Invalid KDF settings.\");\n        }\n    }\n\n    public override bool Equals(object? obj)\n    {\n        if (obj is not KdfSettings other)\n        {\n            return false;\n        }\n\n        return KdfType == other.KdfType &&\n               Iterations == other.Iterations &&\n               Memory == other.Memory &&\n               Parallelism == other.Parallelism;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(KdfType, Iterations, Memory, Parallelism);\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/KeyConnectorConfirmationDetails.cs",
    "content": "﻿namespace Bit.Core.KeyManagement.Models.Data;\n\npublic class KeyConnectorConfirmationDetails\n{\n    public required string OrganizationName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/KeyConnectorKeysData.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Api.Request;\n\nnamespace Bit.Core.KeyManagement.Models.Data;\n\npublic class KeyConnectorKeysData\n{\n    public required string KeyConnectorKeyWrappedUserKey { get; set; }\n\n    public required AccountKeysRequestModel AccountKeys { get; set; }\n\n    public required string OrgIdentifier { get; init; }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Api.Request;\n\nnamespace Bit.Core.KeyManagement.Models.Data;\n\n/// <summary>\n/// Use this datatype when interfacing with commands, queries, services to create a separation of concern.\n/// See <see cref=\"MasterPasswordAuthenticationDataRequestModel\"/> to use for requests.\n/// </summary>\npublic class MasterPasswordAuthenticationData\n{\n    public required KdfSettings Kdf { get; init; }\n    public required string MasterPasswordAuthenticationHash { get; init; }\n    public required string Salt { get; init; }\n\n    public void ValidateSaltUnchangedForUser(User user)\n    {\n        if (user.GetMasterPasswordSalt() != Salt)\n        {\n            throw new BadRequestException(\"Invalid master password salt.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/MasterPasswordUnlockAndAuthenticationData.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.KeyManagement.Models.Data;\n\npublic class MasterPasswordUnlockAndAuthenticationData\n{\n    public KdfType KdfType { get; set; }\n    public int KdfIterations { get; set; }\n    public int? KdfMemory { get; set; }\n    public int? KdfParallelism { get; set; }\n\n    public required string Email { get; set; }\n    public required string MasterKeyAuthenticationHash { get; set; }\n    /// <summary>\n    /// The user's symmetric key encrypted with their master key.\n    /// Also known as \"MasterKeyWrappedUserKey\"\n    /// </summary>\n    public required string MasterKeyEncryptedUserKey { get; set; }\n    public string? MasterPasswordHint { get; set; }\n\n    public bool ValidateForUser(User user)\n    {\n        if (KdfType != user.Kdf || KdfMemory != user.KdfMemory || KdfParallelism != user.KdfParallelism || KdfIterations != user.KdfIterations)\n        {\n            return false;\n        }\n        else if (Email != user.Email)\n        {\n            return false;\n        }\n        else\n        {\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Api.Request;\n\nnamespace Bit.Core.KeyManagement.Models.Data;\n\n/// <summary>\n/// Use this datatype when interfacing with commands, queries, services to create a separation of concern.\n/// See <see cref=\"MasterPasswordUnlockDataRequestModel\"/> to use for requests.\n/// </summary>\npublic class MasterPasswordUnlockData\n{\n    public required KdfSettings Kdf { get; init; }\n    public required string MasterKeyWrappedUserKey { get; init; }\n    public required string Salt { get; init; }\n\n    public void ValidateSaltUnchangedForUser(User user)\n    {\n        if (user.GetMasterPasswordSalt() != Salt)\n        {\n            throw new BadRequestException(\"Invalid master password salt.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs",
    "content": "﻿using System.Text.Json.Serialization;\n\nnamespace Bit.Core.KeyManagement.Models.Data;\n\n\npublic class PublicKeyEncryptionKeyPairData\n{\n    public required string WrappedPrivateKey { get; set; }\n    public string? SignedPublicKey { get; set; }\n    public required string PublicKey { get; set; }\n\n    [JsonConstructor]\n    [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]\n    public PublicKeyEncryptionKeyPairData(string wrappedPrivateKey, string publicKey, string? signedPublicKey = null)\n    {\n        WrappedPrivateKey = wrappedPrivateKey ?? throw new ArgumentNullException(nameof(wrappedPrivateKey));\n        PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey));\n        SignedPublicKey = signedPublicKey;\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs",
    "content": "﻿\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.KeyManagement.Models.Data;\n\npublic class RotateUserAccountKeysData\n{\n    // Authentication for this requests\n    public required string OldMasterKeyAuthenticationHash { get; set; }\n\n    public required UserAccountKeysData AccountKeys { get; set; }\n\n    // All methods to get to the userkey\n    public required MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; }\n    public required IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }\n    public required IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }\n    public required IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }\n    public required IEnumerable<Device> DeviceKeys { get; set; }\n    public V2UpgradeTokenData? V2UpgradeToken { get; set; }\n\n    // User vault data encrypted by the userkey\n    public required IEnumerable<Cipher> Ciphers { get; set; }\n    public required IEnumerable<Folder> Folders { get; set; }\n    public required IReadOnlyList<Send> Sends { get; set; }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/SecurityStateData.cs",
    "content": "﻿\nnamespace Bit.Core.KeyManagement.Models.Data;\n\npublic class SecurityStateData\n{\n    public required string SecurityState { get; set; }\n    // The security version is included in the security state, but needs COSE parsing,\n    // so this is a separate copy that can be used directly.\n    public required int SecurityVersion { get; set; }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs",
    "content": "﻿\nusing System.Text.Json.Serialization;\nusing Bit.Core.KeyManagement.Enums;\n\nnamespace Bit.Core.KeyManagement.Models.Data;\n\npublic class SignatureKeyPairData\n{\n    public required SignatureAlgorithm SignatureAlgorithm { get; set; }\n    public required string WrappedSigningKey { get; set; }\n    public required string VerifyingKey { get; set; }\n\n    [JsonConstructor]\n    [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]\n    public SignatureKeyPairData(SignatureAlgorithm signatureAlgorithm, string wrappedSigningKey, string verifyingKey)\n    {\n        SignatureAlgorithm = signatureAlgorithm;\n        WrappedSigningKey = wrappedSigningKey ?? throw new ArgumentNullException(nameof(wrappedSigningKey));\n        VerifyingKey = verifyingKey ?? throw new ArgumentNullException(nameof(verifyingKey));\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs",
    "content": "﻿namespace Bit.Core.KeyManagement.Models.Data;\n\n/// <summary>\n/// Represents an expanded account cryptographic state for a user. Expanded here means\n/// that it does not only contain the (wrapped) private / signing key, but also the public\n/// key / verifying key. The client side only needs a subset of this data to unlock\n/// their vault and the public parts can be derived.\n/// </summary>\npublic class UserAccountKeysData\n{\n    public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; }\n    public SignatureKeyPairData? SignatureKeyPairData { get; set; }\n    public SecurityStateData? SecurityStateData { get; set; }\n\n    /// <summary>\n    /// Checks whether the account cryptographic state is for a V1 encryption user or a V2 encryption user.\n    /// Throws if the state is invalid\n    /// </summary>\n    public bool IsV2Encryption()\n    {\n        if (PublicKeyEncryptionKeyPairData.SignedPublicKey != null && SignatureKeyPairData != null && SecurityStateData != null)\n        {\n            return true;\n        }\n        else if (PublicKeyEncryptionKeyPairData.SignedPublicKey == null && SignatureKeyPairData == null && SecurityStateData == null)\n        {\n            return false;\n        }\n        else\n        {\n            throw new InvalidOperationException(\"Invalid account cryptographic state: V2 encryption fields must be either all present or all absent.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/UserAsymmetricKeys.cs",
    "content": "﻿#nullable enable\nnamespace Bit.Core.KeyManagement.Models.Data;\n\npublic class UserAsymmetricKeys\n{\n    public Guid UserId { get; set; }\n    public required string PublicKey { get; set; }\n    public required string UserKeyEncryptedPrivateKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Models/Data/V2UpgradeTokenData.cs",
    "content": "﻿using System.Text.Json;\n\nnamespace Bit.Core.KeyManagement.Models.Data;\n\npublic class V2UpgradeTokenData\n{\n    public required string WrappedUserKey1 { get; init; }\n    public required string WrappedUserKey2 { get; init; }\n\n    public string ToJson()\n    {\n        return JsonSerializer.Serialize(this);\n    }\n\n    public static V2UpgradeTokenData? FromJson(string? json)\n    {\n        if (string.IsNullOrWhiteSpace(json))\n        {\n            return null;\n        }\n\n        try\n        {\n            return JsonSerializer.Deserialize<V2UpgradeTokenData>(json);\n        }\n        catch (JsonException)\n        {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Queries/Interfaces/IKeyConnectorConfirmationDetailsQuery.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.KeyManagement.Queries.Interfaces;\n\npublic interface IKeyConnectorConfirmationDetailsQuery\n{\n    public Task<KeyConnectorConfirmationDetails> Run(string orgSsoIdentifier, Guid userId);\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs",
    "content": "﻿\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.KeyManagement.Queries.Interfaces;\n\npublic interface IUserAccountKeysQuery\n{\n    Task<UserAccountKeysData> Run(User user);\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Queries/KeyConnectorConfirmationDetailsQuery.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.KeyManagement.Queries;\n\npublic class KeyConnectorConfirmationDetailsQuery : IKeyConnectorConfirmationDetailsQuery\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public KeyConnectorConfirmationDetailsQuery(IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository)\n    {\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    public async Task<KeyConnectorConfirmationDetails> Run(string orgSsoIdentifier, Guid userId)\n    {\n        var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier);\n        if (org is not { UseKeyConnector: true })\n        {\n            throw new NotFoundException();\n        }\n\n        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, userId);\n        if (orgUser == null)\n        {\n            throw new NotFoundException();\n        }\n\n        return new KeyConnectorConfirmationDetails { OrganizationName = org.Name, };\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs",
    "content": "﻿\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.KeyManagement.Repositories;\n\nnamespace Bit.Core.KeyManagement.Queries;\n\n\npublic class UserAccountKeysQuery(IUserSignatureKeyPairRepository signatureKeyPairRepository) : IUserAccountKeysQuery\n{\n    public async Task<UserAccountKeysData> Run(User user)\n    {\n        if (user.GetSecurityVersion() < 2)\n        {\n            return new UserAccountKeysData\n            {\n                PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),\n            };\n        }\n        else\n        {\n            return new UserAccountKeysData\n            {\n                PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),\n                SignatureKeyPairData = await signatureKeyPairRepository.GetByUserIdAsync(user.Id),\n                SecurityStateData = new SecurityStateData\n                {\n                    SecurityState = user.SecurityState!,\n                    SecurityVersion = user.GetSecurityVersion(),\n                }\n            };\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Repositories/IUserAsymmetricKeysRepository.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.KeyManagement.Repositories;\n\npublic interface IUserAsymmetricKeysRepository\n{\n    Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys);\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs",
    "content": "﻿\nusing Bit.Core.KeyManagement.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.KeyManagement.Repositories;\n\npublic interface IUserSignatureKeyPairRepository : IRepository<UserSignatureKeyPair, Guid>\n{\n    public Task<SignatureKeyPairData?> GetByUserIdAsync(Guid userId);\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signatureKeyPair);\n    public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signatureKeyPair);\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Sends/ISendPasswordHasher.cs",
    "content": "﻿namespace Bit.Core.KeyManagement.Sends;\n\npublic interface ISendPasswordHasher\n{\n    /// <summary>\n    /// Matches the send password hash against the user provided client password hash. The send password is server hashed and the client\n    /// password hash is hashed by the server for comparison <see cref=\"HashOfClientPasswordHash\"/> in this method.\n    /// </summary>\n    /// <param name=\"sendPasswordHash\">The send password that is hashed by the server.</param>\n    /// <param name=\"clientPasswordHash\">The user provided password hash that has not yet been hashed by the server for comparison.</param>\n    /// <returns>true if hashes match false otherwise</returns>\n    bool PasswordHashMatches(string sendPasswordHash, string clientPasswordHash);\n\n    /// <summary>\n    /// Accepts a client hashed send password and returns a server hashed password.\n    /// </summary>\n    /// <param name=\"clientHashedPassword\"></param>\n    /// <returns>server hashed password</returns>\n    string HashOfClientPasswordHash(string clientHashedPassword);\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Sends/SendPasswordHasher.cs",
    "content": "﻿using Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.KeyManagement.Sends;\n\ninternal class SendPasswordHasher(IPasswordHasher<SendPasswordHasherMarker> passwordHasher) : ISendPasswordHasher\n{\n    private readonly IPasswordHasher<SendPasswordHasherMarker> _passwordHasher = passwordHasher;\n\n    /// <summary>\n    /// <inheritdoc cref=\"ISendPasswordHasher.PasswordHashMatches\"/>\n    /// </summary>\n    public bool PasswordHashMatches(string sendPasswordHash, string inputPasswordHash)\n    {\n        if (string.IsNullOrWhiteSpace(sendPasswordHash) || string.IsNullOrWhiteSpace(inputPasswordHash))\n        {\n            return false;\n        }\n\n        var passwordResult = _passwordHasher.VerifyHashedPassword(SendPasswordHasherMarker.Instance, sendPasswordHash, inputPasswordHash);\n\n        /*\n            In our use-case we input a high-entropy, pre-hashed secret sent by the client. Thus, we don't really care\n            about if the hash needs to be rehashed. Sends also only live for 30 days max.\n        */\n        return passwordResult is PasswordVerificationResult.Success or PasswordVerificationResult.SuccessRehashNeeded;\n    }\n\n    /// <summary>\n    /// <inheritdoc cref=\"ISendPasswordHasher.HashOfClientPasswordHash\"/>\n    /// </summary>\n    public string HashOfClientPasswordHash(string clientHashedPassword)\n    {\n        return _passwordHasher.HashPassword(SendPasswordHasherMarker.Instance, clientHashedPassword);\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Sends/SendPasswordHasherMarker.cs",
    "content": "﻿namespace Bit.Core.KeyManagement.Sends;\n\n// This should not be used except for DI as open generic marker class for use with\n// the SendPasswordHasher.\npublic class SendPasswordHasherMarker\n{\n    // We know we will pass a single instance that isn't used to the PasswordHasher so we\n    // gain an efficiency benefit of not creating multiple marker classes.\n    public static readonly SendPasswordHasherMarker Instance = new();\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Sends/SendPasswordHasherServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Auth.UserFeatures.PasswordValidation;\nusing Bit.Core.KeyManagement.Sends;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\nusing Microsoft.Extensions.Options;\n\npublic static class SendPasswordHasherServiceCollectionExtensions\n{\n    public static void AddSendPasswordServices(this IServiceCollection services)\n    {\n        const string sendPasswordHasherMarkerName = \"SendPasswordHasherMarker\";\n\n        services.AddOptions<PasswordHasherOptions>(sendPasswordHasherMarkerName)\n            .Configure(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);\n\n        services.TryAddScoped<IPasswordHasher<SendPasswordHasherMarker>>(sp =>\n            {\n                var opts = sp\n                    .GetRequiredService<IOptionsMonitor<PasswordHasherOptions>>()\n                    .Get(sendPasswordHasherMarkerName);\n\n                var optionsAccessor = Options.Create(opts);\n\n                return new PasswordHasher<SendPasswordHasherMarker>(optionsAccessor);\n            });\n        services.TryAddScoped<ISendPasswordHasher, SendPasswordHasher>();\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Core.KeyManagement.UserKey;\n\n/// <summary>\n/// Responsible for rotation of a user key and updating database with re-encrypted data\n/// </summary>\npublic interface IRotateUserAccountKeysCommand\n{\n    /// <summary>\n    /// Sets a new user key and updates all encrypted data.\n    /// </summary>\n    /// <param name=\"model\">All necessary information for rotation. If data is not included, this will lead to the change being rejected.</param>\n    /// <returns>An IdentityResult for verification of the master password hash</returns>\n    /// <exception cref=\"ArgumentNullException\">User must be provided.</exception>\n    /// <exception cref=\"InvalidOperationException\">User KDF settings and email must match the model provided settings.</exception>\n    Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model);\n}\n\n/// <summary>\n/// A type used to implement updates to the database for key rotations. Each domain that requires an update of encrypted\n/// data during a key rotation should use this to implement its own database call. The user repository loops through\n/// these during a key rotation.\n/// <para>Note: connection and transaction are only used for Dapper. They won't be available in EF</para>\n/// </summary>\npublic delegate Task UpdateEncryptedDataForKeyRotation(SqlConnection connection = null,\n    SqlTransaction transaction = null);\n"
  },
  {
    "path": "src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountKeysCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.KeyManagement.Utilities;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.KeyManagement.UserKey.Implementations;\n\n/// <inheritdoc />\npublic class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand\n{\n    private readonly IUserService _userService;\n    private readonly IUserRepository _userRepository;\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IFolderRepository _folderRepository;\n    private readonly ISendRepository _sendRepository;\n    private readonly IEmergencyAccessRepository _emergencyAccessRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly IPushNotificationService _pushService;\n    private readonly IdentityErrorDescriber _identityErrorDescriber;\n    private readonly IWebAuthnCredentialRepository _credentialRepository;\n    private readonly IPasswordHasher<User> _passwordHasher;\n    private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;\n    private readonly IFeatureService _featureService;\n\n    /// <summary>\n    /// Instantiates a new <see cref=\"RotateUserAccountKeysCommand\"/>\n    /// </summary>\n    /// <param name=\"userService\">Master password hash validation</param>\n    /// <param name=\"userRepository\">Updates user keys and re-encrypted data if needed</param>\n    /// <param name=\"cipherRepository\">Provides a method to update re-encrypted cipher data</param>\n    /// <param name=\"folderRepository\">Provides a method to update re-encrypted folder data</param>\n    /// <param name=\"sendRepository\">Provides a method to update re-encrypted send data</param>\n    /// <param name=\"emergencyAccessRepository\">Provides a method to update re-encrypted emergency access data</param>\n    /// <param name=\"organizationUserRepository\">Provides a method to update re-encrypted organization user data</param>\n    /// <param name=\"deviceRepository\">Provides a method to update re-encrypted device keys</param>\n    /// <param name=\"passwordHasher\">Hashes the new master password</param>\n    /// <param name=\"pushService\">Logs out user from other devices after successful rotation</param>\n    /// <param name=\"errors\">Provides a password mismatch error if master password hash validation fails</param>\n    /// <param name=\"credentialRepository\">Provides a method to update re-encrypted WebAuthn keys</param>\n    /// <param name=\"userSignatureKeyPairRepository\">Provides a method to update re-encrypted signature keys</param>\n    public RotateUserAccountKeysCommand(IUserService userService, IUserRepository userRepository,\n        ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,\n        IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository,\n        IDeviceRepository deviceRepository,\n        IPasswordHasher<User> passwordHasher,\n        IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository,\n        IUserSignatureKeyPairRepository userSignatureKeyPairRepository,\n        IFeatureService featureService)\n    {\n        _userService = userService;\n        _userRepository = userRepository;\n        _cipherRepository = cipherRepository;\n        _folderRepository = folderRepository;\n        _sendRepository = sendRepository;\n        _emergencyAccessRepository = emergencyAccessRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _deviceRepository = deviceRepository;\n        _pushService = pushService;\n        _identityErrorDescriber = errors;\n        _credentialRepository = credentialRepository;\n        _passwordHasher = passwordHasher;\n        _userSignatureKeyPairRepository = userSignatureKeyPairRepository;\n        _featureService = featureService;\n    }\n\n    /// <inheritdoc />\n    public async Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        if (!await _userService.CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash))\n        {\n            return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());\n        }\n\n        var now = DateTime.UtcNow;\n        user.RevisionDate = user.AccountRevisionDate = now;\n        user.LastKeyRotationDate = now;\n\n        // V2UpgradeToken is only valid for V1 users transitioning to V2.\n        // For V2 users the token is semantically invalid — discard it and perform a full logout.\n        var shouldPersistV2UpgradeToken = model.V2UpgradeToken != null && !IsV2EncryptionUserAsync(user);\n        if (shouldPersistV2UpgradeToken)\n        {\n            user.V2UpgradeToken = model.V2UpgradeToken!.ToJson();\n        }\n        else\n        {\n            user.V2UpgradeToken = null;\n            user.SecurityStamp = Guid.NewGuid().ToString();\n        }\n\n        List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];\n\n        await UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);\n        UpdateUnlockMethods(model, user, saveEncryptedDataActions);\n        UpdateUserData(model, user, saveEncryptedDataActions);\n\n        await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);\n\n        if (shouldPersistV2UpgradeToken)\n        {\n            await _pushService.PushLogOutAsync(user.Id,\n                reason: PushNotificationLogOutReason.KeyRotation);\n        }\n        else\n        {\n            await _pushService.PushLogOutAsync(user.Id);\n        }\n\n        return IdentityResult.Success;\n    }\n\n    public async Task RotateV2AccountKeysAsync(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)\n    {\n        ValidateV2Encryption(model);\n        await ValidateVerifyingKeyUnchangedAsync(model, user);\n\n        saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.UpdateForKeyRotation(user.Id, model.AccountKeys.SignatureKeyPairData));\n        user.SignedPublicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;\n        user.SecurityState = model.AccountKeys.SecurityStateData!.SecurityState;\n        user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion;\n    }\n\n    public void UpgradeV1ToV2Keys(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)\n    {\n        ValidateV2Encryption(model);\n        saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.SetUserSignatureKeyPair(user.Id, model.AccountKeys.SignatureKeyPairData));\n        user.SignedPublicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;\n        user.SecurityState = model.AccountKeys.SecurityStateData!.SecurityState;\n        user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion;\n    }\n\n    public async Task UpdateAccountKeysAsync(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)\n    {\n        ValidatePublicKeyEncryptionKeyPairUnchanged(model, user);\n\n        if (IsV2EncryptionUserAsync(user))\n        {\n            await RotateV2AccountKeysAsync(model, user, saveEncryptedDataActions);\n        }\n        else if (model.AccountKeys.SignatureKeyPairData != null)\n        {\n            UpgradeV1ToV2Keys(model, user, saveEncryptedDataActions);\n        }\n        else\n        {\n            if (EncryptionParsing.GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.AesCbc256_HmacSha256_B64)\n            {\n                throw new InvalidOperationException(\"The provided account private key was not wrapped with AES-256-CBC-HMAC\");\n            }\n            // V1 user to V1 user rotation needs to further changes, the private key was re-encrypted.\n        }\n\n        // Private key is re-wrapped with new user key by client\n        user.PrivateKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey;\n    }\n\n    public void UpdateUserData(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)\n    {\n        // The revision date has to be updated so that de-synced clients don't accidentally post over the re-encrypted data\n        // with an old-user key-encrypted copy\n        var now = DateTime.UtcNow;\n\n        if (model.Ciphers.Any())\n        {\n            var ciphersWithUpdatedDate = model.Ciphers.ToList().Select(c => { c.RevisionDate = now; return c; });\n            saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, ciphersWithUpdatedDate));\n        }\n\n        if (model.Folders.Any())\n        {\n            var foldersWithUpdatedDate = model.Folders.ToList().Select(f => { f.RevisionDate = now; return f; });\n            saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, foldersWithUpdatedDate));\n        }\n\n        if (model.Sends.Any())\n        {\n            var sendsWithUpdatedDate = model.Sends.ToList().Select(s => { s.RevisionDate = now; return s; });\n            saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, sendsWithUpdatedDate));\n        }\n    }\n\n    void UpdateUnlockMethods(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)\n    {\n        if (!model.MasterPasswordUnlockData.ValidateForUser(user))\n        {\n            throw new InvalidOperationException(\"The provided master password unlock data is not valid for this user.\");\n        }\n        // Update master password authentication & unlock\n        user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey;\n        user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);\n        user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint;\n\n        if (model.EmergencyAccesses.Any())\n        {\n            saveEncryptedDataActions.Add(_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));\n        }\n\n        if (model.OrganizationUsers.Any())\n        {\n            saveEncryptedDataActions.Add(_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));\n        }\n\n        if (model.WebAuthnKeys.Any())\n        {\n            saveEncryptedDataActions.Add(_credentialRepository.UpdateKeysForRotationAsync(user.Id, model.WebAuthnKeys));\n        }\n\n        if (model.DeviceKeys.Any())\n        {\n            saveEncryptedDataActions.Add(_deviceRepository.UpdateKeysForRotationAsync(user.Id, model.DeviceKeys));\n        }\n    }\n\n    private bool IsV2EncryptionUserAsync(User user)\n    {\n        // Returns whether the user is a V2 user based on the private key's encryption type.\n        ArgumentNullException.ThrowIfNull(user);\n        var isPrivateKeyEncryptionV2 = EncryptionParsing.GetEncryptionType(user.PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;\n        return isPrivateKeyEncryptionV2;\n    }\n\n    private async Task ValidateVerifyingKeyUnchangedAsync(RotateUserAccountKeysData model, User user)\n    {\n        var currentSignatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id) ?? throw new InvalidOperationException(\"User does not have a signature key pair.\");\n        if (model.AccountKeys.SignatureKeyPairData.VerifyingKey != currentSignatureKeyPair!.VerifyingKey)\n        {\n            throw new InvalidOperationException(\"The provided verifying key does not match the user's current verifying key.\");\n        }\n    }\n\n    private static void ValidatePublicKeyEncryptionKeyPairUnchanged(RotateUserAccountKeysData model, User user)\n    {\n        var publicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey;\n        if (publicKey != user.PublicKey)\n        {\n            throw new InvalidOperationException(\"The provided account public key does not match the user's current public key, and changing the account asymmetric key pair is currently not supported during key rotation.\");\n        }\n    }\n\n    private static void ValidateV2Encryption(RotateUserAccountKeysData model)\n    {\n        if (model.AccountKeys.SignatureKeyPairData == null)\n        {\n            throw new InvalidOperationException(\"Signature key pair data is required for V2 encryption.\");\n        }\n        if (EncryptionParsing.GetEncryptionType(model.AccountKeys.SignatureKeyPairData.WrappedSigningKey) != EncryptionType.XChaCha20Poly1305_B64)\n        {\n            throw new InvalidOperationException(\"The provided signing key data is not wrapped with XChaCha20-Poly1305.\");\n        }\n        if (string.IsNullOrEmpty(model.AccountKeys.SignatureKeyPairData.VerifyingKey))\n        {\n            throw new InvalidOperationException(\"The provided signature key pair data does not contain a valid verifying key.\");\n        }\n\n        if (EncryptionParsing.GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.XChaCha20Poly1305_B64)\n        {\n            throw new InvalidOperationException(\"The provided private key encryption key is not wrapped with XChaCha20-Poly1305.\");\n        }\n        if (string.IsNullOrEmpty(model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey))\n        {\n            throw new InvalidOperationException(\"No signed public key provided, but the user already has a signature key pair.\");\n        }\n        if (model.AccountKeys.SecurityStateData == null || string.IsNullOrEmpty(model.AccountKeys.SecurityStateData.SecurityState))\n        {\n            throw new InvalidOperationException(\"No signed security state provider for V2 user\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/KeyManagement/Utilities/EncryptionParsing.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.KeyManagement.Utilities;\n\npublic static class EncryptionParsing\n{\n    /// <summary>\n    /// Helper method to convert an encryption type string to an enum value.\n    /// </summary>\n    public static EncryptionType GetEncryptionType(string? encString)\n    {\n        ArgumentNullException.ThrowIfNull(encString);\n\n        var parts = encString.Split('.');\n        if (parts.Length == 1)\n        {\n            throw new ArgumentException(\"Invalid encryption type string.\");\n        }\n        if (byte.TryParse(parts[0], out var encryptionTypeNumber))\n        {\n            if (Enum.IsDefined(typeof(EncryptionType), encryptionTypeNumber))\n            {\n                return (EncryptionType)encryptionTypeNumber;\n            }\n        }\n        throw new ArgumentException(\"Invalid encryption type string.\");\n    }\n}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AddedCredit.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            A <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{usd Amount}}</b> payment has been processed and credited to your account. This credit is now available to make purchases.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            You can view your account's available credit by logging into the web vault at {{{link WebVaultUrl}}}.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AddedCredit.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nA {{usd Amount}} payment has been processed and credited to your account. This credit is now available to make purchases.\n\nYou can view your account's available credit by logging into the web vault at {{{WebVaultUrl}}}.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/CannotDeleteClaimedAccount.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            You have requested to delete your account. This action cannot be completed because your account is owned by an organization.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            Please contact your organization administrator for additional details.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/CannotDeleteClaimedAccount.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYou have requested to delete your account. This action cannot be completed because your account is owned by an organization.\n\nPlease contact your organization administrator for additional details.\n\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.html.hbs",
    "content": "{{#>TitleContactUsHtmlLayout}}\n    <table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"display: table; width:100%; padding: 30px; text-align: left;\" align=\"center\">\n        <tr>\n            <td style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 14px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;\">\n                <p style=\"margin: 0 0 24px 0;\">An {{OrganizationName}} admin has claimed the domain @{{{DomainName}}}. Your email address {{{UserEmail}}} matches this, so your&nbsp;Bitwarden account is now managed by {{OrganizationName}}.</p>\n            </td>\n        </tr>\n        <tr>\n            <td style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 14px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;\">\n                <p style=\"margin: 0 0 12px 0;\"><b>What this means for you</b></p>\n                <ul style=\"margin: 0 0 24px 0; padding-left: 20px;\">\n                    <li style=\"margin-bottom: 8px;\">Your day-to-day use of Bitwarden remains the same.</li>\n                    <li style=\"margin-bottom: 8px;\">Only store work-related items in your {{OrganizationName}} vault.</li>\n                    <li style=\"margin-bottom: 8px;\">{{OrganizationName}} admins now manage your account, meaning they can revoke or delete your account.</li>\n                </ul>\n            </td>\n        </tr>\n        <tr>\n            <td style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 14px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;\">\n                <p style=\"margin: 0;\">For more information, please refer to the following help&nbsp;article: <a href=\"https://bitwarden.com/help/claimed-accounts\" style=\"color: #175DDC; font-weight: 700; text-decoration: none;\">Claimed&nbsp;accounts</a></p>\n            </td>\n        </tr>\n    </table>\n{{/TitleContactUsHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/DomainClaimedByOrganization.text.hbs",
    "content": "An {{OrganizationName}} admin has claimed the domain @{{{DomainName}}}. Your email address {{{UserEmail}}} matches this, so your Bitwarden account is now managed by {{OrganizationName}}.\n\nWhat this means for you:\n- Your day-to-day use of Bitwarden remains the same.\n- Only store work-related items in your {{OrganizationName}} vault.\n- {{OrganizationName}} admins now manage your account, meaning they can revoke or delete your account.\n\nFor more information, please refer to the following help article: Claimed accounts (https://bitwarden.com/help/claimed-accounts)\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n    style=\"margin: 0; box-sizing: border-box; \">\n    <tr\n        style=\"margin: 0; box-sizing: border-box; \">\n        <td class=\"content-block last\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            {{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.\n            <br class=\"line-break\" />\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{Url}}\" clicktracking=off target=\"_blank\" rel=\"noopener noreferrer\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Review request\n            </a>\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/NotifyAdminDeviceApprovalRequested.text.hbs",
    "content": "{{#>BasicTextLayout}}\n{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.\n\n{{Url}}\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    <!-- Include shared head styles -->\n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              You can now share passwords with members of <b>{{OrganizationName}}!</b>\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{WebVaultUrl}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>Log in</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/ac-spot-enterprise.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">As a member of <b>{{ OrganizationName }}</b>:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Organization Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-enterprise.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Your account is owned by {{OrganizationName}} and is subject to their security and management policies.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">You can easily access and share passwords with your team.</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/help/sharing\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Share passwords in Bitwarden\n              </a></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationEnterpriseTeamsView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n    You may now access logins and other items {{OrganizationName}} has shared with you from your Bitwarden vault.\n    Tip: Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the App Store or Google Play.\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n.mj-column-px-159 { width:159px !important; max-width: 159px; }\n.mj-column-px-140 { width:140px !important; max-width: 140px; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n.moz-text-html .mj-column-px-159 { width:159px !important; max-width: 159px; }\n.moz-text-html .mj-column-px-140 { width:140px !important; max-width: 140px; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n@media only screen and (max-width: 480px) {\n    .hide-mobile {\n      display: none !important;\n    }\n  }\n    </style>\n    <!-- Include shared head styles -->\n<!-- Include admin console shared styles -->\n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              You can now share passwords with members of <b>{{OrganizationName}}!</b>\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{WebVaultUrl}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>Log in</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/ac-spot-family.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">As a member of <b>{{ OrganizationName }}</b>:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Group Users Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">You can access passwords {{OrganizationName}} has shared with you.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">You can easily share passwords with friends, family, or coworkers.</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/help/sharing\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Share passwords in Bitwarden\n              </a></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Download Mobile Apps Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:32px 10px 0px 25px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:585px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0 0 16px 0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:18px;font-weight:500;line-height:24px;text-align:left;color:#1B2029;\">Download Bitwarden on all devices</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0 0 24px 0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Already using the\n            <a href=\"https://bitwarden.com/download/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">browser extension</a>? Download the Bitwarden mobile app from the\n            <a href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">App Store</a>\n            or\n            <a href=\"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">Google Play</a>\n            to quickly save logins and autofill forms on the go.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0 10px 32px 25px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"width:585px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:159px;\" ><![endif]-->\n                \n      <div class=\"mj-column-px-159 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:27.17948717948718%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"hide-mobile\" style=\"font-size:0px;padding:0 24px 0 0;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:135px;\">\n              \n        <a href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\" target=\"_blank\">\n          \n      <img alt=\"Download on the App Store\" src=\"https://assets.bitwarden.com/email/v1/ac-apple-store.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:40px;width:100%;font-size:16px;\" width=\"135\" height=\"40\">\n    \n        </a>\n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:140px;\" ><![endif]-->\n                \n      <div class=\"mj-column-px-140 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:23.931623931623932%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"hide-mobile\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:140px;\">\n              \n        <a href=\"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden\" target=\"_blank\">\n          \n      <img alt=\"Get it on Google Play\" src=\"https://assets.bitwarden.com/email/v1/ac-google-play.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:40px;width:100%;font-size:16px;\" width=\"140\" height=\"40\">\n    \n        </a>\n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue','Inter',Helvetica,Arial,sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationConfirmation/OrganizationConfirmationFamilyFreeView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n    You may now access logins and other items {{OrganizationName}} has shared with you from your Bitwarden vault.\n    Tip: Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the App Store or Google Play.\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              <b>{{OrganizationName}}</b> invited you to join them on Bitwarden\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-enterprise.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><b>{{OrganizationName}}</b> is rolling out Bitwarden to increase security and protect your sensitive data. Once you accept this invitation, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Store logins securely so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Store logins securely so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your team.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your team.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Policy Warning Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 8px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:18px;font-weight:500;line-height:28px;text-align:left;color:#1B2029;\">Your existing account will be owned by {{OrganizationName}}</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">By accepting this invitation, your account ({{Email}}) will be owned by <b>{{OrganizationName}}</b> and will be subject to their security and management policies. Contact your administrator with any questions or concerns.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsExistingUserView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you accept this invitation, you can:\n\n- Store logins securely so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your team.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nYour existing account will be owned by {{OrganizationName}}\n\nBy accepting this invitation, your account ({{Email}}) will be owned by {{OrganizationName}} and will be subject to their security and management policies. Contact your administrator with any questions or concerns.\n\nAccept invitation: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              <b>{{OrganizationName}}</b> set up a Bitwarden password manager account for you.\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-enterprise.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><b>{{OrganizationName}}</b> is rolling out Bitwarden to increase security and protect your sensitive data. Once you finish account setup, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Store logins securely so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Store logins securely so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your team.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your team.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteEnterpriseTeamsNewUserView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n{{OrganizationName}} is rolling out Bitwarden to increase security and protect your sensitive data. Once you finish account setup, you can:\n\n- Store logins securely so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your team.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nFinish account setup: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              <b>{{OrganizationName}}</b> invited you to join them on Bitwarden\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-family-homes.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><b>{{OrganizationName}}</b> is using Bitwarden to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Store logins securely so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Store logins securely so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your friends, family, or coworkers.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your friends, family, or coworkers.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesExistingUserView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:\n\n- Store logins securely so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your friends, family, or coworkers.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nAccept invitation: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              <b>{{OrganizationName}}</b> set up a Bitwarden password manager account for you.\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-family-homes.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><b>{{OrganizationName}}</b> is using Bitwarden to simplify password sharing and protect your sensitive data. Once you finish account setup, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Store logins securely so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Store logins securely so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your friends, family, or coworkers.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your friends, family, or coworkers.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFamiliesNewUserView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\n{{OrganizationName}} is using Bitwarden to simplify password sharing and protect your sensitive data. Once you finish account setup, you can:\n\n- Store logins securely so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your friends, family, or coworkers.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nFinish account setup: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              You have been invited to Bitwarden Password Manager\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:separate;line-height:100%;\">\n        <tbody>\n          <tr>\n            <td align=\"center\" bgcolor=\"#ffffff\" role=\"presentation\" style=\"border:none;border-radius:20px;cursor:auto;mso-padding-alt:12px 24px;background:#ffffff;\" valign=\"middle\">\n              <a href=\"{{Url}}\" style=\"display:inline-block;background:#ffffff;color:#1A41AC;font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 24px;mso-padding-alt:0px;border-radius:20px;\" target=\"_blank\">\n                <b>{{ButtonText}}</b>\n              </a>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-hero-responsive-img\" style=\"font-size:0px;padding:0px 20px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-family-homes.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 8px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 16px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:15px 15px 0px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">Bitwarden is a password manager used to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Store Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Securely store logins so you never forget your passwords.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Securely store logins so you never forget your passwords.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Sign in to accounts quickly by filling passwords with one click.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Sign in to accounts quickly by filling passwords with one click.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 24px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-ac-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-ac-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:middle;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix mj-bw-ac-icon-row-icon\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px 10px 0px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Share Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:middle;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix mj-bw-ac-icon-row-text-column\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:middle;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:middle;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-ac-icon-row-text\" style=\"font-size:0px;padding:0px 0px 0px 0px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>Share logins easily with your friends, family, or coworkers.</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">Share logins easily with your friends, family, or coworkers.</span></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 10px 12px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:0px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:left;color:#1B2029;\">{{#if InviterEmail}}\n            This invitation was sent by <a href=\"mailto:{{InviterEmail}}\" style=\"color: #175ddc; text-decoration: none;\">{{InviterEmail}}</a> and expires {{ExpirationDate}}\n            {{else}}\n            This invitation expires {{ExpirationDate}}\n            {{/if}}</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:8px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:14px 10px 14px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:420px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:180px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"right\" class=\"mj-bw-ac-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:0px 15px 0px 0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa\n        Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration:none;color:#175ddc; font-weight:400\">bitwarden.com</a> |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration:none; color:#175ddc; font-weight:400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationInvite/OrganizationInviteFreeView.text.hbs",
    "content": "{{#>TitleContactUsTextLayout}}\nBitwarden is a password manager used to simplify password sharing and protect your sensitive data. Once you accept this invitation, you can:\n\n- Securely store logins so you never forget your passwords.\n- Sign in to accounts quickly by filling passwords with one click.\n- Share logins easily with your friends, family, or coworkers.\n\n{{#if InviterEmail}}\nThis invitation was sent by {{InviterEmail}} and expires {{ExpirationDate}}.\n{{else}}\nThis invitation expires {{ExpirationDate}}.\n{{/if}}\n\nAccept invitation: {{Url}}\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\" align=\"left\">\n                Your user account has been revoked from the <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{OrganizationName}}</b> organization because your account is part of multiple organizations. Before you can re-join {{OrganizationName}}, you must first leave all other organizations.\n            </td>\n        </tr>\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\" align=\"left\">\n                To leave an organization, first log into the <a href=\"{{{WebVaultUrl}}}/login\">web app</a>, select the three dot menu next to the organization name, and select Leave.\n            </td>\n        </tr>\n    </table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForSingleOrgPolicy.text.hbs",
    "content": "{{#>BasicTextLayout}}\nYour user account has been revoked from the {{OrganizationName}} organization because your account is part of multiple organizations. Before you can rejoin {{OrganizationName}}, you must first leave all other organizations.\n\nTo leave an organization, first log in the web app ({{{WebVaultUrl}}}/login), select the three dot menu next to the organization name, and select Leave.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\" align=\"left\">\n                Your user account has been revoked from the <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{OrganizationName}}</b> organization because you do not have two-step login configured. Before you can re-join {{OrganizationName}}, you need to set up two-step login on your user account.\n            </td>\n        </tr>\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\" align=\"left\">\n                Learn how to enable two-step login on your user account at\n                <a target=\"_blank\" href=\"https://help.bitwarden.com/article/setup-two-step-login/\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;\">https://help.bitwarden.com/article/setup-two-step-login/</a>\n            </td>\n        </tr>\n    </table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/OrganizationUserRevokedForTwoFactorPolicy.text.hbs",
    "content": "{{#>BasicTextLayout}}\n    Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login\n    configured. Before you can re-join this organization you need to set up two-step login on your user account.\n\n    Learn how to enable two-step login on your user account at\n    https://help.bitwarden.com/article/setup-two-step-login/\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n    style=\"margin: 0; box-sizing: border-box; \">\n    <tr style=\"margin: 0; box-sizing: border-box; \">\n        <td class=\"content-block last\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            {{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; \">\n        <td class=\"content-block last\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            To review requests, log in to your self-hosted instance → navigate to the Admin Console → select Device Approvals\n            <br class=\"line-break\" />\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{Url}}\" clicktracking=off target=\"_blank\" rel=\"noopener noreferrer\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Review request\n            </a>\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminConsole/SelfHostNotifyAdminDeviceApprovalRequested.text.hbs",
    "content": "{{#>BasicTextLayout}}\n{{UserNameRequestingAccess}} has sent a device approval request. Review login requests to allow the member to finish logging in.\n\nTo review requests, log in to your self-hosted instance -> navigate to the Admin Console -> select Device Approvals.\n\n{{Url}}\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminResetPassword.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            The master password for <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{UserName}}</b> has been changed by an administrator in your <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{OrgName}}</b> organization. If you did not initiate this request, please reach out to your administrator immediately.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/AdminResetPassword.text.hbs",
    "content": "{{#>BasicTextLayout}}\nThe master password for {{UserName}} has been changed by an administrator in your {{OrgName}} organization. If you did not initiate this request, please reach out to your administrator immediately.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessAccepted.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            This email is to notify you that {{GranteeEmail}} has accepted your invitation to become an emergency access contact.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            To confirm this user, log into the Bitwarden web vault, go to settings and confirm the user.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you do not wish to confirm this user, you can also remove them on the same page.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessAccepted.text.hbs",
    "content": "{{#>BasicTextLayout}}\nThis email is to notify you that {{GranteeEmail}} has accepted your invitation to become an emergency access contact.\n\nTo confirm this user, log into the Bitwarden web vault, go to settings and confirm the user.\n\nIf you do not wish to confirm this user, you can also remove them on the same page.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessApproved.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            {{Name}} has approved your emergency request. You may now login on the web vault and access their account.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessApproved.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\n{{Name}} has approved your emergency request. You may now login on the web vault and access their account.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessConfirmed.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            This email is to notify you that you have been confirmed as an emergency access contact for <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\"></b>{{Name}}</b>.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            You can now initiate emergency access requests from the web vault.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessConfirmed.text.hbs",
    "content": "{{#>BasicTextLayout}}\nThis email is to notify you that you have been confirmed as an emergency access contact for {{Name}}.\n\nYou can now initiate emergency access requests from the web vault.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessInvited.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            You have been invited to become an emergency contact for <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{Name}}</b>.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you do not wish to become an emergency contact for {{Name}}, you can safely ignore this email.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Become emergency contact\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessInvited.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYou have been invited to become an emergency contact for {{Name}}.\n\nIf you do not wish to become an emergency contact for {{Name}}, you can safely ignore this email.\n\n{{{Url}}}\n\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessRecovery.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            {{Name}} has initiated an emergency request to <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{Action}}</b> your account. You may login on the web vault and manually approve or reject this request.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you do nothing, the request will be automatically approved after <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{DaysLeft}} day(s)</b>.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessRecovery.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\n{{Name}} has initiated an emergency request to {{Action}} your account. You may login on the web vault and manually approve or reject this request.\n\nIf you do nothing, the request will automatically be approved after {{DaysLeft}} day(s).\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessRecoveryReminder.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            {{Name}} has a pending emergency request to <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{Action}}</b> your account. You may login on the web vault and manually approve or reject this request.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you do nothing, the request will be automatically approved after <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{DaysLeft}} day(s)</b>.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessRecoveryReminder.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\n{{Name}} has a pending emergency request to {{Action}} your account. You may login on the web vault and manually approve or reject this request.\n\nIf you do nothing, the request will automatically be approved after {{DaysLeft}} day(s).\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessRecoveryTimedOut.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n                {{Name}}  has been granted emergency request to <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{Action}}</b> your account. You may login on the web vault and manually revoke this request.\n            </td>\n        </tr>\n    </table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessRecoveryTimedOut.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\n{{Name}} has been granted emergency request to {{Action}} your account. You may login on the web vault and manually revoke this request.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessRejected.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            {{Name}} has rejected your emergency request.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/EmergencyAccessRejected.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\n{{Name}} has rejected your emergency request.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 24px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; font-weight: bold;\" valign=\"top\">\n                We've detected a failed login attempt\n            </td>\n        </tr>\n         <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n                <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n                If you're having trouble with two-step login, please visit the <a target=\"_blank\" clicktracking=off href=\"https://help.bitwarden.com\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;\">Help Center</a>.\n            </td>\n        </tr>\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n                <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n               If you did not recently try to log in, open the <a target=\"_blank\" clicktracking=off href=\"{{{WebVaultUrl}}}\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;\">web app</a> and take these immediate steps to secure your Bitwarden account:\n                <ul>\n                    <li style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Deauthorize all devices</li>\n                    <li style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Change your master password</li>\n                </ul>\n                <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            </td>\n        </tr>\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n                <hr />\n                <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n                <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Account:</b> {{AffectedEmail}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n                <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Two-Step Login Method:</b> {{TwoFactorType}} <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n                <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n                <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">IP Address:</b> {{IpAddress}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            </td>\n        </tr>\n       \n    </table>\n{{/FullHtmlLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.text.hbs",
    "content": "{{#>BasicTextLayout}}\nWe've detected a failed login attempt\n\nIf you're having trouble with two-step login, please visit the Help Center (https://bitwarden.com/help/).\n\nIf you did not recently try to log in, open the web app ({{{WebVaultUrl}}}) and take these immediate steps to secure your Bitwarden account:\n- Deauthorize all devices\n- Change your master password\n\nAccount: {{AffectedEmail}}\nTwo-Step Login Method: {{TwoFactorType}} \nDate: {{TheDate}} at {{TheTime}} {{TimeZone}}\nIP Address: {{IpAddress}}\n\n{{/BasicTextLayout}}\n\n\n\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/MasterPasswordHint.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            You (or someone) recently requested your master password hint.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Your hint is: \"{{Hint}}\"<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            Log in: {{{link WebVaultUrl}}}\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you still cannot remember your master password, please refer to the following article for your options:<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            {{{link 'https://bitwarden.com/help/article/forgot-master-password/'}}}\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you did not request your master password hint you can safely ignore this email.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/MasterPasswordHint.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYou (or someone) recently requested your master password hint.\n\nYour hint is: \"{{Hint}}\"\nLog in: {{{WebVaultUrl}}}\n\nIf you still cannot remember your master password, please refer to the following article for your options:\nhttps://bitwarden.com/help/article/forgot-master-password/\n\nIf you did not request your master password hint you can safely ignore this email.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/NoMasterPasswordHint.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            You (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. If you cannot remember your master password, please refer to the following article for your options:<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            {{{link 'https://bitwarden.com/help/article/forgot-master-password/'}}}\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you did not request your master password hint you can safely ignore this email.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/NoMasterPasswordHint.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYou (or someone) recently requested your master password hint. Unfortunately, your account does not have a master password hint. If you cannot remember your master password, please refer to the following article for your options:\nhttps://bitwarden.com/help/article/forgot-master-password/\n\nIf you did not request your master password hint you can safely ignore this email.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/OTPEmail.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Your email verification code is: <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{Token}}</b>\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Use this code to complete the protected action in Bitwarden.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/OTPEmail.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYour email verification code is: {{Token}}\n\nUse this code to complete the protected action in Bitwarden.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/PasswordlessSignIn.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            Click the following link to log in:\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you did not request to log in, you can safely ignore this email.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Log In Now\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/PasswordlessSignIn.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nClick the following link to log in:\n\nIf you did not request to log in, you can safely ignore this email.\n\n{{{Url}}}\n\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/RecoverTwoFactor.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Two-step login on your Bitwarden account has been disabled by using the account's recovery code.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">IP Address:</b> {{IpAddress}}\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you did not perform this action, you should immediately take steps to secure your account.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/RecoverTwoFactor.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nTwo-step login on your Bitwarden account has been disabled by using\nthe account's recovery code.\n\nDate: {{TheDate}} at {{TheTime}} {{TimeZone}}\nIP Address: {{IpAddress}}\n\nIf you did not perform this action, you should immediately take steps\nto secure your account.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/RegistrationVerifyEmail.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            Verify your email address below to finish creating your account.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you did not request this email from Bitwarden, you can safely ignore it.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Verify email\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/RegistrationVerifyEmail.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nVerify your email address below to finish creating your account.\n\nIf you did not request this email from Bitwarden, you can safely ignore it.\n\n{{{Url}}}\n\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-90 { width:90% !important; max-width: 90%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-90 { width:90% !important; max-width: 90%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n.send-bubble {\n        padding-left: 20px;\n        padding-right: 20px;\n        width: 90% !important;\n      }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: normal; font-size: 24px; line-height: 32px\">\n              Verify your email to access this Bitwarden Send\n            </h1></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-hero-responsive-img\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-secure-send-round.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n        <tbody>\n          <tr>\n            <td style=\"vertical-align:top;padding:0px;\">\n              \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\">Your verification code is:</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:32px;font-weight:500;line-height:1;text-align:left;color:#1B2029;\">{{ Token }}</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td style=\"font-size:0px;word-break:break-word;\">\n                  \n      <div style=\"height:20px;line-height:20px;\">&#8202;</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#1B2029;\">This code expires in {{ Expiry }} minutes. After that, you'll need\n            to verify your email again.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 0px 20px 0px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"send-bubble-outlook\" style=\"vertical-align:top;width:558px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-90 mj-outlook-group-fix send-bubble\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" width=\"100%\">\n        <tbody>\n          <tr>\n            <td style=\"vertical-align:top;padding:0px;\">\n              \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background-color:#DBE5F6;border-radius:16px;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><p>\n              Bitwarden Send securely shares sensitive information. Learn more\n              about\n              <a href=\"https://bitwarden.com/help/send\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">Bitwarden Send</a>\n              or\n              <a href=\"https://bitwarden.com/signup\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">sign up</a>\n              to try it today.\n            </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin-top: 0px;\">\n              Learn more about Bitwarden\n            </p>\n            Find user guides, product documentation, and videos on the\n            <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nVerify your email to access this Bitwarden Send.\n\nYour verification code is: {{Token}}\n\nThis code can only be used once and expires in {{Expiry}} minutes. After that you'll need to verify your email again.\n\nBitwarden Send transmits sensitive, temporary information to others easily and securely. Learn more about Bitwarden Send or sign up to try it today.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/TrustedDeviceAdminApproval.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            You must log in on the device below within 12 hours or approval will expire.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Device:</b> {{DeviceType}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">IP Address:</b> {{IpAddress}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you do not recognize this device, contact your organization administrator.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/TrustedDeviceAdminApproval.text.hbs",
    "content": "{{#>BasicTextLayout}}\nYou must log in on the device below within 12 hours or approval will expire.\n\nDevice Type: {{DeviceType}}\nIP Address: {{IpAddress}}\nDate: {{TheDate}} at {{TheTime}} {{TimeZone}}\n\nIf you do not recognize this device, contact your organization administrator.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n  <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n      To finish {{EmailTotpAction}}, enter this verification code: <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{Token}}</b>\n    </td>\n  </tr>\n  <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n      <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n      If this was not you, take these immediate steps to secure your account in the <a target=\"_blank\" clicktracking=\"off\" href=\"{{{WebVaultUrl}}}\" style=\"-webkit-font-smoothing: antialiased;-webkit-text-size-adjust: none;box-sizing: border-box;color: #175ddc;font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size: 16px;line-height: 25px;margin: 0;text-decoration: none;\">web app</a>:\n      <ul>\n        <li>Deauthorize unrecognized devices</li>\n        <li>Change your master password</li>\n        {{#if DisplayTwoFactorReminder}}\n          <li style=\"margin-bottom: 5px;\">Turn on two-step login</li>\n        {{/if}}\n      </ul>\n    </td>\n  </tr>\n  <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <td class=\"content-block last\"  style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n      <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n      <hr />\n      <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n      <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Account:</b>\n      {{AccountEmail}}\n      <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n      <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Date:</b>\n      {{TheDate}} at {{TheTime}} {{TimeZone}}\n      <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n      <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">IP:</b>\n      {{DeviceIp}}\n      <br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n      <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">DeviceType:</b>\n      {{DeviceType}}\n    </td>\n  </tr>\n</table>\n{{/FullHtmlLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nTo finish {{EmailTotpAction}}, enter this verification code: {{Token}}\n\nIf this was not you, take these immediate steps to secure your account in the web app:\n\nDeauthorize unrecognized devices\n\nChange your master password\n\nTurn on two-step login\n\nAccount : {{AccountEmail}}\nDate : {{TheDate}} at {{TheTime}} {{TimeZone}}\nIP : {{DeviceIp}}\nDevice Type : {{DeviceType}}\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/VerifyDelete.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            Click the link below to delete your Bitwarden account.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you did not request this email to delete your Bitwarden account, you can safely ignore it.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Delete Your Account\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/VerifyDelete.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nClick the link below to delete your Bitwarden account ({{Email}}).\n\nIf you did not request this email to delete your Bitwarden account, you can safely ignore it.\n\n{{{Url}}}\n\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/VerifyEmail.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            Verify this email address for your Bitwarden account by clicking the link below.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you did not request to verify a Bitwarden account, you can safely ignore this email.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Verify Email Address Now\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Auth/VerifyEmail.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nVerify this email address for your Bitwarden account by clicking the link below.\n\nIf you did not request to verify a Bitwarden account, you can safely ignore this email.\n\n{{{Url}}}\n\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Billing/BusinessUnitConversionInvite.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n                You have been invited to set up a new Business Unit Portal within Bitwarden.\n                <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n                <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            </td>\n        </tr>\n        <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n                <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                    Set Up Business Unit Portal Now\n                </a>\n                <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            </td>\n        </tr>\n    </table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Billing/BusinessUnitConversionInvite.text.hbs",
    "content": "{{#>BasicTextLayout}}\n    You have been invited to set up a new Business Unit Portal within Bitwarden. To continue, click the following link:\n\n    {{{Url}}}\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            {{VerifyYourEmailHTMLCopy}}\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you did not request this email from Bitwarden, you can safely ignore it.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Verify email\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\n{{VerifyYourEmailTextCopy}}\n\nIf you did not request this email from Bitwarden, you can safely ignore it.\n\n{{{Url}}}\n\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/ChangeEmail.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            To finalize changing your Bitwarden email address enter the following code in web vault: <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{Token}}</b>\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you did not try to change an email address, you can safely ignore this email.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/ChangeEmail.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nTo finalize changing your Bitwarden email address enter the following code in the web vault: {{Token}}\n\nIf you did not try to change an email address, you can safely ignore this email.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/ChangeEmailAlreadyExists.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            A user ({{FromEmail}}) recently tried to change their account to use this email address ({{ToEmail}}). An account already exists with this email ({{ToEmail}}).\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you did not try to change an email address, you can safely ignore this email.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/ChangeEmailAlreadyExists.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nA user ({{FromEmail}}) recently tried to change their account to use this email address ({{ToEmail}}). An account already exists with this email ({{ToEmail}}).\n\nIf you did not try to change an email address, you can safely ignore this email.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you do not recognize this account, please ignore this message.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Accept Offer\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.text.hbs",
    "content": "{{#>BasicTextLayout}}\nA Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below.\n\nIf you do not recognize this account, please ignore this message.\n\n{{Url}}\n\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box;line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you do not recognize this account, please ignore this message.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Create Account\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.text.hbs",
    "content": "{{#>BasicTextLayout}}\nA Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address. Click the link below.\n\nIf you do not recognize this account, please ignore this message.\n\n{{Url}}\n\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Your Families subscription has been successfully redeemed.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.text.hbs",
    "content": "{{#>BasicTextLayout}}\nYour Families subscription has been successfully redeemed.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Your Free Families subscription has been successfully accepted. Your subscription is free as long as the sponsoring member continues to have a qualifying Bitwarden subscription.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.text.hbs",
    "content": "{{#>BasicTextLayout}}\nYour Families subscription has been successfully activated. Your subscription is free as long as the sponsoring member continues to have a qualifying Bitwarden subscription.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRemovedFromFamilyUser.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n        <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n                {{SponsoringOrgName}} has removed the Free Bitwarden Families plan sponsorship.\n            </td>\n        </tr>\n        <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n                <strong>Here’s what that means:</strong></br>\n                Your Free Bitwarden Families sponsorship will charge your stored payment method on {{OfferAcceptanceDate}}. To avoid any disruption in your service, please ensure your payment method on the <a target=\"_blank\" clicktracking=off href=\"{{SubscriptionUrl}}\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;\">Subscription page</a> is up to date.\n            </td>\n        </tr>\n        <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n                Contact your organization administrators for more information.\n                <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n                <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            </td>\n        </tr>\n    </table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRemovedFromFamilyUser.text.hbs",
    "content": "{{#>BasicTextLayout}}\n    {{SponsoringOrgName}} has removed the Free Bitwarden Families plan sponsorship.\n    Here’s what that means:\n    Your Free Bitwarden Families sponsorship will charge your stored payment method on {{OfferAcceptanceDate}}. To avoid any disruption in your service, please ensure your payment method on the Subscription page is up to date. Or click the following link: {{{SubscriptionUrl}}}\n    Contact your organization administrators for more information.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; line-height: 25px; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Your Families subscription will remain sponsored until {{ExpirationDate}}. To continue your plan, make sure you have a current payment method for the subscription. Review or update your payment method under Settings in your Families Organization.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.text.hbs",
    "content": "{{#>BasicTextLayout}}\nYour Families subscription will remain sponsored until {{Date}}. To continue your plan, make sure you have a current payment method for the subscription. Review or update your payment method under Settings in your Families Organization.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            We recently received your request to permanently delete the following Bitwarden organization:\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Name:</b> {{OrganizationName}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">ID:</b> {{OrganizationId}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Created:</b> {{OrganizationCreationDate}} at {{OrganizationCreationTime}} {{TimeZone}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Plan:</b> {{OrganizationPlan}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Number of seats:</b> {{OrganizationSeats}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Billing email address:</b> {{OrganizationBillingEmail}}\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            Click the link below to delete your Bitwarden organization.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you did not request this email to delete your Bitwarden organization, please contact us.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Delete Your Organization\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.text.hbs",
    "content": "{{#>BasicTextLayout}}\nWe recently received your request to permanently delete the following Bitwarden organization:\n\n- Name: {{OrganizationName}}\n- ID: {{OrganizationId}}\n- Created: {{OrganizationCreationDate}} at {{OrganizationCreationTime}} {{TimeZone}}\n- Plan: {{OrganizationPlan}}\n- Number of seats: {{OrganizationSeats}}\n- Billing email address: {{OrganizationBillingEmail}}\n\nClick the link below to complete the deletion of your organization.\n\nIf you did not request this email to delete your Bitwarden organization, please contact us.\n\n{{{Url}}}\n\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/InvoiceUpcoming.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            This is a reminder that your Bitwarden subscription is due for renewal soon. Your payment method on file will be charged for <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{usd AmountDue}}</b> on <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{date DueDate 'MMM dd, yyyy'}}</b>.\n        </td>\n    </tr>\n    {{#if Items}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\"><u style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Summary Of Charges</u></b><br />\n            {{#each Items}}\n            {{this}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            {{/each}}\n        </td>\n    </tr>\n    {{/if}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            To avoid any interruption in service, please ensure that your payment method on file is up to date and can be charged for the above amount. You can manage your subscription, payment method, and invoices by logging into the web vault.\n            <a href=\"{{{UpdateBillingInfoUrl}}}\" style=\"text-decoration: none\">\n                <img src=\"https://assets.bitwarden.com/email/v1/BillingLearnMore.png\" alt=\"update-billing-info\" width=\"20\" height=\"20\" style=\"display: inline-block;vertical-align: top;\">\n            </a>\n        </td>\n    </tr>\n    {{#if MentionInvoices}}\n    {{/if}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you have any questions or problems, please feel free to email us at hello@bitwarden.com.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/InvoiceUpcoming.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nThis is a reminder that your Bitwarden subscription is due for renewal soon. Your payment method on file will be charged for {{usd AmountDue}} on {{date DueDate 'MMM dd, yyyy'}}.\n{{#if Items}}\n\nSummary Of Charges\n------------------\n{{#each Items}}\n{{this}}\n{{/each}}\n{{/if}}\n\nTo avoid any interruption in service, please ensure that your payment method on file is up to date and can be charged for the above amount. You can manage your subscription, payment method, and invoices by logging into the web vault. For more information, please visit {{{UpdateBillingInfoUrl}}}.\n{{#if MentionInvoices}}\n{{/if}}\n\nIf you have any questions or problems, please feel free to email us at hello@bitwarden.com.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/Basic.html.hbs",
    "content": "﻿<!DOCTYPE html>\n<html>\n<head>\n    <meta name=\"viewport\" content=\"width=device-width\" />\n    <title></title>\n</head>\n<body>\n    {{>@partial-block}}\n</body>\n</html>\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/Basic.text.hbs",
    "content": "﻿{{>@partial-block}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/Full.html.hbs",
    "content": "﻿<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width\" />\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n    <title>Bitwarden</title>\n</head>\n\n<body style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important; margin: 0;\" bgcolor=\"#f6f6f6\">\n    <style type=\"text/css\">\n        ﻿ body {\n            margin: 0;\n            font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n            box-sizing: border-box;\n            font-size: 16px;\n            color: #333;\n            line-height: 25px;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n        }\n\n        body * {\n            margin: 0;\n            font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n            box-sizing: border-box;\n            font-size: 16px;\n            color: #333;\n            line-height: 25px;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n        }\n\n        img {\n            max-width: 100%;\n            border: none;\n        }\n\n        body {\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n            width: 100% !important;\n            height: 100%;\n            line-height: 25px;\n        }\n\n        body {\n            background-color: #f6f6f6;\n        }\n\n        @media only screen and (max-width: 600px) {\n            body {\n                padding: 0 !important;\n            }\n\n            .container {\n                padding: 0 !important;\n                width: 100% !important;\n            }\n\n            .container-table {\n                padding: 0 !important;\n                width: 100% !important;\n            }\n\n            .content {\n                padding: 0 0 10px 0 !important;\n            }\n\n            .content-wrap {\n                padding: 10px !important;\n            }\n\n            .invoice {\n                width: 100% !important;\n            }\n\n            .main {\n                border-right: none !important;\n                border-left: none !important;\n                border-radius: 0 !important;\n            }\n\n            .logo {\n                padding-top: 10px !important;\n            }\n\n            .footer {\n                margin-top: 10px !important;\n            }\n\n            .indented {\n                padding-left: 10px;\n            }\n        }\n\n        @media only screen and (min-width: 600px) {\n            {{! Fix for Apple Mail }}\n            .content-table {\n                width: 600px !important;\n            }\n        }\n\n        /* Component styling - these are explicitly applied via classes so that they can be\n            gradually introduced as we update templates.*/\n        a.inline-link {\n            font-weight: bold;\n            color: #175DDC;\n            text-decoration: none;\n        }\n\n        br.line-break {\n            margin: 0;\n            box-sizing: border-box;\n            color: #333;\n            line-height: 25px;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n        }\n\n    </style>\n    {{! Yahoo center fix }}\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" bgcolor=\"#f6f6f6\">\n        <tr>\n            <td class=\"container\" width=\"100%\" align=\"center\">\n                {{! 600px container }}\n                <table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" class=\"content-table\">\n                    <tr>\n                        <td></td> {{! Left column (center fix) }}\n                        <td class=\"content\" align=\"center\" valign=\"top\" width=\"600\" style=\"padding-bottom: 20px;\">\n                            <table class=\"header\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n                                <tr>\n                                    <td valign=\"middle\" class=\"aligncenter middle logo\" style=\"padding: 20px 0 10px;\" align=\"center\">\n                                        <img src=\"https://assets.bitwarden.com/email/v1/logo-horizontal-blue.png\" alt=\"\" width=\"250\" height=\"39\" />\n                                    </td>\n                                </tr>\n                            </table>\n                            <table class=\"main\" cellpadding=\"0\" cellspacing=\"0\" style=\"border: 1px solid #e9e9e9; border-radius: 3px;\" bgcolor=\"white\">\n                                <tr>\n                                    <td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;\" valign=\"top\">\n\n                                        {{>@partial-block}}\n\n                                    </td>\n                                </tr>\n                            </table>\n                            <table class=\"footer\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"margin: 0; width: 100%;\">\n                                <tr>\n                                    <td class=\"aligncenter social-icons\" align=\"center\" style=\"margin: 0; padding: 15px 0 0 0;\" valign=\"top\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0 auto;\">\n                                            <tr>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://x.com/bitwarden\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-x.png\" alt=\"X\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-reddit.png\" alt=\"Reddit\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://community.bitwarden.com/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-discourse.png\" alt=\"CommunityForums\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://github.com/bitwarden\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-github.png\" alt=\"GitHub\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-youtube.png\" alt=\"Youtube\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-linkedin.png\" alt=\"LinkedIn\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-facebook.png\" alt=\"Facebook\" width=\"30\" height=\"30\" /></a></td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #666666; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 15px 0 0 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\">\n                                        &copy; {{CurrentYear}} Bitwarden Inc.\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                        <td></td> {{! Right column (center fix) }}\n                    </tr>\n                </table>\n            </td>\n        </tr>\n    </table>\n</body>\n</html>"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/Full.text.hbs",
    "content": "﻿{{>@partial-block}}\n\n----------------------------\n\n- X: https://x.com/bitwarden\n- Reddit: https://www.reddit.com/r/Bitwarden/\n- Community Forums: https://community.bitwarden.com/\n- GitHub: https://github.com/bitwarden\n- Youtube: https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\n- LinkedIn: https://www.linkedin.com/company/bitwarden1/\n- Facebook: https://www.facebook.com/bitwarden/\n\n{{CurrentYear}} Bitwarden Inc."
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.html.hbs",
    "content": "﻿<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width\" />\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n    <title>Bitwarden</title>\n    <style type=\"text/css\">\n        ﻿ body {\n            margin: 0;\n            font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n            box-sizing: border-box;\n            font-size: 16px;\n            color: #333;\n            line-height: 25px;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n        }\n\n        body * {\n            margin: 0;\n            font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n            box-sizing: border-box;\n            font-size: 16px;\n            color: #333;\n            line-height: 25px;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n        }\n\n        img {\n            max-width: 100%;\n            border: none;\n        }\n\n        body {\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n            width: 100% !important;\n            height: 100%;\n            line-height: 25px;\n        }\n\n        body {\n            background-color: #f6f6f6;\n        }\n\n        .white-title {\n            font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n            box-sizing: border-box;\n            font-size: 24px;\n            line-height: 32px;\n            font-weight: 700;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n            color: #ffffff;\n        }\n\n        @media only screen and (max-width: 600px) {\n            body {\n                padding: 0 !important;\n            }\n\n            .container {\n                padding: 0 !important;\n                width: 100% !important;\n            }\n\n            .container-table {\n                padding: 0 !important;\n                width: 100% !important;\n            }\n\n            .content {\n                padding: 0 0 10px 0 !important;\n            }\n\n            .content-wrap {\n                padding: 0px !important;\n            }\n\n            .invoice {\n                width: 100% !important;\n            }\n\n            .main {\n                border-right: none !important;\n                border-left: none !important;\n                border-radius: 0 !important;\n            }\n\n            .logo {\n                padding-top: 10px !important;\n            }\n\n            .footer {\n                margin-top: 10px !important;\n            }\n\n            .indented {\n                padding-left: 10px;\n            }\n\n            .footer-image {\n                display: none !important;\n\t\t\t\tmso-hide:all !important;\n            }\n\n            .footer-text {\n                width: 100% !important;\n            }\n\n            .center {\n                text-align: center !important;\n            }\n\n            .templateColumnContainer{\n                display:block !important;\n                width:100% !important;\n                padding-left: 20px !important;\n                padding-right: 20px !important;\n                padding-bottom: 0px !important;\n            }\n        }\n\n        @media only screen and (min-width: 600px) {\n            {{! Fix for Apple Mail }}\n            .content-table {\n                width: 600px !important;\n            }\n        }\n\n        /* Component styling - these are explicitly applied via classes so that they can be\n        gradually introduced as we update templates.*/\n        a.inline-link {\n            font-weight: bold;\n            color: #175DDC;\n            text-decoration: none;\n        }\n\n        br.line-break {\n            margin: 0;\n            box-sizing: border-box;\n            color: #333;\n            line-height: 25px;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n        }\n\n    </style>\n</head>\n<body style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important; margin: 0;\" bgcolor=\"#f6f6f6\">\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" bgcolor=\"#f6f6f6\">\n        <tr>\n            <td class=\"container\" width=\"100%\" align=\"center\">\n                <table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" class=\"content-table\">\n                    <tr>\n                        <td></td>\n                        <td class=\"content\" align=\"center\" valign=\"top\" width=\"600\" style=\"padding-bottom: 20px;\">\n                            <table class=\"header\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n                                <tr>\n                                    <td valign=\"middle\" class=\"aligncenter middle logo\" style=\"padding: 20px 0 10px;\" align=\"center\">\n                                        <img src=\"https://assets.bitwarden.com/email/v1/logo-horizontal-blue.png\" alt=\"\" width=\"250\" height=\"39\" />\n                                    </td>\n                                </tr>\n                            </table>\n                            <table class=\"main\" cellpadding=\"0\" cellspacing=\"0\" style=\"border: 1px solid #e9e9e9; border-radius: 3px;\" bgcolor=\"white\">\n                                <tr>\n                                    <td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0px; -webkit-text-size-adjust: none;\" valign=\"top\">\n\n                                        {{>@partial-block}}\n\n                                    </td>\n                                </tr>\n                            </table>\n                            <table class=\"footer\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"margin: 0; width: 100%;\">\n                                <tr>\n                                    <td class=\"aligncenter social-icons\" align=\"center\" style=\"margin: 0; padding: 15px 0 0 0;\" valign=\"top\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0 auto;\">\n                                            <tr>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://x.com/bitwarden\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-x.png\" alt=\"X\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-reddit.png\" alt=\"Reddit\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://community.bitwarden.com/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-discourse.png\" alt=\"CommunityForums\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://github.com/bitwarden\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-github.png\" alt=\"GitHub\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-youtube.png\" alt=\"Youtube\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-linkedin.png\" alt=\"LinkedIn\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-facebook.png\" alt=\"Facebook\" width=\"30\" height=\"30\" /></a></td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #666666; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 15px 0 0 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\">\n                                        &copy; {{CurrentYear}} Bitwarden Inc.\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                        <td></td>\n                    </tr>\n                </table>\n            </td>\n        </tr>\n    </table>\n</body>\n</html>"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/FullUpdated.text.hbs",
    "content": "﻿{{>@partial-block}}\n\n----------------------------\n\n- X: https://x.com/bitwarden\n- Reddit: https://www.reddit.com/r/Bitwarden/\n- Community Forums: https://community.bitwarden.com/\n- GitHub: https://github.com/bitwarden\n- Youtube: https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\n- LinkedIn: https://www.linkedin.com/company/bitwarden1/\n- Facebook: https://www.facebook.com/bitwarden/\n\n{{CurrentYear}} Bitwarden Inc."
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/ProviderFull.html.hbs",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width\" />\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n    <title>Bitwarden</title>\n</head>\n\n<body style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important; margin: 0;\" bgcolor=\"#f6f6f6\">\n    <style type=\"text/css\">\n        ﻿ body {\n            margin: 0;\n            font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n            box-sizing: border-box;\n            font-size: 16px;\n            color: #333;\n            line-height: 25px;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n        }\n\n        body * {\n            margin: 0;\n            font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n            box-sizing: border-box;\n            font-size: 16px;\n            color: #333;\n            line-height: 25px;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n        }\n\n        img {\n            max-width: 100%;\n            border: none;\n        }\n\n        body {\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n            width: 100% !important;\n            height: 100%;\n            line-height: 25px;\n        }\n\n        body {\n            background-color: #f6f6f6;\n        }\n\n        /* Provider-specific styles */\n        .provider-header {\n            background-color: #175DDC;\n            height: 84px;\n            border-top-left-radius: 4px;\n            border-top-right-radius: 4px;\n        }\n\n        .provider-content {\n            border-left: 1px solid #e9e9e9;\n            border-right: 1px solid #e9e9e9;\n            border-bottom: 1px solid #e9e9e9;\n            border-bottom-left-radius: 3px;\n            border-bottom-right-radius: 3px;\n        }\n\n        @media only screen and (max-width: 600px) {\n            body {\n                padding: 0 !important;\n            }\n\n            .container {\n                padding: 0 !important;\n                width: 100% !important;\n            }\n\n            .container-table {\n                padding: 0 !important;\n                width: 100% !important;\n            }\n\n            .content {\n                padding: 0 0 10px 0 !important;\n            }\n\n            .content-wrap {\n                padding: 10px !important;\n            }\n\n            .invoice {\n                width: 100% !important;\n            }\n\n            .main {\n                border-right: none !important;\n                border-left: none !important;\n                border-radius: 0 !important;\n            }\n\n            .provider-header {\n                border-radius: 0 !important;\n            }\n\n            .provider-content {\n                border-left: none !important;\n                border-right: none !important;\n                border-radius: 0 !important;\n            }\n\n            .logo {\n                padding-top: 10px !important;\n            }\n\n            .footer {\n                margin-top: 10px !important;\n            }\n\n            .indented {\n                padding-left: 10px;\n            }\n        }\n\n        @media only screen and (min-width: 600px) {\n            {{! Fix for Apple Mail }}\n            .content-table {\n                width: 600px !important;\n            }\n        }\n\n        /* Component styling - these are explicitly applied via classes so that they can be\n            gradually introduced as we update templates.*/\n        a.inline-link {\n            font-weight: bold;\n            color: #175DDC;\n            text-decoration: none;\n        }\n\n        br.line-break {\n            margin: 0;\n            box-sizing: border-box;\n            color: #333;\n            line-height: 25px;\n            -webkit-font-smoothing: antialiased;\n            -webkit-text-size-adjust: none;\n        }\n\n    </style>\n    {{! Yahoo center fix }}\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" bgcolor=\"#f6f6f6\">\n        <tr>\n            <td class=\"container\" width=\"100%\" align=\"center\">\n                {{! 600px container }}\n                <table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" class=\"content-table\">\n                    <tr>\n                        <td></td> {{! Left column (center fix) }}\n                        <td class=\"content\" align=\"center\" valign=\"top\" width=\"660\" style=\"padding-bottom: 20px;\">\n                            <!-- Blue Header with Logo -->\n                            <table class=\"provider-header\" cellpadding=\"0\" cellspacing=\"0\" width=\"660\" bgcolor=\"#175DDC\" style=\"background-color: #175DDC; width: 660px; height: 84px; opacity: 1; border-top-left-radius: 4px; border-top-right-radius: 4px;\">\n                                <tr>\n                                    <td valign=\"top\" style=\"height: 20.53px; width: 417px; padding-left: 32px; padding-top: 32px;\">\n                                        <img src=\"https://assets.bitwarden.com/email/v1/logo-horizontal-white.png\" alt=\"Bitwarden\" style=\"display: block; opacity: 1; width: auto; height: 28px; max-width: 417px;\" />\n                                    </td>\n                                </tr>\n                            </table>\n                            \n                            <!-- Main Content Container -->\n                            <table class=\"main provider-content\" cellpadding=\"0\" cellspacing=\"0\" width=\"660\" style=\"width: 660px; border-left: 1px solid #e9e9e9; border-right: 1px solid #e9e9e9; border-bottom: 1px solid #e9e9e9; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px;\" bgcolor=\"white\">\n                                <tr>\n                                    <td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;\" valign=\"top\">\n\n                                        {{>@partial-block}}\n\n                                    </td>\n                                </tr>\n                            </table>\n                            <table class=\"footer\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"margin: 0; width: 100%;\">\n                                <tr>\n                                    <td class=\"aligncenter social-icons\" align=\"center\" style=\"margin: 0; padding: 15px 0 0 0;\" valign=\"top\">\n                                        <table cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0 auto;\">\n                                            <tr>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://x.com/bitwarden\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-x.png\" alt=\"X\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-reddit.png\" alt=\"Reddit\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://community.bitwarden.com/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-discourse.png\" alt=\"CommunityForums\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://github.com/bitwarden\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-github.png\" alt=\"GitHub\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-youtube.png\" alt=\"Youtube\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-linkedin.png\" alt=\"LinkedIn\" width=\"30\" height=\"30\" /></a></td>\n                                                <td style=\"margin: 0; padding: 0 10px;\" valign=\"top\"><a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\"><img src=\"https://assets.bitwarden.com/email/v1/mail-facebook.png\" alt=\"Facebook\" width=\"30\" height=\"30\" /></a></td>\n                                            </tr>\n                                        </table>\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"content-block\" style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 12px; font-weight: 400; color: #666666; line-height: 16px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 15px 0 0 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\">\n                                        &copy; {{CurrentYear}} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa Barbara, CA, USA\n                                    </td>\n                                </tr>\n                                <tr>\n                                    <td class=\"content-block\" style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 12px; font-weight: 400; color: #999999; line-height: 16px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 10px 0 0 0; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\">\n                                        Always confirm you are on an official Bitwarden domain before logging in:<br/>\n                                        <a href=\"#\" style=\"color: #175DDC; text-decoration: none; font-weight: 700;\">bitwarden.com</a> | <a href=\"#\" style=\"color: #175DDC; text-decoration: none; font-weight: 700;\">Learn why we include this</a>\n                                    </td>\n                                </tr>\n                            </table>\n                        </td>\n                        <td></td> {{! Right column (center fix) }}\n                    </tr>\n                </table>\n            </td>\n        </tr>\n    </table>\n</body>\n</html>"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.html.hbs",
    "content": "﻿{{#>FullUpdatedHtmlLayout}}\n<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"\n  style=\"background-color: #175DDC;padding-top:25px;padding-bottom:15px;\">\n  <tr>\n    <td align=\"center\" valign=\"top\" width=\"70%\" class=\"templateColumnContainer\">\n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\"\n        style=\"padding-left:30px; padding-right: 5px; padding-top: 20px;\">\n        <tr>\n          <td style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 24px; color: #ffffff; line-height: 32px; font-weight: 500; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            {{OrgName}} has identified {{TaskCount}} critical {{plurality TaskCount \"login\" \"logins\"}} that {{plurality TaskCount \"requires\" \"require\"}} a password change\n          </td>\n        </tr>\n      </table>\n    </td>\n    <td align=\"right\" valign=\"bottom\" class=\"templateColumnContainer\" style=\"padding-right: 15px;\">\n      <img width=\"140\" height=\"140\" align=\"right\" valign=\"bottom\"\n        style=\"width: 140px; height:140px; font-size: 0; vertical-align: bottom; text-align: right;\" alt=''\n        src='https://assets.bitwarden.com/email/v1/business-warning.png' />\n    </td>\n  </tr>\n</table>\n\n{{>@partial-block}}\n\n<table width=\"100%\" style=\"display:table; background-color: #FBFBFB; vertical-align: middle; padding:30px\" border=\"0\"\n  cellpadding=\"0\" cellspacing=\"0\" valign=\"middle\">\n  <tr>\n    <td width=\"70%\" class=\"footer-text\" style=\"padding-right: 20px;\">\n      <table align=\"left\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n        <tr>\n          <td\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px;\">\n            <p\n              style=\"margin: 0; padding: 0; margin-bottom: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 600; font-size: 20px; line-height: 28px;\">\n              We’re here for you!</p>\n            If you have any questions, search the Bitwarden <a\n              style=\"text-decoration: none; color: #175DDC; font-weight: 600;\"\n              href=\"https://bitwarden.com/help/\">Help</a> site or <a\n              style=\"text-decoration: none; color: #175DDC; font-weight: 600;\"\n              href=\"https://bitwarden.com/contact/\">contact us</a>.\n          </td>\n        </tr>\n      </table>\n    </td>\n    <td width=\"30%\">\n      <table align=\"right\" valign=\"bottom\" class=\"footer-image\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n        style=\"padding-left: 40px;\">\n        <tr>\n          <td>\n            <img width=\"94\" height=\"77\" src=\"https://assets.bitwarden.com/email/v1/chat.png\"\n              style=\"width: 94px; height: 77px;\" alt=\"\" />\n          </td>\n        </tr>\n      </table>\n    </td>\n  </tr>\n</table>\n{{/FullUpdatedHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/SecurityTasks.text.hbs",
    "content": "﻿{{#>FullTextLayout}}\n{{OrgName}} has identified {{TaskCount}} critical {{plurality TaskCount \"login\" \"logins\"}} that {{plurality TaskCount \"requires\" \"require\"}} a password change\n\n{{>@partial-block}}\n\nWe’re here for you!\nIf you have any questions, search the Bitwarden Help site or contact us.\n- https://bitwarden.com/help/\n- https://bitwarden.com/contact/\n{{/FullTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.html.hbs",
    "content": "{{#>FullUpdatedHtmlLayout}}\n<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"background-color: #175DDC;padding-top:45px; \">\n    <tr>\n        <td align=\"center\" valign=\"top\" width=\"70%\" class=\"templateColumnContainer\">\n            <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"padding-left:30px; padding-right: 5px; padding-bottom: 35px;\">\n                <tr>\n                    <td style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 24px; color: #ffffff; line-height: 32px; font-weight: 400; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                        {{{TitleFirst}}}<b class=\"white-title\">{{TitleSecondBold}}</b>{{TitleThird}}\n                    </td>\n                </tr>\n            </table>\n        </td>\n        <td align=\"right\" valign=\"bottom\" class=\"templateColumnContainer\" style=\"padding-right: 10px;\">\n            <img width=\"140\" height=\"140\" align=\"right\" valign=\"bottom\" style=\"width: 140px; height:140px; font-size: 0; vertical-align: bottom; text-align: right;\" alt='' src='https://assets.bitwarden.com/email/v1/business.png' />\n        </td>\n    </tr>\n</table>\n\n{{>@partial-block}}\n\n<table width=\"100%\" style=\"display:table; background-color: #FBFBFB; vertical-align: middle; padding:30px\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" valign=\"middle\">\n    <tr>\n        <td width=\"70%\" class=\"footer-text\" style=\"padding-right: 20px;\">\n            <table align=\"left\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n                <tr>\n                    <td style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px;\">\n                        <p style=\"margin: 0; padding: 0; margin-bottom: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 600; font-size: 20px; line-height: 28px;\">We’re here for you!</p>\n                        If you have any questions, search the Bitwarden <a style=\"text-decoration: none; color: #175DDC; font-weight: 600;\" href=\"https://bitwarden.com/help/\">Help</a> site or <a style=\"text-decoration: none; color: #175DDC; font-weight: 600;\" href=\"https://bitwarden.com/contact/\">contact us</a>.\n                    </td>\n                </tr>\n            </table>\n        </td>\n        <td width=\"30%\">\n            <table align=\"right\" valign=\"bottom\" class=\"footer-image\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"padding-left: 40px;\">\n                <tr>\n                    <td>\n                        <img width=\"94\" height=\"77\" src=\"https://assets.bitwarden.com/email/v1/chat.png\" style=\"width: 94px; height: 77px;\" alt=\"\" />\n                    </td>\n                </tr>\n            </table>\n        </td>\n    </tr>\n</table>\n{{/FullUpdatedHtmlLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Layouts/TitleContactUs.text.hbs",
    "content": "﻿{{#>FullTextLayout}}\n{{TitleFirst}} {{TitleSecondBold}} {{TitleThird}}\n\n{{>@partial-block}}\n\n\nWe’re here for you!\nIf you have any questions, search the Bitwarden Help site or contact us.\n- https://bitwarden.com/help/\n- https://bitwarden.com/contact/\n{{/FullTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/LicenseExpired.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            {{#if IsOrganization}}\n            This email is to notify you that your Bitwarden organization license for <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{OrganizationName}}</b> has expired and must be updated for continued use. See the following article for details about replacing your license file:\n            {{else}}\n            This email is to notify you that your Bitwarden premium license has expired and must be updated for continued use. See the following article for details about replacing your license file:\n            {{/if}}\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            {{{link 'https://bitwarden.com/help/article/licensing-on-premise/'}}}\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/LicenseExpired.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\n{{#if IsOrganization}}\nThis email is to notify you that your Bitwarden organization license for {{OrganizationName}} has expired and must be updated for continued use. See the following article for details about replacing your license file:\n{{else}}\nThis email is to notify you that your Bitwarden premium license has expired and must be updated for continued use. See the following article for details about replacing your license file:\n{{/if}}\n\nhttps://bitwarden.com/help/article/licensing-on-premise/\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n      <!--[if !mso]><!-->\n        <link href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700\" rel=\"stylesheet\" type=\"text/css\">\n        <style type=\"text/css\">\n          @import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500,700);\n        </style>\n      <!--<![endif]-->\n\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-icon-row-text {\n          padding-left: 5px !important;\n          line-height: 20px;\n        }\n        .mj-bw-icon-row {\n          padding: 10px 15px;\n          width: fit-content !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: normal; font-size: 24px; line-height: 32px\">\n              Welcome to Bitwarden!\n            </h1>\n            <mj-text color=\"#fff\" padding-top=\"0\" padding-bottom=\"0\">\n            <h2 style=\"font-weight: normal; font-size: 16px; line-height: 0px\">\n              Let’s get you set up to autofill.\n            </h2>\n          </mj-text></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-hero-responsive-img\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/account-fill.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:15px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">An administrator from <b>{{ OrganizationName }}</b> will approve you\n            before you can share passwords. While you wait for approval, get\n            started with Bitwarden Password Manager:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Browser Extension Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-browser-extension.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/download/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Get the browser extension\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\" alt=\"External Link Icon\" width=\"16px\" style=\"vertical-align: middle;\">\n                    </span>\n                  </a></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">With the Bitwarden extension, you can fill passwords with one click.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Install Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-install.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/help/import-data/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Add passwords to your vault\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\" alt=\"External Link Icon\" width=\"16px\" style=\"vertical-align: middle;\">\n                    </span>\n                  </a></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Quickly transfer existing passwords to Bitwarden using the importer.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Devices Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-devices.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/download/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Download Bitwarden on all devices\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\" alt=\"External Link Icon\" width=\"16px\" style=\"vertical-align: middle;\">\n                    </span>\n                  </a></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Take your passwords with you anywhere.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0 20px 20px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin-top: 0px;\">\n              Learn more about Bitwarden\n            </p>\n            Find user guides, product documentation, and videos on the\n            <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-family-user.text.hbs",
    "content": "{{#>FullTextLayout}}\nWelcome to Bitwarden!\nLet's get you set up with autofill.\n\nA {{OrganizationName}} administrator will approve you before you can share passwords.\nWhile you wait for approval, get started with Bitwarden Password Manager:\n\nGet the browser extension:\nWith the Bitwarden extension, you can fill passwords with one click. (https://www.bitwarden.com/download)\n\nAdd passwords to your vault:\nQuickly transfer existing passwords to Bitwarden using the importer. (https://bitwarden.com/help/import-data/)\n\nDownload Bitwarden on all devices:\nTake your passwords with you anywhere. (https://www.bitwarden.com/download)\n\nLearn more about Bitwarden\nFind user guides, product documentation, and videos on the Bitwarden Help Center. (https://bitwarden.com/help/)\n{{/FullTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-individual-user.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n      <!--[if !mso]><!-->\n        <link href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700\" rel=\"stylesheet\" type=\"text/css\">\n        <style type=\"text/css\">\n          @import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500,700);\n        </style>\n      <!--<![endif]-->\n\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-icon-row-text {\n          padding-left: 5px !important;\n          line-height: 20px;\n        }\n        .mj-bw-icon-row {\n          padding: 10px 15px;\n          width: fit-content !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: normal; font-size: 24px; line-height: 32px\">\n              Welcome to Bitwarden!\n            </h1>\n            <mj-text color=\"#fff\" padding-top=\"0\" padding-bottom=\"0\">\n            <h2 style=\"font-weight: normal; font-size: 16px; line-height: 0px\">\n              Let’s get you set up to autofill.\n            </h2>\n          </mj-text></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-hero-responsive-img\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/account-fill.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:15px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">Follow these simple steps to get up and running with Bitwarden\n            Password Manager:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Browser Extension Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-browser-extension.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/download/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Get the browser extension\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\" alt=\"External Link Icon\" width=\"16px\" style=\"vertical-align: middle;\">\n                    </span>\n                  </a></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">With the Bitwarden extension, you can fill passwords with one click.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Install Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-install.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/help/import-data/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Add passwords to your vault\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\" alt=\"External Link Icon\" width=\"16px\" style=\"vertical-align: middle;\">\n                    </span>\n                  </a></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Quickly transfer existing passwords to Bitwarden using the importer.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Devices Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-devices.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/download/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Download Bitwarden on all devices\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\" alt=\"External Link Icon\" width=\"16px\" style=\"vertical-align: middle;\">\n                    </span>\n                  </a></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Take your passwords with you anywhere.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0 20px 20px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin-top: 0px;\">\n              Learn more about Bitwarden\n            </p>\n            Find user guides, product documentation, and videos on the\n            <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-individual-user.text.hbs",
    "content": "{{#>FullTextLayout}}\nWelcome to Bitwarden!\nLet's get you set up with autofill.\n\nFollow these simple steps to get up and running with Bitwarden Password Manager:\n\nGet the browser extension:\nWith the Bitwarden extension, you can fill passwords with one click. (https://www.bitwarden.com/download)\n\nAdd passwords to your vault:\nQuickly transfer existing passwords to Bitwarden using the importer. (https://bitwarden.com/help/import-data/)\n\nDownload Bitwarden on all devices:\nTake your passwords with you anywhere. (https://bitwarden.com/help/auto-fill-browser/)\n\nLearn more about Bitwarden\nFind user guides, product documentation, and videos on the Bitwarden Help Center. (https://bitwarden.com/help/)\n{{/FullTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-org-user.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n      <!--[if !mso]><!-->\n        <link href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700\" rel=\"stylesheet\" type=\"text/css\">\n        <style type=\"text/css\">\n          @import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500,700);\n        </style>\n      <!--<![endif]-->\n\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n.mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-15 { width:15% !important; max-width: 15%; }\n.mj-column-per-85 { width:85% !important; max-width: 85%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-15 { width:15% !important; max-width: 15%; }\n.moz-text-html .mj-column-per-85 { width:85% !important; max-width: 85%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n\n      @media only screen and (max-width:480px) {\n        .mj-bw-icon-row-text {\n          padding-left: 5px !important;\n          line-height: 20px;\n        }\n        .mj-bw-icon-row {\n          padding: 10px 15px;\n          width: fit-content !important;\n        }\n      }\n    \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        <!-- Blue Header Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;\"><h1 style=\"font-weight: normal; font-size: 24px; line-height: 32px\">\n              Welcome to Bitwarden!\n            </h1>\n            <mj-text color=\"#fff\" padding-top=\"0\" padding-bottom=\"0\">\n            <h2 style=\"font-weight: normal; font-size: 16px; line-height: 0px\">\n              Let’s get you set up to autofill.\n            </h2>\n          </mj-text></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:bottom;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:bottom;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-hero-responsive-img\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:155px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/account-fill.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"155\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:15px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">An administrator from <b>{{ OrganizationName }}</b> will need to\n            confirm you before you can share passwords. Get started with\n            Bitwarden Password Manager:</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Browser Extension Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-browser-extension.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/download/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Get the browser extension\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\" alt=\"External Link Icon\" width=\"16px\" style=\"vertical-align: middle;\">\n                    </span>\n                  </a></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">With the Bitwarden extension, you can fill passwords with one click.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Install Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-install.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/help/import-data/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Add passwords to your vault\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\" alt=\"External Link Icon\" width=\"16px\" style=\"vertical-align: middle;\">\n                    </span>\n                  </a></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Quickly transfer existing passwords to Bitwarden using the importer.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:10px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"mj-bw-icon-row-outlook\" style=\"width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix mj-bw-icon-row\" style=\"font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;\">\n        <!--[if mso | IE]><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td style=\"vertical-align:top;width:90px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-15 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:15%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:48px;\">\n              \n      <img alt=\"Autofill Icon\" src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\" style=\"border:0;border-radius:8px;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"48\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td><td style=\"vertical-align:top;width:510px;\" ><![endif]-->\n                \n      <div class=\"mj-column-per-85 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:85%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\"><a href=\"https://bitwarden.com/help/auto-fill-browser/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">\n                    Try Bitwarden autofill\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\" alt=\"External Link Icon\" width=\"16px\" style=\"vertical-align: middle;\">\n                    </span>\n                  </a></div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" class=\"mj-bw-icon-row-text\" style=\"font-size:0px;padding:5px 10px 0px 10px;word-break:break-word;\">\n                  \n      <div style=\"font-family:Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:left;color:#1B2029;\">Fill your passwords securely with one click.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n              <!--[if mso | IE]></td></tr></table><![endif]-->\n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0 20px 20px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin-top: 0px;\">\n              Learn more about Bitwarden\n            </p>\n            Find user guides, product documentation, and videos on the\n            <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/MJML/Auth/Onboarding/welcome-org-user.text.hbs",
    "content": "{{#>FullTextLayout}}\nWelcome to Bitwarden!\nLet's get you set up with autofill.\n\nA {{OrganizationName}} administrator will approve you before you can share passwords.\nGet started with Bitwarden Password Manager:\n\nGet the browser extension:\nWith the Bitwarden extension, you can fill passwords with one click. (https://www.bitwarden.com/download)\n\nAdd passwords to your vault:\nQuickly transfer existing passwords to Bitwarden using the importer. (https://bitwarden.com/help/import-data/)\n\nTry Bitwarden autofill:\nFill your passwords securely with one click. (https://bitwarden.com/help/auto-fill-browser/)\n\n\nLearn more about Bitwarden\nFind user guides, product documentation, and videos on the Bitwarden Help Center. (https://bitwarden.com/help/)\n{{/FullTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/NewDeviceLoggedIn.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Your Bitwarden account was just logged into from a new device.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">IP Address:</b> {{IpAddress}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Device Type:</b> {{DeviceType}}\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            You can deauthorize all devices that have access to your account from the <a target=\"_blank\" clicktracking=off href=\"{{{WebVaultUrl}}}\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;\">web vault</a> under Settings &rarr; My Account &rarr; Deauthorize Sessions.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/NewDeviceLoggedIn.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYour Bitwarden account was just logged into from a new device.\n\nDate: {{TheDate}} at {{TheTime}} {{TimeZone}}\nIP Address: {{IpAddress}}\nDevice Type: {{DeviceType}}\n\nYou can deauthorize all devices that have access to your account from the\nweb vault under Settings > My Account > Deauthorize Sessions.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\">\n            The domain {{DomainName}} in your Bitwarden organization could not be claimed.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            Check the corresponding record in your domain host. Then reclaim this domain in Bitwarden to use it for your organization.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            The domain will be removed from your organization in 7 days if it is not claimed.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Manage Domains\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationDomainUnclaimed.text.hbs",
    "content": "{{#>BasicTextLayout}}\nThe domain {{DomainName}} in your Bitwarden organization could not be claimed.\n\nCheck the corresponding record in your domain host. Then reclaim this domain in Bitwarden to use it for your organization.\n\nThe domain will be removed from your organization in 7 days if it is not claimed.\n\n{{Url}}\n\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationSeatsAutoscaled.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n    style=\"margin: 0; box-sizing: border-box;\">\n    <tr\n        style=\"margin: 0; box-sizing: border-box;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            To accommodate new member invitations, your seat count has increased from {{InitialSeatCount}} to {{CurrentSeatCount}}. A\n            prorated charge has been immediately applied to your subscription for the new members. This notification will only be sent\n            once.\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; box-sizing: border-box;\">\n        <td class=\"content-block last\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            For more information, please refer to the following help article:\n            <a href=\"https://bitwarden.com/help/managing-users\" class=\"inline-link\">\n                Member management\n            </a>\n            <br class=\"line-break\" />\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{VaultSubscriptionUrl}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Manage subscription\n            </a>\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationSeatsAutoscaled.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nTo accommodate new member invitations, your seat count has increased from {{InitialSeatCount}} to {{CurrentSeatCount}}. A\nprorated charge has been immediately applied to your subscription for the new members.\nThis notification will only be sent once.\n\nFor more information, please refer to the following help article: https://bitwarden.com/help/managing-users\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationSeatsMaxReached.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n    style=\"margin: 0; box-sizing: border-box; \">\n    <tr\n        style=\"margin: 0; box-sizing: border-box; \">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            Your organization has reached the seat limit of {{MaxSeatCount}} and new members cannot be invited.\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; box-sizing: border-box; \">\n        <td class=\"content-block last\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            For more information, please refer to the following help article:\n            <a href=\"https://bitwarden.com/help/managing-users\" class=\"inline-link\">\n                Member management\n            </a>\n            <br class=\"line-break\" />\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{VaultSubscriptionUrl}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Manage subscription\n            </a>\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationSeatsMaxReached.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYour organization has reached the seat limit of {{MaxSeatCount}} and new members cannot be invited.\n\nFor more information, please refer to the following help article: https://bitwarden.com/help/managing-users\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationSmSeatsMaxReached.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n    style=\"margin: 0; box-sizing: border-box; \">\n    <tr\n        style=\"margin: 0; box-sizing: border-box; \">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            Your organization has reached the Secrets Manager seat limit of {{MaxSeatCount}} and new members cannot be invited.\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; box-sizing: border-box; \">\n        <td class=\"content-block last\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            For more information, please refer to the following help article:\n            <a href=\"https://bitwarden.com/help/managing-users\" class=\"inline-link\">\n                Member management\n            </a>\n            <br class=\"line-break\" />\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{VaultSubscriptionUrl}}}\" clicktracking=off target=\"_blank\" rel=\"noopener noreferrer\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Manage subscription\n            </a>\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationSmSeatsMaxReached.text.hbs",
    "content": "{{#>BasicTextLayout}}\nYour organization has reached the Secrets Manager seat limit of {{MaxSeatCount}} and new members cannot be invited.\n\nFor more information, please refer to the following help article: https://bitwarden.com/help/managing-users\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationSmServiceAccountsMaxReached.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n    style=\"margin: 0; box-sizing: border-box; \">\n    <tr\n        style=\"margin: 0; box-sizing: border-box; \">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created.\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; box-sizing: border-box; \">\n        <td class=\"content-block last\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            For more information, please refer to the following help article:\n            <a href=\"https://bitwarden.com/help/managing-users\" class=\"inline-link\">\n                Member management\n            </a>\n            <br class=\"line-break\" />\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{VaultSubscriptionUrl}}}\" clicktracking=off target=\"_blank\" rel=\"noopener noreferrer\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Manage subscription\n            </a>\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationSmServiceAccountsMaxReached.text.hbs",
    "content": "{{#>BasicTextLayout}}\nYour organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created.\n\nFor more information, please refer to the following help article: https://bitwarden.com/help/managing-users\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            {{UserIdentifier}} needs to be confirmed to {{OrganizationName}} before they can access the organization vault.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            For more information, please refer to the following help article: \n            <a href=\"https://bitwarden.com/help/managing-users/#onboard-users\" class=\"inline-link\">\n                Member management\n            </a>\n            <br class=\"line-break\" />\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{ConfirmUrl}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Confirm member\n            </a>\n            <br class=\"line-break\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationUserAccepted.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\n{{UserIdentifier}} needs to be confirmed to {{OrganizationName}} before they can access the organization vault.\n\nFor more information, please refer to the following help article: https://bitwarden.com/help/managing-users/#onboard-users\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.html.hbs",
    "content": "﻿{{#>TitleContactUsHtmlLayout}}\n<table width=\"100%\" border=\"0\" style=\"display: block; padding: 30px;\" align=\"center\" cellpadding=\"0\" cellspacing=\"0\">\n    <tr>\n        <td style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px;\">\n            You may now access logins and other items this organization has shared with you from your Bitwarden vault.\n        </td>\n    </tr>\n</table>\n<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n    <tr>\n        <td style=\"display: block;\" align=\"center\">\n            <a href=\"{{{WebVaultUrl}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Go to vault\n            </a>\n        </td>\n    </tr>\n</table>\n<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"padding: 30px;\">\n    <tr>\n        <td style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px;\">\n            <b>Tip: </b>Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the <a style=\"text-decoration: none; color: #175DDC; font-weight: 600;\" href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\">App Store</a> or <a style=\"text-decoration: none; color: #175DDC; font-weight: 600;\" href=\"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden\">Google Play</a>.\n        </td>\n    </tr>\n</table>\n<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"display: table; padding-bottom: 20px; margin: 0;\">\n    <tr>\n        <td class=\"templateColumnContainer center\" width=\"45%\" align=\"right\">\n            <a height=\"60\" width=\"150\" href='https://play.google.com/store/apps/details?id=com.x8bit.bitwarden' target=\"_blank\" style=\"height: 60px; width: 150px;\"><img height=\"60\" width=\"150\" style=\"height: 60px; width: 150px; display: inline-block;\" alt='Android download' src='https://assets.bitwarden.com/email/v1/google-play-badge.png' /></a>\n        </td>\n        <td class=\"templateColumnContainer center\" width=\"45%\" style=\"padding-left: 10px;\">\n            <a height=\"40\" width=\"135\" href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\" target=\"_blank\" style=\"height: 40px; width: 135px;\"><img height=\"40\" width=\"135\" style=\"height: 40px; width: 135px; display: inline-block;\" alt=\"iOS download\" src=\"https://assets.bitwarden.com/email/v1/App-store.png\" /></a>\n        </td>\n    </tr>\n</table>\n{{/TitleContactUsHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationUserConfirmed.text.hbs",
    "content": "﻿{{#>TitleContactUsTextLayout}}\nYou may now access logins and other items this organizations has shared with you from your Bitwarden vault.\n\nTip: Use the Bitwarden mobile app to quickly save logins and auto-fill forms. Download from the App Store or Google Play.\n{{/TitleContactUsTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs",
    "content": "﻿{{#>TitleContactUsHtmlLayout}}\n<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"display: table; width:100%; padding-top: 35px; text-align: center;\" align=\"center\">\n    <tr>\n        <td display=\"display: table-cell\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n               {{JoinOrganizationButtonText}}\n            </a>\n        </td>\n    </tr>\n</table>\n<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;\">\n    <tr>\n        <td style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;\">\n            This invitation expires on <b>{{ExpirationDate}}</b>\n        </td>\n    </tr>\n</table>\n{{/TitleContactUsHtmlLayout}}\n\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs",
    "content": "﻿{{#>TitleContactUsTextLayout}}\n{{{Url}}}\n\nThis invitation expires on {{ExpirationDate}}.\n{{/TitleContactUsTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/PaymentFailed.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            We wanted to let you know that a <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{usd Amount}}</b> payment for your subscription with Bitwarden was unsuccessful. We will re-attempt to collect payment a few more times over the coming days.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            To avoid any interruption in service, please ensure that your payment method on file is up to date and can be charged for the above amount. You can manage your subscription, payment method, and invoices by logging into the web vault at {{{link WebVaultUrl}}}.\n        </td>\n    </tr>\n    {{#if MentionInvoices}}\n    {{/if}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            If you have any questions or problems, please feel free to email us at hello@bitwarden.com.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/PaymentFailed.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nWe wanted to let you know that a {{usd Amount}} payment for your subscription with Bitwarden was unsuccessful. We will re-attempt to collect payment a few more times over the coming days.\n\nTo avoid any interruption in service, please ensure that your payment method on file is up to date and can be charged for the above amount. You can manage your subscription, payment method, and invoices by logging into the web vault at {{{WebVaultUrl}}}.\n{{#if MentionInvoices}}\n{{/if}}\n\nIf you have any questions or problems, please feel free to email us at hello@bitwarden.com.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/InitiateDeleteProvider.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            We recently received your request to permanently delete the following Bitwarden provider:\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Name:</b> {{ProviderName}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">ID:</b> {{ProviderId}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Created:</b> {{ProviderCreationDate}} at {{ProviderCreationTime}} {{TimeZone}}<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Billing email address:</b> {{ProviderBillingEmail}}\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            Click the link below to delete your Bitwarden provider.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you did not request this email to delete your Bitwarden provider, you can safely ignore it.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Delete Your Provider\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/InitiateDeleteProvider.text.hbs",
    "content": "{{#>BasicTextLayout}}\nWe recently received your request to permanently delete the following Bitwarden provider:\n\n- Name: {{ProviderName}}\n- ID: {{ProviderId}}\n- Created: {{ProviderCreationDate}} at {{ProviderCreationTime}} {{TimeZone}}\n- Billing email address: {{ProviderBillingEmail}}\n\nClick the link below to complete the deletion of your provider.\n\nIf you did not request this email to delete your Bitwarden provider, you can safely ignore it.\n\n{{{Url}}}\n\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderSetupInvite.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            You have been invited to set up a new Provider within Bitwarden.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Set Up Provider Now\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderSetupInvite.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYou have been invited to setup a new Provider within Bitwarden. To continue, click the following link:\n\n{{{Url}}}\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderUpdatePaymentMethod.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n    <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 20px; -webkit-text-size-adjust: none;\" valign=\"top\">\n                Your organization, {{OrganizationName}}, is no longer managed by {{ProviderName}}. Please update your billing information.\n            </td>\n        </tr>\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 20px; -webkit-text-size-adjust: none;\" valign=\"top\">\n                To maintain your subscription, update your organization billing information by navigating to the web vault -> Organization -> Billing -> <a target=\"_blank\" clicktracking=off href=\"{{PaymentMethodUrl}}\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;\">Payment Method</a>.\n            </td>\n        </tr>\n        <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 20px; -webkit-text-size-adjust: none;\" valign=\"top\">\n                For more information, please refer to the following help article: <a target=\"_blank\" clicktracking=off href=\"https://bitwarden.com/help/update-billing-info/#update-billing-information-for-organizations\" style=\"-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;\">Update billing information for organizations</a>\n            </td>\n        </tr>\n        <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n            <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n                <a href=\"{{{PaymentMethodUrl}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                    Add payment method\n                </a>\n                <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            </td>\n        </tr>\n    </table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderUpdatePaymentMethod.text.hbs",
    "content": "{{#>BasicTextLayout}}\n    Your organization, {{OrganizationName}}, is no longer managed by {{ProviderName}}. Please update your billing information.\n\n    To maintain your subscription, update your organization billing information by navigating to the web vault -> Organization -> Billing -> Payment Method.\n\n    Or click the following link: {{{link PaymentMethodUrl}}}\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderUserConfirmed.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            This email is to notify you that you have been confirmed as a user of <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{ProviderName}}</b>.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            You may now access the provider and manage the connected organizations.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderUserConfirmed.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nThis email is to notify you that you have been confirmed as a user of {{ProviderName}}.\n\nYou may now access the provider and manage the connected organizations.\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderUserInvited.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            You have been invited to join the provider, <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{ProviderName}}</b>.\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;\" valign=\"top\" align=\"center\">\n            If you do not wish to join this provider, you can safely ignore this email.\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n    <tr style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;\" valign=\"top\" align=\"center\">\n            <a href=\"{{{Url}}}\" clicktracking=off target=\"_blank\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Join Provider Now\n            </a>\n            <br style=\"margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderUserInvited.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYou have been invited to join the provider, {{ProviderName}}.\n\nIf you do not wish to join this provider, you can safely ignore this email.\n\n{{{Url}}}\n\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderUserRemoved.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\" align=\"left\">\n            You have been removed from <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{ProviderName}}</b>. You will no longer have access to the Provider Portal. If you have an existing Bitwarden account, your account is unaffected.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Provider/ProviderUserRemoved.text.hbs",
    "content": "﻿{{#>BasicTextLayout}}\nYou have been removed from {{ProviderName}}. You will no longer have access to the Provider Portal.\nIf you have an existing Bitwarden account, your account is unaffected.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.html.hbs",
    "content": "{{#>ProviderFull}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #1B2029; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #1B2029; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 20px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            {{#if (eq CollectionMethod \"send_invoice\")}}\n            <div style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 600; font-size: 24px; line-height: 32px; letter-spacing: 0px; color: #1B2029; margin: 0 0 8px 0;\">Your subscription will renew soon</div>\n            <div style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;\">On <strong>{{date DueDate 'MMMM dd, yyyy'}}</strong> we'll send you an invoice with a summary of the charges including tax.</div>\n            {{else}}\n            <div style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 600; font-size: 24px; line-height: 32px; letter-spacing: 0px; color: #1B2029; margin: 0 0 8px 0;\">Your subscription will renew on {{date DueDate 'MMMM dd, yyyy'}}</div>\n                {{#if HasPaymentMethod}}\n            <div style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;\">To avoid any interruption in service, please ensure your {{PaymentMethodDescription}} can be charged for the following amount:</div>\n                {{else}}\n            <div style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;\">To avoid any interruption in service, please add a payment method that can be charged for the following amount:</div>\n                {{/if}}\n            {{/if}}\n        </td>\n    </tr>\n    {{#unless (eq CollectionMethod \"send_invoice\")}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #1B2029; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 32px; font-weight: bold; color: #1B2029; line-height: 1.2; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 20px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            {{usd AmountDue}}\n        </td>\n    </tr>\n    {{/unless}}\n    {{#if Items}}\n    {{#unless (eq CollectionMethod \"send_invoice\")}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #1B2029; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; font-weight: 400; color: #1B2029; line-height: 24px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <strong style=\"margin: 0; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; font-weight: 700; color: ##1B2029; line-height: 24px; letter-spacing: 0px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">Summary Of Charges</strong><br />\n            <div style=\"border-bottom: 1px solid #ddd; margin: 5px 0 10px 0; padding-bottom: 5px;\"></div>\n            {{#each Items}}\n            <div style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;\">{{this}}</div>\n            {{/each}}\n        </td>\n    </tr>\n    {{/unless}}\n    {{/if}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 20px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            {{#if (eq CollectionMethod \"send_invoice\")}}\n            <div style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; font-size: 16px; line-height: 24px; letter-spacing: 0px; color: #1B2029; margin: 0;\">To avoid any interruption in service for you or your clients, please pay the invoice by the due date, or contact Bitwarden Customer Support to sign up for auto-pay.</div>\n            {{else}}\n\n            {{/if}}\n        </td>\n    </tr>\n    {{#unless (eq CollectionMethod \"send_invoice\")}}\n\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            <table cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0;\">\n                <tr>\n                    <td style=\"background-color: #175DDC; border-radius: 25px; padding: 12px 24px;\">\n                        <a href=\"{{{UpdateBillingInfoUrl}}}\" style=\"color: #ffffff; text-decoration: none; font-weight: 500; font-size: 16px;\">Update payment method</a>\n                    </td>\n                </tr>\n            </table>\n        </td>\n    </tr>\n    {{/unless}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 16px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            {{#if (eq CollectionMethod \"send_invoice\")}}\n            <table cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0;\">\n                <tr>\n                    <td style=\"background-color: #175DDC; border-radius: 25px; padding: 12px 24px;\">\n                        <a href=\"{{{ContactUrl}}}\" style=\"color: #ffffff; text-decoration: none; font-weight: 500; font-size: 16px;\">Contact Bitwarden Support</a>\n                    </td>\n                </tr>\n            </table>\n            {{/if}}\n        </td>\n    </tr>\n    {{#if (eq CollectionMethod \"send_invoice\")}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; font-weight: 400; color: #1B2029; line-height: 20px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            For assistance managing your subscription, please visit <a href=\"https://bitwarden.com/help/update-billing-info\" style=\"color: #175DDC !important; text-decoration: none; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; line-height: 20px; letter-spacing: 0px;\"><strong style=\"color: #175DDC !important;\">the Help Center</strong></a> or <a href=\"https://bitwarden.com/contact/\" style=\"color: #175DDC !important; text-decoration: none; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; line-height: 20px; letter-spacing: 0px;\"><strong style=\"color: #175DDC !important;\">contact Bitwarden Customer Support</strong></a>.\n        </td>\n    </tr>\n    {{/if}}\n    {{#unless (eq CollectionMethod \"send_invoice\")}}\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; font-weight: 400; color: #1B2029; line-height: 20px; letter-spacing: 0px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            For assistance managing your subscription, please visit <a href=\"https://bitwarden.com/help/update-billing-info\" style=\"color: #175DDC !important; text-decoration: none; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; line-height: 20px; letter-spacing: 0px;\"><strong style=\"color: #175DDC !important;\">the Help Center</strong></a> or <a href=\"https://bitwarden.com/contact/\" style=\"color: #175DDC !important; text-decoration: none; font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 700; line-height: 20px; letter-spacing: 0px;\"><strong style=\"color: #175DDC !important;\">contact Bitwarden Customer Support</strong></a>.\n        </td>\n    </tr>\n    {{/unless}}\n</table>\n{{/ProviderFull}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.text.hbs",
    "content": "{{#>BasicTextLayout}}\n{{#if (eq CollectionMethod \"send_invoice\")}}\nYour subscription will renew soon\n\nOn {{date DueDate 'MMMM dd, yyyy'}} we'll send you an invoice with a summary of the charges including tax.\n{{else}}\nYour subscription will renew on {{date DueDate 'MMMM dd, yyyy'}}\n\n    {{#if HasPaymentMethod}}\nTo avoid any interruption in service, please ensure your {{PaymentMethodDescription}} can be charged for the following amount:\n    {{else}}\nTo avoid any interruption in service, please add a payment method that can be charged for the following amount:\n    {{/if}}\n\n{{usd AmountDue}}\n{{/if}}\n{{#if Items}}\n{{#unless (eq CollectionMethod \"send_invoice\")}}\n\nSummary Of Charges\n------------------\n{{#each Items}}\n{{this}}\n{{/each}}\n{{/unless}}\n{{/if}}\n\n{{#if (eq CollectionMethod \"send_invoice\")}}\nTo avoid any interruption in service for you or your clients, please pay the invoice by the due date, or contact Bitwarden Customer Support to sign up for auto-pay.\n\nContact Bitwarden Support: {{{ContactUrl}}}\n\nFor assistance managing your subscription, please visit the **Help center** (https://bitwarden.com/help/update-billing-info) or **contact Bitwarden Customer Support** (https://bitwarden.com/contact/).\n{{else}}\n\n{{/if}}\n\n{{#unless (eq CollectionMethod \"send_invoice\")}}\nFor assistance managing your subscription, please visit the **Help center** (https://bitwarden.com/help/update-billing-info) or **contact Bitwarden Customer Support** (https://bitwarden.com/contact/).\n{{/unless}}\n{{/BasicTextLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\">\n            {{UserNameRequestingAccess}} has requested access to secrets manager for {{OrgName}}: <br /><br />\n            <pre style=\"white-space: pre-wrap; word-wrap: break-word; background-color: #ECECEC; max-width: 700px; border-radius: 10px; padding: 1em; margin-bottom: 2em;\">{{EmailContent}} - {{UserNameRequestingAccess}}</pre>\n        </td>\n        <br/>\n    </tr>\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"margin-bottom:1em; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\" valign=\"top\" align=\"left\">\n            <a href=\"https://bitwarden.com/contact-sales/?utm_source=sm_request_access_email&utm_medium=email\" clicktracking=off target=\"_blank\" rel=\"noopener\" style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                Contact Bitwarden\n            </a>\n            <br/>\n        </td>\n    </tr>\n    <tr style=\"margin-top:1em; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; font-weight: bold;\" valign=\"top\">\n            <br/> Stay safe and secure,<br style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\" />\n            The Bitwarden Team\n        </td>\n    </tr>\n</table>\n\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/SecretsManagerAccessRequest.text.hbs",
    "content": "{{#>FullTextLayout}}\n\n{{UserNameRequestingAccess}} has requested access to secrets manager for {{OrgName}}:\n\n============\n\n{{EmailContent}} - {{UserNameRequestingAccess}}\n\n============\n\nContact Bitwarden (https://bitwarden.com/contact-sales/?utm_source=sm_request_access_email&utm_medium=email)\n\n============\n\nStay safe and secure,\nThe Bitwarden Team\n{{/FullTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs",
    "content": "﻿{{#>SecurityTasksHtmlLayout}}\n<table width=\"100%\" border=\"0\" style=\"display: block; padding: 30px;\" align=\"center\" cellpadding=\"0\" cellspacing=\"0\">\n  <tr>\n    <td\n      style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px;\">\n      Keep yourself and your organization's data safe by changing passwords that are weak, reused, or have been exposed\n      in a data breach.\n    </td>\n  </tr>\n  <tr>\n    <td\n      style=\"padding-top: 24px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px;\">\n      Launch the Bitwarden extension to review your at-risk passwords.\n    </td>\n  </tr>\n</table>\n<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"padding-bottom: 24px; padding-left: 24px; padding-right: 24px; text-align: center;\" align=\"center\">\n  <tr>\n    <td>\n      <a href=\"{{ReviewPasswordsUrl}}\" clicktracking=off target=\"_blank\"\n        style=\"display: inline-block; font-weight: bold; color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 999px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        Review at-risk passwords\n      </a>\n    </td>\n  </tr>\n</table>\n<table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"padding-bottom: 24px; padding-left: 24px; padding-right: 24px; text-align: center;\" align=\"center\">\n  <tr>\n    <td display=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 12px; line-height: 16px;\">\n      {{formatAdminOwnerEmails AdminOwnerEmails}}\n    </td>\n  </tr>\n</table>\n{{/SecurityTasksHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs",
    "content": "﻿{{#>SecurityTasksHtmlLayout}}\nKeep yourself and your organization's data safe by changing passwords that are weak, reused, or have been exposed in a\ndata breach.\n\nLaunch the Bitwarden extension to review your at-risk passwords.\n\nReview at-risk passwords ({{{ReviewPasswordsUrl}}})\n\n{{#if AdminOwnerEmails.[0]}}\n  {{#if AdminOwnerEmails.[1]}}\n    This request was initiated by\n    {{#each AdminOwnerEmails}}\n      {{#if @last}}and {{/if}}{{this}}{{#unless @last}}, {{/unless}}\n    {{/each}}.\n  {{else}}\n    This request was initiated by {{AdminOwnerEmails.[0]}}.\n  {{/if}}\n{{/if}}\n{{/SecurityTasksHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/TrialInitiation.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n    style=\"margin: 12px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            Welcome to Bitwarden!\n            <br />\n            <br />\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            Here are a few simple steps to get up and running with Bitwarden Password Manager:\n            <br />\n            <br />\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            <ol style=\"padding-bottom: 32px; margin-bottom: 32px; border-bottom: 1px solid #e9e9e9;\">\n                <li style=\"margin-bottom: 24px;\">\n                    <b>Install the browser extension</b>\n                    <p>Autofill passwords, save new logins, access the password generator, and more from the Bitwarden\n                        browser extension. The extension keeps Bitwarden easily accessible so you can be secure while\n                        online.\n                    </p>\n                    <br />\n                    <a href=\"https://bitwarden.com/download\" clicktracking=\"off\" target=\"_blank\" rel=\"noopener\"\n                        style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 2px; padding: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                        Install the extension\n                    </a>\n                </li>\n                <li style=\"margin-bottom: 24px;\">\n                    <b>Add passwords to your vault</b>\n                    <p>Your secure vault is ready to safely store your sensitive information. Add logins, credit cards,\n                        notes, and identities for fast access and autofilling!</p>\n                    <br />\n                    <a href=\"{{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email\" clicktracking=\"off\"\n                        target=\"_blank\" rel=\"noopener\"\n                        style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 2px; padding: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                        Add passwords to your vault\n                    </a>\n                </li>\n                <li>\n                    <b>Keep your master password safe and enable biometrics</b>\n                    <p>Vault security starts with your Bitwarden master password. Ensure it is strong and complex, then\n                        memorize\n                        it or store it in a safe place. Now you&lsquo;re ready to enable biometrics for a faster login\n                        experience with\n                        fingerprint or FaceID.</p>\n                    <br />\n                    <a href=\"https://bitwarden.com/learning/unlock-your-vault-with-biometrics/\" clicktracking=\"off\"\n                        target=\"_blank\" rel=\"noopener\"\n                        style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 2px; padding: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                        Enable biometrics\n                    </a>\n                </li>\n            </ol>\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"h3\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;\"\n            valign=\"top\">\n            Learning Center\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            Instructional videos and best practice guides for every level are just a click away.\n            <br />\n            <a href=\"https://bitwarden.com/learning/\" clicktracking=\"off\" target=\"_blank\" rel=\"noopener\"\n                style=\"color: #175DDC; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #fff; border-color: #175DDC; border-style: solid; padding: 10px 20px; border-width: 2px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin-top: 16px;\">\n                Visit learning center\n            </a>\n            <br />\n            <br />\n            <br />\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"h3\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;\"\n            valign=\"top\">\n            Bring Bitwarden to Work\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; padding-bottom: 42px;\"\n            valign=\"top\">\n            Are you using Bitwarden for personal or family use? Join the Bitwarden fans who are bringing the password\n            manager\n            they know and love to their workplace.\n            <br />\n            <a href=\"https://bitwarden.com/go/bring-bitwarden-to-work/\" clicktracking=\"off\" target=\"_blank\" rel=\"noopener\"\n                style=\"color: #175DDC; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #fff; border-color: #175DDC; border-style: solid; padding: 10px 20px; border-width: 2px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin-top: 16px;\">\n                Bring it\n            </a>\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 10px; -webkit-text-size-adjust: none; text-align: center; border: 2px solid #175DDC; border-radius: 24px;\"\n            valign=\"top\">\n            <b>Signed up for Bitwarden Secrets Manager?</b>\n            <p style=\"margin-top: 8px; text-wrap: balance;\">If you signed up for Bitwarden Secrets Manager to secure\n                infrastructure and\n                machine secrets, get up and running quickly with the <a\n                    href=\"https://bitwarden.com/help/secrets-manager-quick-start/\" clicktracking=\"off\" target=\"_blank\"\n                    rel=\"noopener\"\n                    style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;\">Secrets\n                    Manager quick start resource</a>.</p>\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; font-weight: bold; padding-top: 42px;\"\n            valign=\"top\">\n            Stay safe and secure,\n            <br />\n            The Bitwarden Team\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/TrialInitiation.text.hbs",
    "content": "{{#>FullTextLayout}}\nWelcome to Bitwarden!\n\nHere are a few simple steps to get up and running with Bitwarden Password Manager:\n\n\n1. Install the browser extension\n============\n\nAutofill passwords, save new logins, access the password generator, and more from the Bitwarden browser extension. The extension keeps Bitwarden easily accessible so you can be secure while online.\nInstall the extension (http://www.bitwarden.com/download)\n\n\n2. Add passwords to your vault\n============\n\nYour secure vault is ready to safely store your sensitive information. Add logins, credit cards, notes, and identities for fast access and autofilling!\nAdd passwords to your vault ({{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email)\n\n\n3. Keep your master password safe and enable biometrics\n============\n\nVault security starts with your Bitwarden master password. Ensure it is strong and complex, then memorize it or store it in a safe place. Now you're ready to enable biometrics for a faster login experience with fingerprint or FaceID.\nEnable biometrics (https://bitwarden.com/learning/unlock-your-vault-with-biometrics/)\n\n\nLearning Center\n============\n\nInstructional videos and best practice guides for every level are just a click away.\nVisit the learning center (https://bitwarden.com/learning/)\n\n\nBring Bitwarden to Work\n============\n\nAre you using Bitwarden for personal or family use? Join the Bitwarden fans who are bringing the password manager they know and love to their workplace.\nBring it (https://bitwarden.com/go/bring-bitwarden-to-work/)\n\n\nSigned up for Bitwarden Secrets Manager?\n============\n\nIf you signed up for Bitwarden Secrets Manager to secure infrastructure and machine secrets, get up and running quickly with the Secrets Manager quick start resource (https://bitwarden.com/help/secrets-manager-quick-start/).\n\n\nStay safe and secure,\nThe Bitwarden Team\n{{/FullTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/UpdatedTempPassword.html.hbs",
    "content": "{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;\" valign=\"top\">\n            The temporary master password set by an administrator for <b style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">{{UserName}}</b> has been changed. If you did not initiate this request, please reach out to your administrator immediately.\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/UpdatedTempPassword.text.hbs",
    "content": "{{#>BasicTextLayout}}\nThe temporary master password set by an administrator for {{UserName}} has been changed. If you did not initiate this request, please reach out to your administrator immediately.\n{{/BasicTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Welcome.html.hbs",
    "content": "﻿{{#>FullHtmlLayout}}\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"\n    style=\"margin: 12px 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            Welcome to Bitwarden!\n            <br />\n            <br />\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            Here are a few simple steps to get up and running with Bitwarden Password Manager:\n            <br />\n            <br />\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            <ol style=\"padding-bottom: 32px; margin-bottom: 32px; border-bottom: 1px solid #e9e9e9;\">\n                <li style=\"margin-bottom: 24px;\">\n                    <b>Install the browser extension</b>\n                    <p>Autofill passwords, save new logins, access the password generator, and more from the Bitwarden\n                        browser extension. The extension keeps Bitwarden easily accessible so you can be secure while\n                        online.\n                    </p>\n                    <br />\n                    <a href=\"https://bitwarden.com/download\" clicktracking=\"off\" target=\"_blank\" rel=\"noopener\"\n                        style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 2px; padding: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                        Install the extension\n                    </a>\n                </li>\n                <li style=\"margin-bottom: 24px;\">\n                    <b>Add passwords to your vault</b>\n                    <p>Your secure vault is ready to safely store your sensitive information. Add logins, credit cards,\n                        notes, and identities for fast access and autofilling!</p>\n                    <br />\n                    <a href=\"{{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email\" clicktracking=\"off\"\n                        target=\"_blank\" rel=\"noopener\"\n                        style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 2px; padding: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                        Add passwords to your vault\n                    </a>\n                </li>\n                <li>\n                    <b>Keep your master password safe and enable biometrics</b>\n                    <p>Vault security starts with your Bitwarden master password. Ensure it is strong and complex, then\n                        memorize\n                        it or store it in a safe place. Now you&lsquo;re ready to enable biometrics for a faster login\n                        experience with\n                        fingerprint or FaceID.</p>\n                    <br />\n                    <a href=\"https://bitwarden.com/learning/unlock-your-vault-with-biometrics/\" clicktracking=\"off\"\n                        target=\"_blank\" rel=\"noopener\"\n                        style=\"color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 2px; padding: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n                        Enable biometrics\n                    </a>\n                </li>\n            </ol>\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"h3\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;\"\n            valign=\"top\">\n            Learning Center\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;\"\n            valign=\"top\">\n            Instructional videos and best practice guides for every level are just a click away.\n            <br />\n            <a href=\"https://bitwarden.com/learning/\" clicktracking=\"off\" target=\"_blank\" rel=\"noopener\"\n                style=\"color: #175DDC; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #fff; border-color: #175DDC; border-style: solid; padding: 10px 20px; border-width: 2px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin-top: 16px;\">\n                Visit learning center\n            </a>\n            <br />\n            <br />\n            <br />\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"h3\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;\"\n            valign=\"top\">\n            Bring Bitwarden to Work\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; padding-bottom: 42px;\"\n            valign=\"top\">\n            Are you using Bitwarden for personal or family use? Join the Bitwarden fans who are bringing the password\n            manager\n            they know and love to their workplace.\n            <br />\n            <a href=\"https://bitwarden.com/go/bring-bitwarden-to-work/\" clicktracking=\"off\" target=\"_blank\" rel=\"noopener\"\n                style=\"color: #175DDC; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 1000px; background-color: #fff; border-color: #175DDC; border-style: solid; padding: 10px 20px; border-width: 2px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin-top: 16px;\">\n                Bring it\n            </a>\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 10px; -webkit-text-size-adjust: none; text-align: center; border: 2px solid #175DDC; border-radius: 24px;\"\n            valign=\"top\">\n            <b>Signed up for Bitwarden Secrets Manager?</b>\n            <p style=\"margin-top: 8px; text-wrap: balance;\">If you signed up for Bitwarden Secrets Manager to secure\n                infrastructure and\n                machine secrets, get up and running quickly with the <a\n                    href=\"https://bitwarden.com/help/secrets-manager-quick-start/\" clicktracking=\"off\" target=\"_blank\"\n                    rel=\"noopener\"\n                    style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;\">Secrets\n                    Manager quick start resource</a>.</p>\n        </td>\n    </tr>\n    <tr\n        style=\"margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;\">\n        <td class=\"content-block last\"\n            style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; font-weight: bold; padding-top: 42px;\"\n            valign=\"top\">\n            Stay safe and secure,\n            <br />\n            The Bitwarden Team\n        </td>\n    </tr>\n</table>\n{{/FullHtmlLayout}}"
  },
  {
    "path": "src/Core/MailTemplates/Handlebars/Welcome.text.hbs",
    "content": "﻿{{#>FullTextLayout}}\nWelcome to Bitwarden!\n\nHere are a few simple steps to get up and running with Bitwarden Password Manager:\n\n\n1. Install the browser extension\n============\n\nAutofill passwords, save new logins, access the password generator, and more from the Bitwarden browser extension. The extension keeps Bitwarden easily accessible so you can be secure while online.\nInstall the extension (http://www.bitwarden.com/download)\n\n\n2. Add passwords to your vault\n============\n\nYour secure vault is ready to safely store your sensitive information. Add logins, credit cards, notes, and identities for fast access and autofilling!\nAdd passwords to your vault ({{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email)\n\n\n3. Keep your master password safe and enable biometrics\n============\n\nVault security starts with your Bitwarden master password. Ensure it is strong and complex, then memorize it or store it in a safe place. Now you're ready to enable biometrics for a faster login experience with fingerprint or FaceID.\nEnable biometrics (https://bitwarden.com/learning/unlock-your-vault-with-biometrics/)\n\n\nLearning Center\n============\n\nInstructional videos and best practice guides for every level are just a click away.\nVisit the learning center (https://bitwarden.com/learning/)\n\n\nBring Bitwarden to Work\n============\n\nAre you using Bitwarden for personal or family use? Join the Bitwarden fans who are bringing the password manager they know and love to their workplace.\nBring it (https://bitwarden.com/go/bring-bitwarden-to-work/)\n\n\nSigned up for Bitwarden Secrets Manager?\n============\n\nIf you signed up for Bitwarden Secrets Manager to secure infrastructure and machine secrets, get up and running quickly with the Secrets Manager quick start resource (https://bitwarden.com/help/secrets-manager-quick-start/).\n\n\nStay safe and secure,\nThe Bitwarden Team\n{{/FullTextLayout}}\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/.mjmlconfig",
    "content": "{\n  \"packages\": [\n    \"components/mj-bw-hero\",\n    \"components/mj-bw-simple-hero\",\n    \"components/mj-bw-icon-row\",\n    \"components/mj-bw-learn-more-footer\",\n    \"emails/AdminConsole/components/mj-bw-inviter-info\",\n    \"emails/AdminConsole/components/mj-bw-ac-hero\",\n    \"emails/AdminConsole/components/mj-bw-ac-icon-row\",\n    \"emails/AdminConsole/components/mj-bw-ac-icon-row-without-bulletins\",\n    \"emails/AdminConsole/components/mj-bw-ac-learn-more-footer\"\n  ]\n}\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/README.md",
    "content": "# `MJML` email templating\n\nThis directory contains `MJML` templates for emails. `MJML` is a markup language designed to reduce the pain of coding responsive email templates. Component-based development features in `MJML` improve code quality and reusability.\n\n> [!TIP]\n> `MJML` stands for MailJet Markup Language.\n\n## Implementation considerations\n\n`MJML` templates are compiled into `HTML`, and those outputs are then consumed by Handlebars to render the final email for delivery. It builds on top of our existing infrastructure and means we can continue to use the double brace (`{{}}`) syntax within `MJML`, since Handlebars will assign values to those `{{variables}}`.\n\nTo do this, there is an added step where we compile `*.mjml` to `*.html.hbs`. `*.html.hbs` is the format we use so the Handlebars service can apply the variables. This build pipeline process is in progress and may need to be manually done at times.\n\n### `*.txt.hbs`\n\nThere is no change to how we create the `txt.hbs`. MJML does not impact how we create these artifacts.\n\n## Building `MJML` files\n\n```shell\nnpm ci\n\n# Build *.html to ./out directory\nnpm run build\n\n# To build on changes to *.mjml and *.js files, new files will not be tracked, you will need to run again\nnpm run build:watch\n\n# Build *.html.hbs to ./out directory\nnpm run build:hbs\n\n# Build minified *.html.hbs to ./out directory\nnpm run build:minify\n\n# apply prettier formatting\nnpm run prettier\n```\n\n## Development process\n\n`MJML` supports components and you can create your own components by adding them to `.mjmlconfig`. Components are simple JavaScript that return `MJML` markup based on the attributes assigned, see components/mj-bw-hero.js. The markup is not a proper object, but contained in a string.\n\nWhen using `MJML` templating you can use the above [commands](#building-mjml-files) to compile the template and view it in a web browser.\n\nNot all `MJML` tags have the same attributes, it is highly recommended to review the documentation on the official MJML website to understand the usages of each of the tags.\n\n### Developing the mail template\n\n1. Create `cool-email.mjml` in appropriate team directory.\n2. Run `npm run build:watch`.\n3. View compiled `HTML` output in a web browser.\n4. Iterate through your development. While running `build:watch` you should be able to refresh the browser page after the `mjml/js` recompile to see the changes.\n\n### Testing the mail template with `IMailer`\n\nAfter the email is developed in the [initial step](#developing-the-mail-template), we need to make sure that the email `{{variables}}` are populated properly by Handlebars. We can do this by running it through an `IMailer` implementation. The `IMailer`, documented [here](../../Platform/Mail/README.md#step-3-create-handlebars-templates), requires that the ViewModel, the `.html.hbs` `MJML` build artifact, and `.text.hbs` files be in the same directory.\n\n1. Run `npm run build:hbs`.\n2. Copy built `*.html.hbs` files from the build directory to the directory that the `IMailer` expects. All files in the `Core/MailTemplates/Mjml/out` directory should be copied to the `/src/Core/MailTemplates/Mjml` directory, ensuring that the files are in the same directory as the corresponding ViewModels. If a shared component is modified it is important to copy and overwrite all files in that directory to capture changes in the `*.html.hbs` files.\n3. Run code that will send the email.\n\nThe minified `html.hbs` artifacts are deliverables and must be placed into the correct `/src/Core/MailTemplates/Mjml` directories in order to be used by `IMailer` implementations, see step 2 above.\n\n### Testing the mail template with `IMailService`\n\n> [!WARNING]  \n> The `IMailService` has been deprecated. The [IMailer](#testing-the-mail-template-with-imailer) should be used instead.\n\nAfter the email is developed from the [initial step](#developing-the-mail-template), make sure the email `{{variables}}` are populated properly by running it through an `IMailService` implementation.\n\n1. Run `npm run build:hbs`\n2. Copy built `*.html.hbs` files from the build directory to a location the mail service can consume them.\n   - All files in the `Core/MailTemplates/Mjml/out` directory should be copied to the `src/Core/MailTemplates/Handlebars/MJML` directory. If a shared component is modified it is important to copy and overwrite all files in that directory to capture changes in the `*.html.hbs`.\n3. Run code that will send the email.\n\nThe minified `html.hbs` artifacts are deliverables and must be placed into the correct `src/Core/MailTemplates/Handlebars/` directories in order to be used by `IMailService` implementations, see 2.1 above.\n\n### Custom tags\n\nThere is currently a `mj-bw-hero` tag you can use within your `*.mjml` templates. This is a good example of how to create a component that takes in attribute values allowing us to be more DRY in our development of emails. Since the attribute's input is a string we are able to define whatever we need into the component, in this case `mj-bw-hero`.\n\nIn order to view the custom component you have written you will need to include it in the `.mjmlconfig` and reference it in a `.mjml` template file.\n\n```html\n<!-- Custom component implementation-->\n<mj-bw-hero\n  img-src=\"https://assets.bitwarden.com/email/v1/business.png\"\n  title=\"Verify your email to access this Bitwarden Send\"\n/>\n```\n\nAttributes in custom components are defined by the developer. They can be required or optional depending on implementation. See the official `MJML` [documentation](https://documentation.mjml.io/#components) for more information.\n\n```js\nstatic allowedAttributes = {\n  \"img-src\": \"string\", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area\n  title: \"string\", // REQUIRED: large text stating primary purpose of the email\n  \"button-text\": \"string\", // OPTIONAL: text to display in the button\n  \"button-url\": \"string\", // OPTIONAL: URL to navigate to when the button is clicked\n  \"sub-title\": \"string\", // OPTIONAL: smaller text providing additional context for the title\n};\n\nstatic defaultAttributes = {};\n```\n\nCustom components, such as `mj-bw-hero`, must be defined in the `.mjmlconfig` in order for them to be compiled and rendered properly in the templates.\n\n```json\n{\n  \"packages\": [\"components/mj-bw-hero\"]\n}\n```\n\n### `mj-include`\n\nYou are also able to reference other more static `MJML` templates in your `MJML` file simply by referencing the file within the `MJML` template.\n\n```html\n<!-- Example of reference to mjml template -->\n<mj-wrapper padding=\"5px 20px 10px 20px\">\n  <mj-include path=\"../../components/learn-more-footer.mjml\" />\n</mj-wrapper>\n```\n\n#### `head.mjml`\n\nCurrently we include the `head.mjml` file in all `MJML` templates as it contains shared styling and formatting that ensures consistency across all email implementations.\n\nIn the future we may deviate from this practice to support different layouts. At that time we will modify the docs with direction.\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/build.js",
    "content": "const mjml2html = require(\"mjml\");\nconst { registerComponent } = require(\"mjml-core\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst glob = require(\"glob\");\n\n// Parse command line arguments\nconst args = process.argv.slice(2); // Remove 'node' and script path\n\n// Parse flags\nconst flags = {\n  minify: args.includes(\"--minify\") || args.includes(\"-m\"),\n  watch: args.includes(\"--watch\") || args.includes(\"-w\"),\n  hbs: args.includes(\"--hbs\") || args.includes(\"-h\"),\n  trace: args.includes(\"--trace\") || args.includes(\"-t\"),\n  clean: args.includes(\"--clean\") || args.includes(\"-c\"),\n  help: args.includes(\"--help\"),\n};\n\n// Use __dirname to get absolute paths relative to the script location\nconst config = {\n  inputDir: path.join(__dirname, \"emails\"),\n  outputDir: path.join(__dirname, \"out\"),\n  minify: flags.minify,\n  validationLevel: \"strict\",\n  hbsOutput: flags.hbs,\n};\n\n// Debug output\nif (flags.trace) {\n  console.log(\"[DEBUG] Script location:\", __dirname);\n  console.log(\"[DEBUG] Input directory:\", config.inputDir);\n  console.log(\"[DEBUG] Output directory:\", config.outputDir);\n}\n\n// Ensure output directory exists\nif (!fs.existsSync(config.outputDir)) {\n  fs.mkdirSync(config.outputDir, { recursive: true });\n  if (flags.trace) {\n    console.log(\"[INFO] Created output directory:\", config.outputDir);\n  }\n}\n\n// Find all MJML files with absolute paths, excluding components directories\nconst mjmlFiles = glob.sync(`${config.inputDir}/**/*.mjml`, {\n  ignore: [\"**/components/**\"],\n});\n\nconsole.log(`\\n[INFO] Found ${mjmlFiles.length} MJML file(s) to compile...`);\n\nif (mjmlFiles.length === 0) {\n  console.error(\"[ERROR] No MJML files found!\");\n  console.error(\"[ERROR] Looked in:\", config.inputDir);\n  console.error(\n    \"[ERROR] Does this directory exist?\",\n    fs.existsSync(config.inputDir),\n  );\n  process.exit(1);\n}\n\n// Compile each MJML file\nlet successCount = 0;\nlet errorCount = 0;\n\nmjmlFiles.forEach((filePath) => {\n  try {\n    const mjmlContent = fs.readFileSync(filePath, \"utf8\");\n    const fileName = path.basename(filePath, \".mjml\");\n    const relativePath = path.relative(config.inputDir, filePath);\n\n    console.log(`\\n[BUILD] Compiling: ${relativePath}`);\n\n    // Compile MJML to HTML\n    const result = mjml2html(mjmlContent, {\n      minify: config.minify,\n      validationLevel: config.validationLevel,\n      filePath: filePath, // Important: tells MJML where the file is for resolving includes\n      mjmlConfigPath: __dirname, // Point to the directory with .mjmlconfig\n    });\n\n    // Check for errors\n    if (result.errors.length > 0) {\n      console.error(`[ERROR] Failed to compile ${fileName}.mjml:`);\n      result.errors.forEach((err) =>\n        console.error(`        ${err.formattedMessage}`),\n      );\n      errorCount++;\n      return;\n    }\n\n    // Calculate output path preserving directory structure\n    const relativeDir = path.dirname(relativePath);\n    const outputDir = path.join(config.outputDir, relativeDir);\n\n    // Ensure subdirectory exists\n    if (!fs.existsSync(outputDir)) {\n      fs.mkdirSync(outputDir, { recursive: true });\n    }\n\n    const outputExtension = config.hbsOutput ? \".html.hbs\" : \".html\";\n    const outputPath = path.join(outputDir, `${fileName}${outputExtension}`);\n    fs.writeFileSync(outputPath, result.html);\n\n    console.log(\n      `[OK] Built: ${fileName}.mjml → ${path.relative(__dirname, outputPath)}`,\n    );\n    successCount++;\n\n    // Log warnings if any\n    if (result.warnings && result.warnings.length > 0) {\n      console.warn(`[WARN] Warnings for ${fileName}.mjml:`);\n      result.warnings.forEach((warn) =>\n        console.warn(`       ${warn.formattedMessage}`),\n      );\n    }\n  } catch (error) {\n    console.error(`[ERROR] Exception processing ${path.basename(filePath)}:`);\n    console.error(`        ${error.message}`);\n    errorCount++;\n  }\n});\n\nconsole.log(`\\n[SUMMARY] Compilation complete!`);\nconsole.log(`          Success: ${successCount}`);\nconsole.log(`          Failed:  ${errorCount}`);\nconsole.log(`          Output:  ${config.outputDir}`);\n\nif (errorCount > 0) {\n  process.exit(1);\n}\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/components/footer.mjml",
    "content": "<mj-section padding=\"5px 20px 10px 20px\">\n  <mj-column>\n    <mj-social icon-size=\"24px\" inner-padding=\"8px\" padding=\"0\">\n      <mj-social-element\n        href=\"https://x.com/bitwarden\"\n        src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\"\n      ></mj-social-element>\n\n      <mj-social-element\n        href=\"https://www.reddit.com/r/Bitwarden/\"\n        src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\"\n      ></mj-social-element>\n\n      <mj-social-element\n        href=\"https://community.bitwarden.com/\"\n        src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\"\n      ></mj-social-element>\n\n      <mj-social-element\n        href=\"https://github.com/bitwarden\"\n        src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\"\n      ></mj-social-element>\n\n      <mj-social-element\n        href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\"\n        src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\"\n      ></mj-social-element>\n\n      <mj-social-element\n        href=\"https://www.linkedin.com/company/bitwarden1/\"\n        src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\"\n      ></mj-social-element>\n\n      <mj-social-element\n        href=\"https://www.facebook.com/bitwarden/\"\n        src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\"\n      ></mj-social-element>\n    </mj-social>\n\n    <mj-text align=\"center\" font-size=\"12px\" line-height=\"16px\" color=\"#5A6D91\">\n      <p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br />\n        <a\n          href=\"https://bitwarden.com/\"\n          style=\"text-decoration: none; color: #175ddc; font-weight: 400\"\n          >bitwarden.com</a\n        >\n        |\n        <a\n          href=\"https://bitwarden.com/help/emails-from-bitwarden/\"\n          style=\"text-decoration: none; color: #175ddc; font-weight: 400\"\n          >Learn why we include this</a\n        >\n      </p>\n    </mj-text>\n  </mj-column>\n</mj-section>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/components/head.mjml",
    "content": "<mj-attributes>\n  <mj-all\n    font-family=\"'Helvetica Neue', Helvetica, Arial, sans-serif\"\n    font-size=\"16px\"\n  />\n  <mj-button background-color=\"#175ddc\" />\n  <mj-text color=\"#1B2029\" />\n  <mj-body background-color=\"#e6e9ef\" width=\"660px\" />\n</mj-attributes>\n<mj-style inline=\"inline\">\n  .link {\n    text-decoration: none;\n    color: #175ddc;\n    font-weight: 600;\n  }\n</mj-style>\n<mj-style>\n  .border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n</mj-style>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/components/logo.mjml",
    "content": "<mj-section>\n  <mj-column>\n    <mj-image\n      align=\"center\"\n      padding=\"10px 25px\"\n      src=\"https://bitwarden.com/images/logo-horizontal-blue.png\"\n      width=\"250px\"\n      height=\"39px\"\n    ></mj-image>\n  </mj-column>\n</mj-section>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/components/mj-bw-hero.js",
    "content": "const { BodyComponent } = require(\"mjml-core\");\nclass MjBwHero extends BodyComponent {\n  static dependencies = {\n    // Tell the validator which tags are allowed as our component's parent\n    \"mj-column\": [\"mj-bw-hero\"],\n    \"mj-wrapper\": [\"mj-bw-hero\"],\n    // Tell the validator which tags are allowed as our component's children\n    \"mj-bw-hero\": [],\n  };\n\n  static allowedAttributes = {\n    \"img-src\": \"string\", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area\n    title: \"string\", // REQUIRED: large text stating primary purpose of the email\n    \"button-text\": \"string\", // OPTIONAL: text to display in the button\n    \"button-url\": \"string\", // OPTIONAL: URL to navigate to when the button is clicked\n    \"sub-title\": \"string\", // OPTIONAL: smaller text providing additional context for the title\n  };\n\n  static defaultAttributes = {};\n\n  componentHeadStyle = (breakpoint) => {\n    return `\n      @media only screen and (max-width:${breakpoint}) {\n        .mj-bw-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    `;\n  };\n\n  render() {\n    const buttonElement =\n      this.getAttribute(\"button-text\") && this.getAttribute(\"button-url\")\n        ? `<mj-button\n            href=\"${this.getAttribute(\"button-url\")}\"\n            background-color=\"#fff\"\n            color=\"#1A41AC\"\n            border-radius=\"20px\"\n            align=\"left\"\n          >\n              ${this.getAttribute(\"button-text\")}\n            </mj-button\n          >`\n        : \"\";\n    const subTitleElement = this.getAttribute(\"sub-title\")\n      ? `<mj-text color=\"#fff\" padding-top=\"0\" padding-bottom=\"0\">\n            <h2 style=\"font-weight: normal; font-size: 16px; line-height: 0px\">\n              ${this.getAttribute(\"sub-title\")}\n            </h2>\n          </mj-text>`\n      : \"\";\n\n    return this.renderMJML(\n      `\n      <mj-section\n        full-width=\"full-width\"\n        background-color=\"#175ddc\"\n        border-radius=\"4px 4px 0px 0px\"\n      >\n        <mj-column width=\"70%\">\n          <mj-image\n            align=\"left\"\n            src=\"https://bitwarden.com/images/logo-horizontal-white.png\"\n            width=\"150px\"\n            height=\"30px\"\n          ></mj-image>\n          <mj-text color=\"#fff\" padding-top=\"0\" padding-bottom=\"0\">\n            <h1 style=\"font-weight: normal; font-size: 24px; line-height: 32px\">\n              ${this.getAttribute(\"title\")}\n            </h1>\n            ` +\n        subTitleElement +\n        `\n          </mj-text>` +\n        buttonElement +\n        `\n        </mj-column>\n        <mj-column width=\"30%\" vertical-align=\"bottom\">\n          <mj-image\n            src=\"${this.getAttribute(\"img-src\")}\"\n            alt=\"\"\n            width=\"155px\"\n            padding=\"0px\"\n            css-class=\"mj-bw-hero-responsive-img\"\n            />\n        </mj-column>\n      </mj-section>\n    `,\n    );\n  }\n}\n\nmodule.exports = MjBwHero;\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/components/mj-bw-icon-row.js",
    "content": "const { BodyComponent } = require(\"mjml-core\");\n\nconst BODY_TEXT_STYLES = `\n  font-family=\"Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif\"\n  font-size=\"16px\"\n  font-weight=\"400\"\n  line-height=\"24px\"\n`;\n\nclass MjBwIconRow extends BodyComponent {\n  static dependencies = {\n    \"mj-column\": [\"mj-bw-icon-row\"],\n    \"mj-wrapper\": [\"mj-bw-icon-row\"],\n    \"mj-bw-icon-row\": [],\n  };\n\n  static allowedAttributes = {\n    \"icon-src\": \"string\",\n    \"icon-alt\": \"string\",\n    \"head-url-text\": \"string\",\n    \"head-url\": \"string\",\n    text: \"string\",\n    \"foot-url-text\": \"string\",\n    \"foot-url\": \"string\",\n  };\n\n  static defaultAttributes = {};\n\n  headStyle = (breakpoint) => {\n    return `\n      @media only screen and (max-width:${breakpoint}) {\n        .mj-bw-icon-row-text {\n          padding-left: 5px !important;\n          line-height: 20px;\n        }\n        .mj-bw-icon-row {\n          padding: 10px 15px;\n          width: fit-content !important;\n        }\n      }\n    `;\n  };\n\n  render() {\n    const headAnchorElement =\n      this.getAttribute(\"head-url-text\") && this.getAttribute(\"head-url\")\n        ? `\n            <mj-text css-class=\"mj-bw-icon-row-text\" padding=\"5px 10px 0px 10px\" ${BODY_TEXT_STYLES}>\n                <a href=\"${this.getAttribute(\"head-url\")}\" class=\"link\">\n                    ${this.getAttribute(\"head-url-text\")}\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\"\n                        alt=\"External Link Icon\"\n                        width=\"16px\"\n                        style=\"vertical-align: middle;\"\n                      />\n                    </span>\n                  </a>\n            </mj-text>`\n        : \"\";\n\n    const footAnchorElement =\n      this.getAttribute(\"foot-url-text\") && this.getAttribute(\"foot-url\")\n        ? `<mj-text css-class=\"mj-bw-icon-row-text\" padding=\"5px 10px 0px 10px\" ${BODY_TEXT_STYLES}>\n                <a href=\"${this.getAttribute(\"foot-url\")}\" class=\"link\">\n                    ${this.getAttribute(\"foot-url-text\")}\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\"\n                        alt=\"External Link Icon\"\n                        width=\"16px\"\n                        style=\"vertical-align: middle;\"\n                      />\n                    </span>\n              </a>\n          </mj-text>`\n        : \"\";\n\n    return this.renderMJML(\n      `\n      <mj-section background-color=\"#fff\" padding=\"10px 10px 10px 10px\">\n        <mj-group css-class=\"mj-bw-icon-row\">\n          <mj-column width=\"15%\" vertical-align=\"top\">\n            <mj-image\n              src=\"${this.getAttribute(\"icon-src\")}\"\n              alt=\"${this.getAttribute(\"icon-alt\")}\"\n              width=\"48px\"\n              padding=\"0px\"\n              border-radius=\"8px\"\n            />\n          </mj-column>\n          <mj-column width=\"85%\" vertical-align=\"top\">\n              ${headAnchorElement}\n              <mj-text css-class=\"mj-bw-icon-row-text\" padding=\"5px 10px 0px 10px\" ${BODY_TEXT_STYLES}>\n                ${this.getAttribute(\"text\")}\n              </mj-text>\n              ${footAnchorElement}\n          </mj-column>\n        </mj-group>\n      </mj-section>\n    `,\n    );\n  }\n}\n\nmodule.exports = MjBwIconRow;\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/components/mj-bw-learn-more-footer.js",
    "content": "const { BodyComponent } = require(\"mjml-core\");\nclass MjBwLearnMoreFooter extends BodyComponent {\n  static dependencies = {\n    // Tell the validator which tags are allowed as our component's parent\n    \"mj-column\": [\"mj-bw-learn-more-footer\"],\n    \"mj-wrapper\": [\"mj-bw-learn-more-footer\"],\n    // Tell the validator which tags are allowed as our component's children\n    \"mj-bw-learn-more-footer\": [],\n  };\n\n  static allowedAttributes = {};\n\n  static defaultAttributes = {};\n\n  componentHeadStyle = (breakpoint) => {\n    return `\n      @media only screen and (max-width:${breakpoint}) {\n        .mj-bw-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    `;\n  };\n\n  render() {\n    return this.renderMJML(\n      `\n      <mj-section border-radius=\"0px 0px 4px 4px\" background-color=\"#F3F6F9\">\n        <mj-column width=\"70%\">\n          <mj-text line-height=\"24px\">\n            <p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin-top: 0px;\">\n              Learn more about Bitwarden\n            </p>\n            Find user guides, product documentation, and videos on the\n            <a href=\"https://bitwarden.com/help/\" class=\"link\"> Bitwarden Help Center</a>.\n          </mj-text>\n        </mj-column>\n        <mj-column width=\"30%\">\n          <mj-image\n            src=\"https://assets.bitwarden.com/email/v1/spot-community.png\"\n            css-class=\"mj-bw-learn-more-footer-responsive-img\"\n            width=\"94px\"\n          />\n        </mj-column>\n      </mj-section>\n    `,\n    );\n  }\n}\n\nmodule.exports = MjBwLearnMoreFooter;\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/components/mj-bw-simple-hero.js",
    "content": "const { BodyComponent } = require(\"mjml-core\");\n\nclass MjBwSimpleHero extends BodyComponent {\n  static dependencies = {\n    // Tell the validator which tags are allowed as our component's parent\n    \"mj-column\": [\"mj-bw-simple-hero\"],\n    \"mj-wrapper\": [\"mj-bw-simple-hero\"],\n    // Tell the validator which tags are allowed as our component's children\n    \"mj-bw-simple-hero\": [],\n  };\n\n  static allowedAttributes = {};\n\n  static defaultAttributes = {};\n\n  render() {\n    return this.renderMJML(\n      `\n      <mj-section\n        full-width=\"full-width\"\n        background-color=\"#175ddc\"\n        border-radius=\"4px 4px 0px 0px\"\n        padding=\"20px 20px\"\n      >\n        <mj-column width=\"100%\">\n          <mj-image\n            align=\"left\"\n            src=\"https://bitwarden.com/images/logo-horizontal-white.png\"\n            width=\"150px\"\n            height=\"30px\"\n            padding=\"10px 5px\"\n          ></mj-image>\n        </mj-column>\n      </mj-section>\n    `,\n    );\n  }\n}\n\nmodule.exports = MjBwSimpleHero;\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-auto-confirm-enabled.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n  <mj-body>\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 24px 0px 24px\">\n      <mj-bw-simple-hero />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"0px 25px\">\n      <mj-section background-color=\"#fff\" padding=\"24px 0px\">\n        <mj-column padding=\"0px 25px\">\n          <mj-text padding=\"0px 0px 24px 0px\" font-family=\"roboto\" font-weight=\"700\" line-height=\"24px\">\n            Automatic user confirmation is now available!\n          </mj-text>\n          <mj-text padding=\"0px 0px 24px 0px\" font-family=\"roboto\" font-weight=\"400\" line-height=\"24px\">\n            A new policy is available for your organization. It allows new users\n            to be automatically confirmed while an admin’s device is unlocked.\n            Log in to the web app to turn on the policy.\n          </mj-text>\n          <mj-button\n            padding=\"0px 0px 24px 0px\"\n            inner-padding=\"12px 24px\"\n            border-radius=\"20px\"\n            font-weight=\"600\"\n            align=\"left\"\n            href=\"{{WebVaultUrl}}\"\n            >Log in</mj-button\n          >\n          <mj-text\n            padding=\"0px\"\n            font-family=\"roboto\"\n            line-height=\"16px\"\n            font-weight=\"700\"\n            font-size=\"13px\"\n          >\n            <a\n              class=\"link\"\n              href=\"https://bitwarden.com/help/automatic-confirmation/\"\n              >Learn more about this policy</a\n            >\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-wrapper padding-top=\"15px\">\n        <mj-include path=\"../../../components/footer.mjml\" />\n    </mj-wrapper>\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-enterprise-teams.mjml",
    "content": "<mjml>\n  <mj-head>\n    <!-- Include shared head styles -->\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body>\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-ac-hero\n        title=\"You can now share passwords with members of <b>{{OrganizationName}}!</b>\"\n        img-src=\"https://assets.bitwarden.com/email/v1/ac-spot-enterprise.png\"\n        button-text=\"<b>Log in</b>\"\n        button-url=\"{{WebVaultUrl}}\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 8px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"10px 10px 16px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"15px 15px 0px 15px\"\n          >\n            As a member of <b>{{ OrganizationName }}</b\n            >:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-ac-icon-row-without-bulletins\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-enterprise.png\"\n        icon-alt=\"Organization Icon\"\n        text=\"Your account is owned by {{OrganizationName}} and is subject to their security and management policies.\"\n      />\n      <mj-bw-ac-icon-row-without-bulletins\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\"\n        icon-alt=\"Share Icon\"\n        text=\"You can easily access and share passwords with your team.\"\n        foot-url-text=\"Share passwords in Bitwarden\"\n        foot-url=\"https://bitwarden.com/help/sharing\"\n      />\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"8px 20px 10px 20px\">\n      <mj-bw-ac-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationConfirmation/organization-confirmation-family-free.mjml",
    "content": "<mjml>\n  <mj-head>\n    <!-- Include shared head styles -->\n    <mj-include path=\"../../../components/head.mjml\" />\n\n    <!-- Include admin console shared styles --><mj-include\n      path=\"../components/admin-console-head.mjml\"\n    />\n  </mj-head>\n\n  <mj-body>\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-ac-hero\n        title=\"You can now share passwords with members of <b>{{OrganizationName}}!</b>\"\n        img-src=\"https://assets.bitwarden.com/email/v1/ac-spot-family.png\"\n        button-text=\"<b>Log in</b>\"\n        button-url=\"{{WebVaultUrl}}\"\n      >\n      </mj-bw-ac-hero>\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 8px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"10px 10px 16px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"15px 15px 0px 15px\"\n          >\n            As a member of <b>{{ OrganizationName }}</b\n            >:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-ac-icon-row-without-bulletins\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\"\n        icon-alt=\"Group Users Icon\"\n        text=\"You can access passwords {{OrganizationName}} has shared with you.\"\n      >\n      </mj-bw-ac-icon-row-without-bulletins>\n      <mj-bw-ac-icon-row-without-bulletins\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\"\n        icon-alt=\"Share Icon\"\n        text=\"You can easily share passwords with friends, family, or coworkers.\"\n        foot-url-text=\"Share passwords in Bitwarden\"\n        foot-url=\"https://bitwarden.com/help/sharing\"\n      >\n      </mj-bw-ac-icon-row-without-bulletins>\n    </mj-wrapper>\n\n    <!-- Download Mobile Apps Section -->\n    <mj-wrapper padding=\"8px 20px 10px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"32px 10px 0px 25px\">\n        <mj-column>\n          <mj-text\n            font-size=\"18px\"\n            font-weight=\"500\"\n            line-height=\"24px\"\n            padding=\"0 0 16px 0\"\n          >\n            Download Bitwarden on all devices\n          </mj-text>\n\n          <mj-text mj-class=\"ac-text\" padding=\"0 0 24px 0\">\n            Already using the\n            <a href=\"https://bitwarden.com/download/\" class=\"link\"\n              >browser extension</a\n            >? Download the Bitwarden mobile app from the\n            <a\n              href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\"\n              class=\"link\"\n              >App Store</a\n            >\n            or\n            <a\n              href=\"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden\"\n              class=\"link\"\n              >Google Play</a\n            >\n            to quickly save logins and autofill forms on the go.\n          </mj-text>\n        </mj-column>\n      </mj-section>\n\n      <mj-section background-color=\"#fff\" padding=\"0 10px 32px 25px\">\n        <mj-group>\n          <mj-column width=\"159px\">\n            <mj-image\n              css-class=\"hide-mobile\"\n              href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\"\n              src=\"https://assets.bitwarden.com/email/v1/ac-apple-store.png\"\n              alt=\"Download on the App Store\"\n              width=\"135px\"\n              height=\"40px\"\n              padding=\"0 24px 0 0\"\n            />\n          </mj-column>\n          <mj-column width=\"140px\">\n            <mj-image\n              css-class=\"hide-mobile\"\n              href=\"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden\"\n              src=\"https://assets.bitwarden.com/email/v1/ac-google-play.png\"\n              alt=\"Get it on Google Play\"\n              width=\"140px\"\n              height=\"40px\"\n              padding=\"0\"\n            />\n          </mj-column>\n        </mj-group>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"8px 20px 10px 20px\">\n      <mj-bw-ac-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-enterprise-teams-existing-user.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-ac-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/spot-enterprise.png\"\n        title=\"<b>{{OrganizationName}}</b> invited you to join them on Bitwarden\"\n        button-text=\"<b>{{ButtonText}}</b>\"\n        button-url=\"{{Url}}\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 8px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"10px 10px 16px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"15px 15px 0px 15px\"\n          >\n            <b>{{ OrganizationName }}</b> is rolling out Bitwarden to increase\n            security and protect your sensitive data. Once you accept this\n            invitation, you can:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\"\n        icon-alt=\"Store Icon\"\n        text=\"Store logins securely so you never forget your passwords.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\"\n        icon-alt=\"Autofill Icon\"\n        text=\"Sign in to accounts quickly by filling passwords with one click.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\"\n        icon-alt=\"Share Icon\"\n        text=\"Share logins easily with your team.\"\n      />\n      <mj-section background-color=\"#fff\" padding=\"0px 10px 12px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"12px\"\n            line-height=\"16px\"\n            color=\"#1B2029\"\n            padding=\"0px 15px 15px 15px\"\n          >\n            {{#if InviterEmail}}\n            This invitation was sent by\n            <a\n              href=\"mailto:{{InviterEmail}}\"\n              style=\"color: #175ddc; text-decoration: none\"\n              >{{ InviterEmail }}</a\n            >\n            and expires {{ ExpirationDate }}\n            {{else}}\n            This invitation expires {{ ExpirationDate }}\n            {{/if}}\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Policy Warning Section -->\n    <mj-wrapper padding=\"8px 20px 8px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"10px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"18px\"\n            line-height=\"28px\"\n            font-weight=\"500\"\n            padding=\"15px 15px 8px 15px\"\n          >\n            Your existing account will be owned by {{ OrganizationName }}\n          </mj-text>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"0px 15px 15px 15px\"\n          >\n            By accepting this invitation, your account ({{ Email }}) will be\n            owned by <b>{{ OrganizationName }}</b> and will be subject to their\n            security and management policies. Contact your administrator with\n            any questions or concerns.\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"8px 20px 10px 20px\">\n      <mj-bw-ac-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-enterprise-teams-new-user.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-ac-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/spot-enterprise.png\"\n        title=\"<b>{{OrganizationName}}</b> set up a Bitwarden password manager account for you.\"\n        button-text=\"<b>{{ButtonText}}</b>\"\n        button-url=\"{{Url}}\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 8px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"10px 10px 16px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"15px 15px 0px 15px\"\n          >\n            <b>{{ OrganizationName }}</b> is rolling out Bitwarden to increase\n            security and protect your sensitive data. Once you finish account\n            setup, you can:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\"\n        icon-alt=\"Store Icon\"\n        text=\"Store logins securely so you never forget your passwords.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\"\n        icon-alt=\"Autofill Icon\"\n        text=\"Sign in to accounts quickly by filling passwords with one click.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\"\n        icon-alt=\"Share Icon\"\n        text=\"Share logins easily with your team.\"\n      />\n      <mj-section background-color=\"#fff\" padding=\"0px 10px 12px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"12px\"\n            line-height=\"16px\"\n            color=\"#1B2029\"\n            padding=\"0px 15px 15px 15px\"\n          >\n            {{#if InviterEmail}}\n            This invitation was sent by\n            <a\n              href=\"mailto:{{InviterEmail}}\"\n              style=\"color: #175ddc; text-decoration: none\"\n              >{{ InviterEmail }}</a\n            >\n            and expires {{ ExpirationDate }}\n            {{else}}\n            This invitation expires {{ ExpirationDate }}\n            {{/if}}\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"8px 20px 10px 20px\">\n      <mj-bw-ac-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-families-existing-user.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-ac-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/spot-family-homes.png\"\n        title=\"<b>{{OrganizationName}}</b> invited you to join them on Bitwarden\"\n        button-text=\"<b>{{ButtonText}}</b>\"\n        button-url=\"{{Url}}\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 8px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"10px 10px 16px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"15px 15px 0px 15px\"\n          >\n            <b>{{ OrganizationName }}</b> is using Bitwarden to simplify\n            password sharing and protect your sensitive data. Once you accept\n            this invitation, you can:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\"\n        icon-alt=\"Store Icon\"\n        text=\"Store logins securely so you never forget your passwords.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\"\n        icon-alt=\"Autofill Icon\"\n        text=\"Sign in to accounts quickly by filling passwords with one click.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\"\n        icon-alt=\"Share Icon\"\n        text=\"Share logins easily with your friends, family, or coworkers.\"\n      />\n      <mj-section background-color=\"#fff\" padding=\"0px 10px 12px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"12px\"\n            line-height=\"16px\"\n            color=\"#1B2029\"\n            padding=\"0px 15px 15px 15px\"\n          >\n            {{#if InviterEmail}}\n            This invitation was sent by\n            <a\n              href=\"mailto:{{InviterEmail}}\"\n              style=\"color: #175ddc; text-decoration: none\"\n              >{{ InviterEmail }}</a\n            >\n            and expires {{ ExpirationDate }}\n            {{else}}\n            This invitation expires {{ ExpirationDate }}\n            {{/if}}\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"8px 20px 10px 20px\">\n      <mj-bw-ac-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-families-new-user.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-ac-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/spot-family-homes.png\"\n        title=\"<b>{{OrganizationName}}</b> set up a Bitwarden password manager account for you.\"\n        button-text=\"<b>{{ButtonText}}</b>\"\n        button-url=\"{{Url}}\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 8px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"10px 10px 16px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"15px 15px 0px 15px\"\n          >\n            <b>{{ OrganizationName }}</b> is using Bitwarden to simplify\n            password sharing and protect your sensitive data. Once you finish\n            account setup, you can:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\"\n        icon-alt=\"Store Icon\"\n        text=\"Store logins securely so you never forget your passwords.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\"\n        icon-alt=\"Autofill Icon\"\n        text=\"Sign in to accounts quickly by filling passwords with one click.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\"\n        icon-alt=\"Share Icon\"\n        text=\"Share logins easily with your friends, family, or coworkers.\"\n      />\n      <mj-section background-color=\"#fff\" padding=\"0px 10px 12px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"12px\"\n            line-height=\"16px\"\n            color=\"#1B2029\"\n            padding=\"0px 15px 15px 15px\"\n          >\n            {{#if InviterEmail}}\n            This invitation was sent by\n            <a\n              href=\"mailto:{{InviterEmail}}\"\n              style=\"color: #175ddc; text-decoration: none\"\n              >{{ InviterEmail }}</a\n            >\n            and expires {{ ExpirationDate }}\n            {{else}}\n            This invitation expires {{ ExpirationDate }}\n            {{/if}}\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"8px 20px 10px 20px\">\n      <mj-bw-ac-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/OrganizationInvite/organization-invite-free.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-ac-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/spot-family-homes.png\"\n        title=\"You have been invited to Bitwarden Password Manager\"\n        button-text=\"<b>{{ButtonText}}</b>\"\n        button-url=\"{{Url}}\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 8px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"10px 10px 16px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"15px 15px 0px 15px\"\n          >\n            Bitwarden is a password manager used to simplify password sharing\n            and protect your sensitive data. Once you accept this invitation,\n            you can:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-item-type.png\"\n        icon-alt=\"Store Icon\"\n        text=\"Securely store logins so you never forget your passwords.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\"\n        icon-alt=\"Autofill Icon\"\n        text=\"Sign in to accounts quickly by filling passwords with one click.\"\n      />\n      <mj-bw-ac-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-account-switching-new.png\"\n        icon-alt=\"Share Icon\"\n        text=\"Share logins easily with your friends, family, or coworkers.\"\n      />\n      <mj-section background-color=\"#fff\" padding=\"0px 10px 12px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"12px\"\n            line-height=\"16px\"\n            color=\"#1B2029\"\n            padding=\"0px 15px 15px 15px\"\n          >\n            {{#if InviterEmail}}\n            This invitation was sent by\n            <a\n              href=\"mailto:{{InviterEmail}}\"\n              style=\"color: #175ddc; text-decoration: none\"\n              >{{ InviterEmail }}</a\n            >\n            and expires {{ ExpirationDate }}\n            {{else}}\n            This invitation expires {{ ExpirationDate }}\n            {{/if}}\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"8px 20px 10px 20px\">\n      <mj-bw-ac-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/components/admin-console-head.mjml",
    "content": "<mj-attributes>\n  <mj-all font-family=\"'Helvetica Neue','Inter',Helvetica,Arial,sans-serif\" />\n  <mj-class\n    name=\"ac-text\"\n    font-size=\"16px\"\n    font-weight=\"400\"\n    line-height=\"24px\"\n  />\n</mj-attributes>\n\n<mj-style>\n  @media only screen and (max-width: 480px) {\n    .hide-mobile {\n      display: none !important;\n    }\n  }\n</mj-style>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mj-bw-ac-hero.js",
    "content": "const { BodyComponent } = require(\"mjml-core\");\nclass MjBwAcHero extends BodyComponent {\n  static dependencies = {\n    // Tell the validator which tags are allowed as our component's parent\n    \"mj-column\": [\"mj-bw-ac-hero\"],\n    \"mj-wrapper\": [\"mj-bw-ac-hero\"],\n    // Tell the validator which tags are allowed as our component's children\n    \"mj-bw-ac-hero\": [],\n  };\n\n  static allowedAttributes = {\n    \"img-src\": \"string\", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area\n    title: \"string\", // REQUIRED: large text stating primary purpose of the email\n    \"button-text\": \"string\", // OPTIONAL: text to display in the button\n    \"button-url\": \"string\", // OPTIONAL: URL to navigate to when the button is clicked\n    \"sub-title\": \"string\", // OPTIONAL: smaller text providing additional context for the title\n  };\n\n  static defaultAttributes = {};\n\n  componentHeadStyle = (breakpoint) => {\n    return `\n      @media only screen and (max-width:${breakpoint}) {\n        .mj-bw-ac-hero-responsive-img {\n          display: none !important;\n        }\n      }\n    `;\n  };\n\n  render() {\n    const buttonElement =\n      this.getAttribute(\"button-text\") && this.getAttribute(\"button-url\")\n        ? `<mj-button\n            href=\"${this.getAttribute(\"button-url\")}\"\n            background-color=\"#fff\"\n            color=\"#1A41AC\"\n            border-radius=\"20px\"\n            align=\"left\"\n            inner-padding=\"12px 24px\"\n          >\n            ${this.getAttribute(\"button-text\")}\n            </mj-button\n          >`\n        : \"\";\n    const subTitleElement = this.getAttribute(\"sub-title\")\n      ? `<mj-text color=\"#fff\" padding-top=\"0\" padding-bottom=\"0\">\n            <h2 style=\"font-weight: normal; font-size: 16px; line-height: 0px\">\n              ${this.getAttribute(\"sub-title\")}\n            </h2>\n          </mj-text>`\n      : \"\";\n\n    return this.renderMJML(\n      `\n      <mj-section\n        full-width=\"full-width\"\n        background-color=\"#175ddc\"\n        border-radius=\"4px 4px 0px 0px\"\n      >\n        <mj-column width=\"70%\">\n          <mj-image\n            align=\"left\"\n            src=\"https://bitwarden.com/images/logo-horizontal-white.png\"\n            width=\"150px\"\n            height=\"30px\"\n          ></mj-image>\n          <mj-text color=\"#fff\" padding-top=\"0\" padding-bottom=\"0\">\n            <h1 style=\"font-weight: 400; font-size: 24px; line-height: 32px\">\n              ${this.getAttribute(\"title\")}\n            </h1>\n            ` +\n        subTitleElement +\n        `\n          </mj-text>` +\n        buttonElement +\n        `\n        </mj-column>\n        <mj-column width=\"30%\" vertical-align=\"bottom\">\n          <mj-image\n            src=\"${this.getAttribute(\"img-src\")}\"\n            alt=\"\"\n            width=\"155px\"\n            padding=\"0px 20px 0px 0px\"\n            align=\"right\"\n            css-class=\"mj-bw-ac-hero-responsive-img\"\n            />\n        </mj-column>\n      </mj-section>\n    `,\n    );\n  }\n}\n\nmodule.exports = MjBwAcHero;\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mj-bw-ac-icon-row-without-bulletins.js",
    "content": "const { BodyComponent } = require(\"mjml-core\");\n\nconst BODY_TEXT_STYLES = `\n  font-family=\"'Helvetica Neue', Helvetica, Arial, sans-serif\"\n  font-size=\"16px\"\n  font-weight=\"400\"\n  line-height=\"24px\"\n`;\n\nclass MjBwAcIconRowWithoutBulletins extends BodyComponent {\n  static dependencies = {\n    \"mj-column\": [\"mj-bw-ac-icon-row-without-bulletins\"],\n    \"mj-wrapper\": [\"mj-bw-ac-icon-row-without-bulletins\"],\n    \"mj-bw-ac-icon-row-without-bulletins\": [],\n  };\n\n  static allowedAttributes = {\n    \"icon-src\": \"string\",\n    \"icon-alt\": \"string\",\n    \"head-url-text\": \"string\",\n    \"head-url\": \"string\",\n    text: \"string\",\n    \"foot-url-text\": \"string\",\n    \"foot-url\": \"string\",\n  };\n\n  static defaultAttributes = {};\n\n  headStyle = (breakpoint) => {\n    return `\n      @media only screen and (max-width:${breakpoint}) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n      }\n    `;\n  };\n\n  render() {\n    const headAnchorElement =\n      this.getAttribute(\"head-url-text\") && this.getAttribute(\"head-url\")\n        ? `\n            <mj-text css-class=\"mj-bw-ac-icon-row-text\" padding=\"5px 10px 0px 10px\" ${BODY_TEXT_STYLES}>\n                <a href=\"${this.getAttribute(\"head-url\")}\" class=\"link\">\n                    ${this.getAttribute(\"head-url-text\")}\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\"\n                        alt=\"External Link Icon\"\n                        width=\"16px\"\n                        style=\"vertical-align: middle;\"\n                      />\n                    </span>\n                  </a>\n            </mj-text>`\n        : \"\";\n\n    const footAnchorElement =\n      this.getAttribute(\"foot-url-text\") && this.getAttribute(\"foot-url\")\n        ? `<mj-text css-class=\"mj-bw-ac-icon-row-text\" padding=\"0px\" ${BODY_TEXT_STYLES}>\n                <a href=\"${this.getAttribute(\"foot-url\")}\" class=\"link\">\n                    ${this.getAttribute(\"foot-url-text\")}\n              </a>\n          </mj-text>`\n        : \"\";\n\n    return this.renderMJML(\n      `\n      <mj-section background-color=\"#fff\" padding=\"0px 10px 24px 10px\">\n        <mj-group css-class=\"mj-bw-ac-icon-row\">\n          <mj-column width=\"15%\" vertical-align=\"middle\" css-class=\"mj-bw-ac-icon-row-icon\">\n            <mj-image\n              src=\"${this.getAttribute(\"icon-src\")}\"\n              alt=\"${this.getAttribute(\"icon-alt\")}\"\n              width=\"48px\"\n              padding=\"0px 10px 0px 5px\"\n              border-radius=\"8px\"\n            />\n          </mj-column>\n          <mj-column width=\"85%\" vertical-align=\"middle\" css-class=\"mj-bw-ac-icon-row-text-column\">\n              ${headAnchorElement}\n              <mj-text css-class=\"mj-bw-ac-icon-row-text\" padding=\"0px 0px 0px 0px\" ${BODY_TEXT_STYLES}>\n                ${this.getAttribute(\"text\")}\n              </mj-text>\n              ${footAnchorElement}\n          </mj-column>\n        </mj-group>\n      </mj-section>\n    `,\n    );\n  }\n}\n\nmodule.exports = MjBwAcIconRowWithoutBulletins;\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mj-bw-ac-icon-row.js",
    "content": "const { BodyComponent } = require(\"mjml-core\");\n\nconst BODY_TEXT_STYLES = `\n  font-family=\"'Helvetica Neue', Helvetica, Arial, sans-serif\"\n  font-size=\"16px\"\n  font-weight=\"400\"\n  line-height=\"24px\"\n`;\n\nclass MjBwAcIconRow extends BodyComponent {\n  static dependencies = {\n    \"mj-column\": [\"mj-bw-ac-icon-row\"],\n    \"mj-wrapper\": [\"mj-bw-ac-icon-row\"],\n    \"mj-bw-ac-icon-row\": [],\n  };\n\n  static allowedAttributes = {\n    \"icon-src\": \"string\",\n    \"icon-alt\": \"string\",\n    \"head-url-text\": \"string\",\n    \"head-url\": \"string\",\n    text: \"string\",\n    \"foot-url-text\": \"string\",\n    \"foot-url\": \"string\",\n  };\n\n  static defaultAttributes = {};\n\n  headStyle = (breakpoint) => {\n    return `\n      @media only screen and (max-width:${breakpoint}) {\n        .mj-bw-ac-icon-row-text {\n          padding-left: 15px !important;\n          padding-right: 15px !important;\n          line-height: 20px;\n        }\n        .mj-bw-ac-icon-row-icon {\n          display: none !important;\n          width: 0 !important;\n          max-width: 0 !important;\n        }\n        .mj-bw-ac-icon-row-text-column {\n          width: 100% !important;\n        }\n        .mj-bw-ac-icon-row-bullet {\n          display: block !important;\n        }\n        .mj-bw-ac-icon-row-text-inline {\n          display: none !important;\n        }\n      }\n    `;\n  };\n\n  render() {\n    const headAnchorElement =\n      this.getAttribute(\"head-url-text\") && this.getAttribute(\"head-url\")\n        ? `\n            <mj-text css-class=\"mj-bw-ac-icon-row-text\" padding=\"5px 10px 0px 10px\" ${BODY_TEXT_STYLES}>\n                <a href=\"${this.getAttribute(\"head-url\")}\" class=\"link\">\n                    ${this.getAttribute(\"head-url-text\")}\n                    <span style=\"text-decoration: none\">\n                      <img src=\"https://assets.bitwarden.com/email/v1/bwi-external-link-16px.png\"\n                        alt=\"External Link Icon\"\n                        width=\"16px\"\n                        style=\"vertical-align: middle;\"\n                      />\n                    </span>\n                  </a>\n            </mj-text>`\n        : \"\";\n\n    const footAnchorElement =\n      this.getAttribute(\"foot-url-text\") && this.getAttribute(\"foot-url\")\n        ? `<mj-text css-class=\"mj-bw-ac-icon-row-text\" padding=\"0px\" ${BODY_TEXT_STYLES}>\n                <a href=\"${this.getAttribute(\"foot-url\")}\" class=\"link\">\n                    ${this.getAttribute(\"foot-url-text\")}\n              </a>\n          </mj-text>`\n        : \"\";\n\n    return this.renderMJML(\n      `\n      <mj-section background-color=\"#fff\" padding=\"0px 10px 24px 10px\">\n        <mj-group css-class=\"mj-bw-ac-icon-row\">\n          <mj-column width=\"15%\" vertical-align=\"middle\" css-class=\"mj-bw-ac-icon-row-icon\">\n            <mj-image\n              src=\"${this.getAttribute(\"icon-src\")}\"\n              alt=\"${this.getAttribute(\"icon-alt\")}\"\n              width=\"48px\"\n              padding=\"0px 10px 0px 5px\"\n              border-radius=\"8px\"\n            />\n          </mj-column>\n          <mj-column width=\"85%\" vertical-align=\"middle\" css-class=\"mj-bw-ac-icon-row-text-column\">\n              ${headAnchorElement}\n              <mj-text css-class=\"mj-bw-ac-icon-row-text\" padding=\"0px 0px 0px 0px\" ${BODY_TEXT_STYLES}>\n                <ul class=\"mj-bw-ac-icon-row-bullet\" style=\"display: none; margin: 0; padding-left: 24px;\"><li>${this.getAttribute(\"text\")}</li></ul>\n                <span class=\"mj-bw-ac-icon-row-text-inline\">${this.getAttribute(\"text\")}</span>\n              </mj-text>\n              ${footAnchorElement}\n          </mj-column>\n        </mj-group>\n      </mj-section>\n    `,\n    );\n  }\n}\n\nmodule.exports = MjBwAcIconRow;\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mj-bw-ac-learn-more-footer.js",
    "content": "const { BodyComponent } = require(\"mjml-core\");\nclass MjBwAcLearnMoreFooter extends BodyComponent {\n  static dependencies = {\n    // Tell the validator which tags are allowed as our component's parent\n    \"mj-column\": [\"mj-bw-ac-learn-more-footer\"],\n    \"mj-wrapper\": [\"mj-bw-ac-learn-more-footer\"],\n    // Tell the validator which tags are allowed as our component's children\n    \"mj-bw-ac-learn-more-footer\": [],\n  };\n\n  static allowedAttributes = {};\n\n  static defaultAttributes = {};\n\n  componentHeadStyle = (breakpoint) => {\n    return `\n      @media only screen and (max-width:${breakpoint}) {\n        .mj-bw-ac-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    `;\n  };\n\n  render() {\n    return this.renderMJML(\n      `\n      <mj-section border-radius=\"0px 0px 4px 4px\" background-color=\"#F3F6F9\" padding=\"14px 10px 14px 10px\">\n        <mj-column width=\"70%\">\n          <mj-text padding=\"10px 15px 10px 15px\">\n            <p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin: 0 0 8px 0;\">\n              Learn more about Bitwarden\n            </p>\n            <p style=\"font-size: 16px; line-height: 24px; margin: 0;\">\n              Find user guides, product documentation, and videos on the\n              <a href=\"https://bitwarden.com/help/\" class=\"link\"> Bitwarden Help Center</a>.\n            </p>\n          </mj-text>\n        </mj-column>\n        <mj-column width=\"30%\" vertical-align=\"bottom\">\n          <mj-image\n            src=\"https://assets.bitwarden.com/email/v1/spot-community.png\"\n            css-class=\"mj-bw-ac-learn-more-footer-responsive-img\"\n            width=\"94px\"\n            padding=\"0px 15px 0px 0px\"\n            align=\"right\"\n          />\n        </mj-column>\n      </mj-section>\n    `,\n    );\n  }\n}\n\nmodule.exports = MjBwAcLearnMoreFooter;\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mj-bw-inviter-info.js",
    "content": "const { BodyComponent } = require(\"mjml-core\");\n\nclass MjBwInviterInfo extends BodyComponent {\n  static dependencies = {\n    \"mj-column\": [\"mj-bw-inviter-info\"],\n    \"mj-wrapper\": [\"mj-bw-inviter-info\"],\n    \"mj-bw-inviter-info\": [],\n  };\n\n  static allowedAttributes = {\n    \"expiration-date\": \"string\", // REQUIRED: Date to display\n    \"email-address\": \"string\", // Optional: Email address to display\n  };\n\n  render() {\n    const emailAddressText = this.getAttribute(\"email-address\")\n      ? `This invitation was sent by <a href=\"mailto:${this.getAttribute(\"email-address\")}\" class=\"link\">${this.getAttribute(\"email-address\")}</a> and expires `\n      : \"This invitation expires \";\n\n    return this.renderMJML(\n      `\n      <mj-section background-color=\"#fff\" padding=\"15px 10px 10px 10px\">\n        <mj-column>\n          <mj-text font-size=\"12px\" line-height=\"24px\" padding=\"10px 15px\">\n            ${emailAddressText + this.getAttribute(\"expiration-date\")}\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      `,\n    );\n  }\n}\n\nmodule.exports = MjBwInviterInfo;\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/AdminConsole/components/mobile-app-download.mjml",
    "content": "<mj-section background-color=\"#fff\" padding=\"30px 30px 10px 30px\">\n  <mj-column>\n    <mj-text\n      font-size=\"18px\"\n      font-weight=\"700\"\n      line-height=\"32px\"\n      padding=\"0 0 15px 0\"\n    >\n      Download Bitwarden on all devices\n    </mj-text>\n    <mj-text font-size=\"15px\" line-height=\"16px\" padding=\"0 0 20px 0\">\n      Already using the\n      <a href=\"https://bitwarden.com/download/\" class=\"link\"\n        >browser extension</a\n      >? Download the Bitwarden mobile app from the\n      <a\n        href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\"\n        class=\"link\"\n        >App Store</a\n      >\n      or\n      <a\n        href=\"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden\"\n        class=\"link\"\n        >Google Play</a\n      >\n      to quickly save logins and autofill forms on the go.\n    </mj-text>\n  </mj-column>\n</mj-section>\n\n<mj-section background-color=\"#fff\" padding=\"0 30px 20px 30px\">\n  <mj-group>\n    <mj-column width=\"120px\" vertical-align=\"middle\">\n      <mj-image\n        href=\"https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744\"\n        src=\"https://assets.bitwarden.com/email/v1/App-store.png\"\n        alt=\"Download on the App Store\"\n        width=\"120px\"\n        padding=\"0\"\n      />\n    </mj-column>\n    <mj-column width=\"150px\" vertical-align=\"middle\">\n      <mj-image\n        href=\"https://play.google.com/store/apps/details?id=com.x8bit.bitwarden\"\n        src=\"https://assets.bitwarden.com/email/v1/google-play-badge.png\"\n        alt=\"Get it on Google Play\"\n        width=\"150px\"\n        padding=\"0 0 0 10px\"\n      />\n    </mj-column>\n  </mj-group>\n</mj-section>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/Auth/Onboarding/welcome-family-user.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/account-fill.png\"\n        title=\"Welcome to Bitwarden!\"\n        sub-title=\"Let’s get you set up to autofill.\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 10px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"15px 10px 10px 10px\">\n        <mj-column>\n          <mj-text font-size=\"16px\" line-height=\"24px\" padding=\"10px 15px\">\n            An administrator from <b>{{ OrganizationName }}</b> will approve you\n            before you can share passwords. While you wait for approval, get\n            started with Bitwarden Password Manager:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-browser-extension.png\"\n        icon-alt=\"Browser Extension Icon\"\n        head-url-text=\"Get the browser extension\"\n        head-url=\"https://bitwarden.com/download/\"\n        text=\"With the Bitwarden extension, you can fill passwords with one click.\"\n      />\n      <mj-bw-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-install.png\"\n        icon-alt=\"Install Icon\"\n        head-url-text=\"Add passwords to your vault\"\n        head-url=\"https://bitwarden.com/help/import-data/\"\n        text=\"Quickly transfer existing passwords to Bitwarden using the importer.\"\n      />\n      <mj-bw-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-devices.png\"\n        icon-alt=\"Devices Icon\"\n        head-url-text=\"Download Bitwarden on all devices\"\n        head-url=\"https://bitwarden.com/download/\"\n        text=\"Take your passwords with you anywhere.\"\n      />\n      <mj-section background-color=\"#fff\" padding=\"0 20px 20px 20px\">\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"5px 20px 10px 20px\">\n      <mj-bw-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/Auth/Onboarding/welcome-individual-user.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/account-fill.png\"\n        title=\"Welcome to Bitwarden!\"\n        sub-title=\"Let’s get you set up to autofill.\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 10px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"15px 10px 10px 10px\">\n        <mj-column>\n          <mj-text font-size=\"16px\" line-height=\"24px\" padding=\"10px 15px\">\n            Follow these simple steps to get up and running with Bitwarden\n            Password Manager:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-browser-extension.png\"\n        icon-alt=\"Browser Extension Icon\"\n        head-url-text=\"Get the browser extension\"\n        head-url=\"https://bitwarden.com/download/\"\n        text=\"With the Bitwarden extension, you can fill passwords with one click.\"\n      />\n      <mj-bw-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-install.png\"\n        icon-alt=\"Install Icon\"\n        head-url-text=\"Add passwords to your vault\"\n        head-url=\"https://bitwarden.com/help/import-data/\"\n        text=\"Quickly transfer existing passwords to Bitwarden using the importer.\"\n      />\n      <mj-bw-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-devices.png\"\n        icon-alt=\"Devices Icon\"\n        head-url-text=\"Download Bitwarden on all devices\"\n        head-url=\"https://bitwarden.com/download/\"\n        text=\"Take your passwords with you anywhere.\"\n      />\n      <mj-section background-color=\"#fff\" padding=\"0 20px 20px 20px\">\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"5px 20px 10px 20px\">\n      <mj-bw-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/Auth/Onboarding/welcome-org-user.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/account-fill.png\"\n        title=\"Welcome to Bitwarden!\"\n        sub-title=\"Let’s get you set up to autofill.\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 10px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"15px 10px 10px 10px\">\n        <mj-column>\n          <mj-text font-size=\"16px\" line-height=\"24px\" padding=\"10px 15px\">\n            An administrator from <b>{{ OrganizationName }}</b> will need to\n            confirm you before you can share passwords. Get started with\n            Bitwarden Password Manager:\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-browser-extension.png\"\n        icon-alt=\"Browser Extension Icon\"\n        head-url-text=\"Get the browser extension\"\n        head-url=\"https://bitwarden.com/download/\"\n        text=\"With the Bitwarden extension, you can fill passwords with one click.\"\n      />\n      <mj-bw-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-install.png\"\n        icon-alt=\"Install Icon\"\n        head-url-text=\"Add passwords to your vault\"\n        head-url=\"https://bitwarden.com/help/import-data/\"\n        text=\"Quickly transfer existing passwords to Bitwarden using the importer.\"\n      />\n      <mj-bw-icon-row\n        icon-src=\"https://assets.bitwarden.com/email/v1/icon-autofill.png\"\n        icon-alt=\"Autofill Icon\"\n        head-url-text=\"Try Bitwarden autofill\"\n        head-url=\"https://bitwarden.com/help/auto-fill-browser/\"\n        text=\"Fill your passwords securely with one click.\"\n      />\n      <mj-section background-color=\"#fff\" padding=\"0 20px 20px 20px\">\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"5px 20px 10px 20px\">\n      <mj-bw-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/Auth/UserFeatures/EmergencyAccess/emergency-access-remove-grantees.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../../components/head.mjml\" />\n  </mj-head>\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 0px 20px\">\n      <mj-bw-hero title=\"\" />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"0px 20px 0px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"0px 10px 0px 10px\">\n        <mj-column>\n          <mj-text font-size=\"16px\" line-height=\"24px\" padding=\"10px 15px\">\n            The following emergency contacts have been removed from your\n            account:\n            <ul>\n              {{#each RemovedGranteeEmails}}\n              <li>{{ this }}</li>\n              {{/each}}\n            </ul>\n            Learn more about\n            <a href=\"{{EmergencyAccessHelpPageUrl}}\">emergency access</a>.\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../components/head.mjml\" />\n    <mj-style>\n      .send-bubble {\n        padding-left: 20px;\n        padding-right: 20px;\n        width: 90% !important;\n      }\n    </mj-style>\n  </mj-head>\n\n  <mj-body css-class=\"border-fix\">\n    <!-- Blue Header Section -->\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 10px 20px\">\n      <mj-bw-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/spot-secure-send-round.png\"\n        title=\"Verify your email to access this Bitwarden Send\"\n      />\n    </mj-wrapper>\n\n    <!-- Main Content -->\n    <mj-wrapper padding=\"5px 20px 10px 20px\">\n      <mj-section background-color=\"#fff\">\n        <mj-column padding=\"0px\">\n          <mj-text> Your verification code is: </mj-text>\n          <mj-text font-size=\"32px\" font-weight=\"500\">\n            {{ Token }}\n          </mj-text>\n          <mj-spacer height=\"20px\" />\n          <mj-text>\n            This code expires in {{ Expiry }} minutes. After that, you'll need\n            to verify your email again.\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-section background-color=\"#fff\" padding=\"0px 0px 20px 0px\">\n        <mj-column\n          css-class=\"send-bubble\"\n          width=\"90%\"\n          inner-background-color=\"#DBE5F6\"\n          inner-border-radius=\"16px\"\n          padding=\"0px\"\n        >\n          <mj-text line-height=\"24px\">\n            <p>\n              Bitwarden Send securely shares sensitive information. Learn more\n              about\n              <a href=\"https://bitwarden.com/help/send\" class=\"link\"\n                >Bitwarden Send</a\n              >\n              or\n              <a href=\"https://bitwarden.com/signup\" class=\"link\">sign up</a>\n              to try it today.\n            </p>\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"5px 20px 10px 20px\">\n      <mj-bw-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/Auth/two-factor.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body background-color=\"#f6f6f6\">\n    <mj-include path=\"../../components/logo.mjml\" />\n\n    <mj-wrapper\n      background-color=\"#fff\"\n      border=\"1px solid #e9e9e9\"\n      css-class=\"border-fix\"\n      padding=\"0\"\n    >\n      <mj-section>\n        <mj-column>\n          <mj-text>\n            <p>\n              Your two-step verification code is: <b>{{ Token }}</b>\n            </p>\n            <p>Use this code to complete logging in with Bitwarden.</p>\n          </mj-text>\n        </mj-column>\n      </mj-section>\n    </mj-wrapper>\n\n    <mj-wrapper>\n      <mj-bw-learn-more-footer />\n    </mj-wrapper>\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/Billing/Renewals/families-2019-renewal.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <!-- Blue Header Section-->\n  <mj-body css-class=\"border-fix\">\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 0px 20px\">\n      <mj-bw-simple-hero />\n    </mj-wrapper>\n\n    <!-- Main Content Section -->\n    <mj-wrapper padding=\"0px 20px 0px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"15px 10px 10px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"10px 15px 15px 15px\"\n          >\n            Your Bitwarden Families subscription renews in 15 days. The price is\n            updating to {{ BaseMonthlyRenewalPrice }}/month, billed annually at\n            {{ BaseAnnualRenewalPrice }} + tax.\n          </mj-text>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"10px 15px 15px 15px\"\n          >\n            As a long time Bitwarden customer, you will receive a one-time\n            {{ DiscountAmount }} loyalty discount for this year's renewal. This\n            renewal will now be billed annually at\n            {{ DiscountedAnnualRenewalPrice }} + tax.\n          </mj-text>\n          <mj-text font-size=\"16px\" line-height=\"24px\" padding=\"10px 15px\">\n            Questions? Contact\n            <a href=\"mailto:support@bitwarden.com\" class=\"link\"\n              >support@bitwarden.com</a\n            >\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-section background-color=\"#fff\" padding=\"0 20px 20px 20px\">\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"0px 20px 10px 20px\">\n      <mj-bw-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/Billing/Renewals/families-2020-renewal.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <!-- Blue Header Section-->\n  <mj-body css-class=\"border-fix\">\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 0px 20px\">\n      <mj-bw-simple-hero />\n    </mj-wrapper>\n\n    <!-- Main Content Section -->\n    <mj-wrapper padding=\"0px 20px 0px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"15px 10px 10px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"10px 15px 15px 15px\"\n          >\n            Your Bitwarden Families subscription renews in 15 days. The price is\n            updating to {{ MonthlyRenewalPrice }}/month, billed annually.\n          </mj-text>\n          <mj-text font-size=\"16px\" line-height=\"24px\" padding=\"10px 15px\">\n            Questions? Contact\n            <a href=\"mailto:support@bitwarden.com\" class=\"link\"\n              >support@bitwarden.com</a\n            >\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-section background-color=\"#fff\" padding=\"0 20px 20px 20px\">\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"0px 20px 10px 20px\">\n      <mj-bw-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/Billing/Renewals/premium-renewal.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../../../components/head.mjml\" />\n  </mj-head>\n\n  <!-- Blue Header Section-->\n  <mj-body css-class=\"border-fix\">\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px 0px 20px\">\n      <mj-bw-simple-hero />\n    </mj-wrapper>\n\n    <!-- Main Content Section -->\n    <mj-wrapper padding=\"0px 20px 0px 20px\">\n      <mj-section background-color=\"#fff\" padding=\"15px 10px 10px 10px\">\n        <mj-column>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"10px 15px 15px 15px\"\n          >\n            Your Bitwarden Premium subscription renews in 15 days. The price is\n            updating to {{ BaseMonthlyRenewalPrice }}/month, billed annually.\n          </mj-text>\n          <mj-text\n            font-size=\"16px\"\n            line-height=\"24px\"\n            padding=\"10px 15px 15px 15px\"\n          >\n            As an existing Bitwarden customer, you will receive a one-time\n            {{ DiscountAmount }} loyalty discount for this year's renewal. This\n            renewal will now be billed annually at\n            {{ DiscountedAnnualRenewalPrice }} + tax.\n          </mj-text>\n          <mj-text font-size=\"16px\" line-height=\"24px\" padding=\"10px 15px\">\n            Questions? Contact\n            <a href=\"mailto:support@bitwarden.com\" class=\"link\"\n              >support@bitwarden.com</a\n            >\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-section background-color=\"#fff\" padding=\"0 20px 20px 20px\">\n      </mj-section>\n    </mj-wrapper>\n\n    <!-- Learn More Section -->\n    <mj-wrapper padding=\"0px 20px 10px 20px\">\n      <mj-bw-learn-more-footer />\n    </mj-wrapper>\n\n    <!-- Footer -->\n    <mj-include path=\"../../../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/emails/invite.mjml",
    "content": "<mjml>\n  <mj-head>\n    <mj-include path=\"../components/head.mjml\" />\n  </mj-head>\n\n  <mj-body>\n    <mj-wrapper css-class=\"border-fix\" padding=\"20px 20px\">\n      <mj-bw-hero\n        img-src=\"https://assets.bitwarden.com/email/v1/business.png\"\n        title=\"A Bitwarden member has invited you to Bitwarden Password Manager\"\n        button-text=\"Finish account setup\"\n        button-url=\"#\"\n      />\n\n      <mj-section>\n        <mj-column>\n          <mj-button href=\"#\">Join Organization Now</mj-button>\n\n          <mj-text>\n            This invitation expires on\n            <b>Tuesday, January 23, 2024 2:59PM UTC</b>.\n          </mj-text>\n        </mj-column>\n      </mj-section>\n      <mj-bw-learn-more-footer />\n    </mj-wrapper>\n\n    <mj-include path=\"../components/footer.mjml\" />\n  </mj-body>\n</mjml>\n"
  },
  {
    "path": "src/Core/MailTemplates/Mjml/package.json",
    "content": "{\n  \"name\": \"@bitwarden/mjml-emails\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Email templates for Bitwarden\",\n  \"private\": true,\n  \"type\": \"commonjs\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/bitwarden/server.git\"\n  },\n  \"author\": \"Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)\",\n  \"license\": \"SEE LICENSE IN LICENSE.txt\",\n  \"bugs\": {\n    \"url\": \"https://github.com/bitwarden/server/issues\"\n  },\n  \"homepage\": \"https://bitwarden.com\",\n  \"scripts\": {\n    \"build\": \"node ./build.js\",\n    \"build:hbs\": \"node ./build.js --hbs\",\n    \"build:minify\": \"node ./build.js --hbs --minify\",\n    \"build:watch\": \"nodemon ./build.js --watch emails --watch components --ext mjml,js\",\n    \"prettier\": \"prettier --cache --write .\"\n  },\n  \"dependencies\": {\n    \"mjml\": \"4.15.3\",\n    \"mjml-core\": \"4.15.3\"\n  },\n  \"devDependencies\": {\n    \"nodemon\": \"3.1.10\",\n    \"prettier\": \"3.6.2\"\n  }\n}\n"
  },
  {
    "path": "src/Core/MailTemplates/README.md",
    "content": "Email templating\n================\n\nWe use MJML to generate the HTML that our mail services use to send emails to users. To accomplish this, we use different file types depending on which part of the email generation process we're working with.\n\n# File Types\n\n## `*.html.hbs`\nThese are the compiled HTML email templates that serve as the foundation for all HTML emails sent by the Bitwarden platform. They are generated from MJML source files and enhanced with Handlebars templating capabilities.\n\n### Generation Process\n- **Source**: Built from `*.mjml` files in the `./mjml` directory.\n  - The MJML source acts as a toolkit for developers to generate HTML. It is the developers responsibility to generate the HTML and then ensure it is accessible to `IMailService` implementations.\n- **Build Tool**: Generated via node build scripts: `npm run build`.\n  - The build script definitions can be viewed in the `Mjml/package.json` as well as in `Mjml/build.js`. \n- **Output**: Cross-client compatible HTML with embedded CSS for maximum email client support\n- **Template Engine**: Enhanced with Handlebars syntax for dynamic content injection\n\n### Handlebars Integration\nThe templates use Handlebars templating syntax for dynamic content replacement:\n\n```html\n<!-- Example Handlebars usage -->\n<h1>Welcome {{userName}}!</h1>\n<p>Your organization {{organizationName}} has invited you to join.</p>\n<a href=\"{{actionUrl}}\">Accept Invitation</a>\n```\n\n**Variable Types:**\n- **Simple Variables**: `{{userName}}`, `{{email}}`, `{{organizationName}}`\n\n### Email Service Integration\nThe `IMailService` consumes these templates through the following process:\n\n1. **Template Selection**: Service selects appropriate `.html.hbs` template based on email type\n2. **Model Binding**: View model properties are mapped to Handlebars variables\n3. **Compilation**: Handlebars engine processes variables and generates final HTML\n\n### Development Guidelines\n\n**Variable Naming:**\n- Use camelCase for consistency: `{{userName}}`, `{{organizationName}}`\n- Prefix URLs with descriptive names: `{{actionUrl}}`, `{{logoUrl}}`\n\n**Testing Considerations:**\n- Verify Handlebars variable replacement with actual view model data\n- Ensure graceful degradation when variables are missing or null, if necessary\n- Validate HTML structure and accessibility compliance\n\n## `*.txt.hbs`\nThese files provide plain text versions of emails and are essential for email accessibility and deliverability. They serve several important purposes:\n\n### Purpose and Usage\n- **Accessibility**: Screen readers and assistive technologies often work better with plain text versions\n- **Email Client Compatibility**: Some email clients prefer or only display plain text versions\n- **Fallback Content**: When HTML rendering fails, the plain text version ensures the message is still readable\n\n### Structure\nPlain text email templates use the same Handlebars syntax (`{{variable}}`) as HTML templates for dynamic content replacement. They should:\n\n- Contain the core message content without HTML formatting\n- Use line breaks and spacing for readability\n- Include all important links as full URLs\n- Maintain logical content hierarchy using spacing and simple text formatting\n\n### Email Service Integration\nThe `IMailService` automatically uses both versions when sending emails:\n- The HTML version (from `*.html.hbs`) provides rich formatting and styling\n- The plain text version (from `*.txt.hbs`) serves as the text alternative\n- Email clients can choose which version to display based on user preferences and capabilities\n\n### Development Guidelines\n- Always create a corresponding `*.txt.hbs` file for each `*.html.hbs` template\n- Keep the content concise but complete - include all essential information from the HTML version\n- Test plain text templates to ensure they're readable and convey the same message\n\n## `*.mjml`\nThis is a templating language we use to increase efficiency when creating email content. See the `MJML` [documentation](./Mjml/README.md) for more details.\n\n# Managing email assets\n\nWe host assets that are included in emails at `assets.bitwarden.com`, at the `/email/v1` path. This corresponds to a static file storage container that is managed by our SRE team.  For example: https://assets.bitwarden.com/email/v1/mail-github.png. This is the URL for all assets for emails sent from any environment.\n\n## Adding an asset\n\nThe process for adding an asset uses the typical git workflow for submitting a pull request. You can use the steps below to add, remove, or edit assets being hosted in the repo.\n\nClone the assets repo:\n```bash\ngit clone git@github.com:bitwarden/assets.git\n```\n\nCreate a new branch to make and stage the changes you need:\n```bash\ngit checkout -b name-of-your-branch\n```\n\nAdd and commit your changes:\n```bash\ngit add path/to/your-asset.png\ngit commit -m \"commit message\"\n```\n\nPush the changes to remote repo:\n```bash\ngit push origin <branch>\n```\n\nOpen a PR for review.\n\nOnce the changes have been approved, you can merge the PR, which will build and deploy the `assets.bitwarden.com` Github Pages site. \n\n> [!NOTE]\n>\n> The changes following the merge may take some time to propagate."
  },
  {
    "path": "src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Api.OrganizationLicenses;\n\npublic class SelfHostedOrganizationLicenseRequestModel\n{\n    public string LicenseKey { get; set; }\n    public string BillingSyncKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\n\nnamespace Bit.Core.Models.Api.Request.OrganizationSponsorships;\n\npublic class OrganizationSponsorshipRequestModel\n{\n    public Guid SponsoringOrganizationUserId { get; set; }\n    public string FriendlyName { get; set; }\n    public string OfferedToEmail { get; set; }\n    public PlanSponsorshipType PlanSponsorshipType { get; set; }\n    public DateTime? LastSyncDate { get; set; }\n    public DateTime? ValidUntil { get; set; }\n    public bool ToDelete { get; set; }\n\n    public OrganizationSponsorshipRequestModel() { }\n\n    public OrganizationSponsorshipRequestModel(OrganizationSponsorshipData sponsorshipData)\n    {\n        SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId;\n        FriendlyName = sponsorshipData.FriendlyName;\n        OfferedToEmail = sponsorshipData.OfferedToEmail;\n        PlanSponsorshipType = sponsorshipData.PlanSponsorshipType;\n        LastSyncDate = sponsorshipData.LastSyncDate;\n        ValidUntil = sponsorshipData.ValidUntil;\n        ToDelete = sponsorshipData.ToDelete;\n    }\n\n    public OrganizationSponsorshipRequestModel(OrganizationSponsorship sponsorship)\n    {\n        SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId;\n        FriendlyName = sponsorship.FriendlyName;\n        OfferedToEmail = sponsorship.OfferedToEmail;\n        PlanSponsorshipType = sponsorship.PlanSponsorshipType.GetValueOrDefault();\n        LastSyncDate = sponsorship.LastSyncDate;\n        ValidUntil = sponsorship.ValidUntil;\n        ToDelete = sponsorship.ToDelete;\n    }\n\n    public OrganizationSponsorshipData ToOrganizationSponsorship()\n    {\n        return new OrganizationSponsorshipData\n        {\n            SponsoringOrganizationUserId = SponsoringOrganizationUserId,\n            FriendlyName = FriendlyName,\n            OfferedToEmail = OfferedToEmail,\n            PlanSponsorshipType = PlanSponsorshipType,\n            LastSyncDate = LastSyncDate,\n            ValidUntil = ValidUntil,\n            ToDelete = ToDelete,\n        };\n\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipSyncRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\n\nnamespace Bit.Core.Models.Api.Request.OrganizationSponsorships;\n\npublic class OrganizationSponsorshipSyncRequestModel\n{\n    public string BillingSyncKey { get; set; }\n    public Guid SponsoringOrganizationCloudId { get; set; }\n    public IEnumerable<OrganizationSponsorshipRequestModel> SponsorshipsBatch { get; set; }\n\n    public OrganizationSponsorshipSyncRequestModel() { }\n\n    public OrganizationSponsorshipSyncRequestModel(IEnumerable<OrganizationSponsorshipRequestModel> sponsorshipsBatch)\n    {\n        SponsorshipsBatch = sponsorshipsBatch;\n    }\n\n    public OrganizationSponsorshipSyncRequestModel(OrganizationSponsorshipSyncData syncData)\n    {\n        if (syncData == null)\n        {\n            return;\n        }\n        BillingSyncKey = syncData.BillingSyncKey;\n        SponsoringOrganizationCloudId = syncData.SponsoringOrganizationCloudId;\n        SponsorshipsBatch = syncData.SponsorshipsBatch.Select(o => new OrganizationSponsorshipRequestModel(o));\n    }\n\n    public OrganizationSponsorshipSyncData ToOrganizationSponsorshipSync()\n    {\n        return new OrganizationSponsorshipSyncData()\n        {\n            BillingSyncKey = BillingSyncKey,\n            SponsoringOrganizationCloudId = SponsoringOrganizationCloudId,\n            SponsorshipsBatch = SponsorshipsBatch.Select(o => o.ToOrganizationSponsorship())\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Request/PushDeviceRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Models.Api;\n\npublic class PushDeviceRequestModel\n{\n    [Required]\n    public string Id { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Request/PushRegistrationRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Api;\n\npublic class PushRegistrationRequestModel\n{\n    [Required] public string DeviceId { get; set; }\n    [Required] public string PushToken { get; set; }\n    [Required] public string UserId { get; set; }\n    [Required] public DeviceType Type { get; set; }\n    [Required] public string Identifier { get; set; }\n    public IEnumerable<string> OrganizationIds { get; set; }\n    public Guid InstallationId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Request/PushSendRequestModel.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Api;\n\npublic class PushSendRequestModel<T> : IValidatableObject\n{\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Guid? DeviceId { get; set; }\n    public string? Identifier { get; set; }\n    public required PushType Type { get; set; }\n    public required T Payload { get; set; }\n    public ClientType? ClientType { get; set; }\n    public Guid? InstallationId { get; set; }\n\n    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)\n    {\n        if (!UserId.HasValue &&\n            !OrganizationId.HasValue &&\n            !InstallationId.HasValue)\n        {\n            yield return new ValidationResult(\n                $\"{nameof(UserId)} or {nameof(OrganizationId)} or {nameof(InstallationId)} is required.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Request/PushUpdateRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Models.Api;\n\npublic class PushUpdateRequestModel\n{\n    public PushUpdateRequestModel()\n    { }\n\n    public PushUpdateRequestModel(IEnumerable<string> deviceIds, string organizationId)\n    {\n        Devices = deviceIds.Select(d => new PushDeviceRequestModel { Id = d });\n        OrganizationId = organizationId;\n    }\n\n    [Required]\n    public IEnumerable<PushDeviceRequestModel> Devices { get; set; }\n    [Required]\n    public string OrganizationId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Response/Duo/DuoResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Core.Models.Api.Response.Duo;\n\npublic class DuoResponseModel\n{\n    [JsonPropertyName(\"stat\")]\n    public string Stat { get; set; }\n\n    [JsonPropertyName(\"code\")]\n    public int? Code { get; set; }\n\n    [JsonPropertyName(\"message\")]\n    public string Message { get; set; }\n\n    [JsonPropertyName(\"message_detail\")]\n    public string MessageDetail { get; set; }\n\n    [JsonPropertyName(\"response\")]\n    public Response Response { get; set; }\n}\n\npublic class Response\n{\n    [JsonPropertyName(\"time\")]\n    public int Time { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Response/ErrorResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Core.Models.Api;\n\npublic class ErrorResponseModel : ResponseModel\n{\n    public ErrorResponseModel()\n        : base(\"error\")\n    { }\n\n    public ErrorResponseModel(string message)\n        : this()\n    {\n        Message = message;\n    }\n\n    public ErrorResponseModel(ModelStateDictionary modelState)\n        : this()\n    {\n        Message = \"The model state is invalid.\";\n        ValidationErrors = new Dictionary<string, IEnumerable<string>>();\n\n        var keys = modelState.Keys.ToList();\n        var values = modelState.Values.ToList();\n\n        for (var i = 0; i < values.Count; i++)\n        {\n            var value = values[i];\n\n            if (keys.Count <= i)\n            {\n                // Keys not available for some reason.\n                break;\n            }\n\n            var key = keys[i];\n\n            if (value.ValidationState != ModelValidationState.Invalid || value.Errors.Count == 0)\n            {\n                continue;\n            }\n\n            var errors = value.Errors.Select(e => e.ErrorMessage);\n            ValidationErrors.Add(key, errors);\n        }\n    }\n\n    public ErrorResponseModel(Dictionary<string, IEnumerable<string>> errors)\n        : this(\"Errors have occurred.\", errors)\n    { }\n\n    public ErrorResponseModel(string errorKey, string errorValue)\n        : this(errorKey, new string[] { errorValue })\n    { }\n\n    public ErrorResponseModel(string errorKey, IEnumerable<string> errorValues)\n        : this(new Dictionary<string, IEnumerable<string>> { { errorKey, errorValues } })\n    { }\n\n    public ErrorResponseModel(string message, Dictionary<string, IEnumerable<string>> errors)\n        : this()\n    {\n        Message = message;\n        ValidationErrors = errors;\n    }\n\n    public string Message { get; set; }\n    public Dictionary<string, IEnumerable<string>> ValidationErrors { get; set; }\n    // For use in development environments.\n    public string ExceptionMessage { get; set; }\n    public string ExceptionStackTrace { get; set; }\n    public string InnerExceptionMessage { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Response/MasterPasswordPolicyResponseModel.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\nnamespace Bit.Core.Models.Api.Response;\n\npublic class MasterPasswordPolicyResponseModel : ResponseModel\n{\n    public MasterPasswordPolicyResponseModel(MasterPasswordPolicyData data) : base(\"masterPasswordPolicy\")\n    {\n        if (data == null)\n        {\n            return;\n        }\n\n        MinComplexity = data.MinComplexity;\n        MinLength = data.MinLength;\n        RequireLower = data.RequireLower;\n        RequireUpper = data.RequireUpper;\n        RequireNumbers = data.RequireNumbers;\n        RequireSpecial = data.RequireSpecial;\n        EnforceOnLogin = data.EnforceOnLogin;\n    }\n\n    public int? MinComplexity { get; set; }\n\n    public int? MinLength { get; set; }\n\n    public bool? RequireLower { get; set; }\n\n    public bool? RequireUpper { get; set; }\n\n    public bool? RequireNumbers { get; set; }\n\n    public bool? RequireSpecial { get; set; }\n\n    public bool? EnforceOnLogin { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipInvitesResponseModel.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\n\nnamespace Bit.Core.Models.Api.Response.OrganizationSponsorships;\n\npublic class OrganizationSponsorshipInvitesResponseModel : ResponseModel\n{\n    public OrganizationSponsorshipInvitesResponseModel(OrganizationSponsorshipData sponsorshipData, string obj = \"organizationSponsorship\") : base(obj)\n    {\n        if (sponsorshipData == null)\n        {\n            throw new ArgumentNullException(nameof(sponsorshipData));\n        }\n\n        SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId;\n        FriendlyName = sponsorshipData.FriendlyName;\n        OfferedToEmail = sponsorshipData.OfferedToEmail;\n        PlanSponsorshipType = sponsorshipData.PlanSponsorshipType;\n        LastSyncDate = sponsorshipData.LastSyncDate;\n        ValidUntil = sponsorshipData.ValidUntil;\n        ToDelete = sponsorshipData.ToDelete;\n        IsAdminInitiated = sponsorshipData.IsAdminInitiated;\n        Notes = sponsorshipData.Notes;\n        CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved;\n    }\n\n    public Guid SponsoringOrganizationUserId { get; set; }\n    public string FriendlyName { get; set; }\n    public string OfferedToEmail { get; set; }\n    public PlanSponsorshipType PlanSponsorshipType { get; set; }\n    public DateTime? LastSyncDate { get; set; }\n    public DateTime? ValidUntil { get; set; }\n    public bool ToDelete { get; set; }\n    public bool IsAdminInitiated { get; set; }\n    public string Notes { get; set; }\n    public bool CloudSponsorshipRemoved { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\n\nnamespace Bit.Core.Models.Api.Response.OrganizationSponsorships;\n\npublic class OrganizationSponsorshipResponseModel\n{\n    public Guid SponsoringOrganizationUserId { get; set; }\n    public string FriendlyName { get; set; }\n    public string OfferedToEmail { get; set; }\n    public PlanSponsorshipType PlanSponsorshipType { get; set; }\n    public DateTime? LastSyncDate { get; set; }\n    public DateTime? ValidUntil { get; set; }\n    public bool ToDelete { get; set; }\n\n    public bool CloudSponsorshipRemoved { get; set; }\n    public bool IsAdminInitiated { get; set; }\n\n    public OrganizationSponsorshipResponseModel() { }\n\n    public OrganizationSponsorshipResponseModel(OrganizationSponsorshipData sponsorshipData)\n    {\n        SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId;\n        FriendlyName = sponsorshipData.FriendlyName;\n        OfferedToEmail = sponsorshipData.OfferedToEmail;\n        PlanSponsorshipType = sponsorshipData.PlanSponsorshipType;\n        LastSyncDate = sponsorshipData.LastSyncDate;\n        ValidUntil = sponsorshipData.ValidUntil;\n        ToDelete = sponsorshipData.ToDelete;\n        CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved;\n        IsAdminInitiated = sponsorshipData.IsAdminInitiated;\n    }\n\n    public OrganizationSponsorshipData ToOrganizationSponsorship()\n    {\n        return new OrganizationSponsorshipData\n        {\n            SponsoringOrganizationUserId = SponsoringOrganizationUserId,\n            FriendlyName = FriendlyName,\n            OfferedToEmail = OfferedToEmail,\n            PlanSponsorshipType = PlanSponsorshipType,\n            LastSyncDate = LastSyncDate,\n            ValidUntil = ValidUntil,\n            ToDelete = ToDelete,\n            CloudSponsorshipRemoved = CloudSponsorshipRemoved,\n            IsAdminInitiated = IsAdminInitiated,\n        };\n\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipSyncResponseModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\n\nnamespace Bit.Core.Models.Api.Response.OrganizationSponsorships;\n\npublic class OrganizationSponsorshipSyncResponseModel\n{\n    public IEnumerable<OrganizationSponsorshipResponseModel> SponsorshipsBatch { get; set; }\n\n    public OrganizationSponsorshipSyncResponseModel() { }\n\n    public OrganizationSponsorshipSyncResponseModel(OrganizationSponsorshipSyncData syncData)\n    {\n        if (syncData == null)\n        {\n            return;\n        }\n        SponsorshipsBatch = syncData.SponsorshipsBatch.Select(o => new OrganizationSponsorshipResponseModel(o));\n\n    }\n\n    public OrganizationSponsorshipSyncData ToOrganizationSponsorshipSync()\n    {\n        return new OrganizationSponsorshipSyncData()\n        {\n            SponsorshipsBatch = SponsorshipsBatch.Select(o => o.ToOrganizationSponsorship())\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Response/OrganizationSponsorships/PreValidateSponsorshipResponseModel.cs",
    "content": "﻿namespace Bit.Core.Models.Api.Response.OrganizationSponsorships;\n\npublic record PreValidateSponsorshipResponseModel(\n    bool IsTokenValid,\n    bool IsFreeFamilyPolicyEnabled)\n{\n    public static PreValidateSponsorshipResponseModel From(bool validToken, bool policyStatus)\n        => new(validToken, policyStatus);\n}\n"
  },
  {
    "path": "src/Core/Models/Api/Response/ResponseModel.cs",
    "content": "﻿using Newtonsoft.Json;\n\nnamespace Bit.Core.Models.Api;\n\npublic abstract class ResponseModel\n{\n    public ResponseModel(string obj)\n    {\n        if (string.IsNullOrWhiteSpace(obj))\n        {\n            throw new ArgumentNullException(nameof(obj));\n        }\n\n        Object = obj;\n    }\n\n    [JsonProperty(Order = -200)] // Always the first property\n    public string Object { get; private set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/CompleteSubscriptionUpdate.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Exceptions;\nusing Stripe;\nusing Plan = Bit.Core.Models.StaticStore.Plan;\n\nnamespace Bit.Core.Models.Business;\n\n/// <summary>\n/// A model representing the data required to upgrade from one subscription to another using a <see cref=\"CompleteSubscriptionUpdate\"/>.\n/// </summary>\npublic class SubscriptionData\n{\n    public Plan Plan { get; init; }\n    public int PurchasedPasswordManagerSeats { get; init; }\n    public bool SubscribedToSecretsManager { get; set; }\n    public int? PurchasedSecretsManagerSeats { get; init; }\n    public int? PurchasedAdditionalSecretsManagerServiceAccounts { get; init; }\n    public int PurchasedAdditionalStorage { get; init; }\n}\n\npublic class CompleteSubscriptionUpdate : SubscriptionUpdate\n{\n    private readonly SubscriptionData _currentSubscription;\n    private readonly SubscriptionData _updatedSubscription;\n\n    private readonly Dictionary<string, SubscriptionUpdateType> _subscriptionUpdateMap = new();\n\n    private enum SubscriptionUpdateType\n    {\n        PasswordManagerSeats,\n        SecretsManagerSeats,\n        SecretsManagerServiceAccounts,\n        Storage\n    }\n\n    /// <summary>\n    /// A model used to generate the Stripe <see cref=\"SubscriptionItemOptions\"/>\n    /// necessary to both upgrade an organization's subscription and revert that upgrade\n    /// in the case of an error.\n    /// </summary>\n    /// <param name=\"organization\">The <see cref=\"Organization\"/> to upgrade.</param>\n    /// <param name=\"plan\">The organization's plan.</param>\n    /// <param name=\"updatedSubscription\">The updates you want to apply to the organization's subscription.</param>\n    public CompleteSubscriptionUpdate(\n        Organization organization,\n        Plan plan,\n        SubscriptionData updatedSubscription)\n    {\n        _currentSubscription = GetSubscriptionDataFor(organization, plan);\n        _updatedSubscription = updatedSubscription;\n    }\n\n    protected override List<string> PlanIds =>\n    [\n        GetPasswordManagerPlanId(_updatedSubscription.Plan),\n        _updatedSubscription.Plan.SecretsManager.StripeSeatPlanId,\n        _updatedSubscription.Plan.SecretsManager.StripeServiceAccountPlanId,\n        _updatedSubscription.Plan.PasswordManager.StripeStoragePlanId\n    ];\n\n    /// <summary>\n    /// Generates the <see cref=\"SubscriptionItemOptions\"/> necessary to revert an <see cref=\"Organization\"/>'s\n    /// <see cref=\"Subscription\"/> upgrade in the case of an error.\n    /// </summary>\n    /// <param name=\"subscription\">The organization's <see cref=\"Subscription\"/>.</param>\n    public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)\n    {\n        var subscriptionItemOptions = new List<SubscriptionItemOptions>\n        {\n            GetPasswordManagerOptions(subscription, _updatedSubscription, _currentSubscription)\n        };\n\n        if (_updatedSubscription.SubscribedToSecretsManager || _currentSubscription.SubscribedToSecretsManager)\n        {\n            subscriptionItemOptions.Add(GetSecretsManagerOptions(subscription, _updatedSubscription, _currentSubscription));\n\n            if (_updatedSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0 ||\n                _currentSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0)\n            {\n                subscriptionItemOptions.Add(GetServiceAccountsOptions(subscription, _updatedSubscription, _currentSubscription));\n            }\n        }\n\n        if (_updatedSubscription.PurchasedAdditionalStorage != 0 || _currentSubscription.PurchasedAdditionalStorage != 0)\n        {\n            subscriptionItemOptions.Add(GetStorageOptions(subscription, _updatedSubscription, _currentSubscription));\n        }\n\n        return subscriptionItemOptions;\n    }\n\n    /*\n     * This is almost certainly overkill. If we trust the data in the Vault DB, we should just be able to\n     * compare the _currentSubscription against the _updatedSubscription to see if there are any differences.\n     * However, for the sake of ensuring we're checking against the Stripe subscription itself, I'll leave this\n     * included for now.\n     */\n    /// <summary>\n    /// Checks whether the updates provided in the <see cref=\"CompleteSubscriptionUpdate\"/>'s constructor\n    /// are actually different from the organization's current <see cref=\"Subscription\"/>.\n    /// </summary>\n    /// <param name=\"subscription\">The organization's <see cref=\"Subscription\"/>.</param>\n    public override bool UpdateNeeded(Subscription subscription)\n    {\n        var upgradeItemsOptions = UpgradeItemsOptions(subscription);\n\n        foreach (var subscriptionItemOptions in upgradeItemsOptions)\n        {\n            var success = _subscriptionUpdateMap.TryGetValue(subscriptionItemOptions.Price, out var updateType);\n\n            if (!success)\n            {\n                return false;\n            }\n\n            var updateNeeded = updateType switch\n            {\n                SubscriptionUpdateType.PasswordManagerSeats => ContainsUpdatesBetween(\n                    GetPasswordManagerPlanId(_currentSubscription.Plan),\n                    subscriptionItemOptions),\n                SubscriptionUpdateType.SecretsManagerSeats => ContainsUpdatesBetween(\n                    _currentSubscription.Plan.SecretsManager.StripeSeatPlanId,\n                    subscriptionItemOptions),\n                SubscriptionUpdateType.SecretsManagerServiceAccounts => ContainsUpdatesBetween(\n                    _currentSubscription.Plan.SecretsManager.StripeServiceAccountPlanId,\n                    subscriptionItemOptions),\n                SubscriptionUpdateType.Storage => ContainsUpdatesBetween(\n                    _currentSubscription.Plan.PasswordManager.StripeStoragePlanId,\n                    subscriptionItemOptions),\n                _ => false\n            };\n\n            if (updateNeeded)\n            {\n                return true;\n            }\n        }\n\n        return false;\n\n        bool ContainsUpdatesBetween(string currentPlanId, SubscriptionItemOptions options)\n        {\n            var subscriptionItem = FindSubscriptionItem(subscription, currentPlanId);\n\n            return (subscriptionItem.Plan.Id != options.Plan && subscriptionItem.Price.Id != options.Plan) ||\n                   subscriptionItem.Quantity != options.Quantity ||\n                   subscriptionItem.Deleted != options.Deleted;\n        }\n    }\n\n    /// <summary>\n    /// Generates the <see cref=\"SubscriptionItemOptions\"/> necessary to upgrade an <see cref=\"Organization\"/>'s\n    /// <see cref=\"Subscription\"/>.\n    /// </summary>\n    /// <param name=\"subscription\">The organization's <see cref=\"Subscription\"/>.</param>\n    public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)\n    {\n        var subscriptionItemOptions = new List<SubscriptionItemOptions>\n        {\n            GetPasswordManagerOptions(subscription, _currentSubscription, _updatedSubscription)\n        };\n\n        if (_currentSubscription.SubscribedToSecretsManager || _updatedSubscription.SubscribedToSecretsManager)\n        {\n            subscriptionItemOptions.Add(GetSecretsManagerOptions(subscription, _currentSubscription, _updatedSubscription));\n\n            if (_currentSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0 ||\n                _updatedSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0)\n            {\n                subscriptionItemOptions.Add(GetServiceAccountsOptions(subscription, _currentSubscription, _updatedSubscription));\n            }\n        }\n\n        if (_currentSubscription.PurchasedAdditionalStorage != 0 || _updatedSubscription.PurchasedAdditionalStorage != 0)\n        {\n            subscriptionItemOptions.Add(GetStorageOptions(subscription, _currentSubscription, _updatedSubscription));\n        }\n\n        return subscriptionItemOptions;\n    }\n\n    private SubscriptionItemOptions GetPasswordManagerOptions(\n        Subscription subscription,\n        SubscriptionData from,\n        SubscriptionData to)\n    {\n        var currentPlanId = GetPasswordManagerPlanId(from.Plan);\n\n        var subscriptionItem = FindSubscriptionItem(subscription, currentPlanId);\n\n        if (subscriptionItem == null)\n        {\n            throw new GatewayException(\"Could not find Password Manager subscription\");\n        }\n\n        var updatedPlanId = GetPasswordManagerPlanId(to.Plan);\n\n        _subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.PasswordManagerSeats;\n\n        return new SubscriptionItemOptions\n        {\n            Id = subscriptionItem.Id,\n            Price = updatedPlanId,\n            Quantity = IsNonSeatBasedPlan(to.Plan) ? 1 : to.PurchasedPasswordManagerSeats\n        };\n    }\n\n    private SubscriptionItemOptions GetSecretsManagerOptions(\n        Subscription subscription,\n        SubscriptionData from,\n        SubscriptionData to)\n    {\n        var currentPlanId = from.Plan?.SecretsManager?.StripeSeatPlanId;\n\n        var subscriptionItem = !string.IsNullOrEmpty(currentPlanId)\n            ? FindSubscriptionItem(subscription, currentPlanId)\n            : null;\n\n        var updatedPlanId = to.Plan.SecretsManager.StripeSeatPlanId;\n\n        _subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.SecretsManagerSeats;\n\n        return new SubscriptionItemOptions\n        {\n            Id = subscriptionItem?.Id,\n            Price = updatedPlanId,\n            Quantity = to.PurchasedSecretsManagerSeats,\n            Deleted = subscriptionItem?.Id != null && to.PurchasedSecretsManagerSeats == 0\n                ? true\n                : null\n        };\n    }\n\n    private SubscriptionItemOptions GetServiceAccountsOptions(\n        Subscription subscription,\n        SubscriptionData from,\n        SubscriptionData to)\n    {\n        var currentPlanId = from.Plan?.SecretsManager?.StripeServiceAccountPlanId;\n\n        var subscriptionItem = !string.IsNullOrEmpty(currentPlanId)\n            ? FindSubscriptionItem(subscription, currentPlanId)\n            : null;\n\n        var updatedPlanId = to.Plan.SecretsManager.StripeServiceAccountPlanId;\n\n        _subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.SecretsManagerServiceAccounts;\n\n        return new SubscriptionItemOptions\n        {\n            Id = subscriptionItem?.Id,\n            Price = updatedPlanId,\n            Quantity = to.PurchasedAdditionalSecretsManagerServiceAccounts,\n            Deleted = subscriptionItem?.Id != null && to.PurchasedAdditionalSecretsManagerServiceAccounts == 0\n                ? true\n                : null\n        };\n    }\n\n    private SubscriptionItemOptions GetStorageOptions(\n        Subscription subscription,\n        SubscriptionData from,\n        SubscriptionData to)\n    {\n        var currentPlanId = from.Plan.PasswordManager.StripeStoragePlanId;\n\n        var subscriptionItem = FindSubscriptionItem(subscription, currentPlanId);\n\n        var updatedPlanId = to.Plan.PasswordManager.StripeStoragePlanId;\n\n        _subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.Storage;\n\n        return new SubscriptionItemOptions\n        {\n            Id = subscriptionItem?.Id,\n            Price = updatedPlanId,\n            Quantity = to.PurchasedAdditionalStorage,\n            Deleted = subscriptionItem?.Id != null && to.PurchasedAdditionalStorage == 0\n                ? true\n                : null\n        };\n    }\n\n    private static SubscriptionData GetSubscriptionDataFor(Organization organization, Plan plan)\n        => new()\n        {\n            Plan = plan,\n            PurchasedPasswordManagerSeats = organization.Seats.HasValue\n                ? organization.Seats.Value - plan.PasswordManager.BaseSeats\n                : 0,\n            SubscribedToSecretsManager = organization.UseSecretsManager,\n            PurchasedSecretsManagerSeats = plan.SecretsManager is not null\n                ? organization.SmSeats - plan.SecretsManager.BaseSeats\n                : 0,\n            PurchasedAdditionalSecretsManagerServiceAccounts = plan.SecretsManager is not null\n                ? organization.SmServiceAccounts - plan.SecretsManager.BaseServiceAccount\n                : 0,\n            PurchasedAdditionalStorage = organization.MaxStorageGb.HasValue\n                ? organization.MaxStorageGb.Value - plan.PasswordManager.BaseStorageGb :\n                0\n        };\n}\n"
  },
  {
    "path": "src/Core/Models/Business/OrganizationSignup.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Business;\n\npublic class OrganizationSignup : OrganizationUpgrade\n{\n    public string Name { get; set; }\n    public string BillingEmail { get; set; }\n    public User Owner { get; set; }\n    public string OwnerKey { get; set; }\n    public string CollectionName { get; set; }\n    public PaymentMethodType? PaymentMethodType { get; set; }\n    public string PaymentToken { get; set; }\n    public int? MaxAutoscaleSeats { get; set; } = null;\n    public string InitiationPath { get; set; }\n    public bool IsFromSecretsManagerTrial { get; set; }\n    public bool IsFromProvider { get; set; }\n    public bool SkipTrial { get; set; }\n    public string[] Coupons { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/OrganizationUpgrade.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.Models.Business;\n\npublic class OrganizationUpgrade\n{\n    public string BusinessName { get; set; }\n    public PlanType Plan { get; set; }\n    public int AdditionalSeats { get; set; }\n    public short AdditionalStorageGb { get; set; }\n    public bool PremiumAccessAddon { get; set; }\n    public TaxInfo TaxInfo { get; set; }\n    public PublicKeyEncryptionKeyPairData Keys { get; set; }\n    public int? AdditionalSmSeats { get; set; }\n    public int? AdditionalServiceAccounts { get; set; }\n    public bool UseSecretsManager { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/SeatSubscriptionUpdate.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Stripe;\n\nnamespace Bit.Core.Models.Business;\n\npublic class SeatSubscriptionUpdate : SubscriptionUpdate\n{\n    private readonly int _previousSeats;\n    private readonly StaticStore.Plan _plan;\n    private readonly long? _additionalSeats;\n    protected override List<string> PlanIds => new() { _plan.PasswordManager.StripeSeatPlanId };\n    public SeatSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalSeats)\n    {\n        _plan = plan;\n        _additionalSeats = additionalSeats;\n        _previousSeats = organization.Seats.GetValueOrDefault();\n    }\n\n    public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)\n    {\n        var item = FindSubscriptionItem(subscription, PlanIds.Single());\n        return new()\n        {\n            new SubscriptionItemOptions\n            {\n                Id = item?.Id,\n                Plan = PlanIds.Single(),\n                Quantity = _additionalSeats,\n                Deleted = (item?.Id != null && _additionalSeats == 0) ? true : (bool?)null,\n            }\n        };\n    }\n\n    public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)\n    {\n\n        var item = FindSubscriptionItem(subscription, PlanIds.Single());\n        return new()\n        {\n            new SubscriptionItemOptions\n            {\n                Id = item?.Id,\n                Plan = PlanIds.Single(),\n                Quantity = _previousSeats,\n                Deleted = _previousSeats == 0 ? true : (bool?)null,\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/SecretsManagerSubscribeUpdate.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Stripe;\n\nnamespace Bit.Core.Models.Business;\n\npublic class SecretsManagerSubscribeUpdate : SubscriptionUpdate\n{\n    private readonly StaticStore.Plan _plan;\n    private readonly long? _additionalSeats;\n    private readonly long? _additionalServiceAccounts;\n    private readonly int _previousSeats;\n    private readonly int _previousServiceAccounts;\n    protected override List<string> PlanIds => new() { _plan.SecretsManager.StripeSeatPlanId, _plan.SecretsManager.StripeServiceAccountPlanId };\n    public SecretsManagerSubscribeUpdate(Organization organization, StaticStore.Plan plan, long? additionalSeats, long? additionalServiceAccounts)\n    {\n        _plan = plan;\n        _additionalSeats = additionalSeats;\n        _additionalServiceAccounts = additionalServiceAccounts;\n        _previousSeats = organization.SmSeats.GetValueOrDefault();\n        _previousServiceAccounts = organization.SmServiceAccounts.GetValueOrDefault();\n    }\n\n    public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)\n    {\n        var updatedItems = new List<SubscriptionItemOptions>();\n\n        RemovePreviousSecretsManagerItems(updatedItems);\n\n        return updatedItems;\n    }\n\n    public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)\n    {\n        var updatedItems = new List<SubscriptionItemOptions>();\n\n        AddNewSecretsManagerItems(updatedItems);\n\n        return updatedItems;\n    }\n\n    private void AddNewSecretsManagerItems(List<SubscriptionItemOptions> updatedItems)\n    {\n        if (_additionalSeats > 0)\n        {\n            updatedItems.Add(new SubscriptionItemOptions\n            {\n                Price = _plan.SecretsManager.StripeSeatPlanId,\n                Quantity = _additionalSeats\n            });\n        }\n\n        if (_additionalServiceAccounts > 0)\n        {\n            updatedItems.Add(new SubscriptionItemOptions\n            {\n                Price = _plan.SecretsManager.StripeServiceAccountPlanId,\n                Quantity = _additionalServiceAccounts\n            });\n        }\n    }\n\n    private void RemovePreviousSecretsManagerItems(List<SubscriptionItemOptions> updatedItems)\n    {\n        updatedItems.Add(new SubscriptionItemOptions\n        {\n            Price = _plan.SecretsManager.StripeSeatPlanId,\n            Quantity = _previousSeats,\n            Deleted = _previousSeats == 0 ? true : (bool?)null,\n        });\n\n        updatedItems.Add(new SubscriptionItemOptions\n        {\n            Price = _plan.SecretsManager.StripeServiceAccountPlanId,\n            Quantity = _previousServiceAccounts,\n            Deleted = _previousServiceAccounts == 0 ? true : (bool?)null,\n        });\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Models.Business;\n\npublic class SecretsManagerSubscriptionUpdate\n{\n    public Organization Organization { get; }\n    public Plan Plan { get; }\n\n    /// <summary>\n    /// The total seats the organization will have after the update, including any base seats included in the plan\n    /// </summary>\n    public int? SmSeats { get; set; }\n\n    /// <summary>\n    /// The new autoscale limit for seats after the update\n    /// </summary>\n    public int? MaxAutoscaleSmSeats { get; set; }\n\n    /// <summary>\n    /// The total service accounts the organization will have after the update, including the base service accounts\n    /// included in the plan\n    /// </summary>\n    public int? SmServiceAccounts { get; set; }\n\n    /// <summary>\n    /// The new autoscale limit for service accounts after the update\n    /// </summary>\n    public int? MaxAutoscaleSmServiceAccounts { get; set; }\n\n    /// <summary>\n    /// Whether the subscription update is a result of autoscaling\n    /// </summary>\n    public bool Autoscaling { get; }\n\n    /// <summary>\n    /// The seats the organization will have after the update, excluding the base seats included in the plan\n    /// Usually this is what the organization is billed for\n    /// </summary>\n    public int SmSeatsExcludingBase => SmSeats.HasValue ? SmSeats.Value - Plan.SecretsManager.BaseSeats : 0;\n    /// <summary>\n    /// The seats the organization will have after the update, excluding the base seats included in the plan\n    /// Usually this is what the organization is billed for\n    /// </summary>\n    public int SmServiceAccountsExcludingBase => SmServiceAccounts.HasValue ? SmServiceAccounts.Value - Plan.SecretsManager!.BaseServiceAccount : 0;\n    public bool SmSeatsChanged => SmSeats != Organization.SmSeats;\n    public bool SmServiceAccountsChanged => SmServiceAccounts != Organization.SmServiceAccounts;\n    public bool MaxAutoscaleSmSeatsChanged => MaxAutoscaleSmSeats != Organization.MaxAutoscaleSmSeats;\n    public bool MaxAutoscaleSmServiceAccountsChanged =>\n        MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts;\n\n\n    public SecretsManagerSubscriptionUpdate(Organization organization, Plan plan, bool autoscaling)\n    {\n        Organization = organization ?? throw new NotFoundException(\"Organization is not found.\");\n        Plan = plan;\n\n        if (!Plan.SupportsSecretsManager)\n        {\n            throw new NotFoundException(\"Invalid Secrets Manager plan.\");\n        }\n\n        SmSeats = organization.SmSeats;\n        MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats;\n        SmServiceAccounts = organization.SmServiceAccounts;\n        MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;\n        Autoscaling = autoscaling;\n    }\n\n    public SecretsManagerSubscriptionUpdate AdjustSeats(int adjustment)\n    {\n        SmSeats = SmSeats.GetValueOrDefault() + adjustment;\n        return this;\n    }\n\n    public SecretsManagerSubscriptionUpdate AdjustServiceAccounts(int adjustment)\n    {\n        SmServiceAccounts = SmServiceAccounts.GetValueOrDefault() + adjustment;\n        return this;\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/ServiceAccountSubscriptionUpdate.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Stripe;\n\nnamespace Bit.Core.Models.Business;\n\npublic class ServiceAccountSubscriptionUpdate : SubscriptionUpdate\n{\n    private long? _prevServiceAccounts;\n    private readonly StaticStore.Plan _plan;\n    private readonly long? _additionalServiceAccounts;\n    protected override List<string> PlanIds => new() { _plan.SecretsManager.StripeServiceAccountPlanId };\n\n    public ServiceAccountSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalServiceAccounts)\n    {\n        _plan = plan;\n        _additionalServiceAccounts = additionalServiceAccounts;\n        _prevServiceAccounts = organization.SmServiceAccounts ?? 0;\n    }\n\n    public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)\n    {\n        var item = FindSubscriptionItem(subscription, PlanIds.Single());\n        _prevServiceAccounts = item?.Quantity ?? 0;\n        return new()\n        {\n            new SubscriptionItemOptions\n            {\n                Id = item?.Id,\n                Plan = PlanIds.Single(),\n                Quantity = _additionalServiceAccounts,\n                Deleted = (item?.Id != null && _additionalServiceAccounts == 0) ? true : (bool?)null,\n            }\n        };\n    }\n\n    public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)\n    {\n        var item = FindSubscriptionItem(subscription, PlanIds.Single());\n        return new()\n        {\n            new SubscriptionItemOptions\n            {\n                Id = item?.Id,\n                Plan = PlanIds.Single(),\n                Quantity = _prevServiceAccounts,\n                Deleted = _prevServiceAccounts == 0 ? true : (bool?)null,\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/SmSeatSubscriptionUpdate.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Stripe;\n\nnamespace Bit.Core.Models.Business;\n\npublic class SmSeatSubscriptionUpdate : SubscriptionUpdate\n{\n    private readonly int _previousSeats;\n    private readonly StaticStore.Plan _plan;\n    private readonly long? _additionalSeats;\n    protected override List<string> PlanIds => new() { _plan.SecretsManager.StripeSeatPlanId };\n\n    public SmSeatSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalSeats)\n    {\n        _plan = plan;\n        _additionalSeats = additionalSeats;\n        _previousSeats = organization.SmSeats.GetValueOrDefault();\n    }\n\n    public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)\n    {\n        var item = FindSubscriptionItem(subscription, PlanIds.Single());\n        return new()\n        {\n            new SubscriptionItemOptions\n            {\n                Id = item?.Id,\n                Plan = PlanIds.Single(),\n                Quantity = _additionalSeats,\n                Deleted = (item?.Id != null && _additionalSeats == 0) ? true : (bool?)null,\n            }\n        };\n    }\n\n    public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)\n    {\n\n        var item = FindSubscriptionItem(subscription, PlanIds.Single());\n        return new()\n        {\n            new SubscriptionItemOptions\n            {\n                Id = item?.Id,\n                Plan = PlanIds.Single(),\n                Quantity = _previousSeats,\n                Deleted = _previousSeats == 0 ? true : (bool?)null,\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/StorageSubscriptionUpdate.cs",
    "content": "﻿using Stripe;\n\nnamespace Bit.Core.Models.Business;\n\npublic class StorageSubscriptionUpdate : SubscriptionUpdate\n{\n    private long? _prevStorage;\n    private readonly string _plan;\n    private readonly long? _additionalStorage;\n    protected override List<string> PlanIds => new() { _plan };\n\n    public StorageSubscriptionUpdate(string plan, long? additionalStorage)\n    {\n        _plan = plan;\n        _additionalStorage = additionalStorage;\n    }\n\n    public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)\n    {\n        var item = FindSubscriptionItem(subscription, PlanIds.Single());\n        _prevStorage = item?.Quantity ?? 0;\n        return new()\n        {\n            new SubscriptionItemOptions\n            {\n                Id = item?.Id,\n                Plan = _plan,\n                Quantity = _additionalStorage,\n                Deleted = (item?.Id != null && _additionalStorage == 0) ? true : (bool?)null,\n            }\n        };\n    }\n\n    public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)\n    {\n        if (!_prevStorage.HasValue)\n        {\n            throw new Exception(\"Unknown previous value, must first call UpgradeItemsOptions\");\n        }\n\n        var item = FindSubscriptionItem(subscription, PlanIds.Single());\n        return new()\n        {\n            new SubscriptionItemOptions\n            {\n                Id = item?.Id,\n                Plan = _plan,\n                Quantity = _prevStorage.Value,\n                Deleted = _prevStorage.Value == 0 ? true : (bool?)null,\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/SubscriptionCreateOptions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Stripe;\nusing Plan = Bit.Core.Models.StaticStore.Plan;\n\nnamespace Bit.Core.Models.Business;\n\npublic class OrganizationSubscriptionOptionsBase : SubscriptionCreateOptions\n{\n    public OrganizationSubscriptionOptionsBase(\n        Organization org,\n        Plan plan,\n        TaxInfo taxInfo,\n        int additionalSeats,\n        int additionalStorageGb,\n        bool premiumAccessAddon,\n        bool useSecretsManager,\n        int additionalSmSeats,\n        int additionalServiceAccounts)\n    {\n        Items = new List<SubscriptionItemOptions>();\n        Metadata = new Dictionary<string, string>\n        {\n            [org.GatewayIdField()] = org.Id.ToString()\n        };\n\n        AddPlanIdToSubscription(plan);\n\n        if (useSecretsManager)\n        {\n            AddSecretsManagerSeat(plan, additionalSmSeats);\n            AddServiceAccount(plan, additionalServiceAccounts);\n        }\n\n        AddPremiumAccessAddon(plan, premiumAccessAddon);\n        AddPasswordManagerSeat(plan, additionalSeats);\n        AddAdditionalStorage(plan, additionalStorageGb);\n    }\n\n    private void AddSecretsManagerSeat(Plan plan, int additionalSmSeats)\n    {\n        if (additionalSmSeats > 0 && plan.SecretsManager.StripeSeatPlanId != null)\n        {\n            Items.Add(new SubscriptionItemOptions\n            {\n                Plan = plan.SecretsManager.StripeSeatPlanId,\n                Quantity = additionalSmSeats\n            });\n        }\n    }\n\n    private void AddPasswordManagerSeat(Plan plan, int additionalSeats)\n    {\n        if (additionalSeats > 0 && plan.PasswordManager.StripeSeatPlanId != null)\n        {\n            Items.Add(new SubscriptionItemOptions\n            {\n                Plan = plan.PasswordManager.StripeSeatPlanId,\n                Quantity = additionalSeats\n            });\n        }\n    }\n\n    private void AddServiceAccount(Plan plan, int additionalServiceAccounts)\n    {\n        if (additionalServiceAccounts > 0 && plan.SecretsManager.StripeServiceAccountPlanId != null)\n        {\n            Items.Add(new SubscriptionItemOptions\n            {\n                Plan = plan.SecretsManager.StripeServiceAccountPlanId,\n                Quantity = additionalServiceAccounts\n            });\n        }\n    }\n\n    private void AddAdditionalStorage(Plan plan, int additionalStorageGb)\n    {\n        if (additionalStorageGb > 0)\n        {\n            Items.Add(new SubscriptionItemOptions\n            {\n                Plan = plan.PasswordManager.StripeStoragePlanId,\n                Quantity = additionalStorageGb\n            });\n        }\n    }\n\n    private void AddPremiumAccessAddon(Plan plan, bool premiumAccessAddon)\n    {\n        if (premiumAccessAddon && plan.PasswordManager.StripePremiumAccessPlanId != null)\n        {\n            Items.Add(new SubscriptionItemOptions\n            {\n                Plan = plan.PasswordManager.StripePremiumAccessPlanId,\n                Quantity = 1\n            });\n        }\n    }\n\n    private void AddPlanIdToSubscription(Plan plan)\n    {\n        if (plan.PasswordManager.StripePlanId != null)\n        {\n            Items.Add(new SubscriptionItemOptions\n            {\n                Plan = plan.PasswordManager.StripePlanId,\n                Quantity = 1\n            });\n        }\n    }\n}\n\npublic class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase\n{\n    public OrganizationPurchaseSubscriptionOptions(\n        Organization org,\n        Plan plan,\n        TaxInfo taxInfo,\n        int additionalSeats,\n        int additionalStorageGb,\n        bool premiumAccessAddon,\n        int additionalSmSeats,\n        int additionalServiceAccounts) :\n        base(org, plan, taxInfo, additionalSeats,\n            additionalStorageGb, premiumAccessAddon,\n            org.UseSecretsManager, additionalSmSeats,\n            additionalServiceAccounts)\n    {\n        OffSession = true;\n        TrialPeriodDays = plan.TrialPeriodDays;\n    }\n}\n\npublic class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOptionsBase\n{\n    public OrganizationUpgradeSubscriptionOptions(\n        string customerId,\n        Organization org,\n        Plan plan,\n        OrganizationUpgrade upgrade) :\n        base(org, plan, upgrade.TaxInfo, upgrade.AdditionalSeats,\n            upgrade.AdditionalStorageGb, upgrade.PremiumAccessAddon,\n            upgrade.UseSecretsManager, upgrade.AdditionalSmSeats.GetValueOrDefault(),\n            upgrade.AdditionalServiceAccounts.GetValueOrDefault())\n    {\n        Customer = customerId;\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/SubscriptionInfo.cs",
    "content": "﻿using Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models;\nusing Stripe;\n\n#nullable enable\n\nnamespace Bit.Core.Models.Business;\n\npublic class SubscriptionInfo\n{\n    /// <summary>\n    /// Converts Stripe's minor currency units (cents) to major currency units (dollars).\n    /// IMPORTANT: Only supports USD. All Bitwarden subscriptions are USD-only.\n    /// </summary>\n    private const decimal StripeMinorUnitDivisor = 100M;\n\n    /// <summary>\n    /// Converts Stripe's minor currency units (cents) to major currency units (dollars).\n    /// Preserves null semantics to distinguish between \"no amount\" (null) and \"zero amount\" (0.00m).\n    /// </summary>\n    /// <param name=\"amountInCents\">The amount in Stripe's minor currency units (e.g., cents for USD).</param>\n    /// <returns>The amount in major currency units (e.g., dollars for USD), or null if the input is null.</returns>\n    private static decimal? ConvertFromStripeMinorUnits(long? amountInCents)\n    {\n        return amountInCents.HasValue ? amountInCents.Value / StripeMinorUnitDivisor : null;\n    }\n\n    public BillingCustomerDiscount? CustomerDiscount { get; set; }\n    public BillingSubscription? Subscription { get; set; }\n    public BillingUpcomingInvoice? UpcomingInvoice { get; set; }\n\n    /// <summary>\n    /// Represents customer discount information from Stripe billing.\n    /// </summary>\n    public class BillingCustomerDiscount\n    {\n        public BillingCustomerDiscount() { }\n\n        /// <summary>\n        /// Creates a BillingCustomerDiscount from a Stripe Discount object.\n        /// </summary>\n        /// <param name=\"discount\">The Stripe discount containing coupon and expiration information.</param>\n        public BillingCustomerDiscount(Discount discount)\n        {\n            Id = discount.Coupon?.Id;\n            // Active = true only for perpetual/recurring discounts (no end date)\n            // This is intentional for Milestone 2 - only perpetual discounts are shown in UI\n            Active = discount.End == null;\n            PercentOff = discount.Coupon?.PercentOff;\n            AmountOff = ConvertFromStripeMinorUnits(discount.Coupon?.AmountOff);\n            // Stripe's CouponAppliesTo.Products is already IReadOnlyList<string>, so no conversion needed\n            AppliesTo = discount.Coupon?.AppliesTo?.Products;\n        }\n\n        /// <summary>\n        /// The Stripe coupon ID (e.g., \"cm3nHfO1\").\n        /// Note: Only specific coupon IDs are displayed in the UI based on feature flag configuration,\n        /// though Stripe may apply additional discounts that are not shown.\n        /// </summary>\n        public string? Id { get; set; }\n\n        /// <summary>\n        /// True only for perpetual/recurring discounts (End == null).\n        /// False for any discount with an expiration date, even if not yet expired.\n        /// Product decision for Milestone 2: only show perpetual discounts in UI.\n        /// </summary>\n        public bool Active { get; set; }\n\n        /// <summary>\n        /// Percentage discount applied to the subscription (e.g., 20.0 for 20% off).\n        /// Null if this is an amount-based discount.\n        /// </summary>\n        public decimal? PercentOff { get; set; }\n\n        /// <summary>\n        /// Fixed amount discount in USD (e.g., 14.00 for $14 off).\n        /// Converted from Stripe's cent-based values (1400 cents → $14.00).\n        /// Null if this is a percentage-based discount.\n        /// </summary>\n        public decimal? AmountOff { get; set; }\n\n        /// <summary>\n        /// List of Stripe product IDs that this discount applies to (e.g., [\"prod_premium\", \"prod_families\"]).\n        /// <para>\n        /// Null: discount applies to all products with no restrictions (AppliesTo not specified in Stripe).\n        /// Empty list: discount restricted to zero products (edge case - AppliesTo.Products = [] in Stripe).\n        /// Non-empty list: discount applies only to the specified product IDs.\n        /// </para>\n        /// </summary>\n        public IReadOnlyList<string>? AppliesTo { get; set; }\n    }\n\n    public class BillingSubscription\n    {\n        public BillingSubscription(Subscription sub)\n        {\n            Status = sub?.Status;\n            TrialStartDate = sub?.TrialStart;\n            TrialEndDate = sub?.TrialEnd;\n            var currentPeriod = sub?.GetCurrentPeriod();\n            if (currentPeriod != null)\n            {\n                var (start, end) = currentPeriod.Value;\n                PeriodStartDate = start;\n                PeriodEndDate = end;\n            }\n            CancelledDate = sub?.CanceledAt;\n            CancelAtEndDate = sub?.CancelAtPeriodEnd ?? false;\n            var status = sub?.Status;\n            Cancelled = status == \"canceled\" || status == \"unpaid\" || status == \"incomplete_expired\";\n            if (sub?.Items?.Data != null)\n            {\n                Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i));\n            }\n            CollectionMethod = sub?.CollectionMethod;\n            GracePeriod = sub?.CollectionMethod == \"charge_automatically\"\n                ? 14\n                : 30;\n        }\n\n        public DateTime? TrialStartDate { get; set; }\n        public DateTime? TrialEndDate { get; set; }\n        public DateTime? PeriodStartDate { get; set; }\n        public DateTime? PeriodEndDate { get; set; }\n        public TimeSpan? PeriodDuration => PeriodEndDate - PeriodStartDate;\n        public DateTime? CancelledDate { get; set; }\n        public bool CancelAtEndDate { get; set; }\n        public string? Status { get; set; }\n        public bool Cancelled { get; set; }\n        public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();\n        public string? CollectionMethod { get; set; }\n        public DateTime? SuspensionDate { get; set; }\n        public DateTime? UnpaidPeriodEndDate { get; set; }\n        public int GracePeriod { get; set; }\n\n        public class BillingSubscriptionItem\n        {\n            public BillingSubscriptionItem(SubscriptionItem item)\n            {\n                if (item.Plan != null)\n                {\n                    ProductId = item.Plan.ProductId;\n                    Name = item.Plan.Nickname;\n                    Amount = ConvertFromStripeMinorUnits(item.Plan.Amount) ?? 0;\n                    Interval = item.Plan.Interval;\n\n                    if (item.Metadata != null)\n                    {\n                        AddonSubscriptionItem = item.Metadata.TryGetValue(\"isAddOn\", out var value) && bool.Parse(value);\n                    }\n                }\n\n                Quantity = (int)item.Quantity;\n                SponsoredSubscriptionItem = item.Plan != null && SponsoredPlans.All.Any(p => p.StripePlanId == item.Plan.Id);\n            }\n\n            public bool AddonSubscriptionItem { get; set; }\n            public string? ProductId { get; set; }\n            public string? Name { get; set; }\n            public decimal Amount { get; set; }\n            public int Quantity { get; set; }\n            public string? Interval { get; set; }\n            public bool SponsoredSubscriptionItem { get; set; }\n        }\n    }\n\n    public class BillingUpcomingInvoice\n    {\n        public BillingUpcomingInvoice() { }\n\n        public BillingUpcomingInvoice(Invoice inv)\n        {\n            Amount = ConvertFromStripeMinorUnits(inv.AmountDue) ?? 0;\n            Date = inv.Created;\n        }\n\n        public BillingUpcomingInvoice(Braintree.Subscription sub)\n        {\n            Amount = sub.NextBillAmount.GetValueOrDefault() + sub.Balance.GetValueOrDefault();\n            if (Amount < 0)\n            {\n                Amount = 0;\n            }\n            Date = sub.NextBillingDate;\n        }\n\n        public decimal Amount { get; set; }\n        public DateTime? Date { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Business/SubscriptionUpdate.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Billing.Enums;\nusing Stripe;\n\nnamespace Bit.Core.Models.Business;\n\npublic abstract class SubscriptionUpdate\n{\n    protected abstract List<string> PlanIds { get; }\n\n    public abstract List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription);\n    public abstract List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription);\n\n    public virtual bool UpdateNeeded(Subscription subscription)\n    {\n        var upgradeItemsOptions = UpgradeItemsOptions(subscription);\n        foreach (var upgradeItemOptions in upgradeItemsOptions)\n        {\n            var upgradeQuantity = upgradeItemOptions.Quantity ?? 0;\n            var existingQuantity = FindSubscriptionItem(subscription, upgradeItemOptions.Plan)?.Quantity ?? 0;\n            if (upgradeQuantity != existingQuantity)\n            {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    protected static SubscriptionItem FindSubscriptionItem(Subscription subscription, string planId)\n    {\n        if (string.IsNullOrEmpty(planId))\n        {\n            return null;\n        }\n\n        var data = subscription.Items.Data;\n\n        var subscriptionItem = data.FirstOrDefault(item => item.Plan?.Id == planId) ?? data.FirstOrDefault(item => item.Price?.Id == planId);\n\n        return subscriptionItem;\n    }\n\n    protected static string GetPasswordManagerPlanId(StaticStore.Plan plan)\n        => IsNonSeatBasedPlan(plan)\n            ? plan.PasswordManager.StripePlanId\n            : plan.PasswordManager.StripeSeatPlanId;\n\n    protected static bool IsNonSeatBasedPlan(StaticStore.Plan plan)\n        => plan.Type is\n            >= PlanType.FamiliesAnnually2019 and <= PlanType.EnterpriseAnnually2019\n            or PlanType.FamiliesAnnually2025\n            or PlanType.FamiliesAnnually\n            or PlanType.TeamsStarter2023\n            or PlanType.TeamsStarter;\n}\n"
  },
  {
    "path": "src/Core/Models/Business/TaxInfo.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Business;\n\npublic class TaxInfo\n{\n    public string TaxIdNumber { get; set; }\n    public string TaxIdType { get; set; }\n\n    public string BillingAddressLine1 { get; set; }\n    public string BillingAddressLine2 { get; set; }\n    public string BillingAddressCity { get; set; }\n    public string BillingAddressState { get; set; }\n    public string BillingAddressPostalCode { get; set; }\n    public string BillingAddressCountry { get; set; } = Constants.CountryAbbreviations.UnitedStates;\n}\n"
  },
  {
    "path": "src/Core/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenable.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.Models.Business.Tokenables;\n\npublic class OrganizationSponsorshipOfferTokenable : Tokenable\n{\n    public const string ClearTextPrefix = \"BWOrganizationSponsorship_\";\n    public const string DataProtectorPurpose = \"OrganizationSponsorshipDataProtector\";\n    public const string TokenIdentifier = \"OrganizationSponsorshipOfferToken\";\n    public string Identifier { get; set; } = TokenIdentifier;\n    public Guid Id { get; set; }\n    public PlanSponsorshipType SponsorshipType { get; set; }\n    public string Email { get; set; }\n\n    public override bool Valid => !string.IsNullOrWhiteSpace(Email) &&\n        Identifier == TokenIdentifier &&\n        Id != default;\n\n\n    [JsonConstructor]\n    public OrganizationSponsorshipOfferTokenable() { }\n\n    public OrganizationSponsorshipOfferTokenable(OrganizationSponsorship sponsorship)\n    {\n        if (string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail))\n        {\n            throw new ArgumentException(\"Invalid OrganizationSponsorship to create a token, OfferedToEmail is required\", nameof(sponsorship));\n        }\n        Email = sponsorship.OfferedToEmail;\n\n        if (!sponsorship.PlanSponsorshipType.HasValue)\n        {\n            throw new ArgumentException(\"Invalid OrganizationSponsorship to create a token, PlanSponsorshipType is required\", nameof(sponsorship));\n        }\n        SponsorshipType = sponsorship.PlanSponsorshipType.Value;\n\n        if (sponsorship.Id == default)\n        {\n            throw new ArgumentException(\"Invalid OrganizationSponsorship to create a token, Id is required\", nameof(sponsorship));\n        }\n        Id = sponsorship.Id;\n    }\n\n    public bool IsValid(OrganizationSponsorship sponsorship, string currentUserEmail) =>\n        sponsorship != null &&\n        sponsorship.PlanSponsorshipType.HasValue &&\n        SponsorshipType == sponsorship.PlanSponsorshipType.Value &&\n        Id == sponsorship.Id &&\n        !string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail) &&\n        Email.Equals(currentUserEmail, StringComparison.InvariantCultureIgnoreCase) &&\n        Email.Equals(sponsorship.OfferedToEmail, StringComparison.InvariantCultureIgnoreCase);\n\n}\n"
  },
  {
    "path": "src/Core/Models/Data/CollectionAccessDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Data;\n\npublic class CollectionAccessDetails\n{\n    public IEnumerable<CollectionAccessSelection> Groups { get; set; }\n    public IEnumerable<CollectionAccessSelection> Users { get; set; }\n}\n\n"
  },
  {
    "path": "src/Core/Models/Data/CollectionAccessSelection.cs",
    "content": "﻿namespace Bit.Core.Models.Data;\n\npublic class CollectionAccessSelection\n{\n    public Guid Id { get; set; }\n    public bool ReadOnly { get; set; }\n    public bool HidePasswords { get; set; }\n    public bool Manage { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/CollectionAdminDetails.cs",
    "content": "﻿#nullable enable\nnamespace Bit.Core.Models.Data;\n\n/// <summary>\n/// Collection information that includes permission details for a particular user along with optional\n/// access relationships for Groups/Users. Used for collection management.\n/// </summary>\npublic class CollectionAdminDetails : CollectionDetails\n{\n    public IEnumerable<CollectionAccessSelection> Groups { get; set; } = [];\n    public IEnumerable<CollectionAccessSelection> Users { get; set; } = [];\n\n    /// <summary>\n    /// Flag for whether the user has been explicitly assigned to the collection either directly or through a group.\n    /// </summary>\n    public bool Assigned { get; set; }\n\n    /// <summary>\n    /// Flag for whether a collection is managed by an active user or group.\n    /// </summary>\n    public bool Unmanaged { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/CollectionDetails.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.Models.Data;\n\n/// <summary>\n/// Collection information that includes permission details for a particular user\n/// </summary>\npublic class CollectionDetails : Collection\n{\n    public bool ReadOnly { get; set; }\n    public bool HidePasswords { get; set; }\n    public bool Manage { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/InstallationDeviceEntity.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Azure;\nusing Azure.Data.Tables;\n\nnamespace Bit.Core.Models.Data;\n\npublic class InstallationDeviceEntity : ITableEntity\n{\n    public InstallationDeviceEntity() { }\n\n    public InstallationDeviceEntity(Guid installationId, Guid deviceId)\n    {\n        PartitionKey = installationId.ToString();\n        RowKey = deviceId.ToString();\n    }\n\n    public InstallationDeviceEntity(string prefixedDeviceId)\n    {\n        var parts = prefixedDeviceId.Split(\"_\");\n        if (parts.Length < 2)\n        {\n            throw new ArgumentException(\"Not enough parts.\");\n        }\n        if (!Guid.TryParse(parts[0], out var installationId) || !Guid.TryParse(parts[1], out var deviceId))\n        {\n            throw new ArgumentException(\"Could not parse parts.\");\n        }\n        PartitionKey = parts[0];\n        RowKey = parts[1];\n    }\n\n    public string PartitionKey { get; set; }\n    public string RowKey { get; set; }\n    public DateTimeOffset? Timestamp { get; set; }\n    public ETag ETag { get; set; }\n\n    public static bool IsInstallationDeviceId(string deviceId)\n    {\n        return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_';\n    }\n    public static bool TryParse(string deviceId, out InstallationDeviceEntity installationDeviceEntity)\n    {\n        installationDeviceEntity = null;\n        var installationId = Guid.Empty;\n        var deviceIdGuid = Guid.Empty;\n        if (!IsInstallationDeviceId(deviceId))\n        {\n            return false;\n        }\n        var parts = deviceId.Split(\"_\");\n        if (parts.Length < 2)\n        {\n            return false;\n        }\n        if (!Guid.TryParse(parts[0], out installationId) || !Guid.TryParse(parts[1], out deviceIdGuid))\n        {\n            return false;\n        }\n        installationDeviceEntity = new InstallationDeviceEntity(installationId, deviceIdGuid);\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/Organizations/ClaimedUserDomainClaimedEmails.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.Models.Data.Organizations;\n\npublic record ClaimedUserDomainClaimedEmails(IEnumerable<string> EmailList, Organization Organization, string DomainName);\n"
  },
  {
    "path": "src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationConnections;\n\npublic class OrganizationConnectionData<T> where T : IConnectionConfig\n{\n    public Guid? Id { get; set; }\n    public OrganizationConnectionType Type { get; set; }\n    public Guid OrganizationId { get; set; }\n    public bool Enabled { get; set; }\n    public T Config { get; set; }\n\n    public OrganizationConnection ToEntity()\n    {\n        var result = new OrganizationConnection()\n        {\n            Type = Type,\n            OrganizationId = OrganizationId,\n            Enabled = Enabled,\n        };\n        result.SetConfig(Config);\n\n        if (Id.HasValue)\n        {\n            result.Id = Id.Value;\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/Organizations/OrganizationDomainSsoDetailsData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Data.Organizations;\n\npublic class OrganizationDomainSsoDetailsData\n{\n    public Guid OrganizationId { get; set; }\n    public string OrganizationName { get; set; }\n    public string DomainName { get; set; }\n    public bool SsoAvailable { get; set; }\n    public string OrganizationIdentifier { get; set; }\n    public DateTime? VerifiedDate { get; set; }\n    public bool OrganizationEnabled { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\n\npublic class OrganizationSponsorshipData\n{\n    public OrganizationSponsorshipData() { }\n    public OrganizationSponsorshipData(OrganizationSponsorship sponsorship)\n    {\n        SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId;\n        SponsoredOrganizationId = sponsorship.SponsoredOrganizationId;\n        FriendlyName = sponsorship.FriendlyName;\n        OfferedToEmail = sponsorship.OfferedToEmail;\n        PlanSponsorshipType = sponsorship.PlanSponsorshipType.GetValueOrDefault();\n        LastSyncDate = sponsorship.LastSyncDate;\n        ValidUntil = sponsorship.ValidUntil;\n        ToDelete = sponsorship.ToDelete;\n        IsAdminInitiated = sponsorship.IsAdminInitiated;\n        Notes = sponsorship.Notes;\n    }\n    public Guid SponsoringOrganizationUserId { get; set; }\n    public Guid? SponsoredOrganizationId { get; set; }\n    public string FriendlyName { get; set; }\n    public string OfferedToEmail { get; set; }\n    public PlanSponsorshipType PlanSponsorshipType { get; set; }\n    public DateTime? LastSyncDate { get; set; }\n    public DateTime? ValidUntil { get; set; }\n    public bool ToDelete { get; set; }\n    public bool IsAdminInitiated { get; set; }\n    public string Notes { get; set; }\n\n    public bool CloudSponsorshipRemoved { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipSyncData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\n\npublic class OrganizationSponsorshipSyncData\n{\n    public string BillingSyncKey { get; set; }\n    public Guid SponsoringOrganizationCloudId { get; set; }\n    public IEnumerable<OrganizationSponsorshipData> SponsorshipsBatch { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationSeatCounts.cs",
    "content": "﻿namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\npublic class OrganizationSeatCounts\n{\n    public int Users { get; set; }\n    public int Sponsored { get; set; }\n    public int Total => Users + Sponsored;\n}\n"
  },
  {
    "path": "src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Data.Organizations;\n\npublic class VerifiedOrganizationDomainSsoDetail\n{\n    public VerifiedOrganizationDomainSsoDetail()\n    {\n    }\n\n    public VerifiedOrganizationDomainSsoDetail(Guid organizationId, string organizationName, string domainName,\n        string organizationIdentifier)\n    {\n        OrganizationId = organizationId;\n        OrganizationName = organizationName;\n        DomainName = domainName;\n        OrganizationIdentifier = organizationIdentifier;\n    }\n\n    public Guid OrganizationId { get; init; }\n    public string OrganizationName { get; init; }\n    public string DomainName { get; init; }\n    public string OrganizationIdentifier { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/PageOptions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Data;\n\npublic class PageOptions\n{\n    public string ContinuationToken { get; set; }\n    public int PageSize { get; set; } = 50;\n}\n"
  },
  {
    "path": "src/Core/Models/Data/PagedResult.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Data;\n\npublic class PagedResult<T>\n{\n    public List<T> Data { get; set; } = new List<T>();\n    public string ContinuationToken { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/UserKdfInformation.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.Models.Data;\n\npublic class UserKdfInformation\n{\n    public required KdfType Kdf { get; set; }\n    public required int KdfIterations { get; set; }\n    public int? KdfMemory { get; set; }\n    public int? KdfParallelism { get; set; }\n    public string? MasterPasswordSalt { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Data/UserWithCalculatedPremium.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.Models.Data;\n\n/// <summary>\n/// Represents a user with an additional property indicating if the user has premium access.\n/// </summary>\npublic class UserWithCalculatedPremium : User\n{\n    public UserWithCalculatedPremium() { }\n\n    public UserWithCalculatedPremium(User user)\n    {\n        Id = user.Id;\n        Name = user.Name;\n        Email = user.Email;\n        EmailVerified = user.EmailVerified;\n        MasterPassword = user.MasterPassword;\n        MasterPasswordHint = user.MasterPasswordHint;\n        Culture = user.Culture;\n        SecurityStamp = user.SecurityStamp;\n        TwoFactorProviders = user.TwoFactorProviders;\n        TwoFactorRecoveryCode = user.TwoFactorRecoveryCode;\n        EquivalentDomains = user.EquivalentDomains;\n        ExcludedGlobalEquivalentDomains = user.ExcludedGlobalEquivalentDomains;\n        AccountRevisionDate = user.AccountRevisionDate;\n        Key = user.Key;\n        PublicKey = user.PublicKey;\n        PrivateKey = user.PrivateKey;\n        Premium = user.Premium;\n        PremiumExpirationDate = user.PremiumExpirationDate;\n        RenewalReminderDate = user.RenewalReminderDate;\n        Storage = user.Storage;\n        MaxStorageGb = user.MaxStorageGb;\n        Gateway = user.Gateway;\n        GatewayCustomerId = user.GatewayCustomerId;\n        GatewaySubscriptionId = user.GatewaySubscriptionId;\n        ReferenceData = user.ReferenceData;\n        LicenseKey = user.LicenseKey;\n        ApiKey = user.ApiKey;\n        Kdf = user.Kdf;\n        KdfIterations = user.KdfIterations;\n        KdfMemory = user.KdfMemory;\n        KdfParallelism = user.KdfParallelism;\n        CreationDate = user.CreationDate;\n        RevisionDate = user.RevisionDate;\n        ForcePasswordReset = user.ForcePasswordReset;\n        UsesKeyConnector = user.UsesKeyConnector;\n        FailedLoginCount = user.FailedLoginCount;\n        LastFailedLoginDate = user.LastFailedLoginDate;\n        AvatarColor = user.AvatarColor;\n        LastPasswordChangeDate = user.LastPasswordChangeDate;\n        LastKdfChangeDate = user.LastKdfChangeDate;\n        LastKeyRotationDate = user.LastKeyRotationDate;\n        LastEmailChangeDate = user.LastEmailChangeDate;\n    }\n\n    /// <summary>\n    /// Indicates if the user has premium access, either individually or through an organization.\n    /// </summary>\n    public bool HasPremiumAccess { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/IExternal.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models;\n\npublic interface IExternal\n{\n    string ExternalId { get; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/AddedCreditViewModel.cs",
    "content": "﻿namespace Bit.Core.Models.Mail;\n\npublic class AddedCreditViewModel : BaseMailModel\n{\n    public decimal Amount { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/AdminResetPasswordViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class AdminResetPasswordViewModel : BaseMailModel\n{\n    public string UserName { get; set; }\n    public string OrgName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs",
    "content": "﻿namespace Bit.Core.Models.Mail.Auth;\n\n/// <summary>\n/// Send email OTP view model\n/// </summary>\npublic class DefaultEmailOtpViewModel : BaseMailModel\n{\n    public string? Token { get; set; }\n    public string? TheDate { get; set; }\n    public string? TheTime { get; set; }\n    public string? TimeZone { get; set; }\n    public string? Expiry { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Auth/OrganizationWelcomeEmailViewModel.cs",
    "content": "﻿namespace Bit.Core.Models.Mail.Auth;\n\npublic class OrganizationWelcomeEmailViewModel : BaseMailModel\n{\n    public required string OrganizationName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/BaseMailModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class BaseMailModel\n{\n    public string SiteName { get; set; }\n    public string WebVaultUrl { get; set; }\n    public string WebVaultUrlHostname\n    {\n        get\n        {\n            if (Uri.TryCreate(WebVaultUrl, UriKind.Absolute, out Uri uri))\n            {\n                return uri.Host;\n            }\n\n            return WebVaultUrl;\n        }\n    }\n    public string CurrentYear\n    {\n        get\n        {\n            return DateTime.UtcNow.Year.ToString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/BaseTitleContactUsMailModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class BaseTitleContactUsMailModel : BaseMailModel\n{\n    public string TitleFirst { get; set; }\n    public string TitleSecondBold { get; set; }\n    public string TitleThird { get; set; }\n}\n\n"
  },
  {
    "path": "src/Core/Models/Mail/Billing/BusinessUnitConversionInviteModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail.Billing;\n\npublic class BusinessUnitConversionInviteModel : BaseMailModel\n{\n    public string OrganizationId { get; set; }\n    public string Email { get; set; }\n    public string Token { get; set; }\n\n    public string Url =>\n        $\"{WebVaultUrl}/providers/setup-business-unit?organizationId={OrganizationId}&email={Email}&token={Token}\";\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Billing/Renewal/Families2019Renewal/Families2019RenewalMailView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;\n\npublic class Families2019RenewalMailView : BaseMailView\n{\n    public required string BaseMonthlyRenewalPrice { get; set; }\n    public required string BaseAnnualRenewalPrice { get; set; }\n    public required string DiscountedAnnualRenewalPrice { get; set; }\n    public required string DiscountAmount { get; set; }\n}\n\npublic class Families2019RenewalMail : BaseMail<Families2019RenewalMailView>\n{\n    public override string Subject { get; set; } = \"Your Bitwarden Families renewal is updating\";\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Billing/Renewal/Families2019Renewal/Families2019RenewalMailView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        \n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 0px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:580px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 20px 0px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:15px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">Your Bitwarden Families subscription renews in 15 days. The price is\n            updating to {{ BaseMonthlyRenewalPrice }}/month, billed annually at\n            {{ BaseAnnualRenewalPrice }} + tax.</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">As a long time Bitwarden customer, you will receive a one-time\n            {{ DiscountAmount }} loyalty discount for this year's renewal. This\n            renewal will now be billed annually at\n            {{ DiscountedAnnualRenewalPrice }} + tax.</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">Questions? Contact\n            <a href=\"mailto:support@bitwarden.com\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">support@bitwarden.com</a></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0 20px 20px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin-top: 0px;\">\n              Learn more about Bitwarden\n            </p>\n            Find user guides, product documentation, and videos on the\n            <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/Models/Mail/Billing/Renewal/Families2019Renewal/Families2019RenewalMailView.text.hbs",
    "content": "﻿Your Bitwarden Families subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually\nat {{BaseAnnualRenewalPrice}} + tax.\n\nAs a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this year's renewal.\nThis renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.\n\nQuestions? Contact support@bitwarden.com\n"
  },
  {
    "path": "src/Core/Models/Mail/Billing/Renewal/Families2020Renewal/Families2020RenewalMailView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;\n\npublic class Families2020RenewalMailView : BaseMailView\n{\n    public required string MonthlyRenewalPrice { get; set; }\n}\n\npublic class Families2020RenewalMail : BaseMail<Families2020RenewalMailView>\n{\n    public override string Subject { get; set; } = \"Your Bitwarden Families renewal is updating\";\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Billing/Renewal/Families2020Renewal/Families2020RenewalMailView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        \n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 0px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:580px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 20px 0px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:15px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">Your Bitwarden Families subscription renews in 15 days. The price is\n            updating to {{ MonthlyRenewalPrice }}/month, billed annually.</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">Questions? Contact\n            <a href=\"mailto:support@bitwarden.com\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">support@bitwarden.com</a></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0 20px 20px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin-top: 0px;\">\n              Learn more about Bitwarden\n            </p>\n            Find user guides, product documentation, and videos on the\n            <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/Models/Mail/Billing/Renewal/Families2020Renewal/Families2020RenewalMailView.text.hbs",
    "content": "﻿Your Bitwarden Families subscription renews in 15 days. The price is updating to {{MonthlyRenewalPrice}}/month, billed annually.\n\nQuestions? Contact support@bitwarden.com\n"
  },
  {
    "path": "src/Core/Models/Mail/Billing/Renewal/Premium/PremiumRenewalMailView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.Models.Mail.Billing.Renewal.Premium;\n\npublic class PremiumRenewalMailView : BaseMailView\n{\n    public required string BaseMonthlyRenewalPrice { get; set; }\n    public required string DiscountedAnnualRenewalPrice { get; set; }\n    public required string DiscountAmount { get; set; }\n}\n\npublic class PremiumRenewalMail : BaseMail<PremiumRenewalMailView>\n{\n    public override string Subject { get; set; } = \"Your Bitwarden Premium renewal is updating\";\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Billing/Renewal/Premium/PremiumRenewalMailView.html.hbs",
    "content": "<!doctype html>\n<html lang=\"und\" dir=\"auto\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n  <head>\n    <title></title>\n    <!--[if !mso]><!-->\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <!--<![endif]-->\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n      #outlook a { padding:0; }\n      body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }\n      table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }\n      img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }\n      p { display:block;margin:13px 0; }\n    </style>\n    <!--[if mso]>\n    <noscript>\n    <xml>\n    <o:OfficeDocumentSettings>\n      <o:AllowPNG/>\n      <o:PixelsPerInch>96</o:PixelsPerInch>\n    </o:OfficeDocumentSettings>\n    </xml>\n    </noscript>\n    <![endif]-->\n    <!--[if lte mso 11]>\n    <style type=\"text/css\">\n      .mj-outlook-group-fix { width:100% !important; }\n    </style>\n    <![endif]-->\n    \n    \n    <style type=\"text/css\">\n      @media only screen and (min-width:480px) {\n        .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.mj-column-per-70 { width:70% !important; max-width: 70%; }\n.mj-column-per-30 { width:30% !important; max-width: 30%; }\n      }\n    </style>\n    <style media=\"screen and (min-width:480px)\">\n      .moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }\n.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }\n.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }\n    </style>\n    \n    \n  \n    \n    <style type=\"text/css\">\n\n      @media only screen and (max-width:480px) {\n        .mj-bw-learn-more-footer-responsive-img {\n          display: none !important;\n        }\n      }\n    \n\n    @media only screen and (max-width:479px) {\n      table.mj-full-width-mobile { width: 100% !important; }\n      td.mj-full-width-mobile { width: auto !important; }\n    }\n  \n    </style>\n     \n    <style type=\"text/css\">\n.border-fix > table {\n    border-collapse: separate !important;\n  }\n  .border-fix > table > tbody > tr > td {\n    border-radius: 3px;\n  }\n    </style>\n    \n  </head>\n  <body style=\"word-spacing:normal;background-color:#e6e9ef;\">\n    \n    \n      <div class=\"border-fix\" style=\"background-color:#e6e9ef;\" lang=\"und\" dir=\"auto\">\n        \n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"border-fix-outlook\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div class=\"border-fix\" style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px 0px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><![endif]-->\n            \n      <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;\">\n        <tbody>\n          <tr>\n            <td>\n              \n        \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#175ddc\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n        \n      <div style=\"margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;border-radius:4px 4px 0px 0px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:580px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 5px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:150px;\">\n              \n      <img alt src=\"https://bitwarden.com/images/logo-horizontal-white.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;\" width=\"150\" height=\"30\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n        \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n      \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Main Content Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 20px 0px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:15px 10px 10px 10px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:600px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">Your Bitwarden Premium subscription renews in 15 days. The price is\n            updating to {{ BaseMonthlyRenewalPrice }}/month, billed annually.</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">As an existing Bitwarden customer, you will receive a one-time\n            {{ DiscountAmount }} loyalty discount for this year's renewal. This\n            renewal will now be billed annually at\n            {{ DiscountedAnnualRenewalPrice }} + tax.</div>\n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 15px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\">Questions? Contact\n            <a href=\"mailto:support@bitwarden.com\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\">support@bitwarden.com</a></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#ffffff\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#ffffff;background-color:#ffffff;width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0 20px 20px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Learn More Section -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:0px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" width=\"660px\" ><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:620px;\" width=\"620\" bgcolor=\"#F3F6F9\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:20px 0;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:434px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-70 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"left\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;\"><p style=\"font-size: 18px; line-height: 28px; font-weight: 500; margin-top: 0px;\">\n              Learn more about Bitwarden\n            </p>\n            Find user guides, product documentation, and videos on the\n            <a href=\"https://bitwarden.com/help/\" class=\"link\" style=\"text-decoration: none; color: #175ddc; font-weight: 600;\"> Bitwarden Help Center</a>.</div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td><td class=\"\" style=\"vertical-align:top;width:186px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-30 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" class=\"mj-bw-learn-more-footer-responsive-img\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-collapse:collapse;border-spacing:0px;\">\n        <tbody>\n          <tr>\n            <td style=\"width:94px;\">\n              \n      <img alt src=\"https://assets.bitwarden.com/email/v1/spot-community.png\" style=\"border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;\" width=\"94\" height=\"auto\">\n    \n            </td>\n          </tr>\n        </tbody>\n      </table>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    <!-- Footer -->\n      \n      <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" class=\"\" role=\"presentation\" style=\"width:660px;\" width=\"660\" ><tr><td style=\"line-height:0px;font-size:0px;mso-line-height-rule:exactly;\"><![endif]-->\n    \n      \n      <div style=\"margin:0px auto;max-width:660px;\">\n        \n        <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"width:100%;\">\n          <tbody>\n            <tr>\n              <td style=\"direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;\">\n                <!--[if mso | IE]><table role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"><tr><td class=\"\" style=\"vertical-align:top;width:620px;\" ><![endif]-->\n            \n      <div class=\"mj-column-per-100 mj-outlook-group-fix\" style=\"font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;\">\n        \n      <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"vertical-align:top;\" width=\"100%\">\n        <tbody>\n          \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:0;word-break:break-word;\">\n                  \n      \n     <!--[if mso | IE]><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" ><tr><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://x.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.reddit.com/r/Bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-reddit.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://community.bitwarden.com/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-discourse.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://github.com/bitwarden\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-github.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-youtube.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.linkedin.com/company/bitwarden1/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-linkedin.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td><td><![endif]-->\n              <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"float:none;display:inline-table;\">\n                <tbody>\n                  \n      <tr>\n        <td style=\"padding:8px;vertical-align:middle;\">\n          <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" role=\"presentation\" style=\"border-radius:3px;width:24px;\">\n            <tbody>\n              <tr>\n                <td style=\"font-size:0;height:24px;vertical-align:middle;width:24px;\">\n                  <a href=\"https://www.facebook.com/bitwarden/\" target=\"_blank\">\n                    <img alt height=\"24\" src=\"https://assets.bitwarden.com/email/v1/social-icons-facebook.png\" style=\"border-radius:3px;display:block;\" width=\"24\">\n                  </a>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </td>\n        \n      </tr>\n    \n                </tbody>\n              </table>\n            <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n                </td>\n              </tr>\n            \n              <tr>\n                <td align=\"center\" style=\"font-size:0px;padding:10px 25px;word-break:break-word;\">\n                  \n      <div style=\"font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;\"><p style=\"margin-bottom: 5px; margin-top: 5px\">\n        © {{ CurrentYear }} Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102,\n        Santa Barbara, CA, USA\n      </p>\n      <p style=\"margin-top: 5px\">\n        Always confirm you are on a trusted Bitwarden domain before logging\n        in:<br>\n        <a href=\"https://bitwarden.com/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">bitwarden.com</a>\n        |\n        <a href=\"https://bitwarden.com/help/emails-from-bitwarden/\" style=\"text-decoration: none; color: #175ddc; font-weight: 400\">Learn why we include this</a>\n      </p></div>\n    \n                </td>\n              </tr>\n            \n        </tbody>\n      </table>\n    \n      </div>\n    \n          <!--[if mso | IE]></td></tr></table><![endif]-->\n              </td>\n            </tr>\n          </tbody>\n        </table>\n        \n      </div>\n    \n      \n      <!--[if mso | IE]></td></tr></table><![endif]-->\n    \n    \n      </div>\n    \n  </body>\n</html>\n  "
  },
  {
    "path": "src/Core/Models/Mail/Billing/Renewal/Premium/PremiumRenewalMailView.text.hbs",
    "content": "﻿Your Bitwarden Premium subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually.\n\nAs an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this year's renewal.\nThis renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.\n\nQuestions? Contact support@bitwarden.com\n"
  },
  {
    "path": "src/Core/Models/Mail/ChangeEmailExistsViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class ChangeEmailExistsViewModel : BaseMailModel\n{\n    public string FromEmail { get; set; }\n    public string ToEmail { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class ClaimedDomainUserNotificationViewModel : BaseTitleContactUsMailModel\n{\n    public string OrganizationName { get; init; }\n    public string DomainName { get; init; }\n    public string EmailDomain { get; init; }\n    public string UserEmail { get; init; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail.FamiliesForEnterprise;\n\npublic class FamiliesForEnterpriseOfferViewModel : BaseMailModel\n{\n    public string SponsorOrgName { get; set; }\n    public string SponsoredEmail { get; set; }\n    public string SponsorshipToken { get; set; }\n    public bool ExistingAccount { get; set; }\n    public string Url => string.Concat(\n        WebVaultUrl,\n        \"/accept-families-for-enterprise\",\n        $\"?token={SponsorshipToken}\",\n        $\"&email={SponsoredEmail}\",\n        ExistingAccount ? \"\" : \"&register=true\"\n    );\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRemoveOfferViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail.FamiliesForEnterprise;\n\npublic class FamiliesForEnterpriseRemoveOfferViewModel : BaseMailModel\n{\n    public string SponsoringOrgName { get; set; }\n    public string SponsoredOrganizationId { get; set; }\n    public string OfferAcceptanceDate { get; set; }\n    public string SubscriptionUrl =>\n        $\"{WebVaultUrl}/organizations/{SponsoredOrganizationId}/billing/subscription\";\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs",
    "content": "﻿namespace Bit.Core.Models.Mail.FamiliesForEnterprise;\n\npublic class FamiliesForEnterpriseSponsorshipRevertingViewModel : BaseMailModel\n{\n    public DateTime ExpirationDate { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/IMailQueueMessage.cs",
    "content": "﻿namespace Bit.Core.Models.Mail;\n\npublic interface IMailQueueMessage\n{\n    string Subject { get; set; }\n    IEnumerable<string> ToEmails { get; set; }\n    IEnumerable<string> BccEmails { get; set; }\n    string Category { get; set; }\n    string TemplateName { get; set; }\n    object Model { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/InvoiceUpcomingViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class InvoiceUpcomingViewModel : BaseMailModel\n{\n    public decimal AmountDue { get; set; }\n    public DateTime DueDate { get; set; }\n    public List<string> Items { get; set; }\n    public bool MentionInvoices { get; set; }\n    public string UpdateBillingInfoUrl { get; set; } = \"https://bitwarden.com/help/update-billing-info/\";\n    public string CollectionMethod { get; set; }\n    public bool HasPaymentMethod { get; set; }\n    public string PaymentMethodDescription { get; set; }\n    public string HelpUrl { get; set; } = \"https://bitwarden.com/help/\";\n    public string ContactUrl { get; set; } = \"https://bitwarden.com/contact/\";\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/LicenseExpiredViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class LicenseExpiredViewModel : BaseMailModel\n{\n    public string OrganizationName { get; set; }\n    public bool IsOrganization => !string.IsNullOrWhiteSpace(OrganizationName);\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/MailMessage.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class MailMessage\n{\n    public string Subject { get; set; }\n    public IEnumerable<string> ToEmails { get; set; }\n    public IEnumerable<string> BccEmails { get; set; }\n    public string HtmlContent { get; set; }\n    public string TextContent { get; set; }\n    public string Category { get; set; }\n    public IDictionary<string, object> MetaData { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/MailQueueMessage.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Models.Mail;\n\npublic class MailQueueMessage : IMailQueueMessage\n{\n    public string Subject { get; set; }\n    public IEnumerable<string> ToEmails { get; set; }\n    public IEnumerable<string> BccEmails { get; set; }\n    public string Category { get; set; }\n    public string TemplateName { get; set; }\n\n    [JsonConverter(typeof(HandlebarsObjectJsonConverter))]\n    public object Model { get; set; }\n\n    public MailQueueMessage() { }\n\n    public MailQueueMessage(MailMessage message, string templateName, object model)\n    {\n        Subject = message.Subject;\n        ToEmails = message.ToEmails;\n        BccEmails = message.BccEmails;\n        Category = string.IsNullOrEmpty(message.Category) ? templateName : message.Category;\n        TemplateName = templateName;\n        Model = model;\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/NewDeviceLoggedInModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class NewDeviceLoggedInModel : BaseMailModel\n{\n    public string TheDate { get; set; }\n    public string TheTime { get; set; }\n    public string TimeZone { get; set; }\n    public string IpAddress { get; set; }\n    public string DeviceType { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationDomainUnverifiedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationDomainUnverifiedViewModel\n{\n    public string Url { get; set; }\n    public string DomainName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Auth.Models.Mail;\n\npublic class OrganizationInitiateDeleteModel : BaseMailModel\n{\n    public string Url => string.Format(\"{0}/verify-recover-delete-org?orgId={1}&token={2}&name={3}\",\n        WebVaultUrl,\n        OrganizationId,\n        Token,\n        OrganizationNameUrlEncoded);\n\n    public string Token { get; set; }\n    public Guid OrganizationId { get; set; }\n    public string OrganizationName { get; set; }\n    public string OrganizationNameUrlEncoded { get; set; }\n    public string OrganizationPlan { get; set; }\n    public string OrganizationSeats { get; set; }\n    public string OrganizationBillingEmail { get; set; }\n    public string OrganizationCreationDate { get; set; }\n    public string OrganizationCreationTime { get; set; }\n    public string TimeZone { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationInvitesInfo.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Models.Business;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Models.Mail;\npublic class OrganizationInvitesInfo\n{\n    public OrganizationInvitesInfo(\n        Organization org,\n        bool orgSsoEnabled,\n        bool orgSsoLoginRequiredPolicyEnabled,\n        IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> orgUserTokenPairs,\n        Dictionary<Guid, bool> orgUserHasExistingUserDict,\n        bool initOrganization = false,\n        string inviterEmail = null\n        )\n    {\n        OrganizationName = org.DisplayName();\n        OrgSsoIdentifier = org.Identifier;\n        PlanType = org.PlanType;\n\n        IsFreeOrg = org.PlanType == PlanType.Free;\n        InitOrganization = initOrganization;\n\n        OrgSsoEnabled = orgSsoEnabled;\n        OrgSsoLoginRequiredPolicyEnabled = orgSsoLoginRequiredPolicyEnabled;\n\n        OrgUserTokenPairs = orgUserTokenPairs;\n        OrgUserHasExistingUserDict = orgUserHasExistingUserDict;\n        InviterEmail = inviterEmail;\n    }\n\n    public string OrganizationName { get; }\n    public PlanType PlanType { get; }\n    public bool IsFreeOrg { get; }\n    public bool InitOrganization { get; } = false;\n    public bool OrgSsoEnabled { get; }\n    public string OrgSsoIdentifier { get; }\n    public bool OrgSsoLoginRequiredPolicyEnabled { get; }\n    public IEnumerable<(OrganizationUser OrgUser, ExpiringToken Token)> OrgUserTokenPairs { get; }\n    public Dictionary<Guid, bool> OrgUserHasExistingUserDict { get; }\n    public string InviterEmail { get; }\n\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationSeatsAutoscaledViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationSeatsAutoscaledViewModel : BaseMailModel\n{\n    public int InitialSeatCount { get; set; }\n    public int CurrentSeatCount { get; set; }\n    public string VaultSubscriptionUrl { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationSeatsMaxReachedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationSeatsMaxReachedViewModel : BaseMailModel\n{\n    public int MaxSeatCount { get; set; }\n    public string VaultSubscriptionUrl { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationServiceAccountsMaxReachedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationServiceAccountsMaxReachedViewModel\n{\n    public int MaxServiceAccountsCount { get; set; }\n    public string VaultSubscriptionUrl { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationUserAcceptedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationUserAcceptedViewModel : BaseMailModel\n{\n    public Guid OrganizationId { get; set; }\n    public string OrganizationName { get; set; }\n    public string UserIdentifier { get; set; }\n    public string ConfirmUrl => $\"{WebVaultUrl}/organizations/{OrganizationId}/members\";\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationUserConfirmedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationUserConfirmedViewModel : BaseTitleContactUsMailModel\n{\n    public string OrganizationName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing Bit.Core.Auth.Models.Business;\nusing Bit.Core.Entities;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel\n{\n\n    // Private constructor to enforce usage of the factory method.\n    private OrganizationUserInvitedViewModel() { }\n\n    public static OrganizationUserInvitedViewModel CreateFromInviteInfo(\n        OrganizationInvitesInfo orgInvitesInfo,\n        OrganizationUser orgUser,\n        ExpiringToken expiringToken,\n        GlobalSettings globalSettings)\n    {\n        const string freeOrgTitle = \"A Bitwarden member invited you to an organization. \" +\n                                    \"Join now to start securing your passwords!\";\n\n        var userHasExistingUser = orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id];\n\n        return new OrganizationUserInvitedViewModel\n        {\n            TitleFirst = orgInvitesInfo.IsFreeOrg ? freeOrgTitle : \"Join \",\n            TitleSecondBold =\n                orgInvitesInfo.IsFreeOrg\n                    ? string.Empty\n                    : CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false),\n            TitleThird = orgInvitesInfo.IsFreeOrg ? string.Empty : \" on Bitwarden and start securing your passwords!\",\n            OrganizationName = CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false),\n            Email = WebUtility.UrlEncode(orgUser.Email),\n            OrganizationId = orgUser.OrganizationId.ToString(),\n            OrganizationUserId = orgUser.Id.ToString(),\n            Token = WebUtility.UrlEncode(expiringToken.Token),\n            ExpirationDate =\n                $\"{expiringToken.ExpirationDate.ToLongDateString()} {expiringToken.ExpirationDate.ToShortTimeString()} UTC\",\n            OrganizationNameUrlEncoded = WebUtility.UrlEncode(orgInvitesInfo.OrganizationName),\n            WebVaultUrl = globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = globalSettings.SiteName,\n            InitOrganization = orgInvitesInfo.InitOrganization,\n            OrgSsoIdentifier = orgInvitesInfo.OrgSsoIdentifier,\n            OrgSsoEnabled = orgInvitesInfo.OrgSsoEnabled,\n            OrgSsoLoginRequiredPolicyEnabled = orgInvitesInfo.OrgSsoLoginRequiredPolicyEnabled,\n            OrgUserHasExistingUser = userHasExistingUser,\n            JoinOrganizationButtonText = userHasExistingUser || orgInvitesInfo.IsFreeOrg ? \"Accept invitation\" : \"Finish account setup\",\n            IsFreeOrg = orgInvitesInfo.IsFreeOrg\n        };\n    }\n\n    public string OrganizationName { get; set; }\n    public string OrganizationId { get; set; }\n    public string OrganizationUserId { get; set; }\n    public string Email { get; set; }\n    public string OrganizationNameUrlEncoded { get; set; }\n    public string Token { get; set; }\n    public string ExpirationDate { get; set; }\n    public bool InitOrganization { get; set; }\n    public string OrgSsoIdentifier { get; set; }\n    public bool OrgSsoEnabled { get; set; }\n    public bool OrgSsoLoginRequiredPolicyEnabled { get; set; }\n    public bool OrgUserHasExistingUser { get; set; }\n    public string JoinOrganizationButtonText { get; set; } = \"Join Organization\";\n    public bool IsFreeOrg { get; set; }\n\n    public string Url\n    {\n        get\n        {\n            var baseUrl = $\"{WebVaultUrl}/accept-organization\";\n            var queryParams = new List<string>\n            {\n                $\"organizationId={OrganizationId}\",\n                $\"organizationUserId={OrganizationUserId}\",\n                $\"email={Email}\",\n                $\"organizationName={OrganizationNameUrlEncoded}\",\n                $\"token={Token}\",\n                $\"initOrganization={InitOrganization}\",\n                $\"orgUserHasExistingUser={OrgUserHasExistingUser}\"\n            };\n\n            if (OrgSsoEnabled && OrgSsoLoginRequiredPolicyEnabled)\n            {\n                // Only send down the orgSsoIdentifier if we are going to accelerate the user to the SSO login page.\n                queryParams.Add($\"orgSsoIdentifier={OrgSsoIdentifier}\");\n            }\n\n            return $\"{baseUrl}?{string.Join(\"&\", queryParams)}\";\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationUserRemovedForPolicySingleOrgViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationUserRemovedForPolicySingleOrgViewModel : BaseMailModel\n{\n    public string OrganizationName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationUserRemovedForPolicyTwoStepViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationUserRemovedForPolicyTwoStepViewModel : BaseMailModel\n{\n    public string OrganizationName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationUserRevokedForPolicySingleOrgViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationUserRevokedForPolicySingleOrgViewModel : BaseMailModel\n{\n    public string OrganizationName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/OrganizationUserRevokedForPolicyTwoFactorViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class OrganizationUserRevokedForPolicyTwoFactorViewModel : BaseMailModel\n{\n    public string OrganizationName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/PaymentFailedViewModel.cs",
    "content": "﻿namespace Bit.Core.Models.Mail;\n\npublic class PaymentFailedViewModel : BaseMailModel\n{\n    public decimal Amount { get; set; }\n    public bool MentionInvoices { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail.Provider;\n\npublic class ProviderInitiateDeleteModel : BaseMailModel\n{\n    public string Url => string.Format(\"{0}/verify-recover-delete-provider?providerId={1}&token={2}&name={3}\",\n        WebVaultUrl,\n        ProviderId,\n        Token,\n        ProviderNameUrlEncoded);\n\n    public string Token { get; set; }\n    public Guid ProviderId { get; set; }\n    public string ProviderName { get; set; }\n    public string ProviderNameUrlEncoded { get; set; }\n    public string ProviderBillingEmail { get; set; }\n    public string ProviderCreationDate { get; set; }\n    public string ProviderCreationTime { get; set; }\n    public string TimeZone { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail.Provider;\n\npublic class ProviderSetupInviteViewModel : BaseMailModel\n{\n    public string ProviderId { get; set; }\n    public string Email { get; set; }\n    public string Token { get; set; }\n    public string Url => string.Format(\"{0}/providers/setup-provider?providerId={1}&email={2}&token={3}\",\n        WebVaultUrl,\n        ProviderId,\n        Email,\n        Token);\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Provider/ProviderUpdatePaymentMethodViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail.Provider;\n\npublic class ProviderUpdatePaymentMethodViewModel : BaseMailModel\n{\n    public string OrganizationId { get; set; }\n    public string OrganizationName { get; set; }\n    public string ProviderName { get; set; }\n\n    public string PaymentMethodUrl =>\n        $\"{WebVaultUrl}/organizations/{OrganizationId}/billing/payment-method\";\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Provider/ProviderUserConfirmedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail.Provider;\n\npublic class ProviderUserConfirmedViewModel : BaseMailModel\n{\n    public string ProviderName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail.Provider;\n\npublic class ProviderUserInvitedViewModel : BaseMailModel\n{\n    public string ProviderName { get; set; }\n    public string ProviderId { get; set; }\n    public string ProviderUserId { get; set; }\n    public string Email { get; set; }\n    public string ProviderNameUrlEncoded { get; set; }\n    public string Token { get; set; }\n    public string Url => string.Format(\"{0}/providers/accept-provider?providerId={1}&\" +\n        \"providerUserId={2}&email={3}&providerName={4}&token={5}\",\n        WebVaultUrl,\n        ProviderId,\n        ProviderUserId,\n        Email,\n        ProviderNameUrlEncoded,\n        Token);\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/Provider/ProviderUserRemovedViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail.Provider;\n\npublic class ProviderUserRemovedViewModel : BaseMailModel\n{\n    public string ProviderName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class SecurityTaskNotificationViewModel : BaseMailModel\n{\n    public string OrgName { get; set; }\n\n    public int TaskCount { get; set; }\n\n    public List<string> AdminOwnerEmails { get; set; }\n\n    public string ReviewPasswordsUrl => $\"{WebVaultUrl}/browser-extension-prompt\";\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/TrustedDeviceAdminApprovalViewModel.cs",
    "content": "﻿namespace Bit.Core.Models.Mail;\n\npublic class TrustedDeviceAdminApprovalViewModel : NewDeviceLoggedInModel { }\n"
  },
  {
    "path": "src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\n/// <summary>\n/// This view model is used to set-up email two factor authentication, to log in with email two factor authentication,\n/// and for new device verification.\n/// </summary>\npublic class TwoFactorEmailTokenViewModel : BaseMailModel\n{\n    public string Token { get; set; }\n    /// <summary>\n    /// This view model is used to also set-up email two factor authentication. We use this property to communicate\n    /// the purpose of the email, since it can be used for logging in and for setting up.\n    /// </summary>\n    public string EmailTotpAction { get; set; }\n    /// <summary>\n    /// When logging in with email two factor the account email may not be the same as the email used for two factor.\n    /// we want to show the account email in the email, so the user knows which account they are logging into.\n    /// </summary>\n    public string AccountEmail { get; set; }\n    public string TheDate { get; set; }\n    public string TheTime { get; set; }\n    public string TimeZone { get; set; }\n    public string DeviceIp { get; set; }\n    public string DeviceType { get; set; }\n    /// <summary>\n    /// Depending on the context, we may want to show a reminder to the user that they should enable two factor authentication.\n    /// This is not relevant when the user is using the email to verify setting up 2FA, so we hide it in that case.\n    /// </summary>\n    public bool DisplayTwoFactorReminder { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/UpdateTempPasswordViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class UpdateTempPasswordViewModel\n{\n    public string UserName { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Mail/UserVerificationEmailTokenViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.Mail;\n\npublic class UserVerificationEmailTokenViewModel : BaseMailModel\n{\n    public string Token { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Models.OrganizationConnectionConfigs;\n\npublic class BillingSyncConfig : IConnectionConfig\n{\n    public string BillingSyncKey { get; set; }\n    public Guid CloudOrganizationId { get; set; }\n    public DateTime? LastLicenseSync { get; set; }\n\n    public bool Validate(out string exception)\n    {\n        if (string.IsNullOrWhiteSpace(BillingSyncKey))\n        {\n            exception = \"Failed to get Billing Sync Key\";\n            return false;\n        }\n\n        exception = \"\";\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/Models/OrganizationConnectionConfigs/IConnectionConfig.cs",
    "content": "﻿namespace Bit.Core.Models.OrganizationConnectionConfigs;\n\npublic interface IConnectionConfig\n{\n    bool Validate(out string exception);\n}\n"
  },
  {
    "path": "src/Core/Models/PushNotification.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.NotificationCenter.Enums;\n\nnamespace Bit.Core.Models;\n\n// New push notification payload models should not be defined in this file\n// they should instead be defined in file owned by your team.\n\npublic class PushNotificationData<T>\n{\n    public PushNotificationData(PushType type, T payload, string? contextId)\n    {\n        Type = type;\n        Payload = payload;\n        ContextId = contextId;\n    }\n\n    public PushType Type { get; set; }\n    public T Payload { get; set; }\n    public string? ContextId { get; set; }\n}\n\npublic class SyncCipherPushNotification\n{\n    public Guid Id { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public IEnumerable<Guid>? CollectionIds { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n\npublic class SyncFolderPushNotification\n{\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n\npublic class UserPushNotification\n{\n    public Guid UserId { get; set; }\n    public DateTime Date { get; set; }\n}\n\npublic class SyncSendPushNotification\n{\n    public Guid Id { get; set; }\n    public Guid UserId { get; set; }\n    public DateTime RevisionDate { get; set; }\n}\n\npublic class NotificationPushNotification\n{\n    public Guid Id { get; set; }\n    public Priority Priority { get; set; }\n    public bool Global { get; set; }\n    public ClientType ClientType { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Guid? InstallationId { get; set; }\n    public Guid? TaskId { get; set; }\n    public string? Title { get; set; }\n    public string? Body { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime RevisionDate { get; set; }\n    public DateTime? ReadDate { get; set; }\n    public DateTime? DeletedDate { get; set; }\n}\n\npublic class AuthRequestPushNotification\n{\n    public Guid UserId { get; set; }\n    public Guid Id { get; set; }\n}\n\npublic class OrganizationStatusPushNotification\n{\n    public Guid OrganizationId { get; set; }\n    public bool Enabled { get; set; }\n}\n\npublic class OrganizationCollectionManagementPushNotification\n{\n    public Guid OrganizationId { get; init; }\n    public bool LimitCollectionCreation { get; init; }\n    public bool LimitCollectionDeletion { get; init; }\n    public bool LimitItemDeletion { get; init; }\n}\n\npublic class OrganizationBankAccountVerifiedPushNotification\n{\n    public Guid OrganizationId { get; set; }\n}\n\npublic class ProviderBankAccountVerifiedPushNotification\n{\n    public Guid ProviderId { get; set; }\n    public Guid AdminId { get; set; }\n}\n\npublic class LogOutPushNotification\n{\n    public Guid UserId { get; set; }\n    public PushNotificationLogOutReason? Reason { get; set; }\n}\n\npublic class SyncPolicyPushNotification\n{\n    public Guid OrganizationId { get; set; }\n    public required Policy Policy { get; set; }\n}\n\npublic class AutoConfirmPushNotification\n{\n    /// <summary>\n    /// The admin/owner receiving this notification\n    /// </summary>\n    public Guid UserId { get; set; }\n\n    /// <summary>\n    /// The organization the user accepted an invite to\n    /// </summary>\n    public Guid OrganizationId { get; set; }\n\n    /// <summary>\n    /// The user id who accepted the organization invite (Needed for key-exchange on the client)\n    /// </summary>\n    public Guid TargetUserId { get; set; }\n    ///\n    /// <summary>\n    /// The organization user id who accepted the organization invite (will be auto-confirmed)\n    /// </summary>\n    public Guid TargetOrganizationUserId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Models/Stripe/StripeInvoiceListOptions.cs",
    "content": "﻿using Stripe;\n\nnamespace Bit.Core.Models.BitStripe;\n\n/// <summary>\n/// A model derived from the Stripe <see cref=\"InvoiceListOptions\"/> class that includes a flag used to\n/// retrieve all invoices from the Stripe API rather than a limited set.\n/// </summary>\npublic class StripeInvoiceListOptions : InvoiceListOptions\n{\n    public bool SelectAll { get; set; }\n\n    public InvoiceListOptions ToInvoiceListOptions()\n    {\n        var options = (InvoiceListOptions)this;\n\n        if (!SelectAll)\n        {\n            return options;\n        }\n\n        options.EndingBefore = null;\n        options.StartingAfter = null;\n\n        return options;\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Authorization/NotificationAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.NotificationCenter.Entities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.NotificationCenter.Authorization;\n\npublic class NotificationAuthorizationHandler : AuthorizationHandler<NotificationOperationsRequirement, Notification>\n{\n    private readonly ICurrentContext _currentContext;\n\n    public NotificationAuthorizationHandler(ICurrentContext currentContext)\n    {\n        _currentContext = currentContext;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        NotificationOperationsRequirement requirement,\n        Notification notification)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            return;\n        }\n\n        var authorized = requirement switch\n        {\n            not null when requirement == NotificationOperations.Read => CanRead(notification),\n            not null when requirement == NotificationOperations.Create => await CanCreate(notification),\n            not null when requirement == NotificationOperations.Update => await CanUpdate(notification),\n            _ => throw new ArgumentException(\"Unsupported operation requirement type provided.\", nameof(requirement))\n        };\n\n        if (authorized)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private bool CanRead(Notification notification)\n    {\n        var userMatching = !notification.UserId.HasValue || notification.UserId.Value == _currentContext.UserId!.Value;\n        var organizationMatching = !notification.OrganizationId.HasValue ||\n                                   _currentContext.GetOrganization(notification.OrganizationId.Value) != null;\n\n        return notification.Global || (userMatching && organizationMatching);\n    }\n\n    private async Task<bool> CanCreate(Notification notification)\n    {\n        var organizationPermissionsMatching = !notification.OrganizationId.HasValue ||\n                                              await _currentContext.AccessReports(notification.OrganizationId.Value);\n        var userNoOrganizationMatching = !notification.UserId.HasValue || notification.OrganizationId.HasValue ||\n                                         notification.UserId.Value == _currentContext.UserId!.Value;\n\n        return !notification.Global && organizationPermissionsMatching && userNoOrganizationMatching;\n    }\n\n    private async Task<bool> CanUpdate(Notification notification)\n    {\n        var organizationPermissionsMatching = !notification.OrganizationId.HasValue ||\n                                              await _currentContext.AccessReports(notification.OrganizationId.Value);\n        var userNoOrganizationMatching = !notification.UserId.HasValue || notification.OrganizationId.HasValue ||\n                                         notification.UserId.Value == _currentContext.UserId!.Value;\n\n        return !notification.Global && organizationPermissionsMatching && userNoOrganizationMatching;\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Authorization/NotificationOperations.cs",
    "content": "﻿#nullable enable\nusing Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.NotificationCenter.Authorization;\n\npublic class NotificationOperationsRequirement : OperationAuthorizationRequirement\n{\n    public NotificationOperationsRequirement(string name)\n    {\n        Name = name;\n    }\n}\n\npublic static class NotificationOperations\n{\n    public static readonly NotificationOperationsRequirement Read = new(nameof(Read));\n    public static readonly NotificationOperationsRequirement Create = new(nameof(Create));\n    public static readonly NotificationOperationsRequirement Update = new(nameof(Update));\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Authorization/NotificationStatusAuthorizationHandler.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.NotificationCenter.Entities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.NotificationCenter.Authorization;\n\npublic class NotificationStatusAuthorizationHandler : AuthorizationHandler<NotificationStatusOperationsRequirement,\n    NotificationStatus>\n{\n    private readonly ICurrentContext _currentContext;\n\n    public NotificationStatusAuthorizationHandler(ICurrentContext currentContext)\n    {\n        _currentContext = currentContext;\n    }\n\n    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        NotificationStatusOperationsRequirement requirement,\n        NotificationStatus notificationStatus)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            return Task.CompletedTask;\n        }\n\n        var authorized = requirement switch\n        {\n            not null when requirement == NotificationStatusOperations.Read => CanRead(notificationStatus),\n            not null when requirement == NotificationStatusOperations.Create => CanCreate(notificationStatus),\n            not null when requirement == NotificationStatusOperations.Update => CanUpdate(notificationStatus),\n            _ => throw new ArgumentException(\"Unsupported operation requirement type provided.\", nameof(requirement))\n        };\n\n        if (authorized)\n        {\n            context.Succeed(requirement);\n        }\n\n        return Task.CompletedTask;\n    }\n\n    private bool CanRead(NotificationStatus notificationStatus)\n    {\n        return notificationStatus.UserId == _currentContext.UserId!.Value;\n    }\n\n    private bool CanCreate(NotificationStatus notificationStatus)\n    {\n        return notificationStatus.UserId == _currentContext.UserId!.Value;\n    }\n\n    private bool CanUpdate(NotificationStatus notificationStatus)\n    {\n        return notificationStatus.UserId == _currentContext.UserId!.Value;\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Authorization/NotificationStatusOperations.cs",
    "content": "﻿#nullable enable\nusing Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.NotificationCenter.Authorization;\n\npublic class NotificationStatusOperationsRequirement : OperationAuthorizationRequirement\n{\n    public NotificationStatusOperationsRequirement(string name)\n    {\n        Name = name;\n    }\n}\n\npublic static class NotificationStatusOperations\n{\n    public static readonly NotificationStatusOperationsRequirement Read = new(nameof(Read));\n    public static readonly NotificationStatusOperationsRequirement Create = new(nameof(Create));\n    public static readonly NotificationStatusOperationsRequirement Update = new(nameof(Update));\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands.Interfaces;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.NotificationCenter.Commands;\n\npublic class CreateNotificationCommand : ICreateNotificationCommand\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly INotificationRepository _notificationRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    public CreateNotificationCommand(ICurrentContext currentContext,\n        IAuthorizationService authorizationService,\n        INotificationRepository notificationRepository,\n        IPushNotificationService pushNotificationService)\n    {\n        _currentContext = currentContext;\n        _authorizationService = authorizationService;\n        _notificationRepository = notificationRepository;\n        _pushNotificationService = pushNotificationService;\n    }\n\n    public async Task<Notification> CreateAsync(Notification notification, bool sendPush = true)\n    {\n        notification.CreationDate = notification.RevisionDate = DateTime.UtcNow;\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification,\n            NotificationOperations.Create);\n\n        var newNotification = await _notificationRepository.CreateAsync(notification);\n\n        if (sendPush)\n        {\n            await _pushNotificationService.PushNotificationAsync(newNotification);\n        }\n\n        return newNotification;\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands.Interfaces;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.NotificationCenter.Commands;\n\npublic class CreateNotificationStatusCommand : ICreateNotificationStatusCommand\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly INotificationRepository _notificationRepository;\n    private readonly INotificationStatusRepository _notificationStatusRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    public CreateNotificationStatusCommand(ICurrentContext currentContext,\n        IAuthorizationService authorizationService,\n        INotificationRepository notificationRepository,\n        INotificationStatusRepository notificationStatusRepository,\n        IPushNotificationService pushNotificationService)\n    {\n        _currentContext = currentContext;\n        _authorizationService = authorizationService;\n        _notificationRepository = notificationRepository;\n        _notificationStatusRepository = notificationStatusRepository;\n        _pushNotificationService = pushNotificationService;\n    }\n\n    public async Task<NotificationStatus> CreateAsync(NotificationStatus notificationStatus)\n    {\n        var notification = await _notificationRepository.GetByIdAsync(notificationStatus.NotificationId);\n        if (notification == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification,\n            NotificationOperations.Read);\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,\n            NotificationStatusOperations.Create);\n\n        var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus);\n\n        await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus);\n\n        return newNotificationStatus;\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.NotificationCenter.Entities;\n\nnamespace Bit.Core.NotificationCenter.Commands.Interfaces;\n\npublic interface ICreateNotificationCommand\n{\n    Task<Notification> CreateAsync(Notification notification, bool sendPush = true);\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationStatusCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.NotificationCenter.Entities;\n\nnamespace Bit.Core.NotificationCenter.Commands.Interfaces;\n\npublic interface ICreateNotificationStatusCommand\n{\n    Task<NotificationStatus> CreateAsync(NotificationStatus notificationStatus);\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationDeletedCommand.cs",
    "content": "﻿#nullable enable\nnamespace Bit.Core.NotificationCenter.Commands.Interfaces;\n\npublic interface IMarkNotificationDeletedCommand\n{\n    Task MarkDeletedAsync(Guid notificationId);\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationReadCommand.cs",
    "content": "﻿#nullable enable\nnamespace Bit.Core.NotificationCenter.Commands.Interfaces;\n\npublic interface IMarkNotificationReadCommand\n{\n    Task MarkReadAsync(Guid notificationId);\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/Interfaces/IUpdateNotificationCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.NotificationCenter.Entities;\n\nnamespace Bit.Core.NotificationCenter.Commands.Interfaces;\n\npublic interface IUpdateNotificationCommand\n{\n    Task UpdateAsync(Notification notification);\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands.Interfaces;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.NotificationCenter.Commands;\n\npublic class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly INotificationRepository _notificationRepository;\n    private readonly INotificationStatusRepository _notificationStatusRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    public MarkNotificationDeletedCommand(ICurrentContext currentContext,\n        IAuthorizationService authorizationService,\n        INotificationRepository notificationRepository,\n        INotificationStatusRepository notificationStatusRepository,\n        IPushNotificationService pushNotificationService)\n    {\n        _currentContext = currentContext;\n        _authorizationService = authorizationService;\n        _notificationRepository = notificationRepository;\n        _notificationStatusRepository = notificationStatusRepository;\n        _pushNotificationService = pushNotificationService;\n    }\n\n    public async Task MarkDeletedAsync(Guid notificationId)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var notification = await _notificationRepository.GetByIdAsync(notificationId);\n        if (notification == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification,\n            NotificationOperations.Read);\n\n        var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notificationId,\n            _currentContext.UserId.Value);\n\n        if (notificationStatus == null)\n        {\n            notificationStatus = new NotificationStatus\n            {\n                NotificationId = notificationId,\n                UserId = _currentContext.UserId.Value,\n                DeletedDate = DateTime.UtcNow\n            };\n\n            await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,\n                NotificationStatusOperations.Create);\n\n            var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus);\n\n            await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus);\n        }\n        else\n        {\n            await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,\n                NotificationStatusOperations.Update);\n\n            notificationStatus.DeletedDate = DateTime.UtcNow;\n\n            await _notificationStatusRepository.UpdateAsync(notificationStatus);\n\n            await _pushNotificationService.PushNotificationStatusAsync(notification, notificationStatus);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands.Interfaces;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.NotificationCenter.Commands;\n\npublic class MarkNotificationReadCommand : IMarkNotificationReadCommand\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly INotificationRepository _notificationRepository;\n    private readonly INotificationStatusRepository _notificationStatusRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    public MarkNotificationReadCommand(ICurrentContext currentContext,\n        IAuthorizationService authorizationService,\n        INotificationRepository notificationRepository,\n        INotificationStatusRepository notificationStatusRepository,\n        IPushNotificationService pushNotificationService)\n    {\n        _currentContext = currentContext;\n        _authorizationService = authorizationService;\n        _notificationRepository = notificationRepository;\n        _notificationStatusRepository = notificationStatusRepository;\n        _pushNotificationService = pushNotificationService;\n    }\n\n    public async Task MarkReadAsync(Guid notificationId)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var notification = await _notificationRepository.GetByIdAsync(notificationId);\n        if (notification == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification,\n            NotificationOperations.Read);\n\n        var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notificationId,\n            _currentContext.UserId.Value);\n\n        if (notificationStatus == null)\n        {\n            notificationStatus = new NotificationStatus\n            {\n                NotificationId = notificationId,\n                UserId = _currentContext.UserId.Value,\n                ReadDate = DateTime.UtcNow\n            };\n\n            await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,\n                NotificationStatusOperations.Create);\n\n            var newNotificationStatus = await _notificationStatusRepository.CreateAsync(notificationStatus);\n\n            await _pushNotificationService.PushNotificationStatusAsync(notification, newNotificationStatus);\n        }\n        else\n        {\n            await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User,\n                notificationStatus, NotificationStatusOperations.Update);\n\n            notificationStatus.ReadDate = DateTime.UtcNow;\n\n            await _notificationStatusRepository.UpdateAsync(notificationStatus);\n\n            await _pushNotificationService.PushNotificationStatusAsync(notification, notificationStatus);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands.Interfaces;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.NotificationCenter.Commands;\n\npublic class UpdateNotificationCommand : IUpdateNotificationCommand\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly INotificationRepository _notificationRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    public UpdateNotificationCommand(ICurrentContext currentContext,\n        IAuthorizationService authorizationService,\n        INotificationRepository notificationRepository,\n        IPushNotificationService pushNotificationService)\n    {\n        _currentContext = currentContext;\n        _authorizationService = authorizationService;\n        _notificationRepository = notificationRepository;\n        _pushNotificationService = pushNotificationService;\n    }\n\n    public async Task UpdateAsync(Notification notificationToUpdate)\n    {\n        var notification = await _notificationRepository.GetByIdAsync(notificationToUpdate.Id);\n        if (notification == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User,\n            notification, NotificationOperations.Update);\n\n        notification.Priority = notificationToUpdate.Priority;\n        notification.ClientType = notificationToUpdate.ClientType;\n        notification.Title = notificationToUpdate.Title;\n        notification.Body = notificationToUpdate.Body;\n        notification.RevisionDate = DateTime.UtcNow;\n\n        await _notificationRepository.ReplaceAsync(notification);\n\n        await _pushNotificationService.PushNotificationAsync(notification);\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Entities/Notification.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.NotificationCenter.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.NotificationCenter.Entities;\n\npublic class Notification : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Priority Priority { get; set; }\n    public bool Global { get; set; }\n    public ClientType ClientType { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    [MaxLength(256)] public string? Title { get; set; }\n    [MaxLength(3000)] public string? Body { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime RevisionDate { get; set; }\n    public Guid? TaskId { get; set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Entities/NotificationStatus.cs",
    "content": "﻿#nullable enable\nnamespace Bit.Core.NotificationCenter.Entities;\n\npublic class NotificationStatus\n{\n    public Guid NotificationId { get; set; }\n    public Guid UserId { get; set; }\n    public DateTime? ReadDate { get; set; }\n    public DateTime? DeletedDate { get; set; }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Enums/Priority.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.NotificationCenter.Enums;\n\npublic enum Priority : byte\n{\n    [Display(Name = \"Informational\")]\n    Informational = 0,\n    [Display(Name = \"Low\")]\n    Low = 1,\n    [Display(Name = \"Medium\")]\n    Medium = 2,\n    [Display(Name = \"High\")]\n    High = 3,\n    [Display(Name = \"Critical\")]\n    Critical = 4\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Models/Data/NotificationStatusDetails.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.NotificationCenter.Enums;\n\nnamespace Bit.Core.NotificationCenter.Models.Data;\n\npublic class NotificationStatusDetails\n{\n    // Notification fields\n    public Guid Id { get; set; }\n    public Priority Priority { get; set; }\n    public bool Global { get; set; }\n    public ClientType ClientType { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    [MaxLength(256)]\n    public string? Title { get; set; }\n    public string? Body { get; set; }\n    public DateTime CreationDate { get; set; }\n    public DateTime RevisionDate { get; set; }\n    public Guid? TaskId { get; set; }\n    // Notification Status fields\n    public DateTime? ReadDate { get; set; }\n    public DateTime? DeletedDate { get; set; }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Models/Filter/NotificationStatusFilter.cs",
    "content": "﻿#nullable enable\nnamespace Bit.Core.NotificationCenter.Models.Filter;\n\npublic class NotificationStatusFilter\n{\n    public bool? Read { get; set; }\n    public bool? Deleted { get; set; }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/NotificationCenterServiceCollectionExtensions.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands;\nusing Bit.Core.NotificationCenter.Commands.Interfaces;\nusing Bit.Core.NotificationCenter.Queries;\nusing Bit.Core.NotificationCenter.Queries.Interfaces;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.NotificationCenter;\n\npublic static class NotificationCenterServiceCollectionExtensions\n{\n    public static void AddNotificationCenterServices(this IServiceCollection services)\n    {\n        // Authorization Handlers\n        services.AddScoped<IAuthorizationHandler, NotificationAuthorizationHandler>();\n        services.AddScoped<IAuthorizationHandler, NotificationStatusAuthorizationHandler>();\n        // Commands\n        services.AddScoped<ICreateNotificationCommand, CreateNotificationCommand>();\n        services.AddScoped<ICreateNotificationStatusCommand, CreateNotificationStatusCommand>();\n        services.AddScoped<IMarkNotificationDeletedCommand, MarkNotificationDeletedCommand>();\n        services.AddScoped<IMarkNotificationReadCommand, MarkNotificationReadCommand>();\n        services.AddScoped<IUpdateNotificationCommand, UpdateNotificationCommand>();\n        // Queries\n        services.AddScoped<IGetNotificationStatusDetailsForUserQuery, GetNotificationStatusDetailsForUserQuery>();\n        services.AddScoped<IGetNotificationStatusForUserQuery, GetNotificationStatusForUserQuery>();\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Filter;\nusing Bit.Core.NotificationCenter.Queries.Interfaces;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.NotificationCenter.Queries;\n\npublic class GetNotificationStatusDetailsForUserQuery : IGetNotificationStatusDetailsForUserQuery\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly INotificationRepository _notificationRepository;\n\n    public GetNotificationStatusDetailsForUserQuery(ICurrentContext currentContext,\n        INotificationRepository notificationRepository)\n    {\n        _currentContext = currentContext;\n        _notificationRepository = notificationRepository;\n    }\n\n    public async Task<PagedResult<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(\n        NotificationStatusFilter statusFilter, PageOptions pageOptions)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var clientType = DeviceTypes.ToClientType(_currentContext.DeviceType);\n\n        // Note: only returns the user's notifications - no authorization check needed\n        return await _notificationRepository.GetByUserIdAndStatusAsync(_currentContext.UserId.Value, clientType,\n            statusFilter, pageOptions);\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Queries/GetNotificationStatusForUserQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Queries.Interfaces;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.NotificationCenter.Queries;\n\npublic class GetNotificationStatusForUserQuery : IGetNotificationStatusForUserQuery\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly INotificationStatusRepository _notificationStatusRepository;\n\n    public GetNotificationStatusForUserQuery(ICurrentContext currentContext,\n        IAuthorizationService authorizationService,\n        INotificationStatusRepository notificationStatusRepository)\n    {\n        _currentContext = currentContext;\n        _authorizationService = authorizationService;\n        _notificationStatusRepository = notificationStatusRepository;\n    }\n\n    public async Task<NotificationStatus> GetByNotificationIdAndUserIdAsync(Guid notificationId)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notificationId,\n            _currentContext.UserId.Value);\n        if (notificationStatus == null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User,\n            notificationStatus, NotificationStatusOperations.Read);\n\n        return notificationStatus;\n    }\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusDetailsForUserQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Filter;\n\nnamespace Bit.Core.NotificationCenter.Queries.Interfaces;\n\npublic interface IGetNotificationStatusDetailsForUserQuery\n{\n    Task<PagedResult<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter,\n        PageOptions pageOptions);\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusForUserQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.NotificationCenter.Entities;\n\nnamespace Bit.Core.NotificationCenter.Queries.Interfaces;\n\npublic interface IGetNotificationStatusForUserQuery\n{\n    Task<NotificationStatus> GetByNotificationIdAndUserIdAsync(Guid notificationId);\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Repositories/INotificationRepository.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Filter;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.NotificationCenter.Repositories;\n\npublic interface INotificationRepository : IRepository<Notification, Guid>\n{\n    /// <summary>\n    /// Get notifications for a user with the given filters.\n    /// Includes global notifications.\n    /// </summary>\n    /// <param name=\"userId\">User Id</param>\n    /// <param name=\"clientType\">\n    /// Filter for notifications by client type. Always includes notifications with <see cref=\"ClientType.All\"/>.\n    /// </param>\n    /// <param name=\"statusFilter\">\n    /// Filters notifications by status.\n    /// If both <see cref=\"NotificationStatusFilter.Read\"/> and <see cref=\"NotificationStatusFilter.Deleted\"/>\n    /// are not set, includes notifications without a status.\n    /// </param>\n    /// <param name=\"pageOptions\">\n    /// Pagination options.\n    /// </param>\n    /// <returns>\n    /// Paged results ordered by priority (descending, highest to lowest) and creation date (descending).\n    /// Includes all fields from <see cref=\"Notification\"/> and <see cref=\"NotificationStatus\"/>\n    /// </returns>\n    Task<PagedResult<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType,\n        NotificationStatusFilter? statusFilter, PageOptions pageOptions);\n\n    /// <summary>\n    /// Marks notifications as deleted by a taskId.\n    /// </summary>\n    /// <param name=\"taskId\">The unique identifier of the task.</param>\n    /// <returns>\n    /// A collection of UserIds for the notifications that are now marked as deleted.\n    /// </returns>\n    Task<IEnumerable<Guid>> MarkNotificationsAsDeletedByTask(Guid taskId);\n}\n"
  },
  {
    "path": "src/Core/NotificationCenter/Repositories/INotificationStatusRepository.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.NotificationCenter.Entities;\n\nnamespace Bit.Core.NotificationCenter.Repositories;\n\npublic interface INotificationStatusRepository\n{\n    Task<NotificationStatus?> GetByNotificationIdAndUserIdAsync(Guid notificationId, Guid userId);\n    Task<NotificationStatus> CreateAsync(NotificationStatus notificationStatus);\n    Task UpdateAsync(NotificationStatus notificationStatus);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationCollections;\n\npublic class BulkAddCollectionAccessCommand : IBulkAddCollectionAccessCommand\n{\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IEventService _eventService;\n\n    public BulkAddCollectionAccessCommand(\n        ICollectionRepository collectionRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IGroupRepository groupRepository,\n        IEventService eventService)\n    {\n        _collectionRepository = collectionRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _groupRepository = groupRepository;\n        _eventService = eventService;\n    }\n\n    public async Task AddAccessAsync(ICollection<Collection> collections,\n        ICollection<CollectionAccessSelection> users,\n        ICollection<CollectionAccessSelection> groups)\n    {\n        await ValidateRequestAsync(collections, users, groups);\n\n        await _collectionRepository.CreateOrUpdateAccessForManyAsync(\n            collections.First().OrganizationId,\n            collections.Select(c => c.Id),\n            users,\n            groups\n        );\n\n        await _eventService.LogCollectionEventsAsync(collections.Select(c =>\n            (c, EventType.Collection_Updated, (DateTime?)DateTime.UtcNow)));\n    }\n\n    private async Task ValidateRequestAsync(ICollection<Collection> collections, ICollection<CollectionAccessSelection> usersAccess, ICollection<CollectionAccessSelection> groupsAccess)\n    {\n        if (collections == null || collections.Count == 0)\n        {\n            throw new BadRequestException(\"No collections were provided.\");\n        }\n\n        if (collections.Any(c => c.Type == CollectionType.DefaultUserCollection))\n        {\n            throw new BadRequestException(\"You cannot add access to collections with the type as DefaultUserCollection.\");\n        }\n\n        var orgId = collections.First().OrganizationId;\n\n        if (collections.Any(c => c.OrganizationId != orgId))\n        {\n            throw new BadRequestException(\"All collections must belong to the same organization.\");\n        }\n\n        var collectionUserIds = usersAccess?.Select(u => u.Id).Distinct().ToList();\n\n        if (collectionUserIds is { Count: > 0 })\n        {\n            var users = await _organizationUserRepository.GetManyAsync(collectionUserIds);\n\n            if (users.Count != collectionUserIds.Count)\n            {\n                throw new BadRequestException(\"One or more users do not exist.\");\n            }\n\n            if (users.Any(u => u.OrganizationId != orgId))\n            {\n                throw new BadRequestException(\"One or more users do not belong to the same organization as the collection being assigned.\");\n            }\n        }\n\n        var collectionGroupIds = groupsAccess?.Select(g => g.Id).Distinct().ToList();\n\n        if (collectionGroupIds is { Count: > 0 })\n        {\n            var groups = await _groupRepository.GetManyByManyIds(collectionGroupIds);\n\n            if (groups.Count != collectionGroupIds.Count)\n            {\n                throw new BadRequestException(\"One or more groups do not exist.\");\n            }\n\n            if (groups.Any(g => g.OrganizationId != orgId))\n            {\n                throw new BadRequestException(\"One or more groups do not belong to the same organization as the collection being assigned.\");\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationCollections;\n\npublic class CreateCollectionCommand : ICreateCollectionCommand\n{\n    private readonly IEventService _eventService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ICollectionRepository _collectionRepository;\n\n    public CreateCollectionCommand(\n        IEventService eventService,\n        IOrganizationRepository organizationRepository,\n        ICollectionRepository collectionRepository)\n    {\n        _eventService = eventService;\n        _organizationRepository = organizationRepository;\n        _collectionRepository = collectionRepository;\n    }\n\n    public async Task<Collection> CreateAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null,\n        IEnumerable<CollectionAccessSelection> users = null)\n    {\n        if (collection.Type == CollectionType.DefaultUserCollection)\n        {\n            throw new BadRequestException(\"You cannot create a collection with the type as DefaultUserCollection.\");\n        }\n\n        var org = await _organizationRepository.GetByIdAsync(collection.OrganizationId);\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization not found\");\n        }\n\n        var groupsList = groups?.ToList();\n        var usersList = users?.ToList();\n\n        // Cannot use Manage with ReadOnly/HidePasswords permissions\n        var invalidAssociations = groupsList?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));\n        if (invalidAssociations?.Any() ?? false)\n        {\n            throw new BadRequestException(\"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.\");\n        }\n\n        // A collection should always have someone with Can Manage permissions\n        var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false;\n        var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false;\n        if (!groupHasManageAccess && !userHasManageAccess && !org.AllowAdminAccessToAllCollectionItems)\n        {\n            throw new BadRequestException(\n                \"At least one member or group must have can manage permission.\");\n        }\n\n        // Check max collections limit\n        if (org.MaxCollections.HasValue)\n        {\n            var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id);\n            if (org.MaxCollections.Value <= collectionCount)\n            {\n                throw new BadRequestException(\"You have reached the maximum number of collections \" +\n                $\"({org.MaxCollections.Value}) for this organization.\");\n            }\n        }\n\n        await _collectionRepository.CreateAsync(collection, org.UseGroups ? groupsList : null, usersList);\n        await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Created);\n\n        return collection;\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationCollections;\n\npublic class DeleteCollectionCommand : IDeleteCollectionCommand\n{\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IEventService _eventService;\n    private readonly ILogger<DeleteCollectionCommand> _logger;\n\n    public DeleteCollectionCommand(\n        ICollectionRepository collectionRepository,\n        IEventService eventService,\n        ILogger<DeleteCollectionCommand> logger)\n    {\n        _collectionRepository = collectionRepository;\n        _eventService = eventService;\n        _logger = logger;\n    }\n\n    public async Task DeleteAsync(Collection collection)\n    {\n        if (collection.Type == CollectionType.DefaultUserCollection)\n        {\n            throw new BadRequestException(\"You cannot delete a collection with the type as DefaultUserCollection.\");\n        }\n\n        await _collectionRepository.DeleteAsync(collection);\n\n        try\n        {\n            await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Deleted, DateTime.UtcNow);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Failed to log collection deletion event for collection {CollectionId}\", collection.Id);\n        }\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> collectionIds)\n    {\n        var ids = collectionIds as Guid[] ?? collectionIds.ToArray();\n        var collectionsToDelete = await _collectionRepository.GetManyByManyIdsAsync(ids);\n        await this.DeleteManyAsync(collectionsToDelete);\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Collection> collections)\n    {\n        if (collections.Any(c => c.Type == Enums.CollectionType.DefaultUserCollection))\n        {\n            throw new BadRequestException(\"You cannot delete collections with the type as DefaultUserCollection.\");\n        }\n\n        await _collectionRepository.DeleteManyAsync(collections.Select(c => c.Id));\n\n        try\n        {\n            await _eventService.LogCollectionEventsAsync(collections.Select(c => (c, Enums.EventType.Collection_Deleted, (DateTime?)DateTime.UtcNow)));\n        }\n        catch (Exception ex)\n        {\n            var collectionIds = string.Join(\", \", collections.Select(c => c.Id));\n            _logger.LogError(ex, \"Failed to log collection deletion events for collections: {CollectionIds}\", collectionIds);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IBulkAddCollectionAccessCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\n\npublic interface IBulkAddCollectionAccessCommand\n{\n    Task AddAccessAsync(ICollection<Collection> collections,\n        ICollection<CollectionAccessSelection> users, ICollection<CollectionAccessSelection> groups);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/ICreateCollectionCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\n\npublic interface ICreateCollectionCommand\n{\n    /// <summary>\n    /// Creates a new collection.\n    /// </summary>\n    /// <param name=\"collection\">The collection to create.</param>\n    /// <param name=\"groups\">(Optional) The groups that will have access to the collection.</param>\n    /// <param name=\"users\">(Optional) The users that will have access to the collection.</param>\n    /// <returns>The created collection.</returns>\n    Task<Collection> CreateAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null,\n        IEnumerable<CollectionAccessSelection> users = null);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IDeleteCollectionCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\n\npublic interface IDeleteCollectionCommand\n{\n    Task DeleteAsync(Collection collection);\n    Task DeleteManyAsync(IEnumerable<Guid> collectionIds);\n    Task DeleteManyAsync(IEnumerable<Collection> collections);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IUpdateCollectionCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\n\npublic interface IUpdateCollectionCommand\n{\n    /// <summary>\n    /// Updates a collection.\n    /// </summary>\n    /// <param name=\"collection\">The collection to update.</param>\n    /// <param name=\"groups\">(Optional) The groups that will have access to the collection.</param>\n    /// <param name=\"users\">(Optional) The users that will have access to the collection.</param>\n    /// <returns>The updated collection.</returns>\n    Task<Collection> UpdateAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null,\n        IEnumerable<CollectionAccessSelection> users = null);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationCollections;\n\npublic class UpdateCollectionCommand : IUpdateCollectionCommand\n{\n    private readonly IEventService _eventService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ICollectionRepository _collectionRepository;\n\n    public UpdateCollectionCommand(\n        IEventService eventService,\n        IOrganizationRepository organizationRepository,\n        ICollectionRepository collectionRepository)\n    {\n        _eventService = eventService;\n        _organizationRepository = organizationRepository;\n        _collectionRepository = collectionRepository;\n    }\n\n    public async Task<Collection> UpdateAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null,\n        IEnumerable<CollectionAccessSelection> users = null)\n    {\n        if (collection.Type == CollectionType.DefaultUserCollection)\n        {\n            throw new BadRequestException(\"You cannot edit a collection with the type as DefaultUserCollection.\");\n        }\n\n        var org = await _organizationRepository.GetByIdAsync(collection.OrganizationId);\n        if (org == null)\n        {\n            throw new BadRequestException(\"Organization not found\");\n        }\n\n        var groupsList = groups?.ToList();\n        var usersList = users?.ToList();\n\n        // Cannot use Manage with ReadOnly/HidePasswords permissions\n        var invalidAssociations = groupsList?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));\n        if (invalidAssociations?.Any() ?? false)\n        {\n            throw new BadRequestException(\"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.\");\n        }\n\n        // A collection should always have someone with Can Manage permissions\n        var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false;\n        var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false;\n        if (!groupHasManageAccess && !userHasManageAccess && !org.AllowAdminAccessToAllCollectionItems)\n        {\n            throw new BadRequestException(\n                \"At least one member or group must have can manage permission.\");\n        }\n\n        await _collectionRepository.ReplaceAsync(collection, org.UseGroups ? groupsList : null, usersList);\n        await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Updated);\n\n        return collection;\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationAuth;\nusing Bit.Core.AdminConsole.OrganizationAuth.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Import;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;\nusing Bit.Core.Models.Business.Tokenables;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing V1_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;\nusing V2_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\nnamespace Bit.Core.OrganizationFeatures;\n\npublic static class OrganizationServiceCollectionExtensions\n{\n    public static void AddOrganizationServices(this IServiceCollection services, IGlobalSettings globalSettings)\n    {\n        services.AddScoped<IOrganizationService, OrganizationService>();\n        services.AddTokenizers();\n        services.AddOrganizationGroupCommands();\n        services.AddOrganizationConnectionCommands();\n        services.AddOrganizationSponsorshipCommands(globalSettings);\n        services.AddOrganizationApiKeyCommandsQueries();\n        services.AddOrganizationCollectionCommands();\n        services.AddOrganizationGroupCommands();\n        services.AddOrganizationDomainCommandsQueries();\n        services.AddOrganizationSignUpCommands();\n        services.AddOrganizationDeleteCommands();\n        services.AddOrganizationUpdateCommands();\n        services.AddOrganizationEnableCommands();\n        services.AddOrganizationDisableCommands();\n        services.AddOrganizationAuthCommands();\n        services.AddOrganizationUserCommands();\n        services.AddOrganizationUserCommandsQueries();\n        services.AddBaseOrganizationSubscriptionCommandsQueries();\n        services.AddOrganizationFeatureCommands();\n    }\n\n    private static void AddOrganizationFeatureCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IOrganizationAutoConfirmEnabledNotificationCommand, OrganizationAutoConfirmEnabledNotificationCommand>();\n    }\n\n    private static void AddOrganizationSignUpCommands(this IServiceCollection services)\n    {\n        services.AddScoped<ICloudOrganizationSignUpCommand, CloudOrganizationSignUpCommand>();\n        services.AddScoped<IProviderClientOrganizationSignUpCommand, ProviderClientOrganizationSignUpCommand>();\n        services.AddScoped<IResellerClientOrganizationSignUpCommand, ResellerClientOrganizationSignUpCommand>();\n        services.AddScoped<ISelfHostedOrganizationSignUpCommand, SelfHostedOrganizationSignUpCommand>();\n    }\n\n    private static void AddOrganizationDeleteCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IOrganizationDeleteCommand, OrganizationDeleteCommand>();\n        services.AddScoped<IOrganizationInitiateDeleteCommand, OrganizationInitiateDeleteCommand>();\n    }\n\n    private static void AddOrganizationUpdateCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IOrganizationUpdateKeysCommand, OrganizationUpdateKeysCommand>();\n        services.AddScoped<IOrganizationUpdateCommand, OrganizationUpdateCommand>();\n    }\n\n    private static void AddOrganizationEnableCommands(this IServiceCollection services) =>\n        services.AddScoped<IOrganizationEnableCommand, OrganizationEnableCommand>();\n\n    private static void AddOrganizationDisableCommands(this IServiceCollection services) =>\n        services.AddScoped<IOrganizationDisableCommand, OrganizationDisableCommand>();\n\n    private static void AddOrganizationConnectionCommands(this IServiceCollection services)\n    {\n        services.AddScoped<ICreateOrganizationConnectionCommand, CreateOrganizationConnectionCommand>();\n        services.AddScoped<IDeleteOrganizationConnectionCommand, DeleteOrganizationConnectionCommand>();\n        services.AddScoped<IUpdateOrganizationConnectionCommand, UpdateOrganizationConnectionCommand>();\n    }\n\n    private static void AddOrganizationSponsorshipCommands(this IServiceCollection services, IGlobalSettings globalSettings)\n    {\n        services.AddScoped<ICreateSponsorshipCommand, CreateSponsorshipCommand>();\n        services.AddScoped<IRemoveSponsorshipCommand, RemoveSponsorshipCommand>();\n        services.AddScoped<ISendSponsorshipOfferCommand, SendSponsorshipOfferCommand>();\n        services.AddScoped<ISetUpSponsorshipCommand, SetUpSponsorshipCommand>();\n        services.AddScoped<IValidateRedemptionTokenCommand, ValidateRedemptionTokenCommand>();\n        services.AddScoped<IValidateSponsorshipCommand, ValidateSponsorshipCommand>();\n        services.AddScoped<IValidateBillingSyncKeyCommand, ValidateBillingSyncKeyCommand>();\n        services.AddScoped<IOrganizationSponsorshipRenewCommand, OrganizationSponsorshipRenewCommand>();\n        services.AddScoped<ICloudSyncSponsorshipsCommand, CloudSyncSponsorshipsCommand>();\n        services.AddScoped<ISelfHostedSyncSponsorshipsCommand, SelfHostedSyncSponsorshipsCommand>();\n        services.AddScoped<ISelfHostedSyncSponsorshipsCommand, SelfHostedSyncSponsorshipsCommand>();\n        services.AddScoped<ICloudSyncSponsorshipsCommand, CloudSyncSponsorshipsCommand>();\n        services.AddScoped<IValidateBillingSyncKeyCommand, ValidateBillingSyncKeyCommand>();\n        if (globalSettings.SelfHosted)\n        {\n            services.AddScoped<IRevokeSponsorshipCommand, SelfHostedRevokeSponsorshipCommand>();\n        }\n        else\n        {\n            services.AddScoped<IRevokeSponsorshipCommand, CloudRevokeSponsorshipCommand>();\n        }\n    }\n\n    private static void AddOrganizationUserCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IRemoveOrganizationUserCommand, RemoveOrganizationUserCommand>();\n        services.AddScoped<IRevokeNonCompliantOrganizationUserCommand, RevokeNonCompliantOrganizationUserCommand>();\n        services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();\n        services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();\n        services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();\n        services.AddScoped<ISendOrganizationConfirmationCommand, SendOrganizationConfirmationCommand>();\n        services.AddScoped<IAdminRecoverAccountCommand, AdminRecoverAccountCommand>();\n        services.AddScoped<IAutomaticallyConfirmOrganizationUserCommand, AutomaticallyConfirmOrganizationUserCommand>();\n        services.AddScoped<IAutomaticallyConfirmOrganizationUsersValidator, AutomaticallyConfirmOrganizationUsersValidator>();\n\n        services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();\n        services.AddScoped<IDeleteClaimedOrganizationUserAccountValidator, DeleteClaimedOrganizationUserAccountValidator>();\n\n        services.AddScoped<V1_RevokeUsersCommand.IRevokeOrganizationUserCommand, V1_RevokeUsersCommand.RevokeOrganizationUserCommand>();\n\n        services.AddScoped<V2_RevokeUsersCommand.IRevokeOrganizationUserCommand, V2_RevokeUsersCommand.RevokeOrganizationUserCommand>();\n        services.AddScoped<V2_RevokeUsersCommand.IRevokeOrganizationUserValidator, V2_RevokeUsersCommand.RevokeOrganizationUsersValidator>();\n\n        services.AddScoped<ISelfRevokeOrganizationUserCommand, SelfRevokeOrganizationUserCommand>();\n    }\n\n    private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)\n    {\n        services.AddScoped<IGetOrganizationApiKeyQuery, GetOrganizationApiKeyQuery>();\n        services.AddScoped<IRotateOrganizationApiKeyCommand, RotateOrganizationApiKeyCommand>();\n        services.AddScoped<ICreateOrganizationApiKeyCommand, CreateOrganizationApiKeyCommand>();\n    }\n\n    public static void AddOrganizationCollectionCommands(this IServiceCollection services)\n    {\n        services.AddScoped<ICreateCollectionCommand, CreateCollectionCommand>();\n        services.AddScoped<IUpdateCollectionCommand, UpdateCollectionCommand>();\n        services.AddScoped<IDeleteCollectionCommand, DeleteCollectionCommand>();\n        services.AddScoped<IBulkAddCollectionAccessCommand, BulkAddCollectionAccessCommand>();\n    }\n\n    private static void AddOrganizationGroupCommands(this IServiceCollection services)\n    {\n        services.AddScoped<ICreateGroupCommand, CreateGroupCommand>();\n        services.AddScoped<IDeleteGroupCommand, DeleteGroupCommand>();\n        services.AddScoped<IUpdateGroupCommand, UpdateGroupCommand>();\n    }\n\n    private static void AddOrganizationDomainCommandsQueries(this IServiceCollection services)\n    {\n        services.AddScoped<ICreateOrganizationDomainCommand, CreateOrganizationDomainCommand>();\n        services.AddScoped<IVerifyOrganizationDomainCommand, VerifyOrganizationDomainCommand>();\n        services.AddScoped<IGetOrganizationDomainByIdOrganizationIdQuery, GetOrganizationDomainByIdOrganizationIdQuery>();\n        services.AddScoped<IGetOrganizationDomainByOrganizationIdQuery, GetOrganizationDomainByOrganizationIdQuery>();\n        services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();\n        services.AddScoped<IOrganizationHasVerifiedDomainsQuery, OrganizationHasVerifiedDomainsQuery>();\n    }\n\n    private static void AddOrganizationAuthCommands(this IServiceCollection services)\n    {\n        services.AddScoped<IUpdateOrganizationAuthRequestCommand, UpdateOrganizationAuthRequestCommand>();\n    }\n\n    private static void AddOrganizationUserCommandsQueries(this IServiceCollection services)\n    {\n        services.AddScoped<ICountNewSmSeatsRequiredQuery, CountNewSmSeatsRequiredQuery>();\n        services.AddScoped<IAcceptOrgUserCommand, AcceptOrgUserCommand>();\n        services.AddScoped<IPushAutoConfirmNotificationCommand, PushAutoConfirmNotificationCommand>();\n        services.AddScoped<IOrganizationUserUserDetailsQuery, OrganizationUserUserDetailsQuery>();\n        services.AddScoped<IGetOrganizationUsersClaimedStatusQuery, GetOrganizationUsersClaimedStatusQuery>();\n\n        services.AddScoped<IRestoreOrganizationUserCommand, RestoreOrganizationUserCommand>();\n\n        services.AddScoped<IAuthorizationHandler, OrganizationUserUserDetailsAuthorizationHandler>();\n        services.AddScoped<IHasConfirmedOwnersExceptQuery, HasConfirmedOwnersExceptQuery>();\n\n        services.AddScoped<IInviteOrganizationUsersCommand, InviteOrganizationUsersCommand>();\n        services.AddScoped<ISendOrganizationInvitesCommand, SendOrganizationInvitesCommand>();\n        services.AddScoped<IResendOrganizationInviteCommand, ResendOrganizationInviteCommand>();\n        services.AddScoped<IBulkResendOrganizationInvitesCommand, BulkResendOrganizationInvitesCommand>();\n\n        services.AddScoped<IInviteUsersValidator, InviteOrganizationUsersValidator>();\n        services.AddScoped<IInviteUsersOrganizationValidator, InviteUsersOrganizationValidator>();\n        services.AddScoped<IInviteUsersPasswordManagerValidator, InviteUsersPasswordManagerValidator>();\n        services.AddScoped<IInviteUsersEnvironmentValidator, InviteUsersEnvironmentValidator>();\n        services.AddScoped<IInitPendingOrganizationValidator, InitPendingOrganizationValidator>();\n        services.AddScoped<IInitPendingOrganizationCommand, InitPendingOrganizationCommand>();\n        services.AddScoped<IImportOrganizationUsersAndGroupsCommand, ImportOrganizationUsersAndGroupsCommand>();\n    }\n\n    // TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of\n    // TODO: OrganizationService - see PM-1880\n    private static void AddBaseOrganizationSubscriptionCommandsQueries(this IServiceCollection services)\n    {\n        services.AddScoped<IUpdateSecretsManagerSubscriptionCommand, UpdateSecretsManagerSubscriptionCommand>();\n    }\n\n    private static void AddTokenizers(this IServiceCollection services)\n    {\n        services.AddSingleton<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>(\n                OrganizationSponsorshipOfferTokenable.ClearTextPrefix,\n                OrganizationSponsorshipOfferTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>>())\n        );\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;\n\npublic abstract class CancelSponsorshipCommand\n{\n    protected readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;\n    protected readonly IOrganizationRepository _organizationRepository;\n\n    public CancelSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n        IOrganizationRepository organizationRepository)\n    {\n        _organizationSponsorshipRepository = organizationSponsorshipRepository;\n        _organizationRepository = organizationRepository;\n    }\n\n    protected virtual async Task DeleteSponsorshipAsync(OrganizationSponsorship sponsorship = null)\n    {\n        if (sponsorship == null)\n        {\n            return;\n        }\n\n        await _organizationSponsorshipRepository.DeleteAsync(sponsorship);\n    }\n\n    protected async Task MarkToDeleteSponsorshipAsync(OrganizationSponsorship sponsorship)\n    {\n        if (sponsorship == null)\n        {\n            throw new BadRequestException(\"The sponsorship you are trying to cancel does not exist\");\n        }\n\n        sponsorship.ToDelete = true;\n        await _organizationSponsorshipRepository.UpsertAsync(sponsorship);\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudRevokeSponsorshipCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\npublic class CloudRevokeSponsorshipCommand : CancelSponsorshipCommand, IRevokeSponsorshipCommand\n{\n    public CloudRevokeSponsorshipCommand(\n        IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n        IOrganizationRepository organizationRepository) : base(organizationSponsorshipRepository, organizationRepository)\n    {\n    }\n\n    public async Task RevokeSponsorshipAsync(OrganizationSponsorship sponsorship)\n    {\n        if (sponsorship == null)\n        {\n            throw new BadRequestException(\"You are not currently sponsoring an organization.\");\n        }\n\n        if (sponsorship.SponsoredOrganizationId == null)\n        {\n            await base.DeleteSponsorshipAsync(sponsorship);\n        }\n        else\n        {\n            await MarkToDeleteSponsorshipAsync(sponsorship);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\npublic class CloudSyncSponsorshipsCommand : ICloudSyncSponsorshipsCommand\n{\n    private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;\n    private readonly IEventService _eventService;\n\n    public CloudSyncSponsorshipsCommand(\n    IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n    IEventService eventService)\n    {\n        _organizationSponsorshipRepository = organizationSponsorshipRepository;\n        _eventService = eventService;\n    }\n\n    public async Task<(OrganizationSponsorshipSyncData, IEnumerable<OrganizationSponsorship>)> SyncOrganization(Organization sponsoringOrg, IEnumerable<OrganizationSponsorshipData> sponsorshipsData)\n    {\n        if (sponsoringOrg == null)\n        {\n            throw new BadRequestException(\"Failed to sync sponsorship - missing organization.\");\n        }\n\n        var (processedSponsorshipsData, sponsorshipsToEmailOffer) = sponsorshipsData.Any() ?\n            await DoSyncAsync(sponsoringOrg, sponsorshipsData) :\n            (sponsorshipsData, Array.Empty<OrganizationSponsorship>());\n\n        await RecordEvent(sponsoringOrg);\n\n        return (new OrganizationSponsorshipSyncData\n        {\n            SponsorshipsBatch = processedSponsorshipsData\n        }, sponsorshipsToEmailOffer);\n    }\n\n    private async Task<(IEnumerable<OrganizationSponsorshipData> data, IEnumerable<OrganizationSponsorship> toOffer)> DoSyncAsync(Organization sponsoringOrg, IEnumerable<OrganizationSponsorshipData> sponsorshipsData)\n    {\n        var existingSponsorshipsDict = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id))\n            .ToDictionary(i => i.SponsoringOrganizationUserId);\n\n        var sponsorshipsToUpsert = new List<OrganizationSponsorship>();\n        var sponsorshipIdsToDelete = new List<Guid>();\n        var sponsorshipsToReturn = new List<OrganizationSponsorshipData>();\n\n        foreach (var selfHostedSponsorship in sponsorshipsData)\n        {\n            var requiredSponsoringProductType = SponsoredPlans.Get(selfHostedSponsorship.PlanSponsorshipType).SponsoringProductTierType;\n            var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier();\n            if (sponsoringOrgProductTier != requiredSponsoringProductType)\n            {\n                continue; // prevent unsupported sponsorships\n            }\n\n            if (!existingSponsorshipsDict.TryGetValue(selfHostedSponsorship.SponsoringOrganizationUserId, out var cloudSponsorship))\n            {\n                if (selfHostedSponsorship.ToDelete && selfHostedSponsorship.LastSyncDate == null)\n                {\n                    continue; // prevent invalid sponsorships in cloud. These should have been deleted by self hosted\n                }\n                if (OrgDisabledForMoreThanGracePeriod(sponsoringOrg))\n                {\n                    continue; // prevent new sponsorships from disabled orgs\n                }\n                cloudSponsorship = new OrganizationSponsorship\n                {\n                    SponsoringOrganizationId = sponsoringOrg.Id,\n                    SponsoringOrganizationUserId = selfHostedSponsorship.SponsoringOrganizationUserId,\n                    FriendlyName = selfHostedSponsorship.FriendlyName,\n                    OfferedToEmail = selfHostedSponsorship.OfferedToEmail,\n                    PlanSponsorshipType = selfHostedSponsorship.PlanSponsorshipType,\n                    LastSyncDate = DateTime.UtcNow,\n                };\n            }\n            else\n            {\n                cloudSponsorship.LastSyncDate = DateTime.UtcNow;\n            }\n\n            if (selfHostedSponsorship.ToDelete)\n            {\n                if (cloudSponsorship.SponsoredOrganizationId == null)\n                {\n                    sponsorshipIdsToDelete.Add(cloudSponsorship.Id);\n                    selfHostedSponsorship.CloudSponsorshipRemoved = true;\n                }\n                else\n                {\n                    cloudSponsorship.ToDelete = true;\n                }\n            }\n            sponsorshipsToUpsert.Add(cloudSponsorship);\n\n            selfHostedSponsorship.ValidUntil = cloudSponsorship.ValidUntil;\n            selfHostedSponsorship.LastSyncDate = DateTime.UtcNow;\n            sponsorshipsToReturn.Add(selfHostedSponsorship);\n        }\n        var sponsorshipsToEmailOffer = sponsorshipsToUpsert.Where(s => s.Id == default).ToArray();\n        if (sponsorshipsToUpsert.Any())\n        {\n            await _organizationSponsorshipRepository.UpsertManyAsync(sponsorshipsToUpsert);\n        }\n        if (sponsorshipIdsToDelete.Any())\n        {\n            await _organizationSponsorshipRepository.DeleteManyAsync(sponsorshipIdsToDelete);\n        }\n\n        return (sponsorshipsToReturn, sponsorshipsToEmailOffer);\n    }\n\n    /// <summary>\n    /// True if Organization is disabled and the expiration date is more than three months ago\n    /// </summary>\n    /// <param name=\"organization\"></param>\n    private bool OrgDisabledForMoreThanGracePeriod(Organization organization) =>\n        !organization.Enabled &&\n        (\n            !organization.ExpirationDate.HasValue ||\n            DateTime.UtcNow.Subtract(organization.ExpirationDate.Value).TotalDays > 93\n        );\n\n    private async Task RecordEvent(Organization organization)\n    {\n        await _eventService.LogOrganizationEventAsync(organization, EventType.Organization_SponsorshipsSynced);\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/OrganizationSponsorshipRenewCommand.cs",
    "content": "﻿using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\npublic class OrganizationSponsorshipRenewCommand : IOrganizationSponsorshipRenewCommand\n{\n    private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;\n\n    public OrganizationSponsorshipRenewCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository)\n    {\n        _organizationSponsorshipRepository = organizationSponsorshipRepository;\n    }\n\n    public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime expireDate)\n    {\n        var sponsorship = await _organizationSponsorshipRepository.GetBySponsoredOrganizationIdAsync(organizationId);\n\n        if (sponsorship == null)\n        {\n            return;\n        }\n\n        sponsorship.ValidUntil = expireDate;\n        await _organizationSponsorshipRepository.UpsertAsync(sponsorship);\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/RemoveSponsorshipCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\npublic class RemoveSponsorshipCommand : CancelSponsorshipCommand, IRemoveSponsorshipCommand\n{\n    public RemoveSponsorshipCommand(\n        IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n        IOrganizationRepository organizationRepository) : base(organizationSponsorshipRepository, organizationRepository)\n    {\n    }\n\n    public async Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship)\n    {\n        if (sponsorship == null || sponsorship.SponsoredOrganizationId == null)\n        {\n            throw new BadRequestException(\"The requested organization is not currently being sponsored.\");\n        }\n\n        await MarkToDeleteSponsorshipAsync(sponsorship);\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business.Tokenables;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\npublic class SendSponsorshipOfferCommand : ISendSponsorshipOfferCommand\n{\n    private readonly IUserRepository _userRepository;\n    private readonly IMailService _mailService;\n    private readonly IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable> _tokenFactory;\n\n    public SendSponsorshipOfferCommand(IUserRepository userRepository,\n        IMailService mailService,\n        IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable> tokenFactory)\n    {\n        _userRepository = userRepository;\n        _mailService = mailService;\n        _tokenFactory = tokenFactory;\n    }\n\n    public async Task BulkSendSponsorshipOfferAsync(string sponsoringOrgName, IEnumerable<OrganizationSponsorship> sponsorships)\n    {\n        var invites = new List<(string, bool, string)>();\n        foreach (var sponsorship in sponsorships)\n        {\n            var user = await _userRepository.GetByEmailAsync(sponsorship.OfferedToEmail);\n            var isExistingAccount = user != null;\n            invites.Add((sponsorship.OfferedToEmail, user != null, _tokenFactory.Protect(new OrganizationSponsorshipOfferTokenable(sponsorship))));\n        }\n\n        await _mailService.BulkSendFamiliesForEnterpriseOfferEmailAsync(sponsoringOrgName, invites);\n    }\n\n    public async Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, string sponsoringOrgName)\n    {\n        var user = await _userRepository.GetByEmailAsync(sponsorship.OfferedToEmail);\n        var isExistingAccount = user != null;\n\n        await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsoringOrgName, sponsorship.OfferedToEmail,\n            isExistingAccount, _tokenFactory.Protect(new OrganizationSponsorshipOfferTokenable(sponsorship)));\n    }\n\n    public async Task SendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,\n        OrganizationSponsorship sponsorship)\n    {\n        if (sponsoringOrg == null)\n        {\n            throw new BadRequestException(\"Cannot find the requested sponsoring organization.\");\n        }\n\n        if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed)\n        {\n            throw new BadRequestException(\"Only confirmed users can sponsor other organizations.\");\n        }\n\n        if (sponsorship == null || sponsorship.OfferedToEmail == null)\n        {\n            throw new BadRequestException(\"Cannot find an outstanding sponsorship offer for this organization.\");\n        }\n\n        await SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.DisplayName());\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\npublic class SetUpSponsorshipCommand : ISetUpSponsorshipCommand\n{\n    private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly IFeatureService _featureService;\n    private readonly IPricingClient _pricingClient;\n    private readonly IUpdateOrganizationSubscriptionCommand _updateOrganizationSubscriptionCommand;\n\n    public SetUpSponsorshipCommand(\n        IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n        IOrganizationRepository organizationRepository,\n        IStripePaymentService paymentService,\n        IFeatureService featureService,\n        IPricingClient pricingClient,\n        IUpdateOrganizationSubscriptionCommand updateOrganizationSubscriptionCommand)\n    {\n        _organizationSponsorshipRepository = organizationSponsorshipRepository;\n        _organizationRepository = organizationRepository;\n        _paymentService = paymentService;\n        _featureService = featureService;\n        _pricingClient = pricingClient;\n        _updateOrganizationSubscriptionCommand = updateOrganizationSubscriptionCommand;\n    }\n\n    public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship,\n        Organization sponsoredOrganization)\n    {\n        if (sponsorship == null)\n        {\n            throw new BadRequestException(\"No unredeemed sponsorship offer exists for you.\");\n        }\n\n        var existingOrgSponsorship = await _organizationSponsorshipRepository\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrganization.Id);\n        if (existingOrgSponsorship != null)\n        {\n            throw new BadRequestException(\"Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first.\");\n        }\n\n        if (sponsorship.PlanSponsorshipType == null)\n        {\n            throw new BadRequestException(\"Cannot set up sponsorship without a known sponsorship type.\");\n        }\n\n        // Do not allow self-hosted sponsorships that haven't been synced for > 0.5 year\n        if (sponsorship.LastSyncDate != null && DateTime.UtcNow.Subtract(sponsorship.LastSyncDate.Value).TotalDays > 182.5)\n        {\n            await _organizationSponsorshipRepository.DeleteAsync(sponsorship);\n            throw new BadRequestException(\"This sponsorship offer is more than 6 months old and has expired.\");\n        }\n\n        // Check org to sponsor's product type\n        var sponsoredPlan = SponsoredPlans.Get(sponsorship.PlanSponsorshipType.Value);\n        var requiredSponsoredProductType = sponsoredPlan.SponsoredProductTierType;\n        var sponsoredOrganizationProductTier = sponsoredOrganization.PlanType.GetProductTier();\n\n        if (sponsoredOrganizationProductTier != requiredSponsoredProductType)\n        {\n            throw new BadRequestException(\"Can only redeem sponsorship offer on families organizations.\");\n        }\n\n        if (_featureService.IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand))\n        {\n            var existingPlan = await _pricingClient.GetPlanOrThrow(sponsoredOrganization.PlanType);\n            var changeSet = OrganizationSubscriptionChangeSet.Builder()\n                .RemoveItem(existingPlan.PasswordManager.StripePlanId)\n                .AddItem(sponsoredPlan.StripePlanId, 1)\n                .Build();\n\n            var result = await _updateOrganizationSubscriptionCommand.Run(sponsoredOrganization, changeSet);\n            var updatedSubscription = result.GetValueOrThrow();\n            var currentPeriodEnd = updatedSubscription.GetCurrentPeriodEnd();\n            sponsoredOrganization.ExpirationDate = currentPeriodEnd;\n            sponsorship.ValidUntil = currentPeriodEnd;\n        }\n        else\n        {\n            await _paymentService.SponsorOrganizationAsync(sponsoredOrganization, sponsorship);\n        }\n\n        await _organizationRepository.UpsertAsync(sponsoredOrganization);\n        sponsorship.SponsoredOrganizationId = sponsoredOrganization.Id;\n        sponsorship.OfferedToEmail = null;\n        await _organizationSponsorshipRepository.UpsertAsync(sponsorship);\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Business.Tokenables;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\npublic class ValidateRedemptionTokenCommand : IValidateRedemptionTokenCommand\n{\n    private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;\n    private readonly IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable> _dataProtectorTokenFactory;\n\n    public ValidateRedemptionTokenCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n        IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable> dataProtectorTokenFactory)\n    {\n        _organizationSponsorshipRepository = organizationSponsorshipRepository;\n        _dataProtectorTokenFactory = dataProtectorTokenFactory;\n    }\n\n    public async Task<(bool valid, OrganizationSponsorship sponsorship)> ValidateRedemptionTokenAsync(string encryptedToken, string sponsoredUserEmail)\n    {\n\n        if (!_dataProtectorTokenFactory.TryUnprotect(encryptedToken, out var tokenable))\n        {\n            return (false, null);\n        }\n\n        var sponsorship = await _organizationSponsorshipRepository.GetByIdAsync(tokenable.Id);\n        if (!tokenable.IsValid(sponsorship, sponsoredUserEmail))\n        {\n            return (false, sponsorship);\n        }\n        return (true, sponsorship);\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\npublic class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSponsorshipCommand\n{\n    private readonly IStripePaymentService _paymentService;\n    private readonly IMailService _mailService;\n    private readonly ILogger<ValidateSponsorshipCommand> _logger;\n\n    public ValidateSponsorshipCommand(\n        IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n        IOrganizationRepository organizationRepository,\n        IStripePaymentService paymentService,\n        IMailService mailService,\n        ILogger<ValidateSponsorshipCommand> logger) : base(organizationSponsorshipRepository, organizationRepository)\n    {\n        _paymentService = paymentService;\n        _mailService = mailService;\n        _logger = logger;\n    }\n\n    public async Task<bool> ValidateSponsorshipAsync(Guid sponsoredOrganizationId)\n    {\n        var sponsoredOrganization = await _organizationRepository.GetByIdAsync(sponsoredOrganizationId);\n\n        if (sponsoredOrganization == null)\n        {\n            _logger.LogWarning(\"Sponsored Organization {OrganizationId} does not exist\", sponsoredOrganizationId);\n            return false;\n        }\n\n        var existingSponsorship = await _organizationSponsorshipRepository\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrganizationId);\n\n        if (existingSponsorship == null)\n        {\n            _logger.LogWarning(\"Existing sponsorship for sponsored Organization {SponsoredOrganizationId} does not exist\", sponsoredOrganizationId);\n\n            await CancelSponsorshipAsync(sponsoredOrganization, null);\n            return false;\n        }\n\n        if (existingSponsorship.SponsoringOrganizationId == null)\n        {\n            _logger.LogWarning(\"Sponsoring OrganizationId is null for sponsored Organization {SponsoredOrganizationId}\", sponsoredOrganizationId);\n\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n            return false;\n        }\n\n        if (existingSponsorship.SponsoringOrganizationUserId == default)\n        {\n            _logger.LogWarning(\"Sponsoring OrganizationUserId is null for sponsored Organization {SponsoredOrganizationId}\", sponsoredOrganizationId);\n\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n            return false;\n        }\n\n        if (existingSponsorship.PlanSponsorshipType == null)\n        {\n            _logger.LogWarning(\"PlanSponsorshipType is null for sponsored Organization {SponsoredOrganizationId}\", sponsoredOrganizationId);\n\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n            return false;\n        }\n\n        if (existingSponsorship.SponsoringOrganizationId == null)\n        {\n            _logger.LogWarning(\"Sponsoring OrganizationId is null for sponsored Organization {SponsoredOrganizationId}\", sponsoredOrganizationId);\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n            return false;\n        }\n\n        if (existingSponsorship.SponsoringOrganizationUserId == default)\n        {\n            _logger.LogWarning(\"Sponsoring OrganizationUserId is null for sponsored Organization {SponsoredOrganizationId}\", sponsoredOrganizationId);\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n            return false;\n        }\n\n        if (existingSponsorship.PlanSponsorshipType == null)\n        {\n            _logger.LogWarning(\"PlanSponsorshipType is null for sponsored Organization {SponsoredOrganizationId}\", sponsoredOrganizationId);\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n            return false;\n        }\n\n        var sponsoredPlan = SponsoredPlans.Get(existingSponsorship.PlanSponsorshipType.Value);\n\n        var sponsoringOrganization = await _organizationRepository\n            .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value);\n\n        if (sponsoringOrganization == null)\n        {\n            _logger.LogWarning(\"Sponsoring Organization {SponsoringOrganizationId} does not exist\", existingSponsorship.SponsoringOrganizationId);\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n            return false;\n        }\n\n        if (OrgDisabledForMoreThanGracePeriod(sponsoringOrganization))\n        {\n            _logger.LogWarning(\"Sponsoring Organization {SponsoringOrganizationId} is disabled for more than 3 months.\", sponsoringOrganization.Id);\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n\n            return false;\n        }\n\n        if (existingSponsorship.IsAdminInitiated && !sponsoringOrganization.UseAdminSponsoredFamilies)\n        {\n            _logger.LogWarning(\"Admin initiated sponsorship for sponsored Organization {SponsoredOrganizationId} is not allowed because sponsoring organization does not have UseAdminSponsoredFamilies enabled\", sponsoredOrganizationId);\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n            return false;\n        }\n\n        var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier();\n\n        if (sponsoredPlan.SponsoringProductTierType != sponsoringOrgProductTier)\n        {\n            _logger.LogWarning(\"Sponsoring Organization {SponsoringOrganizationId} is not on the required product type.\", sponsoringOrganization.Id);\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n\n            return false;\n        }\n\n        if (existingSponsorship.ToDelete)\n        {\n            _logger.LogWarning(\"Sponsorship for sponsored Organization {SponsoredOrganizationId} is marked for deletion\", sponsoredOrganizationId);\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n\n            return false;\n        }\n\n        if (SponsorshipIsSelfHostedOutOfSync(existingSponsorship))\n        {\n            _logger.LogWarning(\"Sponsorship for sponsored Organization {SponsoredOrganizationId} is out of sync with self-hosted instance.\", sponsoredOrganizationId);\n            await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);\n\n            return false;\n        }\n\n        _logger.LogInformation(\"Sponsorship for sponsored Organization {SponsoredOrganizationId} is valid\", sponsoredOrganizationId);\n        return true;\n    }\n\n    private async Task CancelSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null)\n    {\n        await Task.CompletedTask; // this is intentional\n\n        // if (sponsoredOrganization != null)\n        // {\n        //     await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship);\n        //     await _organizationRepository.UpsertAsync(sponsoredOrganization);\n        //\n        //     try\n        //     {\n        //         if (sponsorship != null)\n        //         {\n        //             await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(\n        //                 sponsoredOrganization.BillingEmailAddress(),\n        //                 sponsorship.ValidUntil ?? DateTime.UtcNow.AddDays(15));\n        //         }\n        //     }\n        //     catch (Exception e)\n        //     {\n        //         _logger.LogError(e, \"Error sending Family sponsorship removed email.\");\n        //     }\n        // }\n        // await base.DeleteSponsorshipAsync(sponsorship);\n    }\n\n    /// <summary>\n    /// True if Sponsorship is from a self-hosted instance that has failed to sync for more than 6 months\n    /// </summary>\n    /// <param name=\"sponsorship\"></param>\n    private bool SponsorshipIsSelfHostedOutOfSync(OrganizationSponsorship sponsorship) =>\n        sponsorship.LastSyncDate.HasValue &&\n        DateTime.UtcNow.Subtract(sponsorship.LastSyncDate.Value).TotalDays > 182.5;\n\n    /// <summary>\n    /// True if Organization is disabled and the expiration date is more than three months ago\n    /// </summary>\n    /// <param name=\"organization\"></param>\n    private bool OrgDisabledForMoreThanGracePeriod(Organization organization) =>\n        !organization.Enabled &&\n        (\n            !organization.ExpirationDate.HasValue ||\n            DateTime.UtcNow.Subtract(organization.ExpirationDate.Value).TotalDays > 93\n        );\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;\n\npublic class CreateSponsorshipCommand(\n    ICurrentContext currentContext,\n    IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n    IUserService userService,\n    IOrganizationService organizationService,\n    IOrganizationRepository organizationRepository) : ICreateSponsorshipCommand\n{\n    public async Task<OrganizationSponsorship> CreateSponsorshipAsync(\n        Organization sponsoringOrganization,\n        OrganizationUser sponsoringMember,\n        PlanSponsorshipType sponsorshipType,\n        string sponsoredEmail,\n        string friendlyName,\n        bool isAdminInitiated,\n        string notes)\n    {\n        var sponsoringUser = await userService.GetUserByIdAsync(sponsoringMember.UserId!.Value);\n\n        if (sponsoringUser == null || string.Equals(sponsoringUser.Email, sponsoredEmail, StringComparison.InvariantCultureIgnoreCase))\n        {\n            throw new BadRequestException(\"Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.\");\n        }\n\n        var requiredSponsoringProductType = SponsoredPlans.Get(sponsorshipType).SponsoringProductTierType;\n        var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier();\n\n        if (sponsoringOrgProductTier != requiredSponsoringProductType)\n        {\n            throw new BadRequestException(\"Specified Organization cannot sponsor other organizations.\");\n        }\n\n        if (sponsoringMember.Status != OrganizationUserStatusType.Confirmed)\n        {\n            throw new BadRequestException(\"Only confirmed users can sponsor other organizations.\");\n        }\n\n        var sponsorships =\n            await organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id);\n        var existingSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName == friendlyName);\n        if (existingSponsorship != null)\n        {\n            return existingSponsorship;\n        }\n\n        if (isAdminInitiated)\n        {\n            ValidateAdminInitiatedSponsorship(sponsoringOrganization);\n        }\n\n        var sponsorship = new OrganizationSponsorship\n        {\n            SponsoringOrganizationId = sponsoringOrganization.Id,\n            SponsoringOrganizationUserId = sponsoringMember.Id,\n            FriendlyName = friendlyName,\n            OfferedToEmail = sponsoredEmail,\n            PlanSponsorshipType = sponsorshipType,\n            IsAdminInitiated = isAdminInitiated,\n            Notes = notes\n        };\n\n        if (!isAdminInitiated)\n        {\n            var existingOrgSponsorship = await organizationSponsorshipRepository\n                .GetBySponsoringOrganizationUserIdAsync(sponsoringMember.Id);\n            if (existingOrgSponsorship?.SponsoredOrganizationId != null)\n            {\n                throw new BadRequestException(\"Can only sponsor one organization per Organization User.\");\n            }\n\n            if (existingOrgSponsorship != null)\n            {\n                sponsorship.Id = existingOrgSponsorship.Id;\n            }\n        }\n\n        if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)\n        {\n            var seatCounts = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id);\n            var availableSeats = sponsoringOrganization.Seats.Value - seatCounts.Total;\n\n            if (availableSeats <= 0)\n            {\n                var newSeatsRequired = 1;\n                var (canScale, failureReason) = await organizationService.CanScaleAsync(sponsoringOrganization, newSeatsRequired);\n                if (!canScale)\n                {\n                    throw new BadRequestException(failureReason);\n                }\n\n                await organizationService.AutoAddSeatsAsync(sponsoringOrganization, newSeatsRequired);\n            }\n        }\n\n        try\n        {\n            if (isAdminInitiated)\n            {\n                await organizationSponsorshipRepository.CreateAsync(sponsorship);\n            }\n            else\n            {\n                await organizationSponsorshipRepository.UpsertAsync(sponsorship);\n            }\n\n            return sponsorship;\n        }\n        catch\n        {\n            if (sponsorship.Id != Guid.Empty)\n            {\n                await organizationSponsorshipRepository.DeleteAsync(sponsorship);\n            }\n            throw;\n        }\n    }\n\n    private void ValidateAdminInitiatedSponsorship(Organization sponsoringOrganization)\n    {\n        var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrganization.Id);\n        OrganizationUserType[] allowedUserTypes =\n        [\n            OrganizationUserType.Admin,\n            OrganizationUserType.Owner\n        ];\n\n        if (!organization.Permissions.ManageUsers && allowedUserTypes.All(x => x != organization.Type))\n        {\n            throw new UnauthorizedAccessException(\"You do not have permissions to send sponsorships on behalf of the organization\");\n        }\n\n        if (!sponsoringOrganization.UseAdminSponsoredFamilies)\n        {\n            throw new BadRequestException(\"Sponsoring organization cannot send admin-initiated sponsorship invitations\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ICreateSponsorshipCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\n\npublic interface ICreateSponsorshipCommand\n{\n    Task<OrganizationSponsorship> CreateSponsorshipAsync(\n        Organization sponsoringOrg,\n        OrganizationUser sponsoringOrgUser,\n        PlanSponsorshipType sponsorshipType,\n        string sponsoredEmail,\n        string friendlyName,\n        bool isAdminInitiated,\n        string notes);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IOrganizationSponsorshipRenewCommand.cs",
    "content": "﻿namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\n\npublic interface IOrganizationSponsorshipRenewCommand\n{\n    Task UpdateExpirationDateAsync(Guid organizationId, DateTime expireDate);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IRemoveSponsorshipCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\n\npublic interface IRemoveSponsorshipCommand\n{\n    Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IRevokeSponsorshipCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\n\npublic interface IRevokeSponsorshipCommand\n{\n    Task RevokeSponsorshipAsync(OrganizationSponsorship sponsorship);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISendSponsorshipOfferCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\n\npublic interface ISendSponsorshipOfferCommand\n{\n    Task BulkSendSponsorshipOfferAsync(string sponsoringOrgName, IEnumerable<OrganizationSponsorship> invites);\n    Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, string sponsoringOrgName);\n    Task SendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,\n        OrganizationSponsorship sponsorship);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISetUpSponsorshipCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\n\npublic interface ISetUpSponsorshipCommand\n{\n    Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship,\n        Organization sponsoredOrganization);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISyncOrganizationSponsorshipsCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\n\npublic interface ISelfHostedSyncSponsorshipsCommand\n{\n    Task SyncOrganization(Guid organizationId, Guid cloudOrganizationId, OrganizationConnection billingSyncConnection);\n}\n\npublic interface ICloudSyncSponsorshipsCommand\n{\n    Task<(OrganizationSponsorshipSyncData, IEnumerable<OrganizationSponsorship>)> SyncOrganization(Organization sponsoringOrg, IEnumerable<OrganizationSponsorshipData> sponsorshipsData);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateRedemptionTokenCommand.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\n\npublic interface IValidateRedemptionTokenCommand\n{\n    Task<(bool valid, OrganizationSponsorship sponsorship)> ValidateRedemptionTokenAsync(string encryptedToken, string sponsoredUserEmail);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateSponsorshipCommand.cs",
    "content": "﻿namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\n\npublic interface IValidateSponsorshipCommand\n{\n    Task<bool> ValidateSponsorshipAsync(Guid sponsoredOrganizationId);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedRevokeSponsorshipCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;\n\npublic class SelfHostedRevokeSponsorshipCommand : CancelSponsorshipCommand, IRevokeSponsorshipCommand\n{\n    public SelfHostedRevokeSponsorshipCommand(\n        IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n        IOrganizationRepository organizationRepository) : base(organizationSponsorshipRepository, organizationRepository)\n    {\n    }\n\n    public async Task RevokeSponsorshipAsync(OrganizationSponsorship sponsorship)\n    {\n        if (sponsorship == null)\n        {\n            throw new BadRequestException(\"You are not currently sponsoring an organization.\");\n        }\n\n        if (sponsorship.LastSyncDate == null)\n        {\n            await base.DeleteSponsorshipAsync(sponsorship);\n        }\n        else\n        {\n            await MarkToDeleteSponsorshipAsync(sponsorship);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api.Request.OrganizationSponsorships;\nusing Bit.Core.Models.Api.Response.OrganizationSponsorships;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;\n\npublic class SelfHostedSyncSponsorshipsCommand : BaseIdentityClientService, ISelfHostedSyncSponsorshipsCommand\n{\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationConnectionRepository _organizationConnectionRepository;\n\n    public SelfHostedSyncSponsorshipsCommand(\n    IHttpClientFactory httpFactory,\n    IOrganizationSponsorshipRepository organizationSponsorshipRepository,\n    IOrganizationUserRepository organizationUserRepository,\n    IOrganizationConnectionRepository organizationConnectionRepository,\n    IGlobalSettings globalSettings,\n    ILogger<SelfHostedSyncSponsorshipsCommand> logger)\n    : base(\n        httpFactory,\n        globalSettings.Installation.ApiUri,\n        globalSettings.Installation.IdentityUri,\n        ApiScopes.ApiInstallation,\n        $\"installation.{globalSettings.Installation.Id}\",\n        globalSettings.Installation.Key,\n        logger)\n    {\n        _globalSettings = globalSettings;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationSponsorshipRepository = organizationSponsorshipRepository;\n        _organizationConnectionRepository = organizationConnectionRepository;\n    }\n\n    public async Task SyncOrganization(Guid organizationId, Guid cloudOrganizationId, OrganizationConnection billingSyncConnection)\n    {\n        if (!_globalSettings.EnableCloudCommunication)\n        {\n            throw new BadRequestException(\"Failed to sync instance with cloud - Cloud communication is disabled in global settings\");\n        }\n\n        if (!billingSyncConnection.Validate<BillingSyncConfig>(out var exception))\n        {\n            throw new BadRequestException(exception);\n        }\n\n        var billingSyncConfig = billingSyncConnection.GetConfig<BillingSyncConfig>();\n        var organizationSponsorshipsDict = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(organizationId))\n            .ToDictionary(i => i.SponsoringOrganizationUserId);\n        if (!organizationSponsorshipsDict.Any())\n        {\n            _logger.LogInformation(\"No existing sponsorships to sync for organization {organizationId}\", organizationId);\n            return;\n        }\n        var syncedSponsorships = new List<OrganizationSponsorshipData>();\n\n        foreach (var orgSponsorshipsBatch in organizationSponsorshipsDict.Values.Chunk(1000))\n        {\n            var response = await SendAsync<OrganizationSponsorshipSyncRequestModel, OrganizationSponsorshipSyncResponseModel>(\n                HttpMethod.Post, \"organization/sponsorship/sync\", new OrganizationSponsorshipSyncRequestModel\n                {\n                    BillingSyncKey = billingSyncConfig.BillingSyncKey,\n                    SponsoringOrganizationCloudId = cloudOrganizationId,\n                    SponsorshipsBatch = orgSponsorshipsBatch.Select(s => new OrganizationSponsorshipRequestModel(s))\n                }, true);\n\n            if (response == null)\n            {\n                _logger.LogDebug(\"Organization sync failed for '{OrgId}'\", organizationId);\n                throw new BadRequestException(\"Organization sync failed\");\n            }\n\n            syncedSponsorships.AddRange(response.ToOrganizationSponsorshipSync().SponsorshipsBatch);\n        }\n\n        var sponsorshipsToDelete = syncedSponsorships.Where(s => s.CloudSponsorshipRemoved).Select(i => organizationSponsorshipsDict[i.SponsoringOrganizationUserId].Id);\n        var sponsorshipsToUpsert = syncedSponsorships.Where(s => !s.CloudSponsorshipRemoved).Select(i =>\n        {\n            var existingSponsorship = organizationSponsorshipsDict[i.SponsoringOrganizationUserId];\n            if (existingSponsorship != null)\n            {\n                existingSponsorship.LastSyncDate = i.LastSyncDate;\n                existingSponsorship.ValidUntil = i.ValidUntil;\n                existingSponsorship.ToDelete = i.ToDelete;\n            }\n            else\n            {\n                // shouldn't occur, added in case self hosted loses a sponsorship\n                existingSponsorship = new OrganizationSponsorship\n                {\n                    SponsoringOrganizationId = organizationId,\n                    SponsoringOrganizationUserId = i.SponsoringOrganizationUserId,\n                    FriendlyName = i.FriendlyName,\n                    OfferedToEmail = i.OfferedToEmail,\n                    PlanSponsorshipType = i.PlanSponsorshipType,\n                    LastSyncDate = i.LastSyncDate,\n                    ValidUntil = i.ValidUntil,\n                    ToDelete = i.ToDelete\n                };\n            }\n            return existingSponsorship;\n        });\n\n        if (sponsorshipsToDelete.Any())\n        {\n            await _organizationSponsorshipRepository.DeleteManyAsync(sponsorshipsToDelete);\n        }\n        if (sponsorshipsToUpsert.Any())\n        {\n            await _organizationSponsorshipRepository.UpsertManyAsync(sponsorshipsToUpsert);\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSubscriptions/AddSecretsManagerSubscriptionCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;\n\npublic class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscriptionCommand\n{\n    private readonly IStripePaymentService _paymentService;\n    private readonly IOrganizationService _organizationService;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IPricingClient _pricingClient;\n\n    public AddSecretsManagerSubscriptionCommand(\n        IStripePaymentService paymentService,\n        IOrganizationService organizationService,\n        IProviderRepository providerRepository,\n        IPricingClient pricingClient)\n    {\n        _paymentService = paymentService;\n        _organizationService = organizationService;\n        _providerRepository = providerRepository;\n        _pricingClient = pricingClient;\n    }\n    public async Task SignUpAsync(Organization organization, int additionalSmSeats,\n        int additionalServiceAccounts)\n    {\n        await ValidateOrganization(organization);\n\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n        var signup = SetOrganizationUpgrade(organization, additionalSmSeats, additionalServiceAccounts);\n        _organizationService.ValidateSecretsManagerPlan(plan, signup);\n\n        if (plan.ProductTier != ProductTierType.Free)\n        {\n            await _paymentService.AddSecretsManagerToSubscription(organization, plan, additionalSmSeats, additionalServiceAccounts);\n        }\n\n        organization.SmSeats = plan.SecretsManager.BaseSeats + additionalSmSeats;\n        organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount + additionalServiceAccounts;\n        organization.UseSecretsManager = true;\n\n        await _organizationService.ReplaceAndUpdateCacheAsync(organization);\n\n        // TODO: call ReferenceEventService - see AC-1481\n    }\n\n    private static OrganizationUpgrade SetOrganizationUpgrade(Organization organization, int additionalSeats,\n        int additionalServiceAccounts)\n    {\n        var signup = new OrganizationUpgrade\n        {\n            UseSecretsManager = true,\n            AdditionalSmSeats = additionalSeats,\n            AdditionalServiceAccounts = additionalServiceAccounts,\n            AdditionalSeats = organization.Seats.GetValueOrDefault()\n        };\n        return signup;\n    }\n\n    private async Task ValidateOrganization(Organization organization)\n    {\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (organization.UseSecretsManager)\n        {\n            throw new BadRequestException(\"Organization already uses Secrets Manager.\");\n        }\n\n        var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        if (!plan.SupportsSecretsManager)\n        {\n            throw new BadRequestException(\"Organization's plan does not support Secrets Manager.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && plan.ProductTier != ProductTierType.Free)\n        {\n            throw new BadRequestException(\"No payment method found.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId) && plan.ProductTier != ProductTierType.Free)\n        {\n            throw new BadRequestException(\"No subscription found.\");\n        }\n\n        var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);\n        if (provider is { Type: ProviderType.Msp })\n        {\n            throw new BadRequestException(\n                \"Organizations with a Managed Service Provider do not support Secrets Manager.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IAddSecretsManagerSubscriptionCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\n\n/// <summary>\n/// This is only for adding SM to an existing organization\n/// </summary>\npublic interface IAddSecretsManagerSubscriptionCommand\n{\n    Task SignUpAsync(Organization organization, int additionalSmSeats, int additionalServiceAccounts);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpdateSecretsManagerSubscriptionCommand.cs",
    "content": "﻿using Bit.Core.Models.Business;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\n\npublic interface IUpdateSecretsManagerSubscriptionCommand\n{\n    Task UpdateSubscriptionAsync(SecretsManagerSubscriptionUpdate update);\n    Task ValidateUpdateAsync(SecretsManagerSubscriptionUpdate update);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSubscriptions/Interface/IUpgradeOrganizationPlanCommand.cs",
    "content": "﻿namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\n\npublic interface IUpgradeOrganizationPlanCommand\n{\n    Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, Models.Business.OrganizationUpgrade upgrade, Guid? userId = null);\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSubscriptions/OrganizationSubscriptionServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;\n\npublic static class OrganizationSubscriptionServiceCollectionExtensions\n{\n    public static void AddOrganizationSubscriptionServices(this IServiceCollection services)\n    {\n        services\n            .AddScoped<IUpgradeOrganizationPlanCommand, UpgradeOrganizationPlanCommand>()\n            .AddScoped<IAddSecretsManagerSubscriptionCommand, AddSecretsManagerSubscriptionCommand>()\n            .AddScoped<IGetOrganizationSubscriptionsToUpdateQuery, GetOrganizationSubscriptionsToUpdateQuery>()\n            .AddScoped<IBulkUpdateOrganizationSubscriptionsCommand, BulkUpdateOrganizationSubscriptionsCommand>();\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;\n\npublic class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubscriptionCommand\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly IMailService _mailService;\n    private readonly ILogger<UpdateSecretsManagerSubscriptionCommand> _logger;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IEventService _eventService;\n    private readonly IFeatureService _featureService;\n    private readonly IUpdateOrganizationSubscriptionCommand _updateOrganizationSubscriptionCommand;\n\n    public UpdateSecretsManagerSubscriptionCommand(\n        IOrganizationUserRepository organizationUserRepository,\n        IStripePaymentService paymentService,\n        IMailService mailService,\n        ILogger<UpdateSecretsManagerSubscriptionCommand> logger,\n        IServiceAccountRepository serviceAccountRepository,\n        IGlobalSettings globalSettings,\n        IOrganizationRepository organizationRepository,\n        IApplicationCacheService applicationCacheService,\n        IEventService eventService,\n        IUpdateOrganizationSubscriptionCommand updateOrganizationSubscriptionCommand,\n        IFeatureService featureService)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _paymentService = paymentService;\n        _mailService = mailService;\n        _logger = logger;\n        _serviceAccountRepository = serviceAccountRepository;\n        _globalSettings = globalSettings;\n        _organizationRepository = organizationRepository;\n        _applicationCacheService = applicationCacheService;\n        _eventService = eventService;\n        _updateOrganizationSubscriptionCommand = updateOrganizationSubscriptionCommand;\n        _featureService = featureService;\n    }\n\n    public async Task UpdateSubscriptionAsync(SecretsManagerSubscriptionUpdate update)\n    {\n        await ValidateUpdateAsync(update);\n\n        await FinalizeSubscriptionAdjustmentAsync(update);\n\n        await ValidateAutoScaleLimitsAsync(update);\n    }\n\n    private async Task FinalizeSubscriptionAdjustmentAsync(SecretsManagerSubscriptionUpdate update)\n    {\n        if (_featureService.IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand))\n        {\n            var builder = OrganizationSubscriptionChangeSet.Builder();\n\n            if (update.SmSeatsChanged)\n            {\n                builder.UpdateItemQuantity(\n                    update.Plan.SecretsManager.StripeSeatPlanId,\n                    update.SmSeatsExcludingBase);\n            }\n\n            if (update.SmServiceAccountsChanged)\n            {\n                if (update.Organization.SmServiceAccounts > update.Plan.SecretsManager.BaseServiceAccount)\n                {\n                    builder.UpdateItemQuantity(\n                        update.Plan.SecretsManager.StripeServiceAccountPlanId,\n                        update.SmServiceAccountsExcludingBase);\n                }\n                else\n                {\n                    builder.AddItem(\n                        update.Plan.SecretsManager.StripeServiceAccountPlanId,\n                        update.SmServiceAccountsExcludingBase);\n                }\n            }\n\n            var changeSet = builder.Build();\n            if (changeSet.Changes.Any())\n            {\n                var result = await _updateOrganizationSubscriptionCommand.Run(update.Organization, changeSet);\n                result.GetValueOrThrow();\n            }\n        }\n        else\n        {\n            if (update.SmSeatsChanged)\n            {\n                await _paymentService.AdjustSmSeatsAsync(update.Organization, update.Plan, update.SmSeatsExcludingBase);\n\n                // TODO: call ReferenceEventService - see AC-1481\n            }\n\n            if (update.SmServiceAccountsChanged)\n            {\n                await _paymentService.AdjustServiceAccountsAsync(update.Organization, update.Plan,\n                    update.SmServiceAccountsExcludingBase);\n\n                // TODO: call ReferenceEventService - see AC-1481\n            }\n        }\n\n        var organization = update.Organization;\n        organization.SmSeats = update.SmSeats;\n        organization.SmServiceAccounts = update.SmServiceAccounts;\n        organization.MaxAutoscaleSmSeats = update.MaxAutoscaleSmSeats;\n        organization.MaxAutoscaleSmServiceAccounts = update.MaxAutoscaleSmServiceAccounts;\n\n        await ReplaceAndUpdateCacheAsync(organization);\n    }\n\n    private async Task SendSeatLimitEmailAsync(Organization organization)\n    {\n        try\n        {\n            var ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,\n                    OrganizationUserType.Owner))\n                .Select(u => u.Email).Distinct();\n\n            await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmSeats!.Value, ownerEmails);\n\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(e, $\"Error encountered notifying organization owners of seats limit reached.\");\n        }\n    }\n\n    private async Task SendServiceAccountLimitEmailAsync(Organization organization)\n    {\n        try\n        {\n            var ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,\n                    OrganizationUserType.Owner))\n                .Select(u => u.Email).Distinct();\n\n            await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts!.Value, ownerEmails);\n\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(e, $\"Error encountered notifying organization owners of machine accounts limit reached.\");\n        }\n\n    }\n\n    public async Task ValidateUpdateAsync(SecretsManagerSubscriptionUpdate update)\n    {\n        if (_globalSettings.SelfHosted)\n        {\n            var message = update.Autoscaling\n                ? \"Cannot autoscale on a self-hosted instance.\"\n                : \"Cannot update subscription on a self-hosted instance.\";\n            throw new BadRequestException(message);\n        }\n\n        ValidateOrganization(update);\n\n        if (update.SmSeatsChanged)\n        {\n            await ValidateSmSeatsUpdateAsync(update);\n        }\n\n        if (update.SmServiceAccountsChanged)\n        {\n            await ValidateSmServiceAccountsUpdateAsync(update);\n        }\n\n        if (update.MaxAutoscaleSmSeatsChanged)\n        {\n            ValidateMaxAutoscaleSmSeatsUpdateAsync(update);\n        }\n\n        if (update.MaxAutoscaleSmServiceAccountsChanged)\n        {\n            ValidateMaxAutoscaleSmServiceAccountUpdate(update);\n        }\n    }\n\n    private void ValidateOrganization(SecretsManagerSubscriptionUpdate update)\n    {\n        var organization = update.Organization;\n\n        if (!organization.UseSecretsManager)\n        {\n            throw new BadRequestException(\"Organization has no access to Secrets Manager.\");\n        }\n\n        if (update.Plan.ProductTier == ProductTierType.Free)\n        {\n            // No need to check the organization is set up with Stripe\n            return;\n        }\n\n        if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))\n        {\n            throw new BadRequestException(\"No payment method found.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))\n        {\n            throw new BadRequestException(\"No subscription found.\");\n        }\n    }\n\n    private async Task ValidateSmSeatsUpdateAsync(SecretsManagerSubscriptionUpdate update)\n    {\n        var organization = update.Organization;\n        var plan = update.Plan;\n\n        // Check if the organization has unlimited seats\n        if (organization.SmSeats == null)\n        {\n            throw new BadRequestException(\"Organization has no Secrets Manager seat limit, no need to adjust seats\");\n        }\n\n        if (update.Autoscaling && update.SmSeats!.Value < organization.SmSeats.Value)\n        {\n            throw new BadRequestException(\"Cannot use autoscaling to subtract seats.\");\n        }\n\n        // Check plan maximum seats\n        if (!plan.SecretsManager.HasAdditionalSeatsOption ||\n            (plan.SecretsManager.MaxAdditionalSeats.HasValue && update.SmSeatsExcludingBase > plan.SecretsManager.MaxAdditionalSeats.Value))\n        {\n            var planMaxSeats = plan.SecretsManager.BaseSeats + plan.SecretsManager.MaxAdditionalSeats.GetValueOrDefault();\n            throw new BadRequestException($\"You have reached the maximum number of Secrets Manager seats ({planMaxSeats}) for this plan.\");\n        }\n\n        // Check autoscale maximum seats\n        if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats!.Value > update.MaxAutoscaleSmSeats.Value)\n        {\n            var message = update.Autoscaling\n                ? \"Secrets Manager seat limit has been reached.\"\n                : \"Cannot set max seat autoscaling below seat count.\";\n            throw new BadRequestException(message);\n        }\n\n        // Check minimum seats included with plan\n        if (plan.SecretsManager.BaseSeats > update.SmSeats!.Value)\n        {\n            throw new BadRequestException($\"Plan has a minimum of {plan.SecretsManager.BaseSeats} Secrets Manager  seats.\");\n        }\n\n        // Check minimum seats required by business logic\n        if (update.SmSeats.Value <= 0)\n        {\n            throw new BadRequestException(\"You must have at least 1 Secrets Manager seat.\");\n        }\n\n        // Check minimum seats currently in use by the organization\n        if (organization.SmSeats.Value > update.SmSeats.Value)\n        {\n            // Retrieve the number of currently occupied Secrets Manager seats for the organization.\n            var occupiedSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);\n\n            // Check if the occupied number of seats exceeds the updated seat count.\n            // If so, throw an exception indicating that the subscription cannot be decreased below the current usage.\n            if (occupiedSeats > update.SmSeats.Value)\n            {\n                throw new BadRequestException($\"{occupiedSeats} users are currently occupying Secrets Manager seats. \" +\n                                              \"You cannot decrease your subscription below your current occupied seat count.\");\n            }\n        }\n\n        // Check that SM seats aren't greater than password manager seats\n        if (organization.Seats < update.SmSeats.Value)\n        {\n            throw new BadRequestException(\"You cannot have more Secrets Manager seats than Password Manager seats.\");\n        }\n    }\n\n    private async Task ValidateSmServiceAccountsUpdateAsync(SecretsManagerSubscriptionUpdate update)\n    {\n        var organization = update.Organization;\n        var plan = update.Plan;\n\n        // Check if the organization has unlimited service accounts\n        if (organization.SmServiceAccounts == null)\n        {\n            throw new BadRequestException(\"Organization has no machine accounts limit, no need to adjust machine accounts\");\n        }\n\n        if (update.Autoscaling && update.SmServiceAccounts!.Value < organization.SmServiceAccounts.Value)\n        {\n            throw new BadRequestException(\"Cannot use autoscaling to subtract machine accounts.\");\n        }\n\n        // Check plan maximum service accounts\n        if (!plan.SecretsManager.HasAdditionalServiceAccountOption ||\n            (plan.SecretsManager.MaxAdditionalServiceAccount.HasValue && update.SmServiceAccountsExcludingBase > plan.SecretsManager.MaxAdditionalServiceAccount.Value))\n        {\n            var planMaxServiceAccounts = plan.SecretsManager.BaseServiceAccount +\n                                         plan.SecretsManager.MaxAdditionalServiceAccount.GetValueOrDefault();\n            throw new BadRequestException($\"You have reached the maximum number of machine accounts ({planMaxServiceAccounts}) for this plan.\");\n        }\n\n        // Check autoscale maximum service accounts\n        if (update.MaxAutoscaleSmServiceAccounts.HasValue &&\n            update.SmServiceAccounts!.Value > update.MaxAutoscaleSmServiceAccounts.Value)\n        {\n            var message = update.Autoscaling\n                ? \"Secrets Manager machine account limit has been reached.\"\n                : \"Cannot set max machine accounts autoscaling below machine account amount.\";\n            throw new BadRequestException(message);\n        }\n\n        // Check minimum service accounts included with plan\n        if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts!.Value)\n        {\n            throw new BadRequestException($\"Plan has a minimum of {plan.SecretsManager.BaseServiceAccount} machine accounts.\");\n        }\n\n        // Check minimum service accounts required by business logic\n        if (update.SmServiceAccounts.Value <= 0)\n        {\n            throw new BadRequestException(\"You must have at least 1 machine account.\");\n        }\n\n        // Check minimum service accounts currently in use by the organization\n        if (!organization.SmServiceAccounts.HasValue || organization.SmServiceAccounts.Value > update.SmServiceAccounts.Value)\n        {\n            var currentServiceAccounts = await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);\n            if (currentServiceAccounts > update.SmServiceAccounts)\n            {\n                throw new BadRequestException($\"Your organization currently has {currentServiceAccounts} machine accounts. \" +\n                                              $\"You cannot decrease your subscription below your current machine account usage.\");\n            }\n        }\n    }\n\n    private void ValidateMaxAutoscaleSmSeatsUpdateAsync(SecretsManagerSubscriptionUpdate update)\n    {\n        var plan = update.Plan;\n\n        if (!update.MaxAutoscaleSmSeats.HasValue)\n        {\n            // autoscale limit has been turned off, no validation required\n            return;\n        }\n\n        if (update.SmSeats.HasValue && update.MaxAutoscaleSmSeats.Value < update.SmSeats.Value)\n        {\n            throw new BadRequestException($\"Cannot set max Secrets Manager seat autoscaling below current Secrets Manager seat count.\");\n        }\n\n        if (plan.SecretsManager.MaxSeats.HasValue && plan.SecretsManager.MaxSeats.Value > 0 && update.MaxAutoscaleSmSeats.Value > plan.SecretsManager.MaxSeats)\n        {\n            throw new BadRequestException(string.Concat(\n                $\"Your plan has a Secrets Manager seat limit of {plan.SecretsManager.MaxSeats}, \",\n                $\"but you have specified a max autoscale count of {update.MaxAutoscaleSmSeats}.\",\n                \"Reduce your max autoscale count.\"));\n        }\n\n        if (!plan.SecretsManager.AllowSeatAutoscale)\n        {\n            throw new BadRequestException(\"Your plan does not allow Secrets Manager seat autoscaling.\");\n        }\n    }\n\n    private void ValidateMaxAutoscaleSmServiceAccountUpdate(SecretsManagerSubscriptionUpdate update)\n    {\n        var plan = update.Plan;\n\n        if (!update.MaxAutoscaleSmServiceAccounts.HasValue)\n        {\n            // autoscale limit has been turned off, no validation required\n            return;\n        }\n\n        if (update.SmServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value < update.SmServiceAccounts.Value)\n        {\n            throw new BadRequestException(\n                $\"Cannot set max machine accounts autoscaling below current machine accounts count.\");\n        }\n\n        if (!plan.SecretsManager.AllowServiceAccountsAutoscale)\n        {\n            throw new BadRequestException(\"Your plan does not allow machine accounts autoscaling.\");\n        }\n\n        if (plan.SecretsManager.MaxServiceAccounts.HasValue && update.MaxAutoscaleSmServiceAccounts.Value > plan.SecretsManager.MaxServiceAccounts)\n        {\n            throw new BadRequestException(string.Concat(\n                $\"Your plan has a machine account limit of {plan.SecretsManager.MaxServiceAccounts}, \",\n                $\"but you have specified a max autoscale count of {update.MaxAutoscaleSmServiceAccounts}.\",\n                \"Reduce your max autoscale count.\"));\n        }\n    }\n\n    // TODO: This is a temporary duplication of OrganizationService.ReplaceAndUpdateCache to avoid a circular dependency.\n    // TODO: This should no longer be necessary when user-related methods are extracted from OrganizationService: see PM-1880\n    private async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)\n    {\n        await _organizationRepository.ReplaceAsync(org);\n        await _applicationCacheService.UpsertOrganizationAbilityAsync(org);\n\n        if (orgEvent.HasValue)\n        {\n            await _eventService.LogOrganizationEventAsync(org, orgEvent.Value);\n        }\n    }\n\n    private async Task ValidateAutoScaleLimitsAsync(SecretsManagerSubscriptionUpdate update)\n    {\n        var (smSeatAutoScaleLimitReached, smServiceAccountsLimitReached) = await AreAutoscaleLimitsReachedAsync(update);\n\n        if (smSeatAutoScaleLimitReached)\n        {\n            await SendSeatLimitEmailAsync(update.Organization);\n        }\n\n        if (smServiceAccountsLimitReached)\n        {\n            await SendServiceAccountLimitEmailAsync(update.Organization);\n        }\n    }\n\n    private async Task<(bool, bool)> AreAutoscaleLimitsReachedAsync(SecretsManagerSubscriptionUpdate update)\n    {\n        var smSeatAutoScaleLimitReached = false;\n        var smServiceAccountsLimitReached = false;\n\n        var (occupiedSmSeats, occupiedSmServiceAccounts) = await GetOccupiedSmSeatsAndServiceAccountsAsync(update.Organization.Id);\n\n        if (occupiedSmSeats > 0\n            && update.MaxAutoscaleSmSeats is not null\n            && occupiedSmSeats == update.MaxAutoscaleSmSeats!.Value)\n        {\n            smSeatAutoScaleLimitReached = true;\n        }\n\n        if (occupiedSmServiceAccounts > 0\n            && update.MaxAutoscaleSmServiceAccounts is not null\n            && occupiedSmServiceAccounts == update.MaxAutoscaleSmServiceAccounts!.Value)\n        {\n            smServiceAccountsLimitReached = true;\n        }\n\n        return (smSeatAutoScaleLimitReached, smServiceAccountsLimitReached);\n    }\n\n    /// <summary>\n    /// Requests the number of Secret Manager seats and service accounts currently used by the organization\n    /// </summary>\n    /// <param name=\"organizationId\"> The id of the organization</param>\n    /// <returns > A tuple containing the occupied seats and the occupied service account counts</returns>\n    private async Task<(int, int)> GetOccupiedSmSeatsAndServiceAccountsAsync(Guid organizationId)\n    {\n        var occupiedSmSeatsTask = _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organizationId);\n        var occupiedServiceAccountsTask = _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organizationId);\n        return (await occupiedSmSeatsTask, await occupiedServiceAccountsTask);\n    }\n}\n"
  },
  {
    "path": "src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;\n\npublic class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand\n{\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly IPolicyRepository _policyRepository;\n    private readonly IPolicyQuery _policyQuery;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly IOrganizationConnectionRepository _organizationConnectionRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationService _organizationService;\n    private readonly IFeatureService _featureService;\n    private readonly IOrganizationBillingService _organizationBillingService;\n    private readonly IPricingClient _pricingClient;\n    private readonly IUpgradeOrganizationPlanVNextCommand _upgradeOrganizationPlanVNextCommand;\n    private readonly IUserRepository _userRepository;\n\n    public UpgradeOrganizationPlanCommand(\n        IOrganizationUserRepository organizationUserRepository,\n        ICollectionRepository collectionRepository,\n        IGroupRepository groupRepository,\n        IStripePaymentService paymentService,\n        IPolicyRepository policyRepository,\n        IPolicyQuery policyQuery,\n        ISsoConfigRepository ssoConfigRepository,\n        IOrganizationConnectionRepository organizationConnectionRepository,\n        IServiceAccountRepository serviceAccountRepository,\n        IOrganizationRepository organizationRepository,\n        IOrganizationService organizationService,\n        IFeatureService featureService,\n        IOrganizationBillingService organizationBillingService,\n        IPricingClient pricingClient,\n        IUpgradeOrganizationPlanVNextCommand upgradeOrganizationPlanVNextCommand,\n        IUserRepository userRepository)\n    {\n        _organizationUserRepository = organizationUserRepository;\n        _collectionRepository = collectionRepository;\n        _groupRepository = groupRepository;\n        _paymentService = paymentService;\n        _policyRepository = policyRepository;\n        _policyQuery = policyQuery;\n        _ssoConfigRepository = ssoConfigRepository;\n        _organizationConnectionRepository = organizationConnectionRepository;\n        _serviceAccountRepository = serviceAccountRepository;\n        _organizationRepository = organizationRepository;\n        _organizationService = organizationService;\n        _featureService = featureService;\n        _organizationBillingService = organizationBillingService;\n        _pricingClient = pricingClient;\n        _upgradeOrganizationPlanVNextCommand = upgradeOrganizationPlanVNextCommand;\n        _userRepository = userRepository;\n    }\n\n    public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade, Guid? userId = null)\n    {\n        var organization = await GetOrgById(organizationId);\n\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        /*\n         * Billing is going to take over this entire command as part of our refactoring work around the\n         * organization subscription upgrade process.\n         */\n        if (_featureService.IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand))\n        {\n            var plan = await _pricingClient.GetPlanOrThrow(upgrade.Plan);\n            var result = await _upgradeOrganizationPlanVNextCommand.Run(\n                organization,\n                plan,\n                upgrade.Keys);\n            result.GetValueOrThrow();\n            return new Tuple<bool, string>(true, null);\n        }\n\n        if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))\n        {\n            throw new BadRequestException(\"Your account has no payment method available.\");\n        }\n\n        var existingPlan = await _pricingClient.GetPlanOrThrow(organization.PlanType);\n\n        var newPlan = await _pricingClient.GetPlanOrThrow(upgrade.Plan);\n\n        if (newPlan.Disabled)\n        {\n            throw new BadRequestException(\"Plan not found.\");\n        }\n\n        if (existingPlan.Type == newPlan.Type)\n        {\n            throw new BadRequestException(\"Organization is already on this plan.\");\n        }\n\n        if (existingPlan.UpgradeSortOrder >= newPlan.UpgradeSortOrder)\n        {\n            throw new BadRequestException(\"You cannot upgrade to this plan.\");\n        }\n\n        _organizationService.ValidatePasswordManagerPlan(newPlan, upgrade);\n\n        if (upgrade.UseSecretsManager)\n        {\n            _organizationService.ValidateSecretsManagerPlan(newPlan, upgrade);\n        }\n\n        var updatedPasswordManagerSeats = (short)(newPlan.PasswordManager.BaseSeats +\n                                                  (newPlan.PasswordManager.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));\n        if (!organization.Seats.HasValue || organization.Seats.Value > updatedPasswordManagerSeats)\n        {\n            var seatCounts =\n                await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);\n            if (seatCounts.Total > updatedPasswordManagerSeats)\n            {\n                if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)\n                {\n                    throw new BadRequestException($\"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. \" +\n                                                  $\"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.\");\n                }\n                else\n                {\n                    throw new BadRequestException($\"Your organization currently has {seatCounts.Total} seats filled. \" +\n                                              $\"Your new plan only has ({updatedPasswordManagerSeats}) seats. Remove some users.\");\n                }\n            }\n        }\n\n        if (newPlan.PasswordManager.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||\n                                                               organization.MaxCollections.Value >\n                                                               newPlan.PasswordManager.MaxCollections.Value))\n        {\n            var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);\n            if (collectionCount > newPlan.PasswordManager.MaxCollections.Value)\n            {\n                throw new BadRequestException($\"Your organization currently has {collectionCount} collections. \" +\n                                              $\"Your new plan allows for a maximum of ({newPlan.PasswordManager.MaxCollections.Value}) collections. \" +\n                                              \"Remove some collections.\");\n            }\n        }\n\n        if (!newPlan.HasGroups && organization.UseGroups)\n        {\n            var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);\n            if (groups.Any())\n            {\n                throw new BadRequestException($\"Your new plan does not allow the groups feature. \" +\n                                              $\"Remove your groups.\");\n            }\n        }\n\n        if (!newPlan.HasPolicies && organization.UsePolicies)\n        {\n            var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);\n            if (policies.Any(p => p.Enabled))\n            {\n                throw new BadRequestException($\"Your new plan does not allow the policies feature. \" +\n                                              $\"Disable your policies.\");\n            }\n        }\n\n        if (!newPlan.HasSso && organization.UseSso)\n        {\n            var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);\n            if (ssoConfig != null && ssoConfig.Enabled)\n            {\n                throw new BadRequestException($\"Your new plan does not allow the SSO feature. \" +\n                                              $\"Disable your SSO configuration.\");\n            }\n        }\n\n        if (!newPlan.HasKeyConnector && organization.UseKeyConnector)\n        {\n            var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);\n            if (ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector)\n            {\n                throw new BadRequestException(\"Your new plan does not allow the Key Connector feature. \" +\n                                              \"Disable your Key Connector.\");\n            }\n        }\n\n        if (!newPlan.HasResetPassword && organization.UseResetPassword)\n        {\n            var resetPasswordPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.ResetPassword);\n            if (resetPasswordPolicy.Enabled)\n            {\n                throw new BadRequestException(\"Your new plan does not allow the Password Reset feature. \" +\n                                              \"Disable your Password Reset policy.\");\n            }\n        }\n\n        if (!newPlan.HasScim && organization.UseScim)\n        {\n            var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,\n                OrganizationConnectionType.Scim);\n            if (scimConnections != null && scimConnections.Any(c => c.GetConfig<ScimConfig>()?.Enabled == true))\n            {\n                throw new BadRequestException(\"Your new plan does not allow the SCIM feature. \" +\n                                              \"Disable your SCIM configuration.\");\n            }\n        }\n\n        if (!newPlan.HasCustomPermissions && organization.UseCustomPermissions)\n        {\n            var organizationCustomUsers =\n                await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id,\n                    OrganizationUserType.Custom);\n            if (organizationCustomUsers.Any())\n            {\n                throw new BadRequestException(\"Your new plan does not allow the Custom Permissions feature. \" +\n                                              \"Disable your Custom Permissions configuration.\");\n            }\n        }\n\n        if (upgrade.UseSecretsManager)\n        {\n            await ValidateSecretsManagerSeatsAndServiceAccountAsync(upgrade, organization, newPlan);\n        }\n\n        // TODO: Check storage?\n        string paymentIntentClientSecret = null;\n        var success = true;\n\n        if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))\n        {\n            // Check if the user performing the upgrade is an owner of the organization\n            // This is used for discount validation - discounts only apply if the owner is upgrading\n            User owner = null;\n            if (userId.HasValue)\n            {\n                var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organization.Id, userId.Value);\n                if (organizationUser != null && organizationUser.Type == OrganizationUserType.Owner)\n                {\n                    owner = await _userRepository.GetByIdAsync(organizationUser.UserId.Value);\n                }\n            }\n\n            var sale = OrganizationSale.From(organization, upgrade, owner);\n            await _organizationBillingService.Finalize(sale);\n        }\n        else\n        {\n            paymentIntentClientSecret = await _paymentService.AdjustSubscription(\n                organization,\n                newPlan,\n                upgrade.AdditionalSeats,\n                upgrade.UseSecretsManager,\n                upgrade.AdditionalSmSeats,\n                upgrade.AdditionalServiceAccounts,\n                upgrade.AdditionalStorageGb);\n\n            success = string.IsNullOrEmpty(paymentIntentClientSecret);\n        }\n\n        organization.BusinessName = upgrade.BusinessName;\n        organization.PlanType = newPlan.Type;\n        organization.Seats = (short)(newPlan.PasswordManager.BaseSeats + upgrade.AdditionalSeats);\n        organization.MaxCollections = newPlan.PasswordManager.MaxCollections;\n        organization.UseGroups = newPlan.HasGroups;\n        organization.UseDirectory = newPlan.HasDirectory;\n        organization.UseEvents = newPlan.HasEvents;\n        organization.UseTotp = newPlan.HasTotp;\n        organization.Use2fa = newPlan.Has2fa;\n        organization.UseApi = newPlan.HasApi;\n        organization.SelfHost = newPlan.HasSelfHost;\n        organization.UsePolicies = newPlan.HasPolicies;\n        organization.UseMyItems = newPlan.HasMyItems;\n        organization.MaxStorageGb = (short)(newPlan.PasswordManager.BaseStorageGb + upgrade.AdditionalStorageGb);\n        organization.UseSso = newPlan.HasSso;\n        organization.UseOrganizationDomains = newPlan.HasOrganizationDomains;\n        organization.UseKeyConnector = newPlan.HasKeyConnector ? organization.UseKeyConnector : false;\n        organization.UseScim = newPlan.HasScim;\n        organization.UseResetPassword = newPlan.HasResetPassword;\n        organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;\n        organization.UseCustomPermissions = newPlan.HasCustomPermissions;\n        organization.Plan = newPlan.Name;\n        organization.Enabled = success;\n        organization.UsePasswordManager = true;\n        organization.UseSecretsManager = upgrade.UseSecretsManager;\n\n        organization.BackfillPublicPrivateKeys(upgrade.Keys);\n\n        if (upgrade.UseSecretsManager)\n        {\n            organization.SmSeats = newPlan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault();\n            organization.SmServiceAccounts = newPlan.SecretsManager.BaseServiceAccount +\n                                             upgrade.AdditionalServiceAccounts.GetValueOrDefault();\n        }\n\n        await _organizationService.ReplaceAndUpdateCacheAsync(organization);\n        return new Tuple<bool, string>(success, paymentIntentClientSecret);\n    }\n\n    private async Task ValidateSecretsManagerSeatsAndServiceAccountAsync(OrganizationUpgrade upgrade, Organization organization,\n        Models.StaticStore.Plan newSecretsManagerPlan)\n    {\n        var newPlanSmSeats = (short)(newSecretsManagerPlan.SecretsManager.BaseSeats +\n                                     (newSecretsManagerPlan.SecretsManager.HasAdditionalSeatsOption\n                                         ? upgrade.AdditionalSmSeats\n                                         : 0));\n        var occupiedSmSeats =\n            await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);\n\n        if (!organization.SmSeats.HasValue || organization.SmSeats.Value > newPlanSmSeats)\n        {\n            if (occupiedSmSeats > newPlanSmSeats)\n            {\n                throw new BadRequestException(\n                    $\"Your organization currently has {occupiedSmSeats} Secrets Manager seats filled. \" +\n                    $\"Your new plan only has {newPlanSmSeats} seats. Remove some users or increase your subscription.\");\n            }\n        }\n\n        var additionalServiceAccounts = newSecretsManagerPlan.SecretsManager.HasAdditionalServiceAccountOption\n            ? upgrade.AdditionalServiceAccounts\n            : 0;\n        var newPlanServiceAccounts = newSecretsManagerPlan.SecretsManager.BaseServiceAccount + additionalServiceAccounts;\n\n        if (!organization.SmServiceAccounts.HasValue || organization.SmServiceAccounts.Value > newPlanServiceAccounts)\n        {\n            var currentServiceAccounts =\n                await _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organization.Id);\n            if (currentServiceAccounts > newPlanServiceAccounts)\n            {\n                throw new BadRequestException(\n                    $\"Your organization currently has {currentServiceAccounts} machine accounts. \" +\n                    $\"Your new plan only allows {newSecretsManagerPlan.SecretsManager.MaxServiceAccounts} machine accounts. \" +\n                    \"Remove some machine accounts or increase your subscription.\");\n            }\n        }\n    }\n\n    private async Task<Organization> GetOrgById(Guid id)\n    {\n        return await _organizationRepository.GetByIdAsync(id);\n    }\n\n    private static string GetUpgradePath(ProductTierType oldProductTierType, ProductTierType newProductTierType)\n    {\n        var oldDescription = _upgradePath.TryGetValue(oldProductTierType, out var description)\n            ? description\n            : $\"{oldProductTierType:G}\";\n\n        var newDescription = _upgradePath.TryGetValue(newProductTierType, out description)\n            ? description\n            : $\"{newProductTierType:G}\";\n\n        return $\"{oldDescription} → {newDescription}\";\n    }\n\n    private static readonly Dictionary<ProductTierType, string> _upgradePath = new()\n    {\n        [ProductTierType.Free] = \"2-person org\",\n        [ProductTierType.Families] = \"Families\",\n        [ProductTierType.TeamsStarter] = \"Teams Starter\",\n        [ProductTierType.Teams] = \"Teams\",\n        [ProductTierType.Enterprise] = \"Enterprise\"\n    };\n}\n"
  },
  {
    "path": "src/Core/Platform/Installations/Commands/UpdateInstallationActivityDateCommand/IUpdateInstallationCommand.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Core.Platform.Installations;\n\n/// <summary>\n/// Command interface responsible for updating data on an `Installation`\n/// record.\n/// </summary>\n/// <remarks>\n/// This interface is implemented by `UpdateInstallationCommand`\n/// </remarks>\n/// <seealso cref=\"Bit.Core.Platform.Installations.UpdateInstallationCommand\"/>\npublic interface IUpdateInstallationCommand\n{\n    Task UpdateLastActivityDateAsync(Guid installationId);\n}\n"
  },
  {
    "path": "src/Core/Platform/Installations/Commands/UpdateInstallationActivityDateCommand/UpdateInstallationCommand.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Core.Platform.Installations;\n\n/// <summary>\n/// Commands responsible for updating an installation from\n/// `InstallationRepository`.\n/// </summary>\n/// <remarks>\n/// If referencing: you probably want the interface\n/// `IUpdateInstallationCommand` instead of directly calling this class.\n/// </remarks>\n/// <seealso cref=\"IUpdateInstallationCommand\"/>\npublic class UpdateInstallationCommand : IUpdateInstallationCommand\n{\n    private readonly IGetInstallationQuery _getInstallationQuery;\n    private readonly IInstallationRepository _installationRepository;\n    private readonly TimeProvider _timeProvider;\n\n    public UpdateInstallationCommand(\n        IGetInstallationQuery getInstallationQuery,\n        IInstallationRepository installationRepository,\n        TimeProvider timeProvider\n    )\n    {\n        _getInstallationQuery = getInstallationQuery;\n        _installationRepository = installationRepository;\n        _timeProvider = timeProvider;\n    }\n\n    public async Task UpdateLastActivityDateAsync(Guid installationId)\n    {\n        if (installationId == default)\n        {\n            throw new Exception\n            (\n              \"Tried to update the last activity date for \" +\n              \"an installation, but an invalid installation id was \" +\n              \"provided.\"\n            );\n        }\n        var installation = await _getInstallationQuery.GetByIdAsync(installationId);\n        if (installation == null)\n        {\n            throw new Exception\n            (\n              \"Tried to update the last activity date for \" +\n              $\"installation {installationId.ToString()}, but no \" +\n              \"installation was found for that id.\"\n            );\n        }\n        installation.LastActivityDate = _timeProvider.GetUtcNow().UtcDateTime;\n        await _installationRepository.UpsertAsync(installation);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Installations/Entities/Installation.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\n#nullable enable\n\nnamespace Bit.Core.Platform.Installations;\n\n/// <summary>\n/// The base entity for the SQL table `dbo.Installation`. Used to store\n/// information pertinent to self hosted Bitwarden installations.\n/// </summary>\npublic class Installation : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    [MaxLength(256)]\n    public string Email { get; set; } = null!;\n    [MaxLength(150)]\n    public string Key { get; set; } = null!;\n    public bool Enabled { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime? LastActivityDate { get; internal set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Installations/Queries/GetInstallationQuery/GetInstallationQuery.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Core.Platform.Installations;\n\n/// <summary>\n/// Queries responsible for fetching an installation from\n/// `InstallationRepository`.\n/// </summary>\n/// <remarks>\n/// If referencing: you probably want the interface `IGetInstallationQuery`\n/// instead of directly calling this class.\n/// </remarks>\n/// <seealso cref=\"IGetInstallationQuery\"/>\npublic class GetInstallationQuery : IGetInstallationQuery\n{\n    private readonly IInstallationRepository _installationRepository;\n\n    public GetInstallationQuery(IInstallationRepository installationRepository)\n    {\n        _installationRepository = installationRepository;\n    }\n\n    /// <inheritdoc cref=\"IGetInstallationQuery.GetByIdAsync\"/>\n    public async Task<Installation?> GetByIdAsync(Guid installationId)\n    {\n        if (installationId == default(Guid))\n        {\n            return null;\n        }\n        return await _installationRepository.GetByIdAsync(installationId);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Installations/Queries/GetInstallationQuery/IGetInstallationQuery.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Core.Platform.Installations;\n\n/// <summary>\n/// Query interface responsible for fetching an installation from\n/// `InstallationRepository`.\n/// </summary>\n/// <remarks>\n/// This interface is implemented by `GetInstallationQuery`\n/// </remarks>\n/// <seealso cref=\"GetInstallationQuery\"/>\npublic interface IGetInstallationQuery\n{\n    /// <summary>\n    /// Retrieves an installation from the `InstallationRepository` by its id.\n    /// </summary>\n    /// <param name=\"installationId\">The GUID id of the installation.</param>\n    /// <returns>A task containing an `Installation`.</returns>\n    /// <seealso cref=\"T:Bit.Core.Platform.Installations.Repositories.IInstallationRepository\"/>\n    Task<Installation?> GetByIdAsync(Guid installationId);\n}\n"
  },
  {
    "path": "src/Core/Platform/Installations/Repositories/IInstallationRepository.cs",
    "content": "﻿using Bit.Core.Repositories;\n\n#nullable enable\n\nnamespace Bit.Core.Platform.Installations;\n\n/// <summary>\n/// The CRUD repository interface for communicating with `dbo.Installation`,\n/// which is used to store information pertinent to self-hosted\n/// installations.\n/// </summary>\n/// <remarks>\n/// This interface is implemented by `InstallationRepository` in the Dapper\n/// and Entity Framework projects.\n/// </remarks>\n/// <seealso cref=\"T:Bit.Infrastructure.Dapper.Platform.Installations.Repositories.InstallationRepository\"/>\npublic interface IInstallationRepository : IRepository<Installation, Guid>\n{\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Delivery/AmazonSesMailDeliveryService.cs",
    "content": "﻿#nullable enable\n\nusing Amazon;\nusing Amazon.SimpleEmail;\nusing Amazon.SimpleEmail.Model;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Mail.Delivery;\n\npublic class AmazonSesMailDeliveryService : IMailDeliveryService, IDisposable\n{\n    private readonly GlobalSettings _globalSettings;\n    private readonly IWebHostEnvironment _hostingEnvironment;\n    private readonly ILogger<AmazonSesMailDeliveryService> _logger;\n    private readonly IAmazonSimpleEmailService _client;\n    private readonly string _source;\n    private readonly string _senderTag;\n    private readonly string? _configSetName;\n\n    public AmazonSesMailDeliveryService(\n        GlobalSettings globalSettings,\n        IWebHostEnvironment hostingEnvironment,\n        ILogger<AmazonSesMailDeliveryService> logger)\n    : this(globalSettings, hostingEnvironment, logger,\n          new AmazonSimpleEmailServiceClient(\n            globalSettings.Amazon.AccessKeyId,\n            globalSettings.Amazon.AccessKeySecret,\n            RegionEndpoint.GetBySystemName(globalSettings.Amazon.Region))\n          )\n    {\n    }\n\n    public AmazonSesMailDeliveryService(\n        GlobalSettings globalSettings,\n        IWebHostEnvironment hostingEnvironment,\n        ILogger<AmazonSesMailDeliveryService> logger,\n        IAmazonSimpleEmailService amazonSimpleEmailService)\n    {\n        if (string.IsNullOrWhiteSpace(globalSettings.Amazon?.AccessKeyId))\n        {\n            throw new ArgumentNullException(nameof(globalSettings.Amazon.AccessKeyId));\n        }\n        if (string.IsNullOrWhiteSpace(globalSettings.Amazon?.AccessKeySecret))\n        {\n            throw new ArgumentNullException(nameof(globalSettings.Amazon.AccessKeySecret));\n        }\n        if (string.IsNullOrWhiteSpace(globalSettings.Amazon?.Region))\n        {\n            throw new ArgumentNullException(nameof(globalSettings.Amazon.Region));\n        }\n\n        var replyToEmail = CoreHelpers.PunyEncode(globalSettings.Mail.ReplyToEmail);\n\n        _globalSettings = globalSettings;\n        _hostingEnvironment = hostingEnvironment;\n        _logger = logger;\n        _client = amazonSimpleEmailService;\n        _source = $\"\\\"{globalSettings.SiteName}\\\" <{replyToEmail}>\";\n        _senderTag = $\"Server_{globalSettings.ProjectName?.Replace(' ', '_')}\";\n        if (!string.IsNullOrWhiteSpace(_globalSettings.Mail.AmazonConfigSetName))\n        {\n            _configSetName = _globalSettings.Mail.AmazonConfigSetName;\n        }\n    }\n\n    public void Dispose()\n    {\n        _client?.Dispose();\n    }\n\n    public async Task SendEmailAsync(MailMessage message)\n    {\n        var request = new SendEmailRequest\n        {\n            ConfigurationSetName = _configSetName,\n            Source = _source,\n            Destination = new Destination\n            {\n                ToAddresses = message.ToEmails\n                    .Select(email => CoreHelpers.PunyEncode(email))\n                    .ToList()\n            },\n            Message = new Message\n            {\n                Subject = new Content(message.Subject),\n                Body = new Body\n                {\n                    Html = new Content\n                    {\n                        Charset = \"UTF-8\",\n                        Data = message.HtmlContent\n                    },\n                    Text = new Content\n                    {\n                        Charset = \"UTF-8\",\n                        Data = message.TextContent\n                    }\n                }\n            },\n            Tags = new List<MessageTag>\n            {\n                new MessageTag { Name = \"Environment\", Value = _hostingEnvironment.EnvironmentName },\n                new MessageTag { Name = \"Sender\", Value = _senderTag }\n            }\n        };\n\n        if (message.BccEmails?.Any() ?? false)\n        {\n            request.Destination.BccAddresses = message.BccEmails\n                .Select(email => CoreHelpers.PunyEncode(email))\n                .ToList();\n        }\n\n        if (!string.IsNullOrWhiteSpace(message.Category))\n        {\n            request.Tags.Add(new MessageTag { Name = \"Category\", Value = message.Category });\n        }\n\n        try\n        {\n            await SendAsync(request, false);\n        }\n        catch (Exception e)\n        {\n            _logger.LogWarning(e, \"Failed to send email. Retrying...\");\n            await SendAsync(request, true);\n            throw;\n        }\n    }\n\n    private async Task SendAsync(SendEmailRequest request, bool retry)\n    {\n        if (retry)\n        {\n            // wait and try again\n            await Task.Delay(2000);\n        }\n        await _client.SendEmailAsync(request);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Delivery/IMailDeliveryService.cs",
    "content": "﻿using Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Platform.Mail.Delivery;\n\npublic interface IMailDeliveryService\n{\n    Task SendEmailAsync(MailMessage message);\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Delivery/MailKitSmtpMailDeliveryService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing MailKit.Net.Smtp;\nusing Microsoft.Extensions.Logging;\nusing MimeKit;\n\nnamespace Bit.Core.Platform.Mail.Delivery;\n\npublic class MailKitSmtpMailDeliveryService : IMailDeliveryService\n{\n    private readonly GlobalSettings _globalSettings;\n    private readonly ILogger<MailKitSmtpMailDeliveryService> _logger;\n    private readonly string _replyDomain;\n    private readonly string _replyEmail;\n\n    public MailKitSmtpMailDeliveryService(\n        GlobalSettings globalSettings,\n        ILogger<MailKitSmtpMailDeliveryService> logger)\n    {\n        if (globalSettings.Mail.Smtp?.Host == null)\n        {\n            throw new ArgumentNullException(nameof(globalSettings.Mail.Smtp.Host));\n        }\n\n        if (globalSettings.Mail.ReplyToEmail == null)\n        {\n            throw new InvalidOperationException(\"A GlobalSettings.Mail.ReplyToEmail is required to be set up.\");\n        }\n\n        _replyEmail = CoreHelpers.PunyEncode(globalSettings.Mail.ReplyToEmail);\n\n        if (_replyEmail.Contains(\"@\"))\n        {\n            _replyDomain = _replyEmail.Split('@')[1];\n        }\n\n        _globalSettings = globalSettings;\n        _logger = logger;\n    }\n\n    public async Task SendEmailAsync(Models.Mail.MailMessage message)\n        => await SendEmailAsync(message, CancellationToken.None);\n\n    public async Task SendEmailAsync(Models.Mail.MailMessage message, CancellationToken cancellationToken)\n    {\n        var mimeMessage = new MimeMessage();\n        mimeMessage.From.Add(new MailboxAddress(_globalSettings.SiteName, _replyEmail));\n        mimeMessage.Subject = message.Subject;\n        if (!string.IsNullOrWhiteSpace(_replyDomain))\n        {\n            mimeMessage.MessageId = $\"<{Guid.NewGuid()}@{_replyDomain}>\";\n        }\n\n        foreach (var address in message.ToEmails)\n        {\n            var punyencoded = CoreHelpers.PunyEncode(address);\n            mimeMessage.To.Add(MailboxAddress.Parse(punyencoded));\n        }\n\n        if (message.BccEmails != null)\n        {\n            foreach (var address in message.BccEmails)\n            {\n                var punyencoded = CoreHelpers.PunyEncode(address);\n                mimeMessage.Bcc.Add(MailboxAddress.Parse(punyencoded));\n            }\n        }\n\n        var builder = new BodyBuilder();\n        if (!string.IsNullOrWhiteSpace(message.TextContent))\n        {\n            builder.TextBody = message.TextContent;\n        }\n        builder.HtmlBody = message.HtmlContent;\n        mimeMessage.Body = builder.ToMessageBody();\n\n        using (var client = new SmtpClient())\n        {\n            if (_globalSettings.Mail.Smtp.TrustServer)\n            {\n                client.ServerCertificateValidationCallback = (s, c, h, e) => true;\n            }\n\n            if (!_globalSettings.Mail.Smtp.StartTls && !_globalSettings.Mail.Smtp.Ssl &&\n                _globalSettings.Mail.Smtp.Port == 25)\n            {\n                await client.ConnectAsync(\n                    _globalSettings.Mail.Smtp.Host,\n                    _globalSettings.Mail.Smtp.Port,\n                    MailKit.Security.SecureSocketOptions.None,\n                    cancellationToken\n                );\n            }\n            else\n            {\n                var useSsl = _globalSettings.Mail.Smtp.Port == 587 && !_globalSettings.Mail.Smtp.SslOverride ?\n                    false : _globalSettings.Mail.Smtp.Ssl;\n                await client.ConnectAsync(\n                    _globalSettings.Mail.Smtp.Host,\n                    _globalSettings.Mail.Smtp.Port,\n                    useSsl,\n                    cancellationToken\n                );\n            }\n\n            if (CoreHelpers.SettingHasValue(_globalSettings.Mail.Smtp.Username) &&\n                CoreHelpers.SettingHasValue(_globalSettings.Mail.Smtp.Password))\n            {\n                await client.AuthenticateAsync(\n                    _globalSettings.Mail.Smtp.Username,\n                    _globalSettings.Mail.Smtp.Password,\n                    cancellationToken\n                );\n            }\n\n            await client.SendAsync(mimeMessage, cancellationToken);\n            await client.DisconnectAsync(true, cancellationToken);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Delivery/MultiServiceMailDeliveryService.cs",
    "content": "﻿using Bit.Core.Models.Mail;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Mail.Delivery;\n\npublic class MultiServiceMailDeliveryService : IMailDeliveryService\n{\n    private readonly IMailDeliveryService _sesService;\n    private readonly IMailDeliveryService _sendGridService;\n    private readonly int _sendGridPercentage;\n\n    private static Random _random = new Random();\n\n    public MultiServiceMailDeliveryService(\n        GlobalSettings globalSettings,\n        IWebHostEnvironment hostingEnvironment,\n        ILogger<AmazonSesMailDeliveryService> sesLogger,\n        ILogger<SendGridMailDeliveryService> sendGridLogger)\n    {\n        _sesService = new AmazonSesMailDeliveryService(globalSettings, hostingEnvironment, sesLogger);\n        _sendGridService = new SendGridMailDeliveryService(globalSettings, hostingEnvironment, sendGridLogger);\n\n        // disabled by default (-1)\n        _sendGridPercentage = (globalSettings.Mail?.SendGridPercentage).GetValueOrDefault(-1);\n    }\n\n    public async Task SendEmailAsync(MailMessage message)\n    {\n        var roll = _random.Next(0, 99);\n        if (roll < _sendGridPercentage)\n        {\n            await _sendGridService.SendEmailAsync(message);\n        }\n        else\n        {\n            await _sesService.SendEmailAsync(message);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Delivery/NoopMailDeliveryService.cs",
    "content": "﻿using Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Platform.Mail.Delivery;\n\npublic class NoopMailDeliveryService : IMailDeliveryService\n{\n    public Task SendEmailAsync(MailMessage message)\n    {\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Delivery/SendGridMailDeliveryService.cs",
    "content": "﻿using Bit.Core.Models.Mail;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Logging;\nusing SendGrid;\nusing SendGrid.Helpers.Mail;\n\nnamespace Bit.Core.Platform.Mail.Delivery;\n\npublic class SendGridMailDeliveryService : IMailDeliveryService, IDisposable\n{\n    private readonly GlobalSettings _globalSettings;\n    private readonly IWebHostEnvironment _hostingEnvironment;\n    private readonly ILogger<SendGridMailDeliveryService> _logger;\n    private readonly ISendGridClient _client;\n    private readonly string _senderTag;\n    private readonly string _replyToEmail;\n\n    public SendGridMailDeliveryService(\n        GlobalSettings globalSettings,\n        IWebHostEnvironment hostingEnvironment,\n        ILogger<SendGridMailDeliveryService> logger)\n        : this(new SendGridClient(globalSettings.Mail.SendGridApiKey, globalSettings.Mail.SendGridApiHost),\n             globalSettings, hostingEnvironment, logger)\n    {\n    }\n\n    public void Dispose()\n    {\n        // TODO: nothing to dispose\n    }\n\n    public SendGridMailDeliveryService(\n        ISendGridClient client,\n        GlobalSettings globalSettings,\n        IWebHostEnvironment hostingEnvironment,\n        ILogger<SendGridMailDeliveryService> logger)\n    {\n        if (string.IsNullOrWhiteSpace(globalSettings.Mail?.SendGridApiKey))\n        {\n            throw new ArgumentNullException(nameof(globalSettings.Mail.SendGridApiKey));\n        }\n\n        _globalSettings = globalSettings;\n        _hostingEnvironment = hostingEnvironment;\n        _logger = logger;\n        _client = client;\n        _senderTag = $\"Server_{globalSettings.ProjectName?.Replace(' ', '_')}\";\n        _replyToEmail = CoreHelpers.PunyEncode(globalSettings.Mail.ReplyToEmail);\n    }\n\n    public async Task SendEmailAsync(MailMessage message)\n    {\n        var msg = new SendGridMessage();\n        msg.SetFrom(new EmailAddress(_replyToEmail, _globalSettings.SiteName));\n        msg.AddTos(message.ToEmails.Select(e => new EmailAddress(CoreHelpers.PunyEncode(e))).ToList());\n        if (message.BccEmails?.Any() ?? false)\n        {\n            msg.AddBccs(message.BccEmails.Select(e => new EmailAddress(CoreHelpers.PunyEncode(e))).ToList());\n        }\n\n        msg.SetSubject(message.Subject);\n        msg.AddContent(MimeType.Text, message.TextContent);\n        msg.AddContent(MimeType.Html, message.HtmlContent);\n\n        msg.AddCategory($\"type:{message.Category}\");\n        msg.AddCategory($\"env:{_hostingEnvironment.EnvironmentName}\");\n        msg.AddCategory($\"sender:{_senderTag}\");\n\n        msg.SetClickTracking(false, false);\n        msg.SetOpenTracking(false);\n\n        if (message.MetaData != null &&\n            message.MetaData.TryGetValue(\"SendGridBypassListManagement\", out var sendGridBypassListManagement) &&\n            Convert.ToBoolean(sendGridBypassListManagement))\n        {\n            msg.SetBypassListManagement(true);\n        }\n\n        try\n        {\n            var success = await SendAsync(msg, false);\n            if (!success)\n            {\n                _logger.LogWarning(\"Failed to send email. Retrying...\");\n                await SendAsync(msg, true);\n            }\n        }\n        catch (Exception e)\n        {\n            _logger.LogWarning(e, \"Failed to send email (with exception). Retrying...\");\n            await SendAsync(msg, true);\n            throw;\n        }\n    }\n\n    private async Task<bool> SendAsync(SendGridMessage message, bool retry)\n    {\n        if (retry)\n        {\n            // wait and try again\n            await Task.Delay(2000);\n        }\n\n        var response = await _client.SendEmailAsync(message);\n        if (!response.IsSuccessStatusCode)\n        {\n            var responseBody = await response.Body.ReadAsStringAsync();\n            _logger.LogError(\"SendGrid email sending failed with {0}: {1}\", response.StatusCode, responseBody);\n        }\n        return response.IsSuccessStatusCode;\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Enqueuing/AzureQueueMailService.cs",
    "content": "﻿using Azure.Storage.Queues;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Platform.Mail.Enqueuing;\npublic class AzureQueueMailService : AzureQueueService<IMailQueueMessage>, IMailEnqueuingService\n{\n    public AzureQueueMailService(GlobalSettings globalSettings) : base(\n        new QueueClient(globalSettings.Mail.ConnectionString, \"mail\"),\n        JsonHelpers.IgnoreWritingNull)\n    { }\n\n    public Task EnqueueAsync(IMailQueueMessage message, Func<IMailQueueMessage, Task> fallback) =>\n        CreateManyAsync(new[] { message });\n\n    public Task EnqueueManyAsync(IEnumerable<IMailQueueMessage> messages, Func<IMailQueueMessage, Task> fallback) =>\n        CreateManyAsync(messages);\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Enqueuing/BlockingMailQueueService.cs",
    "content": "﻿using Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Platform.Mail.Enqueuing;\npublic class BlockingMailEnqueuingService : IMailEnqueuingService\n{\n    public async Task EnqueueAsync(IMailQueueMessage message, Func<IMailQueueMessage, Task> fallback)\n    {\n        await fallback(message);\n    }\n\n    public async Task EnqueueManyAsync(IEnumerable<IMailQueueMessage> messages, Func<IMailQueueMessage, Task> fallback)\n    {\n        foreach (var message in messages)\n        {\n            await fallback(message);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Enqueuing/IMailEnqueuingService.cs",
    "content": "﻿using Bit.Core.Models.Mail;\n\nnamespace Bit.Core.Platform.Mail.Enqueuing;\n\npublic interface IMailEnqueuingService\n{\n    Task EnqueueAsync(IMailQueueMessage message, Func<IMailQueueMessage, Task> fallback);\n    Task EnqueueManyAsync(IEnumerable<IMailQueueMessage> messages, Func<IMailQueueMessage, Task> fallback);\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/HandlebarsMailService.cs",
    "content": "﻿using System.Diagnostics;\nusing System.Net;\nusing System.Reflection;\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Mail;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Business;\nusing Bit.Core.Auth.Models.Mail;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Models.Mail;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Models.Mail.Auth;\nusing Bit.Core.Models.Mail.Billing;\nusing Bit.Core.Models.Mail.FamiliesForEnterprise;\nusing Bit.Core.Models.Mail.Provider;\nusing Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Platform.Mail.Enqueuing;\nusing Bit.Core.SecretsManager.Models.Mail;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\nusing Core.Auth.Enums;\nusing HandlebarsDotNet;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Services.Mail;\n\n[Obsolete(\"The IMailService has been deprecated in favor of the IMailer. All new emails should be sent with an IMailer implementation.\")]\npublic class HandlebarsMailService : IMailService\n{\n    private const string Namespace = \"Bit.Core.MailTemplates.Handlebars\";\n    private const string _utcTimeZoneDisplay = \"UTC\";\n    private const string FailedTwoFactorAttemptCacheKeyFormat = \"FailedTwoFactorAttemptEmail_{0}\";\n\n    private readonly GlobalSettings _globalSettings;\n    private readonly IMailDeliveryService _mailDeliveryService;\n    private readonly IMailEnqueuingService _mailEnqueuingService;\n    private readonly IDistributedCache _distributedCache;\n    private readonly ILogger<HandlebarsMailService> _logger;\n    private readonly Dictionary<string, HandlebarsTemplate<object, object>> _templateCache = new();\n\n    private bool _registeredHelpersAndPartials = false;\n\n    public HandlebarsMailService(\n        GlobalSettings globalSettings,\n        IMailDeliveryService mailDeliveryService,\n        IMailEnqueuingService mailEnqueuingService,\n        IDistributedCache distributedCache,\n        ILogger<HandlebarsMailService> logger)\n    {\n        _globalSettings = globalSettings;\n        _mailDeliveryService = mailDeliveryService;\n        _mailEnqueuingService = mailEnqueuingService;\n        _distributedCache = distributedCache;\n        _logger = logger;\n    }\n\n    public async Task SendVerifyEmailEmailAsync(string email, Guid userId, string token)\n    {\n        var message = CreateDefaultMessage(\"Verify Your Email\", email);\n        var model = new VerifyEmailModel\n        {\n            Token = WebUtility.UrlEncode(token),\n            UserId = userId,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"Auth.VerifyEmail\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        message.Category = \"VerifyEmail\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendRegistrationVerificationEmailAsync(string email, string token, string? fromMarketing)\n    {\n        var message = CreateDefaultMessage(\"Verify Your Email\", email);\n        var model = new RegisterVerifyEmail\n        {\n            Token = WebUtility.UrlEncode(token),\n            Email = WebUtility.UrlEncode(email),\n            WebVaultUrl = _globalSettings.BaseServiceUri.Vault,\n            SiteName = _globalSettings.SiteName,\n            FromMarketing = WebUtility.UrlEncode(fromMarketing),\n        };\n        await AddMessageContentAsync(message, \"Auth.RegistrationVerifyEmail\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        message.Category = \"VerifyEmail\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendTrialInitiationSignupEmailAsync(\n        bool isExistingUser,\n        string email,\n        string token,\n        ProductTierType productTier,\n        IEnumerable<ProductType> products,\n        int trialLength)\n    {\n        var message = CreateDefaultMessage(\"Verify your email\", email);\n        var model = new TrialInitiationVerifyEmail\n        {\n            IsExistingUser = isExistingUser,\n            Token = WebUtility.UrlEncode(token),\n            Email = WebUtility.UrlEncode(email),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            ProductTier = productTier,\n            Product = products,\n            TrialLength = trialLength\n        };\n        await AddMessageContentAsync(message, \"Billing.TrialInitiationVerifyEmail\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        message.Category = \"VerifyEmail\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token)\n    {\n        var message = CreateDefaultMessage(\"Delete Your Account\", email);\n        var model = new VerifyDeleteModel\n        {\n            Token = WebUtility.UrlEncode(token),\n            UserId = userId,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            Email = email,\n            EmailEncoded = WebUtility.UrlEncode(email)\n        };\n        await AddMessageContentAsync(message, \"Auth.VerifyDelete\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        message.Category = \"VerifyDelete\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendCannotDeleteClaimedAccountEmailAsync(string email)\n    {\n        var message = CreateDefaultMessage(\"Delete Your Account\", email);\n        var model = new CannotDeleteClaimedAccountViewModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n        };\n        await AddMessageContentAsync(message, \"AdminConsole.CannotDeleteClaimedAccount\", model);\n        message.Category = \"CannotDeleteClaimedAccount\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)\n    {\n        var message = CreateDefaultMessage(\"Your Email Change\", toEmail);\n        var model = new ChangeEmailExistsViewModel\n        {\n            FromEmail = fromEmail,\n            ToEmail = toEmail,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"ChangeEmailAlreadyExists\", model);\n        message.Category = \"ChangeEmailAlreadyExists\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendChangeEmailEmailAsync(string newEmailAddress, string token)\n    {\n        var message = CreateDefaultMessage(\"Your Email Change\", newEmailAddress);\n        var model = new UserVerificationEmailTokenViewModel\n        {\n            Token = token,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"ChangeEmail\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        message.Category = \"ChangeEmail\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose)\n    {\n        var message = CreateDefaultMessage(\"Your Bitwarden Verification Code\", email);\n        var requestDateTime = DateTime.UtcNow;\n        var model = new TwoFactorEmailTokenViewModel\n        {\n            Token = token,\n            EmailTotpAction = (purpose == TwoFactorEmailPurpose.Setup) ? \"setting up two-step login\" : \"logging in\",\n            AccountEmail = accountEmail,\n            TheDate = requestDateTime.ToLongDateString(),\n            TheTime = requestDateTime.ToShortTimeString(),\n            TimeZone = _utcTimeZoneDisplay,\n            DeviceIp = deviceIp,\n            DeviceType = deviceType,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            // We only want to remind users to set up 2FA if they're getting a new device verification email.\n            // For login with 2FA, and setup of 2FA, we do not want to show the reminder because users are already doing so.\n            DisplayTwoFactorReminder = purpose == TwoFactorEmailPurpose.NewDeviceVerification\n        };\n        await AddMessageContentAsync(message, \"Auth.TwoFactorEmail\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        message.Category = \"TwoFactorEmail\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendSendEmailOtpEmailAsync(string email, string token, string subject)\n    {\n        var message = CreateDefaultMessage(subject, email);\n        var requestDateTime = DateTime.UtcNow;\n        var model = new DefaultEmailOtpViewModel\n        {\n            Token = token,\n            Expiry = \"5\", // This should be configured through the OTPDefaultTokenProviderOptions but for now we will hardcode it to 5 minutes.\n            TheDate = requestDateTime.ToLongDateString(),\n            TheTime = requestDateTime.ToShortTimeString(),\n            TimeZone = _utcTimeZoneDisplay,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n        };\n        await AddMessageContentAsync(message, \"Auth.SendAccessEmailOtpEmail\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        // TODO - PM-25380 change to string constant\n        message.Category = \"SendEmailOtp\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)\n    {\n        // Check if we've sent this email within the last hour\n        var cacheKey = string.Format(FailedTwoFactorAttemptCacheKeyFormat, email);\n        var cachedValue = await _distributedCache.GetAsync(cacheKey);\n\n        if (cachedValue != null)\n        {\n            // Email was already sent within the last hour, skip sending\n            return;\n        }\n\n        var message = CreateDefaultMessage(\"Failed two-step login attempt detected\", email);\n        var model = new FailedAuthAttemptModel()\n        {\n            TheDate = utcNow.ToLongDateString(),\n            TheTime = utcNow.ToShortTimeString(),\n            TimeZone = _utcTimeZoneDisplay,\n            IpAddress = ip,\n            AffectedEmail = email,\n            TwoFactorType = failedType,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash\n\n        };\n        await AddMessageContentAsync(message, \"Auth.FailedTwoFactorAttempt\", model);\n        message.Category = \"FailedTwoFactorAttempt\";\n        await _mailDeliveryService.SendEmailAsync(message);\n\n        // Set cache entry with 1 hour expiration to prevent sending again\n        var cacheOptions = new DistributedCacheEntryOptions\n        {\n            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)\n        };\n        await _distributedCache.SetAsync(cacheKey, [1], cacheOptions);\n    }\n\n    public async Task SendMasterPasswordHintEmailAsync(string email, string hint)\n    {\n        var message = CreateDefaultMessage(\"Your Master Password Hint\", email);\n        var model = new MasterPasswordHintViewModel\n        {\n            Hint = CoreHelpers.SanitizeForEmail(hint, false),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"Auth.MasterPasswordHint\", model);\n        message.Category = \"MasterPasswordHint\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendNoMasterPasswordHintEmailAsync(string email)\n    {\n        var message = CreateDefaultMessage(\"Your Master Password Hint\", email);\n        var model = new BaseMailModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"Auth.NoMasterPasswordHint\", model);\n        message.Category = \"NoMasterPasswordHint\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails)\n    {\n        Debug.Assert(organization.Seats.HasValue, \"Organization is expected to have a non-null value for seats at the time of sending this email\");\n        var message = CreateDefaultMessage($\"{organization.DisplayName()} Seat Count Has Increased\", ownerEmails);\n        var model = new OrganizationSeatsAutoscaledViewModel\n        {\n            InitialSeatCount = initialSeatCount,\n            CurrentSeatCount = organization.Seats.Value,\n            VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)\n        };\n\n        await AddMessageContentAsync(message, \"OrganizationSeatsAutoscaled\", model);\n        message.Category = \"OrganizationSeatsAutoscaled\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails)\n    {\n        var message = CreateDefaultMessage($\"{organization.DisplayName()} Seat Limit Reached\", ownerEmails);\n        var model = new OrganizationSeatsMaxReachedViewModel\n        {\n            MaxSeatCount = maxSeatCount,\n            VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)\n        };\n\n        await AddMessageContentAsync(message, \"OrganizationSeatsMaxReached\", model);\n        message.Category = \"OrganizationSeatsMaxReached\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier,\n        IEnumerable<string> adminEmails, bool hasAccessSecretsManager = false)\n    {\n        var message = CreateDefaultMessage($\"Action Required: {userIdentifier} Needs to Be Confirmed\", adminEmails);\n        var model = new OrganizationUserAcceptedViewModel\n        {\n            OrganizationId = organization.Id,\n            OrganizationName = CoreHelpers.SanitizeForEmail(organization.DisplayName(), false),\n            UserIdentifier = userIdentifier,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"OrganizationUserAccepted\", model);\n        message.Category = \"OrganizationUserAccepted\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false)\n    {\n        var message = CreateDefaultMessage($\"You Have Been Confirmed To {organizationName}\", email);\n        var model = new OrganizationUserConfirmedViewModel\n        {\n            TitleFirst = \"You're confirmed as a member of \",\n            TitleSecondBold = CoreHelpers.SanitizeForEmail(organizationName, false),\n            TitleThird = \"!\",\n            OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),\n            WebVaultUrl = hasAccessSecretsManager\n                ? _globalSettings.BaseServiceUri.VaultWithHashAndSecretManagerProduct\n                : _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"OrganizationUserConfirmed\", model);\n        message.Category = \"OrganizationUserConfirmed\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo)\n    {\n        var messageModels = orgInvitesInfo.OrgUserTokenPairs.Select(orgUserTokenPair =>\n        {\n            Debug.Assert(orgUserTokenPair.OrgUser.Email is not null);\n\n            var orgUserInviteViewModel = OrganizationUserInvitedViewModel.CreateFromInviteInfo(orgInvitesInfo, orgUserTokenPair.OrgUser,\n                orgUserTokenPair.Token, _globalSettings);\n\n            return CreateMessage(orgUserTokenPair.OrgUser.Email, orgUserInviteViewModel);\n        });\n\n        await EnqueueMailAsync(messageModels);\n        return;\n\n        MailQueueMessage CreateMessage(string email, OrganizationUserInvitedViewModel model)\n        {\n            ArgumentNullException.ThrowIfNull(model);\n\n            var subject = model! switch\n            {\n                { IsFreeOrg: true, OrgUserHasExistingUser: true } => \"You have been invited to a Bitwarden Organization\",\n                { IsFreeOrg: true, OrgUserHasExistingUser: false } => \"You have been invited to Bitwarden Password Manager\",\n                { IsFreeOrg: false, OrgUserHasExistingUser: true } => $\"{model.OrganizationName} invited you to their Bitwarden organization\",\n                { IsFreeOrg: false, OrgUserHasExistingUser: false } => $\"{model.OrganizationName} set up a Bitwarden account for you\"\n            };\n\n            var message = CreateDefaultMessage(subject, email);\n\n            return new MailQueueMessage(message, \"OrganizationUserInvited\", model);\n        }\n    }\n\n    public async Task SendUpdatedOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo)\n    {\n        var messageModels = orgInvitesInfo.OrgUserTokenPairs.Select(orgUserTokenPair =>\n        {\n            Debug.Assert(orgUserTokenPair.OrgUser.Email is not null);\n\n            var userHasExistingUser = orgInvitesInfo.OrgUserHasExistingUserDict[orgUserTokenPair.OrgUser.Id];\n            var organizationName = orgInvitesInfo.OrganizationName;\n\n            var (subject, templateName, buttonText) = GetUpdatedInviteTemplateInfo(\n                orgInvitesInfo.PlanType, userHasExistingUser, organizationName);\n\n            var url = BuildInvitationUrl(orgInvitesInfo, orgUserTokenPair.OrgUser, orgUserTokenPair.Token);\n            var expirationDate = $\"{orgUserTokenPair.Token.ExpirationDate.ToLongDateString()} {orgUserTokenPair.Token.ExpirationDate.ToShortTimeString()} UTC\";\n\n            var message = CreateDefaultMessage(subject, orgUserTokenPair.OrgUser.Email);\n\n            return new MailQueueMessage(message, templateName, new\n            {\n                OrganizationName = organizationName,\n                Email = orgUserTokenPair.OrgUser.Email,\n                ExpirationDate = expirationDate,\n                Url = url,\n                ButtonText = buttonText,\n                InviterEmail = orgInvitesInfo.InviterEmail,\n                CurrentYear = DateTime.UtcNow.Year.ToString()\n            });\n        });\n\n        await EnqueueMailAsync(messageModels);\n    }\n\n    public async Task SendUpdatedOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager = false)\n    {\n        var organizationName = organization.DisplayName();\n        var webVaultUrl = accessSecretsManager\n            ? _globalSettings.BaseServiceUri.VaultWithHashAndSecretManagerProduct\n            : _globalSettings.BaseServiceUri.VaultWithHash;\n\n        var templateName = IsEnterpriseOrTeamsPlan(organization.PlanType)\n            ? \"AdminConsole.OrganizationConfirmation.OrganizationConfirmationEnterpriseTeamsView\"\n            : \"AdminConsole.OrganizationConfirmation.OrganizationConfirmationFamilyFreeView\";\n\n        var message = CreateDefaultMessage($\"You can now access items from {organizationName}\", userEmail);\n\n        var queueMessage = new MailQueueMessage(message, templateName, new\n        {\n            OrganizationName = organizationName,\n            TitleFirst = \"You're confirmed as a member of \",\n            TitleSecondBold = organizationName,\n            TitleThird = \"!\",\n            WebVaultUrl = webVaultUrl,\n            CurrentYear = DateTime.UtcNow.Year.ToString()\n        });\n        queueMessage.Category = \"OrganizationUserConfirmed\";\n\n        await EnqueueMailAsync(queueMessage);\n    }\n\n    public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email)\n    {\n        var message = CreateDefaultMessage($\"You have been revoked from {organizationName}\", email);\n        var model = new OrganizationUserRevokedForPolicyTwoFactorViewModel\n        {\n            OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"AdminConsole.OrganizationUserRevokedForTwoFactorPolicy\", model);\n        message.Category = \"OrganizationUserRevokedForTwoFactorPolicy\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    // TODO: DO NOT move to IMailer implementation: PM-27852\n    [Obsolete(\"Use SendIndividualUserWelcomeEmailAsync instead\")]\n    public async Task SendWelcomeEmailAsync(User user)\n    {\n        var message = CreateDefaultMessage(\"Welcome to Bitwarden!\", user.Email);\n        var model = new BaseMailModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"Welcome\", model);\n        message.Category = \"Welcome\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    // TODO: Move to IMailer implementation: PM-27852\n    public async Task SendIndividualUserWelcomeEmailAsync(User user)\n    {\n        var message = CreateDefaultMessage(\"Welcome to Bitwarden!\", user.Email);\n        var model = new BaseMailModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"MJML.Auth.Onboarding.welcome-individual-user\", model);\n        message.Category = \"Welcome\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    // TODO: Move to IMailer implementation: PM-27852\n    public async Task SendOrganizationUserWelcomeEmailAsync(User user, string organizationName)\n    {\n        var message = CreateDefaultMessage(\"Welcome to Bitwarden!\", user.Email);\n        var model = new OrganizationWelcomeEmailViewModel\n        {\n            OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"MJML.Auth.Onboarding.welcome-org-user\", model);\n        message.Category = \"Welcome\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    // TODO: Move to IMailer implementation: PM-27852\n    public async Task SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(User user, string familyOrganizationName)\n    {\n        var message = CreateDefaultMessage(\"Welcome to Bitwarden!\", user.Email);\n        var model = new OrganizationWelcomeEmailViewModel\n        {\n            OrganizationName = CoreHelpers.SanitizeForEmail(familyOrganizationName, false),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"MJML.Auth.Onboarding.welcome-family-user\", model);\n        message.Category = \"Welcome\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendTrialInitiationEmailAsync(string userEmail)\n    {\n        var message = CreateDefaultMessage(\"Welcome to Bitwarden; 3 steps to get started!\", userEmail);\n        var model = new BaseMailModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHashAndSecretManagerProduct,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"TrialInitiation\", model);\n        message.Category = \"Welcome\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token)\n    {\n        var message = CreateDefaultMessage(\"Request to Delete Your Provider\", email);\n        var model = new ProviderInitiateDeleteModel\n        {\n            Token = WebUtility.UrlEncode(token),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            ProviderId = provider.Id,\n            ProviderName = CoreHelpers.SanitizeForEmail(provider.DisplayName()!, false),\n            ProviderNameUrlEncoded = WebUtility.UrlEncode(provider.Name),\n            ProviderBillingEmail = provider.BillingEmail,\n            ProviderCreationDate = provider.CreationDate.ToLongDateString(),\n            ProviderCreationTime = provider.CreationDate.ToShortTimeString(),\n            TimeZone = _utcTimeZoneDisplay,\n        };\n        await AddMessageContentAsync(message, \"Provider.InitiateDeleteProvider\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        message.Category = \"InitiateDeleteProvider\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)\n    {\n        var message = CreateDefaultMessage(\"[Admin] Continue Logging In\", email);\n        var url = CoreHelpers.ExtendQuery(new Uri($\"{_globalSettings.BaseServiceUri.Admin}/login/confirm\"),\n            new Dictionary<string, string>\n            {\n                [\"returnUrl\"] = returnUrl,\n                [\"email\"] = email,\n                [\"token\"] = token,\n            });\n        var model = new PasswordlessSignInModel\n        {\n            Url = url.OriginalString\n        };\n        await AddMessageContentAsync(message, \"Auth.PasswordlessSignIn\", model);\n        message.Category = \"PasswordlessSignIn\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendInvoiceUpcoming(\n        string email,\n        decimal amount,\n        DateTime dueDate,\n        List<string> items,\n        bool mentionInvoices) => await SendInvoiceUpcoming(new List<string> { email }, amount, dueDate, items, mentionInvoices);\n\n    public async Task SendInvoiceUpcoming(\n        IEnumerable<string> emails,\n        decimal amount,\n        DateTime dueDate,\n        List<string> items,\n        bool mentionInvoices)\n    {\n        var message = CreateDefaultMessage(\"Your Subscription Will Renew Soon\", emails);\n        var model = new InvoiceUpcomingViewModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            AmountDue = amount,\n            DueDate = dueDate,\n            Items = items,\n            MentionInvoices = mentionInvoices\n        };\n        await AddMessageContentAsync(message, \"InvoiceUpcoming\", model);\n        message.Category = \"InvoiceUpcoming\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendProviderInvoiceUpcoming(\n        IEnumerable<string> emails,\n        decimal amount,\n        DateTime dueDate,\n        List<string> items,\n        string? collectionMethod = null,\n        bool hasPaymentMethod = true,\n        string? paymentMethodDescription = null)\n    {\n        var message = CreateDefaultMessage(\"Your upcoming Bitwarden invoice\", emails);\n        var model = new InvoiceUpcomingViewModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            AmountDue = amount,\n            DueDate = dueDate,\n            Items = items,\n            MentionInvoices = false,\n            CollectionMethod = collectionMethod,\n            HasPaymentMethod = hasPaymentMethod,\n            PaymentMethodDescription = paymentMethodDescription\n        };\n        await AddMessageContentAsync(message, \"ProviderInvoiceUpcoming\", model);\n        message.Category = \"ProviderInvoiceUpcoming\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendPaymentFailedAsync(string email, decimal amount, bool mentionInvoices)\n    {\n        var message = CreateDefaultMessage(\"Payment Failed\", email);\n        var model = new PaymentFailedViewModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            Amount = amount,\n            MentionInvoices = mentionInvoices\n        };\n        await AddMessageContentAsync(message, \"PaymentFailed\", model);\n        message.Category = \"PaymentFailed\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendAddedCreditAsync(string email, decimal amount)\n    {\n        var message = CreateDefaultMessage(\"Account Credit Payment Processed\", email);\n        var model = new AddedCreditViewModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            Amount = amount\n        };\n        await AddMessageContentAsync(message, \"AddedCredit\", model);\n        message.Category = \"AddedCredit\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendLicenseExpiredAsync(IEnumerable<string> emails, string? organizationName = null)\n    {\n        var message = CreateDefaultMessage(\"License Expired\", emails);\n        var model = new LicenseExpiredViewModel();\n        if (organizationName != null)\n        {\n            model.OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false);\n        }\n        await AddMessageContentAsync(message, \"LicenseExpired\", model);\n        message.Category = \"LicenseExpired\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendRequestSMAccessToAdminEmailAsync(IEnumerable<string> emails, string organizationName, string requestingUserName, string emailContent)\n    {\n        var message = CreateDefaultMessage(\"Access Requested for Secrets Manager\", emails);\n        var model = new RequestSecretsManagerAccessViewModel\n        {\n            OrgName = CoreHelpers.SanitizeForEmail(organizationName, false),\n            UserNameRequestingAccess = CoreHelpers.SanitizeForEmail(requestingUserName, false),\n            EmailContent = CoreHelpers.SanitizeForEmail(emailContent, false),\n        };\n        await AddMessageContentAsync(message, \"SecretsManagerAccessRequest\", model);\n        message.Category = \"SecretsManagerAccessRequest\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendClaimedDomainUserEmailAsync(ClaimedUserDomainClaimedEmails emailList)\n    {\n        await EnqueueMailAsync(emailList.EmailList.Select(email =>\n            CreateMessage(email, emailList.Organization, emailList.DomainName)));\n        return;\n\n        MailQueueMessage CreateMessage(string emailAddress, Organization org, string domainName) =>\n            new(CreateDefaultMessage($\"Important update to your Bitwarden account\", emailAddress),\n                \"AdminConsole.DomainClaimedByOrganization\",\n                new ClaimedDomainUserNotificationViewModel\n                {\n                    TitleFirst = $\"Important update to your<br>Bitwarden account\",\n                    OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false),\n                    DomainName = domainName,\n                    EmailDomain = emailAddress.Split('@').LastOrDefault() ?? \"\",\n                    UserEmail = emailAddress\n                });\n    }\n\n    public async Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip)\n    {\n        var message = CreateDefaultMessage($\"New Device Logged In From {deviceType}\", email);\n        var model = new NewDeviceLoggedInModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            DeviceType = deviceType,\n            TheDate = timestamp.ToLongDateString(),\n            TheTime = timestamp.ToString(\"hh:mm:ss tt\"),\n            TimeZone = _utcTimeZoneDisplay,\n            IpAddress = ip\n        };\n        await AddMessageContentAsync(message, \"NewDeviceLoggedIn\", model);\n        message.Category = \"NewDeviceLoggedIn\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip)\n    {\n        var message = CreateDefaultMessage($\"Recover 2FA From {ip}\", email);\n        var model = new RecoverTwoFactorModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            TheDate = timestamp.ToLongDateString(),\n            TheTime = timestamp.ToShortTimeString(),\n            TimeZone = _utcTimeZoneDisplay,\n            IpAddress = ip\n        };\n        await AddMessageContentAsync(message, \"Auth.RecoverTwoFactor\", model);\n        message.Category = \"RecoverTwoFactor\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email)\n    {\n        var message = CreateDefaultMessage($\"You have been revoked from {organizationName}\", email);\n        var model = new OrganizationUserRevokedForPolicySingleOrgViewModel\n        {\n            OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"AdminConsole.OrganizationUserRevokedForSingleOrgPolicy\", model);\n        message.Category = \"OrganizationUserRevokedForSingleOrgPolicy\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage)\n    {\n        var message = CreateDefaultMessage(queueMessage.Subject, queueMessage.ToEmails);\n        message.BccEmails = queueMessage.BccEmails;\n        message.Category = queueMessage.Category;\n        await AddMessageContentAsync(message, queueMessage.TemplateName, queueMessage.Model);\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendAdminResetPasswordEmailAsync(string email, string? userName, string orgName)\n    {\n        var message = CreateDefaultMessage(\"Your admin has initiated account recovery\", email);\n        var model = new AdminResetPasswordViewModel()\n        {\n            UserName = GetUserIdentifier(email, userName),\n            OrgName = CoreHelpers.SanitizeForEmail(orgName, false),\n        };\n        await AddMessageContentAsync(message, \"AdminResetPassword\", model);\n        message.Category = \"AdminResetPassword\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    private Task EnqueueMailAsync(IMailQueueMessage queueMessage) =>\n        _mailEnqueuingService.EnqueueAsync(queueMessage, SendEnqueuedMailMessageAsync);\n\n    private Task EnqueueMailAsync(IEnumerable<IMailQueueMessage> queueMessages) =>\n        _mailEnqueuingService.EnqueueManyAsync(queueMessages, SendEnqueuedMailMessageAsync);\n\n    private static (string Subject, string TemplateName, string ButtonText) GetUpdatedInviteTemplateInfo(\n        PlanType planType, bool userHasExistingUser, string organizationName)\n    {\n        const string newUserSubject = \"set up a Bitwarden account for you\";\n        const string newUserButton = \"Finish account setup\";\n        const string existingUserSubject = \"invited you to their Bitwarden organization\";\n        const string existingUserButton = \"Accept invitation\";\n\n        if (IsEnterpriseOrTeamsPlan(planType))\n        {\n            return userHasExistingUser\n                ? ($\"{organizationName} {existingUserSubject}\",\n                    \"AdminConsole.OrganizationInvite.OrganizationInviteEnterpriseTeamsExistingUserView\",\n                    existingUserButton)\n                : ($\"{organizationName} {newUserSubject}\",\n                    \"AdminConsole.OrganizationInvite.OrganizationInviteEnterpriseTeamsNewUserView\",\n                    newUserButton);\n        }\n\n        if (IsFamiliesPlan(planType))\n        {\n            return userHasExistingUser\n                ? ($\"{organizationName} {existingUserSubject}\",\n                    \"AdminConsole.OrganizationInvite.OrganizationInviteFamiliesExistingUserView\",\n                    existingUserButton)\n                : ($\"{organizationName} {newUserSubject}\",\n                    \"AdminConsole.OrganizationInvite.OrganizationInviteFamiliesNewUserView\",\n                    newUserButton);\n        }\n\n        return (userHasExistingUser\n                ? \"You have been invited to a Bitwarden Organization\"\n                : \"You have been invited to Bitwarden Password Manager\",\n            \"AdminConsole.OrganizationInvite.OrganizationInviteFreeView\",\n            existingUserButton);\n    }\n\n    private static bool IsEnterpriseOrTeamsPlan(PlanType planType)\n    {\n        return planType switch\n        {\n            PlanType.TeamsMonthly2019 or\n            PlanType.TeamsAnnually2019 or\n            PlanType.TeamsMonthly2020 or\n            PlanType.TeamsAnnually2020 or\n            PlanType.TeamsMonthly2023 or\n            PlanType.TeamsAnnually2023 or\n            PlanType.TeamsStarter2023 or\n            PlanType.TeamsMonthly or\n            PlanType.TeamsAnnually or\n            PlanType.TeamsStarter or\n            PlanType.EnterpriseMonthly2019 or\n            PlanType.EnterpriseAnnually2019 or\n            PlanType.EnterpriseMonthly2020 or\n            PlanType.EnterpriseAnnually2020 or\n            PlanType.EnterpriseMonthly2023 or\n            PlanType.EnterpriseAnnually2023 or\n            PlanType.EnterpriseMonthly or\n            PlanType.EnterpriseAnnually or\n            PlanType.Custom => true,\n            _ => false\n        };\n    }\n\n    private static bool IsFamiliesPlan(PlanType planType)\n    {\n        return planType switch\n        {\n            PlanType.FamiliesAnnually2019 or\n            PlanType.FamiliesAnnually2025 or\n            PlanType.FamiliesAnnually => true,\n            _ => false\n        };\n    }\n\n    private string BuildInvitationUrl(OrganizationInvitesInfo orgInvitesInfo, OrganizationUser orgUser, ExpiringToken token)\n    {\n        var baseUrl = $\"{_globalSettings.BaseServiceUri.VaultWithHash}/accept-organization\";\n        var queryParams = new List<string>\n        {\n            $\"organizationId={orgUser.OrganizationId}\",\n            $\"organizationUserId={orgUser.Id}\",\n            $\"email={WebUtility.UrlEncode(orgUser.Email)}\",\n            $\"organizationName={WebUtility.UrlEncode(orgInvitesInfo.OrganizationName)}\",\n            $\"token={WebUtility.UrlEncode(token.Token)}\",\n            $\"initOrganization={orgInvitesInfo.InitOrganization}\",\n            $\"orgUserHasExistingUser={orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id]}\"\n        };\n\n        if (orgInvitesInfo.OrgSsoEnabled && orgInvitesInfo.OrgSsoLoginRequiredPolicyEnabled)\n        {\n            queryParams.Add($\"orgSsoIdentifier={orgInvitesInfo.OrgSsoIdentifier}\");\n        }\n\n        return $\"{baseUrl}?{string.Join(\"&\", queryParams)}\";\n    }\n\n    private MailMessage CreateDefaultMessage(string subject, string toEmail)\n    {\n        return CreateDefaultMessage(subject, new List<string> { toEmail });\n    }\n\n    private static MailMessage CreateDefaultMessage(string subject, IEnumerable<string> toEmails)\n    {\n        return new MailMessage\n        {\n            ToEmails = toEmails,\n            Subject = subject,\n            MetaData = new Dictionary<string, object>()\n        };\n    }\n\n    private async Task AddMessageContentAsync<T>(MailMessage message, string templateName, T model)\n        where T : notnull\n    {\n        message.HtmlContent = await RenderAsync($\"{templateName}.html\", model);\n        message.TextContent = await RenderAsync($\"{templateName}.text\", model);\n    }\n\n    private async Task<string?> RenderAsync<T>(string templateName, T model)\n        where T : notnull\n    {\n        await RegisterHelpersAndPartialsAsync();\n        if (!_templateCache.TryGetValue(templateName, out var template))\n        {\n            var source = await ReadSourceAsync(templateName);\n            if (source != null)\n            {\n                template = Handlebars.Compile(source);\n                _templateCache.Add(templateName, template);\n            }\n        }\n        return template != null ? template(model) : null;\n    }\n\n    private async Task<string?> ReadSourceAsync(string templateName)\n    {\n        var diskSource = await ReadSourceFromDiskAsync(templateName);\n        if (!string.IsNullOrWhiteSpace(diskSource))\n        {\n            return diskSource;\n        }\n\n        var assembly = typeof(HandlebarsMailService).GetTypeInfo().Assembly;\n        var fullTemplateName = $\"{Namespace}.{templateName}.hbs\";\n        if (!assembly.GetManifestResourceNames().Any(f => f == fullTemplateName))\n        {\n            return null;\n        }\n        using (var s = assembly.GetManifestResourceStream(fullTemplateName)!)\n        using (var sr = new StreamReader(s))\n        {\n            return await sr.ReadToEndAsync();\n        }\n    }\n\n    private async Task<string?> ReadSourceFromDiskAsync(string templateName)\n    {\n        if (!_globalSettings.SelfHosted)\n        {\n            return null;\n        }\n        try\n        {\n            var templateFileSuffix = \".html\";\n            if (templateName.EndsWith(\".txt\"))\n            {\n                templateFileSuffix = \".txt\";\n            }\n            else if (!templateName.EndsWith(\".html\"))\n            {\n                // unexpected suffix\n                return null;\n            }\n            var suffixPosition = templateName.LastIndexOf(templateFileSuffix);\n            var templateNameNoSuffix = templateName.Substring(0, suffixPosition);\n            var templatePathNoSuffix = templateNameNoSuffix.Replace(\".\", \"/\");\n            var diskPath = $\"{_globalSettings.MailTemplateDirectory}/{templatePathNoSuffix}{templateFileSuffix}.hbs\";\n            var directory = Path.GetDirectoryName(diskPath);\n            if (Directory.Exists(directory) && File.Exists(diskPath))\n            {\n                var fileContents = await File.ReadAllTextAsync(diskPath);\n                return fileContents;\n            }\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(e, \"Failed to read mail template from disk.\");\n        }\n        return null;\n    }\n\n    private async Task RegisterHelpersAndPartialsAsync()\n    {\n        if (_registeredHelpersAndPartials)\n        {\n            return;\n        }\n        _registeredHelpersAndPartials = true;\n\n        var basicHtmlLayoutSource = await ReadSourceAsync(\"Layouts.Basic.html\");\n        Handlebars.RegisterTemplate(\"BasicHtmlLayout\", basicHtmlLayoutSource);\n        var basicTextLayoutSource = await ReadSourceAsync(\"Layouts.Basic.text\");\n        Handlebars.RegisterTemplate(\"BasicTextLayout\", basicTextLayoutSource);\n        var fullHtmlLayoutSource = await ReadSourceAsync(\"Layouts.Full.html\");\n        Handlebars.RegisterTemplate(\"FullHtmlLayout\", fullHtmlLayoutSource);\n        var fullTextLayoutSource = await ReadSourceAsync(\"Layouts.Full.text\");\n        Handlebars.RegisterTemplate(\"FullTextLayout\", fullTextLayoutSource);\n        var fullUpdatedHtmlLayoutSource = await ReadSourceAsync(\"Layouts.FullUpdated.html\");\n        Handlebars.RegisterTemplate(\"FullUpdatedHtmlLayout\", fullUpdatedHtmlLayoutSource);\n        var fullUpdatedTextLayoutSource = await ReadSourceAsync(\"Layouts.FullUpdated.text\");\n        Handlebars.RegisterTemplate(\"FullUpdatedTextLayout\", fullUpdatedTextLayoutSource);\n        var titleContactUsHtmlLayoutSource = await ReadSourceAsync(\"Layouts.TitleContactUs.html\");\n        Handlebars.RegisterTemplate(\"TitleContactUsHtmlLayout\", titleContactUsHtmlLayoutSource);\n        var titleContactUsTextLayoutSource = await ReadSourceAsync(\"Layouts.TitleContactUs.text\");\n        Handlebars.RegisterTemplate(\"TitleContactUsTextLayout\", titleContactUsTextLayoutSource);\n        var securityTasksHtmlLayoutSource = await ReadSourceAsync(\"Layouts.SecurityTasks.html\");\n        Handlebars.RegisterTemplate(\"SecurityTasksHtmlLayout\", securityTasksHtmlLayoutSource);\n        var securityTasksTextLayoutSource = await ReadSourceAsync(\"Layouts.SecurityTasks.text\");\n        Handlebars.RegisterTemplate(\"SecurityTasksTextLayout\", securityTasksTextLayoutSource);\n        var providerFullHtmlLayoutSource = await ReadSourceAsync(\"Layouts.ProviderFull.html\");\n        Handlebars.RegisterTemplate(\"ProviderFull\", providerFullHtmlLayoutSource);\n\n        Handlebars.RegisterHelper(\"date\", (writer, context, parameters) =>\n        {\n            if (parameters.Length == 0 || !(parameters[0] is DateTime))\n            {\n                writer.WriteSafeString(string.Empty);\n                return;\n            }\n            if (parameters.Length > 0 && parameters[1] is string)\n            {\n                writer.WriteSafeString(((DateTime)parameters[0]).ToString(parameters[1].ToString()));\n            }\n            else\n            {\n                writer.WriteSafeString(((DateTime)parameters[0]).ToString());\n            }\n        });\n\n        Handlebars.RegisterHelper(\"usd\", (writer, context, parameters) =>\n        {\n            if (parameters.Length == 0 || !(parameters[0] is decimal))\n            {\n                writer.WriteSafeString(string.Empty);\n                return;\n            }\n            writer.WriteSafeString(((decimal)parameters[0]).ToString(\"C\"));\n        });\n\n        Handlebars.RegisterHelper(\"link\", (writer, context, parameters) =>\n        {\n            if (parameters.Length == 0)\n            {\n                writer.WriteSafeString(string.Empty);\n                return;\n            }\n\n            var text = parameters[0].ToString();\n            var href = text;\n            var clickTrackingOff = false;\n            if (parameters.Length == 2)\n            {\n                if (parameters[1] is string)\n                {\n                    var p1 = parameters[1].ToString();\n                    if (p1 == \"true\" || p1 == \"false\")\n                    {\n                        clickTrackingOff = p1 == \"true\";\n                    }\n                    else\n                    {\n                        href = p1;\n                    }\n                }\n                else if (parameters[1] is bool)\n                {\n                    clickTrackingOff = (bool)parameters[1];\n                }\n            }\n            else if (parameters.Length > 2)\n            {\n                if (parameters[1] is string)\n                {\n                    href = parameters[1].ToString();\n                }\n                if (parameters[2] is string)\n                {\n                    var p2 = parameters[2].ToString();\n                    if (p2 == \"true\" || p2 == \"false\")\n                    {\n                        clickTrackingOff = p2 == \"true\";\n                    }\n                }\n                else if (parameters[2] is bool)\n                {\n                    clickTrackingOff = (bool)parameters[2];\n                }\n            }\n\n            var clickTrackingText = (clickTrackingOff ? \"clicktracking=off\" : string.Empty);\n            writer.WriteSafeString($\"<a href=\\\"{href}\\\" target=\\\"_blank\\\" {clickTrackingText}>{text}</a>\");\n        });\n\n        // Construct markup for admin and owner email addresses.\n        // Using conditionals within the handlebar syntax was including extra spaces around\n        // concatenated strings, which this helper avoids.\n        Handlebars.RegisterHelper(\"formatAdminOwnerEmails\", (writer, context, parameters) =>\n        {\n            if (parameters.Length == 0)\n            {\n                writer.WriteSafeString(string.Empty);\n                return;\n            }\n\n            var emailList = new List<string>();\n            if (parameters[0] is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Array)\n            {\n                emailList = jsonElement.EnumerateArray().Select(e => e.GetString()!).ToList();\n            }\n            else if (parameters[0] is IEnumerable<string> emails)\n            {\n                emailList = emails.ToList();\n            }\n            else\n            {\n                writer.WriteSafeString(string.Empty);\n                return;\n            }\n\n            if (emailList.Count == 0)\n            {\n                writer.WriteSafeString(string.Empty);\n                return;\n            }\n\n            string constructAnchorElement(string email)\n            {\n                return $\"<a style=\\\"color: #175DDC\\\" href=\\\"mailto:{email}\\\">{email}</a>\";\n            }\n\n            var outputMessage = \"This request was initiated by \";\n\n            if (emailList.Count == 1)\n            {\n                outputMessage += $\"{constructAnchorElement(emailList[0])}.\";\n            }\n            else\n            {\n                outputMessage += string.Join(\", \", emailList.Take(emailList.Count - 1)\n                    .Select(email => constructAnchorElement(email)));\n                outputMessage += $\" and {constructAnchorElement(emailList.Last())}.\";\n            }\n\n            writer.WriteSafeString($\"{outputMessage}\");\n        });\n\n        // Returns the singular or plural form of a word based on the provided numeric value.\n        Handlebars.RegisterHelper(\"plurality\", (writer, context, parameters) =>\n        {\n            if (parameters.Length != 3)\n            {\n                writer.WriteSafeString(string.Empty);\n                return;\n            }\n\n            if (int.TryParse(parameters[0].ToString(), out var number))\n            {\n                var singularText = parameters[1].ToString();\n                var pluralText = parameters[2].ToString();\n                writer.WriteSafeString(number == 1 ? singularText : pluralText);\n            }\n            else\n            {\n                writer.WriteSafeString(string.Empty);\n            }\n        });\n\n        // Equality comparison helper for conditional templates.\n        Handlebars.RegisterHelper(\"eq\", (context, arguments) =>\n        {\n            if (arguments.Length != 2)\n            {\n                return false;\n            }\n\n            var value1 = arguments[0]?.ToString();\n            var value2 = arguments[1]?.ToString();\n            return string.Equals(value1, value2, StringComparison.OrdinalIgnoreCase);\n        });\n    }\n\n    public async Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)\n    {\n        if (string.IsNullOrEmpty(emergencyAccess.Email))\n        {\n            throw new BadRequestException(\"Emergency Access not valid.\");\n        }\n\n        var message = CreateDefaultMessage($\"Emergency Access Contact Invite\", emergencyAccess.Email);\n        var model = new EmergencyAccessInvitedViewModel\n        {\n            Name = CoreHelpers.SanitizeForEmail(name),\n            Email = WebUtility.UrlEncode(emergencyAccess.Email),\n            Id = emergencyAccess.Id.ToString(),\n            Token = WebUtility.UrlEncode(token),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"Auth.EmergencyAccessInvited\", model);\n        message.Category = \"EmergencyAccessInvited\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email)\n    {\n        var message = CreateDefaultMessage($\"Accepted Emergency Access\", email);\n        var model = new EmergencyAccessAcceptedViewModel\n        {\n            GranteeEmail = granteeEmail,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"Auth.EmergencyAccessAccepted\", model);\n        message.Category = \"EmergencyAccessAccepted\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email)\n    {\n        var message = CreateDefaultMessage($\"You Have Been Confirmed as Emergency Access Contact\", email);\n        var model = new EmergencyAccessConfirmedViewModel\n        {\n            Name = CoreHelpers.SanitizeForEmail(grantorName),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"Auth.EmergencyAccessConfirmed\", model);\n        message.Category = \"EmergencyAccessConfirmed\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendEmergencyAccessRecoveryInitiated(EmergencyAccess emergencyAccess, string initiatingName, string email)\n    {\n        var message = CreateDefaultMessage(\"Emergency Access Initiated\", email);\n\n        var remainingTime = DateTime.UtcNow - emergencyAccess.RecoveryInitiatedDate.GetValueOrDefault();\n\n        var model = new EmergencyAccessRecoveryViewModel\n        {\n            Name = CoreHelpers.SanitizeForEmail(initiatingName),\n            Action = emergencyAccess.Type.ToString(),\n            DaysLeft = emergencyAccess.WaitTimeDays - Convert.ToInt32((remainingTime).TotalDays),\n        };\n        await AddMessageContentAsync(message, \"Auth.EmergencyAccessRecovery\", model);\n        message.Category = \"EmergencyAccessRecovery\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendEmergencyAccessRecoveryApproved(EmergencyAccess emergencyAccess, string approvingName, string email)\n    {\n        var message = CreateDefaultMessage(\"Emergency Access Approved\", email);\n        var model = new EmergencyAccessApprovedViewModel\n        {\n            Name = CoreHelpers.SanitizeForEmail(approvingName),\n        };\n        await AddMessageContentAsync(message, \"Auth.EmergencyAccessApproved\", model);\n        message.Category = \"EmergencyAccessApproved\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendEmergencyAccessRecoveryRejected(EmergencyAccess emergencyAccess, string rejectingName, string email)\n    {\n        var message = CreateDefaultMessage(\"Emergency Access Rejected\", email);\n        var model = new EmergencyAccessRejectedViewModel\n        {\n            Name = CoreHelpers.SanitizeForEmail(rejectingName),\n        };\n        await AddMessageContentAsync(message, \"Auth.EmergencyAccessRejected\", model);\n        message.Category = \"EmergencyAccessRejected\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendEmergencyAccessRecoveryReminder(EmergencyAccess emergencyAccess, string initiatingName, string email)\n    {\n        var message = CreateDefaultMessage(\"Pending Emergency Access Request\", email);\n\n        var remainingTime = DateTime.UtcNow - emergencyAccess.RecoveryInitiatedDate.GetValueOrDefault();\n\n        var model = new EmergencyAccessRecoveryViewModel\n        {\n            Name = CoreHelpers.SanitizeForEmail(initiatingName),\n            Action = emergencyAccess.Type.ToString(),\n            DaysLeft = emergencyAccess.WaitTimeDays - Convert.ToInt32((remainingTime).TotalDays),\n        };\n        await AddMessageContentAsync(message, \"Auth.EmergencyAccessRecoveryReminder\", model);\n        message.Category = \"EmergencyAccessRecoveryReminder\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendEmergencyAccessRecoveryTimedOut(EmergencyAccess emergencyAccess, string initiatingName, string email)\n    {\n        var message = CreateDefaultMessage(\"Emergency Access Granted\", email);\n        var model = new EmergencyAccessRecoveryTimedOutViewModel\n        {\n            Name = CoreHelpers.SanitizeForEmail(initiatingName),\n            Action = emergencyAccess.Type.ToString(),\n        };\n        await AddMessageContentAsync(message, \"Auth.EmergencyAccessRecoveryTimedOut\", model);\n        message.Category = \"EmergencyAccessRecoveryTimedOut\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendProviderSetupInviteEmailAsync(Provider provider, string token, string email)\n    {\n        var message = CreateDefaultMessage($\"Create a Provider\", email);\n        var model = new ProviderSetupInviteViewModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            ProviderId = provider.Id.ToString(),\n            Email = WebUtility.UrlEncode(email),\n            Token = WebUtility.UrlEncode(token),\n        };\n        await AddMessageContentAsync(message, \"Provider.ProviderSetupInvite\", model);\n        message.Category = \"ProviderSetupInvite\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendBusinessUnitConversionInviteAsync(Organization organization, string token, string email)\n    {\n        var message = CreateDefaultMessage(\"Set Up Business Unit\", email);\n        var model = new BusinessUnitConversionInviteModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            OrganizationId = organization.Id.ToString(),\n            Email = WebUtility.UrlEncode(email),\n            Token = WebUtility.UrlEncode(token)\n        };\n        await AddMessageContentAsync(message, \"Billing.BusinessUnitConversionInvite\", model);\n        message.Category = \"BusinessUnitConversionInvite\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email)\n    {\n        var message = CreateDefaultMessage($\"Join {providerName}\", email);\n        var model = new ProviderUserInvitedViewModel\n        {\n            ProviderName = CoreHelpers.SanitizeForEmail(providerName, false),\n            Email = WebUtility.UrlEncode(providerUser.Email),\n            ProviderId = providerUser.ProviderId.ToString(),\n            ProviderUserId = providerUser.Id.ToString(),\n            ProviderNameUrlEncoded = WebUtility.UrlEncode(providerName),\n            Token = WebUtility.UrlEncode(token),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n        };\n        await AddMessageContentAsync(message, \"Provider.ProviderUserInvited\", model);\n        message.Category = \"ProviderSetupInvite\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendProviderConfirmedEmailAsync(string providerName, string email)\n    {\n        var message = CreateDefaultMessage($\"You Have Been Confirmed To {providerName}\", email);\n        var model = new ProviderUserConfirmedViewModel\n        {\n            ProviderName = CoreHelpers.SanitizeForEmail(providerName),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"Provider.ProviderUserConfirmed\", model);\n        message.Category = \"ProviderUserConfirmed\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendProviderUserRemoved(string providerName, string email)\n    {\n        var message = CreateDefaultMessage($\"You Have Been Removed from {providerName}\", email);\n        var model = new ProviderUserRemovedViewModel\n        {\n            ProviderName = CoreHelpers.SanitizeForEmail(providerName),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName\n        };\n        await AddMessageContentAsync(message, \"Provider.ProviderUserRemoved\", model);\n        message.Category = \"ProviderUserRemoved\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendProviderUpdatePaymentMethod(\n        Guid organizationId,\n        string organizationName,\n        string providerName,\n        IEnumerable<string> emails)\n    {\n        var message = CreateDefaultMessage(\"Update your billing information\", emails);\n\n        var model = new ProviderUpdatePaymentMethodViewModel\n        {\n            OrganizationId = organizationId.ToString(),\n            OrganizationName = CoreHelpers.SanitizeForEmail(organizationName),\n            ProviderName = CoreHelpers.SanitizeForEmail(providerName),\n            SiteName = _globalSettings.SiteName,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash\n        };\n\n        await AddMessageContentAsync(message, \"Provider.ProviderUpdatePaymentMethod\", model);\n\n        message.Category = \"ProviderUpdatePaymentMethod\";\n\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendUpdatedTempPasswordEmailAsync(string email, string userName)\n    {\n        var message = CreateDefaultMessage(\"Master Password Has Been Changed\", email);\n        var model = new UpdateTempPasswordViewModel()\n        {\n            UserName = GetUserIdentifier(email, userName)\n        };\n        await AddMessageContentAsync(message, \"UpdatedTempPassword\", model);\n        message.Category = \"UpdatedTempPassword\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, string email, bool existingAccount, string token) =>\n        await BulkSendFamiliesForEnterpriseOfferEmailAsync(sponsorOrgName, new[] { (email, existingAccount, token) });\n\n    public async Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites)\n    {\n        MailQueueMessage CreateMessage((string Email, bool ExistingAccount, string Token) invite)\n        {\n            var message = CreateDefaultMessage(\"Accept Your Free Families Subscription\", invite.Email);\n            message.Category = \"FamiliesForEnterpriseOffer\";\n            var model = new FamiliesForEnterpriseOfferViewModel\n            {\n                SponsorOrgName = sponsorOrgName,\n                SponsoredEmail = WebUtility.UrlEncode(invite.Email),\n                ExistingAccount = invite.ExistingAccount,\n                WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n                SiteName = _globalSettings.SiteName,\n                SponsorshipToken = invite.Token,\n            };\n            var templateName = invite.ExistingAccount ?\n                \"FamiliesForEnterprise.FamiliesForEnterpriseOfferExistingAccount\" :\n                \"FamiliesForEnterprise.FamiliesForEnterpriseOfferNewAccount\";\n\n            return new MailQueueMessage(message, templateName, model);\n        }\n        var messageModels = invites.Select(invite => CreateMessage(invite));\n        await EnqueueMailAsync(messageModels);\n    }\n\n    public async Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail)\n    {\n        // Email family user\n        await SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(familyUserEmail);\n\n        // Email enterprise org user\n        await SendFamiliesForEnterpriseInviteRedeemedToEnterpriseUserEmailAsync(sponsorEmail);\n    }\n\n    private async Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email)\n    {\n        var message = CreateDefaultMessage(\"Success! Families Subscription Accepted\", email);\n        await AddMessageContentAsync(message, \"FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToFamilyUser\", new BaseMailModel());\n        message.Category = \"FamilyForEnterpriseRedeemedToFamilyUser\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    private async Task SendFamiliesForEnterpriseInviteRedeemedToEnterpriseUserEmailAsync(string email)\n    {\n        var message = CreateDefaultMessage(\"Success! Families Subscription Accepted\", email);\n        await AddMessageContentAsync(message, \"FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToEnterpriseUser\", new BaseMailModel());\n        message.Category = \"FamilyForEnterpriseRedeemedToEnterpriseUser\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate)\n    {\n        var message = CreateDefaultMessage(\"Your Families Sponsorship was Removed\", email);\n        var model = new FamiliesForEnterpriseSponsorshipRevertingViewModel\n        {\n            ExpirationDate = expirationDate,\n        };\n        await AddMessageContentAsync(message, \"FamiliesForEnterprise.FamiliesForEnterpriseSponsorshipReverting\", model);\n        message.Category = \"FamiliesForEnterpriseSponsorshipReverting\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendOTPEmailAsync(string email, string token)\n    {\n        var message = CreateDefaultMessage(\"Your Bitwarden Verification Code\", email);\n        var model = new UserVerificationEmailTokenViewModel\n        {\n            Token = token,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n        };\n        await AddMessageContentAsync(message, \"Auth.OTPEmail\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        message.Category = \"OTP\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)\n    {\n        var message = CreateDefaultMessage(\"Domain not claimed\", adminEmails);\n        var model = new OrganizationDomainUnverifiedViewModel\n        {\n            Url = $\"{_globalSettings.BaseServiceUri.VaultWithHash}/organizations/{organizationId}/settings/domain-verification\",\n            DomainName = domainName\n        };\n        await AddMessageContentAsync(message, \"OrganizationDomainUnclaimed\", model);\n        message.Category = \"UnclaimedOrganizationDomain\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,\n        IEnumerable<string> ownerEmails)\n    {\n        var message = CreateDefaultMessage($\"{organization.DisplayName()} Secrets Manager Seat Limit Reached\", ownerEmails);\n        var model = new OrganizationSeatsMaxReachedViewModel\n        {\n            MaxSeatCount = maxSeatCount,\n            VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)\n        };\n\n        await AddMessageContentAsync(message, \"OrganizationSmSeatsMaxReached\", model);\n        message.Category = \"OrganizationSmSeatsMaxReached\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,\n        IEnumerable<string> ownerEmails)\n    {\n        var message = CreateDefaultMessage($\"{organization.DisplayName()} Secrets Manager Machine Accounts Limit Reached\", ownerEmails);\n        var model = new OrganizationServiceAccountsMaxReachedViewModel\n        {\n            MaxServiceAccountsCount = maxSeatCount,\n            VaultSubscriptionUrl = GetCloudVaultSubscriptionUrl(organization.Id)\n        };\n\n        await AddMessageContentAsync(message, \"OrganizationSmServiceAccountsMaxReached\", model);\n        message.Category = \"OrganizationSmServiceAccountsMaxReached\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip,\n        string deviceTypeAndIdentifier)\n    {\n        var message = CreateDefaultMessage(\"Login request approved\", email);\n        var model = new TrustedDeviceAdminApprovalViewModel\n        {\n            TheDate = utcNow.ToLongDateString(),\n            TheTime = utcNow.ToShortTimeString(),\n            TimeZone = _utcTimeZoneDisplay,\n            IpAddress = ip,\n            DeviceType = deviceTypeAndIdentifier,\n        };\n        await AddMessageContentAsync(message, \"Auth.TrustedDeviceAdminApproval\", model);\n        message.Category = \"TrustedDeviceAdminApproval\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token)\n    {\n        var message = CreateDefaultMessage(\"Request to Delete Your Organization\", email);\n        var model = new OrganizationInitiateDeleteModel\n        {\n            Token = WebUtility.UrlEncode(token),\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            SiteName = _globalSettings.SiteName,\n            OrganizationId = organization.Id,\n            OrganizationName = CoreHelpers.SanitizeForEmail(organization.DisplayName(), false),\n            OrganizationNameUrlEncoded = WebUtility.UrlEncode(organization.Name),\n            OrganizationBillingEmail = organization.BillingEmail,\n            OrganizationPlan = organization.Plan,\n            OrganizationSeats = organization.Seats.ToString(),\n            OrganizationCreationDate = organization.CreationDate.ToLongDateString(),\n            OrganizationCreationTime = organization.CreationDate.ToShortTimeString(),\n            TimeZone = _utcTimeZoneDisplay,\n        };\n        await AddMessageContentAsync(message, \"InitiateDeleteOrganzation\", model);\n        message.MetaData.Add(\"SendGridBypassListManagement\", true);\n        message.Category = \"InitiateDeleteOrganzation\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,\n        string organizationName)\n    {\n        var message = CreateDefaultMessage(\"Removal of Free Bitwarden Families plan\", email);\n        var model = new FamiliesForEnterpriseRemoveOfferViewModel\n        {\n            SponsoredOrganizationId = organizationId,\n            SponsoringOrgName = CoreHelpers.SanitizeForEmail(organizationName),\n            OfferAcceptanceDate = offerAcceptanceDate,\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash\n        };\n        await AddMessageContentAsync(message, \"FamiliesForEnterprise.FamiliesForEnterpriseRemovedFromFamilyUser\", model);\n        message.Category = \"FamiliesForEnterpriseRemovedFromFamilyUser\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string? userName)\n    {\n        var templateName = _globalSettings.SelfHosted ?\n            \"AdminConsole.SelfHostNotifyAdminDeviceApprovalRequested\" :\n            \"AdminConsole.NotifyAdminDeviceApprovalRequested\";\n        var message = CreateDefaultMessage(\"Review SSO login request for new device\", adminEmails);\n        var model = new DeviceApprovalRequestedViewModel\n        {\n            WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            UserNameRequestingAccess = GetUserIdentifier(email, userName),\n            OrganizationId = organizationId,\n        };\n        await AddMessageContentAsync(message, templateName, model);\n        message.Category = \"DeviceApprovalRequested\";\n        await _mailDeliveryService.SendEmailAsync(message);\n    }\n\n    public async Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails)\n    {\n        MailQueueMessage CreateMessage(UserSecurityTasksCount notification)\n        {\n            var sanitizedOrgName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false);\n            var message = CreateDefaultMessage($\"{sanitizedOrgName} has identified {notification.TaskCount} at-risk password{(notification.TaskCount.Equals(1) ? \"\" : \"s\")}\", notification.Email);\n            var model = new SecurityTaskNotificationViewModel\n            {\n                OrgName = CoreHelpers.SanitizeForEmail(sanitizedOrgName, false),\n                TaskCount = notification.TaskCount,\n                AdminOwnerEmails = adminOwnerEmails.ToList(),\n                WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,\n            };\n            message.Category = \"SecurityTasksNotification\";\n            return new MailQueueMessage(message, \"SecurityTasksNotification\", model);\n        }\n        var messageModels = securityTaskNotifications.Select(CreateMessage);\n        await EnqueueMailAsync(messageModels.ToList());\n    }\n\n    private static string GetUserIdentifier(string email, string? userName)\n    {\n        return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);\n    }\n\n    private string GetCloudVaultSubscriptionUrl(Guid organizationId)\n        => _globalSettings.BaseServiceUri.CloudRegion?.ToLower() switch\n        {\n            \"eu\" => $\"https://vault.bitwarden.eu/#/organizations/{organizationId}/billing/subscription\",\n            _ => $\"https://vault.bitwarden.com/#/organizations/{organizationId}/billing/subscription\"\n        };\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/IMailService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Vault.Models.Data;\nusing Core.Auth.Enums;\n\nnamespace Bit.Core.Services;\n\n[Obsolete(\"The IMailService has been deprecated in favor of the IMailer. All new emails should be sent with an IMailer implementation.\")]\npublic interface IMailService\n{\n    [Obsolete(\"Use SendIndividualUserWelcomeEmailAsync instead\")]\n    Task SendWelcomeEmailAsync(User user);\n    /// <summary>\n    /// Email sent to users who have created a new account as an individual user.\n    /// </summary>\n    /// <param name=\"user\">The new User</param>\n    /// <returns>Task</returns>\n    Task SendIndividualUserWelcomeEmailAsync(User user);\n    /// <summary>\n    /// Email sent to users who have been confirmed to an organization.\n    /// </summary>\n    /// <param name=\"user\">The User</param>\n    /// <param name=\"organizationName\">The Organization user is being added to</param>\n    /// <returns>Task</returns>\n    Task SendOrganizationUserWelcomeEmailAsync(User user, string organizationName);\n    /// <summary>\n    /// Email sent to users who have been confirmed to a free or families organization.\n    /// </summary>\n    /// <param name=\"user\">The User</param>\n    /// <param name=\"familyOrganizationName\">The Families Organization user is being added to</param>\n    /// <returns>Task</returns>\n    Task SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(User user, string familyOrganizationName);\n    Task SendVerifyEmailEmailAsync(string email, Guid userId, string token);\n    Task SendRegistrationVerificationEmailAsync(string email, string token, string? fromMarketing);\n    Task SendTrialInitiationSignupEmailAsync(\n        bool isExistingUser,\n        string email,\n        string token,\n        ProductTierType productTier,\n        IEnumerable<ProductType> products,\n        int trialLength);\n    Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);\n    Task SendCannotDeleteClaimedAccountEmailAsync(string email);\n    Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);\n    Task SendChangeEmailEmailAsync(string newEmailAddress, string token);\n    Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);\n    /// <summary>\n    /// <see cref=\"DefaultOtpTokenProviderOptions\"/> has a default expiry of 5 minutes so we set the expiry to that value in the view model.\n    /// Sends OTP code token to the specified email address.\n    /// </summary>\n    /// <param name=\"email\">Email address to send the OTP to</param>\n    /// <param name=\"token\">Otp code token</param>\n    /// <param name=\"subject\">Subject line of the email</param>\n    /// <returns>Task</returns>\n    Task SendSendEmailOtpEmailAsync(string email, string token, string subject);\n    Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip);\n    Task SendNoMasterPasswordHintEmailAsync(string email);\n    Task SendMasterPasswordHintEmailAsync(string email, string hint);\n\n    /// <summary>\n    /// Sends one or many organization invite emails.\n    /// </summary>\n    /// <param name=\"orgInvitesInfo\">The information required to send the organization invites.</param>\n    Task SendOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo);\n    Task SendUpdatedOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo);\n    Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);\n    Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails);\n    Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails, bool hasAccessSecretsManager = false);\n    Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false);\n    Task SendUpdatedOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager = false);\n    Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email);\n    Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email);\n    Task SendPasswordlessSignInAsync(string returnUrl, string token, string email);\n    Task SendInvoiceUpcoming(\n        string email,\n        decimal amount,\n        DateTime dueDate,\n        List<string> items,\n        bool mentionInvoices);\n    Task SendInvoiceUpcoming(\n        IEnumerable<string> email,\n        decimal amount,\n        DateTime dueDate,\n        List<string> items,\n        bool mentionInvoices);\n    Task SendProviderInvoiceUpcoming(\n        IEnumerable<string> emails,\n        decimal amount,\n        DateTime dueDate,\n        List<string> items,\n        string? collectionMethod,\n        bool hasPaymentMethod,\n        string? paymentMethodDescription);\n    Task SendPaymentFailedAsync(string email, decimal amount, bool mentionInvoices);\n    Task SendAddedCreditAsync(string email, decimal amount);\n    Task SendLicenseExpiredAsync(IEnumerable<string> emails, string? organizationName = null);\n    Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip);\n    Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip);\n    Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token);\n    Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email);\n    Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email);\n    Task SendEmergencyAccessRecoveryInitiated(EmergencyAccess emergencyAccess, string initiatingName, string email);\n    Task SendEmergencyAccessRecoveryApproved(EmergencyAccess emergencyAccess, string approvingName, string email);\n    Task SendEmergencyAccessRecoveryRejected(EmergencyAccess emergencyAccess, string rejectingName, string email);\n    Task SendEmergencyAccessRecoveryReminder(EmergencyAccess emergencyAccess, string initiatingName, string email);\n    Task SendEmergencyAccessRecoveryTimedOut(EmergencyAccess ea, string initiatingName, string email);\n    Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage);\n    Task SendAdminResetPasswordEmailAsync(string email, string? userName, string orgName);\n    Task SendProviderSetupInviteEmailAsync(Provider provider, string token, string email);\n    Task SendBusinessUnitConversionInviteAsync(Organization organization, string token, string email);\n    Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email);\n    Task SendProviderConfirmedEmailAsync(string providerName, string email);\n    Task SendProviderUserRemoved(string providerName, string email);\n    Task SendProviderUpdatePaymentMethod(\n        Guid organizationId,\n        string organizationName,\n        string providerName,\n        IEnumerable<string> emails);\n    Task SendUpdatedTempPasswordEmailAsync(string email, string userName);\n    Task SendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, string email, bool existingAccount, string token);\n    Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string SponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites);\n    Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);\n    Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);\n    Task SendOTPEmailAsync(string email, string token);\n    Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);\n    Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);\n    Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);\n    Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);\n    Task SendTrialInitiationEmailAsync(string email);\n    Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token);\n    Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token);\n    Task SendRequestSMAccessToAdminEmailAsync(IEnumerable<string> adminEmails, string organizationName, string userRequestingAccess, string emailContent);\n#nullable disable\n    Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate, string organizationId,\n        string organizationName);\n#nullable enable\n    Task SendClaimedDomainUserEmailAsync(ClaimedUserDomainClaimedEmails emailList);\n    Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string? userName);\n    Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails);\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Mailer/BaseMail.cs",
    "content": "﻿namespace Bit.Core.Platform.Mail.Mailer;\n\n#nullable enable\n\n/// <summary>\n/// BaseMail describes a model for emails. It contains metadata about the email such as recipients,\n/// subject, and an optional category for processing at the upstream email delivery service.\n///\n/// Each BaseMail must have a view model that inherits from BaseMailView. The view model is used to\n/// generate the text part and HTML body.\n/// </summary>\npublic abstract class BaseMail<TView> where TView : BaseMailView\n{\n    /// <summary>\n    /// Email recipients.\n    /// </summary>\n    public required IEnumerable<string> ToEmails { get; set; }\n\n    /// <summary>\n    /// The subject of the email.\n    /// </summary>\n    public abstract string Subject { get; set; }\n\n    /// <summary>\n    /// An optional category for processing at the upstream email delivery service.\n    /// </summary>\n    public string? Category { get; }\n\n    /// <summary>\n    /// Allows you to override and ignore the suppression list for this email.\n    ///\n    /// Warning: This should be used with caution, valid reasons are primarily account recovery, email OTP.\n    /// </summary>\n    public virtual bool IgnoreSuppressList { get; } = false;\n\n    /// <summary>\n    /// View model for the email body.\n    /// </summary>\n    public required TView View { get; set; }\n}\n\n/// <summary>\n/// Each MailView consists of two body parts: a text part and an HTML part and the filename must be\n/// relative to the viewmodel and match the following pattern:\n/// - `{ClassName}.html.hbs` for the HTML part\n/// - `{ClassName}.text.hbs` for the text part\n/// </summary>\npublic abstract class BaseMailView\n{\n    /// <summary>\n    /// Current year.\n    /// </summary>\n    public string CurrentYear => DateTime.UtcNow.Year.ToString();\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Mailer/HandlebarMailRenderer.cs",
    "content": "﻿#nullable enable\nusing System.Collections.Concurrent;\nusing System.Reflection;\nusing Bit.Core.Settings;\nusing HandlebarsDotNet;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Mail.Mailer;\npublic class HandlebarMailRenderer : IMailRenderer\n{\n    /// <summary>\n    /// Lazy-initialized Handlebars instance. Thread-safe and ensures initialization occurs only once.\n    /// </summary>\n    private readonly Lazy<Task<IHandlebars>> _handlebarsTask;\n\n    /// <summary>\n    /// Helper function that returns the handlebar instance.\n    /// </summary>\n    private Task<IHandlebars> GetHandlebars() => _handlebarsTask.Value;\n\n    /// <summary>\n    /// This dictionary is used to cache compiled templates in a thread-safe manner.\n    /// </summary>\n    private readonly ConcurrentDictionary<string, Lazy<Task<HandlebarsTemplate<object, object>>>> _templateCache = new();\n\n    private readonly ILogger<HandlebarMailRenderer> _logger;\n    private readonly GlobalSettings _globalSettings;\n\n    public HandlebarMailRenderer(ILogger<HandlebarMailRenderer> logger, GlobalSettings globalSettings)\n    {\n        _logger = logger;\n        _globalSettings = globalSettings;\n\n        _handlebarsTask = new Lazy<Task<IHandlebars>>(InitializeHandlebarsAsync, LazyThreadSafetyMode.ExecutionAndPublication);\n    }\n\n    public async Task<(string html, string txt)> RenderAsync(BaseMailView model)\n    {\n        var html = await CompileTemplateAsync(model, \"html\");\n        var txt = await CompileTemplateAsync(model, \"text\");\n\n        return (html, txt);\n    }\n\n    private async Task<string> CompileTemplateAsync(BaseMailView model, string type)\n    {\n        var templateName = $\"{model.GetType().FullName}.{type}.hbs\";\n        var assembly = model.GetType().Assembly;\n\n        // GetOrAdd is atomic - only one Lazy will be stored per templateName.\n        // The Lazy with ExecutionAndPublication ensures the compilation happens exactly once.\n        var lazyTemplate = _templateCache.GetOrAdd(\n            templateName,\n            key => new Lazy<Task<HandlebarsTemplate<object, object>>>(\n                () => CompileTemplateInternalAsync(assembly, key),\n                LazyThreadSafetyMode.ExecutionAndPublication));\n\n        var template = await lazyTemplate.Value;\n        return template(model);\n    }\n\n    private async Task<HandlebarsTemplate<object, object>> CompileTemplateInternalAsync(Assembly assembly, string templateName)\n    {\n        var source = await ReadSourceAsync(assembly, templateName);\n        var handlebars = await GetHandlebars();\n        return handlebars.Compile(source);\n    }\n\n    private async Task<string> ReadSourceAsync(Assembly assembly, string template)\n    {\n        if (assembly.GetManifestResourceNames().All(f => f != template))\n        {\n            throw new FileNotFoundException(\"Template not found: \" + template);\n        }\n\n        var diskSource = await ReadSourceFromDiskAsync(template);\n        if (!string.IsNullOrWhiteSpace(diskSource))\n        {\n            return diskSource;\n        }\n\n        await using var s = assembly.GetManifestResourceStream(template)!;\n        using var sr = new StreamReader(s);\n        return await sr.ReadToEndAsync();\n    }\n\n    private async Task<string?> ReadSourceFromDiskAsync(string template)\n    {\n        if (!_globalSettings.SelfHosted)\n        {\n            return null;\n        }\n\n        try\n        {\n            var diskPath = Path.GetFullPath(Path.Combine(_globalSettings.MailTemplateDirectory, template));\n            var baseDirectory = Path.GetFullPath(_globalSettings.MailTemplateDirectory);\n\n            // Ensure the resolved path is within the configured directory\n            if (!diskPath.StartsWith(baseDirectory + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) &&\n                !diskPath.Equals(baseDirectory, StringComparison.OrdinalIgnoreCase))\n            {\n                _logger.LogWarning(\"Template path traversal attempt detected: {Template}\", template);\n                return null;\n            }\n\n            if (File.Exists(diskPath))\n            {\n                var fileContents = await File.ReadAllTextAsync(diskPath);\n                return fileContents;\n            }\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(e, \"Failed to read mail template from disk: {TemplateName}\", template);\n        }\n\n        return null;\n    }\n\n    private async Task<IHandlebars> InitializeHandlebarsAsync()\n    {\n        var handlebars = Handlebars.Create();\n\n        // TODO: Do we still need layouts with MJML?\n        var assembly = typeof(HandlebarMailRenderer).Assembly;\n\n        // Register Full layouts\n        var fullHtmlLayoutSource = await ReadSourceAsync(assembly, \"Bit.Core.MailTemplates.Handlebars.Layouts.Full.html.hbs\");\n        handlebars.RegisterTemplate(\"FullHtmlLayout\", fullHtmlLayoutSource);\n\n        var fullTextLayoutSource = await ReadSourceAsync(assembly, \"Bit.Core.MailTemplates.Handlebars.Layouts.Full.text.hbs\");\n        handlebars.RegisterTemplate(\"FullTextLayout\", fullTextLayoutSource);\n\n        // Register TitleContactUs layouts\n        var titleContactUsHtmlLayoutSource = await ReadSourceAsync(assembly, \"Bit.Core.MailTemplates.Handlebars.Layouts.TitleContactUs.html.hbs\");\n        handlebars.RegisterTemplate(\"TitleContactUsHtmlLayout\", titleContactUsHtmlLayoutSource);\n\n        var titleContactUsTextLayoutSource = await ReadSourceAsync(assembly, \"Bit.Core.MailTemplates.Handlebars.Layouts.TitleContactUs.text.hbs\");\n        handlebars.RegisterTemplate(\"TitleContactUsTextLayout\", titleContactUsTextLayoutSource);\n\n        return handlebars;\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Mailer/IMailRenderer.cs",
    "content": "﻿#nullable enable\nnamespace Bit.Core.Platform.Mail.Mailer;\n\npublic interface IMailRenderer\n{\n    Task<(string html, string txt)> RenderAsync(BaseMailView model);\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Mailer/IMailer.cs",
    "content": "﻿namespace Bit.Core.Platform.Mail.Mailer;\n\n#nullable enable\n\n/// <summary>\n/// Generic mailer interface for sending email messages.\n/// </summary>\npublic interface IMailer\n{\n    /// <summary>\n    /// Sends an email message.\n    /// </summary>\n    /// <param name=\"message\"></param>\n    public Task SendEmail<T>(BaseMail<T> message) where T : BaseMailView;\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Mailer/Mailer.cs",
    "content": "﻿using Bit.Core.Models.Mail;\nusing Bit.Core.Platform.Mail.Delivery;\n\nnamespace Bit.Core.Platform.Mail.Mailer;\n\n#nullable enable\n\npublic class Mailer(IMailRenderer renderer, IMailDeliveryService mailDeliveryService) : IMailer\n{\n    public async Task SendEmail<T>(BaseMail<T> message) where T : BaseMailView\n    {\n        var content = await renderer.RenderAsync(message.View);\n\n        var metadata = new Dictionary<string, object>();\n        if (message.IgnoreSuppressList)\n        {\n            metadata.Add(\"SendGridBypassListManagement\", true);\n        }\n\n        var mailMessage = new MailMessage\n        {\n            ToEmails = message.ToEmails,\n            Subject = message.Subject,\n            MetaData = metadata,\n            HtmlContent = content.html,\n            TextContent = content.txt,\n            Category = message.Category,\n        };\n\n        await mailDeliveryService.SendEmailAsync(mailMessage);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/Mailer/MailerServiceCollectionExtensions.cs",
    "content": "﻿using Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace Bit.Core.Platform.Mail.Mailer;\n\n#nullable enable\n\n/// <summary>\n/// Extension methods for adding the Mailer feature to the service collection.\n/// </summary>\npublic static class MailerServiceCollectionExtensions\n{\n    /// <summary>\n    /// Adds the Mailer services to the <see cref=\"IServiceCollection\"/>.\n    /// This includes the mail renderer and mailer for sending templated emails.\n    /// This method is safe to be run multiple times.\n    /// </summary>\n    /// <param name=\"services\">The <see cref=\"IServiceCollection\"/> to add services to.</param>\n    /// <returns>The <see cref=\"IServiceCollection\"/> for additional chaining.</returns>\n    public static IServiceCollection AddMailer(this IServiceCollection services)\n    {\n        services.TryAddSingleton<IMailRenderer, HandlebarMailRenderer>();\n        services.TryAddSingleton<IMailer, Mailer>();\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/NoopMailService.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Vault.Models.Data;\nusing Core.Auth.Enums;\n\nnamespace Bit.Core.Services;\n\n[Obsolete(\"The IMailService has been deprecated in favor of the IMailer. All new emails should be sent with an IMailer implementation.\")]\npublic class NoopMailService : IMailService\n{\n    public Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendVerifyEmailEmailAsync(string email, Guid userId, string hint)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendRegistrationVerificationEmailAsync(string email, string hint, string? fromMarketing)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendTrialInitiationSignupEmailAsync(\n        bool isExistingUser,\n        string email,\n        string token,\n        ProductTierType productTier,\n        IEnumerable<ProductType> products,\n        int trailLength)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendChangeEmailEmailAsync(string newEmailAddress, string token)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendMasterPasswordHintEmailAsync(string email, string hint)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendNoMasterPasswordHintEmailAsync(string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier,\n        IEnumerable<string> adminEmails, bool hasAccessSecretsManager = false)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendUpdatedOrganizationConfirmedEmailAsync(Organization organization, string userEmail, bool accessSecretsManager = false)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendUpdatedOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) =>\n        Task.CompletedTask;\n\n    public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) =>\n        Task.CompletedTask;\n\n    public Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendSendEmailOtpEmailAsync(string email, string token, string subject)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendWelcomeEmailAsync(User user)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendIndividualUserWelcomeEmailAsync(User user)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendOrganizationUserWelcomeEmailAsync(User user, string organizationName)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(User user, string familyOrganizationName)\n    {\n        return Task.FromResult(0);\n    }\n    public Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendCannotDeleteClaimedAccountEmailAsync(string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendInvoiceUpcoming(\n        string email,\n        decimal amount,\n        DateTime dueDate,\n        List<string> items,\n        bool mentionInvoices) => Task.FromResult(0);\n\n    public Task SendInvoiceUpcoming(\n        IEnumerable<string> emails,\n        decimal amount,\n        DateTime dueDate,\n        List<string> items,\n        bool mentionInvoices) => Task.FromResult(0);\n\n    public Task SendProviderInvoiceUpcoming(\n        IEnumerable<string> emails,\n        decimal amount,\n        DateTime dueDate,\n        List<string> items,\n        string? collectionMethod = null,\n        bool hasPaymentMethod = true,\n        string? paymentMethodDescription = null) => Task.FromResult(0);\n\n    public Task SendPaymentFailedAsync(string email, decimal amount, bool mentionInvoices)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendAddedCreditAsync(string email, decimal amount)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendLicenseExpiredAsync(IEnumerable<string> emails, string? organizationName = null)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendEmergencyAccessRecoveryInitiated(EmergencyAccess emergencyAccess, string initiatingName, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendEmergencyAccessRecoveryApproved(EmergencyAccess emergencyAccess, string approvingName, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendEmergencyAccessRecoveryRejected(EmergencyAccess emergencyAccess, string rejectingName, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendEmergencyAccessRecoveryReminder(EmergencyAccess emergencyAccess, string initiatingName, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendEmergencyAccessRecoveryTimedOut(EmergencyAccess ea, string initiatingName, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendAdminResetPasswordEmailAsync(string email, string? userName, string orgName)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendProviderSetupInviteEmailAsync(Provider provider, string token, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendBusinessUnitConversionInviteAsync(Organization organization, string token, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendProviderConfirmedEmailAsync(string providerName, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendProviderUserRemoved(string providerName, string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendProviderUpdatePaymentMethod(Guid organizationId, string organizationName, string providerName,\n        IEnumerable<string> emails) => Task.FromResult(0);\n\n    public Task SendUpdatedTempPasswordEmailAsync(string email, string userName)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendFamiliesForEnterpriseOfferEmailAsync(string SponsorOrgName, string email, bool existingAccount, string token)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string SponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendOTPEmailAsync(string email, string token)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,\n        IEnumerable<string> ownerEmails)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization,\n        int maxSeatCount,\n        IEnumerable<string> ownerEmails)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendTrialInitiationEmailAsync(string email)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token) => throw new NotImplementedException();\n\n    public Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token)\n    {\n        return Task.FromResult(0);\n    }\n    public Task SendRequestSMAccessToAdminEmailAsync(IEnumerable<string> adminEmails, string organizationName, string userRequestingAccess, string emailContent) => throw new NotImplementedException();\n\n    public Task SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(string email, string offerAcceptanceDate,\n        string organizationId,\n        string organizationName)\n    {\n        return Task.FromResult(0);\n    }\n    public Task SendClaimedDomainUserEmailAsync(ClaimedUserDomainClaimedEmails emailList) => Task.CompletedTask;\n\n    public Task SendDeviceApprovalRequestedNotificationEmailAsync(IEnumerable<string> adminEmails, Guid organizationId, string email, string? userName)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task SendBulkSecurityTaskNotificationsAsync(Organization org, IEnumerable<UserSecurityTasksCount> securityTaskNotifications, IEnumerable<string> adminOwnerEmails)\n    {\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Mail/README.md",
    "content": "# Mail Services\n## `MailService`\n\n> [!WARNING]\n> The `MailService` and its implementation in `HandlebarsMailService` has been deprecated in favor of the `Mailer` implementation.\n\nThe `MailService` class manages **all** emails, and has multiple responsibilities, including formatting, email building (instantiation of ViewModels from variables), and deciding if a mail request should be enqueued or sent directly.\n\nThe resulting implementation cannot be owned by a single team (since all emails are in a single class), and as a result, anyone can edit any template without the appropriate team being informed.\n\nTo alleviate these issues, all new emails should be implemented using [MJML](../../MailTemplates/README.md) and the `Mailer`.\n\n## `Mailer`\n\nThe Mailer feature provides a structured, type-safe approach to sending emails in the Bitwarden server application. It\nuses Handlebars templates to render both HTML and plain text email content.\n\n### Architecture\n\nThe Mailer system consists of four main components:\n\n1. **IMailer** - Service interface for sending emails\n2. **BaseMail<TView>** - Abstract base class defining email metadata (recipients, subject, category)\n3. **BaseMailView** - Abstract base class for email template ViewModels\n4. **IMailRenderer** - Internal interface for rendering templates (implemented by `HandlebarMailRenderer`)\n\n### How To Use\n\n1. Define a ViewModel that inherits from `BaseMailView` with properties for template data.\n2. Define an email class that inherits from `BaseMail<TView>` with metadata like `Subject`.\n3. Create Handlebars templates (`.html.hbs` and `.text.hbs`) as embedded resources, preferably using the `MJML` [pipeline](../../MailTemplates/Mjml/README.md#development-process), in\n   a directory in `/src/Core/MailTemplates/Mjml`.\n4. Use `IMailer.SendEmail()` to render and send the email.\n\n### Creating a New Email\n\n#### Step 1: Define the ViewModel\n\nCreate a class that inherits from `BaseMailView`:\n\n```csharp\nusing Bit.Core.Platform.Mailer;\n\nnamespace MyApp.Emails;\n\npublic class WelcomeEmailView : BaseMailView\n{\n    public required string UserName { get; init; }\n    public required string ActivationUrl { get; init; }\n}\n```\n\n#### Step 2: Define the email class\n\nCreate a class that inherits from `BaseMail<TView>`:\n\n```csharp\npublic class WelcomeEmail : BaseMail<WelcomeEmailView>\n{\n    public override string Subject => \"Welcome to Bitwarden\";\n}\n```\n\n#### Step 3: Create Handlebars templates\n\nCreate two template files as embedded resources next to your ViewModel.\n\n> [!IMPORTANT]  \n> The files must be located directly next to the `ViewClass` and match the name of the view.\n\n**WelcomeEmailView.html.hbs** (HTML version):\n\n```handlebars\n<h1>Welcome, {{ UserName }}!</h1>\n<p>Thank you for joining Bitwarden.</p>\n<p>\n    <a href=\"{{ ActivationUrl }}\">Activate your account</a>\n</p>\n<p><small>&copy; {{ CurrentYear }} Bitwarden Inc.</small></p>\n```\n\n**WelcomeEmailView.text.hbs** (plain text version):\n\n```handlebars\nWelcome, {{ UserName }}!\n\nThank you for joining Bitwarden.\n\nActivate your account: {{ ActivationUrl }}\n\n� {{ CurrentYear }} Bitwarden Inc.\n```\n\n**Important**: Template files must be configured as embedded resources in your `.csproj`:\n\n```xml\n\n<ItemGroup>\n    <EmbeddedResource Include=\"**\\*.hbs\" />\n</ItemGroup>\n```\n\n#### Step 4: Send the email\n\nInject `IMailer` and send the email, this may be done in a service, command or some other application layer.\n\n```csharp\npublic class SomeService\n{\n    private readonly IMailer _mailer;\n\n    public SomeService(IMailer mailer)\n    {\n        _mailer = mailer;\n    }\n\n    public async Task SendWelcomeEmailAsync(string email, string userName, string activationUrl)\n    {\n        var mail = new WelcomeEmail\n        {\n            ToEmails = [email],\n            View = new WelcomeEmailView\n            {\n                UserName = userName,\n                ActivationUrl = activationUrl\n            }\n        };\n\n        await _mailer.SendEmail(mail);\n    }\n}\n```\n\n### Advanced Features\n\n#### Multiple Recipients\n\nSend to multiple recipients by providing multiple email addresses:\n\n```csharp\nvar mail = new WelcomeEmail\n{\n    ToEmails = [\"user1@example.com\", \"user2@example.com\"],\n    View = new WelcomeEmailView { /* ... */ }\n};\n```\n\n#### Bypass Suppression List\n\nFor critical emails like account recovery or email OTP, you can bypass the suppression list:\n\n```csharp\npublic class PasswordResetEmail : BaseMail<PasswordResetEmailView>\n{\n    public override string Subject => \"Reset Your Password\";\n    public override bool IgnoreSuppressList => true; // Use with caution\n}\n```\n\n**Warning**: Only use `IgnoreSuppressList = true` for critical account recovery or authentication emails.\n\n#### Email Categories\n\nOptionally categorize emails for processing at the upstream email delivery service:\n\n```csharp\npublic class MarketingEmail : BaseMail<MarketingEmailView>\n{\n    public override string Subject => \"Latest Updates\";\n    public string? Category => \"marketing\";\n}\n```\n\n### Built-in View Properties\n\nAll ViewModels inherit from `BaseMailView`, which provides:\n\n- **CurrentYear** - The current UTC year (useful for copyright notices)\n\n```handlebars\n\n<footer>&copy; {{ CurrentYear }} Bitwarden Inc.</footer>\n```\n\n### Template Naming Convention\n\nTemplates must follow this naming convention:\n\n- HTML template: `{ViewModelFullName}.html.hbs`\n- Text template: `{ViewModelFullName}.text.hbs`\n\nFor example, if your ViewModel is `Bit.Core.Auth.Models.Mail.VerifyEmailView`, the templates must be:\n\n- `Bit.Core.Auth.Models.Mail.VerifyEmailView.html.hbs`\n- `Bit.Core.Auth.Models.Mail.VerifyEmailView.text.hbs`\n\n## Dependency Injection\n\nRegister the Mailer services in your DI container using the extension method:\n\n```csharp\nusing Bit.Core.Platform.Mailer;\n\nservices.AddMailer();\n```\n\nOr manually register the services:\n\n```csharp\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nservices.TryAddSingleton<IMailRenderer, HandlebarMailRenderer>();\nservices.TryAddSingleton<IMailer, Mailer>();\n```\n\n### Performance Notes\n\n- **Template caching** - `HandlebarMailRenderer` automatically caches compiled templates\n- **Lazy initialization** - Handlebars is initialized only when first needed\n- **Thread-safe** - The renderer is thread-safe for concurrent email rendering\n\n# Overriding email templates from disk\n\nThe mail services support loading the mail template from disk. This is intended to be used by self-hosted customers who want to modify their email appearance. These overrides are not intended to be used during local development, as any changes there would not be reflected in the templates used in a normal deployment configuration.\n\nAny customer using this override has worked with Bitwarden support on an approved implementation and has acknowledged that they are responsible for reacting to any changes made to the templates as a part of the Bitwarden development process. This includes, but is not limited to, changes in Handlebars property names, removal of properties from the ViewModel classes, and changes in template names.  **Bitwarden is not responsible for maintaining backward compatibility between releases in order to support any overridden emails.**"
  },
  {
    "path": "src/Core/Platform/PlatformServiceCollectionExtensions.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Platform.Installations;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Platform;\n\npublic static class PlatformServiceCollectionExtensions\n{\n    /// <summary>\n    /// Extend DI to include commands and queries exported from the Platform\n    /// domain.\n    /// </summary>\n    public static IServiceCollection AddPlatformServices(this IServiceCollection services)\n    {\n        services.AddScoped<IGetInstallationQuery, GetInstallationQuery>();\n        services.AddScoped<IUpdateInstallationCommand, UpdateInstallationCommand>();\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/Engines/AzureQueuePushEngine.cs",
    "content": "﻿using System.Text.Json;\nusing Azure.Storage.Queues;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\npublic class AzureQueuePushEngine : IPushEngine\n{\n    private readonly QueueClient _queueClient;\n    private readonly IHttpContextAccessor _httpContextAccessor;\n\n    public AzureQueuePushEngine(\n        [FromKeyedServices(\"notifications\")] QueueClient queueClient,\n        IHttpContextAccessor httpContextAccessor,\n        IGlobalSettings globalSettings,\n        ILogger<AzureQueuePushEngine> logger)\n    {\n        _queueClient = queueClient;\n        _httpContextAccessor = httpContextAccessor;\n        if (globalSettings.Installation.Id == Guid.Empty)\n        {\n            logger.LogWarning(\"Installation ID is not set. Push notifications for installations will not work.\");\n        }\n    }\n\n    public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)\n    {\n        if (cipher.OrganizationId.HasValue)\n        {\n            var message = new SyncCipherPushNotification\n            {\n                Id = cipher.Id,\n                OrganizationId = cipher.OrganizationId,\n                RevisionDate = cipher.RevisionDate,\n                CollectionIds = collectionIds,\n            };\n\n            await SendMessageAsync(type, message, true);\n        }\n        else if (cipher.UserId.HasValue)\n        {\n            var message = new SyncCipherPushNotification\n            {\n                Id = cipher.Id,\n                UserId = cipher.UserId,\n                RevisionDate = cipher.RevisionDate,\n            };\n\n            await SendMessageAsync(type, message, true);\n        }\n    }\n\n    private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)\n    {\n        var contextId = GetContextIdentifier(excludeCurrentContext);\n        var message = JsonSerializer.Serialize(new PushNotificationData<T>(type, payload, contextId),\n            JsonHelpers.IgnoreWritingNull);\n        await _queueClient.SendMessageAsync(message);\n    }\n\n    private string? GetContextIdentifier(bool excludeCurrentContext)\n    {\n        if (!excludeCurrentContext)\n        {\n            return null;\n        }\n\n        var currentContext =\n            _httpContextAccessor?.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;\n        return currentContext?.DeviceIdentifier;\n    }\n\n    public async Task PushAsync<T>(PushNotification<T> pushNotification)\n        where T : class\n    {\n        await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/Engines/MultiServicePushNotificationService.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\npublic class MultiServicePushNotificationService : IPushNotificationService\n{\n    private readonly IPushEngine[] _services;\n\n    public Guid InstallationId { get; }\n\n    public TimeProvider TimeProvider { get; }\n\n    public ILogger Logger { get; }\n\n    public MultiServicePushNotificationService(\n        IEnumerable<IPushEngine> services,\n        ILogger<MultiServicePushNotificationService> logger,\n        GlobalSettings globalSettings,\n        TimeProvider timeProvider)\n    {\n        // Filter out any NoopPushEngine's\n        _services = [.. services.Where(engine => engine is not NoopPushEngine)];\n\n        Logger = logger;\n        Logger.LogInformation(\"Hub services: {Services}\", _services.Count());\n        globalSettings.NotificationHubPool?.NotificationHubs?.ForEach(hub =>\n        {\n            Logger.LogInformation(\"HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}\", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);\n        });\n        InstallationId = globalSettings.Installation.Id;\n        TimeProvider = timeProvider;\n    }\n\n    private Task PushToServices(Func<IPushEngine, Task> pushFunc)\n    {\n        if (!_services.Any())\n        {\n            Logger.LogWarning(\"No services found to push notification\");\n            return Task.CompletedTask;\n        }\n\n\n#if DEBUG\n        var tasks = new List<Task>();\n#endif\n\n        foreach (var service in _services)\n        {\n            Logger.LogDebug(\"Pushing notification to service {ServiceName}\", service.GetType().Name);\n#if DEBUG\n            var task =\n#endif\n            pushFunc(service);\n#if DEBUG\n            tasks.Add(task);\n#endif\n        }\n\n#if DEBUG\n        return Task.WhenAll(tasks);\n#else\n        return Task.CompletedTask;\n#endif\n    }\n\n    public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds)\n    {\n        return PushToServices((s) => s.PushCipherAsync(cipher, pushType, collectionIds));\n    }\n    public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class\n    {\n        return PushToServices((s) => s.PushAsync(pushNotification));\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/Engines/NoopPushEngine.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\ninternal class NoopPushEngine : IPushEngine\n{\n    public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds) => Task.CompletedTask;\n\n    public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class => Task.CompletedTask;\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/Engines/NotificationsApiPushEngine.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\n/// <summary>\n/// Sends non-mobile push notifications to the Azure Queue Api, later received by Notifications Api.\n/// Used by Cloud-Hosted environments.\n/// Received by AzureQueueHostedService message receiver in Notifications project.\n/// </summary>\npublic class NotificationsApiPushEngine : BaseIdentityClientService, IPushEngine\n{\n    private readonly IHttpContextAccessor _httpContextAccessor;\n\n    public NotificationsApiPushEngine(\n        IHttpClientFactory httpFactory,\n        GlobalSettings globalSettings,\n        IHttpContextAccessor httpContextAccessor,\n        ILogger<NotificationsApiPushEngine> logger)\n        : base(\n            httpFactory,\n            globalSettings.BaseServiceUri.InternalNotifications,\n            globalSettings.BaseServiceUri.InternalIdentity,\n            \"internal\",\n            $\"internal.{globalSettings.ProjectName}\",\n            globalSettings.InternalIdentityKey,\n            logger)\n    {\n        _httpContextAccessor = httpContextAccessor;\n    }\n\n    public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)\n    {\n        if (cipher.OrganizationId.HasValue)\n        {\n            var message = new SyncCipherPushNotification\n            {\n                Id = cipher.Id,\n                OrganizationId = cipher.OrganizationId,\n                RevisionDate = cipher.RevisionDate,\n                CollectionIds = collectionIds,\n            };\n\n            await SendMessageAsync(type, message, true);\n        }\n        else if (cipher.UserId.HasValue)\n        {\n            var message = new SyncCipherPushNotification\n            {\n                Id = cipher.Id,\n                UserId = cipher.UserId,\n                RevisionDate = cipher.RevisionDate,\n                CollectionIds = collectionIds,\n            };\n\n            await SendMessageAsync(type, message, true);\n        }\n    }\n\n    private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)\n    {\n        var contextId = GetContextIdentifier(excludeCurrentContext);\n        var request = new PushNotificationData<T>(type, payload, contextId);\n        await SendAsync(HttpMethod.Post, \"send\", request);\n    }\n\n    private string? GetContextIdentifier(bool excludeCurrentContext)\n    {\n        if (!excludeCurrentContext)\n        {\n            return null;\n        }\n\n        var currentContext =\n            _httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;\n        return currentContext?.DeviceIdentifier;\n    }\n\n    public async Task PushAsync<T>(PushNotification<T> pushNotification) where T : class\n    {\n        await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/Engines/RelayPushEngine.cs",
    "content": "﻿using Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\n/// <summary>\n/// Sends mobile push notifications to the Bitwarden Cloud API, then relayed to Azure Notification Hub.\n/// Used by Self-Hosted environments.\n/// Received by PushController endpoint in Api project.\n/// </summary>\npublic class RelayPushEngine : BaseIdentityClientService, IPushEngine\n{\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly IHttpContextAccessor _httpContextAccessor;\n\n\n    public RelayPushEngine(\n        IHttpClientFactory httpFactory,\n        IDeviceRepository deviceRepository,\n        GlobalSettings globalSettings,\n        IHttpContextAccessor httpContextAccessor,\n        ILogger<RelayPushEngine> logger)\n        : base(\n            httpFactory,\n            globalSettings.PushRelayBaseUri,\n            globalSettings.Installation.IdentityUri,\n            ApiScopes.ApiPush,\n            $\"installation.{globalSettings.Installation.Id}\",\n            globalSettings.Installation.Key,\n            logger)\n    {\n        _deviceRepository = deviceRepository;\n        _httpContextAccessor = httpContextAccessor;\n    }\n\n    public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)\n    {\n        if (cipher.OrganizationId.HasValue)\n        {\n            // We cannot send org pushes since access logic is much more complicated than just the fact that they belong\n            // to the organization. Potentially we could blindly send to just users that have the access all permission\n            // device registration needs to be more granular to handle that appropriately. A more brute force approach could\n            // me to send \"full sync\" push to all org users, but that has the potential to DDOS the API in bursts.\n\n            // await SendPayloadToOrganizationAsync(cipher.OrganizationId.Value, type, message, true);\n        }\n        else if (cipher.UserId.HasValue)\n        {\n            var message = new SyncCipherPushNotification\n            {\n                Id = cipher.Id,\n                UserId = cipher.UserId,\n                OrganizationId = cipher.OrganizationId,\n                RevisionDate = cipher.RevisionDate,\n            };\n\n            await PushAsync(new PushNotification<SyncCipherPushNotification>\n            {\n                Type = type,\n                Target = NotificationTarget.User,\n                TargetId = cipher.UserId.Value,\n                Payload = message,\n                ExcludeCurrentContext = true,\n            });\n        }\n    }\n\n    public async Task PushAsync<T>(PushNotification<T> pushNotification)\n        where T : class\n    {\n        var deviceIdentifier = _httpContextAccessor.HttpContext\n            ?.RequestServices.GetService<ICurrentContext>()\n            ?.DeviceIdentifier;\n\n        Guid? deviceId = null;\n\n        if (!string.IsNullOrEmpty(deviceIdentifier))\n        {\n            var device = await _deviceRepository.GetByIdentifierAsync(deviceIdentifier);\n            deviceId = device?.Id;\n        }\n\n        var payload = new PushSendRequestModel<T>\n        {\n            Type = pushNotification.Type,\n            UserId = pushNotification.GetTargetWhen(NotificationTarget.User),\n            OrganizationId = pushNotification.GetTargetWhen(NotificationTarget.Organization),\n            InstallationId = pushNotification.GetTargetWhen(NotificationTarget.Installation),\n            Payload = pushNotification.Payload,\n            Identifier = pushNotification.ExcludeCurrentContext ? deviceIdentifier : null,\n            // We set the device id regardless of if they want to exclude the current context or not\n            DeviceId = deviceId,\n            ClientType = pushNotification.ClientType,\n        };\n\n        await SendAsync(HttpMethod.Post, \"push/send\", payload);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/IPushEngine.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\npublic interface IPushEngine\n{\n    Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds);\n\n    Task PushAsync<T>(PushNotification<T> pushNotification)\n        where T : class;\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/IPushNotificationService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Push;\n\n/// <summary>\n/// Used to Push notifications to end-user devices.\n/// </summary>\n/// <remarks>\n/// New notifications should not be wired up inside this service. You may either directly call the\n/// <see cref=\"PushAsync\"/> method in your service to send your notification or if you want your notification\n/// sent by other teams you can make an extension method on this service with a well typed definition\n/// of your notification. You may also make your own service that injects this and exposes methods for each of\n/// your notifications.\n/// </remarks>\npublic interface IPushNotificationService\n{\n    private const string ServiceDeprecation = \"Do not use the services exposed here, instead use your own services injected in your service.\";\n\n    [Obsolete(ServiceDeprecation, DiagnosticId = \"BWP0001\")]\n    Guid InstallationId { get; }\n\n    [Obsolete(ServiceDeprecation, DiagnosticId = \"BWP0001\")]\n    TimeProvider TimeProvider { get; }\n\n    [Obsolete(ServiceDeprecation, DiagnosticId = \"BWP0001\")]\n    ILogger Logger { get; }\n\n    #region Legacy method, to be removed soon.\n    Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)\n        => PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);\n\n    Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)\n        => PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);\n\n    Task PushSyncCipherDeleteAsync(Cipher cipher)\n        => PushCipherAsync(cipher, PushType.SyncLoginDelete, null);\n\n    Task PushSyncFolderCreateAsync(Folder folder)\n        => PushAsync(new PushNotification<SyncFolderPushNotification>\n        {\n            Type = PushType.SyncFolderCreate,\n            Target = NotificationTarget.User,\n            TargetId = folder.UserId,\n            Payload = new SyncFolderPushNotification\n            {\n                Id = folder.Id,\n                UserId = folder.UserId,\n                RevisionDate = folder.RevisionDate,\n            },\n            ExcludeCurrentContext = true,\n        });\n\n    Task PushSyncFolderUpdateAsync(Folder folder)\n        => PushAsync(new PushNotification<SyncFolderPushNotification>\n        {\n            Type = PushType.SyncFolderUpdate,\n            Target = NotificationTarget.User,\n            TargetId = folder.UserId,\n            Payload = new SyncFolderPushNotification\n            {\n                Id = folder.Id,\n                UserId = folder.UserId,\n                RevisionDate = folder.RevisionDate,\n            },\n            ExcludeCurrentContext = true,\n        });\n\n    Task PushSyncFolderDeleteAsync(Folder folder)\n        => PushAsync(new PushNotification<SyncFolderPushNotification>\n        {\n            Type = PushType.SyncFolderDelete,\n            Target = NotificationTarget.User,\n            TargetId = folder.UserId,\n            Payload = new SyncFolderPushNotification\n            {\n                Id = folder.Id,\n                UserId = folder.UserId,\n                RevisionDate = folder.RevisionDate,\n            },\n            ExcludeCurrentContext = true,\n        });\n\n    Task PushSyncCiphersAsync(Guid userId, bool excludeCurrentContext = false)\n        => PushAsync(new PushNotification<UserPushNotification>\n        {\n            Type = PushType.SyncCiphers,\n            Target = NotificationTarget.User,\n            TargetId = userId,\n            Payload = new UserPushNotification\n            {\n                UserId = userId,\n#pragma warning disable BWP0001 // Type or member is obsolete\n                Date = TimeProvider.GetUtcNow().UtcDateTime,\n#pragma warning restore BWP0001 // Type or member is obsolete\n            },\n            ExcludeCurrentContext = excludeCurrentContext,\n        });\n\n    Task PushSyncVaultAsync(Guid userId)\n        => PushAsync(new PushNotification<UserPushNotification>\n        {\n            Type = PushType.SyncVault,\n            Target = NotificationTarget.User,\n            TargetId = userId,\n            Payload = new UserPushNotification\n            {\n                UserId = userId,\n#pragma warning disable BWP0001 // Type or member is obsolete\n                Date = TimeProvider.GetUtcNow().UtcDateTime,\n#pragma warning restore BWP0001 // Type or member is obsolete\n            },\n            ExcludeCurrentContext = false,\n        });\n\n    Task PushSyncOrganizationsAsync(Guid userId)\n        => PushAsync(new PushNotification<UserPushNotification>\n        {\n            Type = PushType.SyncOrganizations,\n            Target = NotificationTarget.User,\n            TargetId = userId,\n            Payload = new UserPushNotification\n            {\n                UserId = userId,\n#pragma warning disable BWP0001 // Type or member is obsolete\n                Date = TimeProvider.GetUtcNow().UtcDateTime,\n#pragma warning restore BWP0001 // Type or member is obsolete\n            },\n            ExcludeCurrentContext = false,\n        });\n\n    Task PushSyncOrgKeysAsync(Guid userId)\n        => PushAsync(new PushNotification<UserPushNotification>\n        {\n            Type = PushType.SyncOrgKeys,\n            Target = NotificationTarget.User,\n            TargetId = userId,\n            Payload = new UserPushNotification\n            {\n                UserId = userId,\n#pragma warning disable BWP0001 // Type or member is obsolete\n                Date = TimeProvider.GetUtcNow().UtcDateTime,\n#pragma warning restore BWP0001 // Type or member is obsolete\n            },\n            ExcludeCurrentContext = false,\n        });\n\n    Task PushSyncSettingsAsync(Guid userId)\n        => PushAsync(new PushNotification<UserPushNotification>\n        {\n            Type = PushType.SyncSettings,\n            Target = NotificationTarget.User,\n            TargetId = userId,\n            Payload = new UserPushNotification\n            {\n                UserId = userId,\n#pragma warning disable BWP0001 // Type or member is obsolete\n                Date = TimeProvider.GetUtcNow().UtcDateTime,\n#pragma warning restore BWP0001 // Type or member is obsolete\n            },\n            ExcludeCurrentContext = false,\n        });\n\n    Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false,\n        PushNotificationLogOutReason? reason = null)\n        => PushAsync(new PushNotification<LogOutPushNotification>\n        {\n            Type = PushType.LogOut,\n            Target = NotificationTarget.User,\n            TargetId = userId,\n            Payload = new LogOutPushNotification\n            {\n                UserId = userId,\n                Reason = reason\n            },\n            ExcludeCurrentContext = excludeCurrentContextFromPush,\n        });\n\n    Task PushSyncSendCreateAsync(Send send)\n    {\n        if (send.UserId.HasValue)\n        {\n            return PushAsync(new PushNotification<SyncSendPushNotification>\n            {\n                Type = PushType.SyncSendCreate,\n                Target = NotificationTarget.User,\n                TargetId = send.UserId.Value,\n                Payload = new SyncSendPushNotification\n                {\n                    Id = send.Id,\n                    UserId = send.UserId.Value,\n                    RevisionDate = send.RevisionDate,\n                },\n                ExcludeCurrentContext = true,\n            });\n        }\n\n        return Task.CompletedTask;\n    }\n\n    Task PushSyncSendUpdateAsync(Send send)\n    {\n        if (send.UserId.HasValue)\n        {\n            return PushAsync(new PushNotification<SyncSendPushNotification>\n            {\n                Type = PushType.SyncSendUpdate,\n                Target = NotificationTarget.User,\n                TargetId = send.UserId.Value,\n                Payload = new SyncSendPushNotification\n                {\n                    Id = send.Id,\n                    UserId = send.UserId.Value,\n                    RevisionDate = send.RevisionDate,\n                },\n                ExcludeCurrentContext = true,\n            });\n        }\n\n        return Task.CompletedTask;\n    }\n\n    Task PushSyncSendDeleteAsync(Send send)\n    {\n        if (send.UserId.HasValue)\n        {\n            return PushAsync(new PushNotification<SyncSendPushNotification>\n            {\n                Type = PushType.SyncSendDelete,\n                Target = NotificationTarget.User,\n                TargetId = send.UserId.Value,\n                Payload = new SyncSendPushNotification\n                {\n                    Id = send.Id,\n                    UserId = send.UserId.Value,\n                    RevisionDate = send.RevisionDate,\n                },\n                ExcludeCurrentContext = true,\n            });\n        }\n\n        return Task.CompletedTask;\n    }\n\n    Task PushNotificationAsync(Notification notification)\n    {\n        var message = new NotificationPushNotification\n        {\n            Id = notification.Id,\n            Priority = notification.Priority,\n            Global = notification.Global,\n            ClientType = notification.ClientType,\n            UserId = notification.UserId,\n            OrganizationId = notification.OrganizationId,\n#pragma warning disable BWP0001 // Type or member is obsolete\n            InstallationId = notification.Global ? InstallationId : null,\n#pragma warning restore BWP0001 // Type or member is obsolete\n            TaskId = notification.TaskId,\n            Title = notification.Title,\n            Body = notification.Body,\n            CreationDate = notification.CreationDate,\n            RevisionDate = notification.RevisionDate,\n        };\n\n        NotificationTarget target;\n        Guid targetId;\n\n        if (notification.Global)\n        {\n            // TODO: Think about this a bit more\n            target = NotificationTarget.Installation;\n#pragma warning disable BWP0001 // Type or member is obsolete\n            targetId = InstallationId;\n#pragma warning restore BWP0001 // Type or member is obsolete\n        }\n        else if (notification.UserId.HasValue)\n        {\n            target = NotificationTarget.User;\n            targetId = notification.UserId.Value;\n        }\n        else if (notification.OrganizationId.HasValue)\n        {\n            target = NotificationTarget.Organization;\n            targetId = notification.OrganizationId.Value;\n        }\n        else\n        {\n#pragma warning disable BWP0001 // Type or member is obsolete\n            Logger.LogWarning(\"Invalid notification id {NotificationId} push notification\", notification.Id);\n#pragma warning restore BWP0001 // Type or member is obsolete\n            return Task.CompletedTask;\n        }\n\n        return PushAsync(new PushNotification<NotificationPushNotification>\n        {\n            Type = PushType.Notification,\n            Target = target,\n            TargetId = targetId,\n            Payload = message,\n            ExcludeCurrentContext = true,\n            ClientType = notification.ClientType,\n        });\n    }\n\n    Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)\n    {\n        var message = new NotificationPushNotification\n        {\n            Id = notification.Id,\n            Priority = notification.Priority,\n            Global = notification.Global,\n            ClientType = notification.ClientType,\n            UserId = notification.UserId,\n            OrganizationId = notification.OrganizationId,\n#pragma warning disable BWP0001 // Type or member is obsolete\n            InstallationId = notification.Global ? InstallationId : null,\n#pragma warning restore BWP0001 // Type or member is obsolete\n            TaskId = notification.TaskId,\n            Title = notification.Title,\n            Body = notification.Body,\n            CreationDate = notification.CreationDate,\n            RevisionDate = notification.RevisionDate,\n            ReadDate = notificationStatus.ReadDate,\n            DeletedDate = notificationStatus.DeletedDate,\n        };\n\n        NotificationTarget target;\n        Guid targetId;\n\n        if (notification.Global)\n        {\n            // TODO: Think about this a bit more\n            target = NotificationTarget.Installation;\n#pragma warning disable BWP0001 // Type or member is obsolete\n            targetId = InstallationId;\n#pragma warning restore BWP0001 // Type or member is obsolete\n        }\n        else if (notification.UserId.HasValue)\n        {\n            target = NotificationTarget.User;\n            targetId = notification.UserId.Value;\n        }\n        else if (notification.OrganizationId.HasValue)\n        {\n            target = NotificationTarget.Organization;\n            targetId = notification.OrganizationId.Value;\n        }\n        else\n        {\n#pragma warning disable BWP0001 // Type or member is obsolete\n            Logger.LogWarning(\"Invalid notification status id {NotificationId} push notification\", notification.Id);\n#pragma warning restore BWP0001 // Type or member is obsolete\n            return Task.CompletedTask;\n        }\n\n        return PushAsync(new PushNotification<NotificationPushNotification>\n        {\n            Type = PushType.NotificationStatus,\n            Target = target,\n            TargetId = targetId,\n            Payload = message,\n            ExcludeCurrentContext = true,\n            ClientType = notification.ClientType,\n        });\n    }\n\n    Task PushAuthRequestAsync(AuthRequest authRequest)\n        => PushAsync(new PushNotification<AuthRequestPushNotification>\n        {\n            Type = PushType.AuthRequest,\n            Target = NotificationTarget.User,\n            TargetId = authRequest.UserId,\n            Payload = new AuthRequestPushNotification\n            {\n                Id = authRequest.Id,\n                UserId = authRequest.UserId,\n            },\n            ExcludeCurrentContext = true,\n        });\n\n    Task PushAuthRequestResponseAsync(AuthRequest authRequest)\n        => PushAsync(new PushNotification<AuthRequestPushNotification>\n        {\n            Type = PushType.AuthRequestResponse,\n            Target = NotificationTarget.User,\n            TargetId = authRequest.UserId,\n            Payload = new AuthRequestPushNotification\n            {\n                Id = authRequest.Id,\n                UserId = authRequest.UserId,\n            },\n            ExcludeCurrentContext = true,\n        });\n\n    Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization)\n        => PushAsync(new PushNotification<OrganizationCollectionManagementPushNotification>\n        {\n            Type = PushType.SyncOrganizationCollectionSettingChanged,\n            Target = NotificationTarget.Organization,\n            TargetId = organization.Id,\n            Payload = new OrganizationCollectionManagementPushNotification\n            {\n                OrganizationId = organization.Id,\n                LimitCollectionCreation = organization.LimitCollectionCreation,\n                LimitCollectionDeletion = organization.LimitCollectionDeletion,\n                LimitItemDeletion = organization.LimitItemDeletion,\n            },\n            ExcludeCurrentContext = false,\n        });\n\n    Task PushRefreshSecurityTasksAsync(Guid userId)\n        => PushAsync(new PushNotification<UserPushNotification>\n        {\n            Type = PushType.RefreshSecurityTasks,\n            Target = NotificationTarget.User,\n            TargetId = userId,\n            Payload = new UserPushNotification\n            {\n                UserId = userId,\n#pragma warning disable BWP0001 // Type or member is obsolete\n                Date = TimeProvider.GetUtcNow().UtcDateTime,\n#pragma warning restore BWP0001 // Type or member is obsolete\n            },\n            ExcludeCurrentContext = false,\n        });\n    #endregion\n\n    Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds);\n\n    /// <summary>\n    /// Pushes a notification to devices based on the settings given to us in <see cref=\"PushNotification{T}\"/>.\n    /// </summary>\n    /// <typeparam name=\"T\">The type of the payload to be sent along with the notification.</typeparam>\n    /// <param name=\"pushNotification\"></param>\n    /// <returns>A task that is NOT guarunteed to have sent the notification by the time the task resolves.</returns>\n    Task PushAsync<T>(PushNotification<T> pushNotification)\n        where T : class;\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/IPushRelayer.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\n/// <summary>\n/// An object encapsulating the information that is available in a notification\n/// given to us from a self-hosted installation.\n/// </summary>\npublic class RelayedNotification\n{\n    /// <inheritdoc cref=\"PushNotification{T}.Type\"/>\n    public required PushType Type { get; init; }\n    /// <inheritdoc cref=\"PushNotification{T}.Target\"/>\n    public required NotificationTarget Target { get; init; }\n    /// <inheritdoc cref=\"PushNotification{T}.TargetId\"/>\n    public required Guid TargetId { get; init; }\n    /// <inheritdoc cref=\"PushNotification{T}.Payload\"/>\n    public required JsonElement Payload { get; init; }\n    /// <inheritdoc cref=\"PushNotification{T}.ClientType\"/>\n    public required ClientType? ClientType { get; init; }\n    public required Guid? DeviceId { get; init; }\n    public required string? Identifier { get; init; }\n}\n\n/// <summary>\n/// A service for taking a notification that was relayed to us from a self-hosted installation and\n/// will be injested into our infrastructure so that we can get the notification to devices that require\n/// cloud interaction.\n/// </summary>\n/// <remarks>\n/// This interface should be treated as internal and not consumed by other teams.\n/// </remarks>\npublic interface IPushRelayer\n{\n    /// <summary>\n    /// Relays a notification that was received from an authenticated installation into our cloud push notification infrastructure.\n    /// </summary>\n    /// <param name=\"fromInstallation\">The authenticated installation this notification came from.</param>\n    /// <param name=\"relayedNotification\">The information received from the self-hosted installation.</param>\n    Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification);\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/NotificationHub/INotificationHubClientProxy.cs",
    "content": "﻿using Microsoft.Azure.NotificationHubs;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\npublic interface INotificationHubProxy\n{\n    Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression);\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/NotificationHub/INotificationHubPool.cs",
    "content": "﻿using Microsoft.Azure.NotificationHubs;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\npublic interface INotificationHubPool\n{\n    NotificationHubConnection ConnectionFor(Guid comb);\n    INotificationHubClient ClientFor(Guid comb);\n    INotificationHubProxy AllClients { get; }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/NotificationHub/NotificationHubClientProxy.cs",
    "content": "﻿using Microsoft.Azure.NotificationHubs;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\npublic class NotificationHubClientProxy : INotificationHubProxy\n{\n    private readonly IEnumerable<INotificationHubClient> _clients;\n\n    public NotificationHubClientProxy(IEnumerable<INotificationHubClient> clients)\n    {\n        _clients = clients;\n    }\n\n    private async Task<(INotificationHubClient, T)[]> ApplyToAllClientsAsync<T>(Func<INotificationHubClient, Task<T>> action)\n    {\n        var tasks = _clients.Select(async c => (c, await action(c)));\n        return await Task.WhenAll(tasks);\n    }\n\n    // partial proxy of INotificationHubClient implementation\n    // Note: Any other methods that are needed can simply be delegated as done here.\n    public async Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression)\n    {\n        return await ApplyToAllClientsAsync(async c => await c.SendTemplateNotificationAsync(properties, tagExpression));\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/NotificationHub/NotificationHubConnection.cs",
    "content": "﻿using System.Diagnostics.CodeAnalysis;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Web;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Azure.NotificationHubs;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\npublic class NotificationHubConnection\n{\n    public string? HubName { get; init; }\n    public string? ConnectionString { get; init; }\n    private Lazy<NotificationHubConnectionStringBuilder> _parsedConnectionString;\n    public Uri Endpoint => _parsedConnectionString.Value.Endpoint;\n    private string SasKey => _parsedConnectionString.Value.SharedAccessKey;\n    private string SasKeyName => _parsedConnectionString.Value.SharedAccessKeyName;\n    public bool EnableSendTracing { get; init; }\n    private NotificationHubClient? _hubClient;\n    /// <summary>\n    /// Gets the NotificationHubClient for this connection.\n    ///\n    /// If the client is null, it will be initialized.\n    ///\n    /// <throws>Exception</throws> if the connection is invalid.\n    /// </summary>\n    public NotificationHubClient HubClient\n    {\n        get\n        {\n            if (_hubClient == null)\n            {\n                if (!IsValid)\n                {\n                    throw new Exception(\"Invalid notification hub settings\");\n                }\n                Init();\n            }\n            return _hubClient;\n        }\n        private set\n        {\n            _hubClient = value;\n        }\n    }\n    /// <summary>\n    /// Gets the start date for registration.\n    ///\n    /// If null, registration is always disabled.\n    /// </summary>\n    public DateTime? RegistrationStartDate { get; init; }\n    /// <summary>\n    /// Gets the end date for registration.\n    ///\n    /// If null, registration has no end date.\n    /// </summary>\n    public DateTime? RegistrationEndDate { get; init; }\n    /// <summary>\n    /// Gets whether all data needed to generate a connection to Notification Hub is present.\n    /// </summary>\n    public bool IsValid\n    {\n        get\n        {\n            {\n                var invalid = string.IsNullOrWhiteSpace(HubName) || string.IsNullOrWhiteSpace(ConnectionString);\n                return !invalid;\n            }\n        }\n    }\n\n    public string LogString\n    {\n        get\n        {\n            return $\"HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}\";\n        }\n    }\n\n    /// <summary>\n    /// Gets whether registration is enabled for the given comb ID.\n    /// This is based off of the generation time encoded in the comb ID.\n    /// </summary>\n    /// <param name=\"comb\"></param>\n    /// <returns></returns>\n    public bool RegistrationEnabled(Guid comb)\n    {\n        var combTime = CoreHelpers.DateFromComb(comb);\n        return RegistrationEnabled(combTime);\n    }\n\n    /// <summary>\n    /// Gets whether registration is enabled for the given time.\n    /// </summary>\n    /// <param name=\"queryTime\">The time to check</param>\n    /// <returns></returns>\n    public bool RegistrationEnabled(DateTime queryTime)\n    {\n        if (queryTime >= RegistrationEndDate || RegistrationStartDate == null)\n        {\n            return false;\n        }\n\n        return RegistrationStartDate < queryTime;\n    }\n\n    public HttpRequestMessage CreateRequest(HttpMethod method, string pathUri, params string[] queryParameters)\n    {\n        var uriBuilder = new UriBuilder(Endpoint)\n        {\n            Scheme = \"https\",\n            Path = $\"{HubName}/{pathUri.TrimStart('/')}\",\n            Query = string.Join('&', [.. queryParameters, \"api-version=2015-01\"]),\n        };\n\n        var result = new HttpRequestMessage(method, uriBuilder.Uri);\n        result.Headers.Add(\"Authorization\", GenerateSasToken(uriBuilder.Uri));\n        result.Headers.Add(\"TrackingId\", Guid.NewGuid().ToString());\n        return result;\n    }\n\n    private string GenerateSasToken(Uri uri)\n    {\n        string targetUri = Uri.EscapeDataString(uri.ToString().ToLower()).ToLower();\n        long expires = DateTime.UtcNow.AddMinutes(1).Ticks / TimeSpan.TicksPerSecond;\n        string stringToSign = targetUri + \"\\n\" + expires;\n\n        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(SasKey)))\n        {\n            var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));\n            return $\"SharedAccessSignature sr={targetUri}&sig={HttpUtility.UrlEncode(signature)}&se={expires}&skn={SasKeyName}\";\n        }\n    }\n\n    private NotificationHubConnection()\n    {\n        _parsedConnectionString = new(() => new NotificationHubConnectionStringBuilder(ConnectionString));\n    }\n\n    /// <summary>\n    /// Creates a new NotificationHubConnection from the given settings.\n    /// </summary>\n    /// <param name=\"settings\"></param>\n    /// <returns></returns>\n    public static NotificationHubConnection From(GlobalSettings.NotificationHubSettings settings)\n    {\n        return new()\n        {\n            HubName = settings.HubName,\n            ConnectionString = settings.ConnectionString,\n            EnableSendTracing = settings.EnableSendTracing,\n            // Comb time is not precise enough for millisecond accuracy\n            RegistrationStartDate = settings.RegistrationStartDate.HasValue ? Truncate(settings.RegistrationStartDate.Value, TimeSpan.FromMilliseconds(10)) : null,\n            RegistrationEndDate = settings.RegistrationEndDate\n        };\n    }\n\n    [MemberNotNull(nameof(_hubClient))]\n    private NotificationHubConnection Init()\n    {\n        _hubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing);\n        return this;\n    }\n\n    private static DateTime Truncate(DateTime dateTime, TimeSpan resolution)\n    {\n        return dateTime.AddTicks(-(dateTime.Ticks % resolution.Ticks));\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/NotificationHub/NotificationHubPool.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Azure.NotificationHubs;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\npublic class NotificationHubPool : INotificationHubPool\n{\n    private List<NotificationHubConnection> _connections { get; }\n    private readonly IEnumerable<INotificationHubClient> _clients;\n    private readonly ILogger<NotificationHubPool> _logger;\n    public NotificationHubPool(ILogger<NotificationHubPool> logger, GlobalSettings globalSettings)\n    {\n        _logger = logger;\n        _connections = FilterInvalidHubs(globalSettings.NotificationHubPool.NotificationHubs);\n        _clients = _connections.GroupBy(c => c.ConnectionString).Select(g => g.First().HubClient);\n    }\n\n    private List<NotificationHubConnection> FilterInvalidHubs(IEnumerable<GlobalSettings.NotificationHubSettings> hubs)\n    {\n        List<NotificationHubConnection> result = new();\n        _logger.LogDebug(\"Filtering {HubCount} notification hubs\", hubs.Count());\n        foreach (var hub in hubs)\n        {\n            var connection = NotificationHubConnection.From(hub);\n            if (!connection.IsValid)\n            {\n                _logger.LogWarning(\"Invalid notification hub settings: {HubName}\", hub.HubName ?? \"hub name missing\");\n                continue;\n            }\n            _logger.LogDebug(\"Adding notification hub: {ConnectionLogString}\", connection.LogString);\n            result.Add(connection);\n        }\n\n        return result;\n    }\n\n\n    /// <summary>\n    /// Gets the NotificationHubClient for the given comb ID.\n    /// </summary>\n    /// <param name=\"comb\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"InvalidOperationException\">Thrown when no notification hub is found for a given comb.</exception>\n    public INotificationHubClient ClientFor(Guid comb)\n    {\n        var resolvedConnection = ConnectionFor(comb);\n        return resolvedConnection.HubClient;\n    }\n\n    /// <summary>\n    /// Gets the NotificationHubConnection for the given comb ID.\n    /// </summary>\n    /// <param name=\"comb\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"InvalidOperationException\">Thrown when no notification hub is found for a given comb.</exception>\n    public NotificationHubConnection ConnectionFor(Guid comb)\n    {\n        var possibleConnections = _connections.Where(c => c.RegistrationEnabled(comb)).ToArray();\n        if (possibleConnections.Length == 0)\n        {\n            throw new InvalidOperationException($\"No valid notification hubs are available for the given comb ({comb}).\\n\" +\n                $\"The comb's datetime is {CoreHelpers.DateFromComb(comb)}.\" +\n                $\"Hub start and end times are configured as follows:\\n\" +\n                string.Join(\"\\n\", _connections.Select(c => $\"Hub {c.HubName} - Start: {c.RegistrationStartDate}, End: {c.RegistrationEndDate}\")));\n        }\n        var resolvedConnection = possibleConnections[CoreHelpers.BinForComb(comb, possibleConnections.Length)];\n        _logger.LogTrace(\"Resolved notification hub for comb {Comb} out of {HubCount} hubs.\\n{ConnectionInfo}\", comb, possibleConnections.Length, resolvedConnection.LogString);\n        return resolvedConnection;\n\n    }\n\n    public INotificationHubProxy AllClients { get { return new NotificationHubClientProxy(_clients); } }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/NotificationHub/NotificationHubPushEngine.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.RegularExpressions;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.Push.Internal;\n\n/// <summary>\n/// Sends mobile push notifications to the Azure Notification Hub.\n/// Used by Cloud-Hosted environments.\n/// Received by Firebase for Android or APNS for iOS.\n/// </summary>\npublic class NotificationHubPushEngine : IPushEngine, IPushRelayer\n{\n    private readonly IInstallationDeviceRepository _installationDeviceRepository;\n    private readonly IHttpContextAccessor _httpContextAccessor;\n    private readonly bool _enableTracing = false;\n    private readonly INotificationHubPool _notificationHubPool;\n    private readonly ILogger _logger;\n\n    public NotificationHubPushEngine(\n        IInstallationDeviceRepository installationDeviceRepository,\n        INotificationHubPool notificationHubPool,\n        IHttpContextAccessor httpContextAccessor,\n        ILogger<NotificationHubPushEngine> logger,\n        IGlobalSettings globalSettings)\n    {\n        _installationDeviceRepository = installationDeviceRepository;\n        _httpContextAccessor = httpContextAccessor;\n        _notificationHubPool = notificationHubPool;\n        _logger = logger;\n        if (globalSettings.Installation.Id == Guid.Empty)\n        {\n            logger.LogWarning(\"Installation ID is not set. Push notifications for installations will not work.\");\n        }\n    }\n\n    public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)\n    {\n        if (cipher.OrganizationId.HasValue)\n        {\n            // We cannot send org pushes since access logic is much more complicated than just the fact that they belong\n            // to the organization. Potentially we could blindly send to just users that have the access all permission\n            // device registration needs to be more granular to handle that appropriately. A more brute force approach could\n            // me to send \"full sync\" push to all org users, but that has the potential to DDOS the API in bursts.\n\n            // await SendPayloadToOrganizationAsync(cipher.OrganizationId.Value, type, message, true);\n        }\n        else if (cipher.UserId.HasValue)\n        {\n            var message = new SyncCipherPushNotification\n            {\n                Id = cipher.Id,\n                UserId = cipher.UserId,\n                OrganizationId = cipher.OrganizationId,\n                RevisionDate = cipher.RevisionDate,\n                CollectionIds = collectionIds,\n            };\n\n            await PushAsync(new PushNotification<SyncCipherPushNotification>\n            {\n                Type = type,\n                Target = NotificationTarget.User,\n                TargetId = cipher.UserId.Value,\n                Payload = message,\n                ExcludeCurrentContext = true,\n            });\n        }\n    }\n\n    private string? GetContextIdentifier(bool excludeCurrentContext)\n    {\n        if (!excludeCurrentContext)\n        {\n            return null;\n        }\n\n        var currentContext =\n            _httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;\n        return currentContext?.DeviceIdentifier;\n    }\n\n    private string BuildTag(string tag, string? identifier, ClientType? clientType)\n    {\n        if (!string.IsNullOrWhiteSpace(identifier))\n        {\n            tag += $\" && !deviceIdentifier:{SanitizeTagInput(identifier)}\";\n        }\n\n        if (clientType.HasValue && clientType.Value != ClientType.All)\n        {\n            tag += $\" && clientType:{clientType}\";\n        }\n\n        return $\"({tag})\";\n    }\n\n    public async Task PushAsync<T>(PushNotification<T> pushNotification)\n        where T : class\n    {\n        var initialTag = pushNotification.Target switch\n        {\n            NotificationTarget.User => $\"template:payload_userId:{pushNotification.TargetId}\",\n            NotificationTarget.Organization => $\"template:payload && organizationId:{pushNotification.TargetId}\",\n            NotificationTarget.Installation => $\"template:payload && installationId:{pushNotification.TargetId}\",\n            _ => throw new InvalidOperationException($\"Push notification target '{pushNotification.Target}' is not valid.\"),\n        };\n\n        await PushCoreAsync(\n            initialTag,\n            GetContextIdentifier(pushNotification.ExcludeCurrentContext),\n            pushNotification.Type,\n            pushNotification.ClientType,\n            pushNotification.Payload\n        );\n    }\n\n    public async Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification)\n    {\n        // Relayed notifications need identifiers prefixed with the installation they are from and a underscore\n        var initialTag = relayedNotification.Target switch\n        {\n            NotificationTarget.User => $\"template:payload_userId:{fromInstallation}_{relayedNotification.TargetId}\",\n            NotificationTarget.Organization => $\"template:payload && organizationId:{fromInstallation}_{relayedNotification.TargetId}\",\n            NotificationTarget.Installation => $\"template:payload && installationId:{fromInstallation}\",\n            _ => throw new InvalidOperationException($\"Invalid Notification target {relayedNotification.Target}\"),\n        };\n\n        await PushCoreAsync(\n            initialTag,\n            relayedNotification.Identifier,\n            relayedNotification.Type,\n            relayedNotification.ClientType,\n            relayedNotification.Payload\n        );\n\n        if (relayedNotification.DeviceId.HasValue)\n        {\n            await _installationDeviceRepository.UpsertAsync(\n                new InstallationDeviceEntity(fromInstallation, relayedNotification.DeviceId.Value)\n            );\n        }\n        else\n        {\n            _logger.LogWarning(\n                \"A related notification of type '{Type}' came through without a device id from installation {Installation}\",\n                relayedNotification.Type,\n                fromInstallation\n            );\n        }\n    }\n\n    private async Task PushCoreAsync<T>(string initialTag, string? contextId, PushType pushType, ClientType? clientType, T payload)\n    {\n        var finalTag = BuildTag(initialTag, contextId, clientType);\n\n        var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(\n            new Dictionary<string, string>\n            {\n                { \"type\", ((byte)pushType).ToString() },\n                { \"payload\", JsonSerializer.Serialize(payload) },\n            },\n            finalTag\n        );\n\n        if (_enableTracing)\n        {\n            foreach (var (client, outcome) in results)\n            {\n                if (!client.EnableTestSend)\n                {\n                    continue;\n                }\n\n                _logger.LogInformation(\n                    \"Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}\",\n                    outcome.TrackingId, pushType, outcome.Success, outcome.Failure, payload, outcome.Results);\n            }\n        }\n    }\n\n    private string SanitizeTagInput(string input)\n    {\n        // Only allow a-z, A-Z, 0-9, and special characters -_:\n        return Regex.Replace(input, \"[^a-zA-Z0-9-_:]\", string.Empty);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/NotificationInfoAttribute.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.Platform.Push;\n\n/// <summary>\n/// Used to annotate information about a given <see cref=\"PushType\"/>.\n/// </summary>\n[AttributeUsage(AttributeTargets.Field)]\npublic class NotificationInfoAttribute : Attribute\n{\n    // Once upon a time we can feed this information into a C# analyzer to make sure that we validate\n    // the callsites of IPushNotificationService.PushAsync uses the correct payload type for the notification type\n    // for now this only exists as forced documentation to teams who create a push type.\n\n    // It's especially on purpose that we allow ourselves to take a type name via just the string,\n    // this allows teams to make a push type that is only sent with a payload that exists in a separate assembly than\n    // this one.\n\n    public NotificationInfoAttribute(string team, Type payloadType)\n        // It should be impossible to reference an unnamed type for an attributes constructor so this assertion should be safe.\n        : this(team, payloadType.FullName!)\n    {\n        Team = team;\n    }\n\n    public NotificationInfoAttribute(string team, string payloadTypeName)\n    {\n        ArgumentException.ThrowIfNullOrWhiteSpace(team);\n        ArgumentException.ThrowIfNullOrWhiteSpace(payloadTypeName);\n\n        Team = team;\n        PayloadTypeName = payloadTypeName;\n    }\n\n    /// <summary>\n    /// The name of the team that owns this <see cref=\"PushType\"/>.\n    /// </summary>\n    public string Team { get; }\n\n    /// <summary>\n    /// The fully qualified type name of the payload that should be used when sending a notification of this type.\n    /// </summary>\n    public string PayloadTypeName { get; }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/NotificationTarget.cs",
    "content": "﻿namespace Bit.Core.Platform.Push;\n\n/// <summary>\n/// Contains constants for all the available targets for a given notification.\n/// </summary>\n/// <remarks>\n/// Please reach out to the Platform team if you need a new target added.\n/// </remarks>\npublic enum NotificationTarget\n{\n    /// <summary>\n    /// The target for the notification is a single user.\n    /// </summary>\n    User,\n    /// <summary>\n    /// The target for the notification are all the users in an organization.\n    /// </summary>\n    Organization,\n    /// <summary>\n    /// The target for the notification are all the organizations, \n    /// and all the users in that organization for a installation.\n    /// </summary>\n    Installation,\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/PushNotification.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.Platform.Push;\n\n/// <summary>\n/// An object containing all the information required for getting a notification\n/// to an end users device and the information you want available to that device.\n/// </summary>\n/// <typeparam name=\"T\">The type of the payload. This type is expected to be able to be roundtripped as JSON.</typeparam>\npublic record PushNotification<T>\n    where T : class\n{\n    /// <summary>\n    /// The <see cref=\"PushType\"/> to be associated with the notification. This is used to route\n    /// the notification to the correct handler on the client side. Be sure to use the correct payload\n    /// type for the associated <see cref=\"PushType\"/>. \n    /// </summary>\n    public required PushType Type { get; init; }\n\n    /// <summary>\n    /// The target entity type for the notification.\n    /// </summary>\n    /// <remarks>\n    /// When the target type is <see cref=\"NotificationTarget.User\"/> the <see cref=\"TargetId\"/> \n    /// property is expected to be a users ID. When it is <see cref=\"NotificationTarget.Organization\"/>\n    /// it should be an organizations id. When it is a <see cref=\"NotificationTarget.Installation\"/> \n    /// it should be an installation id.\n    /// </remarks>\n    public required NotificationTarget Target { get; init; }\n\n    /// <summary>\n    /// The indentifier for the given <see cref=\"Target\"/>.\n    /// </summary>\n    public required Guid TargetId { get; init; }\n\n    /// <summary>\n    /// The payload to be sent with the notification. This object will be JSON serialized.\n    /// </summary>\n    public required T Payload { get; init; }\n\n    /// <summary>\n    /// When <see langword=\"true\"/> the notification will not include the current context identifier on it, this\n    /// means that the notification may get handled on the device that this notification could have originated from.\n    /// </summary>\n    public required bool ExcludeCurrentContext { get; init; }\n\n    /// <summary>\n    /// The type of clients the notification should be sent to, if <see langword=\"null\"/> then \n    /// <see cref=\"ClientType.All\"/> is inferred.\n    /// </summary>\n    public ClientType? ClientType { get; init; }\n\n    internal Guid? GetTargetWhen(NotificationTarget notificationTarget)\n    {\n        return Target == notificationTarget ? TargetId : null;\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/PushServiceCollectionExtensions.cs",
    "content": "﻿using Azure.Storage.Queues;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\n/// <summary>\n/// Extension methods for adding the Push feature.\n/// </summary>\npublic static class PushServiceCollectionExtensions\n{\n    /// <summary>\n    /// Adds a <see cref=\"IPushNotificationService\"/> to the services that can be used to send push notifications to\n    /// end user devices. This method is safe to be ran multiple time provided <see cref=\"GlobalSettings\"/> does not \n    /// change between calls.\n    /// </summary>\n    /// <param name=\"services\">The <see cref=\"IServiceCollection\"/> to add services to.</param>\n    /// <param name=\"globalSettings\">The <see cref=\"GlobalSettings\"/> to use to configure services.</param>\n    /// <returns>The <see cref=\"IServiceCollection\"/> for additional chaining.</returns>\n    public static IServiceCollection AddPush(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        ArgumentNullException.ThrowIfNull(services);\n        ArgumentNullException.ThrowIfNull(globalSettings);\n\n        services.TryAddSingleton(TimeProvider.System);\n        services.TryAddSingleton<IPushNotificationService, MultiServicePushNotificationService>();\n\n        if (globalSettings.SelfHosted)\n        {\n            if (globalSettings.Installation.Id == Guid.Empty)\n            {\n                throw new InvalidOperationException(\"Installation Id must be set for self-hosted installations.\");\n            }\n\n            if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&\n                CoreHelpers.SettingHasValue(globalSettings.Installation.Key))\n            {\n                // TODO: We should really define the HttpClient we will use here\n                services.AddHttpClient();\n                services.AddHttpContextAccessor();\n                // We also depend on IDeviceRepository but don't explicitly add it right now.\n                services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, RelayPushEngine>());\n            }\n\n            if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&\n                CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))\n            {\n                // TODO: We should really define the HttpClient we will use here\n                services.AddHttpClient();\n                services.AddHttpContextAccessor();\n                services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, NotificationsApiPushEngine>());\n            }\n        }\n        else\n        {\n            services.TryAddSingleton<INotificationHubPool, NotificationHubPool>();\n            services.AddHttpContextAccessor();\n\n            // We also depend on IInstallationDeviceRepository but don't explicitly add it right now.\n            services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, NotificationHubPushEngine>());\n\n            services.TryAddSingleton<IPushRelayer, NotificationHubPushEngine>();\n\n            if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))\n            {\n                services.TryAddKeyedSingleton(\"notifications\", static (sp, _) =>\n                {\n                    var gs = sp.GetRequiredService<GlobalSettings>();\n                    return new QueueClient(gs.Notifications.ConnectionString, \"notifications\");\n                });\n\n                // We not IHttpContextAccessor will be added above, no need to do it here.\n                services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, AzureQueuePushEngine>());\n            }\n        }\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/PushType.cs",
    "content": "﻿using Bit.Core.Platform.Push;\n\n// TODO: This namespace should change to `Bit.Core.Platform.Push`\nnamespace Bit.Core.Enums;\n\n/// <summary>\n///\n/// </summary>\n/// <remarks>\n/// <para>\n/// When adding a new enum member you must annotate it with a <see cref=\"NotificationInfoAttribute\"/>\n/// this is enforced with a unit test. It is preferred that you do NOT add new usings for the type referenced\n/// in <see cref=\"NotificationInfoAttribute\"/>.\n/// </para>\n/// <para>\n/// You may and are\n/// </para>\n/// </remarks>\npublic enum PushType : byte\n{\n    // When adding a new enum member you must annotate it with a NotificationInfoAttribute  this is enforced with a unit\n    // test. It is preferred that you do NOT add new usings for the type referenced for the payload. You are also\n    // encouraged to define the payload type in your own teams owned code.\n\n    [NotificationInfo(\"@bitwarden/team-vault-dev\", typeof(Models.SyncCipherPushNotification))]\n    SyncCipherUpdate = 0,\n\n    [NotificationInfo(\"@bitwarden/team-vault-dev\", typeof(Models.SyncCipherPushNotification))]\n    SyncCipherCreate = 1,\n\n    [NotificationInfo(\"@bitwarden/team-vault-dev\", typeof(Models.SyncCipherPushNotification))]\n    SyncLoginDelete = 2,\n\n    [NotificationInfo(\"@bitwarden/team-vault-dev\", typeof(Models.SyncFolderPushNotification))]\n    SyncFolderDelete = 3,\n\n    [NotificationInfo(\"@bitwarden/team-vault-dev\", typeof(Models.UserPushNotification))]\n    SyncCiphers = 4,\n\n    [NotificationInfo(\"not-specified\", typeof(Models.UserPushNotification))]\n    SyncVault = 5,\n\n    [NotificationInfo(\"@bitwarden/team-admin-console-dev\", typeof(Models.UserPushNotification))]\n    SyncOrgKeys = 6,\n\n    [NotificationInfo(\"@bitwarden/team-vault-dev\", typeof(Models.SyncFolderPushNotification))]\n    SyncFolderCreate = 7,\n\n    [NotificationInfo(\"@bitwarden/team-vault-dev\", typeof(Models.SyncFolderPushNotification))]\n    SyncFolderUpdate = 8,\n\n    [NotificationInfo(\"@bitwarden/team-vault-dev\", typeof(Models.SyncCipherPushNotification))]\n    SyncCipherDelete = 9,\n\n    [NotificationInfo(\"not-specified\", typeof(Models.UserPushNotification))]\n    SyncSettings = 10,\n\n    [NotificationInfo(\"not-specified\", typeof(Models.LogOutPushNotification))]\n    LogOut = 11,\n\n    [NotificationInfo(\"@bitwarden/team-tools-dev\", typeof(Models.SyncSendPushNotification))]\n    SyncSendCreate = 12,\n\n    [NotificationInfo(\"@bitwarden/team-tools-dev\", typeof(Models.SyncSendPushNotification))]\n    SyncSendUpdate = 13,\n\n    [NotificationInfo(\"@bitwarden/team-tools-dev\", typeof(Models.SyncSendPushNotification))]\n    SyncSendDelete = 14,\n\n    [NotificationInfo(\"@bitwarden/team-auth-dev\", typeof(Models.AuthRequestPushNotification))]\n    AuthRequest = 15,\n\n    [NotificationInfo(\"@bitwarden/team-auth-dev\", typeof(Models.AuthRequestPushNotification))]\n    AuthRequestResponse = 16,\n\n    [NotificationInfo(\"not-specified\", typeof(Models.UserPushNotification))]\n    SyncOrganizations = 17,\n\n    [NotificationInfo(\"@bitwarden/team-billing-dev\", typeof(Models.OrganizationStatusPushNotification))]\n    SyncOrganizationStatusChanged = 18,\n\n    [NotificationInfo(\"@bitwarden/team-admin-console-dev\", typeof(Models.OrganizationCollectionManagementPushNotification))]\n    SyncOrganizationCollectionSettingChanged = 19,\n\n    [NotificationInfo(\"not-specified\", typeof(Models.NotificationPushNotification))]\n    Notification = 20,\n\n    [NotificationInfo(\"not-specified\", typeof(Models.NotificationPushNotification))]\n    NotificationStatus = 21,\n\n    [NotificationInfo(\"@bitwarden/team-vault-dev\", typeof(Models.UserPushNotification))]\n    RefreshSecurityTasks = 22,\n\n    [NotificationInfo(\"@bitwarden/team-billing-dev\", typeof(Models.ProviderBankAccountVerifiedPushNotification))]\n    OrganizationBankAccountVerified = 23,\n\n    [NotificationInfo(\"@bitwarden/team-billing-dev\", typeof(Models.ProviderBankAccountVerifiedPushNotification))]\n    ProviderBankAccountVerified = 24,\n\n    [NotificationInfo(\"@bitwarden/team-admin-console-dev\", typeof(Models.SyncPolicyPushNotification))]\n    PolicyChanged = 25,\n\n    [NotificationInfo(\"@bitwarden/team-admin-console-dev\", typeof(Models.AutoConfirmPushNotification))]\n    AutoConfirm = 26,\n\n    [NotificationInfo(\"@bitwarden/team-billing-dev\", typeof(Billing.Models.PremiumStatusPushNotification))]\n    PremiumStatusChanged = 27,\n}\n"
  },
  {
    "path": "src/Core/Platform/Push/README.md",
    "content": "# Push\n\n## About\n\nPush is a feature for sending packets of information to end user devices. This can be useful for\ntelling the device that there is new information that it should request or that the request they\ncreated was just accepted.\n\n## Usage\n\nThe general usage will be to call `Bit.Core.Platform.Push.IPushNotificationService.PushAsync`. That\nmethod takes a `PushNotification<T>`.\n\n```c#\n// This would send a notification to all the devices of the given `userId`.\nawait pushNotificationService.PushAsync(new PushNotification<MyPayload>\n{\n    Type = PushType.MyNotificationType,\n    Target = NotificationTarget.User,\n    TargetId = userId,\n    Payload = new MyPayload\n    {\n        Message = \"Request accepted\",\n    },\n    ExcludeCurrentContext = false,\n});\n```\n\n## Extending\n\nIf you want to extend this framework for sending your own notification type you do so by adding a\nnew enum member to the [`PushType`](./PushType.cs) enum. Assign a number to it that is 1 above the\nnext highest value. You must then annotate that enum member with a\n[`[NotificationInfo]`](./NotificationInfoAttribute.cs) attribute to inform others who the owning\nteam and expected payload type are. Then you may inject\n[`IPushNotificationService`](./IPushNotificationService.cs) into your own service and call its\n`PushAsync` method.\n\nYou also need to add code to [`HubHelpers`](../../../Notifications/HubHelpers.cs) to read your\npayload body and select the appropriate group or user to send the notification to.\n\nYou should NOT add tests for your specific notification type in any of the `IPushEngine`\nimplementations. They do currently have tests for many of the notification types but those will\neventually be deleted and no new ones need to be added.\n\nSince notifications are relayed through our cloud instance for self hosted users (if they opt in)\nit's important to try and keep the information in the notification payload minimal. It's generally\nbest to send a notification with IDs for any entities involved, which mean nothing to our cloud but\ncan then be used to get more detailed information once the notification is received on the device.\n\n## Implementations\n\nThe implementation of push notifications scatters push notification requests to all `IPushEngine`s\nthat have been registered in DI for the current application. In release builds, this service does\nNOT await the underlying engines to make sure that the notification has arrived at its destination\nbefore its returned task completes.\n\n### Azure Notification Hub\n\nUsed when the application is hosted by Bitwarden in the cloud. This sends the notification to the\nconfigured [Azure Notification Hub (ANH)](https://learn.microsoft.com/en-us/azure/notification-hubs/notification-hubs-push-notification-overview),\nwhich we currently rely on for sending notifications to:\n\n- Our mobile clients, through the Notification Hub federation with mobile app notification systems\n- Our clients configured to use Web Push (currently the Chrome Extension).\n\nThis implementation is always assumed to have available configuration when running in the cloud.\n\n### Azure Queue\n\nUsed when the application is hosted by Bitwarden in the cloud, to send the notification over web\nsockets (SignalR). This sends the notification to a Azure Queue. That queue is then consumed in our\nNotifications service, where the notification is sent to a SignalR hub so that our clients connected\nthrough a persistent web socket to our notifications service get the notification.\n\nThis implementation is registered in DI when `GlobalSettings:Notifications:ConnectionString` is set\nto a value.\n\n### Relay\n\nUsed when the application is being self-hosted. This relays a notification from the self-hosted\ninstance to a cloud instance. The notification is received by our cloud and then relayed to\nAzure Notification Hub. This is necessary because self-hosted instance aren't able to directly send\nnotifications to mobile devices.\n\nThis instance is registered in DI when `GlobalSettings:PushRelayBaseUri` and\n`GlobalSettings:Installation:Key` are available.\n\n### Notifications API\n\nUsed when the application is being self-hosted. This sends a API request to the self-hosted instance\nof the Notifications service. The Notifications service receives the request and then sends the\nnotification through the SignalR hub. This is very similar to cloud using an Azure Queue but it\ndoesn't require the self-hosted customer to run their own queuing infrastructure.\n\nThis instance is registered when `GlobalSettings:InternalIdentityKey` and\n`GlobalSettings:BaseServiceUri:InternalNotifications` are set. Both of these settings are usually\nset automatically in supported Bitwarden setups.\n\n## Adding new notification targets\n\n[`NotificationTarget`](./NotificationTarget.cs) is an enum that defines the possible targets for a\nnotification, `IPushEngine` implementations may or may not need to be aware and have special\nconsiderations for each notification target type. For that reason adding a new target is NOT as easy\nas adding a new enum member. The ANH implementation uses it to build its tag query. A new target\nalso needs to be something that is targettable through the tag query. For example, say a team wants\na notification target for users in an org with a verified email. Today this target would not be\nexpressable through a target because when we register a device with ANH we do not include whether\nthat user has a verified email. It would be possible to start including that information but the\neffort of tracking that information and updating push registrations when a user verifies their email\nneeds to be weighed with the amount this notification is sent. What might instead be worth it would\nbe for the team that wants such a target to instead do a query to find users who match that query\nand send a notification for each user individually using `NotificationTarget.User`. If there are\nenough requests like that though we may want to consider adding a `BulkPushAsync` method to\n`IPushNotificationService`.\n\n### Self host diagram\n\n```mermaid\nflowchart TD\n    BitwardenClient[Bitwarden Client] -->|Some action| ApiContainer(API Container)\n    ApiContainer --> |HTTP Call|NotificationsContainer{Notifications Container}\n    ApiContainer -.-> |\"HTTP Call (Can be disabled)\"|PushRelay{Cloud Push Relay}\n    PushRelay --> |ANH Library|ANH{Azure Notifications Hub}\n    NotificationsContainer --> |SignalR/Web Sockets|WebClients[Web, Desktop, Browser]\n    ANH --> Firebase{Firebase}\n    ANH --> APNS{Apple Push Notifications Service}\n    APNS --> iOS[iOS Clients]\n    Firebase --> Android[Android Clients]\n```\n\n### Cloud Diagram\n\n```mermaid\nflowchart TD\n    BitwardenClient[Bitwarden Client] -->|Some action| ApiContainer(API Container)\n    ApiContainer --> |Enqueue|AzureQueue{Azure Queue}\n    ApiContainer --> |ANH Library|ANH{Azure Notification Hub}\n    ANH --> |*Simplified*|Mobile\n    ANH --> WebPush[Web Push]\n    AzureQueue --> |Deque|NotificationsContainer{Notifications Container}\n    NotificationsContainer --> SignalR\n    SignalR --> WebClients[Web, Desktop, Browser]\n```\n"
  },
  {
    "path": "src/Core/Platform/PushRegistration/IPushRegistrationService.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Platform.PushRegistration;\n\n// TODO: Change this namespace to `Bit.Core.Platform.PushRegistration\nnamespace Bit.Core.Platform.Push;\n\n\npublic interface IPushRegistrationService\n{\n    Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId, string identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId);\n    Task DeleteRegistrationAsync(string deviceId);\n    Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);\n    Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);\n}\n"
  },
  {
    "path": "src/Core/Platform/PushRegistration/NoopPushRegistrationService.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\n\nnamespace Bit.Core.Platform.PushRegistration.Internal;\n\npublic class NoopPushRegistrationService : IPushRegistrationService\n{\n    public Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task CreateOrUpdateRegistrationAsync(PushRegistrationData pushRegistrationData, string deviceId, string userId,\n        string identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteRegistrationAsync(string deviceId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)\n    {\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/PushRegistration/NotificationHubPushRegistrationService.cs",
    "content": "﻿using System.Diagnostics.CodeAnalysis;\nusing System.Net.Http.Headers;\nusing System.Net.Http.Json;\nusing System.Text.Encodings.Web;\nusing System.Text.Json;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Microsoft.Azure.NotificationHubs;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.PushRegistration.Internal;\n\npublic class NotificationHubPushRegistrationService : IPushRegistrationService\n{\n    private static readonly JsonSerializerOptions webPushSerializationOptions = new()\n    {\n        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping\n    };\n    private readonly IInstallationDeviceRepository _installationDeviceRepository;\n    private readonly INotificationHubPool _notificationHubPool;\n    private readonly IHttpClientFactory _httpClientFactory;\n    private readonly ILogger<NotificationHubPushRegistrationService> _logger;\n\n    public NotificationHubPushRegistrationService(\n        IInstallationDeviceRepository installationDeviceRepository,\n        INotificationHubPool notificationHubPool,\n        IHttpClientFactory httpClientFactory,\n        ILogger<NotificationHubPushRegistrationService> logger)\n    {\n        _installationDeviceRepository = installationDeviceRepository;\n        _notificationHubPool = notificationHubPool;\n        _httpClientFactory = httpClientFactory;\n        _logger = logger;\n    }\n\n    public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId,\n        string? identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId)\n    {\n        var orgIds = organizationIds.ToList();\n        var clientType = DeviceTypes.ToClientType(type);\n        var installation = new Installation\n        {\n            InstallationId = deviceId,\n            PushChannel = data.Token,\n            Tags = new List<string>\n            {\n                $\"userId:{userId}\",\n                $\"clientType:{clientType}\"\n            }.Concat(orgIds.Select(organizationId => $\"organizationId:{organizationId}\")).ToList(),\n            Templates = new Dictionary<string, InstallationTemplate>()\n        };\n\n        if (!string.IsNullOrWhiteSpace(identifier))\n        {\n            installation.Tags.Add(\"deviceIdentifier:\" + identifier);\n        }\n\n        if (installationId != Guid.Empty)\n        {\n            installation.Tags.Add($\"installationId:{installationId}\");\n        }\n\n        if (data.Token != null)\n        {\n            await CreateOrUpdateMobileRegistrationAsync(installation, userId, identifier, clientType, orgIds, type, installationId);\n        }\n        else if (data.WebPush != null)\n        {\n            await CreateOrUpdateWebRegistrationAsync(data.WebPush.Value.Endpoint, data.WebPush.Value.P256dh, data.WebPush.Value.Auth, installation, userId, identifier, clientType, orgIds, installationId);\n        }\n\n        if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))\n        {\n            await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));\n        }\n    }\n\n    private async Task CreateOrUpdateMobileRegistrationAsync(Installation installation, string userId,\n        string? identifier, ClientType clientType, List<string> organizationIds, DeviceType type, Guid installationId)\n    {\n        if (string.IsNullOrWhiteSpace(installation.PushChannel))\n        {\n            return;\n        }\n\n        switch (type)\n        {\n            case DeviceType.Android:\n                installation.Templates.Add(BuildInstallationTemplate(\"payload\",\n                    \"{\\\"message\\\":{\\\"data\\\":{\\\"type\\\":\\\"$(type)\\\",\\\"payload\\\":\\\"$(payload)\\\"}}}\",\n                    userId, identifier, clientType, organizationIds, installationId));\n                installation.Templates.Add(BuildInstallationTemplate(\"message\",\n                    \"{\\\"message\\\":{\\\"data\\\":{\\\"type\\\":\\\"$(type)\\\"},\" +\n                    \"\\\"notification\\\":{\\\"title\\\":\\\"$(title)\\\",\\\"body\\\":\\\"$(message)\\\"}}}\",\n                    userId, identifier, clientType, organizationIds, installationId));\n                installation.Templates.Add(BuildInstallationTemplate(\"badgeMessage\",\n                    \"{\\\"message\\\":{\\\"data\\\":{\\\"type\\\":\\\"$(type)\\\"},\" +\n                    \"\\\"notification\\\":{\\\"title\\\":\\\"$(title)\\\",\\\"body\\\":\\\"$(message)\\\"}}}\",\n                    userId, identifier, clientType, organizationIds, installationId));\n                installation.Platform = NotificationPlatform.FcmV1;\n                break;\n            case DeviceType.iOS:\n                installation.Templates.Add(BuildInstallationTemplate(\"payload\",\n                    \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"payload\\\":\\\"$(payload)\\\"},\" +\n                    \"\\\"aps\\\":{\\\"content-available\\\":1}}\",\n                    userId, identifier, clientType, organizationIds, installationId));\n                installation.Templates.Add(BuildInstallationTemplate(\"message\",\n                    \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\"},\" +\n                    \"\\\"aps\\\":{\\\"alert\\\":\\\"$(message)\\\",\\\"badge\\\":null,\\\"content-available\\\":1}}\", userId, identifier, clientType, organizationIds, installationId));\n                installation.Templates.Add(BuildInstallationTemplate(\"badgeMessage\",\n                    \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\"},\" +\n                    \"\\\"aps\\\":{\\\"alert\\\":\\\"$(message)\\\",\\\"badge\\\":\\\"#(badge)\\\",\\\"content-available\\\":1}}\",\n                    userId, identifier, clientType, organizationIds, installationId));\n                installation.Platform = NotificationPlatform.Apns;\n                break;\n            case DeviceType.AndroidAmazon:\n                installation.Templates.Add(BuildInstallationTemplate(\"payload\",\n                    \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"payload\\\":\\\"$(payload)\\\"}}\",\n                    userId, identifier, clientType, organizationIds, installationId));\n                installation.Templates.Add(BuildInstallationTemplate(\"message\",\n                    \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"message\\\":\\\"$(message)\\\"}}\",\n                    userId, identifier, clientType, organizationIds, installationId));\n                installation.Templates.Add(BuildInstallationTemplate(\"badgeMessage\",\n                    \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"message\\\":\\\"$(message)\\\"}}\",\n                    userId, identifier, clientType, organizationIds, installationId));\n\n                installation.Platform = NotificationPlatform.Adm;\n                break;\n            default:\n                break;\n        }\n\n        await ClientFor(GetComb(installation.InstallationId)).CreateOrUpdateInstallationAsync(installation);\n    }\n\n    private async Task CreateOrUpdateWebRegistrationAsync(string endpoint, string p256dh, string auth, Installation installation, string userId,\n        string? identifier, ClientType clientType, List<string> organizationIds, Guid installationId)\n    {\n        // The Azure SDK is currently lacking support for web push registrations.\n        // We need to use the REST API directly.\n\n        if (string.IsNullOrWhiteSpace(endpoint) || string.IsNullOrWhiteSpace(p256dh) || string.IsNullOrWhiteSpace(auth))\n        {\n            return;\n        }\n\n        installation.Templates.Add(BuildInstallationTemplate(\"payload\",\n            \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"payload\\\":\\\"$(payload)\\\"}}\",\n            userId, identifier, clientType, organizationIds, installationId));\n        installation.Templates.Add(BuildInstallationTemplate(\"message\",\n            \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"message\\\":\\\"$(message)\\\"}}\",\n                userId, identifier, clientType, organizationIds, installationId));\n        installation.Templates.Add(BuildInstallationTemplate(\"badgeMessage\",\n            \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"message\\\":\\\"$(message)\\\"}}\",\n            userId, identifier, clientType, organizationIds, installationId));\n\n        var content = new\n        {\n            installationId = installation.InstallationId,\n            pushChannel = new\n            {\n                endpoint,\n                p256dh,\n                auth\n            },\n            platform = \"browser\",\n            tags = installation.Tags,\n            templates = installation.Templates\n        };\n\n        var client = _httpClientFactory.CreateClient(\"NotificationHub\");\n        var request = ConnectionFor(GetComb(installation.InstallationId)).CreateRequest(HttpMethod.Put, $\"installations/{installation.InstallationId}\");\n        request.Content = JsonContent.Create(content, new MediaTypeHeaderValue(\"application/json\"), webPushSerializationOptions);\n        var response = await client.SendAsync(request);\n        var body = await response.Content.ReadAsStringAsync();\n        if (!response.IsSuccessStatusCode)\n        {\n            _logger.LogWarning(\"Web push registration failed: {Response}\", body);\n        }\n        else\n        {\n            _logger.LogInformation(\"Web push registration success: {Response}\", body);\n        }\n    }\n\n    private static KeyValuePair<string, InstallationTemplate> BuildInstallationTemplate(string templateId, [StringSyntax(StringSyntaxAttribute.Json)] string templateBody,\n        string userId, string? identifier, ClientType clientType, List<string> organizationIds, Guid installationId)\n    {\n        var fullTemplateId = $\"template:{templateId}\";\n\n        var template = new InstallationTemplate\n        {\n            Body = templateBody,\n            Tags = new List<string>\n            {\n                fullTemplateId, $\"{fullTemplateId}_userId:{userId}\", $\"clientType:{clientType}\"\n            }\n        };\n\n        if (!string.IsNullOrWhiteSpace(identifier))\n        {\n            template.Tags.Add($\"{fullTemplateId}_deviceIdentifier:{identifier}\");\n        }\n\n        foreach (var organizationId in organizationIds)\n        {\n            template.Tags.Add($\"organizationId:{organizationId}\");\n        }\n\n        if (installationId != Guid.Empty)\n        {\n            template.Tags.Add($\"installationId:{installationId}\");\n        }\n\n        return new KeyValuePair<string, InstallationTemplate>(fullTemplateId, template);\n    }\n\n    public async Task DeleteRegistrationAsync(string deviceId)\n    {\n        try\n        {\n            await ClientFor(GetComb(deviceId)).DeleteInstallationAsync(deviceId);\n            if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))\n            {\n                await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));\n            }\n        }\n        catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains(\"(404) Not Found\"))\n        {\n            throw;\n        }\n    }\n\n    public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)\n    {\n        await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $\"organizationId:{organizationId}\");\n        if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))\n        {\n            var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));\n            await _installationDeviceRepository.UpsertManyAsync(entities.ToList());\n        }\n    }\n\n    public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)\n    {\n        await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove,\n            $\"organizationId:{organizationId}\");\n        if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))\n        {\n            var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));\n            await _installationDeviceRepository.UpsertManyAsync(entities.ToList());\n        }\n    }\n\n    private async Task PatchTagsForUserDevicesAsync(IEnumerable<string> deviceIds, UpdateOperationType op,\n        string tag)\n    {\n        if (!deviceIds.Any())\n        {\n            return;\n        }\n\n        var operation = new PartialUpdateOperation\n        {\n            Operation = op,\n            Path = \"/tags\"\n        };\n\n        if (op == UpdateOperationType.Add)\n        {\n            operation.Value = tag;\n        }\n        else if (op == UpdateOperationType.Remove)\n        {\n            operation.Path += $\"/{tag}\";\n        }\n\n        foreach (var deviceId in deviceIds)\n        {\n            try\n            {\n                await ClientFor(GetComb(deviceId)).PatchInstallationAsync(deviceId, new List<PartialUpdateOperation> { operation });\n            }\n            catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains(\"(404) Not Found\"))\n            {\n                throw;\n            }\n        }\n    }\n\n    private INotificationHubClient ClientFor(Guid deviceId)\n    {\n        return _notificationHubPool.ClientFor(deviceId);\n    }\n\n    private NotificationHubConnection ConnectionFor(Guid deviceId)\n    {\n        return _notificationHubPool.ConnectionFor(deviceId);\n    }\n\n    private Guid GetComb(string deviceId)\n    {\n        var deviceIdString = deviceId;\n        InstallationDeviceEntity installationDeviceEntity;\n        Guid deviceIdGuid;\n        if (InstallationDeviceEntity.TryParse(deviceIdString, out installationDeviceEntity))\n        {\n            // Strip off the installation id (PartitionId). RowKey is the ID in the Installation's table.\n            deviceIdString = installationDeviceEntity.RowKey;\n        }\n\n        if (Guid.TryParse(deviceIdString, out deviceIdGuid))\n        {\n        }\n        else\n        {\n            throw new Exception($\"Invalid device id {deviceId}.\");\n        }\n        return deviceIdGuid;\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/PushRegistration/PushRegistrationData.cs",
    "content": "﻿namespace Bit.Core.Platform.PushRegistration;\n\npublic record struct WebPushRegistrationData\n{\n    public string Endpoint { get; init; }\n    public string P256dh { get; init; }\n    public string Auth { get; init; }\n}\n\npublic record class PushRegistrationData\n{\n    public string? Token { get; set; }\n    public WebPushRegistrationData? WebPush { get; set; }\n    public PushRegistrationData(string? token)\n    {\n        Token = token;\n    }\n\n    public PushRegistrationData(string Endpoint, string P256dh, string Auth) : this(new WebPushRegistrationData\n    {\n        Endpoint = Endpoint,\n        P256dh = P256dh,\n        Auth = Auth\n    })\n    { }\n\n    public PushRegistrationData(WebPushRegistrationData webPush)\n    {\n        WebPush = webPush;\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/PushRegistration/PushRegistrationServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Platform.Push;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Platform.PushRegistration.Internal;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\n/// <summary>\n/// Extension methods for adding the Push Registration feature.\n/// </summary>\npublic static class PushRegistrationServiceCollectionExtensions\n{\n    /// <summary>\n    /// Adds a <see cref=\"IPushRegistrationService\"/> to the service collection.\n    /// </summary>\n    /// <param name=\"services\">The <see cref=\"IServiceCollection\"/> to add services to.</param>\n    /// <returns>The <see cref=\"IServiceCollection\"/> for chaining.</returns>\n    public static IServiceCollection AddPushRegistration(this IServiceCollection services)\n    {\n        ArgumentNullException.ThrowIfNull(services);\n\n        // TODO: Should add feature that brings in IInstallationDeviceRepository once that is featurized\n\n        // Register all possible variants under there concrete type.\n        services.TryAddSingleton<RelayPushRegistrationService>();\n        services.TryAddSingleton<NoopPushRegistrationService>();\n\n        services.AddHttpClient();\n        services.TryAddSingleton<INotificationHubPool, NotificationHubPool>();\n        services.TryAddSingleton<NotificationHubPushRegistrationService>();\n\n        services.TryAddSingleton<IPushRegistrationService>(static sp =>\n        {\n            var globalSettings = sp.GetRequiredService<GlobalSettings>();\n\n            if (globalSettings.SelfHosted)\n            {\n                if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&\n                    CoreHelpers.SettingHasValue(globalSettings.Installation.Key))\n                {\n                    return sp.GetRequiredService<RelayPushRegistrationService>();\n                }\n\n                return sp.GetRequiredService<NoopPushRegistrationService>();\n            }\n\n            return sp.GetRequiredService<NotificationHubPushRegistrationService>();\n        });\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Core/Platform/PushRegistration/README.md",
    "content": "# Push Registration\n\n## About\n\nPush Registration is a feature for managing devices that should receive push notifications. The main\nentrypoint for this feature is `IPushRegistrationService`.\n\n## Usage\n\nThis feature is largely used internally to Platform owned endpoints or in concert with another team.\n\nIf your feature changes the status of any of the following pieces of data please contact Platform\nso that we can keep push registration working correctly:\n\n- The creation/deletion of a new `Device`.\n- The addition or removal of an organization a `User` is a part of.\n\n## Implementation\n\n### Azure Notification Hub\n\nUsed when the application is hosted by Bitwarden in the cloud. This registers the device and\nassociated metadata with [Azure Notification Hub (ANH)](https://learn.microsoft.com/en-us/azure/notification-hubs/notification-hubs-push-notification-overview).\nThis is necessary so that when a notification is sent ANH will be able to get the notification to\nthat device.\n\nSince Azure Notification Hub has a limit on the amount of devices per hub we have begun to shard\ndevices across multiple hubs. Multiple hubs can be setup through configuration and each one can\nhave a `RegistrationStartDate` and `RegistrationEndDate`. If the start date is `null` no devices\nwill be registered against that given hub. A `null` end date is treated as no known expiry. The\ncreation date for a device is retrieved by the device's ID, and that date is used to find a hub that\nwas actively accepting registrations on that device's creation date. When a new notification hub\npool entry is a `RegistrationEndDate` should be added for the previous pool entry. The end date\nadded to the previous entry should be equal to the start date of the new entry. Both of these dates\nshould be in the future relative to the release date of the release they are going to be added as a\npart of. This way the release can happen and any current in flight devices will continue to be\nregistered with the previous entry and once the release has completed and had a little time to\nsettle we can start registering devices on the new notification hub. An overlap of one entries end\ndate and another entries start date would be preferable to not having them be equal or no overlap.\n\nNotification hub pool example settings:\n\n```json\n{\n  \"GlobalSettings\": {\n    \"NotificationHubPool\": {\n      \"NotificationHubs\": [\n        {\n          \"HubName\": \"first\",\n          \"ConnectionString\": \"anh-connection-string-1\",\n          \"EnableSendTracing\": true,\n          \"RegistrationStartDate\": \"1900-01-01T00:00:00.0000000Z\",\n          \"RegistrationEndDate\": \"2025-01-01T00:00:00.0000000Z\"\n        },\n        {\n          \"HubName\": \"second\",\n          \"ConnectionString\": \"anh-connection-string-2\",\n          \"EnableSendTracing\": false,\n          \"RegistrationStartDate\": \"2025-01-01T00:00:00.0000000Z\",\n          \"RegistrationEndDate\": null\n        }\n      ]\n    }\n  }\n}\n```\n\nWhen we register a device with Azure Notification Hub we include the following tags:\n\n- User ID\n- Client Type (Web, Desktop, Mobile, etc)\n- Organization IDs of which the user is a confirmed member\n- ID of the self-hosted installation if this device was relayed to us\n- Device identifier\n\nThese tags are used to specifically target a device based on those tags with a notification. Most of\nthis data is considered immutable after the creation of a device, except for the organization\nmemberships of a user. If a user is added/removed from an organization, it is important that\n`CreateOrUpdateRegistrationAsync` is called with the new memberships.\n\n### Relay\n\nUsed when the application is self-hosted. This sends a API request to the configured cloud instance,\nwhich will then use [Azure Notification Hub](#azure-notification-hub) but will associate the\ninstallation as the self-hosted installation ID instead of using the cloud one. The endpoints are\nin the [`PushController`](../../../Api/Platform/Push/Controllers/PushController.cs)\n\n### SignalR\n\nWhile not an implementation of `IPushRegistrationService`, the SignalR hub adds users to various\ngroups in [`NotificationsHub.OnConnectedAsync`](../../../Notifications/NotificationsHub.cs) method.\nIt utilizes a manual build of `ICurrentContext` where it reads the claims provided from the access\ntoken.\n"
  },
  {
    "path": "src/Core/Platform/PushRegistration/RelayPushRegistrationService.cs",
    "content": "﻿using Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Platform.PushRegistration.Internal;\n\npublic class RelayPushRegistrationService : BaseIdentityClientService, IPushRegistrationService\n{\n    public RelayPushRegistrationService(\n        IHttpClientFactory httpFactory,\n        GlobalSettings globalSettings,\n        ILogger<RelayPushRegistrationService> logger)\n        : base(\n            httpFactory,\n            globalSettings.PushRelayBaseUri,\n            globalSettings.Installation.IdentityUri,\n            ApiScopes.ApiPush,\n            $\"installation.{globalSettings.Installation.Id}\",\n            globalSettings.Installation.Key,\n            logger)\n    {\n    }\n\n    public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData pushData, string deviceId, string userId,\n        string identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId)\n    {\n        var requestModel = new PushRegistrationRequestModel\n        {\n            DeviceId = deviceId,\n            Identifier = identifier,\n            PushToken = pushData.Token,\n            Type = type,\n            UserId = userId,\n            OrganizationIds = organizationIds,\n            InstallationId = installationId\n        };\n        await SendAsync(HttpMethod.Post, \"push/register\", requestModel);\n    }\n\n    public async Task DeleteRegistrationAsync(string deviceId)\n    {\n        var requestModel = new PushDeviceRequestModel\n        {\n            Id = deviceId,\n        };\n        await SendAsync(HttpMethod.Post, \"push/delete\", requestModel);\n    }\n\n    public async Task AddUserRegistrationOrganizationAsync(\n        IEnumerable<string> deviceIds, string organizationId)\n    {\n        if (!deviceIds.Any())\n        {\n            return;\n        }\n\n        var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);\n        await SendAsync(HttpMethod.Put, \"push/add-organization\", requestModel);\n    }\n\n    public async Task DeleteUserRegistrationOrganizationAsync(\n        IEnumerable<string> deviceIds, string organizationId)\n    {\n        if (!deviceIds.Any())\n        {\n            return;\n        }\n\n        var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);\n        await SendAsync(HttpMethod.Put, \"push/delete-organization\", requestModel);\n    }\n}\n"
  },
  {
    "path": "src/Core/Properties/AssemblyInfo.cs",
    "content": "﻿using System.Reflection;\nusing Microsoft.Extensions.Localization;\n\n[assembly: ResourceLocation(\"Resources\")]\n[assembly: RootNamespace(\"Bit.Core\")]\n"
  },
  {
    "path": "src/Core/Repositories/ICollectionCipherRepository.cs",
    "content": "﻿using Bit.Core.Entities;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface ICollectionCipherRepository\n{\n    Task<ICollection<CollectionCipher>> GetManyByUserIdAsync(Guid userId);\n    Task<ICollection<CollectionCipher>> GetManyByOrganizationIdAsync(Guid organizationId);\n    Task<ICollection<CollectionCipher>> GetManySharedByOrganizationIdAsync(Guid organizationId);\n    Task<ICollection<CollectionCipher>> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId);\n    Task UpdateCollectionsAsync(Guid cipherId, Guid userId, IEnumerable<Guid> collectionIds);\n    Task UpdateCollectionsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable<Guid> collectionIds);\n    Task UpdateCollectionsForCiphersAsync(IEnumerable<Guid> cipherIds, Guid userId, Guid organizationId,\n        IEnumerable<Guid> collectionIds);\n\n    /// <summary>\n    /// Add the specified collections to the specified ciphers. If a cipher already belongs to a requested collection,\n    /// no action is taken.\n    /// </summary>\n    /// <remarks>\n    /// This method does not perform any authorization checks.\n    /// </remarks>\n    Task AddCollectionsForManyCiphersAsync(Guid organizationId, IEnumerable<Guid> cipherIds, IEnumerable<Guid> collectionIds);\n\n    /// <summary>\n    /// Remove the specified collections from the specified ciphers. If a cipher does not belong to a requested collection,\n    /// no action is taken.\n    /// </summary>\n    /// <remarks>\n    /// This method does not perform any authorization checks.\n    /// </remarks>\n    Task RemoveCollectionsForManyCiphersAsync(Guid organizationId, IEnumerable<Guid> cipherIds, IEnumerable<Guid> collectionIds);\n}\n"
  },
  {
    "path": "src/Core/Repositories/ICollectionRepository.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Models.Data;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface ICollectionRepository : IRepository<Collection, Guid>\n{\n    Task<int> GetCountByOrganizationIdAsync(Guid organizationId);\n\n    /// <summary>\n    /// Returns a collection and fetches group/user associations for the collection.\n    /// </summary>\n    Task<Tuple<Collection?, CollectionAccessDetails>> GetByIdWithAccessAsync(Guid id);\n\n    /// <summary>\n    /// Return all collections that belong to the organization. Does not include any permission details or group/user\n    /// access relationships.\n    /// </summary>\n    Task<ICollection<Collection>> GetManyByOrganizationIdAsync(Guid organizationId);\n\n    /// <inheritdoc cref=\"GetManyByOrganizationIdAsync\"/>\n    /// <remarks>\n    /// Excludes default collections (My Items collections) - used by Admin Console Collections tab.\n    /// </remarks>\n    Task<ICollection<Collection>> GetManySharedCollectionsByOrganizationIdAsync(Guid organizationId);\n\n    /// <summary>\n    /// Return all shared collections that belong to the organization. Includes group/user access relationships for each collection.\n    /// </summary>\n    Task<ICollection<Tuple<Collection, CollectionAccessDetails>>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId);\n\n    Task<ICollection<Collection>> GetManyByManyIdsAsync(IEnumerable<Guid> collectionIds);\n\n    /// <summary>\n    /// Return all collections a user has access to across all of the organization they're a member of. Includes permission\n    /// details for each collection.\n    /// </summary>\n    Task<ICollection<CollectionDetails>> GetManyByUserIdAsync(Guid userId);\n\n    /// <summary>\n    /// Returns all shared collections for an organization, including permission info for the specified user.\n    /// This does not perform any authorization checks internally!\n    /// Optionally, you can include access relationships for other Groups/Users and the collections.\n    /// Excludes default collections (My Items collections) - used by Admin Console Collections tab.\n    /// </summary>\n    Task<ICollection<CollectionAdminDetails>> GetManySharedByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships);\n\n    /// <summary>\n    /// Returns the collection by Id, including permission info for the specified user.\n    /// This does not perform any authorization checks internally!\n    /// Optionally, you can include access relationships for other Groups/Users and the collection.\n    /// </summary>\n    Task<CollectionAdminDetails?> GetByIdWithPermissionsAsync(Guid collectionId, Guid? userId, bool includeAccessRelationships);\n\n    Task CreateAsync(Collection obj, IEnumerable<CollectionAccessSelection>? groups, IEnumerable<CollectionAccessSelection>? users);\n    Task ReplaceAsync(Collection obj, IEnumerable<CollectionAccessSelection>? groups, IEnumerable<CollectionAccessSelection>? users);\n    Task DeleteUserAsync(Guid collectionId, Guid organizationUserId);\n    Task UpdateUsersAsync(Guid id, IEnumerable<CollectionAccessSelection> users);\n    Task<ICollection<CollectionAccessSelection>> GetManyUsersByIdAsync(Guid id);\n    Task DeleteManyAsync(IEnumerable<Guid> collectionIds);\n    Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> collectionIds,\n        IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups);\n\n    /// <summary>\n    /// Creates default user collections for the specified organization users.\n    /// Filters internally to only create collections for users who don't already have one.\n    /// </summary>\n    /// <param name=\"organizationId\">The Organization ID.</param>\n    /// <param name=\"organizationUserIds\">The Organization User IDs to create default collections for.</param>\n    /// <param name=\"defaultCollectionName\">The encrypted string to use as the default collection name.</param>\n    Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);\n\n    /// <summary>\n    /// Creates default user collections for the specified organization users using bulk insert operations.\n    /// Use this if you need to create collections for > ~1k users.\n    /// Filters internally to only create collections for users who don't already have one.\n    /// </summary>\n    /// <param name=\"organizationId\">The Organization ID.</param>\n    /// <param name=\"organizationUserIds\">The Organization User IDs to create default collections for.</param>\n    /// <param name=\"defaultCollectionName\">The encrypted string to use as the default collection name.</param>\n    Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName);\n\n}\n"
  },
  {
    "path": "src/Core/Repositories/IDeviceRepository.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.UserKey;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IDeviceRepository : IRepository<Device, Guid>\n{\n    Task<Device?> GetByIdAsync(Guid id, Guid userId);\n    Task<Device?> GetByIdentifierAsync(string identifier);\n    Task<Device?> GetByIdentifierAsync(string identifier, Guid userId);\n    Task<ICollection<Device>> GetManyByUserIdAsync(Guid userId);\n    // DeviceAuthDetails is passed back to decouple the response model from the\n    // repository in case more fields are ever added to the details response for\n    // other requests.\n    Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId);\n    Task ClearPushTokenAsync(Guid id);\n    UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<Device> devices);\n}\n"
  },
  {
    "path": "src/Core/Repositories/IInstallationDeviceRepository.cs",
    "content": "﻿using Bit.Core.Models.Data;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IInstallationDeviceRepository\n{\n    Task UpsertAsync(InstallationDeviceEntity entity);\n    Task UpsertManyAsync(IList<InstallationDeviceEntity> entities);\n    Task DeleteAsync(InstallationDeviceEntity entity);\n}\n"
  },
  {
    "path": "src/Core/Repositories/IMaintenanceRepository.cs",
    "content": "﻿namespace Bit.Core.Repositories;\n\n#nullable enable\n\npublic interface IMaintenanceRepository\n{\n    Task UpdateStatisticsAsync();\n    Task DisableCipherAutoStatsAsync();\n    Task RebuildIndexesAsync();\n    Task DeleteExpiredGrantsAsync();\n    Task DeleteExpiredSponsorshipsAsync(DateTime validUntilBeforeDate);\n}\n"
  },
  {
    "path": "src/Core/Repositories/IOrganizationApiKeyRepository.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IOrganizationApiKeyRepository : IRepository<OrganizationApiKey, Guid>\n{\n    Task<IEnumerable<OrganizationApiKey>> GetManyByOrganizationIdTypeAsync(Guid organizationId, OrganizationApiKeyType? type = null);\n}\n"
  },
  {
    "path": "src/Core/Repositories/IOrganizationConnectionRepository.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IOrganizationConnectionRepository : IRepository<OrganizationConnection, Guid>\n{\n    Task<OrganizationConnection?> GetByIdOrganizationIdAsync(Guid id, Guid organizationId);\n    Task<ICollection<OrganizationConnection>> GetByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type);\n    Task<ICollection<OrganizationConnection>> GetEnabledByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type);\n}\n"
  },
  {
    "path": "src/Core/Repositories/IOrganizationDomainRepository.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IOrganizationDomainRepository : IRepository<OrganizationDomain, Guid>\n{\n    Task<ICollection<OrganizationDomain>> GetClaimedDomainsByDomainNameAsync(string domainName);\n    Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId);\n    Task<ICollection<OrganizationDomain>> GetManyByNextRunDateAsync(DateTime date);\n    Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email);\n    Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email);\n    Task<IEnumerable<OrganizationDomain>> GetVerifiedDomainsByOrganizationIdsAsync(IEnumerable<Guid> organizationIds);\n    Task<OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId);\n    Task<OrganizationDomain?> GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName);\n    Task<ICollection<OrganizationDomain>> GetExpiredOrganizationDomainsAsync();\n    Task<bool> DeleteExpiredAsync(int expirationPeriod);\n    Task<bool> HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(string domainName, Guid? excludeOrganizationId = null);\n}\n"
  },
  {
    "path": "src/Core/Repositories/IOrganizationSponsorshipRepository.cs",
    "content": "﻿using Bit.Core.Entities;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IOrganizationSponsorshipRepository : IRepository<OrganizationSponsorship, Guid>\n{\n    Task<ICollection<Guid>?> CreateManyAsync(IEnumerable<OrganizationSponsorship> organizationSponsorships);\n    Task ReplaceManyAsync(IEnumerable<OrganizationSponsorship> organizationSponsorships);\n    Task UpsertManyAsync(IEnumerable<OrganizationSponsorship> organizationSponsorships);\n    Task DeleteManyAsync(IEnumerable<Guid> organizationSponsorshipIds);\n    Task<ICollection<OrganizationSponsorship>> GetManyBySponsoringOrganizationAsync(Guid sponsoringOrganizationId);\n    Task<OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated = false);\n    Task<OrganizationSponsorship?> GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId);\n    Task<DateTime?> GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId);\n}\n"
  },
  {
    "path": "src/Core/Repositories/IPlayItemRepository.cs",
    "content": "﻿using Bit.Core.Entities;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IPlayItemRepository : IRepository<PlayItem, Guid>\n{\n    Task<ICollection<PlayItem>> GetByPlayIdAsync(string playId);\n    Task DeleteByPlayIdAsync(string playId);\n}\n"
  },
  {
    "path": "src/Core/Repositories/IRepository.cs",
    "content": "﻿using Bit.Core.Entities;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface IRepository<T, TId> where TId : IEquatable<TId> where T : class, ITableObject<TId>\n{\n    Task<T?> GetByIdAsync(TId id);\n    Task<T> CreateAsync(T obj);\n    Task ReplaceAsync(T obj);\n    Task UpsertAsync(T obj);\n    Task DeleteAsync(T obj);\n}\n"
  },
  {
    "path": "src/Core/Repositories/ITransactionRepository.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories;\n\npublic interface ITransactionRepository : IRepository<Transaction, Guid>\n{\n    Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId, int? limit = null, DateTime? startAfter = null);\n    Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId, int? limit = null, DateTime? startAfter = null);\n    Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId, int? limit = null, DateTime? startAfter = null);\n    Task<Transaction?> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId);\n}\n"
  },
  {
    "path": "src/Core/Repositories/IUserRepository.cs",
    "content": "﻿using Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.Repositories;\n\npublic interface IUserRepository : IRepository<User, Guid>\n{\n    Task<User?> GetByGatewayCustomerIdAsync(string gatewayCustomerId);\n    Task<User?> GetByGatewaySubscriptionIdAsync(string gatewaySubscriptionId);\n    Task<User?> GetByEmailAsync(string email);\n    Task<IEnumerable<User>> GetManyByEmailsAsync(IEnumerable<string> emails);\n    Task<User?> GetBySsoUserAsync(string externalId, Guid? organizationId);\n    Task<UserKdfInformation?> GetKdfInformationByEmailAsync(string email);\n    Task<ICollection<User>> SearchAsync(string email, int skip, int take);\n    Task<ICollection<User>> GetManyByPremiumAsync(bool premium);\n    Task<string?> GetPublicKeyAsync(Guid id);\n    Task<DateTime> GetAccountRevisionDateAsync(Guid id);\n    Task UpdateStorageAsync(Guid id);\n    Task UpdateRenewalReminderDateAsync(Guid id, DateTime renewalReminderDate);\n    Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids);\n    /// <summary>\n    /// Retrieves the data for the requested user IDs and includes an additional property indicating\n    /// whether the user has premium access directly or through an organization.\n    /// </summary>\n    [Obsolete(\"Use GetPremiumAccessByIdsAsync instead. This method will be removed in a future version.\")]\n    Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids);\n    /// <summary>\n    /// Retrieves the data for the requested user ID and includes additional property indicating\n    /// whether the user has premium access directly or through an organization.\n    ///\n    /// Calls the same stored procedure as GetManyWithCalculatedPremiumAsync but handles the query\n    /// for a single user.\n    /// </summary>\n    /// <param name=\"userId\">The user ID to retrieve data for.</param>\n    /// <returns>User data with calculated premium access; null if nothing is found</returns>\n    [Obsolete(\"Use GetPremiumAccessAsync instead. This method will be removed in a future version.\")]\n    Task<UserWithCalculatedPremium?> GetCalculatedPremiumAsync(Guid userId);\n    /// <summary>\n    /// Retrieves premium access status for multiple users.\n    /// For internal use - consumers should use IHasPremiumAccessQuery instead.\n    /// </summary>\n    /// <param name=\"ids\">The user IDs to check</param>\n    /// <returns>Collection of UserPremiumAccess objects containing premium status information</returns>\n    Task<IEnumerable<UserPremiumAccess>> GetPremiumAccessByIdsAsync(IEnumerable<Guid> ids);\n    /// <summary>\n    /// Retrieves premium access status for a single user.\n    /// For internal use - consumers should use IHasPremiumAccessQuery instead.\n    /// </summary>\n    /// <param name=\"userId\">The user ID to check</param>\n    /// <returns>UserPremiumAccess object containing premium status information, or null if user not found</returns>\n    Task<UserPremiumAccess?> GetPremiumAccessAsync(Guid userId);\n    /// <summary>\n    /// Sets a new user key and updates all encrypted data.\n    /// <para>Warning: Any user key encrypted data not included will be lost.</para>\n    /// </summary>\n    /// <param name=\"user\">The user to update</param>\n    /// <param name=\"updateDataActions\">Registered database calls to update re-encrypted data.</param>\n    Task UpdateUserKeyAndEncryptedDataAsync(User user,\n        IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions);\n    Task UpdateUserKeyAndEncryptedDataV2Async(User user,\n        IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions);\n    /// <summary>\n    /// Sets the account cryptographic state to a user in a single transaction. The provided\n    /// MUST be a V2 encryption state. Passing in a V1 encryption state will throw.\n    /// Extra actions can be passed in case other user data needs to be updated in the same transaction.\n    /// </summary>\n    Task SetV2AccountCryptographicStateAsync(\n        Guid userId,\n        UserAccountKeysData accountKeysData,\n        IEnumerable<UpdateUserData>? updateUserDataActions = null);\n    Task DeleteManyAsync(IEnumerable<User> users);\n\n    UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey);\n\n    /// <summary>\n    /// Sets the master password and KDF for a user.\n    /// </summary>\n    /// <param name=\"userId\">The user identifier.</param>\n    /// <param name=\"masterPasswordUnlockData\">Data for unlocking with the master password.</param>\n    /// <param name=\"serverSideHashedMasterPasswordAuthenticationHash\">Server side hash of the user master authentication password hash</param>\n    /// <param name=\"masterPasswordHint\">Optional hint for the master password.</param>\n    /// <returns>A task to complete the operation.</returns>\n    UpdateUserData SetMasterPassword(Guid userId, MasterPasswordUnlockData masterPasswordUnlockData,\n        string serverSideHashedMasterPasswordAuthenticationHash, string? masterPasswordHint);\n\n    /// <summary>\n    /// Updates multiple user data properties in a single transaction.\n    /// </summary>\n    /// <param name=\"updateUserDataActions\">Actions to update user data.</param>\n    /// <returns>On success</returns>\n    Task UpdateUserDataAsync(IEnumerable<UpdateUserData> updateUserDataActions);\n}\n\npublic delegate Task UpdateUserData(Microsoft.Data.SqlClient.SqlConnection? connection = null,\n    Microsoft.Data.SqlClient.SqlTransaction? transaction = null);\n"
  },
  {
    "path": "src/Core/Repositories/Noop/InstallationDeviceRepository.cs",
    "content": "﻿using Bit.Core.Models.Data;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories.Noop;\n\npublic class InstallationDeviceRepository : IInstallationDeviceRepository\n{\n    public Task UpsertAsync(InstallationDeviceEntity entity)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task UpsertManyAsync(IList<InstallationDeviceEntity> entities)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteAsync(InstallationDeviceEntity entity)\n    {\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Core/Repositories/TableStorage/InstallationDeviceRepository.cs",
    "content": "﻿using Azure.Data.Tables;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Settings;\n\n#nullable enable\n\nnamespace Bit.Core.Repositories.TableStorage;\n\npublic class InstallationDeviceRepository : IInstallationDeviceRepository\n{\n    private readonly TableClient _tableClient;\n\n    public InstallationDeviceRepository(GlobalSettings globalSettings)\n        : this(globalSettings.Events.ConnectionString)\n    { }\n\n    public InstallationDeviceRepository(string storageConnectionString)\n    {\n        var tableClient = new TableServiceClient(storageConnectionString);\n        _tableClient = tableClient.GetTableClient(\"installationdevice\");\n    }\n\n    public async Task UpsertAsync(InstallationDeviceEntity entity)\n    {\n        await _tableClient.UpsertEntityAsync(entity);\n    }\n\n    public async Task UpsertManyAsync(IList<InstallationDeviceEntity>? entities)\n    {\n        if (entities is null || !entities.Any())\n        {\n            return;\n        }\n\n        if (entities.Count == 1)\n        {\n            await UpsertAsync(entities.First());\n            return;\n        }\n\n        var entityGroups = entities.GroupBy(ent => ent.PartitionKey);\n        foreach (var group in entityGroups)\n        {\n            var groupEntities = group.ToList();\n            if (groupEntities.Count == 1)\n            {\n                await UpsertAsync(groupEntities.First());\n                continue;\n            }\n\n            // A batch insert can only contain 100 entities at a time\n            var iterations = groupEntities.Count / 100;\n            for (var i = 0; i <= iterations; i++)\n            {\n                var batch = new List<TableTransactionAction>();\n                var batchEntities = groupEntities.Skip(i * 100).Take(100);\n                if (!batchEntities.Any())\n                {\n                    break;\n                }\n\n                foreach (var entity in batchEntities)\n                {\n                    batch.Add(new TableTransactionAction(TableTransactionActionType.UpsertReplace, entity));\n                }\n\n                await _tableClient.SubmitTransactionAsync(batch);\n            }\n        }\n    }\n\n    public async Task DeleteAsync(InstallationDeviceEntity entity)\n    {\n        await _tableClient.DeleteEntityAsync(entity.PartitionKey, entity.RowKey);\n    }\n}\n"
  },
  {
    "path": "src/Core/Resources/SharedResources.cs",
    "content": "﻿namespace Bit.Core.Resources;\n\npublic class SharedResources\n{\n}\n"
  },
  {
    "path": "src/Core/Resources/SharedResources.en.resx",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!--\n    Microsoft ResX Schema\n\n    Version 2.0\n\n    The primary goals of this format is to allow a simple XML format\n    that is mostly human readable. The generation and parsing of the\n    various data types are done through the TypeConverter classes\n    associated with the data types.\n\n    Example:\n\n    ... ado.net/XML headers & schema ...\n    <resheader name=\"resmimetype\">text/microsoft-resx</resheader>\n    <resheader name=\"version\">2.0</resheader>\n    <resheader name=\"reader\">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>\n    <resheader name=\"writer\">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>\n    <data name=\"Name1\"><value>this is my long string</value><comment>this is a comment</comment></data>\n    <data name=\"Color1\" type=\"System.Drawing.Color, System.Drawing\">Blue</data>\n    <data name=\"Bitmap1\" mimetype=\"application/x-microsoft.net.object.binary.base64\">\n        <value>[base64 mime encoded serialized .NET Framework object]</value>\n    </data>\n    <data name=\"Icon1\" type=\"System.Drawing.Icon, System.Drawing\" mimetype=\"application/x-microsoft.net.object.bytearray.base64\">\n        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>\n        <comment>This is a comment</comment>\n    </data>\n\n    There are any number of \"resheader\" rows that contain simple\n    name/value pairs.\n\n    Each data row contains a name, and value. The row also contains a\n    type or mimetype. Type corresponds to a .NET class that support\n    text/value conversion through the TypeConverter architecture.\n    Classes that don't support this are serialized and stored with the\n    mimetype set.\n\n    The mimetype is used for serialized objects, and tells the\n    ResXResourceReader how to depersist the object. This is currently not\n    extensible. For a given mimetype the value must be set accordingly:\n\n    Note - application/x-microsoft.net.object.binary.base64 is the format\n    that the ResXResourceWriter will generate, however the reader can\n    read any of the formats listed below.\n\n    mimetype: application/x-microsoft.net.object.binary.base64\n    value   : The object must be serialized with\n            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.soap.base64\n    value   : The object must be serialized with\n            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter\n            : and then encoded with base64 encoding.\n\n    mimetype: application/x-microsoft.net.object.bytearray.base64\n    value   : The object must be serialized into a byte array\n            : using a System.ComponentModel.TypeConverter\n            : and then encoded with base64 encoding.\n    -->\n  <xsd:schema id=\"root\" xmlns=\"\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\">\n    <xsd:import namespace=\"http://www.w3.org/XML/1998/namespace\" />\n    <xsd:element name=\"root\" msdata:IsDataSet=\"true\">\n      <xsd:complexType>\n        <xsd:choice maxOccurs=\"unbounded\">\n          <xsd:element name=\"metadata\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" use=\"required\" type=\"xsd:string\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"assembly\">\n            <xsd:complexType>\n              <xsd:attribute name=\"alias\" type=\"xsd:string\" />\n              <xsd:attribute name=\"name\" type=\"xsd:string\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"data\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n                <xsd:element name=\"comment\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"2\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" msdata:Ordinal=\"1\" />\n              <xsd:attribute name=\"type\" type=\"xsd:string\" msdata:Ordinal=\"3\" />\n              <xsd:attribute name=\"mimetype\" type=\"xsd:string\" msdata:Ordinal=\"4\" />\n              <xsd:attribute ref=\"xml:space\" />\n            </xsd:complexType>\n          </xsd:element>\n          <xsd:element name=\"resheader\">\n            <xsd:complexType>\n              <xsd:sequence>\n                <xsd:element name=\"value\" type=\"xsd:string\" minOccurs=\"0\" msdata:Ordinal=\"1\" />\n              </xsd:sequence>\n              <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\" />\n            </xsd:complexType>\n          </xsd:element>\n        </xsd:choice>\n      </xsd:complexType>\n    </xsd:element>\n  </xsd:schema>\n  <resheader name=\"resmimetype\">\n    <value>text/microsoft-resx</value>\n  </resheader>\n  <resheader name=\"version\">\n    <value>2.0</value>\n  </resheader>\n  <resheader name=\"reader\">\n    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <resheader name=\"writer\">\n    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>\n  </resheader>\n  <data name=\"Home\" xml:space=\"preserve\">\n    <value>Home</value>\n    <comment>Home page</comment>\n  </data>\n  <data name=\"Policies\" xml:space=\"preserve\">\n    <value>Policies</value>\n  </data>\n  <data name=\"Enabled\" xml:space=\"preserve\">\n    <value>Enabled</value>\n  </data>\n  <data name=\"TwoStepLogin\" xml:space=\"preserve\">\n    <value>Two-step Login</value>\n  </data>\n  <data name=\"TwoStepLoginDescription\" xml:space=\"preserve\">\n    <value>Require users to set up two-step login on their personal accounts.</value>\n  </data>\n  <data name=\"MasterPassword\" xml:space=\"preserve\">\n    <value>Master Password</value>\n  </data>\n  <data name=\"MasterPasswordDescription\" xml:space=\"preserve\">\n    <value>Set minimum requirements for master password strength.</value>\n  </data>\n  <data name=\"PasswordGenerator\" xml:space=\"preserve\">\n    <value>Password Generator</value>\n  </data>\n  <data name=\"PasswordGeneratorDescription\" xml:space=\"preserve\">\n    <value>Set minimum requirements for password generator configuration.</value>\n  </data>\n  <data name=\"EditPolicy\" xml:space=\"preserve\">\n    <value>Edit Policy - {0}</value>\n  </data>\n  <data name=\"EditPolicyTwoStepLoginWarning\" xml:space=\"preserve\">\n    <value>Organization members who are not Owners or Administrators and do not have two-step login enabled for their personal account will be removed from the organization and will receive an email notifying them about the change.</value>\n  </data>\n  <data name=\"Save\" xml:space=\"preserve\">\n    <value>Save</value>\n  </data>\n  <data name=\"Cancel\" xml:space=\"preserve\">\n    <value>Cancel</value>\n  </data>\n  <data name=\"MinimumComplexityScore\" xml:space=\"preserve\">\n    <value>Minimum Complexity Score</value>\n  </data>\n  <data name=\"MinimumLength\" xml:space=\"preserve\">\n    <value>Minimum Length</value>\n  </data>\n  <data name=\"Weak\" xml:space=\"preserve\">\n    <value>Weak</value>\n  </data>\n  <data name=\"Good\" xml:space=\"preserve\">\n    <value>Good</value>\n  </data>\n  <data name=\"Strong\" xml:space=\"preserve\">\n    <value>Strong</value>\n  </data>\n  <data name=\"DefaultType\" xml:space=\"preserve\">\n    <value>Default Type</value>\n  </data>\n  <data name=\"UserPreference\" xml:space=\"preserve\">\n    <value>User Preference</value>\n  </data>\n  <data name=\"Password\" xml:space=\"preserve\">\n    <value>Password</value>\n  </data>\n  <data name=\"Passphrase\" xml:space=\"preserve\">\n    <value>Passphrase</value>\n  </data>\n  <data name=\"MinimumSpecial\" xml:space=\"preserve\">\n    <value>Minimum Special</value>\n  </data>\n  <data name=\"MinimumNumbers\" xml:space=\"preserve\">\n    <value>Minimum Numbers</value>\n  </data>\n  <data name=\"MinimumNumberOfWords\" xml:space=\"preserve\">\n    <value>Minimum Number of Words</value>\n  </data>\n  <data name=\"Capitalize\" xml:space=\"preserve\">\n    <value>Capitalize</value>\n  </data>\n  <data name=\"IncludeNumber\" xml:space=\"preserve\">\n    <value>Include Number</value>\n  </data>\n  <data name=\"Warning\" xml:space=\"preserve\">\n    <value>Warning</value>\n  </data>\n  <data name=\"UppercaseAZ\" xml:space=\"preserve\">\n    <value>A-Z</value>\n  </data>\n  <data name=\"LowercaseAZ\" xml:space=\"preserve\">\n    <value>a-z</value>\n  </data>\n  <data name=\"Numbers09\" xml:space=\"preserve\">\n    <value>0-9</value>\n  </data>\n  <data name=\"SpecialCharacters\" xml:space=\"preserve\">\n    <value>!@#$%^&amp;*</value>\n  </data>\n  <data name=\"Select\" xml:space=\"preserve\">\n    <value>Select</value>\n  </data>\n  <data name=\"MasterPasswordMinLengthError\" xml:space=\"preserve\">\n    <value>The field {0} must be greater than or equal to {1}.</value>\n  </data>\n  <data name=\"SingleSignOn\" xml:space=\"preserve\">\n    <value>Single Sign-On</value>\n  </data>\n  <data name=\"EditSsoConfig\" xml:space=\"preserve\">\n    <value>Edit SSO Configuration</value>\n  </data>\n  <data name=\"ConfigType\" xml:space=\"preserve\">\n    <value>Type</value>\n  </data>\n  <data name=\"OpenIdConnect\" xml:space=\"preserve\">\n    <value>OpenID Connect</value>\n  </data>\n  <data name=\"Saml2\" xml:space=\"preserve\">\n    <value>SAML 2.0</value>\n  </data>\n  <data name=\"SsoConfig\" xml:space=\"preserve\">\n    <value>SSO Configuration</value>\n  </data>\n  <data name=\"OpenIdConnectConfig\" xml:space=\"preserve\">\n    <value>OpenID Connect Configuration</value>\n  </data>\n  <data name=\"Authority\" xml:space=\"preserve\">\n    <value>Authority</value>\n  </data>\n  <data name=\"ClientId\" xml:space=\"preserve\">\n    <value>Client ID</value>\n  </data>\n  <data name=\"ClientSecret\" xml:space=\"preserve\">\n    <value>Client Secret</value>\n  </data>\n  <data name=\"CallbackPath\" xml:space=\"preserve\">\n    <value>Callback Path</value>\n  </data>\n  <data name=\"SignedOutCallbackPath\" xml:space=\"preserve\">\n    <value>Signed Out Callback Path</value>\n  </data>\n  <data name=\"SamlSpConfig\" xml:space=\"preserve\">\n    <value>SAML Service Provider Configuration</value>\n  </data>\n  <data name=\"EntityId\" xml:space=\"preserve\">\n    <value>Entity ID</value>\n  </data>\n  <data name=\"SpEntityId\" xml:space=\"preserve\">\n    <value>SP Entity ID</value>\n  </data>\n  <data name=\"SpMetadataUrl\" xml:space=\"preserve\">\n    <value>SAML 2.0 Metadata URL</value>\n  </data>\n  <data name=\"SpAcsUrl\" xml:space=\"preserve\">\n    <value>Assertion Consumer Service (ACS) URL</value>\n  </data>\n  <data name=\"SpValidateCertificates\" xml:space=\"preserve\">\n    <value>Validate Certificates</value>\n  </data>\n  <data name=\"NameIdFormat\" xml:space=\"preserve\">\n    <value>Name ID Format</value>\n  </data>\n  <data name=\"NotConfigured\" xml:space=\"preserve\">\n    <value>Not Configured</value>\n    <comment>A SAML Name ID format</comment>\n  </data>\n  <data name=\"Unspecified\" xml:space=\"preserve\">\n    <value>Unspecified</value>\n    <comment>A SAML Name ID format</comment>\n  </data>\n  <data name=\"EmailAddress\" xml:space=\"preserve\">\n    <value>Email Address</value>\n    <comment>A SAML Name ID format</comment>\n  </data>\n  <data name=\"X509SubjectName\" xml:space=\"preserve\">\n    <value>X.509 Subject Name</value>\n    <comment>A SAML Name ID format</comment>\n  </data>\n  <data name=\"WindowsDomainQualifiedName\" xml:space=\"preserve\">\n    <value>Windows Domain Qualified Name</value>\n  </data>\n  <data name=\"KerberosPrincipalName\" xml:space=\"preserve\">\n    <value>Kerberos Principal Name</value>\n  </data>\n  <data name=\"EntityIdentifier\" xml:space=\"preserve\">\n    <value>Entity Identifier</value>\n  </data>\n  <data name=\"Persistent\" xml:space=\"preserve\">\n    <value>Persistent</value>\n    <comment>A SAML Name ID format</comment>\n  </data>\n  <data name=\"Transient\" xml:space=\"preserve\">\n    <value>Transient</value>\n    <comment>A SAML Name ID format</comment>\n  </data>\n  <data name=\"PrivateKey\" xml:space=\"preserve\">\n    <value>Private Key</value>\n  </data>\n  <data name=\"SamlIdpConfig\" xml:space=\"preserve\">\n    <value>SAML Identity Provider Configuration</value>\n  </data>\n  <data name=\"SingleSignOnServiceUrl\" xml:space=\"preserve\">\n    <value>Single Sign On Service URL</value>\n  </data>\n  <data name=\"SingleLogoutServiceUrl\" xml:space=\"preserve\">\n    <value>Single Log Out Service URL</value>\n  </data>\n  <data name=\"PublicKey\" xml:space=\"preserve\">\n    <value>Public Key</value>\n  </data>\n  <data name=\"SpWantAssertionsSigned\" xml:space=\"preserve\">\n    <value>Want Assertions Signed</value>\n  </data>\n  <data name=\"SigningAlgorithm\" xml:space=\"preserve\">\n    <value>Signing Algorithm</value>\n  </data>\n  <data name=\"SigningBehavior\" xml:space=\"preserve\">\n    <value>Signing Behavior</value>\n  </data>\n  <data name=\"MinIncomingSigningAlgorithm\" xml:space=\"preserve\">\n    <value>Minimum Incoming Signing Algorithm</value>\n  </data>\n  <data name=\"BindingType\" xml:space=\"preserve\">\n    <value>Binding Type</value>\n  </data>\n  <data name=\"ArtifactResolutionServiceUrl\" xml:space=\"preserve\">\n    <value>Artifact Resolution Service URL</value>\n  </data>\n  <data name=\"X509PublicCert\" xml:space=\"preserve\">\n    <value>X509 Public Certificate</value>\n  </data>\n  <data name=\"OutboundSigningAlgorithm\" xml:space=\"preserve\">\n    <value>Outbound Signing Algorithm</value>\n  </data>\n  <data name=\"AllowUnsolicitedAuthnResponse\" xml:space=\"preserve\">\n    <value>Allow Unsolicited Authentication Response</value>\n  </data>\n  <data name=\"DisableOutboundLogoutRequests\" xml:space=\"preserve\">\n    <value>Disable Outbound Logout Requests</value>\n  </data>\n  <data name=\"WantAuthnRequestsSigned\" xml:space=\"preserve\">\n    <value>Want Authentication Requests Signed</value>\n  </data>\n  <data name=\"MetadataAddress\" xml:space=\"preserve\">\n    <value>Metadata Address</value>\n  </data>\n  <data name=\"GetClaimsFromUserInfoEndpoint\" xml:space=\"preserve\">\n    <value>Get Claims From User Info Endpoint</value>\n  </data>\n  <data name=\"AuthorityValidationError\" xml:space=\"preserve\">\n    <value>The Authority field is required on a Open ID Connect configuration.</value>\n  </data>\n  <data name=\"ClientIdValidationError\" xml:space=\"preserve\">\n    <value>The Client ID field is required on a Open ID Connect configuration.</value>\n  </data>\n  <data name=\"ClientSecretValidationError\" xml:space=\"preserve\">\n    <value>The Client Secret field is required on a Open ID Connect configuration.</value>\n  </data>\n  <data name=\"CallbackPathValidationError\" xml:space=\"preserve\">\n    <value>The Callback Path field is required on a Open ID Connect configuration.</value>\n  </data>\n  <data name=\"SpEntityIdValidationError\" xml:space=\"preserve\">\n    <value>The Service Provider Configuration Entity Id field is required on a SAML configuration.</value>\n  </data>\n  <data name=\"IdpEntityIdValidationError\" xml:space=\"preserve\">\n    <value>The Identity Provider Configuration Entity Id field is required on a SAML configuration.</value>\n  </data>\n  <data name=\"Saml2SigningBehaviorValidationError\" xml:space=\"preserve\">\n    <value>If SAML Signing Behavior is set to never, public and private service provider keys are required.</value>\n  </data>\n  <data name=\"Saml2BindingTypeValidationError\" xml:space=\"preserve\">\n    <value>If SAML Binding Type is set to artifact, identity provider resolution service URL is required.</value>\n  </data>\n  <data name=\"IdpSingleSignOnServiceUrlValidationError\" xml:space=\"preserve\">\n    <value>Single sign on service URL is required.</value>\n  </data>\n  <data name=\"InvalidSchemeConfigurationError\" xml:space=\"preserve\">\n    <value>The configured authentication scheme is not valid: \"{0}\"</value>\n  </data>\n  <data name=\"OrganizationNotFoundByIdentifierError\" xml:space=\"preserve\">\n    <value>Organization not found from identifier.</value>\n  </data>\n  <data name=\"InvalidAuthenticationOptionsForSaml2SchemeError\" xml:space=\"preserve\">\n    <value>Invalid authentication options provided to SAML2 scheme.</value>\n  </data>\n  <data name=\"InvalidAuthenticationOptionsForOidcSchemeError\" xml:space=\"preserve\">\n    <value>Invalid authentication options provided to OpenID Connect scheme.</value>\n  </data>\n  <data name=\"PostConfigurationNotExecutedError\" xml:space=\"preserve\">\n    <value>Post configuration not executed against OpenID Connect scheme.</value>\n  </data>\n  <data name=\"ReadingOpenIdConnectMetadataFailedError\" xml:space=\"preserve\">\n    <value>Reading OpenID Connect metadata failed.</value>\n  </data>\n  <data name=\"NoOpenIdConnectMetadataError\" xml:space=\"preserve\">\n    <value>No OpenID Connect metadata could be found or loaded.</value>\n  </data>\n  <data name=\"PreValidationError\" xml:space=\"preserve\">\n    <value>Error performing pre validation.</value>\n  </data>\n  <data name=\"Error\" xml:space=\"preserve\">\n    <value>Error</value>\n  </data>\n  <data name=\"SsoError\" xml:space=\"preserve\">\n    <value>There was an unexpected error during single sign-on.</value>\n  </data>\n  <data name=\"SsoErrorWithRedirect\" xml:space=\"preserve\">\n    <value>There was an unexpected error during single sign-on. Please go back to &lt;a href=\"{0}\"&gt;{0}&lt;/a&gt;.</value>\n  </data>\n  <data name=\"RequestId\" xml:space=\"preserve\">\n    <value>Request ID</value>\n  </data>\n  <data name=\"Redirecting\" xml:space=\"preserve\">\n    <value>Redirecting</value>\n  </data>\n  <data name=\"RedirectingMessage\" xml:space=\"preserve\">\n    <value>You are now being returned to the application. Once complete, you may close this tab.</value>\n  </data>\n  <data name=\"IfIdpWantAuthnRequestsSigned\" xml:space=\"preserve\">\n    <value>If IdP Wants Authn Requests Signed</value>\n  </data>\n  <data name=\"Always\" xml:space=\"preserve\">\n    <value>Always</value>\n  </data>\n  <data name=\"Never\" xml:space=\"preserve\">\n    <value>Never</value>\n  </data>\n  <data name=\"IdpX509PublicCertValidationError\" xml:space=\"preserve\">\n    <value>The IdP public certificate provided is invalid: {0}</value>\n  </data>\n  <data name=\"IdpX509PublicCertInvalidFormatValidationError\" xml:space=\"preserve\">\n    <value>The IdP public certificate provided is not a valid Base64 encoded string, contains illegal characters or whitespace, or is incomplete.</value>\n  </data>\n  <data name=\"IdpX509PublicCertCryptographicExceptionValidationError\" xml:space=\"preserve\">\n    <value>The IdP public certificate provided does not appear to be a valid certificate, please ensure this is a valid, Base64 encoded PEM or CER format public certificate valid for signing: {0}</value>\n  </data>\n  <data name=\"CopyCallbackPath\" xml:space=\"preserve\">\n    <value>Copy the OIDC callback path to your clipboard</value>\n  </data>\n  <data name=\"CopySignedOutCallbackPath\" xml:space=\"preserve\">\n    <value>Copy the OIDC signed out callback path to your clipboard</value>\n  </data>\n  <data name=\"CopySpEntityId\" xml:space=\"preserve\">\n    <value>Copy the SP Entity Id to your clipboard</value>\n  </data>\n  <data name=\"CopySpMetadataUrl\" xml:space=\"preserve\">\n    <value>Copy the SAML 2.0 Metadata URL to your clipboard</value>\n  </data>\n  <data name=\"LaunchSpMetadataUrl\" xml:space=\"preserve\">\n    <value>View the SAML 2.0 Metadata (opens in a new window)</value>\n  </data>\n  <data name=\"CopySpAcsUrl\" xml:space=\"preserve\">\n    <value>Copy the Assertion Consumer Service (ACS) URL to your clipboard</value>\n  </data>\n  <data name=\"HttpRedirect\" xml:space=\"preserve\">\n    <value>Redirect</value>\n    <comment>A SAML binding type, Redirect</comment>\n  </data>\n  <data name=\"HttpPost\" xml:space=\"preserve\">\n    <value>HTTP POST</value>\n    <comment>A SAML binding type, HTTP POST</comment>\n  </data>\n  <data name=\"Artifact\" xml:space=\"preserve\">\n    <value>Artifact</value>\n    <comment>A SAML binding type, Artifact</comment>\n  </data>\n  <data name=\"NoDomainHintProvided\" xml:space=\"preserve\">\n    <value>No domain_hint provided.</value>\n  </data>\n  <data name=\"InvalidReturnUrl\" xml:space=\"preserve\">\n    <value>invalid return URL</value>\n  </data>\n  <data name=\"ExternalAuthenticationError\" xml:space=\"preserve\">\n    <value>External authentication error</value>\n  </data>\n  <data name=\"UnknownUserId\" xml:space=\"preserve\">\n    <value>Unknown userid</value>\n  </data>\n  <data name=\"OrganizationOrSsoConfigNotFound\" xml:space=\"preserve\">\n    <value>Organization not found or SSO configuration not enabled</value>\n  </data>\n  <data name=\"SSOProviderIsNotAnOrgId\" xml:space=\"preserve\">\n    <value>SSO provider, {0} is not an organization id</value>\n  </data>\n  <data name=\"CannotFindEmailClaim\" xml:space=\"preserve\">\n    <value>Cannot find email claim</value>\n  </data>\n  <data name=\"InvalidUserIdentifier\" xml:space=\"preserve\">\n    <value>Invalid user identifier.</value>\n  </data>\n  <data name=\"UserIdAndTokenMismatch\" xml:space=\"preserve\">\n    <value>Supplied userId and token did not match.</value>\n  </data>\n  <data name=\"UserShouldBeFound\" xml:space=\"preserve\">\n    <value>User should have been defined by this point.</value>\n  </data>\n  <data name=\"CouldNotFindOrganization\" xml:space=\"preserve\">\n    <value>Could not find organization for '{0}'</value>\n  </data>\n  <data name=\"CouldNotFindOrganizationUser\" xml:space=\"preserve\">\n    <value>Could not find organization user for user '{0}' organization '{1}'</value>\n  </data>\n  <data name=\"NoSeatsAvailable\" xml:space=\"preserve\">\n    <value>No seats available for organization, '{0}'</value>\n  </data>\n  <data name=\"AcceptInviteBeforeUsingSSO\" xml:space=\"preserve\">\n    <value>To accept your invite to {0}, you must first log in using your master password. Once your invite has been accepted, you will be able to log in using SSO.</value>\n  </data>\n  <data name=\"OrganizationUserAccessRevoked\" xml:space=\"preserve\">\n    <value>Your access to organization {0} has been revoked. Please contact your administrator for assistance.</value>\n  </data>\n  <data name=\"OrganizationUserUnknownStatus\" xml:space=\"preserve\">\n    <value>Your access to organization {0} is in an unknown state. Please contact your administrator for assistance.</value>\n  </data>\n  <data name=\"UserAlreadyExistsInviteProcess\" xml:space=\"preserve\">\n    <value>You were removed from the organization managing single sign-on for your account. Contact the organization administrator for help regaining access to your account.</value>\n  </data>\n  <data name=\"UserAlreadyExistsKeyConnector\" xml:space=\"preserve\">\n    <value>You were removed from the organization managing single sign-on for your account. Create a new account to continue using Bitwarden.</value>\n  </data>\n  <data name=\"RedirectGet\" xml:space=\"preserve\">\n    <value>Redirect GET</value>\n    <comment>An OIDC Connect Redirect Behavior, Redirect; Emits a 302 response\n    to redirect the user agent to the OpenID Connect provider using a GET request.</comment>\n  </data>\n  <data name=\"FormPost\" xml:space=\"preserve\">\n    <value>Form POST</value>\n    <comment>An OIDC Connect Redirect Behavior, Form POST; Emits an HTML form to\n      redirect the user agent to the OpenID Connect provider using a POST request.</comment>\n  </data>\n  <data name=\"RedirectBehavior\" xml:space=\"preserve\">\n    <value>OIDC Redirect Behavior</value>\n  </data>\n  <data name=\"SingleOrganization\" xml:space=\"preserve\">\n    <value>Single Organization</value>\n  </data>\n  <data name=\"SingleOrganizationDescription\" xml:space=\"preserve\">\n    <value>Restrict users from being able to join any other organizations.</value>\n  </data>\n  <data name=\"SingleOrganizationPolicyWarning\" xml:space=\"preserve\">\n    <value>Organization members who are not Owners or Administrators and are already a part of another organization will be removed from this organization and will receive an email notifying them about the change.</value>\n  </data>\n  <data name=\"RequireSso\" xml:space=\"preserve\">\n    <value>Single Sign-On Authentication</value>\n  </data>\n  <data name=\"RequireSsoDescription\" xml:space=\"preserve\">\n    <value>Require users to log in with the Enterprise Single Sign-On method.</value>\n  </data>\n  <data name=\"Prerequisite\" xml:space=\"preserve\">\n    <value>Prerequisite</value>\n  </data>\n  <data name=\"RequireSsoPolicyReq\" xml:space=\"preserve\">\n    <value>The Single Organization enterprise policy must be enabled before activating this policy.</value>\n  </data>\n  <data name=\"RequireSsoPolicyReqError\" xml:space=\"preserve\">\n    <value>Single Organization policy not enabled.</value>\n  </data>\n  <data name=\"RequireSsoExemption\" xml:space=\"preserve\">\n    <value>Organization Owners and Administrators are exempt from this policy's enforcement.</value>\n  </data>\n  <data name=\"PersonalOwnership\" xml:space=\"preserve\">\n    <value>Personal Ownership</value>\n  </data>\n  <data name=\"PersonalOwnershipDescription\" xml:space=\"preserve\">\n    <value>Require users to save vault items to an organization by removing the personal ownership option.</value>\n  </data>\n  <data name=\"PersonalOwnershipExemption\" xml:space=\"preserve\">\n    <value>Organization users that can manage the organization's policies are exempt from this policy's enforcement.</value>\n  </data>\n  <data name=\"DisableSend\" xml:space=\"preserve\">\n    <value>Disable Send</value>\n    <comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>\n  </data>\n  <data name=\"DisableSendDescription\" xml:space=\"preserve\">\n    <value>Do not allow users to create or edit a Bitwarden Send. Deleting an existing Send is still allowed.</value>\n    <comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>\n  </data>\n  <data name=\"DisableSendExemption\" xml:space=\"preserve\">\n    <value>Organization Owners and Administrators are exempt from this policy's enforcement.</value>\n  </data>\n  <data name=\"SendOptions\" xml:space=\"preserve\">\n    <value>Send Options</value>\n    <comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>\n  </data>\n  <data name=\"Options\" xml:space=\"preserve\">\n    <value>Options</value>\n  </data>\n  <data name=\"DisableHideEmail\" xml:space=\"preserve\">\n    <value>Do not allow users to hide their email address when creating or editing a Send.</value>\n    <comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>\n  </data>\n  <data name=\"SendOptionsDescription\" xml:space=\"preserve\">\n    <value>Set options for creating and editing Sends.</value>\n    <comment>'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.</comment>\n  </data>\n  <data name=\"SendOptionsExemption\" xml:space=\"preserve\">\n    <value>Organization Owners and Administrators are exempt from this policy's enforcement.</value>\n  </data>\n  <data name=\"DisableRequireSsoError\" xml:space=\"preserve\">\n    <value>You must manually disable the Single Sign-On Authentication policy before this policy can be disabled.</value>\n  </data>\n  <data name=\"PersonalOwnershipCheckboxDesc\" xml:space=\"preserve\">\n    <value>Disable personal ownership for organization users</value>\n  </data>\n  <data name=\"AdditionalScopes\" xml:space=\"preserve\">\n    <value>Additional/Custom Scopes (comma delimited)</value>\n  </data>\n  <data name=\"AdditionalUserIdClaimTypes\" xml:space=\"preserve\">\n    <value>Additional/Custom User ID Claim Types (comma delimited)</value>\n  </data>\n  <data name=\"AdditionalEmailClaimTypes\" xml:space=\"preserve\">\n    <value>Additional/Custom Email Claim Types (comma delimited)</value>\n  </data>\n  <data name=\"AdditionalNameClaimTypes\" xml:space=\"preserve\">\n    <value>Additional/Custom Name Claim Types (comma delimited)</value>\n  </data>\n  <data name=\"AcrValues\" xml:space=\"preserve\">\n    <value>Requested Authentication Context Class Reference values (acr_values)</value>\n    <comment>'acr_values' is an explicit OIDC param, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. It should not be translated.</comment>\n  </data>\n  <data name=\"ExpectedReturnAcrValue\" xml:space=\"preserve\">\n    <value>Expected \"acr\" Claim Value In Response (acr validation)</value>\n    <comment>'acr' is an explicit OIDC claim type, see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2 (acr). It should not be translated.</comment>\n  </data>\n  <data name=\"AccessDeniedError\" xml:space=\"preserve\">\n    <value>Access Denied to this resource.</value>\n  </data>\n  <data name=\"AcrMissingOrInvalid\" xml:space=\"preserve\">\n    <value>Expected authentication context class reference (acr) was not returned with the authentication response or is invalid.</value>\n    <comment>'acr' is an explicit OIDC claim type, see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2 (acr). It should not be translated.</comment>\n  </data>\n  <data name=\"ResetPassword\" xml:space=\"preserve\">\n    <value>Master Password Reset</value>\n  </data>\n  <data name=\"ResetPasswordDescription\" xml:space=\"preserve\">\n    <value>Allow administrators in the organization to reset organization users' master password.</value>\n  </data>\n  <data name=\"ResetPasswordWarning\" xml:space=\"preserve\">\n    <value>Users in the organization will need to self-enroll or be auto-enrolled before administrators can reset their master password.</value>\n  </data>\n  <data name=\"ResetPasswordAutoEnroll\" xml:space=\"preserve\">\n    <value>Automatic Enrollment</value>\n  </data>\n  <data name=\"ResetPasswordAutoEnrollDescription\" xml:space=\"preserve\">\n    <value>All users will be automatically enrolled in password reset once their invite is accepted and will not be allowed to withdraw.</value>\n  </data>\n  <data name=\"ResetPasswordAutoEnrollWarning\" xml:space=\"preserve\">\n    <value>Users already in the organization will not be retroactively enrolled in password reset. They will need to self-enroll before administrators can reset their master password.</value>\n  </data>\n  <data name=\"ResetPasswordAutoEnrollCheckbox\" xml:space=\"preserve\">\n    <value>Require new users to be enrolled automatically</value>\n  </data>\n  <data name=\"IdpArtifactResolutionServiceUrlInvalid\" xml:space=\"preserve\">\n    <value>Artifact resolution service URL contains illegal characters.</value>\n  </data>\n  <data name=\"IdpSingleLogoutServiceUrlInvalid\" xml:space=\"preserve\">\n    <value>Single log out service URL contains illegal characters.</value>\n  </data>\n  <data name=\"IdpSingleSignOnServiceUrlInvalid\" xml:space=\"preserve\">\n    <value>Single sign on service URL contains illegal characters.</value>\n  </data>\n  <data name=\"SsoRedirectTokenValidationMissing\" xml:space=\"preserve\">\n    <value>Single sign on redirect token is missing from the request.</value>\n  </data>\n  <data name=\"InvalidSsoRedirectToken\" xml:space=\"preserve\">\n    <value>Single sign on redirect token is invalid or expired.</value>\n  </data>\n  <data name=\"SsoInvalidIdentifierError\" xml:space=\"preserve\">\n    <value>Invalid SSO identifier</value>\n  </data>\n</root>\n"
  },
  {
    "path": "src/Core/SecretsManager/AuthorizationRequirements/BulkSecretOperationRequirement.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.SecretsManager.AuthorizationRequirements;\n\npublic class BulkSecretOperationRequirement : OperationAuthorizationRequirement\n{\n}\n\npublic static class BulkSecretOperations\n{\n    public static readonly BulkSecretOperationRequirement ReadAll = new() { Name = nameof(ReadAll) };\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/AuthorizationRequirements/ProjectOperationRequirement.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.SecretsManager.AuthorizationRequirements;\n\npublic class ProjectOperationRequirement : OperationAuthorizationRequirement\n{\n}\n\npublic static class ProjectOperations\n{\n    public static readonly ProjectOperationRequirement Create = new() { Name = nameof(Create) };\n    public static readonly ProjectOperationRequirement Update = new() { Name = nameof(Update) };\n    public static readonly ProjectOperationRequirement Delete = new() { Name = nameof(Delete) };\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/AuthorizationRequirements/ProjectPeopleAccessPoliciesOperationRequirement.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.SecretsManager.AuthorizationRequirements;\n\npublic class ProjectPeopleAccessPoliciesOperationRequirement : OperationAuthorizationRequirement\n{\n}\n\npublic static class ProjectPeopleAccessPoliciesOperations\n{\n    public static readonly ProjectPeopleAccessPoliciesOperationRequirement Replace = new() { Name = nameof(Replace) };\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/AuthorizationRequirements/ProjectServiceAccountsAccessPoliciesOperationRequirement.cs",
    "content": "﻿#nullable enable\nusing Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.SecretsManager.AuthorizationRequirements;\n\npublic class ProjectServiceAccountsAccessPoliciesOperationRequirement : OperationAuthorizationRequirement\n{\n\n}\n\npublic static class ProjectServiceAccountsAccessPoliciesOperations\n{\n    public static readonly ProjectServiceAccountsAccessPoliciesOperationRequirement Updates = new() { Name = nameof(Updates) };\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/AuthorizationRequirements/SecretAccessPoliciesOperationRequirement.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.SecretsManager.AuthorizationRequirements;\n\npublic class SecretAccessPoliciesOperationRequirement : OperationAuthorizationRequirement\n{\n}\n\npublic static class SecretAccessPoliciesOperations\n{\n    public static readonly SecretAccessPoliciesOperationRequirement Updates = new() { Name = nameof(Updates) };\n    public static readonly SecretAccessPoliciesOperationRequirement Create = new() { Name = nameof(Create) };\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/AuthorizationRequirements/SecretOperationRequirement.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.SecretsManager.AuthorizationRequirements;\n\npublic class SecretOperationRequirement : OperationAuthorizationRequirement\n{\n}\n\npublic static class SecretOperations\n{\n    public static readonly SecretOperationRequirement Create = new() { Name = nameof(Create) };\n    public static readonly SecretOperationRequirement Read = new() { Name = nameof(Read) };\n    public static readonly SecretOperationRequirement Update = new() { Name = nameof(Update) };\n    public static readonly SecretOperationRequirement Delete = new() { Name = nameof(Delete) };\n    public static readonly SecretOperationRequirement ReadAccessPolicies = new() { Name = nameof(ReadAccessPolicies) };\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountGrantedPoliciesOperationRequirement.cs",
    "content": "﻿#nullable enable\nusing Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.SecretsManager.AuthorizationRequirements;\n\npublic class ServiceAccountGrantedPoliciesOperationRequirement : OperationAuthorizationRequirement\n{\n\n}\n\npublic static class ServiceAccountGrantedPoliciesOperations\n{\n    public static readonly ServiceAccountGrantedPoliciesOperationRequirement Updates = new() { Name = nameof(Updates) };\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountOperationRequirement.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.SecretsManager.AuthorizationRequirements;\n\npublic class ServiceAccountOperationRequirement : OperationAuthorizationRequirement\n{\n}\n\npublic static class ServiceAccountOperations\n{\n    public static readonly ServiceAccountOperationRequirement Create = new() { Name = nameof(Create) };\n    public static readonly ServiceAccountOperationRequirement Read = new() { Name = nameof(Read) };\n    public static readonly ServiceAccountOperationRequirement Update = new() { Name = nameof(Update) };\n    public static readonly ServiceAccountOperationRequirement Delete = new() { Name = nameof(Delete) };\n    public static readonly ServiceAccountOperationRequirement ReadAccessTokens = new() { Name = nameof(ReadAccessTokens) };\n    public static readonly ServiceAccountOperationRequirement CreateAccessToken = new() { Name = nameof(CreateAccessToken) };\n    public static readonly ServiceAccountOperationRequirement RevokeAccessTokens = new() { Name = nameof(RevokeAccessTokens) };\n    public static readonly ServiceAccountOperationRequirement ReadEvents = new() { Name = nameof(ReadEvents) };\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/AuthorizationRequirements/ServiceAccountPeopleAccessPoliciesOperationRequirement.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.SecretsManager.AuthorizationRequirements;\n\npublic class ServiceAccountPeopleAccessPoliciesOperationRequirement : OperationAuthorizationRequirement\n{\n}\n\npublic static class ServiceAccountPeopleAccessPoliciesOperations\n{\n    public static readonly ServiceAccountPeopleAccessPoliciesOperationRequirement Replace = new() { Name = nameof(Replace) };\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateProjectServiceAccountsAccessPoliciesCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;\n\npublic interface IUpdateProjectServiceAccountsAccessPoliciesCommand\n{\n    Task UpdateAsync(ProjectServiceAccountsAccessPoliciesUpdates accessPoliciesUpdates);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/AccessPolicies/Interfaces/IUpdateServiceAccountGrantedPoliciesCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;\n\npublic interface IUpdateServiceAccountGrantedPoliciesCommand\n{\n    Task UpdateAsync(ServiceAccountGrantedPoliciesUpdates grantedPoliciesUpdates);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/AccessTokens/Interfaces/ICreateAccessTokenCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;\n\npublic interface ICreateAccessTokenCommand\n{\n    Task<ApiKeyClientSecretDetails> CreateAsync(ApiKey apiKey);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Porting/Interfaces/IImportCommand.cs",
    "content": "﻿namespace Bit.Core.SecretsManager.Commands.Porting.Interfaces;\n\npublic interface IImportCommand\n{\n    Task ImportAsync(Guid organizationId, SMImport import);\n    SMImport AssignNewIds(SMImport import);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Porting/SMImport.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.SecretsManager.Commands.Porting;\n\npublic class SMImport\n{\n    public IEnumerable<InnerProject> Projects { get; set; }\n    public IEnumerable<InnerSecret> Secrets { get; set; }\n\n    public class InnerProject\n    {\n        public InnerProject() { }\n\n        public InnerProject(Core.SecretsManager.Entities.Project project)\n        {\n            Id = project.Id;\n            Name = project.Name;\n        }\n\n        public Guid Id { get; set; }\n        public string Name { get; set; }\n    }\n\n    public class InnerSecret\n    {\n        public InnerSecret() { }\n\n        public InnerSecret(Core.SecretsManager.Entities.Secret secret)\n        {\n            Id = secret.Id;\n            Key = secret.Key;\n            Value = secret.Value;\n            Note = secret.Note;\n            ProjectIds = secret.Projects != null && secret.Projects.Any() ? secret.Projects.Select(p => p.Id) : null;\n        }\n\n        public Guid Id { get; set; }\n        public string Key { get; set; }\n        public string Value { get; set; }\n        public string Note { get; set; }\n        public IEnumerable<Guid> ProjectIds { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Projects/Interfaces/ICreateProjectCommand.cs",
    "content": "﻿using Bit.Core.Auth.Identity;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Commands.Projects.Interfaces;\n\npublic interface ICreateProjectCommand\n{\n    Task<Project> CreateAsync(Project project, Guid userId, IdentityClientType identityClientType);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Projects/Interfaces/IDeleteProjectCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Commands.Projects.Interfaces;\n\npublic interface IDeleteProjectCommand\n{\n    Task DeleteProjects(IEnumerable<Project> projects);\n}\n\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Projects/Interfaces/IUpdateProjectCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Commands.Projects.Interfaces;\n\npublic interface IUpdateProjectCommand\n{\n    Task<Project> UpdateAsync(Project updatedProject);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Requests/Interfaces/IRequestSMAccessCommand.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Core.SecretsManager.Commands.Requests.Interfaces;\n\npublic interface IRequestSMAccessCommand\n{\n    Task SendRequestAccessToSM(Organization organization, ICollection<OrganizationUserUserDetails> orgUsers, User user, string emailContent);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Secrets/Interfaces/ICreateSecretCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;\n\npublic interface ICreateSecretCommand\n{\n    Task<Secret> CreateAsync(Secret secret, SecretAccessPoliciesUpdates? accessPoliciesUpdates);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Secrets/Interfaces/IDeleteSecretCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;\n\npublic interface IDeleteSecretCommand\n{\n    Task DeleteSecrets(IEnumerable<Secret> secrets);\n}\n\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Secrets/Interfaces/IUpdateSecretCommand.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Commands.Secrets.Interfaces;\n\npublic interface IUpdateSecretCommand\n{\n    Task<Secret> UpdateAsync(Secret secret, SecretAccessPoliciesUpdates? accessPolicyUpdates);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/ICreateServiceAccountCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\n\npublic interface ICreateServiceAccountCommand\n{\n    Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount, Guid userId);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/IDeleteServiceAccountsCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\n\npublic interface IDeleteServiceAccountsCommand\n{\n    Task DeleteServiceAccounts(IEnumerable<ServiceAccount> serviceAccounts);\n}\n\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/IRevokeAccessTokensCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\n\npublic interface IRevokeAccessTokensCommand\n{\n    Task RevokeAsync(ServiceAccount serviceAccount, IEnumerable<Guid> ids);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/IUpdateServiceAccountCommand.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\n\npublic interface IUpdateServiceAccountCommand\n{\n    Task<ServiceAccount> UpdateAsync(ServiceAccount serviceAccount);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Trash/IEmptyTrashCommand.cs",
    "content": "﻿namespace Bit.Core.SecretsManager.Commands.Trash.Interfaces;\n\npublic interface IEmptyTrashCommand\n{\n    Task EmptyTrash(Guid organizationId, List<Guid> ids);\n}\n\n"
  },
  {
    "path": "src/Core/SecretsManager/Commands/Trash/IRestoreTrashCommand.cs",
    "content": "﻿namespace Bit.Core.SecretsManager.Commands.Trash.Interfaces;\n\npublic interface IRestoreTrashCommand\n{\n    Task RestoreTrash(Guid organizationId, List<Guid> ids);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Entities/AccessPolicy.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.SecretsManager.Entities;\n\npublic abstract class BaseAccessPolicy\n{\n    public Guid Id { get; set; }\n\n    // Access\n    public bool Read { get; set; }\n    public bool Write { get; set; }\n\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n\npublic class UserProjectAccessPolicy : BaseAccessPolicy\n{\n    public Guid? OrganizationUserId { get; set; }\n    public User? User { get; set; }\n    public Guid? GrantedProjectId { get; set; }\n    public Project? GrantedProject { get; set; }\n}\n\npublic class UserServiceAccountAccessPolicy : BaseAccessPolicy\n{\n    public Guid? OrganizationUserId { get; set; }\n    public User? User { get; set; }\n    public Guid? GrantedServiceAccountId { get; set; }\n    public ServiceAccount? GrantedServiceAccount { get; set; }\n}\n\npublic class UserSecretAccessPolicy : BaseAccessPolicy\n{\n    public Guid? OrganizationUserId { get; set; }\n    public User? User { get; set; }\n    public Guid? GrantedSecretId { get; set; }\n    public Secret? GrantedSecret { get; set; }\n}\n\npublic class GroupProjectAccessPolicy : BaseAccessPolicy\n{\n    public Guid? GroupId { get; set; }\n    public Group? Group { get; set; }\n    public bool? CurrentUserInGroup { get; set; }\n    public Guid? GrantedProjectId { get; set; }\n    public Project? GrantedProject { get; set; }\n}\n\npublic class GroupServiceAccountAccessPolicy : BaseAccessPolicy\n{\n    public Guid? GroupId { get; set; }\n    public Group? Group { get; set; }\n    public bool? CurrentUserInGroup { get; set; }\n    public Guid? GrantedServiceAccountId { get; set; }\n    public ServiceAccount? GrantedServiceAccount { get; set; }\n}\n\npublic class GroupSecretAccessPolicy : BaseAccessPolicy\n{\n    public Guid? GroupId { get; set; }\n    public Group? Group { get; set; }\n    public bool? CurrentUserInGroup { get; set; }\n    public Guid? GrantedSecretId { get; set; }\n    public Secret? GrantedSecret { get; set; }\n}\n\npublic class ServiceAccountProjectAccessPolicy : BaseAccessPolicy\n{\n    public Guid? ServiceAccountId { get; set; }\n    public ServiceAccount? ServiceAccount { get; set; }\n    public Guid? GrantedProjectId { get; set; }\n    public Project? GrantedProject { get; set; }\n}\n\npublic class ServiceAccountSecretAccessPolicy : BaseAccessPolicy\n{\n    public Guid? ServiceAccountId { get; set; }\n    public ServiceAccount? ServiceAccount { get; set; }\n    public Guid? GrantedSecretId { get; set; }\n    public Secret? GrantedSecret { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Entities/ApiKey.cs",
    "content": "﻿#nullable enable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.SecretsManager.Entities;\n\npublic class ApiKey : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid? ServiceAccountId { get; set; }\n    [MaxLength(200)]\n    public required string Name { get; set; }\n    [MaxLength(128)]\n    public string? ClientSecretHash { get; set; }\n    [MaxLength(4000)]\n    public required string Scope { get; set; }\n    [MaxLength(4000)]\n    public required string EncryptedPayload { get; set; }\n    // Key for decrypting `EncryptedPayload`. Encrypted using the organization key.\n    public required string Key { get; set; }\n    public DateTime? ExpireAt { get; set; }\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public ICollection<string> GetScopes()\n    {\n        return CoreHelpers.LoadClassFromJsonData<List<string>>(Scope);\n    }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Entities/Project.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.SecretsManager.Entities;\n\npublic class Project : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n\n    public Guid OrganizationId { get; set; }\n\n    public string? Name { get; set; }\n\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public DateTime? DeletedDate { get; set; }\n\n    public virtual ICollection<Secret>? Secrets { get; set; }\n\n    public void SetNewId()\n    {\n        if (Id == default(Guid))\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Entities/Secret.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.SecretsManager.Entities;\n\npublic class Secret : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n\n    public Guid OrganizationId { get; set; }\n\n    public string? Key { get; set; }\n\n    public string? Value { get; set; }\n\n    public string? Note { get; set; }\n\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public DateTime? DeletedDate { get; set; }\n\n    public ICollection<Project>? Projects { get; set; }\n\n    public void SetNewId()\n    {\n        if (Id == default(Guid))\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Entities/SecretVersion.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.SecretsManager.Entities;\n\npublic class SecretVersion : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n\n    public Guid SecretId { get; set; }\n\n    public string Value { get; set; } = string.Empty;\n\n    public DateTime VersionDate { get; set; }\n\n    public Guid? EditorServiceAccountId { get; set; }\n\n    public Guid? EditorOrganizationUserId { get; set; }\n\n    public void SetNewId()\n    {\n        if (Id == default(Guid))\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Entities/ServiceAccount.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.SecretsManager.Entities;\n\npublic class ServiceAccount : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n\n    public Guid OrganizationId { get; set; }\n\n    public string? Name { get; set; }\n\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        if (Id == default(Guid))\n        {\n            Id = CoreHelpers.GenerateComb();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Enums/AccessPolicies/AccessPolicyOperation.cs",
    "content": "﻿namespace Bit.Core.SecretsManager.Enums.AccessPolicies;\n\npublic enum AccessPolicyOperation\n{\n    Create,\n    Update,\n    Delete\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/AccessPolicyUpdates/AccessPolicyUpdate.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\n\nnamespace Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\npublic class UserSecretAccessPolicyUpdate\n{\n    public AccessPolicyOperation Operation { get; set; }\n    public required UserSecretAccessPolicy AccessPolicy { get; set; }\n}\n\npublic class GroupSecretAccessPolicyUpdate\n{\n    public AccessPolicyOperation Operation { get; set; }\n    public required GroupSecretAccessPolicy AccessPolicy { get; set; }\n}\n\npublic class ServiceAccountSecretAccessPolicyUpdate\n{\n    public AccessPolicyOperation Operation { get; set; }\n    public required ServiceAccountSecretAccessPolicy AccessPolicy { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/AccessPolicyUpdates/ProjectServiceAccountsAccessPoliciesUpdates.cs",
    "content": "﻿#nullable enable\nnamespace Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\npublic class ProjectServiceAccountsAccessPoliciesUpdates\n{\n    public Guid ProjectId { get; set; }\n    public Guid OrganizationId { get; set; }\n    public IEnumerable<ServiceAccountProjectAccessPolicyUpdate> ServiceAccountAccessPolicyUpdates { get; set; } = [];\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/AccessPolicyUpdates/SecretAccessPoliciesUpdates.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\n\nnamespace Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\npublic class SecretAccessPoliciesUpdates\n{\n    public SecretAccessPoliciesUpdates(SecretAccessPolicies accessPolicies)\n    {\n        SecretId = accessPolicies.SecretId;\n        OrganizationId = accessPolicies.OrganizationId;\n        UserAccessPolicyUpdates =\n            accessPolicies.UserAccessPolicies.Select(x =>\n                new UserSecretAccessPolicyUpdate { Operation = AccessPolicyOperation.Create, AccessPolicy = x });\n\n        GroupAccessPolicyUpdates =\n            accessPolicies.GroupAccessPolicies.Select(x =>\n                new GroupSecretAccessPolicyUpdate { Operation = AccessPolicyOperation.Create, AccessPolicy = x });\n\n        ServiceAccountAccessPolicyUpdates = accessPolicies.ServiceAccountAccessPolicies.Select(x =>\n            new ServiceAccountSecretAccessPolicyUpdate { Operation = AccessPolicyOperation.Create, AccessPolicy = x });\n    }\n\n    public SecretAccessPoliciesUpdates() { }\n\n    public Guid SecretId { get; set; }\n    public Guid OrganizationId { get; set; }\n    public IEnumerable<UserSecretAccessPolicyUpdate> UserAccessPolicyUpdates { get; set; } = [];\n    public IEnumerable<GroupSecretAccessPolicyUpdate> GroupAccessPolicyUpdates { get; set; } = [];\n    public IEnumerable<ServiceAccountSecretAccessPolicyUpdate> ServiceAccountAccessPolicyUpdates { get; set; } = [];\n\n    public bool HasUpdates() =>\n        UserAccessPolicyUpdates.Any() ||\n        GroupAccessPolicyUpdates.Any() ||\n        ServiceAccountAccessPolicyUpdates.Any();\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/AccessPolicyUpdates/ServiceAccountProjectAccessPolicyUpdate.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\n\nnamespace Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\npublic class ServiceAccountProjectAccessPolicyUpdate\n{\n    public AccessPolicyOperation Operation { get; set; }\n    public required ServiceAccountProjectAccessPolicy AccessPolicy { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ApiKeyClientSecretDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ApiKeyClientSecretDetails\n{\n    public ApiKey ApiKey { get; set; }\n    public string ClientSecret { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Diagnostics.CodeAnalysis;\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ApiKeyDetails : ApiKey\n{\n    protected ApiKeyDetails() { }\n\n    protected ApiKeyDetails(ApiKey apiKey)\n    {\n        Id = apiKey.Id;\n        ServiceAccountId = apiKey.ServiceAccountId;\n        Name = apiKey.Name;\n        ClientSecretHash = apiKey.ClientSecretHash;\n        Scope = apiKey.Scope;\n        EncryptedPayload = apiKey.EncryptedPayload;\n        Key = apiKey.Key;\n        ExpireAt = apiKey.ExpireAt;\n        CreationDate = apiKey.CreationDate;\n        RevisionDate = apiKey.RevisionDate;\n    }\n}\n\npublic class ServiceAccountApiKeyDetails : ApiKeyDetails\n{\n    public ServiceAccountApiKeyDetails()\n    {\n\n    }\n\n    [SetsRequiredMembers]\n    public ServiceAccountApiKeyDetails(ApiKey apiKey, Guid organizationId) : base(apiKey)\n    {\n        ServiceAccountOrganizationId = organizationId;\n    }\n\n    public Guid ServiceAccountOrganizationId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/PeopleGrantees.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class PeopleGrantees\n{\n    public IEnumerable<UserGrantee> UserGrantees { get; set; }\n    public IEnumerable<GroupGrantee> GroupGrantees { get; set; }\n}\n\npublic class UserGrantee\n{\n    public Guid OrganizationUserId { get; set; }\n    public string Name { get; set; }\n    public string Email { get; set; }\n    public bool CurrentUser { get; set; }\n}\n\npublic class GroupGrantee\n{\n    public Guid GroupId { get; set; }\n    public string Name { get; set; }\n    public bool CurrentUserInGroup { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ProjectCounts.cs",
    "content": "﻿namespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ProjectCounts\n{\n    public int Secrets { get; set; }\n\n    public int People { get; set; }\n\n    public int ServiceAccounts { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ProjectPeopleAccessPolicies.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ProjectPeopleAccessPolicies\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public IEnumerable<UserProjectAccessPolicy> UserAccessPolicies { get; set; }\n    public IEnumerable<GroupProjectAccessPolicy> GroupAccessPolicies { get; set; }\n\n    public IEnumerable<BaseAccessPolicy> ToBaseAccessPolicies()\n    {\n        var policies = new List<BaseAccessPolicy>();\n        if (UserAccessPolicies != null && UserAccessPolicies.Any())\n        {\n            policies.AddRange(UserAccessPolicies);\n        }\n\n        if (GroupAccessPolicies != null && GroupAccessPolicies.Any())\n        {\n            policies.AddRange(GroupAccessPolicies);\n        }\n\n        return policies;\n    }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ProjectPermissionDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ProjectPermissionDetails\n{\n    public Project Project { get; set; }\n    public bool Read { get; set; }\n    public bool Write { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ProjectServiceAccountsAccessPolicies.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ProjectServiceAccountsAccessPolicies\n{\n    public ProjectServiceAccountsAccessPolicies()\n    {\n    }\n\n    public ProjectServiceAccountsAccessPolicies(Guid projectId,\n        IEnumerable<BaseAccessPolicy> policies)\n    {\n        ProjectId = projectId;\n        ServiceAccountAccessPolicies = policies\n            .OfType<ServiceAccountProjectAccessPolicy>()\n            .ToList();\n\n        var project = ServiceAccountAccessPolicies.FirstOrDefault()?.GrantedProject;\n        if (project != null)\n        {\n            OrganizationId = project.OrganizationId;\n        }\n    }\n\n    public Guid ProjectId { get; set; }\n    public Guid OrganizationId { get; set; }\n    public IEnumerable<ServiceAccountProjectAccessPolicy> ServiceAccountAccessPolicies { get; set; } = [];\n\n    public ProjectServiceAccountsAccessPoliciesUpdates GetPolicyUpdates(ProjectServiceAccountsAccessPolicies requested)\n    {\n        var currentServiceAccountIds = GetServiceAccountIds(ServiceAccountAccessPolicies);\n        var requestedServiceAccountIds = GetServiceAccountIds(requested.ServiceAccountAccessPolicies);\n\n        var serviceAccountIdsToBeDeleted = currentServiceAccountIds.Except(requestedServiceAccountIds).ToList();\n        var serviceAccountIdsToBeCreated = requestedServiceAccountIds.Except(currentServiceAccountIds).ToList();\n        var serviceAccountIdsToBeUpdated = GetServiceAccountIdsToBeUpdated(requested);\n\n        var policiesToBeDeleted =\n            CreatePolicyUpdates(ServiceAccountAccessPolicies, serviceAccountIdsToBeDeleted,\n                AccessPolicyOperation.Delete);\n        var policiesToBeCreated = CreatePolicyUpdates(requested.ServiceAccountAccessPolicies,\n            serviceAccountIdsToBeCreated,\n            AccessPolicyOperation.Create);\n        var policiesToBeUpdated = CreatePolicyUpdates(requested.ServiceAccountAccessPolicies,\n            serviceAccountIdsToBeUpdated,\n            AccessPolicyOperation.Update);\n\n        return new ProjectServiceAccountsAccessPoliciesUpdates\n        {\n            OrganizationId = OrganizationId,\n            ProjectId = ProjectId,\n            ServiceAccountAccessPolicyUpdates =\n                policiesToBeDeleted.Concat(policiesToBeCreated).Concat(policiesToBeUpdated)\n        };\n    }\n\n    private static List<ServiceAccountProjectAccessPolicyUpdate> CreatePolicyUpdates(\n        IEnumerable<ServiceAccountProjectAccessPolicy> policies, List<Guid> serviceAccountIds,\n        AccessPolicyOperation operation) =>\n        policies\n            .Where(ap => serviceAccountIds.Contains(ap.ServiceAccountId!.Value))\n            .Select(ap => new ServiceAccountProjectAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })\n            .ToList();\n\n    private List<Guid> GetServiceAccountIdsToBeUpdated(ProjectServiceAccountsAccessPolicies requested) =>\n        ServiceAccountAccessPolicies\n            .Where(currentAp => requested.ServiceAccountAccessPolicies.Any(requestedAp =>\n                requestedAp.GrantedProjectId == currentAp.GrantedProjectId &&\n                requestedAp.ServiceAccountId == currentAp.ServiceAccountId &&\n                (requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))\n            .Select(ap => ap.ServiceAccountId!.Value)\n            .ToList();\n\n    private static List<Guid> GetServiceAccountIds(IEnumerable<ServiceAccountProjectAccessPolicy> policies) =>\n        policies.Select(ap => ap.ServiceAccountId!.Value).ToList();\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/SecretAccessPolicies.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class SecretAccessPolicies\n{\n    public SecretAccessPolicies(Guid secretId, Guid organizationId, List<BaseAccessPolicy> policies)\n    {\n        SecretId = secretId;\n        OrganizationId = organizationId;\n\n        UserAccessPolicies = policies\n            .OfType<UserSecretAccessPolicy>()\n            .ToList();\n\n        GroupAccessPolicies = policies\n            .OfType<GroupSecretAccessPolicy>()\n            .ToList();\n\n        ServiceAccountAccessPolicies = policies\n            .OfType<ServiceAccountSecretAccessPolicy>()\n            .ToList();\n    }\n\n    public SecretAccessPolicies()\n    {\n    }\n\n    public Guid SecretId { get; set; }\n    public Guid OrganizationId { get; set; }\n    public IEnumerable<UserSecretAccessPolicy> UserAccessPolicies { get; set; } = [];\n    public IEnumerable<GroupSecretAccessPolicy> GroupAccessPolicies { get; set; } = [];\n    public IEnumerable<ServiceAccountSecretAccessPolicy> ServiceAccountAccessPolicies { get; set; } = [];\n\n    public SecretAccessPoliciesUpdates GetPolicyUpdates(SecretAccessPolicies requested) =>\n        new()\n        {\n            SecretId = SecretId,\n            OrganizationId = OrganizationId,\n            UserAccessPolicyUpdates = GetUserPolicyUpdates(requested.UserAccessPolicies.ToList()),\n            GroupAccessPolicyUpdates = GetGroupPolicyUpdates(requested.GroupAccessPolicies.ToList()),\n            ServiceAccountAccessPolicyUpdates =\n                GetServiceAccountPolicyUpdates(requested.ServiceAccountAccessPolicies.ToList())\n        };\n\n    private static List<TPolicyUpdate> GetPolicyUpdates<TPolicy, TPolicyUpdate>(\n        List<TPolicy> currentPolicies,\n        List<TPolicy> requestedPolicies,\n        Func<IEnumerable<TPolicy>, List<Guid>> getIds,\n        Func<IEnumerable<TPolicy>, List<Guid>> getIdsToBeUpdated,\n        Func<IEnumerable<TPolicy>, List<Guid>, AccessPolicyOperation, List<TPolicyUpdate>> createPolicyUpdates)\n        where TPolicy : class\n        where TPolicyUpdate : class\n    {\n        var currentIds = getIds(currentPolicies);\n        var requestedIds = getIds(requestedPolicies);\n\n        var idsToBeDeleted = currentIds.Except(requestedIds).ToList();\n        var idsToBeCreated = requestedIds.Except(currentIds).ToList();\n        var idsToBeUpdated = getIdsToBeUpdated(requestedPolicies);\n\n        var policiesToBeDeleted = createPolicyUpdates(currentPolicies, idsToBeDeleted, AccessPolicyOperation.Delete);\n        var policiesToBeCreated = createPolicyUpdates(requestedPolicies, idsToBeCreated, AccessPolicyOperation.Create);\n        var policiesToBeUpdated = createPolicyUpdates(requestedPolicies, idsToBeUpdated, AccessPolicyOperation.Update);\n\n        return policiesToBeDeleted.Concat(policiesToBeCreated).Concat(policiesToBeUpdated).ToList();\n    }\n\n    private static List<Guid> GetOrganizationUserIds(IEnumerable<UserSecretAccessPolicy> policies) =>\n        policies.Select(ap => ap.OrganizationUserId!.Value).ToList();\n\n    private static List<Guid> GetGroupIds(IEnumerable<GroupSecretAccessPolicy> policies) =>\n        policies.Select(ap => ap.GroupId!.Value).ToList();\n\n    private static List<Guid> GetServiceAccountIds(IEnumerable<ServiceAccountSecretAccessPolicy> policies) =>\n        policies.Select(ap => ap.ServiceAccountId!.Value).ToList();\n\n    private static List<UserSecretAccessPolicyUpdate> CreateUserPolicyUpdates(\n        IEnumerable<UserSecretAccessPolicy> policies, List<Guid> userIds,\n        AccessPolicyOperation operation) =>\n        policies\n            .Where(ap => userIds.Contains(ap.OrganizationUserId!.Value))\n            .Select(ap => new UserSecretAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })\n            .ToList();\n\n    private static List<GroupSecretAccessPolicyUpdate> CreateGroupPolicyUpdates(\n        IEnumerable<GroupSecretAccessPolicy> policies, List<Guid> groupIds,\n        AccessPolicyOperation operation) =>\n        policies\n            .Where(ap => groupIds.Contains(ap.GroupId!.Value))\n            .Select(ap => new GroupSecretAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })\n            .ToList();\n\n    private static List<ServiceAccountSecretAccessPolicyUpdate> CreateServiceAccountPolicyUpdates(\n        IEnumerable<ServiceAccountSecretAccessPolicy> policies, List<Guid> serviceAccountIds,\n        AccessPolicyOperation operation) =>\n        policies\n            .Where(ap => serviceAccountIds.Contains(ap.ServiceAccountId!.Value))\n            .Select(ap => new ServiceAccountSecretAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })\n            .ToList();\n\n\n    private List<UserSecretAccessPolicyUpdate> GetUserPolicyUpdates(List<UserSecretAccessPolicy> requestedPolicies) =>\n        GetPolicyUpdates(UserAccessPolicies.ToList(), requestedPolicies, GetOrganizationUserIds, GetUserIdsToBeUpdated,\n            CreateUserPolicyUpdates);\n\n    private List<GroupSecretAccessPolicyUpdate>\n        GetGroupPolicyUpdates(List<GroupSecretAccessPolicy> requestedPolicies) =>\n        GetPolicyUpdates(GroupAccessPolicies.ToList(), requestedPolicies, GetGroupIds, GetGroupIdsToBeUpdated,\n            CreateGroupPolicyUpdates);\n\n    private List<ServiceAccountSecretAccessPolicyUpdate> GetServiceAccountPolicyUpdates(\n        List<ServiceAccountSecretAccessPolicy> requestedPolicies) =>\n        GetPolicyUpdates(ServiceAccountAccessPolicies.ToList(), requestedPolicies, GetServiceAccountIds,\n            GetServiceAccountIdsToBeUpdated, CreateServiceAccountPolicyUpdates);\n\n    private List<Guid> GetUserIdsToBeUpdated(IEnumerable<UserSecretAccessPolicy> requested) =>\n        UserAccessPolicies\n            .Where(currentAp => requested.Any(requestedAp =>\n                requestedAp.GrantedSecretId == currentAp.GrantedSecretId &&\n                requestedAp.OrganizationUserId == currentAp.OrganizationUserId &&\n                (requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))\n            .Select(ap => ap.OrganizationUserId!.Value)\n            .ToList();\n\n    private List<Guid> GetGroupIdsToBeUpdated(IEnumerable<GroupSecretAccessPolicy> requested) =>\n        GroupAccessPolicies\n            .Where(currentAp => requested.Any(requestedAp =>\n                requestedAp.GrantedSecretId == currentAp.GrantedSecretId &&\n                requestedAp.GroupId == currentAp.GroupId &&\n                (requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))\n            .Select(ap => ap.GroupId!.Value)\n            .ToList();\n\n    private List<Guid> GetServiceAccountIdsToBeUpdated(IEnumerable<ServiceAccountSecretAccessPolicy> requested) =>\n        ServiceAccountAccessPolicies\n            .Where(currentAp => requested.Any(requestedAp =>\n                requestedAp.GrantedSecretId == currentAp.GrantedSecretId &&\n                requestedAp.ServiceAccountId == currentAp.ServiceAccountId &&\n                (requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))\n            .Select(ap => ap.ServiceAccountId!.Value)\n            .ToList();\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/SecretPermissionDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class SecretPermissionDetails\n{\n    public Secret Secret { get; set; }\n    public bool Read { get; set; }\n    public bool Write { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/SecretsSyncRequest.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class SecretsSyncRequest\n{\n    public AccessClientType AccessClientType { get; set; }\n    public Guid OrganizationId { get; set; }\n    public Guid ServiceAccountId { get; set; }\n    public DateTime? LastSyncedDate { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ServiceAccountCounts.cs",
    "content": "﻿namespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ServiceAccountCounts\n{\n    public int Projects { get; set; }\n\n    public int People { get; set; }\n\n    public int AccessTokens { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPolicies.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ServiceAccountGrantedPolicies\n{\n    public ServiceAccountGrantedPolicies(Guid serviceAccountId, IEnumerable<BaseAccessPolicy> policies)\n    {\n        ServiceAccountId = serviceAccountId;\n        ProjectGrantedPolicies = policies.Where(x => x is ServiceAccountProjectAccessPolicy)\n            .Cast<ServiceAccountProjectAccessPolicy>().ToList();\n\n        var serviceAccount = ProjectGrantedPolicies.FirstOrDefault()?.ServiceAccount;\n        if (serviceAccount != null)\n        {\n            OrganizationId = serviceAccount.OrganizationId;\n        }\n    }\n\n    public ServiceAccountGrantedPolicies()\n    {\n    }\n\n    public Guid ServiceAccountId { get; set; }\n    public Guid OrganizationId { get; set; }\n\n    public IEnumerable<ServiceAccountProjectAccessPolicy> ProjectGrantedPolicies { get; set; } =\n        new List<ServiceAccountProjectAccessPolicy>();\n\n    public ServiceAccountGrantedPoliciesUpdates GetPolicyUpdates(ServiceAccountGrantedPolicies requested)\n    {\n        var currentProjectIds = ProjectGrantedPolicies.Select(p => p.GrantedProjectId!.Value).ToList();\n        var requestedProjectIds = requested.ProjectGrantedPolicies.Select(p => p.GrantedProjectId!.Value).ToList();\n\n        var projectIdsToBeDeleted = currentProjectIds.Except(requestedProjectIds).ToList();\n        var projectIdsToBeCreated = requestedProjectIds.Except(currentProjectIds).ToList();\n        var projectIdsToBeUpdated = GetProjectIdsToBeUpdated(requested);\n\n        var policiesToBeDeleted =\n            CreatePolicyUpdates(ProjectGrantedPolicies, projectIdsToBeDeleted, AccessPolicyOperation.Delete);\n        var policiesToBeCreated = CreatePolicyUpdates(requested.ProjectGrantedPolicies, projectIdsToBeCreated,\n            AccessPolicyOperation.Create);\n        var policiesToBeUpdated = CreatePolicyUpdates(requested.ProjectGrantedPolicies, projectIdsToBeUpdated,\n            AccessPolicyOperation.Update);\n\n        return new ServiceAccountGrantedPoliciesUpdates\n        {\n            OrganizationId = OrganizationId,\n            ServiceAccountId = ServiceAccountId,\n            ProjectGrantedPolicyUpdates =\n                policiesToBeDeleted.Concat(policiesToBeCreated).Concat(policiesToBeUpdated)\n        };\n    }\n\n    private static List<ServiceAccountProjectAccessPolicyUpdate> CreatePolicyUpdates(\n        IEnumerable<ServiceAccountProjectAccessPolicy> policies, List<Guid> projectIds,\n        AccessPolicyOperation operation) =>\n        policies\n            .Where(ap => projectIds.Contains(ap.GrantedProjectId!.Value))\n            .Select(ap => new ServiceAccountProjectAccessPolicyUpdate { Operation = operation, AccessPolicy = ap })\n            .ToList();\n\n    private List<Guid> GetProjectIdsToBeUpdated(ServiceAccountGrantedPolicies requested) =>\n        ProjectGrantedPolicies\n            .Where(currentAp => requested.ProjectGrantedPolicies.Any(requestedAp =>\n                requestedAp.GrantedProjectId == currentAp.GrantedProjectId &&\n                requestedAp.ServiceAccountId == currentAp.ServiceAccountId &&\n                (requestedAp.Write != currentAp.Write || requestedAp.Read != currentAp.Read)))\n            .Select(ap => ap.GrantedProjectId!.Value)\n            .ToList();\n}\n\npublic class ServiceAccountGrantedPoliciesUpdates\n{\n    public Guid ServiceAccountId { get; set; }\n    public Guid OrganizationId { get; set; }\n\n    public IEnumerable<ServiceAccountProjectAccessPolicyUpdate> ProjectGrantedPolicyUpdates { get; set; } =\n        new List<ServiceAccountProjectAccessPolicyUpdate>();\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ServiceAccountGrantedPoliciesPermissionDetails.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ServiceAccountGrantedPoliciesPermissionDetails\n{\n    public Guid ServiceAccountId { get; set; }\n    public Guid OrganizationId { get; set; }\n    public required IEnumerable<ServiceAccountProjectAccessPolicyPermissionDetails> ProjectGrantedPolicies { get; set; }\n}\n\npublic class ServiceAccountProjectAccessPolicyPermissionDetails\n{\n    public required ServiceAccountProjectAccessPolicy AccessPolicy { get; set; }\n    public bool HasPermission { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ServiceAccountPeopleAccessPolicies.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ServiceAccountPeopleAccessPolicies\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public IEnumerable<UserServiceAccountAccessPolicy> UserAccessPolicies { get; set; }\n    public IEnumerable<GroupServiceAccountAccessPolicy> GroupAccessPolicies { get; set; }\n\n    public IEnumerable<BaseAccessPolicy> ToBaseAccessPolicies()\n    {\n        var policies = new List<BaseAccessPolicy>();\n        if (UserAccessPolicies != null && UserAccessPolicies.Any())\n        {\n            policies.AddRange(UserAccessPolicies);\n        }\n\n        if (GroupAccessPolicies != null && GroupAccessPolicies.Any())\n        {\n            policies.AddRange(GroupAccessPolicies);\n        }\n\n        return policies;\n    }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Data/ServiceAccountSecretsDetails.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Models.Data;\n\npublic class ServiceAccountSecretsDetails\n{\n    public ServiceAccount ServiceAccount { get; set; }\n    public int AccessToSecrets { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Models/Mail/RequestSecretsManagerAccessViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Models.Mail;\n\nnamespace Bit.Core.SecretsManager.Models.Mail;\n\npublic class RequestSecretsManagerAccessViewModel : BaseMailModel\n{\n    public string UserNameRequestingAccess { get; set; }\n    public string OrgName { get; set; }\n    public string EmailContent { get; set; }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/IProjectServiceAccountsAccessPoliciesUpdatesQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\n\npublic interface IProjectServiceAccountsAccessPoliciesUpdatesQuery\n{\n    Task<ProjectServiceAccountsAccessPoliciesUpdates> GetAsync(ProjectServiceAccountsAccessPolicies grantedPolicies);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/ISameOrganizationQuery.cs",
    "content": "﻿namespace Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\n\npublic interface ISameOrganizationQuery\n{\n    Task<bool> OrgUsersInTheSameOrgAsync(List<Guid> organizationUserIds, Guid organizationId);\n    Task<bool> GroupsInTheSameOrgAsync(List<Guid> groupIds, Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/ISecretAccessPoliciesUpdatesQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\n\npublic interface ISecretAccessPoliciesUpdatesQuery\n{\n    Task<SecretAccessPoliciesUpdates> GetAsync(SecretAccessPolicies accessPolicies, Guid userId);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Queries/AccessPolicies/Interfaces/IServiceAccountGrantedPolicyUpdatesQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\n\npublic interface IServiceAccountGrantedPolicyUpdatesQuery\n{\n    Task<ServiceAccountGrantedPoliciesUpdates> GetAsync(ServiceAccountGrantedPolicies grantedPolicies);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Queries/Interfaces/IAccessClientQuery.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.SecretsManager.Queries.Interfaces;\n\npublic interface IAccessClientQuery\n{\n    Task<(AccessClientType AccessClientType, Guid UserId)> GetAccessClientAsync(ClaimsPrincipal claimsPrincipal, Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Queries/Projects/Interfaces/IMaxProjectsQuery.cs",
    "content": "﻿namespace Bit.Core.SecretsManager.Queries.Projects.Interfaces;\n\npublic interface IMaxProjectsQuery\n{\n    Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Queries/Secrets/Interfaces/ISecretsSyncQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Queries.Secrets.Interfaces;\n\npublic interface ISecretsSyncQuery\n{\n    Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetAsync(SecretsSyncRequest syncRequest);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Queries/ServiceAccounts/Interfaces/ICountNewServiceAccountSlotsRequiredQuery.cs",
    "content": "﻿namespace Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;\n\npublic interface ICountNewServiceAccountSlotsRequiredQuery\n{\n    Task<int> CountNewServiceAccountSlotsRequiredAsync(Guid organizationId, int serviceAccountsToAdd);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Queries/ServiceAccounts/Interfaces/IServiceAccountSecretsDetailsQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;\n\npublic interface IServiceAccountSecretsDetailsQuery\n{\n    public Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdAsync(\n        Guid organizationId, Guid userId, AccessClientType accessClient, bool includeAccessToSecrets);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Repositories;\n\npublic interface IAccessPolicyRepository\n{\n    Task<List<BaseAccessPolicy>> CreateManyAsync(List<BaseAccessPolicy> baseAccessPolicies);\n    Task<IEnumerable<BaseAccessPolicy>> GetPeoplePoliciesByGrantedProjectIdAsync(Guid id, Guid userId);\n    Task<IEnumerable<BaseAccessPolicy>> ReplaceProjectPeopleAsync(ProjectPeopleAccessPolicies peopleAccessPolicies, Guid userId);\n    Task<PeopleGrantees> GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId);\n    Task<IEnumerable<BaseAccessPolicy>> GetPeoplePoliciesByGrantedServiceAccountIdAsync(Guid id, Guid userId);\n    Task<IEnumerable<BaseAccessPolicy>> ReplaceServiceAccountPeopleAsync(ServiceAccountPeopleAccessPolicies peopleAccessPolicies, Guid userId);\n    Task<ServiceAccountGrantedPolicies?> GetServiceAccountGrantedPoliciesAsync(Guid serviceAccountId);\n    Task<ServiceAccountGrantedPoliciesPermissionDetails?> GetServiceAccountGrantedPoliciesPermissionDetailsAsync(\n        Guid serviceAccountId, Guid userId, AccessClientType accessClientType);\n    Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates policyUpdates);\n    Task<ProjectServiceAccountsAccessPolicies?> GetProjectServiceAccountsAccessPoliciesAsync(Guid projectId);\n    Task UpdateProjectServiceAccountsAccessPoliciesAsync(ProjectServiceAccountsAccessPoliciesUpdates updates);\n    Task<SecretAccessPolicies?> GetSecretAccessPoliciesAsync(Guid secretId, Guid userId);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/IApiKeyRepository.cs",
    "content": "﻿using Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Repositories;\n\npublic interface IApiKeyRepository : IRepository<ApiKey, Guid>\n{\n    Task<ApiKeyDetails> GetDetailsByIdAsync(Guid id);\n    Task<ICollection<ApiKey>> GetManyByServiceAccountIdAsync(Guid id);\n    Task DeleteManyAsync(IEnumerable<ApiKey> objs);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/IProjectRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Repositories;\n\npublic interface IProjectRepository\n{\n    Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n    Task<IEnumerable<Project>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n    Task<IEnumerable<Project>> GetManyWithSecretsByIds(IEnumerable<Guid> ids);\n    Task<Project> GetByIdAsync(Guid id);\n    Task<Project> CreateAsync(Project project);\n    Task ReplaceAsync(Project project);\n    Task DeleteManyByIdAsync(IEnumerable<Guid> ids);\n    Task<IEnumerable<Project>> ImportAsync(IEnumerable<Project> projects);\n    Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType);\n    Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId);\n    Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId);\n    Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n    Task<ProjectCounts> GetProjectCountsByIdAsync(Guid projectId, Guid userId, AccessClientType accessType);\n    Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(IEnumerable<Guid> projectIds, Guid userId,\n        AccessClientType accessType);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/ISecretRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Repositories;\n\npublic interface ISecretRepository\n{\n    Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n    Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId);\n    Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType);\n    Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n    Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids);\n    Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids);\n    Task<IEnumerable<Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids);\n    Task<Secret> GetByIdAsync(Guid id);\n    Task<Secret> CreateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null);\n    Task<Secret> UpdateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null);\n    Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids);\n    Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids);\n    Task RestoreManyByIdAsync(IEnumerable<Guid> ids);\n    Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets);\n    Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType);\n    Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToSecretsAsync(IEnumerable<Guid> ids, Guid userId, AccessClientType accessType);\n    Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays);\n    Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId);\n    Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/ISecretVersionRepository.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Repositories;\n\npublic interface ISecretVersionRepository\n{\n    Task<SecretVersion?> GetByIdAsync(Guid id);\n    Task<IEnumerable<SecretVersion>> GetManyBySecretIdAsync(Guid secretId);\n    Task<IEnumerable<SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids);\n    Task<SecretVersion> CreateAsync(SecretVersion secretVersion);\n    Task DeleteManyByIdAsync(IEnumerable<Guid> ids);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Repositories;\n\npublic interface IServiceAccountRepository\n{\n    Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n    Task<ServiceAccount> GetByIdAsync(Guid id);\n    Task<IEnumerable<ServiceAccount>> GetManyByIds(IEnumerable<Guid> ids);\n    Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount);\n    Task ReplaceAsync(ServiceAccount serviceAccount);\n    Task DeleteManyByIdAsync(IEnumerable<Guid> ids);\n    Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n    Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType);\n    Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(IEnumerable<Guid> ids, Guid userId,\n        AccessClientType accessType);\n    Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId);\n    Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n    Task<ServiceAccountCounts> GetServiceAccountCountsByIdAsync(Guid serviceAccountId, Guid userId, AccessClientType accessType);\n\n    Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType);\n    Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Repositories.Noop;\n\npublic class NoopProjectRepository : IProjectRepository\n{\n    public Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        return Task.FromResult(null as IEnumerable<ProjectPermissionDetails>);\n    }\n\n    public Task<IEnumerable<Project>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        return Task.FromResult(null as IEnumerable<Project>);\n    }\n\n    public Task<IEnumerable<Project>> GetManyWithSecretsByIds(IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(null as IEnumerable<Project>);\n    }\n\n    public Task<Project> GetByIdAsync(Guid id)\n    {\n        return Task.FromResult(null as Project);\n    }\n\n    public Task<Project> CreateAsync(Project project)\n    {\n        return Task.FromResult(null as Project);\n    }\n\n    public Task ReplaceAsync(Project project)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<IEnumerable<Project>> ImportAsync(IEnumerable<Project> projects)\n    {\n        return Task.FromResult(null as IEnumerable<Project>);\n    }\n\n    public Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType)\n    {\n        return Task.FromResult((false, false));\n    }\n\n    public Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId)\n    {\n        return Task.FromResult(false);\n    }\n\n    public Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<ProjectCounts> GetProjectCountsByIdAsync(Guid projectId, Guid userId, AccessClientType accessType)\n    {\n        return Task.FromResult(null as ProjectCounts);\n    }\n\n    public Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(IEnumerable<Guid> projectIds,\n        Guid userId, AccessClientType accessType)\n    {\n        return Task.FromResult(null as Dictionary<Guid, (bool Read, bool Write)>);\n    }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\n\nnamespace Bit.Core.SecretsManager.Repositories.Noop;\n\npublic class NoopSecretRepository : ISecretRepository\n{\n    public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);\n    }\n\n    public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId)\n    {\n        return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);\n    }\n\n    public Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        return Task.FromResult(null as IEnumerable<Secret>);\n    }\n\n    public Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId,\n        IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(null as IEnumerable<Secret>);\n    }\n\n    public Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(null as IEnumerable<Secret>);\n    }\n\n    public Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId,\n        AccessClientType accessType)\n    {\n        return Task.FromResult(null as IEnumerable<SecretPermissionDetails>);\n    }\n\n    public Task<Secret> GetByIdAsync(Guid id)\n    {\n        return Task.FromResult(null as Secret);\n    }\n\n    public Task<Secret> CreateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates)\n    {\n        return Task.FromResult(null as Secret);\n    }\n\n    public Task<Secret> UpdateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates)\n    {\n        return Task.FromResult(null as Secret);\n    }\n\n    public Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task RestoreManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<IEnumerable<Secret>> ImportAsync(IEnumerable<Secret> secrets)\n    {\n        return Task.FromResult(null as IEnumerable<Secret>);\n    }\n\n    public Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)\n    {\n        return Task.FromResult((false, false));\n    }\n\n    public Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToSecretsAsync(IEnumerable<Guid> ids,\n        Guid userId, AccessClientType accessType)\n    {\n        return Task.FromResult(null as Dictionary<Guid, (bool Read, bool Write)>);\n    }\n\n    public Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<IEnumerable<Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids) => Task.FromResult<IEnumerable<Secret>>([]);\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/Noop/NoopSecretVersionRepository.cs",
    "content": "﻿using Bit.Core.SecretsManager.Entities;\n\nnamespace Bit.Core.SecretsManager.Repositories.Noop;\n\npublic class NoopSecretVersionRepository : ISecretVersionRepository\n{\n    public Task<SecretVersion?> GetByIdAsync(Guid id)\n    {\n        return Task.FromResult(null as SecretVersion);\n    }\n\n    public Task<IEnumerable<SecretVersion>> GetManyBySecretIdAsync(Guid secretId)\n    {\n        return Task.FromResult(Enumerable.Empty<SecretVersion>());\n    }\n\n    public Task<SecretVersion> CreateAsync(SecretVersion secretVersion)\n    {\n        return Task.FromResult(secretVersion);\n    }\n\n    public Task DeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        return Task.CompletedTask;\n    }\n\n    public Task<IEnumerable<SecretVersion>> GetManyByIdsAsync(IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(Enumerable.Empty<SecretVersion>());\n    }\n}\n"
  },
  {
    "path": "src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Core.SecretsManager.Repositories.Noop;\n\npublic class NoopServiceAccountRepository : IServiceAccountRepository\n{\n    public Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)\n    {\n        return Task.FromResult(null as IEnumerable<ServiceAccount>);\n    }\n\n    public Task<ServiceAccount> GetByIdAsync(Guid id)\n    {\n        return Task.FromResult(null as ServiceAccount);\n    }\n\n    public Task<IEnumerable<ServiceAccount>> GetManyByIds(IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(null as IEnumerable<ServiceAccount>);\n    }\n\n    public Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount)\n    {\n        return Task.FromResult(null as ServiceAccount);\n    }\n\n    public Task ReplaceAsync(ServiceAccount serviceAccount)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteManyByIdAsync(IEnumerable<Guid> ids)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<bool> UserHasReadAccessToServiceAccount(Guid id, Guid userId)\n    {\n        return Task.FromResult(false);\n    }\n\n    public Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId)\n    {\n        return Task.FromResult(false);\n    }\n\n    public Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType) => throw new NotImplementedException();\n\n    public Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType)\n    {\n        return Task.FromResult((false, false));\n    }\n\n    public Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(IEnumerable<Guid> ids,\n        Guid userId, AccessClientType accessType)\n    {\n        return Task.FromResult(null as Dictionary<Guid, (bool Read, bool Write)>);\n    }\n\n    public Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId, Guid userId,\n        AccessClientType accessType)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<ServiceAccountCounts> GetServiceAccountCountsByIdAsync(Guid serviceAccountId, Guid userId,\n        AccessClientType accessType)\n    {\n        return Task.FromResult(null as ServiceAccountCounts);\n    }\n\n    public Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(\n        Guid organizationId, Guid userId, AccessClientType accessType)\n    {\n        return Task.FromResult(null as IEnumerable<ServiceAccountSecretsDetails>);\n    }\n\n    public Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId)\n    {\n        return Task.FromResult(false);\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/IApplicationCacheService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Models.Data.Organizations;\n\nnamespace Bit.Core.Services;\n\npublic interface IApplicationCacheService\n{\n    [Obsolete(\"We are transitioning to a new cache pattern. Please consult the Admin Console team before using.\", false)]\n    Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync();\n#nullable enable\n    Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId);\n    /// <summary>\n    /// Gets the cached <see cref=\"ProviderAbility\"/> for the specified provider.\n    /// </summary>\n    /// <param name=\"providerId\">The ID of the provider.</param>\n    /// <returns>The <see cref=\"ProviderAbility\"/> if found; otherwise, <c>null</c>.</returns>\n    Task<ProviderAbility?> GetProviderAbilityAsync(Guid providerId);\n#nullable disable\n    [Obsolete(\"We are transitioning to a new cache pattern. Please consult the Admin Console team before using.\", false)]\n    Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync();\n    /// <summary>\n    /// Gets cached <see cref=\"ProviderAbility\"/> entries for the specified providers.\n    /// Provider IDs not found in the cache are silently excluded from the result.\n    /// </summary>\n    /// <param name=\"providerIds\">The IDs of the providers to look up.</param>\n    /// <returns>A dictionary mapping each found provider ID to its <see cref=\"ProviderAbility\"/>.</returns>\n    Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync(IEnumerable<Guid> providerIds);\n    /// <summary>\n    /// Gets cached <see cref=\"OrganizationAbility\"/> entries for the specified organizations.\n    /// Organization IDs not found in the cache are silently excluded from the result.\n    /// </summary>\n    /// <param name=\"orgIds\">The IDs of the organizations to look up.</param>\n    /// <returns>A dictionary mapping each found organization ID to its <see cref=\"OrganizationAbility\"/>.</returns>\n    Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync(IEnumerable<Guid> orgIds);\n    Task UpsertOrganizationAbilityAsync(Organization organization);\n    Task UpsertProviderAbilityAsync(Provider provider);\n    Task DeleteOrganizationAbilityAsync(Guid organizationId);\n    Task DeleteProviderAbilityAsync(Guid providerId);\n}\n"
  },
  {
    "path": "src/Core/Services/IAttachmentStorageService.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\n\n\nnamespace Bit.Core.Services;\n\npublic interface IAttachmentStorageService\n{\n    FileUploadType FileUploadType { get; }\n    Task UploadNewAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentData);\n    Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData);\n    Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData);\n    Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData, string originalContainer);\n    Task CleanupAsync(Guid cipherId);\n    Task DeleteAttachmentAsync(Guid cipherId, CipherAttachment.MetaData attachmentData);\n    Task DeleteAttachmentsForCipherAsync(Guid cipherId);\n    Task DeleteAttachmentsForOrganizationAsync(Guid organizationId);\n    Task DeleteAttachmentsForUserAsync(Guid userId);\n    Task<string> GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData);\n    Task<string> GetAttachmentDownloadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData);\n    /// <summary>\n    /// Parses and validates a time-limited download token, returning the cipher ID and attachment ID.\n    /// Only supported by storage implementations that use signed URLs (e.g. local/self-hosted storage).\n    /// </summary>\n    (Guid cipherId, string attachmentId) ParseAttachmentDownloadToken(string token);\n    /// <summary>\n    /// Opens a read stream for a locally stored attachment file.\n    /// Returns null if the storage implementation does not support direct streaming (e.g. cloud storage).\n    /// </summary>\n    Task<Stream?> GetAttachmentReadStreamAsync(Cipher cipher, CipherAttachment.MetaData attachmentData);\n    Task<(bool, long?)> ValidateFileAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, long leeway);\n}\n"
  },
  {
    "path": "src/Core/Services/IBraintreeService.cs",
    "content": "﻿using Bit.Core.Billing.Subscriptions.Models;\nusing Braintree;\n\nnamespace Bit.Core.Services;\n\npublic interface IBraintreeService\n{\n    Task<Customer?> GetCustomer(\n        Stripe.Customer customer);\n\n    Task PayInvoice(\n        SubscriberId subscriberId,\n        Stripe.Invoice invoice);\n}\n"
  },
  {
    "path": "src/Core/Services/IDeviceService.cs",
    "content": "﻿using Bit.Core.Auth.Models.Api.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Platform.PushRegistration;\n\nnamespace Bit.Core.Services;\n\npublic interface IDeviceService\n{\n    Task SaveAsync(WebPushRegistrationData webPush, Device device, IEnumerable<string> organizationIds);\n    Task SaveAsync(Device device);\n    Task ClearTokenAsync(Device device);\n    Task DeactivateAsync(Device device);\n    Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,\n        Guid currentUserId,\n        DeviceKeysUpdateRequestModel currentDeviceUpdate,\n        IEnumerable<OtherDeviceKeysUpdateRequestModel> alteredDevices);\n}\n"
  },
  {
    "path": "src/Core/Services/IDnsResolverService.cs",
    "content": "﻿namespace Bit.Core.Services;\n\npublic interface IDnsResolverService\n{\n    Task<bool> ResolveAsync(string domain, string txtRecord, CancellationToken cancellationToken = default);\n}\n"
  },
  {
    "path": "src/Core/Services/IFeatureService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Services;\n\npublic interface IFeatureService\n{\n    /// <summary>\n    /// Checks whether online access to feature status is available.\n    /// </summary>\n    /// <returns>True if the service is online, otherwise false.</returns>\n    bool IsOnline();\n\n    /// <summary>\n    /// Checks whether a given feature is enabled.\n    /// </summary>\n    /// <param name=\"key\">The key of the feature to check.</param>\n    /// <param name=\"defaultValue\">The default value for the feature.</param>\n    /// <returns>True if the feature is enabled, otherwise false.</returns>\n    bool IsEnabled(string key, bool defaultValue = false);\n\n    /// <summary>\n    /// Gets the integer variation of a feature.\n    /// </summary>\n    /// <param name=\"key\">The key of the feature to check.</param>\n    /// <param name=\"defaultValue\">The default value for the feature.</param>\n    /// <returns>The feature variation value.</returns>\n    int GetIntVariation(string key, int defaultValue = 0);\n\n    /// <summary>\n    /// Gets the string variation of a feature.\n    /// </summary>\n    /// <param name=\"key\">The key of the feature to check.</param>\n    /// <param name=\"defaultValue\">The default value for the feature.</param>\n    /// <returns>The feature variation value.</returns>\n    string GetStringVariation(string key, string defaultValue = null);\n\n    /// <summary>\n    /// Gets all feature values.\n    /// </summary>\n    /// <returns>A dictionary of feature keys and their values.</returns>\n    Dictionary<string, object> GetAll();\n}\n"
  },
  {
    "path": "src/Core/Services/II18nService.cs",
    "content": "﻿#nullable enable\n\nusing Microsoft.Extensions.Localization;\n\nnamespace Bit.Core.Services;\n\npublic interface II18nService\n{\n    LocalizedString GetLocalizedHtmlString(string key);\n    LocalizedString GetLocalizedHtmlString(string key, params object?[] args);\n    string Translate(string key, params object?[] args);\n    string T(string key, params object?[] args);\n}\n"
  },
  {
    "path": "src/Core/Services/IUserService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Services;\n\npublic interface IUserService\n{\n    Guid? GetProperUserId(ClaimsPrincipal principal);\n    Task<User> GetUserByIdAsync(string userId);\n    Task<User> GetUserByIdAsync(Guid userId);\n    Task<User> GetUserByPrincipalAsync(ClaimsPrincipal principal);\n    Task<DateTime> GetAccountRevisionDateByIdAsync(Guid userId);\n    Task SaveUserAsync(User user, bool push = false);\n    Task<IdentityResult> CreateUserAsync(User user);\n    Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash);\n    Task SendMasterPasswordHintAsync(string email);\n    Task SendEmailVerificationAsync(User user);\n    Task<IdentityResult> ConfirmEmailAsync(User user, string token);\n    Task InitiateEmailChangeAsync(User user, string newEmail);\n    Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword,\n        string token, string key);\n    Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string passwordHint, string key);\n    // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328\n    [Obsolete(\"Use ISetKeyConnectorKeyCommand instead. This method will be removed in a future version.\")]\n    Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier);\n    Task<IdentityResult> ConvertToKeyConnectorAsync(User user, string keyConnectorKeyWrappedUserKey);\n    Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key);\n    Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint);\n    Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);\n    Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true);\n    Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type);\n    Task<IdentityResult> DeleteAsync(User user);\n    Task<IdentityResult> DeleteAsync(User user, string token);\n    Task SendDeleteConfirmationAsync(string email);\n    Task UpdateLicenseAsync(User user, UserLicense license);\n    Task<string> AdjustStorageAsync(User user, short storageAdjustmentGb);\n    Task CancelPremiumAsync(User user, bool? endOfPeriod = null);\n    Task ReinstatePremiumAsync(User user);\n    Task EnablePremiumAsync(Guid userId, DateTime? expirationDate);\n    Task DisablePremiumAsync(Guid userId, DateTime? expirationDate);\n    Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate);\n    Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,\n        int? version = null);\n    Task<bool> CheckPasswordAsync(User user, string password);\n    /// <summary>\n    /// Checks if the user has access to premium features, either through a personal subscription or through an organization.\n    ///\n    /// This is the preferred way to definitively know if a user has access to premium features when you already have the User object.\n    /// </summary>\n    /// <param name=\"user\">user being acted on</param>\n    /// <returns>true if they can access premium; false otherwise.</returns>\n    Task<bool> CanAccessPremium(User user);\n\n    /// <summary>\n    /// Checks if the user has inherited access to premium features through an organization.\n    ///\n    /// This primarily serves as a means to communicate to the client when a user has inherited their premium status\n    /// through an organization. Feature gating logic probably should not be behind this check.\n    /// </summary>\n    /// <param name=\"user\">user being acted on</param>\n    /// <returns>true if they can access premium because of organization membership; false otherwise.</returns>\n    [Obsolete(\"Use IHasPremiumAccessQuery.HasPremiumFromOrganizationAsync instead. This method will be removed in a future version.\")]\n    Task<bool> HasPremiumFromOrganization(User user);\n    Task<string> GenerateSignInTokenAsync(User user, string purpose);\n\n    Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,\n        bool validatePassword = true, bool refreshStamp = true);\n    Task RotateApiKeyAsync(User user);\n    string GetUserName(ClaimsPrincipal principal);\n    Task SendOTPAsync(User user);\n    Task<bool> VerifyOTPAsync(User user, string token);\n    Task<bool> VerifySecretAsync(User user, string secret, bool isSettingMFA = false);\n    /// <summary>\n    /// We use this method to check if the user has an active new device verification bypass\n    /// </summary>\n    /// <param name=\"userId\">self</param>\n    /// <returns>returns true if the value is found in the cache</returns>\n    Task<bool> ActiveNewDeviceVerificationException(Guid userId);\n    /// <summary>\n    /// We use this method to toggle the new device verification bypass\n    /// </summary>\n    /// <param name=\"userId\">Id of user bypassing new device verification</param>\n    Task ToggleNewDeviceVerificationException(Guid userId);\n\n    void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true);\n\n    /// <summary>\n    /// This method is used by the TwoFactorAuthenticationValidator to recover two\n    /// factor for a user. This allows users to be logged in after a successful recovery\n    /// attempt.\n    ///\n    /// This method logs the event, sends an email to the user, and removes two factor\n    /// providers on the user account. This means that a user will have to accomplish\n    /// new device verification on their account on new logins, if it is enabled for their user.\n    /// </summary>\n    /// <param name=\"recoveryCode\">recovery code associated with the user logging in</param>\n    /// <param name=\"user\">The user to refresh the 2FA and Recovery Code on.</param>\n    /// <returns>true if the recovery code is valid; false otherwise</returns>\n    Task<bool> RecoverTwoFactorAsync(User user, string recoveryCode);\n\n    /// <summary>\n    /// Returns true if the user is a legacy user. Legacy users use their master key as their\n    /// encryption key. We force these users to the web to migrate their encryption scheme.\n    /// </summary>\n    Task<bool> IsLegacyUser(string userId);\n\n    /// <summary>\n    /// Indicates if the user is managed by any organization.\n    /// </summary>\n    /// <remarks>\n    /// A user is considered managed by an organization if their email domain matches one of the\n    /// verified domains of that organization, and the user is a member of it.\n    /// The organization must be enabled and able to have verified domains.\n    /// </remarks>\n    Task<bool> IsClaimedByAnyOrganizationAsync(Guid userId);\n\n    /// <summary>\n    /// Verify whether the new email domain meets the requirements for managed users.\n    /// </summary>\n    /// <returns>\n    /// IdentityResult\n    /// </returns>\n    Task<IdentityResult> ValidateClaimedUserDomainAsync(User user, string newEmail);\n\n    /// <summary>\n    /// Gets the organizations that manage the user.\n    /// </summary>\n    /// <inheritdoc cref=\"IsClaimedByAnyOrganizationAsync\"/>\n    Task<IEnumerable<Organization>> GetOrganizationsClaimingUserAsync(Guid userId);\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/AzureQueueService.cs",
    "content": "﻿#nullable enable\n\nusing System.Text;\nusing System.Text.Json;\nusing Azure.Storage.Queues;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Services;\n\npublic abstract class AzureQueueService<T>\n{\n    protected QueueClient _queueClient;\n    protected JsonSerializerOptions _jsonOptions;\n\n    protected AzureQueueService(QueueClient queueClient, JsonSerializerOptions jsonOptions)\n    {\n        _queueClient = queueClient;\n        _jsonOptions = jsonOptions;\n    }\n\n    public async Task CreateManyAsync(IEnumerable<T> messages)\n    {\n        if (messages?.Any() != true)\n        {\n            return;\n        }\n\n        foreach (var json in SerializeMany(messages, _jsonOptions))\n        {\n            await _queueClient.SendMessageAsync(json);\n        }\n    }\n\n    protected IEnumerable<string> SerializeMany(IEnumerable<T> messages, JsonSerializerOptions jsonOptions)\n    {\n        // Calculate Base-64 encoded text with padding\n        int getBase64Size(int byteCount) => ((4 * byteCount / 3) + 3) & ~3;\n\n        var messagesList = new List<string>();\n        var messagesListSize = 0;\n\n        int calculateByteSize(int totalSize, int toAdd) =>\n            // Calculate the total length this would be w/ \"[]\" and commas\n            getBase64Size(totalSize + toAdd + messagesList.Count + 2);\n\n        // Format the final array string, i.e. [{...},{...}]\n        string getArrayString()\n        {\n            if (messagesList.Count == 1)\n            {\n                return CoreHelpers.Base64EncodeString(messagesList[0]);\n            }\n            return CoreHelpers.Base64EncodeString(\n                string.Concat(\"[\", string.Join(',', messagesList), \"]\"));\n        }\n\n        var serializedMessages = messages.Select(message =>\n            JsonSerializer.Serialize(message, jsonOptions));\n\n        foreach (var message in serializedMessages)\n        {\n            var messageSize = Encoding.UTF8.GetByteCount(message);\n            if (calculateByteSize(messagesListSize, messageSize) > _queueClient.MessageMaxBytes)\n            {\n                yield return getArrayString();\n                messagesListSize = 0;\n                messagesList.Clear();\n            }\n\n            messagesList.Add(message);\n            messagesListSize += messageSize;\n        }\n\n        if (messagesList.Any())\n        {\n            yield return getArrayString();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/BaseIdentityClientService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing System.Net.Http.Headers;\nusing System.Net.Http.Json;\nusing System.Text.Json;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Services;\n\npublic abstract class BaseIdentityClientService : IDisposable\n{\n    private readonly IHttpClientFactory _httpFactory;\n    private readonly string _identityScope;\n    private readonly string _identityClientId;\n    private readonly string _identityClientSecret;\n    protected readonly ILogger<BaseIdentityClientService> _logger;\n\n    private JsonDocument _decodedToken;\n    private DateTime? _nextAuthAttempt = null;\n\n    public BaseIdentityClientService(\n        IHttpClientFactory httpFactory,\n        string baseClientServerUri,\n        string baseIdentityServerUri,\n        string identityScope,\n        string identityClientId,\n        string identityClientSecret,\n        ILogger<BaseIdentityClientService> logger)\n    {\n        _httpFactory = httpFactory;\n        _identityScope = identityScope;\n        _identityClientId = identityClientId;\n        _identityClientSecret = identityClientSecret;\n        _logger = logger;\n\n        Client = _httpFactory.CreateClient(\"client\");\n        Client.BaseAddress = new Uri(baseClientServerUri);\n        Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(\"application/json\"));\n\n        IdentityClient = _httpFactory.CreateClient(\"identity\");\n        IdentityClient.BaseAddress = new Uri(baseIdentityServerUri);\n        IdentityClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(\"application/json\"));\n    }\n\n    protected HttpClient Client { get; private set; }\n    protected HttpClient IdentityClient { get; private set; }\n    protected string AccessToken { get; private set; }\n\n    protected Task SendAsync(HttpMethod method, string path) =>\n        SendAsync<object>(method, path, null);\n\n    protected Task SendAsync<TRequest>(HttpMethod method, string path, TRequest requestModel) =>\n        SendAsync<TRequest, object>(method, path, requestModel, false);\n\n    protected async Task<TResult> SendAsync<TRequest, TResult>(HttpMethod method, string path,\n        TRequest requestModel, bool hasJsonResult)\n    {\n        var fullRequestPath = string.Concat(Client.BaseAddress, path);\n\n        var tokenStateResponse = await HandleTokenStateAsync();\n        if (!tokenStateResponse)\n        {\n            _logger.LogError(\"Unable to send {method} request to {requestUri} because an access token was unable to be obtained\",\n                method.Method, fullRequestPath);\n            return default;\n        }\n\n        var message = new TokenHttpRequestMessage(requestModel, AccessToken)\n        {\n            Method = method,\n            RequestUri = new Uri(fullRequestPath)\n        };\n        try\n        {\n            var response = await Client.SendAsync(message);\n            if (response.IsSuccessStatusCode)\n            {\n                if (hasJsonResult)\n                {\n                    return await response.Content.ReadFromJsonAsync<TResult>();\n                }\n            }\n            else\n            {\n                _logger.LogError(\"Request to {url} is unsuccessful with status of {code}-{reason}\",\n                    message.RequestUri.ToString(), response.StatusCode, response.ReasonPhrase);\n            }\n            return default;\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(12334, e, \"Failed to send to {0}.\", message.RequestUri.ToString());\n            return default;\n        }\n    }\n\n    protected async Task<bool> HandleTokenStateAsync()\n    {\n        if (_nextAuthAttempt.HasValue && DateTime.UtcNow < _nextAuthAttempt.Value)\n        {\n            _logger.LogInformation(\"Not requesting a token at {now} because the next request time is {nextAttempt}\", DateTime.UtcNow, _nextAuthAttempt.Value);\n            return false;\n        }\n        _nextAuthAttempt = null;\n\n        if (!string.IsNullOrWhiteSpace(AccessToken) && !TokenNeedsRefresh())\n        {\n            return true;\n        }\n\n        var requestMessage = new HttpRequestMessage\n        {\n            Method = HttpMethod.Post,\n            RequestUri = new Uri(string.Concat(IdentityClient.BaseAddress, \"connect/token\")),\n            Content = new FormUrlEncodedContent(new Dictionary<string, string>\n            {\n                { \"grant_type\", \"client_credentials\" },\n                { \"scope\", _identityScope },\n                { \"client_id\", _identityClientId },\n                { \"client_secret\", _identityClientSecret }\n            })\n        };\n\n        HttpResponseMessage response = null;\n        try\n        {\n            response = await IdentityClient.SendAsync(requestMessage);\n        }\n        catch (Exception e)\n        {\n            _logger.LogError(12339, e, \"Unable to authenticate with identity server.\");\n        }\n\n        if (response == null)\n        {\n            _logger.LogError(\"Empty token response from {identity} for client {clientId}\", IdentityClient.BaseAddress, _identityClientId);\n            return false;\n        }\n\n        if (!response.IsSuccessStatusCode)\n        {\n            _logger.LogError(\"Unsuccessful token response from {identity} for client {clientId} with status {code}-{reason}\", IdentityClient.BaseAddress, _identityClientId, response.StatusCode, response.ReasonPhrase);\n\n            if (response.StatusCode == HttpStatusCode.BadRequest)\n            {\n                _nextAuthAttempt = DateTime.UtcNow.AddDays(1);\n            }\n\n            if (_logger.IsEnabled(LogLevel.Debug))\n            {\n                var responseBody = await response.Content.ReadAsStringAsync();\n                _logger.LogDebug(\"Error response body:\\n{ResponseBody}\", responseBody);\n            }\n\n            return false;\n        }\n\n        var content = await response.Content.ReadAsStreamAsync();\n        using var jsonDocument = await JsonDocument.ParseAsync(content);\n\n        AccessToken = jsonDocument.RootElement.GetProperty(\"access_token\").GetString();\n        return true;\n    }\n\n    protected class TokenHttpRequestMessage : HttpRequestMessage\n    {\n        public TokenHttpRequestMessage(string token)\n        {\n            Headers.Add(\"Authorization\", $\"Bearer {token}\");\n        }\n\n        public TokenHttpRequestMessage(object requestObject, string token)\n            : this(token)\n        {\n            if (requestObject != null)\n            {\n                Content = JsonContent.Create(requestObject);\n            }\n        }\n    }\n\n    protected bool TokenNeedsRefresh(int minutes = 5)\n    {\n        var decoded = DecodeToken();\n        if (!decoded.RootElement.TryGetProperty(\"exp\", out var expProp))\n        {\n            throw new InvalidOperationException(\"No exp in token.\");\n        }\n\n        var expiration = CoreHelpers.FromEpocSeconds(expProp.GetInt64());\n        return DateTime.UtcNow.AddMinutes(-1 * minutes) > expiration;\n    }\n\n    protected JsonDocument DecodeToken()\n    {\n        if (_decodedToken != null)\n        {\n            return _decodedToken;\n        }\n\n        if (AccessToken == null)\n        {\n            throw new InvalidOperationException($\"{nameof(AccessToken)} not found.\");\n        }\n\n        var parts = AccessToken.Split('.');\n        if (parts.Length != 3)\n        {\n            throw new InvalidOperationException($\"{nameof(AccessToken)} must have 3 parts\");\n        }\n\n        var decodedBytes = CoreHelpers.Base64UrlDecode(parts[1]);\n        if (decodedBytes == null || decodedBytes.Length < 1)\n        {\n            throw new InvalidOperationException($\"{nameof(AccessToken)} must have 3 parts\");\n        }\n\n        _decodedToken = JsonDocument.Parse(decodedBytes);\n        return _decodedToken;\n    }\n\n    public void Dispose()\n    {\n        _decodedToken?.Dispose();\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/BraintreeService.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Models;\nusing Bit.Core.Settings;\nusing Braintree;\nusing Braintree.Exceptions;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Services.Implementations;\n\nusing static StripeConstants;\n\npublic class BraintreeService(\n    IBraintreeGateway braintreeGateway,\n    IGlobalSettings globalSettings,\n    ILogger<BraintreeService> logger,\n    IMailService mailService,\n    IStripeAdapter stripeAdapter) : IBraintreeService\n{\n    private readonly Exceptions.ConflictException _problemPayingInvoice = new(\"There was a problem paying for your invoice. Please contact customer support.\");\n\n    public async Task<Customer?> GetCustomer(\n        Stripe.Customer customer)\n    {\n        if (!customer.Metadata.TryGetValue(MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId))\n        {\n            return null;\n        }\n\n        try\n        {\n            return await braintreeGateway.Customer.FindAsync(braintreeCustomerId);\n        }\n        catch (NotFoundException)\n        {\n            logger.LogWarning(\n                \"Stripe customer ({CustomerId}) is linked to a Braintree Customer ({BraintreeCustomerId}) that does not exist.\",\n                customer.Id,\n                braintreeCustomerId);\n\n            return null;\n        }\n    }\n\n    public async Task PayInvoice(\n        SubscriberId subscriberId,\n        Stripe.Invoice invoice)\n    {\n        if (invoice.Customer == null)\n        {\n            logger.LogError(\"Invoice's ({InvoiceID}) `customer` property must be expanded to be paid with Braintree\",\n                invoice.Id);\n            throw _problemPayingInvoice;\n        }\n\n        if (!invoice.Customer.Metadata.TryGetValue(MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId))\n        {\n            logger.LogError(\n                \"Cannot pay invoice ({InvoiceID}) with Braintree for Customer ({CustomerID}) that does not have a Braintree Customer ID\",\n                invoice.Id, invoice.Customer.Id);\n            throw _problemPayingInvoice;\n        }\n\n        if (invoice is not\n            {\n                AmountDue: > 0,\n                Status: not InvoiceStatus.Paid,\n                CollectionMethod: CollectionMethod.ChargeAutomatically\n            })\n        {\n            logger.LogWarning(\"Attempted to pay invoice ({InvoiceID}) with Braintree that is not eligible for payment\", invoice.Id);\n            return;\n        }\n\n        var amount = Math.Round(invoice.AmountDue / 100M, 2);\n\n        var idKey = subscriberId.Match(\n            _ => \"user_id\",\n            _ => \"organization_id\",\n            _ => \"provider_id\");\n\n        var idValue = subscriberId.Match(\n            userId => userId.Value,\n            organizationId => organizationId.Value,\n            providerId => providerId.Value);\n\n        var request = new TransactionRequest\n        {\n            Amount = amount,\n            CustomerId = braintreeCustomerId,\n            Options = new TransactionOptionsRequest\n            {\n                SubmitForSettlement = true,\n                PayPal = new TransactionOptionsPayPalRequest\n                {\n                    CustomField = $\"{idKey}:{idValue},region:{globalSettings.BaseServiceUri.CloudRegion}\"\n                }\n            },\n            CustomFields = new Dictionary<string, string>\n            {\n                [idKey] = idValue.ToString(),\n                [\"region\"] = globalSettings.BaseServiceUri.CloudRegion\n            }\n        };\n\n        var result = await braintreeGateway.Transaction.SaleAsync(request);\n\n        if (!result.IsSuccess())\n        {\n            if (invoice.AttemptCount < 4)\n            {\n                await mailService.SendPaymentFailedAsync(invoice.Customer.Email, amount, true);\n            }\n\n            return;\n        }\n\n        await stripeAdapter.UpdateInvoiceAsync(invoice.Id, new Stripe.InvoiceUpdateOptions\n        {\n            Metadata = new Dictionary<string, string>\n            {\n                [MetadataKeys.BraintreeTransactionId] = result.Target.Id,\n                [MetadataKeys.PayPalTransactionId] = result.Target.PayPalDetails.AuthorizationId\n            }\n        });\n\n        await stripeAdapter.PayInvoiceAsync(invoice.Id, new Stripe.InvoicePayOptions { PaidOutOfBand = true });\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/DeviceService.cs",
    "content": "﻿using Bit.Core.Auth.Models.Api.Request;\nusing Bit.Core.Auth.Utilities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.PushRegistration;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\n\nnamespace Bit.Core.Services;\n\npublic class DeviceService : IDeviceService\n{\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly IPushRegistrationService _pushRegistrationService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IGlobalSettings _globalSettings;\n\n    public DeviceService(\n        IDeviceRepository deviceRepository,\n        IPushRegistrationService pushRegistrationService,\n        IOrganizationUserRepository organizationUserRepository,\n        IGlobalSettings globalSettings)\n    {\n        _deviceRepository = deviceRepository;\n        _pushRegistrationService = pushRegistrationService;\n        _organizationUserRepository = organizationUserRepository;\n        _globalSettings = globalSettings;\n    }\n\n    public async Task SaveAsync(WebPushRegistrationData webPush, Device device, IEnumerable<string> organizationIds)\n    {\n        await _pushRegistrationService.CreateOrUpdateRegistrationAsync(\n            new PushRegistrationData(webPush.Endpoint, webPush.P256dh, webPush.Auth),\n            device.Id.ToString(),\n            device.UserId.ToString(),\n            device.Identifier,\n            device.Type,\n            organizationIds,\n            _globalSettings.Installation.Id\n        );\n    }\n\n    public async Task SaveAsync(Device device)\n    {\n        await SaveAsync(new PushRegistrationData(device.PushToken), device);\n    }\n\n    private async Task SaveAsync(PushRegistrationData data, Device device)\n    {\n        if (device.Id == default)\n        {\n            await _deviceRepository.CreateAsync(device);\n        }\n        else\n        {\n            device.RevisionDate = DateTime.UtcNow;\n            await _deviceRepository.ReplaceAsync(device);\n        }\n\n        var organizationIdsString =\n            (await _organizationUserRepository.GetManyDetailsByUserAsync(device.UserId,\n                OrganizationUserStatusType.Confirmed))\n            .Select(ou => ou.OrganizationId.ToString());\n\n        await _pushRegistrationService.CreateOrUpdateRegistrationAsync(data, device.Id.ToString(),\n            device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString, _globalSettings.Installation.Id);\n\n    }\n\n    public async Task ClearTokenAsync(Device device)\n    {\n        await _deviceRepository.ClearPushTokenAsync(device.Id);\n        await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());\n    }\n\n    public async Task DeactivateAsync(Device device)\n    {\n        // already deactivated\n        if (!device.Active)\n        {\n            return;\n        }\n\n        device.Active = false;\n        device.RevisionDate = DateTime.UtcNow;\n        device.EncryptedPrivateKey = null;\n        device.EncryptedPublicKey = null;\n        device.EncryptedUserKey = null;\n        await _deviceRepository.UpsertAsync(device);\n\n        await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());\n    }\n\n    public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,\n        Guid currentUserId,\n        DeviceKeysUpdateRequestModel currentDeviceUpdate,\n        IEnumerable<OtherDeviceKeysUpdateRequestModel> alteredDevices)\n    {\n        var existingDevices = await _deviceRepository.GetManyByUserIdAsync(currentUserId);\n\n        var currentDevice = existingDevices.FirstOrDefault(d => d.Identifier == currentDeviceIdentifier);\n\n        if (currentDevice == null)\n        {\n            throw new NotFoundException();\n        }\n\n        existingDevices.Remove(currentDevice);\n\n        var alterDeviceKeysDict = alteredDevices.ToDictionary(d => d.DeviceId);\n\n        if (alterDeviceKeysDict.ContainsKey(currentDevice.Id))\n        {\n            throw new BadRequestException(\"Current device can not be an optional rotation.\");\n        }\n\n        currentDevice.EncryptedPublicKey = currentDeviceUpdate.EncryptedPublicKey;\n        currentDevice.EncryptedUserKey = currentDeviceUpdate.EncryptedUserKey;\n\n        await _deviceRepository.UpsertAsync(currentDevice);\n\n        foreach (var device in existingDevices)\n        {\n            if (!device.IsTrusted())\n            {\n                // You can't update the trust of a device that isn't trusted to begin with\n                // should we throw and consider this a BadRequest? If we want to consider it a invalid request\n                // we need to check that information before we enter this foreach, we don't want to partially complete\n                // this process.\n                continue;\n            }\n\n            if (alterDeviceKeysDict.TryGetValue(device.Id, out var updateRequest))\n            {\n                // An update to this device was requested\n                device.EncryptedPublicKey = updateRequest.EncryptedPublicKey;\n                device.EncryptedUserKey = updateRequest.EncryptedUserKey;\n            }\n            else\n            {\n                // No update to this device requested, just untrust it\n                device.EncryptedUserKey = null;\n                device.EncryptedPublicKey = null;\n                device.EncryptedPrivateKey = null;\n            }\n\n            await _deviceRepository.UpsertAsync(device);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/DnsResolverService.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing DnsClient;\n\nnamespace Bit.Core.Services;\n\npublic class DnsResolverService : IDnsResolverService\n{\n    private readonly ILookupClient _client;\n\n    public DnsResolverService(ILookupClient client)\n    {\n        _client = client;\n    }\n    public async Task<bool> ResolveAsync(string domain, string txtRecord, CancellationToken cancellationToken = default)\n    {\n        var result = await _client.QueryAsync(new DnsQuestion(domain, QueryType.TXT), cancellationToken);\n        if (!result.HasError)\n        {\n            return result.Answers.TxtRecords()\n                .Select(t => t?.EscapedText?.FirstOrDefault())\n                .Any(t => t == txtRecord);\n        }\n\n        throw new DnsQueryException(result.ErrorMessage);\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/FeatureRoutedCacheService.cs",
    "content": "﻿using Bit.Core.AdminConsole.AbilitiesCache;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Models.Data.Organizations;\n\nnamespace Bit.Core.Services.Implementations;\n\npublic class FeatureRoutedCacheService(\n    IVCurrentInMemoryApplicationCacheService inMemoryApplicationCacheService)\n    : IApplicationCacheService\n{\n    public Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync() =>\n        inMemoryApplicationCacheService.GetOrganizationAbilitiesAsync();\n\n    public Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId) =>\n        inMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId);\n\n    public Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync() =>\n        inMemoryApplicationCacheService.GetProviderAbilitiesAsync();\n\n    public async Task<ProviderAbility?> GetProviderAbilityAsync(Guid providerId)\n    {\n        (await GetProviderAbilitiesAsync([providerId])).TryGetValue(providerId, out var providerAbility);\n        return providerAbility;\n    }\n\n    public async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync(IEnumerable<Guid> providerIds)\n    {\n        var allProviderAbilities = await inMemoryApplicationCacheService.GetProviderAbilitiesAsync();\n        return providerIds\n            .Where(allProviderAbilities.ContainsKey)\n            .ToDictionary(id => id, id => allProviderAbilities[id]);\n    }\n\n    public async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync(IEnumerable<Guid> orgIds)\n    {\n        var allOrganizationAbilities = await inMemoryApplicationCacheService.GetOrganizationAbilitiesAsync();\n        return orgIds\n            .Where(allOrganizationAbilities.ContainsKey)\n            .ToDictionary(id => id, id => allOrganizationAbilities[id]);\n    }\n\n    public Task UpsertOrganizationAbilityAsync(Organization organization) =>\n        inMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization);\n\n    public Task UpsertProviderAbilityAsync(Provider provider) =>\n        inMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider);\n\n    public Task DeleteOrganizationAbilityAsync(Guid organizationId) =>\n        inMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId);\n\n    public Task DeleteProviderAbilityAsync(Guid providerId) =>\n        inMemoryApplicationCacheService.DeleteProviderAbilityAsync(providerId);\n\n    public async Task BaseUpsertOrganizationAbilityAsync(Organization organization)\n    {\n        if (inMemoryApplicationCacheService is InMemoryServiceBusApplicationCacheService serviceBusCache)\n        {\n            await serviceBusCache.BaseUpsertOrganizationAbilityAsync(organization);\n        }\n        else\n        {\n            throw new InvalidOperationException($\"Expected {nameof(inMemoryApplicationCacheService)} to be of type {nameof(InMemoryServiceBusApplicationCacheService)}\");\n        }\n    }\n\n    public async Task BaseDeleteOrganizationAbilityAsync(Guid organizationId)\n    {\n        if (inMemoryApplicationCacheService is InMemoryServiceBusApplicationCacheService serviceBusCache)\n        {\n            await serviceBusCache.BaseDeleteOrganizationAbilityAsync(organizationId);\n        }\n        else\n        {\n            throw new InvalidOperationException($\"Expected {nameof(inMemoryApplicationCacheService)} to be of type {nameof(InMemoryServiceBusApplicationCacheService)}\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/I18nService.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Resources;\nusing Microsoft.Extensions.Localization;\n\nnamespace Bit.Core.Services;\n\npublic class I18nService : II18nService\n{\n    private readonly IStringLocalizer _localizer;\n\n    public I18nService(IStringLocalizerFactory factory)\n    {\n        var assemblyName = typeof(SharedResources).Assembly.GetName()!;\n        _localizer = factory.Create(\"SharedResources\", assemblyName.Name!);\n    }\n\n    public LocalizedString GetLocalizedHtmlString(string key)\n    {\n        return _localizer[key];\n    }\n\n    public LocalizedString GetLocalizedHtmlString(string key, params object?[] args)\n    {\n#nullable disable // IStringLocalizer does actually support null args, it is annotated incorrectly: https://github.com/dotnet/aspnetcore/issues/44251\n        return _localizer[key, args];\n#nullable enable\n    }\n\n    public string Translate(string key, params object?[] args)\n    {\n        return string.Format(GetLocalizedHtmlString(key).ToString(), args);\n    }\n\n    public string T(string key, params object?[] args)\n    {\n        return Translate(key, args);\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/I18nViewLocalizer.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Resources;\nusing Microsoft.AspNetCore.Mvc.Localization;\nusing Microsoft.Extensions.Localization;\n\nnamespace Bit.Core.Services;\n\npublic class I18nViewLocalizer : IViewLocalizer\n{\n    private readonly IStringLocalizer _stringLocalizer;\n    private readonly IHtmlLocalizer _htmlLocalizer;\n\n    public I18nViewLocalizer(IStringLocalizerFactory stringFactory,\n        IHtmlLocalizerFactory htmlFactory)\n    {\n        var assemblyName = typeof(SharedResources).Assembly.GetName()!;\n        _stringLocalizer = stringFactory.Create(\"SharedResources\", assemblyName.Name!);\n        _htmlLocalizer = htmlFactory.Create(\"SharedResources\", assemblyName.Name!);\n    }\n\n    public LocalizedHtmlString this[string name] => _htmlLocalizer[name];\n    public LocalizedHtmlString this[string name, params object[] args] => _htmlLocalizer[name, args];\n\n    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) =>\n        _stringLocalizer.GetAllStrings(includeParentCultures);\n\n    public LocalizedString GetString(string name) => _stringLocalizer[name];\n    public LocalizedString GetString(string name, params object[] arguments) =>\n        _stringLocalizer[name, arguments];\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/InMemoryApplicationCacheService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.AbilitiesCache;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Core.Services;\n\npublic class InMemoryApplicationCacheService : IVCurrentInMemoryApplicationCacheService\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IProviderRepository _providerRepository;\n    private DateTime _lastOrgAbilityRefresh = DateTime.MinValue;\n    private IDictionary<Guid, OrganizationAbility> _orgAbilities;\n    private TimeSpan _orgAbilitiesRefreshInterval = TimeSpan.FromMinutes(10);\n\n    private IDictionary<Guid, ProviderAbility> _providerAbilities;\n\n    public InMemoryApplicationCacheService(\n        IOrganizationRepository organizationRepository, IProviderRepository providerRepository)\n    {\n        _organizationRepository = organizationRepository;\n        _providerRepository = providerRepository;\n    }\n\n    public virtual async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync()\n    {\n        await InitOrganizationAbilitiesAsync();\n        return _orgAbilities;\n    }\n\n#nullable enable\n    public async Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid organizationId)\n    {\n        (await GetOrganizationAbilitiesAsync())\n            .TryGetValue(organizationId, out var organizationAbility);\n        return organizationAbility;\n    }\n#nullable disable\n\n    public virtual async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync()\n    {\n        await InitProviderAbilitiesAsync();\n        return _providerAbilities;\n    }\n\n#nullable enable\n    public async Task<ProviderAbility?> GetProviderAbilityAsync(Guid providerId)\n    {\n        (await GetProviderAbilitiesAsync()).TryGetValue(providerId, out var providerAbility);\n        return providerAbility;\n    }\n#nullable disable\n\n    public async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync(IEnumerable<Guid> providerIds)\n    {\n        var allProviderAbilities = await GetProviderAbilitiesAsync();\n        return providerIds\n            .Where(allProviderAbilities.ContainsKey)\n            .ToDictionary(id => id, id => allProviderAbilities[id]);\n    }\n\n    public async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync(IEnumerable<Guid> orgIds)\n    {\n        var allOrganizationAbilities = await GetOrganizationAbilitiesAsync();\n        return orgIds\n            .Where(allOrganizationAbilities.ContainsKey)\n            .ToDictionary(id => id, id => allOrganizationAbilities[id]);\n    }\n\n    public virtual async Task UpsertProviderAbilityAsync(Provider provider)\n    {\n        await InitProviderAbilitiesAsync();\n        var newAbility = new ProviderAbility(provider);\n\n        _providerAbilities[provider.Id] = newAbility;\n    }\n\n    public virtual async Task UpsertOrganizationAbilityAsync(Organization organization)\n    {\n        await InitOrganizationAbilitiesAsync();\n        var newAbility = new OrganizationAbility(organization);\n\n        _orgAbilities[organization.Id] = newAbility;\n    }\n\n    public virtual Task DeleteOrganizationAbilityAsync(Guid organizationId)\n    {\n        _orgAbilities?.Remove(organizationId);\n\n        return Task.FromResult(0);\n    }\n\n    public virtual Task DeleteProviderAbilityAsync(Guid providerId)\n    {\n        _providerAbilities?.Remove(providerId);\n\n        return Task.FromResult(0);\n    }\n\n    private async Task InitOrganizationAbilitiesAsync()\n    {\n        var now = DateTime.UtcNow;\n        if (_orgAbilities == null || (now - _lastOrgAbilityRefresh) > _orgAbilitiesRefreshInterval)\n        {\n            var abilities = await _organizationRepository.GetManyAbilitiesAsync();\n            _orgAbilities = abilities.ToDictionary(a => a.Id);\n            _lastOrgAbilityRefresh = now;\n        }\n    }\n\n    private async Task InitProviderAbilitiesAsync()\n    {\n        var now = DateTime.UtcNow;\n        if (_providerAbilities == null || (now - _lastOrgAbilityRefresh) > _orgAbilitiesRefreshInterval)\n        {\n            var abilities = await _providerRepository.GetManyAbilitiesAsync();\n            _providerAbilities = abilities.ToDictionary(a => a.Id);\n            _lastOrgAbilityRefresh = now;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/InMemoryServiceBusApplicationCacheService.cs",
    "content": "﻿using Azure.Messaging.ServiceBus;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Services;\n\npublic class InMemoryServiceBusApplicationCacheService : InMemoryApplicationCacheService\n{\n    private readonly ServiceBusSender _topicMessageSender;\n    private readonly string _subName;\n\n    public InMemoryServiceBusApplicationCacheService(\n        IOrganizationRepository organizationRepository,\n        IProviderRepository providerRepository,\n        GlobalSettings globalSettings)\n        : base(organizationRepository, providerRepository)\n    {\n        _subName = CoreHelpers.GetApplicationCacheServiceBusSubscriptionName(globalSettings);\n\n        _topicMessageSender = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString).CreateSender(globalSettings.ServiceBus.ApplicationCacheTopicName);\n    }\n\n    public override async Task UpsertOrganizationAbilityAsync(Organization organization)\n    {\n        await base.UpsertOrganizationAbilityAsync(organization);\n        var message = new ServiceBusMessage\n        {\n            Subject = _subName,\n            ApplicationProperties =\n            {\n                { \"type\", (byte)ApplicationCacheMessageType.UpsertOrganizationAbility },\n                { \"id\", organization.Id },\n            }\n        };\n        var task = _topicMessageSender.SendMessageAsync(message);\n    }\n\n    public override async Task DeleteOrganizationAbilityAsync(Guid organizationId)\n    {\n        await base.DeleteOrganizationAbilityAsync(organizationId);\n        var message = new ServiceBusMessage\n        {\n            Subject = _subName,\n            ApplicationProperties =\n            {\n                { \"type\", (byte)ApplicationCacheMessageType.DeleteOrganizationAbility },\n                { \"id\", organizationId },\n            }\n        };\n        var task = _topicMessageSender.SendMessageAsync(message);\n    }\n\n    public async Task BaseUpsertOrganizationAbilityAsync(Organization organization)\n    {\n        await base.UpsertOrganizationAbilityAsync(organization);\n    }\n\n    public async Task BaseDeleteOrganizationAbilityAsync(Guid organizationId)\n    {\n        await base.DeleteOrganizationAbilityAsync(organizationId);\n    }\n\n    public override async Task DeleteProviderAbilityAsync(Guid providerId)\n    {\n        await base.DeleteProviderAbilityAsync(providerId);\n        var message = new ServiceBusMessage\n        {\n            Subject = _subName,\n            ApplicationProperties =\n            {\n                { \"type\", (byte)ApplicationCacheMessageType.DeleteProviderAbility },\n                { \"id\", providerId },\n            }\n        };\n        var task = _topicMessageSender.SendMessageAsync(message);\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/LaunchDarklyFeatureService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing LaunchDarkly.Logging;\nusing LaunchDarkly.Sdk;\nusing LaunchDarkly.Sdk.Server;\nusing LaunchDarkly.Sdk.Server.Integrations;\nusing LaunchDarkly.Sdk.Server.Interfaces;\n\nnamespace Bit.Core.Services;\n\npublic class LaunchDarklyFeatureService : IFeatureService\n{\n    private readonly ILdClient _client;\n    private readonly ICurrentContext _currentContext;\n    private const string _anonymousUser = \"25a15cac-58cf-4ac0-ad0f-b17c4bd92294\";\n\n    private const string _contextKindDevice = \"device\";\n    private const string _contextKindOrganization = \"organization\";\n    private const string _contextKindServiceAccount = \"service-account\";\n\n    private const string _contextAttributeClientVersion = \"client-version\";\n    private const string _contextAttributeClientVersionIsPrerelease = \"client-version-is-prerelease\";\n    private const string _contextAttributeDeviceType = \"device-type\";\n    private const string _contextAttributeClientType = \"client-type\";\n    private const string _contextAttributeOrganizations = \"organizations\";\n\n    public LaunchDarklyFeatureService(\n        ILdClient client,\n        ICurrentContext currentContext)\n    {\n        _client = client;\n        _currentContext = currentContext;\n    }\n\n    public static Configuration GetConfiguredClient(GlobalSettings globalSettings)\n    {\n        var ldConfig = Configuration.Builder(globalSettings.LaunchDarkly?.SdkKey);\n        ldConfig.Logging(Components.Logging().Level(LogLevel.Error));\n\n        if (!string.IsNullOrEmpty(globalSettings.ProjectName))\n        {\n            ldConfig.ApplicationInfo(Components.ApplicationInfo()\n                .ApplicationId(globalSettings.ProjectName)\n                .ApplicationName(globalSettings.ProjectName)\n                .ApplicationVersion(AssemblyHelpers.GetGitHash() ?? $\"v{AssemblyHelpers.GetVersion()}\")\n                .ApplicationVersionName(AssemblyHelpers.GetVersion())\n            );\n        }\n\n        if (string.IsNullOrEmpty(globalSettings.LaunchDarkly?.SdkKey))\n        {\n            // support a file to load flag values\n            if (File.Exists(globalSettings.LaunchDarkly?.FlagDataFilePath))\n            {\n                ldConfig.DataSource(\n                    FileData.DataSource()\n                        .FilePaths(globalSettings.LaunchDarkly?.FlagDataFilePath)\n                        .AutoUpdate(true)\n                );\n            }\n            // support configuration directly from settings\n            else if (globalSettings.LaunchDarkly?.FlagValues?.Any() is true)\n            {\n                ldConfig.DataSource(BuildDataSource(globalSettings.LaunchDarkly.FlagValues));\n            }\n            // support local overrides\n            else if (FeatureFlagKeys.GetLocalOverrideFlagValues()?.Any() is true)\n            {\n                ldConfig.DataSource(BuildDataSource(FeatureFlagKeys.GetLocalOverrideFlagValues()));\n            }\n            else\n            {\n                // when fallbacks aren't available, work offline\n                ldConfig.Offline(true);\n            }\n\n            // do not provide analytics events\n            ldConfig.Events(Components.NoEvents);\n        }\n        else if (globalSettings.SelfHosted)\n        {\n            // when self-hosted, work offline\n            ldConfig.Offline(true);\n        }\n\n        return ldConfig.Build();\n    }\n\n    public bool IsOnline()\n    {\n        return _client.Initialized && !_client.IsOffline();\n    }\n\n    public bool IsEnabled(string key, bool defaultValue = false)\n    {\n        return _client.BoolVariation(key, BuildContext(), defaultValue);\n    }\n\n    public int GetIntVariation(string key, int defaultValue = 0)\n    {\n        return _client.IntVariation(key, BuildContext(), defaultValue);\n    }\n\n    public string GetStringVariation(string key, string defaultValue = null)\n    {\n        return _client.StringVariation(key, BuildContext(), defaultValue);\n    }\n\n    public Dictionary<string, object> GetAll()\n    {\n        var results = new Dictionary<string, object>();\n\n        var keys = FeatureFlagKeys.GetAllKeys();\n\n        var values = _client.AllFlagsState(BuildContext());\n        if (values.Valid)\n        {\n            foreach (var key in keys)\n            {\n                var value = values.GetFlagValueJson(key);\n                switch (value.Type)\n                {\n                    case LdValueType.Bool:\n                        results.Add(key, value.AsBool);\n                        break;\n\n                    case LdValueType.Number:\n                        results.Add(key, value.AsInt);\n                        break;\n\n                    case LdValueType.String:\n                        results.Add(key, value.AsString);\n                        break;\n                }\n            }\n        }\n\n        return results;\n    }\n\n    private LaunchDarkly.Sdk.Context BuildContext()\n    {\n        void SetCommonContextAttributes(ContextBuilder builder)\n        {\n            if (_currentContext.ClientVersion != null)\n            {\n                builder.Set(_contextAttributeClientVersion, _currentContext.ClientVersion.ToString());\n                builder.Set(_contextAttributeClientVersionIsPrerelease, _currentContext.ClientVersionIsPrerelease);\n            }\n\n            if (_currentContext.DeviceType.HasValue)\n            {\n                builder.Set(_contextAttributeDeviceType, (int)_currentContext.DeviceType.Value);\n                builder.Set(_contextAttributeClientType, (int)DeviceTypes.ToClientType(_currentContext.DeviceType.Value));\n            }\n        }\n\n        var builder = LaunchDarkly.Sdk.Context.MultiBuilder();\n\n        if (!string.IsNullOrWhiteSpace(_currentContext.DeviceIdentifier))\n        {\n            var ldDevice = LaunchDarkly.Sdk.Context.Builder(_currentContext.DeviceIdentifier);\n\n            ldDevice.Kind(_contextKindDevice);\n            SetCommonContextAttributes(ldDevice);\n\n            builder.Add(ldDevice.Build());\n        }\n\n        switch (_currentContext.IdentityClientType)\n        {\n            case IdentityClientType.User:\n                {\n                    ContextBuilder ldUser;\n                    if (_currentContext.UserId.HasValue)\n                    {\n                        ldUser = LaunchDarkly.Sdk.Context.Builder(_currentContext.UserId.Value.ToString());\n                    }\n                    else\n                    {\n                        // group all unauthenticated activity under one anonymous user key and mark as such\n                        ldUser = LaunchDarkly.Sdk.Context.Builder(_anonymousUser);\n                        ldUser.Anonymous(true);\n                    }\n\n                    ldUser.Kind(ContextKind.Default);\n                    SetCommonContextAttributes(ldUser);\n\n                    if (_currentContext.Organizations?.Any() ?? false)\n                    {\n                        var ldOrgs = _currentContext.Organizations.Select(o => LdValue.Of(o.Id.ToString()));\n                        ldUser.Set(_contextAttributeOrganizations, LdValue.ArrayFrom(ldOrgs));\n                    }\n\n                    builder.Add(ldUser.Build());\n                }\n                break;\n\n            case IdentityClientType.Organization:\n                {\n                    if (_currentContext.OrganizationId.HasValue)\n                    {\n                        var ldOrg = LaunchDarkly.Sdk.Context.Builder(_currentContext.OrganizationId.Value.ToString());\n\n                        ldOrg.Kind(_contextKindOrganization);\n                        SetCommonContextAttributes(ldOrg);\n\n                        builder.Add(ldOrg.Build());\n                    }\n                }\n                break;\n\n            case IdentityClientType.ServiceAccount:\n                {\n                    if (_currentContext.UserId.HasValue)\n                    {\n                        var ldServiceAccount = LaunchDarkly.Sdk.Context.Builder(_currentContext.UserId.Value.ToString());\n\n                        ldServiceAccount.Kind(_contextKindServiceAccount);\n                        SetCommonContextAttributes(ldServiceAccount);\n\n                        builder.Add(ldServiceAccount.Build());\n                    }\n                    else if (_currentContext.OrganizationId.HasValue)\n                    {\n                        var ldServiceAccount = LaunchDarkly.Sdk.Context.Builder(_currentContext.OrganizationId.Value.ToString());\n\n                        ldServiceAccount.Kind(_contextKindServiceAccount);\n                        SetCommonContextAttributes(ldServiceAccount);\n\n                        builder.Add(ldServiceAccount.Build());\n                    }\n                }\n                break;\n        }\n\n        return builder.Build();\n    }\n\n    private static TestData BuildDataSource(Dictionary<string, string> values)\n    {\n        var source = TestData.DataSource();\n        foreach (var kvp in values)\n        {\n            if (bool.TryParse(kvp.Value, out var boolValue))\n            {\n                source.Update(source.Flag(kvp.Key).ValueForAll(LdValue.Of(boolValue)));\n            }\n            else if (int.TryParse(kvp.Value, out var intValue))\n            {\n                source.Update(source.Flag(kvp.Key).ValueForAll(LdValue.Of(intValue)));\n            }\n            else\n            {\n                source.Update(source.Flag(kvp.Key).ValueForAll(LdValue.Of(kvp.Value)));\n            }\n        }\n\n        return source;\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Implementations/UserService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Licenses;\nusing Bit.Core.Billing.Licenses.Extensions;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Fido2NetLib;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing File = System.IO.File;\nusing JsonSerializer = System.Text.Json.JsonSerializer;\n\nnamespace Bit.Core.Services;\n\npublic class UserService : UserManager<User>, IUserService\n{\n    private readonly IUserRepository _userRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationDomainRepository _organizationDomainRepository;\n    private readonly IMailService _mailService;\n    private readonly IPushNotificationService _pushService;\n    private readonly IdentityErrorDescriber _identityErrorDescriber;\n    private readonly IdentityOptions _identityOptions;\n    private readonly IPasswordHasher<User> _passwordHasher;\n    private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;\n    private readonly ILicensingService _licenseService;\n    private readonly IEventService _eventService;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IStripePaymentService _paymentService;\n    private readonly IPolicyQuery _policyQuery;\n    private readonly IPolicyService _policyService;\n    private readonly IFido2 _fido2;\n    private readonly ICurrentContext _currentContext;\n    private readonly IGlobalSettings _globalSettings;\n    private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IStripeSyncService _stripeSyncService;\n    private readonly IFeatureService _featureService;\n    private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly IDistributedCache _distributedCache;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IPricingClient _pricingClient;\n    private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;\n\n    public UserService(\n        IUserRepository userRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IOrganizationRepository organizationRepository,\n        IOrganizationDomainRepository organizationDomainRepository,\n        IMailService mailService,\n        IPushNotificationService pushService,\n        IUserStore<User> store,\n        IOptions<IdentityOptions> optionsAccessor,\n        IPasswordHasher<User> passwordHasher,\n        IEnumerable<IUserValidator<User>> userValidators,\n        IEnumerable<IPasswordValidator<User>> passwordValidators,\n        ILookupNormalizer keyNormalizer,\n        IdentityErrorDescriber errors,\n        IServiceProvider services,\n        ILogger<UserManager<User>> logger,\n        ILicensingService licenseService,\n        IEventService eventService,\n        IApplicationCacheService applicationCacheService,\n        IStripePaymentService paymentService,\n        IPolicyQuery policyQuery,\n        IPolicyService policyService,\n        IFido2 fido2,\n        ICurrentContext currentContext,\n        IGlobalSettings globalSettings,\n        IAcceptOrgUserCommand acceptOrgUserCommand,\n        IProviderUserRepository providerUserRepository,\n        IStripeSyncService stripeSyncService,\n        IFeatureService featureService,\n        IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IDistributedCache distributedCache,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IPricingClient pricingClient,\n        IHasPremiumAccessQuery hasPremiumAccessQuery)\n        : base(\n              store,\n              optionsAccessor,\n              passwordHasher,\n              userValidators,\n              passwordValidators,\n              keyNormalizer,\n              errors,\n              services,\n              logger)\n    {\n        _userRepository = userRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _organizationRepository = organizationRepository;\n        _organizationDomainRepository = organizationDomainRepository;\n        _mailService = mailService;\n        _pushService = pushService;\n        _identityOptions = optionsAccessor?.Value ?? new IdentityOptions();\n        _identityErrorDescriber = errors;\n        _passwordHasher = passwordHasher;\n        _passwordValidators = passwordValidators;\n        _licenseService = licenseService;\n        _eventService = eventService;\n        _applicationCacheService = applicationCacheService;\n        _paymentService = paymentService;\n        _policyQuery = policyQuery;\n        _policyService = policyService;\n        _fido2 = fido2;\n        _currentContext = currentContext;\n        _globalSettings = globalSettings;\n        _acceptOrgUserCommand = acceptOrgUserCommand;\n        _providerUserRepository = providerUserRepository;\n        _stripeSyncService = stripeSyncService;\n        _featureService = featureService;\n        _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;\n        _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n        _distributedCache = distributedCache;\n        _policyRequirementQuery = policyRequirementQuery;\n        _pricingClient = pricingClient;\n        _hasPremiumAccessQuery = hasPremiumAccessQuery;\n    }\n\n    public Guid? GetProperUserId(ClaimsPrincipal principal)\n    {\n        if (!Guid.TryParse(GetUserId(principal), out var userIdGuid))\n        {\n            return null;\n        }\n\n        return userIdGuid;\n    }\n\n    public async Task<User> GetUserByIdAsync(string userId)\n    {\n        if (_currentContext?.User != null &&\n            string.Equals(_currentContext.User.Id.ToString(), userId, StringComparison.InvariantCultureIgnoreCase))\n        {\n            return _currentContext.User;\n        }\n\n        if (!Guid.TryParse(userId, out var userIdGuid))\n        {\n            return null;\n        }\n\n        _currentContext.User = await _userRepository.GetByIdAsync(userIdGuid);\n        return _currentContext.User;\n    }\n\n    public async Task<User> GetUserByIdAsync(Guid userId)\n    {\n        if (_currentContext?.User != null && _currentContext.User.Id == userId)\n        {\n            return _currentContext.User;\n        }\n\n        _currentContext.User = await _userRepository.GetByIdAsync(userId);\n        return _currentContext.User;\n    }\n\n    public async Task<User> GetUserByPrincipalAsync(ClaimsPrincipal principal)\n    {\n        var userId = GetProperUserId(principal);\n        if (!userId.HasValue)\n        {\n            return null;\n        }\n\n        return await GetUserByIdAsync(userId.Value);\n    }\n\n    public async Task<DateTime> GetAccountRevisionDateByIdAsync(Guid userId)\n    {\n        return await _userRepository.GetAccountRevisionDateAsync(userId);\n    }\n\n    public async Task SaveUserAsync(User user, bool push = false)\n    {\n        if (user.Id == default(Guid))\n        {\n            throw new ApplicationException(\"Use register method to create a new user.\");\n        }\n\n        // if the name is empty, set it to null\n        if (String.Equals(user.Name, String.Empty))\n        {\n            user.Name = null;\n        }\n\n        user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;\n        await _userRepository.ReplaceAsync(user);\n\n        if (push)\n        {\n            // push\n            await _pushService.PushSyncSettingsAsync(user.Id);\n        }\n    }\n\n    public override async Task<IdentityResult> DeleteAsync(User user)\n    {\n        // Check if user is the only owner of any organizations.\n        var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id);\n        if (onlyOwnerCount > 0)\n        {\n            var deletedOrg = false;\n            var orgs = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id,\n                OrganizationUserStatusType.Confirmed);\n            if (orgs.Count == 1)\n            {\n                var org = await _organizationRepository.GetByIdAsync(orgs.First().OrganizationId);\n                if (org != null && (!org.Enabled || string.IsNullOrWhiteSpace(org.GatewaySubscriptionId)))\n                {\n                    var orgCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(org.Id);\n                    if (orgCount <= 1)\n                    {\n                        await _organizationRepository.DeleteAsync(org);\n                        deletedOrg = true;\n                    }\n                }\n            }\n\n            if (!deletedOrg)\n            {\n                return IdentityResult.Failed(new IdentityError\n                {\n                    Description = \"Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user.\",\n                });\n            }\n        }\n\n        var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id);\n        if (onlyOwnerProviderCount > 0)\n        {\n            return IdentityResult.Failed(new IdentityError\n            {\n                Description = \"Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user.\",\n            });\n        }\n\n        if (!string.IsNullOrWhiteSpace(user.GatewaySubscriptionId))\n        {\n            try\n            {\n                await CancelPremiumAsync(user);\n            }\n            catch (GatewayException) { }\n        }\n\n        await _userRepository.DeleteAsync(user);\n        await _pushService.PushLogOutAsync(user.Id);\n        return IdentityResult.Success;\n    }\n\n    public async Task<IdentityResult> DeleteAsync(User user, string token)\n    {\n        if (!(await VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, \"DeleteAccount\", token)))\n        {\n            return IdentityResult.Failed(ErrorDescriber.InvalidToken());\n        }\n\n        return await DeleteAsync(user);\n    }\n\n    public async Task SendDeleteConfirmationAsync(string email)\n    {\n        var user = await _userRepository.GetByEmailAsync(email);\n        if (user == null)\n        {\n            // No user exists.\n            return;\n        }\n\n        if (await IsClaimedByAnyOrganizationAsync(user.Id))\n        {\n            await _mailService.SendCannotDeleteClaimedAccountEmailAsync(user.Email);\n            return;\n        }\n\n        var token = await GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, \"DeleteAccount\");\n        await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);\n    }\n\n    public async Task<IdentityResult> CreateUserAsync(User user)\n    {\n        return await CreateAsync(user);\n    }\n\n    public async Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash)\n    {\n        return await CreateAsync(user, masterPasswordHash);\n    }\n\n    public async Task SendMasterPasswordHintAsync(string email)\n    {\n        var user = await _userRepository.GetByEmailAsync(email);\n        if (user == null)\n        {\n            // No user exists. Do we want to send an email telling them this in the future?\n            return;\n        }\n\n        if (string.IsNullOrWhiteSpace(user.MasterPasswordHint))\n        {\n            await _mailService.SendNoMasterPasswordHintEmailAsync(email);\n            return;\n        }\n\n        await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint);\n    }\n\n    public async Task SendEmailVerificationAsync(User user)\n    {\n        if (user.EmailVerified)\n        {\n            throw new BadRequestException(\"Email already verified.\");\n        }\n\n        var token = await base.GenerateEmailConfirmationTokenAsync(user);\n        await _mailService.SendVerifyEmailEmailAsync(user.Email, user.Id, token);\n    }\n\n    public async Task InitiateEmailChangeAsync(User user, string newEmail)\n    {\n        var existingUser = await _userRepository.GetByEmailAsync(newEmail);\n        if (existingUser != null)\n        {\n            await _mailService.SendChangeEmailAlreadyExistsEmailAsync(user.Email, newEmail);\n            return;\n        }\n\n        var token = await base.GenerateChangeEmailTokenAsync(user, newEmail);\n        await _mailService.SendChangeEmailEmailAsync(newEmail, token);\n    }\n\n    public async Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail,\n        string newMasterPassword, string token, string key)\n    {\n        var verifyPasswordResult = _passwordHasher.VerifyHashedPassword(user, user.MasterPassword, masterPassword);\n        if (verifyPasswordResult == PasswordVerificationResult.Failed)\n        {\n            return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());\n        }\n\n        var claimedUserValidationResult = await ValidateClaimedUserDomainAsync(user, newEmail);\n\n        if (!claimedUserValidationResult.Succeeded)\n        {\n            return claimedUserValidationResult;\n        }\n\n        if (!await base.VerifyUserTokenAsync(user, _identityOptions.Tokens.ChangeEmailTokenProvider,\n            GetChangeEmailTokenPurpose(newEmail), token))\n        {\n            return IdentityResult.Failed(_identityErrorDescriber.InvalidToken());\n        }\n\n        var existingUser = await _userRepository.GetByEmailAsync(newEmail);\n        if (existingUser != null && existingUser.Id != user.Id)\n        {\n            return IdentityResult.Failed(_identityErrorDescriber.DuplicateEmail(newEmail));\n        }\n\n        var previousState = new\n        {\n            Key = user.Key,\n            MasterPassword = user.MasterPassword,\n            SecurityStamp = user.SecurityStamp,\n            Email = user.Email\n        };\n\n        var result = await UpdatePasswordHash(user, newMasterPassword);\n        if (!result.Succeeded)\n        {\n            return result;\n        }\n\n        var now = DateTime.UtcNow;\n\n        user.Key = key;\n        user.Email = newEmail;\n        user.EmailVerified = true;\n        user.RevisionDate = user.AccountRevisionDate = now;\n        user.LastEmailChangeDate = now;\n        await _userRepository.ReplaceAsync(user);\n\n        if (user.Gateway == GatewayType.Stripe)\n        {\n\n            try\n            {\n                await _stripeSyncService.UpdateCustomerEmailAddressAsync(user.GatewayCustomerId,\n                    user.BillingEmailAddress());\n            }\n            catch (Exception ex)\n            {\n                //if sync to strip fails, update email and securityStamp to previous\n                user.Key = previousState.Key;\n                user.Email = previousState.Email;\n                user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;\n                user.MasterPassword = previousState.MasterPassword;\n                user.SecurityStamp = previousState.SecurityStamp;\n\n                await _userRepository.ReplaceAsync(user);\n                return IdentityResult.Failed(new IdentityError\n                {\n                    Description = ex.Message\n                });\n            }\n        }\n\n        await _pushService.PushLogOutAsync(user.Id);\n\n        return IdentityResult.Success;\n    }\n\n    public async Task<IdentityResult> ValidateClaimedUserDomainAsync(User user, string newEmail)\n    {\n        var claimingOrganization = await GetOrganizationsClaimingUserAsync(user.Id);\n\n        if (!claimingOrganization.Any())\n        {\n            return IdentityResult.Success;\n        }\n\n        var newDomain = CoreHelpers.GetEmailDomain(newEmail);\n\n        var verifiedDomains = await _organizationDomainRepository.GetVerifiedDomainsByOrganizationIdsAsync(claimingOrganization.Select(org => org.Id));\n\n        if (verifiedDomains.Any(verifiedDomain => verifiedDomain.DomainName == newDomain))\n        {\n            return IdentityResult.Success;\n        }\n\n        return IdentityResult.Failed(new IdentityError\n        {\n            Code = \"EmailDomainMismatch\",\n            Description = \"Your new email must match your organization domain.\"\n        });\n    }\n\n    public async Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string passwordHint,\n        string key)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        if (await CheckPasswordAsync(user, masterPassword))\n        {\n            var result = await UpdatePasswordHash(user, newMasterPassword);\n            if (!result.Succeeded)\n            {\n                return result;\n            }\n\n            var now = DateTime.UtcNow;\n            user.RevisionDate = user.AccountRevisionDate = now;\n            user.LastPasswordChangeDate = now;\n            user.Key = key;\n            user.MasterPasswordHint = passwordHint;\n\n            await _userRepository.ReplaceAsync(user);\n            await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);\n            await _pushService.PushLogOutAsync(user.Id, true);\n\n            return IdentityResult.Success;\n        }\n\n        Logger.LogWarning(\"Change password failed for user {userId}.\", user.Id);\n        return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());\n    }\n\n    // TODO removed with https://bitwarden.atlassian.net/browse/PM-27328\n    public async Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier)\n    {\n        var identityResult = CheckCanUseKeyConnector(user);\n        if (identityResult != null)\n        {\n            return identityResult;\n        }\n\n        user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;\n        user.Key = key;\n        user.UsesKeyConnector = true;\n\n        await _userRepository.ReplaceAsync(user);\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);\n\n        await _acceptOrgUserCommand.AcceptOrgUserByOrgSsoIdAsync(orgIdentifier, user, this);\n\n        return IdentityResult.Success;\n    }\n\n    public async Task<IdentityResult> ConvertToKeyConnectorAsync(User user, string keyConnectorKeyWrappedUserKey = null)\n    {\n        var identityResult = CheckCanUseKeyConnector(user);\n        if (identityResult != null)\n        {\n            return identityResult;\n        }\n\n        user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;\n        user.MasterPassword = null;\n        user.UsesKeyConnector = true;\n\n        if (!string.IsNullOrWhiteSpace(keyConnectorKeyWrappedUserKey))\n        {\n            user.Key = keyConnectorKeyWrappedUserKey;\n        }\n\n        await _userRepository.ReplaceAsync(user);\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);\n\n        return IdentityResult.Success;\n    }\n\n    private IdentityResult CheckCanUseKeyConnector(User user)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        if (user.UsesKeyConnector)\n        {\n            Logger.LogWarning(\"Already uses Key Connector.\");\n            return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());\n        }\n\n        if (_currentContext.Organizations.Any(u =>\n                u.Type is OrganizationUserType.Owner or OrganizationUserType.Admin))\n        {\n            throw new BadRequestException(\"Cannot use Key Connector when admin or owner of an organization.\");\n        }\n\n        return null;\n    }\n\n    public async Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType callingUserType, Guid orgId, Guid id, string newMasterPassword, string key)\n    {\n        // Org must be able to use reset password\n        var org = await _organizationRepository.GetByIdAsync(orgId);\n        if (org == null || !org.UseResetPassword)\n        {\n            throw new BadRequestException(\"Organization does not allow password reset.\");\n        }\n\n        // Enterprise policy must be enabled\n        var resetPasswordPolicy = await _policyQuery.RunAsync(orgId, PolicyType.ResetPassword);\n        if (!resetPasswordPolicy.Enabled)\n        {\n            throw new BadRequestException(\"Organization does not have the password reset policy enabled.\");\n        }\n\n        // Org User must be confirmed and have a ResetPasswordKey\n        var orgUser = await _organizationUserRepository.GetByIdAsync(id);\n        if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed ||\n            orgUser.OrganizationId != orgId ||\n            !orgUser.IsEnrolledInAccountRecovery() ||\n            !orgUser.UserId.HasValue)\n        {\n            throw new BadRequestException(\"Organization User not valid\");\n        }\n\n        // Calling User must be of higher/equal user type to reset user's password\n        var canAdjustPassword = false;\n        switch (callingUserType)\n        {\n            case OrganizationUserType.Owner:\n                canAdjustPassword = true;\n                break;\n            case OrganizationUserType.Admin:\n                canAdjustPassword = orgUser.Type != OrganizationUserType.Owner;\n                break;\n            case OrganizationUserType.Custom:\n                canAdjustPassword = orgUser.Type != OrganizationUserType.Owner &&\n                    orgUser.Type != OrganizationUserType.Admin;\n                break;\n        }\n\n        if (!canAdjustPassword)\n        {\n            throw new BadRequestException(\"Calling user does not have permission to reset this user's master password\");\n        }\n\n        var user = await GetUserByIdAsync(orgUser.UserId.Value);\n        if (user == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (user.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"Cannot reset password of a user with Key Connector.\");\n        }\n\n        var result = await UpdatePasswordHash(user, newMasterPassword);\n        if (!result.Succeeded)\n        {\n            return result;\n        }\n\n        user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;\n        user.LastPasswordChangeDate = user.RevisionDate;\n        user.ForcePasswordReset = true;\n        user.Key = key;\n\n        await _userRepository.ReplaceAsync(user);\n        await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.DisplayName());\n        await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_AdminResetPassword);\n        await _pushService.PushLogOutAsync(user.Id);\n\n        return IdentityResult.Success;\n    }\n\n    public async Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint)\n    {\n        if (!user.ForcePasswordReset)\n        {\n            throw new BadRequestException(\"User does not have a temporary password to update.\");\n        }\n\n        var result = await UpdatePasswordHash(user, newMasterPassword);\n        if (!result.Succeeded)\n        {\n            return result;\n        }\n\n        user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;\n        user.ForcePasswordReset = false;\n        user.Key = key;\n        user.MasterPasswordHint = hint;\n\n        await _userRepository.ReplaceAsync(user);\n        await _mailService.SendUpdatedTempPasswordEmailAsync(user.Email, user.Name);\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_UpdatedTempPassword);\n        await _pushService.PushLogOutAsync(user.Id);\n\n        return IdentityResult.Success;\n    }\n\n    public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string secret)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        if (await VerifySecretAsync(user, secret))\n        {\n            var result = await base.UpdateSecurityStampAsync(user);\n            if (!result.Succeeded)\n            {\n                return result;\n            }\n\n            await SaveUserAsync(user);\n            await _pushService.PushLogOutAsync(user.Id);\n            return IdentityResult.Success;\n        }\n\n        Logger.LogWarning(\"Refresh security stamp failed for user {userId}.\", user.Id);\n        return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());\n    }\n\n    public async Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true)\n    {\n        SetTwoFactorProvider(user, type, setEnabled);\n        await SaveUserAsync(user);\n        if (logEvent)\n        {\n            await _eventService.LogUserEventAsync(user.Id, EventType.User_Updated2fa);\n        }\n    }\n\n    public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type)\n    {\n        var providers = user.GetTwoFactorProviders();\n        if (!providers?.ContainsKey(type) ?? true)\n        {\n            return;\n        }\n\n        providers.Remove(type);\n        user.SetTwoFactorProviders(providers);\n        await SaveUserAsync(user);\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);\n\n        if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))\n        {\n            await CheckPoliciesOnTwoFactorRemovalAsync(user);\n        }\n    }\n\n    public async Task<bool> RecoverTwoFactorAsync(User user, string recoveryCode)\n    {\n        if (!CoreHelpers.FixedTimeEquals(\n                user.TwoFactorRecoveryCode,\n                recoveryCode.Replace(\" \", string.Empty).Trim().ToLower()))\n        {\n            return false;\n        }\n\n        user.TwoFactorProviders = null;\n        user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);\n        await SaveUserAsync(user);\n        await _mailService.SendRecoverTwoFactorEmail(user.Email, DateTime.UtcNow, _currentContext.IpAddress);\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa);\n        await CheckPoliciesOnTwoFactorRemovalAsync(user);\n\n        return true;\n    }\n\n    public async Task UpdateLicenseAsync(User user, UserLicense license)\n    {\n        if (!_globalSettings.SelfHosted)\n        {\n            throw new InvalidOperationException(\"Licenses require self hosting.\");\n        }\n\n        if (license?.LicenseType != null && license.LicenseType != LicenseType.User)\n        {\n            throw new BadRequestException(\"Organization licenses cannot be applied to a user. \"\n                + \"Upload this license from the Organization settings page.\");\n        }\n\n        if (license == null || !_licenseService.VerifyLicense(license))\n        {\n            throw new BadRequestException(\"Invalid license.\");\n        }\n\n        var claimsPrincipal = _licenseService.GetClaimsPrincipalFromLicense(license);\n\n        if (!license.CanUse(user, claimsPrincipal, out var exceptionMessage))\n        {\n            throw new BadRequestException(exceptionMessage);\n        }\n\n        // If the license has a Token (claims-based), extract all properties from claims\n        // Otherwise, fall back to using the properties already on the license object (backward compatibility)\n        if (claimsPrincipal != null)\n        {\n            license.LicenseKey = claimsPrincipal.GetValue<string>(UserLicenseConstants.LicenseKey);\n            license.Premium = claimsPrincipal.GetValue<bool>(UserLicenseConstants.Premium);\n            license.MaxStorageGb = claimsPrincipal.GetValue<short?>(UserLicenseConstants.MaxStorageGb);\n            license.Expires = claimsPrincipal.GetValue<DateTime?>(UserLicenseConstants.Expires);\n        }\n\n        var dir = $\"{_globalSettings.LicenseDirectory}/user\";\n        Directory.CreateDirectory(dir);\n        using var fs = File.OpenWrite(Path.Combine(dir, $\"{user.Id}.json\"));\n        await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);\n\n        user.Premium = license.Premium;\n        user.RevisionDate = DateTime.UtcNow;\n        user.MaxStorageGb = _globalSettings.SelfHosted ? Constants.SelfHostedMaxStorageGb : license.MaxStorageGb;\n        user.LicenseKey = license.LicenseKey;\n        user.PremiumExpirationDate = license.Expires;\n        await SaveUserAsync(user);\n    }\n\n    // TODO: Remove with deletion of pm-29594-update-individual-subscription-page\n    public async Task<string> AdjustStorageAsync(User user, short storageAdjustmentGb)\n    {\n        if (user == null)\n        {\n            throw new ArgumentNullException(nameof(user));\n        }\n\n        if (!user.Premium)\n        {\n            throw new BadRequestException(\"Not a premium user.\");\n        }\n\n        var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();\n\n        var baseStorageGb = (short)premiumPlan.Storage.Provided;\n        var secret = await BillingHelpers.AdjustStorageAsync(\n            _paymentService,\n            null,\n            _featureService,\n            user,\n            storageAdjustmentGb,\n            premiumPlan.Storage.StripePriceId,\n            baseStorageGb);\n        await SaveUserAsync(user);\n        return secret;\n    }\n\n    public async Task CancelPremiumAsync(User user, bool? endOfPeriod = null)\n    {\n        var eop = endOfPeriod.GetValueOrDefault(true);\n        if (!endOfPeriod.HasValue && user.PremiumExpirationDate.HasValue &&\n            user.PremiumExpirationDate.Value < DateTime.UtcNow)\n        {\n            eop = false;\n        }\n        await _paymentService.CancelSubscriptionAsync(user, eop);\n    }\n\n    // TODO: Remove with deletion of pm-29594-update-individual-subscription-page\n    public async Task ReinstatePremiumAsync(User user)\n    {\n        await _paymentService.ReinstateSubscriptionAsync(user);\n    }\n\n    public async Task EnablePremiumAsync(Guid userId, DateTime? expirationDate)\n    {\n        var user = await _userRepository.GetByIdAsync(userId);\n        await EnablePremiumAsync(user, expirationDate);\n    }\n\n    private async Task EnablePremiumAsync(User user, DateTime? expirationDate)\n    {\n        if (user != null && !user.Premium && user.Gateway.HasValue)\n        {\n            user.Premium = true;\n            user.PremiumExpirationDate = expirationDate;\n            user.RevisionDate = DateTime.UtcNow;\n            await _userRepository.ReplaceAsync(user);\n        }\n    }\n\n    public async Task DisablePremiumAsync(Guid userId, DateTime? expirationDate)\n    {\n        var user = await _userRepository.GetByIdAsync(userId);\n        await DisablePremiumAsync(user, expirationDate);\n    }\n\n    private async Task DisablePremiumAsync(User user, DateTime? expirationDate)\n    {\n        if (user != null && user.Premium)\n        {\n            user.Premium = false;\n            user.PremiumExpirationDate = expirationDate;\n            user.RevisionDate = DateTime.UtcNow;\n            await _userRepository.ReplaceAsync(user);\n        }\n    }\n\n    public async Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate)\n    {\n        var user = await _userRepository.GetByIdAsync(userId);\n        if (user != null)\n        {\n            user.PremiumExpirationDate = expirationDate;\n            user.RevisionDate = DateTime.UtcNow;\n            await _userRepository.ReplaceAsync(user);\n        }\n    }\n\n    public async Task<UserLicense> GenerateLicenseAsync(\n        User user,\n        SubscriptionInfo subscriptionInfo = null,\n        int? version = null)\n    {\n        if (user == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (subscriptionInfo == null && user.Gateway != null)\n        {\n            subscriptionInfo = await _paymentService.GetSubscriptionAsync(user);\n        }\n\n        var userLicense = subscriptionInfo == null\n            ? new UserLicense(user, _licenseService)\n            : new UserLicense(user, subscriptionInfo, _licenseService);\n\n        userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo);\n\n        return userLicense;\n    }\n\n    public override async Task<bool> CheckPasswordAsync(User user, string password)\n    {\n        if (user == null)\n        {\n            return false;\n        }\n\n        var result = await base.VerifyPasswordAsync(Store as IUserPasswordStore<User>, user, password);\n        if (result == PasswordVerificationResult.SuccessRehashNeeded)\n        {\n            await UpdatePasswordHash(user, password, false, false);\n            user.RevisionDate = DateTime.UtcNow;\n            await _userRepository.ReplaceAsync(user);\n        }\n\n        var success = result != PasswordVerificationResult.Failed;\n        if (!success)\n        {\n            Logger.LogWarning(0, \"Invalid password for user {userId}.\", user.Id);\n        }\n        return success;\n    }\n\n    public async Task<bool> CanAccessPremium(User user)\n    {\n        var userId = user.GetUserId();\n        if (!userId.HasValue)\n        {\n            return false;\n        }\n\n        return user.Premium || await _hasPremiumAccessQuery.HasPremiumFromOrganizationAsync(userId.Value);\n    }\n\n    public async Task<bool> HasPremiumFromOrganization(User user)\n    {\n        var userId = user.GetUserId();\n        if (!userId.HasValue)\n        {\n            return false;\n        }\n\n        return await _hasPremiumAccessQuery.HasPremiumFromOrganizationAsync(userId.Value);\n    }\n\n    public async Task<string> GenerateSignInTokenAsync(User user, string purpose)\n    {\n        var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,\n            purpose);\n        return token;\n    }\n\n    public async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,\n        bool validatePassword = true, bool refreshStamp = true)\n    {\n        if (validatePassword)\n        {\n            var validate = await ValidatePasswordInternal(user, newPassword);\n            if (!validate.Succeeded)\n            {\n                return validate;\n            }\n        }\n\n        user.MasterPassword = _passwordHasher.HashPassword(user, newPassword);\n        if (refreshStamp)\n        {\n            user.SecurityStamp = Guid.NewGuid().ToString();\n        }\n\n        return IdentityResult.Success;\n    }\n\n    public async Task<bool> IsLegacyUser(string userId)\n    {\n        if (string.IsNullOrWhiteSpace(userId))\n        {\n            return false;\n        }\n\n        var user = await FindByIdAsync(userId);\n        if (user == null)\n        {\n            return false;\n        }\n\n        return IsLegacyUser(user);\n    }\n\n    public async Task<bool> IsClaimedByAnyOrganizationAsync(Guid userId)\n    {\n        var organizationsClaimingUser = await GetOrganizationsClaimingUserAsync(userId);\n        return organizationsClaimingUser.Any();\n    }\n\n    public async Task<IEnumerable<Organization>> GetOrganizationsClaimingUserAsync(Guid userId)\n    {\n        // Get all organizations that have verified the user's email domain.\n        var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId);\n\n        // Organizations must be enabled and able to have verified domains.\n        return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseOrganizationDomains: true });\n    }\n\n    /// <inheritdoc cref=\"IsLegacyUser(string)\"/>\n    public static bool IsLegacyUser(User user)\n    {\n        return user.Key == null && user.MasterPassword != null && user.PrivateKey != null;\n    }\n\n    private async Task<IdentityResult> ValidatePasswordInternal(User user, string password)\n    {\n        var errors = new List<IdentityError>();\n        foreach (var v in _passwordValidators)\n        {\n            var result = await v.ValidateAsync(this, user, password);\n            if (!result.Succeeded)\n            {\n                errors.AddRange(result.Errors);\n            }\n        }\n\n        if (errors.Count > 0)\n        {\n            Logger.LogWarning(\"User {userId} password validation failed: {errors}.\", await GetUserIdAsync(user),\n                string.Join(\";\", errors.Select(e => e.Code)));\n            return IdentityResult.Failed(errors.ToArray());\n        }\n\n        return IdentityResult.Success;\n    }\n\n    public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true)\n    {\n        var providers = user.GetTwoFactorProviders();\n        if (providers is null || !providers.TryGetValue(type, out var provider))\n        {\n            return;\n        }\n\n        if (setEnabled)\n        {\n            provider.Enabled = true;\n        }\n        user.SetTwoFactorProviders(providers);\n\n        if (string.IsNullOrWhiteSpace(user.TwoFactorRecoveryCode))\n        {\n            user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false);\n        }\n    }\n\n    private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)\n    {\n        if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))\n        {\n            var requirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);\n            if (!requirement.OrganizationsRequiringTwoFactor.Any())\n            {\n                Logger.LogInformation(\"No organizations requiring two factor for user {userId}.\", user.Id);\n                return;\n            }\n\n            var organizationIds = requirement.OrganizationsRequiringTwoFactor.Select(o => o.OrganizationId).ToList();\n            var organizations = await _organizationRepository.GetManyByIdsAsync(organizationIds);\n            var organizationLookup = organizations.ToDictionary(org => org.Id);\n\n            var revokeOrgUserTasks = requirement.OrganizationsRequiringTwoFactor\n                .Where(o => organizationLookup.ContainsKey(o.OrganizationId))\n                .Select(async o =>\n                {\n                    var organization = organizationLookup[o.OrganizationId];\n                    await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(\n                        new RevokeOrganizationUsersRequest(\n                            o.OrganizationId,\n                            [new OrganizationUserUserDetails { Id = o.OrganizationUserId, OrganizationId = o.OrganizationId }],\n                            new SystemUser(EventSystemUser.TwoFactorDisabled)));\n                    await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);\n                }).ToArray();\n\n            await Task.WhenAll(revokeOrgUserTasks);\n\n            return;\n        }\n\n        var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);\n\n        var legacyRevokeOrgUserTasks = twoFactorPolicies.Select(async p =>\n        {\n            var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);\n            await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(\n                new RevokeOrganizationUsersRequest(\n                    p.OrganizationId,\n                    [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],\n                    new SystemUser(EventSystemUser.TwoFactorDisabled)));\n            await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);\n        }).ToArray();\n\n        await Task.WhenAll(legacyRevokeOrgUserTasks);\n    }\n\n    public async Task RotateApiKeyAsync(User user)\n    {\n        user.ApiKey = CoreHelpers.SecureRandomString(30);\n        user.RevisionDate = DateTime.UtcNow;\n        await _userRepository.ReplaceAsync(user);\n    }\n\n    public async Task SendOTPAsync(User user)\n    {\n        if (string.IsNullOrEmpty(user.Email))\n        {\n            throw new BadRequestException(\"No user email.\");\n        }\n\n        var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,\n            \"otp:\" + user.Email);\n        await _mailService.SendOTPEmailAsync(user.Email, token);\n    }\n\n    public async Task<bool> VerifyOTPAsync(User user, string token)\n    {\n        return await base.VerifyUserTokenAsync(user, TokenOptions.DefaultEmailProvider,\n            \"otp:\" + user.Email, token);\n    }\n\n    public async Task<bool> VerifySecretAsync(User user, string secret, bool isSettingMFA = false)\n    {\n        bool isVerified;\n        if (user.HasMasterPassword())\n        {\n            // If the user has a master password the secret is most likely going to be a hash\n            // of their password, but in certain scenarios, like when the user has logged into their\n            // device without a password (trusted device encryption) but the account\n            // does still have a password we will allow the use of OTP.\n            isVerified = await CheckPasswordAsync(user, secret) ||\n                await VerifyOTPAsync(user, secret);\n        }\n        else if (isSettingMFA)\n        {\n            // this is temporary to allow users to view their MFA settings without invalidating email TOTP\n            // Will be removed with PM-9925\n            isVerified = true;\n        }\n        else\n        {\n            // If they don't have a password at all they can only do OTP\n            isVerified = await VerifyOTPAsync(user, secret);\n        }\n\n        return isVerified;\n    }\n\n    public async Task<bool> ActiveNewDeviceVerificationException(Guid userId)\n    {\n        var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString());\n        var cacheValue = await _distributedCache.GetAsync(cacheKey);\n        return cacheValue != null;\n    }\n\n    public async Task ToggleNewDeviceVerificationException(Guid userId)\n    {\n        var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString());\n        var cacheValue = await _distributedCache.GetAsync(cacheKey);\n        if (cacheValue != null)\n        {\n            await _distributedCache.RemoveAsync(cacheKey);\n        }\n        else\n        {\n            await _distributedCache.SetAsync(cacheKey, new byte[1], new DistributedCacheEntryOptions\n            {\n                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24)\n            });\n        }\n    }\n\n    private async Task SendAppropriateWelcomeEmailAsync(User user, string initiationPath)\n    {\n        var isFromMarketingWebsite = initiationPath.Contains(\"Secrets Manager trial\");\n\n        if (isFromMarketingWebsite)\n        {\n            await _mailService.SendTrialInitiationEmailAsync(user.Email);\n        }\n        else\n        {\n            await _mailService.SendWelcomeEmailAsync(user);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Play/IPlayIdService.cs",
    "content": "﻿namespace Bit.Core.Services;\n\n/// <summary>\n/// Service for managing Play identifiers in automated testing infrastructure.\n/// A \"Play\" is a test session that groups entities created during testing to enable cleanup.\n/// The PlayId flows from client request (x-play-id header) through PlayIdMiddleware to this service,\n/// which repositories query to create PlayItem tracking records via IPlayItemService. The SeederAPI uses these records\n/// to bulk delete all entities associated with a PlayId. Only active in Development environments.\n/// </summary>\npublic interface IPlayIdService\n{\n    /// <summary>\n    /// Gets or sets the current Play identifier from the x-play-id request header.\n    /// </summary>\n    string? PlayId { get; set; }\n\n    /// <summary>\n    /// Checks whether the current request is part of an active Play session.\n    /// </summary>\n    /// <param name=\"playId\">The Play identifier if active, otherwise empty string.</param>\n    /// <returns>True if in a Play session (has PlayId and in Development environment), otherwise false.</returns>\n    bool InPlay(out string playId);\n}\n"
  },
  {
    "path": "src/Core/Services/Play/IPlayItemService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\n\nnamespace Bit.Core.Services;\n\n/// <summary>\n/// Service used to track added users and organizations during a Play session.\n/// </summary>\npublic interface IPlayItemService\n{\n    /// <summary>\n    /// Records a PlayItem entry for the given User created during a Play session.\n    ///\n    /// Does nothing if no Play Id is set for this http scope.\n    /// </summary>\n    /// <param name=\"user\"></param>\n    /// <returns></returns>\n    Task Record(User user);\n    /// <summary>\n    /// Records a PlayItem entry for the given Organization created during a Play session.\n    ///\n    /// Does nothing if no Play Id is set for this http scope.\n    /// </summary>\n    /// <param name=\"organization\"></param>\n    /// <returns></returns>\n    Task Record(Organization organization);\n}\n"
  },
  {
    "path": "src/Core/Services/Play/Implementations/NeverPlayIdServices.cs",
    "content": "﻿namespace Bit.Core.Services;\n\npublic class NeverPlayIdServices : IPlayIdService\n{\n    public string? PlayId\n    {\n        get => null;\n        set { }\n    }\n\n    public bool InPlay(out string playId)\n    {\n        playId = string.Empty;\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Play/Implementations/PlayIdService.cs",
    "content": "﻿using Microsoft.Extensions.Hosting;\n\nnamespace Bit.Core.Services;\n\npublic class PlayIdService(IHostEnvironment hostEnvironment) : IPlayIdService\n{\n    public string? PlayId { get; set; }\n    public bool InPlay(out string playId)\n    {\n        playId = PlayId ?? string.Empty;\n        return !string.IsNullOrEmpty(PlayId) && hostEnvironment.IsDevelopment();\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Play/Implementations/PlayIdSingletonService.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Bit.Core.Services;\n\n/// <summary>\n/// Singleton wrapper service that bridges singleton-scoped service boundaries for PlayId tracking.\n/// This allows singleton services to access the scoped PlayIdService via HttpContext.RequestServices.\n///\n/// Uses IHttpContextAccessor to retrieve the current request's scoped PlayIdService instance, enabling\n/// singleton services to participate in Play session tracking without violating DI lifetime rules.\n/// Falls back to NeverPlayIdServices when no HttpContext is available (e.g., background jobs).\n/// </summary>\npublic class PlayIdSingletonService(IHttpContextAccessor httpContextAccessor, IHostEnvironment hostEnvironment) : IPlayIdService\n{\n    private IPlayIdService Current\n    {\n        get\n        {\n            var httpContext = httpContextAccessor.HttpContext;\n            if (httpContext == null)\n            {\n                return new NeverPlayIdServices();\n            }\n            return httpContext.RequestServices.GetRequiredService<PlayIdService>();\n        }\n    }\n\n    public string? PlayId\n    {\n        get => Current.PlayId;\n        set => Current.PlayId = value;\n    }\n\n    public bool InPlay(out string playId)\n    {\n        if (hostEnvironment.IsDevelopment())\n        {\n            return Current.InPlay(out playId);\n        }\n        else\n        {\n            playId = string.Empty;\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Play/Implementations/PlayItemService.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Services;\n\npublic class PlayItemService(IPlayIdService playIdService, IPlayItemRepository playItemRepository, ILogger<PlayItemService> logger) : IPlayItemService\n{\n    public async Task Record(User user)\n    {\n        if (playIdService.InPlay(out var playId))\n        {\n            logger.LogInformation(\"Associating user {UserId} with Play ID {PlayId}\", user.Id, playId);\n            await playItemRepository.CreateAsync(PlayItem.Create(user, playId));\n        }\n    }\n    public async Task Record(Organization organization)\n    {\n        if (playIdService.InPlay(out var playId))\n        {\n            logger.LogInformation(\"Associating organization {OrganizationId} with Play ID {PlayId}\", organization.Id, playId);\n            await playItemRepository.CreateAsync(PlayItem.Create(organization, playId));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Services/Play/README.md",
    "content": "# Play Services\n\n## Overview\n\nThe Play services provide automated testing infrastructure for tracking and cleaning up test data in development\nenvironments. A \"Play\" is a test session that groups entities (users, organizations, etc.) created during testing to\nenable bulk cleanup via the SeederAPI.\n\n## How It Works\n\n1. Test client sends `x-play-id` header with a unique Play identifier\n2. `PlayIdMiddleware` extracts the header and sets it on `IPlayIdService`\n3. Repositories check `IPlayIdService.InPlay()` when creating entities\n4. `IPlayItemService` records PlayItem entries for tracked entities\n5. SeederAPI uses PlayItem records to bulk delete all entities associated with a PlayId\n\nPlay services are **only active in Development environments**.\n\n## Classes\n\n- **`IPlayIdService`** - Interface for managing Play identifiers in the current request scope\n- **`IPlayItemService`** - Interface for tracking entities created during a Play session\n- **`PlayIdService`** - Default scoped implementation for tracking Play sessions per HTTP request\n- **`NeverPlayIdServices`** - No-op implementation used as fallback when no HttpContext is available\n- **`PlayIdSingletonService`** - Singleton wrapper that allows singleton services to access scoped PlayIdService via\n  HttpContext\n- **`PlayItemService`** - Implementation that records PlayItem entries for entities created during Play sessions\n"
  },
  {
    "path": "src/Core/Settings/GlobalSettings.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Globalization;\n\nusing Bit.Core.Auth.Settings;\n\nnamespace Bit.Core.Settings;\n\npublic class GlobalSettings : IGlobalSettings\n{\n    private string _mailTemplateDirectory;\n    private string _licenseDirectory;\n\n    public GlobalSettings()\n    {\n        BaseServiceUri = new BaseServiceUriSettings(this);\n        Attachment = new FileStorageSettings(this, \"attachments\", \"attachments\");\n        Send = new FileStorageSettings(this, \"attachments/send\", \"attachments/send\");\n        DataProtection = new DataProtectionSettings(this);\n    }\n\n    public bool SelfHosted { get; set; }\n    public bool LiteDeployment { get; set; }\n    public virtual string KnownProxies { get; set; }\n    public virtual string KnownNetworks { get; set; }\n    public virtual string SiteName { get; set; }\n    public virtual string ProjectName { get; set; }\n    public virtual string LicenseDirectory\n    {\n        get => BuildDirectory(_licenseDirectory, \"/core/licenses\");\n        set => _licenseDirectory = value;\n    }\n    public virtual string MailTemplateDirectory\n    {\n        get => BuildDirectory(_mailTemplateDirectory, \"/mail-templates\");\n        set => _mailTemplateDirectory = value;\n    }\n    public string LicenseCertificatePassword { get; set; }\n    public virtual string PushRelayBaseUri { get; set; }\n    public virtual string InternalIdentityKey { get; set; }\n    public virtual string OidcIdentityClientKey { get; set; }\n    public virtual string HibpApiKey { get; set; }\n    public virtual bool DisableUserRegistration { get; set; }\n    public virtual bool DisableEmailNewDevice { get; set; }\n    public virtual bool EnableNewDeviceVerification { get; set; }\n    public virtual bool EnableCloudCommunication { get; set; } = false;\n    public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days\n    public virtual string EventGridKey { get; set; }\n    public virtual bool TestPlayIdTrackingEnabled { get; set; } = false;\n    public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings();\n    public virtual IBaseServiceUriSettings BaseServiceUri { get; set; }\n    public virtual string DatabaseProvider { get; set; }\n    public virtual SqlSettings SqlServer { get; set; } = new SqlSettings();\n    public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings();\n    public virtual SqlSettings MySql { get; set; } = new SqlSettings();\n    public virtual SqlSettings Sqlite { get; set; } = new SqlSettings() { ConnectionString = \"Data Source=:memory:\" };\n    public virtual SlackSettings Slack { get; set; } = new SlackSettings();\n    public virtual TeamsSettings Teams { get; set; } = new TeamsSettings();\n    public virtual EventLoggingSettings EventLogging { get; set; } = new EventLoggingSettings();\n    public virtual MailSettings Mail { get; set; } = new MailSettings();\n    public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings();\n    public virtual AzureQueueEventSettings Events { get; set; } = new AzureQueueEventSettings();\n    public virtual DistributedCacheSettings DistributedCache { get; set; } = new DistributedCacheSettings();\n    public virtual NotificationsSettings Notifications { get; set; } = new NotificationsSettings();\n    public virtual IFileStorageSettings Attachment { get; set; }\n    public virtual FileStorageSettings Send { get; set; }\n    public virtual IdentityServerSettings IdentityServer { get; set; } = new IdentityServerSettings();\n    public virtual DataProtectionSettings DataProtection { get; set; }\n    public virtual NotificationHubPoolSettings NotificationHubPool { get; set; } = new();\n    public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();\n    public virtual DuoSettings Duo { get; set; } = new DuoSettings();\n    public virtual WebAuthnSettings WebAuthn { get; set; } = new WebAuthnSettings();\n    public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();\n    public virtual ImportCiphersLimitationSettings ImportCiphersLimitation { get; set; } = new ImportCiphersLimitationSettings();\n    public virtual BitPaySettings BitPay { get; set; } = new BitPaySettings();\n    public virtual AmazonSettings Amazon { get; set; } = new AmazonSettings();\n    public virtual ServiceBusSettings ServiceBus { get; set; } = new ServiceBusSettings();\n    public virtual AppleIapSettings AppleIap { get; set; } = new AppleIapSettings();\n    public virtual ISsoSettings Sso { get; set; } = new SsoSettings();\n    public virtual StripeSettings Stripe { get; set; } = new StripeSettings();\n    public virtual DistributedIpRateLimitingSettings DistributedIpRateLimiting { get; set; } =\n        new DistributedIpRateLimitingSettings();\n    public virtual IPasswordlessAuthSettings PasswordlessAuth { get; set; } = new PasswordlessAuthSettings();\n    public virtual IDomainVerificationSettings DomainVerification { get; set; } = new DomainVerificationSettings();\n    public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();\n    public virtual string DevelopmentDirectory { get; set; }\n    public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings();\n    public virtual int SendAccessTokenLifetimeInMinutes { get; set; } = 5;\n    public virtual bool EnableEmailVerification { get; set; }\n    public virtual string KdfDefaultHashKey { get; set; }\n    /// <summary>\n    /// This Hash Key is used to prevent enumeration attacks against the Send Access feature.\n    /// </summary>\n    public virtual string SendDefaultHashKey { get; set; }\n    public virtual string PricingUri { get; set; }\n    public virtual Fido2Settings Fido2 { get; set; } = new Fido2Settings();\n    public virtual ICommunicationSettings Communication { get; set; } = new CommunicationSettings();\n\n    public string BuildExternalUri(string explicitValue, string name)\n    {\n        if (!string.IsNullOrWhiteSpace(explicitValue))\n        {\n            return explicitValue;\n        }\n        if (!SelfHosted)\n        {\n            return null;\n        }\n        return string.Format(CultureInfo.InvariantCulture, \"{0}/{1}\", BaseServiceUri.Vault, name);\n    }\n\n    public string BuildInternalUri(string explicitValue, string name)\n    {\n        if (!string.IsNullOrWhiteSpace(explicitValue))\n        {\n            return explicitValue;\n        }\n        if (!SelfHosted)\n        {\n            return null;\n        }\n        return string.Format(CultureInfo.InvariantCulture, \"http://{0}:5000\", name);\n    }\n\n    public string BuildDirectory(string explicitValue, string appendedPath)\n    {\n        if (!string.IsNullOrWhiteSpace(explicitValue))\n        {\n            return explicitValue;\n        }\n        if (!SelfHosted)\n        {\n            return null;\n        }\n        return string.Concat(\"/etc/bitwarden\", appendedPath);\n    }\n\n    public class BaseServiceUriSettings : IBaseServiceUriSettings\n    {\n        private readonly GlobalSettings _globalSettings;\n\n        private string _api;\n        private string _identity;\n        private string _admin;\n        private string _notifications;\n        private string _sso;\n        private string _scim;\n        private string _fillAssistRules;\n        private string _internalApi;\n        private string _internalIdentity;\n        private string _internalAdmin;\n        private string _internalNotifications;\n        private string _internalSso;\n        private string _internalVault;\n        private string _internalScim;\n        private string _internalBilling;\n\n        public BaseServiceUriSettings(GlobalSettings globalSettings)\n        {\n            _globalSettings = globalSettings;\n        }\n\n        public string CloudRegion { get; set; }\n        public string Vault { get; set; }\n        public string VaultWithHash => $\"{Vault}/#\";\n\n        public string VaultWithHashAndSecretManagerProduct => $\"{Vault}/#/sm\";\n\n        public string Api\n        {\n            get => _globalSettings.BuildExternalUri(_api, \"api\");\n            set => _api = value;\n        }\n        public string Identity\n        {\n            get => _globalSettings.BuildExternalUri(_identity, \"identity\");\n            set => _identity = value;\n        }\n        public string Admin\n        {\n            get => _globalSettings.BuildExternalUri(_admin, \"admin\");\n            set => _admin = value;\n        }\n        public string Notifications\n        {\n            get => _globalSettings.BuildExternalUri(_notifications, \"notifications\");\n            set => _notifications = value;\n        }\n        public string Sso\n        {\n            get => _globalSettings.BuildExternalUri(_sso, \"sso\");\n            set => _sso = value;\n        }\n        public string Scim\n        {\n            get => _globalSettings.BuildExternalUri(_scim, \"scim\");\n            set => _scim = value;\n        }\n        // Simple passthrough — not derived from the Vault URL because\n        // this points to an external resource, not a Bitwarden service.\n        public string FillAssistRules\n        {\n            get => _fillAssistRules;\n            set => _fillAssistRules = value;\n        }\n\n        public string InternalNotifications\n        {\n            get => _globalSettings.BuildInternalUri(_internalNotifications, \"notifications\");\n            set => _internalNotifications = value;\n        }\n        public string InternalAdmin\n        {\n            get => _globalSettings.BuildInternalUri(_internalAdmin, \"admin\");\n            set => _internalAdmin = value;\n        }\n        public string InternalIdentity\n        {\n            get => _globalSettings.BuildInternalUri(_internalIdentity, \"identity\");\n            set => _internalIdentity = value;\n        }\n        public string InternalApi\n        {\n            get => _globalSettings.BuildInternalUri(_internalApi, \"api\");\n            set => _internalApi = value;\n        }\n        public string InternalVault\n        {\n            get => _globalSettings.BuildInternalUri(_internalVault, \"web\");\n            set => _internalVault = value;\n        }\n        public string InternalSso\n        {\n            get => _globalSettings.BuildInternalUri(_internalSso, \"sso\");\n            set => _internalSso = value;\n        }\n        public string InternalScim\n        {\n            get => _globalSettings.BuildInternalUri(_scim, \"scim\");\n            set => _internalScim = value;\n        }\n\n        public string InternalBilling\n        {\n            get => _globalSettings.BuildInternalUri(_internalBilling, \"billing\");\n            set => _internalBilling = value;\n        }\n    }\n\n    public class SqlSettings\n    {\n        private string _connectionString;\n        private string _readOnlyConnectionString;\n        private string _jobSchedulerConnectionString;\n        public bool SkipDatabasePreparation { get; set; }\n        public bool DisableDatabaseMaintenanceJobs { get; set; }\n\n        public string ConnectionString\n        {\n            get => _connectionString;\n            set\n            {\n                // On development environment, the self-hosted overrides would not override the read-only connection string, since it is already set from the non-self-hosted connection string.\n                // This causes a bug, where the read-only connection string is pointing to self-hosted database.\n                if (!string.IsNullOrWhiteSpace(_readOnlyConnectionString) &&\n                    _readOnlyConnectionString == _connectionString)\n                {\n                    _readOnlyConnectionString = null;\n                }\n\n                _connectionString = value.Trim('\"');\n            }\n        }\n\n        public string ReadOnlyConnectionString\n        {\n            get => string.IsNullOrWhiteSpace(_readOnlyConnectionString) ?\n                _connectionString : _readOnlyConnectionString;\n            set => _readOnlyConnectionString = value.Trim('\"');\n        }\n\n        public string JobSchedulerConnectionString\n        {\n            get => _jobSchedulerConnectionString;\n            set => _jobSchedulerConnectionString = value.Trim('\"');\n        }\n    }\n\n    public class SlackSettings\n    {\n        public virtual string ApiBaseUrl { get; set; } = \"https://slack.com/api\";\n        public virtual string ClientId { get; set; }\n        public virtual string ClientSecret { get; set; }\n        public virtual string Scopes { get; set; }\n    }\n\n    public class TeamsSettings\n    {\n        public virtual string LoginBaseUrl { get; set; } = \"https://login.microsoftonline.com\";\n        public virtual string GraphBaseUrl { get; set; } = \"https://graph.microsoft.com/v1.0\";\n        public virtual string ClientId { get; set; }\n        public virtual string ClientSecret { get; set; }\n        public virtual string Scopes { get; set; }\n    }\n\n    public class EventLoggingSettings\n    {\n        public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings();\n        public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings();\n        public int IntegrationCacheRefreshIntervalMinutes { get; set; } = 10;\n        public int MaxRetries { get; set; } = 3;\n\n        public class AzureServiceBusSettings\n        {\n            private string _connectionString;\n            private string _eventTopicName;\n            private string _integrationTopicName;\n\n            public virtual int DefaultMaxConcurrentCalls { get; set; } = 1;\n            public virtual int DefaultPrefetchCount { get; set; } = 0;\n\n            public virtual string EventRepositorySubscriptionName { get; set; } = \"events-write-subscription\";\n            public virtual string SlackEventSubscriptionName { get; set; } = \"events-slack-subscription\";\n            public virtual string SlackIntegrationSubscriptionName { get; set; } = \"integration-slack-subscription\";\n            public virtual string WebhookEventSubscriptionName { get; set; } = \"events-webhook-subscription\";\n            public virtual string WebhookIntegrationSubscriptionName { get; set; } = \"integration-webhook-subscription\";\n            public virtual string HecEventSubscriptionName { get; set; } = \"events-hec-subscription\";\n            public virtual string HecIntegrationSubscriptionName { get; set; } = \"integration-hec-subscription\";\n            public virtual string DatadogEventSubscriptionName { get; set; } = \"events-datadog-subscription\";\n            public virtual string DatadogIntegrationSubscriptionName { get; set; } = \"integration-datadog-subscription\";\n            public virtual string TeamsEventSubscriptionName { get; set; } = \"events-teams-subscription\";\n            public virtual string TeamsIntegrationSubscriptionName { get; set; } = \"integration-teams-subscription\";\n\n            public string ConnectionString\n            {\n                get => _connectionString;\n                set => _connectionString = value.Trim('\"');\n            }\n\n            public string EventTopicName\n            {\n                get => _eventTopicName;\n                set => _eventTopicName = value.Trim('\"');\n            }\n\n            public string IntegrationTopicName\n            {\n                get => _integrationTopicName;\n                set => _integrationTopicName = value.Trim('\"');\n            }\n        }\n\n        public class RabbitMqSettings\n        {\n            private string _hostName;\n            private string _username;\n            private string _password;\n            private string _eventExchangeName;\n            private string _integrationExchangeName;\n\n            public int RetryTiming { get; set; } = 30000; // 30s\n            public bool UseDelayPlugin { get; set; } = false;\n            public virtual string EventRepositoryQueueName { get; set; } = \"events-write-queue\";\n            public virtual string IntegrationDeadLetterQueueName { get; set; } = \"integration-dead-letter-queue\";\n            public virtual string SlackEventsQueueName { get; set; } = \"events-slack-queue\";\n            public virtual string SlackIntegrationQueueName { get; set; } = \"integration-slack-queue\";\n            public virtual string SlackIntegrationRetryQueueName { get; set; } = \"integration-slack-retry-queue\";\n            public virtual string WebhookEventsQueueName { get; set; } = \"events-webhook-queue\";\n            public virtual string WebhookIntegrationQueueName { get; set; } = \"integration-webhook-queue\";\n            public virtual string WebhookIntegrationRetryQueueName { get; set; } = \"integration-webhook-retry-queue\";\n            public virtual string HecEventsQueueName { get; set; } = \"events-hec-queue\";\n            public virtual string HecIntegrationQueueName { get; set; } = \"integration-hec-queue\";\n            public virtual string HecIntegrationRetryQueueName { get; set; } = \"integration-hec-retry-queue\";\n            public virtual string DatadogEventsQueueName { get; set; } = \"events-datadog-queue\";\n            public virtual string DatadogIntegrationQueueName { get; set; } = \"integration-datadog-queue\";\n            public virtual string DatadogIntegrationRetryQueueName { get; set; } = \"integration-datadog-retry-queue\";\n            public virtual string TeamsEventsQueueName { get; set; } = \"events-teams-queue\";\n            public virtual string TeamsIntegrationQueueName { get; set; } = \"integration-teams-queue\";\n            public virtual string TeamsIntegrationRetryQueueName { get; set; } = \"integration-teams-retry-queue\";\n\n            public string HostName\n            {\n                get => _hostName;\n                set => _hostName = value.Trim('\"');\n            }\n            public string Username\n            {\n                get => _username;\n                set => _username = value.Trim('\"');\n            }\n            public string Password\n            {\n                get => _password;\n                set => _password = value.Trim('\"');\n            }\n            public string EventExchangeName\n            {\n                get => _eventExchangeName;\n                set => _eventExchangeName = value.Trim('\"');\n            }\n            public string IntegrationExchangeName\n            {\n                get => _integrationExchangeName;\n                set => _integrationExchangeName = value.Trim('\"');\n            }\n        }\n    }\n\n    public class AzureQueueEventSettings : IConnectionStringSettings\n    {\n        private string _connectionString;\n        private string _queueName;\n\n        public string ConnectionString\n        {\n            get => _connectionString;\n            set => _connectionString = value?.Trim('\"');\n        }\n\n        public string QueueName\n        {\n            get => _queueName;\n            set => _queueName = value?.Trim('\"');\n        }\n    }\n\n    public class ConnectionStringSettings : IConnectionStringSettings\n    {\n        private string _connectionString;\n\n        public string ConnectionString\n        {\n            get => _connectionString;\n            set => _connectionString = value.Trim('\"');\n        }\n    }\n\n    public class FileStorageSettings : IFileStorageSettings\n    {\n        private readonly GlobalSettings _globalSettings;\n        private readonly string _urlName;\n        private readonly string _directoryName;\n        private string _connectionString;\n        private string _baseDirectory;\n        private string _baseUrl;\n\n        public FileStorageSettings(GlobalSettings globalSettings, string urlName, string directoryName)\n        {\n            _globalSettings = globalSettings;\n            _urlName = urlName;\n            _directoryName = directoryName;\n        }\n\n        public string ConnectionString\n        {\n            get => _connectionString;\n            set => _connectionString = value.Trim('\"');\n        }\n\n        public string BaseDirectory\n        {\n            get => _globalSettings.BuildDirectory(_baseDirectory, string.Concat(\"/core/\", _directoryName));\n            set => _baseDirectory = value;\n        }\n\n        public string BaseUrl\n        {\n            get => _globalSettings.BuildExternalUri(_baseUrl, _urlName);\n            set => _baseUrl = value;\n        }\n    }\n\n    public class MailSettings\n    {\n        private ConnectionStringSettings _connectionStringSettings;\n        public string ConnectionString\n        {\n            get => _connectionStringSettings?.ConnectionString;\n            set\n            {\n                if (_connectionStringSettings == null)\n                {\n                    _connectionStringSettings = new ConnectionStringSettings();\n                }\n                _connectionStringSettings.ConnectionString = value;\n            }\n        }\n        public string ReplyToEmail { get; set; }\n        public string AmazonConfigSetName { get; set; }\n        public SmtpSettings Smtp { get; set; } = new SmtpSettings();\n        public string SendGridApiKey { get; set; }\n        public int? SendGridPercentage { get; set; }\n        public string SendGridApiHost { get; set; } = \"https://api.sendgrid.com\";\n\n        public class SmtpSettings\n        {\n            public string Host { get; set; }\n            public int Port { get; set; } = 25;\n            public bool StartTls { get; set; } = false;\n            public bool Ssl { get; set; } = false;\n            public bool SslOverride { get; set; } = false;\n            public string Username { get; set; }\n            public string Password { get; set; }\n            public bool TrustServer { get; set; } = false;\n        }\n    }\n\n    public class IdentityServerSettings\n    {\n        public string CertificateLocation { get; set; } = \"identity.pfx\";\n        public string CertificateThumbprint { get; set; }\n        public string CertificatePassword { get; set; }\n        public string RedisConnectionString { get; set; }\n        public string CosmosConnectionString { get; set; }\n        public string LicenseKey { get; set; } = \"eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZUtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzY1MDY1NjAwLCJleHAiOjE3OTY1MTUyMDAsImNvbXBhbnlfbmFtZSI6IkJpdHdhcmRlbiBJbmMuIiwiY29udGFjdF9pbmZvIjoiY29udGFjdEBkdWVuZGVzb2Z0d2FyZS5jb20iLCJlZGl0aW9uIjoiU3RhcnRlciIsImlkIjoiOTUxNSIsImZlYXR1cmUiOlsiaXN2IiwidW5saW1pdGVkX2NsaWVudHMiXSwiY2xpZW50X2xpbWl0IjowfQ.rWUsq-XBKNwPG7BRKG-vShXHuyHLHJCh0sEWdWT4Rkz4ArIPOAepEp9wNya-hxFKkBTFlPaQ5IKk4wDTvkQkuq1qaI_v6kSCdaP9fvXp0rmh4KcFEffVLB-wAOK2S2Cld5DzdyCoskUUfwNQP7xuLsz2Ydxe_whSRIdv8bsMbvTC3Kl8PYZPZ4MxqW8rSZ_mEuCpSe5-Q40sB7aiu_7YmWLJaKrfBTIqYH-XuzQj36Aemoei0efcntej-gvxovy-5SiSEsGuRZj41rjEZYOuj5KgHihJViO1VDHK6CNtlu2Ks8bkv6G2hO-TkF16Y28ywEG_beLEf_s5dzhbDBDbvA\";\n        /// <summary>\n        /// Sliding lifetime of a refresh token in seconds.\n        ///\n        /// Each time the refresh token is used before the sliding window ends, its lifetime is extended by another SlidingRefreshTokenLifetimeSeconds.\n        ///\n        /// If AbsoluteRefreshTokenLifetimeSeconds > 0, the sliding extensions are bounded by the absolute maximum lifetime.\n        /// If SlidingRefreshTokenLifetimeSeconds = 0, sliding mode is invalid (refresh tokens cannot be used).\n        /// </summary>\n        public int? SlidingRefreshTokenLifetimeSeconds { get; set; }\n        /// <summary>\n        /// Maximum lifetime of a refresh token in seconds.\n        ///\n        /// Token cannot be refreshed by any means beyond the absolute refresh expiration.\n        ///\n        /// When setting this value to 0, the following effect applies:\n        ///     If ApplyAbsoluteExpirationOnRefreshToken is set to true, the behavior is the same as when no refresh tokens are used.\n        ///     If ApplyAbsoluteExpirationOnRefreshToken is set to false, refresh tokens only expire after the SlidingRefreshTokenLifetimeSeconds has passed.\n        /// </summary>\n        public int? AbsoluteRefreshTokenLifetimeSeconds { get; set; }\n        /// <summary>\n        /// Controls whether refresh tokens expire absolutely or on a sliding window basis.\n        ///\n        /// Absolute:\n        ///     Token expires at a fixed point in time (defined by AbsoluteRefreshTokenLifetimeSeconds). Usage does not extend lifetime.\n        ///\n        /// Sliding(default):\n        ///     Token lifetime is renewed on each use, by the amount in SlidingRefreshTokenLifetimeSeconds. Extensions stop once AbsoluteRefreshTokenLifetimeSeconds is reached (if set > 0).\n        /// </summary>\n        public bool ApplyAbsoluteExpirationOnRefreshToken { get; set; } = false;\n    }\n\n    public class DataProtectionSettings\n    {\n        private readonly GlobalSettings _globalSettings;\n\n        private string _directory;\n\n        public DataProtectionSettings(GlobalSettings globalSettings)\n        {\n            _globalSettings = globalSettings;\n        }\n\n        public string CertificateThumbprint { get; set; }\n        public string CertificatePassword { get; set; }\n        public string Directory\n        {\n            get => _globalSettings.BuildDirectory(_directory, \"/core/aspnet-dataprotection\");\n            set => _directory = value;\n        }\n    }\n\n    public class NotificationsSettings : ConnectionStringSettings\n    {\n        public string RedisConnectionString { get; set; }\n    }\n\n    public class NotificationHubSettings\n    {\n        private string _connectionString;\n\n        public string ConnectionString\n        {\n            get => _connectionString;\n            set => _connectionString = value?.Trim('\"');\n        }\n        public string HubName { get; set; }\n        /// <summary>\n        /// Enables TestSend on the Azure Notification Hub, which allows tracing of the request through the hub and to the platform-specific push notification service (PNS).\n        /// Enabling this will result in delayed responses because the Hub must wait on delivery to the PNS.  This should ONLY be enabled in a non-production environment, as results are throttled.\n        /// </summary>\n        public bool EnableSendTracing { get; set; } = false;\n        /// <summary>\n        /// The date and time at which registration will be enabled.\n        ///\n        /// **This value should not be updated once set, as it is used to determine installation location of devices.**\n        ///\n        /// If null, registration is disabled.\n        ///\n        /// </summary>\n        public DateTime? RegistrationStartDate { get; set; }\n        /// <summary>\n        /// The date and time at which registration will be disabled.\n        ///\n        /// **This value should not be updated once set, as it is used to determine installation location of devices.**\n        ///\n        /// If null, hub registration has no yet known expiry.\n        /// </summary>\n        public DateTime? RegistrationEndDate { get; set; }\n    }\n\n    public class NotificationHubPoolSettings\n    {\n        /// <summary>\n        /// List of Notification Hub settings to use for sending push notifications.\n        ///\n        /// Note that hubs on the same namespace share active device limits, so multiple namespaces should be used to increase capacity.\n        /// </summary>\n        public List<NotificationHubSettings> NotificationHubs { get; set; } = new();\n    }\n\n    public class YubicoSettings\n    {\n        public string ClientId { get; set; }\n        public string Key { get; set; }\n        public string[] ValidationUrls { get; set; }\n    }\n\n    public class DuoSettings\n    {\n        public string AKey { get; set; }\n    }\n\n    public class WebAuthnSettings\n    {\n        public int PremiumMaximumAllowedCredentials { get; set; } = 10;\n        public int NonPremiumMaximumAllowedCredentials { get; set; } = 5;\n    }\n\n    public class BraintreeSettings\n    {\n        public bool Production { get; set; }\n        public string MerchantId { get; set; }\n        public string PublicKey { get; set; }\n        public string PrivateKey { get; set; }\n    }\n\n    public class ImportCiphersLimitationSettings\n    {\n        public int CiphersLimit { get; set; }\n        public int CollectionRelationshipsLimit { get; set; }\n        public int CollectionsLimit { get; set; }\n    }\n\n    public class BitPaySettings\n    {\n        public bool Production { get; set; }\n        public string Token { get; set; }\n        public string NotificationUrl { get; set; }\n        public string WebhookKey { get; set; }\n    }\n\n    public class InstallationSettings : IInstallationSettings\n    {\n        private string _identityUri;\n        private string _apiUri;\n\n        public Guid Id { get; set; }\n        public string Key { get; set; }\n        public string IdentityUri\n        {\n            get => string.IsNullOrWhiteSpace(_identityUri) ? \"https://identity.bitwarden.com\" : _identityUri;\n            set => _identityUri = value;\n        }\n        public string ApiUri\n        {\n            get => string.IsNullOrWhiteSpace(_apiUri) ? \"https://api.bitwarden.com\" : _apiUri;\n            set => _apiUri = value;\n        }\n\n    }\n\n    public class AmazonSettings\n    {\n        public string AccessKeyId { get; set; }\n        public string AccessKeySecret { get; set; }\n        public string Region { get; set; }\n    }\n\n    public class ServiceBusSettings : ConnectionStringSettings\n    {\n        public string ApplicationCacheTopicName { get; set; }\n        public string ApplicationCacheSubscriptionName { get; set; }\n        public string WebSiteInstanceId { get; set; }\n    }\n\n    public class AppleIapSettings\n    {\n        public string Password { get; set; }\n        public bool AppInReview { get; set; }\n    }\n\n    public class SsoSettings : ISsoSettings\n    {\n        public int CacheLifetimeInSeconds { get; set; } = 60;\n        public double SsoTokenLifetimeInSeconds { get; set; } = 5;\n        public bool EnforceSsoPolicyForAllUsers { get; set; }\n    }\n\n    public class StripeSettings\n    {\n        public string ApiKey { get; set; }\n        public int MaxNetworkRetries { get; set; } = 2;\n    }\n\n    public class DistributedIpRateLimitingSettings\n    {\n        public string RedisConnectionString { get; set; }\n        public bool Enabled { get; set; } = true;\n\n        /// <summary>\n        /// Maximum number of Redis timeouts that can be experienced within the sliding timeout\n        /// window before IP rate limiting is temporarily disabled.\n        /// TODO: Determine/discuss a suitable maximum\n        /// </summary>\n        public int MaxRedisTimeoutsThreshold { get; set; } = 10;\n\n        /// <summary>\n        /// Length of the sliding window in seconds to track Redis timeout exceptions.\n        /// TODO: Determine/discuss a suitable sliding window\n        /// </summary>\n        public int SlidingWindowSeconds { get; set; } = 120;\n    }\n\n    public class PasswordlessAuthSettings : IPasswordlessAuthSettings\n    {\n        public bool KnownDevicesOnly { get; set; } = true;\n        public TimeSpan UserRequestExpiration { get; set; } = TimeSpan.FromMinutes(15);\n        public TimeSpan AdminRequestExpiration { get; set; } = TimeSpan.FromDays(7);\n        public TimeSpan AfterAdminApprovalExpiration { get; set; } = TimeSpan.FromHours(12);\n    }\n\n    public class DomainVerificationSettings : IDomainVerificationSettings\n    {\n        public int VerificationInterval { get; set; } = 12;\n        public int ExpirationPeriod { get; set; } = 7;\n    }\n\n    public class LaunchDarklySettings : ILaunchDarklySettings\n    {\n        public string SdkKey { get; set; }\n        public string FlagDataFilePath { get; set; } = \"flags.json\";\n        public Dictionary<string, string> FlagValues { get; set; } = new Dictionary<string, string>();\n    }\n\n    public class DistributedCacheSettings\n    {\n        public virtual IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings();\n        public virtual IConnectionStringSettings Cosmos { get; set; } = new ConnectionStringSettings();\n        public ExtendedCacheSettings DefaultExtendedCache { get; set; } = new ExtendedCacheSettings();\n    }\n\n    /// <summary>\n    /// A collection of Settings for customizing the FusionCache used in extended caching. Defaults are\n    /// provided for every attribute so that only specific values need to be overridden if needed.\n    /// </summary>\n    public class ExtendedCacheSettings\n    {\n        public bool EnableDistributedCache { get; set; } = true;\n        public bool UseSharedDistributedCache { get; set; } = true;\n        public IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings();\n        public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(30);\n        public bool IsFailSafeEnabled { get; set; } = true;\n        public TimeSpan FailSafeMaxDuration { get; set; } = TimeSpan.FromHours(2);\n        public TimeSpan FailSafeThrottleDuration { get; set; } = TimeSpan.FromSeconds(30);\n        public float? EagerRefreshThreshold { get; set; } = 0.9f;\n        public TimeSpan FactorySoftTimeout { get; set; } = TimeSpan.FromMilliseconds(100);\n        public TimeSpan FactoryHardTimeout { get; set; } = TimeSpan.FromMilliseconds(1500);\n        public TimeSpan DistributedCacheSoftTimeout { get; set; } = TimeSpan.FromSeconds(1);\n        public TimeSpan DistributedCacheHardTimeout { get; set; } = TimeSpan.FromSeconds(2);\n        public bool AllowBackgroundDistributedCacheOperations { get; set; } = true;\n        public TimeSpan JitterMaxDuration { get; set; } = TimeSpan.FromSeconds(2);\n        public TimeSpan DistributedCacheCircuitBreakerDuration { get; set; } = TimeSpan.FromSeconds(30);\n    }\n\n    public class WebPushSettings : IWebPushSettings\n    {\n        public string VapidPublicKey { get; set; }\n    }\n\n    public class Fido2Settings\n    {\n        public HashSet<string> Origins { get; set; }\n    }\n\n    public class CommunicationSettings : ICommunicationSettings\n    {\n        public string Bootstrap { get; set; } = \"none\";\n        public ISsoCookieVendorSettings SsoCookieVendor { get; set; } = new SsoCookieVendorSettings();\n    }\n\n    public class SsoCookieVendorSettings : ISsoCookieVendorSettings\n    {\n        public string IdpLoginUrl { get; set; }\n        public string CookieName { get; set; }\n        public string CookieDomain { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Settings/IBaseServiceUriSettings.cs",
    "content": "﻿\nnamespace Bit.Core.Settings;\n\npublic interface IBaseServiceUriSettings\n{\n    string CloudRegion { get; set; }\n    string Vault { get; set; }\n    string VaultWithHash { get; }\n    string VaultWithHashAndSecretManagerProduct { get; }\n    string Api { get; set; }\n    public string Identity { get; set; }\n    public string Admin { get; set; }\n    public string Notifications { get; set; }\n    public string Sso { get; set; }\n    public string Scim { get; set; }\n    public string FillAssistRules { get; set; }\n    public string InternalNotifications { get; set; }\n    public string InternalAdmin { get; set; }\n    public string InternalIdentity { get; set; }\n    public string InternalApi { get; set; }\n    public string InternalVault { get; set; }\n    public string InternalSso { get; set; }\n    public string InternalScim { get; set; }\n    public string InternalBilling { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Settings/ICommunicationSettings.cs",
    "content": "﻿namespace Bit.Core.Settings;\n\npublic interface ICommunicationSettings\n{\n    string Bootstrap { get; set; }\n    ISsoCookieVendorSettings SsoCookieVendor { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Settings/IConnectionStringSettings.cs",
    "content": "﻿namespace Bit.Core.Settings;\n\npublic interface IConnectionStringSettings\n{\n    string ConnectionString { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Settings/IDomainVerificationSettings.cs",
    "content": "﻿namespace Bit.Core.Settings;\n\npublic interface IDomainVerificationSettings\n{\n    public int VerificationInterval { get; set; }\n    public int ExpirationPeriod { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Settings/IFileStorageSettings.cs",
    "content": "﻿namespace Bit.Core.Settings;\n\npublic interface IFileStorageSettings\n{\n    string ConnectionString { get; set; }\n    string BaseDirectory { get; set; }\n    string BaseUrl { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Settings/IGlobalSettings.cs",
    "content": "﻿using Bit.Core.Auth.Settings;\n\nnamespace Bit.Core.Settings;\n\npublic interface IGlobalSettings\n{\n    // This interface exists for testing. Add settings here as needed for testing\n    bool SelfHosted { get; set; }\n    bool LiteDeployment { get; set; }\n    string KnownProxies { get; set; }\n    string KnownNetworks { get; set; }\n    string ProjectName { get; set; }\n    bool EnableCloudCommunication { get; set; }\n    string LicenseDirectory { get; set; }\n    string LicenseCertificatePassword { get; set; }\n    int OrganizationInviteExpirationHours { get; set; }\n    bool DisableUserRegistration { get; set; }\n    bool EnableNewDeviceVerification { get; set; }\n    IInstallationSettings Installation { get; set; }\n    IFileStorageSettings Attachment { get; set; }\n    IConnectionStringSettings Storage { get; set; }\n    IBaseServiceUriSettings BaseServiceUri { get; set; }\n    ISsoSettings Sso { get; set; }\n    IPasswordlessAuthSettings PasswordlessAuth { get; set; }\n    IDomainVerificationSettings DomainVerification { get; set; }\n    ILaunchDarklySettings LaunchDarkly { get; set; }\n    string DatabaseProvider { get; set; }\n    GlobalSettings.SqlSettings SqlServer { get; set; }\n    string DevelopmentDirectory { get; set; }\n    IWebPushSettings WebPush { get; set; }\n    GlobalSettings.EventLoggingSettings EventLogging { get; set; }\n    GlobalSettings.WebAuthnSettings WebAuthn { get; set; }\n    ICommunicationSettings Communication { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Settings/IInstallationSettings.cs",
    "content": "﻿namespace Bit.Core.Settings;\n\npublic interface IInstallationSettings\n{\n    public Guid Id { get; set; }\n    public string Key { get; set; }\n    public string IdentityUri { get; set; }\n    public string ApiUri { get; }\n}\n"
  },
  {
    "path": "src/Core/Settings/ILaunchDarklySettings.cs",
    "content": "﻿namespace Bit.Core.Settings;\n\npublic interface ILaunchDarklySettings\n{\n    public string SdkKey { get; set; }\n    public string FlagDataFilePath { get; set; }\n    public Dictionary<string, string> FlagValues { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Settings/ISsoCookieVendorSettings.cs",
    "content": "﻿namespace Bit.Core.Settings;\n\npublic interface ISsoCookieVendorSettings\n{\n    string IdpLoginUrl { get; set; }\n    string CookieName { get; set; }\n    string CookieDomain { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Settings/IWebPushSettings.cs",
    "content": "﻿namespace Bit.Core.Settings;\n\npublic interface IWebPushSettings\n{\n    public string VapidPublicKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Tokens/BadTokenException.cs",
    "content": "﻿namespace Bit.Core.Tokens;\n\npublic class BadTokenException : Exception\n{\n    public BadTokenException()\n    {\n    }\n\n    public BadTokenException(string message) : base(message)\n    {\n    }\n}\n"
  },
  {
    "path": "src/Core/Tokens/DataProtectorTokenFactory.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Tokens;\n\npublic class DataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where T : Tokenable\n{\n    private readonly IDataProtector _dataProtector;\n    private readonly string _clearTextPrefix;\n    private readonly ILogger<DataProtectorTokenFactory<T>> _logger;\n\n    public DataProtectorTokenFactory(string clearTextPrefix, string purpose, IDataProtectionProvider dataProtectionProvider, ILogger<DataProtectorTokenFactory<T>> logger)\n    {\n        _dataProtector = dataProtectionProvider.CreateProtector(purpose);\n        _clearTextPrefix = clearTextPrefix;\n        _logger = logger;\n    }\n\n    public string Protect(T data) =>\n        data.ToToken().ProtectWith(_dataProtector, _logger).WithPrefix(_clearTextPrefix).ToString();\n\n    /// <summary>\n    /// Unprotect token\n    /// </summary>\n    /// <param name=\"token\">The token to parse</param>\n    /// <returns>The parsed tokenable</returns>\n    /// <exception>Throws CryptographicException if fails to unprotect</exception>\n    public T Unprotect(string token) =>\n        Tokenable.FromToken<T>(new Token(token).RemovePrefix(_clearTextPrefix).UnprotectWith(_dataProtector, _logger).ToString());\n\n    public bool TokenValid(string token)\n    {\n        try\n        {\n            return Unprotect(token).Valid;\n        }\n        catch\n        {\n            return false;\n        }\n    }\n\n    public bool TryUnprotect(string token, out T data)\n    {\n        try\n        {\n            data = Unprotect(token);\n            return true;\n        }\n        catch (Exception ex)\n        {\n            _logger.LogInformation(ex, \"Failed to unprotect token: {rawToken}\", token);\n            data = default;\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Tokens/ExpiringTokenable.cs",
    "content": "﻿using System.Text.Json.Serialization;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Tokens;\n\npublic abstract class ExpiringTokenable : Tokenable\n{\n    [JsonConverter(typeof(EpochDateTimeJsonConverter))]\n    public DateTime ExpirationDate { get; set; }\n\n    /// <summary>\n    /// Checks if the token is still within its valid duration and if its data is valid.\n    /// <para>For data validation, this property relies on the <see cref=\"TokenIsValid\"/> method.</para>\n    /// </summary>\n    public override bool Valid => ExpirationDate > DateTime.UtcNow && TokenIsValid();\n\n    /// <summary>\n    /// Validates that the token data properties are correct.\n    /// <para>For expiration checks, refer to the <see cref=\"Valid\"/> property.</para>\n    /// </summary>\n    protected abstract bool TokenIsValid();\n}\n"
  },
  {
    "path": "src/Core/Tokens/IBillingSyncTokenable.cs",
    "content": "﻿namespace Bit.Core.Tokens;\n\npublic interface IBillingSyncTokenable\n{\n    public Guid OrganizationId { get; set; }\n    public string BillingSyncKey { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Tokens/IDataProtectorTokenFactory.cs",
    "content": "﻿namespace Bit.Core.Tokens;\n\npublic interface IDataProtectorTokenFactory<T> where T : Tokenable\n{\n    string Protect(T data);\n    T Unprotect(string token);\n    bool TryUnprotect(string token, out T data);\n    bool TokenValid(string token);\n}\n"
  },
  {
    "path": "src/Core/Tokens/Token.cs",
    "content": "﻿using Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Tokens;\n\npublic class Token\n{\n    private readonly string _token;\n\n    public Token(string token)\n    {\n        _token = token;\n    }\n\n    public Token WithPrefix(string prefix)\n    {\n        return new Token($\"{prefix}{_token}\");\n    }\n\n    public Token RemovePrefix(string expectedPrefix)\n    {\n        if (!_token.StartsWith(expectedPrefix))\n        {\n            throw new BadTokenException($\"Expected prefix, {expectedPrefix}, was not present.\");\n        }\n\n        return new Token(_token[expectedPrefix.Length..]);\n    }\n\n\n    public Token ProtectWith(IDataProtector dataProtector, ILogger logger)\n    {\n        logger.LogDebug(\"Protecting token: {token}\", this);\n        return new(dataProtector.Protect(ToString()));\n    }\n\n    public Token UnprotectWith(IDataProtector dataProtector, ILogger logger)\n    {\n        var unprotected = \"\";\n        try\n        {\n            unprotected = dataProtector.Unprotect(ToString());\n        }\n        catch (Exception e)\n        {\n            logger.LogInformation(e, \"Failed to unprotect token: {token}\", this);\n            throw;\n        }\n        logger.LogDebug(\"Unprotected token: {token} to {decryptedToken}\", this, unprotected);\n        return new(unprotected);\n    }\n\n    public override string ToString() => _token;\n}\n"
  },
  {
    "path": "src/Core/Tokens/Tokenable.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\n\nnamespace Bit.Core.Tokens;\n\npublic abstract class Tokenable\n{\n    public abstract bool Valid { get; }\n\n    public Token ToToken()\n    {\n        return new Token(JsonSerializer.Serialize(this, this.GetType()));\n    }\n\n    public static T FromToken<T>(string token) => FromToken<T>(new Token(token));\n    public static T FromToken<T>(Token token)\n    {\n        return JsonSerializer.Deserialize<T>(token.ToString());\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/Entities/Send.cs",
    "content": "﻿#nullable enable\n\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Tools.Entities;\n\n/// <summary>\n/// An end-to-end encrypted secret accessible to arbitrary\n/// entities through a fixed URI.\n/// </summary>\npublic class Send : ITableObject<Guid>\n{\n    /// <summary>\n    /// Uniquely identifies this send.\n    /// </summary>\n    public Guid Id { get; set; }\n\n    /// <summary>\n    /// Identifies the user that created this send.\n    /// </summary>\n    public Guid? UserId { get; set; }\n\n    /// <summary>\n    /// Identifies the organization that created this send.\n    /// </summary>\n    /// <remarks>\n    /// Not presently in-use by client applications.\n    /// </remarks>\n    public Guid? OrganizationId { get; set; }\n\n    /// <summary>\n    /// Describes the data being sent. This field determines how\n    /// the <see cref=\"Data\"/> field is interpreted.\n    /// </summary>\n    public SendType Type { get; set; }\n\n    /// <summary>\n    /// Specifies the authentication method required to access this Send.\n    /// </summary>\n    /// <seealso cref=\"Tools.Enums.AuthType\"/>\n    public AuthType? AuthType { get; set; }\n\n    /// <summary>\n    /// Stores data containing or pointing to the transmitted secret. JSON.\n    /// </summary>\n    /// <note>\n    /// Must be nullable due to several database column configuration.\n    /// The application and all other databases assume this is not nullable.\n    /// Tech debt ticket: PM-4128\n    /// </note>\n    public string? Data { get; set; }\n\n    /// <summary>\n    /// Stores the data's encryption key. Encrypted.\n    /// </summary>\n    /// <note>\n    /// Must be nullable due to MySql database column configuration.\n    /// The application and all other databases assume this is not nullable.\n    /// Tech debt ticket: PM-4128\n    /// </note>\n    public string? Key { get; set; }\n\n    /// <summary>\n    /// Password provided by the user. Protected with pbkdf2.\n    /// </summary>\n    /// <remarks>\n    /// This field is mutually exclusive with <see cref=\"Emails\" />\n    /// </remarks>\n    [MaxLength(300)]\n    public string? Password { get; set; }\n\n    /// <summary>\n    /// Comma-separated list of emails for OTP authentication.\n    /// </summary>\n    /// <remarks>\n    /// This field is mutually exclusive with <see cref=\"Password\" />\n    /// </remarks>\n    [MaxLength(4000)]\n    public string? Emails { get; set; }\n\n    /// <summary>\n    /// The send becomes unavailable to API callers when\n    /// <see cref=\"AccessCount\"/>  &gt;= <see cref=\"MaxAccessCount\"/>.\n    /// </summary>\n    public int? MaxAccessCount { get; set; }\n\n    /// <summary>\n    /// Number of times the content was accessed.\n    /// </summary>\n    /// <remarks>\n    /// This value is owned by the server. Clients cannot alter it.\n    /// </remarks>\n    public int AccessCount { get; set; }\n\n    /// <summary>\n    /// The date this send was created.\n    /// </summary>\n    public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;\n\n    /// <summary>\n    /// The date this send was last modified.\n    /// </summary>\n    public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;\n\n    /// <summary>\n    /// The date this send becomes unavailable to API callers.\n    /// </summary>\n    public DateTime? ExpirationDate { get; set; }\n\n    /// <summary>\n    /// The date this send will be unconditionally deleted.\n    /// </summary>\n    /// <remarks>\n    /// This is set by server-side when the user doesn't specify a deletion date.\n    /// </remarks>\n    public DateTime DeletionDate { get; set; }\n\n    /// <summary>\n    /// When this is true the send is not available to API callers,\n    /// unless they're the creator.\n    /// </summary>\n    public bool Disabled { get; set; }\n\n    /// <summary>\n    /// Whether the creator's email address should be shown to the recipient.\n    /// </summary>\n    /// <value>\n    /// <see langword=\"false\"/> indicates the email may be shown.\n    /// <see langword=\"true\"/> indicates the email should be hidden.\n    /// <see langword=\"null\"/> indicates the client doesn't set the field and\n    /// the email should be hidden.\n    /// </value>\n    public bool? HideEmail { get; set; }\n\n    /// <summary>\n    /// Identifies the Cipher associated with this send.\n    /// </summary>\n    public Guid? CipherId { get; set; }\n\n    /// <summary>\n    /// Generates the send's <see cref=\"Id\" />\n    /// </summary>\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/Enums/AuthType.cs",
    "content": "﻿namespace Bit.Core.Tools.Enums;\n\n/// <summary>\n/// Specifies the authentication method required to access a Send.\n/// </summary>\npublic enum AuthType : byte\n{\n    /// <summary>\n    /// Email-based OTP authentication\n    /// </summary>\n    Email = 0,\n\n    /// <summary>\n    /// Password-based authentication\n    /// </summary>\n    Password = 1,\n\n    /// <summary>\n    /// No authentication required\n    /// </summary>\n    None = 2\n}\n"
  },
  {
    "path": "src/Core/Tools/Enums/SendType.cs",
    "content": "﻿namespace Bit.Core.Tools.Enums;\n\npublic enum SendType : byte\n{\n    Text = 0,\n    File = 1\n}\n"
  },
  {
    "path": "src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.ImportFeatures.Interfaces;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\n\nnamespace Bit.Core.Tools.ImportFeatures;\n\npublic class ImportCiphersCommand : IImportCiphersCommand\n{\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IFolderRepository _folderRepository;\n    private readonly IPushNotificationService _pushService;\n    private readonly IPolicyService _policyService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IFeatureService _featureService;\n\n    public ImportCiphersCommand(\n        ICipherRepository cipherRepository,\n        IFolderRepository folderRepository,\n        ICollectionRepository collectionRepository,\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        IPushNotificationService pushService,\n        IPolicyService policyService,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IFeatureService featureService)\n    {\n        _cipherRepository = cipherRepository;\n        _folderRepository = folderRepository;\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _collectionRepository = collectionRepository;\n        _pushService = pushService;\n        _policyService = policyService;\n        _policyRequirementQuery = policyRequirementQuery;\n        _featureService = featureService;\n    }\n\n    public async Task ImportIntoIndividualVaultAsync(\n        List<Folder> folders,\n        List<CipherDetails> ciphers,\n        IEnumerable<KeyValuePair<int, int>> folderRelationships,\n        Guid importingUserId)\n    {\n        // Make sure the user can save new ciphers to their personal vault\n        var organizationDataOwnershipEnabled = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            ? (await _policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(importingUserId)).State == OrganizationDataOwnershipState.Enabled\n            : await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership);\n\n        if (organizationDataOwnershipEnabled)\n        {\n            throw new BadRequestException(\"You cannot import items into your personal vault because you are \" +\n                \"a member of an organization which forbids it.\");\n        }\n\n        foreach (var cipher in ciphers)\n        {\n            cipher.SetNewId();\n\n            if (cipher.UserId.HasValue && cipher.Favorite)\n            {\n                cipher.Favorites = $\"{{\\\"{cipher.UserId.ToString().ToUpperInvariant()}\\\":true}}\";\n            }\n\n            if (cipher.UserId.HasValue && cipher.ArchivedDate.HasValue)\n            {\n                cipher.Archives = $\"{{\\\"{cipher.UserId.Value.ToString().ToUpperInvariant()}\\\":\\\"\" +\n                                  $\"{cipher.ArchivedDate.Value:yyyy-MM-ddTHH:mm:ss.fffffffZ}\\\"}}\";\n            }\n        }\n\n        var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(importingUserId)).Select(f => f.Id).ToList();\n\n        //Assign id to the ones that don't exist in DB\n        //Need to keep the list order to create the relationships\n        List<Folder> newFolders = new List<Folder>();\n        foreach (var folder in folders)\n        {\n            if (!userfoldersIds.Contains(folder.Id))\n            {\n                folder.SetNewId();\n                newFolders.Add(folder);\n            }\n        }\n\n        // Create the folder associations based on the newly created folder ids\n        foreach (var relationship in folderRelationships)\n        {\n            var cipher = ciphers.ElementAtOrDefault(relationship.Key);\n            var folder = folders.ElementAtOrDefault(relationship.Value);\n\n            if (cipher == null || folder == null)\n            {\n                continue;\n            }\n\n            cipher.Folders = $\"{{\\\"{cipher.UserId.ToString().ToUpperInvariant()}\\\":\" +\n                $\"\\\"{folder.Id.ToString().ToUpperInvariant()}\\\"}}\";\n        }\n\n        // Create it all\n        await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders);\n\n        // push\n        await _pushService.PushSyncVaultAsync(importingUserId);\n    }\n\n    public async Task ImportIntoOrganizationalVaultAsync(\n        List<Collection> collections,\n        List<CipherDetails> ciphers,\n        IEnumerable<KeyValuePair<int, int>> collectionRelationships,\n        Guid importingUserId)\n    {\n        var org = collections.Count > 0 ?\n            await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :\n            await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value);\n        var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId);\n\n        if (collections.Count > 0 && org != null && org.MaxCollections.HasValue)\n        {\n            var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(org.Id);\n            if (org.MaxCollections.Value < (collectionCount + collections.Count))\n            {\n                throw new BadRequestException(\"This organization can only have a maximum of \" +\n                    $\"{org.MaxCollections.Value} collections.\");\n            }\n        }\n\n        foreach (var cipher in ciphers)\n        {\n            // Init. ids for ciphers\n            cipher.SetNewId();\n\n            if (cipher.ArchivedDate.HasValue)\n            {\n                cipher.Archives = $\"{{\\\"{importingUserId.ToString().ToUpperInvariant()}\\\":\\\"\" +\n                                  $\"{cipher.ArchivedDate.Value:yyyy-MM-ddTHH:mm:ss.fffffffZ}\\\"}}\";\n            }\n        }\n\n        var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();\n\n        //Assign id to the ones that don't exist in DB\n        //Need to keep the list order to create the relationships\n        var newCollections = new List<Collection>();\n        var newCollectionUsers = new List<CollectionUser>();\n\n        foreach (var collection in collections)\n        {\n            // If the collection already exists, skip it\n            if (organizationCollectionsIds.Contains(collection.Id))\n            {\n                continue;\n            }\n\n            // Create new collections if not already present\n            collection.SetNewId();\n            newCollections.Add(collection);\n\n            /*\n             * If the organization was created by a Provider, the organization may have zero members (users)\n             * In this situation importingOrgUser will be null, and accessing importingOrgUser.Id will\n             * result in a null reference exception.\n             *\n             * Avoid user assignment, but proceed with adding the collection.\n             */\n            if (importingOrgUser == null)\n            {\n                continue;\n            }\n\n            newCollectionUsers.Add(new CollectionUser\n            {\n                CollectionId = collection.Id,\n                OrganizationUserId = importingOrgUser.Id,\n                Manage = true\n            });\n        }\n\n        // Create associations based on the newly assigned ids\n        var collectionCiphers = new List<CollectionCipher>();\n        foreach (var relationship in collectionRelationships)\n        {\n            var cipher = ciphers.ElementAtOrDefault(relationship.Key);\n            var collection = collections.ElementAtOrDefault(relationship.Value);\n\n            if (cipher == null || collection == null)\n            {\n                continue;\n            }\n\n            collectionCiphers.Add(new CollectionCipher\n            {\n                CipherId = cipher.Id,\n                CollectionId = collection.Id\n            });\n        }\n\n        // Create it all\n        await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);\n\n        // push\n        await _pushService.PushSyncVaultAsync(importingUserId);\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/ImportFeatures/ImportServiceCollectionExtension.cs",
    "content": "﻿using Bit.Core.Tools.ImportFeatures.Interfaces;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Tools.ImportFeatures;\n\npublic static class ImportServiceCollectionExtension\n{\n    public static void AddImportServices(this IServiceCollection services)\n    {\n        services.AddScoped<IImportCiphersCommand, ImportCiphersCommand>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Tools.ImportFeatures.Interfaces;\n\npublic interface IImportCiphersCommand\n{\n    Task ImportIntoIndividualVaultAsync(List<Folder> folders, List<CipherDetails> ciphers,\n        IEnumerable<KeyValuePair<int, int>> folderRelationships, Guid importingUserId);\n\n    Task ImportIntoOrganizationalVaultAsync(List<Collection> collections, List<CipherDetails> ciphers,\n        IEnumerable<KeyValuePair<int, int>> collectionRelationships, Guid importingUserId);\n}\n"
  },
  {
    "path": "src/Core/Tools/Models/Data/SendAccessResult.cs",
    "content": "﻿using Bit.Core.Tools.Entities;\n\nnamespace Bit.Core.Tools.Models.Data;\n\n/// <summary>\n/// This enum represents the possible results when attempting to access a <see cref=\"Send\"/>.\n/// </summary>\n/// <member>name=\"Granted\">Access is granted for the <see cref=\"Send\"/>.</member>\n/// <member>name=\"PasswordRequired\">Access is denied, but a password is required to access the <see cref=\"Send\"/>.\n/// </member>\n/// <member>name=\"PasswordInvalid\">Access is denied due to an invalid password.</member>\n/// <member>name=\"Denied\">Access is denied for the <see cref=\"Send\"/>.</member>\npublic enum SendAccessResult\n{\n    Granted,\n    PasswordRequired,\n    PasswordInvalid,\n    Denied\n}\n"
  },
  {
    "path": "src/Core/Tools/Models/Data/SendAuthenticationTypes.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Core.Tools.Models.Data;\n\n/// <summary>\n/// A discriminated union for send authentication.\n/// </summary>\n/// <example>\n/// const method : SendAuthenticationMethod;\n/// // other variable definitions omitted\n///\n/// var token = method switch\n/// {\n///     NotAuthenticated => issueTokenFor(sendId),\n///     ResourcePassword(var expected) => tryIssueTokenFor(sendId, expected, actual),\n///     EmailOtp(_) => tryIssueTokenFor(sendId, email, actualOtp),\n///     _ => throw new Exception()\n/// };\n/// </example>\npublic abstract record SendAuthenticationMethod;\n\n/// <summary>\n/// Never issue a send claim.\n/// </summary>\n/// <remarks>\n/// This claim is issued when a send does not exist or when a send\n/// has exceeded its max access attempts.\n/// </remarks>\npublic record NeverAuthenticate : SendAuthenticationMethod;\n\n/// <summary>\n/// Create a send claim automatically.\n/// </summary>\npublic record NotAuthenticated : SendAuthenticationMethod;\n\n/// <summary>\n/// Create a send claim by requesting a password confirmation hash.\n/// </summary>\n/// <param name=\"Hash\">\n/// A base64 encoded hash that permits access to the send.\n/// </param>\npublic record ResourcePassword(string Hash) : SendAuthenticationMethod;\n\n/// <summary>\n/// Create a send claim by requesting a one time password (OTP) confirmation code.\n/// </summary>\n/// <param name=\"emails\">\n/// The list of email addresses permitted access to the send.\n/// </param>\npublic record EmailOtp(string[] emails) : SendAuthenticationMethod;\n\n/// <summary>\n/// The send exists but cannot be accessed (expired, disabled, max access exceeded, or past deletion date).\n/// </summary>\npublic record SendInaccessible : SendAuthenticationMethod;\n"
  },
  {
    "path": "src/Core/Tools/Models/Data/SendData.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Core.Tools.Models.Data;\n\n/// <summary>\n/// Shared data for a send\n/// </summary>\npublic abstract class SendData\n{\n    /// <summary>\n    /// Instantiates a <see cref=\"SendData\"/>.\n    /// </summary>\n    public SendData() { }\n\n    /// <inheritdoc cref=\"SendData()\" />\n    /// <param name=\"name\">User-provided name of the send.</param>\n    /// <param name=\"notes\">User-provided private notes of the send.</param>\n    public SendData(string name, string? notes)\n    {\n        Name = name;\n        Notes = notes;\n    }\n\n    /// <summary>\n    /// User-provided name of the send.\n    /// </summary>\n    public string Name { get; set; } = string.Empty;\n\n    /// <summary>\n    /// User-provided private notes of the send.\n    /// </summary>\n    public string? Notes { get; set; } = null;\n}\n"
  },
  {
    "path": "src/Core/Tools/Models/Data/SendFileData.cs",
    "content": "﻿#nullable enable\n\nusing System.Diagnostics.CodeAnalysis;\nusing System.Text.Json.Serialization;\nusing static System.Text.Json.Serialization.JsonNumberHandling;\n\nnamespace Bit.Core.Tools.Models.Data;\n\n/// <summary>\n/// A file secret being sent.\n/// </summary>\npublic class SendFileData : SendData\n{\n    /// <summary>\n    /// Instantiates a <see cref=\"SendFileData\"/>.\n    /// </summary>\n    public SendFileData() { }\n\n    /// <inheritdoc cref=\"SendFileData()\"/>\n    /// <param name=\"name\">Attached file name.</param>\n    /// <param name=\"notes\">User-provided private notes of the send.</param>\n    /// <param name=\"fileName\">Attached file name.</param>\n    public SendFileData(string name, string? notes, string fileName)\n        : base(name, notes)\n    {\n        FileName = fileName;\n    }\n\n    /// <summary>\n    /// Size of the attached file in bytes.\n    /// </summary>\n    /// <remarks>\n    /// Serialized as a string since JSON (or Javascript)  doesn't support\n    /// full precision for long numbers\n    /// </remarks>\n    [JsonNumberHandling(WriteAsString | AllowReadingFromString)]\n    public long Size { get; set; }\n\n    /// <summary>\n    /// Uniquely identifies an uploaded file.\n    /// </summary>\n    /// <value>\n    /// Should contain <see langword=\"null\" /> only when a file\n    /// upload is pending. Should never contain null once the\n    /// file upload completes.\n    /// </value>\n    [DisallowNull]\n    public string? Id { get; set; }\n\n    /// <summary>\n    /// Attached file name.\n    /// </summary>\n    /// <value>\n    /// Should contain a non-empty string once the file upload completes.\n    /// </value>\n    public string FileName { get; set; } = string.Empty;\n\n    /// <summary>\n    /// When true the uploaded file's length was confirmed within\n    /// the expected tolerance and below the maximum supported\n    /// file size.\n    /// </summary>\n    public bool Validated { get; set; } = true;\n}\n"
  },
  {
    "path": "src/Core/Tools/Models/Data/SendTextData.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Core.Tools.Models.Data;\n\n/// <summary>\n/// A text secret being sent.\n/// </summary>\npublic class SendTextData : SendData\n{\n    /// <summary>\n    /// Instantiates a <see cref=\"SendTextData\"/>.\n    /// </summary>\n    public SendTextData() { }\n\n    /// <inheritdoc cref=\"SendTextData()\"/>\n    /// <param name=\"name\">Attached file name.</param>\n    /// <param name=\"notes\">User-provided private notes of the send.</param>\n    /// <param name=\"text\">The secret being sent.</param>\n    /// <param name=\"hidden\">\n    /// Indicates whether the secret should be concealed when opening the send.\n    /// </param>\n    public SendTextData(string name, string? notes, string? text, bool hidden)\n        : base(name, notes)\n    {\n        Text = text;\n        Hidden = hidden;\n    }\n\n    /// <summary>\n    /// The secret being sent.\n    /// </summary>\n    public string? Text { get; set; }\n\n    /// <summary>\n    /// Indicates whether the secret should be concealed when opening the send.\n    /// </summary>\n    /// <value>\n    /// <see langword=\"true\" /> when the secret should be concealed.\n    /// Otherwise <see langword=\"false\" />.\n    /// </value>\n    public bool Hidden { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Tools/Repositories/ISendRepository.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\nusing Bit.Core.Tools.Entities;\n\nnamespace Bit.Core.Tools.Repositories;\n\n/// <summary>\n/// Service for saving and loading <see cref=\"Send\"/>s in persistent storage.\n/// </summary>\npublic interface ISendRepository : IRepository<Send, Guid>\n{\n    /// <summary>\n    /// Loads all <see cref=\"Send\"/>s created by a user.\n    /// </summary>\n    /// <param name=\"userId\">\n    /// Identifies the user.\n    /// </param>\n    /// <returns>\n    /// A task that completes once the <see cref=\"Send\"/>s have been loaded.\n    /// The task's result contains the loaded <see cref=\"Send\"/>s.\n    /// </returns>\n    Task<ICollection<Send>> GetManyByUserIdAsync(Guid userId);\n\n    /// <summary>\n    /// Loads <see cref=\"Send\"/>s scheduled for deletion.\n    /// </summary>\n    /// <param name=\"deletionDateBefore\">\n    /// Load sends whose <see cref=\"Send.DeletionDate\" /> is &lt; this date.\n    /// </param>\n    /// <returns>\n    /// A task that completes once the <see cref=\"Send\"/>s have been loaded.\n    /// The task's result contains the loaded <see cref=\"Send\"/>s.\n    /// </returns>\n    Task<ICollection<Send>> GetManyByDeletionDateAsync(DateTime deletionDateBefore);\n\n    /// <summary>\n    /// Updates encrypted data for sends during a key rotation\n    /// </summary>\n    /// <param name=\"userId\">The user that initiated the key rotation</param>\n    /// <param name=\"sends\">A list of sends with updated data</param>\n    UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,\n        IEnumerable<Send> sends);\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Commands.Interfaces;\nusing Bit.Core.Tools.Services;\n\nnamespace Bit.Core.Tools.SendFeatures.Commands;\n\npublic class AnonymousSendCommand : IAnonymousSendCommand\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly ISendFileStorageService _sendFileStorageService;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly ISendAuthorizationService _sendAuthorizationService;\n\n    public AnonymousSendCommand(\n        ISendRepository sendRepository,\n        ISendFileStorageService sendFileStorageService,\n        IPushNotificationService pushNotificationService,\n        ISendAuthorizationService sendAuthorizationService\n        )\n    {\n        _sendRepository = sendRepository;\n        _sendFileStorageService = sendFileStorageService;\n        _pushNotificationService = pushNotificationService;\n        _sendAuthorizationService = sendAuthorizationService;\n    }\n\n    // Response: Send, password required, password invalid\n    public async Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password)\n    {\n        if (send.Type != SendType.File)\n        {\n            throw new BadRequestException(\"Can only get a download URL for a file type of Send\");\n        }\n\n        var result = _sendAuthorizationService.SendCanBeAccessed(send, password);\n\n        if (!result.Equals(SendAccessResult.Granted))\n        {\n            return (null, result);\n        }\n\n        send.AccessCount++;\n        await _sendRepository.ReplaceAsync(send);\n        await _pushNotificationService.PushSyncSendUpdateAsync(send);\n        return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), result);\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs",
    "content": "﻿using Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Models.Data;\n\nnamespace Bit.Core.Tools.SendFeatures.Commands.Interfaces;\n\n/// <summary>\n/// AnonymousSendCommand interface provides methods for managing anonymous Sends.\n/// </summary>\npublic interface IAnonymousSendCommand\n{\n    /// <summary>\n    /// Gets the Send file download URL for a Send object.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> used to help get file download url and validate file</param>\n    /// <param name=\"fileId\">FileId get file download url</param>\n    /// <param name=\"password\">A hashed and base64-encoded password. This is compared with the send's password to authorize access.</param>\n    /// <returns>Async Task object with Tuple containing the string of download url and <see cref=\"SendAccessResult\" />\n    /// to determine if the user can access send.\n    /// </returns>\n    Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password);\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs",
    "content": "﻿using Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Models.Data;\n\nnamespace Bit.Core.Tools.SendFeatures.Commands.Interfaces;\n\n/// <summary>\n/// NonAnonymousSendCommand interface provides methods for managing non-anonymous Sends.\n/// </summary>\npublic interface INonAnonymousSendCommand\n{\n    /// <summary>\n    /// Saves a <see cref=\"Send\" /> to the database.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> that will save to database</param>\n    /// <returns>Task completes as <see cref=\"Send\" /> saves to the database</returns>\n    Task SaveSendAsync(Send send);\n\n    /// <summary>\n    /// Saves the <see cref=\"Send\" /> and <see cref=\"SendFileData\" /> to the database.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> that will save to the database</param>\n    /// <param name=\"data\"><see cref=\"SendFileData\" /> that will save to file storage</param>\n    /// <param name=\"fileLength\">Length of file help with saving to file storage</param>\n    /// <returns>Task object for async operations with file upload url</returns>\n    Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength);\n\n    /// <summary>\n    /// Upload a file to an existing <see cref=\"Send\" />.\n    /// </summary>\n    /// <param name=\"stream\"><see cref=\"Stream\" /> of file to be uploaded. The <see cref=\"Stream\" /> position\n    /// will be set to 0 before uploading the file.</param>\n    /// <param name=\"send\"><see cref=\"Send\" /> used to help with uploading file</param>\n    /// <returns>Task completes after saving <see cref=\"Stream\" /> and <see cref=\"Send\" /> metadata to the file storage</returns>\n    Task UploadFileToExistingSendAsync(Stream stream, Send send);\n\n    /// <summary>\n    /// Deletes a <see cref=\"Send\" /> from the database and file storage.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> is used to delete from database and file storage</param>\n    /// <returns>Task completes once <see cref=\"Send\" /> has been deleted from database and file storage.</returns>\n    Task DeleteSendAsync(Send send);\n\n    /// <summary>\n    /// Stores the confirmed file size of a send; when the file size cannot be confirmed, the send is deleted.\n    /// </summary>\n    /// <param name=\"send\">The <see cref=\"Send\" /> this command acts upon</param>\n    /// <returns><see langword=\"true\" /> when the file is confirmed, otherwise <see langword=\"false\" /></returns>\n    /// <remarks>\n    /// When a file size cannot be confirmed, we assume we're working with a rogue client. The send is deleted out of\n    /// an abundance of caution.\n    /// </remarks>\n    Task<bool> ConfirmFileSize(Send send);\n\n    /// <summary>\n    /// If a File type Send can be downloaded, retrieves the download URL.\n    /// </summary>\n    /// <param name=\"send\">The <see cref=\"Send\" /> this command acts upon</param>\n    /// <param name=\"fileId\">The fileId to be downloaded</param>\n    /// <returns>\n    /// A tuple wrapping the download URL string and <see cref=\"SendAccessResult\" /> indicating whether access was granted\n    /// </returns>\n    /// <remarks>\n    /// This method is intended for authenticated endpoints where authentication has already been validated.\n    /// Returns <see cref=\"SendAccessResult.Denied\" /> when the Send is disabled, MaxAccessCount has been reached,\n    /// expiration date has passed, or deletion date has been reached.\n    /// </remarks>\n    Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId);\n\n    /// <summary>\n    /// Determines whether a <see cref=\"Send\" /> can be accessed based on its current state.\n    /// </summary>\n    /// <param name=\"send\">The <see cref=\"Send\" /> to evaluate for access</param>\n    /// <returns><see langword=\"true\" /> if the Send can be accessed, otherwise <see langword=\"false\" /></returns>\n    /// <remarks>\n    /// This method checks if the Send is disabled, if MaxAccessCount has been reached,\n    /// if the expiration date has passed, or if the deletion date has been reached.\n    /// </remarks>\n    static bool SendCanBeAccessed(Send send)\n    {\n        var now = DateTime.UtcNow;\n        if (send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount ||\n            send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < now ||\n            send.Disabled ||\n            send.DeletionDate <= now)\n        {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Commands.Interfaces;\nusing Bit.Core.Tools.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Tools.SendFeatures.Commands;\n\npublic class NonAnonymousSendCommand : INonAnonymousSendCommand\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly ISendFileStorageService _sendFileStorageService;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly ISendValidationService _sendValidationService;\n    private readonly ISendCoreHelperService _sendCoreHelperService;\n    private readonly ILogger<NonAnonymousSendCommand> _logger;\n\n    public NonAnonymousSendCommand(ISendRepository sendRepository,\n        ISendFileStorageService sendFileStorageService,\n        IPushNotificationService pushNotificationService,\n        ISendValidationService sendValidationService,\n        ISendCoreHelperService sendCoreHelperService,\n        ILogger<NonAnonymousSendCommand> logger)\n    {\n        _sendRepository = sendRepository;\n        _sendFileStorageService = sendFileStorageService;\n        _pushNotificationService = pushNotificationService;\n        _sendValidationService = sendValidationService;\n        _sendCoreHelperService = sendCoreHelperService;\n        _logger = logger;\n    }\n\n    public async Task SaveSendAsync(Send send)\n    {\n        // Make sure user can save Sends\n        await _sendValidationService.ValidateUserCanSaveAsync(send.UserId, send);\n\n        if (send.Id == default(Guid))\n        {\n            await _sendRepository.CreateAsync(send);\n            await _pushNotificationService.PushSyncSendCreateAsync(send);\n        }\n        else\n        {\n            send.RevisionDate = DateTime.UtcNow;\n            await _sendRepository.UpsertAsync(send);\n            await _pushNotificationService.PushSyncSendUpdateAsync(send);\n        }\n    }\n\n    public async Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength)\n    {\n        if (send.Type != SendType.File)\n        {\n            throw new BadRequestException(\"Send is not of type \\\"file\\\".\");\n        }\n\n        if (fileLength < 1)\n        {\n            throw new BadRequestException(\"No file data.\");\n        }\n\n        if (fileLength > SendFileSettingHelper.MAX_FILE_SIZE)\n        {\n            throw new BadRequestException($\"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}.\");\n        }\n\n        var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send);\n\n        if (storageBytesRemaining < fileLength)\n        {\n            throw new BadRequestException(\"Not enough storage available.\");\n        }\n\n        var fileId = _sendCoreHelperService.SecureRandomString(32, useUpperCase: false, useSpecial: false);\n\n        try\n        {\n            data.Id = fileId;\n            data.Size = fileLength;\n            data.Validated = false;\n            send.Data = JsonSerializer.Serialize(data, JsonHelpers.IgnoreWritingNull);\n            await SaveSendAsync(send);\n            return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId);\n        }\n        catch\n        {\n            _logger.LogWarning(\n                \"Deleted file from {SendId} because an error occurred when creating the upload URL.\",\n                send.Id\n            );\n\n            // Clean up since this is not transactional\n            await _sendFileStorageService.DeleteFileAsync(send, fileId);\n            throw;\n        }\n    }\n    public async Task UploadFileToExistingSendAsync(Stream stream, Send send)\n    {\n        if (stream.Position > 0)\n        {\n            stream.Position = 0;\n        }\n\n        if (send?.Data == null)\n        {\n            throw new BadRequestException(\"Send does not have file data\");\n        }\n\n        if (send.Type != SendType.File)\n        {\n            throw new BadRequestException(\"Not a File Type Send.\");\n        }\n\n        var data = JsonSerializer.Deserialize<SendFileData>(send.Data);\n\n        if (data.Validated)\n        {\n            throw new BadRequestException(\"File has already been uploaded.\");\n        }\n\n        await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id);\n\n        if (!await ConfirmFileSize(send))\n        {\n            throw new BadRequestException(\"File received does not match expected file length.\");\n        }\n    }\n    public async Task DeleteSendAsync(Send send)\n    {\n        await _sendRepository.DeleteAsync(send);\n        if (send.Type == Enums.SendType.File)\n        {\n            var data = JsonSerializer.Deserialize<SendFileData>(send.Data);\n            await _sendFileStorageService.DeleteFileAsync(send, data.Id);\n        }\n        await _pushNotificationService.PushSyncSendDeleteAsync(send);\n    }\n\n    public async Task<bool> ConfirmFileSize(Send send)\n    {\n        var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);\n\n        var minimum = fileData.Size - SendFileSettingHelper.FILE_SIZE_LEEWAY;\n        var maximum = Math.Min(\n            fileData.Size + SendFileSettingHelper.FILE_SIZE_LEEWAY,\n            SendFileSettingHelper.MAX_FILE_SIZE\n        );\n        var (valid, size) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, minimum, maximum);\n\n        // protect file service from upload hijacking by deleting invalid sends\n        if (!valid)\n        {\n            _logger.LogWarning(\n                \"Deleted {SendId} because its reported size {Size} was outside the expected range ({Minimum} - {Maximum}).\",\n                send.Id,\n                size,\n                minimum,\n                maximum\n            );\n            await DeleteSendAsync(send);\n            return false;\n        }\n\n        // replace expected size with validated size\n        fileData.Size = size;\n        fileData.Validated = true;\n        send.Data = JsonSerializer.Serialize(fileData, JsonHelpers.IgnoreWritingNull);\n        await SaveSendAsync(send);\n\n        return valid;\n    }\n\n    public async Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId)\n    {\n        if (send.Type != SendType.File)\n        {\n            throw new BadRequestException(\"Can only get a download URL for a file type of Send\");\n        }\n\n        if (!INonAnonymousSendCommand.SendCanBeAccessed(send))\n        {\n            return (null, SendAccessResult.Denied);\n        }\n\n        send.AccessCount++;\n        await _sendRepository.ReplaceAsync(send);\n        await _pushNotificationService.PushSyncSendUpdateAsync(send);\n        return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), SendAccessResult.Granted);\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs",
    "content": "﻿using Bit.Core.Tools.Models.Data;\n\n#nullable enable\n\nnamespace Bit.Core.Tools.SendFeatures.Queries.Interfaces;\n\n/// <summary>\n/// Integration with authentication layer for generating send access claims.\n/// </summary>\npublic interface ISendAuthenticationQuery\n{\n    /// <summary>\n    /// Retrieves the authentication method of a Send.\n    /// </summary>\n    /// <param name=\"sendId\">Identifies the send to inspect.</param>\n    /// <returns>\n    /// The authentication method that should be performed for the send.\n    /// </returns>\n    Task<SendAuthenticationMethod> GetAuthenticationMethod(Guid sendId);\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Queries/Interfaces/ISendOwnerQuery.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Tools.Entities;\n\nnamespace Bit.Core.Tools.SendFeatures.Queries.Interfaces;\n\n/// <summary>\n/// Queries sends owned by the current user.\n/// </summary>\npublic interface ISendOwnerQuery\n{\n    /// <summary>\n    /// Gets a send.\n    /// </summary>\n    /// <param name=\"id\">Identifies the send</param>\n    /// <param name=\"user\">The principal requesting the send.</param>\n    /// <returns>The send</returns>\n    /// <exception cref=\"NotFoundException\">\n    /// Thrown when <paramref name=\"id\"/> fails to identify a send\n    /// owned by the user.\n    /// </exception>\n    /// <exception cref=\"BadRequestException\">\n    /// Thrown when the query cannot identify the current user.\n    /// </exception>\n    Task<Send> Get(Guid id, ClaimsPrincipal user);\n\n    /// <summary>\n    /// Gets all sends owned by the current user.\n    /// </summary>\n    /// <param name=\"user\">The principal requesting the send.</param>\n    /// <returns>\n    /// A sequence of all owned sends.\n    /// </returns>\n    /// <exception cref=\"BadRequestException\">\n    /// Thrown when the query cannot identify the current user.\n    /// </exception>\n    Task<ICollection<Send>> GetOwned(ClaimsPrincipal user);\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs",
    "content": "﻿using Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Queries.Interfaces;\n\n#nullable enable\n\nnamespace Bit.Core.Tools.SendFeatures.Queries;\n\n/// <inheritdoc cref=\"ISendAuthenticationQuery\"/>\npublic class SendAuthenticationQuery : ISendAuthenticationQuery\n{\n    private static readonly NotAuthenticated NOT_AUTHENTICATED = new NotAuthenticated();\n    private static readonly NeverAuthenticate NEVER_AUTHENTICATE = new NeverAuthenticate();\n    private static readonly SendInaccessible SEND_INACCESSIBLE = new SendInaccessible();\n\n    private readonly ISendRepository _sendRepository;\n\n    /// <summary>\n    /// Instantiates the command\n    /// </summary>\n    /// <param name=\"sendRepository\">\n    /// Retrieves send records\n    /// </param>\n    /// <exception cref=\"ArgumentNullException\">\n    /// Thrown when <paramref name=\"sendRepository\"/> is <see langword=\"null\"/>.\n    /// </exception>\n    public SendAuthenticationQuery(ISendRepository sendRepository)\n    {\n        _sendRepository = sendRepository ?? throw new ArgumentNullException(nameof(sendRepository));\n    }\n\n    /// <inheritdoc cref=\"ISendAuthenticationQuery.GetAuthenticationMethod\"/>\n    public async Task<SendAuthenticationMethod> GetAuthenticationMethod(Guid sendId)\n    {\n        var send = await _sendRepository.GetByIdAsync(sendId);\n\n        SendAuthenticationMethod method = send switch\n        {\n            null => NEVER_AUTHENTICATE,\n            var s when s.Disabled => SEND_INACCESSIBLE,\n            var s when s.AccessCount >= s.MaxAccessCount.GetValueOrDefault(int.MaxValue) => SEND_INACCESSIBLE,\n            var s when s.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < DateTime.UtcNow => SEND_INACCESSIBLE,\n            var s when s.DeletionDate <= DateTime.UtcNow => SEND_INACCESSIBLE,\n            var s when s.AuthType == AuthType.Email && s.Emails is not null => EmailOtp(s.Emails),\n            var s when s.AuthType == AuthType.Password && s.Password is not null => new ResourcePassword(s.Password),\n            _ => NOT_AUTHENTICATED\n        };\n\n        return method;\n    }\n\n    private static EmailOtp EmailOtp(string? emails)\n    {\n        if (string.IsNullOrWhiteSpace(emails))\n        {\n            return new EmailOtp([]);\n        }\n        var list = emails.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n        return new EmailOtp(list);\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Queries/SendOwnerQuery.cs",
    "content": "﻿\nusing System.Security.Claims;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Queries.Interfaces;\n\nnamespace Bit.Core.Tools.SendFeatures.Queries;\n\n/// <inheritdoc cref=\"ISendOwnerQuery\"/>\npublic class SendOwnerQuery : ISendOwnerQuery\n{\n    private readonly ISendRepository _repository;\n    private readonly IUserService _users;\n\n    /// <summary>\n    /// Instantiates the command\n    /// </summary>\n    /// <param name=\"sendRepository\">\n    /// Retrieves send records\n    /// </param>\n    /// <exception cref=\"ArgumentNullException\">\n    /// Thrown when <paramref name=\"sendRepository\"/> is <see langword=\"null\"/>.\n    /// </exception>\n    public SendOwnerQuery(ISendRepository sendRepository, IUserService users)\n    {\n        _repository = sendRepository;\n        _users = users ?? throw new ArgumentNullException(nameof(users));\n    }\n\n    /// <inheritdoc cref=\"ISendOwnerQuery.Get\"/>\n    public async Task<Send> Get(Guid id, ClaimsPrincipal user)\n    {\n        var userId = _users.GetProperUserId(user) ?? throw new BadRequestException(\"invalid user.\");\n        var send = await _repository.GetByIdAsync(id);\n\n        if (send == null || send.UserId != userId)\n        {\n            throw new NotFoundException();\n        }\n\n        return send;\n    }\n\n    /// <inheritdoc cref=\"ISendOwnerQuery.GetOwned\"/>\n    public async Task<ICollection<Send>> GetOwned(ClaimsPrincipal user)\n    {\n        var userId = _users.GetProperUserId(user) ?? throw new BadRequestException(\"invalid user.\");\n        var sends = await _repository.GetManyByUserIdAsync(userId);\n\n        return sends;\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs",
    "content": "﻿using Bit.Core.Tools.SendFeatures.Commands;\nusing Bit.Core.Tools.SendFeatures.Commands.Interfaces;\nusing Bit.Core.Tools.SendFeatures.Queries;\nusing Bit.Core.Tools.SendFeatures.Queries.Interfaces;\nusing Bit.Core.Tools.Services;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Tools.SendFeatures;\n\npublic static class SendServiceCollectionExtension\n{\n    public static void AddSendServices(this IServiceCollection services)\n    {\n        services.AddScoped<INonAnonymousSendCommand, NonAnonymousSendCommand>();\n        services.AddScoped<IAnonymousSendCommand, AnonymousSendCommand>();\n        services.AddScoped<ISendAuthorizationService, SendAuthorizationService>();\n        services.AddScoped<ISendValidationService, SendValidationService>();\n        services.AddScoped<ISendCoreHelperService, SendCoreHelperService>();\n        services.AddScoped<ISendAuthenticationQuery, SendAuthenticationQuery>();\n        services.AddScoped<ISendOwnerQuery, SendOwnerQuery>();\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Azure.Storage.Blobs;\nusing Azure.Storage.Blobs.Models;\nusing Azure.Storage.Sas;\nusing Bit.Core.Enums;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Entities;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Tools.Services;\n\npublic class AzureSendFileStorageService : ISendFileStorageService\n{\n    public const string FilesContainerName = \"sendfiles\";\n    private static readonly TimeSpan _downloadLinkLiveTime = TimeSpan.FromMinutes(1);\n    private readonly BlobServiceClient _blobServiceClient;\n    private readonly ILogger<AzureSendFileStorageService> _logger;\n    private BlobContainerClient _sendFilesContainerClient;\n\n    public FileUploadType FileUploadType => FileUploadType.Azure;\n\n    public static string SendIdFromBlobName(string blobName) => blobName.Split('/')[0];\n    public static string BlobName(Send send, string fileId) => $\"{send.Id}/{fileId}\";\n\n    public AzureSendFileStorageService(\n        GlobalSettings globalSettings,\n        ILogger<AzureSendFileStorageService> logger)\n    {\n        _blobServiceClient = new BlobServiceClient(globalSettings.Send.ConnectionString);\n        _logger = logger;\n    }\n\n    public async Task UploadNewFileAsync(Stream stream, Send send, string fileId)\n    {\n        await InitAsync();\n\n        var blobClient = _sendFilesContainerClient.GetBlobClient(BlobName(send, fileId));\n\n        var metadata = new Dictionary<string, string>();\n        if (send.UserId.HasValue)\n        {\n            metadata.Add(\"userId\", send.UserId.Value.ToString());\n        }\n        else\n        {\n            metadata.Add(\"organizationId\", send.OrganizationId.Value.ToString());\n        }\n\n        var headers = new BlobHttpHeaders\n        {\n            ContentDisposition = $\"attachment; filename=\\\"{fileId}\\\"\"\n        };\n\n        await blobClient.UploadAsync(stream, new BlobUploadOptions { Metadata = metadata, HttpHeaders = headers });\n    }\n\n    public async Task DeleteFileAsync(Send send, string fileId) => await DeleteBlobAsync(BlobName(send, fileId));\n\n    public async Task DeleteBlobAsync(string blobName)\n    {\n        await InitAsync();\n        var blobClient = _sendFilesContainerClient.GetBlobClient(blobName);\n        await blobClient.DeleteIfExistsAsync();\n    }\n\n    public async Task DeleteFilesForOrganizationAsync(Guid organizationId)\n    {\n        await InitAsync();\n    }\n\n    public async Task DeleteFilesForUserAsync(Guid userId)\n    {\n        await InitAsync();\n    }\n\n    public async Task<string> GetSendFileDownloadUrlAsync(Send send, string fileId)\n    {\n        await InitAsync();\n        var blobClient = _sendFilesContainerClient.GetBlobClient(BlobName(send, fileId));\n        var sasUri = blobClient.GenerateSasUri(BlobSasPermissions.Read, DateTime.UtcNow.Add(_downloadLinkLiveTime));\n        return sasUri.ToString();\n    }\n\n    public async Task<string> GetSendFileUploadUrlAsync(Send send, string fileId)\n    {\n        await InitAsync();\n        var blobClient = _sendFilesContainerClient.GetBlobClient(BlobName(send, fileId));\n        var sasUri = blobClient.GenerateSasUri(BlobSasPermissions.Create | BlobSasPermissions.Write, DateTime.UtcNow.Add(_downloadLinkLiveTime));\n        return sasUri.ToString();\n    }\n\n    public async Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum)\n    {\n        await InitAsync();\n\n        var blobClient = _sendFilesContainerClient.GetBlobClient(BlobName(send, fileId));\n\n        try\n        {\n            var blobProperties = await blobClient.GetPropertiesAsync();\n            var metadata = blobProperties.Value.Metadata;\n\n            if (send.UserId.HasValue)\n            {\n                metadata[\"userId\"] = send.UserId.Value.ToString();\n            }\n            else\n            {\n                metadata[\"organizationId\"] = send.OrganizationId.Value.ToString();\n            }\n            await blobClient.SetMetadataAsync(metadata);\n\n            var headers = new BlobHttpHeaders\n            {\n                ContentDisposition = $\"attachment; filename=\\\"{fileId}\\\"\"\n            };\n            await blobClient.SetHttpHeadersAsync(headers);\n\n            var length = blobProperties.Value.ContentLength;\n            var valid = minimum <= length || length <= maximum;\n\n            return (valid, length);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, $\"A storage operation failed in {nameof(ValidateFileAsync)}\");\n            return (false, -1);\n        }\n    }\n\n    private async Task InitAsync()\n    {\n        if (_sendFilesContainerClient == null)\n        {\n            _sendFilesContainerClient = _blobServiceClient.GetBlobContainerClient(FilesContainerName);\n            await _sendFilesContainerClient.CreateIfNotExistsAsync(PublicAccessType.None, null, null);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/Interfaces/ISendAuthorizationService.cs",
    "content": "﻿using Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Models.Data;\n\nnamespace Bit.Core.Tools.Services;\n\n/// <summary>\n/// Send Authorization service is responsible for checking if a Send can be accessed.\n/// </summary>\npublic interface ISendAuthorizationService\n{\n    /// <summary>\n    /// Checks if a <see cref=\"Send\" /> can be accessed while updating the <see cref=\"Send\" />, pushing a notification, and sending a reference event.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> used to determine access</param>\n    /// <param name=\"password\">A hashed and base64-encoded password. This is compared with the send's password to authorize access.</param>\n    /// <returns><see cref=\"SendAccessResult\" /> will be returned to determine if the user can access send.\n    /// </returns>\n    Task<SendAccessResult> AccessAsync(Send send, string password);\n    SendAccessResult SendCanBeAccessed(Send send,\n        string password);\n\n    /// <summary>\n    /// Hashes the password using the password hasher.\n    /// </summary>\n    /// <param name=\"password\">Password to be hashed</param>\n    /// <returns>Hashed password of the password given</returns>\n    string HashPassword(string password);\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/Interfaces/ISendCoreHelperService.cs",
    "content": "﻿namespace Bit.Core.Tools.Services;\n\n/// <summary>\n/// This interface provides helper methods for generating secure random strings. Making\n/// it easier to mock the service in unit tests.\n/// </summary>\npublic interface ISendCoreHelperService\n{\n    /// <summary>\n    /// Securely generates a random string of the specified length.\n    /// </summary>\n    /// <param name=\"length\">Desired string length to be returned</param>\n    /// <param name=\"useUpperCase\">Desired casing for the string</param>\n    /// <param name=\"useSpecial\">Determines if special characters will be used in string</param>\n    /// <returns>A secure random string with the desired parameters</returns>\n    string SecureRandomString(int length, bool useUpperCase, bool useSpecial);\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Tools.Entities;\n\nnamespace Bit.Core.Tools.Services;\n\n/// <summary>\n/// Send File Storage Service is responsible for uploading, deleting, and validating files\n/// whether they are in local storage or in cloud storage.\n/// </summary>\npublic interface ISendFileStorageService\n{\n    FileUploadType FileUploadType { get; }\n    /// <summary>\n    ///  Uploads a new file to the storage.\n    /// </summary>\n    /// <param name=\"stream\"><see cref=\"Stream\" /> of the file</param>\n    /// <param name=\"send\"><see cref=\"Send\" /> for the file</param>\n    /// <param name=\"fileId\">File id</param>\n    /// <returns>Task completes once <see cref=\"Stream\" /> and <see cref=\"Send\" /> have been saved to the database</returns>\n    Task UploadNewFileAsync(Stream stream, Send send, string fileId);\n    /// <summary>\n    /// Deletes a file from the storage.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> used to delete file</param>\n    /// <param name=\"fileId\">File id of file to be deleted</param>\n    /// <returns>Task completes once <see cref=\"Send\" /> has been deleted to the database</returns>\n    Task DeleteFileAsync(Send send, string fileId);\n    /// <summary>\n    /// Deletes all files for a specific organization.\n    /// </summary>\n    /// <param name=\"organizationId\"><see cref=\"Guid\" />  used to delete all files pertaining to organization</param>\n    /// <returns>Task completes after running code to delete files by organization id</returns>\n    Task DeleteFilesForOrganizationAsync(Guid organizationId);\n    /// <summary>\n    /// Deletes all files for a specific user.\n    /// </summary>\n    /// <param name=\"userId\"><see cref=\"Guid\" /> used to delete all files pertaining to user</param>\n    /// <returns>Task completes after running code to delete files by user id</returns>\n    Task DeleteFilesForUserAsync(Guid userId);\n    /// <summary>\n    /// Gets the download URL for a file.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> used to help get download url for file</param>\n    /// <param name=\"fileId\">File id to help get download url for file</param>\n    /// <returns>Download url as a string</returns>\n    Task<string> GetSendFileDownloadUrlAsync(Send send, string fileId);\n    /// <summary>\n    /// Gets the upload URL for a file.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> used to help get upload url for file </param>\n    /// <param name=\"fileId\">File id to help get upload url for file</param>\n    /// <returns>File upload url as string</returns>\n    Task<string> GetSendFileUploadUrlAsync(Send send, string fileId);\n    /// <summary>\n    /// Validates the file size of a file in the storage.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> used to help validate file</param>\n    /// <param name=\"fileId\">File id to identify which file to validate</param>\n    /// <param name=\"minimum\">The minimum allowed length of the stored file in bytes.</param>\n    /// <param name=\"maximum\">The maximuim allowed length of the stored file in bytes</param>\n    /// <returns>\n    /// A task that completes when validation is finished. The first element of the tuple is\n    /// <see langword=\"true\" /> when validation succeeded, and false otherwise. The second element\n    /// of the tuple contains the observed file length in bytes. If an error occurs during validation,\n    /// this returns `-1`.\n    /// </returns>\n    Task<(bool valid, long length)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum);\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/Interfaces/ISendValidationService.cs",
    "content": "﻿using Bit.Core.Tools.Entities;\n\nnamespace Bit.Core.Tools.Services;\n\npublic interface ISendValidationService\n{\n    /// <summary>\n    /// Validates a file can be saved by specified user.\n    /// </summary>\n    /// <param name=\"userId\"><see cref=\"Guid\" /> needed to validate file for specific user</param>\n    /// <param name=\"send\"><see cref=\"Send\" /> needed to help validate file</param>\n    /// <returns>Task completes when a conditional statement has been met it will return out of the method or\n    /// throw a BadRequestException.\n    /// </returns>\n    Task ValidateUserCanSaveAsync(Guid? userId, Send send);\n\n    /// <summary>\n    /// Calculates the remaining storage for a Send.\n    /// </summary>\n    /// <param name=\"send\"><see cref=\"Send\" /> needed to help calculate remaining storage</param>\n    /// <returns>Long with the remaining bytes for storage or will throw a BadRequestException if user cannot access\n    /// file or email is not verified.\n    /// </returns>\n    Task<long> StorageRemainingForSendAsync(Send send);\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Entities;\n\nnamespace Bit.Core.Tools.Services;\n\npublic class LocalSendStorageService : ISendFileStorageService\n{\n    private readonly string _baseDirPath;\n    private readonly string _baseSendUrl;\n\n    private string RelativeFilePath(Send send, string fileID) => $\"{send.Id}/{fileID}\";\n    private string FilePath(Send send, string fileID) => $\"{_baseDirPath}/{RelativeFilePath(send, fileID)}\";\n    public FileUploadType FileUploadType => FileUploadType.Direct;\n\n    public LocalSendStorageService(\n        GlobalSettings globalSettings)\n    {\n        _baseDirPath = globalSettings.Send.BaseDirectory;\n        _baseSendUrl = globalSettings.Send.BaseUrl;\n    }\n\n    public async Task UploadNewFileAsync(Stream stream, Send send, string fileId)\n    {\n        await InitAsync();\n        var path = FilePath(send, fileId);\n        Directory.CreateDirectory(Path.GetDirectoryName(path));\n        using (var fs = File.Create(path))\n        {\n            stream.Seek(0, SeekOrigin.Begin);\n            await stream.CopyToAsync(fs);\n        }\n    }\n\n    public async Task DeleteFileAsync(Send send, string fileId)\n    {\n        await InitAsync();\n        var path = FilePath(send, fileId);\n        DeleteFileIfExists(path);\n        DeleteDirectoryIfExistsAndEmpty(Path.GetDirectoryName(path));\n    }\n\n    public async Task DeleteFilesForOrganizationAsync(Guid organizationId)\n    {\n        await InitAsync();\n    }\n\n    public async Task DeleteFilesForUserAsync(Guid userId)\n    {\n        await InitAsync();\n    }\n\n    public async Task<string> GetSendFileDownloadUrlAsync(Send send, string fileId)\n    {\n        await InitAsync();\n        return $\"{_baseSendUrl}/{RelativeFilePath(send, fileId)}\";\n    }\n\n    private void DeleteFileIfExists(string path)\n    {\n        if (File.Exists(path))\n        {\n            File.Delete(path);\n        }\n    }\n\n    private void DeleteDirectoryIfExistsAndEmpty(string path)\n    {\n        if (Directory.Exists(path) && !Directory.EnumerateFiles(path).Any())\n        {\n            Directory.Delete(path);\n        }\n    }\n\n    private Task InitAsync()\n    {\n        if (!Directory.Exists(_baseDirPath))\n        {\n            Directory.CreateDirectory(_baseDirPath);\n        }\n\n        return Task.FromResult(0);\n    }\n\n    public Task<string> GetSendFileUploadUrlAsync(Send send, string fileId)\n        => Task.FromResult($\"/sends/{send.Id}/file/{fileId}\");\n\n    public Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum)\n    {\n        long length = -1;\n        var path = FilePath(send, fileId);\n        if (!File.Exists(path))\n        {\n            return Task.FromResult((false, length));\n        }\n\n        length = new FileInfo(path).Length;\n        var valid = minimum < length || length < maximum;\n        return Task.FromResult((valid, length));\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Core.Tools.Services;\n\npublic class SendAuthorizationService : ISendAuthorizationService\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly IPasswordHasher<User> _passwordHasher;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    public SendAuthorizationService(\n        ISendRepository sendRepository,\n        IPasswordHasher<User> passwordHasher,\n        IPushNotificationService pushNotificationService)\n    {\n        _sendRepository = sendRepository;\n        _passwordHasher = passwordHasher;\n        _pushNotificationService = pushNotificationService;\n    }\n\n    public SendAccessResult SendCanBeAccessed(Send send,\n        string password)\n    {\n        var now = DateTime.UtcNow;\n        if (send == null || send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount ||\n            send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < now || send.Disabled ||\n            send.DeletionDate <= now)\n        {\n            return SendAccessResult.Denied;\n        }\n        if (!string.IsNullOrWhiteSpace(send.Password))\n        {\n            if (string.IsNullOrWhiteSpace(password))\n            {\n                return SendAccessResult.PasswordRequired;\n            }\n            var passwordResult = _passwordHasher.VerifyHashedPassword(new User(), send.Password, password);\n            if (passwordResult == PasswordVerificationResult.SuccessRehashNeeded)\n            {\n                send.Password = HashPassword(password);\n            }\n            if (passwordResult == PasswordVerificationResult.Failed)\n            {\n                return SendAccessResult.PasswordInvalid;\n            }\n        }\n\n        return SendAccessResult.Granted;\n    }\n\n    public async Task<SendAccessResult> AccessAsync(Send sendToBeAccessed, string password)\n    {\n        var accessResult = SendCanBeAccessed(sendToBeAccessed, password);\n\n        if (!accessResult.Equals(SendAccessResult.Granted))\n        {\n            return accessResult;\n        }\n\n        if (sendToBeAccessed.Type != SendType.File)\n        {\n            // File sends are incremented during file download\n            sendToBeAccessed.AccessCount++;\n        }\n\n        await _sendRepository.ReplaceAsync(sendToBeAccessed);\n        await _pushNotificationService.PushSyncSendUpdateAsync(sendToBeAccessed);\n        return accessResult;\n    }\n\n    public string HashPassword(string password)\n    {\n        return _passwordHasher.HashPassword(new User(), password);\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/SendCoreHelperService.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Core.Tools.Services;\n\npublic class SendCoreHelperService : ISendCoreHelperService\n{\n    public string SecureRandomString(int length, bool useUpperCase, bool useSpecial)\n    {\n        return CoreHelpers.SecureRandomString(length, upper: useUpperCase, special: useSpecial);\n    }\n\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/SendFileSettingHelper.cs",
    "content": "﻿using Bit.Core.Tools.Entities;\n\nnamespace Bit.Core.Tools.SendFeatures;\n\n/// <summary>\n/// SendFileSettingHelper is a static class that provides constants and helper methods (if needed) for managing file\n/// settings.\n/// </summary>\npublic static class SendFileSettingHelper\n{\n    /// <summary>\n    /// The leeway for the file size. This is the calculated 1 megabyte of cushion when doing comparisons of file sizes\n    /// within the system.\n    /// </summary>\n    public const long FILE_SIZE_LEEWAY = 1024L * 1024L; // 1MB\n    /// <summary>\n    /// The maximum file size for a file uploaded in a <see cref=\"Send\" />. Units are calculated in bytes but\n    /// represent 501 megabytes. 1 megabyte is added for cushion to account for file size.\n    /// </summary>\n    public const long MAX_FILE_SIZE = Constants.FileSize501mb;\n\n    /// <summary>\n    /// String of the expected file size and to be used when needing to communicate the file size to the client/user.\n    /// </summary>\n    public const string MAX_FILE_SIZE_READABLE = \"500 MB\";\n}\n"
  },
  {
    "path": "src/Core/Tools/SendFeatures/Services/SendValidationService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Entities;\n\nnamespace Bit.Core.Tools.Services;\n\npublic class SendValidationService : ISendValidationService\n{\n\n    private readonly IUserRepository _userRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IUserService _userService;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IPricingClient _pricingClient;\n\n    public SendValidationService(\n        IUserRepository userRepository,\n        IOrganizationRepository organizationRepository,\n        IUserService userService,\n        IPolicyRequirementQuery policyRequirementQuery,\n        GlobalSettings globalSettings,\n        IPricingClient pricingClient)\n    {\n        _userRepository = userRepository;\n        _organizationRepository = organizationRepository;\n        _userService = userService;\n        _policyRequirementQuery = policyRequirementQuery;\n        _globalSettings = globalSettings;\n        _pricingClient = pricingClient;\n    }\n\n    public async Task ValidateUserCanSaveAsync(Guid? userId, Send send)\n    {\n        // The nullable userId is intended to support organization-owned Sends (never implemented).\n        // If it's null, we can't enforce policies, because policies are only enforced against a specific user.\n        if (!userId.HasValue)\n        {\n            return;\n        }\n\n        var disableSendRequirement = await _policyRequirementQuery.GetAsync<DisableSendPolicyRequirement>(userId.Value);\n        if (disableSendRequirement.DisableSend)\n        {\n            throw new BadRequestException(\"Due to an Enterprise Policy, you are only able to delete an existing Send.\");\n        }\n\n        var sendOptionsRequirement = await _policyRequirementQuery.GetAsync<SendOptionsPolicyRequirement>(userId.Value);\n        if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault())\n        {\n            throw new BadRequestException(\"Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.\");\n        }\n    }\n\n    public async Task<long> StorageRemainingForSendAsync(Send send)\n    {\n        var storageBytesRemaining = 0L;\n        if (send.UserId.HasValue)\n        {\n            var user = await _userRepository.GetByIdAsync(send.UserId.Value);\n            if (!await _userService.CanAccessPremium(user))\n            {\n                throw new BadRequestException(\"You must have premium status to use file Sends.\");\n            }\n\n            if (!user.EmailVerified)\n            {\n                throw new BadRequestException(\"You must confirm your email to use file Sends.\");\n            }\n\n            if (user.Premium)\n            {\n                storageBytesRemaining = user.StorageBytesRemaining();\n            }\n            else\n            {\n                // Users that get access to file storage/premium from their organization get storage\n                // based on the current premium plan from the pricing service\n                short provided;\n                if (_globalSettings.SelfHosted)\n                {\n                    provided = Constants.SelfHostedMaxStorageGb;\n                }\n                else\n                {\n                    var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();\n                    provided = (short)premiumPlan.Storage.Provided;\n                }\n                storageBytesRemaining = user.StorageBytesRemaining(provided);\n            }\n        }\n        else if (send.OrganizationId.HasValue)\n        {\n            var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value);\n            if (!org.MaxStorageGb.HasValue)\n            {\n                throw new BadRequestException(\"This organization cannot use file sends.\");\n            }\n\n            storageBytesRemaining = org.StorageBytesRemaining();\n        }\n\n        return storageBytesRemaining;\n    }\n}\n"
  },
  {
    "path": "src/Core/Tools/Services/NoopImplementations/NoopSendFileStorageService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.Tools.Entities;\n\nnamespace Bit.Core.Tools.Services;\n\npublic class NoopSendFileStorageService : ISendFileStorageService\n{\n    public FileUploadType FileUploadType => FileUploadType.Direct;\n\n    public Task UploadNewFileAsync(Stream stream, Send send, string attachmentId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteFileAsync(Send send, string fileId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteFilesForOrganizationAsync(Guid organizationId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteFilesForUserAsync(Guid userId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<string> GetSendFileDownloadUrlAsync(Send send, string fileId)\n    {\n        return Task.FromResult((string)null);\n    }\n\n    public Task<string> GetSendFileUploadUrlAsync(Send send, string fileId)\n    {\n        return Task.FromResult((string)null);\n    }\n\n    public Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum)\n    {\n        return Task.FromResult((false, -1L));\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/AssemblyHelpers.cs",
    "content": "﻿using System.Diagnostics;\nusing System.Reflection;\n\nnamespace Bit.Core.Utilities;\n\npublic static class AssemblyHelpers\n{\n    private static string? _version;\n    private static string? _gitHash;\n\n    static AssemblyHelpers()\n    {\n        var assemblyInformationalVersionAttribute = typeof(AssemblyHelpers).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();\n        if (assemblyInformationalVersionAttribute == null)\n        {\n            Debug.Fail(\"The AssemblyInformationalVersionAttribute is expected to exist in this assembly, possibly its generation was turned off.\");\n            return;\n        }\n\n        var informationalVersion = assemblyInformationalVersionAttribute.InformationalVersion.AsSpan();\n\n        if (!informationalVersion.TrySplitBy('+', out var version, out var gitHash))\n        {\n            // Treat the whole thing as the version\n            _version = informationalVersion.ToString();\n            return;\n        }\n\n        _version = version.ToString();\n        if (gitHash.Length < 8)\n        {\n            return;\n        }\n        _gitHash = gitHash[..8].ToString();\n    }\n\n    public static string? GetVersion()\n    {\n        return _version;\n    }\n\n    public static string? GetGitHash()\n    {\n        return _gitHash;\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/AuthorizationServiceExtensions.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.Utilities;\n\npublic static class AuthorizationServiceExtensions\n{\n    /// <summary>\n    /// Checks if a user meets a specific requirement.\n    /// </summary>\n    /// <param name=\"service\">The <see cref=\"IAuthorizationService\"/> providing authorization.</param>\n    /// <param name=\"user\">The user to evaluate the policy against.</param>\n    /// <param name=\"requirement\">The requirement to evaluate the policy against.</param>\n    /// <returns>\n    /// A flag indicating whether requirement evaluation has succeeded or failed.\n    /// This value is <value>true</value> when the user fulfills the policy, otherwise <value>false</value>.\n    /// </returns>\n    public static Task<AuthorizationResult> AuthorizeAsync(this IAuthorizationService service, ClaimsPrincipal user, IAuthorizationRequirement requirement)\n    {\n        if (service == null)\n        {\n            throw new ArgumentNullException(nameof(service));\n        }\n\n        if (requirement == null)\n        {\n            throw new ArgumentNullException(nameof(requirement));\n        }\n\n        return service.AuthorizeAsync(user, resource: null, new[] { requirement });\n    }\n\n    /// <summary>\n    /// Performs an authorization check and throws a <see cref=\"Bit.Core.Exceptions.NotFoundException\"/> if the\n    /// check fails or the resource is null.\n    /// </summary>\n    public static async Task AuthorizeOrThrowAsync(this IAuthorizationService service,\n        ClaimsPrincipal user, object resource, IAuthorizationRequirement requirement)\n    {\n        ArgumentNullException.ThrowIfNull(service);\n        ArgumentNullException.ThrowIfNull(requirement);\n\n        if (resource == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var authorizationResult = await service.AuthorizeAsync(user, resource, requirement);\n        if (!authorizationResult.Succeeded)\n        {\n            throw new NotFoundException();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/BillingHelpers.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\n\nnamespace Bit.Core.Utilities;\n\npublic static class BillingHelpers\n{\n    internal static async Task<string?> AdjustStorageAsync(\n        IStripePaymentService paymentService,\n        IUpdateOrganizationSubscriptionCommand? updateOrganizationSubscriptionCommand,\n        IFeatureService featureService,\n        IStorableSubscriber storableSubscriber,\n        short storageAdjustmentGb,\n        string storagePlanId,\n        short baseStorageGb)\n    {\n        if (storableSubscriber == null)\n        {\n            throw new ArgumentNullException(nameof(storableSubscriber));\n        }\n\n        if (string.IsNullOrWhiteSpace(storableSubscriber.GatewayCustomerId))\n        {\n            throw new BadRequestException(\"No payment method found.\");\n        }\n\n        if (string.IsNullOrWhiteSpace(storableSubscriber.GatewaySubscriptionId))\n        {\n            throw new BadRequestException(\"No subscription found.\");\n        }\n\n        if (!storableSubscriber.MaxStorageGb.HasValue)\n        {\n            throw new BadRequestException(\"No access to storage.\");\n        }\n\n        var newStorageGb = (short)(storableSubscriber.MaxStorageGb.Value + storageAdjustmentGb);\n        if (newStorageGb < baseStorageGb)\n        {\n            newStorageGb = baseStorageGb;\n        }\n\n        if (newStorageGb > 100)\n        {\n            throw new BadRequestException(\"Maximum storage is 100 GB.\");\n        }\n\n        var remainingStorage = storableSubscriber.StorageBytesRemaining(newStorageGb);\n        if (remainingStorage < 0)\n        {\n            throw new BadRequestException(\"You are currently using \" +\n                $\"{CoreHelpers.ReadableBytesSize(storableSubscriber.Storage.GetValueOrDefault(0))} of storage. \" +\n                \"Delete some stored data first.\");\n        }\n\n        var additionalStorage = newStorageGb - baseStorageGb;\n\n        if (storableSubscriber is Organization organization &&\n            updateOrganizationSubscriptionCommand != null &&\n            featureService.IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand))\n        {\n            var builder = OrganizationSubscriptionChangeSet.Builder();\n            if (organization.MaxStorageGb > baseStorageGb)\n            {\n                builder.UpdateItemQuantity(storagePlanId, additionalStorage);\n            }\n            else\n            {\n                builder.AddItem(storagePlanId, additionalStorage);\n            }\n\n            var changeSet = builder.Build();\n            var result = await updateOrganizationSubscriptionCommand.Run(organization, changeSet);\n            result.GetValueOrThrow();\n            storableSubscriber.MaxStorageGb = newStorageGb;\n            return null!;\n        }\n\n        var paymentIntentClientSecret = await paymentService.AdjustStorageAsync(storableSubscriber,\n            additionalStorage, storagePlanId);\n        storableSubscriber.MaxStorageGb = newStorageGb;\n        return paymentIntentClientSecret;\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/BulkAuthorizationHandler.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.Utilities;\n\n/// <summary>\n/// Allows a single authorization handler implementation to handle requirements for\n/// both singular or bulk operations on single or multiple resources.\n/// </summary>\n/// <typeparam name=\"TRequirement\">The type of the requirement to evaluate.</typeparam>\n/// <typeparam name=\"TResource\">The type of the resource(s) that will be evaluated.</typeparam>\npublic abstract class BulkAuthorizationHandler<TRequirement, TResource> : AuthorizationHandler<TRequirement>\n    where TRequirement : IAuthorizationRequirement\n{\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)\n    {\n        // Attempt to get the resource(s) from the context\n        var bulkResources = GetBulkResourceFromContext(context);\n\n        // No resources of the expected type were found in the context, nothing to evaluate\n        if (bulkResources == null)\n        {\n            return;\n        }\n\n        await HandleRequirementAsync(context, requirement, bulkResources);\n    }\n\n    private static ICollection<TResource> GetBulkResourceFromContext(AuthorizationHandlerContext context)\n    {\n        return context.Resource switch\n        {\n            TResource resource => new List<TResource> { resource },\n            IEnumerable<TResource> resources => resources.ToList(),\n            _ => null\n        };\n    }\n\n    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement,\n        ICollection<TResource> resources);\n}\n"
  },
  {
    "path": "src/Core/Utilities/CACHING.md",
    "content": "# Bitwarden Server Caching\n\nCaching options available in Bitwarden's server. The server uses multiple caching layers and backends to balance performance, scalability, and operational simplicity across both cloud and self-hosted deployments.\n\n---\n\n## Choosing a Caching Option\n\nUse this decision tree to identify the appropriate caching option for your feature:\n\n```\nDoes your data need to be shared across all instances in a horizontally-scaled deployment?\n├─ YES\n│   │\n│   Do you need long-term persistence with TTL (days/weeks)?\n│   ├─ YES → Use `IDistributedCache` with persistent keyed service\n│   └─ NO → Use `ExtendedCache`\n│       │\n│       Notes:\n│       - With Redis configured: memory + distributed + backplane\n│       - Without Redis: memory-only with stampede protection\n│       - Provides fail-safe, eager refresh, circuit breaker\n│       - For org/provider abilities: Use GetOrSetAsync with preloading pattern\n│\n└─ NO (single instance or manual sync acceptable)\n    │\n    Use `ExtendedCache` with memory-only mode (EnableDistributedCache = false)\n    │\n    Notes:\n    - Same performance as raw IMemoryCache\n    - Built-in stampede protection, eager refresh, fail-safe\n    - \"Free\" Redis/backplane if needed at a later date (but not required)\n    - Only use specialized in-memory cache if ExtendedCache API doesn't fit\n\n*Stampede protection = prevents cache stampedes (multiple simultaneous requests for the same expired/missing key triggering redundant backend calls)\n```\n\n---\n\n## Caching Options Overview\n\n| Option                                 | Best For                                       | Horizontal Scale | TTL Support | Backend Options        |\n| -------------------------------------- | ---------------------------------------------- | ---------------- | ----------- | ---------------------- |\n| **ExtendedCache**                      | General-purpose caching with advanced features | ✅ Yes           | ✅ Yes      | Redis, Memory          |\n| **IDistributedCache** (default)        | Short-lived key-value caching                  | ✅ Yes           | ⚠️ Manual   | Redis, SQL, EF         |\n| **IDistributedCache** (`\"persistent\"`) | Long-lived data with TTL                       | ✅ Yes           | ✅ Yes      | Cosmos, Redis, SQL, EF |\n| **In-Memory Cache**                    | High-frequency reads, single instance          | ❌ No            | ⚠️ Manual   | Memory                 |\n\n---\n\n## `ExtendedCache`\n\n`ExtendedCache` is a wrapper around [FusionCache](https://github.com/ZiggyCreatures/FusionCache) that provides a simple way to register **named, isolated caches** with sensible defaults. The goal is to make it trivial for each subsystem or feature to have its own cache - with optional distributed caching and backplane support - without repeatedly wiring up FusionCache, Redis, and related infrastructure.\n\nEach named cache automatically receives:\n\n- Its own `FusionCache` instance\n- Its own configuration (default or overridden)\n- Its own key prefix\n- Optional distributed store\n- Optional backplane\n\n`ExtendedCache` supports three deployment modes:\n\n- **Memory-only caching** (with stampede protection: prevents multiple concurrent requests for the same key from hitting the backend)\n- **Memory + distributed cache + backplane** using the **shared** application Redis\n- **Memory + distributed cache + backplane** using a **fully isolated** Redis instance\n\n### When to Use\n\n- **General-purpose caching** for any domain data\n- Features requiring **stampede protection** (when multiple concurrent requests for the same cache key should result in only a single backend call, with all requesters waiting for the same result)\n- Data that benefits from **fail-safe mode** (serve stale data on backend failures)\n- Multi-instance applications requiring **cache synchronization** via backplane\n- You want **isolated cache configuration** per feature\n\n### Pros\n\n✅ **Advanced features out-of-the-box**:\n\n- Stampede protection (multiple requests for same key = single backend call)\n- Fail-safe mode with stale data serving\n- Adaptive caching with eager refresh\n- Automatic backplane support for multi-instance sync\n- Circuit breaker for backend failures\n\n✅ **Named, isolated caches**: Each feature gets its own cache instance with independent configuration\n\n✅ **Flexible deployment modes**:\n\n- Memory-only (development, testing)\n- Memory + Redis (production cloud)\n- Memory + isolated Redis (specialized features)\n\n✅ **Simple API**: Uses `FusionCache`'s intuitive `GetOrSet` pattern\n\n✅ **Built-in serialization**: Automatic JSON serialization/deserialization\n\n### Cons\n\n❌ Requires understanding of `FusionCache` configuration options\n\n❌ Slightly more overhead than raw `IDistributedCache`\n\n❌ IDistributedCache dependency for multi-instance deployments (typically Redis, but degrades gracefully to memory-only)\n\n### Example Usage\n\n**Note**: When using the shared Redis cache option (which is on by default, if the Redis connection string is configured), it is expected to call `services.AddDistributedCache(globalSettings)` **before** calling `AddExtendedCache`. The idea is to set up the distributed cache in our normal pattern and then \"extend\" it to include more functionality.\n\n#### 1. Register the cache (in Startup.cs):\n\n```csharp\n// Option 1: Use default settings with shared Redis (if available)\nservices.AddDistributedCache(globalSettings);\nservices.AddExtendedCache(\"MyFeatureCache\", globalSettings);\n\n// Option 2: Memory-only mode for high-performance single-instance caching\nservices.AddExtendedCache(\"MyFeatureCache\", globalSettings, new GlobalSettings.ExtendedCacheSettings\n{\n    EnableDistributedCache = false,  // Memory-only, same performance as IMemoryCache\n    Duration = TimeSpan.FromHours(1),\n    IsFailSafeEnabled = true,\n    EagerRefreshThreshold = 0.9 // Refresh at 90% of TTL\n});\n// When EnableDistributedCache = false:\n// - Uses memory-only caching (same performance as raw IMemoryCache)\n// - Still provides stampede protection, eager refresh, fail-safe\n// - Redis/backplane can be enabled later by setting EnableDistributedCache = true\n\n// Option 3: Override default settings with Redis\nservices.AddExtendedCache(\"MyFeatureCache\", globalSettings, new GlobalSettings.ExtendedCacheSettings\n{\n    Duration = TimeSpan.FromHours(1),\n    IsFailSafeEnabled = true,\n    FailSafeMaxDuration = TimeSpan.FromHours(2),\n    EagerRefreshThreshold = 0.9 // Refresh at 90% of TTL\n});\n\n// Option 4: Isolated Redis for specialized features\nservices.AddExtendedCache(\"SpecializedCache\", globalSettings, new GlobalSettings.ExtendedCacheSettings\n{\n    UseSharedDistributedCache = false,\n    Redis = new GlobalSettings.ConnectionStringSettings\n    {\n        ConnectionString = \"localhost:6379,ssl=false\"\n    }\n});\n// When configured this way:\n// - A dedicated IConnectionMultiplexer is created\n// - A dedicated IDistributedCache is created\n// - A dedicated FusionCache backplane is created\n// - All three are exposed to DI as keyed services (using the cache name as service key)\n```\n\n#### 2. Inject and use the cache:\n\nA named cache is retrieved via DI using keyed services (similar to how [IHttpClientFactory](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-7.0#named-clients) works with named clients):\n\n```csharp\npublic class MyService\n{\n    private readonly IFusionCache _cache;\n    private readonly IItemRepository _itemRepository;\n\n    // Option A: Inject via keyed service in constructor\n    public MyService(\n        [FromKeyedServices(\"MyFeatureCache\")] IFusionCache cache,\n        IItemRepository itemRepository)\n    {\n        _cache = cache;\n        _itemRepository = itemRepository;\n    }\n\n    // Option B: Request manually from service provider\n    // cache = provider.GetRequiredKeyedService<IFusionCache>(serviceKey: \"MyFeatureCache\")\n\n    // Option C: Inject IFusionCacheProvider and request the named cache\n    // (similar to IHttpClientFactory pattern)\n    public MyService(\n        IFusionCacheProvider cacheProvider,\n        IItemRepository itemRepository)\n    {\n        _cache = cacheProvider.GetCache(\"MyFeatureCache\");\n        _itemRepository = itemRepository;\n    }\n\n    public async Task<Item> GetItemAsync(Guid id)\n    {\n        return await _cache.GetOrSetAsync<Item>(\n            $\"item:{id}\",\n            async _ => await _itemRepository.GetByIdAsync(id),\n            options => options.SetDuration(TimeSpan.FromMinutes(30))\n        );\n    }\n}\n```\n\n`ExtendedCache` doesn't change how `FusionCache` is used in code, which means all the functionality and full `FusionCache` API is available. See the [FusionCache docs](https://github.com/ZiggyCreatures/FusionCache/blob/main/docs/CoreMethods.md) for more details.\n\n### Specific Example: SSO Authorization Grants\n\nSSO authorization grants are **ephemeral, short-lived data** (typically ≤5 minutes) used to coordinate authorization flows across horizontally-scaled instances. `ExtendedCache` is ideal for this use case:\n\n```csharp\nservices.AddExtendedCache(\"SsoGrants\", globalSettings, new GlobalSettings.ExtendedCacheSettings\n{\n    Duration = TimeSpan.FromMinutes(5),\n    IsFailSafeEnabled = false  // Re-initiate flow rather than serve stale grants\n});\n\npublic class SsoAuthorizationService\n{\n    private readonly IFusionCache _cache;\n\n    public SsoAuthorizationService([FromKeyedServices(\"SsoGrants\")] IFusionCache cache)\n    {\n        _cache = cache;\n    }\n\n    public async Task<SsoGrant> GetGrantAsync(string authorizationCode)\n    {\n        return await _cache.GetOrDefaultAsync<SsoGrant>($\"sso:grant:{authorizationCode}\");\n    }\n\n    public async Task StoreGrantAsync(string authorizationCode, SsoGrant grant)\n    {\n        await _cache.SetAsync($\"sso:grant:{authorizationCode}\", grant);\n    }\n}\n```\n\n**Why `ExtendedCache` for SSO grants:**\n\n- **Not critical if lost**: User can re-initiate SSO flow\n- **Lower latency**: Redis backplane is faster than persistent storage\n- **Simpler infrastructure**: Reuses existing Redis connection\n- **Horizontal scaling**: Redis backplane automatically synchronizes across instances\n\n### Backend Configuration\n\n`ExtendedCache` automatically uses the configured backend:\n\n**Cloud (Bitwarden-hosted)**:\n\n1. Redis (primary, if `GlobalSettings.DistributedCache.Redis.ConnectionString` configured)\n2. Memory-only (fallback if Redis unavailable)\n\n**Self-hosted**:\n\n1. Redis (if configured in `appsettings.json`)\n2. SQL Server / EF Cache (if `IDistributedCache` is registered and no Redis)\n3. Memory-only (default fallback)\n\n> **Note**: ExtendedCache works seamlessly with any `IDistributedCache` backend. In self-hosted scenarios without Redis, you can configure ExtendedCache to use SQL Server or Entity Framework cache as its distributed layer. This provides local memory caching in front of the database cache, with the option to add Redis later if needed. You won't get the backplane (cross-instance invalidation) without Redis, but you still get stampede protection, eager refresh, and fail-safe mode.\n\n### Specific Example: Organization/Provider Abilities\n\nOrganization and provider abilities are read extremely frequently (on every request that checks permissions) but change infrequently. `ExtendedCache` is ideal for this access pattern with its eager refresh and Redis backplane support:\n\n```csharp\nservices.AddExtendedCache(\"OrganizationAbilities\", globalSettings, new GlobalSettings.ExtendedCacheSettings\n{\n    Duration = TimeSpan.FromMinutes(10),\n    EagerRefreshThreshold = 0.9,  // Refresh at 90% of TTL\n    IsFailSafeEnabled = true,\n    FailSafeMaxDuration = TimeSpan.FromHours(1)  // Serve stale data up to 1 hour on backend failures\n});\n\npublic class OrganizationAbilityService\n{\n    private readonly IFusionCache _cache;\n    private readonly IOrganizationRepository _organizationRepository;\n\n    public OrganizationAbilityService(\n        [FromKeyedServices(\"OrganizationAbilities\")] IFusionCache cache,\n        IOrganizationRepository organizationRepository)\n    {\n        _cache = cache;\n        _organizationRepository = organizationRepository;\n    }\n\n    public async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync()\n    {\n        return await _cache.GetOrSetAsync<IDictionary<Guid, OrganizationAbility>>(\n            \"all-org-abilities\",\n            async _ =>\n            {\n                var abilities = await _organizationRepository.GetManyAbilitiesAsync();\n                return abilities.ToDictionary(a => a.Id);\n            }\n        );\n    }\n\n    public async Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId)\n    {\n        var abilities = await GetOrganizationAbilitiesAsync();\n        abilities.TryGetValue(orgId, out var ability);\n        return ability;\n    }\n\n    public async Task UpsertOrganizationAbilityAsync(Organization organization)\n    {\n        // Update database\n        await _organizationRepository.ReplaceAsync(organization);\n\n        // Invalidate cache - with Redis backplane, this broadcasts to all instances\n        await _cache.RemoveAsync(\"all-org-abilities\");\n    }\n}\n```\n\n**Why `ExtendedCache` for org/provider abilities:**\n\n- **High-frequency reads**: Every permission check reads abilities\n- **Infrequent writes**: Abilities change rarely\n- **Eager refresh**: Automatically refreshes at 90% of TTL to prevent cache misses\n- **Fail-safe mode**: Serves stale data if database temporarily unavailable\n- **Redis backplane**: Automatically invalidates across all instances when abilities change\n- **No Service Bus dependency**: Simpler infrastructure (one Redis instead of Redis + Service Bus)\n\n### When NOT to Use\n\n- **Long-term persistent data** (days/weeks) - Use `IDistributedCache` with persistent keyed service for structured TTL support\n- **Custom caching logic** - If ExtendedCache's API doesn't fit your use case, consider specialized in-memory cache\n\n---\n\n## `IDistributedCache`\n\n`IDistributedCache` provides two service registrations for different use cases:\n\n1. **Default (unnamed) service** - For ephemeral, short-lived data\n2. **Persistent cache** (keyed service: `\"persistent\"`) - For longer-lived data with structured TTL\n\n### When to Use\n\n**Default `IDistributedCache`**:\n\n- **Legacy code** already using `IDistributedCache` (consider migrating to `ExtendedCache`)\n- **Third-party integrations** requiring `IDistributedCache` interface\n- **ASP.NET Core session storage** (framework dependency)\n- You have **specific requirements** that ExtendedCache doesn't support\n\n> **Note**: For new code, prefer `ExtendedCache` over default `IDistributedCache`. ExtendedCache can be configured with `EnableDistributedCache = false` to use memory-only caching with the same performance as raw `IMemoryCache`, while still providing stampede protection, fail-safe, and eager refresh.\n\n**Persistent cache** (keyed service: `\"persistent\"`):\n\n- **Critical data where memory loss would impact users** (refresh tokens, consent grants)\n- **Long-lived structured data** with automatic TTL (days to weeks)\n- **Long-lived OAuth/OIDC grants** that must survive application restarts\n- **Payment intents** or workflow state that spans multiple requests\n- Data requiring **automatic expiration** without manual cleanup\n- **Large cache datasets** that benefit from external storage (e.g., thousands of refresh tokens)\n\n### Pros\n\n✅ **Standard ASP.NET Core interface**: Widely understood, well-documented\n\n✅ **Multiple backend support**: Redis, SQL Server, Entity Framework, Cosmos DB\n\n✅ **Automatic backend selection**: Picks the right backend based on configuration\n\n✅ **Simple API**: Just `Get`, `Set`, `Remove`, `Refresh`\n\n✅ **Minimal overhead**: No additional layers beyond the backend\n\n✅ **Keyed services**: Separate configurations for different use cases\n\n### Cons\n\n❌ **No stampede protection**: Multiple requests = multiple backend calls\n\n❌ **No fail-safe mode**: Backend unavailable = cache miss\n\n❌ **No backplane**: Manual cache invalidation across instances\n\n❌ **Manual serialization**: You handle JSON serialization (or use helpers)\n\n❌ **Manual TTL management** (default service): Must track expiration manually\n\n### Example Usage: Default (Ephemeral Data)\n\n#### 1. Registration (already done in Api, Admin, Billing, Events, EventsProcessor, Identity, and Notifications Startup.cs files):\n\n```csharp\nservices.AddDistributedCache(globalSettings);\n```\n\n#### 2. Inject and use for short-lived tokens:\n\n```csharp\npublic class TwoFactorService\n{\n    private readonly IDistributedCache _cache;\n\n    public TwoFactorService(IDistributedCache cache)\n    {\n        _cache = cache;\n    }\n\n    public async Task<string> GetEmailTokenAsync(Guid userId)\n    {\n        var key = $\"email-2fa:{userId}\";\n        var cached = await _cache.GetStringAsync(key);\n        return cached;\n    }\n\n    public async Task SetEmailTokenAsync(Guid userId, string token)\n    {\n        var key = $\"email-2fa:{userId}\";\n        await _cache.SetStringAsync(key, token, new DistributedCacheEntryOptions\n        {\n            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)\n        });\n    }\n}\n```\n\n#### 3. Using JSON helpers:\n\n```csharp\nusing Bit.Core.Utilities;\n\npublic async Task<MyData> GetDataAsync(string key)\n{\n    return await _cache.TryGetValue<MyData>(key);\n}\n\npublic async Task SetDataAsync(string key, MyData data)\n{\n    await _cache.SetAsync(key, data, new DistributedCacheEntryOptions\n    {\n        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)\n    });\n}\n```\n\n### Example Usage: Persistent (Long-Lived Data)\n\nThe persistent cache is accessed via keyed service injection and is optimized for long-lived structured data with automatic TTL support.\n\n#### Specific Example: Payment Workflow State\n\nThe persistent `IDistributedCache` service is appropriate for workflow state that spans multiple requests and needs automatic TTL cleanup.\n\n```csharp\npublic class PaymentWorkflowCache(\n    [FromKeyedServices(\"persistent\")] IDistributedCache distributedCache) : IPaymentWorkflowCache\n{\n    public async Task SetPaymentSessionAsync(Guid userId, string sessionId)\n    {\n        // Bidirectional mapping for payment flow\n        var byUserIdCacheKey = $\"payment_session_for_user_{userId}\";\n        var bySessionIdCacheKey = $\"user_for_payment_session_{sessionId}\";\n\n        // Note: No explicit TTL set here. Cosmos DB uses container-level TTL for automatic cleanup.\n        // In cloud, Cosmos TTL handles expiration. In self-hosted, the cache backend manages TTL.\n        await Task.WhenAll(\n            distributedCache.SetStringAsync(byUserIdCacheKey, sessionId),\n            distributedCache.SetStringAsync(bySessionIdCacheKey, userId.ToString()));\n    }\n\n    public async Task<string?> GetPaymentSessionForUserAsync(Guid userId)\n    {\n        var cacheKey = $\"payment_session_for_user_{userId}\";\n        return await distributedCache.GetStringAsync(cacheKey);\n    }\n\n    public async Task<Guid?> GetUserForPaymentSessionAsync(string sessionId)\n    {\n        var cacheKey = $\"user_for_payment_session_{sessionId}\";\n        var value = await distributedCache.GetStringAsync(cacheKey);\n        if (string.IsNullOrEmpty(value) || !Guid.TryParse(value, out var userId))\n        {\n            return null;\n        }\n        return userId;\n    }\n\n    public async Task RemovePaymentSessionForUserAsync(Guid userId)\n    {\n        var cacheKey = $\"payment_session_for_user_{userId}\";\n        await distributedCache.RemoveAsync(cacheKey);\n    }\n}\n```\n\n#### Specific Example: Long-Lived OAuth Grants\n\nLong-lived OAuth grants (refresh tokens, consent grants, device codes) use the persistent `IDistributedCache` in **cloud** and `IGrantRepository` as a **database fallback for self-hosted** when persistent cache is not configured:\n\n**Cloud (Bitwarden-hosted)**:\n\n- Uses persistent `IDistributedCache` directly (backed by Cosmos DB)\n- Automatic TTL via Cosmos DB container-level TTL\n\n**Self-hosted**:\n\n- Uses `IGrantRepository` as a database fallback when persistent cache backend is not available\n- Stores grants in `Grant` database table with automatic expiration\n\n**Grant type recommendations:**\n\n| Grant Type               | Lifetime     | Durability Requirement | Recommended Storage | Rationale                                                                                   |\n| ------------------------ | ------------ | ---------------------- | ------------------- | ------------------------------------------------------------------------------------------- |\n| SSO authorization codes  | ≤5 min       | Ephemeral, can be lost | `ExtendedCache`     | User can re-initiate SSO flow if code is lost; short lifetime limits exposure window        |\n| OIDC authorization codes | ≤5 min       | Ephemeral, can be lost | `ExtendedCache`     | OAuth spec allows user to retry authorization; code is single-use and short-lived           |\n| PKCE code verifiers      | ≤5 min       | Ephemeral, can be lost | `ExtendedCache`     | Tied to authorization code lifecycle; can be regenerated if authorization is retried        |\n| Refresh tokens           | Days-weeks   | Must persist           | Persistent cache    | Losing these forces user re-authentication; critical for seamless user experience           |\n| Consent grants           | Weeks-months | Must persist           | Persistent cache    | User shouldn't have to re-consent frequently; loss degrades UX and trust                    |\n| Device codes             | Days         | Must persist           | Persistent cache    | Device flow is async; losing codes breaks pending device authorizations with no recovery UX |\n\n### Backend Configuration\n\nThe backend is automatically selected based on configuration and service key:\n\n#### Default `IDistributedCache` (ephemeral)\n\n**Cloud (Bitwarden-hosted)**:\n\n- **Redis** only (always configured in cloud environments)\n\n**Self-hosted priority order**:\n\n1. **Redis** (if `GlobalSettings.DistributedCache.Redis.ConnectionString` is configured)\n2. **SQL Server Cache table** (if database provider is SQL Server)\n3. **Entity Framework Cache table** (for PostgreSQL, MySQL, SQLite)\n\n#### Persistent cache (keyed service: `\"persistent\"`)\n\n**Cloud (Bitwarden-hosted)**:\n\n1. **Cosmos DB** (if `GlobalSettings.DistributedCache.Cosmos.ConnectionString` is configured)\n   - Database: `cache`\n   - Container: `default`\n2. **Falls back to Redis**\n\n**Self-hosted priority order**:\n\n1. **Redis** (if configured)\n2. **SQL Server Cache table** (if database provider is SQL Server)\n3. **Entity Framework Cache table** (for PostgreSQL, MySQL, SQLite)\n\n### Backend Details\n\n#### Redis\n\n```csharp\nservices.AddStackExchangeRedisCache(options =>\n{\n    options.Configuration = globalSettings.DistributedCache.Redis.ConnectionString;\n});\n```\n\n**Used for**: Cloud (always), self-hosted (if configured)\n\n- **Pros**: Fast, horizontally scalable, battle-tested\n- **Cons**: Additional infrastructure dependency (self-hosted only)\n- **TTL**: Via `AbsoluteExpiration` in cache entry options\n\n#### SQL Server Cache Table (Self-hosted only)\n\n```csharp\nservices.AddDistributedSqlServerCache(options =>\n{\n    options.ConnectionString = globalSettings.SqlServer.ConnectionString;\n    options.SchemaName = \"dbo\";\n    options.TableName = \"Cache\";\n});\n```\n\n**Used for**: Self-hosted deployments without Redis\n\n- **Pros**: No additional infrastructure, works with existing database\n- **Cons**: Slower than Redis, adds load to database, less scalable\n- **TTL**: Via `ExpiresAtTime` and `AbsoluteExpiration` columns\n\n#### Entity Framework Cache (Self-hosted only)\n\n```csharp\nservices.AddSingleton<IDistributedCache, EntityFrameworkCache>();\n```\n\n**Used for**: Self-hosted deployments with PostgreSQL, MySQL, or SQLite\n\n- **Pros**: Works with any EF-supported database (PostgreSQL, MySQL, SQLite)\n- **Cons**: Slower than Redis, requires periodic expiration scanning, adds DB load\n\n**Features**:\n\n- Thread-safe operations with mutex locks\n- Automatic expiration scanning every 30 minutes\n- Sliding and absolute expiration support\n- Provider-specific duplicate key handling\n\n**TTL**: Via `ExpiresAtTime` and `AbsoluteExpiration` columns with background scanning\n\n#### Cosmos DB (Cloud only, persistent cache)\n\n```csharp\nservices.AddKeyedSingleton<IDistributedCache, CosmosCache>(\"persistent\", (provider, _) =>\n{\n    return new CosmosCache(new CosmosCacheOptions\n    {\n        DatabaseName = \"cache\",\n        ContainerName = \"default\",\n        ClientBuilder = cosmosClientBuilder\n    });\n});\n```\n\n**Used for**: Cloud persistent keyed service only\n\n- **Pros**: Globally distributed, automatic TTL support via container-level TTL, optimized for long-lived data\n- **Cons**: Cloud-only, higher latency than Redis\n\n**TTL**: Cosmos DB container-level TTL (automatic cleanup, no scanning required)\n\n### Comparison: Default vs Persistent\n\n| Characteristic          | Default                        | Persistent cache (`\"persistent\"`)              |\n| ----------------------- | ------------------------------ | ---------------------------------------------- |\n| **Primary Use Case**    | Ephemeral tokens, session data | Long-lived grants, workflow state              |\n| **Typical TTL**         | 5-15 minutes                   | Hours to weeks                                 |\n| **User Impact if Lost** | Low (user can retry)           | High (forces re-auth, interrupts workflows)    |\n| **Scale Consideration** | Small datasets                 | Large/growing datasets (thousands to millions) |\n| **Cloud Backend**       | Redis                          | Cosmos DB → Redis                              |\n| **Self-Hosted Backend** | Redis → SQL → EF               | Redis → SQL → EF                               |\n| **Automatic Cleanup**   | Manual expiration              | Automatic TTL (Cosmos)                         |\n| **Data Structure**      | Simple key-value               | Supports structured data                       |\n| **Example**             | 2FA codes, TOTP tokens         | Refresh tokens, payment intents                |\n\n### Choosing Default vs Persistent\n\n**Use Default when**:\n\n- Data lifetime < 15 minutes\n- Ephemeral authentication tokens\n- Simple key-value pairs\n- Cost optimization is important\n- Data loss on restart is acceptable\n\n**Use Persistent when**:\n\n- **Data loss would have user impact** (e.g., losing refresh tokens forces re-authentication)\n- Data lifetime > 15 minutes\n- **Cache size is large or growing** (thousands of items that exceed memory constraints)\n- Structured data with relationships\n- Automatic TTL cleanup is required\n- Data must survive restarts and deployments\n- Query capabilities are needed (via Cosmos DB)\n\n### When NOT to Use\n\n- **New general-purpose caching** - Use `ExtendedCache` instead for stampede protection, fail-safe, and backplane support\n- **Organization/Provider abilities** - Use `ExtendedCache` with preloading pattern (see example above)\n- **Short-lived ephemeral data** without persistence requirements - Use `ExtendedCache` (simpler, more features)\n\n---\n\n## `IApplicationCacheService` (Deprecated)\n\n> **⚠️ Deprecated**: This service is being phased out in favor of `ExtendedCache`. New code should use `ExtendedCache` with the preloading pattern shown in the [Organization/Provider Abilities example](#specific-example-organizationprovider-abilities) above.\n\n### Background\n\n`IApplicationCacheService` was a **highly domain-specific caching service** built for Bitwarden organization and provider abilities. It used in-memory cache with Azure Service Bus for cross-instance invalidation.\n\n**Why it's being replaced:**\n\n- **Infrastructure complexity**: Required both Redis and Azure Service Bus\n- **Limited applicability**: Only worked for org/provider abilities\n- **Maintenance burden**: Custom implementation instead of leveraging standard caching primitives\n- **Better alternative exists**: `ExtendedCache` with Redis backplane provides the same functionality with simpler infrastructure\n\n### Migration Path\n\n**Old approach** (IApplicationCacheService):\n\n- In-memory cache with periodic refresh\n- Azure Service Bus for cross-instance invalidation\n- Custom implementation for each domain\n\n**New approach** (ExtendedCache):\n\n- Memory + Redis distributed cache with backplane\n- Eager refresh for automatic background updates\n- Fail-safe mode for resilience\n- Standard FusionCache API\n- One Redis instance instead of Redis + Service Bus\n\nSee the [Organization/Provider Abilities example](#specific-example-organizationprovider-abilities) for the recommended migration pattern.\n\n### When NOT to Use\n\n❌ **Do not use for new code** - Use `ExtendedCache` instead\n\nFor existing code using `IApplicationCacheService`, plan migration to `ExtendedCache` using the pattern shown above.\n\n---\n\n## Specialized In-Memory Cache\n\n> **Recommendation**: In most cases, use `ExtendedCache` with `EnableDistributedCache = false` instead of implementing a specialized in-memory cache. ExtendedCache provides the same memory-only performance with built-in stampede protection, eager refresh, and fail-safe capabilities.\n\n### When to Use\n\nUse a specialized in-memory cache only when:\n\n- **ExtendedCache's API doesn't fit** your specific use case\n- **Custom eviction logic** is required beyond TTL-based expiration\n- **Non-standard data structures** (e.g., priority queues, LRU with custom scoring)\n- **Direct memory access patterns** that bypass serialization entirely\n\nFor general high-performance caching, prefer `ExtendedCache` with memory-only mode.\n\n### Pros\n\n✅ **Maximum performance**: No serialization, no network calls, no locking overhead\n\n✅ **Simple implementation**: Just a `Dictionary` or `ConcurrentDictionary`\n\n✅ **Zero infrastructure**: No Redis, no database, no additional dependencies\n\n### Cons\n\n❌ **No horizontal scaling**: Each instance has separate cache state\n\n❌ **Manual invalidation**: No built-in cache invalidation mechanism\n\n❌ **Manual TTL**: You implement expiration logic\n\n❌ **Memory pressure**: Large datasets can cause GC issues\n\n### Example Implementation\n\n#### Simple in-memory cache:\n\n```csharp\npublic class MyFeatureCache\n{\n    private readonly ConcurrentDictionary<string, CacheEntry<MyData>> _cache = new();\n    private readonly TimeSpan _defaultExpiration = TimeSpan.FromMinutes(30);\n\n    public MyData GetOrAdd(string key, Func<MyData> factory)\n    {\n        var entry = _cache.GetOrAdd(key, _ => new CacheEntry<MyData>\n        {\n            Value = factory(),\n            ExpiresAt = DateTime.UtcNow + _defaultExpiration\n        });\n\n        // WARNING: This implementation has a race condition. Multiple threads detecting\n        // expiration simultaneously may each call TryRemove and then recursively call\n        // GetOrAdd, potentially causing the factory to execute multiple times. For\n        // production use cases requiring thread-safe expiration, consider using\n        // IMemoryCache with GetOrCreateAsync or ExtendedCache with stampede protection.\n        if (entry.ExpiresAt < DateTime.UtcNow)\n        {\n            _cache.TryRemove(key, out _);\n            return GetOrAdd(key, factory);\n        }\n\n        return entry.Value;\n    }\n\n    private class CacheEntry<T>\n    {\n        public T Value { get; set; }\n        public DateTime ExpiresAt { get; set; }\n    }\n}\n```\n\n#### Using `IMemoryCache`:\n\n```csharp\npublic class MyService\n{\n    private readonly IMemoryCache _memoryCache;\n\n    public MyService(IMemoryCache memoryCache)\n    {\n        _memoryCache = memoryCache;\n    }\n\n    public async Task<MyData> GetDataAsync(string key)\n    {\n        return await _memoryCache.GetOrCreateAsync(key, async entry =>\n        {\n            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);\n            entry.SetPriority(CacheItemPriority.High);\n\n            return await _repository.GetDataAsync(key);\n        });\n    }\n}\n```\n\n### When NOT to Use\n\n- **Most general-purpose caching** - Use `ExtendedCache` with memory-only mode instead\n- **Data requiring stampede protection** - Use `ExtendedCache`\n- **Multi-instance deployments** requiring consistency - Use `ExtendedCache` with Redis\n- **Long-lived OAuth grants** - Use persistent `IDistributedCache`\n\n> **Important**: Before implementing a custom in-memory cache, first try `ExtendedCache` with `EnableDistributedCache = false`. This gives you memory-only performance with automatic stampede protection, eager refresh, and fail-safe mode.\n\n---\n\n## Backend Configuration\n\n### Configuration Priority\n\nThe following table shows how different caching options resolve to storage backends based on configuration:\n\n| Cache Option                           | Cloud Backend             | Self-Hosted Backend         | Config Setting                                            |\n| -------------------------------------- | ------------------------- | --------------------------- | --------------------------------------------------------- |\n| **ExtendedCache**                      | Redis → Memory            | Redis → Memory              | `GlobalSettings.DistributedCache.Redis.ConnectionString`  |\n| **IDistributedCache** (default)        | Redis                     | Redis → SQL → EF            | `GlobalSettings.DistributedCache.Redis.ConnectionString`  |\n| **IDistributedCache** (`\"persistent\"`) | Cosmos → Redis            | Redis → SQL → EF            | `GlobalSettings.DistributedCache.Cosmos.ConnectionString` |\n| **OAuth Grants** (long-lived)          | Persistent cache (Cosmos) | `IGrantRepository` (SQL/EF) | Various (see above)                                       |\n\n### Redis Configuration\n\n**Cloud (Bitwarden-hosted)**:\n\n```json\n{\n  \"GlobalSettings\": {\n    \"DistributedCache\": {\n      \"Redis\": {\n        \"ConnectionString\": \"redis.example.com:6379,ssl=true,password=...\"\n      }\n    }\n  }\n}\n```\n\n**Self-hosted** (`appsettings.json`):\n\n```json\n{\n  \"globalSettings\": {\n    \"distributedCache\": {\n      \"redis\": {\n        \"connectionString\": \"localhost:6379\"\n      }\n    }\n  }\n}\n```\n\n### Cosmos DB Configuration\n\n**Persistent `IDistributedCache`** (cloud only):\n\n```json\n{\n  \"GlobalSettings\": {\n    \"DistributedCache\": {\n      \"Cosmos\": {\n        \"ConnectionString\": \"AccountEndpoint=https://...;AccountKey=...\"\n      }\n    }\n  }\n}\n```\n\n- Database: `cache`\n- Container: `default`\n- Used for long-lived grants in cloud deployments\n\n### SQL Server Cache\n\n**Automatic configuration** (if SQL Server is database provider):\n\n```json\n{\n  \"globalSettings\": {\n    \"sqlServer\": {\n      \"connectionString\": \"Server=...;Database=...;User Id=...;Password=...\"\n    }\n  }\n}\n```\n\n- Schema: `dbo`\n- Table: `Cache`\n- Migrations: Applied automatically\n\n### Entity Framework Cache\n\n**Automatic fallback** for PostgreSQL, MySQL, SQLite:\n\nNo additional configuration required. Uses existing database connection.\n\n- Table: `Cache`\n- Migrations: Applied automatically\n\n---\n\n## Performance Considerations\n\n### Performance Characteristics\n\n| Backend              | Read Latency | Write Latency | Throughput    |\n| -------------------- | ------------ | ------------- | ------------- |\n| **Memory**           | <1ms         | <1ms          | >100K req/s   |\n| **Redis**            | 1-5ms        | 1-5ms         | 10K-50K req/s |\n| **SQL Server**       | 5-20ms       | 10-50ms       | 1K-5K req/s   |\n| **Entity Framework** | 5-20ms       | 10-50ms       | 1K-5K req/s   |\n| **Cosmos DB**        | 5-15ms       | 5-15ms        | 10K+ req/s    |\n\n**Note**: Latencies represent typical p95 values in production environments. Redis latencies assume same-datacenter deployment and include serialization overhead. Actual performance varies based on network topology, data size, and load.\n\n### Recommendations\n\n**For high-frequency reads (>1K req/s)**:\n\n1. `ExtendedCache` with Redis (cloud)\n2. `ExtendedCache` memory-only (self-hosted, single instance)\n3. Specialized in-memory cache (extreme performance requirements)\n\n**For moderate traffic (100-1K req/s)**:\n\n1. `ExtendedCache` with shared Redis\n2. `IDistributedCache` with SQL Server cache\n\n**For low traffic (<100 req/s)**:\n\n1. `IDistributedCache` with SQL Server / EF cache\n2. `ExtendedCache` memory-only\n\n---\n\n## Testing Caches\n\n### Unit Testing\n\n**`ExtendedCache`**:\n\n```csharp\n[Fact]\npublic async Task TestCacheHit()\n{\n    var services = new ServiceCollection();\n    services.AddMemoryCache();\n    services.AddExtendedCache(\"TestCache\", new GlobalSettings\n    {\n        DistributedCache = new GlobalSettings.DistributedCacheSettings()\n    });\n\n    var provider = services.BuildServiceProvider();\n    var cache = provider.GetRequiredKeyedService<IFusionCache>(\"TestCache\");\n\n    await cache.SetAsync(\"key\", \"value\");\n    var result = await cache.GetOrDefaultAsync<string>(\"key\");\n\n    Assert.Equal(\"value\", result);\n}\n```\n\n**`IDistributedCache`**:\n\n```csharp\n[Fact]\npublic async Task TestDistributedCache()\n{\n    var cache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()));\n\n    await cache.SetStringAsync(\"key\", \"value\");\n    var result = await cache.GetStringAsync(\"key\");\n\n    Assert.Equal(\"value\", result);\n}\n```\n\n### Integration Testing\n\n**Example**:\n\n```csharp\n[DatabaseTheory, DatabaseData]\npublic async Task Cache_ExpirationScanning_RemovesExpiredItems(IDistributedCache cache)\n{\n    // Set item with 1-second expiration\n    await cache.SetAsync(\"key\", Encoding.UTF8.GetBytes(\"value\"), new DistributedCacheEntryOptions\n    {\n        AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(1)\n    });\n\n    // Wait for expiration\n    await Task.Delay(TimeSpan.FromSeconds(2));\n\n    // Trigger expiration scan\n    var entityCache = cache as EntityFrameworkCache;\n    await entityCache.ScanForExpiredItemsAsync();\n\n    // Verify item is removed\n    var result = await cache.GetAsync(\"key\");\n    Assert.Null(result);\n}\n```\n\n---\n\n## Migration Examples\n\nExamples of migrating from one caching option to another:\n\n### From `IDistributedCache` → `ExtendedCache`\n\n**Before**:\n\n```csharp\n// Registration\nservices.AddDistributedCache(globalSettings);\n\n// Constructor\npublic MyService(IDistributedCache cache, IRepository repository)\n{\n    _cache = cache;\n    _repository = repository;\n}\n\n// Usage\npublic async Task<MyData> GetDataAsync(string key)\n{\n    var data = await _cache.TryGetValue<MyData>(key);\n    if (data == null)\n    {\n        data = await _repository.GetAsync(key);\n        await _cache.SetAsync(key, data, new DistributedCacheEntryOptions\n        {\n            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)\n        });\n    }\n    return data;\n}\n```\n\n**After**:\n\n```csharp\n// Registration\nservices.AddDistributedCache(globalSettings);\nservices.AddExtendedCache(\"MyFeature\", globalSettings);\n\n// Constructor\npublic MyService(\n    [FromKeyedServices(\"MyFeature\")] IFusionCache cache,\n    IRepository repository)\n{\n    _cache = cache;\n    _repository = repository;\n}\n\n// Usage\npublic async Task<MyData> GetDataAsync(string key)\n{\n    return await _cache.GetOrSetAsync(\n        key,\n        async _ => await _repository.GetAsync(key),\n        options => options.SetDuration(TimeSpan.FromMinutes(30))\n    );\n}\n```\n\n### From In-Memory → `ExtendedCache`\n\n**Before**:\n\n```csharp\n// Field\nprivate readonly ConcurrentDictionary<string, MyData> _cache = new();\nprivate readonly IRepository _repository;\n\n// Constructor\npublic MyService(IRepository repository)\n{\n    _repository = repository;\n}\n\n// Usage\npublic async Task<MyData> GetDataAsync(string key)\n{\n    if (_cache.TryGetValue(key, out var cached))\n    {\n        return cached;\n    }\n\n    var data = await _repository.GetAsync(key);\n    _cache.TryAdd(key, data);\n    return data;\n}\n```\n\n**After**:\n\n```csharp\n// Registration\nservices.AddExtendedCache(\"MyFeature\", globalSettings);\n\n// Constructor\npublic MyService(\n    [FromKeyedServices(\"MyFeature\")] IFusionCache cache,\n    IRepository repository)\n{\n    _cache = cache;\n    _repository = repository;\n}\n\n// Usage\npublic async Task<MyData> GetDataAsync(string key)\n{\n    return await _cache.GetOrSetAsync(\n        key,\n        async _ => await _repository.GetAsync(key)\n    );\n}\n```\n"
  },
  {
    "path": "src/Core/Utilities/ClaimsExtensions.cs",
    "content": "﻿using System.Security.Claims;\n\nnamespace Bit.Core.Utilities;\n\npublic static class ClaimsExtensions\n{\n    public static bool HasSsoIdP(this IEnumerable<Claim> claims)\n    {\n        return claims.Any(c => c.Type == \"idp\" && c.Value == \"sso\");\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/CoreHelpers.cs",
    "content": "﻿#nullable enable\n\nusing System.Diagnostics.CodeAnalysis;\nusing System.Globalization;\nusing System.Reflection;\nusing System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing System.Text;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Web;\nusing Azure;\nusing Azure.Storage.Blobs;\nusing Azure.Storage.Blobs.Models;\nusing Azure.Storage.Queues.Models;\nusing Bit.Core.AdminConsole.Context;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Settings;\nusing Duende.IdentityModel;\nusing Microsoft.AspNetCore.DataProtection;\nusing MimeKit;\n\nnamespace Bit.Core.Utilities;\n\npublic static class CoreHelpers\n{\n    private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks;\n    private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);\n    private static readonly DateTime _max = new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc);\n    private static readonly Random _random = new Random();\n    private static readonly string RealConnectingIp = \"X-Connecting-IP\";\n    private static readonly Regex _whiteSpaceRegex = new Regex(@\"\\s+\");\n    private static readonly JsonSerializerOptions _jsonSerializerOptions = new()\n    {\n        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n    };\n\n    /// <summary>\n    /// Generate a sequential Guid for Sql Server. This prevents SQL Server index fragmentation by incorporating timestamp\n    /// information for sequential ordering. This should be preferred to <see cref=\"Guid.NewGuid\"/> for any database IDs.\n    /// </summary>\n    /// <remarks>\n    /// ref: https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs\n    /// </remarks>\n    /// <returns>A comb Guid.</returns>\n    public static Guid GenerateComb()\n        => GenerateComb(Guid.NewGuid(), DateTime.UtcNow);\n\n    /// <summary>\n    /// Implementation of <see cref=\"GenerateComb()\" /> with input parameters to remove randomness.\n    /// This should NOT be used outside of testing.\n    /// </summary>\n    /// <remarks>\n    /// You probably don't want to use this method and instead want to use <see cref=\"GenerateComb()\" /> with no parameters\n    /// </remarks>\n    internal static Guid GenerateComb(Guid startingGuid, DateTime time)\n    {\n        var guidArray = startingGuid.ToByteArray();\n\n        // Get the days and milliseconds which will be used to build the byte string\n        var days = new TimeSpan(time.Ticks - _baseDateTicks);\n        var msecs = time.TimeOfDay;\n\n        // Convert to a byte array\n        // Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333\n        var daysArray = BitConverter.GetBytes(days.Days);\n        var msecsArray = BitConverter.GetBytes((long)(msecs.TotalMilliseconds / 3.333333));\n\n        // Reverse the bytes to match SQL Servers ordering\n        Array.Reverse(daysArray);\n        Array.Reverse(msecsArray);\n\n        // Copy the bytes into the guid\n        Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2);\n        Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4);\n\n        return new Guid(guidArray);\n    }\n\n    internal static DateTime DateFromComb(Guid combGuid)\n    {\n        var guidArray = combGuid.ToByteArray();\n        var daysArray = new byte[4];\n        var msecsArray = new byte[4];\n\n        Array.Copy(guidArray, guidArray.Length - 6, daysArray, 2, 2);\n        Array.Copy(guidArray, guidArray.Length - 4, msecsArray, 0, 4);\n\n        Array.Reverse(daysArray);\n        Array.Reverse(msecsArray);\n\n        var days = BitConverter.ToInt32(daysArray, 0);\n        var msecs = BitConverter.ToInt32(msecsArray, 0);\n\n        var time = TimeSpan.FromDays(days) + TimeSpan.FromMilliseconds(msecs * 3.333333);\n        return new DateTime(_baseDateTicks + time.Ticks, DateTimeKind.Utc);\n    }\n\n    internal static long BinForComb(Guid combGuid, int binCount)\n    {\n        // From System.Web.Util.HashCodeCombiner\n        uint CombineHashCodes(uint h1, byte h2)\n        {\n            return (uint)(((h1 << 5) + h1) ^ h2);\n        }\n        var guidArray = combGuid.ToByteArray();\n        var randomArray = new byte[10];\n        Array.Copy(guidArray, 0, randomArray, 0, 10);\n        var hash = randomArray.Aggregate((uint)randomArray.Length, CombineHashCodes);\n        return hash % binCount;\n    }\n\n    public static string CleanCertificateThumbprint(string thumbprint)\n    {\n        // Clean possible garbage characters from thumbprint copy/paste\n        // ref http://stackoverflow.com/questions/8448147/problems-with-x509store-certificates-find-findbythumbprint\n        return Regex.Replace(thumbprint, @\"[^\\da-fA-F]\", string.Empty).ToUpper();\n    }\n\n    public static X509Certificate2? GetCertificate(string thumbprint)\n    {\n        thumbprint = CleanCertificateThumbprint(thumbprint);\n\n        X509Certificate2? cert = null;\n        var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);\n        certStore.Open(OpenFlags.ReadOnly);\n        var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);\n        if (certCollection.Count > 0)\n        {\n            cert = certCollection[0];\n        }\n\n        certStore.Close();\n        return cert;\n    }\n\n    public static X509Certificate2 GetCertificate(string file, string password)\n    {\n        return new X509Certificate2(file, password);\n    }\n\n    public async static Task<X509Certificate2> GetEmbeddedCertificateAsync(string file, string password)\n    {\n        var assembly = typeof(CoreHelpers).GetTypeInfo().Assembly;\n        using (var s = assembly.GetManifestResourceStream($\"Bit.Core.{file}\")!)\n        using (var ms = new MemoryStream())\n        {\n            await s.CopyToAsync(ms);\n            return new X509Certificate2(ms.ToArray(), password);\n        }\n    }\n\n    public static string GetEmbeddedResourceContentsAsync(string file)\n    {\n        var assembly = Assembly.GetCallingAssembly();\n        var resourceName = assembly.GetManifestResourceNames().Single(n => n.EndsWith(file));\n        using (var stream = assembly.GetManifestResourceStream(resourceName)!)\n        using (var reader = new StreamReader(stream))\n        {\n            return reader.ReadToEnd();\n        }\n    }\n\n    public async static Task<X509Certificate2?> GetBlobCertificateAsync(string connectionString, string container, string file, string password)\n    {\n        try\n        {\n            var blobServiceClient = new BlobServiceClient(connectionString);\n            var containerRef2 = blobServiceClient.GetBlobContainerClient(container);\n            var blobRef = containerRef2.GetBlobClient(file);\n\n            using var memStream = new MemoryStream();\n            await blobRef.DownloadToAsync(memStream).ConfigureAwait(false);\n            return new X509Certificate2(memStream.ToArray(), password);\n        }\n        catch (RequestFailedException ex)\n        when (ex.ErrorCode == BlobErrorCode.ContainerNotFound || ex.ErrorCode == BlobErrorCode.BlobNotFound)\n        {\n            return null;\n        }\n        catch (Exception)\n        {\n            return null;\n        }\n    }\n\n    public static long ToEpocMilliseconds(DateTime date)\n    {\n        return (long)Math.Round((date - _epoc).TotalMilliseconds, 0);\n    }\n\n    public static DateTime FromEpocMilliseconds(long milliseconds)\n    {\n        return _epoc.AddMilliseconds(milliseconds);\n    }\n\n    public static long ToEpocSeconds(DateTime date)\n    {\n        return (long)Math.Round((date - _epoc).TotalSeconds, 0);\n    }\n\n    public static DateTime FromEpocSeconds(long seconds)\n    {\n        return _epoc.AddSeconds(seconds);\n    }\n\n    public static string U2fAppIdUrl(GlobalSettings globalSettings)\n    {\n        return string.Concat(globalSettings.BaseServiceUri.Vault, \"/app-id.json\");\n    }\n\n    public static string RandomString(int length, bool alpha = true, bool upper = true, bool lower = true,\n        bool numeric = true, bool special = false)\n    {\n        return RandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));\n    }\n\n    public static string RandomString(int length, string characters)\n    {\n        return new string(Enumerable.Repeat(characters, length).Select(s => s[_random.Next(s.Length)]).ToArray());\n    }\n\n    public static string SecureRandomString(int length, bool alpha = true, bool upper = true, bool lower = true,\n        bool numeric = true, bool special = false)\n    {\n        return SecureRandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));\n    }\n\n    // ref https://stackoverflow.com/a/8996788/1090359 with modifications\n    public static string SecureRandomString(int length, string characters)\n    {\n        if (length < 0)\n        {\n            throw new ArgumentOutOfRangeException(nameof(length), \"length cannot be less than zero.\");\n        }\n\n        if (string.IsNullOrEmpty(characters))\n        {\n            throw new ArgumentOutOfRangeException(nameof(characters), \"characters invalid.\");\n        }\n\n        const int byteSize = 0x100;\n        if (byteSize < characters.Length)\n        {\n            throw new ArgumentException(\n                string.Format(\"{0} may contain no more than {1} characters.\", nameof(characters), byteSize),\n                nameof(characters));\n        }\n\n        var outOfRangeStart = byteSize - (byteSize % characters.Length);\n        using (var rng = RandomNumberGenerator.Create())\n        {\n            var sb = new StringBuilder();\n            var buffer = new byte[128];\n            while (sb.Length < length)\n            {\n                rng.GetBytes(buffer);\n                for (var i = 0; i < buffer.Length && sb.Length < length; ++i)\n                {\n                    // Divide the byte into charSet-sized groups. If the random value falls into the last group and the\n                    // last group is too small to choose from the entire allowedCharSet, ignore the value in order to\n                    // avoid biasing the result.\n                    if (outOfRangeStart <= buffer[i])\n                    {\n                        continue;\n                    }\n\n                    sb.Append(characters[buffer[i] % characters.Length]);\n                }\n            }\n\n            return sb.ToString();\n        }\n    }\n\n    private static string RandomStringCharacters(bool alpha, bool upper, bool lower, bool numeric, bool special)\n    {\n        var characters = string.Empty;\n        if (alpha)\n        {\n            if (upper)\n            {\n                characters += \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\n            }\n\n            if (lower)\n            {\n                characters += \"abcdefghijklmnopqrstuvwxyz\";\n            }\n        }\n\n        if (numeric)\n        {\n            characters += \"0123456789\";\n        }\n\n        if (special)\n        {\n            characters += \"!@#$%^*&\";\n        }\n\n        return characters;\n    }\n\n    // ref: https://stackoverflow.com/a/11124118/1090359\n    // Returns the human-readable file size for an arbitrary 64-bit file size .\n    // The format is \"0.## XB\", ex: \"4.2 KB\" or \"1.43 GB\"\n    public static string ReadableBytesSize(long size)\n    {\n        // Get absolute value\n        var absoluteSize = (size < 0 ? -size : size);\n\n        // Determine the suffix and readable value\n        string suffix;\n        double readable;\n        if (absoluteSize >= 0x40000000) // 1 Gigabyte\n        {\n            suffix = \"GB\";\n            readable = (size >> 20);\n        }\n        else if (absoluteSize >= 0x100000) // 1 Megabyte\n        {\n            suffix = \"MB\";\n            readable = (size >> 10);\n        }\n        else if (absoluteSize >= 0x400) // 1 Kilobyte\n        {\n            suffix = \"KB\";\n            readable = size;\n        }\n        else\n        {\n            return size.ToString(\"0 Bytes\"); // Byte\n        }\n\n        // Divide by 1024 to get fractional value\n        readable = (readable / 1024);\n\n        // Return formatted number with suffix\n        return readable.ToString(\"0.## \") + suffix;\n    }\n\n    /// <summary>\n    /// Creates a clone of the given object through serializing to json and deserializing.\n    /// This method is subject to the limitations of System.Text.Json. For example, properties with\n    /// inaccessible setters will not be set.\n    /// </summary>\n    public static T CloneObject<T>(T obj)\n    {\n        return JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(obj))!;\n    }\n\n    public static bool SettingHasValue([NotNullWhen(true)] string? setting)\n    {\n        var normalizedSetting = setting?.ToLowerInvariant();\n        return !string.IsNullOrWhiteSpace(normalizedSetting) && !normalizedSetting.Equals(\"secret\") &&\n            !normalizedSetting.Equals(\"replace\");\n    }\n\n    public static string Base64EncodeString(string input)\n    {\n        return Convert.ToBase64String(Encoding.UTF8.GetBytes(input));\n    }\n\n    public static string Base64DecodeString(string input)\n    {\n        return Encoding.UTF8.GetString(Convert.FromBase64String(input));\n    }\n\n    public static string Base64UrlEncodeString(string input)\n    {\n        return Base64UrlEncode(Encoding.UTF8.GetBytes(input));\n    }\n\n    public static string Base64UrlDecodeString(string input)\n    {\n        return Encoding.UTF8.GetString(Base64UrlDecode(input));\n    }\n\n    /// <summary>\n    /// Encodes a Base64 URL formatted string.\n    /// </summary>\n    /// <param name=\"input\">Byte data</param>\n    /// <returns>Base64 URL formatted string</returns>\n    public static string Base64UrlEncode(byte[] input)\n    {\n        // Standard base64 encoder\n        var standardB64 = Convert.ToBase64String(input);\n        return TransformToBase64Url(standardB64);\n    }\n\n    /// <summary>\n    /// Transforms a Base64 standard formatted string to a Base64 URL formatted string.\n    /// </summary>\n    /// <param name=\"input\">Base64 standard formatted string</param>\n    /// <returns>Base64 URL formatted string</returns>\n    public static string TransformToBase64Url(string input)\n    {\n        var output = input\n            .Replace('+', '-')\n            .Replace('/', '_')\n            .Replace(\"=\", string.Empty);\n        return output;\n    }\n\n    /// <summary>\n    /// Decodes a Base64 URL formatted string.\n    /// </summary>\n    /// <param name=\"input\">Base64 URL formatted string</param>\n    /// <returns>Data as bytes</returns>\n    public static byte[] Base64UrlDecode(string input)\n    {\n        var standardB64 = TransformFromBase64Url(input);\n        // Standard base64 decoder\n        return Convert.FromBase64String(standardB64);\n    }\n\n    /// <summary>\n    /// Transforms a Base64 URL formatted string to a Base64 standard formatted string.\n    /// </summary>\n    /// <param name=\"input\">Base64 URL formatted string</param>\n    /// <returns>Base64 standard formatted string</returns>\n    public static string TransformFromBase64Url(string input)\n    {\n        // TODO: .NET 9 Ships Base64Url in box, investigate replacing this usage with that\n        // Ref: https://github.com/dotnet/runtime/pull/102364\n        var output = input;\n        // 62nd char of encoding\n        output = output.Replace('-', '+');\n        // 63rd char of encoding\n        output = output.Replace('_', '/');\n        // Pad with trailing '='s\n        switch (output.Length % 4)\n        {\n            case 0:\n                // No pad chars in this case\n                break;\n            case 2:\n                // Two pad chars\n                output += \"==\"; break;\n            case 3:\n                // One pad char\n                output += \"=\"; break;\n            default:\n                throw new InvalidOperationException(\"Illegal base64url string!\");\n        }\n\n        // Standard base64 string output\n        return output;\n    }\n\n    [return: NotNullIfNotNull(nameof(text))]\n    public static string? PunyEncode(string? text)\n    {\n        if (text == \"\")\n        {\n            return \"\";\n        }\n\n        if (text == null)\n        {\n            return null;\n        }\n\n        if (!text.Contains(\"@\"))\n        {\n            // Assume domain name or non-email address\n            var idn = new IdnMapping();\n            return idn.GetAscii(text);\n        }\n        else\n        {\n            // Assume email address\n            return MailboxAddress.EncodeAddrspec(text);\n        }\n    }\n\n    public static string? FormatLicenseSignatureValue(object val)\n    {\n        if (val == null)\n        {\n            return string.Empty;\n        }\n\n        if (val is DateTime dateTimeVal)\n        {\n            return ToEpocSeconds(dateTimeVal).ToString();\n        }\n\n        if (val is bool boolVal)\n        {\n            return boolVal.ToString().ToLowerInvariant();\n        }\n\n        if (val is PlanType planType)\n        {\n            return planType switch\n            {\n                PlanType.Free => \"Free\",\n                PlanType.FamiliesAnnually2019 => \"FamiliesAnnually\",\n                PlanType.TeamsMonthly2019 => \"TeamsMonthly\",\n                PlanType.TeamsAnnually2019 => \"TeamsAnnually\",\n                PlanType.EnterpriseMonthly2019 => \"EnterpriseMonthly\",\n                PlanType.EnterpriseAnnually2019 => \"EnterpriseAnnually\",\n                PlanType.Custom => \"Custom\",\n                _ => ((byte)planType).ToString(),\n            };\n        }\n\n        return val.ToString();\n    }\n\n    public static string SanitizeForEmail(string value, bool htmlEncode = true)\n    {\n        var cleanedValue = value.Replace(\"@\", \"[at]\");\n        var regexOptions = RegexOptions.CultureInvariant |\n            RegexOptions.Singleline |\n            RegexOptions.IgnoreCase;\n        cleanedValue = Regex.Replace(cleanedValue, @\"(\\.\\w)\",\n                m => string.Concat(\"[dot]\", m.ToString().Last()), regexOptions);\n        while (Regex.IsMatch(cleanedValue, @\"((^|\\b)(\\w*)://)\", regexOptions))\n        {\n            cleanedValue = Regex.Replace(cleanedValue, @\"((^|\\b)(\\w*)://)\",\n                string.Empty, regexOptions);\n        }\n        return htmlEncode ? HttpUtility.HtmlEncode(cleanedValue) : cleanedValue;\n    }\n\n    public static string DateTimeToTableStorageKey(DateTime? date = null)\n    {\n        if (date.HasValue)\n        {\n            date = date.Value.ToUniversalTime();\n        }\n        else\n        {\n            date = DateTime.UtcNow;\n        }\n\n        return _max.Subtract(date.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture);\n    }\n\n    // ref: https://stackoverflow.com/a/27545010/1090359\n    public static Uri ExtendQuery(Uri uri, IDictionary<string, string> values)\n    {\n        var baseUri = uri.ToString();\n        var queryString = string.Empty;\n        if (baseUri.Contains(\"?\"))\n        {\n            var urlSplit = baseUri.Split('?');\n            baseUri = urlSplit[0];\n            queryString = urlSplit.Length > 1 ? urlSplit[1] : string.Empty;\n        }\n\n        var queryCollection = HttpUtility.ParseQueryString(queryString);\n        foreach (var kvp in values ?? new Dictionary<string, string>())\n        {\n            queryCollection[kvp.Key] = kvp.Value;\n        }\n\n        var uriKind = uri.IsAbsoluteUri ? UriKind.Absolute : UriKind.Relative;\n        if (queryCollection.Count == 0)\n        {\n            return new Uri(baseUri, uriKind);\n        }\n        return new Uri(string.Format(\"{0}?{1}\", baseUri, queryCollection), uriKind);\n    }\n\n    public static string CustomProviderName(TwoFactorProviderType type)\n    {\n        return string.Concat(\"Custom_\", type.ToString());\n    }\n\n    public static bool TokenIsValid(string firstTokenPart, IDataProtector protector, string token, string userEmail,\n        Guid id, double expirationInHours)\n    {\n        var invalid = true;\n        try\n        {\n            var unprotectedData = protector.Unprotect(token);\n            var dataParts = unprotectedData.Split(' ');\n            if (dataParts.Length == 4 && dataParts[0] == firstTokenPart &&\n                new Guid(dataParts[1]) == id &&\n                dataParts[2].Equals(userEmail, StringComparison.InvariantCultureIgnoreCase))\n            {\n                var creationTime = FromEpocMilliseconds(Convert.ToInt64(dataParts[3]));\n                var expTime = creationTime.AddHours(expirationInHours);\n                invalid = expTime < DateTime.UtcNow;\n            }\n        }\n        catch\n        {\n            invalid = true;\n        }\n\n        return !invalid;\n    }\n\n    public static string GetApplicationCacheServiceBusSubscriptionName(GlobalSettings globalSettings)\n    {\n        var subName = globalSettings.ServiceBus.ApplicationCacheSubscriptionName;\n        if (string.IsNullOrWhiteSpace(subName))\n        {\n            var websiteInstanceId = Environment.GetEnvironmentVariable(\"WEBSITE_INSTANCE_ID\") ??\n                                    globalSettings.ServiceBus.WebSiteInstanceId;\n            if (string.IsNullOrWhiteSpace(websiteInstanceId))\n            {\n                throw new Exception(\"No service bus subscription name available.\");\n            }\n            else\n            {\n                subName = $\"{globalSettings.ProjectName.ToLower()}_{websiteInstanceId}\";\n                if (subName.Length > 50)\n                {\n                    subName = subName.Substring(0, 50);\n                }\n            }\n        }\n        return subName;\n    }\n\n    public static string? GetIpAddress(this Microsoft.AspNetCore.Http.HttpContext httpContext,\n        GlobalSettings globalSettings)\n    {\n        if (httpContext == null)\n        {\n            return null;\n        }\n\n        if (!globalSettings.SelfHosted && httpContext.Request.Headers.TryGetValue(RealConnectingIp, out var realConnectingIp))\n        {\n            return realConnectingIp.ToString();\n        }\n\n        return httpContext.Connection?.RemoteIpAddress?.ToString();\n    }\n\n    public static bool IsCorsOriginAllowed(string origin, GlobalSettings globalSettings)\n    {\n        return\n            // Web vault\n            origin == globalSettings.BaseServiceUri.Vault ||\n            // Safari extension origin\n            origin == \"file://\" ||\n            // Desktop application custom file protocol\n            origin == \"bw-desktop-file://bundle\" ||\n            // Product website\n            (!globalSettings.SelfHosted && origin == \"https://bitwarden.com\");\n    }\n\n    public static X509Certificate2? GetIdentityServerCertificate(GlobalSettings globalSettings)\n    {\n        if (globalSettings.SelfHosted &&\n            SettingHasValue(globalSettings.IdentityServer.CertificatePassword)\n            && File.Exists(globalSettings.IdentityServer.CertificateLocation))\n        {\n            return GetCertificate(globalSettings.IdentityServer.CertificateLocation,\n                globalSettings.IdentityServer.CertificatePassword);\n        }\n        else if (SettingHasValue(globalSettings.IdentityServer.CertificateThumbprint))\n        {\n            return GetCertificate(\n                globalSettings.IdentityServer.CertificateThumbprint);\n        }\n        else if (!globalSettings.SelfHosted &&\n            SettingHasValue(globalSettings.Storage?.ConnectionString) &&\n            SettingHasValue(globalSettings.IdentityServer.CertificatePassword))\n        {\n            return GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, \"certificates\",\n                \"identity.pfx\", globalSettings.IdentityServer.CertificatePassword).GetAwaiter().GetResult();\n        }\n        return null;\n    }\n\n    public static Dictionary<string, object> AdjustIdentityServerConfig(Dictionary<string, object> configDict,\n        string publicServiceUri, string internalServiceUri)\n    {\n        var dictReplace = new Dictionary<string, object>();\n        foreach (var item in configDict)\n        {\n            if (item.Key == \"authorization_endpoint\" && item.Value is string val)\n            {\n                var uri = new Uri(val);\n                dictReplace.Add(item.Key, string.Concat(publicServiceUri, uri.LocalPath));\n            }\n            else if ((item.Key == \"jwks_uri\" || item.Key.EndsWith(\"_endpoint\")) && item.Value is string val2)\n            {\n                var uri = new Uri(val2);\n                dictReplace.Add(item.Key, string.Concat(internalServiceUri, uri.LocalPath));\n            }\n        }\n        foreach (var replace in dictReplace)\n        {\n            configDict[replace.Key] = replace.Value;\n        }\n        return configDict;\n    }\n\n    public static List<KeyValuePair<string, string>> BuildIdentityClaims(User user, ICollection<CurrentContextOrganization> orgs,\n        ICollection<CurrentContextProvider> providers, bool isPremium)\n    {\n        var claims = new List<KeyValuePair<string, string>>()\n        {\n            new(Claims.Premium, isPremium ? \"true\" : \"false\"),\n            new(JwtClaimTypes.Email, user.Email),\n            new(JwtClaimTypes.EmailVerified, user.EmailVerified ? \"true\" : \"false\"),\n            // TODO: [https://bitwarden.atlassian.net/browse/PM-22171] Remove this since it is already added from the persisted grant\n            new(Claims.SecurityStamp, user.SecurityStamp),\n        };\n\n        if (!string.IsNullOrWhiteSpace(user.Name))\n        {\n            claims.Add(new KeyValuePair<string, string>(JwtClaimTypes.Name, user.Name));\n        }\n\n        // Orgs that this user belongs to\n        if (orgs.Any())\n        {\n            foreach (var group in orgs.GroupBy(o => o.Type))\n            {\n                switch (group.Key)\n                {\n                    case Enums.OrganizationUserType.Owner:\n                        foreach (var org in group)\n                        {\n                            claims.Add(new KeyValuePair<string, string>(Claims.OrganizationOwner, org.Id.ToString()));\n                        }\n                        break;\n                    case Enums.OrganizationUserType.Admin:\n                        foreach (var org in group)\n                        {\n                            claims.Add(new KeyValuePair<string, string>(Claims.OrganizationAdmin, org.Id.ToString()));\n                        }\n                        break;\n                    case Enums.OrganizationUserType.User:\n                        foreach (var org in group)\n                        {\n                            claims.Add(new KeyValuePair<string, string>(Claims.OrganizationUser, org.Id.ToString()));\n                        }\n                        break;\n                    case Enums.OrganizationUserType.Custom:\n                        foreach (var org in group)\n                        {\n                            claims.Add(new KeyValuePair<string, string>(Claims.OrganizationCustom, org.Id.ToString()));\n                            foreach (var (permission, claimName) in org.Permissions.ClaimsMap)\n                            {\n                                if (!permission)\n                                {\n                                    continue;\n                                }\n\n                                claims.Add(new KeyValuePair<string, string>(claimName, org.Id.ToString()));\n                            }\n                        }\n                        break;\n                    default:\n                        break;\n                }\n\n                // Secrets Manager\n                foreach (var org in group)\n                {\n                    if (org.AccessSecretsManager)\n                    {\n                        claims.Add(new KeyValuePair<string, string>(Claims.SecretsManagerAccess, org.Id.ToString()));\n                    }\n                }\n            }\n        }\n\n        if (providers.Any())\n        {\n            foreach (var group in providers.GroupBy(o => o.Type))\n            {\n                switch (group.Key)\n                {\n                    case ProviderUserType.ProviderAdmin:\n                        foreach (var provider in group)\n                        {\n                            claims.Add(new KeyValuePair<string, string>(Claims.ProviderAdmin, provider.Id.ToString()));\n                        }\n                        break;\n                    case ProviderUserType.ServiceUser:\n                        foreach (var provider in group)\n                        {\n                            claims.Add(new KeyValuePair<string, string>(Claims.ProviderServiceUser, provider.Id.ToString()));\n                        }\n                        break;\n                }\n            }\n        }\n\n        return claims;\n    }\n\n    /// <summary>\n    /// Deserializes JSON data into the specified type.\n    /// If the JSON data is a null reference, it will still return an instantiated class.\n    /// However, if the JSON data is a string \"null\", it will return null.\n    /// </summary>\n    /// <param name=\"jsonData\">The JSON data</param>\n    /// <typeparam name=\"T\">The type to deserialize into</typeparam>\n    /// <returns></returns>\n    public static T LoadClassFromJsonData<T>(string? jsonData) where T : new()\n    {\n        if (string.IsNullOrWhiteSpace(jsonData))\n        {\n            return new T();\n        }\n\n#nullable disable // TODO: Remove this and fix any callee warnings.\n        return System.Text.Json.JsonSerializer.Deserialize<T>(jsonData, _jsonSerializerOptions);\n#nullable enable\n    }\n\n    public static string ClassToJsonData<T>(T data)\n    {\n        return System.Text.Json.JsonSerializer.Serialize(data, _jsonSerializerOptions);\n    }\n\n    public static ICollection<T> AddIfNotExists<T>(this ICollection<T> list, T item)\n    {\n        if (list.Contains(item))\n        {\n            return list;\n        }\n        list.Add(item);\n        return list;\n    }\n\n    public static string? DecodeMessageText(this QueueMessage message)\n    {\n        var text = message?.MessageText;\n        if (string.IsNullOrWhiteSpace(text))\n        {\n            return text;\n        }\n        try\n        {\n            return Base64DecodeString(text);\n        }\n        catch\n        {\n            return text;\n        }\n    }\n\n    public static bool FixedTimeEquals(string input1, string input2)\n    {\n        return CryptographicOperations.FixedTimeEquals(\n            Encoding.UTF8.GetBytes(input1), Encoding.UTF8.GetBytes(input2));\n    }\n\n    public static string? ObfuscateEmail(string email)\n    {\n        if (email == null)\n        {\n            return email;\n        }\n\n        var emailParts = email.Split('@', StringSplitOptions.RemoveEmptyEntries);\n\n        if (emailParts.Length != 2)\n        {\n            return email;\n        }\n\n        var username = emailParts[0];\n\n        if (username.Length < 2)\n        {\n            return email;\n        }\n\n        var sb = new StringBuilder();\n        sb.Append(emailParts[0][..2]);\n        for (var i = 2; i < emailParts[0].Length; i++)\n        {\n            sb.Append('*');\n        }\n\n        return sb.Append('@')\n            .Append(emailParts[1])\n            .ToString();\n\n    }\n\n    public static string? GetEmailDomain(string email)\n    {\n        if (!string.IsNullOrWhiteSpace(email))\n        {\n            var emailParts = email.Split('@', StringSplitOptions.RemoveEmptyEntries);\n\n            if (emailParts.Length == 2)\n            {\n                return emailParts[1].Trim();\n            }\n        }\n\n        return null;\n    }\n\n    public static string ReplaceWhiteSpace(string input, string newValue)\n    {\n        return _whiteSpaceRegex.Replace(input, newValue);\n    }\n\n    public static string? RedactEmailAddress(string email)\n    {\n        if (string.IsNullOrWhiteSpace(email))\n        {\n            return null;\n        }\n\n        var emailParts = email.Split('@');\n\n        string shownPart;\n        if (emailParts[0].Length > 2 && emailParts[0].Length <= 4)\n        {\n            shownPart = emailParts[0].Substring(0, 1);\n        }\n        else if (emailParts[0].Length > 4)\n        {\n            shownPart = emailParts[0].Substring(0, 2);\n        }\n        else\n        {\n            shownPart = string.Empty;\n        }\n\n        string redactedPart;\n        if (emailParts[0].Length > 4)\n        {\n            redactedPart = new string('*', emailParts[0].Length - 2);\n        }\n        else\n        {\n            redactedPart = new string('*', emailParts[0].Length - shownPart.Length);\n        }\n\n        return $\"{shownPart}{redactedPart}@{emailParts[1]}\";\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/CurrentContextMiddleware.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Bit.Core.Utilities;\n\npublic class CurrentContextMiddleware\n{\n    private readonly RequestDelegate _next;\n\n    public CurrentContextMiddleware(RequestDelegate next)\n    {\n        _next = next;\n    }\n\n    public async Task Invoke(HttpContext httpContext, ICurrentContext currentContext, GlobalSettings globalSettings)\n    {\n        await currentContext.BuildAsync(httpContext, globalSettings);\n        await _next.Invoke(httpContext);\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/CustomIpRateLimitMiddleware.cs",
    "content": "﻿using AspNetCoreRateLimit;\nusing Bit.Core.Models.Api;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\n\nnamespace Bit.Core.Utilities;\n\npublic class CustomIpRateLimitMiddleware : IpRateLimitMiddleware\n{\n    private readonly IpRateLimitOptions _options;\n\n    public CustomIpRateLimitMiddleware(\n        RequestDelegate next,\n        IProcessingStrategy processingStrategy,\n        IRateLimitConfiguration rateLimitConfiguration,\n        IOptions<IpRateLimitOptions> options,\n        IIpPolicyStore policyStore,\n        ILogger<CustomIpRateLimitMiddleware> logger)\n        : base(next, processingStrategy, options, policyStore, rateLimitConfiguration, logger)\n    {\n        _options = options.Value;\n    }\n\n    public override Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitRule rule, string retryAfter)\n    {\n        var message = string.IsNullOrWhiteSpace(_options.QuotaExceededMessage)\n            ? $\"Slow down! Too many requests. Try again in {rule.Period}.\"\n            : _options.QuotaExceededMessage;\n        httpContext.Response.Headers[\"Retry-After\"] = retryAfter;\n        httpContext.Response.StatusCode = _options.HttpStatusCode;\n        var errorModel = new ErrorResponseModel { Message = message };\n        return httpContext.Response.WriteAsJsonAsync(errorModel, httpContext.RequestAborted);\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/CustomRedisProcessingStrategy.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AspNetCoreRateLimit;\nusing AspNetCoreRateLimit.Redis;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Caching.Memory;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing StackExchange.Redis;\n\nnamespace Bit.Core.Utilities;\n\n/// <summary>\n/// A modified version of <see cref=\"AspNetCoreRateLimit.Redis.RedisProcessingStrategy\"/> that gracefully\n/// handles a disrupted Redis connection. If the connection is down or the number of failed requests within\n/// a given time period exceed the configured threshold, then rate limiting is temporarily disabled.\n/// </summary>\n/// <remarks>\n/// This is necessary to ensure the service does not become unresponsive due to Redis being out of service. As\n/// the default implementation would throw an exception and exit the request pipeline for all requests. \n/// </remarks>\npublic class CustomRedisProcessingStrategy : RedisProcessingStrategy\n{\n    private readonly IConnectionMultiplexer _connectionMultiplexer;\n    private readonly ILogger<CustomRedisProcessingStrategy> _logger;\n    private readonly IMemoryCache _memoryCache;\n    private readonly GlobalSettings.DistributedIpRateLimitingSettings _distributedSettings;\n\n    private const string _redisTimeoutCacheKey = \"IpRateLimitRedisTimeout\";\n\n    public CustomRedisProcessingStrategy(\n        [FromKeyedServices(\"rate-limiter\")]\n        IConnectionMultiplexer connectionMultiplexer,\n        IRateLimitConfiguration config,\n        ILogger<CustomRedisProcessingStrategy> logger,\n        IMemoryCache memoryCache,\n        GlobalSettings globalSettings)\n        : base(connectionMultiplexer, config, logger)\n    {\n        _connectionMultiplexer = connectionMultiplexer;\n        _logger = logger;\n        _memoryCache = memoryCache;\n        _distributedSettings = globalSettings.DistributedIpRateLimiting;\n    }\n\n    public override async Task<RateLimitCounter> ProcessRequestAsync(ClientRequestIdentity requestIdentity,\n        RateLimitRule rule, ICounterKeyBuilder counterKeyBuilder, RateLimitOptions rateLimitOptions,\n        CancellationToken cancellationToken = default)\n    {\n        // If Redis is down entirely, skip rate limiting\n        if (!_connectionMultiplexer.IsConnected)\n        {\n            _logger.LogDebug(\"Redis connection is down, skipping IP rate limiting\");\n            return SkipRateLimitResult();\n        }\n\n        // Check if any Redis timeouts have occurred recently\n        if (_memoryCache.TryGetValue<TimeoutCounter>(_redisTimeoutCacheKey, out var timeoutCounter))\n        {\n            // We've exceeded threshold, backoff Redis and skip rate limiting for now\n            if (timeoutCounter.Count >= _distributedSettings.MaxRedisTimeoutsThreshold)\n            {\n                _logger.LogDebug(\n                    \"Redis timeout threshold has been exceeded, backing off and skipping IP rate limiting\");\n                return SkipRateLimitResult();\n            }\n        }\n\n        try\n        {\n            return await base.ProcessRequestAsync(requestIdentity, rule, counterKeyBuilder, rateLimitOptions, cancellationToken);\n        }\n        catch (Exception ex) when (ex is RedisTimeoutException || ex is RedisConnectionException)\n        {\n            _logger.LogWarning(ex, \"Redis appears down, skipping rate limiting\");\n            // If this is the first timeout/connection error we've had, start a new counter and sliding window \n            timeoutCounter ??= new TimeoutCounter()\n            {\n                Count = 0,\n                ExpiresAt = DateTime.UtcNow.AddSeconds(_distributedSettings.SlidingWindowSeconds)\n            };\n            timeoutCounter.Count++;\n\n            _memoryCache.Set(_redisTimeoutCacheKey, timeoutCounter,\n                new MemoryCacheEntryOptions { AbsoluteExpiration = timeoutCounter.ExpiresAt });\n\n            // Just because Redis timed out does not mean we should kill the request\n            return SkipRateLimitResult();\n        }\n    }\n\n    /// <summary>\n    /// A RateLimitCounter result used when the rate limiting middleware should\n    /// fail open and allow the request to proceed without checking request limits.\n    /// </summary>\n    private static RateLimitCounter SkipRateLimitResult()\n    {\n        return new RateLimitCounter { Count = 0, Timestamp = DateTime.UtcNow };\n    }\n\n    internal class TimeoutCounter\n    {\n        public DateTime ExpiresAt { get; init; }\n\n        public int Count { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/DeviceTypes.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Core.Utilities;\n\npublic static class DeviceTypes\n{\n    public static IReadOnlyCollection<DeviceType> MobileTypes { get; } =\n    [\n        DeviceType.Android,\n        DeviceType.iOS,\n        DeviceType.AndroidAmazon\n    ];\n\n    public static IReadOnlyCollection<DeviceType> DesktopTypes { get; } =\n    [\n        DeviceType.LinuxDesktop,\n        DeviceType.MacOsDesktop,\n        DeviceType.WindowsDesktop,\n        DeviceType.UWP\n    ];\n\n    public static IReadOnlyCollection<DeviceType> CliTypes { get; } =\n    [\n        DeviceType.WindowsCLI,\n        DeviceType.MacOsCLI,\n        DeviceType.LinuxCLI\n    ];\n\n    public static IReadOnlyCollection<DeviceType> BrowserExtensionTypes { get; } =\n    [\n        DeviceType.ChromeExtension,\n        DeviceType.FirefoxExtension,\n        DeviceType.OperaExtension,\n        DeviceType.EdgeExtension,\n        DeviceType.VivaldiExtension,\n        DeviceType.SafariExtension\n    ];\n\n    public static IReadOnlyCollection<DeviceType> BrowserTypes { get; } =\n    [\n        DeviceType.ChromeBrowser,\n        DeviceType.FirefoxBrowser,\n        DeviceType.OperaBrowser,\n        DeviceType.EdgeBrowser,\n        DeviceType.IEBrowser,\n        DeviceType.SafariBrowser,\n        DeviceType.VivaldiBrowser,\n        DeviceType.DuckDuckGoBrowser,\n        DeviceType.UnknownBrowser\n    ];\n\n    public static ClientType ToClientType(DeviceType? deviceType)\n    {\n        return deviceType switch\n        {\n            not null when MobileTypes.Contains(deviceType.Value) => ClientType.Mobile,\n            not null when DesktopTypes.Contains(deviceType.Value) => ClientType.Desktop,\n            not null when CliTypes.Contains(deviceType.Value) => ClientType.Cli,\n            not null when BrowserExtensionTypes.Contains(deviceType.Value) => ClientType.Browser,\n            not null when BrowserTypes.Contains(deviceType.Value) => ClientType.Web,\n            _ => ClientType.All\n        };\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/DistributedCacheExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Microsoft.Extensions.Caching.Distributed;\n\nnamespace Bit.Core.Utilities;\n\npublic static class DistributedCacheExtensions\n{\n    public static void Set<T>(this IDistributedCache cache, string key, T value)\n    {\n        Set(cache, key, value, new DistributedCacheEntryOptions());\n    }\n\n    public static void Set<T>(this IDistributedCache cache, string key, T value,\n        DistributedCacheEntryOptions options)\n    {\n        var bytes = JsonSerializer.SerializeToUtf8Bytes(value);\n        cache.Set(key, bytes, options);\n    }\n\n    public static Task SetAsync<T>(this IDistributedCache cache, string key, T value)\n    {\n        return SetAsync(cache, key, value, new DistributedCacheEntryOptions());\n    }\n\n    public static Task SetAsync<T>(this IDistributedCache cache, string key, T value,\n        DistributedCacheEntryOptions options)\n    {\n        var bytes = JsonSerializer.SerializeToUtf8Bytes(value);\n        return cache.SetAsync(key, bytes, options);\n    }\n\n    public static bool TryGetValue<T>(this IDistributedCache cache, string key, out T value)\n    {\n        var val = cache.Get(key);\n        value = default;\n        if (val == null) return false;\n        try\n        {\n            value = JsonSerializer.Deserialize<T>(val);\n        }\n        catch\n        {\n            return false;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/DomainNameAttribute.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.RegularExpressions;\n\nnamespace Bit.Core.Utilities;\n\n/// <summary>\n/// https://bitwarden.atlassian.net/browse/VULN-376\n/// Domain names are vulnerable to XSS attacks if not properly validated.\n/// Domain names can contain letters, numbers, dots, and hyphens.\n/// Domain names maybe internationalized (IDN) and contain unicode characters.\n/// </summary>\npublic class DomainNameValidatorAttribute : ValidationAttribute\n{\n    // RFC 1123 compliant domain name regex\n    // - Allows alphanumeric characters and hyphens\n    // - Cannot start or end with a hyphen\n    // - Each label (part between dots) must be 1-63 characters\n    // - Total length should not exceed 253 characters\n    // - Supports internationalized domain names (IDN) - which is why this regex includes unicode ranges\n    private static readonly Regex _domainNameRegex = new(\n        @\"^(?:[a-zA-Z0-9\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF](?:[a-zA-Z0-9\\-\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]{0,61}[a-zA-Z0-9\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])?\\.)*[a-zA-Z0-9\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF](?:[a-zA-Z0-9\\-\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]{0,61}[a-zA-Z0-9\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])?$\",\n        RegexOptions.Compiled | RegexOptions.IgnoreCase\n    );\n\n    public DomainNameValidatorAttribute()\n        : base(\"The {0} field is not a valid domain name.\")\n    { }\n\n    public override bool IsValid(object? value)\n    {\n        if (value == null)\n        {\n            return true; // Use [Required] for null checks\n        }\n\n        var domainName = value.ToString();\n\n        if (string.IsNullOrWhiteSpace(domainName))\n        {\n            return false;\n        }\n\n        // Reject if contains any whitespace (including leading/trailing spaces, tabs, newlines)\n        if (domainName.Any(char.IsWhiteSpace))\n        {\n            return false;\n        }\n\n        // Check length constraints\n        if (domainName.Length > 253)\n        {\n            return false;\n        }\n\n        // Check for control characters or other dangerous characters\n        if (domainName.Any(c => char.IsControl(c) || c == '<' || c == '>' || c == '\"' || c == '\\'' || c == '&'))\n        {\n            return false;\n        }\n\n        // Validate against domain name regex\n        return _domainNameRegex.IsMatch(domainName);\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/EmailValidation.cs",
    "content": "﻿using System.Net.Mail;\nusing System.Text.RegularExpressions;\nusing Bit.Core.Exceptions;\nusing MimeKit;\n\nnamespace Bit.Core.Utilities;\n\npublic static class EmailValidation\n{\n    public static bool IsValidEmail(this string emailAddress)\n    {\n        if (string.IsNullOrWhiteSpace(emailAddress))\n        {\n            return false;\n        }\n\n        try\n        {\n            var parsedEmailAddress = MailboxAddress.Parse(emailAddress).Address;\n            if (parsedEmailAddress != emailAddress)\n            {\n                return false;\n            }\n        }\n        catch (ParseException)\n        {\n            return false;\n        }\n\n        // The regex below is intended to catch edge cases that are not handled by the general parsing check above.\n        // This enforces the following rules:\n        // * Requires ASCII only in the local-part (code points 0-127)\n        // * Requires an @ symbol\n        // * Allows any char in second-level domain name, including unicode and symbols\n        // * Requires at least one period (.) separating SLD from TLD\n        // * Must end in a letter (including unicode)\n        // See the unit tests for examples of what is allowed.\n        var emailFormat = @\"^[\\x00-\\x7F]+@.+\\.\\p{L}+$\";\n        if (!Regex.IsMatch(emailAddress, emailFormat))\n        {\n            return false;\n        }\n\n        return true;\n    }\n\n    /// <summary>\n    /// Extracts the domain portion from an email address and normalizes it to lowercase.\n    /// </summary>\n    /// <param name=\"email\">The email address to extract the domain from.</param>\n    /// <returns>The domain portion of the email address in lowercase (e.g., \"example.com\").</returns>\n    /// <exception cref=\"BadRequestException\">Thrown when the email address format is invalid.</exception>\n    public static string GetDomain(string email)\n    {\n        try\n        {\n            return new MailAddress(email).Host.ToLower();\n        }\n        catch (Exception ex) when (ex is FormatException || ex is ArgumentException)\n        {\n            throw new BadRequestException(\"Invalid email address format.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/EncryptedStringAttribute.cs",
    "content": "﻿using System.Buffers.Text;\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\n\n#nullable enable\n\nnamespace Bit.Core.Utilities;\n\n/// <summary>\n/// Validates a string that is in encrypted form: \"head.b64iv=|b64ct=|b64mac=\"\n/// </summary>\npublic class EncryptedStringAttribute : ValidationAttribute\n{\n    internal static readonly Dictionary<EncryptionType, int> _encryptionTypeToRequiredPiecesMap = new()\n    {\n        [EncryptionType.AesCbc256_B64] = 2, // iv|ct\n        [EncryptionType.AesCbc128_HmacSha256_B64] = 3, // iv|ct|mac\n        [EncryptionType.AesCbc256_HmacSha256_B64] = 3, // iv|ct|mac\n        [EncryptionType.XChaCha20Poly1305_B64] = 1, // cose bytes\n        [EncryptionType.Rsa2048_OaepSha256_B64] = 1, // rsaCt\n        [EncryptionType.Rsa2048_OaepSha1_B64] = 1, // rsaCt\n        [EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64] = 2, // rsaCt|mac\n        [EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64] = 2, // rsaCt|mac\n    };\n\n    public EncryptedStringAttribute()\n        : base(\"{0} is not a valid encrypted string.\")\n    { }\n\n    public override bool IsValid(object? value)\n    {\n        try\n        {\n            if (value is null)\n            {\n                return true;\n            }\n\n            if (value is string stringValue)\n            {\n                // Fast path\n                return IsValidCore(stringValue);\n            }\n\n            // This attribute should only be placed on string properties, fail\n            return false;\n        }\n        catch\n        {\n            return false;\n        }\n    }\n\n    internal static bool IsValidCore(ReadOnlySpan<char> value)\n    {\n        if (!value.TrySplitBy('.', out var headerChunk, out var rest))\n        {\n            // We couldn't find a header part, this is the slow path, because we have to do two loops over\n            // the data.\n            // If it has 3 encryption parts that means it is AesCbc128_HmacSha256_B64\n            // else we assume it is AesCbc256_B64\n            var splitChars = rest.Count('|');\n\n            if (splitChars == 2)\n            {\n                return ValidatePieces(rest, _encryptionTypeToRequiredPiecesMap[EncryptionType.AesCbc128_HmacSha256_B64]);\n            }\n            else\n            {\n                return ValidatePieces(rest, _encryptionTypeToRequiredPiecesMap[EncryptionType.AesCbc256_B64]);\n            }\n        }\n\n        EncryptionType encryptionType;\n\n        // Using byte here because that is the backing type for EncryptionType\n        if (!byte.TryParse(headerChunk, out var encryptionTypeNumber))\n        {\n            // We can't read the header chunk as a number, this is the slow path\n            if (!Enum.TryParse(headerChunk, out encryptionType))\n            {\n                // Can't even get the enum from a non-number header, fail\n                return false;\n            }\n\n            // Since this value came from Enum.TryParse we know it is an enumerated object and we can therefore\n            // just access the dictionary\n            return ValidatePieces(rest, _encryptionTypeToRequiredPiecesMap[encryptionType]);\n        }\n\n        // Simply cast the number to the enum, this could be a value that doesn't actually have a backing enum\n        // entry but that is alright we will use it to look in the dictionary and non-valid\n        // numbers will be filtered out there.\n        encryptionType = (EncryptionType)encryptionTypeNumber;\n\n        if (!_encryptionTypeToRequiredPiecesMap.TryGetValue(encryptionType, out var encryptionPieces))\n        {\n            // Could not find a configuration map for the given header piece. This is an invalid string\n            return false;\n        }\n\n        return ValidatePieces(rest, encryptionPieces);\n    }\n\n    private static bool ValidatePieces(ReadOnlySpan<char> encryptionPart, int requiredPieces)\n    {\n        var rest = encryptionPart;\n\n        while (requiredPieces != 0)\n        {\n            if (requiredPieces == 1)\n            {\n                // Only one more part is needed so don't split and check the chunk\n                if (rest.IsEmpty || !Base64.IsValid(rest))\n                {\n                    return false;\n                }\n\n                // Make sure there isn't another split character possibly denoting another chunk\n                return rest.IndexOf('|') == -1;\n            }\n            else\n            {\n                // More than one part is required so split it out\n                if (!rest.TrySplitBy('|', out var chunk, out rest))\n                {\n                    return false;\n                }\n\n                // Is the required chunk valid base 64?\n                if (chunk.IsEmpty || !Base64.IsValid(chunk))\n                {\n                    return false;\n                }\n            }\n\n            // This current piece is valid so we can count down\n            requiredPieces--;\n        }\n\n        // No more parts are required, so check there are no extra parts\n        return rest.IndexOf('|') == -1;\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/EncryptedStringLengthAttribute.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Utilities;\n\npublic class EncryptedStringLengthAttribute : StringLengthAttribute\n{\n    public EncryptedStringLengthAttribute(int maximumLength)\n        : base(maximumLength)\n    { }\n\n    public override string FormatErrorMessage(string name)\n    {\n        return string.Format(\"The field {0} exceeds the maximum encrypted value length of {1} characters.\",\n            name, MaximumLength);\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/EnumMemberJsonConverter.cs",
    "content": "﻿using System.Reflection;\nusing System.Runtime.Serialization;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Core.Utilities;\n\n/// <summary>\n/// A custom JSON converter for enum types that respects the <see cref=\"EnumMemberAttribute\"/> when serializing and deserializing.\n/// </summary>\n/// <typeparam name=\"T\">The enum type to convert. Must be a struct and implement Enum.</typeparam>\n/// <remarks>\n/// This converter builds lookup dictionaries at initialization to efficiently map between enum values and their\n/// string representations. If an enum value has an <see cref=\"EnumMemberAttribute\"/>, the attribute's Value\n/// property is used as the JSON string; otherwise, the enum's ToString() value is used.\n/// </remarks>\npublic class EnumMemberJsonConverter<T> : JsonConverter<T> where T : struct, Enum\n{\n    private readonly Dictionary<T, string> _enumToString = new();\n    private readonly Dictionary<string, T> _stringToEnum = new();\n\n    public EnumMemberJsonConverter()\n    {\n        var type = typeof(T);\n        var values = Enum.GetValues<T>();\n\n        foreach (var value in values)\n        {\n            var fieldInfo = type.GetField(value.ToString());\n            var attribute = fieldInfo?.GetCustomAttribute<EnumMemberAttribute>();\n\n            var stringValue = attribute?.Value ?? value.ToString();\n            _enumToString[value] = stringValue;\n            _stringToEnum[stringValue] = value;\n        }\n    }\n\n    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        var stringValue = reader.GetString();\n\n        if (!string.IsNullOrEmpty(stringValue) && _stringToEnum.TryGetValue(stringValue, out var enumValue))\n        {\n            return enumValue;\n        }\n\n        throw new JsonException($\"Unable to convert '{stringValue}' to {typeof(T).Name}\");\n    }\n\n    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)\n        => writer.WriteStringValue(_enumToString[value]);\n}\n"
  },
  {
    "path": "src/Core/Utilities/EnumerationProtectionHelpers.cs",
    "content": "﻿using System.Text;\n\nnamespace Bit.Core.Utilities;\n\npublic static class EnumerationProtectionHelpers\n{\n    /// <summary>\n    /// Use this method to get a consistent int result based on the inputString that is in the range.\n    /// The same inputString will always return the same index result based on range input.\n    /// </summary>\n    /// <param name=\"hmacKey\">Key used to derive the HMAC hash. Use a different key for each usage for optimal security</param>\n    /// <param name=\"inputString\">The string to derive an index result</param>\n    /// <param name=\"range\">The range of possible index values</param>\n    /// <returns>An int between 0 and range - 1</returns>\n    public static int GetIndexForInputHash(byte[] hmacKey, string inputString, int range)\n    {\n        if (hmacKey == null || range <= 0 || hmacKey.Length == 0)\n        {\n            return 0;\n        }\n        else\n        {\n            // Compute the HMAC hash of the salt\n            var hmacMessage = Encoding.UTF8.GetBytes(inputString.Trim().ToLowerInvariant());\n            using var hmac = new System.Security.Cryptography.HMACSHA256(hmacKey);\n            var hmacHash = hmac.ComputeHash(hmacMessage);\n            // Convert the hash to a number\n            var hashHex = BitConverter.ToString(hmacHash).Replace(\"-\", string.Empty).ToLowerInvariant();\n            var hashFirst8Bytes = hashHex[..16];\n            var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);\n            // Find the default KDF value for this hash number\n            var hashIndex = (int)(Math.Abs(hashNumber) % range);\n            return hashIndex;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/EpochDateTimeJsonConverter.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Core.Utilities;\n\npublic class EpochDateTimeJsonConverter : JsonConverter<DateTime>\n{\n    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        return CoreHelpers.FromEpocMilliseconds(reader.GetInt64());\n    }\n    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)\n    {\n        writer.WriteNumberValue(CoreHelpers.ToEpocMilliseconds(value));\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/EventIntegrationsCacheConstants.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Core.Utilities;\n\n/// <summary>\n/// Provides cache key generation helpers and cache name constants for event integration–related entities.\n/// </summary>\npublic static class EventIntegrationsCacheConstants\n{\n    /// <summary>\n    /// The base cache name used for storing event integration data.\n    /// </summary>\n    public const string CacheName = \"EventIntegrations\";\n\n    /// <summary>\n    /// Duration TimeSpan for adding OrganizationIntegrationConfigurationDetails to the cache.\n    /// </summary>\n    public static readonly TimeSpan DurationForOrganizationIntegrationConfigurationDetails = TimeSpan.FromDays(1);\n\n    /// <summary>\n    /// Builds a deterministic cache key for a <see cref=\"Group\"/>.\n    /// </summary>\n    /// <param name=\"groupId\">The unique identifier of the group.</param>\n    /// <returns>\n    /// A cache key for this Group.\n    /// </returns>\n    public static string BuildCacheKeyForGroup(Guid groupId) =>\n        $\"Group:{groupId:N}\";\n\n    /// <summary>\n    /// Builds a deterministic cache key for an <see cref=\"Organization\"/>.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <returns>\n    /// A cache key for the Organization.\n    /// </returns>\n    public static string BuildCacheKeyForOrganization(Guid organizationId) =>\n        $\"Organization:{organizationId:N}\";\n\n    /// <summary>\n    /// Builds a deterministic cache key for an organization user <see cref=\"OrganizationUserUserDetails\"/>.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization to which the user belongs.</param>\n    /// <param name=\"userId\">The unique identifier of the user.</param>\n    /// <returns>\n    /// A cache key for the user.\n    /// </returns>\n    public static string BuildCacheKeyForOrganizationUser(Guid organizationId, Guid userId) =>\n        $\"OrganizationUserUserDetails:{organizationId:N}:{userId:N}\";\n\n    /// <summary>\n    /// Builds a deterministic cache key for an organization's integration configuration details\n    /// <see cref=\"OrganizationIntegrationConfigurationDetails\"/>.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <param name=\"integrationType\">The <see cref=\"IntegrationType\"/> of the integration.</param>\n    /// <param name=\"eventType\">The specific <see cref=\"EventType\"/> of the event configured.</param>\n    /// <returns>\n    /// A cache key for the configuration details.\n    /// </returns>\n    public static string BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n        Guid organizationId,\n        IntegrationType integrationType,\n        EventType eventType\n    ) => $\"OrganizationIntegrationConfigurationDetails:{organizationId:N}:{integrationType}:{eventType}\";\n\n    /// <summary>\n    /// Builds a deterministic tag for tagging an organization's integration configuration details. This tag is then\n    /// used to tag all of the <see cref=\"OrganizationIntegrationConfigurationDetails\"/> that result from this\n    /// integration, which allows us to remove all relevant entries when an integration is changed or removed.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization to which the user belongs.</param>\n    /// <param name=\"integrationType\">The <see cref=\"IntegrationType\"/> of the integration.</param>\n    /// <returns>\n    /// A cache tag to use for the configuration details.\n    /// </returns>\n    public static string BuildCacheTagForOrganizationIntegration(\n        Guid organizationId,\n        IntegrationType integrationType\n    ) => $\"OrganizationIntegration:{organizationId:N}:{integrationType}\";\n}\n"
  },
  {
    "path": "src/Core/Utilities/ExtendedCacheServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Caching.StackExchangeRedis;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Logging;\nusing StackExchange.Redis;\nusing ZiggyCreatures.Caching.Fusion;\nusing ZiggyCreatures.Caching.Fusion.Backplane;\nusing ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;\nusing ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\npublic static class ExtendedCacheServiceCollectionExtensions\n{\n    /// <summary>\n    /// Adds a new, named Fusion Cache <see href=\"https://github.com/ZiggyCreatures/FusionCache\"/> to the service\n    /// collection. If an existing cache of the same name is found, it will do nothing.<br/>\n    /// <br/>\n    /// <b>Note</b>: When re-using an existing distributed cache, it is expected to call this method <b>after</b> calling\n    /// <code>services.AddDistributedCache(globalSettings)</code><br />This ensures that DI correctly finds\n    /// and re-uses the shared distributed cache infrastructure.<br />\n    /// <br />\n    /// <b>Backplane</b>: Cross-instance cache invalidation is only available when using Redis.\n    /// Non-Redis distributed caches operate with eventual consistency across multiple instances.\n    /// </summary>\n    public static IServiceCollection AddExtendedCache(\n        this IServiceCollection services,\n        string cacheName,\n        GlobalSettings globalSettings,\n        GlobalSettings.ExtendedCacheSettings? settings = null)\n    {\n        settings ??= globalSettings.DistributedCache.DefaultExtendedCache;\n        if (settings is null || string.IsNullOrEmpty(cacheName))\n        {\n            return services;\n        }\n\n        // If a cache already exists with this key, do nothing\n        if (services.Any(s => s.ServiceType == typeof(IFusionCache) &&\n                         s.ServiceKey?.Equals(cacheName) == true))\n        {\n            return services;\n        }\n\n        if (services.All(s => s.ServiceType != typeof(FusionCacheSystemTextJsonSerializer)))\n        {\n            services.AddFusionCacheSystemTextJsonSerializer();\n        }\n        var fusionCacheBuilder = services\n            .AddFusionCache(cacheName)\n            .WithCacheKeyPrefix($\"{cacheName}:\")\n            .AsKeyedServiceByCacheName()\n            .WithOptions(opt =>\n            {\n                opt.DistributedCacheCircuitBreakerDuration = settings.DistributedCacheCircuitBreakerDuration;\n            })\n            .WithDefaultEntryOptions(new FusionCacheEntryOptions\n            {\n                Duration = settings.Duration,\n                IsFailSafeEnabled = settings.IsFailSafeEnabled,\n                FailSafeMaxDuration = settings.FailSafeMaxDuration,\n                FailSafeThrottleDuration = settings.FailSafeThrottleDuration,\n                EagerRefreshThreshold = settings.EagerRefreshThreshold,\n                FactorySoftTimeout = settings.FactorySoftTimeout,\n                FactoryHardTimeout = settings.FactoryHardTimeout,\n                DistributedCacheSoftTimeout = settings.DistributedCacheSoftTimeout,\n                DistributedCacheHardTimeout = settings.DistributedCacheHardTimeout,\n                AllowBackgroundDistributedCacheOperations = settings.AllowBackgroundDistributedCacheOperations,\n                JitterMaxDuration = settings.JitterMaxDuration\n            })\n            .WithRegisteredSerializer();\n\n        if (!settings.EnableDistributedCache)\n            return services;\n\n        if (settings.UseSharedDistributedCache)\n        {\n            if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString))\n            {\n                // Using Shared Non-Redis Distributed Cache:\n                //   1. Assume IDistributedCache is already registered (e.g., Cosmos, SQL Server)\n                //   2. Backplane not supported (Redis-only feature, requires pub/sub)\n\n                fusionCacheBuilder\n                    .TryWithRegisteredDistributedCache();\n\n                return services;\n            }\n\n            // Using Shared Redis, TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)\n\n            services.TryAddSingleton<IConnectionMultiplexer>(sp =>\n                CreateConnectionMultiplexer(sp, cacheName, globalSettings.DistributedCache.Redis.ConnectionString));\n\n            services.TryAddSingleton<IDistributedCache>(sp =>\n            {\n                var mux = sp.GetRequiredService<IConnectionMultiplexer>();\n                return new RedisCache(new RedisCacheOptions\n                {\n                    ConnectionMultiplexerFactory = () => Task.FromResult(mux)\n                });\n            });\n\n            services.TryAddSingleton<IFusionCacheBackplane>(sp =>\n            {\n                var mux = sp.GetRequiredService<IConnectionMultiplexer>();\n                return new RedisBackplane(new RedisBackplaneOptions\n                {\n                    ConnectionMultiplexerFactory = () => Task.FromResult(mux)\n                });\n            });\n\n            fusionCacheBuilder\n                .WithRegisteredDistributedCache()\n                .WithRegisteredBackplane();\n\n            return services;\n        }\n\n        // Using keyed Distributed Cache. Create/Reuse all pieces as keyed services.\n\n        if (!CoreHelpers.SettingHasValue(settings.Redis.ConnectionString))\n        {\n            // Using Keyed Non-Redis Distributed Cache:\n            //   1. Assume IDistributedCache (e.g., Cosmos, SQL Server) is already registered with cacheName as key\n            //   2. Backplane not supported (Redis-only feature, requires pub/sub)\n\n            fusionCacheBuilder\n                .TryWithRegisteredKeyedDistributedCache(serviceKey: cacheName);\n\n            return services;\n        }\n\n        // Using Keyed Redis: TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)\n\n        services.TryAddKeyedSingleton<IConnectionMultiplexer>(\n            cacheName,\n            (sp, _) => CreateConnectionMultiplexer(sp, cacheName, settings.Redis.ConnectionString)\n        );\n        services.TryAddKeyedSingleton<IDistributedCache>(\n            cacheName,\n            (sp, _) =>\n            {\n                var mux = sp.GetRequiredKeyedService<IConnectionMultiplexer>(cacheName);\n                return new RedisCache(new RedisCacheOptions\n                {\n                    ConnectionMultiplexerFactory = () => Task.FromResult(mux)\n                });\n            }\n        );\n        services.TryAddKeyedSingleton<IFusionCacheBackplane>(\n            cacheName,\n            (sp, _) =>\n            {\n                var mux = sp.GetRequiredKeyedService<IConnectionMultiplexer>(cacheName);\n                return new RedisBackplane(new RedisBackplaneOptions\n                {\n                    ConnectionMultiplexerFactory = () => Task.FromResult(mux)\n                });\n            }\n        );\n\n        fusionCacheBuilder\n            .WithRegisteredKeyedDistributedCacheByCacheName()\n            .WithRegisteredKeyedBackplaneByCacheName();\n\n        return services;\n    }\n\n    private static ConnectionMultiplexer CreateConnectionMultiplexer(IServiceProvider sp, string cacheName,\n        string connectionString)\n    {\n        try\n        {\n            return ConnectionMultiplexer.Connect(connectionString);\n        }\n        catch (Exception ex)\n        {\n            var logger = sp.GetService<ILogger>();\n            logger?.LogError(ex, \"Failed to connect to Redis for cache {CacheName}\", cacheName);\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/HandlebarsObjectJsonConverter.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Core.Utilities;\n\npublic class HandlebarsObjectJsonConverter : JsonConverter<object>\n{\n    public override bool CanConvert(Type typeToConvert) => true;\n    public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        return JsonSerializer.Deserialize<Dictionary<string, object>>(ref reader, options);\n    }\n    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)\n    {\n        JsonSerializer.Serialize(writer, value, options);\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/HostBuilderExtensions.cs",
    "content": "﻿using System.Reflection;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Bit.Core.Utilities;\n\npublic static class HostBuilderExtensions\n{\n    public static IHostBuilder ConfigureCustomAppConfiguration(this IHostBuilder hostBuilder, string[] args)\n    {\n        // Reload app configuration with SelfHosted overrides.\n        return hostBuilder.ConfigureAppConfiguration((hostingContext, config) =>\n        {\n            if (Environment.GetEnvironmentVariable(\"globalSettings__selfHosted\")?.ToLower() != \"true\")\n            {\n                return;\n            }\n\n            var env = hostingContext.HostingEnvironment;\n\n            config.AddJsonFile(\"appsettings.json\", optional: true, reloadOnChange: true)\n                .AddJsonFile($\"appsettings.{env.EnvironmentName}.json\", optional: true, reloadOnChange: true)\n                .AddJsonFile(\"appsettings.SelfHosted.json\", optional: true, reloadOnChange: true);\n\n            if (env.IsDevelopment())\n            {\n                var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));\n                if (appAssembly != null)\n                {\n                    config.AddUserSecrets(appAssembly, optional: true);\n                }\n            }\n\n            config.AddEnvironmentVariables();\n\n            if (args != null)\n            {\n                config.AddCommandLine(args);\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/IDbMigrator.cs",
    "content": "﻿namespace Bit.Core.Utilities;\n\npublic interface IDbMigrator\n{\n    bool MigrateDatabase(bool enableLogging = true,\n        CancellationToken cancellationToken = default(CancellationToken));\n}\n"
  },
  {
    "path": "src/Core/Utilities/JsonHelpers.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Globalization;\nusing System.Net;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing NS = Newtonsoft.Json;\n\nnamespace Bit.Core.Utilities;\n\npublic static class JsonHelpers\n{\n    public static JsonSerializerOptions Default { get; }\n    public static JsonSerializerOptions Indented { get; }\n    public static JsonSerializerOptions IgnoreCase { get; }\n    public static JsonSerializerOptions IgnoreWritingNull { get; }\n    public static JsonSerializerOptions CamelCase { get; }\n    public static JsonSerializerOptions IgnoreWritingNullAndCamelCase { get; }\n\n    static JsonHelpers()\n    {\n        Default = new JsonSerializerOptions();\n\n        Indented = new JsonSerializerOptions\n        {\n            WriteIndented = true,\n        };\n\n        IgnoreCase = new JsonSerializerOptions\n        {\n            PropertyNameCaseInsensitive = true,\n        };\n\n        IgnoreWritingNull = new JsonSerializerOptions\n        {\n            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,\n        };\n\n        CamelCase = new JsonSerializerOptions\n        {\n            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n        };\n\n        IgnoreWritingNullAndCamelCase = new JsonSerializerOptions\n        {\n            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,\n        };\n    }\n\n    public static T DeserializeOrNew<T>(string json, JsonSerializerOptions options = null)\n        where T : new()\n    {\n        if (string.IsNullOrWhiteSpace(json))\n        {\n            return new T();\n        }\n\n        return JsonSerializer.Deserialize<T>(json, options);\n    }\n\n    #region Legacy Newtonsoft.Json usage\n    private const string LegacyMessage = \"Usage of Newtonsoft.Json should be kept to a minimum and will further be removed when we move to .NET 6\";\n\n    [Obsolete(LegacyMessage)]\n    public static NS.JsonSerializerSettings LegacyEnumKeyResolver { get; } = new NS.JsonSerializerSettings\n    {\n        ContractResolver = new EnumKeyResolver<byte>(),\n    };\n\n    [Obsolete(LegacyMessage)]\n    public static string LegacySerialize(object value, NS.JsonSerializerSettings settings = null)\n    {\n        return NS.JsonConvert.SerializeObject(value, settings);\n    }\n\n    [Obsolete(LegacyMessage)]\n    public static T LegacyDeserialize<T>(string value, NS.JsonSerializerSettings settings = null)\n    {\n        return NS.JsonConvert.DeserializeObject<T>(value, settings);\n    }\n    #endregion\n}\n\npublic class EnumKeyResolver<T> : NS.Serialization.DefaultContractResolver\n    where T : struct\n{\n    protected override NS.Serialization.JsonDictionaryContract CreateDictionaryContract(Type objectType)\n    {\n        var contract = base.CreateDictionaryContract(objectType);\n        var keyType = contract.DictionaryKeyType;\n\n        if (keyType.BaseType == typeof(Enum))\n        {\n            contract.DictionaryKeyResolver = propName => ((T)Enum.Parse(keyType, propName)).ToString();\n        }\n\n        return contract;\n    }\n}\n\npublic class MsEpochConverter : JsonConverter<DateTime?>\n{\n    public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        if (reader.TokenType == JsonTokenType.Null)\n        {\n            return null;\n        }\n\n        if (!long.TryParse(reader.GetString(), out var milliseconds))\n        {\n            return null;\n        }\n\n        return CoreHelpers.FromEpocMilliseconds(milliseconds);\n    }\n\n    public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)\n    {\n        if (!value.HasValue)\n        {\n            writer.WriteNullValue();\n        }\n\n        writer.WriteStringValue(CoreHelpers.ToEpocMilliseconds(value.Value).ToString());\n    }\n}\n\n/// <summary>\n/// Allows reading a string from a JSON number or string, should only be used on <see cref=\"string\" /> properties\n/// </summary>\npublic class PermissiveStringConverter : JsonConverter<string>\n{\n    internal static readonly PermissiveStringConverter Instance = new();\n    private static readonly CultureInfo _cultureInfo = new(\"en-US\");\n\n    public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        return reader.TokenType switch\n        {\n            JsonTokenType.String => reader.GetString(),\n            JsonTokenType.Number => reader.GetDecimal().ToString(_cultureInfo),\n            JsonTokenType.True => bool.TrueString,\n            JsonTokenType.False => bool.FalseString,\n            _ => throw new JsonException($\"Unsupported TokenType: {reader.TokenType}\"),\n        };\n    }\n\n    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)\n    {\n        writer.WriteStringValue(value);\n    }\n}\n\n/// <summary>\n/// Allows reading a JSON array of number or string, should only be used on <see cref=\"IEnumerable{T}\" /> whose generic type is <see cref=\"string\" />\n/// </summary>\npublic class PermissiveStringEnumerableConverter : JsonConverter<IEnumerable<string>>\n{\n    public override IEnumerable<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        var stringList = new List<string>();\n\n        // Handle special cases or throw\n        if (reader.TokenType != JsonTokenType.StartArray)\n        {\n            // An array was expected but to be extra permissive allow reading from anything other than an object\n            if (reader.TokenType == JsonTokenType.StartObject)\n            {\n                throw new JsonException(\"Cannot read JSON Object to an IEnumerable<string>.\");\n            }\n\n            stringList.Add(PermissiveStringConverter.Instance.Read(ref reader, typeof(string), options));\n            return stringList;\n        }\n\n        while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)\n        {\n            stringList.Add(PermissiveStringConverter.Instance.Read(ref reader, typeof(string), options));\n        }\n\n        return stringList;\n    }\n\n    public override void Write(Utf8JsonWriter writer, IEnumerable<string> value, JsonSerializerOptions options)\n    {\n        writer.WriteStartArray();\n\n        foreach (var str in value)\n        {\n            PermissiveStringConverter.Instance.Write(writer, str, options);\n        }\n\n        writer.WriteEndArray();\n    }\n}\n\n/// <summary>\n/// Encodes incoming strings using HTML encoding\n/// and decodes outgoing strings using HTML decoding.\n/// </summary>\npublic class HtmlEncodingStringConverter : JsonConverter<string>\n{\n    public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n    {\n        if (reader.TokenType == JsonTokenType.String)\n        {\n            var originalValue = reader.GetString();\n            return WebUtility.HtmlEncode(originalValue);\n        }\n        return reader.GetString();\n    }\n\n    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)\n    {\n        if (!string.IsNullOrEmpty(value))\n        {\n            var encodedValue = WebUtility.HtmlDecode(value);\n            writer.WriteStringValue(encodedValue);\n        }\n        else\n        {\n            writer.WriteNullValue();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/KdfSettingsValidator.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\n\nnamespace Bit.Core.Utilities;\n\npublic static class KdfSettingsValidator\n{\n    // PM-28143 - Remove below when fixing ticket\n    public static IEnumerable<ValidationResult> Validate(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        switch (kdfType)\n        {\n            case KdfType.PBKDF2_SHA256:\n                if (!AuthConstants.PBKDF2_ITERATIONS.InsideRange(kdfIterations))\n                {\n                    yield return new ValidationResult($\"KDF iterations must be between {AuthConstants.PBKDF2_ITERATIONS.Min} and {AuthConstants.PBKDF2_ITERATIONS.Max}.\");\n                }\n                break;\n            case KdfType.Argon2id:\n                if (!AuthConstants.ARGON2_ITERATIONS.InsideRange(kdfIterations))\n                {\n                    yield return new ValidationResult($\"Argon2 iterations must be between {AuthConstants.ARGON2_ITERATIONS.Min} and {AuthConstants.ARGON2_ITERATIONS.Max}.\");\n                }\n                else if (!kdfMemory.HasValue || !AuthConstants.ARGON2_MEMORY.InsideRange(kdfMemory.Value))\n                {\n                    yield return new ValidationResult($\"Argon2 memory must be between {AuthConstants.ARGON2_MEMORY.Min}mb and {AuthConstants.ARGON2_MEMORY.Max}mb.\");\n                }\n                else if (!kdfParallelism.HasValue || !AuthConstants.ARGON2_PARALLELISM.InsideRange(kdfParallelism.Value))\n                {\n                    yield return new ValidationResult($\"Argon2 parallelism must be between {AuthConstants.ARGON2_PARALLELISM.Min} and {AuthConstants.ARGON2_PARALLELISM.Max}.\");\n                }\n                break;\n\n            default:\n                break;\n        }\n    }\n\n    public static IEnumerable<ValidationResult> Validate(KdfSettings settings)\n    {\n        return Validate(settings.KdfType, settings.Iterations, settings.Memory, settings.Parallelism);\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/LoggerFactoryExtensions.cs",
    "content": "﻿using System.Globalization;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Utilities;\n\npublic static class LoggerFactoryExtensions\n{\n    /// <summary>\n    ///\n    /// </summary>\n    /// <param name=\"hostBuilder\"></param>\n    /// <returns></returns>\n    public static IHostBuilder AddSerilogFileLogging(this IHostBuilder hostBuilder)\n    {\n        return hostBuilder.ConfigureLogging((context, logging) =>\n        {\n            if (context.HostingEnvironment.IsDevelopment())\n            {\n                return;\n            }\n\n            IConfiguration loggingConfiguration;\n\n            // If they have begun using the new settings location, use that\n            if (!string.IsNullOrEmpty(context.Configuration[\"Logging:PathFormat\"]))\n            {\n                loggingConfiguration = context.Configuration.GetSection(\"Logging\");\n            }\n            else\n            {\n                var globalSettingsSection = context.Configuration.GetSection(\"GlobalSettings\");\n                var loggingOptions = new LegacyFileLoggingOptions();\n                globalSettingsSection.Bind(loggingOptions);\n\n                if (string.IsNullOrWhiteSpace(loggingOptions.LogDirectory))\n                {\n                    return;\n                }\n\n                var projectName = loggingOptions.ProjectName\n                    ?? context.HostingEnvironment.ApplicationName;\n\n                string pathFormat;\n\n                if (loggingOptions.LogRollBySizeLimit.HasValue)\n                {\n                    pathFormat = loggingOptions.LogDirectoryByProject\n                        ? Path.Combine(loggingOptions.LogDirectory, projectName, \"log.txt\")\n                        : Path.Combine(loggingOptions.LogDirectory, $\"{projectName.ToLowerInvariant()}.log\");\n                }\n                else\n                {\n                    pathFormat = loggingOptions.LogDirectoryByProject\n                        ? Path.Combine(loggingOptions.LogDirectory, projectName, \"{Date}.txt\")\n                        : Path.Combine(loggingOptions.LogDirectory, $\"{projectName.ToLowerInvariant()}_{{Date}}.log\");\n                }\n\n                // We want to rely on Serilog using the configuration section to have customization of the log levels\n                // so we make a custom configuration source for them based on the legacy values and allow overrides from\n                // the new location.\n                loggingConfiguration = new ConfigurationBuilder()\n                    .AddInMemoryCollection(new Dictionary<string, string?>\n                    {\n                        {\"PathFormat\", pathFormat},\n                        {\"FileSizeLimitBytes\", loggingOptions.LogRollBySizeLimit?.ToString(CultureInfo.InvariantCulture)}\n                    })\n                    .AddConfiguration(context.Configuration.GetSection(\"Logging\"))\n                    .Build();\n            }\n\n            logging.AddFile(loggingConfiguration);\n        });\n    }\n\n    /// <summary>\n    /// Our own proprietary options that we've always supported in `GlobalSettings` configuration section.\n    /// </summary>\n    private class LegacyFileLoggingOptions\n    {\n        public string? ProjectName { get; set; }\n        public string? LogDirectory { get; set; } = \"/etc/bitwarden/logs\";\n        public bool LogDirectoryByProject { get; set; } = true;\n        public long? LogRollBySizeLimit { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/LoggingExceptionHandlerFilterAttribute.cs",
    "content": "﻿using Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Utilities;\n\npublic class LoggingExceptionHandlerFilterAttribute : ExceptionFilterAttribute\n{\n    public override void OnException(ExceptionContext context)\n    {\n        var exception = context.Exception;\n        if (exception == null)\n        {\n            // Should never happen.\n            return;\n        }\n\n        var logger = context.HttpContext.RequestServices\n            .GetRequiredService<ILogger<LoggingExceptionHandlerFilterAttribute>>();\n        logger.LogError(0, exception, \"Unhandled exception\");\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/ModelStateExtensions.cs",
    "content": "﻿using Microsoft.AspNetCore.Mvc.ModelBinding;\n\nnamespace Bit.Core.Utilities;\n\npublic static class ModelStateExtensions\n{\n    public static string GetErrorMessage(this ModelStateDictionary modelState)\n    {\n        var errors = modelState.Values\n            .SelectMany(v => v.Errors)\n            .Select(e => e.ErrorMessage)\n            .ToList();\n\n        return string.Join(\"; \", errors);\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/OrganizationReportCacheConstants.cs",
    "content": "﻿namespace Bit.Core.Utilities;\n\n/// <summary>\n/// Provides cache key generation helpers and cache name constants for organization report–related entities.\n/// </summary>\npublic static class OrganizationReportCacheConstants\n{\n    /// <summary>\n    /// The cache name used for storing organization report data.\n    /// </summary>\n    public const string CacheName = \"OrganizationReports\";\n\n    /// <summary>\n    /// Duration TimeSpan for caching organization report summary data.\n    /// Consider: Reports might be regenerated daily, so cache for shorter periods.\n    /// </summary>\n    public static readonly TimeSpan DurationForSummaryData = TimeSpan.FromHours(6);\n\n    /// <summary>\n    /// Builds a deterministic cache key for organization report summary data by date range.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <param name=\"startDate\">The start date of the date range.</param>\n    /// <param name=\"endDate\">The end date of the date range.</param>\n    /// <returns>\n    /// A cache key for the organization report summary data.\n    /// </returns>\n    public static string BuildCacheKeyForSummaryDataByDateRange(\n        Guid organizationId,\n        DateTime startDate,\n        DateTime endDate)\n        => $\"OrganizationReportSummaryData:{organizationId:N}:{startDate:yyyy-MM-dd}:{endDate:yyyy-MM-dd}\";\n\n    /// <summary>\n    /// Builds a cache tag for an organization's report data.\n    /// Used for bulk invalidation when organization reports are updated.\n    /// </summary>\n    /// <param name=\"organizationId\">The unique identifier of the organization.</param>\n    /// <returns>\n    /// A cache tag for the organization's reports.\n    /// </returns>\n    public static string BuildCacheTagForOrganizationReports(Guid organizationId)\n        => $\"OrganizationReports:{organizationId:N}\";\n}\n"
  },
  {
    "path": "src/Core/Utilities/RequireFeatureAttribute.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Utilities;\n\n/// <summary>\n/// Specifies that the class or method that this attribute is applied to requires the specified boolean feature flag\n/// to be enabled. If the feature flag is not enabled, a <see cref=\"FeatureUnavailableException\"/> is thrown\n/// </summary>\npublic class RequireFeatureAttribute : ActionFilterAttribute\n{\n    private readonly string _featureFlagKey;\n\n    /// <summary>\n    /// Initializes a new instance of the <see cref=\"RequireFeatureAttribute\"/> class with the specified feature flag key.\n    /// </summary>\n    /// <param name=\"featureFlagKey\">The name of the feature flag to require.</param>\n    public RequireFeatureAttribute(string featureFlagKey)\n    {\n        _featureFlagKey = featureFlagKey;\n    }\n\n    public override void OnActionExecuting(ActionExecutingContext context)\n    {\n        var featureService = context.HttpContext.RequestServices.GetRequiredService<IFeatureService>();\n\n        if (!featureService.IsEnabled(_featureFlagKey))\n        {\n            throw new FeatureUnavailableException();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/RequireLowerEnvironmentAttribute.cs",
    "content": "﻿using Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.Extensions.Hosting;\n\nnamespace Bit.Core.Utilities;\n\n/// <summary>\n/// Authorization attribute that restricts controller/action access to Development and QA environments only.\n/// Returns 404 Not Found in all other environments.\n/// </summary>\npublic class RequireLowerEnvironmentAttribute() : TypeFilterAttribute(typeof(LowerEnvironmentFilter))\n{\n    private class LowerEnvironmentFilter(IWebHostEnvironment environment) : IAuthorizationFilter\n    {\n        public void OnAuthorization(AuthorizationFilterContext context)\n        {\n            if (!environment.IsDevelopment() && !environment.IsEnvironment(\"QA\"))\n            {\n                context.Result = new NotFoundResult();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/SecurityHeadersMiddleware.cs",
    "content": "﻿using Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\n\nnamespace Bit.Core.Utilities;\n\npublic sealed class SecurityHeadersMiddleware\n{\n    private readonly RequestDelegate _next;\n\n    public SecurityHeadersMiddleware(RequestDelegate next)\n    {\n        _next = next;\n    }\n\n    public Task Invoke(HttpContext context)\n    {\n        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options\n        context.Response.Headers.Append(\"x-frame-options\", new StringValues(\"SAMEORIGIN\"));\n\n        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection\n        context.Response.Headers.Append(\"x-xss-protection\", new StringValues(\"1; mode=block\"));\n\n        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options\n        context.Response.Headers.Append(\"x-content-type-options\", new StringValues(\"nosniff\"));\n\n        return _next(context);\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/SelfHostedAttribute.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Utilities;\n\npublic class SelfHostedAttribute : ActionFilterAttribute\n{\n    public bool SelfHostedOnly { get; set; }\n    public bool NotSelfHostedOnly { get; set; }\n\n    public override void OnActionExecuting(ActionExecutingContext context)\n    {\n        var globalSettings = context.HttpContext.RequestServices.GetRequiredService<GlobalSettings>();\n        if (SelfHostedOnly && !globalSettings.SelfHosted)\n        {\n            throw new BadRequestException(\"Only allowed when self hosted.\");\n        }\n        else if (NotSelfHostedOnly && globalSettings.SelfHosted)\n        {\n            throw new BadRequestException(\"Only allowed when not self hosted.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/SpanExtensions.cs",
    "content": "﻿namespace Bit.Core.Utilities;\n\npublic static class SpanExtensions\n{\n    public static bool TrySplitBy(this ReadOnlySpan<char> input,\n        char splitChar, out ReadOnlySpan<char> chunk, out ReadOnlySpan<char> rest)\n    {\n        var splitIndex = input.IndexOf(splitChar);\n\n        if (splitIndex == -1)\n        {\n            chunk = default;\n            rest = input;\n            return false;\n        }\n\n        chunk = input[..splitIndex];\n        rest = input[++splitIndex..];\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/StaticStore.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Utilities;\n\npublic static class StaticStore\n{\n    static StaticStore()\n    {\n        #region Global Domains\n\n        GlobalDomains = new Dictionary<GlobalEquivalentDomainsType, IEnumerable<string>>();\n\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Ameritrade, new List<string> { \"ameritrade.com\", \"tdameritrade.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.BoA, new List<string> { \"bankofamerica.com\", \"bofa.com\", \"mbna.com\", \"usecfo.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Sprint, new List<string> { \"sprint.com\", \"sprintpcs.com\", \"nextel.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Google, new List<string> { \"youtube.com\", \"google.com\", \"gmail.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Apple, new List<string> { \"apple.com\", \"icloud.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.WellsFargo, new List<string> { \"wellsfargo.com\", \"wf.com\", \"wellsfargoadvisors.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Merrill, new List<string> { \"mymerrill.com\", \"ml.com\", \"merrilledge.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Citi, new List<string> { \"accountonline.com\", \"citi.com\", \"citibank.com\", \"citicards.com\", \"citibankonline.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Cnet, new List<string> { \"cnet.com\", \"cnettv.com\", \"com.com\", \"download.com\", \"news.com\", \"search.com\", \"upload.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Gap, new List<string> { \"bananarepublic.com\", \"gap.com\", \"oldnavy.com\", \"piperlime.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Microsoft, new List<string> { \"bing.com\", \"hotmail.com\", \"live.com\", \"microsoft.com\", \"msn.com\", \"passport.net\", \"windows.com\", \"microsoftonline.com\", \"office.com\", \"office365.com\", \"microsoftstore.com\", \"xbox.com\", \"azure.com\", \"windowsazure.com\", \"cloud.microsoft\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.United, new List<string> { \"ua2go.com\", \"ual.com\", \"united.com\", \"unitedwifi.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Yahoo, new List<string> { \"overture.com\", \"yahoo.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Zonelabs, new List<string> { \"zonealarm.com\", \"zonelabs.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.PayPal, new List<string> { \"paypal.com\", \"paypal-search.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Avon, new List<string> { \"avon.com\", \"youravon.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Diapers, new List<string> { \"diapers.com\", \"soap.com\", \"wag.com\", \"yoyo.com\", \"beautybar.com\", \"casa.com\", \"afterschool.com\", \"vine.com\", \"bookworm.com\", \"look.com\", \"vinemarket.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Contacts, new List<string> { \"1800contacts.com\", \"800contacts.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Amazon, new List<string> { \"amazon.com\", \"amazon.com.be\", \"amazon.ae\", \"amazon.ca\", \"amazon.co.uk\", \"amazon.com.au\", \"amazon.com.br\", \"amazon.com.mx\", \"amazon.com.tr\", \"amazon.de\", \"amazon.es\", \"amazon.fr\", \"amazon.in\", \"amazon.it\", \"amazon.nl\", \"amazon.pl\", \"amazon.sa\", \"amazon.se\", \"amazon.sg\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Cox, new List<string> { \"cox.com\", \"cox.net\", \"coxbusiness.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Norton, new List<string> { \"mynortonaccount.com\", \"norton.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Verizon, new List<string> { \"verizon.com\", \"verizon.net\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Buy, new List<string> { \"rakuten.com\", \"buy.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Sirius, new List<string> { \"siriusxm.com\", \"sirius.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Ea, new List<string> { \"ea.com\", \"origin.com\", \"play4free.com\", \"tiberiumalliance.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Basecamp, new List<string> { \"37signals.com\", \"basecamp.com\", \"basecamphq.com\", \"highrisehq.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Steam, new List<string> { \"steampowered.com\", \"steamcommunity.com\", \"steamgames.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Chart, new List<string> { \"chart.io\", \"chartio.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Gotomeeting, new List<string> { \"gotomeeting.com\", \"citrixonline.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Gogo, new List<string> { \"gogoair.com\", \"gogoinflight.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Oracle, new List<string> { \"mysql.com\", \"oracle.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Discover, new List<string> { \"discover.com\", \"discovercard.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Dcu, new List<string> { \"dcu.org\", \"dcu-online.org\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Healthcare, new List<string> { \"healthcare.gov\", \"cuidadodesalud.gov\", \"cms.gov\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Pepco, new List<string> { \"pepco.com\", \"pepcoholdings.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Century21, new List<string> { \"century21.com\", \"21online.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Comcast, new List<string> { \"comcast.com\", \"comcast.net\", \"xfinity.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Cricket, new List<string> { \"cricketwireless.com\", \"aiowireless.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Mtb, new List<string> { \"mandtbank.com\", \"mtb.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Dropbox, new List<string> { \"dropbox.com\", \"getdropbox.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Snapfish, new List<string> { \"snapfish.com\", \"snapfish.ca\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Alibaba, new List<string> { \"alibaba.com\", \"aliexpress.com\", \"aliyun.com\", \"net.cn\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Playstation, new List<string> { \"playstation.com\", \"sonyentertainmentnetwork.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Mercado, new List<string> { \"mercadolivre.com\", \"mercadolivre.com.br\", \"mercadolibre.com\", \"mercadolibre.com.ar\", \"mercadolibre.com.mx\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Zendesk, new List<string> { \"zendesk.com\", \"zopim.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Autodesk, new List<string> { \"autodesk.com\", \"tinkercad.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.RailNation, new List<string> { \"railnation.ru\", \"railnation.de\", \"rail-nation.com\", \"railnation.gr\", \"railnation.us\", \"trucknation.de\", \"traviangames.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Wpcu, new List<string> { \"wpcu.coop\", \"wpcuonline.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Mathletics, new List<string> { \"mathletics.com\", \"mathletics.com.au\", \"mathletics.co.uk\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Discountbank, new List<string> { \"discountbank.co.il\", \"telebank.co.il\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Mi, new List<string> { \"mi.com\", \"xiaomi.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Postepay, new List<string> { \"postepay.it\", \"poste.it\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Facebook, new List<string> { \"facebook.com\", \"messenger.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Skysports, new List<string> { \"skysports.com\", \"skybet.com\", \"skyvegas.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Disney, new List<string> { \"disneymoviesanywhere.com\", \"go.com\", \"disney.com\", \"dadt.com\", \"disneyplus.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Pokemon, new List<string> { \"pokemon-gl.com\", \"pokemon.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Uv, new List<string> { \"myuv.com\", \"uvvu.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Mdsol, new List<string> { \"mdsol.com\", \"imedidata.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Yahavo, new List<string> { \"bank-yahav.co.il\", \"bankhapoalim.co.il\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Sears, new List<string> { \"sears.com\", \"shld.net\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Xiami, new List<string> { \"xiami.com\", \"alipay.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Belkin, new List<string> { \"belkin.com\", \"seedonk.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Turbotax, new List<string> { \"turbotax.com\", \"intuit.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Shopify, new List<string> { \"shopify.com\", \"myshopify.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Ebay, new List<string> { \"ebay.com\", \"ebay.at\", \"ebay.be\", \"ebay.ca\", \"ebay.ch\", \"ebay.cn\", \"ebay.co.jp\", \"ebay.co.th\", \"ebay.co.uk\", \"ebay.com.au\", \"ebay.com.hk\", \"ebay.com.my\", \"ebay.com.sg\", \"ebay.com.tw\", \"ebay.de\", \"ebay.es\", \"ebay.fr\", \"ebay.ie\", \"ebay.in\", \"ebay.it\", \"ebay.nl\", \"ebay.ph\", \"ebay.pl\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Techdata, new List<string> { \"techdata.com\", \"techdata.ch\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Schwab, new List<string> { \"schwab.com\", \"schwabplan.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Tesla, new List<string> { \"tesla.com\", \"teslamotors.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.MorganStanley, new List<string> { \"morganstanley.com\", \"morganstanleyclientserv.com\", \"stockplanconnect.com\", \"ms.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.TaxAct, new List<string> { \"taxact.com\", \"taxactonline.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Wikimedia, new List<string> { \"mediawiki.org\", \"wikibooks.org\", \"wikidata.org\", \"wikimedia.org\", \"wikinews.org\", \"wikipedia.org\", \"wikiquote.org\", \"wikisource.org\", \"wikiversity.org\", \"wikivoyage.org\", \"wiktionary.org\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Airbnb, new List<string> { \"airbnb.at\", \"airbnb.be\", \"airbnb.ca\", \"airbnb.ch\", \"airbnb.cl\", \"airbnb.co.cr\", \"airbnb.co.id\", \"airbnb.co.in\", \"airbnb.co.kr\", \"airbnb.co.nz\", \"airbnb.co.uk\", \"airbnb.co.ve\", \"airbnb.com\", \"airbnb.com.ar\", \"airbnb.com.au\", \"airbnb.com.bo\", \"airbnb.com.br\", \"airbnb.com.bz\", \"airbnb.com.co\", \"airbnb.com.ec\", \"airbnb.com.gt\", \"airbnb.com.hk\", \"airbnb.com.hn\", \"airbnb.com.mt\", \"airbnb.com.my\", \"airbnb.com.ni\", \"airbnb.com.pa\", \"airbnb.com.pe\", \"airbnb.com.py\", \"airbnb.com.sg\", \"airbnb.com.sv\", \"airbnb.com.tr\", \"airbnb.com.tw\", \"airbnb.cz\", \"airbnb.de\", \"airbnb.dk\", \"airbnb.es\", \"airbnb.fi\", \"airbnb.fr\", \"airbnb.gr\", \"airbnb.gy\", \"airbnb.hu\", \"airbnb.ie\", \"airbnb.is\", \"airbnb.it\", \"airbnb.jp\", \"airbnb.mx\", \"airbnb.nl\", \"airbnb.no\", \"airbnb.pl\", \"airbnb.pt\", \"airbnb.ru\", \"airbnb.se\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Eventbrite, new List<string> { \"eventbrite.at\", \"eventbrite.be\", \"eventbrite.ca\", \"eventbrite.ch\", \"eventbrite.cl\", \"eventbrite.co\", \"eventbrite.co.nz\", \"eventbrite.co.uk\", \"eventbrite.com\", \"eventbrite.com.ar\", \"eventbrite.com.au\", \"eventbrite.com.br\", \"eventbrite.com.mx\", \"eventbrite.com.pe\", \"eventbrite.de\", \"eventbrite.dk\", \"eventbrite.es\", \"eventbrite.fi\", \"eventbrite.fr\", \"eventbrite.hk\", \"eventbrite.ie\", \"eventbrite.it\", \"eventbrite.nl\", \"eventbrite.pt\", \"eventbrite.se\", \"eventbrite.sg\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.StackExchange, new List<string> { \"stackexchange.com\", \"superuser.com\", \"stackoverflow.com\", \"serverfault.com\", \"mathoverflow.net\", \"askubuntu.com\", \"stackapps.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Docusign, new List<string> { \"docusign.com\", \"docusign.net\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Envato, new List<string> { \"envato.com\", \"themeforest.net\", \"codecanyon.net\", \"videohive.net\", \"audiojungle.net\", \"graphicriver.net\", \"photodune.net\", \"3docean.net\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.X10Hosting, new List<string> { \"x10hosting.com\", \"x10premium.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Cisco, new List<string> { \"dnsomatic.com\", \"opendns.com\", \"umbrella.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.CedarFair, new List<string> { \"cagreatamerica.com\", \"canadaswonderland.com\", \"carowinds.com\", \"cedarfair.com\", \"cedarpoint.com\", \"dorneypark.com\", \"kingsdominion.com\", \"knotts.com\", \"miadventure.com\", \"schlitterbahn.com\", \"valleyfair.com\", \"visitkingsisland.com\", \"worldsoffun.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Ubiquiti, new List<string> { \"ubnt.com\", \"ui.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Discord, new List<string> { \"discordapp.com\", \"discord.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Netcup, new List<string> { \"netcup.de\", \"netcup.eu\", \"customercontrolpanel.de\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Yandex, new List<string> { \"yandex.com\", \"ya.ru\", \"yandex.az\", \"yandex.by\", \"yandex.co.il\", \"yandex.com.am\", \"yandex.com.ge\", \"yandex.com.tr\", \"yandex.ee\", \"yandex.fi\", \"yandex.fr\", \"yandex.kg\", \"yandex.kz\", \"yandex.lt\", \"yandex.lv\", \"yandex.md\", \"yandex.pl\", \"yandex.ru\", \"yandex.tj\", \"yandex.tm\", \"yandex.ua\", \"yandex.uz\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Sony, new List<string> { \"sonyentertainmentnetwork.com\", \"sony.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Proton, new List<string> { \"proton.me\", \"protonmail.com\", \"protonvpn.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Ubisoft, new List<string> { \"ubisoft.com\", \"ubi.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.TransferWise, new List<string> { \"transferwise.com\", \"wise.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.TakeawayEU, new List<string> { \"takeaway.com\", \"just-eat.dk\", \"just-eat.no\", \"just-eat.fr\", \"just-eat.ch\", \"lieferando.de\", \"lieferando.at\", \"thuisbezorgd.nl\", \"pyszne.pl\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Atlassian, new List<string> { \"atlassian.com\", \"bitbucket.org\", \"trello.com\", \"statuspage.io\", \"atlassian.net\", \"jira.com\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Pinterest, new List<string> { \"pinterest.com\", \"pinterest.com.au\", \"pinterest.cl\", \"pinterest.de\", \"pinterest.dk\", \"pinterest.es\", \"pinterest.fr\", \"pinterest.co.uk\", \"pinterest.jp\", \"pinterest.co.kr\", \"pinterest.nz\", \"pinterest.pt\", \"pinterest.se\" });\n        GlobalDomains.Add(GlobalEquivalentDomainsType.Twitter, new List<string> { \"twitter.com\", \"x.com\" });\n        #endregion\n    }\n\n    public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Utilities/StrictEmailAddressAttribute.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Utilities;\n\npublic class StrictEmailAddressAttribute : ValidationAttribute\n{\n    public StrictEmailAddressAttribute()\n        : base(\"The {0} field is not a supported e-mail address format.\")\n    { }\n\n    public override bool IsValid(object value)\n    {\n        var emailAddress = value?.ToString() ?? string.Empty;\n\n        return emailAddress.IsValidEmail() && new EmailAddressAttribute().IsValid(emailAddress);\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/StrictEmailAddressListAttribute.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Utilities;\n\npublic class StrictEmailAddressListAttribute : ValidationAttribute\n{\n    protected override ValidationResult IsValid(object value, ValidationContext validationContext)\n    {\n        var strictEmailAttribute = new StrictEmailAddressAttribute();\n        var emails = value as IList<string>;\n\n        if (!emails?.Any() ?? true)\n        {\n            return new ValidationResult(\"An email is required.\");\n        }\n\n        if (emails.Count() > 20)\n        {\n            return new ValidationResult(\"You can only submit up to 20 emails at a time.\");\n        }\n\n        for (var i = 0; i < emails.Count(); i++)\n        {\n            var email = emails.ElementAt(i);\n            if (!strictEmailAttribute.IsValid(email))\n            {\n                return new ValidationResult($\"Email #{i + 1} is not valid.\");\n            }\n\n            if (email.Length > 256)\n            {\n                return new ValidationResult($\"Email #{i + 1} is longer than 256 characters.\");\n            }\n        }\n\n        return ValidationResult.Success;\n    }\n}\n"
  },
  {
    "path": "src/Core/Utilities/SystemTextJsonCosmosSerializer.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Azure.Core.Serialization;\nusing Microsoft.Azure.Cosmos;\n\nnamespace Bit.Core.Utilities;\n\n// ref: https://github.com/Azure/azure-cosmos-dotnet-v3/blob/master/Microsoft.Azure.Cosmos.Samples/Usage/SystemTextJson/CosmosSystemTextJsonSerializer.cs\npublic class SystemTextJsonCosmosSerializer : CosmosSerializer\n{\n    private readonly JsonObjectSerializer _systemTextJsonSerializer;\n\n    public SystemTextJsonCosmosSerializer(JsonSerializerOptions jsonSerializerOptions)\n    {\n        _systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions);\n    }\n\n    public override T FromStream<T>(Stream stream)\n    {\n        using (stream)\n        {\n            if (stream.CanSeek && stream.Length == 0)\n            {\n                return default;\n            }\n            if (typeof(Stream).IsAssignableFrom(typeof(T)))\n            {\n                return (T)(object)stream;\n            }\n            return (T)_systemTextJsonSerializer.Deserialize(stream, typeof(T), default);\n        }\n    }\n\n    public override Stream ToStream<T>(T input)\n    {\n        var streamPayload = new MemoryStream();\n        _systemTextJsonSerializer.Serialize(streamPayload, input, input.GetType(), default);\n        streamPayload.Position = 0;\n        return streamPayload;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Authorization/Permissions/NormalCipherPermissions.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Vault.Authorization.Permissions;\n\npublic class NormalCipherPermissions\n{\n    public static bool CanDelete(User user, CipherDetails cipherDetails, OrganizationAbility? organizationAbility)\n    {\n        if (cipherDetails.OrganizationId == null && cipherDetails.UserId == null)\n        {\n            throw new Exception(\"Cipher needs to belong to a user or an organization.\");\n        }\n\n        if (cipherDetails.OrganizationId == null && user.Id == cipherDetails.UserId)\n        {\n            return true;\n        }\n\n        if (organizationAbility?.Id != cipherDetails.OrganizationId)\n        {\n            throw new Exception(\"Cipher does not belong to the input organization.\");\n        }\n\n        if (organizationAbility is { LimitItemDeletion: true })\n        {\n            return cipherDetails.Manage;\n        }\n        return cipherDetails.Manage || cipherDetails.Edit;\n    }\n\n    public static bool CanRestore(User user, CipherDetails cipherDetails, OrganizationAbility? organizationAbility)\n    {\n        return CanDelete(user, cipherDetails, organizationAbility);\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Queries;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.Vault.Authorization.SecurityTasks;\n\npublic class SecurityTaskAuthorizationHandler : AuthorizationHandler<SecurityTaskOperationRequirement, SecurityTask>\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;\n\n    private readonly Dictionary<Guid, IDictionary<Guid, OrganizationCipherPermission>> _cipherPermissionCache = new();\n\n    public SecurityTaskAuthorizationHandler(ICurrentContext currentContext, IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery)\n    {\n        _currentContext = currentContext;\n        _getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;\n    }\n\n    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        SecurityTaskOperationRequirement requirement,\n        SecurityTask task)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            return;\n        }\n\n        var org = _currentContext.GetOrganization(task.OrganizationId);\n\n        if (org == null)\n        {\n            // User must be a member of the organization\n            return;\n        }\n\n        var authorized = requirement switch\n        {\n            not null when requirement == SecurityTaskOperations.Read => await CanReadAsync(task, org),\n            not null when requirement == SecurityTaskOperations.Create => await CanCreateAsync(task, org),\n            not null when requirement == SecurityTaskOperations.Update => await CanUpdateAsync(task, org),\n            _ => throw new ArgumentOutOfRangeException(nameof(requirement), requirement, null)\n        };\n\n        if (authorized)\n        {\n            context.Succeed(requirement);\n        }\n    }\n\n    private async Task<bool> CanReadAsync(SecurityTask task, CurrentContextOrganization org)\n    {\n        if (!task.CipherId.HasValue)\n        {\n            // Tasks without cipher IDs are not possible currently\n            return false;\n        }\n\n        if (HasAdminAccessToSecurityTasks(org))\n        {\n            // Admins can read any task for ciphers in the organization\n            return await CipherBelongsToOrgAsync(org, task.CipherId.Value);\n        }\n\n        return await CanReadCipherForOrgAsync(org, task.CipherId.Value);\n    }\n\n    private async Task<bool> CanCreateAsync(SecurityTask task, CurrentContextOrganization org)\n    {\n        if (!task.CipherId.HasValue)\n        {\n            // Tasks without cipher IDs are not possible currently\n            return false;\n        }\n\n        if (!HasAdminAccessToSecurityTasks(org))\n        {\n            // User must be an Admin/Owner or have custom permissions for reporting\n            return false;\n        }\n\n        return await CipherBelongsToOrgAsync(org, task.CipherId.Value);\n    }\n\n    private async Task<bool> CanUpdateAsync(SecurityTask task, CurrentContextOrganization org)\n    {\n        if (!task.CipherId.HasValue)\n        {\n            // Tasks without cipher IDs are not possible currently\n            return false;\n        }\n\n        // Only users that can edit the cipher can update the task\n        return await CanEditCipherForOrgAsync(org, task.CipherId.Value);\n    }\n\n    private async Task<bool> CanEditCipherForOrgAsync(CurrentContextOrganization org, Guid cipherId)\n    {\n        var ciphers = await GetCipherPermissionsForOrgAsync(org);\n\n        return ciphers.TryGetValue(cipherId, out var cipher) && cipher.Edit;\n    }\n\n    private async Task<bool> CanReadCipherForOrgAsync(CurrentContextOrganization org, Guid cipherId)\n    {\n        var ciphers = await GetCipherPermissionsForOrgAsync(org);\n\n        return ciphers.TryGetValue(cipherId, out var cipher) && cipher.Read;\n    }\n\n    private async Task<bool> CipherBelongsToOrgAsync(CurrentContextOrganization org, Guid cipherId)\n    {\n        var ciphers = await GetCipherPermissionsForOrgAsync(org);\n\n        return ciphers.ContainsKey(cipherId);\n    }\n\n    private bool HasAdminAccessToSecurityTasks(CurrentContextOrganization org)\n    {\n        return org is\n        { Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or\n        { Type: OrganizationUserType.Custom, Permissions.AccessReports: true };\n    }\n\n    private async Task<IDictionary<Guid, OrganizationCipherPermission>> GetCipherPermissionsForOrgAsync(CurrentContextOrganization organization)\n    {\n        // Re-use permissions we've already fetched for the organization\n        if (_cipherPermissionCache.TryGetValue(organization.Id, out var cachedCiphers))\n        {\n            return cachedCiphers;\n        }\n\n        var cipherPermissions = await _getCipherPermissionsForUserQuery.GetByOrganization(organization.Id);\n\n        _cipherPermissionCache.Add(organization.Id, cipherPermissions);\n\n        return cipherPermissions;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOperationRequirement.cs",
    "content": "﻿using Microsoft.AspNetCore.Authorization.Infrastructure;\n\nnamespace Bit.Core.Vault.Authorization.SecurityTasks;\n\npublic class SecurityTaskOperationRequirement : OperationAuthorizationRequirement\n{\n    public SecurityTaskOperationRequirement(string name)\n    {\n        Name = name;\n    }\n}\n\npublic static class SecurityTaskOperations\n{\n    public static readonly SecurityTaskOperationRequirement Read = new SecurityTaskOperationRequirement(nameof(Read));\n    public static readonly SecurityTaskOperationRequirement Create = new SecurityTaskOperationRequirement(nameof(Create));\n    public static readonly SecurityTaskOperationRequirement Update = new SecurityTaskOperationRequirement(nameof(Update));\n\n    /// <summary>\n    /// List all security tasks for a specific organization.\n    /// <example><code>\n    /// var orgContext = _currentContext.GetOrganization(organizationId);\n    /// _authorizationService.AuthorizeOrThrowAsync(User, SecurityTaskOperations.ListAllForOrganization, orgContext);\n    /// </code></example>\n    /// </summary>\n    public static readonly SecurityTaskOperationRequirement ListAllForOrganization = new SecurityTaskOperationRequirement(nameof(ListAllForOrganization));\n}\n"
  },
  {
    "path": "src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.Vault.Authorization.SecurityTasks;\n\npublic class\n    SecurityTaskOrganizationAuthorizationHandler : AuthorizationHandler<SecurityTaskOperationRequirement,\n    CurrentContextOrganization>\n{\n    private readonly ICurrentContext _currentContext;\n\n    public SecurityTaskOrganizationAuthorizationHandler(ICurrentContext currentContext)\n    {\n        _currentContext = currentContext;\n    }\n\n    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,\n        SecurityTaskOperationRequirement requirement,\n        CurrentContextOrganization resource)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            return Task.CompletedTask;\n        }\n\n        var authorized = requirement switch\n        {\n            not null when requirement == SecurityTaskOperations.ListAllForOrganization => CanListAllTasksForOrganization(resource),\n            _ => throw new ArgumentOutOfRangeException(nameof(requirement), requirement, null)\n        };\n\n        if (authorized)\n        {\n            context.Succeed(requirement);\n        }\n\n        return Task.CompletedTask;\n    }\n\n    private static bool CanListAllTasksForOrganization(CurrentContextOrganization org)\n    {\n        return org is\n        { Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or\n        { Type: OrganizationUserType.Custom, Permissions.AccessReports: true };\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/ArchiveCiphersCommand.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Vault.Commands.Interfaces;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\n\nnamespace Bit.Core.Vault.Commands;\n\npublic class ArchiveCiphersCommand : IArchiveCiphersCommand\n{\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IPushNotificationService _pushService;\n\n    public ArchiveCiphersCommand(\n        ICipherRepository cipherRepository,\n        IPushNotificationService pushService\n    )\n    {\n        _cipherRepository = cipherRepository;\n        _pushService = pushService;\n    }\n\n    public async Task<ICollection<CipherDetails>> ArchiveManyAsync(IEnumerable<Guid> cipherIds,\n        Guid archivingUserId)\n    {\n        var cipherIdEnumerable = cipherIds as Guid[] ?? cipherIds.ToArray();\n        if (cipherIds == null || cipherIdEnumerable.Length == 0)\n            throw new BadRequestException(\"No cipher ids provided.\");\n\n        var cipherIdsSet = new HashSet<Guid>(cipherIdEnumerable);\n\n        var ciphers = await _cipherRepository.GetManyByUserIdAsync(archivingUserId);\n\n        if (ciphers == null || ciphers.Count == 0)\n        {\n            return [];\n        }\n\n        var archivingCiphers = ciphers\n            .Where(c => cipherIdsSet.Contains(c.Id) && c is { ArchivedDate: null })\n            .ToList();\n\n        var revisionDate = await _cipherRepository.ArchiveAsync(archivingCiphers.Select(c => c.Id), archivingUserId);\n\n        // Adding specifyKind because revisionDate is currently coming back as Unspecified from the database\n        revisionDate = DateTime.SpecifyKind(revisionDate, DateTimeKind.Utc);\n\n        archivingCiphers.ForEach(c =>\n        {\n            c.RevisionDate = revisionDate;\n            c.ArchivedDate = revisionDate;\n        });\n\n        // Will not log an event because the archive feature is limited to individual ciphers, and event logs only apply to organization ciphers.\n\n        // ExcludeCurrentContext to avoid double syncing when archiving a cipher\n\n        await _pushService.PushSyncCiphersAsync(archivingUserId, true);\n\n        return archivingCiphers;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.NotificationCenter.Commands.Interfaces;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Commands.Interfaces;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Queries;\n\npublic class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCommand\n{\n    private readonly IGetSecurityTasksNotificationDetailsQuery _getSecurityTasksNotificationDetailsQuery;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IMailService _mailService;\n    private readonly ICreateNotificationCommand _createNotificationCommand;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public CreateManyTaskNotificationsCommand(\n        IGetSecurityTasksNotificationDetailsQuery getSecurityTasksNotificationDetailsQuery,\n        IOrganizationRepository organizationRepository,\n        IMailService mailService,\n        ICreateNotificationCommand createNotificationCommand,\n        IPushNotificationService pushNotificationService,\n        IOrganizationUserRepository organizationUserRepository)\n    {\n        _getSecurityTasksNotificationDetailsQuery = getSecurityTasksNotificationDetailsQuery;\n        _organizationRepository = organizationRepository;\n        _mailService = mailService;\n        _createNotificationCommand = createNotificationCommand;\n        _pushNotificationService = pushNotificationService;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    public async Task CreateAsync(Guid orgId, IEnumerable<SecurityTask> securityTasks)\n    {\n        var securityTaskCiphers = await _getSecurityTasksNotificationDetailsQuery.GetNotificationDetailsByManyIds(orgId, securityTasks);\n\n        // Get the number of tasks for each user\n        var userTaskCount = securityTaskCiphers.GroupBy(x => x.UserId).Select(x => new UserSecurityTasksCount\n        {\n            UserId = x.Key,\n            Email = x.First().Email,\n            TaskCount = x.Count()\n        }).ToList();\n\n        var organization = await _organizationRepository.GetByIdAsync(orgId);\n        var orgAdminEmails = (await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Admin))\n            .Select(u => u.Email)\n            .ToList();\n\n        var orgOwnerEmails = (await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Owner))\n            .Select(u => u.Email)\n            .ToList();\n\n        // Ensure proper deserialization of emails\n        var orgAdminAndOwnerEmails = orgAdminEmails.Concat(orgOwnerEmails).Distinct().ToList();\n\n        await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount, orgAdminAndOwnerEmails);\n\n        // Break securityTaskCiphers into separate lists by user Id\n        var securityTaskCiphersByUser = securityTaskCiphers.GroupBy(x => x.UserId)\n                                   .ToDictionary(g => g.Key, g => g.ToList());\n\n        foreach (var userId in securityTaskCiphersByUser.Keys)\n        {\n            // Get the security tasks by the user Id\n            var userSecurityTaskCiphers = securityTaskCiphersByUser[userId];\n\n            // Process each user's security task ciphers\n            for (int i = 0; i < userSecurityTaskCiphers.Count; i++)\n            {\n                var userSecurityTaskCipher = userSecurityTaskCiphers[i];\n\n                // Create a notification for the user with the associated task\n                var notification = new Notification\n                {\n                    UserId = userSecurityTaskCipher.UserId,\n                    OrganizationId = orgId,\n                    Priority = Priority.Informational,\n                    ClientType = ClientType.Browser,\n                    TaskId = userSecurityTaskCipher.TaskId\n                };\n\n                await _createNotificationCommand.CreateAsync(notification, false);\n            }\n\n            // Notify the user that they have pending security tasks\n            await _pushNotificationService.PushRefreshSecurityTasksAsync(userId);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/CreateManyTasksCommand.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Authorization.SecurityTasks;\nusing Bit.Core.Vault.Commands.Interfaces;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Models.Api;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.Vault.Commands;\n\npublic class CreateManyTasksCommand : ICreateManyTasksCommand\n{\n    private readonly IAuthorizationService _authorizationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly ISecurityTaskRepository _securityTaskRepository;\n\n    public CreateManyTasksCommand(\n        ISecurityTaskRepository securityTaskRepository,\n        IAuthorizationService authorizationService,\n        ICurrentContext currentContext)\n    {\n        _securityTaskRepository = securityTaskRepository;\n        _authorizationService = authorizationService;\n        _currentContext = currentContext;\n    }\n\n    /// <inheritdoc />\n    public async Task<ICollection<SecurityTask>> CreateAsync(Guid organizationId,\n        IEnumerable<SecurityTaskCreateRequest> tasks)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var tasksList = tasks?.ToList();\n\n        if (tasksList is null || tasksList.Count == 0)\n        {\n            throw new BadRequestException(\"No tasks provided.\");\n        }\n\n        var securityTasks = tasksList.Select(t => new SecurityTask\n        {\n            OrganizationId = organizationId,\n            CipherId = t.CipherId,\n            Type = t.Type,\n            Status = SecurityTaskStatus.Pending\n        }).ToList();\n\n        // Verify authorization for each task\n        foreach (var task in securityTasks)\n        {\n            await _authorizationService.AuthorizeOrThrowAsync(\n                _currentContext.HttpContext.User,\n                task,\n                SecurityTaskOperations.Create);\n        }\n\n        return await _securityTaskRepository.CreateManyAsync(securityTasks);\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/Interfaces/IArchiveCiphersCommand.cs",
    "content": "﻿using Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Vault.Commands.Interfaces;\n\npublic interface IArchiveCiphersCommand\n{\n    /// <summary>\n    /// Archives a cipher. This fills in the ArchivedDate property on a Cipher.\n    /// </summary>\n    /// <param name=\"cipherIds\">Cipher ID to archive.</param>\n    /// <param name=\"archivingUserId\">User ID to check against the Ciphers that are trying to be archived.</param>\n    /// <returns></returns>\n    public Task<ICollection<CipherDetails>> ArchiveManyAsync(IEnumerable<Guid> cipherIds, Guid archivingUserId);\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/Interfaces/ICreateManyTaskNotificationsCommand.cs",
    "content": "﻿using Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Vault.Commands.Interfaces;\n\npublic interface ICreateManyTaskNotificationsCommand\n{\n    /// <summary>\n    /// Creates email and push notifications for the given security tasks.\n    /// </summary>\n    /// <param name=\"organizationId\">The organization Id </param>\n    /// <param name=\"securityTasks\">All applicable security tasks</param>\n    Task CreateAsync(Guid organizationId, IEnumerable<SecurityTask> securityTasks);\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs",
    "content": "﻿using Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Api;\n\nnamespace Bit.Core.Vault.Commands.Interfaces;\n\npublic interface ICreateManyTasksCommand\n{\n    /// <summary>\n    /// Creates multiple security tasks for an organization.\n    /// Each task must be authorized and the user must have the Create permission\n    /// and associated ciphers must belong to the organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The </param>\n    /// <param name=\"tasks\"></param>\n    /// <returns>Collection of created security tasks</returns>\n    Task<ICollection<SecurityTask>> CreateAsync(Guid organizationId, IEnumerable<SecurityTaskCreateRequest> tasks);\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs",
    "content": "﻿namespace Bit.Core.Vault.Commands.Interfaces;\n\npublic interface IMarkNotificationsForTaskAsDeletedCommand\n{\n    /// <summary>\n    /// Marks notifications associated with a given taskId as deleted.\n    /// </summary>\n    /// <param name=\"taskId\">The unique identifier of the task to complete</param>\n    /// <returns>A task representing the async operation</returns>\n    Task MarkAsDeletedAsync(Guid taskId);\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/Interfaces/IMarkTaskAsCompleteCommand.cs",
    "content": "﻿namespace Bit.Core.Vault.Commands.Interfaces;\n\npublic interface IMarkTaskAsCompleteCommand\n{\n    /// <summary>\n    /// Marks a task as complete.\n    /// </summary>\n    /// <param name=\"taskId\">The unique identifier of the task to complete</param>\n    /// <returns>A task representing the async operation</returns>\n    Task CompleteAsync(Guid taskId);\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/Interfaces/IUnarchiveCiphersCommand.cs",
    "content": "﻿using Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Vault.Commands.Interfaces;\n\npublic interface IUnarchiveCiphersCommand\n{\n    /// <summary>\n    /// Unarchives a cipher. This nulls the ArchivedDate property on a Cipher.\n    /// </summary>\n    /// <param name=\"cipherIds\">Cipher ID to unarchive.</param>\n    /// <param name=\"unarchivingUserId\">User ID to check against the Ciphers that are trying to be unarchived.</param>\n    /// <returns></returns>\n    public Task<ICollection<CipherDetails>> UnarchiveManyAsync(IEnumerable<Guid> cipherIds, Guid unarchivingUserId);\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs",
    "content": "﻿using Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Vault.Commands.Interfaces;\n\nnamespace Bit.Core.Vault.Commands;\n\npublic class MarkNotificationsForTaskAsDeletedCommand : IMarkNotificationsForTaskAsDeletedCommand\n{\n    private readonly INotificationRepository _notificationRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n\n    public MarkNotificationsForTaskAsDeletedCommand(\n        INotificationRepository notificationRepository,\n        IPushNotificationService pushNotificationService)\n    {\n        _notificationRepository = notificationRepository;\n        _pushNotificationService = pushNotificationService;\n\n    }\n\n    public async Task MarkAsDeletedAsync(Guid taskId)\n    {\n        var userIds = await _notificationRepository.MarkNotificationsAsDeletedByTask(taskId);\n\n        // For each user associated with the notifications, send a push notification so local tasks can be updated.\n        var uniqueUserIds = userIds.Distinct();\n        foreach (var id in uniqueUserIds)\n        {\n            await _pushNotificationService.PushRefreshSecurityTasksAsync(id);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Authorization.SecurityTasks;\nusing Bit.Core.Vault.Commands.Interfaces;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.Vault.Commands;\n\npublic class MarkTaskAsCompletedCommand : IMarkTaskAsCompleteCommand\n{\n    private readonly ISecurityTaskRepository _securityTaskRepository;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly ICurrentContext _currentContext;\n    private readonly IMarkNotificationsForTaskAsDeletedCommand _markNotificationsForTaskAsDeletedAsync;\n\n\n    public MarkTaskAsCompletedCommand(\n        ISecurityTaskRepository securityTaskRepository,\n        IAuthorizationService authorizationService,\n        ICurrentContext currentContext,\n        IMarkNotificationsForTaskAsDeletedCommand markNotificationsForTaskAsDeletedAsync)\n    {\n        _securityTaskRepository = securityTaskRepository;\n        _authorizationService = authorizationService;\n        _currentContext = currentContext;\n        _markNotificationsForTaskAsDeletedAsync = markNotificationsForTaskAsDeletedAsync;\n    }\n\n    /// <inheritdoc />\n    public async Task CompleteAsync(Guid taskId)\n    {\n        if (!_currentContext.UserId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var task = await _securityTaskRepository.GetByIdAsync(taskId);\n        if (task is null)\n        {\n            throw new NotFoundException();\n        }\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, task,\n            SecurityTaskOperations.Update);\n\n        task.Status = SecurityTaskStatus.Completed;\n        task.RevisionDate = DateTime.UtcNow;\n\n        await _securityTaskRepository.ReplaceAsync(task);\n\n        // Mark all notifications related to this task as deleted\n        await _markNotificationsForTaskAsDeletedAsync.MarkAsDeletedAsync(taskId);\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Commands/UnarchiveCiphersCommand.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Vault.Commands.Interfaces;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\n\nnamespace Bit.Core.Vault.Commands;\n\npublic class UnarchiveCiphersCommand : IUnarchiveCiphersCommand\n{\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IPushNotificationService _pushService;\n\n    public UnarchiveCiphersCommand(\n        ICipherRepository cipherRepository,\n        IPushNotificationService pushService\n    )\n    {\n        _cipherRepository = cipherRepository;\n        _pushService = pushService;\n    }\n\n    public async Task<ICollection<CipherDetails>> UnarchiveManyAsync(IEnumerable<Guid> cipherIds,\n        Guid unarchivingUserId)\n    {\n        var cipherIdEnumerable = cipherIds as Guid[] ?? cipherIds.ToArray();\n        if (cipherIds == null || cipherIdEnumerable.Length == 0)\n            throw new BadRequestException(\"No cipher ids provided.\");\n\n        var cipherIdsSet = new HashSet<Guid>(cipherIdEnumerable);\n\n        var ciphers = await _cipherRepository.GetManyByUserIdAsync(unarchivingUserId);\n\n        if (ciphers == null || ciphers.Count == 0)\n        {\n            return [];\n        }\n\n        var unarchivingCiphers = ciphers\n            .Where(c => cipherIdsSet.Contains(c.Id) && c is { ArchivedDate: not null })\n            .ToList();\n\n        var revisionDate =\n            await _cipherRepository.UnarchiveAsync(unarchivingCiphers.Select(c => c.Id), unarchivingUserId);\n        // Adding specifyKind because revisionDate is currently coming back as Unspecified from the database\n        revisionDate = DateTime.SpecifyKind(revisionDate, DateTimeKind.Utc);\n\n        unarchivingCiphers.ForEach(c =>\n        {\n            c.RevisionDate = revisionDate;\n            c.ArchivedDate = null;\n        });\n        // Will not log an event because the archive feature is limited to individual ciphers, and event logs only apply to organization ciphers.\n\n        // ExcludeCurrentContext to avoid double syncing when unarchiving a cipher\n\n        await _pushService.PushSyncCiphersAsync(unarchivingUserId, true);\n\n        return unarchivingCiphers;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Entities/Cipher.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Entities;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Vault.Entities;\n\npublic class Cipher : ITableObject<Guid>, ICloneable\n{\n    private Dictionary<string, CipherAttachment.MetaData> _attachmentData;\n\n    public Guid Id { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public Enums.CipherType Type { get; set; }\n    public string Data { get; set; }\n    public string Favorites { get; set; }\n    public string Folders { get; set; }\n    public string Attachments { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n    public DateTime? DeletedDate { get; set; }\n    public Enums.CipherRepromptType? Reprompt { get; set; }\n    public string Key { get; set; }\n    public string Archives { get; set; }\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n\n    public Dictionary<string, CipherAttachment.MetaData> GetAttachments()\n    {\n        if (string.IsNullOrWhiteSpace(Attachments))\n        {\n            return null;\n        }\n\n        if (_attachmentData != null)\n        {\n            return _attachmentData;\n        }\n\n        try\n        {\n            _attachmentData = JsonSerializer.Deserialize<Dictionary<string, CipherAttachment.MetaData>>(Attachments);\n            foreach (var kvp in _attachmentData)\n            {\n                kvp.Value.AttachmentId = kvp.Key;\n                if (kvp.Value.TempMetadata != null)\n                {\n                    kvp.Value.TempMetadata.AttachmentId = kvp.Key;\n                }\n            }\n            return _attachmentData;\n        }\n        catch\n        {\n            return null;\n        }\n    }\n\n    public void SetAttachments(Dictionary<string, CipherAttachment.MetaData> data)\n    {\n        if (data == null || data.Count == 0)\n        {\n            _attachmentData = null;\n            Attachments = null;\n            return;\n        }\n\n        _attachmentData = data;\n        Attachments = JsonSerializer.Serialize(_attachmentData);\n    }\n\n    public void AddAttachment(string id, CipherAttachment.MetaData data)\n    {\n        var attachments = GetAttachments();\n        if (attachments == null)\n        {\n            attachments = new Dictionary<string, CipherAttachment.MetaData>();\n        }\n\n        attachments.Add(id, data);\n        SetAttachments(attachments);\n    }\n\n    public void DeleteAttachment(string id)\n    {\n        var attachments = GetAttachments();\n        if (!attachments?.ContainsKey(id) ?? true)\n        {\n            return;\n        }\n\n        attachments.Remove(id);\n        SetAttachments(attachments);\n    }\n\n    public bool ContainsAttachment(string id)\n    {\n        var attachments = GetAttachments();\n        return attachments?.ContainsKey(id) ?? false;\n    }\n\n    object ICloneable.Clone() => Clone();\n    public Cipher Clone()\n    {\n        var clone = CoreHelpers.CloneObject(this);\n        clone.CreationDate = CreationDate;\n        clone.RevisionDate = RevisionDate;\n\n        return clone;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Entities/SecurityTask.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Vault.Entities;\n\npublic class SecurityTask : ITableObject<Guid>\n{\n    public Guid Id { get; set; }\n    public Guid OrganizationId { get; set; }\n    public Guid? CipherId { get; set; }\n    public Enums.SecurityTaskType Type { get; set; }\n    public Enums.SecurityTaskStatus Status { get; set; }\n    public DateTime CreationDate { get; set; } = DateTime.UtcNow;\n    public DateTime RevisionDate { get; set; } = DateTime.UtcNow;\n\n    public void SetNewId()\n    {\n        Id = CoreHelpers.GenerateComb();\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Entities/SecurityTaskMetrics.cs",
    "content": "﻿namespace Bit.Core.Vault.Entities;\n\npublic class SecurityTaskMetrics\n{\n    public SecurityTaskMetrics(int completedTasks, int totalTasks)\n    {\n        CompletedTasks = completedTasks;\n        TotalTasks = totalTasks;\n    }\n\n    public int CompletedTasks { get; set; }\n    public int TotalTasks { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Enums/CipherRepromptType.cs",
    "content": "﻿namespace Bit.Core.Vault.Enums;\n\npublic enum CipherRepromptType : byte\n{\n    None = 0,\n    Password = 1,\n}\n"
  },
  {
    "path": "src/Core/Vault/Enums/CipherStateAction.cs",
    "content": "﻿namespace Bit.Core.Vault.Enums;\n\npublic enum CipherStateAction\n{\n    Restore,\n    Unarchive,\n    Archive,\n    SoftDelete,\n    HardDelete,\n}\n"
  },
  {
    "path": "src/Core/Vault/Enums/CipherType.cs",
    "content": "﻿namespace Bit.Core.Vault.Enums;\n\npublic enum CipherType : byte\n{\n    // Folder is deprecated\n    //Folder = 0,\n    Login = 1,\n    SecureNote = 2,\n    Card = 3,\n    Identity = 4,\n    SSHKey = 5,\n}\n"
  },
  {
    "path": "src/Core/Vault/Enums/FieldType.cs",
    "content": "﻿namespace Bit.Core.Vault.Enums;\n\npublic enum FieldType : byte\n{\n    Text = 0,\n    Hidden = 1,\n    Boolean = 2,\n    Linked = 3,\n}\n"
  },
  {
    "path": "src/Core/Vault/Enums/SecureNoteType.cs",
    "content": "﻿namespace Bit.Core.Vault.Enums;\n\npublic enum SecureNoteType : byte\n{\n    Generic = 0\n}\n"
  },
  {
    "path": "src/Core/Vault/Enums/SecurityTaskStatus.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Vault.Enums;\n\npublic enum SecurityTaskStatus : byte\n{\n    /// <summary>\n    /// Default status for newly created tasks that have not been completed.\n    /// </summary>\n    [Display(Name = \"Pending\")]\n    Pending = 0,\n\n    /// <summary>\n    /// Status when a task is considered complete and has no remaining actions\n    /// </summary>\n    [Display(Name = \"Completed\")]\n    Completed = 1,\n}\n"
  },
  {
    "path": "src/Core/Vault/Enums/SecurityTaskType.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Core.Vault.Enums;\n\npublic enum SecurityTaskType : byte\n{\n    /// <summary>\n    /// Task to update a cipher's password that was found to be at-risk by an administrator\n    /// </summary>\n    [Display(Name = \"Update at-risk credential\")]\n    UpdateAtRiskCredential = 0\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs",
    "content": "﻿using Bit.Core.Vault.Enums;\n\nnamespace Bit.Core.Vault.Models.Api;\n\npublic class SecurityTaskCreateRequest\n{\n    public SecurityTaskType Type { get; set; }\n    public Guid? CipherId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/AttachmentResponseData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class AttachmentResponseData\n{\n    public string Id { get; set; }\n    public CipherAttachment.MetaData Data { get; set; }\n    public Cipher Cipher { get; set; }\n    public string Url { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherAttachment.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json.Serialization;\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherAttachment\n{\n    public Guid Id { get; set; }\n    public Guid? UserId { get; set; }\n    public Guid? OrganizationId { get; set; }\n    public string AttachmentId { get; set; }\n    public string AttachmentData { get; set; }\n\n    public class MetaData\n    {\n        private long _size;\n\n        // We serialize Size as a string since JSON (or Javascript) doesn't support full precision for long numbers\n        [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]\n        public long Size\n        {\n            get { return _size; }\n            set { _size = value; }\n        }\n\n        public string FileName { get; set; }\n        public string Key { get; set; }\n\n        public string ContainerName { get; set; } = \"attachments\";\n        public bool Validated { get; set; } = true;\n\n        // This is stored alongside metadata as an identifier. It does not need repeating in serialization\n        [JsonIgnore]\n        public string AttachmentId { get; set; }\n\n        /// <summary>\n        /// Temporary metadata used to store original metadata on migrations from a user-owned attachment to an organization-owned one\n        /// </summary>\n        public MetaData TempMetadata { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherCardData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherCardData : CipherData\n{\n    public CipherCardData() { }\n\n    public string CardholderName { get; set; }\n    public string Brand { get; set; }\n    public string Number { get; set; }\n    public string ExpMonth { get; set; }\n    public string ExpYear { get; set; }\n    public string Code { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic abstract class CipherData\n{\n    public CipherData() { }\n\n    public string Name { get; set; }\n    public string Notes { get; set; }\n    public IEnumerable<CipherFieldData> Fields { get; set; }\n    public IEnumerable<CipherPasswordHistoryData> PasswordHistory { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherDetails.cs",
    "content": "﻿using Bit.Core.Entities;\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherDetails : CipherOrganizationDetails\n{\n    public Guid? FolderId { get; set; }\n    public bool Favorite { get; set; }\n    public bool Edit { get; set; }\n    public bool ViewPassword { get; set; }\n    public bool Manage { get; set; }\n    // Per-user archived date from Archives JSON.\n    public DateTime? ArchivedDate { get; set; }\n    public CipherDetails() { }\n\n    public CipherDetails(CipherOrganizationDetails cipher)\n    {\n        Id = cipher.Id;\n        UserId = cipher.UserId;\n        OrganizationId = cipher.OrganizationId;\n        Type = cipher.Type;\n        Data = cipher.Data;\n        Favorites = cipher.Favorites;\n        Folders = cipher.Folders;\n        Attachments = cipher.Attachments;\n        CreationDate = cipher.CreationDate;\n        RevisionDate = cipher.RevisionDate;\n        DeletedDate = cipher.DeletedDate;\n        Reprompt = cipher.Reprompt;\n        Key = cipher.Key;\n        OrganizationUseTotp = cipher.OrganizationUseTotp;\n    }\n}\n\npublic class CipherDetailsWithCollections : CipherDetails\n{\n    public CipherDetailsWithCollections(\n        CipherDetails cipher,\n        Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict)\n    {\n        Id = cipher.Id;\n        UserId = cipher.UserId;\n        OrganizationId = cipher.OrganizationId;\n        Type = cipher.Type;\n        Data = cipher.Data;\n        Favorites = cipher.Favorites;\n        Folders = cipher.Folders;\n        Attachments = cipher.Attachments;\n        CreationDate = cipher.CreationDate;\n        RevisionDate = cipher.RevisionDate;\n        DeletedDate = cipher.DeletedDate;\n        Reprompt = cipher.Reprompt;\n        Key = cipher.Key;\n        FolderId = cipher.FolderId;\n        ArchivedDate = cipher.ArchivedDate;\n        Favorite = cipher.Favorite;\n        Edit = cipher.Edit;\n        ViewPassword = cipher.ViewPassword;\n        Manage = cipher.Manage;\n\n        CollectionIds = collectionCiphersGroupDict.TryGetValue(Id, out var value)\n            ? value.Select(cc => cc.CollectionId)\n            : Array.Empty<Guid>();\n    }\n\n    public CipherDetailsWithCollections(CipherOrganizationDetails cipher, Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict)\n    {\n        Id = cipher.Id;\n        UserId = cipher.UserId;\n        OrganizationId = cipher.OrganizationId;\n        Type = cipher.Type;\n        Data = cipher.Data;\n        Favorites = cipher.Favorites;\n        Folders = cipher.Folders;\n        Attachments = cipher.Attachments;\n        CreationDate = cipher.CreationDate;\n        RevisionDate = cipher.RevisionDate;\n        DeletedDate = cipher.DeletedDate;\n        Reprompt = cipher.Reprompt;\n        Key = cipher.Key;\n        OrganizationUseTotp = cipher.OrganizationUseTotp;\n\n        CollectionIds = collectionCiphersGroupDict != null && collectionCiphersGroupDict.TryGetValue(Id, out var value)\n            ? value.Select(cc => cc.CollectionId)\n            : Array.Empty<Guid>();\n    }\n\n    public IEnumerable<Guid> CollectionIds { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherFieldData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Vault.Enums;\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherFieldData\n{\n    public CipherFieldData() { }\n\n    public FieldType Type { get; set; }\n    public string Name { get; set; }\n    public string Value { get; set; }\n    public int? LinkedId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherIdentityData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherIdentityData : CipherData\n{\n    public CipherIdentityData() { }\n\n    public string Title { get; set; }\n    public string FirstName { get; set; }\n    public string MiddleName { get; set; }\n    public string LastName { get; set; }\n    public string Address1 { get; set; }\n    public string Address2 { get; set; }\n    public string Address3 { get; set; }\n    public string City { get; set; }\n    public string State { get; set; }\n    public string PostalCode { get; set; }\n    public string Country { get; set; }\n    public string Company { get; set; }\n    public string Email { get; set; }\n    public string Phone { get; set; }\n    public string SSN { get; set; }\n    public string Username { get; set; }\n    public string PassportNumber { get; set; }\n    public string LicenseNumber { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherLoginData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherLoginData : CipherData\n{\n    private string _uri;\n\n    public CipherLoginData() { }\n\n    public string Uri\n    {\n        get => Uris?.FirstOrDefault()?.Uri ?? _uri;\n        set { _uri = value; }\n    }\n    public IEnumerable<CipherLoginUriData> Uris { get; set; }\n    public string Username { get; set; }\n    public string Password { get; set; }\n    public DateTime? PasswordRevisionDate { get; set; }\n    public string Totp { get; set; }\n    public bool? AutofillOnPageLoad { get; set; }\n    public CipherLoginFido2CredentialData[] Fido2Credentials { get; set; }\n\n    public class CipherLoginUriData\n    {\n        public CipherLoginUriData() { }\n\n        public string Uri { get; set; }\n        public string UriChecksum { get; set; }\n        public UriMatchType? Match { get; set; } = null;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherLoginFido2CredentialData\n{\n    public CipherLoginFido2CredentialData() { }\n\n    public string CredentialId { get; set; }\n    public string KeyType { get; set; }\n    public string KeyAlgorithm { get; set; }\n    public string KeyCurve { get; set; }\n    public string KeyValue { get; set; }\n    public string RpId { get; set; }\n    public string RpName { get; set; }\n    public string UserHandle { get; set; }\n    public string UserName { get; set; }\n    public string UserDisplayName { get; set; }\n    public string Counter { get; set; }\n    public string Discoverable { get; set; }\n    public DateTime CreationDate { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherOrganizationDetails.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherOrganizationDetails : Cipher\n{\n    public bool OrganizationUseTotp { get; set; }\n}\n\npublic class CipherOrganizationDetailsWithCollections : CipherOrganizationDetails\n{\n    public CipherOrganizationDetailsWithCollections(\n        CipherOrganizationDetails cipher,\n        Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict)\n    {\n        Id = cipher.Id;\n        UserId = cipher.UserId;\n        OrganizationId = cipher.OrganizationId;\n        Type = cipher.Type;\n        Data = cipher.Data;\n        Favorites = cipher.Favorites;\n        Folders = cipher.Folders;\n        Attachments = cipher.Attachments;\n        CreationDate = cipher.CreationDate;\n        RevisionDate = cipher.RevisionDate;\n        DeletedDate = cipher.DeletedDate;\n        Reprompt = cipher.Reprompt;\n        Key = cipher.Key;\n        OrganizationUseTotp = cipher.OrganizationUseTotp;\n\n        CollectionIds = collectionCiphersGroupDict.TryGetValue(Id, out var value)\n            ? value.Select(cc => cc.CollectionId)\n            : Array.Empty<Guid>();\n    }\n    public IEnumerable<Guid> CollectionIds { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherPasswordHistoryData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherPasswordHistoryData\n{\n    public CipherPasswordHistoryData() { }\n\n    public string Password { get; set; }\n    public DateTime LastUsedDate { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherSSHKeyData.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherSSHKeyData : CipherData\n{\n    public CipherSSHKeyData() { }\n\n    public string PrivateKey { get; set; }\n    public string PublicKey { get; set; }\n    public string KeyFingerprint { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/CipherSecureNoteData.cs",
    "content": "﻿using Bit.Core.Vault.Enums;\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class CipherSecureNoteData : CipherData\n{\n    public CipherSecureNoteData() { }\n\n    public SecureNoteType Type { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/DeleteAttachmentReponseData.cs",
    "content": "﻿using Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Vault.Models.Data;\n\npublic class DeleteAttachmentResponseData\n{\n    public Cipher Cipher { get; set; }\n\n    public DeleteAttachmentResponseData(Cipher cipher)\n    {\n        Cipher = cipher;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/OrganizationCipherPermission.cs",
    "content": "﻿namespace Bit.Core.Vault.Models.Data;\n\n/// <summary>\n/// Data model that represents a Users permissions for a given cipher\n/// that belongs to an organization.\n/// To be used internally for authorization.\n/// </summary>\npublic class OrganizationCipherPermission\n{\n    /// <summary>\n    /// The cipher Id\n    /// </summary>\n    public Guid Id { get; set; }\n\n    /// <summary>\n    /// The organization Id that the cipher belongs to.\n    /// </summary>\n    public Guid OrganizationId { get; set; }\n\n    /// <summary>\n    /// The user can read the cipher.\n    /// See <see cref=\"ViewPassword\"/> for password visibility.\n    /// </summary>\n    public bool Read { get; set; }\n\n    /// <summary>\n    /// The user has permission to view the password of the cipher.\n    /// </summary>\n    public bool ViewPassword { get; set; }\n\n    /// <summary>\n    /// The user has permission to edit the cipher.\n    /// </summary>\n    public bool Edit { get; set; }\n\n    /// <summary>\n    /// The user has manage level access to the cipher.\n    /// </summary>\n    public bool Manage { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/UserCipherForTask.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Vault.Models.Data;\n\n/// <summary>\n/// Minimal data model that represents a User and the associated cipher for a security task.\n/// Only to be used for query responses. For full data model, <see cref=\"UserSecurityTaskCipher\"/>.\n/// </summary>\npublic class UserCipherForTask\n{\n    /// <summary>\n    /// The user's Id.\n    /// </summary>\n    public Guid UserId { get; set; }\n\n    /// <summary>\n    /// The user's email.\n    /// </summary>\n    public string Email { get; set; }\n\n    /// <summary>\n    /// The cipher Id of the security task.\n    /// </summary>\n    public Guid CipherId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Vault.Models.Data;\n\n/// <summary>\n/// Data model that represents a User and the associated cipher for a security task.\n/// </summary>\npublic class UserSecurityTaskCipher\n{\n    /// <summary>\n    /// The user's Id.\n    /// </summary>\n    public Guid UserId { get; set; }\n\n    /// <summary>\n    /// The user's email.\n    /// </summary>\n    public string Email { get; set; }\n\n    /// <summary>\n    /// The cipher Id of the security task.\n    /// </summary>\n    public Guid CipherId { get; set; }\n\n    /// <summary>\n    /// The Id of the security task.\n    /// </summary>\n    public Guid TaskId { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Models/Data/UserSecurityTasksCount.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Core.Vault.Models.Data;\n\n/// <summary>\n/// Data model that represents a User and the amount of actionable security tasks.\n/// </summary>\npublic class UserSecurityTasksCount\n{\n    /// <summary>\n    /// The user's Id.\n    /// </summary>\n    public Guid UserId { get; set; }\n\n    /// <summary>\n    /// The user's email.\n    /// </summary>\n    public string Email { get; set; }\n\n    /// <summary>\n    /// The number of actionable security tasks for the respective users.\n    /// </summary>\n    public int TaskCount { get; set; }\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuery\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IApplicationCacheService _applicationCacheService;\n\n    public GetCipherPermissionsForUserQuery(ICurrentContext currentContext, ICipherRepository cipherRepository, IApplicationCacheService applicationCacheService)\n    {\n        _currentContext = currentContext;\n        _cipherRepository = cipherRepository;\n        _applicationCacheService = applicationCacheService;\n    }\n\n    public async Task<IDictionary<Guid, OrganizationCipherPermission>> GetByOrganization(Guid organizationId)\n    {\n        var org = _currentContext.GetOrganization(organizationId);\n        var userId = _currentContext.UserId;\n\n        if (org == null || !userId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        var cipherPermissions =\n            (await _cipherRepository.GetCipherPermissionsForOrganizationAsync(organizationId, userId.Value))\n            .ToList()\n            .ToDictionary(c => c.Id);\n\n        if (await CanEditAllCiphersAsync(org))\n        {\n            foreach (var cipher in cipherPermissions)\n            {\n                cipher.Value.Read = true;\n                cipher.Value.Edit = true;\n                cipher.Value.Manage = true;\n                cipher.Value.ViewPassword = true;\n            }\n        }\n        else if (CanAccessUnassignedCiphers(org))\n        {\n            var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId);\n            foreach (var unassignedCipher in unassignedCiphers)\n            {\n                if (cipherPermissions.TryGetValue(unassignedCipher.Id, out var p))\n                {\n                    p.Read = true;\n                    p.Edit = true;\n                    p.Manage = true;\n                    p.ViewPassword = true;\n                }\n            }\n        }\n\n        return cipherPermissions;\n    }\n\n    private async Task<bool> CanEditAllCiphersAsync(CurrentContextOrganization org)\n    {\n        // Custom users with EditAnyCollection permissions can always edit all ciphers\n        if (org is { Type: OrganizationUserType.Custom, Permissions.EditAnyCollection: true })\n        {\n            return true;\n        }\n\n        var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(org.Id);\n\n        // Owners/Admins can only edit all ciphers if the organization has the setting enabled\n        if (orgAbility is { AllowAdminAccessToAllCollectionItems: true } && org is\n            { Type: OrganizationUserType.Admin or OrganizationUserType.Owner })\n        {\n            return true;\n        }\n\n        return false;\n    }\n\n    private bool CanAccessUnassignedCiphers(CurrentContextOrganization org)\n    {\n        if (org is\n        { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or\n        { Permissions.EditAnyCollection: true })\n        {\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/GetSecurityTasksNotificationDetailsQuery.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic class GetSecurityTasksNotificationDetailsQuery : IGetSecurityTasksNotificationDetailsQuery\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly ICipherRepository _cipherRepository;\n\n    public GetSecurityTasksNotificationDetailsQuery(ICurrentContext currentContext, ICipherRepository cipherRepository)\n    {\n        _currentContext = currentContext;\n        _cipherRepository = cipherRepository;\n    }\n\n    public async Task<ICollection<UserSecurityTaskCipher>> GetNotificationDetailsByManyIds(Guid organizationId, IEnumerable<SecurityTask> tasks)\n    {\n        var org = _currentContext.GetOrganization(organizationId);\n\n        if (org == null)\n        {\n            throw new NotFoundException();\n        }\n\n        var userSecurityTaskCiphers = await _cipherRepository.GetUserSecurityTasksByCipherIdsAsync(organizationId, tasks);\n\n        return userSecurityTaskCiphers;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/GetTaskDetailsForUserQuery.cs",
    "content": "﻿using Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Repositories;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic class GetTaskDetailsForUserQuery(ISecurityTaskRepository securityTaskRepository) : IGetTaskDetailsForUserQuery\n{\n    /// <inheritdoc />\n    public async Task<IEnumerable<SecurityTask>> GetTaskDetailsForUserAsync(Guid userId,\n        SecurityTaskStatus? status = null)\n        => await securityTaskRepository.GetManyByUserIdStatusAsync(userId, status);\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/GetTaskMetricsForOrganizationQuery.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Authorization.SecurityTasks;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic class GetTaskMetricsForOrganizationQuery : IGetTaskMetricsForOrganizationQuery\n{\n    private readonly ISecurityTaskRepository _securityTaskRepository;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly ICurrentContext _currentContext;\n\n    public GetTaskMetricsForOrganizationQuery(\n        ISecurityTaskRepository securityTaskRepository,\n        IAuthorizationService authorizationService,\n        ICurrentContext currentContext\n    )\n    {\n        _securityTaskRepository = securityTaskRepository;\n        _authorizationService = authorizationService;\n        _currentContext = currentContext;\n    }\n\n    public async Task<SecurityTaskMetrics> GetTaskMetrics(Guid organizationId)\n    {\n        var organization = _currentContext.GetOrganization(organizationId);\n        var userId = _currentContext.UserId;\n\n        if (organization == null || !userId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, organization, SecurityTaskOperations.ListAllForOrganization);\n\n        return await _securityTaskRepository.GetTaskMetricsAsync(organizationId);\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/GetTasksForOrganizationQuery.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Authorization.SecurityTasks;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Repositories;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic class GetTasksForOrganizationQuery : IGetTasksForOrganizationQuery\n{\n    private readonly ISecurityTaskRepository _securityTaskRepository;\n    private readonly IAuthorizationService _authorizationService;\n    private readonly ICurrentContext _currentContext;\n\n    public GetTasksForOrganizationQuery(\n        ISecurityTaskRepository securityTaskRepository,\n        IAuthorizationService authorizationService,\n        ICurrentContext currentContext\n    )\n    {\n        _securityTaskRepository = securityTaskRepository;\n        _authorizationService = authorizationService;\n        _currentContext = currentContext;\n    }\n\n    public async Task<ICollection<SecurityTask>> GetTasksAsync(Guid organizationId,\n        SecurityTaskStatus? status = null)\n    {\n        var organization = _currentContext.GetOrganization(organizationId);\n        var userId = _currentContext.UserId;\n\n        if (organization == null || !userId.HasValue)\n        {\n            throw new NotFoundException();\n        }\n\n        await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, organization, SecurityTaskOperations.ListAllForOrganization);\n\n        return (await _securityTaskRepository.GetManyByOrganizationIdStatusAsync(organizationId, status)).ToList();\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/IGetCipherPermissionsForUserQuery.cs",
    "content": "﻿using Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic interface IGetCipherPermissionsForUserQuery\n{\n    /// <summary>\n    /// Retrieves the permissions of every organization cipher (including unassigned) for the\n    /// ICurrentContext's user.\n    ///\n    /// It considers the Collection Management setting for allowing Admin/Owners access to all ciphers.\n    /// </summary>\n    /// <remarks>\n    /// The primary use case of this query is internal cipher authorization logic.\n    /// </remarks>\n    /// <param name=\"organizationId\"></param>\n    /// <returns>A dictionary of CipherIds and a corresponding OrganizationCipherPermission</returns>\n    public Task<IDictionary<Guid, OrganizationCipherPermission>> GetByOrganization(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/IGetSecurityTasksNotificationDetailsQuery.cs",
    "content": "﻿using Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic interface IGetSecurityTasksNotificationDetailsQuery\n{\n    /// <summary>\n    /// Retrieves all users within the given organization that are applicable to the given security tasks.\n    ///\n    /// <param name=\"organizationId\"></param>\n    /// <param name=\"tasks\"></param>\n    /// <returns>A dictionary of UserIds and the corresponding amount of security tasks applicable to them.</returns>\n    /// </summary>\n    public Task<ICollection<UserSecurityTaskCipher>> GetNotificationDetailsByManyIds(Guid organizationId, IEnumerable<SecurityTask> tasks);\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/IGetTaskDetailsForUserQuery.cs",
    "content": "﻿using Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic interface IGetTaskDetailsForUserQuery\n{\n    /// <summary>\n    /// Retrieves security tasks for a user based on their organization and cipher access permissions.\n    /// </summary>\n    /// <param name=\"userId\">The Id of the user retrieving tasks</param>\n    /// <param name=\"status\">Optional filter for task status. If not provided, returns tasks of all statuses</param>\n    /// <returns>A collection of security tasks</returns>\n    Task<IEnumerable<SecurityTask>> GetTaskDetailsForUserAsync(Guid userId, SecurityTaskStatus? status = null);\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/IGetTaskMetricsForOrganizationQuery.cs",
    "content": "﻿using Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic interface IGetTaskMetricsForOrganizationQuery\n{\n    /// <summary>\n    /// Retrieves security task metrics for an organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The Id of the organization</param>\n    /// <returns>Metrics for all security tasks within an organization.</returns>\n    Task<SecurityTaskMetrics> GetTaskMetrics(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/IGetTasksForOrganizationQuery.cs",
    "content": "﻿using Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic interface IGetTasksForOrganizationQuery\n{\n    /// <summary>\n    /// Retrieves all security tasks for an organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The Id of the organization</param>\n    /// <param name=\"status\">Optional filter for task status. If not provided, returns tasks of all statuses</param>\n    /// <returns>A collection of security tasks</returns>\n    Task<ICollection<SecurityTask>> GetTasksAsync(Guid organizationId, SecurityTaskStatus? status = null);\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/IOrganizationCiphersQuery.cs",
    "content": "﻿using Bit.Core.Exceptions;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Vault.Queries;\n\n/// <summary>\n/// Helper queries for retrieving cipher details belonging to an organization including collection information.\n/// </summary>\n/// <remarks>It does not perform any internal authorization checks.</remarks>\npublic interface IOrganizationCiphersQuery\n{\n    /// <summary>\n    /// Returns ciphers belonging to the organization that the user has been assigned to via collections.\n    /// </summary>\n    /// <exception cref=\"FeatureUnavailableException\"></exception>\n    public Task<IEnumerable<CipherDetailsWithCollections>> GetOrganizationCiphersForUser(Guid organizationId, Guid userId);\n\n    /// <summary>\n    /// Returns all ciphers belonging to the organization.\n    /// </summary>\n    /// <param name=\"organizationId\"></param>\n    /// <exception cref=\"FeatureUnavailableException\"></exception>\n    public Task<IEnumerable<CipherOrganizationDetailsWithCollections>> GetAllOrganizationCiphers(Guid organizationId);\n\n    /// <summary>\n    /// Returns ciphers belonging to the organization that are not assigned to any collection.\n    /// </summary>\n    /// <exception cref=\"FeatureUnavailableException\"></exception>\n    Task<IEnumerable<CipherOrganizationDetails>> GetUnassignedOrganizationCiphers(Guid organizationId);\n\n    /// <summary>\n    /// Returns ciphers belonging to the organization that are in the specified collections.\n    /// </summary>\n    /// <remarks>\n    /// Note that the <see cref=\"CipherOrganizationDetailsWithCollections.CollectionIds\"/> will include all collections\n    /// the cipher belongs to even if it is not in the <paramref name=\"collectionIds\"/> parameter.\n    /// </remarks>\n    public Task<IEnumerable<CipherOrganizationDetailsWithCollections>> GetOrganizationCiphersByCollectionIds(\n        Guid organizationId, IEnumerable<Guid> collectionIds);\n\n    /// <summary>\n    /// Returns all organization ciphers except those in default user collections.\n    /// </summary>\n    public Task<IEnumerable<CipherOrganizationDetailsWithCollections>>\n        GetAllOrganizationCiphersExcludingDefaultUserCollections(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Vault/Queries/OrganizationCiphersQuery.cs",
    "content": "﻿using Bit.Core.Repositories;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\n\nnamespace Bit.Core.Vault.Queries;\n\npublic class OrganizationCiphersQuery : IOrganizationCiphersQuery\n{\n    private readonly ICipherRepository _cipherRepository;\n    private readonly ICollectionCipherRepository _collectionCipherRepository;\n\n    public OrganizationCiphersQuery(ICipherRepository cipherRepository, ICollectionCipherRepository collectionCipherRepository)\n    {\n        _cipherRepository = cipherRepository;\n        _collectionCipherRepository = collectionCipherRepository;\n    }\n\n    /// <summary>\n    /// Returns ciphers belonging to the organization that the user has been assigned to via collections.\n    /// </summary>\n    public async Task<IEnumerable<CipherDetailsWithCollections>> GetOrganizationCiphersForUser(Guid organizationId, Guid userId)\n    {\n        var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: true);\n        var orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId).ToList();\n        var orgCipherIds = orgCiphers.Select(c => c.Id);\n\n        var collectionCiphers = await _collectionCipherRepository.GetManySharedByOrganizationIdAsync(organizationId);\n        var collectionCiphersGroupDict = collectionCiphers\n            .Where(c => orgCipherIds.Contains(c.CipherId))\n            .GroupBy(c => c.CipherId).ToDictionary(s => s.Key);\n\n        return orgCiphers.Select(c => new CipherDetailsWithCollections(c, collectionCiphersGroupDict));\n    }\n\n    /// <summary>\n    /// Returns all ciphers belonging to the organization.\n    /// </summary>\n    /// <param name=\"organizationId\"></param>\n    public async Task<IEnumerable<CipherOrganizationDetailsWithCollections>> GetAllOrganizationCiphers(Guid organizationId)\n    {\n        var orgCiphers = await _cipherRepository.GetManyOrganizationDetailsByOrganizationIdAsync(organizationId);\n        var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(organizationId);\n        var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);\n\n        return orgCiphers.Select(c => new CipherOrganizationDetailsWithCollections(c, collectionCiphersGroupDict));\n    }\n\n    /// <summary>\n    /// Returns ciphers belonging to the organization that are not assigned to any collection.\n    /// </summary>\n    public async Task<IEnumerable<CipherOrganizationDetails>> GetUnassignedOrganizationCiphers(Guid organizationId)\n    {\n        return await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId);\n    }\n\n    /// <inheritdoc />\n    public async Task<IEnumerable<CipherOrganizationDetailsWithCollections>> GetOrganizationCiphersByCollectionIds(\n        Guid organizationId, IEnumerable<Guid> collectionIds)\n    {\n        var managedCollectionIds = collectionIds.ToHashSet();\n        var allOrganizationCiphers = await GetAllOrganizationCiphers(organizationId);\n        return allOrganizationCiphers.Where(c => c.CollectionIds.Intersect(managedCollectionIds).Any());\n    }\n\n    public async Task<IEnumerable<CipherOrganizationDetailsWithCollections>>\n        GetAllOrganizationCiphersExcludingDefaultUserCollections(Guid orgId)\n    {\n        return (await _cipherRepository.GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(orgId)).ToList();\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Repositories/ICipherRepository.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Queries;\n\n\nnamespace Bit.Core.Vault.Repositories;\n\npublic interface ICipherRepository : IRepository<Cipher, Guid>\n{\n    Task<CipherDetails> GetByIdAsync(Guid id, Guid userId);\n    Task<CipherOrganizationDetails> GetOrganizationDetailsByIdAsync(Guid id);\n    Task<ICollection<CipherOrganizationDetails>> GetManyOrganizationDetailsByOrganizationIdAsync(Guid organizationId);\n    Task<bool> GetCanEditByIdAsync(Guid userId, Guid cipherId);\n    Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true);\n    Task<ICollection<Cipher>> GetManyByOrganizationIdAsync(Guid organizationId);\n    Task<ICollection<CipherOrganizationDetails>> GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(Guid organizationId);\n    Task CreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);\n    Task CreateAsync(CipherDetails cipher);\n    Task CreateAsync(CipherDetails cipher, IEnumerable<Guid> collectionIds);\n    Task ReplaceAsync(CipherDetails cipher);\n    Task UpsertAsync(CipherDetails cipher);\n    Task<bool> ReplaceAsync(Cipher obj, IEnumerable<Guid> collectionIds);\n    Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite);\n    Task UpdateAttachmentAsync(CipherAttachment attachment);\n    Task<DateTime> ArchiveAsync(IEnumerable<Guid> ids, Guid userId);\n    Task DeleteAttachmentAsync(Guid cipherId, string attachmentId);\n    Task DeleteAsync(IEnumerable<Guid> ids, Guid userId);\n    Task DeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);\n    Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId);\n    Task DeleteByUserIdAsync(Guid userId);\n    Task DeleteByOrganizationIdAsync(Guid organizationId);\n    Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);\n    /// <summary>\n    /// Create ciphers and folders for the specified UserId. Must not be used to create organization owned items.\n    /// </summary>\n    Task CreateAsync(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);\n    Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,\n        IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);\n    Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);\n    Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);\n    Task<DateTime> UnarchiveAsync(IEnumerable<Guid> ids, Guid userId);\n    Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId);\n    Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);\n    Task DeleteDeletedAsync(DateTime deletedDateBefore);\n\n    /// <summary>\n    /// Low-level query to get all cipher permissions for a user in an organization. DOES NOT consider the user's\n    /// organization role, any collection management settings on the organization, or special unassigned cipher\n    /// permissions.\n    ///\n    /// Recommended to use <see cref=\"IGetCipherPermissionsForUserQuery\"/> instead to handle those cases.\n    /// </summary>\n    Task<ICollection<OrganizationCipherPermission>> GetCipherPermissionsForOrganizationAsync(Guid organizationId,\n        Guid userId);\n\n    /// <summary>\n    /// Returns the users and the cipher ids for security tasks that are applicable to them.\n    ///\n    /// Security tasks are actionable when a user has manage access to the associated cipher.\n    /// </summary>\n    Task<ICollection<UserSecurityTaskCipher>> GetUserSecurityTasksByCipherIdsAsync(Guid organizationId, IEnumerable<SecurityTask> tasks);\n\n    /// <summary>\n    /// Updates encrypted data for ciphers during a key rotation\n    /// </summary>\n    /// <param name=\"userId\">The user that initiated the key rotation</param>\n    /// <param name=\"ciphers\">A list of ciphers with updated data</param>\n    UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,\n        IEnumerable<Cipher> ciphers);\n\n    /// <summary>\n    /// Returns all ciphers belonging to the organization excluding those with default collections\n    /// </summary>\n    Task<IEnumerable<CipherOrganizationDetailsWithCollections>>\n    GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(Guid organizationId);\n}\n"
  },
  {
    "path": "src/Core/Vault/Repositories/IFolderRepository.cs",
    "content": "﻿using Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\nusing Bit.Core.Vault.Entities;\n\nnamespace Bit.Core.Vault.Repositories;\n\npublic interface IFolderRepository : IRepository<Folder, Guid>\n{\n    Task<Folder> GetByIdAsync(Guid id, Guid userId);\n    Task<ICollection<Folder>> GetManyByUserIdAsync(Guid userId);\n\n    /// <summary>\n    /// Updates encrypted data for folders during a key rotation\n    /// </summary>\n    /// <param name=\"userId\">The user that initiated the key rotation</param>\n    /// <param name=\"folders\">A list of folders with updated data</param>\n    UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,\n        IEnumerable<Folder> folders);\n}\n"
  },
  {
    "path": "src/Core/Vault/Repositories/ISecurityTaskRepository.cs",
    "content": "﻿using Bit.Core.Repositories;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\n\nnamespace Bit.Core.Vault.Repositories;\n\npublic interface ISecurityTaskRepository : IRepository<SecurityTask, Guid>\n{\n    /// <summary>\n    /// Retrieves security tasks for a user based on their organization and cipher access permissions.\n    /// </summary>\n    /// <param name=\"userId\">The Id of the user retrieving tasks</param>\n    /// <param name=\"status\">Optional filter for task status. If not provided, returns tasks of all statuses</param>\n    /// <returns></returns>\n    Task<ICollection<SecurityTask>> GetManyByUserIdStatusAsync(Guid userId, SecurityTaskStatus? status = null);\n\n    /// <summary>\n    /// Retrieves all security tasks for an organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The id of the organization</param>\n    /// <param name=\"status\">Optional filter for task status. If not provided, returns tasks of all statuses</param>\n    /// <returns></returns>\n    Task<ICollection<SecurityTask>> GetManyByOrganizationIdStatusAsync(Guid organizationId, SecurityTaskStatus? status = null);\n\n    /// <summary>\n    ///  Creates bulk security tasks for an organization.\n    /// </summary>\n    /// <param name=\"tasks\">Collection of tasks to create</param>\n    /// <returns>Collection of created security tasks</returns>\n    Task<ICollection<SecurityTask>> CreateManyAsync(IEnumerable<SecurityTask> tasks);\n\n    /// <summary>\n    /// Retrieves security task metrics for an organization.\n    /// </summary>\n    /// <param name=\"organizationId\">The id of the organization</param>\n    /// <returns>A collection of security task metrics</returns>\n    Task<SecurityTaskMetrics> GetTaskMetricsAsync(Guid organizationId);\n\n    /// <summary>\n    /// Marks all tasks associated with the respective ciphers as complete.\n    /// </summary>\n    /// <param name=\"cipherIds\">Collection of cipher IDs</param>\n    Task MarkAsCompleteByCipherIds(IEnumerable<Guid> cipherIds);\n}\n"
  },
  {
    "path": "src/Core/Vault/Services/ICipherService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nnamespace Bit.Core.Vault.Services;\n\npublic interface ICipherService\n{\n    Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate, IEnumerable<Guid> collectionIds = null,\n        bool skipPermissionCheck = false, bool limitCollectionScope = true);\n    Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,\n        IEnumerable<Guid> collectionIds = null, bool skipPermissionCheck = false);\n    Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher,\n        string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId, DateTime? lastKnownRevisionDate = null);\n    Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,\n        long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null);\n    Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength,\n        string attachmentId, Guid organizationShareId);\n    Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);\n    Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);\n    Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);\n    Task PurgeAsync(Guid organizationId);\n    Task MoveManyAsync(IEnumerable<Guid> cipherIds, Guid? destinationFolderId, Guid movingUserId);\n    Task SaveFolderAsync(Folder folder);\n    Task DeleteFolderAsync(Folder folder);\n    Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable<Guid> collectionIds,\n        Guid userId, DateTime? lastKnownRevisionDate);\n    Task<IEnumerable<CipherDetails>> ShareManyAsync(IEnumerable<(CipherDetails cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId,\n        IEnumerable<Guid> collectionIds, Guid sharingUserId);\n    Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId, bool orgAdmin);\n    Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);\n    Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);\n    Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false);\n    Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false);\n    Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId, Guid savingUserId, bool orgAdmin = false);\n    Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);\n    Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);\n    Task ValidateBulkCollectionAssignmentAsync(IEnumerable<Guid> collectionIds, IEnumerable<Guid> cipherIds, Guid userId);\n    Task ValidateCipherEditForAttachmentAsync(Cipher cipher, Guid savingUserId, bool orgAdmin, long requestLength);\n}\n"
  },
  {
    "path": "src/Core/Vault/Services/Implementations/AzureAttachmentStorageService.cs",
    "content": "﻿using Azure.Storage.Blobs;\nusing Azure.Storage.Blobs.Models;\nusing Azure.Storage.Sas;\nusing Bit.Core.Enums;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Core.Vault.Services;\n\npublic class AzureAttachmentStorageService : IAttachmentStorageService\n{\n    public FileUploadType FileUploadType => FileUploadType.Azure;\n    public const string EventGridEnabledContainerName = \"attachments-v2\";\n    private const string _defaultContainerName = \"attachments\";\n    private readonly static string[] _attachmentContainerName = { \"attachments\", \"attachments-v2\" };\n    private static readonly TimeSpan blobLinkLiveTime = TimeSpan.FromMinutes(1);\n    private readonly BlobServiceClient _blobServiceClient;\n    private readonly Dictionary<string, BlobContainerClient> _attachmentContainers = new Dictionary<string, BlobContainerClient>();\n    private readonly ILogger<AzureAttachmentStorageService> _logger;\n\n    private string BlobName(Guid cipherId, CipherAttachment.MetaData attachmentData, Guid? organizationId = null, bool temp = false) =>\n        string.Concat(\n            temp ? \"temp/\" : \"\",\n            $\"{cipherId}/\",\n            organizationId != null ? $\"{organizationId.Value}/\" : \"\",\n            attachmentData.AttachmentId\n        );\n\n    public static (string cipherId, string? organizationId, string attachmentId) IdentifiersFromBlobName(string blobName)\n    {\n        var parts = blobName.Split('/');\n        switch (parts.Length)\n        {\n            case 4:\n                return (parts[1], parts[2], parts[3]);\n            case 3:\n                if (parts[0] == \"temp\")\n                {\n                    return (parts[1], null, parts[2]);\n                }\n                else\n                {\n                    return (parts[0], parts[1], parts[2]);\n                }\n            case 2:\n                return (parts[0], null, parts[1]);\n            default:\n                throw new Exception(\"Cannot determine cipher information from blob name\");\n        }\n    }\n\n    public AzureAttachmentStorageService(\n        GlobalSettings globalSettings,\n        ILogger<AzureAttachmentStorageService> logger)\n    {\n        _blobServiceClient = new BlobServiceClient(globalSettings.Attachment.ConnectionString);\n        _logger = logger;\n    }\n\n    public async Task<string> GetAttachmentDownloadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        await InitAsync(attachmentData.ContainerName);\n        var blobClient = _attachmentContainers[attachmentData.ContainerName].GetBlobClient(BlobName(cipher.Id, attachmentData));\n        var sasUri = blobClient.GenerateSasUri(BlobSasPermissions.Read, DateTime.UtcNow.Add(blobLinkLiveTime));\n        return sasUri.ToString();\n    }\n\n    public (Guid cipherId, string attachmentId) ParseAttachmentDownloadToken(string token)\n    {\n        throw new NotSupportedException(\"Token-based downloads are not supported with Azure storage.\");\n    }\n\n    public async Task<string> GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        await InitAsync(EventGridEnabledContainerName);\n        var blobClient = _attachmentContainers[EventGridEnabledContainerName].GetBlobClient(BlobName(cipher.Id, attachmentData));\n        attachmentData.ContainerName = EventGridEnabledContainerName;\n        var sasUri = blobClient.GenerateSasUri(BlobSasPermissions.Create | BlobSasPermissions.Write, DateTime.UtcNow.Add(blobLinkLiveTime));\n        return sasUri.ToString();\n    }\n\n    public async Task UploadNewAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        attachmentData.ContainerName = _defaultContainerName;\n        await InitAsync(_defaultContainerName);\n        var blobClient = _attachmentContainers[_defaultContainerName].GetBlobClient(BlobName(cipher.Id, attachmentData));\n\n        var metadata = new Dictionary<string, string>();\n        metadata.Add(\"cipherId\", cipher.Id.ToString());\n        if (cipher.UserId.HasValue)\n        {\n            metadata.Add(\"userId\", cipher.UserId.Value.ToString());\n        }\n        else\n        {\n            metadata.Add(\"organizationId\", cipher.OrganizationId!.Value.ToString());\n        }\n\n        var headers = new BlobHttpHeaders\n        {\n            ContentDisposition = $\"attachment; filename=\\\"{attachmentData.AttachmentId}\\\"\"\n        };\n        await blobClient.UploadAsync(stream, new BlobUploadOptions { Metadata = metadata, HttpHeaders = headers });\n    }\n\n    public async Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData)\n    {\n        attachmentData.ContainerName = _defaultContainerName;\n        await InitAsync(_defaultContainerName);\n        var blobClient = _attachmentContainers[_defaultContainerName].GetBlobClient(\n            BlobName(cipherId, attachmentData, organizationId, temp: true));\n\n        var metadata = new Dictionary<string, string>();\n        metadata.Add(\"cipherId\", cipherId.ToString());\n        metadata.Add(\"organizationId\", organizationId.ToString());\n\n        var headers = new BlobHttpHeaders\n        {\n            ContentDisposition = $\"attachment; filename=\\\"{attachmentData.AttachmentId}\\\"\"\n        };\n        await blobClient.UploadAsync(stream, new BlobUploadOptions { Metadata = metadata, HttpHeaders = headers });\n    }\n\n    public async Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData data)\n    {\n        await InitAsync(data.ContainerName);\n        var source = _attachmentContainers[data.ContainerName].GetBlobClient(\n                BlobName(cipherId, data, organizationId, temp: true));\n        if (!await source.ExistsAsync())\n        {\n            return;\n        }\n\n        await InitAsync(_defaultContainerName);\n        var dest = _attachmentContainers[_defaultContainerName].GetBlobClient(BlobName(cipherId, data));\n        if (!await dest.ExistsAsync())\n        {\n            return;\n        }\n\n        var original = _attachmentContainers[_defaultContainerName].GetBlobClient(\n            BlobName(cipherId, data, temp: true));\n        await original.DeleteIfExistsAsync();\n        await original.StartCopyFromUriAsync(dest.Uri);\n\n        await dest.DeleteIfExistsAsync();\n        await dest.StartCopyFromUriAsync(source.Uri);\n    }\n\n    public async Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData, string originalContainer)\n    {\n        await InitAsync(attachmentData.ContainerName);\n        var source = _attachmentContainers[attachmentData.ContainerName].GetBlobClient(\n            BlobName(cipherId, attachmentData, organizationId, temp: true));\n        await source.DeleteIfExistsAsync();\n\n        await InitAsync(originalContainer);\n        var original = _attachmentContainers[originalContainer].GetBlobClient(\n            BlobName(cipherId, attachmentData, temp: true));\n        if (!await original.ExistsAsync())\n        {\n            return;\n        }\n\n        var dest = _attachmentContainers[originalContainer].GetBlobClient(\n            BlobName(cipherId, attachmentData));\n        await dest.DeleteIfExistsAsync();\n        await dest.StartCopyFromUriAsync(original.Uri);\n        await original.DeleteIfExistsAsync();\n    }\n\n    public async Task DeleteAttachmentAsync(Guid cipherId, CipherAttachment.MetaData attachmentData)\n    {\n        await InitAsync(attachmentData.ContainerName);\n        var blobClient = _attachmentContainers[attachmentData.ContainerName].GetBlobClient(\n            BlobName(cipherId, attachmentData));\n        await blobClient.DeleteIfExistsAsync();\n    }\n\n    public async Task CleanupAsync(Guid cipherId) => await DeleteAttachmentsForPathAsync($\"temp/{cipherId}\");\n\n    public async Task DeleteAttachmentsForCipherAsync(Guid cipherId) =>\n        await DeleteAttachmentsForPathAsync(cipherId.ToString());\n\n    public async Task DeleteAttachmentsForOrganizationAsync(Guid organizationId)\n    {\n        await InitAsync(_defaultContainerName);\n    }\n\n    public async Task DeleteAttachmentsForUserAsync(Guid userId)\n    {\n        await InitAsync(_defaultContainerName);\n    }\n\n    public Task<Stream?> GetAttachmentReadStreamAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        // Azure storage uses SAS URLs for downloads; direct streaming is not supported.\n        return Task.FromResult<Stream?>(null);\n    }\n\n    public async Task<(bool, long?)> ValidateFileAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, long leeway)\n    {\n        await InitAsync(attachmentData.ContainerName);\n\n        var blobClient = _attachmentContainers[attachmentData.ContainerName].GetBlobClient(BlobName(cipher.Id, attachmentData));\n\n        try\n        {\n            var blobProperties = await blobClient.GetPropertiesAsync();\n\n            var metadata = blobProperties.Value.Metadata;\n            metadata[\"cipherId\"] = cipher.Id.ToString();\n            if (cipher.UserId.HasValue)\n            {\n                metadata[\"userId\"] = cipher.UserId.Value.ToString();\n            }\n            else\n            {\n                metadata[\"organizationId\"] = cipher.OrganizationId!.Value.ToString();\n            }\n            await blobClient.SetMetadataAsync(metadata);\n\n            var headers = new BlobHttpHeaders\n            {\n                ContentDisposition = $\"attachment; filename=\\\"{attachmentData.AttachmentId}\\\"\"\n            };\n            await blobClient.SetHttpHeadersAsync(headers);\n\n            var length = blobProperties.Value.ContentLength;\n            if (length < attachmentData.Size - leeway || length > attachmentData.Size + leeway)\n            {\n                return (false, length);\n            }\n\n            return (true, length);\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Unhandled error in ValidateFileAsync\");\n            return (false, null);\n        }\n    }\n\n    private async Task DeleteAttachmentsForPathAsync(string path)\n    {\n        foreach (var container in _attachmentContainerName)\n        {\n            await InitAsync(container);\n            var blobContainerClient = _attachmentContainers[container];\n\n            var blobItems = blobContainerClient.GetBlobsAsync(BlobTraits.None, BlobStates.None, prefix: path);\n            await foreach (var blobItem in blobItems)\n            {\n                BlobClient blobClient = blobContainerClient.GetBlobClient(blobItem.Name);\n                await blobClient.DeleteIfExistsAsync();\n            }\n        }\n    }\n\n    private async Task InitAsync(string containerName)\n    {\n        if (!_attachmentContainers.TryGetValue(containerName, out var attachmentContainer) || attachmentContainer == null)\n        {\n            attachmentContainer = _blobServiceClient.GetBlobContainerClient(containerName);\n            _attachmentContainers[containerName] = attachmentContainer;\n            if (containerName == \"attachments\")\n            {\n                await attachmentContainer.CreateIfNotExistsAsync(PublicAccessType.Blob, null, null);\n            }\n            else\n            {\n                await attachmentContainer.CreateIfNotExistsAsync(PublicAccessType.None, null, null);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Services/Implementations/CipherService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Authorization.Permissions;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Queries;\nusing Bit.Core.Vault.Repositories;\nnamespace Bit.Core.Vault.Services;\n\npublic class CipherService : ICipherService\n{\n    public const long MAX_FILE_SIZE = Constants.FileSize501mb;\n    public const string MAX_FILE_SIZE_READABLE = \"500 MB\";\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IFolderRepository _folderRepository;\n    private readonly ICollectionRepository _collectionRepository;\n    private readonly IUserRepository _userRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ICollectionCipherRepository _collectionCipherRepository;\n    private readonly ISecurityTaskRepository _securityTaskRepository;\n    private readonly IPushNotificationService _pushService;\n    private readonly IAttachmentStorageService _attachmentStorageService;\n    private readonly IEventService _eventService;\n    private readonly IUserService _userService;\n    private readonly IPolicyService _policyService;\n    private readonly GlobalSettings _globalSettings;\n    private const long _fileSizeLeeway = 1024L * 1024L; // 1MB\n    private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;\n    private readonly IPolicyRequirementQuery _policyRequirementQuery;\n    private readonly IApplicationCacheService _applicationCacheService;\n    private readonly IFeatureService _featureService;\n    private readonly IPricingClient _pricingClient;\n\n    public CipherService(\n        ICipherRepository cipherRepository,\n        IFolderRepository folderRepository,\n        ICollectionRepository collectionRepository,\n        IUserRepository userRepository,\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        ICollectionCipherRepository collectionCipherRepository,\n        ISecurityTaskRepository securityTaskRepository,\n        IPushNotificationService pushService,\n        IAttachmentStorageService attachmentStorageService,\n        IEventService eventService,\n        IUserService userService,\n        IPolicyService policyService,\n        GlobalSettings globalSettings,\n        IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IApplicationCacheService applicationCacheService,\n        IFeatureService featureService,\n        IPricingClient pricingClient)\n    {\n        _cipherRepository = cipherRepository;\n        _folderRepository = folderRepository;\n        _collectionRepository = collectionRepository;\n        _userRepository = userRepository;\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _collectionCipherRepository = collectionCipherRepository;\n        _securityTaskRepository = securityTaskRepository;\n        _pushService = pushService;\n        _attachmentStorageService = attachmentStorageService;\n        _eventService = eventService;\n        _userService = userService;\n        _policyService = policyService;\n        _globalSettings = globalSettings;\n        _getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;\n        _policyRequirementQuery = policyRequirementQuery;\n        _applicationCacheService = applicationCacheService;\n        _featureService = featureService;\n        _pricingClient = pricingClient;\n    }\n\n    public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,\n         IEnumerable<Guid> collectionIds = null, bool skipPermissionCheck = false, bool limitCollectionScope = true)\n    {\n        if (!skipPermissionCheck && !(await UserCanEditAsync(cipher, savingUserId)))\n        {\n            throw new BadRequestException(\"You do not have permissions to edit this.\");\n        }\n\n        if (cipher.Id == default(Guid))\n        {\n            if (cipher.OrganizationId.HasValue && collectionIds != null)\n            {\n                if (limitCollectionScope)\n                {\n                    // Set user ID to limit scope of collection ids in the create sproc\n                    cipher.UserId = savingUserId;\n                }\n                await _cipherRepository.CreateAsync(cipher, collectionIds);\n            }\n            else\n            {\n                await _cipherRepository.CreateAsync(cipher);\n            }\n            await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Created);\n\n            // push\n            await _pushService.PushSyncCipherCreateAsync(cipher, null);\n        }\n        else\n        {\n            ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);\n            cipher.RevisionDate = DateTime.UtcNow;\n            await _cipherRepository.ReplaceAsync(cipher);\n            await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);\n\n            // push\n            await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);\n        }\n    }\n\n    public async Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,\n        IEnumerable<Guid> collectionIds = null, bool skipPermissionCheck = false)\n    {\n        if (!skipPermissionCheck && !(await UserCanEditAsync(cipher, savingUserId)))\n        {\n            throw new BadRequestException(\"You do not have permissions to edit this.\");\n        }\n\n        cipher.UserId = savingUserId;\n        if (cipher.Id == default(Guid))\n        {\n            if (cipher.OrganizationId.HasValue && collectionIds != null)\n            {\n                var existingCollectionIds = (await _collectionRepository.GetManyByOrganizationIdAsync(cipher.OrganizationId.Value)).Select(c => c.Id);\n                if (collectionIds.Except(existingCollectionIds).Any())\n                {\n                    throw new BadRequestException(\"Specified CollectionId does not exist on the specified Organization.\");\n                }\n                await _cipherRepository.CreateAsync(cipher, collectionIds);\n            }\n            else\n            {\n                var organizationDataOwnershipEnabled = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)\n                    ? (await _policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(savingUserId)).State == OrganizationDataOwnershipState.Enabled\n                    : await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.OrganizationDataOwnership);\n\n                if (organizationDataOwnershipEnabled)\n                {\n                    throw new BadRequestException(\"Due to an Enterprise Policy, you are restricted from saving items to your personal vault.\");\n                }\n                await _cipherRepository.CreateAsync(cipher);\n            }\n            await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Created);\n\n            if (cipher.OrganizationId.HasValue)\n            {\n                var org = await _organizationRepository.GetByIdAsync(cipher.OrganizationId.Value);\n                cipher.OrganizationUseTotp = org.UseTotp;\n            }\n\n            // push\n            await _pushService.PushSyncCipherCreateAsync(cipher, null);\n        }\n        else\n        {\n            ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);\n            cipher.RevisionDate = DateTime.UtcNow;\n            await ValidateChangeInCollectionsAsync(cipher, collectionIds, savingUserId);\n            await _cipherRepository.ReplaceAsync(cipher);\n            await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);\n\n            // push\n            await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);\n        }\n    }\n\n    public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment, Guid savingUserId, bool orgAdmin = false)\n    {\n        await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, orgAdmin, attachment.Size);\n\n        if (attachment == null)\n        {\n            throw new BadRequestException(\"Cipher attachment does not exist\");\n        }\n\n        await _attachmentStorageService.UploadNewAttachmentAsync(stream, cipher, attachment);\n\n        if (!await ValidateCipherAttachmentFile(cipher, attachment))\n        {\n            throw new BadRequestException(\"File received does not match expected file length.\");\n        }\n    }\n\n    public async Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher,\n        string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId, DateTime? lastKnownRevisionDate = null)\n    {\n        ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);\n        await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, adminRequest, fileSize);\n\n        var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);\n        var data = new CipherAttachment.MetaData\n        {\n            AttachmentId = attachmentId,\n            FileName = fileName,\n            Key = key,\n            Size = fileSize,\n            Validated = false,\n        };\n\n        var uploadUrl = await _attachmentStorageService.GetAttachmentUploadUrlAsync(cipher, data);\n\n        await _cipherRepository.UpdateAttachmentAsync(new CipherAttachment\n        {\n            Id = cipher.Id,\n            UserId = cipher.UserId,\n            OrganizationId = cipher.OrganizationId,\n            AttachmentId = attachmentId,\n            AttachmentData = JsonSerializer.Serialize(data)\n        });\n        cipher.AddAttachment(attachmentId, data);\n\n        // Update the revision date when an attachment is added\n        cipher.RevisionDate = DateTime.UtcNow;\n        await _cipherRepository.ReplaceAsync((CipherDetails)cipher);\n\n        await _pushService.PushSyncCipherUpdateAsync(cipher, null);\n\n        return (attachmentId, uploadUrl);\n    }\n\n    public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,\n        long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null)\n    {\n        ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);\n        await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, orgAdmin, requestLength);\n\n        var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);\n        var data = new CipherAttachment.MetaData\n        {\n            AttachmentId = attachmentId,\n            FileName = fileName,\n            Key = key,\n        };\n\n        await _attachmentStorageService.UploadNewAttachmentAsync(stream, cipher, data);\n        // Must read stream length after it has been saved, otherwise it's 0\n        data.Size = stream.Length;\n\n        try\n        {\n            var attachment = new CipherAttachment\n            {\n                Id = cipher.Id,\n                UserId = cipher.UserId,\n                OrganizationId = cipher.OrganizationId,\n                AttachmentId = attachmentId,\n                AttachmentData = JsonSerializer.Serialize(data)\n            };\n\n            await _cipherRepository.UpdateAttachmentAsync(attachment);\n            await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_AttachmentCreated);\n            cipher.AddAttachment(attachmentId, data);\n\n            if (!await ValidateCipherAttachmentFile(cipher, data))\n            {\n                throw new Exception(\"Content-Length does not match uploaded file size\");\n            }\n        }\n        catch\n        {\n            // Clean up since this is not transactional\n            await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, data);\n            throw;\n        }\n\n        // Update the revision date when an attachment is added\n        cipher.RevisionDate = DateTime.UtcNow;\n        await _cipherRepository.ReplaceAsync((CipherDetails)cipher);\n\n        // push\n        await _pushService.PushSyncCipherUpdateAsync(cipher, null);\n    }\n\n    public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key,\n        long requestLength, string attachmentId, Guid organizationId)\n    {\n        try\n        {\n            if (requestLength < 1)\n            {\n                throw new BadRequestException(\"No data to attach.\");\n            }\n\n            if (cipher.Id == default(Guid))\n            {\n                throw new BadRequestException(nameof(cipher.Id));\n            }\n\n            if (cipher.OrganizationId.HasValue)\n            {\n                throw new BadRequestException(\"Cipher belongs to an organization already.\");\n            }\n\n            var org = await _organizationRepository.GetByIdAsync(organizationId);\n            if (org == null || !org.MaxStorageGb.HasValue)\n            {\n                throw new BadRequestException(\"This organization cannot use attachments.\");\n            }\n\n            var storageBytesRemaining = org.StorageBytesRemaining();\n            if (storageBytesRemaining < requestLength)\n            {\n                throw new BadRequestException(\"Not enough storage available for this organization.\");\n            }\n\n            var attachments = cipher.GetAttachments();\n            if (!attachments.TryGetValue(attachmentId, out var originalAttachmentMetadata))\n            {\n                throw new BadRequestException($\"Cipher does not own specified attachment\");\n            }\n\n            if (originalAttachmentMetadata.TempMetadata != null)\n            {\n                throw new BadRequestException(\"Another process is trying to migrate this attachment\");\n            }\n\n            // Clone metadata to be modified and saved into the TempMetadata,\n            // we cannot change the metadata here directly because if the subsequent endpoint fails\n            // to be called, then the metadata would stay corrupted.\n            var attachmentMetadata = CoreHelpers.CloneObject(originalAttachmentMetadata);\n            attachmentMetadata.AttachmentId = originalAttachmentMetadata.AttachmentId;\n            originalAttachmentMetadata.TempMetadata = attachmentMetadata;\n\n            if (key != null)\n            {\n                attachmentMetadata.Key = key;\n                attachmentMetadata.FileName = fileName;\n            }\n\n            await _attachmentStorageService.UploadShareAttachmentAsync(stream, cipher.Id, organizationId,\n                attachmentMetadata);\n\n            // Previous call may alter metadata\n            var updatedAttachment = new CipherAttachment\n            {\n                Id = cipher.Id,\n                UserId = cipher.UserId,\n                OrganizationId = cipher.OrganizationId,\n                AttachmentId = attachmentId,\n                AttachmentData = JsonSerializer.Serialize(originalAttachmentMetadata)\n            };\n\n            await _cipherRepository.UpdateAttachmentAsync(updatedAttachment);\n        }\n        catch\n        {\n            await _attachmentStorageService.CleanupAsync(cipher.Id);\n            throw;\n        }\n    }\n\n    public async Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        var (valid, realSize) = await _attachmentStorageService.ValidateFileAsync(cipher, attachmentData, _fileSizeLeeway);\n\n        if (!valid || realSize > MAX_FILE_SIZE)\n        {\n            // File reported differs in size from that promised. Must be a rogue client. Delete Send\n            await DeleteAttachmentAsync(cipher, attachmentData, false);\n            return false;\n        }\n        // Update Send data if necessary\n        if (realSize != attachmentData.Size)\n        {\n            attachmentData.Size = realSize.Value;\n        }\n        attachmentData.Validated = true;\n\n        var updatedAttachment = new CipherAttachment\n        {\n            Id = cipher.Id,\n            UserId = cipher.UserId,\n            OrganizationId = cipher.OrganizationId,\n            AttachmentId = attachmentData.AttachmentId,\n            AttachmentData = JsonSerializer.Serialize(attachmentData)\n        };\n\n\n        await _cipherRepository.UpdateAttachmentAsync(updatedAttachment);\n\n        return valid;\n    }\n\n    public async Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId)\n    {\n        var attachments = cipher?.GetAttachments() ?? new Dictionary<string, CipherAttachment.MetaData>();\n\n        if (!attachments.TryGetValue(attachmentId, out var data))\n        {\n            throw new NotFoundException();\n        }\n\n        var url = await _attachmentStorageService.GetAttachmentDownloadUrlAsync(cipher, data);\n\n        var response = new AttachmentResponseData\n        {\n            Cipher = cipher,\n            Data = data,\n            Id = attachmentId,\n            Url = url,\n        };\n\n        return response;\n    }\n\n    public async Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false)\n    {\n        if (!orgAdmin && !await UserCanDeleteAsync(cipherDetails, deletingUserId))\n        {\n            throw new BadRequestException(\"You do not have permissions to delete this.\");\n        }\n\n        await _cipherRepository.DeleteAsync(cipherDetails);\n        await _attachmentStorageService.DeleteAttachmentsForCipherAsync(cipherDetails.Id);\n        await _eventService.LogCipherEventAsync(cipherDetails, EventType.Cipher_Deleted);\n\n        // push\n        await _pushService.PushSyncCipherDeleteAsync(cipherDetails);\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false)\n    {\n        var cipherIdsSet = new HashSet<Guid>(cipherIds);\n        var deletingCiphers = new List<Cipher>();\n\n        if (orgAdmin && organizationId.HasValue)\n        {\n            var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(organizationId.Value);\n            deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id)).ToList();\n            await _cipherRepository.DeleteByIdsOrganizationIdAsync(deletingCiphers.Select(c => c.Id), organizationId.Value);\n        }\n        else\n        {\n            var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId);\n            var filteredCiphers = await FilterCiphersByDeletePermission(ciphers, cipherIdsSet, deletingUserId);\n            deletingCiphers = filteredCiphers.Select(c => (Cipher)c).ToList();\n            await _cipherRepository.DeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId);\n        }\n\n        var events = deletingCiphers.Select(c =>\n            new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Deleted, null));\n        foreach (var eventsBatch in events.Chunk(100))\n        {\n            await _eventService.LogCipherEventsAsync(eventsBatch);\n        }\n\n        // push\n        await _pushService.PushSyncCiphersAsync(deletingUserId);\n    }\n\n    public async Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId,\n        bool orgAdmin = false)\n    {\n        if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))\n        {\n            throw new BadRequestException(\"You do not have permissions to delete this.\");\n        }\n\n        if (!cipher.ContainsAttachment(attachmentId))\n        {\n            throw new NotFoundException();\n        }\n\n        return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId], orgAdmin);\n    }\n\n    public async Task PurgeAsync(Guid organizationId)\n    {\n        var org = await _organizationRepository.GetByIdAsync(organizationId);\n        if (org == null)\n        {\n            throw new NotFoundException();\n        }\n        await _cipherRepository.DeleteByOrganizationIdAsync(organizationId);\n        await _eventService.LogOrganizationEventAsync(org, EventType.Organization_PurgedVault);\n    }\n\n    public async Task MoveManyAsync(IEnumerable<Guid> cipherIds, Guid? destinationFolderId, Guid movingUserId)\n    {\n        if (destinationFolderId.HasValue)\n        {\n            var folder = await _folderRepository.GetByIdAsync(destinationFolderId.Value);\n            if (folder == null || folder.UserId != movingUserId)\n            {\n                throw new BadRequestException(\"Invalid folder.\");\n            }\n        }\n\n        await _cipherRepository.MoveAsync(cipherIds, destinationFolderId, movingUserId);\n        // push\n        await _pushService.PushSyncCiphersAsync(movingUserId);\n    }\n\n    public async Task SaveFolderAsync(Folder folder)\n    {\n        if (folder.Id == default(Guid))\n        {\n            await _folderRepository.CreateAsync(folder);\n\n            // push\n            await _pushService.PushSyncFolderCreateAsync(folder);\n        }\n        else\n        {\n            folder.RevisionDate = DateTime.UtcNow;\n            await _folderRepository.UpsertAsync(folder);\n\n            // push\n            await _pushService.PushSyncFolderUpdateAsync(folder);\n        }\n    }\n\n    public async Task DeleteFolderAsync(Folder folder)\n    {\n        await _folderRepository.DeleteAsync(folder);\n\n        // push\n        await _pushService.PushSyncFolderDeleteAsync(folder);\n    }\n\n    public async Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId,\n        IEnumerable<Guid> collectionIds, Guid sharingUserId, DateTime? lastKnownRevisionDate)\n    {\n        var attachments = cipher.GetAttachments();\n        var hasOldAttachments = attachments?.Values?.Any(a => a.Key == null) ?? false;\n        var updatedCipher = false;\n        var migratedAttachments = false;\n        var originalAttachments = CoreHelpers.CloneObject(originalCipher.GetAttachments());\n\n        try\n        {\n            await ValidateCipherCanBeShared(cipher, sharingUserId, organizationId, lastKnownRevisionDate);\n            await ValidateChangeInCollectionsAsync(cipher, collectionIds, sharingUserId);\n\n            // Sproc will not save this UserId on the cipher. It is used limit scope of the collectionIds.\n            cipher.UserId = sharingUserId;\n            cipher.OrganizationId = organizationId;\n            cipher.RevisionDate = DateTime.UtcNow;\n\n            if (hasOldAttachments)\n            {\n                var attachmentsWithUpdatedMetadata = originalCipher.GetAttachments();\n                var attachmentsToUpdateMetadata = CoreHelpers.CloneObject(attachments);\n                foreach (var updatedMetadata in attachmentsWithUpdatedMetadata.Where(a => a.Value?.TempMetadata != null))\n                {\n                    if (attachmentsToUpdateMetadata.ContainsKey(updatedMetadata.Key))\n                    {\n                        attachmentsToUpdateMetadata[updatedMetadata.Key] = updatedMetadata.Value.TempMetadata;\n                    }\n                }\n                cipher.SetAttachments(attachmentsToUpdateMetadata);\n            }\n\n            if (!await _cipherRepository.ReplaceAsync(cipher, collectionIds))\n            {\n                throw new BadRequestException(\"Unable to save.\");\n            }\n\n            updatedCipher = true;\n            await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Shared);\n\n            if (hasOldAttachments)\n            {\n                // migrate old attachments\n                foreach (var attachment in attachments.Values.Where(a => a.TempMetadata != null).Select(a => a.TempMetadata))\n                {\n                    await _attachmentStorageService.StartShareAttachmentAsync(cipher.Id, organizationId,\n                        attachment);\n                    migratedAttachments = true;\n                }\n\n                // commit attachment migration\n                await _attachmentStorageService.CleanupAsync(cipher.Id);\n            }\n        }\n        catch\n        {\n            // roll everything back\n            if (updatedCipher)\n            {\n                if (hasOldAttachments)\n                {\n                    foreach (var item in originalAttachments)\n                    {\n                        item.Value.TempMetadata = null;\n                    }\n                    originalCipher.SetAttachments(originalAttachments);\n                }\n\n                var currentCollectionsForCipher = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(sharingUserId, originalCipher.Id);\n                var currentCollectionIdsForCipher = currentCollectionsForCipher.Select(c => c.CollectionId).ToList();\n                currentCollectionIdsForCipher.RemoveAll(id => collectionIds.Contains(id));\n\n                await _collectionCipherRepository.UpdateCollectionsAsync(originalCipher.Id, sharingUserId, currentCollectionIdsForCipher);\n                await _cipherRepository.ReplaceAsync(originalCipher);\n            }\n\n            if (!hasOldAttachments || !migratedAttachments)\n            {\n                throw;\n            }\n\n            if (updatedCipher)\n            {\n                await _userRepository.UpdateStorageAsync(sharingUserId);\n                await _organizationRepository.UpdateStorageAsync(organizationId);\n            }\n\n            foreach (var attachment in attachments.Where(a => a.Value.Key == null))\n            {\n                await _attachmentStorageService.RollbackShareAttachmentAsync(cipher.Id, organizationId,\n                    attachment.Value, originalAttachments[attachment.Key].ContainerName);\n            }\n\n            await _attachmentStorageService.CleanupAsync(cipher.Id);\n            throw;\n        }\n\n        // push\n        await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);\n    }\n\n    public async Task<IEnumerable<CipherDetails>> ShareManyAsync(IEnumerable<(CipherDetails cipher, DateTime? lastKnownRevisionDate)> cipherInfos,\n        Guid organizationId, IEnumerable<Guid> collectionIds, Guid sharingUserId)\n    {\n        var cipherIds = new List<Guid>();\n        foreach (var (cipher, lastKnownRevisionDate) in cipherInfos)\n        {\n            await ValidateCipherCanBeShared(cipher, sharingUserId, organizationId, lastKnownRevisionDate);\n\n            cipher.UserId = null;\n            cipher.OrganizationId = organizationId;\n            cipher.RevisionDate = DateTime.UtcNow;\n            cipherIds.Add(cipher.Id);\n        }\n\n        await _cipherRepository.UpdateCiphersAsync(sharingUserId, cipherInfos.Select(c => c.cipher));\n        await _collectionCipherRepository.UpdateCollectionsForCiphersAsync(cipherIds, sharingUserId,\n            organizationId, collectionIds);\n\n        var events = cipherInfos.Select(c =>\n            new Tuple<Cipher, EventType, DateTime?>(c.cipher, EventType.Cipher_Shared, null));\n        foreach (var eventsBatch in events.Chunk(100))\n        {\n            await _eventService.LogCipherEventsAsync(eventsBatch);\n        }\n\n        // push\n        await _pushService.PushSyncCiphersAsync(sharingUserId);\n        return cipherInfos.Select(c => c.cipher);\n    }\n\n    public async Task SaveCollectionsAsync(Cipher cipher, IEnumerable<Guid> collectionIds, Guid savingUserId,\n        bool orgAdmin)\n    {\n        if (cipher.Id == default(Guid))\n        {\n            throw new BadRequestException(nameof(cipher.Id));\n        }\n\n        if (!cipher.OrganizationId.HasValue)\n        {\n            throw new BadRequestException(\"Cipher must belong to an organization.\");\n        }\n        await ValidateChangeInCollectionsAsync(cipher, collectionIds, savingUserId);\n\n        cipher.RevisionDate = DateTime.UtcNow;\n\n        // The sprocs will validate that all collections belong to this org/user and that they have\n        // proper write permissions.\n        if (orgAdmin)\n        {\n            await _collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id,\n                cipher.OrganizationId.Value, collectionIds);\n        }\n        else\n        {\n            if (!(await UserCanEditAsync(cipher, savingUserId)))\n            {\n                throw new BadRequestException(\"You do not have permissions to edit this.\");\n            }\n            await _collectionCipherRepository.UpdateCollectionsAsync(cipher.Id, savingUserId, collectionIds);\n        }\n\n        await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_UpdatedCollections);\n\n        // push\n        await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds);\n    }\n\n    public async Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false)\n    {\n        if (!orgAdmin && !await UserCanDeleteAsync(cipherDetails, deletingUserId))\n        {\n            throw new BadRequestException(\"You do not have permissions to soft delete this.\");\n        }\n\n        if (cipherDetails.DeletedDate.HasValue)\n        {\n            // Already soft-deleted, we can safely ignore this\n            return;\n        }\n\n        cipherDetails.DeletedDate = cipherDetails.RevisionDate = DateTime.UtcNow;\n\n        await _securityTaskRepository.MarkAsCompleteByCipherIds([cipherDetails.Id]);\n        await _cipherRepository.UpsertAsync(cipherDetails);\n        await _eventService.LogCipherEventAsync(cipherDetails, EventType.Cipher_SoftDeleted);\n\n        // push\n        await _pushService.PushSyncCipherUpdateAsync(cipherDetails, null);\n    }\n\n    public async Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId, bool orgAdmin)\n    {\n        var cipherIdsSet = new HashSet<Guid>(cipherIds);\n        var deletingCiphers = new List<Cipher>();\n\n        if (orgAdmin && organizationId.HasValue)\n        {\n            var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(organizationId.Value);\n            deletingCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id)).ToList();\n            await _cipherRepository.SoftDeleteByIdsOrganizationIdAsync(deletingCiphers.Select(c => c.Id), organizationId.Value);\n        }\n        else\n        {\n            var ciphers = await _cipherRepository.GetManyByUserIdAsync(deletingUserId);\n            var filteredCiphers = await FilterCiphersByDeletePermission(ciphers, cipherIdsSet, deletingUserId);\n            deletingCiphers = filteredCiphers.Select(c => (Cipher)c).ToList();\n            await _cipherRepository.SoftDeleteAsync(deletingCiphers.Select(c => c.Id), deletingUserId);\n        }\n\n        await _securityTaskRepository.MarkAsCompleteByCipherIds(deletingCiphers.Select(c => c.Id));\n\n        var events = deletingCiphers.Select(c =>\n            new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_SoftDeleted, null));\n        foreach (var eventsBatch in events.Chunk(100))\n        {\n            await _eventService.LogCipherEventsAsync(eventsBatch);\n        }\n\n        // push\n        await _pushService.PushSyncCiphersAsync(deletingUserId);\n    }\n\n    public async Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false)\n    {\n        if (!orgAdmin && !await UserCanRestoreAsync(cipherDetails, restoringUserId))\n        {\n            throw new BadRequestException(\"You do not have permissions to delete this.\");\n        }\n\n        if (!cipherDetails.DeletedDate.HasValue)\n        {\n            // Already restored, we can safely ignore this\n            return;\n        }\n\n        cipherDetails.DeletedDate = null;\n        cipherDetails.RevisionDate = DateTime.UtcNow;\n\n        await _cipherRepository.UpsertAsync(cipherDetails);\n        await _eventService.LogCipherEventAsync(cipherDetails, EventType.Cipher_Restored);\n\n        // push\n        await _pushService.PushSyncCipherUpdateAsync(cipherDetails, null);\n    }\n\n    public async Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false)\n    {\n        if (cipherIds == null || !cipherIds.Any())\n        {\n            return new List<CipherOrganizationDetails>();\n        }\n\n        var cipherIdsSet = new HashSet<Guid>(cipherIds);\n        List<CipherOrganizationDetails> restoringCiphers;\n        DateTime? revisionDate; // TODO: Make this not nullable\n\n        if (orgAdmin && organizationId.HasValue)\n        {\n            var ciphers = await _cipherRepository.GetManyOrganizationDetailsByOrganizationIdAsync(organizationId.Value);\n            restoringCiphers = ciphers.Where(c => cipherIdsSet.Contains(c.Id)).ToList();\n            revisionDate = await _cipherRepository.RestoreByIdsOrganizationIdAsync(restoringCiphers.Select(c => c.Id), organizationId.Value);\n        }\n        else\n        {\n            var ciphers = await _cipherRepository.GetManyByUserIdAsync(restoringUserId);\n            var filteredCiphers = await FilterCiphersByDeletePermission(ciphers, cipherIdsSet, restoringUserId);\n            restoringCiphers = filteredCiphers.Select(c => (CipherOrganizationDetails)c).ToList();\n            revisionDate = await _cipherRepository.RestoreAsync(restoringCiphers.Select(c => c.Id), restoringUserId);\n        }\n\n        var events = restoringCiphers.Select(c =>\n        {\n            c.RevisionDate = revisionDate.Value;\n            c.DeletedDate = null;\n            return new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Restored, null);\n        });\n        foreach (var eventsBatch in events.Chunk(100))\n        {\n            await _eventService.LogCipherEventsAsync(eventsBatch);\n        }\n\n        // push\n        await _pushService.PushSyncCiphersAsync(restoringUserId);\n\n        return restoringCiphers;\n    }\n\n    public async Task ValidateBulkCollectionAssignmentAsync(IEnumerable<Guid> collectionIds, IEnumerable<Guid> cipherIds, Guid userId)\n    {\n        foreach (var cipherId in cipherIds)\n        {\n            var cipher = await _cipherRepository.GetByIdAsync(cipherId);\n            await ValidateChangeInCollectionsAsync(cipher, collectionIds, userId);\n        }\n    }\n\n    private async Task<bool> UserCanEditAsync(Cipher cipher, Guid userId)\n    {\n        if (!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId)\n        {\n            return true;\n        }\n\n        return await _cipherRepository.GetCanEditByIdAsync(userId, cipher.Id);\n    }\n\n    private async Task<bool> UserCanDeleteAsync(CipherDetails cipher, Guid userId)\n    {\n        var user = await _userService.GetUserByIdAsync(userId);\n        var organizationAbility = cipher.OrganizationId.HasValue ?\n            await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;\n\n        return NormalCipherPermissions.CanDelete(user, cipher, organizationAbility);\n    }\n\n    private async Task<bool> UserCanRestoreAsync(CipherDetails cipher, Guid userId)\n    {\n        var user = await _userService.GetUserByIdAsync(userId);\n        var organizationAbility = cipher.OrganizationId.HasValue ?\n            await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value) : null;\n\n        return NormalCipherPermissions.CanRestore(user, cipher, organizationAbility);\n    }\n\n    private void ValidateCipherLastKnownRevisionDate(Cipher cipher, DateTime? lastKnownRevisionDate)\n    {\n        if (cipher.Id == default || !lastKnownRevisionDate.HasValue)\n        {\n            return;\n        }\n\n        if ((cipher.RevisionDate - lastKnownRevisionDate.Value).Duration() > TimeSpan.FromSeconds(1))\n        {\n            throw new BadRequestException(\n                \"The item cannot be saved because it is out of date. To edit this item, first sync your vault, or log out and back in.\"\n            );\n        }\n    }\n\n    private async Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, bool orgAdmin)\n    {\n        if (attachmentData == null || string.IsNullOrWhiteSpace(attachmentData.AttachmentId))\n        {\n            return null;\n        }\n\n        await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentData.AttachmentId);\n        cipher.DeleteAttachment(attachmentData.AttachmentId);\n        await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, attachmentData);\n        await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_AttachmentDeleted);\n\n        // Update the revision date when an attachment is deleted\n        cipher.RevisionDate = DateTime.UtcNow;\n        if (orgAdmin)\n        {\n            await _cipherRepository.ReplaceAsync(cipher);\n        }\n        else\n        {\n            await _cipherRepository.ReplaceAsync((CipherDetails)cipher);\n        }\n\n        // push\n        await _pushService.PushSyncCipherUpdateAsync(cipher, null);\n\n        return new DeleteAttachmentResponseData(cipher);\n    }\n\n    public async Task ValidateCipherEditForAttachmentAsync(Cipher cipher, Guid savingUserId, bool orgAdmin,\n        long requestLength)\n    {\n        if (!orgAdmin && !(await UserCanEditAsync(cipher, savingUserId)))\n        {\n            throw new BadRequestException(\"You do not have permissions to edit this.\");\n        }\n\n        if (requestLength < 1)\n        {\n            throw new BadRequestException(\"No data to attach.\");\n        }\n\n        var storageBytesRemaining = await StorageBytesRemainingForCipherAsync(cipher);\n\n        if (storageBytesRemaining < requestLength)\n        {\n            throw new BadRequestException(\"Not enough storage available.\");\n        }\n    }\n\n    private async Task<long> StorageBytesRemainingForCipherAsync(Cipher cipher)\n    {\n        var storageBytesRemaining = 0L;\n        if (cipher.UserId.HasValue)\n        {\n            var user = await _userRepository.GetByIdAsync(cipher.UserId.Value);\n            if (!(await _userService.CanAccessPremium(user)))\n            {\n                throw new BadRequestException(\"You must have premium status to use attachments.\");\n            }\n\n            if (user.Premium)\n            {\n                storageBytesRemaining = user.StorageBytesRemaining();\n            }\n            else\n            {\n                // Users that get access to file storage/premium from their organization get storage\n                // based on the current premium plan from the pricing service\n                short provided;\n                if (_globalSettings.SelfHosted)\n                {\n                    provided = Constants.SelfHostedMaxStorageGb;\n                }\n                else\n                {\n                    var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();\n                    provided = (short)premiumPlan.Storage.Provided;\n                }\n                storageBytesRemaining = user.StorageBytesRemaining(provided);\n            }\n        }\n        else if (cipher.OrganizationId.HasValue)\n        {\n            var org = await _organizationRepository.GetByIdAsync(cipher.OrganizationId.Value);\n            if (!org.MaxStorageGb.HasValue)\n            {\n                throw new BadRequestException(\"This organization cannot use attachments.\");\n            }\n\n            storageBytesRemaining = org.StorageBytesRemaining();\n        }\n\n        return storageBytesRemaining;\n    }\n\n    private async Task ValidateCipherCanBeShared(\n        Cipher cipher,\n        Guid sharingUserId,\n        Guid organizationId,\n        DateTime? lastKnownRevisionDate)\n    {\n        if (cipher.Id == default(Guid))\n        {\n            throw new BadRequestException(\"Cipher must already exist.\");\n        }\n\n        if (cipher.OrganizationId.HasValue)\n        {\n            throw new BadRequestException(\"One or more ciphers already belong to an organization.\");\n        }\n\n        if (!cipher.UserId.HasValue || cipher.UserId.Value != sharingUserId)\n        {\n            throw new BadRequestException(\"One or more ciphers do not belong to you.\");\n        }\n\n        var attachments = cipher.GetAttachments();\n        var hasAttachments = attachments?.Any() ?? false;\n        var org = await _organizationRepository.GetByIdAsync(organizationId);\n\n        if (org == null)\n        {\n            throw new BadRequestException(\"Could not find organization.\");\n        }\n\n        if (!await IgnoreStorageLimitsOnMigrationAsync(sharingUserId, org))\n        {\n            if (hasAttachments && !org.MaxStorageGb.HasValue)\n            {\n                throw new BadRequestException(\"This organization cannot use attachments.\");\n            }\n\n            var storageAdjustment = attachments?.Sum(a => a.Value.Size) ?? 0;\n            if (org.StorageBytesRemaining() < storageAdjustment)\n            {\n                throw new BadRequestException(\"Not enough storage available for this organization.\");\n            }\n        }\n\n        ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);\n    }\n\n    /// <summary>\n    /// Checks if the storage limit for the org should be ignored due to the Organization Data Ownership Policy\n    /// </summary>\n    private async Task<bool> IgnoreStorageLimitsOnMigrationAsync(Guid userId, Organization organization)\n    {\n        if (!_featureService.IsEnabled(FeatureFlagKeys.MigrateMyVaultToMyItems))\n        {\n            return false;\n        }\n\n        if (!organization.UsePolicies)\n        {\n            return false;\n        }\n\n        var requirement = await _policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId);\n\n        return requirement.IgnoreStorageLimitsOnMigration(organization.Id);\n    }\n\n    // Validates that a cipher is not being added to a default collection when it is only currently only in shared collections\n    private async Task ValidateChangeInCollectionsAsync(Cipher updatedCipher, IEnumerable<Guid> newCollectionIds, Guid userId)\n    {\n\n        if (updatedCipher.Id == Guid.Empty || !updatedCipher.OrganizationId.HasValue)\n        {\n            return;\n        }\n\n        var currentCollectionsForCipher = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, updatedCipher.Id);\n\n        if (!currentCollectionsForCipher.Any())\n        {\n            // When a cipher is not currently in any collections it can be assigned to any type of collection\n            return;\n        }\n\n        var currentCollections = await _collectionRepository.GetManyByManyIdsAsync(currentCollectionsForCipher.Select(c => c.CollectionId));\n\n        var currentCollectionsContainDefault = currentCollections.Any(c => c.Type == CollectionType.DefaultUserCollection);\n\n        // When the current cipher already contains the default collection, no check is needed for if they added or removed\n        // a default collection, because it is already there.\n        if (currentCollectionsContainDefault)\n        {\n            return;\n        }\n\n        var newCollections = await _collectionRepository.GetManyByManyIdsAsync(newCollectionIds);\n        var newCollectionsContainDefault = newCollections.Any(c => c.Type == CollectionType.DefaultUserCollection);\n\n        if (newCollectionsContainDefault)\n        {\n            // User is trying to add the default collection when the cipher is only in shared collections\n            throw new BadRequestException(\"The cipher(s) cannot be assigned to a default collection when only assigned to non-default collections.\");\n        }\n    }\n\n    private string SerializeCipherData(CipherData data)\n    {\n        return data switch\n        {\n            CipherLoginData loginData => JsonSerializer.Serialize(loginData),\n            CipherIdentityData identityData => JsonSerializer.Serialize(identityData),\n            CipherCardData cardData => JsonSerializer.Serialize(cardData),\n            CipherSecureNoteData noteData => JsonSerializer.Serialize(noteData),\n            CipherSSHKeyData sshKeyData => JsonSerializer.Serialize(sshKeyData),\n            _ => throw new ArgumentException(\"Unsupported cipher data type.\", nameof(data))\n        };\n    }\n\n    private CipherData DeserializeCipherData(Cipher cipher)\n    {\n        return cipher.Type switch\n        {\n            CipherType.Login => JsonSerializer.Deserialize<CipherLoginData>(cipher.Data),\n            CipherType.Identity => JsonSerializer.Deserialize<CipherIdentityData>(cipher.Data),\n            CipherType.Card => JsonSerializer.Deserialize<CipherCardData>(cipher.Data),\n            CipherType.SecureNote => JsonSerializer.Deserialize<CipherSecureNoteData>(cipher.Data),\n            CipherType.SSHKey => JsonSerializer.Deserialize<CipherSSHKeyData>(cipher.Data),\n            _ => throw new ArgumentException(\"Unsupported cipher type.\", nameof(cipher))\n        };\n    }\n\n    // This method is used to filter ciphers based on the user's permissions to delete them.\n    private async Task<List<T>> FilterCiphersByDeletePermission<T>(\n        IEnumerable<T> ciphers,\n        HashSet<Guid> cipherIdsSet,\n        Guid userId) where T : CipherDetails\n    {\n        var user = await _userService.GetUserByIdAsync(userId);\n        var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n\n        var filteredCiphers = ciphers\n            .Where(c => cipherIdsSet.Contains(c.Id))\n            .GroupBy(c => c.OrganizationId)\n            .SelectMany(group =>\n            {\n                var organizationAbility = group.Key.HasValue &&\n                    organizationAbilities.TryGetValue(group.Key.Value, out var ability) ?\n                    ability : null;\n\n                return group.Where(c => NormalCipherPermissions.CanDelete(user, c, organizationAbility));\n            })\n            .ToList();\n\n        return filteredCiphers;\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Services/Implementations/LocalAttachmentStorageService.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Microsoft.AspNetCore.DataProtection;\n\nnamespace Bit.Core.Vault.Services;\n\npublic class LocalAttachmentStorageService : IAttachmentStorageService\n{\n    private readonly string _baseDirPath;\n    private readonly string _baseTempDirPath;\n    private readonly IDataProtectionProvider _dataProtectionProvider;\n    private readonly string _apiBaseUrl;\n\n    internal static readonly string AttachmentDownloadProtectorPurpose = \"AttachmentDownload\";\n    private static readonly TimeSpan _downloadLinkLifetime = TimeSpan.FromMinutes(1);\n\n    public FileUploadType FileUploadType => FileUploadType.Direct;\n\n    public LocalAttachmentStorageService(\n        IGlobalSettings globalSettings,\n        IDataProtectionProvider dataProtectionProvider)\n    {\n        _baseDirPath = globalSettings.Attachment.BaseDirectory;\n        _baseTempDirPath = $\"{_baseDirPath}/temp\";\n        _dataProtectionProvider = dataProtectionProvider;\n        _apiBaseUrl = globalSettings.BaseServiceUri.Api;\n    }\n\n    public async Task<string> GetAttachmentDownloadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        await InitAsync();\n        var protector = _dataProtectionProvider.CreateProtector(AttachmentDownloadProtectorPurpose);\n        var timedProtector = protector.ToTimeLimitedDataProtector();\n        var token = timedProtector.Protect(\n            $\"{cipher.Id}|{attachmentData.AttachmentId}\",\n            _downloadLinkLifetime);\n        return $\"{_apiBaseUrl}/ciphers/attachment/download?token={Uri.EscapeDataString(token)}\";\n    }\n\n    public (Guid cipherId, string attachmentId) ParseAttachmentDownloadToken(string token)\n    {\n        var protector = _dataProtectionProvider\n            .CreateProtector(AttachmentDownloadProtectorPurpose)\n            .ToTimeLimitedDataProtector();\n\n        string payload;\n        try\n        {\n            payload = protector.Unprotect(token);\n        }\n        catch\n        {\n            throw new NotFoundException();\n        }\n\n        var parts = payload.Split('|');\n        if (parts.Length != 2 || !Guid.TryParse(parts[0], out var cipherId))\n        {\n            throw new NotFoundException();\n        }\n\n        return (cipherId, parts[1]);\n    }\n\n    public async Task UploadNewAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        await InitAsync();\n        var cipherDirPath = CipherDirectoryPath(cipher.Id, temp: false);\n        CreateDirectoryIfNotExists(cipherDirPath);\n\n        using (var fs = File.Create(AttachmentFilePath(cipherDirPath, attachmentData.AttachmentId)))\n        {\n            stream.Seek(0, SeekOrigin.Begin);\n            await stream.CopyToAsync(fs);\n        }\n    }\n\n    public async Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData)\n    {\n        await InitAsync();\n        var tempCipherOrgDirPath = OrganizationDirectoryPath(cipherId, organizationId, temp: true);\n        CreateDirectoryIfNotExists(tempCipherOrgDirPath);\n\n        using (var fs = File.Create(AttachmentFilePath(tempCipherOrgDirPath, attachmentData.AttachmentId)))\n        {\n            stream.Seek(0, SeekOrigin.Begin);\n            await stream.CopyToAsync(fs);\n        }\n    }\n\n    public async Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData)\n    {\n        await InitAsync();\n        var sourceFilePath = AttachmentFilePath(attachmentData.AttachmentId, cipherId, organizationId, temp: true);\n        if (!File.Exists(sourceFilePath))\n        {\n            return;\n        }\n\n        var destFilePath = AttachmentFilePath(attachmentData.AttachmentId, cipherId, temp: false);\n        if (!File.Exists(destFilePath))\n        {\n            return;\n        }\n\n        var originalFilePath = AttachmentFilePath(attachmentData.AttachmentId, cipherId, temp: true);\n        DeleteFileIfExists(originalFilePath);\n\n        File.Move(destFilePath, originalFilePath);\n        DeleteFileIfExists(destFilePath);\n\n        File.Move(sourceFilePath, destFilePath);\n    }\n\n    public async Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData, string originalContainer)\n    {\n        await InitAsync();\n        DeleteFileIfExists(AttachmentFilePath(attachmentData.AttachmentId, cipherId, organizationId, temp: true));\n\n        var originalFilePath = AttachmentFilePath(attachmentData.AttachmentId, cipherId, temp: true);\n        if (!File.Exists(originalFilePath))\n        {\n            return;\n        }\n\n        var destFilePath = AttachmentFilePath(attachmentData.AttachmentId, cipherId, temp: false);\n        DeleteFileIfExists(destFilePath);\n\n        File.Move(originalFilePath, destFilePath);\n        DeleteFileIfExists(originalFilePath);\n    }\n\n    public async Task DeleteAttachmentAsync(Guid cipherId, CipherAttachment.MetaData attachmentData)\n    {\n        await InitAsync();\n        DeleteFileIfExists(AttachmentFilePath(attachmentData.AttachmentId, cipherId, temp: false));\n    }\n\n    public async Task CleanupAsync(Guid cipherId)\n    {\n        await InitAsync();\n        DeleteDirectoryIfExists(CipherDirectoryPath(cipherId, temp: true));\n    }\n\n    public async Task DeleteAttachmentsForCipherAsync(Guid cipherId)\n    {\n        await InitAsync();\n        DeleteDirectoryIfExists(CipherDirectoryPath(cipherId, temp: false));\n    }\n\n    public async Task DeleteAttachmentsForOrganizationAsync(Guid organizationId)\n    {\n        await InitAsync();\n    }\n\n    public async Task DeleteAttachmentsForUserAsync(Guid userId)\n    {\n        await InitAsync();\n    }\n\n    private void DeleteFileIfExists(string path)\n    {\n        if (File.Exists(path))\n        {\n            File.Delete(path);\n        }\n    }\n\n    private void DeleteDirectoryIfExists(string path)\n    {\n        if (Directory.Exists(path))\n        {\n            Directory.Delete(path, true);\n        }\n    }\n\n    private void CreateDirectoryIfNotExists(string path)\n    {\n        if (!Directory.Exists(path))\n        {\n            Directory.CreateDirectory(path);\n        }\n    }\n\n    private Task InitAsync()\n    {\n        if (!Directory.Exists(_baseDirPath))\n        {\n            Directory.CreateDirectory(_baseDirPath);\n        }\n\n        if (!Directory.Exists(_baseTempDirPath))\n        {\n            Directory.CreateDirectory(_baseTempDirPath);\n        }\n\n        return Task.FromResult(0);\n    }\n\n    private string CipherDirectoryPath(Guid cipherId, bool temp = false) =>\n        Path.Combine(temp ? _baseTempDirPath : _baseDirPath, cipherId.ToString());\n    private string OrganizationDirectoryPath(Guid cipherId, Guid organizationId, bool temp = false) =>\n        Path.Combine(temp ? _baseTempDirPath : _baseDirPath, cipherId.ToString(), organizationId.ToString());\n\n    private string AttachmentFilePath(string dir, string attachmentId) => Path.Combine(dir, attachmentId);\n    private string AttachmentFilePath(string attachmentId, Guid cipherId, Guid? organizationId = null,\n        bool temp = false) =>\n        organizationId.HasValue ?\n        AttachmentFilePath(OrganizationDirectoryPath(cipherId, organizationId.Value, temp), attachmentId) :\n        AttachmentFilePath(CipherDirectoryPath(cipherId, temp), attachmentId);\n    public Task<Stream?> GetAttachmentReadStreamAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        var path = AttachmentFilePath(attachmentData.AttachmentId, cipher.Id, temp: false);\n        if (!File.Exists(path))\n        {\n            return Task.FromResult<Stream?>(null);\n        }\n\n        return Task.FromResult<Stream?>(File.OpenRead(path));\n    }\n\n    public Task<string> GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)\n        => Task.FromResult($\"{cipher.Id}/attachment/{attachmentData.AttachmentId}\");\n\n    public Task<(bool, long?)> ValidateFileAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, long leeway)\n    {\n        long? length = null;\n        var path = AttachmentFilePath(attachmentData.AttachmentId, cipher.Id, temp: false);\n        if (!File.Exists(path))\n        {\n            return Task.FromResult((false, length));\n        }\n\n        length = new FileInfo(path).Length;\n        if (attachmentData.Size < length - leeway || attachmentData.Size > length + leeway)\n        {\n            return Task.FromResult((false, length));\n        }\n\n        return Task.FromResult((true, length));\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/Services/NoopImplementations/NoopAttachmentStorageService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Enums;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\n\nnamespace Bit.Core.Vault.Services;\n\npublic class NoopAttachmentStorageService : IAttachmentStorageService\n{\n    public FileUploadType FileUploadType => FileUploadType.Direct;\n\n    public Task CleanupAsync(Guid cipherId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteAttachmentAsync(Guid cipherId, CipherAttachment.MetaData attachmentData)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteAttachmentsForCipherAsync(Guid cipherId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteAttachmentsForOrganizationAsync(Guid organizationId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task DeleteAttachmentsForUserAsync(Guid userId)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task RollbackShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData, string originalContainer)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task StartShareAttachmentAsync(Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task UploadNewAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task UploadShareAttachmentAsync(Stream stream, Guid cipherId, Guid organizationId, CipherAttachment.MetaData attachmentData)\n    {\n        return Task.FromResult(0);\n    }\n\n    public Task<string> GetAttachmentDownloadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        return Task.FromResult((string)null);\n    }\n\n    public (Guid cipherId, string attachmentId) ParseAttachmentDownloadToken(string token)\n    {\n        throw new NotSupportedException(\"Token-based downloads are not supported with noop storage.\");\n    }\n\n    public Task<Stream> GetAttachmentReadStreamAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        return Task.FromResult<Stream>(null);\n    }\n\n    public Task<string> GetAttachmentUploadUrlAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        return Task.FromResult(default(string));\n    }\n    public Task<(bool, long?)> ValidateFileAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, long leeway)\n    {\n        return Task.FromResult((false, (long?)null));\n    }\n}\n"
  },
  {
    "path": "src/Core/Vault/VaultServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Vault.Commands;\nusing Bit.Core.Vault.Commands.Interfaces;\nusing Bit.Core.Vault.Queries;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Core.Vault;\n\npublic static class VaultServiceCollectionExtensions\n{\n    public static IServiceCollection AddVaultServices(this IServiceCollection services)\n    {\n        services.AddVaultQueries();\n\n        return services;\n    }\n\n    private static void AddVaultQueries(this IServiceCollection services)\n    {\n        services.AddScoped<IOrganizationCiphersQuery, OrganizationCiphersQuery>();\n        services.AddScoped<IGetTaskDetailsForUserQuery, GetTaskDetailsForUserQuery>();\n        services.AddScoped<IMarkTaskAsCompleteCommand, MarkTaskAsCompletedCommand>();\n        services.AddScoped<IGetCipherPermissionsForUserQuery, GetCipherPermissionsForUserQuery>();\n        services.AddScoped<IGetTasksForOrganizationQuery, GetTasksForOrganizationQuery>();\n        services.AddScoped<IGetSecurityTasksNotificationDetailsQuery, GetSecurityTasksNotificationDetailsQuery>();\n        services.AddScoped<ICreateManyTaskNotificationsCommand, CreateManyTaskNotificationsCommand>();\n        services.AddScoped<ICreateManyTasksCommand, CreateManyTasksCommand>();\n        services.AddScoped<IArchiveCiphersCommand, ArchiveCiphersCommand>();\n        services.AddScoped<IUnarchiveCiphersCommand, UnarchiveCiphersCommand>();\n        services.AddScoped<IMarkNotificationsForTaskAsDeletedCommand, MarkNotificationsForTaskAsDeletedCommand>();\n        services.AddScoped<IGetTaskMetricsForOrganizationQuery, GetTaskMetricsForOrganizationQuery>();\n    }\n}\n"
  },
  {
    "path": "src/Events/Controllers/CollectController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Events.Models;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Events.Controllers;\n\n[Route(\"collect\")]\n[Authorize(\"Application\")]\npublic class CollectController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IEventService _eventService;\n    private readonly ICipherRepository _cipherRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    public CollectController(\n        ICurrentContext currentContext,\n        IEventService eventService,\n        ICipherRepository cipherRepository,\n        IOrganizationRepository organizationRepository,\n        IOrganizationUserRepository organizationUserRepository\n        )\n    {\n        _currentContext = currentContext;\n        _eventService = eventService;\n        _cipherRepository = cipherRepository;\n        _organizationRepository = organizationRepository;\n        _organizationUserRepository = organizationUserRepository;\n    }\n\n    [HttpPost]\n    public async Task<IActionResult> Post([FromBody] IEnumerable<EventModel> model)\n    {\n        if (model == null || !model.Any())\n        {\n            return new BadRequestResult();\n        }\n\n        var cipherEvents = new List<Tuple<Cipher, EventType, DateTime?>>();\n        var ciphersCache = new Dictionary<Guid, Cipher>();\n\n        foreach (var eventModel in model)\n        {\n            switch (eventModel.Type)\n            {\n                // User events\n                case EventType.User_ClientExportedVault:\n                    await _eventService.LogUserEventAsync(_currentContext.UserId.Value, eventModel.Type, eventModel.Date);\n                    break;\n\n                case EventType.Organization_ItemOrganization_Accepted:\n                case EventType.Organization_ItemOrganization_Declined:\n                    if (!eventModel.OrganizationId.HasValue || !_currentContext.UserId.HasValue)\n                    {\n                        continue;\n                    }\n\n                    var orgUser = await _organizationUserRepository.GetByOrganizationAsync(eventModel.OrganizationId.Value, _currentContext.UserId.Value);\n\n                    if (orgUser == null)\n                    {\n                        continue;\n                    }\n\n                    await _eventService.LogOrganizationUserEventAsync(orgUser, eventModel.Type, eventModel.Date);\n\n                    continue;\n\n                // Cipher events\n                case EventType.Cipher_ClientAutofilled:\n                case EventType.Cipher_ClientCopiedHiddenField:\n                case EventType.Cipher_ClientCopiedPassword:\n                case EventType.Cipher_ClientCopiedCardCode:\n                case EventType.Cipher_ClientToggledCardNumberVisible:\n                case EventType.Cipher_ClientToggledCardCodeVisible:\n                case EventType.Cipher_ClientToggledHiddenFieldVisible:\n                case EventType.Cipher_ClientToggledPasswordVisible:\n                case EventType.Cipher_ClientViewed:\n                    if (!eventModel.CipherId.HasValue)\n                    {\n                        continue;\n                    }\n\n                    Cipher cipher;\n                    if (ciphersCache.TryGetValue(eventModel.CipherId.Value, out var cachedCipher))\n                    {\n                        cipher = cachedCipher;\n                    }\n                    else\n                    {\n                        cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value,\n                           _currentContext.UserId.Value);\n                    }\n\n                    if (cipher == null)\n                    {\n                        // When the user cannot access the cipher directly, check if the organization allows for\n                        // admin/owners access to all collections and the user can access the cipher from that perspective.\n                        if (!eventModel.OrganizationId.HasValue)\n                        {\n                            continue;\n                        }\n\n                        cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value);\n                        if (cipher == null)\n                        {\n                            continue;\n                        }\n\n                        var cipherBelongsToOrg = cipher.OrganizationId == eventModel.OrganizationId;\n                        var org = _currentContext.GetOrganization(eventModel.OrganizationId.Value);\n\n                        if (!cipherBelongsToOrg || org == null)\n                        {\n                            continue;\n                        }\n                    }\n\n                    ciphersCache.TryAdd(eventModel.CipherId.Value, cipher);\n                    cipherEvents.Add(new Tuple<Cipher, EventType, DateTime?>(cipher, eventModel.Type, eventModel.Date));\n                    break;\n\n                case EventType.Organization_ClientExportedVault:\n                case EventType.Organization_AutoConfirmEnabled_Admin:\n                case EventType.Organization_AutoConfirmDisabled_Admin:\n                    if (!eventModel.OrganizationId.HasValue)\n                    {\n                        continue;\n                    }\n\n                    var organization = await _organizationRepository.GetByIdAsync(eventModel.OrganizationId.Value);\n                    if (organization == null)\n                    {\n                        continue;\n                    }\n\n                    await _eventService.LogOrganizationEventAsync(organization, eventModel.Type, eventModel.Date);\n                    break;\n\n                default:\n                    continue;\n            }\n        }\n\n        if (cipherEvents.Any())\n        {\n            foreach (var eventsBatch in cipherEvents.Chunk(50))\n            {\n                await _eventService.LogCipherEventsAsync(eventsBatch);\n            }\n        }\n\n        return new OkResult();\n    }\n}\n"
  },
  {
    "path": "src/Events/Controllers/InfoController.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Events.Controllers;\n\npublic class InfoController : Controller\n{\n    [HttpGet(\"~/alive\")]\n    [HttpGet(\"~/now\")]\n    public DateTime GetAlive()\n    {\n        return DateTime.UtcNow;\n    }\n\n    [HttpGet(\"~/version\")]\n    public JsonResult GetVersion()\n    {\n        return Json(AssemblyHelpers.GetVersion());\n    }\n}\n"
  },
  {
    "path": "src/Events/Dockerfile",
    "content": "###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\n# We put the value in a file to be read by later layers.\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/src/Events\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    icu-libs \\\n    krb5 \\\n    shadow \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu\n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/src/Events/out /app\nCOPY ./src/Events/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\nHEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "src/Events/Events.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-Events</UserSecretsId>\n    <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Events' \" />\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Events-SelfHost' \" />\n  <ItemGroup>\n    <ProjectReference Include=\"..\\SharedWeb\\SharedWeb.csproj\" />\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Events/Models/EventModel.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Events.Models;\n\npublic class EventModel\n{\n    public EventType Type { get; set; }\n    public Guid? CipherId { get; set; }\n    public DateTime Date { get; set; }\n    public Guid? OrganizationId { get; set; }\n}\n"
  },
  {
    "path": "src/Events/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Events;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        Host\n            .CreateDefaultBuilder(args)\n            .UseBitwardenSdk()\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging()\n            .Build()\n            .Run();\n    }\n}\n"
  },
  {
    "path": "src/Events/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:46273/\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Events\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:46273/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Events-SelfHost\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:46274/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"developSelfHosted\": \"true\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Events/Startup.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Core.AdminConsole.AbilitiesCache;\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Context;\nusing Bit.Core.Services;\nusing Bit.Core.Services.Implementations;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.SharedWeb.Utilities;\nusing Duende.IdentityModel;\n\nnamespace Bit.Events;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(\"en-US\");\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; }\n    public IWebHostEnvironment Environment { get; set; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n\n        // Data Protection\n        services.AddCustomDataProtectionServices(Environment, globalSettings);\n\n        // Repositories\n        services.AddDatabaseRepositories(globalSettings);\n        services.AddTestPlayIdTracking(globalSettings);\n\n        // Context\n        services.AddScoped<ICurrentContext, CurrentContext>();\n\n        // Identity\n        services.AddIdentityAuthenticationServices(globalSettings, Environment, config =>\n        {\n            config.AddPolicy(\"Application\", policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, \"Application\", \"external\");\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);\n            });\n        });\n\n        // Services\n        var usingServiceBusAppCache = CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&\n            CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName);\n        services.AddScoped<IApplicationCacheService, FeatureRoutedCacheService>();\n\n        if (usingServiceBusAppCache)\n        {\n            services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryServiceBusApplicationCacheService>();\n        }\n        else\n        {\n            services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryApplicationCacheService>();\n        }\n\n        services.AddEventWriteServices(globalSettings);\n        services.AddScoped<IEventService, EventService>();\n\n        services.AddOptionality();\n\n        // Mvc\n        services.AddMvc(config =>\n        {\n            config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());\n        });\n\n        if (usingServiceBusAppCache)\n        {\n            services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();\n        }\n\n        // Add event integration services\n        services.AddDistributedCache(globalSettings);\n        services.AddRabbitMqListeners(globalSettings);\n    }\n\n    public void Configure(\n        IApplicationBuilder app,\n        IWebHostEnvironment env,\n        GlobalSettings globalSettings)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n\n        // Forwarding Headers\n        if (globalSettings.SelfHosted)\n        {\n            app.UseForwardedHeaders(globalSettings);\n        }\n\n        if (env.IsDevelopment())\n        {\n            app.UseDeveloperExceptionPage();\n        }\n\n        // Default Middleware\n        app.UseDefaultMiddleware(env, globalSettings);\n\n        // Add routing\n        app.UseRouting();\n\n        // Add Cors\n        app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))\n            .AllowAnyMethod().AllowAnyHeader().AllowCredentials());\n\n        // Add authentication and authorization to the request pipeline.\n        app.UseAuthentication();\n        app.UseAuthorization();\n\n        // Add current context\n        app.UseMiddleware<CurrentContextMiddleware>();\n\n        // Add MVC to the request pipeline.\n        app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());\n    }\n}\n"
  },
  {
    "path": "src/Events/appsettings.Development.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\",\n      \"internalScim\": \"http://localhost:44559\"\n    },\n    \"events\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"storage\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Events/appsettings.Production.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.bitwarden.com\",\n      \"api\": \"https://api.bitwarden.com\",\n      \"identity\": \"https://identity.bitwarden.com\",\n      \"admin\": \"https://admin.bitwarden.com\",\n      \"notifications\": \"https://notifications.bitwarden.com\",\n      \"sso\": \"https://sso.bitwarden.com\",\n      \"internalNotifications\": \"https://notifications.bitwarden.com\",\n      \"internalAdmin\": \"https://admin.bitwarden.com\",\n      \"internalIdentity\": \"https://identity.bitwarden.com\",\n      \"internalApi\": \"https://api.bitwarden.com\",\n      \"internalVault\": \"https://vault.bitwarden.com\",\n      \"internalSso\": \"https://sso.bitwarden.com\",\n      \"internalScim\": \"https://scim.bitwarden.com\"\n    }\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Warning\",\n          \"System\": \"Warning\",\n          \"Microsoft\": \"Warning\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Events/appsettings.QA.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.qa.bitwarden.pw\",\n      \"api\": \"https://api.qa.bitwarden.pw\",\n      \"identity\": \"https://identity.qa.bitwarden.pw\",\n      \"admin\": \"https://admin.qa.bitwarden.pw\",\n      \"notifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"sso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalNotifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"internalAdmin\": \"https://admin.qa.bitwarden.pw\",\n      \"internalIdentity\": \"https://identity.qa.bitwarden.pw\",\n      \"internalApi\": \"https://api.qa.bitwarden.pw\",\n      \"internalVault\": \"https://vault.qa.bitwarden.pw\",\n      \"internalSso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalScim\": \"https://scim.qa.bitwarden.pw\"\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Debug\",\n          \"System\": \"Debug\",\n          \"Microsoft\": \"Debug\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Events/appsettings.SelfHosted.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": null,\n      \"api\": null,\n      \"identity\": null,\n      \"admin\": null,\n      \"notifications\": null,\n      \"sso\": null,\n      \"internalNotifications\": null,\n      \"internalAdmin\": null,\n      \"internalIdentity\": null,\n      \"internalApi\": null,\n      \"internalVault\": null,\n      \"internalSso\": null,\n      \"internalScim\": null\n    }\n  }\n}\n"
  },
  {
    "path": "src/Events/appsettings.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"selfHosted\": false,\n    \"projectName\": \"Events\",\n    \"sqlServer\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"identityServer\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"storage\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"events\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"amazon\": {\n      \"accessKeyId\": \"SECRET\",\n      \"accessKeySecret\": \"SECRET\",\n      \"region\": \"SECRET\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Events/build.ps1",
    "content": "$dir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\necho \"`n## Building Events\"\n\necho \"`nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore $dir\\Events.csproj\necho \"Clean\"\ndotnet clean $dir\\Events.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\necho \"Publish\"\ndotnet publish $dir\\Events.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\n"
  },
  {
    "path": "src/Events/build.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\necho -e \"\\n## Building Events\"\n\necho -e \"\\nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore \"$DIR/Events.csproj\"\necho \"Clean\"\ndotnet clean \"$DIR/Events.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\necho \"Publish\"\ndotnet publish \"$DIR/Events.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\n"
  },
  {
    "path": "src/Events/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/core\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    if [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n      chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos\n    fi\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nif [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n    cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf\n    $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab\nfi\n\nexec $gosu_cmd /app/Events\n"
  },
  {
    "path": "src/EventsProcessor/AzureQueueHostedService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Azure.Storage.Queues;\nusing Bit.Core;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.EventsProcessor;\n\npublic class AzureQueueHostedService : IHostedService, IDisposable\n{\n    private readonly ILogger<AzureQueueHostedService> _logger;\n    private readonly GlobalSettings _globalSettings;\n\n    private Task _executingTask;\n    private CancellationTokenSource _cts;\n    private QueueClient _queueClient;\n    private IEventWriteService _eventWriteService;\n\n    public AzureQueueHostedService(\n        ILogger<AzureQueueHostedService> logger,\n        GlobalSettings globalSettings)\n    {\n        _logger = logger;\n        _globalSettings = globalSettings;\n    }\n\n    public Task StartAsync(CancellationToken cancellationToken)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId, \"Starting service.\");\n        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        _executingTask = ExecuteAsync(_cts.Token);\n\n        return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;\n    }\n\n    public async Task StopAsync(CancellationToken cancellationToken)\n    {\n        if (_executingTask == null)\n        {\n            return;\n        }\n\n        _logger.LogWarning(\"Stopping service.\");\n\n        await _cts.CancelAsync();\n        await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));\n        cancellationToken.ThrowIfCancellationRequested();\n    }\n\n    public void Dispose()\n    { }\n\n    private async Task ExecuteAsync(CancellationToken cancellationToken)\n    {\n        var storageConnectionString = _globalSettings.Events.ConnectionString;\n        var queueName = _globalSettings.Events.QueueName;\n        if (string.IsNullOrWhiteSpace(storageConnectionString) ||\n            string.IsNullOrWhiteSpace(queueName))\n        {\n            _logger.LogInformation(\"Azure Queue Hosted Service is disabled. Missing connection string or queue name.\");\n            return;\n        }\n\n        var repo = new Core.Repositories.TableStorage.EventRepository(storageConnectionString);\n        _eventWriteService = new RepositoryEventWriteService(repo);\n        _queueClient = new QueueClient(storageConnectionString, queueName);\n\n        while (!cancellationToken.IsCancellationRequested)\n        {\n            try\n            {\n                var messages = await _queueClient.ReceiveMessagesAsync(32,\n                    cancellationToken: cancellationToken);\n                if (messages.Value?.Any() ?? false)\n                {\n                    foreach (var message in messages.Value)\n                    {\n                        await ProcessQueueMessageAsync(message.DecodeMessageText(), cancellationToken);\n                        await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt,\n                            cancellationToken);\n                    }\n                }\n                else\n                {\n                    await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);\n                }\n            }\n            catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)\n            {\n                _logger.LogDebug(\"Task.Delay cancelled during Alpine container shutdown\");\n                break;\n            }\n            catch (Exception ex)\n            {\n                _logger.LogError(ex, \"Error occurred processing message block.\");\n\n                try\n                {\n                    await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);\n                }\n                catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)\n                {\n                    _logger.LogDebug(\"Task.Delay cancelled during Alpine container shutdown\");\n                    break;\n                }\n            }\n        }\n\n        _logger.LogWarning(\"Done processing messages.\");\n    }\n\n    public async Task ProcessQueueMessageAsync(string message, CancellationToken cancellationToken)\n    {\n        if (_eventWriteService == null || message == null || message.Length == 0)\n        {\n            return;\n        }\n\n        try\n        {\n            _logger.LogInformation(\"Processing message.\");\n\n            var events = new List<IEvent>();\n            using var jsonDocument = JsonDocument.Parse(message);\n            var root = jsonDocument.RootElement;\n            if (root.ValueKind == JsonValueKind.Array)\n            {\n                var indexedEntities = root.Deserialize<List<EventMessage>>()\n                    .SelectMany(EventTableEntity.IndexEvent);\n                events.AddRange(indexedEntities);\n            }\n            else if (root.ValueKind == JsonValueKind.Object)\n            {\n                var eventMessage = root.Deserialize<EventMessage>();\n                events.AddRange(EventTableEntity.IndexEvent(eventMessage));\n            }\n\n            cancellationToken.ThrowIfCancellationRequested();\n\n            await _eventWriteService.CreateManyAsync(events);\n\n            _logger.LogInformation(\"Processed message.\");\n        }\n        catch (JsonException ex)\n        {\n            _logger.LogError(ex, \"Unable to parse message.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/EventsProcessor/Dockerfile",
    "content": "###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\n# We put the value in a file to be read by later layers.\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/src/EventsProcessor\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    icu-libs \\\n    shadow \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu \n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/src/EventsProcessor/out /app\nCOPY ./src/EventsProcessor/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\nHEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1\n\nCMD [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "src/EventsProcessor/EventsProcessor.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-EventsProcessor</UserSecretsId>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'EventsProcessor' \" />\n  <ItemGroup>\n    <ProjectReference Include=\"..\\SharedWeb\\SharedWeb.csproj\" />\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/EventsProcessor/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.EventsProcessor;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        Host\n            .CreateDefaultBuilder(args)\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging()\n            .Build()\n            .Run();\n    }\n}\n"
  },
  {
    "path": "src/EventsProcessor/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:54103/\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"EventsProcessor\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:54103/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/EventsProcessor/Startup.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Core.Utilities;\nusing Bit.SharedWeb.Utilities;\n\nnamespace Bit.EventsProcessor;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(\"en-US\");\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; }\n    public IWebHostEnvironment Environment { get; set; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n\n        // Data Protection\n        services.AddCustomDataProtectionServices(Environment, globalSettings);\n\n        // Repositories\n        services.AddDatabaseRepositories(globalSettings);\n        services.AddTestPlayIdTracking(globalSettings);\n\n        // Add event integration services\n        services.AddDistributedCache(globalSettings);\n        services.AddAzureServiceBusListeners(globalSettings);\n        services.AddHostedService<AzureQueueHostedService>();\n    }\n\n    public void Configure(IApplicationBuilder app)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n        app.UseRouting();\n        app.UseEndpoints(endpoints =>\n        {\n            endpoints.MapGet(\"/alive\",\n                async context => await context.Response.WriteAsJsonAsync(System.DateTime.UtcNow));\n            endpoints.MapGet(\"/now\",\n                async context => await context.Response.WriteAsJsonAsync(System.DateTime.UtcNow));\n            endpoints.MapGet(\"/version\",\n                async context => await context.Response.WriteAsJsonAsync(AssemblyHelpers.GetVersion()));\n\n        });\n    }\n}\n"
  },
  {
    "path": "src/EventsProcessor/appsettings.Development.json",
    "content": "﻿{\n  \"azureStorageConnectionString\": \"UseDevelopmentStorage=true\"\n}\n"
  },
  {
    "path": "src/EventsProcessor/appsettings.Production.json",
    "content": "﻿{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Warning\",\n          \"System\": \"Warning\",\n          \"Microsoft\": \"Warning\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/EventsProcessor/appsettings.QA.json",
    "content": "﻿{\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Debug\",\n          \"System\": \"Debug\",\n          \"Microsoft\": \"Debug\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/EventsProcessor/appsettings.json",
    "content": "﻿{\n  \"azureStorageConnectionString\": \"SECRET\",\n  \"globalSettings\": {\n    \"selfHosted\": false,\n    \"projectName\": \"Events Processor\"\n  }\n}\n"
  },
  {
    "path": "src/EventsProcessor/build.ps1",
    "content": "$dir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\necho \"`n## Building Events Processor\"\n\necho \"`nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore $dir\\EventsProcessor.csproj\necho \"Clean\"\ndotnet clean $dir\\EventsProcessor.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\necho \"Publish\"\ndotnet publish $dir\\EventsProcessor.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\n"
  },
  {
    "path": "src/EventsProcessor/build.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\necho -e \"\\n## Building Event Processor\"\n\necho -e \"\\nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore \"$DIR/EventsProcessor.csproj\"\necho \"Clean\"\ndotnet clean \"$DIR/EventsProcessor.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\necho \"Publish\"\ndotnet publish \"$DIR/EventsProcessor.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\n"
  },
  {
    "path": "src/EventsProcessor/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nexec $gosu_cmd /app/EventsProcessor\n"
  },
  {
    "path": "src/Icons/Controllers/ChangePasswordUriController.cs",
    "content": "﻿using Bit.Icons.Models;\nusing Bit.Icons.Services;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Caching.Memory;\n\nnamespace Bit.Icons.Controllers;\n\n[Route(\"~/change-password-uri\")]\npublic class ChangePasswordUriController : Controller\n{\n    private readonly IMemoryCache _memoryCache;\n    private readonly IDomainMappingService _domainMappingService;\n    private readonly IChangePasswordUriService _changePasswordService;\n    private readonly ChangePasswordUriSettings _changePasswordSettings;\n    private readonly ILogger<ChangePasswordUriController> _logger;\n\n    public ChangePasswordUriController(\n        IMemoryCache memoryCache,\n        IDomainMappingService domainMappingService,\n        IChangePasswordUriService changePasswordService,\n        ChangePasswordUriSettings changePasswordUriSettings,\n        ILogger<ChangePasswordUriController> logger)\n    {\n        _memoryCache = memoryCache;\n        _domainMappingService = domainMappingService;\n        _changePasswordService = changePasswordService;\n        _changePasswordSettings = changePasswordUriSettings;\n        _logger = logger;\n    }\n\n    [HttpGet(\"config\")]\n    public IActionResult GetConfig()\n    {\n        return new JsonResult(new\n        {\n            _changePasswordSettings.CacheEnabled,\n            _changePasswordSettings.CacheHours,\n            _changePasswordSettings.CacheSizeLimit\n        });\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> Get([FromQuery] string uri)\n    {\n        if (string.IsNullOrWhiteSpace(uri))\n        {\n            return new BadRequestResult();\n        }\n\n        var uriHasProtocol = uri.StartsWith(\"http://\", StringComparison.OrdinalIgnoreCase) ||\n                          uri.StartsWith(\"https://\", StringComparison.OrdinalIgnoreCase);\n\n        var url = uriHasProtocol ? uri : $\"https://{uri}\";\n        if (!Uri.TryCreate(url, UriKind.Absolute, out var validUri))\n        {\n            return new BadRequestResult();\n        }\n\n        var domain = validUri.Host;\n\n        var mappedDomain = _domainMappingService.MapDomain(domain);\n        if (!_changePasswordSettings.CacheEnabled || !_memoryCache.TryGetValue(mappedDomain, out string? changePasswordUri))\n        {\n            var result = await _changePasswordService.GetChangePasswordUri(domain);\n            if (result == null)\n            {\n                _logger.LogWarning(\"Null result returned for {0}.\", domain);\n                changePasswordUri = null;\n            }\n            else\n            {\n                changePasswordUri = result;\n            }\n\n            if (_changePasswordSettings.CacheEnabled)\n            {\n                _logger.LogInformation(\"Cache uri for {0}.\", domain);\n                _memoryCache.Set(mappedDomain, changePasswordUri, new MemoryCacheEntryOptions\n                {\n                    AbsoluteExpirationRelativeToNow = new TimeSpan(_changePasswordSettings.CacheHours, 0, 0),\n                    Size = changePasswordUri?.Length ?? 0,\n                    Priority = changePasswordUri == null ? CacheItemPriority.High : CacheItemPriority.Normal\n                });\n            }\n        }\n\n        return Ok(new ChangePasswordUriResponse(changePasswordUri));\n    }\n}\n"
  },
  {
    "path": "src/Icons/Controllers/IconsController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Icons.Models;\nusing Bit.Icons.Services;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Caching.Memory;\n\nnamespace Bit.Icons.Controllers;\n\n[Route(\"\")]\npublic class IconsController : Controller\n{\n    // Basic bwi-globe icon\n    private static readonly byte[] _notFoundImage = Convert.FromBase64String(\"iVBORw0KGgoAAAANSUhEUg\" +\n        \"AAABMAAAATCAQAAADYWf5HAAABu0lEQVR42nXSvWuTURTH8R+t0heI9Y04aJycdBLNJNrBFBU7OFgUER3q21I0bXK+JwZ\" +\n        \"pXISm/QdcRB3EgqBBsNihsUbbgODQQSKCuKSDOApJuuhj8tCYQj/jvYfD795z1MZ+nBKrNKhSwrMxbZTrtRnqlEjZkB/x\" +\n        \"C/xmhZrlc71qS0Up8yVzTCGucFNKD1JhORVd70SZNU4okNx5d4+U2UXRIpJFWLClsR79YzN88wQvLWNzzPKEeS/wkQGpW\" +\n        \"VhhqhW8TtDJD3Mm1x/23zLSrZCdpBY8BueTNjHSbc+8wC9HlHgU5Aj5AW5zPdcVdpq0UcknWBSr/pjixO4gfp899Kd23p\" +\n        \"M2qQCH7LkCnqAqGh73OK/8NPOcaibr90LrW/yWAnaUhqjaOSl9nFR2r5rsqo22ypn1B5IN8VOUMHVgOnNQIX+d62plcz6\" +\n        \"rg1/jskK8CMb4we4pG6OWHtR/LBJkC2E4a7ZPkuX5ntumAOM2xxveclEhLvGH6XCmLPs735Eetrw63NnOgr9P9q1viC3x\" +\n        \"lRUGOjImqFDuOBvrYYoaZU9z1uPpYae5NfdvbNVG2ZjDIlXq/oMi46lo++4vjjPBl2Dlg00AAAAASUVORK5CYII=\");\n\n    private readonly IMemoryCache _memoryCache;\n    private readonly IDomainMappingService _domainMappingService;\n    private readonly IIconFetchingService _iconFetchingService;\n    private readonly ILogger<IconsController> _logger;\n    private readonly IconsSettings _iconsSettings;\n\n    public IconsController(\n        IMemoryCache memoryCache,\n        IDomainMappingService domainMappingService,\n        IIconFetchingService iconFetchingService,\n        ILogger<IconsController> logger,\n        IconsSettings iconsSettings)\n    {\n        _memoryCache = memoryCache;\n        _domainMappingService = domainMappingService;\n        _iconFetchingService = iconFetchingService;\n        _logger = logger;\n        _iconsSettings = iconsSettings;\n    }\n\n    [HttpGet(\"~/config\")]\n    public IActionResult GetConfig()\n    {\n        return new JsonResult(new\n        {\n            CacheEnabled = _iconsSettings.CacheEnabled,\n            CacheHours = _iconsSettings.CacheHours,\n            CacheSizeLimit = _iconsSettings.CacheSizeLimit\n        });\n    }\n\n    [HttpGet(\"{hostname}/icon.png\")]\n    public async Task<IActionResult> Get(string hostname)\n    {\n        if (string.IsNullOrWhiteSpace(hostname) || !hostname.Contains(\".\"))\n        {\n            return new BadRequestResult();\n        }\n\n        var url = $\"http://{hostname}\";\n        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))\n        {\n            return new BadRequestResult();\n        }\n\n        var domain = uri.Host;\n        // Convert sub.domain.com => domain.com\n        //if(DomainName.TryParseBaseDomain(domain, out var baseDomain))\n        //{\n        //    domain = baseDomain;\n        //}\n\n        var mappedDomain = _domainMappingService.MapDomain(domain);\n        if (!_iconsSettings.CacheEnabled || !_memoryCache.TryGetValue(mappedDomain, out Icon icon))\n        {\n            var result = await _iconFetchingService.GetIconAsync(domain);\n            if (result == null)\n            {\n                _logger.LogWarning(\"Null result returned for {0}.\", domain);\n                icon = null;\n            }\n            else\n            {\n                icon = result;\n            }\n\n            // Only cache not found and smaller images (<= 50kb)\n            if (_iconsSettings.CacheEnabled && (icon == null || icon.Image.Length <= 50012))\n            {\n                _logger.LogInformation(\"Cache icon for {0}.\", domain);\n                _memoryCache.Set(mappedDomain, icon, new MemoryCacheEntryOptions\n                {\n                    AbsoluteExpirationRelativeToNow = new TimeSpan(_iconsSettings.CacheHours, 0, 0),\n                    Size = icon?.Image.Length ?? 0,\n                    Priority = icon == null ? CacheItemPriority.High : CacheItemPriority.Normal\n                });\n            }\n        }\n\n        if (icon == null)\n        {\n            return new FileContentResult(_notFoundImage, \"image/png\");\n        }\n\n        return new FileContentResult(icon.Image, icon.Format);\n    }\n}\n"
  },
  {
    "path": "src/Icons/Controllers/InfoController.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Icons.Controllers;\n\npublic class InfoController : Controller\n{\n    [HttpGet(\"~/alive\")]\n    [HttpGet(\"~/now\")]\n    public DateTime GetAlive()\n    {\n        return DateTime.UtcNow;\n    }\n\n    [HttpGet(\"~/version\")]\n    public JsonResult GetVersion()\n    {\n        return Json(AssemblyHelpers.GetVersion());\n    }\n}\n"
  },
  {
    "path": "src/Icons/Dockerfile",
    "content": "###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/src/Icons\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    krb5 \\\n    icu-libs \\\n    shadow \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu \n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/src/Icons/out /app\nCOPY ./src/Icons/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\nHEALTHCHECK CMD curl -f http://localhost:5000/google.com/icon.png || exit 1\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "src/Icons/Icons.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-Icons</UserSecretsId>\n    <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304;CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Icons' \" />\n  <ItemGroup>\n    <PackageReference Include=\"AngleSharp\" Version=\"1.4.0\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <EmbeddedResource Include=\"Resources\\public_suffix_list.dat\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\SharedWeb\\SharedWeb.csproj\" />\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Icons/IconsSettings.cs",
    "content": "﻿namespace Bit.Icons;\n\npublic class IconsSettings\n{\n    public virtual bool CacheEnabled { get; set; }\n    public virtual int CacheHours { get; set; }\n    public virtual long? CacheSizeLimit { get; set; }\n}\n"
  },
  {
    "path": "src/Icons/Models/ChangePasswordUriResponse.cs",
    "content": "﻿namespace Bit.Icons.Models;\n\npublic class ChangePasswordUriResponse\n{\n    public string? uri { get; set; }\n\n    public ChangePasswordUriResponse(string? uri)\n    {\n        this.uri = uri;\n    }\n}\n"
  },
  {
    "path": "src/Icons/Models/ChangePasswordUriSettings.cs",
    "content": "﻿namespace Bit.Icons.Models;\n\npublic class ChangePasswordUriSettings\n{\n    public virtual bool CacheEnabled { get; set; }\n    public virtual int CacheHours { get; set; }\n    public virtual long? CacheSizeLimit { get; set; }\n}\n"
  },
  {
    "path": "src/Icons/Models/DomainIcons.cs",
    "content": "﻿#nullable enable\n\nusing System.Collections;\nusing AngleSharp.Html.Parser;\nusing Bit.Icons.Extensions;\nusing Bit.Icons.Services;\n\nnamespace Bit.Icons.Models;\n\npublic class DomainIcons : IEnumerable<Icon>\n{\n    private readonly ILogger<IIconFetchingService> _logger;\n    private readonly IHttpClientFactory _httpClientFactory;\n    private readonly IUriService _uriService;\n    private readonly List<Icon> _icons = new();\n\n    public string Domain { get; }\n    public Icon this[int i]\n    {\n        get\n        {\n            return _icons[i];\n        }\n    }\n    public IEnumerator<Icon> GetEnumerator() => ((IEnumerable<Icon>)_icons).GetEnumerator();\n    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_icons).GetEnumerator();\n\n    private DomainIcons(string domain, ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IUriService uriService)\n    {\n        _logger = logger;\n        _httpClientFactory = httpClientFactory;\n        _uriService = uriService;\n        Domain = domain;\n    }\n\n    public static async Task<DomainIcons> FetchAsync(string domain, ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IHtmlParser parser, IUriService uriService)\n    {\n        var pageIcons = new DomainIcons(domain, logger, httpClientFactory, uriService);\n        await pageIcons.FetchIconsAsync(parser);\n        return pageIcons;\n    }\n\n\n    private async Task FetchIconsAsync(IHtmlParser parser)\n    {\n        if (!Uri.TryCreate($\"https://{Domain}\", UriKind.Absolute, out var uri))\n        {\n            _logger.LogWarning(\"Bad domain: {domain}.\", Domain);\n            return;\n        }\n\n        var host = uri.Host;\n\n        // first try https\n        using (var response = await IconHttpRequest.FetchAsync(uri, _logger, _httpClientFactory, _uriService))\n        {\n            if (response.IsSuccessStatusCode)\n            {\n                _icons.AddRange(await response.RetrieveIconsAsync(uri, Domain, parser));\n                return;\n            }\n        }\n\n        // then try http\n        uri = uri.ChangeScheme(\"http\");\n        using (var response = await IconHttpRequest.FetchAsync(uri, _logger, _httpClientFactory, _uriService))\n        {\n            if (response.IsSuccessStatusCode)\n            {\n                _icons.AddRange(await response.RetrieveIconsAsync(uri, Domain, parser));\n                return;\n            }\n        }\n\n        var dotCount = Domain.Count(c => c == '.');\n\n        // Then try base domain\n        if (dotCount > 1 && DomainName.TryParseBaseDomain(Domain, out var baseDomain) &&\n            Uri.TryCreate($\"https://{baseDomain}\", UriKind.Absolute, out uri))\n        {\n            using var response = await IconHttpRequest.FetchAsync(uri, _logger, _httpClientFactory, _uriService);\n            if (response.IsSuccessStatusCode)\n            {\n                _icons.AddRange(await response.RetrieveIconsAsync(uri, Domain, parser));\n                return;\n            }\n        }\n\n        // Then try www\n        if (dotCount < 2 && Uri.TryCreate($\"https://www.{host}\", UriKind.Absolute, out uri))\n        {\n            using var response = await IconHttpRequest.FetchAsync(uri, _logger, _httpClientFactory, _uriService);\n            if (response.IsSuccessStatusCode)\n            {\n                _icons.AddRange(await response.RetrieveIconsAsync(uri, Domain, parser));\n                return;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Icons/Models/DomainName.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Diagnostics;\nusing System.Reflection;\nusing System.Text.RegularExpressions;\n\nnamespace Bit.Icons.Models;\n\n// ref: https://github.com/danesparza/domainname-parser\npublic class DomainName\n{\n    private const string IpRegex = \"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.\" +\n        \"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.\" +\n        \"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.\" +\n        \"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$\";\n\n    private string _subDomain = string.Empty;\n    private string _domain = string.Empty;\n    private string _tld = string.Empty;\n    private TLDRule _tldRule = null;\n\n    public string SubDomain => _subDomain;\n    public string Domain => _domain;\n    public string SLD => _domain;\n    public string TLD => _tld;\n    public TLDRule Rule => _tldRule;\n    public string BaseDomain => $\"{_domain}.{_tld}\";\n\n    public DomainName(string TLD, string SLD, string SubDomain, TLDRule TLDRule)\n    {\n        _tld = TLD;\n        _domain = SLD;\n        _subDomain = SubDomain;\n        _tldRule = TLDRule;\n    }\n\n    public static bool TryParse(string domainString, out DomainName result)\n    {\n        var retval = false;\n\n        //  Our temporary domain parts:\n        var tld = string.Empty;\n        var sld = string.Empty;\n        var subdomain = string.Empty;\n        TLDRule _tldrule = null;\n        result = null;\n\n        try\n        {\n            //  Try parsing the domain name ... this might throw formatting exceptions\n            ParseDomainName(domainString, out tld, out sld, out subdomain, out _tldrule);\n            //  Construct a new DomainName object and return it\n            result = new DomainName(tld, sld, subdomain, _tldrule);\n            //  Return 'true'\n            retval = true;\n        }\n        catch\n        {\n            //  Looks like something bad happened -- return 'false'\n            retval = false;\n        }\n\n        return retval;\n    }\n\n    public static bool TryParseBaseDomain(string domainString, out string result)\n    {\n        if (Regex.IsMatch(domainString, IpRegex))\n        {\n            result = domainString;\n            return true;\n        }\n\n        DomainName domain;\n        var retval = TryParse(domainString, out domain);\n        result = domain?.BaseDomain;\n        return retval;\n    }\n\n    private static void ParseDomainName(string domainString, out string TLD, out string SLD,\n        out string SubDomain, out TLDRule MatchingRule)\n    {\n        // Make sure domain is all lowercase\n        domainString = domainString.ToLower();\n\n        TLD = string.Empty;\n        SLD = string.Empty;\n        SubDomain = string.Empty;\n        MatchingRule = null;\n\n        //  If the fqdn is empty, we have a problem already\n        if (domainString.Trim() == string.Empty)\n        {\n            throw new ArgumentException(\"The domain cannot be blank\");\n        }\n\n        //  Next, find the matching rule:\n        MatchingRule = FindMatchingTLDRule(domainString);\n\n        //  At this point, no rules match, we have a problem\n        if (MatchingRule == null)\n        {\n            throw new FormatException(\"The domain does not have a recognized TLD\");\n        }\n\n        //  Based on the tld rule found, get the domain (and possibly the subdomain)\n        var tempSudomainAndDomain = string.Empty;\n        var tldIndex = 0;\n\n        //  First, determine what type of rule we have, and set the TLD accordingly\n        switch (MatchingRule.Type)\n        {\n            case TLDRule.RuleType.Normal:\n                tldIndex = domainString.LastIndexOf(\".\" + MatchingRule.Name);\n                tempSudomainAndDomain = domainString.Substring(0, tldIndex);\n                TLD = domainString.Substring(tldIndex + 1);\n                break;\n            case TLDRule.RuleType.Wildcard:\n                //  This finds the last portion of the TLD...\n                tldIndex = domainString.LastIndexOf(\".\" + MatchingRule.Name);\n                tempSudomainAndDomain = domainString.Substring(0, tldIndex);\n\n                //  But we need to find the wildcard portion of it:\n                tldIndex = tempSudomainAndDomain.LastIndexOf(\".\");\n                tempSudomainAndDomain = domainString.Substring(0, tldIndex);\n                TLD = domainString.Substring(tldIndex + 1);\n                break;\n            case TLDRule.RuleType.Exception:\n                tldIndex = domainString.LastIndexOf(\".\");\n                tempSudomainAndDomain = domainString.Substring(0, tldIndex);\n                TLD = domainString.Substring(tldIndex + 1);\n                break;\n        }\n\n        //  See if we have a subdomain:\n        List<string> lstRemainingParts = new List<string>(tempSudomainAndDomain.Split('.'));\n\n        //  If we have 0 parts left, there is just a tld and no domain or subdomain\n        //  If we have 1 part, it's the domain, and there is no subdomain\n        //  If we have 2+ parts, the last part is the domain, the other parts (combined) are the subdomain\n        if (lstRemainingParts.Count > 0)\n        {\n            //  Set the domain:\n            SLD = lstRemainingParts[lstRemainingParts.Count - 1];\n\n            //  Set the subdomain, if there is one to set:\n            if (lstRemainingParts.Count > 1)\n            {\n                //  We strip off the trailing period, too\n                SubDomain = tempSudomainAndDomain.Substring(0, tempSudomainAndDomain.Length - SLD.Length - 1);\n            }\n        }\n    }\n\n    private static TLDRule FindMatchingTLDRule(string domainString)\n    {\n        //  Split our domain into parts (based on the '.')\n        //  ...Put these parts in a list\n        //  ...Make sure these parts are in reverse order \n        //     (we'll be checking rules from the right-most pat of the domain)\n        var lstDomainParts = domainString.Split('.').ToList();\n        lstDomainParts.Reverse();\n\n        //  Begin building our partial domain to check rules with:\n        var checkAgainst = string.Empty;\n\n        //  Our 'matches' collection:\n        var ruleMatches = new List<TLDRule>();\n\n        foreach (string domainPart in lstDomainParts)\n        {\n            //  Add on our next domain part:\n            checkAgainst = string.Format(\"{0}.{1}\", domainPart, checkAgainst);\n\n            //  If we end in a period, strip it off:\n            if (checkAgainst.EndsWith(\".\"))\n            {\n                checkAgainst = checkAgainst.Substring(0, checkAgainst.Length - 1);\n            }\n\n            var rules = Enum.GetValues(typeof(TLDRule.RuleType)).Cast<TLDRule.RuleType>();\n            foreach (var rule in rules)\n            {\n                //  Try to match rule:\n                TLDRule result;\n                if (TLDRulesCache.Instance.TLDRuleLists[rule].TryGetValue(checkAgainst, out result))\n                {\n                    ruleMatches.Add(result);\n                }\n            }\n        }\n\n        //  Sort our matches list (longest rule wins, according to :\n        var results = from match in ruleMatches\n                      orderby match.Name.Length descending\n                      select match;\n\n        //  Take the top result (our primary match):\n        var primaryMatch = results.Take(1).SingleOrDefault();\n        return primaryMatch;\n    }\n\n    public class TLDRule : IComparable<TLDRule>\n    {\n        public string Name { get; private set; }\n        public RuleType Type { get; private set; }\n\n        public TLDRule(string RuleInfo)\n        {\n            //  Parse the rule and set properties accordingly:\n            if (RuleInfo.StartsWith(\"*\"))\n            {\n                Type = RuleType.Wildcard;\n                Name = RuleInfo.Substring(2);\n            }\n            else if (RuleInfo.StartsWith(\"!\"))\n            {\n                Type = RuleType.Exception;\n                Name = RuleInfo.Substring(1);\n            }\n            else\n            {\n                Type = RuleType.Normal;\n                Name = RuleInfo;\n            }\n        }\n\n        public int CompareTo(TLDRule other)\n        {\n            if (other == null)\n            {\n                return -1;\n            }\n\n            return Name.CompareTo(other.Name);\n        }\n\n        public enum RuleType\n        {\n            Normal,\n            Wildcard,\n            Exception\n        }\n    }\n\n    public class TLDRulesCache\n    {\n        private static volatile TLDRulesCache _uniqueInstance;\n        private static object _syncObj = new object();\n        private static object _syncList = new object();\n\n        private TLDRulesCache()\n        {\n            //  Initialize our internal list:\n            TLDRuleLists = GetTLDRules();\n        }\n\n        public static TLDRulesCache Instance\n        {\n            get\n            {\n                if (_uniqueInstance == null)\n                {\n                    lock (_syncObj)\n                    {\n                        if (_uniqueInstance == null)\n                        {\n                            _uniqueInstance = new TLDRulesCache();\n                        }\n                    }\n                }\n                return (_uniqueInstance);\n            }\n        }\n\n        public IDictionary<TLDRule.RuleType, IDictionary<string, TLDRule>> TLDRuleLists { get; set; }\n\n        public static void Reset()\n        {\n            lock (_syncObj)\n            {\n                _uniqueInstance = null;\n            }\n        }\n\n        private IDictionary<TLDRule.RuleType, IDictionary<string, TLDRule>> GetTLDRules()\n        {\n            var results = new Dictionary<TLDRule.RuleType, IDictionary<string, TLDRule>>();\n            var rules = Enum.GetValues(typeof(TLDRule.RuleType)).Cast<TLDRule.RuleType>();\n            foreach (var rule in rules)\n            {\n                results[rule] = new Dictionary<string, TLDRule>(StringComparer.CurrentCultureIgnoreCase);\n            }\n\n            var ruleStrings = ReadRulesData();\n\n            //  Strip out any lines that are:\n            //  a.) A comment\n            //  b.) Blank\n            var rulesStrings = ruleStrings\n                .Where(ruleString => !ruleString.StartsWith(\"//\") && ruleString.Trim().Length != 0);\n            foreach (var ruleString in rulesStrings)\n            {\n                var result = new TLDRule(ruleString);\n                results[result.Type][result.Name] = result;\n            }\n\n            //  Return our results:\n            Debug.WriteLine(string.Format(\"Loaded {0} rules into cache.\",\n                results.Values.Sum(r => r.Values.Count)));\n            return results;\n        }\n\n        private IEnumerable<string> ReadRulesData()\n        {\n            var assembly = typeof(TLDRulesCache).GetTypeInfo().Assembly;\n            var stream = assembly.GetManifestResourceStream(\"Bit.Icons.Resources.public_suffix_list.dat\");\n            string line;\n            using (var reader = new StreamReader(stream))\n            {\n                while ((line = reader.ReadLine()) != null)\n                {\n                    yield return line;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Icons/Models/Icon.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Icons.Models;\n\npublic class Icon\n{\n    public byte[] Image { get; set; }\n    public string Format { get; set; }\n}\n"
  },
  {
    "path": "src/Icons/Models/IconHttpRequest.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\nusing Bit.Icons.Extensions;\nusing Bit.Icons.Services;\n\nnamespace Bit.Icons.Models;\n\npublic class IconHttpRequest\n{\n    private const int _maxRedirects = 2;\n\n    private static readonly HttpStatusCode[] _redirectStatusCodes = new HttpStatusCode[] { HttpStatusCode.Redirect, HttpStatusCode.MovedPermanently, HttpStatusCode.RedirectKeepVerb, HttpStatusCode.SeeOther };\n\n    private readonly ILogger<IIconFetchingService> _logger;\n    private readonly HttpClient _httpClient;\n    private readonly IHttpClientFactory _httpClientFactory;\n    private readonly IUriService _uriService;\n    private readonly int _redirectsCount;\n    private readonly Uri _uri;\n    private static HttpResponseMessage NotFound => new(HttpStatusCode.NotFound);\n\n    private IconHttpRequest(Uri uri, ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IUriService uriService, int redirectsCount)\n    {\n        _logger = logger;\n        _httpClientFactory = httpClientFactory;\n        _httpClient = _httpClientFactory.CreateClient(\"Icons\");\n        _uriService = uriService;\n        _redirectsCount = redirectsCount;\n        _uri = uri;\n    }\n\n    public static async Task<IconHttpResponse> FetchAsync(Uri uri, ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IUriService uriService)\n    {\n        var pageIcons = new IconHttpRequest(uri, logger, httpClientFactory, uriService, 0);\n        var httpResponse = await pageIcons.FetchAsync();\n        return new IconHttpResponse(httpResponse, logger, httpClientFactory, uriService);\n    }\n\n    private async Task<HttpResponseMessage> FetchAsync()\n    {\n        if (!_uriService.TryGetUri(_uri, out var iconUri) || !iconUri!.IsValid)\n        {\n            return NotFound;\n        }\n\n        var response = await GetAsync(iconUri);\n\n        if (response.IsSuccessStatusCode)\n        {\n            return response;\n        }\n\n        using var responseForRedirect = response;\n        return await FollowRedirectsAsync(responseForRedirect, iconUri);\n    }\n\n\n    private async Task<HttpResponseMessage> GetAsync(IconUri iconUri)\n    {\n        using var message = new HttpRequestMessage();\n        message.RequestUri = iconUri.InnerUri;\n        message.Headers.Host = iconUri.Host;\n        message.Method = HttpMethod.Get;\n\n        try\n        {\n            return await _httpClient.SendAsync(message);\n        }\n        catch\n        {\n            return NotFound;\n        }\n    }\n\n    private async Task<HttpResponseMessage> FollowRedirectsAsync(HttpResponseMessage response, IconUri originalIconUri)\n    {\n        if (_redirectsCount >= _maxRedirects || response.Headers.Location == null ||\n            !_redirectStatusCodes.Contains(response.StatusCode))\n        {\n            return NotFound;\n        }\n\n        using var responseForRedirect = response;\n        var redirectUri = DetermineRedirectUri(responseForRedirect.Headers.Location, originalIconUri);\n\n        return await new IconHttpRequest(redirectUri, _logger, _httpClientFactory, _uriService, _redirectsCount + 1).FetchAsync();\n    }\n\n    private static Uri DetermineRedirectUri(Uri responseUri, IconUri originalIconUri)\n    {\n        if (responseUri.IsAbsoluteUri)\n        {\n            if (!responseUri.IsHypertext())\n            {\n                return responseUri.ChangeScheme(\"https\");\n            }\n            return responseUri;\n        }\n        else\n        {\n            return new UriBuilder\n            {\n                Scheme = originalIconUri.Scheme,\n                Host = originalIconUri.Host,\n                Path = responseUri.ToString()\n            }.Uri;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Icons/Models/IconHttpResponse.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\nusing AngleSharp.Html.Parser;\nusing Bit.Icons.Services;\n\nnamespace Bit.Icons.Models;\n\npublic class IconHttpResponse : IDisposable\n{\n    private const int _maxIconLinksProcessed = 200;\n    private const int _maxRetrievedIcons = 10;\n\n    private readonly HttpResponseMessage _response;\n    private readonly ILogger<IIconFetchingService> _logger;\n    private readonly IHttpClientFactory _httpClientFactory;\n    private readonly IUriService _uriService;\n\n    public HttpStatusCode StatusCode => _response.StatusCode;\n    public bool IsSuccessStatusCode => _response.IsSuccessStatusCode;\n    public string? ContentType => _response.Content.Headers.ContentType?.MediaType;\n    public HttpContent Content => _response.Content;\n\n    public IconHttpResponse(HttpResponseMessage response, ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IUriService uriService)\n    {\n        _response = response;\n        _logger = logger;\n        _httpClientFactory = httpClientFactory;\n        _uriService = uriService;\n    }\n\n    public async Task<IEnumerable<Icon>> RetrieveIconsAsync(Uri requestUri, string domain, IHtmlParser parser)\n    {\n        using var htmlStream = await _response.Content.ReadAsStreamAsync();\n        var head = await parser.ParseHeadAsync(htmlStream);\n\n        if (head == null)\n        {\n            _logger.LogWarning(\"No DocumentElement for {domain}.\", domain);\n            return Array.Empty<Icon>();\n        }\n\n        // Make sure uri uses domain name, not ip\n        var uri = _response.RequestMessage?.RequestUri;\n        if (uri == null || IPAddress.TryParse(_response.RequestMessage!.RequestUri!.Host, out var _))\n        {\n            uri = requestUri;\n        }\n\n        var baseUrl = head.QuerySelector(\"base[href]\")?.Attributes[\"href\"]?.Value;\n        if (string.IsNullOrWhiteSpace(baseUrl))\n        {\n            baseUrl = \"/\";\n        }\n\n        var links = head.QuerySelectorAll(\"link[href]\")\n            ?.Take(_maxIconLinksProcessed)\n            .Select(l => new IconLink(l, uri, baseUrl))\n            .Where(l => l.IsUsable())\n            .OrderBy(l => l.Priority)\n            .Take(_maxRetrievedIcons)\n            .ToArray() ?? Array.Empty<IconLink>();\n        var results = await Task.WhenAll(links.Select(l => l.FetchAsync(_logger, _httpClientFactory, _uriService)));\n        return results.Where(r => r != null).Select(r => r!);\n    }\n\n\n    public void Dispose()\n    {\n        _response.Dispose();\n    }\n}\n"
  },
  {
    "path": "src/Icons/Models/IconLink.cs",
    "content": "﻿#nullable enable\n\nusing System.Text;\nusing AngleSharp.Dom;\nusing Bit.Icons.Extensions;\nusing Bit.Icons.Services;\n\nnamespace Bit.Icons.Models;\n\npublic class IconLink\n{\n    private static readonly HashSet<string> _iconRels = new(StringComparer.InvariantCultureIgnoreCase) { \"icon\", \"apple-touch-icon\", \"shortcut icon\" };\n    private static readonly HashSet<string> _blocklistedRels = new(StringComparer.InvariantCultureIgnoreCase) { \"preload\", \"image_src\", \"preconnect\", \"canonical\", \"alternate\", \"stylesheet\" };\n    private static readonly HashSet<string> _iconExtensions = new(StringComparer.InvariantCultureIgnoreCase) { \".ico\", \".png\", \".jpg\", \".jpeg\" };\n    private const string _pngMediaType = \"image/png\";\n    private static readonly byte[] _pngHeader = new byte[] { 137, 80, 78, 71 };\n    private static readonly byte[] _webpHeader = Encoding.UTF8.GetBytes(\"RIFF\");\n\n    private const string _icoMediaType = \"image/x-icon\";\n    private const string _icoAltMediaType = \"image/vnd.microsoft.icon\";\n    private static readonly byte[] _icoHeader = new byte[] { 00, 00, 01, 00 };\n\n    private const string _jpegMediaType = \"image/jpeg\";\n    private static readonly byte[] _jpegHeader = new byte[] { 255, 216, 255 };\n\n    private const string _svgXmlMediaType = \"image/svg+xml\";\n\n    private static readonly HashSet<string> _allowedMediaTypes = new(StringComparer.InvariantCultureIgnoreCase)\n    {\n        _pngMediaType,\n        _icoMediaType,\n        _icoAltMediaType,\n        _jpegMediaType,\n        _svgXmlMediaType,\n    };\n\n    private bool _useUriDirectly = false;\n    private bool _validated = false;\n    private int? _width;\n    private int? _height;\n\n    public IAttr? Href { get; }\n    public IAttr? Rel { get; }\n    public IAttr? Type { get; }\n    public IAttr? Sizes { get; }\n    public Uri ParentUri { get; }\n    public string BaseUrlPath { get; }\n    public int Priority\n    {\n        get\n        {\n            if (_width == null || _width != _height)\n            {\n                return 200;\n            }\n\n            return _width switch\n            {\n                32 => 1,\n                64 => 2,\n                >= 24 and <= 128 => 3,\n                16 => 4,\n                _ => 100,\n            };\n        }\n    }\n\n    public IconLink(Uri parentPage)\n    {\n        _useUriDirectly = true;\n        _validated = true;\n        ParentUri = parentPage;\n        BaseUrlPath = parentPage.PathAndQuery;\n    }\n\n    public IconLink(IElement element, Uri parentPage, string baseUrlPath)\n    {\n        Href = element.Attributes[\"href\"];\n        ParentUri = parentPage;\n        BaseUrlPath = baseUrlPath;\n\n        Rel = element.Attributes[\"rel\"];\n        Type = element.Attributes[\"type\"];\n        Sizes = element.Attributes[\"sizes\"];\n\n        if (!string.IsNullOrWhiteSpace(Sizes?.Value))\n        {\n            var sizeParts = Sizes.Value.Split('x');\n            if (sizeParts.Length == 2 && int.TryParse(sizeParts[0].Trim(), out var width) &&\n                int.TryParse(sizeParts[1].Trim(), out var height))\n            {\n                _width = width;\n                _height = height;\n            }\n        }\n    }\n\n    public bool IsUsable()\n    {\n        if (string.IsNullOrWhiteSpace(Href?.Value))\n        {\n            return false;\n        }\n\n        if (Rel != null && _iconRels.Contains(Rel.Value))\n        {\n            _validated = true;\n        }\n        if (Rel == null || !_blocklistedRels.Contains(Rel.Value))\n        {\n            try\n            {\n                var extension = Path.GetExtension(Href.Value);\n                if (_iconExtensions.Contains(extension))\n                {\n                    _validated = true;\n                }\n            }\n            catch (ArgumentException) { }\n        }\n        return _validated;\n    }\n\n    /// <summary>\n    /// Fetches the icon from the Href. Will always fail unless first validated with IsUsable().\n    /// </summary>\n    public async Task<Icon?> FetchAsync(ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IUriService uriService)\n    {\n        if (!_validated)\n        {\n            return null;\n        }\n\n        var uri = BuildUri();\n        if (uri == null)\n        {\n            return null;\n        }\n\n        using var response = await IconHttpRequest.FetchAsync(uri, logger, httpClientFactory, uriService);\n        if (!response.IsSuccessStatusCode)\n        {\n            return null;\n        }\n\n        var format = response.Content.Headers.ContentType?.MediaType;\n        var bytes = await response.Content.ReadAsByteArrayAsync();\n        response.Content.Dispose();\n        if (format == null || !_allowedMediaTypes.Contains(format))\n        {\n            format = DetermineImageFormatFromFile(bytes);\n        }\n\n        if (format == null || !_allowedMediaTypes.Contains(format))\n        {\n            return null;\n        }\n\n        return new Icon { Image = bytes, Format = format };\n    }\n\n    private Uri? BuildUri()\n    {\n        if (_useUriDirectly)\n        {\n            return ParentUri;\n        }\n\n        if (Href == null)\n        {\n            return null;\n        }\n\n        if (Href.Value.StartsWith(\"//\") && Uri.TryCreate($\"{ParentUri.Scheme}://{Href.Value[2..]}\", UriKind.Absolute, out var uri))\n        {\n            return uri;\n        }\n\n        if (Uri.TryCreate(Href.Value, UriKind.Relative, out uri))\n        {\n            return new UriBuilder()\n            {\n                Scheme = ParentUri.Scheme,\n                Host = ParentUri.Host,\n            }.Uri.ConcatPath(BaseUrlPath, uri.OriginalString);\n        }\n\n        if (Uri.TryCreate(Href.Value, UriKind.Absolute, out uri))\n        {\n            return uri;\n        }\n\n        return null;\n    }\n\n    private static bool HeaderMatch(byte[] imageBytes, byte[] header)\n    {\n        return imageBytes.Length >= header.Length && header.SequenceEqual(imageBytes.Take(header.Length));\n    }\n\n    private static string DetermineImageFormatFromFile(byte[] imageBytes)\n    {\n        if (HeaderMatch(imageBytes, _icoHeader))\n        {\n            return _icoMediaType;\n        }\n        else if (HeaderMatch(imageBytes, _pngHeader) || HeaderMatch(imageBytes, _webpHeader))\n        {\n            return _pngMediaType;\n        }\n        else if (HeaderMatch(imageBytes, _jpegHeader))\n        {\n            return _jpegMediaType;\n        }\n        else\n        {\n            return string.Empty;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Icons/Models/IconUri.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\nusing Bit.Icons.Extensions;\n\nnamespace Bit.Icons.Models;\n\npublic class IconUri\n{\n    private readonly IPAddress _ip;\n    public string Host { get; }\n    public Uri InnerUri { get; }\n    public string Scheme => InnerUri.Scheme;\n\n    public bool IsValid\n    {\n        get\n        {\n            // Prevent direct access to any ip\n            if (IPAddress.TryParse(Host, out _))\n            {\n                return false;\n            }\n\n            // Prevent non-http(s) and non-default ports\n            if ((InnerUri.Scheme != \"http\" && InnerUri.Scheme != \"https\") || !InnerUri.IsDefaultPort)\n            {\n                return false;\n            }\n\n            // Prevent local hosts (localhost, bobs-pc, etc) and IP addresses\n            if (!Host.Contains('.') || _ip.IsInternal())\n            {\n                return false;\n            }\n\n            return true;\n        }\n    }\n\n    /// <summary>\n    /// Represents an ip-validated Uri for use in grabbing an icon.\n    /// </summary>\n    /// <param name=\"uriString\"></param>\n    /// <param name=\"ip\"></param>\n    public IconUri(Uri uri, IPAddress ip)\n    {\n        _ip = ip;\n        InnerUri = uri.ChangeHost(_ip.ToString());\n        Host = uri.Host;\n    }\n}\n"
  },
  {
    "path": "src/Icons/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Icons;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        Host\n            .CreateDefaultBuilder(args)\n            .UseBitwardenSdk()\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging()\n            .Build()\n            .Run();\n    }\n}\n"
  },
  {
    "path": "src/Icons/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:50024/\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"launchUrl\": \"bitwarden.com/icon.png\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Icons\": {\n      \"commandName\": \"Project\",\n      \"launchUrl\": \"bitwarden.com/icon.png\",\n      \"applicationUrl\": \"http://localhost:50024/\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Icons/Services/ChangePasswordUriService.cs",
    "content": "﻿namespace Bit.Icons.Services;\n\npublic class ChangePasswordUriService : IChangePasswordUriService\n{\n    private readonly HttpClient _httpClient;\n\n    public ChangePasswordUriService(IHttpClientFactory httpClientFactory)\n    {\n        _httpClient = httpClientFactory.CreateClient(\"ChangePasswordUri\");\n    }\n\n    /// <summary>\n    /// Fetches the well-known change password URL for the given domain.\n    /// </summary>\n    /// <param name=\"domain\"></param>\n    /// <returns></returns>\n    public async Task<string?> GetChangePasswordUri(string domain)\n    {\n        if (string.IsNullOrWhiteSpace(domain))\n        {\n            return null;\n        }\n\n        var hasReliableStatusCode = await HasReliableHttpStatusCode(domain);\n        var wellKnownChangePasswordUrl = await GetWellKnownChangePasswordUrl(domain);\n\n\n        if (hasReliableStatusCode && wellKnownChangePasswordUrl != null)\n        {\n            return wellKnownChangePasswordUrl;\n        }\n\n        // Reliable well-known URL criteria not met, return null\n        return null;\n    }\n\n    /// <summary>\n    /// Checks if the server returns a non-200 status code for a resource that should not exist.\n    //  See https://w3c.github.io/webappsec-change-password-url/response-code-reliability.html#semantics\n    /// </summary>\n    /// <param name=\"urlDomain\">The domain of the URL to check</param>\n    /// <returns>True when the domain responds with a non-ok response</returns>\n    private async Task<bool> HasReliableHttpStatusCode(string urlDomain)\n    {\n        try\n        {\n            var url = new UriBuilder(urlDomain)\n            {\n                Path = \"/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200\"\n            };\n\n            var request = new HttpRequestMessage(HttpMethod.Get, url.ToString());\n\n            var response = await _httpClient.SendAsync(request);\n            return !response.IsSuccessStatusCode;\n        }\n        catch\n        {\n            return false;\n        }\n    }\n\n    /// <summary>\n    /// Builds a well-known change password URL for the given origin. Attempts to fetch the URL to ensure a valid response\n    /// is returned. Returns null if the request throws or the response is not 200 OK.\n    /// See https://w3c.github.io/webappsec-change-password-url/\n    /// </summary>\n    /// <param name=\"urlDomain\">The domain of the URL to check</param>\n    /// <returns>The well-known change password URL if valid, otherwise null</returns>\n    private async Task<string?> GetWellKnownChangePasswordUrl(string urlDomain)\n    {\n        try\n        {\n            var url = new UriBuilder(urlDomain)\n            {\n                Path = \"/.well-known/change-password\"\n            };\n\n            var request = new HttpRequestMessage(HttpMethod.Get, url.ToString());\n\n            var response = await _httpClient.SendAsync(request);\n            return response.IsSuccessStatusCode ? url.ToString() : null;\n        }\n        catch\n        {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Icons/Services/DomainMappingService.cs",
    "content": "﻿namespace Bit.Icons.Services;\n\npublic class DomainMappingService : IDomainMappingService\n{\n    private readonly Dictionary<string, string> _map = new Dictionary<string, string>\n    {\n        [\"login.yahoo.com\"] = \"yahoo.com\",\n        [\"accounts.google.com\"] = \"google.com\",\n        [\"photo.walgreens.com\"] = \"walgreens.com\",\n        [\"passport.yandex.com\"] = \"yandex.com\",\n        // TODO: Add others here\n    };\n\n    public string MapDomain(string hostname)\n    {\n        if (_map.TryGetValue(hostname, out var mappedDomain))\n        {\n            return mappedDomain;\n        }\n\n        return hostname;\n    }\n}\n"
  },
  {
    "path": "src/Icons/Services/IChangePasswordUriService.cs",
    "content": "﻿namespace Bit.Icons.Services;\n\npublic interface IChangePasswordUriService\n{\n    Task<string?> GetChangePasswordUri(string domain);\n}\n"
  },
  {
    "path": "src/Icons/Services/IDomainMappingService.cs",
    "content": "﻿namespace Bit.Icons.Services;\n\npublic interface IDomainMappingService\n{\n    string MapDomain(string hostname);\n}\n"
  },
  {
    "path": "src/Icons/Services/IIconFetchingService.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Icons.Models;\n\nnamespace Bit.Icons.Services;\n\npublic interface IIconFetchingService\n{\n    Task<Icon?> GetIconAsync(string domain);\n}\n"
  },
  {
    "path": "src/Icons/Services/IUriService.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Icons.Models;\n\nnamespace Bit.Icons.Services;\n\npublic interface IUriService\n{\n    bool TryGetUri(string stringUri, out IconUri? iconUri);\n    bool TryGetUri(Uri uri, out IconUri? iconUri);\n    bool TryGetRedirect(HttpResponseMessage response, IconUri originalUri, out IconUri? iconUri);\n}\n"
  },
  {
    "path": "src/Icons/Services/IconFetchingService.cs",
    "content": "﻿#nullable enable\n\nusing AngleSharp.Html.Parser;\nusing Bit.Icons.Extensions;\nusing Bit.Icons.Models;\n\nnamespace Bit.Icons.Services;\n\npublic class IconFetchingService : IIconFetchingService\n{\n    private readonly IHttpClientFactory _httpClientFactory;\n    private readonly ILogger<IIconFetchingService> _logger;\n    private readonly IHtmlParser _parser;\n    private readonly IUriService _uriService;\n\n    public IconFetchingService(ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IHtmlParser parser, IUriService uriService)\n    {\n        _logger = logger;\n        _httpClientFactory = httpClientFactory;\n        _parser = parser;\n        _uriService = uriService;\n    }\n\n    public async Task<Icon?> GetIconAsync(string domain)\n    {\n        var domainIcons = await DomainIcons.FetchAsync(domain, _logger, _httpClientFactory, _parser, _uriService);\n        var result = domainIcons.Where(result => result != null).FirstOrDefault();\n        return result ?? await GetFaviconAsync(domain);\n    }\n\n    private async Task<Icon?> GetFaviconAsync(string domain)\n    {\n        // Fall back to favicon\n        var faviconUriBuilder = new UriBuilder\n        {\n            Scheme = \"https\",\n            Host = domain,\n            Path = \"/favicon.ico\"\n        };\n\n        if (faviconUriBuilder.TryBuild(out var faviconUri))\n        {\n            return await new IconLink(faviconUri!).FetchAsync(_logger, _httpClientFactory, _uriService);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Icons/Services/UriService.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\nusing System.Net.Sockets;\nusing Bit.Icons.Extensions;\nusing Bit.Icons.Models;\n\nnamespace Bit.Icons.Services;\n\npublic class UriService : IUriService\n{\n    public IconUri GetUri(string inputUri)\n    {\n        var uri = new Uri(inputUri);\n        return new IconUri(uri, DetermineIp(uri));\n    }\n\n    public bool TryGetUri(string stringUri, out IconUri? iconUri)\n    {\n        if (!Uri.TryCreate(stringUri, UriKind.Absolute, out var uri))\n        {\n            iconUri = null;\n            return false;\n        }\n\n        return TryGetUri(uri, out iconUri);\n    }\n\n    public IconUri GetUri(Uri uri)\n    {\n        return new IconUri(uri, DetermineIp(uri));\n    }\n\n    public bool TryGetUri(Uri uri, out IconUri? iconUri)\n    {\n        try\n        {\n            iconUri = GetUri(uri);\n            return true;\n        }\n        catch (Exception)\n        {\n            iconUri = null;\n            return false;\n        }\n    }\n\n    public IconUri GetRedirect(HttpResponseMessage response, IconUri originalUri)\n    {\n        if (response.Headers.Location == null)\n        {\n            throw new Exception(\"No redirect location found.\");\n        }\n\n        var redirectUri = DetermineRedirectUri(response.Headers.Location, originalUri);\n        return new IconUri(redirectUri, DetermineIp(redirectUri));\n    }\n\n    public bool TryGetRedirect(HttpResponseMessage response, IconUri originalUri, out IconUri? iconUri)\n    {\n        try\n        {\n            iconUri = GetRedirect(response, originalUri);\n            return true;\n        }\n        catch (Exception)\n        {\n            iconUri = null;\n            return false;\n        }\n    }\n\n    private static Uri DetermineRedirectUri(Uri responseUri, IconUri originalIconUri)\n    {\n        if (responseUri.IsAbsoluteUri)\n        {\n            if (!responseUri.IsHypertext())\n            {\n                return responseUri.ChangeScheme(\"https\");\n            }\n            return responseUri;\n        }\n        else\n        {\n            return new UriBuilder\n            {\n                Scheme = originalIconUri.Scheme,\n                Host = originalIconUri.Host,\n                Path = responseUri.ToString()\n            }.Uri;\n        }\n    }\n\n    private static IPAddress DetermineIp(Uri uri)\n    {\n        if (IPAddress.TryParse(uri.Host, out var ip))\n        {\n            return ip;\n        }\n\n        var hostEntry = Dns.GetHostEntry(uri.Host);\n        ip = hostEntry.AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork || ip.IsIPv4MappedToIPv6)?.MapToIPv4();\n        if (ip == null)\n        {\n            throw new Exception($\"Unable to determine IP for {uri.Host}\");\n        }\n        return ip;\n    }\n}\n"
  },
  {
    "path": "src/Icons/Startup.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Icons.Extensions;\nusing Bit.Icons.Models;\nusing Bit.SharedWeb.Utilities;\nusing Microsoft.Net.Http.Headers;\n\nnamespace Bit.Icons;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(\"en-US\");\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; }\n    public IWebHostEnvironment Environment { get; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n        var iconsSettings = new IconsSettings();\n        var changePasswordUriSettings = new ChangePasswordUriSettings();\n        ConfigurationBinder.Bind(Configuration.GetSection(\"IconsSettings\"), iconsSettings);\n        ConfigurationBinder.Bind(Configuration.GetSection(\"ChangePasswordUriSettings\"), changePasswordUriSettings);\n        services.AddSingleton(s => iconsSettings);\n        services.AddSingleton(s => changePasswordUriSettings);\n\n        // Http client\n        services.ConfigureHttpClients();\n\n        // Add HtmlParser\n        services.AddHtmlParsing();\n\n        // Cache\n        services.AddMemoryCache(options =>\n        {\n            options.SizeLimit = iconsSettings.CacheSizeLimit;\n        });\n        services.AddMemoryCache(options =>\n        {\n            options.SizeLimit = changePasswordUriSettings.CacheSizeLimit;\n        });\n\n        // Services\n        services.AddServices();\n\n        // Mvc\n        services.AddMvc();\n    }\n\n    public void Configure(\n        IApplicationBuilder app,\n        IWebHostEnvironment env,\n        GlobalSettings globalSettings)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n\n        // Forwarding Headers\n        if (globalSettings.SelfHosted)\n        {\n            app.UseForwardedHeaders(globalSettings);\n        }\n\n        if (env.IsDevelopment())\n        {\n            app.UseDeveloperExceptionPage();\n        }\n\n        app.Use(async (context, next) =>\n        {\n            context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue\n            {\n                Public = true,\n                MaxAge = TimeSpan.FromDays(7)\n            };\n\n            context.Response.Headers.Append(\"Content-Security-Policy\", \"default-src 'self'; script-src 'none'\");\n\n            await next();\n        });\n\n        app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))\n            .AllowAnyMethod().AllowAnyHeader().AllowCredentials());\n\n        app.UseRouting();\n        app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());\n    }\n}\n"
  },
  {
    "path": "src/Icons/Util/IPAddressExtension.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\n\nnamespace Bit.Icons.Extensions;\n\npublic static class IPAddressExtension\n{\n    public static bool IsInternal(this IPAddress ip)\n    {\n        if (IPAddress.IsLoopback(ip))\n        {\n            return true;\n        }\n\n        var ipString = ip.ToString();\n        if (ipString == \"::1\" || ipString == \"::\" || ipString.StartsWith(\"::ffff:\"))\n        {\n            return true;\n        }\n\n        // IPv6\n        if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6)\n        {\n            return ipString.StartsWith(\"fc\") || ipString.StartsWith(\"fd\") ||\n                ipString.StartsWith(\"fe\") || ipString.StartsWith(\"ff\");\n        }\n\n        // IPv4\n        var bytes = ip.GetAddressBytes();\n        return (bytes[0]) switch\n        {\n            0 => true,\n            10 => true,\n            127 => true,\n            169 => bytes[1] == 254, // Cloud environments, such as AWS\n            172 => bytes[1] < 32 && bytes[1] >= 16,\n            192 => bytes[1] == 168,\n            _ => false,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Icons/Util/ServiceCollectionExtension.cs",
    "content": "﻿# nullable enable\n\nusing System.Net;\nusing AngleSharp.Html.Parser;\nusing Bit.Icons.Services;\n\nnamespace Bit.Icons.Extensions;\n\npublic static class ServiceCollectionExtension\n{\n    public static void ConfigureHttpClients(this IServiceCollection services)\n    {\n        services.AddHttpClient(\"Icons\", client =>\n        {\n            client.Timeout = TimeSpan.FromSeconds(20);\n            client.MaxResponseContentBufferSize = 5000000; // 5 MB\n                                                           // Let's add some headers to look like we're coming from a web browser request. Some websites\n                                                           // will block our request without these.\n            client.DefaultRequestHeaders.Add(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \" +\n                \"(KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\");\n            client.DefaultRequestHeaders.Add(\"Accept-Language\", \"en-US,en;q=0.8\");\n            client.DefaultRequestHeaders.Add(\"Cache-Control\", \"no-cache\");\n            client.DefaultRequestHeaders.Add(\"Pragma\", \"no-cache\");\n            client.DefaultRequestHeaders.Add(\"Accept\", \"text/html,application/xhtml+xml,application/xml;\" +\n                \"q=0.9,image/webp,image/apng,*/*;q=0.8\");\n        }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler\n        {\n            AllowAutoRedirect = false,\n            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,\n        });\n\n        // The CreatePasswordUri handler wants similar headers as Icons to portray coming from a browser but\n        // needs to follow redirects to get the final URL.\n        services.AddHttpClient(\"ChangePasswordUri\", client =>\n        {\n            client.Timeout = TimeSpan.FromSeconds(20);\n            client.MaxResponseContentBufferSize = 5000000; // 5 MB\n                                                           // Let's add some headers to look like we're coming from a web browser request. Some websites\n                                                           // will block our request without these.\n            client.DefaultRequestHeaders.Add(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \" +\n                \"(KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36\");\n            client.DefaultRequestHeaders.Add(\"Accept-Language\", \"en-US,en;q=0.8\");\n            client.DefaultRequestHeaders.Add(\"Cache-Control\", \"no-cache\");\n            client.DefaultRequestHeaders.Add(\"Pragma\", \"no-cache\");\n        }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler\n        {\n            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,\n        });\n    }\n\n    public static void AddHtmlParsing(this IServiceCollection services)\n    {\n        services.AddSingleton<IHtmlParser, HtmlParser>();\n    }\n\n    public static void AddServices(this IServiceCollection services)\n    {\n        services.AddSingleton<IUriService, UriService>();\n        services.AddSingleton<IDomainMappingService, DomainMappingService>();\n        services.AddSingleton<IIconFetchingService, IconFetchingService>();\n        services.AddSingleton<IChangePasswordUriService, ChangePasswordUriService>();\n    }\n}\n"
  },
  {
    "path": "src/Icons/Util/UriBuilderExtension.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Icons.Extensions;\n\npublic static class UriBuilderExtension\n{\n    public static bool TryBuild(this UriBuilder builder, out Uri? uri)\n    {\n        try\n        {\n            uri = builder.Uri;\n            return true;\n        }\n        catch (UriFormatException)\n        {\n            uri = null;\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Icons/Util/UriExtension.cs",
    "content": "﻿\n#nullable enable\n\nnamespace Bit.Icons.Extensions;\n\npublic static class UriExtension\n{\n    public static bool IsHypertext(this Uri uri)\n    {\n        return uri.Scheme == \"http\" || uri.Scheme == \"https\";\n    }\n\n    public static Uri ChangeScheme(this Uri uri, string scheme)\n    {\n        return new UriBuilder(scheme, uri.Host) { Path = uri.PathAndQuery }.Uri;\n    }\n\n    public static Uri ChangeHost(this Uri uri, string host)\n    {\n        return new UriBuilder(uri) { Host = host }.Uri;\n    }\n\n    public static Uri ConcatPath(this Uri uri, params string[] paths)\n        => uri.ConcatPath(paths.AsEnumerable());\n    public static Uri ConcatPath(this Uri uri, IEnumerable<string> paths)\n    {\n        if (!paths.Any())\n        {\n            return uri;\n        }\n\n        if (Uri.TryCreate(uri, paths.First(), out var newUri))\n        {\n            return newUri.ConcatPath(paths.Skip(1));\n        }\n        else\n        {\n            return uri;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Icons/appsettings.Development.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\",\n      \"internalScim\": \"http://localhost:44559\"\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Icons/appsettings.Production.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.bitwarden.com\",\n      \"api\": \"https://api.bitwarden.com\",\n      \"identity\": \"https://identity.bitwarden.com\",\n      \"admin\": \"https://admin.bitwarden.com\",\n      \"notifications\": \"https://notifications.bitwarden.com\",\n      \"sso\": \"https://sso.bitwarden.com\",\n      \"internalNotifications\": \"https://notifications.bitwarden.com\",\n      \"internalAdmin\": \"https://admin.bitwarden.com\",\n      \"internalIdentity\": \"https://identity.bitwarden.com\",\n      \"internalApi\": \"https://api.bitwarden.com\",\n      \"internalVault\": \"https://vault.bitwarden.com\",\n      \"internalSso\": \"https://sso.bitwarden.com\",\n      \"internalScim\": \"https://scim.bitwarden.com\"\n    }\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Warning\",\n          \"System\": \"Warning\",\n          \"Microsoft\": \"Warning\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Icons/appsettings.QA.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.qa.bitwarden.pw\",\n      \"api\": \"https://api.qa.bitwarden.pw\",\n      \"identity\": \"https://identity.qa.bitwarden.pw\",\n      \"admin\": \"https://admin.qa.bitwarden.pw\",\n      \"notifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"sso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalNotifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"internalAdmin\": \"https://admin.qa.bitwarden.pw\",\n      \"internalIdentity\": \"https://identity.qa.bitwarden.pw\",\n      \"internalApi\": \"https://api.qa.bitwarden.pw\",\n      \"internalVault\": \"https://vault.qa.bitwarden.pw\",\n      \"internalSso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalScim\": \"https://scim.qa.bitwarden.pw\"\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Debug\",\n          \"System\": \"Debug\",\n          \"Microsoft\": \"Debug\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Icons/appsettings.SelfHosted.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": null,\n      \"api\": null,\n      \"identity\": null,\n      \"admin\": null,\n      \"notifications\": null,\n      \"sso\": null,\n      \"internalNotifications\": null,\n      \"internalAdmin\": null,\n      \"internalIdentity\": null,\n      \"internalApi\": null,\n      \"internalVault\": null,\n      \"internalSso\": null,\n      \"internalScim\": null\n    }\n  }\n}\n"
  },
  {
    "path": "src/Icons/appsettings.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"projectName\": \"Icons\"\n  },\n  \"iconsSettings\": {\n    \"cacheEnabled\": true,\n    \"cacheHours\": 24,\n    \"cacheSizeLimit\": null\n  },\n  \"changePasswordUriSettings\": {\n    \"cacheEnabled\": true,\n    \"cacheHours\": 24,\n    \"cacheSizeLimit\": null\n  }\n}\n"
  },
  {
    "path": "src/Icons/build.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\necho -e \"\\n## Building Icons\"\n\necho -e \"\\nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore \"$DIR/Icons.csproj\"\necho \"Clean\"\ndotnet clean \"$DIR/Icons.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\necho \"Publish\"\ndotnet publish \"$DIR/Icons.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\n"
  },
  {
    "path": "src/Icons/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/core\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    if [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n      chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos\n    fi\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nif [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n    cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf\n    $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab\nfi\n\nexec $gosu_cmd /app/Icons\n"
  },
  {
    "path": "src/Identity/Billing/Controller/AccountsController.cs",
    "content": "﻿using Bit.Core.Billing.Models.Api.Requests.Accounts;\nusing Bit.Core.Billing.TrialInitiation.Registration;\nusing Bit.Core.Utilities;\nusing Bit.SharedWeb.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Identity.Billing.Controller;\n\n[Route(\"accounts\")]\n[ExceptionHandlerFilter]\npublic class AccountsController(\n    ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand) : Microsoft.AspNetCore.Mvc.Controller\n{\n    [HttpPost(\"trial/send-verification-email\")]\n    [SelfHosted(NotSelfHostedOnly = true)]\n    public async Task<IActionResult> PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model)\n    {\n        var trialLength = model.TrialLength ?? 7;\n\n        var token = await sendTrialInitiationEmailForRegistrationCommand.Handle(\n            model.Email,\n            model.Name,\n            model.ReceiveMarketingEmails,\n            model.ProductTier,\n            model.Products,\n            trialLength);\n\n        if (token != null)\n        {\n            return Ok(token);\n        }\n\n        return NoContent();\n    }\n}\n"
  },
  {
    "path": "src/Identity/Controllers/AccountsController.cs",
    "content": "﻿using System.Text;\nusing Bit.Core;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Auth.Models.Api.Response.Accounts;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.UserFeatures.Registration;\nusing Bit.Core.Auth.UserFeatures.WebAuthnLogin;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Bit.Identity.Models.Request.Accounts;\nusing Bit.Identity.Models.Response.Accounts;\nusing Bit.SharedWeb.Utilities;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Identity.Controllers;\n\n[Route(\"accounts\")]\n[ExceptionHandlerFilter]\npublic class AccountsController : Controller\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly ILogger<AccountsController> _logger;\n    private readonly IUserRepository _userRepository;\n    private readonly IRegisterUserCommand _registerUserCommand;\n    private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;\n    private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;\n    private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;\n    private readonly IFeatureService _featureService;\n    private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;\n\n    private readonly byte[]? _defaultKdfHmacKey = null;\n    private static readonly List<UserKdfInformation> _defaultKdfResults =\n    [\n        // The first result (index 0) should always return the \"normal\" default.\n        new()\n        {\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,\n        },\n        // We want more weight for this default, so add it again\n        new()\n        {\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,\n        },\n        // Add some other possible defaults...\n        new()\n        {\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = 100_000,\n        },\n        new()\n        {\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = 5_000,\n        },\n        new()\n        {\n            Kdf = KdfType.Argon2id,\n            KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,\n            KdfMemory = AuthConstants.ARGON2_MEMORY.Default,\n            KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,\n        }\n    ];\n\n    public AccountsController(\n        ICurrentContext currentContext,\n        ILogger<AccountsController> logger,\n        IUserRepository userRepository,\n        IRegisterUserCommand registerUserCommand,\n        IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,\n        IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,\n        ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,\n        IFeatureService featureService,\n        IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,\n        GlobalSettings globalSettings\n        )\n    {\n        _currentContext = currentContext;\n        _logger = logger;\n        _userRepository = userRepository;\n        _registerUserCommand = registerUserCommand;\n        _assertionOptionsDataProtector = assertionOptionsDataProtector;\n        _getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;\n        _sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand;\n        _featureService = featureService;\n        _registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory;\n\n        if (CoreHelpers.SettingHasValue(globalSettings.KdfDefaultHashKey))\n        {\n            _defaultKdfHmacKey = Encoding.UTF8.GetBytes(globalSettings.KdfDefaultHashKey);\n        }\n    }\n\n    [HttpPost(\"register/send-verification-email\")]\n    public async Task<IActionResult> PostRegisterSendVerificationEmail([FromBody] RegisterSendVerificationEmailRequestModel model)\n    {\n        // Only pass fromMarketing if the feature flag is enabled\n        var isMarketingFeatureEnabled = _featureService.IsEnabled(FeatureFlagKeys.MarketingInitiatedPremiumFlow);\n        var fromMarketing = isMarketingFeatureEnabled ? model.FromMarketing : null;\n\n        var token = await _sendVerificationEmailForRegistrationCommand.Run(model.Email, model.Name,\n            model.ReceiveMarketingEmails, fromMarketing);\n\n        if (token != null)\n        {\n            return Ok(token);\n        }\n\n        return NoContent();\n    }\n\n    [HttpPost(\"register/verification-email-clicked\")]\n    public async Task<IActionResult> PostRegisterVerificationEmailClicked([FromBody] RegisterVerificationEmailClickedRequestModel model)\n    {\n        var tokenValid = RegistrationEmailVerificationTokenable.ValidateToken(_registrationEmailVerificationTokenDataFactory, model.EmailVerificationToken, model.Email);\n\n        // Check to see if the user already exists - this is just to catch the unlikely but possible case\n        // where a user finishes registration and then clicks the email verification link again.\n        var user = await _userRepository.GetByEmailAsync(model.Email);\n        var userExists = user != null;\n\n        if (!tokenValid || userExists)\n        {\n            throw new BadRequestException(\"Expired link. Please restart registration or try logging in. You may already have an account\");\n        }\n\n        return Ok();\n    }\n\n    [HttpPost(\"register/finish\")]\n    public async Task<RegisterFinishResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)\n    {\n        User user = model.ToUser();\n\n        // Users will either have an emailed token or an email verification token - not both.\n        IdentityResult? identityResult = null;\n\n        // PM-28143 - Just use the MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash\n        string masterPasswordAuthenticationHash = model.MasterPasswordAuthentication?.MasterPasswordAuthenticationHash\n                                                  ?? model.MasterPasswordHash!;\n\n        switch (model.GetTokenType())\n        {\n            case RegisterFinishTokenType.EmailVerification:\n                identityResult = await _registerUserCommand.RegisterUserViaEmailVerificationToken(\n                    user,\n                    masterPasswordAuthenticationHash,\n                    model.EmailVerificationToken!);\n                return ProcessRegistrationResult(identityResult, user);\n\n            case RegisterFinishTokenType.OrganizationInvite:\n                identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(\n                    user,\n                    masterPasswordAuthenticationHash,\n                    model.OrgInviteToken!,\n                    model.OrganizationUserId);\n                return ProcessRegistrationResult(identityResult, user);\n\n            case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:\n                identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(\n                    user,\n                    masterPasswordAuthenticationHash,\n                    model.OrgSponsoredFreeFamilyPlanToken!);\n                return ProcessRegistrationResult(identityResult, user);\n\n            case RegisterFinishTokenType.EmergencyAccessInvite:\n                identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(\n                    user,\n                    masterPasswordAuthenticationHash,\n                    model.AcceptEmergencyAccessInviteToken!,\n                    (Guid)model.AcceptEmergencyAccessId!);\n                return ProcessRegistrationResult(identityResult, user);\n\n            case RegisterFinishTokenType.ProviderInvite:\n                identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(\n                    user,\n                    masterPasswordAuthenticationHash,\n                    model.ProviderInviteToken!,\n                    (Guid)model.ProviderUserId!);\n                return ProcessRegistrationResult(identityResult, user);\n\n            default:\n                throw new BadRequestException(\"Invalid registration finish request\");\n        }\n    }\n\n    private RegisterFinishResponseModel ProcessRegistrationResult(IdentityResult result, User user)\n    {\n        if (result.Succeeded)\n        {\n            return new RegisterFinishResponseModel();\n        }\n\n        foreach (var error in result.Errors.Where(e => e.Code != \"DuplicateUserName\"))\n        {\n            ModelState.AddModelError(string.Empty, error.Description);\n        }\n\n        throw new BadRequestException(ModelState);\n    }\n\n    [HttpPost(\"prelogin\")]\n    [Obsolete(\"Migrating to use a more descriptive endpoint that would support different types of prelogins. \" +\n              \"Use prelogin/password instead. This endpoint has no EOL at the time of writing.\")]\n    public async Task<PasswordPreloginResponseModel> PostPrelogin([FromBody] PasswordPreloginRequestModel model)\n    {\n        // Same as PostPasswordPrelogin to maintain compatibility. Do not make changes in this function body,\n        // only make changes in MakePasswordPreloginCall\n        return await MakePasswordPreloginCall(model);\n    }\n\n    // There are two functions done this way because the open api docs that get generated in our build pipeline\n    // cannot handle two of the same post attributes on the same function call. That is why there is a\n    // PostPrelogin and the more appropriate PostPasswordPrelogin.\n    [HttpPost(\"prelogin/password\")]\n    public async Task<PasswordPreloginResponseModel> PostPasswordPrelogin([FromBody] PasswordPreloginRequestModel model)\n    {\n        // Same as PostPrelogin to maintain backwards compatibility. Do not make changes in this function body,\n        // only make changes in MakePasswordPreloginCall\n        return await MakePasswordPreloginCall(model);\n    }\n\n    private async Task<PasswordPreloginResponseModel> MakePasswordPreloginCall(PasswordPreloginRequestModel model)\n    {\n        var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);\n        if (kdfInformation == null)\n        {\n            kdfInformation = GetDefaultKdf(model.Email);\n        }\n        return new PasswordPreloginResponseModel(kdfInformation, model.Email);\n    }\n\n    [HttpGet(\"webauthn/assertion-options\")]\n    public WebAuthnLoginAssertionOptionsResponseModel GetWebAuthnLoginAssertionOptions()\n    {\n        var options = _getWebAuthnLoginCredentialAssertionOptionsCommand.GetWebAuthnLoginCredentialAssertionOptions();\n\n        var tokenable = new WebAuthnLoginAssertionOptionsTokenable(WebAuthnLoginAssertionOptionsScope.Authentication, options);\n        var token = _assertionOptionsDataProtector.Protect(tokenable);\n\n        return new WebAuthnLoginAssertionOptionsResponseModel\n        {\n            Options = options,\n            Token = token\n        };\n    }\n\n    private UserKdfInformation GetDefaultKdf(string email)\n    {\n        if (_defaultKdfHmacKey == null)\n        {\n            return _defaultKdfResults[0];\n        }\n\n        // Compute the HMAC hash of the email\n        var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant());\n        using var hmac = new System.Security.Cryptography.HMACSHA256(_defaultKdfHmacKey);\n        var hmacHash = hmac.ComputeHash(hmacMessage);\n        // Convert the hash to a number\n        var hashHex = BitConverter.ToString(hmacHash).Replace(\"-\", string.Empty).ToLowerInvariant();\n        var hashFirst8Bytes = hashHex.Substring(0, 16);\n        var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);\n        // Find the default KDF value for this hash number\n        var hashIndex = (int)(Math.Abs(hashNumber) % _defaultKdfResults.Count);\n        return _defaultKdfResults[hashIndex];\n    }\n}\n"
  },
  {
    "path": "src/Identity/Controllers/InfoController.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Identity.Controllers;\n\npublic class InfoController : Controller\n{\n    [HttpGet(\"~/alive\")]\n    public DateTime GetAlive()\n    {\n        return DateTime.UtcNow;\n    }\n\n    [HttpGet(\"~/now\")]\n    [Obsolete(\"This endpoint is deprecated. Use GET /alive instead.\")]\n    public DateTime GetNow()\n    {\n        return GetAlive();\n    }\n\n    [HttpGet(\"~/version\")]\n    public JsonResult GetVersion()\n    {\n        return Json(AssemblyHelpers.GetVersion());\n    }\n}\n"
  },
  {
    "path": "src/Identity/Controllers/SsoController.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Repositories;\nusing Bit.Identity.Models;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer;\nusing Duende.IdentityServer.Services;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Localization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Identity.Controllers;\n\n[Route(\"sso/[action]\")]\npublic class SsoController : Controller\n{\n    private readonly IIdentityServerInteractionService _interaction;\n    private readonly ILogger<SsoController> _logger;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly IUserRepository _userRepository;\n    private readonly IHttpClientFactory _clientFactory;\n\n    public SsoController(\n        IIdentityServerInteractionService interaction,\n        ILogger<SsoController> logger,\n        ISsoConfigRepository ssoConfigRepository,\n        IUserRepository userRepository,\n        IHttpClientFactory clientFactory)\n    {\n        _interaction = interaction;\n        _logger = logger;\n        _ssoConfigRepository = ssoConfigRepository;\n        _userRepository = userRepository;\n        _clientFactory = clientFactory;\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> PreValidate(string domainHint)\n    {\n        if (string.IsNullOrWhiteSpace(domainHint))\n        {\n            Response.StatusCode = 400;\n            return Json(new ErrorResponseModel(\"No domain hint was provided\"));\n        }\n        try\n        {\n            // Calls Sso Pre-Validate, assumes baseUri set\n            var requestCultureFeature = Request.HttpContext.Features.Get<IRequestCultureFeature>();\n            var culture = requestCultureFeature.RequestCulture.Culture.Name;\n            var requestPath = $\"/Account/PreValidate?domainHint={domainHint}&culture={culture}\";\n            var httpClient = _clientFactory.CreateClient(\"InternalSso\");\n\n            // Forward the internal SSO result\n            using var responseMessage = await httpClient.GetAsync(requestPath);\n            var responseJson = await responseMessage.Content.ReadAsStringAsync();\n            Response.StatusCode = (int)responseMessage.StatusCode;\n            return Content(responseJson, \"application/json\");\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Error pre-validating against SSO service\");\n            Response.StatusCode = 500;\n            return Json(new ErrorResponseModel(\"Error pre-validating SSO authentication\")\n            {\n                ExceptionMessage = ex.Message,\n                ExceptionStackTrace = ex.StackTrace,\n                InnerExceptionMessage = ex.InnerException?.Message,\n            });\n        }\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> Login(string returnUrl)\n    {\n        var context = await _interaction.GetAuthorizationContextAsync(returnUrl);\n\n        var domainHint = context.Parameters.AllKeys.Contains(\"domain_hint\") ?\n            context.Parameters[\"domain_hint\"] : null;\n        var ssoToken = context.Parameters[SsoTokenable.TokenIdentifier];\n\n        if (string.IsNullOrWhiteSpace(domainHint))\n        {\n            throw new Exception(\"No domain_hint provided\");\n        }\n\n        var userIdentifier = context.Parameters.AllKeys.Contains(\"user_identifier\") ?\n            context.Parameters[\"user_identifier\"] : null;\n\n        return RedirectToAction(nameof(ExternalChallenge), new\n        {\n            domainHint = domainHint,\n            returnUrl,\n            userIdentifier,\n            ssoToken,\n        });\n    }\n\n    [HttpGet]\n    public async Task<IActionResult> ExternalChallenge(string domainHint, string returnUrl,\n        string userIdentifier, string ssoToken)\n    {\n        if (string.IsNullOrWhiteSpace(domainHint))\n        {\n            throw new Exception(\"Invalid organization reference id.\");\n        }\n\n        var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);\n        if (ssoConfig == null || !ssoConfig.Enabled)\n        {\n            throw new Exception(\"Organization not found or SSO configuration not enabled\");\n        }\n        var organizationId = ssoConfig.OrganizationId.ToString();\n\n        var scheme = \"sso\";\n        var props = new AuthenticationProperties\n        {\n            RedirectUri = Url.Action(nameof(ExternalCallback)),\n            Items =\n            {\n                { \"return_url\", returnUrl },\n                { \"domain_hint\", domainHint },\n                { \"organizationId\", organizationId },\n                { \"scheme\", scheme },\n            },\n            Parameters =\n            {\n                { \"ssoToken\", ssoToken },\n            }\n        };\n\n        if (!string.IsNullOrWhiteSpace(userIdentifier))\n        {\n            props.Items.Add(\"user_identifier\", userIdentifier);\n        }\n\n        return Challenge(props, scheme);\n    }\n\n    [HttpGet]\n    public async Task<ActionResult> ExternalCallback()\n    {\n        // Read external identity from the temporary cookie\n        var result = await HttpContext.AuthenticateAsync(\n            Core.AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);\n        if (result?.Succeeded != true)\n        {\n            throw new Exception(\"External authentication error\");\n        }\n\n        // Debugging\n        var externalClaims = result.Principal.Claims.Select(c => $\"{c.Type}: {c.Value}\");\n        _logger.LogDebug(\"External claims: {@claims}\", externalClaims);\n\n        var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);\n        if (user == null)\n        {\n            // Should never happen\n            throw new Exception(\"Cannot find user.\");\n        }\n\n        // This allows us to collect any additional claims or properties\n        // for the specific protocols used and store them in the local auth cookie.\n        // this is typically used to store data needed for signout from those protocols.\n        var additionalLocalClaims = new List<Claim>();\n        var localSignInProps = new AuthenticationProperties\n        {\n            IsPersistent = true,\n            ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1)\n        };\n        if (result.Properties != null && result.Properties.Items.TryGetValue(\"organizationId\", out var organization))\n        {\n            additionalLocalClaims.Add(new Claim(\"organizationId\", organization));\n        }\n        ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);\n\n        // Issue authentication cookie for user\n        await HttpContext.SignInAsync(new IdentityServerUser(user.Id.ToString())\n        {\n            DisplayName = user.Email,\n            IdentityProvider = provider,\n            AdditionalClaims = additionalLocalClaims.ToArray()\n        }, localSignInProps);\n\n        // Delete temporary cookie used during external authentication\n        await HttpContext.SignOutAsync(Core.AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);\n\n        // Retrieve return URL\n        var returnUrl = result.Properties.Items[\"return_url\"] ?? \"~/\";\n\n        var context = await _interaction.GetAuthorizationContextAsync(returnUrl);\n        if (context != null)\n        {\n            if (IsNativeClient(context))\n            {\n                // The client is native, so this change in how to\n                // return the response is for better UX for the end user.\n                HttpContext.Response.StatusCode = 200;\n                HttpContext.Response.Headers[\"Location\"] = string.Empty;\n                return View(\"Redirect\", new RedirectViewModel { RedirectUrl = returnUrl });\n            }\n\n            // We can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null\n            return Redirect(returnUrl);\n        }\n\n        // Request for a local page\n        if (Url.IsLocalUrl(returnUrl))\n        {\n            return Redirect(returnUrl);\n        }\n        else if (string.IsNullOrEmpty(returnUrl))\n        {\n            return Redirect(\"~/\");\n        }\n        else\n        {\n            // User might have clicked on a malicious link - should be logged\n            throw new Exception(\"invalid return URL\");\n        }\n    }\n\n    private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)>\n        FindUserFromExternalProviderAsync(AuthenticateResult result)\n    {\n        var externalUser = result.Principal;\n\n        // Try to determine the unique id of the external user (issued by the provider)\n        // the most common claim type for that are the sub claim and the NameIdentifier\n        // depending on the external provider, some other claim type might be used\n        var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??\n                          externalUser.FindFirst(ClaimTypes.NameIdentifier) ??\n                          throw new Exception(\"Unknown userid\");\n\n        // remove the user id claim so we don't include it as an extra claim if/when we provision the user\n        var claims = externalUser.Claims.ToList();\n        claims.Remove(userIdClaim);\n\n        var provider = result.Properties.Items[\"scheme\"];\n        var providerUserId = userIdClaim.Value;\n        var user = await _userRepository.GetByIdAsync(new Guid(providerUserId));\n\n        return (user, provider, providerUserId, claims);\n    }\n\n    private void ProcessLoginCallback(AuthenticateResult externalResult, List<Claim> localClaims,\n        AuthenticationProperties localSignInProps)\n    {\n        // If the external system sent a session id claim, copy it over\n        // so we can use it for single sign-out\n        var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);\n        if (sid != null)\n        {\n            localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));\n        }\n\n        // If the external provider issued an idToken, we'll keep it for signout\n        var idToken = externalResult.Properties.GetTokenValue(\"id_token\");\n        if (idToken != null)\n        {\n            localSignInProps.StoreTokens(\n                new[] { new AuthenticationToken { Name = \"id_token\", Value = idToken } });\n        }\n    }\n\n    private bool IsNativeClient(Duende.IdentityServer.Models.AuthorizationRequest context)\n    {\n        return !context.RedirectUri.StartsWith(\"https\", StringComparison.Ordinal)\n           && !context.RedirectUri.StartsWith(\"http\", StringComparison.Ordinal);\n    }\n}\n"
  },
  {
    "path": "src/Identity/Dockerfile",
    "content": "###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\n# We put the value in a file to be read by later layers.\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/src/Identity\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    krb5 \\\n    icu-libs \\\n    shadow \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu\n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/src/Identity/out /app\nCOPY ./src/Identity/entrypoint.sh /entrypoint.sh\nRUN chmod +x /entrypoint.sh\nHEALTHCHECK CMD curl -f http://localhost:5000/.well-known/openid-configuration || exit 1\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "src/Identity/Identity.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-Identity</UserSecretsId>\n    <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Identity' \" />\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Identity-SelfHost' \" />\n  <ItemGroup>\n    <ProjectReference Include=\"..\\SharedWeb\\SharedWeb.csproj\" />\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <InternalsVisibleTo Include=\"Identity.Test\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Identity/IdentityServer/ApiClient.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Settings;\nusing Bit.Identity.IdentityServer.RequestValidators;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Identity.IdentityServer;\n\npublic class ApiClient : Client\n{\n    public ApiClient(\n        GlobalSettings globalSettings,\n        string id,\n        int refreshTokenSlidingDays,\n        int accessTokenLifetimeHours,\n        string[] scopes = null)\n    {\n        ClientId = id;\n        AllowedGrantTypes = new[] { GrantType.ResourceOwnerPassword, GrantType.AuthorizationCode, WebAuthnGrantValidator.GrantType };\n\n        // Use global setting: false = Sliding (default), true = Absolute\n        RefreshTokenExpiration = globalSettings.IdentityServer.ApplyAbsoluteExpirationOnRefreshToken\n            ? TokenExpiration.Absolute\n            : TokenExpiration.Sliding;\n\n        RefreshTokenUsage = TokenUsage.ReUse;\n\n        // Use global setting if provided, otherwise use constructor parameter\n        SlidingRefreshTokenLifetime = globalSettings.IdentityServer.SlidingRefreshTokenLifetimeSeconds ?? (86400 * refreshTokenSlidingDays);\n        AbsoluteRefreshTokenLifetime = globalSettings.IdentityServer.AbsoluteRefreshTokenLifetimeSeconds ?? 0; // forever\n\n        UpdateAccessTokenClaimsOnRefresh = true;\n        AccessTokenLifetime = 3600 * accessTokenLifetimeHours;\n        AllowOfflineAccess = true;\n\n        RequireConsent = false;\n        RequirePkce = true;\n        RequireClientSecret = false;\n        if (id == \"web\")\n        {\n            RedirectUris = new[] { $\"{globalSettings.BaseServiceUri.Vault}/sso-connector.html\" };\n            PostLogoutRedirectUris = new[] { globalSettings.BaseServiceUri.Vault };\n            AllowedCorsOrigins = new[] { globalSettings.BaseServiceUri.Vault };\n        }\n        else if (id == \"desktop\")\n        {\n            var desktopUris = new List<string>();\n            desktopUris.Add(\"bitwarden://sso-callback\");\n            for (var port = 8065; port <= 8070; port++)\n            {\n                desktopUris.Add(string.Format(\"http://localhost:{0}\", port));\n            }\n            RedirectUris = desktopUris;\n            PostLogoutRedirectUris = new[] { \"bitwarden://logged-out\" };\n        }\n        else if (id == \"connector\")\n        {\n            var connectorUris = new List<string>();\n            for (var port = 8065; port <= 8070; port++)\n            {\n                connectorUris.Add(string.Format(\"http://localhost:{0}\", port));\n            }\n            RedirectUris = connectorUris.Append(\"bwdc://sso-callback\").ToList();\n            PostLogoutRedirectUris = connectorUris.Append(\"bwdc://logged-out\").ToList();\n        }\n        else if (id == \"browser\")\n        {\n            RedirectUris = new[] { $\"{globalSettings.BaseServiceUri.Vault}/sso-connector.html\" };\n            PostLogoutRedirectUris = new[] { globalSettings.BaseServiceUri.Vault };\n            AllowedCorsOrigins = new[] { globalSettings.BaseServiceUri.Vault };\n        }\n        else if (id == \"cli\")\n        {\n            var cliUris = new List<string>();\n            for (var port = 8065; port <= 8070; port++)\n            {\n                cliUris.Add(string.Format(\"http://localhost:{0}\", port));\n            }\n            RedirectUris = cliUris;\n            PostLogoutRedirectUris = cliUris;\n        }\n        else if (id == \"mobile\")\n        {\n            RedirectUris = new[] { \"bitwarden://sso-callback\" };\n            PostLogoutRedirectUris = new[] { \"bitwarden://logged-out\" };\n        }\n\n        if (scopes == null)\n        {\n            scopes = new string[] { \"api\" };\n        }\n        AllowedScopes = scopes;\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/ApiResources.cs",
    "content": "﻿using Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.IdentityServer;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Identity.IdentityServer;\n\npublic class ApiResources\n{\n    public static IEnumerable<ApiResource> GetApiResources()\n    {\n        return new List<ApiResource>\n        {\n            new(\"api\", new[] {\n                JwtClaimTypes.Name,\n                JwtClaimTypes.Email,\n                JwtClaimTypes.EmailVerified,\n                Claims.SecurityStamp,\n                Claims.Premium,\n                Claims.Device,\n                Claims.DeviceType,\n                Claims.OrganizationOwner,\n                Claims.OrganizationAdmin,\n                Claims.OrganizationUser,\n                Claims.OrganizationCustom,\n                Claims.ProviderAdmin,\n                Claims.ProviderServiceUser,\n                Claims.SecretsManagerAccess\n            }),\n            new(ApiScopes.ApiSendAccess, [\n                JwtClaimTypes.Subject,\n                Claims.SendAccessClaims.SendId\n            ]),\n            new(ApiScopes.Internal, new[] { JwtClaimTypes.Subject }),\n            new(ApiScopes.ApiPush, new[] { JwtClaimTypes.Subject }),\n            new(ApiScopes.ApiLicensing, new[] { JwtClaimTypes.Subject }),\n            new(ApiScopes.ApiOrganization, new[] { JwtClaimTypes.Subject }),\n            new(ApiScopes.ApiInstallation, new[] { JwtClaimTypes.Subject }),\n            new(ApiScopes.ApiSecrets, new[] { JwtClaimTypes.Subject, Claims.Organization }),\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/AuthorizationCodeStore.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Duende.IdentityServer;\nusing Duende.IdentityServer.Extensions;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Services;\nusing Duende.IdentityServer.Stores;\nusing Duende.IdentityServer.Stores.Serialization;\n\nnamespace Bit.Identity.IdentityServer;\n\npublic class AuthorizationCodeStore : DefaultGrantStore<AuthorizationCode>, IAuthorizationCodeStore\n{\n    public AuthorizationCodeStore(\n        IPersistedGrantStore store,\n        IPersistentGrantSerializer serializer,\n        IHandleGenerationService handleGenerationService,\n        ILogger<DefaultAuthorizationCodeStore> logger)\n        : base(IdentityServerConstants.PersistedGrantTypes.AuthorizationCode, store, serializer,\n              handleGenerationService, logger)\n    { }\n\n    public Task<string> StoreAuthorizationCodeAsync(AuthorizationCode code)\n    {\n        return CreateItemAsync(code, code.ClientId, code.Subject.GetSubjectId(), code.SessionId,\n            code.Description, code.CreationTime, code.Lifetime);\n    }\n\n    public Task<AuthorizationCode> GetAuthorizationCodeAsync(string code)\n    {\n        return GetItemAsync(code);\n    }\n\n    public Task RemoveAuthorizationCodeAsync(string code)\n    {\n        // return RemoveItemAsync(code);\n\n        // We don't want to delete authorization codes during validation.\n        // We'll rely on the authorization code lifecycle for short term validation and the\n        // DatabaseExpiredGrantsJob to clean up old authorization codes.\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Platform.Installations;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Identity.IdentityServer.ClientProviders;\n\ninternal class InstallationClientProvider : IClientProvider\n{\n    private readonly IInstallationRepository _installationRepository;\n\n    public InstallationClientProvider(IInstallationRepository installationRepository)\n    {\n        _installationRepository = installationRepository;\n    }\n\n    public async Task<Client> GetAsync(string identifier)\n    {\n        if (!Guid.TryParse(identifier, out var installationId))\n        {\n            return null;\n        }\n\n        var installation = await _installationRepository.GetByIdAsync(installationId);\n\n        if (installation == null)\n        {\n            return null;\n        }\n\n        return new Client\n        {\n            ClientId = $\"installation.{installation.Id}\",\n            RequireClientSecret = true,\n            ClientSecrets = { new Secret(installation.Key.Sha256()) },\n            AllowedScopes = new[]\n            {\n                ApiScopes.ApiPush,\n                ApiScopes.ApiLicensing,\n                ApiScopes.ApiInstallation,\n            },\n            AllowedGrantTypes = GrantTypes.ClientCredentials,\n            AccessTokenLifetime = 3600 * 24,\n            Enabled = installation.Enabled,\n            Claims = new List<ClientClaim>\n            {\n                new(JwtClaimTypes.Subject, installation.Id.ToString()),\n            },\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/ClientProviders/InternalClientProvider.cs",
    "content": "﻿#nullable enable\n\nusing System.Diagnostics;\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Settings;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Identity.IdentityServer.ClientProviders;\n\ninternal class InternalClientProvider : IClientProvider\n{\n    private readonly GlobalSettings _globalSettings;\n\n    public InternalClientProvider(GlobalSettings globalSettings)\n    {\n        // This class should not have been registered when it's not self hosted\n        Debug.Assert(globalSettings.SelfHosted);\n\n        _globalSettings = globalSettings;\n    }\n\n    public Task<Client?> GetAsync(string identifier)\n    {\n        return Task.FromResult<Client?>(new Client\n        {\n            ClientId = $\"internal.{identifier}\",\n            RequireClientSecret = true,\n            ClientSecrets = { new Secret(_globalSettings.InternalIdentityKey.Sha256()) },\n            AllowedScopes = [ApiScopes.Internal],\n            AllowedGrantTypes = GrantTypes.ClientCredentials,\n            AccessTokenLifetime = 3600 * 24,\n            Enabled = true,\n            Claims =\n            [\n                new(JwtClaimTypes.Subject, identifier),\n            ],\n        });\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Identity.IdentityServer.ClientProviders;\n\ninternal class OrganizationClientProvider : IClientProvider\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;\n\n    public OrganizationClientProvider(\n        IOrganizationRepository organizationRepository,\n        IOrganizationApiKeyRepository organizationApiKeyRepository\n    )\n    {\n        _organizationRepository = organizationRepository;\n        _organizationApiKeyRepository = organizationApiKeyRepository;\n    }\n\n    public async Task<Client> GetAsync(string identifier)\n    {\n        if (!Guid.TryParse(identifier, out var organizationId))\n        {\n            return null;\n        }\n\n        var organization = await _organizationRepository.GetByIdAsync(organizationId);\n\n        if (organization == null)\n        {\n            return null;\n        }\n\n        var orgApiKey = (await _organizationApiKeyRepository\n            .GetManyByOrganizationIdTypeAsync(organization.Id, OrganizationApiKeyType.Default))\n            .First();\n\n        return new Client\n        {\n            ClientId = $\"organization.{organization.Id}\",\n            RequireClientSecret = true,\n            ClientSecrets = [new Secret(orgApiKey.ApiKey.Sha256())],\n            AllowedScopes = [ApiScopes.ApiOrganization],\n            AllowedGrantTypes = GrantTypes.ClientCredentials,\n            AccessTokenLifetime = 3600 * 1,\n            Enabled = organization.Enabled && organization.UseApi,\n            Claims =\n            [\n                new(JwtClaimTypes.Subject, organization.Id.ToString()),\n                new(Claims.Type, IdentityClientType.Organization.ToString())\n            ],\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Identity.IdentityServer.ClientProviders;\n\ninternal class SecretsManagerApiKeyProvider : IClientProvider\n{\n    public const string ApiKeyPrefix = \"apikey\";\n\n    private readonly IApiKeyRepository _apiKeyRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n\n    public SecretsManagerApiKeyProvider(IApiKeyRepository apiKeyRepository, IOrganizationRepository organizationRepository)\n    {\n        _apiKeyRepository = apiKeyRepository;\n        _organizationRepository = organizationRepository;\n    }\n\n    public async Task<Client> GetAsync(string identifier)\n    {\n        if (!Guid.TryParse(identifier, out var apiKeyId))\n        {\n            return null;\n        }\n\n        var apiKey = await _apiKeyRepository.GetDetailsByIdAsync(apiKeyId);\n\n        if (apiKey == null || apiKey.ExpireAt <= DateTime.UtcNow)\n        {\n            return null;\n        }\n\n        switch (apiKey)\n        {\n            case ServiceAccountApiKeyDetails key:\n                var org = await _organizationRepository.GetByIdAsync(key.ServiceAccountOrganizationId);\n                if (!org.UseSecretsManager || !org.Enabled)\n                {\n                    return null;\n                }\n                break;\n        }\n\n        var client = new Client\n        {\n            ClientId = identifier,\n            RequireClientSecret = true,\n            ClientSecrets = { new Secret(apiKey.ClientSecretHash) },\n            AllowedScopes = apiKey.GetScopes(),\n            AllowedGrantTypes = GrantTypes.ClientCredentials,\n            AccessTokenLifetime = 3600 * 1,\n            ClientClaimsPrefix = null,\n            Properties = new Dictionary<string, string> {\n                {\"encryptedPayload\", apiKey.EncryptedPayload},\n            },\n            Claims = new List<ClientClaim>\n            {\n                new(JwtClaimTypes.Subject, apiKey.ServiceAccountId.ToString()),\n                new(Claims.Type, IdentityClientType.ServiceAccount.ToString()),\n            },\n        };\n\n        switch (apiKey)\n        {\n            case ServiceAccountApiKeyDetails key:\n                client.Claims.Add(new ClientClaim(Claims.Organization, key.ServiceAccountOrganizationId.ToString()));\n                break;\n        }\n\n        return client;\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs",
    "content": "﻿#nullable enable\n\nusing System.Collections.ObjectModel;\nusing System.Security.Claims;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Identity.IdentityServer.ClientProviders;\n\npublic class UserClientProvider : IClientProvider\n{\n    private readonly IUserRepository _userRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly ILicensingService _licensingService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n\n    public UserClientProvider(\n        IUserRepository userRepository,\n        ICurrentContext currentContext,\n        ILicensingService licensingService,\n        IOrganizationUserRepository organizationUserRepository,\n        IProviderUserRepository providerUserRepository)\n    {\n        _userRepository = userRepository;\n        _currentContext = currentContext;\n        _licensingService = licensingService;\n        _organizationUserRepository = organizationUserRepository;\n        _providerUserRepository = providerUserRepository;\n    }\n\n    public async Task<Client?> GetAsync(string identifier)\n    {\n        if (!Guid.TryParse(identifier, out var userId))\n        {\n            return null;\n        }\n\n        var user = await _userRepository.GetByIdAsync(userId);\n        if (user == null)\n        {\n            return null;\n        }\n\n        var claims = new Collection<ClientClaim>\n        {\n            new(JwtClaimTypes.Subject, user.Id.ToString()),\n            new(JwtClaimTypes.AuthenticationMethod, \"Application\", \"external\"),\n            new(Claims.Type, IdentityClientType.User.ToString()),\n        };\n        var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);\n        var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);\n        var isPremium = await _licensingService.ValidateUserPremiumAsync(user);\n        foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium))\n        {\n            var upperValue = claim.Value.ToUpperInvariant();\n            var isBool = upperValue is \"TRUE\" or \"FALSE\";\n            claims.Add(isBool\n                ? new ClientClaim(claim.Key, claim.Value, ClaimValueTypes.Boolean)\n                : new ClientClaim(claim.Key, claim.Value)\n            );\n        }\n\n        return new Client\n        {\n            ClientId = $\"user.{userId}\",\n            RequireClientSecret = true,\n            ClientSecrets = { new Secret(user.ApiKey.Sha256()) },\n            AllowedScopes = new[] { \"api\" },\n            AllowedGrantTypes = GrantTypes.ClientCredentials,\n            AccessTokenLifetime = 3600 * 1,\n            ClientClaimsPrefix = null,\n            Claims = claims,\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/Constants/RequestValidationConstants.cs",
    "content": "﻿namespace Bit.Identity.IdentityServer.RequestValidationConstants;\n\npublic static class CustomResponseConstants\n{\n    public static class ResponseKeys\n    {\n        /// <summary>\n        /// Identifies the error model returned in the custom response when an error occurs.\n        /// </summary>\n        public static string ErrorModel => \"ErrorModel\";\n        /// <summary>\n        /// This Key is used when a user is in a single organization that requires SSO authentication. The identifier\n        /// is used by the client to speed the redirection to the correct IdP for the user's organization.\n        /// </summary>\n        public static string SsoOrganizationIdentifier => \"SsoOrganizationIdentifier\";\n    }\n}\n\npublic static class SsoConstants\n{\n    /// <summary>\n    /// These are messages and errors we return when SSO Validation is unsuccessful\n    /// </summary>\n    public static class RequestErrors\n    {\n        public static string SsoRequired => \"sso_required\";\n        public static string SsoRequiredDescription => \"Sso authentication is required.\";\n        public static string SsoTwoFactorRecoveryDescription => \"Two-factor recovery has been performed. SSO authentication is required.\";\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/CustomValidatorRequestContext.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Entities;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer;\n\npublic class CustomValidatorRequestContext\n{\n    public User User { get; set; }\n    /// <summary>\n    /// This is the device that the user is using to authenticate. It can be either known or unknown.\n    /// We set it here since the ResourceOwnerPasswordValidator needs the device to do device validation.\n    /// The option to set it here saves a trip to the database.\n    /// </summary>\n    public Device Device { get; set; }\n    /// <summary>\n    /// Communicates whether or not the device in the request is known to the user.\n    /// KnownDevice is set in the child classes of the BaseRequestValidator using the DeviceValidator.KnownDeviceAsync method.\n    /// Except in the CustomTokenRequestValidator, where it is hardcoded to true.\n    /// </summary>\n    public bool KnownDevice { get; set; }\n    /// <summary>\n    /// This communicates whether or not two factor is required for the user to authenticate.\n    /// </summary>\n    public bool TwoFactorRequired { get; set; } = false;\n    /// <summary>\n    /// Whether the user has requested recovery of their 2FA methods using their one-time\n    /// recovery code.\n    /// </summary>\n    /// <seealso cref=\"Bit.Core.Auth.Enums.TwoFactorProviderType\"/>\n    public bool TwoFactorRecoveryRequested { get; set; } = false;\n    /// <summary>\n    /// This communicates whether or not SSO is required for the user to authenticate.\n    /// </summary>\n    public bool SsoRequired { get; set; } = false;\n    /// <summary>\n    /// We use the parent class for both GrantValidationResult and TokenRequestValidationResult here for\n    /// flexibility when building an error response.\n    /// This will be null if the authentication request is successful.\n    /// </summary>\n    public ValidationResult ValidationErrorResult { get; set; }\n    /// <summary>\n    /// This dictionary should contain relevant information for the clients to act on.\n    /// This will contain the information used to guide a user to successful authentication, such as TwoFactorProviders.\n    /// This will be null if the authentication request is successful.\n    /// </summary>\n    public Dictionary<string, object> CustomResponse { get; set; }\n    /// <summary>\n    /// A validated auth request\n    /// <see cref=\"AuthRequest.IsValidForAuthentication\"/>\n    /// </summary>\n    public AuthRequest ValidatedAuthRequest { get; set; }\n    /// <summary>\n    /// Whether the user has requested a Remember Me token for their current device.\n    /// </summary>\n    public bool RememberMeRequested { get; set; } = false;\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/DynamicClientStore.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Identity.IdentityServer.ClientProviders;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Stores;\n\nnamespace Bit.Identity.IdentityServer;\n\npublic interface IClientProvider\n{\n    Task<Client?> GetAsync(string identifier);\n}\n\ninternal class DynamicClientStore : IClientStore\n{\n    private readonly IServiceProvider _serviceProvider;\n    private readonly IClientProvider _apiKeyClientProvider;\n    private readonly StaticClientStore _staticClientStore;\n\n    public DynamicClientStore(\n      IServiceProvider serviceProvider,\n      [FromKeyedServices(SecretsManagerApiKeyProvider.ApiKeyPrefix)] IClientProvider apiKeyClientProvider,\n      StaticClientStore staticClientStore\n    )\n    {\n        _serviceProvider = serviceProvider;\n        _apiKeyClientProvider = apiKeyClientProvider;\n        _staticClientStore = staticClientStore;\n    }\n\n    public Task<Client?> FindClientByIdAsync(string clientId)\n    {\n        var clientIdSpan = clientId.AsSpan();\n\n        var firstPeriod = clientIdSpan.IndexOf('.');\n\n        if (firstPeriod == -1)\n        {\n            // No splitter, attempt but don't fail for a static client\n            if (_staticClientStore.Clients.TryGetValue(clientId, out var client))\n            {\n                return Task.FromResult<Client?>(client);\n            }\n        }\n        else\n        {\n            // Increment past the period\n            var identifierName = clientIdSpan[..firstPeriod++];\n\n            var identifier = clientIdSpan[firstPeriod..];\n\n            // The identifier is required to be non-empty\n            if (identifier.IsEmpty || identifier.IsWhiteSpace())\n            {\n                return Task.FromResult<Client?>(null);\n            }\n\n            // Once identifierName is proven valid, materialize the string\n            var clientBuilder = _serviceProvider.GetKeyedService<IClientProvider>(identifierName.ToString());\n\n            if (clientBuilder == null)\n            {\n                // No client registered by this identifier\n                return Task.FromResult<Client?>(null);\n            }\n\n            return clientBuilder.GetAsync(identifier.ToString());\n        }\n\n        // It could be an ApiKey, give them the full thing to try,\n        // this is a special case for legacy reasons, no other client should\n        // be allowed without a prefixing identifier.\n        return _apiKeyClientProvider.GetAsync(clientId);\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/Enums/CustomGrantTypes.cs",
    "content": "﻿namespace Bit.Identity.IdentityServer.Enums;\n\n/// <summary>\n/// A class containing custom grant types used in the Bitwarden IdentityServer implementation\n/// </summary>\npublic static class CustomGrantTypes\n{\n    public const string SendAccess = \"send_access\";\n    // TODO: PM-24471 replace magic string with a constant for webauthn\n    public const string WebAuthn = \"webauthn\";\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/Enums/DeviceValidationResultType.cs",
    "content": "﻿namespace Bit.Identity.IdentityServer.Enums;\n\npublic enum DeviceValidationResultType : byte\n{\n    Success = 0,\n    InvalidUser = 1,\n    InvalidNewDeviceOtp = 2,\n    NewDeviceVerificationRequired = 3,\n    NoDeviceInformationProvided = 4,\n    AuthRequestFlowUnknownDevice = 5,\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Api.Response;\nusing Bit.Core.Entities;\n\nnamespace Bit.Identity.IdentityServer;\npublic interface IUserDecryptionOptionsBuilder\n{\n    IUserDecryptionOptionsBuilder ForUser(User user);\n    IUserDecryptionOptionsBuilder WithDevice(Device device);\n    IUserDecryptionOptionsBuilder WithSso(SsoConfig ssoConfig);\n    IUserDecryptionOptionsBuilder WithWebAuthnLoginCredential(WebAuthnCredential credential);\n    Task<UserDecryptionOptions> BuildAsync();\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/PersistedGrantStore.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Stores;\n\nnamespace Bit.Identity.IdentityServer;\n\npublic class PersistedGrantStore : IPersistedGrantStore\n{\n    private readonly IGrantRepository _grantRepository;\n    private readonly Func<PersistedGrant, IGrant> _toGrant;\n\n    public PersistedGrantStore(\n        IGrantRepository grantRepository,\n        Func<PersistedGrant, IGrant> toGrant)\n    {\n        _grantRepository = grantRepository;\n        _toGrant = toGrant;\n    }\n\n    public async Task<PersistedGrant> GetAsync(string key)\n    {\n        var grant = await _grantRepository.GetByKeyAsync(key);\n        if (grant == null)\n        {\n            return null;\n        }\n\n        var pGrant = ToPersistedGrant(grant);\n        return pGrant;\n    }\n\n    public async Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)\n    {\n        var grants = await _grantRepository.GetManyAsync(filter.SubjectId, filter.SessionId,\n            filter.ClientId, filter.Type);\n        var pGrants = grants.Select(g => ToPersistedGrant(g));\n        return pGrants;\n    }\n\n    public async Task RemoveAllAsync(PersistedGrantFilter filter)\n    {\n        await _grantRepository.DeleteManyAsync(filter.SubjectId, filter.SessionId, filter.ClientId, filter.Type);\n    }\n\n    public async Task RemoveAsync(string key)\n    {\n        await _grantRepository.DeleteByKeyAsync(key);\n    }\n\n    public async Task StoreAsync(PersistedGrant pGrant)\n    {\n        var grant = _toGrant(pGrant);\n        await _grantRepository.SaveAsync(grant);\n    }\n\n    private PersistedGrant ToPersistedGrant(IGrant grant)\n    {\n        return new PersistedGrant\n        {\n            Key = grant.Key,\n            Type = grant.Type,\n            SubjectId = grant.SubjectId,\n            SessionId = grant.SessionId,\n            ClientId = grant.ClientId,\n            Description = grant.Description,\n            CreationTime = grant.CreationDate,\n            Expiration = grant.ExpirationDate,\n            ConsumedTime = grant.ConsumedDate,\n            Data = grant.Data\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/ProfileService.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Services;\n\nnamespace Bit.Identity.IdentityServer;\n\npublic class ProfileService : IProfileService\n{\n    private readonly IUserService _userService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository;\n    private readonly ILicensingService _licensingService;\n    private readonly ICurrentContext _currentContext;\n\n    public ProfileService(\n        IUserService userService,\n        IOrganizationUserRepository organizationUserRepository,\n        IProviderUserRepository providerUserRepository,\n        IProviderOrganizationRepository providerOrganizationRepository,\n        ILicensingService licensingService,\n        ICurrentContext currentContext)\n    {\n        _userService = userService;\n        _organizationUserRepository = organizationUserRepository;\n        _providerUserRepository = providerUserRepository;\n        _providerOrganizationRepository = providerOrganizationRepository;\n        _licensingService = licensingService;\n        _currentContext = currentContext;\n    }\n\n    public async Task GetProfileDataAsync(ProfileDataRequestContext context)\n    {\n        var existingClaims = context.Subject.Claims;\n\n        // If the client is a Send client, we do not add any additional claims\n        if (context.Client.ClientId == BitwardenClient.Send)\n        {\n            // preserve all claims that were already on context.Subject\n            // which includes the ones added by the SendAccessGrantValidator\n            context.IssuedClaims.AddRange(existingClaims);\n            return;\n        }\n\n        // Whenever IdentityServer issues a new access token or services a UserInfo request, it calls\n        // GetProfileDataAsync to determine which claims to include in the token or response.\n        // In normal user identity scenarios, we have to look up the user to get their claims and update\n        // the issued claims collection as claim info can have changed since the last time the user logged in or the\n        // last time the token was issued.\n        var newClaims = new List<Claim>();\n        var user = await _userService.GetUserByPrincipalAsync(context.Subject);\n        if (user != null)\n        {\n            var isPremium = await _licensingService.ValidateUserPremiumAsync(user);\n            var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);\n            var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);\n            foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium))\n            {\n                var upperValue = claim.Value.ToUpperInvariant();\n                var isBool = upperValue == \"TRUE\" || upperValue == \"FALSE\";\n                newClaims.Add(isBool ?\n                    new Claim(claim.Key, claim.Value, ClaimValueTypes.Boolean) :\n                    new Claim(claim.Key, claim.Value)\n                );\n            }\n        }\n\n        // filter out any of the new claims\n        var existingClaimsToKeep = existingClaims\n            .Where(c =>\n                // Drop any org claims\n                !c.Type.StartsWith(\"org\") &&\n                // If we have no new claims, then keep the existing claims\n                // If we have new claims, then keep the existing claim if it does not match a new claim type\n                (newClaims.Count == 0 || !newClaims.Any(nc => nc.Type == c.Type))\n            ).ToList();\n\n        newClaims.AddRange(existingClaimsToKeep);\n        if (newClaims.Count != 0)\n        {\n            context.IssuedClaims.AddRange(newClaims);\n        }\n    }\n\n    public async Task IsActiveAsync(IsActiveContext context)\n    {\n        // Send Tokens are not refreshed so when the token has expired the user must request a new one via the authentication method assigned to the send.\n        if (context.Client.ClientId == BitwardenClient.Send)\n        {\n            context.IsActive = true;\n            return;\n        }\n\n        // We add the security stamp claim to the persisted grant when we issue the refresh token.\n        // IdentityServer will add this claim to the subject, and here we evaluate whether the security stamp that\n        // was persisted matches the current security stamp of the user. If it does not match, then the user has performed\n        // an operation that we want to invalidate the refresh token.\n        var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c => c.Type == Claims.SecurityStamp);\n        var user = await _userService.GetUserByPrincipalAsync(context.Subject);\n\n        if (user != null && securityTokenClaim != null)\n        {\n            context.IsActive = string.Equals(user.SecurityStamp, securityTokenClaim.Value,\n                StringComparison.InvariantCultureIgnoreCase);\n            return;\n        }\n        else\n        {\n            context.IsActive = true;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n\n#nullable disable\n\nusing System.Security.Claims;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.Models.Api.Response;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Api.Response;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Api.Response;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Duende.IdentityServer.Validation;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\npublic abstract class BaseRequestValidator<T> where T : class\n{\n    private UserManager<User> _userManager;\n    private readonly IEventService _eventService;\n    private readonly IDeviceValidator _deviceValidator;\n    private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;\n    private readonly ISsoRequestValidator _ssoRequestValidator;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ILogger _logger;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IUserRepository _userRepository;\n    private readonly IAuthRequestRepository _authRequestRepository;\n    private readonly IMailService _mailService;\n    private readonly IClientVersionValidator _clientVersionValidator;\n\n    protected ICurrentContext CurrentContext { get; }\n    protected IPolicyService PolicyService { get; }\n    protected IFeatureService _featureService { get; }\n    protected ISsoConfigRepository SsoConfigRepository { get; }\n    protected IUserService _userService { get; }\n    protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }\n    protected IPolicyRequirementQuery PolicyRequirementQuery { get; }\n    protected IUserAccountKeysQuery _accountKeysQuery { get; }\n\n    public BaseRequestValidator(\n        UserManager<User> userManager,\n        IUserService userService,\n        IEventService eventService,\n        IDeviceValidator deviceValidator,\n        ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,\n        ISsoRequestValidator ssoRequestValidator,\n        IOrganizationUserRepository organizationUserRepository,\n        ILogger logger,\n        ICurrentContext currentContext,\n        GlobalSettings globalSettings,\n        IUserRepository userRepository,\n        IPolicyService policyService,\n        IFeatureService featureService,\n        ISsoConfigRepository ssoConfigRepository,\n        IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IAuthRequestRepository authRequestRepository,\n        IMailService mailService,\n        IUserAccountKeysQuery userAccountKeysQuery,\n        IClientVersionValidator clientVersionValidator\n    )\n    {\n        _userManager = userManager;\n        _userService = userService;\n        _eventService = eventService;\n        _deviceValidator = deviceValidator;\n        _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;\n        _ssoRequestValidator = ssoRequestValidator;\n        _organizationUserRepository = organizationUserRepository;\n        _logger = logger;\n        CurrentContext = currentContext;\n        _globalSettings = globalSettings;\n        PolicyService = policyService;\n        _userRepository = userRepository;\n        _featureService = featureService;\n        SsoConfigRepository = ssoConfigRepository;\n        UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;\n        PolicyRequirementQuery = policyRequirementQuery;\n        _authRequestRepository = authRequestRepository;\n        _mailService = mailService;\n        _accountKeysQuery = userAccountKeysQuery;\n        _clientVersionValidator = clientVersionValidator;\n    }\n\n    protected async Task ValidateAsync(T context, ValidatedTokenRequest request,\n        CustomValidatorRequestContext validatorContext)\n    {\n        var validators = DetermineValidationOrder(context, request, validatorContext);\n        var allValidationSchemesSuccessful = await ProcessValidatorsAsync(validators);\n        if (!allValidationSchemesSuccessful)\n        {\n            // Each validation task is responsible for setting its own non-success status, if applicable.\n            return;\n        }\n\n        await BuildSuccessResultAsync(validatorContext.User, context, validatorContext.Device,\n            validatorContext.RememberMeRequested);\n    }\n\n    protected async Task FailAuthForLegacyUserAsync(User user, T context)\n    {\n        await BuildErrorResultAsync(\n            $\"Legacy encryption without a userkey is no longer supported. To recover your account, please contact support\",\n            false, context, user);\n    }\n\n    protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);\n\n    /// <summary>\n    /// Composer for validation schemes.\n    /// </summary>\n    /// <param name=\"context\">The current request context.</param>\n    /// <param name=\"request\"><see cref=\"Duende.IdentityServer.Validation.ValidatedTokenRequest\" /></param>\n    /// <param name=\"validatorContext\"><see cref=\"Bit.Identity.IdentityServer.CustomValidatorRequestContext\" /></param>\n    /// <returns>A composed array of validation scheme delegates to evaluate in order.</returns>\n    private Func<Task<bool>>[] DetermineValidationOrder(T context, ValidatedTokenRequest request,\n        CustomValidatorRequestContext validatorContext)\n    {\n        if (RecoveryCodeRequestForSsoRequiredUserScenario())\n        {\n            // Support valid requests to recover 2FA (with account code) for users who require SSO\n            // by organization membership.\n            // This requires an evaluation of 2FA validity in front of SSO, and an opportunity for the 2FA\n            // validation to perform the recovery as part of scheme validation based on the request.\n            return\n            [\n                () => ValidateGrantSpecificContext(context, validatorContext),\n                // Now check the version number of the client. Do this after ValidateContextAsync so that\n                // we prevent account enumeration. If we were to do this before ValidateContextAsync, then attackers\n                // could use a known invalid client version and make a request for a user (before we know if they have\n                // demonstrated ownership of the account via correct credentials) and identify if they exist by getting\n                // an error response back from the validator saying the user is not compatible with the client.\n                () => ValidateClientVersionAsync(context, validatorContext),\n                () => ValidateTwoFactorAsync(context, request, validatorContext),\n                () => ValidateSsoAsync(context, request, validatorContext),\n                () => ValidateNewDeviceAsync(context, request, validatorContext),\n                () => ValidateLegacyMigrationAsync(context, request, validatorContext),\n                () => ValidateAuthRequestAsync(validatorContext)\n            ];\n        }\n        else\n        {\n            // The typical validation scenario.\n            return\n            [\n                () => ValidateGrantSpecificContext(context, validatorContext),\n                // Now check the version number of the client. Do this after ValidateContextAsync so that\n                // we prevent account enumeration. If we were to do this before ValidateContextAsync, then attackers\n                // could use a known invalid client version and make a request for a user (before we know if they have\n                // demonstrated ownership of the account via correct credentials) and identify if they exist by getting\n                // an error response back from the validator saying the user is not compatible with the client.\n                () => ValidateClientVersionAsync(context, validatorContext),\n                () => ValidateSsoAsync(context, request, validatorContext),\n                () => ValidateTwoFactorAsync(context, request, validatorContext),\n                () => ValidateNewDeviceAsync(context, request, validatorContext),\n                () => ValidateLegacyMigrationAsync(context, request, validatorContext),\n                () => ValidateAuthRequestAsync(validatorContext)\n            ];\n        }\n\n        bool RecoveryCodeRequestForSsoRequiredUserScenario()\n        {\n            var twoFactorProvider = request.Raw[\"TwoFactorProvider\"];\n            var twoFactorToken = request.Raw[\"TwoFactorToken\"];\n\n            // Both provider and token must be present;\n            // Validity of the token for a given provider will be evaluated by the TwoFactorAuthenticationValidator.\n            if (string.IsNullOrWhiteSpace(twoFactorProvider) || string.IsNullOrWhiteSpace(twoFactorToken))\n            {\n                return false;\n            }\n\n            if (!int.TryParse(twoFactorProvider, out var providerValue))\n            {\n                return false;\n            }\n\n            return providerValue == (int)TwoFactorProviderType.RecoveryCode;\n        }\n    }\n\n    /// <summary>\n    /// Processes the validation schemes sequentially.\n    /// Each validator is responsible for setting error context responses on failure and adding itself to the\n    /// validatorContext's CompletedValidationSchemes (only) on success.\n    /// Failure of any scheme to validate will short-circuit the collection, causing the validation error to be\n    /// returned and further schemes to not be evaluated.\n    /// </summary>\n    /// <param name=\"validators\">The collection of validation schemes as composed in <see cref=\"DetermineValidationOrder\" /></param>\n    /// <returns>true if all schemes validated successfully, false if any failed.</returns>\n    private static async Task<bool> ProcessValidatorsAsync(params Func<Task<bool>>[] validators)\n    {\n        foreach (var validator in validators)\n        {\n            if (!await validator())\n            {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /// <summary>\n    /// Validates whether the client version is compatible for the user attempting to authenticate.\n    /// </summary>\n    /// <returns>true if the scheme successfully passed validation, otherwise false.</returns>\n    private async Task<bool> ValidateClientVersionAsync(T context, CustomValidatorRequestContext validatorContext)\n    {\n        var ok = _clientVersionValidator.Validate(validatorContext.User, validatorContext);\n        if (ok)\n        {\n            return true;\n        }\n\n        SetValidationErrorResult(context, validatorContext);\n        await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn);\n        return false;\n    }\n\n    /// <summary>\n    /// Validates the user's master password, webauthen, or custom token request via the appropriate context validator.\n    /// </summary>\n    /// <param name=\"context\">The current request context.</param>\n    /// <param name=\"validatorContext\"><see cref=\"Bit.Identity.IdentityServer.CustomValidatorRequestContext\" /></param>\n    /// <returns>true if the scheme successfully passed validation, otherwise false.</returns>\n    private async Task<bool> ValidateGrantSpecificContext(T context, CustomValidatorRequestContext validatorContext)\n    {\n        var valid = await ValidateContextAsync(context, validatorContext);\n        var user = validatorContext.User;\n        if (valid)\n        {\n            return true;\n        }\n\n        await UpdateFailedAuthDetailsAsync(user);\n\n        await BuildErrorResultAsync(\"Username or password is incorrect. Try again.\", false, context, user);\n        return false;\n    }\n\n    /// <summary>\n    /// Validates the user's organization-enforced Single Sign-on (SSO) requirement.\n    /// </summary>\n    /// <param name=\"context\">The current request context.</param>\n    /// <param name=\"request\"><see cref=\"Duende.IdentityServer.Validation.ValidatedTokenRequest\" /></param>\n    /// <param name=\"validatorContext\"><see cref=\"Bit.Identity.IdentityServer.CustomValidatorRequestContext\" /></param>\n    /// <returns>true if the scheme successfully passed validation, otherwise false.</returns>\n    /// <seealso cref=\"DetermineValidationOrder\" />\n    private async Task<bool> ValidateSsoAsync(T context, ValidatedTokenRequest request,\n        CustomValidatorRequestContext validatorContext)\n    {\n        var ssoValid = await _ssoRequestValidator.ValidateAsync(validatorContext.User, request, validatorContext);\n        if (ssoValid)\n        {\n            return true;\n        }\n\n        SetValidationErrorResult(context, validatorContext);\n        return ssoValid;\n    }\n\n    /// <summary>\n    /// Validates the user's Multi-Factor Authentication (2FA) scheme.\n    /// </summary>\n    /// <param name=\"context\">The current request context.</param>\n    /// <param name=\"request\"><see cref=\"Duende.IdentityServer.Validation.ValidatedTokenRequest\" /></param>\n    /// <param name=\"validatorContext\"><see cref=\"Bit.Identity.IdentityServer.CustomValidatorRequestContext\" /></param>\n    /// <returns>true if the scheme successfully passed validation, otherwise false.</returns>\n    private async Task<bool> ValidateTwoFactorAsync(T context, ValidatedTokenRequest request,\n        CustomValidatorRequestContext validatorContext)\n    {\n        (validatorContext.TwoFactorRequired, var twoFactorOrganization) =\n            await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(validatorContext.User, request);\n\n        if (!validatorContext.TwoFactorRequired)\n        {\n            return true;\n        }\n\n        var twoFactorToken = request.Raw[\"TwoFactorToken\"];\n        var twoFactorProvider = request.Raw[\"TwoFactorProvider\"];\n        var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&\n                                    !string.IsNullOrWhiteSpace(twoFactorProvider);\n\n        // 3a. Response for 2FA required and not provided state.\n        if (!validTwoFactorRequest ||\n            !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))\n        {\n            var resultDict = await _twoFactorAuthenticationValidator\n                .BuildTwoFactorResultAsync(validatorContext.User, twoFactorOrganization);\n            if (resultDict == null)\n            {\n                await BuildErrorResultAsync(\"No two-step providers enabled.\", false, context, validatorContext.User);\n                return false;\n            }\n\n            // Include Master Password Policy in 2FA response.\n            resultDict.Add(\"MasterPasswordPolicy\", await GetMasterPasswordPolicyAsync(validatorContext.User));\n            SetTwoFactorResult(context, resultDict);\n            return false;\n        }\n\n        var twoFactorTokenValid =\n            await _twoFactorAuthenticationValidator\n                .VerifyTwoFactorAsync(validatorContext.User, twoFactorOrganization, twoFactorProviderType,\n                    twoFactorToken);\n\n        // 3b. Response for 2FA required but request is not valid or remember token expired state.\n        if (!twoFactorTokenValid)\n        {\n            // The remember me token has expired.\n            if (twoFactorProviderType == TwoFactorProviderType.Remember)\n            {\n                var resultDict = await _twoFactorAuthenticationValidator\n                    .BuildTwoFactorResultAsync(validatorContext.User, twoFactorOrganization);\n\n                // Include Master Password Policy in 2FA response\n                resultDict.Add(\"MasterPasswordPolicy\", await GetMasterPasswordPolicyAsync(validatorContext.User));\n                SetTwoFactorResult(context, resultDict);\n            }\n            else\n            {\n                await SendFailedTwoFactorEmail(validatorContext.User, twoFactorProviderType);\n                await UpdateFailedAuthDetailsAsync(validatorContext.User);\n                await BuildErrorResultAsync(\"Two-step token is invalid. Try again.\", true, context,\n                    validatorContext.User);\n            }\n\n            return false;\n        }\n\n        // 3c. Given a valid token and a successful two-factor verification, if the provider type is Recovery Code,\n        // recovery will have been performed as part of 2FA validation. This will be relevant for, e.g., SSO users\n        // who are requesting recovery, but who will still need to log in after 2FA recovery.\n        if (twoFactorProviderType == TwoFactorProviderType.RecoveryCode)\n        {\n            validatorContext.TwoFactorRecoveryRequested = true;\n        }\n\n        // 3d. When the 2FA authentication is successful, we can check if the user wants a\n        // rememberMe token.\n        var twoFactorRemember = request.Raw[\"TwoFactorRemember\"] == \"1\";\n        // Check if the user wants a rememberMe token.\n        if (twoFactorRemember\n            // if the 2FA auth was rememberMe do not send another token.\n            && twoFactorProviderType != TwoFactorProviderType.Remember)\n        {\n            validatorContext.RememberMeRequested = true;\n        }\n\n        return true;\n    }\n\n    /// <summary>\n    /// Validates whether the user is logging in from a known device.\n    /// </summary>\n    /// <param name=\"context\">The current request context.</param>\n    /// <param name=\"request\"><see cref=\"Duende.IdentityServer.Validation.ValidatedTokenRequest\" /></param>\n    /// <param name=\"validatorContext\"><see cref=\"Bit.Identity.IdentityServer.CustomValidatorRequestContext\" /></param>\n    /// <returns>true if the scheme successfully passed validation, otherwise false.</returns>\n    private async Task<bool> ValidateNewDeviceAsync(T context, ValidatedTokenRequest request,\n        CustomValidatorRequestContext validatorContext)\n    {\n        var deviceValid = await _deviceValidator.ValidateRequestDeviceAsync(request, validatorContext);\n        if (deviceValid)\n        {\n            return true;\n        }\n\n        SetValidationErrorResult(context, validatorContext);\n        await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn);\n        return false;\n    }\n\n    /// <summary>\n    /// Validates whether the user should be denied access on a given non-Web client and sent to the Web client\n    /// for Legacy migration.\n    /// </summary>\n    /// <param name=\"context\">The current request context.</param>\n    /// <param name=\"request\"><see cref=\"Duende.IdentityServer.Validation.ValidatedTokenRequest\" /></param>\n    /// <param name=\"validatorContext\"><see cref=\"Bit.Identity.IdentityServer.CustomValidatorRequestContext\" /></param>\n    /// <returns>true if the scheme successfully passed validation, otherwise false.</returns>\n    private async Task<bool> ValidateLegacyMigrationAsync(T context, ValidatedTokenRequest request,\n        CustomValidatorRequestContext validatorContext)\n    {\n        if (!UserService.IsLegacyUser(validatorContext.User) || request.ClientId == \"web\")\n        {\n            return true;\n        }\n\n        await FailAuthForLegacyUserAsync(validatorContext.User, context);\n        return false;\n    }\n\n    /// <summary>\n    /// Validates and updates the auth request's timestamp.\n    /// </summary>\n    /// <param name=\"validatorContext\"><see cref=\"Bit.Identity.IdentityServer.CustomValidatorRequestContext\" /></param>\n    /// <returns>true on evaluation and/or completed update of the AuthRequest.</returns>\n    private async Task<bool> ValidateAuthRequestAsync(CustomValidatorRequestContext validatorContext)\n    {\n        // TODO: PM-24324 - This should be its own validator at some point.\n        if (validatorContext.ValidatedAuthRequest != null)\n        {\n            validatorContext.ValidatedAuthRequest.AuthenticationDate = DateTime.UtcNow;\n            await _authRequestRepository.ReplaceAsync(validatorContext.ValidatedAuthRequest);\n        }\n\n        return true;\n    }\n\n    /// <summary>\n    /// Responsible for building the response to the client when the user has successfully authenticated.\n    /// </summary>\n    /// <param name=\"user\">The authenticated user.</param>\n    /// <param name=\"context\">The current request context.</param>\n    /// <param name=\"device\">The device used for authentication.</param>\n    /// <param name=\"sendRememberToken\">Whether to send a 2FA remember token.</param>\n    protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)\n    {\n        await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn);\n\n        var claims = this.BuildSubjectClaims(user, context, device);\n\n        var customResponse = await BuildCustomResponse(user, context, device, sendRememberToken);\n\n        await ResetFailedAuthDetailsAsync(user);\n\n        // Once we've built the claims and custom response, we can set the success result.\n        // We delegate this to the derived classes, as the implementation varies based on the grant type.\n        await SetSuccessResult(context, user, claims, customResponse);\n    }\n\n    /// <summary>\n    /// This does two things, it sets the error result for the current ValidatorContext _and_ it logs error.\n    /// These two things should be seperated to maintain single concerns.\n    /// </summary>\n    /// <param name=\"message\">Error message for the error result</param>\n    /// <param name=\"twoFactorRequest\">bool that controls how the error is logged</param>\n    /// <param name=\"context\">used to set the error result in the current validator</param>\n    /// <param name=\"user\">used to associate the failed login with a user</param>\n    /// <returns>void</returns>\n    [Obsolete(\"Consider using SetValidationErrorResult to set the validation result, and LogFailedLoginEvent \" +\n              \"to log the failure.\")]\n    protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user)\n    {\n        if (user != null)\n        {\n            await _eventService.LogUserEventAsync(user.Id,\n                twoFactorRequest ? EventType.User_FailedLogIn2fa : EventType.User_FailedLogIn);\n        }\n\n        if (_globalSettings.SelfHosted)\n        {\n            _logger.LogWarning(Constants.BypassFiltersEventId,\n                \"Failed login attempt. Is2FARequest: {Is2FARequest} IpAddress: {IpAddress}\", twoFactorRequest,\n                CurrentContext.IpAddress);\n        }\n\n        await Task.Delay(2000); // Delay for brute force.\n        SetErrorResult(context,\n            new Dictionary<string, object> { { \"ErrorModel\", new ErrorResponseModel(message) } });\n    }\n\n    protected async Task LogFailedLoginEvent(User user, EventType eventType)\n    {\n        if (user != null)\n        {\n            await _eventService.LogUserEventAsync(user.Id, eventType);\n        }\n\n        if (_globalSettings.SelfHosted)\n        {\n            string formattedMessage;\n            switch (eventType)\n            {\n                case EventType.User_FailedLogIn:\n                    formattedMessage = string.Format(\"Failed login attempt. {0}\", $\" {CurrentContext.IpAddress}\");\n                    break;\n                case EventType.User_FailedLogIn2fa:\n                    formattedMessage = string.Format(\"Failed login attempt, 2FA invalid.{0}\",\n                        $\" {CurrentContext.IpAddress}\");\n                    break;\n                default:\n                    formattedMessage = \"Failed login attempt.\";\n                    break;\n            }\n\n            _logger.LogWarning(Constants.BypassFiltersEventId, \"{FailedLoginMessage}\", formattedMessage);\n        }\n\n        await Task.Delay(2000); // Delay for brute force.\n    }\n\n    [Obsolete(\"Consider using SetValidationErrorResult instead.\")]\n    protected abstract void SetTwoFactorResult(T context, Dictionary<string, object> customResponse);\n\n    [Obsolete(\"Consider using SetValidationErrorResult instead.\")]\n    protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);\n\n    /// <summary>\n    /// This consumes the ValidationErrorResult property in the CustomValidatorRequestContext and sets\n    /// it appropriately in the response object for the token and grant validators.\n    /// </summary>\n    /// <param name=\"context\">The current grant or token context</param>\n    /// <param name=\"requestContext\">The modified request context containing material used to build the response object</param>\n    protected abstract void SetValidationErrorResult(T context, CustomValidatorRequestContext requestContext);\n\n    protected abstract Task SetSuccessResult(T context, User user, List<Claim> claims,\n        Dictionary<string, object> customResponse);\n\n    protected abstract ClaimsPrincipal GetSubject(T context);\n\n    private async Task ResetFailedAuthDetailsAsync(User user)\n    {\n        // Early escape if db hit not necessary\n        if (user == null || user.FailedLoginCount == 0)\n        {\n            return;\n        }\n\n        user.FailedLoginCount = 0;\n        user.RevisionDate = DateTime.UtcNow;\n        await _userRepository.ReplaceAsync(user);\n    }\n\n    private async Task UpdateFailedAuthDetailsAsync(User user)\n    {\n        if (user == null)\n        {\n            return;\n        }\n\n        var utcNow = DateTime.UtcNow;\n        user.FailedLoginCount = ++user.FailedLoginCount;\n        user.LastFailedLoginDate = user.RevisionDate = utcNow;\n        await _userRepository.ReplaceAsync(user);\n    }\n\n    private async Task SendFailedTwoFactorEmail(User user, TwoFactorProviderType failedAttemptType)\n    {\n        await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemptType, DateTime.UtcNow,\n            CurrentContext.IpAddress);\n    }\n\n    private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user)\n    {\n        // Check current context/cache to see if user is in any organizations, avoids extra DB call if not\n        var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))\n            .ToList();\n\n        if (orgs.Count == 0)\n        {\n            return null;\n        }\n\n        return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user));\n    }\n\n    /// <summary>\n    /// Builds the claims that will be stored on the persisted grant.\n    /// These claims are supplemented by the claims in the ProfileService when the access token is returned to the client.\n    /// </summary>\n    /// <param name=\"user\">The authenticated user.</param>\n    /// <param name=\"context\">The current request context.</param>\n    /// <param name=\"device\">The device used for authentication.</param>\n    private List<Claim> BuildSubjectClaims(User user, T context, Device device)\n    {\n        // We are adding the security stamp claim to the list of claims that will be stored in the persisted grant.\n        // We need this because we check for changes in the stamp to determine if we need to invalidate token refresh requests,\n        // in the `ProfileService.IsActiveAsync` method.\n        // If we don't store the security stamp in the persisted grant, we won't have the previous value to compare against.\n        var claims = new List<Claim> { new Claim(Claims.SecurityStamp, user.SecurityStamp) };\n\n        if (device != null)\n        {\n            claims.Add(new Claim(Claims.Device, device.Identifier));\n            claims.Add(new Claim(Claims.DeviceType, device.Type.ToString()));\n        }\n\n        return claims;\n    }\n\n    /// <summary>\n    /// Builds the custom response that will be sent to the client upon successful authentication, which\n    /// includes the information needed for the client to initialize the user's account in state.\n    /// </summary>\n    /// <param name=\"user\">The authenticated user.</param>\n    /// <param name=\"context\">The current request context.</param>\n    /// <param name=\"device\">The device used for authentication.</param>\n    /// <param name=\"sendRememberToken\">Whether to send a 2FA remember token.</param>\n    private async Task<Dictionary<string, object>> BuildCustomResponse(User user, T context, Device device,\n        bool sendRememberToken)\n    {\n        var customResponse = new Dictionary<string, object>();\n        if (!string.IsNullOrWhiteSpace(user.PrivateKey))\n        {\n            // PrivateKey usage is now deprecated in favor of AccountKeys\n            customResponse.Add(\"PrivateKey\", user.PrivateKey);\n            var accountKeys = await _accountKeysQuery.Run(user);\n            customResponse.Add(\"AccountKeys\", new PrivateKeysResponseModel(accountKeys));\n        }\n\n        if (!string.IsNullOrWhiteSpace(user.Key))\n        {\n            // Key is deprecated in favor of UserDecryptionOptions.MasterPasswordUnlock.MasterKeyEncryptedUserKey\n            customResponse.Add(\"Key\", user.Key);\n        }\n\n        customResponse.Add(\"MasterPasswordPolicy\", await GetMasterPasswordPolicyAsync(user));\n        customResponse.Add(\"ForcePasswordReset\", user.ForcePasswordReset);\n        customResponse.Add(\"Kdf\", (byte)user.Kdf);\n        customResponse.Add(\"KdfIterations\", user.KdfIterations);\n        customResponse.Add(\"KdfMemory\", user.KdfMemory);\n        customResponse.Add(\"KdfParallelism\", user.KdfParallelism);\n        customResponse.Add(\"UserDecryptionOptions\",\n            await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context)));\n\n        if (sendRememberToken)\n        {\n            var token = await _userManager.GenerateTwoFactorTokenAsync(user,\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));\n            customResponse.Add(\"TwoFactorToken\", token);\n        }\n\n        return customResponse;\n    }\n\n#nullable enable\n    /// <summary>\n    /// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents\n    /// </summary>\n    private async Task<UserDecryptionOptions> CreateUserDecryptionOptionsAsync(User user, Device device,\n        ClaimsPrincipal subject)\n    {\n        var ssoConfig = await GetSsoConfigurationDataAsync(subject);\n        return await UserDecryptionOptionsBuilder\n            .ForUser(user)\n            .WithDevice(device)\n            .WithSso(ssoConfig)\n            .BuildAsync();\n    }\n\n    private async Task<SsoConfig?> GetSsoConfigurationDataAsync(ClaimsPrincipal subject)\n    {\n        var organizationClaim = subject?.FindFirstValue(\"organizationId\");\n\n        if (organizationClaim == null || !Guid.TryParse(organizationClaim, out var organizationId))\n        {\n            return null;\n        }\n\n        var ssoConfig = await SsoConfigRepository.GetByOrganizationIdAsync(organizationId);\n        if (ssoConfig == null)\n        {\n            return null;\n        }\n\n        return ssoConfig;\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/ClientVersionValidator.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement;\nusing Bit.Core.Models.Api;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\npublic interface IClientVersionValidator\n{\n    bool Validate(User user, CustomValidatorRequestContext requestContext);\n}\n\n/// <summary>\n/// This validator will use the Client Version on a request, which currently maps\n/// to the \"Bitwarden-Client-Version\" header, to determine if a user meets minimum\n/// required client version for issuing tokens on an old client. This is done to\n/// incentivize users to get on an updated client when their password encryption\n/// method has already been updated.\n///\n/// If the header is omitted, then the validator returns that this request is invalid.\n/// </summary>\npublic class ClientVersionValidator(\n    ICurrentContext currentContext)\n    : IClientVersionValidator\n{\n    private const string _upgradeMessage = \"Please update your app to continue using Bitwarden\";\n    private const string _noUserMessage = \"No user found while trying to validate client version\";\n    private const string _versionHeaderMissing = \"No client version header found, required to prevent encryption errors. Please confirm your client is supplying the header: \\\"Bitwarden-Client-Version\\\"\";\n\n    public bool Validate(User? user, CustomValidatorRequestContext requestContext)\n    {\n        // Do this nullish check because the base request validator currently is not\n        // strict null checking. Once that gets fixed then we can see about making\n        // the user not nullish checked. If they are null then the validator should fail.\n        if (user == null)\n        {\n            FillRequestContextWithErrorData(requestContext, \"no_user\", _noUserMessage);\n            return false;\n        }\n\n        Version? clientVersion = currentContext.ClientVersion;\n\n        // Deny access if the client version headers are missing.\n        // We want to establish a strict contract with clients that if they omit this header,\n        // then the server cannot guarantee that a client won't do harm to a user's data\n        // with stale encryption architecture.\n        if (clientVersion == null)\n        {\n            FillRequestContextWithErrorData(requestContext, \"version_header_missing\", _versionHeaderMissing);\n            return false;\n        }\n\n        // Determine the minimum version client that a user needs. If no V2 encryption detected then\n        // no validation needs to occur, which is why min version number can be null.\n        Version? minVersion = user.HasV2Encryption() ? Constants.MinimumClientVersionForV2Encryption : null;\n\n        // If min version is null then we know that the user had an encryption\n        // configuration that doesn't require a minimum version. Allowing through.\n        if (minVersion == null)\n        {\n            return true;\n        }\n\n        if (clientVersion < minVersion)\n        {\n            FillRequestContextWithErrorData(requestContext, \"invalid_client_version\", _upgradeMessage);\n            return false;\n        }\n\n        return true;\n    }\n\n    private void FillRequestContextWithErrorData(\n        CustomValidatorRequestContext requestContext,\n        string errorId,\n        string errorMessage)\n    {\n        requestContext.ValidationErrorResult = new ValidationResult\n        {\n            Error = errorId,\n            ErrorDescription = errorMessage,\n            IsError = true\n        };\n        requestContext.CustomResponse = new Dictionary<string, object>\n        {\n            { \"ErrorModel\", new ErrorResponseModel(errorMessage) }\n        };\n    }\n}\n\n\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs",
    "content": "﻿using System.Diagnostics;\nusing System.Security.Claims;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Platform.Installations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer.Extensions;\nusing Duende.IdentityServer.Validation;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\npublic class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,\n    ICustomTokenRequestValidator\n{\n    private readonly UserManager<User> _userManager;\n    private readonly IUpdateInstallationCommand _updateInstallationCommand;\n    private readonly Version _denyLegacyUserMinimumVersion = new(Constants.DenyLegacyUserMinimumVersion);\n\n    public CustomTokenRequestValidator(\n        UserManager<User> userManager,\n        IUserService userService,\n        IEventService eventService,\n        IDeviceValidator deviceValidator,\n        ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,\n        ISsoRequestValidator ssoRequestValidator,\n        IOrganizationUserRepository organizationUserRepository,\n        ILogger<CustomTokenRequestValidator> logger,\n        ICurrentContext currentContext,\n        GlobalSettings globalSettings,\n        IUserRepository userRepository,\n        IPolicyService policyService,\n        IFeatureService featureService,\n        ISsoConfigRepository ssoConfigRepository,\n        IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,\n        IUpdateInstallationCommand updateInstallationCommand,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IAuthRequestRepository authRequestRepository,\n        IMailService mailService,\n        IUserAccountKeysQuery userAccountKeysQuery,\n        IClientVersionValidator clientVersionValidator)\n        : base(\n            userManager,\n            userService,\n            eventService,\n            deviceValidator,\n            twoFactorAuthenticationValidator,\n            ssoRequestValidator,\n            organizationUserRepository,\n            logger,\n            currentContext,\n            globalSettings,\n            userRepository,\n            policyService,\n            featureService,\n            ssoConfigRepository,\n            userDecryptionOptionsBuilder,\n            policyRequirementQuery,\n            authRequestRepository,\n            mailService,\n            userAccountKeysQuery,\n            clientVersionValidator)\n    {\n        _userManager = userManager;\n        _updateInstallationCommand = updateInstallationCommand;\n    }\n\n    public async Task ValidateAsync(CustomTokenRequestValidationContext context)\n    {\n        Debug.Assert(context.Result is not null);\n        if (context.Result.ValidatedRequest.GrantType == \"refresh_token\")\n        {\n            // Force legacy users to the web for migration\n            if (await _userService.IsLegacyUser(GetSubject(context)?.GetSubjectId()) &&\n                (context.Result.ValidatedRequest.ClientId != \"web\" || CurrentContext.ClientVersion >= _denyLegacyUserMinimumVersion))\n            {\n                await FailAuthForLegacyUserAsync(null, context);\n                return;\n            }\n        }\n\n        string[] allowedGrantTypes = [\"authorization_code\", \"client_credentials\"];\n        string clientId = context.Result.ValidatedRequest.ClientId;\n        if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)\n            || clientId.StartsWith(\"organization\")\n            || clientId.StartsWith(\"installation\")\n            || clientId.StartsWith(\"internal\")\n            || context.Result.ValidatedRequest.Client.AllowedScopes.Contains(ApiScopes.ApiSecrets))\n        {\n            if (context.Result.ValidatedRequest.Client.Properties.TryGetValue(\"encryptedPayload\", out var payload) &&\n                !string.IsNullOrWhiteSpace(payload))\n            {\n                context.Result.CustomResponse = new Dictionary<string, object> { { \"encrypted_payload\", payload } };\n\n            }\n            if (context.Result.ValidatedRequest.ClientId.StartsWith(\"installation\"))\n            {\n                await RecordActivityForInstallation(clientId.Split(\".\")[1]);\n            }\n            return;\n        }\n        await ValidateAsync(context, context.Result.ValidatedRequest, new CustomValidatorRequestContext { });\n    }\n\n    protected async override Task<bool> ValidateContextAsync(CustomTokenRequestValidationContext context,\n        CustomValidatorRequestContext validatorContext)\n    {\n        Debug.Assert(context.Result is not null);\n        var email = context.Result.ValidatedRequest.Subject?.GetDisplayName()\n                    ?? context.Result.ValidatedRequest.ClientClaims\n                        ?.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email)?.Value;\n        if (!string.IsNullOrWhiteSpace(email))\n        {\n            validatorContext.User = await _userManager.FindByEmailAsync(email);\n        }\n        return validatorContext.User != null;\n    }\n\n    protected override Task SetSuccessResult(CustomTokenRequestValidationContext context, User user,\n        List<Claim> claims, Dictionary<string, object> customResponse)\n    {\n        Debug.Assert(context.Result is not null);\n        context.Result.CustomResponse = customResponse;\n        if (claims?.Any() ?? false)\n        {\n            context.Result.ValidatedRequest.Client.AlwaysSendClientClaims = true;\n            context.Result.ValidatedRequest.Client.ClientClaimsPrefix = string.Empty;\n            foreach (var claim in claims)\n            {\n                context.Result.ValidatedRequest.ClientClaims.Add(claim);\n            }\n        }\n        if (context.Result.CustomResponse == null || user.MasterPassword != null)\n        {\n            return Task.CompletedTask;\n        }\n\n        // KeyConnector responses below\n\n        // Apikey login\n        if (context.Result.ValidatedRequest.GrantType == \"client_credentials\")\n        {\n            if (user.UsesKeyConnector)\n            {\n                // KeyConnectorUrl is configured in the CLI client, we just need to tell the client to use it\n                context.Result.CustomResponse[\"ApiUseKeyConnector\"] = true;\n            }\n        }\n\n        return Task.CompletedTask;\n    }\n\n    protected override ClaimsPrincipal? GetSubject(CustomTokenRequestValidationContext context)\n    {\n        Debug.Assert(context.Result is not null);\n        return context.Result.ValidatedRequest.Subject;\n    }\n\n    [Obsolete(\"Consider using SetGrantValidationErrorResult instead.\")]\n    protected override void SetTwoFactorResult(CustomTokenRequestValidationContext context,\n        Dictionary<string, object> customResponse)\n    {\n        Debug.Assert(context.Result is not null);\n        context.Result.Error = \"invalid_grant\";\n        context.Result.ErrorDescription = \"Two factor required.\";\n        context.Result.IsError = true;\n        context.Result.CustomResponse = customResponse;\n    }\n\n    [Obsolete(\"Consider using SetGrantValidationErrorResult instead.\")]\n    protected override void SetErrorResult(CustomTokenRequestValidationContext context,\n        Dictionary<string, object> customResponse)\n    {\n        Debug.Assert(context.Result is not null);\n        context.Result.Error = \"invalid_grant\";\n        context.Result.IsError = true;\n        context.Result.CustomResponse = customResponse;\n    }\n\n    protected override void SetValidationErrorResult(\n        CustomTokenRequestValidationContext context, CustomValidatorRequestContext requestContext)\n    {\n        Debug.Assert(context.Result is not null);\n        context.Result.Error = requestContext.ValidationErrorResult.Error;\n        context.Result.IsError = requestContext.ValidationErrorResult.IsError;\n        context.Result.ErrorDescription = requestContext.ValidationErrorResult.ErrorDescription;\n        context.Result.CustomResponse = requestContext.CustomResponse;\n    }\n\n    /// <summary>\n    /// To help mentally separate organizations that self host from abandoned\n    /// organizations we hook in to the token refresh event for installations\n    /// to write a simple `DateTime.Now` to the database.\n    /// </summary>\n    /// <remarks>\n    /// This works well because installations don't phone home very often.\n    /// Currently self hosted installations only refresh tokens every 24\n    /// hours or so for the sake of hooking in to cloud's push relay service.\n    /// If installations ever start refreshing tokens more frequently we may need to\n    /// adjust this to avoid making a bunch of unnecessary database calls!\n    /// </remarks>\n    private async Task RecordActivityForInstallation(string? installationIdString)\n    {\n        if (!Guid.TryParse(installationIdString, out var installationId))\n        {\n            return;\n        }\n        await _updateInstallationCommand.UpdateLastActivityDateAsync(installationId);\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Reflection;\nusing Bit.Core;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Identity.IdentityServer.Enums;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Validation;\nusing Microsoft.Extensions.Caching.Distributed;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\npublic class DeviceValidator(\n    IDeviceService deviceService,\n    IDeviceRepository deviceRepository,\n    GlobalSettings globalSettings,\n    IMailService mailService,\n    ICurrentContext currentContext,\n    IUserService userService,\n    IDistributedCache distributedCache,\n    ITwoFactorEmailService twoFactorEmailService,\n    ILogger<DeviceValidator> logger) : IDeviceValidator\n{\n    private readonly IDeviceService _deviceService = deviceService;\n    private readonly IDeviceRepository _deviceRepository = deviceRepository;\n    private readonly GlobalSettings _globalSettings = globalSettings;\n    private readonly IMailService _mailService = mailService;\n    private readonly ICurrentContext _currentContext = currentContext;\n    private readonly IUserService _userService = userService;\n    private readonly IDistributedCache distributedCache = distributedCache;\n    private readonly ILogger<DeviceValidator> _logger = logger;\n    private readonly ITwoFactorEmailService _twoFactorEmailService = twoFactorEmailService;\n\n    private const string PasswordGrantType = \"password\";\n\n    public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context)\n    {\n        // Parse device from request and return early if no device information is provided\n        var requestDevice = context.Device ?? GetDeviceFromRequest(request);\n        // If context.Device and request device information are null then return error\n        // backwards compatibility -- check if user is null\n        // PM-13340: Null user check happens in the HandleNewDeviceVerificationAsync method and can be removed from here\n        if (requestDevice == null || context.User == null)\n        {\n            (context.ValidationErrorResult, context.CustomResponse) =\n                BuildDeviceErrorResult(DeviceValidationResultType.NoDeviceInformationProvided);\n            return false;\n        }\n\n        // Check if the request has a NewDeviceOtp, if it does we can assume it is an unknown device\n        // that has already been prompted for new device verification so we don't\n        // have to hit the database to check if the device is known to avoid unnecessary database calls.\n        if (!RequestHasNewDeviceVerificationOtp(request))\n        {\n            var knownDevice = await GetKnownDeviceAsync(context.User, requestDevice);\n            // if the device is know then we return the device fetched from the database\n            // returning the database device is important for TDE\n            if (knownDevice != null)\n            {\n                context.KnownDevice = true;\n                context.Device = knownDevice;\n                return true;\n            }\n        }\n\n        // The device is either unknown or the request has a NewDeviceOtp (implies unknown device)\n\n        var rawAuthRequestId = request.Raw[\"AuthRequest\"]?.ToLowerInvariant();\n        var isAuthRequest = !string.IsNullOrEmpty(rawAuthRequestId);\n\n        // Device unknown, but if we are in an auth request flow, this is not valid\n        // as we only support auth request authN requests on known devices\n        // Note: we re-use the resource owner password flow for auth requests\n        if (request.GrantType == GrantType.ResourceOwnerPassword && isAuthRequest)\n        {\n            (context.ValidationErrorResult, context.CustomResponse) =\n                BuildDeviceErrorResult(DeviceValidationResultType.AuthRequestFlowUnknownDevice);\n            return false;\n        }\n\n        // Enforce new device verification for resource owner password flow (just normal password flow)\n        if (request.GrantType == GrantType.ResourceOwnerPassword &&\n            context is { TwoFactorRequired: false, SsoRequired: false } &&\n            _globalSettings.EnableNewDeviceVerification)\n        {\n            var validationResult = await HandleNewDeviceVerificationAsync(context.User, request);\n            if (validationResult != DeviceValidationResultType.Success)\n            {\n                (context.ValidationErrorResult, context.CustomResponse) =\n                    BuildDeviceErrorResult(validationResult);\n                if (validationResult == DeviceValidationResultType.NewDeviceVerificationRequired)\n                {\n                    await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(context.User);\n                }\n                return false;\n            }\n        }\n\n        // At this point we have established either new device verification is not required or the NewDeviceOtp is valid,\n        // so we save the device to the database and proceed with authentication\n        requestDevice.UserId = context.User.Id;\n        await _deviceService.SaveAsync(requestDevice);\n        context.Device = requestDevice;\n\n        if (!_globalSettings.DisableEmailNewDevice)\n        {\n            await SendNewDeviceLoginEmail(context.User, requestDevice);\n        }\n\n        return true;\n    }\n\n    /// <summary>\n    /// Checks the if the requesting deice requires new device verification otherwise saves the device to the database\n    /// </summary>\n    /// <param name=\"user\">user attempting to authenticate</param>\n    /// <param name=\"ValidatedRequest\">The Request is used to check for the NewDeviceOtp and for the raw device data</param>\n    /// <returns>returns deviceValidationResultType</returns>\n    private async Task<DeviceValidationResultType> HandleNewDeviceVerificationAsync(User user, ValidatedRequest request)\n    {\n        // currently unreachable due to backward compatibility\n        // PM-13340: will address this\n        if (user == null)\n        {\n            return DeviceValidationResultType.InvalidUser;\n        }\n\n        // Has the User opted out of new device verification\n        if (!user.VerifyDevices)\n        {\n            return DeviceValidationResultType.Success;\n        }\n\n        // User is newly registered, so don't require new device verification\n        var createdSpan = DateTime.UtcNow - user.CreationDate;\n        if (createdSpan < TimeSpan.FromHours(24))\n        {\n            return DeviceValidationResultType.Success;\n        }\n\n        // CS exception flow\n        // Check cache for user information\n        var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, user.Id.ToString());\n        var cacheValue = await distributedCache.GetAsync(cacheKey);\n        if (cacheValue != null)\n        {\n            // if found in cache return success result and remove from cache\n            await distributedCache.RemoveAsync(cacheKey);\n            _logger.LogInformation(\"New device verification exception for user {UserId} found in cache\", user.Id);\n            return DeviceValidationResultType.Success;\n        }\n\n        // parse request for NewDeviceOtp to validate\n        var newDeviceOtp = request.Raw[\"NewDeviceOtp\"]?.ToString();\n        // we only check null here since an empty OTP will be considered an incorrect OTP\n        if (newDeviceOtp != null)\n        {\n            // verify the NewDeviceOtp\n            var otpValid = await _userService.VerifyOTPAsync(user, newDeviceOtp);\n            if (otpValid)\n            {\n                // In order to get here they would have to have access to their email so we verify it if it's not already\n                if (!user.EmailVerified)\n                {\n                    user.EmailVerified = true;\n                    await _userService.SaveUserAsync(user);\n                }\n                return DeviceValidationResultType.Success;\n            }\n            return DeviceValidationResultType.InvalidNewDeviceOtp;\n        }\n\n        // if a user has no devices they are assumed to be newly registered user which does not require new device verification\n        var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);\n        if (devices.Count == 0)\n        {\n            return DeviceValidationResultType.Success;\n        }\n\n        // if we get to here then we need to send a new device verification email\n        return DeviceValidationResultType.NewDeviceVerificationRequired;\n    }\n\n    /// <summary>\n    /// Sends an email whenever the user logs in from a new device. Will not send to a user who's account\n    /// is less than 10 minutes old. We assume an account that is less than 10 minutes old is new and does\n    /// not need an email stating they just logged in.\n    /// </summary>\n    /// <param name=\"user\">user logging in</param>\n    /// <param name=\"requestDevice\">current device being approved to login</param>\n    /// <returns>void</returns>\n    private async Task SendNewDeviceLoginEmail(User user, Device requestDevice)\n    {\n        // Ensure that the user doesn't receive a \"new device\" email on the first login\n        var now = DateTime.UtcNow;\n        if (now - user.CreationDate > TimeSpan.FromMinutes(10))\n        {\n            var deviceType = requestDevice.Type.GetType().GetMember(requestDevice.Type.ToString())\n                .FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();\n            await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,\n                _currentContext.IpAddress);\n        }\n    }\n\n    public async Task<Device> GetKnownDeviceAsync(User user, Device device)\n    {\n        if (user == null || device == null)\n        {\n            return null;\n        }\n\n        return await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id);\n    }\n\n    public static Device GetDeviceFromRequest(ValidatedRequest request)\n    {\n        var deviceIdentifier = request.Raw[\"DeviceIdentifier\"]?.ToString();\n        var requestDeviceType = request.Raw[\"DeviceType\"]?.ToString();\n        var deviceName = request.Raw[\"DeviceName\"]?.ToString();\n        var devicePushToken = request.Raw[\"DevicePushToken\"]?.ToString();\n\n        if (string.IsNullOrWhiteSpace(deviceIdentifier) ||\n            string.IsNullOrWhiteSpace(requestDeviceType) ||\n            string.IsNullOrWhiteSpace(deviceName) ||\n            !Enum.TryParse(requestDeviceType, out DeviceType parsedDeviceType))\n        {\n            return null;\n        }\n\n        return new Device\n        {\n            Identifier = deviceIdentifier,\n            Name = deviceName,\n            Type = parsedDeviceType,\n            PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken\n        };\n    }\n\n    /// <summary>\n    /// Checks request for the NewDeviceOtp field to determine if a new device verification is required.\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    public static bool RequestHasNewDeviceVerificationOtp(ValidatedRequest request)\n    {\n        return !string.IsNullOrEmpty(request.Raw[\"NewDeviceOtp\"]?.ToString());\n    }\n\n    /// <summary>\n    /// This builds builds the error result for the various grant and token validators. The Success type is not used here.\n    /// </summary>\n    /// <param name=\"errorType\">DeviceValidationResultType that is an error, success type is not used.</param>\n    /// <returns>validation result used by grant and token validators, and the custom response for either Grant or Token response objects.</returns>\n    private static (Duende.IdentityServer.Validation.ValidationResult, Dictionary<string, object>) BuildDeviceErrorResult(DeviceValidationResultType errorType)\n    {\n        var result = new Duende.IdentityServer.Validation.ValidationResult\n        {\n            IsError = true,\n            Error = \"device_error\",\n        };\n        var customResponse = new Dictionary<string, object>();\n        switch (errorType)\n        {\n            /*\n             * The ErrorMessage is brittle and is used to control the flow in the clients. Do not change them without updating the client as well.\n             * There is a backwards compatibility issue as well: if you make a change on the clients then ensure that they are backwards\n             * compatible.\n             */\n            case DeviceValidationResultType.InvalidUser:\n                result.ErrorDescription = \"Invalid user\";\n                customResponse.Add(\"ErrorModel\", new ErrorResponseModel(\"invalid user\"));\n                break;\n            case DeviceValidationResultType.InvalidNewDeviceOtp:\n                result.ErrorDescription = \"Invalid New Device OTP\";\n                customResponse.Add(\"ErrorModel\", new ErrorResponseModel(\"invalid new device otp\"));\n                break;\n            case DeviceValidationResultType.NewDeviceVerificationRequired:\n                result.ErrorDescription = \"New device verification required\";\n                customResponse.Add(\"ErrorModel\", new ErrorResponseModel(\"new device verification required\"));\n                break;\n            case DeviceValidationResultType.NoDeviceInformationProvided:\n                result.ErrorDescription = \"No device information provided\";\n                customResponse.Add(\"ErrorModel\", new ErrorResponseModel(\"no device information provided\"));\n                break;\n            case DeviceValidationResultType.AuthRequestFlowUnknownDevice:\n                result.ErrorDescription = \"Auth requests are not supported on unknown devices\";\n                customResponse.Add(\"ErrorModel\", new ErrorResponseModel(\"auth request flow unsupported on unknown device\"));\n                break;\n        }\n        return (result, customResponse);\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/IDeviceValidator.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\npublic interface IDeviceValidator\n{\n    /// <summary>\n    /// Fetches device from the database using the Device Identifier and the User Id to know if the user\n    /// has ever tried to authenticate with this specific instance of Bitwarden.\n    /// </summary>\n    /// <param name=\"user\">user attempting to authenticate</param>\n    /// <param name=\"device\">current instance of Bitwarden the user is interacting with</param>\n    /// <returns>null or Device</returns>\n    Task<Device> GetKnownDeviceAsync(User user, Device device);\n\n    /// <summary>\n    /// Validate the requesting device. Modifies the ValidatorRequestContext with error result if any.\n    /// </summary>\n    /// <param name=\"request\">The Request is used to check for the NewDeviceOtp and for the raw device data</param>\n    /// <param name=\"context\">Contains two factor and sso context that are important for decisions on new device verification</param>\n    /// <returns>returns true if device is valid and no other action required; if false modifies the context with an error result to be returned;</returns>\n    Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context);\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/ISsoRequestValidator.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\n/// <summary>\n/// Validates whether a user is required to authenticate via SSO based on organization policies.\n/// </summary>\npublic interface ISsoRequestValidator\n{\n    /// <summary>\n    /// Validates the SSO requirement for a user attempting to authenticate. Sets the error state in the <see cref=\"CustomValidatorRequestContext.CustomResponse\"/> if SSO is required.\n    /// </summary>\n    /// <param name=\"user\">The user attempting to authenticate.</param>\n    /// <param name=\"request\">The token request containing grant type and other authentication details.</param>\n    /// <param name=\"context\">The validator context to be updated with SSO requirement status and error results if applicable.</param>\n    /// <returns>true if the user can proceed with authentication; false if SSO is required and the user must be redirected to SSO flow.</returns>\n    Task<bool> ValidateAsync(User user, ValidatedTokenRequest request, CustomValidatorRequestContext context);\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs",
    "content": "﻿\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Entities;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\npublic interface ITwoFactorAuthenticationValidator\n{\n    /// <summary>\n    /// Check if the user is required to use two-factor authentication to login. This is based on the user's\n    /// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type.\n    /// Client credentials and webauthn grant types do not require two-factor authentication.\n    /// </summary>\n    /// <param name=\"user\">the active user for the request</param>\n    /// <param name=\"request\">the request that contains the grant types</param>\n    /// <returns>boolean</returns>\n    Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request);\n    /// <summary>\n    /// Builds the two-factor authentication result for the user based on the available two-factor providers\n    /// from either their user account or Organization.\n    /// </summary>\n    /// <param name=\"user\">user trying to login</param>\n    /// <param name=\"organization\">organization associated with the user; Can be null</param>\n    /// <returns>Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value</returns>\n    Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization);\n    /// <summary>\n    /// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses\n    /// organization duo, it will use the organization duo token provider to verify the token.\n    /// </summary>\n    /// <param name=\"user\">the active User</param>\n    /// <param name=\"organization\">organization of user; can be null</param>\n    /// <param name=\"twoFactorProviderType\">Two Factor Provider to use to verify the token</param>\n    /// <param name=\"token\">secret passed from the user and consumed by the two-factor provider's verify method</param>\n    /// <returns>boolean</returns>\n    Task<bool> VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token);\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Validation;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\npublic class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>,\n    IResourceOwnerPasswordValidator\n{\n    private UserManager<User> _userManager;\n    private readonly ICurrentContext _currentContext;\n    private readonly IAuthRequestRepository _authRequestRepository;\n    private readonly IDeviceValidator _deviceValidator;\n    public ResourceOwnerPasswordValidator(\n        UserManager<User> userManager,\n        IUserService userService,\n        IEventService eventService,\n        IDeviceValidator deviceValidator,\n        ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,\n        ISsoRequestValidator ssoRequestValidator,\n        IOrganizationUserRepository organizationUserRepository,\n        ILogger<ResourceOwnerPasswordValidator> logger,\n        ICurrentContext currentContext,\n        GlobalSettings globalSettings,\n        IAuthRequestRepository authRequestRepository,\n        IUserRepository userRepository,\n        IPolicyService policyService,\n        IFeatureService featureService,\n        ISsoConfigRepository ssoConfigRepository,\n        IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IMailService mailService,\n        IUserAccountKeysQuery userAccountKeysQuery,\n        IClientVersionValidator clientVersionValidator)\n        : base(\n            userManager,\n            userService,\n            eventService,\n            deviceValidator,\n            twoFactorAuthenticationValidator,\n            ssoRequestValidator,\n            organizationUserRepository,\n            logger,\n            currentContext,\n            globalSettings,\n            userRepository,\n            policyService,\n            featureService,\n            ssoConfigRepository,\n            userDecryptionOptionsBuilder,\n            policyRequirementQuery,\n            authRequestRepository,\n            mailService,\n            userAccountKeysQuery,\n            clientVersionValidator)\n    {\n        _userManager = userManager;\n        _currentContext = currentContext;\n        _authRequestRepository = authRequestRepository;\n        _deviceValidator = deviceValidator;\n    }\n\n    public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)\n    {\n        var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());\n        // We want to keep this device around incase the device is new for the user\n        var requestDevice = DeviceValidator.GetDeviceFromRequest(context.Request);\n        var knownDevice = await _deviceValidator.GetKnownDeviceAsync(user, requestDevice);\n        var validatorContext = new CustomValidatorRequestContext\n        {\n            User = user,\n            KnownDevice = knownDevice != null,\n            Device = knownDevice ?? requestDevice,\n        };\n\n        await ValidateAsync(context, context.Request, validatorContext);\n    }\n\n    protected async override Task<bool> ValidateContextAsync(ResourceOwnerPasswordValidationContext context,\n        CustomValidatorRequestContext validatorContext)\n    {\n        if (string.IsNullOrWhiteSpace(context.UserName) || validatorContext.User == null)\n        {\n            return false;\n        }\n\n        var authRequestId = context.Request.Raw[\"AuthRequest\"]?.ToLowerInvariant();\n        if (!string.IsNullOrEmpty(authRequestId))\n        {\n            // only allow valid guids\n            if (!Guid.TryParse(authRequestId, out var authRequestGuid))\n            {\n                return false;\n            }\n\n            var authRequest = await _authRequestRepository.GetByIdAsync(authRequestGuid);\n\n            if (authRequest == null)\n            {\n                return false;\n            }\n\n            // Auth request is non-null so validate it\n            if (authRequest.IsValidForAuthentication(validatorContext.User.Id, context.Password))\n            {\n                // We save the validated auth request so that we can set it's authentication date\n                // later on only upon successful authentication.\n                // For example, 2FA requires a resubmission so we can't mark the auth request\n                // as authenticated here.\n                validatorContext.ValidatedAuthRequest = authRequest;\n                return true;\n            }\n\n            return false;\n        }\n\n        if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))\n        {\n            return false;\n        }\n        return true;\n    }\n\n    protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,\n        List<Claim> claims, Dictionary<string, object> customResponse)\n    {\n        context.Result = new GrantValidationResult(user.Id.ToString(), \"Application\",\n            identityProvider: Constants.IdentityProvider,\n            claims: claims.Count > 0 ? claims : null,\n            customResponse: customResponse);\n        return Task.CompletedTask;\n    }\n\n    [Obsolete(\"Consider using SetGrantValidationErrorResult instead.\")]\n    protected override void SetTwoFactorResult(ResourceOwnerPasswordValidationContext context,\n        Dictionary<string, object> customResponse)\n    {\n        context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, \"Two factor required.\",\n            customResponse);\n    }\n\n    [Obsolete(\"Consider using SetGrantValidationErrorResult instead.\")]\n    protected override void SetErrorResult(ResourceOwnerPasswordValidationContext context,\n        Dictionary<string, object> customResponse)\n    {\n        context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse);\n    }\n\n    protected override void SetValidationErrorResult(\n        ResourceOwnerPasswordValidationContext context, CustomValidatorRequestContext requestContext)\n    {\n        context.Result = new GrantValidationResult\n        {\n            Error = requestContext.ValidationErrorResult.Error,\n            ErrorDescription = requestContext.ValidationErrorResult.ErrorDescription,\n            IsError = true,\n            CustomResponse = requestContext.CustomResponse\n        };\n    }\n\n    protected override ClaimsPrincipal GetSubject(ResourceOwnerPasswordValidationContext context)\n    {\n        return context.Result.Subject;\n    }\n\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/SendAccess/ISendAuthenticationMethodValidator.cs",
    "content": "﻿using Bit.Core.Tools.Models.Data;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;\n\npublic interface ISendAuthenticationMethodValidator<T> where T : SendAuthenticationMethod\n{\n    /// <summary>\n    /// </summary>\n    /// <param name=\"context\">request context</param>\n    /// <param name=\"authMethod\">SendAuthenticationRecord that contains the information to be compared against the context</param>\n    /// <param name=\"sendId\">the sendId being accessed</param>\n    /// <returns>returns the result of the validation; A failed result will be an error a successful will contain the claims and a success</returns>\n    Task<GrantValidationResult> ValidateRequestAsync(ExtensionGrantValidationContext context, T authMethod, Guid sendId);\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs",
    "content": "﻿using Bit.Core.Auth.Identity.TokenProviders;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;\n\n/// <summary>\n/// String constants for the Send Access user feature\n/// Most of these need to be synced with the `bitwarden-auth` crate in the SDK.\n/// There is snapshot testing to help ensure this.\n/// </summary>\npublic static class SendAccessConstants\n{\n    /// <summary>\n    /// A catch all error type for send access related errors. Used mainly in the <see cref=\"GrantValidationResult.CustomResponse\"/>\n    /// </summary>\n    public const string SendAccessError = \"send_access_error_type\";\n    public static class TokenRequest\n    {\n        /// <summary>\n        /// used to fetch Send from database.\n        /// </summary>\n        public const string SendId = \"send_id\";\n        /// <summary>\n        /// used to validate Send protected passwords\n        /// </summary>\n        public const string ClientB64HashedPassword = \"password_hash_b64\";\n        /// <summary>\n        /// email used to see if email is associated with the Send\n        /// </summary>\n        public const string Email = \"email\";\n        /// <summary>\n        /// Otp code sent to email associated with the Send\n        /// </summary>\n        public const string Otp = \"otp\";\n    }\n\n    public static class SendIdGuidValidatorResults\n    {\n        /// <summary>\n        /// The <see cref=\"TokenRequest.SendId\"/> in the request is a valid GUID and the request is well formed. Not returned in any response.\n        /// </summary>\n        public const string ValidSendGuid = \"valid_send_guid\";\n        /// <summary>\n        /// The <see cref=\"TokenRequest.SendId\"/> is missing from the request.\n        /// </summary>\n        public const string SendIdRequired = \"send_id_required\";\n        /// <summary>\n        /// The <see cref=\"TokenRequest.SendId\"/> is invalid, does not match a known send.\n        /// </summary>\n        public const string InvalidSendId = \"send_id_invalid\";\n    }\n\n    public static class PasswordValidatorResults\n    {\n        /// <summary>\n        /// The <see cref=\"TokenRequest.ClientB64HashedPassword\"/> does not match the send's password hash.\n        /// </summary>\n        public const string RequestPasswordDoesNotMatch = \"password_hash_b64_invalid\";\n        /// <summary>\n        /// The <see cref=\"TokenRequest.ClientB64HashedPassword\"/> is missing from the request.\n        /// </summary>\n        public const string RequestPasswordIsRequired = \"password_hash_b64_required\";\n    }\n\n    public static class EmailOtpValidatorResults\n    {\n        /// <summary>\n        /// Represents the error code indicating that an email address is required.\n        /// </summary>\n        public const string EmailRequired = \"email_required\";\n        /// <summary>\n        /// Represents the status indicating that both email and OTP are required, and the OTP has been sent.\n        /// </summary>\n        public const string EmailAndOtpRequired = \"email_and_otp_required\";\n    }\n\n    /// <summary>\n    /// These are the constants for the OTP token that is generated during the email otp authentication process.\n    /// These items are required by <see cref=\"IOtpTokenProvider{TOptions}\"/> to aid in the creation of a unique lookup key.\n    /// Look up key format is: {TokenProviderName}_{Purpose}_{TokenUniqueIdentifier}\n    /// </summary>\n    public static class OtpToken\n    {\n        public const string TokenProviderName = \"send_access\";\n        public const string Purpose = \"email_otp\";\n        /// <summary>\n        /// This will be send_id {0} and email {1}\n        /// </summary>\n        public const string TokenUniqueIdentifier = \"{0}_{1}\";\n    }\n\n    public static class OtpEmail\n    {\n        public const string Subject = \"Your Bitwarden Send verification code is {0}\";\n    }\n\n    /// <summary>\n    /// We use these static strings to help guide the enumeration protection logic.\n    /// </summary>\n    public static class EnumerationProtection\n    {\n        public const string Guid = \"guid\";\n        public const string Password = \"password\";\n        public const string Email = \"email\";\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.SendFeatures.Queries.Interfaces;\nusing Bit.Core.Utilities;\nusing Bit.Identity.IdentityServer.Enums;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;\n\npublic class SendAccessGrantValidator(\n    ISendAuthenticationQuery _sendAuthenticationQuery,\n    ISendAuthenticationMethodValidator<NeverAuthenticate> _sendNeverAuthenticateValidator,\n    ISendAuthenticationMethodValidator<ResourcePassword> _sendPasswordRequestValidator,\n    ISendAuthenticationMethodValidator<EmailOtp> _sendEmailOtpRequestValidator) : IExtensionGrantValidator\n{\n    string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess;\n\n    private static readonly Dictionary<string, string> _sendGrantValidatorErrorDescriptions = new()\n    {\n        { SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired, $\"{SendAccessConstants.TokenRequest.SendId} is required.\" },\n        { SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, $\"{SendAccessConstants.TokenRequest.SendId} is invalid.\" }\n    };\n\n    public async Task ValidateAsync(ExtensionGrantValidationContext context)\n    {\n        var (sendIdGuid, result) = GetRequestSendId(context);\n        if (result != SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid)\n        {\n            context.Result = BuildErrorResult(result);\n            return;\n        }\n\n        // Look up send by id\n        var method = await _sendAuthenticationQuery.GetAuthenticationMethod(sendIdGuid);\n\n        switch (method)\n        {\n            case NeverAuthenticate never:\n                // null send scenario.\n                context.Result = await _sendNeverAuthenticateValidator.ValidateRequestAsync(context, never, sendIdGuid);\n                return;\n            case SendInaccessible:\n                // send exists but is not accessible (expired, disabled, max access exceeded, or past deletion date).\n                context.Result = new GrantValidationResult(\n                    TokenRequestErrors.InvalidGrant,\n                    SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId,\n                    new Dictionary<string, object>\n                    {\n                        { SendAccessConstants.SendAccessError, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId }\n                    });\n                return;\n            case NotAuthenticated:\n                // automatically issue access token\n                context.Result = BuildBaseSuccessResult(sendIdGuid);\n                return;\n            case ResourcePassword rp:\n                // Validate if the password is correct, or if we need to respond with a 400 stating a password is invalid or required.\n                context.Result = await _sendPasswordRequestValidator.ValidateRequestAsync(context, rp, sendIdGuid);\n                return;\n            case EmailOtp eo:\n                // Validate if the request has the correct email and OTP. If not, respond with a 400 and information about the failure.\n                context.Result = await _sendEmailOtpRequestValidator.ValidateRequestAsync(context, eo, sendIdGuid);\n                return;\n            default:\n                // shouldn’t ever hit this\n                throw new InvalidOperationException($\"Unknown auth method: {method.GetType()}\");\n        }\n    }\n\n    /// <summary>\n    /// tries to parse the send_id from the request.\n    /// If it is not present or invalid, sets the correct result error.\n    /// </summary>\n    /// <param name=\"context\">request context</param>\n    /// <returns>a parsed sendId Guid and success result or a Guid.Empty and error type otherwise</returns>\n    private static (Guid, string) GetRequestSendId(ExtensionGrantValidationContext context)\n    {\n        var request = context.Request.Raw;\n        var sendId = request.Get(SendAccessConstants.TokenRequest.SendId);\n\n        // if the sendId is null then the request is the wrong shape and the request is invalid\n        if (sendId == null)\n        {\n            return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired);\n        }\n        // the send_id is not null so the request is the correct shape, so we will attempt to parse it\n        try\n        {\n            var guidBytes = CoreHelpers.Base64UrlDecode(sendId);\n            var sendGuid = new Guid(guidBytes);\n            // Guid.Empty indicates an invalid send_id return invalid grant\n            if (sendGuid == Guid.Empty)\n            {\n                return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);\n            }\n            return (sendGuid, SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid);\n        }\n        catch\n        {\n            return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId);\n        }\n    }\n\n    /// <summary>\n    /// Builds an error result for the specified error type.\n    /// </summary>\n    /// <param name=\"error\">This error is a constant string from <see cref=\"SendAccessConstants.SendIdGuidValidatorResults\"/></param>\n    /// <returns>The error result.</returns>\n    private static GrantValidationResult BuildErrorResult(string error)\n    {\n        var customResponse = new Dictionary<string, object>\n            {\n                { SendAccessConstants.SendAccessError, error }\n            };\n\n        return error switch\n        {\n            // Request is the wrong shape\n            SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired => new GrantValidationResult(\n                                TokenRequestErrors.InvalidRequest,\n                                errorDescription: _sendGrantValidatorErrorDescriptions[error],\n                                customResponse),\n            // Request is correct shape but data is bad\n            SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId => new GrantValidationResult(\n                                TokenRequestErrors.InvalidGrant,\n                                errorDescription: _sendGrantValidatorErrorDescriptions[error],\n                                customResponse),\n            // should never get here\n            _ => new GrantValidationResult(TokenRequestErrors.InvalidRequest)\n        };\n    }\n\n    private static GrantValidationResult BuildBaseSuccessResult(Guid sendId)\n    {\n        var claims = new List<Claim>\n        {\n            new(Claims.SendAccessClaims.SendId, sendId.ToString()),\n            new(Claims.Type, IdentityClientType.Send.ToString())\n        };\n\n        return new GrantValidationResult(\n            subject: sendId.ToString(),\n            authenticationMethod: CustomGrantTypes.SendAccess,\n            claims: claims);\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs",
    "content": "﻿using System.Globalization;\nusing System.Security.Claims;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Identity.IdentityServer.Enums;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;\n\n/**\n* The error responses here do not fully match the standard for OAuth with respect to Invalid Request vs Invalid Grant. This is intended to better protect\n* against enumeration. We return Invalid Request for all errors related to the email and OTP, even if in some cases Invalid Grant might be more appropriate.\n*/\npublic class SendEmailOtpRequestValidator(\n    ILogger<SendEmailOtpRequestValidator> logger,\n    IOtpTokenProvider<DefaultOtpTokenProviderOptions> otpTokenProvider,\n    IMailService mailService) : ISendAuthenticationMethodValidator<EmailOtp>\n{\n\n    /// <summary>\n    /// static object that contains the error messages for the SendEmailOtpRequestValidator.\n    /// </summary>\n    private static readonly Dictionary<string, string> _sendEmailOtpValidatorErrorDescriptions = new()\n    {\n        { SendAccessConstants.EmailOtpValidatorResults.EmailRequired, $\"{SendAccessConstants.TokenRequest.Email} is required.\" },\n        { SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired, $\"{SendAccessConstants.TokenRequest.Email} and {SendAccessConstants.TokenRequest.Otp} are required.\" }\n    };\n\n    public async Task<GrantValidationResult> ValidateRequestAsync(ExtensionGrantValidationContext context, EmailOtp authMethod, Guid sendId)\n    {\n        var request = context.Request.Raw;\n        // get email\n        var email = request.Get(SendAccessConstants.TokenRequest.Email);\n\n        // It is an invalid request if the email is missing.\n        if (string.IsNullOrEmpty(email))\n        {\n            // Request is the wrong shape and doesn't contain an email field.\n            return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailRequired);\n        }\n\n        if (!authMethod.emails.Contains(email, StringComparer.OrdinalIgnoreCase))\n        {\n            return BuildErrorResult();\n        }\n\n        // get otp from request\n        var requestOtp = request.Get(SendAccessConstants.TokenRequest.Otp);\n        var uniqueIdentifierForTokenCache = string.Format(CultureInfo.InvariantCulture, SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email);\n        if (string.IsNullOrEmpty(requestOtp))\n        {\n            // Since the request doesn't have an OTP, generate one\n            var token = await otpTokenProvider.GenerateTokenAsync(\n                                    SendAccessConstants.OtpToken.TokenProviderName,\n                                    SendAccessConstants.OtpToken.Purpose,\n                                    uniqueIdentifierForTokenCache);\n\n            // Verify that the OTP is generated\n            if (string.IsNullOrEmpty(token))\n            {\n                logger.LogError(\"Failed to generate OTP for SendAccess\");\n                return BuildErrorResult();\n            }\n\n            await mailService.SendSendEmailOtpEmailAsync(\n                email,\n                token,\n                string.Format(CultureInfo.CurrentCulture, SendAccessConstants.OtpEmail.Subject, token));\n\n            return BuildErrorResult();\n        }\n\n        // validate request otp\n        var otpResult = await otpTokenProvider.ValidateTokenAsync(\n                                requestOtp,\n                                SendAccessConstants.OtpToken.TokenProviderName,\n                                SendAccessConstants.OtpToken.Purpose,\n                                uniqueIdentifierForTokenCache);\n\n        // If OTP is invalid return error result\n        if (!otpResult)\n        {\n            return BuildErrorResult();\n        }\n\n        return BuildSuccessResult(sendId, email!);\n    }\n\n    /// <summary>\n    /// Build the error response for the SendEmailOtpRequestValidator.\n    /// </summary>\n    /// <param name=\"error\">The error code to use for the validation result. This is defaulted to EmailAndOtpRequired if not specified because it is the most common response.</param>\n    /// <returns>A GrantValidationResult representing the error.</returns>\n    private static GrantValidationResult BuildErrorResult(string error = SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired)\n    {\n        switch (error)\n        {\n            case SendAccessConstants.EmailOtpValidatorResults.EmailRequired:\n            case SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired:\n                return new GrantValidationResult(TokenRequestErrors.InvalidRequest,\n                    errorDescription: _sendEmailOtpValidatorErrorDescriptions[error],\n                    new Dictionary<string, object>\n                    {\n                        { SendAccessConstants.SendAccessError, error }\n                    });\n            default:\n                return new GrantValidationResult(\n                    TokenRequestErrors.InvalidRequest,\n                    errorDescription: error);\n        }\n    }\n\n    /// <summary>\n    /// Builds a successful validation result for the Send password send_access grant.\n    /// </summary>\n    /// <param name=\"sendId\">Guid of the send being accessed.</param>\n    /// <returns>successful grant validation result</returns>\n    private static GrantValidationResult BuildSuccessResult(Guid sendId, string email)\n    {\n        var claims = new List<Claim>\n        {\n            new(Claims.SendAccessClaims.SendId, sendId.ToString()),\n            new(Claims.SendAccessClaims.Email, email),\n            new(Claims.Type, IdentityClientType.Send.ToString())\n        };\n\n        return new GrantValidationResult(\n            subject: sendId.ToString(),\n            authenticationMethod: CustomGrantTypes.SendAccess,\n            claims: claims);\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/SendAccess/SendNeverAuthenticateRequestValidator.cs",
    "content": "﻿using System.Text;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Utilities;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;\n\n/// <summary>\n/// This class is used to protect our system from enumeration attacks. This Validator will always return an error result.\n/// We hash the SendId Guid passed into the request to select the an error from the list of possible errors. This ensures\n/// that the same error is always returned for the same SendId.\n/// </summary>\n/// <param name=\"globalSettings\">We need access to a hash key to generate the error index.</param>\npublic class SendNeverAuthenticateRequestValidator(GlobalSettings globalSettings) : ISendAuthenticationMethodValidator<NeverAuthenticate>\n{\n    private readonly string[] _errorOptions =\n    [\n        SendAccessConstants.EnumerationProtection.Guid,\n        SendAccessConstants.EnumerationProtection.Password,\n        SendAccessConstants.EnumerationProtection.Email\n    ];\n\n    public Task<GrantValidationResult> ValidateRequestAsync(\n        ExtensionGrantValidationContext context,\n        NeverAuthenticate authMethod,\n        Guid sendId)\n    {\n        var neverAuthenticateError = GetErrorIndex(sendId, _errorOptions.Length);\n        var request = context.Request.Raw;\n        var errorType = neverAuthenticateError;\n\n        switch (neverAuthenticateError)\n        {\n            case SendAccessConstants.EnumerationProtection.Guid:\n                errorType = SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId;\n                break;\n            case SendAccessConstants.EnumerationProtection.Email:\n                var hasEmail = request.Get(SendAccessConstants.TokenRequest.Email) is not null;\n                errorType = hasEmail ? SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired\n                    : SendAccessConstants.EmailOtpValidatorResults.EmailRequired;\n                break;\n            case SendAccessConstants.EnumerationProtection.Password:\n                var hasPassword = request.Get(SendAccessConstants.TokenRequest.ClientB64HashedPassword) is not null;\n                errorType = hasPassword ? SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch\n                    : SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired;\n                break;\n        }\n\n        return Task.FromResult(BuildErrorResult(errorType));\n    }\n\n    private static GrantValidationResult BuildErrorResult(string errorType)\n    {\n        // Create error response with custom response data\n        var customResponse = new Dictionary<string, object>\n        {\n            { SendAccessConstants.SendAccessError, errorType }\n        };\n\n        var requestError = errorType switch\n        {\n            SendAccessConstants.EnumerationProtection.Guid => TokenRequestErrors.InvalidGrant,\n            SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired => TokenRequestErrors.InvalidGrant,\n            SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch => TokenRequestErrors.InvalidRequest,\n            SendAccessConstants.EmailOtpValidatorResults.EmailAndOtpRequired => TokenRequestErrors.InvalidRequest,\n            SendAccessConstants.EmailOtpValidatorResults.EmailRequired => TokenRequestErrors.InvalidRequest,\n            _ => TokenRequestErrors.InvalidGrant\n        };\n\n        return new GrantValidationResult(requestError, errorType, customResponse);\n    }\n\n    private string GetErrorIndex(Guid sendId, int range)\n    {\n        var salt = sendId.ToString();\n        byte[] hmacKey = [];\n        if (CoreHelpers.SettingHasValue(globalSettings.SendDefaultHashKey))\n        {\n            hmacKey = Encoding.UTF8.GetBytes(globalSettings.SendDefaultHashKey);\n        }\n\n        var index = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range);\n        return _errorOptions[index];\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.KeyManagement.Sends;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Identity.IdentityServer.Enums;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators.SendAccess;\n\npublic class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher) : ISendAuthenticationMethodValidator<ResourcePassword>\n{\n    private readonly ISendPasswordHasher _sendPasswordHasher = sendPasswordHasher;\n\n    /// <summary>\n    /// static object that contains the error messages for the SendPasswordRequestValidator.\n    /// </summary>\n    private static readonly Dictionary<string, string> _sendPasswordValidatorErrorDescriptions = new()\n    {\n        { SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, $\"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid.\" },\n        { SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, $\"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required.\" }\n    };\n\n    public Task<GrantValidationResult> ValidateRequestAsync(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId)\n    {\n        var request = context.Request.Raw;\n        var clientHashedPassword = request.Get(SendAccessConstants.TokenRequest.ClientB64HashedPassword);\n\n        // It is an invalid request _only_ if the passwordHashB64 is missing which indicated bad shape.\n        if (clientHashedPassword == null)\n        {\n            // Request is the wrong shape and doesn't contain a passwordHashB64 field.\n            return Task.FromResult(new GrantValidationResult(\n                TokenRequestErrors.InvalidRequest,\n                errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired],\n                new Dictionary<string, object>\n                {\n                    { SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired }\n                }));\n        }\n\n        // _sendPasswordHasher.PasswordHashMatches checks for an empty string so no need to do it before we make the call.\n        var hashMatches = _sendPasswordHasher.PasswordHashMatches(\n            resourcePassword.Hash, clientHashedPassword);\n\n        if (!hashMatches)\n        {\n            // Request is the correct shape but the passwordHashB64 doesn't match, hash could be empty.\n            return Task.FromResult(new GrantValidationResult(\n                TokenRequestErrors.InvalidGrant,\n                errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch],\n                new Dictionary<string, object>\n                {\n                    { SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch }\n                }));\n        }\n\n        return Task.FromResult(BuildSendPasswordSuccessResult(sendId));\n    }\n\n    /// <summary>\n    /// Builds a successful validation result for the Send password send_access grant.\n    /// </summary>\n    /// <param name=\"sendId\"></param>\n    /// <returns></returns>\n    private static GrantValidationResult BuildSendPasswordSuccessResult(Guid sendId)\n    {\n        var claims = new List<Claim>\n        {\n            new(Claims.SendAccessClaims.SendId, sendId.ToString()),\n            new(Claims.Type, IdentityClientType.Send.ToString())\n        };\n\n        return new GrantValidationResult(\n            subject: sendId.ToString(),\n            authenticationMethod: CustomGrantTypes.SendAccess,\n            claims: claims);\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/SendAccess/readme.md",
    "content": "# Send Access Request Validation\n\nThis feature supports the ability of Tools to require specific claims for access to sends.\n\nIn order to access Send data a user must meet the requirements laid out in these request validators.\n\n> [!IMPORTANT]\n> The string constants contained herein are used in conjunction with the Auth module in the SDK. Any change to these string values _must_ be intentional and _must_ have a corresponding change in the SDK.\n\nThere is snapshot testing that will fail if the strings change to help detect unintended changes to the string constants.\n\n## Custom Claims\n\nSend access tokens contain custom claims specific to the Send the Send grant type.\n\n1. `send_id` - is always included in the issued access token. This is the `GUID` of the request Send.\n1. `send_email` - only set when the Send requires `EmailOtp` authentication type.\n1. `type` - this will always be `Send`\n\n## Authentication methods\n\n### `NeverAuthenticate`\n\nFor a Send to be in this state two things can be true:\n1. The Send has been modified and no longer allows access.\n2. The Send does not exist.\n\n### `NotAuthenticated`\n\nIn this scenario the Send is not protected by any added authentication or authorization and the access token is issued to the requesting user.\n\n### `ResourcePassword`\n\nIn this scenario the Send is password protected and a user must supply the correct password hash to be issued an access token.\n\n### `EmailOtp`\n\nIn this scenario the Send is only accessible to owners of specific email addresses. The user must submit a correct email. Once the email has been entered then ownership of the email must be established via OTP. The Otp is sent to the aforementioned email and must be supplied, along with the email, to be issued an access token.\n\n## Send Access Request Validation\n\n### Required Parameters\n\n#### All Requests\n- `send_id` - Base64 URL-encoded GUID of the send being accessed\n\n#### Password Protected Sends\n- `password_hash_b64` - client hashed Base64-encoded password.\n\n#### Email OTP Protected Sends\n- `email` - Email address associated with the send\n- `otp` - One-time password (optional - if missing, OTP is generated and sent)\n\n### Error Responses\n\nAll errors include a custom response field:\n```json\n{\n  \"error\": \"invalid_request|invalid_grant\",\n  \"error_description\": \"Human readable description\",\n  \"send_access_error_type\": \"specific_error_code\"\n}\n```"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/SsoRequestValidator.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Sso;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Services;\nusing Bit.Identity.IdentityServer.RequestValidationConstants;\nusing Duende.IdentityModel;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\n/// <summary>\n/// Validates whether a user is required to authenticate via SSO based on organization policies.\n/// </summary>\npublic class SsoRequestValidator(\n    IPolicyService _policyService,\n    IFeatureService _featureService,\n    IUserSsoOrganizationIdentifierQuery _userSsoOrganizationIdentifierQuery,\n    IPolicyRequirementQuery _policyRequirementQuery) : ISsoRequestValidator\n{\n    /// <summary>\n    /// Validates the SSO requirement for a user attempting to authenticate.\n    /// Sets context.SsoRequired to indicate whether SSO is required.\n    /// If SSO is required, sets the validation error result and custom response in the context.\n    /// </summary>\n    /// <param name=\"user\">The user attempting to authenticate.</param>\n    /// <param name=\"request\">The token request containing grant type and other authentication details.</param>\n    /// <param name=\"context\">The validator context to be updated with SSO requirement status and error results if applicable.</param>\n    /// <returns>true if the user can proceed with authentication; false if SSO is required and the user must be redirected to SSO flow.</returns>\n    public async Task<bool> ValidateAsync(User user, ValidatedTokenRequest request, CustomValidatorRequestContext context)\n    {\n        context.SsoRequired = await RequireSsoAuthenticationAsync(user, request.GrantType);\n\n        if (!context.SsoRequired)\n        {\n            return true;\n        }\n\n        // Users without SSO requirement requesting 2FA recovery will be fast-forwarded through login and are\n        // presented with their 2FA management area as a reminder to re-evaluate their 2FA posture after recovery and\n        // review their new recovery token if desired.\n        // SSO users cannot be assumed to be authenticated, and must prove authentication with their IdP after recovery.\n        // As described in validation order determination, if TwoFactorRequired, the 2FA validation scheme will have been\n        // evaluated, and recovery will have been performed if requested.\n        // We will send a descriptive message in these cases so clients can give the appropriate feedback and redirect\n        // to /login.\n        if (context.TwoFactorRequired && context.TwoFactorRecoveryRequested)\n        {\n            await SetContextCustomResponseSsoErrorAsync(context, SsoConstants.RequestErrors.SsoTwoFactorRecoveryDescription);\n            return false;\n        }\n\n        await SetContextCustomResponseSsoErrorAsync(context, SsoConstants.RequestErrors.SsoRequiredDescription);\n        return false;\n    }\n\n    /// <summary>\n    /// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are\n    /// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement.\n    /// If the GrantType is authorization_code or client_credentials we know the user is trying to log in\n    /// using the SSO flow so they are allowed to continue.\n    /// </summary>\n    /// <param name=\"user\">user trying to log in</param>\n    /// <param name=\"grantType\">magic string identifying the grant type requested</param>\n    /// <returns>true if sso required; false if not required or already in process</returns>\n    private async Task<bool> RequireSsoAuthenticationAsync(User user, string grantType)\n    {\n        if (grantType == OidcConstants.GrantTypes.AuthorizationCode ||\n            grantType == OidcConstants.GrantTypes.ClientCredentials)\n        {\n            // SSO is not required for users already using SSO to authenticate which uses the authorization_code grant type,\n            // or logging-in via API key which is the client_credentials grant type.\n            // Allow user to continue request validation\n            return false;\n        }\n\n        // Check if user belongs to any organization with an active SSO policy\n        var ssoRequired = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            ? (await _policyRequirementQuery.GetAsyncVNext<RequireSsoPolicyRequirement>(user.Id))\n            .SsoRequired\n            : await _policyService.AnyPoliciesApplicableToUserAsync(\n                user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);\n\n        if (ssoRequired)\n        {\n            return true;\n        }\n\n        // Default - SSO is not required\n        return false;\n    }\n\n    /// <summary>\n    /// Sets the customResponse in the context with the error result for the SSO validation failure.\n    /// </summary>\n    /// <param name=\"context\">The validator context to update with error details.</param>\n    /// <param name=\"errorMessage\">The error message to return to the client.</param>\n    private async Task SetContextCustomResponseSsoErrorAsync(CustomValidatorRequestContext context, string errorMessage)\n    {\n        var ssoOrganizationIdentifier = await _userSsoOrganizationIdentifierQuery.GetSsoOrganizationIdentifierAsync(context.User.Id);\n\n        context.ValidationErrorResult = new ValidationResult\n        {\n            IsError = true,\n            Error = OidcConstants.TokenErrors.InvalidGrant,\n            ErrorDescription = errorMessage\n        };\n\n        context.CustomResponse = new Dictionary<string, object>\n        {\n            { CustomResponseConstants.ResponseKeys.ErrorModel, new ErrorResponseModel(errorMessage) }\n        };\n\n        // Include organization identifier in the response if available\n        if (!string.IsNullOrEmpty(ssoOrganizationIdentifier))\n        {\n            context.CustomResponse[CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier] = ssoOrganizationIdentifier;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Duende.IdentityServer.Validation;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\npublic class TwoFactorAuthenticationValidator(\n    IUserService userService,\n    UserManager<User> userManager,\n    IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider,\n    IApplicationCacheService applicationCacheService,\n    IOrganizationUserRepository organizationUserRepository,\n    IOrganizationRepository organizationRepository,\n    IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmail2faSessionTokeFactory,\n    ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n    ICurrentContext currentContext) : ITwoFactorAuthenticationValidator\n{\n    private readonly IUserService _userService = userService;\n    private readonly UserManager<User> _userManager = userManager;\n    private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider;\n    private readonly IApplicationCacheService _applicationCacheService = applicationCacheService;\n    private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository;\n    private readonly IOrganizationRepository _organizationRepository = organizationRepository;\n    private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;\n    private readonly ICurrentContext _currentContext = currentContext;\n\n    public async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)\n    {\n        if (request.GrantType == \"client_credentials\" || request.GrantType == \"webauthn\")\n        {\n            /*\n                Do not require MFA for api key logins.\n                We consider Fido2 userVerification a second factor, so we don't require a second factor here.\n            */\n            return new Tuple<bool, Organization>(false, null);\n        }\n\n        var individualRequired = _userManager.SupportsUserTwoFactor &&\n                                 await _userManager.GetTwoFactorEnabledAsync(user) &&\n                                 (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;\n\n        Organization firstEnabledOrg = null;\n        var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList();\n        if (orgs.Count > 0)\n        {\n            var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();\n            var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id));\n            if (twoFactorOrgs.Any())\n            {\n                var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);\n                firstEnabledOrg = userOrgs.FirstOrDefault(\n                    o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled());\n            }\n        }\n\n        return new Tuple<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);\n    }\n\n    public async Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization)\n    {\n        var enabledProviders = await GetEnabledTwoFactorProvidersAsync(user, organization);\n        if (enabledProviders.Count == 0)\n        {\n            return null;\n        }\n\n        var providers = new Dictionary<string, Dictionary<string, object>>();\n        foreach (var provider in enabledProviders)\n        {\n            var twoFactorParams = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value);\n            providers.Add(((byte)provider.Key).ToString(), twoFactorParams);\n        }\n\n        var twoFactorResultDict = new Dictionary<string, object>\n        {\n            { \"TwoFactorProviders\", providers.Keys }, // backwards compatibility\n            { \"TwoFactorProviders2\", providers },\n        };\n\n        // If we have an Email 2FA provider we need this session token so SSO users\n        // can re-request an email TOTP. The TwoFactorController.SendEmailLoginAsync\n        // endpoint requires a way to authenticate the user before sending another email with\n        // a TOTP, this token acts as the authentication mechanism.\n        if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email))\n        {\n            twoFactorResultDict.Add(\"SsoEmail2faSessionToken\",\n                _ssoEmail2faSessionTokeFactory.Protect(new SsoEmail2faSessionTokenable(user)));\n\n            twoFactorResultDict.Add(\"Email\", user.Email);\n        }\n\n        return twoFactorResultDict;\n    }\n\n    public async Task<bool> VerifyTwoFactorAsync(\n        User user,\n        Organization organization,\n        TwoFactorProviderType type,\n        string token)\n    {\n        if (organization != null && type == TwoFactorProviderType.OrganizationDuo)\n        {\n            if (organization.TwoFactorProviderIsEnabled(type))\n            {\n                return await _organizationDuoUniversalTokenProvider.ValidateAsync(token, organization, user);\n            }\n            return false;\n        }\n\n        if (type is TwoFactorProviderType.RecoveryCode)\n        {\n            return await _userService.RecoverTwoFactorAsync(user, token);\n        }\n\n        // These cases we want to always return false, U2f is deprecated and OrganizationDuo\n        // uses a different flow than the other two factor providers, it follows the same\n        // structure of a UserTokenProvider but has it's logic runs outside the usual token\n        // provider flow. See IOrganizationDuoUniversalTokenProvider.cs\n        if (type is TwoFactorProviderType.U2f or TwoFactorProviderType.OrganizationDuo)\n        {\n            return false;\n        }\n\n        // Now we are concerning the rest of the Two Factor Provider Types\n\n        // The intent of this check is to make sure that the user is using a 2FA provider that\n        // is enabled and allowed by their premium status.\n        // The exception for Remember is because it is a \"special\" 2FA type that isn't ever explicitly\n        // enabled by a user, so we can't check the user's 2FA providers to see if they're\n        // enabled. We just have to check if the token is valid.\n        if (type != TwoFactorProviderType.Remember &&\n            user.GetTwoFactorProvider(type) == null)\n        {\n            return false;\n        }\n\n        // Finally, verify the token based on the provider type.\n        return await _userManager.VerifyTwoFactorTokenAsync(\n            user, CoreHelpers.CustomProviderName(type), token);\n    }\n\n    private async Task<List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>> GetEnabledTwoFactorProvidersAsync(\n        User user, Organization organization)\n    {\n        var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();\n        var organizationTwoFactorProviders = organization?.GetTwoFactorProviders();\n        if (organizationTwoFactorProviders != null)\n        {\n            enabledProviders.AddRange(\n                organizationTwoFactorProviders.Where(\n                    p => (p.Value?.Enabled ?? false) && organization.Use2fa));\n        }\n\n        var userTwoFactorProviders = user.GetTwoFactorProviders();\n        var userCanAccessPremium = await _userService.CanAccessPremium(user);\n        if (userTwoFactorProviders != null)\n        {\n            enabledProviders.AddRange(\n                userTwoFactorProviders.Where(p =>\n                        // Providers that do not require premium\n                        (p.Value.Enabled && !TwoFactorProvider.RequiresPremium(p.Key)) ||\n                        // Providers that require premium and the User has Premium\n                        (p.Value.Enabled && TwoFactorProvider.RequiresPremium(p.Key) && userCanAccessPremium)));\n        }\n\n        return enabledProviders;\n    }\n\n    /// <summary>\n    /// Builds the parameters for the two-factor authentication\n    /// </summary>\n    /// <param name=\"organization\">We need the organization for Organization Duo Provider type</param>\n    /// <param name=\"user\">The user for which the token is being generated</param>\n    /// <param name=\"type\">Provider Type</param>\n    /// <param name=\"provider\">Raw data that is used to create the response</param>\n    /// <returns>a dictionary with the correct provider configuration or null if the provider is not configured properly</returns>\n    private async Task<Dictionary<string, object>> BuildTwoFactorParams(Organization organization, User user,\n        TwoFactorProviderType type, TwoFactorProvider provider)\n    {\n        // We will always return this dictionary. If none of the criteria is met then it will return null.\n        var twoFactorParams = new Dictionary<string, object>();\n\n        // OrganizationDuo is odd since it doesn't use the UserManager built-in TwoFactor flows\n        /*\n            Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class\n            in the future the `AuthUrl` will be the generated \"token\" - PM-8107\n        */\n        if (type == TwoFactorProviderType.OrganizationDuo &&\n            await _organizationDuoUniversalTokenProvider.CanGenerateTwoFactorTokenAsync(organization))\n        {\n            twoFactorParams.Add(\"Host\", provider.MetaData[\"Host\"]);\n            twoFactorParams.Add(\"AuthUrl\",\n                await _organizationDuoUniversalTokenProvider.GenerateAsync(organization, user));\n\n            return twoFactorParams;\n        }\n\n        // Individual 2FA providers use the UserManager built-in TwoFactor flow so we can generate the token before building the params\n        var token = await _userManager.GenerateTwoFactorTokenAsync(user,\n            CoreHelpers.CustomProviderName(type));\n        switch (type)\n        {\n            case TwoFactorProviderType.Duo:\n                twoFactorParams.Add(\"Host\", provider.MetaData[\"Host\"]);\n                twoFactorParams.Add(\"AuthUrl\", token);\n                break;\n            case TwoFactorProviderType.WebAuthn:\n                if (token != null)\n                {\n                    twoFactorParams = JsonSerializer.Deserialize<Dictionary<string, object>>(token);\n                }\n                break;\n            case TwoFactorProviderType.Email:\n                var twoFactorEmail = (string)provider.MetaData[\"Email\"];\n                var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail);\n                twoFactorParams.Add(\"Email\", redactedEmail);\n                break;\n            case TwoFactorProviderType.YubiKey:\n                twoFactorParams.Add(\"Nfc\", (bool)provider.MetaData[\"Nfc\"]);\n                break;\n        }\n\n        // return null if the dictionary is empty\n        return twoFactorParams.Count > 0 ? twoFactorParams : null;\n    }\n\n    private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)\n    {\n        return orgAbilities != null && orgAbilities.TryGetValue(orgId, out var orgAbility) &&\n               orgAbility.Enabled && orgAbility.Using2fa;\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Security.Claims;\nusing System.Text.Json;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.WebAuthnLogin;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Duende.IdentityServer.Models;\nusing Duende.IdentityServer.Validation;\nusing Fido2NetLib;\nusing Microsoft.AspNetCore.Identity;\n\nnamespace Bit.Identity.IdentityServer.RequestValidators;\n\npublic class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidationContext>, IExtensionGrantValidator\n{\n    public const string GrantType = \"webauthn\";\n\n    private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;\n    private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;\n    private readonly IDeviceValidator _deviceValidator;\n\n    public WebAuthnGrantValidator(\n        UserManager<User> userManager,\n        IUserService userService,\n        IEventService eventService,\n        IDeviceValidator deviceValidator,\n        ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,\n        ISsoRequestValidator ssoRequestValidator,\n        IOrganizationUserRepository organizationUserRepository,\n        ILogger<CustomTokenRequestValidator> logger,\n        ICurrentContext currentContext,\n        GlobalSettings globalSettings,\n        ISsoConfigRepository ssoConfigRepository,\n        IUserRepository userRepository,\n        IPolicyService policyService,\n        IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,\n        IFeatureService featureService,\n        IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,\n        IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,\n        IPolicyRequirementQuery policyRequirementQuery,\n        IAuthRequestRepository authRequestRepository,\n        IMailService mailService,\n        IUserAccountKeysQuery userAccountKeysQuery,\n        IClientVersionValidator clientVersionValidator)\n        : base(\n            userManager,\n            userService,\n            eventService,\n            deviceValidator,\n            twoFactorAuthenticationValidator,\n            ssoRequestValidator,\n            organizationUserRepository,\n            logger,\n            currentContext,\n            globalSettings,\n            userRepository,\n            policyService,\n            featureService,\n            ssoConfigRepository,\n            userDecryptionOptionsBuilder,\n            policyRequirementQuery,\n            authRequestRepository,\n            mailService,\n            userAccountKeysQuery,\n            clientVersionValidator)\n    {\n        _assertionOptionsDataProtector = assertionOptionsDataProtector;\n        _assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;\n        _deviceValidator = deviceValidator;\n    }\n\n    string IExtensionGrantValidator.GrantType => \"webauthn\";\n\n    public async Task ValidateAsync(ExtensionGrantValidationContext context)\n    {\n        var rawToken = context.Request.Raw.Get(\"token\");\n        var rawDeviceResponse = context.Request.Raw.Get(\"deviceResponse\");\n        if (string.IsNullOrWhiteSpace(rawToken) || string.IsNullOrWhiteSpace(rawDeviceResponse))\n        {\n            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);\n            return;\n        }\n\n        var verified = _assertionOptionsDataProtector.TryUnprotect(rawToken, out var token) &&\n            token.TokenIsValid(WebAuthnLoginAssertionOptionsScope.Authentication);\n        var deviceResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(rawDeviceResponse);\n\n        if (!verified)\n        {\n            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest);\n            return;\n        }\n\n        var (user, credential) = await _assertWebAuthnLoginCredentialCommand.AssertWebAuthnLoginCredential(token.Options, deviceResponse);\n        UserDecryptionOptionsBuilder.WithWebAuthnLoginCredential(credential);\n\n        await ValidateAsync(context, context.Request, new CustomValidatorRequestContext { User = user });\n    }\n\n    protected override Task<bool> ValidateContextAsync(ExtensionGrantValidationContext context,\n        CustomValidatorRequestContext validatorContext)\n    {\n        if (validatorContext.User == null)\n        {\n            return Task.FromResult(false);\n        }\n\n        return Task.FromResult(true);\n    }\n\n    protected override Task SetSuccessResult(ExtensionGrantValidationContext context, User user,\n        List<Claim> claims, Dictionary<string, object> customResponse)\n    {\n        context.Result = new GrantValidationResult(user.Id.ToString(), \"Application\",\n            identityProvider: Constants.IdentityProvider,\n            claims: claims.Count > 0 ? claims : null,\n            customResponse: customResponse);\n        return Task.CompletedTask;\n    }\n\n    protected override ClaimsPrincipal GetSubject(ExtensionGrantValidationContext context)\n    {\n        return context.Result.Subject;\n    }\n\n    [Obsolete(\"Consider using SetValidationErrorResult instead.\")]\n    protected override void SetTwoFactorResult(ExtensionGrantValidationContext context,\n        Dictionary<string, object> customResponse)\n    {\n        context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, \"Two factor required.\",\n            customResponse);\n    }\n\n    [Obsolete(\"Consider using SetValidationErrorResult instead.\")]\n    protected override void SetErrorResult(ExtensionGrantValidationContext context, Dictionary<string, object> customResponse)\n    {\n        context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse);\n    }\n\n    protected override void SetValidationErrorResult(\n        ExtensionGrantValidationContext context, CustomValidatorRequestContext requestContext)\n    {\n        context.Result = new GrantValidationResult\n        {\n            Error = requestContext.ValidationErrorResult.Error,\n            ErrorDescription = requestContext.ValidationErrorResult.ErrorDescription,\n            IsError = true,\n            CustomResponse = requestContext.CustomResponse\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/ServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Identity.IdentityServer;\n\nnamespace Microsoft.Extensions.DependencyInjection;\n\npublic static class ServiceCollectionExtensions\n{\n    /// <summary>\n    /// Registers a custom <see cref=\"IClientProvider\"/> for the given identifier to be called when a client id with\n    /// the identifier is attempting authentication.\n    /// </summary>\n    /// <typeparam name=\"T\">Your custom implementation of <see cref=\"IClientProvider\"/>.</typeparam>\n    /// <param name=\"services\">The service collection to add services to.</param>\n    /// <param name=\"identifier\">\n    /// The identifier to be used to invoke your client provider if a <c>client_id</c> is prefixed with your identifier\n    /// then your <see cref=\"IClientProvider\"/> implementation will be invoked with the data after the seperating <c>.</c>.\n    /// </param>\n    /// <returns>The <see cref=\"IServiceCollection\"/> for additional chaining.</returns>\n    public static IServiceCollection AddClientProvider<T>(this IServiceCollection services, string identifier)\n        where T : class, IClientProvider\n    {\n        ArgumentNullException.ThrowIfNull(services);\n        ArgumentException.ThrowIfNullOrWhiteSpace(identifier);\n\n        services.AddKeyedTransient<IClientProvider, T>(identifier);\n\n        return services;\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/StaticClientStore.cs",
    "content": "﻿using System.Collections.Frozen;\nusing Bit.Core.Enums;\nusing Bit.Core.Settings;\nusing Bit.Identity.IdentityServer.StaticClients;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Identity.IdentityServer;\n\npublic class StaticClientStore\n{\n    public StaticClientStore(GlobalSettings globalSettings)\n    {\n        Clients = new List<Client>\n        {\n            new ApiClient(globalSettings, BitwardenClient.Mobile, 60, 1),\n            new ApiClient(globalSettings, BitwardenClient.Web, 7, 1),\n            new ApiClient(globalSettings, BitwardenClient.Browser, 30, 1),\n            new ApiClient(globalSettings, BitwardenClient.Desktop, 30, 1),\n            new ApiClient(globalSettings, BitwardenClient.Cli, 30, 1),\n            new ApiClient(globalSettings, BitwardenClient.DirectoryConnector, 30, 24),\n            SendClientBuilder.Build(globalSettings),\n        }.ToFrozenDictionary(c => c.ClientId);\n    }\n\n    public FrozenDictionary<string, Client> Clients { get; }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/StaticClients/SendClientBuilder.cs",
    "content": "﻿using Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Enums;\nusing Bit.Core.Settings;\nusing Bit.Identity.IdentityServer.Enums;\nusing Duende.IdentityServer.Models;\n\nnamespace Bit.Identity.IdentityServer.StaticClients;\npublic static class SendClientBuilder\n{\n    public static Client Build(GlobalSettings globalSettings)\n    {\n        return new Client()\n        {\n            ClientId = BitwardenClient.Send,\n            AllowedGrantTypes = [CustomGrantTypes.SendAccess],\n            AccessTokenLifetime = 60 * globalSettings.SendAccessTokenLifetimeInMinutes,\n\n            // Do not allow refresh tokens to be issued.\n            AllowOfflineAccess = false,\n\n            // Send is a public anonymous client, so no secret is required (or really possible to use securely).\n            RequireClientSecret = false,\n\n            // Allow web vault to use this client.\n            AllowedCorsOrigins = [globalSettings.BaseServiceUri.Vault],\n\n            // Setup API scopes that the client can request in the scope property of the token request.\n            AllowedScopes = [ApiScopes.ApiSendAccess],\n        };\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Api.Response;\nusing Bit.Core.Auth.Utilities;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Api.Response;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Bit.Identity.Utilities;\n\nnamespace Bit.Identity.IdentityServer;\n\n#nullable enable\n/// <summary>\n/// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents\n///\n/// Note: Do not use this as an injected service if you intend to build multiple independent UserDecryptionOptions\n/// </summary>\npublic class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder\n{\n    private readonly ICurrentContext _currentContext;\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ILoginApprovingClientTypes _loginApprovingClientTypes;\n    private UserDecryptionOptions _options = new UserDecryptionOptions();\n    private User _user = null!;\n    private SsoConfig? _ssoConfig;\n    private Device? _device;\n\n    public UserDecryptionOptionsBuilder(\n        ICurrentContext currentContext,\n        IDeviceRepository deviceRepository,\n        IOrganizationUserRepository organizationUserRepository,\n        ILoginApprovingClientTypes loginApprovingClientTypes\n    )\n    {\n        _currentContext = currentContext;\n        _deviceRepository = deviceRepository;\n        _organizationUserRepository = organizationUserRepository;\n        _loginApprovingClientTypes = loginApprovingClientTypes;\n    }\n\n    public IUserDecryptionOptionsBuilder ForUser(User user)\n    {\n        _user = user;\n        return this;\n    }\n\n    public IUserDecryptionOptionsBuilder WithSso(SsoConfig ssoConfig)\n    {\n        _ssoConfig = ssoConfig;\n        return this;\n    }\n\n    public IUserDecryptionOptionsBuilder WithDevice(Device device)\n    {\n        _device = device;\n        return this;\n    }\n\n    public IUserDecryptionOptionsBuilder WithWebAuthnLoginCredential(WebAuthnCredential credential)\n    {\n        if (credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled)\n        {\n            _options.WebAuthnPrfOption = new WebAuthnPrfDecryptionOption(\n                credential.EncryptedPrivateKey,\n                credential.EncryptedUserKey,\n                credential.CredentialId,\n                [] // Stored credentials currently lack Transports, just send an empty array for now\n            );\n        }\n\n        return this;\n    }\n\n    public async Task<UserDecryptionOptions> BuildAsync()\n    {\n        BuildMasterPasswordUnlock();\n        BuildKeyConnectorOptions();\n        await BuildTrustedDeviceOptionsAsync();\n\n        return _options;\n    }\n\n    private void BuildKeyConnectorOptions()\n    {\n        if (_ssoConfig == null)\n        {\n            return;\n        }\n\n        var ssoConfigurationData = _ssoConfig.GetData();\n        if (ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } &&\n            !string.IsNullOrEmpty(ssoConfigurationData.KeyConnectorUrl))\n        {\n            _options.KeyConnectorOption = new KeyConnectorUserDecryptionOption(ssoConfigurationData.KeyConnectorUrl);\n        }\n    }\n\n    private async Task BuildTrustedDeviceOptionsAsync()\n    {\n        // TrustedDeviceEncryption only exists for SSO, if that changes then these guards should change\n        if (_ssoConfig == null)\n        {\n            return;\n        }\n\n        var isTdeActive = _ssoConfig.GetData() is\n        { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption };\n        var isTdeOffboarding = !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive;\n        if (!isTdeActive && !isTdeOffboarding)\n        {\n            return;\n        }\n\n        string? encryptedPrivateKey = null;\n        string? encryptedUserKey = null;\n        if (_device != null && _device.IsTrusted())\n        {\n            encryptedPrivateKey = _device.EncryptedPrivateKey;\n            encryptedUserKey = _device.EncryptedUserKey;\n        }\n\n        var hasLoginApprovingDevice = false;\n        if (_device != null)\n        {\n            var allDevices = await _deviceRepository.GetManyByUserIdAsync(_user.Id);\n            // Checks if the current user has any devices that are capable of approving login with device requests\n            // except for their current device.\n            hasLoginApprovingDevice = allDevices.Any(d =>\n                d.Identifier != _device.Identifier &&\n                _loginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type)));\n        }\n\n        // Just-in-time-provisioned users, which can include users invited to a TDE organization with SSO and granted\n        // the Admin/Owner role or Custom user role with ManageResetPassword permission, will not have claims available\n        // in context to reflect this permission if granted as part of an invite for the current organization.\n        // Therefore, as written today, CurrentContext will not surface those permissions for those users.\n        // In order to make this check accurate at first login for all applicable cases, we have to go back to the\n        // database record.\n        // In the TDE flow, the users will have been JIT-provisioned at SSO callback time, and the relationship between\n        // user and organization user will have been codified.\n        var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id);\n        var hasManageResetPasswordPermission = await EvaluateHasManageResetPasswordPermission();\n\n        // They are only able to be approved by an admin if they have enrolled in account recovery\n        var hasAdminApproval = organizationUser != null && organizationUser.IsEnrolledInAccountRecovery();\n\n        _options.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(\n            hasAdminApproval,\n            hasLoginApprovingDevice,\n            hasManageResetPasswordPermission,\n            isTdeOffboarding,\n            encryptedPrivateKey,\n            encryptedUserKey);\n        return;\n\n        /// Determine if the user has manage reset password permission,  \n        /// as post-SSO logic requires it for forcing users with this permission to set a password.\n        async Task<bool> EvaluateHasManageResetPasswordPermission()\n        {\n            if (organizationUser == null)\n            {\n                return false;\n            }\n\n            var organizationUserHasResetPasswordPermission =\n                // The repository will pull users in all statuses, so we also need to ensure that revoked-status users do not have\n                // permissions sent down.\n                organizationUser.Status is OrganizationUserStatusType.Invited or OrganizationUserStatusType.Accepted or\n                    OrganizationUserStatusType.Confirmed &&\n                // Admins and owners get ManageResetPassword functionally \"for free\" through their role.\n                (organizationUser.Type is OrganizationUserType.Admin or OrganizationUserType.Owner ||\n                 // Custom users can have the ManagePasswordReset permission assigned directly.\n                 organizationUser.GetPermissions() is { ManageResetPassword: true });\n\n            return organizationUserHasResetPasswordPermission ||\n                   // A provider user for the given organization gets ManageResetPassword through that relationship.\n                   await _currentContext.ProviderUserForOrgAsync(_ssoConfig.OrganizationId);\n        }\n    }\n\n    private void BuildMasterPasswordUnlock()\n    {\n        if (_user.HasMasterPassword())\n        {\n            _options.HasMasterPassword = true;\n            _options.MasterPasswordUnlock = new MasterPasswordUnlockResponseModel\n            {\n                Kdf = new MasterPasswordUnlockKdfResponseModel\n                {\n                    KdfType = _user.Kdf,\n                    Iterations = _user.KdfIterations,\n                    Memory = _user.KdfMemory,\n                    Parallelism = _user.KdfParallelism\n                },\n                MasterKeyEncryptedUserKey = _user.Key!,\n                Salt = _user.Email.ToLowerInvariant()\n            };\n        }\n        else\n        {\n            _options.HasMasterPassword = false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Identity/IdentityServer/VaultCorsPolicyService.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Duende.IdentityServer.Services;\n\nnamespace Bit.Identity.IdentityServer;\n\npublic class CustomCorsPolicyService : ICorsPolicyService\n{\n    private readonly GlobalSettings _globalSettings;\n\n    public CustomCorsPolicyService(GlobalSettings globalSettings)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public Task<bool> IsOriginAllowedAsync(string origin)\n    {\n        return Task.FromResult(CoreHelpers.IsCorsOriginAllowed(origin, _globalSettings));\n    }\n}\n"
  },
  {
    "path": "src/Identity/Models/RedirectViewModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nnamespace Bit.Identity.Models;\n\npublic class RedirectViewModel\n{\n    public string RedirectUrl { get; set; }\n}\n"
  },
  {
    "path": "src/Identity/Models/Request/Accounts/PasswordPreloginRequestModel.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\n\nnamespace Bit.Identity.Models.Request.Accounts;\n\npublic class PasswordPreloginRequestModel\n{\n    [Required]\n    [EmailAddress]\n    [StringLength(256)]\n    public string Email { get; set; }\n}\n"
  },
  {
    "path": "src/Identity/Models/Response/Accounts/PasswordPreloginResponseModel.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Identity.Models.Response.Accounts;\n\npublic class PasswordPreloginResponseModel\n{\n    public PasswordPreloginResponseModel(UserKdfInformation kdfInformation, string? salt = null)\n    {\n        // PM-28143 Cleanup\n        Kdf = kdfInformation.Kdf;\n        KdfIterations = kdfInformation.KdfIterations;\n        KdfMemory = kdfInformation.KdfMemory;\n        KdfParallelism = kdfInformation.KdfParallelism;\n        // End Cleanup\n\n        KdfSettings = new KdfSettings()\n        {\n            KdfType = kdfInformation.Kdf,\n            Iterations = kdfInformation.KdfIterations,\n            Memory = kdfInformation.KdfMemory,\n            Parallelism = kdfInformation.KdfParallelism,\n        };\n        Salt = salt;\n    }\n\n    // Old Data Types\n    public KdfType? Kdf { get; set; }           // PM-28143 Remove with cleanup\n    public int? KdfIterations { get; set; }     // PM-28143 Remove with cleanup\n    public int? KdfMemory { get; set; }         // PM-28143 Remove with cleanup\n    public int? KdfParallelism { get; set; }    // PM-28143 Remove with cleanup\n\n    // New Data Types\n    public KdfSettings? KdfSettings { get; set; }  // PM-28143 With cleanup make this not nullish\n    public string? Salt { get; set; }              // PM-28143 With cleanup make this not nullish. Not used yet,\n                                                   // just the email from the request at this time.\n}\n"
  },
  {
    "path": "src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs",
    "content": "﻿using Bit.Core.Models.Api;\n\nnamespace Bit.Identity.Models.Response.Accounts;\n\npublic class RegisterFinishResponseModel : ResponseModel\n{\n    public RegisterFinishResponseModel()\n        : base(\"registerFinish\") { }\n}\n"
  },
  {
    "path": "src/Identity/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Identity;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        CreateHostBuilder(args)\n            .Build()\n            .Run();\n    }\n\n    public static IHostBuilder CreateHostBuilder(string[] args)\n    {\n        return Host\n            .CreateDefaultBuilder(args)\n            .UseBitwardenSdk()\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging();\n    }\n}\n"
  },
  {
    "path": "src/Identity/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:33656/\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Identity\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:33656\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Identity-SelfHost\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:33657\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"developSelfHosted\": \"true\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Identity/Startup.cs",
    "content": "﻿using System.Globalization;\nusing System.IdentityModel.Tokens.Jwt;\nusing AspNetCoreRateLimit;\nusing Bit.Core;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Context;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.SecretsManager.Repositories.Noop;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Identity.Utilities;\nusing Bit.SharedWeb.Swagger;\nusing Bit.SharedWeb.Utilities;\nusing Duende.IdentityServer.Services;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.OpenApi;\n\nnamespace Bit.Identity;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(\"en-US\");\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; private set; }\n    public IWebHostEnvironment Environment { get; set; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n        if (!globalSettings.SelfHosted)\n        {\n            services.Configure<IpRateLimitOptions>(Configuration.GetSection(\"IpRateLimitOptions\"));\n            services.Configure<IpRateLimitPolicies>(Configuration.GetSection(\"IpRateLimitPolicies\"));\n        }\n\n        // Data Protection\n        services.AddCustomDataProtectionServices(Environment, globalSettings);\n\n        // Repositories\n        services.AddDatabaseRepositories(globalSettings);\n        services.AddTestPlayIdTracking(globalSettings);\n\n        // Context\n        services.AddScoped<ICurrentContext, CurrentContext>();\n        services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();\n\n        // Caching\n        services.AddMemoryCache();\n        services.AddDistributedCache(globalSettings);\n\n        // Mvc\n        services.AddMvc(config =>\n        {\n            config.Filters.Add(new ModelStateValidationFilterAttribute());\n        });\n\n        services.AddSwaggerGen(config =>\n        {\n            config.InitializeSwaggerFilters(Environment);\n\n            config.SwaggerDoc(\"v1\", new OpenApiInfo { Title = \"Bitwarden Identity\", Version = \"v1\" });\n        });\n\n        if (!globalSettings.SelfHosted)\n        {\n            services.AddIpRateLimiting(globalSettings);\n        }\n\n        // Cookies\n        if (Environment.IsDevelopment())\n        {\n            services.Configure<CookiePolicyOptions>(options =>\n            {\n                options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;\n                options.OnAppendCookie = ctx =>\n                {\n                    ctx.CookieOptions.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;\n                };\n            });\n        }\n\n        JwtSecurityTokenHandler.DefaultMapInboundClaims = false;\n\n        // Authentication\n        services\n            .AddDistributedIdentityServices()\n            .AddAuthentication()\n            .AddCookie(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)\n            .AddOpenIdConnect(\"sso\", \"Single Sign On\", options =>\n            {\n                options.Authority = globalSettings.BaseServiceUri.InternalSso;\n                options.RequireHttpsMetadata = !Environment.IsDevelopment() &&\n                    globalSettings.BaseServiceUri.InternalIdentity.StartsWith(\"https\");\n                options.ClientId = \"oidc-identity\";\n                options.ClientSecret = globalSettings.OidcIdentityClientKey;\n                options.ResponseMode = \"form_post\";\n\n                options.SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme;\n                options.ResponseType = \"code\";\n                options.SaveTokens = false;\n                options.GetClaimsFromUserInfoEndpoint = true;\n\n                // Some browsers (safari) won't allow Secure cookies to be set on a http connection\n                options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;\n                options.NonceCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;\n\n                options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents\n                {\n                    OnRedirectToIdentityProvider = context =>\n                    {\n                        // Pass domain_hint onto the sso idp\n                        context.ProtocolMessage.DomainHint = context.Properties.Items[\"domain_hint\"];\n                        context.ProtocolMessage.Parameters.Add(\"organizationId\", context.Properties.Items[\"organizationId\"]);\n                        if (context.Properties.Items.TryGetValue(\"user_identifier\", out var userIdentifier))\n                        {\n                            context.ProtocolMessage.SessionState = userIdentifier;\n                        }\n\n                        if (context.Properties.Parameters.Count > 0 &&\n                            context.Properties.Parameters.TryGetValue(SsoTokenable.TokenIdentifier, out var tokenValue))\n                        {\n                            var token = tokenValue?.ToString() ?? \"\";\n                            context.ProtocolMessage.Parameters.Add(SsoTokenable.TokenIdentifier, token);\n                        }\n                        return Task.FromResult(0);\n                    }\n                };\n            });\n\n        // IdentityServer\n        services.AddCustomIdentityServerServices(Environment, globalSettings);\n\n        // Identity\n        services.AddCustomIdentityServices(globalSettings);\n\n        // Services\n        services.AddBaseServices(globalSettings);\n        services.AddDefaultServices(globalSettings);\n        services.AddOptionality();\n        services.AddCoreLocalizationServices();\n        services.AddBillingOperations();\n\n        // TODO: Remove when OrganizationUser methods are moved out of OrganizationService, this noop dependency should\n        // TODO: no longer be required - see PM-1880\n        services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();\n\n        if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&\n            CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName))\n        {\n            services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();\n        }\n\n        // HttpClients\n        services.AddHttpClient(\"InternalSso\", client =>\n        {\n            client.BaseAddress = new Uri(globalSettings.BaseServiceUri.InternalSso);\n        });\n    }\n\n    public void Configure(\n        IApplicationBuilder app,\n        IWebHostEnvironment environment,\n        GlobalSettings globalSettings,\n        ILogger<Startup> logger)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n\n        if (!environment.IsDevelopment())\n        {\n            var uri = new Uri(globalSettings.BaseServiceUri.Identity);\n            app.Use(async (ctx, next) =>\n            {\n                ctx.RequestServices.GetRequiredService<IServerUrls>().Origin = $\"{uri.Scheme}://{uri.Host}\";\n                await next();\n            });\n        }\n\n        if (globalSettings.SelfHosted)\n        {\n            app.UsePathBase(\"/identity\");\n            app.UseForwardedHeaders(globalSettings);\n        }\n\n        // Default Middleware\n        app.UseDefaultMiddleware(environment, globalSettings);\n\n        if (!globalSettings.SelfHosted)\n        {\n            // Rate limiting\n            app.UseMiddleware<CustomIpRateLimitMiddleware>();\n        }\n\n        if (environment.IsDevelopment())\n        {\n            app.UseSwagger();\n            app.UseDeveloperExceptionPage();\n            app.UseCookiePolicy();\n        }\n\n        // Add localization\n        app.UseCoreLocalization();\n\n        // Add static files to the request pipeline.\n        app.UseStaticFiles();\n\n        // Add routing\n        app.UseRouting();\n\n        // Add Cors\n        app.UseCors(policy => policy.SetIsOriginAllowed(o =>\n                CoreHelpers.IsCorsOriginAllowed(o, globalSettings) ||\n\n                // If development - allow requests from the Swagger UI so it can authorize\n                (Environment.IsDevelopment() && o == globalSettings.BaseServiceUri.Api))\n            .AllowAnyMethod().AllowAnyHeader().AllowCredentials());\n\n        // Add current context\n        app.UseMiddleware<CurrentContextMiddleware>();\n\n        // Add IdentityServer to the request pipeline.\n        app.UseIdentityServer();\n\n        // Add Mvc stuff\n        app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());\n\n        // Log startup\n        logger.LogInformation(Constants.BypassFiltersEventId, \"{Project} started.\", globalSettings.ProjectName);\n    }\n}\n"
  },
  {
    "path": "src/Identity/Utilities/DiscoveryResponseGenerator.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Duende.IdentityServer.Configuration;\nusing Duende.IdentityServer.Services;\nusing Duende.IdentityServer.Stores;\nusing Duende.IdentityServer.Validation;\n\nnamespace Bit.Identity.Utilities;\n\npublic class DiscoveryResponseGenerator : Duende.IdentityServer.ResponseHandling.DiscoveryResponseGenerator\n{\n    private readonly GlobalSettings _globalSettings;\n\n    public DiscoveryResponseGenerator(\n        IdentityServerOptions options,\n        IResourceStore resourceStore,\n        IKeyMaterialService keys,\n        ExtensionGrantValidator extensionGrants,\n        ISecretsListParser secretParsers,\n        IResourceOwnerPasswordValidator resourceOwnerValidator,\n        ILogger<DiscoveryResponseGenerator> logger,\n        GlobalSettings globalSettings)\n        : base(options, resourceStore, keys, extensionGrants, secretParsers, resourceOwnerValidator, logger)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public override async Task<Dictionary<string, object>> CreateDiscoveryDocumentAsync(\n        string baseUrl, string issuerUri)\n    {\n        var dict = await base.CreateDiscoveryDocumentAsync(baseUrl, issuerUri);\n        return CoreHelpers.AdjustIdentityServerConfig(dict, _globalSettings.BaseServiceUri.Identity,\n            _globalSettings.BaseServiceUri.InternalIdentity);\n    }\n}\n"
  },
  {
    "path": "src/Identity/Utilities/LoginApprovingClientTypes.cs",
    "content": "﻿using Bit.Core.Enums;\n\nnamespace Bit.Identity.Utilities;\n\npublic interface ILoginApprovingClientTypes\n{\n    IReadOnlyCollection<ClientType> TypesThatCanApprove { get; }\n}\n\npublic class LoginApprovingClientTypes : ILoginApprovingClientTypes\n{\n    public LoginApprovingClientTypes()\n    {\n        TypesThatCanApprove = new List<ClientType>\n        {\n            ClientType.Desktop,\n            ClientType.Mobile,\n            ClientType.Web,\n            ClientType.Browser,\n        };\n    }\n\n    public IReadOnlyCollection<ClientType> TypesThatCanApprove { get; }\n}\n"
  },
  {
    "path": "src/Identity/Utilities/ServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Utilities;\nusing Bit.Identity.IdentityServer;\nusing Bit.Identity.IdentityServer.ClientProviders;\nusing Bit.Identity.IdentityServer.RequestValidators;\nusing Bit.Identity.IdentityServer.RequestValidators.SendAccess;\nusing Bit.SharedWeb.Utilities;\nusing Duende.IdentityServer.ResponseHandling;\nusing Duende.IdentityServer.Services;\nusing Duende.IdentityServer.Stores;\n\nnamespace Bit.Identity.Utilities;\n\npublic static class ServiceCollectionExtensions\n{\n    public static IIdentityServerBuilder AddCustomIdentityServerServices(this IServiceCollection services,\n        IWebHostEnvironment env, GlobalSettings globalSettings)\n    {\n        services.AddTransient<IDiscoveryResponseGenerator, DiscoveryResponseGenerator>();\n\n        services.AddSingleton<StaticClientStore>();\n        services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();\n        services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();\n        services.AddTransient<IDeviceValidator, DeviceValidator>();\n        services.AddTransient<IClientVersionValidator, ClientVersionValidator>();\n        services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();\n        services.AddTransient<ISsoRequestValidator, SsoRequestValidator>();\n        services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();\n        services.AddTransient<ISendAuthenticationMethodValidator<ResourcePassword>, SendPasswordRequestValidator>();\n        services.AddTransient<ISendAuthenticationMethodValidator<EmailOtp>, SendEmailOtpRequestValidator>();\n        services.AddTransient<ISendAuthenticationMethodValidator<NeverAuthenticate>, SendNeverAuthenticateRequestValidator>();\n\n        var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);\n        var identityServerBuilder = services\n            .AddIdentityServer(options =>\n            {\n                options.LicenseKey = globalSettings.IdentityServer.LicenseKey;\n                options.Endpoints.EnableIntrospectionEndpoint = false;\n                options.Endpoints.EnableEndSessionEndpoint = false;\n                options.Endpoints.EnableUserInfoEndpoint = false;\n                options.Endpoints.EnableCheckSessionEndpoint = false;\n                options.Endpoints.EnableTokenRevocationEndpoint = false;\n                options.IssuerUri = $\"{issuerUri.Scheme}://{issuerUri.Host}\";\n                options.Caching.ClientStoreExpiration = new TimeSpan(0, 5, 0);\n                if (env.IsDevelopment())\n                {\n                    options.Authentication.CookieSameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;\n                }\n                options.InputLengthRestrictions.UserName = 256;\n                options.KeyManagement.Enabled = false;\n                options.UserInteraction.LoginUrl = \"/sso/Login\";\n            })\n            .AddInMemoryCaching()\n            .AddInMemoryApiResources(ApiResources.GetApiResources())\n            .AddInMemoryApiScopes(ApiScopes.GetApiScopes())\n            .AddClientStoreCache<DynamicClientStore>()\n            .AddCustomTokenRequestValidator<CustomTokenRequestValidator>()\n            .AddProfileService<ProfileService>()\n            .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()\n            .AddClientStore<DynamicClientStore>()\n            .AddIdentityServerCertificate(env, globalSettings)\n            .AddExtensionGrantValidator<WebAuthnGrantValidator>()\n            .AddExtensionGrantValidator<SendAccessGrantValidator>();\n\n        if (!globalSettings.SelfHosted)\n        {\n            // Only cloud instances should be able to handle installations\n            services.AddClientProvider<InstallationClientProvider>(\"installation\");\n        }\n\n        if (globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey))\n        {\n            services.AddClientProvider<InternalClientProvider>(\"internal\");\n        }\n\n        services.AddClientProvider<UserClientProvider>(\"user\");\n        services.AddClientProvider<OrganizationClientProvider>(\"organization\");\n        services.AddClientProvider<SecretsManagerApiKeyProvider>(SecretsManagerApiKeyProvider.ApiKeyPrefix);\n\n        if (CoreHelpers.SettingHasValue(globalSettings.IdentityServer.CosmosConnectionString))\n        {\n            services.AddSingleton<IPersistedGrantStore>(sp =>\n                new PersistedGrantStore(sp.GetRequiredKeyedService<IGrantRepository>(\"cosmos\"),\n                    g => new Core.Auth.Models.Data.GrantItem(g)));\n        }\n        else\n        {\n            services.AddTransient<IPersistedGrantStore>(sp =>\n                new PersistedGrantStore(sp.GetRequiredService<IGrantRepository>(),\n                    g => new Core.Auth.Entities.Grant(g)));\n        }\n\n        services.AddTransient<ICorsPolicyService, CustomCorsPolicyService>();\n        return identityServerBuilder;\n    }\n}\n"
  },
  {
    "path": "src/Identity/Views/Shared/Redirect.cshtml",
    "content": "﻿@model Bit.Identity.Models.RedirectViewModel\n<html>\n<head>\n    <meta http-equiv=\"refresh\" content=\"0;url=@Model.RedirectUrl\" data-url=\"@Model.RedirectUrl\">\n    <script>\n        window.location.href = document.querySelector(\"meta[http-equiv=refresh]\").getAttribute(\"data-url\");\n    </script>\n</head>\n<body>\n    <p>You are now being returned to the application. Once complete, you may close this tab.</p>\n</body>\n</html>\n"
  },
  {
    "path": "src/Identity/appsettings.Development.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\"\n    },\n    \"mail\": {\n      \"smtp\": {\n        \"host\": \"localhost\",\n        \"port\": 10250\n      }\n    },\n    \"attachment\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"events\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"notifications\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"storage\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    },\n    \"developmentDirectory\": \"../../dev\"\n  }\n}\n"
  },
  {
    "path": "src/Identity/appsettings.Production.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.bitwarden.com\",\n      \"api\": \"https://api.bitwarden.com\",\n      \"identity\": \"https://identity.bitwarden.com\",\n      \"admin\": \"https://admin.bitwarden.com\",\n      \"notifications\": \"https://notifications.bitwarden.com\",\n      \"sso\": \"https://sso.bitwarden.com\",\n      \"internalNotifications\": \"https://notifications.bitwarden.com\",\n      \"internalAdmin\": \"https://admin.bitwarden.com\",\n      \"internalIdentity\": \"https://identity.bitwarden.com\",\n      \"internalApi\": \"https://api.bitwarden.com\",\n      \"internalVault\": \"https://vault.bitwarden.com\",\n      \"internalSso\": \"https://sso.bitwarden.com\",\n      \"internalScim\": \"https://scim.bitwarden.com\"\n    },\n    \"braintree\": {\n      \"production\": true\n    }\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Warning\",\n          \"System\": \"Warning\",\n          \"Microsoft\": \"Warning\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Identity/appsettings.QA.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.qa.bitwarden.pw\",\n      \"api\": \"https://api.qa.bitwarden.pw\",\n      \"identity\": \"https://identity.qa.bitwarden.pw\",\n      \"admin\": \"https://admin.qa.bitwarden.pw\",\n      \"notifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"sso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalNotifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"internalAdmin\": \"https://admin.qa.bitwarden.pw\",\n      \"internalIdentity\": \"https://identity.qa.bitwarden.pw\",\n      \"internalApi\": \"https://api.qa.bitwarden.pw\",\n      \"internalVault\": \"https://vault.qa.bitwarden.pw\",\n      \"internalSso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalScim\": \"https://scim.qa.bitwarden.pw\"\n    },\n    \"braintree\": {\n      \"production\": false\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Debug\",\n          \"System\": \"Debug\",\n          \"Microsoft\": \"Debug\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Identity/appsettings.SelfHosted.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": null,\n      \"api\": null,\n      \"identity\": null,\n      \"admin\": null,\n      \"notifications\": null,\n      \"sso\": null,\n      \"internalNotifications\": null,\n      \"internalAdmin\": null,\n      \"internalIdentity\": null,\n      \"internalApi\": null,\n      \"internalVault\": null,\n      \"internalSso\": null,\n      \"internalScim\": null\n    }\n  }\n}\n"
  },
  {
    "path": "src/Identity/appsettings.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"selfHosted\": false,\n    \"siteName\": \"Bitwarden\",\n    \"projectName\": \"Identity\",\n    \"stripe\": {\n      \"apiKey\": \"SECRET\"\n    },\n    \"oidcIdentityClientKey\": \"SECRET\",\n    \"sqlServer\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"mail\": {\n      \"sendGridApiKey\": \"SECRET\",\n      \"amazonConfigSetName\": \"Email\",\n      \"replyToEmail\": \"no-reply@bitwarden.com\"\n    },\n    \"identityServer\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"dataProtection\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"storage\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"events\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"notificationHub\": {\n      \"connectionString\": \"SECRET\",\n      \"hubName\": \"SECRET\"\n    },\n    \"serviceBus\": {\n      \"connectionString\": \"SECRET\",\n      \"applicationCacheTopicName\": \"SECRET\"\n    },\n    \"yubico\": {\n      \"clientid\": \"SECRET\",\n      \"key\": \"SECRET\"\n    },\n    \"duo\": {\n      \"aKey\": \"SECRET\"\n    },\n    \"braintree\": {\n      \"production\": false,\n      \"merchantId\": \"SECRET\",\n      \"publicKey\": \"SECRET\",\n      \"privateKey\": \"SECRET\"\n    },\n    \"amazon\": {\n      \"accessKeyId\": \"SECRET\",\n      \"accessKeySecret\": \"SECRET\",\n      \"region\": \"SECRET\"\n    },\n    \"distributedIpRateLimiting\": {\n      \"enabled\": true,\n      \"maxRedisTimeoutsThreshold\": 10,\n      \"slidingWindowSeconds\": 120\n    }\n  },\n  \"IpRateLimitOptions\": {\n    \"EnableEndpointRateLimiting\": true,\n    \"StackBlockedRequests\": false,\n    \"RealIpHeader\": \"X-Connecting-IP\",\n    \"ClientIdHeader\": \"X-ClientId\",\n    \"HttpStatusCode\": 429,\n    \"IpWhitelist\": [],\n    \"EndpointWhitelist\": [],\n    \"ClientWhitelist\": [],\n    \"GeneralRules\": [\n      {\n        \"Endpoint\": \"*\",\n        \"Period\": \"1m\",\n        \"Limit\": 60\n      },\n      {\n        \"Endpoint\": \"*\",\n        \"Period\": \"1s\",\n        \"Limit\": 5\n      },\n      {\n        \"Endpoint\": \"post:/connect/token\",\n        \"Period\": \"1m\",\n        \"Limit\": 10\n      }\n    ]\n  },\n  \"IpRateLimitPolicies\": {\n    \"IpRules\": []\n  }\n}\n"
  },
  {
    "path": "src/Identity/build.ps1",
    "content": "$dir = Split-Path -Parent $MyInvocation.MyCommand.Path\n\necho \"`n## Building Identity\"\n\necho \"`nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore $dir\\Identity.csproj\necho \"Clean\"\ndotnet clean $dir\\Identity.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\necho \"Publish\"\ndotnet publish $dir\\Identity.csproj -c \"Release\" -o $dir\\obj\\Azure\\publish\n"
  },
  {
    "path": "src/Identity/build.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\necho -e \"\\n## Building Identity\"\n\necho -e \"\\nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore \"$DIR/Identity.csproj\"\necho \"Clean\"\ndotnet clean \"$DIR/Identity.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\necho \"Publish\"\ndotnet publish \"$DIR/Identity.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\n"
  },
  {
    "path": "src/Identity/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/core\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    if [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n      chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos\n    fi\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nif [ -f \"/etc/bitwarden/kerberos/bitwarden.keytab\" ] && [ -f \"/etc/bitwarden/kerberos/krb5.conf\" ]; then\n    cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf\n    $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab\nfi\n\nif [ \"$globalSettings__selfHosted\" = \"true\" ]; then\n    if [ -z \"$globalSettings__identityServer__certificateLocation\" ]; then\n        export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx\n    fi\nfi\n\nexec $gosu_cmd /app/Identity\n"
  },
  {
    "path": "src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Infrastructure.Dapper.Utilities;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.AdminConsole.Helpers;\n\npublic static class BulkResourceCreationService\n{\n    private const string _defaultErrorMessage = \"Must have at least one record for bulk creation.\";\n    public static async Task CreateCollectionsUsersAsync(SqlConnection connection, SqlTransaction transaction,\n        IEnumerable<CollectionUser> collectionUsers, string errorMessage = _defaultErrorMessage)\n    {\n        // Offload some work from SQL Server by pre-sorting before insert.\n        // This lets us use the SqlBulkCopy.ColumnOrderHints to improve performance and reduce deadlocks.\n        var sortedCollectionUsers = collectionUsers\n            .OrderBySqlGuid(cu => cu.CollectionId)\n            .ThenBySqlGuid(cu => cu.OrganizationUserId)\n            .ToList();\n\n        using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction);\n        bulkCopy.DestinationTableName = \"[dbo].[CollectionUser]\";\n        bulkCopy.BatchSize = 500;\n        bulkCopy.BulkCopyTimeout = 120;\n        bulkCopy.EnableStreaming = true;\n        bulkCopy.ColumnOrderHints.Add(\"CollectionId\", SortOrder.Ascending);\n        bulkCopy.ColumnOrderHints.Add(\"OrganizationUserId\", SortOrder.Ascending);\n\n        var dataTable = BuildCollectionsUsersTable(bulkCopy, sortedCollectionUsers, errorMessage);\n        await bulkCopy.WriteToServerAsync(dataTable);\n    }\n\n    public static async Task CreateCiphersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable<Cipher> ciphers, string errorMessage = _defaultErrorMessage)\n    {\n        using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction);\n        bulkCopy.DestinationTableName = \"[dbo].[Cipher]\";\n        var dataTable = BuildCiphersTable(bulkCopy, ciphers, errorMessage);\n        await bulkCopy.WriteToServerAsync(dataTable);\n    }\n\n    public static async Task CreateFoldersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable<Folder> folders, string errorMessage = _defaultErrorMessage)\n    {\n        using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction);\n        bulkCopy.DestinationTableName = \"[dbo].[Folder]\";\n        var dataTable = BuildFoldersTable(bulkCopy, folders, errorMessage);\n        await bulkCopy.WriteToServerAsync(dataTable);\n    }\n\n    public static async Task CreateCollectionCiphersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable<CollectionCipher> collectionCiphers, string errorMessage = _defaultErrorMessage)\n    {\n        using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction);\n        bulkCopy.DestinationTableName = \"[dbo].[CollectionCipher]\";\n        var dataTable = BuildCollectionCiphersTable(bulkCopy, collectionCiphers, errorMessage);\n        await bulkCopy.WriteToServerAsync(dataTable);\n    }\n\n    public static async Task CreateTempCiphersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable<Cipher> ciphers, string errorMessage = _defaultErrorMessage)\n    {\n        using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction);\n        bulkCopy.DestinationTableName = \"#TempCipher\";\n        var dataTable = BuildCiphersTable(bulkCopy, ciphers, errorMessage);\n        await bulkCopy.WriteToServerAsync(dataTable);\n    }\n\n    private static DataTable BuildCollectionsUsersTable(SqlBulkCopy bulkCopy, IEnumerable<CollectionUser> collectionUsers, string errorMessage)\n    {\n        var collectionUser = collectionUsers.FirstOrDefault();\n\n        if (collectionUser == null)\n        {\n            throw new ApplicationException(errorMessage);\n        }\n\n        var table = new DataTable(\"CollectionUserDataTable\");\n\n        var collectionIdColumn = new DataColumn(nameof(collectionUser.CollectionId), collectionUser.CollectionId.GetType());\n        table.Columns.Add(collectionIdColumn);\n        var orgUserIdColumn = new DataColumn(nameof(collectionUser.OrganizationUserId), collectionUser.OrganizationUserId.GetType());\n        table.Columns.Add(orgUserIdColumn);\n        var readOnlyColumn = new DataColumn(nameof(collectionUser.ReadOnly), collectionUser.ReadOnly.GetType());\n        table.Columns.Add(readOnlyColumn);\n        var hidePasswordsColumn = new DataColumn(nameof(collectionUser.HidePasswords), collectionUser.HidePasswords.GetType());\n        table.Columns.Add(hidePasswordsColumn);\n        var manageColumn = new DataColumn(nameof(collectionUser.Manage), collectionUser.Manage.GetType());\n        table.Columns.Add(manageColumn);\n\n        foreach (DataColumn col in table.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[2];\n        keys[0] = collectionIdColumn;\n        keys[1] = orgUserIdColumn;\n        table.PrimaryKey = keys;\n\n        foreach (var collectionUserRecord in collectionUsers)\n        {\n            var row = table.NewRow();\n\n            row[collectionIdColumn] = collectionUserRecord.CollectionId;\n            row[orgUserIdColumn] = collectionUserRecord.OrganizationUserId;\n            row[readOnlyColumn] = collectionUserRecord.ReadOnly;\n            row[hidePasswordsColumn] = collectionUserRecord.HidePasswords;\n            row[manageColumn] = collectionUserRecord.Manage;\n\n            table.Rows.Add(row);\n        }\n\n        return table;\n    }\n\n    public static async Task CreateCollectionsAsync(SqlConnection connection, SqlTransaction transaction,\n        IEnumerable<Collection> collections, string errorMessage = _defaultErrorMessage)\n    {\n        // Offload some work from SQL Server by pre-sorting before insert.\n        // This lets us use the SqlBulkCopy.ColumnOrderHints to improve performance and reduce deadlocks.\n        var sortedCollections = collections.OrderBySqlGuid(c => c.Id).ToList();\n\n        using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction);\n        bulkCopy.DestinationTableName = \"[dbo].[Collection]\";\n        bulkCopy.BatchSize = 500;\n        bulkCopy.BulkCopyTimeout = 120;\n        bulkCopy.EnableStreaming = true;\n        bulkCopy.ColumnOrderHints.Add(\"Id\", SortOrder.Ascending);\n\n        var dataTable = BuildCollectionsTable(bulkCopy, sortedCollections, errorMessage);\n        await bulkCopy.WriteToServerAsync(dataTable);\n    }\n\n    private static DataTable BuildCollectionsTable(SqlBulkCopy bulkCopy, IEnumerable<Collection> collections, string errorMessage)\n    {\n        var collection = collections.FirstOrDefault();\n\n        if (collection == null)\n        {\n            throw new ApplicationException(errorMessage);\n        }\n\n        var collectionsTable = new DataTable(\"CollectionDataTable\");\n\n        var idColumn = new DataColumn(nameof(collection.Id), collection.Id.GetType());\n        collectionsTable.Columns.Add(idColumn);\n        var organizationIdColumn = new DataColumn(nameof(collection.OrganizationId), collection.OrganizationId.GetType());\n        collectionsTable.Columns.Add(organizationIdColumn);\n        var nameColumn = new DataColumn(nameof(collection.Name), collection.Name.GetType());\n        collectionsTable.Columns.Add(nameColumn);\n        var creationDateColumn = new DataColumn(nameof(collection.CreationDate), collection.CreationDate.GetType());\n        collectionsTable.Columns.Add(creationDateColumn);\n        var revisionDateColumn = new DataColumn(nameof(collection.RevisionDate), collection.RevisionDate.GetType());\n        collectionsTable.Columns.Add(revisionDateColumn);\n        var externalIdColumn = new DataColumn(nameof(collection.ExternalId), typeof(string));\n        collectionsTable.Columns.Add(externalIdColumn);\n        var typeColumn = new DataColumn(nameof(collection.Type), collection.Type.GetType());\n        collectionsTable.Columns.Add(typeColumn);\n        var defaultUserCollectionEmailColumn = new DataColumn(nameof(collection.DefaultUserCollectionEmail), typeof(string));\n        collectionsTable.Columns.Add(defaultUserCollectionEmailColumn);\n\n        foreach (DataColumn col in collectionsTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[1];\n        keys[0] = idColumn;\n        collectionsTable.PrimaryKey = keys;\n\n        foreach (var collectionRecord in collections)\n        {\n            var row = collectionsTable.NewRow();\n\n            row[idColumn] = collectionRecord.Id;\n            row[organizationIdColumn] = collectionRecord.OrganizationId;\n            row[nameColumn] = collectionRecord.Name;\n            row[creationDateColumn] = collectionRecord.CreationDate;\n            row[revisionDateColumn] = collectionRecord.RevisionDate;\n            row[externalIdColumn] = collectionRecord.ExternalId;\n            row[typeColumn] = collectionRecord.Type;\n            row[defaultUserCollectionEmailColumn] = collectionRecord.DefaultUserCollectionEmail;\n\n            collectionsTable.Rows.Add(row);\n        }\n\n        return collectionsTable;\n    }\n\n    private static DataTable BuildCiphersTable(SqlBulkCopy bulkCopy, IEnumerable<Cipher> ciphers, string errorMessage)\n    {\n        var c = ciphers.FirstOrDefault();\n\n        if (c == null)\n        {\n            throw new ApplicationException(errorMessage);\n        }\n\n        var ciphersTable = new DataTable(\"CipherDataTable\");\n\n        var idColumn = new DataColumn(nameof(c.Id), c.Id.GetType());\n        ciphersTable.Columns.Add(idColumn);\n        var userIdColumn = new DataColumn(nameof(c.UserId), typeof(Guid));\n        ciphersTable.Columns.Add(userIdColumn);\n        var organizationId = new DataColumn(nameof(c.OrganizationId), typeof(Guid));\n        ciphersTable.Columns.Add(organizationId);\n        var typeColumn = new DataColumn(nameof(c.Type), typeof(short));\n        ciphersTable.Columns.Add(typeColumn);\n        var dataColumn = new DataColumn(nameof(c.Data), typeof(string));\n        ciphersTable.Columns.Add(dataColumn);\n        var favoritesColumn = new DataColumn(nameof(c.Favorites), typeof(string));\n        ciphersTable.Columns.Add(favoritesColumn);\n        var foldersColumn = new DataColumn(nameof(c.Folders), typeof(string));\n        ciphersTable.Columns.Add(foldersColumn);\n        var attachmentsColumn = new DataColumn(nameof(c.Attachments), typeof(string));\n        ciphersTable.Columns.Add(attachmentsColumn);\n        var creationDateColumn = new DataColumn(nameof(c.CreationDate), c.CreationDate.GetType());\n        ciphersTable.Columns.Add(creationDateColumn);\n        var revisionDateColumn = new DataColumn(nameof(c.RevisionDate), c.RevisionDate.GetType());\n        ciphersTable.Columns.Add(revisionDateColumn);\n        var deletedDateColumn = new DataColumn(nameof(c.DeletedDate), typeof(DateTime));\n        ciphersTable.Columns.Add(deletedDateColumn);\n        var archivesColumn = new DataColumn(nameof(c.Archives), typeof(string));\n        ciphersTable.Columns.Add(archivesColumn);\n        var repromptColumn = new DataColumn(nameof(c.Reprompt), typeof(short));\n        ciphersTable.Columns.Add(repromptColumn);\n        var keyColummn = new DataColumn(nameof(c.Key), typeof(string));\n        ciphersTable.Columns.Add(keyColummn);\n\n        foreach (DataColumn col in ciphersTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[1];\n        keys[0] = idColumn;\n        ciphersTable.PrimaryKey = keys;\n\n        foreach (var cipher in ciphers)\n        {\n            var row = ciphersTable.NewRow();\n\n            row[idColumn] = cipher.Id;\n            row[userIdColumn] = cipher.UserId.HasValue ? (object)cipher.UserId.Value : DBNull.Value;\n            row[organizationId] = cipher.OrganizationId.HasValue ? (object)cipher.OrganizationId.Value : DBNull.Value;\n            row[typeColumn] = (short)cipher.Type;\n            row[dataColumn] = cipher.Data;\n            row[favoritesColumn] = cipher.Favorites;\n            row[foldersColumn] = cipher.Folders;\n            row[attachmentsColumn] = cipher.Attachments;\n            row[creationDateColumn] = cipher.CreationDate;\n            row[revisionDateColumn] = cipher.RevisionDate;\n            row[deletedDateColumn] = cipher.DeletedDate.HasValue ? (object)cipher.DeletedDate : DBNull.Value;\n            row[archivesColumn] = cipher.Archives;\n            row[repromptColumn] = cipher.Reprompt.HasValue ? cipher.Reprompt.Value : DBNull.Value;\n            row[keyColummn] = cipher.Key;\n\n            ciphersTable.Rows.Add(row);\n        }\n\n        return ciphersTable;\n    }\n\n    private static DataTable BuildFoldersTable(SqlBulkCopy bulkCopy, IEnumerable<Folder> folders, string errorMessage)\n    {\n        var f = folders.FirstOrDefault();\n\n        if (f == null)\n        {\n            throw new ApplicationException(errorMessage);\n        }\n\n        var foldersTable = new DataTable(\"FolderDataTable\");\n\n        var idColumn = new DataColumn(nameof(f.Id), f.Id.GetType());\n        foldersTable.Columns.Add(idColumn);\n        var userIdColumn = new DataColumn(nameof(f.UserId), f.UserId.GetType());\n        foldersTable.Columns.Add(userIdColumn);\n        var nameColumn = new DataColumn(nameof(f.Name), typeof(string));\n        foldersTable.Columns.Add(nameColumn);\n        var creationDateColumn = new DataColumn(nameof(f.CreationDate), f.CreationDate.GetType());\n        foldersTable.Columns.Add(creationDateColumn);\n        var revisionDateColumn = new DataColumn(nameof(f.RevisionDate), f.RevisionDate.GetType());\n        foldersTable.Columns.Add(revisionDateColumn);\n\n        foreach (DataColumn col in foldersTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[1];\n        keys[0] = idColumn;\n        foldersTable.PrimaryKey = keys;\n\n        foreach (var folder in folders)\n        {\n            var row = foldersTable.NewRow();\n\n            row[idColumn] = folder.Id;\n            row[userIdColumn] = folder.UserId;\n            row[nameColumn] = folder.Name;\n            row[creationDateColumn] = folder.CreationDate;\n            row[revisionDateColumn] = folder.RevisionDate;\n\n            foldersTable.Rows.Add(row);\n        }\n\n        return foldersTable;\n    }\n\n    private static DataTable BuildCollectionCiphersTable(SqlBulkCopy bulkCopy, IEnumerable<CollectionCipher> collectionCiphers, string errorMessage)\n    {\n        var cc = collectionCiphers.FirstOrDefault();\n\n        if (cc == null)\n        {\n            throw new ApplicationException(errorMessage);\n        }\n\n        var collectionCiphersTable = new DataTable(\"CollectionCipherDataTable\");\n\n        var collectionIdColumn = new DataColumn(nameof(cc.CollectionId), cc.CollectionId.GetType());\n        collectionCiphersTable.Columns.Add(collectionIdColumn);\n        var cipherIdColumn = new DataColumn(nameof(cc.CipherId), cc.CipherId.GetType());\n        collectionCiphersTable.Columns.Add(cipherIdColumn);\n\n        foreach (DataColumn col in collectionCiphersTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[2];\n        keys[0] = collectionIdColumn;\n        keys[1] = cipherIdColumn;\n        collectionCiphersTable.PrimaryKey = keys;\n\n        foreach (var collectionCipher in collectionCiphers)\n        {\n            var row = collectionCiphersTable.NewRow();\n\n            row[collectionIdColumn] = collectionCipher.CollectionId;\n            row[cipherIdColumn] = collectionCipher.CipherId;\n\n            collectionCiphersTable.Rows.Add(row);\n        }\n\n        return collectionCiphersTable;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/AdminConsole/Repositories/GroupRepository.cs",
    "content": "﻿using System.Data;\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.AdminConsole.Repositories;\n\npublic class GroupRepository : Repository<Group, Guid>, IGroupRepository\n{\n    public GroupRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public GroupRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<Tuple<Group?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryMultipleAsync(\n                $\"[{Schema}].[Group_ReadWithCollectionsById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            var group = await results.ReadFirstOrDefaultAsync<Group>();\n            var colletions = (await results.ReadAsync<CollectionAccessSelection>()).ToList();\n\n            return new Tuple<Group?, ICollection<CollectionAccessSelection>>(group, colletions);\n        }\n    }\n\n    public async Task<ICollection<Group>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Group>(\n                $\"[{Schema}].[Group_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<Tuple<Group, ICollection<CollectionAccessSelection>>>> GetManyWithCollectionsByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryMultipleAsync(\n                $\"[{Schema}].[Group_ReadWithCollectionsByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            var groups = (await results.ReadAsync<Group>()).ToList();\n            var collections = (await results.ReadAsync<CollectionGroup>())\n                .GroupBy(c => c.GroupId)\n                .ToList();\n\n            return groups.Select(group =>\n                    new Tuple<Group, ICollection<CollectionAccessSelection>>(\n                        group,\n                        collections.FirstOrDefault(c => c.Key == group.Id)?\n                            .Select(c => new CollectionAccessSelection\n                            {\n                                Id = c.CollectionId,\n                                HidePasswords = c.HidePasswords,\n                                ReadOnly = c.ReadOnly,\n                                Manage = c.Manage\n                            }\n                            ).ToList() ?? new List<CollectionAccessSelection>())\n                ).ToList();\n        }\n    }\n\n    public async Task<ICollection<Group>> GetManyByManyIds(IEnumerable<Guid> groupIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Group>(\n                $\"[{Schema}].[Group_ReadByIds]\",\n                new { Ids = groupIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<Guid>> GetManyIdsByUserIdAsync(Guid organizationUserId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Guid>(\n                $\"[{Schema}].[GroupUser_ReadGroupIdsByOrganizationUserId]\",\n                new { OrganizationUserId = organizationUserId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<Guid>> GetManyUserIdsByIdAsync(Guid id, bool useReadOnlyReplica = false)\n    {\n        var connectionString = useReadOnlyReplica\n            ? ReadOnlyConnectionString\n            : ConnectionString;\n\n        using (var connection = new SqlConnection(connectionString))\n        {\n            var results = await connection.QueryAsync<Guid>(\n                $\"[{Schema}].[GroupUser_ReadOrganizationUserIdsByGroupId]\",\n                new { GroupId = id },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<GroupUser>> GetManyGroupUsersByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<GroupUser>(\n                $\"[{Schema}].[GroupUser_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task CreateAsync(Group obj, IEnumerable<CollectionAccessSelection> collections)\n    {\n        obj.SetNewId();\n        var objWithCollections = JsonSerializer.Deserialize<GroupWithCollections>(JsonSerializer.Serialize(obj))!;\n        objWithCollections.Collections = collections.ToArrayTVP();\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Group_CreateWithCollections]\",\n                objWithCollections,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task ReplaceAsync(Group obj, IEnumerable<CollectionAccessSelection> collections)\n    {\n        var objWithCollections = JsonSerializer.Deserialize<GroupWithCollections>(JsonSerializer.Serialize(obj))!;\n        objWithCollections.Collections = collections.ToArrayTVP();\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Group_UpdateWithCollections]\",\n                objWithCollections,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task DeleteUserAsync(Guid groupId, Guid organizationUserId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[GroupUser_Delete]\",\n                new { GroupId = groupId, OrganizationUserId = organizationUserId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task UpdateUsersAsync(Guid groupId, IEnumerable<Guid> organizationUserIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                \"[dbo].[GroupUser_UpdateUsers]\",\n                new { GroupId = groupId, OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task AddGroupUsersByIdAsync(Guid groupId, IEnumerable<Guid> organizationUserIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                \"[dbo].[GroupUser_AddUsers]\",\n                new { GroupId = groupId, OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> groupIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\"[dbo].[Group_DeleteByIds]\",\n                new { Ids = groupIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs",
    "content": "﻿using System.Data;\nusing System.Data.Common;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\nusing Microsoft.Extensions.Logging;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class OrganizationRepository : Repository<Organization, Guid>, IOrganizationRepository\n{\n    protected readonly ILogger<OrganizationRepository> _logger;\n\n    public OrganizationRepository(\n        GlobalSettings globalSettings,\n        ILogger<OrganizationRepository> logger)\n        : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n        _logger = logger;\n    }\n\n    public async Task<Organization?> GetByGatewayCustomerIdAsync(string gatewayCustomerId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Organization>(\n                \"[dbo].[Organization_ReadByGatewayCustomerId]\",\n                new { GatewayCustomerId = gatewayCustomerId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<Organization?> GetByGatewaySubscriptionIdAsync(string gatewaySubscriptionId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Organization>(\n                \"[dbo].[Organization_ReadByGatewaySubscriptionId]\",\n                new { GatewaySubscriptionId = gatewaySubscriptionId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<Organization?> GetByIdentifierAsync(string identifier)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Organization>(\n                \"[dbo].[Organization_ReadByIdentifier]\",\n                new { Identifier = identifier },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<ICollection<Organization>> GetManyByEnabledAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Organization>(\n                \"[dbo].[Organization_ReadByEnabled]\",\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<Organization>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Organization>(\n                \"[dbo].[Organization_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<Organization>> SearchAsync(string name, string userEmail, bool? paid,\n        int skip, int take)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<Organization>(\n                \"[dbo].[Organization_Search]\",\n                new { Name = name, UserEmail = userEmail, Paid = paid, Skip = skip, Take = take },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 120);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task UpdateStorageAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[Organization_UpdateStorage]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 180);\n        }\n    }\n\n    public async Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationAbility>(\n                \"[dbo].[Organization_ReadAbilities]\",\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<Organization?> GetByLicenseKeyAsync(string licenseKey)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.QueryAsync<Organization>(\n                \"[dbo].[Organization_ReadByLicenseKey]\",\n                new { LicenseKey = licenseKey },\n                commandType: CommandType.StoredProcedure);\n\n            return result.SingleOrDefault();\n        }\n    }\n\n    public async Task<SelfHostedOrganizationDetails?> GetSelfHostedOrganizationDetailsById(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.QueryMultipleAsync(\n                \"[dbo].[Organization_ReadSelfHostedDetailsById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            var selfHostOrganization = await result.ReadSingleOrDefaultAsync<SelfHostedOrganizationDetails>();\n            if (selfHostOrganization == null)\n            {\n                return null;\n            }\n\n            selfHostOrganization.OccupiedSeatCount = await result.ReadSingleAsync<int>();\n            selfHostOrganization.CollectionCount = await result.ReadSingleAsync<int>();\n            selfHostOrganization.GroupCount = await result.ReadSingleAsync<int>();\n            selfHostOrganization.OrganizationUsers = await result.ReadAsync<OrganizationUser>();\n            selfHostOrganization.Policies = await result.ReadAsync<Policy>();\n            selfHostOrganization.SsoConfig = await result.ReadFirstOrDefaultAsync<SsoConfig>();\n            selfHostOrganization.ScimConnections = await result.ReadAsync<OrganizationConnection>();\n\n            return selfHostOrganization;\n        }\n    }\n\n    public async Task<ICollection<Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<Organization>(\n                \"[dbo].[Organization_UnassignedToProviderSearch]\",\n                new { Name = name, OwnerEmail = ownerEmail, Skip = skip, Take = take },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 120);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId)\n    {\n        _logger.LogInformation(\"AC-1758: Executing GetOwnerEmailAddressesById (Dapper)\");\n\n        await using var connection = new SqlConnection(ConnectionString);\n\n        return await connection.QueryAsync<string>(\n            $\"[{Schema}].[{Table}_ReadOwnerEmailAddressesById]\",\n            new { OrganizationId = organizationId },\n            commandType: CommandType.StoredProcedure);\n    }\n\n    public async Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.QueryAsync<Organization>(\n                \"[dbo].[Organization_ReadByClaimedUserEmailDomain]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return result.ToList();\n        }\n    }\n\n    public async Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(\n        Guid userId,\n        ProviderType providerType)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.QueryAsync<Organization>(\n                $\"[{Schema}].[{Table}_ReadAddableToProviderByUserId]\",\n                new { UserId = userId, ProviderType = providerType },\n                commandType: CommandType.StoredProcedure);\n\n            return result.ToList();\n        }\n    }\n\n    public async Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n        return (await connection.QueryAsync<Organization>(\n            $\"[{Schema}].[{Table}_ReadManyByIds]\",\n            new { OrganizationIds = ids.ToGuidIdArrayTVP() },\n            commandType: CommandType.StoredProcedure))\n            .ToList();\n    }\n\n    public async Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.QueryAsync<OrganizationSeatCounts>(\n                \"[dbo].[Organization_ReadOccupiedSeatCountByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return result.SingleOrDefault() ?? new OrganizationSeatCounts();\n        }\n    }\n\n    public async Task<IEnumerable<Organization>> GetOrganizationsForSubscriptionSyncAsync()\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        return await connection.QueryAsync<Organization>(\n            \"[dbo].[Organization_GetOrganizationsForSubscriptionSync]\",\n            commandType: CommandType.StoredProcedure);\n    }\n\n    public async Task UpdateSuccessfulOrganizationSyncStatusAsync(IEnumerable<Guid> successfulOrganizations, DateTime syncDate)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        await connection.ExecuteAsync(\"[dbo].[Organization_UpdateSubscriptionStatus]\",\n            new\n            {\n                SuccessfulOrganizations = successfulOrganizations.ToGuidIdArrayTVP(),\n                SyncDate = syncDate\n            },\n            commandType: CommandType.StoredProcedure);\n    }\n\n    public async Task IncrementSeatCountAsync(Guid organizationId, int increaseAmount, DateTime requestDate)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        await connection.ExecuteAsync(\"[dbo].[Organization_IncrementSeatCount]\",\n            new { OrganizationId = organizationId, SeatsToAdd = increaseAmount, RequestDate = requestDate },\n            commandType: CommandType.StoredProcedure);\n    }\n\n    public async Task InitializeOrganizationAsync(Organization organization, Func<DbConnection, DbTransaction, Task> confirmOwnerAction)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n        await connection.OpenAsync();\n        await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();\n\n        try\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[Organization_Update]\",\n                organization,\n                commandType: CommandType.StoredProcedure,\n                transaction: transaction);\n\n            await confirmOwnerAction(connection, transaction);\n\n            await transaction.CommitAsync();\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex,\n                \"Failed to initialize organization. Rolling back transaction.\");\n            await transaction.RollbackAsync();\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs",
    "content": "﻿using System.Data;\nusing System.Data.Common;\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.Utilities.DebuggingInstruments;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IOrganizationUserRepository\n{\n    /// <summary>\n    /// For use with methods with TDS stream issues.\n    /// This has been observed in Linux-hosted SqlServers with large table-valued-parameters\n    /// https://github.com/dotnet/SqlClient/issues/54\n    /// </summary>\n    private string _marsConnectionString;\n    private readonly ILogger<OrganizationUserRepository> _logger;\n\n    public OrganizationUserRepository(GlobalSettings globalSettings, ILogger<OrganizationUserRepository> logger)\n        : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n        var builder = new SqlConnectionStringBuilder(ConnectionString)\n        {\n            MultipleActiveResultSets = true,\n        };\n        _marsConnectionString = builder.ToString();\n        _logger = logger;\n    }\n\n    public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<int>(\n                \"[dbo].[OrganizationUser_ReadCountByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results;\n        }\n    }\n\n    public async Task<int> GetCountByFreeOrganizationAdminUserAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<int>(\n                \"[dbo].[OrganizationUser_ReadCountByFreeOrganizationAdminUser]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results;\n        }\n    }\n\n    public async Task<int> GetCountByOnlyOwnerAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<int>(\n                \"[dbo].[OrganizationUser_ReadCountByOnlyOwner]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results;\n        }\n    }\n\n    public async Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.ExecuteScalarAsync<int>(\n                \"[dbo].[OrganizationUser_ReadCountByOrganizationIdEmail]\",\n                new { OrganizationId = organizationId, Email = email, OnlyUsers = onlyRegisteredUsers },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n\n    public async Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.ExecuteScalarAsync<int>(\n                \"[dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n\n    public async Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails,\n        bool onlyRegisteredUsers)\n    {\n        var emailsTvp = emails.ToArrayTVP(\"Email\");\n        using (var connection = new SqlConnection(_marsConnectionString))\n        {\n            var result = await connection.QueryAsync<string>(\n                \"[dbo].[OrganizationUser_SelectKnownEmails]\",\n                new { OrganizationId = organizationId, Emails = emailsTvp, OnlyUsers = onlyRegisteredUsers },\n                commandType: CommandType.StoredProcedure);\n\n            // Return as a list to avoid timing out the sql connection\n            return result.ToList();\n        }\n    }\n\n    public async Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUser>(\n                \"[dbo].[OrganizationUser_ReadByOrganizationIdUserId]\",\n                new { OrganizationId = organizationId, UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUser>(\n                \"[dbo].[OrganizationUser_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId,\n        OrganizationUserType? type)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUser>(\n                \"[dbo].[OrganizationUser_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId, Type = type },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryMultipleAsync(\n                \"[dbo].[OrganizationUser_ReadWithCollectionsById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            var user = (await results.ReadAsync<OrganizationUser>()).SingleOrDefault();\n            var collections = (await results.ReadAsync<CollectionAccessSelection>()).ToList();\n            return new Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>(user, collections);\n        }\n    }\n\n    public async Task<OrganizationUserUserDetails?> GetDetailsByIdAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUserUserDetails>(\n                \"[dbo].[OrganizationUserUserDetails_ReadById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n    public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithSharedCollectionsAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryMultipleAsync(\n                \"[dbo].[OrganizationUserUserDetails_ReadWithSharedCollectionsById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            var organizationUserUserDetails = (await results.ReadAsync<OrganizationUserUserDetails>()).SingleOrDefault();\n            var collections = (await results.ReadAsync<CollectionAccessSelection>()).ToList();\n            return (organizationUserUserDetails, collections);\n        }\n    }\n\n    public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeSharedCollections)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUserUserDetails>(\n                \"[dbo].[OrganizationUserUserDetails_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            List<IGrouping<Guid, GroupUser>>? userGroups = null;\n            List<IGrouping<Guid, CollectionUser>>? userCollections = null;\n\n            var users = results.ToList();\n\n            if (!includeSharedCollections && !includeGroups)\n            {\n                return users;\n            }\n\n            var orgUserIds = users.Select(u => u.Id).ToGuidIdArrayTVP();\n\n            if (includeGroups)\n            {\n                userGroups = (await connection.QueryAsync<GroupUser>(\n                    \"[dbo].[GroupUser_ReadByOrganizationUserIds]\",\n                    new { OrganizationUserIds = orgUserIds },\n                    commandType: CommandType.StoredProcedure)).GroupBy(u => u.OrganizationUserId).ToList();\n            }\n\n            if (includeSharedCollections)\n            {\n                userCollections = (await connection.QueryAsync<CollectionUser>(\n                    \"[dbo].[CollectionUser_ReadSharedCollectionsByOrganizationUserIds]\",\n                    new { OrganizationUserIds = orgUserIds },\n                    commandType: CommandType.StoredProcedure)).GroupBy(u => u.OrganizationUserId).ToList();\n            }\n\n            // Map any queried collections and groups to their respective users\n            foreach (var user in users)\n            {\n                if (userGroups != null)\n                {\n                    user.Groups = userGroups\n                        .FirstOrDefault(u => u.Key == user.Id)?\n                        .Select(ug => ug.GroupId).ToList() ?? new List<Guid>();\n                }\n\n                if (userCollections != null)\n                {\n                    user.Collections = userCollections\n                        .FirstOrDefault(u => u.Key == user.Id)?\n                        .Select(uc => new CollectionAccessSelection\n                        {\n                            Id = uc.CollectionId,\n                            ReadOnly = uc.ReadOnly,\n                            HidePasswords = uc.HidePasswords,\n                            Manage = uc.Manage\n                        }).ToList() ?? new List<CollectionAccessSelection>();\n                }\n            }\n\n            return users;\n        }\n    }\n\n    public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups, bool includeSharedCollections)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            // Use a single call that returns multiple result sets\n            var results = await connection.QueryMultipleAsync(\n                \"[dbo].[OrganizationUserUserDetails_ReadByOrganizationId_V2]\",\n                new\n                {\n                    OrganizationId = organizationId,\n                    IncludeGroups = includeGroups,\n                    IncludeCollections = includeSharedCollections\n                },\n                commandType: CommandType.StoredProcedure);\n\n            // Read the user details (first result set)\n            var users = (await results.ReadAsync<OrganizationUserUserDetails>()).ToList();\n\n            // Read group associations (second result set, if requested)\n            Dictionary<Guid, List<Guid>>? userGroupMap = null;\n            if (includeGroups)\n            {\n                var groupUsers = await results.ReadAsync<GroupUser>();\n                userGroupMap = groupUsers\n                    .GroupBy(gu => gu.OrganizationUserId)\n                    .ToDictionary(g => g.Key, g => g.Select(gu => gu.GroupId).ToList());\n            }\n\n            // Read collection associations (third result set, if requested)\n            Dictionary<Guid, List<CollectionAccessSelection>>? userCollectionMap = null;\n            if (includeSharedCollections)\n            {\n                var collectionUsers = await results.ReadAsync<CollectionUser>();\n                userCollectionMap = collectionUsers\n                    .GroupBy(cu => cu.OrganizationUserId)\n                    .ToDictionary(g => g.Key, g => g.Select(cu => new CollectionAccessSelection\n                    {\n                        Id = cu.CollectionId,\n                        ReadOnly = cu.ReadOnly,\n                        HidePasswords = cu.HidePasswords,\n                        Manage = cu.Manage\n                    }).ToList());\n            }\n\n            // Map the associations to users\n            foreach (var user in users)\n            {\n                if (userGroupMap != null)\n                {\n                    user.Groups = userGroupMap.GetValueOrDefault(user.Id, new List<Guid>());\n                }\n\n                if (userCollectionMap != null)\n                {\n                    user.Collections = userCollectionMap.GetValueOrDefault(user.Id, new List<CollectionAccessSelection>());\n                }\n            }\n\n            return users;\n        }\n    }\n\n    public async Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,\n        OrganizationUserStatusType? status = null)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUserOrganizationDetails>(\n                \"[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]\",\n                new { UserId = userId, Status = status },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<OrganizationUserOrganizationDetails?> GetDetailsByUserAsync(Guid userId,\n        Guid organizationId, OrganizationUserStatusType? status = null)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUserOrganizationDetails>(\n                \"[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]\",\n                new { UserId = userId, Status = status, OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task UpdateGroupsAsync(Guid orgUserId, IEnumerable<Guid> groupIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                \"[dbo].[GroupUser_UpdateGroups]\",\n                new { OrganizationUserId = orgUserId, GroupIds = groupIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<Guid> CreateAsync(OrganizationUser obj, IEnumerable<CollectionAccessSelection> collections)\n    {\n        _logger.LogUserInviteStateDiagnostics(obj);\n\n        obj.SetNewId();\n        var objWithCollections = JsonSerializer.Deserialize<OrganizationUserWithCollections>(\n            JsonSerializer.Serialize(obj))!;\n        objWithCollections.Collections = collections.ToArrayTVP();\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[OrganizationUser_CreateWithCollections]\",\n                objWithCollections,\n                commandType: CommandType.StoredProcedure);\n        }\n\n        return obj.Id;\n    }\n\n    public async Task ReplaceAsync(OrganizationUser obj, IEnumerable<CollectionAccessSelection> collections)\n    {\n        _logger.LogUserInviteStateDiagnostics(obj);\n\n        var objWithCollections = JsonSerializer.Deserialize<OrganizationUserWithCollections>(\n            JsonSerializer.Serialize(obj))!;\n        objWithCollections.Collections = collections.ToArrayTVP();\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[OrganizationUser_UpdateWithCollections]\",\n                objWithCollections,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<ICollection<OrganizationUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUser>(\n                \"[dbo].[OrganizationUser_ReadByUserIds]\",\n                new { UserIds = userIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<OrganizationUser>> GetManyAsync(IEnumerable<Guid> Ids)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUser>(\n                \"[dbo].[OrganizationUser_ReadByIds]\",\n                new { Ids = Ids.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<OrganizationUser?> GetByOrganizationEmailAsync(Guid organizationId, string email)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUser>(\n                \"[dbo].[OrganizationUser_ReadByOrganizationIdEmail]\",\n                new { OrganizationId = organizationId, Email = email },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> organizationUserIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\"[dbo].[OrganizationUser_DeleteByIds]\",\n                new { Ids = organizationUserIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task UpsertManyAsync(IEnumerable<OrganizationUser> organizationUsers)\n    {\n        var createUsers = new List<OrganizationUser>();\n        var replaceUsers = new List<OrganizationUser>();\n        foreach (var organizationUser in organizationUsers)\n        {\n            if (organizationUser.Id.Equals(default))\n            {\n                createUsers.Add(organizationUser);\n            }\n            else\n            {\n                replaceUsers.Add(organizationUser);\n            }\n        }\n\n        await CreateManyAsync(createUsers);\n        await ReplaceManyAsync(replaceUsers);\n    }\n\n    public async Task<ICollection<Guid>?> CreateManyAsync(IEnumerable<OrganizationUser> organizationUsers)\n    {\n        _logger.LogUserInviteStateDiagnostics(organizationUsers);\n\n        organizationUsers = organizationUsers.ToList();\n        if (!organizationUsers.Any())\n        {\n            return default;\n        }\n\n        foreach (var organizationUser in organizationUsers)\n        {\n            organizationUser.SetNewId();\n        }\n\n        using (var connection = new SqlConnection(_marsConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_CreateMany]\",\n                new { jsonData = JsonSerializer.Serialize(organizationUsers) },\n                commandType: CommandType.StoredProcedure);\n        }\n\n        return organizationUsers.Select(u => u.Id).ToList();\n    }\n\n    public async Task ReplaceManyAsync(IEnumerable<OrganizationUser> organizationUsers)\n    {\n        _logger.LogUserInviteStateDiagnostics(organizationUsers);\n\n        organizationUsers = organizationUsers.ToList();\n        if (!organizationUsers.Any())\n        {\n            return;\n        }\n\n        using (var connection = new SqlConnection(_marsConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_UpdateMany]\",\n                new { jsonData = JsonSerializer.Serialize(organizationUsers) },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationUserPublicKey>> GetManyPublicKeysByOrganizationUserAsync(\n        Guid organizationId, IEnumerable<Guid> Ids)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUserPublicKey>(\n                \"[dbo].[User_ReadPublicKeysByOrganizationUserIds]\",\n                new { OrganizationId = organizationId, OrganizationUserIds = Ids.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationUserUserDetails>> GetManyByMinimumRoleAsync(Guid organizationId, OrganizationUserType minRole)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUserUserDetails>(\n                \"[dbo].[OrganizationUser_ReadByMinimumRole]\",\n                new { OrganizationId = organizationId, MinRole = minRole },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task RevokeAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_Deactivate]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task RestoreAsync(Guid id, OrganizationUserStatusType status)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_Activate]\",\n                new { Id = id, Status = status },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUserPolicyDetails>(\n                $\"[{Schema}].[{Table}_ReadByUserIdWithPolicyDetails]\",\n                new { UserId = userId, PolicyType = policyType },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationUserResetPasswordDetails>> GetManyAccountRecoveryDetailsByOrganizationUserAsync(\n        Guid organizationId, IEnumerable<Guid> organizationUserIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUserResetPasswordDetails>(\n                \"[dbo].[OrganizationUser_ReadManyAccountRecoveryDetailsByOrganizationUserIds]\",\n                new { OrganizationId = organizationId, OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(\n        Guid userId, IEnumerable<OrganizationUser> resetPasswordKeys)\n    {\n        return async (connection, transaction) =>\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[OrganizationUser_UpdateDataForKeyRotation]\",\n                new { UserId = userId, OrganizationUserJson = JsonSerializer.Serialize(resetPasswordKeys) },\n                transaction: transaction,\n                commandType: CommandType.StoredProcedure);\n    }\n\n    public async Task<ICollection<OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUser>(\n                $\"[{Schema}].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        await connection.ExecuteAsync(\n            \"[dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]\",\n            new\n            {\n                OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP(),\n                Status = OrganizationUserStatusType.Revoked\n            },\n            commandType: CommandType.StoredProcedure);\n    }\n\n    public async Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationUserUserDetails>(\n                \"[dbo].[OrganizationUser_ReadManyDetailsByRole]\",\n                new { OrganizationId = organizationId, Role = role },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task CreateManyAsync(IEnumerable<CreateOrganizationUser> organizationUserCollection)\n    {\n        await using var connection = new SqlConnection(_marsConnectionString);\n\n        var organizationUsersList = organizationUserCollection.ToList();\n\n        await connection.ExecuteAsync(\n            $\"[{Schema}].[OrganizationUser_CreateManyWithCollectionsAndGroups]\",\n            new\n            {\n                OrganizationUserData = JsonSerializer.Serialize(organizationUsersList.Select(x => x.OrganizationUser)),\n                CollectionData = JsonSerializer.Serialize(organizationUsersList\n                    .SelectMany(x => x.Collections, (user, collection) => new CollectionUser\n                    {\n                        CollectionId = collection.Id,\n                        OrganizationUserId = user.OrganizationUser.Id,\n                        ReadOnly = collection.ReadOnly,\n                        HidePasswords = collection.HidePasswords,\n                        Manage = collection.Manage\n                    })),\n                GroupData = JsonSerializer.Serialize(organizationUsersList\n                    .SelectMany(x => x.Groups, (user, group) => new GroupUser\n                    {\n                        GroupId = group,\n                        OrganizationUserId = user.OrganizationUser.Id\n                    }))\n            },\n            commandType: CommandType.StoredProcedure);\n    }\n\n    public async Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm)\n    {\n        await using var connection = new SqlConnection(_marsConnectionString);\n\n        var rowCount = await connection.ExecuteScalarAsync<int>(\n            $\"[{Schema}].[OrganizationUser_ConfirmById]\",\n            new\n            {\n                Id = organizationUserToConfirm.OrganizationUserId,\n                UserId = organizationUserToConfirm.UserId,\n                RevisionDate = DateTime.UtcNow.Date,\n                Key = organizationUserToConfirm.Key\n            });\n\n        return rowCount > 0;\n    }\n\n    public async Task<OrganizationUserUserDetails?> GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.QuerySingleOrDefaultAsync<OrganizationUserUserDetails>(\n                \"[dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId]\",\n                new\n                {\n                    OrganizationId = organizationId,\n                    UserId = userId\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n\n    public Func<DbConnection, DbTransaction, Task> BuildConfirmOwnerAction(OrganizationUser organizationUser)\n    {\n        return async (DbConnection connection, DbTransaction transaction) =>\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[OrganizationUser_Update]\",\n                organizationUser,\n                commandType: CommandType.StoredProcedure,\n                transaction: transaction);\n        };\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.AdminConsole.Repositories;\n\npublic class PolicyRepository : Repository<Policy, Guid>, IPolicyRepository\n{\n    public PolicyRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public PolicyRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<Policy?> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Policy>(\n                $\"[{Schema}].[{Table}_ReadByOrganizationIdType]\",\n                new { OrganizationId = organizationId, Type = (byte)type },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<ICollection<Policy>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Policy>(\n                $\"[{Schema}].[{Table}_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<Policy>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Policy>(\n                $\"[{Schema}].[{Table}_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByUserIdsAndPolicyType(IEnumerable<Guid> userIds, PolicyType type)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n        var results = await connection.QueryAsync<OrganizationPolicyDetails>(\n            $\"[{Schema}].[PolicyDetails_ReadByUserIdsPolicyType]\",\n            new\n            {\n                UserIds = userIds.ToGuidIdArrayTVP(),\n                PolicyType = (byte)type\n            },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToList();\n    }\n\n    public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationPolicyDetails>(\n                $\"[{Schema}].[PolicyDetails_ReadByOrganizationId]\",\n                new { @OrganizationId = organizationId, @PolicyType = policyType },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<IEnumerable<PolicyDetails>> GetPolicyDetailsByUserIdAndPolicyTypeAsync(Guid userId, PolicyType policyType)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n        var results = await connection.QueryAsync<PolicyDetails>(\n            $\"[{Schema}].[PolicyDetails_ReadByUserIdPolicyType]\",\n            new\n            {\n                UserId = userId,\n                PolicyType = (byte)policyType\n            },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToList();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/AdminConsole/Repositories/ProviderOrganizationRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.AdminConsole.Repositories;\n\npublic class ProviderOrganizationRepository : Repository<ProviderOrganization, Guid>, IProviderOrganizationRepository\n{\n    public ProviderOrganizationRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public ProviderOrganizationRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<ICollection<ProviderOrganization>?> CreateManyAsync(IEnumerable<ProviderOrganization> providerOrganizations)\n    {\n        var entities = providerOrganizations.ToList();\n\n        if (!entities.Any())\n        {\n            return default;\n        }\n\n        foreach (var providerOrganization in entities)\n        {\n            providerOrganization.SetNewId();\n        }\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            connection.Open();\n\n            using (var transaction = connection.BeginTransaction())\n            {\n                try\n                {\n                    using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))\n                    {\n                        bulkCopy.DestinationTableName = \"[dbo].[ProviderOrganization]\";\n                        var dataTable = BuildProviderOrganizationsTable(bulkCopy, entities);\n                        await bulkCopy.WriteToServerAsync(dataTable);\n                    }\n\n                    transaction.Commit();\n\n                    return entities.ToList();\n                }\n                catch\n                {\n                    transaction.Rollback();\n                    throw;\n                }\n            }\n        }\n    }\n\n    public async Task<ICollection<ProviderOrganizationOrganizationDetails>> GetManyDetailsByProviderAsync(Guid providerId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderOrganizationOrganizationDetails>(\n                \"[dbo].[ProviderOrganizationOrganizationDetails_ReadByProviderId]\",\n                new { ProviderId = providerId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ProviderOrganization?> GetByOrganizationId(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderOrganization>(\n                \"[dbo].[ProviderOrganization_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<IEnumerable<ProviderOrganizationProviderDetails>> GetManyByUserAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderOrganizationProviderDetails>(\n                \"[dbo].[ProviderOrganizationProviderDetails_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<int> GetCountByOrganizationIdsAsync(\n        IEnumerable<Guid> organizationIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<int>(\n                $\"[{Schema}].[ProviderOrganization_ReadCountByOrganizationIds]\",\n                new { Ids = organizationIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results;\n        }\n    }\n\n    private DataTable BuildProviderOrganizationsTable(SqlBulkCopy bulkCopy, IEnumerable<ProviderOrganization> providerOrganizations)\n    {\n        var po = providerOrganizations.FirstOrDefault();\n        if (po == null)\n        {\n            throw new ApplicationException(\"Must have some ProviderOrganizations to bulk import.\");\n        }\n\n        var providerOrganizationsTable = new DataTable(\"ProviderOrganizationDataTable\");\n\n        var idColumn = new DataColumn(nameof(po.Id), typeof(Guid));\n        providerOrganizationsTable.Columns.Add(idColumn);\n        var providerIdColumn = new DataColumn(nameof(po.ProviderId), typeof(Guid));\n        providerOrganizationsTable.Columns.Add(providerIdColumn);\n        var organizationIdColumn = new DataColumn(nameof(po.OrganizationId), typeof(Guid));\n        providerOrganizationsTable.Columns.Add(organizationIdColumn);\n        var keyColumn = new DataColumn(nameof(po.Key), typeof(string));\n        providerOrganizationsTable.Columns.Add(keyColumn);\n        var settingsColumn = new DataColumn(nameof(po.Settings), typeof(string));\n        providerOrganizationsTable.Columns.Add(settingsColumn);\n        var creationDateColumn = new DataColumn(nameof(po.CreationDate), po.CreationDate.GetType());\n        providerOrganizationsTable.Columns.Add(creationDateColumn);\n        var revisionDateColumn = new DataColumn(nameof(po.RevisionDate), po.RevisionDate.GetType());\n        providerOrganizationsTable.Columns.Add(revisionDateColumn);\n\n        foreach (DataColumn col in providerOrganizationsTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[1];\n        keys[0] = idColumn;\n        providerOrganizationsTable.PrimaryKey = keys;\n\n        foreach (var providerOrganization in providerOrganizations)\n        {\n            var row = providerOrganizationsTable.NewRow();\n\n            row[idColumn] = providerOrganization.Id;\n            row[providerIdColumn] = providerOrganization.ProviderId;\n            row[organizationIdColumn] = providerOrganization.OrganizationId;\n            row[keyColumn] = providerOrganization.Key;\n            row[settingsColumn] = providerOrganization.Settings;\n            row[creationDateColumn] = providerOrganization.CreationDate;\n            row[revisionDateColumn] = providerOrganization.RevisionDate;\n\n            providerOrganizationsTable.Rows.Add(row);\n        }\n\n        return providerOrganizationsTable;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/AdminConsole/Repositories/ProviderRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.AdminConsole.Repositories;\n\npublic class ProviderRepository : Repository<Provider, Guid>, IProviderRepository\n{\n    public ProviderRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public ProviderRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<Provider?> GetByGatewayCustomerIdAsync(string gatewayCustomerId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Provider>(\n                \"[dbo].[Provider_ReadByGatewayCustomerId]\",\n                new { GatewayCustomerId = gatewayCustomerId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<Provider?> GetByGatewaySubscriptionIdAsync(string gatewaySubscriptionId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Provider>(\n                \"[dbo].[Provider_ReadByGatewaySubscriptionId]\",\n                new { GatewaySubscriptionId = gatewaySubscriptionId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<Provider?> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Provider>(\n                \"[dbo].[Provider_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<ICollection<Provider>> SearchAsync(string name, string userEmail, int skip, int take)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<Provider>(\n                \"[dbo].[Provider_Search]\",\n                new { Name = name, UserEmail = userEmail, Skip = skip, Take = take },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 120);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<ProviderAbility>> GetManyAbilitiesAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderAbility>(\n                \"[dbo].[Provider_ReadAbilities]\",\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/AdminConsole/Repositories/ProviderUserRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.AdminConsole.Repositories;\n\npublic class ProviderUserRepository : Repository<ProviderUser, Guid>, IProviderUserRepository\n{\n    public ProviderUserRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public ProviderUserRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<int> GetCountByProviderAsync(Guid providerId, string email, bool onlyRegisteredUsers)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.ExecuteScalarAsync<int>(\n                \"[dbo].[ProviderUser_ReadCountByProviderIdEmail]\",\n                new { ProviderId = providerId, Email = email, OnlyUsers = onlyRegisteredUsers },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyAsync(IEnumerable<Guid> ids)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderUser>(\n                \"[dbo].[ProviderUser_ReadByIds]\",\n                new { Ids = ids.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyByUserAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderUser>(\n                \"[dbo].[ProviderUser_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        var results = await connection.QueryAsync<ProviderUser>(\n            \"[dbo].[ProviderUser_ReadManyByManyUserIds]\",\n            new { UserIds = userIds.ToGuidIdArrayTVP() },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToList();\n    }\n\n    public async Task<ProviderUser?> GetByProviderUserAsync(Guid providerId, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderUser>(\n                \"[dbo].[ProviderUser_ReadByProviderIdUserId]\",\n                new { ProviderId = providerId, UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyByProviderAsync(Guid providerId, ProviderUserType? type)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderUser>(\n                \"[dbo].[ProviderUser_ReadByProviderId]\",\n                new { ProviderId = providerId, Type = type },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderUserUserDetails>(\n                \"[dbo].[ProviderUserUserDetails_ReadByProviderId]\",\n                new { ProviderId = providerId, Status = status },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<ProviderUserProviderDetails>> GetManyDetailsByUserAsync(Guid userId,\n        ProviderUserStatusType? status = null)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderUserProviderDetails>(\n                \"[dbo].[ProviderUserProviderDetails_ReadByUserIdStatus]\",\n                new { UserId = userId, Status = status },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<IEnumerable<ProviderUserOrganizationDetails>> GetManyOrganizationDetailsByUserAsync(Guid userId,\n        ProviderUserStatusType? status = null)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderUserOrganizationDetails>(\n                \"[dbo].[ProviderUserProviderOrganizationDetails_ReadByUserIdStatus]\",\n                new { UserId = userId, Status = status },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> providerUserIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\"[dbo].[ProviderUser_DeleteByIds]\",\n                new { Ids = providerUserIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<IEnumerable<ProviderUserPublicKey>> GetManyPublicKeysByProviderUserAsync(\n        Guid providerId, IEnumerable<Guid> Ids)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderUserPublicKey>(\n                \"[dbo].[User_ReadPublicKeysByProviderUserIds]\",\n                new { ProviderId = providerId, ProviderUserIds = Ids.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<int> GetCountByOnlyOwnerAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<int>(\n                \"[dbo].[ProviderUser_ReadCountByOnlyOwner]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results;\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyByOrganizationAsync(Guid organizationId, ProviderUserStatusType? status = null)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<ProviderUser>(\n                \"[dbo].[ProviderUser_ReadByOrganizationIdStatus]\",\n                new { OrganizationId = organizationId, Status = status },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Auth/Helpers/EmergencyAccessHelpers.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.Auth.Entities;\n\nnamespace Bit.Infrastructure.Dapper.Auth.Helpers;\n\npublic static class EmergencyAccessHelpers\n{\n    public static DataTable ToDataTable(this IEnumerable<EmergencyAccess> emergencyAccesses)\n    {\n        var emergencyAccessTable = new DataTable();\n\n        var columnData = new List<(string name, Type type, Func<EmergencyAccess, object> getter)>\n        {\n            (nameof(EmergencyAccess.Id), typeof(Guid), c => c.Id),\n            (nameof(EmergencyAccess.GrantorId), typeof(Guid), c => c.GrantorId),\n            (nameof(EmergencyAccess.GranteeId), typeof(Guid), c => c.GranteeId),\n            (nameof(EmergencyAccess.Email), typeof(string), c => c.Email),\n            (nameof(EmergencyAccess.KeyEncrypted), typeof(string), c => c.KeyEncrypted),\n            (nameof(EmergencyAccess.WaitTimeDays), typeof(int), c => c.WaitTimeDays),\n            (nameof(EmergencyAccess.Type), typeof(short), c => c.Type),\n            (nameof(EmergencyAccess.Status), typeof(short), c => c.Status),\n            (nameof(EmergencyAccess.RecoveryInitiatedDate), typeof(DateTime), c => c.RecoveryInitiatedDate),\n            (nameof(EmergencyAccess.LastNotificationDate), typeof(DateTime), c => c.LastNotificationDate),\n            (nameof(EmergencyAccess.CreationDate), typeof(DateTime), c => c.CreationDate),\n            (nameof(EmergencyAccess.RevisionDate), typeof(DateTime), c => c.RevisionDate),\n        };\n\n        return emergencyAccesses.BuildTable(emergencyAccessTable, columnData);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Auth/Repositories/AuthRequestRepository.cs",
    "content": "﻿using System.Data;\nusing System.Text.Json;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Auth.Repositories;\n\npublic class AuthRequestRepository : Repository<AuthRequest, Guid>, IAuthRequestRepository\n{\n    private readonly GlobalSettings _globalSettings;\n    public AuthRequestRepository(GlobalSettings globalSettings)\n        : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public async Task<int> DeleteExpiredAsync(\n        TimeSpan userRequestExpiration, TimeSpan adminRequestExpiration, TimeSpan afterAdminApprovalExpiration)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            return await connection.ExecuteAsync(\n                $\"[{Schema}].[AuthRequest_DeleteIfExpired]\",\n                new\n                {\n                    UserExpirationSeconds = (int)userRequestExpiration.TotalSeconds,\n                    AdminExpirationSeconds = (int)adminRequestExpiration.TotalSeconds,\n                    AdminApprovalExpirationSeconds = (int)afterAdminApprovalExpiration.TotalSeconds,\n                },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<ICollection<AuthRequest>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<AuthRequest>(\n                $\"[{Schema}].[AuthRequest_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<IEnumerable<PendingAuthRequestDetails>> GetManyPendingAuthRequestByUserId(Guid userId)\n    {\n        var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;\n        using var connection = new SqlConnection(ConnectionString);\n        var results = await connection.QueryAsync<PendingAuthRequestDetails>(\n            $\"[{Schema}].[AuthRequest_ReadPendingByUserId]\",\n            new { UserId = userId, ExpirationMinutes = expirationMinutes },\n            commandType: CommandType.StoredProcedure);\n\n        return results;\n    }\n\n    public async Task<ICollection<OrganizationAdminAuthRequest>> GetManyPendingByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationAdminAuthRequest>(\n                $\"[{Schema}].[AuthRequest_ReadPendingByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable<Guid> ids)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationAdminAuthRequest>(\n                $\"[{Schema}].[AuthRequest_ReadAdminApprovalsByIds]\",\n                new { OrganizationId = organizationId, Ids = ids.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task UpdateManyAsync(IEnumerable<AuthRequest> authRequests)\n    {\n        if (!authRequests.Any())\n        {\n            return;\n        }\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[dbo].[AuthRequest_UpdateMany]\",\n                new { jsonData = JsonSerializer.Serialize(authRequests) },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Auth/Repositories/EmergencyAccessRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Auth.Helpers;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Auth.Repositories;\n\npublic class EmergencyAccessRepository : Repository<EmergencyAccess, Guid>, IEmergencyAccessRepository\n{\n    public EmergencyAccessRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public EmergencyAccessRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<int> GetCountByGrantorIdEmailAsync(Guid grantorId, string email, bool onlyRegisteredUsers)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<int>(\n                \"[dbo].[EmergencyAccess_ReadCountByGrantorIdEmail]\",\n                new { GrantorId = grantorId, Email = email, OnlyUsers = onlyRegisteredUsers },\n                commandType: CommandType.StoredProcedure);\n\n            return results;\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGrantorIdAsync(Guid grantorId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<EmergencyAccessDetails>(\n                \"[dbo].[EmergencyAccessDetails_ReadByGrantorId]\",\n                new { GrantorId = grantorId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGranteeIdAsync(Guid granteeId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<EmergencyAccessDetails>(\n                \"[dbo].[EmergencyAccessDetails_ReadByGranteeId]\",\n                new { GranteeId = granteeId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByUserIdsAsync(ICollection<Guid> userIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<EmergencyAccessDetails>(\n                \"[dbo].[EmergencyAccessDetails_ReadManyByUserIds]\",\n                new { UserIds = userIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<EmergencyAccessDetails?> GetDetailsByIdGrantorIdAsync(Guid id, Guid grantorId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<EmergencyAccessDetails>(\n                \"[dbo].[EmergencyAccessDetails_ReadByIdGrantorId]\",\n                new { Id = id, GrantorId = grantorId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<EmergencyAccessDetails?> GetDetailsByIdAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<EmergencyAccessDetails>(\n                \"[dbo].[EmergencyAccessDetails_ReadById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessNotify>> GetManyToNotifyAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<EmergencyAccessNotify>(\n                \"[dbo].[EmergencyAccess_ReadToNotify]\",\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessDetails>> GetExpiredRecoveriesAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<EmergencyAccessDetails>(\n                \"[dbo].[EmergencyAccessDetails_ReadExpiredRecoveries]\",\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(\n        Guid grantorId, IEnumerable<EmergencyAccess> emergencyAccessKeys)\n    {\n        return async (SqlConnection connection, SqlTransaction transaction) =>\n        {\n            // Create temp table\n            var sqlCreateTemp = @\"\n                            SELECT TOP 0 *\n                            INTO #TempEmergencyAccess\n                            FROM [dbo].[EmergencyAccess]\";\n\n            await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))\n            {\n                cmd.ExecuteNonQuery();\n            }\n\n            // Bulk copy data into temp table\n            using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))\n            {\n                bulkCopy.DestinationTableName = \"#TempEmergencyAccess\";\n                var emergencyAccessTable = emergencyAccessKeys.ToDataTable();\n                foreach (DataColumn col in emergencyAccessTable.Columns)\n                {\n                    bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n                }\n\n                emergencyAccessTable.PrimaryKey = new DataColumn[] { emergencyAccessTable.Columns[0] };\n                await bulkCopy.WriteToServerAsync(emergencyAccessTable);\n            }\n\n            // Update emergency access table from temp table\n            var sql = @\"\n                UPDATE\n                    [dbo].[EmergencyAccess]\n                SET\n                    [KeyEncrypted] = TE.[KeyEncrypted]\n                FROM\n                    [dbo].[EmergencyAccess] E\n                INNER JOIN\n                    #TempEmergencyAccess TE ON E.Id = TE.Id\n                WHERE\n                    E.[GrantorId] = @GrantorId\n\n                DROP TABLE #TempEmergencyAccess\";\n\n            await using (var cmd = new SqlCommand(sql, connection, transaction))\n            {\n                cmd.Parameters.Add(\"@GrantorId\", SqlDbType.UniqueIdentifier).Value = grantorId;\n                cmd.ExecuteNonQuery();\n            }\n        };\n    }\n\n    /// <inheritdoc />\n    public async Task DeleteManyAsync(ICollection<Guid> emergencyAccessIds)\n    {\n        using var connection = new SqlConnection(ConnectionString);\n        await connection.ExecuteAsync(\n                \"[dbo].[EmergencyAccess_DeleteManyById]\",\n                new { EmergencyAccessIds = emergencyAccessIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Auth/Repositories/GrantRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Auth.Repositories;\n\npublic class GrantRepository : BaseRepository, IGrantRepository\n{\n    public GrantRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public GrantRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<IGrant?> GetByKeyAsync(string key)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Grant>(\n                \"[dbo].[Grant_ReadByKey]\",\n                new { Key = key },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<ICollection<IGrant>> GetManyAsync(string subjectId, string sessionId,\n        string clientId, string type)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Grant>(\n                \"[dbo].[Grant_Read]\",\n                new { SubjectId = subjectId, SessionId = sessionId, ClientId = clientId, Type = type },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList<IGrant>();\n        }\n    }\n\n    public async Task SaveAsync(IGrant obj)\n    {\n        if (obj is not Grant gObj)\n        {\n            throw new ArgumentException(null, nameof(obj));\n        }\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                \"[dbo].[Grant_Save]\",\n                new\n                {\n                    obj.Key,\n                    obj.Type,\n                    obj.SubjectId,\n                    obj.SessionId,\n                    obj.ClientId,\n                    obj.Description,\n                    obj.CreationDate,\n                    obj.ExpirationDate,\n                    obj.ConsumedDate,\n                    obj.Data\n                },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task DeleteByKeyAsync(string key)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[Grant_DeleteByKey]\",\n                new { Key = key },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task DeleteManyAsync(string subjectId, string sessionId, string clientId, string type)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[Grant_Delete]\",\n                new { SubjectId = subjectId, SessionId = sessionId, ClientId = clientId, Type = type },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Auth/Repositories/SsoConfigRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Auth.Repositories;\n\npublic class SsoConfigRepository : Repository<SsoConfig, long>, ISsoConfigRepository\n{\n    public SsoConfigRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public SsoConfigRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<SsoConfig?> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<SsoConfig>(\n                $\"[{Schema}].[{Table}_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<SsoConfig?> GetByIdentifierAsync(string identifier)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<SsoConfig>(\n                $\"[{Schema}].[{Table}_ReadByIdentifier]\",\n                new { Identifier = identifier },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<ICollection<SsoConfig>> GetManyByRevisionNotBeforeDate(DateTime? notBefore)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<SsoConfig>(\n                $\"[{Schema}].[{Table}_ReadManyByNotBeforeRevisionDate]\",\n                new { NotBefore = notBefore },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Auth/Repositories/SsoUserRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Auth.Repositories;\n\npublic class SsoUserRepository : Repository<SsoUser, long>, ISsoUserRepository\n{\n    public SsoUserRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public SsoUserRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task DeleteAsync(Guid userId, Guid? organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[SsoUser_Delete]\",\n                new { UserId = userId, OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<SsoUser?> GetByUserIdOrganizationIdAsync(Guid organizationId, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<SsoUser>(\n                $\"[{Schema}].[SsoUser_ReadByUserIdOrganizationId]\",\n                new { UserId = userId, OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\n\nnamespace Bit.Infrastructure.Dapper.Auth.Repositories;\n\npublic class WebAuthnCredentialRepository : Repository<WebAuthnCredential, Guid>, IWebAuthnCredentialRepository\n{\n    public WebAuthnCredentialRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public WebAuthnCredentialRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<WebAuthnCredential?> GetByIdAsync(Guid id, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<WebAuthnCredential>(\n                $\"[{Schema}].[{Table}_ReadByIdUserId]\",\n                new { Id = id, UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<ICollection<WebAuthnCredential>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<WebAuthnCredential>(\n                $\"[{Schema}].[{Table}_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<bool> UpdateAsync(WebAuthnCredential credential)\n    {\n        using var connection = new SqlConnection(ConnectionString);\n        var affectedRows = await connection.ExecuteAsync(\n            $\"[{Schema}].[{Table}_Update]\",\n            credential,\n            commandType: CommandType.StoredProcedure);\n\n        return affectedRows > 0;\n    }\n\n    public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<WebAuthnLoginRotateKeyData> credentials)\n    {\n        return async (SqlConnection connection, SqlTransaction transaction) =>\n        {\n            const string sql = @\"\n                UPDATE WC\n                SET\n                    WC.[EncryptedPublicKey] = UW.[encryptedPublicKey],\n                    WC.[EncryptedUserKey] = UW.[encryptedUserKey]\n                FROM\n                    [dbo].[WebAuthnCredential] WC\n                INNER JOIN\n                    OPENJSON(@JsonCredentials)\n                    WITH (\n                        id UNIQUEIDENTIFIER,\n                        encryptedPublicKey NVARCHAR(MAX),\n                        encryptedUserKey NVARCHAR(MAX)\n                    ) UW\n                    ON UW.id = WC.Id\n                WHERE\n                    WC.[UserId] = @UserId\";\n\n            var jsonCredentials = CoreHelpers.ClassToJsonData(credentials);\n\n            await connection.ExecuteAsync(\n                sql,\n                new { UserId = userId, JsonCredentials = jsonCredentials },\n                transaction: transaction,\n                commandType: CommandType.Text);\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Billing.Repositories;\n\npublic class ClientOrganizationMigrationRecordRepository(\n    GlobalSettings globalSettings) : Repository<ClientOrganizationMigrationRecord, Guid>(\n        globalSettings.SqlServer.ConnectionString,\n        globalSettings.SqlServer.ReadOnlyConnectionString), IClientOrganizationMigrationRecordRepository\n{\n    public async Task<ClientOrganizationMigrationRecord> GetByOrganizationId(Guid organizationId)\n    {\n        var sqlConnection = new SqlConnection(ConnectionString);\n\n        var results = await sqlConnection.QueryAsync<ClientOrganizationMigrationRecord>(\n            \"[dbo].[ClientOrganizationMigrationRecord_ReadByOrganizationId]\",\n            new { OrganizationId = organizationId },\n            commandType: CommandType.StoredProcedure);\n\n        return results.FirstOrDefault();\n    }\n\n    public async Task<ICollection<ClientOrganizationMigrationRecord>> GetByProviderId(Guid providerId)\n    {\n        var sqlConnection = new SqlConnection(ConnectionString);\n\n        var results = await sqlConnection.QueryAsync<ClientOrganizationMigrationRecord>(\n            \"[dbo].[ClientOrganizationMigrationRecord_ReadByProviderId]\",\n            new { ProviderId = providerId },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToArray();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.Billing.Organizations.Entities;\nusing Bit.Core.Billing.Organizations.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Billing.Repositories;\n\npublic class OrganizationInstallationRepository(\n    GlobalSettings globalSettings) : Repository<OrganizationInstallation, Guid>(\n        globalSettings.SqlServer.ConnectionString,\n        globalSettings.SqlServer.ReadOnlyConnectionString), IOrganizationInstallationRepository\n{\n    public async Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId)\n    {\n        var sqlConnection = new SqlConnection(ConnectionString);\n\n        var results = await sqlConnection.QueryAsync<OrganizationInstallation>(\n            \"[dbo].[OrganizationInstallation_ReadByInstallationId]\",\n            new { InstallationId = installationId },\n            commandType: CommandType.StoredProcedure);\n\n        return results.FirstOrDefault();\n    }\n\n    public async Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        var sqlConnection = new SqlConnection(ConnectionString);\n\n        var results = await sqlConnection.QueryAsync<OrganizationInstallation>(\n            \"[dbo].[OrganizationInstallation_ReadByOrganizationId]\",\n            new { OrganizationId = organizationId },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToArray();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Billing/Repositories/ProviderInvoiceItemRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Billing.Repositories;\n\npublic class ProviderInvoiceItemRepository(\n    GlobalSettings globalSettings)\n    : Repository<ProviderInvoiceItem, Guid>(\n        globalSettings.SqlServer.ConnectionString,\n        globalSettings.SqlServer.ReadOnlyConnectionString), IProviderInvoiceItemRepository\n{\n    public async Task<ICollection<ProviderInvoiceItem>> GetByInvoiceId(string invoiceId)\n    {\n        var sqlConnection = new SqlConnection(ConnectionString);\n\n        var results = await sqlConnection.QueryAsync<ProviderInvoiceItem>(\n            \"[dbo].[ProviderInvoiceItem_ReadByInvoiceId]\",\n            new { InvoiceId = invoiceId },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToArray();\n    }\n\n    public async Task<ICollection<ProviderInvoiceItem>> GetByProviderId(Guid providerId)\n    {\n        var sqlConnection = new SqlConnection(ConnectionString);\n\n        var results = await sqlConnection.QueryAsync<ProviderInvoiceItem>(\n            \"[dbo].[ProviderInvoiceItem_ReadByProviderId]\",\n            new { ProviderId = providerId },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToArray();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Billing/Repositories/ProviderPlanRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Billing.Repositories;\n\npublic class ProviderPlanRepository(\n    GlobalSettings globalSettings)\n    : Repository<ProviderPlan, Guid>(\n        globalSettings.SqlServer.ConnectionString,\n        globalSettings.SqlServer.ReadOnlyConnectionString), IProviderPlanRepository\n{\n    public async Task<ICollection<ProviderPlan>> GetByProviderId(Guid providerId)\n    {\n        var sqlConnection = new SqlConnection(ConnectionString);\n\n        var results = await sqlConnection.QueryAsync<ProviderPlan>(\n            \"[dbo].[ProviderPlan_ReadByProviderId]\",\n            new { ProviderId = providerId },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToArray();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Billing/Repositories/SubscriptionDiscountRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Billing.Subscriptions.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Billing.Repositories;\n\npublic class SubscriptionDiscountRepository(\n    GlobalSettings globalSettings)\n    : Repository<SubscriptionDiscount, Guid>(\n        globalSettings.SqlServer.ConnectionString,\n        globalSettings.SqlServer.ReadOnlyConnectionString), ISubscriptionDiscountRepository\n{\n    public async Task<ICollection<SubscriptionDiscount>> GetActiveDiscountsAsync()\n    {\n        using var sqlConnection = new SqlConnection(ReadOnlyConnectionString);\n\n        var results = await sqlConnection.QueryAsync<SubscriptionDiscount>(\n            \"[dbo].[SubscriptionDiscount_ReadActive]\",\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToArray();\n    }\n\n    public async Task<SubscriptionDiscount?> GetByStripeCouponIdAsync(string stripeCouponId)\n    {\n        using var sqlConnection = new SqlConnection(ReadOnlyConnectionString);\n\n        var result = await sqlConnection.QueryFirstOrDefaultAsync<SubscriptionDiscount>(\n            \"[dbo].[SubscriptionDiscount_ReadByStripeCouponId]\",\n            new { StripeCouponId = stripeCouponId },\n            commandType: CommandType.StoredProcedure);\n\n        return result;\n    }\n\n    public async Task<ICollection<SubscriptionDiscount>> ListAsync(int skip, int take)\n    {\n        using var sqlConnection = new SqlConnection(ReadOnlyConnectionString);\n\n        var results = await sqlConnection.QueryAsync<SubscriptionDiscount>(\n            \"[dbo].[SubscriptionDiscount_List]\",\n            new { Skip = skip, Take = take },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToArray();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/DapperHelpers.cs",
    "content": "﻿using System.Collections.Frozen;\nusing System.Data;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Linq.Expressions;\nusing System.Reflection;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data;\nusing Dapper;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper;\n\n/// <summary>\n/// Provides a way to build a <see cref=\"DataTable\"/> based on the properties of <see cref=\"T\"/>.\n/// </summary>\n/// <typeparam name=\"T\"></typeparam>\npublic class DataTableBuilder<T>\n{\n    private readonly FrozenDictionary<string, (Type Type, Func<T, object?> Getter)> _columnBuilders;\n\n    /// <summary>\n    /// Creates a new instance of <see cref=\"DataTableBuilder{T}\"/>.\n    /// </summary>\n    /// <example>\n    /// <code>\n    /// new DataTableBuilder<MyObject>(\n    ///     [\n    ///         i => i.Id,\n    ///         i => i.Name,\n    ///     ]\n    /// );\n    /// </code>\n    /// </example>\n    /// <param name=\"columnExpressions\"></param>\n    /// <exception cref=\"ArgumentException\"></exception>\n    public DataTableBuilder(Expression<Func<T, object?>>[] columnExpressions)\n    {\n        ArgumentNullException.ThrowIfNull(columnExpressions);\n        ArgumentOutOfRangeException.ThrowIfZero(columnExpressions.Length);\n\n        var columnBuilders = new Dictionary<string, (Type Type, Func<T, object?>)>(columnExpressions.Length);\n\n        for (var i = 0; i < columnExpressions.Length; i++)\n        {\n            var columnExpression = columnExpressions[i];\n\n            if (!TryGetPropertyInfo(columnExpression, out var propertyInfo))\n            {\n                throw new ArgumentException($\"Could not determine the property info from the given expression '{columnExpression}'.\");\n            }\n\n            // Unwrap possible Nullable<T>\n            var type = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;\n\n            // This needs to be after unwrapping the `Nullable` since enums can be nullable\n            if (type.IsEnum)\n            {\n                // Get the backing type of the enum\n                type = Enum.GetUnderlyingType(type);\n            }\n\n            if (!columnBuilders.TryAdd(propertyInfo.Name, (type, columnExpression.Compile())))\n            {\n                throw new ArgumentException($\"Property with name '{propertyInfo.Name}' was already added, properties can only be added once.\");\n            }\n        }\n\n        _columnBuilders = columnBuilders.ToFrozenDictionary();\n    }\n\n    private static bool TryGetPropertyInfo(Expression<Func<T, object?>> columnExpression, [MaybeNullWhen(false)] out PropertyInfo property)\n    {\n        property = null;\n\n        // Reference type properties\n        // i => i.Data\n        if (columnExpression.Body is MemberExpression { Member: PropertyInfo referencePropertyInfo })\n        {\n            property = referencePropertyInfo;\n            return true;\n        }\n\n        // Value type properties will implicitly box into the object so\n        // we need to look past the Convert expression\n        // i => (System.Object?)i.Id\n        if (\n            columnExpression.Body is UnaryExpression\n            {\n                NodeType: ExpressionType.Convert,\n                Operand: MemberExpression { Member: PropertyInfo valuePropertyInfo },\n            }\n        )\n        {\n            // This could be an implicit cast from the property into our return type object?\n            property = valuePropertyInfo;\n            return true;\n        }\n\n        // Other possible expression bodies here\n        return false;\n    }\n\n    public DataTable Build(IEnumerable<T> source)\n    {\n        ArgumentNullException.ThrowIfNull(source);\n\n        var table = new DataTable();\n\n        foreach (var (name, (type, _)) in _columnBuilders)\n        {\n            table.Columns.Add(new DataColumn(name, type));\n        }\n\n        foreach (var entity in source)\n        {\n            var row = table.NewRow();\n\n            foreach (var (name, (_, getter)) in _columnBuilders)\n            {\n                var value = getter(entity);\n                if (value is null)\n                {\n                    row[name] = DBNull.Value;\n                }\n                else\n                {\n                    row[name] = value;\n                }\n            }\n\n            table.Rows.Add(row);\n        }\n\n        return table;\n    }\n}\n\npublic static class DapperHelpers\n{\n    private static readonly DataTableBuilder<OrganizationSponsorship> _organizationSponsorshipTableBuilder = new(\n        [\n            os => os.Id,\n            os => os.SponsoringOrganizationId,\n            os => os.SponsoringOrganizationUserId,\n            os => os.SponsoredOrganizationId,\n            os => os.FriendlyName,\n            os => os.OfferedToEmail,\n            os => os.PlanSponsorshipType,\n            os => os.LastSyncDate,\n            os => os.ValidUntil,\n            os => os.ToDelete,\n            os => os.IsAdminInitiated,\n            os => os.Notes,\n        ]\n    );\n\n    public static DataTable ToGuidIdArrayTVP(this IEnumerable<Guid> ids)\n    {\n        return ids.ToArrayTVP(\"GuidId\");\n    }\n\n    public static DataTable ToTwoGuidIdArrayTVP(this IEnumerable<(Guid id1, Guid id2)> values)\n    {\n        var table = new DataTable();\n        table.SetTypeName(\"[dbo].[TwoGuidIdArray]\");\n        table.Columns.Add(\"Id1\", typeof(Guid));\n        table.Columns.Add(\"Id2\", typeof(Guid));\n\n        foreach (var value in values)\n        {\n            table.Rows.Add(value.id1, value.id2);\n        }\n\n        return table;\n    }\n\n    public static DataTable ToArrayTVP<T>(this IEnumerable<T> values, string columnName)\n    {\n        var table = new DataTable();\n        table.SetTypeName($\"[dbo].[{columnName}Array]\");\n        table.Columns.Add(columnName, typeof(T));\n\n        if (values != null)\n        {\n            foreach (var value in values)\n            {\n                table.Rows.Add(value);\n            }\n        }\n\n        return table;\n    }\n\n    public static DataTable ToArrayTVP(this IEnumerable<CollectionAccessSelection> values)\n    {\n        var table = new DataTable();\n        table.SetTypeName(\"[dbo].[CollectionAccessSelectionType]\");\n\n        var idColumn = new DataColumn(\"Id\", typeof(Guid));\n        table.Columns.Add(idColumn);\n        var readOnlyColumn = new DataColumn(\"ReadOnly\", typeof(bool));\n        table.Columns.Add(readOnlyColumn);\n        var hidePasswordsColumn = new DataColumn(\"HidePasswords\", typeof(bool));\n        table.Columns.Add(hidePasswordsColumn);\n        var manageColumn = new DataColumn(\"Manage\", typeof(bool));\n        table.Columns.Add(manageColumn);\n\n        if (values != null)\n        {\n            foreach (var value in values)\n            {\n                var row = table.NewRow();\n                row[idColumn] = value.Id;\n                row[readOnlyColumn] = value.ReadOnly;\n                row[hidePasswordsColumn] = value.HidePasswords;\n                row[manageColumn] = value.Manage;\n                table.Rows.Add(row);\n            }\n        }\n\n        return table;\n    }\n\n    public static DataTable ToTvp(this IEnumerable<OrganizationSponsorship> organizationSponsorships)\n    {\n        var table = _organizationSponsorshipTableBuilder.Build(organizationSponsorships ?? []);\n        table.SetTypeName(\"[dbo].[OrganizationSponsorshipType]\");\n        return table;\n    }\n\n    public static DataTable BuildTable<T>(this IEnumerable<T> entities, DataTable table,\n        List<(string name, Type type, Func<T, object?> getter)> columnData)\n    {\n        foreach (var (name, type, getter) in columnData)\n        {\n            var column = new DataColumn(name, type);\n            table.Columns.Add(column);\n        }\n\n        foreach (var entity in entities ?? new T[] { })\n        {\n            var row = table.NewRow();\n            foreach (var (name, type, getter) in columnData)\n            {\n                var val = getter(entity);\n                if (val == null)\n                {\n                    row[name] = DBNull.Value;\n                }\n                else\n                {\n                    row[name] = val;\n                }\n            }\n            table.Rows.Add(row);\n        }\n\n        return table;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Billing.Organizations.Repositories;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Billing.Subscriptions.Repositories;\nusing Bit.Core.Dirt.Reports.Repositories;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Installations;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Infrastructure.Dapper.AdminConsole.Repositories;\nusing Bit.Infrastructure.Dapper.Auth.Repositories;\nusing Bit.Infrastructure.Dapper.Billing.Repositories;\nusing Bit.Infrastructure.Dapper.Dirt;\nusing Bit.Infrastructure.Dapper.Dirt.Repositories;\nusing Bit.Infrastructure.Dapper.KeyManagement.Repositories;\nusing Bit.Infrastructure.Dapper.NotificationCenter.Repositories;\nusing Bit.Infrastructure.Dapper.Platform;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Bit.Infrastructure.Dapper.SecretsManager.Repositories;\nusing Bit.Infrastructure.Dapper.Tools.Repositories;\nusing Bit.Infrastructure.Dapper.Vault.Repositories;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.Dapper;\n\npublic static class DapperServiceCollectionExtensions\n{\n    public static void AddDapperRepositories(this IServiceCollection services, bool selfHosted)\n    {\n        services.AddSingleton<IApiKeyRepository, ApiKeyRepository>();\n        services.AddSingleton<IAuthRequestRepository, AuthRequestRepository>();\n        services.AddSingleton<ICipherRepository, CipherRepository>();\n        services.AddSingleton<ICollectionCipherRepository, CollectionCipherRepository>();\n        services.AddSingleton<ICollectionRepository, CollectionRepository>();\n        services.AddSingleton<IDeviceRepository, DeviceRepository>();\n        services.AddSingleton<IEmergencyAccessRepository, EmergencyAccessRepository>();\n        services.AddSingleton<IEmergencyAccessRepository, EmergencyAccessRepository>();\n        services.AddSingleton<IFolderRepository, FolderRepository>();\n        services.AddSingleton<IGrantRepository, GrantRepository>();\n        services.AddSingleton<IGroupRepository, GroupRepository>();\n        services.AddSingleton<IInstallationRepository, InstallationRepository>();\n        services.AddSingleton<IMaintenanceRepository, MaintenanceRepository>();\n        services.AddSingleton<IOrganizationApiKeyRepository, OrganizationApiKeyRepository>();\n        services.AddSingleton<IOrganizationConnectionRepository, OrganizationConnectionRepository>();\n        services.AddSingleton<IOrganizationIntegrationConfigurationRepository, OrganizationIntegrationConfigurationRepository>();\n        services.AddSingleton<IOrganizationIntegrationRepository, OrganizationIntegrationRepository>();\n        services.AddSingleton<IOrganizationRepository, OrganizationRepository>();\n        services.AddSingleton<IOrganizationSponsorshipRepository, OrganizationSponsorshipRepository>();\n        services.AddSingleton<IOrganizationUserRepository, OrganizationUserRepository>();\n        services.AddSingleton<IPlayItemRepository, PlayItemRepository>();\n        services.AddSingleton<IPolicyRepository, PolicyRepository>();\n        services.AddSingleton<IProviderOrganizationRepository, ProviderOrganizationRepository>();\n        services.AddSingleton<IProviderRepository, ProviderRepository>();\n        services.AddSingleton<IProviderUserRepository, ProviderUserRepository>();\n        services.AddSingleton<ISendRepository, SendRepository>();\n        services.AddSingleton<ISsoConfigRepository, SsoConfigRepository>();\n        services.AddSingleton<ISsoUserRepository, SsoUserRepository>();\n        services.AddSingleton<ITransactionRepository, TransactionRepository>();\n        services.AddSingleton<IUserRepository, UserRepository>();\n        services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();\n        services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();\n        services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();\n        services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>();\n        services.AddSingleton<ISubscriptionDiscountRepository, SubscriptionDiscountRepository>();\n        services.AddSingleton<INotificationRepository, NotificationRepository>();\n        services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();\n        services\n            .AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();\n        services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();\n        services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();\n        services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();\n        services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();\n        services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();\n        services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();\n        services.AddSingleton<IOrganizationApplicationRepository, OrganizationApplicationRepository>();\n        services.AddSingleton<IOrganizationMemberBaseDetailRepository, OrganizationMemberBaseDetailRepository>();\n\n        if (selfHosted)\n        {\n            services.AddSingleton<IEventRepository, EventRepository>();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Dirt/OrganizationApplicationRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Dirt;\n\npublic class OrganizationApplicationRepository : Repository<OrganizationApplication, Guid>, IOrganizationApplicationRepository\n{\n    public OrganizationApplicationRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n    }\n\n    public OrganizationApplicationRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    {\n    }\n\n    public async Task<ICollection<OrganizationApplication>> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationApplication>(\n                $\"[{Schema}].[OrganizationApplication_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Dirt/OrganizationMemberBaseDetailRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Reports.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Dirt;\n\npublic class OrganizationMemberBaseDetailRepository : BaseRepository, IOrganizationMemberBaseDetailRepository\n{\n    public OrganizationMemberBaseDetailRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n    }\n\n    public OrganizationMemberBaseDetailRepository(string connectionString, string readOnlyConnectionString) : base(\n        connectionString, readOnlyConnectionString)\n    {\n    }\n\n    public async Task<IEnumerable<OrganizationMemberBaseDetail>> GetOrganizationMemberBaseDetailsByOrganizationId(\n        Guid organizationId)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n\n        var result = await connection.QueryAsync<OrganizationMemberBaseDetail>(\n            \"[dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId]\",\n            new\n            {\n                OrganizationId = organizationId\n\n            }, commandType: CommandType.StoredProcedure);\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Dirt;\n\npublic class OrganizationReportRepository : Repository<OrganizationReport, Guid>, IOrganizationReportRepository\n{\n    public OrganizationReportRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n    }\n\n    public OrganizationReportRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    {\n    }\n\n    public async Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var result = await connection.QuerySingleOrDefaultAsync<OrganizationReport>(\n                $\"[{Schema}].[OrganizationReport_GetLatestByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n\n    public async Task<OrganizationReport> UpdateSummaryDataAsync(Guid organizationId, Guid reportId, string summaryData)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var parameters = new\n            {\n                Id = reportId,\n                OrganizationId = organizationId,\n                SummaryData = summaryData,\n                RevisionDate = DateTime.UtcNow\n            };\n\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[OrganizationReport_UpdateSummaryData]\",\n                parameters,\n                commandType: CommandType.StoredProcedure);\n\n            // Return the updated report\n            return await connection.QuerySingleOrDefaultAsync<OrganizationReport>(\n                $\"[{Schema}].[OrganizationReport_ReadById]\",\n                new { Id = reportId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<OrganizationReportSummaryDataResponse> GetSummaryDataAsync(Guid reportId)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var result = await connection.QuerySingleOrDefaultAsync<OrganizationReportSummaryDataResponse>(\n                $\"[{Schema}].[OrganizationReport_GetSummaryDataById]\",\n                new { Id = reportId },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetSummaryDataByDateRangeAsync(\n        Guid organizationId,\n        DateTime startDate, DateTime\n            endDate)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var parameters = new\n            {\n                OrganizationId = organizationId,\n                StartDate = startDate.ToUniversalTime(),\n                EndDate = endDate.ToUniversalTime()\n            };\n\n            var results = await connection.QueryAsync<OrganizationReportSummaryDataResponse>(\n                $\"[{Schema}].[OrganizationReport_ReadByOrganizationIdAndRevisionDate]\",\n                parameters,\n                commandType: CommandType.StoredProcedure);\n\n            return results;\n        }\n    }\n\n    public async Task<OrganizationReportDataResponse> GetReportDataAsync(Guid reportId)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var result = await connection.QuerySingleOrDefaultAsync<OrganizationReportDataResponse>(\n                $\"[{Schema}].[OrganizationReport_GetReportDataById]\",\n                new { Id = reportId },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n\n    public async Task<OrganizationReport> UpdateReportDataAsync(Guid organizationId, Guid reportId, string reportData)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var parameters = new\n            {\n                OrganizationId = organizationId,\n                Id = reportId,\n                ReportData = reportData,\n                RevisionDate = DateTime.UtcNow\n            };\n\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[OrganizationReport_UpdateReportData]\",\n                parameters,\n                commandType: CommandType.StoredProcedure);\n\n            // Return the updated report\n            return await connection.QuerySingleOrDefaultAsync<OrganizationReport>(\n                $\"[{Schema}].[OrganizationReport_ReadById]\",\n                new { Id = reportId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<OrganizationReportApplicationDataResponse> GetApplicationDataAsync(Guid reportId)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var result = await connection.QuerySingleOrDefaultAsync<OrganizationReportApplicationDataResponse>(\n                $\"[{Schema}].[OrganizationReport_GetApplicationDataById]\",\n                new { Id = reportId },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n\n    public async Task<OrganizationReport> UpdateApplicationDataAsync(Guid organizationId, Guid reportId, string applicationData)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var parameters = new\n            {\n                OrganizationId = organizationId,\n                Id = reportId,\n                ApplicationData = applicationData,\n                RevisionDate = DateTime.UtcNow\n            };\n\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[OrganizationReport_UpdateApplicationData]\",\n                parameters,\n                commandType: CommandType.StoredProcedure);\n\n            // Return the updated report\n            return await connection.QuerySingleOrDefaultAsync<OrganizationReport>(\n                $\"[{Schema}].[OrganizationReport_ReadById]\",\n                new { Id = reportId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task UpdateMetricsAsync(Guid reportId, OrganizationReportMetricsData metrics)\n    {\n        using var connection = new SqlConnection(ConnectionString);\n        var parameters = new\n        {\n            Id = reportId,\n            ApplicationCount = metrics.ApplicationCount,\n            ApplicationAtRiskCount = metrics.ApplicationAtRiskCount,\n            CriticalApplicationCount = metrics.CriticalApplicationCount,\n            CriticalApplicationAtRiskCount = metrics.CriticalApplicationAtRiskCount,\n            MemberCount = metrics.MemberCount,\n            MemberAtRiskCount = metrics.MemberAtRiskCount,\n            CriticalMemberCount = metrics.CriticalMemberCount,\n            CriticalMemberAtRiskCount = metrics.CriticalMemberAtRiskCount,\n            PasswordCount = metrics.PasswordCount,\n            PasswordAtRiskCount = metrics.PasswordAtRiskCount,\n            CriticalPasswordCount = metrics.CriticalPasswordCount,\n            CriticalPasswordAtRiskCount = metrics.CriticalPasswordAtRiskCount,\n            RevisionDate = DateTime.UtcNow\n        };\n\n        await connection.ExecuteAsync(\n            $\"[{Schema}].[OrganizationReport_UpdateMetrics]\",\n            parameters,\n            commandType: CommandType.StoredProcedure);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Dirt/PasswordHealthReportApplicationRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Dirt;\n\npublic class PasswordHealthReportApplicationRepository : Repository<PasswordHealthReportApplication, Guid>, IPasswordHealthReportApplicationRepository\n{\n    public PasswordHealthReportApplicationRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public PasswordHealthReportApplicationRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<ICollection<PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<PasswordHealthReportApplication>(\n                $\"[{Schema}].[PasswordHealthReportApplication_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Dirt/Repositories/EventRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class EventRepository : Repository<Event, Guid>, IEventRepository\n{\n    public EventRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public EventRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<PagedResult<IEvent>> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"[{Schema}].[Event_ReadPageByUserId]\",\n            new Dictionary<string, object?>\n            {\n                [\"@UserId\"] = userId\n            }, startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByOrganizationAsync(Guid organizationId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"[{Schema}].[Event_ReadPageByOrganizationId]\",\n            new Dictionary<string, object?>\n            {\n                [\"@OrganizationId\"] = organizationId\n            }, startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"[{Schema}].[Event_ReadPageBySecretId]\",\n                  new Dictionary<string, object?>\n                  {\n                      [\"@SecretId\"] = secret.Id\n                  }, startDate, endDate, pageOptions);\n\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project,\n      DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"[{Schema}].[Event_ReadPageByProjectId]\",\n                  new Dictionary<string, object?>\n                  {\n                      [\"@ProjectId\"] = project.Id\n                  }, startDate, endDate, pageOptions);\n\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,\n    DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"[{Schema}].[Event_ReadPageByOrganizationIdActingUserId]\",\n            new Dictionary<string, object?>\n            {\n                [\"@OrganizationId\"] = organizationId,\n                [\"@ActingUserId\"] = actingUserId\n            }, startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByProviderAsync(Guid providerId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"[{Schema}].[Event_ReadPageByProviderId]\",\n            new Dictionary<string, object?>\n            {\n                [\"@ProviderId\"] = providerId\n            }, startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByProviderActingUserAsync(Guid providerId, Guid actingUserId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"[{Schema}].[Event_ReadPageByProviderIdActingUserId]\",\n            new Dictionary<string, object?>\n            {\n                [\"@ProviderId\"] = providerId,\n                [\"@ActingUserId\"] = actingUserId\n            }, startDate, endDate, pageOptions);\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate,\n        PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"[{Schema}].[Event_ReadPageByCipherId]\",\n            new Dictionary<string, object?>\n            {\n                [\"@OrganizationId\"] = cipher.OrganizationId,\n                [\"@UserId\"] = cipher.UserId,\n                [\"@CipherId\"] = cipher.Id\n            }, startDate, endDate, pageOptions);\n    }\n\n    public async Task CreateAsync(IEvent e)\n    {\n        if (!(e is Event ev))\n        {\n            ev = new Event(e);\n        }\n\n        await base.CreateAsync(ev);\n    }\n\n    public async Task CreateManyAsync(IEnumerable<IEvent>? entities)\n    {\n        if (entities is null || !entities.Any())\n        {\n            return;\n        }\n\n        if (!entities.Skip(1).Any())\n        {\n            await CreateAsync(entities.First());\n            return;\n        }\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            connection.Open();\n            using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, null))\n            {\n                bulkCopy.DestinationTableName = \"[dbo].[Event]\";\n                var dataTable = BuildEventsTable(bulkCopy, entities.Select(e => e is Event @event ? @event : new Event(e)));\n                await bulkCopy.WriteToServerAsync(dataTable);\n            }\n        }\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId, Guid serviceAccountId,\n        DateTime startDate, DateTime endDate,\n        PageOptions pageOptions)\n    {\n        return await GetManyAsync($\"[{Schema}].[Event_ReadPageByOrganizationIdServiceAccountId]\",\n            new Dictionary<string, object?>\n            {\n                [\"@OrganizationId\"] = organizationId,\n                [\"@ServiceAccountId\"] = serviceAccountId\n            }, startDate, endDate, pageOptions);\n    }\n\n    private async Task<PagedResult<IEvent>> GetManyAsync(string sprocName,\n        IDictionary<string, object?> sprocParams, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n\n        var parameters = new DynamicParameters(sprocParams);\n        parameters.Add(\"@PageSize\", pageOptions.PageSize, DbType.Int32);\n        // Explicitly use DbType.DateTime2 for proper precision.\n        // ref: https://github.com/StackExchange/Dapper/issues/229\n        parameters.Add(\"@StartDate\", startDate.ToUniversalTime(), DbType.DateTime2, null, 7);\n        parameters.Add(\"@EndDate\", endDate.ToUniversalTime(), DbType.DateTime2, null, 7);\n        parameters.Add(\"@BeforeDate\", beforeDate, DbType.DateTime2, null, 7);\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var events = (await connection.QueryAsync<Event>(sprocName, parameters,\n                commandType: CommandType.StoredProcedure)).ToList();\n\n            var result = new PagedResult<IEvent>();\n            if (events.Any() && events.Count >= pageOptions.PageSize)\n            {\n                result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n            }\n            result.Data.AddRange(events);\n            return result;\n        }\n    }\n\n    private DataTable BuildEventsTable(SqlBulkCopy bulkCopy, IEnumerable<Event> events)\n    {\n        var e = events.FirstOrDefault();\n        if (e == null)\n        {\n            throw new ApplicationException(\"Must have some events to bulk import.\");\n        }\n\n        var eventsTable = new DataTable(\"EventDataTable\");\n\n        var idColumn = new DataColumn(nameof(e.Id), e.Id.GetType());\n        eventsTable.Columns.Add(idColumn);\n        var typeColumn = new DataColumn(nameof(e.Type), typeof(int));\n        eventsTable.Columns.Add(typeColumn);\n        var userIdColumn = new DataColumn(nameof(e.UserId), typeof(Guid));\n        eventsTable.Columns.Add(userIdColumn);\n        var organizationIdColumn = new DataColumn(nameof(e.OrganizationId), typeof(Guid));\n        eventsTable.Columns.Add(organizationIdColumn);\n        var cipherIdColumn = new DataColumn(nameof(e.CipherId), typeof(Guid));\n        eventsTable.Columns.Add(cipherIdColumn);\n        var collectionIdColumn = new DataColumn(nameof(e.CollectionId), typeof(Guid));\n        eventsTable.Columns.Add(collectionIdColumn);\n        var policyIdColumn = new DataColumn(nameof(e.PolicyId), typeof(Guid));\n        eventsTable.Columns.Add(policyIdColumn);\n        var groupIdColumn = new DataColumn(nameof(e.GroupId), typeof(Guid));\n        eventsTable.Columns.Add(groupIdColumn);\n        var organizationUserIdColumn = new DataColumn(nameof(e.OrganizationUserId), typeof(Guid));\n        eventsTable.Columns.Add(organizationUserIdColumn);\n        var actingUserIdColumn = new DataColumn(nameof(e.ActingUserId), typeof(Guid));\n        eventsTable.Columns.Add(actingUserIdColumn);\n        var deviceTypeColumn = new DataColumn(nameof(e.DeviceType), typeof(int));\n        eventsTable.Columns.Add(deviceTypeColumn);\n        var ipAddressColumn = new DataColumn(nameof(e.IpAddress), typeof(string));\n        eventsTable.Columns.Add(ipAddressColumn);\n        var dateColumn = new DataColumn(nameof(e.Date), typeof(DateTime));\n        eventsTable.Columns.Add(dateColumn);\n        var secretIdColumn = new DataColumn(nameof(e.SecretId), typeof(Guid));\n        eventsTable.Columns.Add(secretIdColumn);\n        var serviceAccountIdColumn = new DataColumn(nameof(e.ServiceAccountId), typeof(Guid));\n        eventsTable.Columns.Add(serviceAccountIdColumn);\n        var projectIdColumn = new DataColumn(nameof(e.ProjectId), typeof(Guid));\n        eventsTable.Columns.Add(projectIdColumn);\n        var grantedServiceAccountIdColumn = new DataColumn(nameof(e.GrantedServiceAccountId), typeof(Guid));\n        eventsTable.Columns.Add(grantedServiceAccountIdColumn);\n\n        foreach (DataColumn col in eventsTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[1];\n        keys[0] = idColumn;\n        eventsTable.PrimaryKey = keys;\n\n        foreach (var ev in events)\n        {\n            ev.SetNewId();\n\n            var row = eventsTable.NewRow();\n\n            row[idColumn] = ev.Id;\n            row[typeColumn] = (int)ev.Type;\n            row[userIdColumn] = ev.UserId.HasValue ? (object)ev.UserId.Value : DBNull.Value;\n            row[organizationIdColumn] = ev.OrganizationId.HasValue ? (object)ev.OrganizationId.Value : DBNull.Value;\n            row[cipherIdColumn] = ev.CipherId.HasValue ? (object)ev.CipherId.Value : DBNull.Value;\n            row[collectionIdColumn] = ev.CollectionId.HasValue ? (object)ev.CollectionId.Value : DBNull.Value;\n            row[policyIdColumn] = ev.PolicyId.HasValue ? (object)ev.PolicyId.Value : DBNull.Value;\n            row[groupIdColumn] = ev.GroupId.HasValue ? (object)ev.GroupId.Value : DBNull.Value;\n            row[organizationUserIdColumn] = ev.OrganizationUserId.HasValue ?\n                (object)ev.OrganizationUserId.Value : DBNull.Value;\n            row[actingUserIdColumn] = ev.ActingUserId.HasValue ? (object)ev.ActingUserId.Value : DBNull.Value;\n            row[deviceTypeColumn] = ev.DeviceType.HasValue ? (object)ev.DeviceType.Value : DBNull.Value;\n            row[ipAddressColumn] = ev.IpAddress != null ? (object)ev.IpAddress : DBNull.Value;\n            row[dateColumn] = ev.Date;\n            row[secretIdColumn] = ev.SecretId.HasValue ? ev.SecretId.Value : DBNull.Value;\n            row[serviceAccountIdColumn] = ev.ServiceAccountId.HasValue ? ev.ServiceAccountId.Value : DBNull.Value;\n            row[projectIdColumn] = ev.ProjectId.HasValue ? ev.ProjectId.Value : DBNull.Value;\n            row[grantedServiceAccountIdColumn] = ev.GrantedServiceAccountId.HasValue ? ev.GrantedServiceAccountId.Value : DBNull.Value;\n            eventsTable.Rows.Add(row);\n        }\n\n        return eventsTable;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Dirt/Repositories/OrganizationIntegrationConfigurationRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Dirt.Repositories;\n\npublic class OrganizationIntegrationConfigurationRepository : Repository<OrganizationIntegrationConfiguration, Guid>, IOrganizationIntegrationConfigurationRepository\n{\n    public OrganizationIntegrationConfigurationRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public OrganizationIntegrationConfigurationRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<List<OrganizationIntegrationConfigurationDetails>>\n        GetManyByEventTypeOrganizationIdIntegrationType(EventType eventType, Guid organizationId,\n            IntegrationType integrationType)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationIntegrationConfigurationDetails>(\n                \"[dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]\",\n                new\n                {\n                    EventType = eventType,\n                    OrganizationId = organizationId,\n                    IntegrationType = integrationType\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<List<OrganizationIntegrationConfigurationDetails>> GetAllConfigurationDetailsAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationIntegrationConfigurationDetails>(\n                \"[dbo].[OrganizationIntegrationConfigurationDetails_ReadMany]\",\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(Guid organizationIntegrationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationIntegrationConfiguration>(\n                \"[dbo].[OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId]\",\n                new\n                {\n                    OrganizationIntegrationId = organizationIntegrationId\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Dirt/Repositories/OrganizationIntegrationRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Dirt.Repositories;\n\npublic class OrganizationIntegrationRepository : Repository<OrganizationIntegration, Guid>, IOrganizationIntegrationRepository\n{\n    public OrganizationIntegrationRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public OrganizationIntegrationRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationIntegration>(\n                \"[dbo].[OrganizationIntegration_ReadManyByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<OrganizationIntegration?> GetByTeamsConfigurationTenantIdTeamId(string tenantId, string teamId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.QuerySingleOrDefaultAsync<OrganizationIntegration>(\n                \"[dbo].[OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId]\",\n                new { TenantId = tenantId, TeamId = teamId },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Infrastructure.Dapper.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n      <!-- These opt outs should be removed when all warnings are addressed -->\n      <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1305</WarningsNotAsErrors>\n    </PropertyGroup>\n\n    <ItemGroup>\n      <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n    </ItemGroup>\n\n    <ItemGroup>\n      <PackageReference Include=\"Dapper\" Version=\"2.1.66\" />\n    </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Infrastructure.Dapper/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs",
    "content": "﻿#nullable enable\nusing System.Data;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.KeyManagement.Repositories;\n\npublic class UserAsymmetricKeysRepository : BaseRepository, IUserAsymmetricKeysRepository\n{\n    public UserAsymmetricKeysRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n    }\n\n    public UserAsymmetricKeysRepository(string connectionString, string readOnlyConnectionString) : base(\n        connectionString, readOnlyConnectionString)\n    {\n    }\n\n    public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        await connection.ExecuteAsync(\"[dbo].[UserAsymmetricKeys_Regenerate]\",\n            new\n            {\n                userAsymmetricKeys.UserId,\n                userAsymmetricKeys.PublicKey,\n                PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey\n            }, commandType: CommandType.StoredProcedure);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.KeyManagement.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.KeyManagement.Repositories;\n\npublic class UserSignatureKeyPairRepository : Repository<UserSignatureKeyPair, Guid>, IUserSignatureKeyPairRepository\n{\n    public UserSignatureKeyPairRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n    }\n\n    public UserSignatureKeyPairRepository(string connectionString, string readOnlyConnectionString) : base(\n        connectionString, readOnlyConnectionString)\n    {\n    }\n\n    public async Task<SignatureKeyPairData?> GetByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            return (await connection.QuerySingleOrDefaultAsync<UserSignatureKeyPair>(\n                \"[dbo].[UserSignatureKeyPair_ReadByUserId]\",\n                new\n                {\n                    UserId = userId\n                },\n                commandType: CommandType.StoredProcedure))?.ToSignatureKeyPairData();\n        }\n    }\n\n    public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys)\n    {\n        return async (SqlConnection connection, SqlTransaction transaction) =>\n        {\n            await connection.QueryAsync(\n                \"[dbo].[UserSignatureKeyPair_SetForRotation]\",\n                new\n                {\n                    Id = CoreHelpers.GenerateComb(),\n                    UserId = userId,\n                    SignatureAlgorithm = (byte)signingKeys.SignatureAlgorithm,\n                    SigningKey = signingKeys.WrappedSigningKey,\n                    VerifyingKey = signingKeys.VerifyingKey,\n                    CreationDate = DateTime.UtcNow,\n                    RevisionDate = DateTime.UtcNow\n                },\n                commandType: CommandType.StoredProcedure,\n                transaction: transaction);\n        };\n    }\n\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys)\n    {\n        return async (SqlConnection connection, SqlTransaction transaction) =>\n        {\n            await connection.QueryAsync(\n                \"[dbo].[UserSignatureKeyPair_UpdateForRotation]\",\n                new\n                {\n                    UserId = grantorId,\n                    SignatureAlgorithm = (byte)signingKeys.SignatureAlgorithm,\n                    SigningKey = signingKeys.WrappedSigningKey,\n                    VerifyingKey = signingKeys.VerifyingKey,\n                    RevisionDate = DateTime.UtcNow\n                },\n                commandType: CommandType.StoredProcedure,\n                transaction: transaction);\n        };\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs",
    "content": "﻿#nullable enable\nusing System.Data;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Filter;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.NotificationCenter.Repositories;\n\npublic class NotificationRepository : Repository<Notification, Guid>, INotificationRepository\n{\n    public NotificationRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n    }\n\n    public NotificationRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    {\n    }\n\n    public async Task<PagedResult<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId,\n        ClientType clientType, NotificationStatusFilter? statusFilter, PageOptions pageOptions)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        if (!int.TryParse(pageOptions.ContinuationToken, out var pageNumber))\n        {\n            pageNumber = 1;\n        }\n\n        var results = await connection.QueryAsync<NotificationStatusDetails>(\n            \"[dbo].[Notification_ReadByUserIdAndStatus]\",\n            new\n            {\n                UserId = userId,\n                ClientType = clientType,\n                statusFilter?.Read,\n                statusFilter?.Deleted,\n                PageNumber = pageNumber,\n                pageOptions.PageSize\n            },\n            commandType: CommandType.StoredProcedure);\n\n        var data = results.ToList();\n\n        return new PagedResult<NotificationStatusDetails>\n        {\n            Data = data,\n            ContinuationToken = data.Count < pageOptions.PageSize ? null : (pageNumber + 1).ToString()\n        };\n    }\n\n    public async Task<IEnumerable<Guid>> MarkNotificationsAsDeletedByTask(Guid taskId)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        var results = await connection.QueryAsync<Guid>(\n            \"[dbo].[Notification_MarkAsDeletedByTask]\",\n            new\n            {\n                TaskId = taskId,\n            },\n            commandType: CommandType.StoredProcedure);\n\n        var data = results.ToList();\n\n        return data;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationStatusRepository.cs",
    "content": "﻿#nullable enable\nusing System.Data;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.NotificationCenter.Repositories;\n\npublic class NotificationStatusRepository : BaseRepository, INotificationStatusRepository\n{\n    public NotificationStatusRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n    }\n\n    public NotificationStatusRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    {\n    }\n\n    public async Task<NotificationStatus?> GetByNotificationIdAndUserIdAsync(Guid notificationId, Guid userId)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        return await connection.QueryFirstOrDefaultAsync<NotificationStatus>(\n            \"[dbo].[NotificationStatus_ReadByNotificationIdAndUserId]\",\n            new { NotificationId = notificationId, UserId = userId },\n            commandType: CommandType.StoredProcedure);\n    }\n\n    public async Task<NotificationStatus> CreateAsync(NotificationStatus notificationStatus)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        await connection.ExecuteAsync(\"[dbo].[NotificationStatus_Create]\",\n            notificationStatus, commandType: CommandType.StoredProcedure);\n\n        return notificationStatus;\n    }\n\n    public async Task UpdateAsync(NotificationStatus notificationStatus)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        await connection.ExecuteAsync(\"[dbo].[NotificationStatus_Update]\",\n            notificationStatus, commandType: CommandType.StoredProcedure);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Platform/Installations/Repositories/InstallationRepository.cs",
    "content": "﻿using Bit.Core.Platform.Installations;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Platform;\n\n/// <summary>\n/// The CRUD repository for communicating with `dbo.Installation`.\n/// </summary>\n/// <remarks>\n/// If referencing: you probably want the interface `IInstallationRepository`\n/// instead of directly calling this class.\n/// </remarks>\n/// <seealso cref=\"IInstallationRepository\"/>\npublic class InstallationRepository : Repository<Installation, Guid>, IInstallationRepository\n{\n    public InstallationRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public InstallationRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/BaseRepository.cs",
    "content": "﻿using Dapper;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic abstract class BaseRepository\n{\n    static BaseRepository()\n    {\n        SqlMapper.AddTypeHandler(new DateTimeHandler());\n        SqlMapper.AddTypeHandler(new JsonCollectionTypeHandler());\n    }\n\n    public BaseRepository(string connectionString, string readOnlyConnectionString)\n    {\n        if (string.IsNullOrWhiteSpace(connectionString))\n        {\n            throw new ArgumentNullException(nameof(connectionString));\n        }\n        if (string.IsNullOrWhiteSpace(readOnlyConnectionString))\n        {\n            throw new ArgumentNullException(nameof(readOnlyConnectionString));\n        }\n\n        ConnectionString = connectionString;\n        ReadOnlyConnectionString = readOnlyConnectionString;\n    }\n\n    protected string ConnectionString { get; private set; }\n    protected string ReadOnlyConnectionString { get; private set; }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/CollectionCipherRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class CollectionCipherRepository : BaseRepository, ICollectionCipherRepository\n{\n    public CollectionCipherRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public CollectionCipherRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<ICollection<CollectionCipher>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CollectionCipher>(\n                \"[dbo].[CollectionCipher_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<CollectionCipher>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CollectionCipher>(\n                \"[dbo].[CollectionCipher_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<CollectionCipher>> GetManySharedByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CollectionCipher>(\n                \"[dbo].[CollectionCipher_ReadSharedByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<CollectionCipher>> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CollectionCipher>(\n                \"[dbo].[CollectionCipher_ReadByUserIdCipherId]\",\n                new { UserId = userId, CipherId = cipherId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task UpdateCollectionsAsync(Guid cipherId, Guid userId, IEnumerable<Guid> collectionIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                \"[dbo].[CollectionCipher_UpdateCollections]\",\n                new { CipherId = cipherId, UserId = userId, CollectionIds = collectionIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task UpdateCollectionsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable<Guid> collectionIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                \"[dbo].[CollectionCipher_UpdateCollectionsAdmin]\",\n                new { CipherId = cipherId, OrganizationId = organizationId, CollectionIds = collectionIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task UpdateCollectionsForCiphersAsync(IEnumerable<Guid> cipherIds, Guid userId,\n        Guid organizationId, IEnumerable<Guid> collectionIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                \"[dbo].[CollectionCipher_UpdateCollectionsForCiphers]\",\n                new\n                {\n                    CipherIds = cipherIds.ToGuidIdArrayTVP(),\n                    UserId = userId,\n                    OrganizationId = organizationId,\n                    CollectionIds = collectionIds.ToGuidIdArrayTVP()\n                },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task AddCollectionsForManyCiphersAsync(Guid organizationId, IEnumerable<Guid> cipherIds,\n        IEnumerable<Guid> collectionIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[CollectionCipher_AddCollectionsForManyCiphers]\",\n                new { CipherIds = cipherIds.ToGuidIdArrayTVP(), OrganizationId = organizationId, CollectionIds = collectionIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task RemoveCollectionsForManyCiphersAsync(Guid organizationId, IEnumerable<Guid> cipherIds,\n        IEnumerable<Guid> collectionIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[CollectionCipher_RemoveCollectionsForManyCiphers]\",\n                new { CipherIds = cipherIds.ToGuidIdArrayTVP(), OrganizationId = organizationId, CollectionIds = collectionIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/CollectionRepository.cs",
    "content": "﻿using System.Data;\nusing System.Diagnostics.CodeAnalysis;\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Collections;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Infrastructure.Dapper.AdminConsole.Helpers;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class CollectionRepository : Repository<Collection, Guid>, ICollectionRepository\n{\n    public CollectionRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public CollectionRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<int>(\n                \"[dbo].[Collection_ReadCountByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results;\n        }\n    }\n\n    public async Task<Tuple<Collection?, CollectionAccessDetails>> GetByIdWithAccessAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryMultipleAsync(\n                $\"[{Schema}].[Collection_ReadWithGroupsAndUsersById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            var collection = await results.ReadFirstOrDefaultAsync<Collection>();\n            var groups = (await results.ReadAsync<CollectionAccessSelection>()).ToList();\n            var users = (await results.ReadAsync<CollectionAccessSelection>()).ToList();\n            var access = new CollectionAccessDetails { Groups = groups, Users = users };\n\n            return new Tuple<Collection?, CollectionAccessDetails>(collection, access);\n        }\n    }\n\n    public async Task<ICollection<Collection>> GetManyByManyIdsAsync(IEnumerable<Guid> collectionIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Collection>(\n                $\"[{Schema}].[Collection_ReadByIds]\",\n                new { Ids = collectionIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<Collection>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Collection>(\n                $\"[{Schema}].[{Table}_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<Collection>> GetManySharedCollectionsByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Collection>(\n                $\"[{Schema}].[{Table}_ReadSharedCollectionsByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<Tuple<Collection, CollectionAccessDetails>>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryMultipleAsync(\n                $\"[{Schema}].[Collection_ReadWithGroupsAndUsersByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            var collections = (await results.ReadAsync<Collection>());\n            var groups = (await results.ReadAsync<CollectionGroup>())\n                .GroupBy(g => g.CollectionId);\n            var users = (await results.ReadAsync<CollectionUser>())\n                .GroupBy(u => u.CollectionId);\n\n            return collections.Select(collection =>\n                new Tuple<Collection, CollectionAccessDetails>(\n                    collection,\n                    new CollectionAccessDetails\n                    {\n                        Groups = groups\n                            .FirstOrDefault(g => g.Key == collection.Id)?\n                            .Select(g => new CollectionAccessSelection\n                            {\n                                Id = g.GroupId,\n                                HidePasswords = g.HidePasswords,\n                                ReadOnly = g.ReadOnly,\n                                Manage = g.Manage\n                            }).ToList() ?? new List<CollectionAccessSelection>(),\n                        Users = users\n                            .FirstOrDefault(u => u.Key == collection.Id)?\n                            .Select(c => new CollectionAccessSelection\n                            {\n                                Id = c.OrganizationUserId,\n                                HidePasswords = c.HidePasswords,\n                                ReadOnly = c.ReadOnly,\n                                Manage = c.Manage\n                            }).ToList() ?? new List<CollectionAccessSelection>()\n                    }\n                )\n            ).ToList();\n        }\n    }\n\n    public async Task<ICollection<CollectionDetails>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CollectionDetails>(\n                $\"[{Schema}].[Collection_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<CollectionAdminDetails>> GetManySharedByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryMultipleAsync(\n                $\"[{Schema}].[Collection_ReadSharedCollectionsByOrganizationIdWithPermissions]\",\n                new { OrganizationId = organizationId, UserId = userId, IncludeAccessRelationships = includeAccessRelationships },\n                commandType: CommandType.StoredProcedure);\n\n            var collections = (await results.ReadAsync<CollectionAdminDetails>()).ToList();\n\n            if (!includeAccessRelationships)\n            {\n                return collections;\n            }\n\n            var groups = (await results.ReadAsync<CollectionGroup>())\n                .GroupBy(g => g.CollectionId)\n                .ToList();\n            var users = (await results.ReadAsync<CollectionUser>())\n                .GroupBy(u => u.CollectionId)\n                .ToList();\n\n            foreach (var collection in collections)\n            {\n                collection.Groups = groups\n                    .FirstOrDefault(g => g.Key == collection.Id)?\n                    .Select(g => new CollectionAccessSelection\n                    {\n                        Id = g.GroupId,\n                        HidePasswords = g.HidePasswords,\n                        ReadOnly = g.ReadOnly,\n                        Manage = g.Manage\n                    }).ToList() ?? new List<CollectionAccessSelection>();\n                collection.Users = users\n                    .FirstOrDefault(u => u.Key == collection.Id)?\n                    .Select(c => new CollectionAccessSelection\n                    {\n                        Id = c.OrganizationUserId,\n                        HidePasswords = c.HidePasswords,\n                        ReadOnly = c.ReadOnly,\n                        Manage = c.Manage\n                    }).ToList() ?? new List<CollectionAccessSelection>();\n            }\n\n            return collections;\n        }\n    }\n\n    public async Task<CollectionAdminDetails?> GetByIdWithPermissionsAsync(Guid collectionId, Guid? userId, bool includeAccessRelationships)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryMultipleAsync(\n                $\"[{Schema}].[Collection_ReadByIdWithPermissions]\",\n                new { CollectionId = collectionId, UserId = userId, IncludeAccessRelationships = includeAccessRelationships },\n                commandType: CommandType.StoredProcedure);\n\n            var collectionDetails = await results.ReadFirstOrDefaultAsync<CollectionAdminDetails>();\n\n            if (!includeAccessRelationships || collectionDetails == null) return collectionDetails;\n\n            // TODO-NRE: collectionDetails should be checked for null and probably return early\n            collectionDetails!.Groups = (await results.ReadAsync<CollectionAccessSelection>()).ToList();\n            collectionDetails.Users = (await results.ReadAsync<CollectionAccessSelection>()).ToList();\n\n            return collectionDetails;\n        }\n    }\n\n    public async Task CreateAsync(Collection obj, IEnumerable<CollectionAccessSelection>? groups, IEnumerable<CollectionAccessSelection>? users)\n    {\n        obj.SetNewId();\n\n        var objWithGroupsAndUsers = JsonSerializer.Deserialize<CollectionWithGroupsAndUsers>(JsonSerializer.Serialize(obj))!;\n\n        objWithGroupsAndUsers.Groups = groups != null ? groups.ToArrayTVP() : Enumerable.Empty<CollectionAccessSelection>().ToArrayTVP();\n        objWithGroupsAndUsers.Users = users != null ? users.ToArrayTVP() : Enumerable.Empty<CollectionAccessSelection>().ToArrayTVP();\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Collection_CreateWithGroupsAndUsers]\",\n                objWithGroupsAndUsers,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task ReplaceAsync(Collection obj, IEnumerable<CollectionAccessSelection>? groups, IEnumerable<CollectionAccessSelection>? users)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n        await connection.OpenAsync();\n        await using var transaction = await connection.BeginTransactionAsync();\n        try\n        {\n            if (groups == null && users == null)\n            {\n                await connection.ExecuteAsync(\n                    $\"[{Schema}].[Collection_Update]\",\n                    obj,\n                    commandType: CommandType.StoredProcedure,\n                    transaction: transaction);\n            }\n            else if (groups != null && users == null)\n            {\n                await connection.ExecuteAsync(\n                    $\"[{Schema}].[Collection_UpdateWithGroups]\",\n                    new CollectionWithGroups(obj, groups),\n                    commandType: CommandType.StoredProcedure,\n                    transaction: transaction);\n            }\n            else if (groups == null && users != null)\n            {\n                await connection.ExecuteAsync(\n                    $\"[{Schema}].[Collection_UpdateWithUsers]\",\n                    new CollectionWithUsers(obj, users),\n                    commandType: CommandType.StoredProcedure,\n                    transaction: transaction);\n            }\n            else if (groups != null && users != null)\n            {\n                await connection.ExecuteAsync(\n                    $\"[{Schema}].[Collection_UpdateWithGroupsAndUsers]\",\n                    new CollectionWithGroupsAndUsers(obj, groups, users),\n                    commandType: CommandType.StoredProcedure,\n                    transaction: transaction);\n            }\n\n            await transaction.CommitAsync();\n        }\n        catch\n        {\n            await transaction.RollbackAsync();\n            throw;\n        }\n\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> collectionIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\"[dbo].[Collection_DeleteByIds]\",\n                new { Ids = collectionIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> collectionIds,\n        IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var usersArray = users != null ? users.ToArrayTVP() : Enumerable.Empty<CollectionAccessSelection>().ToArrayTVP();\n            var groupsArray = groups != null ? groups.ToArrayTVP() : Enumerable.Empty<CollectionAccessSelection>().ToArrayTVP();\n\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Collection_CreateOrUpdateAccessForMany]\",\n                new { OrganizationId = organizationId, CollectionIds = collectionIds.ToGuidIdArrayTVP(), Users = usersArray, Groups = groupsArray },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task CreateUserAsync(Guid collectionId, Guid organizationUserId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[CollectionUser_Create]\",\n                new { CollectionId = collectionId, OrganizationUserId = organizationUserId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task DeleteUserAsync(Guid collectionId, Guid organizationUserId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[CollectionUser_Delete]\",\n                new { CollectionId = collectionId, OrganizationUserId = organizationUserId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task UpdateUsersAsync(Guid id, IEnumerable<CollectionAccessSelection> users)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[CollectionUser_UpdateUsers]\",\n                new { CollectionId = id, Users = users.ToArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<ICollection<CollectionAccessSelection>> GetManyUsersByIdAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CollectionAccessSelection>(\n                $\"[{Schema}].[CollectionUser_ReadByCollectionId]\",\n                new { CollectionId = id },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)\n    {\n        organizationUserIds = organizationUserIds.ToList();\n        if (!organizationUserIds.Any())\n        {\n            return;\n        }\n\n        var organizationUserCollectionIds = organizationUserIds\n            .Select(ou => (ou, CoreHelpers.GenerateComb()))\n            .ToTwoGuidIdArrayTVP();\n\n        await using var connection = new SqlConnection(ConnectionString);\n        await connection.OpenAsync();\n        await using var transaction = connection.BeginTransaction();\n\n        try\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[Collection_CreateDefaultCollections]\",\n                new\n                {\n                    OrganizationId = organizationId,\n                    DefaultCollectionName = defaultCollectionName,\n                    OrganizationUserCollectionIds = organizationUserCollectionIds\n                },\n                commandType: CommandType.StoredProcedure,\n                transaction: transaction);\n\n            await transaction.CommitAsync();\n        }\n        catch\n        {\n            await transaction.RollbackAsync();\n            throw;\n        }\n    }\n\n    public async Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)\n    {\n        organizationUserIds = organizationUserIds.ToList();\n        if (!organizationUserIds.Any())\n        {\n            return;\n        }\n\n        await using var connection = new SqlConnection(ConnectionString);\n        connection.Open();\n        await using var transaction = connection.BeginTransaction();\n        try\n        {\n            var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(connection, transaction, organizationId);\n\n            var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection);\n\n            var (collections, collectionUsers) =\n                CollectionUtils.BuildDefaultUserCollections(organizationId, missingDefaultCollectionUserIds, defaultCollectionName);\n\n            if (!collectionUsers.Any() || !collections.Any())\n            {\n                return;\n            }\n\n            await BulkResourceCreationService.CreateCollectionsAsync(connection, transaction, collections);\n            await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers);\n\n            await transaction.CommitAsync();\n        }\n        catch\n        {\n            await transaction.RollbackAsync();\n            throw;\n        }\n    }\n\n    private async Task<HashSet<Guid>> GetOrgUserIdsWithDefaultCollectionAsync(SqlConnection connection, SqlTransaction transaction, Guid organizationId)\n    {\n        const string sql = @\"\n                    SELECT\n                        ou.Id AS OrganizationUserId\n                    FROM\n                        OrganizationUser ou\n                    INNER JOIN\n                        CollectionUser cu ON cu.OrganizationUserId = ou.Id\n                    INNER JOIN\n                        Collection c ON c.Id = cu.CollectionId\n                    WHERE\n                        ou.OrganizationId = @OrganizationId\n                        AND c.Type = @CollectionType;\n                \";\n\n        var organizationUserIds = await connection.QueryAsync<Guid>(\n            sql,\n            new { OrganizationId = organizationId, CollectionType = CollectionType.DefaultUserCollection },\n            transaction: transaction\n        );\n\n        return organizationUserIds.ToHashSet();\n    }\n\n    public class CollectionWithGroupsAndUsers : Collection\n    {\n        public CollectionWithGroupsAndUsers() { }\n\n        public CollectionWithGroupsAndUsers(Collection collection,\n            IEnumerable<CollectionAccessSelection> groups,\n            IEnumerable<CollectionAccessSelection> users)\n        {\n            Id = collection.Id;\n            Name = collection.Name;\n            OrganizationId = collection.OrganizationId;\n            CreationDate = collection.CreationDate;\n            RevisionDate = collection.RevisionDate;\n            Type = collection.Type;\n            ExternalId = collection.ExternalId;\n            DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail;\n            Groups = groups.ToArrayTVP();\n            Users = users.ToArrayTVP();\n        }\n\n        [DisallowNull]\n        public DataTable? Groups { get; set; }\n        [DisallowNull]\n        public DataTable? Users { get; set; }\n    }\n\n    public class CollectionWithGroups : Collection\n    {\n        public CollectionWithGroups() { }\n\n        public CollectionWithGroups(Collection collection, IEnumerable<CollectionAccessSelection> groups)\n        {\n            Id = collection.Id;\n            Name = collection.Name;\n            OrganizationId = collection.OrganizationId;\n            CreationDate = collection.CreationDate;\n            RevisionDate = collection.RevisionDate;\n            Type = collection.Type;\n            ExternalId = collection.ExternalId;\n            DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail;\n            Groups = groups.ToArrayTVP();\n        }\n\n        [DisallowNull]\n        public DataTable? Groups { get; set; }\n    }\n\n    public class CollectionWithUsers : Collection\n    {\n        public CollectionWithUsers() { }\n\n        public CollectionWithUsers(Collection collection, IEnumerable<CollectionAccessSelection> users)\n        {\n\n            Id = collection.Id;\n            Name = collection.Name;\n            OrganizationId = collection.OrganizationId;\n            CreationDate = collection.CreationDate;\n            RevisionDate = collection.RevisionDate;\n            Type = collection.Type;\n            ExternalId = collection.ExternalId;\n            DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail;\n            Users = users.ToArrayTVP();\n        }\n\n        [DisallowNull]\n        public DataTable? Users { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/DateTimeHandler.cs",
    "content": "﻿using System.Data;\nusing Dapper;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class DateTimeHandler : SqlMapper.TypeHandler<DateTime>\n{\n    public override void SetValue(IDbDataParameter parameter, DateTime value)\n    {\n        parameter.Value = value;\n    }\n\n    public override DateTime Parse(object value)\n    {\n        return DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/DeviceRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class DeviceRepository : Repository<Device, Guid>, IDeviceRepository\n{\n    private readonly IGlobalSettings _globalSettings;\n\n    public DeviceRepository(GlobalSettings globalSettings)\n        : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public async Task<Device?> GetByIdAsync(Guid id, Guid userId)\n    {\n        var device = await GetByIdAsync(id);\n        if (device == null || device.UserId != userId)\n        {\n            return null;\n        }\n\n        return device;\n    }\n\n    public async Task<Device?> GetByIdentifierAsync(string identifier)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Device>(\n                $\"[{Schema}].[{Table}_ReadByIdentifier]\",\n                new\n                {\n                    Identifier = identifier\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<Device?> GetByIdentifierAsync(string identifier, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Device>(\n                $\"[{Schema}].[{Table}_ReadByIdentifierUserId]\",\n                new\n                {\n                    UserId = userId,\n                    Identifier = identifier\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<ICollection<Device>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Device>(\n                $\"[{Schema}].[{Table}_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId)\n    {\n        var expirationMinutes = _globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<DeviceAuthDetails>(\n                $\"[{Schema}].[{Table}_ReadActiveWithPendingAuthRequestsByUserId]\",\n                new\n                {\n                    UserId = userId,\n                    ExpirationMinutes = expirationMinutes\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task ClearPushTokenAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_ClearPushTokenById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<Device> devices)\n    {\n        return async (SqlConnection connection, SqlTransaction transaction) =>\n        {\n            const string sql = @\"\n                UPDATE D\n                SET\n                    D.[EncryptedPublicKey] = UD.[encryptedPublicKey],\n                    D.[EncryptedUserKey] = UD.[encryptedUserKey]\n                FROM\n                    [dbo].[Device] D\n                INNER JOIN\n                    OPENJSON(@DeviceCredentials)\n                    WITH (\n                        id UNIQUEIDENTIFIER,\n                        encryptedPublicKey NVARCHAR(MAX),\n                        encryptedUserKey NVARCHAR(MAX)\n                    ) UD\n                    ON UD.[id] = D.[Id]\n                WHERE\n                    D.[UserId] = @UserId\";\n            var deviceCredentials = CoreHelpers.ClassToJsonData(devices);\n\n            await connection.ExecuteAsync(\n                sql,\n                new { UserId = userId, DeviceCredentials = deviceCredentials },\n                transaction: transaction,\n                commandType: CommandType.Text);\n        };\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/JsonCollectionTypeHandler.cs",
    "content": "﻿using System.Data;\nusing System.Text.Json;\nusing Dapper;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class JsonCollectionTypeHandler : SqlMapper.TypeHandler<ICollection<string>?>\n{\n    public override void SetValue(IDbDataParameter parameter, ICollection<string>? value)\n    {\n        parameter.Value = value == null ? (object)DBNull.Value : JsonSerializer.Serialize(value);\n    }\n\n    public override ICollection<string>? Parse(object value)\n    {\n        if (value == null || value is DBNull)\n        {\n            return null;\n        }\n\n        var json = value.ToString();\n        if (string.IsNullOrWhiteSpace(json))\n        {\n            return null;\n        }\n\n        return JsonSerializer.Deserialize<List<string>>(json);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/MaintenanceRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class MaintenanceRepository : BaseRepository, IMaintenanceRepository\n{\n    public MaintenanceRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public MaintenanceRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task UpdateStatisticsAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[AzureSQLMaintenance]\",\n                new { operation = \"statistics\", mode = \"smart\", LogToTable = true },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 172800);\n        }\n    }\n\n    public async Task DisableCipherAutoStatsAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"sp_autostats\",\n                new { tblname = \"[dbo].[Cipher]\", flagc = \"OFF\" },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task RebuildIndexesAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[AzureSQLMaintenance]\",\n                new { operation = \"index\", mode = \"smart\", LogToTable = true },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 172800);\n        }\n    }\n\n    public async Task DeleteExpiredGrantsAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[Grant_DeleteExpired]\",\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 172800);\n        }\n    }\n\n    public async Task DeleteExpiredSponsorshipsAsync(DateTime validUntilBeforeDate)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[OrganizationSponsorship_DeleteExpired]\",\n                new { ValidUntilBeforeDate = validUntilBeforeDate },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 172800);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/OrganizationApiKeyRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class OrganizationApiKeyRepository : Repository<OrganizationApiKey, Guid>, IOrganizationApiKeyRepository\n{\n    public OrganizationApiKeyRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n\n    }\n\n    public OrganizationApiKeyRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<IEnumerable<OrganizationApiKey>> GetManyByOrganizationIdTypeAsync(Guid organizationId, OrganizationApiKeyType? type = null)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            return await connection.QueryAsync<OrganizationApiKey>(\n                \"[dbo].[OrganizationApikey_ReadManyByOrganizationIdType]\",\n                new\n                {\n                    OrganizationId = organizationId,\n                    Type = type,\n                },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/OrganizationConnectionRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class OrganizationConnectionRepository : Repository<OrganizationConnection, Guid>, IOrganizationConnectionRepository\n{\n    public OrganizationConnectionRepository(GlobalSettings globalSettings)\n        : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public async Task<OrganizationConnection?> GetByIdOrganizationIdAsync(Guid id, Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationConnection>(\n                $\"[{Schema}].[OrganizationConnection_ReadByIdOrganizationId]\",\n                new\n                {\n                    Id = id,\n                    OrganizationId = organizationId\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<ICollection<OrganizationConnection>> GetByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationConnection>(\n                $\"[{Schema}].[OrganizationConnection_ReadByOrganizationIdType]\",\n                new\n                {\n                    OrganizationId = organizationId,\n                    Type = type\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<OrganizationConnection>> GetEnabledByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type) =>\n        (await GetByOrganizationIdTypeAsync(organizationId, type)).Where(c => c.Enabled).ToList();\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/OrganizationDomainRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class OrganizationDomainRepository : Repository<OrganizationDomain, Guid>, IOrganizationDomainRepository\n{\n    public OrganizationDomainRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public OrganizationDomainRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<ICollection<OrganizationDomain>> GetClaimedDomainsByDomainNameAsync(string domainName)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationDomain>(\n                $\"[{Schema}].[OrganizationDomain_ReadByClaimedDomain]\",\n                new { DomainName = domainName },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationDomain>(\n                $\"[{Schema}].[OrganizationDomain_ReadByOrganizationId]\",\n                new { OrganizationId = orgId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationDomain>> GetVerifiedDomainsByOrganizationIdsAsync(IEnumerable<Guid> organizationIds)\n    {\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationDomain>(\n                $\"[{Schema}].[OrganizationDomain_ReadByOrganizationIds]\",\n                new { OrganizationIds = organizationIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<OrganizationDomain>> GetManyByNextRunDateAsync(DateTime date)\n    {\n        using var connection = new SqlConnection(ConnectionString);\n        var results = await connection.QueryAsync<OrganizationDomain>(\n            $\"[{Schema}].[OrganizationDomain_ReadByNextRunDate]\",\n            new { Date = date }, commandType: CommandType.StoredProcedure\n        );\n\n        return results.ToList();\n    }\n\n    public async Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection\n                .QueryAsync<OrganizationDomainSsoDetailsData>(\n                    $\"[{Schema}].[OrganizationDomainSsoDetails_ReadByEmail]\",\n                    new { Email = email },\n                    commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        return await connection\n            .QueryAsync<VerifiedOrganizationDomainSsoDetail>(\n                $\"[{Schema}].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]\",\n                new { Email = email },\n                commandType: CommandType.StoredProcedure);\n    }\n\n    public async Task<OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection\n                .QueryAsync<OrganizationDomain>(\n                    $\"[{Schema}].[OrganizationDomain_ReadByIdOrganizationId]\",\n                    new { Id = id, OrganizationId = orgId },\n                    commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<OrganizationDomain?> GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection\n                .QueryAsync<OrganizationDomain>(\n                    $\"[{Schema}].[OrganizationDomain_ReadDomainByOrgIdAndDomainName]\",\n                    new { OrganizationId = orgId, DomainName = domainName },\n                    commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<ICollection<OrganizationDomain>> GetExpiredOrganizationDomainsAsync()\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection\n                .QueryAsync<OrganizationDomain>(\n                    $\"[{Schema}].[OrganizationDomain_ReadIfExpired]\",\n                    null,\n                    commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<bool> DeleteExpiredAsync(int expirationPeriod)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            return await connection.ExecuteAsync(\n                $\"[{Schema}].[OrganizationDomain_DeleteIfExpired]\",\n                new { ExpirationPeriod = expirationPeriod },\n                commandType: CommandType.StoredProcedure) > 0;\n        }\n    }\n\n    public async Task<bool> HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(string domainName, Guid? excludeOrganizationId = null)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        var result = await connection.QueryFirstOrDefaultAsync<bool>(\n            $\"[{Schema}].[OrganizationDomain_HasVerifiedDomainWithBlockPolicy]\",\n            new { DomainName = domainName, ExcludeOrganizationId = excludeOrganizationId },\n            commandType: CommandType.StoredProcedure);\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class OrganizationSponsorshipRepository : Repository<OrganizationSponsorship, Guid>, IOrganizationSponsorshipRepository\n{\n    public OrganizationSponsorshipRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public OrganizationSponsorshipRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<ICollection<Guid>?> CreateManyAsync(IEnumerable<OrganizationSponsorship> organizationSponsorships)\n    {\n        if (!organizationSponsorships.Any())\n        {\n            return default;\n        }\n\n        foreach (var organizationSponsorship in organizationSponsorships)\n        {\n            organizationSponsorship.SetNewId();\n        }\n\n        var orgSponsorshipsTVP = organizationSponsorships.ToTvp();\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[dbo].[OrganizationSponsorship_CreateMany]\",\n                new { OrganizationSponsorshipsInput = orgSponsorshipsTVP },\n                commandType: CommandType.StoredProcedure);\n        }\n\n        return organizationSponsorships.Select(u => u.Id).ToList();\n    }\n\n    public async Task ReplaceManyAsync(IEnumerable<OrganizationSponsorship> organizationSponsorships)\n    {\n        if (!organizationSponsorships.Any())\n        {\n            return;\n        }\n\n        var orgSponsorshipsTVP = organizationSponsorships.ToTvp();\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[dbo].[OrganizationSponsorship_UpdateMany]\",\n                new { OrganizationSponsorshipsInput = orgSponsorshipsTVP },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task UpsertManyAsync(IEnumerable<OrganizationSponsorship> organizationSponsorships)\n    {\n        var createSponsorships = new List<OrganizationSponsorship>();\n        var replaceSponsorships = new List<OrganizationSponsorship>();\n        foreach (var organizationSponsorship in organizationSponsorships)\n        {\n            if (organizationSponsorship.Id.Equals(default))\n            {\n                createSponsorships.Add(organizationSponsorship);\n            }\n            else\n            {\n                replaceSponsorships.Add(organizationSponsorship);\n            }\n        }\n\n        await CreateManyAsync(createSponsorships);\n        await ReplaceManyAsync(replaceSponsorships);\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> organizationSponsorshipIds)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\"[dbo].[OrganizationSponsorship_DeleteByIds]\",\n                new { Ids = organizationSponsorshipIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationSponsorship>(\n                \"[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]\",\n                new\n                {\n                    SponsoringOrganizationUserId = sponsoringOrganizationUserId,\n                    isAdminInitiated = isAdminInitiated\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<OrganizationSponsorship?> GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationSponsorship>(\n                \"[dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]\",\n                new { SponsoredOrganizationId = sponsoredOrganizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<DateTime?> GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            return await connection.QuerySingleOrDefaultAsync<DateTime?>(\n                \"[dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]\",\n                new { SponsoringOrganizationId = sponsoringOrganizationId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<ICollection<OrganizationSponsorship>> GetManyBySponsoringOrganizationAsync(Guid sponsoringOrganizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationSponsorship>(\n                \"[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]\",\n                new\n                {\n                    SponsoringOrganizationId = sponsoringOrganizationId\n                },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/PlayItemRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class PlayItemRepository : Repository<PlayItem, Guid>, IPlayItemRepository\n{\n    public PlayItemRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public PlayItemRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<ICollection<PlayItem>> GetByPlayIdAsync(string playId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<PlayItem>(\n                \"[dbo].[PlayItem_ReadByPlayId]\",\n                new { PlayId = playId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task DeleteByPlayIdAsync(string playId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[PlayItem_DeleteByPlayId]\",\n                new { PlayId = playId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/Repository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId>\n    where TId : IEquatable<TId>\n    where T : class, ITableObject<TId>\n{\n    public Repository(string connectionString, string readOnlyConnectionString,\n        string? schema = null, string? table = null)\n        : base(connectionString, readOnlyConnectionString)\n    {\n        if (!string.IsNullOrWhiteSpace(table))\n        {\n            Table = table;\n        }\n\n        if (!string.IsNullOrWhiteSpace(schema))\n        {\n            Schema = schema;\n        }\n    }\n\n    protected string Schema { get; private set; } = \"dbo\";\n    protected string Table { get; private set; } = typeof(T).Name;\n\n    public virtual async Task<T?> GetByIdAsync(TId id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<T>(\n                $\"[{Schema}].[{Table}_ReadById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public virtual async Task<T> CreateAsync(T obj)\n    {\n        obj.SetNewId();\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var parameters = new DynamicParameters();\n            parameters.AddDynamicParams(obj);\n            parameters.Add(\"Id\", obj.Id, direction: ParameterDirection.InputOutput);\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_Create]\",\n                parameters,\n                commandType: CommandType.StoredProcedure);\n            obj.Id = parameters.Get<TId>(nameof(obj.Id));\n        }\n        return obj;\n    }\n\n    public virtual async Task ReplaceAsync(T obj)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_Update]\",\n                obj,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public virtual async Task UpsertAsync(T obj)\n    {\n        if (obj.Id.Equals(default(TId)))\n        {\n            await CreateAsync(obj);\n        }\n        else\n        {\n            await ReplaceAsync(obj);\n        }\n    }\n\n    public virtual async Task DeleteAsync(T obj)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_DeleteById]\",\n                new { Id = obj.Id },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/TransactionRepository.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class TransactionRepository : Repository<Transaction, Guid>, ITransactionRepository\n{\n    public TransactionRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public TransactionRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<ICollection<Transaction>> GetManyByUserIdAsync(\n        Guid userId,\n        int? limit = null,\n        DateTime? startAfter = null)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n        var results = await connection.QueryAsync<Transaction>(\n            $\"[{Schema}].[Transaction_ReadByUserId]\",\n            new\n            {\n                UserId = userId,\n                Limit = limit ?? int.MaxValue,\n                StartAfter = startAfter\n            },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToList();\n    }\n\n    public async Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(\n        Guid organizationId,\n        int? limit = null,\n        DateTime? startAfter = null)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        var results = await connection.QueryAsync<Transaction>(\n            $\"[{Schema}].[Transaction_ReadByOrganizationId]\",\n            new\n            {\n                OrganizationId = organizationId,\n                Limit = limit ?? int.MaxValue,\n                StartAfter = startAfter\n            },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToList();\n    }\n\n    public async Task<ICollection<Transaction>> GetManyByProviderIdAsync(\n        Guid providerId,\n        int? limit = null,\n        DateTime? startAfter = null)\n    {\n        await using var sqlConnection = new SqlConnection(ConnectionString);\n\n        var results = await sqlConnection.QueryAsync<Transaction>(\n            $\"[{Schema}].[Transaction_ReadByProviderId]\",\n            new\n            {\n                ProviderId = providerId,\n                Limit = limit ?? int.MaxValue,\n                StartAfter = startAfter\n            },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToList();\n    }\n\n    public async Task<Transaction?> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId)\n    {\n        // maybe come back to this\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Transaction>(\n                $\"[{Schema}].[Transaction_ReadByGatewayId]\",\n                new { Gateway = gatewayType, GatewayId = gatewayId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Repositories/UserRepository.cs",
    "content": "﻿using System.Data;\nusing System.Text.Json;\nusing Bit.Core;\nusing Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Dapper;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Repositories;\n\npublic class UserRepository : Repository<User, Guid>, IUserRepository\n{\n    private readonly IDataProtector _dataProtector;\n\n    public UserRepository(\n        GlobalSettings globalSettings,\n        IDataProtectionProvider dataProtectionProvider)\n        : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    {\n        _dataProtector = dataProtectionProvider.CreateProtector(Constants.DatabaseFieldProtectorPurpose);\n    }\n\n    public override async Task<User?> GetByIdAsync(Guid id)\n    {\n        var user = await base.GetByIdAsync(id);\n        UnprotectData(user);\n        return user;\n    }\n\n    public async Task<User?> GetByGatewayCustomerIdAsync(string gatewayCustomerId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<User>(\n                \"[dbo].[User_ReadByGatewayCustomerId]\",\n                new { GatewayCustomerId = gatewayCustomerId },\n                commandType: CommandType.StoredProcedure);\n\n            UnprotectData(results);\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<User?> GetByGatewaySubscriptionIdAsync(string gatewaySubscriptionId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<User>(\n                \"[dbo].[User_ReadByGatewaySubscriptionId]\",\n                new { GatewaySubscriptionId = gatewaySubscriptionId },\n                commandType: CommandType.StoredProcedure);\n\n            UnprotectData(results);\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<User?> GetByEmailAsync(string email)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<User>(\n                $\"[{Schema}].[{Table}_ReadByEmail]\",\n                new { Email = email },\n                commandType: CommandType.StoredProcedure);\n\n            UnprotectData(results);\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<IEnumerable<User>> GetManyByEmailsAsync(IEnumerable<string> emails)\n    {\n        var emailTable = new DataTable();\n        emailTable.Columns.Add(\"Email\", typeof(string));\n        foreach (var email in emails)\n        {\n            emailTable.Rows.Add(email);\n        }\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<User>(\n                $\"[{Schema}].[{Table}_ReadByEmails]\",\n                new { Emails = emailTable.AsTableValuedParameter(\"dbo.EmailArray\") },\n                commandType: CommandType.StoredProcedure);\n\n            UnprotectData(results);\n            return results.ToList();\n        }\n    }\n\n    public async Task<User?> GetBySsoUserAsync(string externalId, Guid? organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<User>(\n                $\"[{Schema}].[{Table}_ReadBySsoUserOrganizationIdExternalId]\",\n                new { OrganizationId = organizationId, ExternalId = externalId },\n                commandType: CommandType.StoredProcedure);\n\n            UnprotectData(results);\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<UserKdfInformation?> GetKdfInformationByEmailAsync(string email)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<UserKdfInformation>(\n                $\"[{Schema}].[{Table}_ReadKdfByEmail]\",\n                new { Email = email },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<ICollection<User>> SearchAsync(string email, int skip, int take)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<User>(\n                $\"[{Schema}].[{Table}_Search]\",\n                new { Email = email, Skip = skip, Take = take },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 120);\n\n            UnprotectData(results);\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<User>> GetManyByPremiumAsync(bool premium)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<User>(\n                \"[dbo].[User_ReadByPremium]\",\n                new { Premium = premium },\n                commandType: CommandType.StoredProcedure);\n\n            UnprotectData(results);\n            return results.ToList();\n        }\n    }\n\n    public async Task<string?> GetPublicKeyAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<string>(\n                $\"[{Schema}].[{Table}_ReadPublicKeyById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public async Task<DateTime> GetAccountRevisionDateAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<DateTime>(\n                $\"[{Schema}].[{Table}_ReadAccountRevisionDateById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            return results.SingleOrDefault();\n        }\n    }\n\n    public override async Task<User> CreateAsync(User user)\n    {\n        await ProtectDataAndSaveAsync(user, async () => await base.CreateAsync(user));\n        return user;\n    }\n\n    public override async Task ReplaceAsync(User user)\n    {\n        await ProtectDataAndSaveAsync(user, async () => await base.ReplaceAsync(user));\n    }\n\n    public override async Task DeleteAsync(User user)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_DeleteById]\",\n                new { Id = user.Id },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 180);\n        }\n    }\n    public async Task DeleteManyAsync(IEnumerable<User> users)\n    {\n        var ids = users.Select(user => user.Id);\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_DeleteByIds]\",\n                new { Ids = JsonSerializer.Serialize(ids) },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 180);\n        }\n    }\n\n    public async Task UpdateStorageAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_UpdateStorage]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 180);\n        }\n    }\n\n    public async Task UpdateRenewalReminderDateAsync(Guid id, DateTime renewalReminderDate)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[User_UpdateRenewalReminderDate]\",\n                new { Id = id, RenewalReminderDate = renewalReminderDate },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task UpdateUserKeyAndEncryptedDataAsync(\n        User user,\n        IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n        connection.Open();\n\n        await using var transaction = connection.BeginTransaction();\n        try\n        {\n            // Update user\n            await using (var cmd = new SqlCommand(\"[dbo].[User_UpdateKeys]\", connection, transaction))\n            {\n                cmd.CommandType = CommandType.StoredProcedure;\n                cmd.Parameters.Add(\"@Id\", SqlDbType.UniqueIdentifier).Value = user.Id;\n                cmd.Parameters.Add(\"@SecurityStamp\", SqlDbType.NVarChar).Value = user.SecurityStamp;\n                cmd.Parameters.Add(\"@Key\", SqlDbType.VarChar).Value = user.Key;\n\n                cmd.Parameters.Add(\"@PrivateKey\", SqlDbType.VarChar).Value =\n                    string.IsNullOrWhiteSpace(user.PrivateKey) ? DBNull.Value : user.PrivateKey;\n\n                cmd.Parameters.Add(\"@RevisionDate\", SqlDbType.DateTime2).Value = user.RevisionDate;\n                cmd.Parameters.Add(\"@AccountRevisionDate\", SqlDbType.DateTime2).Value =\n                    user.AccountRevisionDate;\n                cmd.Parameters.Add(\"@LastKeyRotationDate\", SqlDbType.DateTime2).Value =\n                    user.LastKeyRotationDate;\n                cmd.ExecuteNonQuery();\n            }\n\n            //  Update re-encrypted data\n            foreach (var action in updateDataActions)\n            {\n                await action(connection, transaction);\n            }\n\n            transaction.Commit();\n        }\n        catch\n        {\n            transaction.Rollback();\n            throw;\n        }\n    }\n\n    public async Task UpdateUserKeyAndEncryptedDataV2Async(\n        User user,\n        IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n        connection.Open();\n\n        await using var transaction = connection.BeginTransaction();\n        try\n        {\n            user.AccountRevisionDate = user.RevisionDate;\n\n            ProtectData(user);\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[{Table}_Update]\",\n                user,\n                transaction: transaction,\n                commandType: CommandType.StoredProcedure);\n\n            //  Update re-encrypted data\n            foreach (var action in updateDataActions)\n            {\n                await action(connection, transaction);\n            }\n            transaction.Commit();\n        }\n        catch\n        {\n            transaction.Rollback();\n            UnprotectData(user);\n            throw;\n        }\n        UnprotectData(user);\n    }\n\n    public async Task SetV2AccountCryptographicStateAsync(\n        Guid userId,\n        UserAccountKeysData accountKeysData,\n        IEnumerable<UpdateUserData>? updateUserDataActions = null)\n    {\n        if (!accountKeysData.IsV2Encryption())\n        {\n            throw new ArgumentException(\"Provided account keys data is not valid V2 encryption data.\", nameof(accountKeysData));\n        }\n\n        var timestamp = DateTime.UtcNow;\n        var signatureKeyPairId = CoreHelpers.GenerateComb();\n\n        await using var connection = new SqlConnection(ConnectionString);\n        await connection.OpenAsync();\n\n        await using var transaction = connection.BeginTransaction();\n        try\n        {\n            await connection.ExecuteAsync(\n                \"[dbo].[User_UpdateAccountCryptographicState]\",\n                new\n                {\n                    Id = userId,\n                    PublicKey = accountKeysData.PublicKeyEncryptionKeyPairData.PublicKey,\n                    PrivateKey = accountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey,\n                    SignedPublicKey = accountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey,\n                    SecurityState = accountKeysData.SecurityStateData!.SecurityState,\n                    SecurityVersion = accountKeysData.SecurityStateData!.SecurityVersion,\n                    SignatureKeyPairId = signatureKeyPairId,\n                    SignatureAlgorithm = accountKeysData.SignatureKeyPairData!.SignatureAlgorithm,\n                    SigningKey = accountKeysData.SignatureKeyPairData!.WrappedSigningKey,\n                    VerifyingKey = accountKeysData.SignatureKeyPairData!.VerifyingKey,\n                    RevisionDate = timestamp,\n                    AccountRevisionDate = timestamp\n                },\n                transaction: transaction,\n                commandType: CommandType.StoredProcedure);\n\n            //  Update user data that depends on cryptographic state\n            if (updateUserDataActions != null)\n            {\n                foreach (var action in updateUserDataActions)\n                {\n                    await action(connection, transaction);\n                }\n            }\n\n            await transaction.CommitAsync();\n        }\n        catch\n        {\n            await transaction.RollbackAsync();\n            throw;\n        }\n    }\n\n    public async Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<User>(\n                $\"[{Schema}].[{Table}_ReadByIds]\",\n                new { Ids = ids.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            UnprotectData(results);\n            return results.ToList();\n        }\n    }\n\n    public async Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<UserWithCalculatedPremium>(\n                $\"[{Schema}].[{Table}_ReadByIdsWithCalculatedPremium]\",\n                new { Ids = JsonSerializer.Serialize(ids) },\n                commandType: CommandType.StoredProcedure);\n\n            UnprotectData(results);\n            return results.ToList();\n        }\n    }\n\n    public async Task<UserWithCalculatedPremium?> GetCalculatedPremiumAsync(Guid userId)\n    {\n        var result = await GetManyWithCalculatedPremiumAsync([userId]);\n\n        UnprotectData(result);\n        return result.SingleOrDefault();\n    }\n\n    public async Task<IEnumerable<UserPremiumAccess>> GetPremiumAccessByIdsAsync(IEnumerable<Guid> ids)\n    {\n        using (var connection = new SqlConnection(ReadOnlyConnectionString))\n        {\n            var results = await connection.QueryAsync<UserPremiumAccess>(\n                $\"[{Schema}].[{Table}_ReadPremiumAccessByIds]\",\n                new { Ids = ids.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<UserPremiumAccess?> GetPremiumAccessAsync(Guid userId)\n    {\n        var result = await GetPremiumAccessByIdsAsync([userId]);\n        return result.SingleOrDefault();\n    }\n\n    public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey)\n    {\n        var protectedKeyConnectorWrappedUserKey = string.Concat(Constants.DatabaseFieldProtectedPrefix,\n            _dataProtector.Protect(keyConnectorWrappedUserKey));\n\n        return async (connection, transaction) =>\n        {\n            var timestamp = DateTime.UtcNow;\n\n            await connection!.ExecuteAsync(\n                \"[dbo].[User_UpdateKeyConnectorUserKey]\",\n                new\n                {\n                    Id = userId,\n                    Key = protectedKeyConnectorWrappedUserKey,\n                    // Key Connector does not use KDF, so we set some defaults\n                    Kdf = KdfType.Argon2id,\n                    KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,\n                    KdfMemory = AuthConstants.ARGON2_MEMORY.Default,\n                    KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,\n                    UsesKeyConnector = true,\n                    RevisionDate = timestamp,\n                    AccountRevisionDate = timestamp\n                },\n                transaction: transaction,\n                commandType: CommandType.StoredProcedure);\n        };\n    }\n\n    public UpdateUserData SetMasterPassword(Guid userId, MasterPasswordUnlockData masterPasswordUnlockData,\n        string serverSideHashedMasterPasswordAuthenticationHash, string? masterPasswordHint)\n    {\n        var protectedMasterKeyWrappedUserKey = string.Concat(Constants.DatabaseFieldProtectedPrefix,\n            _dataProtector.Protect(masterPasswordUnlockData.MasterKeyWrappedUserKey));\n\n        var protectedServerSideHashedMasterPasswordAuthenticationHash = string.Concat(\n            Constants.DatabaseFieldProtectedPrefix,\n            _dataProtector.Protect(serverSideHashedMasterPasswordAuthenticationHash));\n\n        return async (connection, transaction) =>\n        {\n            var timestamp = DateTime.UtcNow;\n\n            await connection!.ExecuteAsync(\n                \"[dbo].[User_UpdateMasterPassword]\",\n                new\n                {\n                    Id = userId,\n                    MasterPassword = protectedServerSideHashedMasterPasswordAuthenticationHash,\n                    MasterPasswordHint = masterPasswordHint,\n                    Key = protectedMasterKeyWrappedUserKey,\n                    Kdf = masterPasswordUnlockData.Kdf.KdfType,\n                    KdfIterations = masterPasswordUnlockData.Kdf.Iterations,\n                    KdfMemory = masterPasswordUnlockData.Kdf.Memory,\n                    KdfParallelism = masterPasswordUnlockData.Kdf.Parallelism,\n                    RevisionDate = timestamp,\n                    AccountRevisionDate = timestamp,\n                    MasterPasswordSalt = masterPasswordUnlockData.Salt\n                },\n                transaction: transaction,\n                commandType: CommandType.StoredProcedure);\n        };\n    }\n\n    public async Task UpdateUserDataAsync(IEnumerable<UpdateUserData> updateUserDataActions)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n        await connection.OpenAsync();\n\n        await using var transaction = connection.BeginTransaction();\n        try\n        {\n            foreach (var action in updateUserDataActions)\n            {\n                await action(connection, transaction);\n            }\n\n            await transaction.CommitAsync();\n        }\n        catch\n        {\n            await transaction.RollbackAsync();\n            throw;\n        }\n    }\n\n    private async Task ProtectDataAndSaveAsync(User user, Func<Task> saveTask)\n    {\n        if (user == null)\n        {\n            await saveTask();\n            return;\n        }\n\n        // Capture original values\n        var originalMasterPassword = user.MasterPassword;\n        var originalKey = user.Key;\n\n        // Protect values\n        ProtectData(user);\n\n        // Save\n        await saveTask();\n\n        // Restore original values\n        user.MasterPassword = originalMasterPassword;\n        user.Key = originalKey;\n    }\n\n    private void ProtectData(User user)\n    {\n        if (!user.MasterPassword?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)\n        {\n            user.MasterPassword = string.Concat(Constants.DatabaseFieldProtectedPrefix,\n                _dataProtector.Protect(user.MasterPassword!));\n        }\n\n        if (!user.Key?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)\n        {\n            user.Key = string.Concat(Constants.DatabaseFieldProtectedPrefix,\n                _dataProtector.Protect(user.Key!));\n        }\n    }\n\n    private void UnprotectData(User? user)\n    {\n        if (user == null)\n        {\n            return;\n        }\n\n        if (user.MasterPassword?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)\n        {\n            user.MasterPassword = _dataProtector.Unprotect(\n                user.MasterPassword.Substring(Constants.DatabaseFieldProtectedPrefix.Length));\n        }\n\n        if (user.Key?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)\n        {\n            user.Key = _dataProtector.Unprotect(\n                user.Key.Substring(Constants.DatabaseFieldProtectedPrefix.Length));\n        }\n    }\n\n    private void UnprotectData(IEnumerable<User> users)\n    {\n        if (users == null)\n        {\n            return;\n        }\n\n        foreach (var user in users)\n        {\n            UnprotectData(user);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/SecretsManager/Repositories/ApiKeyRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.SecretsManager.Repositories;\n\npublic class ApiKeyRepository : Repository<ApiKey, Guid>, IApiKeyRepository\n{\n    public ApiKeyRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public ApiKeyRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<ApiKeyDetails> GetDetailsByIdAsync(Guid id)\n    {\n        using var connection = new SqlConnection(ConnectionString);\n        // When adding different key details, we should change the QueryAsync type to match the database data,\n        //  but cast it to the appropriate data model.\n        var results = await connection.QueryAsync<ServiceAccountApiKeyDetails>(\n            $\"[{Schema}].[ApiKeyDetails_ReadById]\",\n            new { Id = id },\n            commandType: CommandType.StoredProcedure);\n\n        return results.SingleOrDefault();\n    }\n\n    public async Task<ICollection<ApiKey>> GetManyByServiceAccountIdAsync(Guid serviceAccountId)\n    {\n        using var connection = new SqlConnection(ConnectionString);\n        var results = await connection.QueryAsync<ApiKey>(\n            $\"[{Schema}].[ApiKey_ReadByServiceAccountId]\",\n            new { ServiceAccountId = serviceAccountId },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToList();\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<ApiKey> objs)\n    {\n        using var connection = new SqlConnection(ConnectionString);\n        await connection.QueryAsync<ApiKey>(\n            $\"[{Schema}].[ApiKey_DeleteByIds]\",\n            new { Ids = objs.Select(obj => obj.Id).ToGuidIdArrayTVP() },\n            commandType: CommandType.StoredProcedure);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Tools/Helpers/SendHelpers.cs",
    "content": "﻿using System.Data;\nusing Bit.Core.Tools.Entities;\n\nnamespace Bit.Infrastructure.Dapper.Tools.Helpers;\n\n/// <summary>\n/// Dapper helper methods for Sends\n/// </summary>\npublic static class SendHelpers\n{\n    private static readonly DataTableBuilder<Send> _sendTableBuilder = new(\n        [\n            s => s.Id,\n            s => s.UserId,\n            s => s.OrganizationId,\n            s => s.Type,\n            s => s.Data,\n            s => s.Key,\n            s => s.Password,\n            s => s.MaxAccessCount,\n            s => s.AccessCount,\n            s => s.CreationDate,\n            s => s.RevisionDate,\n            s => s.ExpirationDate,\n            s => s.DeletionDate,\n            s => s.Disabled,\n            s => s.HideEmail,\n        ]\n    );\n\n    /// <summary>\n    /// Converts an IEnumerable of Sends to a DataTable\n    /// </summary>\n    /// <remarks>Contains a hardcoded list of properties and must be updated with model</remarks>\n    /// <param name=\"sends\">List of sends</param>\n    /// <returns>A data table matching the schema of dbo.Send containing one row mapped from the items in <see cref=\"Send\"/>s</returns>\n    public static DataTable ToDataTable(this IEnumerable<Send> sends)\n    {\n        return _sendTableBuilder.Build(sends ?? []);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs",
    "content": "﻿#nullable enable\n\nusing System.Data;\nusing Bit.Core;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Bit.Infrastructure.Dapper.Tools.Helpers;\nusing Dapper;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Tools.Repositories;\n\n/// <inheritdoc cref=\"ISendRepository\" />\npublic class SendRepository : Repository<Send, Guid>, ISendRepository\n{\n    private readonly IDataProtector _dataProtector;\n\n    public SendRepository(GlobalSettings globalSettings, IDataProtectionProvider dataProtectionProvider)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString, dataProtectionProvider)\n    { }\n\n    public SendRepository(string connectionString, string readOnlyConnectionString, IDataProtectionProvider dataProtectionProvider)\n        : base(connectionString, readOnlyConnectionString)\n    {\n        _dataProtector = dataProtectionProvider.CreateProtector(Constants.DatabaseFieldProtectorPurpose);\n    }\n\n    public override async Task<Send?> GetByIdAsync(Guid id)\n    {\n        var send = await base.GetByIdAsync(id);\n        UnprotectData(send);\n        return send;\n    }\n\n    /// <inheritdoc />\n    public async Task<ICollection<Send>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Send>(\n                $\"[{Schema}].[Send_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            var sends = results.ToList();\n            UnprotectData(sends);\n            return sends;\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task<ICollection<Send>> GetManyByDeletionDateAsync(DateTime deletionDateBefore)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Send>(\n                $\"[{Schema}].[Send_ReadByDeletionDateBefore]\",\n                new { DeletionDate = deletionDateBefore },\n                commandType: CommandType.StoredProcedure);\n\n            var sends = results.ToList();\n            UnprotectData(sends);\n            return sends;\n        }\n    }\n\n    public override async Task<Send> CreateAsync(Send send)\n    {\n        await ProtectDataAndSaveAsync(send, async () => await base.CreateAsync(send));\n        return send;\n    }\n\n    public override async Task ReplaceAsync(Send send)\n    {\n        await ProtectDataAndSaveAsync(send, async () => await base.ReplaceAsync(send));\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, IEnumerable<Send> sends)\n    {\n        return async (connection, transaction) =>\n        {\n            // Protect all sends before bulk update\n            var sendsList = sends.ToList();\n            foreach (var send in sendsList)\n            {\n                ProtectData(send);\n            }\n\n            // Create temp table\n            var sqlCreateTemp = @\"\n                            SELECT TOP 0 *\n                            INTO #TempSend\n                            FROM [dbo].[Send]\";\n\n            await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))\n            {\n                cmd.ExecuteNonQuery();\n            }\n\n            // Bulk copy data into temp table\n            using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))\n            {\n                bulkCopy.DestinationTableName = \"#TempSend\";\n                var sendsTable = sendsList.ToDataTable();\n                foreach (DataColumn col in sendsTable.Columns)\n                {\n                    bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n                }\n\n                sendsTable.PrimaryKey = new DataColumn[] { sendsTable.Columns[0] };\n                await bulkCopy.WriteToServerAsync(sendsTable);\n            }\n\n            // Update send table from temp table\n            var sql = @\"\n                UPDATE\n                    [dbo].[Send]\n                SET\n                    [Key] = TS.[Key],\n                    [RevisionDate] = TS.[RevisionDate]\n                FROM\n                    [dbo].[Send] S\n                INNER JOIN\n                    #TempSend TS ON S.Id = TS.Id\n                WHERE\n                    S.[UserId] = @UserId\n                DROP TABLE #TempSend\";\n\n            await using (var cmd = new SqlCommand(sql, connection, transaction))\n            {\n                cmd.Parameters.Add(\"@UserId\", SqlDbType.UniqueIdentifier).Value = userId;\n                cmd.ExecuteNonQuery();\n            }\n\n            // Unprotect after save\n            foreach (var send in sendsList)\n            {\n                UnprotectData(send);\n            }\n        };\n    }\n\n    private async Task ProtectDataAndSaveAsync(Send send, Func<Task> saveTask)\n    {\n        if (send == null)\n        {\n            await saveTask();\n            return;\n        }\n\n        // Capture original value\n        var emails = send.Emails;\n\n        // Protect value\n        ProtectData(send);\n\n        // Save\n        await saveTask();\n\n        // Restore original value\n        send.Emails = emails;\n    }\n\n    private void ProtectData(Send send)\n    {\n        if (!send.Emails?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)\n        {\n            send.Emails = string.Concat(Constants.DatabaseFieldProtectedPrefix,\n                _dataProtector.Protect(send.Emails!));\n        }\n    }\n\n    private void UnprotectData(Send? send)\n    {\n        if (send == null)\n        {\n            return;\n        }\n\n        if (send.Emails?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)\n        {\n            send.Emails = _dataProtector.Unprotect(\n                send.Emails.Substring(Constants.DatabaseFieldProtectedPrefix.Length));\n        }\n    }\n\n    private void UnprotectData(IEnumerable<Send> sends)\n    {\n        if (sends == null)\n        {\n            return;\n        }\n\n        foreach (var send in sends)\n        {\n            UnprotectData(send);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Utilities/SqlGuidHelpers.cs",
    "content": "﻿using System.Data.SqlTypes;\n\nnamespace Bit.Infrastructure.Dapper.Utilities;\n\npublic static class SqlGuidHelpers\n{\n    /// <summary>\n    /// Sorts the source IEnumerable by the specified Guid property using the <see cref=\"SqlGuid\"/> comparison logic.\n    /// This is required because MSSQL server compares (and therefore sorts) Guids differently to C#.\n    /// Ref: https://learn.microsoft.com/en-us/sql/connect/ado-net/sql/compare-guid-uniqueidentifier-values\n    /// </summary>\n    public static IOrderedEnumerable<T> OrderBySqlGuid<T>(\n        this IEnumerable<T> source,\n        Func<T, Guid> keySelector)\n    {\n        return source.OrderBy(x => new SqlGuid(keySelector(x)));\n    }\n\n    /// <inheritdoc cref=\"OrderBySqlGuid\"/>\n    public static IOrderedEnumerable<T> ThenBySqlGuid<T>(\n        this IOrderedEnumerable<T> source,\n        Func<T, Guid> keySelector)\n    {\n        return source.ThenBy(x => new SqlGuid(keySelector(x)));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.Vault.Entities;\nusing Dapper;\n\nnamespace Bit.Infrastructure.Dapper.Vault.Helpers;\n\npublic static class CipherHelpers\n{\n    public static DataTable ToDataTable(this IEnumerable<Cipher> ciphers)\n    {\n        var ciphersTable = new DataTable();\n        ciphersTable.SetTypeName(\"[dbo].[Cipher]\");\n\n        var columnData = new List<(string name, Type type, Func<Cipher, object> getter)>\n        {\n            (nameof(Cipher.Id), typeof(Guid), c => c.Id),\n            (nameof(Cipher.UserId), typeof(Guid), c => c.UserId),\n            (nameof(Cipher.OrganizationId), typeof(Guid), c => c.OrganizationId),\n            (nameof(Cipher.Type), typeof(short), c => c.Type),\n            (nameof(Cipher.Data), typeof(string), c => c.Data),\n            (nameof(Cipher.Favorites), typeof(string), c => c.Favorites),\n            (nameof(Cipher.Folders), typeof(string), c => c.Folders),\n            (nameof(Cipher.Attachments), typeof(string), c => c.Attachments),\n            (nameof(Cipher.CreationDate), typeof(DateTime), c => c.CreationDate),\n            (nameof(Cipher.RevisionDate), typeof(DateTime), c => c.RevisionDate),\n            (nameof(Cipher.DeletedDate), typeof(DateTime), c => c.DeletedDate),\n            (nameof(Cipher.Reprompt), typeof(short), c => c.Reprompt),\n            (nameof(Cipher.Key), typeof(string), c => c.Key),\n        };\n\n        return ciphers.BuildTable(ciphersTable, columnData);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.Vault.Entities;\nusing Dapper;\n\nnamespace Bit.Infrastructure.Dapper.Vault.Helpers;\n\npublic static class FolderHelpers\n{\n    public static DataTable ToDataTable(this IEnumerable<Folder> folders)\n    {\n        var foldersTable = new DataTable();\n        foldersTable.SetTypeName(\"[dbo].[Folder]\");\n\n        var columnData = new List<(string name, Type type, Func<Folder, object> getter)>\n        {\n            (nameof(Folder.Id), typeof(Guid), c => c.Id),\n            (nameof(Folder.UserId), typeof(Guid), c => c.UserId),\n            (nameof(Folder.Name), typeof(string), c => c.Name),\n            (nameof(Folder.CreationDate), typeof(DateTime), c => c.CreationDate),\n            (nameof(Folder.RevisionDate), typeof(DateTime), c => c.RevisionDate),\n        };\n\n        return folders.BuildTable(foldersTable, columnData);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing System.Text.Json;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Infrastructure.Dapper.AdminConsole.Helpers;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Vault.Repositories;\n\npublic class CipherRepository : Repository<Cipher, Guid>, ICipherRepository\n{\n    public CipherRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public CipherRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<CipherDetails> GetByIdAsync(Guid id, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CipherDetails>(\n                $\"[{Schema}].[CipherDetails_ReadByIdUserId]\",\n                new { Id = id, UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<CipherOrganizationDetails> GetOrganizationDetailsByIdAsync(Guid id)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CipherDetails>(\n                $\"[{Schema}].[CipherOrganizationDetails_ReadById]\",\n                new { Id = id },\n                commandType: CommandType.StoredProcedure);\n\n            return results.FirstOrDefault();\n        }\n    }\n\n    public async Task<ICollection<CipherOrganizationDetails>> GetManyOrganizationDetailsByOrganizationIdAsync(\n        Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CipherOrganizationDetails>(\n                $\"[{Schema}].[CipherOrganizationDetails_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<bool> GetCanEditByIdAsync(Guid userId, Guid cipherId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.QueryFirstOrDefaultAsync<bool>(\n                $\"[{Schema}].[Cipher_ReadCanEditByIdUserId]\",\n                new { UserId = userId, Id = cipherId },\n                commandType: CommandType.StoredProcedure);\n\n            return result;\n        }\n    }\n\n    public async Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true)\n    {\n        string sprocName = null;\n        if (withOrganizations)\n        {\n            sprocName = $\"[{Schema}].[CipherDetails_ReadByUserId]\";\n        }\n        else\n        {\n            sprocName = $\"[{Schema}].[CipherDetails_ReadWithoutOrganizationsByUserId]\";\n        }\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CipherDetails>(\n                sprocName,\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results\n                .GroupBy(c => c.Id)\n                .Select(g =>\n                    g.OrderByDescending(og => og.Manage)\n                        .ThenByDescending(og => og.Edit)\n                        .ThenByDescending(og => og.ViewPassword).First())\n                .ToList();\n        }\n    }\n\n    public async Task<ICollection<Cipher>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Cipher>(\n                $\"[{Schema}].[Cipher_ReadByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<CipherOrganizationDetails>> GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<CipherOrganizationDetails>(\n                $\"[{Schema}].[CipherOrganizationDetails_ReadUnassignedByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task CreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)\n    {\n        cipher.SetNewId();\n        var objWithCollections = JsonSerializer.Deserialize<CipherWithCollections>(\n            JsonSerializer.Serialize(cipher));\n        objWithCollections.CollectionIds = collectionIds.ToGuidIdArrayTVP();\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_CreateWithCollections]\",\n                objWithCollections,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task CreateAsync(CipherDetails cipher)\n    {\n        cipher.SetNewId();\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[CipherDetails_Create]\",\n                cipher,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task CreateAsync(CipherDetails cipher, IEnumerable<Guid> collectionIds)\n    {\n        cipher.SetNewId();\n        var objWithCollections = JsonSerializer.Deserialize<CipherDetailsWithCollections>(\n            JsonSerializer.Serialize(cipher));\n        objWithCollections.CollectionIds = collectionIds.ToGuidIdArrayTVP();\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[CipherDetails_CreateWithCollections]\",\n                objWithCollections,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task ReplaceAsync(CipherDetails obj)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[CipherDetails_Update]\",\n                obj,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task UpsertAsync(CipherDetails cipher)\n    {\n        if (cipher.Id.Equals(default))\n        {\n            await CreateAsync(cipher);\n        }\n        else\n        {\n            await ReplaceAsync(cipher);\n        }\n    }\n\n    public async Task<bool> ReplaceAsync(Cipher obj, IEnumerable<Guid> collectionIds)\n    {\n        var objWithCollections = JsonSerializer.Deserialize<CipherWithCollections>(\n            JsonSerializer.Serialize(obj));\n        objWithCollections.CollectionIds = collectionIds.ToGuidIdArrayTVP();\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var result = await connection.ExecuteScalarAsync<int>(\n                $\"[{Schema}].[Cipher_UpdateWithCollections]\",\n                objWithCollections,\n                commandType: CommandType.StoredProcedure);\n            return result >= 0;\n        }\n    }\n\n    public async Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_UpdatePartial]\",\n                new { Id = id, UserId = userId, FolderId = folderId, Favorite = favorite },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task UpdateAttachmentAsync(CipherAttachment attachment)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_UpdateAttachment]\",\n                attachment,\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<DateTime> ArchiveAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<DateTime>(\n                $\"[{Schema}].[Cipher_Archive]\",\n                new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return DateTime.SpecifyKind(results, DateTimeKind.Utc);\n        }\n    }\n\n    public async Task DeleteAttachmentAsync(Guid cipherId, string attachmentId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_DeleteAttachment]\",\n                new { Id = cipherId, AttachmentId = attachmentId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task DeleteAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_Delete]\",\n                new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task DeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_DeleteByIdsOrganizationId]\",\n                new { Ids = ids.ToGuidIdArrayTVP(), OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_SoftDeleteByIdsOrganizationId]\",\n                new { Ids = ids.ToGuidIdArrayTVP(), OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_Move]\",\n                new { Ids = ids.ToGuidIdArrayTVP(), FolderId = folderId, UserId = userId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task DeleteByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_DeleteByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task DeleteByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_DeleteByOrganizationId]\",\n                new { OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<ICollection<OrganizationCipherPermission>> GetCipherPermissionsForOrganizationAsync(\n        Guid organizationId, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<OrganizationCipherPermission>(\n                $\"[{Schema}].[CipherOrganizationPermissions_GetManyByOrganizationId]\",\n                new { OrganizationId = organizationId, UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    public async Task<ICollection<UserSecurityTaskCipher>> GetUserSecurityTasksByCipherIdsAsync(\n        Guid organizationId, IEnumerable<SecurityTask> tasks)\n    {\n        var cipherIds = tasks.Where(t => t.CipherId.HasValue).Select(t => t.CipherId.Value).Distinct().ToList();\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n\n            var results = await connection.QueryAsync<UserCipherForTask>(\n                $\"[{Schema}].[UserSecurityTasks_GetManyByCipherIds]\",\n                new { OrganizationId = organizationId, CipherIds = cipherIds.ToGuidIdArrayTVP() },\n                commandType: CommandType.StoredProcedure);\n\n            return results.Select(r => new UserSecurityTaskCipher\n            {\n                UserId = r.UserId,\n                Email = r.Email,\n                CipherId = r.CipherId,\n                TaskId = tasks.First(t => t.CipherId == r.CipherId).Id\n            }).ToList();\n        }\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(\n        Guid userId, IEnumerable<Cipher> ciphers)\n    {\n        return async (SqlConnection connection, SqlTransaction transaction) =>\n        {\n            // Create temp table\n            var sqlCreateTemp = @\"\n                            SELECT TOP 0 *\n                            INTO #TempCipher\n                            FROM [dbo].[Cipher]\";\n\n            await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))\n            {\n                cmd.ExecuteNonQuery();\n            }\n\n            // Bulk copy data into temp table\n            await BulkResourceCreationService.CreateTempCiphersAsync(connection, transaction, ciphers);\n\n            // Update cipher table from temp table\n            var sql = @\"\n                    UPDATE\n                        [dbo].[Cipher]\n                    SET\n                        [Data] = TC.[Data],\n                        [Attachments] = TC.[Attachments],\n                        [RevisionDate] = TC.[RevisionDate],\n                        [Key] = TC.[Key]\n                    FROM\n                        [dbo].[Cipher] C\n                    INNER JOIN\n                        #TempCipher TC ON C.Id = TC.Id\n                    WHERE\n                        C.[UserId] = @UserId\n\n                    DROP TABLE #TempCipher\";\n\n            await using (var cmd = new SqlCommand(sql, connection, transaction))\n            {\n                cmd.Parameters.Add(\"@UserId\", SqlDbType.UniqueIdentifier).Value = userId;\n                cmd.ExecuteNonQuery();\n            }\n        };\n    }\n\n    public async Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers)\n    {\n        if (!ciphers.Any())\n        {\n            return;\n        }\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            connection.Open();\n\n            using (var transaction = connection.BeginTransaction())\n            {\n                try\n                {\n                    // 1. Create temp tables to bulk copy into.\n\n                    var sqlCreateTemp = @\"\n                            SELECT TOP 0 *\n                            INTO #TempCipher\n                            FROM [dbo].[Cipher]\";\n\n                    using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))\n                    {\n                        cmd.ExecuteNonQuery();\n                    }\n\n                    // 2. Bulk copy into temp tables.\n                    await BulkResourceCreationService.CreateTempCiphersAsync(connection, transaction, ciphers);\n\n                    // 3. Insert into real tables from temp tables and clean up.\n\n                    // Intentionally not including Favorites, Folders, and CreationDate\n                    // since those are not meant to be bulk updated at this time\n                    var sql = @\"\n                            UPDATE\n                                [dbo].[Cipher]\n                            SET\n                                [UserId] = TC.[UserId],\n                                [OrganizationId] = TC.[OrganizationId],\n                                [Type] = TC.[Type],\n                                [Data] = TC.[Data],\n                                [Attachments] = TC.[Attachments],\n                                [RevisionDate] = TC.[RevisionDate],\n                                [DeletedDate] = TC.[DeletedDate],\n                                [Key] = TC.[Key]\n                            FROM\n                                [dbo].[Cipher] C\n                            INNER JOIN\n                                #TempCipher TC ON C.Id = TC.Id\n                            WHERE\n                                C.[UserId] = @UserId\n\n                            DROP TABLE #TempCipher\";\n\n                    using (var cmd = new SqlCommand(sql, connection, transaction))\n                    {\n                        cmd.Parameters.Add(\"@UserId\", SqlDbType.UniqueIdentifier).Value = userId;\n                        cmd.ExecuteNonQuery();\n                    }\n\n                    await connection.ExecuteAsync(\n                        $\"[{Schema}].[User_BumpAccountRevisionDate]\",\n                        new { Id = userId },\n                        commandType: CommandType.StoredProcedure, transaction: transaction);\n\n                    transaction.Commit();\n                }\n                catch\n                {\n                    transaction.Rollback();\n                    throw;\n                }\n            }\n        }\n    }\n\n    public async Task CreateAsync(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders)\n    {\n        if (!ciphers.Any())\n        {\n            return;\n        }\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            connection.Open();\n\n            using (var transaction = connection.BeginTransaction())\n            {\n                try\n                {\n                    if (folders.Any())\n                    {\n                        await BulkResourceCreationService.CreateFoldersAsync(connection, transaction, folders);\n                    }\n\n                    await BulkResourceCreationService.CreateCiphersAsync(connection, transaction, ciphers);\n\n                    await connection.ExecuteAsync(\n                            $\"[{Schema}].[User_BumpAccountRevisionDate]\",\n                            new { Id = userId },\n                            commandType: CommandType.StoredProcedure, transaction: transaction);\n\n                    transaction.Commit();\n                }\n                catch\n                {\n                    transaction.Rollback();\n                    throw;\n                }\n            }\n        }\n    }\n\n    public async Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,\n        IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers)\n    {\n        if (!ciphers.Any())\n        {\n            return;\n        }\n\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            connection.Open();\n\n            using (var transaction = connection.BeginTransaction())\n            {\n                try\n                {\n                    await BulkResourceCreationService.CreateCiphersAsync(connection, transaction, ciphers);\n\n                    if (collections.Any())\n                    {\n                        await BulkResourceCreationService.CreateCollectionsAsync(connection, transaction, collections);\n                    }\n\n                    if (collectionCiphers.Any())\n                    {\n                        await BulkResourceCreationService.CreateCollectionCiphersAsync(connection, transaction, collectionCiphers);\n                    }\n\n                    if (collectionUsers.Any())\n                    {\n                        await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers);\n                    }\n\n                    await connection.ExecuteAsync(\n                            $\"[{Schema}].[User_BumpAccountRevisionDateByOrganizationId]\",\n                            new { OrganizationId = ciphers.First().OrganizationId },\n                            commandType: CommandType.StoredProcedure, transaction: transaction);\n\n                    transaction.Commit();\n                }\n                catch\n                {\n                    transaction.Rollback();\n                    throw;\n                }\n            }\n        }\n    }\n\n    public async Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_SoftDelete]\",\n                new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },\n                commandType: CommandType.StoredProcedure);\n        }\n    }\n\n    public async Task<DateTime> UnarchiveAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<DateTime>(\n                $\"[{Schema}].[Cipher_Unarchive]\",\n                new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return DateTime.SpecifyKind(results, DateTimeKind.Utc);\n        }\n    }\n\n    public async Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<DateTime>(\n                $\"[{Schema}].[Cipher_Restore]\",\n                new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return DateTime.SpecifyKind(results, DateTimeKind.Utc);\n        }\n    }\n\n    public async Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.ExecuteScalarAsync<DateTime>(\n                $\"[{Schema}].[Cipher_RestoreByIdsOrganizationId]\",\n                new { Ids = ids.ToGuidIdArrayTVP(), OrganizationId = organizationId },\n                commandType: CommandType.StoredProcedure);\n\n            return DateTime.SpecifyKind(results, DateTimeKind.Utc);\n        }\n    }\n\n    public async Task DeleteDeletedAsync(DateTime deletedDateBefore)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            await connection.ExecuteAsync(\n                $\"[{Schema}].[Cipher_DeleteDeleted]\",\n                new { DeletedDateBefore = deletedDateBefore },\n                commandType: CommandType.StoredProcedure,\n                commandTimeout: 43200);\n        }\n    }\n\n    public async Task<IEnumerable<CipherOrganizationDetailsWithCollections>>\n        GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(Guid orgId)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        var dict = new Dictionary<Guid, CipherOrganizationDetailsWithCollections>();\n        var tempCollections = new Dictionary<Guid, List<Guid>>();\n\n        await connection.QueryAsync<\n            CipherOrganizationDetails,\n            CollectionCipher,\n            CipherOrganizationDetailsWithCollections\n        >(\n            $\"[{Schema}].[CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections]\",\n            (cipher, cc) =>\n            {\n                if (!dict.TryGetValue(cipher.Id, out var details))\n                {\n                    details = new CipherOrganizationDetailsWithCollections(cipher, new Dictionary<Guid, IGrouping<Guid, CollectionCipher>>());\n                    dict.Add(cipher.Id, details);\n                    tempCollections[cipher.Id] = new List<Guid>();\n                }\n\n                if (cc?.CollectionId != null)\n                {\n                    tempCollections[cipher.Id].AddIfNotExists(cc.CollectionId);\n                }\n\n                return details;\n            },\n            new { OrganizationId = orgId },\n            splitOn: \"CollectionId\",\n            commandType: CommandType.StoredProcedure\n        );\n\n        foreach (var kv in dict)\n        {\n            kv.Value.CollectionIds = tempCollections[kv.Key].ToArray();\n        }\n\n        return dict.Values.ToList();\n    }\n\n\n    private DataTable BuildCiphersTable(SqlBulkCopy bulkCopy, IEnumerable<Cipher> ciphers)\n    {\n        var c = ciphers.FirstOrDefault();\n        if (c == null)\n        {\n            throw new ApplicationException(\"Must have some ciphers to bulk import.\");\n        }\n\n        var ciphersTable = new DataTable(\"CipherDataTable\");\n\n        var idColumn = new DataColumn(nameof(c.Id), c.Id.GetType());\n        ciphersTable.Columns.Add(idColumn);\n        var userIdColumn = new DataColumn(nameof(c.UserId), typeof(Guid));\n        ciphersTable.Columns.Add(userIdColumn);\n        var organizationId = new DataColumn(nameof(c.OrganizationId), typeof(Guid));\n        ciphersTable.Columns.Add(organizationId);\n        var typeColumn = new DataColumn(nameof(c.Type), typeof(short));\n        ciphersTable.Columns.Add(typeColumn);\n        var dataColumn = new DataColumn(nameof(c.Data), typeof(string));\n        ciphersTable.Columns.Add(dataColumn);\n        var favoritesColumn = new DataColumn(nameof(c.Favorites), typeof(string));\n        ciphersTable.Columns.Add(favoritesColumn);\n        var foldersColumn = new DataColumn(nameof(c.Folders), typeof(string));\n        ciphersTable.Columns.Add(foldersColumn);\n        var attachmentsColumn = new DataColumn(nameof(c.Attachments), typeof(string));\n        ciphersTable.Columns.Add(attachmentsColumn);\n        var creationDateColumn = new DataColumn(nameof(c.CreationDate), c.CreationDate.GetType());\n        ciphersTable.Columns.Add(creationDateColumn);\n        var revisionDateColumn = new DataColumn(nameof(c.RevisionDate), c.RevisionDate.GetType());\n        ciphersTable.Columns.Add(revisionDateColumn);\n        var deletedDateColumn = new DataColumn(nameof(c.DeletedDate), typeof(DateTime));\n        ciphersTable.Columns.Add(deletedDateColumn);\n        var repromptColumn = new DataColumn(nameof(c.Reprompt), typeof(short));\n        ciphersTable.Columns.Add(repromptColumn);\n        var keyColummn = new DataColumn(nameof(c.Key), typeof(string));\n        ciphersTable.Columns.Add(keyColummn);\n\n        foreach (DataColumn col in ciphersTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[1];\n        keys[0] = idColumn;\n        ciphersTable.PrimaryKey = keys;\n\n        foreach (var cipher in ciphers)\n        {\n            var row = ciphersTable.NewRow();\n\n            row[idColumn] = cipher.Id;\n            row[userIdColumn] = cipher.UserId.HasValue ? (object)cipher.UserId.Value : DBNull.Value;\n            row[organizationId] = cipher.OrganizationId.HasValue ? (object)cipher.OrganizationId.Value : DBNull.Value;\n            row[typeColumn] = (short)cipher.Type;\n            row[dataColumn] = cipher.Data;\n            row[favoritesColumn] = cipher.Favorites;\n            row[foldersColumn] = cipher.Folders;\n            row[attachmentsColumn] = cipher.Attachments;\n            row[creationDateColumn] = cipher.CreationDate;\n            row[revisionDateColumn] = cipher.RevisionDate;\n            row[deletedDateColumn] = cipher.DeletedDate.HasValue ? (object)cipher.DeletedDate : DBNull.Value;\n            row[repromptColumn] = cipher.Reprompt.HasValue ? cipher.Reprompt.Value : DBNull.Value;\n            row[keyColummn] = cipher.Key;\n\n            ciphersTable.Rows.Add(row);\n        }\n\n        return ciphersTable;\n    }\n\n    private DataTable BuildFoldersTable(SqlBulkCopy bulkCopy, IEnumerable<Folder> folders)\n    {\n        var f = folders.FirstOrDefault();\n        if (f == null)\n        {\n            throw new ApplicationException(\"Must have some folders to bulk import.\");\n        }\n\n        var foldersTable = new DataTable(\"FolderDataTable\");\n\n        var idColumn = new DataColumn(nameof(f.Id), f.Id.GetType());\n        foldersTable.Columns.Add(idColumn);\n        var userIdColumn = new DataColumn(nameof(f.UserId), f.UserId.GetType());\n        foldersTable.Columns.Add(userIdColumn);\n        var nameColumn = new DataColumn(nameof(f.Name), typeof(string));\n        foldersTable.Columns.Add(nameColumn);\n        var creationDateColumn = new DataColumn(nameof(f.CreationDate), f.CreationDate.GetType());\n        foldersTable.Columns.Add(creationDateColumn);\n        var revisionDateColumn = new DataColumn(nameof(f.RevisionDate), f.RevisionDate.GetType());\n        foldersTable.Columns.Add(revisionDateColumn);\n\n        foreach (DataColumn col in foldersTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[1];\n        keys[0] = idColumn;\n        foldersTable.PrimaryKey = keys;\n\n        foreach (var folder in folders)\n        {\n            var row = foldersTable.NewRow();\n\n            row[idColumn] = folder.Id;\n            row[userIdColumn] = folder.UserId;\n            row[nameColumn] = folder.Name;\n            row[creationDateColumn] = folder.CreationDate;\n            row[revisionDateColumn] = folder.RevisionDate;\n\n            foldersTable.Rows.Add(row);\n        }\n\n        return foldersTable;\n    }\n\n    private DataTable BuildCollectionsTable(SqlBulkCopy bulkCopy, IEnumerable<Collection> collections)\n    {\n        var c = collections.FirstOrDefault();\n        if (c == null)\n        {\n            throw new ApplicationException(\"Must have some collections to bulk import.\");\n        }\n\n        var collectionsTable = new DataTable(\"CollectionDataTable\");\n\n        var idColumn = new DataColumn(nameof(c.Id), c.Id.GetType());\n        collectionsTable.Columns.Add(idColumn);\n        var organizationIdColumn = new DataColumn(nameof(c.OrganizationId), c.OrganizationId.GetType());\n        collectionsTable.Columns.Add(organizationIdColumn);\n        var nameColumn = new DataColumn(nameof(c.Name), typeof(string));\n        collectionsTable.Columns.Add(nameColumn);\n        var creationDateColumn = new DataColumn(nameof(c.CreationDate), c.CreationDate.GetType());\n        collectionsTable.Columns.Add(creationDateColumn);\n        var revisionDateColumn = new DataColumn(nameof(c.RevisionDate), c.RevisionDate.GetType());\n        collectionsTable.Columns.Add(revisionDateColumn);\n        var externalIdColumn = new DataColumn(nameof(c.ExternalId), typeof(string));\n        collectionsTable.Columns.Add(externalIdColumn);\n\n        foreach (DataColumn col in collectionsTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[1];\n        keys[0] = idColumn;\n        collectionsTable.PrimaryKey = keys;\n\n        foreach (var collection in collections)\n        {\n            var row = collectionsTable.NewRow();\n\n            row[idColumn] = collection.Id;\n            row[organizationIdColumn] = collection.OrganizationId;\n            row[nameColumn] = collection.Name;\n            row[creationDateColumn] = collection.CreationDate;\n            row[revisionDateColumn] = collection.RevisionDate;\n            row[externalIdColumn] = collection.ExternalId;\n\n            collectionsTable.Rows.Add(row);\n        }\n\n        return collectionsTable;\n    }\n\n    private DataTable BuildCollectionCiphersTable(SqlBulkCopy bulkCopy, IEnumerable<CollectionCipher> collectionCiphers)\n    {\n        var cc = collectionCiphers.FirstOrDefault();\n        if (cc == null)\n        {\n            throw new ApplicationException(\"Must have some collectionCiphers to bulk import.\");\n        }\n\n        var collectionCiphersTable = new DataTable(\"CollectionCipherDataTable\");\n\n        var collectionIdColumn = new DataColumn(nameof(cc.CollectionId), cc.CollectionId.GetType());\n        collectionCiphersTable.Columns.Add(collectionIdColumn);\n        var cipherIdColumn = new DataColumn(nameof(cc.CipherId), cc.CipherId.GetType());\n        collectionCiphersTable.Columns.Add(cipherIdColumn);\n\n        foreach (DataColumn col in collectionCiphersTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[2];\n        keys[0] = collectionIdColumn;\n        keys[1] = cipherIdColumn;\n        collectionCiphersTable.PrimaryKey = keys;\n\n        foreach (var collectionCipher in collectionCiphers)\n        {\n            var row = collectionCiphersTable.NewRow();\n\n            row[collectionIdColumn] = collectionCipher.CollectionId;\n            row[cipherIdColumn] = collectionCipher.CipherId;\n\n            collectionCiphersTable.Rows.Add(row);\n        }\n\n        return collectionCiphersTable;\n    }\n\n    private DataTable BuildCollectionUsersTable(SqlBulkCopy bulkCopy, IEnumerable<CollectionUser> collectionUsers)\n    {\n        var cu = collectionUsers.FirstOrDefault();\n        if (cu == null)\n        {\n            throw new ApplicationException(\"Must have some collectionUsers to bulk import.\");\n        }\n\n        var collectionUsersTable = new DataTable(\"CollectionUserDataTable\");\n\n        var collectionIdColumn = new DataColumn(nameof(cu.CollectionId), cu.CollectionId.GetType());\n        collectionUsersTable.Columns.Add(collectionIdColumn);\n        var organizationUserIdColumn = new DataColumn(nameof(cu.OrganizationUserId), cu.OrganizationUserId.GetType());\n        collectionUsersTable.Columns.Add(organizationUserIdColumn);\n        var readOnlyColumn = new DataColumn(nameof(cu.ReadOnly), cu.ReadOnly.GetType());\n        collectionUsersTable.Columns.Add(readOnlyColumn);\n        var hidePasswordsColumn = new DataColumn(nameof(cu.HidePasswords), cu.HidePasswords.GetType());\n        collectionUsersTable.Columns.Add(hidePasswordsColumn);\n        var manageColumn = new DataColumn(nameof(cu.Manage), cu.Manage.GetType());\n        collectionUsersTable.Columns.Add(manageColumn);\n\n        foreach (DataColumn col in collectionUsersTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[2];\n        keys[0] = collectionIdColumn;\n        keys[1] = organizationUserIdColumn;\n        collectionUsersTable.PrimaryKey = keys;\n\n        foreach (var collectionUser in collectionUsers)\n        {\n            var row = collectionUsersTable.NewRow();\n\n            row[collectionIdColumn] = collectionUser.CollectionId;\n            row[organizationUserIdColumn] = collectionUser.OrganizationUserId;\n            row[readOnlyColumn] = collectionUser.ReadOnly;\n            row[hidePasswordsColumn] = collectionUser.HidePasswords;\n            row[manageColumn] = collectionUser.Manage;\n\n            collectionUsersTable.Rows.Add(row);\n        }\n\n        return collectionUsersTable;\n    }\n\n    private DataTable BuildSendsTable(SqlBulkCopy bulkCopy, IEnumerable<Send> sends)\n    {\n        var s = sends.FirstOrDefault();\n        if (s == null)\n        {\n            throw new ApplicationException(\"Must have some Sends to bulk import.\");\n        }\n\n        var sendsTable = new DataTable(\"SendsDataTable\");\n\n        var idColumn = new DataColumn(nameof(s.Id), s.Id.GetType());\n        sendsTable.Columns.Add(idColumn);\n        var userIdColumn = new DataColumn(nameof(s.UserId), typeof(Guid));\n        sendsTable.Columns.Add(userIdColumn);\n        var organizationIdColumn = new DataColumn(nameof(s.OrganizationId), typeof(Guid));\n        sendsTable.Columns.Add(organizationIdColumn);\n        var typeColumn = new DataColumn(nameof(s.Type), s.Type.GetType());\n        sendsTable.Columns.Add(typeColumn);\n        var dataColumn = new DataColumn(nameof(s.Data), s.Data.GetType());\n        sendsTable.Columns.Add(dataColumn);\n        var keyColumn = new DataColumn(nameof(s.Key), s.Key.GetType());\n        sendsTable.Columns.Add(keyColumn);\n        var passwordColumn = new DataColumn(nameof(s.Password), typeof(string));\n        sendsTable.Columns.Add(passwordColumn);\n        var maxAccessCountColumn = new DataColumn(nameof(s.MaxAccessCount), typeof(int));\n        sendsTable.Columns.Add(maxAccessCountColumn);\n        var accessCountColumn = new DataColumn(nameof(s.AccessCount), s.AccessCount.GetType());\n        sendsTable.Columns.Add(accessCountColumn);\n        var creationDateColumn = new DataColumn(nameof(s.CreationDate), s.CreationDate.GetType());\n        sendsTable.Columns.Add(creationDateColumn);\n        var revisionDateColumn = new DataColumn(nameof(s.RevisionDate), s.RevisionDate.GetType());\n        sendsTable.Columns.Add(revisionDateColumn);\n        var expirationDateColumn = new DataColumn(nameof(s.ExpirationDate), typeof(DateTime));\n        sendsTable.Columns.Add(expirationDateColumn);\n        var deletionDateColumn = new DataColumn(nameof(s.DeletionDate), s.DeletionDate.GetType());\n        sendsTable.Columns.Add(deletionDateColumn);\n        var disabledColumn = new DataColumn(nameof(s.Disabled), s.Disabled.GetType());\n        sendsTable.Columns.Add(disabledColumn);\n        var hideEmailColumn = new DataColumn(nameof(s.HideEmail), typeof(bool));\n        sendsTable.Columns.Add(hideEmailColumn);\n\n        foreach (DataColumn col in sendsTable.Columns)\n        {\n            bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n        }\n\n        var keys = new DataColumn[1];\n        keys[0] = idColumn;\n        sendsTable.PrimaryKey = keys;\n\n        foreach (var send in sends)\n        {\n            var row = sendsTable.NewRow();\n\n            row[idColumn] = send.Id;\n            row[userIdColumn] = send.UserId.HasValue ? (object)send.UserId.Value : DBNull.Value;\n            row[organizationIdColumn] = send.OrganizationId.HasValue ? (object)send.OrganizationId.Value : DBNull.Value;\n            row[typeColumn] = (short)send.Type;\n            row[dataColumn] = send.Data;\n            row[keyColumn] = send.Key;\n            row[passwordColumn] = send.Password;\n            row[maxAccessCountColumn] = send.MaxAccessCount.HasValue ? (object)send.MaxAccessCount : DBNull.Value;\n            row[accessCountColumn] = send.AccessCount;\n            row[creationDateColumn] = send.CreationDate;\n            row[revisionDateColumn] = send.RevisionDate;\n            row[expirationDateColumn] = send.ExpirationDate.HasValue ? (object)send.ExpirationDate : DBNull.Value;\n            row[deletionDateColumn] = send.DeletionDate;\n            row[disabledColumn] = send.Disabled;\n            row[hideEmailColumn] = send.HideEmail.HasValue ? (object)send.HideEmail : DBNull.Value;\n\n            sendsTable.Rows.Add(row);\n        }\n\n        return sendsTable;\n    }\n\n    public class CipherDetailsWithCollections : CipherDetails\n    {\n        public DataTable CollectionIds { get; set; }\n    }\n\n    public class CipherWithCollections : Cipher\n    {\n        public DataTable CollectionIds { get; set; }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Bit.Infrastructure.Dapper.Vault.Helpers;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Vault.Repositories;\n\npublic class FolderRepository : Repository<Folder, Guid>, IFolderRepository\n{\n    public FolderRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public FolderRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    public async Task<Folder> GetByIdAsync(Guid id, Guid userId)\n    {\n        var folder = await GetByIdAsync(id);\n        if (folder == null || folder.UserId != userId)\n        {\n            return null;\n        }\n\n        return folder;\n    }\n\n    public async Task<ICollection<Folder>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var connection = new SqlConnection(ConnectionString))\n        {\n            var results = await connection.QueryAsync<Folder>(\n                $\"[{Schema}].[Folder_ReadByUserId]\",\n                new { UserId = userId },\n                commandType: CommandType.StoredProcedure);\n\n            return results.ToList();\n        }\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(\n        Guid userId, IEnumerable<Folder> folders)\n    {\n        return async (SqlConnection connection, SqlTransaction transaction) =>\n        {\n            // Create temp table\n            var sqlCreateTemp = @\"\n                            SELECT TOP 0 *\n                            INTO #TempFolder\n                            FROM [dbo].[Folder]\";\n\n            await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction))\n            {\n                cmd.ExecuteNonQuery();\n            }\n\n            // Bulk copy data into temp table\n            using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))\n            {\n                bulkCopy.DestinationTableName = \"#TempFolder\";\n                var foldersTable = folders.ToDataTable();\n                foreach (DataColumn col in foldersTable.Columns)\n                {\n                    bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);\n                }\n\n                foldersTable.PrimaryKey = new DataColumn[] { foldersTable.Columns[0] };\n                await bulkCopy.WriteToServerAsync(foldersTable);\n            }\n\n            // Update folder table from temp table\n            var sql = @\"\n                    UPDATE\n                        [dbo].[Folder]\n                    SET\n                        [Name] = TF.[Name],\n                        [RevisionDate] = TF.[RevisionDate]\n                    FROM\n                        [dbo].[Folder] F\n                    INNER JOIN\n                        #TempFolder TF ON F.Id = TF.Id\n                    WHERE\n                        F.[UserId] = @UserId;\n\n                    DROP TABLE #TempFolder\";\n\n            await using (var cmd = new SqlCommand(sql, connection, transaction))\n            {\n                cmd.Parameters.Add(\"@UserId\", SqlDbType.UniqueIdentifier).Value = userId;\n                cmd.ExecuteNonQuery();\n            }\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs",
    "content": "﻿using System.Data;\nusing System.Text.Json;\nusing Bit.Core.Settings;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Dapper;\nusing Microsoft.Data.SqlClient;\n\nnamespace Bit.Infrastructure.Dapper.Vault.Repositories;\n\npublic class SecurityTaskRepository : Repository<SecurityTask, Guid>, ISecurityTaskRepository\n{\n    public SecurityTaskRepository(GlobalSettings globalSettings)\n        : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)\n    { }\n\n    public SecurityTaskRepository(string connectionString, string readOnlyConnectionString)\n        : base(connectionString, readOnlyConnectionString)\n    { }\n\n    /// <inheritdoc />\n    public async Task<ICollection<SecurityTask>> GetManyByUserIdStatusAsync(Guid userId,\n        SecurityTaskStatus? status = null)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        var results = await connection.QueryAsync<SecurityTask>(\n            $\"[{Schema}].[SecurityTask_ReadByUserIdStatus]\",\n            new { UserId = userId, Status = status },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToList();\n    }\n\n    /// <inheritdoc />\n    public async Task<ICollection<SecurityTask>> GetManyByOrganizationIdStatusAsync(Guid organizationId,\n        SecurityTaskStatus? status = null)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        var results = await connection.QueryAsync<SecurityTask>(\n            $\"[{Schema}].[SecurityTask_ReadByOrganizationIdStatus]\",\n            new { OrganizationId = organizationId, Status = status },\n            commandType: CommandType.StoredProcedure);\n\n        return results.ToList();\n    }\n\n    /// <inheritdoc />\n    public async Task<SecurityTaskMetrics> GetTaskMetricsAsync(Guid organizationId)\n    {\n        await using var connection = new SqlConnection(ConnectionString);\n\n        var result = await connection.QueryAsync<SecurityTaskMetrics>(\n            $\"[{Schema}].[SecurityTask_ReadMetricsByOrganizationId]\",\n            new { OrganizationId = organizationId },\n            commandType: CommandType.StoredProcedure);\n\n        return result.FirstOrDefault() ?? new SecurityTaskMetrics(0, 0);\n    }\n\n    /// <inheritdoc />\n    public async Task<ICollection<SecurityTask>> CreateManyAsync(IEnumerable<SecurityTask> tasks)\n    {\n        var tasksList = tasks?.ToList();\n        if (tasksList is null || tasksList.Count == 0)\n        {\n            return Array.Empty<SecurityTask>();\n        }\n\n        foreach (var task in tasksList)\n        {\n            task.SetNewId();\n        }\n\n        var tasksJson = JsonSerializer.Serialize(tasksList);\n\n        await using var connection = new SqlConnection(ConnectionString);\n        await connection.ExecuteAsync(\n            $\"[{Schema}].[{Table}_CreateMany]\",\n            new { SecurityTasksJson = tasksJson },\n            commandType: CommandType.StoredProcedure);\n\n        return tasksList;\n    }\n\n    /// <inheritdoc />\n    public async Task MarkAsCompleteByCipherIds(IEnumerable<Guid> cipherIds)\n    {\n        if (!cipherIds.Any())\n        {\n            return;\n        }\n\n        await using var connection = new SqlConnection(ConnectionString);\n        await connection.ExecuteAsync(\n            $\"[{Schema}].[SecurityTask_MarkCompleteByCipherIds]\",\n            new { CipherIds = cipherIds.ToGuidIdArrayTVP() },\n            commandType: CommandType.StoredProcedure);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;\n\npublic class OrganizationEntityTypeConfiguration : IEntityTypeConfiguration<Organization>\n{\n    public void Configure(EntityTypeBuilder<Organization> builder)\n    {\n        builder\n            .Property(o => o.Id)\n            .ValueGeneratedNever();\n\n        builder.Property(c => c.AllowAdminAccessToAllCollectionItems)\n            .ValueGeneratedNever()\n            .HasDefaultValue(true);\n\n        NpgsqlIndexBuilderExtensions.IncludeProperties(\n            builder.HasIndex(o => new { o.Id, o.Enabled }),\n            o => new { o.UseTotp, o.UsersGetPremium });\n\n        builder.HasIndex(o => o.GatewayCustomerId);\n        builder.HasIndex(o => o.GatewaySubscriptionId);\n\n        builder.ToTable(nameof(Organization));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationConfigurationEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Dirt.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;\n\npublic class OrganizationIntegrationConfigurationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegrationConfiguration>\n{\n    public void Configure(EntityTypeBuilder<OrganizationIntegrationConfiguration> builder)\n    {\n        builder\n            .Property(oic => oic.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasOne(oic => oic.OrganizationIntegration)\n            .WithMany()\n            .HasForeignKey(oic => oic.OrganizationIntegrationId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder.ToTable(nameof(OrganizationIntegrationConfiguration));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationIntegrationEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Dirt.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;\n\npublic class OrganizationIntegrationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegration>\n{\n    public void Configure(EntityTypeBuilder<OrganizationIntegration> builder)\n    {\n        builder\n            .Property(oi => oi.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(oi => oi.OrganizationId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(oi => new { oi.OrganizationId, oi.Type })\n            .IsUnique()\n            .IsClustered(false);\n\n        builder.ToTable(nameof(OrganizationIntegration));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Configurations/PolicyEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;\n\npublic class PolicyEntityTypeConfiguration : IEntityTypeConfiguration<Policy>\n{\n    public void Configure(EntityTypeBuilder<Policy> builder)\n    {\n        builder\n            .Property(p => p.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(p => p.OrganizationId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(p => new { p.OrganizationId, p.Type })\n            .IsUnique()\n            .IsClustered(false);\n\n        builder.ToTable(nameof(Policy));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Configurations/ProviderEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;\n\npublic class ProviderEntityTypeConfiguration : IEntityTypeConfiguration<Provider>\n{\n    public void Configure(EntityTypeBuilder<Provider> builder)\n    {\n        builder\n            .Property(p => p.Id)\n            .ValueGeneratedNever();\n\n        builder.HasIndex(p => p.GatewayCustomerId);\n        builder.HasIndex(p => p.GatewaySubscriptionId);\n\n        builder.ToTable(nameof(Provider));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Models/Organization.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\npublic class Organization : Core.AdminConsole.Entities.Organization\n{\n    public virtual ICollection<Cipher> Ciphers { get; set; }\n    public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; }\n    public virtual ICollection<Group> Groups { get; set; }\n    public virtual ICollection<Policy> Policies { get; set; }\n    public virtual ICollection<Collection> Collections { get; set; }\n    public virtual ICollection<SsoConfig> SsoConfigs { get; set; }\n    public virtual ICollection<SsoUser> SsoUsers { get; set; }\n    public virtual ICollection<Transaction> Transactions { get; set; }\n    public virtual ICollection<OrganizationApiKey> ApiKeys { get; set; }\n    public virtual ICollection<OrganizationConnection> Connections { get; set; }\n    public virtual ICollection<OrganizationDomain> Domains { get; set; }\n}\n\npublic class OrganizationMapperProfile : Profile\n{\n    public OrganizationMapperProfile()\n    {\n        CreateMap<Core.AdminConsole.Entities.Organization, Organization>()\n            .ForMember(org => org.Ciphers, opt => opt.Ignore())\n            .ForMember(org => org.OrganizationUsers, opt => opt.Ignore())\n            .ForMember(org => org.Groups, opt => opt.Ignore())\n            .ForMember(org => org.Policies, opt => opt.Ignore())\n            .ForMember(org => org.Collections, opt => opt.Ignore())\n            .ForMember(org => org.SsoConfigs, opt => opt.Ignore())\n            .ForMember(org => org.SsoUsers, opt => opt.Ignore())\n            .ForMember(org => org.Transactions, opt => opt.Ignore())\n            .ForMember(org => org.ApiKeys, opt => opt.Ignore())\n            .ForMember(org => org.Connections, opt => opt.Ignore())\n            .ForMember(org => org.Domains, opt => opt.Ignore())\n            .ReverseMap();\n\n        CreateProjection<Organization, SelfHostedOrganizationDetails>()\n            .ForMember(sd => sd.CollectionCount, opt => opt.MapFrom(o => o.Collections.Count))\n            .ForMember(sd => sd.GroupCount, opt => opt.MapFrom(o => o.Groups.Count))\n            .ForMember(sd => sd.OccupiedSeatCount, opt => opt.MapFrom(o => o.OrganizationUsers.Count(ou => ou.Status >= OrganizationUserStatusType.Invited)))\n            .ForMember(sd => sd.OrganizationUsers, opt => opt.MapFrom(o => o.OrganizationUsers))\n            .ForMember(sd => sd.ScimConnections, opt => opt.MapFrom(o => o.Connections.Where(c => c.Type == OrganizationConnectionType.Scim)))\n            .ForMember(sd => sd.SsoConfig, opt => opt.MapFrom(o => o.SsoConfigs.SingleOrDefault()));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Models/Policy.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\npublic class Policy : Core.AdminConsole.Entities.Policy\n{\n    public virtual Organization Organization { get; set; }\n}\n\npublic class PolicyMapperProfile : Profile\n{\n    public PolicyMapperProfile()\n    {\n        CreateMap<Core.AdminConsole.Entities.Policy, Policy>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/Provider.cs",
    "content": "﻿using AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;\n\npublic class Provider : Core.AdminConsole.Entities.Provider.Provider\n{\n}\n\npublic class ProviderMapperProfile : Profile\n{\n    public ProviderMapperProfile()\n    {\n        CreateMap<Core.AdminConsole.Entities.Provider.Provider, Provider>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderOrganization.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;\n\npublic class ProviderOrganization : Core.AdminConsole.Entities.Provider.ProviderOrganization\n{\n    public virtual Provider Provider { get; set; }\n    public virtual Organization Organization { get; set; }\n}\n\npublic class ProviderOrganizationMapperProfile : Profile\n{\n    public ProviderOrganizationMapperProfile()\n    {\n        CreateMap<Core.AdminConsole.Entities.Provider.ProviderOrganization, ProviderOrganization>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderUser.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;\n\npublic class ProviderUser : Core.AdminConsole.Entities.Provider.ProviderUser\n{\n    public virtual User User { get; set; }\n    public virtual Provider Provider { get; set; }\n}\n\npublic class ProviderUserMapperProfile : Profile\n{\n    public ProviderUserMapperProfile()\n    {\n        CreateMap<Core.AdminConsole.Entities.Provider.ProviderUser, ProviderUser>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/GroupRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing AdminConsoleEntities = Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;\n\npublic class GroupRepository : Repository<AdminConsoleEntities.Group, Group, Guid>, IGroupRepository\n{\n    public GroupRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Groups)\n    { }\n\n    public async Task CreateAsync(AdminConsoleEntities.Group obj, IEnumerable<CollectionAccessSelection> collections)\n    {\n        var grp = await base.CreateAsync(obj);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var availableCollections = await (\n                from c in dbContext.Collections\n                where c.OrganizationId == grp.OrganizationId\n                select c).ToListAsync();\n            var filteredCollections = collections.Where(c => availableCollections.Any(a => c.Id == a.Id));\n            var collectionGroups = filteredCollections.Select(y => new CollectionGroup\n            {\n                CollectionId = y.Id,\n                GroupId = grp.Id,\n                ReadOnly = y.ReadOnly,\n                HidePasswords = y.HidePasswords,\n                Manage = y.Manage,\n            });\n            await dbContext.CollectionGroups.AddRangeAsync(collectionGroups);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task DeleteUserAsync(Guid groupId, Guid organizationUserId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from gu in dbContext.GroupUsers\n                        where gu.GroupId == groupId &&\n                            gu.OrganizationUserId == organizationUserId\n                        select gu;\n            dbContext.RemoveRange(await query.ToListAsync());\n            await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<Tuple<AdminConsoleEntities.Group, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id)\n    {\n        var grp = await base.GetByIdAsync(id);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = await (\n                from cg in dbContext.CollectionGroups\n                where cg.GroupId == id\n                select cg).ToListAsync();\n            var collections = query.Select(c => new CollectionAccessSelection\n            {\n                Id = c.CollectionId,\n                ReadOnly = c.ReadOnly,\n                HidePasswords = c.HidePasswords,\n                Manage = c.Manage,\n            }).ToList();\n            return new Tuple<AdminConsoleEntities.Group, ICollection<CollectionAccessSelection>>(\n                grp, collections);\n        }\n    }\n\n    public async Task<ICollection<AdminConsoleEntities.Group>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var data = await (\n                from g in dbContext.Groups\n                where g.OrganizationId == organizationId\n                select g).ToListAsync();\n            return Mapper.Map<List<AdminConsoleEntities.Group>>(data);\n        }\n    }\n\n    public async Task<ICollection<Tuple<AdminConsoleEntities.Group, ICollection<CollectionAccessSelection>>>>\n        GetManyWithCollectionsByOrganizationIdAsync(Guid organizationId)\n    {\n        var groups = await GetManyByOrganizationIdAsync(organizationId);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = await (\n                from cg in dbContext.CollectionGroups\n                where cg.Group.OrganizationId == organizationId\n                select cg).ToListAsync();\n\n            var collections = query.GroupBy(c => c.GroupId).ToList();\n\n            return groups.Select(group =>\n                new Tuple<AdminConsoleEntities.Group, ICollection<CollectionAccessSelection>>(\n                    group,\n                    collections\n                        .FirstOrDefault(c => c.Key == group.Id)?\n                        .Select(c => new CollectionAccessSelection\n                        {\n                            Id = c.CollectionId,\n                            HidePasswords = c.HidePasswords,\n                            ReadOnly = c.ReadOnly,\n                            Manage = c.Manage\n                        }\n                        ).ToList() ?? new List<CollectionAccessSelection>())\n            ).ToList();\n        }\n    }\n\n    public async Task<ICollection<AdminConsoleEntities.Group>> GetManyByManyIds(IEnumerable<Guid> groupIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from g in dbContext.Groups\n                        where groupIds.Contains(g.Id)\n                        select g;\n            var groups = await query.ToListAsync();\n            return Mapper.Map<List<AdminConsoleEntities.Group>>(groups);\n        }\n    }\n\n    public async Task<ICollection<AdminConsoleEntities.GroupUser>> GetManyGroupUsersByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query =\n                from gu in dbContext.GroupUsers\n                join g in dbContext.Groups\n                    on gu.GroupId equals g.Id\n                where g.OrganizationId == organizationId\n                select gu;\n            var groupUsers = await query.ToListAsync();\n            return Mapper.Map<List<AdminConsoleEntities.GroupUser>>(groupUsers);\n        }\n    }\n\n    public async Task<ICollection<Guid>> GetManyIdsByUserIdAsync(Guid organizationUserId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query =\n                from gu in dbContext.GroupUsers\n                where gu.OrganizationUserId == organizationUserId\n                select gu;\n            var groupIds = await query.Select(x => x.GroupId).ToListAsync();\n            return groupIds;\n        }\n    }\n\n    public async Task<ICollection<Guid>> GetManyUserIdsByIdAsync(Guid id, bool useReadOnlyReplica = false)\n    {\n        // EF is only used for self-hosted so read-only replica parameter is ignored\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query =\n                from gu in dbContext.GroupUsers\n                where gu.GroupId == id\n                select gu;\n            var groupIds = await query.Select(x => x.OrganizationUserId).ToListAsync();\n            return groupIds;\n        }\n    }\n\n    public async Task ReplaceAsync(AdminConsoleEntities.Group group, IEnumerable<CollectionAccessSelection> requestedCollections)\n    {\n        await base.ReplaceAsync(group);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var availableCollections = await dbContext.Collections\n                .Where(c => c.OrganizationId == group.OrganizationId)\n                .Select(c => c.Id)\n                .ToListAsync();\n\n            var existingCollectionGroups = await dbContext.CollectionGroups\n                .Where(cg => cg.GroupId == group.Id)\n                .ToListAsync();\n\n            foreach (var requestedCollection in requestedCollections)\n            {\n                var existingCollectionGroup = existingCollectionGroups\n                    .FirstOrDefault(cg => cg.CollectionId == requestedCollection.Id);\n\n                if (existingCollectionGroup == null)\n                {\n                    // It needs to be added\n                    dbContext.CollectionGroups.Add(new CollectionGroup\n                    {\n                        CollectionId = requestedCollection.Id,\n                        GroupId = group.Id,\n                        ReadOnly = requestedCollection.ReadOnly,\n                        HidePasswords = requestedCollection.HidePasswords,\n                        Manage = requestedCollection.Manage\n                    });\n                    continue;\n                }\n\n                existingCollectionGroup.ReadOnly = requestedCollection.ReadOnly;\n                existingCollectionGroup.HidePasswords = requestedCollection.HidePasswords;\n                existingCollectionGroup.Manage = requestedCollection.Manage;\n            }\n\n            var requestedCollectionIds = requestedCollections.Select(c => c.Id);\n\n            dbContext.CollectionGroups.RemoveRange(\n                existingCollectionGroups.Where(cg => !requestedCollectionIds.Contains(cg.CollectionId)));\n\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(group.OrganizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task UpdateUsersAsync(Guid groupId, IEnumerable<Guid> organizationUserIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgId = (await dbContext.Groups.FindAsync(groupId)).OrganizationId;\n            var insert = from ou in dbContext.OrganizationUsers\n                         where organizationUserIds.Contains(ou.Id) &&\n                             ou.OrganizationId == orgId &&\n                             !dbContext.GroupUsers.Any(gu => gu.GroupId == groupId && ou.Id == gu.OrganizationUserId)\n                         select new GroupUser\n                         {\n                             GroupId = groupId,\n                             OrganizationUserId = ou.Id,\n                         };\n            await dbContext.AddRangeAsync(insert);\n\n            var delete = from gu in dbContext.GroupUsers\n                         where gu.GroupId == groupId &&\n                         !organizationUserIds.Contains(gu.OrganizationUserId)\n                         select gu;\n            dbContext.RemoveRange(delete);\n            await dbContext.SaveChangesAsync();\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(orgId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task AddGroupUsersByIdAsync(Guid groupId, IEnumerable<Guid> organizationUserIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgId = (await dbContext.Groups.FindAsync(groupId)).OrganizationId;\n            var insert = from ou in dbContext.OrganizationUsers\n                         where organizationUserIds.Contains(ou.Id) &&\n                             ou.OrganizationId == orgId &&\n                             !dbContext.GroupUsers.Any(gu => gu.GroupId == groupId && ou.Id == gu.OrganizationUserId)\n                         select new GroupUser\n                         {\n                             GroupId = groupId,\n                             OrganizationUserId = ou.Id,\n                         };\n            await dbContext.AddRangeAsync(insert);\n\n            await dbContext.SaveChangesAsync();\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(orgId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> groupIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entities = await dbContext.Groups\n                .Where(g => groupIds.Contains(g.Id))\n                .ToListAsync();\n\n            dbContext.Groups.RemoveRange(entities);\n            await dbContext.SaveChangesAsync();\n\n            foreach (var group in entities.GroupBy(g => g.Organization.Id))\n            {\n                await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(group.Key);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data.Common;\nusing AutoMapper;\nusing AutoMapper.QueryableExtensions;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing LinqToDB.Tools;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Organization = Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class OrganizationRepository : Repository<Core.AdminConsole.Entities.Organization, Organization, Guid>, IOrganizationRepository\n{\n    protected readonly ILogger<OrganizationRepository> _logger;\n\n    public OrganizationRepository(\n        IServiceScopeFactory serviceScopeFactory,\n        IMapper mapper,\n        ILogger<OrganizationRepository> logger)\n        : base(serviceScopeFactory, mapper, context => context.Organizations)\n    {\n        _logger = logger;\n    }\n\n    public async Task<Core.AdminConsole.Entities.Organization> GetByGatewayCustomerIdAsync(string gatewayCustomerId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var organization = await GetDbSet(dbContext)\n                .Where(e => e.GatewayCustomerId == gatewayCustomerId)\n                .FirstOrDefaultAsync();\n            return organization;\n        }\n    }\n\n    public async Task<Core.AdminConsole.Entities.Organization> GetByGatewaySubscriptionIdAsync(string gatewaySubscriptionId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var organization = await GetDbSet(dbContext)\n                .Where(e => e.GatewaySubscriptionId == gatewaySubscriptionId)\n                .FirstOrDefaultAsync();\n            return organization;\n        }\n    }\n\n    public async Task<Core.AdminConsole.Entities.Organization> GetByIdentifierAsync(string identifier)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var organization = await GetDbSet(dbContext).Where(e => e.Identifier == identifier)\n                .FirstOrDefaultAsync();\n            return organization;\n        }\n    }\n\n    public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetManyByEnabledAsync()\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var organizations = await GetDbSet(dbContext).Where(e => e.Enabled).ToListAsync();\n            return Mapper.Map<List<Core.AdminConsole.Entities.Organization>>(organizations);\n        }\n    }\n\n    public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var organizations = await GetDbSet(dbContext)\n                .SelectMany(e => e.OrganizationUsers\n                    .Where(ou => ou.UserId == userId))\n                .Include(ou => ou.Organization)\n                .Select(ou => ou.Organization)\n                .ToListAsync();\n            return Mapper.Map<List<Core.AdminConsole.Entities.Organization>>(organizations);\n        }\n    }\n\n    public async Task<ICollection<Core.AdminConsole.Entities.Organization>> SearchAsync(string name, string userEmail,\n        bool? paid, int skip, int take)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var organizations = await GetDbSet(dbContext)\n                .Where(e => name == null || e.Name.Contains(name))\n                .Where(e => userEmail == null || e.OrganizationUsers.Any(u => u.Email == userEmail))\n                .Where(e => paid == null ||\n                        (paid == true && !string.IsNullOrWhiteSpace(e.GatewaySubscriptionId)) ||\n                        (paid == false && e.GatewaySubscriptionId == null))\n                .OrderBy(e => e.CreationDate)\n                .Skip(skip).Take(take)\n                .ToListAsync();\n            return Mapper.Map<List<Core.AdminConsole.Entities.Organization>>(organizations);\n        }\n    }\n\n    public async Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync()\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await GetDbSet(dbContext)\n            .Select(e => new OrganizationAbility\n            {\n                Enabled = e.Enabled,\n                Id = e.Id,\n                Use2fa = e.Use2fa,\n                UseEvents = e.UseEvents,\n                UsersGetPremium = e.UsersGetPremium,\n                Using2fa = e.Use2fa && e.TwoFactorProviders != null,\n                UseSso = e.UseSso,\n                UseKeyConnector = e.UseKeyConnector,\n                UseResetPassword = e.UseResetPassword,\n                UseScim = e.UseScim,\n                UseCustomPermissions = e.UseCustomPermissions,\n                UsePolicies = e.UsePolicies,\n                LimitCollectionCreation = e.LimitCollectionCreation,\n                LimitCollectionDeletion = e.LimitCollectionDeletion,\n                LimitItemDeletion = e.LimitItemDeletion,\n                AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems,\n                UseRiskInsights = e.UseRiskInsights,\n                UseOrganizationDomains = e.UseOrganizationDomains,\n                UseAdminSponsoredFamilies = e.UseAdminSponsoredFamilies,\n                UseAutomaticUserConfirmation = e.UseAutomaticUserConfirmation,\n                UseDisableSmAdsForUsers = e.UseDisableSmAdsForUsers,\n                UsePhishingBlocker = e.UsePhishingBlocker,\n                UseMyItems = e.UseMyItems\n            }).ToListAsync();\n        }\n    }\n\n    public async Task<ICollection<Core.AdminConsole.Entities.Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n\n        var dbContext = GetDatabaseContext(scope);\n\n        var disallowedPlanTypes = new List<PlanType>\n        {\n            PlanType.Free,\n            PlanType.Custom,\n            PlanType.FamiliesAnnually2019,\n            PlanType.FamiliesAnnually2025,\n            PlanType.FamiliesAnnually\n        };\n\n        var query =\n            from o in dbContext.Organizations\n            where o.PlanType.NotIn(disallowedPlanTypes) &&\n                  !dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) &&\n                  (string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $\"%{name}%\"))\n            select o;\n\n        if (string.IsNullOrWhiteSpace(ownerEmail))\n        {\n            return await query.OrderByDescending(o => o.CreationDate)\n                .Skip(skip)\n                .Take(take)\n                .ToArrayAsync();\n        }\n\n        if (dbContext.Database.IsNpgsql())\n        {\n            query = from o in query\n                    join ou in dbContext.OrganizationUsers\n                        on o.Id equals ou.OrganizationId\n                    join u in dbContext.Users\n                        on ou.UserId equals u.Id\n                    where ou.Type == OrganizationUserType.Owner && EF.Functions.ILike(EF.Functions.Collate(u.Email, \"default\"), $\"{ownerEmail}%\")\n                    select o;\n        }\n        else\n        {\n            query = from o in query\n                    join ou in dbContext.OrganizationUsers\n                        on o.Id equals ou.OrganizationId\n                    join u in dbContext.Users\n                        on ou.UserId equals u.Id\n                    where ou.Type == OrganizationUserType.Owner && EF.Functions.Like(u.Email, $\"{ownerEmail}%\")\n                    select o;\n        }\n\n        return await query.OrderByDescending(o => o.CreationDate).ThenByDescending(o => o.Id).Skip(skip).Take(take).ToArrayAsync();\n    }\n\n    public async Task UpdateStorageAsync(Guid id)\n    {\n        await OrganizationUpdateStorage(id);\n    }\n\n    public override async Task DeleteAsync(Core.AdminConsole.Entities.Organization organization)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organization.Id);\n            var deleteCiphersTransaction = await dbContext.Database.BeginTransactionAsync();\n            await dbContext.Ciphers.Where(c => c.UserId == null && c.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await deleteCiphersTransaction.CommitAsync();\n\n            var organizationDeleteTransaction = await dbContext.Database.BeginTransactionAsync();\n            await dbContext.AuthRequests.Where(ar => ar.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.SsoUsers.Where(su => su.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.SsoConfigs.Where(sc => sc.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.CollectionUsers.Where(cu => cu.OrganizationUser.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.UserProjectAccessPolicy.Where(ap => ap.OrganizationUser.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.UserServiceAccountAccessPolicy.Where(ap => ap.OrganizationUser.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.UserSecretAccessPolicy.Where(ap => ap.OrganizationUser.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.OrganizationUsers.Where(ou => ou.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.ProviderOrganizations.Where(po => po.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.OrganizationIntegrations.Where(oi => oi.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n\n            await dbContext.GroupServiceAccountAccessPolicy.Where(ap => ap.GrantedServiceAccount.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.Project.Where(p => p.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.Secret.Where(s => s.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.ApiKeys.Where(ak => ak.ServiceAccount.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.ServiceAccount.Where(sa => sa.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n\n            await dbContext.NotificationStatuses.Where(ns => ns.Notification.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.Notifications.Where(n => n.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n\n            // The below section are 3 SPROCS in SQL Server but are only called by here\n            await dbContext.OrganizationApiKeys.Where(oa => oa.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            await dbContext.OrganizationConnections.Where(oc => oc.OrganizationId == organization.Id)\n                .ExecuteDeleteAsync();\n            var sponsoringOrgs = await dbContext.OrganizationSponsorships\n                .Where(os => os.SponsoringOrganizationId == organization.Id)\n                .ToListAsync();\n            sponsoringOrgs.ForEach(os => os.SponsoringOrganizationId = null);\n            var sponsoredOrgs = await dbContext.OrganizationSponsorships\n                .Where(os => os.SponsoredOrganizationId == organization.Id)\n                .ToListAsync();\n            sponsoredOrgs.ForEach(os => os.SponsoredOrganizationId = null);\n\n            var orgEntity = await dbContext.FindAsync<Organization>(organization.Id);\n            dbContext.Remove(orgEntity);\n\n            await organizationDeleteTransaction.CommitAsync();\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<Core.AdminConsole.Entities.Organization> GetByLicenseKeyAsync(string licenseKey)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var organization = await GetDbSet(dbContext)\n                .FirstOrDefaultAsync(o => o.LicenseKey == licenseKey);\n\n            return organization;\n        }\n    }\n\n    public async Task<SelfHostedOrganizationDetails> GetSelfHostedOrganizationDetailsById(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var selfHostedOrganization = await dbContext.Organizations\n                .Where(o => o.Id == id)\n                .AsSplitQuery()\n                .ProjectTo<SelfHostedOrganizationDetails>(Mapper.ConfigurationProvider)\n                .SingleOrDefaultAsync();\n\n            return selfHostedOrganization;\n        }\n    }\n\n    public async Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId)\n    {\n        _logger.LogInformation(\"AC-1758: Executing GetOwnerEmailAddressesById (Entity Framework)\");\n\n        using var scope = ServiceScopeFactory.CreateScope();\n\n        var dbContext = GetDatabaseContext(scope);\n\n        var query =\n            from u in dbContext.Users\n            join ou in dbContext.OrganizationUsers on u.Id equals ou.UserId\n            where\n                ou.OrganizationId == organizationId &&\n                ou.Type == OrganizationUserType.Owner &&\n                ou.Status == OrganizationUserStatusType.Confirmed\n            group u by u.Email\n            into grouped\n            select grouped.Key;\n\n        return await query.ToListAsync();\n    }\n\n    public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n\n        var dbContext = GetDatabaseContext(scope);\n\n        var userQuery = from u in dbContext.Users\n                        where u.Id == userId\n                        select u;\n\n        var user = await userQuery.FirstOrDefaultAsync();\n\n        if (user is null)\n        {\n            return new List<Core.AdminConsole.Entities.Organization>();\n        }\n\n        var userWithDomain = new { UserId = user.Id, EmailDomain = user.Email.Split('@').Last() };\n\n        var query = from o in dbContext.Organizations\n                    join ou in dbContext.OrganizationUsers on o.Id equals ou.OrganizationId\n                    join od in dbContext.OrganizationDomains on ou.OrganizationId equals od.OrganizationId\n                    where ou.UserId == userWithDomain.UserId &&\n                          od.DomainName == userWithDomain.EmailDomain &&\n                          od.VerifiedDate != null &&\n                          o.Enabled == true &&\n                          ou.Status != OrganizationUserStatusType.Invited\n                    select o;\n\n        return await query.ToArrayAsync();\n    }\n\n    public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetAddableToProviderByUserIdAsync(\n        Guid userId,\n        ProviderType providerType)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var planTypes = providerType switch\n            {\n                ProviderType.Msp => PlanConstants.EnterprisePlanTypes.Concat(PlanConstants.TeamsPlanTypes),\n                ProviderType.BusinessUnit => PlanConstants.EnterprisePlanTypes,\n                _ => []\n            };\n\n            var query =\n                from organizationUser in dbContext.OrganizationUsers\n                join organization in dbContext.Organizations on organizationUser.OrganizationId equals organization.Id\n                where\n                    organizationUser.UserId == userId &&\n                    organizationUser.Type == OrganizationUserType.Owner &&\n                    organizationUser.Status == OrganizationUserStatusType.Confirmed &&\n                    organization.Enabled &&\n                    organization.GatewayCustomerId != null &&\n                    organization.GatewaySubscriptionId != null &&\n                    organization.Seats > 0 &&\n                    organization.Status == OrganizationStatusType.Created &&\n                    !organization.UseSecretsManager &&\n                    organization.PlanType.In(planTypes)\n                select organization;\n\n            return await query.ToArrayAsync();\n        }\n    }\n\n    public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n\n        var dbContext = GetDatabaseContext(scope);\n\n        var query = from organization in dbContext.Organizations\n                    where ids.Contains(organization.Id)\n                    select organization;\n\n        return await query.ToArrayAsync();\n    }\n\n    public async Task<OrganizationSeatCounts> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var users = await dbContext.OrganizationUsers\n                .Where(ou => ou.OrganizationId == organizationId && ou.Status >= 0)\n                .CountAsync();\n\n            var sponsored = await dbContext.OrganizationSponsorships\n                .Where(os => os.SponsoringOrganizationId == organizationId &&\n                    os.IsAdminInitiated &&\n                    (os.ToDelete == false || (os.ToDelete == true && os.ValidUntil != null && os.ValidUntil > DateTime.UtcNow)) &&\n                    (os.SponsoredOrganizationId == null || (os.SponsoredOrganizationId != null && (os.ValidUntil == null || os.ValidUntil > DateTime.UtcNow))))\n                .CountAsync();\n\n            return new OrganizationSeatCounts\n            {\n                Users = users,\n                Sponsored = sponsored\n            };\n        }\n    }\n\n    public async Task<IEnumerable<Core.AdminConsole.Entities.Organization>> GetOrganizationsForSubscriptionSyncAsync()\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        await using var dbContext = GetDatabaseContext(scope);\n\n        var organizations = await dbContext.Organizations\n            .Where(o => o.SyncSeats == true && o.Seats != null)\n            .ToArrayAsync();\n\n        return organizations;\n    }\n\n    public async Task UpdateSuccessfulOrganizationSyncStatusAsync(IEnumerable<Guid> successfulOrganizations, DateTime syncDate)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        await using var dbContext = GetDatabaseContext(scope);\n\n        await dbContext.Organizations\n            .Where(o => successfulOrganizations.Contains(o.Id))\n            .ExecuteUpdateAsync(o => o\n                .SetProperty(x => x.SyncSeats, false)\n                .SetProperty(x => x.RevisionDate, syncDate.Date));\n    }\n\n    public async Task IncrementSeatCountAsync(Guid organizationId, int increaseAmount, DateTime requestDate)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        await using var dbContext = GetDatabaseContext(scope);\n\n        await dbContext.Organizations\n            .Where(o => o.Id == organizationId)\n            .ExecuteUpdateAsync(s => s\n                .SetProperty(o => o.Seats, o => o.Seats + increaseAmount)\n                .SetProperty(o => o.SyncSeats, true)\n                .SetProperty(o => o.RevisionDate, requestDate));\n    }\n\n    public async Task InitializeOrganizationAsync(Core.AdminConsole.Entities.Organization organization, Func<DbConnection, DbTransaction, Task> confirmOwnerAction)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var connection = dbContext.Database.GetDbConnection();\n        await connection.OpenAsync();\n        await using var transaction = await connection.BeginTransactionAsync();\n        await dbContext.Database.UseTransactionAsync(transaction);\n\n        try\n        {\n            var efOrganization = await dbContext.Organizations.FindAsync(organization.Id);\n            if (efOrganization is null)\n            {\n                throw new InvalidOperationException($\"Organization {organization.Id} was not found during initialization.\");\n            }\n\n            efOrganization.Enabled = organization.Enabled;\n            efOrganization.Status = organization.Status;\n            efOrganization.PublicKey = organization.PublicKey;\n            efOrganization.PrivateKey = organization.PrivateKey;\n            efOrganization.RevisionDate = organization.RevisionDate;\n\n            await dbContext.SaveChangesAsync();\n\n            await confirmOwnerAction(connection, transaction);\n\n            await transaction.CommitAsync();\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex,\n                \"Failed to initialize organization. Rolling back transaction.\");\n            await transaction.RollbackAsync();\n            throw;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Data.Common;\nusing AutoMapper;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;\n\npublic class OrganizationUserRepository : Repository<Core.Entities.OrganizationUser, OrganizationUser, Guid>, IOrganizationUserRepository\n{\n    public OrganizationUserRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationUsers)\n    { }\n\n    public async Task<Guid> CreateAsync(Core.Entities.OrganizationUser obj, IEnumerable<CollectionAccessSelection> collections)\n    {\n        var organizationUser = await base.CreateAsync(obj);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var availableCollections = await (\n                from c in dbContext.Collections\n                where c.OrganizationId == organizationUser.OrganizationId\n                select c).ToListAsync();\n            var filteredCollections = collections.Where(c => availableCollections.Any(a => c.Id == a.Id));\n            var collectionUsers = filteredCollections.Select(y => new CollectionUser\n            {\n                CollectionId = y.Id,\n                OrganizationUserId = organizationUser.Id,\n                ReadOnly = y.ReadOnly,\n                HidePasswords = y.HidePasswords,\n                Manage = y.Manage\n            });\n            await dbContext.CollectionUsers.AddRangeAsync(collectionUsers);\n            await dbContext.SaveChangesAsync();\n        }\n\n        return organizationUser.Id;\n    }\n\n    public async Task<ICollection<Guid>> CreateManyAsync(IEnumerable<Core.Entities.OrganizationUser> organizationUsers)\n    {\n        organizationUsers = organizationUsers.ToList();\n        if (!organizationUsers.Any())\n        {\n            return new List<Guid>();\n        }\n\n        foreach (var organizationUser in organizationUsers)\n        {\n            organizationUser.SetNewId();\n        }\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entities = Mapper.Map<List<OrganizationUser>>(organizationUsers);\n            await dbContext.AddRangeAsync(entities);\n            await dbContext.SaveChangesAsync();\n        }\n\n        return organizationUsers.Select(u => u.Id).ToList();\n    }\n\n    public override async Task DeleteAsync(Core.Entities.OrganizationUser organizationUser)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgUser = await dbContext.OrganizationUsers\n                .Where(ou => ou.Id == organizationUser.Id)\n                .Select(ou => new\n                {\n                    ou.Id,\n                    ou.UserId,\n                    OrgEmail = ou.Email,\n                    UserEmail = ou.User.Email\n                })\n                .FirstOrDefaultAsync();\n\n            if (orgUser == null)\n            {\n                throw new NotFoundException(\"User not found.\");\n            }\n\n            var email = !string.IsNullOrEmpty(orgUser.OrgEmail)\n                ? orgUser.OrgEmail\n                : orgUser.UserEmail;\n            var organizationId = organizationUser?.OrganizationId;\n            var userId = orgUser?.UserId;\n            var utcNow = DateTime.UtcNow;\n\n            using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n            try\n            {\n                await dbContext.Collections\n                    .Where(c => c.Type == CollectionType.DefaultUserCollection\n                             && c.CollectionUsers.Any(cu => cu.OrganizationUserId == organizationUser.Id))\n                    .ExecuteUpdateAsync(setters => setters\n                        .SetProperty(c => c.Type, CollectionType.SharedCollection)\n                        .SetProperty(c => c.RevisionDate, utcNow)\n                        .SetProperty(c => c.DefaultUserCollectionEmail,\n                            c => c.DefaultUserCollectionEmail == null ? email : c.DefaultUserCollectionEmail));\n\n                await dbContext.CollectionUsers\n                    .Where(cu => cu.OrganizationUserId == organizationUser.Id)\n                    .ExecuteDeleteAsync();\n\n                await dbContext.GroupUsers\n                    .Where(gu => gu.OrganizationUserId == organizationUser.Id)\n                    .ExecuteDeleteAsync();\n\n                await dbContext.SsoUsers\n                    .Where(su => su.UserId == userId && su.OrganizationId == organizationId)\n                    .ExecuteDeleteAsync();\n\n                await dbContext.UserProjectAccessPolicy\n                    .Where(ap => ap.OrganizationUserId == organizationUser.Id)\n                    .ExecuteDeleteAsync();\n\n                await dbContext.UserServiceAccountAccessPolicy\n                    .Where(ap => ap.OrganizationUserId == organizationUser.Id)\n                    .ExecuteDeleteAsync();\n\n                await dbContext.UserSecretAccessPolicy\n                    .Where(ap => ap.OrganizationUserId == organizationUser.Id)\n                    .ExecuteDeleteAsync();\n\n                await dbContext.OrganizationSponsorships\n                    .Where(os => os.SponsoringOrganizationUserId == organizationUser.Id)\n                    .ExecuteDeleteAsync();\n\n                await dbContext.Users\n                    .Where(u => u.Id == orgUser.UserId)\n                    .ExecuteUpdateAsync(setters => setters\n                        .SetProperty(u => u.AccountRevisionDate, utcNow));\n\n                await dbContext.OrganizationUsers\n                    .Where(ou => ou.Id == organizationUser.Id)\n                    .ExecuteDeleteAsync();\n\n                await transaction.CommitAsync();\n            }\n            catch\n            {\n                await transaction.RollbackAsync();\n                throw;\n            }\n        }\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> organizationUserIds)\n    {\n        var targetOrganizationUserIds = organizationUserIds.ToList();\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        try\n        {\n            await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdsAsync(targetOrganizationUserIds);\n\n            var organizationUsersToDelete = await dbContext.OrganizationUsers\n                .Where(ou => targetOrganizationUserIds.Contains(ou.Id))\n                .Include(ou => ou.User)\n                .ToListAsync();\n\n            var collectionUsers = await dbContext.CollectionUsers\n                .Where(cu => targetOrganizationUserIds.Contains(cu.OrganizationUserId))\n                .ToListAsync();\n\n            var collectionIds = collectionUsers.Select(cu => cu.CollectionId).Distinct().ToList();\n\n            var collections = await dbContext.Collections\n                .Where(c => collectionIds.Contains(c.Id))\n                .ToListAsync();\n\n            var collectionsToUpdate = collections\n                .Where(c => c.Type == CollectionType.DefaultUserCollection)\n                .ToList();\n\n            var collectionUserLookup = collectionUsers.ToLookup(cu => cu.CollectionId);\n\n            foreach (var collection in collectionsToUpdate)\n            {\n                var collectionUser = collectionUserLookup[collection.Id].FirstOrDefault();\n                if (collectionUser != null)\n                {\n                    var orgUser = organizationUsersToDelete.FirstOrDefault(ou => ou.Id == collectionUser.OrganizationUserId);\n\n                    if (orgUser?.User != null)\n                    {\n                        if (string.IsNullOrEmpty(collection.DefaultUserCollectionEmail))\n                        {\n                            var emailToUse = !string.IsNullOrEmpty(orgUser.Email)\n                                ? orgUser.Email\n                                : orgUser.User.Email;\n\n                            if (!string.IsNullOrEmpty(emailToUse))\n                            {\n                                collection.DefaultUserCollectionEmail = emailToUse;\n                            }\n                        }\n                        collection.Type = CollectionType.SharedCollection;\n                    }\n                }\n            }\n\n            await dbContext.CollectionUsers\n                .Where(cu => targetOrganizationUserIds.Contains(cu.OrganizationUserId))\n                .ExecuteDeleteAsync();\n\n            await dbContext.GroupUsers\n                .Where(gu => targetOrganizationUserIds.Contains(gu.OrganizationUserId))\n                .ExecuteDeleteAsync();\n\n            await dbContext.UserProjectAccessPolicy\n                .Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value))\n                .ExecuteDeleteAsync();\n\n            await dbContext.UserServiceAccountAccessPolicy\n                .Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value))\n                .ExecuteDeleteAsync();\n\n            await dbContext.UserSecretAccessPolicy\n                .Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value))\n                .ExecuteDeleteAsync();\n\n            await dbContext.OrganizationSponsorships\n                .Where(os => targetOrganizationUserIds.Contains(os.SponsoringOrganizationUserId))\n                .ExecuteDeleteAsync();\n\n            await dbContext.OrganizationUsers\n                .Where(ou => targetOrganizationUserIds.Contains(ou.Id)).ExecuteDeleteAsync();\n\n            await dbContext.SaveChangesAsync();\n            await transaction.CommitAsync();\n        }\n        catch\n        {\n            await transaction.RollbackAsync();\n            throw;\n        }\n    }\n\n    public async Task<Tuple<Core.Entities.OrganizationUser, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id)\n    {\n        var organizationUser = await base.GetByIdAsync(id);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = await (\n                from ou in dbContext.OrganizationUsers\n                join cu in dbContext.CollectionUsers\n                    on ou.Id equals cu.OrganizationUserId\n                where ou.Id == id\n                select cu).ToListAsync();\n            var collections = query.Select(cu => new CollectionAccessSelection\n            {\n                Id = cu.CollectionId,\n                ReadOnly = cu.ReadOnly,\n                HidePasswords = cu.HidePasswords,\n                Manage = cu.Manage,\n            });\n            return new Tuple<Core.Entities.OrganizationUser, ICollection<CollectionAccessSelection>>(\n                organizationUser, collections.ToList());\n        }\n    }\n\n    public async Task<Core.Entities.OrganizationUser> GetByOrganizationAsync(Guid organizationId, Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await GetDbSet(dbContext)\n                .FirstOrDefaultAsync(e => e.OrganizationId == organizationId && e.UserId == userId);\n            return entity;\n        }\n    }\n\n    public async Task<Core.Entities.OrganizationUser> GetByOrganizationEmailAsync(Guid organizationId, string email)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await GetDbSet(dbContext)\n                .FirstOrDefaultAsync(ou => ou.OrganizationId == organizationId &&\n                    !string.IsNullOrWhiteSpace(ou.Email) &&\n                    ou.Email == email);\n            return entity;\n        }\n    }\n\n    public async Task<int> GetCountByFreeOrganizationAdminUserAsync(Guid userId)\n    {\n        var query = new OrganizationUserReadCountByFreeOrganizationAdminUserQuery(userId);\n        return await GetCountFromQuery(query);\n    }\n\n    public async Task<int> GetCountByOnlyOwnerAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await dbContext.OrganizationUsers\n                .Where(ou => ou.Type == OrganizationUserType.Owner && ou.Status == OrganizationUserStatusType.Confirmed)\n                .GroupBy(ou => ou.UserId)\n                .Select(g => new { UserId = g.Key, ConfirmedOwnerCount = g.Count() })\n                .Where(oc => oc.UserId == userId && oc.ConfirmedOwnerCount == 1)\n                .CountAsync();\n        }\n    }\n\n    public async Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers)\n    {\n        var query = new OrganizationUserReadCountByOrganizationIdEmailQuery(organizationId, email, onlyRegisteredUsers);\n        return await GetCountFromQuery(query);\n    }\n\n    public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)\n    {\n        var query = new OrganizationUserReadCountByOrganizationIdQuery(organizationId);\n        return await GetCountFromQuery(query);\n    }\n\n    public async Task<OrganizationUserUserDetails> GetDetailsByIdAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new OrganizationUserUserDetailsViewQuery();\n            var entity = await view.Run(dbContext).FirstOrDefaultAsync(ou => ou.Id == id);\n            return entity;\n        }\n    }\n\n#nullable enable\n    public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithSharedCollectionsAsync(Guid id)\n    {\n        var organizationUserUserDetails = await GetDetailsByIdAsync(id);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from ou in dbContext.OrganizationUsers\n                        join cu in dbContext.CollectionUsers on ou.Id equals cu.OrganizationUserId\n                        join c in dbContext.Collections on cu.CollectionId equals c.Id\n                        where ou.Id == id && c.Type == CollectionType.SharedCollection\n                        select cu;\n            var collections = await query.Select(cu => new CollectionAccessSelection\n            {\n                Id = cu.CollectionId,\n                ReadOnly = cu.ReadOnly,\n                HidePasswords = cu.HidePasswords,\n                Manage = cu.Manage\n            }).ToListAsync();\n            return (organizationUserUserDetails, collections);\n        }\n    }\n#nullable disable\n\n    public async Task<OrganizationUserOrganizationDetails> GetDetailsByUserAsync(Guid userId, Guid organizationId, OrganizationUserStatusType? status = null)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new OrganizationUserOrganizationDetailsViewQuery();\n            var t = await (view.Run(dbContext)).ToArrayAsync();\n            var entity = await view.Run(dbContext)\n                .FirstOrDefaultAsync(o => o.UserId == userId &&\n                    o.OrganizationId == organizationId &&\n                    (status == null || o.Status == status));\n            return entity;\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationUser>> GetManyAsync(IEnumerable<Guid> Ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from ou in dbContext.OrganizationUsers\n                        where Ids.Contains(ou.Id)\n                        select ou;\n            var data = await query.ToArrayAsync();\n            return data;\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from ou in dbContext.OrganizationUsers\n                        where ou.UserId.HasValue && userIds.Contains(ou.UserId.Value)\n                        select ou;\n            return Mapper.Map<List<Core.Entities.OrganizationUser>>(await query.ToListAsync());\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from ou in dbContext.OrganizationUsers\n                        where ou.OrganizationId == organizationId &&\n                            (type == null || ou.Type == type)\n                        select ou;\n            return Mapper.Map<List<Core.Entities.OrganizationUser>>(await query.ToListAsync());\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationUser>> GetManyByUserAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from ou in dbContext.OrganizationUsers\n                        where ou.UserId == userId\n                        select ou;\n            return Mapper.Map<List<Core.Entities.OrganizationUser>>(await query.ToListAsync());\n        }\n    }\n\n    public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeSharedCollections)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new OrganizationUserUserDetailsViewQuery();\n            var users = await (from ou in view.Run(dbContext)\n                               where ou.OrganizationId == organizationId\n                               select ou).ToListAsync();\n\n            if (!includeSharedCollections && !includeGroups)\n            {\n                return users;\n            }\n\n            List<IGrouping<Guid, GroupUser>> groups = null;\n            List<IGrouping<Guid, CollectionUser>> collections = null;\n            var userIds = users.Select(u => u.Id);\n            var userIdEntities = dbContext.OrganizationUsers.Where(x => userIds.Contains(x.Id));\n\n            // Query groups/collections separately to avoid cartesian explosion\n            if (includeGroups)\n            {\n                groups = (await (from gu in dbContext.GroupUsers\n                                 join ou in userIdEntities on gu.OrganizationUserId equals ou.Id\n                                 select gu).ToListAsync())\n                    .GroupBy(g => g.OrganizationUserId).ToList();\n            }\n\n            if (includeSharedCollections)\n            {\n                collections = (await (from cu in dbContext.CollectionUsers\n                                      join ou in userIdEntities on cu.OrganizationUserId equals ou.Id\n                                      join c in dbContext.Collections on cu.CollectionId equals c.Id\n                                      where c.Type == CollectionType.SharedCollection\n                                      select cu).ToListAsync())\n                    .GroupBy(c => c.OrganizationUserId).ToList();\n            }\n\n            // Map any queried collections and groups to their respective users\n            foreach (var user in users)\n            {\n                if (groups != null)\n                {\n                    user.Groups = groups\n                        .FirstOrDefault(g => g.Key == user.Id)?\n                        .Select(g => g.GroupId).ToList() ?? new List<Guid>();\n                }\n\n                if (collections != null)\n                {\n                    user.Collections = collections\n                        .FirstOrDefault(c => c.Key == user.Id)?\n                        .Select(cu => new CollectionAccessSelection\n                        {\n                            Id = cu.CollectionId,\n                            ReadOnly = cu.ReadOnly,\n                            HidePasswords = cu.HidePasswords,\n                            Manage = cu.Manage,\n                        }).ToList() ?? new List<CollectionAccessSelection>();\n                }\n            }\n\n            return users;\n        }\n    }\n\n    public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(\n        Guid organizationId, bool includeGroups, bool includeSharedCollections)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var query = from ou in dbContext.OrganizationUsers\n                    where ou.OrganizationId == organizationId\n                    select new OrganizationUserUserDetails\n                    {\n                        Id = ou.Id,\n                        UserId = ou.UserId,\n                        OrganizationId = ou.OrganizationId,\n                        Name = ou.User.Name,\n                        Email = ou.User.Email ?? ou.Email,\n                        AvatarColor = ou.User.AvatarColor,\n                        TwoFactorProviders = ou.User.TwoFactorProviders,\n                        Premium = ou.User.Premium,\n                        Status = ou.Status,\n                        Type = ou.Type,\n                        ExternalId = ou.ExternalId,\n                        SsoExternalId = ou.User.SsoUsers\n                            .Where(su => su.OrganizationId == ou.OrganizationId)\n                            .Select(su => su.ExternalId)\n                            .FirstOrDefault(),\n                        Permissions = ou.Permissions,\n                        ResetPasswordKey = ou.ResetPasswordKey,\n                        UsesKeyConnector = ou.User != null && ou.User.UsesKeyConnector,\n                        AccessSecretsManager = ou.AccessSecretsManager,\n                        HasMasterPassword = ou.User != null && !string.IsNullOrWhiteSpace(ou.User.MasterPassword),\n\n                        // Project directly from navigation properties with conditional loading\n                        Groups = includeGroups\n                            ? ou.GroupUsers.Select(gu => gu.GroupId).ToList()\n                            : new List<Guid>(),\n\n                        Collections = includeSharedCollections\n                            ? ou.CollectionUsers\n                                .Where(cu => cu.Collection.Type == CollectionType.SharedCollection)\n                                .Select(cu => new CollectionAccessSelection\n                                {\n                                    Id = cu.CollectionId,\n                                    ReadOnly = cu.ReadOnly,\n                                    HidePasswords = cu.HidePasswords,\n                                    Manage = cu.Manage\n                                }).ToList()\n                            : new List<CollectionAccessSelection>()\n                    };\n\n        return await query.ToListAsync();\n    }\n\n    public async Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,\n            OrganizationUserStatusType? status = null)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new OrganizationUserOrganizationDetailsViewQuery();\n            var query = from ou in view.Run(dbContext)\n                        where ou.UserId == userId &&\n                        (status == null || ou.Status == status)\n                        select ou;\n            var organizationUsers = await query.ToListAsync();\n            return organizationUsers;\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationUserPublicKey>> GetManyPublicKeysByOrganizationUserAsync(Guid organizationId, IEnumerable<Guid> Ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from ou in dbContext.OrganizationUsers\n                        where Ids.Contains(ou.Id) && ou.Status == OrganizationUserStatusType.Accepted\n                        join u in dbContext.Users\n                            on ou.UserId equals u.Id\n                        where ou.OrganizationId == organizationId\n                        select new { ou, u };\n            var data = await query\n                .Select(x => new OrganizationUserPublicKey()\n                {\n                    Id = x.ou.Id,\n                    PublicKey = x.u.PublicKey,\n                }).ToListAsync();\n            return data;\n        }\n    }\n\n    public override async Task ReplaceAsync(Core.Entities.OrganizationUser organizationUser)\n    {\n        await base.ReplaceAsync(organizationUser);\n\n        // Only bump the account revision date if linked to a user account\n        if (!organizationUser.UserId.HasValue)\n        {\n            return;\n        }\n\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        await dbContext.UserBumpAccountRevisionDateAsync(organizationUser.UserId.Value);\n        await dbContext.SaveChangesAsync();\n    }\n\n    public async Task ReplaceAsync(Core.Entities.OrganizationUser obj, IEnumerable<CollectionAccessSelection> requestedCollections)\n    {\n        await ReplaceAsync(obj);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            // Retrieve all collection assignments, excluding DefaultUserCollection\n            var existingCollectionUsers = await (from cu in dbContext.CollectionUsers\n                                                 join c in dbContext.Collections on cu.CollectionId equals c.Id\n                                                 where cu.OrganizationUserId == obj.Id && c.Type != CollectionType.DefaultUserCollection\n                                                 select cu).ToListAsync();\n\n            foreach (var requestedCollection in requestedCollections)\n            {\n                var existingCollectionUser = existingCollectionUsers.FirstOrDefault(cu => cu.CollectionId == requestedCollection.Id);\n                if (existingCollectionUser == null)\n                {\n                    // This is a brand new entry\n                    dbContext.CollectionUsers.Add(new CollectionUser\n                    {\n                        CollectionId = requestedCollection.Id,\n                        OrganizationUserId = obj.Id,\n                        HidePasswords = requestedCollection.HidePasswords,\n                        ReadOnly = requestedCollection.ReadOnly,\n                        Manage = requestedCollection.Manage\n                    });\n                    continue;\n                }\n\n                // It already exists, update it\n                existingCollectionUser.HidePasswords = requestedCollection.HidePasswords;\n                existingCollectionUser.ReadOnly = requestedCollection.ReadOnly;\n                existingCollectionUser.Manage = requestedCollection.Manage;\n                dbContext.CollectionUsers.Update(existingCollectionUser);\n            }\n\n            // Remove all existing ones that are no longer requested\n            var requestedCollectionIds = requestedCollections.Select(c => c.Id).ToList();\n            dbContext.CollectionUsers.RemoveRange(existingCollectionUsers.Where(cu => !requestedCollectionIds.Contains(cu.CollectionId)));\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task ReplaceManyAsync(IEnumerable<Core.Entities.OrganizationUser> organizationUsers)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            dbContext.UpdateRange(organizationUsers);\n            await dbContext.SaveChangesAsync();\n            await dbContext.UserBumpManyAccountRevisionDatesAsync(organizationUsers\n                .Where(ou => ou.UserId.HasValue)\n                .Select(ou => ou.UserId.Value).ToArray());\n        }\n    }\n\n    public async Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var usersQuery = from ou in dbContext.OrganizationUsers\n                             join u in dbContext.Users\n                                 on ou.UserId equals u.Id into u_g\n                             from u in u_g\n                             where ou.OrganizationId == organizationId\n                             select new { ou, u };\n            var ouu = await usersQuery.ToListAsync();\n            var ouEmails = ouu.Select(x => x.ou.Email);\n            var uEmails = ouu.Select(x => x.u.Email);\n            var knownEmails = from e in emails\n                              where (ouEmails.Contains(e) || uEmails.Contains(e)) &&\n                              (!onlyRegisteredUsers && (uEmails.Contains(e) || ouEmails.Contains(e))) ||\n                              (onlyRegisteredUsers && uEmails.Contains(e))\n                              select e;\n            return knownEmails.ToList();\n        }\n    }\n\n    public async Task UpdateGroupsAsync(Guid orgUserId, IEnumerable<Guid> groupIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var procedure = new GroupUserUpdateGroupsQuery(orgUserId, groupIds);\n\n            var insert = procedure.Insert.Run(dbContext);\n            var data = await insert.ToListAsync();\n            await dbContext.AddRangeAsync(data);\n\n            var delete = procedure.Delete.Run(dbContext);\n            var deleteData = await delete.ToListAsync();\n            dbContext.RemoveRange(deleteData);\n            await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(orgUserId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task UpsertManyAsync(IEnumerable<Core.Entities.OrganizationUser> organizationUsers)\n    {\n        var createUsers = new List<Core.Entities.OrganizationUser>();\n        var replaceUsers = new List<Core.Entities.OrganizationUser>();\n        foreach (var organizationUser in organizationUsers)\n        {\n            if (organizationUser.Id.Equals(default))\n            {\n                createUsers.Add(organizationUser);\n            }\n            else\n            {\n                replaceUsers.Add(organizationUser);\n            }\n        }\n\n        await CreateManyAsync(createUsers);\n        await ReplaceManyAsync(replaceUsers);\n    }\n\n    public async Task<IEnumerable<OrganizationUserUserDetails>> GetManyByMinimumRoleAsync(Guid organizationId, OrganizationUserType minRole)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.OrganizationUsers\n                .Include(e => e.User)\n                .Where(e => e.OrganizationId.Equals(organizationId) &&\n                    e.Type <= minRole &&\n                    e.Status == OrganizationUserStatusType.Confirmed)\n                .Select(e => new OrganizationUserUserDetails()\n                {\n                    Id = e.Id,\n                    Email = e.Email ?? e.User.Email\n                });\n            return await query.ToListAsync();\n        }\n    }\n\n    public async Task RevokeAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgUser = await dbContext.OrganizationUsers.FindAsync(id);\n            if (orgUser == null)\n            {\n                return;\n            }\n\n            orgUser.Status = OrganizationUserStatusType.Revoked;\n            await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(id);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task RestoreAsync(Guid id, OrganizationUserStatusType status)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgUser = await dbContext.OrganizationUsers\n                .FirstOrDefaultAsync(ou => ou.Id == id && ou.Status == OrganizationUserStatusType.Revoked);\n\n            if (orgUser == null)\n            {\n                return;\n            }\n\n            orgUser.Status = status;\n            await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(id);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var providerOrganizations = from pu in dbContext.ProviderUsers\n                                        where pu.UserId == userId\n                                        join po in dbContext.ProviderOrganizations\n                                            on pu.ProviderId equals po.ProviderId\n                                        select po;\n\n            var query = from p in dbContext.Policies\n                        join ou in dbContext.OrganizationUsers\n                            on p.OrganizationId equals ou.OrganizationId\n                        let email = dbContext.Users.Find(userId).Email  // Invited orgUsers do not have a UserId associated with them, so we have to match up their email\n                        where p.Type == policyType &&\n                            (ou.UserId == userId || ou.Email == email)\n                        select new OrganizationUserPolicyDetails\n                        {\n                            OrganizationUserId = ou.Id,\n                            OrganizationId = p.OrganizationId,\n                            PolicyType = p.Type,\n                            PolicyEnabled = p.Enabled,\n                            PolicyData = p.Data,\n                            OrganizationUserType = ou.Type,\n                            OrganizationUserStatus = ou.Status,\n                            OrganizationUserPermissionsData = ou.Permissions,\n                            IsProvider = providerOrganizations.Any(po => po.OrganizationId == p.OrganizationId)\n                        };\n            return await query.ToListAsync();\n        }\n    }\n\n    public async Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId)\n    {\n        var query = new OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery(organizationId);\n        return await GetCountFromQuery(query);\n    }\n\n    public async Task<IEnumerable<OrganizationUserResetPasswordDetails>>\n        GetManyAccountRecoveryDetailsByOrganizationUserAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from ou in dbContext.OrganizationUsers\n                        where organizationUserIds.Contains(ou.Id)\n                        join u in dbContext.Users\n                            on ou.UserId equals u.Id\n                        join o in dbContext.Organizations\n                            on ou.OrganizationId equals o.Id\n                        where ou.OrganizationId == organizationId\n                        select new { ou, u, o };\n            var data = await query\n                .Select(x => new OrganizationUserResetPasswordDetails(x.ou, x.u, x.o)).ToListAsync();\n            return data;\n        }\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(\n        Guid userId, IEnumerable<Core.Entities.OrganizationUser> resetPasswordKeys)\n    {\n        return async (_, _) =>\n        {\n            var newOrganizationUsers = resetPasswordKeys.ToList();\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n\n            // Get user organization users\n            var userOrganizationUsers = await GetDbSet(dbContext)\n                .Where(c => c.UserId == userId)\n                .ToListAsync();\n\n            // Filter to only organization users that are included\n            var validOrganizationUsers = userOrganizationUsers\n                .Where(organizationUser =>\n                    newOrganizationUsers.Any(newOrganizationUser => newOrganizationUser.Id == organizationUser.Id));\n\n            foreach (var organizationUser in validOrganizationUsers)\n            {\n                var updateOrganizationUser =\n                    newOrganizationUsers.First(newOrganizationUser => newOrganizationUser.Id == organizationUser.Id);\n                organizationUser.ResetPasswordKey = updateOrganizationUser.ResetPasswordKey;\n            }\n\n            await dbContext.SaveChangesAsync();\n        };\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new OrganizationUserReadByClaimedOrganizationDomainsQuery(organizationId);\n            var data = await query.Run(dbContext).ToListAsync();\n            return data;\n        }\n    }\n\n    public async Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n\n        var dbContext = GetDatabaseContext(scope);\n\n        await dbContext.OrganizationUsers.Where(x => organizationUserIds.Contains(x.Id))\n            .ExecuteUpdateAsync(s => s.SetProperty(x => x.Status, OrganizationUserStatusType.Revoked));\n\n        await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdsAsync(organizationUserIds);\n    }\n\n    public async Task<IEnumerable<OrganizationUserUserDetails>> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from ou in dbContext.OrganizationUsers\n                        join u in dbContext.Users\n                            on ou.UserId equals u.Id\n                        where ou.OrganizationId == organizationId &&\n                            ou.Type == role &&\n                            ou.Status == OrganizationUserStatusType.Confirmed\n                        select new OrganizationUserUserDetails\n                        {\n                            Id = ou.Id,\n                            Email = ou.Email ?? u.Email,\n                            Permissions = ou.Permissions\n                        };\n            return await query.ToListAsync();\n        }\n    }\n\n    public async Task CreateManyAsync(IEnumerable<CreateOrganizationUser> organizationUserCollection)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n\n        await using var dbContext = GetDatabaseContext(scope);\n\n        dbContext.OrganizationUsers.AddRange(Mapper.Map<List<OrganizationUser>>(organizationUserCollection.Select(x => x.OrganizationUser)));\n        dbContext.CollectionUsers.AddRange(organizationUserCollection.SelectMany(x => x.Collections, (user, collection) => new CollectionUser\n        {\n            CollectionId = collection.Id,\n            HidePasswords = collection.HidePasswords,\n            OrganizationUserId = user.OrganizationUser.Id,\n            Manage = collection.Manage,\n            ReadOnly = collection.ReadOnly\n        }));\n        dbContext.GroupUsers.AddRange(organizationUserCollection.SelectMany(x => x.Groups, (user, group) => new GroupUser\n        {\n            GroupId = group,\n            OrganizationUserId = user.OrganizationUser.Id\n        }));\n\n        await dbContext.SaveChangesAsync();\n    }\n\n    public async Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        await using var dbContext = GetDatabaseContext(scope);\n\n        var result = await dbContext.OrganizationUsers\n            .Where(ou => ou.Id == organizationUserToConfirm.OrganizationUserId\n                         && ou.Status == OrganizationUserStatusType.Accepted)\n            .ExecuteUpdateAsync(x => x\n                .SetProperty(y => y.Status, OrganizationUserStatusType.Confirmed)\n                .SetProperty(y => y.Key, organizationUserToConfirm.Key));\n\n        if (result <= 0)\n        {\n            return false;\n        }\n\n        await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserToConfirm.OrganizationUserId);\n        return true;\n\n    }\n\n#nullable enable\n\n    public async Task<OrganizationUserUserDetails?> GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new OrganizationUserUserDetailsViewQuery();\n            var entity = await view.Run(dbContext).SingleOrDefaultAsync(ou => ou.OrganizationId == organizationId && ou.UserId == userId);\n            return entity;\n        }\n    }\n\n    public Func<DbConnection, DbTransaction, Task> BuildConfirmOwnerAction(Core.Entities.OrganizationUser organizationUser)\n    {\n        return async (DbConnection connection, DbTransaction transaction) =>\n        {\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n            dbContext.Database.SetDbConnection(connection);\n            await dbContext.Database.UseTransactionAsync(transaction);\n\n            var efOrganizationUser = await dbContext.OrganizationUsers.FindAsync(organizationUser.Id);\n            if (efOrganizationUser is null)\n            {\n                throw new InvalidOperationException($\"OrganizationUser {organizationUser.Id} was not found during owner confirmation.\");\n            }\n\n            efOrganizationUser.Status = organizationUser.Status;\n            efOrganizationUser.UserId = organizationUser.UserId;\n            efOrganizationUser.Key = organizationUser.Key;\n            efOrganizationUser.Email = organizationUser.Email;\n\n            await dbContext.SaveChangesAsync();\n        };\n    }\n#nullable disable\n\n\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing AdminConsoleEntities = Bit.Core.AdminConsole.Entities;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;\n\npublic class PolicyRepository : Repository<AdminConsoleEntities.Policy, Policy, Guid>, IPolicyRepository\n{\n    public PolicyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Policies)\n    { }\n\n    public async Task<AdminConsoleEntities.Policy> GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var results = await dbContext.Policies\n                .FirstOrDefaultAsync(p => p.OrganizationId == organizationId && p.Type == type);\n            return Mapper.Map<AdminConsoleEntities.Policy>(results);\n        }\n    }\n\n    public async Task<ICollection<AdminConsoleEntities.Policy>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var results = await dbContext.Policies\n                .Where(p => p.OrganizationId == organizationId)\n                .ToListAsync();\n            return Mapper.Map<List<AdminConsoleEntities.Policy>>(results);\n        }\n    }\n\n    public async Task<ICollection<AdminConsoleEntities.Policy>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var query = new PolicyReadByUserIdQuery(userId);\n            var results = await query.Run(dbContext).ToListAsync();\n            return Mapper.Map<List<AdminConsoleEntities.Policy>>(results);\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var givenOrgUsers =\n            from ou in dbContext.OrganizationUsers\n            where ou.OrganizationId == organizationId\n            from u in dbContext.Users\n            where\n                (u.Email == ou.Email && ou.Email != null)\n                || (ou.UserId == u.Id && ou.UserId != null)\n\n            select new\n            {\n                ou.Id,\n                ou.OrganizationId,\n                UserId = u.Id,\n                u.Email\n            };\n\n        var orgUsersLinkedByUserId =\n            from ou in dbContext.OrganizationUsers\n            join gou in givenOrgUsers\n                on ou.UserId equals gou.UserId\n            select new\n            {\n                ou.Id,\n                ou.OrganizationId,\n                gou.UserId,\n                ou.Type,\n                ou.Status,\n                ou.Permissions\n            };\n\n        var orgUsersLinkedByEmail =\n            from ou in dbContext.OrganizationUsers\n            join gou in givenOrgUsers\n                on ou.Email equals gou.Email\n            select new\n            {\n                ou.Id,\n                ou.OrganizationId,\n                gou.UserId,\n                ou.Type,\n                ou.Status,\n                ou.Permissions\n            };\n\n        var allAffectedOrgUsers = orgUsersLinkedByEmail.Union(orgUsersLinkedByUserId);\n\n        var providerOrganizations = from pu in dbContext.ProviderUsers\n                                    join po in dbContext.ProviderOrganizations\n                                        on pu.ProviderId equals po.ProviderId\n                                    join ou in allAffectedOrgUsers\n                                        on pu.UserId equals ou.UserId\n                                    where pu.UserId == ou.UserId\n                                    select new\n                                    {\n                                        pu.UserId,\n                                        po.OrganizationId\n                                    };\n\n        var policyWithAffectedUsers =\n            from p in dbContext.Policies\n            join o in dbContext.Organizations\n                on p.OrganizationId equals o.Id\n            join ou in allAffectedOrgUsers\n                on o.Id equals ou.OrganizationId\n            where p.Enabled\n                   && o.Enabled\n                   && o.UsePolicies\n                   && p.Type == policyType\n            select new OrganizationPolicyDetails\n            {\n                UserId = ou.UserId,\n                OrganizationUserId = ou.Id,\n                OrganizationId = p.OrganizationId,\n                PolicyType = p.Type,\n                PolicyData = p.Data,\n                OrganizationUserType = ou.Type,\n                OrganizationUserStatus = ou.Status,\n                OrganizationUserPermissionsData = ou.Permissions,\n                IsProvider = providerOrganizations.Any(po => po.OrganizationId == p.OrganizationId)\n            };\n\n        return await policyWithAffectedUsers.ToListAsync();\n    }\n\n    public async Task<IEnumerable<OrganizationPolicyDetails>> GetPolicyDetailsByUserIdsAndPolicyType(\n        IEnumerable<Guid> userIds, PolicyType policyType)\n    {\n        ArgumentNullException.ThrowIfNull(userIds);\n\n        var userIdsList = userIds.Where(id => id != Guid.Empty).ToList();\n\n        if (userIdsList.Count == 0)\n        {\n            return [];\n        }\n\n        using var scope = ServiceScopeFactory.CreateScope();\n        await using var dbContext = GetDatabaseContext(scope);\n\n        // Get provider relationships\n        var providerLookup = await (from pu in dbContext.ProviderUsers\n                                    join po in dbContext.ProviderOrganizations on pu.ProviderId equals po.ProviderId\n                                    where pu.UserId != null && userIdsList.Contains(pu.UserId.Value)\n                                    select new { pu.UserId, po.OrganizationId })\n            .ToListAsync();\n\n        // Hashset for lookup\n        var providerSet = new HashSet<(Guid UserId, Guid OrganizationId)>(\n            providerLookup.Select(p => (p.UserId!.Value, p.OrganizationId)));\n\n        // Branch 1: Accepted users\n        var acceptedUsers = await (from p in dbContext.Policies\n                                   join ou in dbContext.OrganizationUsers on p.OrganizationId equals ou.OrganizationId\n                                   join o in dbContext.Organizations on p.OrganizationId equals o.Id\n                                   where p.Enabled\n                                         && p.Type == policyType\n                                         && o.Enabled\n                                         && o.UsePolicies\n                                         && ou.Status != OrganizationUserStatusType.Invited\n                                         && ou.UserId != null\n                                         && userIdsList.Contains(ou.UserId.Value)\n                                   select new\n                                   {\n                                       OrganizationUserId = ou.Id,\n                                       OrganizationId = p.OrganizationId,\n                                       PolicyType = p.Type,\n                                       PolicyData = p.Data,\n                                       OrganizationUserType = ou.Type,\n                                       OrganizationUserStatus = ou.Status,\n                                       OrganizationUserPermissionsData = ou.Permissions,\n                                       UserId = ou.UserId.Value\n                                   }).ToListAsync();\n\n        // Branch 2: Invited users\n        var invitedUsers = await (from p in dbContext.Policies\n                                  join ou in dbContext.OrganizationUsers on p.OrganizationId equals ou.OrganizationId\n                                  join o in dbContext.Organizations on p.OrganizationId equals o.Id\n                                  join u in dbContext.Users on ou.Email equals u.Email\n                                  where p.Enabled\n                                        && o.Enabled\n                                        && o.UsePolicies\n                                        && ou.Status == OrganizationUserStatusType.Invited\n                                        && userIdsList.Contains(u.Id)\n                                        && p.Type == policyType\n                                  select new\n                                  {\n                                      OrganizationUserId = ou.Id,\n                                      OrganizationId = p.OrganizationId,\n                                      PolicyType = p.Type,\n                                      PolicyData = p.Data,\n                                      OrganizationUserType = ou.Type,\n                                      OrganizationUserStatus = ou.Status,\n                                      OrganizationUserPermissionsData = ou.Permissions,\n                                      UserId = u.Id\n                                  }).ToListAsync();\n\n        // Combine results with the provider lookup\n        var allResults = acceptedUsers.Concat(invitedUsers)\n            .Select(item => new OrganizationPolicyDetails\n            {\n                OrganizationUserId = item.OrganizationUserId,\n                OrganizationId = item.OrganizationId,\n                PolicyType = item.PolicyType,\n                PolicyData = item.PolicyData,\n                OrganizationUserType = item.OrganizationUserType,\n                OrganizationUserStatus = item.OrganizationUserStatus,\n                OrganizationUserPermissionsData = item.OrganizationUserPermissionsData,\n                UserId = item.UserId,\n                IsProvider = providerSet.Contains((item.UserId, item.OrganizationId))\n            });\n\n        return allResults.ToList();\n    }\n\n    public async Task<IEnumerable<PolicyDetails>> GetPolicyDetailsByUserIdAndPolicyTypeAsync(Guid userId, PolicyType policyType)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        // Get user email for invited user matching\n        var userEmail = await dbContext.Users\n            .Where(u => u.Id == userId)\n            .Select(u => u.Email)\n            .FirstOrDefaultAsync();\n\n        // Get provider relationships\n        var providerOrganizationIds = await (from pu in dbContext.ProviderUsers\n                                             join po in dbContext.ProviderOrganizations on pu.ProviderId equals po.ProviderId\n                                             where pu.UserId == userId\n                                             select po.OrganizationId)\n            .Distinct()\n            .ToListAsync();\n\n        var providerSet = new HashSet<Guid>(providerOrganizationIds);\n\n        // Get organization users (both confirmed/accepted and invited)\n        var orgUsersQuery = dbContext.OrganizationUsers\n            .Where(ou => (ou.Status != OrganizationUserStatusType.Invited && ou.UserId == userId) ||\n                         (ou.Status == OrganizationUserStatusType.Invited && ou.Email == userEmail && userEmail != null));\n\n        // Join with policies and organizations\n        var query = from policy in dbContext.Policies\n                    join orgUser in orgUsersQuery on policy.OrganizationId equals orgUser.OrganizationId\n                    join org in dbContext.Organizations on policy.OrganizationId equals org.Id\n                    where policy.Type == policyType\n                        && policy.Enabled\n                        && org.Enabled\n                        && org.UsePolicies\n                    select new PolicyDetails\n                    {\n                        OrganizationUserId = orgUser.Id,\n                        OrganizationId = policy.OrganizationId,\n                        PolicyType = policy.Type,\n                        PolicyData = policy.Data,\n                        OrganizationUserType = orgUser.Type,\n                        OrganizationUserStatus = orgUser.Status,\n                        OrganizationUserPermissionsData = orgUser.Permissions,\n                        IsProvider = providerSet.Contains(policy.OrganizationId)\n                    };\n\n        return await query.ToListAsync();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderOrganizationRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;\n\npublic class ProviderOrganizationRepository :\n    Repository<ProviderOrganization, Models.Provider.ProviderOrganization, Guid>, IProviderOrganizationRepository\n{\n    public ProviderOrganizationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, context => context.ProviderOrganizations)\n    { }\n\n    public async Task<ICollection<ProviderOrganization>> CreateManyAsync(IEnumerable<ProviderOrganization> providerOrganizations)\n    {\n        var entities = providerOrganizations.ToList();\n\n        if (!entities.Any())\n        {\n            return default;\n        }\n\n        foreach (var providerOrganization in entities)\n        {\n            providerOrganization.SetNewId();\n        }\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.AddRangeAsync(entities);\n            await dbContext.SaveChangesAsync();\n        }\n\n        return entities;\n    }\n\n    public async Task<ICollection<ProviderOrganizationOrganizationDetails>> GetManyDetailsByProviderAsync(Guid providerId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new ProviderOrganizationOrganizationDetailsReadByProviderIdQuery(providerId);\n            var data = await query.Run(dbContext).ToListAsync();\n            return data;\n        }\n    }\n\n    public async Task<ProviderOrganization> GetByOrganizationId(Guid organizationId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        return await GetDbSet(dbContext).Where(po => po.OrganizationId == organizationId).FirstOrDefaultAsync();\n    }\n\n    public async Task<IEnumerable<ProviderOrganizationProviderDetails>> GetManyByUserAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new ProviderOrganizationReadByUserIdQuery(userId);\n            var data = await query.Run(dbContext).ToListAsync();\n            return data;\n        }\n    }\n\n    public async Task<int> GetCountByOrganizationIdsAsync(IEnumerable<Guid> organizationIds)\n    {\n        var query = new ProviderOrganizationCountByOrganizationIdsQuery(organizationIds);\n        return await GetCountFromQuery(query);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;\n\npublic class ProviderRepository : Repository<Provider, Models.Provider.Provider, Guid>, IProviderRepository\n{\n\n    public ProviderRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, context => context.Providers)\n    { }\n\n    public override async Task DeleteAsync(Provider provider)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.UserBumpAccountRevisionDateByProviderIdAsync(provider.Id);\n            await dbContext.SaveChangesAsync();\n        }\n        await base.DeleteAsync(provider);\n    }\n\n    public async Task<Provider> GetByGatewayCustomerIdAsync(string gatewayCustomerId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var provider = await GetDbSet(dbContext)\n                .Where(e => e.GatewayCustomerId == gatewayCustomerId)\n                .FirstOrDefaultAsync();\n            return Mapper.Map<Provider>(provider);\n        }\n    }\n\n    public async Task<Provider> GetByGatewaySubscriptionIdAsync(string gatewaySubscriptionId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var provider = await GetDbSet(dbContext)\n                .Where(e => e.GatewaySubscriptionId == gatewaySubscriptionId)\n                .FirstOrDefaultAsync();\n            return Mapper.Map<Provider>(provider);\n        }\n    }\n\n    public async Task<Provider> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from p in dbContext.Providers\n                        join po in dbContext.ProviderOrganizations\n                            on p.Id equals po.ProviderId\n                        where po.OrganizationId == organizationId\n                        select p;\n            return await query.FirstOrDefaultAsync();\n        }\n    }\n\n    public async Task<ICollection<Provider>> SearchAsync(string name, string userEmail, int skip, int take)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = !string.IsNullOrWhiteSpace(userEmail) ?\n                (from p in dbContext.Providers\n                 join pu in dbContext.ProviderUsers\n                     on p.Id equals pu.ProviderId\n                 join u in dbContext.Users\n                     on pu.UserId equals u.Id\n                 where (string.IsNullOrWhiteSpace(name) || p.Name.Contains(name)) &&\n                     u.Email == userEmail\n                 orderby p.CreationDate descending\n                 select new { p, pu, u }).Skip(skip).Take(take).Select(x => x.p) :\n                (from p in dbContext.Providers\n                 where string.IsNullOrWhiteSpace(name) || p.Name.Contains(name)\n                 orderby p.CreationDate descending\n                 select new { p }).Skip(skip).Take(take).Select(x => x.p);\n            var providers = await query.ToArrayAsync();\n            return Mapper.Map<List<Provider>>(providers);\n        }\n    }\n\n    public async Task<ICollection<ProviderAbility>> GetManyAbilitiesAsync()\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await GetDbSet(dbContext)\n                .Select(e => new ProviderAbility\n                {\n                    Enabled = e.Enabled,\n                    Id = e.Id,\n                    UseEvents = e.UseEvents,\n                }).ToListAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderUserRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;\n\npublic class ProviderUserRepository :\n    Repository<ProviderUser, Models.Provider.ProviderUser, Guid>, IProviderUserRepository\n{\n    public ProviderUserRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.ProviderUsers)\n    { }\n\n    public override async Task DeleteAsync(ProviderUser providerUser)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.UserBumpAccountRevisionDateByProviderUserIdAsync(providerUser.Id);\n            await dbContext.SaveChangesAsync();\n        }\n        await base.DeleteAsync(providerUser);\n    }\n\n    public async Task<int> GetCountByProviderAsync(Guid providerId, string email, bool onlyRegisteredUsers)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from pu in dbContext.ProviderUsers\n                        join u in dbContext.Users\n                            on pu.UserId equals u.Id into u_g\n                        from u in u_g.DefaultIfEmpty()\n                        where pu.ProviderId == providerId &&\n                            ((!onlyRegisteredUsers && (pu.Email == email || u.Email == email)) ||\n                            (onlyRegisteredUsers && u.Email == email))\n                        select new { pu, u };\n            return await query.CountAsync();\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyAsync(IEnumerable<Guid> ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.ProviderUsers.Where(item => ids.Contains(item.Id));\n            return await query.ToArrayAsync();\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyByProviderAsync(Guid providerId, ProviderUserType? type = null)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.ProviderUsers.Where(pu => pu.ProviderId.Equals(providerId) &&\n                (type != null && pu.Type.Equals(type)));\n            return await query.ToArrayAsync();\n        }\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> providerUserIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            foreach (var providerUserId in providerUserIds)\n            {\n                await dbContext.UserBumpAccountRevisionDateByProviderUserIdAsync(providerUserId);\n            }\n            var entities = dbContext.ProviderUsers.Where(pu => providerUserIds.Contains(pu.Id));\n            dbContext.ProviderUsers.RemoveRange(entities);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyByUserAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from pu in dbContext.ProviderUsers\n                        where pu.UserId == userId\n                        select pu;\n            return await query.ToArrayAsync();\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n\n        var dbContext = GetDatabaseContext(scope);\n\n        var query = from pu in dbContext.ProviderUsers\n                    where pu.UserId != null && userIds.Contains(pu.UserId.Value)\n                    select pu;\n\n        return await query.ToArrayAsync();\n    }\n\n    public async Task<ProviderUser> GetByProviderUserAsync(Guid providerId, Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from pu in dbContext.ProviderUsers\n                        where pu.UserId == userId &&\n                            pu.ProviderId == providerId\n                        select pu;\n            return await query.FirstOrDefaultAsync();\n        }\n    }\n    public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = from pu in dbContext.ProviderUsers\n                       join u in dbContext.Users\n                           on pu.UserId equals u.Id into u_g\n                       from u in u_g.DefaultIfEmpty()\n                       select new { pu, u };\n            var data = await view\n                .Where(e => e.pu.ProviderId == providerId && (status == null || e.pu.Status == status))\n                .Select(e => new ProviderUserUserDetails\n                {\n                    Id = e.pu.Id,\n                    UserId = e.pu.UserId,\n                    ProviderId = e.pu.ProviderId,\n                    Name = e.u.Name,\n                    Email = e.u.Email ?? e.pu.Email,\n                    Status = e.pu.Status,\n                    Type = e.pu.Type,\n                    Permissions = e.pu.Permissions,\n                }).ToArrayAsync();\n            return data;\n        }\n    }\n\n    public async Task<ICollection<ProviderUserProviderDetails>> GetManyDetailsByUserAsync(Guid userId, ProviderUserStatusType? status = null)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new ProviderUserProviderDetailsReadByUserIdStatusQuery(userId, status);\n            var data = await query.Run(dbContext).ToArrayAsync();\n            return data;\n        }\n    }\n\n    public async Task<IEnumerable<ProviderUserPublicKey>> GetManyPublicKeysByProviderUserAsync(Guid providerId, IEnumerable<Guid> Ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new UserReadPublicKeysByProviderUserIdsQuery(providerId, Ids);\n            var data = await query.Run(dbContext).ToListAsync();\n            return data;\n        }\n    }\n\n    public async Task<IEnumerable<ProviderUserOrganizationDetails>> GetManyOrganizationDetailsByUserAsync(Guid userId, ProviderUserStatusType? status = null)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new ProviderUserOrganizationDetailsViewQuery();\n            var query = from ou in view.Run(dbContext)\n                        where ou.UserId == userId &&\n                              (status == null || ou.Status == status)\n                        select ou;\n            var organizationUsers = await query.ToListAsync();\n            return organizationUsers;\n        }\n    }\n\n    public async Task<int> GetCountByOnlyOwnerAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await dbContext.ProviderUsers\n                .Where(pu => pu.Type == ProviderUserType.ProviderAdmin && pu.Status == ProviderUserStatusType.Confirmed)\n                .GroupBy(pu => pu.UserId)\n                .Select(g => new { UserId = g.Key, ConfirmedOwnerCount = g.Count() })\n                .Where(oc => oc.UserId == userId && oc.ConfirmedOwnerCount == 1)\n                .CountAsync();\n        }\n    }\n\n    public async Task<ICollection<ProviderUser>> GetManyByOrganizationAsync(Guid organizationId, ProviderUserStatusType? status = null)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from pu in dbContext.ProviderUsers\n                        join po in dbContext.ProviderOrganizations\n                            on pu.ProviderId equals po.ProviderId\n                        where po.OrganizationId == organizationId &&\n                              (status == null || pu.Status == status)\n                        select pu;\n            return await query.ToArrayAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs",
    "content": "﻿using Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationUserOrganizationDetails>\n{\n    public IQueryable<OrganizationUserOrganizationDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from ou in dbContext.OrganizationUsers\n                    join o in dbContext.Organizations on ou.OrganizationId equals o.Id\n                    join su in dbContext.SsoUsers on new { ou.UserId, OrganizationId = (Guid?)ou.OrganizationId } equals new { UserId = (Guid?)su.UserId, su.OrganizationId } into su_g\n                    from su in su_g.DefaultIfEmpty()\n                    join po in dbContext.ProviderOrganizations on o.Id equals po.OrganizationId into po_g\n                    from po in po_g.DefaultIfEmpty()\n                    join p in dbContext.Providers on po.ProviderId equals p.Id into p_g\n                    from p in p_g.DefaultIfEmpty()\n                    join ss in dbContext.SsoConfigs on ou.OrganizationId equals ss.OrganizationId into ss_g\n                    from ss in ss_g.DefaultIfEmpty()\n                    join os in dbContext.OrganizationSponsorships on ou.Id equals os.SponsoringOrganizationUserId into os_g\n                    from os in os_g.DefaultIfEmpty()\n                    select new OrganizationUserOrganizationDetails\n                    {\n                        UserId = ou.UserId,\n                        OrganizationId = ou.OrganizationId,\n                        OrganizationUserId = ou.Id,\n                        Name = o.Name,\n                        Enabled = o.Enabled,\n                        PlanType = o.PlanType,\n                        UsePolicies = o.UsePolicies,\n                        UseSso = o.UseSso,\n                        UseKeyConnector = o.UseKeyConnector,\n                        UseScim = o.UseScim,\n                        UseGroups = o.UseGroups,\n                        UseDirectory = o.UseDirectory,\n                        UseEvents = o.UseEvents,\n                        UseTotp = o.UseTotp,\n                        Use2fa = o.Use2fa,\n                        UseApi = o.UseApi,\n                        UseResetPassword = o.UseResetPassword,\n                        UseSecretsManager = o.UseSecretsManager,\n                        SelfHost = o.SelfHost,\n                        UsersGetPremium = o.UsersGetPremium,\n                        UseCustomPermissions = o.UseCustomPermissions,\n                        Seats = o.Seats,\n                        MaxCollections = o.MaxCollections,\n                        MaxStorageGb = o.MaxStorageGb,\n                        Identifier = o.Identifier,\n                        Key = ou.Key,\n                        ResetPasswordKey = ou.ResetPasswordKey,\n                        PublicKey = o.PublicKey,\n                        PrivateKey = o.PrivateKey,\n                        Status = ou.Status,\n                        Type = ou.Type,\n                        SsoExternalId = su.ExternalId,\n                        Permissions = ou.Permissions,\n                        ProviderId = p.Id,\n                        ProviderName = p.Name,\n                        ProviderType = p.Type,\n                        SsoEnabled = ss.Enabled,\n                        SsoConfig = ss.Data,\n                        FamilySponsorshipFriendlyName = os.FriendlyName,\n                        FamilySponsorshipLastSyncDate = os.LastSyncDate,\n                        FamilySponsorshipToDelete = os.ToDelete,\n                        FamilySponsorshipValidUntil = os.ValidUntil,\n                        AccessSecretsManager = ou.AccessSecretsManager,\n                        UsePasswordManager = o.UsePasswordManager,\n                        SmSeats = o.SmSeats,\n                        SmServiceAccounts = o.SmServiceAccounts,\n                        LimitCollectionCreation = o.LimitCollectionCreation,\n                        LimitCollectionDeletion = o.LimitCollectionDeletion,\n                        AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,\n                        UseRiskInsights = o.UseRiskInsights,\n                        UseAdminSponsoredFamilies = o.UseAdminSponsoredFamilies,\n                        LimitItemDeletion = o.LimitItemDeletion,\n                        IsAdminInitiated = os.IsAdminInitiated,\n                        UseOrganizationDomains = o.UseOrganizationDomains,\n                        UseAutomaticUserConfirmation = o.UseAutomaticUserConfirmation,\n                        UseDisableSMAdsForUsers = o.UseDisableSmAdsForUsers,\n                        UsePhishingBlocker = o.UsePhishingBlocker,\n                        UseMyItems = o.UseMyItems\n                    };\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadByClaimedOrganizationDomainsQuery.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class OrganizationUserReadByClaimedOrganizationDomainsQuery : IQuery<OrganizationUser>\n{\n    private readonly Guid _organizationId;\n\n    public OrganizationUserReadByClaimedOrganizationDomainsQuery(Guid organizationId)\n    {\n        _organizationId = organizationId;\n    }\n\n    public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)\n    {\n        var query = from ou in dbContext.OrganizationUsers\n                    join u in dbContext.Users on ou.UserId equals u.Id\n                    where ou.OrganizationId == _organizationId\n                          && ou.Status != OrganizationUserStatusType.Invited\n                          && dbContext.OrganizationDomains\n                              .Any(od => od.OrganizationId == _organizationId &&\n                                         od.VerifiedDate != null &&\n                                         u.Email.ToLower().EndsWith(\"@\" + od.DomainName.ToLower()))\n                    select ou;\n\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadCountByFreeOrganizationAdminUserQuery.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class OrganizationUserReadCountByFreeOrganizationAdminUserQuery : IQuery<OrganizationUser>\n{\n    private readonly Guid _userId;\n\n    public OrganizationUserReadCountByFreeOrganizationAdminUserQuery(Guid userId)\n    {\n        _userId = userId;\n    }\n\n    public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)\n    {\n        var query = from ou in dbContext.OrganizationUsers\n                    join o in dbContext.Organizations\n                        on ou.OrganizationId equals o.Id\n                    where ou.UserId == _userId &&\n                        (ou.Type == OrganizationUserType.Owner || ou.Type == OrganizationUserType.Admin) &&\n                        o.PlanType == PlanType.Free &&\n                        ou.Status == OrganizationUserStatusType.Confirmed\n                    select ou;\n\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadCountByOrganizationIdEmailQuery.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class OrganizationUserReadCountByOrganizationIdEmailQuery : IQuery<OrganizationUser>\n{\n    private readonly Guid _organizationId;\n    private readonly string _email;\n    private readonly bool _onlyUsers;\n\n    public OrganizationUserReadCountByOrganizationIdEmailQuery(Guid organizationId, string email, bool onlyUsers)\n    {\n        _organizationId = organizationId;\n        _email = email;\n        _onlyUsers = onlyUsers;\n    }\n\n    public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)\n    {\n        var query = from ou in dbContext.OrganizationUsers\n                    join u in dbContext.Users\n                        on ou.UserId equals u.Id into u_g\n                    from u in u_g.DefaultIfEmpty()\n                    where ou.OrganizationId == _organizationId &&\n                        ((!_onlyUsers && (ou.Email == _email || u.Email == _email))\n                         || (_onlyUsers && u.Email == _email))\n                    select ou;\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadCountByOrganizationIdQuery.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class OrganizationUserReadCountByOrganizationIdQuery : IQuery<OrganizationUser>\n{\n    private readonly Guid _organizationId;\n\n    public OrganizationUserReadCountByOrganizationIdQuery(Guid organizationId)\n    {\n        _organizationId = organizationId;\n    }\n\n    public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)\n    {\n        var query = from ou in dbContext.OrganizationUsers\n                    where ou.OrganizationId == _organizationId\n                    select ou;\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery : IQuery<OrganizationUser>\n{\n    private readonly Guid _organizationId;\n\n    public OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery(Guid organizationId)\n    {\n        _organizationId = organizationId;\n    }\n\n    public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)\n    {\n        var query = from ou in dbContext.OrganizationUsers\n                    where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited && ou.AccessSecretsManager == true\n                    select ou;\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserUpdateWithCollectionsQuery.cs",
    "content": "﻿"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserUserViewQuery.cs",
    "content": "﻿using Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class OrganizationUserUserDetailsViewQuery : IQuery<OrganizationUserUserDetails>\n{\n    public IQueryable<OrganizationUserUserDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from ou in dbContext.OrganizationUsers\n                    join u in dbContext.Users on ou.UserId equals u.Id into u_g\n                    from u in u_g.DefaultIfEmpty()\n                    join su in dbContext.SsoUsers on new { ou.UserId, OrganizationId = (Guid?)ou.OrganizationId } equals new { UserId = (Guid?)su.UserId, su.OrganizationId } into su_g\n                    from su in su_g.DefaultIfEmpty()\n                    select new { ou, u, su };\n        return query.Select(x => new OrganizationUserUserDetails\n        {\n            Id = x.ou.Id,\n            UserId = x.ou.UserId,\n            OrganizationId = x.ou.OrganizationId,\n            Name = x.u.Name,\n            Email = x.u.Email ?? x.ou.Email,\n            AvatarColor = x.u.AvatarColor,\n            TwoFactorProviders = x.u.TwoFactorProviders,\n            Premium = x.u.Premium,\n            Status = x.ou.Status,\n            Type = x.ou.Type,\n            ExternalId = x.ou.ExternalId,\n            SsoExternalId = x.su.ExternalId,\n            Permissions = x.ou.Permissions,\n            ResetPasswordKey = x.ou.ResetPasswordKey,\n            UsesKeyConnector = x.u != null && x.u.UsesKeyConnector,\n            AccessSecretsManager = x.ou.AccessSecretsManager,\n            HasMasterPassword = x.u != null && !string.IsNullOrWhiteSpace(x.u.MasterPassword)\n        });\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/PolicyReadByUserIdQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;\n\npublic class PolicyReadByUserIdQuery : IQuery<Policy>\n{\n    private readonly Guid _userId;\n\n    public PolicyReadByUserIdQuery(Guid userId)\n    {\n        _userId = userId;\n    }\n\n    public IQueryable<Policy> Run(DatabaseContext dbContext)\n    {\n        var query = from p in dbContext.Policies\n                    join ou in dbContext.OrganizationUsers\n                        on p.OrganizationId equals ou.OrganizationId\n                    join o in dbContext.Organizations\n                        on ou.OrganizationId equals o.Id\n                    where ou.UserId == _userId &&\n                        ou.Status == OrganizationUserStatusType.Confirmed\n                    select p;\n\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationCountByOrganizationIdsQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;\n\npublic class ProviderOrganizationCountByOrganizationIdsQuery : IQuery<ProviderOrganization>\n{\n    private readonly IEnumerable<Guid> _organizationIds;\n\n    public ProviderOrganizationCountByOrganizationIdsQuery(IEnumerable<Guid> organizationIds)\n    {\n        _organizationIds = organizationIds;\n    }\n\n    public IQueryable<ProviderOrganization> Run(DatabaseContext dbContext)\n    {\n        var query = from po in dbContext.ProviderOrganizations\n                    where _organizationIds.Contains(po.OrganizationId)\n                    select po;\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationOrganizationDetailsReadByProviderIdQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;\n\npublic class ProviderOrganizationOrganizationDetailsReadByProviderIdQuery : IQuery<ProviderOrganizationOrganizationDetails>\n{\n    private readonly Guid _providerId;\n    public ProviderOrganizationOrganizationDetailsReadByProviderIdQuery(Guid providerId)\n    {\n        _providerId = providerId;\n    }\n\n    public IQueryable<ProviderOrganizationOrganizationDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from po in dbContext.ProviderOrganizations\n                    join o in dbContext.Organizations\n                        on po.OrganizationId equals o.Id\n                    join ou in dbContext.OrganizationUsers\n                        on po.OrganizationId equals ou.OrganizationId\n                    where po.ProviderId == _providerId\n                    select new { po, o };\n        return query.Select(x => new ProviderOrganizationOrganizationDetails()\n        {\n            Id = x.po.Id,\n            ProviderId = x.po.ProviderId,\n            OrganizationId = x.po.OrganizationId,\n            OrganizationName = x.o.Name,\n            Key = x.po.Key,\n            Settings = x.po.Settings,\n            CreationDate = x.po.CreationDate,\n            RevisionDate = x.po.RevisionDate,\n            UserCount = x.o.OrganizationUsers.Count(ou => ou.Status == Core.Enums.OrganizationUserStatusType.Confirmed),\n            OccupiedSeats = x.o.OrganizationUsers.Count(ou => ou.Status >= 0),\n            Seats = x.o.Seats,\n            Plan = x.o.Plan,\n            PlanType = x.o.PlanType,\n            Status = x.o.Status\n        });\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderOrganizationReadByUserIdQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;\n\npublic class ProviderOrganizationReadByUserIdQuery : IQuery<ProviderOrganizationProviderDetails>\n{\n    private readonly Guid _userId;\n\n    public ProviderOrganizationReadByUserIdQuery(Guid userId)\n    {\n        _userId = userId;\n    }\n\n    public IQueryable<ProviderOrganizationProviderDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from po in dbContext.ProviderOrganizations\n                    join ou in dbContext.OrganizationUsers\n                        on po.OrganizationId equals ou.OrganizationId\n                    join p in dbContext.Providers\n                        on po.ProviderId equals p.Id\n                    where ou.UserId == _userId\n                    select new ProviderOrganizationProviderDetails\n                    {\n                        Id = po.Id,\n                        OrganizationId = po.OrganizationId,\n                        ProviderId = po.ProviderId,\n                        ProviderName = p.Name,\n                        ProviderType = p.Type\n                    };\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;\n\npublic class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrganizationDetails>\n{\n    public IQueryable<ProviderUserOrganizationDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from pu in dbContext.ProviderUsers\n                    join po in dbContext.ProviderOrganizations on pu.ProviderId equals po.ProviderId\n                    join o in dbContext.Organizations on po.OrganizationId equals o.Id\n                    join p in dbContext.Providers on pu.ProviderId equals p.Id\n                    join ss in dbContext.SsoConfigs on o.Id equals ss.OrganizationId into ss_g\n                    from ss in ss_g.DefaultIfEmpty()\n                    select new { pu, po, o, p, ss };\n        return query.Select(x => new ProviderUserOrganizationDetails\n        {\n            OrganizationId = x.po.OrganizationId,\n            UserId = x.pu.UserId,\n            Name = x.o.Name,\n            Enabled = x.o.Enabled,\n            UsePolicies = x.o.UsePolicies,\n            UseSso = x.o.UseSso,\n            UseKeyConnector = x.o.UseKeyConnector,\n            UseScim = x.o.UseScim,\n            UseGroups = x.o.UseGroups,\n            UseDirectory = x.o.UseDirectory,\n            UseEvents = x.o.UseEvents,\n            UseTotp = x.o.UseTotp,\n            Use2fa = x.o.Use2fa,\n            UseApi = x.o.UseApi,\n            UseResetPassword = x.o.UseResetPassword,\n            UseSecretsManager = x.o.UseSecretsManager,\n            UsePasswordManager = x.o.UsePasswordManager,\n            SelfHost = x.o.SelfHost,\n            UsersGetPremium = x.o.UsersGetPremium,\n            UseCustomPermissions = x.o.UseCustomPermissions,\n            Seats = x.o.Seats,\n            MaxCollections = x.o.MaxCollections,\n            MaxStorageGb = x.o.MaxStorageGb,\n            Identifier = x.o.Identifier,\n            Key = x.po.Key,\n            Status = x.pu.Status,\n            Type = x.pu.Type,\n            ProviderUserId = x.pu.Id,\n            PublicKey = x.o.PublicKey,\n            PrivateKey = x.o.PrivateKey,\n            ProviderId = x.p.Id,\n            ProviderName = x.p.Name,\n            PlanType = x.o.PlanType,\n            LimitCollectionCreation = x.o.LimitCollectionCreation,\n            LimitCollectionDeletion = x.o.LimitCollectionDeletion,\n            LimitItemDeletion = x.o.LimitItemDeletion,\n            AllowAdminAccessToAllCollectionItems = x.o.AllowAdminAccessToAllCollectionItems,\n            UseRiskInsights = x.o.UseRiskInsights,\n            ProviderType = x.p.Type,\n            UseOrganizationDomains = x.o.UseOrganizationDomains,\n            UseAdminSponsoredFamilies = x.o.UseAdminSponsoredFamilies,\n            UseAutomaticUserConfirmation = x.o.UseAutomaticUserConfirmation,\n            SsoEnabled = x.ss.Enabled,\n            SsoConfig = x.ss.Data,\n            UseDisableSMAdsForUsers = x.o.UseDisableSmAdsForUsers,\n            UsePhishingBlocker = x.o.UsePhishingBlocker,\n            UseMyItems = x.o.UseMyItems\n        });\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserProviderDetailsReadByUserIdStatusQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;\n\npublic class ProviderUserProviderDetailsReadByUserIdStatusQuery : IQuery<ProviderUserProviderDetails>\n{\n    private readonly Guid _userId;\n    private readonly ProviderUserStatusType? _status;\n    public ProviderUserProviderDetailsReadByUserIdStatusQuery(Guid userId, ProviderUserStatusType? status)\n    {\n        _userId = userId;\n        _status = status;\n    }\n\n    public IQueryable<ProviderUserProviderDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from pu in dbContext.ProviderUsers\n                    join p in dbContext.Providers\n                        on pu.ProviderId equals p.Id into p_g\n                    from p in p_g.DefaultIfEmpty()\n                    where pu.UserId == _userId && p.Status != ProviderStatusType.Pending && (_status == null || pu.Status == _status)\n                    select new { pu, p };\n        return query.Select(x => new ProviderUserProviderDetails()\n        {\n            UserId = x.pu.UserId,\n            ProviderId = x.pu.ProviderId,\n            Name = x.p.Name,\n            Key = x.pu.Key,\n            Status = x.pu.Status,\n            Type = x.pu.Type,\n            Enabled = x.p.Enabled,\n            Permissions = x.pu.Permissions,\n            UseEvents = x.p.UseEvents,\n            ProviderStatus = x.p.Status,\n            ProviderType = x.p.Type\n        });\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Configurations/AuthRequestConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Configurations;\n\npublic class AuthRequestConfiguration : IEntityTypeConfiguration<AuthRequest>\n{\n    public void Configure(EntityTypeBuilder<AuthRequest> builder)\n    {\n        builder.Property(ar => ar.Id).ValueGeneratedNever();\n        builder.ToTable(nameof(AuthRequest));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Configurations/GrantEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Configurations;\n\npublic class GrantEntityTypeConfiguration : IEntityTypeConfiguration<Grant>\n{\n    public void Configure(EntityTypeBuilder<Grant> builder)\n    {\n        builder\n            .HasKey(s => s.Id)\n            .HasName(\"PK_Grant\")\n            .IsClustered();\n\n        builder\n            .HasIndex(s => s.Key)\n            .IsUnique(true);\n\n        builder\n            .HasIndex(s => s.ExpirationDate)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(Grant));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Configurations/SsoUserEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Configurations;\n\npublic class SsoUserEntityTypeConfiguration : IEntityTypeConfiguration<SsoUser>\n{\n    public void Configure(EntityTypeBuilder<SsoUser> builder)\n    {\n        builder\n            .HasIndex(su => su.OrganizationId)\n            .IsClustered(false);\n\n        NpgsqlIndexBuilderExtensions.IncludeProperties(\n            builder.HasIndex(su => new { su.OrganizationId, su.ExternalId })\n                .IsUnique()\n                .IsClustered(false),\n            su => su.UserId);\n\n        builder\n            .HasIndex(su => new { su.OrganizationId, su.UserId })\n            .IsUnique()\n            .IsClustered(false);\n\n        builder.ToTable(nameof(SsoUser));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Models/AuthRequest.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Models;\n\npublic class AuthRequest : Core.Auth.Entities.AuthRequest\n{\n    public virtual User User { get; set; }\n    public virtual Device ResponseDevice { get; set; }\n    public virtual Organization Organization { get; set; }\n}\n\npublic class AuthRequestMapperProfile : Profile\n{\n    public AuthRequestMapperProfile()\n    {\n        CreateMap<Core.Auth.Entities.AuthRequest, AuthRequest>().ReverseMap();\n        CreateProjection<AuthRequest, OrganizationAdminAuthRequest>()\n            .ForMember(m => m.Email, opt => opt.MapFrom(t => t.User.Email))\n            .ForMember(m => m.OrganizationUserId, opt => opt.MapFrom(\n                t => t.User.OrganizationUsers.FirstOrDefault(ou => ou.OrganizationId == t.OrganizationId && ou.UserId == t.UserId).Id));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Models/EmergencyAccess.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Models;\n\npublic class EmergencyAccess : Core.Auth.Entities.EmergencyAccess\n{\n    public virtual User Grantee { get; set; }\n    public virtual User Grantor { get; set; }\n}\n\npublic class EmergencyAccessMapperProfile : Profile\n{\n    public EmergencyAccessMapperProfile()\n    {\n        CreateMap<Core.Auth.Entities.EmergencyAccess, EmergencyAccess>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Models/Grant.cs",
    "content": "﻿using AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Models;\n\npublic class Grant : Core.Auth.Entities.Grant\n{\n}\n\npublic class GrantMapperProfile : Profile\n{\n    public GrantMapperProfile()\n    {\n        CreateMap<Core.Auth.Entities.Grant, Grant>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Models/SsoConfig.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Models;\n\npublic class SsoConfig : Core.Auth.Entities.SsoConfig\n{\n    public virtual Organization Organization { get; set; }\n}\n\npublic class SsoConfigMapperProfile : Profile\n{\n    public SsoConfigMapperProfile()\n    {\n        CreateMap<Core.Auth.Entities.SsoConfig, SsoConfig>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Models/SsoUser.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Models;\n\npublic class SsoUser : Core.Auth.Entities.SsoUser\n{\n    public virtual Organization Organization { get; set; }\n    public virtual User User { get; set; }\n}\n\npublic class SsoUserMapperProfile : Profile\n{\n    public SsoUserMapperProfile()\n    {\n        CreateMap<Core.Auth.Entities.SsoUser, SsoUser>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Models/WebAuthnCredential.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Models;\n\npublic class WebAuthnCredential : Core.Auth.Entities.WebAuthnCredential\n{\n    public virtual User User { get; set; }\n}\n\npublic class WebAuthnCredentialMapperProfile : Profile\n{\n    public WebAuthnCredentialMapperProfile()\n    {\n        CreateMap<Core.Auth.Entities.WebAuthnCredential, WebAuthnCredential>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Repositories/AuthRequestRepository.cs",
    "content": "﻿using AutoMapper;\nusing AutoMapper.QueryableExtensions;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Repositories;\n\npublic class AuthRequestRepository : Repository<Core.Auth.Entities.AuthRequest, AuthRequest, Guid>, IAuthRequestRepository\n{\n    private readonly IGlobalSettings _globalSettings;\n    public AuthRequestRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper, IGlobalSettings globalSettings)\n        : base(serviceScopeFactory, mapper, context => context.AuthRequests)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public async Task<int> DeleteExpiredAsync(\n        TimeSpan userRequestExpiration, TimeSpan adminRequestExpiration, TimeSpan afterAdminApprovalExpiration)\n    {\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var expiredRequests = await dbContext.AuthRequests\n                .Where(a => (a.Type != AuthRequestType.AdminApproval && a.CreationDate.AddSeconds(userRequestExpiration.TotalSeconds) < DateTime.UtcNow)\n                    || (a.Type == AuthRequestType.AdminApproval && a.Approved != true && a.CreationDate.AddSeconds(adminRequestExpiration.TotalSeconds) < DateTime.UtcNow)\n                    || (a.Type == AuthRequestType.AdminApproval && a.Approved == true && a.ResponseDate!.Value.AddSeconds(afterAdminApprovalExpiration.TotalSeconds) < DateTime.UtcNow))\n                .ToListAsync();\n            dbContext.AuthRequests.RemoveRange(expiredRequests);\n            return await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<ICollection<Core.Auth.Entities.AuthRequest>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var userAuthRequests = await dbContext.AuthRequests.Where(a => a.UserId.Equals(userId)).ToListAsync();\n            return Mapper.Map<List<Core.Auth.Entities.AuthRequest>>(userAuthRequests);\n        }\n    }\n\n    public async Task<ICollection<OrganizationAdminAuthRequest>> GetManyPendingByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgUserAuthRequests = await (from ar in dbContext.AuthRequests\n                                             where ar.OrganizationId.Equals(organizationId) && ar.ResponseDate == null && ar.Type == AuthRequestType.AdminApproval\n                                             select ar).ProjectTo<OrganizationAdminAuthRequest>(Mapper.ConfigurationProvider).ToListAsync();\n\n            return orgUserAuthRequests;\n        }\n    }\n\n    public async Task<IEnumerable<PendingAuthRequestDetails>> GetManyPendingAuthRequestByUserId(Guid userId)\n    {\n        var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var mostRecentAuthRequests = await\n            (from authRequest in dbContext.AuthRequests\n             where authRequest.Type == AuthRequestType.AuthenticateAndUnlock\n                || authRequest.Type == AuthRequestType.Unlock\n             where authRequest.UserId == userId\n             where authRequest.CreationDate.AddMinutes(expirationMinutes) >= DateTime.UtcNow\n             group authRequest by authRequest.RequestDeviceIdentifier into groupedAuthRequests\n             select\n                 (from r in groupedAuthRequests\n                  join d in dbContext.Devices on new { r.RequestDeviceIdentifier, r.UserId }\n                                            equals new { RequestDeviceIdentifier = d.Identifier, d.UserId } into deviceJoin\n                  from dj in deviceJoin.DefaultIfEmpty() // This creates a left join allowing null for devices\n                  orderby r.CreationDate descending\n                  select new PendingAuthRequestDetails(r, dj.Id)).First()\n             ).ToListAsync();\n\n        mostRecentAuthRequests.RemoveAll(a => a.Approved != null);\n\n        return mostRecentAuthRequests;\n    }\n\n    public async Task<ICollection<OrganizationAdminAuthRequest>> GetManyAdminApprovalRequestsByManyIdsAsync(\n        Guid organizationId,\n        IEnumerable<Guid> ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgUserAuthRequests = await (from ar in dbContext.AuthRequests\n                                             where ar.OrganizationId.Equals(organizationId) && ids.Contains(ar.Id) && ar.Type == AuthRequestType.AdminApproval\n                                             select ar).ProjectTo<OrganizationAdminAuthRequest>(Mapper.ConfigurationProvider).ToListAsync();\n\n            return orgUserAuthRequests;\n        }\n    }\n\n    public async Task UpdateManyAsync(IEnumerable<Core.Auth.Entities.AuthRequest> authRequests)\n    {\n        if (!authRequests.Any())\n        {\n            return;\n        }\n\n        var entities = new List<AuthRequest>();\n        foreach (var authRequest in authRequests)\n        {\n            if (!authRequest.Id.Equals(default))\n            {\n                var entity = Mapper.Map<AuthRequest>(authRequest);\n                entities.Add(entity);\n            }\n        }\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            dbContext.UpdateRange(entities);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Repositories/EmergencyAccessRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.Data.SqlClient;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Repositories;\n\npublic class EmergencyAccessRepository : Repository<Core.Auth.Entities.EmergencyAccess, EmergencyAccess, Guid>, IEmergencyAccessRepository\n{\n    public EmergencyAccessRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.EmergencyAccesses)\n    { }\n\n    public async Task<int> GetCountByGrantorIdEmailAsync(Guid grantorId, string email, bool onlyRegisteredUsers)\n    {\n        var query = new EmergencyAccessReadCountByGrantorIdEmailQuery(grantorId, email, onlyRegisteredUsers);\n        return await GetCountFromQuery(query);\n    }\n\n    public override async Task DeleteAsync(Core.Auth.Entities.EmergencyAccess emergencyAccess)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            // TODO: in future, this probably is not necessary as we have no synced EA data. \n            // if we delete from here, also delete from stored proc as well + update repo tests.\n            await dbContext.UserBumpAccountRevisionDateByEmergencyAccessGranteeIdAsync(emergencyAccess.Id);\n            await dbContext.SaveChangesAsync();\n        }\n        await base.DeleteAsync(emergencyAccess);\n    }\n\n    public async Task<EmergencyAccessDetails?> GetDetailsByIdGrantorIdAsync(Guid id, Guid grantorId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new EmergencyAccessDetailsViewQuery();\n            var query = view.Run(dbContext).Where(ea =>\n                ea.Id == id &&\n                ea.GrantorId == grantorId\n            );\n            return await query.FirstOrDefaultAsync();\n        }\n    }\n\n    public async Task<EmergencyAccessDetails?> GetDetailsByIdAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new EmergencyAccessDetailsViewQuery();\n            var query = view.Run(dbContext).Where(ea => ea.Id == id);\n            return await query.FirstOrDefaultAsync();\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessDetails>> GetExpiredRecoveriesAsync()\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new EmergencyAccessDetailsViewQuery();\n            var query = view.Run(dbContext).Where(ea =>\n                ea.Status == EmergencyAccessStatusType.RecoveryInitiated\n            );\n            return await query.ToListAsync();\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGranteeIdAsync(Guid granteeId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new EmergencyAccessDetailsViewQuery();\n            var query = view.Run(dbContext).Where(ea =>\n                ea.GranteeId == granteeId\n            );\n            return await query.ToListAsync();\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGrantorIdAsync(Guid grantorId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new EmergencyAccessDetailsViewQuery();\n            var query = view.Run(dbContext).Where(ea =>\n                ea.GrantorId == grantorId\n            );\n            return await query.ToListAsync();\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByUserIdsAsync(ICollection<Guid> userIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new EmergencyAccessDetailsViewQuery();\n            var query = view.Run(dbContext).Where(ea =>\n                userIds.Contains(ea.GrantorId) ||\n                (ea.GranteeId.HasValue && userIds.Contains(ea.GranteeId.Value))\n            );\n            return await query.ToListAsync();\n        }\n    }\n\n    public async Task<ICollection<EmergencyAccessNotify>> GetManyToNotifyAsync()\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var view = new EmergencyAccessDetailsViewQuery();\n            var query = view.Run(dbContext).Where(ea =>\n                ea.Status == EmergencyAccessStatusType.RecoveryInitiated\n            );\n            var notifies = await query.Select(ea => new EmergencyAccessNotify\n            {\n                Id = ea.Id,\n                GrantorId = ea.GrantorId,\n                GranteeId = ea.GranteeId,\n                Email = ea.Email,\n                KeyEncrypted = ea.KeyEncrypted,\n                Type = ea.Type,\n                Status = ea.Status,\n                WaitTimeDays = ea.WaitTimeDays,\n                RecoveryInitiatedDate = ea.RecoveryInitiatedDate,\n                LastNotificationDate = ea.LastNotificationDate,\n                CreationDate = ea.CreationDate,\n                RevisionDate = ea.RevisionDate,\n                GranteeName = ea.GranteeName,\n                GranteeEmail = ea.GranteeEmail,\n                GrantorEmail = ea.GrantorEmail,\n            }).ToListAsync();\n            return notifies;\n        }\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(\n        Guid grantorId, IEnumerable<Core.Auth.Entities.EmergencyAccess> emergencyAccessKeys)\n    {\n        return async (SqlConnection connection, SqlTransaction transaction) =>\n        {\n            var newKeys = emergencyAccessKeys.ToList();\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n            var userEmergencyAccess = await GetDbSet(dbContext)\n                .Where(ea => ea.GrantorId == grantorId)\n                .ToListAsync();\n            var validEmergencyAccess = userEmergencyAccess\n                .Where(ea => newKeys.Any(eak => eak.Id == ea.Id));\n\n            foreach (var ea in validEmergencyAccess)\n            {\n                var eak = newKeys.First(eak => eak.Id == ea.Id);\n                ea.KeyEncrypted = eak.KeyEncrypted;\n            }\n\n            await dbContext.SaveChangesAsync();\n        };\n    }\n\n    /// <inheritdoc />\n    public async Task DeleteManyAsync(ICollection<Guid> emergencyAccessIds)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var entitiesToRemove = from ea in dbContext.EmergencyAccesses\n                               where emergencyAccessIds.Contains(ea.Id)\n                               select ea;\n\n        dbContext.EmergencyAccesses.RemoveRange(entitiesToRemove);\n        await dbContext.SaveChangesAsync();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Repositories/GrantRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Repositories;\n\npublic class GrantRepository : BaseEntityFrameworkRepository, IGrantRepository\n{\n    public GrantRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper)\n    { }\n\n    public async Task DeleteByKeyAsync(string key)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.Grants.Where(g => g.Key == key).ExecuteDeleteAsync();\n        }\n    }\n\n    public async Task DeleteManyAsync(string subjectId, string sessionId, string clientId, string type)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.Grants.Where(g =>\n                g.SubjectId == subjectId &&\n                g.ClientId == clientId &&\n                g.SessionId == sessionId &&\n                g.Type == type).ExecuteDeleteAsync();\n        }\n    }\n\n    public async Task<IGrant?> GetByKeyAsync(string key)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from g in dbContext.Grants\n                        where g.Key == key\n                        select g;\n            var grant = await query.FirstOrDefaultAsync();\n            return grant;\n        }\n    }\n\n    public async Task<ICollection<IGrant>> GetManyAsync(string subjectId, string sessionId, string clientId, string type)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from g in dbContext.Grants\n                        where g.SubjectId == subjectId &&\n                            g.ClientId == clientId &&\n                            g.SessionId == sessionId &&\n                            g.Type == type\n                        select g;\n            var grants = await query.ToListAsync();\n            return (ICollection<IGrant>)grants;\n        }\n    }\n\n    public async Task SaveAsync(IGrant obj)\n    {\n        if (obj is not Core.Auth.Entities.Grant gObj)\n        {\n            throw new ArgumentException(null, nameof(obj));\n        }\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var existingGrant = await (from g in dbContext.Grants\n                                       where g.Key == gObj.Key\n                                       select g).FirstOrDefaultAsync();\n            if (existingGrant != null)\n            {\n                gObj.Id = existingGrant.Id;\n                dbContext.Entry(existingGrant).CurrentValues.SetValues(gObj);\n            }\n            else\n            {\n                var entity = Mapper.Map<Grant>(gObj);\n                await dbContext.AddAsync(entity);\n                await dbContext.SaveChangesAsync();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Repositories/Queries/DeviceWithPendingAuthByUserIdQuery.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Repositories;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries;\n\npublic class DeviceWithPendingAuthByUserIdQuery\n{\n    public IQueryable<DeviceAuthDetails> GetQuery(\n        DatabaseContext dbContext,\n        Guid userId,\n        int expirationMinutes)\n    {\n        var devicesWithAuthQuery = (\n            from device in dbContext.Devices\n            where device.UserId == userId && device.Active\n            select new\n            {\n                device,\n                authRequest =\n                (\n                    from authRequest in dbContext.AuthRequests\n                    where authRequest.RequestDeviceIdentifier == device.Identifier\n                    where authRequest.Type == AuthRequestType.AuthenticateAndUnlock || authRequest.Type == AuthRequestType.Unlock\n                    where authRequest.Approved == null\n                    where authRequest.UserId == userId\n                    where authRequest.CreationDate.AddMinutes(expirationMinutes) > DateTime.UtcNow\n                    orderby authRequest.CreationDate descending\n                    select authRequest\n                ).First()\n            }).Select(deviceWithAuthRequest => new DeviceAuthDetails(\n                deviceWithAuthRequest.device,\n                deviceWithAuthRequest.authRequest.Id,\n                deviceWithAuthRequest.authRequest.CreationDate));\n\n        return devicesWithAuthQuery;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Repositories/Queries/EmergencyAccessDetailsViewQuery.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries;\n\npublic class EmergencyAccessDetailsViewQuery : IQuery<EmergencyAccessDetails>\n{\n    public IQueryable<EmergencyAccessDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from ea in dbContext.EmergencyAccesses\n                    join grantee in dbContext.Users\n                        on ea.GranteeId equals grantee.Id into grantee_g\n                    from grantee in grantee_g.DefaultIfEmpty()\n                    join grantor in dbContext.Users\n                        on ea.GrantorId equals grantor.Id into grantor_g\n                    from grantor in grantor_g.DefaultIfEmpty()\n                    select new { ea, grantee, grantor };\n        return query.Select(x => new EmergencyAccessDetails\n        {\n            Id = x.ea.Id,\n            GrantorId = x.ea.GrantorId,\n            GranteeId = x.ea.GranteeId,\n            Email = x.ea.Email,\n            KeyEncrypted = x.ea.KeyEncrypted,\n            Type = x.ea.Type,\n            Status = x.ea.Status,\n            WaitTimeDays = x.ea.WaitTimeDays,\n            RecoveryInitiatedDate = x.ea.RecoveryInitiatedDate,\n            LastNotificationDate = x.ea.LastNotificationDate,\n            CreationDate = x.ea.CreationDate,\n            RevisionDate = x.ea.RevisionDate,\n            GranteeName = x.grantee.Name,\n            GranteeEmail = x.grantee.Email ?? x.ea.Email,\n            GranteeAvatarColor = x.grantee.AvatarColor,\n            GrantorName = x.grantor.Name,\n            GrantorEmail = x.grantor.Email,\n            GrantorAvatarColor = x.grantor.AvatarColor,\n        });\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Repositories/Queries/EmergencyAccessReadCountByGrantorIdEmailQuery.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries;\n\npublic class EmergencyAccessReadCountByGrantorIdEmailQuery : IQuery<EmergencyAccess>\n{\n    private readonly Guid _grantorId;\n    private readonly string _email;\n    private readonly bool _onlyRegisteredUsers;\n\n    public EmergencyAccessReadCountByGrantorIdEmailQuery(Guid grantorId, string email, bool onlyRegisteredUsers)\n    {\n        _grantorId = grantorId;\n        _email = email;\n        _onlyRegisteredUsers = onlyRegisteredUsers;\n    }\n\n    public IQueryable<EmergencyAccess> Run(DatabaseContext dbContext)\n    {\n        var query = from ea in dbContext.EmergencyAccesses\n                    join u in dbContext.Users\n                        on ea.GranteeId equals u.Id into u_g\n                    from u in u_g.DefaultIfEmpty()\n                    where ea.GrantorId == _grantorId &&\n                        ((!_onlyRegisteredUsers && (ea.Email == _email || u.Email == _email))\n                         || (_onlyRegisteredUsers && u.Email == _email))\n                    select ea;\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Repositories/SsoConfigRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class SsoConfigRepository : Repository<Core.Auth.Entities.SsoConfig, SsoConfig, long>, ISsoConfigRepository\n{\n    public SsoConfigRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.SsoConfigs)\n    { }\n\n    public async Task<Core.Auth.Entities.SsoConfig?> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var ssoConfig = await GetDbSet(dbContext).SingleOrDefaultAsync(sc => sc.OrganizationId == organizationId);\n            return Mapper.Map<Core.Auth.Entities.SsoConfig>(ssoConfig);\n        }\n    }\n\n    public async Task<Core.Auth.Entities.SsoConfig?> GetByIdentifierAsync(string identifier)\n    {\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var ssoConfig = await GetDbSet(dbContext).SingleOrDefaultAsync(sc => sc.Organization.Identifier == identifier);\n            return Mapper.Map<Core.Auth.Entities.SsoConfig>(ssoConfig);\n        }\n    }\n\n    public async Task<ICollection<Core.Auth.Entities.SsoConfig>> GetManyByRevisionNotBeforeDate(DateTime? notBefore)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var ssoConfigs = await GetDbSet(dbContext).Where(sc => sc.Enabled && sc.RevisionDate >= notBefore).ToListAsync();\n            return Mapper.Map<List<Core.Auth.Entities.SsoConfig>>(ssoConfigs);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Repositories/SsoUserRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class SsoUserRepository : Repository<Core.Auth.Entities.SsoUser, SsoUser, long>, ISsoUserRepository\n{\n    public SsoUserRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.SsoUsers)\n    { }\n\n    public async Task DeleteAsync(Guid userId, Guid? organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.SsoUsers\n                .Where(su => su.UserId == userId && su.OrganizationId == organizationId)\n                .ExecuteDeleteAsync();\n        }\n    }\n\n    public async Task<Core.Auth.Entities.SsoUser?> GetByUserIdOrganizationIdAsync(Guid organizationId, Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await GetDbSet(dbContext)\n                .FirstOrDefaultAsync(e => e.OrganizationId == organizationId && e.UserId == userId);\n            return entity;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Auth.Repositories;\n\npublic class WebAuthnCredentialRepository : Repository<Core.Auth.Entities.WebAuthnCredential, WebAuthnCredential, Guid>, IWebAuthnCredentialRepository\n{\n    public WebAuthnCredentialRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (context) => context.WebAuthnCredentials)\n    { }\n\n    public async Task<Core.Auth.Entities.WebAuthnCredential?> GetByIdAsync(Guid id, Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.WebAuthnCredentials.Where(d => d.Id == id && d.UserId == userId);\n            var cred = await query.FirstOrDefaultAsync();\n            return Mapper.Map<Core.Auth.Entities.WebAuthnCredential>(cred);\n        }\n    }\n\n    public async Task<ICollection<Core.Auth.Entities.WebAuthnCredential>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.WebAuthnCredentials.Where(d => d.UserId == userId);\n            var creds = await query.ToListAsync();\n            return Mapper.Map<List<Core.Auth.Entities.WebAuthnCredential>>(creds);\n        }\n    }\n\n    public async Task<bool> UpdateAsync(Core.Auth.Entities.WebAuthnCredential credential)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var cred = await dbContext.WebAuthnCredentials\n                                .FirstOrDefaultAsync(d => d.Id == credential.Id &&\n                                                          d.UserId == credential.UserId);\n            if (cred == null)\n            {\n                return false;\n            }\n\n            cred.EncryptedPrivateKey = credential.EncryptedPrivateKey;\n            cred.EncryptedPublicKey = credential.EncryptedPublicKey;\n            cred.EncryptedUserKey = credential.EncryptedUserKey;\n\n            await dbContext.SaveChangesAsync();\n            return true;\n        }\n    }\n\n    public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<WebAuthnLoginRotateKeyData> credentials)\n    {\n        return async (_, _) =>\n        {\n            var newCreds = credentials.ToList();\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n\n            var newCredIds = newCreds.Select(nwc => nwc.Id).ToList();\n            var validUserWebauthnCredentials = await GetDbSet(dbContext)\n                .Where(wc => wc.UserId == userId && newCredIds.Contains(wc.Id))\n                .ToListAsync();\n\n            foreach (var wc in validUserWebauthnCredentials)\n            {\n                var nwc = newCreds.First(eak => eak.Id == wc.Id);\n                wc.EncryptedPublicKey = nwc.EncryptedPublicKey;\n                wc.EncryptedUserKey = nwc.EncryptedUserKey;\n            }\n\n            await dbContext.SaveChangesAsync();\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Configurations/ClientOrganizationMigrationRecordEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Billing.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Configurations;\n\npublic class ClientOrganizationMigrationRecordEntityTypeConfiguration : IEntityTypeConfiguration<ClientOrganizationMigrationRecord>\n{\n    public void Configure(EntityTypeBuilder<ClientOrganizationMigrationRecord> builder)\n    {\n        builder\n            .Property(c => c.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(migrationRecord => new { migrationRecord.ProviderId, migrationRecord.OrganizationId })\n            .IsUnique();\n\n        builder.ToTable(nameof(ClientOrganizationMigrationRecord));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Configurations/OrganizationInstallationEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Billing.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Configurations;\n\npublic class OrganizationInstallationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationInstallation>\n{\n    public void Configure(EntityTypeBuilder<OrganizationInstallation> builder)\n    {\n        builder\n            .Property(oi => oi.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasKey(oi => oi.Id)\n            .IsClustered();\n\n        builder\n            .HasIndex(oi => oi.OrganizationId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(oi => oi.InstallationId)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(OrganizationInstallation));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Configurations/ProviderInvoiceItemEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Billing.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Configurations;\n\npublic class ProviderInvoiceItemEntityTypeConfiguration : IEntityTypeConfiguration<ProviderInvoiceItem>\n{\n    public void Configure(EntityTypeBuilder<ProviderInvoiceItem> builder)\n    {\n        builder\n            .Property(t => t.Id)\n            .ValueGeneratedNever();\n\n        builder.ToTable(nameof(ProviderInvoiceItem));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Configurations/ProviderPlanEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Billing.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Configurations;\n\npublic class ProviderPlanEntityTypeConfiguration : IEntityTypeConfiguration<ProviderPlan>\n{\n    public void Configure(EntityTypeBuilder<ProviderPlan> builder)\n    {\n        builder\n            .Property(t => t.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(providerPlan => new { providerPlan.Id, providerPlan.PlanType })\n            .IsUnique();\n\n        builder.ToTable(nameof(ProviderPlan));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Configurations/SubscriptionDiscountEntityTypeConfiguration.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Infrastructure.EntityFramework.Billing.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.ChangeTracking;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Configurations;\n\npublic class SubscriptionDiscountEntityTypeConfiguration : IEntityTypeConfiguration<SubscriptionDiscount>\n{\n    public void Configure(EntityTypeBuilder<SubscriptionDiscount> builder)\n    {\n        builder\n            .Property(t => t.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(sd => sd.StripeCouponId)\n            .IsUnique();\n\n        builder\n            .Property(sd => sd.StripeProductIds)\n            .HasConversion(\n                v => v == null ? null : JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),\n                v => v == null ? null : JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null),\n                new ValueComparer<ICollection<string>?>(\n                    (c1, c2) => (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.SequenceEqual(c2)),\n                    c => c == null ? 0 : c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),\n                    c => c == null ? null : c.ToList()));\n\n        builder\n            .Property(sd => sd.PercentOff)\n            .HasPrecision(5, 2);\n\n        builder\n            .HasIndex(sd => new { sd.StartDate, sd.EndDate })\n            .IsClustered(false)\n            .HasDatabaseName(\"IX_SubscriptionDiscount_DateRange\");\n\n        builder.ToTable(nameof(SubscriptionDiscount));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Models/ClientOrganizationMigrationRecord.cs",
    "content": "﻿using AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Models;\n\npublic class ClientOrganizationMigrationRecord : Core.Billing.Providers.Entities.ClientOrganizationMigrationRecord\n{\n\n}\n\npublic class ClientOrganizationMigrationRecordProfile : Profile\n{\n    public ClientOrganizationMigrationRecordProfile()\n    {\n        CreateMap<Core.Billing.Providers.Entities.ClientOrganizationMigrationRecord, ClientOrganizationMigrationRecord>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.Platform;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Models;\n\npublic class OrganizationInstallation : Core.Billing.Organizations.Entities.OrganizationInstallation\n{\n    public virtual Installation Installation { get; set; }\n    public virtual Organization Organization { get; set; }\n}\n\npublic class OrganizationInstallationMapperProfile : Profile\n{\n    public OrganizationInstallationMapperProfile()\n    {\n        CreateMap<Core.Billing.Organizations.Entities.OrganizationInstallation, OrganizationInstallation>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Models;\n\n// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global\npublic class ProviderInvoiceItem : Core.Billing.Providers.Entities.ProviderInvoiceItem\n{\n    public virtual Provider Provider { get; set; }\n}\n\npublic class ProviderInvoiceItemMapperProfile : Profile\n{\n    public ProviderInvoiceItemMapperProfile()\n    {\n        CreateMap<Core.Billing.Providers.Entities.ProviderInvoiceItem, ProviderInvoiceItem>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Models;\n\n// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global\npublic class ProviderPlan : Core.Billing.Providers.Entities.ProviderPlan\n{\n    public virtual Provider Provider { get; set; }\n}\n\npublic class ProviderPlanMapperProfile : Profile\n{\n    public ProviderPlanMapperProfile()\n    {\n        CreateMap<Core.Billing.Providers.Entities.ProviderPlan, ProviderPlan>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Models/SubscriptionDiscount.cs",
    "content": "﻿#nullable enable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Models;\n\n// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global\npublic class SubscriptionDiscount : Core.Billing.Subscriptions.Entities.SubscriptionDiscount\n{\n}\n\npublic class SubscriptionDiscountMapperProfile : Profile\n{\n    public SubscriptionDiscountMapperProfile()\n    {\n        CreateMap<Core.Billing.Subscriptions.Entities.SubscriptionDiscount, SubscriptionDiscount>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nusing EFClientOrganizationMigrationRecord = Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Repositories;\n\npublic class ClientOrganizationMigrationRecordRepository(\n    IMapper mapper,\n    IServiceScopeFactory serviceScopeFactory)\n    : Repository<ClientOrganizationMigrationRecord, EFClientOrganizationMigrationRecord, Guid>(\n        serviceScopeFactory,\n        mapper,\n        context => context.ClientOrganizationMigrationRecords), IClientOrganizationMigrationRecordRepository\n{\n    public async Task<ClientOrganizationMigrationRecord> GetByOrganizationId(Guid organizationId)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var query =\n            from clientOrganizationMigrationRecord in databaseContext.ClientOrganizationMigrationRecords\n            where clientOrganizationMigrationRecord.OrganizationId == organizationId\n            select clientOrganizationMigrationRecord;\n\n        return await query.FirstOrDefaultAsync();\n    }\n\n    public async Task<ICollection<ClientOrganizationMigrationRecord>> GetByProviderId(Guid providerId)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var query =\n            from clientOrganizationMigrationRecord in databaseContext.ClientOrganizationMigrationRecords\n            where clientOrganizationMigrationRecord.ProviderId == providerId\n            select clientOrganizationMigrationRecord;\n\n        return await query.ToArrayAsync();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.Billing.Organizations.Entities;\nusing Bit.Core.Billing.Organizations.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing EFOrganizationInstallation = Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Repositories;\n\npublic class OrganizationInstallationRepository(\n    IMapper mapper,\n    IServiceScopeFactory serviceScopeFactory) : Repository<OrganizationInstallation, EFOrganizationInstallation, Guid>(\n        serviceScopeFactory,\n        mapper,\n        context => context.OrganizationInstallations), IOrganizationInstallationRepository\n{\n    public async Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var query =\n            from organizationInstallation in databaseContext.OrganizationInstallations\n            where organizationInstallation.Id == installationId\n            select organizationInstallation;\n\n        return await query.FirstOrDefaultAsync();\n    }\n\n    public async Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var query =\n            from organizationInstallation in databaseContext.OrganizationInstallations\n            where organizationInstallation.OrganizationId == organizationId\n            select organizationInstallation;\n\n        return await query.ToArrayAsync();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Repositories/ProviderInvoiceItemRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing LinqToDB;\nusing Microsoft.Extensions.DependencyInjection;\nusing EFProviderInvoiceItem = Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Repositories;\n\npublic class ProviderInvoiceItemRepository(\n    IMapper mapper,\n    IServiceScopeFactory serviceScopeFactory)\n    : Repository<ProviderInvoiceItem, EFProviderInvoiceItem, Guid>(\n        serviceScopeFactory,\n        mapper,\n        context => context.ProviderInvoiceItems), IProviderInvoiceItemRepository\n{\n    public async Task<ICollection<ProviderInvoiceItem>> GetByInvoiceId(string invoiceId)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var query =\n            from providerInvoiceItem in databaseContext.ProviderInvoiceItems\n            where providerInvoiceItem.InvoiceId == invoiceId\n            select providerInvoiceItem;\n\n        return await query.ToArrayAsync();\n    }\n\n    public async Task<ICollection<ProviderInvoiceItem>> GetByProviderId(Guid providerId)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var query =\n            from providerInvoiceItem in databaseContext.ProviderInvoiceItems\n            where providerInvoiceItem.ProviderId == providerId\n            select providerInvoiceItem;\n\n        return await query.ToArrayAsync();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Repositories/ProviderPlanRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing EFProviderPlan = Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Repositories;\n\npublic class ProviderPlanRepository(\n    IMapper mapper,\n    IServiceScopeFactory serviceScopeFactory)\n    : Repository<ProviderPlan, EFProviderPlan, Guid>(\n        serviceScopeFactory,\n        mapper,\n        context => context.ProviderPlans), IProviderPlanRepository\n{\n    public async Task<ICollection<ProviderPlan>> GetByProviderId(Guid providerId)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var query =\n            from providerPlan in databaseContext.ProviderPlans\n            where providerPlan.ProviderId == providerId\n            select providerPlan;\n\n        return await query.ToArrayAsync();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Billing/Repositories/SubscriptionDiscountRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Billing.Subscriptions.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing EFSubscriptionDiscount = Bit.Infrastructure.EntityFramework.Billing.Models.SubscriptionDiscount;\n\nnamespace Bit.Infrastructure.EntityFramework.Billing.Repositories;\n\npublic class SubscriptionDiscountRepository(\n    IMapper mapper,\n    IServiceScopeFactory serviceScopeFactory)\n    : Repository<SubscriptionDiscount, EFSubscriptionDiscount, Guid>(\n        serviceScopeFactory,\n        mapper,\n        context => context.SubscriptionDiscounts), ISubscriptionDiscountRepository\n{\n    public async Task<ICollection<SubscriptionDiscount>> GetActiveDiscountsAsync()\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var query =\n            from subscriptionDiscount in databaseContext.SubscriptionDiscounts\n            where subscriptionDiscount.StartDate <= DateTime.UtcNow\n                && subscriptionDiscount.EndDate >= DateTime.UtcNow\n            select subscriptionDiscount;\n\n        var results = await query.ToArrayAsync();\n\n        return Mapper.Map<List<SubscriptionDiscount>>(results);\n    }\n\n    public async Task<SubscriptionDiscount?> GetByStripeCouponIdAsync(string stripeCouponId)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var query =\n            from subscriptionDiscount in databaseContext.SubscriptionDiscounts\n            where subscriptionDiscount.StripeCouponId == stripeCouponId\n            select subscriptionDiscount;\n\n        var result = await query.FirstOrDefaultAsync();\n\n        return result == null ? null : Mapper.Map<SubscriptionDiscount>(result);\n    }\n\n    public async Task<ICollection<SubscriptionDiscount>> ListAsync(int skip, int take)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n        var databaseContext = GetDatabaseContext(serviceScope);\n\n        var results = await databaseContext.SubscriptionDiscounts\n            .OrderByDescending(sd => sd.CreationDate)\n            .Skip(skip)\n            .Take(take)\n            .ToArrayAsync();\n\n        return Mapper.Map<List<SubscriptionDiscount>>(results);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Configurations/CacheEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Configurations;\n\npublic class CacheEntityTypeConfiguration : IEntityTypeConfiguration<Cache>\n{\n    public void Configure(EntityTypeBuilder<Cache> builder)\n    {\n        builder\n            .HasKey(s => s.Id)\n            .IsClustered();\n\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(s => s.ExpiresAtTime)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(Cache));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Configurations/DeviceEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Configurations;\n\npublic class DeviceEntityTypeConfiguration : IEntityTypeConfiguration<Device>\n{\n    public void Configure(EntityTypeBuilder<Device> builder)\n    {\n        builder\n            .HasIndex(d => d.UserId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(d => new { d.UserId, d.Identifier })\n            .IsUnique()\n            .IsClustered(false);\n\n        builder\n            .HasIndex(d => d.Identifier)\n            .IsClustered(false);\n\n        builder.Property(c => c.Active)\n            .ValueGeneratedNever()\n            .HasDefaultValue(true);\n\n        builder.ToTable(nameof(Device));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Configurations/OrganizationSponsorshipEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Configurations;\n\npublic class OrganizationSponsorshipEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationSponsorship>\n{\n    public void Configure(EntityTypeBuilder<OrganizationSponsorship> builder)\n    {\n        builder\n            .Property(o => o.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(o => o.SponsoringOrganizationUserId)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(OrganizationSponsorship));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Configurations/OrganizationUserEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Configurations;\n\npublic class OrganizationUserEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationUser>\n{\n    public void Configure(EntityTypeBuilder<OrganizationUser> builder)\n    {\n        builder\n            .Property(ou => ou.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(ou => ou.OrganizationId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(ou => ou.UserId)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(OrganizationUser));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Configurations/PlayItemEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Configurations;\n\npublic class PlayItemEntityTypeConfiguration : IEntityTypeConfiguration<PlayItem>\n{\n    public void Configure(EntityTypeBuilder<PlayItem> builder)\n    {\n        builder\n            .Property(pd => pd.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(pd => pd.PlayId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(pd => pd.UserId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(pd => pd.OrganizationId)\n            .IsClustered(false);\n\n        builder\n            .HasOne(pd => pd.User)\n            .WithMany()\n            .HasForeignKey(pd => pd.UserId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder\n            .HasOne(pd => pd.Organization)\n            .WithMany()\n            .HasForeignKey(pd => pd.OrganizationId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder\n            .ToTable(nameof(PlayItem))\n            .HasCheckConstraint(\n                \"CK_PlayItem_UserOrOrganization\",\n                \"(\\\"UserId\\\" IS NOT NULL AND \\\"OrganizationId\\\" IS NULL) OR (\\\"UserId\\\" IS NULL AND \\\"OrganizationId\\\" IS NOT NULL)\"\n            );\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Configurations/TransactionEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Configurations;\n\npublic class TransactionEntityTypeConfiguration : IEntityTypeConfiguration<Transaction>\n{\n    public void Configure(EntityTypeBuilder<Transaction> builder)\n    {\n        builder\n            .Property(t => t.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(t => t.UserId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(t => new { t.UserId, t.OrganizationId, t.CreationDate })\n            .IsClustered(false);\n\n        builder.ToTable(nameof(Transaction));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Configurations/UserEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Configurations;\n\npublic class UserEntityTypeConfiguration : IEntityTypeConfiguration<User>\n{\n    public void Configure(EntityTypeBuilder<User> builder)\n    {\n        builder\n            .Property(u => u.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(u => u.Email)\n            .IsUnique()\n            .IsClustered(false);\n\n        builder\n            .HasIndex(u => new { u.Premium, u.PremiumExpirationDate, u.RenewalReminderDate })\n            .IsClustered(false);\n\n        builder.HasIndex(u => u.GatewayCustomerId);\n        builder.HasIndex(u => u.GatewaySubscriptionId);\n\n        builder.ToTable(nameof(User));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Converters/DataProtectionConverter.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\n\nnamespace Bit.Infrastructure.EntityFramework.Converters;\npublic class DataProtectionConverter : ValueConverter<string, string>\n{\n    public DataProtectionConverter(IDataProtector dataProtector) :\n        base(s => Protect(dataProtector, s), s => Unprotect(dataProtector, s))\n    { }\n\n    private static string Protect(IDataProtector dataProtector, string value)\n    {\n        if (value?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? true)\n        {\n            return value;\n        }\n\n        return string.Concat(\n            Constants.DatabaseFieldProtectedPrefix, dataProtector.Protect(value));\n    }\n\n    private static string Unprotect(IDataProtector dataProtector, string value)\n    {\n        if (!value?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? true)\n        {\n            return value;\n        }\n\n        return dataProtector.Unprotect(\n            value.Substring(Constants.DatabaseFieldProtectedPrefix.Length));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Configurations/EventEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Configurations;\n\npublic class EventEntityTypeConfiguration : IEntityTypeConfiguration<Event>\n{\n    public void Configure(EntityTypeBuilder<Event> builder)\n    {\n        builder\n            .Property(e => e.Id)\n            .ValueGeneratedNever();\n\n        builder.HasKey(e => e.Id)\n            .IsClustered();\n\n        var index = builder.HasIndex(e => new { e.Date, e.OrganizationId, e.ActingUserId, e.CipherId })\n            .IsClustered(false)\n            .HasDatabaseName(\"IX_Event_DateOrganizationIdUserId\");\n\n        SqlServerIndexBuilderExtensions.IncludeProperties(\n            index,\n            e => new { e.ServiceAccountId, e.GrantedServiceAccountId });\n\n        builder.ToTable(nameof(Event));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationApplicationEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Dirt.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Configurations;\n\npublic class OrganizationApplicationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationApplication>\n{\n    public void Configure(EntityTypeBuilder<OrganizationApplication> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder.HasIndex(s => s.Id)\n            .IsClustered(true);\n\n        builder\n            .HasIndex(s => s.OrganizationId)\n            .IsClustered(false);\n\n        builder\n            .HasOne(s => s.Organization)\n            .WithMany()\n            .HasForeignKey(s => s.OrganizationId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder.ToTable(nameof(OrganizationApplication));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Configurations/OrganizationReportEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Dirt.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Configurations;\n\npublic class OrganizationReportEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationReport>\n{\n    public void Configure(EntityTypeBuilder<OrganizationReport> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder.HasIndex(s => s.Id)\n            .IsClustered(true);\n\n        builder\n            .HasIndex(s => s.OrganizationId)\n            .IsClustered(false);\n\n        builder\n            .HasOne(s => s.Organization)\n            .WithMany()\n            .HasForeignKey(s => s.OrganizationId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder.ToTable(nameof(OrganizationReport));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Configurations/PasswordHealthReportApplicationEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Dirt.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Configurations;\n\npublic class PasswordHealthReportApplicationEntityTypeConfiguration : IEntityTypeConfiguration<PasswordHealthReportApplication>\n{\n    public void Configure(EntityTypeBuilder<PasswordHealthReportApplication> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder.HasIndex(s => s.Id)\n            .IsClustered(true);\n\n        builder\n            .HasIndex(s => s.OrganizationId)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(PasswordHealthReportApplication));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Models/Event.cs",
    "content": "﻿using AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class Event : Core.Entities.Event\n{\n}\n\npublic class EventMapperProfile : Profile\n{\n    public EventMapperProfile()\n    {\n        CreateMap<Core.Entities.Event, Event>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Models/OrganizationApplication.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Models;\npublic class OrganizationApplication : Core.Dirt.Entities.OrganizationApplication\n{\n    public virtual Organization Organization { get; set; }\n}\n\npublic class OrganizationApplicationProfile : Profile\n{\n    public OrganizationApplicationProfile()\n    {\n        CreateMap<Core.Dirt.Entities.OrganizationApplication, OrganizationApplication>()\n            .ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegration.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Models;\n\npublic class OrganizationIntegration : Core.Dirt.Entities.OrganizationIntegration\n{\n    public virtual required Organization Organization { get; set; }\n}\n\npublic class OrganizationIntegrationMapperProfile : Profile\n{\n    public OrganizationIntegrationMapperProfile()\n    {\n        CreateMap<Core.Dirt.Entities.OrganizationIntegration, OrganizationIntegration>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Models/OrganizationIntegrationConfiguration.cs",
    "content": "﻿using AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Models;\n\npublic class OrganizationIntegrationConfiguration : Core.Dirt.Entities.OrganizationIntegrationConfiguration\n{\n    public virtual required OrganizationIntegration OrganizationIntegration { get; set; }\n}\n\npublic class OrganizationIntegrationConfigurationMapperProfile : Profile\n{\n    public OrganizationIntegrationConfigurationMapperProfile()\n    {\n        CreateMap<Core.Dirt.Entities.OrganizationIntegrationConfiguration, OrganizationIntegrationConfiguration>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Models/OrganizationReport.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Models;\npublic class OrganizationReport : Core.Dirt.Entities.OrganizationReport\n{\n    public virtual Organization Organization { get; set; }\n}\n\npublic class OrganizationReportProfile : Profile\n{\n    public OrganizationReportProfile()\n    {\n        CreateMap<Core.Dirt.Entities.OrganizationReport, OrganizationReport>()\n            .ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Models/PasswordHealthReportApplication.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Models;\n\npublic class PasswordHealthReportApplication : Core.Dirt.Entities.PasswordHealthReportApplication\n{\n    public virtual Organization Organization { get; set; }\n}\n\npublic class PasswordHealthReportApplicationProfile : Profile\n{\n    public PasswordHealthReportApplicationProfile()\n    {\n        CreateMap<Core.Dirt.Entities.PasswordHealthReportApplication, PasswordHealthReportApplication>()\n            .ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/OrganizationMemberBaseDetailRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Reports.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.Data.SqlClient;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt;\n\npublic class OrganizationMemberBaseDetailRepository : BaseEntityFrameworkRepository, IOrganizationMemberBaseDetailRepository\n{\n    public OrganizationMemberBaseDetailRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(\n        serviceScopeFactory,\n        mapper)\n    {\n    }\n\n    public async Task<IEnumerable<OrganizationMemberBaseDetail>> GetOrganizationMemberBaseDetailsByOrganizationId(\n        Guid organizationId)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var result = await dbContext.Set<OrganizationMemberBaseDetail>()\n            .FromSqlRaw(\"EXEC [dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId] @OrganizationId\",\n                new SqlParameter(\"@OrganizationId\", organizationId))\n            .ToListAsync();\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/EventRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing LinqToDB.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing Cipher = Bit.Core.Vault.Entities.Cipher;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class EventRepository : Repository<Core.Entities.Event, Event, Guid>, IEventRepository\n{\n    public EventRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Events)\n    { }\n\n    public async Task CreateAsync(IEvent e)\n    {\n        if (e is not Core.Entities.Event ev)\n        {\n            ev = new Core.Entities.Event(e);\n        }\n\n        await base.CreateAsync(ev);\n    }\n\n    public async Task CreateManyAsync(IEnumerable<IEvent> entities)\n    {\n        if (entities is null || !entities.Any())\n        {\n            return;\n        }\n\n        if (!entities.Skip(1).Any())\n        {\n            await CreateAsync(entities.First());\n            return;\n        }\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var tableEvents = entities.Select(e => e as Core.Entities.Event ?? new Core.Entities.Event(e));\n            var entityEvents = Mapper.Map<List<Event>>(tableEvents);\n            entityEvents.ForEach(e => e.SetNewId());\n            await dbContext.BulkCopyAsync(entityEvents);\n        }\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId, Guid serviceAccountId,\n        DateTime startDate, DateTime endDate,\n        PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = new EventReadPageByOrganizationIdServiceAccountIdQuery(organizationId, serviceAccountId,\n            startDate, endDate, beforeDate, pageOptions);\n        var events = await query.Run(dbContext).ToListAsync();\n\n        var result = new PagedResult<IEvent>();\n        if (events.Any() && events.Count >= pageOptions.PageSize)\n        {\n            result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n        }\n        result.Data.AddRange(events);\n        return result;\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new EventReadPageBySecretQuery(secret, startDate, endDate, beforeDate, pageOptions);\n            var events = await query.Run(dbContext).ToListAsync();\n\n            var result = new PagedResult<IEvent>();\n            if (events.Any() && events.Count >= pageOptions.PageSize)\n            {\n                result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n            }\n            result.Data.AddRange(events);\n            return result;\n        }\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project,\n    DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new EventReadPageByProjectQuery(project, startDate, endDate, beforeDate, pageOptions);\n            var events = await query.Run(dbContext).ToListAsync();\n\n            var result = new PagedResult<IEvent>();\n            if (events.Any() && events.Count >= pageOptions.PageSize)\n            {\n                result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n            }\n            result.Data.AddRange(events);\n            return result;\n        }\n    }\n\n\n    public async Task<PagedResult<IEvent>> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new EventReadPageByCipherIdQuery(cipher, startDate, endDate, beforeDate, pageOptions);\n            var events = await query.Run(dbContext).ToListAsync();\n\n            var result = new PagedResult<IEvent>();\n            if (events.Any() && events.Count >= pageOptions.PageSize)\n            {\n                result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n            }\n            result.Data.AddRange(events);\n            return result;\n        }\n    }\n\n\n    public async Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new EventReadPageByOrganizationIdActingUserIdQuery(organizationId, actingUserId,\n                startDate, endDate, beforeDate, pageOptions);\n            var events = await query.Run(dbContext).ToListAsync();\n\n            var result = new PagedResult<IEvent>();\n            if (events.Any() && events.Count >= pageOptions.PageSize)\n            {\n                result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n            }\n            result.Data.AddRange(events);\n            return result;\n        }\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByProviderAsync(Guid providerId, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new EventReadPageByProviderIdQuery(providerId, startDate,\n                endDate, beforeDate, pageOptions);\n            var events = await query.Run(dbContext).ToListAsync();\n\n            var result = new PagedResult<IEvent>();\n            if (events.Any() && events.Count >= pageOptions.PageSize)\n            {\n                result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n            }\n            result.Data.AddRange(events);\n            return result;\n        }\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByProviderActingUserAsync(Guid providerId, Guid actingUserId,\n        DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new EventReadPageByProviderIdActingUserIdQuery(providerId, actingUserId,\n                startDate, endDate, beforeDate, pageOptions);\n            var events = await query.Run(dbContext).ToListAsync();\n\n            var result = new PagedResult<IEvent>();\n            if (events.Any() && events.Count >= pageOptions.PageSize)\n            {\n                result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n            }\n            result.Data.AddRange(events);\n            return result;\n        }\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByOrganizationAsync(Guid organizationId, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new EventReadPageByOrganizationIdQuery(organizationId, startDate,\n                endDate, beforeDate, pageOptions);\n            var events = await query.Run(dbContext).ToListAsync();\n\n            var result = new PagedResult<IEvent>();\n            if (events.Any() && events.Count >= pageOptions.PageSize)\n            {\n                result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n            }\n            result.Data.AddRange(events);\n            return result;\n        }\n    }\n\n    public async Task<PagedResult<IEvent>> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        DateTime? beforeDate = null;\n        if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&\n            long.TryParse(pageOptions.ContinuationToken, out var binaryDate))\n        {\n            beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new EventReadPageByUserIdQuery(userId, startDate,\n                endDate, beforeDate, pageOptions);\n            var events = await query.Run(dbContext).ToListAsync();\n\n            var result = new PagedResult<IEvent>();\n            if (events.Any() && events.Count >= pageOptions.PageSize)\n            {\n                result.ContinuationToken = events.Last().Date.ToBinary().ToString();\n            }\n            result.Data.AddRange(events);\n            return result;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationApplicationRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Infrastructure.EntityFramework.Dirt.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing LinqToDB;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories;\n\npublic class OrganizationApplicationRepository :\n    Repository<Core.Dirt.Entities.OrganizationApplication, OrganizationApplication, Guid>,\n    IOrganizationApplicationRepository\n{\n    public OrganizationApplicationRepository(IServiceScopeFactory serviceScopeFactory,\n        IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationApplications)\n    { }\n\n    public async Task<ICollection<Core.Dirt.Entities.OrganizationApplication>> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var results = await dbContext.OrganizationApplications\n                .Where(p => p.OrganizationId == organizationId)\n                .ToListAsync();\n            return Mapper.Map<ICollection<Core.Dirt.Entities.OrganizationApplication>>(results);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationIntegrationConfigurationRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing OrganizationIntegrationConfiguration = Bit.Core.Dirt.Entities.OrganizationIntegrationConfiguration;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories;\n\npublic class OrganizationIntegrationConfigurationRepository : Repository<OrganizationIntegrationConfiguration, Dirt.Models.OrganizationIntegrationConfiguration, Guid>, IOrganizationIntegrationConfigurationRepository\n{\n    public OrganizationIntegrationConfigurationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, context => context.OrganizationIntegrationConfigurations)\n    { }\n\n    public async Task<List<OrganizationIntegrationConfigurationDetails>>\n        GetManyByEventTypeOrganizationIdIntegrationType(EventType eventType, Guid organizationId,\n            IntegrationType integrationType)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(\n                organizationId,\n                eventType,\n                integrationType\n                );\n            return await query.Run(dbContext).ToListAsync();\n        }\n    }\n\n    public async Task<List<OrganizationIntegrationConfigurationDetails>> GetAllConfigurationDetailsAsync()\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new OrganizationIntegrationConfigurationDetailsReadManyQuery();\n            return await query.Run(dbContext).ToListAsync();\n        }\n    }\n\n    public async Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(\n        Guid organizationIntegrationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery(\n                organizationIntegrationId\n            );\n            return await query.Run(dbContext).ToListAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationIntegrationRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing OrganizationIntegration = Bit.Core.Dirt.Entities.OrganizationIntegration;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories;\n\npublic class OrganizationIntegrationRepository :\n    Repository<OrganizationIntegration, Dirt.Models.OrganizationIntegration, Guid>,\n    IOrganizationIntegrationRepository\n{\n    public OrganizationIntegrationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationIntegrations)\n    {\n    }\n\n    public async Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new OrganizationIntegrationReadManyByOrganizationIdQuery(organizationId);\n            return await query.Run(dbContext).ToListAsync();\n        }\n    }\n\n    public async Task<OrganizationIntegration?> GetByTeamsConfigurationTenantIdTeamId(\n        string tenantId,\n        string teamId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery(tenantId: tenantId, teamId: teamId);\n            return await query.Run(dbContext).SingleOrDefaultAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing LinqToDB;\nusing Microsoft.Extensions.DependencyInjection;\n\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories;\n\npublic class OrganizationReportRepository :\n    Repository<OrganizationReport, Models.OrganizationReport, Guid>,\n    IOrganizationReportRepository\n{\n    public OrganizationReportRepository(IServiceScopeFactory serviceScopeFactory,\n        IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationReports)\n    { }\n\n    public async Task<OrganizationReport> GetLatestByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var result = await dbContext.OrganizationReports\n                .Where(p => p.OrganizationId == organizationId)\n                .OrderByDescending(p => p.RevisionDate)\n                .Take(1)\n                .FirstOrDefaultAsync();\n\n            if (result == null) return default;\n\n            return Mapper.Map<OrganizationReport>(result);\n        }\n    }\n\n    public async Task<OrganizationReport> UpdateSummaryDataAsync(Guid organizationId, Guid reportId, string summaryData)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            // Update only SummaryData and RevisionDate\n            await dbContext.OrganizationReports\n                .Where(p => p.Id == reportId && p.OrganizationId == organizationId)\n                .UpdateAsync(p => new Models.OrganizationReport\n                {\n                    SummaryData = summaryData,\n                    RevisionDate = DateTime.UtcNow\n                });\n\n            // Return the updated report\n            var updatedReport = await dbContext.OrganizationReports\n                .Where(p => p.Id == reportId)\n                .FirstOrDefaultAsync();\n\n            return Mapper.Map<OrganizationReport>(updatedReport);\n        }\n    }\n\n    public async Task<OrganizationReportSummaryDataResponse> GetSummaryDataAsync(Guid reportId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var result = await dbContext.OrganizationReports\n                .Where(p => p.Id == reportId)\n                .Select(p => new OrganizationReportSummaryDataResponse\n                {\n                    OrganizationId = p.OrganizationId,\n                    ContentEncryptionKey = p.ContentEncryptionKey,\n                    SummaryData = p.SummaryData,\n                    RevisionDate = p.RevisionDate\n                })\n                .FirstOrDefaultAsync();\n\n            return result;\n        }\n    }\n\n    public async Task<IEnumerable<OrganizationReportSummaryDataResponse>> GetSummaryDataByDateRangeAsync(\n        Guid organizationId,\n        DateTime startDate,\n        DateTime endDate)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var results = await dbContext.OrganizationReports\n                .Where(p => p.OrganizationId == organizationId &&\n                            p.RevisionDate >= startDate && p.RevisionDate <= endDate)\n                .Select(p => new OrganizationReportSummaryDataResponse\n                {\n                    OrganizationId = p.OrganizationId,\n                    ContentEncryptionKey = p.ContentEncryptionKey,\n                    SummaryData = p.SummaryData,\n                    RevisionDate = p.RevisionDate\n                })\n                .ToListAsync();\n\n            return results;\n        }\n    }\n\n    public async Task<OrganizationReportDataResponse> GetReportDataAsync(Guid reportId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var result = await dbContext.OrganizationReports\n                .Where(p => p.Id == reportId)\n                .Select(p => new OrganizationReportDataResponse\n                {\n                    ReportData = p.ReportData\n                })\n                .FirstOrDefaultAsync();\n\n            return result;\n        }\n    }\n\n    public async Task<OrganizationReport> UpdateReportDataAsync(Guid organizationId, Guid reportId, string reportData)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            // Update only ReportData and RevisionDate\n            await dbContext.OrganizationReports\n                .Where(p => p.Id == reportId && p.OrganizationId == organizationId)\n                .UpdateAsync(p => new Models.OrganizationReport\n                {\n                    ReportData = reportData,\n                    RevisionDate = DateTime.UtcNow\n                });\n\n            // Return the updated report\n            var updatedReport = await dbContext.OrganizationReports\n                .Where(p => p.Id == reportId)\n                .FirstOrDefaultAsync();\n\n            return Mapper.Map<OrganizationReport>(updatedReport);\n        }\n    }\n\n    public async Task<OrganizationReportApplicationDataResponse> GetApplicationDataAsync(Guid reportId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var result = await dbContext.OrganizationReports\n                .Where(p => p.Id == reportId)\n                .Select(p => new OrganizationReportApplicationDataResponse\n                {\n                    ApplicationData = p.ApplicationData\n                })\n                .FirstOrDefaultAsync();\n\n            return result;\n        }\n    }\n\n    public async Task<OrganizationReport> UpdateApplicationDataAsync(Guid organizationId, Guid reportId, string applicationData)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            // Update only ApplicationData and RevisionDate\n            await dbContext.OrganizationReports\n                .Where(p => p.Id == reportId && p.OrganizationId == organizationId)\n                .UpdateAsync(p => new Models.OrganizationReport\n                {\n                    ApplicationData = applicationData,\n                    RevisionDate = DateTime.UtcNow\n                });\n\n            // Return the updated report\n            var updatedReport = await dbContext.OrganizationReports\n                .Where(p => p.Id == reportId)\n                .FirstOrDefaultAsync();\n\n            return Mapper.Map<OrganizationReport>(updatedReport);\n        }\n    }\n\n    public Task UpdateMetricsAsync(Guid reportId, OrganizationReportMetricsData metrics)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            return dbContext.OrganizationReports\n                .Where(p => p.Id == reportId)\n                .UpdateAsync(p => new Models.OrganizationReport\n                {\n                    ApplicationCount = metrics.ApplicationCount,\n                    ApplicationAtRiskCount = metrics.ApplicationAtRiskCount,\n                    CriticalApplicationCount = metrics.CriticalApplicationCount,\n                    CriticalApplicationAtRiskCount = metrics.CriticalApplicationAtRiskCount,\n                    MemberCount = metrics.MemberCount,\n                    MemberAtRiskCount = metrics.MemberAtRiskCount,\n                    CriticalMemberCount = metrics.CriticalMemberCount,\n                    CriticalMemberAtRiskCount = metrics.CriticalMemberAtRiskCount,\n                    PasswordCount = metrics.PasswordCount,\n                    PasswordAtRiskCount = metrics.PasswordAtRiskCount,\n                    CriticalPasswordCount = metrics.CriticalPasswordCount,\n                    CriticalPasswordAtRiskCount = metrics.CriticalPasswordAtRiskCount,\n                    RevisionDate = DateTime.UtcNow\n                });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/PasswordHealthReportApplicationRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Infrastructure.EntityFramework.Dirt.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing LinqToDB;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories;\n\npublic class PasswordHealthReportApplicationRepository :\n    Repository<Core.Dirt.Entities.PasswordHealthReportApplication, PasswordHealthReportApplication, Guid>,\n    IPasswordHealthReportApplicationRepository\n{\n    public PasswordHealthReportApplicationRepository(IServiceScopeFactory serviceScopeFactory,\n        IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PasswordHealthReportApplications)\n    { }\n\n    public async Task<ICollection<Core.Dirt.Entities.PasswordHealthReportApplication>> GetByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var results = await dbContext.PasswordHealthReportApplications\n                .Where(p => p.OrganizationId == organizationId)\n                .ToListAsync();\n            return Mapper.Map<ICollection<Core.Dirt.Entities.PasswordHealthReportApplication>>(results);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByCipherIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Core.Vault.Entities;\nusing Event = Bit.Infrastructure.EntityFramework.Models.Event;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageByCipherIdQuery : IQuery<Event>\n{\n    private readonly Cipher _cipher;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageByCipherIdQuery(Cipher cipher, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        _cipher = cipher;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = null;\n        _pageOptions = pageOptions;\n    }\n\n    public EventReadPageByCipherIdQuery(Cipher cipher, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _cipher = cipher;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                ((!_cipher.OrganizationId.HasValue && !e.OrganizationId.HasValue) ||\n                (_cipher.OrganizationId.HasValue && _cipher.OrganizationId == e.OrganizationId)) &&\n                ((!_cipher.UserId.HasValue && !e.UserId.HasValue) ||\n                    (_cipher.UserId.HasValue && _cipher.UserId == e.UserId)) &&\n                _cipher.Id == e.CipherId\n                orderby e.Date descending\n                select e;\n        return q.Skip(0).Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByOrganizationIdActingUserIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageByOrganizationIdActingUserIdQuery : IQuery<Event>\n{\n    private readonly Guid _organizationId;\n    private readonly Guid _actingUserId;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageByOrganizationIdActingUserIdQuery(Guid organizationId, Guid actingUserId,\n            DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _organizationId = organizationId;\n        _actingUserId = actingUserId;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                (_beforeDate != null || e.Date <= _endDate) &&\n                (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                e.OrganizationId == _organizationId &&\n                e.ActingUserId == _actingUserId\n                orderby e.Date descending\n                select e;\n        return q.Skip(0).Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByOrganizationIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageByOrganizationIdQuery : IQuery<Event>\n{\n    private readonly Guid _organizationId;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageByOrganizationIdQuery(Guid organizationId, DateTime startDate,\n            DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _organizationId = organizationId;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                (_beforeDate != null || e.Date <= _endDate) &&\n                (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                e.OrganizationId == _organizationId\n                orderby e.Date descending\n                select e;\n        return q.Skip(0).Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageByOrganizationIdServiceAccountIdQuery : IQuery<Event>\n{\n    private readonly Guid _organizationId;\n    private readonly Guid _serviceAccountId;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageByOrganizationIdServiceAccountIdQuery(Guid organizationId, Guid serviceAccountId,\n        DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _organizationId = organizationId;\n        _serviceAccountId = serviceAccountId;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                      (_beforeDate != null || e.Date <= _endDate) &&\n                      (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                      e.OrganizationId == _organizationId &&\n                      (e.ServiceAccountId == _serviceAccountId || e.GrantedServiceAccountId == _serviceAccountId)\n                orderby e.Date descending\n                select e;\n        return q.Skip(0).Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByProjectIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Core.SecretsManager.Entities;\nusing Event = Bit.Infrastructure.EntityFramework.Models.Event;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageByProjectQuery : IQuery<Event>\n{\n    private readonly Project _project;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageByProjectQuery(Project project, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        _project = project;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = null;\n        _pageOptions = pageOptions;\n    }\n\n    public EventReadPageByProjectQuery(Project project, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _project = project;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var emptyGuid = Guid.Empty;\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                    (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                    (\n                        (_project.OrganizationId == emptyGuid && !e.OrganizationId.HasValue) ||\n                        (_project.OrganizationId != emptyGuid && e.OrganizationId == _project.OrganizationId)\n                    ) &&\n                    e.ProjectId == _project.Id\n                orderby e.Date descending\n                select e;\n\n        return q.Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByProviderIdActingUserIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageByProviderIdActingUserIdQuery : IQuery<Event>\n{\n    private readonly Guid _providerId;\n    private readonly Guid _actingUserId;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageByProviderIdActingUserIdQuery(Guid providerId, Guid actingUserId,\n            DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _providerId = providerId;\n        _actingUserId = actingUserId;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                (_beforeDate != null || e.Date <= _endDate) &&\n                (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                e.ProviderId == _providerId &&\n                e.ActingUserId == _actingUserId\n                orderby e.Date descending\n                select e;\n        return q.Skip(0).Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByProviderIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageByProviderIdQuery : IQuery<Event>\n{\n    private readonly Guid _providerId;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageByProviderIdQuery(Guid providerId, DateTime startDate,\n            DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _providerId = providerId;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                (_beforeDate != null || e.Date <= _endDate) &&\n                (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                e.ProviderId == _providerId && e.OrganizationId == null\n                orderby e.Date descending\n                select e;\n        return q.Skip(0).Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageBySecretIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Core.SecretsManager.Entities;\nusing Event = Bit.Infrastructure.EntityFramework.Models.Event;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageBySecretQuery : IQuery<Event>\n{\n    private readonly Secret _secret;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageBySecretQuery(Secret secret, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        _secret = secret;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = null;\n        _pageOptions = pageOptions;\n    }\n\n    public EventReadPageBySecretQuery(Secret secret, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _secret = secret;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var emptyGuid = Guid.Empty;\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                    (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                    (\n                        (_secret.OrganizationId == emptyGuid && !e.OrganizationId.HasValue) ||\n                        (_secret.OrganizationId != emptyGuid && e.OrganizationId == _secret.OrganizationId)\n                    ) &&\n                    e.SecretId == _secret.Id\n                orderby e.Date descending\n                select e;\n\n        return q.Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Core.SecretsManager.Entities;\nusing Event = Bit.Infrastructure.EntityFramework.Models.Event;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageByServiceAccountQuery : IQuery<Event>\n{\n    private readonly ServiceAccount _serviceAccount;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageByServiceAccountQuery(ServiceAccount serviceAccount, DateTime startDate, DateTime endDate, PageOptions pageOptions)\n    {\n        _serviceAccount = serviceAccount;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = null;\n        _pageOptions = pageOptions;\n    }\n\n    public EventReadPageByServiceAccountQuery(ServiceAccount serviceAccount, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _serviceAccount = serviceAccount;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                    (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                    (\n                        (_serviceAccount.OrganizationId == Guid.Empty && !e.OrganizationId.HasValue) ||\n                        (_serviceAccount.OrganizationId != Guid.Empty && e.OrganizationId == _serviceAccount.OrganizationId)\n                    ) &&\n                    e.GrantedServiceAccountId == _serviceAccount.Id\n                orderby e.Date descending\n                select e;\n\n        return q.Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/EventReadPageByUserIdQuery.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class EventReadPageByUserIdQuery : IQuery<Event>\n{\n    private readonly Guid _userId;\n    private readonly DateTime _startDate;\n    private readonly DateTime _endDate;\n    private readonly DateTime? _beforeDate;\n    private readonly PageOptions _pageOptions;\n\n    public EventReadPageByUserIdQuery(Guid userId, DateTime startDate,\n            DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)\n    {\n        _userId = userId;\n        _startDate = startDate;\n        _endDate = endDate;\n        _beforeDate = beforeDate;\n        _pageOptions = pageOptions;\n    }\n\n    public IQueryable<Event> Run(DatabaseContext dbContext)\n    {\n        var q = from e in dbContext.Events\n                where e.Date >= _startDate &&\n                (_beforeDate != null || e.Date <= _endDate) &&\n                (_beforeDate == null || e.Date < _beforeDate.Value) &&\n                !e.OrganizationId.HasValue &&\n                e.ActingUserId == _userId\n                orderby e.Date descending\n                select e;\n        return q.Skip(0).Take(_pageOptions.PageSize);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries;\n\npublic class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(\n    Guid organizationId,\n    EventType eventType,\n    IntegrationType integrationType)\n    : IQuery<OrganizationIntegrationConfigurationDetails>\n{\n    public IQueryable<OrganizationIntegrationConfigurationDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from oic in dbContext.OrganizationIntegrationConfigurations\n                    join oi in dbContext.OrganizationIntegrations on oic.OrganizationIntegrationId equals oi.Id\n                    where oi.OrganizationId == organizationId &&\n                          oi.Type == integrationType &&\n                          (oic.EventType == eventType || oic.EventType == null)\n                    select new OrganizationIntegrationConfigurationDetails()\n                    {\n                        Id = oic.Id,\n                        OrganizationId = oi.OrganizationId,\n                        OrganizationIntegrationId = oic.OrganizationIntegrationId,\n                        IntegrationType = oi.Type,\n                        EventType = oic.EventType,\n                        Configuration = oic.Configuration,\n                        Filters = oic.Filters,\n                        IntegrationConfiguration = oi.Configuration,\n                        Template = oic.Template\n                    };\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries;\n\npublic class OrganizationIntegrationConfigurationDetailsReadManyQuery : IQuery<OrganizationIntegrationConfigurationDetails>\n{\n    public IQueryable<OrganizationIntegrationConfigurationDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from oic in dbContext.OrganizationIntegrationConfigurations\n                    join oi in dbContext.OrganizationIntegrations on oic.OrganizationIntegrationId equals oi.Id into oioic\n                    from oi in dbContext.OrganizationIntegrations\n                    select new OrganizationIntegrationConfigurationDetails()\n                    {\n                        Id = oic.Id,\n                        OrganizationId = oi.OrganizationId,\n                        OrganizationIntegrationId = oic.OrganizationIntegrationId,\n                        IntegrationType = oi.Type,\n                        EventType = oic.EventType,\n                        Configuration = oic.Configuration,\n                        Filters = oic.Filters,\n                        IntegrationConfiguration = oi.Configuration,\n                        Template = oic.Template\n                    };\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries;\n\npublic class OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery : IQuery<OrganizationIntegrationConfiguration>\n{\n    private readonly Guid _organizationIntegrationId;\n\n    public OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery(Guid organizationIntegrationId)\n    {\n        _organizationIntegrationId = organizationIntegrationId;\n    }\n\n    public IQueryable<OrganizationIntegrationConfiguration> Run(DatabaseContext dbContext)\n    {\n        var query = from oic in dbContext.OrganizationIntegrationConfigurations\n                    where oic.OrganizationIntegrationId == _organizationIntegrationId\n                    select new OrganizationIntegrationConfiguration()\n                    {\n                        Id = oic.Id,\n                        OrganizationIntegrationId = oic.OrganizationIntegrationId,\n                        Configuration = oic.Configuration,\n                        EventType = oic.EventType,\n                        Filters = oic.Filters,\n                        Template = oic.Template,\n                        RevisionDate = oic.RevisionDate\n                    };\n        return query;\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries;\n\npublic class OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery : IQuery<OrganizationIntegration>\n{\n    private readonly string _tenantId;\n    private readonly string _teamId;\n\n    public OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery(string tenantId, string teamId)\n    {\n        _tenantId = tenantId;\n        _teamId = teamId;\n    }\n\n    public IQueryable<OrganizationIntegration> Run(DatabaseContext dbContext)\n    {\n        var query =\n            from oi in dbContext.OrganizationIntegrations\n            where oi.Type == IntegrationType.Teams &&\n                  oi.Configuration != null &&\n                  oi.Configuration.Contains($\"\\\"TenantId\\\":\\\"{_tenantId}\\\"\") &&\n                  oi.Configuration.Contains($\"\\\"id\\\":\\\"{_teamId}\\\"\")\n            select new OrganizationIntegration()\n            {\n                Id = oi.Id,\n                OrganizationId = oi.OrganizationId,\n                Type = oi.Type,\n                Configuration = oi.Configuration,\n            };\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Dirt/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Dirt.Repositories.Queries;\n\npublic class OrganizationIntegrationReadManyByOrganizationIdQuery : IQuery<OrganizationIntegration>\n{\n    private readonly Guid _organizationId;\n\n    public OrganizationIntegrationReadManyByOrganizationIdQuery(Guid organizationId)\n    {\n        _organizationId = organizationId;\n    }\n\n    public IQueryable<OrganizationIntegration> Run(DatabaseContext dbContext)\n    {\n        var query = from oi in dbContext.OrganizationIntegrations\n                    where oi.OrganizationId == _organizationId\n                    select new OrganizationIntegration()\n                    {\n                        Id = oi.Id,\n                        OrganizationId = oi.OrganizationId,\n                        Type = oi.Type,\n                        Configuration = oi.Configuration,\n                    };\n        return query;\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/EfExtensions.cs",
    "content": "﻿using Microsoft.EntityFrameworkCore;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework;\n\npublic static class EfExtensions\n{\n    public static T AttachToOrGet<T>(this DbContext context, Func<T, bool> predicate, Func<T> factory)\n        where T : class, new()\n    {\n        var match = context.Set<T>().Local.FirstOrDefault(predicate);\n        if (match == null)\n        {\n            match = factory();\n            context.Attach(match);\n        }\n\n        return match;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/EntityFrameworkCache.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework;\n\npublic class EntityFrameworkCache : IDistributedCache\n{\n#if DEBUG\n    // Used for debugging in tests\n    public Task? scanTask;\n#endif\n    private static readonly TimeSpan _defaultSlidingExpiration = TimeSpan.FromMinutes(20);\n    private static readonly TimeSpan _expiredItemsDeletionInterval = TimeSpan.FromMinutes(30);\n    private DateTimeOffset _lastExpirationScan;\n    private readonly Action _deleteExpiredCachedItemsDelegate;\n    private readonly object _mutex = new();\n    private readonly IServiceScopeFactory _serviceScopeFactory;\n    private readonly TimeProvider _timeProvider;\n\n    public EntityFrameworkCache(\n        IServiceScopeFactory serviceScopeFactory,\n        TimeProvider? timeProvider = null)\n    {\n        _deleteExpiredCachedItemsDelegate = DeleteExpiredCacheItems;\n        _serviceScopeFactory = serviceScopeFactory;\n        _timeProvider = timeProvider ?? TimeProvider.System;\n    }\n\n    public byte[]? Get(string key)\n    {\n        ArgumentNullException.ThrowIfNull(key);\n\n        using var scope = _serviceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var cache = dbContext.Cache\n            .Where(c => c.Id == key && _timeProvider.GetUtcNow().UtcDateTime <= c.ExpiresAtTime)\n            .SingleOrDefault();\n\n        if (cache == null)\n        {\n            return null;\n        }\n\n        if (UpdateCacheExpiration(cache))\n        {\n            dbContext.SaveChanges();\n        }\n\n        ScanForExpiredItemsIfRequired();\n        return cache?.Value;\n    }\n\n    public async Task<byte[]?> GetAsync(string key, CancellationToken token = default)\n    {\n        ArgumentNullException.ThrowIfNull(key);\n        token.ThrowIfCancellationRequested();\n\n        using var scope = _serviceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var cache = await dbContext.Cache\n            .Where(c => c.Id == key && _timeProvider.GetUtcNow().UtcDateTime <= c.ExpiresAtTime)\n            .SingleOrDefaultAsync(cancellationToken: token);\n\n        if (cache == null)\n        {\n            return null;\n        }\n\n        if (UpdateCacheExpiration(cache))\n        {\n            await dbContext.SaveChangesAsync(token);\n        }\n\n        ScanForExpiredItemsIfRequired();\n        return cache?.Value;\n    }\n\n    public void Refresh(string key) => Get(key);\n\n    public Task RefreshAsync(string key, CancellationToken token = default) => GetAsync(key, token);\n\n    public void Remove(string key)\n    {\n        ArgumentNullException.ThrowIfNull(key);\n\n        using var scope = _serviceScopeFactory.CreateScope();\n        GetDatabaseContext(scope).Cache\n            .Where(c => c.Id == key)\n            .ExecuteDelete();\n\n        ScanForExpiredItemsIfRequired();\n    }\n\n    public async Task RemoveAsync(string key, CancellationToken token = default)\n    {\n        ArgumentNullException.ThrowIfNull(key);\n\n        token.ThrowIfCancellationRequested();\n        using var scope = _serviceScopeFactory.CreateScope();\n        await GetDatabaseContext(scope).Cache\n            .Where(c => c.Id == key)\n            .ExecuteDeleteAsync(cancellationToken: token);\n\n        ScanForExpiredItemsIfRequired();\n    }\n\n    public void Set(string key, byte[] value, DistributedCacheEntryOptions options)\n    {\n        ArgumentNullException.ThrowIfNull(key);\n        ArgumentNullException.ThrowIfNull(value);\n        ArgumentNullException.ThrowIfNull(options);\n\n        using var scope = _serviceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var cache = dbContext.Cache.Find(key);\n        var insert = cache == null;\n        cache = SetCache(cache, key, value, options);\n        if (insert)\n        {\n            dbContext.Add(cache);\n        }\n\n        try\n        {\n            dbContext.SaveChanges();\n        }\n        catch (DbUpdateException e)\n        {\n            if (IsDuplicateKeyException(e))\n            {\n                // There is a possibility that multiple requests can try to add the same item to the cache, in\n                // which case we receive a 'duplicate key' exception on the primary key column.\n            }\n            else\n            {\n                throw;\n            }\n        }\n\n        ScanForExpiredItemsIfRequired();\n    }\n\n    public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)\n    {\n        ArgumentNullException.ThrowIfNull(key);\n        ArgumentNullException.ThrowIfNull(value);\n        ArgumentNullException.ThrowIfNull(options);\n\n        token.ThrowIfCancellationRequested();\n\n        using var scope = _serviceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var cache = await dbContext.Cache.FindAsync(new object[] { key }, cancellationToken: token);\n        var insert = cache == null;\n        cache = SetCache(cache, key, value, options);\n        if (insert)\n        {\n            await dbContext.AddAsync(cache, token);\n        }\n\n        try\n        {\n            await dbContext.SaveChangesAsync(token);\n        }\n        catch (DbUpdateException e)\n        {\n            if (IsDuplicateKeyException(e))\n            {\n                // There is a possibility that multiple requests can try to add the same item to the cache, in\n                // which case we receive a 'duplicate key' exception on the primary key column.\n            }\n            else\n            {\n                throw;\n            }\n        }\n\n        ScanForExpiredItemsIfRequired();\n    }\n\n    private Cache SetCache(Cache? cache, string key, byte[] value, DistributedCacheEntryOptions options)\n    {\n        var utcNow = _timeProvider.GetUtcNow().UtcDateTime;\n\n        // resolve options\n        if (!options.AbsoluteExpiration.HasValue &&\n            !options.AbsoluteExpirationRelativeToNow.HasValue &&\n            !options.SlidingExpiration.HasValue)\n        {\n            options = new DistributedCacheEntryOptions\n            {\n                SlidingExpiration = _defaultSlidingExpiration\n            };\n        }\n\n        if (cache == null)\n        {\n            // do an insert\n            cache = new Cache { Id = key };\n        }\n\n        var slidingExpiration = (long?)options.SlidingExpiration?.TotalSeconds;\n\n        // calculate absolute expiration\n        DateTime? absoluteExpiration = null;\n        if (options.AbsoluteExpirationRelativeToNow.HasValue)\n        {\n            absoluteExpiration = utcNow.Add(options.AbsoluteExpirationRelativeToNow.Value);\n        }\n        else if (options.AbsoluteExpiration.HasValue)\n        {\n            if (options.AbsoluteExpiration.Value <= utcNow)\n            {\n                throw new InvalidOperationException(\"The absolute expiration value must be in the future.\");\n            }\n\n            absoluteExpiration = options.AbsoluteExpiration.Value.UtcDateTime;\n        }\n\n        // set values on cache\n        cache.Value = value;\n        cache.SlidingExpirationInSeconds = slidingExpiration;\n        cache.AbsoluteExpiration = absoluteExpiration;\n        if (slidingExpiration.HasValue)\n        {\n            cache.ExpiresAtTime = utcNow.AddSeconds(slidingExpiration.Value);\n        }\n        else if (absoluteExpiration.HasValue)\n        {\n            cache.ExpiresAtTime = absoluteExpiration.Value;\n        }\n        else\n        {\n            throw new InvalidOperationException(\"Either absolute or sliding expiration needs to be provided.\");\n        }\n\n        return cache;\n    }\n\n    private bool UpdateCacheExpiration(Cache cache)\n    {\n        var utcNow = _timeProvider.GetUtcNow().UtcDateTime;\n        if (cache.SlidingExpirationInSeconds.HasValue && (cache.AbsoluteExpiration.HasValue || cache.AbsoluteExpiration != cache.ExpiresAtTime))\n        {\n            if (cache.AbsoluteExpiration.HasValue && (cache.AbsoluteExpiration.Value - utcNow).TotalSeconds <= cache.SlidingExpirationInSeconds)\n            {\n                cache.ExpiresAtTime = cache.AbsoluteExpiration.Value;\n            }\n            else\n            {\n                cache.ExpiresAtTime = utcNow.AddSeconds(cache.SlidingExpirationInSeconds.Value);\n            }\n            return true;\n        }\n        return false;\n    }\n\n    private void ScanForExpiredItemsIfRequired()\n    {\n        lock (_mutex)\n        {\n            var utcNow = _timeProvider.GetUtcNow().UtcDateTime;\n            if ((utcNow - _lastExpirationScan) > _expiredItemsDeletionInterval)\n            {\n                _lastExpirationScan = utcNow;\n#if DEBUG\n                scanTask =\n#endif\n                Task.Run(_deleteExpiredCachedItemsDelegate);\n            }\n        }\n    }\n\n    private void DeleteExpiredCacheItems()\n    {\n        using var scope = _serviceScopeFactory.CreateScope();\n        GetDatabaseContext(scope).Cache\n            .Where(c => _timeProvider.GetUtcNow().UtcDateTime > c.ExpiresAtTime)\n            .ExecuteDelete();\n    }\n\n    private DatabaseContext GetDatabaseContext(IServiceScope serviceScope)\n    {\n        return serviceScope.ServiceProvider.GetRequiredService<DatabaseContext>();\n    }\n\n    private static bool IsDuplicateKeyException(DbUpdateException e)\n    {\n        // MySQL\n        if (e.InnerException is MySqlConnector.MySqlException myEx)\n        {\n            return myEx.ErrorCode == MySqlConnector.MySqlErrorCode.DuplicateKeyEntry;\n        }\n        // SQL Server\n        else if (e.InnerException is Microsoft.Data.SqlClient.SqlException msEx)\n        {\n            return msEx.Errors != null &&\n                msEx.Errors.Cast<Microsoft.Data.SqlClient.SqlError>().Any(error => error.Number == 2627);\n        }\n        // Postgres\n        else if (e.InnerException is Npgsql.PostgresException pgEx)\n        {\n            return pgEx.SqlState == \"23505\";\n        }\n        // Sqlite\n        else if (e.InnerException is Microsoft.Data.Sqlite.SqliteException liteEx)\n        {\n            return liteEx.SqliteErrorCode == 19 && liteEx.SqliteExtendedErrorCode == 1555;\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Billing.Organizations.Repositories;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Billing.Subscriptions.Repositories;\nusing Bit.Core.Dirt.Reports.Repositories;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Installations;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;\nusing Bit.Infrastructure.EntityFramework.Auth.Repositories;\nusing Bit.Infrastructure.EntityFramework.Billing.Repositories;\nusing Bit.Infrastructure.EntityFramework.Dirt;\nusing Bit.Infrastructure.EntityFramework.Dirt.Repositories;\nusing Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;\nusing Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;\nusing Bit.Infrastructure.EntityFramework.Platform;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Repositories;\nusing Bit.Infrastructure.EntityFramework.Tools.Repositories;\nusing Bit.Infrastructure.EntityFramework.Vault.Repositories;\nusing LinqToDB.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework;\n\npublic static class EntityFrameworkServiceCollectionExtensions\n{\n    public static void SetupEntityFramework(this IServiceCollection services, string connectionString, SupportedDatabaseProviders provider)\n    {\n        if (string.IsNullOrWhiteSpace(connectionString))\n        {\n            throw new Exception($\"Database provider type {provider} was selected but no connection string was found.\");\n        }\n\n        // TODO: We should move away from using LINQ syntax for EF (TDL-48).\n        LinqToDBForEFTools.Initialize();\n\n        services.AddAutoMapper(typeof(UserRepository));\n        services.AddDbContext<DatabaseContext>(options =>\n        {\n            if (provider == SupportedDatabaseProviders.Postgres)\n            {\n                options.UseNpgsql(connectionString, b => b.MigrationsAssembly(\"PostgresMigrations\"));\n                // Handle NpgSql Legacy Support for `timestamp without timezone` issue\n                AppContext.SetSwitch(\"Npgsql.EnableLegacyTimestampBehavior\", true);\n            }\n            else if (provider == SupportedDatabaseProviders.MySql)\n            {\n                options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),\n                    b => b.MigrationsAssembly(\"MySqlMigrations\"));\n            }\n            else if (provider == SupportedDatabaseProviders.Sqlite)\n            {\n                options.UseSqlite(connectionString, b => b.MigrationsAssembly(\"SqliteMigrations\"));\n            }\n            else if (provider == SupportedDatabaseProviders.SqlServer)\n            {\n                options.UseSqlServer(connectionString);\n            }\n        });\n    }\n\n    public static void AddPasswordManagerEFRepositories(this IServiceCollection services, bool selfHosted)\n    {\n        services.AddSingleton<IApiKeyRepository, ApiKeyRepository>();\n        services.AddSingleton<IAuthRequestRepository, AuthRequestRepository>();\n        services.AddSingleton<ICipherRepository, CipherRepository>();\n        services.AddSingleton<ICollectionCipherRepository, CollectionCipherRepository>();\n        services.AddSingleton<ICollectionRepository, CollectionRepository>();\n        services.AddSingleton<IDeviceRepository, DeviceRepository>();\n        services.AddSingleton<IEmergencyAccessRepository, EmergencyAccessRepository>();\n        services.AddSingleton<IFolderRepository, FolderRepository>();\n        services.AddSingleton<IGrantRepository, GrantRepository>();\n        services.AddSingleton<IGroupRepository, GroupRepository>();\n        services.AddSingleton<IInstallationRepository, InstallationRepository>();\n        services.AddSingleton<IMaintenanceRepository, MaintenanceRepository>();\n        services.AddSingleton<IOrganizationApiKeyRepository, OrganizationApiKeyRepository>();\n        services.AddSingleton<IOrganizationConnectionRepository, OrganizationConnectionRepository>();\n        services.AddSingleton<IOrganizationIntegrationRepository, OrganizationIntegrationRepository>();\n        services.AddSingleton<IOrganizationIntegrationConfigurationRepository, OrganizationIntegrationConfigurationRepository>();\n        services.AddSingleton<IOrganizationRepository, OrganizationRepository>();\n        services.AddSingleton<IOrganizationSponsorshipRepository, OrganizationSponsorshipRepository>();\n        services.AddSingleton<IOrganizationUserRepository, OrganizationUserRepository>();\n        services.AddSingleton<IPlayItemRepository, PlayItemRepository>();\n        services.AddSingleton<IPolicyRepository, PolicyRepository>();\n        services.AddSingleton<IProviderOrganizationRepository, ProviderOrganizationRepository>();\n        services.AddSingleton<IProviderRepository, ProviderRepository>();\n        services.AddSingleton<IProviderUserRepository, ProviderUserRepository>();\n        services.AddSingleton<ISendRepository, SendRepository>();\n        services.AddSingleton<ISsoConfigRepository, SsoConfigRepository>();\n        services.AddSingleton<ISsoUserRepository, SsoUserRepository>();\n        services.AddSingleton<ITransactionRepository, TransactionRepository>();\n        services.AddSingleton<IUserRepository, UserRepository>();\n        services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();\n        services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();\n        services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();\n        services.AddSingleton<IProviderInvoiceItemRepository, ProviderInvoiceItemRepository>();\n        services.AddSingleton<ISubscriptionDiscountRepository, SubscriptionDiscountRepository>();\n        services.AddSingleton<INotificationRepository, NotificationRepository>();\n        services.AddSingleton<INotificationStatusRepository, NotificationStatusRepository>();\n        services\n            .AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();\n        services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();\n        services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();\n        services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();\n        services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();\n        services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();\n        services.AddSingleton<IOrganizationReportRepository, OrganizationReportRepository>();\n        services.AddSingleton<IOrganizationApplicationRepository, OrganizationApplicationRepository>();\n        services.AddSingleton<IOrganizationMemberBaseDetailRepository, OrganizationMemberBaseDetailRepository>();\n\n        if (selfHosted)\n        {\n            services.AddSingleton<IEventRepository, EventRepository>();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n    <PropertyGroup>\n      <!-- These opt outs should be removed when all warnings are addressed -->\n      <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304;CA1305</WarningsNotAsErrors>\n    </PropertyGroup>\n\n\n    <ItemGroup>\n      <PackageReference Include=\"AutoMapper.Extensions.Microsoft.DependencyInjection\" Version=\"12.0.1\" />\n      <PackageReference Include=\"linq2db\" Version=\"5.4.1\" />\n      <PackageReference Include=\"Microsoft.EntityFrameworkCore.Relational\" Version=\"[8.0.8]\" />\n      <PackageReference Include=\"Microsoft.EntityFrameworkCore.SqlServer\" Version=\"[8.0.8]\" />\n      <PackageReference Include=\"Microsoft.EntityFrameworkCore.Sqlite\" Version=\"[8.0.8]\" />\n      <PackageReference Include=\"Npgsql.EntityFrameworkCore.PostgreSQL\" Version=\"[8.0.4]\" />\n      <PackageReference Include=\"Pomelo.EntityFrameworkCore.MySql\" Version=\"[8.0.2]\" />\n      <PackageReference Include=\"linq2db.EntityFrameworkCore\" Version=\"[8.1.0]\" />\n    </ItemGroup>\n\n    <ItemGroup>\n      <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n    </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/KeyManagement/Configurations/UserSignatureKeyPairEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Configurations;\n\npublic class UserSignatureKeyPairEntityTypeConfiguration : IEntityTypeConfiguration<UserSignatureKeyPair>\n{\n    public void Configure(EntityTypeBuilder<UserSignatureKeyPair> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(s => s.UserId)\n            .IsUnique()\n            .IsClustered(false);\n\n        builder.ToTable(nameof(UserSignatureKeyPair));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/KeyManagement/Models/UserSignatureKeyPair.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class UserSignatureKeyPair : Core.KeyManagement.Entities.UserSignatureKeyPair\n{\n    public virtual User User { get; set; }\n}\n\npublic class UserSignatureKeyPairMapperProfile : Profile\n{\n    public UserSignatureKeyPairMapperProfile()\n    {\n        CreateMap<Core.KeyManagement.Entities.UserSignatureKeyPair, UserSignatureKeyPair>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserAsymmetricKeysRepository.cs",
    "content": "﻿#nullable enable\nusing AutoMapper;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;\n\npublic class UserAsymmetricKeysRepository : BaseEntityFrameworkRepository, IUserAsymmetricKeysRepository\n{\n    public UserAsymmetricKeysRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(\n        serviceScopeFactory,\n        mapper)\n    {\n    }\n\n    public async Task RegenerateUserAsymmetricKeysAsync(UserAsymmetricKeys userAsymmetricKeys)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var entity = await dbContext.Users.FindAsync(userAsymmetricKeys.UserId);\n        if (entity != null)\n        {\n            var utcNow = DateTime.UtcNow;\n            entity.PublicKey = userAsymmetricKeys.PublicKey;\n            entity.PrivateKey = userAsymmetricKeys.UserKeyEncryptedPrivateKey;\n            entity.RevisionDate = utcNow;\n            entity.AccountRevisionDate = utcNow;\n            await dbContext.SaveChangesAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs",
    "content": "﻿\nusing AutoMapper;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Utilities;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;\n\npublic class UserSignatureKeyPairRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : Repository<Core.KeyManagement.Entities.UserSignatureKeyPair, Models.UserSignatureKeyPair, Guid>(serviceScopeFactory, mapper, context => context.UserSignatureKeyPairs), IUserSignatureKeyPairRepository\n{\n    public async Task<SignatureKeyPairData?> GetByUserIdAsync(Guid userId)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n        var signingKeys = await dbContext.UserSignatureKeyPairs.FirstOrDefaultAsync(x => x.UserId == userId);\n        if (signingKeys == null)\n        {\n            return null;\n        }\n\n        return signingKeys.ToSignatureKeyPairData();\n    }\n\n    public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys)\n    {\n        return async (_, _) =>\n        {\n            await using var scope = ServiceScopeFactory.CreateAsyncScope();\n            var dbContext = GetDatabaseContext(scope);\n            var entity = new Models.UserSignatureKeyPair\n            {\n                Id = CoreHelpers.GenerateComb(),\n                UserId = userId,\n                SignatureAlgorithm = signingKeys.SignatureAlgorithm,\n                SigningKey = signingKeys.WrappedSigningKey,\n                VerifyingKey = signingKeys.VerifyingKey,\n                CreationDate = DateTime.UtcNow,\n                RevisionDate = DateTime.UtcNow,\n            };\n            await dbContext.UserSignatureKeyPairs.AddAsync(entity);\n            await dbContext.SaveChangesAsync();\n        };\n    }\n\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys)\n    {\n        return async (_, _) =>\n        {\n            await using var scope = ServiceScopeFactory.CreateAsyncScope();\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await dbContext.UserSignatureKeyPairs.FirstOrDefaultAsync(x => x.UserId == grantorId);\n            if (entity != null)\n            {\n                entity.SignatureAlgorithm = signingKeys.SignatureAlgorithm;\n                entity.SigningKey = signingKeys.WrappedSigningKey;\n                entity.VerifyingKey = signingKeys.VerifyingKey;\n                entity.RevisionDate = DateTime.UtcNow;\n                await dbContext.SaveChangesAsync();\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/Cache.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class Cache\n{\n    [StringLength(449)]\n    public required string Id { get; set; }\n    public byte[] Value { get; set; } = null!;\n    public DateTime ExpiresAtTime { get; set; }\n    public long? SlidingExpirationInSeconds { get; set; }\n    public DateTime? AbsoluteExpiration { get; set; }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/Collection.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class Collection : Core.Entities.Collection\n{\n    public virtual Organization Organization { get; set; }\n    public virtual ICollection<CollectionUser> CollectionUsers { get; set; }\n    public virtual ICollection<CollectionCipher> CollectionCiphers { get; set; }\n    public virtual ICollection<CollectionGroup> CollectionGroups { get; set; }\n}\n\npublic class CollectionMapperProfile : Profile\n{\n    public CollectionMapperProfile()\n    {\n        CreateMap<Core.Entities.Collection, Collection>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/CollectionCipher.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class CollectionCipher : Core.Entities.CollectionCipher\n{\n    public virtual Cipher Cipher { get; set; }\n    public virtual Collection Collection { get; set; }\n}\n\npublic class CollectionCipherMapperProfile : Profile\n{\n    public CollectionCipherMapperProfile()\n    {\n        CreateMap<Core.Entities.CollectionCipher, CollectionCipher>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/CollectionGroup.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class CollectionGroup : Core.Entities.CollectionGroup\n{\n    public virtual Collection Collection { get; set; }\n    public virtual Group Group { get; set; }\n}\n\npublic class CollectionGroupMapperProfile : Profile\n{\n    public CollectionGroupMapperProfile()\n    {\n        CreateMap<Core.Entities.CollectionGroup, CollectionGroup>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/CollectionUser.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class CollectionUser : Core.Entities.CollectionUser\n{\n    public virtual Collection Collection { get; set; }\n    public virtual OrganizationUser OrganizationUser { get; set; }\n}\n\npublic class CollectionUserMapperProfile : Profile\n{\n    public CollectionUserMapperProfile()\n    {\n        CreateMap<Core.Entities.CollectionUser, CollectionUser>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/Device.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class Device : Core.Entities.Device\n{\n    public virtual User User { get; set; }\n}\n\npublic class DeviceMapperProfile : Profile\n{\n    public DeviceMapperProfile()\n    {\n        CreateMap<Core.Entities.Device, Device>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/Group.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class Group : Core.AdminConsole.Entities.Group\n{\n    public virtual Organization Organization { get; set; }\n    public virtual ICollection<GroupUser> GroupUsers { get; set; }\n}\n\npublic class GroupMapperProfile : Profile\n{\n    public GroupMapperProfile()\n    {\n        CreateMap<Core.AdminConsole.Entities.Group, Group>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/GroupUser.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class GroupUser : Core.AdminConsole.Entities.GroupUser\n{\n    public virtual Group Group { get; set; }\n    public virtual OrganizationUser OrganizationUser { get; set; }\n}\n\npublic class GroupUserMapperProfile : Profile\n{\n    public GroupUserMapperProfile()\n    {\n        CreateMap<Core.AdminConsole.Entities.GroupUser, GroupUser>().ReverseMap();\n    }\n}\n\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/OrganizationApiKey.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class OrganizationApiKey : Core.Entities.OrganizationApiKey\n{\n    public virtual Organization Organization { get; set; }\n}\n\npublic class OrganizationApiKeyMapperProfile : Profile\n{\n    public OrganizationApiKeyMapperProfile()\n    {\n        CreateMap<Core.Entities.OrganizationApiKey, OrganizationApiKey>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/OrganizationConnection.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class OrganizationConnection : Core.Entities.OrganizationConnection\n{\n    public virtual Organization Organization { get; set; }\n}\n\npublic class OrganizationConnectionMapperProfile : Profile\n{\n    public OrganizationConnectionMapperProfile()\n    {\n        CreateMap<Core.Entities.OrganizationConnection, OrganizationConnection>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/OrganizationDomain.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class OrganizationDomain : Core.Entities.OrganizationDomain\n{\n    public virtual Organization Organization { get; set; }\n}\n\npublic class OrganizationDomainMapperProfile : Profile\n{\n    public OrganizationDomainMapperProfile()\n    {\n        CreateMap<Core.Entities.OrganizationDomain, OrganizationDomain>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/OrganizationSponsorship.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class OrganizationSponsorship : Core.Entities.OrganizationSponsorship\n{\n    public virtual Organization SponsoringOrganization { get; set; }\n    public virtual Organization SponsoredOrganization { get; set; }\n}\n\npublic class OrganizationSponsorshipMapperProfile : Profile\n{\n    public OrganizationSponsorshipMapperProfile()\n    {\n        CreateMap<Core.Entities.OrganizationSponsorship, OrganizationSponsorship>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/OrganizationUser.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class OrganizationUser : Core.Entities.OrganizationUser\n{\n    public virtual Organization Organization { get; set; }\n    public virtual User User { get; set; }\n    public virtual ICollection<CollectionUser> CollectionUsers { get; set; }\n    public virtual ICollection<GroupUser> GroupUsers { get; set; }\n}\n\npublic class OrganizationUserMapperProfile : Profile\n{\n    public OrganizationUserMapperProfile()\n    {\n        CreateMap<Core.Entities.OrganizationUser, OrganizationUser>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/PlayItem.cs",
    "content": "﻿#nullable enable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class PlayItem : Core.Entities.PlayItem\n{\n    public virtual User? User { get; set; }\n    public virtual AdminConsole.Models.Organization? Organization { get; set; }\n}\n\npublic class PlayItemMapperProfile : Profile\n{\n    public PlayItemMapperProfile()\n    {\n        CreateMap<Core.Entities.PlayItem, PlayItem>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/Role.cs",
    "content": "﻿using AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class Role : Core.Entities.Role\n{\n}\n\npublic class RoleMapperProfile : Profile\n{\n    public RoleMapperProfile()\n    {\n        CreateMap<Core.Entities.Role, Role>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/TaxRate.cs",
    "content": "﻿using AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class TaxRate : Core.Entities.TaxRate\n{\n}\n\npublic class TaxRateMapperProfile : Profile\n{\n    public TaxRateMapperProfile()\n    {\n        CreateMap<Core.Entities.TaxRate, TaxRate>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/Transaction.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class Transaction : Core.Entities.Transaction\n{\n    public virtual Organization Organization { get; set; }\n    public virtual User User { get; set; }\n    public virtual Provider Provider { get; set; }\n}\n\npublic class TransactionMapperProfile : Profile\n{\n    public TransactionMapperProfile()\n    {\n        CreateMap<Core.Entities.Transaction, Transaction>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Models/User.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class User : Core.Entities.User\n{\n    public virtual ICollection<Cipher> Ciphers { get; set; }\n    public virtual ICollection<Folder> Folders { get; set; }\n    public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; }\n    public virtual ICollection<SsoUser> SsoUsers { get; set; }\n    public virtual ICollection<Transaction> Transactions { get; set; }\n}\n\npublic class UserMapperProfile : Profile\n{\n    public UserMapperProfile()\n    {\n        CreateMap<Core.Entities.User, User>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationEntityTypeConfiguration.cs",
    "content": "﻿#nullable enable\nusing Bit.Infrastructure.EntityFramework.NotificationCenter.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.NotificationCenter.Configurations;\n\npublic class NotificationEntityTypeConfiguration : IEntityTypeConfiguration<Notification>\n{\n    public void Configure(EntityTypeBuilder<Notification> builder)\n    {\n        builder\n            .Property(n => n.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasKey(n => n.Id)\n            .IsClustered();\n\n        builder\n            .HasIndex(n => new { n.ClientType, n.Global, n.UserId, n.OrganizationId, n.Priority, n.CreationDate })\n            .IsDescending(false, false, false, false, true, true)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(n => n.OrganizationId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(n => n.UserId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(n => n.TaskId)\n            .IsClustered(false);\n\n        builder\n            .HasOne(n => n.Task)\n            .WithMany()\n            .HasForeignKey(n => n.TaskId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder.ToTable(nameof(Notification));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationStatusEntityTypeConfiguration.cs",
    "content": "﻿#nullable enable\nusing Bit.Infrastructure.EntityFramework.NotificationCenter.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.NotificationCenter.Configurations;\n\npublic class NotificationStatusEntityTypeConfiguration : IEntityTypeConfiguration<NotificationStatus>\n{\n    public void Configure(EntityTypeBuilder<NotificationStatus> builder)\n    {\n        builder\n            .HasKey(ns => new { ns.UserId, ns.NotificationId })\n            .IsClustered();\n\n        builder\n            .HasOne(ns => ns.Notification)\n            .WithMany()\n            .HasForeignKey(ns => ns.NotificationId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder.ToTable(nameof(NotificationStatus));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/NotificationCenter/Models/Notification.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.NotificationCenter.Models;\n\npublic class Notification : Core.NotificationCenter.Entities.Notification\n{\n    public virtual User User { get; set; }\n    public virtual Organization Organization { get; set; }\n    public virtual SecurityTask Task { get; set; }\n}\n\npublic class NotificationMapperProfile : Profile\n{\n    public NotificationMapperProfile()\n    {\n        CreateMap<Core.NotificationCenter.Entities.Notification, Notification>()\n            .PreserveReferences()\n            .ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/NotificationCenter/Models/NotificationStatus.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.NotificationCenter.Models;\n\npublic class NotificationStatus : Core.NotificationCenter.Entities.NotificationStatus\n{\n    public virtual Notification Notification { get; set; }\n    public virtual User User { get; set; }\n}\n\npublic class NotificationStatusMapperProfile : Profile\n{\n    public NotificationStatusMapperProfile()\n    {\n        CreateMap<Core.NotificationCenter.Entities.NotificationStatus, NotificationStatus>()\n            .PreserveReferences()\n            .ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs",
    "content": "﻿#nullable enable\nusing AutoMapper;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Filter;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Infrastructure.EntityFramework.NotificationCenter.Models;\nusing Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;\n\npublic class NotificationRepository : Repository<Core.NotificationCenter.Entities.Notification, Notification, Guid>,\n    INotificationRepository\n{\n    public NotificationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, context => context.Notifications)\n    {\n    }\n\n    public async Task<IEnumerable<Core.NotificationCenter.Entities.Notification>> GetByUserIdAsync(Guid userId,\n        ClientType clientType)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var notificationStatusDetailsViewQuery = new NotificationStatusDetailsViewQuery(userId, clientType);\n\n        var notifications = await notificationStatusDetailsViewQuery.Run(dbContext)\n            .OrderByDescending(n => n.Priority)\n            .ThenByDescending(n => n.CreationDate)\n            .ToListAsync();\n\n        return Mapper.Map<List<Core.NotificationCenter.Entities.Notification>>(notifications);\n    }\n\n    public async Task<PagedResult<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId,\n        ClientType clientType, NotificationStatusFilter? statusFilter, PageOptions pageOptions)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        if (!int.TryParse(pageOptions.ContinuationToken, out var pageNumber))\n        {\n            pageNumber = 1;\n        }\n\n        var notificationStatusDetailsViewQuery = new NotificationStatusDetailsViewQuery(userId, clientType);\n\n        var query = notificationStatusDetailsViewQuery.Run(dbContext);\n        if (statusFilter != null && (statusFilter.Read != null || statusFilter.Deleted != null))\n        {\n            query = from n in query\n                    where (statusFilter.Read == null ||\n                           (statusFilter.Read == true ? n.ReadDate != null : n.ReadDate == null)) &&\n                          (statusFilter.Deleted == null ||\n                           (statusFilter.Deleted == true ? n.DeletedDate != null : n.DeletedDate == null))\n                    select n;\n        }\n\n        var results = await query\n            .OrderByDescending(n => n.Priority)\n            .ThenByDescending(n => n.CreationDate)\n            .Skip(pageOptions.PageSize * (pageNumber - 1))\n            .Take(pageOptions.PageSize)\n            .ToListAsync();\n\n        return new PagedResult<NotificationStatusDetails>\n        {\n            Data = results,\n            ContinuationToken = results.Count < pageOptions.PageSize ? null : (pageNumber + 1).ToString()\n        };\n    }\n\n    public async Task<IEnumerable<Guid>> MarkNotificationsAsDeletedByTask(Guid taskId)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var notifications = await dbContext.Notifications\n            .Where(n => n.TaskId == taskId)\n            .ToListAsync();\n\n        var notificationIds = notifications.Select(n => n.Id).ToList();\n\n        var statuses = await dbContext.Set<NotificationStatus>()\n            .Where(ns => notificationIds.Contains(ns.NotificationId))\n            .ToListAsync();\n\n        var now = DateTime.UtcNow;\n\n        // Update existing statuses and add missing ones\n        foreach (var notification in notifications)\n        {\n            var status = statuses.FirstOrDefault(s => s.NotificationId == notification.Id);\n            if (status != null)\n            {\n                if (status.DeletedDate == null)\n                {\n                    status.DeletedDate = now;\n                }\n            }\n            else if (notification.UserId.HasValue)\n            {\n                dbContext.Set<NotificationStatus>().Add(new NotificationStatus\n                {\n                    NotificationId = notification.Id,\n                    UserId = (Guid)notification.UserId,\n                    DeletedDate = now\n                });\n            }\n        }\n\n        await dbContext.SaveChangesAsync();\n\n        var userIds = notifications\n            .Select(n => n.UserId)\n            .Where(u => u.HasValue)\n            .ToList();\n\n        return (IEnumerable<Guid>)userIds;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationStatusRepository.cs",
    "content": "﻿#nullable enable\nusing AutoMapper;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Infrastructure.EntityFramework.NotificationCenter.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;\n\npublic class NotificationStatusRepository : BaseEntityFrameworkRepository, INotificationStatusRepository\n{\n    public NotificationStatusRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(\n        serviceScopeFactory,\n        mapper)\n    {\n    }\n\n    public async Task<Bit.Core.NotificationCenter.Entities.NotificationStatus?> GetByNotificationIdAndUserIdAsync(Guid notificationId, Guid userId)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var entity = await dbContext.NotificationStatuses\n            .Where(ns =>\n                ns.NotificationId == notificationId && ns.UserId == userId)\n            .FirstOrDefaultAsync();\n\n        return Mapper.Map<Bit.Core.NotificationCenter.Entities.NotificationStatus?>(entity);\n    }\n\n    public async Task<Bit.Core.NotificationCenter.Entities.NotificationStatus> CreateAsync(Bit.Core.NotificationCenter.Entities.NotificationStatus notificationStatus)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var entity = Mapper.Map<NotificationStatus>(notificationStatus);\n        await dbContext.AddAsync(entity);\n        await dbContext.SaveChangesAsync();\n        return notificationStatus;\n    }\n\n    public async Task UpdateAsync(Bit.Core.NotificationCenter.Entities.NotificationStatus notificationStatus)\n    {\n        await using var scope = ServiceScopeFactory.CreateAsyncScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var entity = await dbContext.NotificationStatuses\n            .Where(ns =>\n                ns.NotificationId == notificationStatus.NotificationId && ns.UserId == notificationStatus.UserId)\n            .FirstOrDefaultAsync();\n\n        if (entity != null)\n        {\n            entity.DeletedDate = notificationStatus.DeletedDate;\n            entity.ReadDate = notificationStatus.ReadDate;\n            await dbContext.SaveChangesAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/NotificationCenter/Repositories/Queries/NotificationStatusDetailsViewQuery.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Enums;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories.Queries;\n\npublic class NotificationStatusDetailsViewQuery(Guid userId, ClientType clientType) : IQuery<NotificationStatusDetails>\n{\n    public IQueryable<NotificationStatusDetails> Run(DatabaseContext dbContext)\n    {\n        var clientTypes = new[] { ClientType.All };\n        if (clientType != ClientType.All)\n        {\n            clientTypes = [ClientType.All, clientType];\n        }\n\n        var query = from n in dbContext.Notifications\n                    join ou in dbContext.OrganizationUsers.Where(ou => ou.UserId == userId)\n                        on n.OrganizationId equals ou.OrganizationId into groupingOrganizationUsers\n                    from ou in groupingOrganizationUsers.DefaultIfEmpty()\n                    join ns in dbContext.NotificationStatuses.Where(ns => ns.UserId == userId) on n.Id equals ns.NotificationId\n                        into groupingNotificationStatus\n                    from ns in groupingNotificationStatus.DefaultIfEmpty()\n                    where\n                        clientTypes.Contains(n.ClientType) &&\n                        (\n                            (\n                                n.Global &&\n                                n.UserId == null &&\n                                n.OrganizationId == null\n                            ) ||\n                            (\n                                !n.Global &&\n                                n.UserId == userId &&\n                                (n.OrganizationId == null || ou != null)\n                            ) ||\n                            (\n                                !n.Global &&\n                                n.UserId == null &&\n                                ou != null\n                            )\n                        )\n                    select new { n, ns };\n\n        return query.Select(x => new NotificationStatusDetails\n        {\n            Id = x.n.Id,\n            Priority = x.n.Priority,\n            Global = x.n.Global,\n            ClientType = x.n.ClientType,\n            UserId = x.n.UserId,\n            OrganizationId = x.n.OrganizationId,\n            TaskId = x.n.TaskId,\n            Title = x.n.Title,\n            Body = x.n.Body,\n            CreationDate = x.n.CreationDate,\n            RevisionDate = x.n.RevisionDate,\n            ReadDate = x.ns != null ? x.ns.ReadDate : null,\n            DeletedDate = x.ns != null ? x.ns.DeletedDate : null,\n        });\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs",
    "content": "﻿using AutoMapper;\nusing C = Bit.Core.Platform.Installations;\n\nnamespace Bit.Infrastructure.EntityFramework.Platform;\n\npublic class Installation : C.Installation;\n\npublic class InstallationMapperProfile : Profile\n{\n    public InstallationMapperProfile()\n    {\n        CreateMap<C.Installation, Installation>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Platform/Installations/Repositories/InstallationRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.Extensions.DependencyInjection;\nusing C = Bit.Core.Platform.Installations;\nusing Ef = Bit.Infrastructure.EntityFramework.Platform;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Platform;\n\npublic class InstallationRepository : Repository<C.Installation, Ef.Installation, Guid>, C.IInstallationRepository\n{\n    public InstallationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Installations)\n    { }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/BaseEntityFrameworkRepository.cs",
    "content": "﻿using System.Text.Json;\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing LinqToDB.Data;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing User = Bit.Core.Entities.User;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic abstract class BaseEntityFrameworkRepository\n{\n    protected BulkCopyOptions DefaultBulkCopyOptions { get; set; } = new BulkCopyOptions\n    {\n        KeepIdentity = true,\n        BulkCopyType = BulkCopyType.MultipleRows,\n    };\n\n    public BaseEntityFrameworkRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n    {\n        ServiceScopeFactory = serviceScopeFactory;\n        Mapper = mapper;\n    }\n\n    protected IServiceScopeFactory ServiceScopeFactory { get; private set; }\n    protected IMapper Mapper { get; private set; }\n\n    public DatabaseContext GetDatabaseContext(IServiceScope serviceScope)\n    {\n        return serviceScope.ServiceProvider.GetRequiredService<DatabaseContext>();\n    }\n\n    public void ClearChangeTracking()\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            dbContext.ChangeTracker.Clear();\n        }\n    }\n\n    public async Task<int> GetCountFromQuery<T>(IQuery<T> query)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            return await query.Run(GetDatabaseContext(scope)).CountAsync();\n        }\n    }\n\n    protected async Task OrganizationUpdateStorage(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var attachments = await dbContext.Ciphers\n                .Where(e => e.UserId == null &&\n                    e.OrganizationId == organizationId &&\n                    !string.IsNullOrWhiteSpace(e.Attachments))\n                .Select(e => e.Attachments)\n                .ToListAsync();\n            var storage = attachments.Sum(e => JsonDocument.Parse(e)?.RootElement.EnumerateObject().Sum(p =>\n            {\n                if (long.TryParse(p.Value.GetProperty(\"Size\").ToString(), out var s))\n                {\n                    return s;\n                }\n                return 0;\n            }) ?? 0);\n            var organization = new Organization\n            {\n                Id = organizationId,\n                RevisionDate = DateTime.UtcNow,\n                Storage = storage,\n            };\n            dbContext.Organizations.Attach(organization);\n            var entry = dbContext.Entry(organization);\n            entry.Property(e => e.RevisionDate).IsModified = true;\n            entry.Property(e => e.Storage).IsModified = true;\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    protected async Task UserUpdateStorage(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var attachments = await dbContext.Ciphers\n                .Where(e => e.UserId.HasValue &&\n                    e.UserId.Value == userId &&\n                    e.OrganizationId == null &&\n                    !string.IsNullOrWhiteSpace(e.Attachments))\n                .Select(e => e.Attachments)\n                .ToListAsync();\n            var storage = attachments.Sum(e => JsonDocument.Parse(e)?.RootElement.EnumerateObject().Sum(p =>\n            {\n                if (long.TryParse(p.Value.GetProperty(\"Size\").ToString(), out var s))\n                {\n                    return s;\n                }\n                return 0;\n            }) ?? 0);\n            var user = new Models.User\n            {\n                Id = userId,\n                RevisionDate = DateTime.UtcNow,\n                Storage = storage,\n            };\n            dbContext.Users.Attach(user);\n            var entry = dbContext.Entry(user);\n            entry.Property(e => e.RevisionDate).IsModified = true;\n            entry.Property(e => e.Storage).IsModified = true;\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    protected async Task UserUpdateKeys(User user)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await dbContext.Users.FindAsync(user.Id);\n            if (entity == null)\n            {\n                return;\n            }\n            entity.SecurityStamp = user.SecurityStamp;\n            entity.Key = user.Key;\n            entity.PrivateKey = user.PrivateKey;\n            entity.LastKeyRotationDate = user.LastKeyRotationDate;\n            entity.AccountRevisionDate = user.AccountRevisionDate;\n            entity.RevisionDate = user.RevisionDate;\n            await dbContext.SaveChangesAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/CollectionCipherRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing CollectionCipher = Bit.Core.Entities.CollectionCipher;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class CollectionCipherRepository : BaseEntityFrameworkRepository, ICollectionCipherRepository\n{\n    public CollectionCipherRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper)\n    { }\n\n    public async Task<CollectionCipher> CreateAsync(CollectionCipher obj)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = Mapper.Map<Models.CollectionCipher>(obj);\n            dbContext.Add(entity);\n            await dbContext.SaveChangesAsync();\n            var organizationId = (await dbContext.Ciphers.FirstOrDefaultAsync(c => c.Id.Equals(obj.CipherId)))?.OrganizationId;\n            if (organizationId.HasValue)\n            {\n                await dbContext.UserBumpAccountRevisionDateByCollectionIdAsync(obj.CollectionId, organizationId.Value);\n                await dbContext.SaveChangesAsync();\n            }\n            return obj;\n        }\n    }\n\n    public async Task<ICollection<CollectionCipher>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var data = await (from cc in dbContext.CollectionCiphers\n                              join c in dbContext.Collections\n                                  on cc.CollectionId equals c.Id\n                              where c.OrganizationId == organizationId\n                              select cc).ToArrayAsync();\n            return data;\n        }\n    }\n\n    public async Task<ICollection<CollectionCipher>> GetManySharedByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var data = await (from cc in dbContext.CollectionCiphers\n                              join c in dbContext.Collections\n                                  on cc.CollectionId equals c.Id\n                              where c.OrganizationId == organizationId\n                                    && c.Type == Core.Enums.CollectionType.SharedCollection\n                              select cc).ToArrayAsync();\n            return data;\n        }\n    }\n\n    public async Task<ICollection<CollectionCipher>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var data = await new CollectionCipherReadByUserIdQuery(userId)\n                .Run(dbContext)\n                .ToArrayAsync();\n            return data;\n        }\n    }\n\n    public async Task<ICollection<CollectionCipher>> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var data = await new CollectionCipherReadByUserIdCipherIdQuery(userId, cipherId)\n                .Run(dbContext)\n                .ToArrayAsync();\n            return data;\n        }\n    }\n\n    public async Task UpdateCollectionsAsync(Guid cipherId, Guid userId, IEnumerable<Guid> collectionIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var organizationId = await dbContext.Ciphers\n                .Where(c => c.Id == cipherId)\n                .Select(c => c.OrganizationId)\n                .FirstAsync();\n\n            var availableCollectionsQuery = new CollectionsReadByOrganizationIdUserIdQuery(organizationId, userId);\n            var availableCollections = await availableCollectionsQuery\n                .Run(dbContext)\n                .Select(c => c.Id).ToListAsync();\n\n            var collectionCiphers = await (from cc in dbContext.CollectionCiphers\n                                           where cc.CipherId == cipherId\n                                           select cc).ToListAsync();\n\n            foreach (var requestedCollectionId in collectionIds)\n            {\n                // I don't totally agree with t.CipherId = cipherId here because that should have been guaranteed by\n                // the WHERE above but the SQL Server CTE has it\n                var existingCollectionCipher = collectionCiphers\n                    .FirstOrDefault(t => t.CollectionId == requestedCollectionId && t.CipherId == cipherId);\n                // requestedCollectionId = SOURCE\n                // existingCollectionCipher = TARGET\n\n                // They have to want it selected and it has to exist\n                if (existingCollectionCipher == null && availableCollections.Contains(requestedCollectionId))\n                {\n                    // WHEN NOT MATCHED BY TARGET AND ...\n                    dbContext.CollectionCiphers.Add(new Models.CollectionCipher\n                    {\n                        CollectionId = requestedCollectionId,\n                        CipherId = cipherId,\n                    });\n                }\n\n                // If it has fallen to here it's requested but not actually available to don't add anything\n            }\n\n            // Now we need to remove collection ciphers that are no longer requested\n            dbContext.CollectionCiphers.RemoveRange(collectionCiphers.Where(cc => !collectionIds.Contains(cc.CollectionId) && cc.CipherId == cipherId));\n\n            if (organizationId.HasValue)\n            {\n                await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId.Value);\n            }\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task UpdateCollectionsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable<Guid> collectionIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var availableCollectionIds = await (from c in dbContext.Collections\n                                                where c.OrganizationId == organizationId\n                                                && c.Type != CollectionType.DefaultUserCollection\n                                                select c.Id).ToListAsync();\n\n            var currentCollectionCiphers = await (from cc in dbContext.CollectionCiphers\n                                                  where cc.CipherId == cipherId\n                                                  select cc).ToListAsync();\n\n            foreach (var requestedCollectionId in collectionIds)\n            {\n                if (!availableCollectionIds.Contains(requestedCollectionId)) continue;\n\n                var requestedCollectionCipher = currentCollectionCiphers\n                    .FirstOrDefault(cc => cc.CollectionId == requestedCollectionId);\n\n                if (requestedCollectionCipher == null)\n                {\n                    dbContext.CollectionCiphers.Add(new Models.CollectionCipher\n                    {\n                        CipherId = cipherId,\n                        CollectionId = requestedCollectionId,\n                    });\n                }\n            }\n\n            dbContext.RemoveRange(currentCollectionCiphers.Where(cc => availableCollectionIds.Contains(cc.CollectionId) && !collectionIds.Contains(cc.CollectionId)));\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task UpdateCollectionsForCiphersAsync(IEnumerable<Guid> cipherIds, Guid userId, Guid organizationId, IEnumerable<Guid> collectionIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var availableCollectionsQuery = new CollectionsReadByOrganizationIdUserIdQuery(organizationId, userId);\n            var availableCollections = availableCollectionsQuery\n                .Run(dbContext);\n\n            if (await availableCollections.CountAsync() < 1)\n            {\n                return;\n            }\n\n            var insertData = from collectionId in collectionIds\n                             from cipherId in cipherIds\n                             where availableCollections.Select(c => c.Id).Contains(collectionId)\n                             select new Models.CollectionCipher\n                             {\n                                 CollectionId = collectionId,\n                                 CipherId = cipherId,\n                             };\n            await dbContext.AddRangeAsync(insertData);\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task AddCollectionsForManyCiphersAsync(Guid organizationId, IEnumerable<Guid> cipherIds,\n        IEnumerable<Guid> collectionIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var availableCollections = await (from c in dbContext.Collections\n                                              join o in dbContext.Organizations on c.OrganizationId equals o.Id\n                                              where o.Id == organizationId && o.Enabled\n                                              select c).ToListAsync();\n\n            var currentCollectionCiphers = await (from cc in dbContext.CollectionCiphers\n                                                  where cipherIds.Contains(cc.CipherId)\n                                                  select cc).ToListAsync();\n\n            var insertData = from collectionId in collectionIds\n                             from cipherId in cipherIds\n                             where\n                                 availableCollections.Select(c => c.Id).Contains(collectionId) &&\n                                 !currentCollectionCiphers.Any(cc => cc.CipherId == cipherId && cc.CollectionId == collectionId)\n                             select new Models.CollectionCipher { CollectionId = collectionId, CipherId = cipherId, };\n\n            await dbContext.AddRangeAsync(insertData);\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task RemoveCollectionsForManyCiphersAsync(Guid organizationId, IEnumerable<Guid> cipherIds,\n        IEnumerable<Guid> collectionIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var currentCollectionCiphersToBeRemoved = await (from cc in dbContext.CollectionCiphers\n                                                             where cipherIds.Contains(cc.CipherId) && collectionIds.Contains(cc.CollectionId)\n                                                             select cc).ToListAsync();\n            dbContext.RemoveRange(currentCollectionCiphersToBeRemoved);\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Collections;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class CollectionRepository : Repository<Core.Entities.Collection, Collection, Guid>, ICollectionRepository\n{\n    public CollectionRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Collections)\n    { }\n\n    public override async Task<Core.Entities.Collection> CreateAsync(Core.Entities.Collection collection)\n    {\n        await base.CreateAsync(collection);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.UserBumpAccountRevisionDateByCollectionIdAsync(collection.Id, collection.OrganizationId);\n            await dbContext.SaveChangesAsync();\n        }\n        return collection;\n    }\n\n    public override async Task DeleteAsync(Core.Entities.Collection collection)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.UserBumpAccountRevisionDateByCollectionIdAsync(collection.Id, collection.OrganizationId);\n            await dbContext.SaveChangesAsync();\n        }\n        await base.DeleteAsync(collection);\n    }\n\n    public override async Task UpsertAsync(Core.Entities.Collection collection)\n    {\n        await base.UpsertAsync(collection);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await dbContext.UserBumpAccountRevisionDateByCollectionIdAsync(collection.Id, collection.OrganizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task CreateAsync(Core.Entities.Collection obj, IEnumerable<CollectionAccessSelection>? groups, IEnumerable<CollectionAccessSelection>? users)\n    {\n        await CreateAsync(obj);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            if (groups != null)\n            {\n                var availableGroups = await (from g in dbContext.Groups\n                                             where g.OrganizationId == obj.OrganizationId\n                                             select g.Id).ToListAsync();\n                var collectionGroups = groups\n                    .Where(g => availableGroups.Contains(g.Id))\n                    .Select(g => new CollectionGroup\n                    {\n                        CollectionId = obj.Id,\n                        GroupId = g.Id,\n                        ReadOnly = g.ReadOnly,\n                        HidePasswords = g.HidePasswords,\n                        Manage = g.Manage\n                    });\n                await dbContext.AddRangeAsync(collectionGroups);\n            }\n\n            if (users != null)\n            {\n                var availableUsers = await (from g in dbContext.OrganizationUsers\n                                            where g.OrganizationId == obj.OrganizationId\n                                            select g.Id).ToListAsync();\n                var collectionUsers = users\n                    .Where(u => availableUsers.Contains(u.Id))\n                    .Select(u => new CollectionUser\n                    {\n                        CollectionId = obj.Id,\n                        OrganizationUserId = u.Id,\n                        ReadOnly = u.ReadOnly,\n                        HidePasswords = u.HidePasswords,\n                        Manage = u.Manage\n                    });\n                await dbContext.AddRangeAsync(collectionUsers);\n            }\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(obj.OrganizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task DeleteUserAsync(Guid collectionId, Guid organizationUserId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from cu in dbContext.CollectionUsers\n                        where cu.CollectionId == collectionId &&\n                            cu.OrganizationUserId == organizationUserId\n                        select cu;\n            dbContext.RemoveRange(await query.ToListAsync());\n            await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<Tuple<Core.Entities.Collection?, CollectionAccessDetails>> GetByIdWithAccessAsync(Guid id)\n    {\n        var collection = await base.GetByIdAsync(id);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var groupQuery = from cg in dbContext.CollectionGroups\n                             where cg.CollectionId.Equals(id)\n                             select new CollectionAccessSelection\n                             {\n                                 Id = cg.GroupId,\n                                 ReadOnly = cg.ReadOnly,\n                                 HidePasswords = cg.HidePasswords,\n                                 Manage = cg.Manage\n                             };\n            var groups = await groupQuery.ToArrayAsync();\n\n            var userQuery = from cg in dbContext.CollectionUsers\n                            where cg.CollectionId.Equals(id)\n                            select new CollectionAccessSelection\n                            {\n                                Id = cg.OrganizationUserId,\n                                ReadOnly = cg.ReadOnly,\n                                HidePasswords = cg.HidePasswords,\n                                Manage = cg.Manage\n                            };\n            var users = await userQuery.ToArrayAsync();\n            var access = new CollectionAccessDetails { Users = users, Groups = groups };\n\n            return new Tuple<Core.Entities.Collection?, CollectionAccessDetails>(collection, access);\n        }\n    }\n\n    public async Task<ICollection<Tuple<Core.Entities.Collection, CollectionAccessDetails>>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId)\n    {\n        var collections = await GetManyByOrganizationIdAsync(organizationId);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var groups =\n                from c in collections\n                join cg in dbContext.CollectionGroups on c.Id equals cg.CollectionId\n                group cg by cg.CollectionId into g\n                select g;\n            var users =\n                from c in collections\n                join cu in dbContext.CollectionUsers on c.Id equals cu.CollectionId\n                group cu by cu.CollectionId into u\n                select u;\n\n            return collections.Select(collection =>\n                new Tuple<Core.Entities.Collection, CollectionAccessDetails>(\n                    collection,\n                    new CollectionAccessDetails\n                    {\n                        Groups = groups\n                            .FirstOrDefault(g => g.Key == collection.Id)?\n                            .Select(g => new CollectionAccessSelection\n                            {\n                                Id = g.GroupId,\n                                HidePasswords = g.HidePasswords,\n                                ReadOnly = g.ReadOnly,\n                                Manage = g.Manage\n                            }).ToList() ?? new List<CollectionAccessSelection>(),\n                        Users = users\n                            .FirstOrDefault(u => u.Key == collection.Id)?\n                            .Select(c => new CollectionAccessSelection\n                            {\n                                Id = c.OrganizationUserId,\n                                HidePasswords = c.HidePasswords,\n                                ReadOnly = c.ReadOnly,\n                                Manage = c.Manage\n                            }).ToList() ?? new List<CollectionAccessSelection>()\n                    }\n                )\n            ).ToList();\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.Collection>> GetManyByManyIdsAsync(IEnumerable<Guid> collectionIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from c in dbContext.Collections\n                        where collectionIds.Contains(c.Id)\n                        select c;\n            var data = await query.ToArrayAsync();\n            return data;\n        }\n    }\n\n    public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)\n    {\n        var query = new CollectionReadCountByOrganizationIdQuery(organizationId);\n        return await GetCountFromQuery(query);\n    }\n\n    public async Task<ICollection<Core.Entities.Collection>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from c in dbContext.Collections\n                        where c.OrganizationId == organizationId\n                        select c;\n            var collections = await query.ToArrayAsync();\n            return collections;\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.Collection>> GetManySharedCollectionsByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from c in dbContext.Collections\n                        where c.OrganizationId == organizationId &&\n                            c.Type == CollectionType.SharedCollection\n                        select c;\n            var collections = await query.ToArrayAsync();\n            return collections;\n        }\n    }\n\n    public async Task<ICollection<CollectionDetails>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var baseCollectionQuery = new UserCollectionDetailsQuery(userId).Run(dbContext);\n\n            if (dbContext.Database.IsSqlite())\n            {\n                return (await baseCollectionQuery.ToListAsync())\n                    .GroupBy(c => new\n                    {\n                        c.Id,\n                        c.OrganizationId,\n                        c.Name,\n                        c.CreationDate,\n                        c.RevisionDate,\n                        c.ExternalId,\n                        c.Type\n                    })\n                    .Select(collectionGroup => new CollectionDetails\n                    {\n                        Id = collectionGroup.Key.Id,\n                        OrganizationId = collectionGroup.Key.OrganizationId,\n                        Name = collectionGroup.Key.Name,\n                        CreationDate = collectionGroup.Key.CreationDate,\n                        RevisionDate = collectionGroup.Key.RevisionDate,\n                        ExternalId = collectionGroup.Key.ExternalId,\n                        ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),\n                        HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),\n                        Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),\n                        Type = collectionGroup.Key.Type,\n                    })\n                    .ToList();\n            }\n\n            return await (from c in baseCollectionQuery\n                          group c by new\n                          {\n                              c.Id,\n                              c.OrganizationId,\n                              c.Name,\n                              c.CreationDate,\n                              c.RevisionDate,\n                              c.ExternalId,\n                              c.Type\n                          } into collectionGroup\n                          select new CollectionDetails\n                          {\n                              Id = collectionGroup.Key.Id,\n                              OrganizationId = collectionGroup.Key.OrganizationId,\n                              Name = collectionGroup.Key.Name,\n                              CreationDate = collectionGroup.Key.CreationDate,\n                              RevisionDate = collectionGroup.Key.RevisionDate,\n                              ExternalId = collectionGroup.Key.ExternalId,\n                              ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),\n                              HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),\n                              Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),\n                              Type = collectionGroup.Key.Type,\n                          }).ToListAsync();\n        }\n    }\n\n    public async Task<ICollection<CollectionAdminDetails>> GetManySharedByOrganizationIdWithPermissionsAsync(\n        Guid organizationId, Guid userId, bool includeAccessRelationships)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = CollectionAdminDetailsQuery.ByOrganizationId(organizationId, userId).Run(dbContext);\n\n            ICollection<CollectionAdminDetails> collections;\n\n            // SQLite does not support the GROUP BY clause\n            if (dbContext.Database.IsSqlite())\n            {\n                collections = (await query.ToListAsync())\n                    .GroupBy(c => new\n                    {\n                        c.Id,\n                        c.OrganizationId,\n                        c.Name,\n                        c.CreationDate,\n                        c.RevisionDate,\n                        c.ExternalId,\n                        c.Unmanaged,\n                        c.DefaultUserCollectionEmail\n                    }).Select(collectionGroup => new CollectionAdminDetails\n                    {\n                        Id = collectionGroup.Key.Id,\n                        OrganizationId = collectionGroup.Key.OrganizationId,\n                        Name = collectionGroup.Key.Name,\n                        CreationDate = collectionGroup.Key.CreationDate,\n                        RevisionDate = collectionGroup.Key.RevisionDate,\n                        ExternalId = collectionGroup.Key.ExternalId,\n                        ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),\n                        HidePasswords =\n                            Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),\n                        Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),\n                        Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),\n                        Unmanaged = collectionGroup.Key.Unmanaged,\n                        DefaultUserCollectionEmail = collectionGroup.Key.DefaultUserCollectionEmail\n                    }).ToList();\n            }\n            else\n            {\n                collections = await (from c in query\n                                     group c by new\n                                     {\n                                         c.Id,\n                                         c.OrganizationId,\n                                         c.Name,\n                                         c.CreationDate,\n                                         c.RevisionDate,\n                                         c.ExternalId,\n                                         c.Unmanaged,\n                                         c.DefaultUserCollectionEmail\n                                     }\n                    into collectionGroup\n                                     select new CollectionAdminDetails\n                                     {\n                                         Id = collectionGroup.Key.Id,\n                                         OrganizationId = collectionGroup.Key.OrganizationId,\n                                         Name = collectionGroup.Key.Name,\n                                         CreationDate = collectionGroup.Key.CreationDate,\n                                         RevisionDate = collectionGroup.Key.RevisionDate,\n                                         ExternalId = collectionGroup.Key.ExternalId,\n                                         ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),\n                                         HidePasswords =\n                                             Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),\n                                         Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),\n                                         Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),\n                                         Unmanaged = collectionGroup.Key.Unmanaged,\n                                         DefaultUserCollectionEmail = collectionGroup.Key.DefaultUserCollectionEmail\n                                     }).ToListAsync();\n            }\n\n            if (!includeAccessRelationships)\n            {\n                return collections;\n            }\n\n            var groups = (from c in collections\n                          join cg in dbContext.CollectionGroups on c.Id equals cg.CollectionId\n                          group cg by cg.CollectionId into g\n                          select g).ToList();\n\n            var users = (from c in collections\n                         join cu in dbContext.CollectionUsers on c.Id equals cu.CollectionId\n                         group cu by cu.CollectionId into u\n                         select u).ToList();\n\n            foreach (var collection in collections)\n            {\n                collection.Groups = groups\n                    .FirstOrDefault(g => g.Key == collection.Id)?\n                    .Select(g => new CollectionAccessSelection\n                    {\n                        Id = g.GroupId,\n                        HidePasswords = g.HidePasswords,\n                        ReadOnly = g.ReadOnly,\n                        Manage = g.Manage,\n                    }).ToList() ?? new List<CollectionAccessSelection>();\n                collection.Users = users\n                    .FirstOrDefault(u => u.Key == collection.Id)?\n                    .Select(c => new CollectionAccessSelection\n                    {\n                        Id = c.OrganizationUserId,\n                        HidePasswords = c.HidePasswords,\n                        ReadOnly = c.ReadOnly,\n                        Manage = c.Manage\n                    }).ToList() ?? new List<CollectionAccessSelection>();\n            }\n\n            return collections;\n        }\n    }\n\n    public async Task<CollectionAdminDetails?> GetByIdWithPermissionsAsync(Guid collectionId, Guid? userId,\n        bool includeAccessRelationships)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = CollectionAdminDetailsQuery.ByCollectionId(collectionId, userId).Run(dbContext);\n\n            CollectionAdminDetails? collectionDetails;\n\n            // SQLite does not support the GROUP BY clause\n            if (dbContext.Database.IsSqlite())\n            {\n                collectionDetails = (await query.ToListAsync())\n                    .GroupBy(c => new\n                    {\n                        c.Id,\n                        c.OrganizationId,\n                        c.Name,\n                        c.CreationDate,\n                        c.RevisionDate,\n                        c.ExternalId\n                    }).Select(collectionGroup => new CollectionAdminDetails\n                    {\n                        Id = collectionGroup.Key.Id,\n                        OrganizationId = collectionGroup.Key.OrganizationId,\n                        Name = collectionGroup.Key.Name,\n                        CreationDate = collectionGroup.Key.CreationDate,\n                        RevisionDate = collectionGroup.Key.RevisionDate,\n                        ExternalId = collectionGroup.Key.ExternalId,\n                        ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),\n                        HidePasswords =\n                            Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),\n                        Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),\n                        Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),\n                        Unmanaged = collectionGroup.Select(c => c.Unmanaged).FirstOrDefault()\n                    }).FirstOrDefault();\n            }\n            else\n            {\n                collectionDetails = await (from c in query\n                                           group c by new\n                                           {\n                                               c.Id,\n                                               c.OrganizationId,\n                                               c.Name,\n                                               c.CreationDate,\n                                               c.RevisionDate,\n                                               c.ExternalId\n                                           }\n                    into collectionGroup\n                                           select new CollectionAdminDetails\n                                           {\n                                               Id = collectionGroup.Key.Id,\n                                               OrganizationId = collectionGroup.Key.OrganizationId,\n                                               Name = collectionGroup.Key.Name,\n                                               CreationDate = collectionGroup.Key.CreationDate,\n                                               RevisionDate = collectionGroup.Key.RevisionDate,\n                                               ExternalId = collectionGroup.Key.ExternalId,\n                                               ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),\n                                               HidePasswords =\n                                                   Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),\n                                               Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),\n                                               Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),\n                                               Unmanaged = collectionGroup.Select(c => c.Unmanaged).FirstOrDefault()\n                                           }).FirstOrDefaultAsync();\n            }\n\n            if (!includeAccessRelationships)\n            {\n                return collectionDetails;\n            }\n\n            var groupsQuery = from cg in dbContext.CollectionGroups\n                              where cg.CollectionId.Equals(collectionId)\n                              select new CollectionAccessSelection\n                              {\n                                  Id = cg.GroupId,\n                                  ReadOnly = cg.ReadOnly,\n                                  HidePasswords = cg.HidePasswords,\n                                  Manage = cg.Manage\n                              };\n            // TODO-NRE: Probably need to null check and return early\n            collectionDetails!.Groups = await groupsQuery.ToListAsync();\n\n            var usersQuery = from cg in dbContext.CollectionUsers\n                             where cg.CollectionId.Equals(collectionId)\n                             select new CollectionAccessSelection\n                             {\n                                 Id = cg.OrganizationUserId,\n                                 ReadOnly = cg.ReadOnly,\n                                 HidePasswords = cg.HidePasswords,\n                                 Manage = cg.Manage\n                             };\n            collectionDetails.Users = await usersQuery.ToListAsync();\n\n            return collectionDetails;\n        }\n    }\n\n    public async Task<ICollection<CollectionAccessSelection>> GetManyUsersByIdAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from cu in dbContext.CollectionUsers\n                        where cu.CollectionId == id\n                        select cu;\n            var collectionUsers = await query.ToListAsync();\n            return collectionUsers.Select(cu => new CollectionAccessSelection\n            {\n                Id = cu.OrganizationUserId,\n                ReadOnly = cu.ReadOnly,\n                HidePasswords = cu.HidePasswords,\n                Manage = cu.Manage\n            }).ToArray();\n        }\n    }\n\n    public async Task ReplaceAsync(Core.Entities.Collection collection, IEnumerable<CollectionAccessSelection>? groups,\n        IEnumerable<CollectionAccessSelection>? users)\n    {\n        await UpsertAsync(collection);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            if (groups != null)\n            {\n                await ReplaceCollectionGroupsAsync(dbContext, collection, groups);\n            }\n            if (users != null)\n            {\n                await ReplaceCollectionUsersAsync(dbContext, collection, users);\n            }\n            await dbContext.UserBumpAccountRevisionDateByCollectionIdAsync(collection.Id, collection.OrganizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task UpdateUsersAsync(Guid id, IEnumerable<CollectionAccessSelection> requestedUsers)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var organizationId = await dbContext.Collections\n                .Where(c => c.Id == id)\n                .Select(c => c.OrganizationId)\n                .FirstOrDefaultAsync();\n\n            var existingCollectionUsers = await dbContext.CollectionUsers\n                .Where(cu => cu.CollectionId == id)\n                .ToListAsync();\n\n            foreach (var requestedUser in requestedUsers)\n            {\n                var existingCollectionUser = existingCollectionUsers.FirstOrDefault(cu => cu.OrganizationUserId == requestedUser.Id);\n                if (existingCollectionUser == null)\n                {\n                    // This is a brand new entry\n                    dbContext.CollectionUsers.Add(new CollectionUser\n                    {\n                        CollectionId = id,\n                        OrganizationUserId = requestedUser.Id,\n                        HidePasswords = requestedUser.HidePasswords,\n                        ReadOnly = requestedUser.ReadOnly,\n                        Manage = requestedUser.Manage\n                    });\n                    continue;\n                }\n\n                // It already exists, update it\n                existingCollectionUser.HidePasswords = requestedUser.HidePasswords;\n                existingCollectionUser.ReadOnly = requestedUser.ReadOnly;\n                existingCollectionUser.Manage = requestedUser.Manage;\n                dbContext.CollectionUsers.Update(existingCollectionUser);\n            }\n\n            // Remove all existing ones that are no longer requested\n            var requestedUserIds = requestedUsers.Select(u => u.Id);\n            dbContext.CollectionUsers.RemoveRange(existingCollectionUsers.Where(cu => !requestedUserIds.Contains(cu.OrganizationUserId)));\n            // Need to save the new collection users before running the bump revision code\n            await dbContext.SaveChangesAsync();\n            await dbContext.UserBumpAccountRevisionDateByCollectionIdAsync(id, organizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> collectionIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var collectionGroupEntities = await dbContext.CollectionGroups\n                .Where(cg => collectionIds.Contains(cg.CollectionId))\n                .ToListAsync();\n            var collectionEntities = await dbContext.Collections\n                .Where(c => collectionIds.Contains(c.Id))\n                .ToListAsync();\n\n            dbContext.CollectionGroups.RemoveRange(collectionGroupEntities);\n            dbContext.Collections.RemoveRange(collectionEntities);\n            await dbContext.SaveChangesAsync();\n\n            foreach (var collection in collectionEntities.GroupBy(g => g.OrganizationId))\n            {\n                await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(collection.Key);\n            }\n        }\n    }\n\n    public async Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> collectionIds,\n        IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var collectionIdsList = collectionIds.ToList();\n\n            if (users != null)\n            {\n                var existingCollectionUsers = await dbContext.CollectionUsers\n                    .Where(cu => collectionIdsList.Contains(cu.CollectionId))\n                    .ToDictionaryAsync(x => (x.CollectionId, x.OrganizationUserId));\n\n                var requestedUsers = users.ToList();\n\n                foreach (var collectionId in collectionIdsList)\n                {\n                    foreach (var requestedUser in requestedUsers)\n                    {\n                        if (!existingCollectionUsers.TryGetValue(\n                                (collectionId, requestedUser.Id),\n                                out var existingCollectionUser)\n                            )\n                        {\n                            // This is a brand new entry\n                            dbContext.CollectionUsers.Add(new CollectionUser\n                            {\n                                CollectionId = collectionId,\n                                OrganizationUserId = requestedUser.Id,\n                                HidePasswords = requestedUser.HidePasswords,\n                                ReadOnly = requestedUser.ReadOnly,\n                                Manage = requestedUser.Manage\n                            });\n                            continue;\n                        }\n\n                        // It already exists, update it\n                        existingCollectionUser.HidePasswords = requestedUser.HidePasswords;\n                        existingCollectionUser.ReadOnly = requestedUser.ReadOnly;\n                        existingCollectionUser.Manage = requestedUser.Manage;\n                        dbContext.CollectionUsers.Update(existingCollectionUser);\n                    }\n                }\n            }\n\n            if (groups != null)\n            {\n                var existingCollectionGroups = await dbContext.CollectionGroups\n                    .Where(cu => collectionIdsList.Contains(cu.CollectionId))\n                    .ToDictionaryAsync(x => (x.CollectionId, x.GroupId));\n\n                var requestedGroups = groups.ToList();\n\n                foreach (var collectionId in collectionIdsList)\n                {\n                    foreach (var requestedGroup in requestedGroups)\n                    {\n                        if (!existingCollectionGroups.TryGetValue(\n                                (collectionId, requestedGroup.Id),\n                                out var existingCollectionGroup)\n                           )\n                        {\n                            // This is a brand new entry\n                            dbContext.CollectionGroups.Add(new CollectionGroup()\n                            {\n                                CollectionId = collectionId,\n                                GroupId = requestedGroup.Id,\n                                HidePasswords = requestedGroup.HidePasswords,\n                                ReadOnly = requestedGroup.ReadOnly,\n                                Manage = requestedGroup.Manage\n                            });\n                            continue;\n                        }\n\n                        // It already exists, update it\n                        existingCollectionGroup.HidePasswords = requestedGroup.HidePasswords;\n                        existingCollectionGroup.ReadOnly = requestedGroup.ReadOnly;\n                        existingCollectionGroup.Manage = requestedGroup.Manage;\n                        dbContext.CollectionGroups.Update(existingCollectionGroup);\n                    }\n                }\n            }\n            // Need to save the new collection users/groups before running the bump revision code\n            await dbContext.SaveChangesAsync();\n            await dbContext.UserBumpAccountRevisionDateByCollectionIdsAsync(collectionIdsList, organizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n\n    private static async Task ReplaceCollectionGroupsAsync(DatabaseContext dbContext, Core.Entities.Collection collection, IEnumerable<CollectionAccessSelection> groups)\n    {\n        var existingCollectionGroups = await dbContext.CollectionGroups\n            .Where(cg => cg.CollectionId == collection.Id)\n            .ToDictionaryAsync(cg => cg.GroupId);\n\n        foreach (var group in groups)\n        {\n            if (existingCollectionGroups.TryGetValue(group.Id, out var existingCollectionGroup))\n            {\n                // It already exists, update it\n                existingCollectionGroup.HidePasswords = group.HidePasswords;\n                existingCollectionGroup.ReadOnly = group.ReadOnly;\n                existingCollectionGroup.Manage = group.Manage;\n                dbContext.CollectionGroups.Update(existingCollectionGroup);\n            }\n            else\n            {\n                // This is a brand new entry, add it\n                dbContext.CollectionGroups.Add(new CollectionGroup\n                {\n                    GroupId = group.Id,\n                    CollectionId = collection.Id,\n                    HidePasswords = group.HidePasswords,\n                    ReadOnly = group.ReadOnly,\n                    Manage = group.Manage,\n                });\n            }\n        }\n\n        var requestedGroupIds = groups.Select(g => g.Id).ToArray();\n        var toDelete = existingCollectionGroups.Values.Where(cg => !requestedGroupIds.Contains(cg.GroupId));\n        dbContext.CollectionGroups.RemoveRange(toDelete);\n        // SaveChangesAsync is expected to be called outside this method\n    }\n\n    private static async Task ReplaceCollectionUsersAsync(DatabaseContext dbContext, Core.Entities.Collection collection, IEnumerable<CollectionAccessSelection> users)\n    {\n        var existingCollectionUsers = await dbContext.CollectionUsers\n            .Where(cu => cu.CollectionId == collection.Id)\n            .ToDictionaryAsync(cu => cu.OrganizationUserId);\n\n        foreach (var user in users)\n        {\n            if (existingCollectionUsers.TryGetValue(user.Id, out var existingCollectionUser))\n            {\n                // This is an existing entry, update it.\n                existingCollectionUser.HidePasswords = user.HidePasswords;\n                existingCollectionUser.ReadOnly = user.ReadOnly;\n                existingCollectionUser.Manage = user.Manage;\n                dbContext.CollectionUsers.Update(existingCollectionUser);\n            }\n            else\n            {\n                // This is a brand new entry, add it\n                dbContext.CollectionUsers.Add(new CollectionUser\n                {\n                    OrganizationUserId = user.Id,\n                    CollectionId = collection.Id,\n                    HidePasswords = user.HidePasswords,\n                    ReadOnly = user.ReadOnly,\n                    Manage = user.Manage,\n                });\n            }\n        }\n\n        var requestedUserIds = users.Select(u => u.Id).ToArray();\n        var toDelete = existingCollectionUsers.Values.Where(cu => !requestedUserIds.Contains(cu.OrganizationUserId));\n        dbContext.CollectionUsers.RemoveRange(toDelete);\n        // SaveChangesAsync is expected to be called outside this method\n    }\n\n    public async Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)\n    {\n        organizationUserIds = organizationUserIds.ToList();\n        if (!organizationUserIds.Any())\n        {\n            return;\n        }\n\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(dbContext, organizationId);\n        var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection);\n\n        var (collections, collectionUsers) = CollectionUtils.BuildDefaultUserCollections(organizationId, missingDefaultCollectionUserIds, defaultCollectionName);\n\n        if (!collections.Any() || !collectionUsers.Any())\n        {\n            return;\n        }\n\n        await dbContext.Collections.AddRangeAsync(Mapper.Map<IEnumerable<Collection>>(collections));\n        await dbContext.CollectionUsers.AddRangeAsync(Mapper.Map<IEnumerable<CollectionUser>>(collectionUsers));\n\n        await dbContext.SaveChangesAsync();\n    }\n\n    private async Task<HashSet<Guid>> GetOrgUserIdsWithDefaultCollectionAsync(DatabaseContext dbContext, Guid organizationId)\n    {\n        var results = await dbContext.OrganizationUsers\n                 .Where(ou => ou.OrganizationId == organizationId)\n                 .Join(\n                     dbContext.CollectionUsers,\n                     ou => ou.Id,\n                     cu => cu.OrganizationUserId,\n                     (ou, cu) => new { ou, cu }\n                 )\n                 .Join(\n                     dbContext.Collections,\n                     temp => temp.cu.CollectionId,\n                     c => c.Id,\n                     (temp, c) => new { temp.ou, Collection = c }\n                 )\n                 .Where(x => x.Collection.Type == CollectionType.DefaultUserCollection)\n                 .Select(x => x.ou.Id)\n                 .ToListAsync();\n\n        return results.ToHashSet();\n    }\n\n    public Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds,\n        string defaultCollectionName) =>\n        CreateDefaultCollectionsAsync(organizationId, organizationUserIds, defaultCollectionName);\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Dirt.Reports.Models.Data;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;\nusing Bit.Infrastructure.EntityFramework.Auth.Models;\nusing Bit.Infrastructure.EntityFramework.Billing.Models;\nusing Bit.Infrastructure.EntityFramework.Converters;\nusing Bit.Infrastructure.EntityFramework.Dirt.Models;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.NotificationCenter.Models;\nusing Bit.Infrastructure.EntityFramework.Platform;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\nusing Microsoft.EntityFrameworkCore.Storage.ValueConversion;\nusing DP = Microsoft.AspNetCore.DataProtection;\n\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class DatabaseContext : DbContext\n{\n    public const string postgresIndetermanisticCollation = \"postgresIndetermanisticCollation\";\n\n    public DatabaseContext(DbContextOptions<DatabaseContext> options)\n        : base(options)\n    { }\n\n    public DbSet<AccessPolicy> AccessPolicies { get; set; }\n    public DbSet<UserProjectAccessPolicy> UserProjectAccessPolicy { get; set; }\n    public DbSet<GroupProjectAccessPolicy> GroupProjectAccessPolicy { get; set; }\n    public DbSet<ServiceAccountProjectAccessPolicy> ServiceAccountProjectAccessPolicy { get; set; }\n    public DbSet<UserServiceAccountAccessPolicy> UserServiceAccountAccessPolicy { get; set; }\n    public DbSet<GroupServiceAccountAccessPolicy> GroupServiceAccountAccessPolicy { get; set; }\n    public DbSet<UserSecretAccessPolicy> UserSecretAccessPolicy { get; set; }\n    public DbSet<GroupSecretAccessPolicy> GroupSecretAccessPolicy { get; set; }\n    public DbSet<ServiceAccountSecretAccessPolicy> ServiceAccountSecretAccessPolicy { get; set; }\n    public DbSet<ApiKey> ApiKeys { get; set; }\n    public DbSet<Cache> Cache { get; set; }\n    public DbSet<Cipher> Ciphers { get; set; }\n    public DbSet<Collection> Collections { get; set; }\n    public DbSet<CollectionCipher> CollectionCiphers { get; set; }\n    public DbSet<CollectionGroup> CollectionGroups { get; set; }\n    public DbSet<CollectionUser> CollectionUsers { get; set; }\n    public DbSet<Device> Devices { get; set; }\n    public DbSet<EmergencyAccess> EmergencyAccesses { get; set; }\n    public DbSet<Event> Events { get; set; }\n    public DbSet<Folder> Folders { get; set; }\n    public DbSet<Grant> Grants { get; set; }\n    public DbSet<Group> Groups { get; set; }\n    public DbSet<GroupUser> GroupUsers { get; set; }\n    public DbSet<Installation> Installations { get; set; }\n    public DbSet<Organization> Organizations { get; set; }\n    public DbSet<OrganizationApiKey> OrganizationApiKeys { get; set; }\n    public DbSet<OrganizationSponsorship> OrganizationSponsorships { get; set; }\n    public DbSet<OrganizationConnection> OrganizationConnections { get; set; }\n    public DbSet<PlayItem> PlayItem { get; set; }\n    public DbSet<OrganizationIntegration> OrganizationIntegrations { get; set; }\n    public DbSet<OrganizationIntegrationConfiguration> OrganizationIntegrationConfigurations { get; set; }\n    public DbSet<OrganizationUser> OrganizationUsers { get; set; }\n    public DbSet<Policy> Policies { get; set; }\n    public DbSet<Provider> Providers { get; set; }\n    public DbSet<Secret> Secret { get; set; }\n    public DbSet<SecretVersion> SecretVersion { get; set; }\n    public DbSet<ServiceAccount> ServiceAccount { get; set; }\n    public DbSet<Project> Project { get; set; }\n    public DbSet<ProviderUser> ProviderUsers { get; set; }\n    public DbSet<ProviderOrganization> ProviderOrganizations { get; set; }\n    public DbSet<Send> Sends { get; set; }\n    public DbSet<SsoConfig> SsoConfigs { get; set; }\n    public DbSet<SsoUser> SsoUsers { get; set; }\n    public DbSet<TaxRate> TaxRates { get; set; }\n    public DbSet<Transaction> Transactions { get; set; }\n    public DbSet<User> Users { get; set; }\n    public DbSet<UserSignatureKeyPair> UserSignatureKeyPairs { get; set; }\n    public DbSet<AuthRequest> AuthRequests { get; set; }\n    public DbSet<OrganizationDomain> OrganizationDomains { get; set; }\n    public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; }\n    public DbSet<ProviderPlan> ProviderPlans { get; set; }\n    public DbSet<ProviderInvoiceItem> ProviderInvoiceItems { get; set; }\n    public DbSet<SubscriptionDiscount> SubscriptionDiscounts { get; set; }\n    public DbSet<Notification> Notifications { get; set; }\n    public DbSet<NotificationStatus> NotificationStatuses { get; set; }\n    public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }\n    public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }\n    public DbSet<OrganizationMemberBaseDetail> OrganizationMemberBaseDetails { get; set; }\n    public DbSet<SecurityTask> SecurityTasks { get; set; }\n    public DbSet<OrganizationInstallation> OrganizationInstallations { get; set; }\n    public DbSet<OrganizationReport> OrganizationReports { get; set; }\n    public DbSet<OrganizationApplication> OrganizationApplications { get; set; }\n\n    protected override void OnModelCreating(ModelBuilder builder)\n    {\n        // Scans and loads all configurations implementing the `IEntityTypeConfiguration` from the\n        //  `Infrastructure.EntityFramework` Module. Note to get the assembly we can use a random class\n        //   from this module.\n        builder.ApplyConfigurationsFromAssembly(typeof(DatabaseContext).Assembly);\n\n        // Going forward use `IEntityTypeConfiguration` in the Configurations folder for managing\n        // Entity Framework code first database configurations.\n        var eCipher = builder.Entity<Cipher>();\n        var eCollection = builder.Entity<Collection>();\n        var eCollectionCipher = builder.Entity<CollectionCipher>();\n        var eCollectionUser = builder.Entity<CollectionUser>();\n        var eCollectionGroup = builder.Entity<CollectionGroup>();\n        var eEmergencyAccess = builder.Entity<EmergencyAccess>();\n        var eFolder = builder.Entity<Folder>();\n        var eGroup = builder.Entity<Group>();\n        var eGroupUser = builder.Entity<GroupUser>();\n        var eInstallation = builder.Entity<Installation>();\n        var eProvider = builder.Entity<Provider>();\n        var eProviderUser = builder.Entity<ProviderUser>();\n        var eProviderOrganization = builder.Entity<ProviderOrganization>();\n        var eSsoConfig = builder.Entity<SsoConfig>();\n        var eTaxRate = builder.Entity<TaxRate>();\n        var eUser = builder.Entity<User>();\n        var eOrganizationApiKey = builder.Entity<OrganizationApiKey>();\n        var eOrganizationConnection = builder.Entity<OrganizationConnection>();\n        var eOrganizationDomain = builder.Entity<OrganizationDomain>();\n        var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();\n        var eOrganizationMemberBaseDetail = builder.Entity<OrganizationMemberBaseDetail>();\n        var eSend = builder.Entity<Send>();\n\n        // Shadow property configurations go here\n\n        eCipher.Property(c => c.Id).ValueGeneratedNever();\n        eCollection.Property(c => c.Id).ValueGeneratedNever();\n        eEmergencyAccess.Property(c => c.Id).ValueGeneratedNever();\n        eFolder.Property(c => c.Id).ValueGeneratedNever();\n        eGroup.Property(c => c.Id).ValueGeneratedNever();\n        eInstallation.Property(c => c.Id).ValueGeneratedNever();\n        eProvider.Property(c => c.Id).ValueGeneratedNever();\n        eProviderUser.Property(c => c.Id).ValueGeneratedNever();\n        eProviderOrganization.Property(c => c.Id).ValueGeneratedNever();\n        eOrganizationApiKey.Property(c => c.Id).ValueGeneratedNever();\n        eOrganizationConnection.Property(c => c.Id).ValueGeneratedNever();\n        eOrganizationDomain.Property(ar => ar.Id).ValueGeneratedNever();\n        aWebAuthnCredential.Property(ar => ar.Id).ValueGeneratedNever();\n\n        eCollectionCipher.HasKey(cc => new { cc.CollectionId, cc.CipherId });\n        eCollectionUser.HasKey(cu => new { cu.CollectionId, cu.OrganizationUserId });\n        eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });\n        eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });\n\n        eOrganizationMemberBaseDetail.HasNoKey();\n\n        var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector(\n            Constants.DatabaseFieldProtectorPurpose);\n        var dataProtectionConverter = new DataProtectionConverter(dataProtector);\n        eUser.Property(c => c.Key).HasConversion(dataProtectionConverter);\n        eUser.Property(c => c.MasterPassword).HasConversion(dataProtectionConverter);\n        eSend.Property(c => c.Emails).HasConversion(dataProtectionConverter);\n\n        if (Database.IsNpgsql())\n        {\n            // the postgres provider doesn't currently support database level non-deterministic collations.\n            // see https://www.npgsql.org/efcore/misc/collations-and-case-sensitivity.html#database-collation\n            builder.HasCollation(postgresIndetermanisticCollation, locale: \"en-u-ks-primary\", provider: \"icu\", deterministic: false);\n            eUser.Property(e => e.Email).UseCollation(postgresIndetermanisticCollation);\n            builder.Entity<Organization>().Property(e => e.Identifier).UseCollation(postgresIndetermanisticCollation);\n            builder.Entity<SsoUser>().Property(e => e.ExternalId).UseCollation(postgresIndetermanisticCollation);\n            //\n        }\n\n        eCipher.ToTable(nameof(Cipher));\n        eCollection.ToTable(nameof(Collection));\n        eCollectionCipher.ToTable(nameof(CollectionCipher));\n        eEmergencyAccess.ToTable(nameof(EmergencyAccess));\n        eFolder.ToTable(nameof(Folder));\n        eGroup.ToTable(nameof(Group));\n        eGroupUser.ToTable(nameof(GroupUser));\n        eInstallation.ToTable(nameof(Installation));\n        eProvider.ToTable(nameof(Provider));\n        eProviderUser.ToTable(nameof(ProviderUser));\n        eProviderOrganization.ToTable(nameof(ProviderOrganization));\n        eSsoConfig.ToTable(nameof(SsoConfig));\n        eTaxRate.ToTable(nameof(TaxRate));\n        eOrganizationApiKey.ToTable(nameof(OrganizationApiKey));\n        eOrganizationConnection.ToTable(nameof(OrganizationConnection));\n        eOrganizationDomain.ToTable(nameof(OrganizationDomain));\n        aWebAuthnCredential.ToTable(nameof(WebAuthnCredential));\n\n        ConfigureDateTimeUtcQueries(builder);\n    }\n\n    // Make sure this is called after configuring all the entities as it iterates through all setup entities.\n    private void ConfigureDateTimeUtcQueries(ModelBuilder builder)\n    {\n        ValueConverter<DateTime, DateTime> converter;\n        if (Database.IsNpgsql())\n        {\n            converter = new ValueConverter<DateTime, DateTime>(\n                v => v,\n                d => new DateTimeOffset(d).UtcDateTime);\n        }\n        else\n        {\n            converter = new ValueConverter<DateTime, DateTime>(\n                v => v,\n                v => new DateTime(v.Ticks, DateTimeKind.Utc));\n        }\n\n        foreach (var entityType in builder.Model.GetEntityTypes())\n        {\n            if (entityType.IsKeyless)\n            {\n                continue;\n            }\n            foreach (var property in entityType.GetProperties())\n            {\n                if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?))\n                {\n                    property.SetValueConverter(converter);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs",
    "content": "﻿using System.Diagnostics;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic static class DatabaseContextExtensions\n{\n    /// <summary>\n    /// Bump the account revision date for the user.\n    /// The caller is responsible for providing a valid UserId (not a null or default Guid) for a user that exists\n    /// in the database.\n    /// </summary>\n    public static async Task UserBumpAccountRevisionDateAsync(this DatabaseContext context, Guid userId)\n    {\n        if (userId == Guid.Empty)\n        {\n            throw new ArgumentException(\"Invalid UserId.\");\n        }\n\n        var user = await context.Users.FindAsync(userId);\n        Debug.Assert(user is not null, \"The user id is expected to be validated as a true-in database user before making this call.\");\n        user.AccountRevisionDate = DateTime.UtcNow;\n    }\n\n    public static async Task UserBumpManyAccountRevisionDatesAsync(this DatabaseContext context, ICollection<Guid> userIds)\n    {\n        var users = context.Users.Where(u => userIds.Contains(u.Id));\n        var currentTime = DateTime.UtcNow;\n        await users.ForEachAsync(u =>\n        {\n            context.Attach(u);\n            u.AccountRevisionDate = currentTime;\n        });\n    }\n\n    public static async Task UserBumpAccountRevisionDateByOrganizationIdAsync(this DatabaseContext context, Guid organizationId)\n    {\n        var users = await (from u in context.Users\n                           join ou in context.OrganizationUsers on u.Id equals ou.UserId\n                           where ou.OrganizationId == organizationId && ou.Status == OrganizationUserStatusType.Confirmed\n                           select u).ToListAsync();\n\n        UpdateUserRevisionDate(users);\n    }\n\n    public static async Task UserBumpAccountRevisionDateByCipherIdAsync(this DatabaseContext context, Guid cipherId, Guid organizationId)\n    {\n        var query = new UserBumpAccountRevisionDateByCipherIdQuery(cipherId, organizationId);\n        var users = await query.Run(context).ToListAsync();\n        UpdateUserRevisionDate(users);\n    }\n\n    public static async Task UserBumpAccountRevisionDateByCollectionIdAsync(this DatabaseContext context, Guid collectionId, Guid organizationId)\n    {\n        var query = from u in context.Users\n                    join ou in context.OrganizationUsers\n                        on u.Id equals ou.UserId\n                    join cu in context.CollectionUsers\n                        on new { OrganizationUserId = ou.Id, CollectionId = collectionId } equals\n                        new { cu.OrganizationUserId, cu.CollectionId } into cu_g\n                    from cu in cu_g.DefaultIfEmpty()\n                    join gu in context.GroupUsers\n                        on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                        new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n                    from gu in gu_g.DefaultIfEmpty()\n                    join g in context.Groups\n                        on gu.GroupId equals g.Id into g_g\n                    from g in g_g.DefaultIfEmpty()\n                    join cg in context.CollectionGroups\n                        on new { gu.GroupId, CollectionId = collectionId } equals\n                        new { cg.GroupId, cg.CollectionId } into cg_g\n                    from cg in cg_g.DefaultIfEmpty()\n                    where ou.OrganizationId == organizationId &&\n                      ou.Status == OrganizationUserStatusType.Confirmed &&\n                        ((cu == null ? (Guid?)null : cu.CollectionId) != null ||\n                        (cg == null ? (Guid?)null : cg.CollectionId) != null)\n                    select u;\n\n        var users = await query.ToListAsync();\n        UpdateUserRevisionDate(users);\n    }\n\n    public static async Task UserBumpAccountRevisionDateByCollectionIdsAsync(this DatabaseContext context, IEnumerable<Guid> collectionIds, Guid organizationId)\n    {\n        var query = from u in context.Users\n                    from c in context.Collections\n                    join ou in context.OrganizationUsers\n                        on u.Id equals ou.UserId\n                    join cu in context.CollectionUsers\n                        on new { OrganizationUserId = ou.Id, CollectionId = c.Id } equals\n                        new { cu.OrganizationUserId, cu.CollectionId } into cu_g\n                    from cu in cu_g.DefaultIfEmpty()\n                    join gu in context.GroupUsers\n                        on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                        new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n                    from gu in gu_g.DefaultIfEmpty()\n                    join g in context.Groups\n                        on gu.GroupId equals g.Id into g_g\n                    from g in g_g.DefaultIfEmpty()\n                    join cg in context.CollectionGroups\n                        on new { gu.GroupId, CollectionId = c.Id } equals\n                        new { cg.GroupId, cg.CollectionId } into cg_g\n                    from cg in cg_g.DefaultIfEmpty()\n                    where ou.OrganizationId == organizationId && collectionIds.Contains(c.Id) &&\n                      ou.Status == OrganizationUserStatusType.Confirmed &&\n                        ((cu == null ? (Guid?)null : cu.CollectionId) != null ||\n                        (cg == null ? (Guid?)null : cg.CollectionId) != null)\n                    select u;\n\n        var users = await query.ToListAsync();\n        UpdateUserRevisionDate(users);\n    }\n\n    public static async Task UserBumpAccountRevisionDateByOrganizationUserIdAsync(this DatabaseContext context, Guid organizationUserId)\n    {\n        var query = from u in context.Users\n                    join ou in context.OrganizationUsers\n                      on u.Id equals ou.UserId\n                    where ou.Id == organizationUserId && ou.Status == OrganizationUserStatusType.Confirmed\n                    select u;\n\n        var users = await query.ToListAsync();\n        UpdateUserRevisionDate(users);\n    }\n\n    public static async Task UserBumpAccountRevisionDateByOrganizationUserIdsAsync(this DatabaseContext context, IEnumerable<Guid> organizationUserIds)\n    {\n        foreach (var organizationUserId in organizationUserIds)\n        {\n            await context.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserId);\n        }\n    }\n\n    public static async Task UserBumpAccountRevisionDateByEmergencyAccessGranteeIdAsync(this DatabaseContext context, Guid emergencyAccessId)\n    {\n        var query = from u in context.Users\n                    join ea in context.EmergencyAccesses on u.Id equals ea.GranteeId\n                    where ea.Id == emergencyAccessId && ea.Status == EmergencyAccessStatusType.Confirmed\n                    select u;\n\n        var users = await query.ToListAsync();\n\n        UpdateUserRevisionDate(users);\n    }\n\n    public static async Task UserBumpAccountRevisionDateByProviderIdAsync(this DatabaseContext context, Guid providerId)\n    {\n        var query = from u in context.Users\n                    join pu in context.ProviderUsers on u.Id equals pu.UserId\n                    where pu.ProviderId == providerId && pu.Status == ProviderUserStatusType.Confirmed\n                    select u;\n\n        var users = await query.ToListAsync();\n        UpdateUserRevisionDate(users);\n    }\n\n    public static async Task UserBumpAccountRevisionDateByProviderUserIdAsync(this DatabaseContext context, Guid providerUserId)\n    {\n        var query = from u in context.Users\n                    join pu in context.ProviderUsers on u.Id equals pu.UserId\n                    where pu.ProviderId == providerUserId && pu.Status == ProviderUserStatusType.Confirmed\n                    select u;\n\n        var users = await query.ToListAsync();\n        UpdateUserRevisionDate(users);\n    }\n\n    private static void UpdateUserRevisionDate(List<Models.User> users)\n    {\n        var time = DateTime.UtcNow;\n        foreach (var user in users)\n        {\n            user.AccountRevisionDate = time;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/DeviceRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class DeviceRepository : Repository<Core.Entities.Device, Device, Guid>, IDeviceRepository\n{\n    private readonly IGlobalSettings _globalSettings;\n\n    public DeviceRepository(\n        IServiceScopeFactory serviceScopeFactory,\n        IMapper mapper,\n        IGlobalSettings globalSettings\n        )\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Devices)\n    {\n        _globalSettings = globalSettings;\n    }\n\n    public async Task ClearPushTokenAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.Devices.Where(d => d.Id == id);\n            dbContext.AttachRange(query);\n            await query.ForEachAsync(x => x.PushToken = null);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<Core.Entities.Device?> GetByIdAsync(Guid id, Guid userId)\n    {\n        var device = await base.GetByIdAsync(id);\n        if (device == null || device.UserId != userId)\n        {\n            return null;\n        }\n\n        return Mapper.Map<Core.Entities.Device>(device);\n    }\n\n    public async Task<Core.Entities.Device?> GetByIdentifierAsync(string identifier)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.Devices.Where(d => d.Identifier == identifier);\n            var device = await query.FirstOrDefaultAsync();\n            return Mapper.Map<Core.Entities.Device>(device);\n        }\n    }\n\n    public async Task<Core.Entities.Device?> GetByIdentifierAsync(string identifier, Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.Devices.Where(d => d.Identifier == identifier && d.UserId == userId);\n            var device = await query.FirstOrDefaultAsync();\n            return Mapper.Map<Core.Entities.Device>(device);\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.Device>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.Devices.Where(d => d.UserId == userId);\n            var devices = await query.ToListAsync();\n            return Mapper.Map<List<Core.Entities.Device>>(devices);\n        }\n    }\n\n    public async Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId)\n    {\n        var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new DeviceWithPendingAuthByUserIdQuery();\n            return await query.GetQuery(dbContext, userId, expirationMinutes).ToListAsync();\n        }\n    }\n\n    public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<Core.Entities.Device> devices)\n    {\n        return async (_, _) =>\n        {\n            var deviceUpdates = devices.ToList();\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n            var userDevices = await GetDbSet(dbContext)\n                .Where(device => device.UserId == userId)\n                .ToListAsync();\n            var userDevicesWithUpdatesPending = userDevices\n                .Where(existingDevice => deviceUpdates.Any(updatedDevice => updatedDevice.Id == existingDevice.Id))\n                .ToList();\n\n            foreach (var deviceToUpdate in userDevicesWithUpdatesPending)\n            {\n                var deviceUpdate = deviceUpdates.First(deviceUpdate => deviceUpdate.Id == deviceToUpdate.Id);\n                deviceToUpdate.EncryptedPublicKey = deviceUpdate.EncryptedPublicKey;\n                deviceToUpdate.EncryptedUserKey = deviceUpdate.EncryptedUserKey;\n            }\n\n            await dbContext.SaveChangesAsync();\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/MaintenanceRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class MaintenanceRepository : BaseEntityFrameworkRepository, IMaintenanceRepository\n{\n    public MaintenanceRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper)\n    { }\n\n    public async Task DeleteExpiredGrantsAsync()\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from g in dbContext.Grants\n                        where g.ExpirationDate < DateTime.UtcNow\n                        select g;\n            dbContext.RemoveRange(query);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task DeleteExpiredSponsorshipsAsync(DateTime validUntilBeforeDate)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from s in dbContext.OrganizationSponsorships\n                        where s.ValidUntil < validUntilBeforeDate\n                        select s;\n            dbContext.RemoveRange(query);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public Task DisableCipherAutoStatsAsync()\n    {\n        return Task.CompletedTask;\n    }\n\n    public Task RebuildIndexesAsync()\n    {\n        return Task.CompletedTask;\n    }\n\n    public Task UpdateStatisticsAsync()\n    {\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/OrganizationApiKeyRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class OrganizationApiKeyRepository : Repository<OrganizationApiKey, Models.OrganizationApiKey, Guid>, IOrganizationApiKeyRepository\n{\n    public OrganizationApiKeyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, db => db.OrganizationApiKeys)\n    {\n\n    }\n\n    public async Task<IEnumerable<OrganizationApiKey>> GetManyByOrganizationIdTypeAsync(Guid organizationId, OrganizationApiKeyType? type = null)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var apiKeys = await dbContext.OrganizationApiKeys\n                .Where(o => o.OrganizationId == organizationId && (type == null || o.Type == type))\n                .ToListAsync();\n            return Mapper.Map<List<OrganizationApiKey>>(apiKeys);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/OrganizationConnectionRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class OrganizationConnectionRepository : Repository<OrganizationConnection, Models.OrganizationConnection, Guid>, IOrganizationConnectionRepository\n{\n    public OrganizationConnectionRepository(IServiceScopeFactory serviceScopeFactory,\n        IMapper mapper)\n        : base(serviceScopeFactory, mapper, context => context.OrganizationConnections)\n    {\n    }\n\n    public async Task<OrganizationConnection?> GetByIdOrganizationIdAsync(Guid id, Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var connection = await dbContext.OrganizationConnections\n                .FirstOrDefaultAsync(oc => oc.Id == id && oc.OrganizationId == organizationId);\n            return Mapper.Map<OrganizationConnection>(connection);\n        }\n    }\n\n    public async Task<ICollection<OrganizationConnection>> GetByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var connections = await dbContext.OrganizationConnections\n                .Where(oc => oc.OrganizationId == organizationId && oc.Type == type)\n                .ToListAsync();\n            return Mapper.Map<List<OrganizationConnection>>(connections);\n        }\n    }\n\n    public async Task<ICollection<OrganizationConnection>> GetEnabledByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var connections = await dbContext.OrganizationConnections\n                .Where(oc => oc.OrganizationId == organizationId && oc.Type == type && oc.Enabled)\n                .ToListAsync();\n            return Mapper.Map<List<OrganizationConnection>>(connections);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs",
    "content": "﻿using System.Net.Mail;\nusing AutoMapper;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class OrganizationDomainRepository : Repository<Core.Entities.OrganizationDomain, OrganizationDomain, Guid>, IOrganizationDomainRepository\n{\n    public OrganizationDomainRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationDomains)\n    {\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationDomain>> GetClaimedDomainsByDomainNameAsync(\n        string domainName)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var claimedDomains = await dbContext.OrganizationDomains\n            .Where(x => x.DomainName == domainName\n                        && x.VerifiedDate != null)\n            .AsNoTracking()\n            .ToListAsync();\n        return Mapper.Map<List<Core.Entities.OrganizationDomain>>(claimedDomains);\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var domains = await dbContext.OrganizationDomains\n            .Where(x => x.OrganizationId == orgId)\n            .AsNoTracking()\n            .ToListAsync();\n        return Mapper.Map<List<Core.Entities.OrganizationDomain>>(domains);\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationDomain>> GetManyByNextRunDateAsync(DateTime date)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var start36HoursWindow = date.AddHours(-36);\n        var end36HoursWindow = date;\n\n        var pastDomains = await dbContext.OrganizationDomains\n            .Where(x => x.NextRunDate >= start36HoursWindow\n                       && x.NextRunDate <= end36HoursWindow\n                       && x.VerifiedDate == null\n                       && x.JobRunCount != 3)\n            .ToListAsync();\n\n        return Mapper.Map<List<Core.Entities.OrganizationDomain>>(pastDomains);\n    }\n\n    public async Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email)\n    {\n        var domainName = new MailAddress(email).Host;\n\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var ssoDetails = await (from o in dbContext.Organizations\n                                from od in o.Domains\n                                join s in dbContext.SsoConfigs on o.Id equals s.OrganizationId into sJoin\n                                from s in sJoin.DefaultIfEmpty()\n                                where od.DomainName == domainName && o.Enabled\n                                select new OrganizationDomainSsoDetailsData\n                                {\n                                    OrganizationId = o.Id,\n                                    OrganizationName = o.Name,\n                                    SsoAvailable = o.SsoConfigs.Any(sc => sc.Enabled),\n                                    OrganizationIdentifier = o.Identifier,\n                                    VerifiedDate = od.VerifiedDate,\n                                    DomainName = od.DomainName\n                                })\n            .AsNoTracking()\n            .SingleOrDefaultAsync();\n\n        return ssoDetails;\n    }\n\n    public async Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email)\n    {\n        var domainName = new MailAddress(email).Host;\n\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        return await (from o in dbContext.Organizations\n                      from od in o.Domains\n                      join s in dbContext.SsoConfigs on o.Id equals s.OrganizationId into sJoin\n                      from s in sJoin.DefaultIfEmpty()\n                      where od.DomainName == domainName\n                            && o.Enabled\n                            && s.Enabled\n                            && od.VerifiedDate != null\n                      select new VerifiedOrganizationDomainSsoDetail(\n                          o.Id,\n                          o.Name,\n                          od.DomainName,\n                          o.Identifier))\n            .AsNoTracking()\n            .ToListAsync();\n    }\n\n    public async Task<Core.Entities.OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var domain = await dbContext.OrganizationDomains\n            .Where(x => x.Id == id && x.OrganizationId == orgId)\n            .AsNoTracking()\n            .FirstOrDefaultAsync();\n\n        return Mapper.Map<Core.Entities.OrganizationDomain>(domain);\n    }\n\n    public async Task<Core.Entities.OrganizationDomain?> GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var domain = await dbContext.OrganizationDomains\n            .Where(x => x.OrganizationId == orgId && x.DomainName == domainName)\n            .AsNoTracking()\n            .FirstOrDefaultAsync();\n\n        return Mapper.Map<Core.Entities.OrganizationDomain>(domain);\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationDomain>> GetExpiredOrganizationDomainsAsync()\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var threeDaysOldUnverifiedDomains = await dbContext.OrganizationDomains\n            .Where(x => x.CreationDate.Date == DateTime.UtcNow.AddDays(-4).Date\n                      && x.VerifiedDate == null)\n            .AsNoTracking()\n            .ToListAsync();\n\n        return Mapper.Map<List<Core.Entities.OrganizationDomain>>(threeDaysOldUnverifiedDomains);\n    }\n\n    public async Task<bool> DeleteExpiredAsync(int expirationPeriod)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var expiredDomains = await dbContext.OrganizationDomains\n            .Where(x => x.LastCheckedDate < DateTime.UtcNow.AddDays(-expirationPeriod) && x.VerifiedDate == null)\n            .ToListAsync();\n        dbContext.OrganizationDomains.RemoveRange(expiredDomains);\n        return await dbContext.SaveChangesAsync() > 0;\n    }\n\n    public async Task<IEnumerable<Core.Entities.OrganizationDomain>> GetVerifiedDomainsByOrganizationIdsAsync(\n        IEnumerable<Guid> organizationIds)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var verifiedDomains = await (from d in dbContext.OrganizationDomains\n                                     where organizationIds.Contains(d.OrganizationId) && d.VerifiedDate != null\n                                     select new OrganizationDomain\n                                     {\n                                         OrganizationId = d.OrganizationId,\n                                         DomainName = d.DomainName\n                                     })\n            .AsNoTracking()\n            .ToListAsync();\n\n        return Mapper.Map<List<OrganizationDomain>>(verifiedDomains);\n    }\n\n    public async Task<bool> HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(string domainName, Guid? excludeOrganizationId = null)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var query = from od in dbContext.OrganizationDomains\n                    join o in dbContext.Organizations on od.OrganizationId equals o.Id\n                    join p in dbContext.Policies on o.Id equals p.OrganizationId\n                    where od.DomainName == domainName\n                        && od.VerifiedDate != null\n                        && o.Enabled\n                        && o.UsePolicies\n                        && o.UseOrganizationDomains\n                        && (!excludeOrganizationId.HasValue || o.Id != excludeOrganizationId.Value)\n                        && p.Type == Core.AdminConsole.Enums.PolicyType.BlockClaimedDomainAccountCreation\n                        && p.Enabled\n                    select od;\n\n        return await query.AnyAsync();\n    }\n}\n\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/OrganizationSponsorshipRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class OrganizationSponsorshipRepository : Repository<Core.Entities.OrganizationSponsorship, OrganizationSponsorship, Guid>, IOrganizationSponsorshipRepository\n{\n    public OrganizationSponsorshipRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationSponsorships)\n    { }\n\n    public async Task<ICollection<Guid>?> CreateManyAsync(IEnumerable<Core.Entities.OrganizationSponsorship> organizationSponsorships)\n    {\n        if (!organizationSponsorships.Any())\n        {\n            // TODO: This differs from SQL server implementation, we should have both return empty collection\n            return new List<Guid>();\n        }\n\n        foreach (var organizationSponsorship in organizationSponsorships)\n        {\n            organizationSponsorship.SetNewId();\n        }\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entities = Mapper.Map<List<OrganizationUser>>(organizationSponsorships);\n            await dbContext.AddRangeAsync(entities);\n            await dbContext.SaveChangesAsync();\n        }\n\n        return organizationSponsorships.Select(u => u.Id).ToList();\n    }\n\n    public async Task ReplaceManyAsync(IEnumerable<Core.Entities.OrganizationSponsorship> organizationSponsorships)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            dbContext.UpdateRange(organizationSponsorships);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task UpsertManyAsync(IEnumerable<Core.Entities.OrganizationSponsorship> organizationSponsorships)\n    {\n        var createSponsorships = new List<Core.Entities.OrganizationSponsorship>();\n        var replaceSponsorships = new List<Core.Entities.OrganizationSponsorship>();\n        foreach (var organizationSponsorship in organizationSponsorships)\n        {\n            if (organizationSponsorship.Id.Equals(default))\n            {\n                createSponsorships.Add(organizationSponsorship);\n            }\n            else\n            {\n                replaceSponsorships.Add(organizationSponsorship);\n            }\n        }\n\n        await CreateManyAsync(createSponsorships);\n        await ReplaceManyAsync(replaceSponsorships);\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Guid> organizationSponsorshipIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entities = await dbContext.OrganizationSponsorships\n                .Where(os => organizationSponsorshipIds.Contains(os.Id))\n                .ToListAsync();\n\n            dbContext.OrganizationSponsorships.RemoveRange(entities);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<Core.Entities.OrganizationSponsorship?> GetByOfferedToEmailAsync(string email)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgSponsorship = await GetDbSet(dbContext).Where(e => e.OfferedToEmail == email)\n                .FirstOrDefaultAsync();\n            return orgSponsorship;\n        }\n    }\n\n    public async Task<Core.Entities.OrganizationSponsorship?> GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgSponsorship = await GetDbSet(dbContext).Where(e => e.SponsoredOrganizationId == sponsoredOrganizationId)\n                .FirstOrDefaultAsync();\n            return orgSponsorship;\n        }\n    }\n\n    public async Task<Core.Entities.OrganizationSponsorship?> GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated = false)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var orgSponsorship = await GetDbSet(dbContext)\n                .Where(e => e.SponsoringOrganizationUserId == sponsoringOrganizationUserId && e.IsAdminInitiated == isAdminInitiated)\n                .FirstOrDefaultAsync();\n            return orgSponsorship;\n        }\n    }\n\n    public async Task<DateTime?> GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await GetDbSet(dbContext).Where(e => e.SponsoringOrganizationId == sponsoringOrganizationId && e.LastSyncDate != null)\n                .OrderByDescending(e => e.LastSyncDate)\n                .Select(e => e.LastSyncDate)\n                .FirstOrDefaultAsync();\n\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.OrganizationSponsorship>> GetManyBySponsoringOrganizationAsync(Guid sponsoringOrganizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from os in dbContext.OrganizationSponsorships\n                        where os.SponsoringOrganizationId == sponsoringOrganizationId\n                        select os;\n            return Mapper.Map<List<Core.Entities.OrganizationSponsorship>>(await query.ToListAsync());\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/PlayItemRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class PlayItemRepository : Repository<Core.Entities.PlayItem, PlayItem, Guid>, IPlayItemRepository\n{\n    public PlayItemRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.PlayItem)\n    { }\n\n    public async Task<ICollection<Core.Entities.PlayItem>> GetByPlayIdAsync(string playId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var playItemEntities = await GetDbSet(dbContext)\n                .Where(pd => pd.PlayId == playId)\n                .ToListAsync();\n            return Mapper.Map<List<Core.Entities.PlayItem>>(playItemEntities);\n        }\n    }\n\n    public async Task DeleteByPlayIdAsync(string playId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entities = await GetDbSet(dbContext)\n                .Where(pd => pd.PlayId == playId)\n                .ToListAsync();\n\n            dbContext.PlayItem.RemoveRange(entities);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\n/// <summary>\n/// Query to get collection details, including permissions for the specified user if provided.\n/// </summary>\npublic class CollectionAdminDetailsQuery : IQuery<CollectionAdminDetails>\n{\n    private readonly Guid? _userId;\n    private readonly Guid? _organizationId;\n    private readonly Guid? _collectionId;\n\n    private CollectionAdminDetailsQuery(Guid? userId, Guid? organizationId, Guid? collectionId)\n    {\n        _userId = userId;\n        _organizationId = organizationId;\n        _collectionId = collectionId;\n    }\n\n    public virtual IQueryable<CollectionAdminDetails> Run(DatabaseContext dbContext)\n    {\n        var baseCollectionQuery = from c in dbContext.Collections\n                                  join ou in dbContext.OrganizationUsers\n                                      on new { c.OrganizationId, UserId = _userId } equals\n                                      new { ou.OrganizationId, ou.UserId } into ou_g\n                                  from ou in ou_g.DefaultIfEmpty()\n\n                                  join cu in dbContext.CollectionUsers\n                                      on new { CollectionId = c.Id, OrganizationUserId = ou.Id } equals\n                                      new { cu.CollectionId, cu.OrganizationUserId } into cu_g\n                                  from cu in cu_g.DefaultIfEmpty()\n\n                                  join gu in dbContext.GroupUsers\n                                      on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                                      new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n                                  from gu in gu_g.DefaultIfEmpty()\n\n                                  join g in dbContext.Groups\n                                      on gu.GroupId equals g.Id into g_g\n                                  from g in g_g.DefaultIfEmpty()\n\n                                  join cg in dbContext.CollectionGroups\n                                      on new { CollectionId = c.Id, gu.GroupId } equals\n                                      new { cg.CollectionId, cg.GroupId } into cg_g\n                                  from cg in cg_g.DefaultIfEmpty()\n                                  select new { c, cu, cg };\n\n        // Subqueries to determine if a collection is managed by a user or group.\n        var activeUserManageRights = from cu in dbContext.CollectionUsers\n                                     join ou in dbContext.OrganizationUsers\n                                         on cu.OrganizationUserId equals ou.Id\n                                     where cu.Manage\n                                     select cu.CollectionId;\n\n        var activeGroupManageRights = from cg in dbContext.CollectionGroups\n                                      where cg.Manage\n                                      select cg.CollectionId;\n\n        if (_organizationId.HasValue)\n        {\n            baseCollectionQuery = baseCollectionQuery.Where(x =>\n                x.c.OrganizationId == _organizationId &&\n                x.c.Type == CollectionType.SharedCollection);\n        }\n        else if (_collectionId.HasValue)\n        {\n            baseCollectionQuery = baseCollectionQuery.Where(x => x.c.Id == _collectionId);\n        }\n        else\n        {\n            throw new InvalidOperationException(\"OrganizationId or CollectionId must be specified.\");\n        }\n\n        return baseCollectionQuery.Select(x => new CollectionAdminDetails\n        {\n            Id = x.c.Id,\n            OrganizationId = x.c.OrganizationId,\n            Name = x.c.Name,\n            ExternalId = x.c.ExternalId,\n            CreationDate = x.c.CreationDate,\n            RevisionDate = x.c.RevisionDate,\n            DefaultUserCollectionEmail = x.c.DefaultUserCollectionEmail,\n            ReadOnly = (bool?)x.cu.ReadOnly ?? (bool?)x.cg.ReadOnly ?? false,\n            HidePasswords = (bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false,\n            Manage = (bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false,\n            Assigned = x.cu != null || x.cg != null,\n            Unmanaged = !activeUserManageRights.Contains(x.c.Id) && !activeGroupManageRights.Contains(x.c.Id),\n        });\n    }\n\n    public static CollectionAdminDetailsQuery ByCollectionId(Guid collectionId, Guid? userId)\n    {\n        return new CollectionAdminDetailsQuery(userId, null, collectionId);\n    }\n\n    public static CollectionAdminDetailsQuery ByOrganizationId(Guid organizationId, Guid? userId)\n    {\n        return new CollectionAdminDetailsQuery(userId, organizationId, null);\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/CollectionCipherReadByUserIdCipherIdQuery.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class CollectionCipherReadByUserIdCipherIdQuery : CollectionCipherReadByUserIdQuery\n{\n    private readonly Guid _cipherId;\n\n    public CollectionCipherReadByUserIdCipherIdQuery(Guid userId, Guid cipherId) : base(userId)\n    {\n        _cipherId = cipherId;\n    }\n\n    public override IQueryable<CollectionCipher> Run(DatabaseContext dbContext)\n    {\n        var query = base.Run(dbContext);\n        return query.Where(x => x.CipherId == _cipherId);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/CollectionCipherReadByUserIdQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class CollectionCipherReadByUserIdQuery : IQuery<CollectionCipher>\n{\n    private readonly Guid _userId;\n\n    public CollectionCipherReadByUserIdQuery(Guid userId)\n    {\n        _userId = userId;\n    }\n\n    public virtual IQueryable<CollectionCipher> Run(DatabaseContext dbContext)\n    {\n        var query = from cc in dbContext.CollectionCiphers\n\n                    join c in dbContext.Collections\n                        on cc.CollectionId equals c.Id\n\n                    join ou in dbContext.OrganizationUsers\n                        on new { c.OrganizationId, UserId = (Guid?)_userId } equals\n                           new { ou.OrganizationId, ou.UserId }\n\n                    join cu in dbContext.CollectionUsers\n                        on new { CollectionId = c.Id, OrganizationUserId = ou.Id } equals\n                           new { cu.CollectionId, cu.OrganizationUserId } into cu_g\n                    from cu in cu_g.DefaultIfEmpty()\n\n                    join gu in dbContext.GroupUsers\n                        on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                           new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n                    from gu in gu_g.DefaultIfEmpty()\n\n                    join g in dbContext.Groups\n                        on gu.GroupId equals g.Id into g_g\n                    from g in g_g.DefaultIfEmpty()\n\n                    join cg in dbContext.CollectionGroups\n                        on new { CollectionId = c.Id, gu.GroupId } equals\n                           new { cg.CollectionId, cg.GroupId } into cg_g\n                    from cg in cg_g.DefaultIfEmpty()\n\n                    where ou.Status == OrganizationUserStatusType.Confirmed &&\n                        ((cu == null ? (Guid?)null : cu.CollectionId) != null || (cg == null ? (Guid?)null : cg.CollectionId) != null)\n                    select cc;\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/CollectionReadCountByOrganizationIdQuery.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class CollectionReadCountByOrganizationIdQuery : IQuery<Collection>\n{\n    private readonly Guid _organizationId;\n\n    public CollectionReadCountByOrganizationIdQuery(Guid organizationId)\n    {\n        _organizationId = organizationId;\n    }\n\n    public IQueryable<Collection> Run(DatabaseContext dbContext)\n    {\n        var query = from c in dbContext.Collections\n                    where c.OrganizationId == _organizationId\n                    select c;\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/CollectionUserUpdateUsersQuery.cs",
    "content": "﻿"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/CollectionsReadByOrganizationIdUserIdQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\n/// <summary>\n/// Returns all Collections that a user is assigned to in an organization, either directly or via a group.\n/// </summary>\npublic class CollectionsReadByOrganizationIdUserIdQuery : IQuery<Collection>\n{\n    private readonly Guid? _organizationId;\n    private readonly Guid _userId;\n\n    public CollectionsReadByOrganizationIdUserIdQuery(Guid? organizationId, Guid userId)\n    {\n        _organizationId = organizationId;\n        _userId = userId;\n    }\n\n    public virtual IQueryable<Collection> Run(DatabaseContext dbContext)\n    {\n        var query = from c in dbContext.Collections\n                    join o in dbContext.Organizations on c.OrganizationId equals o.Id\n                    join ou in dbContext.OrganizationUsers\n                        on new { OrganizationId = o.Id, UserId = (Guid?)_userId } equals\n                        new { ou.OrganizationId, ou.UserId }\n                    join cu in dbContext.CollectionUsers\n                        on new { CollectionId = c.Id, OrganizationUserId = ou.Id } equals\n                        new { cu.CollectionId, cu.OrganizationUserId } into cu_g\n                    from cu in cu_g.DefaultIfEmpty()\n                    join gu in dbContext.GroupUsers\n                        on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                        new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n                    from gu in gu_g.DefaultIfEmpty()\n                    join g in dbContext.Groups on gu.GroupId equals g.Id into g_g\n                    from g in g_g.DefaultIfEmpty()\n                    join cg in dbContext.CollectionGroups\n                        on new { CollectionId = c.Id, gu.GroupId } equals\n                        new { cg.CollectionId, cg.GroupId } into cg_g\n                    from cg in cg_g.DefaultIfEmpty()\n                    where o.Id == _organizationId && o.Enabled && ou.Status == OrganizationUserStatusType.Confirmed\n                          && (!cu.ReadOnly || !cg.ReadOnly)\n                    select c;\n\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/GroupUserUpdateGroupsQuery.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class GroupUserUpdateGroupsQuery\n{\n    public readonly GroupUserUpdateGroupsInsertQuery Insert;\n    public readonly GroupUserUpdateGroupsDeleteQuery Delete;\n\n    public GroupUserUpdateGroupsQuery(Guid organizationUserId, IEnumerable<Guid> groupIds)\n    {\n        Insert = new GroupUserUpdateGroupsInsertQuery(organizationUserId, groupIds);\n        Delete = new GroupUserUpdateGroupsDeleteQuery(organizationUserId, groupIds);\n    }\n}\n\npublic class GroupUserUpdateGroupsInsertQuery : IQuery<GroupUser>\n{\n    private readonly Guid _organizationUserId;\n    private readonly IEnumerable<Guid> _groupIds;\n\n    public GroupUserUpdateGroupsInsertQuery(Guid organizationUserId, IEnumerable<Guid> collections)\n    {\n        _organizationUserId = organizationUserId;\n        _groupIds = collections;\n    }\n\n    public IQueryable<GroupUser> Run(DatabaseContext dbContext)\n    {\n        var orgUser = from ou in dbContext.OrganizationUsers\n                      where ou.Id == _organizationUserId\n                      select ou;\n        var groupIdEntities = dbContext.Groups.Where(x => _groupIds.Contains(x.Id));\n        var query = from g in dbContext.Groups\n                    join ou in orgUser\n                        on g.OrganizationId equals ou.OrganizationId\n                    join gie in groupIdEntities\n                        on g.Id equals gie.Id\n                    where !dbContext.GroupUsers.Any(gu => gu.GroupId == gie.Id && gu.OrganizationUserId == _organizationUserId)\n                    select g;\n        return query.Select(x => new GroupUser\n        {\n            GroupId = x.Id,\n            OrganizationUserId = _organizationUserId,\n        });\n    }\n}\n\npublic class GroupUserUpdateGroupsDeleteQuery : IQuery<GroupUser>\n{\n    private readonly Guid _organizationUserId;\n    private readonly IEnumerable<Guid> _groupIds;\n\n    public GroupUserUpdateGroupsDeleteQuery(Guid organizationUserId, IEnumerable<Guid> groupIds)\n    {\n        _organizationUserId = organizationUserId;\n        _groupIds = groupIds;\n    }\n\n    public IQueryable<GroupUser> Run(DatabaseContext dbContext)\n    {\n        var deleteQuery = from gu in dbContext.GroupUsers\n                          where gu.OrganizationUserId == _organizationUserId &&\n                              !_groupIds.Any(x => gu.GroupId == x)\n                          select gu;\n        return deleteQuery;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/IQuery.cs",
    "content": "﻿namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic interface IQuery<TOut>\n{\n    IQueryable<TOut> Run(DatabaseContext dbContext);\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/UserBumpAccountRevisionDateByCipherIdQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing User = Bit.Infrastructure.EntityFramework.Models.User;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class UserBumpAccountRevisionDateByCipherIdQuery : IQuery<User>\n{\n    private readonly Guid _cipherId;\n    private readonly Guid _organizationId;\n\n    public UserBumpAccountRevisionDateByCipherIdQuery(Guid cipherId, Guid organizationId)\n    {\n        _cipherId = cipherId;\n        _organizationId = organizationId;\n    }\n\n    public IQueryable<User> Run(DatabaseContext dbContext)\n    {\n        var query = from u in dbContext.Users\n\n                    join ou in dbContext.OrganizationUsers\n                        on u.Id equals ou.UserId\n\n                    join collectionCipher in dbContext.CollectionCiphers\n                        on _cipherId equals collectionCipher.CipherId into cc_g\n                    from cc in cc_g.DefaultIfEmpty()\n\n                    join collectionUser in dbContext.CollectionUsers\n                        on new { OrganizationUserId = ou.Id, cc.CollectionId } equals\n                           new { collectionUser.OrganizationUserId, collectionUser.CollectionId } into cu_g\n                    from cu in cu_g.DefaultIfEmpty()\n\n                    join groupUser in dbContext.GroupUsers\n                        on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                           new { CollectionId = (Guid?)null, groupUser.OrganizationUserId } into gu_g\n                    from gu in gu_g.DefaultIfEmpty()\n\n                    join grp in dbContext.Groups\n                        on gu.GroupId equals grp.Id into g_g\n                    from g in g_g.DefaultIfEmpty()\n\n                    join collectionGroup in dbContext.CollectionGroups\n                        on new { gu.GroupId, cc.CollectionId } equals\n                           new { collectionGroup.GroupId, collectionGroup.CollectionId } into cg_g\n                    from cg in cg_g.DefaultIfEmpty()\n\n                    where ou.OrganizationId == _organizationId &&\n                            ou.Status == OrganizationUserStatusType.Confirmed &&\n                            ((cu == null ? (Guid?)null : cu.CollectionId) != null ||\n                            (cg == null ? (Guid?)null : cg.CollectionId) != null)\n                    select u;\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/UserBumpAccountRevisionDateByOrganizationIdQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class UserBumpAccountRevisionDateByOrganizationIdQuery : IQuery<User>\n{\n    private readonly Guid _organizationId;\n\n    public UserBumpAccountRevisionDateByOrganizationIdQuery(Guid organizationId)\n    {\n        _organizationId = organizationId;\n    }\n\n    public IQueryable<User> Run(DatabaseContext dbContext)\n    {\n        var query = from u in dbContext.Users\n                    join ou in dbContext.OrganizationUsers\n                        on u.Id equals ou.UserId\n                    where ou.OrganizationId == _organizationId &&\n                        ou.Status == OrganizationUserStatusType.Confirmed\n                    select u;\n\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing Bit.Core.Enums;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class UserCipherDetailsQuery : IQuery<CipherDetails>\n{\n    private readonly Guid? _userId;\n    public UserCipherDetailsQuery(Guid? userId)\n    {\n        _userId = userId;\n    }\n\n    public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from c in dbContext.Ciphers\n\n                    join ou in dbContext.OrganizationUsers\n                        on new { CipherUserId = c.UserId, c.OrganizationId, UserId = _userId, Status = OrganizationUserStatusType.Confirmed } equals\n                           new { CipherUserId = (Guid?)null, OrganizationId = (Guid?)ou.OrganizationId, ou.UserId, ou.Status }\n\n                    join o in dbContext.Organizations\n                        on new { c.OrganizationId, OuOrganizationId = ou.OrganizationId, Enabled = true } equals\n                           new { OrganizationId = (Guid?)o.Id, OuOrganizationId = o.Id, o.Enabled }\n\n                    join cc in dbContext.CollectionCiphers\n                        on c.Id equals cc.CipherId into cc_g\n                    from cc in cc_g.DefaultIfEmpty()\n\n                    join cu in dbContext.CollectionUsers\n                        on new { cc.CollectionId, OrganizationUserId = ou.Id } equals\n                           new { cu.CollectionId, cu.OrganizationUserId } into cu_g\n                    from cu in cu_g.DefaultIfEmpty()\n\n                    join gu in dbContext.GroupUsers\n                        on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                           new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n                    from gu in gu_g.DefaultIfEmpty()\n\n                    join g in dbContext.Groups\n                        on gu.GroupId equals g.Id into g_g\n                    from g in g_g.DefaultIfEmpty()\n\n                    join cg in dbContext.CollectionGroups\n                        on new { cc.CollectionId, gu.GroupId } equals\n                           new { cg.CollectionId, cg.GroupId } into cg_g\n                    from cg in cg_g.DefaultIfEmpty()\n\n                    where (cu == null ? (Guid?)null : cu.CollectionId) != null || (cg == null ? (Guid?)null : cg.CollectionId) != null\n\n                    select new\n                    {\n                        c.Id,\n                        c.UserId,\n                        c.OrganizationId,\n                        c.Type,\n                        c.Data,\n                        c.Attachments,\n                        c.CreationDate,\n                        c.RevisionDate,\n                        c.DeletedDate,\n                        c.Favorites,\n                        c.Folders,\n                        Edit = cu == null ? (cg != null && cg.ReadOnly == false) : cu.ReadOnly == false,\n                        ViewPassword = cu == null ? (cg != null && cg.HidePasswords == false) : cu.HidePasswords == false,\n                        Manage = cu == null ? (cg != null && cg.Manage == true) : cu.Manage == true,\n                        OrganizationUseTotp = o.UseTotp,\n                        c.Reprompt,\n                        c.Key,\n                        c.Archives\n                    };\n\n        var query2 = from c in dbContext.Ciphers\n                     where c.UserId == _userId\n                     select new\n                     {\n                         c.Id,\n                         c.UserId,\n                         c.OrganizationId,\n                         c.Type,\n                         c.Data,\n                         c.Attachments,\n                         c.CreationDate,\n                         c.RevisionDate,\n                         c.DeletedDate,\n                         c.Favorites,\n                         c.Folders,\n                         Edit = true,\n                         ViewPassword = true,\n                         Manage = true,\n                         OrganizationUseTotp = false,\n                         c.Reprompt,\n                         c.Key,\n                         c.Archives\n                     };\n\n        var union = query.Union(query2).Select(c => new CipherDetails\n        {\n            Id = c.Id,\n            UserId = c.UserId,\n            OrganizationId = c.OrganizationId,\n            Type = c.Type,\n            Data = c.Data,\n            Attachments = c.Attachments,\n            CreationDate = c.CreationDate,\n            RevisionDate = c.RevisionDate,\n            DeletedDate = c.DeletedDate,\n            Favorite = _userId.HasValue && c.Favorites != null && c.Favorites.ToLowerInvariant().Contains($\"\\\"{_userId}\\\":true\"),\n            FolderId = GetFolderId(_userId, new Cipher { Id = c.Id, Folders = c.Folders }),\n            Edit = c.Edit,\n            Reprompt = c.Reprompt,\n            ViewPassword = c.ViewPassword,\n            Manage = c.Manage,\n            OrganizationUseTotp = c.OrganizationUseTotp,\n            Key = c.Key,\n            ArchivedDate = GetArchivedDate(_userId, new Cipher { Id = c.Id, Archives = c.Archives })\n        });\n        return union;\n    }\n\n    private static DateTime? GetArchivedDate(Guid? userId, Cipher cipher)\n    {\n        try\n        {\n            if (userId.HasValue && !string.IsNullOrWhiteSpace(cipher.Archives))\n            {\n                var archives = JsonSerializer.Deserialize<Dictionary<Guid, DateTime>>(cipher.Archives);\n                if (archives.TryGetValue(userId.Value, out var archivedDate))\n                {\n                    return archivedDate;\n                }\n            }\n\n            return null;\n        }\n        catch\n        {\n            return null;\n        }\n    }\n\n    private static Guid? GetFolderId(Guid? userId, Cipher cipher)\n    {\n        try\n        {\n            if (userId.HasValue && !string.IsNullOrWhiteSpace(cipher.Folders))\n            {\n                var folders = JsonSerializer.Deserialize<Dictionary<Guid, Guid>>(cipher.Folders);\n                if (folders.TryGetValue(userId.Value, out var folder))\n                {\n                    return folder;\n                }\n            }\n\n            return null;\n        }\n        catch\n        {\n            // Some Folders might be in an invalid format like: '{ \"\", \"<ValidGuid>\" }'\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/UserCollectionDetailsQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class UserCollectionDetailsQuery : IQuery<CollectionDetails>\n{\n    private readonly Guid? _userId;\n\n    public UserCollectionDetailsQuery(Guid? userId)\n    {\n        _userId = userId;\n    }\n\n    public virtual IQueryable<CollectionDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from c in dbContext.Collections\n\n                    join ou in dbContext.OrganizationUsers\n                        on c.OrganizationId equals ou.OrganizationId\n\n                    join o in dbContext.Organizations\n                        on c.OrganizationId equals o.Id\n\n                    join cu in dbContext.CollectionUsers\n                        on new { CollectionId = c.Id, OrganizationUserId = ou.Id } equals\n                           new { cu.CollectionId, cu.OrganizationUserId } into cu_g\n                    from cu in cu_g.DefaultIfEmpty()\n\n                    join gu in dbContext.GroupUsers\n                        on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                           new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n                    from gu in gu_g.DefaultIfEmpty()\n\n                    join g in dbContext.Groups\n                        on gu.GroupId equals g.Id into g_g\n                    from g in g_g.DefaultIfEmpty()\n\n                    join cg in dbContext.CollectionGroups\n                        on new { CollectionId = c.Id, gu.GroupId } equals\n                           new { cg.CollectionId, cg.GroupId } into cg_g\n                    from cg in cg_g.DefaultIfEmpty()\n\n                    where ou.UserId == _userId &&\n                        ou.Status == OrganizationUserStatusType.Confirmed &&\n                        o.Enabled &&\n                        ((cu == null ? (Guid?)null : cu.CollectionId) != null || (cg == null ? (Guid?)null : cg.CollectionId) != null)\n                    select new { c, ou, o, cu, gu, g, cg };\n\n        return query.Select(row => new CollectionDetails\n        {\n            Id = row.c.Id,\n            OrganizationId = row.c.OrganizationId,\n            Name = row.c.Name,\n            ExternalId = row.c.ExternalId,\n            CreationDate = row.c.CreationDate,\n            RevisionDate = row.c.RevisionDate,\n            ReadOnly = (bool?)row.cu.ReadOnly ?? (bool?)row.cg.ReadOnly ?? false,\n            HidePasswords = (bool?)row.cu.HidePasswords ?? (bool?)row.cg.HidePasswords ?? false,\n            Manage = (bool?)row.cu.Manage ?? (bool?)row.cg.Manage ?? false,\n            Type = row.c.Type\n        });\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Queries/UserReadPublicKeysByProviderUserIdsQuery.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\npublic class UserReadPublicKeysByProviderUserIdsQuery : IQuery<ProviderUserPublicKey>\n{\n    private readonly Guid _providerId;\n    private readonly IEnumerable<Guid> _ids;\n\n    public UserReadPublicKeysByProviderUserIdsQuery(Guid providerId, IEnumerable<Guid> Ids)\n    {\n        _providerId = providerId;\n        _ids = Ids;\n    }\n\n    public virtual IQueryable<ProviderUserPublicKey> Run(DatabaseContext dbContext)\n    {\n        var query = from pu in dbContext.ProviderUsers\n                    join u in dbContext.Users\n                        on pu.UserId equals u.Id\n                    where _ids.Contains(pu.Id) &&\n                        pu.Status == ProviderUserStatusType.Accepted &&\n                        pu.ProviderId == _providerId\n                    select new { pu, u };\n        return query.Select(x => new ProviderUserPublicKey\n        {\n            Id = x.pu.Id,\n            PublicKey = x.u.PublicKey,\n        });\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/Repository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic abstract class Repository<T, TEntity, TId> : BaseEntityFrameworkRepository, IRepository<T, TId>\n    where TId : IEquatable<TId>\n    where T : class, ITableObject<TId>\n    where TEntity : class, ITableObject<TId>\n{\n    public Repository(IServiceScopeFactory serviceScopeFactory, IMapper mapper, Func<DatabaseContext, DbSet<TEntity>> getDbSet)\n        : base(serviceScopeFactory, mapper)\n    {\n        GetDbSet = getDbSet;\n    }\n\n    protected Func<DatabaseContext, DbSet<TEntity>> GetDbSet { get; private set; }\n\n    public virtual async Task<T?> GetByIdAsync(TId id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await GetDbSet(dbContext).FindAsync(id);\n            return Mapper.Map<T>(entity);\n        }\n    }\n\n    public virtual async Task<T> CreateAsync(T obj)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            obj.SetNewId();\n            var entity = Mapper.Map<TEntity>(obj);\n            await dbContext.AddAsync(entity);\n            await dbContext.SaveChangesAsync();\n            obj.Id = entity.Id;\n            return obj;\n        }\n    }\n\n    public virtual async Task ReplaceAsync(T obj)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await GetDbSet(dbContext).FindAsync(obj.Id);\n            if (entity != null)\n            {\n                var mappedEntity = Mapper.Map<TEntity>(obj);\n                dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);\n                await dbContext.SaveChangesAsync();\n            }\n        }\n    }\n\n    public virtual async Task UpsertAsync(T obj)\n    {\n        if (obj.Id.Equals(default(TId)))\n        {\n            await CreateAsync(obj);\n        }\n        else\n        {\n            await ReplaceAsync(obj);\n        }\n    }\n\n    public virtual async Task DeleteAsync(T obj)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = Mapper.Map<TEntity>(obj);\n            dbContext.Remove(entity);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public virtual async Task RefreshDb()\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var context = GetDatabaseContext(scope);\n            await context.Database.EnsureDeletedAsync();\n            await context.Database.EnsureCreatedAsync();\n        }\n    }\n\n    public virtual async Task<List<T>> CreateMany(List<T> objs)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var entities = new List<TEntity>();\n            foreach (var o in objs)\n            {\n                o.SetNewId();\n                var entity = Mapper.Map<TEntity>(o);\n                entities.Add(entity);\n            }\n            var dbContext = GetDatabaseContext(scope);\n            await GetDbSet(dbContext).AddRangeAsync(entities);\n            await dbContext.SaveChangesAsync();\n            return objs;\n        }\n    }\n\n    public IQueryable<Tout> Run<Tout>(IQuery<Tout> query)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return query.Run(dbContext);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/TransactionRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing LinqToDB;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\n#nullable enable\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class TransactionRepository : Repository<Core.Entities.Transaction, Transaction, Guid>, ITransactionRepository\n{\n    public TransactionRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Transactions)\n    { }\n\n    public async Task<Core.Entities.Transaction?> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var results = await EntityFrameworkQueryableExtensions.FirstOrDefaultAsync(dbContext.Transactions, t => (t.GatewayId == gatewayId && t.Gateway == gatewayType));\n        return Mapper.Map<Core.Entities.Transaction>(results);\n    }\n\n    public async Task<ICollection<Core.Entities.Transaction>> GetManyByOrganizationIdAsync(\n        Guid organizationId,\n        int? limit = null,\n        DateTime? startAfter = null)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.Transactions\n            .Where(t => t.OrganizationId == organizationId && !t.UserId.HasValue);\n\n        if (startAfter.HasValue)\n        {\n            query = query.Where(t => t.CreationDate < startAfter.Value);\n        }\n\n        if (limit.HasValue)\n        {\n            query = query.OrderByDescending(o => o.CreationDate).Take(limit.Value);\n        }\n\n        var results = await EntityFrameworkQueryableExtensions.ToListAsync(query);\n        return Mapper.Map<List<Core.Entities.Transaction>>(results);\n    }\n\n    public async Task<ICollection<Core.Entities.Transaction>> GetManyByUserIdAsync(\n        Guid userId,\n        int? limit = null,\n        DateTime? startAfter = null)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n\n        var dbContext = GetDatabaseContext(scope);\n        var query = dbContext.Transactions\n            .Where(t => t.UserId == userId);\n\n        if (startAfter.HasValue)\n        {\n            query = query.Where(t => t.CreationDate < startAfter.Value);\n        }\n\n        if (limit.HasValue)\n        {\n            query = query.OrderByDescending(o => o.CreationDate).Take(limit.Value);\n        }\n\n        var results = await EntityFrameworkQueryableExtensions.ToListAsync(query);\n\n        return Mapper.Map<List<Core.Entities.Transaction>>(results);\n    }\n\n    public async Task<ICollection<Core.Entities.Transaction>> GetManyByProviderIdAsync(\n        Guid providerId,\n        int? limit = null,\n        DateTime? startAfter = null)\n    {\n        using var serviceScope = ServiceScopeFactory.CreateScope();\n        var databaseContext = GetDatabaseContext(serviceScope);\n        var query = databaseContext.Transactions\n            .Where(transaction => transaction.ProviderId == providerId);\n\n        if (startAfter.HasValue)\n        {\n            query = query.Where(transaction => transaction.CreationDate < startAfter.Value);\n        }\n\n        if (limit.HasValue)\n        {\n            query = query.Take(limit.Value);\n        }\n\n        var results = await EntityFrameworkQueryableExtensions.ToListAsync(query);\n        return Mapper.Map<List<Core.Entities.Transaction>>(results);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Repositories/UserRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core;\nusing Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories;\n\npublic class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserRepository\n{\n    public UserRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Users)\n    { }\n\n    public async Task<Core.Entities.User?> GetByGatewayCustomerIdAsync(string gatewayCustomerId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await GetDbSet(dbContext)\n                .FirstOrDefaultAsync(e => e.GatewayCustomerId == gatewayCustomerId);\n            return Mapper.Map<Core.Entities.User>(entity);\n        }\n    }\n\n    public async Task<Core.Entities.User?> GetByGatewaySubscriptionIdAsync(string gatewaySubscriptionId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await GetDbSet(dbContext)\n                .FirstOrDefaultAsync(e => e.GatewaySubscriptionId == gatewaySubscriptionId);\n            return Mapper.Map<Core.Entities.User>(entity);\n        }\n    }\n\n    public async Task<Core.Entities.User?> GetByEmailAsync(string email)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await GetDbSet(dbContext).FirstOrDefaultAsync(e => e.Email == email);\n            return Mapper.Map<Core.Entities.User>(entity);\n        }\n    }\n\n    public async Task<IEnumerable<Core.Entities.User>> GetManyByEmailsAsync(IEnumerable<string> emails)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var users = await GetDbSet(dbContext)\n                .Where(u => emails.Contains(u.Email))\n                .ToListAsync();\n            return Mapper.Map<List<Core.Entities.User>>(users);\n        }\n    }\n\n    public async Task<UserKdfInformation?> GetKdfInformationByEmailAsync(string email)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await GetDbSet(dbContext).Where(e => e.Email == email)\n                .Select(e => new UserKdfInformation\n                {\n                    Kdf = e.Kdf,\n                    KdfIterations = e.KdfIterations,\n                    KdfMemory = e.KdfMemory,\n                    KdfParallelism = e.KdfParallelism,\n                    MasterPasswordSalt = e.MasterPasswordSalt\n                }).SingleOrDefaultAsync();\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.User>> SearchAsync(string email, int skip, int take)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            List<User> users;\n            if (dbContext.Database.IsNpgsql())\n            {\n                users = await GetDbSet(dbContext)\n                    .Where(e => e.Email == null ||\n                        EF.Functions.ILike(EF.Functions.Collate(e.Email, \"default\"), $\"{email}%\"))\n                    .OrderBy(e => e.Email)\n                    .Skip(skip).Take(take)\n                    .ToListAsync();\n            }\n            else\n            {\n                users = await GetDbSet(dbContext)\n                    .Where(e => email == null || e.Email.StartsWith(email))\n                    .OrderBy(e => e.Email)\n                    .Skip(skip).Take(take)\n                    .ToListAsync();\n            }\n            return Mapper.Map<List<Core.Entities.User>>(users);\n        }\n    }\n\n    public async Task<ICollection<Core.Entities.User>> GetManyByPremiumAsync(bool premium)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var users = await GetDbSet(dbContext).Where(e => e.Premium == premium).ToListAsync();\n            return Mapper.Map<List<Core.Entities.User>>(users);\n        }\n    }\n\n    public async Task<string?> GetPublicKeyAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await GetDbSet(dbContext).Where(e => e.Id == id).Select(e => e.PublicKey).SingleOrDefaultAsync();\n        }\n    }\n\n    public async Task<DateTime> GetAccountRevisionDateAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            return await GetDbSet(dbContext).Where(e => e.Id == id).Select(e => e.AccountRevisionDate)\n                .SingleOrDefaultAsync();\n        }\n    }\n\n    public async Task UpdateStorageAsync(Guid id)\n    {\n        await base.UserUpdateStorage(id);\n    }\n\n    public async Task UpdateRenewalReminderDateAsync(Guid id, DateTime renewalReminderDate)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var user = new User\n            {\n                Id = id,\n                RenewalReminderDate = renewalReminderDate,\n            };\n            var set = GetDbSet(dbContext);\n            set.Attach(user);\n            dbContext.Entry(user).Property(e => e.RenewalReminderDate).IsModified = true;\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<Core.Entities.User?> GetBySsoUserAsync(string externalId, Guid? organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var ssoUser = await dbContext.SsoUsers.SingleOrDefaultAsync(e =>\n                e.OrganizationId == organizationId && e.ExternalId == externalId);\n\n            if (ssoUser == null)\n            {\n                return null;\n            }\n\n            var entity = await dbContext.Users.SingleOrDefaultAsync(e => e.Id == ssoUser.UserId);\n            return Mapper.Map<Core.Entities.User>(entity);\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task UpdateUserKeyAndEncryptedDataAsync(Core.Entities.User user,\n        IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        try\n        {\n            // Update user\n            var entity = await dbContext.Users.FindAsync(user.Id);\n            if (entity == null)\n            {\n                throw new ArgumentException(\"User not found\", nameof(user));\n            }\n\n            entity.SecurityStamp = user.SecurityStamp;\n            entity.Key = user.Key;\n\n            entity.PrivateKey = user.PrivateKey;\n            entity.LastKeyRotationDate = user.LastKeyRotationDate;\n            entity.AccountRevisionDate = user.AccountRevisionDate;\n            entity.RevisionDate = user.RevisionDate;\n\n            await dbContext.SaveChangesAsync();\n\n            //  Update re-encrypted data\n            foreach (var action in updateDataActions)\n            {\n                // connection and transaction aren't used in EF\n                await action();\n            }\n\n            await transaction.CommitAsync();\n        }\n        catch\n        {\n            await transaction.RollbackAsync();\n            throw;\n        }\n\n    }\n\n\n    public async Task UpdateUserKeyAndEncryptedDataV2Async(Core.Entities.User user,\n        IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        // Update user\n        var userEntity = await dbContext.Users.FindAsync(user.Id);\n        if (userEntity == null)\n        {\n            throw new ArgumentException(\"User not found\", nameof(user));\n        }\n\n        userEntity.SecurityStamp = user.SecurityStamp;\n        userEntity.Key = user.Key;\n        userEntity.PrivateKey = user.PrivateKey;\n\n        userEntity.Kdf = user.Kdf;\n        userEntity.KdfIterations = user.KdfIterations;\n        userEntity.KdfMemory = user.KdfMemory;\n        userEntity.KdfParallelism = user.KdfParallelism;\n\n        userEntity.Email = user.Email;\n\n        userEntity.MasterPassword = user.MasterPassword;\n        userEntity.MasterPasswordHint = user.MasterPasswordHint;\n\n        userEntity.LastKeyRotationDate = user.LastKeyRotationDate;\n        userEntity.AccountRevisionDate = user.AccountRevisionDate;\n        userEntity.RevisionDate = user.RevisionDate;\n\n        userEntity.SignedPublicKey = user.SignedPublicKey;\n        userEntity.SecurityState = user.SecurityState;\n        userEntity.SecurityVersion = user.SecurityVersion;\n\n        userEntity.V2UpgradeToken = user.V2UpgradeToken;\n\n        await dbContext.SaveChangesAsync();\n\n        //  Update re-encrypted data\n        foreach (var action in updateDataActions)\n        {\n            // connection and transaction aren't used in EF\n            await action();\n        }\n\n        await transaction.CommitAsync();\n    }\n\n    public async Task SetV2AccountCryptographicStateAsync(\n        Guid userId,\n        UserAccountKeysData accountKeysData,\n        IEnumerable<UpdateUserData>? updateUserDataActions = null)\n    {\n        if (!accountKeysData.IsV2Encryption())\n        {\n            throw new ArgumentException(\"Provided account keys data is not valid V2 encryption data.\", nameof(accountKeysData));\n        }\n\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        // Update user\n        var userEntity = await dbContext.Users.FindAsync(userId);\n        if (userEntity == null)\n        {\n            throw new ArgumentException(\"User not found\", nameof(userId));\n        }\n\n        // Update public key encryption key pair\n        var timestamp = DateTime.UtcNow;\n\n        userEntity.RevisionDate = timestamp;\n        userEntity.AccountRevisionDate = timestamp;\n\n        // V1 + V2 user crypto changes\n        userEntity.PublicKey = accountKeysData.PublicKeyEncryptionKeyPairData.PublicKey;\n        userEntity.PrivateKey = accountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey;\n\n        userEntity.SecurityState = accountKeysData.SecurityStateData!.SecurityState;\n        userEntity.SecurityVersion = accountKeysData.SecurityStateData.SecurityVersion;\n        userEntity.SignedPublicKey = accountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey;\n\n        // Replace existing key-pair if it exists\n        var existingKeyPair = await dbContext.UserSignatureKeyPairs\n            .FirstOrDefaultAsync(x => x.UserId == userId);\n        if (existingKeyPair != null)\n        {\n            existingKeyPair.SignatureAlgorithm = accountKeysData.SignatureKeyPairData!.SignatureAlgorithm;\n            existingKeyPair.SigningKey = accountKeysData.SignatureKeyPairData.WrappedSigningKey;\n            existingKeyPair.VerifyingKey = accountKeysData.SignatureKeyPairData.VerifyingKey;\n            existingKeyPair.RevisionDate = timestamp;\n        }\n        else\n        {\n            var newKeyPair = new UserSignatureKeyPair\n            {\n                UserId = userId,\n                SignatureAlgorithm = accountKeysData.SignatureKeyPairData!.SignatureAlgorithm,\n                SigningKey = accountKeysData.SignatureKeyPairData.WrappedSigningKey,\n                VerifyingKey = accountKeysData.SignatureKeyPairData.VerifyingKey,\n                CreationDate = timestamp,\n                RevisionDate = timestamp\n            };\n            newKeyPair.SetNewId();\n            await dbContext.UserSignatureKeyPairs.AddAsync(newKeyPair);\n        }\n\n        await dbContext.SaveChangesAsync();\n\n        // Update additional user data within the same transaction\n        if (updateUserDataActions != null)\n        {\n            foreach (var action in updateUserDataActions)\n            {\n                await action();\n            }\n        }\n        await transaction.CommitAsync();\n    }\n\n    public async Task<IEnumerable<Core.Entities.User>> GetManyAsync(IEnumerable<Guid> ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var users = dbContext.Users.Where(x => ids.Contains(x.Id));\n            return await users.ToListAsync();\n        }\n    }\n\n    public async Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var users = dbContext.Users.Where(x => ids.Contains(x.Id));\n            return await users.Select(e => new UserWithCalculatedPremium(e)\n            {\n                HasPremiumAccess = e.Premium || dbContext.OrganizationUsers\n                    .Any(ou => ou.UserId == e.Id &&\n                               dbContext.Organizations\n                                   .Any(o => o.Id == ou.OrganizationId &&\n                                             o.UsersGetPremium == true &&\n                                             o.Enabled == true))\n            }).ToListAsync();\n        }\n    }\n\n    public async Task<UserWithCalculatedPremium?> GetCalculatedPremiumAsync(Guid id)\n    {\n        var result = await GetManyWithCalculatedPremiumAsync([id]);\n        return result.FirstOrDefault();\n    }\n\n    public async Task<IEnumerable<UserPremiumAccess>> GetPremiumAccessByIdsAsync(IEnumerable<Guid> ids)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var users = await dbContext.Users\n                .Where(x => ids.Contains(x.Id))\n                .Include(u => u.OrganizationUsers)\n                    .ThenInclude(ou => ou.Organization)\n                .ToListAsync();\n\n            return users.Select(user => new UserPremiumAccess\n            {\n                Id = user.Id,\n                PersonalPremium = user.Premium,\n                OrganizationPremium = user.OrganizationUsers\n                    .Any(ou => ou.Organization != null &&\n                               ou.Organization.Enabled == true &&\n                               ou.Organization.UsersGetPremium == true)\n            }).ToList();\n        }\n    }\n\n    public async Task<UserPremiumAccess?> GetPremiumAccessAsync(Guid userId)\n    {\n        var result = await GetPremiumAccessByIdsAsync([userId]);\n        return result.FirstOrDefault();\n    }\n\n    public override async Task DeleteAsync(Core.Entities.User user)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var transaction = await dbContext.Database.BeginTransactionAsync();\n\n            MigrateDefaultUserCollectionsToShared(dbContext, [user.Id]);\n            await dbContext.SaveChangesAsync();\n\n            dbContext.WebAuthnCredentials.RemoveRange(dbContext.WebAuthnCredentials.Where(w => w.UserId == user.Id));\n            dbContext.Ciphers.RemoveRange(dbContext.Ciphers.Where(c => c.UserId == user.Id));\n            dbContext.Folders.RemoveRange(dbContext.Folders.Where(f => f.UserId == user.Id));\n            dbContext.AuthRequests.RemoveRange(dbContext.AuthRequests.Where(s => s.UserId == user.Id));\n            dbContext.Devices.RemoveRange(dbContext.Devices.Where(d => d.UserId == user.Id));\n            var collectionUsers = from cu in dbContext.CollectionUsers\n                                  join ou in dbContext.OrganizationUsers on cu.OrganizationUserId equals ou.Id\n                                  where ou.UserId == user.Id\n                                  select cu;\n            dbContext.CollectionUsers.RemoveRange(collectionUsers);\n            var groupUsers = from gu in dbContext.GroupUsers\n                             join ou in dbContext.OrganizationUsers on gu.OrganizationUserId equals ou.Id\n                             where ou.UserId == user.Id\n                             select gu;\n            dbContext.GroupUsers.RemoveRange(groupUsers);\n            dbContext.UserProjectAccessPolicy.RemoveRange(\n                dbContext.UserProjectAccessPolicy.Where(ap => ap.OrganizationUser.UserId == user.Id));\n            dbContext.UserServiceAccountAccessPolicy.RemoveRange(\n                dbContext.UserServiceAccountAccessPolicy.Where(ap => ap.OrganizationUser.UserId == user.Id));\n            dbContext.OrganizationUsers.RemoveRange(dbContext.OrganizationUsers.Where(ou => ou.UserId == user.Id));\n            dbContext.ProviderUsers.RemoveRange(dbContext.ProviderUsers.Where(pu => pu.UserId == user.Id));\n            dbContext.SsoUsers.RemoveRange(dbContext.SsoUsers.Where(su => su.UserId == user.Id));\n            dbContext.EmergencyAccesses.RemoveRange(\n                dbContext.EmergencyAccesses.Where(ea => ea.GrantorId == user.Id || ea.GranteeId == user.Id));\n            dbContext.Sends.RemoveRange(dbContext.Sends.Where(s => s.UserId == user.Id));\n            dbContext.NotificationStatuses.RemoveRange(dbContext.NotificationStatuses.Where(ns => ns.UserId == user.Id));\n            dbContext.Notifications.RemoveRange(dbContext.Notifications.Where(n => n.UserId == user.Id));\n\n            var mappedUser = Mapper.Map<User>(user);\n            dbContext.Users.Remove(mappedUser);\n\n            await dbContext.SaveChangesAsync();\n            await transaction.CommitAsync();\n        }\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Core.Entities.User> users)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var transaction = await dbContext.Database.BeginTransactionAsync();\n\n            var targetIds = users.Select(u => u.Id).ToList();\n\n            MigrateDefaultUserCollectionsToShared(dbContext, targetIds);\n            await dbContext.SaveChangesAsync();\n\n            await dbContext.WebAuthnCredentials.Where(wa => targetIds.Contains(wa.UserId)).ExecuteDeleteAsync();\n            await dbContext.Ciphers.Where(c => targetIds.Contains(c.UserId ?? default)).ExecuteDeleteAsync();\n            await dbContext.Folders.Where(f => targetIds.Contains(f.UserId)).ExecuteDeleteAsync();\n            await dbContext.AuthRequests.Where(a => targetIds.Contains(a.UserId)).ExecuteDeleteAsync();\n            await dbContext.Devices.Where(d => targetIds.Contains(d.UserId)).ExecuteDeleteAsync();\n            await dbContext.CollectionUsers\n                .Join(dbContext.OrganizationUsers,\n                      cu => cu.OrganizationUserId,\n                      ou => ou.Id,\n                      (cu, ou) => new { CollectionUser = cu, OrganizationUser = ou })\n                .Where((joined) => targetIds.Contains(joined.OrganizationUser.UserId ?? default))\n                .Select(joined => joined.CollectionUser)\n                .ExecuteDeleteAsync();\n            await dbContext.GroupUsers\n                .Join(dbContext.OrganizationUsers,\n                      gu => gu.OrganizationUserId,\n                      ou => ou.Id,\n                      (gu, ou) => new { GroupUser = gu, OrganizationUser = ou })\n                .Where(joined => targetIds.Contains(joined.OrganizationUser.UserId ?? default))\n                .Select(joined => joined.GroupUser)\n                .ExecuteDeleteAsync();\n            await dbContext.UserProjectAccessPolicy.Where(ap => targetIds.Contains(ap.OrganizationUser.UserId ?? default)).ExecuteDeleteAsync();\n            await dbContext.UserServiceAccountAccessPolicy.Where(ap => targetIds.Contains(ap.OrganizationUser.UserId ?? default)).ExecuteDeleteAsync();\n            await dbContext.OrganizationUsers.Where(ou => targetIds.Contains(ou.UserId ?? default)).ExecuteDeleteAsync();\n            await dbContext.ProviderUsers.Where(pu => targetIds.Contains(pu.UserId ?? default)).ExecuteDeleteAsync();\n            await dbContext.SsoUsers.Where(su => targetIds.Contains(su.UserId)).ExecuteDeleteAsync();\n            await dbContext.EmergencyAccesses.Where(ea => targetIds.Contains(ea.GrantorId) || targetIds.Contains(ea.GranteeId ?? default)).ExecuteDeleteAsync();\n            await dbContext.Sends.Where(s => targetIds.Contains(s.UserId ?? default)).ExecuteDeleteAsync();\n            await dbContext.NotificationStatuses.Where(ns => targetIds.Contains(ns.UserId)).ExecuteDeleteAsync();\n            await dbContext.Notifications.Where(n => targetIds.Contains(n.UserId ?? default)).ExecuteDeleteAsync();\n\n            await dbContext.Users.Where(u => targetIds.Contains(u.Id)).ExecuteDeleteAsync();\n\n            await dbContext.SaveChangesAsync();\n            await transaction.CommitAsync();\n        }\n    }\n\n    public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey)\n    {\n        return async (_, _) =>\n        {\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n\n            var userEntity = await dbContext.Users.FindAsync(userId);\n            if (userEntity == null)\n            {\n                throw new ArgumentException(\"User not found\", nameof(userId));\n            }\n\n            var timestamp = DateTime.UtcNow;\n\n            userEntity.Key = keyConnectorWrappedUserKey;\n            // Key Connector does not use KDF, so we set some defaults\n            userEntity.Kdf = KdfType.Argon2id;\n            userEntity.KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default;\n            userEntity.KdfMemory = AuthConstants.ARGON2_MEMORY.Default;\n            userEntity.KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default;\n            userEntity.UsesKeyConnector = true;\n            userEntity.RevisionDate = timestamp;\n            userEntity.AccountRevisionDate = timestamp;\n\n            await dbContext.SaveChangesAsync();\n        };\n    }\n\n    public UpdateUserData SetMasterPassword(Guid userId, MasterPasswordUnlockData masterPasswordUnlockData,\n        string serverSideHashedMasterPasswordAuthenticationHash, string? masterPasswordHint)\n    {\n        return async (_, _) =>\n        {\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n\n            var userEntity = await dbContext.Users.FindAsync(userId);\n            if (userEntity == null)\n            {\n                throw new ArgumentException(\"User not found\", nameof(userId));\n            }\n\n            var timestamp = DateTime.UtcNow;\n\n            userEntity.MasterPassword = serverSideHashedMasterPasswordAuthenticationHash;\n            userEntity.MasterPasswordHint = masterPasswordHint;\n            userEntity.Key = masterPasswordUnlockData.MasterKeyWrappedUserKey;\n            userEntity.Kdf = masterPasswordUnlockData.Kdf.KdfType;\n            userEntity.KdfIterations = masterPasswordUnlockData.Kdf.Iterations;\n            userEntity.KdfMemory = masterPasswordUnlockData.Kdf.Memory;\n            userEntity.KdfParallelism = masterPasswordUnlockData.Kdf.Parallelism;\n            userEntity.RevisionDate = timestamp;\n            userEntity.AccountRevisionDate = timestamp;\n            userEntity.MasterPasswordSalt = masterPasswordUnlockData.Salt;\n            await dbContext.SaveChangesAsync();\n        };\n    }\n\n    public async Task UpdateUserDataAsync(IEnumerable<UpdateUserData> updateUserDataActions)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        await using var transaction = await dbContext.Database.BeginTransactionAsync();\n\n        foreach (var action in updateUserDataActions)\n        {\n            await action();\n        }\n\n        await transaction.CommitAsync();\n    }\n\n    private static void MigrateDefaultUserCollectionsToShared(DatabaseContext dbContext, IEnumerable<Guid> userIds)\n    {\n        var defaultCollections = (from c in dbContext.Collections\n                                  join cu in dbContext.CollectionUsers on c.Id equals cu.CollectionId\n                                  join ou in dbContext.OrganizationUsers on cu.OrganizationUserId equals ou.Id\n                                  join u in dbContext.Users on ou.UserId equals u.Id\n                                  where userIds.Contains(ou.UserId!.Value)\n                                    && c.Type == Core.Enums.CollectionType.DefaultUserCollection\n                                  select new { Collection = c, UserEmail = u.Email })\n                                 .ToList();\n\n        foreach (var item in defaultCollections)\n        {\n            item.Collection.Type = Core.Enums.CollectionType.SharedCollection;\n            item.Collection.DefaultUserCollectionEmail = item.Collection.DefaultUserCollectionEmail ?? item.UserEmail;\n            item.Collection.RevisionDate = DateTime.UtcNow;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Configurations/AccessPolicyEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Configurations;\n\npublic class AccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<AccessPolicy>\n{\n    public void Configure(EntityTypeBuilder<AccessPolicy> builder)\n    {\n        builder\n            .HasDiscriminator<string>(\"Discriminator\")\n            .HasValue<UserProjectAccessPolicy>(AccessPolicyDiscriminator.UserProject)\n            .HasValue<UserServiceAccountAccessPolicy>(AccessPolicyDiscriminator.UserServiceAccount)\n            .HasValue<UserSecretAccessPolicy>(AccessPolicyDiscriminator.UserSecret)\n            .HasValue<GroupProjectAccessPolicy>(AccessPolicyDiscriminator.GroupProject)\n            .HasValue<GroupServiceAccountAccessPolicy>(AccessPolicyDiscriminator.GroupServiceAccount)\n            .HasValue<GroupSecretAccessPolicy>(AccessPolicyDiscriminator.GroupSecret)\n            .HasValue<ServiceAccountProjectAccessPolicy>(AccessPolicyDiscriminator.ServiceAccountProject)\n            .HasValue<ServiceAccountSecretAccessPolicy>(AccessPolicyDiscriminator.ServiceAccountSecret);\n\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasKey(s => s.Id)\n            .IsClustered();\n\n        builder.ToTable(nameof(AccessPolicy));\n    }\n}\n\npublic class UserProjectAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<UserProjectAccessPolicy>\n{\n    public void Configure(EntityTypeBuilder<UserProjectAccessPolicy> builder)\n    {\n        builder\n            .Property(e => e.OrganizationUserId)\n            .HasColumnName(nameof(UserProjectAccessPolicy.OrganizationUserId));\n\n        builder\n            .Property(e => e.GrantedProjectId)\n            .HasColumnName(nameof(UserProjectAccessPolicy.GrantedProjectId));\n\n        builder\n            .HasOne(e => e.GrantedProject)\n            .WithMany(e => e.UserAccessPolicies)\n            .HasForeignKey(nameof(UserProjectAccessPolicy.GrantedProjectId))\n            .OnDelete(DeleteBehavior.Cascade);\n    }\n}\n\npublic class UserServiceAccountAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<UserServiceAccountAccessPolicy>\n{\n    public void Configure(EntityTypeBuilder<UserServiceAccountAccessPolicy> builder)\n    {\n        builder\n            .Property(e => e.OrganizationUserId)\n            .HasColumnName(nameof(UserServiceAccountAccessPolicy.OrganizationUserId));\n\n        builder\n            .Property(e => e.GrantedServiceAccountId)\n            .HasColumnName(nameof(UserServiceAccountAccessPolicy.GrantedServiceAccountId));\n    }\n}\n\npublic class UserSecretAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<UserSecretAccessPolicy>\n{\n    public void Configure(EntityTypeBuilder<UserSecretAccessPolicy> builder)\n    {\n        builder\n            .Property(e => e.OrganizationUserId)\n            .HasColumnName(nameof(UserSecretAccessPolicy.OrganizationUserId));\n\n        builder\n            .Property(e => e.GrantedSecretId)\n            .HasColumnName(nameof(UserSecretAccessPolicy.GrantedSecretId));\n\n        builder\n            .HasOne(e => e.GrantedSecret)\n            .WithMany(e => e.UserAccessPolicies)\n            .HasForeignKey(nameof(UserSecretAccessPolicy.GrantedSecretId))\n            .OnDelete(DeleteBehavior.Cascade);\n    }\n}\n\npublic class GroupProjectAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<GroupProjectAccessPolicy>\n{\n    public void Configure(EntityTypeBuilder<GroupProjectAccessPolicy> builder)\n    {\n        builder\n            .Property(e => e.GroupId)\n            .HasColumnName(nameof(GroupProjectAccessPolicy.GroupId));\n\n        builder\n            .Property(e => e.GrantedProjectId)\n            .HasColumnName(nameof(GroupProjectAccessPolicy.GrantedProjectId));\n\n        builder\n            .HasOne(e => e.GrantedProject)\n            .WithMany(e => e.GroupAccessPolicies)\n            .HasForeignKey(nameof(GroupProjectAccessPolicy.GrantedProjectId))\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder\n            .HasOne(e => e.Group)\n            .WithMany()\n            .HasForeignKey(nameof(GroupProjectAccessPolicy.GroupId))\n            .OnDelete(DeleteBehavior.Cascade);\n    }\n}\n\npublic class GroupServiceAccountAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<GroupServiceAccountAccessPolicy>\n{\n    public void Configure(EntityTypeBuilder<GroupServiceAccountAccessPolicy> builder)\n    {\n        builder\n            .Property(e => e.GroupId)\n            .HasColumnName(nameof(GroupServiceAccountAccessPolicy.GroupId));\n\n        builder\n            .Property(e => e.GrantedServiceAccountId)\n            .HasColumnName(nameof(GroupServiceAccountAccessPolicy.GrantedServiceAccountId));\n\n        builder\n            .HasOne(e => e.Group)\n            .WithMany()\n            .HasForeignKey(nameof(GroupProjectAccessPolicy.GroupId))\n            .OnDelete(DeleteBehavior.Cascade);\n    }\n}\n\npublic class GroupSecretAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<GroupSecretAccessPolicy>\n{\n    public void Configure(EntityTypeBuilder<GroupSecretAccessPolicy> builder)\n    {\n        builder\n            .Property(e => e.GroupId)\n            .HasColumnName(nameof(GroupSecretAccessPolicy.GroupId));\n\n        builder\n            .Property(e => e.GrantedSecretId)\n            .HasColumnName(nameof(GroupSecretAccessPolicy.GrantedSecretId));\n\n        builder\n            .HasOne(e => e.GrantedSecret)\n            .WithMany(e => e.GroupAccessPolicies)\n            .HasForeignKey(nameof(GroupSecretAccessPolicy.GrantedSecretId))\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder\n            .HasOne(e => e.Group)\n            .WithMany()\n            .HasForeignKey(nameof(GroupSecretAccessPolicy.GroupId))\n            .OnDelete(DeleteBehavior.Cascade);\n    }\n}\n\npublic class ServiceAccountProjectAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<ServiceAccountProjectAccessPolicy>\n{\n    public void Configure(EntityTypeBuilder<ServiceAccountProjectAccessPolicy> builder)\n    {\n        builder\n            .Property(e => e.ServiceAccountId)\n            .HasColumnName(nameof(ServiceAccountProjectAccessPolicy.ServiceAccountId));\n\n        builder\n            .Property(e => e.GrantedProjectId)\n            .HasColumnName(nameof(ServiceAccountProjectAccessPolicy.GrantedProjectId));\n\n        builder\n            .HasOne(e => e.GrantedProject)\n            .WithMany(e => e.ServiceAccountAccessPolicies)\n            .HasForeignKey(nameof(ServiceAccountProjectAccessPolicy.GrantedProjectId))\n            .OnDelete(DeleteBehavior.Cascade);\n    }\n}\n\npublic class ServiceAccountSecretAccessPolicyEntityTypeConfiguration : IEntityTypeConfiguration<ServiceAccountSecretAccessPolicy>\n{\n    public void Configure(EntityTypeBuilder<ServiceAccountSecretAccessPolicy> builder)\n    {\n        builder\n            .Property(e => e.ServiceAccountId)\n            .HasColumnName(nameof(ServiceAccountSecretAccessPolicy.ServiceAccountId));\n\n        builder\n            .Property(e => e.GrantedSecretId)\n            .HasColumnName(nameof(ServiceAccountSecretAccessPolicy.GrantedSecretId));\n\n        builder\n            .HasOne(e => e.GrantedSecret)\n            .WithMany(e => e.ServiceAccountAccessPolicies)\n            .HasForeignKey(nameof(ServiceAccountSecretAccessPolicy.GrantedSecretId))\n            .OnDelete(DeleteBehavior.Cascade);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Configurations/ApiKeyEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Configurations;\n\npublic class ApiKeyEntityTypeConfiguration : IEntityTypeConfiguration<ApiKey>\n{\n    public void Configure(EntityTypeBuilder<ApiKey> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasKey(s => s.Id)\n            .IsClustered();\n\n        builder\n            .HasIndex(s => s.ServiceAccountId)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(ApiKey));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Configurations/ProjectEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Configurations;\n\npublic class ProjectEntityTypeConfiguration : IEntityTypeConfiguration<Project>\n{\n    public void Configure(EntityTypeBuilder<Project> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasKey(s => s.Id)\n            .IsClustered();\n\n        builder\n            .HasIndex(s => s.DeletedDate)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(s => s.OrganizationId)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(Project));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Configurations/SecretEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Configurations;\n\npublic class SecretEntityTypeConfiguration : IEntityTypeConfiguration<Secret>\n{\n    public void Configure(EntityTypeBuilder<Secret> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasKey(s => s.Id)\n            .IsClustered();\n\n        builder\n            .HasIndex(s => s.DeletedDate)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(s => s.OrganizationId)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(Secret));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Configurations/SecretVersionEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Configurations;\n\npublic class SecretVersionEntityTypeConfiguration : IEntityTypeConfiguration<SecretVersion>\n{\n    public void Configure(EntityTypeBuilder<SecretVersion> builder)\n    {\n        builder.Property(sv => sv.Id)\n            .ValueGeneratedNever();\n\n        builder.HasKey(sv => sv.Id)\n            .IsClustered();\n\n        builder.Property(sv => sv.Value)\n            .IsRequired();\n\n        builder.Property(sv => sv.VersionDate)\n            .IsRequired();\n\n        builder.HasOne(sv => sv.EditorServiceAccount)\n            .WithMany()\n            .HasForeignKey(sv => sv.EditorServiceAccountId)\n            .OnDelete(DeleteBehavior.SetNull);\n\n        builder.HasOne(sv => sv.EditorOrganizationUser)\n            .WithMany()\n            .HasForeignKey(sv => sv.EditorOrganizationUserId)\n            .OnDelete(DeleteBehavior.SetNull);\n\n        builder.HasIndex(sv => sv.SecretId)\n            .HasDatabaseName(\"IX_SecretVersion_SecretId\");\n\n        builder.HasIndex(sv => sv.EditorServiceAccountId)\n            .HasDatabaseName(\"IX_SecretVersion_EditorServiceAccountId\");\n\n        builder.HasIndex(sv => sv.EditorOrganizationUserId)\n            .HasDatabaseName(\"IX_SecretVersion_EditorOrganizationUserId\");\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Configurations/ServiceAccountEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Configurations;\n\npublic class ServiceAccountEntityTypeConfiguration : IEntityTypeConfiguration<ServiceAccount>\n{\n    public void Configure(EntityTypeBuilder<ServiceAccount> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasKey(s => s.Id)\n            .IsClustered();\n\n        builder\n            .HasIndex(s => s.OrganizationId)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(ServiceAccount));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Discriminators/AccessPolicyDiscriminator.cs",
    "content": "﻿namespace Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators;\n\npublic static class AccessPolicyDiscriminator\n{\n    public const string UserProject = \"user_project\";\n    public const string UserServiceAccount = \"user_service_account\";\n    public const string UserSecret = \"user_secret\";\n    public const string GroupProject = \"group_project\";\n    public const string GroupServiceAccount = \"group_service_account\";\n    public const string GroupSecret = \"group_secret\";\n    public const string ServiceAccountProject = \"service_account_project\";\n    public const string ServiceAccountSecret = \"service_account_secret\";\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Models/AccessPolicy.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Models;\n\npublic class BaseAccessPolicy : Core.SecretsManager.Entities.BaseAccessPolicy\n{\n    public string Discriminator { get; set; }\n}\n\npublic class AccessPolicyMapperProfile : Profile\n{\n    public AccessPolicyMapperProfile()\n    {\n        CreateMap<Core.SecretsManager.Entities.UserProjectAccessPolicy, UserProjectAccessPolicy>()\n            .ForMember(dst => dst.GrantedProject, opt => opt.Ignore())\n            .ForMember(dst => dst.OrganizationUser, opt => opt.Ignore())\n            .ReverseMap()\n            .ForMember(dst => dst.User, opt => opt.MapFrom(src => src.OrganizationUser.User));\n\n        CreateMap<Core.SecretsManager.Entities.UserServiceAccountAccessPolicy, UserServiceAccountAccessPolicy>()\n            .ForMember(dst => dst.GrantedServiceAccount, opt => opt.Ignore())\n            .ForMember(dst => dst.OrganizationUser, opt => opt.Ignore())\n            .ReverseMap()\n            .ForMember(dst => dst.User, opt => opt.MapFrom(src => src.OrganizationUser.User));\n\n        CreateMap<Core.SecretsManager.Entities.UserSecretAccessPolicy, UserSecretAccessPolicy>()\n            .ForMember(dst => dst.GrantedSecret, opt => opt.Ignore())\n            .ForMember(dst => dst.OrganizationUser, opt => opt.Ignore())\n            .ReverseMap()\n            .ForMember(dst => dst.User, opt => opt.MapFrom(src => src.OrganizationUser.User));\n\n        CreateMap<Core.SecretsManager.Entities.GroupProjectAccessPolicy, GroupProjectAccessPolicy>()\n            .ForMember(dst => dst.GrantedProject, opt => opt.Ignore())\n            .ForMember(dst => dst.Group, opt => opt.Ignore())\n            .ReverseMap();\n\n        CreateMap<Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy, GroupServiceAccountAccessPolicy>()\n            .ForMember(dst => dst.GrantedServiceAccount, opt => opt.Ignore())\n            .ForMember(dst => dst.Group, opt => opt.Ignore())\n            .ReverseMap();\n\n        CreateMap<Core.SecretsManager.Entities.GroupSecretAccessPolicy, GroupSecretAccessPolicy>()\n            .ForMember(dst => dst.GrantedSecret, opt => opt.Ignore())\n            .ForMember(dst => dst.Group, opt => opt.Ignore())\n            .ReverseMap();\n\n        CreateMap<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy, ServiceAccountProjectAccessPolicy>()\n            .ForMember(dst => dst.GrantedProject, opt => opt.Ignore())\n            .ForMember(dst => dst.ServiceAccount, opt => opt.Ignore())\n            .ReverseMap();\n\n        CreateMap<Core.SecretsManager.Entities.ServiceAccountSecretAccessPolicy, ServiceAccountSecretAccessPolicy>()\n            .ForMember(dst => dst.GrantedSecret, opt => opt.Ignore())\n            .ForMember(dst => dst.ServiceAccount, opt => opt.Ignore())\n            .ReverseMap();\n    }\n}\n\npublic class AccessPolicy : BaseAccessPolicy\n{\n}\n\npublic class UserProjectAccessPolicy : AccessPolicy\n{\n    public Guid? OrganizationUserId { get; set; }\n    public virtual OrganizationUser OrganizationUser { get; set; }\n    public Guid? GrantedProjectId { get; set; }\n    public virtual Project GrantedProject { get; set; }\n}\n\npublic class UserServiceAccountAccessPolicy : AccessPolicy\n{\n    public Guid? OrganizationUserId { get; set; }\n    public virtual OrganizationUser OrganizationUser { get; set; }\n    public Guid? GrantedServiceAccountId { get; set; }\n    public virtual ServiceAccount GrantedServiceAccount { get; set; }\n}\n\npublic class UserSecretAccessPolicy : AccessPolicy\n{\n    public Guid? OrganizationUserId { get; set; }\n    public virtual OrganizationUser OrganizationUser { get; set; }\n    public Guid? GrantedSecretId { get; set; }\n    public virtual Secret GrantedSecret { get; set; }\n}\n\npublic class GroupProjectAccessPolicy : AccessPolicy\n{\n    public Guid? GroupId { get; set; }\n    public virtual Group Group { get; set; }\n    public Guid? GrantedProjectId { get; set; }\n    public virtual Project GrantedProject { get; set; }\n}\n\npublic class GroupServiceAccountAccessPolicy : AccessPolicy\n{\n    public Guid? GroupId { get; set; }\n    public virtual Group Group { get; set; }\n    public Guid? GrantedServiceAccountId { get; set; }\n    public virtual ServiceAccount GrantedServiceAccount { get; set; }\n}\n\npublic class GroupSecretAccessPolicy : AccessPolicy\n{\n    public Guid? GroupId { get; set; }\n    public virtual Group Group { get; set; }\n    public Guid? GrantedSecretId { get; set; }\n    public virtual Secret GrantedSecret { get; set; }\n}\n\npublic class ServiceAccountProjectAccessPolicy : AccessPolicy\n{\n    public Guid? ServiceAccountId { get; set; }\n    public virtual ServiceAccount ServiceAccount { get; set; }\n    public Guid? GrantedProjectId { get; set; }\n    public virtual Project GrantedProject { get; set; }\n}\n\npublic class ServiceAccountSecretAccessPolicy : AccessPolicy\n{\n    public Guid? ServiceAccountId { get; set; }\n    public virtual ServiceAccount ServiceAccount { get; set; }\n    public Guid? GrantedSecretId { get; set; }\n    public virtual Secret GrantedSecret { get; set; }\n}\n\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Models/ApiKey.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Models;\n\npublic class ApiKey : Core.SecretsManager.Entities.ApiKey\n{\n    public virtual ServiceAccount ServiceAccount { get; set; }\n}\n\npublic class ApiKeyMapperProfile : Profile\n{\n    public ApiKeyMapperProfile()\n    {\n        CreateMap<Core.SecretsManager.Entities.ApiKey, ApiKey>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Models/Project.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Models;\n\npublic class Project : Core.SecretsManager.Entities.Project\n{\n    public virtual new ICollection<Secret> Secrets { get; set; }\n    public virtual Organization Organization { get; set; }\n    public virtual ICollection<GroupProjectAccessPolicy> GroupAccessPolicies { get; set; }\n    public virtual ICollection<UserProjectAccessPolicy> UserAccessPolicies { get; set; }\n    public virtual ICollection<ServiceAccountProjectAccessPolicy> ServiceAccountAccessPolicies { get; set; }\n}\n\npublic class ProjectMapperProfile : Profile\n{\n    public ProjectMapperProfile()\n    {\n        CreateMap<Core.SecretsManager.Entities.Project, Project>()\n            .PreserveReferences()\n            .ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Models/Secret.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Models;\n\npublic class Secret : Core.SecretsManager.Entities.Secret\n{\n    public new virtual ICollection<Project> Projects { get; set; }\n    public virtual Organization Organization { get; set; }\n    public virtual ICollection<UserSecretAccessPolicy> UserAccessPolicies { get; set; }\n    public virtual ICollection<GroupSecretAccessPolicy> GroupAccessPolicies { get; set; }\n    public virtual ICollection<ServiceAccountSecretAccessPolicy> ServiceAccountAccessPolicies { get; set; }\n    public virtual ICollection<SecretVersion> SecretVersions { get; set; }\n}\n\npublic class SecretMapperProfile : Profile\n{\n    public SecretMapperProfile()\n    {\n        CreateMap<Core.SecretsManager.Entities.Secret, Secret>()\n            .PreserveReferences()\n            .ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Models/SecretVersion.cs",
    "content": "﻿#nullable enable\n\nusing AutoMapper;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Models;\n\npublic class SecretVersion : Core.SecretsManager.Entities.SecretVersion\n{\n    public Secret? Secret { get; set; }\n\n    public ServiceAccount? EditorServiceAccount { get; set; }\n\n    public Bit.Infrastructure.EntityFramework.Models.OrganizationUser? EditorOrganizationUser { get; set; }\n}\n\npublic class SecretVersionMapperProfile : Profile\n{\n    public SecretVersionMapperProfile()\n    {\n        CreateMap<Core.SecretsManager.Entities.SecretVersion, SecretVersion>()\n            .PreserveReferences()\n            .ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Models/ServiceAccount.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Models;\n\npublic class ServiceAccount : Core.SecretsManager.Entities.ServiceAccount\n{\n    public virtual Organization Organization { get; set; }\n    public virtual ICollection<GroupServiceAccountAccessPolicy> GroupAccessPolicies { get; set; }\n    public virtual ICollection<UserServiceAccountAccessPolicy> UserAccessPolicies { get; set; }\n    public virtual ICollection<ServiceAccountProjectAccessPolicy> ProjectAccessPolicies { get; set; }\n    public virtual ICollection<ApiKey> ApiKeys { get; set; }\n}\n\npublic class ServiceAccountMapperProfile : Profile\n{\n    public ServiceAccountMapperProfile()\n    {\n        CreateMap<Core.SecretsManager.Entities.ServiceAccount, ServiceAccount>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/SecretsManager/Repositories/ApiKeyRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.SecretsManager.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.SecretsManager.Repositories;\n\npublic class ApiKeyRepository : Repository<Core.SecretsManager.Entities.ApiKey, ApiKey, Guid>, IApiKeyRepository\n{\n    public ApiKeyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.ApiKeys)\n    {\n    }\n\n    public async Task<ApiKeyDetails> GetDetailsByIdAsync(Guid id)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var entity = await GetDbSet(dbContext)\n            .Where(apiKey => apiKey.Id == id)\n            .Include(apiKey => apiKey.ServiceAccount)\n            .Select(apiKey => new ServiceAccountApiKeyDetails(apiKey, apiKey.ServiceAccount.OrganizationId))\n            .FirstOrDefaultAsync();\n\n        return Mapper.Map<ServiceAccountApiKeyDetails>(entity);\n    }\n\n    public async Task<ICollection<Core.SecretsManager.Entities.ApiKey>> GetManyByServiceAccountIdAsync(Guid id)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var apiKeys = await GetDbSet(dbContext).Where(e => e.ServiceAccountId == id).ToListAsync();\n\n        return Mapper.Map<List<Core.SecretsManager.Entities.ApiKey>>(apiKeys);\n    }\n\n    public async Task DeleteManyAsync(IEnumerable<Core.SecretsManager.Entities.ApiKey> objs)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var entities = objs.Select(obj => Mapper.Map<ApiKey>(obj));\n        dbContext.RemoveRange(entities);\n        await dbContext.SaveChangesAsync();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Tools/Configurations/SendEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Tools.Configurations;\n\npublic class SendEntityTypeConfiguration : IEntityTypeConfiguration<Send>\n{\n    public void Configure(EntityTypeBuilder<Send> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasIndex(s => s.UserId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(s => new { s.UserId, s.OrganizationId })\n            .IsClustered(false);\n\n        builder\n            .HasIndex(s => s.DeletionDate)\n            .IsClustered(false);\n\n        builder.ToTable(nameof(Send));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Tools/Models/Send.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Models;\n\npublic class Send : Core.Tools.Entities.Send\n{\n    public virtual Organization Organization { get; set; }\n    public virtual User User { get; set; }\n}\n\npublic class SendMapperProfile : Profile\n{\n    public SendMapperProfile()\n    {\n        CreateMap<Core.Tools.Entities.Send, Send>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Tools/Repositories/SendRepository.cs",
    "content": "﻿#nullable enable\n\nusing AutoMapper;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.Tools.Repositories;\n\n/// <inheritdoc cref=\"ISendRepository\"/>\npublic class SendRepository : Repository<Core.Tools.Entities.Send, Send, Guid>, ISendRepository\n{\n    /// <summary>\n    /// Initializes the <see cref=\"SendRepository\"/>\n    /// </summary>\n    /// <param name=\"serviceScopeFactory\">An IoC service locator.</param>\n    /// <param name=\"mapper\">An automapper service.</param>\n    public SendRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Sends)\n    { }\n\n    /// <summary>\n    /// Saves a <see cref=\"Send\"/> in the database.\n    /// </summary>\n    /// <param name=\"send\">\n    /// The send being saved.\n    /// </param>\n    /// <returns>\n    /// A task that completes once the save is complete.\n    /// The task result contains the saved <see cref=\"Send\"/>.\n    /// </returns>\n    public override async Task<Core.Tools.Entities.Send> CreateAsync(Core.Tools.Entities.Send send)\n    {\n        send = await base.CreateAsync(send);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            if (send.UserId.HasValue)\n            {\n                await UserUpdateStorage(send.UserId.Value);\n                await dbContext.UserBumpAccountRevisionDateAsync(send.UserId.Value);\n                await dbContext.SaveChangesAsync();\n            }\n        }\n\n        return send;\n    }\n\n    /// <inheritdoc />\n    public async Task<ICollection<Core.Tools.Entities.Send>> GetManyByDeletionDateAsync(DateTime deletionDateBefore)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var results = await dbContext.Sends.Where(s => s.DeletionDate < deletionDateBefore).ToListAsync();\n            return Mapper.Map<List<Core.Tools.Entities.Send>>(results);\n        }\n    }\n\n    /// <inheritdoc />\n    public async Task<ICollection<Core.Tools.Entities.Send>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var results = await dbContext.Sends.Where(s => s.UserId == userId).ToListAsync();\n            return Mapper.Map<List<Core.Tools.Entities.Send>>(results);\n        }\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,\n        IEnumerable<Core.Tools.Entities.Send> sends)\n    {\n        return async (_, _) =>\n        {\n            var newSends = sends.ToDictionary(s => s.Id);\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n            var userSends = await GetDbSet(dbContext)\n                .Where(s => s.UserId == userId)\n                .ToListAsync();\n            var validSends = userSends\n                .Where(send => newSends.ContainsKey(send.Id));\n            foreach (var send in validSends)\n            {\n                send.Key = newSends[send.Id].Key;\n            }\n\n            await dbContext.SaveChangesAsync();\n        };\n    }\n\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Configurations/SecurityTaskEntityTypeConfiguration.cs",
    "content": "﻿using Bit.Infrastructure.EntityFramework.Vault.Models;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Metadata.Builders;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Configurations;\n\npublic class SecurityTaskEntityTypeConfiguration : IEntityTypeConfiguration<SecurityTask>\n{\n    public void Configure(EntityTypeBuilder<SecurityTask> builder)\n    {\n        builder\n            .Property(s => s.Id)\n            .ValueGeneratedNever();\n\n        builder\n            .HasKey(s => s.Id)\n            .IsClustered();\n\n        builder\n            .HasIndex(s => s.OrganizationId)\n            .IsClustered(false);\n\n        builder\n            .HasIndex(s => s.CipherId)\n            .IsClustered(false);\n\n        builder\n            .HasOne(p => p.Organization)\n            .WithMany()\n            .HasForeignKey(p => p.OrganizationId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder\n            .HasOne(p => p.Cipher)\n            .WithMany()\n            .HasForeignKey(p => p.CipherId)\n            .OnDelete(DeleteBehavior.Cascade);\n\n        builder\n            .ToTable(nameof(SecurityTask));\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Models/Cipher.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Models;\n\npublic class Cipher : Core.Vault.Entities.Cipher\n{\n    public virtual User User { get; set; }\n    public virtual Organization Organization { get; set; }\n    public virtual ICollection<CollectionCipher> CollectionCiphers { get; set; }\n}\n\npublic class CipherMapperProfile : Profile\n{\n    public CipherMapperProfile()\n    {\n        CreateMap<Core.Vault.Entities.Cipher, Cipher>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Models/Folder.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Models;\n\npublic class Folder : Core.Vault.Entities.Folder\n{\n    public virtual User User { get; set; }\n}\n\npublic class FolderMapperProfile : Profile\n{\n    public FolderMapperProfile()\n    {\n        CreateMap<Core.Vault.Entities.Folder, Folder>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Models;\n\npublic class SecurityTask : Core.Vault.Entities.SecurityTask\n{\n    public virtual Organization Organization { get; set; }\n    public virtual Cipher Cipher { get; set; }\n}\n\npublic class SecurityTaskMapperProfile : Profile\n{\n    public SecurityTaskMapperProfile()\n    {\n        CreateMap<Core.Vault.Entities.SecurityTask, SecurityTask>().ReverseMap();\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing AutoMapper;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Infrastructure.EntityFramework.Models;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Repositories.Vault.Queries;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\nusing Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;\nusing LinqToDB.EntityFrameworkCore;\nusing Microsoft.Data.SqlClient;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing NS = Newtonsoft.Json;\nusing NSL = Newtonsoft.Json.Linq;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Repositories;\n\npublic class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, Guid>, ICipherRepository\n{\n    public CipherRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Ciphers)\n    { }\n\n    public override async Task<Core.Vault.Entities.Cipher> CreateAsync(Core.Vault.Entities.Cipher cipher)\n    {\n        cipher = await base.CreateAsync(cipher);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            if (cipher.OrganizationId.HasValue)\n            {\n                await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);\n            }\n            else if (cipher.UserId.HasValue)\n            {\n                await dbContext.UserBumpAccountRevisionDateAsync(cipher.UserId.Value);\n            }\n            await dbContext.SaveChangesAsync();\n        }\n        return cipher;\n    }\n\n    public override async Task DeleteAsync(Core.Vault.Entities.Cipher cipher)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var cipherInfo = await dbContext.Ciphers\n                .Where(c => c.Id == cipher.Id)\n                .Select(c => new { c.UserId, c.OrganizationId, HasAttachments = c.Attachments != null })\n                .FirstOrDefaultAsync();\n\n            await base.DeleteAsync(cipher);\n\n            if (cipherInfo?.OrganizationId != null)\n            {\n                if (cipherInfo.HasAttachments == true)\n                {\n                    await OrganizationUpdateStorage(cipherInfo.OrganizationId.Value);\n                }\n\n                await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipherInfo.OrganizationId.Value);\n            }\n            else if (cipherInfo?.UserId != null)\n            {\n                if (cipherInfo.HasAttachments)\n                {\n                    await UserUpdateStorage(cipherInfo.UserId.Value);\n                }\n\n                await dbContext.UserBumpAccountRevisionDateAsync(cipherInfo.UserId.Value);\n            }\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task CreateAsync(Core.Vault.Entities.Cipher cipher, IEnumerable<Guid> collectionIds)\n    {\n        cipher = await CreateAsync(cipher);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await UpdateCollectionsAsync(dbContext, cipher.Id,\n                cipher.UserId, cipher.OrganizationId, collectionIds);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task CreateAsync(CipherDetails cipher)\n    {\n        await CreateAsyncReturnCipher(cipher);\n    }\n\n    private async Task<CipherDetails> CreateAsyncReturnCipher(CipherDetails cipher)\n    {\n        cipher.SetNewId();\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var userIdKey = $\"\\\"{cipher.UserId}\\\"\";\n            cipher.UserId = cipher.OrganizationId.HasValue ? null : cipher.UserId;\n            cipher.Favorites = cipher.Favorite ?\n                $\"{{{userIdKey}:true}}\" :\n                null;\n            cipher.Folders = cipher.FolderId.HasValue ?\n                $\"{{{userIdKey}:\\\"{cipher.FolderId}\\\"}}\" :\n                null;\n            var entity = Mapper.Map<Cipher>((Core.Vault.Entities.Cipher)cipher);\n            await dbContext.AddAsync(entity);\n\n            await dbContext.SaveChangesAsync();\n\n            if (cipher.OrganizationId.HasValue)\n            {\n                await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);\n            }\n            else if (cipher.UserId.HasValue)\n            {\n                await dbContext.UserBumpAccountRevisionDateAsync(cipher.UserId.Value);\n            }\n\n            await dbContext.SaveChangesAsync();\n        }\n        return cipher;\n    }\n\n    public async Task CreateAsync(CipherDetails cipher, IEnumerable<Guid> collectionIds)\n    {\n        cipher = await CreateAsyncReturnCipher(cipher);\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            await UpdateCollectionsAsync(dbContext, cipher.Id,\n                cipher.UserId, cipher.OrganizationId, collectionIds);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task CreateAsync(Guid userId, IEnumerable<Core.Vault.Entities.Cipher> ciphers,\n        IEnumerable<Core.Vault.Entities.Folder> folders)\n    {\n        ciphers = ciphers.ToList();\n        if (!ciphers.Any())\n        {\n            return;\n        }\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var folderEntities = Mapper.Map<List<Folder>>(folders);\n            await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities);\n            var cipherEntities = Mapper.Map<List<Cipher>>(ciphers);\n            await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);\n            await dbContext.UserBumpAccountRevisionDateAsync(userId);\n\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task CreateAsync(IEnumerable<Core.Vault.Entities.Cipher> ciphers,\n        IEnumerable<Core.Entities.Collection> collections,\n        IEnumerable<Core.Entities.CollectionCipher> collectionCiphers,\n        IEnumerable<Core.Entities.CollectionUser> collectionUsers)\n    {\n        if (!ciphers.Any())\n        {\n            return;\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var cipherEntities = Mapper.Map<List<Cipher>>(ciphers);\n            await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);\n\n            if (collections.Any())\n            {\n                var collectionEntities = Mapper.Map<List<Collection>>(collections);\n                await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, collectionEntities);\n            }\n\n            if (collectionCiphers.Any())\n            {\n                var collectionCipherEntities = Mapper.Map<List<CollectionCipher>>(collectionCiphers);\n                await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, collectionCipherEntities);\n            }\n\n            if (collectionUsers.Any())\n            {\n                var collectionUserEntities = Mapper.Map<List<CollectionUser>>(collectionUsers);\n                await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, collectionUserEntities);\n            }\n\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(ciphers.First().OrganizationId.Value);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task DeleteAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        await ToggleDeleteCipherStatesAsync(ids, userId, CipherStateAction.HardDelete);\n    }\n\n    public async Task DeleteAttachmentAsync(Guid cipherId, string attachmentId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var cipher = await dbContext.Ciphers.FindAsync(cipherId);\n            var attachmentsJson = NSL.JObject.Parse(cipher.Attachments);\n            attachmentsJson.Remove(attachmentId);\n            cipher.Attachments = NS.JsonConvert.SerializeObject(attachmentsJson);\n            await dbContext.SaveChangesAsync();\n\n            if (cipher.OrganizationId.HasValue)\n            {\n                await OrganizationUpdateStorage(cipher.OrganizationId.Value);\n                await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);\n            }\n            else if (cipher.UserId.HasValue)\n            {\n                await UserUpdateStorage(cipher.UserId.Value);\n                await dbContext.UserBumpAccountRevisionDateAsync(cipher.UserId.Value);\n            }\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task DeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var ciphers = from c in dbContext.Ciphers\n                          where c.OrganizationId == organizationId &&\n                                ids.Contains(c.Id)\n                          select c;\n            dbContext.RemoveRange(ciphers);\n            await dbContext.SaveChangesAsync();\n            await OrganizationUpdateStorage(organizationId);\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task DeleteByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n\n            var ciphersToDelete = from c in dbContext.Ciphers\n                                  where c.OrganizationId == organizationId\n                                        && !c.CollectionCiphers.Any(cc =>\n                                            cc.Collection.Type == CollectionType.DefaultUserCollection)\n                                  select c;\n            dbContext.RemoveRange(ciphersToDelete);\n\n            var collectionCiphersToRemove = from cc in dbContext.CollectionCiphers\n                                            join col in dbContext.Collections on cc.CollectionId equals col.Id\n                                            join c in dbContext.Ciphers on cc.CipherId equals c.Id\n                                            where col.Type != CollectionType.DefaultUserCollection\n                                                  && c.OrganizationId == organizationId\n                                            select cc;\n            dbContext.RemoveRange(collectionCiphersToRemove);\n\n            await OrganizationUpdateStorage(organizationId);\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task DeleteByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var ciphers = from c in dbContext.Ciphers\n                          where c.UserId == userId\n                          select c;\n            dbContext.RemoveRange(ciphers);\n            var folders = from f in dbContext.Folders\n                          where f.UserId == userId\n                          select f;\n            dbContext.RemoveRange(folders);\n            await dbContext.SaveChangesAsync();\n            await UserUpdateStorage(userId);\n            await dbContext.UserBumpAccountRevisionDateAsync(userId);\n            await dbContext.SaveChangesAsync();\n        }\n\n    }\n\n    public async Task DeleteDeletedAsync(DateTime deletedDateBefore)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.Ciphers.Where(c => c.DeletedDate < deletedDateBefore);\n            dbContext.RemoveRange(query);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task<ICollection<OrganizationCipherPermission>>\n        GetCipherPermissionsForOrganizationAsync(Guid organizationId, Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new CipherOrganizationPermissionsQuery(organizationId, userId).Run(dbContext);\n\n            ICollection<OrganizationCipherPermission> permissions;\n\n            // SQLite does not support the GROUP BY clause\n            if (dbContext.Database.IsSqlite())\n            {\n                permissions = (await query.ToListAsync())\n                    .GroupBy(c => new { c.Id, c.OrganizationId })\n                    .Select(g => new OrganizationCipherPermission\n                    {\n                        Id = g.Key.Id,\n                        OrganizationId = g.Key.OrganizationId,\n                        Read = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Read))),\n                        ViewPassword = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.ViewPassword))),\n                        Edit = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Edit))),\n                        Manage = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Manage))),\n                    }).ToList();\n            }\n            else\n            {\n                var groupByQuery = from p in query\n                                   group p by new { p.Id, p.OrganizationId }\n                    into g\n                                   select new OrganizationCipherPermission\n                                   {\n                                       Id = g.Key.Id,\n                                       OrganizationId = g.Key.OrganizationId,\n                                       Read = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Read))),\n                                       ViewPassword = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.ViewPassword))),\n                                       Edit = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Edit))),\n                                       Manage = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Manage))),\n                                   };\n                permissions = await groupByQuery.ToListAsync();\n            }\n\n            return permissions;\n        }\n    }\n\n    public async Task<ICollection<UserSecurityTaskCipher>> GetUserSecurityTasksByCipherIdsAsync(Guid organizationId, IEnumerable<Core.Vault.Entities.SecurityTask> tasks)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var cipherIds = tasks.Where(t => t.CipherId.HasValue).Select(t => t.CipherId.Value);\n            var dbContext = GetDatabaseContext(scope);\n            var query = new UserSecurityTasksByCipherIdsQuery(organizationId, cipherIds).Run(dbContext);\n\n            ICollection<UserSecurityTaskCipher> userTaskCiphers;\n\n            // SQLite does not support the GROUP BY clause\n            if (dbContext.Database.IsSqlite())\n            {\n                userTaskCiphers = (await query.ToListAsync())\n                    .GroupBy(c => new { c.UserId, c.Email, c.CipherId })\n                    .Select(g => new UserSecurityTaskCipher\n                    {\n                        UserId = g.Key.UserId,\n                        Email = g.Key.Email,\n                        CipherId = g.Key.CipherId,\n                    }).ToList();\n            }\n            else\n            {\n                var groupByQuery = from p in query\n                                   group p by new { p.UserId, p.Email, p.CipherId }\n                    into g\n                                   select new UserSecurityTaskCipher\n                                   {\n                                       UserId = g.Key.UserId,\n                                       CipherId = g.Key.CipherId,\n                                       Email = g.Key.Email,\n                                   };\n                userTaskCiphers = await groupByQuery.ToListAsync();\n            }\n\n            foreach (var userTaskCipher in userTaskCiphers)\n            {\n                userTaskCipher.TaskId = tasks.First(t => t.CipherId == userTaskCipher.CipherId).Id;\n            }\n\n            return userTaskCiphers;\n        }\n    }\n\n    public async Task<CipherDetails> GetByIdAsync(Guid id, Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var userCipherDetails = new UserCipherDetailsQuery(userId);\n            var data = await userCipherDetails.Run(dbContext).FirstOrDefaultAsync(c => c.Id == id);\n            return data;\n        }\n    }\n\n    public async Task<ICollection<CipherOrganizationDetails>> GetManyOrganizationDetailsByOrganizationIdAsync(\n        Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new CipherOrganizationDetailsReadByOrganizationIdQuery(organizationId);\n            var data = await query.Run(dbContext).ToListAsync();\n            return data;\n        }\n    }\n\n    public async Task<bool> GetCanEditByIdAsync(Guid userId, Guid cipherId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new CipherReadCanEditByIdUserIdQuery(userId, cipherId);\n            var canEdit = await query.Run(dbContext).AnyAsync();\n            return canEdit;\n        }\n    }\n\n    public async Task<ICollection<Core.Vault.Entities.Cipher>> GetManyByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = dbContext.Ciphers.Where(x => !x.UserId.HasValue && x.OrganizationId == organizationId);\n            var data = await query.ToListAsync();\n            return Mapper.Map<List<Core.Vault.Entities.Cipher>>(data);\n        }\n    }\n\n    public async Task<ICollection<CipherOrganizationDetails>> GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new CipherOrganizationDetailsReadByOrganizationIdQuery(organizationId, true);\n            var data = await query.Run(dbContext).ToListAsync();\n            return data;\n        }\n    }\n\n    public async Task<ICollection<CipherDetails>> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var cipherDetailsView = withOrganizations ?\n                new UserCipherDetailsQuery(userId).Run(dbContext) :\n                new CipherDetailsQuery(userId).Run(dbContext);\n            if (!withOrganizations)\n            {\n                cipherDetailsView = from c in cipherDetailsView\n                                    where c.UserId == userId\n                                    select new CipherDetails\n                                    {\n                                        Id = c.Id,\n                                        UserId = c.UserId,\n                                        OrganizationId = c.OrganizationId,\n                                        Type = c.Type,\n                                        Data = c.Data,\n                                        Attachments = c.Attachments,\n                                        CreationDate = c.CreationDate,\n                                        RevisionDate = c.RevisionDate,\n                                        DeletedDate = c.DeletedDate,\n                                        Favorite = c.Favorite,\n                                        FolderId = c.FolderId,\n                                        Edit = true,\n                                        Reprompt = c.Reprompt,\n                                        ViewPassword = true,\n                                        Manage = true,\n                                        OrganizationUseTotp = false,\n                                        Key = c.Key,\n                                        ArchivedDate = c.ArchivedDate,\n                                    };\n            }\n\n            var ciphers = await cipherDetailsView.ToListAsync();\n\n            return ciphers.GroupBy(c => c.Id)\n                .Select(g => g.OrderByDescending(c => c.Manage)\n                    .ThenByDescending(c => c.Edit)\n                    .ThenByDescending(c => c.ViewPassword)\n                    .First())\n                .ToList();\n        }\n    }\n\n    public async Task<CipherOrganizationDetails> GetOrganizationDetailsByIdAsync(Guid id)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = new CipherOrganizationDetailsReadByIdQuery(id);\n            var data = await query.Run(dbContext).FirstOrDefaultAsync();\n            return data;\n        }\n    }\n\n    public async Task MoveAsync(IEnumerable<Guid> ids, Guid? folderId, Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var cipherEntities = dbContext.Ciphers.Where(c => ids.Contains(c.Id));\n            var userCipherDetails = new UserCipherDetailsQuery(userId).Run(dbContext);\n            var idsToMove = from ucd in userCipherDetails\n                            join c in cipherEntities\n                                on ucd.Id equals c.Id\n                            select c;\n            await idsToMove.ForEachAsync(cipher =>\n            {\n                var foldersJson = string.IsNullOrWhiteSpace(cipher.Folders) ?\n                    new NSL.JObject() :\n                    NSL.JObject.Parse(cipher.Folders);\n\n                if (folderId.HasValue)\n                {\n                    foldersJson.Remove(userId.ToString());\n                    foldersJson.Add(userId.ToString(), folderId.Value.ToString());\n                }\n                else if (!string.IsNullOrWhiteSpace(cipher.Folders))\n                {\n                    foldersJson.Remove(userId.ToString());\n                }\n                dbContext.Attach(cipher);\n                cipher.Folders = NS.JsonConvert.SerializeObject(foldersJson);\n            });\n            await dbContext.UserBumpAccountRevisionDateAsync(userId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task ReplaceAsync(CipherDetails cipher)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var entity = await dbContext.Ciphers.FindAsync(cipher.Id);\n            if (entity != null)\n            {\n                if (cipher.UserId.HasValue)\n                {\n                    if (cipher.Favorite)\n                    {\n                        if (cipher.Favorites == null)\n                        {\n                            var jsonObject = new JsonObject(new[]\n                            {\n                            new KeyValuePair<string, JsonNode>(cipher.UserId.Value.ToString(), true),\n                        });\n                            cipher.Favorites = JsonSerializer.Serialize(jsonObject);\n                        }\n                        else\n                        {\n                            var favorites = CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, bool>>(cipher.Favorites);\n                            favorites.Add(cipher.UserId.Value, true);\n                            cipher.Favorites = JsonSerializer.Serialize(favorites);\n                        }\n                    }\n                    else\n                    {\n                        if (cipher.Favorites != null && cipher.Favorites.Contains(cipher.UserId.Value.ToString()))\n                        {\n                            var favorites = CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, bool>>(cipher.Favorites);\n                            favorites.Remove(cipher.UserId.Value);\n                            cipher.Favorites = JsonSerializer.Serialize(favorites);\n                        }\n                    }\n                    if (cipher.FolderId.HasValue)\n                    {\n                        if (cipher.Folders == null)\n                        {\n                            var jsonObject = new JsonObject(new[]\n                            {\n                            new KeyValuePair<string, JsonNode>(cipher.UserId.Value.ToString(), cipher.FolderId),\n                        });\n                            cipher.Folders = JsonSerializer.Serialize(jsonObject);\n                        }\n                        else\n                        {\n                            var folders = CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, Guid>>(cipher.Folders);\n                            folders.Add(cipher.UserId.Value, cipher.FolderId.Value);\n                            cipher.Folders = JsonSerializer.Serialize(folders);\n                        }\n                    }\n                    else\n                    {\n                        if (cipher.Folders != null && cipher.Folders.Contains(cipher.UserId.Value.ToString()))\n                        {\n                            var folders = CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, Guid>>(cipher.Folders);\n                            folders.Remove(cipher.UserId.Value);\n                            cipher.Folders = JsonSerializer.Serialize(folders);\n                        }\n                    }\n                }\n                // Check if this cipher is a part of an organization, and if so do\n                // not save the UserId into the database. This must be done after we\n                // set the user specific data like Folders and Favorites because\n                // the UserId key is used for that\n                cipher.UserId = cipher.OrganizationId.HasValue ?\n                    null :\n                    cipher.UserId;\n\n                var mappedEntity = Mapper.Map<Cipher>(cipher);\n                dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);\n\n                if (cipher.OrganizationId.HasValue)\n                {\n                    await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);\n                }\n                else if (cipher.UserId.HasValue)\n                {\n                    await dbContext.UserBumpAccountRevisionDateAsync(cipher.UserId.Value);\n                }\n\n                await dbContext.SaveChangesAsync();\n            }\n        }\n    }\n\n    private static async Task<int> UpdateCollectionsAsync(DatabaseContext context, Guid id, Guid? userId, Guid? organizationId, IEnumerable<Guid> collectionIds)\n    {\n        if (!organizationId.HasValue || !collectionIds.Any())\n        {\n            return -1;\n        }\n\n        IQueryable<Guid> availableCollectionsQuery;\n\n        if (!userId.HasValue)\n        {\n            availableCollectionsQuery = context.Collections\n                .Where(c => c.OrganizationId == organizationId.Value)\n                .Select(c => c.Id);\n        }\n        else\n        {\n            availableCollectionsQuery =\n                new CollectionsReadByOrganizationIdUserIdQuery(organizationId.Value, userId.Value)\n                    .Run(context)\n                    .Select(c => c.Id);\n        }\n\n        var availableCollections = await availableCollectionsQuery.ToListAsync();\n\n        if (!availableCollections.Any())\n        {\n            return -1;\n        }\n\n        var collectionCiphers = collectionIds\n            .Where(collectionId => availableCollections.Contains(collectionId))\n            .Select(collectionId => new CollectionCipher\n            {\n                CollectionId = collectionId,\n                CipherId = id,\n            });\n        context.CollectionCiphers.AddRange(collectionCiphers);\n        return 0;\n    }\n\n    public async Task<bool> ReplaceAsync(Core.Vault.Entities.Cipher cipher, IEnumerable<Guid> collectionIds)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var transaction = await dbContext.Database.BeginTransactionAsync();\n            var successes = await UpdateCollectionsAsync(\n                dbContext, cipher.Id, cipher.UserId,\n                cipher.OrganizationId, collectionIds);\n\n            if (successes < 0)\n            {\n                await transaction.CommitAsync();\n                return false;\n            }\n\n            var trackedCipher = await dbContext.Ciphers.FindAsync(cipher.Id);\n\n            trackedCipher.UserId = null;\n            trackedCipher.OrganizationId = cipher.OrganizationId;\n            trackedCipher.Data = cipher.Data;\n            trackedCipher.Attachments = cipher.Attachments;\n            trackedCipher.RevisionDate = cipher.RevisionDate;\n            trackedCipher.DeletedDate = cipher.DeletedDate;\n            trackedCipher.Key = cipher.Key;\n            trackedCipher.Folders = cipher.Folders;\n            trackedCipher.Favorites = cipher.Favorites;\n            trackedCipher.Reprompt = cipher.Reprompt;\n\n            await transaction.CommitAsync();\n\n            if (!string.IsNullOrWhiteSpace(cipher.Attachments))\n            {\n                if (cipher.OrganizationId.HasValue)\n                {\n                    await OrganizationUpdateStorage(cipher.OrganizationId.Value);\n                }\n                else if (cipher.UserId.HasValue)\n                {\n                    await UserUpdateStorage(cipher.UserId.Value);\n                }\n            }\n\n            if (cipher.OrganizationId.HasValue)\n            {\n                await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);\n            }\n            else if (cipher.UserId.HasValue)\n            {\n                await dbContext.UserBumpAccountRevisionDateAsync(cipher.UserId.Value);\n            }\n\n            await dbContext.SaveChangesAsync();\n            return true;\n        }\n    }\n\n    public async Task<DateTime> UnarchiveAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        return await ToggleArchiveCipherStatesAsync(ids, userId, CipherStateAction.Unarchive);\n    }\n\n    public async Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        return await ToggleDeleteCipherStatesAsync(ids, userId, CipherStateAction.Restore);\n    }\n\n    public async Task<DateTime> RestoreByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var utcNow = DateTime.UtcNow;\n            var ciphers = from c in dbContext.Ciphers\n                          where c.OrganizationId == organizationId &&\n                                ids.Contains(c.Id)\n                          select c;\n\n            await ciphers.ForEachAsync(cipher =>\n            {\n                dbContext.Attach(cipher);\n                cipher.DeletedDate = null;\n                cipher.RevisionDate = utcNow;\n            });\n\n            await OrganizationUpdateStorage(organizationId);\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId);\n            await dbContext.SaveChangesAsync();\n            return utcNow;\n        }\n    }\n\n    public async Task<DateTime> ArchiveAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        return await ToggleArchiveCipherStatesAsync(ids, userId, CipherStateAction.Archive);\n    }\n\n    public async Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId)\n    {\n        await ToggleDeleteCipherStatesAsync(ids, userId, CipherStateAction.SoftDelete);\n    }\n\n    private async Task<DateTime> ToggleArchiveCipherStatesAsync(IEnumerable<Guid> ids, Guid userId, CipherStateAction action)\n    {\n        static bool FilterArchivedDate(CipherStateAction action, CipherDetails ucd)\n        {\n            return action switch\n            {\n                CipherStateAction.Unarchive => ucd.ArchivedDate != null,\n                CipherStateAction.Archive => ucd.ArchivedDate == null,\n                _ => true\n            };\n        }\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var userCipherDetailsQuery = new UserCipherDetailsQuery(userId);\n            var cipherEntitiesToCheck = await dbContext.Ciphers.Where(c => ids.Contains(c.Id)).ToListAsync();\n            var query = from ucd in await userCipherDetailsQuery.Run(dbContext).ToListAsync()\n                        join c in cipherEntitiesToCheck\n                            on ucd.Id equals c.Id\n                        where FilterArchivedDate(action, ucd)\n                        select c;\n\n            var utcNow = DateTime.UtcNow;\n            var cipherIdsToModify = query.Select(c => c.Id);\n            var cipherEntitiesToModify = dbContext.Ciphers.Where(x => cipherIdsToModify.Contains(x.Id));\n\n            await cipherEntitiesToModify.ForEachAsync(cipher =>\n            {\n                dbContext.Attach(cipher);\n\n                // Build or load the per-user archives map\n                var archives = string.IsNullOrWhiteSpace(cipher.Archives)\n                    ? new Dictionary<Guid, DateTime>()\n                    : CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, DateTime>>(cipher.Archives)\n                      ?? new Dictionary<Guid, DateTime>();\n\n                if (action == CipherStateAction.Unarchive)\n                {\n                    // Remove this user's archive record\n                    archives.Remove(userId);\n                }\n                else if (action == CipherStateAction.Archive)\n                {\n                    // Set this user's archive date\n                    archives[userId] = utcNow;\n                }\n\n                // Persist the updated JSON or clear it if empty\n                cipher.Archives = archives.Count == 0\n                    ? null\n                    : CoreHelpers.ClassToJsonData(archives);\n\n                cipher.RevisionDate = utcNow;\n            });\n\n            await dbContext.UserBumpAccountRevisionDateAsync(userId);\n            await dbContext.SaveChangesAsync();\n\n            return utcNow;\n        }\n    }\n\n    private async Task<DateTime> ToggleDeleteCipherStatesAsync(IEnumerable<Guid> ids, Guid userId, CipherStateAction action)\n    {\n        static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd)\n        {\n            return action switch\n            {\n                CipherStateAction.Restore => ucd.DeletedDate != null,\n                CipherStateAction.SoftDelete => ucd.DeletedDate == null,\n                _ => true\n            };\n        }\n\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var userCipherDetailsQuery = new UserCipherDetailsQuery(userId);\n            var cipherEntitiesToCheck = await dbContext.Ciphers.Where(c => ids.Contains(c.Id)).ToListAsync();\n            var query = from ucd in await userCipherDetailsQuery.Run(dbContext).ToListAsync()\n                        join c in cipherEntitiesToCheck\n                            on ucd.Id equals c.Id\n                        where ucd.Edit && FilterDeletedDate(action, ucd)\n                        select c;\n\n            var utcNow = DateTime.UtcNow;\n            var cipherIdsToModify = query.Select(c => c.Id);\n            var cipherEntitiesToModify = dbContext.Ciphers.Where(x => cipherIdsToModify.Contains(x.Id));\n            if (action == CipherStateAction.HardDelete)\n            {\n                dbContext.RemoveRange(cipherEntitiesToModify);\n            }\n            else\n            {\n                await cipherEntitiesToModify.ForEachAsync(cipher =>\n                {\n                    dbContext.Attach(cipher);\n                    cipher.DeletedDate = action == CipherStateAction.Restore ? null : utcNow;\n                    cipher.RevisionDate = utcNow;\n                });\n            }\n\n            var orgIds = query\n                .Where(c => c.OrganizationId.HasValue)\n                .GroupBy(c => c.OrganizationId).Select(x => x.Key);\n\n            foreach (var orgId in orgIds)\n            {\n                await OrganizationUpdateStorage(orgId.Value);\n                await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(orgId.Value);\n            }\n            if (query.Any(c => c.UserId.HasValue && !string.IsNullOrWhiteSpace(c.Attachments)))\n            {\n                await UserUpdateStorage(userId);\n            }\n            await dbContext.UserBumpAccountRevisionDateAsync(userId);\n            await dbContext.SaveChangesAsync();\n\n            return utcNow;\n        }\n    }\n\n    public async Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var utcNow = DateTime.UtcNow;\n            var ciphers = dbContext.Ciphers.Where(c => ids.Contains(c.Id) && c.OrganizationId == organizationId);\n            await ciphers.ForEachAsync(cipher =>\n            {\n                dbContext.Attach(cipher);\n                cipher.DeletedDate = utcNow;\n                cipher.RevisionDate = utcNow;\n            });\n            await OrganizationUpdateStorage(organizationId);\n            await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task UpdateAttachmentAsync(CipherAttachment attachment)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var cipher = await dbContext.Ciphers.FindAsync(attachment.Id);\n            var attachments = string.IsNullOrWhiteSpace(cipher.Attachments) ?\n                new Dictionary<string, CipherAttachment.MetaData>() :\n                NS.JsonConvert.DeserializeObject<Dictionary<string, CipherAttachment.MetaData>>(cipher.Attachments);\n            var metaData = NS.JsonConvert.DeserializeObject<CipherAttachment.MetaData>(attachment.AttachmentData);\n            attachments[attachment.AttachmentId] = metaData;\n            cipher.Attachments = NS.JsonConvert.SerializeObject(attachments);\n            await dbContext.SaveChangesAsync();\n\n            if (attachment.OrganizationId.HasValue)\n            {\n                await OrganizationUpdateStorage(cipher.OrganizationId.Value);\n                await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);\n            }\n            else if (attachment.UserId.HasValue)\n            {\n                await UserUpdateStorage(attachment.UserId.Value);\n                await dbContext.UserBumpAccountRevisionDateAsync(attachment.UserId.Value);\n            }\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task UpdateCiphersAsync(Guid userId, IEnumerable<Core.Vault.Entities.Cipher> ciphers)\n    {\n        if (!ciphers.Any())\n        {\n            return;\n        }\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var ciphersToUpdate = ciphers.ToDictionary(c => c.Id);\n\n            var existingCiphers = await dbContext.Ciphers\n                .Where(c => c.UserId == userId && ciphersToUpdate.Keys.Contains(c.Id))\n                .ToDictionaryAsync(c => c.Id);\n\n            foreach (var (cipherId, cipher) in ciphersToUpdate)\n            {\n                if (!existingCiphers.TryGetValue(cipherId, out var existingCipher))\n                {\n                    // The Dapper version does not validate that the same amount of items given where updated.\n                    continue;\n                }\n\n                existingCipher.UserId = cipher.UserId;\n                existingCipher.OrganizationId = cipher.OrganizationId;\n                existingCipher.Type = cipher.Type;\n                existingCipher.Data = cipher.Data;\n                existingCipher.Attachments = cipher.Attachments;\n                existingCipher.RevisionDate = cipher.RevisionDate;\n                existingCipher.DeletedDate = cipher.DeletedDate;\n                existingCipher.Key = cipher.Key;\n            }\n\n            await dbContext.UserBumpAccountRevisionDateAsync(userId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    public async Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var cipher = await dbContext.Ciphers.FindAsync(id);\n\n            var foldersJson = NSL.JObject.Parse(cipher.Folders);\n            if (foldersJson == null && folderId.HasValue)\n            {\n                foldersJson.Add(userId.ToString(), folderId.Value);\n            }\n            else if (foldersJson != null && folderId.HasValue)\n            {\n                foldersJson[userId] = folderId.Value;\n            }\n            else\n            {\n                foldersJson.Remove(userId.ToString());\n            }\n\n            var favoritesJson = NSL.JObject.Parse(cipher.Favorites);\n            if (favorite)\n            {\n                favoritesJson.Add(userId.ToString(), favorite);\n            }\n            else\n            {\n                favoritesJson.Remove(userId.ToString());\n            }\n\n            await dbContext.UserBumpAccountRevisionDateAsync(userId);\n            await dbContext.SaveChangesAsync();\n        }\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(\n        Guid userId, IEnumerable<Core.Vault.Entities.Cipher> ciphers)\n    {\n        return async (SqlConnection _, SqlTransaction _) =>\n        {\n            var newCiphers = ciphers.ToList();\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n            var userCiphers = await GetDbSet(dbContext)\n                .Where(c => c.UserId == userId)\n                .ToListAsync();\n            var validCiphers = userCiphers\n                .Where(cipher => newCiphers.Any(newCipher => newCipher.Id == cipher.Id));\n            foreach (var cipher in validCiphers)\n            {\n                var updateCipher = newCiphers.First(newCipher => newCipher.Id == cipher.Id);\n                cipher.Data = updateCipher.Data;\n                cipher.Attachments = updateCipher.Attachments;\n                cipher.RevisionDate = updateCipher.RevisionDate;\n                cipher.Key = updateCipher.Key;\n            }\n\n            await dbContext.SaveChangesAsync();\n        };\n    }\n\n    public async Task<IEnumerable<CipherOrganizationDetailsWithCollections>>\n        GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(Guid organizationId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var defaultTypeInt = (int)CollectionType.DefaultUserCollection;\n\n        //  filter out any cipher that belongs *only* to default collections\n        //    i.e. keep ciphers with no collections, or with ≥1 non-default collection\n        var query = from c in dbContext.Ciphers.AsNoTracking()\n                    where c.UserId == null\n                       && c.OrganizationId == organizationId\n                       && c.Organization.Enabled\n                       && (\n                            c.CollectionCiphers.Count() == 0\n                            || c.CollectionCiphers.Any(cc => (int)cc.Collection.Type != defaultTypeInt)\n                          )\n                    select new CipherOrganizationDetailsWithCollections(\n                        new CipherOrganizationDetails\n                        {\n                            Id = c.Id,\n                            UserId = c.UserId,\n                            OrganizationId = c.OrganizationId,\n                            Type = c.Type,\n                            Data = c.Data,\n                            Favorites = c.Favorites,\n                            Folders = c.Folders,\n                            Attachments = c.Attachments,\n                            CreationDate = c.CreationDate,\n                            RevisionDate = c.RevisionDate,\n                            DeletedDate = c.DeletedDate,\n                            Reprompt = c.Reprompt,\n                            Key = c.Key,\n                            OrganizationUseTotp = c.Organization.UseTotp\n                        },\n                        new Dictionary<Guid, IGrouping<Guid, Bit.Core.Entities.CollectionCipher>>()\n                    )\n                    {\n                        CollectionIds = c.CollectionCiphers\n                            .Where(cc => (int)cc.Collection.Type != defaultTypeInt)\n                            .Select(cc => cc.CollectionId)\n                            .ToArray()\n                    };\n\n        var result = await query.ToListAsync();\n        return result;\n    }\n\n    public async Task UpsertAsync(CipherDetails cipher)\n    {\n        if (cipher.Id.Equals(default))\n        {\n            await CreateAsync(cipher);\n        }\n        else\n        {\n            await ReplaceAsync(cipher);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoMapper;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\nusing Microsoft.Data.SqlClient;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Repositories;\n\npublic class FolderRepository : Repository<Core.Vault.Entities.Folder, Folder, Guid>, IFolderRepository\n{\n    public FolderRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.Folders)\n    { }\n\n    public async Task<Core.Vault.Entities.Folder> GetByIdAsync(Guid id, Guid userId)\n    {\n        var folder = await base.GetByIdAsync(id);\n        if (folder == null || folder.UserId != userId)\n        {\n            return null;\n        }\n\n        return folder;\n    }\n\n    public async Task<ICollection<Core.Vault.Entities.Folder>> GetManyByUserIdAsync(Guid userId)\n    {\n        using (var scope = ServiceScopeFactory.CreateScope())\n        {\n            var dbContext = GetDatabaseContext(scope);\n            var query = from f in dbContext.Folders\n                        where f.UserId == userId\n                        select f;\n            var folders = await query.ToListAsync();\n            return Mapper.Map<List<Core.Vault.Entities.Folder>>(folders);\n        }\n    }\n\n    /// <inheritdoc />\n    public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(\n        Guid userId, IEnumerable<Core.Vault.Entities.Folder> folders)\n    {\n        return async (SqlConnection _, SqlTransaction _) =>\n        {\n            var newFolders = folders.ToList();\n            using var scope = ServiceScopeFactory.CreateScope();\n            var dbContext = GetDatabaseContext(scope);\n            var userFolders = await GetDbSet(dbContext)\n                .Where(f => f.UserId == userId)\n                .ToListAsync();\n            var validFolders = userFolders\n                .Where(folder => newFolders.Any(newFolder => newFolder.Id == folder.Id));\n            foreach (var folder in validFolders)\n            {\n                var updateFolder = newFolders.First(newFolder => newFolder.Id == folder.Id);\n                folder.Name = updateFolder.Name;\n            }\n\n            await dbContext.SaveChangesAsync();\n        };\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherDetailsQuery.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;\n\npublic class CipherDetailsQuery : IQuery<CipherDetails>\n{\n    private readonly Guid? _userId;\n    private readonly bool _ignoreFolders;\n    public CipherDetailsQuery(Guid? userId, bool ignoreFolders = false)\n    {\n        _userId = userId;\n        _ignoreFolders = ignoreFolders;\n    }\n    public virtual IQueryable<CipherDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from c in dbContext.Ciphers\n                    select new CipherDetails\n                    {\n                        Id = c.Id,\n                        UserId = c.UserId,\n                        OrganizationId = c.OrganizationId,\n                        Type = c.Type,\n                        Data = c.Data,\n                        Attachments = c.Attachments,\n                        CreationDate = c.CreationDate,\n                        RevisionDate = c.RevisionDate,\n                        DeletedDate = c.DeletedDate,\n                        Reprompt = c.Reprompt,\n                        Key = c.Key,\n                        Favorite = _userId.HasValue && c.Favorites != null && c.Favorites.ToLowerInvariant().Contains($\"\\\"{_userId}\\\":true\"),\n                        FolderId = (_ignoreFolders || !_userId.HasValue || c.Folders == null || !c.Folders.ToLowerInvariant().Contains(_userId.Value.ToString())) ?\n                            null :\n                            CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, Guid>>(c.Folders)[_userId.Value],\n                        ArchivedDate = !_userId.HasValue || c.Archives == null || !c.Archives.ToLowerInvariant().Contains(_userId.Value.ToString()) ?\n                            null :\n                            CoreHelpers.LoadClassFromJsonData<Dictionary<Guid, DateTime?>>(c.Archives)[_userId.Value],\n                    };\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationDetailsReadByIdQuery.cs",
    "content": "﻿using Bit.Core.Vault.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;\n\npublic class CipherOrganizationDetailsReadByIdQuery : IQuery<CipherOrganizationDetails>\n{\n    private readonly Guid _cipherId;\n\n    public CipherOrganizationDetailsReadByIdQuery(Guid cipherId)\n    {\n        _cipherId = cipherId;\n    }\n\n    public virtual IQueryable<CipherOrganizationDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from c in dbContext.Ciphers\n                    join o in dbContext.Organizations\n                        on c.OrganizationId equals o.Id into o_g\n                    from o in o_g.DefaultIfEmpty()\n                    where c.Id == _cipherId\n                    select new CipherOrganizationDetails\n                    {\n                        Id = c.Id,\n                        UserId = c.UserId,\n                        OrganizationId = c.OrganizationId,\n                        Type = c.Type,\n                        Data = c.Data,\n                        Favorites = c.Favorites,\n                        Folders = c.Folders,\n                        Attachments = c.Attachments,\n                        CreationDate = c.CreationDate,\n                        RevisionDate = c.RevisionDate,\n                        DeletedDate = c.DeletedDate,\n                        OrganizationUseTotp = o.UseTotp,\n                    };\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationDetailsReadByOrganizationIdQuery.cs",
    "content": "﻿using Bit.Core.Vault.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Repositories.Vault.Queries;\n\npublic class CipherOrganizationDetailsReadByOrganizationIdQuery : IQuery<CipherOrganizationDetails>\n{\n    private readonly Guid _organizationId;\n    private readonly bool _unassignedOnly;\n\n    /// <summary>\n    /// Query for retrieving ciphers organization details by organization id\n    /// </summary>\n    /// <param name=\"organizationId\">The id of the organization to query</param>\n    /// <param name=\"unassignedOnly\">Only include ciphers that are not assigned to any collection</param>\n    public CipherOrganizationDetailsReadByOrganizationIdQuery(Guid organizationId, bool unassignedOnly = false)\n    {\n        _organizationId = organizationId;\n        _unassignedOnly = unassignedOnly;\n    }\n    public virtual IQueryable<CipherOrganizationDetails> Run(DatabaseContext dbContext)\n    {\n        var query = from c in dbContext.Ciphers\n                    join o in dbContext.Organizations\n                        on c.OrganizationId equals o.Id into o_g\n                    from o in o_g.DefaultIfEmpty()\n                    where c.OrganizationId == _organizationId\n                    select new CipherOrganizationDetails\n                    {\n                        Id = c.Id,\n                        UserId = c.UserId,\n                        OrganizationId = c.OrganizationId,\n                        Type = c.Type,\n                        Data = c.Data,\n                        Favorites = c.Favorites,\n                        Folders = c.Folders,\n                        Attachments = c.Attachments,\n                        CreationDate = c.CreationDate,\n                        RevisionDate = c.RevisionDate,\n                        DeletedDate = c.DeletedDate,\n                        OrganizationUseTotp = o.UseTotp,\n                    };\n\n        if (_unassignedOnly)\n        {\n            var collectionCipherIds = from cc in dbContext.CollectionCiphers\n                                      join c in dbContext.Collections\n                                          on cc.CollectionId equals c.Id\n                                      where c.OrganizationId == _organizationId\n                                      select cc.CipherId;\n\n            query = query.Where(c => !collectionCipherIds.Contains(c.Id));\n        }\n\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs",
    "content": "﻿using Bit.Core.Vault.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;\n\npublic class CipherOrganizationPermissionsQuery : IQuery<OrganizationCipherPermission>\n{\n    private readonly Guid _organizationId;\n    private readonly Guid _userId;\n\n    public CipherOrganizationPermissionsQuery(Guid organizationId, Guid userId)\n    {\n        _organizationId = organizationId;\n        _userId = userId;\n    }\n\n    public IQueryable<OrganizationCipherPermission> Run(DatabaseContext dbContext)\n    {\n        return from c in dbContext.Ciphers\n\n               join ou in dbContext.OrganizationUsers\n                   on new { CipherUserId = c.UserId, c.OrganizationId, UserId = (Guid?)_userId } equals\n                   new { CipherUserId = (Guid?)null, OrganizationId = (Guid?)ou.OrganizationId, ou.UserId, }\n\n               join o in dbContext.Organizations\n                   on new { c.OrganizationId, OuOrganizationId = ou.OrganizationId, Enabled = true } equals\n                   new { OrganizationId = (Guid?)o.Id, OuOrganizationId = o.Id, o.Enabled }\n\n               join cc in dbContext.CollectionCiphers\n                   on c.Id equals cc.CipherId into cc_g\n               from cc in cc_g.DefaultIfEmpty()\n\n               join cu in dbContext.CollectionUsers\n                   on new { cc.CollectionId, OrganizationUserId = ou.Id } equals\n                   new { cu.CollectionId, cu.OrganizationUserId } into cu_g\n               from cu in cu_g.DefaultIfEmpty()\n\n               join gu in dbContext.GroupUsers\n                   on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                   new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n               from gu in gu_g.DefaultIfEmpty()\n\n               join g in dbContext.Groups\n                   on gu.GroupId equals g.Id into g_g\n               from g in g_g.DefaultIfEmpty()\n\n               join cg in dbContext.CollectionGroups\n                   on new { cc.CollectionId, gu.GroupId } equals\n                   new { cg.CollectionId, cg.GroupId } into cg_g\n               from cg in cg_g.DefaultIfEmpty()\n\n               select new OrganizationCipherPermission()\n               {\n                   Id = c.Id,\n                   OrganizationId = o.Id,\n                   Read = cu != null || cg != null,\n                   ViewPassword = !((bool?)cu.HidePasswords ?? (bool?)cg.HidePasswords ?? true),\n                   Edit = !((bool?)cu.ReadOnly ?? (bool?)cg.ReadOnly ?? true),\n                   Manage = (bool?)cu.Manage ?? (bool?)cg.Manage ?? false,\n               };\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherReadCanEditByIdUserIdQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;\n\npublic class CipherReadCanEditByIdUserIdQuery : IQuery<Cipher>\n{\n    private readonly Guid _userId;\n    private readonly Guid _cipherId;\n\n    public CipherReadCanEditByIdUserIdQuery(Guid userId, Guid cipherId)\n    {\n        _userId = userId;\n        _cipherId = cipherId;\n    }\n\n    public virtual IQueryable<Cipher> Run(DatabaseContext dbContext)\n    {\n        var query = from c in dbContext.Ciphers\n\n                    join o in dbContext.Organizations\n                        on new { c.UserId, c.OrganizationId } equals\n                           new { UserId = (Guid?)null, OrganizationId = (Guid?)o.Id } into o_g\n                    from o in o_g.DefaultIfEmpty()\n\n                    join ou in dbContext.OrganizationUsers\n                        on new { OrganizationId = o.Id, UserId = (Guid?)_userId } equals\n                           new { ou.OrganizationId, ou.UserId } into ou_g\n                    from ou in ou_g.DefaultIfEmpty()\n\n                    join cc in dbContext.CollectionCiphers\n                        on new { c.UserId, CipherId = c.Id } equals\n                           new { UserId = (Guid?)null, cc.CipherId } into cc_g\n                    from cc in cc_g.DefaultIfEmpty()\n\n                    join cu in dbContext.CollectionUsers\n                        on new { cc.CollectionId, OrganizationUserId = ou.Id } equals\n                           new { cu.CollectionId, cu.OrganizationUserId } into cu_g\n                    from cu in cu_g.DefaultIfEmpty()\n\n                    join gu in dbContext.GroupUsers\n                        on new { c.UserId, CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                           new { UserId = (Guid?)null, CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n                    from gu in gu_g.DefaultIfEmpty()\n\n                    join g in dbContext.Groups\n                        on gu.GroupId equals g.Id into g_g\n                    from g in g_g.DefaultIfEmpty()\n\n                    join cg in dbContext.CollectionGroups\n                        on new { cc.CollectionId, gu.GroupId } equals\n                           new { cg.CollectionId, cg.GroupId } into cg_g\n                    from cg in cg_g.DefaultIfEmpty()\n\n                    where\n                    c.Id == _cipherId &&\n                    (\n                        c.UserId == _userId ||\n                        (\n                            !c.UserId.HasValue && ou.Status == OrganizationUserStatusType.Confirmed && o.Enabled &&\n                            ((cu == null ? (Guid?)null : cu.CollectionId) != null || (cg == null ? (Guid?)null : cg.CollectionId) != null)\n                        )\n                    ) &&\n                    (c.UserId.HasValue || !cu.ReadOnly || !cg.ReadOnly)\n                    select c;\n        return query;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/Queries/SecurityTaskReadByUserIdStatusQuery.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Enums;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;\n\npublic class SecurityTaskReadByUserIdStatusQuery : IQuery<SecurityTask>\n{\n    private readonly Guid _userId;\n    private readonly SecurityTaskStatus? _status;\n\n    public SecurityTaskReadByUserIdStatusQuery(Guid userId, SecurityTaskStatus? status)\n    {\n        _userId = userId;\n        _status = status;\n    }\n\n    public IQueryable<SecurityTask> Run(DatabaseContext dbContext)\n    {\n        var query = from st in dbContext.SecurityTasks\n\n                    join ou in dbContext.OrganizationUsers\n                        on st.OrganizationId equals ou.OrganizationId\n\n                    join o in dbContext.Organizations\n                        on st.OrganizationId equals o.Id\n\n                    join c in dbContext.Ciphers\n                        on st.CipherId equals c.Id into c_g\n                    from c in c_g.DefaultIfEmpty()\n\n                    join cc in dbContext.CollectionCiphers\n                        on c.Id equals cc.CipherId into cc_g\n                    from cc in cc_g.DefaultIfEmpty()\n\n                    join cu in dbContext.CollectionUsers\n                        on new { cc.CollectionId, OrganizationUserId = ou.Id } equals\n                        new { cu.CollectionId, cu.OrganizationUserId } into cu_g\n                    from cu in cu_g.DefaultIfEmpty()\n\n                    join gu in dbContext.GroupUsers\n                        on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals\n                        new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g\n                    from gu in gu_g.DefaultIfEmpty()\n\n                    join cg in dbContext.CollectionGroups\n                        on new { cc.CollectionId, gu.GroupId } equals\n                        new { cg.CollectionId, cg.GroupId } into cg_g\n                    from cg in cg_g.DefaultIfEmpty()\n\n                    where\n                        ou.UserId == _userId &&\n                        ou.Status == OrganizationUserStatusType.Confirmed &&\n                        o.Enabled &&\n                        (\n                            st.CipherId == null ||\n                            (\n                                c != null &&\n                                (\n                                    (cu != null && !cu.ReadOnly) || (cg != null && !cg.ReadOnly && cu == null)\n                                )\n                            )\n                        ) &&\n                        (_status == null || st.Status == _status)\n                    group st by new\n                    {\n                        st.Id,\n                        st.OrganizationId,\n                        st.CipherId,\n                        st.Type,\n                        st.Status,\n                        st.CreationDate,\n                        st.RevisionDate\n                    } into g\n                    select new SecurityTask\n                    {\n                        Id = g.Key.Id,\n                        OrganizationId = g.Key.OrganizationId,\n                        CipherId = g.Key.CipherId,\n                        Type = g.Key.Type,\n                        Status = g.Key.Status,\n                        CreationDate = g.Key.CreationDate,\n                        RevisionDate = g.Key.RevisionDate\n                    };\n\n        return query.OrderByDescending(st => st.CreationDate);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/Queries/UserSecurityTasksByCipherIdsQuery.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories.Queries;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;\n\npublic class UserSecurityTasksByCipherIdsQuery : IQuery<UserCipherForTask>\n{\n    private readonly Guid _organizationId;\n    private readonly IEnumerable<Guid> _cipherIds;\n\n    public UserSecurityTasksByCipherIdsQuery(Guid organizationId, IEnumerable<Guid> cipherIds)\n    {\n        _organizationId = organizationId;\n        _cipherIds = cipherIds;\n    }\n\n    public IQueryable<UserCipherForTask> Run(DatabaseContext dbContext)\n    {\n        var baseCiphers =\n            from c in dbContext.Ciphers\n            where _cipherIds.Contains(c.Id)\n            join o in dbContext.Organizations\n                on c.OrganizationId equals o.Id\n            where o.Id == _organizationId && o.Enabled\n            select c;\n\n        var userPermissions =\n            from c in baseCiphers\n            join cc in dbContext.CollectionCiphers\n                on c.Id equals cc.CipherId\n            join cu in dbContext.CollectionUsers\n                on cc.CollectionId equals cu.CollectionId\n            join ou in dbContext.OrganizationUsers\n                on cu.OrganizationUserId equals ou.Id\n            where ou.OrganizationId == _organizationId\n                && cu.Manage == true\n            select new { ou.UserId, c.Id };\n\n        var groupPermissions =\n            from c in baseCiphers\n            join cc in dbContext.CollectionCiphers\n                on c.Id equals cc.CipherId\n            join cg in dbContext.CollectionGroups\n                on cc.CollectionId equals cg.CollectionId\n            join gu in dbContext.GroupUsers\n                on cg.GroupId equals gu.GroupId\n            join ou in dbContext.OrganizationUsers\n                on gu.OrganizationUserId equals ou.Id\n            where ou.OrganizationId == _organizationId\n                && cg.Manage == true\n                && !userPermissions.Any(up => up.Id == c.Id && up.UserId == ou.UserId)\n            select new { ou.UserId, c.Id };\n\n        return userPermissions.Union(groupPermissions)\n            .Join(\n                dbContext.Users,\n                p => p.UserId,\n                u => u.Id,\n                (p, u) => new { p.UserId, p.Id, u.Email }\n            )\n            .GroupBy(x => new { x.UserId, x.Email, x.Id })\n            .Select(g => new UserCipherForTask\n            {\n                UserId = (Guid)g.Key.UserId,\n                Email = g.Key.Email,\n                CipherId = g.Key.Id\n            })\n            .OrderByDescending(x => x.Email);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Vault.Enums;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Bit.Infrastructure.EntityFramework.Vault.Models;\nusing Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.Infrastructure.EntityFramework.Vault.Repositories;\n\npublic class SecurityTaskRepository : Repository<Core.Vault.Entities.SecurityTask, SecurityTask, Guid>, ISecurityTaskRepository\n{\n    public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)\n        : base(serviceScopeFactory, mapper, (context) => context.SecurityTasks)\n    { }\n\n    /// <inheritdoc />\n    public async Task<ICollection<Core.Vault.Entities.SecurityTask>> GetManyByUserIdStatusAsync(Guid userId,\n        SecurityTaskStatus? status = null)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = new SecurityTaskReadByUserIdStatusQuery(userId, status);\n        var data = await query.Run(dbContext).ToListAsync();\n        return data;\n    }\n\n    /// <inheritdoc />\n    public async Task<ICollection<Core.Vault.Entities.SecurityTask>> GetManyByOrganizationIdStatusAsync(Guid organizationId,\n        SecurityTaskStatus? status = null)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var query = from st in dbContext.SecurityTasks\n                    join o in dbContext.Organizations\n                        on st.OrganizationId equals o.Id\n                    where\n                        o.Enabled &&\n                        st.OrganizationId == organizationId &&\n                        (status == null || st.Status == status)\n                    select new Core.Vault.Entities.SecurityTask\n                    {\n                        Id = st.Id,\n                        OrganizationId = st.OrganizationId,\n                        CipherId = st.CipherId,\n                        Status = st.Status,\n                        Type = st.Type,\n                        CreationDate = st.CreationDate,\n                        RevisionDate = st.RevisionDate,\n                    };\n\n        return await query.OrderByDescending(st => st.CreationDate).ToListAsync();\n    }\n\n    /// <inheritdoc />\n    public async Task<ICollection<Core.Vault.Entities.SecurityTask>> CreateManyAsync(\n        IEnumerable<Core.Vault.Entities.SecurityTask> tasks)\n    {\n        var tasksList = tasks?.ToList();\n        if (tasksList is null || tasksList.Count == 0)\n        {\n            return Array.Empty<SecurityTask>();\n        }\n\n        foreach (var task in tasksList)\n        {\n            task.SetNewId();\n        }\n\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n        var entities = Mapper.Map<List<SecurityTask>>(tasksList);\n        await dbContext.AddRangeAsync(entities);\n        await dbContext.SaveChangesAsync();\n\n        return tasksList;\n    }\n\n    /// <inheritdoc />\n    public async Task<Core.Vault.Entities.SecurityTaskMetrics> GetTaskMetricsAsync(Guid organizationId)\n    {\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var metrics = await (from st in dbContext.SecurityTasks\n                             join o in dbContext.Organizations on st.OrganizationId equals o.Id\n                             where st.OrganizationId == organizationId && o.Enabled\n                             select st)\n                           .GroupBy(x => 1)\n                           .Select(g => new Core.Vault.Entities.SecurityTaskMetrics(\n                               g.Count(x => x.Status == SecurityTaskStatus.Completed),\n                               g.Count()\n                           ))\n                           .FirstOrDefaultAsync();\n\n        return metrics ?? new Core.Vault.Entities.SecurityTaskMetrics(0, 0);\n    }\n\n    /// <inheritdoc />\n    public async Task MarkAsCompleteByCipherIds(IEnumerable<Guid> cipherIds)\n    {\n        if (!cipherIds.Any())\n        {\n            return;\n        }\n\n        using var scope = ServiceScopeFactory.CreateScope();\n        var dbContext = GetDatabaseContext(scope);\n\n        var cipherIdsList = cipherIds.ToList();\n\n        await dbContext.SecurityTasks\n            .Where(st => st.CipherId.HasValue && cipherIdsList.Contains(st.CipherId.Value) && st.Status != SecurityTaskStatus.Completed)\n            .ExecuteUpdateAsync(st => st\n                .SetProperty(s => s.Status, SecurityTaskStatus.Completed)\n                .SetProperty(s => s.RevisionDate, DateTime.UtcNow));\n    }\n}\n"
  },
  {
    "path": "src/Notifications/AnonymousNotificationsHub.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.SignalR;\n\nnamespace Bit.Notifications;\n\n[AllowAnonymous]\npublic class AnonymousNotificationsHub : Microsoft.AspNetCore.SignalR.Hub, INotificationHub\n{\n    public override async Task OnConnectedAsync()\n    {\n        var httpContext = Context.GetHttpContext();\n        var token = httpContext.Request.Query[\"Token\"].FirstOrDefault();\n        if (!string.IsNullOrWhiteSpace(token))\n        {\n            await Groups.AddToGroupAsync(Context.ConnectionId, token);\n        }\n        await base.OnConnectedAsync();\n    }\n}\n"
  },
  {
    "path": "src/Notifications/AzureQueueHostedService.cs",
    "content": "﻿using Azure.Storage.Queues;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Notifications;\n\npublic class AzureQueueHostedService : IHostedService, IDisposable\n{\n    private readonly ILogger _logger;\n    private readonly HubHelpers _hubHelpers;\n    private readonly GlobalSettings _globalSettings;\n\n    private Task? _executingTask;\n    private CancellationTokenSource? _cts;\n\n    public AzureQueueHostedService(\n        ILogger<AzureQueueHostedService> logger,\n        HubHelpers hubHelpers,\n        GlobalSettings globalSettings)\n    {\n        _logger = logger;\n        _hubHelpers = hubHelpers;\n        _globalSettings = globalSettings;\n    }\n\n    public Task StartAsync(CancellationToken cancellationToken)\n    {\n        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        _executingTask = ExecuteAsync(_cts.Token);\n        return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;\n    }\n\n    public async Task StopAsync(CancellationToken cancellationToken)\n    {\n        if (_executingTask == null)\n        {\n            return;\n        }\n\n        _logger.LogWarning(\"Stopping service.\");\n        _cts?.Cancel();\n        await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));\n        cancellationToken.ThrowIfCancellationRequested();\n    }\n\n    public void Dispose()\n    {\n    }\n\n    private async Task ExecuteAsync(CancellationToken cancellationToken)\n    {\n        var queueClient = new QueueClient(_globalSettings.Notifications.ConnectionString, \"notifications\");\n        while (!cancellationToken.IsCancellationRequested)\n        {\n            try\n            {\n                var messages = await queueClient.ReceiveMessagesAsync(32, cancellationToken: cancellationToken);\n                if (messages.Value?.Any() ?? false)\n                {\n                    foreach (var message in messages.Value)\n                    {\n                        try\n                        {\n                            var decodedMessage = message.DecodeMessageText();\n                            if (!string.IsNullOrWhiteSpace(decodedMessage))\n                            {\n                                await _hubHelpers.SendNotificationToHubAsync(decodedMessage, cancellationToken);\n                            }\n\n                            await queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt,\n                                cancellationToken);\n                        }\n                        catch (Exception e)\n                        {\n                            _logger.LogError(e, \"Error processing dequeued message: {MessageId} x{DequeueCount}.\",\n                                message.MessageId, message.DequeueCount);\n                            if (message.DequeueCount > 2)\n                            {\n                                await queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt,\n                                    cancellationToken);\n                            }\n                        }\n                    }\n                }\n                else\n                {\n                    await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);\n                }\n            }\n            catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)\n            {\n                _logger.LogDebug(\"Task.Delay cancelled during Alpine container shutdown\");\n                break;\n            }\n            catch (Exception e)\n            {\n                _logger.LogError(e, \"Error processing messages.\");\n            }\n        }\n\n        _logger.LogWarning(\"Done processing.\");\n    }\n}\n"
  },
  {
    "path": "src/Notifications/ConnectionCounter.cs",
    "content": "﻿namespace Bit.Notifications;\n\npublic class ConnectionCounter\n{\n    private int _count = 0;\n\n    public void Increment()\n    {\n        Interlocked.Increment(ref _count);\n    }\n\n    public void Decrement()\n    {\n        Interlocked.Decrement(ref _count);\n    }\n\n    public void Reset()\n    {\n        _count = 0;\n    }\n\n    public int GetCount()\n    {\n        return _count;\n    }\n}\n"
  },
  {
    "path": "src/Notifications/Controllers/InfoController.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Notifications.Controllers;\n\npublic class InfoController : Controller\n{\n    [HttpGet(\"~/alive\")]\n    [HttpGet(\"~/now\")]\n    public DateTime GetAlive()\n    {\n        return DateTime.UtcNow;\n    }\n\n    [HttpGet(\"~/version\")]\n    public JsonResult GetVersion()\n    {\n        return Json(AssemblyHelpers.GetVersion());\n    }\n}\n"
  },
  {
    "path": "src/Notifications/Controllers/SendController.cs",
    "content": "﻿#nullable enable\nusing System.Text;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Notifications.Controllers;\n\n[Authorize(\"Internal\")]\npublic class SendController : Controller\n{\n    private readonly HubHelpers _hubHelpers;\n\n    public SendController(HubHelpers hubHelpers)\n    {\n        _hubHelpers = hubHelpers;\n    }\n\n    [HttpPost(\"~/send\")]\n    [SelfHosted(SelfHostedOnly = true)]\n    public async Task PostSendAsync()\n    {\n        using var reader = new StreamReader(Request.Body, Encoding.UTF8);\n        var notificationJson = await reader.ReadToEndAsync();\n        if (!string.IsNullOrWhiteSpace(notificationJson))\n        {\n            await _hubHelpers.SendNotificationToHubAsync(notificationJson);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Notifications/Dockerfile",
    "content": "###############################################\n#                 Build stage                 #\n###############################################\nFROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build\n\n# Docker buildx supplies the value for this arg\nARG TARGETPLATFORM\n\n# Determine proper runtime value for .NET\n# We put the value in a file to be read by later layers.\nRUN if [ \"$TARGETPLATFORM\" = \"linux/amd64\" ]; then \\\n    RID=linux-musl-x64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm64\" ]; then \\\n    RID=linux-musl-arm64 ; \\\n    elif [ \"$TARGETPLATFORM\" = \"linux/arm/v7\" ]; then \\\n    RID=linux-musl-arm ; \\\n    fi \\\n    && echo \"RID=$RID\" > /tmp/rid.txt\n\n# Copy required project files\nWORKDIR /source\nCOPY . ./\n\n# Restore project dependencies and tools\nWORKDIR /source/src/Notifications\nRUN . /tmp/rid.txt && dotnet restore -r $RID\n\n# Build project\nRUN . /tmp/rid.txt && dotnet publish \\\n    -c release \\\n    --no-restore \\\n    --self-contained \\\n    /p:PublishSingleFile=true \\\n    -r $RID \\\n    -o out\n\n###############################################\n#                  App stage                  #\n###############################################\nFROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21\n\nARG TARGETPLATFORM\nLABEL com.bitwarden.product=\"bitwarden\"\nENV ASPNETCORE_ENVIRONMENT=Production\nENV ASPNETCORE_URLS=http://+:5000\nENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates\nENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false\nEXPOSE 5000\n\nRUN apk add --no-cache curl \\\n    icu-libs \\\n    shadow \\\n    tzdata \\\n    && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu\n\n# Copy app from the build stage\nWORKDIR /app\nCOPY --from=build /source/src/Notifications/out /app\nCOPY ./src/Notifications/entrypoint.sh /entrypoint.sh\nRUN echo \"net.ipv4.ip_local_port_range = 5024 65000\" >> /etc/sysctl.d/99-sysctl.conf\nRUN echo \"net.ipv4.tcp_fin_timeout = 30\" >> /etc/sysctl.d/99-sysctl.conf\nRUN chmod +x /entrypoint.sh\nHEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": "src/Notifications/HeartbeatHostedService.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.SignalR;\n\nnamespace Bit.Notifications;\n\npublic class HeartbeatHostedService : IHostedService, IDisposable\n{\n    private readonly ILogger _logger;\n    private readonly IHubContext<NotificationsHub> _hubContext;\n    private readonly GlobalSettings _globalSettings;\n\n    private Task _executingTask;\n    private CancellationTokenSource _cts;\n\n    public HeartbeatHostedService(\n        ILogger<HeartbeatHostedService> logger,\n        IHubContext<NotificationsHub> hubContext,\n        GlobalSettings globalSettings)\n    {\n        _logger = logger;\n        _hubContext = hubContext;\n        _globalSettings = globalSettings;\n    }\n\n    public Task StartAsync(CancellationToken cancellationToken)\n    {\n        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        _executingTask = ExecuteAsync(_cts.Token);\n        return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;\n    }\n\n    public async Task StopAsync(CancellationToken cancellationToken)\n    {\n        if (_executingTask == null)\n        {\n            return;\n        }\n        _logger.LogWarning(\"Stopping service.\");\n        _cts.Cancel();\n        await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));\n        cancellationToken.ThrowIfCancellationRequested();\n    }\n\n    public void Dispose()\n    { }\n\n    private async Task ExecuteAsync(CancellationToken cancellationToken)\n    {\n        while (!cancellationToken.IsCancellationRequested)\n        {\n            await _hubContext.Clients.All.SendAsync(\"Heartbeat\");\n            await Task.Delay(120000, cancellationToken);\n        }\n        _logger.LogWarning(\"Done with heartbeat.\");\n    }\n}\n"
  },
  {
    "path": "src/Notifications/HubHelpers.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Microsoft.AspNetCore.SignalR;\n\nnamespace Bit.Notifications;\n\npublic class HubHelpers\n{\n    private static readonly JsonSerializerOptions _deserializerOptions = new() { PropertyNameCaseInsensitive = true };\n\n    private static readonly string _receiveMessageMethod = \"ReceiveMessage\";\n\n    private readonly IHubContext<NotificationsHub> _hubContext;\n    private readonly IHubContext<AnonymousNotificationsHub> _anonymousHubContext;\n    private readonly ILogger<HubHelpers> _logger;\n\n    public HubHelpers(IHubContext<NotificationsHub> hubContext,\n        IHubContext<AnonymousNotificationsHub> anonymousHubContext,\n        ILogger<HubHelpers> logger)\n    {\n        _hubContext = hubContext;\n        _anonymousHubContext = anonymousHubContext;\n        _logger = logger;\n    }\n\n    public async Task SendNotificationToHubAsync(string notificationJson, CancellationToken cancellationToken = default)\n    {\n        var notification =\n            JsonSerializer.Deserialize<PushNotificationData<object>>(notificationJson, _deserializerOptions);\n        if (notification is null)\n        {\n            return;\n        }\n\n        _logger.LogInformation(\"Sending notification: {NotificationType}\", notification.Type);\n        switch (notification.Type)\n        {\n            case PushType.SyncCipherUpdate:\n            case PushType.SyncCipherCreate:\n            case PushType.SyncCipherDelete:\n            case PushType.SyncLoginDelete:\n                var cipherNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<SyncCipherPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (cipherNotification is null)\n                {\n                    break;\n                }\n\n                if (cipherNotification.Payload.UserId.HasValue)\n                {\n                    await _hubContext.Clients.User(cipherNotification.Payload.UserId.Value.ToString())\n                        .SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken);\n                }\n                else if (cipherNotification.Payload.OrganizationId.HasValue)\n                {\n                    await _hubContext.Clients\n                        .Group(NotificationsHub.GetOrganizationGroup(cipherNotification.Payload.OrganizationId.Value))\n                        .SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken);\n                }\n\n                break;\n            case PushType.SyncFolderUpdate:\n            case PushType.SyncFolderCreate:\n            case PushType.SyncFolderDelete:\n                var folderNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<SyncFolderPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (folderNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients.User(folderNotification.Payload.UserId.ToString())\n                    .SendAsync(_receiveMessageMethod, folderNotification, cancellationToken);\n                break;\n            case PushType.SyncCiphers:\n            case PushType.SyncVault:\n            case PushType.SyncOrganizations:\n            case PushType.SyncOrgKeys:\n            case PushType.SyncSettings:\n            case PushType.LogOut:\n                var userNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<LogOutPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (userNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients.User(userNotification.Payload.UserId.ToString())\n                    .SendAsync(_receiveMessageMethod, userNotification, cancellationToken);\n                break;\n            case PushType.SyncSendCreate:\n            case PushType.SyncSendUpdate:\n            case PushType.SyncSendDelete:\n                var sendNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<SyncSendPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (sendNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients.User(sendNotification.Payload.UserId.ToString())\n                    .SendAsync(_receiveMessageMethod, sendNotification, cancellationToken);\n                break;\n            case PushType.AuthRequestResponse:\n                var authRequestResponseNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (authRequestResponseNotification is null)\n                {\n                    break;\n                }\n\n                await _anonymousHubContext.Clients.Group(authRequestResponseNotification.Payload.Id.ToString())\n                    .SendAsync(\"AuthRequestResponseRecieved\", authRequestResponseNotification, cancellationToken);\n                break;\n            case PushType.AuthRequest:\n                var authRequestNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (authRequestNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString())\n                    .SendAsync(_receiveMessageMethod, authRequestNotification, cancellationToken);\n                break;\n            case PushType.SyncOrganizationStatusChanged:\n                var orgStatusNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<OrganizationStatusPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (orgStatusNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients\n                    .Group(NotificationsHub.GetOrganizationGroup(orgStatusNotification.Payload.OrganizationId))\n                    .SendAsync(_receiveMessageMethod, orgStatusNotification, cancellationToken);\n                break;\n            case PushType.SyncOrganizationCollectionSettingChanged:\n                var organizationCollectionSettingsChangedNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<OrganizationStatusPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (organizationCollectionSettingsChangedNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients\n                    .Group(NotificationsHub.GetOrganizationGroup(organizationCollectionSettingsChangedNotification\n                        .Payload.OrganizationId))\n                    .SendAsync(_receiveMessageMethod, organizationCollectionSettingsChangedNotification,\n                        cancellationToken);\n                break;\n            case PushType.OrganizationBankAccountVerified:\n                var organizationBankAccountVerifiedNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<OrganizationBankAccountVerifiedPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (organizationBankAccountVerifiedNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationBankAccountVerifiedNotification.Payload.OrganizationId))\n                    .SendAsync(_receiveMessageMethod, organizationBankAccountVerifiedNotification, cancellationToken);\n                break;\n            case PushType.ProviderBankAccountVerified:\n                var providerBankAccountVerifiedNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<ProviderBankAccountVerifiedPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (providerBankAccountVerifiedNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients.User(providerBankAccountVerifiedNotification.Payload.AdminId.ToString())\n                    .SendAsync(_receiveMessageMethod, providerBankAccountVerifiedNotification, cancellationToken);\n                break;\n            case PushType.Notification:\n            case PushType.NotificationStatus:\n                var notificationData = JsonSerializer.Deserialize<PushNotificationData<NotificationPushNotification>>(\n                    notificationJson, _deserializerOptions);\n                if (notificationData is null)\n                {\n                    break;\n                }\n\n                if (notificationData.Payload.InstallationId.HasValue)\n                {\n                    await _hubContext.Clients.Group(NotificationsHub.GetInstallationGroup(\n                            notificationData.Payload.InstallationId.Value, notificationData.Payload.ClientType))\n                        .SendAsync(_receiveMessageMethod, notificationData, cancellationToken);\n                }\n                else if (notificationData.Payload.UserId.HasValue)\n                {\n                    if (notificationData.Payload.ClientType == ClientType.All)\n                    {\n                        await _hubContext.Clients.User(notificationData.Payload.UserId.Value.ToString())\n                            .SendAsync(_receiveMessageMethod, notificationData, cancellationToken);\n                    }\n                    else\n                    {\n                        await _hubContext.Clients.Group(NotificationsHub.GetUserGroup(\n                                notificationData.Payload.UserId.Value, notificationData.Payload.ClientType))\n                            .SendAsync(_receiveMessageMethod, notificationData, cancellationToken);\n                    }\n                }\n                else if (notificationData.Payload.OrganizationId.HasValue)\n                {\n                    await _hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(\n                            notificationData.Payload.OrganizationId.Value, notificationData.Payload.ClientType))\n                        .SendAsync(_receiveMessageMethod, notificationData, cancellationToken);\n                }\n\n                break;\n            case PushType.RefreshSecurityTasks:\n                var pendingTasksData =\n                    JsonSerializer.Deserialize<PushNotificationData<UserPushNotification>>(notificationJson,\n                        _deserializerOptions);\n                if (pendingTasksData is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString())\n                    .SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken);\n                break;\n            case PushType.PolicyChanged:\n                await policyChangedNotificationHandler(notificationJson, cancellationToken);\n                break;\n            case PushType.AutoConfirm:\n                var autoConfirmNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<AutoConfirmPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (autoConfirmNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients.User(autoConfirmNotification.Payload.UserId.ToString())\n                    .SendAsync(_receiveMessageMethod, autoConfirmNotification, cancellationToken);\n                break;\n            case PushType.PremiumStatusChanged:\n                var premiumStatusNotification =\n                    JsonSerializer.Deserialize<PushNotificationData<PremiumStatusPushNotification>>(\n                        notificationJson, _deserializerOptions);\n                if (premiumStatusNotification is null)\n                {\n                    break;\n                }\n\n                await _hubContext.Clients.User(premiumStatusNotification.Payload.UserId.ToString())\n                    .SendAsync(_receiveMessageMethod, premiumStatusNotification, cancellationToken);\n                break;\n            default:\n                _logger.LogWarning(\"Notification type '{NotificationType}' has not been registered in HubHelpers and will not be pushed as as result\", notification.Type);\n                break;\n        }\n    }\n\n    private async Task policyChangedNotificationHandler(string notificationJson, CancellationToken cancellationToken)\n    {\n        var policyData = JsonSerializer.Deserialize<PushNotificationData<SyncPolicyPushNotification>>(notificationJson, _deserializerOptions);\n        if (policyData is null)\n        {\n            return;\n        }\n\n        await _hubContext.Clients\n            .Group(NotificationsHub.GetOrganizationGroup(policyData.Payload.OrganizationId))\n            .SendAsync(_receiveMessageMethod, policyData, cancellationToken);\n\n    }\n}\n"
  },
  {
    "path": "src/Notifications/INotificationHub.cs",
    "content": "﻿namespace Bit.Notifications;\n\npublic interface INotificationHub\n{\n    Task OnConnectedAsync();\n    Task OnDisconnectedAsync(Exception exception);\n}\n"
  },
  {
    "path": "src/Notifications/Jobs/JobsHostedService.cs",
    "content": "﻿using Bit.Core.Jobs;\nusing Bit.Core.Settings;\nusing Quartz;\n\nnamespace Bit.Notifications.Jobs;\n\npublic class JobsHostedService : BaseJobsHostedService\n{\n    public JobsHostedService(\n        GlobalSettings globalSettings,\n        IServiceProvider serviceProvider,\n        ILogger<JobsHostedService> logger,\n        ILogger<JobListener> listenerLogger)\n        : base(globalSettings, serviceProvider, logger, listenerLogger) { }\n\n    public override async Task StartAsync(CancellationToken cancellationToken)\n    {\n        var everyFiveMinutesTrigger = TriggerBuilder.Create()\n            .WithIdentity(\"EveryFiveMinutesTrigger\")\n            .StartNow()\n            .WithCronSchedule(\"0 */30 * * * ?\")\n            .Build();\n\n        Jobs = new List<Tuple<Type, ITrigger>>\n        {\n            new Tuple<Type, ITrigger>(typeof(LogConnectionCounterJob), everyFiveMinutesTrigger)\n        };\n\n        await base.StartAsync(cancellationToken);\n    }\n\n    public static void AddJobsServices(IServiceCollection services)\n    {\n        services.AddTransient<LogConnectionCounterJob>();\n    }\n}\n"
  },
  {
    "path": "src/Notifications/Jobs/LogConnectionCounterJob.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Jobs;\nusing Quartz;\n\nnamespace Bit.Notifications.Jobs;\n\npublic class LogConnectionCounterJob : BaseJob\n{\n    private readonly ConnectionCounter _connectionCounter;\n\n    public LogConnectionCounterJob(\n        ILogger<LogConnectionCounterJob> logger,\n        ConnectionCounter connectionCounter)\n        : base(logger)\n    {\n        _connectionCounter = connectionCounter;\n    }\n\n    protected override Task ExecuteJobAsync(IJobExecutionContext context)\n    {\n        _logger.LogInformation(Constants.BypassFiltersEventId,\n            \"Connection count for server {0}: {1}\", Environment.MachineName, _connectionCounter.GetCount());\n        return Task.FromResult(0);\n    }\n}\n"
  },
  {
    "path": "src/Notifications/Notifications.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <Sdk Name=\"Bitwarden.Server.Sdk\" />\n\n  <PropertyGroup>\n    <UserSecretsId>bitwarden-Notifications</UserSecretsId>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Notifications' \" />\n  <PropertyGroup Condition=\" '$(RunConfiguration)' == 'Notifications-SelfHost' \" />\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.AspNetCore.SignalR.Protocols.MessagePack\" Version=\"8.0.8\" />\n    <PackageReference Include=\"Microsoft.AspNetCore.SignalR.StackExchangeRedis\" Version=\"8.0.8\" />\n  </ItemGroup>\n\n  <ItemGroup Label=\"Pinned transitive dependencies\">\n    <PackageReference Include=\"MessagePack\" Version=\"2.5.192\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\SharedWeb\\SharedWeb.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/Notifications/NotificationsHub.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\n\nnamespace Bit.Notifications;\n\n[Authorize(\"Application\")]\npublic class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub\n{\n    private readonly ConnectionCounter _connectionCounter;\n    private readonly GlobalSettings _globalSettings;\n\n    public NotificationsHub(ConnectionCounter connectionCounter, GlobalSettings globalSettings)\n    {\n        _connectionCounter = connectionCounter;\n        _globalSettings = globalSettings;\n    }\n\n    public override async Task OnConnectedAsync()\n    {\n        var currentContext = new CurrentContext(null, null);\n        await currentContext.BuildAsync(Context.User, _globalSettings);\n\n        var clientType = DeviceTypes.ToClientType(currentContext.DeviceType);\n        if (clientType != ClientType.All && currentContext.UserId.HasValue)\n        {\n            await Groups.AddToGroupAsync(Context.ConnectionId, GetUserGroup(currentContext.UserId.Value, clientType));\n        }\n\n        if (_globalSettings.Installation.Id != Guid.Empty)\n        {\n            await Groups.AddToGroupAsync(Context.ConnectionId, GetInstallationGroup(_globalSettings.Installation.Id));\n            if (clientType != ClientType.All)\n            {\n                await Groups.AddToGroupAsync(Context.ConnectionId,\n                    GetInstallationGroup(_globalSettings.Installation.Id, clientType));\n            }\n        }\n\n        if (currentContext.Organizations != null)\n        {\n            foreach (var org in currentContext.Organizations)\n            {\n                await Groups.AddToGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id));\n                if (clientType != ClientType.All)\n                {\n                    await Groups.AddToGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id, clientType));\n                }\n            }\n        }\n\n        _connectionCounter.Increment();\n        await base.OnConnectedAsync();\n    }\n\n    public override async Task OnDisconnectedAsync(Exception exception)\n    {\n        var currentContext = new CurrentContext(null, null);\n        await currentContext.BuildAsync(Context.User, _globalSettings);\n\n        var clientType = DeviceTypes.ToClientType(currentContext.DeviceType);\n        if (clientType != ClientType.All && currentContext.UserId.HasValue)\n        {\n            await Groups.RemoveFromGroupAsync(Context.ConnectionId,\n                GetUserGroup(currentContext.UserId.Value, clientType));\n        }\n\n        if (_globalSettings.Installation.Id != Guid.Empty)\n        {\n            await Groups.RemoveFromGroupAsync(Context.ConnectionId,\n                GetInstallationGroup(_globalSettings.Installation.Id));\n            if (clientType != ClientType.All)\n            {\n                await Groups.RemoveFromGroupAsync(Context.ConnectionId,\n                    GetInstallationGroup(_globalSettings.Installation.Id, clientType));\n            }\n        }\n\n        if (currentContext.Organizations != null)\n        {\n            foreach (var org in currentContext.Organizations)\n            {\n                await Groups.RemoveFromGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id));\n                if (clientType != ClientType.All)\n                {\n                    await Groups.RemoveFromGroupAsync(Context.ConnectionId, GetOrganizationGroup(org.Id, clientType));\n                }\n            }\n        }\n\n        _connectionCounter.Decrement();\n        await base.OnDisconnectedAsync(exception);\n    }\n\n    public static string GetInstallationGroup(Guid installationId, ClientType? clientType = null)\n    {\n        return clientType is null or ClientType.All\n            ? $\"Installation_{installationId}\"\n            : $\"Installation_ClientType_{installationId}_{clientType}\";\n    }\n\n    public static string GetUserGroup(Guid userId, ClientType clientType)\n    {\n        return $\"UserClientType_{userId}_{clientType}\";\n    }\n\n    public static string GetOrganizationGroup(Guid organizationId, ClientType? clientType = null)\n    {\n        return clientType is null or ClientType.All\n            ? $\"Organization_{organizationId}\"\n            : $\"OrganizationClientType_{organizationId}_{clientType}\";\n    }\n}\n"
  },
  {
    "path": "src/Notifications/Program.cs",
    "content": "﻿using Bit.Core.Utilities;\n\nnamespace Bit.Notifications;\n\npublic class Program\n{\n    public static void Main(string[] args)\n    {\n        Host\n            .CreateDefaultBuilder(args)\n            .UseBitwardenSdk()\n            .ConfigureWebHostDefaults(webBuilder =>\n            {\n                webBuilder.UseStartup<Startup>();\n            })\n            .AddSerilogFileLogging()\n            .Build()\n            .Run();\n    }\n}\n"
  },
  {
    "path": "src/Notifications/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true,\n    \"iisExpress\": {\n      \"applicationUrl\": \"http://localhost:61840\",\n      \"sslPort\": 0\n    }\n  },\n  \"profiles\": {\n    \"IIS Express\": {\n      \"commandName\": \"IISExpress\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Notifications\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:61840\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    },\n    \"Notifications-SelfHost\": {\n      \"commandName\": \"Project\",\n      \"applicationUrl\": \"http://localhost:61841\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\",\n        \"developSelfHosted\": \"true\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Notifications/Startup.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.SharedWeb.Utilities;\nusing Duende.IdentityModel;\nusing Microsoft.AspNetCore.SignalR;\n\nnamespace Bit.Notifications;\n\npublic class Startup\n{\n    public Startup(IWebHostEnvironment env, IConfiguration configuration)\n    {\n        CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(\"en-US\");\n        Configuration = configuration;\n        Environment = env;\n    }\n\n    public IConfiguration Configuration { get; }\n    public IWebHostEnvironment Environment { get; set; }\n\n    public void ConfigureServices(IServiceCollection services)\n    {\n        // Options\n        services.AddOptions();\n\n        // Settings\n        var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);\n\n        // Identity\n        services.AddIdentityAuthenticationServices(globalSettings, Environment, config =>\n        {\n            config.AddPolicy(\"Application\", policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, \"Application\", \"external\");\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);\n            });\n            config.AddPolicy(\"Internal\", policy =>\n            {\n                policy.RequireAuthenticatedUser();\n                policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Internal);\n            });\n        });\n\n        // SignalR\n        var signalRServerBuilder = services.AddSignalR().AddMessagePackProtocol(options =>\n        {\n            options.SerializerOptions = MessagePack.MessagePackSerializerOptions.Standard\n                .WithResolver(MessagePack.Resolvers.ContractlessStandardResolver.Instance);\n        });\n        if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.RedisConnectionString))\n        {\n            signalRServerBuilder.AddStackExchangeRedis(globalSettings.Notifications.RedisConnectionString,\n                options =>\n                {\n                    options.Configuration.ChannelPrefix = \"Notifications\";\n                });\n        }\n        services.AddSingleton<IUserIdProvider, SubjectUserIdProvider>();\n        services.AddSingleton<ConnectionCounter>();\n        services.AddSingleton<HubHelpers>();\n\n        // Mvc\n        services.AddMvc();\n\n        services.AddHostedService<HeartbeatHostedService>();\n        if (!globalSettings.SelfHosted)\n        {\n            // Hosted Services\n            Jobs.JobsHostedService.AddJobsServices(services);\n            services.AddHostedService<Jobs.JobsHostedService>();\n            if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))\n            {\n                services.AddHostedService<AzureQueueHostedService>();\n            }\n        }\n    }\n\n    public void Configure(\n        IApplicationBuilder app,\n        IWebHostEnvironment env,\n        GlobalSettings globalSettings)\n    {\n        // Add general security headers\n        app.UseMiddleware<SecurityHeadersMiddleware>();\n\n        // Forwarding Headers\n        if (globalSettings.SelfHosted)\n        {\n            app.UseForwardedHeaders(globalSettings);\n        }\n\n        if (env.IsDevelopment())\n        {\n            app.UseDeveloperExceptionPage();\n        }\n\n        // Add routing\n        app.UseRouting();\n\n        // Add Cors\n        app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))\n            .AllowAnyMethod().AllowAnyHeader().AllowCredentials());\n\n        // Add authentication to the request pipeline.\n        app.UseAuthentication();\n        app.UseAuthorization();\n\n        // Add endpoints to the request pipeline.\n        app.UseEndpoints(endpoints =>\n        {\n            endpoints.MapHub<NotificationsHub>(\"/hub\", options =>\n            {\n                options.ApplicationMaxBufferSize = 2048;\n                options.TransportMaxBufferSize = 4096;\n            });\n            endpoints.MapHub<AnonymousNotificationsHub>(\"/anonymous-hub\", options =>\n            {\n                options.ApplicationMaxBufferSize = 2048;\n                options.TransportMaxBufferSize = 4096;\n            });\n            endpoints.MapDefaultControllerRoute();\n        });\n    }\n}\n"
  },
  {
    "path": "src/Notifications/SubjectUserIdProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Duende.IdentityModel;\nusing Microsoft.AspNetCore.SignalR;\n\nnamespace Bit.Notifications;\n\npublic class SubjectUserIdProvider : IUserIdProvider\n{\n    public string GetUserId(HubConnectionContext connection)\n    {\n        return connection.User?.FindFirst(JwtClaimTypes.Subject)?.Value;\n    }\n}\n"
  },
  {
    "path": "src/Notifications/appsettings.Development.json",
    "content": "{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://localhost:8080\",\n      \"api\": \"http://localhost:4000\",\n      \"identity\": \"http://localhost:33656\",\n      \"admin\": \"http://localhost:62911\",\n      \"notifications\": \"http://localhost:61840\",\n      \"sso\": \"http://localhost:51822\",\n      \"internalNotifications\": \"http://localhost:61840\",\n      \"internalAdmin\": \"http://localhost:62911\",\n      \"internalIdentity\": \"http://localhost:33656\",\n      \"internalApi\": \"http://localhost:4000\",\n      \"internalVault\": \"https://localhost:8080\",\n      \"internalSso\": \"http://localhost:51822\",\n      \"internalScim\": \"http://localhost:44559\"\n    },\n    \"notifications\": {\n      \"connectionString\": \"UseDevelopmentStorage=true\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Notifications/appsettings.Production.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.bitwarden.com\",\n      \"api\": \"https://api.bitwarden.com\",\n      \"identity\": \"https://identity.bitwarden.com\",\n      \"admin\": \"https://admin.bitwarden.com\",\n      \"notifications\": \"https://notifications.bitwarden.com\",\n      \"sso\": \"https://sso.bitwarden.com\",\n      \"internalNotifications\": \"https://notifications.bitwarden.com\",\n      \"internalAdmin\": \"https://admin.bitwarden.com\",\n      \"internalIdentity\": \"https://identity.bitwarden.com\",\n      \"internalApi\": \"https://api.bitwarden.com\",\n      \"internalVault\": \"https://vault.bitwarden.com\",\n      \"internalSso\": \"https://sso.bitwarden.com\",\n      \"internalScim\": \"https://scim.bitwarden.com\"\n    }\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft\": \"Warning\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Warning\",\n          \"System\": \"Warning\",\n          \"Microsoft\": \"Warning\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Notifications/appsettings.QA.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": \"https://vault.qa.bitwarden.pw\",\n      \"api\": \"https://api.qa.bitwarden.pw\",\n      \"identity\": \"https://identity.qa.bitwarden.pw\",\n      \"admin\": \"https://admin.qa.bitwarden.pw\",\n      \"notifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"sso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalNotifications\": \"https://notifications.qa.bitwarden.pw\",\n      \"internalAdmin\": \"https://admin.qa.bitwarden.pw\",\n      \"internalIdentity\": \"https://identity.qa.bitwarden.pw\",\n      \"internalApi\": \"https://api.qa.bitwarden.pw\",\n      \"internalVault\": \"https://vault.qa.bitwarden.pw\",\n      \"internalSso\": \"https://sso.qa.bitwarden.pw\",\n      \"internalScim\": \"https://scim.qa.bitwarden.pw\"\n    }\n  },\n  \"Logging\": {\n    \"IncludeScopes\": false,\n    \"LogLevel\": {\n      \"Default\": \"Debug\",\n      \"System\": \"Information\",\n      \"Microsoft\": \"Information\"\n    },\n    \"Console\": {\n      \"IncludeScopes\": true,\n      \"LogLevel\": {\n          \"Default\": \"Debug\",\n          \"System\": \"Debug\",\n          \"Microsoft\": \"Debug\",\n          \"Microsoft.Hosting.Lifetime\": \"Information\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/Notifications/appsettings.SelfHosted.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"baseServiceUri\": {\n      \"vault\": null,\n      \"api\": null,\n      \"identity\": null,\n      \"admin\": null,\n      \"notifications\": null,\n      \"sso\": null,\n      \"internalNotifications\": null,\n      \"internalAdmin\": null,\n      \"internalIdentity\": null,\n      \"internalApi\": null,\n      \"internalVault\": null,\n      \"internalSso\": null,\n      \"internalScim\": null\n    }\n  }\n}\n"
  },
  {
    "path": "src/Notifications/appsettings.json",
    "content": "﻿{\n  \"globalSettings\": {\n    \"selfHosted\": false,\n    \"projectName\": \"Notifications\",\n    \"sqlServer\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"identityServer\": {\n      \"certificateThumbprint\": \"SECRET\"\n    },\n    \"storage\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"events\": {\n      \"connectionString\": \"SECRET\"\n    },\n    \"serviceBus\": {\n      \"connectionString\": \"SECRET\",\n      \"applicationCacheTopicName\": \"SECRET\"\n    },\n    \"amazon\": {\n      \"accessKeyId\": \"SECRET\",\n      \"accessKeySecret\": \"SECRET\",\n      \"region\": \"SECRET\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/Notifications/build.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\necho -e \"\\n## Building Notifications\"\n\necho -e \"\\nBuilding app\"\necho \".NET Core version $(dotnet --version)\"\necho \"Restore\"\ndotnet restore \"$DIR/Notifications.csproj\"\necho \"Clean\"\ndotnet clean \"$DIR/Notifications.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\necho \"Publish\"\ndotnet publish \"$DIR/Notifications.csproj\" -c \"Release\" -o \"$DIR/obj/build-output/publish\"\n"
  },
  {
    "path": "src/Notifications/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Setup\n\nGROUPNAME=\"bitwarden\"\nUSERNAME=\"bitwarden\"\n\nLUID=${LOCAL_UID:-0}\nLGID=${LOCAL_GID:-0}\n\n# Step down from host root to well-known nobody/nogroup user\n\nif [ $LUID -eq 0 ]\nthen\n    LUID=65534\nfi\nif [ $LGID -eq 0 ]\nthen\n    LGID=65534\nfi\n\nif [ \"$(id -u)\" = \"0\" ]\nthen\n    # Create user and group\n\n    groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||\n    groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1\n    useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||\n    usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1\n    mkhomedir_helper $USERNAME\n\n    # The rest...\n\n    chown -R $USERNAME:$GROUPNAME /app\n    mkdir -p /etc/bitwarden/core\n    mkdir -p /etc/bitwarden/logs\n    mkdir -p /etc/bitwarden/ca-certificates\n    chown -R $USERNAME:$GROUPNAME /etc/bitwarden\n\n    gosu_cmd=\"gosu $USERNAME:$GROUPNAME\"\nelse\n    gosu_cmd=\"\"\nfi\n\nexec $gosu_cmd /app/Notifications\n"
  },
  {
    "path": "src/SharedWeb/Health/HealthCheckServiceExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text;\nusing System.Text.Json;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Diagnostics.HealthChecks;\n\nnamespace Bit.SharedWeb.Health;\n\npublic static class HealthCheckServiceExtensions\n{\n    public static void AddHealthCheckServices(this IServiceCollection services, GlobalSettings globalSettings,\n        Action<IHealthChecksBuilder> addBuilder = null)\n    {\n        var builder = services.AddHealthChecks();\n        addBuilder?.Invoke(builder);\n    }\n\n    public static Task WriteResponse(HttpContext context, HealthReport healthReport)\n    {\n        context.Response.ContentType = \"application/json; charset=utf-8\";\n\n        var options = new JsonWriterOptions { Indented = true };\n\n        using var memoryStream = new MemoryStream();\n        using (var jsonWriter = new Utf8JsonWriter(memoryStream, options))\n        {\n            jsonWriter.WriteStartObject();\n            jsonWriter.WriteString(\"status\", healthReport.Status.ToString());\n            jsonWriter.WriteStartObject(\"results\");\n\n            foreach (var healthReportEntry in healthReport.Entries)\n            {\n                jsonWriter.WriteStartObject(healthReportEntry.Key);\n                jsonWriter.WriteString(\"status\",\n                    healthReportEntry.Value.Status.ToString());\n                jsonWriter.WriteString(\"description\",\n                    healthReportEntry.Value.Description ?? healthReportEntry.Value.Exception?.Message);\n                jsonWriter.WriteStartObject(\"data\");\n\n                foreach (var item in healthReportEntry.Value.Data)\n                {\n                    jsonWriter.WritePropertyName(item.Key);\n\n                    JsonSerializer.Serialize(jsonWriter, item.Value,\n                        item.Value?.GetType() ?? typeof(object));\n                }\n\n                jsonWriter.WriteEndObject();\n                jsonWriter.WriteEndObject();\n            }\n\n            jsonWriter.WriteEndObject();\n            jsonWriter.WriteEndObject();\n        }\n\n        return context.Response.WriteAsync(\n            Encoding.UTF8.GetString(memoryStream.ToArray()));\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Play/PlayServiceCollectionExtensions.cs",
    "content": "﻿using Bit.Core.Repositories;\nusing Bit.SharedWeb.Play.Repositories;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.SharedWeb.Play;\n\npublic static class PlayServiceCollectionExtensions\n{\n    /// <summary>\n    /// Adds PlayId tracking decorators for User and Organization repositories using Dapper implementations.\n    /// This replaces the standard repository implementations with tracking versions\n    /// that record created entities for test data cleanup. Only call when TestPlayIdTrackingEnabled is true.\n    /// </summary>\n    public static void AddPlayIdTrackingDapperRepositories(this IServiceCollection services)\n    {\n        services.AddSingleton<IOrganizationRepository, DapperTestOrganizationTrackingOrganizationRepository>();\n        services.AddSingleton<IUserRepository, DapperTestUserTrackingUserRepository>();\n    }\n\n    /// <summary>\n    /// Adds PlayId tracking decorators for User and Organization repositories using EntityFramework implementations.\n    /// This replaces the standard repository implementations with tracking versions\n    /// that record created entities for test data cleanup. Only call when TestPlayIdTrackingEnabled is true.\n    /// </summary>\n    public static void AddPlayIdTrackingEFRepositories(this IServiceCollection services)\n    {\n        services.AddSingleton<IOrganizationRepository, EFTestOrganizationTrackingOrganizationRepository>();\n        services.AddSingleton<IUserRepository, EFTestUserTrackingUserRepository>();\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Play/Repositories/DapperTestOrganizationTrackingOrganizationRepository.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.SharedWeb.Play.Repositories;\n\n/// <summary>\n/// Dapper decorator around the <see cref=\"Bit.Infrastructure.Dapper.Repositories.OrganizationRepository\"/> that tracks\n/// created Organizations for seeding.\n/// </summary>\npublic class DapperTestOrganizationTrackingOrganizationRepository : OrganizationRepository\n{\n    private readonly IPlayItemService _playItemService;\n\n    public DapperTestOrganizationTrackingOrganizationRepository(\n        IPlayItemService playItemService,\n        GlobalSettings globalSettings,\n        ILogger<OrganizationRepository> logger)\n        : base(globalSettings, logger)\n    {\n        _playItemService = playItemService;\n    }\n\n    public override async Task<Organization> CreateAsync(Organization obj)\n    {\n        var createdOrganization = await base.CreateAsync(obj);\n        await _playItemService.Record(createdOrganization);\n        return createdOrganization;\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Play/Repositories/DapperTestUserTrackingUserRepository.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Infrastructure.Dapper.Repositories;\nusing Microsoft.AspNetCore.DataProtection;\n\nnamespace Bit.SharedWeb.Play.Repositories;\n\n/// <summary>\n/// Dapper decorator around the <see cref=\"Bit.Infrastructure.Dapper.Repositories.UserRepository\"/> that tracks\n/// created Users for seeding.\n/// </summary>\npublic class DapperTestUserTrackingUserRepository : UserRepository\n{\n    private readonly IPlayItemService _playItemService;\n\n    public DapperTestUserTrackingUserRepository(\n        IPlayItemService playItemService,\n        GlobalSettings globalSettings,\n        IDataProtectionProvider dataProtectionProvider)\n        : base(globalSettings, dataProtectionProvider)\n    {\n        _playItemService = playItemService;\n    }\n\n    public override async Task<User> CreateAsync(User user)\n    {\n        var createdUser = await base.CreateAsync(user);\n\n        await _playItemService.Record(createdUser);\n        return createdUser;\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Play/Repositories/EFTestOrganizationTrackingOrganizationRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Services;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\n\nnamespace Bit.SharedWeb.Play.Repositories;\n\n/// <summary>\n/// EntityFramework decorator around the <see cref=\"Bit.Infrastructure.EntityFramework.Repositories.OrganizationRepository\"/> that tracks\n/// created Organizations for seeding.\n/// </summary>\npublic class EFTestOrganizationTrackingOrganizationRepository : OrganizationRepository\n{\n    private readonly IPlayItemService _playItemService;\n\n    public EFTestOrganizationTrackingOrganizationRepository(\n        IPlayItemService playItemService,\n        IServiceScopeFactory serviceScopeFactory,\n        IMapper mapper,\n        ILogger<OrganizationRepository> logger)\n        : base(serviceScopeFactory, mapper, logger)\n    {\n        _playItemService = playItemService;\n    }\n\n    public override async Task<Core.AdminConsole.Entities.Organization> CreateAsync(Core.AdminConsole.Entities.Organization organization)\n    {\n        var createdOrganization = await base.CreateAsync(organization);\n        await _playItemService.Record(createdOrganization);\n        return createdOrganization;\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Play/Repositories/EFTestUserTrackingUserRepository.cs",
    "content": "﻿using AutoMapper;\nusing Bit.Core.Services;\nusing Bit.Infrastructure.EntityFramework.Repositories;\nusing Microsoft.Extensions.DependencyInjection;\n\nnamespace Bit.SharedWeb.Play.Repositories;\n\n/// <summary>\n/// EntityFramework decorator around the <see cref=\"Bit.Infrastructure.EntityFramework.Repositories.UserRepository\"/> that tracks\n/// created Users for seeding.\n/// </summary>\npublic class EFTestUserTrackingUserRepository : UserRepository\n{\n    private readonly IPlayItemService _playItemService;\n\n    public EFTestUserTrackingUserRepository(\n        IPlayItemService playItemService,\n        IServiceScopeFactory serviceScopeFactory,\n        IMapper mapper)\n        : base(serviceScopeFactory, mapper)\n    {\n        _playItemService = playItemService;\n    }\n\n    public override async Task<Core.Entities.User> CreateAsync(Core.Entities.User user)\n    {\n        var createdUser = await base.CreateAsync(user);\n        await _playItemService.Record(createdUser);\n        return createdUser;\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/SharedWeb.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\Infrastructure.Dapper\\Infrastructure.Dapper.csproj\" />\n    <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\Infrastructure.EntityFramework\\Infrastructure.EntityFramework.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Bot.Builder.Integration.AspNet.Core\" Version=\"4.23.0\" />\n    <PackageReference Include=\"Swashbuckle.AspNetCore.SwaggerGen\" Version=\"10.1.0\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/SharedWeb/Swagger/ActionNameOperationFilter.cs",
    "content": "﻿using System.Text.Json;\nusing Microsoft.OpenApi;\nusing Swashbuckle.AspNetCore.SwaggerGen;\n\nnamespace Bit.SharedWeb.Swagger;\n\n/// <summary>\n/// Adds the action name (function name) as an extension to each operation in the Swagger document.\n/// This can be useful for the code generation process, to generate more meaningful names for operations.\n/// Note that we add both the original action name and a snake_case version, as the codegen templates\n/// cannot do case conversions.\n/// </summary>\npublic class ActionNameOperationFilter : IOperationFilter\n{\n    public void Apply(OpenApiOperation operation, OperationFilterContext context)\n    {\n        if (!context.ApiDescription.ActionDescriptor.RouteValues.TryGetValue(\"action\", out var action)) return;\n        if (string.IsNullOrEmpty(action)) return;\n\n        operation.Extensions ??= new Dictionary<string, IOpenApiExtension>();\n        operation.Extensions.Add(\"x-action-name\", new JsonNodeExtension(action));\n        // We can't do case changes in the codegen templates, so we also add the snake_case version of the action name\n        operation.Extensions.Add(\"x-action-name-snake-case\", new JsonNodeExtension(JsonNamingPolicy.SnakeCaseLower.ConvertName(action)));\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Swagger/Base64UrlSchemaFilter.cs",
    "content": "﻿using Fido2NetLib;\nusing Fido2NetLib.Objects;\nusing Microsoft.OpenApi;\nusing Swashbuckle.AspNetCore.SwaggerGen;\n\nnamespace Bit.SharedWeb.Swagger;\n\n/// <summary>\n/// Adds <c>x-base64url</c> extension to fields known to be serialized as base64url for code generation.\n/// </summary>\npublic class Base64UrlSchemaFilter : ISchemaFilter\n{\n    public void Apply(IOpenApiSchema schema, SchemaFilterContext context)\n    {\n        if (context.Type == typeof(AssertionOptions))\n        {\n            MarkPropertyAsBase64Url(schema, \"challenge\");\n        }\n        else if (context.Type == typeof(CredentialCreateOptions))\n        {\n            MarkPropertyAsBase64Url(schema, \"challenge\");\n        }\n        else if (context.Type == typeof(Fido2User))\n        {\n            MarkPropertyAsBase64Url(schema, \"id\");\n        }\n        else if (context.Type == typeof(PublicKeyCredentialDescriptor))\n        {\n            MarkPropertyAsBase64Url(schema, \"id\");\n        }\n    }\n\n    private static void MarkPropertyAsBase64Url(IOpenApiSchema schema, string prop)\n    {\n        if (schema is not OpenApiSchema openApiSchema)\n        {\n            return;\n        }\n        openApiSchema.Properties ??= new Dictionary<string, IOpenApiSchema>();\n        openApiSchema.Properties.TryAdd(prop, new OpenApiSchema());\n        if (openApiSchema.Properties[prop] is OpenApiSchema propSchema)\n        {\n            propSchema.Extensions ??= new Dictionary<string, IOpenApiExtension>();\n            propSchema.Extensions.Add(\"x-base64url\", new JsonNodeExtension(true));\n        }\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Swagger/CheckDuplicateOperationIdsDocumentFilter.cs",
    "content": "﻿using Microsoft.OpenApi;\nusing Swashbuckle.AspNetCore.SwaggerGen;\n\nnamespace Bit.SharedWeb.Swagger;\n\n/// <summary>\n/// Checks for duplicate operation IDs in the Swagger document, and throws an error if any are found.\n/// Operation IDs must be unique across the entire Swagger document according to the OpenAPI specification,\n/// but we use controller action names to generate them, which can lead to duplicates if a Controller function\n/// has multiple HTTP methods or if a Controller has overloaded functions.\n/// </summary>\npublic class CheckDuplicateOperationIdsDocumentFilter(bool printDuplicates = true) : IDocumentFilter\n{\n    public bool PrintDuplicates { get; } = printDuplicates;\n\n    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)\n    {\n        var operationIdMap = new Dictionary<string, List<(string Path, IOpenApiPathItem PathItem, HttpMethod Method, OpenApiOperation Operation)>>();\n\n        foreach (var (path, pathItem) in swaggerDoc.Paths)\n        {\n            if (pathItem.Operations is null) continue;\n\n            foreach (var (method, operation) in pathItem.Operations)\n            {\n                var operationId = operation.OperationId ?? string.Empty;\n                if (!operationIdMap.TryGetValue(operationId, out var list))\n                {\n                    list = [];\n                    operationIdMap[operationId] = list;\n                }\n\n                list.Add((path, pathItem, method, operation));\n\n            }\n        }\n\n        // Find duplicates\n        var duplicates = operationIdMap.Where((kvp) => kvp.Value.Count > 1).ToList();\n        if (duplicates.Count > 0)\n        {\n            if (PrintDuplicates)\n            {\n                Console.WriteLine($\"\\n######## Duplicate operationIds found in the schema ({duplicates.Count} found) ########\\n\");\n\n                Console.WriteLine(\"## Common causes of duplicate operation IDs:\");\n                Console.WriteLine(\"- Multiple HTTP methods (GET, POST, etc.) on the same controller function\");\n                Console.WriteLine(\"    Solution: Split the methods into separate functions, and if appropiate, mark the deprecated ones with [Obsolete]\");\n                Console.WriteLine();\n                Console.WriteLine(\"- Overloaded controller functions with the same name\");\n                Console.WriteLine(\"    Solution: Rename the overloaded functions to have unique names, or combine them into a single function with optional parameters\");\n                Console.WriteLine();\n\n                Console.WriteLine(\"## The duplicate operation IDs are:\");\n\n                foreach (var (operationId, duplicate) in duplicates)\n                {\n                    Console.WriteLine($\"- operationId: {operationId}\");\n                    foreach (var (path, pathItem, method, operation) in duplicate)\n                    {\n                        Console.Write($\"    {method.ToString().ToUpper()} {path}\");\n\n                        if (operation.Extensions is null) continue;\n\n                        if (operation.Extensions.TryGetValue(\"x-source-file\", out var sourceFile)\n                            && operation.Extensions.TryGetValue(\"x-source-line\", out var sourceLine)\n                            && sourceFile is JsonNodeExtension sourceFileNodeExt\n                            && sourceLine is JsonNodeExtension sourceLineNodeExt)\n                        {\n                            var sourceFileString = sourceFileNodeExt.Node.ToString();\n                            var sourceLineString = sourceLineNodeExt.Node.ToString();\n\n                            Console.WriteLine($\"    {sourceFileString}:{sourceLineString}\");\n                        }\n                        else\n                        {\n                            Console.WriteLine();\n                        }\n                    }\n                    Console.WriteLine(\"\\n\");\n                }\n            }\n\n            throw new InvalidOperationException($\"Duplicate operation IDs found in Swagger schema\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Swagger/EncryptedStringSchemaFilter.cs",
    "content": "﻿#nullable enable\n\nusing System.Text.Json;\nusing Bit.Core.Utilities;\nusing Microsoft.OpenApi;\nusing Swashbuckle.AspNetCore.SwaggerGen;\n\nnamespace Bit.SharedWeb.Swagger;\n\n/// <summary>\n///  Set the format of any strings that are decorated with the <see cref=\"EncryptedStringAttribute\"/> to \"x-enc-string\".\n///  This will allow the generated bindings to use a more appropriate type for encrypted strings.\n/// </summary>\npublic class EncryptedStringSchemaFilter : ISchemaFilter\n{\n    public void Apply(IOpenApiSchema schema, SchemaFilterContext context)\n    {\n        if (context.Type == null || schema.Properties == null)\n            return;\n\n        foreach (var prop in context.Type.GetProperties())\n        {\n            // Only apply to string properties\n            if (prop.PropertyType != typeof(string))\n                continue;\n\n            // Check if the property has the EncryptedString attribute\n            if (prop.GetCustomAttributes(typeof(EncryptedStringAttribute), true).FirstOrDefault() != null)\n            {\n                // Convert prop.Name to camelCase for JSON schema property lookup\n                var jsonPropName = JsonNamingPolicy.CamelCase.ConvertName(prop.Name);\n\n                if (schema.Properties.TryGetValue(jsonPropName, out var value) && value is OpenApiSchema innerSchema)\n                {\n                    innerSchema.Format = \"x-enc-string\";\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Swagger/EnumSchemaFilter.cs",
    "content": "﻿using System.Text.Json.Nodes;\nusing Microsoft.OpenApi;\nusing Swashbuckle.AspNetCore.SwaggerGen;\n\nnamespace Bit.SharedWeb.Swagger;\n\n/// <summary>\n/// Adds x-enum-varnames containing the name of enums. Useful for code generation.\n///</summary>\n/// <remarks>\n/// Ideally we would use `oneOf` instead but it's not currently handled well by our swagger generator.\n///\n/// Credits: https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/1287#issuecomment-655164215\n/// </remarks>\npublic class EnumSchemaFilter : ISchemaFilter\n{\n    public void Apply(IOpenApiSchema schema, SchemaFilterContext context)\n    {\n        if (context.Type.IsEnum && schema is OpenApiSchema openApiSchema)\n        {\n            var array = new JsonArray();\n            foreach (var name in Enum.GetNames(context.Type)) array.Add(name);\n\n            openApiSchema.Extensions ??= new Dictionary<string, IOpenApiExtension>();\n            openApiSchema.Extensions.Add(\"x-enum-varnames\", new JsonNodeExtension(array));\n        }\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Swagger/GitCommitDocumentFilter.cs",
    "content": "﻿#nullable enable\n\nusing System.Diagnostics;\nusing Microsoft.OpenApi;\nusing Swashbuckle.AspNetCore.SwaggerGen;\n\nnamespace Bit.SharedWeb.Swagger;\n\n/// <summary>\n/// Add the Git commit that was used to generate the Swagger document, to help with debugging and reproducibility.\n/// </summary>\npublic class GitCommitDocumentFilter : IDocumentFilter\n{\n\n    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)\n    {\n        if (!string.IsNullOrEmpty(GitCommit))\n        {\n            swaggerDoc.Extensions ??= new Dictionary<string, IOpenApiExtension>();\n            swaggerDoc.Extensions.Add(\"x-git-commit\", new JsonNodeExtension(GitCommit));\n        }\n    }\n\n    public static string? GitCommit => _gitCommit.Value;\n\n    private static readonly Lazy<string?> _gitCommit = new(() =>\n    {\n        try\n        {\n            var process = new Process\n            {\n                StartInfo = new ProcessStartInfo\n                {\n                    FileName = \"git\",\n                    Arguments = \"rev-parse HEAD\",\n                    RedirectStandardOutput = true,\n                    UseShellExecute = false,\n                    CreateNoWindow = true\n                }\n            };\n            process.Start();\n            var result = process.StandardOutput.ReadLine()?.Trim();\n            process.WaitForExit();\n            return result ?? string.Empty;\n        }\n        catch\n        {\n            return null;\n        }\n    });\n}\n"
  },
  {
    "path": "src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs",
    "content": "﻿#nullable enable\n\nusing System.Reflection;\nusing System.Reflection.Metadata;\nusing System.Reflection.Metadata.Ecma335;\nusing System.Runtime.CompilerServices;\nusing Microsoft.OpenApi;\nusing Swashbuckle.AspNetCore.SwaggerGen;\n\nnamespace Bit.SharedWeb.Swagger;\n\n/// <summary>\n/// Adds source file and line number information to the Swagger operation description.\n/// This can be useful for locating the source code of the operation in the repository,\n/// as the generated names are based on the HTTP path, and are hard to search for.\n/// </summary>\npublic class SourceFileLineOperationFilter : IOperationFilter\n{\n    public void Apply(OpenApiOperation operation, OperationFilterContext context)\n    {\n\n        var (fileName, lineNumber) = GetSourceFileLine(context.MethodInfo);\n        if (fileName != null && lineNumber > 0)\n        {\n            // Also add the information as extensions, so other tools can use it in the future\n            operation.Extensions ??= new Dictionary<string, IOpenApiExtension>();\n            operation.Extensions.Add(\"x-source-file\", new JsonNodeExtension(fileName));\n            operation.Extensions.Add(\"x-source-line\", new JsonNodeExtension(lineNumber));\n        }\n    }\n\n    private static (string? fileName, int lineNumber) GetSourceFileLine(MethodInfo methodInfo)\n    {\n        // Get the location of the PDB file associated with the module of the method\n        var pdbPath = Path.ChangeExtension(methodInfo.Module.FullyQualifiedName, \".pdb\");\n        if (!File.Exists(pdbPath)) return (null, 0);\n\n        // Open the PDB file and read the metadata\n        using var pdbStream = File.OpenRead(pdbPath);\n        using var metadataReaderProvider = MetadataReaderProvider.FromPortablePdbStream(pdbStream);\n        var metadataReader = metadataReaderProvider.GetMetadataReader();\n\n        // If the method is async, the compiler will generate a state machine,\n        // so we can't look for the original method, but we instead need to look\n        // for the MoveNext method of the state machine.\n        var attr = methodInfo.GetCustomAttribute<AsyncStateMachineAttribute>();\n        if (attr?.StateMachineType != null)\n        {\n            var moveNext = attr.StateMachineType.GetMethod(\"MoveNext\", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);\n            if (moveNext != null) methodInfo = moveNext;\n        }\n\n        // Once we have the  method, we can get its sequence points\n        var handle = (MethodDefinitionHandle)MetadataTokens.Handle(methodInfo.MetadataToken);\n        if (handle.IsNil) return (null, 0);\n        var sequencePoints = metadataReader.GetMethodDebugInformation(handle).GetSequencePoints();\n\n        // Iterate through the sequence points and pick the first one that has a valid line number\n        foreach (var sp in sequencePoints)\n        {\n            var docName = metadataReader.GetDocument(sp.Document).Name;\n            if (sp.StartLine != 0 && sp.StartLine != SequencePoint.HiddenLine && !docName.IsNil)\n            {\n                var fileName = metadataReader.GetString(docName);\n                var repoRoot = FindRepoRoot(AppContext.BaseDirectory);\n                var relativeFileName = repoRoot != null ? Path.GetRelativePath(repoRoot, fileName) : fileName;\n                return (relativeFileName, sp.StartLine);\n            }\n        }\n\n        return (null, 0);\n    }\n\n    private static string? FindRepoRoot(string startPath)\n    {\n        var dir = new DirectoryInfo(startPath);\n        while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, \".git\")))\n            dir = dir.Parent;\n        return dir?.FullName;\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs",
    "content": "﻿using Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Swashbuckle.AspNetCore.SwaggerGen;\n\nnamespace Bit.SharedWeb.Swagger;\n\npublic static class SwaggerGenOptionsExt\n{\n\n    public static void InitializeSwaggerFilters(\n    this SwaggerGenOptions config, IWebHostEnvironment environment)\n    {\n        config.SchemaFilter<EnumSchemaFilter>();\n        config.SchemaFilter<EncryptedStringSchemaFilter>();\n        config.SchemaFilter<Base64UrlSchemaFilter>();\n\n        config.OperationFilter<ActionNameOperationFilter>();\n\n        // Set the operation ID to the name of the controller followed by the name of the function.\n        // Note that the \"Controller\" suffix for the controllers, and the \"Async\" suffix for the actions\n        // are removed already, so we don't need to do that ourselves.\n        config.CustomOperationIds(e => $\"{e.ActionDescriptor.RouteValues[\"controller\"]}_{e.ActionDescriptor.RouteValues[\"action\"]}\");\n        // Because we're setting custom operation IDs, we need to ensure that we don't accidentally\n        // introduce duplicate IDs, which is against the OpenAPI specification and could lead to issues.\n        config.DocumentFilter<CheckDuplicateOperationIdsDocumentFilter>();\n\n        // These two filters require debug symbols/git, so only add them in development mode\n        if (environment.IsDevelopment())\n        {\n            config.DocumentFilter<GitCommitDocumentFilter>();\n            config.OperationFilter<SourceFileLineOperationFilter>();\n        }\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Utilities/DisplayAttributeHelpers.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.ComponentModel.DataAnnotations;\nusing System.Reflection;\n\nnamespace Bit.SharedWeb.Utilities;\n\npublic static class DisplayAttributeHelpers\n{\n    public static DisplayAttribute GetDisplayAttribute(this Enum enumValue)\n    {\n        return enumValue.GetType()\n            .GetMember(enumValue.ToString())\n            .First()\n            .GetCustomAttribute<DisplayAttribute>();\n    }\n\n    public static DisplayAttribute GetDisplayAttribute<T>(this string property)\n    {\n        MemberInfo propertyInfo = typeof(T).GetProperty(property);\n        return propertyInfo?.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute;\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Utilities/ExceptionHandlerFilterAttribute.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.IdentityModel.Tokens;\nusing InternalApi = Bit.Core.Models.Api;\n\nnamespace Bit.SharedWeb.Utilities;\n\npublic class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute\n{\n    public ExceptionHandlerFilterAttribute()\n    {\n    }\n\n    public override void OnException(ExceptionContext context)\n    {\n        var errorMessage = \"An error has occurred.\";\n\n        var exception = context.Exception;\n        if (exception == null)\n        {\n            // Should never happen.\n            return;\n        }\n\n        InternalApi.ErrorResponseModel internalErrorModel = null;\n        if (exception is BadRequestException badRequestException)\n        {\n            context.HttpContext.Response.StatusCode = 400;\n            if (badRequestException.ModelState != null)\n            {\n                internalErrorModel = new InternalApi.ErrorResponseModel(badRequestException.ModelState);\n            }\n            else\n            {\n                errorMessage = badRequestException.Message;\n            }\n        }\n        else if (exception is GatewayException)\n        {\n            errorMessage = exception.Message;\n            context.HttpContext.Response.StatusCode = 400;\n        }\n        else if (exception is NotSupportedException && !string.IsNullOrWhiteSpace(exception.Message))\n        {\n            errorMessage = exception.Message;\n            context.HttpContext.Response.StatusCode = 400;\n        }\n        else if (exception is ApplicationException)\n        {\n            context.HttpContext.Response.StatusCode = 402;\n        }\n        else if (exception is NotFoundException)\n        {\n            errorMessage = \"Resource not found.\";\n            context.HttpContext.Response.StatusCode = 404;\n        }\n        else if (exception is SecurityTokenValidationException)\n        {\n            errorMessage = \"Invalid token.\";\n            context.HttpContext.Response.StatusCode = 403;\n        }\n        else if (exception is UnauthorizedAccessException)\n        {\n            errorMessage = \"Unauthorized.\";\n            context.HttpContext.Response.StatusCode = 401;\n        }\n        else\n        {\n            var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();\n            logger.LogError(0, exception, \"Unhandled exception\");\n            errorMessage = \"An unhandled server error has occurred.\";\n            context.HttpContext.Response.StatusCode = 500;\n        }\n\n        var errorModel = internalErrorModel ?? new InternalApi.ErrorResponseModel(errorMessage);\n        var env = context.HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>();\n        if (env.IsDevelopment())\n        {\n            errorModel.ExceptionMessage = exception.Message;\n            errorModel.ExceptionStackTrace = exception.StackTrace;\n            errorModel.InnerExceptionMessage = exception?.InnerException?.Message;\n        }\n        context.Result = new ObjectResult(errorModel);\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Utilities/ModelStateValidationFilterAttribute.cs",
    "content": "﻿using Bit.Core.Models.Api;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Filters;\n\nnamespace Bit.SharedWeb.Utilities;\n\npublic class ModelStateValidationFilterAttribute : ActionFilterAttribute\n{\n    public ModelStateValidationFilterAttribute()\n    {\n    }\n\n    public override void OnActionExecuting(ActionExecutingContext context)\n    {\n        var model = context.ActionArguments.FirstOrDefault(a => a.Key == \"model\");\n        if (model.Key == \"model\" && model.Value == null)\n        {\n            context.ModelState.AddModelError(string.Empty, \"Body is empty.\");\n        }\n\n        if (!context.ModelState.IsValid)\n        {\n            OnModelStateInvalid(context);\n        }\n    }\n\n    protected virtual void OnModelStateInvalid(ActionExecutingContext context)\n    {\n        context.Result = new BadRequestObjectResult(new ErrorResponseModel(context.ModelState));\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Utilities/PlayIdMiddleware.cs",
    "content": "﻿using Bit.Core.Services;\nusing Microsoft.AspNetCore.Http;\n\nnamespace Bit.SharedWeb.Utilities;\n\n/// <summary>\n/// Middleware to extract the x-play-id header and set it in the PlayIdService.\n/// \n/// PlayId is used in testing infrastructure to track data created during automated testing and fa  cilitate cleanup.\n/// </summary>\n/// <param name=\"next\"></param>\npublic sealed class PlayIdMiddleware(RequestDelegate next)\n{\n    private const int MaxPlayIdLength = 256;\n\n    public async Task Invoke(HttpContext context, PlayIdService playIdService)\n    {\n        if (context.Request.Headers.TryGetValue(\"x-play-id\", out var playId))\n        {\n            var playIdValue = playId.ToString();\n\n            if (string.IsNullOrWhiteSpace(playIdValue))\n            {\n                context.Response.StatusCode = StatusCodes.Status400BadRequest;\n                await context.Response.WriteAsJsonAsync(new { Error = \"x-play-id header cannot be empty or whitespace\" });\n                return;\n            }\n\n            if (playIdValue.Length > MaxPlayIdLength)\n            {\n                context.Response.StatusCode = StatusCodes.Status400BadRequest;\n                await context.Response.WriteAsJsonAsync(new { Error = $\"x-play-id header cannot exceed {MaxPlayIdLength} characters\" });\n                return;\n            }\n\n            playIdService.PlayId = playIdValue;\n        }\n\n        await next(context);\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Utilities/RequestLoggingMiddleware.cs",
    "content": "﻿using System.Collections;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Logging;\n\n#nullable enable\n\nnamespace Bit.SharedWeb.Utilities;\n\npublic sealed class RequestLoggingMiddleware\n{\n    private readonly RequestDelegate _next;\n    private readonly ILogger<RequestLoggingMiddleware> _logger;\n    private readonly GlobalSettings _globalSettings;\n\n    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger, GlobalSettings globalSettings)\n    {\n        _next = next;\n        _logger = logger;\n        _globalSettings = globalSettings;\n    }\n\n    public Task Invoke(HttpContext context, IFeatureService featureService)\n    {\n        using (_logger.BeginScope(\n          new RequestLogScope(context.GetIpAddress(_globalSettings),\n            GetHeaderValue(context, \"user-agent\"),\n            GetHeaderValue(context, \"device-type\"),\n            GetHeaderValue(context, \"device-type\"),\n            GetHeaderValue(context, \"bitwarden-client-version\"))))\n        {\n            return _next(context);\n        }\n\n        static string? GetHeaderValue(HttpContext httpContext, string header)\n        {\n            if (httpContext.Request.Headers.TryGetValue(header, out var value))\n            {\n                return value;\n            }\n\n            return null;\n        }\n    }\n\n\n    private sealed class RequestLogScope : IReadOnlyList<KeyValuePair<string, object?>>\n    {\n        private string? _cachedToString;\n\n        public RequestLogScope(string? ipAddress, string? userAgent, string? deviceType, string? origin, string? clientVersion)\n        {\n            IpAddress = ipAddress;\n            UserAgent = userAgent;\n            DeviceType = deviceType;\n            Origin = origin;\n            ClientVersion = clientVersion;\n        }\n\n        public KeyValuePair<string, object?> this[int index]\n        {\n            get\n            {\n                if (index == 0)\n                {\n                    return new KeyValuePair<string, object?>(nameof(IpAddress), IpAddress);\n                }\n                else if (index == 1)\n                {\n                    return new KeyValuePair<string, object?>(nameof(UserAgent), UserAgent);\n                }\n                else if (index == 2)\n                {\n                    return new KeyValuePair<string, object?>(nameof(DeviceType), DeviceType);\n                }\n                else if (index == 3)\n                {\n                    return new KeyValuePair<string, object?>(nameof(Origin), Origin);\n                }\n                else if (index == 4)\n                {\n                    return new KeyValuePair<string, object?>(nameof(ClientVersion), ClientVersion);\n                }\n\n                throw new ArgumentOutOfRangeException(nameof(index));\n            }\n        }\n\n        public int Count => 5;\n\n        public string? IpAddress { get; }\n        public string? UserAgent { get; }\n        public string? DeviceType { get; }\n        public string? Origin { get; }\n        public string? ClientVersion { get; }\n\n        public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()\n        {\n            for (var i = 0; i < Count; i++)\n            {\n                yield return this[i];\n            }\n        }\n        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n\n        public override string ToString()\n        {\n            _cachedToString ??= $\"IpAddress:{IpAddress} UserAgent:{UserAgent} DeviceType:{DeviceType} Origin:{Origin} ClientVersion:{ClientVersion}\";\n            return _cachedToString;\n        }\n    }\n}\n"
  },
  {
    "path": "src/SharedWeb/Utilities/ServiceCollectionExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing System.Reflection;\nusing System.Security.Claims;\nusing System.Security.Cryptography.X509Certificates;\nusing AspNetCoreRateLimit;\nusing Bit.Core.AdminConsole.AbilitiesCache;\nusing Bit.Core.AdminConsole.Models.Business.Tokenables;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.AdminConsole.Services.Implementations;\nusing Bit.Core.AdminConsole.Services.NoopImplementations;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Auth.IdentityServer;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Auth.Services.Implementations;\nusing Bit.Core.Auth.UserFeatures;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess;\nusing Bit.Core.Auth.UserFeatures.PasswordValidation;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Services.Implementations;\nusing Bit.Core.Billing.TrialInitiation;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.HostedServices;\nusing Bit.Core.KeyManagement;\nusing Bit.Core.NotificationCenter;\nusing Bit.Core.OrganizationFeatures;\nusing Bit.Core.Platform;\nusing Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Platform.Mail.Enqueuing;\nusing Bit.Core.Platform.Mail.Mailer;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.PushRegistration.Internal;\nusing Bit.Core.Repositories;\nusing Bit.Core.Resources;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.SecretsManager.Repositories.Noop;\nusing Bit.Core.Services;\nusing Bit.Core.Services.Implementations;\nusing Bit.Core.Services.Mail;\nusing Bit.Core.Settings;\nusing Bit.Core.Tokens;\nusing Bit.Core.Tools.ImportFeatures;\nusing Bit.Core.Tools.SendFeatures;\nusing Bit.Core.Tools.Services;\nusing Bit.Core.Utilities;\nusing Bit.Core.Vault;\nusing Bit.Core.Vault.Services;\nusing Bit.Infrastructure.Dapper;\nusing Bit.Infrastructure.EntityFramework;\nusing Bit.SharedWeb.Play;\nusing DnsClient;\nusing Duende.IdentityModel;\nusing LaunchDarkly.Sdk.Server;\nusing LaunchDarkly.Sdk.Server.Interfaces;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Authentication.JwtBearer;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.AspNetCore.HttpOverrides;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.Mvc.Localization;\nusing Microsoft.Azure.Cosmos.Fluent;\nusing Microsoft.Extensions.Caching.Cosmos;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Hosting;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Microsoft.OpenApi;\nusing StackExchange.Redis;\nusing Swashbuckle.AspNetCore.SwaggerGen;\nusing Constants = Bit.Core.Constants;\nusing NoopRepos = Bit.Core.Repositories.Noop;\nusing Role = Bit.Core.Entities.Role;\nusing TableStorageRepos = Bit.Core.Repositories.TableStorage;\n\nnamespace Bit.SharedWeb.Utilities;\n\npublic static class ServiceCollectionExtensions\n{\n    public static SupportedDatabaseProviders AddDatabaseRepositories(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        var (provider, connectionString) = GetDatabaseProvider(globalSettings);\n        services.SetupEntityFramework(connectionString, provider);\n\n        if (provider != SupportedDatabaseProviders.SqlServer)\n        {\n            services.AddPasswordManagerEFRepositories(globalSettings.SelfHosted);\n        }\n        else\n        {\n            services.AddDapperRepositories(globalSettings.SelfHosted);\n        }\n\n        if (globalSettings.SelfHosted)\n        {\n            services.AddSingleton<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>();\n        }\n        else\n        {\n            services.AddSingleton<IEventRepository, TableStorageRepos.EventRepository>();\n            services.AddSingleton<IInstallationDeviceRepository, TableStorageRepos.InstallationDeviceRepository>();\n            services.AddKeyedSingleton<IGrantRepository, Core.Auth.Repositories.Cosmos.GrantRepository>(\"cosmos\");\n        }\n\n        return provider;\n    }\n\n    /// <summary>\n    /// Registers test PlayId tracking services for test data management and cleanup.\n    /// This infrastructure is isolated to test environments and enables tracking of test-generated entities.\n    /// </summary>\n    public static void AddTestPlayIdTracking(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        if (globalSettings.TestPlayIdTrackingEnabled)\n        {\n            var (provider, _) = GetDatabaseProvider(globalSettings);\n\n            // Include PlayIdService for tracking Play Ids in repositories\n            // We need the http context accessor to use the Singleton version, which pulls from the scoped version\n            services.AddHttpContextAccessor();\n\n            services.AddSingleton<IPlayItemService, PlayItemService>();\n            services.AddSingleton<IPlayIdService, PlayIdSingletonService>();\n            services.AddScoped<PlayIdService>();\n\n            // Replace standard repositories with PlayId tracking decorators\n            if (provider == SupportedDatabaseProviders.SqlServer)\n            {\n                services.AddPlayIdTrackingDapperRepositories();\n            }\n            else\n            {\n                services.AddPlayIdTrackingEFRepositories();\n            }\n        }\n        else\n        {\n            services.AddSingleton<IPlayIdService, NeverPlayIdServices>();\n        }\n    }\n\n    public static void AddBaseServices(this IServiceCollection services, IGlobalSettings globalSettings)\n    {\n        services.AddScoped<ICipherService, CipherService>();\n        services.AddUserServices(globalSettings);\n        services.AddTrialInitiationServices();\n        services.AddOrganizationServices(globalSettings);\n        services.AddPolicyServices();\n        services.AddScoped<IGroupService, GroupService>();\n        services.AddScoped<IEventService, EventService>();\n        services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();\n        services.AddSingleton<IDeviceService, DeviceService>();\n        services.AddScoped<ISsoConfigService, SsoConfigService>();\n        services.AddScoped<IAuthRequestService, AuthRequestService>();\n        services.AddScoped<IDuoUniversalTokenService, DuoUniversalTokenService>();\n        services.AddScoped<ISendAuthorizationService, SendAuthorizationService>();\n        services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();\n        services.AddVaultServices();\n        services.AddReportingServices(globalSettings);\n        services.AddKeyManagementServices();\n        services.AddNotificationCenterServices();\n        services.AddPlatformServices();\n        services.AddImportServices();\n        services.AddSendServices();\n    }\n\n    public static void AddTokenizers(this IServiceCollection services)\n    {\n        services.AddSingleton<IDataProtectorTokenFactory<OrgDeleteTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<OrgDeleteTokenable>(\n                OrgDeleteTokenable.ClearTextPrefix,\n                OrgDeleteTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<OrgDeleteTokenable>>>())\n        );\n        services.AddSingleton<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<EmergencyAccessInviteTokenable>(\n                EmergencyAccessInviteTokenable.ClearTextPrefix,\n                EmergencyAccessInviteTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<EmergencyAccessInviteTokenable>>>())\n        );\n\n        services.AddSingleton<IDataProtectorTokenFactory<SsoTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<SsoTokenable>(\n                SsoTokenable.ClearTextPrefix,\n                SsoTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<SsoTokenable>>>()));\n        services.AddSingleton<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>(\n                WebAuthnCredentialCreateOptionsTokenable.ClearTextPrefix,\n                WebAuthnCredentialCreateOptionsTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>>()));\n        services.AddSingleton<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>(\n                WebAuthnLoginAssertionOptionsTokenable.ClearTextPrefix,\n                WebAuthnLoginAssertionOptionsTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>>()));\n        services.AddSingleton<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<SsoEmail2faSessionTokenable>(\n                SsoEmail2faSessionTokenable.ClearTextPrefix,\n                SsoEmail2faSessionTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<SsoEmail2faSessionTokenable>>>()));\n\n        services.AddSingleton<IOrgUserInviteTokenableFactory, OrgUserInviteTokenableFactory>();\n        services.AddSingleton<IDataProtectorTokenFactory<OrgUserInviteTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<OrgUserInviteTokenable>(\n                OrgUserInviteTokenable.ClearTextPrefix,\n                OrgUserInviteTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<OrgUserInviteTokenable>>>()));\n        services.AddSingleton<IDataProtectorTokenFactory<DuoUserStateTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<DuoUserStateTokenable>(\n                DuoUserStateTokenable.ClearTextPrefix,\n                DuoUserStateTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<DuoUserStateTokenable>>>()));\n\n        services.AddSingleton<IDataProtectorTokenFactory<ProviderDeleteTokenable>>(serviceProvider =>\n            new DataProtectorTokenFactory<ProviderDeleteTokenable>(\n                ProviderDeleteTokenable.ClearTextPrefix,\n                ProviderDeleteTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<ProviderDeleteTokenable>>>())\n        );\n        services.AddSingleton<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>(\n            serviceProvider => new DataProtectorTokenFactory<RegistrationEmailVerificationTokenable>(\n                RegistrationEmailVerificationTokenable.ClearTextPrefix,\n                RegistrationEmailVerificationTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>>()));\n        services.AddSingleton<IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable>>(\n            serviceProvider => new DataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable>(\n                TwoFactorAuthenticatorUserVerificationTokenable.ClearTextPrefix,\n                TwoFactorAuthenticatorUserVerificationTokenable.DataProtectorPurpose,\n                serviceProvider.GetDataProtectionProvider(),\n                serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable>>>()));\n    }\n\n    public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        // Required for UserService\n        services.AddWebAuthn(globalSettings);\n        // Required for HTTP calls\n        services.AddHttpClient();\n\n        services.AddSingleton<IStripeAdapter, StripeAdapter>();\n        services.AddSingleton<Braintree.IBraintreeGateway>((serviceProvider) =>\n        {\n            return new Braintree.BraintreeGateway\n            {\n                Environment = globalSettings.Braintree.Production ?\n                    Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,\n                MerchantId = globalSettings.Braintree.MerchantId,\n                PublicKey = globalSettings.Braintree.PublicKey,\n                PrivateKey = globalSettings.Braintree.PrivateKey\n            };\n        });\n        services.AddScoped<IStripePaymentService, StripePaymentService>();\n        services.AddScoped<IPaymentHistoryService, PaymentHistoryService>();\n        services.AddScoped<ITwoFactorEmailService, TwoFactorEmailService>();\n        // Legacy mailer service\n        services.AddSingleton<IStripeSyncService, StripeSyncService>();\n        services.AddSingleton<IMailService, HandlebarsMailService>();\n        // Modern mailers\n        services.AddMailer();\n        services.AddSingleton<ILicensingService, LicensingService>();\n        services.AddSingleton<ILookupClient>(_ =>\n        {\n            var options = new LookupClientOptions { Timeout = TimeSpan.FromSeconds(15), UseTcpOnly = true };\n            return new LookupClient(options);\n        });\n        services.AddSingleton<IDnsResolverService, DnsResolverService>();\n        services.AddOptionality();\n        services.AddTokenizers();\n\n        services.AddScoped<IApplicationCacheService, FeatureRoutedCacheService>();\n\n        if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&\n            CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName))\n        {\n            services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryServiceBusApplicationCacheService>();\n        }\n        else\n        {\n            services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryApplicationCacheService>();\n        }\n\n        var awsConfigured = CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret);\n        if (awsConfigured && CoreHelpers.SettingHasValue(globalSettings.Mail?.SendGridApiKey))\n        {\n            services.AddSingleton<IMailDeliveryService, MultiServiceMailDeliveryService>();\n        }\n        else if (awsConfigured)\n        {\n            services.AddSingleton<IMailDeliveryService, AmazonSesMailDeliveryService>();\n        }\n        else if (CoreHelpers.SettingHasValue(globalSettings.Mail?.Smtp?.Host))\n        {\n            services.AddSingleton<IMailDeliveryService, MailKitSmtpMailDeliveryService>();\n        }\n        else\n        {\n            services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();\n        }\n\n        services.AddPush(globalSettings);\n        services.AddPushRegistration();\n\n        if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString))\n        {\n            services.AddSingleton<IMailEnqueuingService, AzureQueueMailService>();\n        }\n        else\n        {\n            services.AddSingleton<IMailEnqueuingService, BlockingMailEnqueuingService>();\n        }\n\n        services.AddEventWriteServices(globalSettings);\n\n        if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString))\n        {\n            services.AddSingleton<IAttachmentStorageService, AzureAttachmentStorageService>();\n        }\n        else if (CoreHelpers.SettingHasValue(globalSettings.Attachment.BaseDirectory))\n        {\n            services.AddSingleton<IAttachmentStorageService, LocalAttachmentStorageService>();\n        }\n        else\n        {\n            services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();\n        }\n\n        if (CoreHelpers.SettingHasValue(globalSettings.Send.ConnectionString))\n        {\n            services.AddSingleton<ISendFileStorageService, AzureSendFileStorageService>();\n        }\n        else if (CoreHelpers.SettingHasValue(globalSettings.Send.BaseDirectory))\n        {\n            services.AddSingleton<ISendFileStorageService, LocalSendStorageService>();\n        }\n        else\n        {\n            services.AddSingleton<ISendFileStorageService, NoopSendFileStorageService>();\n        }\n    }\n\n    public static void AddOosServices(this IServiceCollection services)\n    {\n        services.AddScoped<IProviderService, NoopProviderService>();\n        services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();\n        services.AddScoped<ISecretRepository, NoopSecretRepository>();\n        services.AddScoped<ISecretVersionRepository, NoopSecretVersionRepository>();\n        services.AddScoped<IProjectRepository, NoopProjectRepository>();\n    }\n\n    public static void AddNoopServices(this IServiceCollection services)\n    {\n        services.AddSingleton<IMailService, NoopMailService>();\n        services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();\n        services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();\n        services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();\n        services.AddSingleton<ILicensingService, NoopLicensingService>();\n    }\n\n    public static IdentityBuilder AddCustomIdentityServices(\n        this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        services.TryAddTransient(typeof(IOtpTokenProvider<>), typeof(OtpTokenProvider<>));\n\n        services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>();\n        services.Configure<PasswordHasherOptions>(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);\n        services.Configure<TwoFactorRememberTokenProviderOptions>(options =>\n        {\n            options.TokenLifespan = TimeSpan.FromDays(30);\n        });\n\n        var identityBuilder = services.AddIdentityWithoutCookieAuth<User, Role>(options =>\n        {\n            options.User = new UserOptions\n            {\n                RequireUniqueEmail = true,\n                AllowedUserNameCharacters = null // all\n            };\n            options.Password = new PasswordOptions\n            {\n                RequireDigit = false,\n                RequireLowercase = false,\n                RequiredLength = 12,\n                RequireNonAlphanumeric = false,\n                RequireUppercase = false\n            };\n            options.ClaimsIdentity = new ClaimsIdentityOptions\n            {\n                SecurityStampClaimType = Claims.SecurityStamp,\n                UserNameClaimType = JwtClaimTypes.Email,\n                UserIdClaimType = JwtClaimTypes.Subject,\n            };\n            options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider;\n        });\n\n        identityBuilder\n            .AddUserStore<UserStore>()\n            .AddRoleStore<RoleStore>()\n            .AddTokenProvider<DataProtectorTokenProvider<User>>(TokenOptions.DefaultProvider)\n            .AddTokenProvider<AuthenticatorTokenProvider>(\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator))\n            .AddTokenProvider<EmailTwoFactorTokenProvider>(\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.Email))\n            .AddTokenProvider<YubicoOtpTokenProvider>(\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey))\n            .AddTokenProvider<DuoUniversalTokenProvider>(\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.Duo))\n            .AddTokenProvider<TwoFactorRememberTokenProvider>(\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember))\n            .AddTokenProvider<EmailTokenProvider>(TokenOptions.DefaultEmailProvider)\n            .AddTokenProvider<WebAuthnTokenProvider>(\n                CoreHelpers.CustomProviderName(TwoFactorProviderType.WebAuthn));\n\n        return identityBuilder;\n    }\n\n    public static void AddIdentityAuthenticationServices(\n        this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment,\n        Action<AuthorizationOptions> addAuthorization)\n    {\n        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n            .AddJwtBearer(options =>\n            {\n                options.MapInboundClaims = false;\n                options.Authority = globalSettings.BaseServiceUri.InternalIdentity;\n                options.RequireHttpsMetadata = !environment.IsDevelopment() &&\n                    globalSettings.BaseServiceUri.InternalIdentity.StartsWith(\"https\");\n                options.TokenValidationParameters.ValidateAudience = false;\n                options.TokenValidationParameters.ValidTypes = new[] { \"at+jwt\" };\n                options.TokenValidationParameters.NameClaimType = ClaimTypes.Email;\n                options.Events = new JwtBearerEvents\n                {\n                    OnMessageReceived = (context) =>\n                    {\n                        context.Token = TokenRetrieval.FromAuthorizationHeaderOrQueryString()(context.Request);\n                        return Task.CompletedTask;\n                    }\n                };\n            });\n\n        if (addAuthorization != null)\n        {\n            services.AddAuthorization(config =>\n            {\n                addAuthorization.Invoke(config);\n            });\n        }\n    }\n\n    public static void AddCustomDataProtectionServices(\n        this IServiceCollection services, IWebHostEnvironment env, GlobalSettings globalSettings)\n    {\n        var builder = services.AddDataProtection().SetApplicationName(\"Bitwarden\");\n        if (env.IsDevelopment())\n        {\n            return;\n        }\n\n        if (globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.DataProtection.Directory))\n        {\n            builder.PersistKeysToFileSystem(new DirectoryInfo(globalSettings.DataProtection.Directory));\n        }\n\n        if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Storage?.ConnectionString))\n        {\n            X509Certificate2 dataProtectionCert = null;\n            if (CoreHelpers.SettingHasValue(globalSettings.DataProtection.CertificateThumbprint))\n            {\n                dataProtectionCert = CoreHelpers.GetCertificate(\n                    globalSettings.DataProtection.CertificateThumbprint);\n            }\n            else if (CoreHelpers.SettingHasValue(globalSettings.DataProtection.CertificatePassword))\n            {\n                dataProtectionCert = CoreHelpers.GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, \"certificates\",\n                    \"dataprotection.pfx\", globalSettings.DataProtection.CertificatePassword)\n                    .GetAwaiter().GetResult();\n            }\n            builder\n                .PersistKeysToAzureBlobStorage(globalSettings.Storage.ConnectionString, \"aspnet-dataprotection\", \"keys.xml\")\n                .ProtectKeysWithCertificate(dataProtectionCert);\n        }\n    }\n\n    public static IIdentityServerBuilder AddIdentityServerCertificate(\n        this IIdentityServerBuilder identityServerBuilder, IWebHostEnvironment env, GlobalSettings globalSettings)\n    {\n        var certificate = CoreHelpers.GetIdentityServerCertificate(globalSettings);\n        if (certificate != null)\n        {\n            identityServerBuilder.AddSigningCredential(certificate);\n        }\n        else if (env.IsDevelopment() && !string.IsNullOrEmpty(globalSettings.DevelopmentDirectory))\n        {\n            var developerSigningKeyPath = Path.Combine(globalSettings.DevelopmentDirectory, \"signingkey.jwk\");\n            identityServerBuilder.AddDeveloperSigningCredential(true, developerSigningKeyPath);\n        }\n        else if (env.IsDevelopment())\n        {\n            identityServerBuilder.AddDeveloperSigningCredential(false);\n        }\n        else\n        {\n            throw new Exception(\"No identity certificate to use.\");\n        }\n        return identityServerBuilder;\n    }\n\n    public static GlobalSettings AddGlobalSettingsServices(this IServiceCollection services,\n        IConfiguration configuration, IHostEnvironment environment)\n    {\n        var globalSettings = new GlobalSettings();\n        ConfigurationBinder.Bind(configuration.GetSection(\"GlobalSettings\"), globalSettings);\n\n        if (environment.IsDevelopment() && configuration.GetValue<bool>(\"developSelfHosted\"))\n        {\n            // Override settings with selfHostedOverride settings\n            ConfigurationBinder.Bind(configuration.GetSection(\"Dev:SelfHostOverride:GlobalSettings\"), globalSettings);\n        }\n\n        services.AddSingleton(s => globalSettings);\n        services.AddSingleton<IGlobalSettings, GlobalSettings>(s => globalSettings);\n        return globalSettings;\n    }\n\n    public static void UseDefaultMiddleware(this IApplicationBuilder app,\n        IWebHostEnvironment env, GlobalSettings globalSettings)\n    {\n        app.UseMiddleware<RequestLoggingMiddleware>();\n        if (globalSettings.TestPlayIdTrackingEnabled)\n        {\n            app.UseMiddleware<PlayIdMiddleware>();\n        }\n    }\n\n    public static void UseForwardedHeaders(this IApplicationBuilder app, IGlobalSettings globalSettings)\n    {\n        var options = new ForwardedHeadersOptions\n        {\n            ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto\n        };\n\n        if (!globalSettings.LiteDeployment)\n        {\n            // Trust the X-Forwarded-Host header of the nginx docker container\n            try\n            {\n                var nginxIp = Dns.GetHostEntry(\"nginx\")?.AddressList.FirstOrDefault();\n                if (nginxIp != null)\n                {\n                    options.KnownProxies.Add(nginxIp);\n                }\n            }\n            catch\n            {\n                // Ignore DNS errors\n            }\n        }\n\n        if (!string.IsNullOrWhiteSpace(globalSettings.KnownProxies))\n        {\n            var proxies = globalSettings.KnownProxies.Split(',');\n            foreach (var proxy in proxies)\n            {\n                if (IPAddress.TryParse(proxy.Trim(), out var ip))\n                {\n                    options.KnownProxies.Add(ip);\n                }\n            }\n        }\n\n        if (!string.IsNullOrWhiteSpace(globalSettings.KnownNetworks))\n        {\n            var proxyNetworks = globalSettings.KnownNetworks.Split(',');\n            foreach (var proxyNetwork in proxyNetworks)\n            {\n                if (Microsoft.AspNetCore.HttpOverrides.IPNetwork.TryParse(proxyNetwork.Trim(), out var ipn))\n                {\n                    options.KnownNetworks.Add(ipn);\n                }\n            }\n        }\n\n        if (options.KnownProxies.Count > 1 || options.KnownNetworks.Count > 1)\n        {\n            options.ForwardLimit = null;\n        }\n\n        app.UseForwardedHeaders(options);\n    }\n\n    public static void AddCoreLocalizationServices(this IServiceCollection services)\n    {\n        services.AddTransient<II18nService, I18nService>();\n        services.AddLocalization(options => options.ResourcesPath = \"Resources\");\n    }\n\n    public static IApplicationBuilder UseCoreLocalization(this IApplicationBuilder app)\n    {\n        var supportedCultures = new[] { \"en\" };\n        return app.UseRequestLocalization(options => options\n            .SetDefaultCulture(supportedCultures[0])\n            .AddSupportedCultures(supportedCultures)\n            .AddSupportedUICultures(supportedCultures));\n    }\n\n    public static IMvcBuilder AddViewAndDataAnnotationLocalization(this IMvcBuilder mvc)\n    {\n        mvc.Services.AddTransient<IViewLocalizer, I18nViewLocalizer>();\n        return mvc.AddViewLocalization(options => options.ResourcesPath = \"Resources\")\n            .AddDataAnnotationsLocalization(options =>\n                options.DataAnnotationLocalizerProvider = (type, factory) =>\n                {\n                    var assemblyName = new AssemblyName(typeof(SharedResources).GetTypeInfo().Assembly.FullName);\n                    return factory.Create(\"SharedResources\", assemblyName.Name);\n                });\n    }\n\n    public static IServiceCollection AddDistributedIdentityServices(this IServiceCollection services)\n    {\n        services.AddOidcStateDataFormatterCache();\n        services.AddSession();\n        services.ConfigureApplicationCookie(configure => configure.CookieManager = new DistributedCacheCookieManager());\n        services.ConfigureExternalCookie(configure => configure.CookieManager = new DistributedCacheCookieManager());\n        services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, ConfigureOpenIdConnectDistributedOptions>();\n\n        return services;\n    }\n\n    public static void AddWebAuthn(this IServiceCollection services, GlobalSettings globalSettings)\n    {\n        services.AddFido2(options =>\n        {\n            options.ServerDomain = new Uri(globalSettings.BaseServiceUri.Vault).Host;\n            options.ServerName = \"Bitwarden\";\n            options.TimestampDriftTolerance = 300000;\n\n            if (globalSettings.Fido2?.Origins?.Any() == true)\n            {\n                options.Origins = new HashSet<string>(globalSettings.Fido2.Origins);\n            }\n            else\n            {\n                // Default to allowing the vault domain and chromium browser extension IDs\n                options.Origins = new HashSet<string> {\n                    globalSettings.BaseServiceUri.Vault,\n                    Constants.BrowserExtensions.ChromeId,\n                    Constants.BrowserExtensions.EdgeId,\n                    Constants.BrowserExtensions.OperaId\n                 };\n            }\n        });\n    }\n\n    /// <summary>\n    ///     Adds either an in-memory or distributed IP rate limiter depending if a Redis connection string is available.\n    /// </summary>\n    /// <param name=\"services\"></param>\n    /// <param name=\"globalSettings\"></param>\n    public static void AddIpRateLimiting(this IServiceCollection services,\n        GlobalSettings globalSettings)\n    {\n        services.AddHostedService<IpRateLimitSeedStartupService>();\n        services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();\n\n        if (!globalSettings.DistributedIpRateLimiting.Enabled ||\n            string.IsNullOrEmpty(globalSettings.DistributedIpRateLimiting.RedisConnectionString))\n        {\n            services.AddInMemoryRateLimiting();\n        }\n        else\n        {\n            // Use memory stores for Ip and Client Policy stores as we don't currently use them\n            // and they add unnecessary Redis network delays checking for policies that don't exist\n            services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();\n            services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();\n\n            // Use a custom Redis processing strategy that skips Ip limiting if Redis is down\n            services.AddKeyedSingleton<IConnectionMultiplexer>(\"rate-limiter\", (_, provider) =>\n                ConnectionMultiplexer.Connect(globalSettings.DistributedIpRateLimiting.RedisConnectionString));\n            services.AddSingleton<IProcessingStrategy, CustomRedisProcessingStrategy>();\n        }\n    }\n\n    /// <summary>\n    ///     Adds an implementation of <see cref=\"IDistributedCache\"/> to the service collection. Uses Redis\n    /// if a connection string is available in GlobalSettings, a database-backed distributed cache if\n    /// self-hosted or a distributed memory cache as a final fallback.\n    /// </summary>\n    public static void AddDistributedCache(\n        this IServiceCollection services,\n        GlobalSettings globalSettings)\n    {\n        if (!string.IsNullOrEmpty(globalSettings.DistributedCache?.Redis?.ConnectionString))\n        {\n            services.AddStackExchangeRedisCache(options =>\n            {\n                options.Configuration = globalSettings.DistributedCache.Redis.ConnectionString;\n            });\n        }\n        else\n        {\n            if (globalSettings.SelfHosted)\n            {\n                var (databaseProvider, databaseConnectionString) = GetDatabaseProvider(globalSettings);\n                if (databaseProvider == SupportedDatabaseProviders.SqlServer)\n                {\n                    services.AddDistributedSqlServerCache(o =>\n                    {\n                        o.ConnectionString = databaseConnectionString;\n                        o.SchemaName = \"dbo\";\n                        o.TableName = \"Cache\";\n                    });\n                }\n                else\n                {\n                    services.AddSingleton<IDistributedCache, EntityFrameworkCache>();\n                }\n            }\n            else\n            {\n                services.AddDistributedMemoryCache();\n            }\n        }\n\n        if (!string.IsNullOrEmpty(globalSettings.DistributedCache?.Cosmos?.ConnectionString))\n        {\n            services.AddKeyedSingleton<IDistributedCache>(\"persistent\", (s, _) =>\n                new CosmosCache(new CosmosCacheOptions\n                {\n                    DatabaseName = \"cache\",\n                    ContainerName = \"default\",\n                    CreateIfNotExists = false,\n                    ClientBuilder = new CosmosClientBuilder(globalSettings.DistributedCache?.Cosmos?.ConnectionString)\n                }));\n        }\n        else\n        {\n            services.AddKeyedSingleton(\"persistent\", (s, _) => s.GetRequiredService<IDistributedCache>());\n        }\n    }\n\n    public static IServiceCollection AddOptionality(this IServiceCollection services)\n    {\n        services.AddSingleton<ILdClient>(s =>\n        {\n            return new LdClient(LaunchDarklyFeatureService.GetConfiguredClient(\n                s.GetRequiredService<GlobalSettings>()));\n        });\n\n        services.AddScoped<IFeatureService, LaunchDarklyFeatureService>();\n\n        return services;\n    }\n\n    private static (SupportedDatabaseProviders provider, string connectionString)\n        GetDatabaseProvider(GlobalSettings globalSettings)\n    {\n        var selectedDatabaseProvider = globalSettings.DatabaseProvider;\n        var provider = SupportedDatabaseProviders.SqlServer;\n        var connectionString = string.Empty;\n\n        if (!string.IsNullOrWhiteSpace(selectedDatabaseProvider))\n        {\n            switch (selectedDatabaseProvider.ToLowerInvariant())\n            {\n                case \"postgres\":\n                case \"postgresql\":\n                    provider = SupportedDatabaseProviders.Postgres;\n                    connectionString = globalSettings.PostgreSql.ConnectionString;\n                    break;\n                case \"mysql\":\n                case \"mariadb\":\n                    provider = SupportedDatabaseProviders.MySql;\n                    connectionString = globalSettings.MySql.ConnectionString;\n                    break;\n                case \"sqlite\":\n                    provider = SupportedDatabaseProviders.Sqlite;\n                    connectionString = globalSettings.Sqlite.ConnectionString;\n                    break;\n                case \"sqlserver\":\n                    connectionString = globalSettings.SqlServer.ConnectionString;\n                    break;\n                default:\n                    break;\n            }\n        }\n        else\n        {\n            // Default to attempting to use SqlServer connection string if globalSettings.DatabaseProvider has no value.\n            connectionString = globalSettings.SqlServer.ConnectionString;\n        }\n\n        return (provider, connectionString);\n    }\n\n    /// <summary>\n    /// Adds a server with its corresponding OAuth2 client credentials security definition and requirement.\n    /// </summary>\n    /// <param name=\"config\">The SwaggerGen configuration</param>\n    /// <param name=\"serverId\">Unique identifier for this server (e.g., \"us-server\", \"eu-server\")</param>\n    /// <param name=\"serverUrl\">The API server URL</param>\n    /// <param name=\"identityTokenUrl\">The identity server token URL</param>\n    /// <param name=\"serverDescription\">Human-readable description for the server</param>\n    public static void AddSwaggerServerWithSecurity(\n        this SwaggerGenOptions config,\n        string serverId,\n        string serverUrl,\n        string identityTokenUrl,\n        string serverDescription)\n    {\n        // Add server\n        config.AddServer(new OpenApiServer\n        {\n            Url = serverUrl,\n            Description = serverDescription\n        });\n\n        // Add security definition\n        config.AddSecurityDefinition(serverId, new OpenApiSecurityScheme\n        {\n            Type = SecuritySchemeType.OAuth2,\n            Description = $\"**Use this option if you've selected the {serverDescription}**\",\n            Flows = new OpenApiOAuthFlows\n            {\n                ClientCredentials = new OpenApiOAuthFlow\n                {\n                    TokenUrl = new Uri(identityTokenUrl),\n                    Scopes = new Dictionary<string, string>\n                    {\n                        { ApiScopes.ApiOrganization, $\"Organization APIs ({serverDescription})\" },\n                    },\n                }\n            },\n        });\n\n        // Add security requirement\n        config.AddSecurityRequirement((document) => new OpenApiSecurityRequirement\n        {\n            [new OpenApiSecuritySchemeReference(serverId, document)] = [ApiScopes.ApiOrganization]\n        });\n    }\n}\n"
  },
  {
    "path": "src/Sql/Sql.sqlproj",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project DefaultTargets=\"Build\">\n  <Sdk Name=\"Microsoft.Build.Sql\"/>\n  <PropertyGroup>\n    <Name>Sql</Name>\n    <ProjectGuid>{58554e52-fdec-4832-aff9-302b01e08dca}</ProjectGuid>\n    <DSP>Microsoft.Data.Tools.Schema.Sql.SqlAzureV12DatabaseSchemaProvider</DSP>\n    <ModelCollation>1033,CI</ModelCollation>\n    <TargetDatabaseSet>True</TargetDatabaseSet>\n    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>\n    <TargetFrameworkProfile />\n  </PropertyGroup>\n  <ItemGroup>\n    <!-- Remove file just so we can add it back with some suppressions -->\n    <Build Remove=\"dbo/Stored Procedures/AzureSQLMaintenance.sql\" />\n    <Build Include=\"dbo/Stored Procedures/AzureSQLMaintenance.sql\">\n      <SuppressTSqlWarnings>71502</SuppressTSqlWarnings>\n    </Build>\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Sql/dbo/AdminConsole/Stored Procedures/Collection_CreateDefaultCollections.sql",
    "content": "-- Creates default user collections for organization users\n-- Filters out existing default collections at database level\nCREATE PROCEDURE [dbo].[Collection_CreateDefaultCollections]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @DefaultCollectionName VARCHAR(MAX),\n    @OrganizationUserCollectionIds AS [dbo].[TwoGuidIdArray] READONLY -- OrganizationUserId, CollectionId\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @Now DATETIME2(7) = GETUTCDATE()\n\n    -- Filter to only users who don't have default collections\n    SELECT ids.Id1, ids.Id2\n    INTO #FilteredIds\n    FROM @OrganizationUserCollectionIds ids\n    WHERE NOT EXISTS (\n        SELECT 1\n        FROM [dbo].[CollectionUser] cu\n        INNER JOIN [dbo].[Collection] c ON c.Id = cu.CollectionId\n        WHERE c.OrganizationId = @OrganizationId\n          AND c.[Type] = 1 -- CollectionType.DefaultUserCollection\n          AND cu.OrganizationUserId = ids.Id1\n    );\n\n    -- Insert collections only for users who don't have default collections yet\n    INSERT INTO [dbo].[Collection]\n    (\n        [Id],\n        [OrganizationId],\n        [Name],\n        [CreationDate],\n        [RevisionDate],\n        [Type],\n        [ExternalId],\n        [DefaultUserCollectionEmail]\n    )\n    SELECT\n        ids.Id2, -- CollectionId\n        @OrganizationId,\n        @DefaultCollectionName,\n        @Now,\n        @Now,\n        1, -- CollectionType.DefaultUserCollection\n        NULL,\n        NULL\n    FROM\n        #FilteredIds ids;\n\n    -- Insert collection user mappings\n    INSERT INTO [dbo].[CollectionUser]\n    (\n        [CollectionId],\n        [OrganizationUserId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        ids.Id2, -- CollectionId\n        ids.Id1, -- OrganizationUserId\n        0, -- ReadOnly = false\n        0, -- HidePasswords = false\n        1  -- Manage = true\n    FROM\n        #FilteredIds ids;\n\n    DROP TABLE #FilteredIds;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[AuthRequest_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER = NULL,\n    @Type TINYINT,\n    @RequestDeviceIdentifier NVARCHAR(50),\n    @RequestDeviceType TINYINT,\n    @RequestIpAddress VARCHAR(50),\n    @RequestCountryName NVARCHAR(200),\n    @ResponseDeviceId UNIQUEIDENTIFIER,\n    @AccessCode VARCHAR(25),\n    @PublicKey VARCHAR(MAX),\n    @Key VARCHAR(MAX),\n    @MasterPasswordHash VARCHAR(MAX),\n    @Approved BIT,\n    @CreationDate DATETIME2(7),\n    @ResponseDate DATETIME2(7),\n    @AuthenticationDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[AuthRequest]\n        (\n        [Id],\n        [UserId],\n        [OrganizationId],\n        [Type],\n        [RequestDeviceIdentifier],\n        [RequestDeviceType],\n        [RequestIpAddress],\n        [RequestCountryName],\n        [ResponseDeviceId],\n        [AccessCode],\n        [PublicKey],\n        [Key],\n        [MasterPasswordHash],\n        [Approved],\n        [CreationDate],\n        [ResponseDate],\n        [AuthenticationDate]\n        )\n    VALUES\n        (\n            @Id,\n            @UserId,\n            @OrganizationId,\n            @Type,\n            @RequestDeviceIdentifier,\n            @RequestDeviceType,\n            @RequestIpAddress,\n            @RequestCountryName,\n            @ResponseDeviceId,\n            @AccessCode,\n            @PublicKey,\n            @Key,\n            @MasterPasswordHash,\n            @Approved,\n            @CreationDate,\n            @ResponseDate,\n            @AuthenticationDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[AuthRequest_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[AuthRequest]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_DeleteIfExpired.sql",
    "content": "﻿-- UserExpirationSeconds to 15 minutes (15 * 60)\n-- AdminExpirationSeconds to 7 days (7 * 24 * 60 * 60)\n-- AdminApprovalExpirationSeconds to 12 hour (12 * 60 * 60)\n\nCREATE PROCEDURE [dbo].[AuthRequest_DeleteIfExpired]\n    @UserExpirationSeconds INT = 900,\n    @AdminExpirationSeconds INT = 604800,\n    @AdminApprovalExpirationSeconds INT = 43200\nAS\nBEGIN\n    SET NOCOUNT OFF\n    DELETE FROM [dbo].[AuthRequest]\n        -- User requests expire after 15 minutes (by default) of their creation\n    WHERE ([Type] != 2 AND DATEADD(second, @UserExpirationSeconds, [CreationDate]) < GETUTCDATE())\n        -- Admin requests expire after 7 days (by default) of their creation if they have not been approved\n        OR ([Type] = 2 AND ([Approved] IS NULL OR [Approved] = 0) AND DATEADD(second, @AdminExpirationSeconds,[CreationDate]) < GETUTCDATE())\n        -- Admin requests expire after 12 hours (by default) of their approval\n        OR ([Type] = 2 AND [Approved] = 1 AND DATEADD(second, @AdminApprovalExpirationSeconds, [ResponseDate]) < GETUTCDATE());\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadAdminApprovalsByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[AuthRequest_ReadAdminApprovalsByIds]\n\t@OrganizationId UNIQUEIDENTIFIER,\n\t@Ids AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n    ar.*, u.[Email], ou.[Id] AS [OrganizationUserId]\nFROM\n    [dbo].[AuthRequestView] ar\n    INNER JOIN\n        [dbo].[OrganizationUser] ou ON ou.[UserId] = ar.[UserId] AND ou.[OrganizationId] = ar.[OrganizationId]\n    INNER JOIN\n        [dbo].[User] u ON u.[Id] = ar.[UserId]\n    WHERE\n        ar.[OrganizationId] = @OrganizationId\n    AND\n        ar.[Type] = 2 -- AdminApproval\n    AND\n        ar.[Id] IN (SELECT [Id] FROM @Ids)\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[AuthRequest_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[AuthRequestView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadByUserId.sql",
    "content": "CREATE PROCEDURE [dbo].[AuthRequest_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[AuthRequestView]\n    WHERE\n        [UserId] = @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[AuthRequest_ReadPendingByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n    ar.*, u.[Email], ou.[Id] AS [OrganizationUserId]\nFROM\n    [dbo].[AuthRequestView] ar\n    INNER JOIN\n        [dbo].[OrganizationUser] ou ON ou.[UserId] = ar.[UserId] AND ou.[OrganizationId] = ar.[OrganizationId]\n    INNER JOIN\n        [dbo].[User] u ON u.[Id] = ar.[UserId]\n    WHERE\n        ar.[OrganizationId] = @OrganizationId \n    AND \n        ar.[ResponseDate] IS NULL\n    AND\n        ar.[Type] = 2 -- AdminApproval\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByUserId.sql",
    "content": "CREATE PROCEDURE [dbo].[AuthRequest_ReadPendingByUserId]\n    @UserId UNIQUEIDENTIFIER,\n    @ExpirationMinutes INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT *\n    FROM [dbo].[AuthRequestPendingDetailsView]\n    WHERE [UserId] = @UserId\n        AND [CreationDate] >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE())\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[AuthRequest_Update]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER = NULL,\n    @Type SMALLINT,\n    @RequestDeviceIdentifier NVARCHAR(50),\n    @RequestDeviceType SMALLINT,\n    @RequestIpAddress VARCHAR(50),\n    @RequestCountryName NVARCHAR(200),\n    @ResponseDeviceId UNIQUEIDENTIFIER,\n    @AccessCode VARCHAR(25),\n    @PublicKey VARCHAR(MAX),\n    @Key VARCHAR(MAX),\n    @MasterPasswordHash VARCHAR(MAX),\n    @Approved BIT,\n    @CreationDate DATETIME2 (7),\n    @ResponseDate DATETIME2 (7),\n    @AuthenticationDate DATETIME2 (7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n    [dbo].[AuthRequest]\nSET\n    [UserId] = @UserId,\n    [Type] = @Type,\n    [OrganizationId] = @OrganizationId,\n    [RequestDeviceIdentifier] = @RequestDeviceIdentifier,\n    [RequestDeviceType] = @RequestDeviceType,\n    [RequestIpAddress] = @RequestIpAddress,\n    [RequestCountryName] = @RequestCountryName,\n    [ResponseDeviceId] = @ResponseDeviceId,\n    [AccessCode] = @AccessCode,\n    [PublicKey] = @PublicKey,\n    [Key] = @Key,\n    [MasterPasswordHash] = @MasterPasswordHash,\n    [Approved] = @Approved,\n    [CreationDate] = @CreationDate,\n    [ResponseDate] = @ResponseDate,\n    [AuthenticationDate] = @AuthenticationDate\nWHERE\n    [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/AuthRequest_UpdateMany.sql",
    "content": "CREATE PROCEDURE AuthRequest_UpdateMany\n    @jsonData NVARCHAR(MAX)\nAS\nBEGIN\n    UPDATE AR\n    SET\n        [Id] = ARI.[Id],\n        [UserId] = ARI.[UserId],\n        [Type] = ARI.[Type],\n        [RequestDeviceIdentifier] = ARI.[RequestDeviceIdentifier],\n        [RequestDeviceType] = ARI.[RequestDeviceType],\n        [RequestIpAddress] = ARI.[RequestIpAddress],\n        [RequestCountryName] = ARI.[RequestCountryName],\n        [ResponseDeviceId] = ARI.[ResponseDeviceId],\n        [AccessCode] = ARI.[AccessCode],\n        [PublicKey] = ARI.[PublicKey],\n        [Key] = ARI.[Key],\n        [MasterPasswordHash] = ARI.[MasterPasswordHash],\n        [Approved] = ARI.[Approved],\n        [CreationDate] = ARI.[CreationDate],\n        [ResponseDate] = ARI.[ResponseDate],\n        [AuthenticationDate] = ARI.[AuthenticationDate],\n        [OrganizationId] = ARI.[OrganizationId]\n    FROM\n        [dbo].[AuthRequest] AR\n        INNER JOIN\n        OPENJSON(@jsonData)\n        WITH (\n            Id UNIQUEIDENTIFIER '$.Id',\n            UserId UNIQUEIDENTIFIER '$.UserId',\n            Type SMALLINT '$.Type',\n            RequestDeviceIdentifier NVARCHAR(50) '$.RequestDeviceIdentifier',\n            RequestDeviceType SMALLINT '$.RequestDeviceType',\n            RequestIpAddress VARCHAR(50) '$.RequestIpAddress',\n            RequestCountryName NVARCHAR(200) '$.RequestCountryName',\n            ResponseDeviceId UNIQUEIDENTIFIER '$.ResponseDeviceId',\n            AccessCode VARCHAR(25) '$.AccessCode',\n            PublicKey VARCHAR(MAX) '$.PublicKey',\n            [Key] VARCHAR(MAX) '$.Key',\n            MasterPasswordHash VARCHAR(MAX) '$.MasterPasswordHash',\n            Approved BIT '$.Approved',\n            CreationDate DATETIME2 '$.CreationDate',\n            ResponseDate DATETIME2 '$.ResponseDate',\n            AuthenticationDate DATETIME2 '$.AuthenticationDate',\n            OrganizationId UNIQUEIDENTIFIER '$.OrganizationId'\n        ) ARI ON AR.Id = ARI.Id;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql",
    "content": "CREATE PROCEDURE [dbo].[Device_ReadActiveWithPendingAuthRequestsByUserId]\n    @UserId UNIQUEIDENTIFIER,\n    @ExpirationMinutes INT\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    SELECT\n        D.*,\n        AR.Id as AuthRequestId,\n        AR.CreationDate as AuthRequestCreationDate\n    FROM dbo.DeviceView D\n    LEFT JOIN (\n        SELECT \n            Id,\n            CreationDate,\n            RequestDeviceIdentifier,\n            Approved,\n            ROW_NUMBER() OVER (PARTITION BY RequestDeviceIdentifier ORDER BY CreationDate DESC) as rn\n        FROM dbo.AuthRequestView\n        WHERE Type IN (0, 1)  -- AuthenticateAndUnlock and Unlock types only\n            AND CreationDate >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE()) -- Ensure the request hasn't expired\n            AND UserId = @UserId --  Requests for this user only\n    ) AR -- This join will get the most recent request per device, regardless of approval status \n    ON D.Identifier = AR.RequestDeviceIdentifier AND AR.rn = 1 AND AR.Approved IS NULL  -- Get only the most recent unapproved request per device\n    WHERE\n        D.UserId = @UserId -- Include only devices for this user\n      AND D.Active = 1; -- Include only active devices\nEND;\n\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadByGranteeId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadByGranteeId]\n    @GranteeId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EmergencyAccessDetailsView]\n    WHERE\n        [GranteeId] = @GranteeId\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadByGrantorId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadByGrantorId]\n    @GrantorId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EmergencyAccessDetailsView]\n    WHERE\n        [GrantorId] = @GrantorId\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EmergencyAccessDetailsView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadByIdGrantorId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadByIdGrantorId]\n    @Id UNIQUEIDENTIFIER,\n    @GrantorId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EmergencyAccessDetailsView]\n    WHERE\n        [Id] = @Id\n    AND\n        [GrantorId] = @GrantorId\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadExpiredRecoveries.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadExpiredRecoveries]\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EmergencyAccessDetailsView]\n    WHERE\n        [Status] = 3\n    AND\n        DATEADD(DAY, [WaitTimeDays], [RecoveryInitiatedDate]) <= GETUTCDATE()\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadManyByUserIds.sql",
    "content": "CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadManyByUserIds]\n    @UserIds [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EmergencyAccessDetailsView]\n    WHERE\n        [GrantorId] IN (SELECT [Id] FROM @UserIds)\n        OR [GranteeId] IN (SELECT [Id] FROM @UserIds)\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccess_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @GrantorId UNIQUEIDENTIFIER,\n    @GranteeId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @KeyEncrypted VARCHAR(MAX),\n    @Type TINYINT,\n    @Status TINYINT,\n    @WaitTimeDays SMALLINT,\n    @RecoveryInitiatedDate DATETIME2(7),\n    @LastNotificationDate DATETIME2(7),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[EmergencyAccess]\n    (\n        [Id],\n        [GrantorId],\n        [GranteeId],\n        [Email],\n        [KeyEncrypted],\n        [Type],\n        [Status],\n        [WaitTimeDays],\n        [RecoveryInitiatedDate],\n        [LastNotificationDate],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @GrantorId,\n        @GranteeId,\n        @Email,\n        @KeyEncrypted,\n        @Type,\n        @Status,\n        @WaitTimeDays,\n        @RecoveryInitiatedDate,\n        @LastNotificationDate,\n        @CreationDate,\n        @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccess_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n    \n    EXEC [dbo].[User_BumpAccountRevisionDateByEmergencyAccessGranteeId] @Id\n    \n    DELETE\n    FROM\n        [dbo].[EmergencyAccess]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_DeleteManyById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccess_DeleteManyById]\n    @EmergencyAccessIds [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserIds AS [GuidIdArray];\n    DECLARE @BatchSize INT = 100\n\n    INSERT INTO @UserIds\n    SELECT DISTINCT\n        [GranteeId]\n    FROM\n        [dbo].[EmergencyAccess] EA\n        INNER JOIN\n        @EmergencyAccessIds EAI ON EAI.[Id] = EA.[Id]\n    WHERE\n        EA.[Status] = 2 -- 2 = Bit.Core.Auth.Enums.EmergencyAccessStatusType.Confirmed\n        AND\n        EA.[GranteeId] IS NOT NULL\n\n\n    -- Delete EmergencyAccess Records\n    WHILE @BatchSize > 0\n    BEGIN\n\n        DELETE TOP(@BatchSize) EA\n        FROM\n            [dbo].[EmergencyAccess] EA\n            INNER JOIN\n            @EmergencyAccessIds EAI ON EAI.[Id] = EA.[Id]\n\n        SET @BatchSize = @@ROWCOUNT\n\n    END\n\n    -- Bump AccountRevisionDate for affected users after deletions\n    Exec [dbo].[User_BumpManyAccountRevisionDates] @UserIds\n\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccess_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EmergencyAccess]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_ReadCountByGrantorIdEmail.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccess_ReadCountByGrantorIdEmail]\n    @GrantorId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @OnlyUsers BIT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        COUNT(1)\n    FROM\n        [dbo].[EmergencyAccess] EA\n    LEFT JOIN\n        [dbo].[User] U ON EA.[GranteeId] = U.[Id]\n    WHERE\n        EA.[GrantorId] = @GrantorId\n        AND (\n            (@OnlyUsers = 0 AND (EA.[Email] = @Email OR U.[Email] = @Email))\n            OR (@OnlyUsers = 1 AND U.[Email] = @Email)\n        )\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_ReadToNotify.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccess_ReadToNotify]\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        EA.*,\n        Grantee.Name as GranteeName,\n        Grantee.Email as GranteeEmail,\n        Grantor.Email as GrantorEmail\n    FROM\n        [dbo].[EmergencyAccess] EA\n    LEFT JOIN\n        [dbo].[User] Grantor ON Grantor.[Id] = EA.[GrantorId]\n    LEFT JOIN\n        [dbo].[User] Grantee On Grantee.[Id] = EA.[GranteeId]\n    WHERE\n        EA.[Status] = 3\n    AND\n        DATEADD(DAY, EA.[WaitTimeDays] - 1, EA.[RecoveryInitiatedDate]) <= GETUTCDATE()\n    AND\n        DATEADD(DAY, 1, EA.[LastNotificationDate]) <= GETUTCDATE()\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[EmergencyAccess_Update]\n    @Id UNIQUEIDENTIFIER,\n    @GrantorId UNIQUEIDENTIFIER,\n    @GranteeId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @KeyEncrypted VARCHAR(MAX),\n    @Type TINYINT,\n    @Status TINYINT,\n    @WaitTimeDays SMALLINT,\n    @RecoveryInitiatedDate DATETIME2(7),\n    @LastNotificationDate DATETIME2(7),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[EmergencyAccess]\n    SET\n        [GrantorId] = @GrantorId,\n        [GranteeId] = @GranteeId,\n        [Email] = @Email,\n        [KeyEncrypted] = @KeyEncrypted,\n        [Type] = @Type,\n        [Status] = @Status,\n        [WaitTimeDays] = @WaitTimeDays,\n        [RecoveryInitiatedDate] = @RecoveryInitiatedDate,\n        [LastNotificationDate] = @LastNotificationDate,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @GranteeId\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/Grant_Delete.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Grant_Delete]\n    @SubjectId NVARCHAR(200),\n    @SessionId NVARCHAR(100),\n    @ClientId NVARCHAR(200),\n    @Type NVARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[Grant]\n    WHERE\n        (@SubjectId IS NULL OR [SubjectId] = @SubjectId)\n        AND (@ClientId IS NULL OR [ClientId] = @ClientId)\n        AND (@SessionId IS NULL OR [SessionId] = @SessionId)\n        AND (@Type IS NULL OR [Type] = @Type)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/Grant_DeleteByKey.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Grant_DeleteByKey]\n    @Key NVARCHAR(200)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[Grant]\n    WHERE\n        [Key] = @Key\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/Grant_DeleteExpired.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Grant_DeleteExpired]\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @BatchSize INT = 100\n    DECLARE @Now DATETIME2(7) = GETUTCDATE()\n\n    WHILE @BatchSize > 0\n    BEGIN\n        DELETE TOP(@BatchSize)\n        FROM\n            [dbo].[Grant]\n        WHERE\n            [ExpirationDate] < @Now\n\n        SET @BatchSize = @@ROWCOUNT\n    END\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/Grant_Read.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Grant_Read]\n    @SubjectId NVARCHAR(200),\n    @SessionId NVARCHAR(100),\n    @ClientId NVARCHAR(200),\n    @Type NVARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[GrantView]\n    WHERE\n        (@SubjectId IS NULL OR [SubjectId] = @SubjectId)\n        AND (@ClientId IS NULL OR [ClientId] = @ClientId)\n        AND (@SessionId IS NULL OR [SessionId] = @SessionId)\n        AND (@Type IS NULL OR [Type] = @Type)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/Grant_ReadByKey.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Grant_ReadByKey]\n    @Key NVARCHAR(200)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[GrantView]\n    WHERE\n        [Key] = @Key\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/Grant_Save.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Grant_Save]\n    @Key NVARCHAR(200),\n    @Type NVARCHAR(50),\n    @SubjectId NVARCHAR(200),\n    @SessionId NVARCHAR(100),\n    @ClientId NVARCHAR(200),\n    @Description NVARCHAR(200),\n    @CreationDate DATETIME2,\n    @ExpirationDate DATETIME2,\n    @ConsumedDate DATETIME2,\n    @Data NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    -- First, try to update the existing row\n    UPDATE [dbo].[Grant]\n    SET\n        [Type] = @Type,\n        [SubjectId] = @SubjectId,\n        [SessionId] = @SessionId,\n        [ClientId] = @ClientId,\n        [Description] = @Description,\n        [CreationDate] = @CreationDate,\n        [ExpirationDate] = @ExpirationDate,\n        [ConsumedDate] = @ConsumedDate,\n        [Data] = @Data\n    WHERE\n        [Key] = @Key\n\n    -- If no row was updated, insert a new one\n    IF @@ROWCOUNT = 0\n    BEGIN\n        INSERT INTO [dbo].[Grant]\n            (\n            [Key],\n            [Type],\n            [SubjectId],\n            [SessionId],\n            [ClientId],\n            [Description],\n            [CreationDate],\n            [ExpirationDate],\n            [ConsumedDate],\n            [Data]\n            )\n        VALUES\n            (\n                @Key,\n                @Type,\n                @SubjectId,\n                @SessionId,\n                @ClientId,\n                @Description,\n                @CreationDate,\n                @ExpirationDate,\n                @ConsumedDate,\n                @Data\n            )\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoConfig_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoConfig_Create]\n    @Id BIGINT OUTPUT,\n    @Enabled BIT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Data NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[SsoConfig]\n    (\n        [Enabled],\n        [OrganizationId],\n        [Data],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Enabled,\n        @OrganizationId,\n        @Data,\n        @CreationDate,\n        @RevisionDate\n    )\n\n    SET @Id = SCOPE_IDENTITY();\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoConfig_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoConfig_DeleteById]\n    @Id BIGINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[SsoConfig]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoConfig_ReadById]\n    @Id BIGINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SsoConfigView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadByIdentifier.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoConfig_ReadByIdentifier]\n    @Identifier NVARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT TOP 1\n        SSO.*\n    FROM\n        [dbo].[SsoConfigView] SSO\n    INNER JOIN\n        [dbo].[Organization] O ON O.[Id] = SSO.[OrganizationId]\n        AND O.[Identifier] = @Identifier\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoConfig_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT TOP 1\n        *\n    FROM\n        [dbo].[SsoConfigView]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadManyByNotBeforeRevisionDate.sql",
    "content": "CREATE PROCEDURE [dbo].[SsoConfig_ReadManyByNotBeforeRevisionDate]\n    @NotBefore DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SsoConfigView]\n    WHERE\n        [Enabled] = 1\n        AND [RevisionDate] >= COALESCE(@NotBefore, [RevisionDate]);\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoConfig_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoConfig_Update]\n    @Id BIGINT,\n    @Enabled BIT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Data NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[SsoConfig]\n    SET\n        [Enabled] = @Enabled,\n        [OrganizationId] = @OrganizationId,\n        [Data] = @Data,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoUser_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoUser_Create]\n    @Id BIGINT OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[SsoUser]\n    (\n        [UserId],\n        [OrganizationId],\n        [ExternalId],\n        [CreationDate]\n    )\n    VALUES\n    (\n        @UserId,\n        @OrganizationId,\n        @ExternalId,\n        @CreationDate\n    )\n\n    SET @Id = SCOPE_IDENTITY();\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoUser_Delete.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoUser_Delete]\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[SsoUser]\n    WHERE\n        [UserId] = @UserId\n        AND [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoUser_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoUser_DeleteById]\n    @Id BIGINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[SsoUser]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoUser_DeleteMany.sql",
    "content": "CREATE PROCEDURE [dbo].[SsoUser_DeleteMany]\n    @UserAndOrganizationIds [dbo].[TwoGuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        Id\n    INTO\n        #SSOIds\n    FROM\n        [dbo].[SsoUser] SU\n    INNER JOIN\n        @UserAndOrganizationIds UOI ON UOI.Id1 = SU.UserId AND UOI.Id2 = SU.OrganizationId\n\n    DECLARE @BatchSize INT = 100\n\n    -- Delete SSO Users\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION SsoUser_DeleteMany_SsoUsers\n\n        DELETE TOP(@BatchSize) SU\n        FROM\n            [dbo].[SsoUser] SU\n        INNER JOIN\n            #SSOIds ON #SSOIds.Id = SU.Id\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION SsoUser_DeleteMany_SsoUsers\n    END\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoUser_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoUser_ReadById]\n    @Id BIGINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SsoUserView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoUser_ReadByUserIdOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoUser_ReadByUserIdOrganizationId]\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SsoUserView]\n    WHERE\n        [UserId] = @UserId\n        AND [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/SsoUser_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[SsoUser_Update]\n    @Id BIGINT OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[SsoUser]\n    SET\n        [UserId] = @UserId,\n        [OrganizationId] = @OrganizationId,\n        [ExternalId] = @ExternalId,\n        [CreationDate] = @CreationDate\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Stored Procedures/User_BumpAccountRevisionDateByEmergencyAccessGranteeId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByEmergencyAccessGranteeId]\n    @EmergencyAccessId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        [dbo].[EmergencyAccess] EA ON EA.[GranteeId] = U.[Id]\n    WHERE\n        EA.[Id] = @EmergencyAccessId\n        AND EA.[Status] = 2 -- Confirmed\nEND"
  },
  {
    "path": "src/Sql/dbo/Auth/Tables/AuthRequest.sql",
    "content": "﻿CREATE TABLE [dbo].[AuthRequest] (\n    [Id]                        UNIQUEIDENTIFIER NOT NULL,\n    [UserId]                    UNIQUEIDENTIFIER NOT NULL,\n    [Type]                      SMALLINT         NOT NULL,\n    [RequestDeviceIdentifier]   NVARCHAR(50)     NOT NULL,\n    [RequestDeviceType]         SMALLINT         NOT NULL,\n    [RequestIpAddress]          VARCHAR(50)      NOT NULL,\n    [ResponseDeviceId]          UNIQUEIDENTIFIER NULL,\n    [AccessCode]                VARCHAR(25)      NOT NULL,\n    [PublicKey]                 VARCHAR(MAX)     NOT NULL,\n    [Key]                       VARCHAR(MAX)     NULL,\n    [MasterPasswordHash]        VARCHAR(MAX)     NULL,\n    [Approved]                  BIT              NULL,\n    [CreationDate]              DATETIME2 (7)    NOT NULL,\n    [ResponseDate]              DATETIME2 (7)    NULL,\n    [AuthenticationDate]        DATETIME2 (7)    NULL,\n    [OrganizationId]            UNIQUEIDENTIFIER NULL,\n    [RequestCountryName]        NVARCHAR(200)     NULL,\n    CONSTRAINT [PK_AuthRequest] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_AuthRequest_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]),\n    CONSTRAINT [FK_AuthRequest_ResponseDevice] FOREIGN KEY ([ResponseDeviceId]) REFERENCES [dbo].[Device] ([Id]),\n    CONSTRAINT [FK_AuthRequest_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\n\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Tables/EmergencyAccess.sql",
    "content": "﻿CREATE TABLE [dbo].[EmergencyAccess]\n(\n\t[Id]                    UNIQUEIDENTIFIER NOT NULL,\n    [GrantorId]             UNIQUEIDENTIFIER NOT NULL,\n    [GranteeId]             UNIQUEIDENTIFIER NULL,\n    [Email]                 NVARCHAR (256)   NULL,\n    [KeyEncrypted]          VARCHAR (MAX)    NULL,\n    [WaitTimeDays]          SMALLINT         NULL,\n    [Type]                  TINYINT          NOT NULL,\n    [Status]                TINYINT          NOT NULL,\n    [RecoveryInitiatedDate] DATETIME2 (7)    NULL,\n    [LastNotificationDate]  DATETIME2 (7)    NULL,\n    [CreationDate]          DATETIME2 (7)    NOT NULL,\n    [RevisionDate]          DATETIME2 (7)    NOT NULL,\n    CONSTRAINT [PK_EmergencyAccess] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_EmergencyAccess_GrantorId] FOREIGN KEY ([GrantorId]) REFERENCES [dbo].[User] ([Id]),\n    CONSTRAINT [FK_EmergencyAccess_GranteeId] FOREIGN KEY ([GranteeId]) REFERENCES [dbo].[User] ([Id])\n)\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Tables/Grant.sql",
    "content": "﻿CREATE TABLE [dbo].[Grant]\n(\n    [Id] INT NOT NULL IDENTITY(1,1),\n    [Key] NVARCHAR (200) NOT NULL,\n    [Type] NVARCHAR (50) NOT NULL,\n    [SubjectId] NVARCHAR (200) NULL,\n    [SessionId] NVARCHAR (100) NULL,\n    [ClientId] NVARCHAR (200) NOT NULL,\n    [Description] NVARCHAR (200) NULL,\n    [CreationDate] DATETIME2 (7) NOT NULL,\n    [ExpirationDate] DATETIME2 (7) NULL,\n    [ConsumedDate] DATETIME2 (7) NULL,\n    [Data] NVARCHAR (MAX) NOT NULL,\n    CONSTRAINT [PK_Grant] PRIMARY KEY CLUSTERED ([Id] ASC)\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Grant_ExpirationDate]\n    ON [dbo].[Grant]([ExpirationDate] ASC);\n\nGO\n\nCREATE UNIQUE INDEX [IX_Grant_Key]\n    ON [dbo].[Grant]([Key]);\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Tables/SsoConfig.sql",
    "content": "﻿CREATE TABLE [dbo].[SsoConfig] (\n    [Id]                 BIGINT              IDENTITY (1, 1) NOT NULL,\n    [Enabled]            BIT                 NOT NULL,\n    [OrganizationId]     UNIQUEIDENTIFIER    NOT NULL,\n    [Data]               NVARCHAR (MAX)      NULL,\n    [CreationDate]       DATETIME2 (7)       NOT NULL,\n    [RevisionDate]       DATETIME2 (7)       NOT NULL,\n    CONSTRAINT [PK_SsoConfig] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_SsoConfig_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Tables/SsoUser.sql",
    "content": "﻿CREATE TABLE [dbo].[SsoUser] (\n    [Id]                BIGINT           IDENTITY (1, 1) NOT NULL,\n    [UserId]            UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId]    UNIQUEIDENTIFIER NULL,\n    [ExternalId]        NVARCHAR(300)    NOT NULL,\n    [CreationDate]      DATETIME2 (7)    NOT NULL,\n    CONSTRAINT [PK_SsoUser] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_SsoUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_SsoUser_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\n\n\nGO\nCREATE UNIQUE NONCLUSTERED INDEX [IX_SsoUser_OrganizationIdExternalId]\n    ON [dbo].[SsoUser]([OrganizationId] ASC, [ExternalId] ASC)\n    INCLUDE ([UserId]);\n\nGO\nCREATE UNIQUE NONCLUSTERED INDEX [IX_SsoUser_OrganizationIdUserId]\n    ON [dbo].[SsoUser]([OrganizationId] ASC, [UserId] ASC);\n\n\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Views/AuthRequestPendingDetailsView.sql",
    "content": "﻿CREATE VIEW [dbo].[AuthRequestPendingDetailsView]\nAS\n    WITH\n        PendingRequests\n        AS\n        (\n            SELECT\n                [AR].*,\n                [D].[Id] AS [DeviceId],\n                ROW_NUMBER() OVER (PARTITION BY [AR].[RequestDeviceIdentifier], [AR].[UserId] ORDER BY [AR].[CreationDate] DESC) AS [rn]\n            FROM [dbo].[AuthRequest] [AR]\n                LEFT JOIN [dbo].[Device] [D]\n                ON [AR].[RequestDeviceIdentifier] = [D].[Identifier]\n                    AND [D].[UserId] = [AR].[UserId]\n            WHERE [AR].[Type] IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock\n        )\n    SELECT\n        [PR].[Id],\n        [PR].[UserId],\n        [PR].[OrganizationId],\n        [PR].[Type],\n        [PR].[RequestDeviceIdentifier],\n        [PR].[RequestDeviceType],\n        [PR].[RequestIpAddress],\n        [PR].[RequestCountryName],\n        [PR].[ResponseDeviceId],\n        [PR].[AccessCode],\n        [PR].[PublicKey],\n        [PR].[Key],\n        [PR].[MasterPasswordHash],\n        [PR].[Approved],\n        [PR].[CreationDate],\n        [PR].[ResponseDate],\n        [PR].[AuthenticationDate],\n        [PR].[DeviceId]\n    FROM [PendingRequests] [PR]\n    WHERE [PR].[rn] = 1\n        AND [PR].[Approved] IS NULL -- since we only want pending requests we only want the most recent that is also approved = null\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Views/AuthRequestView.sql",
    "content": "﻿CREATE VIEW [dbo].[AuthRequestView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[AuthRequest]\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Views/EmergencyAccessDetailsView.sql",
    "content": "﻿CREATE VIEW [dbo].[EmergencyAccessDetailsView]\nAS\nSELECT\n    EA.*,\n    GranteeU.[Name] GranteeName,\n    ISNULL(GranteeU.[Email], EA.[Email]) GranteeEmail,\n    GranteeU.[AvatarColor] GranteeAvatarColor,\n    GrantorU.[Name] GrantorName,\n    GrantorU.[Email] GrantorEmail,\n    GrantorU.[AvatarColor] GrantorAvatarColor\nFROM\n    [dbo].[EmergencyAccess] EA\nLEFT JOIN\n    [dbo].[User] GranteeU ON GranteeU.[Id] = EA.[GranteeId]\nLEFT JOIN\n    [dbo].[User] GrantorU ON GrantorU.[Id] = EA.[GrantorId]"
  },
  {
    "path": "src/Sql/dbo/Auth/Views/GrantView.sql",
    "content": "﻿CREATE VIEW [dbo].[GrantView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Grant]\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Views/SsoConfigView.sql",
    "content": "﻿CREATE VIEW [dbo].[SsoConfigView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[SsoConfig]\n"
  },
  {
    "path": "src/Sql/dbo/Auth/Views/SsoUserView.sql",
    "content": "CREATE VIEW [dbo].[SsoUserView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[SsoUser]\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ProviderId UNIQUEIDENTIFIER,\n    @PlanType TINYINT,\n    @Seats SMALLINT,\n    @MaxStorageGb SMALLINT,\n    @GatewayCustomerId VARCHAR(50),\n    @GatewaySubscriptionId VARCHAR(50),\n    @ExpirationDate DATETIME2(7),\n    @MaxAutoscaleSeats INT,\n    @Status TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[ClientOrganizationMigrationRecord]\n    (\n        [Id],\n        [OrganizationId],\n        [ProviderId],\n        [PlanType],\n        [Seats],\n        [MaxStorageGb],\n        [GatewayCustomerId],\n        [GatewaySubscriptionId],\n        [ExpirationDate],\n        [MaxAutoscaleSeats],\n        [Status]\n    )\n    VALUES\n    (\n        @Id,\n        @OrganizationId,\n        @ProviderId,\n        @PlanType,\n        @Seats,\n        @MaxStorageGb,\n        @GatewayCustomerId,\n        @GatewaySubscriptionId,\n        @ExpirationDate,\n        @MaxAutoscaleSeats,\n        @Status\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[ClientOrganizationMigrationRecord]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ClientOrganizationMigrationRecordView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_ReadByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ClientOrganizationMigrationRecordView]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_ReadByProviderId.sql",
    "content": "CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_ReadByProviderId]\n    @ProviderId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ClientOrganizationMigrationRecordView]\n    WHERE\n        [ProviderId] = @ProviderId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[ClientOrganizationMigrationRecord_Update]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ProviderId UNIQUEIDENTIFIER,\n    @PlanType TINYINT,\n    @Seats SMALLINT,\n    @MaxStorageGb SMALLINT,\n    @GatewayCustomerId VARCHAR(50),\n    @GatewaySubscriptionId VARCHAR(50),\n    @ExpirationDate DATETIME2(7),\n    @MaxAutoscaleSeats INT,\n    @Status TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[ClientOrganizationMigrationRecord]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [ProviderId] = @ProviderId,\n        [PlanType] = @PlanType,\n        [Seats] = @Seats,\n        [MaxStorageGb] = @MaxStorageGb,\n        [GatewayCustomerId] = @GatewayCustomerId,\n        [GatewaySubscriptionId] = @GatewaySubscriptionId,\n        [ExpirationDate] = @ExpirationDate,\n        [MaxAutoscaleSeats] = @MaxAutoscaleSeats,\n        [Status] = @Status\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationInstallation_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @InstallationId UNIQUEIDENTIFIER,\n    @CreationDate DATETIME2 (7),\n    @RevisionDate DATETIME2 (7) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationInstallation]\n    (\n        [Id],\n        [OrganizationId],\n        [InstallationId],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n     @Id,\n     @OrganizationId,\n     @InstallationId,\n     @CreationDate,\n     @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationInstallation_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[OrganizationInstallation]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationInstallationView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_ReadByInstallationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByInstallationId]\n    @InstallationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationInstallationView]\n    WHERE\n        [InstallationId] = @InstallationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_ReadByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationInstallationView]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationInstallation_Update]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @InstallationId UNIQUEIDENTIFIER,\n    @CreationDate DATETIME2 (7),\n    @RevisionDate DATETIME2 (7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationInstallation]\n    SET\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderInvoiceItem_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @ProviderId UNIQUEIDENTIFIER,\n    @InvoiceId VARCHAR (50),\n    @InvoiceNumber VARCHAR (50),\n    @ClientName NVARCHAR (50),\n    @PlanName NVARCHAR (50),\n    @AssignedSeats INT,\n    @UsedSeats INT,\n    @Total MONEY,\n    @Created DATETIME2 (7) = NULL,\n    @ClientId UNIQUEIDENTIFIER = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SET @Created = COALESCE(@Created, GETUTCDATE())\n\n    INSERT INTO [dbo].[ProviderInvoiceItem]\n    (\n        [Id],\n        [ProviderId],\n        [InvoiceId],\n        [InvoiceNumber],\n        [ClientName],\n        [PlanName],\n        [AssignedSeats],\n        [UsedSeats],\n        [Total],\n        [Created],\n        [ClientId]\n    )\n    VALUES\n    (\n        @Id,\n        @ProviderId,\n        @InvoiceId,\n        @InvoiceNumber,\n        @ClientName,\n        @PlanName,\n        @AssignedSeats,\n        @UsedSeats,\n        @Total,\n        @Created,\n        @ClientId\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderInvoiceItem_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[ProviderInvoiceItem]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderInvoiceItem_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderInvoiceItemView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_ReadByInvoiceId.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderInvoiceItem_ReadByInvoiceId]\n    @InvoiceId VARCHAR (50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderInvoiceItemView]\n    WHERE\n        [InvoiceId] = @InvoiceId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_ReadByProviderId.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderInvoiceItem_ReadByProviderId]\n    @ProviderId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderInvoiceItemView]\n    WHERE\n        [ProviderId] = @ProviderId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderInvoiceItem_Update]\n    @Id UNIQUEIDENTIFIER,\n    @ProviderId UNIQUEIDENTIFIER,\n    @InvoiceId VARCHAR (50),\n    @InvoiceNumber VARCHAR (50),\n    @ClientName NVARCHAR (50),\n    @PlanName NVARCHAR (50),\n    @AssignedSeats INT,\n    @UsedSeats INT,\n    @Total MONEY,\n    @Created DATETIME2 (7) = NULL,\n    @ClientId UNIQUEIDENTIFIER = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SET @Created = COALESCE(@Created, GETUTCDATE())\n\n    UPDATE\n        [dbo].[ProviderInvoiceItem]\n    SET\n        [ProviderId] = @ProviderId,\n        [InvoiceId] = @InvoiceId,\n        [InvoiceNumber] = @InvoiceNumber,\n        [ClientName] = @ClientName,\n        [PlanName] = @PlanName,\n        [AssignedSeats] = @AssignedSeats,\n        [UsedSeats] = @UsedSeats,\n        [Total] = @Total,\n        [Created] = @Created,\n        [ClientId] = @ClientId\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderPlan_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @ProviderId UNIQUEIDENTIFIER,\n    @PlanType TINYINT,\n    @SeatMinimum INT,\n    @PurchasedSeats INT,\n    @AllocatedSeats INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[ProviderPlan]\n    (\n        [Id],\n        [ProviderId],\n        [PlanType],\n        [SeatMinimum],\n        [PurchasedSeats],\n        [AllocatedSeats]\n    )\n    VALUES\n    (\n        @Id,\n        @ProviderId,\n        @PlanType,\n        @SeatMinimum,\n        @PurchasedSeats,\n        @AllocatedSeats\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderPlan_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[ProviderPlan]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderPlan_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderPlanView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_ReadByProviderId.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderPlan_ReadByProviderId]\n    @ProviderId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderPlanView]\n    WHERE\n        [ProviderId] = @ProviderId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderPlan_Update]\n    @Id UNIQUEIDENTIFIER,\n    @ProviderId UNIQUEIDENTIFIER,\n    @PlanType TINYINT,\n    @SeatMinimum INT,\n    @PurchasedSeats INT,\n    @AllocatedSeats INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[ProviderPlan]\n    SET\n        [ProviderId] = @ProviderId,\n        [PlanType] = @PlanType,\n        [SeatMinimum] = @SeatMinimum,\n        [PurchasedSeats] = @PurchasedSeats,\n        [AllocatedSeats] = @AllocatedSeats\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Tables/ClientOrganizationMigrationRecord.sql",
    "content": "CREATE TABLE [dbo].[ClientOrganizationMigrationRecord] (\n    [Id] UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NOT NULL,\n    [ProviderId] UNIQUEIDENTIFIER NOT NULL,\n    [PlanType] TINYINT NOT NULL,\n    [Seats] SMALLINT NOT NULL,\n    [MaxStorageGb] SMALLINT NULL,\n    [GatewayCustomerId] VARCHAR(50) NOT NULL,\n    [GatewaySubscriptionId] VARCHAR(50) NOT NULL,\n    [ExpirationDate] DATETIME2(7) NULL,\n    [MaxAutoscaleSeats] INT NULL,\n    [Status] TINYINT NOT NULL,\n    CONSTRAINT [PK_ClientOrganizationMigrationRecord] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [PK_OrganizationIdProviderId] UNIQUE ([ProviderId], [OrganizationId])\n);\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Tables/OrganizationInstallation.sql",
    "content": "CREATE TABLE [dbo].[OrganizationInstallation] (\n    [Id]             UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NOT NULL,\n    [InstallationId] UNIQUEIDENTIFIER NOT NULL,\n    [CreationDate]   DATETIME2 (7) NOT NULL,\n    [RevisionDate]   DATETIME2 (7) NULL,\n    CONSTRAINT [PK_OrganizationInstallation] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganizationInstallation_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_OrganizationInstallation_Installation] FOREIGN KEY ([InstallationId]) REFERENCES [dbo].[Installation] ([Id]) ON DELETE CASCADE\n);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_OrganizationId]\n    ON [dbo].[OrganizationInstallation]([OrganizationId] ASC);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_InstallationId]\n    ON [dbo].[OrganizationInstallation]([InstallationId] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Tables/ProviderInvoiceItem.sql",
    "content": "CREATE TABLE [dbo].[ProviderInvoiceItem] (\n    [Id]             UNIQUEIDENTIFIER NOT NULL,\n    [ProviderId]     UNIQUEIDENTIFIER NOT NULL,\n    [InvoiceId]      VARCHAR (50) NOT NULL,\n    [InvoiceNumber]  VARCHAR (50) NULL,\n    [ClientName]     NVARCHAR (50) NOT NULL,\n    [PlanName]       NVARCHAR (50) NOT NULL,\n    [AssignedSeats]  INT NOT NULL,\n    [UsedSeats]      INT NOT NULL,\n    [Total]          MONEY NOT NULL,\n    [Created]        DATETIME2 (7) NOT NULL,\n    [ClientId]       UNIQUEIDENTIFIER NULL,\n    CONSTRAINT [PK_ProviderInvoiceItem] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_ProviderInvoiceItem_Provider] FOREIGN KEY ([ProviderId]) REFERENCES [dbo].[Provider] ([Id]) ON DELETE CASCADE\n);\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Tables/ProviderPlan.sql",
    "content": "CREATE TABLE [dbo].[ProviderPlan] (\n    [Id]             UNIQUEIDENTIFIER NOT NULL,\n    [ProviderId]     UNIQUEIDENTIFIER NOT NULL,\n    [PlanType]       TINYINT          NOT NULL,\n    [SeatMinimum]    INT              NULL,\n    [PurchasedSeats] INT              NULL,\n    [AllocatedSeats] INT              NULL,\n    CONSTRAINT [PK_ProviderPlan] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_ProviderPlan_Provider] FOREIGN KEY ([ProviderId]) REFERENCES [dbo].[Provider] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [PK_ProviderPlanType] UNIQUE ([ProviderId], [PlanType])\n);\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Views/ClientOrganizationMigrationRecordView.sql",
    "content": "CREATE VIEW [dbo].[ClientOrganizationMigrationRecordView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[ClientOrganizationMigrationRecord]\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Views/OrganizationInstallationView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationInstallationView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[OrganizationInstallation];\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Views/ProviderInvoiceItemView.sql",
    "content": "CREATE VIEW [dbo].[ProviderInvoiceItemView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[ProviderInvoiceItem]\n"
  },
  {
    "path": "src/Sql/dbo/Billing/Views/ProviderPlanView.sql",
    "content": "CREATE VIEW [dbo].[ProviderPlanView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[ProviderPlan]\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/Event_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Event_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @Type INT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @InstallationId UNIQUEIDENTIFIER,\n    @ProviderId UNIQUEIDENTIFIER,\n    @CipherId UNIQUEIDENTIFIER,\n    @CollectionId UNIQUEIDENTIFIER,\n    @PolicyId UNIQUEIDENTIFIER,\n    @GroupId UNIQUEIDENTIFIER,\n    @OrganizationUserId UNIQUEIDENTIFIER,\n    @ProviderUserId UNIQUEIDENTIFIER,\n    @ProviderOrganizationId UNIQUEIDENTIFIER = null,\n    @ActingUserId UNIQUEIDENTIFIER,\n    @DeviceType SMALLINT,\n    @IpAddress VARCHAR(50),\n    @Date DATETIME2(7),\n    @SystemUser TINYINT = null,\n    @DomainName VARCHAR(256),\n    @SecretId UNIQUEIDENTIFIER = null,\n    @ServiceAccountId UNIQUEIDENTIFIER = null,\n    @ProjectId UNIQUEIDENTIFIER = null,\n    @GrantedServiceAccountId UNIQUEIDENTIFIER = null\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Event]\n    (\n        [Id],\n        [Type],\n        [UserId],\n        [OrganizationId],\n        [InstallationId],\n        [ProviderId],\n        [CipherId],\n        [CollectionId],\n        [PolicyId],\n        [GroupId],\n        [OrganizationUserId],\n        [ProviderUserId],\n        [ProviderOrganizationId],\n        [ActingUserId],\n        [DeviceType],\n        [IpAddress],\n        [Date],\n        [SystemUser],\n        [DomainName],\n        [SecretId],\n        [ServiceAccountId],\n        [ProjectId],\n        [GrantedServiceAccountId]\n    )\n    VALUES\n    (\n        @Id,\n        @Type,\n        @UserId,\n        @OrganizationId,\n        @InstallationId,\n        @ProviderId,\n        @CipherId,\n        @CollectionId,\n        @PolicyId,\n        @GroupId,\n        @OrganizationUserId,\n        @ProviderUserId,\n        @ProviderOrganizationId,\n        @ActingUserId,\n        @DeviceType,\n        @IpAddress,\n        @Date,\n        @SystemUser,\n        @DomainName,\n        @SecretId,\n        @ServiceAccountId,\n        @ProjectId,\n        @GrantedServiceAccountId\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/Event_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Event_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[Event]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByCipherId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Event_ReadPageByCipherId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @CipherId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EventView]\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND (\n            (@OrganizationId IS NULL AND [OrganizationId] IS NULL)\n            OR (@OrganizationId IS NOT NULL AND [OrganizationId] = @OrganizationId)\n        )\n        AND (\n            (@UserId IS NULL AND [UserId] IS NULL)\n            OR (@UserId IS NOT NULL AND [UserId] = @UserId)\n        )\n        AND [CipherId]  = @CipherId\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Event_ReadPageByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EventView]\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND [OrganizationId] = @OrganizationId\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByOrganizationIdActingUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Event_ReadPageByOrganizationIdActingUserId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ActingUserId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EventView]\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND [OrganizationId] = @OrganizationId\n        AND [ActingUserId] = @ActingUserId\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByProviderId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Event_ReadPageByProviderId]\n    @ProviderId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EventView]\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND [ProviderId] = @ProviderId\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByProviderIdActingUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Event_ReadPageByProviderIdActingUserId]\n    @ProviderId UNIQUEIDENTIFIER,\n    @ActingUserId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EventView]\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND [ProviderId] = @ProviderId\n        AND [ActingUserId] = @ActingUserId\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/Event_ReadPageByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Event_ReadPageByUserId]\n    @UserId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EventView]\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND [OrganizationId] IS NULL\n        AND [ActingUserId] = @UserId\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql",
    "content": "CREATE PROCEDURE dbo.MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId\n    @OrganizationId UNIQUEIDENTIFIER\nAS\n    SET NOCOUNT ON;\n\nIF @OrganizationId IS NULL\n        THROW 50000, 'OrganizationId cannot be null', 1;\n\n    SELECT\n        OU.Id AS UserGuid,\n        U.Name AS UserName,\n        ISNULL(U.Email, OU.Email) as 'Email',\n        U.TwoFactorProviders,\n        U.UsesKeyConnector,\n        OU.ResetPasswordKey,\n        CC.CollectionId,\n        C.Name AS CollectionName,\n        NULL AS GroupId,\n        NULL AS GroupName,\n        CU.ReadOnly,\n        CU.HidePasswords,\n        CU.Manage,\n        Cipher.Id AS CipherId\n    FROM dbo.OrganizationUser OU\n            LEFT JOIN dbo.[User] U ON U.Id = OU.UserId\n        INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId\n        AND O.Id = @OrganizationId\n        AND O.Enabled = 1\n        INNER JOIN dbo.CollectionUser CU ON CU.OrganizationUserId = OU.Id\n        INNER JOIN dbo.Collection C ON C.Id = CU.CollectionId and C.OrganizationId = @OrganizationId\n        INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id\n        INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId AND Cipher.OrganizationId = @OrganizationId\n    WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users\n        AND Cipher.DeletedDate IS NULL\nUNION ALL\n    -- Group-based collection permissions\n    SELECT\n        OU.Id AS UserGuid,\n        U.Name AS UserName,\n        ISNULL(U.Email, OU.Email) as 'Email',\n        U.TwoFactorProviders,\n        U.UsesKeyConnector,\n        OU.ResetPasswordKey,\n        CC.CollectionId,\n        C.Name AS CollectionName,\n        G.Id AS GroupId,\n        G.Name AS GroupName,\n        CG.ReadOnly,\n        CG.HidePasswords,\n        CG.Manage,\n        Cipher.Id AS CipherId\n    FROM dbo.OrganizationUser OU\n        LEFT JOIN dbo.[User] U ON U.Id = OU.UserId\n        INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId\n        AND O.Id = @OrganizationId\n        AND O.Enabled = 1\n        INNER JOIN dbo.GroupUser GU ON GU.OrganizationUserId = OU.Id\n        INNER JOIN dbo.[Group] G ON G.Id = GU.GroupId\n        INNER JOIN dbo.CollectionGroup CG ON CG.GroupId = G.Id\n        INNER JOIN dbo.Collection C ON C.Id = CG.CollectionId AND C.OrganizationId = @OrganizationId\n        INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id\n        INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId and Cipher.OrganizationId = @OrganizationId\n    WHERE OU.Status IN (0,1,2)  -- Invited, Accepted and Confirmed Users\n        AND Cipher.DeletedDate IS NULL\nUNION ALL\n    -- Users without collection access (invited users)\n    -- typically invited users who have not yet accepted the invitation\n    -- and not yet assigned to any collection\n    SELECT\n        OU.Id AS UserGuid,\n        U.Name AS UserName,\n        ISNULL(U.Email, OU.Email) as 'Email',\n        U.TwoFactorProviders,\n        U.UsesKeyConnector,\n        OU.ResetPasswordKey,\n        null as CollectionId,\n        null AS CollectionName,\n        NULL AS GroupId,\n        NULL AS GroupName,\n        null as [ReadOnly],\n        null as HidePasswords,\n        null as Manage,\n        null  AS CipherId\n    FROM dbo.OrganizationUser OU\n            LEFT JOIN dbo.[User] U ON U.Id = OU.UserId\n        INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId AND O.Id = @OrganizationId AND O.Enabled = 1\n    WHERE OU.Status IN (0,1,2)  -- Invited, Accepted and Confirmed Users\n        AND OU.Id not in (\n            select OU1.Id from dbo.OrganizationUser OU1\n                inner join dbo.CollectionUser CU1 on CU1.OrganizationUserId = OU1.Id\n            WHERE OU1.OrganizationId = @organizationId\n        )\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApplication_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Applications NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @ContentEncryptionKey VARCHAR(MAX)\nAS\n    SET NOCOUNT ON;\n\n    INSERT INTO [dbo].[OrganizationApplication]\n    (\n        [Id],\n        [OrganizationId],\n        [Applications],\n        [CreationDate],\n        [RevisionDate],\n        [ContentEncryptionKey]\n    )\n    VALUES\n    (\n        @Id,\n        @OrganizationId,\n        @Applications,\n        @CreationDate,\n        @RevisionDate,\n        @ContentEncryptionKey\n    );\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApplication_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\n    SET NOCOUNT ON;\n\n    DELETE FROM [dbo].[OrganizationApplication]\n    WHERE [Id] = @Id;\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApplication_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\n    SET NOCOUNT ON;\n\n    SELECT\n        *\n    FROM [dbo].[OrganizationApplicationView]\n    WHERE [Id] = @Id;\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_ReadByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApplication_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\n    SET NOCOUNT ON;\n\n    SELECT\n        *\n    FROM [dbo].[OrganizationApplicationView]\n    WHERE [OrganizationId] = @OrganizationId;\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApplication_Update]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Applications NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\n    SET NOCOUNT ON;\n    \n    UPDATE [dbo].[OrganizationApplication]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [Applications] = @Applications,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE [Id] = @Id;\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_Create]\n   @Id UNIQUEIDENTIFIER OUTPUT,\n   @OrganizationId UNIQUEIDENTIFIER,\n   @ReportData NVARCHAR(MAX),\n   @CreationDate DATETIME2(7),\n   @ContentEncryptionKey VARCHAR(MAX),\n   @SummaryData NVARCHAR(MAX),\n   @ApplicationData NVARCHAR(MAX),\n   @RevisionDate DATETIME2(7),\n   @ApplicationCount INT = NULL,\n   @ApplicationAtRiskCount INT = NULL,\n   @CriticalApplicationCount INT = NULL,\n   @CriticalApplicationAtRiskCount INT = NULL,\n   @MemberCount INT = NULL,\n   @MemberAtRiskCount INT = NULL,\n   @CriticalMemberCount INT = NULL,\n   @CriticalMemberAtRiskCount INT = NULL,\n   @PasswordCount INT = NULL,\n   @PasswordAtRiskCount INT = NULL,\n   @CriticalPasswordCount INT = NULL,\n   @CriticalPasswordAtRiskCount INT = NULL,\n   @ReportFile NVARCHAR(MAX) = NULL\nAS\nBEGIN\n   SET NOCOUNT ON;\n\n\nINSERT INTO [dbo].[OrganizationReport](\n    [Id],\n    [OrganizationId],\n    [ReportData],\n    [CreationDate],\n    [ContentEncryptionKey],\n    [SummaryData],\n    [ApplicationData],\n    [RevisionDate],\n    [ApplicationCount],\n    [ApplicationAtRiskCount],\n    [CriticalApplicationCount],\n    [CriticalApplicationAtRiskCount],\n    [MemberCount],\n    [MemberAtRiskCount],\n    [CriticalMemberCount],\n    [CriticalMemberAtRiskCount],\n    [PasswordCount],\n    [PasswordAtRiskCount],\n    [CriticalPasswordCount],\n    [CriticalPasswordAtRiskCount],\n    [ReportFile]\n)\nVALUES (\n    @Id,\n    @OrganizationId,\n    @ReportData,\n    @CreationDate,\n    @ContentEncryptionKey,\n    @SummaryData,\n    @ApplicationData,\n    @RevisionDate,\n    @ApplicationCount,\n    @ApplicationAtRiskCount,\n    @CriticalApplicationCount,\n    @CriticalApplicationAtRiskCount,\n    @MemberCount,\n    @MemberAtRiskCount,\n    @CriticalMemberCount,\n    @CriticalMemberAtRiskCount,\n    @PasswordCount,\n    @PasswordAtRiskCount,\n    @CriticalPasswordCount,\n    @CriticalPasswordAtRiskCount,\n    @ReportFile\n    );\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\n    SET NOCOUNT ON;\n\n    DELETE FROM [dbo].[OrganizationReport]\n    WHERE [Id] = @Id\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetApplicationDataById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_GetApplicationDataById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [ApplicationData]\n    FROM [dbo].[OrganizationReportView]\n    WHERE [Id] = @Id\nEND\n\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetLatestByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_GetLatestByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT TOP 1\n        *\n    FROM [dbo].[OrganizationReportView]\n    WHERE [OrganizationId] = @OrganizationId\n    ORDER BY [RevisionDate] DESC\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetReportDataById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_GetReportDataById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [ReportData]\n    FROM [dbo].[OrganizationReportView]\n    WHERE [Id] = @Id\nEND\n\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetSummaryDataById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_GetSummaryDataById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [SummaryData]\n    FROM [dbo].[OrganizationReportView]\n    WHERE [Id] = @Id\nEND\n\n\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\n    SET NOCOUNT ON;\n\n    SELECT\n        *\n    FROM [dbo].[OrganizationReportView]\n    WHERE [Id] = @Id;\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadByOrganizationIdAndRevisionDate.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_ReadByOrganizationIdAndRevisionDate]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [OrganizationId],\n        [ContentEncryptionKey],\n        [SummaryData],\n        [RevisionDate]\n    FROM \n        [dbo].[OrganizationReportView]\n    WHERE\n        [OrganizationId] = @OrganizationId\n        AND [RevisionDate] >= @StartDate\n        AND [RevisionDate] <= @EndDate\n    ORDER BY\n        [RevisionDate] DESC\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_Update]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ReportData NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @ContentEncryptionKey VARCHAR(MAX),\n    @SummaryData NVARCHAR(MAX),\n    @ApplicationData NVARCHAR(MAX),\n    @RevisionDate DATETIME2(7),\n    @ApplicationCount INT = NULL,\n    @ApplicationAtRiskCount INT = NULL,\n    @CriticalApplicationCount INT = NULL,\n    @CriticalApplicationAtRiskCount INT = NULL,\n    @MemberCount INT = NULL,\n    @MemberAtRiskCount INT = NULL,\n    @CriticalMemberCount INT = NULL,\n    @CriticalMemberAtRiskCount INT = NULL,\n    @PasswordCount INT = NULL,\n    @PasswordAtRiskCount INT = NULL,\n    @CriticalPasswordCount INT = NULL,\n    @CriticalPasswordAtRiskCount INT = NULL,\n    @ReportFile NVARCHAR(MAX) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON;\n    UPDATE [dbo].[OrganizationReport]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [ReportData] = @ReportData,\n        [CreationDate] = @CreationDate,\n        [ContentEncryptionKey] = @ContentEncryptionKey,\n        [SummaryData] = @SummaryData,\n        [ApplicationData] = @ApplicationData,\n        [RevisionDate] = @RevisionDate,\n        [ApplicationCount] = @ApplicationCount,\n        [ApplicationAtRiskCount] = @ApplicationAtRiskCount,\n        [CriticalApplicationCount] = @CriticalApplicationCount,\n        [CriticalApplicationAtRiskCount] = @CriticalApplicationAtRiskCount,\n        [MemberCount] = @MemberCount,\n        [MemberAtRiskCount] = @MemberAtRiskCount,\n        [CriticalMemberCount] = @CriticalMemberCount,\n        [CriticalMemberAtRiskCount] = @CriticalMemberAtRiskCount,\n        [PasswordCount] = @PasswordCount,\n        [PasswordAtRiskCount] = @PasswordAtRiskCount,\n        [CriticalPasswordCount] = @CriticalPasswordCount,\n        [CriticalPasswordAtRiskCount] = @CriticalPasswordAtRiskCount,\n        [ReportFile] = @ReportFile\n    WHERE [Id] = @Id;\nEND;\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateApplicationData.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_UpdateApplicationData]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ApplicationData NVARCHAR(MAX),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    UPDATE [dbo].[OrganizationReport]\n    SET\n        [ApplicationData] = @ApplicationData,\n        [RevisionDate] = @RevisionDate\n    WHERE [Id] = @Id\n      AND [OrganizationId] = @OrganizationId;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateMetrics.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_UpdateMetrics]\n    @Id UNIQUEIDENTIFIER,\n    @ApplicationCount INT,\n    @ApplicationAtRiskCount INT,\n    @CriticalApplicationCount INT,\n    @CriticalApplicationAtRiskCount INT,\n    @MemberCount INT,\n    @MemberAtRiskCount INT,\n    @CriticalMemberCount INT,\n    @CriticalMemberAtRiskCount INT,\n    @PasswordCount INT,\n    @PasswordAtRiskCount INT,\n    @CriticalPasswordCount INT,\n    @CriticalPasswordAtRiskCount INT,\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    UPDATE \n        [dbo].[OrganizationReport]\n    SET\n        [ApplicationCount] = @ApplicationCount,\n        [ApplicationAtRiskCount] = @ApplicationAtRiskCount,\n        [CriticalApplicationCount] = @CriticalApplicationCount,\n        [CriticalApplicationAtRiskCount] = @CriticalApplicationAtRiskCount,\n        [MemberCount] = @MemberCount,\n        [MemberAtRiskCount] = @MemberAtRiskCount,\n        [CriticalMemberCount] = @CriticalMemberCount,\n        [CriticalMemberAtRiskCount] = @CriticalMemberAtRiskCount,\n        [PasswordCount] = @PasswordCount,\n        [PasswordAtRiskCount] = @PasswordAtRiskCount,\n        [CriticalPasswordCount] = @CriticalPasswordCount,\n        [CriticalPasswordAtRiskCount] = @CriticalPasswordAtRiskCount,\n        [RevisionDate] = @RevisionDate\n    WHERE \n        [Id] = @Id\n\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateReportData.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_UpdateReportData]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ReportData NVARCHAR(MAX),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    UPDATE [dbo].[OrganizationReport]\n    SET\n        [ReportData] = @ReportData,\n        [RevisionDate] = @RevisionDate\n    WHERE [Id] = @Id\n      AND [OrganizationId] = @OrganizationId;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateSummaryData.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationReport_UpdateSummaryData]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @SummaryData NVARCHAR(MAX),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    UPDATE [dbo].[OrganizationReport]\n    SET\n        [SummaryData] = @SummaryData,\n        [RevisionDate] = @RevisionDate\n    WHERE [Id] = @Id\n      AND [OrganizationId] = @OrganizationId;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Tables/Event.sql",
    "content": "﻿CREATE TABLE [dbo].[Event] (\n    [Id]                     UNIQUEIDENTIFIER NOT NULL,\n    [Type]                   INT              NOT NULL,\n    [UserId]                 UNIQUEIDENTIFIER NULL,\n    [OrganizationId]         UNIQUEIDENTIFIER NULL,\n    [InstallationId]         UNIQUEIDENTIFIER NULL,\n    [CipherId]               UNIQUEIDENTIFIER NULL,\n    [CollectionId]           UNIQUEIDENTIFIER NULL,\n    [PolicyId]               UNIQUEIDENTIFIER NULL,\n    [GroupId]                UNIQUEIDENTIFIER NULL,\n    [OrganizationUserId]     UNIQUEIDENTIFIER NULL,\n    [ActingUserId]           UNIQUEIDENTIFIER NULL,\n    [DeviceType]             SMALLINT         NULL,\n    [IpAddress]              VARCHAR(50)      NULL,\n    [Date]                   DATETIME2 (7)    NOT NULL,\n    [ProviderId]             UNIQUEIDENTIFIER NULL,\n    [ProviderUserId]         UNIQUEIDENTIFIER NULL,\n    [ProviderOrganizationId] UNIQUEIDENTIFIER NULL,\n    [SystemUser]             TINYINT          NULL,\n    [DomainName]             VARCHAR(256)     NULL,\n    [SecretId]               UNIQUEIDENTIFIER NULL,\n    [ServiceAccountId]       UNIQUEIDENTIFIER NULL,\n    [ProjectId]              UNIQUEIDENTIFIER NULL,\n    [GrantedServiceAccountId] UNIQUEIDENTIFIER NULL,\n    CONSTRAINT [PK_Event] PRIMARY KEY CLUSTERED ([Id] ASC)\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Event_DateOrganizationIdUserId]\n    ON [dbo].[Event]([Date] DESC, [OrganizationId] ASC, [ActingUserId] ASC, [CipherId] ASC) INCLUDE ([ServiceAccountId], [GrantedServiceAccountId]);\n\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Tables/OrganizationApplication.sql",
    "content": "CREATE TABLE [dbo].[OrganizationApplication] (\n    [Id]                       UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId]           UNIQUEIDENTIFIER NOT NULL,\n    [Applications]             NVARCHAR(MAX)    NOT NULL,\n    [CreationDate]             DATETIME2 (7)    NOT NULL,\n    [RevisionDate]             DATETIME2 (7)    NOT NULL,\n    [ContentEncryptionKey]     VARCHAR(MAX)     NOT NULL,\n    CONSTRAINT [PK_OrganizationApplication] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganizationApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationApplication_OrganizationId]\n    ON [dbo].[OrganizationApplication]([OrganizationId] ASC);\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Tables/OrganizationReport.sql",
    "content": "CREATE TABLE [dbo].[OrganizationReport] (\n    [Id]                                UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId]                    UNIQUEIDENTIFIER NOT NULL,\n    [ReportData]                        NVARCHAR(MAX)    NOT NULL,\n    [CreationDate]                      DATETIME2 (7)    NOT NULL,\n    [ContentEncryptionKey]              VARCHAR(MAX)     NOT NULL,\n    [SummaryData]                       NVARCHAR(MAX)    NULL,\n    [ApplicationData]                   NVARCHAR(MAX)    NULL,\n    [RevisionDate]                      DATETIME2 (7)    NULL,\n    [ApplicationCount]                  INT              NULL,\n    [ApplicationAtRiskCount]            INT              NULL,\n    [CriticalApplicationCount]          INT              NULL,\n    [CriticalApplicationAtRiskCount]    INT              NULL,\n    [MemberCount]                       INT              NULL,\n    [MemberAtRiskCount]                 INT              NULL,\n    [CriticalMemberCount]               INT              NULL,\n    [CriticalMemberAtRiskCount]         INT              NULL,\n    [PasswordCount]                     INT              NULL,\n    [PasswordAtRiskCount]               INT              NULL,\n    [CriticalPasswordCount]             INT              NULL,\n    [CriticalPasswordAtRiskCount]       INT              NULL,\n    [ReportFile]                        NVARCHAR(MAX)    NULL,\n    CONSTRAINT [PK_OrganizationReport] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganizationReport_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n    );\nGO\n\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId]\n   ON [dbo].[OrganizationReport] ([OrganizationId] ASC);\nGO\n\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId_RevisionDate]\n   ON [dbo].[OrganizationReport]([OrganizationId] ASC, [RevisionDate] DESC);\nGO\n\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Views/EventView.sql",
    "content": "﻿CREATE VIEW [dbo].[EventView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Event]"
  },
  {
    "path": "src/Sql/dbo/Dirt/Views/OrganizationApplicationView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationApplicationView]\nAS\nSELECT\n    *\nFROM [dbo].[OrganizationApplication];\n"
  },
  {
    "path": "src/Sql/dbo/Dirt/Views/OrganizationReportView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationReportView] AS\nSELECT * FROM [dbo].[OrganizationReport];\n"
  },
  {
    "path": "src/Sql/dbo/Functions/UserCollectionDetails.sql",
    "content": "﻿CREATE FUNCTION [dbo].[UserCollectionDetails](@UserId UNIQUEIDENTIFIER)\nRETURNS TABLE\nAS RETURN\nSELECT\n    C.*,\n    CASE\n        WHEN\n            COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0\n        THEN 0\n        ELSE 1\n    END [ReadOnly],\n    CASE\n        WHEN\n            COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0\n        THEN 0\n        ELSE 1\n    END [HidePasswords],\n    CASE\n        WHEN\n            COALESCE(CU.[Manage], CG.[Manage], 0) = 0\n        THEN 0\n        ELSE 1\n    END [Manage]\nFROM\n    [dbo].[CollectionView] C\nINNER JOIN\n    [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId]\nINNER JOIN\n    [dbo].[Organization] O ON O.[Id] = C.[OrganizationId]\nLEFT JOIN\n    [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id]\nLEFT JOIN\n    [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\nLEFT JOIN\n    [dbo].[Group] G ON G.[Id] = GU.[GroupId]\nLEFT JOIN\n    [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]\nWHERE\n    OU.[UserId] = @UserId\n    AND OU.[Status] = 2 -- 2 = Confirmed\n    AND O.[Enabled] = 1\n    AND (\n        CU.[CollectionId] IS NOT NULL\n        OR CG.[CollectionId] IS NOT NULL\n    )\n"
  },
  {
    "path": "src/Sql/dbo/KeyManagement/Stored Procedures/UserAsymmetricKeys_Regenerate.sql",
    "content": "CREATE PROCEDURE [dbo].[UserAsymmetricKeys_Regenerate]\n    @UserId UNIQUEIDENTIFIER,\n    @PublicKey VARCHAR(MAX),\n    @PrivateKey VARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n    DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();\n\n    UPDATE [dbo].[User]\n    SET [PublicKey] = @PublicKey,\n        [PrivateKey] = @PrivateKey,\n        [RevisionDate] = @UtcNow,\n        [AccountRevisionDate] = @UtcNow\n    WHERE [Id] = @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_ReadByUserId.sql",
    "content": "CREATE PROCEDURE [dbo].[UserSignatureKeyPair_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserSignatureKeyPairView]\n    WHERE\n        [UserId] = @UserId;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_SetForRotation.sql",
    "content": "CREATE PROCEDURE [dbo].[UserSignatureKeyPair_SetForRotation]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @SignatureAlgorithm TINYINT,\n    @SigningKey VARCHAR(MAX),\n    @VerifyingKey VARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    INSERT INTO [dbo].[UserSignatureKeyPair] \n    (\n        [Id], \n        [UserId], \n        [SignatureAlgorithm], \n        [SigningKey], \n        [VerifyingKey], \n        [CreationDate], \n        [RevisionDate]\n    )\n    VALUES \n    (\n        @Id, \n        @UserId, \n        @SignatureAlgorithm, \n        @SigningKey, \n        @VerifyingKey, \n        @CreationDate, \n        @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_UpdateForRotation.sql",
    "content": "CREATE PROCEDURE [dbo].[UserSignatureKeyPair_UpdateForRotation]\n    @UserId UNIQUEIDENTIFIER,\n    @SignatureAlgorithm TINYINT,\n    @SigningKey VARCHAR(MAX),\n    @VerifyingKey VARCHAR(MAX),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON;\n    UPDATE  \n        [dbo].[UserSignatureKeyPair]  \n    SET  \n        [SignatureAlgorithm] = @SignatureAlgorithm,  \n        [SigningKey] = @SigningKey,  \n        [VerifyingKey] = @VerifyingKey,  \n        [RevisionDate] = @RevisionDate  \n    WHERE  \n        [UserId] = @UserId;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/KeyManagement/Stored Procedures/User_UpdateKeyConnectorUserKey.sql",
    "content": "CREATE PROCEDURE [dbo].[User_UpdateKeyConnectorUserKey]\n    @Id UNIQUEIDENTIFIER,\n    @Key VARCHAR(MAX),\n    @Kdf TINYINT,\n    @KdfIterations INT,\n    @KdfMemory INT,\n    @KdfParallelism INT,\n    @UsesKeyConnector BIT,\n    @RevisionDate DATETIME2(7),\n    @AccountRevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[User]\n    SET\n        [Key] = @Key,\n        [Kdf] = @Kdf,\n        [KdfIterations] = @KdfIterations,\n        [KdfMemory] = @KdfMemory,\n        [KdfParallelism] = @KdfParallelism,\n        [UsesKeyConnector] = @UsesKeyConnector,\n        [RevisionDate] = @RevisionDate,\n        [AccountRevisionDate] = @AccountRevisionDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/KeyManagement/Stored Procedures/User_UpdateMasterPassword.sql",
    "content": "CREATE PROCEDURE [dbo].[User_UpdateMasterPassword]\n    @Id UNIQUEIDENTIFIER,\n    @MasterPassword NVARCHAR(300),\n    @MasterPasswordHint NVARCHAR(50) = NULL,\n    @Key VARCHAR(MAX),\n    @Kdf TINYINT,\n    @KdfIterations INT,\n    @KdfMemory INT = NULL,\n    @KdfParallelism INT = NULL,\n    @RevisionDate DATETIME2(7),\n    @AccountRevisionDate DATETIME2(7),\n    @MasterPasswordSalt NVARCHAR(256) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[User]\n    SET\n        [MasterPassword] = @MasterPassword,\n        [MasterPasswordHint] = @MasterPasswordHint,\n        [Key] = @Key,\n        [Kdf] = @Kdf,\n        [KdfIterations] = @KdfIterations,\n        [KdfMemory] = @KdfMemory,\n        [KdfParallelism] = @KdfParallelism,\n        [RevisionDate] = @RevisionDate,\n        [AccountRevisionDate] = @AccountRevisionDate,\n        [MasterPasswordSalt] = @MasterPasswordSalt\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql",
    "content": "﻿CREATE TABLE [dbo].[UserSignatureKeyPair] (\n    [Id]                        UNIQUEIDENTIFIER NOT NULL,\n    [UserId]                    UNIQUEIDENTIFIER NOT NULL,\n    [SignatureAlgorithm]        TINYINT NOT NULL,\n    [SigningKey]                VARCHAR(MAX) NOT NULL,\n    [VerifyingKey]              VARCHAR(MAX) NOT NULL,\n    [CreationDate]              DATETIME2 (7) NOT NULL,\n    [RevisionDate]              DATETIME2 (7) NOT NULL,\n    CONSTRAINT [PK_UserSignatureKeyPair] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_UserSignatureKeyPair_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE\n);\nGO\n\nCREATE UNIQUE NONCLUSTERED INDEX [IX_UserSignatureKeyPair_UserId]\n    ON [dbo].[UserSignatureKeyPair]([UserId] ASC);\nGO\n"
  },
  {
    "path": "src/Sql/dbo/KeyManagement/Views/UserSignatureKeyPairView.sql",
    "content": "﻿CREATE VIEW [dbo].[UserSignatureKeyPairView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[UserSignatureKeyPair]\n"
  },
  {
    "path": "src/Sql/dbo/Platform/Stored Procedures/Installation_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[Installation_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @Email NVARCHAR(256),\n    @Key VARCHAR(150),\n    @Enabled BIT,\n    @CreationDate DATETIME2(7),\n    @LastActivityDate DATETIME2(7) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Installation]\n    (\n        [Id],\n        [Email],\n        [Key],\n        [Enabled],\n        [CreationDate],\n        [LastActivityDate]\n    )\n    VALUES\n    (\n        @Id,\n        @Email,\n        @Key,\n        @Enabled,\n        @CreationDate,\n        @LastActivityDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Platform/Stored Procedures/Installation_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Installation_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[Installation]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Platform/Stored Procedures/Installation_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Installation_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[InstallationView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Platform/Stored Procedures/Installation_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[Installation_Update]\n    @Id UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @Key VARCHAR(150),\n    @Enabled BIT,\n    @CreationDate DATETIME2(7),\n    @LastActivityDate DATETIME2(7) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Installation]\n    SET\n        [Email] = @Email,\n        [Key] = @Key,\n        [Enabled] = @Enabled,\n        [CreationDate] = @CreationDate,\n        [LastActivityDate] = @LastActivityDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Platform/Tables/Installation.sql",
    "content": "CREATE TABLE [dbo].[Installation] (\n    [Id]               UNIQUEIDENTIFIER  NOT NULL,\n    [Email]            NVARCHAR (256)    NOT NULL,\n    [Key]              VARCHAR (150)     NOT NULL,\n    [Enabled]          BIT               NOT NULL,\n    [CreationDate]     DATETIME2 (7)     NOT NULL,\n    [LastActivityDate] DATETIME2 (7)     NULL,\n    CONSTRAINT [PK_Installation] PRIMARY KEY CLUSTERED ([Id] ASC)\n);\n\n"
  },
  {
    "path": "src/Sql/dbo/Platform/Views/InstallationView.sql",
    "content": "﻿CREATE VIEW [dbo].[InstallationView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Installation]"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKeyDetails_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ApiKeyDetails_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ApiKeyDetailsView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKey_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[ApiKey_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @ServiceAccountId UNIQUEIDENTIFIER,\n    @Name VARCHAR(200),\n    @ClientSecretHash VARCHAR(128),\n    @Scope NVARCHAR(4000),\n    @EncryptedPayload NVARCHAR(4000),\n    @Key VARCHAR(MAX),\n    @ExpireAt DATETIME2(7),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[ApiKey]\n    (\n        [Id],\n        [ServiceAccountId],\n        [Name],\n        [ClientSecretHash],\n        [Scope],\n        [EncryptedPayload],\n        [Key],\n        [ExpireAt],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @ServiceAccountId,\n        @Name,\n        @ClientSecretHash,\n        @Scope,\n        @EncryptedPayload,\n        @Key,\n        @ExpireAt,\n        @CreationDate,\n        @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKey_DeleteByIds.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ApiKey_DeleteByIds]\n    @Ids [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @BatchSize INT = 100\n\n    WHILE @BatchSize > 0\n        BEGIN\n            BEGIN TRANSACTION ApiKey_DeleteMany\n\n            DELETE TOP(@BatchSize) AK\n            FROM\n                [dbo].[ApiKey] AK\n            INNER JOIN\n                @Ids I ON I.Id = AK.Id\n\n            SET @BatchSize = @@ROWCOUNT\n\n            COMMIT TRANSACTION ApiKey_DeleteMany\n        END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKey_ReadByServiceAccountId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ApiKey_ReadByServiceAccountId]\n    @ServiceAccountId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ApiKeyView]\n    WHERE\n        [ServiceAccountId] = @ServiceAccountId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql",
    "content": "CREATE PROCEDURE [dbo].[Event_ReadPageByOrganizationIdServiceAccountId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ServiceAccountId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[EventView]\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND [OrganizationId] = @OrganizationId\n        AND ([ServiceAccountId] = @ServiceAccountId OR [GrantedServiceAccountId] = @ServiceAccountId)\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByProjectId.sql",
    "content": "CREATE PROCEDURE [dbo].[Event_ReadPageByProjectId]\n    @ProjectId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        e.Id,\n        e.Date,\n        e.Type,\n        e.UserId,\n        e.OrganizationId,\n        e.InstallationId,\n        e.ProviderId,\n        e.CipherId,\n        e.CollectionId,\n        e.PolicyId,\n        e.GroupId,\n        e.OrganizationUserId,\n        e.ProviderUserId,\n        e.ProviderOrganizationId,\n        e.DeviceType,\n        e.IpAddress,\n        e.ActingUserId,\n        e.SystemUser,\n        e.DomainName,\n        e.SecretId,\n        e.ServiceAccountId,\n        e.ProjectId\n    FROM\n        [dbo].[EventView] e\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND [ProjectId] = @ProjectId\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageBySecretId.sql",
    "content": "CREATE PROCEDURE [dbo].[Event_ReadPageBySecretId]\n    @SecretId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n  SELECT\n        e.Id,\n        e.Date,\n        e.Type,\n        e.UserId,\n        e.OrganizationId,\n        e.InstallationId,\n        e.ProviderId,\n        e.CipherId,\n        e.CollectionId,\n        e.PolicyId,\n        e.GroupId,\n        e.OrganizationUserId,\n        e.ProviderUserId,\n        e.ProviderOrganizationId,\n        e.DeviceType,\n        e.IpAddress,\n        e.ActingUserId,\n        e.SystemUser,\n        e.DomainName,\n        e.SecretId,\n        e.ServiceAccountId,\n        e.ProjectId\n    FROM\n        [dbo].[EventView] e\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND [SecretId] = @SecretId\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByServiceAccountId.sql",
    "content": "CREATE PROCEDURE [dbo].[Event_ReadPageByServiceAccountId]\n    @GrantedServiceAccountId UNIQUEIDENTIFIER,\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @BeforeDate DATETIME2(7),\n    @PageSize INT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        e.Id,\n        e.Date,\n        e.Type,\n        e.UserId,\n        e.OrganizationId,\n        e.InstallationId,\n        e.ProviderId,\n        e.CipherId,\n        e.CollectionId,\n        e.PolicyId,\n        e.GroupId,\n        e.OrganizationUserId,\n        e.ProviderUserId,\n        e.ProviderOrganizationId,\n        e.DeviceType,\n        e.IpAddress,\n        e.ActingUserId,\n        e.SystemUser,\n        e.DomainName,\n        e.SecretId,\n        e.ServiceAccountId,\n        e.ProjectId,\n        e.GrantedServiceAccountId\n    FROM\n        [dbo].[EventView] e\n    WHERE\n        [Date] >= @StartDate\n        AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)\n        AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)\n        AND [GrantedServiceAccountId] = @GrantedServiceAccountId\n    ORDER BY [Date] DESC\n    OFFSET 0 ROWS\n    FETCH NEXT @PageSize ROWS ONLY\nEND\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Tables/AccessPolicy.sql",
    "content": "﻿CREATE TABLE [dbo].[AccessPolicy]\n(\n    [Id] UNIQUEIDENTIFIER NOT NULL,\n    [Discriminator] NVARCHAR (50) NOT NULL,\n    [OrganizationUserId] UNIQUEIDENTIFIER NULL,\n    [GroupId] UNIQUEIDENTIFIER NULL,\n    [ServiceAccountId] UNIQUEIDENTIFIER NULL,\n    [GrantedProjectId] UNIQUEIDENTIFIER NULL,\n    [GrantedServiceAccountId] UNIQUEIDENTIFIER NULL,\n    [Read] BIT NOT NULL,\n    [Write] BIT NOT NULL,\n    [CreationDate] DATETIME2 NOT NULL,\n    [RevisionDate] DATETIME2 NOT NULL,\n    [GrantedSecretId] UNIQUEIDENTIFIER NULL,\n    CONSTRAINT [PK_AccessPolicy] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_AccessPolicy_Group_GroupId] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[Group] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_AccessPolicy_OrganizationUser_OrganizationUserId] FOREIGN KEY ([OrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id]),\n    CONSTRAINT [FK_AccessPolicy_Project_GrantedProjectId] FOREIGN KEY ([GrantedProjectId]) REFERENCES [dbo].[Project] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_AccessPolicy_ServiceAccount_GrantedServiceAccountId] FOREIGN KEY ([GrantedServiceAccountId]) REFERENCES [dbo].[ServiceAccount] ([Id]),\n    CONSTRAINT [FK_AccessPolicy_ServiceAccount_ServiceAccountId] FOREIGN KEY ([ServiceAccountId]) REFERENCES [dbo].[ServiceAccount] ([Id]),\n    CONSTRAINT [FK_AccessPolicy_Secret_GrantedSecretId] FOREIGN KEY ([GrantedSecretId]) REFERENCES [dbo].[Secret] ([Id]) ON DELETE CASCADE\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_AccessPolicy_GroupId] ON [dbo].[AccessPolicy]([GroupId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_AccessPolicy_OrganizationUserId] ON [dbo].[AccessPolicy]([OrganizationUserId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_AccessPolicy_GrantedProjectId] ON [dbo].[AccessPolicy]([GrantedProjectId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_AccessPolicy_ServiceAccountId] ON [dbo].[AccessPolicy]([ServiceAccountId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_AccessPolicy_GrantedServiceAccountId] ON [dbo].[AccessPolicy]([GrantedServiceAccountId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_AccessPolicy_GrantedSecretId] ON [dbo].[AccessPolicy]([GrantedSecretId] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Tables/ApiKey.sql",
    "content": "﻿CREATE TABLE [dbo].[ApiKey] (\n    [Id]                    UNIQUEIDENTIFIER,\n    [ServiceAccountId]      UNIQUEIDENTIFIER NULL,\n    [Name]                  VARCHAR(200) NOT NULL,\n    [ClientSecretHash]      VARCHAR(128) NULL,\n    [Scope]                 NVARCHAR (4000) NOT NULL,\n    [EncryptedPayload]      NVARCHAR (4000) NOT NULL,\n    [Key]                   VARCHAR (MAX) NOT NULL,\n    [ExpireAt]              DATETIME2(7) NULL,\n    [CreationDate]          DATETIME2(7) NOT NULL,\n    [RevisionDate]          DATETIME2(7) NOT NULL,\n    CONSTRAINT [PK_ApiKey] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_ApiKey_ServiceAccountId] FOREIGN KEY ([ServiceAccountId]) REFERENCES [dbo].[ServiceAccount] ([Id])\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_ApiKey_ServiceAccountId]\n    ON [dbo].[ApiKey]([ServiceAccountId] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Tables/Project.sql",
    "content": "﻿CREATE TABLE [dbo].[Project] (\n    [Id]                UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId]    UNIQUEIDENTIFIER NOT NULL,\n    [Name]              NVARCHAR(MAX) NULL, \n    [CreationDate]      DATETIME2 (7),\n    [RevisionDate]      DATETIME2 (7), \n    [DeletedDate]       DATETIME2 (7) NULL,\n    CONSTRAINT [PK_Project] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Project_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Project_OrganizationId] ON [dbo].[Project] ([OrganizationId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Project_DeletedDate] ON [dbo].[Project] ([DeletedDate] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Tables/ProjectSecret.sql",
    "content": "﻿CREATE TABLE [dbo].[ProjectSecret] (\n    [ProjectsId] uniqueidentifier NOT NULL,\n    [SecretsId] uniqueidentifier NOT NULL,\n    CONSTRAINT [PK_ProjectSecret] PRIMARY KEY ([ProjectsId], [SecretsId]),\n    CONSTRAINT [FK_ProjectSecret_Project_ProjectsId] FOREIGN KEY ([ProjectsId]) REFERENCES [Project] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_ProjectSecret_Secret_SecretsId] FOREIGN KEY ([SecretsId]) REFERENCES [Secret] ([Id]) ON DELETE CASCADE\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_ProjectSecret_SecretsId] ON [ProjectSecret] ([SecretsId]);\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Tables/Secret.sql",
    "content": "CREATE TABLE [dbo].[Secret]\n(\n    [Id] UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NOT NULL,\n    [Key] NVARCHAR(MAX) NULL,\n    [Value] NVARCHAR(MAX) NULL,\n    [Note] NVARCHAR(MAX) NULL,\n    [CreationDate] DATETIME2(7) NOT NULL,\n    [RevisionDate] DATETIME2(7) NOT NULL,\n    [DeletedDate] DATETIME2(7) NULL,\n    CONSTRAINT [PK_Secret] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Secret_OrganizationId] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization]([Id])\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Secret_OrganizationId] ON [dbo].[Secret] ([OrganizationId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Secret_DeletedDate] ON [dbo].[Secret] ([DeletedDate] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Tables/SecretVersion.sql",
    "content": "CREATE TABLE [dbo].[SecretVersion] (\n    [Id]                       UNIQUEIDENTIFIER NOT NULL,\n    [SecretId]                 UNIQUEIDENTIFIER NOT NULL,\n    [Value]                    NVARCHAR (MAX)   NOT NULL,\n    [VersionDate]              DATETIME2 (7)    NOT NULL,\n    [EditorServiceAccountId]   UNIQUEIDENTIFIER NULL,\n    [EditorOrganizationUserId] UNIQUEIDENTIFIER NULL,\n    CONSTRAINT [PK_SecretVersion] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_SecretVersion_OrganizationUser] FOREIGN KEY ([EditorOrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id]) ON DELETE SET NULL,\n    CONSTRAINT [FK_SecretVersion_Secret] FOREIGN KEY ([SecretId]) REFERENCES [dbo].[Secret] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_SecretVersion_ServiceAccount] FOREIGN KEY ([EditorServiceAccountId]) REFERENCES [dbo].[ServiceAccount] ([Id]) ON DELETE SET NULL\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_SecretVersion_SecretId]\n    ON [dbo].[SecretVersion]([SecretId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_SecretVersion_EditorServiceAccountId]\n    ON [dbo].[SecretVersion]([EditorServiceAccountId] ASC)\n    WHERE [EditorServiceAccountId] IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_SecretVersion_EditorOrganizationUserId]\n    ON [dbo].[SecretVersion]([EditorOrganizationUserId] ASC)\n    WHERE [EditorOrganizationUserId] IS NOT NULL;\nGO"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Tables/ServiceAccount.sql",
    "content": "CREATE TABLE [dbo].[ServiceAccount]\n(\n    [Id] UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NOT NULL,\n    [Name] NVARCHAR(MAX) NULL,\n    [CreationDate] DATETIME2(7) NOT NULL,\n    [RevisionDate] DATETIME2(7) NOT NULL,\n    CONSTRAINT [PK_ServiceAccount] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_ServiceAccount_OrganizationId] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization]([Id])\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_ServiceAccount_OrganizationId] ON [dbo].[ServiceAccount] ([OrganizationId] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Views/ApiKeyDetailsView.sql",
    "content": "﻿CREATE VIEW [dbo].[ApiKeyDetailsView]\nAS\nSELECT\n    AK.*,\n    SA.[OrganizationId] ServiceAccountOrganizationId\nFROM\n    [dbo].[ApiKey] AS AK\nLEFT JOIN\n    [dbo].[ServiceAccount] SA ON SA.[Id] = AK.[ServiceAccountId]\n"
  },
  {
    "path": "src/Sql/dbo/SecretsManager/Views/ApiKeyView.sql",
    "content": "﻿CREATE VIEW [dbo].[ApiKeyView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[ApiKey]\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/AzureSQLMaintenance.sql",
    "content": "﻿-- ref: https://blogs.msdn.microsoft.com/azuresqldbsupport/2016/07/03/how-to-maintain-azure-sql-indexes-and-statistics/\n\nCREATE Procedure [dbo].[AzureSQLMaintenance]\n    (\n        @operation nvarchar(10) = null,\n        @mode nvarchar(10) = 'smart',\n        @LogToTable bit = 0\n    )\nas\nbegin\n    set nocount on\n    declare @msg nvarchar(max);\n    declare @minPageCountForIndex int = 40;\n    declare @OperationTime datetime2 = sysdatetime();\n    declare @KeepXOperationInLog int =3;\n\n    /* make sure parameters selected correctly */\n    set @operation = lower(@operation)\n    set @mode = lower(@mode)\n    \n    if @mode not in ('smart','dummy')\n        set @mode = 'smart'\n\n    if @operation not in ('index','statistics','all') or @operation is null\n    begin\n        raiserror('@operation (varchar(10)) [mandatory]',0,0)\n        raiserror(' Select operation to perform:',0,0)\n        raiserror('     \"index\" to perform index maintenance',0,0)\n        raiserror('     \"statistics\" to perform statistics maintenance',0,0)\n        raiserror('     \"all\" to perform indexes and statistics maintenance',0,0)\n        raiserror(' ',0,0)\n        raiserror('@mode(varchar(10)) [optional]',0,0)\n        raiserror(' optionaly you can supply second parameter for operation mode: ',0,0)\n        raiserror('     \"smart\" (Default) using smart decition about what index or stats should be touched.',0,0)\n        raiserror('     \"dummy\" going through all indexes and statistics regardless thier modifications or fragmentation.',0,0)\n        raiserror(' ',0,0)\n        raiserror('@LogToTable(bit) [optional]',0,0)\n        raiserror(' Logging option: @LogToTable(bit)',0,0)\n        raiserror('     0 - (Default) do not log operation to table',0,0)\n        raiserror('     1 - log operation to table',0,0)\n        raiserror('\t\tfor logging option only 3 last execution will be kept by default. this can be changed by easily in the procedure body.',0,0)\n        raiserror('\t\tLog table will be created automatically if not exists.',0,0)\n    end\n    else \n    begin\n        /*Write operation parameters*/\n        raiserror('-----------------------',0,0)\n        set @msg = 'set operation = ' + @operation;\n        raiserror(@msg,0,0)\n        set @msg = 'set mode = ' + @mode;\n        raiserror(@msg,0,0)\n        set @msg = 'set LogToTable = ' + cast(@LogToTable as varchar(1));\n        raiserror(@msg,0,0)\n        raiserror('-----------------------',0,0)\n    end\n    \n    /* Prepare Log Table */\n        if object_id('AzureSQLMaintenanceLog') is null \n        begin\n            create table AzureSQLMaintenanceLog (id bigint primary key identity(1,1), OperationTime datetime2, command varchar(4000),ExtraInfo varchar(4000), StartTime datetime2, EndTime datetime2, StatusMessage varchar(1000));\n        end\n\n    if @LogToTable=1 insert into AzureSQLMaintenanceLog values(@OperationTime,null,null,sysdatetime(),sysdatetime(),'Starting operation: Operation=' +@operation + ' Mode=' + @mode + ' Keep log for last ' + cast(@KeepXOperationInLog as varchar(10)) + ' operations' )\t\n\n    create table #cmdQueue (txtCMD nvarchar(max),ExtraInfo varchar(max))\n\n\n    if @operation in('index','all')\n    begin\n        raiserror('Get index information...(wait)',0,0) with nowait;\n        /* Get Index Information */\n        select \n            i.[object_id]\n            ,ObjectSchema = OBJECT_SCHEMA_NAME(i.object_id)\n            ,ObjectName = object_name(i.object_id) \n            ,IndexName = idxs.name\n            ,i.avg_fragmentation_in_percent\n            ,i.page_count\n            ,i.index_id\n            ,i.partition_number\n            ,i.index_type_desc\n            ,i.avg_page_space_used_in_percent\n            ,i.record_count\n            ,i.ghost_record_count\n            ,i.forwarded_record_count\n            ,null as OnlineOpIsNotSupported\n        into #idxBefore\n        from sys.dm_db_index_physical_stats(DB_ID(),NULL, NULL, NULL ,'limited') i\n        left join sys.indexes idxs on i.object_id = idxs.object_id and i.index_id = idxs.index_id\n        where idxs.type in (1/*Clustered index*/,2/*NonClustered index*/) /*Avoid HEAPS*/\n        order by i.avg_fragmentation_in_percent desc, page_count desc\n\n\n        -- mark indexes XML,spatial and columnstore not to run online update \n        update #idxBefore set OnlineOpIsNotSupported=1 where [object_id] in (select [object_id] from #idxBefore where index_id >=1000)\n        \n        \n        raiserror('---------------------------------------',0,0) with nowait\n        raiserror('Index Information:',0,0) with nowait\n        raiserror('---------------------------------------',0,0) with nowait\n\n        select @msg = count(*) from #idxBefore where index_id in (1,2)\n        set @msg = 'Total Indexes: ' + @msg\n        raiserror(@msg,0,0) with nowait\n\n        select @msg = avg(avg_fragmentation_in_percent) from #idxBefore where index_id in (1,2) and page_count>@minPageCountForIndex\n        set @msg = 'Average Fragmentation: ' + @msg\n        raiserror(@msg,0,0) with nowait\n\n        select @msg = sum(iif(avg_fragmentation_in_percent>=5 and page_count>@minPageCountForIndex,1,0)) from #idxBefore where index_id in (1,2)\n        set @msg = 'Fragmented Indexes: ' + @msg\n        raiserror(@msg,0,0) with nowait\n\n                \n        raiserror('---------------------------------------',0,0) with nowait\n\n            \n            \n            \n        /* create queue for update indexes */\n        insert into #cmdQueue\n        select \n        txtCMD = \n        case when avg_fragmentation_in_percent>5 and avg_fragmentation_in_percent<30 and @mode = 'smart' then\n            'ALTER INDEX [' + IndexName + '] ON [' + ObjectSchema + '].[' + ObjectName + '] REORGANIZE;'\n            when OnlineOpIsNotSupported=1 then\n            'ALTER INDEX [' + IndexName + '] ON [' + ObjectSchema + '].[' + ObjectName + '] REBUILD WITH(ONLINE=OFF,MAXDOP=1);'\n            else\n            'ALTER INDEX [' + IndexName + '] ON [' + ObjectSchema + '].[' + ObjectName + '] REBUILD WITH(ONLINE=ON,MAXDOP=1);'\n        end\n        , ExtraInfo = 'Current fragmentation: ' + format(avg_fragmentation_in_percent/100,'p')\n        from #idxBefore\n        where \n            index_id>0 /*disable heaps*/ \n            and index_id < 1000 /* disable XML indexes */\n            --\n            and \n                (\n                    page_count> @minPageCountForIndex and /* not small tables */\n                    avg_fragmentation_in_percent>=5\n                )\n            or\n                (\n                    @mode ='dummy'\n                )\n    end\n\n    if @operation in('statistics','all')\n    begin \n        /*Gets Stats for database*/\n        raiserror('Get statistics information...',0,0) with nowait;\n        select \n            ObjectSchema = OBJECT_SCHEMA_NAME(s.object_id)\n            ,ObjectName = object_name(s.object_id) \n            ,StatsName = s.name\n            ,sp.last_updated\n            ,sp.rows\n            ,sp.rows_sampled\n            ,sp.modification_counter\n        into #statsBefore\n        from sys.stats s cross apply sys.dm_db_stats_properties(s.object_id,s.stats_id) sp \n        where OBJECT_SCHEMA_NAME(s.object_id) != 'sys' and (sp.modification_counter>0 or @mode='dummy')\n        order by sp.last_updated asc\n\n        \n        raiserror('---------------------------------------',0,0) with nowait\n        raiserror('Statistics Information:',0,0) with nowait\n        raiserror('---------------------------------------',0,0) with nowait\n\n        select @msg = sum(modification_counter) from #statsBefore\n        set @msg = 'Total Modifications: ' + @msg\n        raiserror(@msg,0,0) with nowait\n        \n        select @msg = sum(iif(modification_counter>0,1,0)) from #statsBefore\n        set @msg = 'Modified Statistics: ' + @msg\n        raiserror(@msg,0,0) with nowait\n                \n        raiserror('---------------------------------------',0,0) with nowait\n\n\n\n\n        /* create queue for update stats */\n        insert into #cmdQueue\n        select \n        txtCMD = 'UPDATE STATISTICS [' + ObjectSchema + '].[' + ObjectName + '] (['+ StatsName +']) WITH FULLSCAN;'\n        , ExtraInfo = '#rows:' + cast([rows] as varchar(100)) + ' #modifications:' + cast(modification_counter as varchar(100)) + ' modification percent: ' + format((1.0 * modification_counter/ rows ),'p')\n        from #statsBefore\n    end\n\n\n    if @operation in('statistics','index','all')\n    begin \n        /* iterate through all stats */\n        raiserror('Start executing commands...',0,0) with nowait\n        declare @SQLCMD nvarchar(max);\n        declare @ExtraInfo nvarchar(max);\n        declare @T table(txtCMD nvarchar(max),ExtraInfo nvarchar(max));\n        while exists(select * from #cmdQueue)\n        begin\n            delete top (1) from #cmdQueue output deleted.* into @T;\n            select top (1) @SQLCMD = txtCMD, @ExtraInfo=ExtraInfo from @T\n            raiserror(@SQLCMD,0,0) with nowait\n            if @LogToTable=1 insert into AzureSQLMaintenanceLog values(@OperationTime,@SQLCMD,@ExtraInfo,sysdatetime(),null,'Started')\n            begin try\n                exec(@SQLCMD)\t\n                if @LogToTable=1 update AzureSQLMaintenanceLog set EndTime = sysdatetime(), StatusMessage = 'Succeeded' where id=SCOPE_IDENTITY()\n            end try\n            begin catch\n                raiserror('cached',0,0) with nowait\n                if @LogToTable=1 update AzureSQLMaintenanceLog set EndTime = sysdatetime(), StatusMessage = 'FAILED : ' + CAST(ERROR_NUMBER() AS VARCHAR(50)) + ERROR_MESSAGE() where id=SCOPE_IDENTITY()\n            end catch\n            delete from @T\n        end\n    end\n    \n    /* Clean old records from log table */\n    if @LogToTable=1\n    begin\n        delete from AzureSQLMaintenanceLog \n        from \n            AzureSQLMaintenanceLog L join \n            (select distinct OperationTime from AzureSQLMaintenanceLog order by OperationTime desc offset @KeepXOperationInLog rows) F\n                ON L.OperationTime = F.OperationTime\n        insert into AzureSQLMaintenanceLog values(@OperationTime,null,cast(@@rowcount as varchar(100))+ ' rows purged from log table because number of operations to keep is set to: ' + cast( @KeepXOperationInLog as varchar(100)),sysdatetime(),sysdatetime(),'Cleanup Log Table')\n    end\n\n    raiserror('Done',0,0)\n    if @LogToTable=1 insert into AzureSQLMaintenanceLog values(@OperationTime,null,null,sysdatetime(),sysdatetime(),'End of operation')\nend\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections.sql",
    "content": " -- Stored procedure that filters out ciphers that ONLY belong to default collections\nCREATE PROCEDURE\n  [dbo].[CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections]\n      @OrganizationId UNIQUEIDENTIFIER\n  AS\n  BEGIN\n      SET NOCOUNT ON;\n\n      WITH [NonDefaultCiphers] AS (\n          SELECT DISTINCT [Id]\n          FROM [dbo].[OrganizationCipherDetailsCollectionsView]\n          WHERE [OrganizationId] = @OrganizationId\n              AND ([CollectionId] IS NULL\n              OR [CollectionType] <> 1)\n      )\n\n      SELECT\n          V.[Id],\n          V.[UserId],\n          V.[OrganizationId],\n          V.[Type],\n          V.[Data],\n          V.[Favorites],\n          V.[Folders],\n          V.[Attachments],\n          V.[CreationDate],\n          V.[RevisionDate],\n          V.[DeletedDate],\n          V.[Reprompt],\n          V.[Key],\n          V.[OrganizationUseTotp],\n          V.[CollectionId]  -- For Dapper splitOn parameter\n      FROM [dbo].[OrganizationCipherDetailsCollectionsView] V\n      INNER JOIN [NonDefaultCiphers] NDC ON V.[Id] = NDC.[Id]\n      WHERE V.[OrganizationId] = @OrganizationId\n          AND (V.[CollectionId] IS NULL OR V.[CollectionType] <> 1)\n      ORDER BY V.[RevisionDate] DESC;\n  END;\n  GO"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionCipher_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionCipher_Create]\n    @CollectionId UNIQUEIDENTIFIER,\n    @CipherId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[CollectionCipher]\n    (\n        [CollectionId],\n        [CipherId]\n    )\n    VALUES\n    (\n        @CollectionId,\n        @CipherId\n    )\n\n    DECLARE @OrganizationId UNIQUEIDENTIFIER = (SELECT TOP 1 [OrganizationId] FROM [dbo].[Cipher] WHERE [Id] = @CipherId)\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @CollectionId, @OrganizationId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionCipher_Delete.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionCipher_Delete]\n    @CollectionId UNIQUEIDENTIFIER,\n    @CipherId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[CollectionCipher]\n    WHERE\n        [CollectionId] = @CollectionId\n        AND [CipherId] = @CipherId\n\n    DECLARE @OrganizationId UNIQUEIDENTIFIER = (SELECT TOP 1 [OrganizationId] FROM [dbo].[Cipher] WHERE [Id] = @CipherId)\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @CollectionId, @OrganizationId\n    END\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionCipher_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionCipher_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        SC.*\n    FROM\n        [dbo].[CollectionCipher] SC\n    INNER JOIN\n        [dbo].[Collection] S ON S.[Id] = SC.[CollectionId]\n    WHERE\n        S.[OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionCipher_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionCipher_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT \n        CC.*\n    FROM\n        [dbo].[CollectionCipher] CC\n    INNER JOIN\n        [dbo].[Collection] S ON S.[Id] = CC.[CollectionId]\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId\n    INNER JOIN\n        [dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]\n    WHERE\n        OU.[Status] = 2\n\n    UNION ALL\n\n    SELECT\n        CC.*\n    FROM\n        [dbo].[CollectionCipher] CC\n    INNER JOIN\n        [dbo].[Collection] S ON S.[Id] = CC.[CollectionId]\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId\n    INNER JOIN\n        [dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id]\n    INNER JOIN\n        [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]\n    LEFT JOIN\n        [dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]\n    WHERE\n        OU.[Status] = 2\n        AND CU.[CollectionId] IS NULL\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionCipher_ReadByUserIdCipherId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionCipher_ReadByUserIdCipherId]\n    @UserId UNIQUEIDENTIFIER,\n    @CipherId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        CC.*\n    FROM\n        [dbo].[CollectionCipher] CC\n    INNER JOIN\n        [dbo].[Collection] S ON S.[Id] = CC.[CollectionId]\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId\n    LEFT JOIN\n        [dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]\n    LEFT JOIN\n        [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n    LEFT JOIN\n        [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n    LEFT JOIN\n        [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]\n    WHERE\n        CC.[CipherId] = @CipherId\n        AND OU.[Status] = 2 -- Confirmed\n        AND (\n            CU.[CollectionId] IS NOT NULL\n            OR CG.[CollectionId] IS NOT NULL\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionCipher_ReadSharedByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[CollectionCipher_ReadSharedByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        CC.[CollectionId],\n        CC.[CipherId]\n    FROM\n        [dbo].[CollectionCipher] CC\n    INNER JOIN\n        [dbo].[Collection] C ON C.[Id] = CC.[CollectionId]\n    WHERE\n        C.[OrganizationId] = @OrganizationId\n        AND C.[Type] = 0 -- SharedCollections only\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionCipher_UpdateCollections]\n    @CipherId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @CollectionIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @OrgId UNIQUEIDENTIFIER = (\n        SELECT TOP 1\n            [OrganizationId]\n        FROM\n            [dbo].[Cipher]\n        WHERE\n            [Id] = @CipherId\n    )\n    SELECT\n            C.[Id]\n            INTO #TempAvailableCollections\n        FROM\n            [dbo].[Collection] C\n        INNER JOIN\n            [Organization] O ON O.[Id] = C.[OrganizationId]\n        INNER JOIN\n            [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId\n        LEFT JOIN\n            [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = OU.[Id]\n        LEFT JOIN\n            [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n        LEFT JOIN\n            [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n        LEFT JOIN\n            [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]\n        WHERE\n            O.[Id] = @OrgId\n            AND O.[Enabled] = 1\n            AND OU.[Status] = 2 -- Confirmed\n            AND (\n                CU.[ReadOnly] = 0\n                OR CG.[ReadOnly] = 0\n            )\n    -- Insert new collection assignments\n    INSERT INTO [dbo].[CollectionCipher] (\n        [CollectionId],\n        [CipherId]\n    )\n    SELECT\n        [Id],\n        @CipherId\n    FROM @CollectionIds\n    WHERE [Id] IN (SELECT [Id] FROM [#TempAvailableCollections])\n    AND NOT EXISTS (\n        SELECT 1\n        FROM [dbo].[CollectionCipher]\n        WHERE [CollectionId] = [@CollectionIds].[Id]\n        AND [CipherId] = @CipherId\n    );\n\n    -- Delete removed collection assignments\n    DELETE CC\n    FROM [dbo].[CollectionCipher] CC\n    WHERE CC.[CipherId] = @CipherId\n    AND CC.[CollectionId] IN (SELECT [Id] FROM [#TempAvailableCollections])\n    AND CC.[CollectionId] NOT IN (SELECT [Id] FROM @CollectionIds);\n\n    IF @OrgId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId\n    END\n    DROP TABLE #TempAvailableCollections;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollectionsAdmin.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionCipher_UpdateCollectionsAdmin]\n    @CipherId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @CollectionIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    -- Available collections for this org, excluding default collections\n    SELECT\n        C.[Id]\n    INTO #TempAvailableCollections\n    FROM [dbo].[Collection] AS C\n    WHERE\n        C.[OrganizationId] = @OrganizationId\n        AND C.[Type] <> 1;  -- exclude DefaultUserCollection\n\n    -- Insert new collection assignments\n    INSERT INTO [dbo].[CollectionCipher] (\n        [CollectionId],\n        [CipherId]\n    )\n    SELECT\n        S.[Id],\n        @CipherId\n    FROM @CollectionIds AS S\n    INNER JOIN #TempAvailableCollections AS A\n        ON A.[Id] = S.[Id]\n    WHERE NOT EXISTS (\n        SELECT 1\n        FROM [dbo].[CollectionCipher] AS CC\n        WHERE CC.[CollectionId] = S.[Id]\n          AND CC.[CipherId]    = @CipherId\n    );\n\n    -- Delete removed collection assignments\n    DELETE CC\n    FROM [dbo].[CollectionCipher] AS CC\n    INNER JOIN #TempAvailableCollections AS A\n        ON A.[Id] = CC.[CollectionId]\n    WHERE CC.[CipherId] = @CipherId\n      AND NOT EXISTS (\n          SELECT 1\n          FROM @CollectionIds AS S\n          WHERE S.[Id] = CC.[CollectionId]\n      );\n\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId;\n    END\n\n    DROP TABLE #TempAvailableCollections;\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollectionsForCiphers.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionCipher_UpdateCollectionsForCiphers]\n    @CipherIds AS [dbo].[GuidIdArray] READONLY,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @CollectionIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    CREATE TABLE #AvailableCollections (\n        [Id] UNIQUEIDENTIFIER\n    )\n\n    INSERT INTO #AvailableCollections\n        SELECT\n            C.[Id]\n        FROM\n            [dbo].[Collection] C\n        INNER JOIN\n            [Organization] O ON O.[Id] = C.[OrganizationId]\n        INNER JOIN\n            [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId\n        LEFT JOIN\n            [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = OU.[Id]\n        LEFT JOIN\n            [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n        LEFT JOIN\n            [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n        LEFT JOIN\n            [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]\n        WHERE\n            O.[Id] = @OrganizationId\n            AND O.[Enabled] = 1\n            AND OU.[Status] = 2 -- Confirmed\n            AND (\n                CU.[ReadOnly] = 0\n                OR CG.[ReadOnly] = 0\n            )\n\n    IF (SELECT COUNT(1) FROM #AvailableCollections) < 1\n    BEGIN\n        -- No writable collections available to share with in this organization.\n        RETURN\n    END\n\n    INSERT INTO [dbo].[CollectionCipher]\n    (\n        [CollectionId],\n        [CipherId]\n    )\n    SELECT\n        [Collection].[Id],\n        [Cipher].[Id]\n    FROM\n        @CollectionIds [Collection]\n    INNER JOIN\n        @CipherIds [Cipher] ON 1 = 1\n    WHERE\n        [Collection].[Id] IN (SELECT [Id] FROM #AvailableCollections)\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionGroup_ReadByCollectionId.sql",
    "content": "CREATE PROCEDURE [dbo].[CollectionGroup_ReadByCollectionId]\n    @CollectionId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [GroupId] [Id],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    FROM\n        [dbo].[CollectionGroup]\n    WHERE\n        [CollectionId] = @CollectionId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionGroup_ReadByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[CollectionGroup_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    SELECT\n        CG.*\n    FROM\n        [dbo].[CollectionGroup] CG\n    INNER JOIN\n        [dbo].[Group] G ON G.[Id] = CG.[GroupId]\n    WHERE\n        G.[OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionUser_Delete.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionUser_Delete]\n    @CollectionId UNIQUEIDENTIFIER,\n    @OrganizationUserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[CollectionUser]\n    WHERE\n        [CollectionId] = @CollectionId\n        AND [OrganizationUserId] = @OrganizationUserId\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @OrganizationUserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionUser_ReadByCollectionId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionUser_ReadByCollectionId]\n    @CollectionId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [OrganizationUserId] [Id],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    FROM\n        [dbo].[CollectionUser]\n    WHERE\n        [CollectionId] = @CollectionId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionUser_ReadByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[CollectionUser_ReadByOrganizationId]\n\t@OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n    \n    SELECT\n        CU.*\n    FROM\n        [dbo].[CollectionUser] CU\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId]\n    WHERE\n        OU.[OrganizationId] = @OrganizationId\n    \nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionUser_ReadSharedCollectionsByOrganizationUserIds.sql",
    "content": "CREATE PROCEDURE [dbo].[CollectionUser_ReadSharedCollectionsByOrganizationUserIds]\n    @OrganizationUserIds [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        CU.*\n    FROM\n        [dbo].[OrganizationUser] OU\n    INNER JOIN\n        [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = OU.[Id]\n    INNER JOIN\n        [dbo].[Collection] C ON CU.[CollectionId] = C.[Id]\n    INNER JOIN\n        @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id]\n    WHERE\n        C.[Type] = 0 -- Only SharedCollection\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/CollectionUser_UpdateUsers.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CollectionUser_UpdateUsers]\n    @CollectionId UNIQUEIDENTIFIER,\n    @Users AS [dbo].[CollectionAccessSelectionType] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @OrgId UNIQUEIDENTIFIER = (\n        SELECT TOP 1\n            [OrganizationId]\n        FROM\n            [dbo].[Collection]\n        WHERE\n            [Id] = @CollectionId\n    )\n\n    -- Update\n    UPDATE\n        [Target]\n    SET\n        [Target].[ReadOnly] = [Source].[ReadOnly],\n        [Target].[HidePasswords] = [Source].[HidePasswords],\n        [Target].[Manage] = [Source].[Manage]\n    FROM\n        [dbo].[CollectionUser] [Target]\n    INNER JOIN\n        @Users [Source] ON [Source].[Id] = [Target].[OrganizationUserId]\n    WHERE\n        [Target].[CollectionId] = @CollectionId\n        AND (\n            [Target].[ReadOnly] != [Source].[ReadOnly]\n            OR [Target].[HidePasswords] != [Source].[HidePasswords]\n            OR [Target].[Manage] != [Source].[Manage]\n        )\n\n    -- Insert\n    INSERT INTO [dbo].[CollectionUser]\n    (\n        [CollectionId],\n        [OrganizationUserId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        @CollectionId,\n        [Source].[Id],\n        [Source].[ReadOnly],\n        [Source].[HidePasswords],\n        [Source].[Manage]\n    FROM\n        @Users [Source]\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON [Source].[Id] = OU.[Id] AND OU.[OrganizationId] = @OrgId\n    WHERE\n        NOT EXISTS (\n            SELECT\n                1\n            FROM\n                [dbo].[CollectionUser]\n            WHERE\n                [CollectionId] = @CollectionId\n                AND [OrganizationUserId] = [Source].[Id]\n        )\n\n    -- Delete\n    DELETE\n        CU\n    FROM\n        [dbo].[CollectionUser] CU\n    WHERE\n        CU.[CollectionId] = @CollectionId\n        AND NOT EXISTS (\n            SELECT\n                1\n            FROM\n                @Users\n            WHERE\n                [Id] = CU.[OrganizationUserId]\n        )\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @CollectionId, @OrgId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name VARCHAR(MAX),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @DefaultUserCollectionEmail NVARCHAR(256) = NULL,\n    @Type TINYINT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Collection]\n    (\n        [Id],\n        [OrganizationId],\n        [Name],\n        [ExternalId],\n        [CreationDate],\n        [RevisionDate],\n        [DefaultUserCollectionEmail],\n        [Type]\n    )\n    VALUES\n    (\n        @Id,\n        @OrganizationId,\n        @Name,\n        @ExternalId,\n        @CreationDate,\n        @RevisionDate,\n        @DefaultUserCollectionEmail,\n     @Type\n    )\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_CreateOrUpdateAccessForMany.sql",
    "content": "CREATE PROCEDURE [dbo].[Collection_CreateOrUpdateAccessForMany]\n\t@OrganizationId UNIQUEIDENTIFIER,\n\t@CollectionIds AS [dbo].[GuidIdArray] READONLY,\n    @Groups AS [dbo].[CollectionAccessSelectionType] READONLY,\n    @Users AS [dbo].[CollectionAccessSelectionType] READONLY\nAS\nBEGIN\n\tSET NOCOUNT ON\n\n\t -- Groups\n\t;WITH [NewCollectionGroups] AS (\n\t\tSELECT\n\t\t\tcId.[Id] AS [CollectionId],\n\t\t\tcg.[Id] AS [GroupId],\n\t\t\tcg.[ReadOnly],\n\t\t\tcg.[HidePasswords],\n\t\t\tcg.[Manage]\n\t\tFROM\n\t\t\t@Groups AS cg\n\t\tCROSS JOIN -- Create a CollectionGroup record for every CollectionId\n\t\t\t@CollectionIds cId\n\t\tINNER JOIN\n\t\t\t[dbo].[Group] g ON cg.[Id] = g.[Id]\n\t\tWHERE\n\t\t\tg.[OrganizationId] = @OrganizationId\n\t)\n    MERGE\n    \t[dbo].[CollectionGroup] as [Target]\n\tUSING\n\t\t[NewCollectionGroups] AS [Source]\n\tON\n\t\t[Target].[CollectionId] = [Source].[CollectionId]\n\t\tAND [Target].[GroupId] = [Source].[GroupId]\n    -- Update the target if any values are different from the source\n\tWHEN MATCHED AND EXISTS(\n\t\tSELECT [Source].[ReadOnly], [Source].[HidePasswords], [Source].[Manage]\n\t\tEXCEPT\n\t\tSELECT [Target].[ReadOnly], [Target].[HidePasswords], [Target].[Manage]\n\t) THEN UPDATE SET\n\t\t[Target].[ReadOnly] = [Source].[ReadOnly],\n\t\t[Target].[HidePasswords] = [Source].[HidePasswords],\n\t\t[Target].[Manage] = [Source].[Manage]\n\tWHEN NOT MATCHED BY TARGET\n\t\tTHEN INSERT\n\t    (\n\t        [CollectionId],\n\t        [GroupId],\n\t        [ReadOnly],\n\t        [HidePasswords],\n\t        [Manage]\n        )\n\t    VALUES\n\t\t(\n\t\t\t[Source].[CollectionId],\n\t\t\t[Source].[GroupId],\n\t\t\t[Source].[ReadOnly],\n\t\t\t[Source].[HidePasswords],\n\t\t\t[Source].[Manage]\n\t\t);\n\n\t-- Users\n\t;WITH [NewCollectionUsers] AS (\n\t\tSELECT\n\t\t\tcId.[Id] AS [CollectionId],\n\t\t\tcu.[Id] AS [OrganizationUserId],\n\t\t\tcu.[ReadOnly],\n\t\t\tcu.[HidePasswords],\n\t\t\tcu.[Manage]\n\t\tFROM\n\t\t\t@Users AS cu\n\t\tCROSS JOIN -- Create a CollectionUser record for every CollectionId\n\t\t\t@CollectionIds cId\n\t\tINNER JOIN\n\t\t\t[dbo].[OrganizationUser] u ON cu.[Id] = u.[Id]\n\t\tWHERE\n\t\t\tu.[OrganizationId] = @OrganizationId\n\t)\n    MERGE\n    \t[dbo].[CollectionUser] as [Target]\n\tUSING\n\t\t[NewCollectionUsers] AS [Source]\n\tON\n\t\t[Target].[CollectionId] = [Source].[CollectionId]\n\t\tAND [Target].[OrganizationUserId] = [Source].[OrganizationUserId]\n    -- Update the target if any values are different from the source\n\tWHEN MATCHED AND EXISTS(\n\t\tSELECT [Source].[ReadOnly], [Source].[HidePasswords], [Source].[Manage]\n\t\tEXCEPT\n\t\tSELECT [Target].[ReadOnly], [Target].[HidePasswords], [Target].[Manage]\n\t) THEN UPDATE SET\n\t\t[Target].[ReadOnly] = [Source].[ReadOnly],\n\t\t[Target].[HidePasswords] = [Source].[HidePasswords],\n\t\t[Target].[Manage] = [Source].[Manage]\n\tWHEN NOT MATCHED BY TARGET\n\t    THEN INSERT\n\t    (\n\t        [CollectionId],\n\t        [OrganizationUserId],\n\t        [ReadOnly],\n\t        [HidePasswords],\n\t        [Manage]\n        )\n\t    VALUES\n\t\t(\n\t\t\t[Source].[CollectionId],\n\t\t\t[Source].[OrganizationUserId],\n\t\t\t[Source].[ReadOnly],\n\t\t\t[Source].[HidePasswords],\n\t\t\t[Source].[Manage]\n\t\t);\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByCollectionIds] @CollectionIds, @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_CreateWithGroupsAndUsers.sql",
    "content": "CREATE PROCEDURE [dbo].[Collection_CreateWithGroupsAndUsers]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name VARCHAR(MAX),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Groups AS [dbo].[CollectionAccessSelectionType] READONLY,\n    @Users AS [dbo].[CollectionAccessSelectionType] READONLY,\n    @DefaultUserCollectionEmail NVARCHAR(256) = NULL,\n    @Type TINYINT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type\n\n    -- Groups\n    ;WITH [AvailableGroupsCTE] AS(\n        SELECT\n            [Id]\n        FROM\n            [dbo].[Group]\n        WHERE\n            [OrganizationId] = @OrganizationId\n    )\n    INSERT INTO [dbo].[CollectionGroup]\n    (\n        [CollectionId],\n        [GroupId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        @Id,\n        [Id],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    FROM\n        @Groups\n    WHERE\n        [Id] IN (SELECT [Id] FROM [AvailableGroupsCTE])\n\n    -- Users\n    ;WITH [AvailableUsersCTE] AS(\n        SELECT\n            [Id]\n        FROM\n            [dbo].[OrganizationUser]\n        WHERE\n            [OrganizationId] = @OrganizationId\n    )\n    INSERT INTO [dbo].[CollectionUser]\n    (\n        [CollectionId],\n        [OrganizationUserId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        @Id,\n        [Id],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    FROM\n        @Users\n    WHERE\n        [Id] IN (SELECT [Id] FROM [AvailableUsersCTE])\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @OrganizationId UNIQUEIDENTIFIER = (SELECT TOP 1 [OrganizationId] FROM [dbo].[Collection] WHERE [Id] = @Id)\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId\n    END\n\n    DELETE\n    FROM\n        [dbo].[CollectionGroup]\n    WHERE\n        [CollectionId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[Collection]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_DeleteByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[Collection_DeleteByIds]\n    @Ids AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @OrgIds AS [dbo].[GuidIdArray]\n\n    INSERT INTO @OrgIds (Id)\n    SELECT\n        [OrganizationId]\n    FROM\n        [dbo].[Collection]\n    WHERE\n        [Id] in (SELECT [Id] FROM @Ids)\n    GROUP BY\n        [OrganizationId]\n\n    DECLARE @BatchSize INT = 100\n\t\n    -- Delete Collection Groups\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION CollectionGroup_DeleteMany\n        \tDELETE TOP(@BatchSize) \n        \tFROM\n        \t\t[dbo].[CollectionGroup]\n        \tWHERE\n                [CollectionId] IN (SELECT [Id] FROM @Ids)\n\n\n            SET @BatchSize = @@ROWCOUNT\n        COMMIT TRANSACTION CollectionGroup_DeleteMany\n    END\n    \n    -- Reset batch size\n    SET @BatchSize = 100\n\n    -- Delete Collections\n    WHILE @BatchSize > 0\n    BEGIN\n\t    BEGIN TRANSACTION Collection_DeleteMany\n            DELETE TOP(@BatchSize)\n            FROM\n                [dbo].[Collection]\n            WHERE\n                [Id] IN (SELECT [Id] FROM @Ids)\n            \n            SET @BatchSize = @@ROWCOUNT\n        COMMIT TRANSACTION CollectionGroup_DeleteMany\n\tEND\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationIds] @OrgIds\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[CollectionView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_ReadByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[Collection_ReadByIds]\n    @Ids AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    IF (SELECT COUNT(1) FROM @Ids) < 1\n        BEGIN\n            RETURN(-1)\n        END\n\n    SELECT\n        *\n    FROM\n        [dbo].[Collection]\n    WHERE\n        [Id] IN (SELECT [Id] FROM @Ids)\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[CollectionView]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        Id,\n        OrganizationId,\n        [Name],\n        CreationDate,\n        RevisionDate,\n        ExternalId,\n        MIN([ReadOnly]) AS [ReadOnly],\n        MIN([HidePasswords]) AS [HidePasswords],\n        MAX([Manage]) AS [Manage],\n        [DefaultUserCollectionEmail],\n        [Type]\n    FROM\n        [dbo].[UserCollectionDetails](@UserId)\n    GROUP BY\n        Id,\n        OrganizationId,\n        [Name],\n        CreationDate,\n        RevisionDate,\n        ExternalId,\n        [DefaultUserCollectionEmail],\n        [Type]\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_ReadCountByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_ReadCountByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        COUNT(1)\n    FROM\n        [dbo].[Collection]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_ReadSharedCollectionsByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_ReadSharedCollectionsByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[CollectionView]\n    WHERE\n        [OrganizationId] = @OrganizationId AND\n        [Type] = 0 -- SharedCollections only\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_ReadWithGroupsAndUsersById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_ReadWithGroupsAndUsersById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Collection_ReadById] @Id\n\n    EXEC [dbo].[CollectionGroup_ReadByCollectionId] @Id\n\n    EXEC [dbo].[CollectionUser_ReadByCollectionId] @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_ReadWithGroupsAndUsersByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[Collection_ReadWithGroupsAndUsersByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n    \n    EXEC [dbo].[Collection_ReadByOrganizationId] @OrganizationId\n\n    EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId\n\n    EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId\n    \nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_Update]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name VARCHAR(MAX),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @DefaultUserCollectionEmail NVARCHAR(256) = NULL,\n    @Type TINYINT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Collection]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [Name] = @Name,\n        [ExternalId] = @ExternalId,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [DefaultUserCollectionEmail] = @DefaultUserCollectionEmail,\n        [Type] = @Type\n    WHERE\n        [Id] = @Id\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroups.sql",
    "content": "CREATE PROCEDURE [dbo].[Collection_UpdateWithGroups]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name VARCHAR(MAX),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Groups AS [dbo].[CollectionAccessSelectionType] READONLY,\n    @DefaultUserCollectionEmail NVARCHAR(256) = NULL,\n    @Type TINYINT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type\n\n    -- Groups\n    -- Delete groups that are no longer in source\n    DELETE\n        cg\n    FROM\n        [dbo].[CollectionGroup] cg\n    LEFT JOIN\n        @Groups g ON cg.GroupId = g.Id\n    WHERE\n        cg.CollectionId = @Id\n        AND g.Id IS NULL;\n\n    -- Update existing groups\n    UPDATE\n        cg\n    SET\n        cg.ReadOnly = g.ReadOnly,\n        cg.HidePasswords = g.HidePasswords,\n        cg.Manage = g.Manage\n    FROM\n        [dbo].[CollectionGroup] cg\n    INNER JOIN\n        @Groups g ON cg.GroupId = g.Id\n    WHERE\n        cg.CollectionId = @Id\n        AND (\n            cg.ReadOnly != g.ReadOnly\n            OR cg.HidePasswords != g.HidePasswords\n            OR cg.Manage != g.Manage\n        );\n\n    -- Insert new groups\n    INSERT INTO [dbo].[CollectionGroup]\n    (\n        [CollectionId],\n        [GroupId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        @Id,\n        g.Id,\n        g.ReadOnly,\n        g.HidePasswords,\n        g.Manage\n    FROM\n        @Groups g\n    INNER JOIN\n        [dbo].[Group] grp ON grp.Id = g.Id\n    LEFT JOIN\n        [dbo].[CollectionGroup] cg ON cg.CollectionId = @Id AND cg.GroupId = g.Id\n    WHERE\n        grp.OrganizationId = @OrganizationId\n        AND cg.CollectionId IS NULL;\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_UpdateWithGroupsAndUsers.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Collection_UpdateWithGroupsAndUsers]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name VARCHAR(MAX),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Groups AS [dbo].[CollectionAccessSelectionType] READONLY,\n    @Users AS [dbo].[CollectionAccessSelectionType] READONLY,\n    @DefaultUserCollectionEmail NVARCHAR(256) = NULL,\n    @Type TINYINT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type\n\n    -- Groups\n    -- Delete groups that are no longer in source\n    DELETE cg\n    FROM [dbo].[CollectionGroup] cg\n             LEFT JOIN @Groups g ON cg.GroupId = g.Id\n    WHERE cg.CollectionId = @Id\n      AND g.Id IS NULL;\n\n    -- Update existing groups\n    UPDATE cg\n    SET cg.ReadOnly = g.ReadOnly,\n        cg.HidePasswords = g.HidePasswords,\n        cg.Manage = g.Manage\n    FROM [dbo].[CollectionGroup] cg\n             INNER JOIN @Groups g ON cg.GroupId = g.Id\n    WHERE cg.CollectionId = @Id\n      AND (cg.ReadOnly != g.ReadOnly\n        OR cg.HidePasswords != g.HidePasswords\n        OR cg.Manage != g.Manage);\n\n    -- Insert new groups\n    INSERT INTO [dbo].[CollectionGroup]\n    (\n        [CollectionId],\n        [GroupId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        @Id,\n        g.Id,\n        g.ReadOnly,\n        g.HidePasswords,\n        g.Manage\n    FROM @Groups g\n             INNER JOIN [dbo].[Group] grp ON grp.Id = g.Id\n             LEFT JOIN [dbo].[CollectionGroup] cg\n                       ON cg.CollectionId = @Id AND cg.GroupId = g.Id\n    WHERE grp.OrganizationId = @OrganizationId\n      AND cg.CollectionId IS NULL;\n\n    -- Users\n    -- Delete users that are no longer in source\n    DELETE cu\n    FROM [dbo].[CollectionUser] cu\n             LEFT JOIN @Users u ON cu.OrganizationUserId = u.Id\n    WHERE cu.CollectionId = @Id\n      AND u.Id IS NULL;\n\n    -- Update existing users\n    UPDATE cu\n    SET cu.ReadOnly = u.ReadOnly,\n        cu.HidePasswords = u.HidePasswords,\n        cu.Manage = u.Manage\n    FROM [dbo].[CollectionUser] cu\n             INNER JOIN @Users u ON cu.OrganizationUserId = u.Id\n    WHERE cu.CollectionId = @Id\n      AND (cu.ReadOnly != u.ReadOnly\n        OR cu.HidePasswords != u.HidePasswords\n        OR cu.Manage != u.Manage);\n\n    -- Insert new users\n    INSERT INTO [dbo].[CollectionUser]\n    (\n        [CollectionId],\n        [OrganizationUserId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        @Id,\n        u.Id,\n        u.ReadOnly,\n        u.HidePasswords,\n        u.Manage\n    FROM @Users u\n             INNER JOIN [dbo].[OrganizationUser] ou ON ou.Id = u.Id\n             LEFT JOIN [dbo].[CollectionUser] cu\n                       ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id\n    WHERE ou.OrganizationId = @OrganizationId\n      AND cu.CollectionId IS NULL;\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Collection_UpdateWithUsers.sql",
    "content": "CREATE PROCEDURE [dbo].[Collection_UpdateWithUsers]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name VARCHAR(MAX),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Users AS [dbo].[CollectionAccessSelectionType] READONLY,\n    @DefaultUserCollectionEmail NVARCHAR(256) = NULL,\n    @Type TINYINT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate, @DefaultUserCollectionEmail, @Type\n\n    -- Users\n    -- Delete users that are no longer in source\n    DELETE\n        cu\n    FROM\n        [dbo].[CollectionUser] cu\n    LEFT JOIN\n        @Users u ON cu.OrganizationUserId = u.Id\n    WHERE\n        cu.CollectionId = @Id\n        AND u.Id IS NULL;\n\n    -- Update existing users\n    UPDATE\n        cu\n    SET\n        cu.ReadOnly = u.ReadOnly,\n        cu.HidePasswords = u.HidePasswords,\n        cu.Manage = u.Manage\n    FROM\n        [dbo].[CollectionUser] cu\n    INNER JOIN\n        @Users u ON cu.OrganizationUserId = u.Id\n    WHERE\n        cu.CollectionId = @Id\n        AND (\n            cu.ReadOnly != u.ReadOnly\n            OR cu.HidePasswords != u.HidePasswords\n            OR cu.Manage != u.Manage\n        );\n\n    -- Insert new users\n    INSERT INTO [dbo].[CollectionUser]\n    (\n        [CollectionId],\n        [OrganizationUserId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        @Id,\n        u.Id,\n        u.ReadOnly,\n        u.HidePasswords,\n        u.Manage\n    FROM\n        @Users u\n    INNER JOIN\n        [dbo].[OrganizationUser] ou ON ou.Id = u.Id\n    LEFT JOIN\n        [dbo].[CollectionUser] cu ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id\n    WHERE\n        ou.OrganizationId = @OrganizationId\n        AND cu.CollectionId IS NULL;\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Device_ClearPushTokenById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Device_ClearPushTokenById]\n    @Id NVARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Device]\n    SET\n        [PushToken] = NULL\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Device_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Device_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @Name NVARCHAR(50),\n    @Type TINYINT,\n    @Identifier NVARCHAR(50),\n    @PushToken NVARCHAR(255),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @EncryptedUserKey VARCHAR(MAX) = NULL,\n    @EncryptedPublicKey VARCHAR(MAX) = NULL,\n    @EncryptedPrivateKey VARCHAR(MAX) = NULL,\n    @Active BIT = 1\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Device]\n    (\n        [Id],\n        [UserId],\n        [Name],\n        [Type],\n        [Identifier],\n        [PushToken],\n        [CreationDate],\n        [RevisionDate],\n        [EncryptedUserKey],\n        [EncryptedPublicKey],\n        [EncryptedPrivateKey],\n        [Active]\n    )\n    VALUES\n    (\n        @Id,\n        @UserId,\n        @Name,\n        @Type,\n        @Identifier,\n        @PushToken,\n        @CreationDate,\n        @RevisionDate,\n        @EncryptedUserKey,\n        @EncryptedPublicKey,\n        @EncryptedPrivateKey,\n        @Active\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Device_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Device_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[DeviceView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Device_ReadByIdentifier.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Device_ReadByIdentifier]\n    @Identifier NVARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[DeviceView]\n    WHERE\n        [Identifier] = @Identifier\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Device_ReadByIdentifierUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Device_ReadByIdentifierUserId]\n    @UserId UNIQUEIDENTIFIER,\n    @Identifier NVARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[DeviceView]\n    WHERE\n        [UserId] = @UserId\n        AND [Identifier] = @Identifier\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Device_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Device_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[DeviceView]\n    WHERE\n        [UserId] = @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Device_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Device_Update]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @Name NVARCHAR(50),\n    @Type TINYINT,\n    @Identifier NVARCHAR(50),\n    @PushToken NVARCHAR(255),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @EncryptedUserKey VARCHAR(MAX) = NULL,\n    @EncryptedPublicKey VARCHAR(MAX) = NULL,\n    @EncryptedPrivateKey VARCHAR(MAX) = NULL,\n    @Active BIT = 1\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Device]\n    SET\n        [UserId] = @UserId,\n        [Name] = @Name,\n        [Type] = @Type,\n        [Identifier] = @Identifier,\n        [PushToken] = @PushToken,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [EncryptedUserKey] = @EncryptedUserKey,\n        [EncryptedPublicKey] = @EncryptedPublicKey,\n        [EncryptedPrivateKey] = @EncryptedPrivateKey,\n        [Active] = @Active\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/GroupUser_AddUsers.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[GroupUser_AddUsers]\n    @GroupId UNIQUEIDENTIFIER,\n    @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @OrgId UNIQUEIDENTIFIER = (\n        SELECT TOP 1\n            [OrganizationId]\n        FROM\n            [dbo].[Group]\n        WHERE\n            [Id] = @GroupId\n    )\n\n    -- Insert\n    INSERT INTO\n        [dbo].[GroupUser] (GroupId, OrganizationUserId)\n    SELECT DISTINCT\n        @GroupId,\n        [Source].[Id]\n    FROM\n        @OrganizationUserIds AS [Source]\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON [Source].[Id] = OU.[Id] AND OU.[OrganizationId] = @OrgId\n    WHERE\n        NOT EXISTS (\n            SELECT\n                1\n            FROM\n                [dbo].[GroupUser]\n            WHERE\n                [GroupId] = @GroupId\n                AND [OrganizationUserId] = [Source].[Id]\n        )\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/GroupUser_Delete.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[GroupUser_Delete]\n    @GroupId UNIQUEIDENTIFIER,\n    @OrganizationUserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[GroupUser]\n    WHERE\n        [GroupId] = @GroupId\n        AND [OrganizationUserId] = @OrganizationUserId\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @OrganizationUserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/GroupUser_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[GroupUser_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        GU.*\n    FROM\n        [dbo].[GroupUser] GU\n    INNER JOIN\n        [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n    WHERE\n        G.[OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/GroupUser_ReadByOrganizationUserIds.sql",
    "content": "CREATE PROCEDURE [dbo].[GroupUser_ReadByOrganizationUserIds]\n    @OrganizationUserIds [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        GU.*\n    FROM\n        [dbo].[GroupUser] GU\n    INNER JOIN\n        @OrganizationUserIds OUI ON OUI.[Id] = GU.[OrganizationUserId]\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/GroupUser_ReadGroupIdsByOrganizationUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[GroupUser_ReadGroupIdsByOrganizationUserId]\n    @OrganizationUserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [GroupId]\n    FROM\n        [dbo].[GroupUser]\n    WHERE\n        [OrganizationUserId] = @OrganizationUserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/GroupUser_ReadOrganizationUserIdsByGroupId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[GroupUser_ReadOrganizationUserIdsByGroupId]\n    @GroupId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [OrganizationUserId]\n    FROM\n        [dbo].[GroupUser]\n    WHERE\n        [GroupId] = @GroupId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/GroupUser_UpdateGroups.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[GroupUser_UpdateGroups]\n    @OrganizationUserId UNIQUEIDENTIFIER,\n    @GroupIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @OrgId UNIQUEIDENTIFIER = (\n        SELECT TOP 1\n            [OrganizationId]\n        FROM\n            [dbo].[OrganizationUser]\n        WHERE\n            [Id] = @OrganizationUserId\n    )\n\n    -- Insert\n    INSERT INTO\n        [dbo].[GroupUser]\n    SELECT\n        [Source].[Id],\n        @OrganizationUserId\n    FROM\n        @GroupIds [Source]\n    INNER JOIN\n        [dbo].[Group] G ON G.[Id] = [Source].[Id] AND G.[OrganizationId] = @OrgId\n    WHERE\n        NOT EXISTS (\n            SELECT\n                1\n            FROM\n                [dbo].[GroupUser]\n            WHERE\n                [OrganizationUserId] = @OrganizationUserId\n                AND [GroupId] = [Source].[Id]\n        )\n    \n    -- Delete\n    DELETE\n        GU\n    FROM\n        [dbo].[GroupUser] GU\n    WHERE\n        GU.[OrganizationUserId] = @OrganizationUserId\n        AND NOT EXISTS (\n            SELECT\n                1\n            FROM\n                @GroupIds\n            WHERE\n                [Id] = GU.[GroupId]\n        )\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @OrganizationUserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/GroupUser_UpdateUsers.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[GroupUser_UpdateUsers]\n    @GroupId UNIQUEIDENTIFIER,\n    @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @OrgId UNIQUEIDENTIFIER = (\n        SELECT TOP 1\n            [OrganizationId]\n        FROM\n            [dbo].[Group]\n        WHERE\n            [Id] = @GroupId\n    )\n\n    -- Insert\n    INSERT INTO\n        [dbo].[GroupUser]\n    SELECT\n        @GroupId,\n        [Source].[Id]\n    FROM\n        @OrganizationUserIds AS [Source]\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON [Source].[Id] = OU.[Id] AND OU.[OrganizationId] = @OrgId\n    WHERE\n        NOT EXISTS (\n            SELECT\n                1\n            FROM\n                [dbo].[GroupUser]\n            WHERE\n                [GroupId] = @GroupId\n                AND [OrganizationUserId] = [Source].[Id]\n        )\n    \n    -- Delete\n    DELETE\n        GU\n    FROM\n        [dbo].[GroupUser] GU\n    WHERE\n        GU.[GroupId] = @GroupId\n        AND NOT EXISTS (\n            SELECT\n                1\n            FROM\n                @OrganizationUserIds\n            WHERE\n                [Id] = GU.[OrganizationUserId]\n        )\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Group_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name NVARCHAR(100),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Group]\n    (\n        [Id],\n        [OrganizationId],\n        [Name],\n        [ExternalId],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @OrganizationId,\n        @Name,\n        @ExternalId,\n        @CreationDate,\n        @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_CreateWithCollections.sql",
    "content": "CREATE PROCEDURE [dbo].[Group_CreateWithCollections]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name NVARCHAR(100),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Collections AS [dbo].[CollectionAccessSelectionType] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Group_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate\n\n    ;WITH [AvailableCollectionsCTE] AS(\n        SELECT\n            [Id]\n        FROM\n            [dbo].[Collection]\n        WHERE\n            [OrganizationId] = @OrganizationId\n    )\n    INSERT INTO [dbo].[CollectionGroup]\n    (\n        [CollectionId],\n        [GroupId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        [Id],\n        @Id,\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    FROM\n        @Collections\n    WHERE\n        [Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE])\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Group_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @OrganizationId UNIQUEIDENTIFIER = (SELECT TOP 1 [OrganizationId] FROM [dbo].[Group] WHERE [Id] = @Id)\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\n    END\n\n    DELETE\n    FROM\n        [dbo].[Group]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_DeleteByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[Group_DeleteByIds]\n    @Ids AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    DECLARE @OrgIds AS [dbo].[GuidIdArray]\n\n    INSERT INTO @OrgIds (Id)\n    SELECT\n        [OrganizationId]\n    FROM\n        [dbo].[Group]\n    WHERE\n        [Id] in (SELECT [Id] FROM @Ids)\n    GROUP BY\n        [OrganizationId]\n    \n    DECLARE @BatchSize INT = 100\n        \n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION Group_DeleteMany_Groups\n            DELETE TOP(@BatchSize)\n            FROM\n                [dbo].[Group]\n            WHERE\n                [Id] IN (SELECT [Id] FROM @Ids)\n                \n            SET @BatchSize = @@ROWCOUNT\n        COMMIT TRANSACTION Group_DeleteMany_Groups\n    END\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationIds] @OrgIds\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Group_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[GroupView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_ReadByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[Group_ReadByIds]\n    @Ids AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    IF (SELECT COUNT(1) FROM @Ids) < 1\n        BEGIN\n            RETURN(-1)\n        END\n\n    SELECT\n        *\n    FROM\n        [dbo].[Group]\n    WHERE\n        [Id] IN (SELECT [Id] FROM @Ids)\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Group_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[GroupView]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_ReadCountByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Group_ReadCountByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        COUNT(1)\n    FROM\n        [dbo].[Group]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_ReadWithCollectionsById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Group_ReadWithCollectionsById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Group_ReadById] @Id\n\n    SELECT\n        [CollectionId] [Id],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    FROM\n        [dbo].[CollectionGroup]\n    WHERE\n        [GroupId] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_ReadWithCollectionsByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[Group_ReadWithCollectionsByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Group_ReadByOrganizationId] @OrganizationId\n        \n    EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Group_Update]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name NVARCHAR(100),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Group]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [Name] = @Name,\n        [ExternalId] = @ExternalId,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Group_UpdateWithCollections.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Group_UpdateWithCollections]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Name NVARCHAR(100),\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Collections AS [dbo].[CollectionAccessSelectionType] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Group_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate\n\n    ;WITH [AvailableCollectionsCTE] AS(\n        SELECT\n            Id\n        FROM\n            [dbo].[Collection]\n        WHERE\n            OrganizationId = @OrganizationId\n    )\n    MERGE\n        [dbo].[CollectionGroup] AS [Target]\n    USING\n        @Collections AS [Source]\n    ON\n        [Target].[CollectionId] = [Source].[Id]\n        AND [Target].[GroupId] = @Id\n    WHEN NOT MATCHED BY TARGET\n    AND [Source].[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN\n        INSERT\n        (\n        \t[CollectionId],\n        \t[GroupId],\n        \t[ReadOnly],\n        \t[HidePasswords],\n            [Manage]\n    \t)\n        VALUES\n        (\n            [Source].[Id],\n            @Id,\n            [Source].[ReadOnly],\n            [Source].[HidePasswords],\n            [Source].[Manage]\n        )\n    WHEN MATCHED AND (\n        [Target].[ReadOnly] != [Source].[ReadOnly]\n        OR [Target].[HidePasswords] != [Source].[HidePasswords]\n        OR [Target].[Manage] != [Source].[Manage]\n    ) THEN\n        UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly],\n                   [Target].[HidePasswords] = [Source].[HidePasswords],\n                   [Target].[Manage] = [Source].[Manage]\n    WHEN NOT MATCHED BY SOURCE\n    AND [Target].[GroupId] = @Id THEN\n        DELETE\n    ;\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/NotificationStatus_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[NotificationStatus_Create]\n    @NotificationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @ReadDate DATETIME2(7),\n    @DeletedDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[NotificationStatus] (\n        [NotificationId],\n        [UserId],\n        [ReadDate],\n        [DeletedDate]\n        )\n    VALUES (\n        @NotificationId,\n        @UserId,\n        @ReadDate,\n        @DeletedDate\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/NotificationStatus_ReadByNotificationIdAndUserId.sql",
    "content": "CREATE PROCEDURE [dbo].[NotificationStatus_ReadByNotificationIdAndUserId]\n    @NotificationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT TOP 1 *\n    FROM [dbo].[NotificationStatusView]\n    WHERE [NotificationId] = @NotificationId\n        AND [UserId] = @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/NotificationStatus_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[NotificationStatus_Update]\n    @NotificationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @ReadDate DATETIME2(7),\n    @DeletedDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE [dbo].[NotificationStatus]\n    SET [ReadDate] = @ReadDate,\n        [DeletedDate] = @DeletedDate\n    WHERE [NotificationId] = @NotificationId\n        AND [UserId] = @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Notification_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[Notification_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @Priority TINYINT,\n    @Global BIT,\n    @ClientType TINYINT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Title NVARCHAR(256),\n    @Body NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @TaskId UNIQUEIDENTIFIER = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Notification] (\n        [Id],\n        [Priority],\n        [Global],\n        [ClientType],\n        [UserId],\n        [OrganizationId],\n        [Title],\n        [Body],\n        [CreationDate],\n        [RevisionDate],\n        [TaskId]\n        )\n    VALUES (\n        @Id,\n        @Priority,\n        @Global,\n        @ClientType,\n        @UserId,\n        @OrganizationId,\n        @Title,\n        @Body,\n        @CreationDate,\n        @RevisionDate,\n        @TaskId\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql",
    "content": "CREATE PROCEDURE [dbo].[Notification_MarkAsDeletedByTask]\n    @TaskId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    -- Collect UserIds as they are altered\n    DECLARE @UserIdsForAlteredNotifications TABLE (\n        UserId UNIQUEIDENTIFIER\n    );\n\n    -- Update existing NotificationStatus as deleted\n    UPDATE ns\n    SET ns.DeletedDate = GETUTCDATE()\n    OUTPUT inserted.UserId INTO @UserIdsForAlteredNotifications\n    FROM NotificationStatus ns\n    INNER JOIN Notification n ON ns.NotificationId = n.Id\n    WHERE n.TaskId = @TaskId\n      AND ns.DeletedDate IS NULL;\n\n    -- Insert NotificationStatus records for notifications that don't have one yet\n    INSERT INTO NotificationStatus (NotificationId, UserId, DeletedDate)\n    OUTPUT inserted.UserId INTO @UserIdsForAlteredNotifications\n    SELECT n.Id, n.UserId, GETUTCDATE()\n    FROM Notification n\n    LEFT JOIN NotificationStatus ns\n        ON n.Id = ns.NotificationId\n    WHERE n.TaskId = @TaskId\n      AND ns.NotificationId IS NULL;\n\n    -- Return the UserIds associated with the altered notifications\n    SELECT u.UserId\n    FROM @UserIdsForAlteredNotifications u;\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Notification_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[Notification_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT *\n    FROM [dbo].[NotificationView]\n    WHERE [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql",
    "content": "CREATE PROCEDURE [dbo].[Notification_ReadByUserIdAndStatus]\n    @UserId UNIQUEIDENTIFIER,\n    @ClientType TINYINT,\n    @Read BIT,\n    @Deleted BIT,\n    @PageNumber INT = 1,\n    @PageSize INT = 10\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT n.*\n    FROM [dbo].[NotificationStatusDetailsView] n\n             LEFT JOIN [dbo].[OrganizationUserView] ou ON n.[OrganizationId] = ou.[OrganizationId]\n        AND ou.[UserId] = @UserId\n    WHERE (n.[NotificationStatusUserId] IS NULL OR n.[NotificationStatusUserId] = @UserId)\n      AND [ClientType] IN (0, CASE WHEN @ClientType != 0 THEN @ClientType END)\n      AND ([Global] = 1\n        OR (n.[UserId] = @UserId\n            AND (n.[OrganizationId] IS NULL\n                OR ou.[OrganizationId] IS NOT NULL))\n        OR (n.[UserId] IS NULL\n            AND ou.[OrganizationId] IS NOT NULL))\n      AND ((@Read IS NULL AND @Deleted IS NULL)\n        OR (n.[NotificationStatusUserId] IS NOT NULL\n            AND (@Read IS NULL\n                OR IIF((@Read = 1 AND n.[ReadDate] IS NOT NULL) OR\n                       (@Read = 0 AND n.[ReadDate] IS NULL),\n                       1, 0) = 1)\n            AND (@Deleted IS NULL\n                OR IIF((@Deleted = 1 AND n.[DeletedDate] IS NOT NULL) OR\n                       (@Deleted = 0 AND n.[DeletedDate] IS NULL),\n                       1, 0) = 1)))\n    ORDER BY [Priority] DESC, n.[CreationDate] DESC\n    OFFSET @PageSize * (@PageNumber - 1) ROWS FETCH NEXT @PageSize ROWS ONLY\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Notification_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[Notification_Update]\n    @Id UNIQUEIDENTIFIER,\n    @Priority TINYINT,\n    @Global BIT,\n    @ClientType TINYINT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Title NVARCHAR(256),\n    @Body NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @TaskId UNIQUEIDENTIFIER = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE [dbo].[Notification]\n    SET [Priority] = @Priority,\n        [Global] = @Global,\n        [ClientType] = @ClientType,\n        [UserId] = @UserId,\n        [OrganizationId] = @OrganizationId,\n        [Title] = @Title,\n        [Body] = @Body,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [TaskId]       = @TaskId\n    WHERE [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationApiKey_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApiKey_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ApiKey VARCHAR(30),\n    @Type TINYINT,\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationApiKey]\n    (\n        [Id],\n        [OrganizationId],\n        [ApiKey],\n        [Type],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @OrganizationId,\n        @ApiKey,\n        @Type,\n        @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationApiKey_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApiKey_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE FROM [dbo].[OrganizationApiKey]\n    WHERE [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationApiKey_OrganizationDeleted.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApiKey_OrganizationDeleted]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[OrganizationApiKey]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationApiKey_ReadManyByOrganizationIdType.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApiKey_ReadManyByOrganizationIdType]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationApiKeyView]\n    WHERE\n        [OrganizationId] = @OrganizationId AND\n        (@Type IS NULL OR [Type] = @Type)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationApiKey_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationApiKey_Update]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @ApiKey VARCHAR(30),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationApiKey]\n    SET\n        [ApiKey] = @ApiKey,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationConnection_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationConnection_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Enabled BIT,\n    @Config NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationConnection]\n    (\n        [Id],\n        [OrganizationId],\n        [Type],\n        [Enabled],\n        [Config]\n    )\n    VALUES\n    (\n        @Id,\n        @OrganizationId,\n        @Type,\n        @Enabled,\n        @Config\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationConnection_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationConnection_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE FROM\n        [dbo].[OrganizationConnection]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationConnection_OrganizationDeleted.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationConnection_OrganizationDeleted]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE FROM\n        [dbo].[OrganizationConnection]\n    WHERE\n        [OrganizationId] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationConnection_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationConnection_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationConnectionView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationConnection_ReadByIdOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationConnection_ReadByIdOrganizationId]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationConnectionView]\n    WHERE\n        [Id] = @Id AND\n        [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationConnection_ReadByOrganizationIdType.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationConnection_ReadByOrganizationIdType]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationConnectionView]\n    WHERE\n        [OrganizationId] = @OrganizationId AND\n        [Type] = @Type\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationConnection_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationConnection_Update]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Enabled BIT,\n    @Config NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationConnection]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [Type] = @Type,\n        [Enabled] = @Enabled,\n        [Config] = @Config\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomainSsoDetails_ReadByEmail.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomainSsoDetails_ReadByEmail]\n    @Email NVARCHAR(256)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @Domain NVARCHAR(256)\n\n    SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email))\n\n    SELECT\n        O.Id AS OrganizationId,\n        O.[Name] AS OrganizationName,\n        S.Enabled AS SsoAvailable,\n        O.Identifier AS OrganizationIdentifier,\n        OD.VerifiedDate,\n        OD.DomainName\n    FROM\n        [dbo].[OrganizationView] O\n    INNER JOIN [dbo].[OrganizationDomainView] OD\n        ON O.Id = OD.OrganizationId\n    LEFT JOIN [dbo].[SsoConfig] S\n        ON O.Id = S.OrganizationId\n    WHERE OD.DomainName = @Domain\n    AND O.Enabled = 1\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Txt    VARCHAR(MAX),\n    @DomainName NVARCHAR(255),\n    @CreationDate   DATETIME2(7),\n    @VerifiedDate   DATETIME2(7),\n    @LastCheckedDate DATETIME2(7),\n    @NextRunDate    DATETIME2(7),\n    @JobRunCount   TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    INSERT INTO [dbo].[OrganizationDomain]\n    (\n        [Id],\n        [OrganizationId],\n        [Txt],\n        [DomainName],\n        [CreationDate],\n        [VerifiedDate],\n        [LastCheckedDate],\n        [NextRunDate],\n        [JobRunCount]\n    )\n    VALUES\n    (\n        @Id,\n        @OrganizationId,\n        @Txt,\n        @DomainName,\n        @CreationDate,\n        @VerifiedDate,\n        @LastCheckedDate,\n        @NextRunDate,\n        @JobRunCount\n    )\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    DELETE      \n    FROM\n        [dbo].[OrganizationDomain]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_DeleteIfExpired.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_DeleteIfExpired]\n    @ExpirationPeriod TINYINT\nAS\nBEGIN\n    SET NOCOUNT OFF\n        \n    DELETE FROM [dbo].[OrganizationDomain]\n    WHERE DATEDIFF(DAY, [LastCheckedDate], GETUTCDATE()) >= @ExpirationPeriod\n    AND [VerifiedDate] IS NULL\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_HasVerifiedDomainWithBlockPolicy.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_HasVerifiedDomainWithBlockPolicy]\n    @DomainName NVARCHAR(255),\n    @ExcludeOrganizationId UNIQUEIDENTIFIER = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    -- Check if any organization has a verified domain matching the domain name\n    -- with the BlockClaimedDomainAccountCreation policy enabled (Type = 19)\n    -- If @ExcludeOrganizationId is provided, exclude that organization from the check\n    IF EXISTS (\n        SELECT 1\n        FROM [dbo].[OrganizationDomain] OD\n        INNER JOIN [dbo].[Organization] O\n            ON OD.OrganizationId = O.Id\n        INNER JOIN [dbo].[Policy] P\n            ON O.Id = P.OrganizationId\n        WHERE OD.DomainName = @DomainName\n            AND OD.VerifiedDate IS NOT NULL\n            AND O.Enabled = 1\n            AND O.UsePolicies = 1\n            AND O.UseOrganizationDomains = 1\n            AND (@ExcludeOrganizationId IS NULL OR O.Id != @ExcludeOrganizationId)\n            AND P.Type = 19  -- BlockClaimedDomainAccountCreation\n            AND P.Enabled = 1\n    )\n    BEGIN\n        SELECT CAST(1 AS BIT) AS HasBlockPolicy\n    END\n    ELSE\n    BEGIN\n        SELECT CAST(0 AS BIT) AS HasBlockPolicy\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_OrganizationDeleted.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_OrganizationDeleted]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    DELETE \n    FROM\n        [dbo].[OrganizationDomain]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByClaimedDomain.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_ReadByClaimedDomain]\n    @DomainName NVARCHAR(255)\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    SELECT \n        *\n    FROM\n        [dbo].[OrganizationDomain]\n    WHERE\n        [DomainName] = @DomainName\n    AND\n        [VerifiedDate] IS NOT NULL\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    SELECT \n        *\n    FROM\n        [dbo].[OrganizationDomain]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByIdOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_ReadByIdOrganizationId]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n    *\nFROM\n    [dbo].[OrganizationDomain]\nWHERE\n    [Id] = @Id\n  AND\n    [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByNextRunDate.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_ReadByNextRunDate]\n    @Date DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationDomain]\n    WHERE [VerifiedDate] IS NULL\n    AND [JobRunCount] != 3\n    AND DATEPART(year, [NextRunDate]) = DATEPART(year, @Date)\n    AND DATEPART(month, [NextRunDate]) = DATEPART(month, @Date)\n    AND DATEPART(day, [NextRunDate]) = DATEPART(day, @Date)\n    AND DATEPART(hour, [NextRunDate]) = DATEPART(hour, @Date)\n    UNION\n    SELECT \n        *\n    FROM\n        [dbo].[OrganizationDomain]\n    WHERE DATEDIFF(hour, [NextRunDate], @Date) > 36\n    AND [VerifiedDate] IS NULL\n    AND [JobRunCount] != 3\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    SELECT \n        *\n    FROM\n        [dbo].[OrganizationDomain]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadByOrganizationIds.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_ReadByOrganizationIds]\n    @OrganizationIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n\n    SET NOCOUNT ON\n        \n    SELECT\n        d.OrganizationId,\n        d.DomainName\n    FROM dbo.OrganizationDomainView AS d\n    WHERE d.OrganizationId IN (SELECT [Id] FROM @OrganizationIds)\n        AND d.VerifiedDate IS NOT NULL;\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadDomainByOrgIdAndDomainName.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_ReadDomainByOrgIdAndDomainName]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @DomainName NVARCHAR(255)\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n    *\nFROM\n    [dbo].[OrganizationDomain]\nWHERE\n    [OrganizationId] = @OrganizationId\n  AND\n    [DomainName] = @DomainName\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_ReadIfExpired.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_ReadIfExpired]\nAS\nBEGIN\n    SET NOCOUNT OFF\n        \n    SELECT \n        *\n    FROM\n        [dbo].[OrganizationDomain]\n    WHERE\n        DATEDIFF(DAY, [CreationDate], GETUTCDATE()) = 4 --Get domains that have not been verified after 3 days (72 hours)\n    AND\n        [VerifiedDate] IS NULL\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationDomain_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationDomain_Update]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Txt VARCHAR(MAX),\n    @DomainName NVARCHAR(255),\n    @CreationDate   DATETIME2(7),\n    @VerifiedDate   DATETIME2(7),\n    @LastCheckedDate DATETIME2(7),\n    @NextRunDate    DATETIME2(7),\n    @JobRunCount   TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n    \n    UPDATE\n        [dbo].[OrganizationDomain]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [Txt] = @Txt,\n        [DomainName] = @DomainName,\n        [CreationDate] = @CreationDate,\n        [VerifiedDate] = @VerifiedDate,\n        [LastCheckedDate] = @LastCheckedDate,\n        [NextRunDate] = @NextRunDate,\n        [JobRunCount] = @JobRunCount\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfigurationDetails_ReadMany.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadMany]\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        oic.*\n    FROM\n        [dbo].[OrganizationIntegrationConfigurationDetailsView] oic\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]\n    @EventType SMALLINT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @IntegrationType SMALLINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        oic.*\n    FROM\n        [dbo].[OrganizationIntegrationConfigurationDetailsView] oic\n    WHERE\n        (oic.[EventType] = @EventType OR oic.[EventType] IS NULL)\n        AND\n        oic.[OrganizationId] = @OrganizationId\n        AND\n        oic.[IntegrationType] = @IntegrationType\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationIntegrationConfiguration_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationIntegrationId UNIQUEIDENTIFIER,\n    @EventType SMALLINT,\n    @Configuration VARCHAR(MAX),\n    @Template VARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Filters VARCHAR(MAX) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationIntegrationConfiguration]\n        (\n        [Id],\n        [OrganizationIntegrationId],\n        [EventType],\n        [Configuration],\n        [Template],\n        [CreationDate],\n        [RevisionDate],\n        [Filters]\n        )\n    VALUES\n        (\n            @Id,\n            @OrganizationIntegrationId,\n            @EventType,\n            @Configuration,\n            @Template,\n            @CreationDate,\n            @RevisionDate,\n            @Filters\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegrationConfiguration_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[OrganizationIntegrationConfiguration]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationIntegrationConfiguration]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId]\n    @OrganizationIntegrationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n    *\nFROM\n    [dbo].[OrganizationIntegrationConfigurationView]\nWHERE\n    [OrganizationIntegrationId] = @OrganizationIntegrationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationIntegrationConfiguration_Update]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationIntegrationId UNIQUEIDENTIFIER,\n    @EventType SMALLINT,\n    @Configuration VARCHAR(MAX),\n    @Template VARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Filters VARCHAR(MAX) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationIntegrationConfiguration]\n    SET\n        [OrganizationIntegrationId] = @OrganizationIntegrationId,\n        [EventType] = @EventType,\n        [Configuration] = @Configuration,\n        [Template] = @Template,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [Filters] = @Filters\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegration_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationIntegration_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type SMALLINT,\n    @Configuration VARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationIntegration]\n        (\n        [Id],\n        [OrganizationId],\n        [Type],\n        [Configuration],\n        [CreationDate],\n        [RevisionDate]\n        )\n    VALUES\n        (\n            @Id,\n            @OrganizationId,\n            @Type,\n            @Configuration,\n            @CreationDate,\n            @RevisionDate\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegration_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegration_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[OrganizationIntegration]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegration_OrganizationDeleted.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegration_OrganizationDeleted]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[OrganizationIntegration]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegration_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationIntegration]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId]\n    @TenantId NVARCHAR(200),\n    @TeamId NVARCHAR(200)\nAS\nBEGIN\n    SET NOCOUNT ON;\n\nSELECT TOP 1 *\nFROM [dbo].[OrganizationIntegrationView]\n    CROSS APPLY OPENJSON([Configuration], '$.Teams')\n    WITH ( TeamId NVARCHAR(MAX) '$.id' ) t\nWHERE [Type] = 7\n  AND JSON_VALUE([Configuration], '$.TenantId') = @TenantId\n  AND t.TeamId = @TeamId\n  AND JSON_VALUE([Configuration], '$.ChannelId') IS NULL\n  AND JSON_VALUE([Configuration], '$.ServiceUrl') IS NULL;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadManyByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationIntegration_ReadManyByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n    *\nFROM\n    [dbo].[OrganizationIntegrationView]\nWHERE\n    [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationIntegration_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationIntegration_Update]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type SMALLINT,\n    @Configuration VARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationIntegration]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [Type] = @Type,\n        [Configuration] = @Configuration,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @SponsoringOrganizationId UNIQUEIDENTIFIER,\n    @SponsoringOrganizationUserID UNIQUEIDENTIFIER,\n    @SponsoredOrganizationId UNIQUEIDENTIFIER,\n    @FriendlyName NVARCHAR(256),\n    @OfferedToEmail NVARCHAR(256),\n    @PlanSponsorshipType TINYINT,\n    @ToDelete BIT,\n    @LastSyncDate DATETIME2 (7),\n    @ValidUntil DATETIME2 (7),\n    @IsAdminInitiated BIT = 0,\n    @Notes NVARCHAR(512) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationSponsorship]\n    (\n        [Id],\n        [SponsoringOrganizationId],\n        [SponsoringOrganizationUserID],\n        [SponsoredOrganizationId],\n        [FriendlyName],\n        [OfferedToEmail],\n        [PlanSponsorshipType],\n        [ToDelete],\n        [LastSyncDate],\n        [ValidUntil],\n        [IsAdminInitiated],\n        [Notes]\n    )\n    VALUES\n    (\n        @Id,\n        @SponsoringOrganizationId,\n        @SponsoringOrganizationUserID,\n        @SponsoredOrganizationId,\n        @FriendlyName,\n        @OfferedToEmail,\n        @PlanSponsorshipType,\n        @ToDelete,\n        @LastSyncDate,\n        @ValidUntil,\n        @IsAdminInitiated,\n        @Notes\n    )\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_CreateMany.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_CreateMany]\n    @OrganizationSponsorshipsInput [dbo].[OrganizationSponsorshipType] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationSponsorship]\n    (\n\t\t[Id],\n        [SponsoringOrganizationId],\n        [SponsoringOrganizationUserID],\n        [SponsoredOrganizationId],\n        [FriendlyName],\n        [OfferedToEmail],\n        [PlanSponsorshipType],\n        [ToDelete],\n        [LastSyncDate],\n        [ValidUntil],\n        [IsAdminInitiated],\n        [Notes]\n    )\n    SELECT\n        OS.[Id],\n        OS.[SponsoringOrganizationId],\n        OS.[SponsoringOrganizationUserID],\n        OS.[SponsoredOrganizationId],\n        OS.[FriendlyName],\n        OS.[OfferedToEmail],\n        OS.[PlanSponsorshipType],\n        OS.[ToDelete],\n        OS.[LastSyncDate],\n        OS.[ValidUntil],\n        OS.[IsAdminInitiated],\n        OS.[Notes]\n    FROM\n        @OrganizationSponsorshipsInput OS\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    BEGIN TRANSACTION OrgSponsorship_DeleteById\n\n        DELETE\n        FROM\n            [dbo].[OrganizationSponsorship]\n        WHERE\n            [Id] = @Id\n\n    COMMIT TRANSACTION OrgSponsorship_DeleteById\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_DeleteByIds]\n    @Ids [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @BatchSize INT = 100\n\n    WHILE @BatchSize > 0\n        BEGIN\n            BEGIN TRANSACTION OrgSponsorship_DeleteMany\n\n            DELETE TOP(@BatchSize) OS\n            FROM\n                [dbo].[OrganizationSponsorship] OS\n            INNER JOIN\n                @Ids I ON I.Id = OS.Id\n\n            SET @BatchSize = @@ROWCOUNT\n\n            COMMIT TRANSACTION OrgSponsorship_DeleteMany\n        END\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteExpired.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_DeleteExpired]\n    @ValidUntilBeforeDate DATETIME2 (7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @BatchSize INT = 100\n\n    WHILE @BatchSize > 0\n    BEGIN\n        DELETE TOP(@BatchSize)\n        FROM\n            [dbo].[OrganizationSponsorship]\n        WHERE\n            [ValidUntil] < @ValidUntilBeforeDate\n\n        SET @BatchSize = @@ROWCOUNT\n    END\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationDeleted.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationSponsorship]\n    SET\n        [SponsoringOrganizationId] = NULL\n    WHERE\n        [SponsoringOrganizationId] = @OrganizationId\n\n    UPDATE\n        [dbo].[OrganizationSponsorship]\n    SET\n        [SponsoredOrganizationId] = NULL\n    WHERE\n        [SponsoredOrganizationId] = @OrganizationId\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUserDeleted.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUserDeleted]\n    @OrganizationUserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        OS\n    SET\n        [ToDelete] = 1\n    FROM\n        [dbo].[OrganizationSponsorship] OS\n    WHERE\n        [SponsoringOrganizationUserID] = @OrganizationUserId\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUsersDeleted]\n    @SponsoringOrganizationUserIds [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        OS\n    SET\n        [ToDelete] = 1\n    FROM\n        [dbo].[OrganizationSponsorship] OS\n    INNER JOIN\n        @SponsoringOrganizationUserIds I ON I.Id = OS.SponsoringOrganizationUserID\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationSponsorshipView]\n    WHERE\n        [Id] = @Id\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadByOfferedToEmail.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadByOfferedToEmail]\n    @OfferedToEmail NVARCHAR (256) -- Should not be null\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationSponsorshipView]\n    WHERE\n        [OfferedToEmail] = @OfferedToEmail\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoredOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]\n    @SponsoredOrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationSponsorshipView]\n    WHERE\n        [SponsoredOrganizationId] = @SponsoredOrganizationId\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]\n    @SponsoringOrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n    \t*\n    FROM\n        [dbo].[OrganizationSponsorshipView]\n    WHERE\n        [SponsoringOrganizationId] = @SponsoringOrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]\n    @SponsoringOrganizationUserId UNIQUEIDENTIFIER,\n    @IsAdminInitiated BIT = 0\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationSponsorshipView]\n    WHERE\n        [SponsoringOrganizationUserID] = @SponsoringOrganizationUserId\n    and [IsAdminInitiated] = @IsAdminInitiated\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadLatestBySponsoringOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]\n    @SponsoringOrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SELECT TOP 1\n        [LastSyncDate]\n    FROM\n        [dbo].[OrganizationSponsorshipView]\n    WHERE\n        [SponsoringOrganizationId] = @SponsoringOrganizationId AND\n        [LastSyncDate] IS NOT NULL\n    ORDER BY [LastSyncDate] DESC\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update]\n    @Id UNIQUEIDENTIFIER,\n    @SponsoringOrganizationId UNIQUEIDENTIFIER,\n    @SponsoringOrganizationUserID UNIQUEIDENTIFIER,\n    @SponsoredOrganizationId UNIQUEIDENTIFIER,\n    @FriendlyName NVARCHAR(256),\n    @OfferedToEmail NVARCHAR(256),\n    @PlanSponsorshipType TINYINT,\n    @ToDelete BIT,\n    @LastSyncDate DATETIME2 (7),\n    @ValidUntil DATETIME2 (7),\n    @IsAdminInitiated BIT = 0,\n    @Notes NVARCHAR(512) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationSponsorship]\n    SET\n        [SponsoringOrganizationId] = @SponsoringOrganizationId,\n        [SponsoringOrganizationUserID] = @SponsoringOrganizationUserID,\n        [SponsoredOrganizationId] = @SponsoredOrganizationId,\n        [FriendlyName] = @FriendlyName,\n        [OfferedToEmail] = @OfferedToEmail,\n        [PlanSponsorshipType] = @PlanSponsorshipType,\n        [ToDelete] = @ToDelete,\n        [LastSyncDate] = @LastSyncDate,\n        [ValidUntil] = @ValidUntil,\n        [IsAdminInitiated] = @IsAdminInitiated,\n        [Notes] = @Notes\n    WHERE\n        [Id] = @Id\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationSponsorship_UpdateMany.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationSponsorship_UpdateMany]\n    @OrganizationSponsorshipsInput [dbo].[OrganizationSponsorshipType] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        OS\n    SET\n        [Id] = OSI.[Id],\n        [SponsoringOrganizationId] = OSI.[SponsoringOrganizationId],\n        [SponsoringOrganizationUserID] = OSI.[SponsoringOrganizationUserID],\n        [SponsoredOrganizationId] = OSI.[SponsoredOrganizationId],\n        [FriendlyName] = OSI.[FriendlyName],\n        [OfferedToEmail] = OSI.[OfferedToEmail],\n        [PlanSponsorshipType] = OSI.[PlanSponsorshipType],\n        [ToDelete] = OSI.[ToDelete],\n        [LastSyncDate] = OSI.[LastSyncDate],\n        [ValidUntil] = OSI.[ValidUntil],\n        [IsAdminInitiated] = OSI.[IsAdminInitiated],\n        [Notes] = OSI.[Notes]\n    FROM\n        [dbo].[OrganizationSponsorship] OS\n    INNER JOIN\n        @OrganizationSponsorshipsInput OSI ON OS.Id = OSI.Id\n\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUserOrganizationDetails_ReadByUserIdStatus.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]\n    @UserId UNIQUEIDENTIFIER,\n    @Status SMALLINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserOrganizationDetailsView]\n    WHERE\n        [UserId] = @UserId\n        AND (@Status IS NULL OR [Status] = @Status)\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]\n    @UserId UNIQUEIDENTIFIER,\n    @Status SMALLINT,\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserOrganizationDetailsView]\n    WHERE\n        [UserId] = @UserId\n        AND [OrganizationId] = @OrganizationId\n        AND (@Status IS NULL OR [Status] = @Status)\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserUserDetailsView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserUserDetailsView]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n    *\nFROM\n    [dbo].[OrganizationUserUserDetailsView]\nWHERE\n    [OrganizationId] = @OrganizationId\nAND\n    [UserId] = @UserId\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId_V2.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationId_V2]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @IncludeGroups BIT = 0,\n    @IncludeCollections BIT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    -- Result Set 1: User Details (always returned)\n    SELECT * \n    FROM [dbo].[OrganizationUserUserDetailsView] \n    WHERE OrganizationId = @OrganizationId\n\n    -- Result Set 2: Group associations (if requested)\n    IF @IncludeGroups = 1\n    BEGIN\n        SELECT gu.*\n        FROM [dbo].[GroupUser] gu\n        INNER JOIN [dbo].[OrganizationUser] ou ON gu.OrganizationUserId = ou.Id\n        WHERE ou.OrganizationId = @OrganizationId\n    END\n\n    -- Result Set 3: Collection associations (if requested)  \n    IF @IncludeCollections = 1\n    BEGIN\n        SELECT cu.*\n        FROM [dbo].[CollectionUser] cu\n        INNER JOIN [dbo].[OrganizationUser] ou ON cu.OrganizationUserId = ou.Id\n        INNER JOIN [dbo].[Collection] c ON cu.CollectionId = c.Id\n        WHERE ou.OrganizationId = @OrganizationId \n            AND c.Type = 0 -- SharedCollections only\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithSharedCollectionsById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadWithSharedCollectionsById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [OrganizationUserUserDetails_ReadById] @Id\n\n    SELECT\n        CU.[CollectionId] Id,\n        CU.[ReadOnly],\n        CU.[HidePasswords],\n        CU.[Manage]\n    FROM\n        [dbo].[OrganizationUser] OU\n    INNER JOIN\n        [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = [OU].[Id]\n    INNER JOIN\n        [dbo].[Collection] C ON CU.[CollectionId] = C.[Id]\n    WHERE\n        [OrganizationUserId] = @Id\n        AND C.[Type] = 0 -- Only SharedCollection\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_Activate.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_Activate]\n    @Id UNIQUEIDENTIFIER,\n    @Status SMALLINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationUser]\n    SET\n        [Status] = @Status\n    WHERE\n        [Id] = @Id\n        AND [Status] = -1 -- Deactivated\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ConfirmById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_ConfirmById]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @RevisionDate DATETIME2(7),\n    @Key NVARCHAR(MAX) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @RowCount INT;\n\n    UPDATE\n        [dbo].[OrganizationUser]\n    SET\n        [Status] = 2, -- Set to Confirmed\n        [RevisionDate] = @RevisionDate,\n        [Key] = @Key\n    WHERE\n        [Id] = @Id\n      AND [Status] = 1 -- Only update if status is Accepted\n\n    SET @RowCount = @@ROWCOUNT;\n\n    IF @RowCount > 0\n        BEGIN\n            EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n        END\n\n    SELECT @RowCount;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @Key VARCHAR(MAX),\n    @Status SMALLINT,\n    @Type TINYINT,\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Permissions NVARCHAR(MAX),\n    @ResetPasswordKey VARCHAR(MAX),\n    @AccessSecretsManager BIT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationUser]\n    (\n        [Id],\n        [OrganizationId],\n        [UserId],\n        [Email],\n        [Key],\n        [Status],\n        [Type],\n        [ExternalId],\n        [CreationDate],\n        [RevisionDate],\n        [Permissions],\n        [ResetPasswordKey],\n        [AccessSecretsManager]\n    )\n    VALUES\n    (\n        @Id,\n        @OrganizationId,\n        @UserId,\n        @Email,\n        @Key,\n        @Status,\n        @Type,\n        @ExternalId,\n        @CreationDate,\n        @RevisionDate,\n        @Permissions,\n        @ResetPasswordKey,\n        @AccessSecretsManager\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_CreateMany.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_CreateMany]\n    @jsonData NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationUser]\n        (\n        [Id],\n        [OrganizationId],\n        [UserId],\n        [Email],\n        [Key],\n        [Status],\n        [Type],\n        [ExternalId],\n        [CreationDate],\n        [RevisionDate],\n        [Permissions],\n        [ResetPasswordKey],\n        [AccessSecretsManager]\n        )\n    SELECT\n        OUI.[Id],\n        OUI.[OrganizationId],\n        OUI.[UserId],\n        OUI.[Email],\n        OUI.[Key],\n        OUI.[Status],\n        OUI.[Type],\n        OUI.[ExternalId],\n        OUI.[CreationDate],\n        OUI.[RevisionDate],\n        OUI.[Permissions],\n        OUI.[ResetPasswordKey],\n        OUI.[AccessSecretsManager]\n    FROM\n        OPENJSON(@jsonData)\n        WITH (\n            [Id] UNIQUEIDENTIFIER '$.Id',\n            [OrganizationId] UNIQUEIDENTIFIER '$.OrganizationId',\n            [UserId] UNIQUEIDENTIFIER '$.UserId',\n            [Email] NVARCHAR(256) '$.Email',\n            [Key] VARCHAR(MAX) '$.Key',\n            [Status] SMALLINT '$.Status',\n            [Type] TINYINT '$.Type',\n            [ExternalId] NVARCHAR(300) '$.ExternalId',\n            [CreationDate] DATETIME2(7) '$.CreationDate',\n            [RevisionDate] DATETIME2(7) '$.RevisionDate',\n            [Permissions] NVARCHAR (MAX) '$.Permissions',\n            [ResetPasswordKey] VARCHAR (MAX) '$.ResetPasswordKey',\n            [AccessSecretsManager] BIT '$.AccessSecretsManager'\n        ) OUI\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_CreateManyWithCollectionsGroups.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_CreateManyWithCollectionsAndGroups]\n    @organizationUserData NVARCHAR(MAX),\n    @collectionData NVARCHAR(MAX),\n    @groupData NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[OrganizationUser]\n    (\n        [Id],\n        [OrganizationId],\n        [UserId],\n        [Email],\n        [Key],\n        [Status],\n        [Type],\n        [ExternalId],\n        [CreationDate],\n        [RevisionDate],\n        [Permissions],\n        [ResetPasswordKey],\n        [AccessSecretsManager]\n    )\n    SELECT\n        OUI.[Id],\n        OUI.[OrganizationId],\n        OUI.[UserId],\n        OUI.[Email],\n        OUI.[Key],\n        OUI.[Status],\n        OUI.[Type],\n        OUI.[ExternalId],\n        OUI.[CreationDate],\n        OUI.[RevisionDate],\n        OUI.[Permissions],\n        OUI.[ResetPasswordKey],\n        OUI.[AccessSecretsManager]\n    FROM\n        OPENJSON(@organizationUserData)\n                 WITH (\n                     [Id] UNIQUEIDENTIFIER '$.Id',\n                     [OrganizationId] UNIQUEIDENTIFIER '$.OrganizationId',\n                     [UserId] UNIQUEIDENTIFIER '$.UserId',\n                     [Email] NVARCHAR(256) '$.Email',\n                     [Key] VARCHAR(MAX) '$.Key',\n                     [Status] SMALLINT '$.Status',\n                     [Type] TINYINT '$.Type',\n                     [ExternalId] NVARCHAR(300) '$.ExternalId',\n                     [CreationDate] DATETIME2(7) '$.CreationDate',\n                     [RevisionDate] DATETIME2(7) '$.RevisionDate',\n                     [Permissions] NVARCHAR (MAX) '$.Permissions',\n                     [ResetPasswordKey] VARCHAR (MAX) '$.ResetPasswordKey',\n                     [AccessSecretsManager] BIT '$.AccessSecretsManager'\n                     ) OUI\n\n    INSERT INTO [dbo].[GroupUser]\n    (\n        [OrganizationUserId],\n        [GroupId]\n    )\n    SELECT\n        OUG.OrganizationUserId,\n        OUG.GroupId\n    FROM\n        OPENJSON(@groupData)\n            WITH(\n                [OrganizationUserId] UNIQUEIDENTIFIER '$.OrganizationUserId',\n                [GroupId] UNIQUEIDENTIFIER '$.GroupId'\n            ) OUG\n\n    INSERT INTO [dbo].[CollectionUser]\n    (\n        [CollectionId],\n        [OrganizationUserId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        OUC.[CollectionId],\n        OUC.[OrganizationUserId],\n        OUC.[ReadOnly],\n        OUC.[HidePasswords],\n        OUC.[Manage]\n    FROM\n        OPENJSON(@collectionData)\n            WITH(\n                [CollectionId] UNIQUEIDENTIFIER '$.CollectionId',\n                [OrganizationUserId] UNIQUEIDENTIFIER '$.OrganizationUserId',\n                [ReadOnly] BIT '$.ReadOnly',\n                [HidePasswords] BIT '$.HidePasswords',\n                [Manage] BIT '$.Manage'\n            ) OUC\nEND\ngo\n\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_CreateWithCollections.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_CreateWithCollections]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @Key VARCHAR(MAX),\n    @Status SMALLINT,\n    @Type TINYINT,\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Permissions NVARCHAR(MAX),\n    @ResetPasswordKey VARCHAR(MAX),\n    @Collections AS [dbo].[CollectionAccessSelectionType] READONLY,\n    @AccessSecretsManager BIT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager\n\n    ;WITH [AvailableCollectionsCTE] AS(\n        SELECT\n            [Id]\n        FROM\n            [dbo].[Collection]\n        WHERE\n            [OrganizationId] = @OrganizationId\n    )\n    INSERT INTO [dbo].[CollectionUser]\n    (\n        [CollectionId],\n        [OrganizationUserId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        [Id],\n        @Id,\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    FROM\n        @Collections\n    WHERE\n        [Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE])\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_Deactivate.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_Deactivate]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationUser]\n    SET\n        [Status] = -1 -- Deactivated\n    WHERE\n        [Id] = @Id\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @Id\n\n    DECLARE @OrganizationId UNIQUEIDENTIFIER\n    DECLARE @UserId UNIQUEIDENTIFIER\n\n    SELECT\n        @OrganizationId = [OrganizationId],\n        @UserId = [UserId]\n    FROM\n        [dbo].[OrganizationUser]\n    WHERE\n        [Id] = @Id\n\n    -- Migrate DefaultUserCollection to SharedCollection\n    DECLARE @Ids [dbo].[GuidIdArray]\n    INSERT INTO @Ids (Id) VALUES (@Id)\n    EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids\n\n    IF @OrganizationId IS NOT NULL AND @UserId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[SsoUser_Delete] @UserId, @OrganizationId\n    END\n\n    DELETE\n    FROM\n        [dbo].[CollectionUser]\n    WHERE\n        [OrganizationUserId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[GroupUser]\n    WHERE\n        [OrganizationUserId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[AccessPolicy]\n    WHERE\n        [OrganizationUserId] = @Id\n\n    EXEC [dbo].[OrganizationSponsorship_OrganizationUserDeleted] @Id\n\n    DELETE\n    FROM\n        [dbo].[OrganizationUser]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_DeleteByIds]\n    @Ids [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @Ids\n\n    -- Migrate DefaultCollection to SharedCollection\n    EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids\n\n    DECLARE @UserAndOrganizationIds [dbo].[TwoGuidIdArray]\n\n    INSERT INTO @UserAndOrganizationIds\n        (Id1, Id2)\n    SELECT\n        UserId,\n        OrganizationId\n    FROM\n        [dbo].[OrganizationUser] OU\n    INNER JOIN\n        @Ids OUIds ON OUIds.Id = OU.Id\n    WHERE\n        UserId IS NOT NULL AND\n        OrganizationId IS NOT NULL\n\n    BEGIN\n        EXEC [dbo].[SsoUser_DeleteMany] @UserAndOrganizationIds\n    END\n\n    DECLARE @BatchSize INT = 100\n\n    -- Delete CollectionUsers\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION CollectionUser_DeleteMany_CUs\n\n        DELETE TOP(@BatchSize) CU\n        FROM\n            [dbo].[CollectionUser] CU\n        INNER JOIN\n            @Ids I ON I.Id = CU.OrganizationUserId\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION CollectionUser_DeleteMany_CUs\n    END\n\n    SET @BatchSize = 100;\n\n    -- Delete GroupUsers\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION GroupUser_DeleteMany_GroupUsers\n\n        DELETE TOP(@BatchSize) GU\n        FROM\n            [dbo].[GroupUser] GU\n        INNER JOIN\n            @Ids I ON I.Id = GU.OrganizationUserId\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION GroupUser_DeleteMany_GroupUsers\n    END\n\n    SET @BatchSize = 100;\n\n    -- Delete User Access Policies\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION AccessPolicy_DeleteMany_Users\n\n        DELETE TOP(@BatchSize) AP\n        FROM\n            [dbo].[AccessPolicy] AP\n        INNER JOIN\n            @Ids I ON I.Id = AP.OrganizationUserId\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION AccessPolicy_DeleteMany_Users\n    END\n\n    EXEC [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] @Ids\n\n    SET @BatchSize = 100;\n\n    -- Delete OrganizationUsers\n    WHILE @BatchSize > 0\n        BEGIN\n        BEGIN TRANSACTION OrganizationUser_DeleteMany_OUs\n\n        DELETE TOP(@BatchSize) OU\n        FROM\n            [dbo].[OrganizationUser] OU\n        INNER JOIN\n            @Ids I ON I.Id = OU.Id\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION OrganizationUser_DeleteMany_OUs\n    END\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_MigrateDefaultCollection.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_MigrateDefaultCollection]\n    @Ids [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n    DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();\n\n    UPDATE c\n    SET\n        [DefaultUserCollectionEmail] = CASE WHEN c.[DefaultUserCollectionEmail] IS NULL THEN u.[Email] ELSE c.[DefaultUserCollectionEmail] END,\n        [RevisionDate] = @UtcNow,\n        [Type] = 0\n    FROM\n        [dbo].[Collection] c\n        INNER JOIN [dbo].[CollectionUser] cu ON c.[Id] = cu.[CollectionId]\n        INNER JOIN [dbo].[OrganizationUser] ou ON cu.[OrganizationUserId] = ou.[Id]\n        INNER JOIN [dbo].[User] u ON ou.[UserId] = u.[Id]\n        INNER JOIN @Ids i ON ou.[Id] = i.[Id]\n    WHERE\n        c.[Type] = 1\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_ReadByIds]\n    @Ids AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    IF (SELECT COUNT(1) FROM @Ids) < 1\n    BEGIN\n        RETURN(-1)\n    END\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserView]\n    WHERE\n        [Id] IN (SELECT [Id] FROM @Ids)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByMinimumRole.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_ReadByMinimumRole]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @MinRole TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserUserDetailsView]\n    WHERE\n        OrganizationId = @OrganizationId \n        AND Status = 2 -- 2 = Confirmed \n        AND [Type] <= @MinRole\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserView]\n    WHERE\n        [OrganizationId] = @OrganizationId\n        AND (@Type IS NULL OR [Type] = @Type)\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdEmail.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdEmail]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserView]\n    WHERE\n        [OrganizationId] = @OrganizationId\n        AND [Email] IS NOT NULL\n        AND @Email IS NOT NULL\n        AND [Email] = @Email\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdUserId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserView]\n    WHERE\n        [OrganizationId] = @OrganizationId\n        AND [UserId] = @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    SELECT OU.*\n    FROM [dbo].[OrganizationUserView] OU\n    INNER JOIN [dbo].[UserView] U ON OU.[UserId] = U.[Id]\n    WHERE OU.[OrganizationId] = @OrganizationId\n        AND EXISTS (\n            SELECT 1\n            FROM [dbo].[OrganizationDomainView] OD\n            WHERE OD.[OrganizationId] = @OrganizationId\n                AND OD.[VerifiedDate] IS NOT NULL\n                AND U.[Email] LIKE '%@' + OD.[DomainName]\n        );\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    WITH OrgUsers AS (\n        SELECT *\n        FROM [dbo].[OrganizationUserView]\n        WHERE [OrganizationId] = @OrganizationId\n            AND [Status] != 0   -- Exclude invited users\n    ),\n    UserDomains AS (\n        SELECT U.[Id], U.[EmailDomain]\n        FROM [dbo].[UserEmailDomainView] U\n        WHERE EXISTS (\n            SELECT 1\n            FROM [dbo].[OrganizationDomainView] OD\n            WHERE OD.[OrganizationId] = @OrganizationId\n            AND OD.[VerifiedDate] IS NOT NULL\n            AND OD.[DomainName] = U.[EmailDomain]\n        )\n    )\n    SELECT OU.*\n    FROM OrgUsers OU\n    JOIN UserDomains UD ON OU.[UserId] = UD.[Id]\n    OPTION (RECOMPILE);\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserView]\n    WHERE\n        [UserId] = @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadByUserIdWithPolicyDetails]\n    @UserId UNIQUEIDENTIFIER,\n    @PolicyType TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserEmail NVARCHAR(256)\n    SELECT @UserEmail = Email\n    FROM\n        [dbo].[UserView]\n    WHERE\n        Id = @UserId\n\n    ;WITH OrgUsers AS\n    (\n        -- All users except invited (Status <> 0): direct UserId match\n        SELECT\n            OU.[Id],\n            OU.[OrganizationId],\n            OU.[Type],\n            OU.[Status],\n            OU.[Permissions]\n        FROM\n            [dbo].[OrganizationUserView] OU\n        WHERE\n            OU.[Status] <> 0\n            AND OU.[UserId] = @UserId\n\n        UNION ALL\n\n        -- Invited users: email match\n        SELECT\n            OU.[Id],\n            OU.[OrganizationId],\n            OU.[Type],\n            OU.[Status],\n            OU.[Permissions]\n        FROM\n            [dbo].[OrganizationUserView] OU\n        WHERE\n            OU.[Status] = 0\n            AND OU.[Email] = @UserEmail\n            AND @UserEmail IS NOT NULL\n    ),\n    Providers AS\n    (\n        SELECT\n            OrganizationId\n        FROM\n            [dbo].[UserProviderAccessView]\n        WHERE\n            UserId = @UserId\n    )\n    SELECT\n        OU.[Id] AS [OrganizationUserId],\n        P.[OrganizationId],\n        P.[Type] AS [PolicyType],\n        P.[Enabled] AS [PolicyEnabled],\n        P.[Data] AS [PolicyData],\n        OU.[Type] AS [OrganizationUserType],\n        OU.[Status] AS [OrganizationUserStatus],\n        OU.[Permissions] AS [OrganizationUserPermissionsData],\n        CASE WHEN PR.[OrganizationId] IS NULL THEN 0 ELSE 1 END AS [IsProvider]\n    FROM\n        [dbo].[PolicyView] P\n    INNER JOIN\n        OrgUsers OU ON P.[OrganizationId] = OU.[OrganizationId]\n    LEFT JOIN\n        Providers PR ON PR.[OrganizationId] = OU.[OrganizationId]\n    WHERE\n        P.[Type] = @PolicyType\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIds.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_ReadByUserIds]\n    @UserIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    IF (SELECT COUNT(1) FROM @UserIds) < 1\n    BEGIN\n        RETURN(-1)\n    END\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserView]\n    WHERE\n        [UserId] IN (SELECT [Id] FROM @UserIds)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadCountByFreeOrganizationAdminUser.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadCountByFreeOrganizationAdminUser]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        COUNT(1)\n    FROM\n        [dbo].[OrganizationUser] OU\n    INNER JOIN\n        [dbo].[Organization] O ON O.Id = OU.[OrganizationId]\n    WHERE\n        OU.[UserId] = @UserId\n        AND OU.[Type] < 2 -- Owner or Admin\n        AND O.[PlanType] = 0 -- Free\n        AND OU.[Status] = 2 -- 2 = Confirmed\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadCountByOnlyOwner.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadCountByOnlyOwner]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    ;WITH [OwnerCountCTE] AS\n    (\n        SELECT\n            OU.[UserId],\n            COUNT(1) OVER (PARTITION BY OU.[OrganizationId]) [ConfirmedOwnerCount]\n        FROM\n            [dbo].[OrganizationUser] OU\n        WHERE\n            OU.[Type] = 0 -- 0 = Owner\n            AND OU.[Status] = 2 -- 2 = Confirmed\n    )\n    SELECT\n        COUNT(1)\n    FROM\n        [OwnerCountCTE] OC\n    WHERE\n        OC.[UserId] = @UserId\n        AND OC.[ConfirmedOwnerCount] = 1\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadCountByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadCountByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        COUNT(1)\n    FROM\n        [dbo].[OrganizationUser]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadCountByOrganizationIdEmail.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadCountByOrganizationIdEmail]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @OnlyUsers BIT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        COUNT(1)\n    FROM\n        [dbo].[OrganizationUser] OU\n    LEFT JOIN\n        [dbo].[User] U ON OU.[UserId] = U.[Id]\n    WHERE\n        OU.[OrganizationId] = @OrganizationId\n        AND (\n            (@OnlyUsers = 0 AND (OU.[Email] = @Email OR U.[Email] = @Email))\n            OR (@OnlyUsers = 1 AND U.[Email] = @Email)\n        )\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadManyAccountRecoveryDetailsByOrganizationUserIds.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadManyAccountRecoveryDetailsByOrganizationUserIds]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        OU.[Id] AS OrganizationUserId,\n        U.[Kdf],\n        U.[KdfIterations],\n        U.[KdfMemory],\n        U.[KdfParallelism],\n        U.[MasterPasswordSalt],\n        OU.[ResetPasswordKey],\n        O.[PrivateKey] AS EncryptedPrivateKey\n    FROM\n        @OrganizationUserIds AS OUIDs\n    INNER JOIN\n        [dbo].[OrganizationUser] AS OU ON OUIDs.[Id] = OU.[Id]\n    INNER JOIN\n        [dbo].[Organization] AS O ON OU.[OrganizationId] = O.[Id]\n    INNER JOIN\n        [dbo].[User] U ON U.[Id] = OU.[UserId]\n    WHERE\n        OU.[OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadManyDetailsByRole.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_ReadManyDetailsByRole]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Role TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationUserUserDetailsView]\n    WHERE\n        OrganizationId = @OrganizationId \n        AND Status = 2 -- 2 = Confirmed \n        AND [Type] = @Role\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n    \n    SELECT\n        (\n            -- Count organization users\n            SELECT COUNT(1)\n            FROM [dbo].[OrganizationUserView]\n            WHERE OrganizationId = @OrganizationId\n            AND Status >= 0 --Invited\n        ) + \n        (\n            -- Count admin-initiated sponsorships towards the seat count\n            -- Introduced in https://bitwarden.atlassian.net/browse/PM-17772\n            SELECT COUNT(1)\n            FROM [dbo].[OrganizationSponsorship]\n            WHERE SponsoringOrganizationId = @OrganizationId\n            AND IsAdminInitiated = 1\n            AND (\n                -- Not marked for deletion - always count\n                (ToDelete = 0) \n                OR\n                -- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)\n                (ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())\n            )\n            AND (\n                -- SENT status: When SponsoredOrganizationId is null\n                SponsoredOrganizationId IS NULL\n                OR\n                -- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future\n                (SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))\n            )\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n    \n    SELECT\n        COUNT(1)\n    FROM\n        [dbo].[OrganizationUserView]\n    WHERE\n        OrganizationId = @OrganizationId\n        AND Status >= 0 --Invited\n        AND AccessSecretsManager = 1\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_ReadWithCollectionsById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_ReadWithCollectionsById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [OrganizationUser_ReadById] @Id\n\n    SELECT\n        CU.[CollectionId] Id,\n        CU.[ReadOnly],\n        CU.[HidePasswords],\n        CU.[Manage]\n    FROM\n        [dbo].[OrganizationUser] OU\n    INNER JOIN\n        [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = [OU].[Id]\n    WHERE\n        [OrganizationUserId] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_SelectKnownEmails.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_SelectKnownEmails]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Emails [dbo].[EmailArray] READONLY,\n    @OnlyUsers BIT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        E.Email\n    FROM\n        @Emails E\n        INNER JOIN\n        (\n            SELECT\n            U.[Email] as 'UEmail',\n            OU.[Email] as 'OUEmail',\n            OU.OrganizationId\n        FROM\n            [dbo].[User] U\n            RIGHT JOIN\n            [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]\n        WHERE\n                OU.OrganizationId = @OrganizationId\n        ) OUU ON OUU.[UEmail] = E.[Email] OR OUU.[OUEmail] = E.[Email]\n    WHERE\n        (@OnlyUsers = 0 AND (OUU.UEmail IS NOT NULL OR OUU.OUEmail IS NOT NULL)) OR\n        (@OnlyUsers = 1 AND (OUU.UEmail IS NOT NULL))\n\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersByGuidIdArray.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]\n    @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY,\n    @Status SMALLINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE OU\n    SET OU.[Status] = @Status\n    FROM [dbo].[OrganizationUser] OU\n    INNER JOIN @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id]\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @OrganizationUserIds\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_Update]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @Key VARCHAR(MAX),\n    @Status SMALLINT,\n    @Type TINYINT,\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Permissions NVARCHAR(MAX),\n    @ResetPasswordKey VARCHAR(MAX),\n    @AccessSecretsManager BIT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[OrganizationUser]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [UserId] = @UserId,\n        [Email] = @Email,\n        [Key] = @Key,\n        [Status] = @Status,\n        [Type] = @Type,\n        [ExternalId] = @ExternalId,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [Permissions] = @Permissions,\n        [ResetPasswordKey] = @ResetPasswordKey,\n        [AccessSecretsManager] = @AccessSecretsManager\n    WHERE\n        [Id] = @Id\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateDataForKeyRotation.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_UpdateDataForKeyRotation]\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationUserJson NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    -- Parse the JSON string and insert into a temporary table\n    DECLARE @OrganizationUserInput AS TABLE (\n        [Id] UNIQUEIDENTIFIER,\n        [ResetPasswordKey] VARCHAR(MAX)\n    )\n\n    INSERT INTO @OrganizationUserInput\n    SELECT\n        [Id],\n        [ResetPasswordKey]\n    FROM OPENJSON(@OrganizationUserJson)\n    WITH (\n        [Id] UNIQUEIDENTIFIER '$.Id',\n        [ResetPasswordKey] VARCHAR(MAX) '$.ResetPasswordKey'\n    )\n\n    -- Perform the update\n    UPDATE\n        [dbo].[OrganizationUser]\n    SET\n        [ResetPasswordKey] = OUI.[ResetPasswordKey]\n    FROM\n        [dbo].[OrganizationUser] OU\n    INNER JOIN\n        @OrganizationUserInput OUI ON OU.Id = OUI.Id\n    WHERE\n        OU.[UserId] = @UserId\n\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateMany.sql",
    "content": "CREATE PROCEDURE [dbo].[OrganizationUser_UpdateMany]\n    @jsonData NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserIds [dbo].[GuidIdArray]\n\n    -- Parse the JSON string\n    DECLARE @OrganizationUserInput AS TABLE (\n        [Id] UNIQUEIDENTIFIER,\n        [OrganizationId] UNIQUEIDENTIFIER,\n        [UserId] UNIQUEIDENTIFIER,\n        [Email] NVARCHAR(256),\n        [Key] VARCHAR(MAX),\n        [Status] SMALLINT,\n        [Type] TINYINT,\n        [ExternalId] NVARCHAR(300),\n        [CreationDate] DATETIME2(7),\n        [RevisionDate] DATETIME2(7),\n        [Permissions] NVARCHAR(MAX),\n        [ResetPasswordKey] VARCHAR(MAX),\n        [AccessSecretsManager] BIT\n    )\n\n    INSERT INTO @OrganizationUserInput\n    SELECT\n        [Id],\n        [OrganizationId],\n        [UserId],\n        [Email],\n        [Key],\n        [Status],\n        [Type],\n        [ExternalId],\n        [CreationDate],\n        [RevisionDate],\n        [Permissions],\n        [ResetPasswordKey],\n        [AccessSecretsManager]\n    FROM OPENJSON(@jsonData)\n    WITH (\n        [Id] UNIQUEIDENTIFIER '$.Id',\n        [OrganizationId] UNIQUEIDENTIFIER '$.OrganizationId',\n        [UserId] UNIQUEIDENTIFIER '$.UserId',\n        [Email] NVARCHAR(256) '$.Email',\n        [Key] VARCHAR(MAX) '$.Key',\n        [Status] SMALLINT '$.Status',\n        [Type] TINYINT '$.Type',\n        [ExternalId] NVARCHAR(300) '$.ExternalId',\n        [CreationDate] DATETIME2(7) '$.CreationDate',\n        [RevisionDate] DATETIME2(7) '$.RevisionDate',\n        [Permissions] NVARCHAR (MAX) '$.Permissions',\n        [ResetPasswordKey] VARCHAR (MAX) '$.ResetPasswordKey',\n        [AccessSecretsManager] BIT '$.AccessSecretsManager'\n    )\n\n    -- Perform the update\n    UPDATE\n        OU\n    SET\n        [OrganizationId] = OUI.[OrganizationId],\n        [UserId] = OUI.[UserId],\n        [Email] = OUI.[Email],\n        [Key] = OUI.[Key],\n        [Status] = OUI.[Status],\n        [Type] = OUI.[Type],\n        [ExternalId] = OUI.[ExternalId],\n        [CreationDate] = OUI.[CreationDate],\n        [RevisionDate] = OUI.[RevisionDate],\n        [Permissions] = OUI.[Permissions],\n        [ResetPasswordKey] = OUI.[ResetPasswordKey],\n        [AccessSecretsManager] = OUI.[AccessSecretsManager]\n    FROM\n        [dbo].[OrganizationUser] OU\n    INNER JOIN\n        @OrganizationUserInput OUI ON OU.Id = OUI.Id\n\n    -- Bump account revision dates\n    INSERT INTO @UserIds\n    SELECT [UserId]\n    FROM @OrganizationUserInput\n\n    EXEC [dbo].[User_BumpManyAccountRevisionDates] @UserIds\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateWithCollections.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[OrganizationUser_UpdateWithCollections]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @Key VARCHAR(MAX),\n    @Status SMALLINT,\n    @Type TINYINT,\n    @ExternalId NVARCHAR(300),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Permissions NVARCHAR(MAX),\n    @ResetPasswordKey VARCHAR(MAX),\n    @Collections AS [dbo].[CollectionAccessSelectionType] READONLY,\n    @AccessSecretsManager BIT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager\n    -- Update\n    UPDATE\n        [Target]\n    SET\n        [Target].[ReadOnly] = [Source].[ReadOnly],\n        [Target].[HidePasswords] = [Source].[HidePasswords],\n        [Target].[Manage] = [Source].[Manage]\n    FROM\n        [dbo].[CollectionUser] AS [Target]\n    INNER JOIN\n        @Collections AS [Source] ON [Source].[Id] = [Target].[CollectionId]\n    WHERE\n        [Target].[OrganizationUserId] = @Id\n        AND (\n            [Target].[ReadOnly] != [Source].[ReadOnly]\n            OR [Target].[HidePasswords] != [Source].[HidePasswords]\n            OR [Target].[Manage] != [Source].[Manage]\n        )\n\n    -- Insert\n    INSERT INTO [dbo].[CollectionUser]\n    (\n        [CollectionId],\n        [OrganizationUserId],\n        [ReadOnly],\n        [HidePasswords],\n        [Manage]\n    )\n    SELECT\n        [Source].[Id],\n        @Id,\n        [Source].[ReadOnly],\n        [Source].[HidePasswords],\n        [Source].[Manage]\n    FROM\n        @Collections AS [Source]\n    INNER JOIN\n        [dbo].[Collection] C ON C.[Id] = [Source].[Id] AND C.[OrganizationId] = @OrganizationId\n    WHERE\n        NOT EXISTS (\n            SELECT\n                1\n            FROM\n                [dbo].[CollectionUser]\n            WHERE\n                [CollectionId] = [Source].[Id]\n                AND [OrganizationUserId] = @Id\n        )\n\n    -- Delete\n    DELETE\n        CU\n    FROM\n        [dbo].[CollectionUser] CU\n    INNER JOIN\n        [dbo].[Collection] C ON C.[Id] = CU.[CollectionId]\n    WHERE\n        CU.[OrganizationUserId] = @Id\n        AND C.[Type] != 1  -- Don't delete default collections\n        AND NOT EXISTS (\n            SELECT\n                1\n            FROM\n                @Collections\n            WHERE\n                [Id] = CU.[CollectionId]\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @Identifier NVARCHAR(50),\n    @Name NVARCHAR(50),\n    @BusinessName NVARCHAR(50),\n    @BusinessAddress1 NVARCHAR(50),\n    @BusinessAddress2 NVARCHAR(50),\n    @BusinessAddress3 NVARCHAR(50),\n    @BusinessCountry VARCHAR(2),\n    @BusinessTaxNumber NVARCHAR(30),\n    @BillingEmail NVARCHAR(256),\n    @Plan NVARCHAR(50),\n    @PlanType TINYINT,\n    @Seats INT,\n    @MaxCollections SMALLINT,\n    @UsePolicies BIT,\n    @UseSso BIT,\n    @UseGroups BIT,\n    @UseDirectory BIT,\n    @UseEvents BIT,\n    @UseTotp BIT,\n    @Use2fa BIT,\n    @UseApi BIT,\n    @UseResetPassword BIT,\n    @SelfHost BIT,\n    @UsersGetPremium BIT,\n    @Storage BIGINT,\n    @MaxStorageGb SMALLINT,\n    @Gateway TINYINT,\n    @GatewayCustomerId VARCHAR(50),\n    @GatewaySubscriptionId VARCHAR(50),\n    @ReferenceData VARCHAR(MAX),\n    @Enabled BIT,\n    @LicenseKey VARCHAR(100),\n    @PublicKey VARCHAR(MAX),\n    @PrivateKey VARCHAR(MAX),\n    @TwoFactorProviders NVARCHAR(MAX),\n    @ExpirationDate DATETIME2(7),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @OwnersNotifiedOfAutoscaling DATETIME2(7),\n    @MaxAutoscaleSeats INT,\n    @UseKeyConnector BIT = 0,\n    @UseScim BIT = 0,\n    @UseCustomPermissions BIT = 0,\n    @UseSecretsManager BIT = 0,\n    @Status TINYINT = 0,\n    @UsePasswordManager BIT = 1,\n    @SmSeats INT = null,\n    @SmServiceAccounts INT = null,\n    @MaxAutoscaleSmSeats INT= null,\n    @MaxAutoscaleSmServiceAccounts INT = null,\n    @SecretsManagerBeta BIT = 0,\n    @LimitCollectionCreation BIT = NULL,\n    @LimitCollectionDeletion BIT = NULL,\n    @AllowAdminAccessToAllCollectionItems BIT = 0,\n    @UseRiskInsights BIT = 0,\n    @LimitItemDeletion BIT = 0,\n    @UseOrganizationDomains BIT = 0,\n    @UseAdminSponsoredFamilies BIT = 0,\n    @SyncSeats BIT = 0,\n    @UseAutomaticUserConfirmation BIT = 0,\n    @UsePhishingBlocker BIT = 0,\n    @UseDisableSmAdsForUsers BIT = 0,\n    @UseMyItems BIT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Organization]\n    (\n        [Id],\n        [Identifier],\n        [Name],\n        [BusinessName],\n        [BusinessAddress1],\n        [BusinessAddress2],\n        [BusinessAddress3],\n        [BusinessCountry],\n        [BusinessTaxNumber],\n        [BillingEmail],\n        [Plan],\n        [PlanType],\n        [Seats],\n        [MaxCollections],\n        [UsePolicies],\n        [UseSso],\n        [UseGroups],\n        [UseDirectory],\n        [UseEvents],\n        [UseTotp],\n        [Use2fa],\n        [UseApi],\n        [UseResetPassword],\n        [SelfHost],\n        [UsersGetPremium],\n        [Storage],\n        [MaxStorageGb],\n        [Gateway],\n        [GatewayCustomerId],\n        [GatewaySubscriptionId],\n        [ReferenceData],\n        [Enabled],\n        [LicenseKey],\n        [PublicKey],\n        [PrivateKey],\n        [TwoFactorProviders],\n        [ExpirationDate],\n        [CreationDate],\n        [RevisionDate],\n        [OwnersNotifiedOfAutoscaling],\n        [MaxAutoscaleSeats],\n        [UseKeyConnector],\n        [UseScim],\n        [UseCustomPermissions],\n        [UseSecretsManager],\n        [Status],\n        [UsePasswordManager],\n        [SmSeats],\n        [SmServiceAccounts],\n        [MaxAutoscaleSmSeats],\n        [MaxAutoscaleSmServiceAccounts],\n        [SecretsManagerBeta],\n        [LimitCollectionCreation],\n        [LimitCollectionDeletion],\n        [AllowAdminAccessToAllCollectionItems],\n        [UseRiskInsights],\n        [LimitItemDeletion],\n        [UseOrganizationDomains],\n        [UseAdminSponsoredFamilies],\n        [SyncSeats],\n        [UseAutomaticUserConfirmation],\n        [UsePhishingBlocker],\n        [MaxStorageGbIncreased],\n        [UseDisableSmAdsForUsers],\n        [UseMyItems]\n    )\n    VALUES\n        (\n            @Id,\n            @Identifier,\n            @Name,\n            @BusinessName,\n            @BusinessAddress1,\n            @BusinessAddress2,\n            @BusinessAddress3,\n            @BusinessCountry,\n            @BusinessTaxNumber,\n            @BillingEmail,\n            @Plan,\n            @PlanType,\n            @Seats,\n            @MaxCollections,\n            @UsePolicies,\n            @UseSso,\n            @UseGroups,\n            @UseDirectory,\n            @UseEvents,\n            @UseTotp,\n            @Use2fa,\n            @UseApi,\n            @UseResetPassword,\n            @SelfHost,\n            @UsersGetPremium,\n            @Storage,\n            @MaxStorageGb,\n            @Gateway,\n            @GatewayCustomerId,\n            @GatewaySubscriptionId,\n            @ReferenceData,\n            @Enabled,\n            @LicenseKey,\n            @PublicKey,\n            @PrivateKey,\n            @TwoFactorProviders,\n            @ExpirationDate,\n            @CreationDate,\n            @RevisionDate,\n            @OwnersNotifiedOfAutoscaling,\n            @MaxAutoscaleSeats,\n            @UseKeyConnector,\n            @UseScim,\n            @UseCustomPermissions,\n            @UseSecretsManager,\n            @Status,\n            @UsePasswordManager,\n            @SmSeats,\n            @SmServiceAccounts,\n            @MaxAutoscaleSmSeats,\n            @MaxAutoscaleSmServiceAccounts,\n            @SecretsManagerBeta,\n            @LimitCollectionCreation,\n            @LimitCollectionDeletion,\n            @AllowAdminAccessToAllCollectionItems,\n            @UseRiskInsights,\n            @LimitItemDeletion,\n            @UseOrganizationDomains,\n            @UseAdminSponsoredFamilies,\n            @SyncSeats,\n            @UseAutomaticUserConfirmation,\n            @UsePhishingBlocker,\n            @MaxStorageGb,\n            @UseDisableSmAdsForUsers,\n            @UseMyItems\n        );\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_DeleteById]\n    @Id UNIQUEIDENTIFIER\nWITH RECOMPILE\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @Id\n\n    DECLARE @BatchSize INT = 100\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION Organization_DeleteById_Ciphers\n\n        DELETE TOP(@BatchSize)\n        FROM\n            [dbo].[Cipher]\n        WHERE\n            [UserId] IS NULL\n            AND [OrganizationId] = @Id\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION Organization_DeleteById_Ciphers\n    END\n\n    BEGIN TRANSACTION Organization_DeleteById\n\n    DELETE\n    FROM\n        [dbo].[AuthRequest]\n    WHERE\n        [OrganizationId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[SsoUser]\n    WHERE\n        [OrganizationId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[SsoConfig]\n    WHERE\n        [OrganizationId] = @Id\n\n    DELETE CU\n    FROM\n        [dbo].[CollectionUser] CU\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON [CU].[OrganizationUserId] = [OU].[Id]\n    WHERE\n        [OU].[OrganizationId] = @Id\n\n    DELETE AP\n    FROM\n        [dbo].[AccessPolicy] AP\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON [AP].[OrganizationUserId] = [OU].[Id]\n    WHERE\n        [OU].[OrganizationId] = @Id\n\n    DELETE GU\n    FROM\n        [dbo].[GroupUser] GU\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON [GU].[OrganizationUserId] = [OU].[Id]\n    WHERE\n        [OU].[OrganizationId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[OrganizationUser]\n    WHERE\n        [OrganizationId] = @Id\n\n    DELETE\n    FROM\n         [dbo].[ProviderOrganization]\n    WHERE\n        [OrganizationId] = @Id\n\n    EXEC [dbo].[OrganizationApiKey_OrganizationDeleted] @Id\n    EXEC [dbo].[OrganizationConnection_OrganizationDeleted] @Id\n    EXEC [dbo].[OrganizationSponsorship_OrganizationDeleted] @Id\n    EXEC [dbo].[OrganizationDomain_OrganizationDeleted] @Id\n    EXEC [dbo].[OrganizationIntegration_OrganizationDeleted] @Id\n\n    DELETE\n    FROM\n        [dbo].[Project]\n    WHERE\n        [OrganizationId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[Secret]\n    WHERE\n        [OrganizationId] = @Id\n\n    DELETE AK\n    FROM\n        [dbo].[ApiKey] AK\n    INNER JOIN\n        [dbo].[ServiceAccount] SA ON [AK].[ServiceAccountId] = [SA].[Id]\n    WHERE\n        [SA].[OrganizationId] = @Id\n\n    DELETE AP\n    FROM\n        [dbo].[AccessPolicy] AP\n    INNER JOIN\n        [dbo].[ServiceAccount] SA ON [AP].[GrantedServiceAccountId] = [SA].[Id]\n    WHERE\n        [SA].[OrganizationId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[ServiceAccount]\n    WHERE\n        [OrganizationId] = @Id\n\n    -- Delete Notification Status\n    DELETE\n        NS\n    FROM\n        [dbo].[NotificationStatus] NS\n    INNER JOIN\n        [dbo].[Notification] N ON N.[Id] = NS.[NotificationId]\n    WHERE\n        N.[OrganizationId] = @Id\n\n    -- Delete Notification\n    DELETE\n    FROM\n        [dbo].[Notification]\n    WHERE\n        [OrganizationId] = @Id\n\n    -- Delete Organization Application\n    DELETE\n    FROM\n        [dbo].[OrganizationApplication]\n    WHERE\n        [OrganizationId] = @Id\n\n    -- Delete Organization Report\n    DELETE\n    FROM\n        [dbo].[OrganizationReport]\n    WHERE\n        [OrganizationId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[Organization]\n    WHERE\n        [Id] = @Id\n\n    COMMIT TRANSACTION Organization_DeleteById\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_GetOrganizationsForSubscriptionSync.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_GetOrganizationsForSubscriptionSync]\nAS\nBEGIN\n    SELECT *\n    FROM [dbo].[OrganizationView]\n    WHERE [Seats] IS NOT NULL AND [SyncSeats] = 1\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_IncrementSeatCount.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_IncrementSeatCount]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @SeatsToAdd INT,\n    @RequestDate DATETIME2\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE [dbo].[Organization]\n    SET\n        [Seats] = [Seats] + @SeatsToAdd,\n        [SyncSeats] = 1,\n        [RevisionDate] = @RequestDate\n    WHERE [Id] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_ReadAbilities]\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [Id],\n        [UseEvents],\n        [Use2fa],\n        CASE\n        WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN\n            1\n        ELSE\n            0\n        END AS [Using2fa],\n        [UsersGetPremium],\n        [UseCustomPermissions],\n        [UseSso],\n        [UseKeyConnector],\n        [UseScim],\n        [UseResetPassword],\n        [UsePolicies],\n        [Enabled],\n        [LimitCollectionCreation],\n        [LimitCollectionDeletion],\n        [AllowAdminAccessToAllCollectionItems],\n        [UseRiskInsights],\n        [LimitItemDeletion],\n        [UseOrganizationDomains],\n        [UseAdminSponsoredFamilies],\n        [UseAutomaticUserConfirmation],\n        [UsePhishingBlocker],\n        [UseDisableSmAdsForUsers],\n        [UseMyItems]\n    FROM\n        [dbo].[Organization]\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadAddableToProviderByUserId.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_ReadAddableToProviderByUserId]\n    @UserId UNIQUEIDENTIFIER,\n    @ProviderType TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n    SELECT O.* FROM [dbo].[OrganizationUser] AS OU\n    JOIN [dbo].[Organization] AS O ON O.[Id] = OU.[OrganizationId]\n    WHERE\n        OU.[UserId] = @UserId AND\n        OU.[Type] = 0 AND\n        OU.[Status] = 2 AND\n        O.[Enabled] = 1 AND\n        O.[GatewayCustomerId] IS NOT NULL AND\n        O.[GatewaySubscriptionId] IS NOT NULL AND\n        O.[Seats] > 0 AND\n        O.[Status] = 1 AND\n        O.[UseSecretsManager] = 0 AND\n        -- All Teams & Enterprise for MSP\n        (@ProviderType = 0 AND O.[PlanType] IN (2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20) OR\n        -- All Enterprise for MOE\n         @ProviderType = 2 AND O.[PlanType] IN (4, 5, 10, 11, 14, 15, 19, 20));\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadByClaimedUserEmailDomain.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_ReadByClaimedUserEmailDomain]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    WITH CTE_User AS (\n        SELECT\n            U.[Id],\n            SUBSTRING(U.Email, CHARINDEX('@', U.Email) + 1, LEN(U.Email)) AS EmailDomain\n        FROM dbo.[UserView] U\n        WHERE U.[Id] = @UserId\n    )\n    SELECT O.*\n    FROM CTE_User CU\n             INNER JOIN dbo.[OrganizationUserView] OU ON CU.[Id] = OU.[UserId]\n             INNER JOIN dbo.[OrganizationView] O ON OU.[OrganizationId] = O.[Id]\n             INNER JOIN dbo.[OrganizationDomainView] OD ON OU.[OrganizationId] = OD.[OrganizationId]\n    WHERE OD.[VerifiedDate] IS NOT NULL\n      AND CU.EmailDomain = OD.[DomainName]\n      AND O.[Enabled] = 1\n      AND OU.[Status] != 0 -- Exclude invited users\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadByEnabled.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_ReadByEnabled]\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationView]\n    WHERE\n        [Enabled] = 1\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadByGatewayCustomerId.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_ReadByGatewayCustomerId]\n    @GatewayCustomerId VARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationView]\n    WHERE\n        [GatewayCustomerId] = @GatewayCustomerId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadByGatewaySubscriptionId.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_ReadByGatewaySubscriptionId]\n    @GatewaySubscriptionId VARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationView]\n    WHERE\n        [GatewaySubscriptionId] = @GatewaySubscriptionId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadByIdentifier.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_ReadByIdentifier]\n    @Identifier NVARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationView]\n    WHERE\n        [Identifier] = @Identifier\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadByLicenseKey.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_ReadByLicenseKey]\n    @LicenseKey VARCHAR (100)\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n    *\nFROM\n    [dbo].[OrganizationView]\nWHERE\n    [LicenseKey] = @LicenseKey\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadByProviderId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_ReadByProviderId]\n    @ProviderId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        O.*\n    FROM\n        [dbo].[OrganizationView] O\n    INNER JOIN\n        [dbo].[ProviderOrganization] PO ON O.[Id] = PO.[OrganizationId]\n    WHERE\n        PO.[ProviderId] = @ProviderId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        O.*\n    FROM\n        [dbo].[OrganizationView] O\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId]\n    WHERE\n        OU.[UserId] = @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadManyByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_ReadManyByIds] @OrganizationIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT o.[Id],\n           o.[Identifier],\n           o.[Name],\n           o.[BusinessName],\n           o.[BusinessAddress1],\n           o.[BusinessAddress2],\n           o.[BusinessAddress3],\n           o.[BusinessCountry],\n           o.[BusinessTaxNumber],\n           o.[BillingEmail],\n           o.[Plan],\n           o.[PlanType],\n           o.[Seats],\n           o.[MaxCollections],\n           o.[UsePolicies],\n           o.[UseSso],\n           o.[UseGroups],\n           o.[UseDirectory],\n           o.[UseEvents],\n           o.[UseTotp],\n           o.[Use2fa],\n           o.[UseApi],\n           o.[UseResetPassword],\n           o.[SelfHost],\n           o.[UsersGetPremium],\n           o.[Storage],\n           o.[MaxStorageGb],\n           o.[Gateway],\n           o.[GatewayCustomerId],\n           o.[GatewaySubscriptionId],\n           o.[ReferenceData],\n           o.[Enabled],\n           o.[LicenseKey],\n           o.[PublicKey],\n           o.[PrivateKey],\n           o.[TwoFactorProviders],\n           o.[ExpirationDate],\n           o.[CreationDate],\n           o.[RevisionDate],\n           o.[OwnersNotifiedOfAutoscaling],\n           o.[MaxAutoscaleSeats],\n           o.[UseKeyConnector],\n           o.[UseScim],\n           o.[UseCustomPermissions],\n           o.[UseSecretsManager],\n           o.[Status],\n           o.[UsePasswordManager],\n           o.[SmSeats],\n           o.[SmServiceAccounts],\n           o.[MaxAutoscaleSmSeats],\n           o.[MaxAutoscaleSmServiceAccounts],\n           o.[SecretsManagerBeta],\n           o.[LimitCollectionCreation],\n           o.[LimitCollectionDeletion],\n           o.[LimitItemDeletion],\n           o.[AllowAdminAccessToAllCollectionItems],\n           o.[UseRiskInsights]\n    FROM [dbo].[OrganizationView] o\n    INNER JOIN @OrganizationIds ids ON o.[Id] = ids.[Id]\n\nEND\n\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadOccupiedSeatCountByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_ReadOccupiedSeatCountByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n    \n    SELECT\n        (\n            -- Count organization users\n            SELECT COUNT(1)\n            FROM [dbo].[OrganizationUserView]\n            WHERE OrganizationId = @OrganizationId\n            AND Status >= 0 --Invited\n        ) as Users,\n        (\n            -- Count admin-initiated sponsorships towards the seat count\n            -- Introduced in https://bitwarden.atlassian.net/browse/PM-17772\n            SELECT COUNT(1)\n            FROM [dbo].[OrganizationSponsorship]\n            WHERE SponsoringOrganizationId = @OrganizationId\n            AND IsAdminInitiated = 1\n            AND (\n                -- Not marked for deletion - always count\n                (ToDelete = 0) \n                OR\n                -- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)\n                (ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())\n            )\n            AND (\n                -- SENT status: When SponsoredOrganizationId is null\n                SponsoredOrganizationId IS NULL\n                OR\n                -- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future\n                (SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))\n            )\n        ) as Sponsored\nEND\nGO "
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_ReadSelfHostedDetailsById.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_ReadSelfHostedDetailsById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    EXEC [dbo].[Organization_ReadById] @Id\n    EXEC [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId] @Id\n    EXEC [dbo].[Collection_ReadCountByOrganizationId] @Id\n    EXEC [dbo].[Group_ReadCountByOrganizationId] @Id\n    EXEC [dbo].[OrganizationUser_ReadByOrganizationId] @Id, NULL\n    EXEC [dbo].[Policy_ReadByOrganizationId] @Id\n    EXEC [dbo].[SsoConfig_ReadByOrganizationId] @Id\n    EXEC [dbo].[OrganizationConnection_ReadByOrganizationIdType] @Id, 2 --Scim connection type\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_Search.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_Search]\n    @Name NVARCHAR(50),\n    @UserEmail NVARCHAR(256),\n    @Paid BIT,\n    @Skip INT = 0,\n    @Take INT = 25\nWITH RECOMPILE\nAS\nBEGIN\n    SET NOCOUNT ON\n    DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%'\n\n    IF @UserEmail IS NOT NULL\n    BEGIN\n        SELECT\n            O.*\n        FROM\n            [dbo].[OrganizationView] O\n        INNER JOIN\n            [dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId]\n        INNER JOIN\n            [dbo].[User] U ON U.[Id] = OU.[UserId]\n        WHERE\n            (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)\n            AND (@UserEmail IS NULL OR U.[Email] = @UserEmail)\n            AND\n            (\n                @Paid IS NULL OR\n                (\n                    (@Paid = 1 AND O.[GatewaySubscriptionId] IS NOT NULL) OR\n                    (@Paid = 0 AND O.[GatewaySubscriptionId] IS NULL)\n                )\n            )\n        ORDER BY O.[CreationDate] DESC\n        OFFSET @Skip ROWS\n        FETCH NEXT @Take ROWS ONLY\n    END\n    ELSE\n    BEGIN\n        SELECT\n            O.*\n        FROM\n            [dbo].[OrganizationView] O\n        WHERE\n            (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)\n            AND\n            (\n                @Paid IS NULL OR\n                (\n                    (@Paid = 1 AND O.[GatewaySubscriptionId] IS NOT NULL) OR\n                    (@Paid = 0 AND O.[GatewaySubscriptionId] IS NULL)\n                )\n            )\n        ORDER BY O.[CreationDate] DESC\n        OFFSET @Skip ROWS\n        FETCH NEXT @Take ROWS ONLY\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_UnassignedToProviderSearch.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_UnassignedToProviderSearch]\n    @Name NVARCHAR(55),\n    @OwnerEmail NVARCHAR(256),\n    @Skip INT = 0,\n    @Take INT = 25\nWITH RECOMPILE\nAS\nBEGIN\n    SET NOCOUNT ON\n    DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%'\n    DECLARE @OwnerLikeSearch NVARCHAR(55) = @OwnerEmail + '%'\n\n    IF @OwnerEmail IS NOT NULL\n    BEGIN\n        SELECT\n            O.*\n        FROM\n            [dbo].[OrganizationView] O\n            INNER JOIN\n                [dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId]\n            INNER JOIN\n                [dbo].[User] U ON U.[Id] = OU.[UserId]\n        WHERE\n            O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families'\n            AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])\n            AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)\n            AND (U.[Email] LIKE @OwnerLikeSearch)\n        ORDER BY O.[CreationDate] DESC, O.[Id]\n        OFFSET @Skip ROWS\n        FETCH NEXT @Take ROWS ONLY\n    END\n    ELSE\n    BEGIN\n        SELECT\n            O.*\n        FROM\n            [dbo].[OrganizationView] O\n        WHERE\n            O.[PlanType] NOT IN (0, 1, 6, 7) -- Not 'Free', 'Custom' or 'Families'\n            AND NOT EXISTS (SELECT * FROM [dbo].[ProviderOrganizationView] PO WHERE PO.[OrganizationId] = O.[Id])\n            AND (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)\n        ORDER BY O.[CreationDate] DESC, O.[Id]\n        OFFSET @Skip ROWS\n        FETCH NEXT @Take ROWS ONLY\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_Update]\n    @Id UNIQUEIDENTIFIER,\n    @Identifier NVARCHAR(50),\n    @Name NVARCHAR(50),\n    @BusinessName NVARCHAR(50),\n    @BusinessAddress1 NVARCHAR(50),\n    @BusinessAddress2 NVARCHAR(50),\n    @BusinessAddress3 NVARCHAR(50),\n    @BusinessCountry VARCHAR(2),\n    @BusinessTaxNumber NVARCHAR(30),\n    @BillingEmail NVARCHAR(256),\n    @Plan NVARCHAR(50),\n    @PlanType TINYINT,\n    @Seats INT,\n    @MaxCollections SMALLINT,\n    @UsePolicies BIT,\n    @UseSso BIT,\n    @UseGroups BIT,\n    @UseDirectory BIT,\n    @UseEvents BIT,\n    @UseTotp BIT,\n    @Use2fa BIT,\n    @UseApi BIT,\n    @UseResetPassword BIT,\n    @SelfHost BIT,\n    @UsersGetPremium BIT,\n    @Storage BIGINT,\n    @MaxStorageGb SMALLINT,\n    @Gateway TINYINT,\n    @GatewayCustomerId VARCHAR(50),\n    @GatewaySubscriptionId VARCHAR(50),\n    @ReferenceData VARCHAR(MAX),\n    @Enabled BIT,\n    @LicenseKey VARCHAR(100),\n    @PublicKey VARCHAR(MAX),\n    @PrivateKey VARCHAR(MAX),\n    @TwoFactorProviders NVARCHAR(MAX),\n    @ExpirationDate DATETIME2(7),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @OwnersNotifiedOfAutoscaling DATETIME2(7),\n    @MaxAutoscaleSeats INT,\n    @UseKeyConnector BIT = 0,\n    @UseScim BIT = 0,\n    @UseCustomPermissions BIT = 0,\n    @UseSecretsManager BIT = 0,\n    @Status TINYINT = 0,\n    @UsePasswordManager BIT = 1,\n    @SmSeats INT = null,\n    @SmServiceAccounts INT = null,\n    @MaxAutoscaleSmSeats INT = null,\n    @MaxAutoscaleSmServiceAccounts INT = null,\n    @SecretsManagerBeta BIT = 0,\n    @LimitCollectionCreation BIT = null,\n    @LimitCollectionDeletion BIT = null,\n    @AllowAdminAccessToAllCollectionItems BIT = 0,\n    @UseRiskInsights BIT = 0,\n    @LimitItemDeletion BIT = 0,\n    @UseOrganizationDomains BIT = 0,\n    @UseAdminSponsoredFamilies BIT = 0,\n    @SyncSeats BIT = 0,\n    @UseAutomaticUserConfirmation BIT = 0,\n    @UsePhishingBlocker BIT = 0,\n    @UseDisableSmAdsForUsers BIT = 0,\n    @UseMyItems BIT = 0\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Organization]\n    SET\n        [Identifier] = @Identifier,\n        [Name] = @Name,\n        [BusinessName] = @BusinessName,\n        [BusinessAddress1] = @BusinessAddress1,\n        [BusinessAddress2] = @BusinessAddress2,\n        [BusinessAddress3] = @BusinessAddress3,\n        [BusinessCountry] = @BusinessCountry,\n        [BusinessTaxNumber] = @BusinessTaxNumber,\n        [BillingEmail] = @BillingEmail,\n        [Plan] = @Plan,\n        [PlanType] = @PlanType,\n        [Seats] = @Seats,\n        [MaxCollections] = @MaxCollections,\n        [UsePolicies] = @UsePolicies,\n        [UseSso] = @UseSso,\n        [UseGroups] = @UseGroups,\n        [UseDirectory] = @UseDirectory,\n        [UseEvents] = @UseEvents,\n        [UseTotp] = @UseTotp,\n        [Use2fa] = @Use2fa,\n        [UseApi] = @UseApi,\n        [UseResetPassword] = @UseResetPassword,\n        [SelfHost] = @SelfHost,\n        [UsersGetPremium] = @UsersGetPremium,\n        [Storage] = @Storage,\n        [MaxStorageGb] = @MaxStorageGb,\n        [Gateway] = @Gateway,\n        [GatewayCustomerId] = @GatewayCustomerId,\n        [GatewaySubscriptionId] = @GatewaySubscriptionId,\n        [ReferenceData] = @ReferenceData,\n        [Enabled] = @Enabled,\n        [LicenseKey] = @LicenseKey,\n        [PublicKey] = @PublicKey,\n        [PrivateKey] = @PrivateKey,\n        [TwoFactorProviders] = @TwoFactorProviders,\n        [ExpirationDate] = @ExpirationDate,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling,\n        [MaxAutoscaleSeats] = @MaxAutoscaleSeats,\n        [UseKeyConnector] = @UseKeyConnector,\n        [UseScim] = @UseScim,\n        [UseCustomPermissions] = @UseCustomPermissions,\n        [UseSecretsManager] = @UseSecretsManager,\n        [Status] = @Status,\n        [UsePasswordManager] = @UsePasswordManager,\n        [SmSeats] = @SmSeats,\n        [SmServiceAccounts] = @SmServiceAccounts,\n        [MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats,\n        [MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts,\n        [SecretsManagerBeta] = @SecretsManagerBeta,\n        [LimitCollectionCreation] = @LimitCollectionCreation,\n        [LimitCollectionDeletion] = @LimitCollectionDeletion,\n        [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems,\n        [UseRiskInsights] = @UseRiskInsights,\n        [LimitItemDeletion] = @LimitItemDeletion,\n        [UseOrganizationDomains] = @UseOrganizationDomains,\n        [UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies,\n        [SyncSeats] = @SyncSeats,\n        [UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation,\n        [UsePhishingBlocker] = @UsePhishingBlocker,\n        [MaxStorageGbIncreased] = @MaxStorageGb,\n        [UseDisableSmAdsForUsers] = @UseDisableSmAdsForUsers,\n        [UseMyItems] = @UseMyItems\n    WHERE\n        [Id] = @Id;\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_UpdateStorage.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Organization_UpdateStorage]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @AttachmentStorage BIGINT\n    DECLARE @SendStorage BIGINT\n\n    CREATE TABLE #OrgStorageUpdateTemp\n    ( \n        [Id] UNIQUEIDENTIFIER NOT NULL,\n        [Attachments] VARCHAR(MAX) NULL\n    )\n\n    INSERT INTO #OrgStorageUpdateTemp\n    SELECT\n        [Id],\n        [Attachments]\n    FROM\n        [dbo].[Cipher]\n    WHERE\n        [UserId] IS NULL\n        AND [OrganizationId] = @Id\n\n    ;WITH [CTE] AS (\n        SELECT\n            [Id],\n            (\n                SELECT\n                    SUM(CAST(JSON_VALUE(value,'$.Size') AS BIGINT))\n                FROM\n                    OPENJSON([Attachments])\n            ) [Size]\n        FROM\n            #OrgStorageUpdateTemp\n    )\n    SELECT\n        @AttachmentStorage = SUM([Size])\n    FROM\n        [CTE]\n\n    DROP TABLE #OrgStorageUpdateTemp\n\n    ;WITH [CTE] AS (\n        SELECT\n            [Id],\n            CAST(JSON_VALUE([Data],'$.Size') AS BIGINT) [Size]\n        FROM\n            [Send]\n        WHERE\n            [UserId] IS NULL\n            AND [OrganizationId] = @Id\n    )\n    SELECT\n        @SendStorage = SUM([CTE].[Size])\n    FROM\n        [CTE]\n\n    UPDATE\n        [dbo].[Organization]\n    SET\n        [Storage] = (ISNULL(@AttachmentStorage, 0) + ISNULL(@SendStorage, 0)),\n        [RevisionDate] = GETUTCDATE()\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Organization_UpdateSubscriptionStatus.sql",
    "content": "CREATE PROCEDURE [dbo].[Organization_UpdateSubscriptionStatus]\n    @SuccessfulOrganizations AS [dbo].[GuidIdArray] READONLY,\n    @SyncDate DATETIME2\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE o\n    SET\n        [SyncSeats] = 0,\n        [RevisionDate] = @SyncDate\n    FROM [dbo].[Organization] o\n    INNER JOIN @SuccessfulOrganizations success on success.Id = o.Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PasswordHealthReportApplication_Create.sql",
    "content": "CREATE PROCEDURE dbo.PasswordHealthReportApplication_Create\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Uri nvarchar(max),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\n    SET NOCOUNT ON;\n    INSERT INTO dbo.PasswordHealthReportApplication ( Id, OrganizationId, Uri, CreationDate, RevisionDate ) \n    VALUES ( @Id, @OrganizationId, @Uri, @CreationDate, @RevisionDate )"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PasswordHealthReportApplication_DeleteById.sql",
    "content": "CREATE PROCEDURE dbo.PasswordHealthReportApplication_DeleteById\n    @Id UNIQUEIDENTIFIER\nAS\n    SET NOCOUNT ON;\n\n    IF @Id IS NULL\n        THROW 50000, 'Id cannot be null', 1;\n\n    DELETE FROM [dbo].[PasswordHealthReportApplication]\n    WHERE [Id] = @Id"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PasswordHealthReportApplication_ReadById.sql",
    "content": "CREATE PROCEDURE dbo.PasswordHealthReportApplication_ReadById\n    @Id UNIQUEIDENTIFIER\nAS\n    SET NOCOUNT ON;\n    \n    IF @Id IS NULL\n        THROW 50000, 'Id cannot be null', 1;\n        \n    SELECT \n        Id,\n        OrganizationId,\n        Uri,\n        CreationDate,\n        RevisionDate\n    FROM [dbo].[PasswordHealthReportApplicationView]\n    WHERE Id = @Id;"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PasswordHealthReportApplication_ReadByOrganizationId.sql",
    "content": "CREATE PROCEDURE dbo.PasswordHealthReportApplication_ReadByOrganizationId\n    @OrganizationId UNIQUEIDENTIFIER\nAS\n    SET NOCOUNT ON;\n    \n    IF @OrganizationId IS NULL\n        THROW 50000, 'OrganizationId cannot be null', 1;\n        \n    SELECT \n        Id,\n        OrganizationId,\n        Uri,\n        CreationDate,\n        RevisionDate\n    FROM [dbo].[PasswordHealthReportApplicationView]\n    WHERE OrganizationId = @OrganizationId;"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PasswordHealthReportApplication_Update.sql",
    "content": "CREATE PROC dbo.PasswordHealthReportApplication_Update\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Uri nvarchar(max),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\n    SET NOCOUNT ON;\n    UPDATE dbo.PasswordHealthReportApplication \n        SET OrganizationId = @OrganizationId, \n            Uri = @Uri, \n            RevisionDate = @RevisionDate\n    WHERE Id = @Id"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PlayItem_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[PlayItem_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @PlayId NVARCHAR(256),\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @CreationDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[PlayItem]\n    (\n        [Id],\n        [PlayId],\n        [UserId],\n        [OrganizationId],\n        [CreationDate]\n    )\n    VALUES\n    (\n        @Id,\n        @PlayId,\n        @UserId,\n        @OrganizationId,\n        @CreationDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PlayItem_DeleteByPlayId.sql",
    "content": "CREATE PROCEDURE [dbo].[PlayItem_DeleteByPlayId]\n    @PlayId NVARCHAR(256)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[PlayItem]\n    WHERE\n        [PlayId] = @PlayId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PlayItem_ReadByPlayId.sql",
    "content": "CREATE PROCEDURE [dbo].[PlayItem_ReadByPlayId]\n    @PlayId NVARCHAR(256)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [Id],\n        [PlayId],\n        [UserId],\n        [OrganizationId],\n        [CreationDate]\n    FROM\n        [dbo].[PlayItem]\n    WHERE\n        [PlayId] = @PlayId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[PolicyDetails_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @PolicyType  TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    -- Get users in the given organization (@OrganizationId) by matching either on UserId or Email.\n    ;WITH GivenOrgUsers AS (\n        SELECT\n            OU.[UserId],\n            U.[Email]\n         FROM [dbo].[OrganizationUserView] OU\n            INNER JOIN [dbo].[UserView] U ON U.[Id] = OU.[UserId]\n         WHERE OU.[OrganizationId] = @OrganizationId\n\n         UNION ALL\n\n        SELECT\n            U.[Id] AS [UserId],\n            U.[Email]\n        FROM [dbo].[OrganizationUserView] OU\n            INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email]\n        WHERE OU.[OrganizationId] = @OrganizationId\n    ),\n\n    -- Retrieve all organization users that match on either UserId or Email from GivenOrgUsers.\n    AllOrgUsers AS (\n        SELECT\n            OU.[Id] AS [OrganizationUserId],\n            OU.[UserId],\n            OU.[OrganizationId],\n            AU.[Email],\n            OU.[Type] AS [OrganizationUserType],\n            OU.[Status] AS [OrganizationUserStatus],\n            OU.[Permissions] AS [OrganizationUserPermissionsData]\n        FROM [dbo].[OrganizationUserView] OU\n            INNER JOIN GivenOrgUsers AU ON AU.[UserId] = OU.[UserId]\n        UNION ALL\n        SELECT\n            OU.[Id] AS [OrganizationUserId],\n            AU.[UserId],\n            OU.[OrganizationId],\n            AU.[Email],\n            OU.[Type] AS [OrganizationUserType],\n            OU.[Status] AS [OrganizationUserStatus],\n            OU.[Permissions] AS [OrganizationUserPermissionsData]\n        FROM [dbo].[OrganizationUserView] OU\n            INNER JOIN GivenOrgUsers AU ON AU.[Email] = OU.[Email]\n    )\n\n    -- Return policy details for each matching organization user.\n    SELECT\n        OU.[OrganizationUserId],\n        OU.[UserId],\n        P.[OrganizationId],\n        P.[Type] AS [PolicyType],\n            P.[Data] AS [PolicyData],\n            OU.[OrganizationUserType],\n            OU.[OrganizationUserStatus],\n            OU.[OrganizationUserPermissionsData],\n            -- Check if user is a provider for the organization\n            CASE\n                WHEN EXISTS (\n                    SELECT 1\n                    FROM [dbo].[ProviderUserView] PU\n                    INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]\n                    WHERE PU.[UserId] = OU.[UserId]\n                    AND PO.[OrganizationId] = P.[OrganizationId]\n                ) THEN 1\n                ELSE 0\n            END AS [IsProvider]\n    FROM [dbo].[PolicyView] P\n    INNER JOIN [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id]\n    INNER JOIN AllOrgUsers OU ON OU.[OrganizationId] = O.[Id]\n    WHERE P.[Enabled] = 1\n      AND O.[Enabled] = 1\n      AND O.[UsePolicies] = 1\n      AND P.[Type] = @PolicyType\n\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserId.sql",
    "content": "CREATE PROCEDURE [dbo].[PolicyDetails_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\nSELECT\n    OU.[Id] AS OrganizationUserId,\n    P.[OrganizationId],\n    P.[Type] AS PolicyType,\n    P.[Data] AS PolicyData,\n    OU.[Type] AS OrganizationUserType,\n    OU.[Status] AS OrganizationUserStatus,\n    OU.[Permissions] AS OrganizationUserPermissionsData,\n    CASE WHEN EXISTS (\n            SELECT 1\n            FROM [dbo].[ProviderUserView] PU\n            INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]\n            WHERE PU.[UserId] = OU.[UserId] AND PO.[OrganizationId] = P.[OrganizationId]\n        ) THEN 1 ELSE 0 END AS IsProvider\nFROM [dbo].[PolicyView] P\nINNER JOIN [dbo].[OrganizationUserView] OU\n    ON P.[OrganizationId] = OU.[OrganizationId]\nINNER JOIN [dbo].[OrganizationView] O\n    ON P.[OrganizationId] = O.[Id]\nWHERE\n    P.Enabled = 1\n    AND O.Enabled = 1\n    AND O.UsePolicies = 1\n    AND (\n            -- OrgUsers who have accepted their invite and are linked to a UserId\n            -- (Note: this excludes \"invited but revoked\" users who don't have an OU.UserId yet,\n            -- but those users will go through policy enforcement later as part of accepting their invite after being restored.\n            -- This is an intentionally unhandled edge case for now.)\n            (OU.[Status] != 0 AND OU.[UserId] = @UserId)\n\n            -- 'Invited' OrgUsers are not linked to a UserId yet, so we have to look up their email\n            OR EXISTS (\n                SELECT 1\n                FROM [dbo].[UserView] U\n                WHERE U.[Id] = @UserId AND OU.[Email] = U.[Email] AND OU.[Status] = 0\n            )\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserIdPolicyType.sql",
    "content": "CREATE PROCEDURE [dbo].[PolicyDetails_ReadByUserIdPolicyType]\n    @UserId UNIQUEIDENTIFIER,\n    @PolicyType TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserEmail NVARCHAR(256)\n    SELECT @UserEmail = Email\n    FROM\n        [dbo].[UserView]\n    WHERE\n        Id = @UserId\n\n    ;WITH OrgUsers AS\n    (\n        -- Non-invited users (Status != 0): direct UserId match\n        SELECT\n            OU.[Id],\n            OU.[OrganizationId],\n            OU.[Type],\n            OU.[Status],\n            OU.[Permissions]\n        FROM\n            [dbo].[OrganizationUserView] OU\n        WHERE\n            OU.[Status] != 0\n            AND OU.[UserId] = @UserId\n\n        UNION ALL\n\n        -- Invited users (Status = 0): email match\n        SELECT\n            OU.[Id],\n            OU.[OrganizationId],\n            OU.[Type],\n            OU.[Status],\n            OU.[Permissions]\n        FROM\n            [dbo].[OrganizationUserView] OU\n        WHERE\n            OU.[Status] = 0\n            AND OU.[Email] = @UserEmail\n            AND @UserEmail IS NOT NULL\n    ),\n    Providers AS\n    (\n        SELECT\n            OrganizationId\n        FROM\n            [dbo].[UserProviderAccessView]\n        WHERE\n            UserId = @UserId\n    )\n    SELECT\n        OU.[Id] AS OrganizationUserId,\n        P.[OrganizationId],\n        P.[Type] AS PolicyType,\n        P.[Data] AS PolicyData,\n        OU.[Type] AS OrganizationUserType,\n        OU.[Status] AS OrganizationUserStatus,\n        OU.[Permissions] AS OrganizationUserPermissionsData,\n        CASE WHEN PR.[OrganizationId] IS NULL THEN 0 ELSE 1 END AS IsProvider\n    FROM\n        [dbo].[PolicyView] P\n    INNER JOIN\n        OrgUsers OU ON P.[OrganizationId] = OU.[OrganizationId]\n    INNER JOIN\n        [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id]\n    LEFT JOIN\n        Providers PR ON PR.[OrganizationId] = OU.[OrganizationId]\n    WHERE\n        P.[Type] = @PolicyType\n        AND P.[Enabled] = 1\n        AND O.[Enabled] = 1\n        AND O.[UsePolicies] = 1\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserIdsPolicyType.sql",
    "content": "CREATE PROCEDURE [dbo].[PolicyDetails_ReadByUserIdsPolicyType]\n    @UserIds AS [dbo].[GuidIdArray] READONLY,\n    @PolicyType AS TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    WITH AcceptedUsers AS (\n        -- Branch 1: Accepted users linked by UserId\n        SELECT\n            OU.[Id]          AS OrganizationUserId,\n            P.[OrganizationId],\n            P.[Type]         AS PolicyType,\n            P.[Data]         AS PolicyData,\n            OU.[Type]        AS OrganizationUserType,\n            OU.[Status]      AS OrganizationUserStatus,\n            OU.[Permissions] AS OrganizationUserPermissionsData,\n            OU.[UserId]      AS UserId\n        FROM [dbo].[PolicyView] P\n                 INNER JOIN [dbo].[OrganizationUserView] OU ON P.[OrganizationId] = OU.[OrganizationId]\n                 INNER JOIN [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id]\n                 INNER JOIN @UserIds UI ON OU.[UserId] = UI.Id -- Direct join with TVP\n        WHERE\n            P.Enabled = 1\n          AND O.Enabled = 1\n          AND O.UsePolicies = 1\n          AND OU.[Status] != 0 -- Accepted users\n          AND P.[Type] = @PolicyType\n    ),\n         InvitedUsers AS (\n             -- Branch 2: Invited users matched by email\n             SELECT\n                 OU.[Id]          AS OrganizationUserId,\n                 P.[OrganizationId],\n                 P.[Type]         AS PolicyType,\n                 P.[Data]         AS PolicyData,\n                 OU.[Type]        AS OrganizationUserType,\n                 OU.[Status]      AS OrganizationUserStatus,\n                 OU.[Permissions] AS OrganizationUserPermissionsData,\n                 U.[Id]           AS UserId\n             FROM [dbo].[PolicyView] P\n                      INNER JOIN [dbo].[OrganizationUserView] OU ON P.[OrganizationId] = OU.[OrganizationId]\n                      INNER JOIN [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id]\n                      INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email] -- Join on email\n                      INNER JOIN @UserIds UI ON U.[Id] = UI.Id -- Join with TVP\n             WHERE\n                 P.Enabled = 1\n               AND O.Enabled = 1\n               AND O.UsePolicies = 1\n               AND OU.[Status] = 0 -- Invited users only\n               AND P.[Type] = @PolicyType\n         ),\n         AllUsers AS (\n             -- Combine both user sets\n             SELECT * FROM AcceptedUsers\n             UNION\n             SELECT * FROM InvitedUsers\n         ),\n         ProviderLookup AS (\n             -- Pre-calculate provider relationships for all relevant user/org combinations\n             SELECT DISTINCT\n                 PU.[UserId],\n                 PO.[OrganizationId]\n             FROM [dbo].[ProviderUserView] PU\n                      INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]\n                      INNER JOIN AllUsers AU ON PU.[UserId] = AU.UserId AND PO.[OrganizationId] = AU.OrganizationId\n         )\n    -- Final result with efficient IsProvider lookup\n    SELECT\n        AU.OrganizationUserId,\n        AU.OrganizationId,\n        AU.PolicyType,\n        AU.PolicyData,\n        AU.OrganizationUserType,\n        AU.OrganizationUserStatus,\n        AU.OrganizationUserPermissionsData,\n        AU.UserId,\n        IIF(PL.UserId IS NOT NULL, 1, 0) AS IsProvider\n    FROM AllUsers AU\n             LEFT JOIN ProviderLookup PL\n                       ON AU.UserId = PL.UserId\n                           AND AU.OrganizationId = PL.OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Policy_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Policy_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data NVARCHAR(MAX),\n    @Enabled BIT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Policy]\n    (\n        [Id],\n        [OrganizationId],\n        [Type],\n        [Data],\n        [Enabled],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @OrganizationId,\n        @Type,\n        @Data,\n        @Enabled,\n        @CreationDate,\n        @RevisionDate\n    )\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Policy_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Policy_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @OrganizationId UNIQUEIDENTIFIER = (SELECT TOP 1 [OrganizationId] FROM [dbo].[Policy] WHERE [Id] = @Id)\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\n    END\n\n    DELETE\n    FROM\n        [dbo].[Policy]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Policy_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Policy_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[PolicyView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Policy_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Policy_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[PolicyView]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Policy_ReadByOrganizationIdType.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Policy_ReadByOrganizationIdType]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT TOP 1\n        *\n    FROM\n        [dbo].[PolicyView]\n    WHERE\n        [OrganizationId] = @OrganizationId\n        AND [Type] = @Type\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Policy_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Policy_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        P.*\n    FROM\n        [dbo].[PolicyView] P\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON P.[OrganizationId] = OU.[OrganizationId]\n    INNER JOIN\n        [dbo].[Organization] O ON OU.[OrganizationId] = O.[Id]\n    WHERE\n        OU.[UserId] = @UserId\n        AND OU.[Status] = 2 -- 2 = Confirmed\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Policy_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Policy_Update]\n    @Id UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data NVARCHAR(MAX),\n    @Enabled BIT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Policy]\n    SET\n        [OrganizationId] = @OrganizationId,\n        [Type] = @Type,\n        [Data] = @Data,\n        [Enabled] = @Enabled,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderOrganizationOrganizationDetails_ReadByProviderId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderOrganizationOrganizationDetails_ReadByProviderId]\n    @ProviderId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderOrganizationOrganizationDetailsView]\n    WHERE\n        [ProviderId] = @ProviderId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderOrganizationProviderDetails_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderOrganizationProviderDetails_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n    PO.Id,\n    PO.OrganizationId,\n    PO.ProviderId,\n    P.Name as ProviderName,\n    P.[Type] as ProviderType\nFROM\n    [dbo].[ProviderOrganizationView] PO\nINNER JOIN\n    [dbo].[OrganizationUser] OU ON PO.OrganizationId = OU.OrganizationId\nINNER JOIN\n        [dbo].[Provider] P ON PO.ProviderId = P.Id\nWHERE\n    OU.UserId = @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderOrganization_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderOrganization_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @ProviderId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Key VARCHAR(MAX),\n    @Settings NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[ProviderOrganization]\n    (\n        [Id],\n        [ProviderId],\n        [OrganizationId],\n        [Key],\n        [Settings],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @ProviderId,\n        @OrganizationId,\n        @Key,\n        @Settings,\n        @CreationDate,\n        @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderOrganization_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderOrganization_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    BEGIN TRANSACTION ProviderOrganization_DeleteById\n\n    DECLARE @ProviderId UNIQUEIDENTIFIER\n    DECLARE @OrganizationId UNIQUEIDENTIFIER\n\n    SELECT\n        @ProviderId = [ProviderId],\n        @OrganizationId = [OrganizationId]\n    FROM\n        [dbo].[ProviderOrganization]\n    WHERE\n        [Id] = @Id\n\n    DELETE\n    FROM\n        [dbo].[ProviderOrganization]\n    WHERE\n        [Id] = @Id\n\n    COMMIT TRANSACTION ProviderOrganization_DeleteById\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderOrganization_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderOrganization_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderOrganizationView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderOrganization_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderOrganization_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderOrganizationView]\n    WHERE\n        [OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderOrganization_ReadCountByOrganizationIds.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderOrganization_ReadCountByOrganizationIds]\n    @Ids AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    IF (SELECT COUNT(1) FROM @Ids) < 1\n    BEGIN\n        RETURN(-1)\n    END\n\n    SELECT\n        COUNT(1)\n    FROM\n        [dbo].[ProviderOrganizationView]\n    WHERE\n        [OrganizationId] IN (SELECT [Id] FROM @Ids)\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderOrganization_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderOrganization_Update]\n    @Id UNIQUEIDENTIFIER,\n    @ProviderId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Key VARCHAR(MAX),\n    @Settings NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[ProviderOrganization]\n    SET\n        [ProviderId] = @ProviderId,\n        [OrganizationId] = @OrganizationId,\n        [Key] = @Key,\n        [Settings] = @Settings,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUserProviderDetails_ReadByUserIdStatus.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUserProviderDetails_ReadByUserIdStatus]\n    @UserId UNIQUEIDENTIFIER,\n    @Status TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderUserProviderDetailsView]\n    WHERE\n        [UserId] = @UserId\n        AND [ProviderStatus] != 0 -- Not Pending\n        AND (@Status IS NULL OR [Status] = @Status)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUserProviderOrganizationDetails_ReadByUserIdStatus.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUserProviderOrganizationDetails_ReadByUserIdStatus]\n    @UserId UNIQUEIDENTIFIER,\n    @Status TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderUserProviderOrganizationDetailsView]\n    WHERE\n        [UserId] = @UserId\n        AND (@Status IS NULL OR [Status] = @Status)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUserUserDetails_ReadByProviderId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUserUserDetails_ReadByProviderId]\n    @ProviderId UNIQUEIDENTIFIER,\n    @Status TINYINT = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderUserUserDetailsView]\n    WHERE\n        [ProviderId] = @ProviderId\n        AND [Status] = COALESCE(@Status, [Status])\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @ProviderId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @Key VARCHAR(MAX),\n    @Status TINYINT,\n    @Type TINYINT,\n    @Permissions NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[ProviderUser]\n    (\n        [Id],\n        [ProviderId],\n        [UserId],\n        [Email],\n        [Key],\n        [Status],\n        [Type],\n        [Permissions],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @ProviderId,\n        @UserId,\n        @Email,\n        @Key,\n        @Status,\n        @Type,\n        @Permissions,\n        @CreationDate,\n        @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByProviderUserId] @Id\n\n    BEGIN TRANSACTION ProviderUser_DeleteById\n\n    DECLARE @ProviderId UNIQUEIDENTIFIER\n    DECLARE @UserId UNIQUEIDENTIFIER\n\n    SELECT\n        @ProviderId = [ProviderId],\n        @UserId = [UserId]\n    FROM\n        [dbo].[ProviderUser]\n    WHERE\n        [Id] = @Id\n\n    DELETE\n    FROM\n        [dbo].[ProviderUser]\n    WHERE\n        [Id] = @Id\n\n    COMMIT TRANSACTION ProviderUser_DeleteById\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_DeleteByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderUser_DeleteByIds]\n    @Ids [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByProviderUserIds] @Ids\n\n    DECLARE @UserAndProviderIds [dbo].[TwoGuidIdArray]\n\n    INSERT INTO @UserAndProviderIds\n        (Id1, Id2)\n    SELECT\n        UserId,\n        ProviderId\n    FROM\n        [dbo].[ProviderUser] PU\n    INNER JOIN\n        @Ids PUIds ON PUIds.Id = PU.Id\n    WHERE\n        UserId IS NOT NULL AND\n        ProviderId IS NOT NULL\n\n    DECLARE @BatchSize INT = 100\n\n    -- Delete ProviderUsers\n    WHILE @BatchSize > 0\n        BEGIN\n        BEGIN TRANSACTION ProviderUser_DeleteMany_PUs\n\n        DELETE TOP(@BatchSize) PU\n        FROM\n            [dbo].[ProviderUser] PU\n        INNER JOIN\n            @Ids I ON I.Id = PU.Id\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION ProviderUser_DeleteMany_PUs\n    END\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderUserView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_ReadByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderUser_ReadByIds]\n    @Ids AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    IF (SELECT COUNT(1) FROM @Ids) < 1\n    BEGIN\n        RETURN(-1)\n    END\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderUserView]\n    WHERE\n        [Id] IN (SELECT [Id] FROM @Ids)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_ReadByOrganizationIdStatus.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_ReadByOrganizationIdStatus]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Status TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        PU.*\n    FROM\n        [dbo].[ProviderUserView] PU\n    INNER JOIN [dbo].[ProviderOrganizationView] as PO\n        ON PU.[ProviderId] = PO.[ProviderId]\n    WHERE\n        PO.[OrganizationId] = @OrganizationId\n        AND (@Status IS NULL OR PU.[Status] = @Status)\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_ReadByProviderId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_ReadByProviderId]\n    @ProviderId UNIQUEIDENTIFIER,\n    @Type TINYINT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderUserView]\n    WHERE\n        [ProviderId] = @ProviderId\n        AND [Type] = COALESCE(@Type, [Type])\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_ReadByProviderIdUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_ReadByProviderIdUserId]\n    @ProviderId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderUserView]\n    WHERE\n        [ProviderId] = @ProviderId\n        AND [UserId] = @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderUserView]\n    WHERE\n        [UserId] = @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_ReadCountByOnlyOwner.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_ReadCountByOnlyOwner]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    ;WITH [OwnerCountCTE] AS\n    (\n        SELECT\n            PU.[UserId],\n            COUNT(1) OVER (PARTITION BY PU.[ProviderId]) [ConfirmedOwnerCount]\n        FROM\n            [dbo].[ProviderUser] PU\n        WHERE\n            PU.[Type] = 0 -- 0 = ProviderAdmin\n            AND PU.[Status] = 2 -- 2 = Confirmed\n    )\n    SELECT\n        COUNT(1)\n    FROM\n        [OwnerCountCTE] OC\n    WHERE\n        OC.[UserId] = @UserId\n        AND OC.[ConfirmedOwnerCount] = 1\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_ReadCountByProviderIdEmail.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_ReadCountByProviderIdEmail]\n    @ProviderId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @OnlyUsers BIT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        COUNT(1)\n    FROM\n        [dbo].[ProviderUser] OU\n    LEFT JOIN\n        [dbo].[User] U ON OU.[UserId] = U.[Id]\n    WHERE\n        OU.[ProviderId] = @ProviderId\n        AND (\n            (@OnlyUsers = 0 AND @Email IN (OU.[Email], U.[Email]))\n            OR (@OnlyUsers = 1 AND U.[Email] = @Email)\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_ReadManyByManyUserIds.sql",
    "content": "CREATE PROCEDURE [dbo].[ProviderUser_ReadManyByManyUserIds]\n    @UserIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [pu].*\n    FROM\n        [dbo].[ProviderUserView] AS [pu]\n    INNER JOIN\n        @UserIds [u] ON [u].[Id] = [pu].[UserId]\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/ProviderUser_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[ProviderUser_Update]\n    @Id UNIQUEIDENTIFIER,\n    @ProviderId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @Email NVARCHAR(256),\n    @Key VARCHAR(MAX),\n    @Status TINYINT,\n    @Type TINYINT,\n    @Permissions NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[ProviderUser]\n    SET\n        [ProviderId] = @ProviderId,\n        [UserId] = @UserId,\n        [Email] = @Email,\n        [Key] = @Key,\n        [Status] = @Status,\n        [Type] = @Type,\n        [Permissions] = @Permissions,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Provider_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Provider_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @Name NVARCHAR(50),\n    @BusinessName NVARCHAR(50),\n    @BusinessAddress1 NVARCHAR(50),\n    @BusinessAddress2 NVARCHAR(50),\n    @BusinessAddress3 NVARCHAR(50),\n    @BusinessCountry VARCHAR(2),\n    @BusinessTaxNumber NVARCHAR(30),\n    @BillingEmail NVARCHAR(256),\n    @BillingPhone NVARCHAR(50) = NULL,\n    @Status TINYINT,\n    @Type TINYINT = 0,\n    @UseEvents BIT,\n    @Enabled BIT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Gateway TINYINT = 0,\n    @GatewayCustomerId VARCHAR(50) = NULL,\n    @GatewaySubscriptionId VARCHAR(50) = NULL,\n    @DiscountId VARCHAR(50) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Provider]\n    (\n        [Id],\n        [Name],\n        [BusinessName],\n        [BusinessAddress1],\n        [BusinessAddress2],\n        [BusinessAddress3],\n        [BusinessCountry],\n        [BusinessTaxNumber],\n        [BillingEmail],\n        [BillingPhone],\n        [Status],\n        [Type],\n        [UseEvents],\n        [Enabled],\n        [CreationDate],\n        [RevisionDate],\n        [Gateway],\n        [GatewayCustomerId],\n        [GatewaySubscriptionId],\n        [DiscountId]\n    )\n    VALUES\n    (\n        @Id,\n        @Name,\n        @BusinessName,\n        @BusinessAddress1,\n        @BusinessAddress2,\n        @BusinessAddress3,\n        @BusinessCountry,\n        @BusinessTaxNumber,\n        @BillingEmail,\n        @BillingPhone,\n        @Status,\n        @Type,\n        @UseEvents,\n        @Enabled,\n        @CreationDate,\n        @RevisionDate,\n        @Gateway,\n        @GatewayCustomerId,\n        @GatewaySubscriptionId,\n        @DiscountId\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Provider_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Provider_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByProviderId] @Id\n\n    BEGIN TRANSACTION Provider_DeleteById\n\n    DELETE\n    FROM \n        [dbo].[ProviderUser]\n    WHERE \n        [ProviderId] = @Id\n\n    DELETE\n    FROM\n        [dbo].[Provider]\n    WHERE\n        [Id] = @Id\n\n    COMMIT TRANSACTION Provider_DeleteById\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Provider_ReadAbilities.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Provider_ReadAbilities]\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [Id],\n        [UseEvents],\n        [Enabled]\n    FROM\n        [dbo].[Provider]\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Provider_ReadByGatewayCustomerId.sql",
    "content": "CREATE PROCEDURE [dbo].[Provider_ReadByGatewayCustomerId]\n    @GatewayCustomerId VARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderView]\n    WHERE\n        [GatewayCustomerId] = @GatewayCustomerId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Provider_ReadByGatewaySubscriptionId.sql",
    "content": "CREATE PROCEDURE [dbo].[Provider_ReadByGatewaySubscriptionId]\n    @GatewaySubscriptionId VARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderView]\n    WHERE\n        [GatewaySubscriptionId] = @GatewaySubscriptionId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Provider_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Provider_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[ProviderView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Provider_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Provider_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        P.*\n    FROM\n        [dbo].[ProviderView] P\n    INNER JOIN\n        [dbo].[ProviderOrganization] PO ON PO.[ProviderId] = P.[Id]\n    WHERE\n        PO.[OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Provider_Search.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Provider_Search]\n    @Name NVARCHAR(50),\n    @UserEmail NVARCHAR(256),\n    @Skip INT = 0,\n    @Take INT = 25\nWITH RECOMPILE\nAS\nBEGIN\n    SET NOCOUNT ON\n    DECLARE @NameLikeSearch NVARCHAR(55) = '%' + @Name + '%'\n\n    IF @UserEmail IS NOT NULL\n    BEGIN\n        SELECT\n            O.*\n        FROM\n            [dbo].[ProviderView] O\n        INNER JOIN\n            [dbo].[ProviderUser] OU ON O.[Id] = OU.[ProviderId]\n        INNER JOIN\n            [dbo].[User] U ON U.[Id] = OU.[UserId]\n        WHERE\n            (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)\n            AND U.[Email] = COALESCE(@UserEmail, U.[Email])\n        ORDER BY O.[CreationDate] DESC\n        OFFSET @Skip ROWS\n        FETCH NEXT @Take ROWS ONLY\n    END\n    ELSE\n    BEGIN\n        SELECT\n            O.*\n        FROM\n            [dbo].[ProviderView] O\n        WHERE\n            (@Name IS NULL OR O.[Name] LIKE @NameLikeSearch)\n        ORDER BY O.[CreationDate] DESC\n        OFFSET @Skip ROWS\n        FETCH NEXT @Take ROWS ONLY\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Provider_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Provider_Update]\n    @Id UNIQUEIDENTIFIER,\n    @Name NVARCHAR(50),\n    @BusinessName NVARCHAR(50),\n    @BusinessAddress1 NVARCHAR(50),\n    @BusinessAddress2 NVARCHAR(50),\n    @BusinessAddress3 NVARCHAR(50),\n    @BusinessCountry VARCHAR(2),\n    @BusinessTaxNumber NVARCHAR(30),\n    @BillingEmail NVARCHAR(256),\n    @BillingPhone NVARCHAR(50) = NULL,\n    @Status TINYINT,\n    @Type TINYINT = 0,\n    @UseEvents BIT,\n    @Enabled BIT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @Gateway TINYINT = 0,\n    @GatewayCustomerId VARCHAR(50) = NULL,\n    @GatewaySubscriptionId VARCHAR(50) = NULL,\n    @DiscountId VARCHAR(50) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Provider]\n    SET\n        [Name] = @Name,\n        [BusinessName] = @BusinessName,\n        [BusinessAddress1] = @BusinessAddress1,\n        [BusinessAddress2] = @BusinessAddress2,\n        [BusinessAddress3] = @BusinessAddress3,\n        [BusinessCountry] = @BusinessCountry,\n        [BusinessTaxNumber] = @BusinessTaxNumber,\n        [BillingEmail] = @BillingEmail,\n        [BillingPhone] = @BillingPhone,\n        [Status] = @Status,\n        [Type] = @Type,\n        [UseEvents] = @UseEvents,\n        [Enabled] = @Enabled,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [Gateway] = @Gateway,\n        [GatewayCustomerId] = @GatewayCustomerId,\n        [GatewaySubscriptionId] = @GatewaySubscriptionId,\n        [DiscountId] = @DiscountId\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/SubscriptionDiscount_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[SubscriptionDiscount_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @StripeCouponId VARCHAR(50),\n    @StripeProductIds NVARCHAR(MAX),\n    @PercentOff DECIMAL(5,2),\n    @AmountOff BIGINT,\n    @Currency VARCHAR(10),\n    @Duration VARCHAR(20),\n    @DurationInMonths INT,\n    @Name NVARCHAR(100),\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @AudienceType INT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[SubscriptionDiscount]\n    (\n        [Id],\n        [StripeCouponId],\n        [StripeProductIds],\n        [PercentOff],\n        [AmountOff],\n        [Currency],\n        [Duration],\n        [DurationInMonths],\n        [Name],\n        [StartDate],\n        [EndDate],\n        [AudienceType],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @StripeCouponId,\n        @StripeProductIds,\n        @PercentOff,\n        @AmountOff,\n        @Currency,\n        @Duration,\n        @DurationInMonths,\n        @Name,\n        @StartDate,\n        @EndDate,\n        @AudienceType,\n        @CreationDate,\n        @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/SubscriptionDiscount_DeleteById.sql",
    "content": "CREATE PROCEDURE [dbo].[SubscriptionDiscount_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[SubscriptionDiscount]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/SubscriptionDiscount_List.sql",
    "content": "CREATE PROCEDURE [dbo].[SubscriptionDiscount_List]\n    @Skip INT = 0,\n    @Take INT = 25\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SubscriptionDiscountView]\n    ORDER BY [CreationDate] DESC\n    OFFSET @Skip ROWS\n    FETCH NEXT @Take ROWS ONLY\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/SubscriptionDiscount_ReadActive.sql",
    "content": "CREATE PROCEDURE [dbo].[SubscriptionDiscount_ReadActive]\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SubscriptionDiscountView]\n    WHERE\n        [StartDate] <= GETUTCDATE()\n        AND [EndDate] >= GETUTCDATE()\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/SubscriptionDiscount_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[SubscriptionDiscount_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SubscriptionDiscountView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/SubscriptionDiscount_ReadByStripeCouponId.sql",
    "content": "CREATE PROCEDURE [dbo].[SubscriptionDiscount_ReadByStripeCouponId]\n    @StripeCouponId VARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SubscriptionDiscountView]\n    WHERE\n        [StripeCouponId] = @StripeCouponId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/SubscriptionDiscount_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[SubscriptionDiscount_Update]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @StripeCouponId VARCHAR(50),\n    @StripeProductIds NVARCHAR(MAX),\n    @PercentOff DECIMAL(5,2),\n    @AmountOff BIGINT,\n    @Currency VARCHAR(10),\n    @Duration VARCHAR(20),\n    @DurationInMonths INT,\n    @Name NVARCHAR(100),\n    @StartDate DATETIME2(7),\n    @EndDate DATETIME2(7),\n    @AudienceType INT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[SubscriptionDiscount]\n    SET\n        [StripeCouponId] = @StripeCouponId,\n        [StripeProductIds] = @StripeProductIds,\n        [PercentOff] = @PercentOff,\n        [AmountOff] = @AmountOff,\n        [Currency] = @Currency,\n        [Duration] = @Duration,\n        [DurationInMonths] = @DurationInMonths,\n        [Name] = @Name,\n        [StartDate] = @StartDate,\n        [EndDate] = @EndDate,\n        [AudienceType] = @AudienceType,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/TaxRate_Archive.sql",
    "content": "CREATE PROCEDURE [dbo].[TaxRate_Archive]\n    @Id VARCHAR(40)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[TaxRate]\n    SET\n        [Active] = 0\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/TaxRate_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[TaxRate_Create]\n    @Id VARCHAR(40) OUTPUT,\n    @Country VARCHAR(50),\n    @State VARCHAR(2),\n    @PostalCode VARCHAR(10),\n    @Rate DECIMAL(6,3),\n    @Active BIT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[TaxRate]\n    (\n        [Id],\n        [Country],\n        [State],\n        [PostalCode],\n        [Rate],\n        [Active]\n    )\n    VALUES\n    (\n        @Id,\n        @Country,\n        @State,\n        @PostalCode,\n        @Rate,\n        1\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/TaxRate_ReadAllActive.sql",
    "content": "CREATE PROCEDURE [dbo].[TaxRate_ReadAllActive]\nAS\nBEGIN\n    SET NOCOUNT ON \n    \n    SELECT * FROM [dbo].[TaxRate]\n    WHERE Active = 1\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/TaxRate_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[TaxRate_ReadById]\n    @Id VARCHAR(40)\nAS\nBEGIN\n    SET NOCOUNT ON \n    \n    SELECT * FROM [dbo].[TaxRate]\n    WHERE Id = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/TaxRate_ReadByLocation.sql",
    "content": "CREATE PROCEDURE [dbo].[TaxRate_ReadByLocation]\n    @Country VARCHAR(50),\n    @PostalCode VARCHAR(10)\nAS\nBEGIN\n    SET NOCOUNT ON \n    \n    SELECT * FROM [dbo].[TaxRate]\n    WHERE Active = 1\n        AND [Country] = @Country\n        AND [PostalCode] = @PostalCode\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/TaxRate_Search.sql",
    "content": "CREATE PROCEDURE [dbo].[TaxRate_Search]\n    @Skip INT = 0,\n    @Count INT = 25\nAS\nBEGIN\n    SET NOCOUNT ON \n    \n    SELECT * FROM [dbo].[TaxRate]\n    WHERE Active = 1\n    ORDER BY Country, PostalCode DESC\n    OFFSET @Skip ROWS\n    FETCH NEXT @Count ROWS ONLY\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Transaction_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Transaction_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Amount MONEY,\n    @Refunded BIT,\n    @RefundedAmount MONEY,\n    @Details NVARCHAR(100),\n    @PaymentMethodType TINYINT,\n    @Gateway TINYINT,\n    @GatewayId VARCHAR(50),\n    @CreationDate DATETIME2(7),\n    @ProviderId UNIQUEIDENTIFIER = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Transaction]\n    (\n        [Id],\n        [UserId],\n        [OrganizationId],\n        [Type],\n        [Amount],\n        [Refunded],\n        [RefundedAmount],\n        [Details],\n        [PaymentMethodType],\n        [Gateway],\n        [GatewayId],\n        [CreationDate],\n        [ProviderId]\n    )\n    VALUES\n    (\n        @Id,\n        @UserId,\n        @OrganizationId,\n        @Type,\n        @Amount,\n        @Refunded,\n        @RefundedAmount,\n        @Details,\n        @PaymentMethodType,\n        @Gateway,\n        @GatewayId,\n        @CreationDate,\n        @ProviderId\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Transaction_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Transaction_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[Transaction]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Transaction_ReadByGatewayId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Transaction_ReadByGatewayId]\n    @Gateway TINYINT,\n    @GatewayId VARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[TransactionView]\n    WHERE\n        [Gateway] = @Gateway\n        AND [GatewayId] = @GatewayId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Transaction_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Transaction_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[TransactionView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Transaction_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Transaction_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Limit INT,\n    @StartAfter DATETIME2 = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT TOP (@Limit) *\n    FROM [dbo].[TransactionView]\n    WHERE\n        [OrganizationId] = @OrganizationId\n      AND (@StartAfter IS NULL OR [CreationDate] < @StartAfter)\n    ORDER BY\n        [CreationDate] DESC\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Transaction_ReadByProviderId.sql",
    "content": "CREATE PROCEDURE [dbo].[Transaction_ReadByProviderId]\n    @ProviderId UNIQUEIDENTIFIER,\n    @Limit INT,\n    @StartAfter DATETIME2 = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        TOP (@Limit) *\n    FROM\n        [dbo].[TransactionView]\n    WHERE\n        [ProviderId] = @ProviderId\n        AND (@StartAfter IS NULL OR [CreationDate] < @StartAfter)\n    ORDER BY\n        [CreationDate] DESC\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Transaction_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Transaction_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER,\n    @Limit INT,\n    @StartAfter DATETIME2 = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        TOP (@Limit) *\n    FROM\n        [dbo].[TransactionView]\n    WHERE\n        [UserId] = @UserId\n        AND (@StartAfter IS NULL OR [CreationDate] < @StartAfter)\n    ORDER BY\n        [CreationDate] DESC\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/Transaction_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Transaction_Update]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Amount MONEY,\n    @Refunded BIT,\n    @RefundedAmount MONEY,\n    @Details NVARCHAR(100),\n    @PaymentMethodType TINYINT,\n    @Gateway TINYINT,\n    @GatewayId VARCHAR(50),\n    @CreationDate DATETIME2(7),\n    @ProviderId UNIQUEIDENTIFIER = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Transaction]\n    SET\n        [UserId] = @UserId,\n        [OrganizationId] = @OrganizationId,\n        [Type] = @Type,\n        [Amount] = @Amount,\n        [Refunded] = @Refunded,\n        [RefundedAmount] = @RefundedAmount,\n        [Details] = @Details,\n        [PaymentMethodType] = @PaymentMethodType,\n        [Gateway] = @Gateway,\n        [GatewayId] = @GatewayId,\n        [CreationDate] = @CreationDate,\n        [ProviderId] = @ProviderId\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDate.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDate]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [User]\n    SET\n        [AccountRevisionDate] = GETUTCDATE()\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByCipherId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByCipherId]\n    @CipherId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]\n    LEFT JOIN\n        [dbo].[CollectionCipher] CC ON CC.[CipherId] = @CipherId\n    LEFT JOIN\n        [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] = CC.[CollectionId]\n    LEFT JOIN\n        [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n    LEFT JOIN\n        [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n    LEFT JOIN\n        [dbo].[CollectionGroup] CG ON CG.[GroupId] = GU.[GroupId] AND CG.[CollectionId] = CC.[CollectionId]\n    WHERE\n        OU.[OrganizationId] = @OrganizationId\n        AND OU.[Status] = 2 -- 2 = Confirmed\n        AND (\n            CU.[CollectionId] IS NOT NULL\n            OR CG.[CollectionId] IS NOT NULL\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByCollectionId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByCollectionId]\n    @CollectionId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]\n    LEFT JOIN\n        [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] = @CollectionId\n    LEFT JOIN\n        [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n    LEFT JOIN\n        [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n    LEFT JOIN\n        [dbo].[CollectionGroup] CG ON CG.[GroupId] = GU.[GroupId] AND CG.[CollectionId] = @CollectionId\n    WHERE\n        OU.[OrganizationId] = @OrganizationId\n        AND OU.[Status] = 2 -- 2 = Confirmed\n        AND (\n            CU.[CollectionId] IS NOT NULL\n            OR CG.[CollectionId] IS NOT NULL\n        )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByCollectionIds.sql",
    "content": "CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByCollectionIds]\n\t@CollectionIds AS [dbo].[GuidIdArray] READONLY,\n\t@OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n\tSET NOCOUNT ON\n\nUPDATE\n    U\nSET\n    U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n    \t[dbo].[Collection] C ON C.[Id] IN (SELECT [Id] FROM @CollectionIds)\n    INNER JOIN\n    [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]\n    LEFT JOIN\n    [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] = C.[Id]\n    LEFT JOIN\n    [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n    LEFT JOIN\n    [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n    LEFT JOIN\n    [dbo].[CollectionGroup] CG ON CG.[GroupId] = GU.[GroupId] AND CG.[CollectionId] = C.[Id]\nWHERE\n    OU.[OrganizationId] = @OrganizationId\n  AND OU.[Status] = 2 -- 2 = Confirmed\n  AND (\n    CU.[CollectionId] IS NOT NULL\n   OR CG.[CollectionId] IS NOT NULL\n  )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]\n    WHERE\n        OU.[OrganizationId] = @OrganizationId\n        AND OU.[Status] = 2 -- Confirmed\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationIds.sql",
    "content": "CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByOrganizationIds]\n    @OrganizationIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]\n    WHERE\n        OU.[OrganizationId] IN (SELECT [Id] FROM @OrganizationIds)\n        AND OU.[Status] = 2 -- Confirmed\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByOrganizationUserId]\n    @OrganizationUserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]\n    WHERE\n        OU.[Id] = @OrganizationUserId\n        AND OU.[Status] = 2 -- Confirmed\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserIds.sql",
    "content": "CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds]\n    @OrganizationUserIds [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        OU.UserId\n    INTO\n        #UserIds\n    FROM\n        [dbo].[OrganizationUser] OU\n    INNER JOIN\n        @OrganizationUserIds OUIds ON OUIds.Id = OU.Id\n    WHERE\n        OU.[Status] = 2 -- Confirmed\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        #UserIds ON U.[Id] = #UserIds.[UserId]\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByProviderId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByProviderId]\n    @ProviderId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        [dbo].[ProviderUser] PU ON PU.[UserId] = U.[Id]\n    WHERE\n        PU.[ProviderId] = @ProviderId\n        AND PU.[Status] = 2 -- Confirmed\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByProviderUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByProviderUserId]\n    @ProviderUserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        [dbo].[ProviderUser] PU ON PU.[UserId] = U.[Id]\n    WHERE\n        PU.[Id] = @ProviderUserId\n        AND PU.[Status] = 2 -- Confirmed\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByProviderUserIds.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByProviderUserIds]\n    @ProviderUserIds [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        U.[AccountRevisionDate] = GETUTCDATE()\n    FROM\n        @ProviderUserIds OUIDs\n    INNER JOIN\n        [dbo].[ProviderUser] PU ON OUIDs.Id = PU.Id AND PU.[Status] = 2 -- Confirmed\n    INNER JOIN\n        [dbo].[User] U ON PU.UserId = U.Id\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_BumpManyAccountRevisionDates.sql",
    "content": "CREATE PROCEDURE [dbo].[User_BumpManyAccountRevisionDates]\n    @Ids [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        U\n    SET\n        [AccountRevisionDate] = GETUTCDATE()\n    FROM\n        [dbo].[User] U\n    INNER JOIN\n        @Ids IDs ON IDs.Id = U.Id\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @Name NVARCHAR(50),\n    @Email NVARCHAR(256),\n    @EmailVerified BIT,\n    @MasterPassword NVARCHAR(300),\n    @MasterPasswordHint NVARCHAR(50),\n    @Culture NVARCHAR(10),\n    @SecurityStamp NVARCHAR(50),\n    @TwoFactorProviders NVARCHAR(MAX),\n    @TwoFactorRecoveryCode NVARCHAR(32),\n    @EquivalentDomains NVARCHAR(MAX),\n    @ExcludedGlobalEquivalentDomains NVARCHAR(MAX),\n    @AccountRevisionDate DATETIME2(7),\n    @Key NVARCHAR(MAX),\n    @PublicKey NVARCHAR(MAX),\n    @PrivateKey NVARCHAR(MAX),\n    @Premium BIT,\n    @PremiumExpirationDate DATETIME2(7),\n    @RenewalReminderDate DATETIME2(7),\n    @Storage BIGINT,\n    @MaxStorageGb SMALLINT,\n    @Gateway TINYINT,\n    @GatewayCustomerId VARCHAR(50),\n    @GatewaySubscriptionId VARCHAR(50),\n    @ReferenceData VARCHAR(MAX),\n    @LicenseKey VARCHAR(100),\n    @Kdf TINYINT,\n    @KdfIterations INT,\n    @KdfMemory INT = NULL,\n    @KdfParallelism INT = NULL,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @ApiKey VARCHAR(30),\n    @ForcePasswordReset BIT = 0,\n    @UsesKeyConnector BIT = 0,\n    @FailedLoginCount INT = 0,\n    @LastFailedLoginDate DATETIME2(7),\n    @AvatarColor VARCHAR(7) = NULL,\n    @LastPasswordChangeDate DATETIME2(7) = NULL,\n    @LastKdfChangeDate DATETIME2(7) = NULL,\n    @LastKeyRotationDate DATETIME2(7) = NULL,\n    @LastEmailChangeDate DATETIME2(7) = NULL,\n    @VerifyDevices BIT = 1,\n    @SecurityState VARCHAR(MAX) = NULL,\n    @SecurityVersion INT = NULL,\n    @SignedPublicKey VARCHAR(MAX) = NULL,\n    @V2UpgradeToken VARCHAR(MAX) = NULL,\n    @MasterPasswordSalt NVARCHAR(256) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[User]\n    (\n        [Id],\n        [Name],\n        [Email],\n        [EmailVerified],\n        [MasterPassword],\n        [MasterPasswordHint],\n        [Culture],\n        [SecurityStamp],\n        [TwoFactorProviders],\n        [TwoFactorRecoveryCode],\n        [EquivalentDomains],\n        [ExcludedGlobalEquivalentDomains],\n        [AccountRevisionDate],\n        [Key],\n        [PublicKey],\n        [PrivateKey],\n        [Premium],\n        [PremiumExpirationDate],\n        [RenewalReminderDate],\n        [Storage],\n        [MaxStorageGb],\n        [Gateway],\n        [GatewayCustomerId],\n        [GatewaySubscriptionId],\n        [ReferenceData],\n        [LicenseKey],\n        [Kdf],\n        [KdfIterations],\n        [CreationDate],\n        [RevisionDate],\n        [ApiKey],\n        [ForcePasswordReset],\n        [UsesKeyConnector],\n        [FailedLoginCount],\n        [LastFailedLoginDate],\n        [AvatarColor],\n        [KdfMemory],\n        [KdfParallelism],\n        [LastPasswordChangeDate],\n        [LastKdfChangeDate],\n        [LastKeyRotationDate],\n        [LastEmailChangeDate],\n        [VerifyDevices],\n        [SecurityState],\n        [SecurityVersion],\n        [SignedPublicKey],\n        [MaxStorageGbIncreased],\n        [V2UpgradeToken],\n        [MasterPasswordSalt]\n    )\n    VALUES\n    (\n        @Id,\n        @Name,\n        @Email,\n        @EmailVerified,\n        @MasterPassword,\n        @MasterPasswordHint,\n        @Culture,\n        @SecurityStamp,\n        @TwoFactorProviders,\n        @TwoFactorRecoveryCode,\n        @EquivalentDomains,\n        @ExcludedGlobalEquivalentDomains,\n        @AccountRevisionDate,\n        @Key,\n        @PublicKey,\n        @PrivateKey,\n        @Premium,\n        @PremiumExpirationDate,\n        @RenewalReminderDate,\n        @Storage,\n        @MaxStorageGb,\n        @Gateway,\n        @GatewayCustomerId,\n        @GatewaySubscriptionId,\n        @ReferenceData,\n        @LicenseKey,\n        @Kdf,\n        @KdfIterations,\n        @CreationDate,\n        @RevisionDate,\n        @ApiKey,\n        @ForcePasswordReset,\n        @UsesKeyConnector,\n        @FailedLoginCount,\n        @LastFailedLoginDate,\n        @AvatarColor,\n        @KdfMemory,\n        @KdfParallelism,\n        @LastPasswordChangeDate,\n        @LastKdfChangeDate,\n        @LastKeyRotationDate,\n        @LastEmailChangeDate,\n        @VerifyDevices,\n        @SecurityState,\n        @SecurityVersion,\n        @SignedPublicKey,\n        @MaxStorageGb,\n        @V2UpgradeToken,\n        @MasterPasswordSalt\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_DeleteById]\n    @Id UNIQUEIDENTIFIER\nWITH RECOMPILE\nAS\nBEGIN\n    SET NOCOUNT ON\n    DECLARE @BatchSize INT = 100\n\n    -- Delete ciphers\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION User_DeleteById_Ciphers\n\n        DELETE TOP(@BatchSize)\n        FROM\n            [dbo].[Cipher]\n        WHERE\n            [UserId] = @Id\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION User_DeleteById_Ciphers\n    END\n\n    BEGIN TRANSACTION User_DeleteById\n\n    -- Delete WebAuthnCredentials\n    DELETE\n    FROM\n        [dbo].[WebAuthnCredential]\n    WHERE\n        [UserId] = @Id\n\n    -- Delete folders\n    DELETE\n    FROM\n        [dbo].[Folder]\n    WHERE\n        [UserId] = @Id\n\n    -- Delete AuthRequest, must be before Device\n    DELETE\n    FROM\n        [dbo].[AuthRequest]\n    WHERE \n        [UserId] = @Id\n\n    -- Delete devices\n    DELETE\n    FROM\n        [dbo].[Device]\n    WHERE\n        [UserId] = @Id\n\n    -- Migrate DefaultUserCollection to SharedCollection before deleting CollectionUser records\n    DECLARE @OrgUserIds [dbo].[GuidIdArray]\n    INSERT INTO @OrgUserIds (Id)\n    SELECT [Id] FROM [dbo].[OrganizationUser] WHERE [UserId] = @Id\n    \n    IF EXISTS (SELECT 1 FROM @OrgUserIds)\n    BEGIN\n        EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @OrgUserIds\n    END\n\n    -- Delete collection users\n    DELETE\n        CU\n    FROM\n        [dbo].[CollectionUser] CU\n        INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId]\n    WHERE\n        OU.[UserId] = @Id\n\n    -- Delete group users\n    DELETE\n        GU\n    FROM\n        [dbo].[GroupUser] GU\n        INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId]\n    WHERE\n        OU.[UserId] = @Id\n\n    -- Delete AccessPolicy\n    DELETE\n        AP\n    FROM\n        [dbo].[AccessPolicy] AP\n        INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[Id] = AP.[OrganizationUserId]\n    WHERE\n        [UserId] = @Id\n\n    -- Delete organization users\n    DELETE\n    FROM\n        [dbo].[OrganizationUser]\n    WHERE\n        [UserId] = @Id\n\n    -- Delete provider users\n    DELETE\n    FROM\n        [dbo].[ProviderUser]\n    WHERE\n        [UserId] = @Id\n\n    -- Delete SSO Users\n    DELETE\n    FROM\n        [dbo].[SsoUser]\n    WHERE\n        [UserId] = @Id\n\n    -- Delete Emergency Accesses\n    DELETE\n    FROM\n        [dbo].[EmergencyAccess]\n    WHERE\n        [GrantorId] = @Id\n        OR\n        [GranteeId] = @Id\n\n    -- Delete Sends\n    DELETE\n    FROM\n        [dbo].[Send]\n    WHERE \n        [UserId] = @Id\n\n    -- Delete Notification Status\n    DELETE\n    FROM\n        [dbo].[NotificationStatus]\n    WHERE\n        [UserId] = @Id\n\n    -- Delete Notification\n    DELETE\n    FROM\n        [dbo].[Notification]\n    WHERE\n        [UserId] = @Id\n    \n    -- Finally, delete the user\n    DELETE\n    FROM\n        [dbo].[User]\n    WHERE\n        [Id] = @Id\n\n    COMMIT TRANSACTION User_DeleteById\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_DeleteByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[User_DeleteByIds]\n    @Ids NVARCHAR(MAX)\nWITH RECOMPILE\nAS\nBEGIN\n    SET NOCOUNT ON\n    -- Declare a table variable to hold the parsed JSON data\n    DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER);\n\n    -- Parse the JSON input into the table variable\n    INSERT INTO @ParsedIds (Id)\n    SELECT value\n    FROM OPENJSON(@Ids);\n\n    -- Check if the input table is empty\n    IF (SELECT COUNT(1) FROM @ParsedIds) < 1\n    BEGIN\n        RETURN(-1);\n    END\n\n    DECLARE @BatchSize INT = 100\n\n    -- Delete ciphers\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION User_DeleteById_Ciphers\n\n        DELETE TOP(@BatchSize)\n        FROM\n            [dbo].[Cipher]\n        WHERE\n            [UserId] IN (SELECT * FROM @ParsedIds)\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION User_DeleteById_Ciphers\n    END\n\n    BEGIN TRANSACTION User_DeleteById\n\n    -- Delete WebAuthnCredentials\n    DELETE\n    FROM\n        [dbo].[WebAuthnCredential]\n    WHERE\n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete folders\n    DELETE\n    FROM\n        [dbo].[Folder]\n    WHERE\n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete AuthRequest, must be before Device\n    DELETE\n    FROM\n        [dbo].[AuthRequest]\n    WHERE \n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete devices\n    DELETE\n    FROM\n        [dbo].[Device]\n    WHERE\n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Migrate DefaultUserCollection to SharedCollection before deleting CollectionUser records\n    DECLARE @OrgUserIds [dbo].[GuidIdArray]\n    INSERT INTO @OrgUserIds (Id)\n    SELECT [Id] FROM [dbo].[OrganizationUser] WHERE [UserId] IN (SELECT * FROM @ParsedIds)\n    \n    IF EXISTS (SELECT 1 FROM @OrgUserIds)\n    BEGIN\n        EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @OrgUserIds\n    END\n\n    -- Delete collection users\n    DELETE\n        CU\n    FROM\n        [dbo].[CollectionUser] CU\n        INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId]\n    WHERE\n        OU.[UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete group users\n    DELETE\n        GU\n    FROM\n        [dbo].[GroupUser] GU\n        INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId]\n    WHERE\n        OU.[UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete AccessPolicy\n    DELETE\n        AP\n    FROM\n        [dbo].[AccessPolicy] AP\n        INNER JOIN\n        [dbo].[OrganizationUser] OU ON OU.[Id] = AP.[OrganizationUserId]\n    WHERE\n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete organization users\n    DELETE\n    FROM\n        [dbo].[OrganizationUser]\n    WHERE\n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete provider users\n    DELETE\n    FROM\n        [dbo].[ProviderUser]\n    WHERE\n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete SSO Users\n    DELETE\n    FROM\n        [dbo].[SsoUser]\n    WHERE\n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete Emergency Accesses\n    DELETE\n    FROM\n        [dbo].[EmergencyAccess]\n    WHERE\n        [GrantorId] IN (SELECT * FROM @ParsedIds)\n        OR\n        [GranteeId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete Sends\n    DELETE\n    FROM\n        [dbo].[Send]\n    WHERE \n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete Notification Status\n    DELETE\n    FROM\n        [dbo].[NotificationStatus]\n    WHERE\n        [UserId] IN (SELECT * FROM @ParsedIds)\n\n    -- Delete Notification\n    DELETE\n    FROM\n        [dbo].[Notification]\n    WHERE\n        [UserId] IN (SELECT * FROM @ParsedIds)\n    \n    -- Finally, delete the user\n    DELETE\n    FROM\n        [dbo].[User]\n    WHERE\n        [Id] IN (SELECT * FROM @ParsedIds)\n\n    COMMIT TRANSACTION User_DeleteById\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadAccountRevisionDateById.sql",
    "content": "﻿\nCREATE PROCEDURE [dbo].[User_ReadAccountRevisionDateById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [AccountRevisionDate]\n    FROM\n        [dbo].[User]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadByEmail.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadByEmail]\n    @Email NVARCHAR(256)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserView]\n    WHERE\n        [Email] = @Email\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadByEmails.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadByEmails]\n    @Emails AS [dbo].[EmailArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    IF (SELECT COUNT(1) FROM @Emails) < 1\n    BEGIN\n        RETURN(-1)\n    END\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserView]\n    WHERE\n        [Email] IN (SELECT [Email] FROM @Emails)\nEND\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadByGatewayCustomerId.sql",
    "content": "CREATE PROCEDURE [dbo].[User_ReadByGatewayCustomerId]\n    @GatewayCustomerId VARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserView]\n    WHERE\n        [GatewayCustomerId] = @GatewayCustomerId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadByGatewaySubscriptionId.sql",
    "content": "CREATE PROCEDURE [dbo].[User_ReadByGatewaySubscriptionId]\n    @GatewaySubscriptionId VARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserView]\n    WHERE\n        [GatewaySubscriptionId] = @GatewaySubscriptionId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadByIds.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadByIds]\n@Ids AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    IF (SELECT COUNT(1) FROM @Ids) < 1\n        BEGIN\n            RETURN(-1)\n        END\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserView]\n    WHERE\n        [Id] IN (SELECT [Id] FROM @Ids)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadByIdsWithCalculatedPremium.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadByIdsWithCalculatedPremium]\n    @Ids NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    -- Declare a table variable to hold the parsed JSON data\n    DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER);\n\n    -- Parse the JSON input into the table variable\n    INSERT INTO @ParsedIds (Id)\n    SELECT value\n    FROM OPENJSON(@Ids);\n\n    -- Check if the input table is empty\n    IF (SELECT COUNT(1) FROM @ParsedIds) < 1\n    BEGIN\n        RETURN(-1);\n    END\n\n    -- Main query to fetch user details and calculate premium access\n    SELECT\n        U.*,\n        CASE\n            WHEN U.[Premium] = 1\n                OR EXISTS (\n                    SELECT 1\n                    FROM [dbo].[OrganizationUser] OU\n                    JOIN [dbo].[Organization] O ON OU.[OrganizationId] = O.[Id]\n                    WHERE OU.[UserId] = U.[Id]\n                        AND O.[UsersGetPremium] = 1\n                        AND O.[Enabled] = 1\n                )\n                THEN 1\n            ELSE 0\n            END AS HasPremiumAccess\n    FROM\n        [dbo].[UserView] U\n    WHERE\n        U.[Id] IN (SELECT [Id] FROM @ParsedIds);\nEND;\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadByPremium.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadByPremium]\n    @Premium BIT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserView]\n    WHERE\n        [Premium] = @Premium\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadBySsoUserOrganizationIdExternalId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadBySsoUserOrganizationIdExternalId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @ExternalId NVARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        U.*\n    FROM\n        [dbo].[UserView] U\n    INNER JOIN\n        [dbo].[SsoUser] SU ON SU.[UserId] = U.[Id]\n    WHERE\n        (\n            (@OrganizationId IS NULL AND SU.[OrganizationId] IS NULL)\n            OR (@OrganizationId IS NOT NULL AND SU.[OrganizationId] = @OrganizationId)\n        )\n        AND SU.[ExternalId] = @ExternalId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadKdfByEmail.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadKdfByEmail]\n    @Email NVARCHAR(256)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [Kdf],\n        [KdfIterations],\n        [KdfMemory],\n        [KdfParallelism],\n        [MasterPasswordSalt]\n    FROM\n        [dbo].[User]\n    WHERE\n        [Email] = @Email\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadPremiumAccessByIds.sql",
    "content": "CREATE PROCEDURE [dbo].[User_ReadPremiumAccessByIds]\n    @Ids [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        UPA.[Id],\n        UPA.[PersonalPremium],\n        UPA.[OrganizationPremium]\n    FROM\n        [dbo].[UserPremiumAccessView] UPA\n    WHERE\n        UPA.[Id] IN (SELECT [Id] FROM @Ids)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadPublicKeyById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadPublicKeyById]\n    @Id NVARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        [PublicKey]\n    FROM\n        [dbo].[User]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadPublicKeysByOrganizationUserIds.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadPublicKeysByOrganizationUserIds]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @OrganizationUserIds [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        OU.[Id],\n        OU.[UserId],\n        U.[PublicKey]\n    FROM\n        @OrganizationUserIds OUIDs\n    INNER JOIN\n        [dbo].[OrganizationUser] OU ON OUIDs.Id = OU.Id AND OU.[Status] = 1 -- Accepted\n    INNER JOIN\n        [dbo].[User] U ON OU.UserId = U.Id\n    WHERE\n        OU.OrganizationId = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_ReadPublicKeysByProviderUserIds.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_ReadPublicKeysByProviderUserIds]\n    @ProviderId UNIQUEIDENTIFIER,\n    @ProviderUserIds [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        PU.[Id],\n        PU.[UserId],\n        U.[PublicKey]\n    FROM\n        @ProviderUserIds PUIDs\n    INNER JOIN\n        [dbo].[ProviderUser] PU ON PUIDs.Id = PU.Id AND PU.[Status] = 1 -- Accepted\n    INNER JOIN\n        [dbo].[User] U ON PU.UserId = U.Id\n    WHERE\n        PU.ProviderId = @ProviderId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_Search.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_Search]\n    @Email NVARCHAR(256),\n    @Skip INT = 0,\n    @Take INT = 25\nWITH RECOMPILE\nAS\nBEGIN\n    SET NOCOUNT ON\n    DECLARE @EmailLikeSearch NVARCHAR(261) = @Email + '%'\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserView]\n    WHERE\n        (@Email IS NULL OR [Email] LIKE @EmailLikeSearch)\n    ORDER BY [Email] ASC\n    OFFSET @Skip ROWS\n    FETCH NEXT @Take ROWS ONLY\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_Update]\n    @Id UNIQUEIDENTIFIER,\n    @Name NVARCHAR(50),\n    @Email NVARCHAR(256),\n    @EmailVerified BIT,\n    @MasterPassword NVARCHAR(300),\n    @MasterPasswordHint NVARCHAR(50),\n    @Culture NVARCHAR(10),\n    @SecurityStamp NVARCHAR(50),\n    @TwoFactorProviders NVARCHAR(MAX),\n    @TwoFactorRecoveryCode NVARCHAR(32),\n    @EquivalentDomains NVARCHAR(MAX),\n    @ExcludedGlobalEquivalentDomains NVARCHAR(MAX),\n    @AccountRevisionDate DATETIME2(7),\n    @Key NVARCHAR(MAX),\n    @PublicKey NVARCHAR(MAX),\n    @PrivateKey NVARCHAR(MAX),\n    @Premium BIT,\n    @PremiumExpirationDate DATETIME2(7),\n    @RenewalReminderDate DATETIME2(7),\n    @Storage BIGINT,\n    @MaxStorageGb SMALLINT,\n    @Gateway TINYINT,\n    @GatewayCustomerId VARCHAR(50),\n    @GatewaySubscriptionId VARCHAR(50),\n    @ReferenceData VARCHAR(MAX),\n    @LicenseKey VARCHAR(100),\n    @Kdf TINYINT,\n    @KdfIterations INT,\n    @KdfMemory INT = NULL,\n    @KdfParallelism INT = NULL,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @ApiKey VARCHAR(30),\n    @ForcePasswordReset BIT = 0,\n    @UsesKeyConnector BIT = 0,\n    @FailedLoginCount INT,\n    @LastFailedLoginDate DATETIME2(7),\n    @AvatarColor VARCHAR(7),\n    @LastPasswordChangeDate DATETIME2(7) = NULL,\n    @LastKdfChangeDate DATETIME2(7) = NULL,\n    @LastKeyRotationDate DATETIME2(7) = NULL,\n    @LastEmailChangeDate DATETIME2(7) = NULL,\n    @VerifyDevices BIT = 1,\n    @SecurityState VARCHAR(MAX) = NULL,\n    @SecurityVersion INT = NULL,\n    @SignedPublicKey VARCHAR(MAX) = NULL,\n    @V2UpgradeToken VARCHAR(MAX) = NULL,\n    @MasterPasswordSalt NVARCHAR(256) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[User]\n    SET\n        [Name] = @Name,\n        [Email] = @Email,\n        [EmailVerified] = @EmailVerified,\n        [MasterPassword] = @MasterPassword,\n        [MasterPasswordHint] = @MasterPasswordHint,\n        [Culture] = @Culture,\n        [SecurityStamp] = @SecurityStamp,\n        [TwoFactorProviders] = @TwoFactorProviders,\n        [TwoFactorRecoveryCode] = @TwoFactorRecoveryCode,\n        [EquivalentDomains] = @EquivalentDomains,\n        [ExcludedGlobalEquivalentDomains] = @ExcludedGlobalEquivalentDomains,\n        [AccountRevisionDate] = @AccountRevisionDate,\n        [Key] = @Key,\n        [PublicKey] = @PublicKey,\n        [PrivateKey] = @PrivateKey,\n        [Premium] = @Premium,\n        [PremiumExpirationDate] = @PremiumExpirationDate,\n        [RenewalReminderDate] = @RenewalReminderDate,\n        [Storage] = @Storage,\n        [MaxStorageGb] = @MaxStorageGb,\n        [Gateway] = @Gateway,\n        [GatewayCustomerId] = @GatewayCustomerId,\n        [GatewaySubscriptionId] = @GatewaySubscriptionId,\n        [ReferenceData] = @ReferenceData,\n        [LicenseKey] = @LicenseKey,\n        [Kdf] = @Kdf,\n        [KdfIterations] = @KdfIterations,\n        [KdfMemory] = @KdfMemory,\n        [KdfParallelism] = @KdfParallelism,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [ApiKey] = @ApiKey,\n        [ForcePasswordReset] = @ForcePasswordReset,\n        [UsesKeyConnector] = @UsesKeyConnector,\n        [FailedLoginCount] = @FailedLoginCount,\n        [LastFailedLoginDate] = @LastFailedLoginDate,\n        [AvatarColor] = @AvatarColor,\n        [LastPasswordChangeDate] = @LastPasswordChangeDate,\n        [LastKdfChangeDate] = @LastKdfChangeDate,\n        [LastKeyRotationDate] = @LastKeyRotationDate,\n        [LastEmailChangeDate] = @LastEmailChangeDate,\n        [VerifyDevices] = @VerifyDevices,\n        [SecurityState] = @SecurityState,\n        [SecurityVersion] = @SecurityVersion,\n        [SignedPublicKey] = @SignedPublicKey,\n        [MaxStorageGbIncreased] = @MaxStorageGb,\n        [V2UpgradeToken] = @V2UpgradeToken,\n        [MasterPasswordSalt] = @MasterPasswordSalt\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_UpdateAccountCryptographicState.sql",
    "content": "CREATE PROCEDURE [dbo].[User_UpdateAccountCryptographicState]\n    @Id UNIQUEIDENTIFIER,\n    @PublicKey NVARCHAR(MAX),\n    @PrivateKey NVARCHAR(MAX),\n    @SignedPublicKey NVARCHAR(MAX) = NULL,\n    @SecurityState NVARCHAR(MAX) = NULL,\n    @SecurityVersion INT = NULL,\n    @SignatureKeyPairId UNIQUEIDENTIFIER = NULL,\n    @SignatureAlgorithm TINYINT = NULL,\n    @SigningKey VARCHAR(MAX) = NULL,\n    @VerifyingKey VARCHAR(MAX) = NULL,\n    @RevisionDate DATETIME2(7),\n    @AccountRevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[User]\n    SET\n        [PublicKey] = @PublicKey,\n        [PrivateKey] = @PrivateKey,\n        [SignedPublicKey] = @SignedPublicKey,\n        [SecurityState] = @SecurityState,\n        [SecurityVersion] = @SecurityVersion,\n        [RevisionDate] = @RevisionDate,\n        [AccountRevisionDate] = @AccountRevisionDate\n    WHERE\n        [Id] = @Id\n\n    IF EXISTS (SELECT 1 FROM [dbo].[UserSignatureKeyPair] WHERE [UserId] = @Id)\n    BEGIN\n        UPDATE [dbo].[UserSignatureKeyPair]\n        SET\n            [SignatureAlgorithm] = @SignatureAlgorithm,\n            [SigningKey] = @SigningKey,\n            [VerifyingKey] = @VerifyingKey,\n            [RevisionDate] = @RevisionDate\n        WHERE\n            [UserId] = @Id\n    END\n    ELSE\n    BEGIN\n        INSERT INTO [dbo].[UserSignatureKeyPair]\n        (\n            [Id],\n            [UserId],\n            [SignatureAlgorithm],\n            [SigningKey],\n            [VerifyingKey],\n            [CreationDate],\n            [RevisionDate]\n        )\n        VALUES\n        (\n            @SignatureKeyPairId,\n            @Id,\n            @SignatureAlgorithm,\n            @SigningKey,\n            @VerifyingKey,\n            @RevisionDate,\n            @RevisionDate\n        )\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_UpdateKeys.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_UpdateKeys]\n    @Id UNIQUEIDENTIFIER,\n    @SecurityStamp NVARCHAR(50),\n    @Key NVARCHAR(MAX),\n    @PrivateKey VARCHAR(MAX),\n    @RevisionDate DATETIME2(7),\n    @AccountRevisionDate DATETIME2(7) = NULL,\n    @LastKeyRotationDate DATETIME2(7) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[User]\n    SET\n        [SecurityStamp] = @SecurityStamp,\n        [Key] = @Key,\n        [PrivateKey] = @PrivateKey,\n        [RevisionDate] = @RevisionDate,\n        [AccountRevisionDate] = ISNULL(@AccountRevisionDate, @RevisionDate),\n        [LastKeyRotationDate] = @LastKeyRotationDate\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_UpdateRenewalReminderDate.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_UpdateRenewalReminderDate]\n    @Id UNIQUEIDENTIFIER,\n    @RenewalReminderDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[User]\n    SET\n        [RenewalReminderDate] = @RenewalReminderDate\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/User_UpdateStorage.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[User_UpdateStorage]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @AttachmentStorage BIGINT\n    DECLARE @SendStorage BIGINT\n\n    CREATE TABLE #UserStorageUpdateTemp\n    ( \n        [Id] UNIQUEIDENTIFIER NOT NULL,\n        [Attachments] VARCHAR(MAX) NULL\n    )\n\n    INSERT INTO #UserStorageUpdateTemp\n    SELECT\n        [Id],\n        [Attachments]\n    FROM\n        [dbo].[Cipher]\n    WHERE\n        [UserId] = @Id\n\n    ;WITH [CTE] AS (\n        SELECT\n            [Id],\n            (\n                SELECT\n                    SUM(CAST(JSON_VALUE(value,'$.Size') AS BIGINT))\n                FROM\n                    OPENJSON([Attachments])\n            ) [Size]\n        FROM\n            #UserStorageUpdateTemp\n    )\n    SELECT\n        @AttachmentStorage = SUM([CTE].[Size])\n    FROM\n        [CTE]\n\n    DROP TABLE #UserStorageUpdateTemp\n\n    ;WITH [CTE] AS (\n        SELECT\n            [Id],\n            CAST(JSON_VALUE([Data],'$.Size') AS BIGINT) [Size]\n        FROM\n            [Send]\n        WHERE\n            [UserId] = @Id\n    )\n    SELECT\n        @SendStorage = SUM([CTE].[Size])\n    FROM\n        [CTE]\n\n    UPDATE\n        [dbo].[User]\n    SET\n        [Storage] = (ISNULL(@AttachmentStorage, 0) + ISNULL(@SendStorage, 0)),\n        [RevisionDate] = GETUTCDATE()\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql",
    "content": "CREATE PROCEDURE [dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]\n@Email NVARCHAR(256)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @Domain NVARCHAR(256)\n\n    SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email))\n\n    SELECT\n        O.Id AS OrganizationId,\n        O.Name AS OrganizationName,\n        O.Identifier AS OrganizationIdentifier,\n        OD.DomainName\n    FROM [dbo].[OrganizationView] O\n             INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId\n             LEFT JOIN [dbo].[SsoConfig] S ON O.Id = S.OrganizationId\n    WHERE OD.DomainName = @Domain\n      AND O.Enabled = 1\n      AND OD.VerifiedDate IS NOT NULL\n      AND S.Enabled = 1\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/WebAuthnCredential_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[WebAuthnCredential_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @Name NVARCHAR(50),\n    @PublicKey VARCHAR (256),\n    @CredentialId VARCHAR(256),\n    @Counter INT,\n    @Type VARCHAR(20),\n    @AaGuid UNIQUEIDENTIFIER,\n    @EncryptedUserKey VARCHAR (MAX),\n    @EncryptedPrivateKey VARCHAR (MAX),\n    @EncryptedPublicKey VARCHAR (MAX),\n    @SupportsPrf BIT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[WebAuthnCredential]\n    (\n        [Id],\n        [UserId],\n        [Name],\n        [PublicKey],\n        [CredentialId],\n        [Counter],\n        [Type],\n        [AaGuid],\n        [EncryptedUserKey],\n        [EncryptedPrivateKey],\n        [EncryptedPublicKey],\n        [SupportsPrf],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @UserId,\n        @Name,\n        @PublicKey,\n        @CredentialId,\n        @Counter,\n        @Type,\n        @AaGuid,\n        @EncryptedUserKey,\n        @EncryptedPrivateKey,\n        @EncryptedPublicKey,\n        @SupportsPrf,\n        @CreationDate,\n        @RevisionDate\n    )\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/WebAuthnCredential_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[WebAuthnCredential_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DELETE\n    FROM\n        [dbo].[WebAuthnCredential]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/WebAuthnCredential_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[WebAuthnCredential_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[WebAuthnCredentialView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/WebAuthnCredential_ReadByIdUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[WebAuthnCredential_ReadByIdUserId]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[WebAuthnCredentialView]\n    WHERE\n        [Id] = @Id\n    AND\n        [UserId] = @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/WebAuthnCredential_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[WebAuthnCredential_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[WebAuthnCredentialView]\n    WHERE\n        [UserId] = @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Stored Procedures/WebAuthnCredential_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[WebAuthnCredential_Update]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @Name NVARCHAR(50),\n    @PublicKey VARCHAR (256),\n    @CredentialId VARCHAR(256),\n    @Counter INT,\n    @Type VARCHAR(20),\n    @AaGuid UNIQUEIDENTIFIER,\n    @EncryptedUserKey VARCHAR (MAX),\n    @EncryptedPrivateKey VARCHAR (MAX),\n    @EncryptedPublicKey VARCHAR (MAX),\n    @SupportsPrf BIT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[WebAuthnCredential]\n    SET\n        [UserId] = @UserId,\n        [Name] = @Name,\n        [PublicKey] = @PublicKey,\n        [CredentialId] = @CredentialId,\n        [Counter] = @Counter,\n        [Type] = @Type,\n        [AaGuid] = @AaGuid,\n        [EncryptedUserKey] = @EncryptedUserKey,\n        [EncryptedPrivateKey] = @EncryptedPrivateKey,\n        [EncryptedPublicKey] = @EncryptedPublicKey,\n        [SupportsPrf] = @SupportsPrf,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Tables/Cache.sql",
    "content": "﻿CREATE TABLE [dbo].[Cache]\n(\n    [Id] NVARCHAR (449) NOT NULL,\n    [Value] VARBINARY (MAX) NOT NULL,\n    [ExpiresAtTime] DATETIMEOFFSET (7) NOT NULL,\n    [SlidingExpirationInSeconds] BIGINT NULL,\n    [AbsoluteExpiration] DATETIMEOFFSET (7) NULL,\n    CONSTRAINT [PK_Cache] PRIMARY KEY CLUSTERED ([Id] ASC)\n);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_Cache_ExpiresAtTime]\n    ON [dbo].[Cache]([ExpiresAtTime] ASC);\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Tables/Collection.sql",
    "content": "﻿CREATE TABLE [dbo].[Collection] (\n    [Id]                            UNIQUEIDENTIFIER    NOT NULL,\n    [OrganizationId]                UNIQUEIDENTIFIER    NOT NULL,\n    [Name]                          VARCHAR (MAX)       NOT NULL,\n    [ExternalId]                    NVARCHAR (300)      NULL,\n    [CreationDate]                  DATETIME2 (7)       NOT NULL,\n    [RevisionDate]                  DATETIME2 (7)       NOT NULL,\n    [DefaultUserCollectionEmail]    NVARCHAR(256)       NULL,\n    [Type]                          TINYINT             NOT NULL DEFAULT(0),\n    CONSTRAINT [PK_Collection] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Collection_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE\n);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_Collection_OrganizationId_IncludeAll]\n    ON [dbo].[Collection]([OrganizationId] ASC)\n    INCLUDE([CreationDate], [Name], [RevisionDate], [Type]);\nGO\n\n"
  },
  {
    "path": "src/Sql/dbo/Tables/CollectionCipher.sql",
    "content": "﻿CREATE TABLE [dbo].[CollectionCipher] (\n    [CollectionId] UNIQUEIDENTIFIER NOT NULL,\n    [CipherId]     UNIQUEIDENTIFIER NOT NULL,\n    CONSTRAINT [PK_CollectionCipher] PRIMARY KEY CLUSTERED ([CollectionId] ASC, [CipherId] ASC),\n    CONSTRAINT [FK_CollectionCipher_Cipher] FOREIGN KEY ([CipherId]) REFERENCES [dbo].[Cipher] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_CollectionCipher_Collection] FOREIGN KEY ([CollectionId]) REFERENCES [dbo].[Collection] ([Id]) ON DELETE CASCADE\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_CollectionCipher_CipherId]\n    ON [dbo].[CollectionCipher]([CipherId] ASC);\n\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Tables/CollectionGroup.sql",
    "content": "﻿CREATE TABLE [dbo].[CollectionGroup] (\n    [CollectionId]  UNIQUEIDENTIFIER NOT NULL,\n    [GroupId]       UNIQUEIDENTIFIER NOT NULL,\n    [ReadOnly]      BIT              NOT NULL,\n    [HidePasswords] BIT              NOT NULL,\n    [Manage]        BIT              NOT NULL CONSTRAINT D_CollectionGroup_Manage DEFAULT (0),\n    CONSTRAINT [PK_CollectionGroup] PRIMARY KEY CLUSTERED ([CollectionId] ASC, [GroupId] ASC),\n    CONSTRAINT [FK_CollectionGroup_Collection] FOREIGN KEY ([CollectionId]) REFERENCES [dbo].[Collection] ([Id]),\n    CONSTRAINT [FK_CollectionGroup_Group] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[Group] ([Id]) ON DELETE CASCADE\n);\n\nGO\nCREATE NONCLUSTERED INDEX IX_CollectionGroup_GroupId\n    ON [dbo].[CollectionGroup] (GroupId)\n    INCLUDE (ReadOnly, HidePasswords, Manage)\n\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Tables/CollectionUser.sql",
    "content": "﻿CREATE TABLE [dbo].[CollectionUser] (\n    [CollectionId]       UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationUserId] UNIQUEIDENTIFIER NOT NULL,\n    [ReadOnly]           BIT              NOT NULL,\n    [HidePasswords]      BIT              NOT NULL,\n    [Manage]             BIT              NOT NULL CONSTRAINT D_CollectionUser_Manage DEFAULT (0),\n    CONSTRAINT [PK_CollectionUser] PRIMARY KEY CLUSTERED ([CollectionId] ASC, [OrganizationUserId] ASC),\n    CONSTRAINT [FK_CollectionUser_Collection] FOREIGN KEY ([CollectionId]) REFERENCES [dbo].[Collection] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_CollectionUser_OrganizationUser] FOREIGN KEY ([OrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id])\n);\n\nGO\nCREATE NONCLUSTERED INDEX IX_CollectionUser_OrganizationUserId\n    ON [dbo].[CollectionUser] (OrganizationUserId)\n    INCLUDE (ReadOnly, HidePasswords, Manage)\n\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Tables/Device.sql",
    "content": "﻿CREATE TABLE [dbo].[Device] (\n    [Id]                  UNIQUEIDENTIFIER NOT NULL,\n    [UserId]              UNIQUEIDENTIFIER NOT NULL,\n    [Name]                NVARCHAR (50)    NOT NULL,\n    [Type]                SMALLINT         NOT NULL,\n    [Identifier]          NVARCHAR (50)    NOT NULL,\n    [PushToken]           NVARCHAR (255)   NULL,\n    [CreationDate]        DATETIME2 (7)    NOT NULL,\n    [RevisionDate]        DATETIME2 (7)    NOT NULL,\n    [EncryptedUserKey]    VARCHAR (MAX)    NULL,\n    [EncryptedPublicKey]  VARCHAR (MAX)    NULL,\n    [EncryptedPrivateKey] VARCHAR (MAX)    NULL,\n    [Active]              BIT              NOT NULL CONSTRAINT [DF_Device_Active] DEFAULT (1),\n    CONSTRAINT [PK_Device] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Device_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])\n);\n\nGO\nCREATE UNIQUE NONCLUSTERED INDEX [UX_Device_UserId_Identifier]\n    ON [dbo].[Device]([UserId] ASC, [Identifier] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Device_Identifier]\n    ON [dbo].[Device]([Identifier] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/Tables/Group.sql",
    "content": "﻿CREATE TABLE [dbo].[Group] (\n    [Id]             UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NOT NULL,\n    [Name]           NVARCHAR (100)   NOT NULL,\n    [ExternalId]     NVARCHAR (300)   NULL,\n    [CreationDate]   DATETIME         NOT NULL,\n    [RevisionDate]   DATETIME         NOT NULL,\n    CONSTRAINT [PK_Group] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Group_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE\n);\n\n"
  },
  {
    "path": "src/Sql/dbo/Tables/GroupUser.sql",
    "content": "﻿CREATE TABLE [dbo].[GroupUser] (\n    [GroupId]            UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationUserId] UNIQUEIDENTIFIER NOT NULL,\n    CONSTRAINT [PK_GroupUser] PRIMARY KEY CLUSTERED ([GroupId] ASC, [OrganizationUserId] ASC),\n    CONSTRAINT [FK_GroupUser_Group] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[Group] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_GroupUser_OrganizationUser] FOREIGN KEY ([OrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id])\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_GroupUser_OrganizationUserId]\n    ON [dbo].[GroupUser]([OrganizationUserId] ASC);\n\n\n\n"
  },
  {
    "path": "src/Sql/dbo/Tables/Notification.sql",
    "content": "CREATE TABLE [dbo].[Notification]\n(\n    [Id] UNIQUEIDENTIFIER NOT NULL,\n    [Priority] TINYINT NOT NULL,\n    [Global] BIT NOT NULL,\n    [ClientType] TINYINT NOT NULL,\n    [UserId] UNIQUEIDENTIFIER NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NULL,\n    [Title] NVARCHAR (256) NULL,\n    [Body] NVARCHAR (MAX) NULL,\n    [CreationDate] DATETIME2 (7) NOT NULL,\n    [RevisionDate] DATETIME2 (7) NOT NULL,\n    [TaskId] UNIQUEIDENTIFIER NULL,\n    CONSTRAINT [PK_Notification] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Notification_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),\n    CONSTRAINT [FK_Notification_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]),\n    CONSTRAINT [FK_Notification_SecurityTask] FOREIGN KEY ([TaskId]) REFERENCES [dbo].[SecurityTask] ([Id]) ON DELETE CASCADE\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Notification_Priority_CreationDate_ClientType_Global_UserId_OrganizationId]\n    ON [dbo].[Notification]([Priority] DESC, [CreationDate] DESC, [ClientType], [Global], [UserId], [OrganizationId]);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Notification_UserId]\n    ON [dbo].[Notification]([UserId] ASC) WHERE UserId IS NOT NULL;\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Notification_OrganizationId]\n    ON [dbo].[Notification]([OrganizationId] ASC) WHERE OrganizationId IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Notification_TaskId]\n    ON [dbo].[Notification] ([TaskId] ASC) WHERE TaskId IS NOT NULL;\n"
  },
  {
    "path": "src/Sql/dbo/Tables/NotificationStatus.sql",
    "content": "CREATE TABLE [dbo].[NotificationStatus]\n(\n    [NotificationId] UNIQUEIDENTIFIER NOT NULL,\n    [UserId] UNIQUEIDENTIFIER NOT NULL,\n    [ReadDate] DATETIME2 (7) NULL,\n    [DeletedDate] DATETIME2 (7) NULL,\n    CONSTRAINT [PK_NotificationStatus] PRIMARY KEY CLUSTERED ([NotificationId] ASC, [UserId] ASC),\n    CONSTRAINT [FK_NotificationStatus_Notification] FOREIGN KEY ([NotificationId]) REFERENCES [dbo].[Notification] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_NotificationStatus_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_NotificationStatus_UserId]\n    ON [dbo].[NotificationStatus]([UserId] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/Tables/Organization.sql",
    "content": "CREATE TABLE [dbo].[Organization] (\n    [Id]                            UNIQUEIDENTIFIER NOT NULL,\n    [Identifier]                    NVARCHAR (50)    NULL,\n    [Name]                          NVARCHAR (50)    NOT NULL,\n    [BusinessName]                  NVARCHAR (50)    NULL,\n    [BusinessAddress1]              NVARCHAR (50)    NULL,\n    [BusinessAddress2]              NVARCHAR (50)    NULL,\n    [BusinessAddress3]              NVARCHAR (50)    NULL,\n    [BusinessCountry]               VARCHAR (2)      NULL,\n    [BusinessTaxNumber]             NVARCHAR (30)    NULL,\n    [BillingEmail]                  NVARCHAR (256)   NOT NULL,\n    [Plan]                          NVARCHAR (50)    NOT NULL,\n    [PlanType]                      TINYINT          NOT NULL,\n    [Seats]                         INT              NULL,\n    [MaxCollections]                SMALLINT         NULL,\n    [UsePolicies]                   BIT              NOT NULL,\n    [UseSso]                        BIT              NOT NULL,\n    [UseGroups]                     BIT              NOT NULL,\n    [UseDirectory]                  BIT              NOT NULL,\n    [UseEvents]                     BIT              NOT NULL,\n    [UseTotp]                       BIT              NOT NULL,\n    [Use2fa]                        BIT              NOT NULL,\n    [UseApi]                        BIT              NOT NULL,\n    [UseResetPassword]              BIT              NOT NULL,\n    [SelfHost]                      BIT              NOT NULL,\n    [UsersGetPremium]               BIT              NOT NULL,\n    [Storage]                       BIGINT           NULL,\n    [MaxStorageGb]                  SMALLINT         NULL,\n    [Gateway]                       TINYINT          NULL,\n    [GatewayCustomerId]             VARCHAR (50)     NULL,\n    [GatewaySubscriptionId]         VARCHAR (50)     NULL,\n    [ReferenceData]                 NVARCHAR (MAX)   NULL,\n    [Enabled]                       BIT              NOT NULL,\n    [LicenseKey]                    VARCHAR (100)    NULL,\n    [PublicKey]                     VARCHAR (MAX)    NULL,\n    [PrivateKey]                    VARCHAR (MAX)    NULL,\n    [TwoFactorProviders]            NVARCHAR (MAX)   NULL,\n    [ExpirationDate]                DATETIME2 (7)    NULL,\n    [CreationDate]                  DATETIME2 (7)    NOT NULL,\n    [RevisionDate]                  DATETIME2 (7)    NOT NULL,\n    [OwnersNotifiedOfAutoscaling]   DATETIME2(7)     NULL,\n    [MaxAutoscaleSeats]             INT              NULL,\n    [UseKeyConnector]               BIT              NOT NULL,\n    [UseScim]                       BIT              NOT NULL CONSTRAINT [DF_Organization_UseScim] DEFAULT (0),\n    [UseCustomPermissions]          BIT              NOT NULL CONSTRAINT [DF_Organization_UseCustomPermissions] DEFAULT (0),\n    [UseSecretsManager]             BIT              NOT NULL CONSTRAINT [DF_Organization_UseSecretsManager] DEFAULT (0),\n    [Status]                        TINYINT          NOT NULL CONSTRAINT [DF_Organization_Status] DEFAULT (1),\n    [UsePasswordManager]            BIT              NOT NULL CONSTRAINT [DF_Organization_UsePasswordManager] DEFAULT (1),\n    [SmSeats]                       INT              NULL,\n    [SmServiceAccounts]             INT              NULL,\n    [MaxAutoscaleSmSeats]           INT              NULL,\n    [MaxAutoscaleSmServiceAccounts] INT              NULL,\n    [SecretsManagerBeta]            BIT              NOT NULL CONSTRAINT [DF_Organization_SecretsManagerBeta] DEFAULT (0),\n    [LimitCollectionCreation]       BIT              NOT NULL CONSTRAINT [DF_Organization_LimitCollectionCreation] DEFAULT (0),\n    [LimitCollectionDeletion]       BIT              NOT NULL CONSTRAINT [DF_Organization_LimitCollectionDeletion] DEFAULT (0),\n    [LimitItemDeletion]             BIT              NOT NULL CONSTRAINT [DF_Organization_LimitItemDeletion] DEFAULT (0),\n    [AllowAdminAccessToAllCollectionItems]   BIT              NOT NULL CONSTRAINT [DF_Organization_AllowAdminAccessToAllCollectionItems] DEFAULT (0),\n    [UseRiskInsights]               BIT              NOT NULL CONSTRAINT [DF_Organization_UseRiskInsights] DEFAULT (0),\n    [UseOrganizationDomains]        BIT              NOT NULL CONSTRAINT [DF_Organization_UseOrganizationDomains] DEFAULT (0),\n    [UseAdminSponsoredFamilies]     BIT              NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] DEFAULT (0),\n    [SyncSeats]                     BIT              NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0),\n    [UseAutomaticUserConfirmation]  BIT              NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0),\n    [MaxStorageGbIncreased]         SMALLINT         NULL,\n    [UsePhishingBlocker]            BIT              NOT NULL CONSTRAINT [DF_Organization_UsePhishingBlocker] DEFAULT (0),\n    [UseDisableSmAdsForUsers]       BIT              NOT NULL CONSTRAINT [DF_Organization_UseDisableSmAdsForUsers] DEFAULT (0),\n    [UseMyItems]                    BIT              NOT NULL CONSTRAINT [DF_Organization_UseMyItems] DEFAULT (0),\n    CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Organization_Enabled]\n    ON [dbo].[Organization]([Id] ASC, [Enabled] ASC)\n    INCLUDE ([UseTotp], [UsersGetPremium]);\n\nGO\nCREATE UNIQUE NONCLUSTERED INDEX [IX_Organization_Identifier]\n    ON [dbo].[Organization]([Identifier] ASC)\n    WHERE [Identifier] IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Organization_GatewayCustomerId]\n    ON [dbo].[Organization]([GatewayCustomerId])\n    WHERE [GatewayCustomerId] IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Organization_GatewaySubscriptionId]\n    ON [dbo].[Organization]([GatewaySubscriptionId])\n    WHERE [GatewaySubscriptionId] IS NOT NULL;\n"
  },
  {
    "path": "src/Sql/dbo/Tables/OrganizationApiKey.sql",
    "content": "CREATE TABLE [dbo].[OrganizationApiKey] (\n    [Id]                UNIQUEIDENTIFIER,\n    [OrganizationId]    UNIQUEIDENTIFIER NOT NULL,\n    [Type]              TINYINT NOT NULL,\n    [ApiKey]            VARCHAR(30) NOT NULL,\n    [RevisionDate]      DATETIME2(7) NOT NULL\n    CONSTRAINT [PK_OrganizationApiKey] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganizationApiKey_OrganizationId] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\n\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationApiKey_OrganizationId]\n    ON [dbo].[OrganizationApiKey]([OrganizationId] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/Tables/OrganizationConnection.sql",
    "content": "CREATE TABLE [dbo].[OrganizationConnection] (\n    [Id]                UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId]    UNIQUEIDENTIFIER NOT NULL,\n    [Type]              TINYINT NOT NULL,\n    [Enabled]           BIT NOT NULL,\n    [Config]            NVARCHAR (MAX) NULL,\n    CONSTRAINT [PK_OrganizationConnection] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganizationConnection_OrganizationId] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationConnection_OrganizationId]\n    ON [dbo].[OrganizationConnection]([OrganizationId] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/Tables/OrganizationDomain.sql",
    "content": "CREATE TABLE [dbo].[OrganizationDomain] (\n    [Id]                UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId]    UNIQUEIDENTIFIER NOT NULL,\n    [Txt]               VARCHAR(MAX)     NOT NULL,\n    [DomainName]        NVARCHAR(255)    NOT NULL,\n    [CreationDate]      DATETIME2(7)     NOT NULL,\n    [VerifiedDate]      DATETIME2(7)     NULL,\n    [LastCheckedDate]   DATETIME2(7)     NULL,\n    [NextRunDate]       DATETIME2(7)     NOT NULL,\n    [JobRunCount]      TINYINT          NOT NULL\n    CONSTRAINT [PK_OrganizationDomain] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganzationDomain_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\n\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationDomain_OrganizationIdVerifiedDate]\n    ON [dbo].[OrganizationDomain] ([OrganizationId],[VerifiedDate]);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationDomain_VerifiedDate]\n    ON [dbo].[OrganizationDomain] ([VerifiedDate])\n    INCLUDE ([OrganizationId],[DomainName]);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationDomain_DomainNameVerifiedDateOrganizationId]\n    ON [dbo].[OrganizationDomain] ([DomainName],[VerifiedDate])\n    INCLUDE ([OrganizationId]);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationDomain_OrganizationId_VerifiedDate]\n    ON [dbo].[OrganizationDomain] ([OrganizationId], [VerifiedDate])\n    INCLUDE ([DomainName])\n    WHERE [VerifiedDate] IS NOT NULL;\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Tables/OrganizationIntegration.sql",
    "content": "CREATE TABLE [dbo].[OrganizationIntegration]\n(\n    [Id] UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NOT NULL,\n    [Type] SMALLINT NOT NULL,\n    [Configuration] VARCHAR (MAX) NULL,\n    [CreationDate] DATETIME2 (7) NOT NULL,\n    [RevisionDate] DATETIME2 (7) NOT NULL,\n    CONSTRAINT [PK_OrganizationIntegration] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganizationIntegration_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationIntegration_OrganizationId]\n    ON [dbo].[OrganizationIntegration]([OrganizationId] ASC);\nGO\n\nCREATE UNIQUE INDEX [IX_OrganizationIntegration_Organization_Type]\n    ON [dbo].[OrganizationIntegration]([OrganizationId], [Type]);\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql",
    "content": "CREATE TABLE [dbo].[OrganizationIntegrationConfiguration]\n(\n    [Id] UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationIntegrationId] UNIQUEIDENTIFIER NOT NULL,\n    [EventType] SMALLINT NULL,\n    [Configuration] VARCHAR (MAX) NULL,\n    [Template] VARCHAR (MAX) NULL,\n    [CreationDate] DATETIME2 (7) NOT NULL,\n    [RevisionDate] DATETIME2 (7) NOT NULL,\n    [Filters] VARCHAR (MAX) NULL,\n    CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id]) ON DELETE CASCADE\n);\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Tables/OrganizationSponsorship.sql",
    "content": "CREATE TABLE [dbo].[OrganizationSponsorship] (\n    [Id]                            UNIQUEIDENTIFIER NOT NULL,\n    [SponsoringOrganizationId]      UNIQUEIDENTIFIER NULL,\n    [SponsoringOrganizationUserID]  UNIQUEIDENTIFIER NOT NULL,\n    [SponsoredOrganizationId]       UNIQUEIDENTIFIER NULL,\n    [FriendlyName]                  NVARCHAR(256)    NULL,\n    [OfferedToEmail]                NVARCHAR (256)   NULL,\n    [PlanSponsorshipType]           TINYINT          NULL,\n    [ToDelete]                      BIT              DEFAULT (0) NOT NULL,\n    [LastSyncDate]                  DATETIME2 (7)    NULL,\n    [ValidUntil]                    DATETIME2 (7)    NULL,\n    [IsAdminInitiated]              BIT              NOT NULL CONSTRAINT [DF_OrganizationSponsorship_IsAdminInitiated] DEFAULT (0),\n    [Notes]                         NVARCHAR(512)    NULL,\n    CONSTRAINT [PK_OrganizationSponsorship] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganizationSponsorship_SponsoringOrg] FOREIGN KEY ([SponsoringOrganizationId]) REFERENCES [dbo].[Organization] ([Id]),\n    CONSTRAINT [FK_OrganizationSponsorship_SponsoredOrg] FOREIGN KEY ([SponsoredOrganizationId]) REFERENCES [dbo].[Organization] ([Id]),\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId]\n    ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC)\n    WHERE [SponsoringOrganizationId] IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId]\n    ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_OfferedToEmail]\n    ON [dbo].[OrganizationSponsorship]([OfferedToEmail] ASC)\n    WHERE [OfferedToEmail] IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoredOrganizationID]\n    ON [dbo].[OrganizationSponsorship]([SponsoredOrganizationId] ASC)\n    WHERE [SponsoredOrganizationId] IS NOT NULL;\n"
  },
  {
    "path": "src/Sql/dbo/Tables/OrganizationUser.sql",
    "content": "﻿CREATE TABLE [dbo].[OrganizationUser] (\n    [Id]                            UNIQUEIDENTIFIER    NOT NULL,\n    [OrganizationId]                UNIQUEIDENTIFIER    NOT NULL,\n    [UserId]                        UNIQUEIDENTIFIER    NULL,\n    [Email]                         NVARCHAR (256)      NULL,\n    [Key]                           VARCHAR (MAX)       NULL,\n    [ResetPasswordKey]              VARCHAR (MAX)       NULL,\n    [Status]                        SMALLINT            NOT NULL,\n    [Type]                          TINYINT             NOT NULL,\n    [ExternalId]                    NVARCHAR (300)      NULL,\n    [CreationDate]                  DATETIME2 (7)       NOT NULL,\n    [RevisionDate]                  DATETIME2 (7)       NOT NULL,\n    [Permissions]                   NVARCHAR (MAX)      NULL,\n    [AccessSecretsManager]          BIT                 NOT NULL CONSTRAINT [DF_OrganizationUser_SecretsManager] DEFAULT (0),\n    CONSTRAINT [PK_OrganizationUser] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_OrganizationUser_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_OrganizationUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_OrganizationUser_UserIdOrganizationIdStatusV2]\n    ON [dbo].[OrganizationUser]([UserId] ASC, [OrganizationId] ASC, [Status] ASC);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId]\n    ON [dbo].[OrganizationUser]([OrganizationId] ASC);\nGO\n\nCREATE NONCLUSTERED INDEX  IX_OrganizationUser_EmailOrganizationIdStatus\n    ON OrganizationUser (Email ASC, OrganizationId ASC, [Status] ASC);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId_UserId]\n    ON [dbo].[OrganizationUser] ([OrganizationId], [UserId])\n    INCLUDE ([Email], [Status], [Type], [ExternalId], [CreationDate],\n        [RevisionDate], [Permissions], [ResetPasswordKey], [AccessSecretsManager]);\nGO\n\nCREATE NONCLUSTERED INDEX [IX_OrganizationUser_UserId_Status_Filtered]\n    ON [dbo].[OrganizationUser] ([UserId])\n    INCLUDE ([Id], [OrganizationId])\n    WHERE [Status] = 2; -- Confirmed\n\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Tables/PasswordHealthReportApplication.sql",
    "content": "CREATE TABLE [dbo].[PasswordHealthReportApplication] \n    (\n        Id UNIQUEIDENTIFIER NOT NULL,\n        OrganizationId UNIQUEIDENTIFIER NOT NULL,\n        Uri nvarchar(max),\n        CreationDate   DATETIME2(7)     NOT NULL,\n        RevisionDate   DATETIME2(7)     NOT NULL,\n        CONSTRAINT [PK_PasswordHealthReportApplication] PRIMARY KEY CLUSTERED ([Id] ASC),\n        CONSTRAINT [FK_PasswordHealthReportApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE\n    );\nGO\n\nCREATE NONCLUSTERED INDEX [IX_PasswordHealthReportApplication_OrganizationId]\n        ON [dbo].[PasswordHealthReportApplication] (OrganizationId);\nGO"
  },
  {
    "path": "src/Sql/dbo/Tables/PlayItem.sql",
    "content": "CREATE TABLE [dbo].[PlayItem] (\n    [Id]             UNIQUEIDENTIFIER NOT NULL,\n    [PlayId]         NVARCHAR (256)    NOT NULL,\n    [UserId]         UNIQUEIDENTIFIER NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NULL,\n    [CreationDate]   DATETIME2 (7)    NOT NULL,\n    CONSTRAINT [PK_PlayItem] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_PlayItem_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_PlayItem_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [CK_PlayItem_UserOrOrganization] CHECK (([UserId] IS NOT NULL AND [OrganizationId] IS NULL) OR ([UserId] IS NULL AND [OrganizationId] IS NOT NULL))\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_PlayItem_PlayId]\n    ON [dbo].[PlayItem]([PlayId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_PlayItem_UserId]\n    ON [dbo].[PlayItem]([UserId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_PlayItem_OrganizationId]\n    ON [dbo].[PlayItem]([OrganizationId] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/Tables/Policy.sql",
    "content": "﻿CREATE TABLE [dbo].[Policy] (\n    [Id]             UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NOT NULL,\n    [Type]           TINYINT          NOT NULL,\n    [Data]           NVARCHAR (MAX)   NULL,\n    [Enabled]        BIT              NOT NULL,\n    [CreationDate]   DATETIME2 (7)    NOT NULL,\n    [RevisionDate]   DATETIME2 (7)    NOT NULL,\n    CONSTRAINT [PK_Policy] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Policy_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE\n);\n\n\nGO\nCREATE UNIQUE NONCLUSTERED INDEX [IX_Policy_OrganizationId_Type]\n    ON [dbo].[Policy]([OrganizationId] ASC, [Type] ASC);\n\n"
  },
  {
    "path": "src/Sql/dbo/Tables/Provider.sql",
    "content": "﻿CREATE TABLE [dbo].[Provider] (\n    [Id]                    UNIQUEIDENTIFIER NOT NULL,\n    [Name]                  NVARCHAR (50)    NULL,\n    [BusinessName]          NVARCHAR (50)    NULL,\n    [BusinessAddress1]      NVARCHAR (50)    NULL,\n    [BusinessAddress2]      NVARCHAR (50)    NULL,\n    [BusinessAddress3]      NVARCHAR (50)    NULL,\n    [BusinessCountry]       VARCHAR (2)      NULL,\n    [BusinessTaxNumber]     NVARCHAR (30)    NULL,\n    [BillingEmail]          NVARCHAR (256)   NULL,\n    [BillingPhone]          NVARCHAR (50)    NULL,\n    [Status]                TINYINT          NOT NULL,\n    [UseEvents]             BIT              NOT NULL,\n    [Type]                  TINYINT          NOT NULL CONSTRAINT DF_Provider_Type DEFAULT (0),\n    [Enabled]               BIT              NOT NULL,\n    [CreationDate]          DATETIME2 (7)    NOT NULL,\n    [RevisionDate]          DATETIME2 (7)    NOT NULL,\n    [Gateway]               TINYINT          NULL,\n    [GatewayCustomerId]     VARCHAR (50)     NULL,\n    [GatewaySubscriptionId] VARCHAR (50)     NULL,\n    [DiscountId]            VARCHAR (50)     NULL,\n    CONSTRAINT [PK_Provider] PRIMARY KEY CLUSTERED ([Id] ASC)\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Provider_GatewayCustomerId]\n    ON [dbo].[Provider]([GatewayCustomerId])\n    WHERE [GatewayCustomerId] IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Provider_GatewaySubscriptionId]\n    ON [dbo].[Provider]([GatewaySubscriptionId])\n    WHERE [GatewaySubscriptionId] IS NOT NULL;\n"
  },
  {
    "path": "src/Sql/dbo/Tables/ProviderOrganization.sql",
    "content": "﻿CREATE TABLE [dbo].[ProviderOrganization] (\n    [Id]             UNIQUEIDENTIFIER    NOT NULL,\n    [ProviderId]     UNIQUEIDENTIFIER    NOT NULL,\n    [OrganizationId] UNIQUEIDENTIFIER    NULL,\n    [Key]            VARCHAR (MAX)       NULL,\n    [Settings]       NVARCHAR(MAX)       NULL,\n    [CreationDate]   DATETIME2 (7)       NOT NULL,\n    [RevisionDate]   DATETIME2 (7)       NOT NULL,\n    CONSTRAINT [PK_ProviderOrganization] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_ProviderOrganization_Provider] FOREIGN KEY ([ProviderId]) REFERENCES [dbo].[Provider] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_ProviderOrganization_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])\n);\n\nGO\nCREATE NONCLUSTERED INDEX IX_ProviderOrganization_OrganizationIdProviderId\n       ON [dbo].[ProviderOrganization] ([OrganizationId], [ProviderId]);\n"
  },
  {
    "path": "src/Sql/dbo/Tables/ProviderUser.sql",
    "content": "﻿CREATE TABLE [dbo].[ProviderUser] (\n    [Id]           UNIQUEIDENTIFIER    NOT NULL,\n    [ProviderId]   UNIQUEIDENTIFIER    NOT NULL,\n    [UserId]       UNIQUEIDENTIFIER    NULL,\n    [Email]        NVARCHAR (256)      NULL,\n    [Key]          VARCHAR (MAX)       NULL,\n    [Status]       TINYINT             NOT NULL,\n    [Type]         TINYINT             NOT NULL,\n    [Permissions]  NVARCHAR (MAX)      NULL,\n    [CreationDate] DATETIME2 (7)       NOT NULL,\n    [RevisionDate] DATETIME2 (7)       NOT NULL,\n    CONSTRAINT [PK_ProviderUser] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_ProviderUser_Provider] FOREIGN KEY ([ProviderId]) REFERENCES [dbo].[Provider] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_ProviderUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX IX_ProviderUser_UserIdProviderId\n       ON [dbo].[ProviderUser] ([UserId], [ProviderId]);\n"
  },
  {
    "path": "src/Sql/dbo/Tables/SubscriptionDiscount.sql",
    "content": "CREATE TABLE [dbo].[SubscriptionDiscount] (\n    [Id]                    UNIQUEIDENTIFIER NOT NULL,\n    [StripeCouponId]        VARCHAR (50)     NOT NULL,\n    [StripeProductIds]      NVARCHAR (MAX)   NULL,\n    [PercentOff]            DECIMAL (5, 2)   NULL,\n    [AmountOff]             BIGINT           NULL,\n    [Currency]              VARCHAR (10)     NULL,\n    [Duration]              VARCHAR (20)     NOT NULL,\n    [DurationInMonths]      INT              NULL,\n    [Name]                  NVARCHAR (100)   NULL,\n    [StartDate]             DATETIME2 (7)    NOT NULL,\n    [EndDate]               DATETIME2 (7)    NOT NULL,\n    [AudienceType]          INT              NOT NULL CONSTRAINT [DF_SubscriptionDiscount_AudienceType] DEFAULT (0),\n    [CreationDate]          DATETIME2 (7)    NOT NULL,\n    [RevisionDate]          DATETIME2 (7)    NOT NULL,\n    CONSTRAINT [PK_SubscriptionDiscount] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [IX_SubscriptionDiscount_StripeCouponId] UNIQUE NONCLUSTERED ([StripeCouponId] ASC)\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_SubscriptionDiscount_DateRange]\n    ON [dbo].[SubscriptionDiscount]([StartDate] ASC, [EndDate] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/Tables/TaxRate.sql",
    "content": "CREATE TABLE [dbo].[TaxRate] (\n    [Id]                VARCHAR(40)         NOT NULL,\n    [Country]           VARCHAR(50)         NOT NULL,\n    [State]             VARCHAR(2)          NULL,\n    [PostalCode]        VARCHAR(10)         NOT NULL,\n    [Rate]              DECIMAL(6,3)        NOT NULL,\n    [Active]            BIT                 NOT NULL,\n    CONSTRAINT [PK_TaxRate] PRIMARY KEY CLUSTERED ([Id] ASC)\n);\nGO\n\nCREATE UNIQUE INDEX [IX_TaxRate_Country_PostalCode_Active_Uniqueness]\nON [dbo].[TaxRate](Country, PostalCode)\nWHERE Active = 1;\n"
  },
  {
    "path": "src/Sql/dbo/Tables/Transaction.sql",
    "content": "﻿CREATE TABLE [dbo].[Transaction] (\n    [Id]                    UNIQUEIDENTIFIER    NOT NULL,\n    [UserId]                UNIQUEIDENTIFIER    NULL,\n    [OrganizationId]        UNIQUEIDENTIFIER    NULL,\n    [Type]                  TINYINT             NOT NULL,\n    [Amount]                MONEY               NOT NULL,\n    [Refunded]              BIT                 NULL,\n    [RefundedAmount]        MONEY               NULL,\n    [Details]               NVARCHAR(100)       NULL,\n    [PaymentMethodType]     TINYINT             NULL,\n    [Gateway]               TINYINT             NULL,\n    [GatewayId]             VARCHAR(50)         NULL,\n    [CreationDate]          DATETIME2 (7)       NOT NULL,\n    [ProviderId]            UNIQUEIDENTIFIER    NULL,\n    CONSTRAINT [PK_Transaction] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Transaction_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_Transaction_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_Transaction_Provider] FOREIGN KEY ([ProviderId]) REFERENCES [dbo].[Provider] ([Id]) ON DELETE CASCADE\n);\n\nGO\nCREATE UNIQUE NONCLUSTERED INDEX [IX_Transaction_Gateway_GatewayId]\n    ON [dbo].[Transaction]([Gateway] ASC, [GatewayId] ASC)\n    WHERE [Gateway] IS NOT NULL AND [GatewayId] IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Transaction_UserId_OrganizationId_CreationDate]\n    ON [dbo].[Transaction]([UserId] ASC, [OrganizationId] ASC, [CreationDate] ASC);\n"
  },
  {
    "path": "src/Sql/dbo/Tables/User.sql",
    "content": "﻿CREATE TABLE [dbo].[User] (\n    [Id]                               UNIQUEIDENTIFIER NOT NULL,\n    [Name]                             NVARCHAR (50)    NULL,\n    [Email]                            NVARCHAR (256)   NOT NULL,\n    [EmailVerified]                    BIT              NOT NULL,\n    [MasterPassword]                   NVARCHAR (300)   NULL,\n    [MasterPasswordHint]               NVARCHAR (50)    NULL,\n    [Culture]                          NVARCHAR (10)    NOT NULL,\n    [SecurityStamp]                    NVARCHAR (50)    NOT NULL,\n    [TwoFactorProviders]               NVARCHAR (MAX)   NULL,\n    [TwoFactorRecoveryCode]            NVARCHAR (32)    NULL,\n    [EquivalentDomains]                NVARCHAR (MAX)   NULL,\n    [ExcludedGlobalEquivalentDomains]  NVARCHAR (MAX)   NULL,\n    [AccountRevisionDate]              DATETIME2 (7)    NOT NULL,\n    [Key]                              VARCHAR (MAX)    NULL,\n    [PublicKey]                        VARCHAR (MAX)    NULL,\n    [PrivateKey]                       VARCHAR (MAX)    NULL,\n    [Premium]                          BIT              NOT NULL,\n    [PremiumExpirationDate]            DATETIME2 (7)    NULL,\n    [RenewalReminderDate]              DATETIME2 (7)    NULL,\n    [Storage]                          BIGINT           NULL,\n    [MaxStorageGb]                     SMALLINT         NULL,\n    [Gateway]                          TINYINT          NULL,\n    [GatewayCustomerId]                VARCHAR (50)     NULL,\n    [GatewaySubscriptionId]            VARCHAR (50)     NULL,\n    [ReferenceData]                    NVARCHAR (MAX)   NULL,\n    [LicenseKey]                       VARCHAR (100)    NULL,\n    [Kdf]                              TINYINT          NOT NULL,\n    [KdfIterations]                    INT              NOT NULL,\n    [KdfMemory]                        INT              NULL,\n    [KdfParallelism]                   INT              NULL,\n    [CreationDate]                     DATETIME2 (7)    NOT NULL,\n    [RevisionDate]                     DATETIME2 (7)    NOT NULL,\n    [ApiKey]                           VARCHAR (30)     NOT NULL,\n    [ForcePasswordReset]               BIT              NOT NULL,\n    [UsesKeyConnector]                 BIT              NOT NULL,\n    [FailedLoginCount]                 INT              CONSTRAINT [D_User_FailedLoginCount] DEFAULT ((0)) NOT NULL,\n    [LastFailedLoginDate]              DATETIME2 (7)    NULL,\n    [AvatarColor]                      VARCHAR(7)       NULL,\n    [LastPasswordChangeDate]           DATETIME2 (7)    NULL,\n    [LastKdfChangeDate]                DATETIME2 (7)    NULL,\n    [LastKeyRotationDate]              DATETIME2 (7)    NULL,\n    [LastEmailChangeDate]              DATETIME2 (7)    NULL,\n    [VerifyDevices]                    BIT              DEFAULT ((1)) NOT NULL,\n    [SecurityState]                    VARCHAR (MAX)    NULL,\n    [SecurityVersion]                  INT              NULL,\n    [SignedPublicKey]                  VARCHAR (MAX)    NULL,\n    [MaxStorageGbIncreased]            SMALLINT         NULL,\n    [V2UpgradeToken]                   VARCHAR(MAX)     NULL,\n    [MasterPasswordSalt]               NVARCHAR (256)   NULL,\n    CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)\n);\n\nGO\nCREATE UNIQUE NONCLUSTERED INDEX [IX_User_Email]\n    ON [dbo].[User]([Email] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_User_Premium_PremiumExpirationDate_RenewalReminderDate]\n    ON [dbo].[User]([Premium] ASC, [PremiumExpirationDate] ASC, [RenewalReminderDate] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_User_Id_EmailDomain]\n    ON [dbo].[User]([Id] ASC, [Email] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_User_GatewayCustomerId]\n    ON [dbo].[User]([GatewayCustomerId])\n    WHERE [GatewayCustomerId] IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_User_GatewaySubscriptionId]\n    ON [dbo].[User]([GatewaySubscriptionId])\n    WHERE [GatewaySubscriptionId] IS NOT NULL;\n"
  },
  {
    "path": "src/Sql/dbo/Tables/WebAuthnCredential.sql",
    "content": "﻿CREATE TABLE [dbo].[WebAuthnCredential] (\n    [Id]                    UNIQUEIDENTIFIER NOT NULL,\n    [UserId]                UNIQUEIDENTIFIER NOT NULL,\n    [Name]                  NVARCHAR (50)    NOT NULL,\n    [PublicKey]             VARCHAR (256)    NOT NULL,\n    [CredentialId]          VARCHAR (256)    NOT NULL,\n    [Counter]               INT              NOT NULL,\n    [Type]                  VARCHAR (20)     NULL,\n    [AaGuid]                UNIQUEIDENTIFIER NOT NULL,\n    [EncryptedUserKey]      VARCHAR (MAX)    NULL,\n    [EncryptedPrivateKey]   VARCHAR (MAX)    NULL,\n    [EncryptedPublicKey]    VARCHAR (MAX)    NULL,\n    [SupportsPrf]           BIT              NOT NULL,\n    [CreationDate]          DATETIME2 (7)    NOT NULL,\n    [RevisionDate]          DATETIME2 (7)    NOT NULL,\n    CONSTRAINT [PK_WebAuthnCredential] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_WebAuthnCredential_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_WebAuthnCredential_UserId]\n    ON [dbo].[WebAuthnCredential]([UserId] ASC);\n\n"
  },
  {
    "path": "src/Sql/dbo/Tools/Stored Procedures/Send_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Send_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data VARCHAR(MAX),\n    @Key VARCHAR(MAX),\n    @Password NVARCHAR(300),\n    @MaxAccessCount INT,\n    @AccessCount INT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @ExpirationDate DATETIME2(7),\n    @DeletionDate DATETIME2(7),\n    @Disabled BIT,\n    @HideEmail BIT,\n    @CipherId UNIQUEIDENTIFIER = NULL,\n--  FIXME: remove null default value once this argument has been\n--         in 2 server releases\n    @Emails NVARCHAR(4000) = NULL,\n    @AuthType TINYINT = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Send]\n    (\n        [Id],\n        [UserId],\n        [OrganizationId],\n        [Type],\n        [Data],\n        [Key],\n        [Password],\n        [MaxAccessCount],\n        [AccessCount],\n        [CreationDate],\n        [RevisionDate],\n        [ExpirationDate],\n        [DeletionDate],\n        [Disabled],\n        [HideEmail],\n        [CipherId],\n        [Emails],\n        [AuthType]\n    )\n    VALUES\n    (\n        @Id,\n        @UserId,\n        @OrganizationId,\n        @Type,\n        @Data,\n        @Key,\n        @Password,\n        @MaxAccessCount,\n        @AccessCount,\n        @CreationDate,\n        @RevisionDate,\n        @ExpirationDate,\n        @DeletionDate,\n        @Disabled,\n        @HideEmail,\n        @CipherId,\n        @Emails,\n        @AuthType\n    )\n\n    IF @UserId IS NOT NULL\n    BEGIN\n        IF @Type = 1 --File\n        BEGIN\n            EXEC [dbo].[User_UpdateStorage] @UserId\n        END\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\n    -- TODO: OrganizationId bump?\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Tools/Stored Procedures/Send_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Send_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserId UNIQUEIDENTIFIER\n    DECLARE @OrganizationId UNIQUEIDENTIFIER\n    DECLARE @Type TINYINT\n\n    SELECT TOP 1\n        @UserId = [UserId],\n        @OrganizationId = [OrganizationId],\n        @Type = [Type]\n    FROM\n        [dbo].[Send]\n    WHERE\n        [Id] = @Id\n\n    DELETE\n    FROM\n        [dbo].[Send]\n    WHERE\n        [Id] = @Id\n\n    IF @UserId IS NOT NULL\n    BEGIN\n        IF @Type = 1 --File\n        BEGIN\n            EXEC [dbo].[User_UpdateStorage] @UserId\n        END\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\n    -- TODO: OrganizationId bump?\nEND"
  },
  {
    "path": "src/Sql/dbo/Tools/Stored Procedures/Send_ReadByDeletionDateBefore.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Send_ReadByDeletionDateBefore]\n    @DeletionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SendView]\n    WHERE\n        [DeletionDate] < @DeletionDate\nEND"
  },
  {
    "path": "src/Sql/dbo/Tools/Stored Procedures/Send_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Send_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SendView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Tools/Stored Procedures/Send_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Send_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SendView]\n    WHERE\n        [OrganizationId] IS NULL\n        AND [UserId] = @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Tools/Stored Procedures/Send_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Send_Update]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data VARCHAR(MAX),\n    @Key VARCHAR(MAX),\n    @Password NVARCHAR(300),\n    @MaxAccessCount INT,\n    @AccessCount INT,\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @ExpirationDate DATETIME2(7),\n    @DeletionDate DATETIME2(7),\n    @Disabled BIT,\n    @HideEmail BIT,\n    @CipherId UNIQUEIDENTIFIER = NULL,\n    @Emails NVARCHAR(4000) = NULL,\n    @AuthType TINYINT = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Send]\n    SET\n        [UserId] = @UserId,\n        [OrganizationId] = @OrganizationId,\n        [Type] = @Type,\n        [Data] = @Data,\n        [Key] = @Key,\n        [Password] = @Password,\n        [MaxAccessCount] = @MaxAccessCount,\n        [AccessCount] = @AccessCount,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [ExpirationDate] = @ExpirationDate,\n        [DeletionDate] = @DeletionDate,\n        [Disabled] = @Disabled,\n        [HideEmail] = @HideEmail,\n        [CipherId] = @CipherId,\n        [Emails] = @Emails,\n        [AuthType] = @AuthType\n    WHERE\n        [Id] = @Id\n\n    IF @UserId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\n    -- TODO: OrganizationId bump?\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Tools/Tables/Send.sql",
    "content": "﻿CREATE TABLE [dbo].[Send]\n(\n    [Id]             UNIQUEIDENTIFIER NOT NULL,\n    [UserId]         UNIQUEIDENTIFIER NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NULL,\n    [Type]           TINYINT          NOT NULL,\n    [Data]           VARCHAR(MAX)     NOT NULL,\n    [Key]            VARCHAR(MAX)     NOT NULL,\n    [Password]       NVARCHAR(300)    NULL,\n    [Emails]         NVARCHAR(4000)   NULL,\n    [MaxAccessCount] INT              NULL,\n    [AccessCount]    INT              NOT NULL,\n    [CreationDate]   DATETIME2(7)     NOT NULL,\n    [RevisionDate]   DATETIME2(7)     NOT NULL,\n    [ExpirationDate] DATETIME2(7)     NULL,\n    [DeletionDate]   DATETIME2(7)     NOT NULL,\n    [Disabled]       BIT              NOT NULL,\n    [HideEmail]      BIT              NULL,\n    [CipherId]       UNIQUEIDENTIFIER NULL,\n    [AuthType]       TINYINT          NULL,\n    CONSTRAINT [PK_Send] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Send_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),\n    CONSTRAINT [FK_Send_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]),\n    CONSTRAINT [FK_Send_Cipher] FOREIGN KEY ([CipherId]) REFERENCES [dbo].[Cipher] ([Id])\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Send_UserId_OrganizationId]\n    ON [dbo].[Send] ([UserId] ASC, [OrganizationId] ASC);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Send_DeletionDate]\n    ON [dbo].[Send] ([DeletionDate] ASC);\n\n"
  },
  {
    "path": "src/Sql/dbo/Tools/Views/SendView.sql",
    "content": "﻿CREATE VIEW [dbo].[SendView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Send]"
  },
  {
    "path": "src/Sql/dbo/User Defined Types/CollectionAccessSelection.sql",
    "content": "CREATE TYPE [dbo].[CollectionAccessSelectionType] AS TABLE (\n    [Id]            UNIQUEIDENTIFIER NOT NULL,\n    [ReadOnly]      BIT              NOT NULL,\n    [HidePasswords] BIT              NOT NULL,\n    [Manage]        BIT              NOT NULL);\n\n"
  },
  {
    "path": "src/Sql/dbo/User Defined Types/EmailArray.sql",
    "content": "CREATE TYPE [dbo].[EmailArray] AS TABLE (\n    [Email] NVARCHAR(256) NOT NULL);\nGO\n"
  },
  {
    "path": "src/Sql/dbo/User Defined Types/GuidIdArray.sql",
    "content": "﻿CREATE TYPE [dbo].[GuidIdArray] AS TABLE (\n    [Id] UNIQUEIDENTIFIER NOT NULL);\n\n"
  },
  {
    "path": "src/Sql/dbo/User Defined Types/OrganizationSponsorshipType.sql",
    "content": "CREATE TYPE [dbo].[OrganizationSponsorshipType] AS TABLE(\n    [Id] UNIQUEIDENTIFIER,\n    [SponsoringOrganizationId] UNIQUEIDENTIFIER,\n    [SponsoringOrganizationUserID] UNIQUEIDENTIFIER,\n    [SponsoredOrganizationId] UNIQUEIDENTIFIER,\n    [FriendlyName] NVARCHAR(256),\n    [OfferedToEmail] VARCHAR(256),\n    [PlanSponsorshipType] TINYINT,\n    [LastSyncDate] DATETIME2(7),\n    [ValidUntil] DATETIME2(7),\n    [ToDelete] BIT,\n    [IsAdminInitiated] BIT DEFAULT 0,\n    [Notes] NVARCHAR(512) NULL\n)\n"
  },
  {
    "path": "src/Sql/dbo/User Defined Types/TwoGuidIdArray.sql",
    "content": "CREATE TYPE [dbo].[TwoGuidIdArray] AS TABLE (\n    [Id1] UNIQUEIDENTIFIER NOT NULL,\n    [Id2] UNIQUEIDENTIFIER NOT NULL);\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Functions/CipherDetails.sql",
    "content": "﻿CREATE FUNCTION [dbo].[CipherDetails](@UserId UNIQUEIDENTIFIER)\nRETURNS TABLE\nAS RETURN\nSELECT\n    C.[Id],\n    C.[UserId],\n    C.[OrganizationId],\n    C.[Type],\n    C.[Data],\n    C.[Attachments],\n    C.[CreationDate],\n    C.[RevisionDate],\n    CASE\n        WHEN\n            @UserId IS NULL\n            OR C.[Favorites] IS NULL\n            OR JSON_VALUE(C.[Favorites], CONCAT('$.\"', @UserId, '\"')) IS NULL\n        THEN 0\n        ELSE 1\n    END [Favorite],\n    CASE\n        WHEN\n            @UserId IS NULL\n            OR C.[Folders] IS NULL\n        THEN NULL\n        ELSE TRY_CONVERT(UNIQUEIDENTIFIER, JSON_VALUE(C.[Folders], CONCAT('$.\"', @UserId, '\"')))\n    END [FolderId],\n    C.[DeletedDate],\n    C.[Reprompt],\n    C.[Key],\n    CASE\n        WHEN\n            @UserId IS NULL\n            OR C.[Archives] IS NULL\n        THEN NULL\n        ELSE TRY_CONVERT(DATETIME2(7), JSON_VALUE(C.[Archives], CONCAT('$.\"', @UserId, '\"')))\n    END [ArchivedDate]\nFROM\n    [dbo].[Cipher] C;\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Functions/UserCipherDetails.sql",
    "content": "﻿CREATE FUNCTION [dbo].[UserCipherDetails](@UserId UNIQUEIDENTIFIER)\nRETURNS TABLE\nAS RETURN\nWITH [CTE] AS (\n    SELECT\n        [Id],\n        [OrganizationId]\n    FROM\n        [OrganizationUser]\n    WHERE\n        [UserId] = @UserId\n        AND [Status] = 2 -- Confirmed\n)\nSELECT\n    C.*,\n    CASE\n        WHEN COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0\n        THEN 1\n        ELSE 0\n    END [Edit],\n    CASE\n        WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0\n        THEN 1\n        ELSE 0\n    END [ViewPassword],\n    CASE\n        WHEN COALESCE(CU.[Manage], CG.[Manage], 0) = 1\n        THEN 1\n        ELSE 0\n    END [Manage],\n    CASE\n        WHEN O.[UseTotp] = 1\n        THEN 1\n        ELSE 0\n    END [OrganizationUseTotp]\nFROM\n    [dbo].[CipherDetails](@UserId) C\nINNER JOIN\n    [CTE] OU ON C.[UserId] IS NULL AND C.[OrganizationId] IN (SELECT [OrganizationId] FROM [CTE])\nINNER JOIN\n    [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] AND O.[Id] = C.[OrganizationId] AND O.[Enabled] = 1\nLEFT JOIN\n    [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id]\nLEFT JOIN\n    [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id]\nLEFT JOIN\n    [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\nLEFT JOIN\n    [dbo].[Group] G ON G.[Id] = GU.[GroupId]\nLEFT JOIN\n    [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]\nWHERE\n    CU.[CollectionId] IS NOT NULL\n    OR CG.[CollectionId] IS NOT NULL\n\nUNION ALL\n\nSELECT\n    *,\n    1 [Edit],\n    1 [ViewPassword],\n    1 [Manage],\n    0 [OrganizationUseTotp]\nFROM\n    [dbo].[CipherDetails](@UserId)\nWHERE\n    [UserId] = @UserId;\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CipherDetails_Create]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data NVARCHAR(MAX),\n    @Favorites NVARCHAR(MAX), -- not used\n    @Folders NVARCHAR(MAX), -- not used\n    @Attachments NVARCHAR(MAX), -- not used\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @FolderId UNIQUEIDENTIFIER,\n    @Favorite BIT,\n    @Edit BIT, -- not used\n    @ViewPassword BIT, -- not used\n    @Manage BIT, -- not used\n    @OrganizationUseTotp BIT, -- not used\n    @DeletedDate DATETIME2(7),\n    @Reprompt TINYINT,\n    @Key VARCHAR(MAX) = NULL,\n    @ArchivedDate DATETIME2(7) = NULL,\n    @Archives NVARCHAR(MAX) = NULL -- not used\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserIdKey VARCHAR(50) = CONCAT('\"', @UserId, '\"')\n    DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey)\n\n    INSERT INTO [dbo].[Cipher]\n    (\n        [Id],\n        [UserId],\n        [OrganizationId],\n        [Type],\n        [Data],\n        [Favorites],\n        [Folders],\n        [CreationDate],\n        [RevisionDate],\n        [DeletedDate],\n        [Reprompt],\n        [Key],\n        [Archives]\n    )\n    VALUES\n    (\n        @Id,\n        CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,\n        @OrganizationId,\n        @Type,\n        @Data,\n        CASE WHEN @Favorite = 1 THEN CONCAT('{', @UserIdKey, ':true}') ELSE NULL END,\n        CASE WHEN @FolderId IS NOT NULL THEN CONCAT('{', @UserIdKey, ':\"', @FolderId, '\"', '}') ELSE NULL END,\n        @CreationDate,\n        @RevisionDate,\n        @DeletedDate,\n        @Reprompt,\n        @Key,\n        CASE WHEN @ArchivedDate IS NOT NULL THEN CONCAT('{', @UserIdKey, ':\"', CONVERT(NVARCHAR(30), @ArchivedDate, 127), '\"}') ELSE NULL END\n    )\n\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n    END\n    ELSE IF @UserId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_CreateWithCollections.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CipherDetails_CreateWithCollections]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data NVARCHAR(MAX),\n    @Favorites NVARCHAR(MAX), -- not used\n    @Folders NVARCHAR(MAX), -- not used\n    @Attachments NVARCHAR(MAX), -- not used\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @FolderId UNIQUEIDENTIFIER,\n    @Favorite BIT,\n    @Edit BIT, -- not used\n    @ViewPassword BIT, -- not used\n    @Manage BIT, -- not used\n    @OrganizationUseTotp BIT, -- not used\n    @DeletedDate DATETIME2(7),\n    @Reprompt TINYINT,\n    @Key VARCHAR(MAX) = NULL,\n    @ArchivedDate DATETIME2(7) = NULL,\n    @Archives NVARCHAR(MAX) = NULL, -- not used\n    @CollectionIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,\n        @Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @ViewPassword, @Manage,\n        @OrganizationUseTotp, @DeletedDate, @Reprompt, @Key, @ArchivedDate\n\n    DECLARE @UpdateCollectionsSuccess INT\n    EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds\n\n    -- Bump the account revision date AFTER collections are assigned.\n    IF @UpdateCollectionsSuccess = 0\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CipherDetails_ReadByIdUserId]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\nSELECT\n        [Id],\n        [UserId],\n        [OrganizationId],\n        [Type],\n        [Data],\n        [Attachments],\n        [CreationDate],\n        [RevisionDate],\n        [Favorite],\n        [FolderId],\n        [DeletedDate],\n        [Reprompt],\n        [Key],\n        [OrganizationUseTotp],\n        [ArchivedDate],\n        MAX ([Edit]) AS [Edit],\n        MAX ([ViewPassword]) AS [ViewPassword],\n        MAX ([Manage]) AS [Manage]\n    FROM\n        [dbo].[UserCipherDetails](@UserId)\n    WHERE\n        [Id] = @Id\n    GROUP BY\n        [Id],\n        [UserId],\n        [OrganizationId],\n        [Type],\n        [Data],\n        [Attachments],\n        [CreationDate],\n        [RevisionDate],\n        [Favorite],\n        [FolderId],\n        [DeletedDate],\n        [Reprompt],\n        [Key],\n        [OrganizationUseTotp],\n        [ArchivedDate]\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CipherDetails_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[UserCipherDetails](@UserId)\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadWithoutOrganizationsByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CipherDetails_ReadWithoutOrganizationsByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *,\n        1 [Edit],\n        1 [ViewPassword],\n        1 [Manage],\n        0 [OrganizationUseTotp]\n    FROM\n        [dbo].[CipherDetails](@UserId)\n    WHERE\n        [UserId] = @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CipherDetails_Update]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data NVARCHAR(MAX),\n    @Favorites NVARCHAR(MAX), -- not used\n    @Folders NVARCHAR(MAX), -- not used\n    @Attachments NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @FolderId UNIQUEIDENTIFIER,\n    @Favorite BIT,\n    @Edit BIT, -- not used\n    @ViewPassword BIT, -- not used\n    @Manage BIT, -- not used\n    @OrganizationUseTotp BIT, -- not used\n    @DeletedDate DATETIME2(2),\n    @Reprompt TINYINT,\n    @Key VARCHAR(MAX) = NULL,\n    @ArchivedDate DATETIME2(7) = NULL,\n    @Archives NVARCHAR(MAX) = NULL -- not used\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserIdKey VARCHAR(50) = CONCAT('\"', @UserId, '\"')\n    DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey)\n\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,\n        [OrganizationId] = @OrganizationId,\n        [Type] = @Type,\n        [Data] = @Data,\n        [Folders] =\n            CASE\n            WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN\n                CONCAT('{', @UserIdKey, ':\"', @FolderId, '\"', '}')\n            WHEN @FolderId IS NOT NULL THEN\n                JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50)))\n            ELSE\n                JSON_MODIFY([Folders], @UserIdPath, NULL)\n            END,\n        [Favorites] =\n            CASE\n            WHEN @Favorite = 1 AND [Favorites] IS NULL THEN\n                CONCAT('{', @UserIdKey, ':true}')\n            WHEN @Favorite = 1 THEN\n                JSON_MODIFY([Favorites], @UserIdPath, CAST(1 AS BIT))\n            ELSE\n                JSON_MODIFY([Favorites], @UserIdPath, NULL)\n            END,\n        [Archives] =\n            CASE\n            WHEN @ArchivedDate IS NOT NULL AND [Archives] IS NULL THEN\n                CONCAT('{', @UserIdKey, ':\"', CONVERT(NVARCHAR(30), @ArchivedDate, 127), '\"}')\n            WHEN @ArchivedDate IS NOT NULL THEN\n                JSON_MODIFY([Archives], @UserIdPath, CONVERT(NVARCHAR(30), @ArchivedDate, 127))\n            ELSE\n                JSON_MODIFY([Archives], @UserIdPath, NULL)\n            END,\n        [Attachments] = @Attachments,\n        [Reprompt] = @Reprompt,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [DeletedDate] = @DeletedDate,\n        [Key] = @Key\n    WHERE\n        [Id] = @Id\n\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n    END\n    ELSE IF @UserId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationDetails_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CipherOrganizationDetails_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        C.*,\n        CASE \n            WHEN O.[UseTotp] = 1 THEN 1\n            ELSE 0\n        END [OrganizationUseTotp]\n    FROM\n        [dbo].[CipherView] C\n    LEFT JOIN\n        [dbo].[Organization] O ON O.[Id] = C.[OrganizationId]\n    WHERE\n        C.[Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationDetails_ReadByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[CipherOrganizationDetails_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        C.*,\n        CASE \n            WHEN O.[UseTotp] = 1 THEN 1\n            ELSE 0\n        END [OrganizationUseTotp]\n    FROM\n        [dbo].[CipherView] C\n    LEFT JOIN\n        [dbo].[OrganizationView] O ON O.[Id] = C.[OrganizationId]\n    WHERE\n        C.[OrganizationId] = @OrganizationId \nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationDetails_ReadUnassignedByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[CipherOrganizationDetails_ReadUnassignedByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        C.*,\n        CASE\n            WHEN O.[UseTotp] = 1 THEN 1\n            ELSE 0\n        END [OrganizationUseTotp]\n    FROM\n        [dbo].[CipherView] C\n    LEFT JOIN\n        [dbo].[OrganizationView] O ON O.[Id] = C.[OrganizationId]\n    LEFT JOIN\n        [dbo].[CollectionCipher] CC ON C.[Id] = CC.[CipherId]\n    LEFT JOIN\n        [dbo].[Collection] S ON S.[Id] = CC.[CollectionId]\n        AND S.[OrganizationId] = C.[OrganizationId]\n    WHERE\n        C.[UserId] IS NULL\n        AND C.[OrganizationId] = @OrganizationId\n        AND CC.[CipherId] IS NULL\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationPermissions_GetManyByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[CipherOrganizationPermissions_GetManyByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    ;WITH BaseCiphers AS (\n        SELECT C.[Id], C.[OrganizationId]\n        FROM [dbo].[CipherDetails](@UserId) C\n        INNER JOIN [OrganizationUser] OU ON\n            C.[UserId] IS NULL\n            AND C.[OrganizationId] = @OrganizationId\n            AND OU.[UserId] = @UserId\n        INNER JOIN [dbo].[Organization] O ON\n            O.[Id] = OU.[OrganizationId]\n            AND O.[Id] = C.[OrganizationId]\n            AND O.[Enabled] = 1\n    ),\n    UserPermissions AS (\n        SELECT DISTINCT\n            CC.[CipherId],\n            CASE WHEN CC.[CollectionId] IS NULL THEN 0 ELSE 1 END as [Read],\n            CASE WHEN CU.[HidePasswords] = 0 THEN 1 ELSE 0 END as [ViewPassword],\n            CASE WHEN CU.[ReadOnly] = 0 THEN 1 ELSE 0 END as [Edit],\n            COALESCE(CU.[Manage], 0) as [Manage]\n        FROM [dbo].[CollectionCipher] CC\n        INNER JOIN [dbo].[CollectionUser] CU ON\n            CU.[CollectionId] = CC.[CollectionId]\n            AND CU.[OrganizationUserId] = (\n                SELECT [Id] FROM [OrganizationUser]\n                WHERE [UserId] = @UserId\n                AND [OrganizationId] = @OrganizationId\n            )\n    ),\n    GroupPermissions AS (\n        SELECT DISTINCT\n            CC.[CipherId],\n            CASE WHEN CC.[CollectionId] IS NULL THEN 0 ELSE 1 END as [Read],\n            CASE WHEN CG.[HidePasswords] = 0 THEN 1 ELSE 0 END as [ViewPassword],\n            CASE WHEN CG.[ReadOnly] = 0 THEN 1 ELSE 0 END as [Edit],\n            COALESCE(CG.[Manage], 0) as [Manage]\n        FROM [dbo].[CollectionCipher] CC\n        INNER JOIN [dbo].[CollectionGroup] CG ON\n            CG.[CollectionId] = CC.[CollectionId]\n        INNER JOIN [dbo].[GroupUser] GU ON\n            GU.[GroupId] = CG.[GroupId]\n            AND GU.[OrganizationUserId] = (\n                SELECT [Id] FROM [OrganizationUser]\n                WHERE [UserId] = @UserId\n                AND [OrganizationId] = @OrganizationId\n            )\n        WHERE NOT EXISTS (\n            SELECT 1\n            FROM UserPermissions UP\n            WHERE UP.[CipherId] = CC.[CipherId]\n        )\n    ),\n    CombinedPermissions AS (\n        SELECT CipherId, [Read], ViewPassword, Edit, Manage\n        FROM UserPermissions\n        UNION ALL\n        SELECT CipherId, [Read], ViewPassword, Edit, Manage\n        FROM GroupPermissions\n    )\n    SELECT\n        C.[Id],\n        C.[OrganizationId],\n        ISNULL(MAX(P.[Read]), 0) as [Read],\n        ISNULL(MAX(P.[ViewPassword]), 0) as [ViewPassword],\n        ISNULL(MAX(P.[Edit]), 0) as [Edit],\n        ISNULL(MAX(P.[Manage]), 0) as [Manage]\n    FROM BaseCiphers C\n    LEFT JOIN CombinedPermissions P ON P.CipherId = C.[Id]\n    GROUP BY C.[Id], C.[OrganizationId]\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_Archive]\n    @Ids AS [dbo].[GuidIdArray] READONLY,\n    @UserId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    CREATE TABLE #Temp\n    (\n        [Id] UNIQUEIDENTIFIER NOT NULL,\n        [UserId] UNIQUEIDENTIFIER NULL\n    )\n\n    INSERT INTO #Temp\n    SELECT\n        ucd.[Id],\n        ucd.[UserId]\n    FROM\n        [dbo].[UserCipherDetails](@UserId) ucd\n        INNER JOIN @Ids ids ON ids.Id = ucd.[Id]\n    WHERE\n        ucd.[ArchivedDate] IS NULL\n\n    DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME();\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [Archives] = JSON_MODIFY(\n            COALESCE([Archives], N'{}'),\n            CONCAT('$.\"', @UserId, '\"'),\n            CONVERT(NVARCHAR(30), @UtcNow, 127)\n        ),\n        [RevisionDate] = @UtcNow\n    FROM [dbo].[Cipher] AS c\n    INNER JOIN #Temp AS t\n        ON t.[Id] = c.[Id];\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n\n    DROP TABLE #Temp\n\n    SELECT @UtcNow\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data NVARCHAR(MAX),\n    @Favorites NVARCHAR(MAX),\n    @Folders NVARCHAR(MAX),\n    @Attachments NVARCHAR(MAX), -- not used\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @DeletedDate DATETIME2(7),\n    @Reprompt TINYINT,\n    @Key VARCHAR(MAX) = NULL,\n    @Archives NVARCHAR(MAX) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Cipher]\n    (\n        [Id],\n        [UserId],\n        [OrganizationId],\n        [Type],\n        [Data],\n        [Favorites],\n        [Folders],\n        [CreationDate],\n        [RevisionDate],\n        [DeletedDate],\n        [Reprompt],\n        [Key],\n        [Archives]\n    )\n    VALUES\n    (\n        @Id,\n        CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,\n        @OrganizationId,\n        @Type,\n        @Data,\n        @Favorites,\n        @Folders,\n        @CreationDate,\n        @RevisionDate,\n        @DeletedDate,\n        @Reprompt,\n        @Key,\n        @Archives\n    )\n\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n    END\n    ELSE IF @UserId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_CreateWithCollections.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_CreateWithCollections]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data NVARCHAR(MAX),\n    @Favorites NVARCHAR(MAX),\n    @Folders NVARCHAR(MAX),\n    @Attachments NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @DeletedDate DATETIME2(7),\n    @Reprompt TINYINT,\n    @Key VARCHAR(MAX) = NULL,\n    @Archives NVARCHAR(MAX) = NULL,\n    @CollectionIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    EXEC [dbo].[Cipher_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,\n        @Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt, @Key, @Archives\n\n    DECLARE @UpdateCollectionsSuccess INT\n    EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds\n\n    -- Bump the account revision date AFTER collections are assigned.\n    IF @UpdateCollectionsSuccess = 0\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Delete.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_Delete]\n    @Ids AS [dbo].[GuidIdArray] READONLY,\n    @UserId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    CREATE TABLE #Temp\n    ( \n        [Id] UNIQUEIDENTIFIER NOT NULL,\n        [UserId] UNIQUEIDENTIFIER NULL,\n        [OrganizationId] UNIQUEIDENTIFIER NULL,\n        [Attachments] BIT NOT NULL\n    )\n\n    INSERT INTO #Temp\n    SELECT\n        [Id],\n        [UserId],\n        [OrganizationId],\n        CASE WHEN [Attachments] IS NULL THEN 0 ELSE 1 END\n    FROM\n        [dbo].[UserCipherDetails](@UserId)\n    WHERE\n        [Edit] = 1\n        AND [Id] IN (SELECT * FROM @Ids)\n\n    -- Delete ciphers\n    DELETE\n    FROM\n        [dbo].[Cipher]\n    WHERE\n        [Id] IN (SELECT [Id] FROM #Temp)\n\n    -- Cleanup orgs\n    DECLARE @OrgId UNIQUEIDENTIFIER\n    DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR\n        SELECT\n            [OrganizationId]\n        FROM\n            #Temp\n        WHERE\n            [OrganizationId] IS NOT NULL\n        GROUP BY\n            [OrganizationId]\n    OPEN [OrgCursor]\n    FETCH NEXT FROM [OrgCursor] INTO @OrgId\n    WHILE @@FETCH_STATUS = 0 BEGIN\n        EXEC [dbo].[Organization_UpdateStorage] @OrgId\n        EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId\n        FETCH NEXT FROM [OrgCursor] INTO @OrgId\n    END\n    CLOSE [OrgCursor]\n    DEALLOCATE [OrgCursor]\n\n    -- Cleanup user\n    DECLARE @UserCiphersWithStorageCount INT\n    SELECT\n        @UserCiphersWithStorageCount = COUNT(1)\n    FROM\n        #Temp\n    WHERE\n        [UserId] IS NOT NULL\n        AND [Attachments] = 1\n\n    IF @UserCiphersWithStorageCount > 0\n    BEGIN\n        EXEC [dbo].[User_UpdateStorage] @UserId\n    END\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n\n    DROP TABLE #Temp\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_DeleteAttachment]\n    @Id UNIQUEIDENTIFIER,\n    @AttachmentId VARCHAR(50)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('\"', @AttachmentId, '\"')\n    DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey)\n\n    DECLARE @UserId UNIQUEIDENTIFIER\n    DECLARE @OrganizationId UNIQUEIDENTIFIER\n    DECLARE @CurrentAttachments NVARCHAR(MAX)\n    DECLARE @NewAttachments NVARCHAR(MAX)\n\n    -- Get current cipher data\n    SELECT\n        @UserId = [UserId],\n        @OrganizationId = [OrganizationId],\n        @CurrentAttachments = [Attachments]\n    FROM\n        [dbo].[Cipher]\n    WHERE [Id] = @Id\n\n    -- If there are no attachments, nothing to do\n    IF @CurrentAttachments IS NULL\n    BEGIN\n        RETURN;\n    END\n\n    -- Validate the initial JSON\n    IF ISJSON(@CurrentAttachments) = 0\n    BEGIN\n        THROW 50000, 'Current initial attachments data is not valid JSON', 1;\n        RETURN;\n    END\n\n    -- Check if the attachment exists before trying to remove it\n    IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL\n    BEGIN\n        -- Attachment doesn't exist, nothing to do\n        RETURN;\n    END\n\n    -- Create the new attachments JSON with the specified attachment removed\n    SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL)\n\n    -- Validate the resulting JSON\n    IF ISJSON(@NewAttachments) = 0\n    BEGIN\n        THROW 50000, 'Failed to create valid JSON when removing attachment', 1;\n        RETURN;\n    END\n\n    -- Check if we've removed all attachments and have an empty object\n    IF @NewAttachments = '{}'\n    BEGIN\n        -- If we have an empty JSON object, set to NULL instead\n        SET @NewAttachments = NULL;\n    END\n\n    -- Update with validated JSON\n    UPDATE [dbo].[Cipher]\n    SET [Attachments] = @NewAttachments\n    WHERE [Id] = @Id\n\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[Organization_UpdateStorage] @OrganizationId\n        EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n    END\n    ELSE IF @UserId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_UpdateStorage] @UserId\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_DeleteById]\n    @Id UNIQUEIDENTIFIER\nWITH RECOMPILE\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserId UNIQUEIDENTIFIER\n    DECLARE @OrganizationId UNIQUEIDENTIFIER\n    DECLARE @Attachments BIT\n\n    SELECT TOP 1\n        @UserId = [UserId],\n        @OrganizationId = [OrganizationId],\n        @Attachments = CASE WHEN [Attachments] IS NOT NULL THEN 1 ELSE 0 END\n    FROM\n        [dbo].[Cipher]\n    WHERE\n        [Id] = @Id\n\n    DELETE\n    FROM\n        [dbo].[Cipher]\n    WHERE\n        [Id] = @Id\n\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        IF @Attachments = 1\n        BEGIN\n            EXEC [dbo].[Organization_UpdateStorage] @OrganizationId\n        END\n        EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n    END\n    ELSE IF @UserId IS NOT NULL\n    BEGIN\n        IF @Attachments = 1\n        BEGIN\n            EXEC [dbo].[User_UpdateStorage] @UserId\n        END\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByIdsOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[Cipher_DeleteByIdsOrganizationId]\n    @Ids AS [dbo].[GuidIdArray] READONLY,\n    @OrganizationId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    -- Delete ciphers\n    DELETE\n    FROM\n        [dbo].[Cipher]\n    WHERE\n        [Id] IN (SELECT * FROM @Ids)\n        AND OrganizationId = @OrganizationId\n\n    -- Cleanup organization\n    EXEC [dbo].[Organization_UpdateStorage] @OrganizationId\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_DeleteByOrganizationId]\n     @OrganizationId UNIQUEIDENTIFIER\n AS\n BEGIN\n     SET NOCOUNT ON;\n\n     DECLARE @BatchSize INT = 1000;\n\n     BEGIN TRY\n         BEGIN TRANSACTION;\n\n         ---------------------------------------------------------------------\n         -- 1. Delete organization ciphers that are NOT in any default\n         --    user collection (Collection.Type = 1).\n         ---------------------------------------------------------------------\n         WHILE 1 = 1\n         BEGIN\n             ;WITH Target AS\n             (\n                 SELECT TOP (@BatchSize) C.Id\n                 FROM dbo.Cipher C\n                 WHERE C.OrganizationId = @OrganizationId\n                   AND NOT EXISTS (\n                       SELECT 1\n                       FROM dbo.CollectionCipher CC2\n                       INNER JOIN dbo.Collection Col2\n                         ON Col2.Id = CC2.CollectionId\n                         AND Col2.Type = 1  -- Default user collection\n                       WHERE CC2.CipherId = C.Id\n                   )\n                 ORDER BY C.Id  -- Deterministic ordering (matches clustered index)\n             )\n             DELETE C\n             FROM dbo.Cipher C\n             INNER JOIN Target T ON T.Id = C.Id;\n\n             IF @@ROWCOUNT = 0 BREAK;\n         END\n\n         ---------------------------------------------------------------------\n         -- 2. Remove remaining CollectionCipher rows that reference\n         --    non-default (Type = 0 / shared) collections, for ciphers\n         --    that were preserved because they belong to at least one\n         --    default (Type = 1) collection.\n         ---------------------------------------------------------------------\n         SET @BatchSize = 1000;\n         WHILE 1 = 1\n         BEGIN\n             ;WITH ToDelete AS\n             (\n                 SELECT TOP (@BatchSize)\n                        CC.CipherId,\n                        CC.CollectionId\n                 FROM dbo.CollectionCipher CC\n                 INNER JOIN dbo.Collection Col\n                         ON Col.Id = CC.CollectionId\n                        AND Col.Type = 0  -- Non-default collections\n                 INNER JOIN dbo.Cipher C\n                         ON C.Id = CC.CipherId\n                 WHERE C.OrganizationId = @OrganizationId\n                 ORDER BY CC.CollectionId, CC.CipherId  -- Matches clustered index\n             )\n             DELETE CC\n             FROM dbo.CollectionCipher CC\n             INNER JOIN ToDelete TD\n                 ON CC.CipherId = TD.CipherId\n                AND CC.CollectionId = TD.CollectionId;\n\n             IF @@ROWCOUNT = 0 BREAK;\n         END\n\n         ---------------------------------------------------------------------\n         -- 3. Bump revision date (inside transaction for consistency)\n         ---------------------------------------------------------------------\n         EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId;\n\n         COMMIT TRANSACTION ;\n\n         ---------------------------------------------------------------------\n         -- 4. Update storage usage (outside the transaction to avoid\n         --    holding locks during long-running calculation)\n         ---------------------------------------------------------------------\n         EXEC [dbo].[Organization_UpdateStorage] @OrganizationId;\n     END TRY\n     BEGIN CATCH\n         IF @@TRANCOUNT > 0\n             ROLLBACK TRANSACTION;\n         THROW;\n     END CATCH\n END\n GO\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_DeleteByUserId]\n    @UserId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @BatchSize INT = 100\n\n    -- Delete ciphers\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION Cipher_DeleteByUserId_Ciphers\n\n        DELETE TOP(@BatchSize)\n        FROM\n            [dbo].[Cipher]\n        WHERE\n            [UserId] = @UserId\n\n        SET @BatchSize = @@ROWCOUNT\n\n        COMMIT TRANSACTION Cipher_DeleteByUserId_Ciphers\n    END\n\n    -- Delete folders\n    DELETE\n    FROM\n        [dbo].[Folder]\n    WHERE\n        [UserId] = @UserId\n\n    -- Cleanup user\n    EXEC [dbo].[User_UpdateStorage] @UserId\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteDeleted.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_DeleteDeleted]\n    @DeletedDateBefore DATETIME2 (7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @BatchSize INT = 100\n\n    WHILE @BatchSize > 0\n    BEGIN\n        DELETE TOP(@BatchSize)\n        FROM\n            [dbo].[Cipher]\n        WHERE\n            [DeletedDate] < @DeletedDateBefore\n\n        SET @BatchSize = @@ROWCOUNT\n    END\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Move.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_Move]\n    @Ids AS [dbo].[GuidIdArray] READONLY,\n    @FolderId AS UNIQUEIDENTIFIER,\n    @UserId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserIdKey VARCHAR(50) = CONCAT('\"', @UserId, '\"')\n    DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey)\n\n    ;WITH [IdsToMoveCTE] AS (\n        SELECT\n            [Id]\n        FROM\n            [dbo].[UserCipherDetails](@UserId)\n        WHERE\n            [Id] IN (SELECT * FROM @Ids)\n    )\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [Folders] = \n            CASE\n            WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN\n                CONCAT('{', @UserIdKey, ':\"', @FolderId, '\"', '}')\n            WHEN @FolderId IS NOT NULL THEN\n                JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50)))\n            ELSE\n                JSON_MODIFY([Folders], @UserIdPath, NULL)\n            END\n    WHERE\n        [Id] IN (SELECT * FROM [IdsToMoveCTE])\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[CipherView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_ReadByOrganizationId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_ReadByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[CipherView]\n    WHERE\n        [UserId] IS NULL\n        AND [OrganizationId] = @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_ReadCanEditByIdUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_ReadCanEditByIdUserId]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @CanEdit BIT\n\n    ;WITH [CTE] AS (\n        SELECT\n            CASE\n                WHEN C.[UserId] IS NOT NULL OR CU.[ReadOnly] = 0 OR CG.[ReadOnly] = 0 THEN 1\n                ELSE 0\n            END [Edit]\n        FROM\n            [dbo].[Cipher] C\n        LEFT JOIN\n            [dbo].[Organization] O ON C.[UserId] IS NULL AND O.[Id] = C.[OrganizationId]\n        LEFT JOIN\n            [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId\n        LEFT JOIN\n            [dbo].[CollectionCipher] CC ON C.[UserId] IS NULL AND CC.[CipherId] = C.[Id]\n        LEFT JOIN\n            [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id]\n        LEFT JOIN\n            [dbo].[GroupUser] GU ON C.[UserId] IS NULL AND CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n        LEFT JOIN\n            [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n        LEFT JOIN\n            [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]\n        WHERE\n            C.Id = @Id\n            AND (\n                C.[UserId] = @UserId\n                OR (\n                    C.[UserId] IS NULL\n                    AND OU.[Status] = 2 -- 2 = Confirmed\n                    AND O.[Enabled] = 1\n                    AND (\n                        CU.[CollectionId] IS NOT NULL\n                        OR CG.[CollectionId] IS NOT NULL\n                    )\n                )\n            )\n    )\n    SELECT\n        @CanEdit = CASE\n            WHEN COUNT(1) > 0 THEN 1\n            ELSE 0\n        END\n    FROM\n        [CTE]\n    WHERE\n        [Edit] = 1\n\n    SELECT @CanEdit\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Restore.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_Restore]\n    @Ids AS [dbo].[GuidIdArray] READONLY,\n    @UserId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    CREATE TABLE #Temp\n    ( \n        [Id] UNIQUEIDENTIFIER NOT NULL,\n        [UserId] UNIQUEIDENTIFIER NULL,\n        [OrganizationId] UNIQUEIDENTIFIER NULL\n    )\n\n    INSERT INTO #Temp\n    SELECT\n        [Id],\n        [UserId],\n        [OrganizationId]\n    FROM\n        [dbo].[UserCipherDetails](@UserId)\n    WHERE\n        [Edit] = 1\n        AND [DeletedDate] IS NOT NULL\n        AND [Id] IN (SELECT * FROM @Ids)\n\n    DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [DeletedDate] = NULL,\n        [RevisionDate] = @UtcNow\n    WHERE\n        [Id] IN (SELECT [Id] FROM #Temp)\n\n    -- Bump orgs\n    DECLARE @OrgId UNIQUEIDENTIFIER\n    DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR\n        SELECT\n            [OrganizationId]\n        FROM\n            #Temp\n        WHERE\n            [OrganizationId] IS NOT NULL\n        GROUP BY\n            [OrganizationId]\n    OPEN [OrgCursor]\n    FETCH NEXT FROM [OrgCursor] INTO @OrgId\n    WHILE @@FETCH_STATUS = 0 BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId\n        FETCH NEXT FROM [OrgCursor] INTO @OrgId\n    END\n    CLOSE [OrgCursor]\n    DEALLOCATE [OrgCursor]\n\n    -- Bump user\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n\n    DROP TABLE #Temp\n\n    SELECT @UtcNow\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_RestoreByIdsOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[Cipher_RestoreByIdsOrganizationId]\n    @Ids AS [dbo].[GuidIdArray] READONLY,\n    @OrganizationId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n        \n    IF (SELECT COUNT(1) FROM @Ids) < 1\n        BEGIN\n            RETURN(-1)\n        END\n\n    -- Delete ciphers\n    DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [DeletedDate] = NULL,\n        [RevisionDate] = @UtcNow\n    WHERE\n        [Id] IN (SELECT * FROM @Ids)\n        AND OrganizationId = @OrganizationId\n\n    -- Cleanup organization\n    EXEC [dbo].[Organization_UpdateStorage] @OrganizationId\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\n\n    SELECT @UtcNow\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_SoftDelete.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_SoftDelete]\n    @Ids AS [dbo].[GuidIdArray] READONLY,\n    @UserId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    CREATE TABLE #Temp\n    (\n        [Id] UNIQUEIDENTIFIER NOT NULL,\n        [UserId] UNIQUEIDENTIFIER NULL,\n        [OrganizationId] UNIQUEIDENTIFIER NULL\n    )\n\n    INSERT INTO #Temp\n    SELECT\n        [Id],\n        [UserId],\n        [OrganizationId]\n    FROM\n        [dbo].[UserCipherDetails](@UserId)\n    WHERE\n        [Edit] = 1\n        AND [DeletedDate] IS NULL\n        AND [Id] IN (SELECT * FROM @Ids)\n\n    -- Delete ciphers\n    DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [DeletedDate] = @UtcNow,\n        [RevisionDate] = @UtcNow\n    WHERE\n        [Id] IN (SELECT [Id] FROM #Temp)\n\n    -- Cleanup orgs\n    DECLARE @OrgId UNIQUEIDENTIFIER\n    DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR\n        SELECT\n            [OrganizationId]\n        FROM\n            #Temp\n        WHERE\n            [OrganizationId] IS NOT NULL\n        GROUP BY\n            [OrganizationId]\n    OPEN [OrgCursor]\n    FETCH NEXT FROM [OrgCursor] INTO @OrgId\n    WHILE @@FETCH_STATUS = 0 BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId\n        FETCH NEXT FROM [OrgCursor] INTO @OrgId\n    END\n    CLOSE [OrgCursor]\n    DEALLOCATE [OrgCursor]\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n\n    DROP TABLE #Temp\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_SoftDeleteByIdsOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[Cipher_SoftDeleteByIdsOrganizationId]\n    @Ids AS [dbo].[GuidIdArray] READONLY,\n    @OrganizationId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    -- Delete ciphers\n    DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [DeletedDate] = @UtcNow,\n        [RevisionDate] = @UtcNow\n    WHERE\n        [Id] IN (SELECT * FROM @Ids)\n        AND OrganizationId = @OrganizationId\n\n    -- Cleanup organization\n    EXEC [dbo].[Organization_UpdateStorage] @OrganizationId\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Unarchive.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_Unarchive]\n    @Ids AS [dbo].[GuidIdArray] READONLY,\n    @UserId AS UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    CREATE TABLE #Temp\n    (\n        [Id] UNIQUEIDENTIFIER NOT NULL,\n        [UserId] UNIQUEIDENTIFIER NULL\n    )\n\n    INSERT INTO #Temp\n    SELECT\n        ucd.[Id],\n        ucd.[UserId]\n    FROM\n        [dbo].[UserCipherDetails](@UserId) ucd\n        INNER JOIN @Ids ids ON ids.Id = ucd.[Id]\n    WHERE\n        ucd.[ArchivedDate] IS NOT NULL\n\n    DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME();\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [Archives] = JSON_MODIFY(\n            COALESCE([Archives], N'{}'),\n            CONCAT('$.\"', @UserId, '\"'),\n            NULL\n        ),\n        [RevisionDate] = @UtcNow\n    FROM [dbo].[Cipher] AS c\n    INNER JOIN #Temp AS t\n        ON t.[Id] = c.[Id];\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n\n    DROP TABLE #Temp\n\n    SELECT @UtcNow\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_Update]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data NVARCHAR(MAX),\n    @Favorites NVARCHAR(MAX),\n    @Folders NVARCHAR(MAX),\n    @Attachments NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @DeletedDate DATETIME2(7),\n    @Reprompt TINYINT,\n    @Key VARCHAR(MAX) = NULL,\n    @Archives NVARCHAR(MAX) = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,\n        [OrganizationId] = @OrganizationId,\n        [Type] = @Type,\n        [Data] = @Data,\n        [Favorites] = @Favorites,\n        [Folders] = @Folders,\n        [Attachments] = @Attachments,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate,\n        [DeletedDate] = @DeletedDate,\n        [Reprompt] = @Reprompt,\n        [Key] = @Key,\n        [Archives] = @Archives\n    WHERE\n        [Id] = @Id\n\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n    END\n    ELSE IF @UserId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_UpdateAttachment]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @AttachmentId VARCHAR(50),\n    @AttachmentData NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    -- Validate that AttachmentData is valid JSON\n    IF ISJSON(@AttachmentData) = 0\n    BEGIN\n        THROW 50000, 'Invalid JSON format in AttachmentData parameter', 1;\n        RETURN;\n    END\n\n    -- Validate that AttachmentData has the expected structure\n    -- Check for required fields\n    IF JSON_VALUE(@AttachmentData, '$.FileName') IS NULL OR\n       JSON_VALUE(@AttachmentData, '$.Size') IS NULL\n    BEGIN\n        THROW 50000, 'AttachmentData is missing required fields (FileName, Size)', 1;\n        RETURN;\n    END\n\n    -- Validate data types for critical fields\n    DECLARE @Size BIGINT = TRY_CAST(JSON_VALUE(@AttachmentData, '$.Size') AS BIGINT)\n    IF @Size IS NULL OR @Size <= 0\n    BEGIN\n        THROW 50000, 'AttachmentData has invalid Size value', 1;\n        RETURN;\n    END\n\n    DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('\"', @AttachmentId, '\"')\n    DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey)\n    DECLARE @NewAttachments NVARCHAR(MAX)\n\n    -- Get current attachments\n    DECLARE @CurrentAttachments NVARCHAR(MAX)\n    SELECT @CurrentAttachments = [Attachments] FROM [dbo].[Cipher] WHERE [Id] = @Id\n\n    -- Prepare the new attachments value based on current state\n    IF @CurrentAttachments IS NULL\n    BEGIN\n        -- Create new JSON object with the attachment\n        SET @NewAttachments = CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}')\n\n        -- Validate the constructed JSON\n        IF ISJSON(@NewAttachments) = 0\n        BEGIN\n            THROW 50000, 'Failed to create valid JSON when adding new attachment', 1;\n            RETURN;\n        END\n    END\n    ELSE\n    BEGIN\n        -- Validate existing attachments\n        IF ISJSON(@CurrentAttachments) = 0\n        BEGIN\n            THROW 50000, 'Current attachments data is not valid JSON', 1;\n            RETURN;\n        END\n\n        -- Modify existing JSON\n        SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$'))\n\n        -- Validate the modified JSON\n        IF ISJSON(@NewAttachments) = 0\n        BEGIN\n            THROW 50000, 'Failed to create valid JSON when updating existing attachments', 1;\n            RETURN;\n        END\n    END\n\n    -- Update with validated JSON\n    UPDATE [dbo].[Cipher]\n    SET [Attachments] = @NewAttachments\n    WHERE [Id] = @Id\n\n    IF @OrganizationId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[Organization_UpdateStorage] @OrganizationId\n        EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n    END\n    ELSE IF @UserId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_UpdateStorage] @UserId\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateCollections.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_UpdateCollections]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @CollectionIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    IF @OrganizationId IS NULL OR (SELECT COUNT(1) FROM @CollectionIds) < 1\n    BEGIN\n        RETURN(-1)\n    END\n\n    CREATE TABLE #AvailableCollections (\n        [Id] UNIQUEIDENTIFIER\n    )\n\n    IF @UserId IS NULL\n    BEGIN\n        INSERT INTO #AvailableCollections\n            SELECT\n                [Id]\n            FROM\n                [dbo].[Collection]\n            WHERE\n                [OrganizationId] = @OrganizationId\n    END\n    ELSE\n    BEGIN\n        INSERT INTO #AvailableCollections\n            SELECT\n                C.[Id]\n            FROM\n                [dbo].[Collection] C\n            INNER JOIN\n                [Organization] O ON O.[Id] = C.[OrganizationId]\n            INNER JOIN\n                [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId\n            LEFT JOIN\n                [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = OU.[Id]\n            LEFT JOIN\n                [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n            LEFT JOIN\n                [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n            LEFT JOIN\n                [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]\n            WHERE\n                O.[Id] = @OrganizationId\n                AND O.[Enabled] = 1\n                AND OU.[Status] = 2 -- Confirmed\n                AND (\n                    CU.[ReadOnly] = 0\n                    OR CG.[ReadOnly] = 0\n                )\n    END\n\n    IF (SELECT COUNT(1) FROM #AvailableCollections) < 1\n    BEGIN\n        -- No writable collections available to share with in this organization.\n        RETURN(-1)\n    END\n\n    INSERT INTO [dbo].[CollectionCipher]\n    (\n        [CollectionId],\n        [CipherId]\n    )\n    SELECT\n        [Id],\n        @Id\n    FROM\n        @CollectionIds\n    WHERE\n        [Id] IN (SELECT [Id] FROM #AvailableCollections)\n\n    RETURN(0)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdatePartial.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_UpdatePartial]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @FolderId UNIQUEIDENTIFIER,\n    @Favorite BIT\nAS\nBEGIN\n    SET NOCOUNT ON\n    \n    DECLARE @UserIdKey VARCHAR(50) = CONCAT('\"', @UserId, '\"')\n    DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey)\n\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [Folders] = \n            CASE\n            WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN\n                CONCAT('{', @UserIdKey, ':\"', @FolderId, '\"', '}')\n            WHEN @FolderId IS NOT NULL THEN\n                JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50)))\n            ELSE\n                JSON_MODIFY([Folders], @UserIdPath, NULL)\n            END,\n        [Favorites] =\n            CASE\n            WHEN @Favorite = 1 AND [Favorites] IS NULL THEN\n                CONCAT('{', @UserIdKey, ':true}')\n            WHEN @Favorite = 1 THEN\n                JSON_MODIFY([Favorites], @UserIdPath, CAST(1 AS BIT))\n            ELSE\n                JSON_MODIFY([Favorites], @UserIdPath, NULL)\n            END\n    WHERE\n        [Id] = @Id\n\n    IF @UserId IS NOT NULL\n    BEGIN\n        EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\n    END\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateWithCollections.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Cipher_UpdateWithCollections]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Type TINYINT,\n    @Data NVARCHAR(MAX),\n    @Favorites NVARCHAR(MAX),\n    @Folders NVARCHAR(MAX),\n    @Attachments NVARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7),\n    @DeletedDate DATETIME2(7),\n    @Reprompt TINYINT,\n    @Key VARCHAR(MAX) = NULL,\n    @Archives NVARCHAR(MAX) = NULL,\n    @CollectionIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    BEGIN TRANSACTION Cipher_UpdateWithCollections\n\n    DECLARE @UpdateCollectionsSuccess INT\n    EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds\n\n    IF @UpdateCollectionsSuccess < 0\n    BEGIN\n        COMMIT TRANSACTION Cipher_UpdateWithCollections\n        SELECT -1 -- -1 = Failure\n        RETURN\n    END\n\n    UPDATE\n        [dbo].[Cipher]\n    SET\n        [UserId] = NULL,\n        [OrganizationId] = @OrganizationId,\n        [Data] = @Data,\n        [Attachments] = @Attachments,\n        [RevisionDate] = @RevisionDate,\n        [DeletedDate] = @DeletedDate,\n        [Key] = @Key,\n        [Folders] = @Folders,\n        [Favorites] = @Favorites,\n        [Reprompt] = @Reprompt,\n        [Archives] = @Archives\n        -- No need to update CreationDate or Type since that data will not change\n    WHERE\n        [Id] = @Id\n\n    COMMIT TRANSACTION Cipher_UpdateWithCollections\n\n    IF @Attachments IS NOT NULL\n    BEGIN\n        EXEC [dbo].[Organization_UpdateStorage] @OrganizationId\n        EXEC [dbo].[User_UpdateStorage] @UserId\n    END\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId\n\n    SELECT 0 -- 0 = Success\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Cipher/UserSecurityTasks_GetManyByCipherIds.sql",
    "content": "CREATE PROCEDURE [dbo].[UserSecurityTasks_GetManyByCipherIds]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @CipherIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    ;WITH BaseCiphers AS (\n        SELECT C.[Id], C.[OrganizationId]\n        FROM [dbo].[Cipher] C\n        INNER JOIN @CipherIds CI ON C.[Id] = CI.[Id]\n        INNER JOIN [dbo].[Organization] O ON\n            O.[Id] = C.[OrganizationId]\n            AND O.[Id] = @OrganizationId\n            AND O.[Enabled] = 1\n    ),\n    UserPermissions AS (\n        SELECT DISTINCT\n            CC.[CipherId],\n            OU.[UserId],\n            COALESCE(CU.[Manage], 0) as [Manage]\n        FROM [dbo].[CollectionCipher] CC\n        INNER JOIN [dbo].[CollectionUser] CU ON\n            CU.[CollectionId] = CC.[CollectionId]\n        INNER JOIN [dbo].[OrganizationUser] OU ON\n            CU.[OrganizationUserId] = OU.[Id]\n            AND OU.[OrganizationId] = @OrganizationId\n        WHERE COALESCE(CU.[Manage], 0) = 1\n    ),\n    GroupPermissions AS (\n        SELECT DISTINCT\n            CC.[CipherId],\n            OU.[UserId],\n            COALESCE(CG.[Manage], 0) as [Manage]\n        FROM [dbo].[CollectionCipher] CC\n        INNER JOIN [dbo].[CollectionGroup] CG ON\n            CG.[CollectionId] = CC.[CollectionId]\n        INNER JOIN [dbo].[GroupUser] GU ON\n            GU.[GroupId] = CG.[GroupId]\n        INNER JOIN [dbo].[OrganizationUser] OU ON\n            GU.[OrganizationUserId] = OU.[Id]\n            AND OU.[OrganizationId] = @OrganizationId\n        WHERE COALESCE(CG.[Manage], 0) = 1\n            AND NOT EXISTS (\n                SELECT 1\n                FROM UserPermissions UP\n                WHERE UP.[CipherId] = CC.[CipherId]\n                AND UP.[UserId] = OU.[UserId]\n            )\n    ),\n    CombinedPermissions AS (\n        SELECT CipherId, UserId, [Manage]\n        FROM UserPermissions\n        UNION\n        SELECT CipherId, UserId, [Manage]\n        FROM GroupPermissions\n    )\n    SELECT\n        P.[UserId],\n        U.[Email],\n        C.[Id] as CipherId\n    FROM BaseCiphers C\n    INNER JOIN CombinedPermissions P ON P.CipherId = C.[Id]\n    INNER JOIN [dbo].[User] U ON U.[Id] = P.[UserId]\n    WHERE P.[Manage] = 1\n    ORDER BY U.[Email], C.[Id]\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/CollectionCipher/CollectionCipher_AddCollectionsForManyCiphers.sql",
    "content": "CREATE PROCEDURE [dbo].[CollectionCipher_AddCollectionsForManyCiphers]\n    @CipherIds AS [dbo].[GuidIdArray] READONLY,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @CollectionIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    CREATE TABLE #AvailableCollections (\n        [Id] UNIQUEIDENTIFIER\n    )\n\n    INSERT INTO #AvailableCollections\n        SELECT\n            C.[Id]\n        FROM\n            [dbo].[Collection] C\n        INNER JOIN\n            [dbo].[Organization] O ON O.[Id] = C.[OrganizationId]\n        WHERE\n            O.[Id] = @OrganizationId AND O.[Enabled] = 1\n\n    IF (SELECT COUNT(1) FROM #AvailableCollections) < 1\n    BEGIN\n        -- No collections available\n        RETURN\n    END\n\n    ;WITH [SourceCollectionCipherCTE] AS(\n        SELECT\n            [Collection].[Id] AS [CollectionId],\n            [Cipher].[Id] AS [CipherId]\n        FROM\n            @CollectionIds AS [Collection]\n        CROSS JOIN\n            @CipherIds AS [Cipher]\n        WHERE\n            [Collection].[Id] IN (SELECT [Id] FROM #AvailableCollections)\n    )\n    MERGE\n        [CollectionCipher] AS [Target]\n    USING\n        [SourceCollectionCipherCTE] AS [Source]\n    ON\n        [Target].[CollectionId] = [Source].[CollectionId]\n        AND [Target].[CipherId] = [Source].[CipherId]\n    WHEN NOT MATCHED BY TARGET THEN\n        INSERT VALUES\n        (\n            [Source].[CollectionId],\n            [Source].[CipherId]\n        )\n   ;\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/CollectionCipher/CollectionCipher_RemoveCollectionsFromManyCiphers.sql",
    "content": "CREATE PROCEDURE [dbo].[CollectionCipher_RemoveCollectionsForManyCiphers]\n    @CipherIds AS [dbo].[GuidIdArray] READONLY,\n    @OrganizationId UNIQUEIDENTIFIER,\n    @CollectionIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @BatchSize INT = 100\n\n    WHILE @BatchSize > 0\n    BEGIN\n        BEGIN TRANSACTION CollectionCipher_DeleteMany\n            DELETE TOP(@BatchSize)\n            FROM\n                [dbo].[CollectionCipher]\n            WHERE\n                [CipherId] IN (SELECT [Id] FROM @CipherIds) AND\n                [CollectionId] IN (SELECT [Id] FROM @CollectionIds)\n\n            SET @BatchSize = @@ROWCOUNT\n       COMMIT TRANSACTION CollectionCipher_DeleteMany\n    END\n\n    EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql",
    "content": "CREATE PROCEDURE [dbo].[Collection_ReadByIdWithPermissions]\n    @CollectionId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @IncludeAccessRelationships BIT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n\tSELECT\n\t    C.*,\n\t    MIN(CASE\n\t        WHEN\n\t            COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0\n\t        THEN 0\n\t        ELSE 1\n\t    END) AS [ReadOnly],\n\t    MIN (CASE\n\t        WHEN\n\t            COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0\n\t        THEN 0\n\t        ELSE 1\n\t    END) AS [HidePasswords],\n\t    MAX(CASE\n\t        WHEN\n\t            COALESCE(CU.[Manage], CG.[Manage], 0) = 0\n\t        THEN 0\n\t        ELSE 1\n\t    END) AS [Manage],\n\t    MAX(CASE\n\t    \tWHEN\n\t    \t    CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL\n\t    \tTHEN 0\n\t    \tELSE 1\n\t    END) AS [Assigned],\n\t    CASE\n            WHEN\n                -- No user or group has manage rights\n                 NOT EXISTS(\n                    SELECT 1\n                    FROM [dbo].[CollectionUser] CU2\n                             JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id]\n                    WHERE\n                        CU2.[CollectionId] = C.[Id] AND\n                        CU2.[Manage] = 1\n                )\n                    AND NOT EXISTS (\n                    SELECT 1\n                    FROM [dbo].[CollectionGroup] CG2\n                    WHERE\n                        CG2.[CollectionId] = C.[Id] AND\n                        CG2.[Manage] = 1\n                )\n            THEN 1\n            ELSE 0\n        END AS [Unmanaged]\n\tFROM\n\t    [dbo].[CollectionView] C\n\tLEFT JOIN\n\t    [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId\n\tLEFT JOIN\n\t    [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id]\n\tLEFT JOIN\n\t    [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n\tLEFT JOIN\n\t    [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n\tLEFT JOIN\n\t    [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]\n\tWHERE\n\t    C.[Id] = @CollectionId\n    GROUP BY\n    \tC.[Id],\n    \tC.[OrganizationId],\n    \tC.[Name],\n    \tC.[CreationDate],\n    \tC.[RevisionDate],\n    \tC.[ExternalId],\n        C.[DefaultUserCollectionEmail],\n        C.[Type]\n\n   IF (@IncludeAccessRelationships = 1)\n    BEGIN\n        EXEC [dbo].[CollectionGroup_ReadByCollectionId] @CollectionId\n        EXEC [dbo].[CollectionUser_ReadByCollectionId] @CollectionId\n\tEND\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadSharedCollectionsByOrganizationIdWithPermissions.sql",
    "content": "CREATE PROCEDURE [dbo].[Collection_ReadSharedCollectionsByOrganizationIdWithPermissions]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @IncludeAccessRelationships BIT\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        C.*,\n        MIN(CASE\n            WHEN\n                COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0\n            THEN 0\n            ELSE 1\n        END) AS [ReadOnly],\n        MIN(CASE\n            WHEN\n                COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0\n            THEN 0\n            ELSE 1\n        END) AS [HidePasswords],\n        MAX(CASE\n            WHEN\n                COALESCE(CU.[Manage], CG.[Manage], 0) = 0\n            THEN 0\n            ELSE 1\n        END) AS [Manage],\n        MAX(CASE\n            WHEN\n                CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL\n            THEN 0\n            ELSE 1\n        END) AS [Assigned],\n        CASE\n            WHEN\n                -- No user or group has manage rights\n                NOT EXISTS(\n                    SELECT 1\n                    FROM [dbo].[CollectionUser] CU2\n                    JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id]\n                    WHERE\n                        CU2.[CollectionId] = C.[Id] AND\n                        CU2.[Manage] = 1\n                )\n                AND NOT EXISTS (\n                    SELECT 1\n                    FROM [dbo].[CollectionGroup] CG2\n                    WHERE\n                        CG2.[CollectionId] = C.[Id] AND\n                        CG2.[Manage] = 1\n                )\n            THEN 1\n            ELSE 0\n        END AS [Unmanaged]\n    FROM\n        [dbo].[CollectionView] C\n    LEFT JOIN\n        [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId\n    LEFT JOIN\n        [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id]\n    LEFT JOIN\n        [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n    LEFT JOIN\n        [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n    LEFT JOIN\n        [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId]\n    WHERE\n        C.[OrganizationId] = @OrganizationId AND\n        C.[Type] = 0 -- Only SharedCollection\n    GROUP BY\n        C.[Id],\n        C.[OrganizationId],\n        C.[Name],\n        C.[CreationDate],\n        C.[RevisionDate],\n        C.[ExternalId],\n        C.[DefaultUserCollectionEmail],\n        C.[Type]\n\n    IF (@IncludeAccessRelationships = 1)\n    BEGIN\n        EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId\n        EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId\n    END\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_Create.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Folder_Create]\n    @Id UNIQUEIDENTIFIER OUTPUT,\n    @UserId UNIQUEIDENTIFIER,\n    @Name VARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    INSERT INTO [dbo].[Folder]\n    (\n        [Id],\n        [UserId],\n        [Name],\n        [CreationDate],\n        [RevisionDate]\n    )\n    VALUES\n    (\n        @Id,\n        @UserId,\n        @Name,\n        @CreationDate,\n        @RevisionDate\n    )\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_DeleteById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Folder_DeleteById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    DECLARE @UserId UNIQUEIDENTIFIER = (SELECT TOP 1 [UserId] FROM [dbo].[Folder] WHERE [Id] = @Id)\n    DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.\"', @UserId, '\"')\n\n    ;WITH [CTE] AS (\n        SELECT\n            [Id],\n            [OrganizationId]\n        FROM\n            [OrganizationUser]\n        WHERE\n            [UserId] = @UserId\n            AND [Status] = 2 -- Confirmed\n    )\n    UPDATE\n        C\n    SET\n        C.[Folders] = JSON_MODIFY(C.[Folders], @UserIdPath, NULL)\n    FROM\n        [dbo].[Cipher] C\n    INNER JOIN\n        [CTE] OU ON C.[UserId] IS NULL AND C.[OrganizationId] IN (SELECT [OrganizationId] FROM [CTE])\n    INNER JOIN\n        [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] AND O.[Id] = C.[OrganizationId] AND O.[Enabled] = 1\n    LEFT JOIN\n        [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id]\n    LEFT JOIN\n        [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id]\n    LEFT JOIN\n        [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]\n    LEFT JOIN\n        [dbo].[Group] G ON G.[Id] = GU.[GroupId]\n    LEFT JOIN\n        [dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]\n    WHERE\n        (\n            CU.[CollectionId] IS NOT NULL\n            OR CG.[CollectionId] IS NOT NULL\n        )\n        AND JSON_VALUE(C.[Folders], @UserIdPath) = @Id\n\n    UPDATE\n        C\n    SET\n        C.[Folders] = JSON_MODIFY(C.[Folders], @UserIdPath, NULL)\n    FROM\n        [dbo].[Cipher] C\n    WHERE\n        [UserId] = @UserId\n        AND JSON_VALUE([Folders], @UserIdPath) = @Id\n\n    DELETE\n    FROM\n        [dbo].[Folder]\n    WHERE\n        [Id] = @Id\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_ReadById.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Folder_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[FolderView]\n    WHERE\n        [Id] = @Id\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_ReadByUserId.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Folder_ReadByUserId]\n    @UserId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[FolderView]\n    WHERE\n        [UserId] = @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_Update.sql",
    "content": "﻿CREATE PROCEDURE [dbo].[Folder_Update]\n    @Id UNIQUEIDENTIFIER,\n    @UserId UNIQUEIDENTIFIER,\n    @Name VARCHAR(MAX),\n    @CreationDate DATETIME2(7),\n    @RevisionDate DATETIME2(7)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[Folder]\n    SET\n        [UserId] = @UserId,\n        [Name] = @Name,\n        [CreationDate] = @CreationDate,\n        [RevisionDate] = @RevisionDate\n    WHERE\n        [Id] = @Id\n\n    EXEC [dbo].[User_BumpAccountRevisionDate] @UserId\nEND"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_Create.sql",
    "content": "CREATE PROCEDURE [dbo].[SecurityTask_Create]\n\t@Id UNIQUEIDENTIFIER OUTPUT,\n\t@OrganizationId UNIQUEIDENTIFIER,\n\t@CipherId UNIQUEIDENTIFIER,\n\t@Type TINYINT,\n\t@Status TINYINT,\n\t@CreationDate DATETIME2(7),\n\t@RevisionDate DATETIME2(7)\nAS\nBEGIN\n\tSET NOCOUNT ON\n\n\tINSERT INTO [dbo].[SecurityTask]\n    (\n\t\t[Id],\n\t\t[OrganizationId],\n\t\t[CipherId],\n\t\t[Type],\n\t\t[Status],\n\t\t[CreationDate],\n\t\t[RevisionDate]\n\t)\n    VALUES\n    (\n\t\t@Id,\n\t\t@OrganizationId,\n\t\t@CipherId,\n\t\t@Type,\n\t\t@Status,\n\t\t@CreationDate,\n\t\t@RevisionDate\n\t)\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql",
    "content": "CREATE PROCEDURE [dbo].[SecurityTask_CreateMany]\n    @SecurityTasksJson NVARCHAR(MAX)\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    CREATE TABLE #TempSecurityTasks\n    (\n        [Id]             UNIQUEIDENTIFIER,\n        [OrganizationId] UNIQUEIDENTIFIER,\n        [CipherId]       UNIQUEIDENTIFIER,\n        [Type]           TINYINT,\n        [Status]         TINYINT,\n        [CreationDate]   DATETIME2(7),\n        [RevisionDate]   DATETIME2(7)\n    )\n\n    INSERT INTO #TempSecurityTasks\n    ([Id],\n     [OrganizationId],\n     [CipherId],\n     [Type],\n     [Status],\n     [CreationDate],\n     [RevisionDate])\n    SELECT CAST(JSON_VALUE([value], '$.Id') AS UNIQUEIDENTIFIER),\n           CAST(JSON_VALUE([value], '$.OrganizationId') AS UNIQUEIDENTIFIER),\n           CAST(JSON_VALUE([value], '$.CipherId') AS UNIQUEIDENTIFIER),\n           CAST(JSON_VALUE([value], '$.Type') AS TINYINT),\n           CAST(JSON_VALUE([value], '$.Status') AS TINYINT),\n           CAST(JSON_VALUE([value], '$.CreationDate') AS DATETIME2(7)),\n           CAST(JSON_VALUE([value], '$.RevisionDate') AS DATETIME2(7))\n    FROM OPENJSON(@SecurityTasksJson) ST\n\n    INSERT INTO [dbo].[SecurityTask]\n    (\n        [Id],\n        [OrganizationId],\n        [CipherId],\n        [Type],\n        [Status],\n        [CreationDate],\n        [RevisionDate]\n    )\n    SELECT [Id],\n           [OrganizationId],\n           [CipherId],\n           [Type],\n           [Status],\n           [CreationDate],\n           [RevisionDate]\n    FROM #TempSecurityTasks\n\n    DROP TABLE #TempSecurityTasks\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_MarkCompleteByCipherIds.sql",
    "content": "CREATE PROCEDURE [dbo].[SecurityTask_MarkCompleteByCipherIds]\n    @CipherIds AS [dbo].[GuidIdArray] READONLY\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    UPDATE\n        [dbo].[SecurityTask]\n    SET\n        [Status] = 1, -- completed\n        [RevisionDate] = SYSUTCDATETIME()\n    WHERE\n        [CipherId] IN (SELECT [Id] FROM @CipherIds)\n        AND [Status] <> 1 -- Not already completed\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadById.sql",
    "content": "CREATE PROCEDURE [dbo].[SecurityTask_ReadById]\n    @Id UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        *\n    FROM\n        [dbo].[SecurityTaskView]\n    WHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByOrganizationIdStatus.sql",
    "content": "CREATE PROCEDURE [dbo].[SecurityTask_ReadByOrganizationIdStatus]\n    @OrganizationId UNIQUEIDENTIFIER,\n    @Status TINYINT = NULL\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        ST.*\n    FROM\n        [dbo].[SecurityTaskView] ST\n    INNER JOIN\n        [dbo].[Organization] O ON O.[Id] = ST.[OrganizationId]\n    WHERE\n        ST.[OrganizationId] = @OrganizationId\n        AND O.[Enabled] = 1\n        AND ST.[Status] = COALESCE(@Status, ST.[Status])\n    ORDER BY ST.[CreationDate] DESC\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql",
    "content": "CREATE PROCEDURE [dbo].[SecurityTask_ReadByUserIdStatus]\n    @UserId [UNIQUEIDENTIFIER],\n    @Status [TINYINT] = NULL\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    WITH [OrganizationAccess] AS (\n        SELECT\n            [OU].[OrganizationId]\n        FROM\n            [dbo].[OrganizationUser] [OU]\n        INNER JOIN [dbo].[OrganizationView] [O]\n            ON [O].[Id] = [OU].[OrganizationId]\n        WHERE\n            [OU].[UserId] = @UserId\n            AND [OU].[Status] = 2 -- Confirmed\n            AND [O].[Enabled] = 1\n    ),\n    [UserCollectionAccess] AS (\n        SELECT\n            [CC].[CipherId]\n        FROM\n            [dbo].[OrganizationUser] [OU]\n        INNER JOIN [dbo].[OrganizationView] [O]\n            ON [O].[Id] = [OU].[OrganizationId]\n        INNER JOIN [dbo].[CollectionUser] [CU]\n            ON [CU].[OrganizationUserId] = [OU].[Id]\n        INNER JOIN [dbo].[CollectionCipher] [CC]\n            ON [CC].[CollectionId] = [CU].[CollectionId]\n        WHERE\n            [OU].[UserId] = @UserId\n            AND [OU].[Status] = 2 -- Confirmed\n            AND [O].[Enabled] = 1\n            AND [CU].[ReadOnly] = 0\n    ),\n    [GroupCollectionAccess] AS (\n        SELECT\n            [CC].[CipherId]\n        FROM\n            [dbo].[OrganizationUser] [OU]\n        INNER JOIN [dbo].[OrganizationView] [O]\n            ON [O].[Id] = [OU].[OrganizationId]\n        INNER JOIN [dbo].[GroupUser] [GU]\n            ON [GU].[OrganizationUserId] = [OU].[Id]\n        INNER JOIN [dbo].[CollectionGroup] [CG]\n            ON [CG].[GroupId] = [GU].[GroupId]\n        INNER JOIN [dbo].[CollectionCipher] [CC]\n            ON [CC].[CollectionId] = [CG].[CollectionId]\n        WHERE\n            [OU].[UserId] = @UserId\n            AND [OU].[Status] = 2 -- Confirmed\n            AND [CG].[ReadOnly] = 0\n    ),\n    [AccessibleCiphers] AS (\n        SELECT\n            [CipherId] FROM [UserCollectionAccess]\n        UNION\n        SELECT\n            [CipherId] FROM [GroupCollectionAccess]\n    )\n    SELECT\n        [ST].[Id],\n        [ST].[OrganizationId],\n        [ST].[CipherId],\n        [ST].[Type],\n        [ST].[Status],\n        [ST].[CreationDate],\n        [ST].[RevisionDate]\n    FROM\n        [dbo].[SecurityTaskView] [ST]\n        INNER JOIN [OrganizationAccess] [OA]\n            ON [ST].[OrganizationId] = [OA].[OrganizationId]\n    WHERE\n        (@Status IS NULL OR [ST].[Status] = @Status)\n        AND (\n          [ST].[CipherId] IS NULL\n          OR EXISTS (\n              SELECT 1\n              FROM [AccessibleCiphers] [AC]\n              WHERE [AC].[CipherId] = [ST].[CipherId]\n          )\n        )\n    ORDER BY\n        [ST].[CreationDate] DESC\n    OPTION (RECOMPILE);\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadMetricsByOrganizationId.sql",
    "content": "CREATE PROCEDURE [dbo].[SecurityTask_ReadMetricsByOrganizationId]\n    @OrganizationId UNIQUEIDENTIFIER\nAS\nBEGIN\n    SET NOCOUNT ON\n\n    SELECT\n        COUNT(CASE WHEN st.[Status] = 1 THEN 1 END) AS CompletedTasks,\n        COUNT(*) AS TotalTasks\n    FROM\n        [dbo].[SecurityTaskView] st\n    WHERE\n        st.[OrganizationId] = @OrganizationId\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_Update.sql",
    "content": "CREATE PROCEDURE [dbo].[SecurityTask_Update]\n\t@Id UNIQUEIDENTIFIER,\n\t@OrganizationId UNIQUEIDENTIFIER,\n\t@CipherId UNIQUEIDENTIFIER,\n\t@Type TINYINT,\n\t@Status TINYINT,\n\t@CreationDate DATETIME2(7),\n\t@RevisionDate DATETIME2(7)\nAS\nBEGIN\n\tSET NOCOUNT ON\n\n\tUPDATE\n\t    [dbo].[SecurityTask]\n\tSET\n        [OrganizationId] = @OrganizationId,\n\t\t[CipherId] = @CipherId,\n\t\t[Type] = @Type,\n\t\t[Status] = @Status,\n\t\t[CreationDate] = @CreationDate,\n\t\t[RevisionDate] = @RevisionDate\n\tWHERE\n        [Id] = @Id\nEND\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Tables/Cipher.sql",
    "content": "﻿\nCREATE TABLE [dbo].[Cipher] (\n    [Id]             UNIQUEIDENTIFIER NOT NULL,\n    [UserId]         UNIQUEIDENTIFIER NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NULL,\n    [Type]           TINYINT          NOT NULL,\n    [Data]           NVARCHAR (MAX)   NOT NULL,\n    [Favorites]      NVARCHAR (MAX)   NULL,\n    [Folders]        NVARCHAR (MAX)   NULL,\n    [Attachments]    NVARCHAR (MAX)   NULL,\n    [CreationDate]   DATETIME2 (7)    NOT NULL,\n    [RevisionDate]   DATETIME2 (7)    NOT NULL,\n    [DeletedDate]    DATETIME2 (7)    NULL,\n    [Reprompt]       TINYINT          NULL,\n    [Key]            VARCHAR(MAX)     NULL,\n    [ArchivedDate]   DATETIME2 (7)    NULL,\n    [Archives]       NVARCHAR(MAX)    NULL,\n    CONSTRAINT [PK_Cipher] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Cipher_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),\n    CONSTRAINT [FK_Cipher_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Cipher_UserId_OrganizationId_IncludeAll]\n    ON [dbo].[Cipher]([UserId] ASC, [OrganizationId] ASC)\n    INCLUDE ([Type], [Data], [Favorites], [Folders], [Attachments], [CreationDate], [RevisionDate], [DeletedDate]);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Cipher_OrganizationId]\n    ON [dbo].[Cipher]([OrganizationId] ASC);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Cipher_DeletedDate]\n    ON [dbo].[Cipher]([DeletedDate] ASC);\n\n\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Tables/Folder.sql",
    "content": "﻿CREATE TABLE [dbo].[Folder] (\n    [Id]           UNIQUEIDENTIFIER NOT NULL,\n    [UserId]       UNIQUEIDENTIFIER NOT NULL,\n    [Name]         VARCHAR (MAX)    NULL,\n    [CreationDate] DATETIME2 (7)    NOT NULL,\n    [RevisionDate] DATETIME2 (7)    NOT NULL,\n    CONSTRAINT [PK_Folder] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_Folder_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])\n);\n\n\nGO\nCREATE NONCLUSTERED INDEX [IX_Folder_UserId_IncludeAll]\n    ON [dbo].[Folder]([UserId] ASC)\n    INCLUDE ([Name], [CreationDate], [RevisionDate]);\n\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Tables/SecurityTask.sql",
    "content": "CREATE TABLE [dbo].[SecurityTask]\n(\n    [Id] UNIQUEIDENTIFIER NOT NULL,\n    [OrganizationId] UNIQUEIDENTIFIER NOT NULL,\n    [CipherId] UNIQUEIDENTIFIER NULL,\n    [Type] TINYINT NOT NULL,\n    [Status] TINYINT NOT NULL,\n    [CreationDate] DATETIME2 (7) NOT NULL,\n    [RevisionDate] DATETIME2 (7) NOT NULL,\n    CONSTRAINT [PK_SecurityTask] PRIMARY KEY CLUSTERED ([Id] ASC),\n    CONSTRAINT [FK_SecurityTask_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,\n    CONSTRAINT [FK_SecurityTask_Cipher] FOREIGN KEY ([CipherId]) REFERENCES [dbo].[Cipher] ([Id]) ON DELETE CASCADE,\n);\n\nGO\nCREATE NONCLUSTERED INDEX [IX_SecurityTask_CipherId]\n    ON [dbo].[SecurityTask]([CipherId] ASC) WHERE CipherId IS NOT NULL;\n\nGO\nCREATE NONCLUSTERED INDEX [IX_SecurityTask_OrganizationId]\n    ON [dbo].[SecurityTask]([OrganizationId] ASC) WHERE OrganizationId IS NOT NULL;\n\nGO\n\n"
  },
  {
    "path": "src/Sql/dbo/Vault/Views/CipherView.sql",
    "content": "﻿CREATE VIEW [dbo].[CipherView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Cipher]"
  },
  {
    "path": "src/Sql/dbo/Vault/Views/FolderView.sql",
    "content": "﻿CREATE VIEW [dbo].[FolderView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Folder]"
  },
  {
    "path": "src/Sql/dbo/Vault/Views/SecurityTaskView.sql",
    "content": "CREATE VIEW [dbo].[SecurityTaskView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[SecurityTask]\n"
  },
  {
    "path": "src/Sql/dbo/Views/CollectionView.sql",
    "content": "﻿CREATE VIEW [dbo].[CollectionView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Collection]"
  },
  {
    "path": "src/Sql/dbo/Views/DeviceView.sql",
    "content": "﻿CREATE VIEW [dbo].[DeviceView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Device]"
  },
  {
    "path": "src/Sql/dbo/Views/GroupView.sql",
    "content": "﻿CREATE VIEW [dbo].[GroupView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Group]"
  },
  {
    "path": "src/Sql/dbo/Views/NotificationStatusDetailsView.sql",
    "content": "﻿CREATE VIEW [dbo].[NotificationStatusDetailsView]\nAS\nSELECT\n    N.[Id],\n    N.[Priority],\n    N.[Global],\n    N.[ClientType],\n    N.[UserId],\n    N.[OrganizationId],\n    N.[Title],\n    N.[Body],\n    N.[CreationDate],\n    N.[RevisionDate],\n    N.[TaskId],\n    NS.[UserId] AS [NotificationStatusUserId],\n    NS.[ReadDate],\n    NS.[DeletedDate]\nFROM\n    [dbo].[Notification] AS N\nLEFT JOIN\n    [dbo].[NotificationStatus] as NS\nON\n    N.[Id] = NS.[NotificationId]\n"
  },
  {
    "path": "src/Sql/dbo/Views/NotificationStatusView.sql",
    "content": "﻿CREATE VIEW [dbo].[NotificationStatusView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[NotificationStatus]\n"
  },
  {
    "path": "src/Sql/dbo/Views/NotificationView.sql",
    "content": "﻿CREATE VIEW [dbo].[NotificationView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Notification]\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationApiKeyView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationApiKeyView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[OrganizationApiKey]\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationCipherDetailsCollectionsView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationCipherDetailsCollectionsView]\nAS\n    SELECT\n      C.[Id],\n      C.[UserId],\n      C.[OrganizationId],\n      C.[Type],\n      C.[Data],\n      C.[Attachments],\n      C.[Favorites],\n      C.[Folders],\n      C.[CreationDate],\n      C.[RevisionDate],\n      C.[DeletedDate],\n      C.[Reprompt],\n      C.[Key],\n      CASE\n          WHEN O.[UseTotp] = 1 THEN 1\n          ELSE 0\n      END AS [OrganizationUseTotp],\n      CC.[CollectionId],\n      COL.[Type] AS [CollectionType]\n    FROM [dbo].[Cipher] C\n    INNER JOIN [dbo].[Organization] O ON C.[OrganizationId] = O.[Id]\n    LEFT JOIN [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id]\n    LEFT JOIN [dbo].[Collection] COL ON CC.[CollectionId] = COL.[Id]\n    WHERE C.[UserId] IS NULL -- Organization ciphers only\n      AND O.[Enabled] = 1; -- Only enabled organizations\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationConnectionView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationConnectionView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[OrganizationConnection]\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationDomainView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationDomainView]\nAS\nSELECT \n    *\nFROM\n    [dbo].[OrganizationDomain]"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationIntegrationConfigurationDetailsView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView]\nAS\n    SELECT\n        oi.[OrganizationId],\n        oi.[Type] AS [IntegrationType],\n        oic.[EventType],\n        oic.[Configuration],\n        oi.[Configuration] AS [IntegrationConfiguration],\n        oic.[Template],\n        oic.[Filters]\n    FROM\n        [dbo].[OrganizationIntegrationConfiguration] oic\n        INNER JOIN\n        [dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId]\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationIntegrationConfigurationView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationIntegrationConfigurationView]\nAS\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationIntegrationConfiguration]\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationIntegrationView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationIntegrationView]\nAS\n    SELECT\n        *\n    FROM\n        [dbo].[OrganizationIntegration]\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationSponsorship.sql",
    "content": "﻿CREATE VIEW [dbo].[OrganizationSponsorshipView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[OrganizationSponsorship]\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView]\nAS\nSELECT\n    OU.[UserId],\n    OU.[OrganizationId],\n    OU.[Id] OrganizationUserId,\n    O.[Name],\n    O.[Enabled],\n    O.[PlanType],\n    O.[UsePolicies],\n    O.[UseSso],\n    O.[UseKeyConnector],\n    O.[UseScim],\n    O.[UseGroups],\n    O.[UseDirectory],\n    O.[UseEvents],\n    O.[UseTotp],\n    O.[Use2fa],\n    O.[UseApi],\n    O.[UseResetPassword],\n    O.[SelfHost],\n    O.[UsersGetPremium],\n    O.[UseCustomPermissions],\n    O.[UseSecretsManager],\n    O.[Seats],\n    O.[MaxCollections],\n    COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],\n    O.[Identifier],\n    OU.[Key],\n    OU.[ResetPasswordKey],\n    O.[PublicKey],\n    O.[PrivateKey],\n    OU.[Status],\n    OU.[Type],\n    SU.[ExternalId] SsoExternalId,\n    OU.[Permissions],\n    PO.[ProviderId],\n    P.[Name] ProviderName,\n    P.[Type] ProviderType,\n    SS.[Enabled] SsoEnabled,\n    SS.[Data] SsoConfig,\n    OS.[FriendlyName] FamilySponsorshipFriendlyName,\n    OS.[LastSyncDate] FamilySponsorshipLastSyncDate,\n    OS.[ToDelete] FamilySponsorshipToDelete,\n    OS.[ValidUntil] FamilySponsorshipValidUntil,\n    OU.[AccessSecretsManager],\n    O.[UsePasswordManager],\n    O.[SmSeats],\n    O.[SmServiceAccounts],\n    O.[LimitCollectionCreation],\n    O.[LimitCollectionDeletion],\n    O.[AllowAdminAccessToAllCollectionItems],\n    O.[UseRiskInsights],\n    O.[LimitItemDeletion],\n    O.[UseAdminSponsoredFamilies],\n    O.[UseOrganizationDomains],\n    OS.[IsAdminInitiated],\n    O.[UseAutomaticUserConfirmation],\n    O.[UsePhishingBlocker],\n    O.[UseDisableSmAdsForUsers],\n    O.[UseMyItems]\nFROM\n    [dbo].[OrganizationUser] OU\nLEFT JOIN\n    [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]\nLEFT JOIN\n    [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]\nLEFT JOIN\n    [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id]\nLEFT JOIN\n    [dbo].[Provider] P ON P.[Id] = PO.[ProviderId]\nLEFT JOIN\n    [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId]\nLEFT JOIN\n    [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id]\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationUserUserDetailsView.sql",
    "content": "﻿CREATE VIEW [dbo].[OrganizationUserUserDetailsView]\nAS\nSELECT\n    OU.[Id],\n    OU.[UserId],\n    OU.[OrganizationId],\n    U.[Name],\n    ISNULL(U.[Email], OU.[Email]) Email,\n    U.[AvatarColor],\n    U.[TwoFactorProviders],\n    U.[Premium],\n    OU.[Status],\n    OU.[Type],\n    OU.[AccessSecretsManager],\n    OU.[ExternalId],\n    SU.[ExternalId] SsoExternalId,\n    OU.[Permissions],\n    OU.[ResetPasswordKey],\n    U.[UsesKeyConnector],\n    CASE WHEN U.[MasterPassword] IS NOT NULL THEN 1 ELSE 0 END AS HasMasterPassword\nFROM\n    [dbo].[OrganizationUser] OU\nLEFT JOIN\n    [dbo].[User] U ON U.[Id] = OU.[UserId]\nLEFT JOIN\n    [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]\n"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationUserView.sql",
    "content": "﻿CREATE VIEW [dbo].[OrganizationUserView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[OrganizationUser]"
  },
  {
    "path": "src/Sql/dbo/Views/OrganizationView.sql",
    "content": "CREATE VIEW [dbo].[OrganizationView]\nAS\nSELECT\n    [Id],\n    [Identifier],\n    [Name],\n    [BusinessName],\n    [BusinessAddress1],\n    [BusinessAddress2],\n    [BusinessAddress3],\n    [BusinessCountry],\n    [BusinessTaxNumber],\n    [BillingEmail],\n    [Plan],\n    [PlanType],\n    [Seats],\n    [MaxCollections],\n    [UsePolicies],\n    [UseSso],\n    [UseGroups],\n    [UseDirectory],\n    [UseEvents],\n    [UseTotp],\n    [Use2fa],\n    [UseApi],\n    [UseResetPassword],\n    [SelfHost],\n    [UsersGetPremium],\n    [Storage],\n    COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],\n    [Gateway],\n    [GatewayCustomerId],\n    [GatewaySubscriptionId],\n    [ReferenceData],\n    [Enabled],\n    [LicenseKey],\n    [PublicKey],\n    [PrivateKey],\n    [TwoFactorProviders],\n    [ExpirationDate],\n    [CreationDate],\n    [RevisionDate],\n    [OwnersNotifiedOfAutoscaling],\n    [MaxAutoscaleSeats],\n    [UseKeyConnector],\n    [UseScim],\n    [UseCustomPermissions],\n    [UseSecretsManager],\n    [Status],\n    [UsePasswordManager],\n    [SmSeats],\n    [SmServiceAccounts],\n    [MaxAutoscaleSmSeats],\n    [MaxAutoscaleSmServiceAccounts],\n    [SecretsManagerBeta],\n    [LimitCollectionCreation],\n    [LimitCollectionDeletion],\n    [LimitItemDeletion],\n    [AllowAdminAccessToAllCollectionItems],\n    [UseRiskInsights],\n    [UseOrganizationDomains],\n    [UseAdminSponsoredFamilies],\n    [SyncSeats],\n    [UseAutomaticUserConfirmation],\n    [UsePhishingBlocker],\n    [UseDisableSmAdsForUsers],\n    [UseMyItems]\nFROM\n    [dbo].[Organization]\n"
  },
  {
    "path": "src/Sql/dbo/Views/PasswordHealthReportApplicationView.sql",
    "content": "CREATE VIEW [dbo].[PasswordHealthReportApplicationView] AS\n    SELECT * FROM [dbo].[PasswordHealthReportApplication]"
  },
  {
    "path": "src/Sql/dbo/Views/PolicyView.sql",
    "content": "﻿CREATE VIEW [dbo].[PolicyView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Policy]"
  },
  {
    "path": "src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql",
    "content": "﻿CREATE VIEW [dbo].[ProviderOrganizationOrganizationDetailsView]\nAS\nSELECT\n    PO.[Id],\n    PO.[ProviderId],\n    PO.[OrganizationId],\n    O.[Name] OrganizationName,\n    PO.[Key],\n    PO.[Settings],\n    PO.[CreationDate],\n    PO.[RevisionDate],\n    (SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status = 2) UserCount,\n    (SELECT COUNT(1) FROM [dbo].[OrganizationUser] OU WHERE OU.OrganizationId = PO.OrganizationId AND OU.Status >= 0) OccupiedSeats,\n    O.[Seats],\n    O.[Plan],\n    O.[PlanType],\n    O.[Status]\nFROM\n    [dbo].[ProviderOrganization] PO\nLEFT JOIN\n    [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId]\n"
  },
  {
    "path": "src/Sql/dbo/Views/ProviderOrganizationView.sql",
    "content": "﻿CREATE VIEW [dbo].[ProviderOrganizationView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[ProviderOrganization]\n"
  },
  {
    "path": "src/Sql/dbo/Views/ProviderUserProviderDetailsView.sql",
    "content": "﻿CREATE VIEW [dbo].[ProviderUserProviderDetailsView]\nAS\nSELECT\n    PU.[UserId],\n    PU.[ProviderId],\n    P.[Name],\n    PU.[Key],\n    PU.[Status],\n    PU.[Type],\n    P.[Enabled],\n    PU.[Permissions],\n    P.[UseEvents],\n    P.[Status] ProviderStatus,\n    P.[Type] ProviderType\nFROM\n    [dbo].[ProviderUser] PU\nLEFT JOIN\n    [dbo].[Provider] P ON P.[Id] = PU.[ProviderId]\n"
  },
  {
    "path": "src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql",
    "content": "CREATE VIEW [dbo].[ProviderUserProviderOrganizationDetailsView]\nAS\nSELECT\n    PU.[UserId],\n    PO.[OrganizationId],\n    O.[Name],\n    O.[Enabled],\n    O.[UsePolicies],\n    O.[UseSso],\n    O.[UseKeyConnector],\n    O.[UseScim],\n    O.[UseGroups],\n    O.[UseDirectory],\n    O.[UseEvents],\n    O.[UseTotp],\n    O.[Use2fa],\n    O.[UseApi],\n    O.[UseResetPassword],\n    O.[UseSecretsManager],\n    O.[UsePasswordManager],\n    O.[SelfHost],\n    O.[UsersGetPremium],\n    O.[UseCustomPermissions],\n    O.[Seats],\n    O.[MaxCollections],\n    COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],\n    O.[Identifier],\n    PO.[Key],\n    O.[PublicKey],\n    O.[PrivateKey],\n    PU.[Status],\n    PU.[Type],\n    PO.[ProviderId],\n    PU.[Id] ProviderUserId,\n    P.[Name] ProviderName,\n    O.[PlanType],\n    O.[LimitCollectionCreation],\n    O.[LimitCollectionDeletion],\n    O.[AllowAdminAccessToAllCollectionItems],\n    O.[UseRiskInsights],\n    O.[UseAdminSponsoredFamilies],\n    P.[Type] ProviderType,\n    O.[LimitItemDeletion],\n    O.[UseOrganizationDomains],\n    O.[UseAutomaticUserConfirmation],\n    SS.[Enabled] SsoEnabled,\n    SS.[Data] SsoConfig,\n    O.[UsePhishingBlocker],\n    O.[UseDisableSmAdsForUsers],\n    O.[UseMyItems]\nFROM\n    [dbo].[ProviderUser] PU\nINNER JOIN\n    [dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId]\nINNER JOIN\n    [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId]\nINNER JOIN\n    [dbo].[Provider] P ON P.[Id] = PU.[ProviderId]\nLEFT JOIN\n    [dbo].[SsoConfig] SS ON SS.[OrganizationId] = O.[Id]\n"
  },
  {
    "path": "src/Sql/dbo/Views/ProviderUserUserDetailsView.sql",
    "content": "﻿CREATE VIEW [dbo].[ProviderUserUserDetailsView]\nAS\nSELECT\n    PU.[Id],\n    PU.[UserId],\n    PU.[ProviderId],\n    U.[Name],\n    ISNULL(U.[Email], PU.[Email]) Email,\n    PU.[Status],\n    PU.[Type],\n    PU.[Permissions]\nFROM\n    [dbo].[ProviderUser] PU\nLEFT JOIN\n    [dbo].[User] U ON U.[Id] = PU.[UserId]\n"
  },
  {
    "path": "src/Sql/dbo/Views/ProviderUserView.sql",
    "content": "﻿CREATE VIEW [dbo].[ProviderUserView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[ProviderUser]\n"
  },
  {
    "path": "src/Sql/dbo/Views/ProviderView.sql",
    "content": "﻿CREATE VIEW [dbo].[ProviderView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Provider]\n"
  },
  {
    "path": "src/Sql/dbo/Views/SubscriptionDiscountView.sql",
    "content": "CREATE VIEW [dbo].[SubscriptionDiscountView]\nAS\nSELECT *\nFROM\n    [dbo].[SubscriptionDiscount]\n"
  },
  {
    "path": "src/Sql/dbo/Views/TransactionView.sql",
    "content": "﻿CREATE VIEW [dbo].[TransactionView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[Transaction]\n"
  },
  {
    "path": "src/Sql/dbo/Views/UserEmailDomainView.sql",
    "content": "CREATE VIEW [dbo].[UserEmailDomainView]\nAS\nSELECT \n    Id,\n    Email,\n    SUBSTRING(Email, CHARINDEX('@', Email) + 1, LEN(Email)) AS EmailDomain\nFROM dbo.[User]\nWHERE Email IS NOT NULL \n    AND CHARINDEX('@', Email) > 0\nGO\n"
  },
  {
    "path": "src/Sql/dbo/Views/UserPremiumAccessView.sql",
    "content": "CREATE VIEW [dbo].[UserPremiumAccessView]\nAS\nSELECT \n    U.[Id],\n    U.[Premium] AS [PersonalPremium],\n    CAST(\n        MAX(CASE \n            WHEN O.[Id] IS NOT NULL THEN 1 \n            ELSE 0 \n        END) AS BIT\n    ) AS [OrganizationPremium]\nFROM \n    [dbo].[User] U\nLEFT JOIN \n    [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]\nLEFT JOIN \n    [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]\n    AND O.[UsersGetPremium] = 1\n    AND O.[Enabled] = 1\nGROUP BY \n    U.[Id], U.[Premium];\n"
  },
  {
    "path": "src/Sql/dbo/Views/UserProviderAccessView.sql",
    "content": "CREATE VIEW [dbo].[UserProviderAccessView]\nAS\nSELECT DISTINCT\n    PU.[UserId],\n    PO.[OrganizationId]\nFROM\n    [dbo].[ProviderUserView] PU\nINNER JOIN\n    [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]\n"
  },
  {
    "path": "src/Sql/dbo/Views/UserView.sql",
    "content": "﻿CREATE VIEW [dbo].[UserView]\nAS\nSELECT\n    [Id],\n    [Name],\n    [Email],\n    [EmailVerified],\n    [MasterPassword],\n    [MasterPasswordHint],\n    [Culture],\n    [SecurityStamp],\n    [TwoFactorProviders],\n    [TwoFactorRecoveryCode],\n    [EquivalentDomains],\n    [ExcludedGlobalEquivalentDomains],\n    [AccountRevisionDate],\n    [Key],\n    [PublicKey],\n    [PrivateKey],\n    [Premium],\n    [PremiumExpirationDate],\n    [RenewalReminderDate],\n    [Storage],\n    COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],\n    [Gateway],\n    [GatewayCustomerId],\n    [GatewaySubscriptionId],\n    [ReferenceData],\n    [LicenseKey],\n    [ApiKey],\n    [Kdf],\n    [KdfIterations],\n    [KdfMemory],\n    [KdfParallelism],\n    [CreationDate],\n    [RevisionDate],\n    [ForcePasswordReset],\n    [UsesKeyConnector],\n    [FailedLoginCount],\n    [LastFailedLoginDate],\n    [AvatarColor],\n    [LastPasswordChangeDate],\n    [LastKdfChangeDate],\n    [LastKeyRotationDate],\n    [LastEmailChangeDate],\n    [VerifyDevices],\n    [SecurityState],\n    [SecurityVersion],\n    [SignedPublicKey],\n    [V2UpgradeToken],\n    [MasterPasswordSalt]\nFROM\n    [dbo].[User]\n"
  },
  {
    "path": "src/Sql/dbo/Views/WebAuthnCredentialView.sql",
    "content": "﻿CREATE VIEW [dbo].[WebAuthnCredentialView]\nAS\nSELECT\n    *\nFROM\n    [dbo].[WebAuthnCredential]\n"
  },
  {
    "path": "src/Sql/dbo_finalization/.gitkeep",
    "content": ""
  },
  {
    "path": "test/Admin.Test/Admin.Test.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <IsPackable>false</IsPackable>\n    <RootNamespace>Admin.Test</RootNamespace>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\"\n      Version=\"$(XUnitRunnerVisualStudioVersion)\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Admin\\Admin.csproj\" />\n    <ProjectReference Include=\"..\\Common\\Common.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs",
    "content": "﻿using Bit.Admin.AdminConsole.Controllers;\nusing Bit.Admin.AdminConsole.Models;\nusing Bit.Admin.Enums;\nusing Bit.Admin.Services;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.ViewFeatures;\nusing NSubstitute;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Admin.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(OrganizationsController))]\n[SutProviderCustomize]\npublic class OrganizationsControllerTests\n{\n    #region Edit (POST)\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_ProviderSeatScaling_NonBillableProvider_NoOp(\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var organizationId = new Guid();\n        var update = new OrganizationEditModel { UseSecretsManager = false };\n\n        var organization = new Organization\n        {\n            Id = organizationId\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Created };\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organizationId).Returns(provider);\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organizationId, update);\n\n        // Assert\n        await sutProvider.GetDependency<IProviderBillingService>().DidNotReceiveWithAnyArgs()\n            .ScaleSeats(Arg.Any<Provider>(), Arg.Any<PlanType>(), Arg.Any<int>());\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_ProviderSeatScaling_UnmanagedOrganization_NoOp(\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var organizationId = new Guid();\n        var update = new OrganizationEditModel { UseSecretsManager = false };\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Status = OrganizationStatusType.Created\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organizationId).Returns(provider);\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organizationId, update);\n\n        // Assert\n        await sutProvider.GetDependency<IProviderBillingService>().DidNotReceiveWithAnyArgs()\n            .ScaleSeats(Arg.Any<Provider>(), Arg.Any<PlanType>(), Arg.Any<int>());\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_ProviderSeatScaling_NonCBPlanType_NoOp(\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var organizationId = new Guid();\n\n        var update = new OrganizationEditModel\n        {\n            UseSecretsManager = false,\n            Seats = 10,\n            PlanType = PlanType.FamiliesAnnually\n        };\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Status = OrganizationStatusType.Managed,\n            Seats = 10\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organizationId).Returns(provider);\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organizationId, update);\n\n        // Assert\n        await sutProvider.GetDependency<IProviderBillingService>().DidNotReceiveWithAnyArgs()\n            .ScaleSeats(Arg.Any<Provider>(), Arg.Any<PlanType>(), Arg.Any<int>());\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_ProviderSeatScaling_NoUpdateRequired_NoOp(\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var organizationId = new Guid();\n        var update = new OrganizationEditModel\n        {\n            UseSecretsManager = false,\n            Seats = 10,\n            PlanType = PlanType.EnterpriseMonthly\n        };\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Status = OrganizationStatusType.Managed,\n            Seats = 10,\n            PlanType = PlanType.EnterpriseMonthly\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organizationId).Returns(provider);\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organizationId, update);\n\n        // Assert\n        await sutProvider.GetDependency<IProviderBillingService>().DidNotReceiveWithAnyArgs()\n            .ScaleSeats(Arg.Any<Provider>(), Arg.Any<PlanType>(), Arg.Any<int>());\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_ProviderSeatScaling_PlanTypesUpdate_ScalesSeatsCorrectly(\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var organizationId = new Guid();\n        var update = new OrganizationEditModel\n        {\n            UseSecretsManager = false,\n            Seats = 10,\n            PlanType = PlanType.EnterpriseMonthly\n        };\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Status = OrganizationStatusType.Managed,\n            Seats = 10,\n            PlanType = PlanType.TeamsMonthly\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organizationId).Returns(provider);\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organizationId, update);\n\n        // Assert\n        var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();\n\n        await providerBillingService.Received(1).ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);\n        await providerBillingService.Received(1).ScaleSeats(provider, update.PlanType!.Value, organization.Seats.Value);\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_ProviderSeatScaling_SeatsUpdate_ScalesSeatsCorrectly(\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var organizationId = new Guid();\n        var update = new OrganizationEditModel\n        {\n            UseSecretsManager = false,\n            Seats = 15,\n            PlanType = PlanType.EnterpriseMonthly\n        };\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Status = OrganizationStatusType.Managed,\n            Seats = 10,\n            PlanType = PlanType.EnterpriseMonthly\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organizationId).Returns(provider);\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organizationId, update);\n\n        // Assert\n        var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();\n\n        await providerBillingService.Received(1).ScaleSeats(provider, organization.PlanType, update.Seats!.Value - organization.Seats.Value);\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_ProviderSeatScaling_FullUpdate_ScalesSeatsCorrectly(\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var organizationId = new Guid();\n        var update = new OrganizationEditModel\n        {\n            UseSecretsManager = false,\n            Seats = 15,\n            PlanType = PlanType.EnterpriseMonthly\n        };\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Status = OrganizationStatusType.Managed,\n            Seats = 10,\n            PlanType = PlanType.TeamsMonthly\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var provider = new Provider { Type = ProviderType.Msp, Status = ProviderStatusType.Billable };\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organizationId).Returns(provider);\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organizationId, update);\n\n        // Assert\n        var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();\n\n        await providerBillingService.Received(1).ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);\n        await providerBillingService.Received(1).ScaleSeats(provider, update.PlanType!.Value, update.Seats!.Value - organization.Seats.Value + organization.Seats.Value);\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_UseAutomaticUserConfirmation_FullUpdate_SavesFeatureCorrectly(\n        Organization organization,\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var update = new OrganizationEditModel\n        {\n            PlanType = PlanType.TeamsMonthly,\n            UseAutomaticUserConfirmation = true\n        };\n\n        organization.UseAutomaticUserConfirmation = false;\n\n        sutProvider.GetDependency<IAccessControlService>()\n                .UserHasPermission(Permission.Org_Plan_Edit)\n                .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id);\n        sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())\n            .Returns(Valid(request));\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organization.Id, update);\n\n        // Assert\n        await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(o => o.Id == organization.Id\n            && o.UseAutomaticUserConfirmation == true));\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_EnableUseAutomaticUserConfirmation_ValidationFails_RedirectsWithError(\n        Organization organization,\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var update = new OrganizationEditModel\n        {\n            PlanType = PlanType.TeamsMonthly,\n            UseAutomaticUserConfirmation = true\n        };\n\n        organization.UseAutomaticUserConfirmation = false;\n\n        sutProvider.GetDependency<IAccessControlService>()\n            .UserHasPermission(Permission.Org_Plan_Edit)\n            .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id);\n        sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())\n            .Returns(Invalid(request, new UserNotCompliantWithSingleOrganization()));\n\n        sutProvider.Sut.TempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());\n\n        // Act\n        var result = await sutProvider.Sut.Edit(organization.Id, update);\n\n        // Assert\n        var redirectResult = Assert.IsType<RedirectToActionResult>(result);\n        Assert.Equal(\"Edit\", redirectResult.ActionName);\n        Assert.Equal(organization.Id, redirectResult.RouteValues![\"id\"]);\n\n        await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_EnableUseAutomaticUserConfirmation_ProviderValidationFails_RedirectsWithError(\n        Organization organization,\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var update = new OrganizationEditModel\n        {\n            PlanType = PlanType.TeamsMonthly,\n            UseAutomaticUserConfirmation = true\n        };\n\n        organization.UseAutomaticUserConfirmation = false;\n\n        sutProvider.GetDependency<IAccessControlService>()\n            .UserHasPermission(Permission.Org_Plan_Edit)\n            .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id);\n        sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())\n            .Returns(Invalid(request, new ProviderExistsInOrganization()));\n\n        sutProvider.Sut.TempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());\n\n        // Act\n        var result = await sutProvider.Sut.Edit(organization.Id, update);\n\n        // Assert\n        var redirectResult = Assert.IsType<RedirectToActionResult>(result);\n        Assert.Equal(\"Edit\", redirectResult.ActionName);\n\n        await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_UseAutomaticUserConfirmation_NotChanged_DoesNotCallValidator(\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var organizationId = new Guid();\n        var update = new OrganizationEditModel\n        {\n            UseSecretsManager = false,\n            UseAutomaticUserConfirmation = false\n        };\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            UseAutomaticUserConfirmation = false\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organizationId, update);\n\n        // Assert\n        await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .DidNotReceive()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>());\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_UseAutomaticUserConfirmation_AlreadyEnabled_DoesNotCallValidator(\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        // Arrange\n        var organizationId = new Guid();\n        var update = new OrganizationEditModel\n        {\n            UseSecretsManager = false,\n            UseAutomaticUserConfirmation = true\n        };\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            UseAutomaticUserConfirmation = true\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        // Act\n        _ = await sutProvider.Sut.Edit(organizationId, update);\n\n        // Assert\n        await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .DidNotReceive()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>());\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_UseAutomaticUserConfirmation_EnabledByPortal_LogsEvent(\n        Organization organization,\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        var update = new OrganizationEditModel\n        {\n            PlanType = PlanType.TeamsMonthly,\n            UseAutomaticUserConfirmation = true\n        };\n\n        organization.UseAutomaticUserConfirmation = false;\n        organization.Enabled = true;\n        organization.UseEvents = true;\n\n        sutProvider.GetDependency<IAccessControlService>()\n                .UserHasPermission(Permission.Org_Plan_Edit)\n                .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id);\n        sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())\n            .Returns(Valid(request));\n\n        _ = await sutProvider.Sut.Edit(organization.Id, update);\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationEventAsync(\n                Arg.Is<Organization>(o => o.Id == organization.Id),\n                EventType.Organization_AutoConfirmEnabled_Portal,\n                EventSystemUser.BitwardenPortal);\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_UseAutomaticUserConfirmation_DisabledByPortal_LogsEvent(\n        Organization organization,\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        var update = new OrganizationEditModel\n        {\n            PlanType = PlanType.TeamsMonthly,\n            UseAutomaticUserConfirmation = false\n        };\n\n        organization.UseAutomaticUserConfirmation = true;\n        organization.Enabled = true;\n        organization.UseEvents = true;\n\n        sutProvider.GetDependency<IAccessControlService>()\n                .UserHasPermission(Permission.Org_Plan_Edit)\n                .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        _ = await sutProvider.Sut.Edit(organization.Id, update);\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationEventAsync(\n                Arg.Is<Organization>(o => o.Id == organization.Id),\n                EventType.Organization_AutoConfirmDisabled_Portal,\n                EventSystemUser.BitwardenPortal);\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task Edit_UseAutomaticUserConfirmation_NoChange_DoesNotLogEvent(\n        Organization organization,\n        SutProvider<OrganizationsController> sutProvider)\n    {\n        var update = new OrganizationEditModel\n        {\n            PlanType = PlanType.TeamsMonthly,\n            UseAutomaticUserConfirmation = true\n        };\n\n        organization.UseAutomaticUserConfirmation = true;\n        organization.Enabled = true;\n        organization.UseEvents = true;\n\n        sutProvider.GetDependency<IAccessControlService>()\n                .UserHasPermission(Permission.Org_Plan_Edit)\n                .Returns(true);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        _ = await sutProvider.Sut.Edit(organization.Id, update);\n\n        await sutProvider.GetDependency<IEventService>().DidNotReceive()\n            .LogOrganizationEventAsync(\n                Arg.Any<Organization>(),\n                Arg.Any<EventType>(),\n                Arg.Any<EventSystemUser>());\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Admin.Test/AdminConsole/Controllers/ProvidersControllerTests.cs",
    "content": "﻿using Bit.Admin.AdminConsole.Controllers;\nusing Bit.Admin.AdminConsole.Models;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Providers.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing NSubstitute.ReceivedExtensions;\n\nnamespace Admin.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(ProvidersController))]\n[SutProviderCustomize]\npublic class ProvidersControllerTests\n{\n    #region CreateMspAsync\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task CreateMspAsync_WithValidModel_CreatesProvider(\n        CreateMspProviderModel model,\n        SutProvider<ProvidersController> sutProvider)\n    {\n        // Arrange\n\n        // Act\n        var actual = await sutProvider.Sut.CreateMsp(model);\n\n        // Assert\n        Assert.NotNull(actual);\n        await sutProvider.GetDependency<ICreateProviderCommand>()\n            .Received(Quantity.Exactly(1))\n            .CreateMspAsync(\n                Arg.Is<Provider>(x => x.Type == ProviderType.Msp),\n                model.OwnerEmail,\n                model.TeamsMonthlySeatMinimum,\n                model.EnterpriseMonthlySeatMinimum);\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task CreateMspAsync_RedirectsToExpectedPage_AfterCreatingProvider(\n        CreateMspProviderModel model,\n        Guid expectedProviderId,\n        SutProvider<ProvidersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICreateProviderCommand>()\n            .When(x =>\n                x.CreateMspAsync(\n                    Arg.Is<Provider>(y => y.Type == ProviderType.Msp),\n                    model.OwnerEmail,\n                    model.TeamsMonthlySeatMinimum,\n                    model.EnterpriseMonthlySeatMinimum))\n            .Do(callInfo =>\n            {\n                var providerArgument = callInfo.ArgAt<Provider>(0);\n                providerArgument.Id = expectedProviderId;\n            });\n\n        // Act\n        var actual = await sutProvider.Sut.CreateMsp(model);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.IsType<RedirectToActionResult>(actual);\n        var actualResult = (RedirectToActionResult)actual;\n        Assert.Equal(\"Edit\", actualResult.ActionName);\n        Assert.Null(actualResult.ControllerName);\n        Assert.Equal(expectedProviderId, actualResult.RouteValues[\"Id\"]);\n    }\n    #endregion\n\n    #region CreateBusinessUnitAsync\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task CreateBusinessUnitAsync_WithValidModel_CreatesProvider(\n        CreateBusinessUnitProviderModel model,\n        SutProvider<ProvidersController> sutProvider)\n    {\n        // Arrange\n\n        // Act\n        var actual = await sutProvider.Sut.CreateBusinessUnit(model);\n\n        // Assert\n        Assert.NotNull(actual);\n        await sutProvider.GetDependency<ICreateProviderCommand>()\n            .Received(Quantity.Exactly(1))\n            .CreateBusinessUnitAsync(\n                Arg.Is<Provider>(x => x.Type == ProviderType.BusinessUnit),\n                model.OwnerEmail,\n                Arg.Is<PlanType>(y => y == model.Plan),\n                model.EnterpriseSeatMinimum);\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task CreateBusinessUnitAsync_RedirectsToExpectedPage_AfterCreatingProvider(\n        CreateBusinessUnitProviderModel model,\n        Guid expectedProviderId,\n        SutProvider<ProvidersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICreateProviderCommand>()\n            .When(x =>\n                x.CreateBusinessUnitAsync(\n                    Arg.Is<Provider>(y => y.Type == ProviderType.BusinessUnit),\n                    model.OwnerEmail,\n                    Arg.Is<PlanType>(y => y == model.Plan),\n                    model.EnterpriseSeatMinimum))\n            .Do(callInfo =>\n            {\n                var providerArgument = callInfo.ArgAt<Provider>(0);\n                providerArgument.Id = expectedProviderId;\n            });\n\n        // Act\n        var actual = await sutProvider.Sut.CreateBusinessUnit(model);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.IsType<RedirectToActionResult>(actual);\n        var actualResult = (RedirectToActionResult)actual;\n        Assert.Equal(\"Edit\", actualResult.ActionName);\n        Assert.Null(actualResult.ControllerName);\n        Assert.Equal(expectedProviderId, actualResult.RouteValues[\"Id\"]);\n    }\n    #endregion\n\n    #region CreateResellerAsync\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task CreateResellerAsync_WithValidModel_CreatesProvider(\n        CreateResellerProviderModel model,\n        SutProvider<ProvidersController> sutProvider)\n    {\n        // Arrange\n\n        // Act\n        var actual = await sutProvider.Sut.CreateReseller(model);\n\n        // Assert\n        Assert.NotNull(actual);\n        await sutProvider.GetDependency<ICreateProviderCommand>()\n            .Received(Quantity.Exactly(1))\n            .CreateResellerAsync(\n                Arg.Is<Provider>(x => x.Type == ProviderType.Reseller));\n    }\n\n    [BitAutoData]\n    [SutProviderCustomize]\n    [Theory]\n    public async Task CreateResellerAsync_RedirectsToExpectedPage_AfterCreatingProvider(\n        CreateResellerProviderModel model,\n        Guid expectedProviderId,\n        SutProvider<ProvidersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICreateProviderCommand>()\n            .When(x =>\n                x.CreateResellerAsync(\n                    Arg.Is<Provider>(y => y.Type == ProviderType.Reseller)))\n            .Do(callInfo =>\n            {\n                var providerArgument = callInfo.ArgAt<Provider>(0);\n                providerArgument.Id = expectedProviderId;\n            });\n\n        // Act\n        var actual = await sutProvider.Sut.CreateReseller(model);\n\n        // Assert\n        Assert.NotNull(actual);\n        Assert.IsType<RedirectToActionResult>(actual);\n        var actualResult = (RedirectToActionResult)actual;\n        Assert.Equal(\"Edit\", actualResult.ActionName);\n        Assert.Null(actualResult.ControllerName);\n        Assert.Equal(expectedProviderId, actualResult.RouteValues[\"Id\"]);\n    }\n    #endregion\n}\n"
  },
  {
    "path": "test/Admin.Test/Billing/Controllers/SubscriptionDiscountsControllerTests.cs",
    "content": "﻿using Bit.Admin.Billing.Controllers;\nusing Bit.Admin.Billing.Models;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Billing.Subscriptions.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.ViewFeatures;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Stripe;\n\nnamespace Admin.Test.Billing.Controllers;\n\n[ControllerCustomize(typeof(SubscriptionDiscountsController))]\n[SutProviderCustomize]\npublic class SubscriptionDiscountsControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task Index_DefaultParameters_ReturnsViewWithDiscounts(\n        List<SubscriptionDiscount> discounts,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .ListAsync(0, 25)\n            .Returns(discounts);\n\n        var result = await sutProvider.Sut.Index();\n\n        Assert.IsType<ViewResult>(result);\n        var viewResult = (ViewResult)result;\n        var model = Assert.IsType<SubscriptionDiscountPagedModel>(viewResult.Model);\n        Assert.Equal(25, model.Count);\n        Assert.Equal(1, model.Page);\n        Assert.Equal(discounts.Count, model.Items.Count);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Index_WithPagination_CalculatesCorrectSkip(\n        List<SubscriptionDiscount> discounts,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .ListAsync(50, 25)\n            .Returns(discounts);\n\n        var result = await sutProvider.Sut.Index(page: 3, count: 25);\n\n        await sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .Received(1)\n            .ListAsync(50, 25);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Index_WithInvalidPage_DefaultsToPage1(\n        List<SubscriptionDiscount> discounts,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .ListAsync(0, 25)\n            .Returns(discounts);\n\n        var result = await sutProvider.Sut.Index(page: -1, count: 25);\n\n        await sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .Received(1)\n            .ListAsync(0, 25);\n    }\n\n    [Theory, BitAutoData]\n    public void Create_Get_ReturnsViewWithEmptyModel(\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var result = sutProvider.Sut.Create();\n\n        Assert.IsType<ViewResult>(result);\n        var viewResult = (ViewResult)result;\n        var model = Assert.IsType<CreateSubscriptionDiscountModel>(viewResult.Model);\n        Assert.False(model.IsImported);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportCoupon_ValidCoupon_ReturnsViewWithStripeProperties(\n        CreateSubscriptionDiscountModel model,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var stripeCoupon = new Stripe.Coupon\n        {\n            Name = \"Test Coupon\",\n            PercentOff = 25,\n            Duration = \"once\"\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns((SubscriptionDiscount?)null);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCouponAsync(Arg.Any<string>(), Arg.Any<CouponGetOptions>())\n            .Returns(stripeCoupon);\n\n        var result = await sutProvider.Sut.ImportCoupon(model);\n\n        Assert.IsType<ViewResult>(result);\n        var viewResult = (ViewResult)result;\n        Assert.Equal(\"Create\", viewResult.ViewName);\n        var returnedModel = Assert.IsType<CreateSubscriptionDiscountModel>(viewResult.Model);\n        Assert.Equal(stripeCoupon.Name, returnedModel.Name);\n        Assert.Equal(stripeCoupon.PercentOff, returnedModel.PercentOff);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportCoupon_ValidCoupon_SetsIsImportedToTrue(\n        CreateSubscriptionDiscountModel model,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var stripeCoupon = new Stripe.Coupon\n        {\n            Name = \"Test Coupon\",\n            PercentOff = 25,\n            Duration = \"once\"\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns((SubscriptionDiscount?)null);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCouponAsync(Arg.Any<string>(), Arg.Any<CouponGetOptions>())\n            .Returns(stripeCoupon);\n\n        // Ensure IsImported starts as false\n        model.IsImported = false;\n\n        var result = await sutProvider.Sut.ImportCoupon(model);\n\n        var viewResult = Assert.IsType<ViewResult>(result);\n        var returnedModel = Assert.IsType<CreateSubscriptionDiscountModel>(viewResult.Model);\n        Assert.True(returnedModel.IsImported, \"IsImported should be set to true after successful import\");\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportCoupon_CouponWithProductRestrictions_MapsProductIds(\n        CreateSubscriptionDiscountModel model,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var productIds = new List<string> { \"prod_test1\", \"prod_test2\", \"prod_test3\" };\n        var stripeCoupon = new Stripe.Coupon\n        {\n            Name = \"Test Coupon\",\n            PercentOff = 25,\n            Duration = \"once\",\n            AppliesTo = new Stripe.CouponAppliesTo\n            {\n                Products = productIds\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns((SubscriptionDiscount?)null);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCouponAsync(Arg.Any<string>(), Arg.Any<CouponGetOptions>())\n            .Returns(stripeCoupon);\n\n        var products = new List<Stripe.Product>\n        {\n            new() { Id = \"prod_test1\", Name = \"Test Product 1\" },\n            new() { Id = \"prod_test2\", Name = \"Test Product 2\" },\n            new() { Id = \"prod_test3\", Name = \"Test Product 3\" }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListProductsAsync(Arg.Is<ProductListOptions>(o =>\n                o.Ids != null &&\n                o.Ids.Count == 3 &&\n                o.Ids.Contains(\"prod_test1\") &&\n                o.Ids.Contains(\"prod_test2\") &&\n                o.Ids.Contains(\"prod_test3\")))\n            .Returns(products);\n\n        var result = await sutProvider.Sut.ImportCoupon(model);\n\n        Assert.IsType<ViewResult>(result);\n        var viewResult = (ViewResult)result;\n        var returnedModel = Assert.IsType<CreateSubscriptionDiscountModel>(viewResult.Model);\n        Assert.NotNull(returnedModel.AppliesToProducts);\n        Assert.Equal(3, returnedModel.AppliesToProducts.Count);\n        Assert.Equal(\"Test Product 1\", returnedModel.AppliesToProducts[\"prod_test1\"]);\n        Assert.Equal(\"Test Product 2\", returnedModel.AppliesToProducts[\"prod_test2\"]);\n        Assert.Equal(\"Test Product 3\", returnedModel.AppliesToProducts[\"prod_test3\"]);\n\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .ListProductsAsync(Arg.Is<ProductListOptions>(o => o.Ids != null && o.Ids.Count == 3));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportCoupon_EmptyCouponId_ReturnsViewWithError(\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var model = new CreateSubscriptionDiscountModel { StripeCouponId = \"\" };\n        sutProvider.Sut.ModelState.AddModelError(nameof(model.StripeCouponId), \"The Stripe Coupon ID field is required.\");\n\n        var result = await sutProvider.Sut.ImportCoupon(model);\n\n        Assert.IsType<ViewResult>(result);\n        var viewResult = (ViewResult)result;\n        Assert.False(sutProvider.Sut.ModelState.IsValid);\n        Assert.Contains(\"required\", sutProvider.Sut.ModelState[nameof(model.StripeCouponId)]!.Errors[0].ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportCoupon_DuplicateCoupon_ReturnsViewWithError(\n        CreateSubscriptionDiscountModel model,\n        SubscriptionDiscount existingDiscount,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns(existingDiscount);\n\n        var result = await sutProvider.Sut.ImportCoupon(model);\n\n        Assert.IsType<ViewResult>(result);\n        var viewResult = (ViewResult)result;\n        Assert.False(sutProvider.Sut.ModelState.IsValid);\n        Assert.Contains(\"already been imported\", sutProvider.Sut.ModelState[nameof(model.StripeCouponId)]!.Errors[0].ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportCoupon_StripeApiError_ReturnsViewWithError(\n        CreateSubscriptionDiscountModel model,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns((SubscriptionDiscount?)null);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCouponAsync(Arg.Any<string>(), Arg.Any<CouponGetOptions>())\n            .Throws(new StripeException());\n\n        var result = await sutProvider.Sut.ImportCoupon(model);\n\n        Assert.IsType<ViewResult>(result);\n        Assert.False(sutProvider.Sut.ModelState.IsValid);\n        Assert.Contains(\"error occurred\", sutProvider.Sut.ModelState[nameof(model.StripeCouponId)]!.Errors[0].ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportCoupon_StripeResourceMissingError_ReturnsViewWithSpecificError(\n        CreateSubscriptionDiscountModel model,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns((SubscriptionDiscount?)null);\n\n        var stripeError = new StripeError { Code = \"resource_missing\" };\n        var stripeException = new StripeException { StripeError = stripeError };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCouponAsync(Arg.Any<string>(), Arg.Any<CouponGetOptions>())\n            .Throws(stripeException);\n\n        var result = await sutProvider.Sut.ImportCoupon(model);\n\n        Assert.IsType<ViewResult>(result);\n        Assert.False(sutProvider.Sut.ModelState.IsValid);\n        Assert.Contains(\"not found in Stripe\", sutProvider.Sut.ModelState[nameof(model.StripeCouponId)]!.Errors[0].ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportCoupon_WithDurationInMonths_ConvertsToInt(\n        CreateSubscriptionDiscountModel model,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var stripeCoupon = new Stripe.Coupon\n        {\n            Name = \"Test Coupon\",\n            PercentOff = 25,\n            Duration = \"repeating\",\n            DurationInMonths = 12L\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns((SubscriptionDiscount?)null);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCouponAsync(Arg.Any<string>(), Arg.Any<CouponGetOptions>())\n            .Returns(stripeCoupon);\n\n        var result = await sutProvider.Sut.ImportCoupon(model);\n\n        var viewResult = (ViewResult)result;\n        var returnedModel = Assert.IsType<CreateSubscriptionDiscountModel>(viewResult.Model);\n        Assert.Equal(12, returnedModel.DurationInMonths);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Create_ValidModel_CreatesDiscountAndRedirects(\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            StripeCouponId = \"TEST123\",\n            Name = \"Test Coupon\",\n            PercentOff = 25,\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date.AddMonths(1),\n            RestrictToNewUsersOnly = false,\n            IsImported = true\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns((SubscriptionDiscount?)null);\n\n        sutProvider.Sut.ModelState.Clear();\n        var tempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());\n        sutProvider.Sut.TempData = tempData;\n\n        var result = await sutProvider.Sut.Create(model);\n\n        Assert.IsType<RedirectToActionResult>(result);\n        var redirectResult = (RedirectToActionResult)result;\n        Assert.Equal(nameof(SubscriptionDiscountsController.Index), redirectResult.ActionName);\n\n        await sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Is<SubscriptionDiscount>(d =>\n                d.StripeCouponId == model.StripeCouponId &&\n                d.Name == model.Name &&\n                d.StartDate == model.StartDate &&\n                d.EndDate == model.EndDate &&\n                d.AudienceType == DiscountAudienceType.AllUsers));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Create_WithRestrictToNewUsersOnly_SetsCorrectAudienceType(\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            StripeCouponId = \"TEST123\",\n            Name = \"Test Coupon\",\n            PercentOff = 25,\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date.AddMonths(1),\n            RestrictToNewUsersOnly = true,\n            IsImported = true\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns((SubscriptionDiscount?)null);\n\n        sutProvider.Sut.ModelState.Clear();\n        var tempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());\n        sutProvider.Sut.TempData = tempData;\n\n        var result = await sutProvider.Sut.Create(model);\n\n        await sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Is<SubscriptionDiscount>(d =>\n                d.AudienceType == DiscountAudienceType.UserHasNoPreviousSubscriptions));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Create_NotImported_ReturnsViewWithError(\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            StripeCouponId = \"TEST123\",\n            Name = null\n        };\n\n        var result = await sutProvider.Sut.Create(model);\n\n        Assert.IsType<ViewResult>(result);\n        Assert.False(sutProvider.Sut.ModelState.IsValid);\n        Assert.Contains(\"import the coupon\", sutProvider.Sut.ModelState[string.Empty]!.Errors[0].ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Create_DuplicateCoupon_ReturnsViewWithError(\n        SubscriptionDiscount existingDiscount,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            StripeCouponId = \"TEST123\",\n            Name = \"Test Coupon\",\n            PercentOff = 25,\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date.AddMonths(1),\n            IsImported = true\n        };\n\n        // Simulate race condition: another admin imported the same coupon between import and save\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByStripeCouponIdAsync(model.StripeCouponId)\n            .Returns(existingDiscount);\n\n        sutProvider.Sut.ModelState.Clear();\n\n        var result = await sutProvider.Sut.Create(model);\n\n        Assert.IsType<ViewResult>(result);\n        Assert.False(sutProvider.Sut.ModelState.IsValid);\n        Assert.Contains(\"already been imported\", sutProvider.Sut.ModelState[nameof(model.StripeCouponId)]!.Errors[0].ErrorMessage);\n\n        // Verify CreateAsync was NOT called since we detected the duplicate\n        await sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .DidNotReceive()\n            .CreateAsync(Arg.Any<SubscriptionDiscount>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Create_RepositoryThrowsException_ReturnsViewWithError(\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            StripeCouponId = \"TEST123\",\n            Name = \"Test Coupon\",\n            PercentOff = 25,\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date.AddMonths(1),\n            IsImported = true\n        };\n\n        sutProvider.Sut.ModelState.Clear();\n        var tempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());\n        sutProvider.Sut.TempData = tempData;\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .CreateAsync(Arg.Any<SubscriptionDiscount>())\n            .Throws(new Exception(\"Database error\"));\n\n        var result = await sutProvider.Sut.Create(model);\n\n        Assert.IsType<ViewResult>(result);\n        Assert.False(sutProvider.Sut.ModelState.IsValid);\n        Assert.Contains(\"error occurred\", sutProvider.Sut.ModelState[string.Empty]!.Errors[0].ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Edit_Get_ReturnsViewWithModel(\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(discount.Id)\n            .Returns(discount);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListProductsAsync(Arg.Any<ProductListOptions>())\n            .Returns(new List<Stripe.Product>());\n\n        var result = await sutProvider.Sut.Edit(discount.Id);\n\n        Assert.IsType<ViewResult>(result);\n        var viewResult = (ViewResult)result;\n        var model = Assert.IsType<EditSubscriptionDiscountModel>(viewResult.Model);\n        Assert.Equal(discount.Id, model.Id);\n        Assert.Equal(discount.StripeCouponId, model.StripeCouponId);\n        Assert.Equal(discount.StartDate, model.StartDate);\n        Assert.Equal(discount.EndDate, model.EndDate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Edit_Get_WithStripeProducts_PopulatesAppliesToProducts(\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        discount.StripeProductIds = new List<string> { \"prod_1\", \"prod_2\" };\n        var stripeProducts = new List<Stripe.Product>\n        {\n            new() { Id = \"prod_1\", Name = \"Product One\" },\n            new() { Id = \"prod_2\", Name = \"Product Two\" }\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(discount.Id)\n            .Returns(discount);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListProductsAsync(Arg.Any<ProductListOptions>())\n            .Returns(stripeProducts);\n\n        var result = await sutProvider.Sut.Edit(discount.Id);\n\n        var viewResult = Assert.IsType<ViewResult>(result);\n        var model = Assert.IsType<EditSubscriptionDiscountModel>(viewResult.Model);\n        Assert.NotNull(model.AppliesToProducts);\n        Assert.Equal(2, model.AppliesToProducts.Count);\n        Assert.Equal(\"Product One\", model.AppliesToProducts[\"prod_1\"]);\n        Assert.Equal(\"Product Two\", model.AppliesToProducts[\"prod_2\"]);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Edit_Get_WhenStripeProductLookupFails_StillReturnsView(\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        discount.StripeProductIds = new List<string> { \"prod_1\" };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(discount.Id)\n            .Returns(discount);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListProductsAsync(Arg.Any<ProductListOptions>())\n            .Throws(new StripeException());\n\n        var result = await sutProvider.Sut.Edit(discount.Id);\n\n        var viewResult = Assert.IsType<ViewResult>(result);\n        var model = Assert.IsType<EditSubscriptionDiscountModel>(viewResult.Model);\n        Assert.Null(model.AppliesToProducts);\n        Assert.False(sutProvider.Sut.ModelState.IsValid);\n        Assert.Contains(\"Failed to fetch\", sutProvider.Sut.ModelState[string.Empty]!.Errors[0].ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Edit_Get_WhenNotFound_ReturnsNotFound(\n        Guid id,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(id)\n            .Returns((SubscriptionDiscount?)null);\n\n        var result = await sutProvider.Sut.Edit(id);\n\n        Assert.IsType<NotFoundResult>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Edit_Post_ValidModel_UpdatesBitwardenFieldsAndRedirects(\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var model = new EditSubscriptionDiscountModel\n        {\n            StartDate = DateTime.UtcNow.Date.AddDays(1),\n            EndDate = DateTime.UtcNow.Date.AddMonths(2),\n            RestrictToNewUsersOnly = true\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(discount.Id)\n            .Returns(discount);\n\n        var tempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());\n        sutProvider.Sut.TempData = tempData;\n\n        var result = await sutProvider.Sut.Edit(discount.Id, model);\n\n        Assert.IsType<RedirectToActionResult>(result);\n        var redirectResult = (RedirectToActionResult)result;\n        Assert.Equal(nameof(SubscriptionDiscountsController.Index), redirectResult.ActionName);\n\n        await sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<SubscriptionDiscount>(d =>\n                d.StartDate == model.StartDate &&\n                d.EndDate == model.EndDate &&\n                d.AudienceType == DiscountAudienceType.UserHasNoPreviousSubscriptions));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Edit_Post_ValidModel_DoesNotUpdateStripeFields(\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        var originalStripeCouponId = discount.StripeCouponId;\n        var originalPercentOff = discount.PercentOff;\n        var originalAmountOff = discount.AmountOff;\n        var originalDuration = discount.Duration;\n\n        var model = new EditSubscriptionDiscountModel\n        {\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date.AddMonths(1),\n            RestrictToNewUsersOnly = false\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(discount.Id)\n            .Returns(discount);\n\n        var tempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());\n        sutProvider.Sut.TempData = tempData;\n\n        await sutProvider.Sut.Edit(discount.Id, model);\n\n        await sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<SubscriptionDiscount>(d =>\n                d.StripeCouponId == originalStripeCouponId &&\n                d.PercentOff == originalPercentOff &&\n                d.AmountOff == originalAmountOff &&\n                d.Duration == originalDuration));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Edit_Post_InvalidModelState_ReturnsView(\n        Guid id,\n        EditSubscriptionDiscountModel model,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.Sut.ModelState.AddModelError(nameof(model.EndDate), \"End Date must be on or after Start Date.\");\n\n        var result = await sutProvider.Sut.Edit(id, model);\n\n        Assert.IsType<ViewResult>(result);\n        await sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .DidNotReceive()\n            .ReplaceAsync(Arg.Any<SubscriptionDiscount>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Edit_Post_RepositoryThrowsException_ReturnsViewWithError(\n        SubscriptionDiscount discount,\n        EditSubscriptionDiscountModel model,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(discount.Id)\n            .Returns(discount);\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .ReplaceAsync(Arg.Any<SubscriptionDiscount>())\n            .Throws(new Exception(\"Database error\"));\n\n        var result = await sutProvider.Sut.Edit(discount.Id, model);\n\n        Assert.IsType<ViewResult>(result);\n        Assert.False(sutProvider.Sut.ModelState.IsValid);\n        Assert.Contains(\"error occurred\", sutProvider.Sut.ModelState[string.Empty]!.Errors[0].ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Edit_Post_WhenNotFound_ReturnsNotFound(\n        Guid id,\n        EditSubscriptionDiscountModel model,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(id)\n            .Returns((SubscriptionDiscount?)null);\n\n        var result = await sutProvider.Sut.Edit(id, model);\n\n        Assert.IsType<NotFoundResult>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Delete_Post_DeletesDiscountAndRedirectsToIndex(\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(discount.Id)\n            .Returns(discount);\n\n        var tempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());\n        sutProvider.Sut.TempData = tempData;\n\n        var result = await sutProvider.Sut.Delete(discount.Id);\n\n        Assert.IsType<RedirectToActionResult>(result);\n        var redirectResult = (RedirectToActionResult)result;\n        Assert.Equal(nameof(SubscriptionDiscountsController.Index), redirectResult.ActionName);\n\n        await sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .Received(1)\n            .DeleteAsync(discount);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Delete_Post_RepositoryThrowsException_RedirectsToEditWithError(\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(discount.Id)\n            .Returns(discount);\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .DeleteAsync(discount)\n            .Throws(new Exception(\"Database error\"));\n\n        var tempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>());\n        sutProvider.Sut.TempData = tempData;\n\n        var result = await sutProvider.Sut.Delete(discount.Id);\n\n        var redirectResult = Assert.IsType<RedirectToActionResult>(result);\n        Assert.Equal(nameof(SubscriptionDiscountsController.Edit), redirectResult.ActionName);\n        Assert.Contains(\"attempting to delete\", sutProvider.Sut.TempData[\"Error\"]!.ToString());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Delete_Post_WhenNotFound_ReturnsNotFound(\n        Guid id,\n        SutProvider<SubscriptionDiscountsController> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetByIdAsync(id)\n            .Returns((SubscriptionDiscount?)null);\n\n        var result = await sutProvider.Sut.Delete(id);\n\n        Assert.IsType<NotFoundResult>(result);\n    }\n}\n"
  },
  {
    "path": "test/Admin.Test/Billing/Models/CreateSubscriptionDiscountModelTests.cs",
    "content": "﻿using Bit.Admin.Billing.Models;\nusing Bit.Core.Billing.Enums;\n\nnamespace Admin.Test.Billing.Models;\n\npublic class CreateSubscriptionDiscountModelTests\n{\n    [Fact]\n    public void AudienceType_WhenCheckboxUnchecked_ReturnsAllUsers()\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            RestrictToNewUsersOnly = false\n        };\n\n        Assert.Equal(DiscountAudienceType.AllUsers, model.AudienceType);\n    }\n\n    [Fact]\n    public void AudienceType_WhenCheckboxChecked_ReturnsUserHasNoPreviousSubscriptions()\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            RestrictToNewUsersOnly = true\n        };\n\n        Assert.Equal(DiscountAudienceType.UserHasNoPreviousSubscriptions, model.AudienceType);\n    }\n\n    [Fact]\n    public void Validate_WhenEndDateBeforeStartDate_ReturnsError()\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            StartDate = DateTime.UtcNow.Date.AddDays(10),\n            EndDate = DateTime.UtcNow.Date\n        };\n\n        var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(model);\n        var results = model.Validate(validationContext).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(\"End Date must be on or after Start Date\", results[0].ErrorMessage);\n        Assert.Contains(nameof(model.EndDate), results[0].MemberNames);\n    }\n\n    [Fact]\n    public void Validate_WhenEndDateEqualsStartDate_NoError()\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date\n        };\n\n        var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(model);\n        var results = model.Validate(validationContext).ToList();\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_WhenEndDateAfterStartDate_NoError()\n    {\n        var model = new CreateSubscriptionDiscountModel\n        {\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date.AddDays(10)\n        };\n\n        var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(model);\n        var results = model.Validate(validationContext).ToList();\n\n        Assert.Empty(results);\n    }\n}\n"
  },
  {
    "path": "test/Admin.Test/Billing/Models/EditSubscriptionDiscountModelTests.cs",
    "content": "﻿using Bit.Admin.Billing.Models;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Subscriptions.Entities;\n\nnamespace Admin.Test.Billing.Models;\n\npublic class EditSubscriptionDiscountModelTests\n{\n    [Fact]\n    public void AudienceType_WhenRestrictToNewUsersOnly_ReturnsUserHasNoPreviousSubscriptions()\n    {\n        var model = new EditSubscriptionDiscountModel\n        {\n            RestrictToNewUsersOnly = true\n        };\n\n        Assert.Equal(DiscountAudienceType.UserHasNoPreviousSubscriptions, model.AudienceType);\n    }\n\n    [Fact]\n    public void AudienceType_WhenNotRestricted_ReturnsAllUsers()\n    {\n        var model = new EditSubscriptionDiscountModel\n        {\n            RestrictToNewUsersOnly = false\n        };\n\n        Assert.Equal(DiscountAudienceType.AllUsers, model.AudienceType);\n    }\n\n    [Fact]\n    public void Validate_WhenEndDateBeforeStartDate_ReturnsError()\n    {\n        var model = new EditSubscriptionDiscountModel\n        {\n            StartDate = DateTime.UtcNow.Date.AddDays(10),\n            EndDate = DateTime.UtcNow.Date\n        };\n\n        var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(model);\n        var results = model.Validate(validationContext).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(\"End Date must be on or after Start Date\", results[0].ErrorMessage);\n        Assert.Contains(nameof(model.EndDate), results[0].MemberNames);\n    }\n\n    [Fact]\n    public void Validate_WhenEndDateEqualsStartDate_NoError()\n    {\n        var model = new EditSubscriptionDiscountModel\n        {\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date\n        };\n\n        var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(model);\n        var results = model.Validate(validationContext).ToList();\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_WhenEndDateAfterStartDate_NoError()\n    {\n        var model = new EditSubscriptionDiscountModel\n        {\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date.AddDays(10)\n        };\n\n        var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(model);\n        var results = model.Validate(validationContext).ToList();\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Constructor_FromEntity_MapsAllProperties()\n    {\n        var discount = new SubscriptionDiscount\n        {\n            Id = Guid.NewGuid(),\n            StripeCouponId = \"COUPON123\",\n            Name = \"Test Coupon\",\n            PercentOff = 25m,\n            AmountOff = null,\n            Currency = \"usd\",\n            Duration = \"once\",\n            DurationInMonths = null,\n            StripeProductIds = new List<string> { \"prod_1\", \"prod_2\" },\n            StartDate = new DateTime(2025, 1, 1),\n            EndDate = new DateTime(2025, 12, 31),\n            AudienceType = DiscountAudienceType.AllUsers\n        };\n\n        var model = new EditSubscriptionDiscountModel(discount);\n\n        Assert.Equal(discount.Id, model.Id);\n        Assert.Equal(discount.StripeCouponId, model.StripeCouponId);\n        Assert.Equal(discount.Name, model.Name);\n        Assert.Equal(discount.PercentOff, model.PercentOff);\n        Assert.Equal(discount.AmountOff, model.AmountOff);\n        Assert.Equal(discount.Currency, model.Currency);\n        Assert.Equal(discount.Duration, model.Duration);\n        Assert.Equal(discount.DurationInMonths, model.DurationInMonths);\n        Assert.Equal(discount.StripeProductIds, model.StripeProductIds);\n        Assert.Equal(discount.StartDate, model.StartDate);\n        Assert.Equal(discount.EndDate, model.EndDate);\n    }\n\n    [Fact]\n    public void Constructor_FromEntity_WhenAudienceTypeIsUserHasNoPreviousSubscriptions_SetsRestrictToNewUsersOnlyTrue()\n    {\n        var discount = new SubscriptionDiscount\n        {\n            StripeCouponId = \"COUPON123\",\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date.AddMonths(1),\n            AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions\n        };\n\n        var model = new EditSubscriptionDiscountModel(discount);\n\n        Assert.True(model.RestrictToNewUsersOnly);\n    }\n\n    [Fact]\n    public void Constructor_FromEntity_WhenAudienceTypeIsAllUsers_SetsRestrictToNewUsersOnlyFalse()\n    {\n        var discount = new SubscriptionDiscount\n        {\n            StripeCouponId = \"COUPON123\",\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow.Date,\n            EndDate = DateTime.UtcNow.Date.AddMonths(1),\n            AudienceType = DiscountAudienceType.AllUsers\n        };\n\n        var model = new EditSubscriptionDiscountModel(discount);\n\n        Assert.False(model.RestrictToNewUsersOnly);\n    }\n}\n"
  },
  {
    "path": "test/Admin.Test/Billing/Models/SubscriptionDiscountViewModelTests.cs",
    "content": "﻿using Bit.Admin.Billing.Models;\nusing Bit.Core.Billing.Enums;\n\nnamespace Admin.Test.Billing.Models;\n\npublic class SubscriptionDiscountViewModelTests\n{\n    [Fact]\n    public void DiscountDisplay_WithPercentOff_ReturnsFormattedPercent()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            PercentOff = 25m\n        };\n\n        Assert.Equal(\"25% off\", model.DiscountDisplay);\n    }\n\n    [Fact]\n    public void DiscountDisplay_WithDecimalPercentOff_ReturnsFormattedPercentWithDecimals()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            PercentOff = 33.5m\n        };\n\n        Assert.Equal(\"33.5% off\", model.DiscountDisplay);\n    }\n\n    [Fact]\n    public void DiscountDisplay_WithWholeNumberPercentOff_ReturnsFormattedPercentWithoutDecimals()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            PercentOff = 50.00m\n        };\n\n        Assert.Equal(\"50% off\", model.DiscountDisplay);\n    }\n\n    [Fact]\n    public void DiscountDisplay_WithAmountOff_ReturnsFormattedDollar()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            AmountOff = 1000\n        };\n\n        Assert.Equal(\"$10 off\", model.DiscountDisplay);\n    }\n\n    [Fact]\n    public void DiscountDisplay_WithZeroAmountOff_ReturnsZero()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            AmountOff = 0\n        };\n\n        Assert.Equal(\"$0 off\", model.DiscountDisplay);\n    }\n\n    [Fact]\n    public void IsRestrictedToNewUsersOnly_WithMatchingAudienceType_ReturnsTrue()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions\n        };\n\n        Assert.True(model.IsRestrictedToNewUsersOnly);\n    }\n\n    [Fact]\n    public void IsRestrictedToNewUsersOnly_WithAllUsersAudienceType_ReturnsFalse()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            AudienceType = DiscountAudienceType.AllUsers\n        };\n\n        Assert.False(model.IsRestrictedToNewUsersOnly);\n    }\n\n    [Fact]\n    public void IsAvailableToAllUsers_WithAllUsersAudienceType_ReturnsTrue()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            AudienceType = DiscountAudienceType.AllUsers\n        };\n\n        Assert.True(model.IsAvailableToAllUsers);\n    }\n\n    [Fact]\n    public void IsAvailableToAllUsers_WithRestrictedAudienceType_ReturnsFalse()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions\n        };\n\n        Assert.False(model.IsAvailableToAllUsers);\n    }\n\n    [Fact]\n    public void IsActive_WhenWithinDateRange_ReturnsTrue()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            StartDate = DateTime.UtcNow.AddDays(-1),\n            EndDate = DateTime.UtcNow.AddDays(1)\n        };\n\n        Assert.True(model.IsActive);\n    }\n\n    [Fact]\n    public void IsActive_WhenBeforeStartDate_ReturnsFalse()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            StartDate = DateTime.UtcNow.AddDays(1),\n            EndDate = DateTime.UtcNow.AddDays(2)\n        };\n\n        Assert.False(model.IsActive);\n    }\n\n    [Fact]\n    public void IsActive_WhenAfterEndDate_ReturnsFalse()\n    {\n        var model = new SubscriptionDiscountViewModel\n        {\n            StartDate = DateTime.UtcNow.AddDays(-2),\n            EndDate = DateTime.UtcNow.AddDays(-1)\n        };\n\n        Assert.False(model.IsActive);\n    }\n\n    [Fact]\n    public void IsActive_WhenExactlyOnStartDate_ReturnsTrue()\n    {\n        var now = DateTime.UtcNow;\n        var model = new SubscriptionDiscountViewModel\n        {\n            StartDate = now,\n            EndDate = now.AddDays(1)\n        };\n\n        Assert.True(model.IsActive);\n    }\n\n    [Fact]\n    public void IsActive_WhenCurrentTimeIsOnEndDate_ReturnsTrue()\n    {\n        var now = DateTime.UtcNow;\n        var model = new SubscriptionDiscountViewModel\n        {\n            StartDate = now.AddDays(-1),\n            EndDate = now.AddSeconds(1)\n        };\n\n        Assert.True(model.IsActive);\n    }\n}\n"
  },
  {
    "path": "test/Admin.Test/GlobalUsings.cs",
    "content": "﻿global using Xunit;\n"
  },
  {
    "path": "test/Admin.Test/Models/UserViewModelTests.cs",
    "content": "﻿using Bit.Admin.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Admin.Test.Models;\n\npublic class UserViewModelTests\n{\n    [Theory]\n    [BitAutoData]\n    public void IsTwoFactorEnabled_GivenUserAndIsInLookup_WhenUserHasTwoFactorEnabled_ThenReturnsTrue(User user)\n    {\n        var lookup = new List<(Guid, bool)>\n        {\n            (user.Id, true)\n        };\n\n        var actual = UserViewModel.IsTwoFactorEnabled(user, lookup);\n\n        Assert.True(actual);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void IsTwoFactorEnabled_GivenUserAndIsInLookup_WhenUserDoesNotHaveTwoFactorEnabled_ThenReturnsFalse(User user)\n    {\n        var lookup = new List<(Guid, bool)>\n        {\n            (Guid.NewGuid(), true)\n        };\n\n        var actual = UserViewModel.IsTwoFactorEnabled(user, lookup);\n\n        Assert.False(actual);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void IsTwoFactorEnabled_GivenUserAndIsNotInLookup_WhenUserDoesNotHaveTwoFactorEnabled_ThenReturnsFalse(User user)\n    {\n        var lookup = new List<(Guid, bool)>();\n\n        var actual = UserViewModel.IsTwoFactorEnabled(user, lookup);\n\n        Assert.False(actual);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void MapUserViewModel_GivenUser_WhenPopulated_ThenMapsToUserViewModel(User user)\n    {\n        var actual = UserViewModel.MapViewModel(user, true);\n\n        Assert.Equal(actual.Id, user.Id);\n        Assert.Equal(actual.Email, user.Email);\n        Assert.Equal(actual.CreationDate, user.CreationDate);\n        Assert.Equal(actual.PremiumExpirationDate, user.PremiumExpirationDate);\n        Assert.Equal(actual.Premium, user.Premium);\n        Assert.Equal(actual.MaxStorageGb, user.MaxStorageGb);\n        Assert.Equal(actual.EmailVerified, user.EmailVerified);\n        Assert.True(actual.TwoFactorEnabled);\n        Assert.Equal(actual.AccountRevisionDate, user.AccountRevisionDate);\n        Assert.Equal(actual.RevisionDate, user.RevisionDate);\n        Assert.Equal(actual.LastEmailChangeDate, user.LastEmailChangeDate);\n        Assert.Equal(actual.LastKdfChangeDate, user.LastKdfChangeDate);\n        Assert.Equal(actual.LastKeyRotationDate, user.LastKeyRotationDate);\n        Assert.Equal(actual.LastPasswordChangeDate, user.LastPasswordChangeDate);\n        Assert.Equal(actual.Gateway, user.Gateway);\n        Assert.Equal(actual.GatewayCustomerId, user.GatewayCustomerId);\n        Assert.Equal(actual.GatewaySubscriptionId, user.GatewaySubscriptionId);\n        Assert.Equal(actual.LicenseKey, user.LicenseKey);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void MapUserViewModel_GivenUserWithTwoFactorEnabled_WhenPopulated_ThenMapsToUserViewModel(User user)\n    {\n        var lookup = new List<(Guid, bool)> { (user.Id, true) };\n\n        var actual = UserViewModel.MapViewModel(user, lookup, false);\n\n        Assert.True(actual.TwoFactorEnabled);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void MapUserViewModel_GivenUserWithoutTwoFactorEnabled_WhenPopulated_ThenTwoFactorIsEnabled(User user)\n    {\n        var lookup = new List<(Guid, bool)> { (user.Id, false) };\n\n        var actual = UserViewModel.MapViewModel(user, lookup, false);\n\n        Assert.False(actual.TwoFactorEnabled);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void MapUserViewModel_GivenUser_WhenNotInLookUpList_ThenTwoFactorIsDisabled(User user)\n    {\n        var lookup = new List<(Guid, bool)> { (Guid.NewGuid(), true) };\n\n        var actual = UserViewModel.MapViewModel(user, lookup, false);\n\n        Assert.False(actual.TwoFactorEnabled);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void MapUserViewModel_WithVerifiedDomain_ReturnsUserViewModel(User user)\n    {\n\n        var verifiedDomain = true;\n\n        var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);\n\n        Assert.True(actual.ClaimedAccount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void MapUserViewModel_WithoutVerifiedDomain_ReturnsUserViewModel(User user)\n    {\n        var verifiedDomain = false;\n\n        var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);\n\n        Assert.False(actual.ClaimedAccount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void MapUserViewModel_WithNullVerifiedDomain_ReturnsUserViewModel(User user)\n    {\n        var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), null);\n\n        Assert.Null(actual.ClaimedAccount);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/GroupsControllerPerformanceTests.cs",
    "content": "﻿using System.Net;\nusing System.Text;\nusing System.Text.Json;\nusing AutoMapper;\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Request;\nusing Bit.Core.Entities;\nusing Bit.Seeder.Recipes;\nusing Bit.Seeder.Services;\nusing Microsoft.AspNetCore.Identity;\nusing Xunit;\nusing Xunit.Abstractions;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class GroupsControllerPerformanceTests(ITestOutputHelper testOutputHelper)\n{\n    /// <summary>\n    /// Tests PUT /organizations/{orgId}/groups/{id}\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10, 5)]\n    //[InlineData(100, 10)]\n    //[InlineData(1000, 20)]\n    public async Task UpdateGroup_WithUsersAndCollections(int userCount, int collectionCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = new NoOpManglerService();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var collectionsSeeder = new CollectionsRecipe(db);\n        var groupsSeeder = new GroupsRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: userCount);\n\n        var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();\n        var collectionIds = collectionsSeeder.Seed(orgId, collectionCount, orgUserIds, 0);\n        var groupIds = groupsSeeder.Seed(orgId, 1, orgUserIds, 0);\n\n        var groupId = groupIds.First();\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var updateRequest = new GroupRequestModel\n        {\n            Name = \"Updated Group Name\",\n            Collections = collectionIds.Select(c => new SelectionReadOnlyRequestModel { Id = c, ReadOnly = false, HidePasswords = false, Manage = false }),\n            Users = orgUserIds\n        };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(updateRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PutAsync($\"/organizations/{orgId}/groups/{groupId}\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"PUT /organizations/{{orgId}}/groups/{{id}} - Users: {orgUserIds.Count}; Collections: {collectionIds.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerAutoConfirmTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class OrganizationUserControllerAutoConfirmTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private const string _mockEncryptedString = \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private string _ownerEmail = null!;\n\n    public OrganizationUserControllerAutoConfirmTests(ApiApplicationFactory apiFactory)\n    {\n        _factory = apiFactory;\n        _factory.SubstituteService<IFeatureService>(featureService =>\n        {\n            featureService\n                .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n                .Returns(true);\n        });\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"org-owner-{Guid.NewGuid()}@example.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n    }\n\n    [Fact]\n    public async Task AutoConfirm_WhenUserCannotManageOtherUsers_ThenShouldReturnForbidden()\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);\n\n        organization.UseAutomaticUserConfirmation = true;\n\n        await _factory.GetService<IOrganizationRepository>()\n            .UpsertAsync(organization);\n\n        var testKey = $\"test-key-{Guid.NewGuid()}\";\n\n        var userToConfirmEmail = $\"org-user-to-confirm-{Guid.NewGuid()}@example.com\";\n        await _factory.LoginWithNewAccount(userToConfirmEmail);\n\n        var (confirmingUserEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, organization.Id, OrganizationUserType.User);\n        await _loginHelper.LoginAsync(confirmingUserEmail);\n\n        var organizationUser = await OrganizationTestHelpers.CreateUserAsync(\n            _factory,\n            organization.Id,\n            userToConfirmEmail,\n            OrganizationUserType.User,\n            false,\n            new Permissions { ManageUsers = false },\n            OrganizationUserStatusType.Accepted);\n\n        var result = await _client.PostAsJsonAsync($\"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm\",\n            new OrganizationUserConfirmRequestModel\n            {\n                Key = testKey,\n                DefaultUserCollectionName = _mockEncryptedString\n            });\n\n        Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);\n\n        await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);\n    }\n\n    [Fact]\n    public async Task AutoConfirm_WhenOwnerConfirmsValidUser_ThenShouldReturnNoContent()\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);\n\n        organization.UseAutomaticUserConfirmation = true;\n\n        await _factory.GetService<IOrganizationRepository>()\n            .UpsertAsync(organization);\n\n        var testKey = $\"test-key-{Guid.NewGuid()}\";\n\n        await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy\n        {\n            OrganizationId = organization.Id,\n            Type = PolicyType.AutomaticUserConfirmation,\n            Enabled = true\n        });\n\n        await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy\n        {\n            OrganizationId = organization.Id,\n            Type = PolicyType.OrganizationDataOwnership,\n            Enabled = true\n        });\n\n        var userToConfirmEmail = $\"org-user-to-confirm-{Guid.NewGuid()}@example.com\";\n        await _factory.LoginWithNewAccount(userToConfirmEmail);\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n        var organizationUser = await OrganizationTestHelpers.CreateUserAsync(\n            _factory,\n            organization.Id,\n            userToConfirmEmail,\n            OrganizationUserType.User,\n            false,\n            new Permissions(),\n            OrganizationUserStatusType.Accepted);\n\n        var result = await _client.PostAsJsonAsync($\"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm\",\n            new OrganizationUserConfirmRequestModel\n            {\n                Key = testKey,\n                DefaultUserCollectionName = _mockEncryptedString\n            });\n\n        Assert.Equal(HttpStatusCode.NoContent, result.StatusCode);\n\n        var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var confirmedUser = await orgUserRepository.GetByIdAsync(organizationUser.Id);\n        Assert.NotNull(confirmedUser);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);\n        Assert.Equal(testKey, confirmedUser.Key);\n\n        var collectionRepository = _factory.GetService<ICollectionRepository>();\n        var collections = await collectionRepository.GetManyByUserIdAsync(organizationUser.UserId!.Value);\n\n        Assert.NotEmpty(collections);\n        Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection));\n\n        await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);\n    }\n\n    [Fact]\n    public async Task AutoConfirm_WhenUserIsConfirmedMultipleTimes_ThenShouldSuccessAndOnlyConfirmOneUser()\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);\n\n        organization.UseAutomaticUserConfirmation = true;\n\n        await _factory.GetService<IOrganizationRepository>()\n            .UpsertAsync(organization);\n\n        var testKey = $\"test-key-{Guid.NewGuid()}\";\n\n        var userToConfirmEmail = $\"org-user-to-confirm-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(userToConfirmEmail);\n\n        await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy\n        {\n            OrganizationId = organization.Id,\n            Type = PolicyType.AutomaticUserConfirmation,\n            Enabled = true\n        });\n\n        await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy\n        {\n            OrganizationId = organization.Id,\n            Type = PolicyType.OrganizationDataOwnership,\n            Enabled = true\n        });\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var organizationUser = await OrganizationTestHelpers.CreateUserAsync(\n            _factory,\n            organization.Id,\n            userToConfirmEmail,\n            OrganizationUserType.User,\n            false,\n            new Permissions(),\n            OrganizationUserStatusType.Accepted);\n\n        var results = new List<HttpResponseMessage>();\n\n        foreach (var _ in Enumerable.Range(0, 10))\n        {\n            results.Add(await _client.PostAsJsonAsync($\"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm\",\n                new OrganizationUserConfirmRequestModel\n                {\n                    Key = testKey,\n                    DefaultUserCollectionName = _mockEncryptedString\n                }));\n        }\n\n        Assert.Contains(results, r => r.StatusCode == HttpStatusCode.NoContent);\n\n        var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var confirmedUser = await orgUserRepository.GetByIdAsync(organizationUser.Id);\n        Assert.NotNull(confirmedUser);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);\n        Assert.Equal(testKey, confirmedUser.Key);\n\n        var collections = await _factory.GetService<ICollectionRepository>()\n            .GetManyByUserIdAsync(organizationUser.UserId!.Value);\n        Assert.NotEmpty(collections);\n        // validates user only received one default collection\n        Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection));\n\n        await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Providers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class OrganizationUserControllerBulkRevokeTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private Organization _organization = null!;\n    private string _ownerEmail = null!;\n\n    public OrganizationUserControllerBulkRevokeTests(ApiApplicationFactory apiFactory)\n    {\n        _factory = apiFactory;\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"org-user-bulk-revoke-test-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseMonthly,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task BulkRevoke_Success()\n    {\n        var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Owner);\n\n        await _loginHelper.LoginAsync(ownerEmail);\n\n        var (_, orgUser1) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n        var (_, orgUser2) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [orgUser1.Id, orgUser2.Id]\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/revoke\", request);\n        var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n        Assert.NotNull(content);\n        Assert.Equal(2, content.Data.Count());\n        Assert.All(content.Data, r => Assert.Empty(r.Error));\n\n        var actualUsers = await organizationUserRepository.GetManyAsync([orgUser1.Id, orgUser2.Id]);\n        Assert.All(actualUsers, u => Assert.Equal(OrganizationUserStatusType.Revoked, u.Status));\n    }\n\n    [Fact]\n    public async Task BulkRevoke_AsAdmin_Success()\n    {\n        var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Admin);\n\n        await _loginHelper.LoginAsync(adminEmail);\n\n        var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [orgUser.Id]\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/revoke\", request);\n        var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n        Assert.NotNull(content);\n        Assert.Single(content.Data);\n        Assert.All(content.Data, r => Assert.Empty(r.Error));\n\n        var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(orgUser.Id);\n        Assert.NotNull(actualUser);\n        Assert.Equal(OrganizationUserStatusType.Revoked, actualUser.Status);\n    }\n\n    [Fact]\n    public async Task BulkRevoke_CannotRevokeSelf_ReturnsError()\n    {\n        var (userEmail, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Admin);\n\n        await _loginHelper.LoginAsync(userEmail);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [orgUser.Id]\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/revoke\", request);\n        var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n        Assert.NotNull(content);\n        Assert.Single(content.Data);\n        Assert.Contains(content.Data, r => r.Id == orgUser.Id && r.Error == \"You cannot revoke yourself.\");\n\n        var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(orgUser.Id);\n        Assert.NotNull(actualUser);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, actualUser.Status);\n    }\n\n    [Fact]\n    public async Task BulkRevoke_AlreadyRevoked_ReturnsError()\n    {\n        var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Owner);\n\n        await _loginHelper.LoginAsync(ownerEmail);\n\n        var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n\n        await organizationUserRepository.RevokeAsync(orgUser.Id);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [orgUser.Id]\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/revoke\", request);\n        var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n        Assert.NotNull(content);\n        Assert.Single(content.Data);\n        Assert.Contains(content.Data, r => r.Id == orgUser.Id && r.Error == \"Already revoked.\");\n\n        var actualUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);\n        Assert.NotNull(actualUser);\n        Assert.Equal(OrganizationUserStatusType.Revoked, actualUser.Status);\n    }\n\n    [Fact]\n    public async Task BulkRevoke_AdminCannotRevokeOwner_ReturnsError()\n    {\n        var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Admin);\n\n        await _loginHelper.LoginAsync(adminEmail);\n\n        var (_, ownerOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.Owner);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [ownerOrgUser.Id]\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/revoke\", request);\n        var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n        Assert.NotNull(content);\n        Assert.Single(content.Data);\n        Assert.Contains(content.Data, r => r.Id == ownerOrgUser.Id && r.Error == \"Only owners can revoke other owners.\");\n\n        var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(ownerOrgUser.Id);\n        Assert.NotNull(actualUser);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, actualUser.Status);\n    }\n\n    [Fact]\n    public async Task BulkRevoke_MixedResults()\n    {\n        var (ownerEmail, requestingOwner) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Owner);\n\n        await _loginHelper.LoginAsync(ownerEmail);\n\n        var (_, validOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n        var (_, alreadyRevokedOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n\n        await organizationUserRepository.RevokeAsync(alreadyRevokedOrgUser.Id);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [validOrgUser.Id, alreadyRevokedOrgUser.Id, requestingOwner.Id]\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/revoke\", request);\n        var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n        Assert.NotNull(content);\n        Assert.Equal(3, content.Data.Count());\n\n        Assert.Contains(content.Data, r => r.Id == validOrgUser.Id && r.Error == string.Empty);\n        Assert.Contains(content.Data, r => r.Id == alreadyRevokedOrgUser.Id && r.Error == \"Already revoked.\");\n        Assert.Contains(content.Data, r => r.Id == requestingOwner.Id && r.Error == \"You cannot revoke yourself.\");\n\n        var actualUsers = await organizationUserRepository.GetManyAsync([validOrgUser.Id, alreadyRevokedOrgUser.Id, requestingOwner.Id]);\n        Assert.Equal(OrganizationUserStatusType.Revoked, actualUsers.First(u => u.Id == validOrgUser.Id).Status);\n        Assert.Equal(OrganizationUserStatusType.Revoked, actualUsers.First(u => u.Id == alreadyRevokedOrgUser.Id).Status);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, actualUsers.First(u => u.Id == requestingOwner.Id).Status);\n    }\n\n    [Theory]\n    [InlineData(OrganizationUserType.User)]\n    [InlineData(OrganizationUserType.Custom)]\n    public async Task BulkRevoke_WithoutManageUsersPermission_ReturnsForbidden(OrganizationUserType organizationUserType)\n    {\n        var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, organizationUserType, new Permissions { ManageUsers = false });\n\n        await _loginHelper.LoginAsync(userEmail);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [Guid.NewGuid()]\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/revoke\", request);\n\n        Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);\n    }\n\n    [Fact]\n    public async Task BulkRevoke_WithEmptyIds_ReturnsBadRequest()\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = []\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/revoke\", request);\n\n        Assert.Equal(HttpStatusCode.BadRequest, httpResponse.StatusCode);\n    }\n\n    [Fact]\n    public async Task BulkRevoke_WithInvalidOrganizationId_ReturnsForbidden()\n    {\n        var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Owner);\n\n        await _loginHelper.LoginAsync(ownerEmail);\n\n        var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n\n        var invalidOrgId = Guid.NewGuid();\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [orgUser.Id]\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{invalidOrgId}/users/revoke\", request);\n\n        Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);\n    }\n\n    [Fact]\n    public async Task BulkRevoke_ProviderRevokesOwner_ReturnsOk()\n    {\n        var providerEmail = $\"provider-user{Guid.NewGuid()}@example.com\";\n\n        // create user for provider\n        await _factory.LoginWithNewAccount(providerEmail);\n\n        // create provider and provider user\n        await _factory.GetService<ICreateProviderCommand>()\n            .CreateBusinessUnitAsync(\n                new Provider\n                {\n                    Name = \"provider\",\n                    Type = ProviderType.BusinessUnit\n                },\n                providerEmail,\n                PlanType.EnterpriseAnnually2023,\n                10);\n\n        await _loginHelper.LoginAsync(providerEmail);\n\n        var providerUserUser = await _factory.GetService<IUserRepository>().GetByEmailAsync(providerEmail);\n\n        var providerUserCollection = await _factory.GetService<IProviderUserRepository>()\n            .GetManyByUserAsync(providerUserUser!.Id);\n\n        var providerUser = providerUserCollection.First();\n\n        await _factory.GetService<IProviderOrganizationRepository>().CreateAsync(new ProviderOrganization\n        {\n            ProviderId = providerUser.ProviderId,\n            OrganizationId = _organization.Id,\n            Key = null,\n            Settings = null\n        });\n\n        var (_, ownerOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Owner);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [ownerOrgUser.Id]\n        };\n\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/revoke\", request);\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Request;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class OrganizationUserControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private static readonly string _mockEncryptedString =\n        \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n\n    public OrganizationUserControllerTests(ApiApplicationFactory apiFactory)\n    {\n        _factory = apiFactory;\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private Organization _organization = null!;\n    private string _ownerEmail = null!;\n\n    [Fact]\n    public async Task BulkDeleteAccount_Success()\n    {\n        var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Owner);\n\n        await _loginHelper.LoginAsync(userEmail);\n\n        var (_, orgUserToDelete) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n        await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, \"bitwarden.com\");\n\n        var userRepository = _factory.GetService<IUserRepository>();\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n\n        Assert.NotNull(orgUserToDelete.UserId);\n        Assert.NotNull(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));\n        Assert.NotNull(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [orgUserToDelete.Id]\n        };\n\n        var httpResponse = await _client.PostAsJsonAsync($\"organizations/{_organization.Id}/users/delete-account\", request);\n        var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();\n        Assert.Single(content.Data, r => r.Id == orgUserToDelete.Id && r.Error == string.Empty);\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n        Assert.Null(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));\n        Assert.Null(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));\n    }\n\n    [Fact]\n    public async Task BulkDeleteAccount_MixedResults()\n    {\n        var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Admin);\n\n        await _loginHelper.LoginAsync(userEmail);\n\n        // Can delete users\n        var (_, validOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n        // Cannot delete owners\n        var (_, invalidOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.Owner);\n        await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, \"bitwarden.com\");\n\n        var userRepository = _factory.GetService<IUserRepository>();\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n\n        Assert.NotNull(validOrgUser.UserId);\n        Assert.NotNull(invalidOrgUser.UserId);\n\n        var arrangedUsers =\n            await userRepository.GetManyAsync([validOrgUser.UserId.Value, invalidOrgUser.UserId.Value]);\n        Assert.Equal(2, arrangedUsers.Count());\n\n        var arrangedOrgUsers =\n            await organizationUserRepository.GetManyAsync([validOrgUser.Id, invalidOrgUser.Id]);\n        Assert.Equal(2, arrangedOrgUsers.Count);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = [validOrgUser.Id, invalidOrgUser.Id]\n        };\n\n        var httpResponse = await _client.PostAsJsonAsync($\"organizations/{_organization.Id}/users/delete-account\", request);\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n        var debug = await httpResponse.Content.ReadAsStringAsync();\n        var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();\n        Assert.Equal(2, content.Data.Count());\n        Assert.Contains(content.Data, r => r.Id == validOrgUser.Id && r.Error == string.Empty);\n        Assert.Contains(content.Data, r =>\n            r.Id == invalidOrgUser.Id &&\n            string.Equals(r.Error, new CannotDeleteOwnersError().Message, StringComparison.Ordinal));\n\n        var actualUsers =\n            await userRepository.GetManyAsync([validOrgUser.UserId.Value, invalidOrgUser.UserId.Value]);\n        Assert.Single(actualUsers, u => u.Id == invalidOrgUser.UserId.Value);\n\n        var actualOrgUsers =\n            await organizationUserRepository.GetManyAsync([validOrgUser.Id, invalidOrgUser.Id]);\n        Assert.Single(actualOrgUsers, ou => ou.Id == invalidOrgUser.Id);\n    }\n\n    [Theory]\n    [InlineData(OrganizationUserType.User)]\n    [InlineData(OrganizationUserType.Custom)]\n    public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ReturnsForbiddenResponse(OrganizationUserType organizationUserType)\n    {\n        var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, organizationUserType, new Permissions { ManageUsers = false });\n\n        await _loginHelper.LoginAsync(userEmail);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = new List<Guid> { Guid.NewGuid() }\n        };\n\n        var httpResponse = await _client.PostAsJsonAsync($\"organizations/{_organization.Id}/users/delete-account\", request);\n\n        Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);\n    }\n\n    [Fact]\n    public async Task DeleteAccount_Success()\n    {\n        var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Owner);\n\n        await _loginHelper.LoginAsync(userEmail);\n\n        var (_, orgUserToDelete) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);\n        await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, \"bitwarden.com\");\n\n        var userRepository = _factory.GetService<IUserRepository>();\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n\n        Assert.NotNull(orgUserToDelete.UserId);\n        Assert.NotNull(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));\n        Assert.NotNull(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));\n\n        var httpResponse = await _client.DeleteAsync($\"organizations/{_organization.Id}/users/{orgUserToDelete.Id}/delete-account\");\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n        Assert.Null(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value));\n        Assert.Null(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id));\n    }\n\n    [Theory]\n    [InlineData(OrganizationUserType.User)]\n    [InlineData(OrganizationUserType.Custom)]\n    public async Task DeleteAccount_WhenUserCannotManageUsers_ReturnsForbiddenResponse(OrganizationUserType organizationUserType)\n    {\n        var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, organizationUserType, new Permissions { ManageUsers = false });\n\n        await _loginHelper.LoginAsync(userEmail);\n\n        var userToRemove = Guid.NewGuid();\n\n        var httpResponse = await _client.DeleteAsync($\"organizations/{_organization.Id}/users/{userToRemove}/delete-account\");\n\n        Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(OrganizationUserType.User)]\n    [InlineData(OrganizationUserType.Custom)]\n    public async Task GetAccountRecoveryDetails_WithoutManageResetPasswordPermission_ReturnsForbiddenResponse(OrganizationUserType organizationUserType)\n    {\n        var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, organizationUserType, new Permissions { ManageUsers = false });\n\n        await _loginHelper.LoginAsync(userEmail);\n\n        var request = new OrganizationUserBulkRequestModel\n        {\n            Ids = []\n        };\n\n        var httpResponse =\n            await _client.PostAsJsonAsync($\"organizations/{_organization.Id}/users/account-recovery-details\", request);\n\n        Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"org-user-integration-test-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);\n    }\n\n    [Fact]\n    public async Task Confirm_WithValidUser_ReturnsSuccess()\n    {\n        await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);\n\n        var acceptedOrgUser = (await CreateAcceptedUsersAsync(new[] { (\"test1@bitwarden.com\", OrganizationUserType.User) })).First();\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var confirmModel = new OrganizationUserConfirmRequestModel\n        {\n            Key = \"test-key\",\n            DefaultUserCollectionName = _mockEncryptedString\n        };\n        var confirmResponse = await _client.PostAsJsonAsync($\"organizations/{_organization.Id}/users/{acceptedOrgUser.Id}/confirm\", confirmModel);\n\n        Assert.Equal(HttpStatusCode.OK, confirmResponse.StatusCode);\n\n        await VerifyUserConfirmedAsync(acceptedOrgUser, \"test-key\");\n        await VerifyDefaultCollectionCountAsync(acceptedOrgUser, 1);\n    }\n\n    [Fact]\n    public async Task Confirm_WithValidOwner_ReturnsSuccess()\n    {\n        await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);\n\n        var acceptedOrgUser = (await CreateAcceptedUsersAsync(new[] { (\"owner1@bitwarden.com\", OrganizationUserType.Owner) })).First();\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var confirmModel = new OrganizationUserConfirmRequestModel\n        {\n            Key = \"test-key\",\n            DefaultUserCollectionName = _mockEncryptedString\n        };\n        var confirmResponse = await _client.PostAsJsonAsync($\"organizations/{_organization.Id}/users/{acceptedOrgUser.Id}/confirm\", confirmModel);\n\n        Assert.Equal(HttpStatusCode.OK, confirmResponse.StatusCode);\n\n        await VerifyUserConfirmedAsync(acceptedOrgUser, \"test-key\");\n        await VerifyDefaultCollectionCountAsync(acceptedOrgUser, 0);\n    }\n\n    [Fact]\n    public async Task BulkConfirm_WithValidUsers_ReturnsSuccess()\n    {\n        const string testKeyFormat = \"test-key-{0}\";\n        await OrganizationTestHelpers.EnableOrganizationDataOwnershipPolicyAsync(_factory, _organization.Id);\n\n        var acceptedUsers = await CreateAcceptedUsersAsync([\n            (\"test1@example.com\", OrganizationUserType.User),\n            (\"test2@example.com\", OrganizationUserType.Owner),\n            (\"test3@example.com\", OrganizationUserType.User)\n        ]);\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var bulkConfirmModel = new OrganizationUserBulkConfirmRequestModel\n        {\n            Keys = acceptedUsers.Select((organizationUser, index) => new OrganizationUserBulkConfirmRequestModelEntry\n            {\n                Id = organizationUser.Id,\n                Key = string.Format(testKeyFormat, index)\n            }),\n            DefaultUserCollectionName = _mockEncryptedString\n        };\n\n        var bulkConfirmResponse = await _client.PostAsJsonAsync($\"organizations/{_organization.Id}/users/confirm\", bulkConfirmModel);\n\n        Assert.Equal(HttpStatusCode.OK, bulkConfirmResponse.StatusCode);\n\n        await VerifyMultipleUsersConfirmedAsync(acceptedUsers.Select((organizationUser, index) =>\n            (organizationUser, string.Format(testKeyFormat, index))).ToList());\n        await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(0), 1);\n        await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(1), 0); // Owner does not get a default collection\n        await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(2), 1);\n    }\n\n    [Fact]\n    public async Task Put_WithExistingDefaultCollection_Success()\n    {\n        // Arrange\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var (userEmail, organizationUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.User);\n\n        var (group, sharedCollection, defaultCollection) = await CreateTestDataAsync();\n        await AssignDefaultCollectionToUserAsync(organizationUser, defaultCollection);\n\n        // Act\n        var updateRequest = CreateUpdateRequest(sharedCollection, group);\n        var httpResponse = await _client.PutAsJsonAsync($\"organizations/{_organization.Id}/users/{organizationUser.Id}\", updateRequest);\n\n        Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);\n\n        // Assert\n        await VerifyUserWasUpdatedCorrectlyAsync(organizationUser, expectedType: OrganizationUserType.Custom, expectedManageGroups: true);\n        await VerifyGroupAccessWasAddedAsync(organizationUser, [group]);\n        await VerifyCollectionAccessWasUpdatedCorrectlyAsync(organizationUser, sharedCollection.Id, defaultCollection.Id);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    private async Task<(Group group, Collection sharedCollection, Collection defaultCollection)> CreateTestDataAsync()\n    {\n        var groupRepository = _factory.GetService<IGroupRepository>();\n        var group = await groupRepository.CreateAsync(new Group\n        {\n            OrganizationId = _organization.Id,\n            Name = $\"Test Group {Guid.NewGuid()}\"\n        });\n\n        var collectionRepository = _factory.GetService<ICollectionRepository>();\n        var sharedCollection = await collectionRepository.CreateAsync(new Collection\n        {\n            OrganizationId = _organization.Id,\n            Name = $\"Test Collection {Guid.NewGuid()}\",\n            Type = CollectionType.SharedCollection\n        });\n\n        var defaultCollection = await collectionRepository.CreateAsync(new Collection\n        {\n            OrganizationId = _organization.Id,\n            Name = $\"My Items {Guid.NewGuid()}\",\n            Type = CollectionType.DefaultUserCollection\n        });\n\n        return (group, sharedCollection, defaultCollection);\n    }\n\n    private async Task AssignDefaultCollectionToUserAsync(OrganizationUser organizationUser, Collection defaultCollection)\n    {\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        await organizationUserRepository.ReplaceAsync(organizationUser,\n            new List<CollectionAccessSelection>\n            {\n                new CollectionAccessSelection\n                {\n                    Id = defaultCollection.Id,\n                    ReadOnly = false,\n                    HidePasswords = false,\n                    Manage = true\n                }\n            });\n    }\n\n    private static OrganizationUserUpdateRequestModel CreateUpdateRequest(Collection sharedCollection, Group group)\n    {\n        return new OrganizationUserUpdateRequestModel\n        {\n            Type = OrganizationUserType.Custom,\n            Permissions = new Permissions\n            {\n                ManageGroups = true\n            },\n            Collections = new List<SelectionReadOnlyRequestModel>\n            {\n                new SelectionReadOnlyRequestModel\n                {\n                    Id = sharedCollection.Id,\n                    ReadOnly = true,\n                    HidePasswords = false,\n                    Manage = false\n                }\n            },\n            Groups = new List<Guid> { group.Id }\n        };\n    }\n\n    private async Task VerifyUserWasUpdatedCorrectlyAsync(\n        OrganizationUser organizationUser,\n        OrganizationUserType expectedType,\n        bool expectedManageGroups)\n    {\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var updatedOrgUser = await organizationUserRepository.GetByIdAsync(organizationUser.Id);\n        Assert.NotNull(updatedOrgUser);\n        Assert.Equal(expectedType, updatedOrgUser.Type);\n        Assert.Equal(expectedManageGroups, updatedOrgUser.GetPermissions().ManageGroups);\n    }\n\n    private async Task VerifyGroupAccessWasAddedAsync(\n        OrganizationUser organizationUser, IEnumerable<Group> groups)\n    {\n        var groupRepository = _factory.GetService<IGroupRepository>();\n        var userGroups = await groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);\n        Assert.All(groups, group => Assert.Contains(group.Id, userGroups));\n    }\n\n    private async Task VerifyCollectionAccessWasUpdatedCorrectlyAsync(\n        OrganizationUser organizationUser, Guid sharedCollectionId, Guid defaultCollectionId)\n    {\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var (_, collectionAccess) = await organizationUserRepository.GetByIdWithCollectionsAsync(organizationUser.Id);\n        var collectionIds = collectionAccess.Select(c => c.Id).ToHashSet();\n\n        Assert.Contains(defaultCollectionId, collectionIds);\n        Assert.Contains(sharedCollectionId, collectionIds);\n\n        var newCollectionAccess = collectionAccess.First(c => c.Id == sharedCollectionId);\n        Assert.True(newCollectionAccess.ReadOnly);\n        Assert.False(newCollectionAccess.HidePasswords);\n        Assert.False(newCollectionAccess.Manage);\n    }\n\n    private async Task<List<OrganizationUser>> CreateAcceptedUsersAsync(\n        IEnumerable<(string email, OrganizationUserType userType)> newUsers)\n    {\n        var acceptedUsers = new List<OrganizationUser>();\n\n        foreach (var (email, userType) in newUsers)\n        {\n            await _factory.LoginWithNewAccount(email);\n\n            var acceptedOrgUser = await OrganizationTestHelpers.CreateUserAsync(\n                _factory, _organization.Id, email,\n                userType, userStatusType: OrganizationUserStatusType.Accepted);\n\n            acceptedUsers.Add(acceptedOrgUser);\n        }\n\n        return acceptedUsers;\n    }\n\n    private async Task VerifyDefaultCollectionCountAsync(OrganizationUser orgUser, int expectedCount)\n    {\n        var collectionRepository = _factory.GetService<ICollectionRepository>();\n        var collections = await collectionRepository.GetManyByUserIdAsync(orgUser.UserId!.Value);\n        Assert.Equal(expectedCount, collections.Count);\n    }\n\n    private async Task VerifyUserConfirmedAsync(OrganizationUser orgUser, string expectedKey)\n    {\n        await VerifyMultipleUsersConfirmedAsync(new List<(OrganizationUser orgUser, string key)> { (orgUser, expectedKey) });\n    }\n\n    private async Task VerifyMultipleUsersConfirmedAsync(List<(OrganizationUser orgUser, string key)> acceptedOrganizationUsers)\n    {\n        var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        for (int i = 0; i < acceptedOrganizationUsers.Count; i++)\n        {\n            var confirmedUser = await orgUserRepository.GetByIdAsync(acceptedOrganizationUsers[i].orgUser.Id);\n            Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);\n            Assert.Equal(acceptedOrganizationUsers[i].key, confirmedUser.Key);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerAcceptInitTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\n/// <summary>\n/// Integration tests for the AcceptInit endpoint (POST /organizations/{orgId}/users/{organizationUserId}/accept-init).\n/// This endpoint is used when a user accepts an invitation to a pending organization, initializing the organization\n/// by setting its keys and status, accepting the user's invitation, and confirming the user as a member.\n/// </summary>\npublic class OrganizationUsersControllerAcceptInitTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private const string _mockEncryptedString = \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n    private const string _mockPublicKey = \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwMj7W00xS7H0NWasGn7PfEq8VfH3fa5XuZucsKxLLRAHHZk0xGRZJH2lFIznizv3GpF8vzhHhe9VpmMkrdIa5oWhwHpy+D7Z1QCQxuUXzvMKpa95GOntr89nN/mWKpk6abjgjmDcqFJ0lhDqkKnDfes+d8BBd5oEA8p41/Ykz7OfG7AiktVBpTQFW09MQh1NOvcLxVgiUUVRPwNRKrOeCekWDtOjZhASMETv3kI1ogvhHukOQ3ztDzrxvmwnLQ+cXl1EeD8gQnGDp3QLiJqxPgh2EdmANh4IzjRexoDn6BqhRGqLLIoLAbbkoiNrd6NYujrWW0N8KMMoVEXuJL2g4wIDAQAB\";\n    private const string _mockEncryptedPrivateKey = \"2.Ytudv+Qk3ET9hN8whqpuGg==|ijsFhmjaf1aaT9uz+IPhVTzMS+2W/ldAP8LdT5VyJaFdx4HSdLcWSZvz5xWuuW94zfv1Qh+p3iQIuZOr29G4jcx47rYtz4ssiFtB7Ia552ZeF+cb7uuVg40CIe7ycuJQITk00o8gots+wFnaEvk0Vjgycnqutm0jpeBJ1joWJWqTVgSsYdUGLu7PiJywQ9NgY4+bJXqadlcviS3rhPKJXtiXYJhqJqSw+vI0Yxp96MJ0HcFJk/LG22YJPTvL5kzuDq/Wzj40kj8blQ+ag+xHD4P/KJ/MppEB3OpDw3UoJ50Ek+YB9pOqGxZtvqMEzBDsgh0yoz1O992UnhaUqtJ5e9Bxy3PA6cJsdyn9npduNOreEb8vePCidN2XC+chjJpPFpjms9muHLKgfaTIfpiJA2Tz8E9dvSyhHHTE1mY+xEA7P08BYKN3LNoSGIjdiZuouJ1V/KZvCssDfVG1tli2qpnhTIh4m3rAMhbM8WW3B7wCV8N0MpcJJSvndkVcMgRbgWcbivLeXuKdE/K98n01RvOLSJyslhLGCGEQQKw6N3HQ2iELfv84YQZi2fjDK+OqAmXDq1pNcjKX2I8dqBwl31tPC8qSZiWnfinwLdqQTvSQjOIyAHb4sSjAwgdMbCRzUTChRr09l+PAZqGWdMC5N2Bw+bA8WP0l2Wdxuv9Abxl3F7xGeAA9Rw9PU5wGKujaMRmO4V9MFjNyyCcw4D9pzKMW6OUKsHsHE7tsG7KskCzksHzrZGawAt0S41BYQA/JwePCrD3F6dM92anlC1LfA00KJb0tmFdU0yJNmJfR+S78yn8yM6wDgIs2cFB3W1fYfpfUvQm+zzPoEQihNxBxnwFsBtMAOtPy54FjSzKmxsQTrYT9E6NFb8k6ZIIm2gNeOPK9OUJgjw+4g2BXErM6ikHTzM3xcaTq/cQaePZ52emndw1qOtdV06hr2EeuLM8frfLHpsknUe8JeYeW5p9E8QdZjjSN9034usdYNamUdxzmn/Mw/ar8z1xSKS6zcaQoTQ7aYLEX3dWJndc4W64HyiaRkLjO6qLUFeOerfz5UvcxxRY89eAA0KLC2xnGkBMOhXxYzIB3lF8Zxqb4JMhoBGw1n31TDfhRDGDHHEAsZuAIcH7aC5RDVxU08Jxmw4oLmeTDZA5BFcqp2A3fusNVZUnfpmMy6DCJyFprlRl8jSlJMAvhbxVuuLFDZnjl77Z2of796Ur6DgmNwYtMPNEntZPIcZ76VPLWAL8lqiRBm20c4qiwr5rNSr5kry9bR1EfXHwFRjy5pxFQ+5+ilpRl8WPfT/iUuORd8J2wnCmghm7uxiJd9t82kX0s6benhL29dQ1etqt5soX2RnlfKan16GVWoI3xrljIQrCAY4xpdptSpglOnrpSClbN1nhGkDfFPNq2pWhQrDbznDknAJ9MxQaVnLYPhn7I849GMd7EvpSkydwQu7QXn9+H4jxn6UEntNGxcL0xkG+xippvZEe+HBvcDD40efDQW1bDbILLjPb4rNRx4d3xaQnVNaF7L33osm5LgfXAQSwHJiURdkU4zmhtPP4zn0br0OdFlR3mPcrkeNeSvs7FxiKtD6n6s+av+4bKjbLL1OyuwmTnMilL6p+m8ldte0yos/r+zOuxWeI=|euhiXWXehYbFQhlAV6LIECSIPCIRaHbNdr9OI4cTPUM=\";\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n    private IFeatureService _featureService = null!;\n\n    private Organization _pendingOrganization = null!;\n    private User _invitedUser = null!;\n    private OrganizationUser _invitedOrgUser = null!;\n    private string _invitedUserEmail = null!;\n\n    public OrganizationUsersControllerAcceptInitTests(ApiApplicationFactory apiFactory)\n    {\n        _factory = apiFactory;\n        _factory.SubstituteService<IFeatureService>(_ => { });\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n        _featureService = _factory.GetService<IFeatureService>();\n    }\n\n    public async Task InitializeAsync()\n    {\n        // Create a pending organization without keys\n        var organizationRepository = _factory.GetService<IOrganizationRepository>();\n        _pendingOrganization = new Organization\n        {\n            Name = \"Pending Test Org\",\n            BillingEmail = $\"{Guid.NewGuid()}@example.com\",\n            Plan = \"Free\",\n            PlanType = PlanType.Free,\n            Enabled = false,\n            Status = OrganizationStatusType.Pending,\n            PublicKey = null,\n            PrivateKey = null,\n            CreationDate = DateTime.UtcNow,\n            RevisionDate = DateTime.UtcNow\n        };\n        await organizationRepository.CreateAsync(_pendingOrganization);\n\n        // Create a user who will be invited to the pending organization\n        _invitedUserEmail = $\"{Guid.NewGuid()}@example.com\";\n        await _factory.LoginWithNewAccount(_invitedUserEmail);\n\n        var userRepository = _factory.GetService<IUserRepository>();\n        _invitedUser = await userRepository.GetByEmailAsync(_invitedUserEmail);\n\n        // Create organization user as invited (not yet accepted)\n        // Note: UserId should be NULL for invited users who haven't accepted yet\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        _invitedOrgUser = new OrganizationUser\n        {\n            OrganizationId = _pendingOrganization.Id,\n            UserId = null,  // NULL until they accept\n            Email = _invitedUserEmail,\n            Key = null,\n            Type = OrganizationUserType.Owner,\n            Status = OrganizationUserStatusType.Invited,\n            AccessSecretsManager = false\n        };\n        await organizationUserRepository.CreateAsync(_invitedOrgUser);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public async Task AcceptInit_WithValidData_InitializesOrganizationAndConfirmsUser(bool featureFlagEnabled)\n    {\n        // Arrange\n        _featureService.IsEnabled(FeatureFlagKeys.RefactorOrgAcceptInit).Returns(featureFlagEnabled);\n\n        await _loginHelper.LoginAsync(_invitedUserEmail);\n\n        var token = GenerateInviteToken(_invitedOrgUser, _invitedUser.Email);\n\n        var acceptInitRequest = new OrganizationUserAcceptInitRequestModel\n        {\n            Token = token,\n            Key = \"test-user-key\",\n            Keys = new OrganizationKeysRequestModel\n            {\n                PublicKey = _mockPublicKey,\n                EncryptedPrivateKey = _mockEncryptedPrivateKey\n            },\n            CollectionName = _mockEncryptedString\n        };\n\n        // Act\n        var response = await _client.PostAsJsonAsync(\n            $\"organizations/{_pendingOrganization.Id}/users/{_invitedOrgUser.Id}/accept-init\",\n            acceptInitRequest);\n\n        // Assert\n        var expectedStatusCode = featureFlagEnabled ? HttpStatusCode.NoContent : HttpStatusCode.OK;\n        Assert.Equal(expectedStatusCode, response.StatusCode);\n\n        // Verify organization was initialized\n        var organizationRepository = _factory.GetService<IOrganizationRepository>();\n        var updatedOrganization = await organizationRepository.GetByIdAsync(_pendingOrganization.Id);\n\n        Assert.NotNull(updatedOrganization);\n        Assert.True(updatedOrganization.Enabled);\n        Assert.Equal(OrganizationStatusType.Created, updatedOrganization.Status);\n        Assert.Equal(_mockPublicKey, updatedOrganization.PublicKey);\n        Assert.Equal(_mockEncryptedPrivateKey, updatedOrganization.PrivateKey);\n\n        // Verify user was confirmed and properly linked\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var confirmedOrgUser = await organizationUserRepository.GetByIdAsync(_invitedOrgUser.Id);\n\n        Assert.NotNull(confirmedOrgUser);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedOrgUser.Status);\n        Assert.Equal(\"test-user-key\", confirmedOrgUser.Key);\n        Assert.Equal(_invitedUser.Id, confirmedOrgUser.UserId);\n        Assert.Null(confirmedOrgUser.Email); // Email should be cleared after acceptance\n\n        // Verify user's email was verified\n        var userRepository = _factory.GetService<IUserRepository>();\n        var user = await userRepository.GetByEmailAsync(_invitedUserEmail);\n        Assert.True(user.EmailVerified);\n\n        // Verify default collection was created\n        var collectionRepository = _factory.GetService<ICollectionRepository>();\n        var collections = await collectionRepository.GetManyByOrganizationIdAsync(_pendingOrganization.Id);\n\n        Assert.Single(collections);\n        Assert.Equal(_mockEncryptedString, collections.First().Name);\n        Assert.Equal(_pendingOrganization.Id, collections.First().OrganizationId);\n\n        // Verify user has access to the collection\n        var (_, collectionAccess) = await organizationUserRepository.GetByIdWithCollectionsAsync(_invitedOrgUser.Id);\n        Assert.Single(collectionAccess);\n        Assert.Equal(collections.First().Id, collectionAccess.First().Id);\n        Assert.True(collectionAccess.First().Manage);\n        Assert.False(collectionAccess.First().ReadOnly);\n        Assert.False(collectionAccess.First().HidePasswords);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public async Task AcceptInit_WithoutAuthentication_ReturnsUnauthorized(bool featureFlagEnabled)\n    {\n        // Arrange\n        _featureService.IsEnabled(FeatureFlagKeys.RefactorOrgAcceptInit).Returns(featureFlagEnabled);\n\n        // Don't log in\n        var token = GenerateInviteToken(_invitedOrgUser, _invitedUser.Email);\n\n        var acceptInitRequest = new OrganizationUserAcceptInitRequestModel\n        {\n            Token = token,\n            Key = \"test-user-key\",\n            Keys = new OrganizationKeysRequestModel\n            {\n                PublicKey = _mockPublicKey,\n                EncryptedPrivateKey = _mockEncryptedPrivateKey\n            },\n            CollectionName = _mockEncryptedString\n        };\n\n        // Act\n        var response = await _client.PostAsJsonAsync(\n            $\"organizations/{_pendingOrganization.Id}/users/{_invitedOrgUser.Id}/accept-init\",\n            acceptInitRequest);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    private string GenerateInviteToken(OrganizationUser orgUser, string email)\n    {\n        var tokenFactory = _factory.GetService<IDataProtectorTokenFactory<OrgUserInviteTokenable>>();\n\n        var tokenable = new OrgUserInviteTokenable(orgUser);\n        return tokenFactory.Protect(tokenable);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs",
    "content": "﻿using System.Net;\nusing System.Text;\nusing System.Text.Json;\nusing AutoMapper;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Seeder.Recipes;\nusing Bit.Seeder.Services;\nusing Microsoft.AspNetCore.Identity;\nusing Xunit;\nusing Xunit.Abstractions;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testOutputHelper)\n{\n    /// <summary>\n    /// Tests GET /organizations/{orgId}/users?includeCollections=true\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10)]\n    //[InlineData(100)]\n    //[InlineData(1000)]\n    public async Task GetAllUsers_WithCollections(int seats)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var collectionsSeeder = new CollectionsRecipe(db);\n        var groupsSeeder = new GroupsRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: seats);\n\n        var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();\n        collectionsSeeder.Seed(orgId, 10, orgUserIds);\n        groupsSeeder.Seed(orgId, 5, orgUserIds);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.GetAsync($\"/organizations/{orgId}/users?includeCollections=true\");\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        stopwatch.Stop();\n        testOutputHelper.WriteLine($\"GET /users - Seats: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms\");\n    }\n\n    /// <summary>\n    /// Tests GET /organizations/{orgId}/users/mini-details\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10)]\n    //[InlineData(100)]\n    //[InlineData(1000)]\n    public async Task GetAllUsers_MiniDetails(int seats)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var collectionsSeeder = new CollectionsRecipe(db);\n        var groupsSeeder = new GroupsRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: seats);\n\n        var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();\n        collectionsSeeder.Seed(orgId, 10, orgUserIds);\n        groupsSeeder.Seed(orgId, 5, orgUserIds);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.GetAsync($\"/organizations/{orgId}/users/mini-details\");\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"GET /users/mini-details - Seats: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Tests GET /organizations/{orgId}/users/{id}?includeGroups=true\n    /// </summary>\n    [Fact(Skip = \"Performance test\")]\n    public async Task GetSingleUser_WithGroups()\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var groupsSeeder = new GroupsRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: 1);\n\n        var orgUserId = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).FirstOrDefault();\n        groupsSeeder.Seed(orgId, 2, [orgUserId]);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.GetAsync($\"/organizations/{orgId}/users/{orgUserId}?includeGroups=true\");\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"GET /users/{{id}} - Request duration: {stopwatch.ElapsedMilliseconds} ms\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Tests GET /organizations/{orgId}/users/{id}/reset-password-details\n    /// </summary>\n    [Fact(Skip = \"Performance test\")]\n    public async Task GetResetPasswordDetails_ForSingleUser()\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: 1);\n\n        var orgUserId = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).FirstOrDefault();\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.GetAsync($\"/organizations/{orgId}/users/{orgUserId}/reset-password-details\");\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"GET /users/{{id}}/reset-password-details - Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Tests POST /organizations/{orgId}/users/confirm\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10)]\n    //[InlineData(100)]\n    //[InlineData(1000)]\n    public async Task BulkConfirmUsers(int userCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(\n            name: \"Org\",\n            domain: domain,\n            users: userCount,\n            usersStatus: OrganizationUserStatusType.Accepted);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var acceptedUserIds = db.OrganizationUsers\n            .Where(ou => ou.OrganizationId == orgId && ou.Status == OrganizationUserStatusType.Accepted)\n            .Select(ou => ou.Id)\n            .ToList();\n\n        var confirmRequest = new OrganizationUserBulkConfirmRequestModel\n        {\n            Keys = acceptedUserIds.Select(id => new OrganizationUserBulkConfirmRequestModelEntry { Id = id, Key = \"test-key-\" + id }),\n            DefaultUserCollectionName = \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\"\n        };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(confirmRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PostAsync($\"/organizations/{orgId}/users/confirm\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"POST /users/confirm - Users: {acceptedUserIds.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.True(response.IsSuccessStatusCode);\n    }\n\n    /// <summary>\n    /// Tests POST /organizations/{orgId}/users/remove\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10)]\n    //[InlineData(100)]\n    //[InlineData(1000)]\n    public async Task BulkRemoveUsers(int userCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: userCount);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var usersToRemove = db.OrganizationUsers\n            .Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)\n            .Select(ou => ou.Id)\n            .ToList();\n\n        var removeRequest = new OrganizationUserBulkRequestModel { Ids = usersToRemove };\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(removeRequest), Encoding.UTF8, \"application/json\");\n\n        var response = await client.PostAsync($\"/organizations/{orgId}/users/remove\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"POST /users/remove - Users: {usersToRemove.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.True(response.IsSuccessStatusCode);\n    }\n\n    /// <summary>\n    /// Tests PUT /organizations/{orgId}/users/revoke\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10)]\n    //[InlineData(100)]\n    //[InlineData(1000)]\n    public async Task BulkRevokeUsers(int userCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(\n            name: \"Org\",\n            domain: domain,\n            users: userCount,\n            usersStatus: OrganizationUserStatusType.Confirmed);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var usersToRevoke = db.OrganizationUsers\n            .Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)\n            .Select(ou => ou.Id)\n            .ToList();\n\n        var revokeRequest = new OrganizationUserBulkRequestModel { Ids = usersToRevoke };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(revokeRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PutAsync($\"/organizations/{orgId}/users/revoke\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"PUT /users/revoke - Users: {usersToRevoke.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.True(response.IsSuccessStatusCode);\n    }\n\n    /// <summary>\n    /// Tests PUT /organizations/{orgId}/users/restore\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10)]\n    //[InlineData(100)]\n    //[InlineData(1000)]\n    public async Task BulkRestoreUsers(int userCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(\n            name: \"Org\",\n            domain: domain,\n            users: userCount,\n            usersStatus: OrganizationUserStatusType.Revoked);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var usersToRestore = db.OrganizationUsers\n            .Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)\n            .Select(ou => ou.Id)\n            .ToList();\n\n        var restoreRequest = new OrganizationUserBulkRequestModel { Ids = usersToRestore };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(restoreRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PutAsync($\"/organizations/{orgId}/users/restore\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"PUT /users/restore - Users: {usersToRestore.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.True(response.IsSuccessStatusCode);\n    }\n\n    /// <summary>\n    /// Tests POST /organizations/{orgId}/users/delete-account\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10)]\n    //[InlineData(100)]\n    //[InlineData(1000)]\n    public async Task BulkDeleteAccounts(int userCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var domainSeeder = new OrganizationDomainRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n\n        var orgId = orgSeeder.Seed(\n            name: \"Org\",\n            domain: domain,\n            users: userCount,\n            usersStatus: OrganizationUserStatusType.Confirmed);\n\n        domainSeeder.Seed(orgId, domain);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var usersToDelete = db.OrganizationUsers\n            .Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)\n            .Select(ou => ou.Id)\n            .ToList();\n\n        var deleteRequest = new OrganizationUserBulkRequestModel { Ids = usersToDelete };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(deleteRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PostAsync($\"/organizations/{orgId}/users/delete-account\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"POST /users/delete-account - Users: {usersToDelete.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.True(response.IsSuccessStatusCode);\n    }\n\n    /// <summary>\n    /// Tests PUT /organizations/{orgId}/users/{id}\n    /// </summary>\n    [Fact(Skip = \"Performance test\")]\n    public async Task UpdateSingleUser_WithCollectionsAndGroups()\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var collectionsSeeder = new CollectionsRecipe(db);\n        var groupsSeeder = new GroupsRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: 1);\n\n        var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();\n        var collectionIds = collectionsSeeder.Seed(orgId, 3, orgUserIds, 0);\n        var groupIds = groupsSeeder.Seed(orgId, 2, orgUserIds, 0);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var userToUpdate = db.OrganizationUsers\n            .FirstOrDefault(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User);\n\n        var updateRequest = new OrganizationUserUpdateRequestModel\n        {\n            Type = OrganizationUserType.Custom,\n            Collections = collectionIds.Select(c => new SelectionReadOnlyRequestModel { Id = c, ReadOnly = false, HidePasswords = false, Manage = false }),\n            Groups = groupIds,\n            AccessSecretsManager = false,\n            Permissions = new Permissions { AccessEventLogs = true }\n        };\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PutAsync($\"/organizations/{orgId}/users/{userToUpdate.Id}\",\n            new StringContent(JsonSerializer.Serialize(updateRequest), Encoding.UTF8, \"application/json\"));\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"PUT /users/{{id}} - Collections: {collectionIds.Count}; Groups: {groupIds.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.True(response.IsSuccessStatusCode);\n    }\n\n    /// <summary>\n    /// Tests PUT /organizations/{orgId}/users/enable-secrets-manager\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10)]\n    //[InlineData(100)]\n    //[InlineData(1000)]\n    public async Task BulkEnableSecretsManager(int userCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: userCount);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var usersToEnable = db.OrganizationUsers\n            .Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)\n            .Select(ou => ou.Id)\n            .ToList();\n\n        var enableRequest = new OrganizationUserBulkRequestModel { Ids = usersToEnable };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(enableRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PutAsync($\"/organizations/{orgId}/users/enable-secrets-manager\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"PUT /users/enable-secrets-manager - Users: {usersToEnable.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.True(response.IsSuccessStatusCode);\n    }\n\n    /// <summary>\n    /// Tests DELETE /organizations/{orgId}/users/{id}/delete-account\n    /// </summary>\n    [Fact(Skip = \"Performance test\")]\n    public async Task DeleteSingleUserAccount_FromVerifiedDomain()\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var domainSeeder = new OrganizationDomainRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(\n            name: \"Org\",\n            domain: domain,\n            users: 2,\n            usersStatus: OrganizationUserStatusType.Confirmed);\n\n        domainSeeder.Seed(orgId, domain);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var userToDelete = db.OrganizationUsers\n            .FirstOrDefault(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User);\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.DeleteAsync($\"/organizations/{orgId}/users/{userToDelete.Id}/delete-account\");\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"DELETE /users/{{id}}/delete-account - Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Tests POST /organizations/{orgId}/users/invite\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(1)]\n    //[InlineData(5)]\n    //[InlineData(20)]\n    public async Task InviteUsers(int emailCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var collectionsSeeder = new CollectionsRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: 1);\n\n        var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();\n        var collectionIds = collectionsSeeder.Seed(orgId, 2, orgUserIds, 0);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var emails = Enumerable.Range(0, emailCount).Select(i => $\"{i:D4}@{domain}\").ToArray();\n        var inviteRequest = new OrganizationUserInviteRequestModel\n        {\n            Emails = emails,\n            Type = OrganizationUserType.User,\n            AccessSecretsManager = false,\n            Collections = Array.Empty<SelectionReadOnlyRequestModel>(),\n            Groups = Array.Empty<Guid>(),\n            Permissions = null\n        };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(inviteRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PostAsync($\"/organizations/{orgId}/users/invite\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"POST /users/invite - Emails: {emails.Length}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Tests POST /organizations/{orgId}/users/reinvite\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10)]\n    //[InlineData(100)]\n    //[InlineData(1000)]\n    public async Task BulkReinviteUsers(int userCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = factory.GetService<IManglerService>();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(\n            name: \"Org\",\n            domain: domain,\n            users: userCount,\n            usersStatus: OrganizationUserStatusType.Invited);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var usersToReinvite = db.OrganizationUsers\n            .Where(ou => ou.OrganizationId == orgId && ou.Status == OrganizationUserStatusType.Invited)\n            .Select(ou => ou.Id)\n            .ToList();\n\n        var reinviteRequest = new OrganizationUserBulkRequestModel { Ids = usersToReinvite };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(reinviteRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PostAsync($\"/organizations/{orgId}/users/reinvite\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"POST /users/reinvite - Users: {usersToReinvite.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.True(response.IsSuccessStatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPutResetPasswordTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class OrganizationUsersControllerPutResetPasswordTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private Organization _organization = null!;\n    private string _ownerEmail = null!;\n\n    public OrganizationUsersControllerPutResetPasswordTests(ApiApplicationFactory apiFactory)\n    {\n        _factory = apiFactory;\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"reset-password-test-{Guid.NewGuid()}@example.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);\n\n        // Enable reset password and policies for the organization\n        var organizationRepository = _factory.GetService<IOrganizationRepository>();\n        _organization.UseResetPassword = true;\n        _organization.UsePolicies = true;\n        await organizationRepository.ReplaceAsync(_organization);\n\n        // Enable the ResetPassword policy\n        var policyRepository = _factory.GetService<IPolicyRepository>();\n        await policyRepository.CreateAsync(new Policy\n        {\n            OrganizationId = _organization.Id,\n            Type = PolicyType.ResetPassword,\n            Enabled = true,\n            Data = \"{}\"\n        });\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    /// <summary>\n    /// Helper method to set the ResetPasswordKey on an organization user, which is required for account recovery\n    /// </summary>\n    private async Task SetResetPasswordKeyAsync(OrganizationUser orgUser)\n    {\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        orgUser.ResetPasswordKey = \"encrypted-reset-password-key\";\n        await organizationUserRepository.ReplaceAsync(orgUser);\n    }\n\n    [Fact]\n    public async Task PutResetPassword_AsHigherRole_CanRecoverLowerRole()\n    {\n        // Arrange\n        var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Owner);\n        await _loginHelper.LoginAsync(ownerEmail);\n\n        var (_, targetOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.User);\n        await SetResetPasswordKeyAsync(targetOrgUser);\n\n        var resetPasswordRequest = new OrganizationUserResetPasswordRequestModel\n        {\n            NewMasterPasswordHash = \"new-master-password-hash\",\n            Key = \"encrypted-recovery-key\"\n        };\n\n        // Act\n        var response = await _client.PutAsJsonAsync(\n            $\"organizations/{_organization.Id}/users/{targetOrgUser.Id}/reset-password\",\n            resetPasswordRequest);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PutResetPassword_AsLowerRole_CannotRecoverHigherRole()\n    {\n        // Arrange\n        var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Admin);\n        await _loginHelper.LoginAsync(adminEmail);\n\n        var (_, targetOwnerOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.Owner);\n        await SetResetPasswordKeyAsync(targetOwnerOrgUser);\n\n        var resetPasswordRequest = new OrganizationUserResetPasswordRequestModel\n        {\n            NewMasterPasswordHash = \"new-master-password-hash\",\n            Key = \"encrypted-recovery-key\"\n        };\n\n        // Act\n        var response = await _client.PutAsJsonAsync(\n            $\"organizations/{_organization.Id}/users/{targetOwnerOrgUser.Id}/reset-password\",\n            resetPasswordRequest);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var model = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();\n        Assert.Contains(RecoverAccountAuthorizationHandler.FailureReason, model.Message);\n    }\n\n    [Fact]\n    public async Task PutResetPassword_CannotRecoverProviderAccount()\n    {\n        // Arrange - Create owner who will try to recover the provider account\n        var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Owner);\n        await _loginHelper.LoginAsync(ownerEmail);\n\n        // Create a user who is also a provider user\n        var (targetUserEmail, targetOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.User);\n        await SetResetPasswordKeyAsync(targetOrgUser);\n\n        // Add the target user as a provider user to a different provider\n        var providerRepository = _factory.GetService<IProviderRepository>();\n        var providerUserRepository = _factory.GetService<IProviderUserRepository>();\n        var userRepository = _factory.GetService<IUserRepository>();\n\n        var provider = await providerRepository.CreateAsync(new Provider\n        {\n            Name = \"Test Provider\",\n            BusinessName = \"Test Provider Business\",\n            BillingEmail = \"provider@example.com\",\n            Type = ProviderType.Msp,\n            Status = ProviderStatusType.Created,\n            Enabled = true\n        });\n\n        var targetUser = await userRepository.GetByEmailAsync(targetUserEmail);\n        Assert.NotNull(targetUser);\n\n        await providerUserRepository.CreateAsync(new ProviderUser\n        {\n            ProviderId = provider.Id,\n            UserId = targetUser.Id,\n            Status = ProviderUserStatusType.Confirmed,\n            Type = ProviderUserType.ProviderAdmin\n        });\n\n        var resetPasswordRequest = new OrganizationUserResetPasswordRequestModel\n        {\n            NewMasterPasswordHash = \"new-master-password-hash\",\n            Key = \"encrypted-recovery-key\"\n        };\n\n        // Act\n        var response = await _client.PutAsJsonAsync(\n            $\"organizations/{_organization.Id}/users/{targetOrgUser.Id}/reset-password\",\n            resetPasswordRequest);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var model = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();\n        Assert.Equal(RecoverAccountAuthorizationHandler.ProviderFailureReason, model.Message);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerSelfRevokeTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class OrganizationUsersControllerSelfRevokeTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private string _ownerEmail = null!;\n\n    public OrganizationUsersControllerSelfRevokeTests(ApiApplicationFactory apiFactory)\n    {\n        _factory = apiFactory;\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"{Guid.NewGuid()}@example.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task SelfRevoke_WhenPolicyEnabledAndUserIsEligible_ReturnsOk()\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);\n\n        var policy = new Policy\n        {\n            OrganizationId = organization.Id,\n            Type = PolicyType.OrganizationDataOwnership,\n            Enabled = true,\n            Data = null\n        };\n        await _factory.GetService<IPolicyRepository>().CreateAsync(policy);\n\n        var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory,\n            organization.Id,\n            OrganizationUserType.User);\n        await _loginHelper.LoginAsync(userEmail);\n\n        var result = await _client.PutAsync($\"organizations/{organization.Id}/users/revoke-self\", null);\n\n        Assert.Equal(HttpStatusCode.NoContent, result.StatusCode);\n\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(organization.Id);\n        var revokedUser = organizationUsers.FirstOrDefault(u => u.Email == userEmail);\n\n        Assert.NotNull(revokedUser);\n        Assert.Equal(OrganizationUserStatusType.Revoked, revokedUser.Status);\n    }\n\n    [Fact]\n    public async Task SelfRevoke_WhenUserNotMemberOfOrganization_ReturnsForbidden()\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);\n\n        var policy = new Policy\n        {\n            OrganizationId = organization.Id,\n            Type = PolicyType.OrganizationDataOwnership,\n            Enabled = true,\n            Data = null\n        };\n        await _factory.GetService<IPolicyRepository>().CreateAsync(policy);\n\n        var nonMemberEmail = $\"{Guid.NewGuid()}@example.com\";\n        await _factory.LoginWithNewAccount(nonMemberEmail);\n        await _loginHelper.LoginAsync(nonMemberEmail);\n\n        var result = await _client.PutAsync($\"organizations/{organization.Id}/users/revoke-self\", null);\n\n        Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(OrganizationUserType.Owner)]\n    [InlineData(OrganizationUserType.Admin)]\n    public async Task SelfRevoke_WhenUserIsOwnerOrAdmin_ReturnsBadRequest(OrganizationUserType userType)\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);\n\n        var policy = new Policy\n        {\n            OrganizationId = organization.Id,\n            Type = PolicyType.OrganizationDataOwnership,\n            Enabled = true,\n            Data = null\n        };\n        await _factory.GetService<IPolicyRepository>().CreateAsync(policy);\n\n        string userEmail;\n        if (userType == OrganizationUserType.Owner)\n        {\n            userEmail = _ownerEmail;\n        }\n        else\n        {\n            (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n                _factory,\n                organization.Id,\n                userType);\n        }\n\n        await _loginHelper.LoginAsync(userEmail);\n\n        var result = await _client.PutAsync($\"organizations/{organization.Id}/users/revoke-self\", null);\n\n        Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);\n    }\n\n    [Fact]\n    public async Task SelfRevoke_WhenUserIsProviderButNotMember_ReturnsForbidden()\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);\n\n        var policy = new Policy\n        {\n            OrganizationId = organization.Id,\n            Type = PolicyType.OrganizationDataOwnership,\n            Enabled = true,\n            Data = null\n        };\n        await _factory.GetService<IPolicyRepository>().CreateAsync(policy);\n\n        var provider = await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(\n            _factory,\n            organization.Id,\n            ProviderType.Msp,\n            ProviderStatusType.Billable);\n\n        var providerUserEmail = $\"{Guid.NewGuid()}@example.com\";\n        await _factory.LoginWithNewAccount(providerUserEmail);\n        await ProviderTestHelpers.CreateProviderUserAsync(\n            _factory,\n            provider.Id,\n            providerUserEmail,\n            ProviderUserType.ProviderAdmin);\n\n        await _loginHelper.LoginAsync(providerUserEmail);\n\n        var result = await _client.PutAsync($\"organizations/{organization.Id}/users/revoke-self\", null);\n\n        Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationsControllerPerformanceTests.cs",
    "content": "﻿using System.Net;\nusing System.Text;\nusing System.Text.Json;\nusing AutoMapper;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core.AdminConsole.Models.Business.Tokenables;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\nusing Bit.Seeder.Recipes;\nusing Bit.Seeder.Services;\nusing Microsoft.AspNetCore.Identity;\nusing Xunit;\nusing Xunit.Abstractions;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutputHelper)\n{\n    /// <summary>\n    /// Tests DELETE /organizations/{id} with password verification\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10, 5, 3)]\n    //[InlineData(100, 20, 10)]\n    //[InlineData(1000, 50, 25)]\n    public async Task DeleteOrganization_WithPasswordVerification(int userCount, int collectionCount, int groupCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = new NoOpManglerService();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var collectionsSeeder = new CollectionsRecipe(db);\n        var groupsSeeder = new GroupsRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: userCount);\n\n        var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();\n        collectionsSeeder.Seed(orgId, collectionCount, orgUserIds, 0);\n        groupsSeeder.Seed(orgId, groupCount, orgUserIds, 0);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var deleteRequest = new SecretVerificationRequestModel\n        {\n            MasterPasswordHash = \"c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs=\"\n        };\n\n        var request = new HttpRequestMessage(HttpMethod.Delete, $\"/organizations/{orgId}\")\n        {\n            Content = new StringContent(JsonSerializer.Serialize(deleteRequest), Encoding.UTF8, \"application/json\")\n        };\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n\n        var response = await client.SendAsync(request);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"DELETE /organizations/{{id}} - Users: {userCount}; Collections: {collectionCount}; Groups: {groupCount}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Tests POST /organizations/{id}/delete-recover-token with token verification\n    /// </summary>\n    [Theory(Skip = \"Performance test\")]\n    [InlineData(10, 5, 3)]\n    //[InlineData(100, 20, 10)]\n    //[InlineData(1000, 50, 25)]\n    public async Task DeleteOrganization_WithTokenVerification(int userCount, int collectionCount, int groupCount)\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var db = factory.GetDatabaseContext();\n        var mapper = factory.GetService<IMapper>();\n        var passwordHasher = factory.GetService<IPasswordHasher<User>>();\n        var manglerService = new NoOpManglerService();\n        var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);\n        var collectionsSeeder = new CollectionsRecipe(db);\n        var groupsSeeder = new GroupsRecipe(db);\n\n        var domain = OrganizationTestHelpers.GenerateRandomDomain();\n        var orgId = orgSeeder.Seed(name: \"Org\", domain: domain, users: userCount);\n\n        var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();\n        collectionsSeeder.Seed(orgId, collectionCount, orgUserIds, 0);\n        groupsSeeder.Seed(orgId, groupCount, orgUserIds, 0);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $\"owner@{domain}\");\n\n        var organization = db.Organizations.FirstOrDefault(o => o.Id == orgId);\n        Assert.NotNull(organization);\n\n        var tokenFactory = factory.GetService<IDataProtectorTokenFactory<OrgDeleteTokenable>>();\n        var tokenable = new OrgDeleteTokenable(organization, 24);\n        var token = tokenFactory.Protect(tokenable);\n\n        var deleteRequest = new OrganizationVerifyDeleteRecoverRequestModel\n        {\n            Token = token\n        };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(deleteRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PostAsync($\"/organizations/{orgId}/delete-recover-token\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"POST /organizations/{{id}}/delete-recover-token - Users: {userCount}; Collections: {collectionCount}; Groups: {groupCount}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Tests POST /organizations/create-without-payment\n    /// </summary>\n    [Fact(Skip = \"Performance test\")]\n    public async Task CreateOrganization_WithoutPayment()\n    {\n        await using var factory = new SqlServerApiApplicationFactory();\n        var client = factory.CreateClient();\n\n        var email = $\"user@{OrganizationTestHelpers.GenerateRandomDomain()}\";\n        var masterPasswordHash = \"c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs=\";\n\n        await factory.LoginWithNewAccount(email, masterPasswordHash);\n\n        await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, email, masterPasswordHash);\n\n        var createRequest = new OrganizationNoPaymentCreateRequest\n        {\n            Name = \"Test Organization\",\n            BusinessName = \"Test Business Name\",\n            BillingEmail = email,\n            PlanType = PlanType.EnterpriseAnnually,\n            Key = \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\",\n            AdditionalSeats = 1,\n            AdditionalStorageGb = 1,\n            UseSecretsManager = true,\n            AdditionalSmSeats = 1,\n            AdditionalServiceAccounts = 2,\n            MaxAutoscaleSeats = 100,\n            PremiumAccessAddon = false,\n            CollectionName = \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\"\n        };\n\n        var requestContent = new StringContent(JsonSerializer.Serialize(createRequest), Encoding.UTF8, \"application/json\");\n\n        var stopwatch = System.Diagnostics.Stopwatch.StartNew();\n\n        var response = await client.PostAsync(\"/organizations/create-without-payment\", requestContent);\n\n        stopwatch.Stop();\n\n        testOutputHelper.WriteLine($\"POST /organizations/create-without-payment - AdditionalSeats: {createRequest.AdditionalSeats}; AdditionalStorageGb: {createRequest.AdditionalStorageGb}; AdditionalSmSeats: {createRequest.AdditionalSmSeats}; AdditionalServiceAccounts: {createRequest.AdditionalServiceAccounts}; MaxAutoscaleSeats: {createRequest.MaxAutoscaleSeats}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationsControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class OrganizationsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private Organization _organization = null!;\n    private string _ownerEmail = null!;\n    private readonly string _billingEmail = \"billing@example.com\";\n    private readonly string _organizationName = \"Organizations Controller Test Org\";\n\n    public OrganizationsControllerTests(ApiApplicationFactory apiFactory)\n    {\n        _factory = apiFactory;\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"org-integration-test-{Guid.NewGuid()}@example.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            name: _organizationName,\n            billingEmail: _billingEmail,\n            plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail,\n            passwordManagerSeats: 5,\n            paymentMethod: PaymentMethodType.Card);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task Put_AsOwner_WithoutProvider_CanUpdateOrganization()\n    {\n        // Arrange - Regular organization owner (no provider)\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var updateRequest = new OrganizationUpdateRequestModel\n        {\n            Name = \"Updated Organization Name\",\n            BillingEmail = \"newbillingemail@example.com\"\n        };\n\n        // Act\n        var response = await _client.PutAsJsonAsync($\"/organizations/{_organization.Id}\", updateRequest);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Verify the organization name was updated\n        var organizationRepository = _factory.GetService<IOrganizationRepository>();\n        var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);\n        Assert.NotNull(updatedOrg);\n        Assert.Equal(\"Updated Organization Name\", updatedOrg.Name);\n        Assert.Equal(\"newbillingemail@example.com\", updatedOrg.BillingEmail);\n    }\n\n    [Fact]\n    public async Task Put_AsProvider_CanUpdateOrganization()\n    {\n        // Create and login as a new account to be the provider user (not the owner)\n        var providerUserEmail = $\"provider-{Guid.NewGuid()}@example.com\";\n        var (token, _) = await _factory.LoginWithNewAccount(providerUserEmail);\n\n        // Set up provider linked to org and ProviderUser entry\n        var provider = await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id,\n            ProviderType.Msp);\n        await ProviderTestHelpers.CreateProviderUserAsync(_factory, provider.Id, providerUserEmail,\n            ProviderUserType.ProviderAdmin);\n\n        await _loginHelper.LoginAsync(providerUserEmail);\n\n        var updateRequest = new OrganizationUpdateRequestModel\n        {\n            Name = \"Updated Organization Name\",\n            BillingEmail = \"newbillingemail@example.com\"\n        };\n\n        // Act\n        var response = await _client.PutAsJsonAsync($\"/organizations/{_organization.Id}\", updateRequest);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Verify the organization name was updated\n        var organizationRepository = _factory.GetService<IOrganizationRepository>();\n        var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);\n        Assert.NotNull(updatedOrg);\n        Assert.Equal(\"Updated Organization Name\", updatedOrg.Name);\n        Assert.Equal(\"newbillingemail@example.com\", updatedOrg.BillingEmail);\n    }\n\n    [Fact]\n    public async Task Put_NotMemberOrProvider_CannotUpdateOrganization()\n    {\n        // Create and login as a new account to be unrelated to the org\n        var userEmail = \"stranger@example.com\";\n        await _factory.LoginWithNewAccount(userEmail);\n        await _loginHelper.LoginAsync(userEmail);\n\n        var updateRequest = new OrganizationUpdateRequestModel\n        {\n            Name = \"Updated Organization Name\",\n            BillingEmail = \"newbillingemail@example.com\"\n        };\n\n        // Act\n        var response = await _client.PutAsJsonAsync($\"/organizations/{_organization.Id}\", updateRequest);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n\n        // Verify the organization name was not updated\n        var organizationRepository = _factory.GetService<IOrganizationRepository>();\n        var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);\n        Assert.NotNull(updatedOrg);\n        Assert.Equal(_organizationName, updatedOrg.Name);\n        Assert.Equal(_billingEmail, updatedOrg.BillingEmail);\n    }\n\n    [Fact]\n    public async Task Put_AsOwner_WithProvider_CanRenameOrganization()\n    {\n        // Arrange - Create provider and link to organization\n        // The active user is ONLY an org owner, NOT a provider user\n        await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id, ProviderType.Msp);\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var updateRequest = new OrganizationUpdateRequestModel\n        {\n            Name = \"Updated Organization Name\",\n            BillingEmail = null\n        };\n\n        // Act\n        var response = await _client.PutAsJsonAsync($\"/organizations/{_organization.Id}\", updateRequest);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Verify the organization name was actually updated\n        var organizationRepository = _factory.GetService<IOrganizationRepository>();\n        var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);\n        Assert.NotNull(updatedOrg);\n        Assert.Equal(\"Updated Organization Name\", updatedOrg.Name);\n        Assert.Equal(_billingEmail, updatedOrg.BillingEmail);\n    }\n\n    [Fact]\n    public async Task Put_AsOwner_WithProvider_CannotChangeBillingEmail()\n    {\n        // Arrange - Create provider and link to organization\n        // The active user is ONLY an org owner, NOT a provider user\n        await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id, ProviderType.Msp);\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var updateRequest = new OrganizationUpdateRequestModel\n        {\n            Name = \"Updated Organization Name\",\n            BillingEmail = \"updatedbilling@example.com\"\n        };\n\n        // Act\n        var response = await _client.PutAsJsonAsync($\"/organizations/{_organization.Id}\", updateRequest);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n\n        // Verify the organization was not updated\n        var organizationRepository = _factory.GetService<IOrganizationRepository>();\n        var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);\n        Assert.NotNull(updatedOrg);\n        Assert.Equal(_organizationName, updatedOrg.Name);\n        Assert.Equal(_billingEmail, updatedOrg.BillingEmail);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs",
    "content": "﻿using System.Net;\nusing System.Text.Json;\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Controllers;\n\npublic class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private Organization _organization = null!;\n    private string _ownerEmail = null!;\n\n    public PoliciesControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _factory.SubstituteService<Core.Services.IFeatureService>(featureService =>\n        {\n            featureService\n                .IsEnabled(\"pm-19467-create-default-location\")\n                .Returns(true);\n        });\n        _client = factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task PutVNext_OrganizationDataOwnershipPolicy_Success()\n    {\n        // Arrange\n        const PolicyType policyType = PolicyType.OrganizationDataOwnership;\n\n        const string defaultCollectionName = \"Test Default Collection\";\n        var request = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true,\n            },\n            Metadata = new Dictionary<string, object>\n            {\n                { \"defaultUserCollectionName\", defaultCollectionName }\n            }\n        };\n\n        var (_, admin) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.Admin);\n\n        var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,\n            _organization.Id, OrganizationUserType.User);\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}/vnext\",\n            JsonContent.Create(request));\n\n        // Assert\n        await AssertResponse();\n\n        await AssertPolicy();\n\n        await AssertDefaultCollectionCreatedOnlyForUserTypeAsync();\n        return;\n\n        async Task AssertDefaultCollectionCreatedOnlyForUserTypeAsync()\n        {\n            var collectionRepository = _factory.GetService<ICollectionRepository>();\n            await AssertUserExpectations(collectionRepository);\n            await AssertAdminExpectations(collectionRepository);\n        }\n\n        async Task AssertUserExpectations(ICollectionRepository collectionRepository)\n        {\n            var collections = await collectionRepository.GetManyByUserIdAsync(user.UserId!.Value);\n            var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName);\n            Assert.NotNull(defaultCollection);\n            Assert.Equal(_organization.Id, defaultCollection.OrganizationId);\n        }\n\n        async Task AssertAdminExpectations(ICollectionRepository collectionRepository)\n        {\n            var collections = await collectionRepository.GetManyByUserIdAsync(admin.UserId!.Value);\n            var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName);\n            Assert.Null(defaultCollection);\n        }\n\n        async Task AssertResponse()\n        {\n            Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n            var content = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();\n\n            Assert.True(content.Enabled);\n            Assert.Equal(policyType, content.Type);\n            Assert.Equal(_organization.Id, content.OrganizationId);\n        }\n\n        async Task AssertPolicy()\n        {\n            var policyRepository = _factory.GetService<IPolicyRepository>();\n            var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);\n\n            Assert.NotNull(policy);\n            Assert.True(policy.Enabled);\n            Assert.Equal(policyType, policy.Type);\n            Assert.Null(policy.Data);\n            Assert.Equal(_organization.Id, policy.OrganizationId);\n        }\n    }\n\n    [Fact]\n    public async Task PutVNext_MasterPasswordPolicy_Success()\n    {\n        // Arrange\n        var policyType = PolicyType.MasterPassword;\n        var request = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true,\n                Data = new Dictionary<string, object>\n                {\n                    { \"minComplexity\", 4 },\n                    { \"minLength\", 128 },\n                    { \"requireUpper\", true },\n                    { \"requireLower\", false },\n                    { \"requireNumbers\", true },\n                    { \"requireSpecial\", false },\n                    { \"enforceOnLogin\", true }\n                }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}/vnext\",\n            JsonContent.Create(request));\n\n        // Assert\n        await AssertResponse();\n\n        await AssertPolicyDataForMasterPasswordPolicy();\n        return;\n\n        async Task AssertPolicyDataForMasterPasswordPolicy()\n        {\n            var policyRepository = _factory.GetService<IPolicyRepository>();\n            var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);\n\n            AssertPolicy(policy);\n            AssertMasterPasswordPolicyData(policy);\n        }\n\n        async Task AssertResponse()\n        {\n            Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n            var content = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();\n\n            Assert.True(content.Enabled);\n            Assert.Equal(policyType, content.Type);\n            Assert.Equal(_organization.Id, content.OrganizationId);\n        }\n\n        void AssertPolicy(Policy policy)\n        {\n            Assert.NotNull(policy);\n            Assert.True(policy.Enabled);\n            Assert.Equal(policyType, policy.Type);\n            Assert.Equal(_organization.Id, policy.OrganizationId);\n            Assert.NotNull(policy.Data);\n        }\n\n        void AssertMasterPasswordPolicyData(Policy policy)\n        {\n            var resultData = policy.GetDataModel<MasterPasswordPolicyData>();\n\n            var json = JsonSerializer.Serialize(request.Policy.Data);\n            var expectedData = JsonSerializer.Deserialize<MasterPasswordPolicyData>(json);\n            AssertHelper.AssertPropertyEqual(resultData, expectedData);\n        }\n    }\n\n    [Fact]\n    public async Task Put_MasterPasswordPolicy_InvalidDataType_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.MasterPassword;\n        var request = new PolicyRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"minLength\", \"not a number\" }, // Wrong type - should be int\n                { \"requireUpper\", true }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"minLength\", content); // Verify field name is in error message\n    }\n\n    [Fact]\n    public async Task Put_SendOptionsPolicy_InvalidDataType_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.SendOptions;\n        var request = new PolicyRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"disableHideEmail\", \"not a boolean\" } // Wrong type - should be bool\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_ResetPasswordPolicy_InvalidDataType_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.ResetPassword;\n        var request = new PolicyRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"autoEnrollEnabled\", 123 } // Wrong type - should be bool\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PutVNext_MasterPasswordPolicy_InvalidDataType_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.MasterPassword;\n        var request = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true,\n                Data = new Dictionary<string, object>\n                {\n                    { \"minComplexity\", \"not a number\" }, // Wrong type - should be int\n                    { \"minLength\", 12 }\n                }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}/vnext\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"minComplexity\", content); // Verify field name is in error message\n    }\n\n    [Fact]\n    public async Task PutVNext_SendOptionsPolicy_InvalidDataType_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.SendOptions;\n        var request = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true,\n                Data = new Dictionary<string, object>\n                {\n                    { \"disableHideEmail\", \"not a boolean\" } // Wrong type - should be bool\n                }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}/vnext\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PutVNext_ResetPasswordPolicy_InvalidDataType_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.ResetPassword;\n        var request = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true,\n                Data = new Dictionary<string, object>\n                {\n                    { \"autoEnrollEnabled\", 123 } // Wrong type - should be bool\n                }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}/vnext\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_PolicyWithNullData_Success()\n    {\n        // Arrange\n        var policyType = PolicyType.SingleOrg;\n        var request = new PolicyRequestModel\n        {\n            Enabled = true,\n            Data = null\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PutVNext_PolicyWithNullData_Success()\n    {\n        // Arrange\n        var policyType = PolicyType.TwoFactorAuthentication;\n        var request = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true,\n                Data = null\n            },\n            Metadata = null\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}/vnext\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_MasterPasswordPolicy_ExcessiveMinLength_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.MasterPassword;\n        var request = new PolicyRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"minLength\", 129 }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_MasterPasswordPolicy_ExcessiveMinComplexity_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.MasterPassword;\n        var request = new PolicyRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"minComplexity\", 5 }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/organizations/{_organization.Id}/policies/{policyType}\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_SingleOrgPolicy_RevokesNonCompliantUser()\n    {\n        // Arrange\n        // Create a second organization (Org B) with its own owner\n        var orgBOwnerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(orgBOwnerEmail);\n        var (orgB, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: orgBOwnerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);\n\n        // Create a user that belongs to both Org A and Org B\n        var multiOrgUserEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(multiOrgUserEmail);\n\n        var orgUserInOrgA = await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id,\n            multiOrgUserEmail, OrganizationUserType.User);\n        await OrganizationTestHelpers.CreateUserAsync(_factory, orgB.Id,\n            multiOrgUserEmail, OrganizationUserType.User);\n\n        // Re-authenticate as the owner of Org A\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var request = new PolicyRequestModel\n        {\n            Enabled = true,\n            Data = null\n        };\n\n        // Act - Enable Single Org policy on Org A\n        var response = await _client.PutAsync(\n            $\"/organizations/{_organization.Id}/policies/{PolicyType.SingleOrg}\",\n            JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Verify the multi-org user was revoked in Org A\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var updatedOrgUser = await organizationUserRepository.GetByIdAsync(orgUserInOrgA.Id);\n        Assert.NotNull(updatedOrgUser);\n        Assert.Equal(OrganizationUserStatusType.Revoked, updatedOrgUser.Status);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Import;\n\npublic class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n    private Organization _organization = null!;\n    private string _ownerEmail = null!;\n\n    public ImportOrganizationUsersAndGroupsCommandTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        // Create the owner account\n        _ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        // Create the organization\n        (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);\n\n        // Authorize with the organization api key\n        await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task Import_Existing_Organization_User_Succeeds()\n    {\n        var (email, ou) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,\n            OrganizationUserType.User);\n\n        var externalId = Guid.NewGuid().ToString();\n        var request = new OrganizationImportRequestModel();\n        request.LargeImport = false;\n        request.OverwriteExisting = false;\n        request.Groups = [];\n        request.Members = [\n            new OrganizationImportRequestModel.OrganizationImportMemberRequestModel\n            {\n                Email = email,\n                ExternalId = externalId,\n                Deleted = false\n            }\n        ];\n\n        var response = await _client.PostAsync($\"/public/organization/import\", JsonContent.Create(request));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Assert against the database values\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var orgUser = await organizationUserRepository.GetByIdAsync(ou.Id);\n\n        Assert.NotNull(orgUser);\n        Assert.Equal(ou.Id, orgUser.Id);\n        Assert.Equal(email, orgUser.Email);\n        Assert.Equal(OrganizationUserType.User, orgUser.Type);\n        Assert.Equal(externalId, orgUser.ExternalId);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, orgUser.Status);\n        Assert.Equal(_organization.Id, orgUser.OrganizationId);\n\n    }\n\n    [Fact]\n    public async Task Import_New_Organization_User_Succeeds()\n    {\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n\n        var externalId = Guid.NewGuid().ToString();\n        var request = new OrganizationImportRequestModel();\n        request.LargeImport = false;\n        request.OverwriteExisting = false;\n        request.Groups = [];\n        request.Members = [\n            new OrganizationImportRequestModel.OrganizationImportMemberRequestModel\n            {\n                Email = email,\n                ExternalId = externalId,\n                Deleted = false\n            }\n        ];\n\n        var response = await _client.PostAsync($\"/public/organization/import\", JsonContent.Create(request));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Assert against the database values\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var orgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, email);\n\n        Assert.NotNull(orgUser);\n        Assert.Equal(email, orgUser.Email);\n        Assert.Equal(OrganizationUserType.User, orgUser.Type);\n        Assert.Equal(externalId, orgUser.ExternalId);\n        Assert.Equal(OrganizationUserStatusType.Invited, orgUser.Status);\n        Assert.Equal(_organization.Id, orgUser.OrganizationId);\n    }\n\n    [Fact]\n    public async Task Import_New_And_Existing_Organization_Users_Succeeds()\n    {\n        // Existing organization user\n        var (existingEmail, ou) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,\n            OrganizationUserType.User);\n        var existingExternalId = Guid.NewGuid().ToString();\n\n        // New organization user\n        var newEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(newEmail);\n        var newExternalId = Guid.NewGuid().ToString();\n\n        var request = new OrganizationImportRequestModel();\n        request.LargeImport = false;\n        request.OverwriteExisting = false;\n        request.Groups = [];\n        request.Members = [\n            new OrganizationImportRequestModel.OrganizationImportMemberRequestModel\n            {\n                Email = existingEmail,\n                ExternalId = existingExternalId,\n                Deleted = false\n            },\n            new OrganizationImportRequestModel.OrganizationImportMemberRequestModel\n            {\n                Email = newEmail,\n                ExternalId = newExternalId,\n                Deleted = false\n            }\n        ];\n\n        var response = await _client.PostAsync($\"/public/organization/import\", JsonContent.Create(request));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Assert against the database values\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n\n        // Existing user\n        var existingOrgUser = await organizationUserRepository.GetByIdAsync(ou.Id);\n        Assert.NotNull(existingOrgUser);\n        Assert.Equal(existingEmail, existingOrgUser.Email);\n        Assert.Equal(OrganizationUserType.User, existingOrgUser.Type);\n        Assert.Equal(existingExternalId, existingOrgUser.ExternalId);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, existingOrgUser.Status);\n        Assert.Equal(_organization.Id, existingOrgUser.OrganizationId);\n\n        // New User\n        var newOrgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, newEmail);\n        Assert.NotNull(newOrgUser);\n        Assert.Equal(newEmail, newOrgUser.Email);\n        Assert.Equal(OrganizationUserType.User, newOrgUser.Type);\n        Assert.Equal(newExternalId, newOrgUser.ExternalId);\n        Assert.Equal(OrganizationUserStatusType.Invited, newOrgUser.Status);\n        Assert.Equal(_organization.Id, newOrgUser.OrganizationId);\n    }\n\n    [Fact]\n    public async Task Import_Existing_Groups_Succeeds()\n    {\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var group = await OrganizationTestHelpers.CreateGroup(_factory, _organization.Id);\n        var request = new OrganizationImportRequestModel();\n        var addedMember = new OrganizationImportRequestModel.OrganizationImportMemberRequestModel\n        {\n            Email = \"test@test.com\",\n            ExternalId = \"bwtest-externalId\",\n            Deleted = false\n        };\n\n        request.LargeImport = false;\n        request.OverwriteExisting = false;\n        request.Groups = [\n            new OrganizationImportRequestModel.OrganizationImportGroupRequestModel\n            {\n                Name = \"new-name\",\n                ExternalId = \"bwtest-externalId\",\n                MemberExternalIds = []\n            }\n        ];\n        request.Members = [addedMember];\n\n        var response = await _client.PostAsync($\"/public/organization/import\", JsonContent.Create(request));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Assert against the database values\n        var groupRepository = _factory.GetService<IGroupRepository>();\n        var existingGroups = (await groupRepository.GetManyByOrganizationIdAsync(_organization.Id)).ToArray();\n\n        // Assert that we are actually updating the existing group, not adding a new one.\n        Assert.Single(existingGroups);\n        Assert.NotNull(existingGroups[0]);\n        Assert.Equal(group.Id, existingGroups[0].Id);\n        Assert.Equal(\"new-name\", existingGroups[0].Name);\n        Assert.Equal(group.ExternalId, existingGroups[0].ExternalId);\n\n        var addedOrgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, addedMember.Email);\n        Assert.NotNull(addedOrgUser);\n    }\n\n    [Fact]\n    public async Task Import_New_Groups_Succeeds()\n    {\n        var group = new Group\n        {\n            OrganizationId = _organization.Id,\n            ExternalId = new Guid().ToString(),\n            Name = \"bwtest1\"\n        };\n\n        var request = new OrganizationImportRequestModel();\n        request.LargeImport = false;\n        request.OverwriteExisting = false;\n        request.Groups = [\n            new OrganizationImportRequestModel.OrganizationImportGroupRequestModel\n            {\n                Name = group.Name,\n                ExternalId = group.ExternalId,\n                MemberExternalIds = []\n            }\n        ];\n        request.Members = [];\n\n        var response = await _client.PostAsync($\"/public/organization/import\", JsonContent.Create(request));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Assert against the database values\n        var groupRepository = _factory.GetService<IGroupRepository>();\n        var existingGroups = await groupRepository.GetManyByOrganizationIdAsync(_organization.Id);\n        var existingGroup = existingGroups.Where(g => g.ExternalId == group.ExternalId).FirstOrDefault();\n\n        Assert.NotNull(existingGroup);\n        Assert.Equal(existingGroup.Name, group.Name);\n        Assert.Equal(existingGroup.ExternalId, group.ExternalId);\n    }\n\n    [Fact]\n    public async Task Import_New_And_Existing_Groups_Succeeds()\n    {\n        var existingGroup = await OrganizationTestHelpers.CreateGroup(_factory, _organization.Id);\n\n        var newGroup = new Group\n        {\n            OrganizationId = _organization.Id,\n            ExternalId = \"test\",\n            Name = \"bwtest1\"\n        };\n\n        var request = new OrganizationImportRequestModel();\n        request.LargeImport = false;\n        request.OverwriteExisting = false;\n        request.Groups = [\n            new OrganizationImportRequestModel.OrganizationImportGroupRequestModel\n            {\n                Name = \"new-name\",\n                ExternalId = existingGroup.ExternalId,\n                MemberExternalIds = []\n            },\n            new OrganizationImportRequestModel.OrganizationImportGroupRequestModel\n            {\n                Name = newGroup.Name,\n                ExternalId = newGroup.ExternalId,\n                MemberExternalIds = []\n            }\n        ];\n        request.Members = [];\n\n        var response = await _client.PostAsync($\"/public/organization/import\", JsonContent.Create(request));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Assert against the database values\n        var groupRepository = _factory.GetService<IGroupRepository>();\n        var groups = await groupRepository.GetManyByOrganizationIdAsync(_organization.Id);\n\n        var newGroupInDb = groups.Where(g => g.ExternalId == newGroup.ExternalId).FirstOrDefault();\n        Assert.NotNull(newGroupInDb);\n        Assert.Equal(newGroupInDb.Name, newGroup.Name);\n        Assert.Equal(newGroupInDb.ExternalId, newGroup.ExternalId);\n\n        var existingGroupInDb = groups.Where(g => g.ExternalId == existingGroup.ExternalId).FirstOrDefault();\n        Assert.NotNull(existingGroupInDb);\n        Assert.Equal(existingGroup.Id, existingGroupInDb.Id);\n        Assert.Equal(\"new-name\", existingGroupInDb.Name);\n        Assert.Equal(existingGroup.ExternalId, existingGroupInDb.ExternalId);\n    }\n\n    [Fact]\n    public async Task Import_Remove_Member_Without_Master_Password_Throws_400_Error()\n    {\n        // ARRANGE: a member without a master password\n        await OrganizationTestHelpers.CreateUserWithoutMasterPasswordAsync(_factory, Guid.NewGuid() + \"@example.com\",\n            _organization.Id);\n\n        // ACT: an import request that would remove that member\n        var request = new OrganizationImportRequestModel\n        {\n            LargeImport = false,\n            OverwriteExisting = true, // removes all members not in the request\n            Groups = [],\n            Members = []\n        };\n\n        var response = await _client.PostAsync($\"/public/organization/import\", JsonContent.Create(request));\n\n        // ASSERT: that a 400 error is thrown with the correct error message\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n\n        var responseContent = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Sync failed. To proceed, disable the 'Remove and re-add users during next sync' setting and try again.\", responseContent);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Public/Controllers/MembersControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.AdminConsole.Public.Models;\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Api.AdminConsole.Public.Models.Response;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Public.Controllers;\n\npublic class MembersControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    // These will get set in `InitializeAsync` which is ran before all tests\n    private Organization _organization = null!;\n    private string _ownerEmail = null!;\n\n    public MembersControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _factory.SubstituteService<IFeatureService>(_ => { });\n        _client = factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        // Create the owner account\n        _ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        // Create the organization\n        (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);\n\n        // Authorize with the organization api key\n        await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task List_Member_Success()\n    {\n        var (userEmail1, orgUser1) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,\n            OrganizationUserType.Custom, new Permissions { AccessImportExport = true, ManagePolicies = true, AccessReports = true });\n        var (userEmail2, orgUser2) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,\n            OrganizationUserType.Owner);\n        var (userEmail3, orgUser3) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,\n            OrganizationUserType.User);\n        var (userEmail4, orgUser4) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,\n            OrganizationUserType.Admin);\n\n        var collection1 = await OrganizationTestHelpers.CreateCollectionAsync(_factory, _organization.Id, \"Test Collection 1\", users:\n        [\n            new CollectionAccessSelection { Id = orgUser1.Id, ReadOnly = false, HidePasswords = false, Manage = true },\n            new CollectionAccessSelection { Id = orgUser3.Id, ReadOnly = true, HidePasswords = false, Manage = false }\n        ]);\n\n        var collection2 = await OrganizationTestHelpers.CreateCollectionAsync(_factory, _organization.Id, \"Test Collection 2\", users:\n        [\n            new CollectionAccessSelection { Id = orgUser1.Id, ReadOnly = false, HidePasswords = true, Manage = false }\n        ]);\n\n        var response = await _client.GetAsync($\"/public/members\");\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<MemberResponseModel>>();\n        Assert.NotNull(result?.Data);\n        Assert.Equal(5, result.Data.Count());\n\n        // The owner\n        var ownerResult = result.Data.SingleOrDefault(m => m.Email == _ownerEmail && m.Type == OrganizationUserType.Owner);\n        Assert.NotNull(ownerResult);\n        Assert.Empty(ownerResult.Collections);\n\n        // The custom user with collections\n        var user1Result = result.Data.Single(m => m.Email == userEmail1);\n        Assert.Equal(OrganizationUserType.Custom, user1Result.Type);\n        AssertHelper.AssertPropertyEqual(\n            new PermissionsModel { AccessImportExport = true, ManagePolicies = true, AccessReports = true },\n            user1Result.Permissions);\n        // Verify collections\n        Assert.NotNull(user1Result.Collections);\n        Assert.Equal(2, user1Result.Collections.Count());\n        var user1Collection1 = user1Result.Collections.Single(c => c.Id == collection1.Id);\n        Assert.False(user1Collection1.ReadOnly);\n        Assert.False(user1Collection1.HidePasswords);\n        Assert.True(user1Collection1.Manage);\n        var user1Collection2 = user1Result.Collections.Single(c => c.Id == collection2.Id);\n        Assert.False(user1Collection2.ReadOnly);\n        Assert.True(user1Collection2.HidePasswords);\n        Assert.False(user1Collection2.Manage);\n\n        // The other owner\n        var user2Result = result.Data.SingleOrDefault(m => m.Email == userEmail2 && m.Type == OrganizationUserType.Owner);\n        Assert.NotNull(user2Result);\n        Assert.Empty(user2Result.Collections);\n\n        // The user with one collection\n        var user3Result = result.Data.SingleOrDefault(m => m.Email == userEmail3 && m.Type == OrganizationUserType.User);\n        Assert.NotNull(user3Result);\n        Assert.NotNull(user3Result.Collections);\n        Assert.Single(user3Result.Collections);\n        var user3Collection1 = user3Result.Collections.Single(c => c.Id == collection1.Id);\n        Assert.True(user3Collection1.ReadOnly);\n        Assert.False(user3Collection1.HidePasswords);\n        Assert.False(user3Collection1.Manage);\n\n        // The admin with no collections\n        var user4Result = result.Data.SingleOrDefault(m => m.Email == userEmail4 && m.Type == OrganizationUserType.Admin);\n        Assert.NotNull(user4Result);\n        Assert.Empty(user4Result.Collections);\n    }\n\n    [Fact]\n    public async Task Get_CustomMember_Success()\n    {\n        var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,\n            OrganizationUserType.Custom, new Permissions { AccessReports = true, ManageScim = true });\n\n        var response = await _client.GetAsync($\"/public/members/{orgUser.Id}\");\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();\n        Assert.NotNull(result);\n        Assert.Equal(email, result.Email);\n\n        Assert.Equal(OrganizationUserType.Custom, result.Type);\n        AssertHelper.AssertPropertyEqual(new PermissionsModel { AccessReports = true, ManageScim = true },\n            result.Permissions);\n    }\n\n    [Fact]\n    public async Task Post_CustomMember_Success()\n    {\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        var request = new MemberCreateRequestModel\n        {\n            Email = email,\n            Type = OrganizationUserType.Custom,\n            ExternalId = \"myCustomUser\",\n            Collections = [],\n            Groups = []\n        };\n\n        var response = await _client.PostAsync(\"/public/members\", JsonContent.Create(request));\n\n        // Assert against the response\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();\n        Assert.NotNull(result);\n\n        Assert.Equal(email, result.Email);\n        Assert.Equal(OrganizationUserType.Custom, result.Type);\n        Assert.Equal(\"myCustomUser\", result.ExternalId);\n        Assert.Empty(result.Collections);\n\n        // Assert against the database values\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var orgUser = await organizationUserRepository.GetByIdAsync(result.Id);\n\n        Assert.NotNull(orgUser);\n        Assert.Equal(email, orgUser.Email);\n        Assert.Equal(OrganizationUserType.Custom, orgUser.Type);\n        Assert.Equal(\"myCustomUser\", orgUser.ExternalId);\n        Assert.Equal(OrganizationUserStatusType.Invited, orgUser.Status);\n        Assert.Equal(_organization.Id, orgUser.OrganizationId);\n    }\n\n    [Fact]\n    public async Task Put_CustomMember_Success()\n    {\n        var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,\n            OrganizationUserType.User);\n\n        var request = new MemberUpdateRequestModel\n        {\n            Type = OrganizationUserType.Custom,\n            Permissions = new PermissionsModel\n            {\n                DeleteAnyCollection = true,\n                EditAnyCollection = true,\n                AccessEventLogs = true\n            },\n            ExternalId = \"example\",\n            Collections = []\n        };\n\n        var response = await _client.PutAsync($\"/public/members/{orgUser.Id}\", JsonContent.Create(request));\n\n        // Assert against the response\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();\n        Assert.NotNull(result);\n\n        Assert.Equal(email, result.Email);\n        Assert.Equal(OrganizationUserType.Custom, result.Type);\n        Assert.Equal(\"example\", result.ExternalId);\n        AssertHelper.AssertPropertyEqual(\n            new PermissionsModel { DeleteAnyCollection = true, EditAnyCollection = true, AccessEventLogs = true },\n            result.Permissions);\n        Assert.Empty(result.Collections);\n\n        // Assert against the database values\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var updatedOrgUser = await organizationUserRepository.GetByIdAsync(result.Id);\n\n        Assert.NotNull(updatedOrgUser);\n        Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type);\n        Assert.Equal(\"example\", updatedOrgUser.ExternalId);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, updatedOrgUser.Status);\n        Assert.Equal(_organization.Id, updatedOrgUser.OrganizationId);\n    }\n\n    /// <summary>\n    /// The Permissions property is optional and should not overwrite existing Permissions if not provided.\n    /// This is to preserve backwards compatibility with existing usage.\n    /// </summary>\n    [Fact]\n    public async Task Put_ExistingCustomMember_NullPermissions_DoesNotOverwritePermissions()\n    {\n        var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,\n            OrganizationUserType.Custom, new Permissions { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true });\n\n        var request = new MemberUpdateRequestModel\n        {\n            Type = OrganizationUserType.Custom,\n            ExternalId = \"example\",\n            Collections = []\n        };\n\n        var response = await _client.PutAsync($\"/public/members/{orgUser.Id}\", JsonContent.Create(request));\n\n        // Assert against the response\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();\n        Assert.NotNull(result);\n\n        Assert.Equal(OrganizationUserType.Custom, result.Type);\n        AssertHelper.AssertPropertyEqual(\n            new PermissionsModel { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true },\n            result.Permissions);\n\n        // Assert against the database values\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var updatedOrgUser = await organizationUserRepository.GetByIdAsync(result.Id);\n\n        Assert.NotNull(updatedOrgUser);\n        Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type);\n        AssertHelper.AssertPropertyEqual(\n            new Permissions { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true },\n            orgUser.GetPermissions());\n    }\n\n    [Fact]\n    public async Task Revoke_Member_Success()\n    {\n        var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.User);\n\n        var response = await _client.PostAsync($\"/public/members/{orgUser.Id}/revoke\", null);\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        var updatedUser = await _factory.GetService<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUser.Id);\n        Assert.NotNull(updatedUser);\n        Assert.Equal(OrganizationUserStatusType.Revoked, updatedUser.Status);\n    }\n\n    [Fact]\n    public async Task Revoke_AlreadyRevoked_ReturnsBadRequest()\n    {\n        var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.User);\n\n        var revokeResponse = await _client.PostAsync($\"/public/members/{orgUser.Id}/revoke\", null);\n        Assert.Equal(HttpStatusCode.OK, revokeResponse.StatusCode);\n\n        var response = await _client.PostAsync($\"/public/members/{orgUser.Id}/revoke\", null);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var error = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();\n        Assert.Equal(\"Already revoked.\", error?.Message);\n    }\n\n    [Fact]\n    public async Task Revoke_NotFound_ReturnsNotFound()\n    {\n        var response = await _client.PostAsync($\"/public/members/{Guid.NewGuid()}/revoke\", null);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Revoke_DifferentOrganization_ReturnsNotFound()\n    {\n        // Create a different organization\n        var ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(ownerEmail);\n        var (otherOrganization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);\n\n        // Create a user in the other organization\n        var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, otherOrganization.Id, OrganizationUserType.User);\n\n        // Re-authenticate with the original organization\n        await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);\n\n        // Try to revoke the user from the other organization\n        var response = await _client.PostAsync($\"/public/members/{orgUser.Id}/revoke\", null);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Restore_Member_Success()\n    {\n        // Invite a user to revoke\n        var email = $\"integration-test{Guid.NewGuid()}@example.com\";\n        var inviteRequest = new MemberCreateRequestModel\n        {\n            Email = email,\n            Type = OrganizationUserType.User,\n        };\n\n        var inviteResponse = await _client.PostAsync(\"/public/members\", JsonContent.Create(inviteRequest));\n        Assert.Equal(HttpStatusCode.OK, inviteResponse.StatusCode);\n        var invitedMember = await inviteResponse.Content.ReadFromJsonAsync<MemberResponseModel>();\n        Assert.NotNull(invitedMember);\n\n        // Revoke the invited user\n        var revokeResponse = await _client.PostAsync($\"/public/members/{invitedMember.Id}/revoke\", null);\n        Assert.Equal(HttpStatusCode.OK, revokeResponse.StatusCode);\n\n        // Restore the user\n        var response = await _client.PostAsync($\"/public/members/{invitedMember.Id}/restore\", null);\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Verify user is restored to Invited state\n        var updatedUser = await _factory.GetService<IOrganizationUserRepository>()\n            .GetByIdAsync(invitedMember.Id);\n        Assert.NotNull(updatedUser);\n        Assert.Equal(OrganizationUserStatusType.Invited, updatedUser.Status);\n    }\n\n    [Fact]\n    public async Task Restore_AlreadyActive_ReturnsBadRequest()\n    {\n        var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.User);\n\n        var response = await _client.PostAsync($\"/public/members/{orgUser.Id}/restore\", null);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var error = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();\n        Assert.Equal(\"Already active.\", error?.Message);\n    }\n\n    [Fact]\n    public async Task Restore_NotFound_ReturnsNotFound()\n    {\n        var response = await _client.PostAsync($\"/public/members/{Guid.NewGuid()}/restore\", null);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Restore_DifferentOrganization_ReturnsNotFound()\n    {\n        // Create a different organization\n        var ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(ownerEmail);\n        var (otherOrganization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);\n\n        // Create a user in the other organization\n        var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, otherOrganization.Id, OrganizationUserType.User);\n\n        // Re-authenticate with the original organization\n        await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);\n\n        // Try to restore the user from the other organization\n        var response = await _client.PostAsync($\"/public/members/{orgUser.Id}/restore\", null);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Post_CustomMember_WithPublicMembersInviteRefactor_Success()\n    {\n        var featureService = _factory.GetService<IFeatureService>();\n        featureService\n            .IsEnabled(FeatureFlagKeys.PublicMembersInviteRefactor)\n            .Returns(true);\n\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        var request = new MemberCreateRequestModel\n        {\n            Email = email,\n            Type = OrganizationUserType.Custom,\n            ExternalId = \"myCustomUser\",\n            Collections = [],\n            Groups = []\n        };\n\n        var response = await _client.PostAsync(\"/public/members\", JsonContent.Create(request));\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();\n        Assert.NotNull(result);\n\n        Assert.Equal(email, result.Email);\n        Assert.Equal(OrganizationUserType.Custom, result.Type);\n        Assert.Equal(\"myCustomUser\", result.ExternalId);\n        Assert.Empty(result.Collections);\n\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var orgUser = await organizationUserRepository.GetByIdAsync(result.Id);\n\n        Assert.NotNull(orgUser);\n        Assert.Equal(email, orgUser.Email);\n        Assert.Equal(OrganizationUserType.Custom, orgUser.Type);\n        Assert.Equal(\"myCustomUser\", orgUser.ExternalId);\n        Assert.Equal(OrganizationUserStatusType.Invited, orgUser.Status);\n        Assert.Equal(_organization.Id, orgUser.OrganizationId);\n    }\n\n    [Fact]\n    public async Task Post_UserMember_WithPublicMembersInviteRefactor_Success()\n    {\n        var featureService = _factory.GetService<IFeatureService>();\n        featureService\n            .IsEnabled(FeatureFlagKeys.PublicMembersInviteRefactor)\n            .Returns(true);\n\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        var request = new MemberCreateRequestModel\n        {\n            Email = email,\n            Type = OrganizationUserType.User,\n            Collections = [],\n            Groups = []\n        };\n\n        var response = await _client.PostAsync(\"/public/members\", JsonContent.Create(request));\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();\n        Assert.NotNull(result);\n\n        Assert.Equal(email, result.Email);\n        Assert.Equal(OrganizationUserType.User, result.Type);\n\n        var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        var orgUser = await organizationUserRepository.GetByIdAsync(result.Id);\n\n        Assert.NotNull(orgUser);\n        Assert.Equal(email, orgUser.Email);\n        Assert.Equal(OrganizationUserType.User, orgUser.Type);\n        Assert.Equal(OrganizationUserStatusType.Invited, orgUser.Status);\n        Assert.Equal(_organization.Id, orgUser.OrganizationId);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/AdminConsole/Public/Controllers/PoliciesControllerTests.cs",
    "content": "﻿using System.Net;\nusing System.Text.Json;\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Api.AdminConsole.Public.Models.Response;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.AdminConsole.Public.Controllers;\n\npublic class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    // These will get set in `InitializeAsync` which is ran before all tests\n    private Organization _organization = null!;\n    private string _ownerEmail = null!;\n\n    public PoliciesControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        // Create the owner account\n        _ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        // Create the organization\n        (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);\n\n        // Authorize with the organization api key\n        await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task Post_NewPolicy()\n    {\n        var policyType = PolicyType.MasterPassword;\n        var request = new PolicyUpdateRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"minComplexity\", 4},\n                { \"minLength\", 128 },\n                { \"requireLower\", true}\n            }\n        };\n\n        var response = await _client.PutAsync($\"/public/policies/{policyType}\", JsonContent.Create(request));\n\n        // Assert against the response\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();\n        Assert.NotNull(result);\n\n        Assert.True(result.Enabled);\n        Assert.Equal(policyType, result.Type);\n        Assert.IsType<Guid>(result.Id);\n        Assert.NotEqual(default, result.Id);\n        Assert.NotNull(result.Data);\n        Assert.Equal(4, ((JsonElement)result.Data[\"minComplexity\"]).GetInt32());\n        Assert.Equal(128, ((JsonElement)result.Data[\"minLength\"]).GetInt32());\n        Assert.True(((JsonElement)result.Data[\"requireLower\"]).GetBoolean());\n\n        // Assert against the database values\n        var policyRepository = _factory.GetService<IPolicyRepository>();\n        var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);\n        Assert.NotNull(policy);\n\n        Assert.True(policy.Enabled);\n        Assert.Equal(policyType, policy.Type);\n        Assert.IsType<Guid>(policy.Id);\n        Assert.NotEqual(default, policy.Id);\n        Assert.Equal(_organization.Id, policy.OrganizationId);\n\n        Assert.NotNull(policy.Data);\n        var data = policy.GetDataModel<MasterPasswordPolicyData>();\n        var expectedData = new MasterPasswordPolicyData { MinComplexity = 4, MinLength = 128, RequireLower = true };\n        AssertHelper.AssertPropertyEqual(expectedData, data);\n    }\n\n    [Fact]\n    public async Task Post_UpdatePolicy()\n    {\n        var policyType = PolicyType.MasterPassword;\n        var existingPolicy = new Policy\n        {\n            OrganizationId = _organization.Id,\n            Enabled = true,\n            Type = policyType\n        };\n        existingPolicy.SetDataModel(new MasterPasswordPolicyData\n        {\n            EnforceOnLogin = true,\n            MinLength = 22,\n            RequireSpecial = true\n        });\n\n        var policyRepository = _factory.GetService<IPolicyRepository>();\n        await policyRepository.UpsertAsync(existingPolicy);\n\n        // The Id isn't set until it's created in the database, get it back out to get the id\n        var createdPolicy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);\n        var expectedId = createdPolicy!.Id;\n\n        var request = new PolicyUpdateRequestModel\n        {\n            Enabled = false,\n            Data = new Dictionary<string, object>\n            {\n                { \"minLength\", 15},\n                { \"requireUpper\", true}\n            }\n        };\n\n        var response = await _client.PutAsync($\"/public/policies/{policyType}\", JsonContent.Create(request));\n\n        // Assert against the response\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<PolicyResponseModel>();\n        Assert.NotNull(result);\n\n        Assert.False(result.Enabled);\n        Assert.Equal(policyType, result.Type);\n        Assert.Equal(expectedId, result.Id);\n        Assert.NotNull(result.Data);\n        Assert.Equal(15, ((JsonElement)result.Data[\"minLength\"]).GetInt32());\n        Assert.True(((JsonElement)result.Data[\"requireUpper\"]).GetBoolean());\n\n        // Assert against the database values\n        var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType);\n        Assert.NotNull(policy);\n\n        Assert.False(policy.Enabled);\n        Assert.Equal(policyType, policy.Type);\n        Assert.Equal(expectedId, policy.Id);\n        Assert.Equal(_organization.Id, policy.OrganizationId);\n\n        Assert.NotNull(policy.Data);\n        var data = policy.GetDataModel<MasterPasswordPolicyData>();\n        Assert.Equal(15, data.MinLength);\n        Assert.Equal(true, data.RequireUpper);\n    }\n\n    [Fact]\n    public async Task Put_MasterPasswordPolicy_InvalidDataType_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.MasterPassword;\n        var request = new PolicyUpdateRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"minLength\", \"not a number\" }, // Wrong type - should be int\n                { \"requireUpper\", true }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/public/policies/{policyType}\", JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_SendOptionsPolicy_InvalidDataType_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.SendOptions;\n        var request = new PolicyUpdateRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"disableHideEmail\", \"not a boolean\" } // Wrong type - should be bool\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/public/policies/{policyType}\", JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_ResetPasswordPolicy_InvalidDataType_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.ResetPassword;\n        var request = new PolicyUpdateRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"autoEnrollEnabled\", 123 } // Wrong type - should be bool\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/public/policies/{policyType}\", JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_PolicyWithNullData_Success()\n    {\n        // Arrange\n        var policyType = PolicyType.DisableSend;\n        var request = new PolicyUpdateRequestModel\n        {\n            Enabled = true,\n            Data = null\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/public/policies/{policyType}\", JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_MasterPasswordPolicy_ExcessiveMinLength_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.MasterPassword;\n        var request = new PolicyUpdateRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"minLength\", 129 }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/public/policies/{policyType}\", JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Put_MasterPasswordPolicy_ExcessiveMinComplexity_ReturnsBadRequest()\n    {\n        // Arrange\n        var policyType = PolicyType.MasterPassword;\n        var request = new PolicyUpdateRequestModel\n        {\n            Enabled = true,\n            Data = new Dictionary<string, object>\n            {\n                { \"minComplexity\", 5 }\n            }\n        };\n\n        // Act\n        var response = await _client.PutAsync($\"/public/policies/{policyType}\", JsonContent.Create(request));\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Api.IntegrationTest.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n  <PropertyGroup>\n    <IsPackable>false</IsPackable>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304;CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Api\\Api.csproj\" />\n    <ProjectReference Include=\"..\\..\\util\\Seeder\\Seeder.csproj\" />\n    <ProjectReference Include=\"..\\IntegrationTestCommon\\IntegrationTestCommon.csproj\" />\n\n    <Content Include=\"..\\..\\src\\Api\\appsettings.*.json\">\n      <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </Content>\n  </ItemGroup>\n  <ItemGroup>\n    <Content Update=\"Properties\\launchSettings.json\">\n      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <CopyToPublishDirectory>Never</CopyToPublishDirectory>\n    </Content>\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Api.IntegrationTest/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.Billing.Controllers;\n\n/// <summary>\n/// Integration tests for OrganizationSponsorshipsController, focusing on authorization checks\n/// for the admin-initiated sponsorship endpoints.\n/// </summary>\npublic class OrganizationSponsorshipsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private Organization _organization = null!;\n    private OrganizationUser _ownerOrgUser = null!;\n    private string _ownerEmail = null!;\n\n    public OrganizationSponsorshipsControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        // Create an Enterprise org (required for sponsorship features)\n        _ownerEmail = $\"sponsorship-test-owner-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        (_organization, _ownerOrgUser) = await OrganizationTestHelpers.SignUpAsync(\n            _factory,\n            plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail,\n            passwordManagerSeats: 5,\n            paymentMethod: PaymentMethodType.Card);\n\n        // Enable the AdminSponsoredFamilies feature on the org\n        var organizationRepository = _factory.GetService<IOrganizationRepository>();\n        _organization.UseAdminSponsoredFamilies = true;\n        await organizationRepository.ReplaceAsync(_organization);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    /// <summary>\n    /// Reproduces VULN-441: Any authenticated user (not a member of the org) can revoke\n    /// admin-initiated sponsorships by calling DELETE /{sponsoringOrgId}/{friendlyName}/revoke.\n    /// This test asserts the CORRECT behavior (should return Forbidden/Unauthorized),\n    /// and will FAIL until the fix is applied.\n    /// </summary>\n    [Fact]\n    public async Task AdminInitiatedRevokeSponsorship_AsNonMember_ReturnsForbidden()\n    {\n        // Arrange: Create a sponsorship directly in the DB for the org\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"victim@example.com\");\n\n        // Create a completely separate user who is NOT a member of the org\n        var attackerEmail = $\"attacker-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(attackerEmail);\n        await _loginHelper.LoginAsync(attackerEmail);\n\n        // Act: The attacker tries to revoke the sponsorship\n        var response = await _client.DeleteAsync(\n            $\"organization/sponsorship/{_organization.Id}/{Uri.EscapeDataString(sponsorship.FriendlyName!)}/revoke\");\n\n        // Assert: Should be rejected — non-members must not be able to revoke sponsorships\n        Assert.True(\n            response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized,\n            $\"Expected 401 or 403 but got {(int)response.StatusCode} {response.StatusCode}. \" +\n            \"Non-org-members should not be able to revoke admin-initiated sponsorships.\");\n\n        // Verify the sponsorship still exists (was NOT deleted)\n        var sponsorshipRepository = _factory.GetService<IOrganizationSponsorshipRepository>();\n        var stillExists = await sponsorshipRepository.GetByIdAsync(sponsorship.Id);\n        Assert.NotNull(stillExists);\n        Assert.False(stillExists.ToDelete, \"Sponsorship should not have been marked for deletion.\");\n    }\n\n    /// <summary>\n    /// Verifies that a regular member (User type, no special permissions) of the org\n    /// also cannot revoke admin-initiated sponsorships.\n    /// </summary>\n    [Fact]\n    public async Task AdminInitiatedRevokeSponsorship_AsRegularMember_ReturnsForbidden()\n    {\n        // Arrange: Create a sponsorship\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"victim2@example.com\");\n\n        // Create a regular member of the org (User type, no ManageUsers permission)\n        var (memberEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.User,\n            permissions: new Permissions { ManageUsers = false });\n\n        await _loginHelper.LoginAsync(memberEmail);\n\n        // Act\n        var response = await _client.DeleteAsync(\n            $\"organization/sponsorship/{_organization.Id}/{Uri.EscapeDataString(sponsorship.FriendlyName!)}/revoke\");\n\n        // Assert: Regular members without ManageUsers should not be able to revoke\n        Assert.True(\n            response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized,\n            $\"Expected 401 or 403 but got {(int)response.StatusCode} {response.StatusCode}. \" +\n            \"Regular org members without ManageUsers should not be able to revoke admin-initiated sponsorships.\");\n\n        // Verify the sponsorship still exists\n        var sponsorshipRepository = _factory.GetService<IOrganizationSponsorshipRepository>();\n        var stillExists = await sponsorshipRepository.GetByIdAsync(sponsorship.Id);\n        Assert.NotNull(stillExists);\n        Assert.False(stillExists.ToDelete, \"Sponsorship should not have been marked for deletion.\");\n    }\n\n    /// <summary>\n    /// Verifies that an org Owner CAN revoke admin-initiated sponsorships (positive test).\n    /// </summary>\n    [Fact]\n    public async Task AdminInitiatedRevokeSponsorship_AsOwner_Succeeds()\n    {\n        // Arrange: Create a sponsorship\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"employee@example.com\");\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        // Act\n        var response = await _client.DeleteAsync(\n            $\"organization/sponsorship/{_organization.Id}/{Uri.EscapeDataString(sponsorship.FriendlyName!)}/revoke\");\n\n        // Assert: Owner should be able to revoke\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Verifies that an org Admin CAN revoke admin-initiated sponsorships.\n    /// </summary>\n    [Fact]\n    public async Task AdminInitiatedRevokeSponsorship_AsAdmin_Succeeds()\n    {\n        // Arrange\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"employee-admin@example.com\");\n\n        var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.Admin);\n\n        await _loginHelper.LoginAsync(adminEmail);\n\n        // Act\n        var response = await _client.DeleteAsync(\n            $\"organization/sponsorship/{_organization.Id}/{Uri.EscapeDataString(sponsorship.FriendlyName!)}/revoke\");\n\n        // Assert: Admin should be able to revoke\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Verifies that a Custom user with ManageUsers permission CAN revoke admin-initiated sponsorships.\n    /// </summary>\n    [Fact]\n    public async Task AdminInitiatedRevokeSponsorship_AsCustomWithManageUsers_Succeeds()\n    {\n        // Arrange\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"employee-custom@example.com\");\n\n        var (customEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.Custom,\n            permissions: new Permissions { ManageUsers = true });\n\n        await _loginHelper.LoginAsync(customEmail);\n\n        // Act\n        var response = await _client.DeleteAsync(\n            $\"organization/sponsorship/{_organization.Id}/{Uri.EscapeDataString(sponsorship.FriendlyName!)}/revoke\");\n\n        // Assert: Custom user with ManageUsers should be able to revoke\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n    }\n\n    /// <summary>\n    /// Reproduces the cross-org attack: user is admin of Org A but tries to revoke\n    /// sponsorships of Org B (of which they are NOT a member).\n    /// </summary>\n    [Fact]\n    public async Task AdminInitiatedRevokeSponsorship_AsAdminOfDifferentOrg_ReturnsForbidden()\n    {\n        // Arrange: Create a sponsorship on the target org\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"cross-org-victim@example.com\");\n\n        // Create a different org and make the attacker its owner\n        var attackerEmail = $\"other-org-admin-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(attackerEmail);\n\n        var (otherOrg, _) = await OrganizationTestHelpers.SignUpAsync(\n            _factory,\n            plan: PlanType.EnterpriseAnnually,\n            ownerEmail: attackerEmail,\n            name: \"Attacker Org\",\n            billingEmail: attackerEmail,\n            passwordManagerSeats: 5,\n            paymentMethod: PaymentMethodType.Card);\n\n        // Log in as the attacker (owner of otherOrg, NOT a member of _organization)\n        await _loginHelper.LoginAsync(attackerEmail);\n\n        // Act: Try to revoke a sponsorship on the target org\n        var response = await _client.DeleteAsync(\n            $\"organization/sponsorship/{_organization.Id}/{Uri.EscapeDataString(sponsorship.FriendlyName!)}/revoke\");\n\n        // Assert: Should be rejected — being admin of another org doesn't grant access\n        Assert.True(\n            response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized,\n            $\"Expected 401 or 403 but got {(int)response.StatusCode} {response.StatusCode}. \" +\n            \"Admin of a different org should not be able to revoke sponsorships of another org.\");\n\n        // Verify the sponsorship still exists\n        var sponsorshipRepository = _factory.GetService<IOrganizationSponsorshipRepository>();\n        var stillExists = await sponsorshipRepository.GetByIdAsync(sponsorship.Id);\n        Assert.NotNull(stillExists);\n        Assert.False(stillExists.ToDelete, \"Sponsorship should not have been marked for deletion.\");\n    }\n\n    #region ResendSponsorshipOffer authorization tests\n\n    /// <summary>\n    /// Verifies that a non-member cannot trigger sponsorship offer emails\n    /// for an organization they don't belong to.\n    /// </summary>\n    [Fact]\n    public async Task ResendSponsorshipOffer_AsNonMember_ReturnsForbidden()\n    {\n        // Arrange\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"resend-victim@example.com\");\n\n        var attackerEmail = $\"resend-attacker-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(attackerEmail);\n        await _loginHelper.LoginAsync(attackerEmail);\n\n        // Act\n        var response = await _client.PostAsync(\n            $\"organization/sponsorship/{_organization.Id}/families-for-enterprise/resend?sponsoredFriendlyName={Uri.EscapeDataString(sponsorship.FriendlyName!)}\",\n            null);\n\n        // Assert\n        Assert.True(\n            response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized,\n            $\"Expected 401 or 403 but got {(int)response.StatusCode} {response.StatusCode}. \" +\n            \"Non-org-members should not be able to resend sponsorship offers.\");\n    }\n\n    /// <summary>\n    /// Verifies that a regular member without ManageUsers cannot resend sponsorship offers.\n    /// </summary>\n    [Fact]\n    public async Task ResendSponsorshipOffer_AsRegularMember_ReturnsForbidden()\n    {\n        // Arrange\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"resend-victim2@example.com\");\n\n        var (memberEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.User,\n            permissions: new Permissions { ManageUsers = false });\n\n        await _loginHelper.LoginAsync(memberEmail);\n\n        // Act\n        var response = await _client.PostAsync(\n            $\"organization/sponsorship/{_organization.Id}/families-for-enterprise/resend?sponsoredFriendlyName={Uri.EscapeDataString(sponsorship.FriendlyName!)}\",\n            null);\n\n        // Assert\n        Assert.True(\n            response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized,\n            $\"Expected 401 or 403 but got {(int)response.StatusCode} {response.StatusCode}. \" +\n            \"Regular org members without ManageUsers should not be able to resend sponsorship offers.\");\n    }\n\n    /// <summary>\n    /// Verifies that an admin of a different org cannot resend sponsorship offers\n    /// for the target org (cross-org attack).\n    /// </summary>\n    [Fact]\n    public async Task ResendSponsorshipOffer_AsAdminOfDifferentOrg_ReturnsForbidden()\n    {\n        // Arrange\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"resend-cross-org@example.com\");\n\n        var attackerEmail = $\"resend-other-org-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(attackerEmail);\n\n        await OrganizationTestHelpers.SignUpAsync(\n            _factory,\n            plan: PlanType.EnterpriseAnnually,\n            ownerEmail: attackerEmail,\n            name: \"Resend Attacker Org\",\n            billingEmail: attackerEmail,\n            passwordManagerSeats: 5,\n            paymentMethod: PaymentMethodType.Card);\n\n        await _loginHelper.LoginAsync(attackerEmail);\n\n        // Act\n        var response = await _client.PostAsync(\n            $\"organization/sponsorship/{_organization.Id}/families-for-enterprise/resend?sponsoredFriendlyName={Uri.EscapeDataString(sponsorship.FriendlyName!)}\",\n            null);\n\n        // Assert\n        Assert.True(\n            response.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized,\n            $\"Expected 401 or 403 but got {(int)response.StatusCode} {response.StatusCode}. \" +\n            \"Admin of a different org should not be able to resend sponsorship offers for another org.\");\n    }\n\n    /// <summary>\n    /// Verifies that an org Owner CAN resend sponsorship offers.\n    /// Note: The endpoint may still return a non-200 due to downstream email/policy logic,\n    /// but crucially it should NOT return 401/403.\n    /// </summary>\n    [Fact]\n    public async Task ResendSponsorshipOffer_AsOwner_IsNotForbidden()\n    {\n        // Arrange\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"resend-employee@example.com\");\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        // Act\n        var response = await _client.PostAsync(\n            $\"organization/sponsorship/{_organization.Id}/families-for-enterprise/resend?sponsoredFriendlyName={Uri.EscapeDataString(sponsorship.FriendlyName!)}\",\n            null);\n\n        // Assert: Should pass authorization (may fail downstream for other reasons, but not 401/403)\n        Assert.True(\n            response.StatusCode is not HttpStatusCode.Forbidden and not HttpStatusCode.Unauthorized,\n            $\"Expected to pass authorization but got {(int)response.StatusCode} {response.StatusCode}.\");\n    }\n\n    /// <summary>\n    /// Verifies that an org Admin CAN resend sponsorship offers.\n    /// </summary>\n    [Fact]\n    public async Task ResendSponsorshipOffer_AsAdmin_IsNotForbidden()\n    {\n        // Arrange\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"resend-admin@example.com\");\n\n        var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.Admin);\n\n        await _loginHelper.LoginAsync(adminEmail);\n\n        // Act\n        var response = await _client.PostAsync(\n            $\"organization/sponsorship/{_organization.Id}/families-for-enterprise/resend?sponsoredFriendlyName={Uri.EscapeDataString(sponsorship.FriendlyName!)}\",\n            null);\n\n        // Assert\n        Assert.True(\n            response.StatusCode is not HttpStatusCode.Forbidden and not HttpStatusCode.Unauthorized,\n            $\"Expected to pass authorization but got {(int)response.StatusCode} {response.StatusCode}.\");\n    }\n\n    /// <summary>\n    /// Verifies that a Custom user with ManageUsers CAN resend sponsorship offers.\n    /// </summary>\n    [Fact]\n    public async Task ResendSponsorshipOffer_AsCustomWithManageUsers_IsNotForbidden()\n    {\n        // Arrange\n        var sponsorship = await CreateAdminInitiatedSponsorshipAsync(\n            _organization.Id, _ownerOrgUser.Id, \"resend-custom@example.com\");\n\n        var (customEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory, _organization.Id, OrganizationUserType.Custom,\n            permissions: new Permissions { ManageUsers = true });\n\n        await _loginHelper.LoginAsync(customEmail);\n\n        // Act\n        var response = await _client.PostAsync(\n            $\"organization/sponsorship/{_organization.Id}/families-for-enterprise/resend?sponsoredFriendlyName={Uri.EscapeDataString(sponsorship.FriendlyName!)}\",\n            null);\n\n        // Assert\n        Assert.True(\n            response.StatusCode is not HttpStatusCode.Forbidden and not HttpStatusCode.Unauthorized,\n            $\"Expected to pass authorization but got {(int)response.StatusCode} {response.StatusCode}.\");\n    }\n\n    #endregion\n\n    /// <summary>\n    /// Helper to create an admin-initiated sponsorship directly in the DB,\n    /// bypassing the command layer (which has its own auth checks).\n    /// </summary>\n    private async Task<OrganizationSponsorship> CreateAdminInitiatedSponsorshipAsync(\n        Guid sponsoringOrgId, Guid sponsoringOrgUserId, string friendlyName)\n    {\n        var sponsorshipRepository = _factory.GetService<IOrganizationSponsorshipRepository>();\n\n        var sponsorship = new OrganizationSponsorship\n        {\n            SponsoringOrganizationId = sponsoringOrgId,\n            SponsoringOrganizationUserId = sponsoringOrgUserId,\n            FriendlyName = friendlyName,\n            OfferedToEmail = friendlyName,\n            PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,\n            IsAdminInitiated = true,\n            ToDelete = false,\n        };\n        sponsorship.SetNewId();\n\n        await sponsorshipRepository.CreateAsync(sponsorship);\n        return sponsorship;\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs",
    "content": "﻿using System.Net;\nusing System.Text.Json;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Response;\nusing Bit.Core;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing Xunit;\nusing static Bit.Core.KeyManagement.Enums.SignatureAlgorithm;\n\nnamespace Bit.Api.IntegrationTest.Controllers;\n\npublic class AccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private static readonly string _masterKeyWrappedUserKey =\n        \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n    private static readonly string _mockEncryptedType7String = \"7.AOs41Hd8OQiCPXjyJKCiDA==\";\n    private static readonly string _mockEncryptedType7WrappedSigningKey = \"7.DRv74Kg1RSlFSam1MNFlGD==\";\n\n    private static readonly string _masterPasswordHash = \"master_password_hash\";\n    private static readonly string _newMasterPasswordHash = \"new_master_password_hash\";\n\n    private static readonly KdfRequestModel _defaultKdfRequest =\n        new() { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 };\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n    private readonly IUserRepository _userRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IFeatureService _featureService;\n    private readonly IPasswordHasher<User> _passwordHasher;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;\n    private readonly IEventRepository _eventRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n\n    private string _ownerEmail = null!;\n\n    public AccountsControllerTest(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _factory.SubstituteService<IPushNotificationService>(_ => { });\n        _factory.SubstituteService<IFeatureService>(_ => { });\n        _client = factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n        _userRepository = _factory.GetService<IUserRepository>();\n        _pushNotificationService = _factory.GetService<IPushNotificationService>();\n        _featureService = _factory.GetService<IFeatureService>();\n        _passwordHasher = _factory.GetService<IPasswordHasher<User>>();\n        _organizationRepository = _factory.GetService<IOrganizationRepository>();\n        _ssoConfigRepository = _factory.GetService<ISsoConfigRepository>();\n        _userSignatureKeyPairRepository = _factory.GetService<IUserSignatureKeyPairRepository>();\n        _eventRepository = _factory.GetService<IEventRepository>();\n        _organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task GetAccountsProfile_success()\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        using var message = new HttpRequestMessage(HttpMethod.Get, \"/accounts/profile\");\n        var response = await _client.SendAsync(message);\n\n        response.EnsureSuccessStatusCode();\n\n        var content = await response.Content.ReadFromJsonAsync<ProfileResponseModel>();\n        Assert.NotNull(content);\n        Assert.Equal(_ownerEmail, content.Email);\n        Assert.NotNull(content.Name);\n        Assert.True(content.EmailVerified);\n        Assert.False(content.Premium);\n        Assert.False(content.PremiumFromOrganization);\n        Assert.Equal(\"en-US\", content.Culture);\n        Assert.NotNull(content.Key);\n        Assert.NotNull(content.PrivateKey);\n        Assert.NotNull(content.SecurityStamp);\n    }\n\n    [Theory]\n    [BitAutoData(KdfType.PBKDF2_SHA256, 600001, null, null)]\n    [BitAutoData(KdfType.Argon2id, 4, 65, 5)]\n    public async Task PostKdf_ValidRequestLogoutOnKdfChangeFeatureFlagOff_SuccessLogout(KdfType kdf,\n        int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        var userBeforeKdfChange = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userBeforeKdfChange);\n\n        _featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange).Returns(false);\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var kdfRequest = new KdfRequestModel\n        {\n            KdfType = kdf,\n            Iterations = kdfIterations,\n            Memory = kdfMemory,\n            Parallelism = kdfParallelism,\n        };\n\n        var response = await PostKdfWithKdfRequestAsync(kdfRequest);\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Validate that the user fields were updated correctly\n        var user = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(user);\n        Assert.Equal(kdfRequest.KdfType, user.Kdf);\n        Assert.Equal(kdfRequest.Iterations, user.KdfIterations);\n        Assert.Equal(kdfRequest.Memory, user.KdfMemory);\n        Assert.Equal(kdfRequest.Parallelism, user.KdfParallelism);\n        Assert.Equal(_masterKeyWrappedUserKey, user.Key);\n        Assert.NotNull(user.LastKdfChangeDate);\n        Assert.True(user.LastKdfChangeDate > DateTime.UtcNow.AddMinutes(-1));\n        Assert.True(user.RevisionDate > DateTime.UtcNow.AddMinutes(-1));\n        Assert.True(user.AccountRevisionDate > DateTime.UtcNow.AddMinutes(-1));\n        Assert.NotEqual(userBeforeKdfChange.SecurityStamp, user.SecurityStamp);\n        Assert.Equal(PasswordVerificationResult.Success,\n            _passwordHasher.VerifyHashedPassword(user, user.MasterPassword!, _newMasterPasswordHash));\n\n        // Validate push notification\n        await _pushNotificationService.Received(1).PushLogOutAsync(user.Id);\n    }\n\n    [Theory]\n    [BitAutoData(KdfType.PBKDF2_SHA256, 600001, null, null)]\n    [BitAutoData(KdfType.Argon2id, 4, 65, 5)]\n    public async Task PostKdf_ValidRequestLogoutOnKdfChangeFeatureFlagOn_SuccessSyncAndLogoutWithReason(KdfType kdf,\n        int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        var userBeforeKdfChange = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userBeforeKdfChange);\n\n        _featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange).Returns(true);\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var kdfRequest = new KdfRequestModel\n        {\n            KdfType = kdf,\n            Iterations = kdfIterations,\n            Memory = kdfMemory,\n            Parallelism = kdfParallelism,\n        };\n\n        var response = await PostKdfWithKdfRequestAsync(kdfRequest);\n\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        // Validate that the user fields were updated correctly\n        var user = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(user);\n        Assert.Equal(kdfRequest.KdfType, user.Kdf);\n        Assert.Equal(kdfRequest.Iterations, user.KdfIterations);\n        Assert.Equal(kdfRequest.Memory, user.KdfMemory);\n        Assert.Equal(kdfRequest.Parallelism, user.KdfParallelism);\n        Assert.Equal(_masterKeyWrappedUserKey, user.Key);\n        Assert.NotNull(user.LastKdfChangeDate);\n        Assert.True(user.LastKdfChangeDate > DateTime.UtcNow.AddMinutes(-1));\n        Assert.True(user.RevisionDate > DateTime.UtcNow.AddMinutes(-1));\n        Assert.True(user.AccountRevisionDate > DateTime.UtcNow.AddMinutes(-1));\n        Assert.Equal(userBeforeKdfChange.SecurityStamp, user.SecurityStamp);\n        Assert.Equal(PasswordVerificationResult.Success,\n            _passwordHasher.VerifyHashedPassword(user, user.MasterPassword!, _newMasterPasswordHash));\n\n        // Validate push notification\n        await _pushNotificationService.Received(1)\n            .PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KdfChange);\n        await _pushNotificationService.Received(1).PushSyncSettingsAsync(user.Id);\n    }\n\n    [Fact]\n    public async Task PostKdf_Unauthorized_ReturnsUnauthorized()\n    {\n        // Don't call LoginAsync to test unauthorized access\n\n        var response = await PostKdfWithKdfRequestAsync(_defaultKdfRequest);\n\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(false, true)]\n    [InlineData(true, false)]\n    [InlineData(true, true)]\n    public async Task PostKdf_AuthenticationDataOrUnlockDataNull_BadRequest(bool authenticationDataNull,\n        bool unlockDataNull)\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var authenticationData = authenticationDataNull\n            ? null\n            : new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = _defaultKdfRequest,\n                MasterPasswordAuthenticationHash = _newMasterPasswordHash,\n                Salt = _ownerEmail\n            };\n\n        var unlockData = unlockDataNull\n            ? null\n            : new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = _defaultKdfRequest,\n                MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,\n                Salt = _ownerEmail\n            };\n\n        var response = await PostKdfAsync(authenticationData, unlockData);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"AuthenticationData and UnlockData must be provided.\", content);\n    }\n\n    [Fact]\n    public async Task PostKdf_InvalidMasterPasswordHash_BadRequest()\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var authenticationData = new MasterPasswordAuthenticationDataRequestModel\n        {\n            Kdf = _defaultKdfRequest,\n            MasterPasswordAuthenticationHash = _newMasterPasswordHash,\n            Salt = _ownerEmail\n        };\n\n        var unlockData = new MasterPasswordUnlockDataRequestModel\n        {\n            Kdf = _defaultKdfRequest,\n            MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,\n            Salt = _ownerEmail\n        };\n\n        var requestModel = new PasswordRequestModel\n        {\n            MasterPasswordHash = \"wrong-master-password-hash\",\n            NewMasterPasswordHash = _newMasterPasswordHash,\n            Key = _masterKeyWrappedUserKey,\n            AuthenticationData = authenticationData,\n            UnlockData = unlockData\n        };\n\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/kdf\");\n        message.Content = JsonContent.Create(requestModel);\n        var response = await _client.SendAsync(message);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Incorrect password\", content);\n    }\n\n    [Fact]\n    public async Task PostKdf_ChangedSaltInAuthenticationData_BadRequest()\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var authenticationData = new MasterPasswordAuthenticationDataRequestModel\n        {\n            Kdf = _defaultKdfRequest,\n            MasterPasswordAuthenticationHash = _newMasterPasswordHash,\n            Salt = \"wrong-salt@bitwarden.com\"\n        };\n\n        var unlockData = new MasterPasswordUnlockDataRequestModel\n        {\n            Kdf = _defaultKdfRequest,\n            MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,\n            Salt = _ownerEmail\n        };\n\n        var response = await PostKdfAsync(authenticationData, unlockData);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Invalid master password salt.\", content);\n    }\n\n    [Fact]\n    public async Task PostKdf_ChangedSaltInUnlockData_BadRequest()\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var authenticationData = new MasterPasswordAuthenticationDataRequestModel\n        {\n            Kdf = _defaultKdfRequest,\n            MasterPasswordAuthenticationHash = _newMasterPasswordHash,\n            Salt = _ownerEmail\n        };\n\n        var unlockData = new MasterPasswordUnlockDataRequestModel\n        {\n            Kdf = _defaultKdfRequest,\n            MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,\n            Salt = \"wrong-salt@bitwarden.com\"\n        };\n\n        var response = await PostKdfAsync(authenticationData, unlockData);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Invalid master password salt.\", content);\n    }\n\n    [Fact]\n    public async Task PostKdf_KdfNotMatching_BadRequest()\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var authenticationData = new MasterPasswordAuthenticationDataRequestModel\n        {\n            Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 },\n            MasterPasswordAuthenticationHash = _newMasterPasswordHash,\n            Salt = _ownerEmail\n        };\n\n        var unlockData = new MasterPasswordUnlockDataRequestModel\n        {\n            Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_001 },\n            MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,\n            Salt = _ownerEmail\n        };\n\n        var response = await PostKdfAsync(authenticationData, unlockData);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"KDF settings must be equal for authentication and unlock.\", content);\n    }\n\n    [Theory]\n    [InlineData(KdfType.PBKDF2_SHA256, 1, null, null)]\n    [InlineData(KdfType.Argon2id, 4, null, 5)]\n    [InlineData(KdfType.Argon2id, 4, 65, null)]\n    public async Task PostKdf_InvalidKdf_BadRequest(KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var kdfRequest = new KdfRequestModel\n        {\n            KdfType = kdf,\n            Iterations = kdfIterations,\n            Memory = kdfMemory,\n            Parallelism = kdfParallelism\n        };\n\n        var response = await PostKdfWithKdfRequestAsync(kdfRequest);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"The model state is invalid\", content);\n    }\n\n    [Fact]\n    public async Task PostKdf_InvalidNewMasterPassword_BadRequest()\n    {\n        var newMasterPasswordHash = \"too-short\";\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var authenticationData = new MasterPasswordAuthenticationDataRequestModel\n        {\n            Kdf = _defaultKdfRequest,\n            MasterPasswordAuthenticationHash = newMasterPasswordHash,\n            Salt = _ownerEmail\n        };\n\n        var unlockData = new MasterPasswordUnlockDataRequestModel\n        {\n            Kdf = _defaultKdfRequest,\n            MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,\n            Salt = _ownerEmail\n        };\n\n        var requestModel = new PasswordRequestModel\n        {\n            MasterPasswordHash = _masterPasswordHash,\n            NewMasterPasswordHash = newMasterPasswordHash,\n            Key = _masterKeyWrappedUserKey,\n            AuthenticationData = authenticationData,\n            UnlockData = unlockData\n        };\n\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/kdf\");\n        message.Content = JsonContent.Create(requestModel);\n        var response = await _client.SendAsync(message);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var content = await response.Content.ReadAsStringAsync();\n        Assert.Contains(\"Passwords must be at least\", content);\n    }\n\n    private async Task<HttpResponseMessage> PostKdfWithKdfRequestAsync(KdfRequestModel kdfRequest)\n    {\n        var authenticationData = new MasterPasswordAuthenticationDataRequestModel\n        {\n            Kdf = kdfRequest,\n            MasterPasswordAuthenticationHash = _newMasterPasswordHash,\n            Salt = _ownerEmail\n        };\n\n        var unlockData = new MasterPasswordUnlockDataRequestModel\n        {\n            Kdf = kdfRequest,\n            MasterKeyWrappedUserKey = _masterKeyWrappedUserKey,\n            Salt = _ownerEmail\n        };\n\n        return await PostKdfAsync(authenticationData, unlockData);\n    }\n\n    private async Task<HttpResponseMessage> PostKdfAsync(\n        MasterPasswordAuthenticationDataRequestModel? authenticationDataRequest,\n        MasterPasswordUnlockDataRequestModel? unlockDataRequest)\n    {\n        var requestModel = new PasswordRequestModel\n        {\n            MasterPasswordHash = _masterPasswordHash,\n            NewMasterPasswordHash = _newMasterPasswordHash,\n            Key = _masterKeyWrappedUserKey,\n            AuthenticationData = authenticationDataRequest,\n            UnlockData = unlockDataRequest\n        };\n\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/kdf\");\n        message.Content = JsonContent.Create(requestModel);\n        return await _client.SendAsync(message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V1_MasterPasswordDecryption_Success(string organizationSsoIdentifier)\n    {\n        // Arrange - Create organization and user\n        var ownerEmail = $\"owner-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(ownerEmail);\n\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            ownerEmail: ownerEmail,\n            name: \"Test Org V1\");\n        organization.UseSso = true;\n        organization.Identifier = organizationSsoIdentifier;\n        await _organizationRepository.ReplaceAsync(organization);\n\n        await _ssoConfigRepository.CreateAsync(new SsoConfig\n        {\n            OrganizationId = organization.Id,\n            Enabled = true,\n            Data = JsonSerializer.Serialize(new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.MasterPassword,\n            }, JsonHelpers.CamelCase),\n        });\n\n        // Create user with password initially, so we can login\n        var userEmail = $\"user-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(userEmail);\n\n        // Add user to organization\n        var user = await _userRepository.GetByEmailAsync(userEmail);\n        Assert.NotNull(user);\n        await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, userEmail,\n            OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Invited);\n\n        // Login as the user\n        await _loginHelper.LoginAsync(userEmail);\n\n        // Remove the master password and keys to simulate newly registered SSO user\n        user.MasterPassword = null;\n        user.Key = null;\n        user.PrivateKey = null;\n        user.PublicKey = null;\n        await _userRepository.ReplaceAsync(user);\n\n        // V1 (Obsolete) request format - to be removed with PM-27327\n        var request = new\n        {\n            masterPasswordHash = _newMasterPasswordHash,\n            key = _masterKeyWrappedUserKey,\n            keys = new\n            {\n                publicKey = \"v1-publicKey\",\n                encryptedPrivateKey = \"v1-encryptedPrivateKey\"\n            },\n            kdf = 0,  // PBKDF2_SHA256\n            kdfIterations = 600000,\n            kdfMemory = (int?)null,\n            kdfParallelism = (int?)null,\n            masterPasswordHint = \"v1-integration-test-hint\",\n            orgIdentifier = organization.Identifier\n        };\n\n        var jsonRequest = JsonSerializer.Serialize(request, JsonHelpers.CamelCase);\n\n        // Act\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/set-password\");\n        message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, \"application/json\");\n        var response = await _client.SendAsync(message);\n\n        // Assert\n        if (!response.IsSuccessStatusCode)\n        {\n            var errorContent = await response.Content.ReadAsStringAsync();\n            Assert.Fail($\"Expected success but got {response.StatusCode}. Error: {errorContent}\");\n        }\n\n        // Verify user in database\n        var updatedUser = await _userRepository.GetByEmailAsync(userEmail);\n        Assert.NotNull(updatedUser);\n        Assert.Equal(\"v1-integration-test-hint\", updatedUser.MasterPasswordHint);\n\n        // Verify the master password is hashed and stored\n        Assert.NotNull(updatedUser.MasterPassword);\n        var verificationResult = _passwordHasher.VerifyHashedPassword(updatedUser, updatedUser.MasterPassword, _newMasterPasswordHash);\n        Assert.Equal(PasswordVerificationResult.Success, verificationResult);\n\n        // Verify KDF settings\n        Assert.Equal(KdfType.PBKDF2_SHA256, updatedUser.Kdf);\n        Assert.Equal(600_000, updatedUser.KdfIterations);\n        Assert.Null(updatedUser.KdfMemory);\n        Assert.Null(updatedUser.KdfParallelism);\n\n        // Verify timestamps are updated\n        Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));\n        Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));\n\n        // Verify keys are set (V1 uses Keys property)\n        Assert.Equal(_masterKeyWrappedUserKey, updatedUser.Key);\n        Assert.Equal(\"v1-publicKey\", updatedUser.PublicKey);\n        Assert.Equal(\"v1-encryptedPrivateKey\", updatedUser.PrivateKey);\n\n        // Verify User_ChangedPassword event was logged\n        var events = await _eventRepository.GetManyByUserAsync(updatedUser.Id, DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow.AddMinutes(1), new PageOptions { PageSize = 100 });\n        Assert.NotNull(events);\n        Assert.Contains(events.Data, e => e.Type == EventType.User_ChangedPassword && e.UserId == updatedUser.Id);\n\n        // Verify user was accepted into the organization\n        var orgUsers = await _organizationUserRepository.GetManyByUserAsync(updatedUser.Id);\n        var orgUser = orgUsers.FirstOrDefault(ou => ou.OrganizationId == organization.Id);\n        Assert.NotNull(orgUser);\n        Assert.Equal(OrganizationUserStatusType.Accepted, orgUser.Status);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V2_MasterPasswordDecryption_Success(string organizationSsoIdentifier)\n    {\n        // Arrange - Create organization and user\n        var ownerEmail = $\"owner-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(ownerEmail);\n\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            ownerEmail: ownerEmail,\n            name: \"Test Org\");\n        organization.UseSso = true;\n        organization.Identifier = organizationSsoIdentifier;\n        await _organizationRepository.ReplaceAsync(organization);\n\n        await _ssoConfigRepository.CreateAsync(new SsoConfig\n        {\n            OrganizationId = organization.Id,\n            Enabled = true,\n            Data = JsonSerializer.Serialize(new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.MasterPassword,\n            }, JsonHelpers.CamelCase),\n        });\n\n        // Create user with password initially, so we can login\n        var userEmail = $\"user-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(userEmail);\n\n        // Add user to organization\n        var user = await _userRepository.GetByEmailAsync(userEmail);\n        Assert.NotNull(user);\n        await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, userEmail,\n            OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Invited);\n\n        // Login as the user\n        await _loginHelper.LoginAsync(userEmail);\n\n        // Remove the master password and keys to simulate newly registered SSO user\n        user.MasterPassword = null;\n        user.Key = null;\n        user.PrivateKey = null;\n        user.PublicKey = null;\n        user.SignedPublicKey = null;\n        await _userRepository.ReplaceAsync(user);\n\n        var jsonRequest = CreateV2SetPasswordRequestJson(\n            userEmail,\n            organization.Identifier,\n            \"integration-test-hint\",\n            includeAccountKeys: true);\n\n        // Act\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/set-password\");\n        message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, \"application/json\");\n        var response = await _client.SendAsync(message);\n\n        // Assert\n        if (!response.IsSuccessStatusCode)\n        {\n            var errorContent = await response.Content.ReadAsStringAsync();\n            Assert.Fail($\"Expected success but got {response.StatusCode}. Error: {errorContent}\");\n        }\n\n        // Verify user in database\n        var updatedUser = await _userRepository.GetByEmailAsync(userEmail);\n        Assert.NotNull(updatedUser);\n        Assert.Equal(\"integration-test-hint\", updatedUser.MasterPasswordHint);\n\n        // Verify the master password is hashed and stored\n        Assert.NotNull(updatedUser.MasterPassword);\n        var verificationResult = _passwordHasher.VerifyHashedPassword(updatedUser, updatedUser.MasterPassword, _newMasterPasswordHash);\n        Assert.Equal(PasswordVerificationResult.Success, verificationResult);\n\n        // Verify KDF settings\n        Assert.Equal(KdfType.PBKDF2_SHA256, updatedUser.Kdf);\n        Assert.Equal(600_000, updatedUser.KdfIterations);\n        Assert.Null(updatedUser.KdfMemory);\n        Assert.Null(updatedUser.KdfParallelism);\n\n        // Verify timestamps are updated\n        Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));\n        Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));\n\n        // Verify keys are set\n        Assert.Equal(_masterKeyWrappedUserKey, updatedUser.Key);\n        Assert.Equal(\"publicKey\", updatedUser.PublicKey);\n        Assert.Equal(_mockEncryptedType7String, updatedUser.PrivateKey);\n        Assert.Equal(\"signedPublicKey\", updatedUser.SignedPublicKey);\n\n        // Verify security state\n        Assert.Equal(2, updatedUser.SecurityVersion);\n        Assert.Equal(\"v2\", updatedUser.SecurityState);\n\n        // Verify signature key pair data\n        var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(updatedUser.Id);\n        Assert.NotNull(signatureKeyPair);\n        Assert.Equal(Ed25519, signatureKeyPair.SignatureAlgorithm);\n        Assert.Equal(_mockEncryptedType7WrappedSigningKey, signatureKeyPair.WrappedSigningKey);\n        Assert.Equal(\"verifyingKey\", signatureKeyPair.VerifyingKey);\n\n        // Verify User_ChangedPassword event was logged\n        var events = await _eventRepository.GetManyByUserAsync(updatedUser.Id, DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow.AddMinutes(1), new PageOptions { PageSize = 100 });\n        Assert.NotNull(events);\n        Assert.Contains(events.Data, e => e.Type == EventType.User_ChangedPassword && e.UserId == updatedUser.Id);\n\n        // Verify user was accepted into the organization\n        var orgUsers = await _organizationUserRepository.GetManyByUserAsync(updatedUser.Id);\n        var orgUser = orgUsers.FirstOrDefault(ou => ou.OrganizationId == organization.Id);\n        Assert.NotNull(orgUser);\n        Assert.Equal(OrganizationUserStatusType.Accepted, orgUser.Status);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V2_TDEDecryption_Success(string organizationSsoIdentifier)\n    {\n        // Arrange - Create organization with TDE\n        var ownerEmail = $\"owner-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(ownerEmail);\n\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            ownerEmail: ownerEmail,\n            name: \"Test Org TDE\");\n        organization.UseSso = true;\n        organization.Identifier = organizationSsoIdentifier;\n        await _organizationRepository.ReplaceAsync(organization);\n\n        // Configure SSO for TDE (TrustedDeviceEncryption)\n        await _ssoConfigRepository.CreateAsync(new SsoConfig\n        {\n            OrganizationId = organization.Id,\n            Enabled = true,\n            Data = JsonSerializer.Serialize(new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,\n            }, JsonHelpers.CamelCase),\n        });\n\n        // Create user with password initially, so we can login\n        var userEmail = $\"user-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(userEmail);\n\n        var user = await _userRepository.GetByEmailAsync(userEmail);\n        Assert.NotNull(user);\n\n        // Add user to organization and confirm them (TDE users are confirmed, not invited)\n        await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, userEmail,\n            OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Confirmed);\n\n        // Login as the user\n        await _loginHelper.LoginAsync(userEmail);\n\n        // Set up TDE user with V2 account keys but no master password\n        // TDE users already have their account keys from device provisioning\n        user.MasterPassword = null;\n        user.Key = null;\n        user.PublicKey = \"tde-publicKey\";\n        user.PrivateKey = _mockEncryptedType7String;\n        user.SignedPublicKey = \"tde-signedPublicKey\";\n        user.SecurityVersion = 2;\n        user.SecurityState = \"v2-tde\";\n        await _userRepository.ReplaceAsync(user);\n\n        // Create signature key pair for TDE user\n        var signatureKeyPairData = new Core.KeyManagement.Models.Data.SignatureKeyPairData(\n            Ed25519,\n            _mockEncryptedType7WrappedSigningKey,\n            \"tde-verifyingKey\");\n        var setSignatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id);\n        if (setSignatureKeyPair == null)\n        {\n            var newKeyPair = new Core.KeyManagement.Entities.UserSignatureKeyPair\n            {\n                UserId = user.Id,\n                SignatureAlgorithm = signatureKeyPairData.SignatureAlgorithm,\n                SigningKey = signatureKeyPairData.WrappedSigningKey,\n                VerifyingKey = signatureKeyPairData.VerifyingKey,\n                CreationDate = DateTime.UtcNow,\n                RevisionDate = DateTime.UtcNow\n            };\n            newKeyPair.SetNewId();\n            await _userSignatureKeyPairRepository.CreateAsync(newKeyPair);\n        }\n\n        var jsonRequest = CreateV2SetPasswordRequestJson(\n            userEmail,\n            organization.Identifier,\n            \"tde-test-hint\",\n            includeAccountKeys: false);\n\n        // Act\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/set-password\");\n        message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, \"application/json\");\n        var response = await _client.SendAsync(message);\n\n        // Assert\n        if (!response.IsSuccessStatusCode)\n        {\n            var errorContent = await response.Content.ReadAsStringAsync();\n            Assert.Fail($\"Expected success but got {response.StatusCode}. Error: {errorContent}\");\n        }\n\n        // Verify user in database\n        var updatedUser = await _userRepository.GetByEmailAsync(userEmail);\n        Assert.NotNull(updatedUser);\n        Assert.Equal(\"tde-test-hint\", updatedUser.MasterPasswordHint);\n\n        // Verify the master password is hashed and stored\n        Assert.NotNull(updatedUser.MasterPassword);\n        var verificationResult = _passwordHasher.VerifyHashedPassword(updatedUser, updatedUser.MasterPassword, _newMasterPasswordHash);\n        Assert.Equal(PasswordVerificationResult.Success, verificationResult);\n\n        // Verify KDF settings\n        Assert.Equal(KdfType.PBKDF2_SHA256, updatedUser.Kdf);\n        Assert.Equal(600_000, updatedUser.KdfIterations);\n        Assert.Null(updatedUser.KdfMemory);\n        Assert.Null(updatedUser.KdfParallelism);\n\n        // Verify timestamps are updated\n        Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));\n        Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));\n\n        // Verify key is set\n        Assert.Equal(_masterKeyWrappedUserKey, updatedUser.Key);\n\n        // Verify AccountKeys are preserved (TDE users already had V2 keys)\n        Assert.Equal(\"tde-publicKey\", updatedUser.PublicKey);\n        Assert.Equal(_mockEncryptedType7String, updatedUser.PrivateKey);\n        Assert.Equal(\"tde-signedPublicKey\", updatedUser.SignedPublicKey);\n        Assert.Equal(2, updatedUser.SecurityVersion);\n        Assert.Equal(\"v2-tde\", updatedUser.SecurityState);\n\n        // Verify signature key pair is preserved (TDE users already had signature keys)\n        var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(updatedUser.Id);\n        Assert.NotNull(signatureKeyPair);\n        Assert.Equal(Ed25519, signatureKeyPair.SignatureAlgorithm);\n        Assert.Equal(_mockEncryptedType7WrappedSigningKey, signatureKeyPair.WrappedSigningKey);\n        Assert.Equal(\"tde-verifyingKey\", signatureKeyPair.VerifyingKey);\n\n        // Verify User_ChangedPassword event was logged\n        var events = await _eventRepository.GetManyByUserAsync(updatedUser.Id, DateTime.UtcNow.AddMinutes(-5), DateTime.UtcNow.AddMinutes(1), new PageOptions { PageSize = 100 });\n        Assert.NotNull(events);\n        Assert.Contains(events.Data, e => e.Type == EventType.User_ChangedPassword && e.UserId == updatedUser.Id);\n\n        // Verify user remains confirmed in the organization\n        var orgUsers = await _organizationUserRepository.GetManyByUserAsync(updatedUser.Id);\n        var orgUser = orgUsers.FirstOrDefault(ou => ou.OrganizationId == organization.Id);\n        Assert.NotNull(orgUser);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, orgUser.Status);\n    }\n\n    [Fact]\n    public async Task PostSetPasswordAsync_V2_Unauthorized_ReturnsUnauthorized()\n    {\n        // Arrange - Don't login\n        var jsonRequest = CreateV2SetPasswordRequestJson(\n            \"test@bitwarden.com\",\n            \"test-org-identifier\",\n            \"test-hint\",\n            includeAccountKeys: true);\n\n        // Act\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/set-password\");\n        message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, \"application/json\");\n        var response = await _client.SendAsync(message);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PostSetPasswordAsync_V2_MismatchedKdfSettings_ReturnsBadRequest()\n    {\n        // Arrange\n        var email = $\"kdf-mismatch-test-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        await _loginHelper.LoginAsync(email);\n\n        // Test mismatched KDF settings (600000 vs 650000 iterations)\n        var request = new\n        {\n            masterPasswordAuthentication = new\n            {\n                kdf = new\n                {\n                    kdfType = 0,\n                    iterations = 600000\n                },\n                masterPasswordAuthenticationHash = _newMasterPasswordHash,\n                salt = email\n            },\n            masterPasswordUnlock = new\n            {\n                kdf = new\n                {\n                    kdfType = 0,\n                    iterations = 650000  // Different from authentication KDF\n                },\n                masterKeyWrappedUserKey = _masterKeyWrappedUserKey,\n                salt = email\n            },\n            accountKeys = new\n            {\n                userKeyEncryptedAccountPrivateKey = \"7.AOs41Hd8OQiCPXjyJKCiDA==\",\n                accountPublicKey = \"public-key\"\n            },\n            orgIdentifier = \"test-org-identifier\"\n        };\n\n        var jsonRequest = JsonSerializer.Serialize(request, JsonHelpers.CamelCase);\n\n        // Act\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/set-password\");\n        message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, \"application/json\");\n        var response = await _client.SendAsync(message);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(KdfType.PBKDF2_SHA256, 1, null, null)]\n    [InlineData(KdfType.Argon2id, 4, null, 5)]\n    [InlineData(KdfType.Argon2id, 4, 65, null)]\n    public async Task PostSetPasswordAsync_V2_InvalidKdfSettings_ReturnsBadRequest(\n        KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        // Arrange\n        var email = $\"invalid-kdf-test-{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        await _loginHelper.LoginAsync(email);\n\n        var jsonRequest = CreateV2SetPasswordRequestJson(\n            email,\n            \"test-org-identifier\",\n            \"test-hint\",\n            includeAccountKeys: true,\n            kdfType: kdf,\n            kdfIterations: kdfIterations,\n            kdfMemory: kdfMemory,\n            kdfParallelism: kdfParallelism);\n\n        // Act\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/set-password\");\n        message.Content = new StringContent(jsonRequest, System.Text.Encoding.UTF8, \"application/json\");\n        var response = await _client.SendAsync(message);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PostEmail_Success_UpdatesEmailAndPassword()\n    {\n        // Arrange\n        var newEmail = $\"new-email-{Guid.NewGuid()}@bitwarden.com\";\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var user = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(user);\n\n        var userManager = _factory.GetService<UserManager<User>>();\n        var token = await userManager.GenerateChangeEmailTokenAsync(user, newEmail);\n\n        // Act\n        var response = await PostEmailAsync(newEmail, token);\n\n        // Assert\n        response.EnsureSuccessStatusCode();\n\n        var updatedUser = await _userRepository.GetByEmailAsync(newEmail);\n        Assert.NotNull(updatedUser);\n        Assert.Equal(newEmail, updatedUser.Email);\n        Assert.True(updatedUser.EmailVerified);\n        Assert.Equal(_masterKeyWrappedUserKey, updatedUser.Key);\n        Assert.Equal(PasswordVerificationResult.Success,\n            _passwordHasher.VerifyHashedPassword(updatedUser, updatedUser.MasterPassword!, _newMasterPasswordHash));\n    }\n\n    [Fact]\n    public async Task PostEmail_WhenInvalidMasterPassword_ReturnsBadRequest()\n    {\n        // Arrange\n        var newEmail = $\"new-email-{Guid.NewGuid()}@bitwarden.com\";\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var user = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(user);\n\n        var userManager = _factory.GetService<UserManager<User>>();\n        var token = await userManager.GenerateChangeEmailTokenAsync(user, newEmail);\n\n        var requestModel = new EmailRequestModel\n        {\n            MasterPasswordHash = \"wrong_master_password_hash\",\n            NewEmail = newEmail,\n            NewMasterPasswordHash = _newMasterPasswordHash,\n            Token = token,\n            Key = _masterKeyWrappedUserKey\n        };\n\n        // Act\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/email\");\n        message.Content = JsonContent.Create(requestModel);\n        var response = await _client.SendAsync(message);\n\n        // Assert\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n\n        // Verify email was not changed\n        var unchangedUser = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(unchangedUser);\n    }\n\n    private async Task<HttpResponseMessage> PostEmailAsync(string newEmail, string token)\n    {\n        var requestModel = new EmailRequestModel\n        {\n            MasterPasswordHash = _masterPasswordHash,\n            NewEmail = newEmail,\n            NewMasterPasswordHash = _newMasterPasswordHash,\n            Token = token,\n            Key = _masterKeyWrappedUserKey\n        };\n\n        using var message = new HttpRequestMessage(HttpMethod.Post, \"/accounts/email\");\n        message.Content = JsonContent.Create(requestModel);\n        return await _client.SendAsync(message);\n    }\n\n    private static string CreateV2SetPasswordRequestJson(\n        string userEmail,\n        string orgIdentifier,\n        string hint,\n        bool includeAccountKeys = true,\n        KdfType? kdfType = null,\n        int? kdfIterations = null,\n        int? kdfMemory = null,\n        int? kdfParallelism = null)\n    {\n        var kdf = new\n        {\n            kdfType = (int)(kdfType ?? KdfType.PBKDF2_SHA256),\n            iterations = kdfIterations ?? 600000,\n            memory = kdfMemory,\n            parallelism = kdfParallelism\n        };\n\n        var request = new\n        {\n            masterPasswordAuthentication = new\n            {\n                kdf,\n                masterPasswordAuthenticationHash = _newMasterPasswordHash,\n                salt = userEmail\n            },\n            masterPasswordUnlock = new\n            {\n                kdf,\n                masterKeyWrappedUserKey = _masterKeyWrappedUserKey,\n                salt = userEmail\n            },\n            accountKeys = includeAccountKeys ? new\n            {\n                accountPublicKey = \"publicKey\",\n                userKeyEncryptedAccountPrivateKey = _mockEncryptedType7String,\n                publicKeyEncryptionKeyPair = new\n                {\n                    publicKey = \"publicKey\",\n                    wrappedPrivateKey = _mockEncryptedType7String,\n                    signedPublicKey = \"signedPublicKey\"\n                },\n                signatureKeyPair = new\n                {\n                    signatureAlgorithm = \"ed25519\",\n                    wrappedSigningKey = _mockEncryptedType7WrappedSigningKey,\n                    verifyingKey = \"verifyingKey\"\n                },\n                securityState = new\n                {\n                    securityVersion = 2,\n                    securityState = \"v2\"\n                }\n            } : null,\n            masterPasswordHint = hint,\n            orgIdentifier\n        };\n\n        return JsonSerializer.Serialize(request, JsonHelpers.CamelCase);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Controllers/ConfigControllerTests.cs",
    "content": "﻿using System.Net.Http.Headers;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.Controllers;\n\npublic class ConfigControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n\n    private string _email = null!;\n\n    public ConfigControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n    }\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n\n        var tokens = await _factory.LoginWithNewAccount(_email);\n        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", tokens.Token);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    private async Task LoginAsync()\n    {\n        var tokens = await _factory.LoginAsync(_email);\n        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", tokens.Token);\n    }\n\n    [Fact]\n    public async Task GetConfigs_Unauthenticated()\n    {\n        _client.DefaultRequestHeaders.Authorization = null;\n\n        var response = await _client.GetAsync(\"/config\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ConfigResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result!.Version);\n    }\n\n    [Fact]\n    public async Task GetConfigs_Authenticated()\n    {\n        await LoginAsync();\n\n        var response = await _client.GetAsync(\"/config\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ConfigResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result!.Version);\n    }\n\n    [Theory]\n    [InlineData(1)]\n    [InlineData(3)]\n    public async Task GetConfigs_WithOrganizations(int orgCount)\n    {\n        for (var i = 0; i < orgCount; i++)\n        {\n            var ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n            await _factory.LoginWithNewAccount(ownerEmail);\n\n            Organization org;\n            (org, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.Free, ownerEmail: ownerEmail,\n                name: i.ToString(), billingEmail: ownerEmail, ownerKey: i.ToString());\n            await OrganizationTestHelpers.CreateUserAsync(_factory, org.Id, _email, Core.Enums.OrganizationUserType.User);\n        }\n\n        await LoginAsync();\n\n        var response = await _client.GetAsync(\"/config\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ConfigResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result!.Version);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Controllers/Public/CollectionsControllerTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Public.Request;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.Controllers.Public;\n\npublic class CollectionsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private string _ownerEmail = null!;\n    private Organization _organization = null!;\n\n    public CollectionsControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _factory.SubstituteService<IPushNotificationService>(_ => { });\n        _factory.SubstituteService<IFeatureService>(_ => { });\n        _client = factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            plan: PlanType.EnterpriseAnnually,\n            ownerEmail: _ownerEmail,\n            passwordManagerSeats: 10,\n            paymentMethod: PaymentMethodType.Card);\n\n        await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    public async Task CreateCollectionWithMultipleUsersAndVariedPermissions_Success()\n    {\n        // Arrange\n        _organization.AllowAdminAccessToAllCollectionItems = true;\n        await _factory.GetService<IOrganizationRepository>().UpsertAsync(_organization);\n\n        var groupRepository = _factory.GetService<IGroupRepository>();\n        var group = await groupRepository.CreateAsync(new Group\n        {\n            OrganizationId = _organization.Id,\n            Name = \"CollectionControllerTests.CreateCollectionWithMultipleUsersAndVariedPermissions_Success\",\n            ExternalId = $\"CollectionControllerTests.CreateCollectionWithMultipleUsersAndVariedPermissions_Success{Guid.NewGuid()}\",\n        });\n\n        var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory,\n            _organization.Id,\n            OrganizationUserType.User);\n\n        var collection = await OrganizationTestHelpers.CreateCollectionAsync(\n            _factory,\n            _organization.Id,\n            \"Shared Collection with a group\",\n            externalId: \"shared-collection-with-group\",\n            groups:\n            [\n                new CollectionAccessSelection { Id = group.Id, ReadOnly = false, HidePasswords = false, Manage = true }\n            ],\n            users:\n            [\n                new CollectionAccessSelection { Id = user.Id, ReadOnly = false, HidePasswords = false, Manage = true }\n            ]);\n\n        var getCollectionsResponse = await _client.GetFromJsonAsync<ListResponseModel<CollectionResponseModel>>(\"public/collections\");\n        var getCollectionResponse = await _client.GetFromJsonAsync<CollectionResponseModel>($\"public/collections/{collection.Id}\");\n\n        var firstCollection = getCollectionsResponse.Data.First(x => x.ExternalId == \"shared-collection-with-group\");\n\n        var update = new CollectionUpdateRequestModel\n        {\n            ExternalId = firstCollection.ExternalId,\n            Groups = firstCollection.Groups?.Select(x => new AssociationWithPermissionsRequestModel\n            {\n                Id = x.Id,\n                ReadOnly = x.ReadOnly,\n                HidePasswords = x.HidePasswords,\n                Manage = x.Manage\n            }),\n        };\n\n        await _client.PutAsJsonAsync($\"public/collections/{firstCollection.Id}\", update);\n\n        var result = await _factory.GetService<ICollectionRepository>()\n            .GetByIdWithAccessAsync(firstCollection.Id);\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.Item2.Groups);\n        Assert.NotEmpty(result.Item2.Users);\n    }\n\n    [Fact]\n    public async Task List_ExcludesDefaultUserCollections_IncludesGroupsAndUsers()\n    {\n        // Arrange\n        var collectionRepository = _factory.GetService<ICollectionRepository>();\n        var groupRepository = _factory.GetService<IGroupRepository>();\n\n        var defaultCollection = new Collection\n        {\n            OrganizationId = _organization.Id,\n            Name = \"My Items\",\n            Type = CollectionType.DefaultUserCollection\n        };\n        await collectionRepository.CreateAsync(defaultCollection, null, null);\n\n        var group = await groupRepository.CreateAsync(new Group\n        {\n            OrganizationId = _organization.Id,\n            Name = \"Test Group\",\n            ExternalId = $\"test-group-{Guid.NewGuid()}\",\n        });\n\n        var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(\n            _factory,\n            _organization.Id,\n            OrganizationUserType.User);\n\n        var sharedCollection = await OrganizationTestHelpers.CreateCollectionAsync(\n            _factory,\n            _organization.Id,\n            \"Shared Collection with Access\",\n            externalId: \"shared-collection-with-access\",\n            groups:\n            [\n                new CollectionAccessSelection { Id = group.Id, ReadOnly = false, HidePasswords = false, Manage = true }\n            ],\n            users:\n            [\n                new CollectionAccessSelection { Id = user.Id, ReadOnly = true, HidePasswords = true, Manage = false }\n            ]);\n\n        // Act\n        var response = await _client.GetFromJsonAsync<ListResponseModel<CollectionResponseModel>>(\"public/collections\");\n\n        // Assert\n        Assert.NotNull(response);\n\n        Assert.DoesNotContain(response.Data, c => c.Id == defaultCollection.Id);\n\n        var collectionResponse = response.Data.First(c => c.Id == sharedCollection.Id);\n        Assert.NotNull(collectionResponse.Groups);\n        Assert.Single(collectionResponse.Groups);\n\n        var groupResponse = collectionResponse.Groups.First();\n        Assert.Equal(group.Id, groupResponse.Id);\n        Assert.False(groupResponse.ReadOnly);\n        Assert.False(groupResponse.HidePasswords);\n        Assert.True(groupResponse.Manage);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Enums;\nusing Bit.IntegrationTestCommon;\nusing Bit.IntegrationTestCommon.Factories;\nusing Bit.Test.Common.Constants;\nusing Microsoft.AspNetCore.Authentication.JwtBearer;\nusing Microsoft.AspNetCore.TestHost;\nusing Xunit;\n\n#nullable enable\n\nnamespace Bit.Api.IntegrationTest.Factories;\n\npublic class ApiApplicationFactory : WebApplicationFactoryBase<Startup>\n{\n    protected IdentityApplicationFactory _identityApplicationFactory;\n\n    public ApiApplicationFactory() : this(new SqliteTestDatabase())\n    {\n    }\n\n    protected ApiApplicationFactory(ITestDatabase db)\n    {\n        TestDatabase = db;\n\n        _identityApplicationFactory = new IdentityApplicationFactory();\n        _identityApplicationFactory.TestDatabase = TestDatabase;\n        _identityApplicationFactory.ManagesDatabase = false;\n    }\n\n    public IdentityApplicationFactory Identity => _identityApplicationFactory;\n\n    protected override void ConfigureWebHost(IWebHostBuilder builder)\n    {\n        base.ConfigureWebHost(builder);\n\n        builder.ConfigureTestServices(services =>\n        {\n            // Remove scheduled background jobs to prevent errors in parallel test execution\n            var jobService = services.First(sd => sd.ServiceType == typeof(IHostedService) && sd.ImplementationType == typeof(Jobs.JobsHostedService));\n            services.Remove(jobService);\n\n            services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>\n            {\n                options.BackchannelHttpHandler = _identityApplicationFactory.Server.CreateHandler();\n            });\n        });\n    }\n\n    /// <summary>\n    /// Helper for registering and logging in to a new account\n    /// </summary>\n    public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(\n        string email = \"integration-test@bitwarden.com\", string masterPasswordHash = \"master_password_hash\")\n    {\n        // This might be the first action in a test and since it forwards to the Identity server, we need to ensure that\n        // this server is initialized since it's responsible for seeding the database.\n        Assert.NotNull(Services);\n\n        await _identityApplicationFactory.RegisterNewIdentityFactoryUserAsync(\n            new RegisterFinishRequestModel\n            {\n                Email = email,\n                MasterPasswordHash = masterPasswordHash,\n                Kdf = KdfType.PBKDF2_SHA256,\n                KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,\n                UserAsymmetricKeys = new KeysRequestModel()\n                {\n                    PublicKey = TestEncryptionConstants.PublicKey,\n                    EncryptedPrivateKey = TestEncryptionConstants.AES256_CBC_HMAC_Encstring\n                },\n                UserSymmetricKey = TestEncryptionConstants.AES256_CBC_HMAC_Encstring,\n            });\n\n        return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);\n    }\n\n    /// <summary>\n    /// Helper for logging in to an account\n    /// </summary>\n    public async Task<(string Token, string RefreshToken)> LoginAsync(string email = \"integration-test@bitwarden.com\", string masterPasswordHash = \"master_password_hash\")\n    {\n        return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);\n    }\n\n    /// <summary>\n    /// Helper for logging in via client secret.\n    /// Currently used for Secrets Manager service accounts\n    /// </summary>\n    public async Task<string> LoginWithClientSecretAsync(Guid clientId, string clientSecret)\n    {\n        return await _identityApplicationFactory.TokenFromAccessTokenAsync(clientId, clientSecret);\n    }\n\n    /// <summary>\n    /// Helper for logging in with an Organization api key.\n    /// Currently used for the Public Api\n    /// </summary>\n    public async Task<string> LoginWithOrganizationApiKeyAsync(string clientId, string clientSecret)\n    {\n        return await _identityApplicationFactory.TokenFromOrganizationApiKeyAsync(clientId, clientSecret);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Factories/SqlServerApiApplicationFactory.cs",
    "content": "﻿using Bit.IntegrationTestCommon;\n\n#nullable enable\n\nnamespace Bit.Api.IntegrationTest.Factories;\n\npublic class SqlServerApiApplicationFactory() : ApiApplicationFactory(new SqlServerTestDatabase());\n"
  },
  {
    "path": "test/Api.IntegrationTest/Helpers/LoginHelper.cs",
    "content": "﻿using System.Net.Http.Headers;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Core.Repositories;\nusing Bit.IntegrationTestCommon.Factories;\n\nnamespace Bit.Api.IntegrationTest.Helpers;\n\npublic class LoginHelper\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n\n    public LoginHelper(ApiApplicationFactory factory, HttpClient client)\n    {\n        _factory = factory;\n        _client = client;\n    }\n\n    public async Task LoginAsync(string email)\n    {\n        var tokens = await _factory.LoginAsync(email);\n        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", tokens.Token);\n    }\n\n    public async Task LoginWithOrganizationApiKeyAsync(Guid organizationId)\n    {\n        var (clientId, apiKey) = await GetOrganizationApiKey(_factory, organizationId);\n        var token = await _factory.LoginWithOrganizationApiKeyAsync(clientId, apiKey);\n        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n        _client.DefaultRequestHeaders.Add(\"client_id\", clientId);\n    }\n\n    private async Task<(string clientId, string apiKey)> GetOrganizationApiKey<T>(\n        WebApplicationFactoryBase<T> factory,\n        Guid organizationId)\n        where T : class\n    {\n        var organizationApiKeyRepository = factory.GetService<IOrganizationApiKeyRepository>();\n        var apiKeys = await organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(organizationId);\n        var clientId = $\"organization.{organizationId}\";\n        return (clientId, apiKeys.Single().ApiKey);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs",
    "content": "﻿using System.Diagnostics;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.IntegrationTestCommon.Factories;\n\nnamespace Bit.Api.IntegrationTest.Helpers;\n\npublic static class OrganizationTestHelpers\n{\n    public static async Task<Tuple<Organization, OrganizationUser>> SignUpAsync<T>(WebApplicationFactoryBase<T> factory,\n        PlanType plan = PlanType.Free,\n        string ownerEmail = \"integration-test@bitwarden.com\",\n        string name = \"Integration Test Org\",\n        string billingEmail = \"integration-test@bitwarden.com\",\n        string ownerKey = \"test-key\",\n        int passwordManagerSeats = 0,\n        PaymentMethodType paymentMethod = PaymentMethodType.None) where T : class\n    {\n        var userRepository = factory.GetService<IUserRepository>();\n        var organizationSignUpCommand = factory.GetService<ICloudOrganizationSignUpCommand>();\n\n        var owner = await userRepository.GetByEmailAsync(ownerEmail);\n\n        var signUpResult = await organizationSignUpCommand.SignUpOrganizationAsync(new OrganizationSignup\n        {\n            Name = name,\n            BillingEmail = billingEmail,\n            Plan = plan,\n            OwnerKey = ownerKey,\n            Owner = owner,\n            AdditionalSeats = passwordManagerSeats,\n            PaymentMethodType = paymentMethod,\n            PaymentToken = \"TOKEN\",\n            TaxInfo = new TaxInfo\n            {\n                BillingAddressCountry = \"US\",\n                BillingAddressPostalCode = \"12345\"\n            }\n        });\n\n        Debug.Assert(signUpResult.OrganizationUser is not null);\n\n        return new Tuple<Organization, OrganizationUser>(signUpResult.Organization, signUpResult.OrganizationUser);\n    }\n\n    /// <summary>\n    /// Creates an OrganizationUser. The user account must already be created.\n    /// </summary>\n    public static async Task<OrganizationUser> CreateUserAsync<T>(\n        WebApplicationFactoryBase<T> factory,\n        Guid organizationId,\n        string userEmail,\n        OrganizationUserType type,\n        bool accessSecretsManager = false,\n        Permissions? permissions = null,\n        OrganizationUserStatusType userStatusType = OrganizationUserStatusType.Confirmed,\n        string? externalId = null\n    ) where T : class\n    {\n        var userRepository = factory.GetService<IUserRepository>();\n        var organizationUserRepository = factory.GetService<IOrganizationUserRepository>();\n\n        var user = await userRepository.GetByEmailAsync(userEmail);\n        Debug.Assert(user is not null);\n\n        var orgUser = new OrganizationUser\n        {\n            OrganizationId = organizationId,\n            UserId = user.Id,\n            Key = null,\n            Type = type,\n            Status = userStatusType,\n            ExternalId = externalId,\n            AccessSecretsManager = accessSecretsManager,\n            Email = userEmail\n        };\n\n        if (permissions != null)\n        {\n            orgUser.SetPermissions(permissions);\n        }\n\n        await organizationUserRepository.CreateAsync(orgUser);\n\n        return orgUser;\n    }\n\n    /// <summary>\n    /// Creates a new User account with a unique email address and a corresponding OrganizationUser for\n    /// the specified organization.\n    /// </summary>\n    public static async Task<(string, OrganizationUser)> CreateNewUserWithAccountAsync(\n        ApiApplicationFactory factory,\n        Guid organizationId,\n        OrganizationUserType userType,\n        Permissions? permissions = null\n    )\n    {\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n\n        // Create user\n        await factory.LoginWithNewAccount(email);\n\n        // Create organizationUser\n        var organizationUser = await CreateUserAsync(factory, organizationId, email, userType,\n            permissions: permissions);\n\n        return (email, organizationUser);\n    }\n\n    /// <summary>\n    /// Creates a VerifiedDomain for the specified organization.\n    /// </summary>\n    public static async Task CreateVerifiedDomainAsync(ApiApplicationFactory factory, Guid organizationId, string domain)\n    {\n        var organizationDomainRepository = factory.GetService<IOrganizationDomainRepository>();\n\n        var verifiedDomain = new OrganizationDomain\n        {\n            OrganizationId = organizationId,\n            DomainName = domain,\n            Txt = \"btw+test18383838383\"\n        };\n        verifiedDomain.SetVerifiedDate();\n\n        await organizationDomainRepository.CreateAsync(verifiedDomain);\n    }\n\n    public static async Task<Group> CreateGroup(ApiApplicationFactory factory, Guid organizationId)\n    {\n\n        var groupRepository = factory.GetService<IGroupRepository>();\n        var group = new Group\n        {\n            OrganizationId = organizationId,\n            Id = new Guid(),\n            ExternalId = \"bwtest-externalId\",\n            Name = \"bwtest\"\n        };\n\n        await groupRepository.CreateAsync(group, new List<CollectionAccessSelection>());\n        return group;\n    }\n\n    /// <summary>\n    /// Creates a collection with optional user and group associations.\n    /// </summary>\n    public static async Task<Collection> CreateCollectionAsync(\n        ApiApplicationFactory factory,\n        Guid organizationId,\n        string name,\n        IEnumerable<CollectionAccessSelection>? users = null,\n        IEnumerable<CollectionAccessSelection>? groups = null,\n        string? externalId = null)\n    {\n        var collectionRepository = factory.GetService<ICollectionRepository>();\n        var collection = new Collection\n        {\n            OrganizationId = organizationId,\n            Name = name,\n            Type = CollectionType.SharedCollection,\n            ExternalId = externalId\n        };\n\n        await collectionRepository.CreateAsync(collection, groups, users);\n        return collection;\n    }\n\n    /// <summary>\n    /// Enables the Organization Data Ownership policy for the specified organization.\n    /// </summary>\n    public static async Task EnableOrganizationDataOwnershipPolicyAsync<T>(\n        WebApplicationFactoryBase<T> factory,\n        Guid organizationId) where T : class\n    {\n        var policyRepository = factory.GetService<IPolicyRepository>();\n\n        var policy = new Policy\n        {\n            OrganizationId = organizationId,\n            Type = PolicyType.OrganizationDataOwnership,\n            Enabled = true\n        };\n\n        await policyRepository.CreateAsync(policy);\n    }\n\n    /// <summary>\n    /// Generates a unique random domain name for testing purposes.\n    /// </summary>\n    /// <returns>A domain string like \"a1b2c3d4.com\"</returns>\n    public static string GenerateRandomDomain()\n    {\n        return $\"{Guid.NewGuid().ToString(\"N\").Substring(0, 8)}.com\";\n    }\n\n    /// <summary>\n    /// Creates a user account without a Master Password and adds them as a member to the specified organization.\n    /// </summary>\n    public static async Task<(User User, OrganizationUser OrganizationUser)> CreateUserWithoutMasterPasswordAsync(ApiApplicationFactory factory, string email, Guid organizationId)\n    {\n        var userRepository = factory.GetService<IUserRepository>();\n        var user = await userRepository.CreateAsync(new User\n        {\n            Email = email,\n            Culture = \"en-US\",\n            SecurityStamp = \"D7ZH62BWAZ5R5CASKULCDDIQGKDA2EJ6\",\n            PublicKey = \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwMj7W00xS7H0NWasGn7PfEq8VfH3fa5XuZucsKxLLRAHHZk0xGRZJH2lFIznizv3GpF8vzhHhe9VpmMkrdIa5oWhwHpy+D7Z1QCQxuUXzvMKpa95GOntr89nN/mWKpk6abjgjmDcqFJ0lhDqkKnDfes+d8BBd5oEA8p41/Ykz7OfG7AiktVBpTQFW09MQh1NOvcLxVgiUUVRPwNRKrOeCekWDtOjZhASMETv3kI1ogvhHukOQ3ztDzrxvmwnLQ+cXl1EeD8gQnGDp3QLiJqxPgh2EdmANh4IzjRexoDn6BqhRGqLLIoLAbbkoiNrd6NYujrWW0N8KMMoVEXuJL2g4wIDAQAB\",\n            PrivateKey = \"2.Ytudv+Qk3ET9hN8whqpuGg==|ijsFhmjaf1aaT9uz+IPhVTzMS+2W/ldAP8LdT5VyJaFdx4HSdLcWSZvz5xWuuW94zfv1Qh+p3iQIuZOr29G4jcx47rYtz4ssiFtB7Ia552ZeF+cb7uuVg40CIe7ycuJQITk00o8gots+wFnaEvk0Vjgycnqutm0jpeBJ1joWJWqTVgSsYdUGLu7PiJywQ9NgY4+bJXqadlcviS3rhPKJXtiXYJhqJqSw+vI0Yxp96MJ0HcFJk/LG22YJPTvL5kzuDq/Wzj40kj8blQ+ag+xHD4P/KJ/MppEB3OpDw3UoJ50Ek+YB9pOqGxZtvqMEzBDsgh0yoz1O992UnhaUqtJ5e9Bxy3PA6cJsdyn9npduNOreEb8vePCidN2XC+chjJpPFpjms9muHLKgfaTIfpiJA2Tz8E9dvSyhHHTE1mY+xEA7P08BYKN3LNoSGIjdiZuouJ1V/KZvCssDfVG1tli2qpnhTIh4m3rAMhbM8WW3B7wCV8N0MpcJJSvndkVcMgRbgWcbivLeXuKdE/K98n01RvOLSJyslhLGCGEQQKw6N3HQ2iELfv84YQZi2fjDK+OqAmXDq1pNcjKX2I8dqBwl31tPC8qSZiWnfinwLdqQTvSQjOIyAHb4sSjAwgdMbCRzUTChRr09l+PAZqGWdMC5N2Bw+bA8WP0l2Wdxuv9Abxl3F7xGeAA9Rw9PU5wGKujaMRmO4V9MFjNyyCcw4D9pzKMW6OUKsHsHE7tsG7KskCzksHzrZGawAt0S41BYQA/JwePCrD3F6dM92anlC1LfA00KJb0tmFdU0yJNmJfR+S78yn8yM6wDgIs2cFB3W1fYfpfUvQm+zzPoEQihNxBxnwFsBtMAOtPy54FjSzKmxsQTrYT9E6NFb8k6ZIIm2gNeOPK9OUJgjw+4g2BXErM6ikHTzM3xcaTq/cQaePZ52emndw1qOtdV06hr2EeuLM8frfLHpsknUe8JeYeW5p9E8QdZjjSN9034usdYNamUdxzmn/Mw/ar8z1xSKS6zcaQoTQ7aYLEX3dWJndc4W64HyiaRkLjO6qLUFeOerfz5UvcxxRY89eAA0KLC2xnGkBMOhXxYzIB3lF8Zxqb4JMhoBGw1n31TDfhRDGDHHEAsZuAIcH7aC5RDVxU08Jxmw4oLmeTDZA5BFcqp2A3fusNVZUnfpmMy6DCJyFprlRl8jSlJMAvhbxVuuLFDZnjl77Z2of796Ur6DgmNwYtMPNEntZPIcZ76VPLWAL8lqiRBm20c4qiwr5rNSr5kry9bR1EfXHwFRjy5pxFQ+5+ilpRl8WPfT/iUuORd8J2wnCmghm7uxiJd9t82kX0s6benhL29dQ1etqt5soX2RnlfKan16GVWoI3xrljIQrCAY4xpdptSpglOnrpSClbN1nhGkDfFPNq2pWhQrDbznDknAJ9MxQaVnLYPhn7I849GMd7EvpSkydwQu7QXn9+H4jxn6UEntNGxcL0xkG+xippvZEe+HBvcDD40efDQW1bDbILLjPb4rNRx4d3xaQnVNaF7L33osm5LgfXAQSwHJiURdkU4zmhtPP4zn0br0OdFlR3mPcrkeNeSvs7FxiKtD6n6s+av+4bKjbLL1OyuwmTnMilL6p+m8ldte0yos/r+zOuxWeI=|euhiXWXehYbFQhlAV6LIECSIPCIRaHbNdr9OI4cTPUM=\",\n            ApiKey = \"CfGrD4MoJu3NprOBZNL8tu5ocmtnmU\",\n            KdfIterations = 600000\n        });\n\n        var organizationUser = await CreateUserAsync(factory, organizationId, user.Email,\n            OrganizationUserType.User, externalId: email);\n\n        return (user, organizationUser);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Helpers/PerformanceTestHelpers.cs",
    "content": "﻿using System.Net.Http.Headers;\nusing Bit.Api.IntegrationTest.Factories;\n\nnamespace Bit.Api.IntegrationTest.Helpers;\n\n/// <summary>\n/// Helper methods for performance tests to reduce code duplication.\n/// </summary>\npublic static class PerformanceTestHelpers\n{\n    /// <summary>\n    /// Standard password hash used across performance tests.\n    /// </summary>\n    public const string StandardPasswordHash = \"c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs=\";\n\n    /// <summary>\n    /// Authenticates an HttpClient with a bearer token for the specified user.\n    /// </summary>\n    /// <param name=\"factory\">The application factory to use for login.</param>\n    /// <param name=\"client\">The HttpClient to authenticate.</param>\n    /// <param name=\"email\">The user's email address.</param>\n    /// <param name=\"masterPasswordHash\">The user's master password hash. Defaults to StandardPasswordHash.</param>\n    public static async Task AuthenticateClientAsync(\n        SqlServerApiApplicationFactory factory,\n        HttpClient client,\n        string email,\n        string? masterPasswordHash = null)\n    {\n        var tokens = await factory.LoginAsync(email, masterPasswordHash ?? StandardPasswordHash);\n        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", tokens.Token);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Helpers/ProviderTestHelpers.cs",
    "content": "﻿using Bit.Api.IntegrationTest.Factories;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Repositories;\n\nnamespace Bit.Api.IntegrationTest.Helpers;\n\npublic static class ProviderTestHelpers\n{\n    /// <summary>\n    /// Creates a provider and links it to an organization.\n    /// This does NOT create any provider users.\n    /// </summary>\n    /// <param name=\"factory\">The API application factory</param>\n    /// <param name=\"organizationId\">The organization ID to link to the provider</param>\n    /// <param name=\"providerType\">The type of provider to create</param>\n    /// <param name=\"providerStatus\">The provider status (defaults to Created)</param>\n    /// <returns>The created provider</returns>\n    public static async Task<Provider> CreateProviderAndLinkToOrganizationAsync(\n        ApiApplicationFactory factory,\n        Guid organizationId,\n        ProviderType providerType,\n        ProviderStatusType providerStatus = ProviderStatusType.Created)\n    {\n        var providerRepository = factory.GetService<IProviderRepository>();\n        var providerOrganizationRepository = factory.GetService<IProviderOrganizationRepository>();\n\n        // Create the provider\n        var provider = await providerRepository.CreateAsync(new Provider\n        {\n            Name = $\"Test {providerType} Provider\",\n            BusinessName = $\"Test {providerType} Provider Business\",\n            BillingEmail = $\"provider-{providerType.ToString().ToLower()}@example.com\",\n            Type = providerType,\n            Status = providerStatus,\n            Enabled = true\n        });\n\n        // Link the provider to the organization\n        await providerOrganizationRepository.CreateAsync(new ProviderOrganization\n        {\n            ProviderId = provider.Id,\n            OrganizationId = organizationId,\n            Key = \"test-provider-key\"\n        });\n\n        return provider;\n    }\n\n    /// <summary>\n    /// Creates a providerUser for a provider.\n    /// </summary>\n    public static async Task<ProviderUser> CreateProviderUserAsync(\n        ApiApplicationFactory factory,\n        Guid providerId,\n        string userEmail,\n        ProviderUserType providerUserType)\n    {\n        var userRepository = factory.GetService<IUserRepository>();\n        var user = await userRepository.GetByEmailAsync(userEmail);\n        if (user is null)\n        {\n            throw new Exception(\"No user found in test setup.\");\n        }\n\n        var providerUserRepository = factory.GetService<IProviderUserRepository>();\n        return await providerUserRepository.CreateAsync(new ProviderUser\n        {\n            ProviderId = providerId,\n            Status = ProviderUserStatusType.Confirmed,\n            UserId = user.Id,\n            Key = Guid.NewGuid().ToString(),\n            Type = providerUserType\n        });\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs",
    "content": "﻿#nullable enable\nusing System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.KeyManagement.Models.Requests;\nusing Bit.Api.KeyManagement.Models.Responses;\nusing Bit.Api.Tools.Models.Request;\nusing Bit.Api.Vault.Models;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Entities;\nusing Bit.Core.KeyManagement.Enums;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Vault.Enums;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.KeyManagement.Controllers;\n\npublic class AccountsKeyManagementControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private static readonly string _mockEncryptedString =\n        \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n    private static readonly string _mockEncryptedType2String2 =\n        \"2.06CDSJjTZaigYHUuswIq5A==|trxgZl2RCkYrrmCvGE9WNA==|w5p05eI5wsaYeSyWtsAPvBX63vj798kIMxBTfSB0BQg=\";\n    private static readonly string _mockEncryptedType7String = \"7.AOs41Hd8OQiCPXjyJKCiDA==\";\n    private static readonly string _mockEncryptedType7String2 = \"7.Mi1iaXR3YXJkZW4tZGF0YQo=\";\n    private static readonly string _mockEncryptedType7WrappedSigningKey = \"7.DRv74Kg1RSlFSam1MNFlGD==\";\n\n    private readonly HttpClient _client;\n    private readonly IEmergencyAccessRepository _emergencyAccessRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n    private readonly IUserRepository _userRepository;\n    private readonly IDeviceRepository _deviceRepository;\n    private readonly IPasswordHasher<User> _passwordHasher;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;\n    private readonly IPushNotificationService _pushNotificationService;\n    private string _ownerEmail = null!;\n\n    public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _factory.SubstituteService<IPushNotificationService>(_ => { });\n        _client = factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n        _userRepository = _factory.GetService<IUserRepository>();\n        _deviceRepository = _factory.GetService<IDeviceRepository>();\n        _emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>();\n        _organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        _passwordHasher = _factory.GetService<IPasswordHasher<User>>();\n        _organizationRepository = _factory.GetService<IOrganizationRepository>();\n        _userSignatureKeyPairRepository = _factory.GetService<IUserSignatureKeyPairRepository>();\n        _pushNotificationService = _factory.GetService<IPushNotificationService>();\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegenerateKeysAsync_NotLoggedIn_Unauthorized(KeyRegenerationRequestModel request)\n    {\n        request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/regenerate-keys\", request);\n\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.Confirmed)]\n    [BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryApproved)]\n    [BitAutoData(OrganizationUserStatusType.Confirmed, EmergencyAccessStatusType.RecoveryInitiated)]\n    [BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.Confirmed)]\n    [BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryApproved)]\n    [BitAutoData(OrganizationUserStatusType.Revoked, EmergencyAccessStatusType.RecoveryInitiated)]\n    [BitAutoData(OrganizationUserStatusType.Confirmed, null)]\n    [BitAutoData(OrganizationUserStatusType.Revoked, null)]\n    [BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.Confirmed)]\n    [BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryApproved)]\n    [BitAutoData(OrganizationUserStatusType.Invited, EmergencyAccessStatusType.RecoveryInitiated)]\n    public async Task RegenerateKeysAsync_UserInOrgOrHasDesignatedEmergencyAccess_ThrowsBadRequest(\n        OrganizationUserStatusType organizationUserStatus,\n        EmergencyAccessStatusType? emergencyAccessStatus,\n        KeyRegenerationRequestModel request)\n    {\n        if (organizationUserStatus is OrganizationUserStatusType.Confirmed or OrganizationUserStatusType.Revoked)\n        {\n            await CreateOrganizationUserAsync(organizationUserStatus);\n        }\n\n        if (emergencyAccessStatus != null)\n        {\n            await CreateDesignatedEmergencyAccessAsync(emergencyAccessStatus.Value);\n        }\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n        request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/regenerate-keys\", request);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegenerateKeysAsync_Success(KeyRegenerationRequestModel request)\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n        request.UserKeyEncryptedUserPrivateKey = _mockEncryptedString;\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/regenerate-keys\", request);\n        response.EnsureSuccessStatusCode();\n\n        var user = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(user);\n        Assert.Equal(request.UserPublicKey, user.PublicKey);\n        Assert.Equal(request.UserKeyEncryptedUserPrivateKey, user.PrivateKey);\n    }\n\n    private async Task CreateOrganizationUserAsync(OrganizationUserStatusType organizationUserStatus)\n    {\n        var (_, organizationUser) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,\n            paymentMethod: PaymentMethodType.Card);\n        organizationUser.Status = organizationUserStatus;\n        await _organizationUserRepository.ReplaceAsync(organizationUser);\n    }\n\n    private async Task CreateDesignatedEmergencyAccessAsync(EmergencyAccessStatusType emergencyAccessStatus)\n    {\n        var tempEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(tempEmail);\n\n        var tempUser = await _userRepository.GetByEmailAsync(tempEmail);\n        var user = await _userRepository.GetByEmailAsync(_ownerEmail);\n        var emergencyAccess = new EmergencyAccess\n        {\n            GrantorId = tempUser!.Id,\n            GranteeId = user!.Id,\n            KeyEncrypted = _mockEncryptedString,\n            Status = emergencyAccessStatus,\n            Type = EmergencyAccessType.View,\n            WaitTimeDays = 10,\n            CreationDate = DateTime.UtcNow,\n            RevisionDate = DateTime.UtcNow\n        };\n        await _emergencyAccessRepository.CreateAsync(emergencyAccess);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeysAsync_NotLoggedIn_Unauthorized(\n        RotateUserAccountKeysAndDataRequestModel request)\n    {\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)\n    {\n        var user = await SetupUserForKeyRotationAsync();\n        SetupRotateUserAccountUnlockData(request, user);\n        SetupRotateUserAccountData(request);\n        SetupRotateUserAccountKeys(request, isV2Crypto: false);\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n        var responseMessage = await response.Content.ReadAsStringAsync();\n        response.EnsureSuccessStatusCode();\n\n        var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userNewState);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.Email, userNewState.Email);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfType, userNewState.Kdf);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations, userNewState.KdfIterations);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetKeyConnectorKeyAsync_NotLoggedIn_Unauthorized(SetKeyConnectorKeyRequestModel request)\n    {\n        var response = await _client.PostAsJsonAsync(\"/accounts/set-key-connector-key\", request);\n\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier)\n    {\n        var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);\n\n        var ssoUser = await _userRepository.GetByEmailAsync(ssoUserEmail);\n        Assert.NotNull(ssoUser);\n\n        var request = new SetKeyConnectorKeyRequestModel\n        {\n            Key = _mockEncryptedString,\n            Keys = new KeysRequestModel { PublicKey = ssoUser.PublicKey, EncryptedPrivateKey = ssoUser.PrivateKey },\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,\n            OrgIdentifier = organizationSsoIdentifier\n        };\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/set-key-connector-key\", request);\n        response.EnsureSuccessStatusCode();\n\n        var user = await _userRepository.GetByEmailAsync(ssoUserEmail);\n        Assert.NotNull(user);\n        Assert.Equal(request.Key, user.Key);\n        Assert.True(user.UsesKeyConnector);\n        Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));\n        Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));\n        var ssoOrganizationUser = await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);\n        Assert.NotNull(ssoOrganizationUser);\n        Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);\n        Assert.Equal(user.Id, ssoOrganizationUser.UserId);\n        Assert.Null(ssoOrganizationUser.Email);\n    }\n\n    [Fact]\n    public async Task PostSetKeyConnectorKeyAsync_V2_NotLoggedIn_Unauthorized()\n    {\n        var request = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = _mockEncryptedString,\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = \"publicKey\",\n                UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String\n            },\n            OrgIdentifier = \"test-org\"\n        };\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/set-key-connector-key\", request);\n\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetKeyConnectorKeyAsync_V2_Success(string organizationSsoIdentifier)\n    {\n        var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);\n\n        var request = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = _mockEncryptedString,\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = \"publicKey\",\n                UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String,\n                PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel\n                {\n                    PublicKey = \"publicKey\",\n                    WrappedPrivateKey = _mockEncryptedType7String,\n                    SignedPublicKey = \"signedPublicKey\"\n                },\n                SignatureKeyPair = new SignatureKeyPairRequestModel\n                {\n                    SignatureAlgorithm = \"ed25519\",\n                    WrappedSigningKey = _mockEncryptedType7WrappedSigningKey,\n                    VerifyingKey = \"verifyingKey\"\n                },\n                SecurityState = new SecurityStateModel\n                {\n                    SecurityVersion = 2,\n                    SecurityState = \"v2\"\n                }\n            },\n            OrgIdentifier = organizationSsoIdentifier\n        };\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/set-key-connector-key\", request);\n        response.EnsureSuccessStatusCode();\n\n        var user = await _userRepository.GetByEmailAsync(ssoUserEmail);\n        Assert.NotNull(user);\n        Assert.Equal(request.KeyConnectorKeyWrappedUserKey, user.Key);\n        Assert.True(user.UsesKeyConnector);\n        Assert.Equal(KdfType.Argon2id, user.Kdf);\n        Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, user.KdfIterations);\n        Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, user.KdfMemory);\n        Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, user.KdfParallelism);\n        Assert.Equal(request.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey, user.SignedPublicKey);\n        Assert.Equal(request.AccountKeys.SecurityState!.SecurityState, user.SecurityState);\n        Assert.Equal(request.AccountKeys.SecurityState.SecurityVersion, user.SecurityVersion);\n        Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));\n        Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));\n\n        var ssoOrganizationUser =\n            await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);\n        Assert.NotNull(ssoOrganizationUser);\n        Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);\n        Assert.Equal(user.Id, ssoOrganizationUser.UserId);\n        Assert.Null(ssoOrganizationUser.Email);\n\n        var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id);\n        Assert.NotNull(signatureKeyPair);\n        Assert.Equal(SignatureAlgorithm.Ed25519, signatureKeyPair.SignatureAlgorithm);\n        Assert.Equal(_mockEncryptedType7WrappedSigningKey, signatureKeyPair.WrappedSigningKey);\n        Assert.Equal(\"verifyingKey\", signatureKeyPair.VerifyingKey);\n    }\n\n    [Fact]\n    public async Task PostConvertToKeyConnectorAsync_NotLoggedIn_Unauthorized()\n    {\n        var response = await _client.PostAsJsonAsync(\"/accounts/convert-to-key-connector\", new { });\n\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PostConvertToKeyConnectorAsync_Success()\n    {\n        var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Accepted);\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/convert-to-key-connector\", new { });\n        response.EnsureSuccessStatusCode();\n\n        var user = await _userRepository.GetByEmailAsync(ssoUserEmail);\n        Assert.NotNull(user);\n        Assert.Null(user.MasterPassword);\n        Assert.True(user.UsesKeyConnector);\n        Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));\n        Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));\n    }\n\n    [Fact]\n    public async Task PostEnrollToKeyConnectorAsync_NotLoggedIn_Unauthorized()\n    {\n        var request = new KeyConnectorEnrollmentRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = _mockEncryptedString\n        };\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-connector/enroll\", request);\n\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PostEnrollToKeyConnectorAsync_KeyConnectorKeyWrappedUserKeyMissing_BadRequest()\n    {\n        var (ssoUserEmail, _) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Accepted);\n\n        var request = new KeyConnectorEnrollmentRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = \" \"\n        };\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-connector/enroll\", request);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n\n        var user = await _userRepository.GetByEmailAsync(ssoUserEmail);\n        Assert.NotNull(user);\n        Assert.False(user.UsesKeyConnector);\n    }\n\n    [Fact]\n    public async Task PostEnrollToKeyConnectorAsync_Success()\n    {\n        var (ssoUserEmail, _) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Accepted);\n\n        var request = new KeyConnectorEnrollmentRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = _mockEncryptedString\n        };\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-connector/enroll\", request);\n        response.EnsureSuccessStatusCode();\n\n        var user = await _userRepository.GetByEmailAsync(ssoUserEmail);\n        Assert.NotNull(user);\n        Assert.Null(user.MasterPassword);\n        Assert.True(user.UsesKeyConnector);\n        Assert.Equal(request.KeyConnectorKeyWrappedUserKey, user.Key);\n        Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));\n        Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateV2UserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)\n    {\n        var user = await SetupUserForKeyRotationAsync(_mockEncryptedType7String, createSignatureKeyPair: true);\n        SetupRotateUserAccountUnlockData(request, user);\n        SetupRotateUserAccountData(request);\n        SetupRotateUserAccountKeys(request, isV2Crypto: true);\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n        var responseMessage = await response.Content.ReadAsStringAsync();\n        response.EnsureSuccessStatusCode();\n\n        var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userNewState);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.Email, userNewState.Email);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfType, userNewState.Kdf);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations, userNewState.KdfIterations);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);\n\n        // Assert V2-specific fields\n        Assert.Equal(request.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey, userNewState.SignedPublicKey);\n        Assert.Equal(request.AccountKeys.SecurityState!.SecurityState, userNewState.SecurityState);\n        Assert.Equal(request.AccountKeys.SecurityState.SecurityVersion, userNewState.SecurityVersion);\n\n        var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(userNewState.Id);\n        Assert.NotNull(signatureKeyPair);\n        Assert.Equal(SignatureAlgorithm.Ed25519, signatureKeyPair.SignatureAlgorithm);\n        Assert.Equal(request.AccountKeys.SignatureKeyPair!.WrappedSigningKey, signatureKeyPair.WrappedSigningKey);\n        Assert.Equal(request.AccountKeys.SignatureKeyPair.VerifyingKey, signatureKeyPair.VerifyingKey);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUpgradeToV2UserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)\n    {\n        var user = await SetupUserForKeyRotationAsync();\n        SetupRotateUserAccountUnlockData(request, user);\n        SetupRotateUserAccountData(request);\n        SetupRotateUserAccountKeys(request, isV2Crypto: true);\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n        var responseMessage = await response.Content.ReadAsStringAsync();\n        response.EnsureSuccessStatusCode();\n\n        var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userNewState);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.Email, userNewState.Email);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfType, userNewState.Kdf);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations, userNewState.KdfIterations);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);\n        Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);\n\n        // Assert V2 upgrade-specific fields\n        Assert.Equal(request.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey, userNewState.SignedPublicKey);\n        Assert.Equal(request.AccountKeys.SecurityState!.SecurityState, userNewState.SecurityState);\n        Assert.Equal(request.AccountKeys.SecurityState.SecurityVersion, userNewState.SecurityVersion);\n\n        var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(userNewState.Id);\n        Assert.NotNull(signatureKeyPair);\n        Assert.Equal(SignatureAlgorithm.Ed25519, signatureKeyPair.SignatureAlgorithm);\n        Assert.Equal(request.AccountKeys.SignatureKeyPair!.WrappedSigningKey, signatureKeyPair.WrappedSigningKey);\n        Assert.Equal(request.AccountKeys.SignatureKeyPair.VerifyingKey, signatureKeyPair.VerifyingKey);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_V1Crypto_WithV2UpgradeToken_PersistsToken_AndDoesNotLogout(\n        RotateUserAccountKeysAndDataRequestModel request)\n    {\n        var user = await SetupUserForKeyRotationAsync();\n        SetupRotateUserAccountUnlockData(request, user);\n        SetupRotateUserAccountData(request);\n        SetupRotateUserAccountKeys(request, isV2Crypto: false);\n        request.AccountUnlockData.V2UpgradeToken = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedString\n        };\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n        response.EnsureSuccessStatusCode();\n\n        var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userNewState);\n        Assert.NotNull(userNewState.V2UpgradeToken);\n        Assert.Contains($\"\\\"WrappedUserKey1\\\":\\\"{_mockEncryptedType7String}\\\"\", userNewState.V2UpgradeToken);\n        Assert.Contains($\"\\\"WrappedUserKey2\\\":\\\"{_mockEncryptedString}\\\"\", userNewState.V2UpgradeToken);\n        Assert.Equal(user.SecurityStamp, userNewState.SecurityStamp);\n        await _pushNotificationService.Received(1)\n            .PushLogOutAsync(userNewState.Id, false, PushNotificationLogOutReason.KeyRotation);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_V2Crypto_WithV2UpgradeToken_IgnoresToken_AndLogsOut(\n        RotateUserAccountKeysAndDataRequestModel request)\n    {\n        var user = await SetupUserForKeyRotationAsync(_mockEncryptedType7String, createSignatureKeyPair: true);\n        SetupRotateUserAccountUnlockData(request, user);\n        SetupRotateUserAccountData(request);\n        SetupRotateUserAccountKeys(request, isV2Crypto: true);\n        request.AccountUnlockData.V2UpgradeToken = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedString\n        };\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n        response.EnsureSuccessStatusCode();\n\n        var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userNewState);\n\n        // Token must NOT be stored (V2 users don't need upgrade token)\n        Assert.Null(userNewState.V2UpgradeToken);\n\n        // Security stamp must change (logout occurred)\n        Assert.NotEqual(user.SecurityStamp, userNewState.SecurityStamp);\n\n        // Standard logout push sent without a reason (full logout, not KeyRotation)\n        await _pushNotificationService.Received(1)\n            .PushLogOutAsync(userNewState.Id, false, null);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_WithoutV2UpgradeToken_DoesNotSetToken_AndLogsOut(\n        RotateUserAccountKeysAndDataRequestModel request)\n    {\n        var user = await SetupUserForKeyRotationAsync();\n        SetupRotateUserAccountUnlockData(request, user);\n        SetupRotateUserAccountData(request);\n        SetupRotateUserAccountKeys(request, isV2Crypto: false);\n\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n        response.EnsureSuccessStatusCode();\n\n        var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userNewState);\n        Assert.Null(userNewState.V2UpgradeToken);\n        Assert.NotEqual(user.SecurityStamp, userNewState.SecurityStamp);\n        await _pushNotificationService.Received(1).PushLogOutAsync(userNewState.Id, false, null);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_WithExistingV2UpgradeToken_WithoutNewToken_ClearsStaleToken_AndLogsOut(\n        RotateUserAccountKeysAndDataRequestModel request)\n    {\n        // Arrange\n        var user = await SetupUserForKeyRotationAsync();\n\n        // Add existing stale token to user BEFORE rotation\n        var staleToken = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedString\n        };\n        user.V2UpgradeToken = staleToken.ToJson();\n        await _userRepository.ReplaceAsync(user);\n\n        // Setup request WITHOUT V2UpgradeToken\n        SetupRotateUserAccountUnlockData(request, user);\n        SetupRotateUserAccountData(request);\n        SetupRotateUserAccountKeys(request, isV2Crypto: false);\n        request.AccountUnlockData.V2UpgradeToken = null; // Explicit: No new token\n\n        // Act\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n        response.EnsureSuccessStatusCode();\n\n        // Assert\n        var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userNewState);\n\n        // Critical: Verify stale token is cleared\n        Assert.Null(userNewState.V2UpgradeToken);\n\n        // Verify logout behavior (SecurityStamp should be different)\n        Assert.NotEqual(user.SecurityStamp, userNewState.SecurityStamp);\n        await _pushNotificationService.Received(1).PushLogOutAsync(userNewState.Id, false, null);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_WithExistingV2UpgradeToken_WithNewToken_ReplacesToken_AndDoesNotLogout(\n        RotateUserAccountKeysAndDataRequestModel request)\n    {\n        // Arrange\n        var user = await SetupUserForKeyRotationAsync();\n\n        // Add existing old token to user BEFORE rotation\n        var oldToken = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedType7String2,\n            WrappedUserKey2 = _mockEncryptedType2String2\n        };\n        user.V2UpgradeToken = oldToken.ToJson();\n        await _userRepository.ReplaceAsync(user);\n\n        // Setup request WITH new V2UpgradeToken\n        SetupRotateUserAccountUnlockData(request, user);\n        SetupRotateUserAccountData(request);\n        SetupRotateUserAccountKeys(request, isV2Crypto: false);\n        request.AccountUnlockData.V2UpgradeToken = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedString\n        };\n\n        // Act\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n        response.EnsureSuccessStatusCode();\n\n        // Assert\n        var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userNewState);\n        Assert.NotNull(userNewState.V2UpgradeToken);\n\n        // Verify new token is present\n        Assert.Contains($\"\\\"WrappedUserKey1\\\":\\\"{_mockEncryptedType7String}\\\"\", userNewState.V2UpgradeToken);\n        Assert.Contains($\"\\\"WrappedUserKey2\\\":\\\"{_mockEncryptedString}\\\"\", userNewState.V2UpgradeToken);\n\n        // Verify old token is NOT present\n        Assert.DoesNotContain(oldToken.WrappedUserKey1, userNewState.V2UpgradeToken);\n        Assert.DoesNotContain(oldToken.WrappedUserKey2, userNewState.V2UpgradeToken);\n\n        // Verify NO logout (SecurityStamp should be the same for key rotation with token)\n        Assert.Equal(user.SecurityStamp, userNewState.SecurityStamp);\n        await _pushNotificationService.Received(1)\n            .PushLogOutAsync(userNewState.Id, false, PushNotificationLogOutReason.KeyRotation);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_V2Crypto_WithExistingV2UpgradeToken_WithoutNewToken_ClearsStaleToken_AndLogsOut(\n        RotateUserAccountKeysAndDataRequestModel request)\n    {\n        // Arrange\n        var user = await SetupUserForKeyRotationAsync(_mockEncryptedType7String, createSignatureKeyPair: true);\n\n        // Add existing stale token to V2 crypto user BEFORE rotation\n        var staleToken = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedString\n        };\n        user.V2UpgradeToken = staleToken.ToJson();\n        await _userRepository.ReplaceAsync(user);\n\n        // Setup request WITHOUT V2UpgradeToken\n        SetupRotateUserAccountUnlockData(request, user);\n        SetupRotateUserAccountData(request);\n        SetupRotateUserAccountKeys(request, isV2Crypto: true);\n        request.AccountUnlockData.V2UpgradeToken = null; // Explicit: No new token\n\n        // Act\n        var response = await _client.PostAsJsonAsync(\"/accounts/key-management/rotate-user-account-keys\", request);\n        response.EnsureSuccessStatusCode();\n\n        // Assert\n        var userNewState = await _userRepository.GetByEmailAsync(_ownerEmail);\n        Assert.NotNull(userNewState);\n\n        // Critical: Verify stale token is cleared for V2 crypto users\n        Assert.Null(userNewState.V2UpgradeToken);\n\n        // Verify logout behavior (SecurityStamp should be different)\n        Assert.NotEqual(user.SecurityStamp, userNewState.SecurityStamp);\n        await _pushNotificationService.Received(1).PushLogOutAsync(userNewState.Id, false, null);\n    }\n\n    [Fact]\n    public async Task GetKeyConnectorConfirmationDetailsAsync_Success()\n    {\n        var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited);\n\n        await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,\n            OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted);\n\n        var response = await _client.GetAsync($\"/accounts/key-connector/confirmation-details/{organization.Identifier}\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<KeyConnectorConfirmationDetailsResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(organization.Name, result.OrganizationName);\n    }\n\n    private async Task<(string, Organization)> SetupKeyConnectorTestAsync(OrganizationUserStatusType userStatusType,\n        string organizationSsoIdentifier = \"test-sso-identifier\")\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10,\n            paymentMethod: PaymentMethodType.Card);\n        organization.UseKeyConnector = true;\n        organization.UseSso = true;\n        organization.Identifier = organizationSsoIdentifier;\n        await _organizationRepository.ReplaceAsync(organization);\n\n        var ssoUserEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(ssoUserEmail);\n        await _loginHelper.LoginAsync(ssoUserEmail);\n\n        await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail,\n            OrganizationUserType.User, userStatusType: userStatusType);\n\n        return (ssoUserEmail, organization);\n    }\n\n    private async Task<User> SetupUserForKeyRotationAsync(\n        string? privateKey = null,\n        bool createSignatureKeyPair = false)\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n        var user = await _userRepository.GetByEmailAsync(_ownerEmail);\n        if (user == null)\n        {\n            throw new InvalidOperationException(\"User not found.\");\n        }\n\n        var password = _passwordHasher.HashPassword(user, \"newMasterPassword\");\n        user.MasterPassword = password;\n        user.PublicKey = \"publicKey\";\n        user.PrivateKey = privateKey ?? _mockEncryptedString;\n\n        // If creating signature key pair, user should already have V2 signed state\n        if (createSignatureKeyPair)\n        {\n            user.SignedPublicKey = \"signedPublicKey\";\n            user.SecurityState = \"v2\";\n            user.SecurityVersion = 2;\n        }\n\n        await _userRepository.ReplaceAsync(user);\n\n        if (createSignatureKeyPair)\n        {\n            await _userSignatureKeyPairRepository.CreateAsync(new UserSignatureKeyPair\n            {\n                UserId = user.Id,\n                SignatureAlgorithm = SignatureAlgorithm.Ed25519,\n                SigningKey = _mockEncryptedType7String,\n                VerifyingKey = \"verifyingKey\",\n            });\n        }\n\n        return user;\n    }\n\n    private void SetupRotateUserAccountUnlockData(\n        RotateUserAccountKeysAndDataRequestModel request,\n        User user)\n    {\n        // KDF settings\n        request.AccountUnlockData.MasterPasswordUnlockData.KdfType = user.Kdf;\n        request.AccountUnlockData.MasterPasswordUnlockData.KdfIterations = user.KdfIterations;\n        request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory = user.KdfMemory;\n        request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism = user.KdfParallelism;\n        request.AccountUnlockData.MasterPasswordUnlockData.Email = user.Email;\n        request.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey = _mockEncryptedString;\n\n        // Unlock data arrays\n        request.AccountUnlockData.PasskeyUnlockData = [];\n        request.AccountUnlockData.DeviceKeyUnlockData = [];\n        request.AccountUnlockData.EmergencyAccessUnlockData = [];\n        request.AccountUnlockData.OrganizationAccountRecoveryUnlockData = [];\n\n        // Authentication hash\n        request.OldMasterKeyAuthenticationHash = \"newMasterPassword\";\n    }\n\n    private void SetupRotateUserAccountData(RotateUserAccountKeysAndDataRequestModel request)\n    {\n        request.AccountData.Ciphers =\n        [\n            new CipherWithIdRequestModel\n            {\n                Id = Guid.NewGuid(),\n                Type = CipherType.Login,\n                Name = _mockEncryptedString,\n                Login = new CipherLoginModel\n                {\n                    Username = _mockEncryptedString,\n                    Password = _mockEncryptedString,\n                },\n            },\n        ];\n\n        request.AccountData.Folders =\n        [\n            new FolderWithIdRequestModel\n            {\n                Id = Guid.NewGuid(),\n                Name = _mockEncryptedString,\n            },\n        ];\n\n        request.AccountData.Sends =\n        [\n            new SendWithIdRequestModel\n            {\n                Id = Guid.NewGuid(),\n                Name = _mockEncryptedString,\n                Key = _mockEncryptedString,\n                Disabled = false,\n                DeletionDate = DateTime.UtcNow.AddDays(1),\n            },\n        ];\n    }\n\n    private void SetupRotateUserAccountKeys(\n        RotateUserAccountKeysAndDataRequestModel request,\n        bool isV2Crypto)\n    {\n        request.AccountKeys.AccountPublicKey = \"publicKey\";\n\n        if (isV2Crypto)\n        {\n            // V2 crypto: Type 7 encryption with V2 keys and SecurityState\n            request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String;\n            request.AccountKeys.PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel\n            {\n                PublicKey = \"publicKey\",\n                WrappedPrivateKey = _mockEncryptedType7String,\n                SignedPublicKey = \"signedPublicKey\",\n            };\n            request.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel\n            {\n                SignatureAlgorithm = \"ed25519\",\n                WrappedSigningKey = _mockEncryptedType7String,\n                VerifyingKey = \"verifyingKey\",\n            };\n            request.AccountKeys.SecurityState = new SecurityStateModel\n            {\n                SecurityVersion = 2,\n                SecurityState = \"v2\",\n            };\n        }\n        else\n        {\n            // V1 crypto: Type 2 encryption, no V2 keys\n            request.AccountKeys.UserKeyEncryptedAccountPrivateKey = _mockEncryptedString;\n            request.AccountKeys.PublicKeyEncryptionKeyPair = null;\n            request.AccountKeys.SignatureKeyPair = null;\n            request.AccountKeys.SecurityState = null;\n        }\n\n        request.AccountUnlockData.V2UpgradeToken = null;\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/NotificationCenter/Controllers/NotificationsControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Models.Response;\nusing Bit.Api.NotificationCenter.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Enums;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.NotificationCenter.Controllers;\n\npublic class NotificationsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private static readonly string _mockEncryptedBody =\n        \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n\n    private static readonly string _mockEncryptedTitle =\n        \"2.06CDSJjTZaigYHUuswIq5A==|trxgZl2RCkYrrmCvGE9WNA==|w5p05eI5wsaYeSyWtsAPvBX63vj798kIMxBTfSB0BQg=\";\n\n    private static readonly Random _random = new();\n\n    private static TimeSpan OneMinuteTimeSpan => TimeSpan.FromMinutes(1);\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n    private readonly INotificationRepository _notificationRepository;\n    private readonly INotificationStatusRepository _notificationStatusRepository;\n    private readonly IUserRepository _userRepository;\n    private Organization _organization = null!;\n    private OrganizationUser _organizationUserOwner = null!;\n    private string _ownerEmail = null!;\n    private List<(Notification, NotificationStatus?)> _notificationsWithStatuses = null!;\n\n    public NotificationsControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n        _notificationRepository = _factory.GetService<INotificationRepository>();\n        _notificationStatusRepository = _factory.GetService<INotificationStatusRepository>();\n        _userRepository = _factory.GetService<IUserRepository>();\n    }\n\n    public async Task InitializeAsync()\n    {\n        // Create the owner account\n        _ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n\n        // Create the organization\n        (_organization, _organizationUserOwner) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            plan: PlanType.EnterpriseAnnually, ownerEmail: _ownerEmail, passwordManagerSeats: 10,\n            paymentMethod: PaymentMethodType.Card);\n\n        _notificationsWithStatuses = await CreateNotificationsWithStatusesAsync();\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n\n        foreach (var (notification, _) in _notificationsWithStatuses)\n        {\n            _notificationRepository.DeleteAsync(notification);\n        }\n\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(\"invalid\")]\n    [InlineData(\"-1\")]\n    [InlineData(\"0\")]\n    public async Task ListAsync_RequestValidationContinuationInvalidNumber_BadRequest(string continuationToken)\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var response = await _client.GetAsync($\"/notifications?continuationToken={continuationToken}\");\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();\n        Assert.NotNull(result);\n        Assert.Contains(\"ContinuationToken\", result.ValidationErrors);\n        Assert.Contains(\"Continuation token must be a positive, non zero integer.\",\n            result.ValidationErrors[\"ContinuationToken\"]);\n    }\n\n    [Fact]\n    public async Task ListAsync_RequestValidationContinuationTokenMaxLengthExceeded_BadRequest()\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var response = await _client.GetAsync(\"/notifications?continuationToken=1234567890\");\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();\n        Assert.NotNull(result);\n        Assert.Contains(\"ContinuationToken\", result.ValidationErrors);\n        Assert.Contains(\"The field ContinuationToken must be a string with a maximum length of 9.\",\n            result.ValidationErrors[\"ContinuationToken\"]);\n    }\n\n    [Theory]\n    [InlineData(\"9\")]\n    [InlineData(\"1001\")]\n    public async Task ListAsync_RequestValidationPageSizeInvalidRange_BadRequest(string pageSize)\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var response = await _client.GetAsync($\"/notifications?pageSize={pageSize}\");\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();\n        Assert.NotNull(result);\n        Assert.Contains(\"PageSize\", result.ValidationErrors);\n        Assert.Contains(\"The field PageSize must be between 10 and 1000.\",\n            result.ValidationErrors[\"PageSize\"]);\n    }\n\n    [Fact]\n    public async Task ListAsync_NotLoggedIn_Unauthorized()\n    {\n        var response = await _client.GetAsync(\"/notifications\");\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(null, null, \"2\", 10)]\n    [InlineData(10, null, \"2\", 10)]\n    [InlineData(10, 2, \"3\", 10)]\n    [InlineData(10, 3, null, 4)]\n    [InlineData(24, null, \"2\", 24)]\n    [InlineData(24, 2, null, 0)]\n    [InlineData(1000, null, null, 24)]\n    public async Task ListAsync_PaginationFilter_ReturnsNextPageOfNotificationsCorrectOrder(\n        int? pageSize, int? pageNumber, string? expectedContinuationToken, int expectedCount)\n    {\n        var pageSizeWithDefault = pageSize ?? 10;\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var skip = pageNumber == null ? 0 : (pageNumber.Value - 1) * pageSizeWithDefault;\n\n        var notificationsInOrder = _notificationsWithStatuses.OrderByDescending(e => e.Item1.Priority)\n            .ThenByDescending(e => e.Item1.CreationDate)\n            .Skip(skip)\n            .Take(pageSizeWithDefault)\n            .ToList();\n\n        var url = \"/notifications\";\n        if (pageNumber != null)\n        {\n            url += $\"?continuationToken={pageNumber}\";\n        }\n\n        if (pageSize != null)\n        {\n            url += url.Contains('?') ? \"&\" : \"?\";\n            url += $\"pageSize={pageSize}\";\n        }\n\n        var response = await _client.GetAsync(url);\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<NotificationResponseModel>>();\n        Assert.NotNull(result?.Data);\n        Assert.InRange(result.Data.Count(), 0, pageSizeWithDefault);\n        Assert.Equal(expectedCount, notificationsInOrder.Count);\n        Assert.Equal(notificationsInOrder.Count, result.Data.Count());\n        AssertNotificationResponseModels(result.Data, notificationsInOrder);\n\n        Assert.Equal(expectedContinuationToken, result.ContinuationToken);\n    }\n\n    [Theory]\n    [InlineData(null, null)]\n    [InlineData(null, false)]\n    [InlineData(null, true)]\n    [InlineData(false, null)]\n    [InlineData(true, null)]\n    [InlineData(false, false)]\n    [InlineData(false, true)]\n    [InlineData(true, false)]\n    [InlineData(true, true)]\n    public async Task ListAsync_ReadStatusDeletedStatusFilter_ReturnsFilteredNotificationsCorrectOrder(\n        bool? readStatusFilter, bool? deletedStatusFilter)\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n        var notificationsInOrder = _notificationsWithStatuses.FindAll(e =>\n                (readStatusFilter == null || readStatusFilter == (e.Item2?.ReadDate != null)) &&\n                (deletedStatusFilter == null || deletedStatusFilter == (e.Item2?.DeletedDate != null)))\n            .OrderByDescending(e => e.Item1.Priority)\n            .ThenByDescending(e => e.Item1.CreationDate)\n            .Take(10)\n            .ToList();\n\n        var url = \"/notifications\";\n        if (readStatusFilter != null)\n        {\n            url += $\"?readStatusFilter={readStatusFilter}\";\n        }\n\n        if (deletedStatusFilter != null)\n        {\n            url += url.Contains('?') ? \"&\" : \"?\";\n            url += $\"deletedStatusFilter={deletedStatusFilter}\";\n        }\n\n        var response = await _client.GetAsync(url);\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<NotificationResponseModel>>();\n        Assert.NotNull(result?.Data);\n        Assert.InRange(result.Data.Count(), 0, 10);\n        Assert.Equal(notificationsInOrder.Count, result.Data.Count());\n        AssertNotificationResponseModels(result.Data, notificationsInOrder);\n    }\n\n    [Fact]\n    private async void MarkAsDeletedAsync_NotLoggedIn_Unauthorized()\n    {\n        var url = $\"/notifications/{Guid.NewGuid().ToString()}/delete\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsDeletedAsync_NonExistentNotificationId_NotFound()\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var url = $\"/notifications/{Guid.NewGuid()}/delete\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsDeletedAsync_UserIdNotMatching_NotFound()\n    {\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        var user = (await _userRepository.GetByEmailAsync(email))!;\n        var notifications = await CreateNotificationsAsync(user.Id);\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var url = $\"/notifications/{notifications[0].Id}/delete\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsDeletedAsync_OrganizationIdNotMatchingUserNotPartOfOrganization_NotFound()\n    {\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        var user = (await _userRepository.GetByEmailAsync(email))!;\n        var notifications = await CreateNotificationsAsync(user.Id, _organization.Id);\n\n        await _loginHelper.LoginAsync(email);\n\n        var url = $\"/notifications/{notifications[0].Id}/delete\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsDeletedAsync_OrganizationIdNotMatchingUserPartOfDifferentOrganization_NotFound()\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            plan: PlanType.EnterpriseAnnually, ownerEmail: _ownerEmail, passwordManagerSeats: 10,\n            paymentMethod: PaymentMethodType.Card);\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        var user = (await _userRepository.GetByEmailAsync(email))!;\n        await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, email, OrganizationUserType.User);\n        var notifications = await CreateNotificationsAsync(user.Id, _organization.Id);\n\n        await _loginHelper.LoginAsync(email);\n\n        var url = $\"/notifications/{notifications[0].Id}/delete\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsDeletedAsync_NotificationStatusNotExisting_Created()\n    {\n        var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId);\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var url = $\"/notifications/{notifications[0].Id}/delete\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(\n            notifications[0].Id, _organizationUserOwner.UserId!.Value);\n        Assert.NotNull(notificationStatus);\n        Assert.NotNull(notificationStatus.DeletedDate);\n        Assert.Equal(DateTime.UtcNow, notificationStatus.DeletedDate.Value, OneMinuteTimeSpan);\n        Assert.Null(notificationStatus.ReadDate);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    private async void MarkAsDeletedAsync_NotificationStatusExisting_Updated(bool deletedDateNull)\n    {\n        var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId);\n        await _notificationStatusRepository.CreateAsync(new NotificationStatus\n        {\n            NotificationId = notifications[0].Id,\n            UserId = _organizationUserOwner.UserId!.Value,\n            ReadDate = null,\n            DeletedDate = deletedDateNull ? null : DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600))\n        });\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var url = $\"/notifications/{notifications[0].Id}/delete\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(\n            notifications[0].Id, _organizationUserOwner.UserId!.Value);\n        Assert.NotNull(notificationStatus);\n        Assert.NotNull(notificationStatus.DeletedDate);\n        Assert.Equal(DateTime.UtcNow, notificationStatus.DeletedDate.Value, OneMinuteTimeSpan);\n        Assert.Null(notificationStatus.ReadDate);\n    }\n\n    [Fact]\n    private async void MarkAsReadAsync_NotLoggedIn_Unauthorized()\n    {\n        var url = $\"/notifications/{Guid.NewGuid().ToString()}/read\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsReadAsync_NonExistentNotificationId_NotFound()\n    {\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var url = $\"/notifications/{Guid.NewGuid()}/read\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsReadAsync_UserIdNotMatching_NotFound()\n    {\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        var user = (await _userRepository.GetByEmailAsync(email))!;\n        var notifications = await CreateNotificationsAsync(user.Id);\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var url = $\"/notifications/{notifications[0].Id}/read\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsReadAsync_OrganizationIdNotMatchingUserNotPartOfOrganization_NotFound()\n    {\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        var user = (await _userRepository.GetByEmailAsync(email))!;\n        var notifications = await CreateNotificationsAsync(user.Id, _organization.Id);\n\n        await _loginHelper.LoginAsync(email);\n\n        var url = $\"/notifications/{notifications[0].Id}/read\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsReadAsync_OrganizationIdNotMatchingUserPartOfDifferentOrganization_NotFound()\n    {\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,\n            plan: PlanType.EnterpriseAnnually, ownerEmail: _ownerEmail, passwordManagerSeats: 10,\n            paymentMethod: PaymentMethodType.Card);\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        var user = (await _userRepository.GetByEmailAsync(email))!;\n        await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, email, OrganizationUserType.User);\n        var notifications = await CreateNotificationsAsync(user.Id, _organization.Id);\n\n        await _loginHelper.LoginAsync(email);\n\n        var url = $\"/notifications/{notifications[0].Id}/read\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    private async void MarkAsReadAsync_NotificationStatusNotExisting_Created()\n    {\n        var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId);\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var url = $\"/notifications/{notifications[0].Id}/read\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(\n            notifications[0].Id, _organizationUserOwner.UserId!.Value);\n        Assert.NotNull(notificationStatus);\n        Assert.NotNull(notificationStatus.ReadDate);\n        Assert.Equal(DateTime.UtcNow, notificationStatus.ReadDate.Value, OneMinuteTimeSpan);\n        Assert.Null(notificationStatus.DeletedDate);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    private async void MarkAsReadAsync_NotificationStatusExisting_Updated(bool readDateNull)\n    {\n        var notifications = await CreateNotificationsAsync(_organizationUserOwner.UserId);\n        await _notificationStatusRepository.CreateAsync(new NotificationStatus\n        {\n            NotificationId = notifications[0].Id,\n            UserId = _organizationUserOwner.UserId!.Value,\n            ReadDate = readDateNull ? null : DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)),\n            DeletedDate = null\n        });\n\n        await _loginHelper.LoginAsync(_ownerEmail);\n\n        var url = $\"/notifications/{notifications[0].Id}/read\";\n        var response = await _client.PatchAsync(url, new StringContent(\"\"));\n        Assert.Equal(HttpStatusCode.OK, response.StatusCode);\n\n        var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(\n            notifications[0].Id, _organizationUserOwner.UserId!.Value);\n        Assert.NotNull(notificationStatus);\n        Assert.NotNull(notificationStatus.ReadDate);\n        Assert.Equal(DateTime.UtcNow, notificationStatus.ReadDate.Value, OneMinuteTimeSpan);\n        Assert.Null(notificationStatus.DeletedDate);\n    }\n\n    private static void AssertNotificationResponseModels(\n        IEnumerable<NotificationResponseModel> notificationResponseModels,\n        List<(Notification, NotificationStatus?)> expectedNotificationsWithStatuses)\n    {\n        var i = 0;\n        foreach (var notificationResponseModel in notificationResponseModels)\n        {\n            Assert.Contains(expectedNotificationsWithStatuses, e => e.Item1.Id == notificationResponseModel.Id);\n            var (expectedNotification, expectedNotificationStatus) = expectedNotificationsWithStatuses[i];\n            Assert.NotNull(expectedNotification);\n            Assert.Equal(expectedNotification.Priority, notificationResponseModel.Priority);\n            Assert.Equal(expectedNotification.Title, notificationResponseModel.Title);\n            Assert.Equal(expectedNotification.Body, notificationResponseModel.Body);\n            Assert.Equal(expectedNotification.RevisionDate, notificationResponseModel.Date);\n            if (expectedNotificationStatus != null)\n            {\n                Assert.Equal(expectedNotificationStatus.ReadDate, notificationResponseModel.ReadDate);\n                Assert.Equal(expectedNotificationStatus.DeletedDate, notificationResponseModel.DeletedDate);\n            }\n            else\n            {\n                Assert.Null(notificationResponseModel.ReadDate);\n                Assert.Null(notificationResponseModel.DeletedDate);\n            }\n\n            Assert.Equal(\"notification\", notificationResponseModel.Object);\n            i++;\n        }\n    }\n\n    private async Task<List<(Notification, NotificationStatus?)>> CreateNotificationsWithStatusesAsync()\n    {\n        var userId = (Guid)_organizationUserOwner.UserId!;\n\n        var globalNotifications = await CreateNotificationsAsync();\n        var userWithoutOrganizationNotifications = await CreateNotificationsAsync(userId: userId);\n        var organizationWithoutUserNotifications = await CreateNotificationsAsync(organizationId: _organization.Id);\n        var userPartOrOrganizationNotifications = await CreateNotificationsAsync(userId: userId,\n            organizationId: _organization.Id);\n\n        var globalNotificationWithStatuses = await CreateNotificationStatusesAsync(globalNotifications, userId);\n        var userWithoutOrganizationNotificationWithStatuses =\n            await CreateNotificationStatusesAsync(userWithoutOrganizationNotifications, userId);\n        var organizationWithoutUserNotificationWithStatuses =\n            await CreateNotificationStatusesAsync(organizationWithoutUserNotifications, userId);\n        var userPartOrOrganizationNotificationWithStatuses =\n            await CreateNotificationStatusesAsync(userPartOrOrganizationNotifications, userId);\n\n        return new List<List<(Notification, NotificationStatus?)>>\n            {\n                globalNotificationWithStatuses,\n                userWithoutOrganizationNotificationWithStatuses,\n                organizationWithoutUserNotificationWithStatuses,\n                userPartOrOrganizationNotificationWithStatuses\n            }\n            .SelectMany(n => n)\n            .Where(n => n.Item1.ClientType is ClientType.All or ClientType.Web)\n            .ToList();\n    }\n\n    private async Task<List<Notification>> CreateNotificationsAsync(Guid? userId = null, Guid? organizationId = null,\n        int numberToCreate = 3)\n    {\n        var priorities = Enum.GetValues<Priority>();\n        var clientTypes = Enum.GetValues<ClientType>();\n\n        var notifications = new List<Notification>();\n\n        foreach (var clientType in clientTypes)\n        {\n            for (var i = 0; i < numberToCreate; i++)\n            {\n                var notification = new Notification\n                {\n                    Global = userId == null && organizationId == null,\n                    UserId = userId,\n                    OrganizationId = organizationId,\n                    Title = _mockEncryptedTitle,\n                    Body = _mockEncryptedBody,\n                    Priority = (Priority)priorities.GetValue(_random.Next(priorities.Length))!,\n                    ClientType = clientType,\n                    CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)),\n                    RevisionDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600))\n                };\n\n                notification = await _notificationRepository.CreateAsync(notification);\n\n                notifications.Add(notification);\n            }\n        }\n\n        return notifications;\n    }\n\n    private async Task<List<(Notification, NotificationStatus?)>> CreateNotificationStatusesAsync(\n        List<Notification> notifications, Guid userId)\n    {\n        var readDateNotificationStatus = await _notificationStatusRepository.CreateAsync(new NotificationStatus\n        {\n            NotificationId = notifications[0].Id,\n            UserId = userId,\n            ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)),\n            DeletedDate = null\n        });\n\n        var deletedDateNotificationStatus = await _notificationStatusRepository.CreateAsync(new NotificationStatus\n        {\n            NotificationId = notifications[1].Id,\n            UserId = userId,\n            ReadDate = null,\n            DeletedDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600))\n        });\n\n        var readDateAndDeletedDateNotificationStatus = await _notificationStatusRepository.CreateAsync(\n            new NotificationStatus\n            {\n                NotificationId = notifications[2].Id,\n                UserId = userId,\n                ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600)),\n                DeletedDate = DateTime.UtcNow - TimeSpan.FromMinutes(_random.Next(3600))\n            });\n\n        List<NotificationStatus> statuses =\n            [readDateNotificationStatus, deletedDateNotificationStatus, readDateAndDeletedDateNotificationStatus];\n\n        return notifications.Select(n => (n, statuses.Find(s => s.NotificationId == n.Id))).ToList();\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs",
    "content": "﻿using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing Azure.Storage.Queues;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Platform.Installations;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Repositories;\nusing NSubstitute;\nusing Xunit;\nusing static Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Api.IntegrationTest.Platform.Controllers;\n\npublic class PushControllerTests\n{\n    private static readonly Guid _userId = Guid.NewGuid();\n    private static readonly Guid _organizationId = Guid.NewGuid();\n    private static readonly Guid _deviceId = Guid.NewGuid();\n\n    public static IEnumerable<object[]> SendData()\n    {\n        static object[] Typed<T>(PushSendRequestModel<T> pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall = true)\n        {\n            return [pushSendRequestModel, expectedHubTagExpression, expectHubCall];\n        }\n\n        static object[] UserTyped(PushType pushType)\n        {\n            return Typed(new PushSendRequestModel<UserPushNotification>\n            {\n                Type = pushType,\n                UserId = _userId,\n                DeviceId = _deviceId,\n                Payload = new UserPushNotification\n                {\n                    Date = DateTime.UtcNow,\n                    UserId = _userId,\n                },\n            }, $\"(template:payload_userId:%installation%_{_userId})\");\n        }\n\n        // User cipher\n        yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>\n        {\n            Type = PushType.SyncCipherUpdate,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            Payload = new SyncCipherPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        // Organization cipher, an org cipher would not naturally be synced from our \n        // code but it is technically possible to be submitted to the endpoint.\n        yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>\n        {\n            Type = PushType.SyncCipherUpdate,\n            OrganizationId = _organizationId,\n            DeviceId = _deviceId,\n            Payload = new SyncCipherPushNotification\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = _organizationId,\n            },\n        }, $\"(template:payload && organizationId:%installation%_{_organizationId})\");\n\n        yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>\n        {\n            Type = PushType.SyncCipherCreate,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            Payload = new SyncCipherPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        // Organization cipher, an org cipher would not naturally be synced from our \n        // code but it is technically possible to be submitted to the endpoint.\n        yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>\n        {\n            Type = PushType.SyncCipherCreate,\n            OrganizationId = _organizationId,\n            DeviceId = _deviceId,\n            Payload = new SyncCipherPushNotification\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = _organizationId,\n            },\n        }, $\"(template:payload && organizationId:%installation%_{_organizationId})\");\n\n        yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>\n        {\n            Type = PushType.SyncCipherDelete,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            Payload = new SyncCipherPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        // Organization cipher, an org cipher would not naturally be synced from our \n        // code but it is technically possible to be submitted to the endpoint.\n        yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>\n        {\n            Type = PushType.SyncCipherDelete,\n            OrganizationId = _organizationId,\n            DeviceId = _deviceId,\n            Payload = new SyncCipherPushNotification\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = _organizationId,\n            },\n        }, $\"(template:payload && organizationId:%installation%_{_organizationId})\");\n\n        yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>\n        {\n            Type = PushType.SyncFolderDelete,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            Payload = new SyncFolderPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>\n        {\n            Type = PushType.SyncFolderCreate,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            Payload = new SyncFolderPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>\n        {\n            Type = PushType.SyncFolderCreate,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            Payload = new SyncFolderPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        yield return UserTyped(PushType.SyncCiphers);\n        yield return UserTyped(PushType.SyncVault);\n        yield return UserTyped(PushType.SyncOrganizations);\n        yield return UserTyped(PushType.SyncOrgKeys);\n        yield return UserTyped(PushType.SyncSettings);\n        yield return UserTyped(PushType.LogOut);\n        yield return UserTyped(PushType.RefreshSecurityTasks);\n\n        yield return Typed(new PushSendRequestModel<AuthRequestPushNotification>\n        {\n            Type = PushType.AuthRequest,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            Payload = new AuthRequestPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        yield return Typed(new PushSendRequestModel<AuthRequestPushNotification>\n        {\n            Type = PushType.AuthRequestResponse,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            Payload = new AuthRequestPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        yield return Typed(new PushSendRequestModel<NotificationPushNotification>\n        {\n            Type = PushType.Notification,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            Payload = new NotificationPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        yield return Typed(new PushSendRequestModel<NotificationPushNotification>\n        {\n            Type = PushType.Notification,\n            UserId = _userId,\n            DeviceId = _deviceId,\n            ClientType = ClientType.All,\n            Payload = new NotificationPushNotification\n            {\n                Id = Guid.NewGuid(),\n                Global = true,\n            },\n        }, $\"(template:payload_userId:%installation%_{_userId})\");\n\n        yield return Typed(new PushSendRequestModel<NotificationPushNotification>\n        {\n            Type = PushType.NotificationStatus,\n            OrganizationId = _organizationId,\n            DeviceId = _deviceId,\n            Payload = new NotificationPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload && organizationId:%installation%_{_organizationId})\");\n\n        yield return Typed(new PushSendRequestModel<NotificationPushNotification>\n        {\n            Type = PushType.NotificationStatus,\n            OrganizationId = _organizationId,\n            DeviceId = _deviceId,\n            Payload = new NotificationPushNotification\n            {\n                Id = Guid.NewGuid(),\n                UserId = _userId,\n            },\n        }, $\"(template:payload && organizationId:%installation%_{_organizationId})\");\n    }\n\n    [Theory]\n    [MemberData(nameof(SendData))]\n    public async Task Send_Works<T>(PushSendRequestModel<T> pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall)\n    {\n        var (apiFactory, httpClient, installation, queueClient, notificationHubProxy) = await SetupTest();\n\n        // Act\n        var pushSendResponse = await httpClient.PostAsJsonAsync(\"push/send\", pushSendRequestModel);\n\n        // Assert \n        pushSendResponse.EnsureSuccessStatusCode();\n\n        // Relayed notifications, the ones coming to this endpoint should\n        // not make their way into our Azure Queue and instead should only be sent to Azure Notifications\n        // hub.\n        await queueClient\n            .Received(0)\n            .SendMessageAsync(Arg.Any<string>());\n\n        // Check that this notification was sent through hubs the expected number of times\n        await notificationHubProxy\n            .Received(expectHubCall ? 1 : 0)\n            .SendTemplateNotificationAsync(\n                Arg.Any<Dictionary<string, string>>(),\n                Arg.Is(expectedHubTagExpression.Replace(\"%installation%\", installation.Id.ToString()))\n            );\n\n        // TODO: Expect on the dictionary more?\n\n        // Notifications being relayed from SH should have the device id\n        // tracked so that we can later send the notification to that device.\n        await apiFactory.GetService<IInstallationDeviceRepository>()\n            .Received(1)\n            .UpsertAsync(Arg.Is<InstallationDeviceEntity>(\n                ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == pushSendRequestModel.DeviceId.ToString()\n            ));\n    }\n\n    [Fact]\n    public async Task Send_InstallationNotification_NotAuthenticatedInstallation_Fails()\n    {\n        var (_, httpClient, _, _, _) = await SetupTest();\n\n        var response = await httpClient.PostAsJsonAsync(\"push/send\", new PushSendRequestModel<object>\n        {\n            Type = PushType.NotificationStatus,\n            InstallationId = Guid.NewGuid(),\n            Payload = new { }\n        });\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var body = await response.Content.ReadFromJsonAsync<JsonNode>();\n        Assert.Equal(JsonValueKind.Object, body.GetValueKind());\n        Assert.True(body.AsObject().TryGetPropertyValue(\"message\", out var message));\n        Assert.Equal(JsonValueKind.String, message.GetValueKind());\n        Assert.Equal(\"InstallationId does not match current context.\", message.GetValue<string>());\n    }\n\n    [Fact]\n    public async Task Send_InstallationNotification_Works()\n    {\n        var (apiFactory, httpClient, installation, _, notificationHubProxy) = await SetupTest();\n\n        var deviceId = Guid.NewGuid();\n\n        var response = await httpClient.PostAsJsonAsync(\"push/send\", new PushSendRequestModel<object>\n        {\n            Type = PushType.NotificationStatus,\n            InstallationId = installation.Id,\n            Payload = new { },\n            DeviceId = deviceId,\n            ClientType = ClientType.Web,\n        });\n\n        response.EnsureSuccessStatusCode();\n\n        await notificationHubProxy\n            .Received(1)\n            .SendTemplateNotificationAsync(\n                Arg.Any<Dictionary<string, string>>(),\n                Arg.Is($\"(template:payload && installationId:{installation.Id} && clientType:Web)\")\n            );\n\n        await apiFactory.GetService<IInstallationDeviceRepository>()\n            .Received(1)\n            .UpsertAsync(Arg.Is<InstallationDeviceEntity>(\n                ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == deviceId.ToString()\n            ));\n    }\n\n    [Fact]\n    public async Task Send_NoOrganizationNoInstallationNoUser_FailsModelValidation()\n    {\n        var (_, client, _, _, _) = await SetupTest();\n\n        var response = await client.PostAsJsonAsync(\"push/send\", new PushSendRequestModel<object>\n        {\n            Type = PushType.AuthRequest,\n            Payload = new { },\n        });\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n        var body = await response.Content.ReadFromJsonAsync<JsonNode>();\n        Assert.Equal(JsonValueKind.Object, body.GetValueKind());\n        Assert.True(body.AsObject().TryGetPropertyValue(\"message\", out var message));\n        Assert.Equal(JsonValueKind.String, message.GetValueKind());\n        Assert.Equal(\"The model state is invalid.\", message.GetValue<string>());\n    }\n\n    private static async Task<(ApiApplicationFactory Factory, HttpClient AuthedClient, Installation Installation, QueueClient MockedQueue, INotificationHubProxy MockedHub)> SetupTest()\n    {\n        // Arrange\n        var apiFactory = new ApiApplicationFactory();\n\n        var queueClient = Substitute.For<QueueClient>();\n\n        // Substitute the underlying queue messages will go to.\n        apiFactory.ConfigureServices(services =>\n        {\n            var queueClientService = services.FirstOrDefault(\n                sd => sd.ServiceKey == (object)\"notifications\"\n                    && sd.ServiceType == typeof(QueueClient)\n            ) ?? throw new InvalidOperationException(\"Expected service was not found.\");\n\n            services.Remove(queueClientService);\n\n            services.AddKeyedSingleton(\"notifications\", queueClient);\n        });\n\n        var notificationHubProxy = Substitute.For<INotificationHubProxy>();\n\n        apiFactory.SubstituteService<INotificationHubPool>(s =>\n        {\n            s.AllClients\n                .Returns(notificationHubProxy);\n        });\n\n        apiFactory.SubstituteService<IInstallationDeviceRepository>(s => { });\n\n        // Setup as cloud with NotificationHub setup and Azure Queue\n        apiFactory.UpdateConfiguration(\"GlobalSettings:Notifications:ConnectionString\", \"any_value\");\n\n        // Configure hubs\n        var index = 0;\n        void AddHub(NotificationHubSettings notificationHubSettings)\n        {\n            apiFactory.UpdateConfiguration(\n                $\"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:ConnectionString\",\n                notificationHubSettings.ConnectionString\n            );\n            apiFactory.UpdateConfiguration(\n                $\"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:HubName\",\n                notificationHubSettings.HubName\n            );\n            apiFactory.UpdateConfiguration(\n                $\"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationStartDate\",\n                notificationHubSettings.RegistrationStartDate?.ToString()\n            );\n            apiFactory.UpdateConfiguration(\n                $\"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationEndDate\",\n                notificationHubSettings.RegistrationEndDate?.ToString()\n            );\n            index++;\n        }\n\n        AddHub(new NotificationHubSettings\n        {\n            ConnectionString = \"some_value\",\n            RegistrationStartDate = DateTime.UtcNow.AddDays(-2),\n        });\n\n        var httpClient = apiFactory.CreateClient();\n\n        // Add installation into database\n        var installationRepository = apiFactory.GetService<IInstallationRepository>();\n        var installation = await installationRepository.CreateAsync(new Installation\n        {\n            Key = \"my_test_key\",\n            Email = \"test@example.com\",\n            Enabled = true,\n        });\n\n        var identityClient = apiFactory.Identity.CreateDefaultClient();\n\n        var connectTokenResponse = await identityClient.PostAsync(\"connect/token\", new FormUrlEncodedContent(new Dictionary<string, string>\n        {\n            { \"grant_type\", \"client_credentials\" },\n            { \"scope\", \"api.push\" },\n            { \"client_id\", $\"installation.{installation.Id}\" },\n            { \"client_secret\", installation.Key },\n        }));\n\n        connectTokenResponse.EnsureSuccessStatusCode();\n\n        var connectTokenResponseModel = await connectTokenResponse.Content.ReadFromJsonAsync<JsonNode>();\n\n        // Setup authentication\n        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\n            connectTokenResponseModel[\"token_type\"].GetValue<string>(),\n            connectTokenResponseModel[\"access_token\"].GetValue<string>()\n        );\n\n        return (apiFactory, httpClient, installation, queueClient, notificationHubProxy);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Properties/launchSettings.json",
    "content": "{\n  \"iisSettings\": {\n    \"windowsAuthentication\": false,\n    \"anonymousAuthentication\": true\n  },\n  \"profiles\": {\n    \"Bit.Api.IntegrationTest\": {\n      \"commandName\": \"Project\",\n      \"launchBrowser\": true,\n      \"applicationUrl\": \"http://localhost:33506\",\n      \"environmentVariables\": {\n        \"ASPNETCORE_ENVIRONMENT\": \"Development\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.SecretsManager.Enums;\nusing Bit.Api.IntegrationTest.SecretsManager.Helpers;\nusing Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Controllers;\n\npublic class AccessPoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private const string _mockEncryptedString =\n        \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly IGroupRepository _groupRepository;\n    private readonly LoginHelper _loginHelper;\n    private readonly IProjectRepository _projectRepository;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private string _email = null!;\n    private SecretsManagerOrganizationHelper _organizationHelper = null!;\n\n    public AccessPoliciesControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();\n        _serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();\n        _secretRepository = _factory.GetService<ISecretRepository>();\n        _projectRepository = _factory.GetService<IProjectRepository>();\n        _groupRepository = _factory.GetService<IGroupRepository>();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_email);\n        _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetPeoplePotentialGrantees_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response =\n            await _client.GetAsync(\n                $\"/organizations/{org.Id}/access-policies/people/potential-grantees\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetPeoplePotentialGrantees_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n        }\n\n        var response =\n            await _client.GetAsync(\n                $\"/organizations/{org.Id}/access-policies/people/potential-grantees\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<PotentialGranteeResponseModel>>();\n\n        Assert.NotNull(result?.Data);\n        Assert.NotEmpty(result.Data);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetServiceAccountPotentialGrantees_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response =\n            await _client.GetAsync(\n                $\"/organizations/{org.Id}/access-policies/service-accounts/potential-grantees\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetServiceAccountPotentialGrantees_OnlyReturnsServiceAccountsWithWriteAccess()\n    {\n        // Create a new account as a user\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n\n        var response =\n            await _client.GetAsync(\n                $\"/organizations/{org.Id}/access-policies/service-accounts/potential-grantees\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<PotentialGranteeResponseModel>>();\n\n        Assert.NotNull(result?.Data);\n        Assert.Empty(result.Data);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetServiceAccountsPotentialGrantees_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            await _accessPolicyRepository.CreateManyAsync(\n            [\n                new UserServiceAccountAccessPolicy\n                {\n                    GrantedServiceAccountId = serviceAccount.Id,\n                    OrganizationUserId = orgUser.Id,\n                    Read = true,\n                    Write = true\n                }\n            ]);\n        }\n\n        var response =\n            await _client.GetAsync(\n                $\"/organizations/{org.Id}/access-policies/service-accounts/potential-grantees\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<PotentialGranteeResponseModel>>();\n\n        Assert.NotNull(result?.Data);\n        Assert.NotEmpty(result.Data);\n        Assert.Equal(serviceAccount.Id, result.Data.First(x => x.Id == serviceAccount.Id).Id);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetProjectPotentialGrantees_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response =\n            await _client.GetAsync(\n                $\"/organizations/{org.Id}/access-policies/projects/potential-grantees\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetProjectPotentialGrantees_OnlyReturnsProjectsWithWriteAccess()\n    {\n        // Create a new account as a user\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = _mockEncryptedString });\n\n\n        var response =\n            await _client.GetAsync(\n                $\"/organizations/{org.Id}/access-policies/projects/potential-grantees\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<PotentialGranteeResponseModel>>();\n\n        Assert.NotNull(result?.Data);\n        Assert.Empty(result.Data);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetProjectPotentialGrantees_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            await _accessPolicyRepository.CreateManyAsync(\n            [\n                new UserProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id,\n                    OrganizationUserId = orgUser.Id,\n                    Read = true,\n                    Write = true\n                }\n            ]);\n        }\n\n        var response =\n            await _client.GetAsync(\n                $\"/organizations/{org.Id}/access-policies/projects/potential-grantees\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<PotentialGranteeResponseModel>>();\n\n        Assert.NotNull(result?.Data);\n        Assert.NotEmpty(result.Data);\n        Assert.Equal(project.Id, result.Data.First(x => x.Id == project.Id).Id);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetProjectPeopleAccessPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}/access-policies/people\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetProjectPeopleAccessPolicies_ReturnsEmpty()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}/access-policies/people\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ProjectPeopleAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Empty(result.UserAccessPolicies);\n        Assert.Empty(result.GroupAccessPolicies);\n    }\n\n    [Fact]\n    public async Task GetProjectPeopleAccessPolicies_NoPermission_NotFound()\n    {\n        await _organizationHelper.Initialize(true, true, true);\n        var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = orgUser.OrganizationId,\n            Name = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}/access-policies/people\");\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetProjectPeopleAccessPolicies_Success(PermissionType permissionType)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (project, _) = await SetupProjectPeoplePermissionAsync(permissionType, organizationUser);\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}/access-policies/people\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ProjectPeopleAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result?.UserAccessPolicies);\n        Assert.Single(result.UserAccessPolicies);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task PutProjectPeopleAccessPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (_, organizationUser) =\n            await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var (project, request) = await SetupProjectPeopleRequestAsync(PermissionType.RunAsAdmin, organizationUser);\n\n        var response = await _client.PutAsJsonAsync($\"/projects/{project.Id}/access-policies/people\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PutProjectPeopleAccessPolicies_NoPermission()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, organizationUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var request = new PeopleAccessPoliciesRequestModel\n        {\n            UserAccessPolicyRequests = new List<AccessPolicyRequest>\n            {\n                new() { GranteeId = organizationUser.Id, Read = true, Write = true }\n            }\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/projects/{project.Id}/access-policies/people\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task PutProjectPeopleAccessPolicies_MismatchedOrgIds_NotFound(PermissionType permissionType)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (project, request) = await SetupProjectPeopleRequestAsync(permissionType, organizationUser);\n        var newOrg = await _organizationHelper.CreateSmOrganizationAsync();\n        var group = await _groupRepository.CreateAsync(new Group\n        {\n            OrganizationId = newOrg.Id,\n            Name = _mockEncryptedString\n        });\n        request.GroupAccessPolicyRequests = new List<AccessPolicyRequest>\n        {\n            new() { GranteeId = group.Id, Read = true, Write = true }\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/projects/{project.Id}/access-policies/people\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task PutProjectPeopleAccessPolicies_Success(PermissionType permissionType)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (project, request) = await SetupProjectPeopleRequestAsync(permissionType, organizationUser);\n\n        var response = await _client.PutAsJsonAsync($\"/projects/{project.Id}/access-policies/people\", request);\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ProjectPeopleAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.UserAccessPolicyRequests.First().GranteeId,\n            result.UserAccessPolicies.First().OrganizationUserId);\n        Assert.True(result.UserAccessPolicies.First().Read);\n        Assert.True(result.UserAccessPolicies.First().Write);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetServiceAccountPeopleAccessPolicies_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}/access-policies/people\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetServiceAccountPeopleAccessPolicies_ReturnsEmpty()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}/access-policies/people\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ServiceAccountPeopleAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Empty(result.UserAccessPolicies);\n        Assert.Empty(result.GroupAccessPolicies);\n    }\n\n    [Fact]\n    public async Task GetServiceAccountPeopleAccessPolicies_NoPermission()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}/access-policies/people\");\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetServiceAccountPeopleAccessPolicies_Success(PermissionType permissionType)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (serviceAccount, _) = await SetupServiceAccountPeoplePermissionAsync(permissionType, organizationUser);\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}/access-policies/people\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ServiceAccountPeopleAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result?.UserAccessPolicies);\n        Assert.Single(result.UserAccessPolicies);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task PutServiceAccountPeopleAccessPolicies_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (_, organizationUser) =\n            await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var (serviceAccount, request) =\n            await SetupServiceAccountPeopleRequestAsync(PermissionType.RunAsAdmin, organizationUser);\n\n        var response =\n            await _client.PutAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-policies/people\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PutServiceAccountPeopleAccessPolicies_NoPermission()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, organizationUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n\n        var request = new PeopleAccessPoliciesRequestModel\n        {\n            UserAccessPolicyRequests = new List<AccessPolicyRequest>\n            {\n                new() { GranteeId = organizationUser.Id, Read = true, Write = true }\n            }\n        };\n\n        var response =\n            await _client.PutAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-policies/people\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task PutServiceAccountPeopleAccessPolicies_MismatchedOrgIds_NotFound(PermissionType permissionType)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (serviceAccount, request) = await SetupServiceAccountPeopleRequestAsync(permissionType, organizationUser);\n        var newOrg = await _organizationHelper.CreateSmOrganizationAsync();\n        var group = await _groupRepository.CreateAsync(new Group\n        {\n            OrganizationId = newOrg.Id,\n            Name = _mockEncryptedString\n        });\n        request.GroupAccessPolicyRequests = new List<AccessPolicyRequest>\n        {\n            new() { GranteeId = group.Id, Read = true, Write = true }\n        };\n\n        var response =\n            await _client.PutAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-policies/people\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task PutServiceAccountPeopleAccessPolicies_Success(PermissionType permissionType)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (serviceAccount, request) = await SetupServiceAccountPeopleRequestAsync(permissionType, organizationUser);\n\n        var response =\n            await _client.PutAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-policies/people\", request);\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ServiceAccountPeopleAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.UserAccessPolicyRequests.First().GranteeId,\n            result.UserAccessPolicies.First().OrganizationUserId);\n        Assert.True(result.UserAccessPolicies.First().Read);\n        Assert.True(result.UserAccessPolicies.First().Write);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetServiceAccountGrantedPoliciesAsync_SmAccessDenied_ReturnsNotFound(bool useSecrets,\n        bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n        var initData = await CreateServiceAccountProjectAccessPolicyAsync(org.Id);\n\n        var response = await _client.GetAsync($\"/service-accounts/{initData.ServiceAccountId}/granted-policies\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetServiceAccountGrantedPoliciesAsync_NoAccessPolicies_ReturnsEmpty()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}/granted-policies\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content\n            .ReadFromJsonAsync<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Empty(result.GrantedProjectPolicies);\n    }\n\n    [Fact]\n    public async Task GetServiceAccountGrantedPoliciesAsync_UserDoesntHavePermission_ReturnsNotFound()\n    {\n        // Create a new account as a user\n        await _organizationHelper.Initialize(true, true, true);\n        var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var initData = await CreateServiceAccountProjectAccessPolicyAsync(orgUser.OrganizationId);\n\n        var response = await _client.GetAsync($\"/service-accounts/{initData.ServiceAccountId}/granted-policies\");\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetServiceAccountGrantedPoliciesAsync_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n        var initData = await CreateServiceAccountProjectAccessPolicyAsync(org.Id);\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserServiceAccountAccessPolicy\n                {\n                    GrantedServiceAccountId = initData.ServiceAccountId,\n                    OrganizationUserId = orgUser.Id,\n                    Read = true,\n                    Write = true\n                }\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        var response = await _client.GetAsync($\"/service-accounts/{initData.ServiceAccountId}/granted-policies\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content\n            .ReadFromJsonAsync<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.GrantedProjectPolicies);\n        Assert.NotNull(result.GrantedProjectPolicies.First().AccessPolicy.GrantedProjectName);\n        Assert.NotNull(result.GrantedProjectPolicies.First().AccessPolicy.GrantedProjectId);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task PutServiceAccountGrantedPoliciesAsync_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (_, organizationUser) =\n            await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var (serviceAccount, request) =\n            await SetupServiceAccountGrantedPoliciesRequestAsync(PermissionType.RunAsAdmin, organizationUser, false);\n\n        var response = await _client.PutAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/granted-policies\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PutServiceAccountGrantedPoliciesAsync_UserHasNoPermission_ReturnsNotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var (projectId, serviceAccountId) = await CreateProjectAndServiceAccountAsync(org.Id);\n\n        var request = new ServiceAccountGrantedPoliciesRequestModel\n        {\n            ProjectGrantedPolicyRequests = new List<GrantedAccessPolicyRequest>\n            {\n                new() { GrantedId = projectId, Read = true, Write = true }\n            }\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/service-accounts/{serviceAccountId}/granted-policies\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task PutServiceAccountGrantedPoliciesAsync_MismatchedOrgIds_ReturnsNotFound(\n        PermissionType permissionType)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (serviceAccount, request) =\n            await SetupServiceAccountGrantedPoliciesRequestAsync(permissionType, organizationUser, false);\n        var newOrg = await _organizationHelper.CreateSmOrganizationAsync();\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            Name = _mockEncryptedString,\n            OrganizationId = newOrg.Id\n        });\n        request.ProjectGrantedPolicyRequests = new List<GrantedAccessPolicyRequest>\n        {\n            new() { GrantedId = project.Id, Read = true, Write = true }\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/granted-policies\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin, false)]\n    [InlineData(PermissionType.RunAsAdmin, true)]\n    [InlineData(PermissionType.RunAsUserWithPermission, false)]\n    [InlineData(PermissionType.RunAsUserWithPermission, true)]\n    public async Task PutServiceAccountGrantedPoliciesAsync_Success(PermissionType permissionType,\n        bool createPreviousAccessPolicy)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (serviceAccount, request) =\n            await SetupServiceAccountGrantedPoliciesRequestAsync(permissionType, organizationUser,\n                createPreviousAccessPolicy);\n\n        var response = await _client.PutAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/granted-policies\", request);\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content\n            .ReadFromJsonAsync<ServiceAccountGrantedPoliciesPermissionDetailsResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.ProjectGrantedPolicyRequests.First().GrantedId,\n            result.GrantedProjectPolicies.First().AccessPolicy.GrantedProjectId);\n        Assert.True(result.GrantedProjectPolicies.First().AccessPolicy.Read);\n        Assert.True(result.GrantedProjectPolicies.First().AccessPolicy.Write);\n        Assert.True(result.GrantedProjectPolicies.First().HasPermission);\n        Assert.Single(result.GrantedProjectPolicies);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_SmAccessDenied_ReturnsNotFound(bool useSecrets,\n        bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n        var initData = await CreateServiceAccountProjectAccessPolicyAsync(org.Id);\n\n        var response = await _client.GetAsync($\"/projects/{initData.ProjectId}/access-policies/service-accounts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_NoAccessPolicies_ReturnsEmpty()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}/access-policies/service-accounts\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ProjectServiceAccountsAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Empty(result.ServiceAccountAccessPolicies);\n    }\n\n    [Fact]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_UserDoesntHavePermission_ReturnsNotFound()\n    {\n        // Create a new account as a user\n        await _organizationHelper.Initialize(true, true, true);\n        var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var initData = await CreateServiceAccountProjectAccessPolicyAsync(orgUser.OrganizationId);\n\n        var response = await _client.GetAsync($\"/projects/{initData.ProjectId}/access-policies/service-accounts\");\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n        var initData = await CreateServiceAccountProjectAccessPolicyAsync(org.Id);\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserProjectAccessPolicy\n                {\n                    GrantedProjectId = initData.ProjectId,\n                    OrganizationUserId = orgUser.Id,\n                    Read = true,\n                    Write = true\n                }\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        var response = await _client.GetAsync($\"/projects/{initData.ProjectId}/access-policies/service-accounts\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content\n            .ReadFromJsonAsync<ProjectServiceAccountsAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.ServiceAccountAccessPolicies);\n        Assert.Equal(initData.ServiceAccountId, result.ServiceAccountAccessPolicies.First().ServiceAccountId);\n        Assert.NotNull(result.ServiceAccountAccessPolicies.First().ServiceAccountName);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task PutProjectServiceAccountsAccessPoliciesAsync_SmNotEnabled_NotFound(bool useSecrets,\n        bool accessSecrets, bool organizationEnabled)\n    {\n        var (_, organizationUser) =\n            await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var (projectId, serviceAccountId) = await CreateProjectAndServiceAccountAsync(organizationUser.OrganizationId);\n\n        var request = new ProjectServiceAccountsAccessPoliciesRequestModel\n        {\n            ServiceAccountAccessPolicyRequests =\n            [\n                new AccessPolicyRequest { GranteeId = serviceAccountId, Read = true, Write = true }\n            ]\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/projects/{projectId}/access-policies/service-accounts\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task PutProjectServiceAccountsAccessPoliciesAsync_UserHasNoPermission_ReturnsNotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var (projectId, serviceAccountId) = await CreateProjectAndServiceAccountAsync(org.Id);\n\n        var request = new ProjectServiceAccountsAccessPoliciesRequestModel\n        {\n            ServiceAccountAccessPolicyRequests =\n            [\n                new AccessPolicyRequest { GranteeId = serviceAccountId, Read = true, Write = true }\n            ]\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/projects/{projectId}/access-policies/service-accounts\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task PutProjectServiceAccountsAccessPoliciesAsync_MismatchedOrgIds_ReturnsNotFound(\n        PermissionType permissionType)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (project, request) =\n            await SetupProjectServiceAccountsAccessPoliciesRequestAsync(permissionType, organizationUser,\n                false);\n\n        var newOrg = await _organizationHelper.CreateSmOrganizationAsync();\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            Name = _mockEncryptedString,\n            OrganizationId = newOrg.Id\n        });\n        request.ServiceAccountAccessPolicyRequests =\n        [\n            new AccessPolicyRequest { GranteeId = serviceAccount.Id, Read = true, Write = true }\n        ];\n\n        var response =\n            await _client.PutAsJsonAsync($\"/projects/{project.Id}/access-policies/service-accounts\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin, false)]\n    [InlineData(PermissionType.RunAsAdmin, true)]\n    [InlineData(PermissionType.RunAsUserWithPermission, false)]\n    [InlineData(PermissionType.RunAsUserWithPermission, true)]\n    public async Task PutProjectServiceAccountsAccessPoliciesAsync_Success(PermissionType permissionType,\n        bool createPreviousAccessPolicy)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (project, request) =\n            await SetupProjectServiceAccountsAccessPoliciesRequestAsync(permissionType, organizationUser,\n                createPreviousAccessPolicy);\n\n        var response =\n            await _client.PutAsJsonAsync($\"/projects/{project.Id}/access-policies/service-accounts\", request);\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content\n            .ReadFromJsonAsync<ProjectServiceAccountsAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.ServiceAccountAccessPolicyRequests.First().GranteeId,\n            result.ServiceAccountAccessPolicies.First().ServiceAccountId);\n        Assert.True(result.ServiceAccountAccessPolicies.First().Read);\n        Assert.True(result.ServiceAccountAccessPolicies.First().Write);\n        Assert.Single(result.ServiceAccountAccessPolicies);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetSecretAccessPoliciesAsync_SmAccessDenied_ReturnsNotFound(bool useSecrets,\n        bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/secrets/{secret.Id}/access-policies\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetSecretAccessPoliciesAsync_NoAccessPolicies_ReturnsEmpty()\n    {\n        var (secretId, _) = await SetupSecretAccessPoliciesTest(PermissionType.RunAsAdmin);\n\n        var response = await _client.GetAsync($\"/secrets/{secretId}/access-policies\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content\n            .ReadFromJsonAsync<SecretAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Empty(result.UserAccessPolicies);\n        Assert.Empty(result.GroupAccessPolicies);\n        Assert.Empty(result.ServiceAccountAccessPolicies);\n    }\n\n    [Fact]\n    public async Task GetSecretAccessPoliciesAsync_UserDoesntHavePermission_ReturnsNotFound()\n    {\n        var (secretId, _) = await SetupSecretAccessPoliciesTest(PermissionType.RunAsUserWithPermission);\n\n        var response = await _client.GetAsync($\"/secrets/{secretId}/access-policies\");\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    public async Task GetSecretAccessPoliciesAsync_Success(PermissionType permissionType)\n    {\n        var (secretId, currentOrgUser) = await SetupSecretAccessPoliciesTest(permissionType);\n\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new UserSecretAccessPolicy\n            {\n                GrantedSecretId = secretId, OrganizationUserId = currentOrgUser.Id, Read = true, Write = true\n            }\n        };\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n\n        var response = await _client.GetAsync($\"/secrets/{secretId}/access-policies\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content\n            .ReadFromJsonAsync<SecretAccessPoliciesResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.UserAccessPolicies);\n        Assert.Empty(result.GroupAccessPolicies);\n        Assert.Empty(result.ServiceAccountAccessPolicies);\n        Assert.NotNull(result.UserAccessPolicies.First().OrganizationUserName);\n        Assert.NotNull(result.UserAccessPolicies.First().OrganizationUserId);\n        Assert.NotNull(result.UserAccessPolicies.First().CurrentUser);\n        Assert.Equal(currentOrgUser.Id, result.UserAccessPolicies.First().OrganizationUserId);\n    }\n\n    private async Task<(Guid ProjectId, Guid ServiceAccountId)> CreateServiceAccountProjectAccessPolicyAsync(\n        Guid organizationId)\n    {\n        var (projectId, serviceAccountId) = await CreateProjectAndServiceAccountAsync(organizationId);\n\n        await _accessPolicyRepository.CreateManyAsync(\n        [\n            new ServiceAccountProjectAccessPolicy\n            {\n                Read = true,\n                Write = true,\n                ServiceAccountId = serviceAccountId,\n                GrantedProjectId = projectId\n            }\n        ]);\n\n        return (projectId, serviceAccountId);\n    }\n\n    private async Task<(Project project, OrganizationUser currentUser)> SetupProjectPeoplePermissionAsync(\n        PermissionType permissionType,\n        OrganizationUser organizationUser)\n    {\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = organizationUser.OrganizationId,\n            Name = _mockEncryptedString\n        });\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n            organizationUser = orgUser;\n        }\n\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new UserProjectAccessPolicy\n            {\n                GrantedProjectId = project.Id,\n                OrganizationUserId = organizationUser.Id,\n                Read = true,\n                Write = true\n            }\n        };\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n\n        return (project, organizationUser);\n    }\n\n    private async Task<(ServiceAccount serviceAccount, OrganizationUser currentUser)>\n        SetupServiceAccountPeoplePermissionAsync(\n            PermissionType permissionType,\n            OrganizationUser organizationUser)\n    {\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = organizationUser.OrganizationId,\n            Name = _mockEncryptedString\n        });\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n            organizationUser = orgUser;\n        }\n\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new UserServiceAccountAccessPolicy\n            {\n                GrantedServiceAccountId = serviceAccount.Id,\n                OrganizationUserId = organizationUser.Id,\n                Read = true,\n                Write = true\n            }\n        };\n\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n\n        return (serviceAccount, organizationUser);\n    }\n\n    private async Task<(Project project, PeopleAccessPoliciesRequestModel request)> SetupProjectPeopleRequestAsync(\n        PermissionType permissionType, OrganizationUser organizationUser)\n    {\n        var (project, currentUser) = await SetupProjectPeoplePermissionAsync(permissionType, organizationUser);\n        var request = new PeopleAccessPoliciesRequestModel\n        {\n            UserAccessPolicyRequests = new List<AccessPolicyRequest>\n            {\n                new() { GranteeId = currentUser.Id, Read = true, Write = true }\n            }\n        };\n        return (project, request);\n    }\n\n    private async Task<(ServiceAccount serviceAccount, PeopleAccessPoliciesRequestModel request)>\n        SetupServiceAccountPeopleRequestAsync(\n            PermissionType permissionType, OrganizationUser organizationUser)\n    {\n        var (serviceAccount, currentUser) =\n            await SetupServiceAccountPeoplePermissionAsync(permissionType, organizationUser);\n        var request = new PeopleAccessPoliciesRequestModel\n        {\n            UserAccessPolicyRequests = new List<AccessPolicyRequest>\n            {\n                new() { GranteeId = currentUser.Id, Read = true, Write = true }\n            }\n        };\n        return (serviceAccount, request);\n    }\n\n    private async Task<(Guid ProjectId, Guid ServiceAccountId)> CreateProjectAndServiceAccountAsync(Guid organizationId,\n        bool misMatchOrganization = false)\n    {\n        var newOrg = new Organization();\n        if (misMatchOrganization)\n        {\n            newOrg = await _organizationHelper.CreateSmOrganizationAsync();\n        }\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = misMatchOrganization ? newOrg.Id : organizationId,\n            Name = _mockEncryptedString\n        });\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = organizationId,\n            Name = _mockEncryptedString\n        });\n\n        return (project.Id, serviceAccount.Id);\n    }\n\n    private async Task<(ServiceAccount serviceAccount, ServiceAccountGrantedPoliciesRequestModel request)>\n        SetupServiceAccountGrantedPoliciesRequestAsync(\n            PermissionType permissionType, OrganizationUser organizationUser, bool createPreviousAccessPolicy)\n    {\n        var (serviceAccount, currentUser) =\n            await SetupServiceAccountPeoplePermissionAsync(permissionType, organizationUser);\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            Name = _mockEncryptedString,\n            OrganizationId = organizationUser.OrganizationId\n        });\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new UserProjectAccessPolicy\n            {\n                GrantedProjectId = project.Id, OrganizationUserId = currentUser.Id, Read = true, Write = true\n            }\n        };\n\n        if (createPreviousAccessPolicy)\n        {\n            var anotherProject = await _projectRepository.CreateAsync(new Project\n            {\n                Name = _mockEncryptedString,\n                OrganizationId = organizationUser.OrganizationId\n            });\n\n            accessPolicies.Add(new UserProjectAccessPolicy\n            {\n                GrantedProjectId = anotherProject.Id,\n                OrganizationUserId = currentUser.Id,\n                Read = true,\n                Write = true\n            });\n            accessPolicies.Add(new ServiceAccountProjectAccessPolicy\n            {\n                GrantedProjectId = anotherProject.Id,\n                ServiceAccountId = serviceAccount.Id,\n                Read = true,\n                Write = true\n            });\n        }\n\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n\n        var request = new ServiceAccountGrantedPoliciesRequestModel\n        {\n            ProjectGrantedPolicyRequests = new List<GrantedAccessPolicyRequest>\n            {\n                new() { GrantedId = project.Id, Read = true, Write = true }\n            }\n        };\n        return (serviceAccount, request);\n    }\n\n    private async Task<(Project project, ProjectServiceAccountsAccessPoliciesRequestModel request)>\n        SetupProjectServiceAccountsAccessPoliciesRequestAsync(\n            PermissionType permissionType, OrganizationUser organizationUser, bool createPreviousAccessPolicy)\n    {\n        var (project, currentUser) = await SetupProjectPeoplePermissionAsync(permissionType, organizationUser);\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            Name = _mockEncryptedString,\n            OrganizationId = currentUser.OrganizationId\n        });\n\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new UserServiceAccountAccessPolicy\n            {\n                GrantedServiceAccountId = serviceAccount.Id,\n                OrganizationUserId = currentUser.Id,\n                Read = true,\n                Write = true\n            }\n        };\n\n        var request = new ProjectServiceAccountsAccessPoliciesRequestModel\n        {\n            ServiceAccountAccessPolicyRequests =\n            [\n                new AccessPolicyRequest { GranteeId = serviceAccount.Id, Read = true, Write = true }\n            ]\n        };\n\n        if (createPreviousAccessPolicy)\n        {\n            var anotherServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n            {\n                Name = _mockEncryptedString,\n                OrganizationId = currentUser.OrganizationId\n            });\n\n            accessPolicies.Add(new UserServiceAccountAccessPolicy\n            {\n                GrantedServiceAccountId = anotherServiceAccount.Id,\n                OrganizationUserId = currentUser.Id,\n                Read = true,\n                Write = true\n            });\n            accessPolicies.Add(new ServiceAccountProjectAccessPolicy\n            {\n                GrantedProjectId = project.Id,\n                ServiceAccountId = anotherServiceAccount.Id,\n                Read = true,\n                Write = true\n            });\n        }\n\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n\n        return (project, request);\n    }\n\n    private async Task<(Guid SecretId, OrganizationUser currentOrgUser)> SetupSecretAccessPoliciesTest(\n        PermissionType permissionType)\n    {\n        var (org, orgAdmin) = await _organizationHelper.Initialize(true, true, true);\n        var currentOrgUser = orgAdmin;\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n            currentOrgUser = orgUser;\n        }\n        else\n        {\n            await _loginHelper.LoginAsync(_email);\n        }\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        return (secret.Id, currentOrgUser);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Controllers/CountsControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.SecretsManager.Enums;\nusing Bit.Api.IntegrationTest.SecretsManager.Helpers;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Controllers;\n\npublic class CountsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly string _mockEncryptedString =\n        \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly IProjectRepository _projectRepository;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly IApiKeyRepository _apiKeyRepository;\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n    private readonly IGroupRepository _groupRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly LoginHelper _loginHelper;\n\n    private string _email = null!;\n    private SecretsManagerOrganizationHelper _organizationHelper = null!;\n\n\n    public CountsControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _projectRepository = _factory.GetService<IProjectRepository>();\n        _secretRepository = _factory.GetService<ISecretRepository>();\n        _serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();\n        _apiKeyRepository = _factory.GetService<IApiKeyRepository>();\n        _accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();\n        _groupRepository = _factory.GetService<IGroupRepository>();\n        _organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_email);\n        _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetByOrganizationAsync_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/sm-counts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetByOrganizationAsync_RunAsServiceAccount_NotFound()\n    {\n        var (_, org, _) = await SetupProjectsWithAccessAsync(PermissionType.RunAsServiceAccountWithPermission);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/sm-counts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetByOrganizationAsync_UserWithoutPermission_ZeroCounts()\n    {\n        var (_, org, _) = await SetupProjectsWithAccessAsync(PermissionType.RunAsUserWithPermission, 0);\n\n        var projects = await CreateProjectsAsync(org.Id);\n        await CreateSecretsAsync(org.Id, projects[0]);\n        await CreateServiceAccountsAsync(org.Id);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/sm-counts\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<OrganizationCountsResponseModel>();\n        Assert.NotNull(result);\n        Assert.Equal(0, result.Projects);\n        Assert.Equal(0, result.Secrets);\n        Assert.Equal(0, result.ServiceAccounts);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetByOrganizationAsync_Success(PermissionType permissionType)\n    {\n        var (projects, org, user) = await SetupProjectsWithAccessAsync(permissionType);\n        var projectsWithoutAccess = await CreateProjectsAsync(org.Id);\n\n        var secrets = await CreateSecretsAsync(org.Id, projects[0]);\n        var secretsWithoutAccess = await CreateSecretsAsync(org.Id, projectsWithoutAccess[0]);\n        var secretsWithoutProject = await CreateSecretsAsync(org.Id, null);\n\n        var serviceAccounts = await CreateServiceAccountsAsync(org.Id);\n        await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccounts[0].Id);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/sm-counts\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<OrganizationCountsResponseModel>();\n        Assert.NotNull(result);\n        if (permissionType == PermissionType.RunAsAdmin)\n        {\n            Assert.Equal(projects.Count + projectsWithoutAccess.Count, result.Projects);\n            Assert.Equal(secrets.Count + secretsWithoutAccess.Count + secretsWithoutProject.Count,\n                result.Secrets);\n            Assert.Equal(serviceAccounts.Count, result.ServiceAccounts);\n        }\n        else\n        {\n            Assert.Equal(projects.Count, result.Projects);\n            Assert.Equal(secrets.Count, result.Secrets);\n            Assert.Equal(1, result.ServiceAccounts);\n        }\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetByProjectAsync_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var projects = await CreateProjectsAsync(org.Id);\n\n        var response = await _client.GetAsync($\"/projects/{projects[0].Id}/sm-counts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetByProjectAsync_RunAsServiceAccount_NotFound()\n    {\n        var (projects, _, _) = await SetupProjectsWithAccessAsync(PermissionType.RunAsServiceAccountWithPermission);\n\n        var response = await _client.GetAsync($\"/projects/{projects[0].Id}/sm-counts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetByProjectAsync_NonExistingProject_NotFound(PermissionType permissionType)\n    {\n        await SetupProjectsWithAccessAsync(permissionType);\n\n        var response = await _client.GetAsync($\"/projects/{Guid.NewGuid().ToString()}/sm-counts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetByProjectAsync_UserWithoutPermission_ZeroCounts()\n    {\n        var (_, org, user) = await SetupProjectsWithAccessAsync(PermissionType.RunAsUserWithPermission, 0);\n\n        var projects = await CreateProjectsAsync(org.Id);\n\n        await CreateSecretsAsync(org.Id, projects[0]);\n\n        var groups = await CreateGroupsAsync(org.Id, user);\n        await CreateGroupProjectAccessPolicyAsync(groups[0].Id, projects[0].Id);\n\n        var serviceAccounts = await CreateServiceAccountsAsync(org.Id);\n        await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[0].Id);\n\n        var response = await _client.GetAsync($\"/projects/{projects[0].Id}/sm-counts\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ProjectCountsResponseModel>();\n        Assert.NotNull(result);\n        Assert.Equal(0, result.Secrets);\n        Assert.Equal(0, result.People);\n        Assert.Equal(0, result.ServiceAccounts);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin, true)]\n    [InlineData(PermissionType.RunAsUserWithPermission, false)]\n    [InlineData(PermissionType.RunAsUserWithPermission, true)]\n    public async Task GetByProjectAsync_Success(PermissionType permissionType, bool userProjectWriteAccess)\n    {\n        var (projects, org, user) = await SetupProjectsWithAccessAsync(permissionType, 3, userProjectWriteAccess);\n\n        var secrets = await CreateSecretsAsync(org.Id, projects[0]);\n        await CreateSecretsAsync(org.Id, projects[1]);\n\n        var groups = await CreateGroupsAsync(org.Id, user);\n        await CreateGroupProjectAccessPolicyAsync(groups[0].Id, projects[0].Id);\n        await CreateGroupProjectAccessPolicyAsync(groups[0].Id, projects[1].Id);\n        var (_, user2) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await CreateUserProjectAccessPolicyAsync(user2.Id, projects[0].Id);\n\n        var serviceAccounts = await CreateServiceAccountsAsync(org.Id);\n        await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccounts[0].Id);\n        await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[0].Id);\n\n        var response = await _client.GetAsync($\"/projects/{projects[0].Id}/sm-counts\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ProjectCountsResponseModel>();\n        Assert.NotNull(result);\n        Assert.Equal(secrets.Count, result.Secrets);\n        if (userProjectWriteAccess)\n        {\n            Assert.Equal(permissionType == PermissionType.RunAsAdmin ? 2 : 3, result.People);\n            Assert.Equal(1, result.ServiceAccounts);\n        }\n        else\n        {\n            Assert.Equal(0, result.People);\n            Assert.Equal(0, result.ServiceAccounts);\n        }\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetByServiceAccountAsync_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccounts = await CreateServiceAccountsAsync(org.Id);\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccounts[0].Id}/sm-counts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetByServiceAccountAsync_RunAsServiceAccount_NotFound()\n    {\n        var (_, org, _) = await SetupProjectsWithAccessAsync(PermissionType.RunAsServiceAccountWithPermission);\n\n        var serviceAccounts = await CreateServiceAccountsAsync(org.Id);\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccounts[0].Id}/sm-counts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetByServiceAccountAsync_NonExistingServiceAccount_NotFound(PermissionType permissionType)\n    {\n        await SetupProjectsWithAccessAsync(permissionType);\n\n        var response = await _client.GetAsync($\"/service-accounts/{Guid.NewGuid().ToString()}/sm-counts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetByServiceAccountAsync_UserWithoutPermission_ZeroCounts()\n    {\n        var (_, org, user) = await SetupProjectsWithAccessAsync(PermissionType.RunAsUserWithPermission, 0);\n\n        var projects = await CreateProjectsAsync(org.Id);\n\n        var serviceAccounts = await CreateServiceAccountsAsync(org.Id);\n        await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[0].Id);\n\n        var groups = await CreateGroupsAsync(org.Id, user);\n        await CreateGroupServiceAccountAccessPolicyAsync(groups[0].Id, serviceAccounts[0].Id);\n\n        await CreateApiKeysAsync(serviceAccounts[0]);\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccounts[0].Id}/sm-counts\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ServiceAccountCountsResponseModel>();\n        Assert.NotNull(result);\n        Assert.Equal(0, result.Projects);\n        Assert.Equal(0, result.People);\n        Assert.Equal(0, result.AccessTokens);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetByServiceAccountAsync_Success(PermissionType permissionType)\n    {\n        var (projects, org, user) = await SetupProjectsWithAccessAsync(permissionType);\n\n        var serviceAccounts = await CreateServiceAccountsAsync(org.Id);\n        await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[0].Id);\n        await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[1].Id);\n        await CreateServiceAccountProjectAccessPolicyAsync(projects[1].Id, serviceAccounts[0].Id);\n\n        await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccounts[0].Id);\n        var groups = await CreateGroupsAsync(org.Id, user);\n        await CreateGroupServiceAccountAccessPolicyAsync(groups[0].Id, serviceAccounts[0].Id);\n        await CreateGroupServiceAccountAccessPolicyAsync(groups[0].Id, serviceAccounts[1].Id);\n        var (_, user2) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await CreateUserServiceAccountAccessPolicyAsync(user2.Id, serviceAccounts[0].Id);\n\n        var apiKeys = await CreateApiKeysAsync(serviceAccounts[0]);\n        await CreateApiKeysAsync(serviceAccounts[1]);\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccounts[0].Id}/sm-counts\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ServiceAccountCountsResponseModel>();\n        Assert.NotNull(result);\n        Assert.Equal(2, result.Projects);\n        Assert.Equal(3, result.People);\n        Assert.Equal(apiKeys.Count, result.AccessTokens);\n    }\n\n    private async Task<List<Project>> CreateProjectsAsync(Guid orgId, int numberToCreate = 3)\n    {\n        var projects = new List<Project>();\n        for (var i = 0; i < numberToCreate; i++)\n        {\n            var project = await _projectRepository.CreateAsync(new Project\n            {\n                OrganizationId = orgId,\n                Name = _mockEncryptedString,\n            });\n            projects.Add(project);\n        }\n\n        return projects;\n    }\n\n    private async Task<List<Secret>> CreateSecretsAsync(Guid organizationId, Project? project, int numberToCreate = 3)\n    {\n        var secrets = new List<Secret>();\n        for (var i = 0; i < numberToCreate; i++)\n        {\n            var secret = await _secretRepository.CreateAsync(new Secret\n            {\n                OrganizationId = organizationId,\n                Key = _mockEncryptedString,\n                Value = _mockEncryptedString,\n                Note = _mockEncryptedString,\n                Projects = project != null ? new List<Project> { project } : null\n            });\n            secrets.Add(secret);\n        }\n\n        return secrets;\n    }\n\n    private async Task<List<ServiceAccount>> CreateServiceAccountsAsync(Guid organizationId, int numberToCreate = 3)\n    {\n        var serviceAccounts = new List<ServiceAccount>();\n        for (var i = 0; i < numberToCreate; i++)\n        {\n            var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n            {\n                OrganizationId = organizationId,\n                Name = _mockEncryptedString\n            });\n            serviceAccounts.Add(serviceAccount);\n        }\n\n        return serviceAccounts;\n    }\n\n    private async Task<List<Group>> CreateGroupsAsync(Guid organizationId, OrganizationUser? user,\n        int numberToCreate = 3)\n    {\n        var groups = new List<Group>();\n\n        for (var i = 0; i < numberToCreate; i++)\n        {\n            var group = await _groupRepository.CreateAsync(new Group\n            {\n                OrganizationId = organizationId,\n                Name = _mockEncryptedString,\n            });\n            groups.Add(group);\n\n            if (user != null)\n            {\n                await _organizationUserRepository.UpdateGroupsAsync(user.Id, [group.Id]);\n            }\n        }\n\n        return groups;\n    }\n\n    private async Task<List<ApiKey>> CreateApiKeysAsync(ServiceAccount serviceAccount, int numberToCreate = 3)\n    {\n        var apiKeys = new List<ApiKey>();\n\n        for (var i = 0; i < numberToCreate; i++)\n        {\n            var apiKey = await _apiKeyRepository.CreateAsync(new ApiKey\n            {\n                Name = _mockEncryptedString,\n                ServiceAccountId = serviceAccount.Id,\n                Scope = \"api.secrets\",\n                Key = serviceAccount.OrganizationId.ToString(),\n                EncryptedPayload = _mockEncryptedString,\n                ClientSecretHash = \"807613bbf6692e6809a571bc694a4719a5aa6863f7a62bd714003ab73de588e6\"\n            });\n            apiKeys.Add(apiKey);\n        }\n\n        return apiKeys;\n    }\n\n    private async Task<(List<Project>, Organization, OrganizationUser)> SetupProjectsWithAccessAsync(\n        PermissionType permissionType,\n        int projectsToCreate = 3,\n        bool writeAccess = false)\n    {\n        var (org, owner) = await _organizationHelper.Initialize(true, true, true);\n        var projects = await CreateProjectsAsync(org.Id, projectsToCreate);\n        var user = owner;\n\n        switch (permissionType)\n        {\n            case PermissionType.RunAsAdmin:\n                await _loginHelper.LoginAsync(_email);\n                break;\n            case PermissionType.RunAsUserWithPermission:\n                {\n                    var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n                    user = orgUser;\n                    await _loginHelper.LoginAsync(email);\n\n                    foreach (var project in projects)\n                    {\n                        await CreateUserProjectAccessPolicyAsync(user.Id, project.Id, writeAccess);\n                    }\n\n                    break;\n                }\n            case PermissionType.RunAsServiceAccountWithPermission:\n                {\n                    var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();\n                    await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);\n\n                    foreach (var project in projects)\n                    {\n                        await CreateServiceAccountProjectAccessPolicyAsync(project.Id, apiKeyDetails.ApiKey.ServiceAccountId!.Value);\n                    }\n\n                    break;\n                }\n            default:\n                throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);\n        }\n\n        return (projects, org, user);\n    }\n\n    private async Task CreateUserProjectAccessPolicyAsync(Guid userId, Guid projectId, bool write = false)\n    {\n        var policy = new UserProjectAccessPolicy\n        {\n            OrganizationUserId = userId,\n            GrantedProjectId = projectId,\n            Read = true,\n            Write = write,\n        };\n        await _accessPolicyRepository.CreateManyAsync([policy]);\n    }\n\n    private async Task CreateGroupProjectAccessPolicyAsync(Guid groupId, Guid projectId)\n    {\n        var policy = new GroupProjectAccessPolicy\n        {\n            GroupId = groupId,\n            GrantedProjectId = projectId,\n            Read = true,\n            Write = false,\n        };\n        await _accessPolicyRepository.CreateManyAsync([policy]);\n    }\n\n\n    private async Task CreateUserServiceAccountAccessPolicyAsync(Guid userId, Guid serviceAccountId)\n    {\n        var policy = new UserServiceAccountAccessPolicy\n        {\n            OrganizationUserId = userId,\n            GrantedServiceAccountId = serviceAccountId,\n            Read = true,\n            Write = false,\n        };\n        await _accessPolicyRepository.CreateManyAsync([policy]);\n    }\n\n    private async Task CreateGroupServiceAccountAccessPolicyAsync(Guid groupId, Guid serviceAccountId)\n    {\n        var policy = new GroupServiceAccountAccessPolicy\n        {\n            GroupId = groupId,\n            GrantedServiceAccountId = serviceAccountId,\n            Read = true,\n            Write = false\n        };\n        await _accessPolicyRepository.CreateManyAsync([policy]);\n    }\n\n    private async Task CreateServiceAccountProjectAccessPolicyAsync(Guid projectId, Guid serviceAccountId)\n    {\n        var policy = new ServiceAccountProjectAccessPolicy\n        {\n            ServiceAccountId = serviceAccountId,\n            GrantedProjectId = projectId,\n            Read = true,\n            Write = false,\n        };\n        await _accessPolicyRepository.CreateManyAsync([policy]);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.SecretsManager.Enums;\nusing Bit.Api.IntegrationTest.SecretsManager.Helpers;\nusing Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Controllers;\n\npublic class ProjectsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly string _mockEncryptedString =\n        \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n    private readonly LoginHelper _loginHelper;\n\n    private string _email = null!;\n    private SecretsManagerOrganizationHelper _organizationHelper = null!;\n\n    public ProjectsControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _projectRepository = _factory.GetService<IProjectRepository>();\n        _accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_email);\n        _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task ListByOrganization_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/projects\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task ListByOrganization_UserWithoutPermission_EmptyList()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        await CreateProjectsAsync(org.Id);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/projects\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ProjectResponseModel>>();\n        Assert.NotNull(result);\n        Assert.Empty(result.Data);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task ListByOrganization_Success(PermissionType permissionType)\n    {\n        var (projectIds, org) = await SetupProjectsWithAccessAsync(permissionType);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/projects\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ProjectResponseModel>>();\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.Data);\n        Assert.Equal(projectIds.Count, result.Data.Count());\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Create_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var request = new ProjectCreateRequestModel { Name = _mockEncryptedString };\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{org.Id}/projects\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task Create_AtMaxProjects_BadRequest(PermissionType permissionType)\n    {\n        var (_, organization) = await SetupProjectsWithAccessAsync(permissionType);\n        var request = new ProjectCreateRequestModel { Name = _mockEncryptedString };\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{organization.Id}/projects\", request);\n\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task Create_Success(PermissionType permissionType)\n    {\n        var (org, adminOrgUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n        var orgUserId = adminOrgUser.Id;\n        var currentUserId = adminOrgUser.UserId!.Value;\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n            orgUserId = orgUser.Id;\n            currentUserId = orgUser.UserId!.Value;\n        }\n\n        var request = new ProjectCreateRequestModel { Name = _mockEncryptedString };\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{org.Id}/projects\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ProjectResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, result.Name);\n        AssertHelper.AssertRecent(result.RevisionDate);\n        AssertHelper.AssertRecent(result.CreationDate);\n\n        var createdProject = await _projectRepository.GetByIdAsync(result.Id);\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, createdProject.Name);\n        AssertHelper.AssertRecent(createdProject.RevisionDate);\n        AssertHelper.AssertRecent(createdProject.CreationDate);\n        Assert.Null(createdProject.DeletedDate);\n\n        // Check permissions have been bootstrapped.\n        var accessPolicies = await _accessPolicyRepository.GetPeoplePoliciesByGrantedProjectIdAsync(createdProject.Id, currentUserId);\n        Assert.NotNull(accessPolicies);\n        var ap = (UserProjectAccessPolicy)accessPolicies.First();\n        Assert.Equal(createdProject.Id, ap.GrantedProjectId);\n        Assert.Equal(orgUserId, ap.OrganizationUserId);\n        Assert.True(ap.Read);\n        Assert.True(ap.Write);\n        AssertHelper.AssertRecent(ap.CreationDate);\n        AssertHelper.AssertRecent(ap.RevisionDate);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Update_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var initialProject = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var mockEncryptedString2 =\n            \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n        var request = new ProjectCreateRequestModel { Name = mockEncryptedString2 };\n\n        var response = await _client.PutAsJsonAsync($\"/projects/{initialProject.Id}\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task Update_Success(PermissionType permissionType)\n    {\n        var initialProject = await SetupProjectWithAccessAsync(permissionType);\n\n        var mockEncryptedString2 =\n            \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n        var request = new ProjectUpdateRequestModel { Name = mockEncryptedString2 };\n\n        var response = await _client.PutAsJsonAsync($\"/projects/{initialProject.Id}\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ProjectResponseModel>();\n        Assert.NotEqual(initialProject.Name, result!.Name);\n        AssertHelper.AssertRecent(result.RevisionDate);\n        Assert.NotEqual(initialProject.RevisionDate, result.RevisionDate);\n\n        var updatedProject = await _projectRepository.GetByIdAsync(result.Id);\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, updatedProject.Name);\n        AssertHelper.AssertRecent(updatedProject.RevisionDate);\n        Assert.Null(updatedProject.DeletedDate);\n        Assert.NotEqual(initialProject.Name, updatedProject.Name);\n        Assert.NotEqual(initialProject.RevisionDate, updatedProject.RevisionDate);\n    }\n\n    [Fact]\n    public async Task Update_NonExistingProject_NotFound()\n    {\n        await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var request = new ProjectUpdateRequestModel\n        {\n            Name =\n                \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\",\n        };\n\n        var response = await _client.PutAsJsonAsync(\"/projects/c53de509-4581-402c-8cbd-f26d2c516fba\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Update_MissingAccessPolicy_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var request = new ProjectUpdateRequestModel\n        {\n            Name =\n                \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\",\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/projects/{project.Id}\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Get_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Get_MissingAccessPolicy_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var createdProject = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var response = await _client.GetAsync($\"/projects/{createdProject.Id}\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Get_NonExistingProject_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var createdProject = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        await _client.PostAsync(\"/projects/delete\", JsonContent.Create(createdProject.Id));\n\n        var response = await _client.GetAsync($\"/projects/{createdProject.Id}\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task Get_Success(PermissionType permissionType)\n    {\n        var project = await SetupProjectWithAccessAsync(permissionType);\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ProjectResponseModel>();\n        Assert.Equal(project.Name, result!.Name);\n        Assert.Equal(project.RevisionDate, result.RevisionDate);\n        Assert.Equal(project.CreationDate, result.CreationDate);\n        Assert.True(result.Read);\n        Assert.True(result.Write);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Delete_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var projectIds = await CreateProjectsAsync(org.Id);\n\n        var response = await _client.PostAsync(\"/projects/delete\", JsonContent.Create(projectIds));\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Delete_MissingAccessPolicy_AccessDenied()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var projectIds = await CreateProjectsAsync(org.Id);\n\n        var response = await _client.PostAsync(\"/projects/delete\", JsonContent.Create(projectIds));\n\n        var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>();\n        Assert.NotNull(results);\n        Assert.Equal(projectIds.OrderBy(x => x),\n            results.Data.Select(x => x.Id).OrderBy(x => x));\n        Assert.All(results.Data, item => Assert.Equal(\"access denied\", item.Error));\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task Delete_Success(PermissionType permissionType)\n    {\n        var (projectIds, _) = await SetupProjectsWithAccessAsync(permissionType);\n\n        var response = await _client.PostAsync(\"/projects/delete\", JsonContent.Create(projectIds));\n        response.EnsureSuccessStatusCode();\n\n        var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>();\n        Assert.NotNull(results);\n        Assert.Equal(projectIds.OrderBy(x => x),\n            results.Data.Select(x => x.Id).OrderBy(x => x));\n        Assert.DoesNotContain(results.Data, x => x.Error != null);\n\n        var projects = await _projectRepository.GetManyWithSecretsByIds(projectIds);\n        Assert.Empty(projects);\n    }\n\n    private async Task<List<Guid>> CreateProjectsAsync(Guid orgId, int numberToCreate = 3)\n    {\n        var projectIds = new List<Guid>();\n        for (var i = 0; i < numberToCreate; i++)\n        {\n            var project = await _projectRepository.CreateAsync(new Project\n            {\n                OrganizationId = orgId,\n                Name = _mockEncryptedString,\n            });\n            projectIds.Add(project.Id);\n        }\n\n        return projectIds;\n    }\n\n    private async Task<(List<Guid>, Organization)> SetupProjectsWithAccessAsync(PermissionType permissionType,\n        int projectsToCreate = 3)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n        var projectIds = await CreateProjectsAsync(org.Id, projectsToCreate);\n\n        if (permissionType == PermissionType.RunAsAdmin)\n        {\n            return (projectIds, org);\n        }\n\n        var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var accessPolicies = projectIds.Select(projectId => new UserProjectAccessPolicy\n        {\n            GrantedProjectId = projectId,\n            OrganizationUserId = orgUser.Id,\n            Read = true,\n            Write = true,\n        })\n            .Cast<BaseAccessPolicy>()\n            .ToList();\n\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n\n        return (projectIds, org);\n    }\n\n    private async Task<Project> SetupProjectWithAccessAsync(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var initialProject = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        if (permissionType == PermissionType.RunAsAdmin)\n        {\n            return initialProject;\n        }\n\n        var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new UserProjectAccessPolicy\n            {\n                GrantedProjectId = initialProject.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,\n            },\n        };\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n\n        return initialProject;\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Controllers/SecretVersionsControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.SecretsManager.Enums;\nusing Bit.Api.IntegrationTest.SecretsManager.Helpers;\nusing Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Controllers;\n\npublic class SecretVersionsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly string _mockEncryptedString =\n        \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly ISecretRepository _secretRepository;\n    private readonly ISecretVersionRepository _secretVersionRepository;\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n    private readonly LoginHelper _loginHelper;\n\n    private string _email = null!;\n    private SecretsManagerOrganizationHelper _organizationHelper = null!;\n\n    public SecretVersionsControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _secretRepository = _factory.GetService<ISecretRepository>();\n        _secretVersionRepository = _factory.GetService<ISecretVersionRepository>();\n        _accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_email);\n        _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetVersionsBySecretId_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/secrets/{secret.Id}/versions\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetVersionsBySecretId_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        // Create some versions\n        var version1 = await _secretVersionRepository.CreateAsync(new SecretVersion\n        {\n            SecretId = secret.Id,\n            Value = _mockEncryptedString,\n            VersionDate = DateTime.UtcNow.AddDays(-2)\n        });\n\n        var version2 = await _secretVersionRepository.CreateAsync(new SecretVersion\n        {\n            SecretId = secret.Id,\n            Value = _mockEncryptedString,\n            VersionDate = DateTime.UtcNow.AddDays(-1)\n        });\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserSecretAccessPolicy\n                {\n                    GrantedSecretId = secret.Id,\n                    OrganizationUserId = orgUser.Id,\n                    Read = true,\n                    Write = true\n                }\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        var response = await _client.GetAsync($\"/secrets/{secret.Id}/versions\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<SecretVersionResponseModel>>();\n\n        Assert.NotNull(result);\n        Assert.Equal(2, result.Data.Count());\n    }\n\n    [Fact]\n    public async Task GetVersionById_Success()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        var version = await _secretVersionRepository.CreateAsync(new SecretVersion\n        {\n            SecretId = secret.Id,\n            Value = _mockEncryptedString,\n            VersionDate = DateTime.UtcNow\n        });\n\n        var response = await _client.GetAsync($\"/secret-versions/{version.Id}\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<SecretVersionResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(version.Id, result.Id);\n        Assert.Equal(secret.Id, result.SecretId);\n    }\n\n    [Fact]\n    public async Task RestoreVersion_Success()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = \"OriginalValue\",\n            Note = _mockEncryptedString\n        });\n\n        var version = await _secretVersionRepository.CreateAsync(new SecretVersion\n        {\n            SecretId = secret.Id,\n            Value = \"OldValue\",\n            VersionDate = DateTime.UtcNow.AddDays(-1)\n        });\n\n        var request = new RestoreSecretVersionRequestModel\n        {\n            VersionId = version.Id\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/secrets/{secret.Id}/versions/restore\", request);\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<SecretResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(\"OldValue\", result.Value);\n    }\n\n    [Fact]\n    public async Task BulkDelete_Success()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        var version1 = await _secretVersionRepository.CreateAsync(new SecretVersion\n        {\n            SecretId = secret.Id,\n            Value = _mockEncryptedString,\n            VersionDate = DateTime.UtcNow.AddDays(-2)\n        });\n\n        var version2 = await _secretVersionRepository.CreateAsync(new SecretVersion\n        {\n            SecretId = secret.Id,\n            Value = _mockEncryptedString,\n            VersionDate = DateTime.UtcNow.AddDays(-1)\n        });\n\n        var ids = new List<Guid> { version1.Id, version2.Id };\n\n        var response = await _client.PostAsJsonAsync(\"/secret-versions/delete\", ids);\n        response.EnsureSuccessStatusCode();\n\n        var versions = await _secretVersionRepository.GetManyBySecretIdAsync(secret.Id);\n        Assert.Empty(versions);\n    }\n\n    [Fact]\n    public async Task GetVersionsBySecretId_ReturnsOrderedByVersionDate()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        // Create versions in random order\n        await _secretVersionRepository.CreateAsync(new SecretVersion\n        {\n            SecretId = secret.Id,\n            Value = \"Version2\",\n            VersionDate = DateTime.UtcNow.AddDays(-1)\n        });\n\n        await _secretVersionRepository.CreateAsync(new SecretVersion\n        {\n            SecretId = secret.Id,\n            Value = \"Version3\",\n            VersionDate = DateTime.UtcNow\n        });\n\n        await _secretVersionRepository.CreateAsync(new SecretVersion\n        {\n            SecretId = secret.Id,\n            Value = \"Version1\",\n            VersionDate = DateTime.UtcNow.AddDays(-2)\n        });\n\n        var response = await _client.GetAsync($\"/secrets/{secret.Id}/versions\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<SecretVersionResponseModel>>();\n\n        Assert.NotNull(result);\n        Assert.Equal(3, result.Data.Count());\n\n        var versions = result.Data.ToList();\n        // Should be ordered by VersionDate descending (newest first)\n        Assert.Equal(\"Version3\", versions[0].Value);\n        Assert.Equal(\"Version2\", versions[1].Value);\n        Assert.Equal(\"Version1\", versions[2].Value);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.SecretsManager.Enums;\nusing Bit.Api.IntegrationTest.SecretsManager.Helpers;\nusing Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Controllers;\n\npublic class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly string _mockEncryptedString =\n        \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly ISecretRepository _secretRepository;\n    private readonly IProjectRepository _projectRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n    private readonly LoginHelper _loginHelper;\n\n    private string _email = null!;\n    private SecretsManagerOrganizationHelper _organizationHelper = null!;\n\n    public SecretsControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _secretRepository = _factory.GetService<ISecretRepository>();\n        _projectRepository = _factory.GetService<IProjectRepository>();\n        _accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();\n        _serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_email);\n        _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task ListByOrganization_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/secrets\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task ListByOrganization_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            Id = new Guid(),\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,\n                },\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        var secretIds = new List<Guid>();\n        for (var i = 0; i < 3; i++)\n        {\n            var secret = await _secretRepository.CreateAsync(new Secret\n            {\n                OrganizationId = org.Id,\n                Key = _mockEncryptedString,\n                Value = _mockEncryptedString,\n                Note = _mockEncryptedString,\n                Projects = new List<Project> { project }\n\n            });\n            secretIds.Add(secret.Id);\n        }\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/secrets\");\n        response.EnsureSuccessStatusCode();\n\n        var result = await response.Content.ReadFromJsonAsync<SecretWithProjectsListResponseModel>();\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.Secrets);\n        Assert.Equal(secretIds.Count, result.Secrets.Count());\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Create_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var request = new SecretCreateRequestModel\n        {\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{org.Id}/secrets\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task Create_WithoutProject_RunAsAdmin_Success(bool withAccessPolicies)\n    {\n        var (organizationUser, request) = await SetupSecretCreateRequestAsync(withAccessPolicies);\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{organizationUser.OrganizationId}/secrets\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<SecretResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.Key, result.Key);\n        Assert.Equal(request.Value, result.Value);\n        Assert.Equal(request.Note, result.Note);\n        AssertHelper.AssertRecent(result.RevisionDate);\n        AssertHelper.AssertRecent(result.CreationDate);\n\n        var createdSecret = await _secretRepository.GetByIdAsync(result.Id);\n        Assert.NotNull(result);\n        Assert.Equal(request.Key, createdSecret.Key);\n        Assert.Equal(request.Value, createdSecret.Value);\n        Assert.Equal(request.Note, createdSecret.Note);\n        AssertHelper.AssertRecent(createdSecret.RevisionDate);\n        AssertHelper.AssertRecent(createdSecret.CreationDate);\n        Assert.Null(createdSecret.DeletedDate);\n\n        if (withAccessPolicies)\n        {\n            var secretAccessPolicies = await _accessPolicyRepository.GetSecretAccessPoliciesAsync(result.Id, organizationUser.UserId!.Value);\n            Assert.NotNull(secretAccessPolicies);\n            Assert.NotEmpty(secretAccessPolicies.UserAccessPolicies);\n            Assert.Equal(organizationUser.Id, secretAccessPolicies.UserAccessPolicies.First().OrganizationUserId);\n            Assert.Equal(result.Id, secretAccessPolicies.UserAccessPolicies.First().GrantedSecretId);\n            Assert.True(secretAccessPolicies.UserAccessPolicies.First().Read);\n            Assert.True(secretAccessPolicies.UserAccessPolicies.First().Write);\n        }\n    }\n\n    [Fact]\n    public async Task CreateWithDifferentProjectOrgId_RunAsAdmin_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n        var anotherOrg = await _organizationHelper.CreateSmOrganizationAsync();\n\n        var project =\n            await _projectRepository.CreateAsync(new Project { Name = \"123\", OrganizationId = anotherOrg.Id });\n\n        var request = new SecretCreateRequestModel\n        {\n            ProjectIds = new[] { project.Id },\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{org.Id}/secrets\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task CreateWithMultipleProjects_RunAsAdmin_BadRequest()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var projectA = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = \"123A\" });\n        var projectB = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = \"123B\" });\n\n        var request = new SecretCreateRequestModel\n        {\n            ProjectIds = new Guid[] { projectA.Id, projectB.Id },\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString,\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{org.Id}/secrets\", request);\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task CreateWithoutProject_RunAsUser_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var request = new SecretCreateRequestModel\n        {\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{org.Id}/secrets\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Create_RunAsServiceAccount_WithAccessPolicies_NotFound()\n    {\n        var (organizationUser, secretRequest) =\n            await SetupSecretWithProjectCreateRequestAsync(PermissionType.RunAsServiceAccountWithPermission, true);\n\n        var response =\n            await _client.PostAsJsonAsync($\"/organizations/{organizationUser.OrganizationId}/secrets\", secretRequest);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin, false)]\n    [InlineData(PermissionType.RunAsAdmin, true)]\n    [InlineData(PermissionType.RunAsUserWithPermission, false)]\n    [InlineData(PermissionType.RunAsUserWithPermission, true)]\n    [InlineData(PermissionType.RunAsServiceAccountWithPermission, false)]\n    public async Task Create_WithProject_Success(PermissionType permissionType, bool withAccessPolicies)\n    {\n        var (organizationUser, secretRequest) = await SetupSecretWithProjectCreateRequestAsync(permissionType, withAccessPolicies);\n\n        var secretResponse = await _client.PostAsJsonAsync($\"/organizations/{organizationUser.OrganizationId}/secrets\", secretRequest);\n        secretResponse.EnsureSuccessStatusCode();\n        var result = await secretResponse.Content.ReadFromJsonAsync<SecretResponseModel>();\n\n        Assert.NotNull(result);\n        var secret = await _secretRepository.GetByIdAsync(result.Id);\n        Assert.Equal(secret.Id, result.Id);\n        Assert.Equal(secret.OrganizationId, result.OrganizationId);\n        Assert.Equal(secret.Key, result.Key);\n        Assert.Equal(secret.Value, result.Value);\n        Assert.Equal(secret.Note, result.Note);\n        Assert.Equal(secret.CreationDate, result.CreationDate);\n        Assert.Equal(secret.RevisionDate, result.RevisionDate);\n\n        if (withAccessPolicies)\n        {\n            var secretAccessPolicies = await _accessPolicyRepository.GetSecretAccessPoliciesAsync(secret.Id, organizationUser.UserId!.Value);\n            Assert.NotNull(secretAccessPolicies);\n            Assert.NotEmpty(secretAccessPolicies.UserAccessPolicies);\n            Assert.Equal(organizationUser.Id, secretAccessPolicies.UserAccessPolicies.First().OrganizationUserId);\n            Assert.Equal(secret.Id, secretAccessPolicies.UserAccessPolicies.First().GrantedSecretId);\n            Assert.True(secretAccessPolicies.UserAccessPolicies.First().Read);\n            Assert.True(secretAccessPolicies.UserAccessPolicies.First().Write);\n        }\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Get_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/organizations/secrets/{secret.Id}\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task Get_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project()\n        {\n            Id = new Guid(),\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,\n                },\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n        else\n        {\n            var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.Admin, true);\n            await _loginHelper.LoginAsync(email);\n        }\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString,\n            Projects = new List<Project> { project }\n        });\n\n        var response = await _client.GetAsync($\"/secrets/{secret.Id}\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<SecretResponseModel>();\n        Assert.Equal(secret.Key, result!.Key);\n        Assert.Equal(secret.Value, result.Value);\n        Assert.Equal(secret.Note, result.Note);\n        Assert.Equal(secret.RevisionDate, result.RevisionDate);\n        Assert.Equal(secret.CreationDate, result.CreationDate);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetSecretsByProject_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}/secrets\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetSecretsByProject_UserWithNoPermission_EmptyList()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var project = await _projectRepository.CreateAsync(new Project()\n        {\n            Id = new Guid(),\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString,\n            Projects = new List<Project> { project },\n        });\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}/secrets\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<SecretWithProjectsListResponseModel>();\n        Assert.NotNull(result);\n        Assert.Empty(result.Secrets);\n        Assert.Empty(result.Projects);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetSecretsByProject_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project()\n        {\n            Id = new Guid(),\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,\n                },\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString,\n            Projects = new List<Project> { project },\n        });\n\n        var response = await _client.GetAsync($\"/projects/{project.Id}/secrets\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<SecretWithProjectsListResponseModel>();\n        Assert.NotEmpty(result!.Secrets);\n        Assert.Equal(secret.Id, result.Secrets.First().Id);\n        Assert.Equal(secret.OrganizationId, result.Secrets.First().OrganizationId);\n        Assert.Equal(secret.Key, result.Secrets.First().Key);\n        Assert.Equal(secret.CreationDate, result.Secrets.First().CreationDate);\n        Assert.Equal(secret.RevisionDate, result.Secrets.First().RevisionDate);\n        Assert.Equal(secret.Projects!.First().Id, result.Projects.First().Id);\n        Assert.Equal(secret.Projects!.First().Name, result.Projects.First().Name);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Update_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        var request = new SecretUpdateRequestModel\n        {\n            Key = _mockEncryptedString,\n            Value = \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\",\n            Note = _mockEncryptedString\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/organizations/secrets/{secret.Id}\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsServiceAccountWithPermission, true)]\n    public async Task Update_RunAsServiceAccountWithAccessPolicyUpdate_NotFound(PermissionType permissionType, bool withAccessPolices)\n    {\n        var (secret, request) = await SetupSecretUpdateRequestAsync(permissionType, withAccessPolices);\n\n        var response = await _client.PutAsJsonAsync($\"/secrets/{secret.Id}\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin, false)]\n    [InlineData(PermissionType.RunAsAdmin, true)]\n    [InlineData(PermissionType.RunAsUserWithPermission, false)]\n    [InlineData(PermissionType.RunAsUserWithPermission, true)]\n    [InlineData(PermissionType.RunAsServiceAccountWithPermission, false)]\n    public async Task Update_Success(PermissionType permissionType, bool withAccessPolices)\n    {\n        var (secret, request) = await SetupSecretUpdateRequestAsync(permissionType, withAccessPolices);\n\n        var response = await _client.PutAsJsonAsync($\"/secrets/{secret.Id}\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<SecretResponseModel>();\n        Assert.Equal(request.Key, result!.Key);\n        Assert.Equal(request.Value, result.Value);\n        Assert.NotEqual(secret.Value, result.Value);\n        Assert.Equal(request.Note, result.Note);\n        AssertHelper.AssertRecent(result.RevisionDate);\n        Assert.NotEqual(secret.RevisionDate, result.RevisionDate);\n\n        var updatedSecret = await _secretRepository.GetByIdAsync(result.Id);\n        Assert.NotNull(result);\n        Assert.Equal(request.Key, updatedSecret.Key);\n        Assert.Equal(request.Value, updatedSecret.Value);\n        Assert.Equal(request.Note, updatedSecret.Note);\n        AssertHelper.AssertRecent(updatedSecret.RevisionDate);\n        AssertHelper.AssertRecent(updatedSecret.CreationDate);\n        Assert.Null(updatedSecret.DeletedDate);\n        Assert.NotEqual(secret.Value, updatedSecret.Value);\n        Assert.NotEqual(secret.RevisionDate, updatedSecret.RevisionDate);\n\n        if (withAccessPolices)\n        {\n            var secretAccessPolicies = await _accessPolicyRepository.GetSecretAccessPoliciesAsync(secret.Id,\n                request.AccessPoliciesRequests.UserAccessPolicyRequests.First().GranteeId);\n            Assert.NotNull(secretAccessPolicies);\n            Assert.NotEmpty(secretAccessPolicies.UserAccessPolicies);\n            Assert.Equal(request.AccessPoliciesRequests.UserAccessPolicyRequests.First().GranteeId,\n                secretAccessPolicies.UserAccessPolicies.First().OrganizationUserId);\n            Assert.Equal(secret.Id, secretAccessPolicies.UserAccessPolicies.First().GrantedSecretId);\n            Assert.True(secretAccessPolicies.UserAccessPolicies.First().Read);\n            Assert.True(secretAccessPolicies.UserAccessPolicies.First().Write);\n        }\n    }\n\n    [Fact]\n    public async Task UpdateWithDifferentProjectOrgId_RunAsAdmin_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n        var anotherOrg = await _organizationHelper.CreateSmOrganizationAsync();\n\n        var project = await _projectRepository.CreateAsync(new Project { Name = \"123\", OrganizationId = anotherOrg.Id });\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        var request = new SecretUpdateRequestModel\n        {\n            Key = _mockEncryptedString,\n            Value = \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\",\n            Note = _mockEncryptedString,\n            ProjectIds = new Guid[] { project.Id },\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/secrets/{secret.Id}\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task UpdateWithMultipleProjects_BadRequest()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var projectA = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = \"123A\" });\n        var projectB = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = \"123B\" });\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n\n        var request = new SecretUpdateRequestModel\n        {\n            Key = _mockEncryptedString,\n            Value = \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\",\n            Note = _mockEncryptedString,\n            ProjectIds = new Guid[] { projectA.Id, projectB.Id },\n        };\n\n        var response = await _client.PutAsJsonAsync($\"/secrets/{secret.Id}\", request);\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Delete_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        });\n        var secretIds = new[] { secret.Id };\n\n        var response = await _client.PostAsJsonAsync($\"/secrets/delete\", secretIds);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Delete_MissingAccessPolicy_AccessDenied()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var (_, secretIds) = await CreateSecretsAsync(org.Id);\n\n        var response = await _client.PostAsync(\"/secrets/delete\", JsonContent.Create(secretIds));\n\n        var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>();\n        Assert.NotNull(results);\n        Assert.Equal(secretIds.OrderBy(x => x),\n            results.Data.Select(x => x.Id).OrderBy(x => x));\n        Assert.All(results.Data, item => Assert.Equal(\"access denied\", item.Error));\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    [InlineData(PermissionType.RunAsServiceAccountWithPermission)]\n    public async Task Delete_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (project, secretIds) = await CreateSecretsAsync(org.Id);\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true\n                }\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        var response = await _client.PostAsJsonAsync(\"/secrets/delete\", secretIds);\n        response.EnsureSuccessStatusCode();\n\n        var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>();\n        Assert.NotNull(results?.Data);\n        Assert.Equal(secretIds.Count, results.Data.Count());\n        foreach (var result in results.Data)\n        {\n            Assert.Contains(result.Id, secretIds);\n            Assert.Null(result.Error);\n        }\n\n        var secrets = await _secretRepository.GetManyByIds(secretIds);\n        Assert.Empty(secrets);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetSecretsByIds_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString,\n        });\n\n        var request = new GetSecretsRequestModel { Ids = new[] { secret.Id } };\n\n        var response = await _client.PostAsJsonAsync(\"/secrets/get-by-ids\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetSecretsByIds_SecretsNotInTheSameOrganization_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n        var otherOrg = await _organizationHelper.CreateSmOrganizationAsync();\n        var (_, secretIds) = await CreateSecretsAsync(org.Id);\n        var (_, diffOrgSecrets) = await CreateSecretsAsync(otherOrg.Id, 1);\n        secretIds.AddRange(diffOrgSecrets);\n\n        var request = new GetSecretsRequestModel { Ids = secretIds };\n\n        var response = await _client.PostAsJsonAsync(\"/secrets/get-by-ids\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public async Task GetSecretsByIds_SecretsNonExistent_NotFound(bool partial)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n        var ids = new List<Guid>();\n\n        if (partial)\n        {\n            var (_, secretIds) = await CreateSecretsAsync(org.Id);\n            ids = secretIds;\n            ids.Add(Guid.NewGuid());\n        }\n\n        var request = new GetSecretsRequestModel { Ids = ids };\n\n        var response = await _client.PostAsJsonAsync(\"/secrets/get-by-ids\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(true, false)]\n    [InlineData(true, true)]\n    [InlineData(false, false)]\n    [InlineData(false, true)]\n    public async Task GetSecretsByIds_NoAccess_NotFound(bool runAsServiceAccount, bool partialAccess)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n\n        var request = await SetupNoAccessRequestAsync(org.Id, runAsServiceAccount, partialAccess);\n\n        var response = await _client.PostAsJsonAsync(\"/secrets/get-by-ids\", request);\n\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    [InlineData(PermissionType.RunAsServiceAccountWithPermission)]\n    public async Task GetSecretsByIds_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n        var request = await SetupGetSecretsByIdsRequestAsync(org.Id, permissionType);\n\n        var response = await _client.PostAsJsonAsync(\"/secrets/get-by-ids\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ListResponseModel<BaseSecretResponseModel>>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.Data);\n        Assert.Equal(request.Ids.Count(), result.Data.Count());\n        Assert.All(result.Data, data => Assert.Equal(_mockEncryptedString, data.Value));\n        Assert.All(result.Data, data => Assert.Equal(_mockEncryptedString, data.Key));\n        Assert.All(result.Data, data => Assert.Equal(_mockEncryptedString, data.Note));\n        Assert.All(result.Data, data => Assert.Equal(org.Id, data.OrganizationId));\n    }\n\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetSecretsByIds_DuplicateIds_BadRequest(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var (project, secretIds) = await CreateSecretsAsync(org.Id);\n\n        secretIds.Add(secretIds[0]);\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,\n                },\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n        else\n        {\n            var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.Admin, true);\n            await _loginHelper.LoginAsync(email);\n        }\n\n        var request = new GetSecretsRequestModel { Ids = secretIds };\n        var response = await _client.PostAsJsonAsync(\"/secrets/get-by-ids\", request);\n        var content = await response.Content.ReadAsStringAsync();\n\n        Assert.True(response.StatusCode == HttpStatusCode.BadRequest);\n        Assert.Contains(\"The following GUIDs were duplicated\", content);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetSecretsSyncAsync_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets,\n        bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/secrets/sync\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetSecretsSyncAsync_UserClient_BadRequest()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/secrets/sync\");\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task GetSecretsSyncAsync_NoSecrets_ReturnsEmptyList(bool useLastSyncedDate)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();\n        await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);\n\n        var requestUrl = $\"/organizations/{org.Id}/secrets/sync\";\n        if (useLastSyncedDate)\n        {\n            requestUrl = $\"/organizations/{org.Id}/secrets/sync?lastSyncedDate={DateTime.UtcNow.AddDays(-1)}\";\n        }\n\n        var response = await _client.GetAsync(requestUrl);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<SecretsSyncResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.True(result.HasChanges);\n        Assert.NotNull(result.Secrets);\n        Assert.Empty(result.Secrets.Data);\n    }\n\n    [Theory]\n    [InlineData(true)]\n    [InlineData(false)]\n    public async Task GetSecretsSyncAsync_HasSecrets_ReturnsAll(bool useLastSyncedDate)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();\n        await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);\n        var secretIds = await SetupSecretsSyncRequestAsync(org.Id, apiKeyDetails.ApiKey.ServiceAccountId!.Value);\n\n        var requestUrl = $\"/organizations/{org.Id}/secrets/sync\";\n        if (useLastSyncedDate)\n        {\n            requestUrl = $\"/organizations/{org.Id}/secrets/sync?lastSyncedDate={DateTime.UtcNow.AddDays(-1)}\";\n        }\n\n        var response = await _client.GetAsync(requestUrl);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<SecretsSyncResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.True(result.HasChanges);\n        Assert.NotNull(result.Secrets);\n        Assert.NotEmpty(result.Secrets.Data);\n        Assert.Equal(secretIds.Count, result.Secrets.Data.Count());\n        Assert.All(result.Secrets.Data, item => Assert.Contains(item.Id, secretIds));\n    }\n\n    [Fact]\n    public async Task GetSecretsSyncAsync_ServiceAccountNotRevised_ReturnsNoChanges()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();\n        var serviceAccountId = apiKeyDetails.ApiKey.ServiceAccountId!.Value;\n        await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);\n        await SetupSecretsSyncRequestAsync(org.Id, serviceAccountId);\n        await UpdateServiceAccountRevisionAsync(serviceAccountId, DateTime.UtcNow.AddDays(-1));\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/secrets/sync?lastSyncedDate={DateTime.UtcNow}\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<SecretsSyncResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.False(result.HasChanges);\n        Assert.Null(result.Secrets);\n    }\n\n    private async Task<(Project Project, List<Guid> secretIds)> CreateSecretsAsync(Guid orgId, int numberToCreate = 3)\n    {\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            Id = new Guid(),\n            OrganizationId = orgId,\n            Name = _mockEncryptedString\n        });\n\n        var secretIds = new List<Guid>();\n        for (var i = 0; i < numberToCreate; i++)\n        {\n            var secret = await _secretRepository.CreateAsync(new Secret\n            {\n                OrganizationId = orgId,\n                Key = _mockEncryptedString,\n                Value = _mockEncryptedString,\n                Note = _mockEncryptedString,\n                Projects = new List<Project>() { project }\n            });\n            secretIds.Add(secret.Id);\n        }\n\n        return (project, secretIds);\n    }\n\n    private async Task SetupProjectPermissionAndLoginAsync(PermissionType permissionType, Project project)\n    {\n        switch (permissionType)\n        {\n            case PermissionType.RunAsAdmin:\n                {\n                    await _loginHelper.LoginAsync(_email);\n                    break;\n                }\n            case PermissionType.RunAsUserWithPermission:\n                {\n                    var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n                    await _loginHelper.LoginAsync(email);\n\n                    var accessPolicies = new List<BaseAccessPolicy>\n                {\n                    new UserProjectAccessPolicy\n                    {\n                        GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,\n                    },\n                };\n                    await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n                    break;\n                }\n            case PermissionType.RunAsServiceAccountWithPermission:\n                {\n                    var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();\n                    await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);\n\n                    var accessPolicies = new List<BaseAccessPolicy>\n                {\n                    new ServiceAccountProjectAccessPolicy\n                    {\n                        GrantedProjectId = project.Id, ServiceAccountId = apiKeyDetails.ApiKey.ServiceAccountId, Read = true, Write = true,\n                    },\n                };\n                    await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n                    break;\n                }\n            default:\n                throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);\n        }\n    }\n\n    private async Task<List<Guid>> SetupSecretsSyncRequestAsync(Guid organizationId, Guid serviceAccountId)\n    {\n        var (project, secretIds) = await CreateSecretsAsync(organizationId);\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new ServiceAccountProjectAccessPolicy\n            {\n                GrantedProjectId = project.Id, ServiceAccountId = serviceAccountId, Read = true, Write = true\n            }\n        };\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        return secretIds;\n    }\n\n    private async Task UpdateServiceAccountRevisionAsync(Guid serviceAccountId, DateTime revisionDate)\n    {\n        var sa = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);\n        sa.RevisionDate = revisionDate;\n        await _serviceAccountRepository.ReplaceAsync(sa);\n    }\n\n    private async Task<(OrganizationUser, SecretCreateRequestModel)> SetupSecretCreateRequestAsync(\n        bool withAccessPolicies)\n    {\n        var (_, organizationUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var request = new SecretCreateRequestModel\n        {\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString\n        };\n\n        if (withAccessPolicies)\n        {\n            request.AccessPoliciesRequests = new SecretAccessPoliciesRequestsModel\n            {\n                UserAccessPolicyRequests =\n                [\n                    new AccessPolicyRequest { GranteeId = organizationUser.Id, Read = true, Write = true }\n                ],\n                GroupAccessPolicyRequests = [],\n                ServiceAccountAccessPolicyRequests = []\n            };\n        }\n\n        return (organizationUser, request);\n    }\n\n    private async Task<(OrganizationUser, SecretCreateRequestModel)> SetupSecretWithProjectCreateRequestAsync(\n        PermissionType permissionType, bool withAccessPolicies)\n    {\n        var (org, orgAdminUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            Id = new Guid(),\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var currentOrganizationUser = orgAdminUser;\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true\n                }\n            };\n            currentOrganizationUser = orgUser;\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        if (permissionType == PermissionType.RunAsServiceAccountWithPermission)\n        {\n            var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();\n            await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new ServiceAccountProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id,\n                    ServiceAccountId = apiKeyDetails.ApiKey.ServiceAccountId,\n                    Read = true,\n                    Write = true\n                }\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        var secretRequest = new SecretCreateRequestModel\n        {\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString,\n            ProjectIds = [project.Id]\n        };\n\n        if (withAccessPolicies)\n        {\n            secretRequest.AccessPoliciesRequests = new SecretAccessPoliciesRequestsModel\n            {\n                UserAccessPolicyRequests =\n                [\n                    new AccessPolicyRequest { GranteeId = currentOrganizationUser.Id, Read = true, Write = true }\n                ],\n                GroupAccessPolicyRequests = [],\n                ServiceAccountAccessPolicyRequests = []\n            };\n        }\n\n        return (currentOrganizationUser, secretRequest);\n    }\n\n    private async Task<(Secret, SecretUpdateRequestModel)> SetupSecretUpdateRequestAsync(PermissionType permissionType,\n        bool withAccessPolicies)\n    {\n        var (org, adminOrgUser) = await _organizationHelper.Initialize(true, true, true);\n        var project = await _projectRepository.CreateAsync(new Project\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        await SetupProjectPermissionAndLoginAsync(permissionType, project);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            Note = _mockEncryptedString,\n            Projects = permissionType != PermissionType.RunAsAdmin ? new List<Project> { project } : null\n        });\n\n        var request = new SecretUpdateRequestModel\n        {\n            Key = _mockEncryptedString,\n            Value =\n                \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\",\n            Note = _mockEncryptedString,\n            ProjectIds = permissionType != PermissionType.RunAsAdmin ? [project.Id] : null\n        };\n\n        if (!withAccessPolicies)\n        {\n            return (secret, request);\n        }\n\n        request.AccessPoliciesRequests = new SecretAccessPoliciesRequestsModel\n        {\n            UserAccessPolicyRequests =\n                [new AccessPolicyRequest { GranteeId = adminOrgUser.Id, Read = true, Write = true }],\n            GroupAccessPolicyRequests = [],\n            ServiceAccountAccessPolicyRequests = []\n        };\n\n        return (secret, request);\n    }\n\n    private async Task<GetSecretsRequestModel> SetupGetSecretsByIdsRequestAsync(Guid organizationId,\n        PermissionType permissionType)\n    {\n        var (project, secretIds) = await CreateSecretsAsync(organizationId);\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new UserProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true\n                }\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        if (permissionType == PermissionType.RunAsServiceAccountWithPermission)\n        {\n            var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();\n            await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);\n\n            var accessPolicies = new List<BaseAccessPolicy>\n            {\n                new ServiceAccountProjectAccessPolicy\n                {\n                    GrantedProjectId = project.Id,\n                    ServiceAccountId = apiKeyDetails.ApiKey.ServiceAccountId,\n                    Read = true,\n                    Write = true\n                }\n            };\n            await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n        }\n\n        return new GetSecretsRequestModel { Ids = secretIds };\n    }\n\n    private async Task<GetSecretsRequestModel> SetupNoAccessRequestAsync(Guid organizationId, bool runAsServiceAccount,\n        bool partialAccess)\n    {\n        var (_, secretIds) = await CreateSecretsAsync(organizationId);\n\n        if (runAsServiceAccount)\n        {\n            var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync();\n            await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails);\n\n            if (partialAccess)\n            {\n                var accessPolicies = new List<BaseAccessPolicy>\n                {\n                    new ServiceAccountSecretAccessPolicy\n                    {\n                        GrantedSecretId = secretIds[0],\n                        ServiceAccountId = apiKeyDetails.ApiKey.ServiceAccountId,\n                        Read = true,\n                        Write = true\n                    }\n                };\n                await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n            }\n        }\n        else\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            if (partialAccess)\n            {\n                var accessPolicies = new List<BaseAccessPolicy>\n                {\n                    new UserSecretAccessPolicy\n                    {\n                        GrantedSecretId = secretIds[0],\n                        OrganizationUserId = orgUser.Id,\n                        Read = true,\n                        Write = true\n                    }\n                };\n                await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n            }\n        }\n\n        return new GetSecretsRequestModel { Ids = secretIds };\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Controllers/SecretsManagerEventsControllerTests.cs",
    "content": "﻿using System.Net;\nusing System.Net.Http.Headers;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.SecretsManager.Helpers;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Controllers;\n\npublic class SecretsManagerEventsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private const string _mockEncryptedString =\n        \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n\n    private string _email = null!;\n    private SecretsManagerOrganizationHelper _organizationHelper = null!;\n\n    public SecretsManagerEventsControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();\n    }\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_email);\n        _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    private async Task LoginAsync(string email)\n    {\n        var tokens = await _factory.LoginAsync(email);\n        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", tokens.Token);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetServiceAccountEvents_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var response = await _client.GetAsync($\"/sm/events/service-accounts/{serviceAccount.Id}\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Controllers/SecretsManagerPortingControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.SecretsManager.Helpers;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Controllers;\n\npublic class SecretsManagerPortingControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private string _email = null!;\n    private SecretsManagerOrganizationHelper _organizationHelper = null!;\n\n    public SecretsManagerPortingControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_email);\n        _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Import_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var projectsList = new List<SMImportRequestModel.InnerProjectImportRequestModel>();\n        var secretsList = new List<SMImportRequestModel.InnerSecretImportRequestModel>();\n        var request = new SMImportRequestModel { Projects = projectsList, Secrets = secretsList };\n\n        var response = await _client.PostAsJsonAsync($\"sm/{org.Id}/import\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Export_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response = await _client.GetAsync($\"sm/{org.Id}/export\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Controllers/SecretsTrashControllerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.SecretsManager.Helpers;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Repositories;\nusing Xunit;\nusing Secret = Bit.Core.SecretsManager.Entities.Secret;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Controllers;\n\npublic class SecretsTrashControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly string _mockEncryptedString =\n        \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly ISecretRepository _secretRepository;\n    private readonly LoginHelper _loginHelper;\n\n    private string _email = null!;\n    private SecretsManagerOrganizationHelper _organizationHelper = null!;\n\n    public SecretsTrashControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _secretRepository = _factory.GetService<ISecretRepository>();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_email);\n        _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task ListByOrganization_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response = await _client.GetAsync($\"/secrets/{org.Id}/trash\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task ListByOrganization_NotAdmin_Unauthorized()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var response = await _client.GetAsync($\"/secrets/{org.Id}/trash\");\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task ListByOrganization_Success()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            DeletedDate = DateTime.Now,\n        });\n\n        await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n        });\n\n        var response = await _client.GetAsync($\"/secrets/{org.Id}/trash\");\n\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<SecretWithProjectsListResponseModel>();\n        Assert.Single(result!.Secrets);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Empty_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var ids = new List<Guid> { Guid.NewGuid() };\n        var response = await _client.PostAsJsonAsync($\"/secrets/{org.Id}/trash/empty\", ids);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Empty_NotAdmin_Unauthorized()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var ids = new List<Guid> { Guid.NewGuid() };\n        var response = await _client.PostAsJsonAsync($\"/secrets/{org.Id}/trash/empty\", ids);\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Empty_Invalid_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString\n        });\n\n        var ids = new List<Guid> { secret.Id };\n        var response = await _client.PostAsJsonAsync($\"/secrets/{org.Id}/trash/empty\", ids);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Empty_Success()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            DeletedDate = DateTime.Now,\n        });\n\n        var ids = new List<Guid> { secret.Id };\n        var response = await _client.PostAsJsonAsync($\"/secrets/{org.Id}/trash/empty\", ids);\n        response.EnsureSuccessStatusCode();\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Restore_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var ids = new List<Guid> { Guid.NewGuid() };\n        var response = await _client.PostAsJsonAsync($\"/secrets/{org.Id}/trash/restore\", ids);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Restore_NotAdmin_Unauthorized()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var ids = new List<Guid> { Guid.NewGuid() };\n        var response = await _client.PostAsJsonAsync($\"/secrets/{org.Id}/trash/restore\", ids);\n        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Restore_Invalid_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString\n        });\n\n        var ids = new List<Guid> { secret.Id };\n        var response = await _client.PostAsJsonAsync($\"/secrets/{org.Id}/trash/restore\", ids);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Restore_Success()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            OrganizationId = org.Id,\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            DeletedDate = DateTime.Now,\n        });\n\n        var ids = new List<Guid> { secret.Id };\n        var response = await _client.PostAsJsonAsync($\"/secrets/{org.Id}/trash/restore\", ids);\n        response.EnsureSuccessStatusCode();\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs",
    "content": "﻿using System.Net;\nusing System.Text.Json.Nodes;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.SecretsManager.Enums;\nusing Bit.Api.IntegrationTest.SecretsManager.Helpers;\nusing Bit.Api.Models.Response;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Controllers;\n\npublic class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private const string _mockEncryptedString =\n        \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    private const string _mockNewName =\n        \"2.3AZ+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n    private readonly LoginHelper _loginHelper;\n\n    private readonly IAccessPolicyRepository _accessPolicyRepository;\n    private readonly IApiKeyRepository _apiKeyRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly IProjectRepository _projectRepository;\n    private readonly ISecretRepository _secretRepository;\n\n    private string _email = null!;\n    private SecretsManagerOrganizationHelper _organizationHelper = null!;\n\n    public ServiceAccountsControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = _factory.CreateClient();\n        _serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();\n        _accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();\n        _apiKeyRepository = _factory.GetService<IApiKeyRepository>();\n        _secretRepository = _factory.GetService<ISecretRepository>();\n        _projectRepository = _factory.GetService<IProjectRepository>();\n        _loginHelper = new LoginHelper(_factory, _client);\n    }\n\n    public async Task InitializeAsync()\n    {\n        _email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_email);\n        _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task ListByOrganization_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var response = await _client.GetAsync($\"/organizations/{org.Id}/service-accounts\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task ListByOrganization_NoSecretAccess_Success(PermissionType permissionType)\n    {\n        var (orgId, serviceAccountIds) = await SetupListByOrganizationRequestAsync(permissionType);\n\n        var response = await _client.GetAsync($\"/organizations/{orgId}/service-accounts\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content\n            .ReadFromJsonAsync<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.Data);\n        Assert.Equal(serviceAccountIds.Count, result.Data.Count());\n        Assert.DoesNotContain(result.Data, x => x.AccessToSecrets != 0);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task ListByOrganization_SecretAccess_Success(PermissionType permissionType)\n    {\n        var (orgId, serviceAccountIds) = await SetupListByOrganizationRequestAsync(permissionType);\n        var expectedAccess = await SetupServiceAccountSecretAccessAsync(serviceAccountIds, orgId);\n\n        var response = await _client.GetAsync($\"/organizations/{orgId}/service-accounts?includeAccessToSecrets=true\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content\n            .ReadFromJsonAsync<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.Data);\n        Assert.Equal(serviceAccountIds.Count, result.Data.Count());\n\n        foreach (var item in expectedAccess)\n        {\n            var serviceAccountResult = result.Data.FirstOrDefault(x => x.Id == item.Key);\n            Assert.NotNull(serviceAccountResult);\n            Assert.Equal(item.Value, serviceAccountResult.AccessToSecrets);\n        }\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public async Task ListByOrganization_UserPartialAccess_ReturnsServiceAccountsUserHasAccessTo(\n        bool includeAccessToSecrets)\n    {\n        var (orgId, serviceAccountIds) =\n            await SetupListByOrganizationRequestAsync(PermissionType.RunAsUserWithPermission);\n        var expectedAccess = await SetupServiceAccountSecretAccessAsync(serviceAccountIds, orgId);\n\n        var serviceAccountWithoutAccess = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = orgId,\n            Name = _mockEncryptedString\n        });\n\n        var response =\n            await _client.GetAsync(\n                $\"/organizations/{orgId}/service-accounts?includeAccessToSecrets={includeAccessToSecrets}\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content\n            .ReadFromJsonAsync<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>>();\n\n        Assert.NotNull(result);\n        Assert.NotEmpty(result.Data);\n        Assert.Equal(serviceAccountIds.Count, result.Data.Count());\n        Assert.DoesNotContain(result.Data, x => x.Id == serviceAccountWithoutAccess.Id);\n\n        if (includeAccessToSecrets)\n        {\n            foreach (var item in expectedAccess)\n            {\n                var serviceAccountResult = result.Data.FirstOrDefault(x => x.Id == item.Key);\n                Assert.NotNull(serviceAccountResult);\n                Assert.Equal(item.Value, serviceAccountResult.AccessToSecrets);\n            }\n        }\n        else\n        {\n            Assert.Contains(result.Data, x => x.AccessToSecrets == 0);\n        }\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetByServiceAccountId_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetByServiceAccountId_ServiceAccountDoesNotExist_NotFound()\n    {\n        await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var response = await _client.GetAsync($\"/service-accounts/{new Guid()}\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetByServiceAccountId_UserWithoutPermission_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetByServiceAccountId_Success(PermissionType permissionType)\n    {\n        var serviceAccount = await SetupServiceAccountWithAccessAsync(permissionType);\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}\");\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();\n        Assert.NotNull(result);\n        Assert.Equal(serviceAccount.Id, result.Id);\n        Assert.Equal(serviceAccount.OrganizationId, result.OrganizationId);\n        Assert.Equal(serviceAccount.Name, result.Name);\n        Assert.Equal(serviceAccount.CreationDate, result.CreationDate);\n        Assert.Equal(serviceAccount.RevisionDate, result.RevisionDate);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Create_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var request = new ServiceAccountCreateRequestModel { Name = _mockEncryptedString };\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{org.Id}/service-accounts\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task Create_Success(PermissionType permissionType)\n    {\n        var (org, adminOrgUser) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var orgUserId = adminOrgUser.Id;\n        var currentUserId = adminOrgUser.UserId!.Value;\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n            orgUserId = orgUser.Id;\n            currentUserId = orgUser.UserId!.Value;\n        }\n\n        var request = new ServiceAccountCreateRequestModel { Name = _mockEncryptedString };\n\n        var response = await _client.PostAsJsonAsync($\"/organizations/{org.Id}/service-accounts\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, result.Name);\n        AssertHelper.AssertRecent(result.RevisionDate);\n        AssertHelper.AssertRecent(result.CreationDate);\n\n        var createdServiceAccount = await _serviceAccountRepository.GetByIdAsync(result.Id);\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, createdServiceAccount.Name);\n        AssertHelper.AssertRecent(createdServiceAccount.RevisionDate);\n        AssertHelper.AssertRecent(createdServiceAccount.CreationDate);\n\n        // Check permissions have been bootstrapped.\n        var accessPolicies = await _accessPolicyRepository.GetPeoplePoliciesByGrantedServiceAccountIdAsync(createdServiceAccount.Id, currentUserId);\n        Assert.NotNull(accessPolicies);\n        var ap = (UserServiceAccountAccessPolicy)accessPolicies.First();\n        Assert.Equal(createdServiceAccount.Id, ap.GrantedServiceAccountId);\n        Assert.Equal(orgUserId, ap.OrganizationUserId);\n        Assert.True(ap.Read);\n        Assert.True(ap.Write);\n        AssertHelper.AssertRecent(ap.CreationDate);\n        AssertHelper.AssertRecent(ap.RevisionDate);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Update_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };\n\n        var response = await _client.PutAsJsonAsync($\"/service-accounts/{initialServiceAccount.Id}\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Update_User_NoPermissions()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };\n\n        var response = await _client.PutAsJsonAsync($\"/service-accounts/{initialServiceAccount.Id}\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Update_NonExistingServiceAccount_NotFound()\n    {\n        await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };\n\n        var response = await _client.PutAsJsonAsync(\"/service-accounts/c53de509-4581-402c-8cbd-f26d2c516fba\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task Update_Success(PermissionType permissionType)\n    {\n        var initialServiceAccount = await SetupServiceAccountWithAccessAsync(permissionType);\n\n        var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };\n\n        var response = await _client.PutAsJsonAsync($\"/service-accounts/{initialServiceAccount.Id}\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, result.Name);\n        Assert.NotEqual(initialServiceAccount.Name, result.Name);\n        AssertHelper.AssertRecent(result.RevisionDate);\n        Assert.NotEqual(initialServiceAccount.RevisionDate, result.RevisionDate);\n\n        var updatedServiceAccount = await _serviceAccountRepository.GetByIdAsync(initialServiceAccount.Id);\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, updatedServiceAccount.Name);\n        AssertHelper.AssertRecent(updatedServiceAccount.RevisionDate);\n        AssertHelper.AssertRecent(updatedServiceAccount.CreationDate);\n        Assert.NotEqual(initialServiceAccount.Name, updatedServiceAccount.Name);\n        Assert.NotEqual(initialServiceAccount.RevisionDate, updatedServiceAccount.RevisionDate);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task Delete_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var request = new List<Guid> { initialServiceAccount.Id };\n\n        var response = await _client.PutAsJsonAsync(\"/service-accounts/delete\", request);\n        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task Delete_MissingAccessPolicy_AccessDenied()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var ids = new List<Guid> { serviceAccount.Id };\n\n        var response = await _client.PostAsJsonAsync(\"/service-accounts/delete\", ids);\n\n        var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>();\n        Assert.NotNull(results);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task Delete_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        await _apiKeyRepository.CreateAsync(CreateTestApiKey(serviceAccount.Id));\n\n        if (permissionType == PermissionType.RunAsAdmin)\n        {\n            await _loginHelper.LoginAsync(_email);\n        }\n        else\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> {\n                new UserServiceAccountAccessPolicy\n                {\n                    GrantedServiceAccountId = serviceAccount.Id,\n                    OrganizationUserId = orgUser.Id,\n                    Write = true,\n                    Read = true,\n                },\n            });\n        }\n\n        var ids = new List<Guid> { serviceAccount.Id };\n\n        var response = await _client.PostAsJsonAsync(\"/service-accounts/delete\", ids);\n        response.EnsureSuccessStatusCode();\n\n        var sa = await _serviceAccountRepository.GetManyByIds(ids);\n        Assert.Empty(sa);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task GetAccessTokens_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task GetAccessTokens_UserNoPermission_NotFound()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        await _apiKeyRepository.CreateAsync(\n            CreateTestApiKey(serviceAccount.Id, _mockEncryptedString, DateTime.UtcNow.AddDays(30))\n        );\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens\");\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetAccessTokens_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        if (permissionType == PermissionType.RunAsUserWithPermission)\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> {\n                new UserServiceAccountAccessPolicy\n                {\n                    GrantedServiceAccountId = serviceAccount.Id,\n                    OrganizationUserId = orgUser.Id,\n                    Write = true,\n                    Read = true,\n                },\n            });\n        }\n\n        var accessToken = await _apiKeyRepository.CreateAsync(\n            CreateTestApiKey(serviceAccount.Id, _mockEncryptedString, DateTime.UtcNow.AddDays(30))\n        );\n\n\n        var response = await _client.GetAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens\");\n        response.EnsureSuccessStatusCode();\n        var results = await response.Content.ReadFromJsonAsync<ListResponseModel<AccessTokenResponseModel>>();\n        Assert.NotEmpty(results!.Data);\n        Assert.Equal(accessToken.Id, results.Data.First().Id);\n        Assert.Equal(accessToken.Name, results.Data.First().Name);\n        Assert.Equal(accessToken.GetScopes(), results.Data.First().Scopes);\n        Assert.Equal(accessToken.ExpireAt, results.Data.First().ExpireAt);\n        Assert.Equal(accessToken.CreationDate, results.Data.First().CreationDate);\n        Assert.Equal(accessToken.RevisionDate, results.Data.First().RevisionDate);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task CreateAccessToken_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var mockExpiresAt = DateTime.UtcNow.AddDays(30);\n        var request = new AccessTokenCreateRequestModel\n        {\n            Name = _mockEncryptedString,\n            EncryptedPayload = _mockEncryptedString,\n            Key = _mockEncryptedString,\n            ExpireAt = mockExpiresAt,\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task CreateAccessToken_Admin()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var mockExpiresAt = DateTime.UtcNow.AddDays(30);\n        var request = new AccessTokenCreateRequestModel\n        {\n            Name = _mockEncryptedString,\n            EncryptedPayload = _mockEncryptedString,\n            Key = _mockEncryptedString,\n            ExpireAt = mockExpiresAt,\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<AccessTokenCreationResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, result.Name);\n        Assert.NotNull(result.ClientSecret);\n        Assert.Equal(mockExpiresAt, result.ExpireAt);\n        AssertHelper.AssertRecent(result.RevisionDate);\n        AssertHelper.AssertRecent(result.CreationDate);\n    }\n\n    [Fact]\n    public async Task CreateAccessToken_User_WithPermission()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        await CreateUserPolicyAsync(orgUser.Id, serviceAccount.Id, true, true);\n\n        var mockExpiresAt = DateTime.UtcNow.AddDays(30);\n        var request = new AccessTokenCreateRequestModel\n        {\n            Name = _mockEncryptedString,\n            EncryptedPayload = _mockEncryptedString,\n            Key = _mockEncryptedString,\n            ExpireAt = mockExpiresAt,\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<AccessTokenCreationResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, result.Name);\n        Assert.NotNull(result.ClientSecret);\n        Assert.Equal(mockExpiresAt, result.ExpireAt);\n        AssertHelper.AssertRecent(result.RevisionDate);\n        AssertHelper.AssertRecent(result.CreationDate);\n    }\n\n    [Fact]\n    public async Task CreateAccessToken_User_NoPermission()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var mockExpiresAt = DateTime.UtcNow.AddDays(30);\n        var request = new AccessTokenCreateRequestModel\n        {\n            Name = _mockEncryptedString,\n            EncryptedPayload = _mockEncryptedString,\n            Key = _mockEncryptedString,\n            ExpireAt = mockExpiresAt,\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Fact]\n    public async Task CreateAccessToken_ExpireAtNull_Admin()\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        var request = new AccessTokenCreateRequestModel\n        {\n            Name = _mockEncryptedString,\n            EncryptedPayload = _mockEncryptedString,\n            Key = _mockEncryptedString,\n            ExpireAt = null,\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens\", request);\n        response.EnsureSuccessStatusCode();\n        var result = await response.Content.ReadFromJsonAsync<AccessTokenCreationResponseModel>();\n\n        Assert.NotNull(result);\n        Assert.Equal(request.Name, result.Name);\n        Assert.NotNull(result.ClientSecret);\n        Assert.Null(result.ExpireAt);\n        AssertHelper.AssertRecent(result.RevisionDate);\n        AssertHelper.AssertRecent(result.CreationDate);\n    }\n\n    [Theory]\n    [InlineData(false, false, false)]\n    [InlineData(false, false, true)]\n    [InlineData(false, true, false)]\n    [InlineData(false, true, true)]\n    [InlineData(true, false, false)]\n    [InlineData(true, false, true)]\n    [InlineData(true, true, false)]\n    public async Task RevokeAccessToken_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled)\n    {\n        var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString\n        });\n\n        var accessToken = await _apiKeyRepository.CreateAsync(\n            CreateTestApiKey(serviceAccount.Id, _mockEncryptedString, DateTime.UtcNow.AddDays(30))\n        );\n\n        var request = new RevokeAccessTokensRequest\n        {\n            Ids = new[] { accessToken.Id },\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens/revoke\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(false)]\n    [InlineData(true)]\n    public async Task RevokeAccessToken_User_NoPermission(bool hasReadAccess)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        if (hasReadAccess)\n        {\n            await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> {\n                new UserServiceAccountAccessPolicy\n                {\n                    GrantedServiceAccountId = serviceAccount.Id,\n                    OrganizationUserId = orgUser.Id,\n                    Write = false,\n                    Read = true,\n                },\n            });\n        }\n\n        var accessToken = await _apiKeyRepository.CreateAsync(\n            CreateTestApiKey(serviceAccount.Id, _mockEncryptedString, DateTime.UtcNow.AddDays(30))\n        );\n\n        var request = new RevokeAccessTokensRequest\n        {\n            Ids = new[] { accessToken.Id },\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens/revoke\", request);\n        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);\n    }\n\n    [Theory]\n    [InlineData(PermissionType.RunAsAdmin)]\n    [InlineData(PermissionType.RunAsUserWithPermission)]\n    public async Task RevokeAccessToken_Success(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n\n        var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        if (permissionType == PermissionType.RunAsAdmin)\n        {\n            await _loginHelper.LoginAsync(_email);\n        }\n        else\n        {\n            var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n            await _loginHelper.LoginAsync(email);\n\n            await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> {\n                new UserServiceAccountAccessPolicy\n                {\n                    GrantedServiceAccountId = serviceAccount.Id,\n                    OrganizationUserId = orgUser.Id,\n                    Write = true,\n                    Read = true,\n                },\n            });\n        }\n\n        var accessToken = await _apiKeyRepository.CreateAsync(\n            CreateTestApiKey(serviceAccount.Id, _mockEncryptedString, DateTime.UtcNow.AddDays(30))\n        );\n\n        var request = new RevokeAccessTokensRequest\n        {\n            Ids = new[] { accessToken.Id },\n        };\n\n        var response = await _client.PostAsJsonAsync($\"/service-accounts/{serviceAccount.Id}/access-tokens/revoke\", request);\n        response.EnsureSuccessStatusCode();\n    }\n\n    private async Task CreateUserPolicyAsync(Guid userId, Guid serviceAccountId, bool read, bool write)\n    {\n        var policy = new UserServiceAccountAccessPolicy\n        {\n            OrganizationUserId = userId,\n            GrantedServiceAccountId = serviceAccountId,\n            Read = read,\n            Write = write,\n        };\n        await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { policy });\n    }\n\n    private async Task<List<Guid>> CreateServiceAccountsInOrganizationAsync(Organization org)\n    {\n        var serviceAccountIds = new List<Guid>();\n        for (var i = 0; i < 4; i++)\n        {\n            var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n            {\n                OrganizationId = org.Id,\n                Name = _mockEncryptedString,\n            });\n            serviceAccountIds.Add(serviceAccount.Id);\n        }\n\n        return serviceAccountIds;\n    }\n\n    private async Task<ServiceAccount> SetupServiceAccountWithAccessAsync(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount\n        {\n            OrganizationId = org.Id,\n            Name = _mockEncryptedString,\n        });\n\n        if (permissionType == PermissionType.RunAsAdmin)\n        {\n            return initialServiceAccount;\n        }\n\n        var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new UserServiceAccountAccessPolicy\n            {\n                GrantedServiceAccountId = initialServiceAccount.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,\n            },\n        };\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n\n        return initialServiceAccount;\n    }\n\n    private async Task<(Guid OrganizationId, List<Guid> ServiceAccountIds)> SetupListByOrganizationRequestAsync(PermissionType permissionType)\n    {\n        var (org, _) = await _organizationHelper.Initialize(true, true, true);\n        await _loginHelper.LoginAsync(_email);\n\n        var serviceAccountIds = await CreateServiceAccountsInOrganizationAsync(org);\n\n        if (permissionType == PermissionType.RunAsAdmin)\n        {\n            return (org.Id, serviceAccountIds);\n        }\n\n        var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);\n        await _loginHelper.LoginAsync(email);\n\n        var accessPolicies = serviceAccountIds.Select(\n            id => new UserServiceAccountAccessPolicy\n            {\n                OrganizationUserId = orgUser.Id,\n                GrantedServiceAccountId = id,\n                Read = true,\n                Write = false,\n            }).Cast<BaseAccessPolicy>().ToList();\n\n        await _accessPolicyRepository.CreateManyAsync(accessPolicies);\n\n        return (org.Id, serviceAccountIds);\n    }\n\n    private static ApiKey CreateTestApiKey(Guid serviceAccountId, string name = _mockEncryptedString, DateTime? expiresAt = null)\n    {\n        return new ApiKey\n        {\n            ServiceAccountId = serviceAccountId,\n            Name = name,\n            ExpireAt = expiresAt,\n            Scope = new JsonArray\n            {\n                \"api.secrets\",\n            }.ToJsonString(),\n            EncryptedPayload = _mockEncryptedString,\n            Key = _mockEncryptedString,\n        };\n    }\n\n    private async Task<Dictionary<Guid, int>> SetupServiceAccountSecretAccessAsync(List<Guid> serviceAccountIds,\n        Guid organizationId)\n    {\n        var project =\n            await _projectRepository.CreateAsync(new Project\n            {\n                Name = _mockEncryptedString,\n                OrganizationId = organizationId\n            });\n\n        var secret = await _secretRepository.CreateAsync(new Secret\n        {\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            OrganizationId = organizationId,\n            Projects = [project]\n        });\n\n        var secretNoProject = await _secretRepository.CreateAsync(new Secret\n        {\n            Key = _mockEncryptedString,\n            Value = _mockEncryptedString,\n            OrganizationId = organizationId\n        });\n\n        var serviceAccountWithProjectAccess = serviceAccountIds[0];\n        var serviceAccountWithProjectAndSecretAccess = serviceAccountIds[1];\n        var serviceAccountWithSecretAccess = serviceAccountIds[2];\n        var serviceAccountWithNoAccess = serviceAccountIds[3];\n        await _accessPolicyRepository.CreateManyAsync([\n            new ServiceAccountProjectAccessPolicy\n            {\n                ServiceAccountId = serviceAccountWithProjectAccess,\n                GrantedProjectId = project.Id,\n                Read = true,\n                Write = true\n            },\n            new ServiceAccountProjectAccessPolicy\n            {\n                ServiceAccountId = serviceAccountWithProjectAndSecretAccess,\n                GrantedProjectId = project.Id,\n                Read = true,\n                Write = true\n            },\n            new ServiceAccountSecretAccessPolicy\n            {\n                ServiceAccountId = serviceAccountWithProjectAndSecretAccess,\n                GrantedSecretId = secret.Id,\n                Read = true,\n                Write = true\n            },\n            new ServiceAccountSecretAccessPolicy\n            {\n                ServiceAccountId = serviceAccountWithProjectAndSecretAccess,\n                GrantedSecretId = secretNoProject.Id,\n                Read = true,\n                Write = true\n            },\n            new ServiceAccountSecretAccessPolicy\n            {\n                ServiceAccountId = serviceAccountWithSecretAccess,\n                GrantedSecretId = secretNoProject.Id,\n                Read = true,\n                Write = true\n            }\n        ]);\n\n        return new Dictionary<Guid, int>\n        {\n            { serviceAccountWithProjectAccess, 1 },\n            { serviceAccountWithProjectAndSecretAccess, 2 },\n            { serviceAccountWithSecretAccess, 1 },\n            { serviceAccountWithNoAccess, 0 }\n        };\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Enums/PermissionType.cs",
    "content": "﻿namespace Bit.Api.IntegrationTest.SecretsManager.Enums;\n\npublic enum PermissionType\n{\n    RunAsAdmin,\n    RunAsUserWithPermission,\n    RunAsServiceAccountWithPermission,\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Helpers/LoginHelper.cs",
    "content": "﻿using System.Net.Http.Headers;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Core.SecretsManager.Models.Data;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Helpers;\n\npublic class LoginHelper\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n\n    public LoginHelper(ApiApplicationFactory factory, HttpClient client)\n    {\n        _factory = factory;\n        _client = client;\n    }\n\n    public async Task LoginAsync(string email)\n    {\n        var tokens = await _factory.LoginAsync(email);\n        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", tokens.Token);\n    }\n\n    public async Task LoginWithApiKeyAsync(ApiKeyClientSecretDetails apiKeyDetails)\n    {\n        var token = await _factory.LoginWithClientSecretAsync(apiKeyDetails.ApiKey.Id, apiKeyDetails.ClientSecret);\n        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", token);\n        _client.DefaultRequestHeaders.Add(\"service_account_id\", apiKeyDetails.ApiKey.ServiceAccountId.ToString());\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/SecretsManager/Helpers/SecretsManagerOrganizationHelper.cs",
    "content": "﻿using System.Diagnostics;\nusing Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Repositories;\n\nnamespace Bit.Api.IntegrationTest.SecretsManager.Helpers;\n\npublic class SecretsManagerOrganizationHelper\n{\n    private readonly ApiApplicationFactory _factory;\n    private readonly string _ownerEmail;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IServiceAccountRepository _serviceAccountRepository;\n    private readonly ICreateAccessTokenCommand _createAccessTokenCommand;\n\n    private Organization _organization = null!;\n    private OrganizationUser _owner = null!;\n\n    public SecretsManagerOrganizationHelper(ApiApplicationFactory factory, string ownerEmail)\n    {\n        _factory = factory;\n        _organizationRepository = factory.GetService<IOrganizationRepository>();\n        _organizationUserRepository = factory.GetService<IOrganizationUserRepository>();\n        _ownerEmail = ownerEmail;\n        _serviceAccountRepository = factory.GetService<IServiceAccountRepository>();\n        _createAccessTokenCommand = factory.GetService<ICreateAccessTokenCommand>();\n    }\n\n    public async Task<(Organization organization, OrganizationUser owner)> Initialize(bool useSecrets, bool ownerAccessSecrets, bool organizationEnabled)\n    {\n        (_organization, _owner!) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: _ownerEmail, billingEmail: _ownerEmail);\n        Debug.Assert(_owner is not null);\n\n        if (useSecrets || !organizationEnabled)\n        {\n            if (useSecrets)\n            {\n                _organization.UseSecretsManager = true;\n            }\n\n            if (!organizationEnabled)\n            {\n                _organization.Enabled = false;\n            }\n\n            await _organizationRepository.ReplaceAsync(_organization);\n        }\n\n        if (ownerAccessSecrets)\n        {\n            _owner.AccessSecretsManager = ownerAccessSecrets;\n            await _organizationUserRepository.ReplaceAsync(_owner);\n        }\n\n        return (_organization, _owner);\n    }\n\n    public async Task<Organization> CreateSmOrganizationAsync()\n    {\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: email, billingEmail: email);\n        return organization;\n    }\n\n    public async Task<(string email, OrganizationUser orgUser)> CreateNewUser(OrganizationUserType userType, bool accessSecrets)\n    {\n        var email = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(email);\n        var orgUser = await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email, userType, accessSecrets);\n\n        return (email, orgUser);\n    }\n\n    public async Task<ApiKeyClientSecretDetails> CreateNewServiceAccountApiKeyAsync()\n    {\n        var serviceAccountId = Guid.NewGuid();\n        var serviceAccount = new ServiceAccount\n        {\n            Id = serviceAccountId,\n            OrganizationId = _organization.Id,\n            Name = $\"integration-test-{serviceAccountId}sa\",\n            CreationDate = DateTime.UtcNow,\n            RevisionDate = DateTime.UtcNow\n        };\n        await _serviceAccountRepository.CreateAsync(serviceAccount);\n\n        var apiKey = new ApiKey\n        {\n            ServiceAccountId = serviceAccountId,\n            Name = \"integration-token\",\n            Key = Guid.NewGuid().ToString(),\n            ExpireAt = null,\n            Scope = \"[\\\"api.secrets\\\"]\",\n            EncryptedPayload = Guid.NewGuid().ToString()\n        };\n        return await _createAccessTokenCommand.CreateAsync(apiKey);\n    }\n}\n"
  },
  {
    "path": "test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs",
    "content": "﻿using Bit.Api.IntegrationTest.Factories;\nusing Bit.Api.IntegrationTest.Helpers;\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.IntegrationTest.Vault.Controllers;\n\npublic class SyncControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime\n{\n    private readonly HttpClient _client;\n    private readonly ApiApplicationFactory _factory;\n\n    private readonly LoginHelper _loginHelper;\n\n    private readonly IUserRepository _userRepository;\n    private string _ownerEmail = null!;\n\n    public SyncControllerTests(ApiApplicationFactory factory)\n    {\n        _factory = factory;\n        _client = factory.CreateClient();\n        _loginHelper = new LoginHelper(_factory, _client);\n        _userRepository = _factory.GetService<IUserRepository>();\n    }\n\n    public async Task InitializeAsync()\n    {\n        _ownerEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(_ownerEmail);\n    }\n\n    public Task DisposeAsync()\n    {\n        _client.Dispose();\n        return Task.CompletedTask;\n    }\n\n    [Fact]\n    // [BitAutoData]\n    public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull()\n    {\n        var tempEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(tempEmail);\n        await _loginHelper.LoginAsync(tempEmail);\n\n        // Remove user's password.\n        var user = await _userRepository.GetByEmailAsync(tempEmail);\n        Assert.NotNull(user);\n        user.MasterPassword = null;\n        await _userRepository.UpsertAsync(user);\n\n        var response = await _client.GetAsync(\"/sync\");\n        response.EnsureSuccessStatusCode();\n\n        var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();\n\n        Assert.NotNull(syncResponseModel);\n        Assert.NotNull(syncResponseModel.UserDecryption);\n        Assert.Null(syncResponseModel.UserDecryption.MasterPasswordUnlock);\n    }\n\n    [Theory]\n    [BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]\n    [BitAutoData(KdfType.Argon2id, 11, 128, 5)]\n    public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull(\n        KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        var tempEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(tempEmail);\n        await _loginHelper.LoginAsync(tempEmail);\n\n        // Change KDF settings\n        var user = await _userRepository.GetByEmailAsync(tempEmail);\n        Assert.NotNull(user);\n        user.Kdf = kdfType;\n        user.KdfIterations = kdfIterations;\n        user.KdfMemory = kdfMemory;\n        user.KdfParallelism = kdfParallelism;\n        await _userRepository.UpsertAsync(user);\n\n        var response = await _client.GetAsync(\"/sync\");\n        response.EnsureSuccessStatusCode();\n\n        var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();\n\n        Assert.NotNull(syncResponseModel);\n        Assert.NotNull(syncResponseModel.UserDecryption);\n        Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock);\n        Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf);\n        Assert.Equal(kdfType, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);\n        Assert.Equal(kdfIterations, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);\n        Assert.Equal(kdfMemory, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Memory);\n        Assert.Equal(kdfParallelism, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);\n        Assert.Equal(user.Key, syncResponseModel.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);\n        Assert.Equal(user.GetMasterPasswordSalt(), syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt);\n    }\n\n    [Fact]\n    public async Task Get_HaveExplicitMasterPasswordSalt_SaltReturnedInSync()\n    {\n        var tempEmail = $\"integration-test{Guid.NewGuid()}@bitwarden.com\";\n        await _factory.LoginWithNewAccount(tempEmail);\n        await _loginHelper.LoginAsync(tempEmail);\n\n        var user = await _userRepository.GetByEmailAsync(tempEmail);\n        Assert.NotNull(user);\n        user.MasterPasswordSalt = \"explicit-salt-value\";\n        await _userRepository.UpsertAsync(user);\n\n        var response = await _client.GetAsync(\"/sync\");\n        response.EnsureSuccessStatusCode();\n\n        var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();\n\n        Assert.NotNull(syncResponseModel);\n        Assert.NotNull(syncResponseModel.UserDecryption?.MasterPasswordUnlock);\n        Assert.Equal(\"explicit-salt-value\", syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Bit.Api.AdminConsole.Authorization;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Routing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Authorization;\n\npublic class HttpContextExtensionsTests\n{\n    [Fact]\n    public async Task WithFeaturesCacheAsync_OnlyExecutesCallbackOnce()\n    {\n        var httpContext = new DefaultHttpContext();\n        var callback = Substitute.For<Func<Task<string>>>();\n        callback().Returns(Task.FromResult(\"hello world\"));\n\n        // Call once\n        var result1 = await httpContext.WithFeaturesCacheAsync(callback);\n        Assert.Equal(\"hello world\", result1);\n        await callback.ReceivedWithAnyArgs(1).Invoke();\n\n        // Call again - callback not executed again\n        var result2 = await httpContext.WithFeaturesCacheAsync(callback);\n        Assert.Equal(\"hello world\", result2);\n        await callback.ReceivedWithAnyArgs(1).Invoke();\n    }\n\n    [Theory]\n    [InlineAutoData(\"orgId\")]\n    [InlineAutoData(\"organizationId\")]\n    public void GetOrganizationId_GivenValidParameter_ReturnsOrganizationId(string paramName, Guid orgId)\n    {\n        var httpContext = new DefaultHttpContext\n        {\n            Request = { RouteValues = new RouteValueDictionary\n            {\n                { \"userId\", \"someGuid\" },\n                { paramName, orgId.ToString() }\n            }\n        }\n        };\n\n        var result = httpContext.GetOrganizationId();\n        Assert.Equal(orgId, result);\n    }\n\n    [Theory]\n    [InlineAutoData(\"orgId\")]\n    [InlineAutoData(\"organizationId\")]\n    [InlineAutoData(\"missingParameter\")]\n    public void GetOrganizationId_GivenMissingOrInvalidGuid_Throws(string paramName)\n    {\n        var httpContext = new DefaultHttpContext\n        {\n            Request = { RouteValues = new RouteValueDictionary\n            {\n                { \"userId\", \"someGuid\" },\n                { paramName, \"invalidGuid\" }\n            }\n        }\n        };\n\n        var exception = Assert.Throws<InvalidOperationException>(() => httpContext.GetOrganizationId());\n        Assert.Equal(HttpContextExtensions.NoOrgIdError, exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Authorization/OrganizationClaimsExtensionsTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.AdminConsole.Helpers;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Authorization;\n\npublic class OrganizationClaimsExtensionsTests\n{\n    [Theory, BitMemberAutoData(nameof(GetTestOrganizations))]\n    public void GetCurrentContextOrganization_ParsesOrganizationFromClaims(CurrentContextOrganization expected, User user)\n    {\n        var claims = CoreHelpers.BuildIdentityClaims(user, [expected], [], false)\n            .Select(c => new Claim(c.Key, c.Value));\n\n        var claimsPrincipal = new ClaimsPrincipal();\n        claimsPrincipal.AddIdentities([new ClaimsIdentity(claims)]);\n\n        var actual = claimsPrincipal.GetCurrentContextOrganization(expected.Id);\n\n        AssertHelper.AssertPropertyEqual(expected, actual);\n    }\n\n    public static IEnumerable<object[]> GetTestOrganizations()\n    {\n        var roles = new List<OrganizationUserType> { OrganizationUserType.Owner, OrganizationUserType.Admin, OrganizationUserType.User };\n        foreach (var role in roles)\n        {\n            yield return\n            [\n                new CurrentContextOrganization\n                {\n                    Id = Guid.NewGuid(),\n                    Type = role,\n                    AccessSecretsManager = true\n                }\n            ];\n        }\n\n        var permissions = PermissionsHelpers.GetAllPermissions();\n        foreach (var permission in permissions)\n        {\n            yield return\n            [\n                new CurrentContextOrganization\n                {\n                    Id = Guid.NewGuid(),\n                    Type = OrganizationUserType.Custom,\n                    Permissions = permission\n                }\n            ];\n        }\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Authorization/OrganizationContextTests.cs",
    "content": "﻿using System.Security.Claims;\nusing AutoFixture;\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Authorization;\n\n[SutProviderCustomize]\npublic class OrganizationContextTests\n{\n    [Theory, BitAutoData]\n    public async Task IsProviderUserForOrganization_UserIsProviderUser_ReturnsTrue(\n        Guid userId, Guid organizationId, Guid otherOrganizationId,\n        SutProvider<OrganizationContext> sutProvider)\n    {\n        var claimsPrincipal = new ClaimsPrincipal();\n        var providerUserOrganizations = new List<ProviderUserOrganizationDetails>\n        {\n            new() { OrganizationId = organizationId },\n            new() { OrganizationId = otherOrganizationId }\n        };\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(claimsPrincipal)\n            .Returns(userId);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)\n            .Returns(providerUserOrganizations);\n\n        var result = await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);\n\n        Assert.True(result);\n        await sutProvider.GetDependency<IProviderUserRepository>()\n            .Received(1)\n            .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed);\n    }\n\n    public static IEnumerable<object[]> UserIsNotProviderUserData()\n    {\n        // User has provider organizations, but not for the target organization\n        yield return\n        [\n            new List<ProviderUserOrganizationDetails>\n            {\n                new Fixture().Create<ProviderUserOrganizationDetails>()\n            }\n        ];\n\n        // User has no provider organizations\n        yield return [Array.Empty<ProviderUserOrganizationDetails>()];\n    }\n\n    [Theory, BitMemberAutoData(nameof(UserIsNotProviderUserData))]\n    public async Task IsProviderUserForOrganization_UserIsNotProviderUser_ReturnsFalse(\n        IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizations,\n        Guid userId, Guid organizationId,\n        SutProvider<OrganizationContext> sutProvider)\n    {\n        var claimsPrincipal = new ClaimsPrincipal();\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(claimsPrincipal)\n            .Returns(userId);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)\n            .Returns(providerUserOrganizations);\n\n        var result = await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);\n\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsProviderUserForOrganization_UserIdIsNull_ThrowsException(\n        Guid organizationId,\n        SutProvider<OrganizationContext> sutProvider)\n    {\n        var claimsPrincipal = new ClaimsPrincipal();\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(claimsPrincipal)\n            .Returns((Guid?)null);\n\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>\n            sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId));\n\n        Assert.Equal(OrganizationContext.NoUserIdError, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsProviderUserForOrganization_UsesCaching(\n        Guid userId, Guid organizationId,\n        SutProvider<OrganizationContext> sutProvider)\n    {\n        var claimsPrincipal = new ClaimsPrincipal();\n        var providerUserOrganizations = new List<ProviderUserOrganizationDetails>\n        {\n            new() { OrganizationId = organizationId }\n        };\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(claimsPrincipal)\n            .Returns(userId);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed)\n            .Returns(providerUserOrganizations);\n\n        await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);\n        await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId);\n\n        await sutProvider.GetDependency<IProviderUserRepository>()\n            .Received(1)\n            .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Authorization/OrganizationRequirementHandlerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Http;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Authorization;\n\n[SutProviderCustomize]\npublic class OrganizationRequirementHandlerTests\n{\n    [Theory]\n    [BitAutoData((string)null)]\n    [BitAutoData(\"malformed guid\")]\n    public async Task IfInvalidOrganizationId_Throws(string orgId, Guid userId, SutProvider<OrganizationRequirementHandler> sutProvider)\n    {\n        // Arrange\n        ArrangeRouteAndUser(sutProvider, orgId, userId);\n        var testRequirement = Substitute.For<IOrganizationRequirement>();\n        var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);\n\n        // Act\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => sutProvider.Sut.HandleAsync(authContext));\n        Assert.Contains(HttpContextExtensions.NoOrgIdError, exception.Message);\n        Assert.False(authContext.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IfHttpContextIsNull_Throws(SutProvider<OrganizationRequirementHandler> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IHttpContextAccessor>().HttpContext = null;\n        var testRequirement = Substitute.For<IOrganizationRequirement>();\n        var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);\n\n        // Act\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => sutProvider.Sut.HandleAsync(authContext));\n        Assert.Contains(OrganizationRequirementHandler.NoHttpContextError, exception.Message);\n        Assert.False(authContext.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IfUserIdIsNull_Throws(Guid orgId, SutProvider<OrganizationRequirementHandler> sutProvider)\n    {\n        // Arrange\n        ArrangeRouteAndUser(sutProvider, orgId.ToString(), null);\n        var testRequirement = Substitute.For<IOrganizationRequirement>();\n        var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);\n\n        // Act\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => sutProvider.Sut.HandleAsync(authContext));\n        Assert.Contains(OrganizationRequirementHandler.NoUserIdError, exception.Message);\n        Assert.False(authContext.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DoesNotAuthorize_IfAuthorizeAsync_ReturnsFalse(\n        SutProvider<OrganizationRequirementHandler> sutProvider, Guid organizationId, Guid userId)\n    {\n        // Arrange route values\n        ArrangeRouteAndUser(sutProvider, organizationId.ToString(), userId);\n\n        // Arrange requirement\n        var testRequirement = Substitute.For<IOrganizationRequirement>();\n        testRequirement\n            .AuthorizeAsync(null, Arg.Any<Func<Task<bool>>>())\n            .ReturnsForAnyArgs(false);\n        var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(authContext);\n\n        // Assert\n        await testRequirement.Received(1).AuthorizeAsync(null, Arg.Any<Func<Task<bool>>>());\n        Assert.False(authContext.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Authorizes_IfAuthorizeAsync_ReturnsTrue(\n        SutProvider<OrganizationRequirementHandler> sutProvider, Guid organizationId, Guid userId)\n    {\n        // Arrange route values\n        ArrangeRouteAndUser(sutProvider, organizationId.ToString(), userId);\n\n        // Arrange requirement\n        var testRequirement = Substitute.For<IOrganizationRequirement>();\n        testRequirement\n            .AuthorizeAsync(null, Arg.Any<Func<Task<bool>>>())\n            .ReturnsForAnyArgs(true);\n        var authContext = new AuthorizationHandlerContext([testRequirement], new ClaimsPrincipal(), null);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(authContext);\n\n        // Assert\n        await testRequirement.Received(1).AuthorizeAsync(null, Arg.Any<Func<Task<bool>>>());\n        Assert.True(authContext.HasSucceeded);\n    }\n\n    private static void ArrangeRouteAndUser(SutProvider<OrganizationRequirementHandler> sutProvider, string orgIdRouteValue,\n        Guid? userId)\n    {\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.RouteValues[\"orgId\"] = orgIdRouteValue;\n        sutProvider.GetDependency<IHttpContextAccessor>().HttpContext = httpContext;\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Authorization/RecoverAccountAuthorizationHandlerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Authorization;\n\n[SutProviderCustomize]\npublic class RecoverAccountAuthorizationHandlerTests\n{\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_CurrentUserIsProvider_TargetUserNotProvider_Authorized(\n        SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        [OrganizationUser] OrganizationUser targetOrganizationUser,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        // Arrange\n        var context = new AuthorizationHandlerContext(\n            [new RecoverAccountAuthorizationRequirement()],\n            claimsPrincipal,\n            targetOrganizationUser);\n\n        MockOrganizationClaims(sutProvider, claimsPrincipal, targetOrganizationUser, null);\n        MockCurrentUserIsProvider(sutProvider, claimsPrincipal, targetOrganizationUser);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_CurrentUserIsNotMemberOrProvider_NotAuthorized(\n        SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        [OrganizationUser] OrganizationUser targetOrganizationUser,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        // Arrange\n        var context = new AuthorizationHandlerContext(\n            [new RecoverAccountAuthorizationRequirement()],\n            claimsPrincipal,\n            targetOrganizationUser);\n\n        MockOrganizationClaims(sutProvider, claimsPrincipal, targetOrganizationUser, null);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        AssertFailed(context, RecoverAccountAuthorizationHandler.FailureReason);\n    }\n\n    // Pairing of CurrentContextOrganization (current user permissions) and target user role\n    // Read this as: a ___ can recover the account for a ___\n    public static IEnumerable<object[]> AuthorizedRoleCombinations => new object[][]\n    {\n        [new CurrentContextOrganization { Type = OrganizationUserType.Owner }, OrganizationUserType.Owner],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Owner }, OrganizationUserType.Admin],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Owner }, OrganizationUserType.Custom],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Owner }, OrganizationUserType.User],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Admin }, OrganizationUserType.Admin],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Admin }, OrganizationUserType.Custom],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Admin }, OrganizationUserType.User],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Custom, Permissions = new Permissions { ManageResetPassword = true}}, OrganizationUserType.Custom],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Custom, Permissions = new Permissions { ManageResetPassword = true}}, OrganizationUserType.User],\n    };\n\n    [Theory, BitMemberAutoData(nameof(AuthorizedRoleCombinations))]\n    public async Task AuthorizeMemberAsync_RecoverEqualOrLesserRoles_TargetUserNotProvider_Authorized(\n        CurrentContextOrganization currentContextOrganization,\n        OrganizationUserType targetOrganizationUserType,\n        SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        [OrganizationUser] OrganizationUser targetOrganizationUser,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        // Arrange\n        targetOrganizationUser.Type = targetOrganizationUserType;\n        currentContextOrganization.Id = targetOrganizationUser.OrganizationId;\n\n        var context = new AuthorizationHandlerContext(\n            [new RecoverAccountAuthorizationRequirement()],\n            claimsPrincipal,\n            targetOrganizationUser);\n\n        MockOrganizationClaims(sutProvider, claimsPrincipal, targetOrganizationUser, currentContextOrganization);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        Assert.True(context.HasSucceeded);\n    }\n\n    // Pairing of CurrentContextOrganization (current user permissions) and target user role\n    // Read this as: a ___ cannot recover the account for a ___\n    public static IEnumerable<object[]> UnauthorizedRoleCombinations => new object[][]\n    {\n        // These roles should fail because you cannot recover a greater role\n        [new CurrentContextOrganization { Type = OrganizationUserType.Admin }, OrganizationUserType.Owner],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Custom, Permissions = new Permissions { ManageResetPassword = true}}, OrganizationUserType.Owner],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Custom, Permissions = new Permissions { ManageResetPassword = true} }, OrganizationUserType.Admin],\n\n        // These roles are never authorized to recover any account\n        [new CurrentContextOrganization { Type = OrganizationUserType.User }, OrganizationUserType.Owner],\n        [new CurrentContextOrganization { Type = OrganizationUserType.User }, OrganizationUserType.Admin],\n        [new CurrentContextOrganization { Type = OrganizationUserType.User }, OrganizationUserType.Custom],\n        [new CurrentContextOrganization { Type = OrganizationUserType.User }, OrganizationUserType.User],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Custom }, OrganizationUserType.Owner],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Custom }, OrganizationUserType.Admin],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Custom }, OrganizationUserType.Custom],\n        [new CurrentContextOrganization { Type = OrganizationUserType.Custom }, OrganizationUserType.User],\n    };\n\n    [Theory, BitMemberAutoData(nameof(UnauthorizedRoleCombinations))]\n    public async Task AuthorizeMemberAsync_InvalidRoles_TargetUserNotProvider_Unauthorized(\n        CurrentContextOrganization currentContextOrganization,\n        OrganizationUserType targetOrganizationUserType,\n        SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        [OrganizationUser] OrganizationUser targetOrganizationUser,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        // Arrange\n        targetOrganizationUser.Type = targetOrganizationUserType;\n        currentContextOrganization.Id = targetOrganizationUser.OrganizationId;\n\n        var context = new AuthorizationHandlerContext(\n            [new RecoverAccountAuthorizationRequirement()],\n            claimsPrincipal,\n            targetOrganizationUser);\n\n        MockOrganizationClaims(sutProvider, claimsPrincipal, targetOrganizationUser, currentContextOrganization);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        AssertFailed(context, RecoverAccountAuthorizationHandler.FailureReason);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_TargetUserIdIsNull_DoesNotBlock(\n        SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        OrganizationUser targetOrganizationUser,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        // Arrange\n        targetOrganizationUser.UserId = null;\n        MockCurrentUserIsOwner(sutProvider, claimsPrincipal, targetOrganizationUser);\n\n        var context = new AuthorizationHandlerContext(\n            [new RecoverAccountAuthorizationRequirement()],\n            claimsPrincipal,\n            targetOrganizationUser);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        Assert.True(context.HasSucceeded);\n        // This should shortcut the provider escalation check\n        await sutProvider.GetDependency<IProviderUserRepository>().DidNotReceiveWithAnyArgs()\n            .GetManyByUserAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_CurrentUserIsMemberOfAllTargetUserProviders_DoesNotBlock(\n        SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        [OrganizationUser] OrganizationUser targetOrganizationUser,\n        ClaimsPrincipal claimsPrincipal,\n        Guid providerId1,\n        Guid providerId2)\n    {\n        // Arrange\n        var targetUserProviders = new List<ProviderUser>\n        {\n            new() { ProviderId = providerId1, UserId = targetOrganizationUser.UserId },\n            new() { ProviderId = providerId2, UserId = targetOrganizationUser.UserId }\n        };\n\n        var context = new AuthorizationHandlerContext(\n            [new RecoverAccountAuthorizationRequirement()],\n            claimsPrincipal,\n            targetOrganizationUser);\n\n        MockCurrentUserIsProvider(sutProvider, claimsPrincipal, targetOrganizationUser);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(targetOrganizationUser.UserId!.Value)\n            .Returns(targetUserProviders);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .ProviderUser(providerId1)\n            .Returns(true);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .ProviderUser(providerId2)\n            .Returns(true);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_CurrentUserMissingProviderMembership_Blocks(\n        SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        [OrganizationUser] OrganizationUser targetOrganizationUser,\n        ClaimsPrincipal claimsPrincipal,\n        Guid providerId1,\n        Guid providerId2)\n    {\n        // Arrange\n        var targetUserProviders = new List<ProviderUser>\n        {\n            new() { ProviderId = providerId1, UserId = targetOrganizationUser.UserId },\n            new() { ProviderId = providerId2, UserId = targetOrganizationUser.UserId }\n        };\n\n        var context = new AuthorizationHandlerContext(\n            [new RecoverAccountAuthorizationRequirement()],\n            claimsPrincipal,\n            targetOrganizationUser);\n\n        MockCurrentUserIsOwner(sutProvider, claimsPrincipal, targetOrganizationUser);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(targetOrganizationUser.UserId!.Value)\n            .Returns(targetUserProviders);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .ProviderUser(providerId1)\n            .Returns(true);\n\n        // Not a member of this provider\n        sutProvider.GetDependency<ICurrentContext>()\n            .ProviderUser(providerId2)\n            .Returns(false);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        AssertFailed(context, RecoverAccountAuthorizationHandler.ProviderFailureReason);\n    }\n\n    private static void MockOrganizationClaims(SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        ClaimsPrincipal currentUser, OrganizationUser targetOrganizationUser,\n        CurrentContextOrganization? currentContextOrganization)\n    {\n        sutProvider.GetDependency<IOrganizationContext>()\n            .GetOrganizationClaims(currentUser, targetOrganizationUser.OrganizationId)\n            .Returns(currentContextOrganization);\n    }\n\n    private static void MockCurrentUserIsProvider(SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        ClaimsPrincipal currentUser, OrganizationUser targetOrganizationUser)\n    {\n        sutProvider.GetDependency<IOrganizationContext>()\n            .IsProviderUserForOrganization(currentUser, targetOrganizationUser.OrganizationId)\n            .Returns(true);\n    }\n\n    private static void MockCurrentUserIsOwner(SutProvider<RecoverAccountAuthorizationHandler> sutProvider,\n        ClaimsPrincipal currentUser, OrganizationUser targetOrganizationUser)\n    {\n        var currentContextOrganization = new CurrentContextOrganization\n        {\n            Id = targetOrganizationUser.OrganizationId,\n            Type = OrganizationUserType.Owner\n        };\n\n        sutProvider.GetDependency<IOrganizationContext>()\n            .GetOrganizationClaims(currentUser, targetOrganizationUser.OrganizationId)\n            .Returns(currentContextOrganization);\n    }\n\n    private static void AssertFailed(AuthorizationHandlerContext context, string expectedMessage)\n    {\n        Assert.True(context.HasFailed);\n        var failureReason = Assert.Single(context.FailureReasons);\n        Assert.Equal(expectedMessage, failureReason.Message);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Test.AdminConsole.Helpers;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Authorization.Requirements;\n\npublic class BasePermissionRequirementTests\n{\n    [Theory, BitAutoData]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Owner)]\n    public async Task Authorizes_Owners(CurrentContextOrganization organizationClaims)\n    {\n        var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin)]\n    public async Task Authorizes_Admins(CurrentContextOrganization organizationClaims)\n    {\n        var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]\n    public async Task Authorizes_Providers(CurrentContextOrganization organizationClaims)\n    {\n        var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(true));\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]\n    public async Task Authorizes_CustomPermission(CurrentContextOrganization organizationClaims)\n    {\n        organizationClaims.Permissions.ManageGroups = true;\n        var result = await new TestCustomPermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]\n    public async Task DoesNotAuthorize_Users(CurrentContextOrganization organizationClaims)\n    {\n        var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]\n    public async Task DoesNotAuthorize_OtherCustomPermissions(CurrentContextOrganization organizationClaims)\n    {\n        organizationClaims.Permissions.ManageGroups = true;\n        organizationClaims.Permissions = organizationClaims.Permissions.Invert();\n        var result = await new TestCustomPermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));\n        Assert.False(result);\n    }\n\n    private class PermissionRequirement() : BasePermissionRequirement(_ => false);\n    private class TestCustomPermissionRequirement() : BasePermissionRequirement(p => p.ManageGroups);\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirementTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Authorization.Requirements;\n\n[SutProviderCustomize]\npublic class ManageGroupsOrUsersRequirementTests\n{\n    [Theory]\n    [CurrentContextOrganizationCustomize]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task AuthorizeAsync_WhenUserTypeCanManageUsers_ThenRequestShouldBeAuthorized(\n        OrganizationUserType type,\n        CurrentContextOrganization organization,\n        SutProvider<ManageGroupsOrUsersRequirement> sutProvider)\n    {\n        organization.Type = type;\n\n        var actual = await sutProvider.Sut.AuthorizeAsync(organization, () => Task.FromResult(false));\n\n        Assert.True(actual);\n    }\n\n    [Theory]\n    [CurrentContextOrganizationCustomize]\n    [BitAutoData(OrganizationUserType.Custom, true, false)]\n    [BitAutoData(OrganizationUserType.Custom, false, true)]\n    public async Task AuthorizeAsync_WhenCustomUserThatCanManageUsersOrGroups_ThenRequestShouldBeAuthorized(\n        OrganizationUserType type,\n        bool canManageUsers,\n        bool canManageGroups,\n        CurrentContextOrganization organization,\n        SutProvider<ManageGroupsOrUsersRequirement> sutProvider)\n    {\n        organization.Type = type;\n        organization.Permissions = new Permissions { ManageUsers = canManageUsers, ManageGroups = canManageGroups };\n\n        var actual = await sutProvider.Sut.AuthorizeAsync(organization, () => Task.FromResult(false));\n\n        Assert.True(actual);\n    }\n\n    [Theory]\n    [CurrentContextOrganizationCustomize]\n    [BitAutoData]\n    public async Task AuthorizeAsync_WhenProviderUserForAnOrganization_ThenRequestShouldBeAuthorized(\n        CurrentContextOrganization organization,\n        SutProvider<ManageGroupsOrUsersRequirement> sutProvider)\n    {\n        var actual = await sutProvider.Sut.AuthorizeAsync(organization, IsProviderUserForOrg);\n\n        Assert.True(actual);\n        return;\n\n        Task<bool> IsProviderUserForOrg() => Task.FromResult(true);\n    }\n\n    [Theory]\n    [CurrentContextOrganizationCustomize]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task AuthorizeAsync_WhenUserCannotManageUsersOrGroupsAndIsNotAProviderUser_ThenRequestShouldBeDenied(\n        OrganizationUserType type,\n        CurrentContextOrganization organization,\n        SutProvider<ManageGroupsOrUsersRequirement> sutProvider)\n    {\n        organization.Type = type;\n        organization.Permissions = new Permissions { ManageUsers = false, ManageGroups = false }; // When Type is User, the canManage permissions don't matter\n\n        var actual = await sutProvider.Sut.AuthorizeAsync(organization, IsNotProviderUserForOrg);\n\n        Assert.False(actual);\n        return;\n\n        Task<bool> IsNotProviderUserForOrg() => Task.FromResult(false);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Authorization/Requirements/MemberRequirementTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Authorization.Requirements;\n\n[SutProviderCustomize]\npublic class MemberRequirementTests\n{\n    [Theory]\n    [CurrentContextOrganizationCustomize]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task AuthorizeAsync_WhenUserIsOrganizationMember_ThenRequestShouldBeAuthorized(\n        OrganizationUserType type,\n        CurrentContextOrganization organization,\n        SutProvider<MemberRequirement> sutProvider)\n    {\n        organization.Type = type;\n\n        var actual = await sutProvider.Sut.AuthorizeAsync(organization, () => Task.FromResult(false));\n\n        Assert.True(actual);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AuthorizeAsync_WhenUserIsNotOrganizationMember_ThenRequestShouldBeDenied(\n        SutProvider<MemberRequirement> sutProvider)\n    {\n        var actual = await sutProvider.Sut.AuthorizeAsync(null, () => Task.FromResult(false));\n\n        Assert.False(actual);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AuthorizeAsync_WhenUserIsProviderButNotMember_ThenRequestShouldBeDenied(\n        SutProvider<MemberRequirement> sutProvider)\n    {\n        var actual = await sutProvider.Sut.AuthorizeAsync(null, () => Task.FromResult(true));\n\n        Assert.False(actual);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.AdminConsole.Authorization.Requirements;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Test.AdminConsole.Helpers;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Authorization.Requirements;\n\npublic class PermissionRequirementsTests\n{\n    /// <summary>\n    /// Correlates each IOrganizationRequirement with its custom permission. If you add a new requirement,\n    /// add a new entry here to have it automatically included in the tests below.\n    /// </summary>\n    public static IEnumerable<object[]> RequirementData => new List<object[]>\n    {\n        new object[] { new AccessEventLogsRequirement(), nameof(Permissions.AccessEventLogs) },\n        new object[] { new AccessImportExportRequirement(), nameof(Permissions.AccessImportExport) },\n        new object[] { new AccessReportsRequirement(), nameof(Permissions.AccessReports) },\n        new object[] { new ManageAccountRecoveryRequirement(), nameof(Permissions.ManageResetPassword) },\n        new object[] { new ManageGroupsRequirement(), nameof(Permissions.ManageGroups) },\n        new object[] { new ManagePoliciesRequirement(), nameof(Permissions.ManagePolicies) },\n        new object[] { new ManageScimRequirement(), nameof(Permissions.ManageScim) },\n        new object[] { new ManageSsoRequirement(), nameof(Permissions.ManageSso) },\n        new object[] { new ManageUsersRequirement(), nameof(Permissions.ManageUsers) },\n    };\n\n    [Theory]\n    [BitMemberAutoData(nameof(RequirementData))]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]\n    public async Task Authorizes_Provider(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization)\n    {\n        var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(true));\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(RequirementData))]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Owner)]\n    public async Task Authorizes_Owner(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization)\n    {\n        var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(RequirementData))]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin)]\n    public async Task Authorizes_Admin(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization)\n    {\n        var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(RequirementData))]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]\n    public async Task Authorizes_Custom_With_Correct_Permission(IOrganizationRequirement requirement, string permissionName, CurrentContextOrganization organization)\n    {\n        organization.Permissions.SetPermission(permissionName, true);\n        var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(RequirementData))]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]\n    public async Task DoesNotAuthorize_Custom_With_Other_Permissions(IOrganizationRequirement requirement, string permissionName, CurrentContextOrganization organization)\n    {\n        organization.Permissions.SetPermission(permissionName, true);\n        organization.Permissions = organization.Permissions.Invert();\n        var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(RequirementData))]\n    [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]\n    public async Task DoesNotAuthorize_User(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization)\n    {\n        var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false));\n        Assert.False(result);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/AuthorizationHandlers/GroupAuthorizationHandlerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.AuthorizationHandlers;\n\n[SutProviderCustomize]\npublic class GroupAuthorizationHandlerTests\n{\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task CanReadAllAsync_WhenMemberOfOrg_Success(\n        OrganizationUserType userType,\n        OrganizationScope scope,\n        Guid userId, SutProvider<GroupAuthorizationHandler> sutProvider,\n        CurrentContextOrganization organization)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            new[] { GroupOperations.ReadAll },\n            new ClaimsPrincipal(),\n            scope);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(scope).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CanReadAllAsync_WithProviderUser_Success(\n        Guid userId,\n        OrganizationScope scope,\n        SutProvider<GroupAuthorizationHandler> sutProvider, CurrentContextOrganization organization)\n    {\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            new[] { GroupOperations.ReadAll },\n            new ClaimsPrincipal(),\n            scope);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId\n            .Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>()\n            .ProviderUserForOrgAsync(scope)\n            .Returns(true);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CanReadAllAsync_WhenMissingOrgAccess_NoSuccess(\n        Guid userId,\n        OrganizationScope scope,\n        SutProvider<GroupAuthorizationHandler> sutProvider)\n    {\n\n        var context = new AuthorizationHandlerContext(\n            new[] { GroupOperations.ReadAll },\n            new ClaimsPrincipal(),\n            scope\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserIsAdminOwner_ThenShouldSucceed(OrganizationUserType userType,\n        OrganizationScope scope,\n        CurrentContextOrganization organization, SutProvider<GroupAuthorizationHandler> sutProvider)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            [GroupOperations.ReadAllDetails],\n            new ClaimsPrincipal(),\n            scope\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(scope).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserIsNotOwnerOrAdmin_ThenShouldFail(OrganizationUserType userType,\n        OrganizationScope scope,\n        CurrentContextOrganization organization, SutProvider<GroupAuthorizationHandler> sutProvider)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            [GroupOperations.ReadAllDetails],\n            new ClaimsPrincipal(),\n            scope\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(scope).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserHasManageGroupPermission_ThenShouldSucceed(\n        OrganizationScope scope,\n        CurrentContextOrganization organization, SutProvider<GroupAuthorizationHandler> sutProvider)\n    {\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            ManageGroups = true\n        };\n\n        var context = new AuthorizationHandlerContext(\n            [GroupOperations.ReadAllDetails],\n            new ClaimsPrincipal(),\n            scope\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(scope).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserHasManageUserPermission_ThenShouldSucceed(\n        OrganizationScope scope,\n        CurrentContextOrganization organization, SutProvider<GroupAuthorizationHandler> sutProvider)\n    {\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            ManageUsers = true\n        };\n\n        var context = new AuthorizationHandlerContext(\n            [GroupOperations.ReadAllDetails],\n            new ClaimsPrincipal(),\n            scope\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(scope).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenUserIsStandardUserTypeWithoutElevatedPermissions_ThenShouldFail(\n        OrganizationScope scope,\n        CurrentContextOrganization organization, SutProvider<GroupAuthorizationHandler> sutProvider)\n    {\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            [GroupOperations.ReadAllDetails],\n            new ClaimsPrincipal(),\n            scope\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(scope).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(scope).Returns(false);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementsAsync_GivenViewDetailsOperation_WhenIsProviderUser_ThenShouldSucceed(\n        OrganizationScope scope,\n        SutProvider<GroupAuthorizationHandler> sutProvider, CurrentContextOrganization organization)\n    {\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            new[] { GroupOperations.ReadAll },\n            new ClaimsPrincipal(),\n            scope);\n\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(scope).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(scope).Returns(true);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.Models.Request;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\n#nullable enable\n\nnamespace Bit.Api.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(GroupsController))]\n[SutProviderCustomize]\npublic class GroupsControllerPutTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Put_WithAdminAccess_Success(Organization organization, Group group,\n        GroupRequestModel groupRequestModel, List<CollectionAccessSelection> existingCollectionAccess,\n        OrganizationUser savingUser, SutProvider<GroupsController> sutProvider)\n    {\n        Put_Setup(sutProvider, organization, true, group, savingUser, existingCollectionAccess, []);\n\n        var requestModelCollectionIds = groupRequestModel.Collections.Select(c => c.Id).ToHashSet();\n\n        // Authorize all changes for basic happy path test\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Collection>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyGroupAccess)))\n            .Returns(AuthorizationResult.Success());\n\n        var response = await sutProvider.Sut.Put(organization.Id, group.Id, groupRequestModel);\n\n        await sutProvider.GetDependency<ICurrentContext>().Received(1).ManageGroups(organization.Id);\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(\n            Arg.Is<Group>(g =>\n                g.OrganizationId == organization.Id && g.Name == groupRequestModel.Name),\n            Arg.Is<Organization>(o => o.Id == organization.Id),\n            // Should overwrite any existing collections\n            Arg.Is<ICollection<CollectionAccessSelection>>(access =>\n                access.All(c => requestModelCollectionIds.Contains(c.Id))),\n            Arg.Is<IEnumerable<Guid>>(guids => guids.ToHashSet().SetEquals(groupRequestModel.Users.ToHashSet())));\n        Assert.Equal(groupRequestModel.Name, response.Name);\n        Assert.Equal(organization.Id, response.OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateMembers_NoAdminAccess_CannotAddSelfToGroup(Organization organization, Group group,\n        GroupRequestModel groupRequestModel, OrganizationUser savingUser, List<Guid> currentGroupUsers,\n        SutProvider<GroupsController> sutProvider)\n    {\n        // Not updating collections\n        groupRequestModel.Collections = [];\n\n        Put_Setup(sutProvider, organization, false, group, savingUser,\n            currentCollectionAccess: [], currentGroupUsers);\n\n        // Saving user is trying to add themselves to the group\n        var updatedUsers = groupRequestModel.Users.ToList();\n        updatedUsers.Add(savingUser.Id);\n        groupRequestModel.Users = updatedUsers;\n\n        var exception = await\n            Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Put(organization.Id, group.Id, groupRequestModel));\n\n        Assert.Contains(\"You cannot add yourself to groups\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateMembers_NoAdminAccess_AlreadyInGroup_Success(Organization organization, Group group,\n        GroupRequestModel groupRequestModel, OrganizationUser savingUser, List<Guid> currentGroupUsers,\n        SutProvider<GroupsController> sutProvider)\n    {\n        // Not changing collection access\n        groupRequestModel.Collections = [];\n\n        // Saving user is trying to add themselves to the group\n        var updatedUsers = groupRequestModel.Users.ToList();\n        updatedUsers.Add(savingUser.Id);\n        groupRequestModel.Users = updatedUsers;\n\n        // But! they are already a member of the group\n        currentGroupUsers.Add(savingUser.Id);\n\n        Put_Setup(sutProvider, organization, false, group, savingUser, currentCollectionAccess: [], currentGroupUsers);\n\n        var response = await sutProvider.Sut.Put(organization.Id, group.Id, groupRequestModel);\n\n        await sutProvider.GetDependency<ICurrentContext>().Received(1).ManageGroups(organization.Id);\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(\n            Arg.Is<Group>(g =>\n                g.OrganizationId == organization.Id && g.Name == groupRequestModel.Name),\n            Arg.Is<Organization>(o => o.Id == organization.Id),\n            Arg.Any<ICollection<CollectionAccessSelection>>(),\n            Arg.Any<IEnumerable<Guid>>());\n        Assert.Equal(groupRequestModel.Name, response.Name);\n        Assert.Equal(organization.Id, response.OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateMembers_WithAdminAccess_CanAddSelfToGroup(Organization organization, Group group,\n        GroupRequestModel groupRequestModel, OrganizationUser savingUser, List<Guid> currentGroupUsers,\n        SutProvider<GroupsController> sutProvider)\n    {\n        // Not updating collections\n        groupRequestModel.Collections = [];\n\n        Put_Setup(sutProvider, organization, true, group, savingUser,\n            currentCollectionAccess: [], currentGroupUsers);\n\n        // Saving user is trying to add themselves to the group\n        var updatedUsers = groupRequestModel.Users.ToList();\n        updatedUsers.Add(savingUser.Id);\n        groupRequestModel.Users = updatedUsers;\n\n        var response = await sutProvider.Sut.Put(organization.Id, group.Id, groupRequestModel);\n\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(\n            Arg.Is<Group>(g =>\n                g.OrganizationId == organization.Id && g.Name == groupRequestModel.Name),\n            Arg.Is<Organization>(o => o.Id == organization.Id),\n            Arg.Any<ICollection<CollectionAccessSelection>>(),\n            Arg.Is<IEnumerable<Guid>>(guids => guids.ToHashSet().SetEquals(groupRequestModel.Users.ToHashSet())));\n        Assert.Equal(groupRequestModel.Name, response.Name);\n        Assert.Equal(organization.Id, response.OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateMembers_NoAdminAccess_ProviderUser_Success(Organization organization, Group group,\n        GroupRequestModel groupRequestModel, List<Guid> currentGroupUsers, SutProvider<GroupsController> sutProvider)\n    {\n        // Make collection authorization pass, it's not being tested here\n        groupRequestModel.Collections = Array.Empty<SelectionReadOnlyRequestModel>();\n\n        Put_Setup(sutProvider, organization, false, group, null, currentCollectionAccess: [], currentGroupUsers);\n\n        var response = await sutProvider.Sut.Put(organization.Id, group.Id, groupRequestModel);\n\n        await sutProvider.GetDependency<ICurrentContext>().Received(1).ManageGroups(organization.Id);\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(\n            Arg.Is<Group>(g =>\n                g.OrganizationId == organization.Id && g.Name == groupRequestModel.Name),\n            Arg.Is<Organization>(o => o.Id == organization.Id),\n            Arg.Any<ICollection<CollectionAccessSelection>>(),\n            Arg.Any<IEnumerable<Guid>>());\n        Assert.Equal(groupRequestModel.Name, response.Name);\n        Assert.Equal(organization.Id, response.OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateCollections_DoesNotOverwriteUnauthorizedCollections(GroupRequestModel groupRequestModel,\n        Group group, Organization organization,\n        SutProvider<GroupsController> sutProvider, OrganizationUser savingUser)\n    {\n        var editedCollectionId = CoreHelpers.GenerateComb();\n        var readonlyCollectionId1 = CoreHelpers.GenerateComb();\n        var readonlyCollectionId2 = CoreHelpers.GenerateComb();\n\n        var currentCollectionAccess = new List<CollectionAccessSelection>\n        {\n            new()\n            {\n                Id = editedCollectionId,\n                HidePasswords = true,\n                Manage = false,\n                ReadOnly = true\n            },\n            new()\n            {\n                Id = readonlyCollectionId1,\n                HidePasswords = false,\n                Manage = true,\n                ReadOnly = false\n            },\n            new()\n            {\n                Id = readonlyCollectionId2,\n                HidePasswords = false,\n                Manage = false,\n                ReadOnly = false\n            },\n        };\n\n        Put_Setup(sutProvider, organization, false, group, savingUser, currentCollectionAccess, currentGroupUsers: []);\n\n        // User is upgrading editedCollectionId to manage\n        groupRequestModel.Collections = new List<SelectionReadOnlyRequestModel>\n        {\n            new() { Id = editedCollectionId, HidePasswords = false, Manage = true, ReadOnly = false }\n        };\n\n        // Authorize the editedCollection\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == editedCollectionId),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyGroupAccess)))\n            .Returns(AuthorizationResult.Success());\n\n        // Do not authorize the readonly collections\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == readonlyCollectionId1 || c.Id == readonlyCollectionId2),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyGroupAccess)))\n            .Returns(AuthorizationResult.Failed());\n\n        var response = await sutProvider.Sut.Put(organization.Id, group.Id, groupRequestModel);\n\n        // Expect all collection access (modified and unmodified) to be saved\n        await sutProvider.GetDependency<ICurrentContext>().Received(1).ManageGroups(organization.Id);\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(\n            Arg.Is<Group>(g =>\n                g.OrganizationId == organization.Id && g.Name == groupRequestModel.Name),\n            Arg.Is<Organization>(o => o.Id == organization.Id),\n            Arg.Is<List<CollectionAccessSelection>>(cas =>\n                cas.Select(c => c.Id).SequenceEqual(currentCollectionAccess.Select(c => c.Id)) &&\n                cas.First(c => c.Id == editedCollectionId).Manage == true &&\n                cas.First(c => c.Id == editedCollectionId).ReadOnly == false &&\n                cas.First(c => c.Id == editedCollectionId).HidePasswords == false),\n            Arg.Any<IEnumerable<Guid>>());\n        Assert.Equal(groupRequestModel.Name, response.Name);\n        Assert.Equal(organization.Id, response.OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateCollections_ThrowsIfSavingUserCannotUpdateCollections(GroupRequestModel groupRequestModel,\n        Group group, Organization organization,\n        SutProvider<GroupsController> sutProvider, OrganizationUser savingUser)\n    {\n        // Group is currently assigned to the POSTed collections\n        Put_Setup(sutProvider, organization, false, group, savingUser,\n            groupRequestModel.Collections.Select(cas => cas.ToSelectionReadOnly()).ToList(),\n            []);\n\n        var postedCollectionIds = groupRequestModel.Collections.Select(c => c.Id).ToHashSet();\n\n        // But the saving user does not have permission to update them\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => postedCollectionIds.Contains(c.Id)),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyGroupAccess)))\n            .Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Put(organization.Id, group.Id, groupRequestModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateCollections_ThrowsIfSavingUserCannotAddCollections(GroupRequestModel groupRequestModel,\n        Group group, Organization organization,\n        SutProvider<GroupsController> sutProvider, OrganizationUser savingUser)\n    {\n        // Group is not assigned to the POSTed collections\n        Put_Setup(sutProvider, organization, false, group, savingUser, [], []);\n\n        var postedCollectionIds = groupRequestModel.Collections.Select(c => c.Id).ToHashSet();\n\n        // But the saving user does not have permission to update them\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => postedCollectionIds.Contains(c.Id)),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyGroupAccess)))\n            .Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Put(organization.Id, group.Id, groupRequestModel));\n    }\n\n    private void Put_Setup(SutProvider<GroupsController> sutProvider, Organization organization,\n        bool adminAccess, Group group, OrganizationUser? savingUser, List<CollectionAccessSelection> currentCollectionAccess,\n        List<Guid> currentGroupUsers)\n    {\n        var orgId = organization.Id = group.OrganizationId;\n\n        // Arrange org and orgAbility\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(orgId)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                AllowAdminAccessToAllCollectionItems = adminAccess\n            });\n\n        // Arrange user\n        // If no savingUser provided, they're not an org user, just return a random guid\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(savingUser?.UserId ?? CoreHelpers.GenerateComb());\n        sutProvider.GetDependency<ICurrentContext>().ManageGroups(orgId).Returns(true);\n\n        // Arrange repositories\n        sutProvider.GetDependency<IGroupRepository>().GetManyUserIdsByIdAsync(group.Id).Returns(currentGroupUsers ?? []);\n        sutProvider.GetDependency<IGroupRepository>().GetByIdWithCollectionsAsync(group.Id)\n            .Returns(new Tuple<Group?, ICollection<CollectionAccessSelection>>(group, currentCollectionAccess ?? []));\n        if (savingUser != null)\n        {\n            sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(orgId, savingUser.UserId!.Value)\n                .Returns(savingUser);\n        }\n\n        // Collection repository: return mock Collection objects for any ids passed in\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>().Select(guid => new Collection { Id = guid }).ToList());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Controllers/GroupsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(GroupsController))]\n[SutProviderCustomize]\npublic class GroupsControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Post_AuthorizedToGiveAccessToCollections_Success(Organization organization,\n        GroupRequestModel groupRequestModel, SutProvider<GroupsController> sutProvider)\n    {\n        // Enable FC\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(\n            new OrganizationAbility { Id = organization.Id, AllowAdminAccessToAllCollectionItems = false });\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                 Arg.Any<IEnumerable<Collection>>(),\n                 Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyGroupAccess)))\n             .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().ManageGroups(organization.Id).Returns(true);\n\n        var response = await sutProvider.Sut.Post(organization.Id, groupRequestModel);\n\n        var requestModelCollectionIds = groupRequestModel.Collections.Select(c => c.Id).ToHashSet();\n\n        // Assert that it checked permissions\n        await sutProvider.GetDependency<ICurrentContext>().Received(1).ManageGroups(organization.Id);\n        await sutProvider.GetDependency<IAuthorizationService>()\n            .Received(1)\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Is<IEnumerable<Collection>>(collections =>\n                    collections.All(c => requestModelCollectionIds.Contains(c.Id))),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Single() == BulkCollectionOperations.ModifyGroupAccess));\n\n        // Assert that it saved the data\n        await sutProvider.GetDependency<ICreateGroupCommand>().Received(1).CreateGroupAsync(\n            Arg.Is<Group>(g =>\n                g.OrganizationId == organization.Id && g.Name == groupRequestModel.Name),\n            organization,\n            Arg.Is<ICollection<CollectionAccessSelection>>(access =>\n                access.All(c => requestModelCollectionIds.Contains(c.Id))),\n            Arg.Any<IEnumerable<Guid>>());\n        Assert.Equal(groupRequestModel.Name, response.Name);\n        Assert.Equal(organization.Id, response.OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Post_NotAuthorizedToGiveAccessToCollections_Throws(Organization organization, GroupRequestModel groupRequestModel, SutProvider<GroupsController> sutProvider)\n    {\n        // Enable FC\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(\n            new OrganizationAbility { Id = organization.Id, AllowAdminAccessToAllCollectionItems = false });\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().ManageGroups(organization.Id).Returns(true);\n\n        var requestModelCollectionIds = groupRequestModel.Collections.Select(c => c.Id).ToHashSet();\n        sutProvider.GetDependency<IAuthorizationService>()\n           .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Is<IEnumerable<Collection>>(collections => collections.All(c => requestModelCollectionIds.Contains(c.Id))),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyGroupAccess)))\n            .Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Post(organization.Id, groupRequestModel));\n\n        await sutProvider.GetDependency<ICreateGroupCommand>().DidNotReceiveWithAnyArgs()\n            .CreateGroupAsync(default, default, default, default);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Controllers/OrganizationAuthRequestsControllerTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Core.Context;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(OrganizationAuthRequestsController))]\n[SutProviderCustomize]\npublic class OrganizationAuthRequestsControllerTests\n{\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAdminRequest_UserDoesNotHaveManageResetPasswordPermissions_ThrowsUnauthorized(\n        SutProvider<OrganizationAuthRequestsController> sutProvider,\n        Guid organizationId\n    )\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageResetPassword(organizationId).Returns(false);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>\n            sutProvider.Sut.ValidateAdminRequest(organizationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAdminRequest_UserHasManageResetPasswordPermissions_DoesNotThrow(\n        SutProvider<OrganizationAuthRequestsController> sutProvider,\n        Guid organizationId\n    )\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageResetPassword(organizationId).Returns(true);\n        await sutProvider.Sut.ValidateAdminRequest(organizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateManyAuthRequests_ValidInput_DoesNotThrow(\n        SutProvider<OrganizationAuthRequestsController> sutProvider,\n        IEnumerable<OrganizationAuthRequestUpdateManyRequestModel> request,\n        Guid organizationId\n    )\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageResetPassword(organizationId).Returns(true);\n        await sutProvider.Sut.UpdateManyAuthRequests(organizationId, request);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateManyAuthRequests_NotPermissioned_ThrowsUnauthorized(\n        SutProvider<OrganizationAuthRequestsController> sutProvider,\n        IEnumerable<OrganizationAuthRequestUpdateManyRequestModel> request,\n        Guid organizationId\n    )\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageResetPassword(organizationId).Returns(false);\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>\n            sutProvider.Sut.UpdateManyAuthRequests(organizationId, request));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Controllers/OrganizationConnectionsControllerTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(OrganizationConnectionsController))]\n[SutProviderCustomize]\n[JsonDocumentCustomize]\npublic class OrganizationConnectionsControllerTests\n{\n    public static IEnumerable<object[]> ConnectionTypes =>\n        Enum.GetValues<OrganizationConnectionType>().Select(p => new object[] { p });\n\n\n    [Theory]\n    [BitAutoData(true, true)]\n    [BitAutoData(false, true)]\n    [BitAutoData(true, false)]\n    [BitAutoData(false, false)]\n    public void ConnectionEnabled_RequiresBothSelfHostAndCommunications(bool selfHosted, bool enableCloudCommunication, SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        var globalSettingsMock = sutProvider.GetDependency<IGlobalSettings>();\n        globalSettingsMock.SelfHosted.Returns(selfHosted);\n        globalSettingsMock.EnableCloudCommunication.Returns(enableCloudCommunication);\n\n        Action<bool> assert = selfHosted && enableCloudCommunication ? Assert.True : Assert.False;\n\n        var result = sutProvider.Sut.ConnectionsEnabled();\n\n        assert(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateConnection_CloudBillingSync_RequiresOwnerPermissions(SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        var model = new OrganizationConnectionRequestModel\n        {\n            Type = OrganizationConnectionType.CloudBillingSync,\n        };\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateConnection(model));\n\n        Assert.Contains($\"You do not have permission to create a connection of type\", exception.Message);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(ConnectionTypes))]\n    public async Task CreateConnection_OnlyOneConnectionOfEachType(OrganizationConnectionType type,\n        OrganizationConnectionRequestModel model, BillingSyncConfig config, Guid existingEntityId,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        model.Type = type;\n        model.Config = JsonDocumentFromObject(config);\n        var typedModel = new OrganizationConnectionRequestModel<BillingSyncConfig>(model);\n        var existing = typedModel.ToData(existingEntityId).ToEntity();\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId).Returns(true);\n\n        sutProvider.GetDependency<IOrganizationConnectionRepository>().GetByOrganizationIdTypeAsync(model.OrganizationId, type).Returns(new[] { existing });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateConnection(model));\n\n        Assert.Contains($\"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateConnection_BillingSyncType_InvalidLicense_Throws(OrganizationConnectionRequestModel model,\n        BillingSyncConfig config, Guid cloudOrgId, OrganizationLicense organizationLicense,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        model.Type = OrganizationConnectionType.CloudBillingSync;\n        organizationLicense.Id = cloudOrgId;\n\n        model.Config = JsonDocumentFromObject(config);\n        var typedModel = new OrganizationConnectionRequestModel<BillingSyncConfig>(model);\n        typedModel.ParsedConfig.CloudOrganizationId = cloudOrgId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(model.OrganizationId)\n            .Returns(true);\n\n        sutProvider.GetDependency<ILicensingService>()\n            .ReadOrganizationLicenseAsync(model.OrganizationId)\n            .Returns(organizationLicense);\n\n        sutProvider.GetDependency<ILicensingService>()\n            .VerifyLicense(organizationLicense)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateConnection(model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateConnection_Success(OrganizationConnectionRequestModel model, BillingSyncConfig config,\n        Guid cloudOrgId, OrganizationLicense organizationLicense, SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        organizationLicense.Id = cloudOrgId;\n\n        model.Config = JsonDocumentFromObject(config);\n        var typedModel = new OrganizationConnectionRequestModel<BillingSyncConfig>(model);\n        typedModel.ParsedConfig.CloudOrganizationId = cloudOrgId;\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);\n        sutProvider.GetDependency<ICreateOrganizationConnectionCommand>().CreateAsync<BillingSyncConfig>(default)\n            .ReturnsForAnyArgs(typedModel.ToData(Guid.NewGuid()).ToEntity());\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId).Returns(true);\n        sutProvider.GetDependency<ILicensingService>()\n            .ReadOrganizationLicenseAsync(Arg.Any<Guid>())\n            .Returns(organizationLicense);\n\n        sutProvider.GetDependency<ILicensingService>()\n            .VerifyLicense(organizationLicense)\n            .Returns(true);\n\n        await sutProvider.Sut.CreateConnection(model);\n\n        await sutProvider.GetDependency<ICreateOrganizationConnectionCommand>().Received(1)\n            .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(typedModel.ToData())));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateConnection_RequiresOwnerPermissions(SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationConnectionRepository>()\n            .GetByIdOrganizationIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>())\n            .Returns(new OrganizationConnection());\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateConnection(default, new OrganizationConnectionRequestModel()));\n\n        Assert.Contains(\"You do not have permission to update this connection.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationConnectionType.CloudBillingSync)]\n    public async Task UpdateConnection_BillingSync_OnlyOneConnectionOfEachType(OrganizationConnectionType type,\n        OrganizationConnection existing1, OrganizationConnection existing2, BillingSyncConfig config,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        existing1.Type = existing2.Type = type;\n        existing1.Config = JsonSerializer.Serialize(config);\n        var typedModel = RequestModelFromEntity<BillingSyncConfig>(existing1);\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(typedModel.OrganizationId).Returns(true);\n\n        var orgConnectionRepository = sutProvider.GetDependency<IOrganizationConnectionRepository>();\n        orgConnectionRepository.GetByIdOrganizationIdAsync(existing1.Id, existing1.OrganizationId).Returns(existing1);\n        orgConnectionRepository.GetByIdOrganizationIdAsync(existing2.Id, existing2.OrganizationId).Returns(existing2);\n        orgConnectionRepository.GetByOrganizationIdTypeAsync(typedModel.OrganizationId, type).Returns(new[] { existing1, existing2 });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateConnection(existing1.Id, typedModel));\n\n        Assert.Contains($\"The requested organization already has a connection of type {typedModel.Type}. Only one of each connection type may exist per organization.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationConnectionType.Scim)]\n    public async Task UpdateConnection_Scim_OnlyOneConnectionOfEachType(OrganizationConnectionType type,\n        OrganizationConnection existing1, OrganizationConnection existing2, ScimConfig config,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        existing1.Type = existing2.Type = type;\n        existing1.Config = JsonSerializer.Serialize(config);\n        var typedModel = RequestModelFromEntity<ScimConfig>(existing1);\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(typedModel.OrganizationId).Returns(true);\n\n        sutProvider.GetDependency<IOrganizationConnectionRepository>()\n            .GetByIdOrganizationIdAsync(existing1.Id, existing1.OrganizationId)\n            .Returns(existing1);\n\n        sutProvider.GetDependency<ICurrentContext>().ManageScim(typedModel.OrganizationId).Returns(true);\n\n        sutProvider.GetDependency<IOrganizationConnectionRepository>()\n            .GetByOrganizationIdTypeAsync(typedModel.OrganizationId, type)\n            .Returns(new[] { existing1, existing2 });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateConnection(existing1.Id, typedModel));\n\n        Assert.Contains($\"The requested organization already has a connection of type {typedModel.Type}. Only one of each connection type may exist per organization.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateConnection_Success(OrganizationConnection existing, BillingSyncConfig config,\n        OrganizationConnection updated,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        existing.SetConfig(new BillingSyncConfig\n        {\n            CloudOrganizationId = config.CloudOrganizationId,\n        });\n        updated.Config = JsonSerializer.Serialize(config);\n        updated.Id = existing.Id;\n        updated.OrganizationId = existing.OrganizationId;\n        updated.Type = OrganizationConnectionType.CloudBillingSync;\n        var model = RequestModelFromEntity<BillingSyncConfig>(updated);\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationConnectionRepository>()\n            .GetByOrganizationIdTypeAsync(model.OrganizationId, model.Type)\n            .Returns(new[] { existing });\n        sutProvider.GetDependency<IUpdateOrganizationConnectionCommand>()\n            .UpdateAsync<BillingSyncConfig>(default)\n            .ReturnsForAnyArgs(updated);\n        sutProvider.GetDependency<IOrganizationConnectionRepository>()\n            .GetByIdOrganizationIdAsync(existing.Id, existing.OrganizationId)\n            .Returns(existing);\n\n        OrganizationLicense organizationLicense = new OrganizationLicense();\n        var now = DateTime.UtcNow;\n        organizationLicense.Issued = now.AddDays(-10);\n        organizationLicense.Expires = now.AddDays(10);\n        organizationLicense.Version = 1;\n        organizationLicense.UsersGetPremium = true;\n        organizationLicense.Id = config.CloudOrganizationId;\n        organizationLicense.Trial = true;\n\n        sutProvider.GetDependency<ILicensingService>()\n            .ReadOrganizationLicenseAsync(Arg.Any<Guid>())\n            .Returns(organizationLicense);\n\n        sutProvider.GetDependency<ILicensingService>()\n            .VerifyLicense(organizationLicense)\n            .Returns(true);\n\n        var expected = new OrganizationConnectionResponseModel(updated, typeof(BillingSyncConfig));\n        var result = await sutProvider.Sut.UpdateConnection(existing.Id, model);\n\n        AssertHelper.AssertPropertyEqual(expected, result);\n        await sutProvider.GetDependency<IUpdateOrganizationConnectionCommand>().Received(1)\n            .UpdateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(model.ToData(updated.Id))));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateConnection_BillingSyncType_InvalidLicense_ErrorThrows(OrganizationConnection existing, BillingSyncConfig config,\n        OrganizationConnection updated,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        existing.SetConfig(new BillingSyncConfig\n        {\n            CloudOrganizationId = config.CloudOrganizationId,\n        });\n        updated.Config = JsonSerializer.Serialize(config);\n        updated.Id = existing.Id;\n        updated.OrganizationId = existing.OrganizationId;\n        updated.Type = OrganizationConnectionType.CloudBillingSync;\n        var model = RequestModelFromEntity<BillingSyncConfig>(updated);\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationConnectionRepository>()\n            .GetByOrganizationIdTypeAsync(model.OrganizationId, model.Type)\n            .Returns(new[] { existing });\n        sutProvider.GetDependency<IUpdateOrganizationConnectionCommand>()\n            .UpdateAsync<BillingSyncConfig>(default)\n            .ReturnsForAnyArgs(updated);\n        sutProvider.GetDependency<IOrganizationConnectionRepository>()\n            .GetByIdOrganizationIdAsync(existing.Id, existing.OrganizationId)\n            .Returns(existing);\n\n        OrganizationLicense organizationLicense = new OrganizationLicense();\n        var now = DateTime.UtcNow;\n        organizationLicense.Issued = now.AddDays(-10);\n        organizationLicense.Expires = now.AddDays(10);\n        organizationLicense.Version = 1;\n        organizationLicense.UsersGetPremium = true;\n        organizationLicense.Id = config.CloudOrganizationId;\n        organizationLicense.Trial = true;\n\n        sutProvider.GetDependency<ILicensingService>()\n                    .VerifyLicense(organizationLicense)\n                    .Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.UpdateConnection(existing.Id, model));\n\n        Assert.Contains(\"Cannot verify license file.\", exception.Message);\n\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateConnection_DoesNotExist_ThrowsNotFound(SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateConnection(Guid.NewGuid(), null));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetConnection_RequiresOwnerPermissions(Guid connectionId, SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.GetConnection(connectionId, OrganizationConnectionType.CloudBillingSync));\n\n        Assert.Contains(\"You do not have permission to retrieve a connection of type\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetConnection_Success(OrganizationConnection connection, BillingSyncConfig config,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        connection.Config = JsonSerializer.Serialize(config);\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);\n        sutProvider.GetDependency<IOrganizationConnectionRepository>()\n            .GetByOrganizationIdTypeAsync(connection.OrganizationId, connection.Type)\n            .Returns(new[] { connection });\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(connection.OrganizationId).Returns(true);\n\n        var expected = new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig));\n        var actual = await sutProvider.Sut.GetConnection(connection.OrganizationId, connection.Type);\n\n        AssertHelper.AssertPropertyEqual(expected, actual);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteConnection_NotFound(Guid connectionId,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteConnection(connectionId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteConnection_RequiresOwnerPermissions(OrganizationConnection connection,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationConnectionRepository>().GetByIdAsync(connection.Id).Returns(connection);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteConnection(connection.Id));\n\n        Assert.Contains(\"You do not have permission to remove this connection of type\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteConnection_Success(OrganizationConnection connection,\n        SutProvider<OrganizationConnectionsController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationConnectionRepository>().GetByIdAsync(connection.Id).Returns(connection);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(connection.OrganizationId).Returns(true);\n\n        await sutProvider.Sut.DeleteConnection(connection.Id);\n\n        await sutProvider.GetDependency<IDeleteOrganizationConnectionCommand>().DeleteAsync(connection);\n    }\n\n    private static OrganizationConnectionRequestModel<T> RequestModelFromEntity<T>(OrganizationConnection entity)\n        where T : IConnectionConfig\n    {\n        return new(new OrganizationConnectionRequestModel()\n        {\n            Type = entity.Type,\n            OrganizationId = entity.OrganizationId,\n            Enabled = entity.Enabled,\n            Config = JsonDocument.Parse(entity.Config),\n        });\n    }\n\n    private static JsonDocument JsonDocumentFromObject<T>(T obj) => JsonDocument.Parse(JsonSerializer.Serialize(obj));\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(OrganizationDomainController))]\n[SutProviderCustomize]\npublic class OrganizationDomainControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task Get_ShouldThrowUnauthorized_WhenOrgIdCannotManageSso(Guid orgId,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(false);\n\n        var requestAction = async () => await sutProvider.Sut.GetAll(orgId);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_ShouldNotFound_WhenOrganizationDoesNotExist(Guid orgId,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.GetAll(orgId);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_ShouldReturnOrganizationDomainList_WhenOrgIdIsValid(Guid orgId,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(new Organization());\n        sutProvider.GetDependency<IGetOrganizationDomainByOrganizationIdQuery>()\n            .GetDomainsByOrganizationIdAsync(orgId).Returns(new List<OrganizationDomain>\n            {\n                new()\n                {\n                    Id = Guid.NewGuid(),\n                    OrganizationId = orgId,\n                    CreationDate = DateTime.UtcNow.AddDays(-7),\n                    DomainName = \"test.com\",\n                    Txt = \"btw+12342\"\n                }\n            });\n\n        var result = await sutProvider.Sut.GetAll(orgId);\n\n        Assert.IsType<ListResponseModel<OrganizationDomainResponseModel>>(result);\n        Assert.Equal(orgId, result.Data.Select(x => x.OrganizationId).FirstOrDefault());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetByOrgIdAndId_ShouldThrowUnauthorized_WhenOrgIdCannotManageSso(Guid orgId, Guid id,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(false);\n\n        var requestAction = async () => await sutProvider.Sut.Get(orgId, id);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetByOrgIdAndId_ShouldThrowNotFound_WhenOrganizationDoesNotExist(Guid orgId, Guid id,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.Get(orgId, id);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetByOrgIdAndId_ShouldThrowNotFound_WhenOrganizationDomainEntryNotExist(Guid orgId, Guid id,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(new Organization());\n        sutProvider.GetDependency<IGetOrganizationDomainByIdOrganizationIdQuery>().GetOrganizationDomainByIdOrganizationIdAsync(id, orgId).ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.Get(orgId, id);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetByOrgIdAndId_ShouldThrowNotFound_WhenOrgIdDoesNotMatch(OrganizationDomain organizationDomain,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(organizationDomain.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization());\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)\n            .ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.Get(organizationDomain.OrganizationId, organizationDomain.Id);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_ShouldReturnOrganizationDomain_WhenOrgIdAndIdAreValid(Guid orgId, Guid id,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(new Organization());\n        sutProvider.GetDependency<IGetOrganizationDomainByIdOrganizationIdQuery>().GetOrganizationDomainByIdOrganizationIdAsync(id, orgId)\n            .Returns(new OrganizationDomain\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = orgId,\n                CreationDate = DateTime.UtcNow.AddDays(-7),\n                DomainName = \"test.com\",\n                Txt = \"btw+12342\"\n            });\n\n        var result = await sutProvider.Sut.Get(orgId, id);\n\n        Assert.IsType<OrganizationDomainResponseModel>(result);\n        Assert.Equal(orgId, result.OrganizationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_ShouldThrowUnauthorized_OrgIdCannotManageSso(Guid orgId, OrganizationDomainRequestModel model,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(false);\n\n        var requestAction = async () => await sutProvider.Sut.Post(orgId, model);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_ShouldThrowNotFound_WhenOrganizationDoesNotExist(Guid orgId, OrganizationDomainRequestModel model,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.Post(orgId, model);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_ShouldCreateEntry_WhenRequestIsValid(Guid orgId, OrganizationDomainRequestModel model,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(new Organization());\n        sutProvider.GetDependency<ICreateOrganizationDomainCommand>().CreateAsync(Arg.Any<OrganizationDomain>())\n            .Returns(new OrganizationDomain());\n\n        var result = await sutProvider.Sut.Post(orgId, model);\n\n        await sutProvider.GetDependency<ICreateOrganizationDomainCommand>().ReceivedWithAnyArgs(1)\n            .CreateAsync(Arg.Any<OrganizationDomain>());\n        Assert.IsType<OrganizationDomainResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Verify_ShouldThrowUnauthorized_WhenOrgIdCannotManageSso(Guid orgId, Guid id,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(false);\n\n        var requestAction = async () => await sutProvider.Sut.Verify(orgId, id);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Verify_ShouldThrowNotFound_WhenOrganizationDoesNotExist(Guid orgId, Guid id,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.Verify(orgId, id);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task VerifyOrganizationDomain_ShouldThrowNotFound_WhenOrgIdDoesNotMatch(OrganizationDomain organizationDomain,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(organizationDomain.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization());\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)\n            .ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Verify_WhenRequestIsValid(OrganizationDomain organizationDomain,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(organizationDomain.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization());\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)\n            .Returns(organizationDomain);\n        sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().UserVerifyOrganizationDomainAsync(organizationDomain)\n            .Returns(new OrganizationDomain());\n\n        var result = await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id);\n\n        await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().Received(1)\n            .UserVerifyOrganizationDomainAsync(organizationDomain);\n        Assert.IsType<OrganizationDomainResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveDomain_ShouldThrowUnauthorized_OrgIdCannotManageSso(Guid orgId, Guid id,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(false);\n\n        var requestAction = async () => await sutProvider.Sut.RemoveDomain(orgId, id);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveDomain_ShouldThrowNotFound_WhenOrganizationDoesNotExist(Guid orgId, Guid id,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.RemoveDomain(orgId, id);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveDomain_ShouldThrowNotFound_WhenOrgIdDoesNotMatch(OrganizationDomain organizationDomain,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(organizationDomain.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization());\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)\n            .ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.RemoveDomain(organizationDomain.OrganizationId, organizationDomain.Id);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveDomain_WhenRequestIsValid(OrganizationDomain organizationDomain,\n        SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageSso(organizationDomain.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationDomain.OrganizationId).Returns(new Organization());\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)\n            .Returns(organizationDomain);\n\n        await sutProvider.Sut.RemoveDomain(organizationDomain.OrganizationId, organizationDomain.Id);\n\n        await sutProvider.GetDependency<IDeleteOrganizationDomainCommand>().Received(1)\n            .DeleteAsync(organizationDomain);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrgDomainSsoDetails_ShouldThrowNotFound_WhenEmailHasNotClaimedDomain(\n        OrganizationDomainSsoDetailsRequestModel model, SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetOrganizationDomainSsoDetailsAsync(model.Email).ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.GetOrgDomainSsoDetails(model);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrgDomainSsoDetails_ShouldReturnOrganizationDomainSsoDetails_WhenEmailHasClaimedDomain(\n        OrganizationDomainSsoDetailsRequestModel model, OrganizationDomainSsoDetailsData ssoDetailsData, SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetOrganizationDomainSsoDetailsAsync(model.Email).Returns(ssoDetailsData);\n\n        var result = await sutProvider.Sut.GetOrgDomainSsoDetails(model);\n\n        Assert.IsType<OrganizationDomainSsoDetailsResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetVerifiedOrgDomainSsoDetails_ShouldThrowNotFound_WhenEmailHasNotClaimedDomain(\n        OrganizationDomainSsoDetailsRequestModel model, SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(Array.Empty<VerifiedOrganizationDomainSsoDetail>());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetOrgDomainSsoDetails(model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetVerifiedOrgDomainSsoDetails_ShouldReturnOrganizationDomainSsoDetails_WhenEmailHasClaimedDomain(\n        OrganizationDomainSsoDetailsRequestModel model, IEnumerable<VerifiedOrganizationDomainSsoDetail> ssoDetailsData, SutProvider<OrganizationDomainController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(ssoDetailsData);\n\n        var result = await sutProvider.Sut.GetVerifiedOrgDomainSsoDetailsAsync(model);\n\n        Assert.IsType<VerifiedOrganizationDomainSsoDetailsResponseModel>(result);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Controllers/OrganizationUserControllerPutTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.Models.Request;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(OrganizationUsersController))]\n[SutProviderCustomize]\npublic class OrganizationUserControllerPutTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Put_Success(OrganizationUserUpdateRequestModel model,\n        OrganizationUser organizationUser, OrganizationAbility organizationAbility,\n        SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)\n    {\n        // Arrange\n        Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []);\n\n        // Authorize all changes for basic happy path test\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Collection>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))\n            .Returns(AuthorizationResult.Success());\n\n        // Save these for later - organizationUser object will be mutated\n        var orgUserId = organizationUser.Id;\n        var orgUserEmail = organizationUser.Email;\n        var existingUserType = organizationUser.Type;\n\n        // Act\n        await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);\n\n        // Assert\n        await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>\n                ou.Type == model.Type &&\n                ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&\n                ou.AccessSecretsManager == model.AccessSecretsManager &&\n                ou.Id == orgUserId &&\n                ou.Email == orgUserEmail), existingUserType,\n            savingUserId,\n            Arg.Is<List<CollectionAccessSelection>>(cas =>\n                cas.All(c => model.Collections.Any(m => m.Id == c.Id))),\n            model.Groups);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_NoAdminAccess_CannotAddSelfToCollections(OrganizationUserUpdateRequestModel model,\n        OrganizationUser organizationUser, OrganizationAbility organizationAbility,\n        SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)\n    {\n        // Updating self\n        organizationUser.UserId = savingUserId;\n        organizationAbility.AllowAdminAccessToAllCollectionItems = false;\n\n        Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model));\n        Assert.Contains(\"You cannot add yourself to a collection.\", exception.Message);\n    }\n    [Theory]\n    [BitAutoData]\n    public async Task Put_NoAdminAccess_CannotAddSelfToGroups(OrganizationUserUpdateRequestModel model,\n        OrganizationUser organizationUser, OrganizationAbility organizationAbility,\n        SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)\n    {\n        // Arrange\n        // Updating self\n        organizationUser.UserId = savingUserId;\n        organizationAbility.AllowAdminAccessToAllCollectionItems = false;\n\n        Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []);\n\n        // Not changing any collection access\n        model.Collections = new List<SelectionReadOnlyRequestModel>();\n\n        var orgUserId = organizationUser.Id;\n        var orgUserEmail = organizationUser.Email;\n        var existingUserType = organizationUser.Type;\n\n        // Act\n        await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);\n\n        // Assert\n        await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>\n                ou.Type == model.Type &&\n                ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&\n                ou.AccessSecretsManager == model.AccessSecretsManager &&\n                ou.Id == orgUserId &&\n                ou.Email == orgUserEmail), existingUserType,\n            savingUserId,\n            Arg.Is<List<CollectionAccessSelection>>(cas =>\n                cas.All(c => model.Collections.Any(m => m.Id == c.Id))),\n            // Main assertion: groups are not updated (are null)\n            null);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_WithAdminAccess_CanAddSelfToGroups(OrganizationUserUpdateRequestModel model,\n        OrganizationUser organizationUser, OrganizationAbility organizationAbility,\n        SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)\n    {\n        // Arrange\n        // Updating self\n        organizationUser.UserId = savingUserId;\n        organizationAbility.AllowAdminAccessToAllCollectionItems = true;\n\n        Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []);\n\n        // Not changing any collection access\n        model.Collections = new List<SelectionReadOnlyRequestModel>();\n\n        var orgUserId = organizationUser.Id;\n        var orgUserEmail = organizationUser.Email;\n        var existingUserType = organizationUser.Type;\n\n        // Act\n        await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);\n\n        // Assert\n        await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>\n                ou.Type == model.Type &&\n                ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&\n                ou.AccessSecretsManager == model.AccessSecretsManager &&\n                ou.Id == orgUserId &&\n                ou.Email == orgUserEmail), existingUserType,\n            savingUserId,\n            Arg.Is<List<CollectionAccessSelection>>(cas =>\n                cas.All(c => model.Collections.Any(m => m.Id == c.Id))),\n            model.Groups);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateCollections_DoesNotOverwriteUnauthorizedCollections(OrganizationUserUpdateRequestModel model,\n        OrganizationUser organizationUser, OrganizationAbility organizationAbility,\n        SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)\n    {\n        // Arrange\n        var editedCollectionId = CoreHelpers.GenerateComb();\n        var readonlyCollectionId1 = CoreHelpers.GenerateComb();\n        var readonlyCollectionId2 = CoreHelpers.GenerateComb();\n\n        var currentCollectionAccess = new List<CollectionAccessSelection>\n        {\n            new()\n            {\n                Id = editedCollectionId,\n                HidePasswords = true,\n                Manage = false,\n                ReadOnly = true\n            },\n            new()\n            {\n                Id = readonlyCollectionId1,\n                HidePasswords = false,\n                Manage = true,\n                ReadOnly = false\n            },\n            new()\n            {\n                Id = readonlyCollectionId2,\n                HidePasswords = false,\n                Manage = false,\n                ReadOnly = false\n            },\n        };\n\n        Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess);\n\n        // User is upgrading editedCollectionId to manage\n        model.Collections = new List<SelectionReadOnlyRequestModel>\n        {\n            new() { Id = editedCollectionId, HidePasswords = false, Manage = true, ReadOnly = false }\n        };\n\n        // Save these for later - organizationUser object will be mutated\n        var orgUserId = organizationUser.Id;\n        var orgUserEmail = organizationUser.Email;\n\n        // Authorize the editedCollection\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == editedCollectionId),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))\n            .Returns(AuthorizationResult.Success());\n\n        // Do not authorize the readonly collections\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => c.Id == readonlyCollectionId1 || c.Id == readonlyCollectionId2),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))\n            .Returns(AuthorizationResult.Failed());\n        var existingUserType = organizationUser.Type;\n\n        // Act\n        await sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model);\n\n        // Assert\n        // Expect all collection access (modified and unmodified) to be saved\n        await sutProvider.GetDependency<IUpdateOrganizationUserCommand>().Received(1).UpdateUserAsync(Arg.Is<OrganizationUser>(ou =>\n                ou.Type == model.Type &&\n                ou.Permissions == CoreHelpers.ClassToJsonData(model.Permissions) &&\n                ou.AccessSecretsManager == model.AccessSecretsManager &&\n                ou.Id == orgUserId &&\n                ou.Email == orgUserEmail), existingUserType,\n            savingUserId,\n            Arg.Is<List<CollectionAccessSelection>>(cas =>\n                cas.Select(c => c.Id).SequenceEqual(currentCollectionAccess.Select(c => c.Id)) &&\n                cas.First(c => c.Id == editedCollectionId).Manage == true &&\n                cas.First(c => c.Id == editedCollectionId).ReadOnly == false &&\n                cas.First(c => c.Id == editedCollectionId).HidePasswords == false),\n            model.Groups);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateCollections_ThrowsIfSavingUserCannotUpdateCollections(OrganizationUserUpdateRequestModel model,\n        OrganizationUser organizationUser, OrganizationAbility organizationAbility,\n        SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)\n    {\n        // Target user is currently assigned to the POSTed collections\n        Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId,\n            currentCollectionAccess: model.Collections.Select(cas => cas.ToSelectionReadOnly()).ToList());\n\n        var postedCollectionIds = model.Collections.Select(c => c.Id).ToHashSet();\n\n        // But the saving user does not have permission to update them\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => postedCollectionIds.Contains(c.Id)),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))\n            .Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UpdateCollections_ThrowsIfSavingUserCannotAddCollections(OrganizationUserUpdateRequestModel model,\n        OrganizationUser organizationUser, OrganizationAbility organizationAbility,\n        SutProvider<OrganizationUsersController> sutProvider, Guid savingUserId)\n    {\n        // The target user is not currently assigned to any collections, so we're granting access for the first time\n        Put_Setup(sutProvider, organizationAbility, organizationUser, savingUserId, currentCollectionAccess: []);\n\n        var postedCollectionIds = model.Collections.Select(c => c.Id).ToHashSet();\n        // But the saving user does not have permission to assign access to the collections\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Is<Collection>(c => postedCollectionIds.Contains(c.Id)),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))\n            .Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Put(organizationAbility.Id, organizationUser.Id, model));\n    }\n\n    private void Put_Setup(SutProvider<OrganizationUsersController> sutProvider,\n        OrganizationAbility organizationAbility, OrganizationUser organizationUser, Guid savingUserId,\n        List<CollectionAccessSelection> currentCollectionAccess)\n    {\n        var orgId = organizationAbility.Id = organizationUser.OrganizationId;\n\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(orgId)\n            .Returns(organizationAbility);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(savingUserId);\n\n        // OrganizationUserRepository: return the user with current collection access\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdWithCollectionsAsync(organizationUser.Id)\n            .Returns(new Tuple<OrganizationUser, ICollection<CollectionAccessSelection>>(organizationUser,\n                currentCollectionAccess ?? []));\n\n        // Collection repository: return mock Collection objects for any ids passed in\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>().Select(guid => new Collection { Id = guid }).ToList());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Authorization;\nusing Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Utilities.v2.Results;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\nusing NSubstitute;\nusing OneOf.Types;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(OrganizationUsersController))]\n[SutProviderCustomize]\npublic class OrganizationUsersControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task PutResetPasswordEnrollment_InvitedUser_AcceptsInvite(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,\n        User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        orgUser.Status = Core.Enums.OrganizationUserStatusType.Invited;\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(default, default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(default, default).ReturnsForAnyArgs(orgUser);\n\n        await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);\n\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1).AcceptOrgUserByOrgIdAsync(orgId, user, sutProvider.GetDependency<IUserService>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutResetPasswordEnrollment_ConfirmedUser_AcceptsInvite(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,\n        User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        orgUser.Status = Core.Enums.OrganizationUserStatusType.Confirmed;\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(default, default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(default, default).ReturnsForAnyArgs(orgUser);\n\n        await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);\n\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(0).AcceptOrgUserByOrgIdAsync(orgId, user, sutProvider.GetDependency<IUserService>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutResetPasswordEnrollment_PasswordValidationFails_Throws(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,\n        User user, SutProvider<OrganizationUsersController> sutProvider, OrganizationUser orgUser)\n    {\n        orgUser.Status = OrganizationUserStatusType.Confirmed;\n        model.MasterPasswordHash = \"NotThePassword\";\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(default).ReturnsForAnyArgs((SsoConfig)null);\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutResetPasswordEnrollment_PasswordValidationPasses_Continues(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,\n        User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, model.MasterPasswordHash).Returns(true);\n        sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(default).ReturnsForAnyArgs((SsoConfig)null);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(default, default).ReturnsForAnyArgs(orgUser);\n        await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);\n        await sutProvider.GetDependency<IOrganizationService>().Received(1).UpdateUserResetPasswordEnrollmentAsync(\n            orgId,\n            userId,\n            model.ResetPasswordKey,\n            user.Id\n        );\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Accept_RequiresKnownUser(Guid orgId, Guid orgUserId, OrganizationUserAcceptRequestModel model,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.Accept(orgId, orgUserId, model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Accept_WhenOrganizationUserNotFound_ThrowsNotFoundException(\n        Guid orgId, Guid orgUserId, OrganizationUserAcceptRequestModel model, User user,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns((OrganizationUser)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Accept(orgId, orgUserId, model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Accept_WhenOrganizationIdMismatch_ThrowsNotFoundException(\n        Guid orgId, Guid orgUserId, OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        organizationUser.OrganizationId = Guid.NewGuid(); // Different org ID\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Accept(orgId, orgUserId, model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Accept_NoMasterPasswordReset(Guid orgId, Guid orgUserId,\n        OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        organizationUser.OrganizationId = orgId;\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n\n        await sutProvider.Sut.Accept(orgId, orgUserId, model);\n\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)\n            .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, sutProvider.GetDependency<IUserService>());\n        await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs()\n            .UpdateUserResetPasswordEnrollmentAsync(default, default, default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,\n        OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser,\n        [Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = orgId;\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n        applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });\n\n        policy.Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, });\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n\n        var policyQuery = sutProvider.GetDependency<IPolicyQuery>();\n        policyQuery.RunAsync(orgId,\n            PolicyType.ResetPassword).Returns(policy);\n\n        // Act\n        await sutProvider.Sut.Accept(orgId, orgUserId, model);\n\n        // Assert\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)\n            .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);\n        await sutProvider.GetDependency<IOrganizationService>().Received(1)\n            .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);\n\n        await userService.Received(1).GetUserByPrincipalAsync(default);\n        await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);\n        await policyQuery.Received(1).RunAsync(orgId, PolicyType.ResetPassword);\n\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Accept_WhenOrganizationUsePoliciesIsDisabled_ShouldNotHandleResetPassword(Guid orgId, Guid orgUserId,\n        OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser,\n        [Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = orgId;\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n        applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = false });\n\n        policy.Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, });\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n\n        var policyQuery = sutProvider.GetDependency<IPolicyQuery>();\n        policyQuery.RunAsync(orgId,\n            PolicyType.ResetPassword).Returns(policy);\n\n        // Act\n        await sutProvider.Sut.Accept(orgId, orgUserId, model);\n\n        // Assert\n        await userService.Received(1).GetUserByPrincipalAsync(default);\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)\n            .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);\n        await sutProvider.GetDependency<IOrganizationService>().Received(0)\n            .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);\n\n        await policyQuery.Received(0).RunAsync(orgId, PolicyType.ResetPassword);\n        await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Invite_Success(OrganizationAbility organizationAbility, OrganizationUserInviteRequestModel model,\n        Guid userId, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationAbility.Id).Returns(true);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)\n            .Returns(organizationAbility);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))\n            .Returns(AuthorizationResult.Success());\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n\n        await sutProvider.Sut.Invite(organizationAbility.Id, model);\n\n        await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUsersAsync(organizationAbility.Id,\n            userId, systemUser: null, Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites =>\n                invites.Count() == 1 &&\n                invites.First().Item1.Emails.SequenceEqual(model.Emails) &&\n                invites.First().Item1.Type == model.Type &&\n                invites.First().Item1.AccessSecretsManager == model.AccessSecretsManager));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Invite_NotAuthorizedToGiveAccessToCollections_Throws(OrganizationAbility organizationAbility, OrganizationUserInviteRequestModel model,\n        Guid userId, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationAbility.Id).Returns(true);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)\n            .Returns(organizationAbility);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ModifyUserAccess)))\n            .Returns(AuthorizationResult.Failed());\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_ReturnsUser(\n        OrganizationUserUserDetails organizationUser, ICollection<CollectionAccessSelection> collections,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        organizationUser.Permissions = null;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .ManageUsers(organizationUser.OrganizationId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetDetailsByIdWithSharedCollectionsAsync(organizationUser.Id)\n            .Returns((organizationUser, collections));\n\n        sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .GetUsersOrganizationClaimedStatusAsync(organizationUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))\n            .Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });\n\n        var response = await sutProvider.Sut.Get(organizationUser.OrganizationId, organizationUser.Id, false);\n\n        Assert.Equal(organizationUser.Id, response.Id);\n        Assert.True(response.ManagedByOrganization);\n        Assert.True(response.ClaimedByOrganization);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetMany_ReturnsUsers(\n        ICollection<OrganizationUserUserDetails> organizationUsers, OrganizationAbility organizationAbility,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        GetMany_Setup(organizationAbility, organizationUsers, sutProvider);\n        var response = await sutProvider.Sut.GetAll(organizationAbility.Id, false, false);\n\n        Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAccountRecoveryDetails_ReturnsDetails(\n        Guid organizationId,\n        OrganizationUserBulkRequestModel bulkRequestModel,\n        ICollection<OrganizationUserResetPasswordDetails> resetPasswordDetails,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageResetPassword(organizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAccountRecoveryDetailsByOrganizationUserAsync(organizationId, bulkRequestModel.Ids)\n            .Returns(resetPasswordDetails);\n\n        var response = await sutProvider.Sut.GetAccountRecoveryDetails(organizationId, bulkRequestModel);\n\n        Assert.Equal(resetPasswordDetails.Count, response.Data.Count());\n        Assert.True(response.Data.All(r =>\n            resetPasswordDetails.Any(ou =>\n                ou.OrganizationUserId == r.OrganizationUserId &&\n                ou.Kdf == r.Kdf &&\n                ou.KdfIterations == r.KdfIterations &&\n                ou.KdfMemory == r.KdfMemory &&\n                ou.KdfParallelism == r.KdfParallelism &&\n                ou.ResetPasswordKey == r.ResetPasswordKey &&\n                ou.EncryptedPrivateKey == r.EncryptedPrivateKey)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetResetPasswordDetails_WhenOrganizationUserNotFound_ThrowsNotFound(\n        Guid orgId, Guid orgUserId,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns((OrganizationUser)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetResetPasswordDetails(orgId, orgUserId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetResetPasswordDetails_WhenOrganizationIdMismatch_ThrowsNotFound(\n        Guid orgId, Guid orgUserId, OrganizationUser organizationUser,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = Guid.NewGuid(); // Different org ID\n        organizationUser.UserId = Guid.NewGuid();\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetResetPasswordDetails(orgId, orgUserId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetResetPasswordDetails_WhenUserIdIsNull_ThrowsNotFound(\n        Guid orgId, Guid orgUserId, OrganizationUser organizationUser,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = orgId;\n        organizationUser.UserId = null;\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetResetPasswordDetails(orgId, orgUserId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetResetPasswordDetails_WhenValid_ReturnsDetails(\n        Guid orgId, Guid orgUserId, OrganizationUser organizationUser, User user, Organization org,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = orgId;\n        organizationUser.UserId = user.Id;\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id).Returns(user);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);\n\n        // Act\n        var response = await sutProvider.Sut.GetResetPasswordDetails(orgId, orgUserId);\n\n        // Assert\n        Assert.Equal(organizationUser.Id, response.OrganizationUserId);\n        Assert.Equal(user.Kdf, response.Kdf);\n        Assert.Equal(user.KdfIterations, response.KdfIterations);\n        Assert.Equal(org.PrivateKey, response.EncryptedPrivateKey);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteAccount_WhenCurrentUserNotFound_ReturnsUnauthorizedResult(\n        Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs((Guid?)null);\n\n        var result = await sutProvider.Sut.DeleteAccount(orgId, id);\n\n        Assert.IsType<UnauthorizedHttpResult>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(\n        Guid orgId, OrganizationUserBulkRequestModel model, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>\n            sutProvider.Sut.BulkDeleteAccount(orgId, model));\n    }\n\n    private void GetMany_Setup(OrganizationAbility organizationAbility,\n        ICollection<OrganizationUserUserDetails> organizationUsers,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        foreach (var orgUser in organizationUsers)\n        {\n            orgUser.Permissions = null;\n        }\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)\n            .Returns(organizationAbility);\n\n        sutProvider.GetDependency<IOrganizationUserUserDetailsQuery>().GetOrganizationUserUserDetails(Arg.Any<OrganizationUserUserDetailsQueryRequest>()).Returns(organizationUsers);\n\n        sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(\n            user: Arg.Any<ClaimsPrincipal>(),\n            resource: Arg.Any<Object>(),\n            requirements: Arg.Any<IEnumerable<IAuthorizationRequirement>>())\n            .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationAbility.Id, Arg.Any<bool>(), Arg.Any<bool>())\n            .Returns(organizationUsers);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_WithPolicyRequirementsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,\n        OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = orgId;\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n        applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });\n\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n\n        var policy = new Policy\n        {\n            Enabled = true,\n            Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),\n        };\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n\n        var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();\n\n        var policyQuery = sutProvider.GetDependency<IPolicyQuery>();\n\n        var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };\n\n        policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);\n\n        // Act\n        await sutProvider.Sut.Accept(orgId, orgUserId, model);\n\n        // Assert\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)\n            .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);\n        await sutProvider.GetDependency<IOrganizationService>().Received(1)\n            .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);\n\n        await userService.Received(1).GetUserByPrincipalAsync(default);\n        await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);\n        await policyQuery.Received(0).RunAsync(orgId, PolicyType.ResetPassword);\n        await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);\n        Assert.True(policyRequirement.AutoEnrollEnabled(orgId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Accept_WithInvalidModelResetPasswordKey_WithPolicyRequirementsEnabled_ThrowsBadRequestException(Guid orgId, Guid orgUserId,\n        OrganizationUserAcceptRequestModel model, User user, OrganizationUser organizationUser, SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        model.ResetPasswordKey = \" \";\n        organizationUser.OrganizationId = orgId;\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n        applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });\n\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n\n        var policy = new Policy\n        {\n            Enabled = true,\n            Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),\n        };\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n\n        var policyQuery = sutProvider.GetDependency<IPolicyQuery>();\n\n        var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();\n\n        var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };\n\n        policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n        sutProvider.Sut.Accept(orgId, orgUserId, model));\n\n        // Assert\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(0)\n            .AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);\n        await sutProvider.GetDependency<IOrganizationService>().Received(0)\n            .UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);\n\n        await userService.Received(1).GetUserByPrincipalAsync(default);\n        await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);\n        await policyQuery.Received(0).RunAsync(orgId, PolicyType.ResetPassword);\n        await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);\n\n        Assert.Equal(\"Master Password reset is required, but not provided.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutResetPassword_WhenOrganizationUserNotFound_ReturnsNotFound(\n        Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns((OrganizationUser)null);\n\n        var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);\n\n        Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutResetPassword_WhenOrganizationIdMismatch_ReturnsNotFound(\n        Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        organizationUser.OrganizationId = Guid.NewGuid();\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n\n        var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);\n\n        Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutResetPassword_WhenAuthorizationFails_ReturnsBadRequest(\n        Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        organizationUser.OrganizationId = orgId;\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(),\n                organizationUser,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))\n            .Returns(AuthorizationResult.Failed());\n\n        var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);\n\n        Assert.IsType<BadRequest<ErrorResponseModel>>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutResetPassword_WhenRecoverAccountSucceeds_ReturnsOk(\n        Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        organizationUser.OrganizationId = orgId;\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(),\n                organizationUser,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))\n            .Returns(AuthorizationResult.Success());\n        sutProvider.GetDependency<IAdminRecoverAccountCommand>()\n            .RecoverAccountAsync(orgId, organizationUser, model.NewMasterPasswordHash, model.Key)\n            .Returns(Microsoft.AspNetCore.Identity.IdentityResult.Success);\n\n        var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);\n\n        Assert.IsType<Ok>(result);\n        await sutProvider.GetDependency<IAdminRecoverAccountCommand>().Received(1)\n            .RecoverAccountAsync(orgId, organizationUser, model.NewMasterPasswordHash, model.Key);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutResetPassword_WhenRecoverAccountFails_ReturnsBadRequest(\n        Guid orgId, Guid orgUserId, OrganizationUserResetPasswordRequestModel model, OrganizationUser organizationUser,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        organizationUser.OrganizationId = orgId;\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(organizationUser);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(),\n                organizationUser,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(x => x.SingleOrDefault() is RecoverAccountAuthorizationRequirement))\n            .Returns(AuthorizationResult.Success());\n        sutProvider.GetDependency<IAdminRecoverAccountCommand>()\n            .RecoverAccountAsync(orgId, organizationUser, model.NewMasterPasswordHash, model.Key)\n            .Returns(Microsoft.AspNetCore.Identity.IdentityResult.Failed(new Microsoft.AspNetCore.Identity.IdentityError { Description = \"Error message\" }));\n\n        var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);\n\n        Assert.IsType<BadRequest<ModelStateDictionary>>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdNull_ReturnsUnauthorized(\n        Guid orgId,\n        Guid orgUserId,\n        OrganizationUserConfirmRequestModel model,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns((Guid?)null);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);\n\n        // Assert\n        Assert.IsType<UnauthorizedHttpResult>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdEmpty_ReturnsUnauthorized(\n        Guid orgId,\n        Guid orgUserId,\n        OrganizationUserConfirmRequestModel model,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(Guid.Empty);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);\n\n        // Assert\n        Assert.IsType<UnauthorizedHttpResult>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_Success_ReturnsOk(\n        Guid orgId,\n        Guid orgUserId,\n        Guid userId,\n        OrganizationUserConfirmRequestModel model,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(userId);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()\n            .AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())\n            .Returns(new CommandResult(new None()));\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);\n\n        // Assert\n        Assert.IsType<NoContent>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_NotFoundError_ReturnsNotFound(\n        Guid orgId,\n        Guid orgUserId,\n        Guid userId,\n        OrganizationUserConfirmRequestModel model,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(userId);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(orgId)\n            .Returns(false);\n\n        var notFoundError = new OrganizationNotFound();\n        sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()\n            .AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())\n            .Returns(new CommandResult(notFoundError));\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);\n\n        // Assert\n        var notFoundResult = Assert.IsType<NotFound<ErrorResponseModel>>(result);\n        Assert.Equal(notFoundError.Message, notFoundResult.Value.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_BadRequestError_ReturnsBadRequest(\n        Guid orgId,\n        Guid orgUserId,\n        Guid userId,\n        OrganizationUserConfirmRequestModel model,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(userId);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(orgId)\n            .Returns(true);\n\n        var badRequestError = new UserIsNotAccepted();\n        sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()\n            .AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())\n            .Returns(new CommandResult(badRequestError));\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);\n\n        // Assert\n        var badRequestResult = Assert.IsType<BadRequest<ErrorResponseModel>>(result);\n        Assert.Equal(badRequestError.Message, badRequestResult.Value.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_InternalError_ReturnsProblem(\n        Guid orgId,\n        Guid orgUserId,\n        Guid userId,\n        OrganizationUserConfirmRequestModel model,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(userId);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(orgId)\n            .Returns(true);\n\n        var internalError = new FailedToWriteToEventLog();\n        sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()\n            .AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())\n            .Returns(new CommandResult(internalError));\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);\n\n        // Assert\n        var problemResult = Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);\n        Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkReinvite_UsesBulkResendOrganizationInvitesCommand(\n        Guid organizationId,\n        OrganizationUserBulkRequestModel bulkRequestModel,\n        List<OrganizationUser> organizationUsers,\n        Guid userId,\n        SutProvider<OrganizationUsersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n\n        var expectedResults = organizationUsers.Select(u => Tuple.Create(u, \"\")).ToList();\n        sutProvider.GetDependency<IBulkResendOrganizationInvitesCommand>()\n            .BulkResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids)\n            .Returns(expectedResults);\n\n        // Act\n        var response = await sutProvider.Sut.BulkReinvite(organizationId, bulkRequestModel);\n\n        // Assert\n        Assert.Equal(organizationUsers.Count, response.Data.Count());\n\n        await sutProvider.GetDependency<IBulkResendOrganizationInvitesCommand>()\n            .Received(1)\n            .BulkResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(OrganizationsController))]\n[SutProviderCustomize]\npublic class OrganizationsControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task OrganizationsController_UserCannotLeaveOrganizationThatProvidesKeyConnector(\n        SutProvider<OrganizationsController> sutProvider,\n        Guid orgId,\n        User user)\n    {\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = orgId,\n        };\n\n        user.UsesKeyConnector = true;\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationUser(orgId).Returns(true);\n        sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(orgId).Returns(ssoConfig);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IUserService>().GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { null });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Leave(orgId));\n\n        Assert.Contains(\"Your organization's Single Sign-On settings prevent you from leaving.\",\n            exception.Message);\n\n        await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().DidNotReceiveWithAnyArgs().UserLeaveAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task OrganizationsController_UserCannotLeaveOrganizationThatManagesUser(\n        SutProvider<OrganizationsController> sutProvider,\n        Guid orgId,\n        User user)\n    {\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = orgId,\n        };\n        var foundOrg = new Organization\n        {\n            Id = orgId\n        };\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationUser(orgId).Returns(true);\n        sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(orgId).Returns(ssoConfig);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IUserService>().GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { foundOrg });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Leave(orgId));\n\n        Assert.Contains(\"Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.\",\n            exception.Message);\n\n        await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default);\n    }\n\n    [Theory]\n    [BitAutoData(true, false)]\n    [BitAutoData(false, true)]\n    [BitAutoData(false, false)]\n    public async Task OrganizationsController_UserCanLeaveOrganizationThatDoesntProvideKeyConnector(\n        bool keyConnectorEnabled,\n        bool userUsesKeyConnector,\n        SutProvider<OrganizationsController> sutProvider,\n        Guid orgId,\n        User user)\n    {\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = keyConnectorEnabled\n                    ? MemberDecryptionType.KeyConnector\n                    : MemberDecryptionType.MasterPassword\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = orgId,\n        };\n\n        user.UsesKeyConnector = userUsesKeyConnector;\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationUser(orgId).Returns(true);\n        sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(orgId).Returns(ssoConfig);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IUserService>().GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization>());\n\n        await sutProvider.Sut.Leave(orgId);\n\n        await sutProvider.GetDependency<IRemoveOrganizationUserCommand>().Received(1).UserLeaveAsync(orgId, user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Delete_OrganizationIsConsolidatedBillingClient_ScalesProvidersSeats(\n        SutProvider<OrganizationsController> sutProvider,\n        Provider provider,\n        Organization organization,\n        User user,\n        Guid organizationId,\n        SecretVerificationRequestModel requestModel)\n    {\n        organization.Status = OrganizationStatusType.Managed;\n        organization.PlanType = PlanType.TeamsMonthly;\n        organization.Seats = 10;\n\n        provider.Type = ProviderType.Msp;\n        provider.Status = ProviderStatusType.Billable;\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, requestModel.Secret).Returns(true);\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);\n\n        await sutProvider.Sut.Delete(organizationId.ToString(), requestModel);\n\n        await sutProvider.GetDependency<IProviderBillingService>().Received(1)\n            .ScaleSeats(provider, organization.PlanType, -organization.Seats.Value);\n\n        await sutProvider.GetDependency<IOrganizationDeleteCommand>().Received(1).DeleteAsync(organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAutoEnrollStatus_WithPolicyRequirementsEnabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue(\n        SutProvider<OrganizationsController> sutProvider,\n        User user,\n        Organization organization,\n        OrganizationUser organizationUser)\n    {\n        var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [organization.Id] };\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);\n        sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<ResetPasswordPolicyRequirement>(user.Id).Returns(policyRequirement);\n\n        var result = await sutProvider.Sut.GetAutoEnrollStatus(organization.Id.ToString());\n\n        await sutProvider.GetDependency<IUserService>().Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdentifierAsync(organization.Id.ToString());\n        await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);\n\n        Assert.True(result.ResetPasswordEnabled);\n        Assert.Equal(result.Id, organization.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAutoEnrollStatus_WithPolicyRequirementsDisabled_ReturnsOrganizationAutoEnrollStatus_WithResetPasswordEnabledTrue(\n        SutProvider<OrganizationsController> sutProvider,\n        User user,\n        Organization organization,\n        OrganizationUser organizationUser,\n        [Policy(PolicyType.ResetPassword, data: \"{\\\"AutoEnrollEnabled\\\": true}\")] PolicyStatus policy)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);\n        sutProvider.GetDependency<IPolicyQuery>().RunAsync(organization.Id, PolicyType.ResetPassword).Returns(policy);\n\n        var result = await sutProvider.Sut.GetAutoEnrollStatus(organization.Id.ToString());\n\n        await sutProvider.GetDependency<IUserService>().Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdentifierAsync(organization.Id.ToString());\n        await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(0).GetAsync<ResetPasswordPolicyRequirement>(user.Id);\n        await sutProvider.GetDependency<IPolicyQuery>().Received(1).RunAsync(organization.Id, PolicyType.ResetPassword);\n\n        Assert.True(result.ResetPasswordEnabled);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutCollectionManagement_ValidRequest_Success(\n        SutProvider<OrganizationsController> sutProvider,\n        Organization organization,\n        OrganizationCollectionManagementUpdateRequestModel model)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);\n\n        var plan = MockPlans.Get(PlanType.EnterpriseAnnually);\n        sutProvider.GetDependency<IPricingClient>().GetPlan(Arg.Any<PlanType>()).Returns(plan);\n\n        sutProvider.GetDependency<IOrganizationService>()\n            .UpdateCollectionManagementSettingsAsync(\n                organization.Id,\n                Arg.Is<OrganizationCollectionManagementSettings>(s =>\n                    s.LimitCollectionCreation == model.LimitCollectionCreation &&\n                    s.LimitCollectionDeletion == model.LimitCollectionDeletion &&\n                    s.LimitItemDeletion == model.LimitItemDeletion &&\n                    s.AllowAdminAccessToAllCollectionItems == model.AllowAdminAccessToAllCollectionItems))\n            .Returns(organization);\n\n        // Act\n        await sutProvider.Sut.PutCollectionManagement(organization.Id, model);\n\n        // Assert\n        await sutProvider.GetDependency<IOrganizationService>()\n            .Received(1)\n            .UpdateCollectionManagementSettingsAsync(\n                organization.Id,\n                Arg.Is<OrganizationCollectionManagementSettings>(s =>\n                    s.LimitCollectionCreation == model.LimitCollectionCreation &&\n                    s.LimitCollectionDeletion == model.LimitCollectionDeletion &&\n                    s.LimitItemDeletion == model.LimitItemDeletion &&\n                    s.AllowAdminAccessToAllCollectionItems == model.AllowAdminAccessToAllCollectionItems));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.Billing.Models.Requests;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\nusing static Bit.Api.Test.Billing.Utilities;\n\nnamespace Bit.Api.Test.AdminConsole.Controllers;\n\n[ControllerCustomize(typeof(ProviderClientsController))]\n[SutProviderCustomize]\npublic class ProviderClientsControllerTests\n{\n    #region CreateAsync\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_NoPrincipalUser_Unauthorized(\n        Provider provider,\n        CreateClientOrganizationRequestBody requestBody,\n        SutProvider<ProviderClientsController> sutProvider)\n    {\n        ConfigureStableProviderAdminInputs(provider, sutProvider);\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();\n\n        var result = await sutProvider.Sut.CreateAsync(provider.Id, requestBody);\n\n        AssertUnauthorized(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_OK(\n        Provider provider,\n        CreateClientOrganizationRequestBody requestBody,\n        SutProvider<ProviderClientsController> sutProvider)\n    {\n        ConfigureStableProviderAdminInputs(provider, sutProvider);\n\n        var user = new User();\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        var clientOrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IProviderService>().CreateOrganizationAsync(\n                provider.Id,\n                Arg.Is<OrganizationSignup>(signup =>\n                    signup.Name == requestBody.Name &&\n                    signup.Plan == requestBody.PlanType &&\n                    signup.AdditionalSeats == requestBody.Seats &&\n                    signup.OwnerKey == requestBody.Key &&\n                    signup.Keys.PublicKey == requestBody.KeyPair.PublicKey &&\n                    signup.Keys.WrappedPrivateKey == requestBody.KeyPair.EncryptedPrivateKey &&\n                    signup.CollectionName == requestBody.CollectionName),\n                requestBody.OwnerEmail,\n                user)\n            .Returns(new ProviderOrganization\n            {\n                OrganizationId = clientOrganizationId\n            });\n\n        var clientOrganization = new Organization { Id = clientOrganizationId };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(clientOrganizationId)\n            .Returns(clientOrganization);\n\n        var result = await sutProvider.Sut.CreateAsync(provider.Id, requestBody);\n\n        Assert.IsType<Ok>(result);\n\n        await sutProvider.GetDependency<IProviderBillingService>().Received(1).CreateCustomerForClientOrganization(\n            provider,\n            clientOrganization);\n    }\n\n    #endregion\n\n    #region UpdateAsync\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ServiceUserMakingPurchase_Unauthorized(\n        Provider provider,\n        Guid providerOrganizationId,\n        UpdateClientOrganizationRequestBody requestBody,\n        ProviderOrganization providerOrganization,\n        Organization organization,\n        SutProvider<ProviderClientsController> sutProvider)\n    {\n        organization.PlanType = PlanType.TeamsMonthly;\n        organization.Seats = 10;\n        organization.Status = OrganizationStatusType.Managed;\n        requestBody.AssignedSeats = 20;\n        providerOrganization.ProviderId = provider.Id;\n\n        ConfigureStableProviderServiceUserInputs(provider, sutProvider);\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)\n            .Returns(providerOrganization);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(providerOrganization.OrganizationId)\n            .Returns(organization);\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);\n\n        sutProvider.GetDependency<IProviderBillingService>().SeatAdjustmentResultsInPurchase(\n            provider,\n            PlanType.TeamsMonthly,\n            10).Returns(true);\n\n        var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody);\n\n        AssertUnauthorized(result, message: \"Service users cannot purchase additional seats.\");\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ProviderOrganizationBelongsToDifferentProvider_NotFound(\n        Provider provider,\n        Guid providerOrganizationId,\n        UpdateClientOrganizationRequestBody requestBody,\n        ProviderOrganization providerOrganization,\n        SutProvider<ProviderClientsController> sutProvider)\n    {\n        ConfigureStableProviderServiceUserInputs(provider, sutProvider);\n\n        providerOrganization.ProviderId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)\n            .Returns(providerOrganization);\n\n        var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody);\n\n        AssertNotFound(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_Ok(\n        Provider provider,\n        Guid providerOrganizationId,\n        UpdateClientOrganizationRequestBody requestBody,\n        ProviderOrganization providerOrganization,\n        Organization organization,\n        SutProvider<ProviderClientsController> sutProvider)\n    {\n        organization.PlanType = PlanType.TeamsMonthly;\n        organization.Seats = 10;\n        organization.Status = OrganizationStatusType.Managed;\n        requestBody.AssignedSeats = 20;\n        providerOrganization.ProviderId = provider.Id;\n\n        ConfigureStableProviderServiceUserInputs(provider, sutProvider);\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)\n            .Returns(providerOrganization);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(providerOrganization.OrganizationId)\n            .Returns(organization);\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(false);\n\n        sutProvider.GetDependency<IProviderBillingService>().SeatAdjustmentResultsInPurchase(\n            provider,\n            PlanType.TeamsMonthly,\n            10).Returns(false);\n\n        var result = await sutProvider.Sut.UpdateAsync(provider.Id, providerOrganizationId, requestBody);\n\n        await sutProvider.GetDependency<IProviderBillingService>().Received(1)\n            .ScaleSeats(\n                provider,\n                PlanType.TeamsMonthly,\n                10);\n\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1)\n            .ReplaceAsync(Arg.Is<Organization>(org => org.Seats == requestBody.AssignedSeats && org.Name == requestBody.Name));\n\n        Assert.IsType<Ok>(result);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Jobs/OrganizationSubscriptionUpdateJobTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Jobs;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Models.Data.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Quartz;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Jobs;\n\n[SutProviderCustomize]\npublic class OrganizationSubscriptionUpdateJobTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ExecuteJobAsync_WhenScimInviteUserIsDisabled_ThenQueryAndCommandAreNotExecuted(\n        SutProvider<OrganizationSubscriptionUpdateJob> sutProvider)\n    {\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization)\n            .Returns(false);\n\n        var contextMock = Substitute.For<IJobExecutionContext>();\n\n        await sutProvider.Sut.Execute(contextMock);\n\n        await sutProvider.GetDependency<IGetOrganizationSubscriptionsToUpdateQuery>()\n            .DidNotReceive()\n            .GetOrganizationSubscriptionsToUpdateAsync();\n\n        await sutProvider.GetDependency<IBulkUpdateOrganizationSubscriptionsCommand>()\n            .DidNotReceive()\n            .BulkUpdateOrganizationSubscriptionsAsync(Arg.Any<IEnumerable<OrganizationSubscriptionUpdate>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ExecuteJobAsync_WhenScimInviteUserIsEnabled_ThenQueryAndCommandAreExecuted(\n        SutProvider<OrganizationSubscriptionUpdateJob> sutProvider)\n    {\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization)\n            .Returns(true);\n\n        var contextMock = Substitute.For<IJobExecutionContext>();\n\n        await sutProvider.Sut.Execute(contextMock);\n\n        await sutProvider.GetDependency<IGetOrganizationSubscriptionsToUpdateQuery>()\n            .Received(1)\n            .GetOrganizationSubscriptionsToUpdateAsync();\n\n        await sutProvider.GetDependency<IBulkUpdateOrganizationSubscriptionsCommand>()\n            .Received(1)\n            .BulkUpdateOrganizationSubscriptionsAsync(Arg.Any<IEnumerable<OrganizationSubscriptionUpdate>>());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Models/Request/SavePolicyRequestTests.cs",
    "content": "﻿\nusing System.Text.Json;\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.Context;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Models.Request;\n\n[SutProviderCustomize]\npublic class SavePolicyRequestTests\n{\n    [Theory, BitAutoData]\n    public async Task ToSavePolicyModelAsync_WithValidData_ReturnsCorrectSavePolicyModel(\n        Guid organizationId,\n        Guid userId)\n    {\n        // Arrange\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.UserId.Returns(userId);\n        currentContext.OrganizationOwner(organizationId).Returns(true);\n\n        var testData = new Dictionary<string, object> { { \"test\", \"value\" } };\n        var policyType = PolicyType.TwoFactorAuthentication;\n        var model = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true,\n                Data = testData\n            },\n            Metadata = new Dictionary<string, object>()\n        };\n\n        // Act\n        var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);\n\n        // Assert\n        Assert.Equal(PolicyType.TwoFactorAuthentication, result.PolicyUpdate.Type);\n        Assert.Equal(organizationId, result.PolicyUpdate.OrganizationId);\n        Assert.True(result.PolicyUpdate.Enabled);\n        Assert.NotNull(result.PolicyUpdate.Data);\n\n        var deserializedData = JsonSerializer.Deserialize<Dictionary<string, object>>(result.PolicyUpdate.Data);\n        Assert.Equal(\"value\", deserializedData[\"test\"].ToString());\n\n        Assert.Equal(userId, result!.PerformedBy.UserId);\n        Assert.True(result!.PerformedBy.IsOrganizationOwnerOrProvider);\n\n        Assert.IsType<EmptyMetadataModel>(result.Metadata);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ToSavePolicyModelAsync_WithEmptyData_HandlesCorrectly(\n        Guid organizationId,\n        Guid userId)\n    {\n        // Arrange\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.UserId.Returns(userId);\n        currentContext.OrganizationOwner(organizationId).Returns(false);\n\n        var policyType = PolicyType.SingleOrg;\n        var model = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = false\n            }\n        };\n\n        // Act\n        var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);\n\n        // Assert\n        Assert.Null(result.PolicyUpdate.Data);\n        Assert.False(result.PolicyUpdate.Enabled);\n\n        Assert.Equal(userId, result!.PerformedBy.UserId);\n        Assert.False(result!.PerformedBy.IsOrganizationOwnerOrProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ToSavePolicyModelAsync_WithNonOrganizationOwner_HandlesCorrectly(\n        Guid organizationId,\n        Guid userId)\n    {\n        // Arrange\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.UserId.Returns(userId);\n        currentContext.OrganizationOwner(organizationId).Returns(true);\n\n        var policyType = PolicyType.SingleOrg;\n        var model = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = false\n            }\n        };\n\n        // Act\n        var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);\n\n        // Assert\n        Assert.Null(result.PolicyUpdate.Data);\n        Assert.False(result.PolicyUpdate.Enabled);\n\n        Assert.Equal(userId, result!.PerformedBy.UserId);\n        Assert.True(result!.PerformedBy.IsOrganizationOwnerOrProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithValidMetadata_ReturnsCorrectMetadata(\n        Guid organizationId,\n        Guid userId,\n        string defaultCollectionName)\n    {\n        // Arrange\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.UserId.Returns(userId);\n        currentContext.OrganizationOwner(organizationId).Returns(true);\n\n        var policyType = PolicyType.OrganizationDataOwnership;\n        var model = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true\n            },\n            Metadata = new Dictionary<string, object>\n            {\n                { \"defaultUserCollectionName\", defaultCollectionName }\n            }\n        };\n\n        // Act\n        var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);\n\n        // Assert\n        Assert.IsType<OrganizationModelOwnershipPolicyModel>(result.Metadata);\n        var metadata = (OrganizationModelOwnershipPolicyModel)result.Metadata;\n        Assert.Equal(defaultCollectionName, metadata.DefaultUserCollectionName);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithEmptyMetadata_ReturnsEmptyMetadata(\n        Guid organizationId,\n        Guid userId)\n    {\n        // Arrange\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.UserId.Returns(userId);\n        currentContext.OrganizationOwner(organizationId).Returns(true);\n\n        var policyType = PolicyType.OrganizationDataOwnership;\n        var model = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true\n            }\n        };\n\n        // Act\n        var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<EmptyMetadataModel>(result.Metadata);\n    }\n\n    private static readonly Dictionary<string, object> _complexData = new Dictionary<string,\n     object>\n      {\n          { \"stringValue\", \"test\" },\n          { \"numberValue\", 42 },\n          { \"boolValue\", true },\n          { \"arrayValue\", new[] { \"item1\", \"item2\" } },\n          { \"nestedObject\", new Dictionary<string, object> { { \"nested\", \"value\" } } }\n      };\n\n    [Theory, BitAutoData]\n    public async Task ToSavePolicyModelAsync_ComplexData_SerializesCorrectly(\n        Guid organizationId,\n        Guid userId)\n    {\n        // Arrange\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.UserId.Returns(userId);\n        currentContext.OrganizationOwner(organizationId).Returns(true);\n\n        var policyType = PolicyType.ResetPassword;\n        var model = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true,\n                Data = _complexData\n            },\n            Metadata = new Dictionary<string, object>()\n        };\n\n        // Act\n        var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);\n\n        // Assert\n        var deserializedData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(result.PolicyUpdate.Data);\n        Assert.Equal(\"test\", deserializedData[\"stringValue\"].GetString());\n        Assert.Equal(42, deserializedData[\"numberValue\"].GetInt32());\n        Assert.True(deserializedData[\"boolValue\"].GetBoolean());\n        Assert.Equal(2, deserializedData[\"arrayValue\"].GetArrayLength());\n        var array = deserializedData[\"arrayValue\"].EnumerateArray()\n            .Select(e => e.GetString())\n            .ToArray();\n        Assert.Contains(\"item1\", array);\n        Assert.Contains(\"item2\", array);\n        Assert.True(deserializedData[\"nestedObject\"].TryGetProperty(\"nested\", out var nestedValue));\n        Assert.Equal(\"value\", nestedValue.GetString());\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task MapToPolicyMetadata_UnknownPolicyType_ReturnsEmptyMetadata(\n        Guid organizationId,\n        Guid userId)\n    {\n        // Arrange\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.UserId.Returns(userId);\n        currentContext.OrganizationOwner(organizationId).Returns(true);\n\n        var policyType = PolicyType.MaximumVaultTimeout;\n        var model = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true\n            },\n            Metadata = new Dictionary<string, object>\n            {\n                { \"someProperty\", \"someValue\" }\n            }\n        };\n\n        // Act\n        var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<EmptyMetadataModel>(result.Metadata);\n    }\n\n    [Theory, BitAutoData]\n    public async Task MapToPolicyMetadata_JsonSerializationException_ReturnsEmptyMetadata(\n        Guid organizationId,\n        Guid userId)\n    {\n        // Arrange\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.UserId.Returns(userId);\n        currentContext.OrganizationOwner(organizationId).Returns(true);\n\n        var errorDictionary = BuildErrorDictionary();\n        var policyType = PolicyType.OrganizationDataOwnership;\n        var model = new SavePolicyRequest\n        {\n            Policy = new PolicyRequestModel\n            {\n                Enabled = true\n            },\n            Metadata = errorDictionary\n        };\n\n        // Act\n        var result = await model.ToSavePolicyModelAsync(organizationId, policyType, currentContext);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<EmptyMetadataModel>(result.Metadata);\n    }\n\n    private static Dictionary<string, object> BuildErrorDictionary()\n    {\n        var circularDict = new Dictionary<string, object>();\n        circularDict[\"self\"] = circularDict;\n        return circularDict;\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Models/Response/Helpers/PolicyStatusResponsesTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Response.Helpers;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Models.Response.Helpers;\n\npublic class PolicyStatusResponsesTests\n{\n    [Theory]\n    [InlineData(true, false)]\n    [InlineData(false, true)]\n    public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsSingleOrgTypeAndHasVerifiedDomains_ShouldReturnExpectedToggleState(\n        bool policyEnabled,\n        bool expectedCanToggle)\n    {\n        var policy = new PolicyStatus(Guid.NewGuid(), PolicyType.SingleOrg) { Enabled = policyEnabled };\n\n        var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();\n        querySub.HasVerifiedDomainsAsync(policy.OrganizationId)\n            .Returns(true);\n\n        var result = await policy.GetSingleOrgPolicyStatusResponseAsync(querySub);\n\n        Assert.Equal(expectedCanToggle, result.CanToggleState);\n    }\n\n    [Fact]\n    public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsNotSingleOrgType_ThenShouldThrowArgumentException()\n    {\n        var policy = new PolicyStatus(Guid.NewGuid(), PolicyType.TwoFactorAuthentication);\n\n        var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();\n        querySub.HasVerifiedDomainsAsync(policy.OrganizationId)\n            .Returns(true);\n\n        var action = async () => await policy.GetSingleOrgPolicyStatusResponseAsync(querySub);\n\n        await Assert.ThrowsAsync<ArgumentException>(\"policy\", action);\n    }\n\n    [Fact]\n    public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsSingleOrgTypeAndDoesNotHaveVerifiedDomains_ThenShouldBeAbleToToggle()\n    {\n        var policy = new PolicyStatus(Guid.NewGuid(), PolicyType.SingleOrg);\n\n        var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();\n        querySub.HasVerifiedDomainsAsync(policy.OrganizationId)\n            .Returns(false);\n\n        var result = await policy.GetSingleOrgPolicyStatusResponseAsync(querySub);\n\n        Assert.True(result.CanToggleState);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Models/Response/ProfileOrganizationResponseModelTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Models.Response;\n\npublic class ProfileOrganizationResponseModelTests\n{\n    [Theory, BitAutoData]\n    public void Constructor_ShouldPopulatePropertiesCorrectly(Organization organization)\n    {\n        var userId = Guid.NewGuid();\n        var organizationUserId = Guid.NewGuid();\n        var providerId = Guid.NewGuid();\n        var organizationIdsClaimingUser = new[] { organization.Id };\n\n        var organizationDetails = new OrganizationUserOrganizationDetails\n        {\n            OrganizationId = organization.Id,\n            UserId = userId,\n            OrganizationUserId = organizationUserId,\n            Name = organization.Name,\n            Enabled = organization.Enabled,\n            Identifier = organization.Identifier,\n            PlanType = organization.PlanType,\n            UsePolicies = organization.UsePolicies,\n            UseSso = organization.UseSso,\n            UseKeyConnector = organization.UseKeyConnector,\n            UseScim = organization.UseScim,\n            UseGroups = organization.UseGroups,\n            UseDirectory = organization.UseDirectory,\n            UseEvents = organization.UseEvents,\n            UseTotp = organization.UseTotp,\n            Use2fa = organization.Use2fa,\n            UseApi = organization.UseApi,\n            UseResetPassword = organization.UseResetPassword,\n            UseSecretsManager = organization.UseSecretsManager,\n            UsePasswordManager = organization.UsePasswordManager,\n            UsersGetPremium = organization.UsersGetPremium,\n            UseCustomPermissions = organization.UseCustomPermissions,\n            UseRiskInsights = organization.UseRiskInsights,\n            UsePhishingBlocker = organization.UsePhishingBlocker,\n            UseDisableSMAdsForUsers = organization.UseDisableSmAdsForUsers,\n            UseOrganizationDomains = organization.UseOrganizationDomains,\n            UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,\n            UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,\n            SelfHost = organization.SelfHost,\n            Seats = organization.Seats,\n            MaxCollections = organization.MaxCollections,\n            MaxStorageGb = organization.MaxStorageGb,\n            Key = \"organization-key\",\n            PublicKey = \"public-key\",\n            PrivateKey = \"private-key\",\n            LimitCollectionCreation = organization.LimitCollectionCreation,\n            LimitCollectionDeletion = organization.LimitCollectionDeletion,\n            LimitItemDeletion = organization.LimitItemDeletion,\n            AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems,\n            ProviderId = providerId,\n            ProviderName = \"Test Provider\",\n            ProviderType = ProviderType.Msp,\n            SsoEnabled = true,\n            SsoConfig = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector,\n                KeyConnectorUrl = \"https://keyconnector.example.com\"\n            }.Serialize(),\n            SsoExternalId = \"external-sso-id\",\n            Permissions = CoreHelpers.ClassToJsonData(new Core.Models.Data.Permissions { ManageUsers = true }),\n            ResetPasswordKey = \"reset-password-key\",\n            FamilySponsorshipFriendlyName = \"Family Sponsorship\",\n            FamilySponsorshipLastSyncDate = DateTime.UtcNow.AddDays(-1),\n            FamilySponsorshipToDelete = false,\n            FamilySponsorshipValidUntil = DateTime.UtcNow.AddYears(1),\n            IsAdminInitiated = true,\n            Status = OrganizationUserStatusType.Confirmed,\n            Type = OrganizationUserType.Owner,\n            AccessSecretsManager = true,\n            SmSeats = 5,\n            SmServiceAccounts = 10\n        };\n\n        var result = new ProfileOrganizationResponseModel(organizationDetails, organizationIdsClaimingUser);\n\n        Assert.Equal(\"profileOrganization\", result.Object);\n        Assert.Equal(organization.Id, result.Id);\n        Assert.Equal(userId, result.UserId);\n        Assert.Equal(organization.Name, result.Name);\n        Assert.Equal(organization.Enabled, result.Enabled);\n        Assert.Equal(organization.Identifier, result.Identifier);\n        Assert.Equal(organization.PlanType.GetProductTier(), result.ProductTierType);\n        Assert.Equal(organization.UsePolicies, result.UsePolicies);\n        Assert.Equal(organization.UseSso, result.UseSso);\n        Assert.Equal(organization.UseKeyConnector, result.UseKeyConnector);\n        Assert.Equal(organization.UseScim, result.UseScim);\n        Assert.Equal(organization.UseGroups, result.UseGroups);\n        Assert.Equal(organization.UseDirectory, result.UseDirectory);\n        Assert.Equal(organization.UseEvents, result.UseEvents);\n        Assert.Equal(organization.UseTotp, result.UseTotp);\n        Assert.Equal(organization.Use2fa, result.Use2fa);\n        Assert.Equal(organization.UseApi, result.UseApi);\n        Assert.Equal(organization.UseResetPassword, result.UseResetPassword);\n        Assert.Equal(organization.UseSecretsManager, result.UseSecretsManager);\n        Assert.Equal(organization.UsePasswordManager, result.UsePasswordManager);\n        Assert.Equal(organization.UsersGetPremium, result.UsersGetPremium);\n        Assert.Equal(organization.UseCustomPermissions, result.UseCustomPermissions);\n        Assert.Equal(organization.PlanType.GetProductTier() == ProductTierType.Enterprise, result.UseActivateAutofillPolicy);\n        Assert.Equal(organization.UseRiskInsights, result.UseRiskInsights);\n        Assert.Equal(organization.UseOrganizationDomains, result.UseOrganizationDomains);\n        Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies);\n        Assert.Equal(organization.UseAutomaticUserConfirmation, result.UseAutomaticUserConfirmation);\n        Assert.Equal(organization.SelfHost, result.SelfHost);\n        Assert.Equal(organization.Seats, result.Seats);\n        Assert.Equal(organization.MaxCollections, result.MaxCollections);\n        Assert.Equal(organization.MaxStorageGb, result.MaxStorageGb);\n        Assert.Equal(organizationDetails.Key, result.Key);\n        Assert.True(result.HasPublicAndPrivateKeys);\n        Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation);\n        Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion);\n        Assert.Equal(organization.LimitItemDeletion, result.LimitItemDeletion);\n        Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems);\n        Assert.Equal(organizationDetails.ProviderId, result.ProviderId);\n        Assert.Equal(organizationDetails.ProviderName, result.ProviderName);\n        Assert.Equal(organizationDetails.ProviderType, result.ProviderType);\n        Assert.Equal(organizationDetails.SsoEnabled, result.SsoEnabled);\n        Assert.True(result.KeyConnectorEnabled);\n        Assert.Equal(\"https://keyconnector.example.com\", result.KeyConnectorUrl);\n        Assert.Equal(MemberDecryptionType.KeyConnector, result.SsoMemberDecryptionType);\n        Assert.True(result.SsoBound);\n        Assert.Equal(organizationDetails.Status, result.Status);\n        Assert.Equal(organizationDetails.Type, result.Type);\n        Assert.Equal(organizationDetails.OrganizationUserId, result.OrganizationUserId);\n        Assert.True(result.UserIsClaimedByOrganization);\n        Assert.NotNull(result.Permissions);\n        Assert.True(result.ResetPasswordEnrolled);\n        Assert.Equal(organizationDetails.AccessSecretsManager, result.AccessSecretsManager);\n        Assert.Equal(organizationDetails.FamilySponsorshipFriendlyName, result.FamilySponsorshipFriendlyName);\n        Assert.Equal(organizationDetails.FamilySponsorshipLastSyncDate, result.FamilySponsorshipLastSyncDate);\n        Assert.Equal(organizationDetails.FamilySponsorshipToDelete, result.FamilySponsorshipToDelete);\n        Assert.Equal(organizationDetails.FamilySponsorshipValidUntil, result.FamilySponsorshipValidUntil);\n        Assert.True(result.IsAdminInitiated);\n        Assert.False(result.FamilySponsorshipAvailable);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModelTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Models.Response;\n\npublic class ProfileProviderOrganizationResponseModelTests\n{\n    [Theory, BitAutoData]\n    public void Constructor_ShouldPopulatePropertiesCorrectly(Organization organization)\n    {\n        var userId = Guid.NewGuid();\n        var providerId = Guid.NewGuid();\n        var providerUserId = Guid.NewGuid();\n\n        var organizationDetails = new ProviderUserOrganizationDetails\n        {\n            OrganizationId = organization.Id,\n            UserId = userId,\n            Name = organization.Name,\n            Enabled = organization.Enabled,\n            Identifier = organization.Identifier,\n            PlanType = organization.PlanType,\n            UsePolicies = organization.UsePolicies,\n            UseSso = organization.UseSso,\n            UseKeyConnector = organization.UseKeyConnector,\n            UseScim = organization.UseScim,\n            UseGroups = organization.UseGroups,\n            UseDirectory = organization.UseDirectory,\n            UseEvents = organization.UseEvents,\n            UseTotp = organization.UseTotp,\n            Use2fa = organization.Use2fa,\n            UseApi = organization.UseApi,\n            UseResetPassword = organization.UseResetPassword,\n            UseSecretsManager = organization.UseSecretsManager,\n            UsePasswordManager = organization.UsePasswordManager,\n            UsersGetPremium = organization.UsersGetPremium,\n            UseCustomPermissions = organization.UseCustomPermissions,\n            UseRiskInsights = organization.UseRiskInsights,\n            UsePhishingBlocker = organization.UsePhishingBlocker,\n            UseDisableSMAdsForUsers = organization.UseDisableSmAdsForUsers,\n            UseOrganizationDomains = organization.UseOrganizationDomains,\n            UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,\n            UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,\n            SelfHost = organization.SelfHost,\n            Seats = organization.Seats,\n            MaxCollections = organization.MaxCollections,\n            MaxStorageGb = organization.MaxStorageGb,\n            Key = \"provider-org-key\",\n            PublicKey = \"public-key\",\n            PrivateKey = \"private-key\",\n            LimitCollectionCreation = organization.LimitCollectionCreation,\n            LimitCollectionDeletion = organization.LimitCollectionDeletion,\n            LimitItemDeletion = organization.LimitItemDeletion,\n            AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems,\n            ProviderId = providerId,\n            ProviderName = \"Test MSP Provider\",\n            ProviderType = ProviderType.Msp,\n            SsoEnabled = true,\n            SsoConfig = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption\n            }.Serialize(),\n            Status = ProviderUserStatusType.Confirmed,\n            Type = ProviderUserType.ProviderAdmin,\n            ProviderUserId = providerUserId\n        };\n\n        var result = new ProfileProviderOrganizationResponseModel(organizationDetails);\n\n        Assert.Equal(\"profileProviderOrganization\", result.Object);\n        Assert.Equal(organization.Id, result.Id);\n        Assert.Equal(userId, result.UserId);\n        Assert.Equal(organization.Name, result.Name);\n        Assert.Equal(organization.Enabled, result.Enabled);\n        Assert.Equal(organization.Identifier, result.Identifier);\n        Assert.Equal(organization.PlanType.GetProductTier(), result.ProductTierType);\n        Assert.Equal(organization.UsePolicies, result.UsePolicies);\n        Assert.Equal(organization.UseSso, result.UseSso);\n        Assert.Equal(organization.UseKeyConnector, result.UseKeyConnector);\n        Assert.Equal(organization.UseScim, result.UseScim);\n        Assert.Equal(organization.UseGroups, result.UseGroups);\n        Assert.Equal(organization.UseDirectory, result.UseDirectory);\n        Assert.Equal(organization.UseEvents, result.UseEvents);\n        Assert.Equal(organization.UseTotp, result.UseTotp);\n        Assert.Equal(organization.Use2fa, result.Use2fa);\n        Assert.Equal(organization.UseApi, result.UseApi);\n        Assert.Equal(organization.UseResetPassword, result.UseResetPassword);\n        Assert.Equal(organization.UseSecretsManager, result.UseSecretsManager);\n        Assert.Equal(organization.UsePasswordManager, result.UsePasswordManager);\n        Assert.Equal(organization.UsersGetPremium, result.UsersGetPremium);\n        Assert.Equal(organization.UseCustomPermissions, result.UseCustomPermissions);\n        Assert.Equal(organization.PlanType.GetProductTier() == ProductTierType.Enterprise, result.UseActivateAutofillPolicy);\n        Assert.Equal(organization.UseRiskInsights, result.UseRiskInsights);\n        Assert.Equal(organization.UseOrganizationDomains, result.UseOrganizationDomains);\n        Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies);\n        Assert.Equal(organization.UseAutomaticUserConfirmation, result.UseAutomaticUserConfirmation);\n        Assert.Equal(organization.SelfHost, result.SelfHost);\n        Assert.Equal(organization.Seats, result.Seats);\n        Assert.Equal(organization.MaxCollections, result.MaxCollections);\n        Assert.Equal(organization.MaxStorageGb, result.MaxStorageGb);\n        Assert.Equal(organizationDetails.Key, result.Key);\n        Assert.True(result.HasPublicAndPrivateKeys);\n        Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation);\n        Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion);\n        Assert.Equal(organization.LimitItemDeletion, result.LimitItemDeletion);\n        Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems);\n        Assert.Equal(organizationDetails.ProviderId, result.ProviderId);\n        Assert.Equal(organizationDetails.ProviderName, result.ProviderName);\n        Assert.Equal(organizationDetails.ProviderType, result.ProviderType);\n        Assert.Equal(OrganizationUserStatusType.Confirmed, result.Status);\n        Assert.Equal(OrganizationUserType.Owner, result.Type);\n        Assert.Equal(organizationDetails.SsoEnabled, result.SsoEnabled);\n        Assert.False(result.KeyConnectorEnabled);\n        Assert.Null(result.KeyConnectorUrl);\n        Assert.Equal(MemberDecryptionType.TrustedDeviceEncryption, result.SsoMemberDecryptionType);\n        Assert.False(result.SsoBound);\n        Assert.NotNull(result.Permissions);\n        Assert.False(result.Permissions.ManageUsers);\n        Assert.False(result.ResetPasswordEnrolled);\n        Assert.False(result.AccessSecretsManager);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Public/Controllers/GroupsControllerTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Public.Controllers;\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Api.AdminConsole.Public.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Public.Controllers;\n\n[ControllerCustomize(typeof(GroupsController))]\n[SutProviderCustomize]\npublic class GroupsControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Post_Success(Organization organization, GroupCreateUpdateRequestModel groupRequestModel, SutProvider<GroupsController> sutProvider)\n    {\n        // Contains at least one can manage\n        groupRequestModel.Collections.First().Manage = true;\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationId.Returns(organization.Id);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var response = await sutProvider.Sut.Post(groupRequestModel) as JsonResult;\n        var responseValue = response.Value as GroupResponseModel;\n\n        await sutProvider.GetDependency<ICreateGroupCommand>().Received(1).CreateGroupAsync(\n            Arg.Is<Group>(g =>\n                g.OrganizationId == organization.Id && g.Name == groupRequestModel.Name &&\n                g.ExternalId == groupRequestModel.ExternalId),\n            organization,\n            Arg.Any<ICollection<CollectionAccessSelection>>());\n\n        Assert.Equal(groupRequestModel.Name, responseValue.Name);\n        Assert.Equal(groupRequestModel.ExternalId, responseValue.ExternalId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_Success(Organization organization, Group group, GroupCreateUpdateRequestModel groupRequestModel, SutProvider<GroupsController> sutProvider)\n    {\n        // Contains at least one can manage\n        groupRequestModel.Collections.First().Manage = true;\n\n        group.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(group.Id).Returns(group);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationId.Returns(organization.Id);\n\n        var response = await sutProvider.Sut.Put(group.Id, groupRequestModel) as JsonResult;\n        var responseValue = response.Value as GroupResponseModel;\n\n        await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(\n            Arg.Is<Group>(g =>\n                g.OrganizationId == organization.Id && g.Name == groupRequestModel.Name &&\n                g.ExternalId == groupRequestModel.ExternalId),\n            Arg.Is<Organization>(o => o.Id == organization.Id),\n            Arg.Any<ICollection<CollectionAccessSelection>>());\n\n        Assert.Equal(groupRequestModel.Name, responseValue.Name);\n        Assert.Equal(groupRequestModel.ExternalId, responseValue.ExternalId);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Public/Controllers/PoliciesControllerTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Public.Controllers;\nusing Bit.Api.AdminConsole.Public.Models.Request;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Public.Controllers;\n\n[ControllerCustomize(typeof(PoliciesController))]\n[SutProviderCustomize]\npublic class PoliciesControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UsesVNextSavePolicyCommand(\n        Guid organizationId,\n        PolicyType policyType,\n        PolicyUpdateRequestModel model,\n        Policy policy,\n        SutProvider<PoliciesController> sutProvider)\n    {\n        // Arrange\n        policy.Data = null;\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationId.Returns(organizationId);\n        sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .SaveAsync(Arg.Any<SavePolicyModel>())\n            .Returns(policy);\n\n        // Act\n        await sutProvider.Sut.Put(policyType, model);\n\n        // Assert\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .Received(1)\n            .SaveAsync(Arg.Is<SavePolicyModel>(m =>\n                m.PolicyUpdate.OrganizationId == organizationId &&\n                m.PolicyUpdate.Type == policyType &&\n                m.PolicyUpdate.Enabled == model.Enabled.GetValueOrDefault() &&\n                m.PerformedBy is SystemUser));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Public/Models/Response/MemberResponseModelTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Public.Models.Response;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.AdminConsole.Public.Models.Response;\n\n\npublic class MemberResponseModelTests\n{\n    [Fact]\n    public void ResetPasswordEnrolled_ShouldBeTrue_WhenUserIsResetPasswordEnrolled()\n    {\n        // Arrange\n        var user = Substitute.For<OrganizationUser>();\n        var collections = Substitute.For<IEnumerable<CollectionAccessSelection>>();\n        user.ResetPasswordKey = \"none-empty\";\n\n        // Act\n        var sut = new MemberResponseModel(user, collections);\n\n        // Assert\n        Assert.True(sut.ResetPasswordEnrolled);\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\"   \")]\n    public void ResetPasswordEnrolled_ShouldBeFalse_WhenResetPasswordKeyIsInvalid(string? resetPasswordKey)\n    {\n        // Arrange\n        var user = Substitute.For<OrganizationUser>();\n        user.ResetPasswordKey = resetPasswordKey;\n\n        var collections = Substitute.For<IEnumerable<CollectionAccessSelection>>();\n\n        // Act\n        var sut = new MemberResponseModel(user, collections);\n\n        // Assert\n        Assert.False(sut.ResetPasswordEnrolled);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/AdminConsole/Queries/OrganizationUserUserDetailsQueryTests.cs",
    "content": "﻿using Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Api.Test.AdminConsole.Queries;\n\n[SutProviderCustomize]\npublic class OrganizationUserUserDetailsQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Get_HandlesNullPermissionsObject(\n        ICollection<OrganizationUserUserDetails> organizationUsers,\n        SutProvider<OrganizationUserUserDetailsQuery> sutProvider,\n        Guid organizationId)\n    {\n        Get_Setup(organizationUsers, sutProvider, organizationId);\n        organizationUsers.First().Permissions = \"null\";\n        var response = await sutProvider.Sut.GetOrganizationUserUserDetails(new OrganizationUserUserDetailsQueryRequest { OrganizationId = organizationId });\n\n        Assert.True(response.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Get_ReturnsUsers(\n        ICollection<OrganizationUserUserDetails> organizationUsers,\n        SutProvider<OrganizationUserUserDetailsQuery> sutProvider,\n        Guid organizationId)\n    {\n        Get_Setup(organizationUsers, sutProvider, organizationId);\n        var response = await sutProvider.Sut.GetOrganizationUserUserDetails(new OrganizationUserUserDetailsQueryRequest { OrganizationId = organizationId });\n\n        Assert.True(response.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));\n    }\n\n    private void Get_Setup(\n        ICollection<OrganizationUserUserDetails> organizationUsers,\n        SutProvider<OrganizationUserUserDetailsQuery> sutProvider,\n        Guid organizationId)\n    {\n        foreach (var orgUser in organizationUsers)\n        {\n            orgUser.Permissions = null;\n        }\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId, Arg.Any<bool>(), Arg.Any<bool>())\n            .Returns(organizationUsers);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Api.Test.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <IsPackable>false</IsPackable>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304</WarningsNotAsErrors>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"NSubstitute\" Version=\"$(NSubstituteVersion)\" />\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"AutoFixture.Xunit2\" Version=\"$(AutoFixtureXUnit2Version)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Api\\Api.csproj\" />\n    <ProjectReference Include=\"..\\..\\src\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\Common\\Common.csproj\" />\n    <ProjectReference Include=\"..\\Core.Test\\Core.Test.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Api.Test/Auth/Controllers/AccountsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.Auth.Controllers;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Kdf;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Controllers;\n\npublic class AccountsControllerTests : IDisposable\n{\n\n    private readonly AccountsController _sut;\n    private readonly IOrganizationService _organizationService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IUserService _userService;\n    private readonly IProviderUserRepository _providerUserRepository;\n    private readonly IPolicyService _policyService;\n    private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;\n    private readonly ISetInitialMasterPasswordCommandV1 _setInitialMasterPasswordCommandV1;\n    private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;\n    private readonly ITdeSetPasswordCommand _tdeSetPasswordCommand;\n    private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;\n    private readonly IFeatureService _featureService;\n    private readonly IUserAccountKeysQuery _userAccountKeysQuery;\n    private readonly ITwoFactorEmailService _twoFactorEmailService;\n    private readonly IChangeKdfCommand _changeKdfCommand;\n    private readonly IUserRepository _userRepository;\n\n    public AccountsControllerTests()\n    {\n        _userService = Substitute.For<IUserService>();\n        _organizationService = Substitute.For<IOrganizationService>();\n        _organizationUserRepository = Substitute.For<IOrganizationUserRepository>();\n        _providerUserRepository = Substitute.For<IProviderUserRepository>();\n        _policyService = Substitute.For<IPolicyService>();\n        _setInitialMasterPasswordCommand = Substitute.For<ISetInitialMasterPasswordCommand>();\n        _setInitialMasterPasswordCommandV1 = Substitute.For<ISetInitialMasterPasswordCommandV1>();\n        _twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();\n        _tdeSetPasswordCommand = Substitute.For<ITdeSetPasswordCommand>();\n        _tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();\n        _featureService = Substitute.For<IFeatureService>();\n        _userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();\n        _twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();\n        _changeKdfCommand = Substitute.For<IChangeKdfCommand>();\n        _userRepository = Substitute.For<IUserRepository>();\n\n        _sut = new AccountsController(\n            _organizationService,\n            _organizationUserRepository,\n            _providerUserRepository,\n            _userService,\n            _policyService,\n            _setInitialMasterPasswordCommand,\n            _setInitialMasterPasswordCommandV1,\n            _tdeSetPasswordCommand,\n            _tdeOffboardingPasswordCommand,\n            _twoFactorIsEnabledQuery,\n            _featureService,\n            _userAccountKeysQuery,\n            _twoFactorEmailService,\n            _changeKdfCommand,\n            _userRepository\n        );\n    }\n\n    public void Dispose()\n    {\n        _sut?.Dispose();\n    }\n\n    [Fact]\n    public async Task PostPasswordHint_ShouldNotifyUserService()\n    {\n        var email = \"user@example.com\";\n\n        await _sut.PostPasswordHint(new PasswordHintRequestModel { Email = email });\n\n        await _userService.Received(1).SendMasterPasswordHintAsync(email);\n    }\n\n    [Fact]\n    public async Task PostEmailToken_ShouldInitiateEmailChange()\n    {\n        // Arrange\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        ConfigureUserServiceToAcceptPasswordFor(user);\n        const string newEmail = \"example@user.com\";\n        _userService.ValidateClaimedUserDomainAsync(user, newEmail).Returns(IdentityResult.Success);\n\n        // Act\n        await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail });\n\n        // Assert\n        await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);\n    }\n\n    [Fact]\n    public async Task PostEmailToken_WhenValidateClaimedUserDomainAsyncFails_ShouldReturnError()\n    {\n        // Arrange\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        ConfigureUserServiceToAcceptPasswordFor(user);\n\n        const string newEmail = \"example@user.com\";\n\n        _userService.ValidateClaimedUserDomainAsync(user, newEmail)\n            .Returns(IdentityResult.Failed(new IdentityError\n            {\n                Code = \"TestFailure\",\n                Description = \"This is a test.\"\n            }));\n\n\n        // Act\n        // Assert\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail })\n        );\n    }\n\n    [Fact]\n    public async Task PostEmailToken_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()\n    {\n        ConfigureUserServiceToReturnNullPrincipal();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(\n            () => _sut.PostEmailToken(new EmailTokenRequestModel())\n        );\n    }\n\n    [Fact]\n    public async Task PostEmailToken_WhenInvalidPasssword_ShouldThrowBadRequestException()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        ConfigureUserServiceToRejectPasswordFor(user);\n\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.PostEmailToken(new EmailTokenRequestModel())\n        );\n    }\n\n    [Fact]\n    public async Task PostEmail_ShouldChangeUserEmail()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        _userService.ChangeEmailAsync(user, default, default, default, default, default)\n                    .Returns(Task.FromResult(IdentityResult.Success));\n\n        await _sut.PostEmail(new EmailRequestModel());\n\n        await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);\n    }\n\n    [Fact]\n    public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()\n    {\n        ConfigureUserServiceToReturnNullPrincipal();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(\n            () => _sut.PostEmail(new EmailRequestModel())\n        );\n    }\n\n    [Fact]\n    public async Task PostEmail_WhenEmailCannotBeChanged_ShouldThrowBadRequestException()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        _userService.ChangeEmailAsync(user, default, default, default, default, default)\n                    .Returns(Task.FromResult(IdentityResult.Failed()));\n\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.PostEmail(new EmailRequestModel())\n        );\n    }\n\n\n    [Fact]\n    public async Task PostVerifyEmail_ShouldSendEmailVerification()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n\n        await _sut.PostVerifyEmail();\n\n        await _userService.Received(1).SendEmailVerificationAsync(user);\n    }\n\n    [Fact]\n    public async Task PostVerifyEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()\n    {\n        ConfigureUserServiceToReturnNullPrincipal();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(\n            () => _sut.PostVerifyEmail()\n        );\n    }\n\n    [Fact]\n    public async Task PostVerifyEmailToken_ShouldConfirmEmail()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidIdFor(user);\n        _userService.ConfirmEmailAsync(user, Arg.Any<string>())\n                    .Returns(Task.FromResult(IdentityResult.Success));\n\n        await _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = \"12345678-1234-1234-1234-123456789012\" });\n\n        await _userService.Received(1).ConfirmEmailAsync(user, Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task PostVerifyEmailToken_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnNullUserId();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(\n            () => _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = \"12345678-1234-1234-1234-123456789012\" })\n        );\n    }\n\n    [Fact]\n    public async Task PostVerifyEmailToken_WhenEmailConfirmationFails_ShouldThrowBadRequestException()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidIdFor(user);\n        _userService.ConfirmEmailAsync(user, Arg.Any<string>())\n                    .Returns(Task.FromResult(IdentityResult.Failed()));\n\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = \"12345678-1234-1234-1234-123456789012\" })\n        );\n    }\n\n    [Fact]\n    public async Task PostPassword_ShouldChangePassword()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        _userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n                    .Returns(Task.FromResult(IdentityResult.Success));\n\n        await _sut.PostPassword(new PasswordRequestModel\n        {\n            MasterPasswordHash = \"masterPasswordHash\",\n            NewMasterPasswordHash = \"newMasterPasswordHash\",\n            MasterPasswordHint = \"masterPasswordHint\",\n            Key = \"key\"\n        });\n\n        await _userService.Received(1).ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task PostPassword_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()\n    {\n        ConfigureUserServiceToReturnNullPrincipal();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(\n            () => _sut.PostPassword(new PasswordRequestModel\n            {\n                MasterPasswordHash = \"masterPasswordHash\",\n                NewMasterPasswordHash = \"newMasterPasswordHash\",\n                MasterPasswordHint = \"masterPasswordHint\",\n                Key = \"key\"\n            })\n        );\n    }\n\n    [Fact]\n    public async Task PostPassword_WhenPasswordChangeFails_ShouldBadRequestException()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        _userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n                    .Returns(Task.FromResult(IdentityResult.Failed()));\n\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.PostPassword(new PasswordRequestModel\n            {\n                MasterPasswordHash = \"masterPasswordHash\",\n                NewMasterPasswordHash = \"newMasterPasswordHash\",\n                MasterPasswordHint = \"masterPasswordHint\",\n                Key = \"key\"\n            })\n        );\n    }\n\n    [Fact]\n    public async Task GetApiKey_ShouldReturnApiKeyResponse()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        ConfigureUserServiceToAcceptPasswordFor(user);\n        await _sut.ApiKey(new SecretVerificationRequestModel());\n    }\n\n    [Fact]\n    public async Task GetApiKey_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()\n    {\n        ConfigureUserServiceToReturnNullPrincipal();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(\n            () => _sut.ApiKey(new SecretVerificationRequestModel())\n        );\n    }\n\n    [Fact]\n    public async Task GetApiKey_WhenPasswordCheckFails_ShouldThrowBadRequestException()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        ConfigureUserServiceToRejectPasswordFor(user);\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.ApiKey(new SecretVerificationRequestModel())\n        );\n    }\n\n    [Fact]\n    public async Task PostRotateApiKey_ShouldRotateApiKey()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        ConfigureUserServiceToAcceptPasswordFor(user);\n        await _sut.RotateApiKey(new SecretVerificationRequestModel());\n    }\n\n    [Fact]\n    public async Task PostRotateApiKey_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()\n    {\n        ConfigureUserServiceToReturnNullPrincipal();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(\n            () => _sut.ApiKey(new SecretVerificationRequestModel())\n        );\n    }\n\n    [Fact]\n    public async Task PostRotateApiKey_WhenPasswordCheckFails_ShouldThrowBadRequestException()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        ConfigureUserServiceToRejectPasswordFor(user);\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.ApiKey(new SecretVerificationRequestModel())\n        );\n    }\n\n\n    [Theory]\n    [BitAutoData(true, \"existingPrivateKey\", \"existingPublicKey\", true)] // allow providing existing keys in the request\n    [BitAutoData(true, null, null, true)] // allow not setting the public key when the user already has a key\n    [BitAutoData(false, \"newPrivateKey\", \"newPublicKey\", true)] // allow setting new keys when the user has no keys\n    [BitAutoData(false, null, null, true)] // allow not setting the public key when the user has no keys\n    // do not allow single key\n    [BitAutoData(false, \"existingPrivateKey\", null, false)]\n    [BitAutoData(false, null, \"existingPublicKey\", false)]\n    [BitAutoData(false, \"newPrivateKey\", null, false)]\n    [BitAutoData(false, null, \"newPublicKey\", false)]\n    [BitAutoData(true, \"existingPrivateKey\", null, false)]\n    [BitAutoData(true, null, \"existingPublicKey\", false)]\n    [BitAutoData(true, \"newPrivateKey\", null, false)]\n    [BitAutoData(true, null, \"newPublicKey\", false)]\n    // reject overwriting existing keys\n    [BitAutoData(true, \"newPrivateKey\", \"newPublicKey\", false)]\n    public async Task PostSetPasswordAsync_V1_WhenUserExistsAndSettingPasswordSucceeds_ShouldHandleKeysCorrectlyAndReturn(\n        bool hasExistingKeys,\n        string requestPrivateKey,\n        string requestPublicKey,\n        bool shouldSucceed,\n        User user,\n        SetInitialPasswordRequestModel setInitialPasswordRequestModel)\n    {\n        // Arrange\n        const string existingPublicKey = \"existingPublicKey\";\n        const string existingEncryptedPrivateKey = \"existingPrivateKey\";\n\n        if (hasExistingKeys)\n        {\n            user.PublicKey = existingPublicKey;\n            user.PrivateKey = existingEncryptedPrivateKey;\n        }\n        else\n        {\n            user.PublicKey = null;\n            user.PrivateKey = null;\n        }\n\n        UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);\n\n        if (requestPrivateKey == null && requestPublicKey == null)\n        {\n            setInitialPasswordRequestModel.Keys = null;\n        }\n        else\n        {\n            setInitialPasswordRequestModel.Keys = new KeysRequestModel\n            {\n                EncryptedPrivateKey = requestPrivateKey,\n                PublicKey = requestPublicKey\n            };\n        }\n\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(\n                user,\n                setInitialPasswordRequestModel.MasterPasswordHash,\n                setInitialPasswordRequestModel.Key,\n                setInitialPasswordRequestModel.OrgIdentifier)\n            .Returns(Task.FromResult(IdentityResult.Success));\n\n        // Act\n        if (shouldSucceed)\n        {\n            await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);\n            // Assert\n            await _setInitialMasterPasswordCommandV1.Received(1)\n                .SetInitialMasterPasswordAsync(\n                    Arg.Is<User>(u => u == user),\n                    Arg.Is<string>(s => s == setInitialPasswordRequestModel.MasterPasswordHash),\n                    Arg.Is<string>(s => s == setInitialPasswordRequestModel.Key),\n                    Arg.Is<string>(s => s == setInitialPasswordRequestModel.OrgIdentifier));\n\n            // Additional Assertions for User object modifications\n            Assert.Equal(setInitialPasswordRequestModel.MasterPasswordHint, user.MasterPasswordHint);\n            Assert.Equal(setInitialPasswordRequestModel.Kdf, user.Kdf);\n            Assert.Equal(setInitialPasswordRequestModel.KdfIterations, user.KdfIterations);\n            Assert.Equal(setInitialPasswordRequestModel.KdfMemory, user.KdfMemory);\n            Assert.Equal(setInitialPasswordRequestModel.KdfParallelism, user.KdfParallelism);\n            Assert.Equal(setInitialPasswordRequestModel.Key, user.Key);\n        }\n        else\n        {\n            await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));\n        }\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V1_WhenUserExistsAndHasKeysAndKeysAreUpdated_ShouldThrowAsync(\n    User user,\n    SetInitialPasswordRequestModel setInitialPasswordRequestModel)\n    {\n        // Arrange\n        const string existingPublicKey = \"existingPublicKey\";\n        const string existingEncryptedPrivateKey = \"existingEncryptedPrivateKey\";\n\n        const string newPublicKey = \"newPublicKey\";\n        const string newEncryptedPrivateKey = \"newEncryptedPrivateKey\";\n\n        user.PublicKey = existingPublicKey;\n        user.PrivateKey = existingEncryptedPrivateKey;\n\n        UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);\n\n        setInitialPasswordRequestModel.Keys = new KeysRequestModel()\n        {\n            PublicKey = newPublicKey,\n            EncryptedPrivateKey = newEncryptedPrivateKey\n        };\n\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(\n                user,\n                setInitialPasswordRequestModel.MasterPasswordHash,\n                setInitialPasswordRequestModel.Key,\n                setInitialPasswordRequestModel.OrgIdentifier)\n            .Returns(Task.FromResult(IdentityResult.Success));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V1_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(\n        SetInitialPasswordRequestModel setInitialPasswordRequestModel)\n    {\n        UpdateSetInitialPasswordRequestModelToV1(setInitialPasswordRequestModel);\n\n        // Arrange\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V1_WhenSettingPasswordFails_ShouldThrowBadRequestException(\n        User user,\n        SetInitialPasswordRequestModel model)\n    {\n        UpdateSetInitialPasswordRequestModelToV1(model);\n        model.Keys = null;\n        // Arrange\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _setInitialMasterPasswordCommandV1.SetInitialMasterPasswordAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n            .Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = \"Some Error\" })));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(model));\n    }\n\n    [Fact]\n    public async Task Delete_WithUserManagedByAnOrganization_ThrowsBadRequestException()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        ConfigureUserServiceToAcceptPasswordFor(user);\n        _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true);\n\n        var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));\n\n        Assert.Equal(\"Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.\", result.Message);\n    }\n\n    [Fact]\n    public async Task Delete_WithUserNotManagedByAnOrganization_ShouldSucceed()\n    {\n        var user = GenerateExampleUser();\n        ConfigureUserServiceToReturnValidPrincipalFor(user);\n        ConfigureUserServiceToAcceptPasswordFor(user);\n        _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false);\n        _userService.DeleteAsync(user).Returns(IdentityResult.Success);\n\n        await _sut.Delete(new SecretVerificationRequestModel());\n\n        await _userService.Received(1).DeleteAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetVerifyDevices_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(\n        SetVerifyDevicesRequestModel model)\n    {\n        // Arrange\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.SetUserVerifyDevicesAsync(model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetVerifyDevices_WhenInvalidSecret_ShouldFail(\n        User user, SetVerifyDevicesRequestModel model)\n    {\n        // Arrange\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((user)));\n        _userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(false));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(() => _sut.SetUserVerifyDevicesAsync(model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetVerifyDevices_WhenRequestValid_ShouldSucceed(\n        User user, SetVerifyDevicesRequestModel model)\n    {\n        // Arrange\n        user.VerifyDevices = false;\n        model.VerifyDevices = true;\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((user)));\n        _userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(true));\n\n        // Act\n        await _sut.SetUserVerifyDevicesAsync(model);\n\n        await _userService.Received(1).SaveUserAsync(user);\n        Assert.Equal(model.VerifyDevices, user.VerifyDevices);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ResendNewDeviceVerificationEmail_WhenUserNotFound_ShouldFail(\n    UnauthenticatedSecretVerificationRequestModel model)\n    {\n        // Arrange\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.ResendNewDeviceOtpAsync(model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResendNewDeviceVerificationEmail_WhenSecretNotValid_ShouldFail(\n        User user,\n        UnauthenticatedSecretVerificationRequestModel model)\n    {\n        // Arrange\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(false));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(() => _sut.ResendNewDeviceOtpAsync(model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResendNewDeviceVerificationEmail_WhenTokenValid_SendsEmail(User user,\n        UnauthenticatedSecretVerificationRequestModel model)\n    {\n        // Arrange\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(true));\n\n        // Act\n        await _sut.ResendNewDeviceOtpAsync(model);\n\n        // Assert\n        await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostKdf_UserNotFound_ShouldFail(PasswordRequestModel model)\n    {\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult<User>(null));\n\n        // Act\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostKdf(model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostKdf_WithNullAuthenticationData_ShouldFail(\n        User user, PasswordRequestModel model)\n    {\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        model.AuthenticationData = null;\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));\n\n        Assert.Contains(\"AuthenticationData and UnlockData must be provided.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostKdf_WithNullUnlockData_ShouldFail(\n        User user, PasswordRequestModel model)\n    {\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        model.UnlockData = null;\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));\n\n        Assert.Contains(\"AuthenticationData and UnlockData must be provided.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostKdf_ChangeKdfFailed_ShouldFail(\n        User user, PasswordRequestModel model)\n    {\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),\n                Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())\n            .Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = \"Change KDF failed\" })));\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));\n\n        Assert.NotNull(exception.ModelState);\n        Assert.Contains(\"Change KDF failed\",\n            exception.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostKdf_ChangeKdfSuccess_NoError(\n        User user, PasswordRequestModel model)\n    {\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _changeKdfCommand.ChangeKdfAsync(Arg.Any<User>(), Arg.Any<string>(),\n                Arg.Any<MasterPasswordAuthenticationData>(), Arg.Any<MasterPasswordUnlockData>())\n            .Returns(Task.FromResult(IdentityResult.Success));\n\n        // Act\n        await _sut.PostKdf(model);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostKeys_NoUser_Errors(KeysRequestModel model)\n    {\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult<User>(null));\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostKeys(model));\n    }\n\n    [Theory]\n    [BitAutoData(\"existing\", \"existing\")]\n    [BitAutoData((string)null, \"existing\")]\n    [BitAutoData(\"\", \"existing\")]\n    [BitAutoData(\" \", \"existing\")]\n    [BitAutoData(\"existing\", null)]\n    [BitAutoData(\"existing\", \"\")]\n    [BitAutoData(\"existing\", \" \")]\n    public async Task PostKeys_UserAlreadyHasKeys_Errors(string? existingPrivateKey, string? existingPublicKey,\n        KeysRequestModel model)\n    {\n        var user = GenerateExampleUser();\n        user.PrivateKey = existingPrivateKey;\n        user.PublicKey = existingPublicKey;\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKeys(model));\n\n        Assert.NotNull(exception.Message);\n        Assert.Contains(\"User has existing keypair\", exception.Message);\n    }\n\n    // Below are helper functions that currently belong to this\n    // test class, but ultimately may need to be split out into\n    // something greater in order to share common test steps with\n    // other test suites. They are included here for the time being\n    // until that day comes.\n    private User GenerateExampleUser()\n    {\n        return new User\n        {\n            Email = \"user@example.com\"\n        };\n    }\n\n    private void ConfigureUserServiceToReturnNullPrincipal()\n    {\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n                    .Returns(Task.FromResult((User)null));\n    }\n\n    private void ConfigureUserServiceToReturnValidPrincipalFor(User user)\n    {\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n                    .Returns(Task.FromResult(user));\n    }\n\n    private void ConfigureUserServiceToRejectPasswordFor(User user)\n    {\n        _userService.CheckPasswordAsync(user, Arg.Any<string>())\n                    .Returns(Task.FromResult(false));\n    }\n\n    private void ConfigureUserServiceToAcceptPasswordFor(User user)\n    {\n        _userService.CheckPasswordAsync(user, Arg.Any<string>())\n                    .Returns(Task.FromResult(true));\n        _userService.VerifySecretAsync(user, Arg.Any<string>())\n                    .Returns(Task.FromResult(true));\n    }\n\n    private void ConfigureUserServiceToReturnValidIdFor(User user)\n    {\n        _userService.GetUserByIdAsync(Arg.Any<Guid>())\n                    .Returns(Task.FromResult(user));\n    }\n\n    private void ConfigureUserServiceToReturnNullUserId()\n    {\n        _userService.GetUserByIdAsync(Arg.Any<Guid>())\n                    .Returns(Task.FromResult((User)null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostKeys_WithAccountKeys_CallsSetV2AccountCryptographicState(\n        User user,\n        KeysRequestModel model)\n    {\n        // Arrange\n        user.PublicKey = null;\n        user.PrivateKey = null;\n        model.AccountKeys = new AccountKeysRequestModel\n        {\n            UserKeyEncryptedAccountPrivateKey = \"wrapped-private-key\",\n            AccountPublicKey = \"public-key\",\n            PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel\n            {\n                PublicKey = \"public-key\",\n                WrappedPrivateKey = \"wrapped-private-key\",\n                SignedPublicKey = \"signed-public-key\"\n            },\n            SignatureKeyPair = new SignatureKeyPairRequestModel\n            {\n                VerifyingKey = \"verifying-key\",\n                SignatureAlgorithm = \"ed25519\",\n                WrappedSigningKey = \"wrapped-signing-key\"\n            },\n            SecurityState = new SecurityStateModel\n            {\n                SecurityState = \"security-state\",\n                SecurityVersion = 2\n            }\n        };\n\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n\n        // Act\n        var result = await _sut.PostKeys(model);\n\n        // Assert\n        await _userRepository.Received(1).SetV2AccountCryptographicStateAsync(\n            user.Id,\n            Arg.Any<UserAccountKeysData>());\n        await _userService.DidNotReceiveWithAnyArgs().SaveUserAsync(Arg.Any<User>());\n        Assert.NotNull(result);\n        Assert.Equal(\"keys\", result.Object);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostKeys_WithoutAccountKeys_CallsSaveUser(\n        User user,\n        KeysRequestModel model)\n    {\n        // Arrange\n        user.PublicKey = null;\n        user.PrivateKey = null;\n        model.AccountKeys = null;\n        model.PublicKey = \"public-key\";\n        model.EncryptedPrivateKey = \"encrypted-private-key\";\n\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n\n        // Act\n        var result = await _sut.PostKeys(model);\n\n        // Assert\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>\n            u.PublicKey == model.PublicKey &&\n            u.PrivateKey == model.EncryptedPrivateKey));\n        await _userRepository.DidNotReceiveWithAnyArgs()\n            .SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>());\n        Assert.NotNull(result);\n        Assert.Equal(\"keys\", result.Object);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V2_WhenUserExistsAndSettingPasswordSucceeds_ShouldSetInitialMasterPassword(\n        User user,\n        SetInitialPasswordRequestModel setInitialPasswordRequestModel)\n    {\n        // Arrange\n        UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel);\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(user, Arg.Any<SetInitialMasterPasswordDataModel>())\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);\n\n        // Assert\n        await _setInitialMasterPasswordCommand.Received(1)\n            .SetInitialMasterPasswordAsync(\n                Arg.Is<User>(u => u == user),\n                Arg.Is<SetInitialMasterPasswordDataModel>(d =>\n                    d.MasterPasswordAuthentication != null &&\n                    d.MasterPasswordUnlock != null &&\n                    d.AccountKeys != null &&\n                    d.OrgSsoIdentifier == setInitialPasswordRequestModel.OrgIdentifier));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V2_WithTdeSetPassword_ShouldCallTdeSetPasswordCommand(\n        User user,\n        SetInitialPasswordRequestModel setInitialPasswordRequestModel)\n    {\n        // Arrange\n        UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel, includeTdeSetPassword: true);\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _tdeSetPasswordCommand.SetMasterPasswordAsync(user, Arg.Any<SetInitialMasterPasswordDataModel>())\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await _sut.PostSetPasswordAsync(setInitialPasswordRequestModel);\n\n        // Assert\n        await _tdeSetPasswordCommand.Received(1)\n            .SetMasterPasswordAsync(\n                Arg.Is<User>(u => u == user),\n                Arg.Is<SetInitialMasterPasswordDataModel>(d =>\n                    d.MasterPasswordAuthentication != null &&\n                    d.MasterPasswordUnlock != null &&\n                    d.AccountKeys == null &&\n                    d.OrgSsoIdentifier == setInitialPasswordRequestModel.OrgIdentifier));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V2_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException(\n        SetInitialPasswordRequestModel setInitialPasswordRequestModel)\n    {\n        // Arrange\n        UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel);\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetPasswordAsync_V2_WhenSettingPasswordFails_ShouldThrowException(\n        User user,\n        SetInitialPasswordRequestModel setInitialPasswordRequestModel)\n    {\n        // Arrange\n        UpdateSetInitialPasswordRequestModelToV2(setInitialPasswordRequestModel);\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));\n        _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(user, Arg.Any<SetInitialMasterPasswordDataModel>())\n            .Returns(Task.FromException(new Exception(\"Setting password failed\")));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<Exception>(() => _sut.PostSetPasswordAsync(setInitialPasswordRequestModel));\n    }\n\n    private void UpdateSetInitialPasswordRequestModelToV1(SetInitialPasswordRequestModel model)\n    {\n        model.MasterPasswordAuthentication = null;\n        model.MasterPasswordUnlock = null;\n        model.AccountKeys = null;\n    }\n\n    private void UpdateSetInitialPasswordRequestModelToV2(SetInitialPasswordRequestModel model, bool includeTdeSetPassword = false)\n    {\n        var kdf = new KdfRequestModel\n        {\n            KdfType = KdfType.PBKDF2_SHA256,\n            Iterations = 600000\n        };\n\n        model.MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n        {\n            Kdf = kdf,\n            MasterPasswordAuthenticationHash = \"authHash\",\n            Salt = \"salt\"\n        };\n\n        model.MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n        {\n            Kdf = kdf,\n            MasterKeyWrappedUserKey = \"wrappedKey\",\n            Salt = \"salt\"\n        };\n\n        if (includeTdeSetPassword)\n        {\n            // TDE set password does not include AccountKeys\n            model.AccountKeys = null;\n        }\n        else\n        {\n            model.AccountKeys = new AccountKeysRequestModel\n            {\n                UserKeyEncryptedAccountPrivateKey = \"privateKey\",\n                AccountPublicKey = \"publicKey\"\n            };\n        }\n\n        // Clear V1 properties\n        model.MasterPasswordHash = null;\n        model.Key = null;\n        model.Keys = null;\n        model.Kdf = null;\n        model.KdfIterations = null;\n        model.KdfMemory = null;\n        model.KdfParallelism = null;\n    }\n}\n\n"
  },
  {
    "path": "test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.Auth.Controllers;\nusing Bit.Api.Auth.Models.Response;\nusing Bit.Api.Models.Response;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Api.Request.AuthRequest;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Controllers;\n\n[ControllerCustomize(typeof(AuthRequestsController))]\n[SutProviderCustomize]\npublic class AuthRequestsControllerTests\n{\n    const string _testGlobalSettingsBaseUri = \"https://vault.test.dev\";\n\n    [Theory, BitAutoData]\n    public async Task Get_ReturnsExpectedResult(\n        SutProvider<AuthRequestsController> sutProvider,\n        User user,\n        AuthRequest authRequest)\n    {\n        // Arrange\n        SetBaseServiceUri(sutProvider);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns([authRequest]);\n\n        // Act\n        var result = await sutProvider.Sut.GetAll();\n\n        // Assert\n        Assert.NotNull(result);\n        var expectedCount = 1;\n        Assert.Equal(result.Data.Count(), expectedCount);\n        Assert.IsType<ListResponseModel<AuthRequestResponseModel>>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetById_ThrowsNotFoundException(\n        SutProvider<AuthRequestsController> sutProvider,\n        User user,\n        AuthRequest authRequest)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IAuthRequestService>()\n            .GetAuthRequestAsync(authRequest.Id, user.Id)\n            .Returns((AuthRequest)null);\n\n        // Act\n        // Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(\n           () => sutProvider.Sut.Get(authRequest.Id));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetById_ReturnsAuthRequest(\n        SutProvider<AuthRequestsController> sutProvider,\n        User user,\n        AuthRequest authRequest)\n    {\n        // Arrange\n        SetBaseServiceUri(sutProvider);\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IAuthRequestService>()\n            .GetAuthRequestAsync(authRequest.Id, user.Id)\n            .Returns(authRequest);\n\n        // Act\n        var result = await sutProvider.Sut.Get(authRequest.Id);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<AuthRequestResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPending_ReturnsExpectedResult(\n        SutProvider<AuthRequestsController> sutProvider,\n        User user,\n        PendingAuthRequestDetails authRequest)\n    {\n        // Arrange\n        SetBaseServiceUri(sutProvider);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetManyPendingAuthRequestByUserId(user.Id)\n            .Returns([authRequest]);\n\n        // Act\n        var result = await sutProvider.Sut.GetPendingAuthRequestsAsync();\n\n        // Assert\n        Assert.NotNull(result);\n        var expectedCount = 1;\n        Assert.Equal(result.Data.Count(), expectedCount);\n        Assert.IsType<ListResponseModel<PendingAuthRequestResponseModel>>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetResponseById_ThrowsNotFoundException(\n        SutProvider<AuthRequestsController> sutProvider,\n        AuthRequest authRequest)\n    {\n        // Arrange\n        sutProvider.GetDependency<IAuthRequestService>()\n            .GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode)\n            .Returns((AuthRequest)null);\n\n        // Act\n        // Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(\n           () => sutProvider.Sut.GetResponse(authRequest.Id, authRequest.AccessCode));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetResponseById_ReturnsAuthRequest(\n        SutProvider<AuthRequestsController> sutProvider,\n        AuthRequest authRequest)\n    {\n        // Arrange\n        SetBaseServiceUri(sutProvider);\n\n        sutProvider.GetDependency<IAuthRequestService>()\n            .GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode)\n            .Returns(authRequest);\n\n        // Act\n        var result = await sutProvider.Sut.GetResponse(authRequest.Id, authRequest.AccessCode);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<AuthRequestResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_AdminApprovalRequest_ThrowsBadRequestException(\n        SutProvider<AuthRequestsController> sutProvider,\n        AuthRequestCreateRequestModel authRequest)\n    {\n        // Arrange\n        authRequest.Type = AuthRequestType.AdminApproval;\n\n        // Act\n        // Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n           () => sutProvider.Sut.Post(authRequest));\n\n        var expectedMessage = \"You must be authenticated to create a request of that type.\";\n        Assert.Equal(exception.Message, expectedMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_ReturnsAuthRequest(\n        SutProvider<AuthRequestsController> sutProvider,\n        AuthRequestCreateRequestModel requestModel,\n        AuthRequest authRequest)\n    {\n        // Arrange\n        SetBaseServiceUri(sutProvider);\n\n        requestModel.Type = AuthRequestType.AuthenticateAndUnlock;\n        sutProvider.GetDependency<IAuthRequestService>()\n            .CreateAuthRequestAsync(requestModel)\n            .Returns(authRequest);\n\n        // Act\n        var result = await sutProvider.Sut.Post(requestModel);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<AuthRequestResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostAdminRequest_ReturnsAuthRequest(\n        SutProvider<AuthRequestsController> sutProvider,\n        AuthRequestCreateRequestModel requestModel,\n        AuthRequest authRequest)\n    {\n        // Arrange\n        SetBaseServiceUri(sutProvider);\n\n        requestModel.Type = AuthRequestType.AuthenticateAndUnlock;\n        sutProvider.GetDependency<IAuthRequestService>()\n            .CreateAuthRequestAsync(requestModel)\n            .Returns(authRequest);\n\n        // Act\n        var result = await sutProvider.Sut.PostAdminRequest(requestModel);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<AuthRequestResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithRequestNotApproved_ReturnsAuthRequest(\n        SutProvider<AuthRequestsController> sutProvider,\n        User user,\n        AuthRequestUpdateRequestModel requestModel,\n        AuthRequest authRequest)\n    {\n        // Arrange\n        SetBaseServiceUri(sutProvider);\n        requestModel.RequestApproved = false; // Not an approval, so validation should be skipped\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IAuthRequestService>()\n            .UpdateAuthRequestAsync(authRequest.Id, user.Id, requestModel)\n            .Returns(authRequest);\n\n        // Act\n        var result = await sutProvider.Sut\n                .Put(authRequest.Id, requestModel);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<AuthRequestResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithApprovedRequest_ValidatesAndReturnsAuthRequest(\n        SutProvider<AuthRequestsController> sutProvider,\n        User user,\n        AuthRequestUpdateRequestModel requestModel,\n        AuthRequest currentAuthRequest,\n        AuthRequest updatedAuthRequest,\n        List<PendingAuthRequestDetails> pendingRequests)\n    {\n        // Arrange\n        SetBaseServiceUri(sutProvider);\n        requestModel.RequestApproved = true; // Approval triggers validation\n        currentAuthRequest.RequestDeviceIdentifier = \"device-identifier-123\";\n\n        // Setup pending requests - make the current request the most recent for its device\n        var mostRecentForDevice = new PendingAuthRequestDetails(currentAuthRequest, Guid.NewGuid());\n        pendingRequests.Add(mostRecentForDevice);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        // Setup validation dependencies\n        sutProvider.GetDependency<IAuthRequestService>()\n            .GetAuthRequestAsync(currentAuthRequest.Id, user.Id)\n            .Returns(currentAuthRequest);\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetManyPendingAuthRequestByUserId(user.Id)\n            .Returns(pendingRequests);\n\n        sutProvider.GetDependency<IAuthRequestService>()\n            .UpdateAuthRequestAsync(currentAuthRequest.Id, user.Id, requestModel)\n            .Returns(updatedAuthRequest);\n\n        // Act\n        var result = await sutProvider.Sut\n                .Put(currentAuthRequest.Id, requestModel);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<AuthRequestResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithApprovedRequest_CurrentAuthRequestNotFound_ThrowsNotFoundException(\n        SutProvider<AuthRequestsController> sutProvider,\n        User user,\n        AuthRequestUpdateRequestModel requestModel,\n        Guid authRequestId)\n    {\n        // Arrange\n        requestModel.RequestApproved = true; // Approval triggers validation\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        // Current auth request not found\n        sutProvider.GetDependency<IAuthRequestService>()\n            .GetAuthRequestAsync(authRequestId, user.Id)\n            .Returns((AuthRequest)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.Put(authRequestId, requestModel));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithApprovedRequest_NotMostRecentForDevice_ThrowsBadRequestException(\n        SutProvider<AuthRequestsController> sutProvider,\n        User user,\n        AuthRequestUpdateRequestModel requestModel,\n        AuthRequest currentAuthRequest,\n        List<PendingAuthRequestDetails> pendingRequests)\n    {\n        // Arrange\n        requestModel.RequestApproved = true; // Approval triggers validation\n        currentAuthRequest.RequestDeviceIdentifier = \"device-identifier-123\";\n\n        // Setup pending requests - make a different request the most recent for the same device\n        var differentAuthRequest = new AuthRequest\n        {\n            Id = Guid.NewGuid(), // Different ID than current request\n            RequestDeviceIdentifier = currentAuthRequest.RequestDeviceIdentifier,\n            UserId = user.Id,\n            Type = AuthRequestType.AuthenticateAndUnlock,\n            CreationDate = DateTime.UtcNow\n        };\n        var mostRecentForDevice = new PendingAuthRequestDetails(differentAuthRequest, Guid.NewGuid());\n        pendingRequests.Add(mostRecentForDevice);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IAuthRequestService>()\n            .GetAuthRequestAsync(currentAuthRequest.Id, user.Id)\n            .Returns(currentAuthRequest);\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetManyPendingAuthRequestByUserId(user.Id)\n            .Returns(pendingRequests);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.Put(currentAuthRequest.Id, requestModel));\n\n        Assert.Equal(\"This request is no longer valid. Make sure to approve the most recent request.\", exception.Message);\n    }\n\n    private void SetBaseServiceUri(SutProvider<AuthRequestsController> sutProvider)\n    {\n        sutProvider.GetDependency<IGlobalSettings>()\n            .BaseServiceUri\n            .Vault\n            .Returns(_testGlobalSettingsBaseUri);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Controllers/DevicesControllerTests.cs",
    "content": "﻿using Bit.Api.Controllers;\nusing Bit.Api.Models.Response;\nusing Bit.Core.Auth.Models.Api.Response;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.UserFeatures.DeviceTrust;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Controllers;\n\npublic class DevicesControllerTest\n{\n    private readonly IDeviceRepository _deviceRepositoryMock;\n    private readonly IDeviceService _deviceServiceMock;\n    private readonly IUserService _userServiceMock;\n    private readonly IUntrustDevicesCommand _untrustDevicesCommand;\n    private readonly IUserRepository _userRepositoryMock;\n    private readonly ICurrentContext _currentContextMock;\n    private readonly ILogger<DevicesController> _loggerMock;\n    private readonly DevicesController _sut;\n\n    public DevicesControllerTest()\n    {\n        _deviceRepositoryMock = Substitute.For<IDeviceRepository>();\n        _deviceServiceMock = Substitute.For<IDeviceService>();\n        _userServiceMock = Substitute.For<IUserService>();\n        _untrustDevicesCommand = Substitute.For<IUntrustDevicesCommand>();\n        _userRepositoryMock = Substitute.For<IUserRepository>();\n        _currentContextMock = Substitute.For<ICurrentContext>();\n        _loggerMock = Substitute.For<ILogger<DevicesController>>();\n\n        _sut = new DevicesController(\n            _deviceRepositoryMock,\n            _deviceServiceMock,\n            _userServiceMock,\n            _untrustDevicesCommand,\n            _userRepositoryMock,\n            _currentContextMock,\n            _loggerMock);\n    }\n\n    [Fact]\n    public async Task Get_ReturnsExpectedResult()\n    {\n        // Arrange\n        var userId = Guid.Parse(\"AD89E6F8-4E84-4CFE-A978-256CC0DBF974\");\n\n        var authDateTimeResponse = new DateTime(2024, 12, 9, 12, 0, 0);\n        var devicesWithPendingAuthData = new List<DeviceAuthDetails>\n        {\n            new (\n                new Device\n                {\n                    Id = Guid.Parse(\"B3136B10-7818-444F-B05B-4D7A9B8C48BF\"),\n                    UserId = userId,\n                    Name = \"chrome\",\n                    Type = DeviceType.ChromeBrowser,\n                    Identifier = Guid.Parse(\"811E9254-F77C-48C8-AF0A-A181943F5708\").ToString(),\n                    EncryptedPublicKey = \"PublicKey\",\n                    EncryptedUserKey = \"UserKey\",\n                },\n                Guid.Parse(\"E09D6943-D574-49E5-AC85-C3F12B4E019E\"),\n                authDateTimeResponse)\n        };\n\n        _userServiceMock.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>()).Returns(userId);\n        _deviceRepositoryMock.GetManyByUserIdWithDeviceAuth(userId).Returns(devicesWithPendingAuthData);\n\n        // Act\n        var result = await _sut.GetAll();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<ListResponseModel<DeviceAuthRequestResponseModel>>(result);\n        var resultDevice = result.Data.First();\n        Assert.Equal(\"chrome\", resultDevice.Name);\n        Assert.Equal(DeviceType.ChromeBrowser, resultDevice.Type);\n        Assert.Equal(Guid.Parse(\"B3136B10-7818-444F-B05B-4D7A9B8C48BF\"), resultDevice.Id);\n        Assert.Equal(Guid.Parse(\"811E9254-F77C-48C8-AF0A-A181943F5708\").ToString(), resultDevice.Identifier);\n        Assert.Equal(\"PublicKey\", resultDevice.EncryptedPublicKey);\n        Assert.Equal(\"UserKey\", resultDevice.EncryptedUserKey);\n    }\n\n    [Fact]\n    public async Task Get_ThrowsException_WhenUserIdIsInvalid()\n    {\n        // Arrange\n        _userServiceMock.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>()).Returns((Guid?)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.GetAll());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Controllers/EmergencyAccessControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Api.Auth.Controllers;\nusing Bit.Api.Auth.Models.Response;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Controllers;\n\n[ControllerCustomize(typeof(EmergencyAccessController))]\n[SutProviderCustomize]\npublic class EmergencyAccessControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task GetContacts_ReturnsExpectedResult(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User user,\n        List<EmergencyAccessDetails> details)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByGrantorIdAsync(user.Id)\n            .Returns(details);\n\n        // Act\n        var result = await sutProvider.Sut.GetContacts();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<ListResponseModel<EmergencyAccessGranteeDetailsResponseModel>>(result);\n        Assert.Equal(details.Count, result.Data.Count());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetGrantees_ReturnsExpectedResult(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User user,\n        List<EmergencyAccessDetails> details)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByGranteeIdAsync(user.Id)\n            .Returns(details);\n\n        // Act\n        var result = await sutProvider.Sut.GetGrantees();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<ListResponseModel<EmergencyAccessGrantorDetailsResponseModel>>(result);\n        Assert.Equal(details.Count, result.Data.Count());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_ReturnsGranteeDetailsResponseModel(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User user,\n        EmergencyAccessDetails details)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IEmergencyAccessService>()\n            .GetAsync(details.Id, user.Id)\n            .Returns(details);\n\n        // Act\n        var result = await sutProvider.Sut.Get(details.Id);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<EmergencyAccessGranteeDetailsResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Policies_ReturnsListResponseModel(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User user,\n        List<Policy> policies,\n        Guid id)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        sutProvider.GetDependency<IEmergencyAccessService>()\n            .GetPoliciesAsync(id, user)\n            .Returns(policies);\n\n        // Act\n        var result = await sutProvider.Sut.Policies(id);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<ListResponseModel<PolicyResponseModel>>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Policies_WhenGrantorIsNotOrgOwner_ReturnsNullDataAsync(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User user,\n        Guid id)\n    {\n        // Arrange\n        // GetPoliciesAsync returns null when the grantor is not an org owner\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        sutProvider.GetDependency<IEmergencyAccessService>()\n            .GetPoliciesAsync(id, user)\n            .Returns((ICollection<Policy>)null);\n\n        // Act\n        var result = await sutProvider.Sut.Policies(id);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<ListResponseModel<PolicyResponseModel>>(result);\n        Assert.Null(result.Data);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithNullEmergencyAccess_ThrowsNotFoundException(\n        SutProvider<EmergencyAccessController> sutProvider,\n        Guid id,\n        Bit.Api.Auth.Models.Request.EmergencyAccessUpdateRequestModel model)\n    {\n        // Arrange\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(id)\n            .Returns((EmergencyAccess)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Put(id, model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithValidEmergencyAccess_CallsSaveAsync(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User user,\n        EmergencyAccess emergencyAccess,\n        Bit.Api.Auth.Models.Request.EmergencyAccessUpdateRequestModel model)\n    {\n        // Arrange\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(emergencyAccess.Id)\n            .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        // Act\n        await sutProvider.Sut.Put(emergencyAccess.Id, model);\n\n        // Assert\n        await sutProvider.GetDependency<IEmergencyAccessService>()\n            .Received(1)\n            .SaveAsync(Arg.Any<EmergencyAccess>(), user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Invite_CallsInviteAsync(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User user,\n        Bit.Api.Auth.Models.Request.EmergencyAccessInviteRequestModel model)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        // Act\n        await sutProvider.Sut.Invite(model);\n\n        // Assert\n        await sutProvider.GetDependency<IEmergencyAccessService>()\n            .Received(1)\n            .InviteAsync(user, model.Email, model.Type!.Value, model.WaitTimeDays);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Takeover_ReturnsTakeoverResponseModel(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User granteeUser,\n        User grantorUser,\n        EmergencyAccess emergencyAccess,\n        Guid id)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(granteeUser);\n\n        sutProvider.GetDependency<IEmergencyAccessService>()\n            .TakeoverAsync(id, granteeUser)\n            .Returns((emergencyAccess, grantorUser));\n\n        // Act\n        var result = await sutProvider.Sut.Takeover(id);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<EmergencyAccessTakeoverResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ViewCiphers_ReturnsViewResponseModel(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User user,\n        EmergencyAccessViewData viewData,\n        Guid id)\n    {\n        // Arrange\n        viewData.Ciphers = [];\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        sutProvider.GetDependency<IEmergencyAccessService>()\n            .ViewAsync(id, user)\n            .Returns(viewData);\n\n        // Act\n        var result = await sutProvider.Sut.ViewCiphers(id);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<EmergencyAccessViewResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAttachmentData_ReturnsAttachmentResponseModel(\n        SutProvider<EmergencyAccessController> sutProvider,\n        User user,\n        Guid id,\n        Guid cipherId,\n        string attachmentId)\n    {\n        // Arrange\n        // CipherAttachment.MetaData has a circular self-reference, so construct manually\n        var attachmentData = new AttachmentResponseData\n        {\n            Id = attachmentId,\n            Url = \"https://example.com/attachment\",\n            Cipher = new Cipher(),\n            Data = new CipherAttachment.MetaData { FileName = \"file.txt\", Key = \"key\", Size = 1024 },\n        };\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        sutProvider.GetDependency<IEmergencyAccessService>()\n            .GetAttachmentDownloadAsync(id, cipherId, attachmentId, user)\n            .Returns(attachmentData);\n\n        // Act\n        var result = await sutProvider.Sut.GetAttachmentData(id, cipherId, attachmentId);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<AttachmentResponseModel>(result);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs",
    "content": "﻿using Bit.Api.Auth.Controllers;\nusing Bit.Api.Auth.Models.Request;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Auth.Models.Response.TwoFactor;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Controllers;\n\n[ControllerCustomize(typeof(TwoFactorController))]\n[SutProviderCustomize]\npublic class TwoFactorControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task CheckAsync_UserNull_ThrowsUnauthorizedException(SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(null as User);\n\n        // Act\n        var result = () => sutProvider.Sut.GetDuo(request);\n\n        // Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CheckAsync_BadSecret_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n\n        sutProvider.GetDependency<IUserService>()\n            .VerifySecretAsync(default, default)\n            .ReturnsForAnyArgs(false);\n\n        // Act\n        try\n        {\n            await sutProvider.Sut.GetDuo(request);\n        }\n        catch (BadRequestException e)\n        {\n            // Assert\n            Assert.Equal(\"The model state is invalid.\", e.Message);\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task CheckAsync_CannotAccessPremium_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n\n        sutProvider.GetDependency<IUserService>()\n            .VerifySecretAsync(default, default)\n            .ReturnsForAnyArgs(true);\n\n        sutProvider.GetDependency<IUserService>()\n            .CanAccessPremium(default)\n            .ReturnsForAnyArgs(false);\n\n        // Act\n        try\n        {\n            await sutProvider.Sut.GetDuo(request);\n        }\n        catch (BadRequestException e)\n        {\n            // Assert\n            Assert.Equal(\"Premium status is required.\", e.Message);\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetDuo_Success(User user, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson();\n        SetupCheckAsyncToPass(sutProvider, user);\n\n        // Act\n        var result = await sutProvider.Sut.GetDuo(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<TwoFactorDuoResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutDuo_InvalidConfiguration_ThrowsBadRequestException(User user, UpdateTwoFactorDuoRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        SetupCheckAsyncToPass(sutProvider, user);\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .ValidateDuoConfiguration(default, default, default)\n            .Returns(false);\n\n        // Act\n        try\n        {\n            await sutProvider.Sut.PutDuo(request);\n        }\n        catch (BadRequestException e)\n        {\n            // Assert\n            Assert.Equal(\"Duo configuration settings are not valid. Please re-check the Duo Admin panel.\", e.Message);\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutDuo_Success(User user, UpdateTwoFactorDuoRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson();\n        SetupCheckAsyncToPass(sutProvider, user);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .ValidateDuoConfiguration(default, default, default)\n            .ReturnsForAnyArgs(true);\n\n        // Act\n        var result = await sutProvider.Sut.PutDuo(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<TwoFactorDuoResponseModel>(result);\n        Assert.Equal(user.TwoFactorProviders, request.ToUser(user).TwoFactorProviders);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CheckOrganizationAsync_ManagePolicies_ThrowsNotFoundException(\n        User user, Organization organization, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson();\n        SetupCheckAsyncToPass(sutProvider, user);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .ManagePolicies(default)\n            .ReturnsForAnyArgs(false);\n\n        // Act\n        var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request);\n\n        // Assert\n        await Assert.ThrowsAsync<NotFoundException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CheckOrganizationAsync_GetByIdAsync_ThrowsNotFoundException(\n        User user, Organization organization, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson();\n        SetupCheckAsyncToPass(sutProvider, user);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .ManagePolicies(default)\n            .ReturnsForAnyArgs(true);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(default)\n            .ReturnsForAnyArgs(null as Organization);\n\n        // Act\n        var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request);\n\n        // Assert\n        await Assert.ThrowsAsync<NotFoundException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationDuo_Success(\n        User user, Organization organization, SecretVerificationRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson();\n        SetupCheckAsyncToPass(sutProvider, user);\n        SetupCheckOrganizationAsyncToPass(sutProvider, organization);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<TwoFactorDuoResponseModel>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutOrganizationDuo_InvalidConfiguration_ThrowsBadRequestException(\n        User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        SetupCheckAsyncToPass(sutProvider, user);\n        SetupCheckOrganizationAsyncToPass(sutProvider, organization);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .ValidateDuoConfiguration(default, default, default)\n            .ReturnsForAnyArgs(false);\n\n        // Act\n        try\n        {\n            await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request);\n        }\n        catch (BadRequestException e)\n        {\n            // Assert\n            Assert.Equal(\"Duo configuration settings are not valid. Please re-check the Duo Admin panel.\", e.Message);\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutOrganizationDuo_Success(\n        User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider<TwoFactorController> sutProvider)\n    {\n        // Arrange\n        SetupCheckAsyncToPass(sutProvider, user);\n        SetupCheckOrganizationAsyncToPass(sutProvider, organization);\n        organization.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson();\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .ValidateDuoConfiguration(default, default, default)\n            .ReturnsForAnyArgs(true);\n\n        // Act\n        var result =\n            await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<TwoFactorDuoResponseModel>(result);\n        Assert.Equal(organization.TwoFactorProviders, request.ToOrganization(organization).TwoFactorProviders);\n    }\n\n\n    private string GetUserTwoFactorDuoProvidersJson()\n    {\n        return\n            \"{\\\"2\\\":{\\\"Enabled\\\":true,\\\"MetaData\\\":{\\\"ClientSecret\\\":\\\"secretClientSecret\\\",\\\"ClientId\\\":\\\"clientId\\\",\\\"Host\\\":\\\"example.com\\\"}}}\";\n    }\n\n    private string GetOrganizationTwoFactorDuoProvidersJson()\n    {\n        return\n            \"{\\\"6\\\":{\\\"Enabled\\\":true,\\\"MetaData\\\":{\\\"ClientSecret\\\":\\\"secretClientSecret\\\",\\\"ClientId\\\":\\\"clientId\\\",\\\"Host\\\":\\\"example.com\\\"}}}\";\n    }\n\n    /// <summary>\n    /// Sets up the CheckAsync method to pass.\n    /// </summary>\n    /// <param name=\"sutProvider\">uses bit auto data</param>\n    /// <param name=\"user\">uses bit auto data</param>\n    private void SetupCheckAsyncToPass(SutProvider<TwoFactorController> sutProvider, User user)\n    {\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n\n        sutProvider.GetDependency<IUserService>()\n            .VerifySecretAsync(default, default)\n            .ReturnsForAnyArgs(true);\n\n        sutProvider.GetDependency<IUserService>()\n            .CanAccessPremium(default)\n            .ReturnsForAnyArgs(true);\n    }\n\n    private void SetupCheckOrganizationAsyncToPass(SutProvider<TwoFactorController> sutProvider, Organization organization)\n    {\n        sutProvider.GetDependency<ICurrentContext>()\n            .ManagePolicies(default)\n            .ReturnsForAnyArgs(true);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(default)\n            .ReturnsForAnyArgs(organization);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs",
    "content": "﻿using Bit.Api.Auth.Controllers;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Auth.Models.Request.WebAuthn;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Api.Response.Accounts;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.WebAuthnLogin;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Fido2NetLib;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Controllers;\n\n[ControllerCustomize(typeof(WebAuthnController))]\n[SutProviderCustomize]\npublic class WebAuthnControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task Get_UserNotFound_ThrowsUnauthorizedAccessException(SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();\n\n        // Act\n        var result = () => sutProvider.Sut.Get();\n\n        // Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AttestationOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();\n\n        // Act\n        var result = () => sutProvider.Sut.AttestationOptions(requestModel);\n\n        // Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AttestationOptions_UserVerificationFailed_ThrowsBadRequestException(SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).Returns(false);\n\n        // Act\n        var result = () => sutProvider.Sut.AttestationOptions(requestModel);\n\n        // Assert\n        await Assert.ThrowsAsync<BadRequestException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AttestationOptions_RequireSsoPolicyApplicable_ThrowsBadRequestException(\n        SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AttestationOptions(requestModel));\n        Assert.Contains(\"Passkeys cannot be created for your account. SSO login is required\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AttestationOptions_RequireSsoPolicyNotApplicable_Succeeds(\n        SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(false);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()\n            .Protect(Arg.Any<WebAuthnCredentialCreateOptionsTokenable>()).Returns(\"token\");\n\n        var result = await sutProvider.Sut.AttestationOptions(requestModel);\n\n        Assert.NotNull(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AttestationOptions_WithPolicyRequirementsEnabled_CanUsePasskeyLoginFalse_ThrowsBadRequestException(\n        SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsyncVNext<RequireSsoPolicyRequirement>(user.Id)\n            .ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = false });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AttestationOptions(requestModel));\n        Assert.Contains(\"Passkeys cannot be created for your account. SSO login is required\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AttestationOptions_WithPolicyRequirementsEnabled_CanUsePasskeyLoginTrue_Succeeds(\n        SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsyncVNext<RequireSsoPolicyRequirement>(user.Id)\n            .ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = true });\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()\n            .Protect(Arg.Any<WebAuthnCredentialCreateOptionsTokenable>()).Returns(\"token\");\n\n        var result = await sutProvider.Sut.AttestationOptions(requestModel);\n\n        Assert.NotNull(result);\n    }\n\n    #region Assertion Options\n    [Theory, BitAutoData]\n    public async Task AssertionOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();\n\n        // Act\n        var result = () => sutProvider.Sut.AssertionOptions(requestModel);\n\n        // Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AssertionOptions_UserVerificationFailed_ThrowsBadRequestException(SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).Returns(false);\n\n        // Act\n        var result = () => sutProvider.Sut.AssertionOptions(requestModel);\n\n        // Assert\n        await Assert.ThrowsAsync<BadRequestException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AssertionOptions_UserVerificationSuccess_ReturnsAssertionOptions(SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, requestModel.Secret).Returns(true);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()\n            .Protect(Arg.Any<WebAuthnLoginAssertionOptionsTokenable>()).Returns(\"token\");\n\n        // Act\n        var result = await sutProvider.Sut.AssertionOptions(requestModel);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.IsType<WebAuthnLoginAssertionOptionsResponseModel>(result);\n    }\n    #endregion\n\n    [Theory, BitAutoData]\n    public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnLoginCredentialCreateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();\n\n        // Act\n        var result = () => sutProvider.Sut.Post(requestModel);\n\n        // Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_ExpiredToken_ThrowsBadRequestException(WebAuthnLoginCredentialCreateRequestModel requestModel, CredentialCreateOptions createOptions, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n\n        // Act\n        var result = () => sutProvider.Sut.Post(requestModel);\n\n        // Assert\n        await Assert.ThrowsAsync<BadRequestException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_ValidInput_ReturnsCredential(WebAuthnLoginCredentialCreateRequestModel requestModel, CredentialCreateOptions createOptions, User user, WebAuthnCredential credential, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()\n            .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)\n            .Returns(credential);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n\n        // Act\n        var result = await sutProvider.Sut.Post(requestModel);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(credential.Id.ToString(), result.Id);\n        await sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()\n            .Received(1)\n            .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_CredentialCreationFailed_ThrowsBadRequestException(WebAuthnLoginCredentialCreateRequestModel requestModel, CredentialCreateOptions createOptions, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()\n            .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)\n            .Returns((WebAuthnCredential)null);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Post(requestModel));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_RequireSsoPolicyApplicable_ThrowsBadRequestException(\n        WebAuthnLoginCredentialCreateRequestModel requestModel,\n        CredentialCreateOptions createOptions,\n        User user,\n        SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n        sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.Post(requestModel));\n        Assert.Contains(\"Passkeys cannot be created for your account. SSO login is required\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_RequireSsoPolicyNotApplicable_Succeeds(\n        WebAuthnLoginCredentialCreateRequestModel requestModel,\n        CredentialCreateOptions createOptions,\n        User user,\n        WebAuthnCredential credential,\n        SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()\n            .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)\n            .Returns(credential);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n        sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(false);\n\n        // Act\n        await sutProvider.Sut.Post(requestModel);\n\n        // Assert\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .GetUserByPrincipalAsync(default);\n        await sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()\n            .Received(1)\n            .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_WithPolicyRequirementsEnabled_CanUsePasskeyLoginFalse_ThrowsBadRequestException(\n        WebAuthnLoginCredentialCreateRequestModel requestModel,\n        CredentialCreateOptions createOptions,\n        User user,\n        SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsyncVNext<RequireSsoPolicyRequirement>(user.Id)\n            .ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = false });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.Post(requestModel));\n        Assert.Contains(\"Passkeys cannot be created for your account. SSO login is required\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Post_WithPolicyRequirementsEnabled_CanUsePasskeyLoginTrue_Succeeds(\n        WebAuthnLoginCredentialCreateRequestModel requestModel,\n        CredentialCreateOptions createOptions,\n        User user,\n        WebAuthnCredential credential,\n        SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(default)\n            .ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()\n            .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)\n            .Returns(credential);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsyncVNext<RequireSsoPolicyRequirement>(user.Id)\n            .ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = true });\n\n        // Act\n        await sutProvider.Sut.Post(requestModel);\n\n        // Assert\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .GetUserByPrincipalAsync(default);\n        await sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()\n            .Received(1)\n            .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();\n\n        // Act\n        var result = () => sutProvider.Sut.Delete(credentialId, requestModel);\n\n        // Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Delete_UserVerificationFailed_ThrowsBadRequestException(Guid credentialId, SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).Returns(false);\n\n        // Act\n        var result = () => sutProvider.Sut.Delete(credentialId, requestModel);\n\n        // Assert\n        await Assert.ThrowsAsync<BadRequestException>(result);\n    }\n\n    #region Update Credential\n    [Theory, BitAutoData]\n    public async Task Put_TokenVerificationFailed_ThrowsBadRequestException(AssertionOptions assertionOptions, WebAuthnLoginCredentialUpdateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var expectedMessage = \"The token associated with your request is invalid or has expired. A valid token is required to continue.\";\n        var token = new WebAuthnLoginAssertionOptionsTokenable(\n            Core.Auth.Enums.WebAuthnLoginAssertionOptionsScope.PrfRegistration, assertionOptions);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateCredential(requestModel));\n        // Assert\n        Assert.Equal(expectedMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_CredentialNotFound_ThrowsBadRequestException(AssertionOptions assertionOptions, WebAuthnLoginCredentialUpdateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var expectedMessage = \"Unable to update credential.\";\n        var token = new WebAuthnLoginAssertionOptionsTokenable(\n            Core.Auth.Enums.WebAuthnLoginAssertionOptionsScope.UpdateKeySet, assertionOptions);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateCredential(requestModel));\n        // Assert\n        Assert.Equal(expectedMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_PrfNotSupported_ThrowsBadRequestException(User user, WebAuthnCredential credential, AssertionOptions assertionOptions, WebAuthnLoginCredentialUpdateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var expectedMessage = \"Unable to update credential.\";\n        credential.SupportsPrf = false;\n        var token = new WebAuthnLoginAssertionOptionsTokenable(\n            Core.Auth.Enums.WebAuthnLoginAssertionOptionsScope.UpdateKeySet, assertionOptions);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n\n        sutProvider.GetDependency<IAssertWebAuthnLoginCredentialCommand>()\n            .AssertWebAuthnLoginCredential(assertionOptions, requestModel.DeviceResponse)\n            .Returns((user, credential));\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateCredential(requestModel));\n        // Assert\n        Assert.Equal(expectedMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_UpdateCredential_Success(User user, WebAuthnCredential credential, AssertionOptions assertionOptions, WebAuthnLoginCredentialUpdateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)\n    {\n        // Arrange\n        var token = new WebAuthnLoginAssertionOptionsTokenable(\n            Core.Auth.Enums.WebAuthnLoginAssertionOptionsScope.UpdateKeySet, assertionOptions);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()\n            .Unprotect(requestModel.Token)\n            .Returns(token);\n\n        sutProvider.GetDependency<IAssertWebAuthnLoginCredentialCommand>()\n            .AssertWebAuthnLoginCredential(assertionOptions, requestModel.DeviceResponse)\n            .Returns((user, credential));\n\n        // Act\n        await sutProvider.Sut.UpdateCredential(requestModel);\n\n        // Assert\n        sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()\n            .Received(1)\n            .Unprotect(requestModel.Token);\n        await sutProvider.GetDependency<IAssertWebAuthnLoginCredentialCommand>()\n            .Received(1)\n            .AssertWebAuthnLoginCredential(assertionOptions, requestModel.DeviceResponse);\n        await sutProvider.GetDependency<IWebAuthnCredentialRepository>()\n            .Received(1)\n            .UpdateAsync(credential);\n    }\n    #endregion\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Models/Request/Accounts/SetInitialPasswordRequestModelTests.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Models.Request.Accounts;\n\npublic class SetInitialPasswordRequestModelTests\n{\n    #region V2 Validation Tests\n\n    [Theory]\n    [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]\n    [InlineData(KdfType.Argon2id, 3, 64, 4)]\n    public void Validate_V2Request_WithMatchingKdf_ReturnsNoErrors(KdfType kdfType, int iterations, int? memory, int? parallelism)\n    {\n        // Arrange\n        var kdf = new KdfRequestModel\n        {\n            KdfType = kdfType,\n            Iterations = iterations,\n            Memory = memory,\n            Parallelism = parallelism\n        };\n\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = \"orgIdentifier\",\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = kdf,\n                MasterPasswordAuthenticationHash = \"authHash\",\n                Salt = \"salt\"\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = kdf,\n                MasterKeyWrappedUserKey = \"wrappedKey\",\n                Salt = \"salt\"\n            },\n            AccountKeys = new AccountKeysRequestModel\n            {\n                UserKeyEncryptedAccountPrivateKey = \"privateKey\",\n                AccountPublicKey = \"publicKey\"\n            }\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model));\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Validate_V2Request_WithMismatchedKdfSettings_ReturnsValidationError(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 600000\n                },\n                MasterPasswordAuthenticationHash = \"authHash\",\n                Salt = \"salt\"\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 650000 // Different iterations\n                },\n                MasterKeyWrappedUserKey = \"wrappedKey\",\n                Salt = \"salt\"\n            }\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model)).ToList();\n\n        // Assert\n        Assert.Single(result);\n        Assert.Contains(\"KDF settings must be equal\", result[0].ErrorMessage);\n        var memberNames = result[0].MemberNames.ToList();\n        Assert.Equal(2, memberNames.Count);\n        Assert.Contains(\"MasterPasswordAuthentication.Kdf\", memberNames);\n        Assert.Contains(\"MasterPasswordUnlock.Kdf\", memberNames);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Validate_V2Request_WithInvalidAuthenticationKdf_ReturnsValidationError(string orgIdentifier)\n    {\n        // Arrange\n        var kdf = new KdfRequestModel\n        {\n            KdfType = KdfType.PBKDF2_SHA256,\n            Iterations = 1 // Too low\n        };\n\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = kdf,\n                MasterPasswordAuthenticationHash = \"authHash\",\n                Salt = \"salt\"\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = kdf,\n                MasterKeyWrappedUserKey = \"wrappedKey\",\n                Salt = \"salt\"\n            }\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model)).ToList();\n\n        // Assert\n        Assert.NotEmpty(result);\n        Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains(\"KDF iterations must be between\"));\n    }\n\n    #endregion\n\n    #region V1 Validation Tests (Obsolete)\n\n    [Theory]\n    [BitAutoData]\n    public void Validate_V1Request_WithMissingMasterPasswordHash_ReturnsValidationError(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            Key = \"key\",\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = 600000\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model)).ToList();\n\n        // Assert\n        Assert.Contains(result, r => r.ErrorMessage.Contains(\"MasterPasswordHash must be supplied\"));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Validate_V1Request_WithMissingKey_ReturnsValidationError(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordHash = \"hash\",\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = 600000\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model)).ToList();\n\n        // Assert\n        Assert.Contains(result, r => r.ErrorMessage.Contains(\"Key must be supplied\"));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Validate_V1Request_WithMissingKdf_ReturnsValidationError(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordHash = \"hash\",\n            Key = \"key\",\n            KdfIterations = 600000\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model)).ToList();\n\n        // Assert\n        Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains(\"Kdf must be supplied\"));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Validate_V1Request_WithMissingKdfIterations_ReturnsValidationError(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordHash = \"hash\",\n            Key = \"key\",\n            Kdf = KdfType.PBKDF2_SHA256\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model)).ToList();\n\n        // Assert\n        Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains(\"KdfIterations must be supplied\"));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Validate_V1Request_WithArgon2idAndMissingMemory_ReturnsValidationError(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordHash = \"hash\",\n            Key = \"key\",\n            Kdf = KdfType.Argon2id,\n            KdfIterations = 3,\n            KdfParallelism = 4\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model)).ToList();\n\n        // Assert\n        Assert.Contains(result, r => r.ErrorMessage.Contains(\"KdfMemory must be supplied when Kdf is Argon2id\"));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Validate_V1Request_WithArgon2idAndMissingParallelism_ReturnsValidationError(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordHash = \"hash\",\n            Key = \"key\",\n            Kdf = KdfType.Argon2id,\n            KdfIterations = 3,\n            KdfMemory = 64\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model)).ToList();\n\n        // Assert\n        Assert.Contains(result, r => r.ErrorMessage.Contains(\"KdfParallelism must be supplied when Kdf is Argon2id\"));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Validate_V1Request_WithInvalidKdfSettings_ReturnsValidationError(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordHash = \"hash\",\n            Key = \"key\",\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = 5000 // Too low\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model)).ToList();\n\n        // Assert\n        Assert.NotEmpty(result);\n        Assert.Contains(result, r => r.ErrorMessage != null && r.ErrorMessage.Contains(\"KDF iterations must be between\"));\n    }\n\n    [Theory]\n    [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]\n    [InlineData(KdfType.Argon2id, 3, 64, 4)]\n    public void Validate_V1Request_WithValidSettings_ReturnsNoErrors(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = \"orgIdentifier\",\n            MasterPasswordHash = \"hash\",\n            Key = \"key\",\n            Kdf = kdfType,\n            KdfIterations = kdfIterations,\n            KdfMemory = kdfMemory,\n            KdfParallelism = kdfParallelism\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model));\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    #endregion\n\n    #region IsV2Request Tests\n\n    [Theory]\n    [BitAutoData]\n    public void IsV2Request_WithV2Properties_ReturnsTrue(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 600000\n                },\n                MasterPasswordAuthenticationHash = \"authHash\",\n                Salt = \"salt\"\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 600000\n                },\n                MasterKeyWrappedUserKey = \"wrappedKey\",\n                Salt = \"salt\"\n            }\n        };\n\n        // Act\n        var result = model.IsV2Request();\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void IsV2Request_WithoutMasterPasswordAuthentication_ReturnsFalse(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 600000\n                },\n                MasterKeyWrappedUserKey = \"wrappedKey\",\n                Salt = \"salt\"\n            }\n        };\n\n        // Act\n        var result = model.IsV2Request();\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void IsV2Request_WithoutMasterPasswordUnlock_ReturnsFalse(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 600000\n                },\n                MasterPasswordAuthenticationHash = \"authHash\",\n                Salt = \"salt\"\n            }\n        };\n\n        // Act\n        var result = model.IsV2Request();\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void IsV2Request_WithV1Properties_ReturnsFalse(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordHash = \"hash\",\n            Key = \"key\",\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = 600000\n        };\n\n        // Act\n        var result = model.IsV2Request();\n\n        // Assert\n        Assert.False(result);\n    }\n\n    #endregion\n\n    #region IsTdeSetPasswordRequest Tests\n\n    [Theory]\n    [BitAutoData]\n    public void IsTdeSetPasswordRequest_WithNullAccountKeys_ReturnsTrue(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 600000\n                },\n                MasterPasswordAuthenticationHash = \"authHash\",\n                Salt = \"salt\"\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 600000\n                },\n                MasterKeyWrappedUserKey = \"wrappedKey\",\n                Salt = \"salt\"\n            },\n            AccountKeys = null\n        };\n\n        // Act\n        var result = model.IsTdeSetPasswordRequest();\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void IsTdeSetPasswordRequest_WithAccountKeys_ReturnsFalse(string orgIdentifier)\n    {\n        // Arrange\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 600000\n                },\n                MasterPasswordAuthenticationHash = \"authHash\",\n                Salt = \"salt\"\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = new KdfRequestModel\n                {\n                    KdfType = KdfType.PBKDF2_SHA256,\n                    Iterations = 600000\n                },\n                MasterKeyWrappedUserKey = \"wrappedKey\",\n                Salt = \"salt\"\n            },\n            AccountKeys = new AccountKeysRequestModel\n            {\n                UserKeyEncryptedAccountPrivateKey = \"privateKey\",\n                AccountPublicKey = \"publicKey\"\n            }\n        };\n\n        // Act\n        var result = model.IsTdeSetPasswordRequest();\n\n        // Assert\n        Assert.False(result);\n    }\n\n    #endregion\n\n    #region ToUser Tests (Obsolete)\n\n    [Theory]\n    [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]\n    [InlineData(KdfType.Argon2id, 3, 64, 4)]\n    public void ToUser_WithKeys_MapsPropertiesCorrectly(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        // Arrange\n        var existingUser = new User();\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = \"orgIdentifier\",\n            MasterPasswordHash = \"hash\",\n            MasterPasswordHint = \"hint\",\n            Key = \"key\",\n            Kdf = kdfType,\n            KdfIterations = kdfIterations,\n            KdfMemory = kdfMemory,\n            KdfParallelism = kdfParallelism,\n            Keys = new KeysRequestModel\n            {\n                PublicKey = \"publicKey\",\n                EncryptedPrivateKey = \"encryptedPrivateKey\"\n            }\n        };\n\n        // Act\n        var result = model.ToUser(existingUser);\n\n        // Assert\n        Assert.Same(existingUser, result);\n        Assert.Equal(\"hint\", result.MasterPasswordHint);\n        Assert.Equal(kdfType, result.Kdf);\n        Assert.Equal(kdfIterations, result.KdfIterations);\n        Assert.Equal(kdfMemory, result.KdfMemory);\n        Assert.Equal(kdfParallelism, result.KdfParallelism);\n        Assert.Equal(\"key\", result.Key);\n        Assert.Equal(\"publicKey\", result.PublicKey);\n        Assert.Equal(\"encryptedPrivateKey\", result.PrivateKey);\n    }\n\n    [Theory]\n    [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]\n    [InlineData(KdfType.Argon2id, 3, 64, 4)]\n    public void ToUser_WithoutKeys_MapsPropertiesCorrectly(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        // Arrange\n        var existingUser = new User();\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = \"orgIdentifier\",\n            MasterPasswordHash = \"hash\",\n            MasterPasswordHint = \"hint\",\n            Key = \"key\",\n            Kdf = kdfType,\n            KdfIterations = kdfIterations,\n            KdfMemory = kdfMemory,\n            KdfParallelism = kdfParallelism,\n            Keys = null\n        };\n\n        // Act\n        var result = model.ToUser(existingUser);\n\n        // Assert\n        Assert.Same(existingUser, result);\n        Assert.Equal(\"hint\", result.MasterPasswordHint);\n        Assert.Equal(kdfType, result.Kdf);\n        Assert.Equal(kdfIterations, result.KdfIterations);\n        Assert.Equal(kdfMemory, result.KdfMemory);\n        Assert.Equal(kdfParallelism, result.KdfParallelism);\n        Assert.Equal(\"key\", result.Key);\n        Assert.Null(result.PublicKey);\n        Assert.Null(result.PrivateKey);\n    }\n\n    #endregion\n\n    #region ToData Tests\n\n    [Theory]\n    [BitAutoData]\n    public void ToData_MapsPropertiesCorrectly(string orgIdentifier)\n    {\n        // Arrange\n        var kdf = new KdfRequestModel\n        {\n            KdfType = KdfType.PBKDF2_SHA256,\n            Iterations = 600000\n        };\n\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordHint = \"hint\",\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = kdf,\n                MasterPasswordAuthenticationHash = \"authHash\",\n                Salt = \"salt\"\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = kdf,\n                MasterKeyWrappedUserKey = \"wrappedKey\",\n                Salt = \"salt\"\n            },\n            AccountKeys = new AccountKeysRequestModel\n            {\n                UserKeyEncryptedAccountPrivateKey = \"privateKey\",\n                AccountPublicKey = \"publicKey\"\n            }\n        };\n\n        // Act\n        var result = model.ToData();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(orgIdentifier, result.OrgSsoIdentifier);\n        Assert.Equal(\"hint\", result.MasterPasswordHint);\n        Assert.NotNull(result.MasterPasswordAuthentication);\n        Assert.NotNull(result.MasterPasswordUnlock);\n        Assert.NotNull(result.AccountKeys);\n        Assert.Equal(\"authHash\", result.MasterPasswordAuthentication.MasterPasswordAuthenticationHash);\n        Assert.Equal(\"wrappedKey\", result.MasterPasswordUnlock.MasterKeyWrappedUserKey);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void ToData_WithNullAccountKeys_MapsCorrectly(string orgIdentifier)\n    {\n        // Arrange\n        var kdf = new KdfRequestModel\n        {\n            KdfType = KdfType.PBKDF2_SHA256,\n            Iterations = 600000\n        };\n\n        var model = new SetInitialPasswordRequestModel\n        {\n            OrgIdentifier = orgIdentifier,\n            MasterPasswordHint = \"hint\",\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = kdf,\n                MasterPasswordAuthenticationHash = \"authHash\",\n                Salt = \"salt\"\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = kdf,\n                MasterKeyWrappedUserKey = \"wrappedKey\",\n                Salt = \"salt\"\n            },\n            AccountKeys = null\n        };\n\n        // Act\n        var result = model.ToData();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(orgIdentifier, result.OrgSsoIdentifier);\n        Assert.Null(result.AccountKeys);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Models/Request/EmergencyAccessRequestModelsTests.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Auth.Models.Request;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Models.Request;\n\npublic class EmergencyAccessInviteRequestModelTests\n{\n    [Theory]\n    [InlineData(0)]\n    [InlineData(-1)]\n    [InlineData(-100)]\n    public void Validate_WaitTimeDays_BelowMinimum_Invalid(int waitTimeDays)\n    {\n        var model = new EmergencyAccessInviteRequestModel\n        {\n            Email = \"test@example.com\",\n            Type = EmergencyAccessType.View,\n            WaitTimeDays = waitTimeDays,\n        };\n        var result = Validate(model);\n        Assert.Contains(result, r => r.MemberNames.Contains(\"WaitTimeDays\"));\n    }\n\n    [Theory]\n    [InlineData(1)]\n    [InlineData(7)]\n    [InlineData(90)]\n    [InlineData(short.MaxValue)]\n    public void Validate_WaitTimeDays_ValidRange_Valid(int waitTimeDays)\n    {\n        var model = new EmergencyAccessInviteRequestModel\n        {\n            Email = \"test@example.com\",\n            Type = EmergencyAccessType.View,\n            WaitTimeDays = waitTimeDays,\n        };\n        var result = Validate(model);\n        Assert.DoesNotContain(result, r => r.MemberNames.Contains(\"WaitTimeDays\"));\n    }\n\n    private static List<ValidationResult> Validate(EmergencyAccessInviteRequestModel model)\n    {\n        var results = new List<ValidationResult>();\n        Validator.TryValidateObject(model, new ValidationContext(model), results, true);\n        return results;\n    }\n}\n\npublic class EmergencyAccessUpdateRequestModelTests\n{\n    [Theory]\n    [InlineData(0)]\n    [InlineData(-1)]\n    [InlineData(-100)]\n    public void Validate_WaitTimeDays_BelowMinimum_Invalid(int waitTimeDays)\n    {\n        var model = new EmergencyAccessUpdateRequestModel\n        {\n            Type = EmergencyAccessType.View,\n            WaitTimeDays = waitTimeDays,\n        };\n        var result = Validate(model);\n        Assert.Contains(result, r => r.MemberNames.Contains(\"WaitTimeDays\"));\n    }\n\n    [Theory]\n    [InlineData(1)]\n    [InlineData(7)]\n    [InlineData(90)]\n    [InlineData(short.MaxValue)]\n    public void Validate_WaitTimeDays_ValidRange_Valid(int waitTimeDays)\n    {\n        var model = new EmergencyAccessUpdateRequestModel\n        {\n            Type = EmergencyAccessType.View,\n            WaitTimeDays = waitTimeDays,\n        };\n        var result = Validate(model);\n        Assert.DoesNotContain(result, r => r.MemberNames.Contains(\"WaitTimeDays\"));\n    }\n\n    [Fact]\n    public void ToEmergencyAccess_BothKeysPresent_UpdatesKey()\n    {\n        var model = new EmergencyAccessUpdateRequestModel\n        {\n            Type = EmergencyAccessType.Takeover,\n            WaitTimeDays = 7,\n            KeyEncrypted = \"new-encrypted-key\",\n        };\n        var existing = new EmergencyAccess { KeyEncrypted = \"old-encrypted-key\" };\n\n        var result = model.ToEmergencyAccess(existing);\n\n        Assert.Equal(\"new-encrypted-key\", result.KeyEncrypted);\n    }\n\n    [Theory]\n    [InlineData(null, \"new-encrypted-key\")]\n    [InlineData(\"\", \"new-encrypted-key\")]\n    [InlineData(\"   \", \"new-encrypted-key\")]\n    [InlineData(\"old-encrypted-key\", null)]\n    [InlineData(\"old-encrypted-key\", \"\")]\n    [InlineData(\"old-encrypted-key\", \"   \")]\n    public void ToEmergencyAccess_EitherKeyMissingOrWhitespace_DoesNotUpdateKey(\n        string? existingKey, string? newKey)\n    {\n        var model = new EmergencyAccessUpdateRequestModel\n        {\n            Type = EmergencyAccessType.Takeover,\n            WaitTimeDays = 7,\n            KeyEncrypted = newKey,\n        };\n        var existing = new EmergencyAccess { KeyEncrypted = existingKey };\n\n        var result = model.ToEmergencyAccess(existing);\n\n        Assert.Equal(existingKey, result.KeyEncrypted);\n    }\n\n    [Fact]\n    public void ToEmergencyAccess_AlwaysUpdatesTypeAndWaitTimeDays()\n    {\n        var model = new EmergencyAccessUpdateRequestModel\n        {\n            Type = EmergencyAccessType.Takeover,\n            WaitTimeDays = 14,\n        };\n        var existing = new EmergencyAccess\n        {\n            Type = EmergencyAccessType.View,\n            WaitTimeDays = 7,\n        };\n\n        var result = model.ToEmergencyAccess(existing);\n\n        Assert.Equal(EmergencyAccessType.Takeover, result.Type);\n        Assert.Equal(14, result.WaitTimeDays);\n    }\n\n    private static List<ValidationResult> Validate(EmergencyAccessUpdateRequestModel model)\n    {\n        var results = new List<ValidationResult>();\n        Validator.TryValidateObject(model, new ValidationContext(model), results, true);\n        return results;\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Models/Request/OrganizationSsoRequestModelTests.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Auth.Models.Request.Organizations;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Services;\nusing Bit.Core.Sso;\nusing Microsoft.Extensions.Localization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Models.Request;\n\npublic class OrganizationSsoRequestModelTests\n{\n    [Fact]\n    public void ToSsoConfig_WithOrganizationId_CreatesNewSsoConfig()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var model = new OrganizationSsoRequestModel\n        {\n            Enabled = true,\n            Identifier = \"test-identifier\",\n            Data = new SsoConfigurationDataRequest\n            {\n                ConfigType = SsoType.OpenIdConnect,\n                Authority = \"https://example.com\",\n                ClientId = \"test-client\",\n                ClientSecret = \"test-secret\"\n            }\n        };\n\n        // Act\n        var result = model.ToSsoConfig(organizationId);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(organizationId, result.OrganizationId);\n        Assert.True(result.Enabled);\n    }\n\n    [Fact]\n    public void ToSsoConfig_WithExistingConfig_UpdatesExistingConfig()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var existingConfig = new SsoConfig\n        {\n            Id = 1,\n            OrganizationId = organizationId,\n            Enabled = false\n        };\n\n        var model = new OrganizationSsoRequestModel\n        {\n            Enabled = true,\n            Identifier = \"updated-identifier\",\n            Data = new SsoConfigurationDataRequest\n            {\n                ConfigType = SsoType.Saml2,\n                IdpEntityId = \"test-entity\",\n                IdpSingleSignOnServiceUrl = \"https://sso.example.com\"\n            }\n        };\n\n        // Act\n        var result = model.ToSsoConfig(existingConfig);\n\n        // Assert\n        Assert.Same(existingConfig, result);\n        Assert.Equal(organizationId, result.OrganizationId);\n        Assert.True(result.Enabled);\n    }\n}\n\npublic class SsoConfigurationDataRequestTests\n{\n    private readonly TestI18nService _i18nService;\n    private readonly ValidationContext _validationContext;\n\n    public SsoConfigurationDataRequestTests()\n    {\n        _i18nService = new TestI18nService();\n        var serviceProvider = Substitute.For<IServiceProvider>();\n        serviceProvider.GetService(typeof(II18nService)).Returns(_i18nService);\n        _validationContext = new ValidationContext(new object(), serviceProvider, null);\n    }\n\n    [Fact]\n    public void ToConfigurationData_MapsProperties()\n    {\n        // Arrange\n        var model = new SsoConfigurationDataRequest\n        {\n            ConfigType = SsoType.OpenIdConnect,\n            MemberDecryptionType = MemberDecryptionType.KeyConnector,\n            Authority = \"https://authority.example.com\",\n            ClientId = \"test-client-id\",\n            ClientSecret = \"test-client-secret\",\n            IdpX509PublicCert = \"-----BEGIN CERTIFICATE-----\\nMIIC...test\\n-----END CERTIFICATE-----\",\n            SpOutboundSigningAlgorithm = null // Test default\n        };\n\n        // Act\n        var result = model.ToConfigurationData();\n\n        // Assert\n        Assert.Equal(SsoType.OpenIdConnect, result.ConfigType);\n        Assert.Equal(MemberDecryptionType.KeyConnector, result.MemberDecryptionType);\n        Assert.Equal(\"https://authority.example.com\", result.Authority);\n        Assert.Equal(\"test-client-id\", result.ClientId);\n        Assert.Equal(\"test-client-secret\", result.ClientSecret);\n        Assert.Equal(\"MIIC...test\", result.IdpX509PublicCert); // PEM headers stripped\n        Assert.Equal(SamlSigningAlgorithms.Sha256, result.SpOutboundSigningAlgorithm); // Default applied\n        Assert.Null(result.IdpArtifactResolutionServiceUrl); // Always null\n    }\n\n    [Fact]\n    public void KeyConnectorEnabled_Setter_UpdatesMemberDecryptionType()\n    {\n        // Arrange\n        var model = new SsoConfigurationDataRequest();\n\n        // Act & Assert\n#pragma warning disable CS0618 // Type or member is obsolete\n        model.KeyConnectorEnabled = true;\n        Assert.Equal(MemberDecryptionType.KeyConnector, model.MemberDecryptionType);\n\n        model.KeyConnectorEnabled = false;\n        Assert.Equal(MemberDecryptionType.MasterPassword, model.MemberDecryptionType);\n#pragma warning restore CS0618 // Type or member is obsolete\n    }\n\n    // Validation Tests\n    [Fact]\n    public void Validate_OpenIdConnect_ValidData_NoErrors()\n    {\n        // Arrange\n        var model = new SsoConfigurationDataRequest\n        {\n            ConfigType = SsoType.OpenIdConnect,\n            Authority = \"https://example.com\",\n            ClientId = \"test-client\",\n            ClientSecret = \"test-secret\"\n        };\n\n        // Act\n        var results = model.Validate(_validationContext).ToList();\n\n        // Assert\n        Assert.Empty(results);\n    }\n\n    [Theory]\n    [InlineData(\"\", \"test-client\", \"test-secret\", \"AuthorityValidationError\")]\n    [InlineData(\"https://example.com\", \"\", \"test-secret\", \"ClientIdValidationError\")]\n    [InlineData(\"https://example.com\", \"test-client\", \"\", \"ClientSecretValidationError\")]\n    public void Validate_OpenIdConnect_MissingRequiredFields_ReturnsErrors(string authority, string clientId, string clientSecret, string expectedError)\n    {\n        // Arrange\n        var model = new SsoConfigurationDataRequest\n        {\n            ConfigType = SsoType.OpenIdConnect,\n            Authority = authority,\n            ClientId = clientId,\n            ClientSecret = clientSecret\n        };\n\n        // Act\n        var results = model.Validate(_validationContext).ToList();\n\n        // Assert\n        Assert.Single(results);\n        Assert.Equal(expectedError, results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Saml2_ValidData_NoErrors()\n    {\n        // Arrange\n        var model = new SsoConfigurationDataRequest\n        {\n            ConfigType = SsoType.Saml2,\n            IdpEntityId = \"https://idp.example.com\",\n            IdpSingleSignOnServiceUrl = \"https://sso.example.com\",\n            IdpSingleLogoutServiceUrl = \"https://logout.example.com\"\n        };\n\n        // Act\n        var results = model.Validate(_validationContext).ToList();\n\n        // Assert\n        Assert.Empty(results);\n    }\n\n    [Theory]\n    [InlineData(\"\", \"https://sso.example.com\", \"IdpEntityIdValidationError\")]\n    [InlineData(\"not-a-valid-uri\", \"\", \"IdpSingleSignOnServiceUrlValidationError\")]\n    public void Validate_Saml2_MissingRequiredFields_ReturnsErrors(string entityId, string signOnUrl, string expectedError)\n    {\n        // Arrange\n        var model = new SsoConfigurationDataRequest\n        {\n            ConfigType = SsoType.Saml2,\n            IdpEntityId = entityId,\n            IdpSingleSignOnServiceUrl = signOnUrl\n        };\n\n        // Act\n        var results = model.Validate(_validationContext).ToList();\n\n        // Assert\n        Assert.Contains(results, r => r.ErrorMessage == expectedError);\n    }\n\n    [Theory]\n    [InlineData(\"not-a-url\")]\n    [InlineData(\"ftp://example.com\")]\n    [InlineData(\"https://example.com<script>\")]\n    [InlineData(\"https://example.com\\\"onclick\")]\n    public void Validate_Saml2_InvalidUrls_ReturnsErrors(string invalidUrl)\n    {\n        // Arrange\n        var model = new SsoConfigurationDataRequest\n        {\n            ConfigType = SsoType.Saml2,\n            IdpEntityId = \"https://idp.example.com\",\n            IdpSingleSignOnServiceUrl = invalidUrl,\n            IdpSingleLogoutServiceUrl = invalidUrl\n        };\n\n        // Act\n        var results = model.Validate(_validationContext).ToList();\n\n        // Assert\n        Assert.Contains(results, r => r.ErrorMessage == \"IdpSingleSignOnServiceUrlInvalid\");\n        Assert.Contains(results, r => r.ErrorMessage == \"IdpSingleLogoutServiceUrlInvalid\");\n    }\n\n    [Fact]\n    public void Validate_Saml2_MissingSignOnUrl_AlwaysReturnsError()\n    {\n        // Arrange - SignOnUrl is always required for SAML2, regardless of EntityId format\n        var model = new SsoConfigurationDataRequest\n        {\n            ConfigType = SsoType.Saml2,\n            IdpEntityId = \"https://idp.example.com\", // Valid URI\n            IdpSingleSignOnServiceUrl = \"\" // Missing - always causes error\n        };\n\n        // Act\n        var results = model.Validate(_validationContext).ToList();\n\n        // Assert - Should always fail validation when SignOnUrl is missing\n        Assert.Contains(results, r => r.ErrorMessage == \"IdpSingleSignOnServiceUrlValidationError\");\n    }\n\n    [Fact]\n    public void Validate_Saml2_InvalidCertificate_ReturnsError()\n    {\n        // Arrange\n        var model = new SsoConfigurationDataRequest\n        {\n            ConfigType = SsoType.Saml2,\n            IdpEntityId = \"https://idp.example.com\",\n            IdpSingleSignOnServiceUrl = \"https://sso.example.com\",\n            IdpX509PublicCert = \"invalid-certificate-data\"\n        };\n\n        // Act\n        var results = model.Validate(_validationContext).ToList();\n\n        // Assert\n        Assert.Contains(results, r => r.ErrorMessage.Contains(\"IdpX509PublicCert\") && r.ErrorMessage.Contains(\"ValidationError\"));\n    }\n\n    // TODO: On server, make public certificate required for SAML2 SSO: https://bitwarden.atlassian.net/browse/PM-26028\n    [Fact]\n    public void Validate_Saml2_EmptyCertificate_PassesValidation()\n    {\n        // Arrange\n        var model = new SsoConfigurationDataRequest\n        {\n            ConfigType = SsoType.Saml2,\n            IdpEntityId = \"https://idp.example.com\",\n            IdpSingleSignOnServiceUrl = \"https://sso.example.com\",\n            IdpX509PublicCert = \"\"\n        };\n\n        // Act\n        var results = model.Validate(_validationContext).ToList();\n\n        // Assert\n        Assert.DoesNotContain(results, r => r.MemberNames.Contains(\"IdpX509PublicCert\"));\n    }\n\n    private class TestI18nService : I18nService\n    {\n        public TestI18nService() : base(CreateMockLocalizerFactory()) { }\n\n        private static IStringLocalizerFactory CreateMockLocalizerFactory()\n        {\n            var factory = Substitute.For<IStringLocalizerFactory>();\n            var localizer = Substitute.For<IStringLocalizer>();\n\n            localizer[Arg.Any<string>()].Returns(callInfo => new LocalizedString(callInfo.Arg<string>(), callInfo.Arg<string>()));\n            localizer[Arg.Any<string>(), Arg.Any<object[]>()].Returns(callInfo => new LocalizedString(callInfo.Arg<string>(), callInfo.Arg<string>()));\n\n            factory.Create(Arg.Any<string>(), Arg.Any<string>()).Returns(localizer);\n            return factory;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs",
    "content": "﻿using Bit.Api.Auth.Models.Request;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Models.Request;\n\npublic class OrganizationTwoFactorDuoRequestModelTests\n{\n\n    [Fact]\n    public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist()\n    {\n        // Arrange\n        var existingOrg = new Organization();\n        var model = new UpdateTwoFactorDuoRequestModel\n        {\n            ClientId = \"clientId\",\n            ClientSecret = \"clientSecret\",\n            Host = \"example.com\"\n        };\n\n        // Act\n        var result = model.ToOrganization(existingOrg);\n\n        // Assert\n        Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo));\n        Assert.Equal(\"clientId\", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData[\"ClientId\"]);\n        Assert.Equal(\"clientSecret\", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData[\"ClientSecret\"]);\n        Assert.Equal(\"example.com\", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData[\"Host\"]);\n        Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled);\n    }\n\n    [Fact]\n    public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists()\n    {\n        // Arrange\n        var existingOrg = new Organization();\n        existingOrg.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider() }\n        });\n        var model = new UpdateTwoFactorDuoRequestModel\n        {\n            ClientId = \"newClientId\",\n            ClientSecret = \"newClientSecret\",\n            Host = \"newExample.com\"\n        };\n\n        // Act\n        var result = model.ToOrganization(existingOrg);\n\n        // Assert\n        Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo));\n        Assert.Equal(\"newClientId\", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData[\"ClientId\"]);\n        Assert.Equal(\"newClientSecret\", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData[\"ClientSecret\"]);\n        Assert.Equal(\"newExample.com\", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData[\"Host\"]);\n        Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Auth.Models.Request;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Models.Request;\n\npublic class TwoFactorDuoRequestModelValidationTests\n{\n    [Fact]\n    public void ShouldReturnValidationError_WhenHostIsInvalid()\n    {\n        // Arrange\n        var model = new UpdateTwoFactorDuoRequestModel\n        {\n            Host = \"invalidHost\",\n            ClientId = \"clientId\",\n            ClientSecret = \"clientSecret\",\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model));\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"Host is invalid.\", result.First().ErrorMessage);\n        Assert.Equal(\"Host\", result.First().MemberNames.First());\n    }\n\n    [Fact]\n    public void ShouldReturnValidationError_WhenValuesAreInvalid()\n    {\n        // Arrange\n        var model = new UpdateTwoFactorDuoRequestModel\n        {\n            Host = \"api-12345abc.duosecurity.com\"\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model));\n\n        // Assert\n        Assert.NotEmpty(result);\n        Assert.True(result.Select(x => x.MemberNames.Contains(\"ClientId\")).Any());\n        Assert.True(result.Select(x => x.MemberNames.Contains(\"ClientSecret\")).Any());\n    }\n\n    [Fact]\n    public void ShouldReturnSuccess_WhenValuesAreValid()\n    {\n        // Arrange\n        var model = new UpdateTwoFactorDuoRequestModel\n        {\n            Host = \"api-12345abc.duosecurity.com\",\n            ClientId = \"clientId\",\n            ClientSecret = \"clientSecret\",\n        };\n\n        // Act\n        var result = model.Validate(new ValidationContext(model));\n\n        // Assert\n        Assert.Empty(result);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs",
    "content": "﻿using Bit.Api.Auth.Models.Request;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Models.Request;\n\npublic class UserTwoFactorDuoRequestModelTests\n{\n    [Fact]\n    public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist()\n    {\n        // Arrange\n        var existingUser = new User();\n        var model = new UpdateTwoFactorDuoRequestModel\n        {\n            ClientId = \"clientId\",\n            ClientSecret = \"clientSecret\",\n            Host = \"example.com\"\n        };\n\n        // Act\n        var result = model.ToUser(existingUser);\n\n        // Assert\n        Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo));\n        Assert.Equal(\"clientId\", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData[\"ClientId\"]);\n        Assert.Equal(\"clientSecret\", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData[\"ClientSecret\"]);\n        Assert.Equal(\"example.com\", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData[\"Host\"]);\n        Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled);\n    }\n\n    [Fact]\n    public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists()\n    {\n        // Arrange\n        var existingUser = new User();\n        existingUser.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { TwoFactorProviderType.Duo, new TwoFactorProvider() }\n        });\n        var model = new UpdateTwoFactorDuoRequestModel\n        {\n            ClientId = \"newClientId\",\n            ClientSecret = \"newClientSecret\",\n            Host = \"newExample.com\"\n        };\n\n        // Act\n        var result = model.ToUser(existingUser);\n\n        // Assert\n        Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo));\n        Assert.Equal(\"newClientId\", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData[\"ClientId\"]);\n        Assert.Equal(\"newClientSecret\", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData[\"ClientSecret\"]);\n        Assert.Equal(\"newExample.com\", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData[\"Host\"]);\n        Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Models/Response/EmergencyAccessTakeoverResponseModelTests.cs",
    "content": "﻿using Bit.Api.Auth.Models.Response;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Models.Response;\n\npublic class EmergencyAccessTakeoverResponseModelTests\n{\n    [Theory]\n    [BitAutoData]\n    public void Constructor_EmergencyAccessNull_ThrowsArgumentNullException(User grantor)\n    {\n        var exception = Assert.Throws<ArgumentNullException>(\n            () => new EmergencyAccessTakeoverResponseModel(null, grantor));\n        Assert.Equal(\"emergencyAccess\", exception.ParamName);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_ValidInputs_SetsAllPropertiesCorrectly(\n        EmergencyAccess emergencyAccess, User grantor)\n    {\n        var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor);\n\n        Assert.Equal(emergencyAccess.KeyEncrypted, model.KeyEncrypted);\n        Assert.Equal(grantor.Kdf, model.Kdf);\n        Assert.Equal(grantor.KdfIterations, model.KdfIterations);\n        Assert.Equal(grantor.KdfMemory, model.KdfMemory);\n        Assert.Equal(grantor.KdfParallelism, model.KdfParallelism);\n        Assert.Equal(grantor.GetMasterPasswordSalt(), model.Salt);\n    }\n\n    [Theory]\n    [InlineData(\"user@domain.com\", \"user@domain.com\")]\n    [InlineData(\"USER@DOMAIN.COM\", \"user@domain.com\")]\n    [InlineData(\"  user@domain.com  \", \"user@domain.com\")]\n    [InlineData(\"  USER@DOMAIN.COM  \", \"user@domain.com\")]\n    public void Constructor_SaltWithVariousEmailFormats_NormalizesCorrectly(\n        string email, string expectedSalt)\n    {\n        var emergencyAccess = new EmergencyAccess\n        {\n            Id = Guid.NewGuid(),\n            KeyEncrypted = \"test-key-encrypted\"\n        };\n        var grantor = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = email,\n            SecurityStamp = \"security-stamp\",\n            ApiKey = \"api-key\"\n        };\n\n        var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor);\n\n        Assert.Equal(expectedSalt, model.Salt);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithPBKDF2_SetsKdfTypeCorrectly(\n        EmergencyAccess emergencyAccess, User grantor)\n    {\n        grantor.Kdf = KdfType.PBKDF2_SHA256;\n        grantor.KdfIterations = 600000;\n        grantor.KdfMemory = null;\n        grantor.KdfParallelism = null;\n\n        var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor);\n\n        Assert.Equal(KdfType.PBKDF2_SHA256, model.Kdf);\n        Assert.Equal(600000, model.KdfIterations);\n        Assert.Null(model.KdfMemory);\n        Assert.Null(model.KdfParallelism);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithArgon2id_SetsAllKdfPropertiesCorrectly(\n        EmergencyAccess emergencyAccess, User grantor)\n    {\n        grantor.Kdf = KdfType.Argon2id;\n        grantor.KdfIterations = 3;\n        grantor.KdfMemory = 64;\n        grantor.KdfParallelism = 4;\n\n        var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor);\n\n        Assert.Equal(KdfType.Argon2id, model.Kdf);\n        Assert.Equal(3, model.KdfIterations);\n        Assert.Equal(64, model.KdfMemory);\n        Assert.Equal(4, model.KdfParallelism);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_SetsObjectTypeCorrectly(\n        EmergencyAccess emergencyAccess, User grantor)\n    {\n        var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor);\n\n        Assert.Equal(\"emergencyAccessTakeover\", model.Object);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_CustomObjectName_SetsObjectTypeCorrectly(\n        EmergencyAccess emergencyAccess, User grantor)\n    {\n        var model = new EmergencyAccessTakeoverResponseModel(emergencyAccess, grantor, \"customObject\");\n\n        Assert.Equal(\"customObject\", model.Object);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs",
    "content": "﻿\nusing Bit.Api.Auth.Models.Response.TwoFactor;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Models.Response;\n\npublic class OrganizationTwoFactorDuoResponseModelTests\n{\n    [Theory]\n    [BitAutoData]\n    public void Organization_WithDuo_ShouldBuildModel(Organization organization)\n    {\n        // Arrange\n        organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProvidersJson();\n\n        // Act\n        var model = new TwoFactorDuoResponseModel(organization);\n\n        // Assert\n        Assert.NotNull(model);\n        Assert.Equal(\"clientId\", model.ClientId);\n        Assert.Equal(\"secret************\", model.ClientSecret);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Organization_WithDuoEmpty_ShouldFail(Organization organization)\n    {\n        // Arrange\n        organization.TwoFactorProviders = \"{\\\"6\\\" : {}}\";\n\n        // Act\n        var model = new TwoFactorDuoResponseModel(organization);\n\n        // Assert\n        Assert.False(model.Enabled);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Organization_WithTwoFactorProvidersNull_ShouldThrow(Organization organization)\n    {\n        // Arrange\n        organization.TwoFactorProviders = null;\n\n        // Act\n        try\n        {\n            var model = new TwoFactorDuoResponseModel(organization);\n\n        }\n        catch (Exception ex)\n        {\n            // Assert\n            Assert.IsType<ArgumentNullException>(ex);\n        }\n    }\n\n    private string GetTwoFactorOrganizationDuoProvidersJson()\n    {\n        return\n            \"{\\\"6\\\":{\\\"Enabled\\\":true,\\\"MetaData\\\":{\\\"ClientSecret\\\":\\\"secretClientSecret\\\",\\\"ClientId\\\":\\\"clientId\\\",\\\"Host\\\":\\\"example.com\\\"}}}\";\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs",
    "content": "﻿\nusing Bit.Api.Auth.Models.Response.TwoFactor;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.Auth.Models.Response;\n\npublic class UserTwoFactorDuoResponseModelTests\n{\n    [Theory]\n    [BitAutoData]\n    public void User_WithDuo_UserNull_ThrowsArgumentException(User user)\n    {\n        // Arrange\n        user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();\n\n        // Act\n        try\n        {\n            var model = new TwoFactorDuoResponseModel(null as User);\n        }\n        catch (ArgumentNullException e)\n        {\n            // Assert\n            Assert.Equal(\"Value cannot be null. (Parameter 'user')\", e.Message);\n        }\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void User_WithDuo_ShouldBuildModel(User user)\n    {\n        // Arrange\n        user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();\n\n        // Act\n        var model = new TwoFactorDuoResponseModel(user);\n\n        // Assert\n        Assert.NotNull(model);\n        Assert.Equal(\"clientId\", model.ClientId);\n        Assert.Equal(\"secret************\", model.ClientSecret);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void User_WithDuoEmpty_ShouldFail(User user)\n    {\n        // Arrange\n        user.TwoFactorProviders = \"{\\\"2\\\" : {}}\";\n\n        // Act\n        var model = new TwoFactorDuoResponseModel(user);\n\n        /// Assert\n        Assert.False(model.Enabled);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void User_WithTwoFactorProvidersNull_ShouldFail(User user)\n    {\n        // Arrange\n        user.TwoFactorProviders = null;\n\n        // Act\n        try\n        {\n            var model = new TwoFactorDuoResponseModel(user);\n\n        }\n        catch (Exception ex)\n        {\n            // Assert\n            Assert.IsType<ArgumentNullException>(ex);\n\n        }\n\n    }\n\n    private string GetTwoFactorDuoProvidersJson()\n    {\n        return\n            \"{\\\"2\\\":{\\\"Enabled\\\":true,\\\"MetaData\\\":{\\\"ClientSecret\\\":\\\"secretClientSecret\\\",\\\"ClientId\\\":\\\"clientId\\\",\\\"Host\\\":\\\"example.com\\\"}}}\";\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Attributes/InjectOrganizationAttributeTests.cs",
    "content": "﻿using Bit.Api.Billing.Attributes;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Repositories;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Abstractions;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.Extensions.DependencyInjection;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Billing.Attributes;\n\npublic class InjectOrganizationAttributeTests\n{\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly ActionExecutionDelegate _next;\n    private readonly ActionExecutingContext _context;\n    private readonly Organization _organization;\n    private readonly Guid _organizationId;\n\n    public InjectOrganizationAttributeTests()\n    {\n        _organizationRepository = Substitute.For<IOrganizationRepository>();\n        _organizationId = Guid.NewGuid();\n        _organization = new Organization { Id = _organizationId };\n\n        var httpContext = new DefaultHttpContext();\n        var services = new ServiceCollection();\n        services.AddScoped(_ => _organizationRepository);\n        httpContext.RequestServices = services.BuildServiceProvider();\n\n        var routeData = new RouteData { Values = { [\"organizationId\"] = _organizationId.ToString() } };\n\n        var actionContext = new ActionContext(\n            httpContext,\n            routeData,\n            new ActionDescriptor(),\n            new ModelStateDictionary()\n        );\n\n        _next = () => Task.FromResult(new ActionExecutedContext(\n            actionContext,\n            new List<IFilterMetadata>(),\n            new object()));\n\n        _context = new ActionExecutingContext(\n            actionContext,\n            new List<IFilterMetadata>(),\n            new Dictionary<string, object>(),\n            new object());\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithExistingOrganization_InjectsOrganization()\n    {\n        var attribute = new InjectOrganizationAttribute();\n        _organizationRepository.GetByIdAsync(_organizationId)\n            .Returns(_organization);\n\n        var parameter = new ParameterDescriptor\n        {\n            Name = \"organization\",\n            ParameterType = typeof(Organization)\n        };\n        _context.ActionDescriptor.Parameters = [parameter];\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.Equal(_organization, _context.ActionArguments[\"organization\"]);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithNonExistentOrganization_ReturnsNotFound()\n    {\n        var attribute = new InjectOrganizationAttribute();\n        _organizationRepository.GetByIdAsync(_organizationId)\n            .Returns((Organization)null);\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.IsType<NotFoundObjectResult>(_context.Result);\n        var result = (NotFoundObjectResult)_context.Result;\n        Assert.IsType<ErrorResponseModel>(result.Value);\n        Assert.Equal(\"Organization not found.\", ((ErrorResponseModel)result.Value).Message);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithInvalidOrganizationId_ReturnsBadRequest()\n    {\n        var attribute = new InjectOrganizationAttribute();\n        _context.RouteData.Values[\"organizationId\"] = \"not-a-guid\";\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.IsType<BadRequestObjectResult>(_context.Result);\n        var result = (BadRequestObjectResult)_context.Result;\n        Assert.IsType<ErrorResponseModel>(result.Value);\n        Assert.Equal(\"Route parameter 'organizationId' is missing or invalid.\", ((ErrorResponseModel)result.Value).Message);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithMissingOrganizationId_ReturnsBadRequest()\n    {\n        var attribute = new InjectOrganizationAttribute();\n        _context.RouteData.Values.Clear();\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.IsType<BadRequestObjectResult>(_context.Result);\n        var result = (BadRequestObjectResult)_context.Result;\n        Assert.IsType<ErrorResponseModel>(result.Value);\n        Assert.Equal(\"Route parameter 'organizationId' is missing or invalid.\", ((ErrorResponseModel)result.Value).Message);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithoutOrganizationParameter_ContinuesExecution()\n    {\n        var attribute = new InjectOrganizationAttribute();\n        _organizationRepository.GetByIdAsync(_organizationId)\n            .Returns(_organization);\n\n        _context.ActionDescriptor.Parameters = Array.Empty<ParameterDescriptor>();\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.Empty(_context.ActionArguments);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Attributes/InjectProviderAttributeTests.cs",
    "content": "﻿using Bit.Api.Billing.Attributes;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Abstractions;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.Extensions.DependencyInjection;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Billing.Attributes;\n\npublic class InjectProviderAttributeTests\n{\n    private readonly IProviderRepository _providerRepository;\n    private readonly ICurrentContext _currentContext;\n    private readonly ActionExecutionDelegate _next;\n    private readonly ActionExecutingContext _context;\n    private readonly Provider _provider;\n    private readonly Guid _providerId;\n\n    public InjectProviderAttributeTests()\n    {\n        _providerRepository = Substitute.For<IProviderRepository>();\n        _currentContext = Substitute.For<ICurrentContext>();\n        _providerId = Guid.NewGuid();\n        _provider = new Provider { Id = _providerId };\n\n        var httpContext = new DefaultHttpContext();\n        var services = new ServiceCollection();\n        services.AddScoped(_ => _providerRepository);\n        services.AddScoped(_ => _currentContext);\n        httpContext.RequestServices = services.BuildServiceProvider();\n\n        var routeData = new RouteData { Values = { [\"providerId\"] = _providerId.ToString() } };\n\n        var actionContext = new ActionContext(\n            httpContext,\n            routeData,\n            new ActionDescriptor(),\n            new ModelStateDictionary()\n        );\n\n        _next = () => Task.FromResult(new ActionExecutedContext(\n            actionContext,\n            new List<IFilterMetadata>(),\n            new object()));\n\n        _context = new ActionExecutingContext(\n            actionContext,\n            new List<IFilterMetadata>(),\n            new Dictionary<string, object>(),\n            new object());\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithExistingProvider_InjectsProvider()\n    {\n        var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);\n        _providerRepository.GetByIdAsync(_providerId).Returns(_provider);\n        _currentContext.ProviderProviderAdmin(_providerId).Returns(true);\n\n        var parameter = new ParameterDescriptor\n        {\n            Name = \"provider\",\n            ParameterType = typeof(Provider)\n        };\n        _context.ActionDescriptor.Parameters = [parameter];\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.Equal(_provider, _context.ActionArguments[\"provider\"]);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithNonExistentProvider_ReturnsNotFound()\n    {\n        var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);\n        _providerRepository.GetByIdAsync(_providerId).Returns((Provider)null);\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.IsType<NotFoundObjectResult>(_context.Result);\n        var result = (NotFoundObjectResult)_context.Result;\n        Assert.IsType<ErrorResponseModel>(result.Value);\n        Assert.Equal(\"Provider not found.\", ((ErrorResponseModel)result.Value).Message);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithInvalidProviderId_ReturnsBadRequest()\n    {\n        var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);\n        _context.RouteData.Values[\"providerId\"] = \"not-a-guid\";\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.IsType<BadRequestObjectResult>(_context.Result);\n        var result = (BadRequestObjectResult)_context.Result;\n        Assert.IsType<ErrorResponseModel>(result.Value);\n        Assert.Equal(\"Route parameter 'providerId' is missing or invalid.\", ((ErrorResponseModel)result.Value).Message);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithMissingProviderId_ReturnsBadRequest()\n    {\n        var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);\n        _context.RouteData.Values.Clear();\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.IsType<BadRequestObjectResult>(_context.Result);\n        var result = (BadRequestObjectResult)_context.Result;\n        Assert.IsType<ErrorResponseModel>(result.Value);\n        Assert.Equal(\"Route parameter 'providerId' is missing or invalid.\", ((ErrorResponseModel)result.Value).Message);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithoutProviderParameter_ContinuesExecution()\n    {\n        var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);\n        _providerRepository.GetByIdAsync(_providerId).Returns(_provider);\n        _currentContext.ProviderProviderAdmin(_providerId).Returns(true);\n\n        _context.ActionDescriptor.Parameters = Array.Empty<ParameterDescriptor>();\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.Empty(_context.ActionArguments);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_UnauthorizedProviderAdmin_ReturnsUnauthorized()\n    {\n        var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);\n        _providerRepository.GetByIdAsync(_providerId).Returns(_provider);\n        _currentContext.ProviderProviderAdmin(_providerId).Returns(false);\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.IsType<UnauthorizedObjectResult>(_context.Result);\n        var result = (UnauthorizedObjectResult)_context.Result;\n        Assert.IsType<ErrorResponseModel>(result.Value);\n        Assert.Equal(\"Unauthorized.\", ((ErrorResponseModel)result.Value).Message);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_UnauthorizedServiceUser_ReturnsUnauthorized()\n    {\n        var attribute = new InjectProviderAttribute(ProviderUserType.ServiceUser);\n        _providerRepository.GetByIdAsync(_providerId).Returns(_provider);\n        _currentContext.ProviderUser(_providerId).Returns(false);\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.IsType<UnauthorizedObjectResult>(_context.Result);\n        var result = (UnauthorizedObjectResult)_context.Result;\n        Assert.IsType<ErrorResponseModel>(result.Value);\n        Assert.Equal(\"Unauthorized.\", ((ErrorResponseModel)result.Value).Message);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_AuthorizedProviderAdmin_Succeeds()\n    {\n        var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin);\n        _providerRepository.GetByIdAsync(_providerId).Returns(_provider);\n        _currentContext.ProviderProviderAdmin(_providerId).Returns(true);\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.Null(_context.Result);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_AuthorizedServiceUser_Succeeds()\n    {\n        var attribute = new InjectProviderAttribute(ProviderUserType.ServiceUser);\n        _providerRepository.GetByIdAsync(_providerId).Returns(_provider);\n        _currentContext.ProviderUser(_providerId).Returns(true);\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.Null(_context.Result);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Attributes/InjectUserAttributesTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.Billing.Attributes;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Abstractions;\nusing Microsoft.AspNetCore.Mvc.Filters;\nusing Microsoft.AspNetCore.Mvc.ModelBinding;\nusing Microsoft.AspNetCore.Routing;\nusing Microsoft.Extensions.DependencyInjection;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Billing.Attributes;\n\npublic class InjectUserAttributesTests\n{\n    private readonly IUserService _userService;\n    private readonly ActionExecutionDelegate _next;\n    private readonly ActionExecutingContext _context;\n    private readonly User _user;\n\n    public InjectUserAttributesTests()\n    {\n        _userService = Substitute.For<IUserService>();\n        _user = new User { Id = Guid.NewGuid() };\n\n        var httpContext = new DefaultHttpContext();\n        var services = new ServiceCollection();\n        services.AddScoped(_ => _userService);\n        httpContext.RequestServices = services.BuildServiceProvider();\n\n        var actionContext = new ActionContext(\n            httpContext,\n            new RouteData(),\n            new ActionDescriptor(),\n            new ModelStateDictionary()\n        );\n\n        _next = () => Task.FromResult(new ActionExecutedContext(\n            actionContext,\n            new List<IFilterMetadata>(),\n            new object()));\n\n        _context = new ActionExecutingContext(\n            actionContext,\n            new List<IFilterMetadata>(),\n            new Dictionary<string, object>(),\n            new object());\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithAuthorizedUser_InjectsUser()\n    {\n        var attribute = new InjectUserAttribute();\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(_user);\n\n        var parameter = new ParameterDescriptor\n        {\n            Name = \"user\",\n            ParameterType = typeof(User)\n        };\n        _context.ActionDescriptor.Parameters = [parameter];\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.Equal(_user, _context.ActionArguments[\"user\"]);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithUnauthorizedUser_ReturnsUnauthorized()\n    {\n        var attribute = new InjectUserAttribute();\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns((User)null);\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.IsType<UnauthorizedObjectResult>(_context.Result);\n        var result = (UnauthorizedObjectResult)_context.Result;\n        Assert.IsType<ErrorResponseModel>(result.Value);\n        Assert.Equal(\"Unauthorized.\", ((ErrorResponseModel)result.Value).Message);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithoutUserParameter_ContinuesExecution()\n    {\n        var attribute = new InjectUserAttribute();\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(_user);\n\n        _context.ActionDescriptor.Parameters = Array.Empty<ParameterDescriptor>();\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.Empty(_context.ActionArguments);\n    }\n\n    [Fact]\n    public async Task OnActionExecutionAsync_WithMultipleParameters_InjectsUserCorrectly()\n    {\n        var attribute = new InjectUserAttribute();\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(_user);\n\n        var parameters = new[]\n        {\n            new ParameterDescriptor\n            {\n                Name = \"otherParam\",\n                ParameterType = typeof(string)\n            },\n            new ParameterDescriptor\n            {\n                Name = \"user\",\n                ParameterType = typeof(User)\n            }\n        };\n        _context.ActionDescriptor.Parameters = parameters;\n\n        await attribute.OnActionExecutionAsync(_context, _next);\n\n        Assert.Single(_context.ActionArguments);\n        Assert.Equal(_user, _context.ActionArguments[\"user\"]);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Controllers/AccountsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.Billing.Controllers;\nusing Bit.Core;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Api.Test.Billing.Controllers;\n\n[SubscriptionInfoCustomize]\npublic class AccountsControllerTests : IDisposable\n{\n    private const string TestMilestone2CouponId = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount;\n\n    private readonly IUserService _userService;\n    private readonly IFeatureService _featureService;\n    private readonly IStripePaymentService _paymentService;\n    private readonly ILicensingService _licensingService;\n    private readonly GlobalSettings _globalSettings;\n    private readonly AccountsController _sut;\n\n    public AccountsControllerTests()\n    {\n        _userService = Substitute.For<IUserService>();\n        _featureService = Substitute.For<IFeatureService>();\n        _paymentService = Substitute.For<IStripePaymentService>();\n        _licensingService = Substitute.For<ILicensingService>();\n        _globalSettings = new GlobalSettings { SelfHosted = false };\n\n        _sut = new AccountsController(\n            _userService,\n            _featureService,\n            _licensingService\n        );\n    }\n\n    public void Dispose()\n    {\n        _sut?.Dispose();\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WhenFeatureFlagEnabled_IncludesDiscount(\n        User user,\n        SubscriptionInfo subscriptionInfo,\n        UserLicense license)\n    {\n        // Arrange\n        subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n        {\n            Id = TestMilestone2CouponId,\n            Active = true,\n            PercentOff = 20m,\n            AmountOff = null,\n            AppliesTo = new List<string> { \"product1\" }\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n\n        user.Gateway = GatewayType.Stripe; // User has payment gateway\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);\n        Assert.Equal(20m, result.CustomerDiscount.PercentOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WhenFeatureFlagDisabled_ExcludesDiscount(\n        User user,\n        SubscriptionInfo subscriptionInfo,\n        UserLicense license)\n    {\n        // Arrange\n        subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n        {\n            Id = TestMilestone2CouponId,\n            Active = true,\n            PercentOff = 20m,\n            AmountOff = null,\n            AppliesTo = new List<string> { \"product1\" }\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(false);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n\n        user.Gateway = GatewayType.Stripe; // User has payment gateway\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Null(result.CustomerDiscount); // Should be null when feature flag is disabled\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithNonMatchingCouponId_ExcludesDiscount(\n        User user,\n        SubscriptionInfo subscriptionInfo,\n        UserLicense license)\n    {\n        // Arrange\n        subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n        {\n            Id = \"different-coupon-id\", // Non-matching coupon ID\n            Active = true,\n            PercentOff = 20m,\n            AmountOff = null,\n            AppliesTo = new List<string> { \"product1\" }\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n\n        user.Gateway = GatewayType.Stripe; // User has payment gateway\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Null(result.CustomerDiscount); // Should be null when coupon ID doesn't match\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WhenSelfHosted_ReturnsBasicResponse(User user)\n    {\n        // Arrange\n        var selfHostedSettings = new GlobalSettings { SelfHosted = true };\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(selfHostedSettings, _paymentService);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Null(result.CustomerDiscount);\n        await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WhenNoGateway_ExcludesDiscount(User user, UserLicense license)\n    {\n        // Arrange\n        user.Gateway = null; // No gateway configured\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _userService.GenerateLicenseAsync(user).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Null(result.CustomerDiscount); // Should be null when no gateway\n        await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithInactiveDiscount_ExcludesDiscount(\n        User user,\n        SubscriptionInfo subscriptionInfo,\n        UserLicense license)\n    {\n        // Arrange\n        subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n        {\n            Id = TestMilestone2CouponId,\n            Active = false, // Inactive discount\n            PercentOff = 20m,\n            AmountOff = null,\n            AppliesTo = new List<string> { \"product1\" }\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n\n        user.Gateway = GatewayType.Stripe; // User has payment gateway\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Null(result.CustomerDiscount); // Should be null when discount is inactive\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_FullPipeline_ConvertsStripeDiscountToApiResponse(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Create a Stripe Discount object with real structure\n        var stripeDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = TestMilestone2CouponId,\n                PercentOff = 25m,\n                AmountOff = 1400, // 1400 cents = $14.00\n                AppliesTo = new CouponAppliesTo\n                {\n                    Products = new List<string> { \"prod_premium\", \"prod_families\" }\n                }\n            },\n            End = null // Active discount\n        };\n\n        // Convert Stripe Discount to BillingCustomerDiscount (simulating what StripePaymentService does)\n        var billingDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);\n\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = billingDiscount\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n\n        user.Gateway = GatewayType.Stripe;\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert - Verify full pipeline conversion\n        Assert.NotNull(result);\n        Assert.NotNull(result.CustomerDiscount);\n\n        // Verify Stripe data correctly converted to API response\n        Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);\n        Assert.True(result.CustomerDiscount.Active);\n        Assert.Equal(25m, result.CustomerDiscount.PercentOff);\n\n        // Verify cents-to-dollars conversion (1400 cents -> $14.00)\n        Assert.Equal(14.00m, result.CustomerDiscount.AmountOff);\n\n        // Verify AppliesTo products are preserved\n        Assert.NotNull(result.CustomerDiscount.AppliesTo);\n        Assert.Equal(2, result.CustomerDiscount.AppliesTo.Count());\n        Assert.Contains(\"prod_premium\", result.CustomerDiscount.AppliesTo);\n        Assert.Contains(\"prod_families\", result.CustomerDiscount.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_FullPipeline_WithFeatureFlagToggle_ControlsVisibility(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Create Stripe Discount\n        var stripeDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = TestMilestone2CouponId,\n                PercentOff = 20m\n            },\n            End = null\n        };\n\n        var billingDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = billingDiscount\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n        user.Gateway = GatewayType.Stripe;\n\n        // Act & Assert - Feature flag ENABLED\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        var resultWithFlag = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n        Assert.NotNull(resultWithFlag.CustomerDiscount);\n\n        // Act & Assert - Feature flag DISABLED\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(false);\n        var resultWithoutFlag = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n        Assert.Null(resultWithoutFlag.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_IntegrationTest_CompletePipelineFromStripeToApiResponse(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Create a real Stripe Discount object as it would come from Stripe API\n        var stripeDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = TestMilestone2CouponId,\n                PercentOff = 30m,\n                AmountOff = 2000, // 2000 cents = $20.00\n                AppliesTo = new CouponAppliesTo\n                {\n                    Products = new List<string> { \"prod_premium\", \"prod_families\", \"prod_teams\" }\n                }\n            },\n            End = null // Active discount (no end date)\n        };\n\n        // Step 1: Map Stripe Discount through SubscriptionInfo.BillingCustomerDiscount\n        // This simulates what StripePaymentService.GetSubscriptionAsync does\n        var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);\n\n        // Verify the mapping worked correctly\n        Assert.Equal(TestMilestone2CouponId, billingCustomerDiscount.Id);\n        Assert.True(billingCustomerDiscount.Active);\n        Assert.Equal(30m, billingCustomerDiscount.PercentOff);\n        Assert.Equal(20.00m, billingCustomerDiscount.AmountOff); // Converted from cents\n        Assert.NotNull(billingCustomerDiscount.AppliesTo);\n        Assert.Equal(3, billingCustomerDiscount.AppliesTo.Count);\n\n        // Step 2: Create SubscriptionInfo with the mapped discount\n        // This simulates what StripePaymentService returns\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = billingCustomerDiscount\n        };\n\n        // Step 3: Set up controller dependencies\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n        user.Gateway = GatewayType.Stripe;\n\n        // Act - Step 4: Call AccountsController.GetSubscriptionAsync\n        // This exercises the complete pipeline:\n        // - Retrieves subscriptionInfo from paymentService (with discount from Stripe)\n        // - Maps through SubscriptionInfo.BillingCustomerDiscount (already done above)\n        // - Filters in SubscriptionResponseModel constructor (based on feature flag, coupon ID, active status)\n        // - Returns via AccountsController\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert - Verify the complete pipeline worked end-to-end\n        Assert.NotNull(result);\n        Assert.NotNull(result.CustomerDiscount);\n\n        // Verify Stripe Discount → SubscriptionInfo.BillingCustomerDiscount mapping\n        // (verified above, but confirming it made it through)\n\n        // Verify SubscriptionInfo.BillingCustomerDiscount → SubscriptionResponseModel.BillingCustomerDiscount filtering\n        // The filter should pass because:\n        // - includeMilestone2Discount = true (feature flag enabled)\n        // - subscription.CustomerDiscount != null\n        // - subscription.CustomerDiscount.Id == Milestone2SubscriptionDiscount\n        // - subscription.CustomerDiscount.Active = true\n        Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id);\n        Assert.True(result.CustomerDiscount.Active);\n        Assert.Equal(30m, result.CustomerDiscount.PercentOff);\n        Assert.Equal(20.00m, result.CustomerDiscount.AmountOff); // Verify cents-to-dollars conversion\n\n        // Verify AppliesTo products are preserved through the entire pipeline\n        Assert.NotNull(result.CustomerDiscount.AppliesTo);\n        Assert.Equal(3, result.CustomerDiscount.AppliesTo.Count());\n        Assert.Contains(\"prod_premium\", result.CustomerDiscount.AppliesTo);\n        Assert.Contains(\"prod_families\", result.CustomerDiscount.AppliesTo);\n        Assert.Contains(\"prod_teams\", result.CustomerDiscount.AppliesTo);\n\n        // Verify the payment service was called correctly\n        await _paymentService.Received(1).GetSubscriptionAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_IntegrationTest_MultipleDiscountsInSubscription_PrefersCustomerDiscount(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Create Stripe subscription with multiple discounts\n        // Customer discount should be preferred over subscription discounts\n        var customerDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = TestMilestone2CouponId,\n                PercentOff = 30m,\n                AmountOff = null\n            },\n            End = null\n        };\n\n        var subscriptionDiscount1 = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = \"other-coupon-1\",\n                PercentOff = 10m\n            },\n            End = null\n        };\n\n        var subscriptionDiscount2 = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = \"other-coupon-2\",\n                PercentOff = 15m\n            },\n            End = null\n        };\n\n        // Map through SubscriptionInfo.BillingCustomerDiscount\n        var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(customerDiscount);\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = billingCustomerDiscount\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n        user.Gateway = GatewayType.Stripe;\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert - Should use customer discount, not subscription discounts\n        Assert.NotNull(result);\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id);\n        Assert.Equal(30m, result.CustomerDiscount.PercentOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_IntegrationTest_BothPercentOffAndAmountOffPresent_HandlesEdgeCase(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Edge case: Stripe coupon with both PercentOff and AmountOff\n        // This tests the scenario mentioned in BillingCustomerDiscountTests.cs line 212-232\n        var stripeDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = TestMilestone2CouponId,\n                PercentOff = 25m,\n                AmountOff = 2000, // 2000 cents = $20.00\n                AppliesTo = new CouponAppliesTo\n                {\n                    Products = new List<string> { \"prod_premium\" }\n                }\n            },\n            End = null\n        };\n\n        // Map through SubscriptionInfo.BillingCustomerDiscount\n        var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = billingCustomerDiscount\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n        user.Gateway = GatewayType.Stripe;\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert - Both values should be preserved through the pipeline\n        Assert.NotNull(result);\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id);\n        Assert.Equal(25m, result.CustomerDiscount.PercentOff);\n        Assert.Equal(20.00m, result.CustomerDiscount.AmountOff); // Converted from cents\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_IntegrationTest_BillingSubscriptionMapsThroughPipeline(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Create Stripe subscription with subscription details\n        var stripeSubscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            TrialStart = DateTime.UtcNow.AddDays(-30),\n            TrialEnd = DateTime.UtcNow.AddDays(-20),\n            CanceledAt = null,\n            CancelAtPeriodEnd = false,\n            CollectionMethod = \"charge_automatically\"\n        };\n\n        // Map through SubscriptionInfo.BillingSubscription\n        var billingSubscription = new SubscriptionInfo.BillingSubscription(stripeSubscription);\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            Subscription = billingSubscription,\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = TestMilestone2CouponId,\n                Active = true,\n                PercentOff = 20m\n            }\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n        user.Gateway = GatewayType.Stripe;\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert - Verify BillingSubscription mapped through pipeline\n        Assert.NotNull(result);\n        Assert.NotNull(result.Subscription);\n        Assert.Equal(\"active\", result.Subscription.Status);\n        Assert.Equal(14, result.Subscription.GracePeriod); // charge_automatically = 14 days\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_IntegrationTest_BillingUpcomingInvoiceMapsThroughPipeline(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Create Stripe invoice for upcoming invoice\n        var stripeInvoice = new Invoice\n        {\n            AmountDue = 2000, // 2000 cents = $20.00\n            Created = DateTime.UtcNow.AddDays(1)\n        };\n\n        // Map through SubscriptionInfo.BillingUpcomingInvoice\n        var billingUpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice);\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            UpcomingInvoice = billingUpcomingInvoice,\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = TestMilestone2CouponId,\n                Active = true,\n                PercentOff = 20m\n            }\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n        user.Gateway = GatewayType.Stripe;\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert - Verify BillingUpcomingInvoice mapped through pipeline\n        Assert.NotNull(result);\n        Assert.NotNull(result.UpcomingInvoice);\n        Assert.Equal(20.00m, result.UpcomingInvoice.Amount); // Converted from cents\n        Assert.NotNull(result.UpcomingInvoice.Date);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_IntegrationTest_CompletePipelineWithAllComponents(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Complete Stripe objects for full pipeline test\n        var stripeDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = TestMilestone2CouponId,\n                PercentOff = 20m,\n                AmountOff = 1000, // $10.00\n                AppliesTo = new CouponAppliesTo\n                {\n                    Products = new List<string> { \"prod_premium\", \"prod_families\" }\n                }\n            },\n            End = null\n        };\n\n        var stripeSubscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\"\n        };\n\n        var stripeInvoice = new Invoice\n        {\n            AmountDue = 1500, // $15.00\n            Created = DateTime.UtcNow.AddDays(7)\n        };\n\n        // Map through SubscriptionInfo (simulating StripePaymentService)\n        var billingCustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(stripeDiscount);\n        var billingSubscription = new SubscriptionInfo.BillingSubscription(stripeSubscription);\n        var billingUpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice);\n\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = billingCustomerDiscount,\n            Subscription = billingSubscription,\n            UpcomingInvoice = billingUpcomingInvoice\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);\n        _userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n        user.Gateway = GatewayType.Stripe;\n\n        // Act - Full pipeline: Stripe → SubscriptionInfo → SubscriptionResponseModel → API response\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert - Verify all components mapped correctly through the pipeline\n        Assert.NotNull(result);\n\n        // Verify discount\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(TestMilestone2CouponId, result.CustomerDiscount.Id);\n        Assert.Equal(20m, result.CustomerDiscount.PercentOff);\n        Assert.Equal(10.00m, result.CustomerDiscount.AmountOff);\n        Assert.NotNull(result.CustomerDiscount.AppliesTo);\n        Assert.Equal(2, result.CustomerDiscount.AppliesTo.Count());\n\n        // Verify subscription\n        Assert.NotNull(result.Subscription);\n        Assert.Equal(\"active\", result.Subscription.Status);\n        Assert.Equal(14, result.Subscription.GracePeriod);\n\n        // Verify upcoming invoice\n        Assert.NotNull(result.UpcomingInvoice);\n        Assert.Equal(15.00m, result.UpcomingInvoice.Amount);\n        Assert.NotNull(result.UpcomingInvoice.Date);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_SelfHosted_WithDiscountFlagEnabled_NeverIncludesDiscount(User user)\n    {\n        // Arrange - Self-hosted user with discount flag enabled (should still return null)\n        var selfHostedSettings = new GlobalSettings { SelfHosted = true };\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); // Flag enabled\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(selfHostedSettings, _paymentService);\n\n        // Assert - Should never include discount for self-hosted, even with flag enabled\n        Assert.NotNull(result);\n        Assert.Null(result.CustomerDiscount);\n        await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_NullGateway_WithDiscountFlagEnabled_NeverIncludesDiscount(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - User with null gateway and discount flag enabled (should still return null)\n        user.Gateway = null; // No gateway configured\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());\n        _sut.ControllerContext = new ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = claimsPrincipal }\n        };\n        _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        _userService.GenerateLicenseAsync(user).Returns(license);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); // Flag enabled\n\n        // Act\n        var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);\n\n        // Assert - Should never include discount when no gateway, even with flag enabled\n        Assert.NotNull(result);\n        Assert.Null(result.CustomerDiscount);\n        await _paymentService.DidNotReceive().GetSubscriptionAsync(Arg.Any<User>());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs",
    "content": "﻿using Bit.Api.Billing.Controllers;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing NSubstitute;\nusing Xunit;\n\nusing static Bit.Api.Test.Billing.Utilities;\n\nnamespace Bit.Api.Test.Billing.Controllers;\n\n[ControllerCustomize(typeof(OrganizationBillingController))]\n[SutProviderCustomize]\npublic class OrganizationBillingControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task GetHistoryAsync_Unauthorized_ReturnsUnauthorized(\n        Guid organizationId,\n        SutProvider<OrganizationBillingController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ViewBillingHistory(organizationId).Returns(false);\n\n        var result = await sutProvider.Sut.GetHistoryAsync(organizationId);\n\n        AssertUnauthorized(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetHistoryAsync_OrganizationNotFound_ReturnsNotFound(\n        Guid organizationId,\n        SutProvider<OrganizationBillingController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ViewBillingHistory(organizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns((Organization)null);\n\n        var result = await sutProvider.Sut.GetHistoryAsync(organizationId);\n\n        AssertNotFound(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetHistoryAsync_OK(\n        Guid organizationId,\n        Organization organization,\n        SutProvider<OrganizationBillingController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().ViewBillingHistory(organizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(organization);\n\n        // Manually create a BillingHistoryInfo object to avoid requiring AutoFixture to create HttpResponseHeaders\n        var billingInfo = new BillingHistoryInfo();\n\n        sutProvider.GetDependency<IStripePaymentService>().GetBillingHistoryAsync(organization).Returns(billingInfo);\n\n        // Act\n        var result = await sutProvider.Sut.GetHistoryAsync(organizationId);\n\n        // Assert\n        var okResult = Assert.IsType<Ok<BillingHistoryInfo>>(result);\n        Assert.Equal(billingInfo, okResult.Value);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs",
    "content": "﻿using Bit.Api.Billing.Controllers;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Api.Test.Billing.Controllers;\n\n[ControllerCustomize(typeof(OrganizationSponsorshipsController))]\n[SutProviderCustomize]\npublic class OrganizationSponsorshipsControllerTests\n{\n    public static IEnumerable<object[]> EnterprisePlanTypes =>\n        Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p });\n    public static IEnumerable<object[]> NonEnterprisePlanTypes =>\n        Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p });\n    public static IEnumerable<object[]> NonFamiliesPlanTypes =>\n        Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p });\n\n    public static IEnumerable<object[]> NonConfirmedOrganizationUsersStatuses =>\n        Enum.GetValues<OrganizationUserStatusType>()\n            .Where(s => s != OrganizationUserStatusType.Confirmed)\n            .Select(s => new object[] { s });\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task RedeemSponsorship_BadToken_ThrowsBadRequest(string sponsorshipToken, User user,\n        OrganizationSponsorshipRedeemRequestModel model, SutProvider<OrganizationSponsorshipsController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)\n            .Returns(user);\n        sutProvider.GetDependency<IValidateRedemptionTokenCommand>().ValidateRedemptionTokenAsync(sponsorshipToken,\n            user.Email).Returns((false, null));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model));\n\n        Assert.Contains(\"Failed to parse sponsorship token.\", exception.Message);\n        await sutProvider.GetDependency<ISetUpSponsorshipCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .SetUpSponsorshipAsync(default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RedeemSponsorship_NotSponsoredOrgOwner_ThrowsBadRequest(string sponsorshipToken, User user,\n        OrganizationSponsorship sponsorship, OrganizationSponsorshipRedeemRequestModel model,\n        SutProvider<OrganizationSponsorshipsController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)\n            .Returns(user);\n        sutProvider.GetDependency<IValidateRedemptionTokenCommand>().ValidateRedemptionTokenAsync(sponsorshipToken,\n            user.Email).Returns((true, sponsorship));\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.SponsoredOrganizationId).Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model));\n\n        Assert.Contains(\"Can only redeem sponsorship for an organization you own.\", exception.Message);\n        await sutProvider.GetDependency<ISetUpSponsorshipCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .SetUpSponsorshipAsync(default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RedeemSponsorship_NotSponsoredOrgOwner_Success(string sponsorshipToken, User user,\n        OrganizationSponsorship sponsorship, Organization sponsoringOrganization,\n        OrganizationSponsorshipRedeemRequestModel model,\n        [Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] PolicyStatus policy,\n        SutProvider<OrganizationSponsorshipsController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)\n            .Returns(user);\n        sutProvider.GetDependency<IValidateRedemptionTokenCommand>().ValidateRedemptionTokenAsync(sponsorshipToken,\n            user.Email).Returns((true, sponsorship));\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.SponsoredOrganizationId).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(model.SponsoredOrganizationId).Returns(sponsoringOrganization);\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), PolicyType.FreeFamiliesSponsorshipPolicy)\n            .Returns(policy);\n\n        await sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model);\n\n        await sutProvider.GetDependency<ISetUpSponsorshipCommand>().Received(1)\n            .SetUpSponsorshipAsync(sponsorship, sponsoringOrganization);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PreValidateSponsorshipToken_ValidatesToken_Success(string sponsorshipToken, User user,\n        OrganizationSponsorship sponsorship,\n        [Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] PolicyStatus policy,\n        SutProvider<OrganizationSponsorshipsController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)\n            .Returns(user);\n        sutProvider.GetDependency<IValidateRedemptionTokenCommand>()\n            .ValidateRedemptionTokenAsync(sponsorshipToken, user.Email).Returns((true, sponsorship));\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), PolicyType.FreeFamiliesSponsorshipPolicy)\n            .Returns(policy);\n        await sutProvider.Sut.PreValidateSponsorshipToken(sponsorshipToken);\n\n        await sutProvider.GetDependency<IValidateRedemptionTokenCommand>().Received(1)\n            .ValidateRedemptionTokenAsync(sponsorshipToken, user.Email);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeSponsorship_WrongSponsoringUser_ThrowsBadRequest(OrganizationUser sponsoringOrgUser,\n        Guid currentUserId, SutProvider<OrganizationSponsorshipsController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(sponsoringOrgUser.Id)\n            .Returns(sponsoringOrgUser);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id));\n\n        Assert.Contains(\"Can only revoke a sponsorship you granted.\", exception.Message);\n        await sutProvider.GetDependency<IRemoveSponsorshipCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .RemoveSponsorshipAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RemoveSponsorship_WrongOrgUserType_ThrowsBadRequest(Organization sponsoredOrg,\n        SutProvider<OrganizationSponsorshipsController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(Arg.Any<Guid>()).Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RemoveSponsorship(sponsoredOrg.Id));\n\n        Assert.Contains(\"Only the owner of an organization can remove sponsorship.\", exception.Message);\n        await sutProvider.GetDependency<IRemoveSponsorshipCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .RemoveSponsorshipAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSponsoredOrganizations_OrganizationNotFound_ThrowsNotFound(\n        Guid sponsoringOrgId,\n        SutProvider<OrganizationSponsorshipsController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrgId).ReturnsNull();\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrgId));\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .GetManyBySponsoringOrganizationAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSponsoredOrganizations_NotOrganizationOwner_ThrowsNotFound(\n        Organization sponsoringOrg,\n        SutProvider<OrganizationSponsorshipsController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(sponsoringOrg.Id).Returns(false);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(sponsoringOrg.Id).Returns(false);\n\n        // Create a CurrentContextOrganization with ManageUsers set to false\n        var currentContextOrg = new CurrentContextOrganization\n        {\n            Id = sponsoringOrg.Id,\n            Permissions = new Permissions { ManageUsers = false }\n        };\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(new List<CurrentContextOrganization> { currentContextOrg });\n\n        // Act & Assert\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>\n            sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id));\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .GetManyBySponsoringOrganizationAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSponsoredOrganizations_Success_ReturnsSponsorships(\n        Organization sponsoringOrg,\n        List<OrganizationSponsorship> sponsorships,\n        SutProvider<OrganizationSponsorshipsController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(sponsoringOrg.Id).Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(sponsoringOrg.Id).Returns(false);\n\n        // Create a CurrentContextOrganization from the sponsoringOrg\n        var currentContextOrg = new CurrentContextOrganization\n        {\n            Id = sponsoringOrg.Id,\n            Permissions = new Permissions { ManageUsers = true }\n        };\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(new List<CurrentContextOrganization> { currentContextOrg });\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id).Returns(sponsorships);\n\n        // Set IsAdminInitiated to true for all test sponsorships\n        foreach (var sponsorship in sponsorships)\n        {\n            sponsorship.IsAdminInitiated = true;\n        }\n\n        // Act\n        var result = await sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id);\n\n        // Assert\n        Assert.Equal(sponsorships.Count, result.Data.Count());\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)\n            .GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing AutoFixture.Xunit2;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.Billing.Controllers;\nusing Bit.Api.Models.Request.Organizations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Billing.Organizations.Repositories;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Api.Test.Billing.Controllers;\n\npublic class OrganizationsControllerTests : IDisposable\n{\n    private readonly GlobalSettings _globalSettings;\n    private readonly ICurrentContext _currentContext;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationService _organizationService;\n    private readonly IOrganizationUserRepository _organizationUserRepository;\n    private readonly IStripePaymentService _paymentService;\n    private readonly ISsoConfigRepository _ssoConfigRepository;\n    private readonly IUserService _userService;\n    private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery;\n    private readonly ILicensingService _licensingService;\n    private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;\n    private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand;\n    private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;\n    private readonly ISubscriberService _subscriberService;\n    private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;\n    private readonly IOrganizationInstallationRepository _organizationInstallationRepository;\n    private readonly IPricingClient _pricingClient;\n\n    private readonly OrganizationsController _sut;\n\n    public OrganizationsControllerTests()\n    {\n        _currentContext = Substitute.For<ICurrentContext>();\n        _globalSettings = Substitute.For<GlobalSettings>();\n        _organizationRepository = Substitute.For<IOrganizationRepository>();\n        _organizationService = Substitute.For<IOrganizationService>();\n        _organizationUserRepository = Substitute.For<IOrganizationUserRepository>();\n        _paymentService = Substitute.For<IStripePaymentService>();\n        Substitute.For<IPolicyRepository>();\n        _ssoConfigRepository = Substitute.For<ISsoConfigRepository>();\n        Substitute.For<ISsoConfigService>();\n        _userService = Substitute.For<IUserService>();\n        _getCloudOrganizationLicenseQuery = Substitute.For<IGetCloudOrganizationLicenseQuery>();\n        _licensingService = Substitute.For<ILicensingService>();\n        _updateSecretsManagerSubscriptionCommand = Substitute.For<IUpdateSecretsManagerSubscriptionCommand>();\n        _upgradeOrganizationPlanCommand = Substitute.For<IUpgradeOrganizationPlanCommand>();\n        _addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();\n        _subscriberService = Substitute.For<ISubscriberService>();\n        _removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();\n        _organizationInstallationRepository = Substitute.For<IOrganizationInstallationRepository>();\n        _pricingClient = Substitute.For<IPricingClient>();\n\n        _sut = new OrganizationsController(\n            _organizationRepository,\n            _organizationUserRepository,\n            _organizationService,\n            _userService,\n            _paymentService,\n            _currentContext,\n            _getCloudOrganizationLicenseQuery,\n            _globalSettings,\n            _licensingService,\n            _updateSecretsManagerSubscriptionCommand,\n            _upgradeOrganizationPlanCommand,\n            _addSecretsManagerSubscriptionCommand,\n            _subscriberService,\n            _organizationInstallationRepository,\n            _pricingClient);\n    }\n\n    public void Dispose()\n    {\n        _sut?.Dispose();\n    }\n\n    [Theory, AutoData]\n    public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException(\n        Guid organizationId,\n        OrganizationUpgradeRequestModel model)\n    {\n        _currentContext.EditSubscription(organizationId).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostUpgrade(organizationId, model));\n    }\n\n    [Theory, AutoData]\n    public async Task OrganizationsController_PostUpgrade_NonSMUpgrade_ReturnsCorrectResponse(\n        Guid organizationId,\n        OrganizationUpgradeRequestModel model,\n        bool success,\n        string paymentIntentClientSecret)\n    {\n        model.UseSecretsManager = false;\n\n        _currentContext.EditSubscription(organizationId).Returns(true);\n\n        _upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>(), Arg.Any<Guid?>())\n            .Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));\n\n        var response = await _sut.PostUpgrade(organizationId, model);\n\n        Assert.Equal(success, response.Success);\n        Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);\n    }\n\n    [Theory, AutoData]\n    public async Task OrganizationsController_PostUpgrade_SMUpgrade_ProvidesAccess_ReturnsCorrectResponse(\n        Guid organizationId,\n        Guid userId,\n        OrganizationUpgradeRequestModel model,\n        bool success,\n        string paymentIntentClientSecret,\n        OrganizationUser organizationUser)\n    {\n        model.UseSecretsManager = true;\n        organizationUser.AccessSecretsManager = false;\n\n        _currentContext.EditSubscription(organizationId).Returns(true);\n\n        _upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>(), Arg.Any<Guid?>())\n            .Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));\n\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n\n        _organizationUserRepository.GetByOrganizationAsync(organizationId, userId).Returns(organizationUser);\n\n        var response = await _sut.PostUpgrade(organizationId, model);\n\n        Assert.Equal(success, response.Success);\n        Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);\n\n        await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>\n            orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));\n    }\n\n    [Theory, AutoData]\n    public async Task OrganizationsController_PostUpgrade_SMUpgrade_NullOrgUser_ReturnsCorrectResponse(\n        Guid organizationId,\n        Guid userId,\n        OrganizationUpgradeRequestModel model,\n        bool success,\n        string paymentIntentClientSecret)\n    {\n        model.UseSecretsManager = true;\n\n        _currentContext.EditSubscription(organizationId).Returns(true);\n\n        _upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any<OrganizationUpgrade>(), Arg.Any<Guid?>())\n            .Returns(new Tuple<bool, string>(success, paymentIntentClientSecret));\n\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n\n        _organizationUserRepository.GetByOrganizationAsync(organizationId, userId).ReturnsNull();\n\n        var response = await _sut.PostUpgrade(organizationId, model);\n\n        Assert.Equal(success, response.Success);\n        Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret);\n\n        await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());\n    }\n\n    [Theory, AutoData]\n    public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrg_ThrowsNotFoundException(\n        Guid organizationId,\n        SecretsManagerSubscribeRequestModel model)\n    {\n        _organizationRepository.GetByIdAsync(organizationId).ReturnsNull();\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));\n    }\n\n    [Theory, AutoData]\n    public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_UserCannotEditSubscription_ThrowsNotFoundException(\n        Guid organizationId,\n        SecretsManagerSubscribeRequestModel model,\n        Organization organization)\n    {\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        _currentContext.EditSubscription(organizationId).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model));\n    }\n\n    [Theory, AutoData]\n    public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_ProvidesAccess_ReturnsCorrectResponse(\n        Guid organizationId,\n        SecretsManagerSubscribeRequestModel model,\n        Organization organization,\n        Guid userId,\n        OrganizationUser organizationUser,\n        OrganizationUserOrganizationDetails organizationUserOrganizationDetails)\n    {\n        organizationUser.AccessSecretsManager = false;\n\n        var ssoConfigurationData = new SsoConfigurationData\n        {\n            MemberDecryptionType = MemberDecryptionType.KeyConnector,\n            KeyConnectorUrl = \"https://example.com\"\n        };\n\n        organizationUserOrganizationDetails.Permissions = string.Empty;\n        organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();\n\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        _currentContext.EditSubscription(organizationId).Returns(true);\n\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n\n        _organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).Returns(organizationUser);\n\n        _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)\n            .Returns(organizationUserOrganizationDetails);\n\n        var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);\n\n        Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);\n        Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);\n\n        await _addSecretsManagerSubscriptionCommand.Received(1)\n            .SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);\n        await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is<OrganizationUser>(orgUser =>\n            orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true));\n    }\n\n    [Theory, AutoData]\n    public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrgUser_ReturnsCorrectResponse(\n        Guid organizationId,\n        SecretsManagerSubscribeRequestModel model,\n        Organization organization,\n        Guid userId,\n        OrganizationUserOrganizationDetails organizationUserOrganizationDetails)\n    {\n        var ssoConfigurationData = new SsoConfigurationData\n        {\n            MemberDecryptionType = MemberDecryptionType.KeyConnector,\n            KeyConnectorUrl = \"https://example.com\"\n        };\n\n        organizationUserOrganizationDetails.Permissions = string.Empty;\n        organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize();\n\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        _currentContext.EditSubscription(organizationId).Returns(true);\n\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n\n        _organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).ReturnsNull();\n\n        _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed)\n            .Returns(organizationUserOrganizationDetails);\n\n        var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model);\n\n        Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId);\n        Assert.Equal(response.Name, organizationUserOrganizationDetails.Name);\n\n        await _addSecretsManagerSubscriptionCommand.Received(1)\n            .SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts);\n        await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<OrganizationUser>());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs",
    "content": "﻿using Bit.Api.Billing.Controllers;\nusing Bit.Api.Billing.Models.Responses;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Billing.Providers.Services;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Models.BitStripe;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\n\nusing static Bit.Api.Test.Billing.Utilities;\n\nnamespace Bit.Api.Test.Billing.Controllers;\n\n[ControllerCustomize(typeof(ProviderBillingController))]\n[SutProviderCustomize]\npublic class ProviderBillingControllerTests\n{\n    #region GetInvoicesAsync & TryGetBillableProviderForAdminOperations\n\n    [Theory, BitAutoData]\n    public async Task GetInvoicesAsync_NullProvider_NotFound(\n        Guid providerId,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId).ReturnsNull();\n\n        var result = await sutProvider.Sut.GetInvoicesAsync(providerId);\n\n        AssertNotFound(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetInvoicesAsync_NotProviderUser_Unauthorized(\n        Provider provider,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)\n            .Returns(false);\n\n        var result = await sutProvider.Sut.GetInvoicesAsync(provider.Id);\n\n        AssertUnauthorized(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetInvoicesAsync_ProviderNotBillable_Unauthorized(\n        Provider provider,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        provider.Type = ProviderType.Reseller;\n        provider.Status = ProviderStatusType.Created;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)\n            .Returns(true);\n\n        var result = await sutProvider.Sut.GetInvoicesAsync(provider.Id);\n\n        AssertUnauthorized(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetInvoices_Ok(\n        Provider provider,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        ConfigureStableProviderAdminInputs(provider, sutProvider);\n\n        var invoices = new List<Invoice>\n        {\n            new ()\n            {\n                Id = \"3\",\n                Created = new DateTime(2024, 7, 1),\n                Status = \"draft\",\n                Total = 100000,\n                HostedInvoiceUrl = \"https://example.com/invoice/3\",\n                InvoicePdf = \"https://example.com/invoice/3/pdf\"\n            },\n            new ()\n            {\n                Id = \"2\",\n                Created = new DateTime(2024, 6, 1),\n                Number = \"B\",\n                Status = \"open\",\n                Total = 100000,\n                DueDate = new DateTime(2024, 7, 1),\n                HostedInvoiceUrl = \"https://example.com/invoice/2\",\n                InvoicePdf = \"https://example.com/invoice/2/pdf\"\n            },\n            new ()\n            {\n                Id = \"1\",\n                Created = new DateTime(2024, 5, 1),\n                Number = \"A\",\n                Status = \"paid\",\n                Total = 100000,\n                DueDate = new DateTime(2024, 6, 1),\n                HostedInvoiceUrl = \"https://example.com/invoice/1\",\n                InvoicePdf = \"https://example.com/invoice/1/pdf\"\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>().ListInvoicesAsync(Arg.Is<StripeInvoiceListOptions>(\n            options =>\n                options.Customer == provider.GatewayCustomerId)).Returns(invoices);\n\n        var result = await sutProvider.Sut.GetInvoicesAsync(provider.Id);\n\n        Assert.IsType<Ok<InvoicesResponse>>(result);\n\n        var response = ((Ok<InvoicesResponse>)result).Value;\n\n        Assert.Equal(2, response.Invoices.Count);\n\n        var openInvoice = response.Invoices.FirstOrDefault(i => i.Status == \"open\");\n\n        Assert.NotNull(openInvoice);\n        Assert.Equal(\"2\", openInvoice.Id);\n        Assert.Equal(new DateTime(2024, 6, 1), openInvoice.Date);\n        Assert.Equal(\"B\", openInvoice.Number);\n        Assert.Equal(1000, openInvoice.Total);\n        Assert.Equal(new DateTime(2024, 7, 1), openInvoice.DueDate);\n        Assert.Equal(\"https://example.com/invoice/2\", openInvoice.Url);\n\n        var paidInvoice = response.Invoices.FirstOrDefault(i => i.Status == \"paid\");\n\n        Assert.NotNull(paidInvoice);\n        Assert.Equal(\"1\", paidInvoice.Id);\n        Assert.Equal(new DateTime(2024, 5, 1), paidInvoice.Date);\n        Assert.Equal(\"A\", paidInvoice.Number);\n        Assert.Equal(1000, paidInvoice.Total);\n        Assert.Equal(new DateTime(2024, 6, 1), paidInvoice.DueDate);\n        Assert.Equal(\"https://example.com/invoice/1\", paidInvoice.Url);\n    }\n\n    #endregion\n\n    #region GenerateClientInvoiceReportAsync\n\n    [Theory, BitAutoData]\n    public async Task GenerateClientInvoiceReportAsync_NullReportContent_ServerError(\n        Provider provider,\n        string invoiceId,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        ConfigureStableProviderAdminInputs(provider, sutProvider);\n\n        sutProvider.GetDependency<IProviderBillingService>().GenerateClientInvoiceReport(invoiceId)\n            .ReturnsNull();\n\n        var result = await sutProvider.Sut.GenerateClientInvoiceReportAsync(provider.Id, invoiceId);\n\n        Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);\n\n        var response = (JsonHttpResult<ErrorResponseModel>)result;\n\n        Assert.Equal(StatusCodes.Status500InternalServerError, response.StatusCode);\n        Assert.Equal(\"We had a problem generating your invoice CSV. Please contact support.\", response.Value.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GenerateClientInvoiceReportAsync_Ok(\n        Provider provider,\n        string invoiceId,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        ConfigureStableProviderAdminInputs(provider, sutProvider);\n\n        var reportContent = \"Report\"u8.ToArray();\n\n        sutProvider.GetDependency<IProviderBillingService>().GenerateClientInvoiceReport(invoiceId)\n            .Returns(reportContent);\n\n        var result = await sutProvider.Sut.GenerateClientInvoiceReportAsync(provider.Id, invoiceId);\n\n        Assert.IsType<FileContentHttpResult>(result);\n\n        var response = (FileContentHttpResult)result;\n\n        Assert.Equal(\"text/csv\", response.ContentType);\n        Assert.Equal(reportContent, response.FileContents);\n    }\n\n    #endregion\n\n    #region GetSubscriptionAsync & TryGetBillableProviderForServiceUserOperation\n\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionAsync_NullProvider_NotFound(\n        Guid providerId,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId).ReturnsNull();\n\n        var result = await sutProvider.Sut.GetSubscriptionAsync(providerId);\n\n        AssertNotFound(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionAsync_NotProviderUser_Unauthorized(\n        Provider provider,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderUser(provider.Id)\n            .Returns(false);\n\n        var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);\n\n        AssertUnauthorized(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionAsync_ProviderNotBillable_Unauthorized(\n        Provider provider,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        provider.Type = ProviderType.Reseller;\n        provider.Status = ProviderStatusType.Created;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderUser(provider.Id)\n            .Returns(true);\n\n        var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);\n\n        AssertUnauthorized(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionAsync_Ok(\n        Provider provider,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        ConfigureStableProviderServiceUserInputs(provider, sutProvider);\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        var now = DateTime.UtcNow;\n        var oneMonthAgo = now.AddMonths(-1);\n\n        var daysInThisMonth = DateTime.DaysInMonth(now.Year, now.Month);\n\n        var subscription = new Subscription\n        {\n            CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,\n            Customer = new Customer\n            {\n                Address = new Address\n                {\n                    Country = \"US\",\n                    PostalCode = \"12345\",\n                    Line1 = \"123 Example St.\",\n                    Line2 = \"Unit 1\",\n                    City = \"Example Town\",\n                    State = \"NY\"\n                },\n                Balance = -100000,\n                Discount = new Discount { Coupon = new Coupon { PercentOff = 10 } },\n                TaxIds = new StripeList<TaxId> { Data = [new TaxId { Value = \"123456789\" }] }\n            },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),\n                        Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }\n                    },\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),\n                        Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams }\n                    }\n                ]\n            },\n            Status = \"unpaid\"\n        };\n\n        stripeAdapter.GetSubscriptionAsync(provider.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(\n            options =>\n                options.Expand.Contains(\"customer.tax_ids\") &&\n                options.Expand.Contains(\"discounts\") &&\n                options.Expand.Contains(\"test_clock\"))).Returns(subscription);\n\n        var daysInLastMonth = DateTime.DaysInMonth(oneMonthAgo.Year, oneMonthAgo.Month);\n\n        var overdueInvoice = new Invoice\n        {\n            Id = \"invoice_id\",\n            Status = \"open\",\n            Created = new DateTime(oneMonthAgo.Year, oneMonthAgo.Month, 1),\n            PeriodEnd = new DateTime(oneMonthAgo.Year, oneMonthAgo.Month, daysInLastMonth),\n            Attempted = true\n        };\n\n        stripeAdapter.SearchInvoiceAsync(Arg.Is<InvoiceSearchOptions>(\n                options => options.Query == $\"subscription:'{subscription.Id}' status:'open'\"))\n            .Returns([overdueInvoice]);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new ()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.TeamsMonthly,\n                SeatMinimum = 50,\n                PurchasedSeats = 10,\n                AllocatedSeats = 60\n            },\n            new ()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.EnterpriseMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 90\n            }\n        };\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);\n\n        foreach (var providerPlan in providerPlans)\n        {\n            var plan = MockPlans.Get(providerPlan.PlanType);\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);\n            var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);\n            sutProvider.GetDependency<IStripeAdapter>().GetPriceAsync(priceId)\n                .Returns(new Price\n                {\n                    UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100\n                });\n        }\n\n        var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);\n\n        Assert.IsType<Ok<ProviderSubscriptionResponse>>(result);\n\n        var response = ((Ok<ProviderSubscriptionResponse>)result).Value;\n\n        Assert.Equal(subscription.Status, response.Status);\n        Assert.Equal(subscription.GetCurrentPeriodEnd(), response.CurrentPeriodEndDate);\n        Assert.Equal(subscription.Customer!.Discount!.Coupon!.PercentOff, response.DiscountPercentage);\n        Assert.Equal(subscription.CollectionMethod, response.CollectionMethod);\n\n        var teamsPlan = MockPlans.Get(PlanType.TeamsMonthly);\n        var providerTeamsPlan = response.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name);\n        Assert.NotNull(providerTeamsPlan);\n        Assert.Equal(50, providerTeamsPlan.SeatMinimum);\n        Assert.Equal(10, providerTeamsPlan.PurchasedSeats);\n        Assert.Equal(60, providerTeamsPlan.AssignedSeats);\n        Assert.Equal(60 * teamsPlan.PasswordManager.ProviderPortalSeatPrice, providerTeamsPlan.Cost);\n        Assert.Equal(\"Monthly\", providerTeamsPlan.Cadence);\n\n        var enterprisePlan = MockPlans.Get(PlanType.EnterpriseMonthly);\n        var providerEnterprisePlan = response.Plans.FirstOrDefault(plan => plan.PlanName == enterprisePlan.Name);\n        Assert.NotNull(providerEnterprisePlan);\n        Assert.Equal(100, providerEnterprisePlan.SeatMinimum);\n        Assert.Equal(0, providerEnterprisePlan.PurchasedSeats);\n        Assert.Equal(90, providerEnterprisePlan.AssignedSeats);\n        Assert.Equal(100 * enterprisePlan.PasswordManager.ProviderPortalSeatPrice, providerEnterprisePlan.Cost);\n        Assert.Equal(\"Monthly\", providerEnterprisePlan.Cadence);\n\n        Assert.Equal(1000.00M, response.AccountCredit);\n\n        var customer = subscription.Customer;\n        Assert.Equal(customer.Address.Country, response.TaxInformation.Country);\n        Assert.Equal(customer.Address.PostalCode, response.TaxInformation.PostalCode);\n        Assert.Equal(customer.TaxIds.First().Value, response.TaxInformation.TaxId);\n        Assert.Equal(customer.Address.Line1, response.TaxInformation.Line1);\n        Assert.Equal(customer.Address.Line2, response.TaxInformation.Line2);\n        Assert.Equal(customer.Address.City, response.TaxInformation.City);\n        Assert.Equal(customer.Address.State, response.TaxInformation.State);\n\n        Assert.Null(response.CancelAt);\n\n        Assert.Equal(overdueInvoice.Created.AddDays(14), response.Suspension.SuspensionDate);\n        Assert.Equal(overdueInvoice.PeriodEnd, response.Suspension.UnpaidPeriodEndDate);\n        Assert.Equal(14, response.Suspension.GracePeriod);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionAsync_SubscriptionLevelDiscount_Ok(\n        Provider provider,\n        SutProvider<ProviderBillingController> sutProvider)\n    {\n        ConfigureStableProviderServiceUserInputs(provider, sutProvider);\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        var now = DateTime.UtcNow;\n        var oneMonthAgo = now.AddMonths(-1);\n\n        var daysInThisMonth = DateTime.DaysInMonth(now.Year, now.Month);\n\n        var subscription = new Subscription\n        {\n            CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,\n            Customer = new Customer\n            {\n                Address = new Address\n                {\n                    Country = \"US\",\n                    PostalCode = \"12345\",\n                    Line1 = \"123 Example St.\",\n                    Line2 = \"Unit 1\",\n                    City = \"Example Town\",\n                    State = \"NY\"\n                },\n                Balance = -100000,\n                Discount = null, // No customer-level discount\n                TaxIds = new StripeList<TaxId> { Data = [new TaxId { Value = \"123456789\" }] }\n            },\n            Discounts =\n            [\n                new Discount { Coupon = new Coupon { PercentOff = 15 } } // Subscription-level discount\n            ],\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),\n                        Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise }\n                    },\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth),\n                        Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams }\n                    }\n                ]\n            },\n            Status = \"active\"\n        };\n\n        stripeAdapter.GetSubscriptionAsync(provider.GatewaySubscriptionId, Arg.Is<SubscriptionGetOptions>(\n            options =>\n                options.Expand.Contains(\"customer.tax_ids\") &&\n                options.Expand.Contains(\"discounts\") &&\n                options.Expand.Contains(\"test_clock\"))).Returns(subscription);\n\n        stripeAdapter.SearchInvoiceAsync(Arg.Is<InvoiceSearchOptions>(\n                options => options.Query == $\"subscription:'{subscription.Id}' status:'open'\"))\n            .Returns([]);\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new ()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.TeamsMonthly,\n                SeatMinimum = 50,\n                PurchasedSeats = 10,\n                AllocatedSeats = 60\n            },\n            new ()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = provider.Id,\n                PlanType = PlanType.EnterpriseMonthly,\n                SeatMinimum = 100,\n                PurchasedSeats = 0,\n                AllocatedSeats = 90\n            }\n        };\n\n        sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);\n\n        foreach (var providerPlan in providerPlans)\n        {\n            var plan = MockPlans.Get(providerPlan.PlanType);\n            sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(providerPlan.PlanType).Returns(plan);\n            var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType);\n            sutProvider.GetDependency<IStripeAdapter>().GetPriceAsync(priceId)\n                .Returns(new Price\n                {\n                    UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100\n                });\n        }\n\n        var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);\n\n        Assert.IsType<Ok<ProviderSubscriptionResponse>>(result);\n\n        var response = ((Ok<ProviderSubscriptionResponse>)result).Value;\n\n        Assert.Equal(subscription.Status, response.Status);\n        Assert.Equal(subscription.GetCurrentPeriodEnd(), response.CurrentPeriodEndDate);\n        Assert.Equal(15, response.DiscountPercentage); // Verify subscription-level discount is used\n        Assert.Equal(subscription.CollectionMethod, response.CollectionMethod);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Controllers/VNext/AccountBillingVNextControllerTests.cs",
    "content": "﻿using Bit.Api.Billing.Controllers.VNext;\nusing Bit.Api.Billing.Models.Requests.Storage;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Licenses.Queries;\nusing Bit.Core.Billing.Models.Api.Response;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Portal.Commands;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Billing.Subscriptions.Commands;\nusing Bit.Core.Billing.Subscriptions.Queries;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing NSubstitute;\nusing OneOf.Types;\nusing Stripe;\nusing Xunit;\nusing BadRequest = Bit.Core.Billing.Commands.BadRequest;\nusing Conflict = Bit.Core.Billing.Commands.Conflict;\nusing NotFound = Microsoft.AspNetCore.Http.HttpResults.NotFound;\n\nnamespace Bit.Api.Test.Billing.Controllers.VNext;\n\npublic class AccountBillingVNextControllerTests\n{\n    private readonly IUpdatePremiumStorageCommand _updatePremiumStorageCommand;\n    private readonly IGetUserLicenseQuery _getUserLicenseQuery;\n    private readonly IUpgradePremiumToOrganizationCommand _upgradePremiumToOrganizationCommand;\n    private readonly IGetApplicableDiscountsQuery _getApplicableDiscountsQuery;\n    private readonly ICreateBillingPortalSessionCommand _createBillingPortalSessionCommand;\n    private readonly ICurrentContext _currentContext;\n    private readonly AccountBillingVNextController _sut;\n\n    public AccountBillingVNextControllerTests()\n    {\n        _updatePremiumStorageCommand = Substitute.For<IUpdatePremiumStorageCommand>();\n        _getUserLicenseQuery = Substitute.For<IGetUserLicenseQuery>();\n        _upgradePremiumToOrganizationCommand = Substitute.For<IUpgradePremiumToOrganizationCommand>();\n        _getApplicableDiscountsQuery = Substitute.For<IGetApplicableDiscountsQuery>();\n        _createBillingPortalSessionCommand = Substitute.For<ICreateBillingPortalSessionCommand>();\n        _currentContext = Substitute.For<ICurrentContext>();\n\n        _sut = new AccountBillingVNextController(\n            _createBillingPortalSessionCommand,\n            Substitute.For<Core.Billing.Payment.Commands.ICreateBitPayInvoiceForCreditCommand>(),\n            Substitute.For<Core.Billing.Premium.Commands.ICreatePremiumCloudHostedSubscriptionCommand>(),\n            _currentContext,\n            _getApplicableDiscountsQuery,\n            Substitute.For<IGetBitwardenSubscriptionQuery>(),\n            Substitute.For<Core.Billing.Payment.Queries.IGetCreditQuery>(),\n            Substitute.For<Core.Billing.Payment.Queries.IGetPaymentMethodQuery>(),\n            _getUserLicenseQuery,\n            Substitute.For<IReinstateSubscriptionCommand>(),\n            Substitute.For<Core.Billing.Payment.Commands.IUpdatePaymentMethodCommand>(),\n            _updatePremiumStorageCommand,\n            _upgradePremiumToOrganizationCommand);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetLicenseAsync_ValidUser_ReturnsLicenseResponse(\n        User user,\n        UserLicense license)\n    {\n        // Arrange\n        _getUserLicenseQuery.Run(user).Returns(license);\n        // Act\n        var result = await _sut.GetLicenseAsync(user);\n        // Assert\n        var okResult = Assert.IsAssignableFrom<IResult>(result);\n        await _getUserLicenseQuery.Received(1).Run(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_Success_ReturnsOk(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 10 };\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 10))\n            .Returns(new BillingCommandResult<None>(new None()));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var okResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 10);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_UserNotPremium_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 10 };\n        var errorMessage = \"User does not have a premium subscription.\";\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 10))\n            .Returns(new BadRequest(errorMessage));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var badRequestResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 10);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_NoPaymentMethod_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 10 };\n        var errorMessage = \"No payment method found.\";\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 10))\n            .Returns(new BadRequest(errorMessage));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var badRequestResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 10);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_StorageLessThanBase_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 1 };\n        var errorMessage = \"Storage cannot be less than the base amount of 1 GB.\";\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 1))\n            .Returns(new BadRequest(errorMessage));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var badRequestResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 1);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_StorageExceedsMaximum_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 100 };\n        var errorMessage = \"Maximum storage is 100 GB.\";\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 100))\n            .Returns(new BadRequest(errorMessage));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var badRequestResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 100);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_StorageExceedsCurrentUsage_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 2 };\n        var errorMessage = \"You are currently using 5.00 GB of storage. Delete some stored data first.\";\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 2))\n            .Returns(new BadRequest(errorMessage));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var badRequestResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 2);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_IncreaseStorage_Success(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 15 };\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 15))\n            .Returns(new BillingCommandResult<None>(new None()));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var okResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 15);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_DecreaseStorage_Success(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 3 };\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 3))\n            .Returns(new BillingCommandResult<None>(new None()));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var okResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 3);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_MaximumStorage_Success(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 100 };\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 100))\n            .Returns(new BillingCommandResult<None>(new None()));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var okResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 100);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateStorageAsync_NullPaymentSecret_Success(User user)\n    {\n        // Arrange\n        var request = new StorageUpdateRequest { AdditionalStorageGb = 5 };\n\n        _updatePremiumStorageCommand.Run(\n            Arg.Is<User>(u => u.Id == user.Id),\n            Arg.Is<short>(s => s == 5))\n            .Returns(new BillingCommandResult<None>(new None()));\n\n        // Act\n        var result = await _sut.UpdateSubscriptionStorageAsync(user, request);\n\n        // Assert\n        var okResult = Assert.IsAssignableFrom<IResult>(result);\n        await _updatePremiumStorageCommand.Received(1).Run(user, 5);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetApplicableDiscountsAsync_NoEligibleDiscounts_ReturnsOkWithEmptyArray(User user)\n    {\n        // Arrange\n        _getApplicableDiscountsQuery.Run(user)\n            .Returns(Array.Empty<SubscriptionDiscountResponseModel>());\n\n        // Act\n        var result = await _sut.GetApplicableDiscountsAsync(user);\n\n        // Assert\n        var okResult = Assert.IsType<Ok<SubscriptionDiscountResponseModel[]>>(result);\n        Assert.Empty(okResult.Value!);\n        await _getApplicableDiscountsQuery.Received(1).Run(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetApplicableDiscountsAsync_EligibleDiscounts_ReturnsOkWithDiscounts(\n        User user,\n        SubscriptionDiscountResponseModel firstModel,\n        SubscriptionDiscountResponseModel secondModel)\n    {\n        // Arrange\n        var models = new[] { firstModel, secondModel };\n        _getApplicableDiscountsQuery.Run(user).Returns(models);\n\n        // Act\n        var result = await _sut.GetApplicableDiscountsAsync(user);\n\n        // Assert\n        var okResult = Assert.IsType<Ok<SubscriptionDiscountResponseModel[]>>(result);\n        Assert.Equal(models, okResult.Value);\n        await _getApplicableDiscountsQuery.Received(1).Run(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreatePortalSessionAsync_Success_ReturnsPortalUrlAsync(User user)\n    {\n        // Arrange\n        var portalUrl = \"https://billing.stripe.com/session/test123\";\n        var expectedReturnUrl = \"bitwarden://premium-upgrade-callback\";\n\n        _currentContext.DeviceType.Returns(DeviceType.Android);\n        _createBillingPortalSessionCommand.Run(user, expectedReturnUrl)\n            .Returns(new BillingCommandResult<string>(portalUrl));\n\n        // Act\n        var result = await _sut.CreatePortalSessionAsync(user);\n\n        // Assert\n        Assert.IsAssignableFrom<IResult>(result);\n        await _createBillingPortalSessionCommand.Received(1).Run(user, expectedReturnUrl);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreatePortalSessionAsync_NoCustomerId_ReturnsConflictAsync(User user)\n    {\n        // Arrange\n        var expectedReturnUrl = \"bitwarden://premium-upgrade-callback\";\n\n        _currentContext.DeviceType.Returns(DeviceType.AndroidAmazon);\n        _createBillingPortalSessionCommand.Run(user, expectedReturnUrl)\n            .Returns(new BillingCommandResult<string>(new Conflict(\"Unable to create billing portal session. Please contact support for assistance.\")));\n\n        // Act\n        var result = await _sut.CreatePortalSessionAsync(user);\n\n        // Assert\n        Assert.IsAssignableFrom<IResult>(result);\n        await _createBillingPortalSessionCommand.Received(1).Run(user, expectedReturnUrl);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreatePortalSessionAsync_NoSubscriptionId_ReturnsConflictAsync(User user)\n    {\n        // Arrange\n        var expectedReturnUrl = \"bitwarden://premium-upgrade-callback\";\n\n        _currentContext.DeviceType.Returns(DeviceType.iOS);\n        _createBillingPortalSessionCommand.Run(user, expectedReturnUrl)\n            .Returns(new BillingCommandResult<string>(new Conflict(\"Unable to create billing portal session. Please contact support for assistance.\")));\n\n        // Act\n        var result = await _sut.CreatePortalSessionAsync(user);\n\n        // Assert\n        Assert.IsAssignableFrom<IResult>(result);\n        await _createBillingPortalSessionCommand.Received(1).Run(user, expectedReturnUrl);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreatePortalSessionAsync_InvalidSubscriptionStatus_ReturnsBadRequestAsync(User user)\n    {\n        // Arrange\n        var expectedReturnUrl = \"bitwarden://premium-upgrade-callback\";\n\n        _currentContext.DeviceType.Returns(DeviceType.iOS);\n        _createBillingPortalSessionCommand.Run(user, expectedReturnUrl)\n            .Returns(new BillingCommandResult<string>(new BadRequest(\"Your subscription cannot be managed in its current status.\")));\n\n        // Act\n        var result = await _sut.CreatePortalSessionAsync(user);\n\n        // Assert\n        Assert.IsAssignableFrom<IResult>(result);\n        await _createBillingPortalSessionCommand.Received(1).Run(user, expectedReturnUrl);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreatePortalSessionAsync_SubscriptionNotFound_ReturnsConflictAsync(User user)\n    {\n        // Arrange\n        var expectedReturnUrl = \"bitwarden://premium-upgrade-callback\";\n\n        _currentContext.DeviceType.Returns(DeviceType.Android);\n        _createBillingPortalSessionCommand.Run(user, expectedReturnUrl)\n            .Returns(new BillingCommandResult<string>(new Conflict(\"Unable to create billing portal session. Please contact support for assistance.\")));\n\n        // Act\n        var result = await _sut.CreatePortalSessionAsync(user);\n\n        // Assert\n        Assert.IsAssignableFrom<IResult>(result);\n        await _createBillingPortalSessionCommand.Received(1).Run(user, expectedReturnUrl);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreatePortalSessionAsync_StripeException_ReturnsServerErrorAsync(User user)\n    {\n        // Arrange\n        var expectedReturnUrl = \"bitwarden://premium-upgrade-callback\";\n        var exception = new StripeException(\"Stripe API error\");\n\n        _currentContext.DeviceType.Returns(DeviceType.iOS);\n        _createBillingPortalSessionCommand.Run(user, expectedReturnUrl)\n            .Returns(new BillingCommandResult<string>(new Unhandled(exception)));\n\n        // Act\n        var result = await _sut.CreatePortalSessionAsync(user);\n\n        // Assert\n        Assert.IsAssignableFrom<IResult>(result);\n        await _createBillingPortalSessionCommand.Received(1).Run(user, expectedReturnUrl);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreatePortalSessionAsync_SessionWithNullUrl_ReturnsServerErrorAsync(User user)\n    {\n        // Arrange\n        var expectedReturnUrl = \"bitwarden://premium-upgrade-callback\";\n\n        _currentContext.DeviceType.Returns(DeviceType.Android);\n        _createBillingPortalSessionCommand.Run(user, expectedReturnUrl)\n            .Returns(new BillingCommandResult<string>(new Conflict(\"Unable to create billing portal session. Please contact support for assistance.\")));\n\n        // Act\n        var result = await _sut.CreatePortalSessionAsync(user);\n\n        // Assert\n        Assert.IsAssignableFrom<IResult>(result);\n        await _createBillingPortalSessionCommand.Received(1).Run(user, expectedReturnUrl);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreatePortalSessionAsync_NullSession_ReturnsServerErrorAsync(User user)\n    {\n        // Arrange\n        var expectedReturnUrl = \"bitwarden://premium-upgrade-callback\";\n\n        _currentContext.DeviceType.Returns(DeviceType.iOS);\n        _createBillingPortalSessionCommand.Run(user, expectedReturnUrl)\n            .Returns(new BillingCommandResult<string>(new Conflict(\"Unable to create billing portal session. Please contact support for assistance.\")));\n\n        // Act\n        var result = await _sut.CreatePortalSessionAsync(user);\n\n        // Assert\n        Assert.IsAssignableFrom<IResult>(result);\n        await _createBillingPortalSessionCommand.Received(1).Run(user, expectedReturnUrl);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreatePortalSessionAsync_NonMobileDevice_ReturnsNotFoundAsync(User user)\n    {\n        // Arrange\n        _currentContext.DeviceType.Returns(DeviceType.ChromeBrowser);\n\n        // Act\n        var result = await _sut.CreatePortalSessionAsync(user);\n\n        // Assert\n        Assert.IsType<NotFound>(result);\n        await _createBillingPortalSessionCommand.DidNotReceiveWithAnyArgs().Run(Arg.Any<User>(), Arg.Any<string>());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Models/Requests/PreviewPremiumUpgradeProrationRequestTests.cs",
    "content": "﻿using Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Api.Billing.Models.Requests.PreviewInvoice;\nusing Bit.Core.Billing.Enums;\nusing Xunit;\n\nnamespace Bit.Api.Test.Billing.Models.Requests;\n\npublic class PreviewPremiumUpgradeProrationRequestTests\n{\n    [Theory]\n    [InlineData(ProductTierType.Families, PlanType.FamiliesAnnually)]\n    [InlineData(ProductTierType.Teams, PlanType.TeamsAnnually)]\n    [InlineData(ProductTierType.Enterprise, PlanType.EnterpriseAnnually)]\n    public void ToDomain_ValidTierTypes_ReturnsPlanType(ProductTierType tierType, PlanType expectedPlanType)\n    {\n        // Arrange\n        var sut = new PreviewPremiumUpgradeProrationRequest\n        {\n            TargetProductTierType = tierType,\n            BillingAddress = new MinimalBillingAddressRequest\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            }\n        };\n\n        // Act\n        var (planType, billingAddress) = sut.ToDomain();\n\n        // Assert\n        Assert.Equal(expectedPlanType, planType);\n        Assert.Equal(\"US\", billingAddress.Country);\n        Assert.Equal(\"12345\", billingAddress.PostalCode);\n    }\n\n    [Theory]\n    [InlineData(ProductTierType.Free)]\n    [InlineData(ProductTierType.TeamsStarter)]\n    public void ToDomain_InvalidTierTypes_ThrowsInvalidOperationException(ProductTierType tierType)\n    {\n        // Arrange\n        var sut = new PreviewPremiumUpgradeProrationRequest\n        {\n            TargetProductTierType = tierType,\n            BillingAddress = new MinimalBillingAddressRequest\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            }\n        };\n\n        // Act & Assert\n        var exception = Assert.Throws<InvalidOperationException>(() => sut.ToDomain());\n        Assert.Contains($\"Cannot upgrade Premium subscription to {tierType} plan\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Models/Requests/UpgradePremiumToOrganizationRequestTests.cs",
    "content": "﻿using Bit.Api.Billing.Models.Requests.Payment;\nusing Bit.Api.Billing.Models.Requests.Premium;\nusing Bit.Core.Billing.Enums;\nusing Xunit;\n\nnamespace Bit.Api.Test.Billing.Models.Requests;\n\npublic class UpgradePremiumToOrganizationRequestTests\n{\n    [Theory]\n    [InlineData(ProductTierType.Families, PlanType.FamiliesAnnually)]\n    [InlineData(ProductTierType.Teams, PlanType.TeamsAnnually)]\n    [InlineData(ProductTierType.Enterprise, PlanType.EnterpriseAnnually)]\n    public void ToDomain_ValidTierTypes_ReturnsPlanType(ProductTierType tierType, PlanType expectedPlanType)\n    {\n        // Arrange\n        var sut = new UpgradePremiumToOrganizationRequest\n        {\n            OrganizationName = \"Test Organization\",\n            Key = \"encrypted-key\",\n            PublicKey = \"public-key\",\n            EncryptedPrivateKey = \"encrypted-private-key\",\n            CollectionName = \"Default Collection\",\n            TargetProductTierType = tierType,\n            BillingAddress = new CheckoutBillingAddressRequest\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            }\n        };\n\n        // Act\n        var (organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress) = sut.ToDomain();\n\n        // Assert\n        Assert.Equal(\"Test Organization\", organizationName);\n        Assert.Equal(\"encrypted-key\", key);\n        Assert.Equal(\"public-key\", publicKey);\n        Assert.Equal(\"encrypted-private-key\", encryptedPrivateKey);\n        Assert.Equal(\"Default Collection\", collectionName);\n        Assert.Equal(expectedPlanType, planType);\n        Assert.Equal(\"US\", billingAddress.Country);\n        Assert.Equal(\"12345\", billingAddress.PostalCode);\n    }\n\n    [Theory]\n    [InlineData(ProductTierType.Free)]\n    [InlineData(ProductTierType.TeamsStarter)]\n    public void ToDomain_InvalidTierTypes_ThrowsInvalidOperationException(ProductTierType tierType)\n    {\n        // Arrange\n        var sut = new UpgradePremiumToOrganizationRequest\n        {\n            OrganizationName = \"Test Organization\",\n            Key = \"encrypted-key\",\n            PublicKey = \"public-key\",\n            EncryptedPrivateKey = \"encrypted-private-key\",\n            TargetProductTierType = tierType,\n            BillingAddress = new CheckoutBillingAddressRequest\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            }\n        };\n\n        // Act & Assert\n        var exception = Assert.Throws<InvalidOperationException>(() => sut.ToDomain());\n        Assert.Contains($\"Cannot upgrade Premium subscription to {tierType} plan\", exception.Message);\n    }\n\n    [Theory]\n    [InlineData(ProductTierType.Teams, PlanType.TeamsAnnually, \"DE\", \"10115\", \"eu_vat\", \"DE123456789\")]\n    [InlineData(ProductTierType.Enterprise, PlanType.EnterpriseAnnually, \"FR\", \"75001\", \"eu_vat\", \"FR12345678901\")]\n    public void ToDomain_BusinessPlansWithNonUsTaxId_IncludesTaxIdInBillingAddress(\n        ProductTierType tierType,\n        PlanType expectedPlanType,\n        string country,\n        string postalCode,\n        string taxIdCode,\n        string taxIdValue)\n    {\n        // Arrange\n        var sut = new UpgradePremiumToOrganizationRequest\n        {\n            OrganizationName = \"International Business\",\n            Key = \"encrypted-key\",\n            TargetProductTierType = tierType,\n            PublicKey = \"public-key\",\n            EncryptedPrivateKey = \"encrypted-private-key\",\n            CollectionName = \"Default Collection\",\n            BillingAddress = new CheckoutBillingAddressRequest\n            {\n                Country = country,\n                PostalCode = postalCode,\n                TaxId = new CheckoutBillingAddressRequest.TaxIdRequest\n                {\n                    Code = taxIdCode,\n                    Value = taxIdValue\n                }\n            }\n        };\n\n        // Act\n        var (organizationName, key, publicKey, encryptedPrivateKey, collectionName, planType, billingAddress) = sut.ToDomain();\n\n        // Assert\n        Assert.Equal(\"International Business\", organizationName);\n        Assert.Equal(\"encrypted-key\", key);\n        Assert.Equal(\"public-key\", publicKey);\n        Assert.Equal(\"encrypted-private-key\", encryptedPrivateKey);\n        Assert.Equal(\"Default Collection\", collectionName);\n        Assert.Equal(expectedPlanType, planType);\n        Assert.Equal(country, billingAddress.Country);\n        Assert.Equal(postalCode, billingAddress.PostalCode);\n        Assert.NotNull(billingAddress.TaxId);\n        Assert.Equal(taxIdCode, billingAddress.TaxId.Code);\n        Assert.Equal(taxIdValue, billingAddress.TaxId.Value);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Billing/Utilities.cs",
    "content": "﻿using Bit.Api.Billing.Controllers;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Models.Api;\nusing Bit.Test.Common.AutoFixture;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Http.HttpResults;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Billing;\n\npublic static class Utilities\n{\n    public static void AssertNotFound(IResult result)\n    {\n        Assert.IsType<NotFound<ErrorResponseModel>>(result);\n\n        var response = ((NotFound<ErrorResponseModel>)result).Value;\n\n        Assert.Equal(\"Resource not found.\", response.Message);\n    }\n\n    public static void AssertUnauthorized(IResult result, string message = \"Unauthorized.\")\n    {\n        Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);\n\n        var response = (JsonHttpResult<ErrorResponseModel>)result;\n\n        Assert.Equal(StatusCodes.Status401Unauthorized, response.StatusCode);\n        Assert.Equal(message, response.Value.Message);\n    }\n\n    public static void ConfigureStableProviderAdminInputs<T>(\n        Provider provider,\n        SutProvider<T> sutProvider) where T : BaseProviderController\n    {\n        ConfigureBaseProviderInputs(provider, sutProvider);\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)\n            .Returns(true);\n    }\n\n    public static void ConfigureStableProviderServiceUserInputs<T>(\n        Provider provider,\n        SutProvider<T> sutProvider) where T : BaseProviderController\n    {\n        ConfigureBaseProviderInputs(provider, sutProvider);\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderUser(provider.Id)\n            .Returns(true);\n    }\n\n    private static void ConfigureBaseProviderInputs<T>(\n        Provider provider,\n        SutProvider<T> sutProvider) where T : BaseProviderController\n    {\n        provider.Type = ProviderType.Msp;\n        provider.Status = ProviderStatusType.Billable;\n\n        sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Controllers/CollectionsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.Controllers;\nusing Bit.Api.Models.Request;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Controllers;\n\n[ControllerCustomize(typeof(CollectionsController))]\n[SutProviderCustomize]\npublic class CollectionsControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task Post_Success(Organization organization, CreateCollectionRequestModel collectionRequest,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        Collection ExpectedCollection() => Arg.Is<Collection>(c =>\n            c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&\n            c.OrganizationId == organization.Id);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                ExpectedCollection(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Success());\n\n        _ = await sutProvider.Sut.Post(organization.Id, collectionRequest);\n\n        await sutProvider.GetDependency<ICreateCollectionCommand>()\n            .Received(1)\n            .CreateAsync(Arg.Is<Collection>(c =>\n                c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId && c.OrganizationId == organization.Id),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_Success(Collection collection, UpdateCollectionRequestModel collectionRequest,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        collection.DefaultUserCollectionEmail = null;\n        Collection ExpectedCollection() => Arg.Is<Collection>(c => c.Id == collection.Id &&\n            c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&\n            c.OrganizationId == collection.OrganizationId);\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdAsync(collection.Id)\n            .Returns(collection);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                collection,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))\n            .Returns(AuthorizationResult.Success());\n\n        _ = await sutProvider.Sut.Put(collection.OrganizationId, collection.Id, collectionRequest);\n\n        await sutProvider.GetDependency<IUpdateCollectionCommand>()\n            .Received(1)\n            .UpdateAsync(ExpectedCollection(), Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, UpdateCollectionRequestModel collectionRequest,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                collection,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))\n            .Returns(AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdAsync(collection.Id)\n            .Returns(collection);\n\n        _ = await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.Put(collection.OrganizationId, collection.Id, collectionRequest));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationCollectionsWithGroups_WithReadAllPermissions_GetsAllCollections(Organization organization,\n        Guid userId, SutProvider<CollectionsController> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<object>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>\n                    requirements.Cast<CollectionOperationRequirement>().All(operation =>\n                        operation.Name == nameof(CollectionOperations.ReadAllWithAccess)\n                        && operation.OrganizationId == organization.Id)))\n            .Returns(AuthorizationResult.Success());\n\n        await sutProvider.Sut.GetManyWithDetails(organization.Id);\n\n        await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, userId, true);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationCollectionsWithGroups_MissingReadAllPermissions_GetsAssignedCollections(\n        Organization organization, Guid userId, SutProvider<CollectionsController> sutProvider,\n        List<CollectionAdminDetails> collections)\n    {\n        collections.ForEach(c => c.OrganizationId = organization.Id);\n        collections.ForEach(c => c.Manage = false);\n\n        var managedCollection = collections.First();\n        managedCollection.Manage = true;\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<object>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>\n                    requirements.Cast<CollectionOperationRequirement>().All(operation =>\n                        operation.Name == nameof(CollectionOperations.ReadAllWithAccess)\n                        && operation.OrganizationId == organization.Id)))\n            .Returns(AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<object>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>\n                    requirements.Cast<BulkCollectionOperationRequirement>().All(operation =>\n                        operation.Name == nameof(BulkCollectionOperations.ReadWithAccess))))\n            .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, userId, true)\n            .Returns(collections);\n\n        var response = await sutProvider.Sut.GetManyWithDetails(organization.Id);\n\n        await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, userId, true);\n        Assert.Single(response.Data);\n        Assert.All(response.Data, c => Assert.Equal(organization.Id, c.OrganizationId));\n        Assert.All(response.Data, c => Assert.Equal(managedCollection.Id, c.Id));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationCollections_WithReadAllPermissions_GetsAllCollections(\n        Organization organization, List<Collection> collections, Guid userId,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        collections.ForEach(c => c.OrganizationId = organization.Id);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<object>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>\n                    requirements.Cast<CollectionOperationRequirement>().All(operation =>\n                        operation.Name == nameof(CollectionOperations.ReadAll)\n                        && operation.OrganizationId == organization.Id)))\n            .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManySharedCollectionsByOrganizationIdAsync(organization.Id)\n            .Returns(collections);\n\n        var response = await sutProvider.Sut.GetAll(organization.Id);\n\n        await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManySharedCollectionsByOrganizationIdAsync(organization.Id);\n\n        Assert.Equal(collections.Count, response.Data.Count());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationCollections_MissingReadAllPermissions_GetsManageableCollections(\n        Organization organization, List<CollectionDetails> collections, Guid userId, SutProvider<CollectionsController> sutProvider)\n    {\n        collections.ForEach(c => c.OrganizationId = organization.Id);\n        collections.ForEach(c => c.Manage = false);\n\n        var managedCollection = collections.First();\n        managedCollection.Manage = true;\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<object>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>\n                    requirements.Cast<CollectionOperationRequirement>().All(operation =>\n                        operation.Name == nameof(CollectionOperations.ReadAll)\n                        && operation.OrganizationId == organization.Id)))\n            .Returns(AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<object>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>\n                    requirements.Cast<BulkCollectionOperationRequirement>().All(operation =>\n                        operation.Name == nameof(BulkCollectionOperations.Read))))\n            .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(collections);\n\n        var result = await sutProvider.Sut.GetAll(organization.Id);\n\n        await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdAsync(organization.Id);\n        await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(userId);\n\n        Assert.Single(result.Data);\n        Assert.All(result.Data, c => Assert.Equal(organization.Id, c.OrganizationId));\n        Assert.All(result.Data, c => Assert.Equal(managedCollection.Id, c.Id));\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteMany_Success(Organization organization, Collection collection1, Collection collection2,\n         SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        var orgId = organization.Id;\n        var model = new CollectionBulkDeleteRequestModel\n        {\n            Ids = new[] { collection1.Id, collection2.Id }\n        };\n\n        var collections = new List<Collection>\n            {\n                new CollectionDetails\n                {\n                    Id = collection1.Id,\n                    OrganizationId = orgId,\n                },\n                new CollectionDetails\n                {\n                    Id = collection2.Id,\n                    OrganizationId = orgId,\n                },\n            };\n\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(collections);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                collections,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Delete)))\n            .Returns(AuthorizationResult.Success());\n\n        // Act\n        await sutProvider.Sut.DeleteMany(orgId, model);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteCollectionCommand>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<IEnumerable<Collection>>(coll => coll.Select(c => c.Id).SequenceEqual(collections.Select(c => c.Id))));\n\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteMany_PermissionDenied_ThrowsNotFound(Organization organization, Collection collection1,\n        Collection collection2, SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        var orgId = organization.Id;\n        var model = new CollectionBulkDeleteRequestModel\n        {\n            Ids = new[] { collection1.Id, collection2.Id }\n        };\n\n        var collections = new List<Collection>\n        {\n            new CollectionDetails\n            {\n                Id = collection1.Id,\n                OrganizationId = orgId,\n            },\n            new CollectionDetails\n            {\n                Id = collection2.Id,\n                OrganizationId = orgId,\n            },\n        };\n\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(collections);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                collections,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Delete)))\n            .Returns(AuthorizationResult.Failed());\n\n        // Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.DeleteMany(orgId, model));\n\n        await sutProvider.GetDependency<IDeleteCollectionCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync((IEnumerable<Collection>)default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostBulkCollectionAccess_Success(User actingUser, List<Collection> collections,\n        Organization organization, SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        collections.ForEach(c => c.OrganizationId = organization.Id);\n        var userId = Guid.NewGuid();\n        var groupId = Guid.NewGuid();\n        var model = new BulkCollectionAccessRequestModel\n        {\n            CollectionIds = collections.Select(c => c.Id),\n            Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },\n            Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },\n        };\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(model.CollectionIds)\n            .Returns(collections);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(actingUser.Id);\n\n        sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(), ExpectedCollectionAccess(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(\n                    reqs => reqs.All(r =>\n                        r == BulkCollectionOperations.ModifyUserAccess ||\n                        r == BulkCollectionOperations.ModifyGroupAccess)\n                ))\n            .Returns(AuthorizationResult.Success());\n\n        IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));\n\n        // Act\n        await sutProvider.Sut.PostBulkCollectionAccess(organization.Id, model);\n\n        // Assert\n        await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(\n            Arg.Any<ClaimsPrincipal>(),\n            ExpectedCollectionAccess(),\n            Arg.Is<IEnumerable<IAuthorizationRequirement>>(\n                reqs => reqs.All(r =>\n                    r == BulkCollectionOperations.ModifyUserAccess ||\n                    r == BulkCollectionOperations.ModifyGroupAccess)));\n        await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().Received()\n            .AddAccessAsync(\n                Arg.Is<ICollection<Collection>>(g => g.SequenceEqual(collections)),\n                Arg.Is<ICollection<CollectionAccessSelection>>(u => u.All(c => c.Id == userId && c.Manage)),\n                Arg.Is<ICollection<CollectionAccessSelection>>(g => g.All(c => c.Id == groupId && c.ReadOnly)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostBulkCollectionAccess_CollectionsNotFound_Throws(User actingUser,\n        Organization organization, List<Collection> collections,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        collections.ForEach(c => c.OrganizationId = organization.Id);\n\n        var userId = Guid.NewGuid();\n        var groupId = Guid.NewGuid();\n        var model = new BulkCollectionAccessRequestModel\n        {\n            CollectionIds = collections.Select(c => c.Id),\n            Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },\n            Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },\n        };\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(actingUser.Id);\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(model.CollectionIds)\n            .Returns(collections.Skip(1).ToList());\n\n        var exception = await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.PostBulkCollectionAccess(organization.Id, model));\n\n        Assert.Equal(\"One or more collections not found.\", exception.Message);\n        await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync(\n            Arg.Any<ClaimsPrincipal>(),\n            Arg.Any<IEnumerable<Collection>>(),\n            Arg.Any<IEnumerable<IAuthorizationRequirement>>()\n        );\n        await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()\n            .AddAccessAsync(default, default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostBulkCollectionAccess_CollectionsBelongToDifferentOrganizations_Throws(User actingUser,\n        Organization organization, List<Collection> collections,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        // First collection has a different orgId\n        collections.Skip(1).ToList().ForEach(c => c.OrganizationId = organization.Id);\n\n        var userId = Guid.NewGuid();\n        var groupId = Guid.NewGuid();\n        var model = new BulkCollectionAccessRequestModel\n        {\n            CollectionIds = collections.Select(c => c.Id),\n            Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },\n            Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },\n        };\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(actingUser.Id);\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(model.CollectionIds)\n            .Returns(collections);\n\n        var exception = await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.PostBulkCollectionAccess(organization.Id, model));\n\n        Assert.Equal(\"One or more collections not found.\", exception.Message);\n        await sutProvider.GetDependency<IAuthorizationService>().DidNotReceiveWithAnyArgs().AuthorizeAsync(\n            Arg.Any<ClaimsPrincipal>(),\n            Arg.Any<IEnumerable<Collection>>(),\n            Arg.Any<IEnumerable<IAuthorizationRequirement>>()\n        );\n        await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()\n            .AddAccessAsync(default, default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostBulkCollectionAccess_AccessDenied_Throws(User actingUser, List<Collection> collections,\n        Organization organization, SutProvider<CollectionsController> sutProvider)\n    {\n        collections.ForEach(c => c.OrganizationId = organization.Id);\n\n        var userId = Guid.NewGuid();\n        var groupId = Guid.NewGuid();\n        var model = new BulkCollectionAccessRequestModel\n        {\n            CollectionIds = collections.Select(c => c.Id),\n            Users = new[] { new SelectionReadOnlyRequestModel { Id = userId, Manage = true } },\n            Groups = new[] { new SelectionReadOnlyRequestModel { Id = groupId, ReadOnly = true } },\n        };\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(actingUser.Id);\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(model.CollectionIds)\n            .Returns(collections);\n\n        sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(\n                Arg.Any<ClaimsPrincipal>(), ExpectedCollectionAccess(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(\n                    reqs => reqs.All(r =>\n                        r == BulkCollectionOperations.ModifyUserAccess ||\n                        r == BulkCollectionOperations.ModifyGroupAccess)\n                ))\n            .Returns(AuthorizationResult.Failed());\n\n        IEnumerable<Collection> ExpectedCollectionAccess() => Arg.Is<IEnumerable<Collection>>(cols => cols.SequenceEqual(collections));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostBulkCollectionAccess(organization.Id, model));\n        await sutProvider.GetDependency<IAuthorizationService>().Received().AuthorizeAsync(\n            Arg.Any<ClaimsPrincipal>(),\n            ExpectedCollectionAccess(),\n            Arg.Is<IEnumerable<IAuthorizationRequirement>>(\n                    reqs => reqs.All(r =>\n                        r == BulkCollectionOperations.ModifyUserAccess ||\n                        r == BulkCollectionOperations.ModifyGroupAccess))\n            );\n        await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()\n            .AddAccessAsync(default, default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_With_NonNullName_DoesNotPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        var newName = \"new name\";\n        var originalName = \"original name\";\n\n        existingCollection.Name = originalName;\n        existingCollection.DefaultUserCollectionEmail = null;\n\n        collectionRequest.Name = newName;\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdAsync(existingCollection.Id)\n            .Returns(existingCollection);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                existingCollection,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))\n            .Returns(AuthorizationResult.Success());\n\n        // Act\n        await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);\n\n        // Assert\n        await sutProvider.GetDependency<IUpdateCollectionCommand>()\n            .Received(1)\n            .UpdateAsync(\n                Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == newName),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithNullName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        var originalName = \"original name\";\n\n        existingCollection.Name = originalName;\n        existingCollection.DefaultUserCollectionEmail = null;\n\n        collectionRequest.Name = null;\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdAsync(existingCollection.Id)\n            .Returns(existingCollection);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                existingCollection,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))\n            .Returns(AuthorizationResult.Success());\n\n        // Act\n        await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);\n\n        // Assert\n        await sutProvider.GetDependency<IUpdateCollectionCommand>()\n            .Received(1)\n            .UpdateAsync(\n                Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithDefaultUserCollectionEmail_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        var originalName = \"original name\";\n        var defaultUserCollectionEmail = \"user@email.com\";\n\n        existingCollection.Name = originalName;\n        existingCollection.DefaultUserCollectionEmail = defaultUserCollectionEmail;\n\n        collectionRequest.Name = \"new name\";\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdAsync(existingCollection.Id)\n            .Returns(existingCollection);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                existingCollection,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))\n            .Returns(AuthorizationResult.Success());\n\n        // Act\n        await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);\n\n        // Assert\n        await sutProvider.GetDependency<IUpdateCollectionCommand>()\n            .Received(1)\n            .UpdateAsync(\n                Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName && c.DefaultUserCollectionEmail == defaultUserCollectionEmail),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithEmptyName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        var originalName = \"original name\";\n\n        existingCollection.Name = originalName;\n        existingCollection.DefaultUserCollectionEmail = null;\n\n        collectionRequest.Name = \"\"; // Empty string\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdAsync(existingCollection.Id)\n            .Returns(existingCollection);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                existingCollection,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))\n            .Returns(AuthorizationResult.Success());\n\n        // Act\n        await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);\n\n        // Assert\n        await sutProvider.GetDependency<IUpdateCollectionCommand>()\n            .Received(1)\n            .UpdateAsync(\n                Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Put_WithWhitespaceOnlyName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,\n        SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        var originalName = \"original name\";\n\n        existingCollection.Name = originalName;\n        existingCollection.DefaultUserCollectionEmail = null;\n\n        collectionRequest.Name = \"   \"; // Whitespace only\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdAsync(existingCollection.Id)\n            .Returns(existingCollection);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                existingCollection,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))\n            .Returns(AuthorizationResult.Success());\n\n        // Act\n        await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);\n\n        // Assert\n        await sutProvider.GetDependency<IUpdateCollectionCommand>()\n            .Received(1)\n            .UpdateAsync(\n                Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Controllers/ConfigControllerTests.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Bit.Api.Controllers;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Controllers;\n\npublic class ConfigControllerTests : IDisposable\n{\n    private readonly ConfigController _sut;\n    private readonly GlobalSettings _globalSettings;\n    private readonly IFeatureService _featureService;\n\n    public ConfigControllerTests()\n    {\n        _globalSettings = new GlobalSettings();\n        _featureService = Substitute.For<IFeatureService>();\n        _featureService.GetAll().Returns(new Dictionary<string, object>());\n\n        _sut = new ConfigController(\n            _globalSettings,\n            _featureService\n        );\n    }\n\n    public void Dispose()\n    {\n        _sut?.Dispose();\n    }\n\n    [Theory, AutoData]\n    public void GetConfigs_WithFeatureStates(Dictionary<string, object> featureStates)\n    {\n        _featureService.GetAll().Returns(featureStates);\n\n        var response = _sut.GetConfigs();\n\n        Assert.NotNull(response);\n        Assert.NotNull(response.FeatureStates);\n        Assert.Equal(featureStates, response.FeatureStates);\n    }\n\n    [Fact]\n    public void GetConfigs_FillAssistRulesNotConfigured_ReturnsNullEnvironmentValue()\n    {\n        // BaseServiceUriSettings.FillAssistRules defaults to null when not explicitly set\n        var response = _sut.GetConfigs();\n\n        Assert.NotNull(response.Environment);\n        Assert.Null(response.Environment.FillAssistRules);\n    }\n\n    [Fact]\n    public void GetConfigs_FillAssistRulesConfigured_ReturnsConfiguredValue()\n    {\n        var expectedUri = \"https://example.com/custom-rules.json\";\n        _globalSettings.BaseServiceUri.FillAssistRules = expectedUri;\n\n        var response = _sut.GetConfigs();\n\n        Assert.NotNull(response.Environment);\n        Assert.Equal(expectedUri, response.Environment.FillAssistRules);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Controllers/PoliciesControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing System.Text.Json;\nusing Bit.Api.AdminConsole.Controllers;\nusing Bit.Api.AdminConsole.Models.Request;\nusing Bit.Api.AdminConsole.Models.Response.Organizations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Controllers;\n\n\n// Note: test names follow MethodName_StateUnderTest_ExpectedBehavior pattern.\n[ControllerCustomize(typeof(PoliciesController))]\n[SutProviderCustomize]\npublic class PoliciesControllerTests\n{\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetMasterPasswordPolicy_WhenCalled_ReturnsMasterPasswordPolicy(\n        SutProvider<PoliciesController> sutProvider,\n        Guid orgId, Guid userId,\n        OrganizationUser orgUser,\n        Policy policy,\n        MasterPasswordPolicyData mpPolicyData,\n        Organization organization)\n    {\n        // Arrange\n        organization.UsePolicies = true;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(organization);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(userId);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(orgId, userId)\n            .Returns(orgUser);\n\n\n        policy.Type = PolicyType.MasterPassword;\n        policy.Enabled = true;\n        // data should be a JSON serialized version of the mpPolicyData object\n        policy.Data = JsonSerializer.Serialize(mpPolicyData);\n\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(orgId, PolicyType.MasterPassword)\n            .Returns(policy);\n\n        // Act\n        var result = await sutProvider.Sut.GetMasterPasswordPolicy(orgId);\n\n        // Assert\n\n        Assert.NotNull(result);\n        Assert.Equal(policy.Id, result.Id);\n        Assert.Equal(policy.Type, result.Type);\n        Assert.Equal(policy.Enabled, result.Enabled);\n\n        // Assert that the data is deserialized correctly into a Dictionary<string, object>\n        // for all MasterPasswordPolicyData properties\n        Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data[\"minComplexity\"]).GetInt32());\n        Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data[\"minLength\"]).GetInt32());\n        Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data[\"requireLower\"]).GetBoolean());\n        Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data[\"requireUpper\"]).GetBoolean());\n        Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data[\"requireNumbers\"]).GetBoolean());\n        Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data[\"requireSpecial\"]).GetBoolean());\n        Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data[\"enforceOnLogin\"]).GetBoolean());\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetMasterPasswordPolicy_OrgUserIsNull_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider, Guid orgId, Guid userId)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(userId);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(orgId, userId)\n            .Returns((OrganizationUser)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetMasterPasswordPolicy_PolicyIsNull_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider, Guid orgId, Guid userId, OrganizationUser orgUser)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(userId);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(orgId, userId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(orgId, PolicyType.MasterPassword)\n            .Returns((Policy)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetMasterPasswordPolicy_PolicyNotEnabled_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider, Guid orgId, Guid userId, OrganizationUser orgUser, Policy policy)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(userId);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(orgId, userId)\n            .Returns(orgUser);\n\n        policy.Enabled = false; // Ensuring the policy is not enabled\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(orgId, PolicyType.MasterPassword)\n            .Returns(policy);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetMasterPasswordPolicy_WhenUsePoliciesIsFalse_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns((Organization)null);\n\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetMasterPasswordPolicy_WhenOrgIsNull_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider,\n        Guid orgId,\n        Organization organization)\n    {\n        // Arrange\n        organization.UsePolicies = false;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(organization);\n\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy(\n        SutProvider<PoliciesController> sutProvider, Guid orgId, PolicyStatus policy, PolicyType type)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .ManagePolicies(orgId)\n            .Returns(true);\n\n        policy.Type = type;\n        policy.Enabled = true;\n        policy.Data = null;\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(orgId, type)\n            .Returns(policy);\n\n        // Act\n        var result = await sutProvider.Sut.Get(orgId, type);\n\n        // Assert\n        Assert.IsType<PolicyStatusResponseModel>(result);\n        Assert.Equal(policy.Type, result.Type);\n        Assert.Equal(policy.Enabled, result.Enabled);\n        Assert.Equal(policy.OrganizationId, result.OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Get_WhenUserCannotManagePolicies_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider, Guid orgId, PolicyType type)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .ManagePolicies(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Get(orgId, type));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByToken_WhenOrganizationUseUsePoliciesIsFalse_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider, Guid orgId, Guid organizationUserId, string token, string email,\n        Organization organization)\n    {\n        // Arrange\n        organization.UsePolicies = false;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(organization);\n\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByToken_WhenOrganizationIsNull_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider, Guid orgId, Guid organizationUserId, string token, string email)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns((Organization)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByToken_WhenTokenIsInvalid_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider,\n        Guid orgId,\n        Guid organizationUserId,\n        string token,\n        string email,\n        Organization organization\n    )\n    {\n        // Arrange\n        organization.UsePolicies = true;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(organization);\n\n        var decryptedToken = Substitute.For<OrgUserInviteTokenable>();\n        decryptedToken.Valid.Returns(false);\n\n        var orgUserInviteTokenDataFactory =\n            sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>();\n\n        orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(x =>\n            {\n                x[1] = decryptedToken;\n                return true;\n            });\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByToken_WhenUserIsNull_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider,\n        Guid orgId,\n        Guid organizationUserId,\n        string token,\n        string email,\n        Organization organization\n    )\n    {\n        // Arrange\n        organization.UsePolicies = true;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(organization);\n\n        var decryptedToken = Substitute.For<OrgUserInviteTokenable>();\n        decryptedToken.Valid.Returns(true);\n        decryptedToken.OrgUserId = organizationUserId;\n        decryptedToken.OrgUserEmail = email;\n\n        var orgUserInviteTokenDataFactory =\n            sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>();\n\n        orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(x =>\n            {\n                x[1] = decryptedToken;\n                return true;\n            });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUserId)\n            .Returns((OrganizationUser)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByToken_WhenUserOrgIdDoesNotMatchOrgId_ThrowsNotFoundException(\n        SutProvider<PoliciesController> sutProvider,\n        Guid orgId,\n        Guid organizationUserId,\n        string token,\n        string email,\n        OrganizationUser orgUser,\n        Organization organization\n    )\n    {\n        // Arrange\n        organization.UsePolicies = true;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(organization);\n\n        var decryptedToken = Substitute.For<OrgUserInviteTokenable>();\n        decryptedToken.Valid.Returns(true);\n        decryptedToken.OrgUserId = organizationUserId;\n        decryptedToken.OrgUserEmail = email;\n\n        var orgUserInviteTokenDataFactory =\n            sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>();\n\n        orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(x =>\n            {\n                x[1] = decryptedToken;\n                return true;\n            });\n\n        orgUser.OrganizationId = Guid.Empty;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUserId)\n            .Returns(orgUser);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByToken_ShouldReturnEnabledPolicies(\n        SutProvider<PoliciesController> sutProvider,\n        Guid orgId,\n        Guid organizationUserId,\n        string token,\n        string email,\n        OrganizationUser orgUser,\n        Organization organization\n    )\n    {\n        // Arrange\n        organization.UsePolicies = true;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(organization);\n\n        var decryptedToken = Substitute.For<OrgUserInviteTokenable>();\n        decryptedToken.Valid.Returns(true);\n        decryptedToken.OrgUserId = organizationUserId;\n        decryptedToken.OrgUserEmail = email;\n\n        var orgUserInviteTokenDataFactory =\n            sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>();\n\n        orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(x =>\n            {\n                x[1] = decryptedToken;\n                return true;\n            });\n\n        orgUser.OrganizationId = orgId;\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUserId)\n            .Returns(orgUser);\n\n        var enabledPolicy = Substitute.For<Policy>();\n        enabledPolicy.Enabled = true;\n        var disabledPolicy = Substitute.For<Policy>();\n        disabledPolicy.Enabled = false;\n\n        var policies = new[] { enabledPolicy, disabledPolicy };\n\n\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(orgId)\n            .Returns(policies);\n\n        // Act\n        var result = await sutProvider.Sut.GetByToken(orgId, email, token, organizationUserId);\n\n        // Assert\n        var expectedPolicy = result.Data.Single();\n\n        Assert.NotNull(result);\n\n        Assert.Equal(enabledPolicy.Id, expectedPolicy.Id);\n        Assert.Equal(enabledPolicy.Type, expectedPolicy.Type);\n        Assert.Equal(enabledPolicy.Enabled, expectedPolicy.Enabled);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Put_UsesVNextSavePolicyCommand(\n        SutProvider<PoliciesController> sutProvider, Guid orgId,\n        SavePolicyRequest model, Policy policy, Guid userId)\n    {\n        // Arrange\n        policy.Data = null;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId\n            .Returns(userId);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .SaveAsync(Arg.Any<SavePolicyModel>())\n            .Returns(policy);\n\n        // Act\n        var result = await sutProvider.Sut.Put(orgId, policy.Type, model.Policy);\n\n        // Assert\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .Received(1)\n            .SaveAsync(Arg.Is<SavePolicyModel>(m => m.PolicyUpdate.OrganizationId == orgId &&\n                                                    m.PolicyUpdate.Type == policy.Type &&\n                                                    m.PolicyUpdate.Enabled == model.Policy.Enabled &&\n                                                    m.PerformedBy.UserId == userId &&\n                                                    m.PerformedBy.IsOrganizationOwnerOrProvider == true));\n\n        await sutProvider.GetDependency<ISavePolicyCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .VNextSaveAsync(default);\n\n        Assert.NotNull(result);\n        Assert.Equal(policy.Id, result.Id);\n        Assert.Equal(policy.Type, result.Type);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutVNext_UsesVNextSavePolicyCommand(\n        SutProvider<PoliciesController> sutProvider, Guid orgId,\n        SavePolicyRequest model, Policy policy, Guid userId)\n    {\n        // Arrange\n        policy.Data = null;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId\n            .Returns(userId);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .SaveAsync(Arg.Any<SavePolicyModel>())\n            .Returns(policy);\n\n        // Act\n        var result = await sutProvider.Sut.PutVNext(orgId, policy.Type, model);\n\n        // Assert\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .Received(1)\n            .SaveAsync(Arg.Is<SavePolicyModel>(m => m.PolicyUpdate.OrganizationId == orgId &&\n                                                    m.PolicyUpdate.Type == policy.Type &&\n                                                    m.PolicyUpdate.Enabled == model.Policy.Enabled &&\n                                                    m.PerformedBy.UserId == userId &&\n                                                    m.PerformedBy.IsOrganizationOwnerOrProvider == true));\n\n        await sutProvider.GetDependency<ISavePolicyCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .VNextSaveAsync(default);\n\n        Assert.NotNull(result);\n        Assert.Equal(policy.Id, result.Id);\n        Assert.Equal(policy.Type, result.Type);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Dirt/Controllers/OrganizationIntegrationControllerTests.cs",
    "content": "﻿using Bit.Api.Dirt.Controllers;\nusing Bit.Api.Dirt.Models.Request;\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Dirt.Controllers;\n\n[ControllerCustomize(typeof(OrganizationIntegrationController))]\n[SutProviderCustomize]\npublic class OrganizationIntegrationControllerTests\n{\n    private readonly OrganizationIntegrationRequestModel _webhookRequestModel = new()\n    {\n        Configuration = null,\n        Type = IntegrationType.Webhook\n    };\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAsync(organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_IntegrationsExist_ReturnsIntegrations(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId,\n        List<OrganizationIntegration> integrations)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IGetOrganizationIntegrationsQuery>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns(integrations);\n\n        var result = await sutProvider.Sut.GetAsync(organizationId);\n\n        await sutProvider.GetDependency<IGetOrganizationIntegrationsQuery>().Received(1)\n            .GetManyByOrganizationAsync(organizationId);\n\n        Assert.Equal(integrations.Count, result.Count);\n        Assert.All(result, r => Assert.IsType<OrganizationIntegrationResponseModel>(r));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_NoIntegrations_ReturnsEmptyList(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IGetOrganizationIntegrationsQuery>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns([]);\n\n        var result = await sutProvider.Sut.GetAsync(organizationId);\n\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_AllParamsProvided_Succeeds(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId,\n        OrganizationIntegration integration)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<ICreateOrganizationIntegrationCommand>()\n            .CreateAsync(Arg.Any<OrganizationIntegration>())\n            .Returns(integration);\n\n        var response = await sutProvider.Sut.CreateAsync(organizationId, _webhookRequestModel);\n\n        await sutProvider.GetDependency<ICreateOrganizationIntegrationCommand>().Received(1)\n            .CreateAsync(Arg.Is<OrganizationIntegration>(i =>\n                i.OrganizationId == organizationId &&\n                i.Type == IntegrationType.Webhook));\n        Assert.IsType<OrganizationIntegrationResponseModel>(response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.CreateAsync(organizationId, _webhookRequestModel));\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_AllParamsProvided_Succeeds(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n\n        await sutProvider.Sut.DeleteAsync(organizationId, integrationId);\n\n        await sutProvider.GetDependency<IDeleteOrganizationIntegrationCommand>().Received(1)\n            .DeleteAsync(organizationId, integrationId);\n    }\n\n    [Theory, BitAutoData]\n    [Obsolete(\"Obsolete\")]\n    public async Task PostDeleteAsync_AllParamsProvided_Succeeds(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n\n        await sutProvider.Sut.PostDeleteAsync(organizationId, integrationId);\n\n        await sutProvider.GetDependency<IDeleteOrganizationIntegrationCommand>().Received(1)\n            .DeleteAsync(organizationId, integrationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.DeleteAsync(organizationId, integrationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_AllParamsProvided_Succeeds(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration)\n    {\n        integration.OrganizationId = organizationId;\n        integration.Id = integrationId;\n        integration.Type = IntegrationType.Webhook;\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IUpdateOrganizationIntegrationCommand>()\n            .UpdateAsync(organizationId, integrationId, Arg.Any<OrganizationIntegration>())\n            .Returns(integration);\n\n        var response = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, _webhookRequestModel);\n\n        await sutProvider.GetDependency<IUpdateOrganizationIntegrationCommand>().Received(1)\n            .UpdateAsync(organizationId, integrationId, Arg.Is<OrganizationIntegration>(i =>\n                i.OrganizationId == organizationId &&\n                i.Type == IntegrationType.Webhook));\n        Assert.IsType<OrganizationIntegrationResponseModel>(response);\n        Assert.Equal(IntegrationType.Webhook, response.Type);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(\n        SutProvider<OrganizationIntegrationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.UpdateAsync(organizationId, integrationId, _webhookRequestModel));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Dirt/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs",
    "content": "﻿using Bit.Api.Dirt.Controllers;\nusing Bit.Api.Dirt.Models.Request;\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Dirt.Controllers;\n\n[ControllerCustomize(typeof(OrganizationIntegrationConfigurationController))]\n[SutProviderCustomize]\npublic class OrganizationIntegrationsConfigurationControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_AllParamsProvided_Succeeds(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n\n        await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);\n\n        await sutProvider.GetDependency<IDeleteOrganizationIntegrationConfigurationCommand>().Received(1)\n            .DeleteAsync(organizationId, integrationId, configurationId);\n    }\n\n    [Theory, BitAutoData]\n    [Obsolete(\"Obsolete\")]\n    public async Task PostDeleteAsync_AllParamsProvided_Succeeds(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n\n        await sutProvider.Sut.PostDeleteAsync(organizationId, integrationId, configurationId);\n\n        await sutProvider.GetDependency<IDeleteOrganizationIntegrationConfigurationCommand>().Received(1)\n            .DeleteAsync(organizationId, integrationId, configurationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_ConfigurationsExist_Succeeds(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        List<OrganizationIntegrationConfiguration> configurations)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>()\n            .GetManyByIntegrationAsync(organizationId, integrationId)\n            .Returns(configurations);\n\n        var result = await sutProvider.Sut.GetAsync(organizationId, integrationId);\n\n        Assert.NotNull(result);\n        Assert.Equal(configurations.Count, result.Count);\n        Assert.All(result, r => Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(r));\n        await sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>().Received(1)\n            .GetManyByIntegrationAsync(organizationId, integrationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_NoConfigurationsExist_ReturnsEmptyList(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>()\n            .GetManyByIntegrationAsync(organizationId, integrationId)\n            .Returns([]);\n\n        var result = await sutProvider.Sut.GetAsync(organizationId, integrationId);\n\n        Assert.NotNull(result);\n        Assert.Empty(result);\n        await sutProvider.GetDependency<IGetOrganizationIntegrationConfigurationsQuery>().Received(1)\n            .GetManyByIntegrationAsync(organizationId, integrationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.GetAsync(organizationId, integrationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostAsync_AllParamsProvided_Succeeds(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegrationConfiguration configuration,\n        OrganizationIntegrationConfigurationRequestModel model)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<ICreateOrganizationIntegrationConfigurationCommand>()\n            .CreateAsync(organizationId, integrationId, Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(configuration);\n\n        var createResponse = await sutProvider.Sut.CreateAsync(organizationId, integrationId, model);\n\n        await sutProvider.GetDependency<ICreateOrganizationIntegrationConfigurationCommand>().Received(1)\n            .CreateAsync(organizationId, integrationId, Arg.Any<OrganizationIntegrationConfiguration>());\n        Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(createResponse);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.CreateAsync(organizationId, integrationId, new OrganizationIntegrationConfigurationRequestModel()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_AllParamsProvided_Succeeds(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegrationConfiguration configuration,\n        OrganizationIntegrationConfigurationRequestModel model)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IUpdateOrganizationIntegrationConfigurationCommand>()\n            .UpdateAsync(organizationId, integrationId, configurationId, Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(configuration);\n\n        var updateResponse = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, model);\n\n        await sutProvider.GetDependency<IUpdateOrganizationIntegrationConfigurationCommand>().Received(1)\n            .UpdateAsync(organizationId, integrationId, configurationId, Arg.Any<OrganizationIntegrationConfiguration>());\n        Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(updateResponse);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(\n        SutProvider<OrganizationIntegrationConfigurationController> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, new OrganizationIntegrationConfigurationRequestModel()));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Dirt/Controllers/SlackIntegrationControllerTests.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Api.Dirt.Controllers;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Routing;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Dirt.Controllers;\n\n[ControllerCustomize(typeof(SlackIntegrationController))]\n[SutProviderCustomize]\npublic class SlackIntegrationControllerTests\n{\n    private const string _slackToken = \"xoxb-test-token\";\n    private const string _validSlackCode = \"A_test_code\";\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_AllParamsProvided_Succeeds(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Slack;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ISlackService>()\n            .ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())\n            .Returns(_slackToken);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n        var requestAction = await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString());\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .UpsertAsync(Arg.Any<OrganizationIntegration>());\n        Assert.IsType<CreatedResult>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Slack;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.CreateAsync(string.Empty, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Slack;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns((string?)null);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Slack;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n        sutProvider.GetDependency<ISlackService>()\n            .ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())\n            .Returns(string.Empty);\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateEmpty_ThrowsNotFound(\n        SutProvider<SlackIntegrationController> sutProvider)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ISlackService>()\n            .ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())\n            .Returns(_slackToken);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, string.Empty));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateExpired_ThrowsNotFound(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc));\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ISlackService>()\n            .ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())\n            .Returns(_slackToken);\n        var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);\n        timeProvider.Advance(TimeSpan.FromMinutes(30));\n\n        sutProvider.SetDependency<TimeProvider>(timeProvider);\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateHasNonexistentIntegration_ThrowsNotFound(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ISlackService>()\n            .ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())\n            .Returns(_slackToken);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration,\n        OrganizationIntegration wrongOrgIntegration)\n    {\n        wrongOrgIntegration.Id = integration.Id;\n        wrongOrgIntegration.Type = IntegrationType.Slack;\n        wrongOrgIntegration.Configuration = null;\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ISlackService>()\n            .ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())\n            .Returns(_slackToken);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(wrongOrgIntegration);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateHasNonEmptyIntegration_ThrowsNotFound(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Slack;\n        integration.Configuration = \"{}\";\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ISlackService>()\n            .ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())\n            .Returns(_slackToken);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateHasNonSlackIntegration_ThrowsNotFound(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Hec;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ISlackService>()\n            .ObtainTokenViaOAuth(_validSlackCode, Arg.Any<string>())\n            .Returns(_slackToken);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_Success(\n        SutProvider<SlackIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Configuration = null;\n        var expectedUrl = \"https://localhost/\";\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(expectedUrl);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(integration.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(integration.OrganizationId)\n            .Returns([]);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .CreateAsync(Arg.Any<OrganizationIntegration>())\n            .Returns(integration);\n        sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);\n\n        var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        var requestAction = await sutProvider.Sut.RedirectAsync(integration.OrganizationId);\n\n        Assert.IsType<RedirectResult>(requestAction);\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .CreateAsync(Arg.Any<OrganizationIntegration>());\n        sutProvider.GetDependency<ISlackService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_IntegrationAlreadyExistsWithNullConfig_Success(\n        SutProvider<SlackIntegrationController> sutProvider,\n        Guid organizationId,\n        OrganizationIntegration integration)\n    {\n        integration.OrganizationId = organizationId;\n        integration.Configuration = null;\n        integration.Type = IntegrationType.Slack;\n        var expectedUrl = \"https://localhost/\";\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(expectedUrl);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns([integration]);\n        sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);\n\n        var requestAction = await sutProvider.Sut.RedirectAsync(organizationId);\n\n        var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        Assert.IsType<RedirectResult>(requestAction);\n        sutProvider.GetDependency<ISlackService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest(\n        SutProvider<SlackIntegrationController> sutProvider,\n        Guid organizationId,\n        OrganizationIntegration integration)\n    {\n        integration.OrganizationId = organizationId;\n        integration.Configuration = \"{}\";\n        integration.Type = IntegrationType.Slack;\n        var expectedUrl = \"https://localhost/\";\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(expectedUrl);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns([integration]);\n        sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_CallbackUrlReturnsEmpty_ThrowsBadRequest(\n        SutProvider<SlackIntegrationController> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns((string?)null);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound(\n        SutProvider<SlackIntegrationController> sutProvider,\n        Guid organizationId,\n        OrganizationIntegration integration)\n    {\n        integration.OrganizationId = organizationId;\n        integration.Configuration = null;\n        var expectedUrl = \"https://localhost/\";\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"SlackIntegration_Create\"))\n            .Returns(expectedUrl);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns([]);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .CreateAsync(Arg.Any<OrganizationIntegration>())\n            .Returns(integration);\n        sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(string.Empty);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Dirt/Controllers/TeamsIntegrationControllerTests.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Api.Dirt.Controllers;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Models.Data.Teams;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Routing;\nusing Microsoft.Bot.Builder;\nusing Microsoft.Bot.Builder.Integration.AspNet.Core;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Dirt.Controllers;\n\n[ControllerCustomize(typeof(TeamsIntegrationController))]\n[SutProviderCustomize]\npublic class TeamsIntegrationControllerTests\n{\n    private const string _teamsToken = \"test-token\";\n    private const string _validTeamsCode = \"A_test_code\";\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_AllParamsProvided_Succeeds(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Teams;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ITeamsService>()\n            .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())\n            .Returns(_teamsToken);\n        sutProvider.GetDependency<ITeamsService>()\n            .GetJoinedTeamsAsync(_teamsToken)\n            .Returns([\n                new TeamInfo() { DisplayName = \"Test Team\", Id = Guid.NewGuid().ToString(), TenantId = Guid.NewGuid().ToString() }\n            ]);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n        var requestAction = await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString());\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .UpsertAsync(Arg.Any<OrganizationIntegration>());\n        Assert.IsType<CreatedResult>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_CallbackUrlIsEmpty_ThrowsBadRequest(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Teams;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns((string?)null);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Teams;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.CreateAsync(string.Empty, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_NoTeamsFound_ThrowsBadRequest(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Teams;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ITeamsService>()\n            .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())\n            .Returns(_teamsToken);\n        sutProvider.GetDependency<ITeamsService>()\n            .GetJoinedTeamsAsync(_teamsToken)\n            .Returns([]);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_TeamsServiceReturnsEmptyToken_ThrowsBadRequest(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Teams;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n        sutProvider.GetDependency<ITeamsService>()\n            .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())\n            .Returns(string.Empty);\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateEmpty_ThrowsNotFound(\n        SutProvider<TeamsIntegrationController> sutProvider)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ITeamsService>()\n            .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())\n            .Returns(_teamsToken);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, string.Empty));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateExpired_ThrowsNotFound(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc));\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ITeamsService>()\n            .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())\n            .Returns(_teamsToken);\n        var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);\n        timeProvider.Advance(TimeSpan.FromMinutes(30));\n\n        sutProvider.SetDependency<TimeProvider>(timeProvider);\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateHasNonexistentIntegration_ThrowsNotFound(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ITeamsService>()\n            .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())\n            .Returns(_teamsToken);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration,\n        OrganizationIntegration wrongOrgIntegration)\n    {\n        wrongOrgIntegration.Id = integration.Id;\n        wrongOrgIntegration.Type = IntegrationType.Teams;\n        wrongOrgIntegration.Configuration = null;\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ITeamsService>()\n            .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())\n            .Returns(_teamsToken);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(wrongOrgIntegration);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateHasNonEmptyIntegration_ThrowsNotFound(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Teams;\n        integration.Configuration = \"{}\";\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ITeamsService>()\n            .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())\n            .Returns(_teamsToken);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_StateHasNonTeamsIntegration_ThrowsNotFound(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Hec;\n        integration.Configuration = null;\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(\"https://localhost\");\n        sutProvider.GetDependency<ITeamsService>()\n            .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any<string>())\n            .Returns(_teamsToken);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integration.Id)\n            .Returns(integration);\n\n        var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_Success(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Configuration = null;\n        var expectedUrl = \"https://localhost/\";\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(expectedUrl);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(integration.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(integration.OrganizationId)\n            .Returns([]);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .CreateAsync(Arg.Any<OrganizationIntegration>())\n            .Returns(integration);\n        sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);\n\n        var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        var requestAction = await sutProvider.Sut.RedirectAsync(integration.OrganizationId);\n\n        Assert.IsType<RedirectResult>(requestAction);\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .CreateAsync(Arg.Any<OrganizationIntegration>());\n        sutProvider.GetDependency<ITeamsService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_IntegrationAlreadyExistsWithNullConfig_Success(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        Guid organizationId,\n        OrganizationIntegration integration)\n    {\n        integration.OrganizationId = organizationId;\n        integration.Configuration = null;\n        integration.Type = IntegrationType.Teams;\n        var expectedUrl = \"https://localhost/\";\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(expectedUrl);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns([integration]);\n        sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);\n\n        var requestAction = await sutProvider.Sut.RedirectAsync(organizationId);\n\n        var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency<TimeProvider>());\n\n        Assert.IsType<RedirectResult>(requestAction);\n        sutProvider.GetDependency<ITeamsService>().Received(1).GetRedirectUrl(Arg.Any<string>(), expectedState.ToString());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_CallbackUrlIsEmpty_ThrowsBadRequest(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        Guid organizationId,\n        OrganizationIntegration integration)\n    {\n        integration.OrganizationId = organizationId;\n        integration.Configuration = null;\n        integration.Type = IntegrationType.Teams;\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns((string?)null);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns([integration]);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        Guid organizationId,\n        OrganizationIntegration integration)\n    {\n        integration.OrganizationId = organizationId;\n        integration.Configuration = \"{}\";\n        integration.Type = IntegrationType.Teams;\n        var expectedUrl = \"https://localhost/\";\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(expectedUrl);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns([integration]);\n        sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(expectedUrl);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_TeamsServiceReturnsEmpty_ThrowsNotFound(\n        SutProvider<TeamsIntegrationController> sutProvider,\n        Guid organizationId,\n        OrganizationIntegration integration)\n    {\n        integration.OrganizationId = organizationId;\n        integration.Configuration = null;\n        var expectedUrl = \"https://localhost/\";\n\n        sutProvider.Sut.Url = Substitute.For<IUrlHelper>();\n        sutProvider.Sut.Url\n            .RouteUrl(Arg.Is<UrlRouteContext>(c => c.RouteName == \"TeamsIntegration_Create\"))\n            .Returns(expectedUrl);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns([]);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .CreateAsync(Arg.Any<OrganizationIntegration>())\n            .Returns(integration);\n        sutProvider.GetDependency<ITeamsService>().GetRedirectUrl(Arg.Any<string>(), Arg.Any<string>()).Returns(string.Empty);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<TeamsIntegrationController> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task IncomingPostAsync_ForwardsToBot(SutProvider<TeamsIntegrationController> sutProvider)\n    {\n        var adapter = sutProvider.GetDependency<IBotFrameworkHttpAdapter>();\n        var bot = sutProvider.GetDependency<IBot>();\n\n        await sutProvider.Sut.IncomingPostAsync();\n        await adapter.Received(1).ProcessAsync(Arg.Any<HttpRequest>(), Arg.Any<HttpResponse>(), bot);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Dirt/HibpControllerTests.cs",
    "content": "﻿using System.Net;\nusing System.Reflection;\nusing Bit.Api.Dirt.Controllers;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Api.Test.Dirt;\n\n[ControllerCustomize(typeof(HibpController))]\n[SutProviderCustomize]\npublic class HibpControllerTests : IDisposable\n{\n    private readonly HttpClient _originalHttpClient;\n    private readonly FieldInfo _httpClientField;\n\n    public HibpControllerTests()\n    {\n        // Store original HttpClient for restoration\n        _httpClientField = typeof(HibpController).GetField(\"_httpClient\", BindingFlags.Static | BindingFlags.NonPublic);\n        _originalHttpClient = (HttpClient)_httpClientField?.GetValue(null);\n    }\n\n    public void Dispose()\n    {\n        // Restore original HttpClient after tests\n        _httpClientField?.SetValue(null, _originalHttpClient);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_WithMissingApiKey_ThrowsBadRequestException(\n        SutProvider<HibpController> sutProvider,\n        string username)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>().HibpApiKey = null;\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.Get(username));\n        Assert.Equal(\"HaveIBeenPwned API key not set.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_WithValidApiKeyAndNoBreaches_Returns200WithEmptyArray(\n        SutProvider<HibpController> sutProvider,\n        string username,\n        Guid userId)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>().HibpApiKey = \"test-api-key\";\n        var user = new User { Id = userId };\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())\n            .Returns(userId);\n\n        // Mock HttpClient to return 404 (no breaches found)\n        var mockHttpClient = CreateMockHttpClient(HttpStatusCode.NotFound, \"\");\n        _httpClientField.SetValue(null, mockHttpClient);\n\n        // Act\n        var result = await sutProvider.Sut.Get(username);\n\n        // Assert\n        var contentResult = Assert.IsType<ContentResult>(result);\n        Assert.Equal(\"[]\", contentResult.Content);\n        Assert.Equal(\"application/json\", contentResult.ContentType);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_WithValidApiKeyAndBreachesFound_Returns200WithBreachData(\n        SutProvider<HibpController> sutProvider,\n        string username,\n        Guid userId)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>().HibpApiKey = \"test-api-key\";\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())\n            .Returns(userId);\n\n        var breachData = \"[{\\\"Name\\\":\\\"Adobe\\\",\\\"Title\\\":\\\"Adobe\\\",\\\"Domain\\\":\\\"adobe.com\\\"}]\";\n        var mockHttpClient = CreateMockHttpClient(HttpStatusCode.OK, breachData);\n        _httpClientField.SetValue(null, mockHttpClient);\n\n        // Act\n        var result = await sutProvider.Sut.Get(username);\n\n        // Assert\n        var contentResult = Assert.IsType<ContentResult>(result);\n        Assert.Equal(breachData, contentResult.Content);\n        Assert.Equal(\"application/json\", contentResult.ContentType);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_WithRateLimiting_RetriesWithDelay(\n        SutProvider<HibpController> sutProvider,\n        string username,\n        Guid userId)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>().HibpApiKey = \"test-api-key\";\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())\n            .Returns(userId);\n\n        // First response is rate limited, second is success\n        var requestCount = 0;\n        var mockHandler = new MockHttpMessageHandler((request, cancellationToken) =>\n        {\n            requestCount++;\n            if (requestCount == 1)\n            {\n                var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests);\n                response.Headers.Add(\"retry-after\", \"1\");\n                return Task.FromResult(response);\n            }\n            else\n            {\n                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)\n                {\n                    Content = new StringContent(\"\")\n                });\n            }\n        });\n\n        var mockHttpClient = new HttpClient(mockHandler);\n        _httpClientField.SetValue(null, mockHttpClient);\n\n        // Act\n        var result = await sutProvider.Sut.Get(username);\n\n        // Assert\n        Assert.Equal(2, requestCount); // Verify retry happened\n        var contentResult = Assert.IsType<ContentResult>(result);\n        Assert.Equal(\"[]\", contentResult.Content);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_WithServerError_ThrowsBadRequestException(\n        SutProvider<HibpController> sutProvider,\n        string username,\n        Guid userId)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>().HibpApiKey = \"test-api-key\";\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())\n            .Returns(userId);\n\n        var mockHttpClient = CreateMockHttpClient(HttpStatusCode.InternalServerError, \"\");\n        _httpClientField.SetValue(null, mockHttpClient);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.Get(username));\n        Assert.Contains(\"Request failed. Status code:\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_WithBadRequest_ThrowsBadRequestException(\n        SutProvider<HibpController> sutProvider,\n        string username,\n        Guid userId)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>().HibpApiKey = \"test-api-key\";\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())\n            .Returns(userId);\n\n        var mockHttpClient = CreateMockHttpClient(HttpStatusCode.BadRequest, \"\");\n        _httpClientField.SetValue(null, mockHttpClient);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.Get(username));\n        Assert.Contains(\"Request failed. Status code:\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_EncodesUsernameCorrectly(\n        SutProvider<HibpController> sutProvider,\n        Guid userId)\n    {\n        // Arrange\n        var usernameWithSpecialChars = \"test+user@example.com\";\n        sutProvider.GetDependency<GlobalSettings>().HibpApiKey = \"test-api-key\";\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())\n            .Returns(userId);\n\n        string capturedUrl = null;\n        var mockHandler = new MockHttpMessageHandler((request, cancellationToken) =>\n        {\n            capturedUrl = request.RequestUri.ToString();\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)\n            {\n                Content = new StringContent(\"\")\n            });\n        });\n\n        var mockHttpClient = new HttpClient(mockHandler);\n        _httpClientField.SetValue(null, mockHttpClient);\n\n        // Act\n        await sutProvider.Sut.Get(usernameWithSpecialChars);\n\n        // Assert\n        Assert.NotNull(capturedUrl);\n        // Username should be URL encoded (+ becomes %2B, @ becomes %40)\n        Assert.Contains(\"test%2Buser%40example.com\", capturedUrl);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendAsync_IncludesRequiredHeaders(\n        SutProvider<HibpController> sutProvider,\n        string username,\n        Guid userId)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>().HibpApiKey = \"test-api-key\";\n        sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())\n            .Returns(userId);\n\n        HttpRequestMessage capturedRequest = null;\n        var mockHandler = new MockHttpMessageHandler((request, cancellationToken) =>\n        {\n            capturedRequest = request;\n            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)\n            {\n                Content = new StringContent(\"\")\n            });\n        });\n\n        var mockHttpClient = new HttpClient(mockHandler);\n        _httpClientField.SetValue(null, mockHttpClient);\n\n        // Act\n        await sutProvider.Sut.Get(username);\n\n        // Assert\n        Assert.NotNull(capturedRequest);\n        Assert.True(capturedRequest.Headers.Contains(\"hibp-api-key\"));\n        Assert.True(capturedRequest.Headers.Contains(\"hibp-client-id\"));\n        Assert.True(capturedRequest.Headers.Contains(\"User-Agent\"));\n        Assert.Equal(\"Bitwarden\", capturedRequest.Headers.GetValues(\"User-Agent\").First());\n    }\n\n    /// <summary>\n    /// Helper to create a mock HttpClient that returns a specific status code and content\n    /// </summary>\n    private HttpClient CreateMockHttpClient(HttpStatusCode statusCode, string content)\n    {\n        var mockHandler = new MockHttpMessageHandler((request, cancellationToken) =>\n        {\n            return Task.FromResult(new HttpResponseMessage(statusCode)\n            {\n                Content = new StringContent(content)\n            });\n        });\n\n        return new HttpClient(mockHandler);\n    }\n}\n\n/// <summary>\n/// Mock HttpMessageHandler for testing HttpClient behavior\n/// </summary>\npublic class MockHttpMessageHandler : HttpMessageHandler\n{\n    private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsync;\n\n    public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsync)\n    {\n        _sendAsync = sendAsync;\n    }\n\n    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        return _sendAsync(request, cancellationToken);\n    }\n}\n\n"
  },
  {
    "path": "test/Api.Test/Dirt/Models/Request/OrganizationIntegrationRequestModelTests.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing System.Text.Json;\nusing Bit.Api.Dirt.Models.Request;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.Dirt.Models.Request;\n\npublic class OrganizationIntegrationRequestModelTests\n{\n    [Fact]\n    public void ToOrganizationIntegration_CreatesNewOrganizationIntegration()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Hec,\n            Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri(\"http://localhost\"), Scheme: \"Bearer\", Token: \"Token\"))\n        };\n\n        var organizationId = Guid.NewGuid();\n        var organizationIntegration = model.ToOrganizationIntegration(organizationId);\n\n        Assert.Equal(organizationIntegration.Type, model.Type);\n        Assert.Equal(organizationIntegration.Configuration, model.Configuration);\n        Assert.Equal(organizationIntegration.OrganizationId, organizationId);\n    }\n\n    [Theory, BitAutoData]\n    public void ToOrganizationIntegration_UpdatesExistingOrganizationIntegration(OrganizationIntegration integration)\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Hec,\n            Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri(\"http://localhost\"), Scheme: \"Bearer\", Token: \"Token\"))\n        };\n\n        var organizationIntegration = model.ToOrganizationIntegration(integration);\n\n        Assert.Equal(organizationIntegration.Configuration, model.Configuration);\n    }\n\n    [Fact]\n    public void Validate_CloudBillingSync_ReturnsNotYetSupportedError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.CloudBillingSync,\n            Configuration = null\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Type), results[0].MemberNames);\n        Assert.Contains(\"not yet supported\", results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Scim_ReturnsNotYetSupportedError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Scim,\n            Configuration = null\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Type), results[0].MemberNames);\n        Assert.Contains(\"not yet supported\", results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Slack_ReturnsCannotBeCreatedDirectlyError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Slack,\n            Configuration = null\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Type), results[0].MemberNames);\n        Assert.Contains(\"cannot be created directly\", results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Teams_ReturnsCannotBeCreatedDirectlyError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Teams,\n            Configuration = null\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Type), results[0].MemberNames);\n        Assert.Contains(\"cannot be created directly\", results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Webhook_WithNullConfiguration_ReturnsNoErrors()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Webhook,\n            Configuration = null\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_Webhook_WithInvalidConfiguration_ReturnsConfigurationError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Webhook,\n            Configuration = \"something\"\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Configuration), results[0].MemberNames);\n        Assert.Contains(\"Must include valid\", results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Webhook_WithValidConfiguration_ReturnsNoErrors()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Webhook,\n            Configuration = JsonSerializer.Serialize(new WebhookIntegration(new Uri(\"https://example.com\")))\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_Hec_WithNullConfiguration_ReturnsError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Hec,\n            Configuration = null\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Configuration), results[0].MemberNames);\n        Assert.Contains(\"Must include valid\", results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Hec_WithInvalidConfiguration_ReturnsError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Hec,\n            Configuration = \"Not valid\"\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Configuration), results[0].MemberNames);\n        Assert.Contains(\"Must include valid\", results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Hec_WithValidConfiguration_ReturnsNoErrors()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Hec,\n            Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri(\"http://localhost\"), Scheme: \"Bearer\", Token: \"Token\"))\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_Datadog_WithNullConfiguration_ReturnsError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Datadog,\n            Configuration = null\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Configuration), results[0].MemberNames);\n        Assert.Contains(\"Must include valid\", results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Datadog_WithInvalidConfiguration_ReturnsError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Datadog,\n            Configuration = \"Not valid\"\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Configuration), results[0].MemberNames);\n        Assert.Contains(\"Must include valid\", results[0].ErrorMessage);\n    }\n\n    [Fact]\n    public void Validate_Datadog_WithValidConfiguration_ReturnsNoErrors()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = IntegrationType.Datadog,\n            Configuration = JsonSerializer.Serialize(\n                new DatadogIntegration(ApiKey: \"API1234\", Uri: new Uri(\"http://localhost\"))\n            )\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_UnknownIntegrationType_ReturnsUnrecognizedError()\n    {\n        var model = new OrganizationIntegrationRequestModel\n        {\n            Type = (IntegrationType)999,\n            Configuration = null\n        };\n\n        var results = model.Validate(new ValidationContext(model)).ToList();\n\n        Assert.Single(results);\n        Assert.Contains(nameof(model.Type), results[0].MemberNames);\n        Assert.Contains(\"not recognized\", results[0].ErrorMessage);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Dirt/Models/Response/OrganizationIntegrationResponseModelTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Text.Json;\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Models.Data.Teams;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.Dirt.Models.Response;\n\npublic class OrganizationIntegrationResponseModelTests\n{\n    [Theory, BitAutoData]\n    public void Status_CloudBillingSync_AlwaysNotApplicable(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.CloudBillingSync;\n        oi.Configuration = null;\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n        Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);\n\n        model.Configuration = \"{}\";\n        Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Scim_AlwaysNotApplicable(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Scim;\n        oi.Configuration = null;\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n        Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);\n\n        model.Configuration = \"{}\";\n        Assert.Equal(OrganizationIntegrationStatus.NotApplicable, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Slack_NullConfig_ReturnsInitiated(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Slack;\n        oi.Configuration = null;\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n\n        Assert.Equal(OrganizationIntegrationStatus.Initiated, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Slack_WithConfig_ReturnsCompleted(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Slack;\n        oi.Configuration = \"{}\";\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n\n        Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Teams_NullConfig_ReturnsInitiated(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Teams;\n        oi.Configuration = null;\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n\n        Assert.Equal(OrganizationIntegrationStatus.Initiated, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Teams_WithTenantAndTeamsConfig_ReturnsInProgress(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Teams;\n        oi.Configuration = JsonSerializer.Serialize(new TeamsIntegration(\n            TenantId: \"tenant\", Teams: [new TeamInfo() { DisplayName = \"Team\", Id = \"TeamId\", TenantId = \"tenant\" }]\n        ));\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n\n        Assert.Equal(OrganizationIntegrationStatus.InProgress, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Teams_WithCompletedConfig_ReturnsCompleted(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Teams;\n        oi.Configuration = JsonSerializer.Serialize(new TeamsIntegration(\n            TenantId: \"tenant\",\n            Teams: [new TeamInfo() { DisplayName = \"Team\", Id = \"TeamId\", TenantId = \"tenant\" }],\n            ServiceUrl: new Uri(\"https://example.com\"),\n            ChannelId: \"channellId\"\n        ));\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n\n        Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Webhook_AlwaysCompleted(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Webhook;\n        oi.Configuration = null;\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n        Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);\n\n        model.Configuration = \"{}\";\n        Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Hec_NullConfig_ReturnsInvalid(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Hec;\n        oi.Configuration = null;\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n\n        Assert.Equal(OrganizationIntegrationStatus.Invalid, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Hec_WithConfig_ReturnsCompleted(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Hec;\n        oi.Configuration = \"{}\";\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n\n        Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Datadog_NullConfig_ReturnsInvalid(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Datadog;\n        oi.Configuration = null;\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n\n        Assert.Equal(OrganizationIntegrationStatus.Invalid, model.Status);\n    }\n\n    [Theory, BitAutoData]\n    public void Status_Datadog_WithConfig_ReturnsCompleted(OrganizationIntegration oi)\n    {\n        oi.Type = IntegrationType.Datadog;\n        oi.Configuration = \"{}\";\n\n        var model = new OrganizationIntegrationResponseModel(oi);\n\n        Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Dirt/OrganizationReportsControllerTests.cs",
    "content": "﻿using Bit.Api.Dirt.Controllers;\nusing Bit.Api.Dirt.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Dirt;\n\n[ControllerCustomize(typeof(OrganizationReportsController))]\n[SutProviderCustomize]\npublic class OrganizationReportControllerTests\n{\n    #region Whole OrganizationReport Endpoints\n\n    [Theory, BitAutoData]\n    public async Task GetLatestOrganizationReportAsync_WithValidOrgId_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .GetLatestOrganizationReportAsync(orgId)\n            .Returns(expectedReport);\n\n        // Act\n        var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(orgId);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        var expectedResponse = new OrganizationReportResponseModel(expectedReport);\n        Assert.Equivalent(expectedResponse, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetLatestOrganizationReportAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(Task.FromResult(false));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetLatestOrganizationReportAsync(orgId));\n\n        // Verify that the query was not called\n        await sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .DidNotReceive()\n            .GetLatestOrganizationReportAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetLatestOrganizationReportAsync_WhenNoReportFound_ReturnsOkWithNull(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .GetLatestOrganizationReportAsync(orgId)\n            .Returns((OrganizationReport)null);\n\n        // Act\n        var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(orgId);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        Assert.Null(okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetLatestOrganizationReportAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .GetLatestOrganizationReportAsync(orgId)\n            .Returns(expectedReport);\n\n        // Act\n        await sutProvider.Sut.GetLatestOrganizationReportAsync(orgId);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .Received(1)\n            .GetLatestOrganizationReportAsync(orgId);\n    }\n\n\n\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportAsync_WithValidIds_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        expectedReport.OrganizationId = orgId;\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .GetOrganizationReportAsync(reportId)\n            .Returns(expectedReport);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        Assert.Equal(expectedReport, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(Task.FromResult(false));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId));\n\n        // Verify that the query was not called\n        await sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .DidNotReceive()\n            .GetOrganizationReportAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportAsync_WhenReportNotFound_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .GetOrganizationReportAsync(reportId)\n            .Returns((OrganizationReport)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId));\n\n        Assert.Equal(\"Report not found for the specified organization.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        expectedReport.OrganizationId = orgId;\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .GetOrganizationReportAsync(reportId)\n            .Returns(expectedReport);\n\n        // Act\n        await sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .Received(1)\n            .GetOrganizationReportAsync(reportId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportAsync_WithValidAccess_UsesCorrectReportId(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        expectedReport.OrganizationId = orgId;\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .GetOrganizationReportAsync(reportId)\n            .Returns(expectedReport);\n\n        // Act\n        await sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId);\n\n        // Assert\n        await sutProvider.GetDependency<IGetOrganizationReportQuery>()\n            .Received(1)\n            .GetOrganizationReportAsync(reportId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateOrganizationReportAsync_WithValidRequest_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        AddOrganizationReportRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IAddOrganizationReportCommand>()\n            .AddOrganizationReportAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        var result = await sutProvider.Sut.CreateOrganizationReportAsync(orgId, request);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        var expectedResponse = new OrganizationReportResponseModel(expectedReport);\n        Assert.Equivalent(expectedResponse, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateOrganizationReportAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        AddOrganizationReportRequest request)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.CreateOrganizationReportAsync(orgId, request));\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IAddOrganizationReportCommand>()\n            .DidNotReceive()\n            .AddOrganizationReportAsync(Arg.Any<AddOrganizationReportRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateOrganizationReportAsync_WithMismatchedOrgId_ThrowsBadRequestException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        AddOrganizationReportRequest request)\n    {\n        // Arrange\n        request.OrganizationId = Guid.NewGuid(); // Different from orgId\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CreateOrganizationReportAsync(orgId, request));\n\n        Assert.Equal(\"Organization ID in the request body must match the route parameter\", exception.Message);\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IAddOrganizationReportCommand>()\n            .DidNotReceive()\n            .AddOrganizationReportAsync(Arg.Any<AddOrganizationReportRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateOrganizationReportAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        AddOrganizationReportRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IAddOrganizationReportCommand>()\n            .AddOrganizationReportAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        await sutProvider.Sut.CreateOrganizationReportAsync(orgId, request);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IAddOrganizationReportCommand>()\n            .Received(1)\n            .AddOrganizationReportAsync(request);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithValidRequest_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        UpdateOrganizationReportRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUpdateOrganizationReportCommand>()\n            .UpdateOrganizationReportAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        var result = await sutProvider.Sut.UpdateOrganizationReportAsync(orgId, request);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        var expectedResponse = new OrganizationReportResponseModel(expectedReport);\n        Assert.Equivalent(expectedResponse, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        UpdateOrganizationReportRequest request)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportAsync(orgId, request));\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportAsync(Arg.Any<UpdateOrganizationReportRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithMismatchedOrgId_ThrowsBadRequestException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        UpdateOrganizationReportRequest request)\n    {\n        // Arrange\n        request.OrganizationId = Guid.NewGuid(); // Different from orgId\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportAsync(orgId, request));\n\n        Assert.Equal(\"Organization ID in the request body must match the route parameter\", exception.Message);\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportAsync(Arg.Any<UpdateOrganizationReportRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        UpdateOrganizationReportRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUpdateOrganizationReportCommand>()\n            .UpdateOrganizationReportAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationReportAsync(orgId, request);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IUpdateOrganizationReportCommand>()\n            .Received(1)\n            .UpdateOrganizationReportAsync(request);\n    }\n\n    #endregion\n\n    #region SummaryData Field Endpoints\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithValidParameters_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        DateTime startDate,\n        DateTime endDate,\n        List<OrganizationReportSummaryDataResponse> expectedSummaryData)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportSummaryDataByDateRangeQuery>()\n            .GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate)\n            .Returns(expectedSummaryData);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        Assert.Equal(expectedSummaryData, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        DateTime startDate,\n        DateTime endDate)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate));\n\n        // Verify that the query was not called\n        await sutProvider.GetDependency<IGetOrganizationReportSummaryDataByDateRangeQuery>()\n            .DidNotReceive()\n            .GetOrganizationReportSummaryDataByDateRangeAsync(Arg.Any<Guid>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportSummaryDataByDateRangeAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        DateTime startDate,\n        DateTime endDate,\n        List<OrganizationReportSummaryDataResponse> expectedSummaryData)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportSummaryDataByDateRangeQuery>()\n            .GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate)\n            .Returns(expectedSummaryData);\n\n        // Act\n        await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IGetOrganizationReportSummaryDataByDateRangeQuery>()\n            .Received(1)\n            .GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportSummaryAsync_WithValidIds_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        OrganizationReportSummaryDataResponse expectedSummaryData)\n    {\n        // Arrange\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportSummaryDataQuery>()\n            .GetOrganizationReportSummaryDataAsync(orgId, reportId)\n            .Returns(expectedSummaryData);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportSummaryAsync(orgId, reportId);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        Assert.Equal(expectedSummaryData, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportSummaryAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetOrganizationReportSummaryAsync(orgId, reportId));\n\n        // Verify that the query was not called\n        await sutProvider.GetDependency<IGetOrganizationReportSummaryDataQuery>()\n            .DidNotReceive()\n            .GetOrganizationReportSummaryDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithValidRequest_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportSummaryRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n        request.ReportId = reportId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUpdateOrganizationReportSummaryCommand>()\n            .UpdateOrganizationReportSummaryAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        var result = await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        var expectedResponse = new OrganizationReportResponseModel(expectedReport);\n        Assert.Equivalent(expectedResponse, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportSummaryRequest request)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request));\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportSummaryCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportSummaryAsync(Arg.Any<UpdateOrganizationReportSummaryRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithMismatchedOrgId_ThrowsBadRequestException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportSummaryRequest request)\n    {\n        // Arrange\n        request.OrganizationId = Guid.NewGuid(); // Different from orgId\n        request.ReportId = reportId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request));\n\n        Assert.Equal(\"Organization ID in the request body must match the route parameter\", exception.Message);\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportSummaryCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportSummaryAsync(Arg.Any<UpdateOrganizationReportSummaryRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithMismatchedReportId_ThrowsBadRequestException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportSummaryRequest request)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n        request.ReportId = Guid.NewGuid(); // Different from reportId\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request));\n\n        Assert.Equal(\"Report ID in the request body must match the route parameter\", exception.Message);\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportSummaryCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportSummaryAsync(Arg.Any<UpdateOrganizationReportSummaryRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportSummaryRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n        request.ReportId = reportId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUpdateOrganizationReportSummaryCommand>()\n            .UpdateOrganizationReportSummaryAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IUpdateOrganizationReportSummaryCommand>()\n            .Received(1)\n            .UpdateOrganizationReportSummaryAsync(request);\n    }\n\n    #endregion\n\n    #region ReportData Field Endpoints\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportDataAsync_WithValidIds_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        OrganizationReportDataResponse expectedReportData)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportDataQuery>()\n            .GetOrganizationReportDataAsync(orgId, reportId)\n            .Returns(expectedReportData);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportDataAsync(orgId, reportId);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        Assert.Equal(expectedReportData, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportDataAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetOrganizationReportDataAsync(orgId, reportId));\n\n        // Verify that the query was not called\n        await sutProvider.GetDependency<IGetOrganizationReportDataQuery>()\n            .DidNotReceive()\n            .GetOrganizationReportDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportDataAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        OrganizationReportDataResponse expectedReportData)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportDataQuery>()\n            .GetOrganizationReportDataAsync(orgId, reportId)\n            .Returns(expectedReportData);\n\n        // Act\n        await sutProvider.Sut.GetOrganizationReportDataAsync(orgId, reportId);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IGetOrganizationReportDataQuery>()\n            .Received(1)\n            .GetOrganizationReportDataAsync(orgId, reportId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithValidRequest_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportDataRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n        request.ReportId = reportId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUpdateOrganizationReportDataCommand>()\n            .UpdateOrganizationReportDataAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        var result = await sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        var expectedResponse = new OrganizationReportResponseModel(expectedReport);\n        Assert.Equivalent(expectedResponse, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportDataRequest request)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request));\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportDataCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportDataAsync(Arg.Any<UpdateOrganizationReportDataRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithMismatchedOrgId_ThrowsBadRequestException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportDataRequest request)\n    {\n        // Arrange\n        request.OrganizationId = Guid.NewGuid(); // Different from orgId\n        request.ReportId = reportId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request));\n\n        Assert.Equal(\"Organization ID in the request body must match the route parameter\", exception.Message);\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportDataCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportDataAsync(Arg.Any<UpdateOrganizationReportDataRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithMismatchedReportId_ThrowsBadRequestException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportDataRequest request)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n        request.ReportId = Guid.NewGuid(); // Different from reportId\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request));\n\n        Assert.Equal(\"Report ID in the request body must match the route parameter\", exception.Message);\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportDataCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportDataAsync(Arg.Any<UpdateOrganizationReportDataRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportDataRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n        request.ReportId = reportId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUpdateOrganizationReportDataCommand>()\n            .UpdateOrganizationReportDataAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IUpdateOrganizationReportDataCommand>()\n            .Received(1)\n            .UpdateOrganizationReportDataAsync(request);\n    }\n\n    #endregion\n\n    #region ApplicationData Field Endpoints\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportApplicationDataAsync_WithValidIds_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        OrganizationReportApplicationDataResponse expectedApplicationData)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportApplicationDataQuery>()\n            .GetOrganizationReportApplicationDataAsync(orgId, reportId)\n            .Returns(expectedApplicationData);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(orgId, reportId);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        Assert.Equal(expectedApplicationData, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportApplicationDataAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetOrganizationReportApplicationDataAsync(orgId, reportId));\n\n        // Verify that the query was not called\n        await sutProvider.GetDependency<IGetOrganizationReportApplicationDataQuery>()\n            .DidNotReceive()\n            .GetOrganizationReportApplicationDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportApplicationDataAsync_WhenApplicationDataNotFound_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportApplicationDataQuery>()\n            .GetOrganizationReportApplicationDataAsync(orgId, reportId)\n            .Returns((OrganizationReportApplicationDataResponse)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetOrganizationReportApplicationDataAsync(orgId, reportId));\n\n        Assert.Equal(\"Organization report application data not found.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationReportApplicationDataAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        OrganizationReportApplicationDataResponse expectedApplicationData)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationReportApplicationDataQuery>()\n            .GetOrganizationReportApplicationDataAsync(orgId, reportId)\n            .Returns(expectedApplicationData);\n\n        // Act\n        await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(orgId, reportId);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IGetOrganizationReportApplicationDataQuery>()\n            .Received(1)\n            .GetOrganizationReportApplicationDataAsync(orgId, reportId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithValidRequest_ReturnsOkResult(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportApplicationDataRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n        request.Id = reportId;\n        expectedReport.Id = request.Id;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUpdateOrganizationReportApplicationDataCommand>()\n            .UpdateOrganizationReportApplicationDataAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        var result = await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request);\n\n        // Assert\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        var expectedResponse = new OrganizationReportResponseModel(expectedReport);\n        Assert.Equivalent(expectedResponse, okResult.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithoutAccess_ThrowsNotFoundException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportApplicationDataRequest request)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request));\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportApplicationDataCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportApplicationDataAsync(Arg.Any<UpdateOrganizationReportApplicationDataRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithMismatchedOrgId_ThrowsBadRequestException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportApplicationDataRequest request)\n    {\n        // Arrange\n        request.OrganizationId = Guid.NewGuid(); // Different from orgId\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request));\n\n        Assert.Equal(\"Organization ID in the request body must match the route parameter\", exception.Message);\n\n        // Verify that the command was not called\n        await sutProvider.GetDependency<IUpdateOrganizationReportApplicationDataCommand>()\n            .DidNotReceive()\n            .UpdateOrganizationReportApplicationDataAsync(Arg.Any<UpdateOrganizationReportApplicationDataRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithMismatchedReportId_ThrowsBadRequestException(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportApplicationDataRequest request,\n        OrganizationReport updatedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUpdateOrganizationReportApplicationDataCommand>()\n            .UpdateOrganizationReportApplicationDataAsync(request)\n            .Returns(updatedReport);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request));\n\n        Assert.Equal(\"Report ID in the request body must match the route parameter\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_CallsCorrectMethods(\n        SutProvider<OrganizationReportsController> sutProvider,\n        Guid orgId,\n        Guid reportId,\n        UpdateOrganizationReportApplicationDataRequest request,\n        OrganizationReport expectedReport)\n    {\n        // Arrange\n        request.OrganizationId = orgId;\n        request.Id = reportId;\n        expectedReport.Id = reportId;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessReports(orgId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IUpdateOrganizationReportApplicationDataCommand>()\n            .UpdateOrganizationReportApplicationDataAsync(request)\n            .Returns(expectedReport);\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request);\n\n        // Assert\n        await sutProvider.GetDependency<ICurrentContext>()\n            .Received(1)\n            .AccessReports(orgId);\n\n        await sutProvider.GetDependency<IUpdateOrganizationReportApplicationDataCommand>()\n            .Received(1)\n            .UpdateOrganizationReportApplicationDataAsync(request);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Api.Test/Dirt/ReportsControllerTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Api.Dirt.Controllers;\nusing Bit.Api.Dirt.Models;\nusing Bit.Core.Context;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Dirt;\n\n\n[ControllerCustomize(typeof(ReportsController))]\n[SutProviderCustomize]\npublic class ReportsControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task GetPasswordHealthReportApplicationAsync_Success(SutProvider<ReportsController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);\n\n        // Act\n        var orgId = Guid.NewGuid();\n        var result = await sutProvider.Sut.GetPasswordHealthReportApplications(orgId);\n\n        // Assert\n        _ = sutProvider.GetDependency<IGetPasswordHealthReportApplicationQuery>()\n            .Received(1)\n            .GetPasswordHealthReportApplicationAsync(Arg.Is<Guid>(_ => _ == orgId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPasswordHealthReportApplicationAsync_withoutAccess(SutProvider<ReportsController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);\n\n        // Act & Assert\n        var orgId = Guid.NewGuid();\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetPasswordHealthReportApplications(orgId));\n\n        // Assert\n        _ = sutProvider.GetDependency<IGetPasswordHealthReportApplicationQuery>()\n            .Received(0);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddPasswordHealthReportApplicationAsync_withAccess_success(SutProvider<ReportsController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);\n\n        // Act\n        var request = new PasswordHealthReportApplicationModel\n        {\n            OrganizationId = Guid.NewGuid(),\n            Url = \"https://example.com\",\n        };\n        await sutProvider.Sut.AddPasswordHealthReportApplication(request);\n\n        // Assert\n        _ = sutProvider.GetDependency<IAddPasswordHealthReportApplicationCommand>()\n            .Received(1)\n            .AddPasswordHealthReportApplicationAsync(Arg.Is<AddPasswordHealthReportApplicationRequest>(_ =>\n                _.OrganizationId == request.OrganizationId && _.Url == request.Url));\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddPasswordHealthReportApplicationAsync_multiple_withAccess_success(\n        SutProvider<ReportsController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);\n\n        // Act\n        var fixture = new Fixture();\n        var request = fixture.CreateMany<PasswordHealthReportApplicationModel>(2);\n        await sutProvider.Sut.AddPasswordHealthReportApplications(request);\n\n        // Assert\n        _ = sutProvider.GetDependency<IAddPasswordHealthReportApplicationCommand>()\n            .Received(1)\n            .AddPasswordHealthReportApplicationAsync(Arg.Any<IEnumerable<AddPasswordHealthReportApplicationRequest>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task AddPasswordHealthReportApplicationAsync_withoutAccess(SutProvider<ReportsController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);\n\n        // Act\n        var request = new PasswordHealthReportApplicationModel\n        {\n            OrganizationId = Guid.NewGuid(),\n            Url = \"https://example.com\",\n        };\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n                await sutProvider.Sut.AddPasswordHealthReportApplication(request));\n\n        // Assert\n        _ = sutProvider.GetDependency<IAddPasswordHealthReportApplicationCommand>()\n            .Received(0);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DropPasswordHealthReportApplicationAsync_withoutAccess(SutProvider<ReportsController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(false);\n\n        // Act\n        var fixture = new Fixture();\n        var request = fixture.Create<PasswordHealthReportApplicationModel>();\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n                await sutProvider.Sut.AddPasswordHealthReportApplication(request));\n\n        // Assert\n        _ = sutProvider.GetDependency<IDropPasswordHealthReportApplicationCommand>()\n            .Received(0);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DropPasswordHealthReportApplicationAsync_withAccess_success(SutProvider<ReportsController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(Arg.Any<Guid>()).Returns(true);\n\n        // Act\n        var fixture = new Fixture();\n        var request = fixture.Create<DropPasswordHealthReportApplicationRequest>();\n        await sutProvider.Sut.DropPasswordHealthReportApplication(request);\n\n        // Assert\n        _ = sutProvider.GetDependency<IDropPasswordHealthReportApplicationCommand>()\n            .Received(1)\n            .DropPasswordHealthReportApplicationAsync(Arg.Is<DropPasswordHealthReportApplicationRequest>(_ =>\n                _.OrganizationId == request.OrganizationId &&\n                _.PasswordHealthReportApplicationIds == request.PasswordHealthReportApplicationIds));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs",
    "content": "﻿#nullable enable\nusing System.Security.Claims;\nusing Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.Auth.Models.Request;\nusing Bit.Api.Auth.Models.Request.WebAuthn;\nusing Bit.Api.KeyManagement.Controllers;\nusing Bit.Api.KeyManagement.Models.Requests;\nusing Bit.Api.KeyManagement.Validators;\nusing Bit.Api.Tools.Models.Request;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Commands.Interfaces;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Controllers;\n\n[ControllerCustomize(typeof(AccountsKeyManagementController))]\n[SutProviderCustomize]\n[JsonDocumentCustomize]\npublic class AccountsKeyManagementControllerTests\n{\n    private static readonly string _mockEncryptedType2String =\n        \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n    private static readonly string _mockEncryptedType7String = \"7.AOs41Hd8OQiCPXjyJKCiDA==\";\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegenerateKeysAsync_UserNull_Throws(SutProvider<AccountsKeyManagementController> sutProvider,\n        KeyRegenerationRequestModel data)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.RegenerateKeysAsync(data));\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().ReceivedWithAnyArgs(0)\n            .GetManyByUserAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IEmergencyAccessRepository>().ReceivedWithAnyArgs(0)\n            .GetManyDetailsByGranteeIdAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().ReceivedWithAnyArgs(0)\n            .RegenerateKeysAsync(Arg.Any<UserAsymmetricKeys>(),\n                Arg.Any<ICollection<OrganizationUser>>(),\n                Arg.Any<ICollection<EmergencyAccessDetails>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegenerateKeysAsync_Success(SutProvider<AccountsKeyManagementController> sutProvider,\n        KeyRegenerationRequestModel data, User user, ICollection<OrganizationUser> orgUsers,\n        ICollection<EmergencyAccessDetails> accessDetails)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(Arg.Is(user.Id)).Returns(orgUsers);\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id))\n            .Returns(accessDetails);\n\n        await sutProvider.Sut.RegenerateKeysAsync(data);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)\n            .GetManyByUserAsync(Arg.Is(user.Id));\n        await sutProvider.GetDependency<IEmergencyAccessRepository>().Received(1)\n            .GetManyDetailsByGranteeIdAsync(Arg.Is(user.Id));\n        await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().Received(1)\n            .RegenerateKeysAsync(\n                Arg.Is<UserAsymmetricKeys>(u =>\n                    u.UserId == user.Id && u.PublicKey == data.UserPublicKey &&\n                    u.UserKeyEncryptedPrivateKey == data.UserKeyEncryptedUserPrivateKey),\n                Arg.Is(orgUsers),\n                Arg.Is(accessDetails));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_UserCryptoV1_Success(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        RotateUserAccountKeysAndDataRequestModel data, User user)\n    {\n        data.AccountKeys.SignatureKeyPair = null;\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())\n            .Returns(IdentityResult.Success);\n        await sutProvider.Sut.RotateUserAccountKeysAsync(data);\n\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData));\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.OrganizationAccountRecoveryUnlockData));\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.PasskeyUnlockData));\n\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Ciphers));\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Folders));\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Sends));\n\n        await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)\n            .RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>\n                d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash\n\n                && d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType\n                && d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations\n                && d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory\n                && d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism\n                && d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email\n\n                && d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash\n                && d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey\n\n                && d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey\n                && d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey\n            ));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_UserCryptoV2_Success_Async(SutProvider<AccountsKeyManagementController> sutProvider,\n    RotateUserAccountKeysAndDataRequestModel data, User user)\n    {\n        data.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel\n        {\n            SignatureAlgorithm = \"ed25519\",\n            WrappedSigningKey = \"wrappedSigningKey\",\n            VerifyingKey = \"verifyingKey\"\n        };\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())\n            .Returns(IdentityResult.Success);\n        await sutProvider.Sut.RotateUserAccountKeysAsync(data);\n\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData));\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.OrganizationAccountRecoveryUnlockData));\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.PasskeyUnlockData));\n\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Ciphers));\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Folders));\n        await sutProvider.GetDependency<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>().Received(1)\n            .ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Sends));\n\n        await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)\n            .RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>\n                d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash\n\n                && d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType\n                && d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations\n                && d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory\n                && d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism\n                && d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email\n\n                && d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash\n                && d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey\n\n                && d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey\n                && d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey\n                && d.AccountKeys!.PublicKeyEncryptionKeyPairData.SignedPublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey\n                && d.AccountKeys!.SignatureKeyPairData!.SignatureAlgorithm == Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519\n                && d.AccountKeys!.SignatureKeyPairData.WrappedSigningKey == data.AccountKeys.SignatureKeyPair!.WrappedSigningKey\n                && d.AccountKeys!.SignatureKeyPairData.VerifyingKey == data.AccountKeys.SignatureKeyPair!.VerifyingKey\n            ));\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserKeyNoUser_Throws(SutProvider<AccountsKeyManagementController> sutProvider,\n        RotateUserAccountKeysAndDataRequestModel data)\n    {\n        data.AccountKeys.SignatureKeyPair = null;\n        User? user = null;\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())\n            .Returns(IdentityResult.Success);\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.RotateUserAccountKeysAsync(data));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserKeyWrongData_Throws(SutProvider<AccountsKeyManagementController> sutProvider,\n        RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber)\n    {\n        data.AccountKeys.SignatureKeyPair = null;\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())\n            .Returns(IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()));\n        try\n        {\n            await sutProvider.Sut.RotateUserAccountKeysAsync(data);\n            Assert.Fail(\"Should have thrown\");\n        }\n        catch (BadRequestException ex)\n        {\n            Assert.NotEmpty(ex.ModelState!.Values);\n        }\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_WithV2UpgradeToken_PassesTokenToCommand(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        RotateUserAccountKeysAndDataRequestModel data,\n        User user)\n    {\n        // Arrange\n        data.AccountKeys.SignatureKeyPair = null;\n        data.AccountUnlockData.V2UpgradeToken = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedType2String\n        };\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IRotateUserAccountKeysCommand>()\n            .RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())\n            .Returns(IdentityResult.Success);\n\n        // Act\n        await sutProvider.Sut.RotateUserAccountKeysAsync(data);\n\n        // Assert\n        await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)\n            .RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>\n                d.V2UpgradeToken != null &&\n                d.V2UpgradeToken.WrappedUserKey1 == _mockEncryptedType7String &&\n                d.V2UpgradeToken.WrappedUserKey2 == _mockEncryptedType2String));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RotateUserAccountKeys_WithoutV2UpgradeToken_PassesNullToCommand(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        RotateUserAccountKeysAndDataRequestModel data,\n        User user)\n    {\n        // Arrange\n        data.AccountKeys.SignatureKeyPair = null;\n        data.AccountUnlockData.V2UpgradeToken = null;\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IRotateUserAccountKeysCommand>()\n            .RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())\n            .Returns(IdentityResult.Success);\n\n        // Act\n        await sutProvider.Sut.RotateUserAccountKeysAsync(data);\n\n        // Assert\n        await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)\n            .RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>\n                d.V2UpgradeToken == null));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetKeyConnectorKeyAsync_V1_UserNull_Throws(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        SetKeyConnectorKeyRequestModel data)\n    {\n        data.KeyConnectorKeyWrappedUserKey = null;\n        data.AccountKeys = null;\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));\n\n        await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)\n            .SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        SetKeyConnectorKeyRequestModel data, User expectedUser)\n    {\n        data.KeyConnectorKeyWrappedUserKey = null;\n        data.AccountKeys = null;\n\n        expectedUser.PublicKey = null;\n        expectedUser.PrivateKey = null;\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(expectedUser);\n        sutProvider.GetDependency<IUserService>()\n            .SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>())\n            .Returns(IdentityResult.Failed(new IdentityError { Description = \"set key connector key error\" }));\n\n        var badRequestException =\n            await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));\n\n        Assert.Equal(1, badRequestException.ModelState!.ErrorCount);\n        Assert.Equal(\"set key connector key error\", badRequestException.ModelState.Root.Errors[0].ErrorMessage);\n        await sutProvider.GetDependency<IUserService>().Received(1)\n            .SetKeyConnectorKeyAsync(Arg.Do<User>(user =>\n            {\n                Assert.Equal(expectedUser.Id, user.Id);\n                Assert.Equal(data.Key, user.Key);\n                Assert.Equal(data.Kdf, user.Kdf);\n                Assert.Equal(data.KdfIterations, user.KdfIterations);\n                Assert.Equal(data.KdfMemory, user.KdfMemory);\n                Assert.Equal(data.KdfParallelism, user.KdfParallelism);\n                Assert.Equal(data.Keys!.PublicKey, user.PublicKey);\n                Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);\n            }), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeySucceeds_OkResponse(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        SetKeyConnectorKeyRequestModel data, User expectedUser)\n    {\n        data.KeyConnectorKeyWrappedUserKey = null;\n        data.AccountKeys = null;\n\n        expectedUser.PublicKey = null;\n        expectedUser.PrivateKey = null;\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(expectedUser);\n        sutProvider.GetDependency<IUserService>()\n            .SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>())\n            .Returns(IdentityResult.Success);\n\n        await sutProvider.Sut.PostSetKeyConnectorKeyAsync(data);\n\n        await sutProvider.GetDependency<IUserService>().Received(1)\n            .SetKeyConnectorKeyAsync(Arg.Do<User>(user =>\n            {\n                Assert.Equal(expectedUser.Id, user.Id);\n                Assert.Equal(data.Key, user.Key);\n                Assert.Equal(data.Kdf, user.Kdf);\n                Assert.Equal(data.KdfIterations, user.KdfIterations);\n                Assert.Equal(data.KdfMemory, user.KdfMemory);\n                Assert.Equal(data.KdfParallelism, user.KdfParallelism);\n                Assert.Equal(data.Keys!.PublicKey, user.PublicKey);\n                Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);\n            }), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetKeyConnectorKeyAsync_V2_UserNull_Throws(\n        SutProvider<AccountsKeyManagementController> sutProvider)\n    {\n        var request = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = \"wrapped-user-key\",\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = \"public-key\",\n                UserKeyEncryptedAccountPrivateKey = \"encrypted-private-key\"\n            },\n            OrgIdentifier = \"test-org\"\n        };\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));\n\n        await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().DidNotReceive()\n            .SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetKeyConnectorKeyAsync_V2_Success(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        User expectedUser)\n    {\n        var request = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = \"wrapped-user-key\",\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = \"public-key\",\n                UserKeyEncryptedAccountPrivateKey = \"encrypted-private-key\"\n            },\n            OrgIdentifier = \"test-org\"\n        };\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(expectedUser);\n\n        await sutProvider.Sut.PostSetKeyConnectorKeyAsync(request);\n\n        await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)\n            .SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),\n                Arg.Do<KeyConnectorKeysData>(data =>\n                {\n                    Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);\n                    Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);\n                    Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,\n                        data.AccountKeys.UserKeyEncryptedAccountPrivateKey);\n                    Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);\n                }));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostSetKeyConnectorKeyAsync_V2_CommandThrows_PropagatesException(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        User expectedUser)\n    {\n        var request = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = \"wrapped-user-key\",\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = \"public-key\",\n                UserKeyEncryptedAccountPrivateKey = \"encrypted-private-key\"\n            },\n            OrgIdentifier = \"test-org\"\n        };\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(expectedUser);\n        sutProvider.GetDependency<ISetKeyConnectorKeyCommand>()\n            .When(x => x.SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>()))\n            .Do(_ => throw new BadRequestException(\"Command failed\"));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));\n\n        Assert.Equal(\"Command failed\", exception.Message);\n        await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)\n            .SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),\n                Arg.Do<KeyConnectorKeysData>(data =>\n                {\n                    Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);\n                    Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);\n                    Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,\n                        data.AccountKeys.UserKeyEncryptedAccountPrivateKey);\n                    Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);\n                }));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostConvertToKeyConnectorAsync_UserNull_Throws(\n        SutProvider<AccountsKeyManagementController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());\n\n        await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)\n            .ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string?>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorFails_ThrowsBadRequestWithErrorResponse(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        User expectedUser)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(expectedUser);\n        sutProvider.GetDependency<IUserService>()\n            .ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string?>())\n            .Returns(IdentityResult.Failed(new IdentityError { Description = \"convert to key connector error\" }));\n\n        var badRequestException =\n            await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());\n\n        Assert.Equal(1, badRequestException.ModelState!.ErrorCount);\n        Assert.Equal(\"convert to key connector error\", badRequestException.ModelState.Root.Errors[0].ErrorMessage);\n        await sutProvider.GetDependency<IUserService>().Received(1)\n            .ConvertToKeyConnectorAsync(Arg.Is(expectedUser), Arg.Any<string?>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorSucceeds_OkResponse(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        User expectedUser)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(expectedUser);\n        sutProvider.GetDependency<IUserService>()\n            .ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string?>())\n            .Returns(IdentityResult.Success);\n\n        await sutProvider.Sut.PostConvertToKeyConnectorAsync();\n\n        await sutProvider.GetDependency<IUserService>().Received(1)\n            .ConvertToKeyConnectorAsync(Arg.Is(expectedUser), Arg.Any<string?>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostEnrollToKeyConnectorAsync_UserNull_Throws(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        KeyConnectorEnrollmentRequestModel data)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostEnrollToKeyConnectorAsync(data));\n\n        await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)\n            .ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostEnrollToKeyConnectorAsync_ConvertToKeyConnectorFails_ThrowsBadRequestWithErrorResponse(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        User expectedUser,\n        KeyConnectorEnrollmentRequestModel data)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(expectedUser);\n        sutProvider.GetDependency<IUserService>()\n            .ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(IdentityResult.Failed(new IdentityError { Description = \"convert to key connector error\" }));\n\n        var badRequestException =\n            await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostEnrollToKeyConnectorAsync(data));\n\n        Assert.Equal(1, badRequestException.ModelState!.ErrorCount);\n        Assert.Equal(\"convert to key connector error\", badRequestException.ModelState.Root.Errors[0].ErrorMessage);\n        await sutProvider.GetDependency<IUserService>().Received(1)\n            .ConvertToKeyConnectorAsync(Arg.Is(expectedUser), Arg.Is(data.KeyConnectorKeyWrappedUserKey));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PostEnrollToKeyConnectorAsync_ConvertToKeyConnectorSucceeds_OkResponse(\n        SutProvider<AccountsKeyManagementController> sutProvider,\n        User expectedUser,\n        KeyConnectorEnrollmentRequestModel data)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(expectedUser);\n        sutProvider.GetDependency<IUserService>()\n            .ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(IdentityResult.Success);\n\n        await sutProvider.Sut.PostEnrollToKeyConnectorAsync(data);\n\n        await sutProvider.GetDependency<IUserService>().Received(1)\n            .ConvertToKeyConnectorAsync(Arg.Is(expectedUser), Arg.Is(data.KeyConnectorKeyWrappedUserKey));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetKeyConnectorConfirmationDetailsAsync_NoUser_Throws(\n        SutProvider<AccountsKeyManagementController> sutProvider, string orgSsoIdentifier)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .ReturnsNull();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>\n            sutProvider.Sut.GetKeyConnectorConfirmationDetailsAsync(orgSsoIdentifier));\n\n        await sutProvider.GetDependency<IKeyConnectorConfirmationDetailsQuery>().ReceivedWithAnyArgs(0)\n            .Run(Arg.Any<string>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetKeyConnectorConfirmationDetailsAsync_Success(\n        SutProvider<AccountsKeyManagementController> sutProvider, User expectedUser, string orgSsoIdentifier)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(expectedUser);\n        sutProvider.GetDependency<IKeyConnectorConfirmationDetailsQuery>().Run(orgSsoIdentifier, expectedUser.Id)\n            .Returns(\n                new KeyConnectorConfirmationDetails { OrganizationName = \"test\" }\n            );\n\n        var result = await sutProvider.Sut.GetKeyConnectorConfirmationDetailsAsync(orgSsoIdentifier);\n\n        Assert.NotNull(result);\n        Assert.Equal(\"test\", result.OrganizationName);\n        await sutProvider.GetDependency<IKeyConnectorConfirmationDetailsQuery>().Received(1)\n            .Run(orgSsoIdentifier, expectedUser.Id);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.KeyManagement.Controllers;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Controllers;\n\n[ControllerCustomize(typeof(UsersController))]\n[SutProviderCustomize]\n[JsonDocumentCustomize]\npublic class UsersControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetPublicKey_NotFound_ThrowsNotFoundException(\n        SutProvider<UsersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(Arg.Any<Guid>()).ReturnsNull();\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetPublicKeyAsync(new Guid()));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetPublicKey_ReturnsUserKeyResponseModel(\n        SutProvider<UsersController> sutProvider,\n        Guid userId)\n    {\n        var publicKey = \"publicKey\";\n        sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(userId).Returns(publicKey);\n\n        var result = await sutProvider.Sut.GetPublicKeyAsync(userId);\n        Assert.NotNull(result);\n        Assert.Equal(userId, result.UserId);\n        Assert.Equal(publicKey, result.PublicKey);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAccountKeys_UserNotFound_ThrowsNotFoundException(\n        SutProvider<UsersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).ReturnsNull();\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAccountKeysAsync(new Guid()));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel(\n        SutProvider<UsersController> sutProvider,\n        Guid userId)\n    {\n        var user = new User\n        {\n            Id = userId,\n            PublicKey = \"publicKey\",\n            SignedPublicKey = \"signedPublicKey\",\n        };\n\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(userId).Returns(user);\n        sutProvider.GetDependency<IUserAccountKeysQuery>()\n            .Run(user)\n            .Returns(new UserAccountKeysData\n            {\n                PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(\"wrappedPrivateKey\", \"publicKey\", \"signedPublicKey\"),\n                SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, \"wrappedSigningKey\", \"verifyingKey\"),\n            });\n\n        var result = await sutProvider.Sut.GetAccountKeysAsync(userId);\n        Assert.NotNull(result);\n        Assert.Equal(\"publicKey\", result.PublicKey);\n        Assert.Equal(\"signedPublicKey\", result.SignedPublicKey);\n        Assert.Equal(\"verifyingKey\", result.VerifyingKey);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel_WithNullVerifyingKey(\n        SutProvider<UsersController> sutProvider,\n        Guid userId)\n    {\n        var user = new User\n        {\n            Id = userId,\n            PublicKey = \"publicKey\",\n            SignedPublicKey = null,\n        };\n\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(userId).Returns(user);\n        sutProvider.GetDependency<IUserAccountKeysQuery>()\n            .Run(user)\n            .Returns(new UserAccountKeysData\n            {\n                PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(\"wrappedPrivateKey\", \"publicKey\", null),\n                SignatureKeyPairData = null,\n            });\n\n        var result = await sutProvider.Sut.GetAccountKeysAsync(userId);\n        Assert.NotNull(result);\n        Assert.Equal(\"publicKey\", result.PublicKey);\n        Assert.Null(result.SignedPublicKey);\n        Assert.Null(result.VerifyingKey);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Models/Request/KeyConnectorEnrollmentRequestModelTests.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.KeyManagement.Models.Requests;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Models.Request;\n\npublic class KeyConnectorEnrollmentRequestModelTests\n{\n    private const string _wrappedUserKey = \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n\n    [Fact]\n    public void Validate_KeyConnectorKeyWrappedUserKeyNull_Invalid()\n    {\n        var model = new KeyConnectorEnrollmentRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = null!\n        };\n\n        var results = Validate(model);\n\n        Assert.Contains(results,\n            r => r.ErrorMessage == \"KeyConnectorKeyWrappedUserKey must be supplied when request body is provided.\");\n    }\n\n    [Fact]\n    public void Validate_KeyConnectorKeyWrappedUserKeyWhitespace_Invalid()\n    {\n        var model = new KeyConnectorEnrollmentRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = \" \"\n        };\n\n        var results = Validate(model);\n\n        Assert.Contains(results,\n            r => r.ErrorMessage == \"KeyConnectorKeyWrappedUserKey is not a valid encrypted string.\");\n    }\n\n    [Fact]\n    public void Validate_KeyConnectorKeyWrappedUserKeyValid_Success()\n    {\n        var model = new KeyConnectorEnrollmentRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = _wrappedUserKey\n        };\n\n        var results = Validate(model);\n\n        Assert.Empty(results);\n    }\n\n    private static List<ValidationResult> Validate(KeyConnectorEnrollmentRequestModel model)\n    {\n        var results = new List<ValidationResult>();\n        Validator.TryValidateObject(model, new ValidationContext(model), results, true);\n        return results;\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Models/Request/MasterPasswordUnlockDataModel.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Core.Enums;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Models.Request;\n\npublic class MasterPasswordUnlockDataModelTests\n{\n\n    readonly string _mockEncryptedString = \"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=\";\n\n    [Theory]\n    [InlineData(KdfType.PBKDF2_SHA256, 5000, null, null)]\n    [InlineData(KdfType.PBKDF2_SHA256, 100000, null, null)]\n    [InlineData(KdfType.PBKDF2_SHA256, 600000, null, null)]\n    [InlineData(KdfType.Argon2id, 3, 64, 4)]\n    public void Validate_Success(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        var model = new MasterPasswordUnlockAndAuthenticationDataModel\n        {\n            KdfType = kdfType,\n            KdfIterations = kdfIterations,\n            KdfMemory = kdfMemory,\n            KdfParallelism = kdfParallelism,\n            Email = \"example@example.com\",\n            MasterKeyAuthenticationHash = \"hash\",\n            MasterKeyEncryptedUserKey = _mockEncryptedString,\n            MasterPasswordHint = \"hint\"\n        };\n        var result = Validate(model);\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [InlineData(KdfType.Argon2id, 1, null, 1)]\n    [InlineData(KdfType.Argon2id, 1, 64, null)]\n    [InlineData(KdfType.PBKDF2_SHA256, 5000, 0, null)]\n    [InlineData(KdfType.PBKDF2_SHA256, 5000, null, 0)]\n    [InlineData(KdfType.PBKDF2_SHA256, 5000, 0, 0)]\n    [InlineData((KdfType)2, 100000, null, null)]\n    [InlineData((KdfType)2, 2, 64, 4)]\n    public void Validate_Failure(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        var model = new MasterPasswordUnlockAndAuthenticationDataModel\n        {\n            KdfType = kdfType,\n            KdfIterations = kdfIterations,\n            KdfMemory = kdfMemory,\n            KdfParallelism = kdfParallelism,\n            Email = \"example@example.com\",\n            MasterKeyAuthenticationHash = \"hash\",\n            MasterKeyEncryptedUserKey = _mockEncryptedString,\n            MasterPasswordHint = \"hint\"\n        };\n        var result = Validate(model);\n        Assert.Single(result);\n        Assert.NotNull(result.First().ErrorMessage);\n    }\n\n    private static List<ValidationResult> Validate(MasterPasswordUnlockAndAuthenticationDataModel model)\n    {\n        var results = new List<ValidationResult>();\n        Validator.TryValidateObject(model, new ValidationContext(model), results, true);\n        return results;\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Models/Request/SetKeyConnectorKeyRequestModelTests.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.KeyManagement.Models.Requests;\nusing Bit.Core;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Models.Request;\n\npublic class SetKeyConnectorKeyRequestModelTests\n{\n    private const string _wrappedUserKey = \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n    private const string _publicKey = \"public-key\";\n    private const string _privateKey = \"private-key\";\n    private const string _userKey = \"user-key\";\n    private const string _orgIdentifier = \"org-identifier\";\n\n    [Fact]\n    public void Validate_V2Registration_Valid()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = _wrappedUserKey,\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = _publicKey,\n                UserKeyEncryptedAccountPrivateKey = _privateKey\n            },\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_V2Registration_WrappedUserKeyNotEncryptedString_Invalid()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = \"not-encrypted-string\",\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = _publicKey,\n                UserKeyEncryptedAccountPrivateKey = _privateKey\n            },\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results,\n            r => r.ErrorMessage == \"KeyConnectorKeyWrappedUserKey is not a valid encrypted string.\");\n    }\n\n    [Fact]\n    public void Validate_V1Registration_Valid()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            Key = _userKey,\n            Keys = new KeysRequestModel\n            {\n                PublicKey = _publicKey,\n                EncryptedPrivateKey = _privateKey\n            },\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_V1Registration_MissingKey_Invalid()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            Key = null,\n            Keys = new KeysRequestModel\n            {\n                PublicKey = _publicKey,\n                EncryptedPrivateKey = _privateKey\n            },\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.ErrorMessage == \"Key must be supplied.\");\n    }\n\n    [Fact]\n    public void Validate_V1Registration_MissingKeys_Invalid()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            Key = _userKey,\n            Keys = null,\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.ErrorMessage == \"Keys must be supplied.\");\n    }\n\n    [Fact]\n    public void Validate_V1Registration_MissingKdf_Invalid()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            Key = _userKey,\n            Keys = new KeysRequestModel\n            {\n                PublicKey = _publicKey,\n                EncryptedPrivateKey = _privateKey\n            },\n            Kdf = null,\n            KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.ErrorMessage == \"Kdf must be supplied.\");\n    }\n\n    [Fact]\n    public void Validate_V1Registration_MissingKdfIterations_Invalid()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            Key = _userKey,\n            Keys = new KeysRequestModel\n            {\n                PublicKey = _publicKey,\n                EncryptedPrivateKey = _privateKey\n            },\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = null,\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.ErrorMessage == \"KdfIterations must be supplied.\");\n    }\n\n    [Fact]\n    public void Validate_V1Registration_Argon2id_MissingKdfMemory_Invalid()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            Key = _userKey,\n            Keys = new KeysRequestModel\n            {\n                PublicKey = _publicKey,\n                EncryptedPrivateKey = _privateKey\n            },\n            Kdf = KdfType.Argon2id,\n            KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,\n            KdfMemory = null,\n            KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.ErrorMessage == \"KdfMemory must be supplied when Kdf is Argon2id.\");\n    }\n\n    [Fact]\n    public void Validate_V1Registration_Argon2id_MissingKdfParallelism_Invalid()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            Key = _userKey,\n            Keys = new KeysRequestModel\n            {\n                PublicKey = _publicKey,\n                EncryptedPrivateKey = _privateKey\n            },\n            Kdf = KdfType.Argon2id,\n            KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,\n            KdfMemory = AuthConstants.ARGON2_MEMORY.Default,\n            KdfParallelism = null,\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.ErrorMessage == \"KdfParallelism must be supplied when Kdf is Argon2id.\");\n    }\n\n    [Fact]\n    public void ToKeyConnectorKeysData_EmptyKeyConnectorKeyWrappedUserKey_ThrowsException()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = \"\",\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = _publicKey,\n                UserKeyEncryptedAccountPrivateKey = _privateKey\n            },\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());\n\n        // Assert\n        Assert.Equal(\"KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.\", exception.Message);\n    }\n\n    [Fact]\n    public void ToKeyConnectorKeysData_NullKeyConnectorKeyWrappedUserKey_ThrowsException()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = null,\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = _publicKey,\n                UserKeyEncryptedAccountPrivateKey = _privateKey\n            },\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());\n\n        // Assert\n        Assert.Equal(\"KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.\", exception.Message);\n    }\n\n    [Fact]\n    public void ToKeyConnectorKeysData_NullAccountKeys_ThrowsException()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = _wrappedUserKey,\n            AccountKeys = null,\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());\n\n        // Assert\n        Assert.Equal(\"KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.\", exception.Message);\n    }\n\n    [Fact]\n    public void ToKeyConnectorKeysData_Valid_Success()\n    {\n        // Arrange\n        var model = new SetKeyConnectorKeyRequestModel\n        {\n            KeyConnectorKeyWrappedUserKey = _wrappedUserKey,\n            AccountKeys = new AccountKeysRequestModel\n            {\n                AccountPublicKey = _publicKey,\n                UserKeyEncryptedAccountPrivateKey = _privateKey\n            },\n            OrgIdentifier = _orgIdentifier\n        };\n\n        // Act\n        var data = model.ToKeyConnectorKeysData();\n\n        // Assert\n        Assert.Equal(_wrappedUserKey, data.KeyConnectorKeyWrappedUserKey);\n        Assert.Equal(_publicKey, data.AccountKeys.AccountPublicKey);\n        Assert.Equal(_privateKey, data.AccountKeys.UserKeyEncryptedAccountPrivateKey);\n        Assert.Equal(_orgIdentifier, data.OrgIdentifier);\n    }\n\n    private static List<ValidationResult> Validate(SetKeyConnectorKeyRequestModel model)\n    {\n        var results = new List<ValidationResult>();\n        Validator.TryValidateObject(model, new ValidationContext(model), results, true);\n        return results;\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Models/Request/SignatureKeyPairRequestModel.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Models.Request;\n\npublic class SignatureKeyPairRequestModelTests\n{\n    [Fact]\n    public void ToSignatureKeyPairData_WrongAlgorithm_Rejects()\n    {\n        var model = new SignatureKeyPairRequestModel\n        {\n            SignatureAlgorithm = \"abc\",\n            WrappedSigningKey = \"wrappedKey\",\n            VerifyingKey = \"verifyingKey\"\n        };\n\n        Assert.Throws<ArgumentException>(() => model.ToSignatureKeyPairData());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Models/Request/V2UpgradeTokenRequestModelTests.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.KeyManagement.Models.Requests;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Models.Request;\n\npublic class V2UpgradeTokenRequestModelTests\n{\n    private const string _validWrappedKey1 = \"7.AOs41Hd8OQiCPXjyJKCiDA==\";\n    private const string _validWrappedKey2 = \"2.BPt52Ie9PQjDQYkzKLDjEB==|P7PIiu3V3iKHCTOHojnKnh==|jE44t9C79D9KiZZiTb5W2uBskwMs9fFbHrPW8CSp6Kl=\";\n\n    [Fact]\n    public void Validate_WithValidEncStrings_ReturnsNoErrors()\n    {\n        // Arrange\n        var model = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = _validWrappedKey1,\n            WrappedUserKey2 = _validWrappedKey2\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_WithMissingWrappedUserKey1_ReturnsValidationError()\n    {\n        // Arrange\n        var model = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = null!,\n            WrappedUserKey2 = _validWrappedKey2\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.MemberNames.Contains(\"WrappedUserKey1\"));\n    }\n\n    [Fact]\n    public void Validate_WithMissingWrappedUserKey2_ReturnsValidationError()\n    {\n        // Arrange\n        var model = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = _validWrappedKey1,\n            WrappedUserKey2 = null!\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.MemberNames.Contains(\"WrappedUserKey2\"));\n    }\n\n    [Fact]\n    public void Validate_WithInvalidEncStringFormatKey1_ReturnsValidationError()\n    {\n        // Arrange\n        var model = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = \"not-an-encrypted-string\",\n            WrappedUserKey2 = _validWrappedKey2\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.ErrorMessage == \"WrappedUserKey1 is not a valid encrypted string.\");\n    }\n\n    [Fact]\n    public void Validate_WithInvalidEncStringFormatKey2_ReturnsValidationError()\n    {\n        // Arrange\n        var model = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = _validWrappedKey1,\n            WrappedUserKey2 = \"not-an-encrypted-string\"\n        };\n\n        // Act\n        var results = Validate(model);\n\n        // Assert\n        Assert.Single(results);\n        Assert.Contains(results, r => r.ErrorMessage == \"WrappedUserKey2 is not a valid encrypted string.\");\n    }\n\n    [Fact]\n    public void ToData_WithValidModel_MapsPropertiesCorrectly()\n    {\n        // Arrange\n        var model = new V2UpgradeTokenRequestModel\n        {\n            WrappedUserKey1 = _validWrappedKey1,\n            WrappedUserKey2 = _validWrappedKey2\n        };\n\n        // Act\n        var data = model.ToData();\n\n        // Assert\n        Assert.Equal(_validWrappedKey1, data.WrappedUserKey1);\n        Assert.Equal(_validWrappedKey2, data.WrappedUserKey2);\n    }\n\n    private static List<ValidationResult> Validate(V2UpgradeTokenRequestModel model)\n    {\n        var results = new List<ValidationResult>();\n        Validator.TryValidateObject(model, new ValidationContext(model), results, true);\n        return results;\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Validators/CipherRotationValidatorTests.cs",
    "content": "﻿using Bit.Api.KeyManagement.Validators;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Validators;\n\n[SutProviderCustomize]\npublic class CipherRotationValidatorTests\n{\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_MissingCipher_Throws(SutProvider<CipherRotationValidator> sutProvider, User user,\n        IEnumerable<CipherWithIdRequestModel> ciphers)\n    {\n        var userCiphers = ciphers.Select(c => new CipherDetails { Id = c.Id.GetValueOrDefault(), Type = c.Type })\n            .ToList();\n        userCiphers.Add(new CipherDetails { Id = Guid.NewGuid(), Type = Core.Vault.Enums.CipherType.Login });\n        sutProvider.GetDependency<ICipherRepository>().GetManyByUserIdAsync(user.Id, Arg.Any<bool>())\n            .Returns(userCiphers);\n\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.ValidateAsync(user, ciphers));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_CipherDoesNotBelongToUser_NotIncluded(\n        SutProvider<CipherRotationValidator> sutProvider, User user, IEnumerable<CipherWithIdRequestModel> ciphers)\n    {\n        var userCiphers = ciphers.Select(c => new CipherDetails { Id = c.Id.GetValueOrDefault(), Type = c.Type })\n            .ToList();\n        userCiphers.RemoveAt(0);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByUserIdAsync(user.Id, Arg.Any<bool>())\n            .Returns(userCiphers);\n\n        var result = await sutProvider.Sut.ValidateAsync(user, ciphers);\n\n        Assert.DoesNotContain(result, c => c.Id == ciphers.First().Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_SentCiphersAreEmptyButDatabaseCiphersAreNot_Throws(\n        SutProvider<CipherRotationValidator> sutProvider, User user, IEnumerable<CipherWithIdRequestModel> ciphers)\n    {\n        var userCiphers = ciphers.Select(c => new CipherDetails { Id = c.Id.GetValueOrDefault(), Type = c.Type })\n            .ToList();\n        sutProvider.GetDependency<ICipherRepository>().GetManyByUserIdAsync(user.Id, Arg.Any<bool>())\n            .Returns(userCiphers);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.ValidateAsync(user, Enumerable.Empty<CipherWithIdRequestModel>()));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Validators/DeviceRotationValidatorTests.cs",
    "content": "﻿using Bit.Api.KeyManagement.Validators;\nusing Bit.Core.Auth.Models.Api.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Validators;\n\n[SutProviderCustomize]\npublic class DeviceRotationValidatorTests\n{\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_SentDevicesAreEmptyButDatabaseDevicesAreNot_Throws(\n        SutProvider<DeviceRotationValidator> sutProvider, User user, IEnumerable<OtherDeviceKeysUpdateRequestModel> devices)\n    {\n        var userCiphers = devices.Select(c => new Device { Id = c.DeviceId, EncryptedPrivateKey = \"EncryptedPrivateKey\", EncryptedPublicKey = \"EncryptedPublicKey\", EncryptedUserKey = \"EncryptedUserKey\" }).ToList();\n        sutProvider.GetDependency<IDeviceRepository>().GetManyByUserIdAsync(user.Id)\n            .Returns(userCiphers);\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.ValidateAsync(user, Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_SentDevicesTrustedButDatabaseUntrusted_Throws(\n        SutProvider<DeviceRotationValidator> sutProvider, User user, IEnumerable<OtherDeviceKeysUpdateRequestModel> devices)\n    {\n        var userCiphers = devices.Select(c => new Device { Id = c.DeviceId, EncryptedPrivateKey = \"Key\", EncryptedPublicKey = \"Key\", EncryptedUserKey = \"Key\" }).ToList();\n        sutProvider.GetDependency<IDeviceRepository>().GetManyByUserIdAsync(user.Id)\n            .Returns(userCiphers);\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.ValidateAsync(user, [\n            new OtherDeviceKeysUpdateRequestModel { DeviceId = userCiphers.First().Id, EncryptedPublicKey = null, EncryptedUserKey = null }\n        ]));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_Validates(\n        SutProvider<DeviceRotationValidator> sutProvider, User user, IEnumerable<OtherDeviceKeysUpdateRequestModel> devices)\n    {\n        var userCiphers = devices.Select(c => new Device { Id = c.DeviceId, EncryptedPrivateKey = \"Key\", EncryptedPublicKey = \"Key\", EncryptedUserKey = \"Key\" }).ToList().Slice(0, 1);\n        sutProvider.GetDependency<IDeviceRepository>().GetManyByUserIdAsync(user.Id)\n            .Returns(userCiphers);\n        Assert.NotEmpty(await sutProvider.Sut.ValidateAsync(user, [\n            new OtherDeviceKeysUpdateRequestModel { DeviceId = userCiphers.First().Id, EncryptedPublicKey = \"Key\", EncryptedUserKey = \"Key\" }\n        ]));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Validators/EmergencyAccessRotationValidatorTests.cs",
    "content": "﻿using Bit.Api.Auth.Models.Request;\nusing Bit.Api.KeyManagement.Validators;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Validators;\n\n[SutProviderCustomize]\npublic class EmergencyAccessRotationValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_MissingEmergencyAccess_Throws(\n        SutProvider<EmergencyAccessRotationValidator> sutProvider, User user,\n        IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)\n    {\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);\n        var userEmergencyAccess = emergencyAccessKeys.Select(e => new EmergencyAccessDetails\n        {\n            Id = e.Id,\n            GrantorName = user.Name,\n            GrantorEmail = user.Email,\n            KeyEncrypted = e.KeyEncrypted,\n            Type = e.Type\n        }).ToList();\n        userEmergencyAccess.Add(new EmergencyAccessDetails { Id = Guid.NewGuid(), GrantorEmail = \"grantor@example.com\", KeyEncrypted = \"TestKey\" });\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGrantorIdAsync(user.Id)\n            .Returns(userEmergencyAccess);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ValidateAsync(user, emergencyAccessKeys));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_EmergencyAccessDoesNotBelongToUser_NotIncluded(\n        SutProvider<EmergencyAccessRotationValidator> sutProvider, User user,\n        IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)\n    {\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);\n        var userEmergencyAccess = emergencyAccessKeys.Select(e => new EmergencyAccessDetails\n        {\n            Id = e.Id,\n            GrantorName = user.Name,\n            GrantorEmail = user.Email,\n            KeyEncrypted = e.KeyEncrypted,\n            Type = e.Type\n        }).ToList();\n        userEmergencyAccess.RemoveAt(0);\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGrantorIdAsync(user.Id)\n            .Returns(userEmergencyAccess);\n\n        var result = await sutProvider.Sut.ValidateAsync(user, emergencyAccessKeys);\n\n        Assert.DoesNotContain(result, c => c.Id == emergencyAccessKeys.First().Id);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_UserNotPremium_Success(\n        SutProvider<EmergencyAccessRotationValidator> sutProvider, User user,\n        IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)\n    {\n        // We want to allow users who have lost premium to rotate their key for any existing emergency access, as long\n        // as we restrict it to existing records and don't let them alter data\n        user.Premium = false;\n        var userEmergencyAccess = emergencyAccessKeys.Select(e => new EmergencyAccessDetails\n        {\n            Id = e.Id,\n            GrantorName = user.Name,\n            GrantorEmail = user.Email,\n            KeyEncrypted = e.KeyEncrypted,\n            Type = e.Type\n        }).ToList();\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGrantorIdAsync(user.Id)\n            .Returns(userEmergencyAccess);\n\n        var result = await sutProvider.Sut.ValidateAsync(user, emergencyAccessKeys);\n\n        Assert.Equal(userEmergencyAccess, result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_NonConfirmedEmergencyAccess_NotReturned(\n        SutProvider<EmergencyAccessRotationValidator> sutProvider, User user,\n        IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)\n    {\n        emergencyAccessKeys.First().KeyEncrypted = null;\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);\n        var userEmergencyAccess = emergencyAccessKeys.Select(e => new EmergencyAccessDetails\n        {\n            Id = e.Id,\n            GrantorName = user.Name,\n            GrantorEmail = user.Email,\n            KeyEncrypted = e.KeyEncrypted,\n            Type = e.Type\n        }).ToList();\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGrantorIdAsync(user.Id)\n            .Returns(userEmergencyAccess);\n\n        var result = await sutProvider.Sut.ValidateAsync(user, emergencyAccessKeys);\n\n        Assert.DoesNotContain(result, c => c.Id == emergencyAccessKeys.First().Id);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_AttemptToSetKeyToNull_Throws(\n        SutProvider<EmergencyAccessRotationValidator> sutProvider, User user,\n        IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)\n    {\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);\n        var userEmergencyAccess = emergencyAccessKeys.Select(e => new EmergencyAccessDetails\n        {\n            Id = e.Id,\n            GrantorName = user.Name,\n            GrantorEmail = user.Email,\n            KeyEncrypted = e.KeyEncrypted,\n            Type = e.Type\n        }).ToList();\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGrantorIdAsync(user.Id)\n            .Returns(userEmergencyAccess);\n        emergencyAccessKeys.First().KeyEncrypted = null;\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ValidateAsync(user, emergencyAccessKeys));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_SentKeysAreEmptyButDatabaseIsNot_Throws(\n        SutProvider<EmergencyAccessRotationValidator> sutProvider, User user,\n        IEnumerable<EmergencyAccessWithIdRequestModel> emergencyAccessKeys)\n    {\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);\n        var userEmergencyAccess = emergencyAccessKeys.Select(e => new EmergencyAccessDetails\n        {\n            Id = e.Id,\n            GrantorName = user.Name,\n            GrantorEmail = user.Email,\n            KeyEncrypted = e.KeyEncrypted,\n            Type = e.Type\n        }).ToList();\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetManyDetailsByGrantorIdAsync(user.Id)\n            .Returns(userEmergencyAccess);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.ValidateAsync(user, Enumerable.Empty<EmergencyAccessWithIdRequestModel>()));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Validators/FolderRotationValidatorTests.cs",
    "content": "﻿using Bit.Api.KeyManagement.Validators;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Validators;\n\n[SutProviderCustomize]\npublic class FolderRotationValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_MissingFolder_Throws(SutProvider<FolderRotationValidator> sutProvider, User user,\n        IEnumerable<FolderWithIdRequestModel> folders)\n    {\n        var userFolders = folders.Select(f => f.ToFolder(new Folder())).ToList();\n        userFolders.Add(new Folder { Id = Guid.NewGuid(), Name = \"Missing Folder\" });\n        sutProvider.GetDependency<IFolderRepository>().GetManyByUserIdAsync(user.Id).Returns(userFolders);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.ValidateAsync(user, folders));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_FolderDoesNotBelongToUser_NotReturned(\n        SutProvider<FolderRotationValidator> sutProvider, User user, IEnumerable<FolderWithIdRequestModel> folders)\n    {\n        var userFolders = folders.Select(f => f.ToFolder(new Folder())).ToList();\n        userFolders.RemoveAt(0);\n        sutProvider.GetDependency<IFolderRepository>().GetManyByUserIdAsync(user.Id).Returns(userFolders);\n\n        var result = await sutProvider.Sut.ValidateAsync(user, folders);\n\n        Assert.DoesNotContain(result, c => c.Id == folders.First().Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_SentFoldersAreEmptyButDatabaseFoldersAreNot_Throws(\n        SutProvider<FolderRotationValidator> sutProvider, User user, IEnumerable<FolderWithIdRequestModel> folders)\n    {\n        var userFolders = folders.Select(f => f.ToFolder(new Folder())).ToList();\n        sutProvider.GetDependency<IFolderRepository>().GetManyByUserIdAsync(user.Id).Returns(userFolders);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.ValidateAsync(user, Enumerable.Empty<FolderWithIdRequestModel>()));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Validators/OrganizationUserRotationValidatorTests.cs",
    "content": "﻿using Bit.Api.AdminConsole.Models.Request.Organizations;\nusing Bit.Api.KeyManagement.Validators;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Validators;\n\n[SutProviderCustomize]\npublic class OrganizationUserRotationValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_Success_ReturnsValid(\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user,\n        IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)\n    {\n        var existingUserResetPassword = resetPasswordKeys\n            .Select(a =>\n                new OrganizationUser\n                {\n                    Id = new Guid(),\n                    ResetPasswordKey = a.ResetPasswordKey,\n                    OrganizationId = a.OrganizationId\n                }).ToList();\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)\n            .Returns(existingUserResetPassword);\n\n        var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);\n\n        Assert.Equal(result.Select(r => r.OrganizationId), resetPasswordKeys.Select(a => a.OrganizationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_NullResetPasswordKeys_ReturnsEmptyList(\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user)\n    {\n        // Arrange\n        IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys = null;\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_NoOrgUsers_ReturnsEmptyList(\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user,\n        IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)\n            .Returns(new List<OrganizationUser>()); // Return an empty list\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [BitAutoData([null])]\n    [BitAutoData(\"\")]\n    public async Task ValidateAsync_OrgUsersWithNullOrEmptyResetPasswordKey_FiltersOutInvalidKeys(\n        string? invalidResetPasswordKey,\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user,\n        ResetPasswordWithOrgIdRequestModel validResetPasswordKey)\n    {\n        // Arrange\n        var existingUserResetPassword = new List<OrganizationUser>\n        {\n            // Valid org user with reset password key\n            new OrganizationUser\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = validResetPasswordKey.OrganizationId,\n                ResetPasswordKey = validResetPasswordKey.ResetPasswordKey\n            },\n            // Invalid org user with null or empty reset password key - should be filtered out\n            new OrganizationUser\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = Guid.NewGuid(),\n                ResetPasswordKey = invalidResetPasswordKey\n            }\n        };\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)\n            .Returns(existingUserResetPassword);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(user, new[] { validResetPasswordKey });\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Single(result);\n        Assert.Equal(validResetPasswordKey.OrganizationId, result[0].OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_MissingResetPassword_Throws(\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user,\n        IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)\n    {\n        var existingUserResetPassword = resetPasswordKeys\n            .Select(a =>\n                new OrganizationUser\n                {\n                    Id = new Guid(),\n                    ResetPasswordKey = a.ResetPasswordKey,\n                    OrganizationId = a.OrganizationId\n                }).ToList();\n        existingUserResetPassword.Add(new OrganizationUser\n        {\n            Id = Guid.NewGuid(),\n            ResetPasswordKey = \"Missing ResetPasswordKey\"\n        });\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)\n            .Returns(existingUserResetPassword);\n\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_ResetPasswordDoesNotBelongToUser_NotReturned(\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user,\n        IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)\n    {\n        var existingUserResetPassword = resetPasswordKeys\n            .Select(a =>\n                new OrganizationUser\n                {\n                    Id = new Guid(),\n                    ResetPasswordKey = a.ResetPasswordKey,\n                    OrganizationId = a.OrganizationId\n                }).ToList();\n        existingUserResetPassword.RemoveAt(0);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)\n            .Returns(existingUserResetPassword);\n\n        var result = await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys);\n\n        Assert.DoesNotContain(result, c => c.Id == resetPasswordKeys.First().OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_AttemptToSetKeyToNull_Throws(\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user,\n        IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)\n    {\n        var existingUserResetPassword = resetPasswordKeys\n            .Select(a =>\n                new OrganizationUser\n                {\n                    Id = new Guid(),\n                    ResetPasswordKey = a.ResetPasswordKey,\n                    OrganizationId = a.OrganizationId\n                }).ToList();\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)\n            .Returns(existingUserResetPassword);\n        resetPasswordKeys.First().ResetPasswordKey = null;\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ValidateAsync(user, resetPasswordKeys));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_NoOrganizationsInRequestButInDatabase_Throws(\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user,\n        IEnumerable<ResetPasswordWithOrgIdRequestModel> resetPasswordKeys)\n    {\n        var existingUserResetPassword = resetPasswordKeys\n            .Select(a =>\n                new OrganizationUser\n                {\n                    Id = new Guid(),\n                    ResetPasswordKey = a.ResetPasswordKey,\n                    OrganizationId = a.OrganizationId\n                }).ToList();\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)\n            .Returns(existingUserResetPassword);\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ValidateAsync(user, Enumerable.Empty<ResetPasswordWithOrgIdRequestModel>()));\n    }\n\n    // TODO: Remove this test after https://bitwarden.atlassian.net/browse/PM-31001 is resolved.\n    // Clients currently send \"\" as a reset password key value during rotation due to a client-side bug.\n    // The server must accept \"\" to avoid blocking key rotation for affected users.\n    // After PM-31001 is fixed, this should be replaced with a test asserting that \"\" throws BadRequestException.\n    [Theory]\n    [BitAutoData(\"\")]\n    [BitAutoData(\" \")]\n    public async Task ValidateAsync_EmptyOrWhitespaceKey_AcceptedDueToClientBug(\n        string emptyKey,\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user,\n        ResetPasswordWithOrgIdRequestModel validResetPasswordKey)\n    {\n        // Arrange\n        var existingUserResetPassword = new List<OrganizationUser>\n        {\n            new OrganizationUser\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = validResetPasswordKey.OrganizationId,\n                ResetPasswordKey = \"existing-valid-key\"\n            }\n        };\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)\n            .Returns(existingUserResetPassword);\n\n        // Set the incoming key to empty/whitespace (simulating client bug)\n        validResetPasswordKey.ResetPasswordKey = emptyKey;\n\n        // Act — rotation should succeed (not throw) to preserve backward compatibility\n        var result = await sutProvider.Sut.ValidateAsync(user, new[] { validResetPasswordKey });\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Single(result);\n        Assert.Equal(emptyKey, result[0].ResetPasswordKey);\n    }\n\n    [Theory]\n    [BitAutoData(\" \")]\n    public async Task ValidateAsync_WhitespaceOnlyExistingKey_FiltersOut(\n        string whitespaceKey,\n        SutProvider<OrganizationUserRotationValidator> sutProvider, User user,\n        ResetPasswordWithOrgIdRequestModel validResetPasswordKey)\n    {\n        // Arrange\n        var existingUserResetPassword = new List<OrganizationUser>\n        {\n            new OrganizationUser\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = validResetPasswordKey.OrganizationId,\n                ResetPasswordKey = validResetPasswordKey.ResetPasswordKey\n            },\n            // Whitespace-only key should be filtered out\n            new OrganizationUser\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = Guid.NewGuid(),\n                ResetPasswordKey = whitespaceKey\n            }\n        };\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)\n            .Returns(existingUserResetPassword);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(user, new[] { validResetPasswordKey });\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Single(result);\n        Assert.Equal(validResetPasswordKey.OrganizationId, result[0].OrganizationId);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Api.KeyManagement.Validators;\nusing Bit.Api.Tools.Models;\nusing Bit.Api.Tools.Models.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.Services;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Validators;\n\n[SutProviderCustomize]\npublic class SendRotationValidatorTests\n{\n    [Fact]\n    public async Task ValidateAsync_Success()\n    {\n        // Arrange\n        var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();\n        var sendRepository = Substitute.For<ISendRepository>();\n\n        var sut = new SendRotationValidator(\n            sendAuthorizationService,\n            sendRepository\n        );\n\n        var user = new User { Id = new Guid() };\n        var sends = CreateInputSendRequests();\n\n        sendRepository.GetManyByUserIdAsync(user.Id).Returns(MockUserSends(user));\n\n        // Act\n        var result = await sut.ValidateAsync(user, sends);\n\n        // Assert\n        var sendIds = new Guid[]\n        {\n            new(\"72e9ac6d-05f4-4227-ae0d-8a5207623a1a\"), new(\"6b55836c-9280-4589-8762-01b0d8172c97\"),\n            new(\"9a65bbfb-8138-4aa5-a572-e5c0a41b540e\"),\n        };\n        Assert.All(result, c => Assert.Contains(c.Id, sendIds));\n    }\n\n    [Fact]\n    public async Task ValidateAsync_SendNotReturnedFromRepository_NotIncludedInOutput()\n    {\n        // Arrange\n        var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();\n        var sendRepository = Substitute.For<ISendRepository>();\n\n        var sut = new SendRotationValidator(\n            sendAuthorizationService,\n            sendRepository\n        );\n\n        var user = new User { Id = new Guid() };\n        var sends = CreateInputSendRequests();\n\n        var userSends = MockUserSends(user);\n        userSends.RemoveAll(c => c.Id == new Guid(\"72e9ac6d-05f4-4227-ae0d-8a5207623a1a\"));\n        sendRepository.GetManyByUserIdAsync(user.Id).Returns(userSends);\n\n        var result = await sut.ValidateAsync(user, sends);\n\n        Assert.DoesNotContain(result, c => c.Id == new Guid(\"72e9ac6d-05f4-4227-ae0d-8a5207623a1a\"));\n    }\n\n    [Fact]\n    public async Task ValidateAsync_InputMissingUserSend_Throws()\n    {\n        // Arrange\n        var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();\n        var sendRepository = Substitute.For<ISendRepository>();\n\n        var sut = new SendRotationValidator(\n            sendAuthorizationService,\n            sendRepository\n        );\n\n        var user = new User { Id = new Guid() };\n        var sends = CreateInputSendRequests();\n\n        var userSends = MockUserSends(user);\n        userSends.Add(new Send { Id = new Guid(), Data = \"{}\" });\n        sendRepository.GetManyByUserIdAsync(user.Id).Returns(userSends);\n\n        // Act, Assert\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sut.ValidateAsync(user, sends));\n    }\n\n    private IEnumerable<SendWithIdRequestModel> CreateInputSendRequests()\n    {\n        return new[]\n        {\n            new SendWithIdRequestModel\n            {\n                AuthType = AuthType.None,\n                DeletionDate = new DateTime(2080, 12, 31),\n                Disabled = false,\n                Id = new Guid(\"72e9ac6d-05f4-4227-ae0d-8a5207623a1a\"),\n                Key = \"Send1Key\",\n                Name = \"Send 1\",\n                Type = SendType.Text,\n                Text = new SendTextModel(new SendTextData(\"Text name\", \"Notes\", \"Encrypted text for Send 1\", false))\n            },\n            new SendWithIdRequestModel\n            {\n                AuthType = AuthType.None,\n                DeletionDate = new DateTime(2080, 12, 31),\n                Disabled = true,\n                Id = new Guid(\"6b55836c-9280-4589-8762-01b0d8172c97\"),\n                Key = \"Send2Key\",\n                Name = \"Send 2\",\n                Type = SendType.Text,\n                Text = new SendTextModel(new SendTextData(\"Text name\", \"Notes\", \"Encrypted text for Send 2\",\n                    false)),\n            },\n            new SendWithIdRequestModel\n            {\n                AuthType = AuthType.None,\n                DeletionDate = new DateTime(2080, 12, 31),\n                Disabled = false,\n                Id = new Guid(\"9a65bbfb-8138-4aa5-a572-e5c0a41b540e\"),\n                Key = \"Send3Key\",\n                Name = \"Send 3\",\n                Type = SendType.File,\n                File = new SendFileModel(new SendFileData(\"File name\", \"Notes\", \"File name here\")),\n                HideEmail = true\n            }\n        };\n    }\n\n    private List<Send> MockUserSends(User user)\n    {\n        return new List<Send>(new[]\n        {\n            new Send\n            {\n                DeletionDate = new DateTime(2080, 12, 31),\n                Disabled = false,\n                Id = new Guid(\"72e9ac6d-05f4-4227-ae0d-8a5207623a1a\"),\n                UserId = user.Id,\n                Key = \"Send1Key\",\n                Type = SendType.Text,\n                Data = JsonSerializer.Serialize(\n                    new SendTextModel(new SendTextData(\"Text name\", \"Notes\", \"Encrypted text for Send 1\", false)),\n                    JsonHelpers.IgnoreWritingNull),\n            },\n            new Send\n            {\n                DeletionDate = new DateTime(2080, 12, 31),\n                Disabled = true,\n                Id = new Guid(\"6b55836c-9280-4589-8762-01b0d8172c97\"),\n                UserId = user.Id,\n                Key = \"Send2Key\",\n                Type = SendType.Text,\n                Data = JsonSerializer.Serialize(\n                    new SendTextModel(new SendTextData(\"Text name\", \"Notes\", \"Encrypted text for Send 2\",\n                        false)),\n                    JsonHelpers.IgnoreWritingNull),\n            },\n            new Send\n            {\n                DeletionDate = new DateTime(2080, 12, 31),\n                Disabled = false,\n                Id = new Guid(\"9a65bbfb-8138-4aa5-a572-e5c0a41b540e\"),\n                UserId = user.Id,\n                Key = \"Send3Key\",\n                Type = SendType.File,\n                Data = JsonSerializer.Serialize(\n                    new SendFileModel(new SendFileData(\"File name\", \"Notes\", \"File name here\")),\n                    JsonHelpers.IgnoreWritingNull),\n                HideEmail = true\n            }\n        });\n    }\n\n\n}\n"
  },
  {
    "path": "test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs",
    "content": "﻿using Bit.Api.Auth.Models.Request.WebAuthn;\nusing Bit.Api.KeyManagement.Validators;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.KeyManagement.Validators;\n\n[SutProviderCustomize]\npublic class WebAuthnLoginKeyRotationValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_Succeeds_ReturnsValidCredentials(\n        SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,\n        IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)\n    {\n        var guid = Guid.NewGuid();\n\n        var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel\n        {\n            Id = guid,\n            EncryptedPublicKey = e.EncryptedPublicKey,\n            EncryptedUserKey = e.EncryptedUserKey\n        }).ToList();\n\n        var data = new WebAuthnCredential\n        {\n            Id = guid,\n            SupportsPrf = true,\n            EncryptedPublicKey = \"TestPublicKey\",\n            EncryptedUserKey = \"TestUserKey\",\n            EncryptedPrivateKey = \"TestPrivateKey\"\n        };\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)\n            .Returns(new List<WebAuthnCredential> { data });\n\n        var result = await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate);\n        Assert.Single(result);\n        Assert.Equal(guid, result.First().Id);\n    }\n\n    [Theory]\n    [BitAutoData(false, null, null, null)]\n    [BitAutoData(true, null, \"TestPublicKey\", \"TestPrivateKey\")]\n    [BitAutoData(true, \"TestUserKey\", null, \"TestPrivateKey\")]\n    [BitAutoData(true, \"TestUserKey\", \"TestPublicKey\", null)]\n    public async Task ValidateAsync_NotEncryptedOrPrfNotSupported_Ignores(\n        bool supportsPrf, string encryptedUserKey, string encryptedPublicKey, string encryptedPrivateKey,\n        SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,\n        IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)\n    {\n        var guid = Guid.NewGuid();\n        var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel\n        {\n            Id = guid,\n            EncryptedUserKey = e.EncryptedUserKey,\n            EncryptedPublicKey = e.EncryptedPublicKey,\n        }).ToList();\n\n        var data = new WebAuthnCredential\n        {\n            Id = guid,\n            SupportsPrf = supportsPrf,\n            EncryptedUserKey = encryptedUserKey,\n            EncryptedPublicKey = encryptedPublicKey,\n            EncryptedPrivateKey = encryptedPrivateKey\n        };\n\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)\n            .Returns(new List<WebAuthnCredential> { data });\n\n        var result = await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate);\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WebAuthnKeysNotMatchingExisting_Throws(\n        SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,\n        IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)\n    {\n        var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel\n        {\n            Id = Guid.Parse(\"00000000-0000-0000-0000-000000000001\"),\n            EncryptedPublicKey = e.EncryptedPublicKey,\n            EncryptedUserKey = e.EncryptedUserKey\n        }).ToList();\n\n        var data = new WebAuthnCredential\n        {\n            Id = Guid.Parse(\"00000000-0000-0000-0000-000000000002\"),\n            SupportsPrf = true,\n            EncryptedPublicKey = \"TestPublicKey\",\n            EncryptedUserKey = \"TestUserKey\",\n            EncryptedPrivateKey = \"TestPrivateKey\"\n        };\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)\n            .Returns(new List<WebAuthnCredential> { data });\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_NullUserKey_Throws(\n        SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,\n        IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)\n    {\n        var guid = Guid.NewGuid();\n        var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e =>\n            new WebAuthnLoginRotateKeyRequestModel\n            {\n                Id = guid,\n                EncryptedPublicKey = e.EncryptedPublicKey,\n                EncryptedUserKey = null\n            }).ToList();\n\n        var data = new WebAuthnCredential\n        {\n            Id = guid,\n            SupportsPrf = true,\n            EncryptedPublicKey = \"TestPublicKey\",\n            EncryptedUserKey = \"TestUserKey\",\n            EncryptedPrivateKey = \"TestPrivateKey\"\n        };\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)\n            .Returns(new List<WebAuthnCredential> { data });\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_NullPublicKey_Throws(\n        SutProvider<WebAuthnLoginKeyRotationValidator> sutProvider, User user,\n        IEnumerable<WebAuthnLoginRotateKeyRequestModel> webauthnRotateCredentialData)\n    {\n        var guid = Guid.NewGuid();\n        var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel\n        {\n            Id = guid,\n            EncryptedUserKey = e.EncryptedUserKey,\n            EncryptedPublicKey = null,\n        }).ToList();\n\n        var data = new WebAuthnCredential\n        {\n            Id = guid,\n            SupportsPrf = true,\n            EncryptedPublicKey = \"TestPublicKey\",\n            EncryptedUserKey = \"TestUserKey\",\n            EncryptedPrivateKey = \"TestPrivateKey\"\n        };\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id)\n            .Returns(new List<WebAuthnCredential> { data });\n\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Models/Request/Accounts/PremiumRequestModelTests.cs",
    "content": "﻿using Bit.Api.Models.Request.Accounts;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\n\nnamespace Bit.Api.Test.Models.Request.Accounts;\n\npublic class PremiumRequestModelTests\n{\n    public static IEnumerable<object[]> GetValidateData()\n    {\n        // 1. selfHosted\n        // 2. formFile\n        // 3. country\n        // 4. expected\n\n        yield return new object[] { true, null, null, false };\n        yield return new object[] { true, null, \"US\", false };\n        yield return new object[] { true, new NotImplementedFormFile(), null, false };\n        yield return new object[] { true, new NotImplementedFormFile(), \"US\", false };\n\n        yield return new object[] { false, null, null, false };\n        yield return new object[] { false, null, \"US\", true }; // Only true, cloud with null license AND a Country\n        yield return new object[] { false, new NotImplementedFormFile(), null, false };\n        yield return new object[] { false, new NotImplementedFormFile(), \"US\", false };\n    }\n\n    [Theory]\n    [MemberData(nameof(GetValidateData))]\n    public void Validate_Success(bool selfHosted, IFormFile formFile, string country, bool expected)\n    {\n        var gs = new GlobalSettings\n        {\n            SelfHosted = selfHosted\n        };\n\n        var sut = new PremiumRequestModel\n        {\n            License = formFile,\n            Country = country,\n        };\n\n        Assert.Equal(expected, sut.Validate(gs));\n    }\n}\n\npublic class NotImplementedFormFile : IFormFile\n{\n    public string ContentType => throw new NotImplementedException();\n\n    public string ContentDisposition => throw new NotImplementedException();\n\n    public IHeaderDictionary Headers => throw new NotImplementedException();\n\n    public long Length => throw new NotImplementedException();\n\n    public string Name => throw new NotImplementedException();\n\n    public string FileName => throw new NotImplementedException();\n\n    public void CopyTo(Stream target) => throw new NotImplementedException();\n    public Task CopyToAsync(Stream target, CancellationToken cancellationToken = default) => throw new NotImplementedException();\n    public Stream OpenReadStream() => throw new NotImplementedException();\n}\n"
  },
  {
    "path": "test/Api.Test/Models/Response/EnvironmentConfigResponseModelTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Api.Models.Response;\nusing Xunit;\n\nnamespace Bit.Api.Test.Models.Response;\n\npublic class EnvironmentConfigResponseModelTests\n{\n    [Fact]\n    public void Serialize_FillAssistRulesNull_OmitsPropertyFromJson()\n    {\n        var model = new EnvironmentConfigResponseModel\n        {\n            CloudRegion = \"US\",\n            Vault = \"https://vault.bitwarden.com\",\n            FillAssistRules = null\n        };\n\n        var json = JsonSerializer.Serialize(model);\n\n        Assert.DoesNotContain(\"FillAssistRules\", json, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Fact]\n    public void Serialize_FillAssistRulesSet_IncludesPropertyInJson()\n    {\n        var expectedUri = \"https://example.com/rules.json\";\n        var model = new EnvironmentConfigResponseModel\n        {\n            CloudRegion = \"US\",\n            Vault = \"https://vault.bitwarden.com\",\n            FillAssistRules = expectedUri\n        };\n\n        var json = JsonSerializer.Serialize(model);\n\n        Assert.Contains(\"FillAssistRules\", json);\n        Assert.Contains(expectedUri, json);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Models/Response/SubscriptionResponseModelTests.cs",
    "content": "﻿using Bit.Api.Models.Response;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Business;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Api.Test.Models.Response;\n\npublic class SubscriptionResponseModelTests\n{\n    [Theory]\n    [BitAutoData]\n    public void Constructor_IncludeMilestone2DiscountTrueMatchingCouponId_ReturnsDiscount(\n        User user,\n        UserLicense license)\n    {\n        // Arrange\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID\n                Active = true,\n                PercentOff = 20m,\n                AmountOff = null,\n                AppliesTo = new List<string> { \"product1\" }\n            }\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);\n\n        // Assert\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);\n        Assert.True(result.CustomerDiscount.Active);\n        Assert.Equal(20m, result.CustomerDiscount.PercentOff);\n        Assert.Null(result.CustomerDiscount.AmountOff);\n        Assert.NotNull(result.CustomerDiscount.AppliesTo);\n        Assert.Single(result.CustomerDiscount.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_IncludeMilestone2DiscountTrueNonMatchingCouponId_ReturnsNull(\n        User user,\n        UserLicense license)\n    {\n        // Arrange\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = \"different-coupon-id\", // Non-matching coupon ID\n                Active = true,\n                PercentOff = 20m,\n                AmountOff = null,\n                AppliesTo = new List<string> { \"product1\" }\n            }\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);\n\n        // Assert\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_IncludeMilestone2DiscountFalseMatchingCouponId_ReturnsNull(\n        User user,\n        UserLicense license)\n    {\n        // Arrange\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID\n                Active = true,\n                PercentOff = 20m,\n                AmountOff = null,\n                AppliesTo = new List<string> { \"product1\" }\n            }\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: false);\n\n        // Assert - Should be null because includeMilestone2Discount is false\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_NullCustomerDiscount_ReturnsNull(\n        User user,\n        UserLicense license)\n    {\n        // Arrange\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = null\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);\n\n        // Assert\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_AmountOffDiscountMatchingCouponId_ReturnsDiscount(\n        User user,\n        UserLicense license)\n    {\n        // Arrange\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,\n                Active = true,\n                PercentOff = null,\n                AmountOff = 14.00m, // Already converted from cents in BillingCustomerDiscount\n                AppliesTo = new List<string>()\n            }\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);\n\n        // Assert\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);\n        Assert.Null(result.CustomerDiscount.PercentOff);\n        Assert.Equal(14.00m, result.CustomerDiscount.AmountOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_DefaultIncludeMilestone2DiscountParameter_ReturnsNull(\n        User user,\n        UserLicense license)\n    {\n        // Arrange\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,\n                Active = true,\n                PercentOff = 20m\n            }\n        };\n\n        // Act - Using default parameter (includeMilestone2Discount defaults to false)\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license);\n\n        // Assert\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_NullDiscountIdIncludeMilestone2DiscountTrue_ReturnsNull(\n        User user,\n        UserLicense license)\n    {\n        // Arrange\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = null, // Null discount ID\n                Active = true,\n                PercentOff = 20m,\n                AmountOff = null,\n                AppliesTo = new List<string> { \"product1\" }\n            }\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);\n\n        // Assert\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_MatchingCouponIdInactiveDiscount_ReturnsNull(\n        User user,\n        UserLicense license)\n    {\n        // Arrange\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, // Matching coupon ID\n                Active = false, // Inactive discount\n                PercentOff = 20m,\n                AmountOff = null,\n                AppliesTo = new List<string> { \"product1\" }\n            }\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);\n\n        // Assert\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_UserOnly_SetsBasicProperties(User user)\n    {\n        // Arrange\n        user.Storage = 5368709120; // 5 GB in bytes\n        user.MaxStorageGb = (short)10;\n        user.PremiumExpirationDate = DateTime.UtcNow.AddMonths(12);\n\n        // Act\n        var result = new SubscriptionResponseModel(user);\n\n        // Assert\n        Assert.NotNull(result.StorageName);\n        Assert.Equal(5.0, result.StorageGb);\n        Assert.Equal((short)10, result.MaxStorageGb);\n        Assert.Equal(user.PremiumExpirationDate, result.Expiration);\n        Assert.Null(result.License);\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_UserAndLicense_IncludesLicense(User user, UserLicense license)\n    {\n        // Arrange\n        user.Storage = 1073741824; // 1 GB in bytes\n        user.MaxStorageGb = (short)5;\n\n        // Act\n        var result = new SubscriptionResponseModel(user, license);\n\n        // Assert\n        Assert.NotNull(result.License);\n        Assert.Equal(license, result.License);\n        Assert.Equal(1.0, result.StorageGb);\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_NullStorage_SetsStorageToZero(User user)\n    {\n        // Arrange\n        user.Storage = null;\n\n        // Act\n        var result = new SubscriptionResponseModel(user);\n\n        // Assert\n        Assert.Null(result.StorageName);\n        Assert.Equal(0, result.StorageGb);\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_NullLicense_ExcludesLicense(User user)\n    {\n        // Act\n        var result = new SubscriptionResponseModel(user, null);\n\n        // Assert\n        Assert.Null(result.License);\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_BothPercentOffAndAmountOffPresent_HandlesEdgeCase(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Edge case: Both PercentOff and AmountOff present\n        // This tests the scenario where Stripe coupon has both discount types\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,\n                Active = true,\n                PercentOff = 25m,\n                AmountOff = 20.00m, // Already converted from cents\n                AppliesTo = new List<string> { \"prod_premium\" }\n            }\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);\n\n        // Assert - Both values should be preserved\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);\n        Assert.Equal(25m, result.CustomerDiscount.PercentOff);\n        Assert.Equal(20.00m, result.CustomerDiscount.AmountOff);\n        Assert.NotNull(result.CustomerDiscount.AppliesTo);\n        Assert.Single(result.CustomerDiscount.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithSubscriptionAndInvoice_MapsAllProperties(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Test with Subscription, UpcomingInvoice, and CustomerDiscount\n        var stripeSubscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\"\n        };\n\n        var stripeInvoice = new Invoice\n        {\n            AmountDue = 1500, // 1500 cents = $15.00\n            Created = DateTime.UtcNow.AddDays(7)\n        };\n\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription),\n            UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice(stripeInvoice),\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,\n                Active = true,\n                PercentOff = 20m,\n                AmountOff = null,\n                AppliesTo = new List<string> { \"prod_premium\" }\n            }\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);\n\n        // Assert - Verify all properties are mapped correctly\n        Assert.NotNull(result.Subscription);\n        Assert.Equal(\"active\", result.Subscription.Status);\n        Assert.Equal(14, result.Subscription.GracePeriod); // charge_automatically = 14 days\n\n        Assert.NotNull(result.UpcomingInvoice);\n        Assert.Equal(15.00m, result.UpcomingInvoice.Amount);\n        Assert.NotNull(result.UpcomingInvoice.Date);\n\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);\n        Assert.True(result.CustomerDiscount.Active);\n        Assert.Equal(20m, result.CustomerDiscount.PercentOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithNullSubscriptionAndInvoice_HandlesNullsGracefully(\n        User user,\n        UserLicense license)\n    {\n        // Arrange - Test with null Subscription and UpcomingInvoice\n        var subscriptionInfo = new SubscriptionInfo\n        {\n            Subscription = null,\n            UpcomingInvoice = null,\n            CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,\n                Active = true,\n                PercentOff = 20m\n            }\n        };\n\n        // Act\n        var result = new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount: true);\n\n        // Assert - Null Subscription and UpcomingInvoice should be handled gracefully\n        Assert.Null(result.Subscription);\n        Assert.Null(result.UpcomingInvoice);\n        Assert.NotNull(result.CustomerDiscount);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/NotificationCenter/Controllers/NotificationsControllerTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.NotificationCenter.Controllers;\nusing Bit.Api.NotificationCenter.Models.Request;\nusing Bit.Core.Models.Data;\nusing Bit.Core.NotificationCenter.Commands.Interfaces;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Filter;\nusing Bit.Core.NotificationCenter.Queries.Interfaces;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.NotificationCenter.Controllers;\n\n[ControllerCustomize(typeof(NotificationsController))]\n[SutProviderCustomize]\npublic class NotificationsControllerTests\n{\n    [Theory]\n    [BitAutoData([null, null])]\n    [BitAutoData([null, false])]\n    [BitAutoData([null, true])]\n    [BitAutoData(false, null)]\n    [BitAutoData(true, null)]\n    [BitAutoData(false, false)]\n    [BitAutoData(false, true)]\n    [BitAutoData(true, false)]\n    [BitAutoData(true, true)]\n    [NotificationStatusDetailsListCustomize(5)]\n    public async Task ListAsync_StatusFilter_ReturnedMatchingNotifications(bool? readStatusFilter, bool? deletedStatusFilter,\n        SutProvider<NotificationsController> sutProvider,\n        IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)\n    {\n        var notificationStatusDetailsList = notificationStatusDetailsEnumerable\n            .OrderByDescending(n => n.Priority)\n            .ThenByDescending(n => n.CreationDate)\n            .ToList();\n\n        sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()\n            .GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())\n            .Returns(new PagedResult<NotificationStatusDetails> { Data = notificationStatusDetailsList });\n\n        var expectedNotificationStatusDetailsMap = notificationStatusDetailsList\n            .Take(10)\n            .ToDictionary(n => n.Id);\n\n        var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel\n        {\n            ReadStatusFilter = readStatusFilter,\n            DeletedStatusFilter = deletedStatusFilter\n        });\n\n        Assert.Equal(\"list\", listResponse.Object);\n        Assert.Equal(5, listResponse.Data.Count());\n        Assert.All(listResponse.Data, notificationResponseModel =>\n        {\n            Assert.Equal(\"notification\", notificationResponseModel.Object);\n            Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id));\n            var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id];\n            Assert.NotNull(expectedNotificationStatusDetails);\n            Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id);\n            Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority);\n            Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title);\n            Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body);\n            Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date);\n            Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);\n            Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);\n            Assert.Equal(expectedNotificationStatusDetails.TaskId, notificationResponseModel.TaskId);\n        });\n        Assert.Null(listResponse.ContinuationToken);\n\n        await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()\n            .Received(1)\n            .GetByUserIdStatusFilterAsync(Arg.Is<NotificationStatusFilter>(filter =>\n                    filter.Read == readStatusFilter && filter.Deleted == deletedStatusFilter),\n                Arg.Is<PageOptions>(pageOptions =>\n                    pageOptions.ContinuationToken == null && pageOptions.PageSize == 10));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationStatusDetailsListCustomize(19)]\n    public async Task ListAsync_PagingRequestNoContinuationToken_ReturnedFirst10MatchingNotifications(\n        SutProvider<NotificationsController> sutProvider,\n        IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)\n    {\n        var notificationStatusDetailsList = notificationStatusDetailsEnumerable\n            .OrderByDescending(n => n.Priority)\n            .ThenByDescending(n => n.CreationDate)\n            .ToList();\n\n        sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()\n            .GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())\n            .Returns(new PagedResult<NotificationStatusDetails>\n            { Data = notificationStatusDetailsList.Take(10).ToList(), ContinuationToken = \"2\" });\n\n        var expectedNotificationStatusDetailsMap = notificationStatusDetailsList\n            .Take(10)\n            .ToDictionary(n => n.Id);\n\n        var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel());\n\n        Assert.Equal(\"list\", listResponse.Object);\n        Assert.Equal(10, listResponse.Data.Count());\n        Assert.All(listResponse.Data, notificationResponseModel =>\n        {\n            Assert.Equal(\"notification\", notificationResponseModel.Object);\n            Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id));\n            var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id];\n            Assert.NotNull(expectedNotificationStatusDetails);\n            Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id);\n            Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority);\n            Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title);\n            Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body);\n            Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date);\n            Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);\n            Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);\n            Assert.Equal(expectedNotificationStatusDetails.TaskId, notificationResponseModel.TaskId);\n        });\n        Assert.Equal(\"2\", listResponse.ContinuationToken);\n\n        await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()\n            .Received(1)\n            .GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(),\n                Arg.Is<PageOptions>(pageOptions =>\n                    pageOptions.ContinuationToken == null && pageOptions.PageSize == 10));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationStatusDetailsListCustomize(19)]\n    public async Task ListAsync_PagingRequestUsingContinuationToken_ReturnedLast9MatchingNotifications(\n        SutProvider<NotificationsController> sutProvider,\n        IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)\n    {\n        var notificationStatusDetailsList = notificationStatusDetailsEnumerable\n            .OrderByDescending(n => n.Priority)\n            .ThenByDescending(n => n.CreationDate)\n            .ToList();\n\n        sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()\n            .GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())\n            .Returns(new PagedResult<NotificationStatusDetails>\n            { Data = notificationStatusDetailsList.Skip(10).ToList() });\n\n        var expectedNotificationStatusDetailsMap = notificationStatusDetailsList\n            .Skip(10)\n            .ToDictionary(n => n.Id);\n\n        var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel { ContinuationToken = \"2\" });\n\n        Assert.Equal(\"list\", listResponse.Object);\n        Assert.Equal(9, listResponse.Data.Count());\n        Assert.All(listResponse.Data, notificationResponseModel =>\n        {\n            Assert.Equal(\"notification\", notificationResponseModel.Object);\n            Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id));\n            var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id];\n            Assert.NotNull(expectedNotificationStatusDetails);\n            Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id);\n            Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority);\n            Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title);\n            Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body);\n            Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date);\n            Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);\n            Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);\n            Assert.Equal(expectedNotificationStatusDetails.TaskId, notificationResponseModel.TaskId);\n        });\n        Assert.Null(listResponse.ContinuationToken);\n\n        await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()\n            .Received(1)\n            .GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(),\n                Arg.Is<PageOptions>(pageOptions =>\n                    pageOptions.ContinuationToken == \"2\" && pageOptions.PageSize == 10));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkAsDeletedAsync_NotificationId_MarkedAsDeleted(\n        SutProvider<NotificationsController> sutProvider,\n        Guid notificationId)\n    {\n        await sutProvider.Sut.MarkAsDeletedAsync(notificationId);\n\n        await sutProvider.GetDependency<IMarkNotificationDeletedCommand>()\n            .Received(1)\n            .MarkDeletedAsync(notificationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkAsReadAsync_NotificationId_MarkedAsRead(\n        SutProvider<NotificationsController> sutProvider,\n        Guid notificationId)\n    {\n        await sutProvider.Sut.MarkAsReadAsync(notificationId);\n\n        await sutProvider.GetDependency<IMarkNotificationReadCommand>()\n            .Received(1)\n            .MarkReadAsync(notificationId);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/NotificationCenter/Models/Request/NotificationFilterRequestModelTests.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing Bit.Api.NotificationCenter.Models.Request;\nusing Xunit;\n\nnamespace Bit.Api.Test.NotificationCenter.Models.Request;\n\npublic class NotificationFilterRequestModelTests\n{\n    [Theory]\n    [InlineData(\"invalid\")]\n    [InlineData(\"-1\")]\n    [InlineData(\"0\")]\n    public void Validate_ContinuationTokenInvalidNumber_Invalid(string continuationToken)\n    {\n        var model = new NotificationFilterRequestModel\n        {\n            ContinuationToken = continuationToken,\n        };\n        var result = Validate(model);\n        Assert.Single(result);\n        Assert.Contains(\"Continuation token must be a positive, non zero integer.\", result[0].ErrorMessage);\n        Assert.Contains(\"ContinuationToken\", result[0].MemberNames);\n    }\n\n    [Fact]\n    public void Validate_ContinuationTokenMaxLengthExceeded_Invalid()\n    {\n        var model = new NotificationFilterRequestModel\n        {\n            ContinuationToken = \"1234567890\"\n        };\n        var result = Validate(model);\n        Assert.Single(result);\n        Assert.Contains(\"The field ContinuationToken must be a string with a maximum length of 9.\",\n            result[0].ErrorMessage);\n        Assert.Contains(\"ContinuationToken\", result[0].MemberNames);\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\" \")]\n    [InlineData(\"1\")]\n    [InlineData(\"123456789\")]\n    public void Validate_ContinuationTokenCorrect_Valid(string? continuationToken)\n    {\n        var model = new NotificationFilterRequestModel\n        {\n            ContinuationToken = continuationToken\n        };\n        var result = Validate(model);\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [InlineData(9)]\n    [InlineData(1001)]\n    public void Validate_PageSizeInvalidRange_Invalid(int pageSize)\n    {\n        var model = new NotificationFilterRequestModel\n        {\n            PageSize = pageSize\n        };\n        var result = Validate(model);\n        Assert.Single(result);\n        Assert.Contains(\"The field PageSize must be between 10 and 1000.\", result[0].ErrorMessage);\n        Assert.Contains(\"PageSize\", result[0].MemberNames);\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(10)]\n    [InlineData(1000)]\n    public void Validate_PageSizeCorrect_Valid(int? pageSize)\n    {\n        var model = pageSize == null\n            ? new NotificationFilterRequestModel()\n            : new NotificationFilterRequestModel\n            {\n                PageSize = pageSize.Value\n            };\n        var result = Validate(model);\n        Assert.Empty(result);\n    }\n\n    private static List<ValidationResult> Validate(NotificationFilterRequestModel model)\n    {\n        var results = new List<ValidationResult>();\n        Validator.TryValidateObject(model, new ValidationContext(model), results, true);\n        return results;\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/NotificationCenter/Models/Response/NotificationResponseModelTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.NotificationCenter.Models.Response;\nusing Bit.Core.Enums;\nusing Bit.Core.NotificationCenter.Enums;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Xunit;\n\nnamespace Bit.Api.Test.NotificationCenter.Models.Response;\n\npublic class NotificationResponseModelTests\n{\n    [Fact]\n    public void Constructor_NotificationStatusDetailsNull_CorrectFields()\n    {\n        Assert.Throws<ArgumentNullException>(() => new NotificationResponseModel(null!));\n    }\n\n    [Fact]\n    public void Constructor_NotificationStatusDetails_CorrectFields()\n    {\n        var notificationStatusDetails = new NotificationStatusDetails\n        {\n            Id = Guid.NewGuid(),\n            Global = true,\n            Priority = Priority.High,\n            ClientType = ClientType.All,\n            Title = \"Test Title\",\n            Body = \"Test Body\",\n            TaskId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow - TimeSpan.FromMinutes(3),\n            ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(1),\n            DeletedDate = DateTime.UtcNow,\n        };\n        var model = new NotificationResponseModel(notificationStatusDetails);\n\n        Assert.Equal(model.Id, notificationStatusDetails.Id);\n        Assert.Equal(model.Priority, notificationStatusDetails.Priority);\n        Assert.Equal(model.Title, notificationStatusDetails.Title);\n        Assert.Equal(model.Body, notificationStatusDetails.Body);\n        Assert.Equal(model.Date, notificationStatusDetails.RevisionDate);\n        Assert.Equal(model.ReadDate, notificationStatusDetails.ReadDate);\n        Assert.Equal(model.DeletedDate, notificationStatusDetails.DeletedDate);\n        Assert.Equal(model.TaskId, notificationStatusDetails.TaskId);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.Platform.Push;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.PushRegistration;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Platform.Push.Controllers;\n\n[ControllerCustomize(typeof(PushController))]\n[SutProviderCustomize]\npublic class PushControllerTests\n{\n    [Theory]\n    [BitAutoData(false, true)]\n    [BitAutoData(false, false)]\n    [BitAutoData(true, true)]\n    public async Task RegisterAsync_InstallationIdNotSetOrSelfHosted_BadRequest(bool haveInstallationId,\n        bool selfHosted,\n        SutProvider<PushController> sutProvider, Guid installationId, Guid userId, Guid identifier, Guid deviceId)\n    {\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted = selfHosted;\n        if (haveInstallationId)\n        {\n            sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);\n        }\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterAsync(new PushRegistrationRequestModel\n            {\n                DeviceId = deviceId.ToString(),\n                PushToken = \"test-push-token\",\n                UserId = userId.ToString(),\n                Type = DeviceType.Android,\n                Identifier = identifier.ToString(),\n            }));\n\n        Assert.Equal(\"Not correctly configured for push relays.\", exception.Message);\n\n        await sutProvider.GetDependency<IPushRegistrationService>().Received(0)\n            .CreateOrUpdateRegistrationAsync(Arg.Any<PushRegistrationData>(), Arg.Any<string>(), Arg.Any<string>(),\n                Arg.Any<string>(), Arg.Any<DeviceType>(), Arg.Any<IEnumerable<string>>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(false)]\n    [BitAutoData(true)]\n    public async Task RegisterAsync_ValidModel_CreatedOrUpdatedRegistration(bool haveOrganizationId,\n        SutProvider<PushController> sutProvider, Guid installationId, Guid userId, Guid identifier, Guid deviceId,\n        Guid organizationId)\n    {\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;\n        sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);\n\n        var expectedUserId = $\"{installationId}_{userId}\";\n        var expectedIdentifier = $\"{installationId}_{identifier}\";\n        var expectedDeviceId = $\"{installationId}_{deviceId}\";\n        var expectedOrganizationId = $\"{installationId}_{organizationId}\";\n\n        var model = new PushRegistrationRequestModel\n        {\n            DeviceId = deviceId.ToString(),\n            PushToken = \"test-push-token\",\n            UserId = userId.ToString(),\n            Type = DeviceType.Android,\n            Identifier = identifier.ToString(),\n            OrganizationIds = haveOrganizationId ? [organizationId.ToString()] : null,\n            InstallationId = installationId\n        };\n\n        await sutProvider.Sut.RegisterAsync(model);\n\n        await sutProvider.GetDependency<IPushRegistrationService>().Received(1)\n            .CreateOrUpdateRegistrationAsync(\n                Arg.Is<PushRegistrationData>(data => data == new PushRegistrationData(model.PushToken)),\n                expectedDeviceId, expectedUserId,\n                expectedIdentifier, DeviceType.Android, Arg.Do<IEnumerable<string>>(organizationIds =>\n                {\n                    Assert.NotNull(organizationIds);\n                    var organizationIdsList = organizationIds.ToList();\n                    if (haveOrganizationId)\n                    {\n                        Assert.Contains(expectedOrganizationId, organizationIdsList);\n                        Assert.Single(organizationIdsList);\n                    }\n                    else\n                    {\n                        Assert.Empty(organizationIdsList);\n                    }\n                }), installationId);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Platform/SsoCookieVendor/Controllers/SsoCookieVendorControllerTests.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Api.Platform.SsoCookieVendor;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Platform.SsoCookieVendor.Controllers;\n\npublic class SsoCookieVendorControllerTests : IDisposable\n{\n    private readonly SsoCookieVendorController _sut;\n    private readonly GlobalSettings _globalSettings;\n\n    public SsoCookieVendorControllerTests()\n    {\n        _globalSettings = new GlobalSettings\n        {\n            Communication = new GlobalSettings.CommunicationSettings\n            {\n                Bootstrap = \"ssoCookieVendor\",\n                SsoCookieVendor = new GlobalSettings.SsoCookieVendorSettings\n                {\n                    CookieName = \"test-cookie\"\n                }\n            }\n        };\n        _sut = new SsoCookieVendorController(_globalSettings);\n    }\n\n    public void Dispose()\n    {\n        _sut?.Dispose();\n    }\n\n    private void MockHttpContextWithCookies(Dictionary<string, string> cookies)\n    {\n        var httpContext = new DefaultHttpContext();\n        var cookieCollection = Substitute.For<IRequestCookieCollection>();\n\n        // Mock the TryGetValue method\n        cookieCollection.TryGetValue(Arg.Any<string>(), out Arg.Any<string?>())\n            .Returns(callInfo =>\n            {\n                var key = callInfo.ArgAt<string>(0);\n                if (cookies.TryGetValue(key, out var value))\n                {\n                    callInfo[1] = value;\n                    return true;\n                }\n                callInfo[1] = null;\n                return false;\n            });\n\n        // Mock the indexer if needed\n        cookieCollection[Arg.Any<string>()].Returns(callInfo =>\n        {\n            var key = callInfo.ArgAt<string>(0);\n            return cookies.TryGetValue(key, out var value) ? value : null;\n        });\n\n        httpContext.Request.Cookies = cookieCollection;\n        _sut.ControllerContext = new ControllerContext { HttpContext = httpContext };\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\"none\")]\n    public void Get_WhenBootstrapNotConfigured_Returns404(string? bootstrap)\n    {\n        // Arrange\n#nullable disable\n        _globalSettings.Communication.Bootstrap = bootstrap;\n#nullable restore\n        MockHttpContextWithCookies([]);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        Assert.IsType<NotFoundResult>(result);\n    }\n\n    [Fact]\n    public void Get_WhenCookieNameNotConfigured_Returns500()\n    {\n        // Arrange\n        _globalSettings.Communication.SsoCookieVendor.CookieName = string.Empty;\n        MockHttpContextWithCookies([]);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        var statusCodeResult = Assert.IsType<ObjectResult>(result);\n        Assert.Equal(500, statusCodeResult.StatusCode);\n    }\n\n    [Fact]\n    public void Get_WhenCookieNameIsEmpty_Returns500()\n    {\n        // Arrange\n        _globalSettings.Communication.SsoCookieVendor.CookieName = \"\";\n        MockHttpContextWithCookies([]);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        var statusCodeResult = Assert.IsType<ObjectResult>(result);\n        Assert.Equal(500, statusCodeResult.StatusCode);\n    }\n\n    [Fact]\n    public void Get_WhenSingleCookieExists_ReturnsRedirectWithCorrectUri()\n    {\n        // Arrange\n        var cookies = new Dictionary<string, string>\n        {\n            { \"test-cookie\", \"my-token-value-123\" }\n        };\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        var redirectResult = Assert.IsType<RedirectResult>(result);\n        Assert.Equal(\"bitwarden://sso-cookie-vendor?test-cookie=my-token-value-123&d=1\", redirectResult.Url);\n    }\n\n    [Fact]\n    public void Get_WhenSingleCookieHasSpecialCharacters_EncodesCorrectly()\n    {\n        // Arrange\n        var cookies = new Dictionary<string, string>\n        {\n            { \"test-cookie\", \"value with spaces & special=chars!\" }\n        };\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        var redirectResult = Assert.IsType<RedirectResult>(result);\n        Assert.Contains(\"value%20with%20spaces\", redirectResult.Url);\n        Assert.Contains(\"%26\", redirectResult.Url); // & encoded\n        Assert.Contains(\"%3D\", redirectResult.Url); // = encoded\n        Assert.Contains(\"%21\", redirectResult.Url); // ! encoded\n    }\n\n    [Fact]\n    public void Get_WhenShardedCookiesExist_ReturnsRedirectWithShardedUri()\n    {\n        // Arrange\n        var cookies = new Dictionary<string, string>\n        {\n            { \"test-cookie-0\", \"part1\" },\n            { \"test-cookie-1\", \"part2\" },\n            { \"test-cookie-2\", \"part3\" }\n        };\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        var redirectResult = Assert.IsType<RedirectResult>(result);\n        Assert.StartsWith(\"bitwarden://sso-cookie-vendor?\", redirectResult.Url);\n        Assert.Contains(\"test-cookie-0=part1\", redirectResult.Url);\n        Assert.Contains(\"test-cookie-1=part2\", redirectResult.Url);\n        Assert.Contains(\"test-cookie-2=part3\", redirectResult.Url);\n        Assert.EndsWith(\"d=1\", redirectResult.Url);\n    }\n\n    [Fact]\n    public void Get_WhenShardedCookiesWithGap_StopsAtFirstGap()\n    {\n        // Arrange\n        var cookies = new Dictionary<string, string>\n        {\n            { \"test-cookie-0\", \"part0\" },\n            { \"test-cookie-1\", \"part1\" },\n            // Missing test-cookie-2\n            { \"test-cookie-3\", \"part3\" },\n            { \"test-cookie-4\", \"part4\" }\n        };\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        var redirectResult = Assert.IsType<RedirectResult>(result);\n        Assert.Contains(\"test-cookie-0=part0\", redirectResult.Url);\n        Assert.Contains(\"test-cookie-1=part1\", redirectResult.Url);\n        Assert.DoesNotContain(\"test-cookie-3\", redirectResult.Url);\n        Assert.DoesNotContain(\"test-cookie-4\", redirectResult.Url);\n        Assert.EndsWith(\"d=1\", redirectResult.Url);\n    }\n\n    [Fact]\n    public void Get_WhenOnlyGappedShardsExist_Returns404()\n    {\n        // Arrange - only test-cookie-2 exists, not test-cookie-0 or test-cookie-1\n        var cookies = new Dictionary<string, string>\n        {\n            { \"test-cookie-2\", \"part2\" },\n            { \"test-cookie-3\", \"part3\" }\n        };\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        Assert.IsType<NotFoundObjectResult>(result);\n    }\n\n    [Fact]\n    public void Get_WhenNoCookiesFound_Returns404()\n    {\n        // Arrange\n        MockHttpContextWithCookies([]);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        Assert.IsType<NotFoundObjectResult>(result);\n    }\n\n    [Fact]\n    public void Get_WhenUnrelatedCookiesExist_Returns404()\n    {\n        // Arrange\n        var cookies = new Dictionary<string, string>\n        {\n            { \"other-cookie\", \"value\" },\n            { \"another-cookie\", \"value2\" }\n        };\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        Assert.IsType<NotFoundObjectResult>(result);\n    }\n\n    [Fact]\n    public void Get_WhenUriExceedsMaxLength_Returns400()\n    {\n        // Arrange - create a very long cookie value that will exceed 8192 characters\n        // URI format: \"bitwarden://sso-cookie-vendor?test-cookie={value}\"\n        // Base URI length is about 43 characters, so we need value > 8149\n        var longValue = new string('a', 8200);\n        var cookies = new Dictionary<string, string>\n        {\n            { \"test-cookie\", longValue }\n        };\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        Assert.IsType<BadRequestResult>(result);\n    }\n\n    [Fact]\n    public void Get_WhenSingleCookiePreferredOverSharded_ReturnsSingleCookie()\n    {\n        // Arrange - both single and sharded cookies exist\n        var cookies = new Dictionary<string, string>\n        {\n            { \"test-cookie\", \"single-value\" },\n            { \"test-cookie-0\", \"shard0\" },\n            { \"test-cookie-1\", \"shard1\" }\n        };\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        var redirectResult = Assert.IsType<RedirectResult>(result);\n        Assert.Equal(\"bitwarden://sso-cookie-vendor?test-cookie=single-value&d=1\", redirectResult.Url);\n    }\n\n    [Fact]\n    public void Get_WhenEmptyCookieValue_TreatsAsNotFound()\n    {\n        // Arrange\n        var cookies = new Dictionary<string, string>\n        {\n            { \"test-cookie\", \"\" }\n        };\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        Assert.IsType<NotFoundObjectResult>(result);\n    }\n\n    [Fact]\n    public void Get_WhenShardedCookiesHaveMaxCount_ProcessesAllShards()\n    {\n        // Arrange - create 20 sharded cookies (MaxShardCount)\n        var cookies = new Dictionary<string, string>();\n        for (var i = 0; i < 20; i++)\n        {\n            cookies[$\"test-cookie-{i}\"] = $\"part{i}\";\n        }\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        var redirectResult = Assert.IsType<RedirectResult>(result);\n        for (var i = 0; i < 20; i++)\n        {\n            Assert.Contains($\"test-cookie-{i}=part{i}\", redirectResult.Url);\n        }\n        Assert.EndsWith(\"d=1\", redirectResult.Url);\n    }\n\n    [Fact]\n    public void Get_WhenShardedCookiesExceedMaxCount_OnlyProcessesFirst20()\n    {\n        // Arrange - create 25 sharded cookies (more than MaxShardCount of 20)\n        var cookies = new Dictionary<string, string>();\n        for (var i = 0; i < 25; i++)\n        {\n            cookies[$\"test-cookie-{i}\"] = $\"part{i}\";\n        }\n        MockHttpContextWithCookies(cookies);\n\n        // Act\n        var result = _sut.Get();\n\n        // Assert\n        var redirectResult = Assert.IsType<RedirectResult>(result);\n        // Should contain first 20\n        for (var i = 0; i < 20; i++)\n        {\n            Assert.Contains($\"test-cookie-{i}=part{i}\", redirectResult.Url);\n        }\n        // Should NOT contain 21-25\n        for (var i = 20; i < 25; i++)\n        {\n            Assert.DoesNotContain($\"test-cookie-{i}=part{i}\", redirectResult.Url);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Public/Controllers/CollectionsControllerTests.cs",
    "content": "﻿using Bit.Api.Models.Public.Response;\nusing Bit.Api.Public.Controllers;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Public.Controllers;\n\n[ControllerCustomize(typeof(CollectionsController))]\n[SutProviderCustomize]\npublic class CollectionsControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task Get_WithDefaultUserCollection_ReturnsNotFound(\n        Collection collection, SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        collection.Type = CollectionType.DefaultUserCollection;\n        var access = new CollectionAccessDetails\n        {\n            Groups = new List<CollectionAccessSelection>(),\n            Users = new List<CollectionAccessSelection>()\n        };\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationId.Returns(collection.OrganizationId);\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdWithAccessAsync(collection.Id)\n            .Returns(new Tuple<Collection?, CollectionAccessDetails>(collection, access));\n\n        // Act\n        var result = await sutProvider.Sut.Get(collection.Id);\n\n        // Assert\n        Assert.IsType<NotFoundResult>(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Get_WithSharedCollection_ReturnsCollection(\n        Collection collection, SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        collection.Type = CollectionType.SharedCollection;\n        var access = new CollectionAccessDetails\n        {\n            Groups = [],\n            Users = []\n        };\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationId.Returns(collection.OrganizationId);\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdWithAccessAsync(collection.Id)\n            .Returns(new Tuple<Collection?, CollectionAccessDetails>(collection, access));\n\n        // Act\n        var result = await sutProvider.Sut.Get(collection.Id);\n\n        // Assert\n        var jsonResult = Assert.IsType<JsonResult>(result);\n        var response = Assert.IsType<CollectionResponseModel>(jsonResult.Value);\n        Assert.Equal(collection.Id, response.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Delete_WithDefaultUserCollection_ReturnsBadRequest(\n        Collection collection, SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        collection.Type = CollectionType.DefaultUserCollection;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationId.Returns(collection.OrganizationId);\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdAsync(collection.Id)\n            .Returns(collection);\n\n        // Act\n        var result = await sutProvider.Sut.Delete(collection.Id);\n\n        // Assert\n        var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);\n        var errorResponse = Assert.IsType<ErrorResponseModel>(badRequestResult.Value);\n        Assert.Contains(\"You cannot delete a collection with the type as DefaultUserCollection\", errorResponse.Message);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .DeleteAsync(Arg.Any<Collection>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Delete_WithSharedCollection_ReturnsOk(\n        Collection collection, SutProvider<CollectionsController> sutProvider)\n    {\n        // Arrange\n        collection.Type = CollectionType.SharedCollection;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationId.Returns(collection.OrganizationId);\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetByIdAsync(collection.Id)\n            .Returns(collection);\n\n        // Act\n        var result = await sutProvider.Sut.Delete(collection.Id);\n\n        // Assert\n        Assert.IsType<OkResult>(result);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .DeleteAsync(collection);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs",
    "content": "﻿#nullable enable\nusing System.Security.Claims;\nusing Bit.Api.SecretsManager.Controllers;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Api.Test.SecretsManager.Controllers;\n\n[ControllerCustomize(typeof(AccessPoliciesController))]\n[SutProviderCustomize]\n[ProjectCustomize]\n[JsonDocumentCustomize]\npublic class AccessPoliciesControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetPeoplePotentialGrantees_UserWithoutPermission_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetPeoplePotentialGranteesAsync(id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .GetPeopleGranteesAsync(id, Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetPeoplePotentialGrantees_HasAccessNoPotentialGrantees_ReturnsEmptyList(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(Guid.NewGuid());\n        sutProvider.GetDependency<IAccessPolicyRepository>().GetPeopleGranteesAsync(id, Arg.Any<Guid>())\n            .ReturnsForAnyArgs(new PeopleGrantees\n            {\n                UserGrantees = new List<UserGrantee>(),\n                GroupGrantees = new List<GroupGrantee>()\n            });\n\n        var result = await sutProvider.Sut.GetPeoplePotentialGranteesAsync(id);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)\n            .GetPeopleGranteesAsync(id, Arg.Any<Guid>());\n        Assert.Empty(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetPeoplePotentialGrantees_Success(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id,\n        GroupGrantee groupGrantee)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(Guid.NewGuid());\n        sutProvider.GetDependency<IAccessPolicyRepository>().GetPeopleGranteesAsync(id, Arg.Any<Guid>())\n            .ReturnsForAnyArgs(new PeopleGrantees\n            {\n                UserGrantees = new List<UserGrantee>(),\n                GroupGrantees = new List<GroupGrantee> { groupGrantee }\n            });\n\n        var result = await sutProvider.Sut.GetPeoplePotentialGranteesAsync(id);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)\n            .GetPeopleGranteesAsync(id, Arg.Any<Guid>());\n\n        Assert.NotEmpty(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountsPotentialGranteesAsync_UserWithoutPermission_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetServiceAccountsPotentialGranteesAsync(id));\n\n        await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs()\n            .GetManyByOrganizationIdWriteAccessAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountsPotentialGrantees_HasAccessNoPotentialGrantees_ReturnsEmptyList(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);\n\n        var result = await sutProvider.Sut.GetServiceAccountsPotentialGranteesAsync(id);\n\n        await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)\n            .GetManyByOrganizationIdWriteAccessAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)),\n                Arg.Is(AssertHelper.AssertPropertyEqual(id)),\n                Arg.Any<AccessClientType>());\n\n        Assert.Empty(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountsPotentialGranteesAsync_Success(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id,\n        ServiceAccount serviceAccount)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .GetManyByOrganizationIdWriteAccessAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .ReturnsForAnyArgs(new List<ServiceAccount> { serviceAccount });\n\n        var result = await sutProvider.Sut.GetServiceAccountsPotentialGranteesAsync(id);\n\n        await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)\n            .GetManyByOrganizationIdWriteAccessAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)),\n                Arg.Is(AssertHelper.AssertPropertyEqual(id)),\n                Arg.Any<AccessClientType>());\n\n        Assert.NotEmpty(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectPotentialGrantees_UserWithoutPermission_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetProjectPotentialGranteesAsync(id));\n\n        await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()\n            .GetManyByOrganizationIdWriteAccessAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectPotentialGrantees_HasAccessNoPotentialGrantees_ReturnsEmptyList(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);\n\n        var result = await sutProvider.Sut.GetProjectPotentialGranteesAsync(id);\n\n        await sutProvider.GetDependency<IProjectRepository>().Received(1)\n            .GetManyByOrganizationIdWriteAccessAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)),\n                Arg.Is(AssertHelper.AssertPropertyEqual(id)),\n                Arg.Any<AccessClientType>());\n\n        Assert.Empty(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectPotentialGrantees_Success(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id,\n        Project project)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);\n        sutProvider.GetDependency<IProjectRepository>()\n            .GetManyByOrganizationIdWriteAccessAsync(default, default, default)\n            .ReturnsForAnyArgs(new List<Project> { project });\n\n        var result = await sutProvider.Sut.GetProjectPotentialGranteesAsync(id);\n\n        await sutProvider.GetDependency<IProjectRepository>().Received(1)\n            .GetManyByOrganizationIdWriteAccessAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)),\n                Arg.Is(AssertHelper.AssertPropertyEqual(id)),\n                Arg.Any<AccessClientType>());\n\n        Assert.NotEmpty(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectPeopleAccessPolicies_ProjectDoesNotExist_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsNull();\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectPeopleAccessPolicies_NoAccessSecretsManager_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).Returns(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Any<Guid>())\n            .ReturnsForAnyArgs(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task GetProjectPeopleAccessPolicies_UserWithoutPermission_ThrowsNotFound(\n        bool canRead,\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).Returns(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Any<Guid>())\n            .ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .ReturnsForAnyArgs((canRead, false));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectPeopleAccessPolicies_ServiceAccountClient_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data)\n    {\n        SetupProjectAccessPoliciesTest(sutProvider, data, AccessClientType.ServiceAccount);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task GetProjectPeopleAccessPolicies_ReturnsEmptyList(\n        AccessClientType accessClientType,\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data)\n    {\n        SetupProjectAccessPoliciesTest(sutProvider, data, accessClientType);\n\n        var result = await sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(data.Id);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)\n            .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.Id)),\n                Arg.Any<Guid>());\n\n        Assert.Empty(result.GroupAccessPolicies);\n        Assert.Empty(result.UserAccessPolicies);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task GetProjectPeopleAccessPolicies_Success(\n        AccessClientType accessClientType,\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data,\n        UserProjectAccessPolicy resultUserPolicy,\n        GroupProjectAccessPolicy resultGroupPolicy)\n    {\n        SetupProjectAccessPoliciesTest(sutProvider, data, accessClientType);\n\n        sutProvider.GetDependency<IAccessPolicyRepository>().GetPeoplePoliciesByGrantedProjectIdAsync(default, default)\n            .ReturnsForAnyArgs(new List<BaseAccessPolicy> { resultUserPolicy, resultGroupPolicy });\n\n        var result = await sutProvider.Sut.GetProjectPeopleAccessPoliciesAsync(data.Id);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)\n            .GetPeoplePoliciesByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.Id)),\n                Arg.Any<Guid>());\n\n        Assert.NotEmpty(result.GroupAccessPolicies);\n        Assert.NotEmpty(result.UserAccessPolicies);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutProjectPeopleAccessPolicies_ProjectDoesNotExist_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid id,\n        PeopleAccessPoliciesRequestModel request)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.PutProjectPeopleAccessPoliciesAsync(id, request));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .ReplaceProjectPeopleAsync(Arg.Any<ProjectPeopleAccessPolicies>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutProjectPeopleAccessPoliciesAsync_DuplicatePolicy_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project project,\n        PeopleAccessPoliciesRequestModel request)\n    {\n        var dup = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = true, Write = true };\n        request.UserAccessPolicyRequests = new[] { dup, dup };\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(project);\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.PutProjectPeopleAccessPoliciesAsync(project.Id, request));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .ReplaceProjectPeopleAsync(Arg.Any<ProjectPeopleAccessPolicies>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutProjectPeopleAccessPoliciesAsync_NoAccess_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project project,\n        PeopleAccessPoliciesRequestModel request)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(project);\n        var peoplePolicies = request.ToProjectPeopleAccessPolicies(project.Id, project.OrganizationId);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), peoplePolicies,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.PutProjectPeopleAccessPoliciesAsync(project.Id, request));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .ReplaceProjectPeopleAsync(Arg.Any<ProjectPeopleAccessPolicies>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutProjectPeopleAccessPoliciesAsync_Success(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid userId,\n        Project project,\n        PeopleAccessPoliciesRequestModel request)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(project);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        var peoplePolicies = request.ToProjectPeopleAccessPolicies(project.Id, project.OrganizationId);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), peoplePolicies,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<IAccessPolicyRepository>().ReplaceProjectPeopleAsync(peoplePolicies, Arg.Any<Guid>())\n            .Returns(peoplePolicies.ToBaseAccessPolicies());\n\n        await sutProvider.Sut.PutProjectPeopleAccessPoliciesAsync(project.Id, request);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)\n            .ReplaceProjectPeopleAsync(Arg.Any<ProjectPeopleAccessPolicies>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountPeopleAccessPoliciesAsync_ServiceAccountDoesntExist_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsNull();\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetServiceAccountPeopleAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .GetPeoplePoliciesByGrantedServiceAccountIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountPeopleAccessPoliciesAsync_NoAccessSecretsManager_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId)\n            .ReturnsForAnyArgs(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetServiceAccountPeopleAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .GetPeoplePoliciesByGrantedServiceAccountIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task GetServiceAccountPeopleAccessPoliciesAsync_UserWithoutPermission_Throws(\n        bool canRead,\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId)\n            .ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .ReturnsForAnyArgs((canRead, false));\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetServiceAccountPeopleAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .GetPeoplePoliciesByGrantedServiceAccountIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountPeopleAccessPoliciesAsync_HasPermissionNoPolicies_ReturnsEmptyList(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default)\n            .ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .ReturnsForAnyArgs((true, true));\n\n        var result = await sutProvider.Sut.GetServiceAccountPeopleAccessPoliciesAsync(data.Id);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)\n            .GetPeoplePoliciesByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.Id)),\n                Arg.Any<Guid>());\n\n        Assert.Empty(result.UserAccessPolicies);\n        Assert.Empty(result.GroupAccessPolicies);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountPeopleAccessPoliciesAsync_Success(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        UserServiceAccountAccessPolicy resultAccessPolicy)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default)\n            .ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .AccessToServiceAccountAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .ReturnsForAnyArgs((true, true));\n\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetPeoplePoliciesByGrantedServiceAccountIdAsync(default, default)\n            .ReturnsForAnyArgs(new List<BaseAccessPolicy> { resultAccessPolicy });\n\n        var result = await sutProvider.Sut.GetServiceAccountPeopleAccessPoliciesAsync(data.Id);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)\n            .GetPeoplePoliciesByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.Id)),\n                Arg.Any<Guid>());\n\n        Assert.Empty(result.GroupAccessPolicies);\n        Assert.NotEmpty(result.UserAccessPolicies);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountPeopleAccessPolicies_ServiceAccountDoesNotExist_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        PeopleAccessPoliciesRequestModel request)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .ReplaceServiceAccountPeopleAsync(Arg.Any<ServiceAccountPeopleAccessPolicies>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountPeopleAccessPolicies_DuplicatePolicy_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        PeopleAccessPoliciesRequestModel request)\n    {\n        var dup = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = true, Write = true };\n        request.UserAccessPolicyRequests = new[] { dup, dup };\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .ReplaceServiceAccountPeopleAsync(Arg.Any<ServiceAccountPeopleAccessPolicies>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountPeopleAccessPolicies_NotCanReadWrite_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        PeopleAccessPoliciesRequestModel request)\n    {\n        request.UserAccessPolicyRequests.First().Read = false;\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .ReplaceServiceAccountPeopleAsync(Arg.Any<ServiceAccountPeopleAccessPolicies>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountPeopleAccessPolicies_NoAccess_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        PeopleAccessPoliciesRequestModel request)\n    {\n        request = SetRequestToCanReadWrite(request);\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n        var peoplePolicies = request.ToServiceAccountPeopleAccessPolicies(data.Id, data.OrganizationId);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), peoplePolicies,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs()\n            .ReplaceServiceAccountPeopleAsync(Arg.Any<ServiceAccountPeopleAccessPolicies>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountPeopleAccessPolicies_Success(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        Guid userId,\n        PeopleAccessPoliciesRequestModel request)\n    {\n        request = SetRequestToCanReadWrite(request);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n        var peoplePolicies = request.ToServiceAccountPeopleAccessPolicies(data.Id, data.OrganizationId);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), peoplePolicies,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .ReplaceServiceAccountPeopleAsync(peoplePolicies, Arg.Any<Guid>())\n            .Returns(peoplePolicies.ToBaseAccessPolicies());\n\n        await sutProvider.Sut.PutServiceAccountPeopleAccessPoliciesAsync(data.Id, request);\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)\n            .ReplaceServiceAccountPeopleAsync(Arg.Any<ServiceAccountPeopleAccessPolicies>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountGrantedPoliciesAsync_NoAccess_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)\n            .GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Arg.Any<Guid>(), Arg.Any<Guid>(),\n                Arg.Any<AccessClientType>());\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task GetServiceAccountGrantedPoliciesAsync_HasAccessNoPolicies_ReturnsEmptyList(\n        AccessClientType accessClientType,\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid userId,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), data.OrganizationId).Returns((accessClientType, userId));\n\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Arg.Any<Guid>(), Arg.Any<Guid>(),\n                Arg.Any<AccessClientType>())\n            .ReturnsNull();\n\n        var result = await sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(data.Id);\n\n        Assert.Empty(result.GrantedProjectPolicies);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task GetServiceAccountGrantedPoliciesAsync_HasAccess_Success(\n        AccessClientType accessClientType,\n        SutProvider<AccessPoliciesController> sutProvider,\n        Guid userId,\n        ServiceAccountGrantedPoliciesPermissionDetails policies,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), data.OrganizationId).Returns((accessClientType, userId));\n\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Arg.Any<Guid>(), Arg.Any<Guid>(),\n                Arg.Any<AccessClientType>())\n            .Returns(policies);\n\n        var result = await sutProvider.Sut.GetServiceAccountGrantedPoliciesAsync(data.Id);\n\n        Assert.NotEmpty(result.GrantedProjectPolicies);\n        Assert.Equal(policies.ProjectGrantedPolicies.Count(), result.GrantedProjectPolicies.Count);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountGrantedPoliciesAsync_ServiceAccountDoesNotExist_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        ServiceAccountGrantedPoliciesRequestModel request)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountGrantedPoliciesAsync_DuplicatePolicyRequest_ThrowsBadRequestException(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        ServiceAccountGrantedPoliciesRequestModel request)\n    {\n        var dup = new GrantedAccessPolicyRequest { GrantedId = Guid.NewGuid(), Read = true, Write = true };\n        request.ProjectGrantedPolicyRequests = new[] { dup, dup };\n\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountGrantedPoliciesAsync_InvalidPolicyRequest_ThrowsBadRequestException(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        ServiceAccountGrantedPoliciesRequestModel request)\n    {\n        var policyRequest = new GrantedAccessPolicyRequest { GrantedId = Guid.NewGuid(), Read = false, Write = true };\n        request.ProjectGrantedPolicyRequests = new[] { policyRequest };\n\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountGrantedPoliciesAsync_UserHasNoAccess_ThrowsNotFoundException(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        ServiceAccountGrantedPoliciesRequestModel request)\n    {\n        request = SetupValidRequest(request);\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<ServiceAccountGrantedPoliciesUpdates>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutServiceAccountGrantedPoliciesAsync_Success(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data,\n        ServiceAccountGrantedPoliciesRequestModel request)\n    {\n        request = SetupValidRequest(request);\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<ServiceAccountGrantedPoliciesUpdates>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());\n\n        await sutProvider.Sut.PutServiceAccountGrantedPoliciesAsync(data.Id, request);\n\n        await sutProvider.GetDependency<IUpdateServiceAccountGrantedPoliciesCommand>().Received(1)\n            .UpdateAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_ProjectDoesntExist_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsNull();\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)\n            .GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_NoAccessSecretsManager_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).Returns(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Any<Guid>()).ReturnsForAnyArgs(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)\n            .GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_NoAccess_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).Returns(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)\n            .ReturnsForAnyArgs((false, false));\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)\n            .GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_ClientIsServiceAccount_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data)\n    {\n        SetupProjectAccessPoliciesTest(sutProvider, data, AccessClientType.ServiceAccount);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)\n            .GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_HasAccessNoPolicies_ReturnsEmptyList(\n        AccessClientType accessClientType,\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data)\n    {\n        SetupProjectAccessPoliciesTest(sutProvider, data, accessClientType);\n\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>())\n            .ReturnsNullForAnyArgs();\n\n        var result = await sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id);\n\n        Assert.Empty(result.ServiceAccountAccessPolicies);\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task GetProjectServiceAccountsAccessPoliciesAsync_HasAccess_Success(\n        AccessClientType accessClientType,\n        SutProvider<AccessPoliciesController> sutProvider,\n        ProjectServiceAccountsAccessPolicies policies,\n        Project data)\n    {\n        SetupProjectAccessPoliciesTest(sutProvider, data, accessClientType);\n\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetProjectServiceAccountsAccessPoliciesAsync(Arg.Any<Guid>())\n            .ReturnsForAnyArgs(policies);\n\n        var result = await sutProvider.Sut.GetProjectServiceAccountsAccessPoliciesAsync(data.Id);\n\n        Assert.NotEmpty(result.ServiceAccountAccessPolicies);\n        Assert.Equal(policies.ServiceAccountAccessPolicies.Count(), result.ServiceAccountAccessPolicies.Count);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutProjectServiceAccountsAccessPoliciesAsync_ProjectDoesNotExist_Throws(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data,\n        ProjectServiceAccountsAccessPoliciesRequestModel request)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutProjectServiceAccountsAccessPoliciesAsync_DuplicatePolicyRequest_ThrowsBadRequestException(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data,\n        ProjectServiceAccountsAccessPoliciesRequestModel request)\n    {\n        var dup = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = true, Write = true };\n        request.ServiceAccountAccessPolicyRequests = [dup, dup];\n\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutProjectServiceAccountsAccessPoliciesAsync_InvalidPolicyRequest_ThrowsBadRequestException(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data,\n        ProjectServiceAccountsAccessPoliciesRequestModel request)\n    {\n        var policyRequest = new AccessPolicyRequest { GranteeId = Guid.NewGuid(), Read = false, Write = true };\n        request.ServiceAccountAccessPolicyRequests = [policyRequest];\n\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutProjectServiceAccountsAccessPoliciesAsync_UserHasNoAccess_ThrowsNotFoundException(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data,\n        ProjectServiceAccountsAccessPoliciesRequestModel request)\n    {\n        request = SetupValidRequest(request);\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request));\n\n        await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutProjectServiceAccountsAccessPoliciesAsync_Success(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Project data,\n        ProjectServiceAccountsAccessPoliciesRequestModel request)\n    {\n        request = SetupValidRequest(request);\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(data.Id).ReturnsForAnyArgs(data);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());\n\n        await sutProvider.Sut.PutProjectServiceAccountsAccessPoliciesAsync(data.Id, request);\n\n        await sutProvider.GetDependency<IUpdateProjectServiceAccountsAccessPoliciesCommand>().Received(1)\n            .UpdateAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretAccessPoliciesAsync_NoAccess_ThrowsNotFound(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Secret data)\n    {\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(data.Id).Returns(data);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetSecretAccessPoliciesAsync(data.Id));\n\n        await sutProvider.GetDependency<IAccessPolicyRepository>().Received(0)\n            .GetSecretAccessPoliciesAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretAccessPoliciesAsync_HasAccessNoPolicies_ReturnsEmptyList(\n        SutProvider<AccessPoliciesController> sutProvider,\n        Secret data)\n    {\n        SetupSecretAccessPoliciesTest(sutProvider, data);\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetSecretAccessPoliciesAsync(Arg.Any<Guid>(), Arg.Any<Guid>())\n            .ReturnsNull();\n\n        var result = await sutProvider.Sut.GetSecretAccessPoliciesAsync(data.Id);\n\n        Assert.Empty(result.UserAccessPolicies);\n        Assert.Empty(result.GroupAccessPolicies);\n        Assert.Empty(result.ServiceAccountAccessPolicies);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretAccessPoliciesAsync_HasAccess_Success(\n        SutProvider<AccessPoliciesController> sutProvider,\n        SecretAccessPolicies policies,\n        Secret data)\n    {\n        SetupSecretAccessPoliciesTest(sutProvider, data);\n        sutProvider.GetDependency<IAccessPolicyRepository>()\n            .GetSecretAccessPoliciesAsync(Arg.Any<Guid>(), Arg.Any<Guid>())\n            .Returns(policies);\n\n        var result = await sutProvider.Sut.GetSecretAccessPoliciesAsync(data.Id);\n\n        Assert.NotEmpty(result.UserAccessPolicies);\n        Assert.NotEmpty(result.GroupAccessPolicies);\n        Assert.NotEmpty(result.ServiceAccountAccessPolicies);\n    }\n\n    private static PeopleAccessPoliciesRequestModel SetRequestToCanReadWrite(PeopleAccessPoliciesRequestModel request)\n    {\n        foreach (var ap in request.UserAccessPolicyRequests)\n        {\n            ap.Read = true;\n            ap.Write = true;\n        }\n\n        foreach (var ap in request.GroupAccessPolicyRequests)\n        {\n            ap.Read = true;\n            ap.Write = true;\n        }\n\n        return request;\n    }\n\n    private static ServiceAccountGrantedPoliciesRequestModel SetupValidRequest(\n        ServiceAccountGrantedPoliciesRequestModel request)\n    {\n        foreach (var policyRequest in request.ProjectGrantedPolicyRequests)\n        {\n            policyRequest.Read = true;\n        }\n\n        return request;\n    }\n\n    private static ProjectServiceAccountsAccessPoliciesRequestModel SetupValidRequest(\n        ProjectServiceAccountsAccessPoliciesRequestModel request)\n    {\n        foreach (var policyRequest in request.ServiceAccountAccessPolicyRequests)\n        {\n            policyRequest.Read = true;\n        }\n\n        return request;\n    }\n\n    private static void SetupProjectAccessPoliciesTest(SutProvider<AccessPoliciesController> sutProvider, Project data,\n        AccessClientType accessClientType)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Any<Guid>())\n            .ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IProjectRepository>()\n            .AccessToProjectAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>())\n            .ReturnsForAnyArgs((true, true));\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Guid>())\n            .ReturnsForAnyArgs((accessClientType, Guid.NewGuid()));\n    }\n\n    private static void SetupSecretAccessPoliciesTest(SutProvider<AccessPoliciesController> sutProvider, Secret data)\n    {\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(data.Id).Returns(data);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(Guid.NewGuid());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Controllers/CountsControllerTests.cs",
    "content": "﻿#nullable enable\nusing System.Security.Claims;\nusing Bit.Api.SecretsManager.Controllers;\nusing Bit.Api.SecretsManager.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.SecretsManager.Controllers;\n\n[ControllerCustomize(typeof(CountsController))]\n[SutProviderCustomize]\n[ProjectCustomize]\n[JsonDocumentCustomize]\npublic class CountsControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetByOrganizationAsync_NoAccess_Throws(SutProvider<CountsController> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetByOrganizationAsync(organizationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByOrganizationAsync_ServiceAccountAccess_Throws(SutProvider<CountsController> sutProvider,\n        Guid organizationId, Guid userId)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);\n\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), organizationId)\n            .Returns((AccessClientType.ServiceAccount, userId));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetByOrganizationAsync(organizationId));\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task GetByOrganizationAsync_HasAccess_Success(AccessClientType accessClientType,\n        SutProvider<CountsController> sutProvider, Guid organizationId, Guid userId,\n        OrganizationCountsResponseModel expectedCountsResponseModel)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);\n\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), organizationId).Returns((accessClientType, userId));\n\n        sutProvider.GetDependency<IProjectRepository>()\n            .GetProjectCountByOrganizationIdAsync(organizationId, userId, accessClientType)\n            .Returns(expectedCountsResponseModel.Projects);\n\n        sutProvider.GetDependency<ISecretRepository>()\n            .GetSecretsCountByOrganizationIdAsync(organizationId, userId, accessClientType)\n            .Returns(expectedCountsResponseModel.Secrets);\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .GetServiceAccountCountByOrganizationIdAsync(organizationId, userId, accessClientType)\n            .Returns(expectedCountsResponseModel.ServiceAccounts);\n\n        var response = await sutProvider.Sut.GetByOrganizationAsync(organizationId);\n\n        Assert.Equal(expectedCountsResponseModel.Projects, response.Projects);\n        Assert.Equal(expectedCountsResponseModel.Secrets, response.Secrets);\n        Assert.Equal(expectedCountsResponseModel.ServiceAccounts, response.ServiceAccounts);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByProjectAsync_ProjectNotFound_Throws(SutProvider<CountsController> sutProvider,\n        Guid projectId)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(projectId).Returns(default(Project));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetByProjectAsync(projectId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByProjectAsync_NoAccess_Throws(SutProvider<CountsController> sutProvider, Project project)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetByProjectAsync(project.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByProjectAsync_ServiceAccountAccess_Throws(SutProvider<CountsController> sutProvider,\n        Guid userId, Project project)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);\n\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), project.OrganizationId)\n            .Returns((AccessClientType.ServiceAccount, userId));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetByProjectAsync(project.Id));\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task GetByProjectAsync_HasAccess_Success(AccessClientType accessClientType,\n        SutProvider<CountsController> sutProvider, Guid userId, Project project,\n        ProjectCountsResponseModel expectedProjectCountsResponseModel)\n    {\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), project.OrganizationId)\n            .Returns((accessClientType, userId));\n\n        sutProvider.GetDependency<IProjectRepository>()\n            .GetProjectCountsByIdAsync(project.Id, userId, accessClientType)\n            .Returns(new ProjectCounts\n            {\n                Secrets = expectedProjectCountsResponseModel.Secrets,\n                People = expectedProjectCountsResponseModel.People,\n                ServiceAccounts = expectedProjectCountsResponseModel.ServiceAccounts\n            });\n\n        var response = await sutProvider.Sut.GetByProjectAsync(project.Id);\n\n        Assert.Equal(expectedProjectCountsResponseModel.Secrets, response.Secrets);\n        Assert.Equal(expectedProjectCountsResponseModel.People, response.People);\n        Assert.Equal(expectedProjectCountsResponseModel.ServiceAccounts, response.ServiceAccounts);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByServiceAccountAsync_ServiceAccountNotFound_Throws(SutProvider<CountsController> sutProvider,\n        Guid serviceAccountId)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(serviceAccountId)\n            .Returns(default(ServiceAccount));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetByServiceAccountAsync(serviceAccountId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByServiceAccountAsync_NoAccess_Throws(SutProvider<CountsController> sutProvider,\n        ServiceAccount serviceAccount)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(serviceAccount.Id)\n            .Returns(serviceAccount);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetByServiceAccountAsync(serviceAccount.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByServiceAccountAsync_ServiceAccountAccess_Throws(SutProvider<CountsController> sutProvider,\n        Guid userId, ServiceAccount serviceAccount)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId).Returns(true);\n\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), serviceAccount.OrganizationId)\n            .Returns((AccessClientType.ServiceAccount, userId));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetByServiceAccountAsync(serviceAccount.Id));\n    }\n\n    [Theory]\n    [BitAutoData(AccessClientType.NoAccessCheck)]\n    [BitAutoData(AccessClientType.User)]\n    public async Task GetByServiceAccountAsync_HasAccess_Success(AccessClientType accessClientType,\n        SutProvider<CountsController> sutProvider, Guid userId, ServiceAccount serviceAccount,\n        ServiceAccountCountsResponseModel expectedServiceAccountCountsResponseModel)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(serviceAccount.Id)\n            .Returns(serviceAccount);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(serviceAccount.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), serviceAccount.OrganizationId)\n            .Returns((accessClientType, userId));\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .GetServiceAccountCountsByIdAsync(serviceAccount.Id, userId, accessClientType)\n            .Returns(new ServiceAccountCounts\n            {\n                Projects = expectedServiceAccountCountsResponseModel.Projects,\n                People = expectedServiceAccountCountsResponseModel.People,\n                AccessTokens = expectedServiceAccountCountsResponseModel.AccessTokens\n            });\n\n        var response = await sutProvider.Sut.GetByServiceAccountAsync(serviceAccount.Id);\n\n        Assert.Equal(expectedServiceAccountCountsResponseModel.Projects, response.Projects);\n        Assert.Equal(expectedServiceAccountCountsResponseModel.People, response.People);\n        Assert.Equal(expectedServiceAccountCountsResponseModel.AccessTokens, response.AccessTokens);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.SecretsManager.Controllers;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.Test.SecretsManager.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.Projects.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.SecretsManager.Controllers;\n\n[ControllerCustomize(typeof(ProjectsController))]\n[SutProviderCustomize]\n[ProjectCustomize]\n[JsonDocumentCustomize]\npublic class ProjectsControllerTests\n{\n    private static void SetupAdmin(SutProvider<ProjectsController> sutProvider, Guid organizationId)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);\n    }\n\n    private static void SetupUserWithPermission(SutProvider<ProjectsController> sutProvider, Guid organizationId)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ListByOrganization_SmAccessDenied_Throws(SutProvider<ProjectsController> sutProvider, Guid data)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.ListByOrganizationAsync(data));\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission)]\n    public async Task ListByOrganization_ReturnsEmptyList(PermissionType permissionType,\n        SutProvider<ProjectsController> sutProvider, Guid data)\n    {\n        switch (permissionType)\n        {\n            case PermissionType.RunAsAdmin:\n                SetupAdmin(sutProvider, data);\n                break;\n            case PermissionType.RunAsUserWithPermission:\n                SetupUserWithPermission(sutProvider, data);\n                break;\n        }\n\n        var result = await sutProvider.Sut.ListByOrganizationAsync(data);\n\n        await sutProvider.GetDependency<IProjectRepository>().Received(1)\n            .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)), Arg.Any<Guid>(),\n                Arg.Any<AccessClientType>());\n        Assert.Empty(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission)]\n    public async Task ListByOrganization_Success(PermissionType permissionType,\n        SutProvider<ProjectsController> sutProvider, Guid data, Project mockProject)\n    {\n        switch (permissionType)\n        {\n            case PermissionType.RunAsAdmin:\n                SetupAdmin(sutProvider, data);\n                break;\n            case PermissionType.RunAsUserWithPermission:\n                SetupUserWithPermission(sutProvider, data);\n                break;\n        }\n\n        sutProvider.GetDependency<IProjectRepository>().GetManyByOrganizationIdAsync(default, default, default)\n            .ReturnsForAnyArgs(new List<ProjectPermissionDetails> { new() { Project = mockProject, Read = true, Write = true } });\n\n        var result = await sutProvider.Sut.ListByOrganizationAsync(data);\n\n        await sutProvider.GetDependency<IProjectRepository>().Received(1)\n            .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)), Arg.Any<Guid>(),\n                Arg.Any<AccessClientType>());\n        Assert.NotEmpty(result.Data);\n        Assert.Single(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Create_NoAccess_Throws(SutProvider<ProjectsController> sutProvider,\n        Guid orgId, ProjectCreateRequestModel data)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(orgId),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n\n        var resultProject = data.ToProject(orgId);\n\n        sutProvider.GetDependency<ICreateProjectCommand>().CreateAsync(default, default, sutProvider.GetDependency<ICurrentContext>().IdentityClientType)\n            .ReturnsForAnyArgs(resultProject);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(orgId, data));\n        await sutProvider.GetDependency<ICreateProjectCommand>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(Arg.Any<Project>(), Arg.Any<Guid>(), sutProvider.GetDependency<ICurrentContext>().IdentityClientType);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Create_AtMaxProjects_Throws(SutProvider<ProjectsController> sutProvider,\n        Guid orgId, ProjectCreateRequestModel data)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(orgId),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n        sutProvider.GetDependency<IMaxProjectsQuery>().GetByOrgIdAsync(orgId, 1).Returns(((short)3, true));\n\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(orgId, data));\n\n        await sutProvider.GetDependency<ICreateProjectCommand>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(Arg.Any<Project>(), Arg.Any<Guid>(), sutProvider.GetDependency<ICurrentContext>().IdentityClientType);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Create_Success(SutProvider<ProjectsController> sutProvider,\n        Guid orgId, ProjectCreateRequestModel data)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(orgId),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n\n        var resultProject = data.ToProject(orgId);\n\n        sutProvider.GetDependency<ICreateProjectCommand>().CreateAsync(default, default, sutProvider.GetDependency<ICurrentContext>().IdentityClientType)\n            .ReturnsForAnyArgs(resultProject);\n\n        await sutProvider.Sut.CreateAsync(orgId, data);\n\n        await sutProvider.GetDependency<ICreateProjectCommand>().Received(1)\n            .CreateAsync(Arg.Any<Project>(), Arg.Any<Guid>(), sutProvider.GetDependency<ICurrentContext>().IdentityClientType);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Update_NoAccess_Throws(SutProvider<ProjectsController> sutProvider,\n        Guid userId, ProjectUpdateRequestModel data, Project existingProject)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(existingProject.Id),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(existingProject.Id).ReturnsForAnyArgs(existingProject);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n\n        var resultProject = data.ToProject(existingProject.Id);\n        sutProvider.GetDependency<IUpdateProjectCommand>().UpdateAsync(default)\n            .ReturnsForAnyArgs(resultProject);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(existingProject.Id, data));\n        await sutProvider.GetDependency<IUpdateProjectCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<Project>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Update_Success(SutProvider<ProjectsController> sutProvider,\n        Guid userId, ProjectUpdateRequestModel data, Project existingProject)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(existingProject.Id),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(existingProject.Id).ReturnsForAnyArgs(existingProject);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n\n        var resultProject = data.ToProject(existingProject.Id);\n        sutProvider.GetDependency<IUpdateProjectCommand>().UpdateAsync(default)\n            .ReturnsForAnyArgs(resultProject);\n\n        await sutProvider.Sut.UpdateAsync(existingProject.Id, data);\n\n        await sutProvider.GetDependency<IUpdateProjectCommand>().Received(1)\n            .UpdateAsync(Arg.Any<Project>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Get_SmAccessDenied_Throws(SutProvider<ProjectsController> sutProvider, Guid data, Guid orgId)\n    {\n        SetupAdmin(sutProvider, orgId);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(orgId).Returns(false);\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAsync(data));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Get_ThrowsNotFound(SutProvider<ProjectsController> sutProvider, Guid data, Guid orgId)\n    {\n        SetupAdmin(sutProvider, orgId);\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAsync(data));\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission)]\n    public async Task Get_Success(PermissionType permissionType, SutProvider<ProjectsController> sutProvider,\n        Guid orgId, Guid data)\n    {\n        switch (permissionType)\n        {\n            case PermissionType.RunAsAdmin:\n                SetupAdmin(sutProvider, orgId);\n                break;\n            case PermissionType.RunAsUserWithPermission:\n                SetupUserWithPermission(sutProvider, orgId);\n                sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)\n                    .Returns((true, true));\n                break;\n        }\n\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(Arg.Is(data))\n            .ReturnsForAnyArgs(new Project { Id = data, OrganizationId = orgId });\n\n        sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)\n            .ReturnsForAnyArgs((true, false));\n\n        await sutProvider.Sut.GetAsync(data);\n\n        await sutProvider.GetDependency<IProjectRepository>().Received(1)\n            .GetByIdAsync(Arg.Is(data));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Get_UserWithoutPermission_Throws(SutProvider<ProjectsController> sutProvider, Guid orgId,\n        Guid data)\n    {\n        SetupUserWithPermission(sutProvider, orgId);\n        sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)\n            .Returns((false, false));\n\n        sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(Arg.Is(data))\n            .ReturnsForAnyArgs(new Project { Id = data, OrganizationId = orgId });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAsync(data));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDeleteProjects_NoProjectsFound_ThrowsNotFound(\n        SutProvider<ProjectsController> sutProvider, List<Project> data)\n    {\n        var ids = data.Select(project => project.Id).ToList();\n        sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List<Project>());\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDeleteProjects_ProjectsFoundMisMatch_ThrowsNotFound(\n        SutProvider<ProjectsController> sutProvider, List<Project> data, Project mockProject)\n    {\n        data.Add(mockProject);\n        var ids = data.Select(project => project.Id).ToList();\n        sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List<Project> { mockProject });\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDeleteProjects_OrganizationMistMatch_ThrowsNotFound(\n        SutProvider<ProjectsController> sutProvider, List<Project> data)\n    {\n\n        var ids = data.Select(project => project.Id).ToList();\n        sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDeleteProjects_NoAccessToSecretsManager_ThrowsNotFound(\n        SutProvider<ProjectsController> sutProvider, List<Project> data)\n    {\n\n        var ids = data.Select(project => project.Id).ToList();\n        var organizationId = data.First().OrganizationId;\n        foreach (var project in data)\n        {\n            project.OrganizationId = organizationId;\n        }\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(false);\n        sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDeleteProjects_ReturnsAccessDeniedForProjectsWithoutAccess_Success(\n        SutProvider<ProjectsController> sutProvider, Guid userId, List<Project> data)\n    {\n\n        var ids = data.Select(project => project.Id).ToList();\n        var organizationId = data.First().OrganizationId;\n        foreach (var project in data)\n        {\n            project.OrganizationId = organizationId;\n            sutProvider.GetDependency<IAuthorizationService>()\n                .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), project,\n                    Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        }\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        var results = await sutProvider.Sut.BulkDeleteAsync(ids);\n        Assert.Equal(data.Count, results.Data.Count());\n        Assert.Equal(\"access denied\", results.Data.First().Error);\n\n        data.Remove(data.First());\n        await sutProvider.GetDependency<IDeleteProjectCommand>().Received(1)\n            .DeleteProjects(Arg.Is(AssertHelper.AssertPropertyEqual(data)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDeleteProjects_Success(SutProvider<ProjectsController> sutProvider, Guid userId, List<Project> data)\n    {\n        var ids = data.Select(project => project.Id).ToList();\n        var organizationId = data.First().OrganizationId;\n        foreach (var project in data)\n        {\n            project.OrganizationId = organizationId;\n            sutProvider.GetDependency<IAuthorizationService>()\n                .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), project,\n                    Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        }\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);\n\n        var results = await sutProvider.Sut.BulkDeleteAsync(ids);\n        await sutProvider.GetDependency<IDeleteProjectCommand>().Received(1)\n            .DeleteProjects(Arg.Is(AssertHelper.AssertPropertyEqual(data)));\n        Assert.Equal(data.Count, results.Data.Count());\n        foreach (var result in results.Data)\n        {\n            Assert.Null(result.Error);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Controllers/RequestSMAccessControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.SecretsManager.Controllers;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Commands.Requests.Interfaces;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Api.Test.SecretsManager.Controllers;\n\n[ControllerCustomize(typeof(RequestSMAccessController))]\n[SutProviderCustomize]\npublic class RequestSMAccessControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task RequestSMAccessFromAdmins_WhenSendingNoModel_ShouldThrowNotFoundException(\n    User user, SutProvider<RequestSMAccessController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(Arg.Any<string>()).ReturnsNullForAnyArgs();\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RequestSMAccessFromAdmins(new RequestSMAccessRequestModel()));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RequestSMAccessFromAdmins_WhenSendingValidData_ShouldSucceed(\n    User user,\n    RequestSMAccessRequestModel model,\n    Core.AdminConsole.Entities.Organization org,\n    ICollection<OrganizationUserUserDetails> orgUsers,\n    SutProvider<RequestSMAccessController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(model.OrganizationId).Returns(org);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(orgUsers);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationUser(model.OrganizationId).Returns(true);\n\n        await sutProvider.Sut.RequestSMAccessFromAdmins(model);\n\n        //Also check that the command was called\n        await sutProvider.GetDependency<IRequestSMAccessCommand>()\n            .Received(1)\n            .SendRequestAccessToSM(org, orgUsers, user, model.EmailContent);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RequestSMAccessFromAdmins_WhenUserInvalid_ShouldThrowBadRequestException(RequestSMAccessRequestModel model, SutProvider<RequestSMAccessController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNullForAnyArgs();\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.RequestSMAccessFromAdmins(model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RequestSMAccessFromAdmins_WhenOrgInvalid_ShouldThrowNotFoundException(RequestSMAccessRequestModel model, User user, SutProvider<RequestSMAccessController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(Arg.Any<string>()).ReturnsNullForAnyArgs();\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationUser(model.OrganizationId).Returns(true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RequestSMAccessFromAdmins(model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RequestSMAccessFromAdmins_WhenOrgUserInvalid_ShouldThrowNotFoundException(RequestSMAccessRequestModel model, User user, SutProvider<RequestSMAccessController> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(Arg.Any<string>()).ReturnsNullForAnyArgs();\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationUser(model.OrganizationId).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RequestSMAccessFromAdmins(model));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Controllers/SecretVersionsControllerTests.cs",
    "content": "﻿using Bit.Api.SecretsManager.Controllers;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.SecretsManager.Controllers;\n\n[ControllerCustomize(typeof(SecretVersionsController))]\n[SutProviderCustomize]\n[SecretCustomize]\npublic class SecretVersionsControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetVersionsBySecretId_SecretNotFound_Throws(\n        SutProvider<SecretVersionsController> sutProvider,\n        Guid secretId)\n    {\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secretId).Returns((Secret?)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetVersionsBySecretIdAsync(secretId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetVersionsBySecretId_NoAccess_Throws(\n        SutProvider<SecretVersionsController> sutProvider,\n        Secret secret)\n    {\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secret.Id).Returns(secret);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetVersionsBySecretIdAsync(secret.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetVersionsBySecretId_NoReadAccess_Throws(\n        SutProvider<SecretVersionsController> sutProvider,\n        Secret secret,\n        Guid userId)\n    {\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secret.Id).Returns(secret);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(secret.OrganizationId).Returns(false);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default)\n            .ReturnsForAnyArgs((false, false));\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetVersionsBySecretIdAsync(secret.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetVersionsBySecretId_Success(\n        SutProvider<SecretVersionsController> sutProvider,\n        Secret secret,\n        List<SecretVersion> versions,\n        Guid userId)\n    {\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secret.Id).Returns(secret);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(secret.OrganizationId).Returns(false);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default)\n            .ReturnsForAnyArgs((true, false));\n\n        foreach (var version in versions)\n        {\n            version.SecretId = secret.Id;\n        }\n        sutProvider.GetDependency<ISecretVersionRepository>().GetManyBySecretIdAsync(secret.Id).Returns(versions);\n\n        var result = await sutProvider.Sut.GetVersionsBySecretIdAsync(secret.Id);\n\n        Assert.Equal(versions.Count, result.Data.Count());\n        await sutProvider.GetDependency<ISecretVersionRepository>().Received(1)\n            .GetManyBySecretIdAsync(Arg.Is(secret.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetById_VersionNotFound_Throws(\n        SutProvider<SecretVersionsController> sutProvider,\n        Guid versionId)\n    {\n        sutProvider.GetDependency<ISecretVersionRepository>().GetByIdAsync(versionId).Returns((SecretVersion?)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByIdAsync(versionId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetById_Success(\n        SutProvider<SecretVersionsController> sutProvider,\n        SecretVersion version,\n        Secret secret,\n        Guid userId)\n    {\n        version.SecretId = secret.Id;\n        sutProvider.GetDependency<ISecretVersionRepository>().GetByIdAsync(version.Id).Returns(version);\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secret.Id).Returns(secret);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(secret.OrganizationId).Returns(false);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default)\n            .ReturnsForAnyArgs((true, false));\n\n        var result = await sutProvider.Sut.GetByIdAsync(version.Id);\n\n        Assert.Equal(version.Id, result.Id);\n        Assert.Equal(version.SecretId, result.SecretId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RestoreVersion_NoWriteAccess_Throws(\n        SutProvider<SecretVersionsController> sutProvider,\n        Secret secret,\n        SecretVersion version,\n        RestoreSecretVersionRequestModel request,\n        Guid userId)\n    {\n        version.SecretId = secret.Id;\n        request.VersionId = version.Id;\n\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secret.Id).Returns(secret);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(secret.OrganizationId).Returns(false);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default)\n            .ReturnsForAnyArgs((true, false));\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.RestoreVersionAsync(secret.Id, request));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RestoreVersion_VersionNotFound_Throws(\n        SutProvider<SecretVersionsController> sutProvider,\n        Secret secret,\n        RestoreSecretVersionRequestModel request,\n        Guid userId)\n    {\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secret.Id).Returns(secret);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default)\n            .ReturnsForAnyArgs((true, true));\n        sutProvider.GetDependency<ISecretVersionRepository>().GetByIdAsync(request.VersionId).Returns((SecretVersion?)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.RestoreVersionAsync(secret.Id, request));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RestoreVersion_VersionBelongsToDifferentSecret_Throws(\n        SutProvider<SecretVersionsController> sutProvider,\n        Secret secret,\n        SecretVersion version,\n        RestoreSecretVersionRequestModel request,\n        Guid userId)\n    {\n        version.SecretId = Guid.NewGuid(); // Different secret\n        request.VersionId = version.Id;\n\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secret.Id).Returns(secret);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default)\n            .ReturnsForAnyArgs((true, true));\n        sutProvider.GetDependency<ISecretVersionRepository>().GetByIdAsync(request.VersionId).Returns(version);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.RestoreVersionAsync(secret.Id, request));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RestoreVersion_Success(\n        SutProvider<SecretVersionsController> sutProvider,\n        Secret secret,\n        SecretVersion version,\n        RestoreSecretVersionRequestModel request,\n        Guid userId,\n        OrganizationUser organizationUser)\n    {\n        version.SecretId = secret.Id;\n        request.VersionId = version.Id;\n        var versionValue = version.Value;\n        organizationUser.OrganizationId = secret.OrganizationId;\n        organizationUser.UserId = userId;\n\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(secret.Id).Returns(secret);\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default)\n            .ReturnsForAnyArgs((true, true));\n        sutProvider.GetDependency<ISecretVersionRepository>().GetByIdAsync(request.VersionId).Returns(version);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(secret.OrganizationId, userId).Returns(organizationUser);\n        sutProvider.GetDependency<ISecretRepository>().UpdateAsync(Arg.Any<Secret>()).Returns(x => x.Arg<Secret>());\n\n        var result = await sutProvider.Sut.RestoreVersionAsync(secret.Id, request);\n\n        await sutProvider.GetDependency<ISecretRepository>().Received(1)\n            .UpdateAsync(Arg.Is<Secret>(s => s.Value == versionValue));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_EmptyIds_Throws(\n        SutProvider<SecretVersionsController> sutProvider)\n    {\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.BulkDeleteAsync(new List<Guid>()));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_VersionNotFound_Throws(\n        SutProvider<SecretVersionsController> sutProvider,\n        List<Guid> ids)\n    {\n        sutProvider.GetDependency<ISecretVersionRepository>().GetByIdAsync(ids[0]).Returns((SecretVersion?)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.BulkDeleteAsync(ids));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_NoWriteAccess_Throws(\n        SutProvider<SecretVersionsController> sutProvider,\n        List<SecretVersion> versions,\n        Secret secret,\n        Guid userId)\n    {\n        var ids = versions.Select(v => v.Id).ToList();\n        foreach (var version in versions)\n        {\n            version.SecretId = secret.Id;\n            sutProvider.GetDependency<ISecretVersionRepository>().GetByIdAsync(version.Id).Returns(version);\n        }\n\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<Secret> { secret });\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(secret.OrganizationId).Returns(false);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default)\n            .ReturnsForAnyArgs((true, false));\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.BulkDeleteAsync(ids));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_Success(\n        SutProvider<SecretVersionsController> sutProvider,\n        List<SecretVersion> versions,\n        Secret secret,\n        Guid userId)\n    {\n        var ids = versions.Select(v => v.Id).ToList();\n        foreach (var version in versions)\n        {\n            version.SecretId = secret.Id;\n        }\n\n        sutProvider.GetDependency<ISecretVersionRepository>().GetManyByIdsAsync(ids).Returns(versions);\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<Secret> { secret });\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().IdentityClientType.Returns(IdentityClientType.ServiceAccount);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(secret.OrganizationId).Returns(true);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(secret.Id, userId, default)\n            .ReturnsForAnyArgs((true, true));\n\n        await sutProvider.Sut.BulkDeleteAsync(ids);\n\n        await sutProvider.GetDependency<ISecretVersionRepository>().Received(1)\n            .DeleteManyByIdAsync(Arg.Is<IEnumerable<Guid>>(x => x.SequenceEqual(ids)));\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.SecretsManager.Controllers;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Api.Test.SecretsManager.Enums;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Commands.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;\nusing Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Interfaces;\nusing Bit.Core.SecretsManager.Queries.Secrets.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Http;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.SecretsManager.Controllers;\n\n[ControllerCustomize(typeof(SecretsController))]\n[SutProviderCustomize]\n[JsonDocumentCustomize]\n[SecretCustomize]\npublic class SecretsControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretsByOrganization_ReturnsEmptyList(SutProvider<SecretsController> sutProvider, Guid id, Guid organizationId, Guid userId, AccessClientType accessType)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n\n        var result = await sutProvider.Sut.ListByOrganizationAsync(id);\n\n        await sutProvider.GetDependency<ISecretRepository>().Received(1)\n                     .GetManyDetailsByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), userId, accessType);\n\n        Assert.Empty(result.Secrets);\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetSecretsByOrganization_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, Secret resultSecret, Guid organizationId, Guid userId, Project mockProject, AccessClientType accessType)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<ISecretRepository>().GetManyDetailsByOrganizationIdAsync(default, default, default)\n            .ReturnsForAnyArgs(new List<SecretPermissionDetails>\n            {\n                new() { Secret = resultSecret, Read = true, Write = true },\n            });\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n\n        if (permissionType == PermissionType.RunAsAdmin)\n        {\n            sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);\n        }\n        else\n        {\n            resultSecret.Projects = new List<Project>() { mockProject };\n            sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);\n            sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(default, default, default)\n                .Returns((true, true));\n        }\n\n\n        await sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId);\n\n        await sutProvider.GetDependency<ISecretRepository>().Received(1)\n            .GetManyDetailsByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.OrganizationId)), userId, accessType);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretsByOrganization_AccessDenied_Throws(SutProvider<SecretsController> sutProvider, Secret resultSecret)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.ListByOrganizationAsync(resultSecret.OrganizationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecret_NotFound(SutProvider<SecretsController> sutProvider)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAsync(Guid.NewGuid()));\n    }\n\n    [Theory]\n    [BitAutoData(PermissionType.RunAsAdmin)]\n    [BitAutoData(PermissionType.RunAsUserWithPermission)]\n    public async Task GetSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, Secret resultSecret, Guid userId, Guid organizationId, Project mockProject)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        mockProject.OrganizationId = organizationId;\n        resultSecret.Projects = new List<Project>() { mockProject };\n        resultSecret.OrganizationId = organizationId;\n\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(default).ReturnsForAnyArgs(resultSecret);\n        sutProvider.GetDependency<ISecretRepository>().AccessToSecretAsync(default, default, default)\n            .ReturnsForAnyArgs(Task.FromResult((true, true)));\n\n        if (permissionType == PermissionType.RunAsAdmin)\n        {\n            resultSecret.OrganizationId = organizationId;\n            sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);\n            sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), AccessClientType.NoAccessCheck)\n                .Returns((true, true));\n        }\n        else\n        {\n            sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(false);\n            sutProvider.GetDependency<IProjectRepository>().AccessToProjectAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), AccessClientType.User)\n                .Returns((true, true));\n        }\n\n        await sutProvider.Sut.GetAsync(resultSecret.Id);\n\n        await sutProvider.GetDependency<ISecretRepository>().Received(1)\n                     .GetByIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultSecret.Id)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateSecret_NoAccess_Throws(SutProvider<SecretsController> sutProvider,\n        SecretCreateRequestModel data, Guid organizationId)\n    {\n        data = SetupSecretCreateRequest(sutProvider, data, organizationId);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToSecret(organizationId),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, data));\n        await sutProvider.GetDependency<ICreateSecretCommand>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateSecret_NoAccessPolicyUpdates_Success(SutProvider<SecretsController> sutProvider,\n        SecretCreateRequestModel data, Guid organizationId)\n    {\n        data = SetupSecretCreateRequest(sutProvider, data, organizationId);\n        SetControllerUser(sutProvider, new Guid());\n\n        await sutProvider.Sut.CreateAsync(organizationId, data);\n\n        await sutProvider.GetDependency<ICreateSecretCommand>().Received(1)\n            .CreateAsync(Arg.Any<Secret>(), null);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateSecret_AccessPolicyUpdates_NoAccess_Throws(SutProvider<SecretsController> sutProvider,\n        SecretCreateRequestModel data, Guid organizationId)\n    {\n        data = SetupSecretCreateRequest(sutProvider, data, organizationId, true);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<SecretAccessPoliciesUpdates>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, data));\n        await sutProvider.GetDependency<ICreateSecretCommand>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateSecret_AccessPolicyUpdate_Success(SutProvider<SecretsController> sutProvider,\n        SecretCreateRequestModel data, Guid organizationId)\n    {\n        data = SetupSecretCreateRequest(sutProvider, data, organizationId, true);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<SecretAccessPoliciesUpdates>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());\n\n        SetControllerUser(sutProvider, new Guid());\n\n        await sutProvider.Sut.CreateAsync(organizationId, data);\n\n        await sutProvider.GetDependency<ICreateSecretCommand>().Received(1)\n            .CreateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSecret_NoAccess_Throws(SutProvider<SecretsController> sutProvider,\n        SecretUpdateRequestModel data, Secret currentSecret)\n    {\n        data = SetupSecretUpdateRequest(data);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Secret>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(currentSecret.Id).ReturnsForAnyArgs(currentSecret);\n\n        sutProvider.GetDependency<IUpdateSecretCommand>()\n            .UpdateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>())\n            .ReturnsForAnyArgs(data.ToSecret(currentSecret));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateSecretAsync(currentSecret.Id, data));\n        await sutProvider.GetDependency<IUpdateSecretCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSecret_SecretDoesNotExist_Throws(SutProvider<SecretsController> sutProvider,\n        SecretUpdateRequestModel data, Secret currentSecret)\n    {\n        data = SetupSecretUpdateRequest(data);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Secret>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<IUpdateSecretCommand>()\n            .UpdateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>())\n            .ReturnsForAnyArgs(data.ToSecret(currentSecret));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateSecretAsync(currentSecret.Id, data));\n        await sutProvider.GetDependency<IUpdateSecretCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSecret_NoAccessPolicyUpdates_Success(SutProvider<SecretsController> sutProvider,\n        SecretUpdateRequestModel data, Secret currentSecret)\n    {\n        data = SetupSecretUpdateRequest(data);\n        SetControllerUser(sutProvider, new Guid());\n        sutProvider.GetDependency<ICurrentContext>().IdentityClientType.Returns(IdentityClientType.ServiceAccount);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Secret>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(currentSecret.Id).ReturnsForAnyArgs(currentSecret);\n\n        sutProvider.GetDependency<IUpdateSecretCommand>()\n            .UpdateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>())\n            .ReturnsForAnyArgs(data.ToSecret(currentSecret));\n\n        await sutProvider.Sut.UpdateSecretAsync(currentSecret.Id, data);\n        await sutProvider.GetDependency<IUpdateSecretCommand>().Received(1)\n            .UpdateAsync(Arg.Any<Secret>(), null);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSecret_AccessPolicyUpdate_NoAccess_Throws(SutProvider<SecretsController> sutProvider,\n        SecretUpdateRequestModel data, Secret currentSecret, SecretAccessPoliciesUpdates accessPoliciesUpdates)\n    {\n        data = SetupSecretUpdateAccessPoliciesRequest(sutProvider, data, currentSecret, accessPoliciesUpdates);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<SecretAccessPoliciesUpdates>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateSecretAsync(currentSecret.Id, data));\n        await sutProvider.GetDependency<IUpdateSecretCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSecret_AccessPolicyUpdate_Access_Success(SutProvider<SecretsController> sutProvider,\n        SecretUpdateRequestModel data, Secret currentSecret, SecretAccessPoliciesUpdates accessPoliciesUpdates)\n    {\n        data = SetupSecretUpdateAccessPoliciesRequest(sutProvider, data, currentSecret, accessPoliciesUpdates);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<SecretAccessPoliciesUpdates>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());\n\n        await sutProvider.Sut.UpdateSecretAsync(currentSecret.Id, data);\n        await sutProvider.GetDependency<IUpdateSecretCommand>().Received(1)\n            .UpdateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_NoSecretsFound_ThrowsNotFound(SutProvider<SecretsController> sutProvider, List<Secret> data)\n    {\n        var ids = data.Select(s => s.Id).ToList();\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List<Secret>());\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n        await sutProvider.GetDependency<IDeleteSecretCommand>().DidNotReceiveWithAnyArgs().DeleteSecrets(Arg.Any<List<Secret>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_SecretsFoundMisMatch_ThrowsNotFound(SutProvider<SecretsController> sutProvider, List<Secret> data, Secret mockSecret)\n    {\n        data.Add(mockSecret);\n        var ids = data.Select(s => s.Id).ToList();\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List<Secret> { mockSecret });\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n        await sutProvider.GetDependency<IDeleteSecretCommand>().DidNotReceiveWithAnyArgs().DeleteSecrets(Arg.Any<List<Secret>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_OrganizationMistMatch_ThrowsNotFound(SutProvider<SecretsController> sutProvider, List<Secret> data)\n    {\n        var ids = data.Select(s => s.Id).ToList();\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n        await sutProvider.GetDependency<IDeleteSecretCommand>().DidNotReceiveWithAnyArgs().DeleteSecrets(Arg.Any<List<Secret>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_NoAccessToSecretsManager_ThrowsNotFound(SutProvider<SecretsController> sutProvider, List<Secret> data)\n    {\n        var ids = data.Select(s => s.Id).ToList();\n        var organizationId = data.First().OrganizationId;\n        foreach (var s in data)\n        {\n            s.OrganizationId = organizationId;\n        }\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(false);\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n        await sutProvider.GetDependency<IDeleteSecretCommand>().DidNotReceiveWithAnyArgs().DeleteSecrets(Arg.Any<List<Secret>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_ReturnsAccessDeniedForSecretsWithoutAccess_Success(SutProvider<SecretsController> sutProvider, List<Secret> data)\n    {\n        var ids = data.Select(s => s.Id).ToList();\n        var organizationId = data.First().OrganizationId;\n        SetControllerUser(sutProvider, new Guid());\n\n        foreach (var secret in data)\n        {\n            secret.OrganizationId = organizationId;\n            sutProvider.GetDependency<IAuthorizationService>()\n                .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), secret,\n                    Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        }\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n\n        var results = await sutProvider.Sut.BulkDeleteAsync(ids);\n\n        Assert.Equal(data.Count, results.Data.Count());\n        Assert.Equal(\"access denied\", results.Data.First().Error);\n\n        data.Remove(data.First());\n        await sutProvider.GetDependency<IDeleteSecretCommand>().Received(1)\n            .DeleteSecrets(Arg.Is(AssertHelper.AssertPropertyEqual(data)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_Success(SutProvider<SecretsController> sutProvider, List<Secret> data)\n    {\n        var ids = data.Select(sa => sa.Id).ToList();\n        var organizationId = data.First().OrganizationId;\n        foreach (var secret in data)\n        {\n            secret.OrganizationId = organizationId;\n            sutProvider.GetDependency<IAuthorizationService>()\n                .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), secret,\n                    Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        }\n\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        SetControllerUser(sutProvider, new Guid());\n        var results = await sutProvider.Sut.BulkDeleteAsync(ids);\n\n        await sutProvider.GetDependency<IDeleteSecretCommand>().Received(1)\n            .DeleteSecrets(Arg.Is(AssertHelper.AssertPropertyEqual(data)));\n        Assert.Equal(data.Count, results.Data.Count());\n        foreach (var result in results.Data)\n        {\n            Assert.Null(result.Error);\n        }\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretsByIds_NoSecretsFound_ThrowsNotFound(SutProvider<SecretsController> sutProvider,\n        List<Secret> data)\n    {\n        var (ids, request) = BuildGetSecretsRequestModel(data);\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List<Secret>());\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretsByIds_SecretsFoundMisMatch_ThrowsNotFound(SutProvider<SecretsController> sutProvider,\n        List<Secret> data, Secret mockSecret)\n    {\n        var (ids, request) = BuildGetSecretsRequestModel(data);\n        ids.Add(mockSecret.Id);\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids))\n            .ReturnsForAnyArgs(new List<Secret> { mockSecret });\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretsByIds_AccessDenied_ThrowsNotFound(SutProvider<SecretsController> sutProvider,\n        List<Secret> data)\n    {\n        var (ids, request) = BuildGetSecretsRequestModel(data);\n        var organizationId = SetOrganizations(ref data);\n\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetSecretsByIdsAsync(request));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretsByIds_Success(SutProvider<SecretsController> sutProvider, List<Secret> data)\n    {\n        var (ids, request) = BuildGetSecretsRequestModel(data);\n        var organizationId = SetOrganizations(ref data);\n        SetControllerUser(sutProvider, new Guid());\n        sutProvider.GetDependency<ISecretRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n\n        var results = await sutProvider.Sut.GetSecretsByIdsAsync(request);\n        Assert.Equal(data.Count, results.Data.Count());\n    }\n\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task GetSecretsSyncAsync_AccessSecretsManagerFalse_ThrowsNotFound(\n        bool nullLastSyncedDate,\n        SutProvider<SecretsController> sutProvider, Guid organizationId)\n    {\n        var lastSyncedDate = GetLastSyncedDate(nullLastSyncedDate);\n\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))\n            .ReturnsForAnyArgs(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate));\n    }\n\n    [Theory]\n    [BitAutoData(true, AccessClientType.NoAccessCheck)]\n    [BitAutoData(true, AccessClientType.User)]\n    [BitAutoData(true, AccessClientType.Organization)]\n    [BitAutoData(false, AccessClientType.NoAccessCheck)]\n    [BitAutoData(false, AccessClientType.User)]\n    [BitAutoData(false, AccessClientType.Organization)]\n    public async Task GetSecretsSyncAsync_AccessClientIsNotAServiceAccount_ThrowsBadRequest(\n        bool nullLastSyncedDate,\n        AccessClientType accessClientType,\n        SutProvider<SecretsController> sutProvider, Guid organizationId)\n    {\n        var lastSyncedDate = GetLastSyncedDate(nullLastSyncedDate);\n\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))\n            .ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Guid>())\n            .Returns((accessClientType, new Guid()));\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSecretsSyncAsync_LastSyncedInFuture_ThrowsBadRequest(\n        List<Secret> secrets,\n        SutProvider<SecretsController> sutProvider, Guid organizationId)\n    {\n        DateTime? lastSyncedDate = DateTime.UtcNow.AddDays(3);\n\n        SetupSecretsSyncRequest(false, secrets, sutProvider, organizationId);\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate));\n    }\n\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task GetSecretsSyncAsync_AccessClientIsAServiceAccount_Success(\n        bool nullLastSyncedDate,\n        List<Secret> secrets,\n        SutProvider<SecretsController> sutProvider, Guid organizationId)\n    {\n        var lastSyncedDate = SetupSecretsSyncRequest(nullLastSyncedDate, secrets, sutProvider, organizationId);\n        SetControllerUser(sutProvider, new Guid());\n        var result = await sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate);\n        Assert.True(result.HasChanges);\n        Assert.NotNull(result.Secrets);\n        Assert.NotEmpty(result.Secrets.Data);\n    }\n\n    private static (List<Guid> Ids, GetSecretsRequestModel request) BuildGetSecretsRequestModel(\n        IEnumerable<Secret> data)\n    {\n        var ids = data.Select(s => s.Id).ToList();\n        var request = new GetSecretsRequestModel { Ids = ids };\n        return (ids, request);\n    }\n\n    private static Guid SetOrganizations(ref List<Secret> data)\n    {\n        var organizationId = data.First().OrganizationId;\n        foreach (var s in data)\n        {\n            s.OrganizationId = organizationId;\n        }\n\n        return organizationId;\n    }\n\n    private static DateTime? SetupSecretsSyncRequest(bool nullLastSyncedDate, List<Secret> secrets,\n        SutProvider<SecretsController> sutProvider, Guid organizationId)\n    {\n        var lastSyncedDate = GetLastSyncedDate(nullLastSyncedDate);\n\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId))\n            .ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IAccessClientQuery>()\n            .GetAccessClientAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Guid>())\n            .Returns((AccessClientType.ServiceAccount, new Guid()));\n        sutProvider.GetDependency<ISecretsSyncQuery>().GetAsync(Arg.Any<SecretsSyncRequest>())\n            .Returns((true, secrets));\n        return lastSyncedDate;\n    }\n\n    private static DateTime? GetLastSyncedDate(bool nullLastSyncedDate)\n    {\n        return nullLastSyncedDate ? null : DateTime.UtcNow.AddDays(-1);\n    }\n\n    private static SecretCreateRequestModel SetupSecretCreateRequest(SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId, bool accessPolicyRequest = false)\n    {\n        // We currently only allow a secret to be in one project at a time\n        if (data.ProjectIds != null && data.ProjectIds.Length > 1)\n        {\n            data.ProjectIds = [data.ProjectIds.ElementAt(0)];\n        }\n\n        if (!accessPolicyRequest)\n        {\n            data.AccessPoliciesRequests = null;\n        }\n\n        sutProvider.GetDependency<ICreateSecretCommand>()\n            .CreateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>())\n            .ReturnsForAnyArgs(data.ToSecret(organizationId));\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Secret>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());\n\n        return data;\n    }\n\n    private static SecretUpdateRequestModel SetupSecretUpdateRequest(SecretUpdateRequestModel data, bool accessPolicyRequest = false)\n    {\n        // We currently only allow a secret to be in one project at a time\n        if (data.ProjectIds != null && data.ProjectIds.Length > 1)\n        {\n            data.ProjectIds = [data.ProjectIds.ElementAt(0)];\n        }\n\n        if (!accessPolicyRequest)\n        {\n            data.AccessPoliciesRequests = null;\n        }\n\n        return data;\n    }\n\n    private static SecretUpdateRequestModel SetupSecretUpdateAccessPoliciesRequest(SutProvider<SecretsController> sutProvider, SecretUpdateRequestModel data, Secret currentSecret, SecretAccessPoliciesUpdates accessPoliciesUpdates)\n    {\n        data = SetupSecretUpdateRequest(data, true);\n\n        sutProvider.GetDependency<ICurrentContext>().IdentityClientType.Returns(IdentityClientType.ServiceAccount);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), Arg.Any<Secret>(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Success());\n        sutProvider.GetDependency<ISecretRepository>().GetByIdAsync(currentSecret.Id).ReturnsForAnyArgs(currentSecret);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(Guid.NewGuid());\n        sutProvider.GetDependency<ISecretAccessPoliciesUpdatesQuery>()\n            .GetAsync(Arg.Any<SecretAccessPolicies>(), Arg.Any<Guid>())\n            .ReturnsForAnyArgs(accessPoliciesUpdates);\n        sutProvider.GetDependency<IUpdateSecretCommand>()\n            .UpdateAsync(Arg.Any<Secret>(), Arg.Any<SecretAccessPoliciesUpdates>())\n            .ReturnsForAnyArgs(data.ToSecret(currentSecret));\n        return data;\n    }\n\n    private static void SetControllerUser(SutProvider<SecretsController> sutProvider, Guid userId)\n    {\n        var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, userId.ToString()) };\n        var identity = new ClaimsIdentity(claims, \"Test\");\n        var principal = new ClaimsPrincipal(identity);\n\n        sutProvider.Sut.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext\n        {\n            HttpContext = new DefaultHttpContext { User = principal }\n        };\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(principal).Returns(userId);\n    }\n\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Controllers/SecretsManagerEventsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.SecretsManager.Controllers;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.SecretsManager.Controllers;\n\n[ControllerCustomize(typeof(SecretsManagerEventsController))]\n[SutProviderCustomize]\n[JsonDocumentCustomize]\npublic class SecretsManagerEventsControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountEvents_NoAccess_Throws(SutProvider<SecretsManagerEventsController> sutProvider,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetServiceAccountEventsAsync(data.Id));\n        await sutProvider.GetDependency<IEventRepository>().DidNotReceiveWithAnyArgs()\n            .GetManyByOrganizationServiceAccountAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<DateTime>(),\n                Arg.Any<DateTime>(), Arg.Any<PageOptions>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountEvents_DateRangeOver_Throws(\n        SutProvider<SecretsManagerEventsController> sutProvider,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n\n        var start = DateTime.UtcNow.AddYears(-1);\n        var end = DateTime.UtcNow.AddYears(1);\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.GetServiceAccountEventsAsync(data.Id, start, end));\n\n        await sutProvider.GetDependency<IEventRepository>().DidNotReceiveWithAnyArgs()\n            .GetManyByOrganizationServiceAccountAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<DateTime>(),\n                Arg.Any<DateTime>(), Arg.Any<PageOptions>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountEvents_Success(SutProvider<SecretsManagerEventsController> sutProvider,\n        ServiceAccount data)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        sutProvider.GetDependency<IEventRepository>()\n            .GetManyByOrganizationServiceAccountAsync(default, default, default, default, default)\n            .ReturnsForAnyArgs(new PagedResult<IEvent>());\n\n        await sutProvider.Sut.GetServiceAccountEventsAsync(data.Id);\n\n        await sutProvider.GetDependency<IEventRepository>().Received(1)\n            .GetManyByOrganizationServiceAccountAsync(data.OrganizationId, data.Id, Arg.Any<DateTime>(),\n                Arg.Any<DateTime>(), Arg.Any<PageOptions>());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.SecretsManager.Controllers;\nusing Bit.Api.SecretsManager.Models.Request;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;\nusing Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.SecretsManager.Controllers;\n\n[ControllerCustomize(typeof(ServiceAccountsController))]\n[SutProviderCustomize]\n[JsonDocumentCustomize]\npublic class ServiceAccountsControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountsByOrganization_ReturnsEmptyList(\n        SutProvider<ServiceAccountsController> sutProvider, Guid id)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(id).Returns(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n        var result = await sutProvider.Sut.ListByOrganizationAsync(id);\n\n        await sutProvider.GetDependency<IServiceAccountSecretsDetailsQuery>().Received(1)\n            .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)),\n                Arg.Any<Guid>(), Arg.Any<AccessClientType>(), Arg.Any<bool>());\n\n        Assert.Empty(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountsByOrganization_Success(SutProvider<ServiceAccountsController> sutProvider,\n        ServiceAccountSecretsDetails resultServiceAccount)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n        sutProvider.GetDependency<IServiceAccountSecretsDetailsQuery>().GetManyByOrganizationIdAsync(default, default, default, default)\n            .ReturnsForAnyArgs(new List<ServiceAccountSecretsDetails> { resultServiceAccount });\n\n        var result = await sutProvider.Sut.ListByOrganizationAsync(resultServiceAccount.ServiceAccount.OrganizationId);\n\n        await sutProvider.GetDependency<IServiceAccountSecretsDetailsQuery>().Received(1)\n            .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultServiceAccount.ServiceAccount.OrganizationId)),\n                Arg.Any<Guid>(), Arg.Any<AccessClientType>(), Arg.Any<bool>());\n        Assert.NotEmpty(result.Data);\n        Assert.Single(result.Data);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetServiceAccountsByOrganization_AccessDenied_Throws(\n        SutProvider<ServiceAccountsController> sutProvider, Guid orgId)\n    {\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.ListByOrganizationAsync(orgId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateServiceAccount_NoAccess_Throws(SutProvider<ServiceAccountsController> sutProvider,\n        ServiceAccountCreateRequestModel data, Guid organizationId)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organizationId),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n        var resultServiceAccount = data.ToServiceAccount(organizationId);\n        sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default)\n            .ReturnsForAnyArgs(resultServiceAccount);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, data));\n        await sutProvider.GetDependency<ICreateServiceAccountCommand>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(0)]\n    public async Task CreateServiceAccount_WhenAutoscalingNotRequired_DoesNotCallUpdateSubscription(\n        int newSlotsRequired, SutProvider<ServiceAccountsController> sutProvider,\n        ServiceAccountCreateRequestModel data, Organization organization)\n    {\n        ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization);\n\n        await sutProvider.Sut.CreateAsync(organization.Id, data);\n\n        await sutProvider.GetDependency<ICreateServiceAccountCommand>().Received(1)\n            .CreateAsync(Arg.Is<ServiceAccount>(sa => sa.Name == data.Name), Arg.Any<Guid>());\n\n        await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateSubscriptionAsync(Arg.Any<SecretsManagerSubscriptionUpdate>());\n    }\n\n    [Theory]\n    [BitAutoData(1)]\n    [BitAutoData(2)]\n    public async Task CreateServiceAccount_WhenAutoscalingRequired_CallsUpdateSubscription(int newSlotsRequired,\n        SutProvider<ServiceAccountsController> sutProvider,\n        ServiceAccountCreateRequestModel data, Organization organization)\n    {\n        ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));\n\n        await sutProvider.Sut.CreateAsync(organization.Id, data);\n\n        await sutProvider.GetDependency<ICreateServiceAccountCommand>().Received(1)\n            .CreateAsync(Arg.Is<ServiceAccount>(sa => sa.Name == data.Name), Arg.Any<Guid>());\n\n        await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1)\n            .UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>\n                update.Autoscaling == true &&\n                update.SmServiceAccounts == organization.SmServiceAccounts + newSlotsRequired &&\n                !update.SmSeatsChanged &&\n                !update.MaxAutoscaleSmSeatsChanged &&\n                !update.MaxAutoscaleSmServiceAccountsChanged));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider,\n        ServiceAccountCreateRequestModel data, Guid organizationId, Organization mockOrg)\n    {\n        mockOrg.Id = organizationId;\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organizationId),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Is(organizationId)).Returns(mockOrg);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n        var resultServiceAccount = data.ToServiceAccount(organizationId);\n        sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default)\n            .ReturnsForAnyArgs(resultServiceAccount);\n\n        await sutProvider.Sut.CreateAsync(organizationId, data);\n        await sutProvider.GetDependency<ICreateServiceAccountCommand>().Received(1)\n            .CreateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateServiceAccount_NoAccess_Throws(SutProvider<ServiceAccountsController> sutProvider,\n        ServiceAccountUpdateRequestModel data, ServiceAccount existingServiceAccount)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(existingServiceAccount.Id),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id)\n            .ReturnsForAnyArgs(existingServiceAccount);\n        var resultServiceAccount = data.ToServiceAccount(existingServiceAccount.Id);\n        sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default)\n            .ReturnsForAnyArgs(resultServiceAccount);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(existingServiceAccount.Id, data));\n        await sutProvider.GetDependency<IUpdateServiceAccountCommand>().DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<ServiceAccount>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider,\n        ServiceAccountUpdateRequestModel data, ServiceAccount existingServiceAccount)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(existingServiceAccount.Id),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        var resultServiceAccount = data.ToServiceAccount(existingServiceAccount.Id);\n        sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default)\n            .ReturnsForAnyArgs(resultServiceAccount);\n\n        var result = await sutProvider.Sut.UpdateAsync(existingServiceAccount.Id, data);\n        await sutProvider.GetDependency<IUpdateServiceAccountCommand>().Received(1)\n            .UpdateAsync(Arg.Any<ServiceAccount>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAccessToken_NoAccess_Throws(SutProvider<ServiceAccountsController> sutProvider,\n        AccessTokenCreateRequestModel data, ServiceAccount serviceAccount, string mockClientSecret)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), serviceAccount,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n        var resultAccessToken = data.ToApiKey(serviceAccount.Id);\n\n        sutProvider.GetDependency<ICreateAccessTokenCommand>()\n            .CreateAsync(default)\n            .ReturnsForAnyArgs(new ApiKeyClientSecretDetails { ApiKey = resultAccessToken, ClientSecret = mockClientSecret });\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.CreateAccessTokenAsync(serviceAccount.Id, data));\n        await sutProvider.GetDependency<ICreateAccessTokenCommand>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(Arg.Any<ApiKey>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAccessToken_Success(SutProvider<ServiceAccountsController> sutProvider,\n        AccessTokenCreateRequestModel data, ServiceAccount serviceAccount, string mockClientSecret)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), serviceAccount,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        var resultAccessToken = data.ToApiKey(serviceAccount.Id);\n\n        sutProvider.GetDependency<ICreateAccessTokenCommand>().CreateAsync(default)\n            .ReturnsForAnyArgs(new ApiKeyClientSecretDetails { ApiKey = resultAccessToken, ClientSecret = mockClientSecret });\n\n        await sutProvider.Sut.CreateAccessTokenAsync(serviceAccount.Id, data);\n        await sutProvider.GetDependency<ICreateAccessTokenCommand>().Received(1)\n            .CreateAsync(Arg.Any<ApiKey>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAccessTokens_NoAccess_Throws(SutProvider<ServiceAccountsController> sutProvider,\n        ServiceAccount data, ICollection<ApiKey> resultApiKeys)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n\n        foreach (var apiKey in resultApiKeys)\n        {\n            apiKey.Scope = \"[\\\"api.secrets\\\"]\";\n        }\n\n        sutProvider.GetDependency<IApiKeyRepository>().GetManyByServiceAccountIdAsync(default)\n            .ReturnsForAnyArgs(resultApiKeys);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAccessTokens(data.Id));\n        await sutProvider.GetDependency<IApiKeyRepository>().DidNotReceiveWithAnyArgs()\n            .GetManyByServiceAccountIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAccessTokens_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccount data,\n        ICollection<ApiKey> resultApiKeys)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n\n        foreach (var apiKey in resultApiKeys)\n        {\n            apiKey.Scope = \"[\\\"api.secrets\\\"]\";\n        }\n\n        sutProvider.GetDependency<IApiKeyRepository>().GetManyByServiceAccountIdAsync(default)\n            .ReturnsForAnyArgs(resultApiKeys);\n\n        var result = await sutProvider.Sut.GetAccessTokens(data.Id);\n        await sutProvider.GetDependency<IApiKeyRepository>().Received(1)\n            .GetManyByServiceAccountIdAsync(Arg.Any<Guid>());\n        Assert.NotEmpty(result.Data);\n        Assert.Equal(resultApiKeys.Count, result.Data.Count());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeAccessTokens_NoAccess_Throws(SutProvider<ServiceAccountsController> sutProvider,\n        RevokeAccessTokensRequest data, ServiceAccount serviceAccount)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), serviceAccount,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.RevokeAccessTokensAsync(serviceAccount.Id, data));\n        await sutProvider.GetDependency<IRevokeAccessTokensCommand>().DidNotReceiveWithAnyArgs()\n            .RevokeAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid[]>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeAccessTokens_Success(SutProvider<ServiceAccountsController> sutProvider,\n        RevokeAccessTokensRequest data, ServiceAccount serviceAccount)\n    {\n        sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), serviceAccount,\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n\n        await sutProvider.Sut.RevokeAccessTokensAsync(serviceAccount.Id, data);\n        await sutProvider.GetDependency<IRevokeAccessTokensCommand>().Received(1)\n            .RevokeAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid[]>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_NoServiceAccountsFound_ThrowsNotFound(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data)\n    {\n        var ids = data.Select(sa => sa.Id).ToList();\n        sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List<ServiceAccount>());\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n        await sutProvider.GetDependency<IDeleteServiceAccountsCommand>().DidNotReceiveWithAnyArgs().DeleteServiceAccounts(Arg.Any<List<ServiceAccount>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_ServiceAccountsFoundMisMatch_ThrowsNotFound(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data, ServiceAccount mockSa)\n    {\n        data.Add(mockSa);\n        var ids = data.Select(sa => sa.Id).ToList();\n        sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List<ServiceAccount> { mockSa });\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n        await sutProvider.GetDependency<IDeleteServiceAccountsCommand>().DidNotReceiveWithAnyArgs().DeleteServiceAccounts(Arg.Any<List<ServiceAccount>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_OrganizationMistMatch_ThrowsNotFound(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data)\n    {\n        var ids = data.Select(sa => sa.Id).ToList();\n        sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n        await sutProvider.GetDependency<IDeleteServiceAccountsCommand>().DidNotReceiveWithAnyArgs().DeleteServiceAccounts(Arg.Any<List<ServiceAccount>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_NoAccessToSecretsManager_ThrowsNotFound(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data)\n    {\n        var ids = data.Select(sa => sa.Id).ToList();\n        var organizationId = data.First().OrganizationId;\n        foreach (var sa in data)\n        {\n            sa.OrganizationId = organizationId;\n        }\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(false);\n        sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.BulkDeleteAsync(ids));\n        await sutProvider.GetDependency<IDeleteServiceAccountsCommand>().DidNotReceiveWithAnyArgs().DeleteServiceAccounts(Arg.Any<List<ServiceAccount>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_ReturnsAccessDeniedForProjectsWithoutAccess_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data, Guid userId)\n    {\n        var ids = data.Select(sa => sa.Id).ToList();\n        var organizationId = data.First().OrganizationId;\n        foreach (var sa in data)\n        {\n            sa.OrganizationId = organizationId;\n            sutProvider.GetDependency<IAuthorizationService>()\n                .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), sa,\n                    Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        }\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n\n        var results = await sutProvider.Sut.BulkDeleteAsync(ids);\n\n        Assert.Equal(data.Count, results.Data.Count());\n        Assert.Equal(\"access denied\", results.Data.First().Error);\n\n        data.Remove(data.First());\n        await sutProvider.GetDependency<IDeleteServiceAccountsCommand>().Received(1)\n            .DeleteServiceAccounts(Arg.Is(AssertHelper.AssertPropertyEqual(data)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkDelete_Success(SutProvider<ServiceAccountsController> sutProvider, List<ServiceAccount> data, Guid userId)\n    {\n        var ids = data.Select(sa => sa.Id).ToList();\n        var organizationId = data.First().OrganizationId;\n        foreach (var sa in data)\n        {\n            sa.OrganizationId = organizationId;\n            sutProvider.GetDependency<IAuthorizationService>()\n                .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), sa,\n                    Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        }\n\n        sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<IServiceAccountRepository>().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n\n        var results = await sutProvider.Sut.BulkDeleteAsync(ids);\n\n        await sutProvider.GetDependency<IDeleteServiceAccountsCommand>().Received(1)\n            .DeleteServiceAccounts(Arg.Is(AssertHelper.AssertPropertyEqual(data)));\n        Assert.Equal(data.Count, results.Data.Count());\n        foreach (var result in results.Data)\n        {\n            Assert.Null(result.Error);\n        }\n    }\n\n    private static void ArrangeCreateServiceAccountAutoScalingTest(int newSlotsRequired, SutProvider<ServiceAccountsController> sutProvider,\n        ServiceAccountCreateRequestModel data, Organization organization)\n    {\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organization.Id),\n                Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Is(organization.Id)).Returns(organization);\n        sutProvider.GetDependency<ICountNewServiceAccountSlotsRequiredQuery>()\n            .CountNewServiceAccountSlotsRequiredAsync(organization.Id, 1)\n            .ReturnsForAnyArgs(newSlotsRequired);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());\n        var resultServiceAccount = data.ToServiceAccount(organization.Id);\n        sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default)\n            .ReturnsForAnyArgs(resultServiceAccount);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Enums/PermissionType.cs",
    "content": "﻿namespace Bit.Api.Test.SecretsManager.Enums;\n\npublic enum PermissionType\n{\n    RunAsAdmin,\n    RunAsUserWithPermission,\n}\n"
  },
  {
    "path": "test/Api.Test/SecretsManager/Utilities/AccessPolicyHelpersTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Api.SecretsManager.Utilities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.SecretsManager.Utilities;\n\n[ProjectCustomize]\n[SecretCustomize]\npublic class AccessPolicyHelpersTests\n{\n    [Theory]\n    [BitAutoData]\n    public void CheckForDistinctAccessPolicies_DuplicateAccessPolicies_ThrowsBadRequestException(\n        UserProjectAccessPolicy userProjectAccessPolicy, UserServiceAccountAccessPolicy userServiceAccountAccessPolicy,\n        GroupProjectAccessPolicy groupProjectAccessPolicy,\n        GroupServiceAccountAccessPolicy groupServiceAccountAccessPolicy,\n        ServiceAccountProjectAccessPolicy serviceAccountProjectAccessPolicy)\n    {\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            userProjectAccessPolicy,\n            userProjectAccessPolicy,\n            userServiceAccountAccessPolicy,\n            userServiceAccountAccessPolicy,\n            groupProjectAccessPolicy,\n            groupProjectAccessPolicy,\n            groupServiceAccountAccessPolicy,\n            groupServiceAccountAccessPolicy,\n            serviceAccountProjectAccessPolicy,\n            serviceAccountProjectAccessPolicy\n        };\n\n        Assert.Throws<BadRequestException>(() =>\n        {\n            AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies);\n        });\n    }\n\n    [Fact]\n    public void CheckForDistinctAccessPolicies_UnsupportedAccessPolicy_ThrowsArgumentException()\n    {\n        var accessPolicies = new List<BaseAccessPolicy> { new UnsupportedAccessPolicy() };\n\n        Assert.Throws<ArgumentException>(() => { AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies); });\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public void CheckForDistinctAccessPolicies_DistinctPolicies_Success(UserProjectAccessPolicy userProjectAccessPolicy,\n        UserServiceAccountAccessPolicy userServiceAccountAccessPolicy,\n        GroupProjectAccessPolicy groupProjectAccessPolicy,\n        GroupServiceAccountAccessPolicy groupServiceAccountAccessPolicy,\n        ServiceAccountProjectAccessPolicy serviceAccountProjectAccessPolicy)\n    {\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            userProjectAccessPolicy,\n            userServiceAccountAccessPolicy,\n            groupProjectAccessPolicy,\n            groupServiceAccountAccessPolicy,\n            serviceAccountProjectAccessPolicy\n        };\n\n        AccessPolicyHelpers.CheckForDistinctAccessPolicies(accessPolicies);\n    }\n\n    [Fact]\n    public void CheckAccessPoliciesHaveReadPermission_ReadPermissionFalse_ThrowsBadRequestException()\n    {\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new UserProjectAccessPolicy { Read = false, Write = true },\n            new GroupProjectAccessPolicy { Read = true, Write = false }\n        };\n\n        Assert.Throws<BadRequestException>(() =>\n        {\n            AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(accessPolicies);\n        });\n    }\n\n    [Fact]\n    public void CheckAccessPoliciesHaveReadPermission_AllReadIsTrue_Success()\n    {\n        var accessPolicies = new List<BaseAccessPolicy>\n        {\n            new UserProjectAccessPolicy { Read = true, Write = true },\n            new GroupProjectAccessPolicy { Read = true, Write = false }\n        };\n\n        AccessPolicyHelpers.CheckAccessPoliciesHaveReadPermission(accessPolicies);\n    }\n\n    private class UnsupportedAccessPolicy : BaseAccessPolicy;\n}\n"
  },
  {
    "path": "test/Api.Test/Tools/Authorization/VaultExportAuthorizationHandlerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.Tools.Authorization;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Test.AdminConsole.Helpers;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Tools.Authorization;\n\n[SutProviderCustomize]\npublic class VaultExportAuthorizationHandlerTests\n{\n    public static IEnumerable<object[]> CanExportWholeVault => new List<CurrentContextOrganization>\n    {\n        new () { Type = OrganizationUserType.Owner },\n        new () { Type = OrganizationUserType.Admin },\n        new ()\n        {\n            Type = OrganizationUserType.Custom, Permissions = new Permissions { AccessImportExport = true }\n        }\n    }.Select(org => new[] { org });\n\n    [Theory]\n    [BitMemberAutoData(nameof(CanExportWholeVault))]\n    public async Task ExportAll_PermittedRoles_Success(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user,\n        SutProvider<VaultExportAuthorizationHandler> sutProvider)\n    {\n        org.Id = orgScope;\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgScope).Returns(org);\n\n        var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportWholeVault }, user, orgScope);\n        await sutProvider.Sut.HandleAsync(authContext);\n\n        Assert.True(authContext.HasSucceeded);\n    }\n\n    public static IEnumerable<object[]> CannotExportWholeVault => new List<CurrentContextOrganization>\n    {\n        new () { Type = OrganizationUserType.User },\n        new ()\n        {\n            Type = OrganizationUserType.Custom, Permissions = new Permissions { AccessImportExport = true }.Invert()\n        }\n    }.Select(org => new[] { org });\n\n    [Theory]\n    [BitMemberAutoData(nameof(CannotExportWholeVault))]\n    public async Task ExportAll_NotPermitted_Failure(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user,\n        SutProvider<VaultExportAuthorizationHandler> sutProvider)\n    {\n        org.Id = orgScope;\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgScope).Returns(org);\n\n        var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportWholeVault }, user, orgScope);\n        await sutProvider.Sut.HandleAsync(authContext);\n\n        Assert.False(authContext.HasSucceeded);\n    }\n\n    public static IEnumerable<object[]> CanExportManagedCollections =>\n        PermissionsHelpers.AllRoles().Select(o => new[] { o });\n\n    [Theory]\n    [BitMemberAutoData(nameof(CanExportManagedCollections))]\n    public async Task ExportManagedCollections_PermittedRoles_Success(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user,\n        SutProvider<VaultExportAuthorizationHandler> sutProvider)\n    {\n        org.Id = orgScope;\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgScope).Returns(org);\n\n        var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportManagedCollections }, user, orgScope);\n        await sutProvider.Sut.HandleAsync(authContext);\n\n        Assert.True(authContext.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData([null])]\n    public async Task ExportManagedCollections_NotPermitted_Failure(CurrentContextOrganization org, OrganizationScope orgScope, ClaimsPrincipal user,\n        SutProvider<VaultExportAuthorizationHandler> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgScope).Returns(org);\n\n        var authContext = new AuthorizationHandlerContext(new[] { VaultExportOperations.ExportManagedCollections }, user, orgScope);\n        await sutProvider.Sut.HandleAsync(authContext);\n\n        Assert.False(authContext.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing AutoFixture;\nusing Bit.Api.Models.Request;\nusing Bit.Api.Tools.Controllers;\nusing Bit.Api.Tools.Models.Request.Accounts;\nusing Bit.Api.Tools.Models.Request.Organizations;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Tools.ImportFeatures.Interfaces;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing NSubstitute.ClearExtensions;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\nusing ImportCiphersLimitationSettings = Bit.Core.Settings.GlobalSettings.ImportCiphersLimitationSettings;\n\nnamespace Bit.Api.Test.Tools.Controllers;\n\n[ControllerCustomize(typeof(ImportCiphersController))]\n[SutProviderCustomize]\npublic class ImportCiphersControllerTests\n{\n    private readonly ImportCiphersLimitationSettings _organizationCiphersLimitations = new()\n    {\n        CiphersLimit = 40000,\n        CollectionRelationshipsLimit = 80000,\n        CollectionsLimit = 2000\n    };\n\n    /*************************\n     * PostImport - Individual\n     *************************/\n    [Theory, BitAutoData]\n    public async Task PostImportIndividual_ImportCiphersRequestModel_BadRequestException(SutProvider<ImportCiphersController> sutProvider, IFixture fixture)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        var ciphers = fixture.CreateMany<CipherRequestModel>(7001).ToArray();\n        var model = new ImportCiphersRequestModel\n        {\n            Ciphers = ciphers,\n            FolderRelationships = null,\n            Folders = null\n        };\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostImport(model));\n\n        // Assert\n        Assert.Equal(\"You cannot import this much data at once.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportIndividual_ImportCiphersRequestModel_Success(User user,\n        IFixture fixture, SutProvider<ImportCiphersController> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n\n        sutProvider.GetDependency<Bit.Core.Services.IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        var request = fixture.Build<ImportCiphersRequestModel>()\n            .With(x => x.Ciphers, fixture.Build<CipherRequestModel>()\n                .With(c => c.OrganizationId, Guid.NewGuid().ToString())\n                .With(c => c.FolderId, Guid.NewGuid().ToString())\n                .With(c => c.ArchivedDate, (DateTime?)null)\n                .CreateMany(1).ToArray())\n            .Create();\n\n        // Act\n        await sutProvider.Sut.PostImport(request);\n\n        // Assert\n        await sutProvider.GetDependency<IImportCiphersCommand>()\n            .Received()\n            .ImportIntoIndividualVaultAsync(\n            Arg.Any<List<Folder>>(),\n            Arg.Any<List<CipherDetails>>(),\n            Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),\n            user.Id\n            );\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportIndividual_WithArchivedDate_SavesArchivedDate(User user,\n        IFixture fixture, SutProvider<ImportCiphersController> sutProvider)\n    {\n        var archivedDate = DateTime.UtcNow;\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n\n        sutProvider.GetDependency<Core.Services.IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        var request = fixture.Build<ImportCiphersRequestModel>()\n            .With(x => x.Ciphers, fixture.Build<CipherRequestModel>()\n                .With(c => c.ArchivedDate, archivedDate)\n                .With(c => c.FolderId, (string)null)\n                .CreateMany(1).ToArray())\n            .Create();\n\n        await sutProvider.Sut.PostImport(request);\n\n        await sutProvider.GetDependency<IImportCiphersCommand>()\n            .Received()\n            .ImportIntoIndividualVaultAsync(\n                Arg.Any<List<Folder>>(),\n                Arg.Is<List<CipherDetails>>(ciphers => ciphers.First().ArchivedDate == archivedDate),\n                Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),\n                user.Id\n            );\n    }\n\n    /****************************\n     * PostImport - Organization\n     ****************************/\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_ImportOrganizationCiphersRequestModel_BadRequestException(\n        SutProvider<ImportCiphersController> sutProvider,\n        IFixture fixture)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        // Limits are set in appsettings.json, making values small for test to run faster.\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = new()\n            {\n                CiphersLimit = 4,\n                CollectionRelationshipsLimit = 8,\n                CollectionsLimit = 2\n            };\n\n        var userService = sutProvider.GetDependency<Bit.Core.Services.IUserService>();\n        userService.GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(null as Guid?);\n\n        var ciphers = fixture.CreateMany<CipherRequestModel>(5).ToArray();\n        var model = new ImportOrganizationCiphersRequestModel\n        {\n            Collections = null,\n            Ciphers = ciphers,\n            CollectionRelationships = null\n        };\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostImportOrganization(Arg.Any<string>(), model));\n\n        // Assert\n        Assert.Equal(\"You cannot import this much data at once.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_ImportOrganizationCiphersRequestModel_Succeeds(\n        SutProvider<ImportCiphersController> sutProvider,\n        IFixture fixture,\n        User user)\n    {\n        // Arrange\n        var orgId = \"AD89E6F8-4E84-4CFE-A978-256CC0DBF974\";\n        var orgIdGuid = Guid.Parse(orgId);\n        var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = _organizationCiphersLimitations;\n\n        sutProvider.GetDependency<Bit.Core.Services.IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        var request = fixture.Build<ImportOrganizationCiphersRequestModel>()\n            .With(x => x.Ciphers, fixture.Build<CipherRequestModel>()\n                .With(c => c.OrganizationId, Guid.NewGuid().ToString())\n                .With(c => c.FolderId, Guid.NewGuid().ToString())\n                .With(c => c.ArchivedDate, (DateTime?)null)\n                .CreateMany(1).ToArray())\n            .With(y => y.Collections, fixture.Build<CollectionWithIdRequestModel>()\n                .With(c => c.Id, orgIdGuid)\n                .CreateMany(1).ToArray())\n            .Create();\n\n        // AccessImportExport permission setup\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessImportExport(Arg.Any<Guid>())\n            .Returns(false);\n\n        // BulkCollectionOperations.ImportCiphers permission setup\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ImportCiphers)))\n            .Returns(AuthorizationResult.Success());\n\n        // BulkCollectionOperations.Create permission setup\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(orgIdGuid)\n            .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList());\n\n        // Act\n        await sutProvider.Sut.PostImportOrganization(orgId, request);\n\n        // Assert\n        await sutProvider.GetDependency<IImportCiphersCommand>()\n            .Received(1)\n            .ImportIntoOrganizationalVaultAsync(\n                Arg.Any<List<Collection>>(),\n                Arg.Any<List<CipherDetails>>(),\n                Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),\n                Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_WithAccessImportExport_Succeeds(\n    SutProvider<ImportCiphersController> sutProvider,\n    IFixture fixture,\n    User user)\n    {\n        // Arrange\n        var orgId = \"AD89E6F8-4E84-4CFE-A978-256CC0DBF974\";\n        var orgIdGuid = Guid.Parse(orgId);\n        var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = _organizationCiphersLimitations;\n\n        var importCiphersLimitation = new GlobalSettings.ImportCiphersLimitationSettings();\n        importCiphersLimitation.CiphersLimit = 40000;\n        importCiphersLimitation.CollectionRelationshipsLimit = 80000;\n        importCiphersLimitation.CollectionsLimit = 2000;\n\n        sutProvider.GetDependency<Bit.Core.Services.IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        var request = fixture.Build<ImportOrganizationCiphersRequestModel>()\n            .With(x => x.Ciphers, fixture.Build<CipherRequestModel>()\n                .With(c => c.OrganizationId, Guid.NewGuid().ToString())\n                .With(c => c.FolderId, Guid.NewGuid().ToString())\n                .With(c => c.ArchivedDate, (DateTime?)null)\n                .CreateMany(1).ToArray())\n            .With(y => y.Collections, fixture.Build<CollectionWithIdRequestModel>()\n                .With(c => c.Id, orgIdGuid)\n                .CreateMany(1).ToArray())\n            .Create();\n\n        // AccessImportExport permission setup\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessImportExport(Arg.Any<Guid>())\n            .Returns(false);\n\n        // BulkCollectionOperations.ImportCiphers permission setup\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.ImportCiphers)))\n            .Returns(AuthorizationResult.Success());\n\n        // BulkCollectionOperations.Create permission setup\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs => reqs.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(orgIdGuid)\n            .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList());\n\n        // Act\n        await sutProvider.Sut.PostImportOrganization(orgId, request);\n\n        // Assert\n        await sutProvider.GetDependency<IImportCiphersCommand>()\n            .Received(1)\n            .ImportIntoOrganizationalVaultAsync(\n                Arg.Any<List<Collection>>(),\n                Arg.Any<List<CipherDetails>>(),\n                Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),\n                Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_WithExistingCollectionsAndWithoutImportCiphersPermissions_ThrowsException(\n        SutProvider<ImportCiphersController> sutProvider,\n        IFixture fixture,\n        User user)\n    {\n        // Arrange\n        var orgId = \"AD89E6F8-4E84-4CFE-A978-256CC0DBF974\";\n        var orgIdGuid = Guid.Parse(orgId);\n        var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = _organizationCiphersLimitations;\n\n        SetupUserService(sutProvider, user);\n\n        var request = fixture.Build<ImportOrganizationCiphersRequestModel>()\n            .With(x => x.Ciphers, fixture.Build<CipherRequestModel>()\n                .With(c => c.OrganizationId, Guid.NewGuid().ToString())\n                .With(c => c.FolderId, Guid.NewGuid().ToString())\n                .With(c => c.ArchivedDate, (DateTime?)null)\n                .CreateMany(1).ToArray())\n            .With(y => y.Collections, fixture.Build<CollectionWithIdRequestModel>()\n                .With(c => c.Id, orgIdGuid)\n                .CreateMany(1).ToArray())\n            .Create();\n\n        // AccessImportExport permission setup\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessImportExport(Arg.Any<Guid>())\n            .Returns(false);\n\n        // BulkCollectionOperations.ImportCiphers permission setup\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.ImportCiphers)))\n            .Returns(AuthorizationResult.Failed());\n\n        // BulkCollectionOperations.Create permission setup\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(orgIdGuid)\n            .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList());\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.PostImportOrganization(orgId, request));\n\n        // Assert\n        Assert.IsType<Bit.Core.Exceptions.BadRequestException>(exception);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_WithoutCreatePermissions_ThrowsException(\n        SutProvider<ImportCiphersController> sutProvider,\n        IFixture fixture,\n        User user)\n    {\n        // Arrange\n        var orgId = \"AD89E6F8-4E84-4CFE-A978-256CC0DBF974\";\n        var orgIdGuid = Guid.Parse(orgId);\n        var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = _organizationCiphersLimitations;\n\n        sutProvider.GetDependency<Bit.Core.Services.IUserService>()\n            .GetProperUserId(Arg.Any<ClaimsPrincipal>())\n            .Returns(user.Id);\n\n        var request = fixture.Build<ImportOrganizationCiphersRequestModel>()\n            .With(x => x.Ciphers, fixture.Build<CipherRequestModel>()\n                .With(c => c.OrganizationId, Guid.NewGuid().ToString())\n                .With(c => c.FolderId, Guid.NewGuid().ToString())\n                .With(c => c.ArchivedDate, (DateTime?)null)\n                .CreateMany(1).ToArray())\n            .With(y => y.Collections, fixture.Build<CollectionWithIdRequestModel>()\n                .With(c => c.Id, orgIdGuid)\n                .CreateMany(1).ToArray())\n            .Create();\n\n        // AccessImportExport permission setup\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessImportExport(Arg.Any<Guid>())\n            .Returns(false);\n\n        // BulkCollectionOperations.ImportCiphers permission setup\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.ImportCiphers)))\n            .Returns(AuthorizationResult.Failed());\n\n        // BulkCollectionOperations.Create permission setup\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(orgIdGuid)\n            .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList());\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.PostImportOrganization(orgId, request));\n\n        // Assert\n        Assert.IsType<Bit.Core.Exceptions.BadRequestException>(exception);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_CanCreateChildCollectionsWithCreateAndImportPermissionsAsync(\n        SutProvider<ImportCiphersController> sutProvider,\n        IFixture fixture,\n        User user)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = _organizationCiphersLimitations;\n\n        SetupUserService(sutProvider, user);\n\n        // Create new collections\n        var newCollections = fixture.Build<CollectionWithIdRequestModel>()\n                .CreateMany(2).ToArray();\n\n        // define existing collections\n        var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();\n\n        // import model includes new and existing collection\n        var request = new ImportOrganizationCiphersRequestModel\n        {\n            Collections = newCollections.Concat(existingCollections).ToArray(),\n            Ciphers = fixture.Build<CipherRequestModel>()\n                .With(_ => _.OrganizationId, orgId.ToString())\n                .With(_ => _.FolderId, Guid.NewGuid().ToString())\n                .With(_ => _.ArchivedDate, (DateTime?)null)\n                .CreateMany(2).ToArray(),\n            CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),\n        };\n\n        // AccessImportExport permission - false\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessImportExport(Arg.Any<Guid>())\n            .Returns(false);\n\n        // BulkCollectionOperations.ImportCiphers permission - true\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.ImportCiphers)))\n            .Returns(AuthorizationResult.Success());\n\n        // BulkCollectionOperations.Create permission - true\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(orgId)\n            .Returns(existingCollections.Select(c =>\n                new Collection { OrganizationId = orgId, Id = c.Id.GetValueOrDefault() })\n                .ToList());\n\n        // Act\n        // User imports into collections and creates new collections\n        // User has ImportCiphers and Create ciphers permission\n        await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);\n\n        // Assert\n        await sutProvider.GetDependency<IImportCiphersCommand>()\n            .Received(1)\n            .ImportIntoOrganizationalVaultAsync(\n                Arg.Any<List<Collection>>(),\n                Arg.Any<List<CipherDetails>>(),\n                Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),\n                Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_CannotCreateChildCollectionsWithoutCreatePermissionsAsync(\n        SutProvider<ImportCiphersController> sutProvider,\n        IFixture fixture,\n        User user)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = _organizationCiphersLimitations;\n\n        SetupUserService(sutProvider, user);\n\n        // Create new collections\n        var newCollections = fixture.Build<CollectionWithIdRequestModel>()\n                .CreateMany(2).ToArray();\n\n        // define existing collections\n        var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(2).ToArray();\n\n        // import model includes new and existing collection\n        var request = new ImportOrganizationCiphersRequestModel\n        {\n            Collections = newCollections.Concat(existingCollections).ToArray(),\n            Ciphers = fixture.Build<CipherRequestModel>()\n                .With(_ => _.OrganizationId, orgId.ToString())\n                .With(_ => _.FolderId, Guid.NewGuid().ToString())\n                .With(_ => _.ArchivedDate, (DateTime?)null)\n                .CreateMany(2).ToArray(),\n            CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),\n        };\n\n        // AccessImportExport permission - false\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessImportExport(Arg.Any<Guid>())\n            .Returns(false);\n\n        // BulkCollectionOperations.ImportCiphers permission - true\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.ImportCiphers)))\n            .Returns(AuthorizationResult.Success());\n\n        // BulkCollectionOperations.Create permission - FALSE\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(orgId)\n            .Returns(existingCollections.Select(c =>\n                new Collection { OrganizationId = orgId, Id = c.Id.GetValueOrDefault() })\n                .ToList());\n\n        // Act\n        // User imports into an existing collection and creates new collections\n        // User has ImportCiphers permission only and doesn't have Create permission\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n        {\n            await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);\n        });\n\n        // Assert\n        Assert.IsType<BadRequestException>(exception);\n        await sutProvider.GetDependency<IImportCiphersCommand>()\n            .DidNotReceive()\n            .ImportIntoOrganizationalVaultAsync(\n                Arg.Any<List<Collection>>(),\n                Arg.Any<List<CipherDetails>>(),\n                Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),\n                Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_ImportIntoNewCollectionWithCreatePermissionsOnlyAsync(\n      SutProvider<ImportCiphersController> sutProvider,\n      IFixture fixture,\n      User user)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = _organizationCiphersLimitations;\n\n        SetupUserService(sutProvider, user);\n\n        // Create new collections\n        var newCollections = fixture.CreateMany<CollectionWithIdRequestModel>(1).ToArray();\n\n        // Define existing collections\n        var existingCollections = new List<CollectionWithIdRequestModel>();\n\n        // Import model includes new and existing collection\n        var request = new ImportOrganizationCiphersRequestModel\n        {\n            Collections = newCollections.Concat(existingCollections).ToArray(),\n            Ciphers = fixture.Build<CipherRequestModel>()\n                .With(_ => _.OrganizationId, orgId.ToString())\n                .With(_ => _.FolderId, Guid.NewGuid().ToString())\n                .With(_ => _.ArchivedDate, (DateTime?)null)\n                .CreateMany(2).ToArray(),\n            CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),\n        };\n\n        // AccessImportExport permission - false\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessImportExport(Arg.Any<Guid>())\n            .Returns(false);\n\n        // BulkCollectionOperations.ImportCiphers permission - FALSE\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.ImportCiphers)))\n            .Returns(AuthorizationResult.Failed());\n\n        // BulkCollectionOperations.Create permission - TRUE\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(orgId)\n            .Returns(new List<Collection>());\n\n        // Act\n        // User imports/creates a new collection - existing collections not affected\n        // User has create permissions and doesn't need import permissions\n        await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);\n\n        // Assert\n        await sutProvider.GetDependency<IImportCiphersCommand>()\n            .Received(1)\n            .ImportIntoOrganizationalVaultAsync(\n                Arg.Any<List<Collection>>(),\n                Arg.Any<List<CipherDetails>>(),\n                Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),\n                Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_ImportIntoExistingCollectionWithImportPermissionsOnlySuccessAsync(\n      SutProvider<ImportCiphersController> sutProvider,\n      IFixture fixture,\n      User user)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = _organizationCiphersLimitations;\n\n        SetupUserService(sutProvider, user);\n\n        // No new collections\n        var newCollections = new List<CollectionWithIdRequestModel>();\n\n        // Define existing collections\n        var existingCollections = fixture.CreateMany<CollectionWithIdRequestModel>(1).ToArray();\n\n        // Import model includes new and existing collection\n        var request = new ImportOrganizationCiphersRequestModel\n        {\n            Collections = newCollections.Concat(existingCollections).ToArray(),\n            Ciphers = fixture.Build<CipherRequestModel>()\n                .With(_ => _.OrganizationId, orgId.ToString())\n                .With(_ => _.FolderId, Guid.NewGuid().ToString())\n                .With(_ => _.ArchivedDate, (DateTime?)null)\n                .CreateMany(2).ToArray(),\n            CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),\n        };\n\n        // AccessImportExport permission - false\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessImportExport(Arg.Any<Guid>())\n            .Returns(false);\n\n        // BulkCollectionOperations.ImportCiphers permission - true\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.ImportCiphers)))\n            .Returns(AuthorizationResult.Success());\n\n        // BulkCollectionOperations.Create permission - FALSE\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(orgId)\n            .Returns(existingCollections.Select(c =>\n                new Collection { OrganizationId = orgId, Id = c.Id.GetValueOrDefault() })\n                .ToList());\n\n        // Act\n        // User import into existing collection\n        // User has ImportCiphers permission only and doesn't need create permission\n        await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);\n\n        // Assert\n        await sutProvider.GetDependency<IImportCiphersCommand>()\n            .Received(1)\n            .ImportIntoOrganizationalVaultAsync(\n                Arg.Any<List<Collection>>(),\n                Arg.Any<List<CipherDetails>>(),\n                Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),\n                Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostImportOrganization_ImportWithNoCollectionsWithCreatePermissionsOnlySuccessAsync(\n      SutProvider<ImportCiphersController> sutProvider,\n      IFixture fixture,\n      User user)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .SelfHosted = false;\n        sutProvider.GetDependency<GlobalSettings>()\n            .ImportCiphersLimitation = _organizationCiphersLimitations;\n\n        SetupUserService(sutProvider, user);\n\n        // Import model includes new and existing collection\n        var request = new ImportOrganizationCiphersRequestModel\n        {\n            Collections = new List<CollectionWithIdRequestModel>().ToArray(),   // No collections\n            Ciphers = fixture.Build<CipherRequestModel>()\n                .With(_ => _.OrganizationId, orgId.ToString())\n                .With(_ => _.FolderId, Guid.NewGuid().ToString())\n                .With(_ => _.ArchivedDate, (DateTime?)null)\n                .CreateMany(2).ToArray(),\n            CollectionRelationships = new List<KeyValuePair<int, int>>().ToArray(),\n        };\n\n        // AccessImportExport permission - false\n        sutProvider.GetDependency<ICurrentContext>()\n            .AccessImportExport(Arg.Any<Guid>())\n            .Returns(false);\n\n        // BulkCollectionOperations.ImportCiphers permission - false\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.ImportCiphers)))\n            .Returns(AuthorizationResult.Failed());\n\n        // BulkCollectionOperations.Create permission - TRUE\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(BulkCollectionOperations.Create)))\n            .Returns(AuthorizationResult.Success());\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(orgId)\n            .Returns(new List<Collection>());\n\n        // Act\n        // import ciphers only and no collections\n        // User has Create permissions\n        // expected to be successful\n        await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request);\n\n        // Assert\n        await sutProvider.GetDependency<IImportCiphersCommand>()\n            .Received(1)\n            .ImportIntoOrganizationalVaultAsync(\n                Arg.Any<List<Collection>>(),\n                Arg.Any<List<CipherDetails>>(),\n                Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),\n                Arg.Any<Guid>());\n    }\n\n    private static void SetupUserService(SutProvider<ImportCiphersController> sutProvider, User user)\n    {\n        // This is a workaround for the NSubstitute issue with ambiguous arguments\n        // when using Arg.Any<ClaimsPrincipal>() in the GetProperUserId method\n        // It clears the previous calls to the userService and sets up a new call\n        // with the same argument\n        var userService = sutProvider.GetDependency<Bit.Core.Services.IUserService>();\n        try\n        {\n            // in order to fix the Ambiguous Arguments error in NSubstitute\n            // we need to clear the previous calls\n            userService.ClearSubstitute();\n            userService.ClearReceivedCalls();\n            userService.GetProperUserId(Arg.Any<ClaimsPrincipal>());\n        }\n        catch { }\n\n        userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(user.Id);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Tools/Controllers/SendsControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing System.Text;\nusing System.Text.Json;\nusing AutoFixture.Xunit2;\nusing Bit.Api.Models.Response;\nusing Bit.Api.Tools.Controllers;\nusing Bit.Api.Tools.Models;\nusing Bit.Api.Tools.Models.Request;\nusing Bit.Api.Tools.Models.Response;\nusing Bit.Core;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Commands.Interfaces;\nusing Bit.Core.Tools.SendFeatures.Queries.Interfaces;\nusing Bit.Core.Tools.Services;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Tools.Controllers;\n\npublic class SendsControllerTests : IDisposable\n{\n    private readonly SendsController _sut;\n    private readonly IUserService _userService;\n    private readonly ISendRepository _sendRepository;\n    private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;\n    private readonly IAnonymousSendCommand _anonymousSendCommand;\n    private readonly ISendOwnerQuery _sendOwnerQuery;\n    private readonly ISendAuthorizationService _sendAuthorizationService;\n    private readonly ISendFileStorageService _sendFileStorageService;\n    private readonly ILogger<SendsController> _logger;\n    private readonly IFeatureService _featureService;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;\n\n    public SendsControllerTests()\n    {\n        _userService = Substitute.For<IUserService>();\n        _sendRepository = Substitute.For<ISendRepository>();\n        _nonAnonymousSendCommand = Substitute.For<INonAnonymousSendCommand>();\n        _anonymousSendCommand = Substitute.For<IAnonymousSendCommand>();\n        _sendOwnerQuery = Substitute.For<ISendOwnerQuery>();\n        _sendAuthorizationService = Substitute.For<ISendAuthorizationService>();\n        _sendFileStorageService = Substitute.For<ISendFileStorageService>();\n        _logger = Substitute.For<ILogger<SendsController>>();\n        _featureService = Substitute.For<IFeatureService>();\n        _pushNotificationService = Substitute.For<IPushNotificationService>();\n        _hasPremiumAccessQuery = Substitute.For<IHasPremiumAccessQuery>();\n\n        _sut = new SendsController(\n            _sendRepository,\n            _userService,\n            _sendAuthorizationService,\n            _anonymousSendCommand,\n            _nonAnonymousSendCommand,\n            _sendOwnerQuery,\n            _sendFileStorageService,\n            _logger,\n            _featureService,\n            _pushNotificationService,\n            _hasPremiumAccessQuery\n        );\n    }\n\n    public void Dispose()\n    {\n        _sut?.Dispose();\n    }\n\n    [Theory, AutoData]\n    public async Task SendsController_WhenSendHidesEmail_CreatorIdentifierShouldBeNull(\n        Guid id, Send send, User user)\n    {\n        var accessId = CoreHelpers.Base64UrlEncode(id.ToByteArray());\n\n        send.Id = default;\n        send.Type = SendType.Text;\n        send.Data = JsonSerializer.Serialize(new Dictionary<string, string>());\n        send.AuthType = AuthType.None;\n        send.Emails = null;\n        send.HideEmail = true;\n\n        _sendRepository.GetByIdAsync(Arg.Any<Guid>()).Returns(send);\n        _sendAuthorizationService.AccessAsync(send, null).Returns(SendAccessResult.Granted);\n        _userService.GetUserByIdAsync(Arg.Any<Guid>()).Returns(user);\n\n        var request = new SendAccessRequestModel();\n        var actionResult = await _sut.Access(accessId, request);\n        var response = (actionResult as ObjectResult)?.Value as SendAccessResponseModel;\n\n        Assert.NotNull(response);\n        Assert.Null(response.CreatorIdentifier);\n    }\n\n    [Fact]\n    public async Task Post_DeletionDateIsMoreThan31DaysFromNow_ThrowsBadRequest()\n    {\n        var now = DateTime.UtcNow;\n        var expected = \"You cannot have a Send with a deletion date that far \" +\n                       \"into the future. Adjust the Deletion Date to a value less than 31 days from now \" +\n                       \"and try again.\";\n        var request = new SendRequestModel() { DeletionDate = now.AddDays(32) };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Post(request));\n        Assert.Equal(expected, exception.Message);\n    }\n\n    [Theory, AutoData]\n    public async Task Get_WithValidId_ReturnsSendResponseModel(Guid sendId, Send send)\n    {\n        send.Type = SendType.Text;\n        var textData = new SendTextData(\"Test Send\", \"Notes\", \"Sample text\", false);\n        send.Data = JsonSerializer.Serialize(textData);\n        _sendOwnerQuery.Get(sendId, Arg.Any<ClaimsPrincipal>()).Returns(send);\n\n        var result = await _sut.Get(sendId.ToString());\n\n        Assert.NotNull(result);\n        Assert.IsType<SendResponseModel>(result);\n        Assert.Equal(send.Id, result.Id);\n        await _sendOwnerQuery.Received(1).Get(sendId, Arg.Any<ClaimsPrincipal>());\n    }\n\n    [Theory, AutoData]\n    public async Task Get_WithInvalidGuid_ThrowsException(string invalidId)\n    {\n        await Assert.ThrowsAsync<FormatException>(() => _sut.Get(invalidId));\n    }\n\n    [Fact]\n    public async Task GetAllOwned_ReturnsListResponseModelWithSendResponseModels()\n    {\n        var textSendData = new SendTextData(\"Test Send 1\", \"Notes 1\", \"Sample text\", false);\n        var fileSendData = new SendFileData(\"Test Send 2\", \"Notes 2\", \"test.txt\") { Id = \"file-123\", Size = 1024 };\n        var sends = new List<Send>\n        {\n            new Send { Id = Guid.NewGuid(), Type = SendType.Text, Data = JsonSerializer.Serialize(textSendData) },\n            new Send { Id = Guid.NewGuid(), Type = SendType.File, Data = JsonSerializer.Serialize(fileSendData) }\n        };\n        _sendOwnerQuery.GetOwned(Arg.Any<ClaimsPrincipal>()).Returns(sends);\n\n        var result = await _sut.GetAll();\n\n        Assert.NotNull(result);\n        Assert.IsType<ListResponseModel<SendResponseModel>>(result);\n        Assert.Equal(2, result.Data.Count());\n        var sendResponseModels = result.Data.ToList();\n        Assert.Equal(sends[0].Id, sendResponseModels[0].Id);\n        Assert.Equal(sends[1].Id, sendResponseModels[1].Id);\n        await _sendOwnerQuery.Received(1).GetOwned(Arg.Any<ClaimsPrincipal>());\n    }\n\n    [Fact]\n    public async Task GetAllOwned_WhenNoSends_ReturnsEmptyListResponseModel()\n    {\n        _sendOwnerQuery.GetOwned(Arg.Any<ClaimsPrincipal>()).Returns(new List<Send>());\n\n        var result = await _sut.GetAll();\n\n        Assert.NotNull(result);\n        Assert.IsType<ListResponseModel<SendResponseModel>>(result);\n        Assert.Empty(result.Data);\n        await _sendOwnerQuery.Received(1).GetOwned(Arg.Any<ClaimsPrincipal>());\n    }\n\n    [Theory, AutoData]\n    public async Task Post_WithPasswordAuthType_SetsAuthTypePassword(Guid userId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _sendAuthorizationService.HashPassword(Arg.Any<string>()).Returns(\"hashed_password\");\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Password,\n            Type = SendType.Text,\n            Key = \"key\",\n            Text = new SendTextModel { Text = \"text\" },\n            Password = \"password\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.Post(request);\n\n        Assert.NotNull(result);\n        Assert.Equal(AuthType.Password, result.AuthType);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.AuthType == AuthType.Password &&\n            s.Password != null &&\n            s.Emails == null &&\n            s.UserId == userId &&\n            s.Type == SendType.Text));\n        _userService.Received(1).GetProperUserId(Arg.Any<ClaimsPrincipal>());\n    }\n\n    [Theory, AutoData]\n    public async Task Post_WithEmailAuthType_SetsAuthTypeEmail(Guid userId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(true);\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Email,\n            Type = SendType.Text,\n            Key = \"key\",\n            Text = new SendTextModel { Text = \"text\" },\n            Emails = \"test@example.com\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.Post(request);\n\n        Assert.NotNull(result);\n        Assert.Equal(AuthType.Email, result.AuthType);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.AuthType == AuthType.Email &&\n            s.Emails != null &&\n            s.Password == null &&\n            s.UserId == userId &&\n            s.Type == SendType.Text));\n        _userService.Received(1).GetProperUserId(Arg.Any<ClaimsPrincipal>());\n    }\n\n    [Theory, AutoData]\n    public async Task Post_WithNoneAuthType_SetsAuthTypeNone(Guid userId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.None,\n            Type = SendType.Text,\n            Key = \"key\",\n            Text = new SendTextModel { Text = \"text\" },\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.Post(request);\n\n        Assert.NotNull(result);\n        Assert.Equal(AuthType.None, result.AuthType);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.AuthType == AuthType.None &&\n            s.Password == null &&\n            s.Emails == null &&\n            s.UserId == userId &&\n            s.Type == SendType.Text));\n        _userService.Received(1).GetProperUserId(Arg.Any<ClaimsPrincipal>());\n    }\n\n    [Theory, AutoData]\n    public async Task Post_WithEmails_WhenNotPremium_ThrowsBadRequestException(Guid userId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(false);\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Email,\n            Type = SendType.Text,\n            Key = \"key\",\n            Text = new SendTextModel { Text = \"text\" },\n            Emails = \"test@example.com\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Post(request));\n\n        Assert.Equal(\"Email verified Sends require a premium membership\", exception.Message);\n        await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task PostFile_WithEmails_WhenNotPremium_ThrowsBadRequestException(Guid userId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(false);\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Email,\n            Type = SendType.File,\n            Key = \"key\",\n            File = new SendFileModel { FileName = \"test.txt\" },\n            FileLength = 1024L,\n            Emails = \"test@example.com\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostFile(request));\n\n        Assert.Equal(\"Email verified Sends require a premium membership\", exception.Message);\n        await _nonAnonymousSendCommand.DidNotReceive()\n            .SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>());\n    }\n\n    [Theory, AutoData]\n    public async Task Put_WithEmails_WhenNotPremium_ThrowsBadRequestException(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(false);\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Email,\n            Type = SendType.Text,\n            Key = \"key\",\n            Text = new SendTextModel { Text = \"text\" },\n            Emails = \"test@example.com\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Put(sendId.ToString(), request));\n\n        Assert.Equal(\"Email verified Sends require a premium membership\", exception.Message);\n        await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any<Send>());\n    }\n\n    [Theory]\n    [InlineData(AuthType.Password)]\n    [InlineData(AuthType.Email)]\n    [InlineData(AuthType.None)]\n    public async Task Access_ReturnsCorrectAuthType(AuthType authType)\n    {\n        var sendId = Guid.NewGuid();\n        var accessId = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new Dictionary<string, string>()),\n            AuthType = authType\n        };\n\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n        _sendAuthorizationService.AccessAsync(send, \"pwd123\").Returns(SendAccessResult.Granted);\n\n        var request = new SendAccessRequestModel();\n        var actionResult = await _sut.Access(accessId, request);\n        var response = (actionResult as ObjectResult)?.Value as SendAccessResponseModel;\n\n        Assert.NotNull(response);\n        Assert.Equal(authType, response.AuthType);\n    }\n\n    [Theory]\n    [InlineData(AuthType.Password)]\n    [InlineData(AuthType.Email)]\n    [InlineData(AuthType.None)]\n    public async Task Get_ReturnsCorrectAuthType(AuthType authType)\n    {\n        var sendId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"a\", \"b\", \"c\", false)),\n            AuthType = authType\n        };\n\n        _sendOwnerQuery.Get(sendId, Arg.Any<ClaimsPrincipal>()).Returns(send);\n\n        var result = await _sut.Get(sendId.ToString());\n\n        Assert.NotNull(result);\n        Assert.Equal(authType, result.AuthType);\n    }\n\n    [Theory, AutoData]\n    public async Task Put_WithValidSend_UpdatesSuccessfully(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Old\", \"Old notes\", \"Old text\", false)),\n            AuthType = AuthType.None\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Password,\n            Type = SendType.Text,\n            Key = \"updated-key\",\n            Text = new SendTextModel { Text = \"updated text\" },\n            Password = \"new-password\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.Put(sendId.ToString(), request);\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s => s.Id == sendId));\n    }\n\n    [Theory, AutoData]\n    public async Task Put_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _sendRepository.GetByIdAsync(sendId).Returns((Send)null);\n\n        var request = new SendRequestModel\n        {\n            Type = SendType.Text,\n            Key = \"key\",\n            Text = new SendTextModel { Text = \"text\" },\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.Put(sendId.ToString(), request));\n    }\n\n    [Theory, AutoData]\n    public async Task Put_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = otherUserId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Old\", \"Old notes\", \"Old text\", false))\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var request = new SendRequestModel\n        {\n            Type = SendType.Text,\n            Key = \"key\",\n            Text = new SendTextModel { Text = \"text\" },\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.Put(sendId.ToString(), request));\n    }\n\n    [Theory, AutoData]\n    public async Task PutRemovePassword_WithValidSend_RemovesPasswordAndSetsAuthTypeNone(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            Password = \"hashed-password\",\n            AuthType = AuthType.Password\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var result = await _sut.PutRemovePassword(sendId.ToString());\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        Assert.Equal(AuthType.None, result.AuthType);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.Id == sendId &&\n            s.Password == null &&\n            s.AuthType == AuthType.None));\n    }\n\n    [Theory, AutoData]\n    public async Task PutRemovePassword_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _sendRepository.GetByIdAsync(sendId).Returns((Send)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.PutRemovePassword(sendId.ToString()));\n    }\n\n    [Theory, AutoData]\n    public async Task PutRemovePassword_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId,\n        Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = otherUserId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            Password = \"hashed-password\"\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.PutRemovePassword(sendId.ToString()));\n    }\n\n    [Theory, AutoData]\n    public async Task Delete_WithValidSend_DeletesSuccessfully(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false))\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        await _sut.Delete(sendId.ToString());\n\n        await _nonAnonymousSendCommand.Received(1).DeleteSendAsync(Arg.Is<Send>(s => s.Id == sendId));\n    }\n\n    [Theory, AutoData]\n    public async Task Delete_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _sendRepository.GetByIdAsync(sendId).Returns((Send)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.Delete(sendId.ToString()));\n    }\n\n    [Theory, AutoData]\n    public async Task Delete_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = otherUserId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false))\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.Delete(sendId.ToString()));\n    }\n\n    [Theory, AutoData]\n    public async Task PostFile_WithPasswordAuthType_SetsAuthTypePassword(Guid userId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _sendAuthorizationService.HashPassword(Arg.Any<string>()).Returns(\"hashed_password\");\n        _nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())\n            .Returns(\"https://example.com/upload\")\n            .AndDoes(callInfo =>\n            {\n                var send = callInfo.ArgAt<Send>(0);\n                var data = callInfo.ArgAt<SendFileData>(1);\n                send.Data = JsonSerializer.Serialize(data);\n            });\n\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Password,\n            Type = SendType.File,\n            Key = \"key\",\n            File = new SendFileModel { FileName = \"test.txt\" },\n            FileLength = 1024L,\n            Password = \"password\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.PostFile(request);\n\n        Assert.NotNull(result);\n        Assert.NotNull(result.SendResponse);\n        Assert.Equal(AuthType.Password, result.SendResponse.AuthType);\n        await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync(\n            Arg.Is<Send>(s =>\n                s.AuthType == AuthType.Password &&\n                s.Password != null &&\n                s.Emails == null &&\n                s.UserId == userId),\n            Arg.Any<SendFileData>(),\n            1024L);\n    }\n\n    [Theory, AutoData]\n    public async Task PostFile_WithEmailAuthType_SetsAuthTypeEmail(Guid userId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(true);\n        _nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())\n            .Returns(\"https://example.com/upload\")\n            .AndDoes(callInfo =>\n            {\n                var send = callInfo.ArgAt<Send>(0);\n                var data = callInfo.ArgAt<SendFileData>(1);\n                send.Data = JsonSerializer.Serialize(data);\n            });\n\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Email,\n            Type = SendType.File,\n            Key = \"key\",\n            File = new SendFileModel { FileName = \"test.txt\" },\n            FileLength = 1024L,\n            Emails = \"test@example.com\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.PostFile(request);\n\n        Assert.NotNull(result);\n        Assert.NotNull(result.SendResponse);\n        Assert.Equal(AuthType.Email, result.SendResponse.AuthType);\n        await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync(\n            Arg.Is<Send>(s =>\n                s.AuthType == AuthType.Email &&\n                s.Emails != null &&\n                s.Password == null &&\n                s.UserId == userId),\n            Arg.Any<SendFileData>(),\n            1024L);\n    }\n\n    [Theory, AutoData]\n    public async Task PostFile_WithNoneAuthType_SetsAuthTypeNone(Guid userId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())\n            .Returns(\"https://example.com/upload\")\n            .AndDoes(callInfo =>\n            {\n                var send = callInfo.ArgAt<Send>(0);\n                var data = callInfo.ArgAt<SendFileData>(1);\n                send.Data = JsonSerializer.Serialize(data);\n            });\n\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.None,\n            Type = SendType.File,\n            Key = \"key\",\n            File = new SendFileModel { FileName = \"test.txt\" },\n            FileLength = 1024L,\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.PostFile(request);\n\n        Assert.NotNull(result);\n        Assert.NotNull(result.SendResponse);\n        Assert.Equal(AuthType.None, result.SendResponse.AuthType);\n        await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync(\n            Arg.Is<Send>(s =>\n                s.AuthType == AuthType.None &&\n                s.Password == null &&\n                s.Emails == null &&\n                s.UserId == userId),\n            Arg.Any<SendFileData>(),\n            1024L);\n    }\n\n    [Theory, AutoData]\n    public async Task Put_ChangingFromPasswordToEmails_UpdatesAuthTypeToEmail(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(true);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Old\", \"Old notes\", \"Old text\", false)),\n            Password = \"hashed-password\",\n            AuthType = AuthType.Password\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Email,\n            Type = SendType.Text,\n            Key = \"updated-key\",\n            Text = new SendTextModel { Text = \"updated text\" },\n            Emails = \"new@example.com\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.Put(sendId.ToString(), request);\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.Id == sendId &&\n            s.AuthType == AuthType.Email &&\n            s.Emails != null &&\n            s.Password == null));\n    }\n\n    [Theory, AutoData]\n    public async Task Put_ChangingFromEmailToPassword_UpdatesAuthTypeToPassword(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _sendAuthorizationService.HashPassword(Arg.Any<string>()).Returns(\"hashed_password\");\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Old\", \"Old notes\", \"Old text\", false)),\n            Emails = \"old@example.com\",\n            AuthType = AuthType.Email\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Password,\n            Type = SendType.Text,\n            Key = \"updated-key\",\n            Text = new SendTextModel { Text = \"updated text\" },\n            Password = \"new-password\",\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.Put(sendId.ToString(), request);\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.Id == sendId &&\n            s.AuthType == AuthType.Password &&\n            s.Password != null &&\n            s.Emails == null));\n    }\n\n    [Theory, AutoData]\n    public async Task Put_WithoutPasswordOrEmails_PreservesNoneAuthType(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Old\", \"Old notes\", \"Old text\", false)),\n            Password = null,\n            Emails = null,\n            AuthType = AuthType.None\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.None,\n            Type = SendType.Text,\n            Key = \"updated-key\",\n            Text = new SendTextModel { Text = \"updated text\" },\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.Put(sendId.ToString(), request);\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.Id == sendId &&\n            s.AuthType == AuthType.None &&\n            s.Password == null &&\n            s.Emails == null));\n    }\n\n    [Theory, AutoData]\n    public async Task Put_WithExistingAndRequestPasswordAuth_PreservesExistingPasswordHash(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Old\", \"Old notes\", \"Old text\", false)),\n            Password = \"hashed-password\",\n            Emails = null,\n            AuthType = AuthType.Password\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.Password,\n            Type = SendType.Text,\n            Key = \"updated-key\",\n            Text = new SendTextModel { Text = \"updated text\" },\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.Put(sendId.ToString(), request);\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.Id == sendId &&\n            s.AuthType == AuthType.Password &&\n            s.Password != null &&\n            s.Emails == null));\n    }\n\n    [Theory, AutoData]\n    public async Task Put_ChangingFromEmailToNone_ClearsEmailAuth(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(true);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Old\", \"Old notes\", \"Old text\", false)),\n            Emails = \"old@example.com\",\n            Password = null,\n            AuthType = AuthType.Email\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var request = new SendRequestModel\n        {\n            AuthType = AuthType.None,\n            Type = SendType.Text,\n            Key = \"updated-key\",\n            Text = new SendTextModel { Text = \"updated text\" },\n            DeletionDate = DateTime.UtcNow.AddDays(7)\n        };\n\n        var result = await _sut.Put(sendId.ToString(), request);\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.Id == sendId &&\n            s.AuthType == AuthType.None &&\n            s.Emails == null &&\n            s.Password == null));\n    }\n\n    #region Authenticated Access Endpoints\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithValidSend_ReturnsSendAccessResponse(Guid sendId, User creator)\n    {\n        var send = new Send\n        {\n            Id = sendId,\n            UserId = creator.Id,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            HideEmail = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n        _userService.GetUserByIdAsync(creator.Id).Returns(creator);\n\n        var result = await _sut.AccessUsingAuth();\n\n        Assert.NotNull(result);\n        var objectResult = Assert.IsType<ObjectResult>(result);\n        var response = Assert.IsType<SendAccessResponseModel>(objectResult.Value);\n        Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id);\n        Assert.Equal(creator.Email, response.CreatorIdentifier);\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _userService.Received(1).GetUserByIdAsync(creator.Id);\n    }\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithEmailProtectedSend_WithFfDisabled_ReturnsUnauthorizedResult(Guid sendId, User creator)\n    {\n        var send = new Send\n        {\n            Id = sendId,\n            UserId = creator.Id,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            HideEmail = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            AuthType = AuthType.Email,\n            Emails = \"test@example.com\",\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n        _userService.GetUserByIdAsync(creator.Id).Returns(creator);\n        _featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());\n    }\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithHideEmail_DoesNotIncludeCreatorIdentifier(Guid sendId, User creator)\n    {\n        var send = new Send\n        {\n            Id = sendId,\n            UserId = creator.Id,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            HideEmail = true,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        var result = await _sut.AccessUsingAuth();\n\n        Assert.NotNull(result);\n        var objectResult = Assert.IsType<ObjectResult>(result);\n        var response = Assert.IsType<SendAccessResponseModel>(objectResult.Value);\n        Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id);\n        Assert.Null(response.CreatorIdentifier);\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithNoUserId_DoesNotIncludeCreatorIdentifier(Guid sendId)\n    {\n        var send = new Send\n        {\n            Id = sendId,\n            UserId = null,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            HideEmail = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        var result = await _sut.AccessUsingAuth();\n\n        Assert.NotNull(result);\n        var objectResult = Assert.IsType<ObjectResult>(result);\n        var response = Assert.IsType<SendAccessResponseModel>(objectResult.Value);\n        Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id);\n        Assert.Null(response.CreatorIdentifier);\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithNonExistentSend_ThrowsBadRequestException(Guid sendId)\n    {\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns((Send)null);\n\n        var exception =\n            await Assert.ThrowsAsync<BadRequestException>(() => _sut.AccessUsingAuth());\n\n        Assert.Equal(\"Could not locate send\", exception.Message);\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n    }\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithFileSend_ReturnsCorrectResponse(Guid sendId, User creator)\n    {\n        var fileData = new SendFileData(\"Test File\", \"Notes\", \"document.pdf\") { Id = \"file-123\", Size = 2048 };\n        var send = new Send\n        {\n            Id = sendId,\n            UserId = creator.Id,\n            Type = SendType.File,\n            Data = JsonSerializer.Serialize(fileData),\n            HideEmail = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n        _userService.GetUserByIdAsync(creator.Id).Returns(creator);\n\n        var result = await _sut.AccessUsingAuth();\n\n        Assert.NotNull(result);\n        var objectResult = Assert.IsType<ObjectResult>(result);\n        var response = Assert.IsType<SendAccessResponseModel>(objectResult.Value);\n        Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id);\n        Assert.Equal(SendType.File, response.Type);\n        Assert.NotNull(response.File);\n        Assert.Equal(\"file-123\", response.File.Id);\n        Assert.Equal(creator.Email, response.CreatorIdentifier);\n    }\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithDisabledSend_ThrowsNotFoundException(Guid sendId)\n    {\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = true,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());\n        await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithMaxAccessCountReached_ThrowsNotFoundException(Guid sendId)\n    {\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 10,\n            MaxAccessCount = 10\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());\n        await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithExpiredSend_ThrowsNotFoundException(Guid sendId)\n    {\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired yesterday\n            Disabled = false,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());\n        await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task AccessUsingAuth_WithDeletionDatePassed_ThrowsNotFoundException(Guid sendId)\n    {\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            DeletionDate = DateTime.UtcNow.AddDays(-1), // Deletion date has passed\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());\n        await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task GetSendFileDownloadDataUsingAuth_WithValidFileId_ReturnsDownloadUrl(\n        Guid sendId, string fileId, string expectedUrl)\n    {\n        var fileData = new SendFileData(\"Test File\", \"Notes\", \"document.pdf\") { Id = fileId, Size = 2048 };\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.File,\n            Data = JsonSerializer.Serialize(fileData),\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n        _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId)\n            .Returns((expectedUrl, SendAccessResult.Granted));\n\n        var result = await _sut.GetSendFileDownloadDataUsingAuth(fileId);\n\n        Assert.NotNull(result);\n        var objectResult = Assert.IsType<ObjectResult>(result);\n        var response = Assert.IsType<SendFileDownloadDataResponseModel>(objectResult.Value);\n        Assert.Equal(fileId, response.Id);\n        Assert.Equal(expectedUrl, response.Url);\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId);\n    }\n\n    [Theory, AutoData]\n    public async Task GetSendFileDownloadDataUsingAuth_WithEmailProtectedSend_WithFfDisabled_ReturnsUnauthorizedResult(\n        Guid sendId, string fileId, string expectedUrl)\n    {\n        var fileData = new SendFileData(\"Test File\", \"Notes\", \"document.pdf\") { Id = fileId, Size = 2048 };\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.File,\n            Data = JsonSerializer.Serialize(fileData),\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            AuthType = AuthType.Email,\n            Emails = \"test@example.com\",\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n        _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl);\n        _featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP).Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));\n    }\n\n    [Theory, AutoData]\n    public async Task GetSendFileDownloadDataUsingAuth_WithNonExistentSend_ThrowsBadRequestException(\n        Guid sendId, string fileId)\n    {\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns((Send)null);\n\n        var exception =\n            await Assert.ThrowsAsync<BadRequestException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));\n\n        Assert.Equal(\"Could not locate send\", exception.Message);\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _nonAnonymousSendCommand.DidNotReceive()\n            .GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n    }\n\n    [Theory, AutoData]\n    public async Task GetSendFileDownloadDataUsingAuth_WithTextSend_ThrowsBadRequestException(\n        Guid sendId, string fileId)\n    {\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n        _nonAnonymousSendCommand\n            .When(x => x.GetSendFileDownloadUrlAsync(send, fileId))\n            .Do(x => throw new BadRequestException(\"Can only get a download URL for a file type of Send\"));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.GetSendFileDownloadDataUsingAuth(fileId));\n\n        Assert.Equal(\"Can only get a download URL for a file type of Send\", exception.Message);\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId);\n    }\n\n    [Theory, AutoData]\n    public async Task GetSendFileDownloadDataUsingAuth_WithAccessDenied_ThrowsNotFoundException(\n        Guid sendId, string fileId)\n    {\n        var fileData = new SendFileData(\"Test File\", \"Notes\", \"document.pdf\") { Id = fileId, Size = 2048 };\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.File,\n            Data = JsonSerializer.Serialize(fileData),\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            Disabled = false,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n        var user = CreateUserWithSendIdClaim(sendId);\n        _sut.ControllerContext = CreateControllerContextWithUser(user);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n        _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId)\n            .Returns((null, SendAccessResult.Denied));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId);\n    }\n\n\n    #endregion\n\n    #region PutRemoveAuth Tests\n\n    [Theory, AutoData]\n    public async Task PutRemoveAuth_WithPasswordProtectedSend_RemovesPasswordAndSetsAuthTypeNone(Guid userId,\n        Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            Password = \"hashed-password\",\n            Emails = null,\n            AuthType = AuthType.Password\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var result = await _sut.PutRemoveAuth(sendId.ToString());\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        Assert.Equal(AuthType.None, result.AuthType);\n        Assert.Null(result.Password);\n        Assert.Null(result.Emails);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.Id == sendId &&\n            s.Password == null &&\n            s.Emails == null &&\n            s.AuthType == AuthType.None));\n    }\n\n    [Theory, AutoData]\n    public async Task PutRemoveAuth_WithEmailProtectedSend_RemovesEmailsAndSetsAuthTypeNone(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            Password = null,\n            Emails = \"test@example.com,user@example.com\",\n            AuthType = AuthType.Email\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var result = await _sut.PutRemoveAuth(sendId.ToString());\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        Assert.Equal(AuthType.None, result.AuthType);\n        Assert.Null(result.Password);\n        Assert.Null(result.Emails);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.Id == sendId &&\n            s.Password == null &&\n            s.Emails == null &&\n            s.AuthType == AuthType.None));\n    }\n\n    [Theory, AutoData]\n    public async Task PutRemoveAuth_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        _sendRepository.GetByIdAsync(sendId).Returns((Send)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.PutRemoveAuth(sendId.ToString()));\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task PutRemoveAuth_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = otherUserId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            Password = \"hashed-password\",\n            AuthType = AuthType.Password\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => _sut.PutRemoveAuth(sendId.ToString()));\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task PutRemoveAuth_WithNullUserId_ThrowsInvalidOperationException(Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns((Guid?)null);\n\n        var exception =\n            await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.PutRemoveAuth(sendId.ToString()));\n\n        Assert.Equal(\"User ID not found\", exception.Message);\n        await _sendRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());\n        await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task PutRemoveAuth_WithSendHavingBothPasswordAndEmails_RemovesBoth(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            Password = \"hashed-password\",\n            Emails = \"test@example.com\",\n            AuthType = AuthType.Password\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var result = await _sut.PutRemoveAuth(sendId.ToString());\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        Assert.Equal(AuthType.None, result.AuthType);\n        Assert.Null(result.Password);\n        Assert.Null(result.Emails);\n        await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>\n            s.Id == sendId &&\n            s.Password == null &&\n            s.Emails == null &&\n            s.AuthType == AuthType.None));\n    }\n\n    [Theory, AutoData]\n    public async Task PutRemoveAuth_PreservesOtherSendProperties(Guid userId, Guid sendId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var deletionDate = DateTime.UtcNow.AddDays(7);\n        var expirationDate = DateTime.UtcNow.AddDays(3);\n        var existingSend = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.Text,\n            Data = JsonSerializer.Serialize(new SendTextData(\"Test\", \"Notes\", \"Text\", false)),\n            Password = \"hashed-password\",\n            AuthType = AuthType.Password,\n            Key = \"encryption-key\",\n            MaxAccessCount = 10,\n            AccessCount = 3,\n            DeletionDate = deletionDate,\n            ExpirationDate = expirationDate,\n            Disabled = false,\n            HideEmail = true\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        var result = await _sut.PutRemoveAuth(sendId.ToString());\n\n        Assert.NotNull(result);\n        Assert.Equal(sendId, result.Id);\n        Assert.Equal(AuthType.None, result.AuthType);\n        // Verify other properties are preserved\n        Assert.Equal(\"encryption-key\", result.Key);\n        Assert.Equal(10, result.MaxAccessCount);\n        Assert.Equal(3, result.AccessCount);\n        Assert.Equal(deletionDate, result.DeletionDate);\n        Assert.Equal(expirationDate, result.ExpirationDate);\n        Assert.False(result.Disabled);\n        Assert.True(result.HideEmail);\n    }\n\n    #endregion\n\n    #region PostFileForExistingSend Tests\n\n    [Theory, AutoData]\n    public async Task PostFileForExistingSend_WithNullUserId_ThrowsInvalidOperationException(\n        Guid sendId, string fileId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns((Guid?)null);\n\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(\n            () => _sut.PostFileForExistingSend(sendId.ToString(), fileId));\n\n        Assert.Equal(\"User ID not found\", exception.Message);\n        await _sendRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());\n        await _nonAnonymousSendCommand.DidNotReceive()\n            .UploadFileToExistingSendAsync(Arg.Any<Stream>(), Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task PostFileForExistingSend_WithNonMultipartContentType_ThrowsBadRequestException(\n        Guid userId, Guid sendId, string fileId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var context = new DefaultHttpContext();\n        context.Request.ContentType = \"application/json\";\n        _sut.ControllerContext = new ControllerContext { HttpContext = context };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.PostFileForExistingSend(sendId.ToString(), fileId));\n\n        Assert.Equal(\"Invalid content.\", exception.Message);\n        await _sendRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());\n        await _nonAnonymousSendCommand.DidNotReceive()\n            .UploadFileToExistingSendAsync(Arg.Any<Stream>(), Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task PostFileForExistingSend_WithNullContentType_ThrowsBadRequestException(\n        Guid userId, Guid sendId, string fileId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var context = new DefaultHttpContext();\n        _sut.ControllerContext = new ControllerContext { HttpContext = context };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => _sut.PostFileForExistingSend(sendId.ToString(), fileId));\n\n        Assert.Equal(\"Invalid content.\", exception.Message);\n        await _sendRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());\n        await _nonAnonymousSendCommand.DidNotReceive()\n            .UploadFileToExistingSendAsync(Arg.Any<Stream>(), Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task PostFileForExistingSend_WithNonExistentSend_ThrowsNotFoundException(\n        Guid userId, Guid sendId, string fileId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var context = new DefaultHttpContext();\n        context.Request.ContentType = \"multipart/form-data; boundary=test-boundary\";\n        _sut.ControllerContext = new ControllerContext { HttpContext = context };\n        _sendRepository.GetByIdAsync(sendId).Returns((Send)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => _sut.PostFileForExistingSend(sendId.ToString(), fileId));\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _nonAnonymousSendCommand.DidNotReceive()\n            .UploadFileToExistingSendAsync(Arg.Any<Stream>(), Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task PostFileForExistingSend_WithWrongUser_ThrowsNotFoundException(\n        Guid userId, Guid otherUserId, Guid sendId, string fileId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var context = new DefaultHttpContext();\n        context.Request.ContentType = \"multipart/form-data; boundary=test-boundary\";\n        _sut.ControllerContext = new ControllerContext { HttpContext = context };\n        var existingSend = new Send { Id = sendId, UserId = otherUserId };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => _sut.PostFileForExistingSend(sendId.ToString(), fileId));\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _nonAnonymousSendCommand.DidNotReceive()\n            .UploadFileToExistingSendAsync(Arg.Any<Stream>(), Arg.Any<Send>());\n    }\n\n    [Theory, AutoData]\n    public async Task PostFileForExistingSend_WithValidData_UploadsFileSuccessfully(\n        Guid userId, Guid sendId, string fileId)\n    {\n        _userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);\n        var existingSend = new Send { Id = sendId, UserId = userId };\n        _sendRepository.GetByIdAsync(sendId).Returns(existingSend);\n\n        const string boundary = \"test-boundary-123\";\n        var bodyBuilder = new StringBuilder();\n        bodyBuilder.Append($\"--{boundary}\\r\\n\");\n        bodyBuilder.Append(\"Content-Disposition: form-data; name=\\\"data\\\"; filename=\\\"test.txt\\\"\\r\\n\");\n        bodyBuilder.Append(\"\\r\\n\");\n        bodyBuilder.Append(\"file content here\");\n        bodyBuilder.Append($\"\\r\\n--{boundary}--\\r\\n\");\n        var bodyBytes = Encoding.UTF8.GetBytes(bodyBuilder.ToString());\n\n        var context = new DefaultHttpContext();\n        context.Request.ContentType = $\"multipart/form-data; boundary={boundary}\";\n        context.Request.Body = new MemoryStream(bodyBytes);\n        _sut.ControllerContext = new ControllerContext { HttpContext = context };\n\n        await _sut.PostFileForExistingSend(sendId.ToString(), fileId);\n\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n        await _nonAnonymousSendCommand.Received(1)\n            .UploadFileToExistingSendAsync(Arg.Any<Stream>(), Arg.Is<Send>(s => s.Id == sendId));\n    }\n\n    #endregion\n\n    #region Test Helpers\n\n    private static ClaimsPrincipal CreateUserWithSendIdClaim(Guid sendId)\n    {\n        var claims = new List<Claim> { new Claim(\"send_id\", sendId.ToString()) };\n        var identity = new ClaimsIdentity(claims, \"TestAuth\");\n        return new ClaimsPrincipal(identity);\n    }\n\n    private static ControllerContext CreateControllerContextWithUser(ClaimsPrincipal user)\n    {\n        return new ControllerContext { HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext { User = user } };\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Api.Tools.Models;\nusing Bit.Api.Tools.Models.Request;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Services;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\nnamespace Bit.Api.Test.Models.Request;\n\npublic class SendRequestModelTests\n{\n    [Fact]\n    public void ToSend_Text_Success()\n    {\n        var deletionDate = DateTime.UtcNow.AddDays(5);\n        var sendRequest = new SendRequestModel\n        {\n            AuthType = AuthType.Password,\n            DeletionDate = deletionDate,\n            Disabled = false,\n            ExpirationDate = null,\n            HideEmail = false,\n            Key = \"encrypted_key\",\n            MaxAccessCount = null,\n            Name = \"encrypted_name\",\n            Notes = null,\n            Password = \"Password\",\n            Text = new SendTextModel()\n            {\n                Hidden = false,\n                Text = \"encrypted_text\"\n            },\n            Type = SendType.Text,\n        };\n\n        var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();\n        sendAuthorizationService.HashPassword(Arg.Any<string>())\n            .Returns((info) => $\"hashed_{(string)info[0]}\");\n\n        var send = sendRequest.ToSend(Guid.NewGuid(), sendAuthorizationService);\n\n        Assert.Equal(deletionDate, send.DeletionDate);\n        Assert.False(send.Disabled);\n        Assert.Null(send.ExpirationDate);\n        Assert.False(send.HideEmail);\n        Assert.Equal(\"encrypted_key\", send.Key);\n        Assert.Equal(\"hashed_Password\", send.Password);\n\n        using var jsonDocument = JsonDocument.Parse(send.Data);\n        var root = jsonDocument.RootElement;\n        var text = AssertHelper.AssertJsonProperty(root, \"Text\", JsonValueKind.String).GetString();\n        Assert.Equal(\"encrypted_text\", text);\n        AssertHelper.AssertJsonProperty(root, \"Hidden\", JsonValueKind.False);\n        Assert.False(root.TryGetProperty(\"Notes\", out var _));\n        var name = AssertHelper.AssertJsonProperty(root, \"Name\", JsonValueKind.String).GetString();\n        Assert.Equal(\"encrypted_name\", name);\n    }\n\n    [Fact]\n    public void ValidateEdit_DeletionDateInPast_ThrowsBadRequestException()\n    {\n        var send = new SendRequestModel\n        {\n            DeletionDate = DateTime.UtcNow.AddMinutes(-5)\n        };\n\n        Assert.Throws<BadRequestException>(() => send.ValidateEdit());\n    }\n\n    [Fact]\n    public void ValidateEdit_DeletionDateTooFarInFuture_ThrowsBadRequestException()\n    {\n        var send = new SendRequestModel\n        {\n            DeletionDate = DateTime.UtcNow.AddDays(32)\n        };\n\n        Assert.Throws<BadRequestException>(() => send.ValidateEdit());\n    }\n\n    [Fact]\n    public void ValidateEdit_ExpirationDateInPast_ThrowsBadRequestException()\n    {\n        var send = new SendRequestModel\n        {\n            ExpirationDate = DateTime.UtcNow.AddMinutes(-5)\n        };\n\n        Assert.Throws<BadRequestException>(() => send.ValidateEdit());\n    }\n\n    [Fact]\n    public void ValidateEdit_ExpirationDateGreaterThanDeletionDate_ThrowsBadRequestException()\n    {\n        var send = new SendRequestModel\n        {\n            DeletionDate = DateTime.UtcNow.AddDays(1),\n            ExpirationDate = DateTime.UtcNow.AddDays(2)\n        };\n\n        Assert.Throws<BadRequestException>(() => send.ValidateEdit());\n    }\n\n    [Fact]\n    public void ValidateEdit_ValidDates_Success()\n    {\n        var send = new SendRequestModel\n        {\n            DeletionDate = DateTime.UtcNow.AddDays(10),\n            ExpirationDate = DateTime.UtcNow.AddDays(5)\n        };\n\n        Exception ex = Record.Exception(() => send.ValidateEdit());\n\n        Assert.Null(ex);\n    }\n\n    [Fact]\n    public void UpdateSend_WithExistingAndRequestPasswordAuth_PreservesExistingPasswordHash()\n    {\n        var deletionDate = DateTime.UtcNow.AddDays(5);\n        var sendRequest = new SendRequestModel\n        {\n            AuthType = AuthType.Password,\n            DeletionDate = deletionDate,\n            Disabled = false,\n            Key = \"encrypted_key\",\n            Name = \"encrypted_name\",\n            Text = new SendTextModel { Hidden = false, Text = \"encrypted_text\" },\n            Type = SendType.Text,\n        };\n\n        var existingSend = new Send\n        {\n            Type = SendType.Text,\n            Password = \"existing_hashed_password\",\n            AuthType = AuthType.Password,\n            Emails = null,\n        };\n\n        var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();\n\n        var updatedSend = sendRequest.UpdateSend(existingSend, sendAuthorizationService);\n\n        Assert.Equal(AuthType.Password, updatedSend.AuthType);\n        Assert.Equal(\"existing_hashed_password\", updatedSend.Password);\n        Assert.Null(updatedSend.Emails);\n    }\n\n    [Fact]\n    public void UpdateSend_ChangingFromEmailToNone_ClearsEmailsAndSetsAuthTypeNone()\n    {\n        var deletionDate = DateTime.UtcNow.AddDays(5);\n        var sendRequest = new SendRequestModel\n        {\n            AuthType = AuthType.None,\n            DeletionDate = deletionDate,\n            Disabled = false,\n            Key = \"encrypted_key\",\n            Name = \"encrypted_name\",\n            Text = new SendTextModel { Hidden = false, Text = \"encrypted_text\" },\n            Type = SendType.Text,\n        };\n\n        var existingSend = new Send\n        {\n            Type = SendType.Text,\n            Emails = \"old@example.com\",\n            AuthType = AuthType.Email,\n            Password = null,\n        };\n\n        var sendAuthorizationService = Substitute.For<ISendAuthorizationService>();\n\n        var updatedSend = sendRequest.UpdateSend(existingSend, sendAuthorizationService);\n\n        Assert.Equal(AuthType.None, updatedSend.AuthType);\n        Assert.Null(updatedSend.Emails);\n        Assert.Null(updatedSend.Password);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Utilities/ApiHelpersTests.cs",
    "content": "﻿using System.Text;\nusing Bit.Api.Utilities;\nusing Bit.Core.Billing.Organizations.Models;\nusing Microsoft.AspNetCore.Http;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Utilities;\n\npublic class ApiHelpersTests\n{\n    [Fact]\n    public async Task ReadJsonFileFromBody_Success()\n    {\n        var context = Substitute.For<HttpContext>();\n        context.Request.ContentLength.Returns(200);\n        var bytes = Encoding.UTF8.GetBytes(testFile);\n        var formFile = new FormFile(new MemoryStream(bytes), 0, bytes.Length, \"bitwarden_organization_license\", \"bitwarden_organization_license.json\");\n\n\n        var license = await ApiHelpers.ReadJsonFileFromBody<OrganizationLicense>(context, formFile);\n        Assert.Equal(8, license.Version);\n    }\n\n    const string testFile = \"{\\\"licenseKey\\\": \\\"licenseKey\\\", \\\"installationId\\\": \\\"6285f891-b2ec-4047-84c5-2eb7f7747e74\\\", \\\"id\\\": \\\"1065216d-5854-4326-838d-635487f30b43\\\",\\\"name\\\": \\\"Test Org\\\",\\\"billingEmail\\\": \\\"test@email.com\\\",\\\"businessName\\\": null,\\\"enabled\\\": true, \\\"plan\\\": \\\"Enterprise (Annually)\\\",\\\"planType\\\": 11,\\\"seats\\\": 6,\\\"maxCollections\\\": null,\\\"usePolicies\\\": true,\\\"useSso\\\": true,\\\"useKeyConnector\\\": false,\\\"useGroups\\\": true,\\\"useEvents\\\": true,\\\"useDirectory\\\": true,\\\"useTotp\\\": true,\\\"use2fa\\\": true,\\\"useApi\\\": true,\\\"useResetPassword\\\": true,\\\"maxStorageGb\\\": 1,\\\"selfHost\\\": true,\\\"usersGetPremium\\\": true,\\\"version\\\": 8,\\\"issued\\\": \\\"2022-01-25T21:58:38.9454581Z\\\",\\\"refresh\\\": \\\"2022-01-28T14:26:31Z\\\",\\\"expires\\\": \\\"2022-01-28T14:26:31Z\\\",\\\"trial\\\": true,\\\"hash\\\": \\\"testvalue\\\",\\\"signature\\\": \\\"signature\\\"}\";\n}\n"
  },
  {
    "path": "test/Api.Test/Utilities/DiagnosticTools/EventDiagnosticLoggerTests.cs",
    "content": "﻿using Bit.Api.Dirt.Public.Models;\nusing Bit.Api.Models.Public.Response;\nusing Bit.Api.Utilities.DiagnosticTools;\nusing Bit.Core;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Utilities.DiagnosticTools;\n\npublic class EventDiagnosticLoggerTests\n{\n    [Theory, BitAutoData]\n    public void LogAggregateData_WithPublicResponse_FeatureFlagEnabled_LogsInformation(\n        Guid organizationId)\n    {\n        // Arrange\n        var logger = Substitute.For<ILogger>();\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);\n\n        var request = new EventFilterRequestModel()\n        {\n            Start = DateTime.UtcNow.AddMinutes(-3),\n            End = DateTime.UtcNow,\n            ActingUserId = Guid.NewGuid(),\n            ItemId = Guid.NewGuid(),\n        };\n\n        var newestEvent = Substitute.For<IEvent>();\n        newestEvent.Date.Returns(DateTime.UtcNow);\n        var middleEvent = Substitute.For<IEvent>();\n        middleEvent.Date.Returns(DateTime.UtcNow.AddDays(-1));\n        var oldestEvent = Substitute.For<IEvent>();\n        oldestEvent.Date.Returns(DateTime.UtcNow.AddDays(-3));\n\n        var eventResponses = new List<EventResponseModel>\n        {\n            new (newestEvent),\n            new (middleEvent),\n            new (oldestEvent)\n        };\n        var response = new PagedListResponseModel<EventResponseModel>(eventResponses, \"continuation-token\");\n\n        // Act\n        logger.LogAggregateData(featureService, organizationId, response, request);\n\n        // Assert\n        logger.Received(1).Log(\n            LogLevel.Information,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains(organizationId.ToString()) &&\n                o.ToString().Contains($\"Event count:{eventResponses.Count}\") &&\n                o.ToString().Contains($\"newest record:{newestEvent.Date:O}\") &&\n                o.ToString().Contains($\"oldest record:{oldestEvent.Date:O}\") &&\n                o.ToString().Contains(\"HasMore:True\") &&\n                o.ToString().Contains($\"Start:{request.Start:o}\") &&\n                o.ToString().Contains($\"End:{request.End:o}\") &&\n                o.ToString().Contains($\"ActingUserId:{request.ActingUserId}\") &&\n                o.ToString().Contains($\"ItemId:{request.ItemId}\"))\n            ,\n            null,\n            Arg.Any<Func<object, Exception, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public void LogAggregateData_WithPublicResponse_FeatureFlagDisabled_DoesNotLog(\n        Guid organizationId,\n        EventFilterRequestModel request)\n    {\n        // Arrange\n        var logger = Substitute.For<ILogger>();\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(false);\n\n        PagedListResponseModel<EventResponseModel> dummy = null;\n\n        // Act\n        logger.LogAggregateData(featureService, organizationId, dummy, request);\n\n        // Assert\n        logger.DidNotReceive().Log(\n            LogLevel.Information,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public void LogAggregateData_WithPublicResponse_EmptyData_LogsZeroCount(\n        Guid organizationId)\n    {\n        // Arrange\n        var logger = Substitute.For<ILogger>();\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);\n\n        var request = new EventFilterRequestModel()\n        {\n            Start = null,\n            End = null,\n            ActingUserId = null,\n            ItemId = null,\n            ContinuationToken = null,\n        };\n        var response = new PagedListResponseModel<EventResponseModel>(new List<EventResponseModel>(), null);\n\n        // Act\n        logger.LogAggregateData(featureService, organizationId, response, request);\n\n        // Assert\n        logger.Received(1).Log(\n            LogLevel.Information,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains(organizationId.ToString()) &&\n                o.ToString().Contains(\"Event count:0\") &&\n                o.ToString().Contains(\"HasMore:False\")),\n            null,\n            Arg.Any<Func<object, Exception, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public void LogAggregateData_WithInternalResponse_FeatureFlagDisabled_DoesNotLog(Guid organizationId)\n    {\n        // Arrange\n        var logger = Substitute.For<ILogger>();\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(false);\n\n\n        // Act\n        logger.LogAggregateData(featureService, organizationId, null, null, null, null);\n\n        // Assert\n        logger.DidNotReceive().Log(\n            LogLevel.Information,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public void LogAggregateData_WithInternalResponse_EmptyData_LogsZeroCount(\n        Guid organizationId)\n    {\n        // Arrange\n        var logger = Substitute.For<ILogger>();\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);\n\n        Api.Dirt.Models.Response.EventResponseModel[] emptyEvents = [];\n\n        // Act\n        logger.LogAggregateData(featureService, organizationId, emptyEvents, null, null, null);\n\n        // Assert\n        logger.Received(1).Log(\n            LogLevel.Information,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains(organizationId.ToString()) &&\n                o.ToString().Contains(\"Event count:0\") &&\n                o.ToString().Contains(\"HasMore:False\")),\n            null,\n            Arg.Any<Func<object, Exception, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public void LogAggregateData_WithInternalResponse_FeatureFlagEnabled_LogsInformation(\n        Guid organizationId)\n    {\n        // Arrange\n        var logger = Substitute.For<ILogger>();\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.EventDiagnosticLogging).Returns(true);\n\n        var newestEvent = Substitute.For<IEvent>();\n        newestEvent.Date.Returns(DateTime.UtcNow);\n        var middleEvent = Substitute.For<IEvent>();\n        middleEvent.Date.Returns(DateTime.UtcNow.AddDays(-1));\n        var oldestEvent = Substitute.For<IEvent>();\n        oldestEvent.Date.Returns(DateTime.UtcNow.AddDays(-2));\n\n        var events = new List<Api.Dirt.Models.Response.EventResponseModel>\n        {\n            new (newestEvent),\n            new (middleEvent),\n            new (oldestEvent)\n        };\n\n        var queryStart = DateTime.UtcNow.AddMinutes(-3);\n        var queryEnd = DateTime.UtcNow;\n        const string continuationToken = \"continuation-token\";\n\n        // Act\n        logger.LogAggregateData(featureService, organizationId, events, continuationToken, queryStart, queryEnd);\n\n        // Assert\n        logger.Received(1).Log(\n            LogLevel.Information,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains(organizationId.ToString()) &&\n                o.ToString().Contains($\"Event count:{events.Count}\") &&\n                o.ToString().Contains($\"newest record:{newestEvent.Date:O}\") &&\n                o.ToString().Contains($\"oldest record:{oldestEvent.Date:O}\") &&\n                o.ToString().Contains(\"HasMore:True\") &&\n                o.ToString().Contains($\"Start:{queryStart:o}\") &&\n                o.ToString().Contains($\"End:{queryEnd:o}\"))\n            ,\n            null,\n            Arg.Any<Func<object, Exception, string>>());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Utilities/EnumMatchesAttributeTests.cs",
    "content": "﻿using Bit.Api.Utilities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Xunit;\n\nnamespace Bit.Api.Test.Utilities;\n\npublic class EnumMatchesAttributeTests\n{\n    [Fact]\n    public void IsValid_NullInput_False()\n    {\n        var enumMatchesAttribute =\n            new EnumMatchesAttribute<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly);\n\n        var result = enumMatchesAttribute.IsValid(null);\n\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void IsValid_NullAccepted_False()\n    {\n        var enumMatchesAttribute =\n            new EnumMatchesAttribute<PlanType>();\n\n        var result = enumMatchesAttribute.IsValid(PlanType.TeamsMonthly);\n\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void IsValid_EmptyAccepted_False()\n    {\n        var enumMatchesAttribute =\n            new EnumMatchesAttribute<PlanType>([]);\n\n        var result = enumMatchesAttribute.IsValid(PlanType.TeamsMonthly);\n\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void IsValid_ParseFails_False()\n    {\n        var enumMatchesAttribute =\n            new EnumMatchesAttribute<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly);\n\n        var result = enumMatchesAttribute.IsValid(GatewayType.Stripe);\n\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void IsValid_Matches_True()\n    {\n        var enumMatchesAttribute =\n            new EnumMatchesAttribute<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly);\n\n        var result = enumMatchesAttribute.IsValid(PlanType.TeamsMonthly);\n\n        Assert.True(result);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Utilities/KdfSettingsValidatorTests.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Utilities;\nusing Xunit;\n\nnamespace Bit.Api.Test.Utilities;\n\npublic class KdfSettingsValidatorTests\n{\n    [Theory]\n    [InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle\n    [InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary\n    [InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary\n    [InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle\n    [InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary\n    [InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary\n    public void Validate_IsValid(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)\n    {\n        var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);\n        Assert.Empty(results);\n    }\n\n    [Theory]\n    [InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations\n    [InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations\n    [InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0\n    [InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory\n    [InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value\n    [InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory\n    [InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value\n    public void Validate_Fails(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures)\n    {\n        var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);\n        Assert.NotEmpty(results);\n        Assert.Equal(expectedFailures, results.Count());\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Vault/AuthorizationHandlers/BulkCollectionAuthorizationHandlerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Vault.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Vault.AuthorizationHandlers;\n\n[SutProviderCustomize]\npublic class BulkCollectionAuthorizationHandlerTests\n{\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanCreateAsync_WhenAdminOrOwner_Success(\n        OrganizationUserType userType,\n        Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        ArrangeOrganizationAbility(sutProvider, organization, true, true);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Create },\n            new ClaimsPrincipal(),\n            collections);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.User;\n\n        ArrangeOrganizationAbility(sutProvider, organization, false, false);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Create },\n            new ClaimsPrincipal(),\n            collections);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task CanCreateAsync_WhenMissingPermissions_NoSuccess(\n        OrganizationUserType userType,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = false,\n            DeleteAnyCollection = false,\n            ManageGroups = false,\n            ManageUsers = false\n        };\n\n        ArrangeOrganizationAbility(sutProvider, organization, true, true);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Create },\n            new ClaimsPrincipal(),\n            collections);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess(\n        Guid userId,\n        CurrentContextOrganization organization,\n        List<Collection> collections,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider)\n    {\n        collections.ForEach(c => c.OrganizationId = organization.Id);\n        ArrangeOrganizationAbility(sutProvider, organization, true, true);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Create },\n            new ClaimsPrincipal(),\n            collections\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanReadAsync_WhenAdminOrOwner_Success(\n        OrganizationUserType userType,\n        Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { BulkCollectionOperations.Read },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.True(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(true, false)]\n    [BitAutoData(false, true)]\n    public async Task CanReadAsync_WhenCustomUserWithRequiredPermissions_Success(\n        bool editAnyCollection, bool deleteAnyCollection,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = editAnyCollection,\n            DeleteAnyCollection = deleteAnyCollection\n        };\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { BulkCollectionOperations.Read },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.True(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanReadAsync_WhenUserCanManageCollections_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        foreach (var c in collections)\n        {\n            c.Manage = true;\n        }\n\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n            sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { BulkCollectionOperations.Read },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.True(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanReadAsync_WhenUserIsNotAssignedToCollections_NoSuccess(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { BulkCollectionOperations.Read },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.False(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task CanReadAsync_WhenMissingPermissions_NoSuccess(\n        OrganizationUserType userType,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = false,\n            DeleteAnyCollection = false,\n            ManageGroups = false,\n            ManageUsers = false\n        };\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { BulkCollectionOperations.Read },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.False(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanReadAsync_WhenMissingOrgAccess_NoSuccess(\n        Guid userId,\n        ICollection<Collection> collections,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider)\n    {\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Read, BulkCollectionOperations.ReadAccess\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { op },\n                new ClaimsPrincipal(),\n                collections\n            );\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.False(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanReadWithAccessAsync_WhenAdminOrOwner_Success(\n        OrganizationUserType userType,\n        Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ReadWithAccess },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(true, false, false)]\n    [BitAutoData(false, true, false)]\n    [BitAutoData(false, false, true)]\n\n    public async Task CanReadWithAccessAsync_WhenCustomUserWithRequiredPermissions_Success(\n        bool editAnyCollection, bool deleteAnyCollection, bool manageUsers,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = editAnyCollection,\n            DeleteAnyCollection = deleteAnyCollection,\n            ManageUsers = manageUsers\n        };\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ReadWithAccess },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanReadWithAccessAsync_WhenUserCanManageCollections_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        foreach (var c in collections)\n        {\n            c.Manage = true;\n        }\n\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ReadWithAccess },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanReadWithAccessAsync_WhenUserCanNotManageCollections_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        foreach (var c in collections)\n        {\n            c.Manage = false;\n        }\n\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ReadWithAccess },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task CanReadWithAccessAsync_WhenMissingPermissions_NoSuccess(\n        OrganizationUserType userType,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = false,\n            DeleteAnyCollection = false,\n            ManageGroups = false,\n            ManageUsers = false\n        };\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ReadWithAccess },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanReadWithAccessAsync_WhenMissingOrgAccess_NoSuccess(\n        Guid userId,\n        ICollection<Collection> collections,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ReadWithAccess },\n            new ClaimsPrincipal(),\n            collections\n        );\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanUpdateCollection_WhenAdminOrOwner_WithV1Enabled_PermittedByCollectionManagementSettings_Success(\n        OrganizationUserType userType,\n        Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections, CurrentContextOrganization organization,\n        OrganizationAbility organizationAbility)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n        organizationAbility.Id = organization.Id;\n        organizationAbility.AllowAdminAccessToAllCollectionItems = true;\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Update,\n            BulkCollectionOperations.ModifyUserAccess,\n            BulkCollectionOperations.ModifyGroupAccess,\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n            sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)\n                .Returns(organizationAbility);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { op },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.True(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanUpdateCollection_WhenAdminOrOwner_WithV1Enabled_NotPermittedByCollectionManagementSettings_Failure(\n        OrganizationUserType userType,\n        Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections, CurrentContextOrganization organization,\n        OrganizationAbility organizationAbility)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n        organizationAbility.Id = organization.Id;\n        organizationAbility.AllowAdminAccessToAllCollectionItems = false;\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Update,\n            BulkCollectionOperations.ModifyUserAccess,\n            BulkCollectionOperations.ModifyGroupAccess,\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n            sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)\n                .Returns(organizationAbility);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { op },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.False(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanUpdateCollection_WithEditAnyCollectionPermission_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = true\n        };\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Update,\n            BulkCollectionOperations.ModifyUserAccess,\n            BulkCollectionOperations.ModifyGroupAccess,\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { op },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.True(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanUpdateCollection_WithManageCollectionPermission_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        foreach (var c in collections)\n        {\n            c.Manage = true;\n        }\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Update,\n            BulkCollectionOperations.ModifyUserAccess,\n            BulkCollectionOperations.ModifyGroupAccess,\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n            sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { op },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.True(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task CanUpdateCollection_WhenMissingPermissions_NoSuccess(\n        OrganizationUserType userType,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = false,\n            DeleteAnyCollection = false,\n            ManageGroups = false,\n            ManageUsers = false\n        };\n\n        foreach (var collectionDetail in collections)\n        {\n            collectionDetail.Manage = true;\n        }\n        // Simulate one collection missing the manage permission\n        collections.First().Manage = false;\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Update,\n            BulkCollectionOperations.ModifyUserAccess,\n            BulkCollectionOperations.ModifyGroupAccess,\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { op },\n                new ClaimsPrincipal(),\n                collections);\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.False(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanUpdateCollection_WhenMissingOrgAccess_NoSuccess(\n        Guid userId,\n        ICollection<Collection> collections,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider)\n    {\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Update,\n            BulkCollectionOperations.ModifyUserAccess,\n            BulkCollectionOperations.ModifyGroupAccess,\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { op },\n                new ClaimsPrincipal(),\n                collections\n            );\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.False(context.HasSucceeded);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanUpdateUsers_WithManageUsersCustomPermission_AllowAdminAccessIsTrue_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,\n        CurrentContextOrganization organization, Guid actingUserId)\n    {\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            ManageUsers = true\n        };\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility { AllowAdminAccessToAllCollectionItems = true });\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ModifyUserAccess },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanUpdateUsers_WithManageUsersCustomPermission_AllowAdminAccessIsFalse_Failure(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,\n        CurrentContextOrganization organization, Guid actingUserId)\n    {\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            ManageUsers = true\n        };\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility { AllowAdminAccessToAllCollectionItems = false });\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ModifyUserAccess },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanUpdateGroups_WithManageGroupsCustomPermission_AllowAdminAccessIsTrue_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,\n        CurrentContextOrganization organization, Guid actingUserId)\n    {\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            ManageGroups = true\n        };\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility { AllowAdminAccessToAllCollectionItems = true });\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ModifyGroupAccess },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanUpdateGroups_WithManageGroupsCustomPermission_AllowAdminAccessIsFalse_Failure(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider, ICollection<Collection> collections,\n        CurrentContextOrganization organization, Guid actingUserId)\n    {\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            ManageGroups = true\n        };\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility { AllowAdminAccessToAllCollectionItems = false });\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.ModifyGroupAccess },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanDeleteAsync_WithDeleteAnyCollectionPermission_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            DeleteAnyCollection = true\n        };\n\n        // `LimitCollectonCreationDeletionSplit` feature flag state isn't\n        // relevant for this test. The flag is never checked for in this\n        // test. This is asserted below.\n        ArrangeOrganizationAbility(sutProvider, organization, true, true);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Delete },\n            new ClaimsPrincipal(),\n            collections);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanDeleteAsync_WhenAdminOrOwner_AllowAdminAccessToAllCollectionItemsTrue_Success(\n        OrganizationUserType userType,\n        Guid userId, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        // `LimitCollectonCreationDeletionSplit` feature flag state isn't\n        // relevant for this test. The flag is never checked for in this\n        // test. This is asserted below.\n        ArrangeOrganizationAbility(sutProvider, organization, true, true);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Delete },\n            new ClaimsPrincipal(),\n            collections);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        ArrangeOrganizationAbility(sutProvider, organization, false, false);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n\n        foreach (var c in collections)\n        {\n            c.Manage = true;\n        }\n\n        var context = new AuthorizationHandlerContext(\n                new[] { BulkCollectionOperations.Delete },\n                new ClaimsPrincipal(),\n                collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.User)]\n    public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(\n        OrganizationUserType userType,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        ArrangeOrganizationAbility(sutProvider, organization, false, false, false);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n\n        foreach (var c in collections)\n        {\n            c.Manage = true;\n        }\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Delete },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(\n        OrganizationUserType userType,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        ArrangeOrganizationAbility(sutProvider, organization, true, true, false);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n\n        foreach (var c in collections)\n        {\n            c.Manage = true;\n        }\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Delete },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_Failure(\n        OrganizationUserType userType,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        ArrangeOrganizationAbility(sutProvider, organization, true, true, false);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        foreach (var c in collections)\n        {\n            c.Manage = false;\n        }\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Delete },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_Failure(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        ArrangeOrganizationAbility(sutProvider, organization, true, true);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        foreach (var c in collections)\n        {\n            c.Manage = true;\n        }\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Delete },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_Failure(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<CollectionDetails> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        ArrangeOrganizationAbility(sutProvider, organization, true, true, false);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        foreach (var c in collections)\n        {\n            c.Manage = true;\n        }\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Delete },\n            new ClaimsPrincipal(),\n            collections);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, CollectionCustomization]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task CanDeleteAsync_WhenMissingPermissions_NoSuccess(\n        OrganizationUserType userType,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = false,\n            DeleteAnyCollection = false,\n            ManageGroups = false,\n            ManageUsers = false\n        };\n\n        ArrangeOrganizationAbility(sutProvider, organization, true, true);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Delete },\n            new ClaimsPrincipal(),\n            collections);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CanDeleteAsync_WhenMissingOrgAccess_NoSuccess(\n        Guid userId,\n        ICollection<Collection> collections,\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider)\n    {\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Delete },\n            new ClaimsPrincipal(),\n            collections\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task HandleRequirementAsync_MissingUserId_Failure(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections)\n    {\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Create },\n            new ClaimsPrincipal(),\n            collections\n        );\n\n        // Simulate missing user id\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns((Guid?)null);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.True(context.HasFailed);\n        sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs();\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task HandleRequirementAsync_TargetCollectionsMultipleOrgs_Exception(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        IList<Collection> collections)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        // Simulate a collection in a different organization\n        collections.First().OrganizationId = Guid.NewGuid();\n\n        var context = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Create },\n            new ClaimsPrincipal(),\n            collections\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.HandleAsync(context));\n        Assert.Equal(\"Requested collections must belong to the same organization.\", exception.Message);\n        sutProvider.GetDependency<ICurrentContext>().DidNotReceiveWithAnyArgs().GetOrganization(default);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task HandleRequirementAsync_Provider_Success(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        ICollection<Collection> collections)\n    {\n        var actingUserId = Guid.NewGuid();\n        var orgId = collections.First().OrganizationId;\n\n        var organizationAbilities = new Dictionary<Guid, OrganizationAbility>\n        {\n            { collections.First().OrganizationId,\n                new OrganizationAbility\n                {\n                    LimitCollectionCreation = true,\n                    LimitCollectionDeletion = true,\n                    AllowAdminAccessToAllCollectionItems = true\n                }\n            }\n        };\n\n        var operationsToTest = new[]\n        {\n            BulkCollectionOperations.Create,\n            BulkCollectionOperations.Read,\n            BulkCollectionOperations.ReadAccess,\n            BulkCollectionOperations.Update,\n            BulkCollectionOperations.ModifyUserAccess,\n            BulkCollectionOperations.ModifyGroupAccess,\n            BulkCollectionOperations.Delete,\n        };\n\n        foreach (var op in operationsToTest)\n        {\n            sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n            sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgId).Returns((CurrentContextOrganization)null);\n            sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()\n                .Returns(organizationAbilities);\n            sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);\n\n            var context = new AuthorizationHandlerContext(\n                new[] { op },\n                new ClaimsPrincipal(),\n                collections\n            );\n\n            await sutProvider.Sut.HandleAsync(context);\n\n            Assert.True(context.HasSucceeded);\n            await sutProvider.GetDependency<ICurrentContext>().Received().ProviderUserForOrgAsync(orgId);\n\n            // Recreate the SUT to reset the mocks/dependencies between tests\n            sutProvider.Recreate();\n        }\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task CachesCollectionsWithCanManagePermissions(\n        SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n        CollectionDetails collection1, CollectionDetails collection2,\n        CurrentContextOrganization organization, Guid actingUserId)\n    {\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByUserIdAsync(actingUserId)\n            .Returns(new List<CollectionDetails>() { collection1, collection2 });\n\n        var context1 = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Update },\n            new ClaimsPrincipal(),\n            collection1);\n\n        await sutProvider.Sut.HandleAsync(context1);\n\n        var context2 = new AuthorizationHandlerContext(\n            new[] { BulkCollectionOperations.Update },\n            new ClaimsPrincipal(),\n            collection2);\n\n        await sutProvider.Sut.HandleAsync(context2);\n\n        // Expect: only calls the database once\n        await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(Arg.Any<Guid>());\n    }\n\n    private static void ArrangeOrganizationAbility(\n         SutProvider<BulkCollectionAuthorizationHandler> sutProvider,\n         CurrentContextOrganization organization,\n         bool limitCollectionCreation,\n         bool limitCollectionDeletion,\n         bool allowAdminAccessToAllCollectionItems = true)\n    {\n        var organizationAbility = new OrganizationAbility();\n        organizationAbility.Id = organization.Id;\n\n        organizationAbility.LimitCollectionCreation = limitCollectionCreation;\n        organizationAbility.LimitCollectionDeletion = limitCollectionDeletion;\n\n        organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;\n\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)\n            .Returns(organizationAbility);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Vault/AuthorizationHandlers/CollectionAuthorizationHandlerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Api.Vault.AuthorizationHandlers.Collections;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Api.Test.Vault.AuthorizationHandlers;\n\n[SutProviderCustomize]\npublic class CollectionAuthorizationHandlerTests\n{\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanReadAllAsync_WhenAdminOrOwner_Success(\n        OrganizationUserType userType,\n        Guid userId, SutProvider<CollectionAuthorizationHandler> sutProvider,\n        CurrentContextOrganization organization)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAll(organization.Id) },\n            new ClaimsPrincipal(),\n            null);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CanReadAllAsync_WhenProviderUser_Success(\n        Guid userId,\n        SutProvider<CollectionAuthorizationHandler> sutProvider, CurrentContextOrganization organization)\n    {\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAll(organization.Id) },\n            new ClaimsPrincipal(),\n            null);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId\n            .Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>()\n            .ProviderUserForOrgAsync(organization.Id)\n            .Returns(true);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(true, false, false, false)]\n    [BitAutoData(false, true, false, false)]\n    [BitAutoData(false, false, true, false)]\n    [BitAutoData(false, false, false, true)]\n    public async Task CanReadAllAsync_WhenCustomUserWithRequiredPermissions_Success(\n        bool editAnyCollection, bool deleteAnyCollection, bool accessImportExport, bool manageGroups,\n        SutProvider<CollectionAuthorizationHandler> sutProvider,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = editAnyCollection,\n            DeleteAnyCollection = deleteAnyCollection,\n            AccessImportExport = accessImportExport,\n            ManageGroups = manageGroups\n        };\n\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAll(organization.Id) },\n            new ClaimsPrincipal(),\n            null);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task CanReadAllAsync_WhenMissingPermissions_NoSuccess(\n        OrganizationUserType userType,\n        SutProvider<CollectionAuthorizationHandler> sutProvider,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = false,\n            DeleteAnyCollection = false,\n            AccessImportExport = false\n        };\n\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAll(organization.Id) },\n            new ClaimsPrincipal(),\n            null);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CanReadAllWithAccessAsync_WhenAdminOrOwner_Success(\n        OrganizationUserType userType,\n        Guid userId, SutProvider<CollectionAuthorizationHandler> sutProvider,\n        CurrentContextOrganization organization)\n    {\n        organization.Type = userType;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAllWithAccess(organization.Id) },\n            new ClaimsPrincipal(),\n            null);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CanReadAllWithAccessAsync_WhenProviderUser_Success(\n        Guid userId,\n        SutProvider<CollectionAuthorizationHandler> sutProvider, CurrentContextOrganization organization)\n    {\n        organization.Type = OrganizationUserType.User;\n        organization.Permissions = new Permissions();\n\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAllWithAccess(organization.Id) },\n            new ClaimsPrincipal(),\n            null);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId\n            .Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>()\n            .ProviderUserForOrgAsync(organization.Id)\n            .Returns(true);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(true, false, false)]\n    [BitAutoData(false, true, false)]\n    [BitAutoData(false, false, true)]\n    public async Task CanReadAllWithAccessAsync_WhenCustomUserWithRequiredPermissions_Success(\n        bool editAnyCollection, bool deleteAnyCollection, bool manageUsers,\n        SutProvider<CollectionAuthorizationHandler> sutProvider,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = editAnyCollection,\n            DeleteAnyCollection = deleteAnyCollection,\n            ManageUsers = manageUsers\n        };\n\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAllWithAccess(organization.Id) },\n            new ClaimsPrincipal(),\n            null);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task CanReadAllWithAccessAsync_WhenMissingPermissions_NoSuccess(\n        OrganizationUserType userType,\n        SutProvider<CollectionAuthorizationHandler> sutProvider,\n        CurrentContextOrganization organization)\n    {\n        var actingUserId = Guid.NewGuid();\n\n        organization.Type = userType;\n        organization.Permissions = new Permissions\n        {\n            EditAnyCollection = false,\n            DeleteAnyCollection = false,\n            AccessImportExport = false\n        };\n\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAllWithAccess(organization.Id) },\n            new ClaimsPrincipal(),\n            null);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_WhenMissingOrgAccess_NoSuccess(\n        Guid userId,\n        Guid organizationId,\n        SutProvider<CollectionAuthorizationHandler> sutProvider)\n    {\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAll(organizationId) },\n            new ClaimsPrincipal(),\n            null\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_MissingUserId_Failure(\n        Guid organizationId,\n        SutProvider<CollectionAuthorizationHandler> sutProvider)\n    {\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAll(organizationId) },\n            new ClaimsPrincipal(),\n            null\n        );\n\n        // Simulate missing user id\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns((Guid?)null);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.True(context.HasFailed);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_NoSpecifiedOrgId_Failure(\n        SutProvider<CollectionAuthorizationHandler> sutProvider)\n    {\n        var context = new AuthorizationHandlerContext(\n            new[] { CollectionOperations.ReadAll(default) },\n            new ClaimsPrincipal(),\n            null\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(new Guid());\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasFailed);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Vault/Controllers/CiphersControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing System.Text.Json;\nusing Bit.Api.Auth.Models.Request.Accounts;\nusing Bit.Api.Vault.Controllers;\nusing Bit.Api.Vault.Models;\nusing Bit.Api.Vault.Models.Request;\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Core.Vault.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\nusing CipherType = Bit.Core.Vault.Enums.CipherType;\n\nnamespace Bit.Api.Test.Controllers;\n\n[ControllerCustomize(typeof(CiphersController))]\n[SutProviderCustomize]\npublic class CiphersControllerTests\n{\n    [Theory, BitAutoData]\n    public async Task PutPartialShouldReturnCipherWithGivenFolderAndFavoriteValues(User user, Guid folderId, SutProvider<CiphersController> sutProvider)\n    {\n        var isFavorite = true;\n        var cipherId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        var cipherDetails = new CipherDetails\n        {\n            UserId = user.Id,\n            Favorite = isFavorite,\n            FolderId = folderId,\n            Type = Core.Vault.Enums.CipherType.SecureNote,\n            Data = \"{}\"\n        };\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetByIdAsync(cipherId, user.Id)\n            .Returns(Task.FromResult(cipherDetails));\n\n        var result = await sutProvider.Sut.PutPartial(cipherId, new CipherPartialRequestModel { Favorite = isFavorite, FolderId = folderId.ToString() });\n\n        Assert.Equal(folderId, result.FolderId);\n        Assert.Equal(isFavorite, result.Favorite);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutPartialShouldThrowNotFoundExceptionWhenCipherDoesNotExist(User user, Guid folderId, SutProvider<CiphersController> sutProvider)\n    {\n        var isFavorite = true;\n        var cipherId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId, user.Id).ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.PutPartial(cipherId, new CipherPartialRequestModel { Favorite = isFavorite, FolderId = folderId.ToString() });\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n\n        await sutProvider.GetDependency<ICipherRepository>()\n            .DidNotReceive()\n            .UpdatePartialAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<Guid?>(), Arg.Any<bool>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutCollections_vNextShouldThrowExceptionWhenCipherIsNullOrNoOrgValue(Guid id, CipherCollectionsRequestModel model, User user,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationUser(Guid.NewGuid()).Returns(false);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, user.Id).ReturnsNull();\n\n        var requestAction = async () => await sutProvider.Sut.PutCollections_vNext(id, model);\n\n        await Assert.ThrowsAsync<NotFoundException>(requestAction);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutCollections_vNextShouldSaveUpdatedCipher(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        SetupUserAndOrgMocks(id, userId, sutProvider);\n        var cipherDetails = CreateCipherDetailsMock(id, userId);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails);\n\n        sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(new Dictionary<Guid, OrganizationAbility> { { cipherDetails.OrganizationId.Value, new OrganizationAbility { Id = cipherDetails.OrganizationId.Value } } });\n        var cipherService = sutProvider.GetDependency<ICipherService>();\n\n        await sutProvider.Sut.PutCollections_vNext(id, model);\n\n        await cipherService.ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutCollections_vNextReturnOptionalDetailsCipherUnavailableFalse(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        SetupUserAndOrgMocks(id, userId, sutProvider);\n        var cipherDetails = CreateCipherDetailsMock(id, userId);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails);\n\n        sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(new Dictionary<Guid, OrganizationAbility> { { cipherDetails.OrganizationId.Value, new OrganizationAbility { Id = cipherDetails.OrganizationId.Value } } });\n\n        var result = await sutProvider.Sut.PutCollections_vNext(id, model);\n\n        Assert.IsType<OptionalCipherDetailsResponseModel>(result);\n        Assert.False(result.Unavailable);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutCollections_vNextReturnOptionalDetailsCipherUnavailableTrue(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        SetupUserAndOrgMocks(id, userId, sutProvider);\n        var cipherDetails = CreateCipherDetailsMock(id, userId);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails, [(CipherDetails)null]);\n\n        sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());\n\n        var result = await sutProvider.Sut.PutCollections_vNext(id, model);\n\n        Assert.IsType<OptionalCipherDetailsResponseModel>(result);\n        Assert.True(result.Unavailable);\n    }\n\n    private void SetupUserAndOrgMocks(Guid id, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);\n        sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns(new List<CollectionCipher>());\n    }\n\n    private CipherDetails CreateCipherDetailsMock(Guid id, Guid userId)\n    {\n        return new CipherDetails\n        {\n            Id = id,\n            UserId = userId,\n            OrganizationId = Guid.NewGuid(),\n            Type = CipherType.Login,\n            ViewPassword = true,\n            Data = @\"\n            {\n                \"\"Uris\"\": [\n                    {\n                        \"\"Uri\"\": \"\"https://bitwarden.com\"\"\n                    }\n                ],\n                \"\"Username\"\": \"\"testuser\"\",\n                \"\"Password\"\": \"\"securepassword123\"\"\n            }\"\n        };\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin, true, true)]\n    [BitAutoData(OrganizationUserType.Owner, true, true)]\n    [BitAutoData(OrganizationUserType.Custom, false, true)]\n    [BitAutoData(OrganizationUserType.Custom, true, true)]\n    [BitAutoData(OrganizationUserType.Admin, false, false)]\n    [BitAutoData(OrganizationUserType.Owner, false, false)]\n    [BitAutoData(OrganizationUserType.Custom, false, false)]\n    public async Task CanEditCiphersAsAdminAsync_FlexibleCollections_Success(\n        OrganizationUserType userType, bool allowAdminsAccessToAllItems, bool shouldSucceed,\n        CurrentContextOrganization organization, Guid userId, CipherOrganizationDetails cipherOrgDetails, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.OrganizationId = organization.Id;\n        organization.Type = userType;\n        if (userType == OrganizationUserType.Custom)\n        {\n            // Assume custom users have EditAnyCollections for success case\n            organization.Permissions.EditAnyCollection = shouldSucceed;\n        }\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });\n\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility\n        {\n            Id = organization.Id,\n            AllowAdminAccessToAllCollectionItems = allowAdminsAccessToAllItems\n        });\n\n        if (shouldSucceed)\n        {\n            await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);\n            await sutProvider.GetDependency<ICipherService>().ReceivedWithAnyArgs()\n                .DeleteAsync(default, default);\n        }\n        else\n        {\n            await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id));\n            await sutProvider.GetDependency<ICipherService>().DidNotReceiveWithAnyArgs()\n                .DeleteAsync(default, default);\n        }\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task DeleteAdmin_WithOwnerOrAdmin_WithManagePermission_DeletesCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.UserId = null;\n        cipherOrgDetails.OrganizationId = organization.Id;\n\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails>\n            {\n                new CipherDetails(cipherOrgDetails) { Edit = true, Manage = true }\n            });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);\n\n        await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(\n            Arg.Is<CipherDetails>(c => c.Id == cipherOrgDetails.Id && c.OrganizationId == cipherOrgDetails.OrganizationId),\n            userId,\n            true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task DeleteAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.UserId = null;\n        cipherOrgDetails.OrganizationId = organization.Id;\n\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails>\n            {\n                new CipherDetails(cipherOrgDetails) { Edit = true, Manage = false }\n            });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id));\n\n        await sutProvider.GetDependency<ICipherService>().DidNotReceive().DeleteAsync(Arg.Any<CipherDetails>(), Arg.Any<Guid>(), Arg.Any<bool>());\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task DeleteAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_DeletesCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.OrganizationId = organization.Id;\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)\n            .Returns(new List<CipherOrganizationDetails>\n            {\n                new() { Id = cipherOrgDetails.Id, OrganizationId = cipherOrgDetails.OrganizationId }\n            });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);\n\n        await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(Arg.Is<CipherDetails>(c => c.Id == cipherOrgDetails.Id && c.OrganizationId == cipherOrgDetails.OrganizationId),\n        userId,\n        true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task DeleteAdmin_WithAdminOrOwner_WithAccessToAllCollectionItems_DeletesCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n\n        organization.Type = organizationUserType;\n\n        cipherOrgDetails.OrganizationId = organization.Id;\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility\n        {\n            Id = organization.Id,\n            AllowAdminAccessToAllCollectionItems = true\n        });\n\n        await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);\n\n        await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(\n            Arg.Is<CipherDetails>(c => c.Id == cipherOrgDetails.Id && c.OrganizationId == cipherOrgDetails.OrganizationId),\n            userId,\n            true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteAdmin_WithCustomUser_WithEditAnyCollectionTrue_DeletesCipher(\n        CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.OrganizationId = organization.Id;\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = true;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });\n\n        await sutProvider.Sut.DeleteAdmin(cipherOrgDetails.Id);\n\n        await sutProvider.GetDependency<ICipherService>().Received(1).DeleteAsync(\n            Arg.Is<CipherDetails>(c => c.Id == cipherOrgDetails.Id && c.OrganizationId == cipherOrgDetails.OrganizationId),\n            userId,\n            true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(\n        Cipher cipher, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipher.OrganizationId = organization.Id;\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = false;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteAdmin_WithProviderUser_ThrowsNotFoundException(\n        Cipher cipher, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        cipher.OrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteAdmin(cipher.Id));\n    }\n\n\n\n\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithManagePermission_DeletesCiphers(\n        OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(ciphers.Select(c => new CipherDetails\n            {\n                Id = c.Id,\n                OrganizationId = organization.Id,\n                Edit = true,\n                Manage = true\n            }).ToList());\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await sutProvider.Sut.DeleteManyAdmin(model);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .DeleteManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(\n        OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(ciphers.Select(c => new CipherDetails\n            {\n                Id = c.Id,\n                OrganizationId = organization.Id,\n                Edit = true,\n                Manage = false\n            }).ToList());\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteManyAdmin(model));\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCiphers_DeletesCiphers(\n        OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)\n            .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id, OrganizationId = organization.Id }).ToList());\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await sutProvider.Sut.DeleteManyAdmin(model);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .DeleteManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task DeleteManyAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_DeletesCiphers(\n        OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility\n        {\n            Id = organization.Id,\n            AllowAdminAccessToAllCollectionItems = true\n        });\n\n        await sutProvider.Sut.DeleteManyAdmin(model);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .DeleteManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteManyAdmin_WithCustomUser_WithEditAnyCollectionTrue_DeletesCiphers(\n        CipherBulkDeleteRequestModel model,\n        Guid userId, List<Cipher> ciphers, CurrentContextOrganization organization,\n        SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = true;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);\n\n        await sutProvider.Sut.DeleteManyAdmin(model);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .DeleteManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteManyAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(\n        CipherBulkDeleteRequestModel model,\n        Guid userId, List<Cipher> ciphers, CurrentContextOrganization organization,\n        SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = false;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteManyAdmin(model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteManyAdmin_WithProviderUser_ThrowsNotFoundException(\n        CipherBulkDeleteRequestModel model, SutProvider<CiphersController> sutProvider)\n    {\n        var organizationId = Guid.NewGuid();\n        model.OrganizationId = organizationId.ToString();\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organizationId).Returns(true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteManyAdmin(model));\n    }\n\n\n\n\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.UserId = null;\n        cipherOrgDetails.OrganizationId = organization.Id;\n\n        var cipherDetails = new CipherDetails(cipherOrgDetails);\n        cipherDetails.Edit = true;\n        cipherDetails.Manage = true;\n\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails>\n            {\n                cipherDetails\n            });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);\n\n        await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(\n                Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(\n        OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherDetails.UserId = null;\n        cipherDetails.OrganizationId = organization.Id;\n        cipherDetails.Edit = true;\n        cipherDetails.Manage = false;\n\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails>\n            {\n                cipherDetails\n            });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id));\n\n        await sutProvider.GetDependency<ICipherService>()\n            .DidNotReceiveWithAnyArgs()\n            .SoftDeleteManyAsync(default, default, default, default);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_SoftDeletesCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.OrganizationId = organization.Id;\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)\n            .Returns(new List<CipherOrganizationDetails> { new() { Id = cipherOrgDetails.Id, OrganizationId = organization.Id } });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await sutProvider.Sut.PutDeleteAdmin(cipherOrgDetails.Id);\n\n        await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(\n                Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_SoftDeletesCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.OrganizationId = organization.Id;\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility\n        {\n            Id = organization.Id,\n            AllowAdminAccessToAllCollectionItems = true\n        });\n\n        await sutProvider.Sut.PutDeleteAdmin(cipherOrgDetails.Id);\n\n        await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(\n                Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutDeleteAdmin_WithCustomUser_WithEditAnyCollectionTrue_SoftDeletesCipher(\n        CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.OrganizationId = organization.Id;\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = true;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });\n\n        await sutProvider.Sut.PutDeleteAdmin(cipherOrgDetails.Id);\n\n        await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(\n                Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_WithLimitItemDeletionFalse_SoftDeletesCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.UserId = null;\n        cipherOrgDetails.OrganizationId = organization.Id;\n\n        var cipherDetails = new CipherDetails(cipherOrgDetails);\n        cipherDetails.Edit = true;\n        cipherDetails.Manage = false; // Only Edit permission, not Manage\n\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails> { cipherDetails });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = false\n            });\n\n        await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id);\n\n        await sutProvider.GetDependency<ICipherService>().Received(1).SoftDeleteAsync(\n                Arg.Is<CipherDetails>(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_WithLimitItemDeletionTrue_ThrowsNotFoundException(\n        OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherDetails.UserId = null;\n        cipherDetails.OrganizationId = organization.Id;\n        cipherDetails.Edit = true;\n        cipherDetails.Manage = false; // Only Edit permission, not Manage\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails> { cipherDetails });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutDeleteAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(\n        Cipher cipher, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipher.OrganizationId = organization.Id;\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = false;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipher });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteAdmin(cipher.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutDeleteAdmin_WithProviderUser_ThrowsNotFoundException(\n        Cipher cipher, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        cipher.OrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipher.Id).Returns(cipher);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteAdmin(cipher.Id));\n    }\n\n\n\n\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCiphers(\n        OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(ciphers.Select(c => new CipherDetails\n            {\n                Id = c.Id,\n                OrganizationId = organization.Id,\n                Edit = true,\n                Manage = true\n            }).ToList());\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await sutProvider.Sut.PutDeleteManyAdmin(model);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .SoftDeleteManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(\n        OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(ciphers.Select(c => new CipherDetails\n            {\n                Id = c.Id,\n                OrganizationId = organization.Id,\n                Edit = true,\n                Manage = false\n            }).ToList());\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteManyAdmin(model));\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCiphers_SoftDeletesCiphers(\n        OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)\n            .Returns(ciphers.Select(c => new CipherOrganizationDetails { Id = c.Id, OrganizationId = organization.Id }).ToList());\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await sutProvider.Sut.PutDeleteManyAdmin(model);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .SoftDeleteManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutDeleteManyAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_SoftDeletesCiphers(\n        OrganizationUserType organizationUserType, CipherBulkDeleteRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        // Set organization ID on ciphers to avoid \"Cipher needs to belong to a user or an organization\" error\n        foreach (var cipher in ciphers)\n        {\n            cipher.OrganizationId = organization.Id;\n        }\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility\n        {\n            Id = organization.Id,\n            AllowAdminAccessToAllCollectionItems = true\n        });\n\n        await sutProvider.Sut.PutDeleteManyAdmin(model);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .SoftDeleteManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutDeleteManyAdmin_WithCustomUser_WithEditAnyCollectionTrue_SoftDeletesCiphers(\n        CipherBulkDeleteRequestModel model,\n        Guid userId, List<Cipher> ciphers, CurrentContextOrganization organization,\n        SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = true;\n\n        // Set organization ID on ciphers to avoid \"Cipher needs to belong to a user or an organization\" error\n        foreach (var cipher in ciphers)\n        {\n            cipher.OrganizationId = organization.Id;\n        }\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);\n\n        await sutProvider.Sut.PutDeleteManyAdmin(model);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .SoftDeleteManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutDeleteManyAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(\n        CipherBulkDeleteRequestModel model,\n        Guid userId, List<Cipher> ciphers, CurrentContextOrganization organization,\n        SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id.ToString();\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = false;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteManyAdmin(model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutDeleteManyAdmin_WithProviderUser_ThrowsNotFoundException(\n        CipherBulkDeleteRequestModel model, SutProvider<CiphersController> sutProvider)\n    {\n        var organizationId = Guid.NewGuid();\n        model.OrganizationId = organizationId.ToString();\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(organizationId).Returns(true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutDeleteManyAdmin(model));\n    }\n\n\n\n\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithManagePermission_RestoresCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.UserId = null;\n        cipherOrgDetails.OrganizationId = organization.Id;\n        cipherOrgDetails.Type = CipherType.Login;\n        cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());\n\n        var cipherDetails = new CipherDetails(cipherOrgDetails);\n        cipherDetails.Edit = true;\n        cipherDetails.Manage = true;\n\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails> { cipherDetails });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id);\n\n        Assert.IsType<CipherMiniResponseModel>(result);\n        await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(\n                    (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.UserId = null;\n        cipherOrgDetails.OrganizationId = organization.Id;\n\n        var cipherDetails = new CipherDetails(cipherOrgDetails);\n        cipherDetails.Edit = true;\n        cipherDetails.Manage = false;\n\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails> { cipherDetails });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_RestoresCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.OrganizationId = organization.Id;\n        cipherOrgDetails.Type = CipherType.Login;\n        cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());\n\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)\n            .Returns(new List<CipherOrganizationDetails> { new() { Id = cipherOrgDetails.Id, OrganizationId = organization.Id } });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id);\n\n        Assert.IsType<CipherMiniResponseModel>(result);\n        await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(\n                    (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_RestoresCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.OrganizationId = organization.Id;\n        cipherOrgDetails.Type = CipherType.Login;\n        cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility\n        {\n            Id = organization.Id,\n            AllowAdminAccessToAllCollectionItems = true\n        });\n\n        var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id);\n\n        Assert.IsType<CipherMiniResponseModel>(result);\n        await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(\n                    (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionTrue_RestoresCipher(\n        CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.OrganizationId = organization.Id;\n        cipherOrgDetails.Type = CipherType.Login;\n        cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = true;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherOrgDetails });\n\n        var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id);\n\n        Assert.IsType<CipherMiniResponseModel>(result);\n        await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(\n                    (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_LimitItemDeletionFalse_RestoresCipher(\n        OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherOrgDetails.UserId = null;\n        cipherOrgDetails.OrganizationId = organization.Id;\n        cipherOrgDetails.Type = CipherType.Login;\n        cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData());\n\n        var cipherDetails = new CipherDetails(cipherOrgDetails);\n        cipherDetails.Edit = true;\n        cipherDetails.Manage = false; // Only Edit permission, not Manage\n\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails> { cipherDetails });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = false // Permissive mode - Edit permission should work\n            });\n\n        var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id);\n\n        Assert.IsType<CipherMiniResponseModel>(result);\n        await sutProvider.GetDependency<ICipherService>().Received(1).RestoreAsync(Arg.Is<CipherDetails>(\n                    (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_LimitItemDeletionTrue_ThrowsNotFoundException(\n        OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherDetails.UserId = null;\n        cipherDetails.OrganizationId = organization.Id;\n        cipherDetails.Type = CipherType.Login;\n        cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());\n        cipherDetails.Edit = true;\n        cipherDetails.Manage = false; // Only Edit permission, not Manage\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(new List<CipherDetails> { cipherDetails });\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true // Restrictive mode - Edit permission should NOT work\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(\n        CipherDetails cipherDetails, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        cipherDetails.OrganizationId = organization.Id;\n        cipherDetails.Type = CipherType.Login;\n        cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData());\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = false;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(new List<Cipher> { cipherDetails });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutRestoreAdmin_WithProviderUser_ThrowsNotFoundException(\n        CipherDetails cipherDetails, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        cipherDetails.OrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true);\n        sutProvider.GetDependency<ICipherRepository>().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id));\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithManagePermission_RestoresCiphers(\n        OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id;\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(ciphers.Select(c => new CipherDetails\n            {\n                Id = c.Id,\n                OrganizationId = organization.Id,\n                Edit = true,\n                Manage = true\n            }).ToList());\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails\n        {\n            Id = c.Id,\n            OrganizationId = organization.Id,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new CipherLoginData())\n        }).ToList();\n\n        sutProvider.GetDependency<ICipherService>()\n            .RestoreManyAsync(\n                Arg.Is<HashSet<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),\n                userId, organization.Id, true)\n            .Returns(cipherOrgDetails);\n\n        var result = await sutProvider.Sut.PutRestoreManyAdmin(model);\n\n        Assert.Equal(ciphers.Count, result.Data.Count());\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .RestoreManyAsync(\n                Arg.Is<HashSet<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException(\n        OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id;\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId)\n            .Returns(ciphers.Select(c => new CipherDetails\n            {\n                Id = c.Id,\n                OrganizationId = organization.Id,\n                Edit = true,\n                Manage = false,\n                Type = CipherType.Login,\n                Data = JsonSerializer.Serialize(new CipherLoginData())\n            }).ToList());\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreManyAdmin(model));\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCiphers_RestoresCiphers(\n        OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId,\n        List<Cipher> ciphers, CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id;\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails\n        {\n            Id = c.Id,\n            OrganizationId = organization.Id,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new CipherLoginData())\n        }).ToList();\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id)\n            .Returns(cipherOrgDetails);\n        sutProvider.GetDependency<ICipherService>()\n            .RestoreManyAsync(Arg.Is<HashSet<Guid>>(ids =>\n                ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()),\n                userId, organization.Id, true)\n            .Returns(cipherOrgDetails);\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility\n            {\n                Id = organization.Id,\n                LimitItemDeletion = true\n            });\n\n        var result = await sutProvider.Sut.PutRestoreManyAdmin(model);\n\n        Assert.NotNull(result);\n        Assert.Equal(model.Ids.Count(), result.Data.Count());\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .RestoreManyAsync(\n                Arg.Is<HashSet<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task PutRestoreManyAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_RestoresCiphers(\n        OrganizationUserType organizationUserType, CipherBulkRestoreRequestModel model, Guid userId, List<Cipher> ciphers,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id;\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = organizationUserType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility\n        {\n            Id = organization.Id,\n            AllowAdminAccessToAllCollectionItems = true\n        });\n\n        var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails\n        {\n            Id = c.Id,\n            OrganizationId = organization.Id,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new CipherLoginData())\n        }).ToList();\n\n        sutProvider.GetDependency<ICipherService>()\n            .RestoreManyAsync(Arg.Any<HashSet<Guid>>(), userId, organization.Id, true)\n            .Returns(cipherOrgDetails);\n\n        var result = await sutProvider.Sut.PutRestoreManyAdmin(model);\n\n        Assert.NotNull(result);\n        Assert.Equal(ciphers.Count, result.Data.Count());\n        await sutProvider.GetDependency<ICipherService>().Received(1)\n            .RestoreManyAsync(\n                Arg.Is<HashSet<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutRestoreManyAdmin_WithCustomUser_WithEditAnyCollectionTrue_RestoresCiphers(\n        CipherBulkRestoreRequestModel model,\n        Guid userId, List<Cipher> ciphers, CurrentContextOrganization organization,\n        SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id;\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = true;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);\n\n        var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails\n        {\n            Id = c.Id,\n            OrganizationId = organization.Id,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new CipherLoginData())\n        }).ToList();\n\n        sutProvider.GetDependency<ICipherService>()\n            .RestoreManyAsync(\n                Arg.Is<HashSet<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),\n                userId, organization.Id, true)\n            .Returns(cipherOrgDetails);\n\n        var result = await sutProvider.Sut.PutRestoreManyAdmin(model);\n\n        Assert.NotNull(result);\n        Assert.Equal(ciphers.Count, result.Data.Count());\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .RestoreManyAsync(\n                Arg.Is<HashSet<Guid>>(ids =>\n                    ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()),\n                userId, organization.Id, true);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutRestoreManyAdmin_WithCustomUser_WithEditAnyCollectionFalse_ThrowsNotFoundException(\n        CipherBulkRestoreRequestModel model,\n        Guid userId, List<Cipher> ciphers, CurrentContextOrganization organization,\n        SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = organization.Id;\n        model.Ids = ciphers.Select(c => c.Id.ToString()).ToList();\n        organization.Type = OrganizationUserType.Custom;\n        organization.Permissions.EditAnyCollection = false;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id).Returns(ciphers);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PutRestoreManyAdmin(model));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutRestoreManyAdmin_WithProviderUser_ThrowsNotFoundException(\n        CipherBulkRestoreRequestModel model, SutProvider<CiphersController> sutProvider)\n    {\n        model.OrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .ProviderUserForOrgAsync(new Guid(model.OrganizationId.ToString()))\n            .Returns(Task.FromResult(true));\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.PutRestoreManyAdmin(model)\n        );\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PutShareMany_ShouldShareCiphersAndReturnRevisionDateMap(\n        User user,\n        Guid organizationId,\n        Guid userId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        var oldDate1 = DateTime.UtcNow.AddDays(-1);\n        var oldDate2 = DateTime.UtcNow.AddDays(-2);\n        var detail1 = new CipherDetails\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            OrganizationId = organizationId,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new CipherLoginData()),\n            RevisionDate = oldDate1\n        };\n        var detail2 = new CipherDetails\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            OrganizationId = organizationId,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new CipherLoginData()),\n            RevisionDate = oldDate2\n        };\n        var preloadedDetails = new List<CipherDetails> { detail1, detail2 };\n\n        var newDate1 = oldDate1.AddMinutes(5);\n        var newDate2 = oldDate2.AddMinutes(5);\n        var updatedCipher1 = new CipherDetails { Id = detail1.Id, RevisionDate = newDate1, Type = detail1.Type, Data = detail1.Data };\n        var updatedCipher2 = new CipherDetails { Id = detail2.Id, RevisionDate = newDate2, Type = detail2.Type, Data = detail2.Data };\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationUser(organizationId)\n            .Returns(Task.FromResult(true));\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(Task.FromResult(user));\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(default!)\n            .ReturnsForAnyArgs(userId);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId, withOrganizations: false)\n            .Returns(Task.FromResult((ICollection<CipherDetails>)preloadedDetails));\n\n        sutProvider.GetDependency<ICipherService>()\n            .ShareManyAsync(\n                Arg.Any<IEnumerable<(CipherDetails, DateTime?)>>(),\n                organizationId,\n                Arg.Any<IEnumerable<Guid>>(),\n                userId\n            )\n            .Returns(Task.FromResult<IEnumerable<CipherDetails>>(new[] { updatedCipher1, updatedCipher2 }));\n\n        var cipherRequests = preloadedDetails.Select(d =>\n        {\n            var m = new CipherWithIdRequestModel\n            {\n                Id = d.Id,\n                OrganizationId = d.OrganizationId!.Value.ToString(),\n                LastKnownRevisionDate = d.RevisionDate,\n                Type = d.Type,\n            };\n\n            if (d.Type == CipherType.Login)\n            {\n                m.Login = new CipherLoginModel\n                {\n                    Username = \"\",\n                    Password = \"\",\n                    Uris = [],\n                };\n                m.Name = \"\";\n                m.Notes = \"\";\n                m.Fields = Array.Empty<CipherFieldModel>();\n                m.PasswordHistory = Array.Empty<CipherPasswordHistoryModel>();\n            }\n\n            // similar for SecureNote, Card, etc., if you ever hit those branches\n            return m;\n        }).ToList();\n\n        var model = new CipherBulkShareRequestModel\n        {\n            Ciphers = cipherRequests,\n            CollectionIds = new[] { Guid.NewGuid().ToString() }\n        };\n\n        var result = await sutProvider.Sut.PutShareMany(model);\n\n        Assert.Equal(2, result.Data.Count());\n        var revisionDates = result.Data.Select(x => x.RevisionDate).ToList();\n        Assert.Contains(newDate1, revisionDates);\n        Assert.Contains(newDate2, revisionDates);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .ShareManyAsync(\n                Arg.Is<IEnumerable<(CipherDetails, DateTime?)>>(list =>\n                    list.Select(x => x.Item1.Id).OrderBy(id => id)\n                        .SequenceEqual(new[] { detail1.Id, detail2.Id }.OrderBy(id => id))\n                ),\n                organizationId,\n                Arg.Any<IEnumerable<Guid>>(),\n                userId\n            );\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutShareMany_OrganizationUserFalse_ThrowsNotFound(\n        CipherBulkShareRequestModel model,\n        SutProvider<CiphersController> sut)\n    {\n        model.Ciphers = new[] {\n          new CipherWithIdRequestModel { Id = Guid.NewGuid(), OrganizationId = Guid.NewGuid().ToString() }\n        };\n        sut.GetDependency<ICurrentContext>()\n            .OrganizationUser(Arg.Any<Guid>())\n            .Returns(Task.FromResult(false));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sut.Sut.PutShareMany(model));\n    }\n    [Theory, BitAutoData]\n    public async Task PutShareMany_CipherNotOwned_ThrowsNotFoundException(\n        Guid organizationId,\n        Guid userId,\n        CipherWithIdRequestModel request,\n        SutProvider<CiphersController> sutProvider)\n    {\n        request.EncryptedFor = userId;\n        var model = new CipherBulkShareRequestModel\n        {\n            Ciphers = new[] { request },\n            CollectionIds = new[] { Guid.NewGuid().ToString() }\n        };\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationUser(organizationId)\n            .Returns(Task.FromResult(true));\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(default)\n            .ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId, withOrganizations: false)\n            .Returns(Task.FromResult((ICollection<CipherDetails>)new List<CipherDetails>()));\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.PutShareMany(model)\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutShareMany_EncryptedForWrongUser_ThrowsNotFoundException(\n        Guid organizationId,\n        Guid userId,\n        CipherWithIdRequestModel request,\n        SutProvider<CiphersController> sutProvider)\n    {\n        request.EncryptedFor = Guid.NewGuid(); // not equal to userId\n        var model = new CipherBulkShareRequestModel\n        {\n            Ciphers = new[] { request },\n            CollectionIds = new[] { Guid.NewGuid().ToString() }\n        };\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationUser(organizationId)\n            .Returns(Task.FromResult(true));\n        sutProvider.GetDependency<IUserService>()\n            .GetProperUserId(default)\n            .ReturnsForAnyArgs(userId);\n\n        var existing = new CipherDetails { Id = request.Id.Value };\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetManyByUserIdAsync(userId, withOrganizations: false)\n            .Returns(Task.FromResult((ICollection<CipherDetails>)(new[] { existing })));\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.PutShareMany(model)\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostPurge_WhenUserNotFound_ThrowsUnauthorizedAccessException(\n        SecretVerificationRequestModel model,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns((User)null);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostPurge(model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostPurge_WhenUserVerificationFails_ThrowsBadRequestException(\n        User user,\n        SecretVerificationRequestModel model,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n        sutProvider.GetDependency<IUserService>()\n            .VerifySecretAsync(user, model.Secret)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostPurge(model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostPurge_UserPurge_WithClaimedUser_ThrowsBadRequestException(\n        User user,\n        SecretVerificationRequestModel model,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n        sutProvider.GetDependency<IUserService>()\n            .VerifySecretAsync(user, model.Secret)\n            .Returns(true);\n        sutProvider.GetDependency<IUserService>()\n            .IsClaimedByAnyOrganizationAsync(user.Id)\n            .Returns(true);\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostPurge(model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostPurge_UserPurge_WithUnclaimedUser_Successful(\n        User user,\n        SecretVerificationRequestModel model,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n        sutProvider.GetDependency<IUserService>()\n            .VerifySecretAsync(user, model.Secret)\n            .Returns(true);\n        sutProvider.GetDependency<IUserService>()\n            .IsClaimedByAnyOrganizationAsync(user.Id)\n            .Returns(false);\n\n        await sutProvider.Sut.PostPurge(model);\n\n        await sutProvider.GetDependency<ICipherRepository>()\n            .Received(1)\n            .DeleteByUserIdAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostPurge_OrganizationPurge_WithEditAnyCollectionPermission_Successful(\n        User user,\n        SecretVerificationRequestModel model,\n        Guid organizationId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n        sutProvider.GetDependency<IUserService>()\n            .VerifySecretAsync(user, model.Secret)\n            .Returns(true);\n        sutProvider.GetDependency<IUserService>()\n            .IsClaimedByAnyOrganizationAsync(user.Id)\n            .Returns(true);\n        sutProvider.GetDependency<ICurrentContext>()\n            .EditAnyCollection(organizationId)\n            .Returns(true);\n\n        await sutProvider.Sut.PostPurge(model, organizationId);\n\n        await sutProvider.GetDependency<ICipherService>()\n            .Received(1)\n            .PurgeAsync(organizationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostPurge_OrganizationPurge_WithInsufficientPermissions_ThrowsNotFoundException(\n        User user,\n        Guid organizationId,\n        SecretVerificationRequestModel model,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n        sutProvider.GetDependency<IUserService>()\n            .VerifySecretAsync(user, model.Secret)\n            .Returns(true);\n        sutProvider.GetDependency<IUserService>()\n            .IsClaimedByAnyOrganizationAsync(user.Id)\n            .Returns(false);\n        sutProvider.GetDependency<ICurrentContext>()\n            .EditAnyCollection(organizationId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PostPurge(model, organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutShare_WithNullFolderAndFalseFavorite_UpdatesFieldsCorrectly(\n        Guid cipherId,\n        Guid userId,\n        Guid organizationId,\n        Guid folderId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        var user = new User { Id = userId };\n        var userIdKey = userId.ToString().ToUpperInvariant();\n\n        var existingCipher = new Cipher\n        {\n            Id = cipherId,\n            UserId = userId,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new { Username = \"test\", Password = \"test\" }),\n            Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, folderId.ToString().ToUpperInvariant() } }),\n            Favorites = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, true } })\n        };\n\n        // Clears folder and favorite when sharing\n        var model = new CipherShareRequestModel\n        {\n            Cipher = new CipherRequestModel\n            {\n                Type = CipherType.Login,\n                OrganizationId = organizationId.ToString(),\n                Name = \"SharedCipher\",\n                Data = JsonSerializer.Serialize(new { Username = \"test\", Password = \"test\" }),\n                FolderId = null,\n                Favorite = false,\n                EncryptedFor = userId\n            },\n            CollectionIds = [Guid.NewGuid().ToString()]\n        };\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetByIdAsync(cipherId)\n            .Returns(existingCipher);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationUser(organizationId)\n            .Returns(true);\n\n        var sharedCipher = new CipherDetails\n        {\n            Id = cipherId,\n            OrganizationId = organizationId,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new { Username = \"test\", Password = \"test\" }),\n            FolderId = null,\n            Favorite = false\n        };\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetByIdAsync(cipherId, userId)\n            .Returns(sharedCipher);\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilitiesAsync()\n            .Returns(new Dictionary<Guid, OrganizationAbility>\n            {\n                { organizationId, new OrganizationAbility { Id = organizationId } }\n            });\n\n        var result = await sutProvider.Sut.PutShare(cipherId, model);\n\n        Assert.Null(result.FolderId);\n        Assert.False(result.Favorite);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutShare_WithFolderAndFavoriteSet_AddsUserSpecificFields(\n        Guid cipherId,\n        Guid userId,\n        Guid organizationId,\n        Guid folderId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        var user = new User { Id = userId };\n        var userIdKey = userId.ToString().ToUpperInvariant();\n\n        var existingCipher = new Cipher\n        {\n            Id = cipherId,\n            UserId = userId,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new { Username = \"test\", Password = \"test\" }),\n            Folders = null,\n            Favorites = null\n        };\n\n        // Sets folder and favorite when sharing\n        var model = new CipherShareRequestModel\n        {\n            Cipher = new CipherRequestModel\n            {\n                Type = CipherType.Login,\n                OrganizationId = organizationId.ToString(),\n                Name = \"SharedCipher\",\n                Data = JsonSerializer.Serialize(new { Username = \"test\", Password = \"test\" }),\n                FolderId = folderId.ToString(),\n                Favorite = true,\n                EncryptedFor = userId\n            },\n            CollectionIds = [Guid.NewGuid().ToString()]\n        };\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetByIdAsync(cipherId)\n            .Returns(existingCipher);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationUser(organizationId)\n            .Returns(true);\n\n        var sharedCipher = new CipherDetails\n        {\n            Id = cipherId,\n            OrganizationId = organizationId,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new { Username = \"test\", Password = \"test\" }),\n            Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, folderId.ToString().ToUpperInvariant() } }),\n            Favorites = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, true } }),\n            FolderId = folderId,\n            Favorite = true\n        };\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetByIdAsync(cipherId, userId)\n            .Returns(sharedCipher);\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilitiesAsync()\n            .Returns(new Dictionary<Guid, OrganizationAbility>\n            {\n                { organizationId, new OrganizationAbility { Id = organizationId } }\n            });\n\n        var result = await sutProvider.Sut.PutShare(cipherId, model);\n\n        Assert.Equal(folderId, result.FolderId);\n        Assert.True(result.Favorite);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PutShare_UpdateExistingFolderAndFavorite_UpdatesUserSpecificFields(\n        Guid cipherId,\n        Guid userId,\n        Guid organizationId,\n        Guid oldFolderId,\n        Guid newFolderId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        var user = new User { Id = userId };\n        var userIdKey = userId.ToString().ToUpperInvariant();\n\n        // Existing cipher with old folder and not favorited\n        var existingCipher = new Cipher\n        {\n            Id = cipherId,\n            UserId = userId,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new { Username = \"test\", Password = \"test\" }),\n            Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, oldFolderId.ToString().ToUpperInvariant() } }),\n            Favorites = null\n        };\n\n        var model = new CipherShareRequestModel\n        {\n            Cipher = new CipherRequestModel\n            {\n                Type = CipherType.Login,\n                OrganizationId = organizationId.ToString(),\n                Name = \"SharedCipher\",\n                Data = JsonSerializer.Serialize(new { Username = \"test\", Password = \"test\" }),\n                FolderId = newFolderId.ToString(),  // Update to new folder\n                Favorite = true,  // Add favorite\n                EncryptedFor = userId\n            },\n            CollectionIds = [Guid.NewGuid().ToString()]\n        };\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())\n            .Returns(user);\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetByIdAsync(cipherId)\n            .Returns(existingCipher);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationUser(organizationId)\n            .Returns(true);\n\n        var sharedCipher = new CipherDetails\n        {\n            Id = cipherId,\n            OrganizationId = organizationId,\n            Type = CipherType.Login,\n            Data = JsonSerializer.Serialize(new { Username = \"test\", Password = \"test\" }),\n            Folders = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, newFolderId.ToString().ToUpperInvariant() } }),\n            Favorites = JsonSerializer.Serialize(new Dictionary<string, object> { { userIdKey, true } }),\n            FolderId = newFolderId,\n            Favorite = true\n        };\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetByIdAsync(cipherId, userId)\n            .Returns(sharedCipher);\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilitiesAsync()\n            .Returns(new Dictionary<Guid, OrganizationAbility>\n            {\n                { organizationId, new OrganizationAbility { Id = organizationId } }\n            });\n\n        var result = await sutProvider.Sut.PutShare(cipherId, model);\n\n        Assert.Equal(newFolderId, result.FolderId);\n        Assert.True(result.Favorite);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RenewFileUploadUrl_WithReadOnlyUser_ThrowsBadRequest(\n        Guid cipherId, string attachmentId, Guid userId, Guid organizationId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        var attachmentData = new CipherAttachment.MetaData\n        {\n            Size = 100,\n            FileName = \"test.txt\",\n            Validated = false\n        };\n\n        var cipherDetails = new CipherDetails\n        {\n            Id = cipherId,\n            OrganizationId = organizationId,\n            Type = CipherType.Login,\n            Data = \"{}\",\n            Edit = false\n        };\n        cipherDetails.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>\n        {\n            { attachmentId, attachmentData }\n        });\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId, userId).Returns(cipherDetails);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organizationId).ReturnsNull();\n        sutProvider.GetDependency<ICipherService>()\n            .ValidateCipherEditForAttachmentAsync(cipherDetails, userId, false, attachmentData.Size)\n            .ThrowsAsync(new BadRequestException(\"You do not have permissions to edit this.\"));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RenewFileUploadUrl(cipherId, attachmentId));\n        Assert.Equal(\"You do not have permissions to edit this.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task RenewFileUploadUrl_WithOrgAdmin_Success(\n        OrganizationUserType userType, Guid cipherId, string attachmentId, Guid userId,\n        CurrentContextOrganization organization, SutProvider<CiphersController> sutProvider)\n    {\n        var attachmentData = new CipherAttachment.MetaData\n        {\n            Size = 100,\n            FileName = \"test.txt\",\n            Validated = false\n        };\n\n        var cipherDetails = new CipherDetails\n        {\n            Id = cipherId,\n            OrganizationId = organization.Id,\n            Type = CipherType.Login,\n            Data = \"{}\"\n        };\n        cipherDetails.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>\n        {\n            { attachmentId, attachmentData }\n        });\n\n        organization.Type = userType;\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId, userId).Returns(cipherDetails);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICipherRepository>().GetManyByOrganizationIdAsync(organization.Id)\n            .Returns(new List<Cipher> { cipherDetails });\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility { Id = organization.Id, AllowAdminAccessToAllCollectionItems = true });\n\n        var expectedUrl = \"https://example.com/upload\";\n        sutProvider.GetDependency<IAttachmentStorageService>()\n            .GetAttachmentUploadUrlAsync(cipherDetails, Arg.Any<CipherAttachment.MetaData>())\n            .Returns(expectedUrl);\n\n        var result = await sutProvider.Sut.RenewFileUploadUrl(cipherId, attachmentId);\n\n        Assert.Equal(expectedUrl, result.Url);\n        await sutProvider.GetDependency<ICipherService>().Received(1)\n            .ValidateCipherEditForAttachmentAsync(cipherDetails, userId, true, attachmentData.Size);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RenewFileUploadUrl_WithMissingAttachment_ThrowsNotFoundException(\n        Guid cipherId, string attachmentId, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        var cipherDetails = new CipherDetails\n        {\n            Id = cipherId,\n            Type = CipherType.Login,\n            Data = \"{}\"\n        };\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId, userId).Returns(cipherDetails);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RenewFileUploadUrl(cipherId, attachmentId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RenewFileUploadUrl_WithValidatedAttachment_ThrowsNotFoundException(\n        Guid cipherId, string attachmentId, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        var attachmentData = new CipherAttachment.MetaData\n        {\n            Size = 100,\n            FileName = \"test.txt\",\n            Validated = true\n        };\n\n        var cipherDetails = new CipherDetails\n        {\n            Id = cipherId,\n            Type = CipherType.Login,\n            Data = \"{}\"\n        };\n        cipherDetails.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>\n        {\n            { attachmentId, attachmentData }\n        });\n\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId, userId).Returns(cipherDetails);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RenewFileUploadUrl(cipherId, attachmentId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task PostFileForExistingAttachment_WithInvalidContentType_ThrowsBadRequest(\n        Guid cipherId, string attachmentId, Guid userId, SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);\n\n        var httpContext = new DefaultHttpContext();\n        httpContext.Request.ContentType = \"application/json\";\n        sutProvider.Sut.ControllerContext = new ControllerContext { HttpContext = httpContext };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.PostFileForExistingAttachment(cipherId, attachmentId));\n        Assert.Equal(\"Invalid content.\", exception.Message);\n    }\n    [Theory, BitAutoData]\n    public async Task GetAttachmentData_CipherNotFound_ThrowsNotFoundException(\n        Guid cipherId, string attachmentId, Guid userId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs((Guid?)userId);\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId, userId).ReturnsNull();\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.GetAttachmentData(cipherId, attachmentId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAttachmentData_CipherFound_ReturnsAttachmentResponse(\n        Guid cipherId, string attachmentId, Guid userId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs((Guid?)userId);\n\n        var cipherDetails = new CipherDetails { Id = cipherId, UserId = userId, Type = CipherType.Login, Data = \"{}\" };\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId, userId)\n            .Returns(Task.FromResult(cipherDetails));\n\n        var responseData = new AttachmentResponseData\n        {\n            Id = attachmentId,\n            Url = \"https://example.com/download\",\n            Data = new CipherAttachment.MetaData { FileName = \"test.txt\" },\n            Cipher = cipherDetails,\n        };\n        sutProvider.GetDependency<ICipherService>()\n            .GetAttachmentDownloadDataAsync(cipherDetails, attachmentId)\n            .Returns(Task.FromResult(responseData));\n\n        var result = await sutProvider.Sut.GetAttachmentData(cipherId, attachmentId);\n\n        Assert.NotNull(result);\n        Assert.Equal(attachmentId, result.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DownloadAttachmentAsync_EmptyToken_ThrowsNotFoundException(\n        SutProvider<CiphersController> sutProvider)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DownloadAttachmentAsync(string.Empty));\n    }\n\n    [Theory, BitAutoData]\n    public async Task DownloadAttachmentAsync_InvalidToken_ThrowsNotFoundException(\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IAttachmentStorageService>()\n            .ParseAttachmentDownloadToken(Arg.Any<string>())\n            .Throws(new NotFoundException());\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DownloadAttachmentAsync(\"invalid-token\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task DownloadAttachmentAsync_ValidToken_CipherNotFound_ThrowsNotFoundException(\n        Guid cipherId, string attachmentId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IAttachmentStorageService>()\n            .ParseAttachmentDownloadToken(Arg.Any<string>())\n            .Returns((cipherId, attachmentId));\n\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId).ReturnsNull();\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DownloadAttachmentAsync(\"some-token\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task DownloadAttachmentAsync_ValidToken_NoAttachments_ThrowsNotFoundException(\n        Guid cipherId, string attachmentId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        sutProvider.GetDependency<IAttachmentStorageService>()\n            .ParseAttachmentDownloadToken(Arg.Any<string>())\n            .Returns((cipherId, attachmentId));\n\n        var cipher = new Cipher { Id = cipherId, Attachments = null };\n        sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(cipherId).Returns(cipher);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DownloadAttachmentAsync(\"some-token\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task DownloadAttachmentAsync_ValidToken_ReturnsFile(\n        Guid cipherId, string attachmentId,\n        SutProvider<CiphersController> sutProvider)\n    {\n        var fileName = \"secret-document.txt\";\n        var fileContent = new byte[] { 1, 2, 3 };\n        var stream = new MemoryStream(fileContent);\n\n        var metaData = new CipherAttachment.MetaData\n        {\n            AttachmentId = attachmentId,\n            FileName = fileName,\n            Size = fileContent.Length,\n        };\n\n        var cipher = new Cipher\n        {\n            Id = cipherId,\n            Attachments = JsonSerializer.Serialize(\n                new Dictionary<string, CipherAttachment.MetaData> { { attachmentId, metaData } }),\n        };\n\n        sutProvider.GetDependency<IAttachmentStorageService>()\n            .ParseAttachmentDownloadToken(Arg.Any<string>())\n            .Returns((cipherId, attachmentId));\n\n        sutProvider.GetDependency<ICipherRepository>()\n            .GetByIdAsync(cipherId)\n            .Returns(cipher);\n\n        sutProvider.GetDependency<IAttachmentStorageService>()\n            .GetAttachmentReadStreamAsync(cipher, Arg.Any<CipherAttachment.MetaData>())\n            .Returns(stream);\n\n        var result = await sutProvider.Sut.DownloadAttachmentAsync(\"valid-token\");\n\n        var fileResult = Assert.IsType<FileStreamResult>(result);\n        Assert.Equal(\"application/octet-stream\", fileResult.ContentType);\n        Assert.Equal(fileName, fileResult.FileDownloadName);\n        Assert.Same(stream, fileResult.FileStream);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Vault/Controllers/SyncControllerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing System.Text.Json;\nusing AutoFixture;\nusing Bit.Api.Vault.Controllers;\nusing Bit.Api.Vault.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries.Interfaces;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Api.Test.Controllers;\n\n[ControllerCustomize(typeof(SyncController))]\n[SutProviderCustomize]\npublic class SyncControllerTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Get_ThrowBadRequest_WhenUserNotFound(SutProvider<SyncController> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();\n\n        async Task<SyncResponseModel> GetAction()\n        {\n            return await sutProvider.Sut.Get();\n        }\n\n        await Assert.ThrowsAsync<BadRequestException>((Func<Task<SyncResponseModel>>)GetAction);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Get_Success_AtLeastOneEnabledOrg(User user,\n        List<List<string>> userEquivalentDomains,\n        List<GlobalEquivalentDomainsType> userExcludedGlobalEquivalentDomains,\n        ICollection<OrganizationUserOrganizationDetails> organizationUserDetails,\n        ICollection<ProviderUserProviderDetails> providerUserDetails,\n        IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,\n        ICollection<Folder> folders,\n        ICollection<CipherDetails> ciphers,\n        ICollection<Send> sends,\n        ICollection<Policy> policies,\n        ICollection<CollectionDetails> collections,\n        SutProvider<SyncController> sutProvider)\n    {\n        // Get dependencies\n        var userService = sutProvider.GetDependency<IUserService>();\n        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        var folderRepository = sutProvider.GetDependency<IFolderRepository>();\n        var cipherRepository = sutProvider.GetDependency<ICipherRepository>();\n        var sendRepository = sutProvider.GetDependency<ISendRepository>();\n        var policyRepository = sutProvider.GetDependency<IPolicyRepository>();\n        var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();\n        var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();\n        var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();\n\n        // Adjust random data to match required formats / test intentions\n        user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);\n        user.ExcludedGlobalEquivalentDomains = JsonSerializer.Serialize(userExcludedGlobalEquivalentDomains);\n\n        // At least 1 org needs to be enabled to fully test\n        if (!organizationUserDetails.Any(o => o.Enabled))\n        {\n            // We need at least 1 enabled org\n            if (organizationUserDetails.Count > 0)\n            {\n                organizationUserDetails.First().Enabled = true;\n            }\n            else\n            {\n                // create an enabled org\n                var enabledOrg = new Fixture().Create<OrganizationUserOrganizationDetails>();\n                enabledOrg.Enabled = true;\n                organizationUserDetails.Add((enabledOrg));\n            }\n        }\n\n        // Setup returns\n        userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);\n        userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData\n        {\n            PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),\n            SignatureKeyPairData = null,\n        });\n\n        organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);\n\n        providerUserRepository\n            .GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);\n\n        providerUserRepository\n            .GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)\n            .Returns(providerUserOrganizationDetails);\n\n        folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);\n        cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers);\n\n        sendRepository\n            .GetManyByUserIdAsync(user.Id).Returns(sends);\n\n        policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies);\n\n        // Returns for methods only called if we have enabled orgs\n        collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections);\n        collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List<CollectionCipher>());\n        // Back to standard test setup\n        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);\n        userService.HasPremiumFromOrganization(user).Returns(false);\n\n        // Execute GET\n        var result = await sutProvider.Sut.Get();\n\n        // Asserts\n        // Assert that methods are called\n        var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);\n        await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository,\n            cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs);\n\n        Assert.IsType<SyncResponseModel>(result);\n\n        // Collections should not be empty when at least 1 org is enabled\n        Assert.NotEmpty(result.Collections);\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task Get_Success_AllDisabledOrgs(User user,\n        List<List<string>> userEquivalentDomains,\n        List<GlobalEquivalentDomainsType> userExcludedGlobalEquivalentDomains,\n        ICollection<OrganizationUserOrganizationDetails> organizationUserDetails,\n        ICollection<ProviderUserProviderDetails> providerUserDetails,\n        IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,\n        ICollection<Folder> folders,\n        ICollection<CipherDetails> ciphers,\n        ICollection<Send> sends,\n        ICollection<Policy> policies,\n        SutProvider<SyncController> sutProvider)\n    {\n        // Get dependencies\n        var userService = sutProvider.GetDependency<IUserService>();\n        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        var folderRepository = sutProvider.GetDependency<IFolderRepository>();\n        var cipherRepository = sutProvider.GetDependency<ICipherRepository>();\n        var sendRepository = sutProvider.GetDependency<ISendRepository>();\n        var policyRepository = sutProvider.GetDependency<IPolicyRepository>();\n        var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();\n        var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();\n        var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();\n\n        // Adjust random data to match required formats / test intentions\n        user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);\n        user.ExcludedGlobalEquivalentDomains = JsonSerializer.Serialize(userExcludedGlobalEquivalentDomains);\n\n        // All orgs disabled\n        if (organizationUserDetails.Count > 0)\n        {\n            foreach (var orgUserDetails in organizationUserDetails)\n            {\n                orgUserDetails.Enabled = false;\n            }\n        }\n        else\n        {\n            var disabledOrg = new Fixture().Create<OrganizationUserOrganizationDetails>();\n            disabledOrg.Enabled = false;\n            organizationUserDetails.Add((disabledOrg));\n        }\n\n\n        // Setup returns\n        userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);\n        userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData\n        {\n            PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),\n            SignatureKeyPairData = null,\n        });\n\n        organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);\n\n        providerUserRepository\n            .GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);\n\n        providerUserRepository\n            .GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)\n            .Returns(providerUserOrganizationDetails);\n\n        folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);\n        cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers);\n\n        sendRepository\n            .GetManyByUserIdAsync(user.Id).Returns(sends);\n\n        policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies);\n\n        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);\n        userService.HasPremiumFromOrganization(user).Returns(false);\n\n        // Execute GET\n        var result = await sutProvider.Sut.Get();\n\n\n        // Asserts\n        // Assert that methods are called\n\n        var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);\n        await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository,\n            cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs);\n\n        Assert.IsType<SyncResponseModel>(result);\n\n        // Collections should be empty when all standard orgs are disabled.\n        Assert.Empty(result.Collections);\n    }\n\n\n    // Test where provider org has specific plan type and assert plan type comes out on SyncResponseModel class on ProfileResponseModel\n    [Theory]\n    [BitAutoData]\n    public async Task Get_ProviderPlanTypeProperlyPopulated(User user,\n        List<List<string>> userEquivalentDomains,\n        List<GlobalEquivalentDomainsType> userExcludedGlobalEquivalentDomains,\n        ICollection<OrganizationUserOrganizationDetails> organizationUserDetails,\n        ICollection<ProviderUserProviderDetails> providerUserDetails,\n        IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,\n        ICollection<Folder> folders,\n        ICollection<CipherDetails> ciphers,\n        ICollection<Send> sends,\n        ICollection<Policy> policies,\n        ICollection<CollectionDetails> collections,\n        SutProvider<SyncController> sutProvider)\n    {\n        // Get dependencies\n        var userService = sutProvider.GetDependency<IUserService>();\n        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();\n        var folderRepository = sutProvider.GetDependency<IFolderRepository>();\n        var cipherRepository = sutProvider.GetDependency<ICipherRepository>();\n        var sendRepository = sutProvider.GetDependency<ISendRepository>();\n        var policyRepository = sutProvider.GetDependency<IPolicyRepository>();\n        var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();\n        var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();\n        var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();\n\n        // Adjust random data to match required formats / test intentions\n        user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);\n        user.ExcludedGlobalEquivalentDomains = JsonSerializer.Serialize(userExcludedGlobalEquivalentDomains);\n\n\n        // Setup returns\n        userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);\n\n        organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);\n\n        providerUserRepository\n            .GetManyDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed).Returns(providerUserDetails);\n\n        foreach (var p in providerUserOrganizationDetails)\n        {\n            p.SsoConfig = null;\n        }\n        providerUserRepository\n            .GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed)\n            .Returns(providerUserOrganizationDetails);\n\n        folderRepository.GetManyByUserIdAsync(user.Id).Returns(folders);\n        cipherRepository.GetManyByUserIdAsync(user.Id).Returns(ciphers);\n\n        sendRepository\n            .GetManyByUserIdAsync(user.Id).Returns(sends);\n\n        policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies);\n\n        // Returns for methods only called if we have enabled orgs\n        collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections);\n        collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List<CollectionCipher>());\n        // Back to standard test setup\n        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);\n        userService.HasPremiumFromOrganization(user).Returns(false);\n\n        userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData\n        {\n            PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),\n            SignatureKeyPairData = null,\n        });\n\n        // Execute GET\n        var result = await sutProvider.Sut.Get();\n\n        // Asserts\n        // Assert that methods are called\n\n        var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);\n        await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository,\n            cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs);\n\n        Assert.IsType<SyncResponseModel>(result);\n\n        // Look up ProviderOrg output and compare to ProviderOrg method inputs to ensure\n        // product type is set correctly.\n        foreach (var profProviderOrg in result.Profile.ProviderOrganizations)\n        {\n            var matchedProviderUserOrgDetails =\n                providerUserOrganizationDetails.FirstOrDefault(p => p.OrganizationId == profProviderOrg.Id);\n\n            if (matchedProviderUserOrgDetails != null)\n            {\n                var providerOrgProductType = MockPlans.Get(matchedProviderUserOrgDetails.PlanType).ProductTier;\n                Assert.Equal(providerOrgProductType, profProviderOrg.ProductTierType);\n            }\n        }\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull(\n        User user, SutProvider<SyncController> sutProvider)\n    {\n        user.EquivalentDomains = null;\n        user.ExcludedGlobalEquivalentDomains = null;\n\n        user.MasterPassword = null;\n\n        var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();\n        userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData\n        {\n            PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),\n            SignatureKeyPairData = null,\n        });\n\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);\n\n        var result = await sutProvider.Sut.Get();\n\n        Assert.Null(result.UserDecryption.MasterPasswordUnlock);\n    }\n\n    [Theory]\n    [BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]\n    [BitAutoData(KdfType.Argon2id, 11, 128, 5)]\n    public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull(\n        KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism,\n        User user, SutProvider<SyncController> sutProvider)\n    {\n        user.EquivalentDomains = null;\n        user.ExcludedGlobalEquivalentDomains = null;\n\n        user.Key = \"test-key\";\n        user.MasterPassword = \"test-master-password\";\n        user.Kdf = kdfType;\n        user.KdfIterations = kdfIterations;\n        user.KdfMemory = kdfMemory;\n        user.KdfParallelism = kdfParallelism;\n\n        var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();\n        userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData\n        {\n            PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),\n            SignatureKeyPairData = null,\n        });\n\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);\n\n        var result = await sutProvider.Sut.Get();\n\n        Assert.NotNull(result.UserDecryption.MasterPasswordUnlock);\n        Assert.NotNull(result.UserDecryption.MasterPasswordUnlock.Kdf);\n        Assert.Equal(kdfType, result.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);\n        Assert.Equal(kdfIterations, result.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);\n        Assert.Equal(kdfMemory, result.UserDecryption.MasterPasswordUnlock.Kdf.Memory);\n        Assert.Equal(kdfParallelism, result.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);\n        Assert.Equal(user.Key, result.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);\n        Assert.Equal(user.GetMasterPasswordSalt(), result.UserDecryption.MasterPasswordUnlock.Salt);\n    }\n\n    private async Task AssertMethodsCalledAsync(IUserService userService,\n        ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,\n        IOrganizationUserRepository organizationUserRepository,\n        IProviderUserRepository providerUserRepository, IFolderRepository folderRepository,\n        ICipherRepository cipherRepository, ISendRepository sendRepository,\n        ICollectionRepository collectionRepository,\n        ICollectionCipherRepository collectionCipherRepository,\n        bool hasEnabledOrgs)\n    {\n        await userService.ReceivedWithAnyArgs(1).GetUserByPrincipalAsync(default);\n        await organizationUserRepository.ReceivedWithAnyArgs(1)\n            .GetManyDetailsByUserAsync(default);\n        await providerUserRepository.ReceivedWithAnyArgs(1)\n            .GetManyDetailsByUserAsync(default);\n        await providerUserRepository.ReceivedWithAnyArgs(1)\n            .GetManyOrganizationDetailsByUserAsync(default);\n\n        await folderRepository.ReceivedWithAnyArgs(1)\n            .GetManyByUserIdAsync(default);\n\n        await cipherRepository.ReceivedWithAnyArgs(1)\n            .GetManyByUserIdAsync(default);\n\n        await sendRepository.ReceivedWithAnyArgs(1)\n            .GetManyByUserIdAsync(default);\n\n        // These two are only called when at least 1 enabled org.\n        if (hasEnabledOrgs)\n        {\n            await collectionRepository.ReceivedWithAnyArgs(1)\n                .GetManyByUserIdAsync(default);\n            await collectionCipherRepository.ReceivedWithAnyArgs(1)\n                .GetManyByUserIdAsync(default);\n        }\n        else\n        {\n            // all disabled orgs\n            await collectionRepository.ReceivedWithAnyArgs(0)\n                .GetManyByUserIdAsync(default);\n            await collectionCipherRepository.ReceivedWithAnyArgs(0)\n                .GetManyByUserIdAsync(default);\n        }\n\n        await twoFactorIsEnabledQuery.ReceivedWithAnyArgs(1)\n            .TwoFactorIsEnabledAsync(default(ITwoFactorProvidersUser));\n        await userService.ReceivedWithAnyArgs(1)\n            .HasPremiumFromOrganization(default);\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Vault/Models/CipherFieldModelTests.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing Bit.Api.Vault.Models;\nusing Bit.Core.Vault.Enums;\nusing Xunit;\n\nnamespace Bit.Api.Test.Vault.Models;\n\npublic class CipherFieldModelTests\n{\n    /// <summary>\n    /// Tests that plain text in the Name field is rejected by validation.\n    /// This is a regression test for the DoS vulnerability where a user could\n    /// submit plain text instead of encrypted data, causing decryption failures\n    /// that broke the vault for all organization members.\n    /// </summary>\n    [Theory]\n    [InlineData(\"Test\")] // Plain text - should be rejected\n    [InlineData(\"Hello World\")] // Plain text - should be rejected\n    [InlineData(\"\")] // Empty string - should be rejected\n    [InlineData(\"not-encrypted-at-all\")] // Plain text - should be rejected\n    [InlineData(\"invalid|format\")] // Invalid format - should be rejected\n    public void Validate_PlainTextName_ReturnsValidationError(string plainTextName)\n    {\n        var model = new CipherFieldModel\n        {\n            Type = FieldType.Text,\n            Name = plainTextName,\n            Value = \"2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\" // Valid encrypted value\n        };\n\n        var validationResults = new List<ValidationResult>();\n        var validationContext = new ValidationContext(model);\n        var isValid = Validator.TryValidateObject(model, validationContext, validationResults, validateAllProperties: true);\n\n        Assert.False(isValid, $\"Plain text '{plainTextName}' should have been rejected by validation\");\n        Assert.Contains(validationResults, r => r.MemberNames.Contains(nameof(CipherFieldModel.Name)));\n    }\n\n    /// <summary>\n    /// Tests that plain text in the Value field is rejected by validation.\n    /// </summary>\n    [Theory]\n    [InlineData(\"Test\")] // Plain text - should be rejected\n    [InlineData(\"SecretPassword123\")] // Plain text - should be rejected\n    [InlineData(\"\")] // Empty string - should be rejected\n    public void Validate_PlainTextValue_ReturnsValidationError(string plainTextValue)\n    {\n        var model = new CipherFieldModel\n        {\n            Type = FieldType.Text,\n            Name = \"2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\", // Valid encrypted name\n            Value = plainTextValue\n        };\n\n        var validationResults = new List<ValidationResult>();\n        var validationContext = new ValidationContext(model);\n        var isValid = Validator.TryValidateObject(model, validationContext, validationResults, validateAllProperties: true);\n\n        Assert.False(isValid, $\"Plain text value '{plainTextValue}' should have been rejected by validation\");\n        Assert.Contains(validationResults, r => r.MemberNames.Contains(nameof(CipherFieldModel.Value)));\n    }\n\n    /// <summary>\n    /// Tests that properly encrypted strings in Name and Value pass validation.\n    /// </summary>\n    [Theory]\n    [InlineData(\"2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // AesCbc256_HmacSha256_B64\n    [InlineData(\"0.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // AesCbc256_B64\n    [InlineData(\"aXY=|Y3Q=|cnNhQ3Q=\")] // Legacy format without header\n    public void Validate_EncryptedStrings_PassesValidation(string encryptedString)\n    {\n        var model = new CipherFieldModel\n        {\n            Type = FieldType.Text,\n            Name = encryptedString,\n            Value = encryptedString\n        };\n\n        var validationResults = new List<ValidationResult>();\n        var validationContext = new ValidationContext(model);\n        var isValid = Validator.TryValidateObject(model, validationContext, validationResults, validateAllProperties: true);\n\n        Assert.True(isValid, $\"Encrypted string '{encryptedString}' should have passed validation. Errors: {string.Join(\", \", validationResults.Select(r => r.ErrorMessage))}\");\n    }\n\n    /// <summary>\n    /// Tests that null values are allowed (fields are optional).\n    /// </summary>\n    [Fact]\n    public void Validate_NullNameAndValue_PassesValidation()\n    {\n        var model = new CipherFieldModel\n        {\n            Type = FieldType.Text,\n            Name = null,\n            Value = null\n        };\n\n        var validationResults = new List<ValidationResult>();\n        var validationContext = new ValidationContext(model);\n        var isValid = Validator.TryValidateObject(model, validationContext, validationResults, validateAllProperties: true);\n\n        Assert.True(isValid, $\"Null values should be allowed. Errors: {string.Join(\", \", validationResults.Select(r => r.ErrorMessage))}\");\n    }\n}\n"
  },
  {
    "path": "test/Api.Test/Vault/Models/Response/SyncResponseModelTests.cs",
    "content": "﻿using Bit.Api.Vault.Models.Response;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Api.Test.Vault.Models.Response;\n\npublic class SyncResponseModelTests\n{\n    private const string _mockEncryptedKey1 = \"2.key1==|data1==|hmac1==\";\n    private const string _mockEncryptedKey2 = \"2.key2==|data2==|hmac2==\";\n    private const string _mockEncryptedKey3 = \"2.key3==|data3==|hmac3==\";\n\n    private static SyncResponseModel CreateSyncResponseModel(\n        User user,\n        IEnumerable<WebAuthnCredential>? webAuthnCredentials = null)\n    {\n        return new SyncResponseModel(\n            new GlobalSettings(),\n            user,\n            new UserAccountKeysData\n            {\n                PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(\"private\", \"public\", null)\n            },\n            false,\n            false,\n            new Dictionary<Guid, OrganizationAbility>(),\n            new List<Guid>(),\n            new List<OrganizationUserOrganizationDetails>(),\n            new List<ProviderUserProviderDetails>(),\n            new List<ProviderUserOrganizationDetails>(),\n            new List<Folder>(),\n            new List<CollectionDetails>(),\n            new List<CipherDetails>(),\n            new Dictionary<Guid, IGrouping<Guid, CollectionCipher>>(),\n            true, // excludeDomains: true to avoid JSON deserialization issues in tests\n            new List<Policy>(),\n            new List<Send>(),\n            webAuthnCredentials ?? new List<WebAuthnCredential>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_UserWithMasterPassword_SetsMasterPasswordUnlock(User user)\n    {\n        // Arrange\n        user.MasterPassword = \"hashed-password\";\n        user.Key = _mockEncryptedKey1;\n        user.MasterPasswordSalt = null;\n        user.Kdf = KdfType.Argon2id;\n        user.KdfIterations = 3;\n        user.KdfMemory = 64;\n        user.KdfParallelism = 4;\n\n        // Act\n        var result = CreateSyncResponseModel(user);\n\n        // Assert\n        Assert.NotNull(result.UserDecryption);\n        Assert.NotNull(result.UserDecryption.MasterPasswordUnlock);\n        Assert.Equal(_mockEncryptedKey1, result.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);\n        Assert.Equal(user.GetMasterPasswordSalt(), result.UserDecryption.MasterPasswordUnlock.Salt);\n        Assert.NotNull(result.UserDecryption.MasterPasswordUnlock.Kdf);\n        Assert.Equal(KdfType.Argon2id, result.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);\n        Assert.Equal(3, result.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);\n        Assert.Equal(64, result.UserDecryption.MasterPasswordUnlock.Kdf.Memory);\n        Assert.Equal(4, result.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_UserWithExplicitSalt_UsesMasterPasswordSalt(User user)\n    {\n        // Arrange\n        user.MasterPassword = \"hashed-password\";\n        user.Key = _mockEncryptedKey1;\n        user.MasterPasswordSalt = \"explicit-salt-value\";\n        user.Kdf = KdfType.Argon2id;\n        user.KdfIterations = 3;\n        user.KdfMemory = 64;\n        user.KdfParallelism = 4;\n\n        // Act\n        var result = CreateSyncResponseModel(user);\n\n        // Assert\n        Assert.NotNull(result.UserDecryption?.MasterPasswordUnlock);\n        Assert.Equal(\"explicit-salt-value\", result.UserDecryption.MasterPasswordUnlock.Salt);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_UserWithoutMasterPassword_MasterPasswordUnlockIsNull(User user)\n    {\n        // Arrange\n        user.MasterPassword = null;\n\n        // Act\n        var result = CreateSyncResponseModel(user);\n\n        // Assert\n        Assert.NotNull(result.UserDecryption);\n        Assert.Null(result.UserDecryption.MasterPasswordUnlock);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithEnabledWebAuthnPrfCredentials_SetsWebAuthnPrfOptions(\n        User user,\n        WebAuthnCredential credential)\n    {\n        // Arrange\n        credential.SupportsPrf = true;\n        credential.EncryptedPrivateKey = _mockEncryptedKey1;\n        credential.EncryptedUserKey = _mockEncryptedKey2;\n        credential.EncryptedPublicKey = _mockEncryptedKey3;\n\n        // Act\n        var result = CreateSyncResponseModel(user, new List<WebAuthnCredential> { credential });\n\n        // Assert\n        Assert.NotNull(result.UserDecryption);\n        Assert.NotNull(result.UserDecryption.WebAuthnPrfOptions);\n        Assert.Single(result.UserDecryption.WebAuthnPrfOptions);\n        var option = result.UserDecryption.WebAuthnPrfOptions[0];\n        Assert.Equal(_mockEncryptedKey1, option.EncryptedPrivateKey);\n        Assert.Equal(_mockEncryptedKey2, option.EncryptedUserKey);\n        Assert.Equal(credential.CredentialId, option.CredentialId);\n        Assert.Empty(option.Transports);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithoutEnabledWebAuthnPrfCredentials_WebAuthnPrfOptionsIsNull(User user)\n    {\n        // Act\n        var result = CreateSyncResponseModel(user);\n\n        // Assert\n        Assert.NotNull(result.UserDecryption);\n        Assert.Null(result.UserDecryption.WebAuthnPrfOptions);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_UserWithV2UpgradeToken_SetsV2UpgradeToken(User user)\n    {\n        // Arrange\n        var tokenData = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedKey1,\n            WrappedUserKey2 = _mockEncryptedKey2\n        };\n        user.V2UpgradeToken = tokenData.ToJson();\n\n        // Act\n        var result = CreateSyncResponseModel(user);\n\n        // Assert\n        Assert.NotNull(result.UserDecryption);\n        Assert.NotNull(result.UserDecryption.V2UpgradeToken);\n        Assert.Equal(_mockEncryptedKey1, result.UserDecryption.V2UpgradeToken.WrappedUserKey1);\n        Assert.Equal(_mockEncryptedKey2, result.UserDecryption.V2UpgradeToken.WrappedUserKey2);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_UserWithoutV2UpgradeToken_V2UpgradeTokenIsNull(User user)\n    {\n        // Arrange\n        user.V2UpgradeToken = null;\n\n        // Act\n        var result = CreateSyncResponseModel(user);\n\n        // Assert\n        Assert.NotNull(result.UserDecryption);\n        Assert.Null(result.UserDecryption.V2UpgradeToken);\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Billing.Test.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <IsPackable>false</IsPackable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"Neovolve.Logging.Xunit\" Version=\"6.3.0\" />\n    <PackageReference Include=\"RichardSzalay.MockHttp\" Version=\"7.0.0\" />\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"AutoFixture.Xunit2\" Version=\"$(AutoFixtureXUnit2Version)\" />\n    <PackageReference Include=\"NSubstitute\" Version=\"$(NSubstituteVersion)\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Billing\\Billing.csproj\" />\n    <ProjectReference Include=\"..\\Common\\Common.csproj\" />\n    <ProjectReference Include=\"..\\Core.Test\\Core.Test.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <EmbeddedResource Include=\"Resources\\IPN\\echeck-payment.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </EmbeddedResource>\n    <EmbeddedResource Include=\"Resources\\IPN\\non-usd-payment.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </EmbeddedResource>\n    <EmbeddedResource Include=\"Resources\\IPN\\refund-missing-parent-transaction.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </EmbeddedResource>\n    <EmbeddedResource Include=\"Resources\\IPN\\successful-payment.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </EmbeddedResource>\n    <EmbeddedResource Include=\"Resources\\IPN\\successful-payment-org-credit.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </EmbeddedResource>\n    <EmbeddedResource Include=\"Resources\\IPN\\successful-payment-user-credit.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </EmbeddedResource>\n    <EmbeddedResource Include=\"Resources\\IPN\\successful-refund.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </EmbeddedResource>\n    <EmbeddedResource Include=\"Resources\\IPN\\transaction-missing-entity-ids.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </EmbeddedResource>\n    <EmbeddedResource Include=\"Resources\\IPN\\unsupported-transaction-type.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </EmbeddedResource>\n    <None Remove=\"Resources\\Events\\invoice.finalized.json\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "test/Billing.Test/Controllers/BitPayControllerTests.cs",
    "content": "﻿using Bit.Billing.Controllers;\nusing Bit.Billing.Models;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Payment.Clients;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing BitPayLight.Models.Invoice;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\nusing Transaction = Bit.Core.Entities.Transaction;\n\nnamespace Bit.Billing.Test.Controllers;\n\nusing static BitPayConstants;\n\npublic class BitPayControllerTests\n{\n    private readonly GlobalSettings _globalSettings = new();\n    private readonly IBitPayClient _bitPayClient = Substitute.For<IBitPayClient>();\n    private readonly ITransactionRepository _transactionRepository = Substitute.For<ITransactionRepository>();\n    private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();\n    private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();\n    private readonly IProviderRepository _providerRepository = Substitute.For<IProviderRepository>();\n    private readonly IMailService _mailService = Substitute.For<IMailService>();\n    private readonly IStripePaymentService _paymentService = Substitute.For<IStripePaymentService>();\n\n    private readonly IPremiumUserBillingService _premiumUserBillingService =\n        Substitute.For<IPremiumUserBillingService>();\n\n    private const string _validWebhookKey = \"valid-webhook-key\";\n    private const string _invalidWebhookKey = \"invalid-webhook-key\";\n\n    public BitPayControllerTests()\n    {\n        var bitPaySettings = new GlobalSettings.BitPaySettings { WebhookKey = _validWebhookKey };\n        _globalSettings.BitPay = bitPaySettings;\n    }\n\n    private BitPayController CreateController() => new(\n        _globalSettings,\n        _bitPayClient,\n        _transactionRepository,\n        _organizationRepository,\n        _userRepository,\n        _providerRepository,\n        _mailService,\n        _paymentService,\n        Substitute.For<ILogger<BitPayController>>(),\n        _premiumUserBillingService);\n\n    [Fact]\n    public async Task PostIpn_InvalidKey_BadRequest()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n\n        var result = await controller.PostIpn(eventModel, _invalidWebhookKey);\n\n        var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);\n        Assert.Equal(\"Invalid key\", badRequestResult.Value);\n    }\n\n    [Fact]\n    public async Task PostIpn_NullKey_ThrowsException()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n\n        await Assert.ThrowsAsync<ArgumentNullException>(() => controller.PostIpn(eventModel, null!));\n    }\n\n    [Fact]\n    public async Task PostIpn_EmptyKey_BadRequest()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n\n        var result = await controller.PostIpn(eventModel, string.Empty);\n\n        var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);\n        Assert.Equal(\"Invalid key\", badRequestResult.Value);\n    }\n\n    [Fact]\n    public async Task PostIpn_NonUsdCurrency_BadRequest()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var invoice = CreateValidInvoice(currency: \"EUR\");\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);\n        Assert.Equal(\"Cannot process non-USD payments\", badRequestResult.Value);\n    }\n\n    [Fact]\n    public async Task PostIpn_NullPosData_BadRequest()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var invoice = CreateValidInvoice(posData: null!);\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);\n        Assert.Equal(\"Invalid POS data\", badRequestResult.Value);\n    }\n\n    [Fact]\n    public async Task PostIpn_EmptyPosData_BadRequest()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var invoice = CreateValidInvoice(posData: \"\");\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);\n        Assert.Equal(\"Invalid POS data\", badRequestResult.Value);\n    }\n\n    [Fact]\n    public async Task PostIpn_PosDataWithoutAccountCredit_BadRequest()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var invoice = CreateValidInvoice(posData: \"organizationId:550e8400-e29b-41d4-a716-446655440000\");\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);\n        Assert.Equal(\"Invalid POS data\", badRequestResult.Value);\n    }\n\n    [Fact]\n    public async Task PostIpn_PosDataWithoutValidId_BadRequest()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var invoice = CreateValidInvoice(posData: PosDataKeys.AccountCredit);\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);\n        Assert.Equal(\"Invalid POS data\", badRequestResult.Value);\n    }\n\n    [Fact]\n    public async Task PostIpn_IncompleteInvoice_Ok()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var invoice = CreateValidInvoice(status: \"paid\");\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        Assert.Equal(\"Waiting for invoice to be completed\", okResult.Value);\n    }\n\n    [Fact]\n    public async Task PostIpn_ExistingTransaction_Ok()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var invoice = CreateValidInvoice();\n        var existingTransaction = new Transaction { GatewayId = invoice.Id };\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n        _transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id).Returns(existingTransaction);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        var okResult = Assert.IsType<OkObjectResult>(result);\n        Assert.Equal(\"Invoice already processed\", okResult.Value);\n    }\n\n    [Fact]\n    public async Task PostIpn_ValidOrganizationTransaction_Success()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var organizationId = Guid.NewGuid();\n        var invoice = CreateValidInvoice(posData: $\"organizationId:{organizationId},{PosDataKeys.AccountCredit}\");\n        var organization = new Organization { Id = organizationId, BillingEmail = \"billing@example.com\" };\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n        _transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id).Returns((Transaction)null);\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n        _paymentService.CreditAccountAsync(organization, Arg.Any<decimal>()).Returns(true);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        Assert.IsType<OkResult>(result);\n        await _transactionRepository.Received(1).CreateAsync(Arg.Is<Transaction>(t =>\n            t.OrganizationId == organizationId &&\n            t.Type == TransactionType.Credit &&\n            t.Gateway == GatewayType.BitPay &&\n            t.PaymentMethodType == PaymentMethodType.BitPay));\n        await _organizationRepository.Received(1).ReplaceAsync(organization);\n        await _mailService.Received(1).SendAddedCreditAsync(\"billing@example.com\", 100.00m);\n    }\n\n    [Fact]\n    public async Task PostIpn_ValidUserTransaction_Success()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var userId = Guid.NewGuid();\n        var invoice = CreateValidInvoice(posData: $\"userId:{userId},{PosDataKeys.AccountCredit}\");\n        var user = new User { Id = userId, Email = \"user@example.com\" };\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n        _transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id).Returns((Transaction)null);\n        _userRepository.GetByIdAsync(userId).Returns(user);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        Assert.IsType<OkResult>(result);\n        await _transactionRepository.Received(1).CreateAsync(Arg.Is<Transaction>(t =>\n            t.UserId == userId &&\n            t.Type == TransactionType.Credit &&\n            t.Gateway == GatewayType.BitPay &&\n            t.PaymentMethodType == PaymentMethodType.BitPay));\n        await _premiumUserBillingService.Received(1).Credit(user, 100.00m);\n        await _mailService.Received(1).SendAddedCreditAsync(\"user@example.com\", 100.00m);\n    }\n\n    [Fact]\n    public async Task PostIpn_ValidProviderTransaction_Success()\n    {\n        var controller = CreateController();\n        var eventModel = CreateValidEventModel();\n        var providerId = Guid.NewGuid();\n        var invoice = CreateValidInvoice(posData: $\"providerId:{providerId},{PosDataKeys.AccountCredit}\");\n        var provider = new Provider { Id = providerId, BillingEmail = \"provider@example.com\" };\n\n        _bitPayClient.GetInvoice(eventModel.Data.Id).Returns(invoice);\n        _transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id).Returns((Transaction)null);\n        _providerRepository.GetByIdAsync(providerId).Returns(Task.FromResult(provider));\n        _paymentService.CreditAccountAsync(provider, Arg.Any<decimal>()).Returns(true);\n\n        var result = await controller.PostIpn(eventModel, _validWebhookKey);\n\n        Assert.IsType<OkResult>(result);\n        await _transactionRepository.Received(1).CreateAsync(Arg.Is<Transaction>(t =>\n            t.ProviderId == providerId &&\n            t.Type == TransactionType.Credit &&\n            t.Gateway == GatewayType.BitPay &&\n            t.PaymentMethodType == PaymentMethodType.BitPay));\n        await _providerRepository.Received(1).ReplaceAsync(provider);\n        await _mailService.Received(1).SendAddedCreditAsync(\"provider@example.com\", 100.00m);\n    }\n\n    [Fact]\n    public void GetIdsFromPosData_ValidOrganizationId_ReturnsCorrectId()\n    {\n        var controller = CreateController();\n        var organizationId = Guid.NewGuid();\n        var invoice = CreateValidInvoice(posData: $\"organizationId:{organizationId},{PosDataKeys.AccountCredit}\");\n\n        var result = controller.GetIdsFromPosData(invoice);\n\n        Assert.Equal(organizationId, result.OrganizationId);\n        Assert.Null(result.UserId);\n        Assert.Null(result.ProviderId);\n    }\n\n    [Fact]\n    public void GetIdsFromPosData_ValidUserId_ReturnsCorrectId()\n    {\n        var controller = CreateController();\n        var userId = Guid.NewGuid();\n        var invoice = CreateValidInvoice(posData: $\"userId:{userId},{PosDataKeys.AccountCredit}\");\n\n        var result = controller.GetIdsFromPosData(invoice);\n\n        Assert.Null(result.OrganizationId);\n        Assert.Equal(userId, result.UserId);\n        Assert.Null(result.ProviderId);\n    }\n\n    [Fact]\n    public void GetIdsFromPosData_ValidProviderId_ReturnsCorrectId()\n    {\n        var controller = CreateController();\n        var providerId = Guid.NewGuid();\n        var invoice = CreateValidInvoice(posData: $\"providerId:{providerId},{PosDataKeys.AccountCredit}\");\n\n        var result = controller.GetIdsFromPosData(invoice);\n\n        Assert.Null(result.OrganizationId);\n        Assert.Null(result.UserId);\n        Assert.Equal(providerId, result.ProviderId);\n    }\n\n    [Fact]\n    public void GetIdsFromPosData_InvalidGuid_ReturnsNull()\n    {\n        var controller = CreateController();\n        var invoice = CreateValidInvoice(posData: \"organizationId:invalid-guid,{PosDataKeys.AccountCredit}\");\n\n        var result = controller.GetIdsFromPosData(invoice);\n\n        Assert.Null(result.OrganizationId);\n        Assert.Null(result.UserId);\n        Assert.Null(result.ProviderId);\n    }\n\n    [Fact]\n    public void GetIdsFromPosData_NullPosData_ReturnsNull()\n    {\n        var controller = CreateController();\n        var invoice = CreateValidInvoice(posData: null!);\n\n        var result = controller.GetIdsFromPosData(invoice);\n\n        Assert.Null(result.OrganizationId);\n        Assert.Null(result.UserId);\n        Assert.Null(result.ProviderId);\n    }\n\n    [Fact]\n    public void GetIdsFromPosData_EmptyPosData_ReturnsNull()\n    {\n        var controller = CreateController();\n        var invoice = CreateValidInvoice(posData: \"\");\n\n        var result = controller.GetIdsFromPosData(invoice);\n\n        Assert.Null(result.OrganizationId);\n        Assert.Null(result.UserId);\n        Assert.Null(result.ProviderId);\n    }\n\n    private static BitPayEventModel CreateValidEventModel(string invoiceId = \"test-invoice-id\")\n    {\n        return new BitPayEventModel\n        {\n            Event = new BitPayEventModel.EventModel { Code = 1005, Name = \"invoice_confirmed\" },\n            Data = new BitPayEventModel.InvoiceDataModel { Id = invoiceId }\n        };\n    }\n\n    private static Invoice CreateValidInvoice(string invoiceId = \"test-invoice-id\", string status = \"complete\",\n        string currency = \"USD\", decimal price = 100.00m,\n        string posData = \"organizationId:550e8400-e29b-41d4-a716-446655440000,accountCredit:1\")\n    {\n        return new Invoice\n        {\n            Id = invoiceId,\n            Status = status,\n            Currency = currency,\n            Price = (double)price,\n            PosData = posData,\n            CurrentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),\n            Transactions =\n            [\n                new InvoiceTransaction\n                {\n                    Type = null,\n                    Confirmations = \"1\",\n                    ReceivedTime = DateTime.UtcNow.ToString(\"O\")\n                }\n            ]\n        };\n    }\n\n}\n"
  },
  {
    "path": "test/Billing.Test/Controllers/PayPalControllerTests.cs",
    "content": "﻿using System.Globalization;\nusing System.Text;\nusing Bit.Billing.Controllers;\nusing Bit.Billing.Test.Utilities;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\nusing Microsoft.AspNetCore.Mvc.Infrastructure;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing Microsoft.Extensions.Primitives;\nusing Neovolve.Logging.Xunit;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\nusing Xunit.Abstractions;\nusing Transaction = Bit.Core.Entities.Transaction;\n\nnamespace Bit.Billing.Test.Controllers;\n\npublic class PayPalControllerTests(ITestOutputHelper testOutputHelper)\n{\n    private readonly IOptions<BillingSettings> _billingSettings = Substitute.For<IOptions<BillingSettings>>();\n    private readonly IMailService _mailService = Substitute.For<IMailService>();\n    private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();\n    private readonly IStripePaymentService _paymentService = Substitute.For<IStripePaymentService>();\n    private readonly ITransactionRepository _transactionRepository = Substitute.For<ITransactionRepository>();\n    private readonly IUserRepository _userRepository = Substitute.For<IUserRepository>();\n    private readonly IProviderRepository _providerRepository = Substitute.For<IProviderRepository>();\n    private readonly IPremiumUserBillingService _premiumUserBillingService = Substitute.For<IPremiumUserBillingService>();\n\n    private const string _defaultWebhookKey = \"webhook-key\";\n\n    [Fact]\n    public async Task PostIpn_NullKey_BadRequest()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        var controller = ConfigureControllerContextWith(logger, null, null);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 400);\n\n        LoggedError(logger, \"PayPal IPN: Key is missing\");\n    }\n\n    [Fact]\n    public async Task PostIpn_IncorrectKey_BadRequest()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal = { WebhookKey = \"INCORRECT\" }\n        });\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, null);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 400);\n\n        LoggedError(logger, \"PayPal IPN: Key is incorrect\");\n    }\n\n    [Fact]\n    public async Task PostIpn_EmptyIPNBody_BadRequest()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal = { WebhookKey = _defaultWebhookKey }\n        });\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, null);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 400);\n\n        LoggedError(logger, \"PayPal IPN: Request body is null or empty\");\n    }\n\n    [Fact]\n    public async Task PostIpn_IPNHasNoEntityId_BadRequest()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal = { WebhookKey = _defaultWebhookKey }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.TransactionMissingEntityIds);\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 400);\n\n        LoggedError(logger, \"PayPal IPN (2PK15573S8089712Y): 'custom' did not contain a User ID or Organization ID or provider ID\");\n    }\n\n    [Fact]\n    public async Task PostIpn_OtherTransactionType_Unprocessed_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal = { WebhookKey = _defaultWebhookKey }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.UnsupportedTransactionType);\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        LoggedWarning(logger, \"PayPal IPN (2PK15573S8089712Y): Transaction type (other) not supported for payments\");\n    }\n\n    [Fact]\n    public async Task PostIpn_MismatchedReceiverID_Unprocessed_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"INCORRECT\"\n            }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment);\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        LoggedWarning(logger, \"PayPal IPN (2PK15573S8089712Y): Receiver ID (NHDYKLQ3L4LWL) does not match Bitwarden business ID (INCORRECT)\");\n    }\n\n    [Fact]\n    public async Task PostIpn_RefundMissingParent_Unprocessed_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.RefundMissingParentTransaction);\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        LoggedWarning(logger, \"PayPal IPN (2PK15573S8089712Y): Parent transaction ID is required for refund\");\n    }\n\n    [Fact]\n    public async Task PostIpn_eCheckPayment_Unprocessed_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.ECheckPayment);\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        LoggedWarning(logger, \"PayPal IPN (2PK15573S8089712Y): Transaction was an eCheck payment\");\n    }\n\n    [Fact]\n    public async Task PostIpn_NonUSD_Unprocessed_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.NonUSDPayment);\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        LoggedWarning(logger, \"PayPal IPN (2PK15573S8089712Y): Transaction was not in USD (CAD)\");\n    }\n\n    [Fact]\n    public async Task PostIpn_Completed_ExistingTransaction_Unprocessed_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment);\n\n        _transactionRepository.GetByGatewayIdAsync(\n            GatewayType.PayPal,\n            \"2PK15573S8089712Y\").Returns(new Transaction());\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        LoggedWarning(logger, \"PayPal IPN (2PK15573S8089712Y): Already processed this completed transaction\");\n    }\n\n    [Fact]\n    public async Task PostIpn_Completed_CreatesTransaction_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment);\n\n        _transactionRepository.GetByGatewayIdAsync(\n            GatewayType.PayPal,\n            \"2PK15573S8089712Y\").ReturnsNull();\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        await _transactionRepository.Received().CreateAsync(Arg.Any<Transaction>());\n\n        await _paymentService.DidNotReceiveWithAnyArgs().CreditAccountAsync(Arg.Any<ISubscriber>(), Arg.Any<decimal>());\n    }\n\n    [Fact]\n    public async Task PostIpn_Completed_CreatesTransaction_CreditsOrganizationAccount_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var organizationId = new Guid(\"ca8c6f2b-2d7b-4639-809f-b0e5013a304e\");\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPaymentForOrganizationCredit);\n\n        _transactionRepository.GetByGatewayIdAsync(\n            GatewayType.PayPal,\n            \"2PK15573S8089712Y\").ReturnsNull();\n\n        const string billingEmail = \"billing@organization.com\";\n\n        var organization = new Organization { BillingEmail = billingEmail };\n\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        _paymentService.CreditAccountAsync(organization, 48M).Returns(true);\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        await _transactionRepository.Received(1).CreateAsync(Arg.Is<Transaction>(transaction =>\n            transaction.GatewayId == \"2PK15573S8089712Y\" &&\n            transaction.OrganizationId == organizationId &&\n            transaction.Amount == 48M));\n\n        await _paymentService.Received(1).CreditAccountAsync(organization, 48M);\n\n        await _organizationRepository.Received(1).ReplaceAsync(organization);\n\n        await _mailService.Received(1).SendAddedCreditAsync(billingEmail, 48M);\n    }\n\n    [Fact]\n    public async Task PostIpn_Completed_CreatesTransaction_CreditsUserAccount_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var userId = new Guid(\"ca8c6f2b-2d7b-4639-809f-b0e5013a304e\");\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPaymentForUserCredit);\n\n        _transactionRepository.GetByGatewayIdAsync(\n            GatewayType.PayPal,\n            \"2PK15573S8089712Y\").ReturnsNull();\n\n        const string billingEmail = \"billing@user.com\";\n\n        var user = new User { Email = billingEmail };\n\n        _userRepository.GetByIdAsync(userId).Returns(user);\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        await _transactionRepository.Received(1).CreateAsync(Arg.Is<Transaction>(transaction =>\n            transaction.GatewayId == \"2PK15573S8089712Y\" &&\n            transaction.UserId == userId &&\n            transaction.Amount == 48M));\n\n        await _premiumUserBillingService.Received(1).Credit(user, 48M);\n\n        await _mailService.Received(1).SendAddedCreditAsync(billingEmail, 48M);\n    }\n\n    [Fact]\n    public async Task PostIpn_Refunded_ExistingTransaction_Unprocessed_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulRefund);\n\n        _transactionRepository.GetByGatewayIdAsync(\n            GatewayType.PayPal,\n            \"2PK15573S8089712Y\").Returns(new Transaction());\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        LoggedWarning(logger, \"PayPal IPN (2PK15573S8089712Y): Already processed this refunded transaction\");\n\n        await _transactionRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<Transaction>());\n\n        await _transactionRepository.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<Transaction>());\n    }\n\n    [Fact]\n    public async Task PostIpn_Refunded_MissingParentTransaction_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulRefund);\n\n        _transactionRepository.GetByGatewayIdAsync(\n            GatewayType.PayPal,\n            \"2PK15573S8089712Y\").ReturnsNull();\n\n        _transactionRepository.GetByGatewayIdAsync(\n            GatewayType.PayPal,\n            \"PARENT\").ReturnsNull();\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        LoggedWarning(logger, \"PayPal IPN (2PK15573S8089712Y): Could not find parent transaction\");\n\n        await _transactionRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<Transaction>());\n\n        await _transactionRepository.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<Transaction>());\n    }\n\n    [Fact]\n    public async Task PostIpn_Refunded_ReplacesParent_CreatesTransaction_Ok()\n    {\n        var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n        _billingSettings.Value.Returns(new BillingSettings\n        {\n            PayPal =\n            {\n                WebhookKey = _defaultWebhookKey,\n                BusinessId = \"NHDYKLQ3L4LWL\"\n            }\n        });\n\n        var organizationId = new Guid(\"ca8c6f2b-2d7b-4639-809f-b0e5013a304e\");\n\n        var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulRefund);\n\n        _transactionRepository.GetByGatewayIdAsync(\n            GatewayType.PayPal,\n            \"2PK15573S8089712Y\").ReturnsNull();\n\n        var parentTransaction = new Transaction\n        {\n            GatewayId = \"PARENT\",\n            Amount = 48M,\n            RefundedAmount = 0,\n            Refunded = false\n        };\n\n        _transactionRepository.GetByGatewayIdAsync(\n            GatewayType.PayPal,\n            \"PARENT\").Returns(parentTransaction);\n\n        var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n        var result = await controller.PostIpn();\n\n        HasStatusCode(result, 200);\n\n        await _transactionRepository.Received(1).ReplaceAsync(Arg.Is<Transaction>(transaction =>\n            transaction.GatewayId == \"PARENT\" &&\n            transaction.RefundedAmount == 48M &&\n            transaction.Refunded == true));\n\n        await _transactionRepository.Received(1).CreateAsync(Arg.Is<Transaction>(transaction =>\n            transaction.GatewayId == \"2PK15573S8089712Y\" &&\n            transaction.Amount == 48M &&\n            transaction.OrganizationId == organizationId &&\n            transaction.Type == TransactionType.Refund));\n    }\n\n    private PayPalController ConfigureControllerContextWith(\n        ILogger<PayPalController> logger,\n        string? webhookKey,\n        string? ipnBody)\n    {\n        var controller = new PayPalController(\n            _billingSettings,\n            logger,\n            _mailService,\n            _organizationRepository,\n            _paymentService,\n            _transactionRepository,\n            _userRepository,\n            _providerRepository,\n            _premiumUserBillingService);\n\n        var httpContext = new DefaultHttpContext();\n\n        if (!string.IsNullOrEmpty(webhookKey))\n        {\n            httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>\n            {\n                { \"key\", new StringValues(webhookKey) }\n            });\n        }\n\n        if (!string.IsNullOrEmpty(ipnBody))\n        {\n            var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(ipnBody));\n\n            httpContext.Request.Body = memoryStream;\n            httpContext.Request.ContentLength = memoryStream.Length;\n        }\n\n        controller.ControllerContext = new ControllerContext\n        {\n            HttpContext = httpContext\n        };\n\n        return controller;\n    }\n\n    private static void HasStatusCode(IActionResult result, int statusCode)\n    {\n        var statusCodeActionResult = (IStatusCodeActionResult)result;\n\n        Assert.Equal(statusCode, statusCodeActionResult.StatusCode);\n    }\n\n    private static void Logged(ICacheLogger<PayPalController> logger, LogLevel logLevel, string message)\n    {\n        Assert.NotNull(logger.Last);\n        Assert.Equal(logLevel, logger.Last!.LogLevel);\n        Assert.Equal(message, logger.Last!.Message);\n    }\n\n    private static void LoggedError(ICacheLogger<PayPalController> logger, string message)\n        => Logged(logger, LogLevel.Error, message);\n\n    private static void LoggedWarning(ICacheLogger<PayPalController> logger, string message)\n        => Logged(logger, LogLevel.Warning, message);\n\n    [Fact]\n    public async Task PostIpn_Completed_CreatesTransaction_WithSwedishCulture_Ok()\n    {\n        // Save current culture\n        var originalCulture = CultureInfo.CurrentCulture;\n        var originalUICulture = CultureInfo.CurrentUICulture;\n\n        try\n        {\n            // Set Swedish culture (uses comma as decimal separator)\n            var swedishCulture = new CultureInfo(\"sv-SE\");\n            CultureInfo.CurrentCulture = swedishCulture;\n            CultureInfo.CurrentUICulture = swedishCulture;\n\n            var logger = testOutputHelper.BuildLoggerFor<PayPalController>();\n\n            _billingSettings.Value.Returns(new BillingSettings\n            {\n                PayPal =\n                {\n                    WebhookKey = _defaultWebhookKey,\n                    BusinessId = \"NHDYKLQ3L4LWL\"\n                }\n            });\n\n            var ipnBody = await PayPalTestIPN.GetAsync(IPNBody.SuccessfulPayment);\n\n            _transactionRepository.GetByGatewayIdAsync(\n                GatewayType.PayPal,\n                \"2PK15573S8089712Y\").ReturnsNull();\n\n            var controller = ConfigureControllerContextWith(logger, _defaultWebhookKey, ipnBody);\n\n            var result = await controller.PostIpn();\n\n            HasStatusCode(result, 200);\n\n            await _transactionRepository.Received().CreateAsync(Arg.Is<Transaction>(transaction =>\n                transaction.Amount == 48M &&\n                transaction.GatewayId == \"2PK15573S8089712Y\"));\n        }\n        finally\n        {\n            // Restore original culture\n            CultureInfo.CurrentCulture = originalCulture;\n            CultureInfo.CurrentUICulture = originalUICulture;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Jobs/ProviderOrganizationDisableJobTests.cs",
    "content": "﻿using Bit.Billing.Jobs;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Quartz;\nusing Xunit;\n\nnamespace Bit.Billing.Test.Jobs;\n\npublic class ProviderOrganizationDisableJobTests\n{\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository;\n    private readonly IOrganizationDisableCommand _organizationDisableCommand;\n    private readonly ILogger<ProviderOrganizationDisableJob> _logger;\n    private readonly ProviderOrganizationDisableJob _sut;\n\n    public ProviderOrganizationDisableJobTests()\n    {\n        _providerOrganizationRepository = Substitute.For<IProviderOrganizationRepository>();\n        _organizationDisableCommand = Substitute.For<IOrganizationDisableCommand>();\n        _logger = Substitute.For<ILogger<ProviderOrganizationDisableJob>>();\n        _sut = new ProviderOrganizationDisableJob(\n            _providerOrganizationRepository,\n            _organizationDisableCommand,\n            _logger);\n    }\n\n    [Fact]\n    public async Task Execute_NoOrganizations_LogsAndReturns()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(providerId, DateTime.UtcNow);\n        _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId)\n            .Returns((ICollection<ProviderOrganizationOrganizationDetails>)null);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _organizationDisableCommand.DidNotReceiveWithAnyArgs().DisableAsync(default, default);\n    }\n\n    [Fact]\n    public async Task Execute_WithOrganizations_DisablesAllOrganizations()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var expirationDate = DateTime.UtcNow.AddDays(30);\n        var org1Id = Guid.NewGuid();\n        var org2Id = Guid.NewGuid();\n        var org3Id = Guid.NewGuid();\n\n        var organizations = new List<ProviderOrganizationOrganizationDetails>\n        {\n            new() { OrganizationId = org1Id },\n            new() { OrganizationId = org2Id },\n            new() { OrganizationId = org3Id }\n        };\n\n        var context = CreateJobExecutionContext(providerId, expirationDate);\n        _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId)\n            .Returns(organizations);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _organizationDisableCommand.Received(1).DisableAsync(org1Id, Arg.Any<DateTime?>());\n        await _organizationDisableCommand.Received(1).DisableAsync(org2Id, Arg.Any<DateTime?>());\n        await _organizationDisableCommand.Received(1).DisableAsync(org3Id, Arg.Any<DateTime?>());\n    }\n\n    [Fact]\n    public async Task Execute_WithExpirationDate_PassesDateToDisableCommand()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var expirationDate = new DateTime(2025, 12, 31, 23, 59, 59);\n        var orgId = Guid.NewGuid();\n\n        var organizations = new List<ProviderOrganizationOrganizationDetails>\n        {\n            new() { OrganizationId = orgId }\n        };\n\n        var context = CreateJobExecutionContext(providerId, expirationDate);\n        _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId)\n            .Returns(organizations);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _organizationDisableCommand.Received(1).DisableAsync(orgId, expirationDate);\n    }\n\n    [Fact]\n    public async Task Execute_WithNullExpirationDate_PassesNullToDisableCommand()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var orgId = Guid.NewGuid();\n\n        var organizations = new List<ProviderOrganizationOrganizationDetails>\n        {\n            new() { OrganizationId = orgId }\n        };\n\n        var context = CreateJobExecutionContext(providerId, null);\n        _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId)\n            .Returns(organizations);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _organizationDisableCommand.Received(1).DisableAsync(orgId, null);\n    }\n\n    [Fact]\n    public async Task Execute_OneOrganizationFails_ContinuesProcessingOthers()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var expirationDate = DateTime.UtcNow.AddDays(30);\n        var org1Id = Guid.NewGuid();\n        var org2Id = Guid.NewGuid();\n        var org3Id = Guid.NewGuid();\n\n        var organizations = new List<ProviderOrganizationOrganizationDetails>\n        {\n            new() { OrganizationId = org1Id },\n            new() { OrganizationId = org2Id },\n            new() { OrganizationId = org3Id }\n        };\n\n        var context = CreateJobExecutionContext(providerId, expirationDate);\n        _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId)\n            .Returns(organizations);\n\n        // Make org2 fail\n        _organizationDisableCommand.DisableAsync(org2Id, Arg.Any<DateTime?>())\n            .Throws(new Exception(\"Database error\"));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert - all three should be attempted\n        await _organizationDisableCommand.Received(1).DisableAsync(org1Id, Arg.Any<DateTime?>());\n        await _organizationDisableCommand.Received(1).DisableAsync(org2Id, Arg.Any<DateTime?>());\n        await _organizationDisableCommand.Received(1).DisableAsync(org3Id, Arg.Any<DateTime?>());\n    }\n\n    [Fact]\n    public async Task Execute_ManyOrganizations_ProcessesWithLimitedConcurrency()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var expirationDate = DateTime.UtcNow.AddDays(30);\n\n        // Create 20 organizations\n        var organizations = Enumerable.Range(1, 20)\n            .Select(_ => new ProviderOrganizationOrganizationDetails { OrganizationId = Guid.NewGuid() })\n            .ToList();\n\n        var context = CreateJobExecutionContext(providerId, expirationDate);\n        _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId)\n            .Returns(organizations);\n\n        var concurrentCalls = 0;\n        var maxConcurrentCalls = 0;\n        var lockObj = new object();\n\n        _organizationDisableCommand.DisableAsync(Arg.Any<Guid>(), Arg.Any<DateTime?>())\n            .Returns(callInfo =>\n            {\n                lock (lockObj)\n                {\n                    concurrentCalls++;\n                    if (concurrentCalls > maxConcurrentCalls)\n                    {\n                        maxConcurrentCalls = concurrentCalls;\n                    }\n                }\n\n                return Task.Delay(50).ContinueWith(_ =>\n                {\n                    lock (lockObj)\n                    {\n                        concurrentCalls--;\n                    }\n                });\n            });\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        Assert.True(maxConcurrentCalls <= 5, $\"Expected max concurrency of 5, but got {maxConcurrentCalls}\");\n        await _organizationDisableCommand.Received(20).DisableAsync(Arg.Any<Guid>(), Arg.Any<DateTime?>());\n    }\n\n    [Fact]\n    public async Task Execute_EmptyOrganizationsList_DoesNotCallDisableCommand()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(providerId, DateTime.UtcNow);\n        _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId)\n            .Returns(new List<ProviderOrganizationOrganizationDetails>());\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _organizationDisableCommand.DidNotReceiveWithAnyArgs().DisableAsync(default, default);\n    }\n\n    private static IJobExecutionContext CreateJobExecutionContext(Guid providerId, DateTime? expirationDate)\n    {\n        var context = Substitute.For<IJobExecutionContext>();\n        var jobDataMap = new JobDataMap\n        {\n            { \"providerId\", providerId.ToString() },\n            { \"expirationDate\", expirationDate?.ToString(\"O\") }\n        };\n        context.MergedJobDataMap.Returns(jobDataMap);\n        return context;\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Jobs/ReconcileAdditionalStorageJobTests.cs",
    "content": "﻿using Bit.Billing.Jobs;\nusing Bit.Billing.Services;\nusing Bit.Core;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Quartz;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Billing.Test.Jobs;\n\npublic class ReconcileAdditionalStorageJobTests\n{\n    private readonly IStripeFacade _stripeFacade;\n    private readonly ILogger<ReconcileAdditionalStorageJob> _logger;\n    private readonly IFeatureService _featureService;\n    private readonly IUserRepository _userRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n    private readonly ReconcileAdditionalStorageJob _sut;\n\n    public ReconcileAdditionalStorageJobTests()\n    {\n        _stripeFacade = Substitute.For<IStripeFacade>();\n        _logger = Substitute.For<ILogger<ReconcileAdditionalStorageJob>>();\n        _featureService = Substitute.For<IFeatureService>();\n        _userRepository = Substitute.For<IUserRepository>();\n        _organizationRepository = Substitute.For<IOrganizationRepository>();\n        _stripeEventUtilityService = Substitute.For<IStripeEventUtilityService>();\n\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, null));\n\n        _sut = new ReconcileAdditionalStorageJob(\n            _stripeFacade,\n            _logger,\n            _featureService,\n            _userRepository,\n            _organizationRepository,\n            _stripeEventUtilityService);\n    }\n\n    #region Feature Flag Tests\n\n    [Fact]\n    public async Task Execute_FeatureFlagDisabled_SkipsProcessing()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob)\n            .Returns(false);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        _stripeFacade.DidNotReceiveWithAnyArgs().ListSubscriptionsAutoPagingAsync();\n    }\n\n    [Fact]\n    public async Task Execute_FeatureFlagEnabled_ProcessesSubscriptions()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob)\n            .Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode)\n            .Returns(false);\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Empty<Subscription>());\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        _stripeFacade.Received(3).ListSubscriptionsAutoPagingAsync(\n            Arg.Is<SubscriptionListOptions>(o => o.Limit == 100));\n    }\n\n    #endregion\n\n    #region Dry Run Mode Tests\n\n    [Fact]\n    public async Task Execute_DryRunMode_DoesNotUpdateSubscriptions()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(false); // Dry run ON\n\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10);\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);\n    }\n\n    [Fact]\n    public async Task Execute_DryRunMode_DoesNotUpdateDatabase()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(false); // Dry run ON\n\n        // Create a personal subscription that would normally trigger a database update\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10);\n        subscription.Metadata = new Dictionary<string, string> { [\"userId\"] = userId.ToString() };\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n\n        // Mock GetIdsFromMetadata to return userId\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert - Verify database repositories are never called\n        await _userRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);\n        await _userRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default!);\n        await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);\n        await _organizationRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default!);\n    }\n\n    [Fact]\n    public async Task Execute_DryRunModeDisabled_UpdatesSubscriptions()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true); // Dry run OFF\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(o => o.Items.Count == 1));\n    }\n\n    [Fact]\n    public async Task Execute_LiveMode_PersonalSubscription_UpdatesUserDatabase()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        // Setup user\n        var userId = Guid.NewGuid();\n        var user = new Bit.Core.Entities.User\n        {\n            Id = userId,\n            Email = \"test@example.com\",\n            GatewaySubscriptionId = \"sub_personal\",\n            MaxStorageGb = 15 // Old value\n        };\n        _userRepository.GetByIdAsync(userId).Returns(user);\n        _userRepository.ReplaceAsync(user).Returns(Task.CompletedTask);\n\n        // Create personal subscription with premium seat + 10 GB storage (will be reduced to 6 GB)\n        var subscription = CreateSubscriptionWithMultipleItems(\"sub_personal\",\n            [(\"premium-annually\", 1L), (\"storage-gb-monthly\", 10L)]);\n        subscription.Metadata = new Dictionary<string, string> { [\"userId\"] = userId.ToString() };\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Mock GetIdsFromMetadata to return userId\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert - Verify Stripe update happened\n        await _stripeFacade.Received(1).UpdateSubscription(\n            \"sub_personal\",\n            Arg.Is<SubscriptionUpdateOptions>(o => o.Items.Count == 1 && o.Items[0].Quantity == 6));\n\n        // Assert - Verify database update with correct MaxStorageGb (5 included + 6 new quantity = 11)\n        await _userRepository.Received(1).GetByIdAsync(userId);\n        await _userRepository.Received(1).ReplaceAsync(user);\n        Assert.Equal((short)11, user.MaxStorageGb);\n    }\n\n    [Fact]\n    public async Task Execute_LiveMode_OrganizationSubscription_UpdatesOrganizationDatabase()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        // Setup organization\n        var organizationId = Guid.NewGuid();\n        var organization = new Bit.Core.AdminConsole.Entities.Organization\n        {\n            Id = organizationId,\n            Name = \"Test Organization\",\n            GatewaySubscriptionId = \"sub_org\",\n            MaxStorageGb = 13 // Old value\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n        _organizationRepository.ReplaceAsync(organization).Returns(Task.CompletedTask);\n\n        // Create organization subscription with org seat plan + 8 GB storage (will be reduced to 4 GB)\n        var subscription = CreateSubscriptionWithMultipleItems(\"sub_org\",\n            [(\"2023-teams-org-seat-annually\", 5L), (\"storage-gb-monthly\", 8L)]);\n        subscription.Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() };\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Mock GetIdsFromMetadata to return organizationId\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(organizationId, null, null));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert - Verify Stripe update happened\n        await _stripeFacade.Received(1).UpdateSubscription(\n            \"sub_org\",\n            Arg.Is<SubscriptionUpdateOptions>(o => o.Items.Count == 1 && o.Items[0].Quantity == 4));\n\n        // Assert - Verify database update with correct MaxStorageGb (5 included + 4 new quantity = 9)\n        await _organizationRepository.Received(1).GetByIdAsync(organizationId);\n        await _organizationRepository.Received(1).ReplaceAsync(organization);\n        Assert.Equal((short)9, organization.MaxStorageGb);\n    }\n\n    [Fact]\n    public async Task Execute_LiveMode_StorageItemDeleted_UpdatesDatabaseWithBaseStorage()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        // Setup user\n        var userId = Guid.NewGuid();\n        var user = new Bit.Core.Entities.User\n        {\n            Id = userId,\n            Email = \"test@example.com\",\n            GatewaySubscriptionId = \"sub_delete\",\n            MaxStorageGb = 8 // Old value\n        };\n        _userRepository.GetByIdAsync(userId).Returns(user);\n        _userRepository.ReplaceAsync(user).Returns(Task.CompletedTask);\n\n        // Create personal subscription with premium seat + 3 GB storage (will be deleted since 3 < 4)\n        var subscription = CreateSubscriptionWithMultipleItems(\"sub_delete\",\n            [(\"premium-annually\", 1L), (\"storage-gb-monthly\", 3L)]);\n        subscription.Metadata = new Dictionary<string, string> { [\"userId\"] = userId.ToString() };\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Mock GetIdsFromMetadata to return userId\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert - Verify Stripe update happened (item deleted)\n        await _stripeFacade.Received(1).UpdateSubscription(\n            \"sub_delete\",\n            Arg.Is<SubscriptionUpdateOptions>(o => o.Items.Count == 1 && o.Items[0].Deleted == true));\n\n        // Assert - Verify database update with base storage only (5 GB)\n        await _userRepository.Received(1).GetByIdAsync(userId);\n        await _userRepository.Received(1).ReplaceAsync(user);\n        Assert.Equal((short)5, user.MaxStorageGb);\n    }\n\n    #endregion\n\n    #region Price ID Processing Tests\n\n    [Fact]\n    public async Task Execute_ProcessesAllThreePriceIds()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(false);\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Empty<Subscription>());\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        _stripeFacade.Received(1).ListSubscriptionsAutoPagingAsync(\n            Arg.Is<SubscriptionListOptions>(o => o.Price == \"storage-gb-monthly\"));\n        _stripeFacade.Received(1).ListSubscriptionsAutoPagingAsync(\n            Arg.Is<SubscriptionListOptions>(o => o.Price == \"storage-gb-annually\"));\n        _stripeFacade.Received(1).ListSubscriptionsAutoPagingAsync(\n            Arg.Is<SubscriptionListOptions>(o => o.Price == \"personal-storage-gb-annually\"));\n    }\n\n    #endregion\n\n    #region Already Processed Tests\n\n    [Fact]\n    public async Task Execute_SubscriptionAlreadyProcessed_SkipsUpdate()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var metadata = new Dictionary<string, string>\n        {\n            [StripeConstants.MetadataKeys.StorageReconciled2025] = DateTime.UtcNow.ToString(\"o\")\n        };\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10, metadata: metadata);\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);\n    }\n\n    [Fact]\n    public async Task Execute_SubscriptionWithInvalidProcessedDate_ProcessesSubscription()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var metadata = new Dictionary<string, string>\n        {\n            [StripeConstants.MetadataKeys.StorageReconciled2025] = \"invalid-date\"\n        };\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10, metadata: metadata);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_123\", Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_SubscriptionWithoutMetadata_ProcessesSubscription()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10, metadata: null);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_123\", Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    #endregion\n\n    #region Quantity Reduction Logic Tests\n\n    [Fact]\n    public async Task Execute_QuantityGreaterThan4_ReducesBy4()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items.Count == 1 &&\n                o.Items[0].Quantity == 6 &&\n                o.Items[0].Deleted != true));\n    }\n\n    [Fact]\n    public async Task Execute_QuantityEquals4_DeletesItem()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 4);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items.Count == 1 &&\n                o.Items[0].Deleted == true));\n    }\n\n    [Fact]\n    public async Task Execute_QuantityLessThan4_DeletesItem()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 2);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items.Count == 1 &&\n                o.Items[0].Deleted == true));\n    }\n\n    #endregion\n\n    #region Update Options Tests\n\n    [Fact]\n    public async Task Execute_UpdateOptions_SetsProrationBehaviorToCreateProrations()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(o => o.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations));\n    }\n\n    [Fact]\n    public async Task Execute_UpdateOptions_SetsReconciledMetadata()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Metadata.ContainsKey(StripeConstants.MetadataKeys.StorageReconciled2025) &&\n                !string.IsNullOrEmpty(o.Metadata[StripeConstants.MetadataKeys.StorageReconciled2025])));\n    }\n\n    #endregion\n\n    #region Subscription Filtering Tests\n\n    [Fact]\n    public async Task Execute_SubscriptionWithNoItems_SkipsUpdate()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = null\n        };\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);\n    }\n\n    [Fact]\n    public async Task Execute_SubscriptionWithDifferentPriceId_SkipsUpdate()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var subscription = CreateSubscription(\"sub_123\", \"different-price-id\", quantity: 10);\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);\n    }\n\n    [Fact]\n    public async Task Execute_NullSubscription_SkipsProcessing()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create<Subscription>(null!));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);\n    }\n\n    #endregion\n\n    #region Multiple Subscriptions Tests\n\n    [Fact]\n    public async Task Execute_MultipleSubscriptions_ProcessesAll()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription1 = CreateSubscription(\"sub_1\", \"storage-gb-monthly\", quantity: 10);\n        var subscription2 = CreateSubscription(\"sub_2\", \"storage-gb-monthly\", quantity: 5);\n        var subscription3 = CreateSubscription(\"sub_3\", \"storage-gb-monthly\", quantity: 3);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription1, subscription2, subscription3));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(callInfo => callInfo.Arg<string>() switch\n            {\n                \"sub_1\" => subscription1,\n                \"sub_2\" => subscription2,\n                \"sub_3\" => subscription3,\n                _ => null\n            });\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_1\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_2\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_3\", Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_MixedSubscriptionsWithProcessed_OnlyProcessesUnprocessed()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var processedMetadata = new Dictionary<string, string>\n        {\n            [StripeConstants.MetadataKeys.StorageReconciled2025] = DateTime.UtcNow.ToString(\"o\")\n        };\n\n        var subscription1 = CreateSubscription(\"sub_1\", \"storage-gb-monthly\", quantity: 10);\n        var subscription2 = CreateSubscription(\"sub_2\", \"storage-gb-monthly\", quantity: 5, metadata: processedMetadata);\n        var subscription3 = CreateSubscription(\"sub_3\", \"storage-gb-monthly\", quantity: 3);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription1, subscription2, subscription3));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(callInfo => callInfo.Arg<string>() switch\n            {\n                \"sub_1\" => subscription1,\n                \"sub_3\" => subscription3,\n                _ => null\n            });\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_1\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.DidNotReceive().UpdateSubscription(\"sub_2\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_3\", Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    #endregion\n\n    #region Error Handling Tests\n\n    [Fact]\n    public async Task Execute_UpdateFails_ContinuesProcessingOthers()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription1 = CreateSubscription(\"sub_1\", \"storage-gb-monthly\", quantity: 10);\n        var subscription2 = CreateSubscription(\"sub_2\", \"storage-gb-monthly\", quantity: 5);\n        var subscription3 = CreateSubscription(\"sub_3\", \"storage-gb-monthly\", quantity: 3);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription1, subscription2, subscription3));\n\n        _stripeFacade.UpdateSubscription(\"sub_1\", Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription1);\n        _stripeFacade.UpdateSubscription(\"sub_2\", Arg.Any<SubscriptionUpdateOptions>())\n            .Throws(new Exception(\"Stripe API error\"));\n        _stripeFacade.UpdateSubscription(\"sub_3\", Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription3);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_1\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_2\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_3\", Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_UpdateFails_LogsError()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10);\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Throws(new Exception(\"Stripe API error\"));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        _logger.Received().Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception, string>>());\n    }\n\n    #endregion\n\n    #region Subscription Status Filtering Tests\n\n    [Fact]\n    public async Task Execute_ActiveStatusSubscription_ProcessesSubscription()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10, status: StripeConstants.SubscriptionStatus.Active);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_123\", Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_TrialingStatusSubscription_ProcessesSubscription()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10, status: StripeConstants.SubscriptionStatus.Trialing);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_123\", Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_PastDueStatusSubscription_ProcessesSubscription()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10, status: StripeConstants.SubscriptionStatus.PastDue);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_123\", Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_CanceledStatusSubscription_SkipsSubscription()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10, status: StripeConstants.SubscriptionStatus.Canceled);\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);\n    }\n\n    [Fact]\n    public async Task Execute_IncompleteStatusSubscription_SkipsSubscription()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", quantity: 10, status: StripeConstants.SubscriptionStatus.Incomplete);\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null!);\n    }\n\n    [Fact]\n    public async Task Execute_MixedSubscriptionStatuses_OnlyProcessesValidStatuses()\n    {\n        // Arrange\n        var context = CreateJobExecutionContext();\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var userId = Guid.NewGuid();\n        var activeSubscription = CreateSubscription(\"sub_active\", \"storage-gb-monthly\", quantity: 10, status: StripeConstants.SubscriptionStatus.Active);\n        var trialingSubscription = CreateSubscription(\"sub_trialing\", \"storage-gb-monthly\", quantity: 8, status: StripeConstants.SubscriptionStatus.Trialing);\n        var pastDueSubscription = CreateSubscription(\"sub_pastdue\", \"storage-gb-monthly\", quantity: 6, status: StripeConstants.SubscriptionStatus.PastDue);\n        var canceledSubscription = CreateSubscription(\"sub_canceled\", \"storage-gb-monthly\", quantity: 5, status: StripeConstants.SubscriptionStatus.Canceled);\n        var incompleteSubscription = CreateSubscription(\"sub_incomplete\", \"storage-gb-monthly\", quantity: 4, status: StripeConstants.SubscriptionStatus.Incomplete);\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(activeSubscription, trialingSubscription, pastDueSubscription, canceledSubscription, incompleteSubscription));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(callInfo => callInfo.Arg<string>() switch\n            {\n                \"sub_active\" => activeSubscription,\n                \"sub_trialing\" => trialingSubscription,\n                \"sub_pastdue\" => pastDueSubscription,\n                _ => null\n            });\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_active\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_trialing\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.Received(1).UpdateSubscription(\"sub_pastdue\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.DidNotReceive().UpdateSubscription(\"sub_canceled\", Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.DidNotReceive().UpdateSubscription(\"sub_incomplete\", Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    #endregion\n\n    #region Cancellation Tests\n\n    [Fact]\n    public async Task Execute_CancellationRequested_LogsWarningAndExits()\n    {\n        // Arrange\n        var cts = new CancellationTokenSource();\n        cts.Cancel(); // Cancel immediately\n        var context = CreateJobExecutionContext(cts.Token);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_EnableReconcileAdditionalStorageJob).Returns(true);\n        _featureService.IsEnabled(FeatureFlagKeys.PM28265_ReconcileAdditionalStorageJobEnableLiveMode).Returns(true);\n\n        var subscription1 = CreateSubscription(\"sub_1\", \"storage-gb-monthly\", quantity: 10);\n\n        _stripeFacade.ListSubscriptionsAutoPagingAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(AsyncEnumerable.Create(subscription1));\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert - Should not process any subscriptions due to immediate cancellation\n        await _stripeFacade.DidNotReceiveWithAnyArgs().UpdateSubscription(null);\n        _logger.Received().Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception, string>>());\n    }\n\n    #endregion\n\n    #region Helper Method Tests\n\n    #region DetermineSubscriptionPlanTier Tests\n\n    [Fact]\n    public void DetermineSubscriptionPlanTier_WithUserId_ReturnsPersonal()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        Guid? organizationId = null;\n\n        // Act\n        var result = _sut.DetermineSubscriptionPlanTier(userId, organizationId);\n\n        // Assert\n        Assert.Equal(ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal, result);\n    }\n\n    [Fact]\n    public void DetermineSubscriptionPlanTier_WithOrganizationId_ReturnsOrganization()\n    {\n        // Arrange\n        Guid? userId = null;\n        var organizationId = Guid.NewGuid();\n\n        // Act\n        var result = _sut.DetermineSubscriptionPlanTier(userId, organizationId);\n\n        // Assert\n        Assert.Equal(ReconcileAdditionalStorageJob.SubscriptionPlanTier.Organization, result);\n    }\n\n    [Fact]\n    public void DetermineSubscriptionPlanTier_WithBothIds_ReturnsPersonal()\n    {\n        // Arrange - Personal takes precedence\n        var userId = Guid.NewGuid();\n        var organizationId = Guid.NewGuid();\n\n        // Act\n        var result = _sut.DetermineSubscriptionPlanTier(userId, organizationId);\n\n        // Assert\n        Assert.Equal(ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal, result);\n    }\n\n    [Fact]\n    public void DetermineSubscriptionPlanTier_WithNoIds_ReturnsUnknown()\n    {\n        // Arrange\n        Guid? userId = null;\n        Guid? organizationId = null;\n\n        // Act\n        var result = _sut.DetermineSubscriptionPlanTier(userId, organizationId);\n\n        // Assert\n        Assert.Equal(ReconcileAdditionalStorageJob.SubscriptionPlanTier.Unknown, result);\n    }\n\n    #endregion\n\n    #region GetCurrentStorageQuantityFromSubscription Tests\n\n    [Theory]\n    [InlineData(\"storage-gb-monthly\", 10L, 10L)]\n    [InlineData(\"storage-gb-annually\", 25L, 25L)]\n    [InlineData(\"personal-storage-gb-annually\", 5L, 5L)]\n    [InlineData(\"storage-gb-monthly\", 0L, 0L)]\n    public void GetCurrentStorageQuantityFromSubscription_WithMatchingPriceId_ReturnsQuantity(\n        string priceId, long quantity, long expectedQuantity)\n    {\n        // Arrange\n        var subscription = CreateSubscription(\"sub_123\", priceId, quantity);\n\n        // Act\n        var result = _sut.GetCurrentStorageQuantityFromSubscription(subscription, priceId);\n\n        // Assert\n        Assert.Equal(expectedQuantity, result);\n    }\n\n    [Fact]\n    public void GetCurrentStorageQuantityFromSubscription_WithNonMatchingPriceId_ReturnsZero()\n    {\n        // Arrange\n        var subscription = CreateSubscription(\"sub_123\", \"storage-gb-monthly\", 10L);\n\n        // Act\n        var result = _sut.GetCurrentStorageQuantityFromSubscription(subscription, \"different-price-id\");\n\n        // Assert\n        Assert.Equal(0, result);\n    }\n\n    [Fact]\n    public void GetCurrentStorageQuantityFromSubscription_WithNullItems_ReturnsZero()\n    {\n        // Arrange\n        var subscription = new Subscription { Id = \"sub_123\", Items = null };\n\n        // Act\n        var result = _sut.GetCurrentStorageQuantityFromSubscription(subscription, \"storage-gb-monthly\");\n\n        // Assert\n        Assert.Equal(0, result);\n    }\n\n    [Fact]\n    public void GetCurrentStorageQuantityFromSubscription_WithEmptyItems_ReturnsZero()\n    {\n        // Arrange\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem> { Data = [] }\n        };\n\n        // Act\n        var result = _sut.GetCurrentStorageQuantityFromSubscription(subscription, \"storage-gb-monthly\");\n\n        // Assert\n        Assert.Equal(0, result);\n    }\n\n    #endregion\n\n    #region CalculateNewMaxStorageGb Tests\n\n    [Theory]\n    [InlineData(10L, 6L, 11)] // 5 included + 6 new quantity\n    [InlineData(15L, 11L, 16)] // 5 included + 11 new quantity\n    [InlineData(4L, 0L, 5)] // Item deleted, returns base storage\n    [InlineData(2L, 0L, 5)] // Item deleted, returns base storage\n    [InlineData(8L, 4L, 9)] // 5 included + 4 new quantity\n    public void CalculateNewMaxStorageGb_WithQuantityUpdate_ReturnsCorrectMaxStorage(\n        long currentQuantity, long newQuantity, short expectedMaxStorageGb)\n    {\n        // Arrange\n        var updateOptions = new SubscriptionUpdateOptions\n        {\n            Items =\n            [\n                newQuantity == 0\n                    ? new SubscriptionItemOptions { Id = \"si_123\", Deleted = true } // Item marked as deleted\n                    : new SubscriptionItemOptions { Id = \"si_123\", Quantity = newQuantity } // Item quantity updated\n            ]\n        };\n\n        // Act\n        var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);\n\n        // Assert\n        Assert.Equal(expectedMaxStorageGb, result);\n    }\n\n    [Fact]\n    public void CalculateNewMaxStorageGb_WithNullUpdateOptions_ReturnsCurrentQuantityPlusBaseIncluded()\n    {\n        // Arrange\n        const long currentQuantity = 10;\n\n        // Act\n        var result = _sut.CalculateNewMaxStorageGb(currentQuantity, null);\n\n        // Assert\n        Assert.Equal((short)(5 + currentQuantity), result);\n    }\n\n    [Fact]\n    public void CalculateNewMaxStorageGb_WithNullItems_ReturnsCurrentQuantityPlusBaseIncluded()\n    {\n        // Arrange\n        const long currentQuantity = 10;\n        var updateOptions = new SubscriptionUpdateOptions { Items = null };\n\n        // Act\n        var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);\n\n        // Assert\n        Assert.Equal(5 + currentQuantity, result);\n    }\n\n    [Fact]\n    public void CalculateNewMaxStorageGb_WithEmptyItems_ReturnsCurrentQuantity()\n    {\n        // Arrange\n        const long currentQuantity = 10;\n        var updateOptions = new SubscriptionUpdateOptions\n        {\n            Items = []\n        };\n\n        // Act\n        var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);\n\n        // Assert\n        Assert.Equal(5 + currentQuantity, result);\n    }\n\n    [Fact]\n    public void CalculateNewMaxStorageGb_WithDeletedItem_ReturnsBaseStorage()\n    {\n        // Arrange\n        const long currentQuantity = 100;\n        var updateOptions = new SubscriptionUpdateOptions\n        {\n            Items = [new SubscriptionItemOptions { Id = \"si_123\", Deleted = true }]\n        };\n\n        // Act\n        var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);\n\n        // Assert\n        Assert.Equal((short)5, result); // Base storage\n    }\n\n    [Fact]\n    public void CalculateNewMaxStorageGb_WithItemWithoutQuantity_ReturnsCurrentQuantity()\n    {\n        // Arrange\n        const long currentQuantity = 10;\n        var updateOptions = new SubscriptionUpdateOptions\n        {\n            Items = [new SubscriptionItemOptions { Id = \"si_123\", Quantity = null }]\n        };\n\n        // Act\n        var result = _sut.CalculateNewMaxStorageGb(currentQuantity, updateOptions);\n\n        // Assert\n        Assert.Equal(5 + currentQuantity, result);\n    }\n\n    #endregion\n\n    #region UpdateDatabaseMaxStorageAsync Tests\n\n    [Fact]\n    public async Task UpdateDatabaseMaxStorageAsync_PersonalTier_UpdatesUser()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var user = new Bit.Core.Entities.User\n        {\n            Id = userId,\n            Email = \"test@example.com\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n        _userRepository.GetByIdAsync(userId).Returns(user);\n        _userRepository.ReplaceAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _sut.UpdateDatabaseMaxStorageAsync(\n            ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal,\n            userId,\n            10,\n            \"sub_123\");\n\n        // Assert\n        Assert.True(result);\n        Assert.Equal((short)10, user.MaxStorageGb);\n        await _userRepository.Received(1).GetByIdAsync(userId);\n        await _userRepository.Received(1).ReplaceAsync(user);\n    }\n\n    [Fact]\n    public async Task UpdateDatabaseMaxStorageAsync_PersonalTier_UserNotFound_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        _userRepository.GetByIdAsync(userId).Returns((Bit.Core.Entities.User?)null);\n\n        // Act\n        var result = await _sut.UpdateDatabaseMaxStorageAsync(\n            ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal,\n            userId,\n            10,\n            \"sub_123\");\n\n        // Assert\n        Assert.False(result);\n        await _userRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default!);\n    }\n\n    [Fact]\n    public async Task UpdateDatabaseMaxStorageAsync_PersonalTier_ReplaceThrowsException_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var user = new Bit.Core.Entities.User\n        {\n            Id = userId,\n            Email = \"test@example.com\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n        _userRepository.GetByIdAsync(userId).Returns(user);\n        _userRepository.ReplaceAsync(user).Throws(new Exception(\"Database error\"));\n\n        // Act\n        var result = await _sut.UpdateDatabaseMaxStorageAsync(\n            ReconcileAdditionalStorageJob.SubscriptionPlanTier.Personal,\n            userId,\n            10,\n            \"sub_123\");\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public async Task UpdateDatabaseMaxStorageAsync_OrganizationTier_UpdatesOrganization()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var organization = new Bit.Core.AdminConsole.Entities.Organization\n        {\n            Id = organizationId,\n            Name = \"Test Org\",\n            GatewaySubscriptionId = \"sub_456\"\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n        _organizationRepository.ReplaceAsync(organization).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _sut.UpdateDatabaseMaxStorageAsync(\n            ReconcileAdditionalStorageJob.SubscriptionPlanTier.Organization,\n            organizationId,\n            20,\n            \"sub_456\");\n\n        // Assert\n        Assert.True(result);\n        Assert.Equal((short)20, organization.MaxStorageGb);\n        await _organizationRepository.Received(1).GetByIdAsync(organizationId);\n        await _organizationRepository.Received(1).ReplaceAsync(organization);\n    }\n\n    [Fact]\n    public async Task UpdateDatabaseMaxStorageAsync_OrganizationTier_OrganizationNotFound_ReturnsFalse()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        _organizationRepository.GetByIdAsync(organizationId)\n            .Returns((Bit.Core.AdminConsole.Entities.Organization?)null);\n\n        // Act\n        var result = await _sut.UpdateDatabaseMaxStorageAsync(\n            ReconcileAdditionalStorageJob.SubscriptionPlanTier.Organization,\n            organizationId,\n            20,\n            \"sub_456\");\n\n        // Assert\n        Assert.False(result);\n        await _organizationRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default!);\n    }\n\n    [Fact]\n    public async Task UpdateDatabaseMaxStorageAsync_OrganizationTier_ReplaceThrowsException_ReturnsFalse()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var organization = new Bit.Core.AdminConsole.Entities.Organization\n        {\n            Id = organizationId,\n            Name = \"Test Org\",\n            GatewaySubscriptionId = \"sub_456\"\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n        _organizationRepository.ReplaceAsync(organization).Throws(new Exception(\"Database error\"));\n\n        // Act\n        var result = await _sut.UpdateDatabaseMaxStorageAsync(\n            ReconcileAdditionalStorageJob.SubscriptionPlanTier.Organization,\n            organizationId,\n            20,\n            \"sub_456\");\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public async Task UpdateDatabaseMaxStorageAsync_UnknownTier_ReturnsFalse()\n    {\n        // Arrange & Act\n        var entityId = Guid.NewGuid();\n        var result = await _sut.UpdateDatabaseMaxStorageAsync(\n            ReconcileAdditionalStorageJob.SubscriptionPlanTier.Unknown,\n            entityId,\n            15,\n            \"sub_789\");\n\n        // Assert\n        Assert.False(result);\n        await _userRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);\n        await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);\n    }\n\n    #endregion\n\n    #endregion\n\n    #region Helper Methods\n\n    private static IJobExecutionContext CreateJobExecutionContext(CancellationToken cancellationToken = default)\n    {\n        var context = Substitute.For<IJobExecutionContext>();\n        context.CancellationToken.Returns(cancellationToken);\n        return context;\n    }\n\n    private static Subscription CreateSubscription(\n        string id,\n        string priceId,\n        long? quantity = null,\n        Dictionary<string, string>? metadata = null,\n        string status = StripeConstants.SubscriptionStatus.Active)\n    {\n        var price = new Price { Id = priceId };\n        var item = new SubscriptionItem\n        {\n            Id = $\"si_{id}\",\n            Price = price,\n            Quantity = quantity ?? 0\n        };\n\n        return new Subscription\n        {\n            Id = id,\n            Status = status,\n            Metadata = metadata,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [item]\n            }\n        };\n    }\n\n    private static Subscription CreateSubscriptionWithMultipleItems(string id, (string priceId, long quantity)[] items)\n    {\n        var subscriptionItems = items.Select(i => new SubscriptionItem\n        {\n            Id = $\"si_{id}_{i.priceId}\",\n            Price = new Price { Id = i.priceId },\n            Quantity = i.quantity\n        }).ToList();\n\n        return new Subscription\n        {\n            Id = id,\n            Status = StripeConstants.SubscriptionStatus.Active,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = subscriptionItems\n            }\n        };\n    }\n\n    #endregion\n}\n\ninternal static class AsyncEnumerable\n{\n    public static async IAsyncEnumerable<T> Create<T>(params T[] items)\n    {\n        foreach (var item in items)\n        {\n            yield return item;\n        }\n        await Task.CompletedTask;\n    }\n\n    public static async IAsyncEnumerable<T> Empty<T>()\n    {\n        await Task.CompletedTask;\n        yield break;\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Jobs/SubscriptionCancellationJobTests.cs",
    "content": "﻿using Bit.Billing.Constants;\nusing Bit.Billing.Jobs;\nusing Bit.Billing.Services;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Quartz;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Billing.Test.Jobs;\n\npublic class SubscriptionCancellationJobTests\n{\n    private readonly IStripeFacade _stripeFacade;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly SubscriptionCancellationJob _sut;\n\n    public SubscriptionCancellationJobTests()\n    {\n        _stripeFacade = Substitute.For<IStripeFacade>();\n        _organizationRepository = Substitute.For<IOrganizationRepository>();\n        _sut = new SubscriptionCancellationJob(_stripeFacade, _organizationRepository, Substitute.For<ILogger<SubscriptionCancellationJob>>());\n    }\n\n    [Fact]\n    public async Task Execute_OrganizationIsNull_SkipsCancellation()\n    {\n        // Arrange\n        const string subscriptionId = \"sub_123\";\n        var organizationId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(subscriptionId, organizationId);\n\n        _organizationRepository.GetByIdAsync(organizationId).Returns((Organization)null);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());\n        await _stripeFacade.DidNotReceiveWithAnyArgs().CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_OrganizationIsEnabled_SkipsCancellation()\n    {\n        // Arrange\n        const string subscriptionId = \"sub_123\";\n        var organizationId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(subscriptionId, organizationId);\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Enabled = true\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());\n        await _stripeFacade.DidNotReceiveWithAnyArgs().CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_SubscriptionStatusIsNotUnpaid_SkipsCancellation()\n    {\n        // Arrange\n        const string subscriptionId = \"sub_123\";\n        var organizationId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(subscriptionId, organizationId);\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Enabled = false\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = StripeSubscriptionStatus.Active,\n            LatestInvoice = new Invoice\n            {\n                BillingReason = \"subscription_cycle\"\n            }\n        };\n        _stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains(\"latest_invoice\")))\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceive().CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_BillingReasonIsInvalid_SkipsCancellation()\n    {\n        // Arrange\n        const string subscriptionId = \"sub_123\";\n        var organizationId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(subscriptionId, organizationId);\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Enabled = false\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = StripeSubscriptionStatus.Unpaid,\n            LatestInvoice = new Invoice\n            {\n                BillingReason = \"manual\"\n            }\n        };\n        _stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains(\"latest_invoice\")))\n            .Returns(subscription);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.DidNotReceive().CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_ValidConditions_CancelsSubscriptionAndVoidsInvoices()\n    {\n        // Arrange\n        const string subscriptionId = \"sub_123\";\n        var organizationId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(subscriptionId, organizationId);\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Enabled = false\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = StripeSubscriptionStatus.Unpaid,\n            LatestInvoice = new Invoice\n            {\n                BillingReason = \"subscription_cycle\"\n            }\n        };\n        _stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains(\"latest_invoice\")))\n            .Returns(subscription);\n\n        var invoices = new StripeList<Invoice>\n        {\n            Data =\n            [\n                new Invoice { Id = \"inv_1\" },\n                new Invoice { Id = \"inv_2\" }\n            ],\n            HasMore = false\n        };\n        _stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>()).Returns(invoices);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());\n        await _stripeFacade.Received(1).VoidInvoice(\"inv_1\");\n        await _stripeFacade.Received(1).VoidInvoice(\"inv_2\");\n    }\n\n    [Fact]\n    public async Task Execute_WithSubscriptionCreateBillingReason_CancelsSubscription()\n    {\n        // Arrange\n        const string subscriptionId = \"sub_123\";\n        var organizationId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(subscriptionId, organizationId);\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Enabled = false\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = StripeSubscriptionStatus.Unpaid,\n            LatestInvoice = new Invoice\n            {\n                BillingReason = \"subscription_create\"\n            }\n        };\n        _stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains(\"latest_invoice\")))\n            .Returns(subscription);\n\n        var invoices = new StripeList<Invoice>\n        {\n            Data = [],\n            HasMore = false\n        };\n        _stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>()).Returns(invoices);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_NoOpenInvoices_CancelsSubscriptionOnly()\n    {\n        // Arrange\n        const string subscriptionId = \"sub_123\";\n        var organizationId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(subscriptionId, organizationId);\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Enabled = false\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = StripeSubscriptionStatus.Unpaid,\n            LatestInvoice = new Invoice\n            {\n                BillingReason = \"subscription_cycle\"\n            }\n        };\n        _stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains(\"latest_invoice\")))\n            .Returns(subscription);\n\n        var invoices = new StripeList<Invoice>\n        {\n            Data = [],\n            HasMore = false\n        };\n        _stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>()).Returns(invoices);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());\n        await _stripeFacade.DidNotReceiveWithAnyArgs().VoidInvoice(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task Execute_WithPagination_VoidsAllInvoices()\n    {\n        // Arrange\n        const string subscriptionId = \"sub_123\";\n        var organizationId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(subscriptionId, organizationId);\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Enabled = false\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = StripeSubscriptionStatus.Unpaid,\n            LatestInvoice = new Invoice\n            {\n                BillingReason = \"subscription_cycle\"\n            }\n        };\n        _stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains(\"latest_invoice\")))\n            .Returns(subscription);\n\n        // First page of invoices\n        var firstPage = new StripeList<Invoice>\n        {\n            Data =\n            [\n                new Invoice { Id = \"inv_1\" },\n                new Invoice { Id = \"inv_2\" }\n            ],\n            HasMore = true\n        };\n\n        // Second page of invoices\n        var secondPage = new StripeList<Invoice>\n        {\n            Data =\n            [\n                new Invoice { Id = \"inv_3\" },\n                new Invoice { Id = \"inv_4\" }\n            ],\n            HasMore = false\n        };\n\n        _stripeFacade.ListInvoices(Arg.Is<InvoiceListOptions>(o => o.StartingAfter == null))\n            .Returns(firstPage);\n        _stripeFacade.ListInvoices(Arg.Is<InvoiceListOptions>(o => o.StartingAfter == \"inv_2\"))\n            .Returns(secondPage);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());\n        await _stripeFacade.Received(1).VoidInvoice(\"inv_1\");\n        await _stripeFacade.Received(1).VoidInvoice(\"inv_2\");\n        await _stripeFacade.Received(1).VoidInvoice(\"inv_3\");\n        await _stripeFacade.Received(1).VoidInvoice(\"inv_4\");\n        await _stripeFacade.Received(2).ListInvoices(Arg.Any<InvoiceListOptions>());\n    }\n\n    [Fact]\n    public async Task Execute_ListInvoicesCalledWithCorrectOptions()\n    {\n        // Arrange\n        const string subscriptionId = \"sub_123\";\n        var organizationId = Guid.NewGuid();\n        var context = CreateJobExecutionContext(subscriptionId, organizationId);\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            Enabled = false\n        };\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = StripeSubscriptionStatus.Unpaid,\n            LatestInvoice = new Invoice\n            {\n                BillingReason = \"subscription_cycle\"\n            }\n        };\n        _stripeFacade.GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains(\"latest_invoice\")))\n            .Returns(subscription);\n\n        var invoices = new StripeList<Invoice>\n        {\n            Data = [],\n            HasMore = false\n        };\n        _stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>()).Returns(invoices);\n\n        // Act\n        await _sut.Execute(context);\n\n        // Assert\n        await _stripeFacade.Received(1).GetSubscription(subscriptionId, Arg.Is<SubscriptionGetOptions>(o => o.Expand.Contains(\"latest_invoice\")));\n        await _stripeFacade.Received(1).ListInvoices(Arg.Is<InvoiceListOptions>(o =>\n            o.Status == \"open\" &&\n            o.Subscription == subscriptionId &&\n            o.Limit == 100));\n    }\n\n    private static IJobExecutionContext CreateJobExecutionContext(string subscriptionId, Guid organizationId)\n    {\n        var context = Substitute.For<IJobExecutionContext>();\n        var jobDataMap = new JobDataMap\n        {\n            { \"subscriptionId\", subscriptionId },\n            { \"organizationId\", organizationId.ToString() }\n        };\n        context.MergedJobDataMap.Returns(jobDataMap);\n        return context;\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Resources/IPN/echeck-payment.txt",
    "content": "mc_gross=48.00&\nmp_custom=&\nmp_currency=USD&\nprotection_eligibility=Eligible&\npayer_id=SVELHYY6G7TJ4&\npayment_date=11%3A07%3A43+Dec+27%2C+2023+PST&\nmp_id=B-4DP02332FD689211K&\npayment_status=Completed&\ncharset=UTF-8&\nfirst_name=John&\nmp_status=0&\nmc_fee=2.17&\nnotify_version=3.9&\ncustom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS&\npayer_status=verified&\nbusiness=sb-edwkp27927299%40business.example.com&\nquantity=1&\nverify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G&\npayer_email=sb-xuhf727950096%40personal.example.com&\ntxn_id=2PK15573S8089712Y&\npayment_type=echeck&\nlast_name=Doe&\nmp_desc=&\nreceiver_email=sb-edwkp27927299%40business.example.com&\npayment_fee=2.17&\nmp_cycle_start=30&\nshipping_discount=0.00&\ninsurance_amount=0.00&\nreceiver_id=NHDYKLQ3L4LWL&\ntxn_type=merch_pmt&\nitem_name=&\ndiscount=0.00&\nmc_currency=USD&\nitem_number=&\nresidence_country=US&\ntest_ipn=1&\nshipping_method=Default&\ntransaction_subject=&\npayment_gross=48.00&\nipn_track_id=769757969c134\n"
  },
  {
    "path": "test/Billing.Test/Resources/IPN/non-usd-payment.txt",
    "content": "mc_gross=48.00&\nmp_custom=&\nmp_currency=CAD&\nprotection_eligibility=Eligible&\npayer_id=SVELHYY6G7TJ4&\npayment_date=11%3A07%3A43+Dec+27%2C+2023+PST&\nmp_id=B-4DP02332FD689211K&\npayment_status=Completed&\ncharset=UTF-8&\nfirst_name=John&\nmp_status=0&\nmc_fee=2.17&\nnotify_version=3.9&\ncustom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS&\npayer_status=verified&\nbusiness=sb-edwkp27927299%40business.example.com&\nquantity=1&\nverify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G&\npayer_email=sb-xuhf727950096%40personal.example.com&\ntxn_id=2PK15573S8089712Y&\npayment_type=instant&\nlast_name=Doe&\nmp_desc=&\nreceiver_email=sb-edwkp27927299%40business.example.com&\npayment_fee=2.17&\nmp_cycle_start=30&\nshipping_discount=0.00&\ninsurance_amount=0.00&\nreceiver_id=NHDYKLQ3L4LWL&\ntxn_type=merch_pmt&\nitem_name=&\ndiscount=0.00&\nmc_currency=CAD&\nitem_number=&\nresidence_country=US&\ntest_ipn=1&\nshipping_method=Default&\ntransaction_subject=&\npayment_gross=48.00&\nipn_track_id=769757969c134\n"
  },
  {
    "path": "test/Billing.Test/Resources/IPN/refund-missing-parent-transaction.txt",
    "content": "mc_gross=48.00&\nmp_custom=&\nmp_currency=USD&\nprotection_eligibility=Eligible&\npayer_id=SVELHYY6G7TJ4&\npayment_date=11%3A07%3A43+Dec+27%2C+2023+PST&\nmp_id=B-4DP02332FD689211K&\npayment_status=Refunded&\ncharset=UTF-8&\nfirst_name=John&\nmp_status=0&\nmc_fee=2.17&\nnotify_version=3.9&\ncustom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS&\npayer_status=verified&\nbusiness=sb-edwkp27927299%40business.example.com&\nquantity=1&\nverify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G&\npayer_email=sb-xuhf727950096%40personal.example.com&\ntxn_id=2PK15573S8089712Y&\npayment_type=instant&\nlast_name=Doe&\nmp_desc=&\nreceiver_email=sb-edwkp27927299%40business.example.com&\npayment_fee=2.17&\nmp_cycle_start=30&\nshipping_discount=0.00&\ninsurance_amount=0.00&\nreceiver_id=NHDYKLQ3L4LWL&\ntxn_type=merch_pmt&\nitem_name=&\ndiscount=0.00&\nmc_currency=USD&\nitem_number=&\nresidence_country=US&\ntest_ipn=1&\nshipping_method=Default&\ntransaction_subject=&\npayment_gross=48.00&\nipn_track_id=769757969c134\n"
  },
  {
    "path": "test/Billing.Test/Resources/IPN/successful-payment-org-credit.txt",
    "content": "mc_gross=48.00&\nmp_custom=&\nmp_currency=USD&\nprotection_eligibility=Eligible&\npayer_id=SVELHYY6G7TJ4&\npayment_date=11%3A07%3A43+Dec+27%2C+2023+PST&\nmp_id=B-4DP02332FD689211K&\npayment_status=Completed&\ncharset=UTF-8&\nfirst_name=John&\nmp_status=0&\nmc_fee=2.17&\nnotify_version=3.9&\ncustom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS%2Caccount_credit%3A1&\npayer_status=verified&\nbusiness=sb-edwkp27927299%40business.example.com&\nquantity=1&\nverify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G&\npayer_email=sb-xuhf727950096%40personal.example.com&\ntxn_id=2PK15573S8089712Y&\npayment_type=instant&\nlast_name=Doe&\nmp_desc=&\nreceiver_email=sb-edwkp27927299%40business.example.com&\npayment_fee=2.17&\nmp_cycle_start=30&\nshipping_discount=0.00&\ninsurance_amount=0.00&\nreceiver_id=NHDYKLQ3L4LWL&\ntxn_type=merch_pmt&\nitem_name=&\ndiscount=0.00&\nmc_currency=USD&\nitem_number=&\nresidence_country=US&\ntest_ipn=1&\nshipping_method=Default&\ntransaction_subject=&\npayment_gross=48.00&\nipn_track_id=769757969c134\n"
  },
  {
    "path": "test/Billing.Test/Resources/IPN/successful-payment-user-credit.txt",
    "content": "mc_gross=48.00&\nmp_custom=&\nmp_currency=USD&\nprotection_eligibility=Eligible&\npayer_id=SVELHYY6G7TJ4&\npayment_date=11%3A07%3A43+Dec+27%2C+2023+PST&\nmp_id=B-4DP02332FD689211K&\npayment_status=Completed&\ncharset=UTF-8&\nfirst_name=John&\nmp_status=0&\nmc_fee=2.17&\nnotify_version=3.9&\ncustom=user_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS%2Caccount_credit%3A1&\npayer_status=verified&\nbusiness=sb-edwkp27927299%40business.example.com&\nquantity=1&\nverify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G&\npayer_email=sb-xuhf727950096%40personal.example.com&\ntxn_id=2PK15573S8089712Y&\npayment_type=instant&\nlast_name=Doe&\nmp_desc=&\nreceiver_email=sb-edwkp27927299%40business.example.com&\npayment_fee=2.17&\nmp_cycle_start=30&\nshipping_discount=0.00&\ninsurance_amount=0.00&\nreceiver_id=NHDYKLQ3L4LWL&\ntxn_type=merch_pmt&\nitem_name=&\ndiscount=0.00&\nmc_currency=USD&\nitem_number=&\nresidence_country=US&\ntest_ipn=1&\nshipping_method=Default&\ntransaction_subject=&\npayment_gross=48.00&\nipn_track_id=769757969c134\n"
  },
  {
    "path": "test/Billing.Test/Resources/IPN/successful-payment.txt",
    "content": "mc_gross=48.00&\nmp_custom=&\nmp_currency=USD&\nprotection_eligibility=Eligible&\npayer_id=SVELHYY6G7TJ4&\npayment_date=11%3A07%3A43+Dec+27%2C+2023+PST&\nmp_id=B-4DP02332FD689211K&\npayment_status=Completed&\ncharset=UTF-8&\nfirst_name=John&\nmp_status=0&\nmc_fee=2.17&\nnotify_version=3.9&\ncustom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS&\npayer_status=verified&\nbusiness=sb-edwkp27927299%40business.example.com&\nquantity=1&\nverify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G&\npayer_email=sb-xuhf727950096%40personal.example.com&\ntxn_id=2PK15573S8089712Y&\npayment_type=instant&\nlast_name=Doe&\nmp_desc=&\nreceiver_email=sb-edwkp27927299%40business.example.com&\npayment_fee=2.17&\nmp_cycle_start=30&\nshipping_discount=0.00&\ninsurance_amount=0.00&\nreceiver_id=NHDYKLQ3L4LWL&\ntxn_type=merch_pmt&\nitem_name=&\ndiscount=0.00&\nmc_currency=USD&\nitem_number=&\nresidence_country=US&\ntest_ipn=1&\nshipping_method=Default&\ntransaction_subject=&\npayment_gross=48.00&\nipn_track_id=769757969c134\n"
  },
  {
    "path": "test/Billing.Test/Resources/IPN/successful-refund.txt",
    "content": "mc_gross=48.00&\nmp_custom=&\nmp_currency=USD&\nprotection_eligibility=Eligible&\npayer_id=SVELHYY6G7TJ4&\npayment_date=11%3A07%3A43+Dec+27%2C+2023+PST&\nmp_id=B-4DP02332FD689211K&\npayment_status=Refunded&\ncharset=UTF-8&\nfirst_name=John&\nmp_status=0&\nmc_fee=2.17&\nnotify_version=3.9&\ncustom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS&\npayer_status=verified&\nbusiness=sb-edwkp27927299%40business.example.com&\nquantity=1&\nverify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G&\npayer_email=sb-xuhf727950096%40personal.example.com&\ntxn_id=2PK15573S8089712Y&\npayment_type=instant&\nlast_name=Doe&\nmp_desc=&\nreceiver_email=sb-edwkp27927299%40business.example.com&\npayment_fee=2.17&\nmp_cycle_start=30&\nshipping_discount=0.00&\ninsurance_amount=0.00&\nreceiver_id=NHDYKLQ3L4LWL&\ntxn_type=merch_pmt&\nitem_name=&\ndiscount=0.00&\nmc_currency=USD&\nitem_number=&\nresidence_country=US&\ntest_ipn=1&\nshipping_method=Default&\ntransaction_subject=&\npayment_gross=48.00&\nipn_track_id=769757969c134&\nparent_txn_id=PARENT\n"
  },
  {
    "path": "test/Billing.Test/Resources/IPN/transaction-missing-entity-ids.txt",
    "content": "mc_gross=48.00&\nmp_custom=&\nmp_currency=USD&\nprotection_eligibility=Eligible&\npayer_id=SVELHYY6G7TJ4&\npayment_date=11%3A07%3A43+Dec+27%2C+2023+PST&\nmp_id=B-4DP02332FD689211K&\npayment_status=Completed&\ncharset=UTF-8&\nfirst_name=John&\nmp_status=0&\nmc_fee=2.17&\nnotify_version=3.9&\ncustom=region%3AUS&\npayer_status=verified&\nbusiness=sb-edwkp27927299%40business.example.com&\nquantity=1&\nverify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G&\npayer_email=sb-xuhf727950096%40personal.example.com&\ntxn_id=2PK15573S8089712Y&\npayment_type=instant&\nlast_name=Doe&\nmp_desc=&\nreceiver_email=sb-edwkp27927299%40business.example.com&\npayment_fee=2.17&\nmp_cycle_start=30&\nshipping_discount=0.00&\ninsurance_amount=0.00&\nreceiver_id=NHDYKLQ3L4LWL&\ntxn_type=merch_pmt&\nitem_name=&\ndiscount=0.00&\nmc_currency=USD&\nitem_number=&\nresidence_country=US&\ntest_ipn=1&\nshipping_method=Default&\ntransaction_subject=&\npayment_gross=48.00&\nipn_track_id=769757969c134\n"
  },
  {
    "path": "test/Billing.Test/Resources/IPN/unsupported-transaction-type.txt",
    "content": "mc_gross=48.00&\nmp_custom=&\nmp_currency=USD&\nprotection_eligibility=Eligible&\npayer_id=SVELHYY6G7TJ4&\npayment_date=11%3A07%3A43+Dec+27%2C+2023+PST&\nmp_id=B-4DP02332FD689211K&\npayment_status=Completed&\ncharset=UTF-8&\nfirst_name=John&\nmp_status=0&\nmc_fee=2.17&\nnotify_version=3.9&\ncustom=organization_id%3Aca8c6f2b-2d7b-4639-809f-b0e5013a304e%2Cregion%3AUS&\npayer_status=verified&\nbusiness=sb-edwkp27927299%40business.example.com&\nquantity=1&\nverify_sign=ADVh..MARsZyzWEIrCQ5ouOAs1ILA3B0hB.p9fXf41nrdYnQQO5Xid7G&\npayer_email=sb-xuhf727950096%40personal.example.com&\ntxn_id=2PK15573S8089712Y&\npayment_type=instant&\nlast_name=Doe&\nmp_desc=&\nreceiver_email=sb-edwkp27927299%40business.example.com&\npayment_fee=2.17&\nmp_cycle_start=30&\nshipping_discount=0.00&\ninsurance_amount=0.00&\nreceiver_id=NHDYKLQ3L4LWL&\ntxn_type=other&\nitem_name=&\ndiscount=0.00&\nmc_currency=USD&\nitem_number=&\nresidence_country=US&\ntest_ipn=1&\nshipping_method=Default&\ntransaction_subject=&\npayment_gross=48.00&\nipn_track_id=769757969c134\n"
  },
  {
    "path": "test/Billing.Test/Services/CouponDeletedHandlerTests.cs",
    "content": "﻿using Bit.Billing.Services.Implementations;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Billing.Subscriptions.Repositories;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Billing.Test.Services;\n\npublic class CouponDeletedHandlerTests\n{\n    private readonly ILogger<CouponDeletedHandler> _logger = Substitute.For<ILogger<CouponDeletedHandler>>();\n    private readonly ISubscriptionDiscountRepository _subscriptionDiscountRepository = Substitute.For<ISubscriptionDiscountRepository>();\n    private readonly CouponDeletedHandler _sut;\n\n    public CouponDeletedHandlerTests()\n    {\n        _sut = new CouponDeletedHandler(_logger, _subscriptionDiscountRepository);\n    }\n\n    [Fact]\n    public async Task HandleAsync_EventObjectNotCoupon_ReturnsWithoutDeleting()\n    {\n        // Arrange\n        var stripeEvent = new Event\n        {\n            Id = \"evt_test\",\n            Data = new EventData { Object = new Customer { Id = \"cus_unexpected\" } }\n        };\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        await _subscriptionDiscountRepository.DidNotReceiveWithAnyArgs()\n            .GetByStripeCouponIdAsync(null!);\n        await _subscriptionDiscountRepository.DidNotReceiveWithAnyArgs()\n            .DeleteAsync(null!);\n    }\n\n    [Fact]\n    public async Task HandleAsync_CouponNotInDatabase_DoesNotDeleteAnything()\n    {\n        // Arrange\n        var stripeEvent = new Event { Data = new EventData { Object = new Coupon { Id = \"cou_test\" } } };\n\n        _subscriptionDiscountRepository.GetByStripeCouponIdAsync(\"cou_test\").Returns((SubscriptionDiscount?)null);\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        await _subscriptionDiscountRepository.DidNotReceiveWithAnyArgs().DeleteAsync(null!);\n    }\n\n    [Fact]\n    public async Task HandleAsync_CouponExistsInDatabase_DeletesDiscount()\n    {\n        // Arrange\n        var stripeEvent = new Event { Data = new EventData { Object = new Coupon { Id = \"cou_test\" } } };\n\n        var discount = new SubscriptionDiscount { StripeCouponId = \"cou_test\" };\n        _subscriptionDiscountRepository.GetByStripeCouponIdAsync(\"cou_test\").Returns(discount);\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        await _subscriptionDiscountRepository.Received(1).DeleteAsync(discount);\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Services/PayPalIPNClientTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Billing.Services;\nusing Bit.Billing.Services.Implementations;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing NSubstitute;\nusing RichardSzalay.MockHttp;\nusing Xunit;\n\nnamespace Bit.Billing.Test.Services;\n\npublic class PayPalIPNClientTests\n{\n    private readonly Uri _endpoint = new(\"https://www.sandbox.paypal.com/cgi-bin/webscr\");\n    private readonly MockHttpMessageHandler _mockHttpMessageHandler = new();\n\n    private readonly IOptions<BillingSettings> _billingSettings = Substitute.For<IOptions<BillingSettings>>();\n    private readonly ILogger<PayPalIPNClient> _logger = Substitute.For<ILogger<PayPalIPNClient>>();\n\n    private readonly IPayPalIPNClient _payPalIPNClient;\n\n    public PayPalIPNClientTests()\n    {\n        var httpClient = new HttpClient(_mockHttpMessageHandler)\n        {\n            BaseAddress = _endpoint\n        };\n\n        _payPalIPNClient = new PayPalIPNClient(\n            _billingSettings,\n            httpClient,\n            _logger);\n    }\n\n    [Fact]\n    public async Task VerifyIPN_FormDataNull_ThrowsArgumentNullException()\n        => await Assert.ThrowsAsync<ArgumentNullException>(() => _payPalIPNClient.VerifyIPN(string.Empty, null));\n\n    [Fact]\n    public async Task VerifyIPN_Unauthorized_ReturnsFalse()\n    {\n        const string formData = \"form=data\";\n\n        var request = _mockHttpMessageHandler\n            .Expect(HttpMethod.Post, _endpoint.ToString())\n            .WithFormData(new Dictionary<string, string> { { \"cmd\", \"_notify-validate\" }, { \"form\", \"data\" } })\n            .Respond(HttpStatusCode.Unauthorized);\n\n        var verified = await _payPalIPNClient.VerifyIPN(string.Empty, formData);\n\n        Assert.False(verified);\n        Assert.Equal(1, _mockHttpMessageHandler.GetMatchCount(request));\n    }\n\n    [Fact]\n    public async Task VerifyIPN_OK_Invalid_ReturnsFalse()\n    {\n        const string formData = \"form=data\";\n\n        var request = _mockHttpMessageHandler\n            .Expect(HttpMethod.Post, _endpoint.ToString())\n            .WithFormData(new Dictionary<string, string> { { \"cmd\", \"_notify-validate\" }, { \"form\", \"data\" } })\n            .Respond(\"application/text\", \"INVALID\");\n\n        var verified = await _payPalIPNClient.VerifyIPN(string.Empty, formData);\n\n        Assert.False(verified);\n        Assert.Equal(1, _mockHttpMessageHandler.GetMatchCount(request));\n    }\n\n    [Fact]\n    public async Task VerifyIPN_OK_Verified_ReturnsTrue()\n    {\n        const string formData = \"form=data\";\n\n        var request = _mockHttpMessageHandler\n            .Expect(HttpMethod.Post, _endpoint.ToString())\n            .WithFormData(new Dictionary<string, string> { { \"cmd\", \"_notify-validate\" }, { \"form\", \"data\" } })\n            .Respond(\"application/text\", \"VERIFIED\");\n\n        var verified = await _payPalIPNClient.VerifyIPN(string.Empty, formData);\n\n        Assert.True(verified);\n        Assert.Equal(1, _mockHttpMessageHandler.GetMatchCount(request));\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Services/ProviderEventServiceTests.cs",
    "content": "﻿using Bit.Billing.Services;\nusing Bit.Billing.Services.Implementations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Providers.Entities;\nusing Bit.Core.Billing.Providers.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.Billing.Mocks;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Billing.Test.Services;\n\npublic class ProviderEventServiceTests\n{\n    private readonly IOrganizationRepository _organizationRepository =\n        Substitute.For<IOrganizationRepository>();\n\n    private readonly IPricingClient _pricingClient =\n        Substitute.For<IPricingClient>();\n\n    private readonly IProviderInvoiceItemRepository _providerInvoiceItemRepository =\n        Substitute.For<IProviderInvoiceItemRepository>();\n\n    private readonly IProviderOrganizationRepository _providerOrganizationRepository =\n        Substitute.For<IProviderOrganizationRepository>();\n\n    private readonly IProviderPlanRepository _providerPlanRepository =\n        Substitute.For<IProviderPlanRepository>();\n\n    private readonly IStripeEventService _stripeEventService =\n        Substitute.For<IStripeEventService>();\n\n    private readonly IStripeFacade _stripeFacade =\n        Substitute.For<IStripeFacade>();\n\n    private readonly ProviderEventService _providerEventService;\n\n    public ProviderEventServiceTests()\n    {\n        _providerEventService = new ProviderEventService(\n            _organizationRepository,\n            _pricingClient,\n            _providerInvoiceItemRepository,\n            _providerOrganizationRepository,\n            _providerPlanRepository,\n            _stripeEventService,\n            _stripeFacade);\n    }\n\n    #region TryRecordInvoiceLineItems\n    [Fact]\n    public async Task TryRecordInvoiceLineItems_EventTypeNotInvoiceCreatedOrInvoiceFinalized_NoOp()\n    {\n        // Arrange\n        var stripeEvent = new Event { Type = \"payment_method.attached\" };\n\n        // Act\n        await _providerEventService.TryRecordInvoiceLineItems(stripeEvent);\n\n        // Assert\n        await _stripeEventService.DidNotReceiveWithAnyArgs().GetInvoice(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>?>());\n    }\n\n    [Fact]\n    public async Task TryRecordInvoiceLineItems_InvoiceParentTypeNotSubscriptionDetails_NoOp()\n    {\n        // Arrange\n        var stripeEvent = new Event\n        {\n            Type = \"invoice.created\"\n        };\n\n        var invoice = new Invoice\n        {\n            Parent = new InvoiceParent\n            {\n                Type = \"credit_note\",\n                SubscriptionDetails = new InvoiceParentSubscriptionDetails\n                {\n                    SubscriptionId = \"sub_1\"\n                }\n            }\n        };\n\n        _stripeEventService.GetInvoice(stripeEvent, true, Arg.Any<List<string>?>()).Returns(invoice);\n\n        // Act\n        await _providerEventService.TryRecordInvoiceLineItems(stripeEvent);\n\n        // Assert\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task TryRecordInvoiceLineItems_EventNotProviderRelated_NoOp()\n    {\n        // Arrange\n        var stripeEvent = new Event\n        {\n            Type = \"invoice.created\"\n        };\n\n        const string subscriptionId = \"sub_1\";\n\n        var invoice = new Invoice\n        {\n            Parent = new InvoiceParent\n            {\n                Type = \"subscription_details\",\n                SubscriptionDetails = new InvoiceParentSubscriptionDetails\n                {\n                    SubscriptionId = subscriptionId\n                }\n            }\n        };\n\n        _stripeEventService.GetInvoice(stripeEvent, true, Arg.Any<List<string>?>()).Returns(invoice);\n\n        var subscription = new Subscription\n        {\n            Metadata = new Dictionary<string, string> { { \"organizationId\", Guid.NewGuid().ToString() } }\n        };\n\n        _stripeFacade.GetSubscription(subscriptionId).Returns(subscription);\n\n        // Act\n        await _providerEventService.TryRecordInvoiceLineItems(stripeEvent);\n\n        // Assert\n        await _providerOrganizationRepository.DidNotReceiveWithAnyArgs().GetManyDetailsByProviderAsync(Arg.Any<Guid>());\n    }\n\n    [Fact]\n    public async Task TryRecordInvoiceLineItems_InvoiceCreated_Succeeds()\n    {\n        // Arrange\n        var stripeEvent = new Event\n        {\n            Type = \"invoice.created\"\n        };\n\n        const string subscriptionId = \"sub_1\";\n        var providerId = Guid.NewGuid();\n\n        var invoice = new Invoice\n        {\n            Id = \"invoice_1\",\n            Number = \"A\",\n            Parent = new InvoiceParent\n            {\n                Type = \"subscription_details\",\n                SubscriptionDetails = new InvoiceParentSubscriptionDetails\n                {\n                    SubscriptionId = subscriptionId\n                }\n            },\n            Discounts = [\n                new Discount\n                {\n                    Coupon = new Coupon\n                    {\n                        PercentOff = 35\n                    }\n                }\n            ]\n        };\n\n        _stripeEventService.GetInvoice(stripeEvent, true, Arg.Any<List<string>?>()).Returns(invoice);\n\n        var subscription = new Subscription\n        {\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } }\n        };\n\n        _stripeFacade.GetSubscription(subscriptionId).Returns(subscription);\n\n        var client1Id = Guid.NewGuid();\n        var client2Id = Guid.NewGuid();\n\n        var clients = new List<ProviderOrganizationOrganizationDetails>\n        {\n            new ()\n            {\n                OrganizationId = client1Id,\n                OrganizationName = \"Client 1\",\n                Plan = \"Teams (Monthly)\",\n                Seats = 50,\n                OccupiedSeats = 30,\n                Status = OrganizationStatusType.Managed\n            },\n            new ()\n            {\n                OrganizationId = client2Id,\n                OrganizationName = \"Client 2\",\n                Plan = \"Enterprise (Monthly)\",\n                Seats = 50,\n                OccupiedSeats = 30,\n                Status = OrganizationStatusType.Managed\n            }\n        };\n\n        _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId).Returns(clients);\n\n        _organizationRepository.GetByIdAsync(client1Id)\n            .Returns(new Organization { PlanType = PlanType.TeamsMonthly });\n\n        _organizationRepository.GetByIdAsync(client2Id)\n            .Returns(new Organization { PlanType = PlanType.EnterpriseMonthly });\n\n        var providerPlans = new List<ProviderPlan>\n        {\n            new ()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = providerId,\n                PlanType = PlanType.TeamsMonthly,\n                AllocatedSeats = 50,\n                PurchasedSeats = 0,\n                SeatMinimum = 100\n            },\n            new ()\n            {\n                Id = Guid.NewGuid(),\n                ProviderId = providerId,\n                PlanType = PlanType.EnterpriseMonthly,\n                AllocatedSeats = 50,\n                PurchasedSeats = 0,\n                SeatMinimum = 100\n            }\n        };\n\n        foreach (var providerPlan in providerPlans)\n        {\n            _pricingClient.GetPlanOrThrow(providerPlan.PlanType).Returns(MockPlans.Get(providerPlan.PlanType));\n        }\n\n        _providerPlanRepository.GetByProviderId(providerId).Returns(providerPlans);\n\n        // Act\n        await _providerEventService.TryRecordInvoiceLineItems(stripeEvent);\n\n        // Assert\n        var teamsPlan = MockPlans.Get(PlanType.TeamsMonthly);\n        var enterprisePlan = MockPlans.Get(PlanType.EnterpriseMonthly);\n\n        await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(\n            options =>\n                options.ProviderId == providerId &&\n                options.InvoiceId == invoice.Id &&\n                options.InvoiceNumber == invoice.Number &&\n                options.ClientName == \"Client 1\" &&\n                options.ClientId == client1Id &&\n                options.PlanName == \"Teams (Monthly)\" &&\n                options.AssignedSeats == 50 &&\n                options.UsedSeats == 30 &&\n                options.Total == options.AssignedSeats * teamsPlan.PasswordManager.ProviderPortalSeatPrice * 0.65M));\n\n        await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(\n            options =>\n                options.ProviderId == providerId &&\n                options.InvoiceId == invoice.Id &&\n                options.InvoiceNumber == invoice.Number &&\n                options.ClientName == \"Client 2\" &&\n                options.ClientId == client2Id &&\n                options.PlanName == \"Enterprise (Monthly)\" &&\n                options.AssignedSeats == 50 &&\n                options.UsedSeats == 30 &&\n                options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.ProviderPortalSeatPrice * 0.65M));\n\n        await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(\n            options =>\n                options.ProviderId == providerId &&\n                options.InvoiceId == invoice.Id &&\n                options.InvoiceNumber == invoice.Number &&\n                options.ClientName == \"Unassigned seats\" &&\n                options.PlanName == \"Teams (Monthly)\" &&\n                options.AssignedSeats == 50 &&\n                options.UsedSeats == 0 &&\n                options.Total == options.AssignedSeats * teamsPlan.PasswordManager.ProviderPortalSeatPrice * 0.65M));\n\n        await _providerInvoiceItemRepository.Received(1).CreateAsync(Arg.Is<ProviderInvoiceItem>(\n            options =>\n                options.ProviderId == providerId &&\n                options.InvoiceId == invoice.Id &&\n                options.InvoiceNumber == invoice.Number &&\n                options.ClientName == \"Unassigned seats\" &&\n                options.PlanName == \"Enterprise (Monthly)\" &&\n                options.AssignedSeats == 50 &&\n                options.UsedSeats == 0 &&\n                options.Total == options.AssignedSeats * enterprisePlan.PasswordManager.ProviderPortalSeatPrice * 0.65M));\n    }\n\n    [Fact]\n    public async Task TryRecordInvoiceLineItems_InvoiceFinalized_Succeeds()\n    {\n        // Arrange\n        var stripeEvent = new Event\n        {\n            Type = \"invoice.finalized\"\n        };\n\n        const string subscriptionId = \"sub_1\";\n        var providerId = Guid.NewGuid();\n\n        var invoice = new Invoice\n        {\n            Id = \"invoice_1\",\n            Number = \"A\",\n            Parent = new InvoiceParent\n            {\n                Type = \"subscription_details\",\n                SubscriptionDetails = new InvoiceParentSubscriptionDetails\n                {\n                    SubscriptionId = subscriptionId\n                }\n            },\n        };\n\n        _stripeEventService.GetInvoice(stripeEvent, true, Arg.Any<List<string>?>()).Returns(invoice);\n\n        var subscription = new Subscription\n        {\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } }\n        };\n\n        _stripeFacade.GetSubscription(subscriptionId).Returns(subscription);\n\n        var invoiceItems = new List<ProviderInvoiceItem>\n        {\n            new ()\n            {\n                Id = Guid.NewGuid(),\n                ClientName = \"Client 1\"\n            },\n            new ()\n            {\n                Id = Guid.NewGuid(),\n                ClientName = \"Client 2\"\n            }\n        };\n\n        _providerInvoiceItemRepository.GetByInvoiceId(invoice.Id).Returns(invoiceItems);\n\n        // Act\n        await _providerEventService.TryRecordInvoiceLineItems(stripeEvent);\n\n        // Assert\n        await _providerInvoiceItemRepository.Received(2).ReplaceAsync(Arg.Is<ProviderInvoiceItem>(\n            options => options.InvoiceNumber == \"A\"));\n    }\n    #endregion\n}\n"
  },
  {
    "path": "test/Billing.Test/Services/SetupIntentSucceededHandlerTests.cs",
    "content": "﻿using Bit.Billing.Services;\nusing Bit.Billing.Services.Implementations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Repositories;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Test.Services;\n\npublic class SetupIntentSucceededHandlerTests\n{\n    private static readonly Event _mockEvent = new() { Id = \"evt_test\", Type = \"setup_intent.succeeded\" };\n    private static readonly string[] _expand = [\"payment_method\"];\n\n    private readonly ILogger<SetupIntentSucceededHandler> _logger;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IPushNotificationAdapter _pushNotificationAdapter;\n    private readonly IStripeAdapter _stripeAdapter;\n    private readonly IStripeEventService _stripeEventService;\n    private readonly SetupIntentSucceededHandler _handler;\n\n    public SetupIntentSucceededHandlerTests()\n    {\n        _logger = Substitute.For<ILogger<SetupIntentSucceededHandler>>();\n        _organizationRepository = Substitute.For<IOrganizationRepository>();\n        _providerRepository = Substitute.For<IProviderRepository>();\n        _pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();\n        _stripeAdapter = Substitute.For<IStripeAdapter>();\n        _stripeEventService = Substitute.For<IStripeEventService>();\n\n        _handler = new SetupIntentSucceededHandler(\n            _logger,\n            _organizationRepository,\n            _providerRepository,\n            _pushNotificationAdapter,\n            _stripeAdapter,\n            _stripeEventService);\n    }\n\n    [Fact]\n    public async Task HandleAsync_PaymentMethodNotUSBankAccount_Returns()\n    {\n        // Arrange\n        var setupIntent = CreateSetupIntent(hasUSBankAccount: false);\n\n        _stripeEventService.GetSetupIntent(\n                _mockEvent,\n                true,\n                Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))\n            .Returns(setupIntent);\n\n        // Act\n        await _handler.HandleAsync(_mockEvent);\n\n        // Assert\n        await _organizationRepository.DidNotReceiveWithAnyArgs().GetByGatewayCustomerIdAsync(Arg.Any<string>());\n        await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(\n            Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_NoCustomerIdOnSetupIntent_Returns()\n    {\n        // Arrange\n        var setupIntent = CreateSetupIntent(customerId: null);\n\n        _stripeEventService.GetSetupIntent(\n                _mockEvent,\n                true,\n                Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))\n            .Returns(setupIntent);\n\n        // Act\n        await _handler.HandleAsync(_mockEvent);\n\n        // Assert\n        await _organizationRepository.DidNotReceiveWithAnyArgs().GetByGatewayCustomerIdAsync(Arg.Any<string>());\n        await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(\n            Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_NoOrganizationOrProviderFound_LogsErrorAndReturns()\n    {\n        // Arrange\n        var customerId = \"cus_test\";\n        var setupIntent = CreateSetupIntent(customerId: customerId);\n\n        _stripeEventService.GetSetupIntent(\n                _mockEvent,\n                true,\n                Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))\n            .Returns(setupIntent);\n\n        _organizationRepository.GetByGatewayCustomerIdAsync(customerId)\n            .Returns((Organization?)null);\n\n        _providerRepository.GetByGatewayCustomerIdAsync(customerId)\n            .Returns((Provider?)null);\n\n        // Act\n        await _handler.HandleAsync(_mockEvent);\n\n        // Assert\n        await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(\n            Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_ValidOrganization_AttachesPaymentMethodAndSendsNotification()\n    {\n        // Arrange\n        var customerId = \"cus_test\";\n        var organization = new Organization { Id = Guid.NewGuid(), Name = \"Test Org\", GatewayCustomerId = customerId };\n        var setupIntent = CreateSetupIntent(customerId: customerId);\n\n        _stripeEventService.GetSetupIntent(\n                _mockEvent,\n                true,\n                Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))\n            .Returns(setupIntent);\n\n        _organizationRepository.GetByGatewayCustomerIdAsync(customerId)\n            .Returns(organization);\n\n        // Act\n        await _handler.HandleAsync(_mockEvent);\n\n        // Assert\n        await _stripeAdapter.Received(1).AttachPaymentMethodAsync(\n            \"pm_test\",\n            Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == organization.GatewayCustomerId));\n\n        await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(organization);\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());\n\n        // Provider should not be queried when organization is found\n        await _providerRepository.DidNotReceiveWithAnyArgs().GetByGatewayCustomerIdAsync(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_ValidProvider_AttachesPaymentMethodAndSendsNotification()\n    {\n        // Arrange\n        var customerId = \"cus_test\";\n        var provider = new Provider { Id = Guid.NewGuid(), Name = \"Test Provider\", GatewayCustomerId = customerId };\n        var setupIntent = CreateSetupIntent(customerId: customerId);\n\n        _stripeEventService.GetSetupIntent(\n                _mockEvent,\n                true,\n                Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))\n            .Returns(setupIntent);\n\n        _organizationRepository.GetByGatewayCustomerIdAsync(customerId)\n            .Returns((Organization?)null);\n\n        _providerRepository.GetByGatewayCustomerIdAsync(customerId)\n            .Returns(provider);\n\n        // Act\n        await _handler.HandleAsync(_mockEvent);\n\n        // Assert\n        await _stripeAdapter.Received(1).AttachPaymentMethodAsync(\n            \"pm_test\",\n            Arg.Is<PaymentMethodAttachOptions>(o => o.Customer == provider.GatewayCustomerId));\n\n        await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(provider);\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_OrganizationWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()\n    {\n        // Arrange\n        var customerId = \"cus_test\";\n        var organization = new Organization { Id = Guid.NewGuid(), Name = \"Test Org\", GatewayCustomerId = null };\n        var setupIntent = CreateSetupIntent(customerId: customerId);\n\n        _stripeEventService.GetSetupIntent(\n                _mockEvent,\n                true,\n                Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))\n            .Returns(setupIntent);\n\n        _organizationRepository.GetByGatewayCustomerIdAsync(customerId)\n            .Returns(organization);\n\n        // Act\n        await _handler.HandleAsync(_mockEvent);\n\n        // Assert\n        await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(\n            Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_ProviderWithoutGatewayCustomerId_DoesNotAttachPaymentMethod()\n    {\n        // Arrange\n        var customerId = \"cus_test\";\n        var provider = new Provider { Id = Guid.NewGuid(), Name = \"Test Provider\", GatewayCustomerId = null };\n        var setupIntent = CreateSetupIntent(customerId: customerId);\n\n        _stripeEventService.GetSetupIntent(\n                _mockEvent,\n                true,\n                Arg.Is<List<string>>(options => options.SequenceEqual(_expand)))\n            .Returns(setupIntent);\n\n        _organizationRepository.GetByGatewayCustomerIdAsync(customerId)\n            .Returns((Organization?)null);\n\n        _providerRepository.GetByGatewayCustomerIdAsync(customerId)\n            .Returns(provider);\n\n        // Act\n        await _handler.HandleAsync(_mockEvent);\n\n        // Assert\n        await _stripeAdapter.DidNotReceiveWithAnyArgs().AttachPaymentMethodAsync(\n            Arg.Any<string>(), Arg.Any<PaymentMethodAttachOptions>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Organization>());\n        await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any<Provider>());\n    }\n\n    private static SetupIntent CreateSetupIntent(bool hasUSBankAccount = true, string? customerId = \"cus_default\")\n    {\n        var paymentMethod = new PaymentMethod\n        {\n            Id = \"pm_test\",\n            Type = \"us_bank_account\",\n            UsBankAccount = hasUSBankAccount ? new PaymentMethodUsBankAccount() : null\n        };\n\n        var setupIntent = new SetupIntent\n        {\n            Id = \"seti_test\",\n            CustomerId = customerId,\n            PaymentMethod = paymentMethod\n        };\n\n        return setupIntent;\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Services/StripeEventServiceTests.cs",
    "content": "﻿using Bit.Billing.Services;\nusing Bit.Billing.Services.Implementations;\nusing Bit.Core.Settings;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Billing.Test.Services;\n\npublic class StripeEventServiceTests\n{\n    private readonly IStripeFacade _stripeFacade;\n    private readonly StripeEventService _stripeEventService;\n\n    public StripeEventServiceTests()\n    {\n        var globalSettings = new GlobalSettings();\n        var baseServiceUriSettings = new GlobalSettings.BaseServiceUriSettings(globalSettings) { CloudRegion = \"US\" };\n        globalSettings.BaseServiceUri = baseServiceUriSettings;\n\n        _stripeFacade = Substitute.For<IStripeFacade>();\n        _stripeEventService = new StripeEventService(\n            globalSettings,\n            _stripeFacade);\n    }\n\n    #region GetCharge\n    [Fact]\n    public async Task GetCharge_EventNotChargeRelated_ThrowsException()\n    {\n        // Arrange\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"invoice.created\", new Invoice { Id = \"in_test\" });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetCharge(stripeEvent));\n        Assert.Equal($\"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'\", exception.Message);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(\n            Arg.Any<string>(),\n            Arg.Any<ChargeGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetCharge_NotFresh_ReturnsEventCharge()\n    {\n        // Arrange\n        var mockCharge = new Charge { Id = \"ch_test\", Amount = 1000 };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"charge.succeeded\", mockCharge);\n\n        // Act\n        var charge = await _stripeEventService.GetCharge(stripeEvent);\n\n        // Assert\n        Assert.Equal(mockCharge.Id, charge.Id);\n        Assert.Equal(mockCharge.Amount, charge.Amount);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(\n            Arg.Any<string>(),\n            Arg.Any<ChargeGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetCharge_Fresh_Expand_ReturnsAPICharge()\n    {\n        // Arrange\n        var eventCharge = new Charge { Id = \"ch_test\", Amount = 1000 };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"charge.succeeded\", eventCharge);\n\n        var apiCharge = new Charge { Id = \"ch_test\", Amount = 2000 };\n\n        var expand = new List<string> { \"customer\" };\n\n        _stripeFacade.GetCharge(\n                apiCharge.Id,\n                Arg.Is<ChargeGetOptions>(options => options.Expand == expand))\n            .Returns(apiCharge);\n\n        // Act\n        var charge = await _stripeEventService.GetCharge(stripeEvent, true, expand);\n\n        // Assert\n        Assert.Equal(apiCharge, charge);\n        Assert.NotSame(eventCharge, charge);\n\n        await _stripeFacade.Received().GetCharge(\n            apiCharge.Id,\n            Arg.Is<ChargeGetOptions>(options => options.Expand == expand));\n    }\n    #endregion\n\n    #region GetCustomer\n    [Fact]\n    public async Task GetCustomer_EventNotCustomerRelated_ThrowsException()\n    {\n        // Arrange\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"invoice.created\", new Invoice { Id = \"in_test\" });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetCustomer(stripeEvent));\n        Assert.Equal($\"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'\", exception.Message);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(\n            Arg.Any<string>(),\n            Arg.Any<CustomerGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetCustomer_NotFresh_ReturnsEventCustomer()\n    {\n        // Arrange\n        var mockCustomer = new Customer { Id = \"cus_test\", Email = \"test@example.com\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.updated\", mockCustomer);\n\n        // Act\n        var customer = await _stripeEventService.GetCustomer(stripeEvent);\n\n        // Assert\n        Assert.Equal(mockCustomer.Id, customer.Id);\n        Assert.Equal(mockCustomer.Email, customer.Email);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(\n            Arg.Any<string>(),\n            Arg.Any<CustomerGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetCustomer_Fresh_Expand_ReturnsAPICustomer()\n    {\n        // Arrange\n        var eventCustomer = new Customer { Id = \"cus_test\", Email = \"test@example.com\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.updated\", eventCustomer);\n\n        var apiCustomer = new Customer { Id = \"cus_test\", Email = \"updated@example.com\" };\n\n        var expand = new List<string> { \"subscriptions\" };\n\n        _stripeFacade.GetCustomer(\n                apiCustomer.Id,\n                Arg.Is<CustomerGetOptions>(options => options.Expand == expand))\n            .Returns(apiCustomer);\n\n        // Act\n        var customer = await _stripeEventService.GetCustomer(stripeEvent, true, expand);\n\n        // Assert\n        Assert.Equal(apiCustomer, customer);\n        Assert.NotSame(eventCustomer, customer);\n\n        await _stripeFacade.Received().GetCustomer(\n            apiCustomer.Id,\n            Arg.Is<CustomerGetOptions>(options => options.Expand == expand));\n    }\n    #endregion\n\n    #region GetInvoice\n    [Fact]\n    public async Task GetInvoice_EventNotInvoiceRelated_ThrowsException()\n    {\n        // Arrange\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.updated\", new Customer { Id = \"cus_test\" });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetInvoice(stripeEvent));\n        Assert.Equal($\"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'\", exception.Message);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(\n            Arg.Any<string>(),\n            Arg.Any<InvoiceGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetInvoice_NotFresh_ReturnsEventInvoice()\n    {\n        // Arrange\n        var mockInvoice = new Invoice { Id = \"in_test\", AmountDue = 1000 };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"invoice.created\", mockInvoice);\n\n        // Act\n        var invoice = await _stripeEventService.GetInvoice(stripeEvent);\n\n        // Assert\n        Assert.Equal(mockInvoice.Id, invoice.Id);\n        Assert.Equal(mockInvoice.AmountDue, invoice.AmountDue);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(\n            Arg.Any<string>(),\n            Arg.Any<InvoiceGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetInvoice_Fresh_Expand_ReturnsAPIInvoice()\n    {\n        // Arrange\n        var eventInvoice = new Invoice { Id = \"in_test\", AmountDue = 1000 };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"invoice.created\", eventInvoice);\n\n        var apiInvoice = new Invoice { Id = \"in_test\", AmountDue = 2000 };\n\n        var expand = new List<string> { \"customer\" };\n\n        _stripeFacade.GetInvoice(\n                apiInvoice.Id,\n                Arg.Is<InvoiceGetOptions>(options => options.Expand == expand))\n            .Returns(apiInvoice);\n\n        // Act\n        var invoice = await _stripeEventService.GetInvoice(stripeEvent, true, expand);\n\n        // Assert\n        Assert.Equal(apiInvoice, invoice);\n        Assert.NotSame(eventInvoice, invoice);\n\n        await _stripeFacade.Received().GetInvoice(\n            apiInvoice.Id,\n            Arg.Is<InvoiceGetOptions>(options => options.Expand == expand));\n    }\n    #endregion\n\n    #region GetPaymentMethod\n    [Fact]\n    public async Task GetPaymentMethod_EventNotPaymentMethodRelated_ThrowsException()\n    {\n        // Arrange\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.updated\", new Customer { Id = \"cus_test\" });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetPaymentMethod(stripeEvent));\n        Assert.Equal($\"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'\", exception.Message);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(\n            Arg.Any<string>(),\n            Arg.Any<PaymentMethodGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetPaymentMethod_NotFresh_ReturnsEventPaymentMethod()\n    {\n        // Arrange\n        var mockPaymentMethod = new PaymentMethod { Id = \"pm_test\", Type = \"card\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"payment_method.attached\", mockPaymentMethod);\n\n        // Act\n        var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent);\n\n        // Assert\n        Assert.Equal(mockPaymentMethod.Id, paymentMethod.Id);\n        Assert.Equal(mockPaymentMethod.Type, paymentMethod.Type);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(\n            Arg.Any<string>(),\n            Arg.Any<PaymentMethodGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetPaymentMethod_Fresh_Expand_ReturnsAPIPaymentMethod()\n    {\n        // Arrange\n        var eventPaymentMethod = new PaymentMethod { Id = \"pm_test\", Type = \"card\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"payment_method.attached\", eventPaymentMethod);\n\n        var apiPaymentMethod = new PaymentMethod { Id = \"pm_test\", Type = \"card\" };\n\n        var expand = new List<string> { \"customer\" };\n\n        _stripeFacade.GetPaymentMethod(\n                apiPaymentMethod.Id,\n                Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand))\n            .Returns(apiPaymentMethod);\n\n        // Act\n        var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent, true, expand);\n\n        // Assert\n        Assert.Equal(apiPaymentMethod, paymentMethod);\n        Assert.NotSame(eventPaymentMethod, paymentMethod);\n\n        await _stripeFacade.Received().GetPaymentMethod(\n            apiPaymentMethod.Id,\n            Arg.Is<PaymentMethodGetOptions>(options => options.Expand == expand));\n    }\n    #endregion\n\n    #region GetSubscription\n    [Fact]\n    public async Task GetSubscription_EventNotSubscriptionRelated_ThrowsException()\n    {\n        // Arrange\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.updated\", new Customer { Id = \"cus_test\" });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetSubscription(stripeEvent));\n        Assert.Equal($\"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'\", exception.Message);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(\n            Arg.Any<string>(),\n            Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetSubscription_NotFresh_ReturnsEventSubscription()\n    {\n        // Arrange\n        var mockSubscription = new Subscription { Id = \"sub_test\", Status = \"active\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.subscription.updated\", mockSubscription);\n\n        // Act\n        var subscription = await _stripeEventService.GetSubscription(stripeEvent);\n\n        // Assert\n        Assert.Equal(mockSubscription.Id, subscription.Id);\n        Assert.Equal(mockSubscription.Status, subscription.Status);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(\n            Arg.Any<string>(),\n            Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetSubscription_Fresh_Expand_ReturnsAPISubscription()\n    {\n        // Arrange\n        var eventSubscription = new Subscription { Id = \"sub_test\", Status = \"active\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.subscription.updated\", eventSubscription);\n\n        var apiSubscription = new Subscription { Id = \"sub_test\", Status = \"canceled\" };\n\n        var expand = new List<string> { \"customer\" };\n\n        _stripeFacade.GetSubscription(\n                apiSubscription.Id,\n                Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand))\n            .Returns(apiSubscription);\n\n        // Act\n        var subscription = await _stripeEventService.GetSubscription(stripeEvent, true, expand);\n\n        // Assert\n        Assert.Equal(apiSubscription, subscription);\n        Assert.NotSame(eventSubscription, subscription);\n\n        await _stripeFacade.Received().GetSubscription(\n            apiSubscription.Id,\n            Arg.Is<SubscriptionGetOptions>(options => options.Expand == expand));\n    }\n    #endregion\n\n    #region GetSetupIntent\n    [Fact]\n    public async Task GetSetupIntent_EventNotSetupIntentRelated_ThrowsException()\n    {\n        // Arrange\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.updated\", new Customer { Id = \"cus_test\" });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<Exception>(async () => await _stripeEventService.GetSetupIntent(stripeEvent));\n        Assert.Equal($\"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(SetupIntent)}'\", exception.Message);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent(\n            Arg.Any<string>(),\n            Arg.Any<SetupIntentGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetSetupIntent_NotFresh_ReturnsEventSetupIntent()\n    {\n        // Arrange\n        var mockSetupIntent = new SetupIntent { Id = \"seti_test\", Status = \"succeeded\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"setup_intent.succeeded\", mockSetupIntent);\n\n        // Act\n        var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent);\n\n        // Assert\n        Assert.Equal(mockSetupIntent.Id, setupIntent.Id);\n        Assert.Equal(mockSetupIntent.Status, setupIntent.Status);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent(\n            Arg.Any<string>(),\n            Arg.Any<SetupIntentGetOptions>());\n    }\n\n    [Fact]\n    public async Task GetSetupIntent_Fresh_Expand_ReturnsAPISetupIntent()\n    {\n        // Arrange\n        var eventSetupIntent = new SetupIntent { Id = \"seti_test\", Status = \"succeeded\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"setup_intent.succeeded\", eventSetupIntent);\n\n        var apiSetupIntent = new SetupIntent { Id = \"seti_test\", Status = \"requires_action\" };\n\n        var expand = new List<string> { \"customer\" };\n\n        _stripeFacade.GetSetupIntent(\n                apiSetupIntent.Id,\n                Arg.Is<SetupIntentGetOptions>(options => options.Expand == expand))\n            .Returns(apiSetupIntent);\n\n        // Act\n        var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent, true, expand);\n\n        // Assert\n        Assert.Equal(apiSetupIntent, setupIntent);\n        Assert.NotSame(eventSetupIntent, setupIntent);\n\n        await _stripeFacade.Received().GetSetupIntent(\n            apiSetupIntent.Id,\n            Arg.Is<SetupIntentGetOptions>(options => options.Expand == expand));\n    }\n    #endregion\n\n    #region ValidateCloudRegion\n    [Fact]\n    public async Task ValidateCloudRegion_SubscriptionUpdated_Success()\n    {\n        // Arrange\n        var mockSubscription = new Subscription { Id = \"sub_test\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.subscription.updated\", mockSubscription);\n\n        var customer = CreateMockCustomer();\n        mockSubscription.Customer = customer;\n\n        _stripeFacade.GetSubscription(\n                mockSubscription.Id,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(mockSubscription);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetSubscription(\n            mockSubscription.Id,\n            Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_ChargeSucceeded_Success()\n    {\n        // Arrange\n        var mockCharge = new Charge { Id = \"ch_test\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"charge.succeeded\", mockCharge);\n\n        var customer = CreateMockCustomer();\n        mockCharge.Customer = customer;\n\n        _stripeFacade.GetCharge(\n                mockCharge.Id,\n                Arg.Any<ChargeGetOptions>())\n            .Returns(mockCharge);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetCharge(\n            mockCharge.Id,\n            Arg.Any<ChargeGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_UpcomingInvoice_Success()\n    {\n        // Arrange\n        var mockInvoice = new Invoice { Id = \"in_test\", CustomerId = \"cus_test\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"invoice.upcoming\", mockInvoice);\n\n        var customer = CreateMockCustomer();\n\n        _stripeFacade.GetCustomer(\n                mockInvoice.CustomerId,\n                Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetCustomer(\n            mockInvoice.CustomerId,\n            Arg.Any<CustomerGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_InvoiceCreated_Success()\n    {\n        // Arrange\n        var mockInvoice = new Invoice { Id = \"in_test\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"invoice.created\", mockInvoice);\n\n        var customer = CreateMockCustomer();\n        mockInvoice.Customer = customer;\n\n        _stripeFacade.GetInvoice(\n                mockInvoice.Id,\n                Arg.Any<InvoiceGetOptions>())\n            .Returns(mockInvoice);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetInvoice(\n            mockInvoice.Id,\n            Arg.Any<InvoiceGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_PaymentMethodAttached_Success()\n    {\n        // Arrange\n        var mockPaymentMethod = new PaymentMethod { Id = \"pm_test\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"payment_method.attached\", mockPaymentMethod);\n\n        var customer = CreateMockCustomer();\n        mockPaymentMethod.Customer = customer;\n\n        _stripeFacade.GetPaymentMethod(\n                mockPaymentMethod.Id,\n                Arg.Any<PaymentMethodGetOptions>())\n            .Returns(mockPaymentMethod);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetPaymentMethod(\n            mockPaymentMethod.Id,\n            Arg.Any<PaymentMethodGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_CustomerUpdated_Success()\n    {\n        // Arrange\n        var mockCustomer = CreateMockCustomer();\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.updated\", mockCustomer);\n\n        _stripeFacade.GetCustomer(\n                mockCustomer.Id,\n                Arg.Any<CustomerGetOptions>())\n            .Returns(mockCustomer);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetCustomer(\n            mockCustomer.Id,\n            Arg.Any<CustomerGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_MetadataNull_ReturnsFalse()\n    {\n        // Arrange\n        var mockSubscription = new Subscription { Id = \"sub_test\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.subscription.updated\", mockSubscription);\n\n        var customer = new Customer { Id = \"cus_test\", Metadata = null };\n        mockSubscription.Customer = customer;\n\n        _stripeFacade.GetSubscription(\n                mockSubscription.Id,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(mockSubscription);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.False(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetSubscription(\n            mockSubscription.Id,\n            Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_MetadataNoRegion_DefaultUS_ReturnsTrue()\n    {\n        // Arrange\n        var mockSubscription = new Subscription { Id = \"sub_test\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.subscription.updated\", mockSubscription);\n\n        var customer = new Customer { Id = \"cus_test\", Metadata = new Dictionary<string, string>() };\n        mockSubscription.Customer = customer;\n\n        _stripeFacade.GetSubscription(\n                mockSubscription.Id,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(mockSubscription);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetSubscription(\n            mockSubscription.Id,\n            Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_MetadataIncorrectlyCasedRegion_ReturnsTrue()\n    {\n        // Arrange\n        var mockSubscription = new Subscription { Id = \"sub_test\" };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"customer.subscription.updated\", mockSubscription);\n\n        var customer = new Customer\n        {\n            Id = \"cus_test\",\n            Metadata = new Dictionary<string, string> { { \"Region\", \"US\" } }\n        };\n        mockSubscription.Customer = customer;\n\n        _stripeFacade.GetSubscription(\n                mockSubscription.Id,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(mockSubscription);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetSubscription(\n            mockSubscription.Id,\n            Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_SetupIntentSucceeded_WithCustomer_Success()\n    {\n        // Arrange\n        var customer = CreateMockCustomer();\n        var mockSetupIntent = new SetupIntent { Id = \"seti_test\", Customer = customer };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"setup_intent.succeeded\", mockSetupIntent);\n\n        _stripeFacade.GetSetupIntent(\n                mockSetupIntent.Id,\n                Arg.Any<SetupIntentGetOptions>())\n            .Returns(mockSetupIntent);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetSetupIntent(\n            mockSetupIntent.Id,\n            Arg.Any<SetupIntentGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_SetupIntentSucceeded_NoCustomer_ReturnsFalse()\n    {\n        // Arrange\n        var mockSetupIntent = new SetupIntent { Id = \"seti_test\", Customer = null };\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"setup_intent.succeeded\", mockSetupIntent);\n\n        _stripeFacade.GetSetupIntent(\n                mockSetupIntent.Id,\n                Arg.Any<SetupIntentGetOptions>())\n            .Returns(mockSetupIntent);\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.False(cloudRegionValid);\n\n        await _stripeFacade.Received(1).GetSetupIntent(\n            mockSetupIntent.Id,\n            Arg.Any<SetupIntentGetOptions>());\n    }\n\n    [Fact]\n    public async Task ValidateCloudRegion_CouponDeleted_ReturnsTrue()\n    {\n        // Arrange\n        var stripeEvent = CreateMockEvent(\"evt_test\", \"coupon.deleted\", new Coupon { Id = \"cou_test\" });\n\n        // Act\n        var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent);\n\n        // Assert\n        Assert.True(cloudRegionValid);\n\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(null);\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge(null);\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice(null);\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod(null);\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer(null);\n        await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent(null);\n    }\n    #endregion\n\n    private static Event CreateMockEvent<T>(string id, string type, T dataObject) where T : IStripeEntity\n    {\n        return new Event\n        {\n            Id = id,\n            Type = type,\n            Data = new EventData\n            {\n                Object = (IHasObject)dataObject\n            }\n        };\n    }\n\n    private static Customer CreateMockCustomer()\n    {\n        return new Customer\n        {\n            Id = \"cus_test\",\n            Metadata = new Dictionary<string, string> { { \"region\", \"US\" } }\n        };\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Services/SubscriptionDeletedHandlerTests.cs",
    "content": "﻿using Bit.Billing.Constants;\nusing Bit.Billing.Jobs;\nusing Bit.Billing.Services;\nusing Bit.Billing.Services.Implementations;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing NSubstitute;\nusing Quartz;\nusing Stripe;\nusing Xunit;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Test.Services;\n\npublic class SubscriptionDeletedHandlerTests\n{\n    private readonly IStripeEventService _stripeEventService;\n    private readonly IUserService _userService;\n    private readonly IUserRepository _userRepository;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n    private readonly IOrganizationDisableCommand _organizationDisableCommand;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderService _providerService;\n    private readonly ISchedulerFactory _schedulerFactory;\n    private readonly IPushNotificationAdapter _pushNotificationAdapter;\n    private readonly IScheduler _scheduler;\n    private readonly SubscriptionDeletedHandler _sut;\n\n    public SubscriptionDeletedHandlerTests()\n    {\n        _stripeEventService = Substitute.For<IStripeEventService>();\n        _userService = Substitute.For<IUserService>();\n        _userRepository = Substitute.For<IUserRepository>();\n        _stripeEventUtilityService = Substitute.For<IStripeEventUtilityService>();\n        _organizationDisableCommand = Substitute.For<IOrganizationDisableCommand>();\n        _providerRepository = Substitute.For<IProviderRepository>();\n        _providerService = Substitute.For<IProviderService>();\n        _schedulerFactory = Substitute.For<ISchedulerFactory>();\n        _pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();\n        _scheduler = Substitute.For<IScheduler>();\n        _schedulerFactory.GetScheduler().Returns(_scheduler);\n        _sut = new SubscriptionDeletedHandler(\n            _stripeEventService,\n            _userService,\n            _userRepository,\n            _stripeEventUtilityService,\n            _organizationDisableCommand,\n            _providerRepository,\n            _providerService,\n            _schedulerFactory,\n            _pushNotificationAdapter);\n    }\n\n    [Fact]\n    public async Task HandleAsync_SubscriptionNotCanceled_DoesNothing()\n    {\n        // Arrange\n        var stripeEvent = new Event();\n        var subscription = new Subscription\n        {\n            Status = \"active\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, null));\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        await _organizationDisableCommand.DidNotReceiveWithAnyArgs().DisableAsync(default, default);\n        await _userService.DidNotReceiveWithAnyArgs().DisablePremiumAsync(default, default);\n        await _providerService.DidNotReceiveWithAnyArgs().UpdateAsync(default);\n    }\n\n    [Fact]\n    public async Task HandleAsync_OrganizationSubscriptionCanceled_DisablesOrganization()\n    {\n        // Arrange\n        var stripeEvent = new Event();\n        var organizationId = Guid.NewGuid();\n        var subscription = new Subscription\n        {\n            Status = StripeSubscriptionStatus.Canceled,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"organizationId\", organizationId.ToString() } }\n        };\n\n        _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(organizationId, null, null));\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        await _organizationDisableCommand.Received(1)\n            .DisableAsync(organizationId, subscription.GetCurrentPeriodEnd());\n    }\n\n    [Fact]\n    public async Task HandleAsync_UserSubscriptionCanceled_DisablesUserPremium()\n    {\n        // Arrange\n        var stripeEvent = new Event();\n        var userId = Guid.NewGuid();\n        var subscription = new Subscription\n        {\n            Status = StripeSubscriptionStatus.Canceled,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"userId\", userId.ToString() } }\n        };\n\n        var user = new User { Id = userId, Premium = false, PremiumExpirationDate = subscription.GetCurrentPeriodEnd() };\n\n        _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n        _userRepository.GetByIdAsync(userId).Returns(user);\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        await _userService.Received(1)\n            .DisablePremiumAsync(userId, subscription.GetCurrentPeriodEnd());\n        await _userRepository.Received(1).GetByIdAsync(userId);\n        await _pushNotificationAdapter.Received(1).NotifyPremiumStatusChangedAsync(user);\n    }\n\n    [Fact]\n    public async Task HandleAsync_ProviderMigrationCancellation_DoesNotDisableOrganization()\n    {\n        // Arrange\n        var stripeEvent = new Event();\n        var organizationId = Guid.NewGuid();\n        var subscription = new Subscription\n        {\n            Status = StripeSubscriptionStatus.Canceled,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"organizationId\", organizationId.ToString() } },\n            CancellationDetails = new SubscriptionCancellationDetails\n            {\n                Comment = \"Cancelled as part of provider migration to Consolidated Billing\"\n            }\n        };\n\n        _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(organizationId, null, null));\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        await _organizationDisableCommand.DidNotReceiveWithAnyArgs()\n            .DisableAsync(default, default);\n    }\n\n    [Fact]\n    public async Task HandleAsync_AddedToProviderCancellation_DoesNotDisableOrganization()\n    {\n        // Arrange\n        var stripeEvent = new Event();\n        var organizationId = Guid.NewGuid();\n        var subscription = new Subscription\n        {\n            Status = StripeSubscriptionStatus.Canceled,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"organizationId\", organizationId.ToString() } },\n            CancellationDetails = new SubscriptionCancellationDetails\n            {\n                Comment = \"Organization was added to Provider\"\n            }\n        };\n\n        _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(organizationId, null, null));\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        await _organizationDisableCommand.DidNotReceiveWithAnyArgs()\n            .DisableAsync(default, default);\n    }\n\n    [Fact]\n    public async Task HandleAsync_ProviderSubscriptionCanceled_DisablesProviderAndQueuesJob()\n    {\n        // Arrange\n        var stripeEvent = new Event();\n        var providerId = Guid.NewGuid();\n        var provider = new Provider\n        {\n            Id = providerId,\n            Enabled = true\n        };\n        var subscription = new Subscription\n        {\n            Status = StripeSubscriptionStatus.Canceled,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } }\n        };\n\n        _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));\n        _providerRepository.GetByIdAsync(providerId).Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        Assert.False(provider.Enabled);\n        await _providerService.Received(1).UpdateAsync(provider);\n        await _scheduler.Received(1).ScheduleJob(\n            Arg.Is<IJobDetail>(j => j.JobType == typeof(ProviderOrganizationDisableJob)),\n            Arg.Any<ITrigger>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_ProviderSubscriptionCanceled_ProviderNotFound_DoesNotThrow()\n    {\n        // Arrange\n        var stripeEvent = new Event();\n        var providerId = Guid.NewGuid();\n        var subscription = new Subscription\n        {\n            Status = StripeSubscriptionStatus.Canceled,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } }\n        };\n\n        _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));\n        _providerRepository.GetByIdAsync(providerId).Returns((Provider)null);\n\n        // Act & Assert - Should not throw\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        await _providerService.DidNotReceiveWithAnyArgs().UpdateAsync(default);\n        await _scheduler.DidNotReceiveWithAnyArgs().ScheduleJob(default, default);\n    }\n\n    [Fact]\n    public async Task HandleAsync_ProviderSubscriptionCanceled_QueuesJobWithCorrectParameters()\n    {\n        // Arrange\n        var stripeEvent = new Event();\n        var providerId = Guid.NewGuid();\n        var expirationDate = DateTime.UtcNow.AddDays(30);\n        var provider = new Provider\n        {\n            Id = providerId,\n            Enabled = true\n        };\n        var subscription = new Subscription\n        {\n            Status = StripeSubscriptionStatus.Canceled,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = expirationDate }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } }\n        };\n\n        _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));\n        _providerRepository.GetByIdAsync(providerId).Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(stripeEvent);\n\n        // Assert\n        Assert.False(provider.Enabled);\n        await _providerService.Received(1).UpdateAsync(provider);\n        await _scheduler.Received(1).ScheduleJob(\n            Arg.Is<IJobDetail>(j =>\n                j.JobType == typeof(ProviderOrganizationDisableJob) &&\n                j.JobDataMap.GetString(\"providerId\") == providerId.ToString() &&\n                j.JobDataMap.GetString(\"expirationDate\") == expirationDate.ToString(\"O\")),\n            Arg.Is<ITrigger>(t => t.Key.Name == $\"disable-trigger-{providerId}\"));\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs",
    "content": "﻿using Bit.Billing.Services;\nusing Bit.Billing.Services.Implementations;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Entities;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Newtonsoft.Json.Linq;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\nusing static Bit.Core.Billing.Constants.StripeConstants;\nusing Event = Stripe.Event;\n\nnamespace Bit.Billing.Test.Services;\n\npublic class SubscriptionUpdatedHandlerTests\n{\n    private readonly IStripeEventService _stripeEventService;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n    private readonly IOrganizationService _organizationService;\n    private readonly IStripeFacade _stripeFacade;\n    private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand;\n    private readonly IUserService _userService;\n    private readonly IUserRepository _userRepository;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IOrganizationEnableCommand _organizationEnableCommand;\n    private readonly IOrganizationDisableCommand _organizationDisableCommand;\n    private readonly IPricingClient _pricingClient;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IProviderService _providerService;\n    private readonly IPushNotificationAdapter _pushNotificationAdapter;\n    private readonly SubscriptionUpdatedHandler _sut;\n\n    public SubscriptionUpdatedHandlerTests()\n    {\n        _stripeEventService = Substitute.For<IStripeEventService>();\n        _stripeEventUtilityService = Substitute.For<IStripeEventUtilityService>();\n        _organizationService = Substitute.For<IOrganizationService>();\n        _stripeFacade = Substitute.For<IStripeFacade>();\n        _organizationSponsorshipRenewCommand = Substitute.For<IOrganizationSponsorshipRenewCommand>();\n        _userService = Substitute.For<IUserService>();\n        _userRepository = Substitute.For<IUserRepository>();\n        _providerService = Substitute.For<IProviderService>();\n        _organizationRepository = Substitute.For<IOrganizationRepository>();\n        _organizationEnableCommand = Substitute.For<IOrganizationEnableCommand>();\n        _organizationDisableCommand = Substitute.For<IOrganizationDisableCommand>();\n        _pricingClient = Substitute.For<IPricingClient>();\n        _providerRepository = Substitute.For<IProviderRepository>();\n        _providerService = Substitute.For<IProviderService>();\n        _pushNotificationAdapter = Substitute.For<IPushNotificationAdapter>();\n\n        _sut = new SubscriptionUpdatedHandler(\n            _stripeEventService,\n            _stripeEventUtilityService,\n            _organizationService,\n            _stripeFacade,\n            _organizationSponsorshipRenewCommand,\n            _userService,\n            _userRepository,\n            _organizationRepository,\n            _organizationEnableCommand,\n            _organizationDisableCommand,\n            _pricingClient,\n            _providerRepository,\n            _providerService,\n            _pushNotificationAdapter);\n    }\n\n    [Fact]\n    public async Task HandleAsync_UnpaidOrganizationSubscription_DisablesOrganizationAndSetsCancellation()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Active\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Unpaid,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = currentPeriodEnd,\n                        Plan = new Plan { Id = \"2023-enterprise-org-seat-annually\" }\n                    }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"organizationId\", organizationId.ToString() } },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCycle }\n        };\n\n        var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var plan = new Enterprise2023Plan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n        _pricingClient.ListPlans().Returns(MockPlans.Plans);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _organizationDisableCommand.Received(1)\n            .DisableAsync(organizationId, currentPeriodEnd);\n        await _pushNotificationAdapter.Received(1)\n            .NotifyEnabledChangedAsync(organization);\n        await _stripeFacade.Received(1).UpdateSubscription(\n            subscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAt.HasValue &&\n                options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&\n                options.ProrationBehavior == ProrationBehavior.None &&\n                options.CancellationDetails != null &&\n                options.CancellationDetails.Comment != null));\n    }\n\n    [Fact]\n    public async Task\n        HandleAsync_UnpaidProviderSubscription_WithValidTransition_DisablesProviderAndSetsCancellation()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var subscriptionId = \"sub_test123\";\n\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Active\n        };\n\n        var currentSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Unpaid,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"providerId\"] = providerId.ToString() },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCycle },\n            TestClock = null\n        };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = currentSubscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        var provider = new Provider { Id = providerId, Enabled = true };\n\n        _stripeEventService.GetSubscription(parsedEvent, true, Arg.Any<List<string>>()).Returns(currentSubscription);\n        _providerRepository.GetByIdAsync(providerId).Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        Assert.False(provider.Enabled);\n        await _providerService.Received(1).UpdateAsync(provider);\n\n        // Verify that UpdateSubscription was called with CancelAt\n        await _stripeFacade.Received(1).UpdateSubscription(\n            subscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAt.HasValue &&\n                options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&\n                options.ProrationBehavior == ProrationBehavior.None &&\n                options.CancellationDetails != null &&\n                options.CancellationDetails.Comment != null));\n    }\n\n    [Fact]\n    public async Task HandleAsync_UnpaidProviderSubscription_WithoutValidTransition_DoesNotDisableProvider()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        const string subscriptionId = \"sub_123\";\n\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Unpaid // No valid transition (already unpaid)\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Status = SubscriptionStatus.Unpaid,\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCycle }\n        };\n\n        var provider = new Provider { Id = providerId, Name = \"Test Provider\", Enabled = true };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _providerRepository.GetByIdAsync(providerId)\n            .Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - No disable or cancellation since there was no valid status transition\n        Assert.True(provider.Enabled);\n        await _providerService.DidNotReceive().UpdateAsync(Arg.Any<Provider>());\n        await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_UnpaidProviderSubscription_WithNonMatchingPreviousStatus_DoesNotDisableProvider()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        const string subscriptionId = \"sub_123\";\n\n        // Previous status is Canceled, which is not a valid transition source (Trialing/Active/PastDue)\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Canceled\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Status = SubscriptionStatus.Unpaid,\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCycle }\n        };\n\n        var provider = new Provider { Id = providerId, Name = \"Test Provider\", Enabled = true };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _providerRepository.GetByIdAsync(providerId)\n            .Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - No disable or cancellation since the previous status (Canceled) is not a valid transition source\n        Assert.True(provider.Enabled);\n        await _providerService.DidNotReceive().UpdateAsync(Arg.Any<Provider>());\n        await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_IncompleteToIncompleteExpiredTransition_DisablesProviderAndSetsCancellation()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        // Previous status was Incomplete - this is the valid transition for IncompleteExpired\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Incomplete\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.IncompleteExpired,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCreate }\n        };\n\n        var provider = new Provider { Id = providerId, Name = \"Test Provider\", Enabled = true };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _providerRepository.GetByIdAsync(providerId)\n            .Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Incomplete to IncompleteExpired should trigger disable and cancellation\n        Assert.False(provider.Enabled);\n        await _providerService.Received(1).UpdateAsync(provider);\n        await _stripeFacade.Received(1).UpdateSubscription(\n            subscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAt.HasValue &&\n                options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&\n                options.ProrationBehavior == ProrationBehavior.None &&\n                options.CancellationDetails != null &&\n                options.CancellationDetails.Comment != null));\n    }\n\n    [Fact]\n    public async Task HandleAsync_IncompleteToIncompleteExpiredUserSubscription_DisablesPremiumAndSetsCancellation()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Incomplete\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.IncompleteExpired,\n            Metadata = new Dictionary<string, string> { { \"userId\", userId.ToString() } },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }\n                ]\n            },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCreate }\n        };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _userService.Received(1).DisablePremiumAsync(userId, currentPeriodEnd);\n        await _stripeFacade.Received(1).UpdateSubscription(\n            subscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAt.HasValue &&\n                options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&\n                options.ProrationBehavior == ProrationBehavior.None &&\n                options.CancellationDetails != null &&\n                options.CancellationDetails.Comment != null));\n    }\n\n    [Fact]\n    public async Task HandleAsync_IncompleteToIncompleteExpiredOrganizationSubscription_DisablesOrganizationAndSetsCancellation()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Incomplete\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.IncompleteExpired,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = currentPeriodEnd,\n                        Plan = new Plan { Id = \"2023-enterprise-org-seat-annually\" }\n                    }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"organizationId\", organizationId.ToString() } },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCreate }\n        };\n\n        var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var plan = new Enterprise2023Plan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n        _pricingClient.ListPlans().Returns(MockPlans.Plans);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _organizationDisableCommand.Received(1).DisableAsync(organizationId, currentPeriodEnd);\n        await _pushNotificationAdapter.Received(1).NotifyEnabledChangedAsync(organization);\n        await _stripeFacade.Received(1).UpdateSubscription(\n            subscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAt.HasValue &&\n                options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&\n                options.ProrationBehavior == ProrationBehavior.None &&\n                options.CancellationDetails != null &&\n                options.CancellationDetails.Comment != null));\n    }\n\n    [Fact]\n    public async Task HandleAsync_UnpaidProviderSubscription_WhenProviderNotFound_StillSetsCancellation()\n    {\n        // Arrange\n        var providerId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Active\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Unpaid,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCycle }\n        };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _providerRepository.GetByIdAsync(providerId)\n            .Returns((Provider)null);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Provider not updated (since not found), but cancellation is still set\n        await _providerService.DidNotReceive().UpdateAsync(Arg.Any<Provider>());\n        await _stripeFacade.Received(1).UpdateSubscription(\n            subscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAt.HasValue &&\n                options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&\n                options.ProrationBehavior == ProrationBehavior.None &&\n                options.CancellationDetails != null &&\n                options.CancellationDetails.Comment != null));\n    }\n\n    [Fact]\n    public async Task HandleAsync_UnpaidUserSubscription_DisablesPremiumAndSetsCancellation()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Active\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Unpaid,\n            Metadata = new Dictionary<string, string> { { \"userId\", userId.ToString() } },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }\n                ]\n            },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCycle }\n        };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        var user = new User { Id = userId, Premium = false, PremiumExpirationDate = currentPeriodEnd };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        _userRepository.GetByIdAsync(userId).Returns(user);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _userService.Received(1)\n            .DisablePremiumAsync(userId, currentPeriodEnd);\n        await _userRepository.Received(1).GetByIdAsync(userId);\n        await _pushNotificationAdapter.Received(1).NotifyPremiumStatusChangedAsync(user);\n        await _stripeFacade.Received(1).UpdateSubscription(\n            subscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAt.HasValue &&\n                options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) &&\n                options.ProrationBehavior == ProrationBehavior.None &&\n                options.CancellationDetails != null &&\n                options.CancellationDetails.Comment != null));\n        await _stripeFacade.DidNotReceive()\n            .CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());\n        await _stripeFacade.DidNotReceive()\n            .ListInvoices(Arg.Any<InvoiceListOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_IncompleteExpiredUserSubscription_OnlyUpdatesExpiration()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        // Previous status that doesn't trigger enable/disable logic\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Incomplete\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.IncompleteExpired,\n            Metadata = new Dictionary<string, string> { { \"userId\", userId.ToString() } },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }\n                ]\n            }\n        };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())\n            .Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - IncompleteExpired is no longer handled specially, only expiration is updated\n        await _userService.DidNotReceive().DisablePremiumAsync(Arg.Any<Guid>(), Arg.Any<DateTime?>());\n        await _userService.Received(1).UpdatePremiumExpirationAsync(userId, currentPeriodEnd);\n        await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n        await _stripeFacade.DidNotReceive()\n            .CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());\n        await _stripeFacade.DidNotReceive()\n            .ListInvoices(Arg.Any<InvoiceListOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_ActiveOrganizationSubscription_EnablesOrganizationAndUpdatesExpiration()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Unpaid\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Active,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = currentPeriodEnd,\n                        Plan = new Plan { Id = \"2023-enterprise-org-seat-annually\" }\n                    }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"organizationId\", organizationId.ToString() } },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCycle }\n        };\n\n        var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 };\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _organizationRepository.GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var plan = new Enterprise2023Plan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType)\n            .Returns(plan);\n        _pricingClient.ListPlans()\n            .Returns(MockPlans.Plans);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _organizationEnableCommand.Received(1)\n            .EnableAsync(organizationId, currentPeriodEnd);\n        await _organizationService.Received(1)\n            .UpdateExpirationDateAsync(organizationId, currentPeriodEnd);\n        await _pushNotificationAdapter.Received(1)\n            .NotifyEnabledChangedAsync(organization);\n        await _stripeFacade.Received(1).UpdateSubscription(\n            subscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAtPeriodEnd == false &&\n                options.ProrationBehavior == ProrationBehavior.None));\n    }\n\n    [Fact]\n    public async Task HandleAsync_ActiveUserSubscription_EnablesPremiumAndUpdatesExpiration()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Unpaid\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Active,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"userId\", userId.ToString() } },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCycle }\n        };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        var user = new User { Id = userId, Premium = true, PremiumExpirationDate = currentPeriodEnd };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n        _userRepository.GetByIdAsync(userId).Returns(user);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _userService.Received(1)\n            .EnablePremiumAsync(userId, currentPeriodEnd);\n        await _userService.Received(1)\n            .UpdatePremiumExpirationAsync(userId, currentPeriodEnd);\n        await _userRepository.Received(1).GetByIdAsync(userId);\n        await _pushNotificationAdapter.Received(1).NotifyPremiumStatusChangedAsync(user);\n        await _stripeFacade.Received(1).UpdateSubscription(\n            subscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAtPeriodEnd == false &&\n                options.ProrationBehavior == ProrationBehavior.None));\n    }\n\n    [Fact]\n    public async Task HandleAsync_SponsoredSubscription_RenewsSponsorship()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        // Use a previous status that won't trigger enable/disable logic\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Active\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Active,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { { \"organizationId\", organizationId.ToString() } }\n        };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription)\n            .Returns(true);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _organizationSponsorshipRenewCommand.Received(1)\n            .UpdateExpirationDateAsync(organizationId, currentPeriodEnd);\n    }\n\n    [Fact]\n    public async Task\n        HandleAsync_WhenSubscriptionIsActive_AndOrganizationHasSecretsManagerTrial_AndRemovingSecretsManagerTrial_RemovesPasswordManagerCoupon()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Status = SubscriptionStatus.Active,\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),\n                        Plan = new Plan { Id = \"2023-enterprise-org-seat-annually\" }\n                    }\n                ]\n            },\n            Customer = new Customer\n            {\n                Balance = 0,\n                Discount = new Discount { Coupon = new Coupon { Id = \"sm-standalone\" } }\n            },\n            Discounts = [new Discount { Coupon = new Coupon { Id = \"sm-standalone\" } }],\n            Metadata = new Dictionary<string, string> { { \"organizationId\", organizationId.ToString() } }\n        };\n\n        var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 };\n\n        var plan = new Enterprise2023Plan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType)\n            .Returns(plan);\n        _pricingClient.ListPlans()\n            .Returns(MockPlans.Plans);\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(new\n                {\n                    items = new\n                    {\n                        data = new[] { new { plan = new { id = \"secrets-manager-enterprise-seat-annually\" } } }\n                    },\n                    Items = new StripeList<SubscriptionItem>\n                    {\n                        Data =\n                        [\n                            new SubscriptionItem { Plan = new Plan { Id = \"secrets-manager-enterprise-seat-annually\" } }\n                        ]\n                    }\n                })\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _organizationRepository.GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).DeleteCustomerDiscount(subscription.CustomerId);\n        await _stripeFacade.Received(1).DeleteSubscriptionDiscount(subscription.Id);\n    }\n    [Fact]\n    public async Task\n        HandleAsync_WhenUpgradingPlan_AndPreviousPlanHasSecretsManagerTrial_AndCurrentPlanHasSecretsManagerTrial_DoesNotRemovePasswordManagerCoupon()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Status = SubscriptionStatus.Active,\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),\n                        Plan = new Plan { Id = \"2023-enterprise-org-seat-annually\" }\n                    },\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodEnd = DateTime.UtcNow.AddDays(10),\n                        Plan = new Plan { Id = \"secrets-manager-enterprise-seat-annually\" }\n                    }\n                ]\n            },\n            Customer = new Customer\n            {\n                Balance = 0,\n                Discount = new Discount { Coupon = new Coupon { Id = \"sm-standalone\" } }\n            },\n            Discounts = [new Discount { Coupon = new Coupon { Id = \"sm-standalone\" } }],\n            Metadata = new Dictionary<string, string> { { \"organizationId\", organizationId.ToString() } }\n        };\n\n        // Note: The organization plan is still the previous plan because the subscription is updated before the organization is updated\n        var organization = new Organization { Id = organizationId, PlanType = PlanType.TeamsAnnually2023 };\n\n        var plan = new Teams2023Plan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType)\n            .Returns(plan);\n        _pricingClient.ListPlans()\n            .Returns(MockPlans.Plans);\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(new\n                {\n                    items = new\n                    {\n                        data = new[]\n                        {\n                            new { plan = new { id = \"secrets-manager-teams-seat-annually\" } },\n                        }\n                    },\n                    Items = new StripeList<SubscriptionItem>\n                    {\n                        Data =\n                        [\n                            new SubscriptionItem { Plan = new Stripe.Plan { Id = \"secrets-manager-teams-seat-annually\" } },\n                        ]\n                    }\n                })\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        _organizationRepository.GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.DidNotReceive().DeleteCustomerDiscount(subscription.CustomerId);\n        await _stripeFacade.DidNotReceive().DeleteSubscriptionDiscount(subscription.Id);\n    }\n\n    [Theory]\n    [MemberData(nameof(GetValidTransitionToActiveSubscriptions))]\n    public async Task\n        HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasIncompleteOrUnpaid_EnableProviderAndUpdateSubscription(\n            Subscription previousSubscription)\n    {\n        // Arrange\n        var (providerId, newSubscription, provider, parsedEvent) =\n            CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);\n\n        _stripeEventService\n            .GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(newSubscription);\n\n        _providerRepository\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(provider);\n        _stripeFacade\n            .UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(newSubscription);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeEventService\n            .Received(1)\n            .GetSubscription(parsedEvent, true, Arg.Any<List<string>>());\n        await _providerRepository\n            .Received(1)\n            .GetByIdAsync(providerId);\n        await _providerService\n            .Received(1)\n            .UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));\n        await _stripeFacade\n            .Received(1)\n            .UpdateSubscription(newSubscription.Id,\n                Arg.Is<SubscriptionUpdateOptions>(options =>\n                    options.CancelAtPeriodEnd == false &&\n                    options.ProrationBehavior == ProrationBehavior.None));\n    }\n\n    [Fact]\n    public async Task\n        HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasCanceled_DoesNotEnableProvider()\n    {\n        // Arrange\n        var previousSubscription = new Subscription { Id = \"sub_123\", Status = SubscriptionStatus.Canceled };\n        var (providerId, newSubscription, provider, parsedEvent) =\n            CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);\n\n        _stripeEventService\n            .GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(newSubscription);\n        _providerRepository\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Canceled is not a valid transition source for SubscriptionBecameActive\n        await _stripeEventService\n            .Received(1)\n            .GetSubscription(parsedEvent, true, Arg.Any<List<string>>());\n        await _providerRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());\n        await _providerService\n            .DidNotReceive()\n            .UpdateAsync(Arg.Any<Provider>());\n        await _stripeFacade\n            .DidNotReceiveWithAnyArgs()\n            .UpdateSubscription(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task\n        HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasAlreadyActive_DoesNotEnableProvider()\n    {\n        // Arrange\n        var previousSubscription = new Subscription { Id = \"sub_123\", Status = SubscriptionStatus.Active };\n        var (providerId, newSubscription, provider, parsedEvent) =\n            CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);\n\n        _stripeEventService\n            .GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(newSubscription);\n        _providerRepository\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Already Active is not a valid transition for SubscriptionBecameActive\n        await _stripeEventService\n            .Received(1)\n            .GetSubscription(parsedEvent, true, Arg.Any<List<string>>());\n        await _providerRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());\n        await _providerService\n            .DidNotReceive()\n            .UpdateAsync(Arg.Any<Provider>());\n        await _stripeFacade\n            .DidNotReceiveWithAnyArgs()\n            .UpdateSubscription(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task\n        HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasTrialing_DoesNotEnableProvider()\n    {\n        // Arrange\n        var previousSubscription = new Subscription { Id = \"sub_123\", Status = SubscriptionStatus.Trialing };\n        var (providerId, newSubscription, provider, parsedEvent) =\n            CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);\n\n        _stripeEventService\n            .GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(newSubscription);\n        _providerRepository\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Trialing is not a valid transition source for SubscriptionBecameActive\n        await _stripeEventService\n            .Received(1)\n            .GetSubscription(parsedEvent, true, Arg.Any<List<string>>());\n        await _providerRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());\n        await _providerService\n            .DidNotReceive()\n            .UpdateAsync(Arg.Any<Provider>());\n        await _stripeFacade\n            .DidNotReceiveWithAnyArgs()\n            .UpdateSubscription(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task\n        HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasPastDue_DoesNotEnableProvider()\n    {\n        // Arrange\n        var previousSubscription = new Subscription { Id = \"sub_123\", Status = SubscriptionStatus.PastDue };\n        var (providerId, newSubscription, provider, parsedEvent) =\n            CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);\n\n        _stripeEventService\n            .GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(newSubscription);\n        _providerRepository\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - PastDue is not a valid transition source for SubscriptionBecameActive\n        await _stripeEventService\n            .Received(1)\n            .GetSubscription(parsedEvent, true, Arg.Any<List<string>>());\n        await _providerRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());\n        await _providerService\n            .DidNotReceive()\n            .UpdateAsync(Arg.Any<Provider>());\n        await _stripeFacade\n            .DidNotReceiveWithAnyArgs()\n            .UpdateSubscription(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_ActiveProviderSubscriptionEvent_AndProviderDoesNotExist_NoChanges()\n    {\n        // Arrange\n        var previousSubscription = new Subscription { Id = \"sub_123\", Status = SubscriptionStatus.Unpaid };\n        var (providerId, newSubscription, _, parsedEvent) =\n            CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);\n\n        _stripeEventService\n            .GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(newSubscription);\n        _providerRepository\n            .GetByIdAsync(Arg.Any<Guid>())\n            .ReturnsNull();\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeEventService\n            .Received(1)\n            .GetSubscription(parsedEvent, true, Arg.Any<List<string>>());\n        await _providerRepository\n            .Received(1)\n            .GetByIdAsync(providerId);\n        await _providerService\n            .DidNotReceive()\n            .UpdateAsync(Arg.Any<Provider>());\n        await _stripeFacade\n            .DidNotReceive()\n            .UpdateSubscription(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_ActiveProviderSubscriptionEvent_WithNonMatchingPreviousStatus_DoesNotEnableProvider()\n    {\n        // Arrange - Using a previous status (Canceled) that doesn't trigger SubscriptionBecameActive\n        var previousSubscription = new Subscription { Id = \"sub_123\", Status = SubscriptionStatus.Canceled };\n        var (providerId, newSubscription, provider, parsedEvent) =\n            CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription);\n\n        _stripeEventService\n            .GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(newSubscription);\n        _providerRepository\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Canceled is not a valid transition source, so no enable logic is triggered\n        await _stripeEventService\n            .Received(1)\n            .GetSubscription(parsedEvent, true, Arg.Any<List<string>>());\n        await _providerRepository.DidNotReceive().GetByIdAsync(Arg.Any<Guid>());\n        await _providerService\n            .DidNotReceive()\n            .UpdateAsync(Arg.Any<Provider>());\n        await _stripeFacade\n            .DidNotReceive()\n            .UpdateSubscription(Arg.Any<string>());\n    }\n\n    private static (Guid providerId, Subscription newSubscription, Provider provider, Event parsedEvent)\n        CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(Subscription? previousSubscription)\n    {\n        var providerId = Guid.NewGuid();\n        var newSubscription = new Subscription\n        {\n            Id = previousSubscription?.Id ?? \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) }\n                ]\n            },\n            Status = SubscriptionStatus.Active,\n            Metadata = new Dictionary<string, string> { { \"providerId\", providerId.ToString() } },\n            LatestInvoice = new Invoice { BillingReason = BillingReasons.SubscriptionCycle }\n        };\n\n        var provider = new Provider { Id = providerId, Enabled = false };\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = newSubscription,\n                PreviousAttributes =\n                    previousSubscription == null ? null : JObject.FromObject(previousSubscription)\n            }\n        };\n        return (providerId, newSubscription, provider, parsedEvent);\n    }\n\n    [Fact]\n    public async Task HandleAsync_IncompleteUserSubscription_OnlyUpdatesExpiration()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var subscriptionId = \"sub_123\";\n        var currentPeriodEnd = DateTime.UtcNow.AddDays(30);\n\n        // Previous status that doesn't trigger enable/disable logic (already was incomplete)\n        var previousSubscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Incomplete\n        };\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = SubscriptionStatus.Incomplete,\n            Metadata = new Dictionary<string, string> { { \"userId\", userId.ToString() } },\n            LatestInvoice = new Invoice { Status = \"open\" },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }\n                ]\n            }\n        };\n\n        var parsedEvent = new Event\n        {\n            Data = new EventData\n            {\n                Object = subscription,\n                PreviousAttributes = JObject.FromObject(previousSubscription)\n            }\n        };\n\n        _stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Incomplete status is no longer handled specially, only expiration is updated\n        await _userService.DidNotReceive().DisablePremiumAsync(Arg.Any<Guid>(), Arg.Any<DateTime?>());\n        await _userService.Received(1).UpdatePremiumExpirationAsync(userId, currentPeriodEnd);\n        await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    public static IEnumerable<object[]> GetValidTransitionToActiveSubscriptions()\n    {\n        // Only Incomplete and Unpaid are valid previous statuses for SubscriptionBecameActive\n        return new List<object[]>\n        {\n            new object[] { new Subscription { Id = \"sub_123\", Status = SubscriptionStatus.Unpaid } },\n            new object[] { new Subscription { Id = \"sub_123\", Status = SubscriptionStatus.Incomplete } }\n        };\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Billing.Services;\nusing Bit.Billing.Services.Implementations;\nusing Bit.Core;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Pricing.Premium;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;\nusing Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;\nusing Bit.Core.Models.Mail.Billing.Renewal.Premium;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Platform.Mail.Mailer;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Stripe;\nusing Xunit;\nusing static Bit.Core.Billing.Constants.StripeConstants;\nusing Address = Stripe.Address;\nusing Event = Stripe.Event;\nusing PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;\n\nnamespace Bit.Billing.Test.Services;\n\npublic class UpcomingInvoiceHandlerTests\n{\n    private readonly IGetPaymentMethodQuery _getPaymentMethodQuery;\n    private readonly ILogger<StripeEventProcessor> _logger;\n    private readonly IMailService _mailService;\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IPricingClient _pricingClient;\n    private readonly IProviderRepository _providerRepository;\n    private readonly IStripeFacade _stripeFacade;\n    private readonly IStripeEventService _stripeEventService;\n    private readonly IStripeEventUtilityService _stripeEventUtilityService;\n    private readonly IUserRepository _userRepository;\n    private readonly IValidateSponsorshipCommand _validateSponsorshipCommand;\n    private readonly IMailer _mailer;\n    private readonly IFeatureService _featureService;\n\n    private readonly UpcomingInvoiceHandler _sut;\n\n    private readonly Guid _userId = Guid.NewGuid();\n    private readonly Guid _organizationId = Guid.NewGuid();\n    private readonly Guid _providerId = Guid.NewGuid();\n\n\n    public UpcomingInvoiceHandlerTests()\n    {\n        _getPaymentMethodQuery = Substitute.For<IGetPaymentMethodQuery>();\n        _logger = Substitute.For<ILogger<StripeEventProcessor>>();\n        _mailService = Substitute.For<IMailService>();\n        _organizationRepository = Substitute.For<IOrganizationRepository>();\n        _pricingClient = Substitute.For<IPricingClient>();\n        _providerRepository = Substitute.For<IProviderRepository>();\n        _stripeFacade = Substitute.For<IStripeFacade>();\n        _stripeEventService = Substitute.For<IStripeEventService>();\n        _stripeEventUtilityService = Substitute.For<IStripeEventUtilityService>();\n        _userRepository = Substitute.For<IUserRepository>();\n        _validateSponsorshipCommand = Substitute.For<IValidateSponsorshipCommand>();\n        _mailer = Substitute.For<IMailer>();\n        _featureService = Substitute.For<IFeatureService>();\n\n        _sut = new UpcomingInvoiceHandler(\n            _getPaymentMethodQuery,\n            _logger,\n            _mailService,\n            _organizationRepository,\n            _pricingClient,\n            _providerRepository,\n            _stripeFacade,\n            _stripeEventService,\n            _stripeEventUtilityService,\n            _userRepository,\n            _validateSponsorshipCommand,\n            _mailer,\n            _featureService);\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenNullSubscription_DoesNothing()\n    {\n        // Arrange\n        var parsedEvent = new Event();\n        var invoice = new Invoice { CustomerId = \"cus_123\" };\n        var customer = new Customer { Id = \"cus_123\", Subscriptions = new StripeList<Subscription> { Data = [] } };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.DidNotReceive()\n            .UpdateCustomer(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenValidUser_SendsEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var customerId = \"cus_123\";\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new() { Id = \"si_123\", Price = new Price { Id = Prices.PremiumAnnually } }]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = customerId },\n            Metadata = new Dictionary<string, string>()\n        };\n        var user = new User { Id = _userId, Email = \"user@example.com\", Premium = true };\n        var plan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Purchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually },\n            Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }\n        };\n        var customer = new Customer\n        {\n            Id = customerId,\n            Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported },\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(customerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));\n\n        _userRepository.GetByIdAsync(_userId).Returns(user);\n        _pricingClient.GetAvailablePremiumPlan().Returns(plan);\n\n        // If milestone 2 is disabled, the default email is sent\n        _featureService\n            .IsEnabled(FeatureFlagKeys.PM23341_Milestone_2)\n            .Returns(false);\n\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _userRepository.Received(1).GetByIdAsync(_userId);\n\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"user@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n    }\n\n    [Fact]\n    public async Task\n        HandleAsync_WhenUserValid_AndMilestone2Enabled_UpdatesPriceId_AndSendsUpdatedInvoiceUpcomingEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var customerId = \"cus_123\";\n        var priceSubscriptionId = \"sub-1\";\n        var priceId = \"price-id-2\";\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new() { Id = priceSubscriptionId, Price = new Price { Id = Prices.PremiumAnnually } }]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer\n            {\n                Id = customerId,\n                Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n        var user = new User { Id = _userId, Email = \"user@example.com\", Premium = true };\n        var plan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Purchasable { Price = 10M, StripePriceId = priceId },\n            Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }\n        };\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(customerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));\n\n        _userRepository.GetByIdAsync(_userId).Returns(user);\n        _pricingClient.GetAvailablePremiumPlan().Returns(plan);\n        _stripeFacade.UpdateSubscription(\n                subscription.Id,\n                Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // If milestone 2 is true, the updated invoice email is sent\n        _featureService\n            .IsEnabled(FeatureFlagKeys.PM23341_Milestone_2)\n            .Returns(true);\n\n        var coupon = new Coupon { PercentOff = 20, Id = CouponIDs.Milestone2SubscriptionDiscount };\n\n        _stripeFacade.GetCoupon(CouponIDs.Milestone2SubscriptionDiscount).Returns(coupon);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _userRepository.Received(1).GetByIdAsync(_userId);\n        await _pricingClient.Received(1).GetAvailablePremiumPlan();\n        await _stripeFacade.Received(1).GetCoupon(CouponIDs.Milestone2SubscriptionDiscount);\n        await _stripeFacade.Received(1).UpdateSubscription(\n            Arg.Is(\"sub_123\"),\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items[0].Id == priceSubscriptionId &&\n                o.Items[0].Price == priceId &&\n                o.Discounts[0].Coupon == CouponIDs.Milestone2SubscriptionDiscount &&\n                o.ProrationBehavior == \"none\"));\n\n        // Verify the updated invoice email was sent with correct price\n        var discountedPrice = plan.Seat.Price * (100 - coupon.PercentOff.Value) / 100;\n        await _mailer.Received(1).SendEmail(\n            Arg.Is<PremiumRenewalMail>(email =>\n                email.ToEmails.Contains(\"user@example.com\") &&\n                email.Subject == \"Your Bitwarden Premium renewal is updating\" &&\n                email.View.BaseMonthlyRenewalPrice == (plan.Seat.Price / 12).ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.DiscountedAnnualRenewalPrice == discountedPrice.ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.DiscountAmount == $\"{coupon.PercentOff}%\"\n            ));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenOrganizationHasSponsorship_SendsEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>(),\n            LatestInvoiceId = \"inv_latest\"\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.EnterpriseAnnually\n        };\n        var plan = new FamiliesPlan();\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n\n        _organizationRepository\n            .GetByIdAsync(_organizationId)\n            .Returns(organization);\n\n        _pricingClient\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(plan);\n\n        _stripeEventUtilityService\n            .IsSponsoredSubscription(subscription)\n            .Returns(true);\n        // Configure that this is a sponsored subscription\n        _stripeEventUtilityService\n            .IsSponsoredSubscription(subscription)\n            .Returns(true);\n        _validateSponsorshipCommand\n            .ValidateSponsorshipAsync(_organizationId)\n            .Returns(true);\n\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _organizationRepository.Received(1).GetByIdAsync(_organizationId);\n        await _validateSponsorshipCommand.Received(1).ValidateSponsorshipAsync(_organizationId);\n\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"org@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n    }\n\n    [Fact]\n    public async Task\n        HandleAsync_WhenOrganizationHasSponsorship_ButInvalidSponsorship_RetrievesUpdatedInvoice_SendsEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                    [new SubscriptionItem { Price = new Price { Id = \"2021-family-for-enterprise-annually\" } }]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>(),\n            LatestInvoiceId = \"inv_latest\"\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.EnterpriseAnnually\n        };\n        var plan = new FamiliesPlan();\n\n        var paymentMethod = new Card { Last4 = \"4242\", Brand = \"visa\" };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n\n        _organizationRepository\n            .GetByIdAsync(_organizationId)\n            .Returns(organization);\n\n        _pricingClient\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(plan);\n\n        // Configure that this is not a sponsored subscription\n        _stripeEventUtilityService\n            .IsSponsoredSubscription(subscription)\n            .Returns(true);\n\n        // Validate sponsorship should return false\n        _validateSponsorshipCommand\n            .ValidateSponsorshipAsync(_organizationId)\n            .Returns(false);\n        _stripeFacade\n            .GetInvoice(subscription.LatestInvoiceId)\n            .Returns(invoice);\n\n        _getPaymentMethodQuery.Run(organization).Returns(MaskedPaymentMethod.From(paymentMethod));\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _organizationRepository.Received(1).GetByIdAsync(_organizationId);\n        _stripeEventUtilityService.Received(1).IsSponsoredSubscription(subscription);\n        await _validateSponsorshipCommand.Received(1).ValidateSponsorshipAsync(_organizationId);\n        await _stripeFacade.Received(1).GetInvoice(Arg.Is(\"inv_latest\"));\n\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"org@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenValidOrganization_SendsEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                    [new SubscriptionItem { Price = new Price { Id = \"enterprise-annually\" } }]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>(),\n            LatestInvoiceId = \"inv_latest\"\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.EnterpriseAnnually\n        };\n        var plan = new FamiliesPlan();\n\n        var paymentMethod = new Card { Last4 = \"4242\", Brand = \"visa\" };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n\n        _organizationRepository\n            .GetByIdAsync(_organizationId)\n            .Returns(organization);\n\n        _pricingClient\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(plan);\n\n        _stripeEventUtilityService\n            .IsSponsoredSubscription(subscription)\n            .Returns(false);\n\n        _stripeFacade\n            .GetInvoice(subscription.LatestInvoiceId)\n            .Returns(invoice);\n\n        _getPaymentMethodQuery.Run(organization).Returns(MaskedPaymentMethod.From(paymentMethod));\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _organizationRepository.Received(1).GetByIdAsync(_organizationId);\n        _stripeEventUtilityService.Received(1).IsSponsoredSubscription(subscription);\n\n        // Should not validate sponsorship for non-sponsored subscription\n        await _validateSponsorshipCommand.DidNotReceive().ValidateSponsorshipAsync(Arg.Any<Guid>());\n\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"org@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenNonDirectTaxCountryOrganization_SetsReverseCharge()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice { CustomerId = \"cus_123\", AmountDue = 0, Lines = new StripeList<InvoiceLineItem> { Data = [] } };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>()\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"DE\" },\n            TaxExempt = TaxExempt.None\n        };\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(new EnterprisePlan(isAnnual: true));\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateCustomer(\n            Arg.Is(\"cus_123\"),\n            Arg.Is<CustomerUpdateOptions>(o => o.TaxExempt == TaxExempt.Reverse));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenUSOrganizationWithManualReverseCharge_CorrectsTaxExemptToNone()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice { CustomerId = \"cus_123\", AmountDue = 0, Lines = new StripeList<InvoiceLineItem> { Data = [] } };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>()\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" },\n            TaxExempt = TaxExempt.Reverse\n        };\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(new EnterprisePlan(isAnnual: true));\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateCustomer(\n            Arg.Is(\"cus_123\"),\n            Arg.Is<CustomerUpdateOptions>(o => o.TaxExempt == TaxExempt.None));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenSwissOrganizationWithReverse_CorrectsTaxExemptToNone()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice { CustomerId = \"cus_123\", AmountDue = 0, Lines = new StripeList<InvoiceLineItem> { Data = [] } };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>()\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"CH\" },\n            TaxExempt = TaxExempt.Reverse\n        };\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(new EnterprisePlan(isAnnual: true));\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateCustomer(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(options => options.TaxExempt == TaxExempt.None));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenOrganizationCustomerIsExempt_DoesNotUpdateTaxExemption()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice { CustomerId = \"cus_123\", AmountDue = 0, Lines = new StripeList<InvoiceLineItem> { Data = [] } };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>()\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"DE\" },\n            TaxExempt = TaxExempt.Exempt\n        };\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(new EnterprisePlan(isAnnual: true));\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.DidNotReceive().UpdateCustomer(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenValidProviderSubscription_SendsEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>(),\n            CollectionMethod = \"charge_automatically\"\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"UK\" },\n            TaxExempt = TaxExempt.None\n        };\n        var provider = new Provider { Id = _providerId, BillingEmail = \"provider@example.com\" };\n\n        var paymentMethod = new Card { Last4 = \"4242\", Brand = \"visa\" };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, null, _providerId));\n\n        _providerRepository.GetByIdAsync(_providerId).Returns(provider);\n        _getPaymentMethodQuery.Run(provider).Returns(MaskedPaymentMethod.From(paymentMethod));\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _providerRepository.Received(2).GetByIdAsync(_providerId);\n\n        // Verify tax exempt was set to reverse for non-direct-tax-country providers\n        await _stripeFacade.Received(1).UpdateCustomer(\n            Arg.Is(\"cus_123\"),\n            Arg.Is<CustomerUpdateOptions>(o => o.TaxExempt == TaxExempt.Reverse));\n\n        // Verify automatic tax was enabled\n        await _stripeFacade.Received(1).UpdateSubscription(\n            Arg.Is(\"sub_123\"),\n            Arg.Is<SubscriptionUpdateOptions>(o => o.AutomaticTax.Enabled == true));\n\n        // Verify provider invoice email was sent\n        await _mailService.Received(1).SendProviderInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(e => e.Contains(\"provider@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<string>(s => s == subscription.CollectionMethod),\n            Arg.Is<bool>(b => b == true),\n            Arg.Is<string>(s => s == $\"{paymentMethod.Brand} ending in {paymentMethod.Last4}\"));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenSwissProviderWithReverse_CorrectsTaxExemptToNone()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>(),\n            CollectionMethod = \"charge_automatically\"\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"CH\" },\n            TaxExempt = TaxExempt.Reverse\n        };\n        var provider = new Provider { Id = _providerId, BillingEmail = \"provider@example.com\" };\n\n        var paymentMethod = new Card { Last4 = \"4242\", Brand = \"visa\" };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, null, _providerId));\n\n        _providerRepository.GetByIdAsync(_providerId).Returns(provider);\n        _getPaymentMethodQuery.Run(provider).Returns(MaskedPaymentMethod.From(paymentMethod));\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _providerRepository.Received(2).GetByIdAsync(_providerId);\n\n        await _stripeFacade.Received(1).UpdateCustomer(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(options => options.TaxExempt == TaxExempt.None));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenProviderCustomerIsExempt_DoesNotUpdateTaxExemption()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>(),\n            CollectionMethod = \"charge_automatically\"\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"DE\" },\n            TaxExempt = TaxExempt.Exempt\n        };\n        var provider = new Provider { Id = _providerId, BillingEmail = \"provider@example.com\" };\n        var paymentMethod = new Card { Last4 = \"4242\", Brand = \"visa\" };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, null, _providerId));\n        _providerRepository.GetByIdAsync(_providerId).Returns(provider);\n        _getPaymentMethodQuery.Run(provider).Returns(MaskedPaymentMethod.From(paymentMethod));\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.DidNotReceive().UpdateCustomer(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenNonDirectTaxCountryProvider_SetsReverseCharge()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice { CustomerId = \"cus_123\", AmountDue = 0, Lines = new StripeList<InvoiceLineItem> { Data = [] } };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>(),\n            CollectionMethod = \"charge_automatically\"\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"DE\" },\n            TaxExempt = TaxExempt.None\n        };\n        var provider = new Provider { Id = _providerId, BillingEmail = \"provider@example.com\" };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, null, _providerId));\n        _providerRepository.GetByIdAsync(_providerId).Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateCustomer(\n            Arg.Is(\"cus_123\"),\n            Arg.Is<CustomerUpdateOptions>(o => o.TaxExempt == TaxExempt.Reverse));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenUSProviderWithManualReverseCharge_CorrectsTaxExemptToNone()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice { CustomerId = \"cus_123\", AmountDue = 0, Lines = new StripeList<InvoiceLineItem> { Data = [] } };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>(),\n            CollectionMethod = \"charge_automatically\"\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" },\n            TaxExempt = TaxExempt.Reverse\n        };\n        var provider = new Provider { Id = _providerId, BillingEmail = \"provider@example.com\" };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, null, _providerId));\n        _providerRepository.GetByIdAsync(_providerId).Returns(provider);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateCustomer(\n            Arg.Is(\"cus_123\"),\n            Arg.Is<CustomerUpdateOptions>(o => o.TaxExempt == TaxExempt.None));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenUpdateSubscriptionItemPriceIdFails_LogsErrorAndSendsTraditionalEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var customerId = \"cus_123\";\n        var priceSubscriptionId = \"sub-1\";\n        var priceId = \"price-id-2\";\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new() { Id = priceSubscriptionId, Price = new Price { Id = Prices.PremiumAnnually } }]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer\n            {\n                Id = customerId,\n                Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n        var user = new User { Id = _userId, Email = \"user@example.com\", Premium = true };\n        var plan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Purchasable { Price = 10M, StripePriceId = priceId },\n            Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }\n        };\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));\n\n        _userRepository.GetByIdAsync(_userId).Returns(user);\n\n        _featureService\n            .IsEnabled(FeatureFlagKeys.PM23341_Milestone_2)\n            .Returns(true);\n\n        _pricingClient.GetAvailablePremiumPlan().Returns(plan);\n\n        // Setup exception when updating subscription\n        _stripeFacade\n            .UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .ThrowsAsync(new Exception());\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString()\n                    .Contains(\n                        $\"Failed to update user's ({user.Id}) subscription price id while processing event with ID {parsedEvent.Id}\")),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception, string>>());\n\n        // Verify that traditional email was sent when update fails\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"user@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n\n        // Verify renewal email was NOT sent\n        await _mailer.DidNotReceive().SendEmail(Arg.Any<Families2020RenewalMail>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenOrganizationNotFound_DoesNothing()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>()\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n\n        // Organization not found\n        _organizationRepository.GetByIdAsync(_organizationId).Returns((Organization)null);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _organizationRepository.Received(1).GetByIdAsync(_organizationId);\n\n        // Verify no emails were sent\n        await _mailService.DidNotReceive().SendInvoiceUpcoming(\n            Arg.Any<IEnumerable<string>>(),\n            Arg.Any<decimal>(),\n            Arg.Any<DateTime>(),\n            Arg.Any<List<string>>(),\n            Arg.Any<bool>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenZeroAmountInvoice_DoesNothing()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 0, // Zero amount due\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Free Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>()\n        };\n        var user = new User { Id = _userId, Email = \"user@example.com\", Premium = true };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));\n\n        _userRepository.GetByIdAsync(_userId).Returns(user);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _userRepository.Received(1).GetByIdAsync(_userId);\n\n        // Should not\n        await _mailService.DidNotReceive().SendInvoiceUpcoming(\n            Arg.Any<IEnumerable<string>>(),\n            Arg.Any<decimal>(),\n            Arg.Any<DateTime>(),\n            Arg.Any<List<string>>(),\n            Arg.Any<bool>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenUserNotFound_DoesNothing()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>()\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));\n\n        // User not found\n        _userRepository.GetByIdAsync(_userId).Returns((User)null);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _userRepository.Received(1).GetByIdAsync(_userId);\n\n        // Verify no emails were sent\n        await _mailService.DidNotReceive().SendInvoiceUpcoming(\n            Arg.Any<IEnumerable<string>>(),\n            Arg.Any<decimal>(),\n            Arg.Any<DateTime>(),\n            Arg.Any<List<string>>(),\n            Arg.Any<bool>());\n\n        await _mailer.DidNotReceive().SendEmail(Arg.Any<Families2020RenewalMail>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenProviderNotFound_DoesNothing()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var invoice = new Invoice\n        {\n            CustomerId = \"cus_123\",\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>(),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = \"cus_123\" },\n            Metadata = new Dictionary<string, string>()\n        };\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade\n            .GetCustomer(invoice.CustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, null, _providerId));\n\n        // Provider not found\n        _providerRepository.GetByIdAsync(_providerId).Returns((Provider)null);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _providerRepository.Received(1).GetByIdAsync(_providerId);\n\n        // Verify no provider emails were sent\n        await _mailService.DidNotReceive().SendProviderInvoiceUpcoming(\n            Arg.Any<IEnumerable<string>>(),\n            Arg.Any<decimal>(),\n            Arg.Any<DateTime>(),\n            Arg.Any<List<string>>(),\n            Arg.Any<string>(),\n            Arg.Any<bool>(),\n            Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_AndFamilies2019Plan_UpdatesSubscriptionAndOrganization()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n        var premiumAccessItemId = \"si_premium_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }\n                    },\n                    new()\n                    {\n                        Id = premiumAccessItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePremiumAccessPlanId }\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        var coupon = new Coupon { PercentOff = 25, Id = CouponIDs.Milestone3SubscriptionDiscount };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            Arg.Is(subscriptionId),\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items.Count == 2 &&\n                o.Items[0].Id == passwordManagerItemId &&\n                o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&\n                o.Items[1].Id == premiumAccessItemId &&\n                o.Items[1].Deleted == true &&\n                o.Discounts.Count == 1 &&\n                o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&\n                o.ProrationBehavior == ProrationBehavior.None));\n\n        await _stripeFacade.Received(1).GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);\n\n        await _organizationRepository.Received(1).ReplaceAsync(\n            Arg.Is<Organization>(org =>\n                org.Id == _organizationId &&\n                org.PlanType == PlanType.FamiliesAnnually &&\n                org.Plan == familiesPlan.Name &&\n                org.UsersGetPremium == familiesPlan.UsersGetPremium &&\n                org.Seats == familiesPlan.PasswordManager.BaseSeats));\n\n        await _mailer.Received(1).SendEmail(\n            Arg.Is<Families2019RenewalMail>(email =>\n                email.ToEmails.Contains(\"org@example.com\") &&\n                email.Subject == \"Your Bitwarden Families renewal is updating\" &&\n                email.View.BaseMonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.BaseAnnualRenewalPrice == familiesPlan.PasswordManager.BasePrice.ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.DiscountAmount == $\"{coupon.PercentOff}%\"\n                ));\n\n        // Families plan is excluded from tax exempt alignment\n        await _stripeFacade.DidNotReceive().UpdateCustomer(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_AndFamilies2019Plan_WithoutPremiumAccess_UpdatesSubscriptionAndOrganization()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            Arg.Is(subscriptionId),\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items.Count == 1 &&\n                o.Items[0].Id == passwordManagerItemId &&\n                o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&\n                o.Discounts.Count == 1 &&\n                o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&\n                o.ProrationBehavior == ProrationBehavior.None));\n\n        await _organizationRepository.Received(1).ReplaceAsync(\n            Arg.Is<Organization>(org =>\n                org.Id == _organizationId &&\n                org.PlanType == PlanType.FamiliesAnnually &&\n                org.Plan == familiesPlan.Name &&\n                org.UsersGetPremium == familiesPlan.UsersGetPremium &&\n                org.Seats == familiesPlan.PasswordManager.BaseSeats));\n\n        // Families plan is excluded from tax exempt alignment\n        await _stripeFacade.DidNotReceive().UpdateCustomer(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2019Plan_DoesNotUpdateSubscription()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - should not update subscription or organization when feature flag is disabled\n        await _stripeFacade.DidNotReceive().UpdateSubscription(\n            Arg.Any<string>(),\n            Arg.Is<SubscriptionUpdateOptions>(o => o.Discounts != null));\n\n        await _organizationRepository.DidNotReceive().ReplaceAsync(\n            Arg.Is<Organization>(org => org.PlanType == PlanType.FamiliesAnnually));\n\n        // Families plan is excluded from tax exempt alignment\n        await _stripeFacade.DidNotReceive().UpdateCustomer(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_ButNotFamilies2019Plan_DoesNotUpdateSubscription()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new() { Id = \"si_pm_123\", Price = new Price { Id = familiesPlan.PasswordManager.StripePlanId } }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually // Already on the new plan\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - should not update subscription when not on FamiliesAnnually2019 plan\n        await _stripeFacade.DidNotReceive().UpdateSubscription(\n            Arg.Any<string>(),\n            Arg.Is<SubscriptionUpdateOptions>(o => o.Discounts != null));\n\n        await _organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());\n        // Families plan is excluded from tax exempt alignment\n        await _stripeFacade.DidNotReceive().UpdateCustomer(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_AndPasswordManagerItemNotFound_LogsWarning()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new() { Id = \"si_different_item\", Price = new Price { Id = \"different-price-id\" } }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        _logger.Received(1).Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains($\"Could not find Organization's ({_organizationId}) password manager item\") &&\n                o.ToString().Contains(parsedEvent.Id)),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception, string>>());\n\n        // Should not update subscription or organization when password manager item not found\n        await _stripeFacade.DidNotReceive().UpdateSubscription(\n            Arg.Any<string>(),\n            Arg.Is<SubscriptionUpdateOptions>(o => o.Discounts != null));\n\n        await _organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any<Organization>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_AndUpdateFails_LogsErrorAndSendsTraditionalEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Simulate update failure\n        _stripeFacade\n            .UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .ThrowsAsync(new Exception(\"Stripe API error\"));\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains($\"Failed to align subscription concerns for Organization ({_organizationId})\") &&\n                o.ToString().Contains(parsedEvent.Type) &&\n                o.ToString().Contains(parsedEvent.Id)),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception, string>>());\n\n        // Should send traditional email when update fails\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"org@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n\n        // Verify renewal email was NOT sent\n        await _mailer.DidNotReceive().SendEmail(Arg.Any<Families2020RenewalMail>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_AndCouponNotFound_LogsErrorAndSendsTraditionalEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns((Coupon)null);\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Exception is caught, error is logged, and traditional email is sent\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains($\"Failed to align subscription concerns for Organization ({_organizationId})\") &&\n                o.ToString().Contains(parsedEvent.Type) &&\n                o.ToString().Contains(parsedEvent.Id)),\n            Arg.Is<Exception>(e => e is InvalidOperationException && e.Message.Contains(\"Coupon for sending families 2019 email\")),\n            Arg.Any<Func<object, Exception, string>>());\n\n        await _mailer.DidNotReceive().SendEmail(Arg.Any<Families2019RenewalMail>());\n\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"org@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_AndCouponPercentOffIsNull_LogsErrorAndSendsTraditionalEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        var coupon = new Coupon\n        {\n            Id = CouponIDs.Milestone3SubscriptionDiscount,\n            PercentOff = null\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Exception is caught, error is logged, and traditional email is sent\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains($\"Failed to align subscription concerns for Organization ({_organizationId})\") &&\n                o.ToString().Contains(parsedEvent.Type) &&\n                o.ToString().Contains(parsedEvent.Id)),\n            Arg.Is<Exception>(e => e is InvalidOperationException && e.Message.Contains(\"coupon.PercentOff\")),\n            Arg.Any<Func<object, Exception, string>>());\n\n        await _mailer.DidNotReceive().SendEmail(Arg.Any<Families2019RenewalMail>());\n\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"org@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_AndSeatAddOnExists_DeletesItem()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n        var seatAddOnItemId = \"si_seat_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }\n                    },\n\n                    new()\n                    {\n                        Id = seatAddOnItemId,\n                        Price = new Price { Id = \"personal-org-seat-annually\" },\n                        Quantity = 3\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        var coupon = new Coupon { PercentOff = 25, Id = CouponIDs.Milestone3SubscriptionDiscount };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            Arg.Is(subscriptionId),\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items.Count == 2 &&\n                o.Items[0].Id == passwordManagerItemId &&\n                o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&\n                o.Items[1].Id == seatAddOnItemId &&\n                o.Items[1].Deleted == true &&\n                o.Discounts.Count == 1 &&\n                o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&\n                o.ProrationBehavior == ProrationBehavior.None));\n\n        await _stripeFacade.Received(1).GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);\n\n        await _organizationRepository.Received(1).ReplaceAsync(\n            Arg.Is<Organization>(org =>\n                org.Id == _organizationId &&\n                org.PlanType == PlanType.FamiliesAnnually &&\n                org.Plan == familiesPlan.Name &&\n                org.UsersGetPremium == familiesPlan.UsersGetPremium &&\n                org.Seats == familiesPlan.PasswordManager.BaseSeats));\n\n        await _mailer.Received(1).SendEmail(\n            Arg.Is<Families2019RenewalMail>(email =>\n                email.ToEmails.Contains(\"org@example.com\") &&\n                email.Subject == \"Your Bitwarden Families renewal is updating\" &&\n                email.View.BaseMonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.BaseAnnualRenewalPrice == familiesPlan.PasswordManager.BasePrice.ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.DiscountAmount == $\"{coupon.PercentOff}%\"\n            ));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_AndSeatAddOnWithQuantityOne_DeletesItem()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n        var seatAddOnItemId = \"si_seat_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }\n                    },\n\n                    new()\n                    {\n                        Id = seatAddOnItemId,\n                        Price = new Price { Id = \"personal-org-seat-annually\" },\n                        Quantity = 1\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        var coupon = new Coupon { PercentOff = 25, Id = CouponIDs.Milestone3SubscriptionDiscount };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            Arg.Is(subscriptionId),\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items.Count == 2 &&\n                o.Items[0].Id == passwordManagerItemId &&\n                o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&\n                o.Items[1].Id == seatAddOnItemId &&\n                o.Items[1].Deleted == true &&\n                o.Discounts.Count == 1 &&\n                o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&\n                o.ProrationBehavior == ProrationBehavior.None));\n\n        await _stripeFacade.Received(1).GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);\n\n        await _organizationRepository.Received(1).ReplaceAsync(\n            Arg.Is<Organization>(org =>\n                org.Id == _organizationId &&\n                org.PlanType == PlanType.FamiliesAnnually &&\n                org.Plan == familiesPlan.Name &&\n                org.UsersGetPremium == familiesPlan.UsersGetPremium &&\n                org.Seats == familiesPlan.PasswordManager.BaseSeats));\n\n        await _mailer.Received(1).SendEmail(\n            Arg.Is<Families2019RenewalMail>(email =>\n                email.ToEmails.Contains(\"org@example.com\") &&\n                email.Subject == \"Your Bitwarden Families renewal is updating\" &&\n                email.View.BaseMonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.BaseAnnualRenewalPrice == familiesPlan.PasswordManager.BasePrice.ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.DiscountAmount == $\"{coupon.PercentOff}%\"\n            ));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_WithPremiumAccessAndSeatAddOn_UpdatesBothItems()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n        var premiumAccessItemId = \"si_premium_123\";\n        var seatAddOnItemId = \"si_seat_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2019Plan = new Families2019Plan();\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePlanId }\n                    },\n\n                    new()\n                    {\n                        Id = premiumAccessItemId,\n                        Price = new Price { Id = families2019Plan.PasswordManager.StripePremiumAccessPlanId }\n                    },\n\n                    new()\n                    {\n                        Id = seatAddOnItemId,\n                        Price = new Price { Id = \"personal-org-seat-annually\" },\n                        Quantity = 2\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2019\n        };\n\n        var coupon = new Coupon { PercentOff = 25, Id = CouponIDs.Milestone3SubscriptionDiscount };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019).Returns(families2019Plan);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            Arg.Is(subscriptionId),\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items.Count == 3 &&\n                o.Items[0].Id == passwordManagerItemId &&\n                o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&\n                o.Items[1].Id == premiumAccessItemId &&\n                o.Items[1].Deleted == true &&\n                o.Items[2].Id == seatAddOnItemId &&\n                o.Items[2].Deleted == true &&\n                o.Discounts.Count == 1 &&\n                o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&\n                o.ProrationBehavior == ProrationBehavior.None));\n\n        await _stripeFacade.Received(1).GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);\n\n        await _organizationRepository.Received(1).ReplaceAsync(\n            Arg.Is<Organization>(org =>\n                org.Id == _organizationId &&\n                org.PlanType == PlanType.FamiliesAnnually &&\n                org.Plan == familiesPlan.Name &&\n                org.UsersGetPremium == familiesPlan.UsersGetPremium &&\n                org.Seats == familiesPlan.PasswordManager.BaseSeats));\n\n        await _mailer.Received(1).SendEmail(\n            Arg.Is<Families2019RenewalMail>(email =>\n                email.ToEmails.Contains(\"org@example.com\") &&\n                email.Subject == \"Your Bitwarden Families renewal is updating\" &&\n                email.View.BaseMonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.BaseAnnualRenewalPrice == familiesPlan.PasswordManager.BasePrice.ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.DiscountAmount == $\"{coupon.PercentOff}%\"\n            ));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Enabled_AndFamilies2025Plan_UpdatesSubscriptionOnlyNoAddons()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2025Plan = new Families2025Plan();\n        var familiesPlan = new FamiliesPlan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId }\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2025\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2025).Returns(families2025Plan);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(familiesPlan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        await _stripeFacade.Received(1).UpdateSubscription(\n            Arg.Is(subscriptionId),\n            Arg.Is<SubscriptionUpdateOptions>(o =>\n                o.Items.Count == 1 &&\n                o.Items[0].Id == passwordManagerItemId &&\n                o.Items[0].Price == familiesPlan.PasswordManager.StripePlanId &&\n                o.Discounts == null &&\n                o.ProrationBehavior == ProrationBehavior.None));\n\n        await _organizationRepository.Received(1).ReplaceAsync(\n            Arg.Is<Organization>(org =>\n                org.Id == _organizationId &&\n                org.PlanType == PlanType.FamiliesAnnually &&\n                org.Plan == familiesPlan.Name &&\n                org.UsersGetPremium == familiesPlan.UsersGetPremium &&\n                org.Seats == familiesPlan.PasswordManager.BaseSeats));\n\n        await _mailer.Received(1).SendEmail(\n            Arg.Is<Families2020RenewalMail>(email =>\n                email.ToEmails.Contains(\"org@example.com\") &&\n                email.Subject == \"Your Bitwarden Families renewal is updating\" &&\n                email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString(\"C\", new CultureInfo(\"en-US\"))));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone3Disabled_AndFamilies2025Plan_DoesNotUpdateSubscription()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\", Type = \"invoice.upcoming\" };\n        var customerId = \"cus_123\";\n        var subscriptionId = \"sub_123\";\n        var passwordManagerItemId = \"si_pm_123\";\n\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 40000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n\n        var families2025Plan = new Families2025Plan();\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new()\n                    {\n                        Id = passwordManagerItemId,\n                        Price = new Price { Id = families2025Plan.PasswordManager.StripePlanId }\n                    }\n                ]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var customer = new Customer\n        {\n            Id = customerId,\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] },\n            Address = new Address { Country = \"US\" }\n        };\n\n        var organization = new Organization\n        {\n            Id = _organizationId,\n            BillingEmail = \"org@example.com\",\n            PlanType = PlanType.FamiliesAnnually2025\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService\n            .GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));\n        _organizationRepository.GetByIdAsync(_organizationId).Returns(organization);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2025).Returns(families2025Plan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);\n        _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - should not update subscription or organization when feature flag is disabled\n        await _stripeFacade.DidNotReceive().UpdateSubscription(\n            Arg.Any<string>(),\n            Arg.Any<SubscriptionUpdateOptions>());\n\n        await _organizationRepository.DidNotReceive().ReplaceAsync(\n            Arg.Is<Organization>(org => org.PlanType == PlanType.FamiliesAnnually));\n    }\n\n    #region Premium Renewal Email Tests\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone2Enabled_AndCouponNotFound_LogsErrorAndSendsTraditionalEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var customerId = \"cus_123\";\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new() { Id = \"si_123\", Price = new Price { Id = Prices.PremiumAnnually } }]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = customerId },\n            Metadata = new Dictionary<string, string>()\n        };\n        var user = new User { Id = _userId, Email = \"user@example.com\", Premium = true };\n        var plan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Purchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually },\n            Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }\n        };\n        var customer = new Customer\n        {\n            Id = customerId,\n            Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported },\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));\n        _userRepository.GetByIdAsync(_userId).Returns(user);\n        _pricingClient.GetAvailablePremiumPlan().Returns(plan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone2SubscriptionDiscount).Returns((Coupon)null);\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Exception is caught, error is logged, and traditional email is sent\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains($\"Failed to update user's ({user.Id}) subscription price id\") &&\n                o.ToString().Contains(parsedEvent.Id)),\n            Arg.Is<Exception>(e => e is InvalidOperationException\n                                   && e.Message == $\"Coupon for sending premium renewal email id:{CouponIDs.Milestone2SubscriptionDiscount} not found\"),\n            Arg.Any<Func<object, Exception, string>>());\n\n        await _mailer.DidNotReceive().SendEmail(Arg.Any<PremiumRenewalMail>());\n\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"user@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone2Enabled_AndCouponPercentOffIsNull_LogsErrorAndSendsTraditionalEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var customerId = \"cus_123\";\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new() { Id = \"si_123\", Price = new Price { Id = Prices.PremiumAnnually } }]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = customerId },\n            Metadata = new Dictionary<string, string>()\n        };\n        var user = new User { Id = _userId, Email = \"user@example.com\", Premium = true };\n        var plan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Purchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually },\n            Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }\n        };\n        var customer = new Customer\n        {\n            Id = customerId,\n            Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported },\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n        var coupon = new Coupon\n        {\n            Id = CouponIDs.Milestone2SubscriptionDiscount,\n            PercentOff = null\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));\n        _userRepository.GetByIdAsync(_userId).Returns(user);\n        _pricingClient.GetAvailablePremiumPlan().Returns(plan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone2SubscriptionDiscount).Returns(coupon);\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Exception is caught, error is logged, and traditional email is sent\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains($\"Failed to update user's ({user.Id}) subscription price id\") &&\n                o.ToString().Contains(parsedEvent.Id)),\n            Arg.Is<Exception>(e => e is InvalidOperationException\n                                   && e.Message == $\"coupon.PercentOff for sending premium renewal email id:{CouponIDs.Milestone2SubscriptionDiscount} is null\"),\n            Arg.Any<Func<object, Exception, string>>());\n\n        await _mailer.DidNotReceive().SendEmail(Arg.Any<PremiumRenewalMail>());\n\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"user@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone2Enabled_AndValidCoupon_SendsPremiumRenewalEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var customerId = \"cus_123\";\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new() { Id = \"si_123\", Price = new Price { Id = Prices.PremiumAnnually } }]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = customerId },\n            Metadata = new Dictionary<string, string>()\n        };\n        var user = new User { Id = _userId, Email = \"user@example.com\", Premium = true };\n        var plan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Purchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually },\n            Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }\n        };\n        var customer = new Customer\n        {\n            Id = customerId,\n            Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported },\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n        var coupon = new Coupon\n        {\n            Id = CouponIDs.Milestone2SubscriptionDiscount,\n            PercentOff = 30\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));\n        _userRepository.GetByIdAsync(_userId).Returns(user);\n        _pricingClient.GetAvailablePremiumPlan().Returns(plan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone2SubscriptionDiscount).Returns(coupon);\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert\n        var expectedDiscountedPrice = plan.Seat.Price * (100 - coupon.PercentOff.Value) / 100;\n        await _mailer.Received(1).SendEmail(\n            Arg.Is<PremiumRenewalMail>(email =>\n                email.ToEmails.Contains(\"user@example.com\") &&\n                email.Subject == \"Your Bitwarden Premium renewal is updating\" &&\n                email.View.BaseMonthlyRenewalPrice == (plan.Seat.Price / 12).ToString(\"C\", new CultureInfo(\"en-US\")) &&\n                email.View.DiscountAmount == \"30%\" &&\n                email.View.DiscountedAnnualRenewalPrice == expectedDiscountedPrice.ToString(\"C\", new CultureInfo(\"en-US\"))\n            ));\n\n        await _mailService.DidNotReceive().SendInvoiceUpcoming(\n            Arg.Any<IEnumerable<string>>(),\n            Arg.Any<decimal>(),\n            Arg.Any<DateTime>(),\n            Arg.Any<List<string>>(),\n            Arg.Any<bool>());\n    }\n\n    [Fact]\n    public async Task HandleAsync_WhenMilestone2Enabled_AndGetCouponThrowsException_LogsErrorAndSendsTraditionalEmail()\n    {\n        // Arrange\n        var parsedEvent = new Event { Id = \"evt_123\" };\n        var customerId = \"cus_123\";\n        var invoice = new Invoice\n        {\n            CustomerId = customerId,\n            AmountDue = 10000,\n            NextPaymentAttempt = DateTime.UtcNow.AddDays(7),\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = [new() { Description = \"Test Item\" }]\n            }\n        };\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            CustomerId = customerId,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new() { Id = \"si_123\", Price = new Price { Id = Prices.PremiumAnnually } }]\n            },\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false },\n            Customer = new Customer { Id = customerId },\n            Metadata = new Dictionary<string, string>()\n        };\n        var user = new User { Id = _userId, Email = \"user@example.com\", Premium = true };\n        var plan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Purchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually },\n            Storage = new Purchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }\n        };\n        var customer = new Customer\n        {\n            Id = customerId,\n            Tax = new CustomerTax { AutomaticTax = AutomaticTaxStatus.Supported },\n            Subscriptions = new StripeList<Subscription> { Data = [subscription] }\n        };\n\n        _stripeEventService.GetInvoice(parsedEvent).Returns(invoice);\n        _stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);\n        _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata)\n            .Returns(new Tuple<Guid?, Guid?, Guid?>(null, _userId, null));\n        _userRepository.GetByIdAsync(_userId).Returns(user);\n        _pricingClient.GetAvailablePremiumPlan().Returns(plan);\n        _featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);\n        _stripeFacade.GetCoupon(CouponIDs.Milestone2SubscriptionDiscount)\n            .ThrowsAsync(new StripeException(\"Stripe API error\"));\n        _stripeFacade.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n\n        // Act\n        await _sut.HandleAsync(parsedEvent);\n\n        // Assert - Exception is caught, error is logged, and traditional email is sent\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o =>\n                o.ToString().Contains($\"Failed to update user's ({user.Id}) subscription price id\") &&\n                o.ToString().Contains(parsedEvent.Id)),\n            Arg.Is<Exception>(e => e is StripeException),\n            Arg.Any<Func<object, Exception, string>>());\n\n        await _mailer.DidNotReceive().SendEmail(Arg.Any<PremiumRenewalMail>());\n\n        await _mailService.Received(1).SendInvoiceUpcoming(\n            Arg.Is<IEnumerable<string>>(emails => emails.Contains(\"user@example.com\")),\n            Arg.Is<decimal>(amount => amount == invoice.AmountDue / 100M),\n            Arg.Is<DateTime>(dueDate => dueDate == invoice.NextPaymentAttempt.Value),\n            Arg.Is<List<string>>(items => items.Count == invoice.Lines.Data.Count),\n            Arg.Is<bool>(b => b == true));\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Billing.Test/Utilities/EmbeddedResourceReader.cs",
    "content": "﻿using System.Reflection;\n\nnamespace Bit.Billing.Test.Utilities;\n\npublic static class EmbeddedResourceReader\n{\n    public static async Task<string> ReadAsync(string resourceType, string fileName)\n    {\n        var assembly = Assembly.GetExecutingAssembly();\n\n        await using var stream = assembly.GetManifestResourceStream($\"Bit.Billing.Test.Resources.{resourceType}.{fileName}\");\n\n        if (stream == null)\n        {\n            throw new Exception($\"Failed to retrieve manifest resource stream for file: {fileName}.\");\n        }\n\n        using var reader = new StreamReader(stream);\n\n        return await reader.ReadToEndAsync();\n    }\n}\n"
  },
  {
    "path": "test/Billing.Test/Utilities/PayPalTestIPN.cs",
    "content": "﻿namespace Bit.Billing.Test.Utilities;\n\npublic enum IPNBody\n{\n    SuccessfulPayment,\n    ECheckPayment,\n    TransactionMissingEntityIds,\n    NonUSDPayment,\n    SuccessfulPaymentForOrganizationCredit,\n    UnsupportedTransactionType,\n    SuccessfulRefund,\n    RefundMissingParentTransaction,\n    SuccessfulPaymentForUserCredit\n}\n\npublic static class PayPalTestIPN\n{\n    public static async Task<string> GetAsync(IPNBody ipnBody)\n    {\n        var fileName = ipnBody switch\n        {\n            IPNBody.ECheckPayment => \"echeck-payment.txt\",\n            IPNBody.NonUSDPayment => \"non-usd-payment.txt\",\n            IPNBody.RefundMissingParentTransaction => \"refund-missing-parent-transaction.txt\",\n            IPNBody.SuccessfulPayment => \"successful-payment.txt\",\n            IPNBody.SuccessfulPaymentForOrganizationCredit => \"successful-payment-org-credit.txt\",\n            IPNBody.SuccessfulRefund => \"successful-refund.txt\",\n            IPNBody.SuccessfulPaymentForUserCredit => \"successful-payment-user-credit.txt\",\n            IPNBody.TransactionMissingEntityIds => \"transaction-missing-entity-ids.txt\",\n            IPNBody.UnsupportedTransactionType => \"unsupported-transaction-type.txt\"\n        };\n\n        var content = await EmbeddedResourceReader.ReadAsync(\"IPN\", fileName);\n\n        return content.Replace(\"\\n\", string.Empty);\n    }\n}\n"
  },
  {
    "path": "test/Bitwarden.Tests.proj",
    "content": "<Project Sdk=\"Microsoft.Build.Traversal\">\n  <ItemGroup>\n    <ProjectReference Include=\"**\\*.*proj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs",
    "content": "﻿#nullable enable\n\nusing System.Reflection;\nusing AutoFixture;\nusing Bit.Test.Common.Helpers;\nusing Xunit.Sdk;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\n[DataDiscoverer(\"AutoFixture.Xunit2.NoPreDiscoveryDataDiscoverer\", \"AutoFixture.Xunit2\")]\npublic class BitAutoDataAttribute : DataAttribute\n{\n    private readonly Func<IFixture> _createFixture;\n    private readonly object?[] _fixedTestParameters;\n\n    public BitAutoDataAttribute() : this(Array.Empty<object>()) { }\n\n    public BitAutoDataAttribute(params object?[] fixedTestParameters) :\n        this(() => new Fixture(), fixedTestParameters)\n    { }\n\n    public BitAutoDataAttribute(Func<IFixture> createFixture, params object?[] fixedTestParameters) :\n        base()\n    {\n        _createFixture = createFixture;\n        _fixedTestParameters = fixedTestParameters;\n    }\n\n    public override IEnumerable<object?[]> GetData(MethodInfo testMethod)\n        => BitAutoDataAttributeHelpers.GetData(testMethod, _createFixture(), _fixedTestParameters);\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs",
    "content": "﻿using AutoFixture;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\n/// <summary>\n/// <para>\n///     Base class for customizing parameters in methods decorated with the\n///     Bit.Test.Common.AutoFixture.Attributes.MemberAutoDataAttribute.\n/// </para>\n/// ⚠ Warning ⚠ Will not insert customizations into AutoFixture's AutoDataAttribute build chain\n/// </summary>\n[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true)]\npublic abstract class BitCustomizeAttribute : Attribute\n{\n    /// <summary>\n    /// Gets a customization for the method's parameters.\n    /// </summary>\n    /// <returns>A customization for the method's parameters.</returns>\n    public abstract ICustomization GetCustomization();\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/BitMemberAutoDataAttribute.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Reflection;\nusing AutoFixture;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\npublic class BitMemberAutoDataAttribute : MemberDataAttributeBase\n{\n    private readonly Func<IFixture> _createFixture;\n\n    public BitMemberAutoDataAttribute(string memberName, params object[] parameters) :\n        this(() => new Fixture(), memberName, parameters)\n    { }\n\n    public BitMemberAutoDataAttribute(Func<IFixture> createFixture, string memberName, params object[] parameters) :\n        base(memberName, parameters)\n    {\n        _createFixture = createFixture;\n    }\n\n    protected override object[] ConvertDataItem(MethodInfo testMethod, object item) =>\n        BitAutoDataAttributeHelpers.GetData(testMethod, _createFixture(), item as object[]).First();\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/ControllerCustomizeAttribute.cs",
    "content": "﻿using AutoFixture;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\n/// <summary>\n/// Disables setting of Auto Properties on the Controller to avoid ASP.net initialization errors from a mock environment. Still sets constructor dependencies.\n/// </summary>\npublic class ControllerCustomizeAttribute : BitCustomizeAttribute\n{\n    private readonly Type _controllerType;\n\n    /// <summary>\n    /// Initialize an instance of the ControllerCustomizeAttribute class\n    /// </summary>\n    /// <param name=\"controllerType\">The Type of the controller to allow autofixture to create</param>\n    public ControllerCustomizeAttribute(Type controllerType)\n    {\n        _controllerType = controllerType;\n    }\n\n    public override ICustomization GetCustomization() => new ControllerCustomization(_controllerType);\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoFixture;\nusing AutoFixture.Xunit2;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\npublic class CustomAutoDataAttribute : AutoDataAttribute\n{\n    public CustomAutoDataAttribute(params Type[] iCustomizationTypes) : this(iCustomizationTypes\n        .Select(t => (ICustomization)Activator.CreateInstance(t)).ToArray())\n    { }\n\n    public CustomAutoDataAttribute(params ICustomization[] customizations) : base(() =>\n    {\n        var fixture = new Fixture().WithAutoNSubstitutions();\n        foreach (var customization in customizations)\n        {\n            fixture.Customize(customization);\n        }\n        return fixture;\n    })\n    { }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/EnvironmentDataAttribute.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Reflection;\nusing Xunit.Sdk;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\n/// <summary>\n/// Used for collecting data from environment useful for when we want to test an integration with another service and\n/// it might require an api key or other piece of sensitive data that we don't want slipping into the wrong hands.\n/// </summary>\n/// <remarks>\n/// It probably should be refactored to support fixtures and other customization so it can more easily be used in conjunction\n/// with more parameters.  Currently it attempt to match environment variable names to values of the parameter type in that positions.\n/// It will start from the first parameter and go for each supplied name.\n/// </remarks>\npublic class EnvironmentDataAttribute : DataAttribute\n{\n    private readonly string[] _environmentVariableNames;\n\n    public EnvironmentDataAttribute(params string[] environmentVariableNames)\n    {\n        _environmentVariableNames = environmentVariableNames;\n    }\n\n    public override IEnumerable<object[]> GetData(MethodInfo testMethod)\n    {\n        var methodParameters = testMethod.GetParameters();\n\n        if (methodParameters.Length < _environmentVariableNames.Length)\n        {\n            throw new ArgumentException($\"The target test method only has {methodParameters.Length} arguments but you supplied {_environmentVariableNames.Length}\");\n        }\n\n        var values = new object[_environmentVariableNames.Length];\n\n        for (var i = 0; i < _environmentVariableNames.Length; i++)\n        {\n            values[i] = Convert.ChangeType(Environment.GetEnvironmentVariable(_environmentVariableNames[i]), methodParameters[i].ParameterType);\n        }\n\n        return new[] { values };\n    }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Xunit;\nusing Xunit.Sdk;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\npublic class InlineCustomAutoDataAttribute : CompositeDataAttribute\n{\n    public InlineCustomAutoDataAttribute(Type[] iCustomizationTypes, params object[] values) : base(new DataAttribute[] {\n        new InlineDataAttribute(values),\n        new CustomAutoDataAttribute(iCustomizationTypes)\n    })\n    { }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/JsonDocumentCustomizeAttribute.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoFixture;\nusing Bit.Test.Common.AutoFixture.JsonDocumentFixtures;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\npublic class JsonDocumentCustomizeAttribute : BitCustomizeAttribute\n{\n    public string Json { get; set; }\n    public override ICustomization GetCustomization() => new JsonDocumentCustomization() { Json = Json };\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttribute.cs",
    "content": "﻿#nullable enable\nusing System.Reflection;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\n/// <summary>\n/// This attribute helps to generate all possible combinations of the provided pattern values for a given number of parameters.\n/// <remarks>\n/// <para>\n/// The repeating pattern values should be provided as an array for each parameter. Currently supports up to 3 parameters.\n/// </para>\n/// <para>\n/// The attribute is a variation of the <see cref=\"BitAutoDataAttribute\"/> attribute and can be used in the same way, except that all fixed value parameters needs to be provided as an array.\n/// </para>\n/// <para>\n/// Note: Use it with caution. While this attribute is useful for handling repeating parameters, having too many parameters should be avoided as it is considered a code smell in most of the cases.\n/// If your test requires more than 2 repeating parameters, or the test have too many conditions that change the behavior of the test, consider refactoring the test by splitting it into multiple smaller ones.\n/// </para>\n/// </remarks>\n/// <example>\n/// 1st example:\n/// <code>\n/// [RepeatingPatternBitAutoData([false], [1,2,3])]\n/// public void TestMethod(bool first, int second, SomeOtherData third, ...)\n/// </code>\n/// Would generate the following test cases:\n/// <list type=\"bullet\">\n/// <item>false, 1</item>\n/// <item>false, 2</item>\n/// <item>false, 3</item>\n/// </list>\n/// 2nd example:\n/// <code>\n/// [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])]\n/// public void TestMethod(bool first, bool second, bool third)\n/// </code>\n/// Would generate the following test cases:\n/// <list type=\"bullet\">\n/// <item>false, false, false</item>\n/// <item>false, false, true</item>\n/// <item>false, true, false</item>\n/// <item>false, true, true</item>\n/// <item>true, false, false</item>\n/// <item>true, false, true</item>\n/// <item>true, true, false</item>\n/// <item>true, true, true</item>\n/// </list>\n/// </example>\n/// </summary>\npublic class RepeatingPatternBitAutoDataAttribute : BitAutoDataAttribute\n{\n    private readonly List<object?[]> _repeatingDataList;\n\n    public RepeatingPatternBitAutoDataAttribute(object?[] first)\n    {\n        _repeatingDataList = AllValues([first]);\n    }\n\n    public RepeatingPatternBitAutoDataAttribute(object?[] first, object?[] second)\n    {\n        _repeatingDataList = AllValues([first, second]);\n    }\n\n    public RepeatingPatternBitAutoDataAttribute(object?[] first, object?[] second, object?[] third)\n    {\n        _repeatingDataList = AllValues([first, second, third]);\n    }\n\n    public override IEnumerable<object?[]> GetData(MethodInfo testMethod)\n    {\n        if (_repeatingDataList.Count == 0)\n        {\n            yield return base.GetData(testMethod).First();\n        }\n\n        foreach (var repeatingData in _repeatingDataList)\n        {\n            var bitData = base.GetData(testMethod).First();\n            for (var i = 0; i < repeatingData.Length; i++)\n            {\n                bitData[i] = repeatingData[i];\n            }\n\n            yield return bitData;\n        }\n    }\n\n    private static List<object?[]> AllValues(object?[][] parameterToPatternValues)\n    {\n        var result = new List<object?[]>();\n        GenerateCombinations(parameterToPatternValues, new object[parameterToPatternValues.Length], 0, result);\n        return result;\n    }\n\n    private static void GenerateCombinations(object?[][] parameterToPatternValues, object?[] current, int index,\n        List<object?[]> result)\n    {\n        if (index == current.Length)\n        {\n            result.Add((object[])current.Clone());\n            return;\n        }\n\n        var patternValues = parameterToPatternValues[index];\n\n        foreach (var value in patternValues)\n        {\n            current[index] = value;\n            GenerateCombinations(parameterToPatternValues, current, index + 1, result);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttributeTests.cs",
    "content": "﻿#nullable enable\nusing Xunit;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\npublic class RepeatingPatternBitAutoDataAttributeTests\n{\n    public class OneParam1 : IClassFixture<TestDataContext>\n    {\n        private readonly TestDataContext _context;\n\n        public OneParam1(TestDataContext context)\n        {\n            context.SetData(1, [], [], []);\n            _context = context;\n        }\n\n        [Theory]\n        [RepeatingPatternBitAutoData([])]\n        public void NoPattern_NoTestExecution(string autoDataFilled)\n        {\n            Assert.NotEmpty(autoDataFilled);\n            _context.TestExecuted();\n        }\n    }\n\n    public class OneParam2 : IClassFixture<TestDataContext>\n    {\n        private readonly TestDataContext _context;\n\n        public OneParam2(TestDataContext context)\n        {\n            context.SetData(2, [false, true], [], []);\n            _context = context;\n        }\n\n        [Theory]\n        [RepeatingPatternBitAutoData([false, true])]\n        public void TrueFalsePattern_2Executions(bool first, string autoDataFilled)\n        {\n            Assert.True(_context.ExpectedBooleans1.Remove(first));\n            Assert.NotEmpty(autoDataFilled);\n            _context.TestExecuted();\n        }\n    }\n\n    public class OneParam3 : IClassFixture<TestDataContext>\n    {\n        private readonly TestDataContext _context;\n\n        public OneParam3(TestDataContext context)\n        {\n            context.SetData(4, [], [], [null, \"\", \" \", \"\\t\"]);\n            _context = context;\n        }\n\n        [Theory]\n        [RepeatingPatternBitAutoData([null, \"\", \" \", \"\\t\"])]\n        public void NullableEmptyStringPattern_4Executions(string? first, string autoDataFilled)\n        {\n            Assert.True(_context.ExpectedStrings.Remove(first));\n            Assert.NotEmpty(autoDataFilled);\n            _context.TestExecuted();\n        }\n    }\n\n    public class OneParam4 : IClassFixture<TestDataContext>\n    {\n        private readonly TestDataContext _context;\n\n        public OneParam4(TestDataContext context)\n        {\n            context.SetData(6, [], [], [null, \"\", \" \", \"\\t\", \"\\n\", \" \\t\\n\"]);\n            _context = context;\n        }\n\n        [Theory]\n        [RepeatingPatternBitAutoData([null, \"\", \" \", \"\\t\"])] // 4 executions\n        [BitAutoData(\"\\n\")] // 1 execution\n        [BitAutoData(\" \\t\\n\", \"test data\")] // 1 execution\n        public void MixedPatternsWithBitAutoData_6Executions(string? first, string autoDataFilled)\n        {\n            Assert.True(_context.ExpectedStrings.Remove(first));\n            Assert.NotEmpty(autoDataFilled);\n            if (first == \" \\t\\n\")\n            {\n                Assert.Equal(\"test data\", autoDataFilled);\n            }\n\n            _context.TestExecuted();\n        }\n    }\n\n    public class TwoParams1 : IClassFixture<TestDataContext>\n    {\n        private readonly TestDataContext _context;\n\n        public TwoParams1(TestDataContext context)\n        {\n            context.SetData(8, TestDataContext.GenerateData([false, true], 4), [],\n                TestDataContext.GenerateData([null, \"\", \" \", \"\\t\"], 2));\n            _context = context;\n        }\n\n        [Theory]\n        [RepeatingPatternBitAutoData([false, true], [null, \"\", \" \", \"\\t\"])]\n        public void TrueFalsePatternFirstNullableEmptyStringPatternSecond_8Executions(\n            bool first, string? second,\n            string autoDataFilled)\n        {\n            Assert.True(_context.ExpectedBooleans1.Remove(first));\n            Assert.True(_context.ExpectedStrings.Remove(second));\n            Assert.NotEmpty(autoDataFilled);\n            _context.TestExecuted();\n        }\n    }\n\n    public class TwoParams2 : IClassFixture<TestDataContext>\n    {\n        private readonly TestDataContext _context;\n\n        public TwoParams2(TestDataContext context)\n        {\n            context.SetData(8, TestDataContext.GenerateData([false, true], 4), [],\n                TestDataContext.GenerateData([null, \"\", \" \", \"\\t\"], 2));\n            _context = context;\n        }\n\n        [Theory]\n        [RepeatingPatternBitAutoData([null, \"\", \" \", \"\\t\"], [false, true])]\n        public void NullableEmptyStringPatternFirstTrueFalsePatternSecond_8Executions(\n            string? first, bool second,\n            string autoDataFilled)\n        {\n            Assert.True(_context.ExpectedStrings.Remove(first));\n            Assert.True(_context.ExpectedBooleans1.Remove(second));\n            Assert.NotEmpty(autoDataFilled);\n            _context.TestExecuted();\n        }\n    }\n\n    public class TwoParams3 : IClassFixture<TestDataContext>\n    {\n        private readonly TestDataContext _context;\n\n        public TwoParams3(TestDataContext context)\n        {\n            var expectedBooleans1 = TestDataContext.GenerateData([false], 4);\n            expectedBooleans1.AddRange(TestDataContext.GenerateData([true], 5));\n            var expectedStrings = TestDataContext.GenerateData([null, \"\", \" \"], 2);\n            expectedStrings.AddRange([\"\\t\", \"\\n\", \" \\t\\n\"]);\n            context.SetData(9, expectedBooleans1, [], expectedStrings);\n            _context = context;\n        }\n\n        [Theory]\n        [RepeatingPatternBitAutoData([null, \"\", \" \"], [false, true])] // 6 executions\n        [RepeatingPatternBitAutoData([\"\\t\"], [false])] // 1 execution\n        [BitAutoData(\"\\n\", true)] // 1 execution\n        [BitAutoData(\" \\t\\n\", true, \"test data\")] // 1 execution\n        public void MixedPatternsWithBitAutoData_9Executions(\n            string? first, bool second,\n            string autoDataFilled)\n        {\n            Assert.True(_context.ExpectedStrings.Remove(first));\n            Assert.True(_context.ExpectedBooleans1.Remove(second));\n            Assert.NotEmpty(autoDataFilled);\n            if (first == \" \\t\\n\")\n            {\n                Assert.Equal(\"test data\", autoDataFilled);\n            }\n\n            _context.TestExecuted();\n        }\n    }\n\n    public class ThreeParams1 : IClassFixture<TestDataContext>\n    {\n        private readonly TestDataContext _context;\n\n        public ThreeParams1(TestDataContext context)\n        {\n            context.SetData(16, TestDataContext.GenerateData([false, true], 8),\n                TestDataContext.GenerateData([false, true], 8),\n                TestDataContext.GenerateData([null, \"\", \" \", \"\\t\"], 4));\n            _context = context;\n        }\n\n        [Theory]\n        [RepeatingPatternBitAutoData([false, true], [null, \"\", \" \", \"\\t\"], [false, true])]\n        public void TrueFalsePatternFirstNullableEmptyStringPatternSecondFalsePatternThird_16Executions(\n            bool first, string? second, bool third,\n            string autoDataFilled)\n        {\n            Assert.True(_context.ExpectedBooleans1.Remove(first));\n            Assert.True(_context.ExpectedStrings.Remove(second));\n            Assert.True(_context.ExpectedBooleans2.Remove(third));\n            Assert.NotEmpty(autoDataFilled);\n            _context.TestExecuted();\n        }\n    }\n\n    public class ThreeParams2 : IClassFixture<TestDataContext>\n    {\n        private readonly TestDataContext _context;\n\n        public ThreeParams2(TestDataContext context)\n        {\n            var expectedBooleans1 = TestDataContext.GenerateData([false, true], 6);\n            expectedBooleans1.AddRange(TestDataContext.GenerateData([true], 3));\n            var expectedBooleans2 = TestDataContext.GenerateData([false, true], 7);\n            expectedBooleans2.Add(true);\n            var expectedStrings = TestDataContext.GenerateData([null, \"\", \" \"], 4);\n            expectedStrings.AddRange([\"\\t\", \"\\t\", \" \\t\\n\"]);\n            context.SetData(15, expectedBooleans1, expectedBooleans2, expectedStrings);\n            _context = context;\n        }\n\n        [Theory]\n        [RepeatingPatternBitAutoData([false, true], [null, \"\", \" \"], [false, true])] // 12 executions\n        [RepeatingPatternBitAutoData([true], [\"\\t\"], [false, true])] // 2 executions\n        [BitAutoData(true, \" \\t\\n\", true, \"test data\")] // 1 execution\n        public void MixedPatternsWithBitAutoData_15Executions(\n            bool first, string? second, bool third,\n            string autoDataFilled)\n        {\n            Assert.True(_context.ExpectedBooleans1.Remove(first));\n            Assert.True(_context.ExpectedStrings.Remove(second));\n            Assert.True(_context.ExpectedBooleans2.Remove(third));\n            Assert.NotEmpty(autoDataFilled);\n            if (second == \" \\t\\n\")\n            {\n                Assert.Equal(\"test data\", autoDataFilled);\n            }\n\n            _context.TestExecuted();\n        }\n    }\n}\n\npublic class TestDataContext : IDisposable\n{\n    internal List<bool> ExpectedBooleans1 = [];\n    internal List<bool> ExpectedBooleans2 = [];\n\n    internal List<string?> ExpectedStrings = [];\n\n    private int _expectedExecutionCount;\n    private bool _dataSet;\n\n    public void TestExecuted()\n    {\n        _expectedExecutionCount--;\n    }\n\n    public void SetData(int expectedExecutionCount, List<bool> expectedBooleans1, List<bool> expectedBooleans2,\n        List<string?> expectedStrings)\n    {\n        if (_dataSet)\n        {\n            return;\n        }\n\n        _expectedExecutionCount = expectedExecutionCount;\n        ExpectedBooleans1 = expectedBooleans1;\n        ExpectedBooleans2 = expectedBooleans2;\n        ExpectedStrings = expectedStrings;\n\n        _dataSet = true;\n    }\n\n    public static List<T> GenerateData<T>(List<T> list, int count)\n    {\n        var repeatedList = new List<T>();\n        for (var i = 0; i < count; i++)\n        {\n            repeatedList.AddRange(list);\n        }\n\n        return repeatedList;\n    }\n\n    public void Dispose()\n    {\n        Assert.Equal(0, _expectedExecutionCount);\n        Assert.Empty(ExpectedBooleans1);\n        Assert.Empty(ExpectedBooleans2);\n        Assert.Empty(ExpectedStrings);\n    }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/RequiredEnvironmentTheoryAttribute.cs",
    "content": "﻿using Xunit;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\n/// <summary>\n/// Used for requiring certain environment variables exist at the time. Mostly used for more edge unit tests that shouldn't\n/// be run during CI builds or should only be ran in CI builds when pieces of information are available.\n/// </summary>\npublic class RequiredEnvironmentTheoryAttribute : TheoryAttribute\n{\n    private readonly string[] _environmentVariableNames;\n\n    public RequiredEnvironmentTheoryAttribute(params string[] environmentVariableNames)\n    {\n        _environmentVariableNames = environmentVariableNames;\n\n        if (!HasRequiredEnvironmentVariables())\n        {\n            Skip = $\"Missing one or more required environment variables. ({string.Join(\", \", _environmentVariableNames)})\";\n        }\n    }\n\n    private bool HasRequiredEnvironmentVariables()\n    {\n        foreach (var env in _environmentVariableNames)\n        {\n            var value = Environment.GetEnvironmentVariable(env);\n\n            if (value == null)\n            {\n                return false;\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/Attributes/SutProviderCustomizeAttribute.cs",
    "content": "﻿using AutoFixture;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\npublic class SutProviderCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new SutProviderCustomization();\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/BuilderWithoutAutoProperties.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoFixture;\nusing AutoFixture.Kernel;\n\nnamespace Bit.Test.Common.AutoFixture;\n\npublic class BuilderWithoutAutoProperties : ISpecimenBuilder\n{\n    private readonly Type _type;\n    public BuilderWithoutAutoProperties(Type type)\n    {\n        _type = type;\n    }\n\n    public object Create(object request, ISpecimenContext context)\n    {\n        if (context == null)\n        {\n            throw new ArgumentNullException(nameof(context));\n        }\n\n        var type = request as Type;\n        if (type == null || type != _type)\n        {\n            return new NoSpecimen();\n        }\n\n        var fixture = new Fixture();\n        // This is the equivalent of _fixture.Build<_type>().OmitAutoProperties().Create(request, context), but no overload for\n        // Build(Type type) exists.\n        dynamic reflectedComposer = typeof(Fixture).GetMethod(\"Build\").MakeGenericMethod(_type).Invoke(fixture, null);\n        return reflectedComposer.OmitAutoProperties().Create(request, context);\n    }\n}\npublic class BuilderWithoutAutoProperties<T> : ISpecimenBuilder\n{\n    public object Create(object request, ISpecimenContext context) =>\n        new BuilderWithoutAutoProperties(typeof(T)).Create(request, context);\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/ControllerCustomization.cs",
    "content": "﻿using AutoFixture;\nusing Microsoft.AspNetCore.Mvc;\n\nnamespace Bit.Test.Common.AutoFixture;\n\n/// <summary>\n/// Disables setting of Auto Properties on the Controller to avoid ASP.net initialization errors. Still sets constructor dependencies.\n/// </summary>\n/// <param name=\"fixture\"></param>\npublic class ControllerCustomization : ICustomization\n{\n    private readonly Type _controllerType;\n    public ControllerCustomization(Type controllerType)\n    {\n        if (!controllerType.IsAssignableTo(typeof(Controller)))\n        {\n            throw new Exception($\"{nameof(controllerType)} must derive from {typeof(Controller).Name}\");\n        }\n\n        _controllerType = controllerType;\n    }\n\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customizations.Add(new BuilderWithoutAutoProperties(_controllerType));\n    }\n}\npublic class ControllerCustomization<T> : ICustomization where T : Controller\n{\n    public void Customize(IFixture fixture) => new ControllerCustomization(typeof(T)).Customize(fixture);\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/FixtureExtensions.cs",
    "content": "﻿using AutoFixture;\nusing AutoFixture.AutoNSubstitute;\n\nnamespace Bit.Test.Common.AutoFixture;\n\npublic static class FixtureExtensions\n{\n    public static IFixture WithAutoNSubstitutions(this IFixture fixture)\n        => fixture.Customize(new AutoNSubstituteCustomization());\n\n    public static IFixture WithAutoNSubstitutionsAutoPopulatedProperties(this IFixture fixture)\n        => fixture.Customize(new AutoNSubstituteCustomization { ConfigureMembers = true });\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/GlobalSettingsFixtures.cs",
    "content": "﻿using AutoFixture;\n\nnamespace Bit.Test.Common.AutoFixture;\n\npublic class GlobalSettings : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<Bit.Core.Settings.GlobalSettings>(composer => composer\n            .Without(s => s.BaseServiceUri)\n            .Without(s => s.Attachment)\n            .Without(s => s.Send)\n            .Without(s => s.DataProtection));\n    }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/ISutProvider.cs",
    "content": "﻿namespace Bit.Test.Common.AutoFixture;\n\npublic interface ISutProvider\n{\n    Type SutType { get; }\n    ISutProvider Create();\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/JsonDocumentFixtures.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Text.Json;\nusing AutoFixture;\nusing AutoFixture.Kernel;\n\nnamespace Bit.Test.Common.AutoFixture.JsonDocumentFixtures;\n\npublic class JsonDocumentCustomization : ICustomization, ISpecimenBuilder\n{\n\n    public string Json { get; set; }\n\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customizations.Add(this);\n    }\n\n    public object Create(object request, ISpecimenContext context)\n    {\n        if (context == null)\n        {\n            throw new ArgumentNullException(nameof(context));\n        }\n        var type = request as Type;\n        if (type == null || (type != typeof(JsonDocument)))\n        {\n            return new NoSpecimen();\n        }\n\n        return JsonDocument.Parse(Json ?? \"{}\");\n    }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/SutProvider.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Kernel;\n\nnamespace Bit.Test.Common.AutoFixture;\n\n/// <summary>\n/// A utility class that encapsulates a system under test (sut) and its dependencies.\n/// By default, all dependencies are initialized as mocks using the NSubstitute library.\n/// SutProvider provides an interface for accessing these dependencies in the arrange and assert stages of your tests.\n/// </summary>\n/// <typeparam name=\"TSut\">The concrete implementation of the class being tested.</typeparam>\npublic class SutProvider<TSut> : ISutProvider\n{\n    /// <summary>\n    /// A record of the configured dependencies (constructor parameters). The outer Dictionary is keyed by the dependency's\n    /// type, and the inner dictionary is keyed by the parameter name (optionally used to disambiguate parameters with the same type).\n    /// The inner dictionary value is the dependency.\n    /// </summary>\n    private Dictionary<Type, Dictionary<string, object>> _dependencies;\n    private readonly IFixture _fixture;\n    private readonly ConstructorParameterRelay<TSut> _constructorParameterRelay;\n\n    public TSut Sut { get; private set; }\n    public Type SutType => typeof(TSut);\n    public IFixture Fixture => _fixture;\n\n    public SutProvider() : this(new Fixture()) { }\n\n    public SutProvider(IFixture fixture)\n    {\n        _dependencies = new Dictionary<Type, Dictionary<string, object>>();\n        _fixture = (fixture ?? new Fixture()).WithAutoNSubstitutions().Customize(new GlobalSettings());\n        _constructorParameterRelay = new ConstructorParameterRelay<TSut>(this, _fixture);\n        _fixture.Customizations.Add(_constructorParameterRelay);\n    }\n\n    /// <summary>\n    /// Registers a dependency to be injected when the sut is created. You must call <see cref=\"Create\"/> after\n    /// this method to (re)create the sut with the dependency.\n    /// </summary>\n    /// <param name=\"dependency\">The dependency to register.</param>\n    /// <param name=\"parameterName\">An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this.</param>\n    /// <typeparam name=\"T\">The type to register the dependency under - usually an interface. This should match the type expected by the sut's constructor.</typeparam>\n    /// <returns></returns>\n    public SutProvider<TSut> SetDependency<T>(T dependency, string parameterName = \"\")\n        => SetDependency(typeof(T), dependency, parameterName);\n\n    /// <summary>\n    /// An overload for <see cref=\"SetDependency{T}\"/> which takes a runtime <see cref=\"Type\"/> object rather than a compile-time type.\n    /// </summary>\n    private SutProvider<TSut> SetDependency(Type dependencyType, object dependency, string parameterName = \"\")\n    {\n        if (_dependencies.TryGetValue(dependencyType, out var dependencyForType))\n        {\n            dependencyForType[parameterName] = dependency;\n        }\n        else\n        {\n            _dependencies[dependencyType] = new Dictionary<string, object> { { parameterName, dependency } };\n        }\n\n        return this;\n    }\n\n    /// <summary>\n    /// Creates and registers a dependency to be injected when the sut is created.\n    /// </summary>\n    /// <typeparam name=\"TDep\">The Dependency type to create</typeparam>\n    /// <param name=\"parameterName\">The (optional) parameter name to register the dependency under</param>\n    /// <returns>The created dependency value</returns>\n    public TDep CreateDependency<TDep>(string parameterName = \"\")\n    {\n        var dependency = _fixture.Create<TDep>();\n        SetDependency(dependency, parameterName);\n        return dependency;\n    }\n\n    /// <summary>\n    /// Gets a dependency of the sut. Can only be called after the dependency has been set, either explicitly with\n    /// <see cref=\"SetDependency{T}\"/> or automatically with <see cref=\"Create\"/>.\n    /// As dependencies are initialized with NSubstitute mocks by default, this is often used to retrieve those mocks in order to\n    /// configure them during the arrange stage, or check received calls in the assert stage.\n    /// </summary>\n    /// <param name=\"parameterName\">An optional parameter name to disambiguate the dependency if there are multiple of the same type. You generally don't need this.</param>\n    /// <typeparam name=\"T\">The type of the dependency you want to get - usually an interface.</typeparam>\n    /// <returns>The dependency.</returns>\n    public T GetDependency<T>(string parameterName = \"\") => (T)GetDependency(typeof(T), parameterName);\n\n    /// <summary>\n    /// An overload for <see cref=\"GetDependency{T}\"/> which takes a runtime <see cref=\"Type\"/> object rather than a compile-time type.\n    /// </summary>\n    private object GetDependency(Type dependencyType, string parameterName = \"\")\n    {\n        if (DependencyIsSet(dependencyType, parameterName))\n        {\n            return _dependencies[dependencyType][parameterName];\n        }\n\n        if (_dependencies.TryGetValue(dependencyType, out var knownDependencies))\n        {\n            if (knownDependencies.Values.Count == 1)\n            {\n                return knownDependencies.Values.Single();\n            }\n\n            throw new ArgumentException(string.Concat($\"Dependency of type {dependencyType.Name} and name \",\n                $\"{parameterName} does not exist. Available dependency names are: \",\n                string.Join(\", \", knownDependencies.Keys)));\n        }\n\n        throw new ArgumentException($\"Dependency of type {dependencyType.Name} and name {parameterName} has not been set.\");\n    }\n\n    /// <summary>\n    /// Clear all the dependencies and the sut. This reverts the SutProvider back to a fully uninitialized state.\n    /// </summary>\n    public void Reset()\n    {\n        _dependencies = new Dictionary<Type, Dictionary<string, object>>();\n        Sut = default;\n    }\n\n    /// <summary>\n    /// Recreate a new sut with all new dependencies. This will reset all dependencies, including mocked return values\n    /// and any dependencies set with <see cref=\"SetDependency{T}\"/>.\n    /// </summary>\n    public void Recreate()\n    {\n        _dependencies = new Dictionary<Type, Dictionary<string, object>>();\n        Sut = _fixture.Create<TSut>();\n    }\n\n    /// <inheritdoc cref=\"Create()\"/>>\n    ISutProvider ISutProvider.Create() => Create();\n\n    /// <summary>\n    /// Creates the sut, injecting any dependencies configured via <see cref=\"SetDependency{T}\"/> and falling back to\n    /// NSubstitute mocks for any dependencies that have not been explicitly configured.\n    /// </summary>\n    /// <returns></returns>\n    public SutProvider<TSut> Create()\n    {\n        Sut = _fixture.Create<TSut>();\n        return this;\n    }\n\n    private bool DependencyIsSet(Type dependencyType, string parameterName = \"\")\n        => _dependencies.ContainsKey(dependencyType) && _dependencies[dependencyType].ContainsKey(parameterName);\n\n    private object GetDefault(Type type) => type.IsValueType ? Activator.CreateInstance(type) : null;\n\n    /// <summary>\n    /// A specimen builder which tells Autofixture to use the dependency registered in <see cref=\"SutProvider{T}\"/>\n    /// when creating test data. If no matching dependency exists in <see cref=\"SutProvider{TSut}\"/>, it creates\n    /// an NSubstitute mock and registers it using <see cref=\"SutProvider{TSut}.SetDependency{T}\"/>\n    /// so it can be retrieved later.\n    /// This is the link between <see cref=\"SutProvider{T}\"/> and Autofixture.\n    /// </summary>\n    /// <remarks>\n    /// Autofixture knows how to create sample data of simple types (such as an int or string) but not more complex classes.\n    /// We create our own <see cref=\"ISpecimenBuilder\"/> and register it with the <see cref=\"Fixture\"/> in\n    /// <see cref=\"SutProvider{TSut}\"/> to provide that instruction.\n    /// </remarks>\n    /// <typeparam name=\"T\">The type of the sut.</typeparam>\n    private class ConstructorParameterRelay<T> : ISpecimenBuilder\n    {\n        private readonly SutProvider<T> _sutProvider;\n        private readonly IFixture _fixture;\n\n        public ConstructorParameterRelay(SutProvider<T> sutProvider, IFixture fixture)\n        {\n            _sutProvider = sutProvider;\n            _fixture = fixture;\n        }\n\n        public object Create(object request, ISpecimenContext context)\n        {\n            // Basic checks to filter out irrelevant requests from Autofixture\n            if (context == null)\n            {\n                throw new ArgumentNullException(nameof(context));\n            }\n            if (!(request is ParameterInfo parameterInfo))\n            {\n                return new NoSpecimen();\n            }\n            if (parameterInfo.Member.DeclaringType != typeof(T) ||\n                parameterInfo.Member.MemberType != MemberTypes.Constructor)\n            {\n                return new NoSpecimen();\n            }\n\n            // Use the dependency set under this parameter name, if any\n            if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, parameterInfo.Name))\n            {\n                return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name);\n            }\n\n            // Use the default dependency set for this type, if any (i.e. no parameter name has been specified)\n            if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, \"\"))\n            {\n                return _sutProvider.GetDependency(parameterInfo.ParameterType, \"\");\n            }\n\n            // Fallback: pass the request down the chain. This lets another fixture customization populate the value.\n            // If you haven't added any customizations, this should be an NSubstitute mock.\n            // It is registered with SetDependency so you can retrieve it later.\n\n            // This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for\n            // Create(Type type) exists.\n            var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,\n                _sutProvider.GetDefault(parameterInfo.ParameterType)));\n            _sutProvider.SetDependency(parameterInfo.ParameterType, dependency, parameterInfo.Name);\n            return dependency;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/SutProviderCustomization.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoFixture;\nusing AutoFixture.Kernel;\n\nnamespace Bit.Test.Common.AutoFixture.Attributes;\n\npublic class SutProviderCustomization : ICustomization, ISpecimenBuilder\n{\n    private IFixture _fixture = null;\n\n    public object Create(object request, ISpecimenContext context)\n    {\n        if (context == null)\n        {\n            throw new ArgumentNullException(nameof(context));\n        }\n        if (!(request is Type typeRequest))\n        {\n            return new NoSpecimen();\n        }\n        if (!typeof(ISutProvider).IsAssignableFrom(typeRequest))\n        {\n            return new NoSpecimen();\n        }\n\n        return ((ISutProvider)Activator.CreateInstance(typeRequest, _fixture)).Create();\n    }\n\n    public void Customize(IFixture fixture)\n    {\n        _fixture = fixture;\n        fixture.Customizations.Add(this);\n    }\n}\n"
  },
  {
    "path": "test/Common/AutoFixture/SutProviderExtensions.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing AutoFixture;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing RichardSzalay.MockHttp;\n\nnamespace Bit.Test.Common.AutoFixture;\n\npublic static class SutProviderExtensions\n{\n    public static SutProvider<T> ConfigureBaseIdentityClientService<T>(this SutProvider<T> sutProvider,\n        string requestUrlFragment, HttpMethod requestHttpMethod, string identityResponse = null, string apiResponse = null)\n        where T : BaseIdentityClientService\n    {\n        var fixture = new Fixture().WithAutoNSubstitutionsAutoPopulatedProperties();\n        fixture.AddMockHttp();\n\n        var settings = fixture.Create<IGlobalSettings>();\n        settings.SelfHosted = true;\n        settings.EnableCloudCommunication = true;\n\n        var apiUri = fixture.Create<Uri>();\n        var identityUri = fixture.Create<Uri>();\n        settings.Installation.ApiUri.Returns(apiUri.ToString());\n        settings.Installation.IdentityUri.Returns(identityUri.ToString());\n\n        var apiHandler = new MockHttpMessageHandler();\n        var identityHandler = new MockHttpMessageHandler();\n        var syncUri = string.Concat(apiUri, requestUrlFragment);\n        var tokenUri = string.Concat(identityUri, \"connect/token\");\n\n        apiHandler.When(requestHttpMethod, syncUri)\n            .Respond(\"application/json\", apiResponse);\n        identityHandler.When(HttpMethod.Post, tokenUri)\n            .Respond(\"application/json\", identityResponse ?? \"{\\\"access_token\\\":\\\"string\\\",\\\"expires_in\\\":3600,\\\"token_type\\\":\\\"Bearer\\\",\\\"scope\\\":\\\"string\\\"}\");\n\n\n        var apiHttp = apiHandler.ToHttpClient();\n        var identityHttp = identityHandler.ToHttpClient();\n\n        var mockHttpClientFactory = Substitute.For<IHttpClientFactory>();\n        mockHttpClientFactory.CreateClient(Arg.Is(\"client\")).Returns(apiHttp);\n        mockHttpClientFactory.CreateClient(Arg.Is(\"identity\")).Returns(identityHttp);\n\n        return sutProvider\n            .SetDependency(settings)\n            .SetDependency(mockHttpClientFactory)\n            .Create();\n    }\n\n    /// <summary>\n    /// Configures SutProvider to use FakeTimeProvider.\n    /// It is registered under both the TimeProvider type and the FakeTimeProvider type\n    /// so that it can be retrieved in a type-safe manner with GetDependency.\n    /// This can be chained with other builder methods; make sure to call\n    /// <see cref=\"ISutProvider.Create\"/> before use.\n    /// </summary>\n    public static SutProvider<T> WithFakeTimeProvider<T>(this SutProvider<T> sutProvider)\n    {\n        var fakeTimeProvider = new FakeTimeProvider();\n        return sutProvider\n            .SetDependency((TimeProvider)fakeTimeProvider)\n            .SetDependency(fakeTimeProvider);\n    }\n}\n"
  },
  {
    "path": "test/Common/Common.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <IsPackable>false</IsPackable>\n    <RootNamespace>Bit.Test.Common</RootNamespace>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n\n    <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.TimeProvider.Testing\" Version=\"8.10.0\" />\n    <PackageReference Include=\"NSubstitute\" Version=\"$(NSubstituteVersion)\" />\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"AutoFixture.Xunit2\" Version=\"$(AutoFixtureXUnit2Version)\" />\n    <PackageReference Include=\"AutoFixture.AutoNSubstitute\" Version=\"$(AutoFixtureAutoNSubstituteVersion)\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"Kralizek.AutoFixture.Extensions.MockHttp\" Version=\"2.1.0\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Core\\Core.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Common/Constants/TestEncryptionConstants.cs",
    "content": "﻿namespace Bit.Test.Common.Constants;\n\npublic static class TestEncryptionConstants\n{\n\n    // Simple stubs for different encrypted string versions\n    [Obsolete]\n    public const string AES256_CBC_B64_Encstring = \"0.stub\";\n    public const string AES256_CBC_HMAC_EmptySuffix = \"2.\";\n    // Intended for use as a V1 encrypted string, accepted by validators\n    public const string AES256_CBC_HMAC_Encstring = \"2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\";\n    public const string RSA2048_OAEPSHA1_B64_Encstring = \"4.stub\";\n    public const string XCHACHA20POLY1305_B64_Encstring = \"7.stub\";\n\n    // Public key test placeholder\n    public const string PublicKey = \"pk_test\";\n\n    // V2-style values used across tests\n    // Private key indicating v2 (used in multiple tests to mark v2 state)\n    public const string V2PrivateKey = \"7.cose\";\n    // Wrapped signing key and verifying key values from real tests\n    public const string V2WrappedSigningKey = \"7.cose_signing\";\n    public const string V2VerifyingKey = \"vk\";\n}\n"
  },
  {
    "path": "test/Common/Fakes/FakeDataProtectorTokenFactory.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing Bit.Core.Tokens;\n\nnamespace Bit.Test.Common.Fakes;\n\n/// <summary>\n/// Used to fake the IDataProtectorTokenFactory for testing purposes.\n/// Generalized for use with all Tokenables.\n/// </summary>\npublic class FakeDataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where T : Tokenable, new()\n{\n    // Instead of real encryption, use a simple Dictionary to emulate protection/unprotection\n    private readonly Dictionary<string, T> _tokenDatabase = new Dictionary<string, T>();\n\n    public string Protect(T data)\n    {\n        // Generate a simple token representation\n        var token = Guid.NewGuid().ToString();\n\n        // Store the data against the token\n        _tokenDatabase[token] = data;\n\n        return token;\n    }\n\n    public T Unprotect(string token)\n    {\n        // If the token exists in the dictionary, return the corresponding data\n        if (_tokenDatabase.TryGetValue(token, out var data))\n        {\n            return data;\n        }\n\n        // If the token doesn't exist, throw an exception similar to a decryption failure.\n        throw new Exception(\"Failed to unprotect token.\");\n    }\n\n    public bool TryUnprotect(string token, out T data)\n    {\n        try\n        {\n            data = Unprotect(token);\n            return true;\n        }\n        catch\n        {\n            data = default;\n            return false;\n        }\n    }\n\n    public bool TokenValid(string token)\n    {\n        return _tokenDatabase.ContainsKey(token);\n    }\n}\n"
  },
  {
    "path": "test/Common/Helpers/AssertHelper.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Collections;\nusing System.Diagnostics;\nusing System.Linq.Expressions;\nusing System.Reflection;\nusing System.Text.Json;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Http;\nusing Xunit;\nusing Xunit.Sdk;\n\nnamespace Bit.Test.Common.Helpers;\n\npublic static class AssertHelper\n{\n    public static void AssertPropertyEqual(object expected, object actual, params string[] excludedPropertyStrings)\n    {\n        var relevantExcludedProperties = excludedPropertyStrings.Where(name => !name.Contains('.')).ToList();\n        if (expected == null)\n        {\n            Assert.Null(actual);\n            return;\n        }\n\n        if (actual == null)\n        {\n            throw new Exception(\"Actual object is null but expected is not\");\n        }\n\n        foreach (var expectedPropInfo in expected.GetType().GetProperties().Where(pi => !relevantExcludedProperties.Contains(pi.Name) && !pi.GetIndexParameters().Any()))\n        {\n            var actualPropInfo = actual.GetType().GetProperty(expectedPropInfo.Name);\n\n            if (actualPropInfo == null)\n            {\n                throw new Exception(string.Concat($\"Expected actual object to contain a property named {expectedPropInfo.Name}, but it does not\\n\",\n                $\"Expected:\\n{JsonSerializer.Serialize(expected, JsonHelpers.Indented)}\\n\",\n                $\"Actual:\\n{JsonSerializer.Serialize(actual, JsonHelpers.Indented)}\"));\n            }\n\n            if (typeof(IComparable).IsAssignableFrom(expectedPropInfo.PropertyType) || expectedPropInfo.PropertyType.IsPrimitive || expectedPropInfo.PropertyType.IsValueType)\n            {\n                Assert.Equal(expectedPropInfo.GetValue(expected), actualPropInfo.GetValue(actual));\n            }\n            else if (expectedPropInfo.PropertyType == typeof(JsonDocument) && actualPropInfo.PropertyType == typeof(JsonDocument))\n            {\n                static string JsonDocString(PropertyInfo info, object obj) => JsonSerializer.Serialize(info.GetValue(obj));\n                Assert.Equal(JsonDocString(expectedPropInfo, expected), JsonDocString(actualPropInfo, actual));\n            }\n            else if (typeof(IEnumerable).IsAssignableFrom(expectedPropInfo.PropertyType) && typeof(IEnumerable).IsAssignableFrom(actualPropInfo.PropertyType))\n            {\n                var expectedItems = ((IEnumerable)expectedPropInfo.GetValue(expected)).Cast<object>();\n                var actualItems = ((IEnumerable)actualPropInfo.GetValue(actual)).Cast<object>();\n\n                AssertPropertyEqualPredicate(expectedItems, excludedPropertyStrings)(actualItems);\n            }\n            else\n            {\n                var prefix = $\"{expectedPropInfo.PropertyType.Name}.\";\n                var nextExcludedProperties = excludedPropertyStrings.Where(name => name.StartsWith(prefix))\n                    .Select(name => name[prefix.Length..]).ToArray();\n                AssertPropertyEqual(expectedPropInfo.GetValue(expected), actualPropInfo.GetValue(actual), nextExcludedProperties);\n            }\n        }\n    }\n\n    private static Predicate<T> AssertPropertyEqualPredicate<T>(T expected, params string[] excludedPropertyStrings) => (actual) =>\n    {\n        AssertPropertyEqual(expected, actual, excludedPropertyStrings);\n        return true;\n    };\n\n    public static Expression<Predicate<T>> AssertPropertyEqual<T>(T expected, params string[] excludedPropertyStrings) =>\n        (T actual) => AssertPropertyEqualPredicate(expected, excludedPropertyStrings)(actual);\n\n    private static Predicate<IEnumerable<T>> AssertPropertyEqualPredicate<T>(IEnumerable<T> expected, params string[] excludedPropertyStrings) => (actual) =>\n    {\n        // IEnumerable.Zip doesn't account for different lengths, we need to check this ourselves\n        if (actual.Count() != expected.Count())\n        {\n            throw new Exception(string.Concat($\"Actual IEnumerable does not have the expected length.\\n\",\n            $\"Expected: {expected.Count()}\\n\",\n            $\"Actual: {actual.Count()}\"));\n        }\n\n        var elements = expected.Zip(actual);\n        foreach (var (expectedEl, actualEl) in elements)\n        {\n            AssertPropertyEqual(expectedEl, actualEl, excludedPropertyStrings);\n        }\n\n        return true;\n    };\n\n    public static Expression<Predicate<IEnumerable<T>>> AssertPropertyEqual<T>(IEnumerable<T> expected, params string[] excludedPropertyStrings) =>\n        (actual) => AssertPropertyEqualPredicate(expected, excludedPropertyStrings)(actual);\n\n    private static Predicate<T> AssertEqualExpectedPredicate<T>(T expected) => (actual) =>\n    {\n        Assert.Equal(expected, actual);\n        return true;\n    };\n\n    public static Expression<Predicate<T>> AssertEqualExpected<T>(T expected) =>\n        (T actual) => AssertEqualExpectedPredicate(expected)(actual);\n\n    [StackTraceHidden]\n    public static JsonElement AssertJsonProperty(JsonElement element, string propertyName, JsonValueKind jsonValueKind)\n    {\n        if (!element.TryGetProperty(propertyName, out var subElement))\n        {\n            throw new XunitException($\"Could not find property by name '{propertyName}'\");\n        }\n\n        Assert.Equal(jsonValueKind, subElement.ValueKind);\n        return subElement;\n    }\n\n    public static void AssertEqualJson(JsonElement a, JsonElement b)\n    {\n        switch (a.ValueKind)\n        {\n            case JsonValueKind.Array:\n                Assert.Equal(JsonValueKind.Array, b.ValueKind);\n                AssertEqualJsonArray(a, b);\n                break;\n            case JsonValueKind.Object:\n                Assert.Equal(JsonValueKind.Object, b.ValueKind);\n                AssertEqualJsonObject(a, b);\n                break;\n            case JsonValueKind.False:\n                Assert.Equal(JsonValueKind.False, b.ValueKind);\n                break;\n            case JsonValueKind.True:\n                Assert.Equal(JsonValueKind.True, b.ValueKind);\n                break;\n            case JsonValueKind.Number:\n                Assert.Equal(JsonValueKind.Number, b.ValueKind);\n                Assert.Equal(a.GetDouble(), b.GetDouble());\n                break;\n            case JsonValueKind.String:\n                Assert.Equal(JsonValueKind.String, b.ValueKind);\n                Assert.Equal(a.GetString(), b.GetString());\n                break;\n            case JsonValueKind.Null:\n                Assert.Equal(JsonValueKind.Null, b.ValueKind);\n                break;\n            default:\n                throw new XunitException($\"Bad JsonValueKind '{a.ValueKind}'\");\n        }\n    }\n\n    private static void AssertEqualJsonObject(JsonElement a, JsonElement b)\n    {\n        Debug.Assert(a.ValueKind == JsonValueKind.Object && b.ValueKind == JsonValueKind.Object);\n\n        var aObjectEnumerator = a.EnumerateObject();\n        var bObjectEnumerator = b.EnumerateObject();\n\n        while (true)\n        {\n            var aCanMove = aObjectEnumerator.MoveNext();\n            var bCanMove = bObjectEnumerator.MoveNext();\n\n            if (aCanMove)\n            {\n                Assert.True(bCanMove, $\"a was able to enumerate over object '{a}' but b was NOT able to '{b}'\");\n            }\n            else\n            {\n                Assert.False(bCanMove, $\"a was NOT able to enumerate over object '{a}' but b was able to '{b}'\");\n            }\n\n            if (aCanMove == false && bCanMove == false)\n            {\n                // They both can't continue to enumerate at the same time, that is valid\n                break;\n            }\n\n            var aProp = aObjectEnumerator.Current;\n            var bProp = bObjectEnumerator.Current;\n\n            Assert.Equal(aProp.Name, bProp.Name);\n            // Recursion!\n            AssertEqualJson(aProp.Value, bProp.Value);\n        }\n    }\n\n    private static void AssertEqualJsonArray(JsonElement a, JsonElement b)\n    {\n        Debug.Assert(a.ValueKind == JsonValueKind.Array && b.ValueKind == JsonValueKind.Array);\n\n        var aArrayEnumerator = a.EnumerateArray();\n        var bArrayEnumerator = b.EnumerateArray();\n\n        while (true)\n        {\n            var aCanMove = aArrayEnumerator.MoveNext();\n            var bCanMove = bArrayEnumerator.MoveNext();\n\n            if (aCanMove)\n            {\n                Assert.True(bCanMove, $\"a was able to enumerate over array '{a}' but b was NOT able to '{b}'\");\n            }\n            else\n            {\n                Assert.False(bCanMove, $\"a was NOT able to enumerate over array '{a}' but b was able to '{b}'\");\n            }\n\n            if (aCanMove == false && bCanMove == false)\n            {\n                // They both can't continue to enumerate at the same time, that is valid\n                break;\n            }\n\n            var aElement = aArrayEnumerator.Current;\n            var bElement = bArrayEnumerator.Current;\n\n            // Recursion!\n            AssertEqualJson(aElement, bElement);\n        }\n    }\n\n    public async static Task<T> AssertResponseTypeIs<T>(HttpContext context)\n    {\n        return await JsonSerializer.DeserializeAsync<T>(context.Response.Body);\n    }\n\n    public static TimeSpan AssertRecent(DateTime dateTime, int skewSeconds = 5)\n        => AssertRecent(dateTime, TimeSpan.FromSeconds(skewSeconds));\n\n    public static TimeSpan AssertRecent(DateTime dateTime, TimeSpan skew)\n    {\n        var difference = DateTime.UtcNow - dateTime;\n        Assert.True(difference < skew);\n        return difference;\n    }\n}\n"
  },
  {
    "path": "test/Common/Helpers/BitAutoDataAttributeHelpers.cs",
    "content": "﻿#nullable enable\n\nusing System.ComponentModel;\nusing System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Kernel;\nusing AutoFixture.Xunit2;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Test.Common.Helpers;\n\npublic static class BitAutoDataAttributeHelpers\n{\n    public static IEnumerable<object?[]> GetData(MethodInfo testMethod, IFixture fixture, object?[] fixedTestParameters)\n    {\n        var methodParameters = testMethod.GetParameters();\n        // We aren't worried about a test method not having a class it belongs to.\n        var classCustomizations = testMethod.DeclaringType!.GetCustomAttributes<BitCustomizeAttribute>().Select(attr => attr.GetCustomization());\n        var methodCustomizations = testMethod.GetCustomAttributes<BitCustomizeAttribute>().Select(attr => attr.GetCustomization());\n\n        fixedTestParameters ??= Array.Empty<object>();\n\n        fixture = ApplyCustomizations(ApplyCustomizations(fixture, classCustomizations), methodCustomizations);\n\n        // The first n number of parameters should be match to the supplied parameters\n        var fixedTestInputParameters = methodParameters.Take(fixedTestParameters.Length).Zip(fixedTestParameters);\n\n        var missingParameters = methodParameters.Skip(fixedTestParameters.Length).Select(p => CustomizeAndCreate(p, fixture));\n\n        return new object?[1][] { ConvertFixedParameters(fixedTestInputParameters.ToArray()).Concat(missingParameters).ToArray() };\n    }\n\n    public static object CustomizeAndCreate(ParameterInfo p, IFixture fixture)\n    {\n        var customizations = p.GetCustomAttributes(typeof(CustomizeAttribute), false)\n            .OfType<CustomizeAttribute>()\n            .Select(attr => attr.GetCustomization(p));\n\n        var context = new SpecimenContext(ApplyCustomizations(fixture, customizations));\n        return context.Resolve(p);\n    }\n\n    public static IFixture ApplyCustomizations(IFixture fixture, IEnumerable<ICustomization> customizations)\n    {\n        var newFixture = new Fixture();\n\n        foreach (var customization in fixture.Customizations.Reverse().Select(b => b.ToCustomization()))\n        {\n            newFixture.Customize(customization);\n        }\n\n        foreach (var customization in customizations)\n        {\n            newFixture.Customize(customization);\n        }\n\n        return newFixture;\n    }\n\n    public static IEnumerable<object?> ConvertFixedParameters((ParameterInfo Parameter, object? Value)[] fixedParameters)\n    {\n        var output = new object?[fixedParameters.Length];\n        for (var i = 0; i < fixedParameters.Length; i++)\n        {\n            var (parameter, value) = fixedParameters[i];\n            // If the value is null, just return the value\n            if (value is null || value.GetType() == parameter.ParameterType)\n            {\n                output[i] = value;\n                continue;\n            }\n\n            // If the value is a string and it's not a perfect match, try to convert it.\n            if (value is string stringValue)\n            {\n                // \n                if (parameter.ParameterType.IsGenericType && parameter.ParameterType.GetGenericTypeDefinition() == typeof(Nullable<>))\n                {\n                    if (TryConvertToType(stringValue, Nullable.GetUnderlyingType(parameter.ParameterType)!, out var nullableConvertedValue))\n                    {\n                        output[i] = nullableConvertedValue;\n                        continue;\n                    }\n\n                    // We couldn't convert it, so set it as the input value and let XUnit throw\n                    output[i] = value;\n                    continue;\n                }\n\n                if (TryConvertToType(stringValue, parameter.ParameterType, out var convertedValue))\n                {\n                    output[i] = convertedValue;\n                    continue;\n                }\n\n                // We couldn't convert it, so set it as the input value and let XUnit throw\n                output[i] = value;\n            }\n\n            // No easy conversion, give them back the value\n            output[i] = value;\n        }\n\n        return output;\n    }\n\n    private static bool TryConvertToType(string value, Type destinationType, out object? convertedValue)\n    {\n        convertedValue = null;\n\n        if (string.IsNullOrEmpty(value))\n        {\n            return false;\n        }\n\n        var converter = TypeDescriptor.GetConverter(destinationType);\n\n        if (converter.CanConvertFrom(typeof(string)))\n        {\n            convertedValue = converter.ConvertFromInvariantString(value);\n            return true;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "test/Common/Helpers/HtmlBuilder.cs",
    "content": "﻿using System.Text;\n\nnamespace Bit.Test.Common.Helpers;\n\npublic class HtmlBuilder\n{\n    private string _topLevelNode;\n    private readonly StringBuilder _builder = new();\n\n    public HtmlBuilder(string topLevelNode = \"html\")\n    {\n        _topLevelNode = CoerceTopLevelNode(topLevelNode);\n    }\n\n    public HtmlBuilder Append(string node)\n    {\n        _builder.Append(node);\n        return this;\n    }\n\n    public HtmlBuilder Append(HtmlBuilder builder)\n    {\n        _builder.Append(builder.ToString());\n        return this;\n    }\n\n    public HtmlBuilder WithAttribute(string name, string value)\n    {\n        _topLevelNode = $\"{_topLevelNode} {name}=\\\"{value}\\\"\";\n        return this;\n    }\n\n    public override string ToString()\n    {\n        _builder.Insert(0, $\"<{_topLevelNode}>\");\n        _builder.Append($\"</{_topLevelNode}>\");\n        return _builder.ToString();\n    }\n\n    private static string CoerceTopLevelNode(string topLevelNode)\n    {\n        var result = topLevelNode;\n        if (topLevelNode.StartsWith(\"<\"))\n        {\n            result = topLevelNode[1..];\n        }\n        if (topLevelNode.EndsWith(\">\"))\n        {\n            result = result[..^1];\n        }\n\n        if (topLevelNode.IndexOf(\">\") != -1)\n        {\n            throw new ArgumentException(\"Top level nodes cannot contain '>' characters.\");\n        }\n\n        return result;\n    }\n}\n"
  },
  {
    "path": "test/Common/Helpers/TestCaseHelper.cs",
    "content": "﻿namespace Bit.Test.Common.Helpers;\n\npublic static class TestCaseHelper\n{\n    public static IEnumerable<IEnumerable<T>> GetCombinations<T>(params T[] items)\n    {\n        var count = Math.Pow(2, items.Length);\n        for (var i = 0; i < count; i++)\n        {\n            var str = Convert.ToString(i, 2).PadLeft(items.Length, '0');\n            List<T> combination = new();\n            for (var j = 0; j < str.Length; j++)\n            {\n                if (str[j] == '1')\n                {\n                    combination.Add(items[j]);\n                }\n            }\n            yield return combination;\n        }\n    }\n\n    public static IEnumerable<IEnumerable<object>> GetCombinationsOfMultipleLists(params IEnumerable<object>[] optionLists)\n    {\n        if (!optionLists.Any())\n        {\n            yield break;\n        }\n\n        foreach (var item in optionLists.First())\n        {\n            var itemArray = new[] { item };\n\n            if (optionLists.Length == 1)\n            {\n                yield return itemArray;\n            }\n\n            foreach (var nextCombination in GetCombinationsOfMultipleLists(optionLists.Skip(1).ToArray()))\n            {\n                yield return itemArray.Concat(nextCombination);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Common/MockedHttpClient/HttpRequestMatcher.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\n\nnamespace Bit.Test.Common.MockedHttpClient;\n\npublic class HttpRequestMatcher : IHttpRequestMatcher\n{\n    private readonly Func<HttpRequestMessage, bool> _matcher;\n    private HttpRequestMatcher? _childMatcher;\n    private MockedHttpResponse _mockedResponse = new(HttpStatusCode.OK);\n    private bool _responseSpecified = false;\n\n    public int NumberOfMatches { get; private set; }\n\n    /// <summary>\n    /// Returns whether or not the provided request can be handled by this matcher chain.\n    /// </summary>\n    /// <param name=\"request\"></param>\n    /// <returns></returns>\n    public bool Matches(HttpRequestMessage request) => _matcher(request) && (_childMatcher == null || _childMatcher.Matches(request));\n\n    public HttpRequestMatcher(HttpMethod method)\n    {\n        _matcher = request => request.Method == method;\n    }\n\n    public HttpRequestMatcher(string uri)\n    {\n        _matcher = request => request.RequestUri == new Uri(uri);\n    }\n\n    public HttpRequestMatcher(Uri uri)\n    {\n        _matcher = request => request.RequestUri == uri;\n    }\n\n    public HttpRequestMatcher(HttpMethod method, string uri)\n    {\n        _matcher = request => request.Method == method && request.RequestUri == new Uri(uri);\n    }\n\n    public HttpRequestMatcher(Func<HttpRequestMessage, bool> matcher)\n    {\n        _matcher = matcher;\n    }\n\n    public HttpRequestMatcher WithHeader(string name, string value)\n    {\n        return AddChild(request => request.Headers.TryGetValues(name, out var values) && values.Contains(value));\n    }\n\n    public HttpRequestMatcher WithQueryParameters(Dictionary<string, string> requiredQueryParameters) =>\n        WithQueryParameters(requiredQueryParameters.Select(x => $\"{x.Key}={x.Value}\").ToArray());\n    public HttpRequestMatcher WithQueryParameters(string name, string value) =>\n        WithQueryParameters($\"{name}={value}\");\n    public HttpRequestMatcher WithQueryParameters(params string[] queryKeyValues)\n    {\n        bool matcher(HttpRequestMessage request)\n        {\n            var query = request.RequestUri?.Query;\n            if (query == null)\n            {\n                return false;\n            }\n\n            return queryKeyValues.All(queryKeyValue => query.Contains(queryKeyValue));\n        }\n        return AddChild(matcher);\n    }\n\n    /// <summary>\n    /// Configure how this matcher should respond to matching HttpRequestMessages.\n    /// Note, after specifying a response, you can no longer further specify match criteria.\n    /// </summary>\n    /// <param name=\"statusCode\"></param>\n    /// <returns></returns>\n    public MockedHttpResponse RespondWith(HttpStatusCode statusCode)\n    {\n        _responseSpecified = true;\n        _mockedResponse = new MockedHttpResponse(statusCode);\n        return _mockedResponse;\n    }\n\n    /// <summary>\n    /// Called to produce an HttpResponseMessage for the given request. This is probably something you want to leave alone\n    /// </summary>\n    /// <param name=\"request\"></param>\n    public async Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request)\n    {\n        NumberOfMatches++;\n        return await (_childMatcher == null ? _mockedResponse.RespondToAsync(request) : _childMatcher.RespondToAsync(request));\n    }\n\n    private HttpRequestMatcher AddChild(Func<HttpRequestMessage, bool> matcher)\n    {\n        if (_responseSpecified)\n        {\n            throw new Exception(\"Cannot continue to configure a matcher after a response has been specified\");\n        }\n        _childMatcher = new HttpRequestMatcher(matcher);\n        return _childMatcher;\n    }\n}\n"
  },
  {
    "path": "test/Common/MockedHttpClient/HttpResponseBuilder.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\n\nnamespace Bit.Test.Common.MockedHttpClient;\n\npublic class HttpResponseBuilder : IDisposable\n{\n    private bool _disposedValue;\n\n    public HttpStatusCode StatusCode { get; set; }\n    public IEnumerable<KeyValuePair<string, string>> Headers { get; set; } = new List<KeyValuePair<string, string>>();\n    public IEnumerable<string> HeadersToRemove { get; set; } = new List<string>();\n    public HttpContent Content { get; set; }\n\n    public async Task<HttpResponseMessage> ToHttpResponseAsync()\n    {\n        var copiedContentStream = new MemoryStream();\n        await Content.CopyToAsync(copiedContentStream); // This is important, otherwise the content stream will be disposed when the response is disposed.\n        copiedContentStream.Seek(0, SeekOrigin.Begin);\n        var message = new HttpResponseMessage(StatusCode)\n        {\n            Content = new StreamContent(copiedContentStream),\n        };\n\n        foreach (var header in Headers)\n        {\n            message.Headers.TryAddWithoutValidation(header.Key, header.Value);\n        }\n\n        return message;\n    }\n\n    public HttpResponseBuilder WithStatusCode(HttpStatusCode statusCode)\n    {\n        return new()\n        {\n            StatusCode = statusCode,\n            Headers = Headers,\n            HeadersToRemove = HeadersToRemove,\n            Content = Content,\n        };\n    }\n\n    public HttpResponseBuilder WithHeader(string name, string value)\n    {\n        return new()\n        {\n            StatusCode = StatusCode,\n            Headers = Headers.Append(new KeyValuePair<string, string>(name, value)),\n            HeadersToRemove = HeadersToRemove,\n            Content = Content,\n        };\n    }\n\n    public HttpResponseBuilder WithContent(HttpContent content)\n    {\n        return new()\n        {\n            StatusCode = StatusCode,\n            Headers = Headers,\n            HeadersToRemove = HeadersToRemove,\n            Content = content,\n        };\n    }\n\n    protected virtual void Dispose(bool disposing)\n    {\n        if (!_disposedValue)\n        {\n            if (disposing)\n            {\n                Content?.Dispose();\n            }\n\n            _disposedValue = true;\n        }\n    }\n\n    public void Dispose()\n    {\n        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method\n        Dispose(disposing: true);\n        GC.SuppressFinalize(this);\n    }\n}\n"
  },
  {
    "path": "test/Common/MockedHttpClient/IHttpRequestMatcher.cs",
    "content": "﻿#nullable enable\n\nnamespace Bit.Test.Common.MockedHttpClient;\n\npublic interface IHttpRequestMatcher\n{\n    int NumberOfMatches { get; }\n    bool Matches(HttpRequestMessage request);\n    Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request);\n}\n"
  },
  {
    "path": "test/Common/MockedHttpClient/IMockedHttpResponse.cs",
    "content": "﻿namespace Bit.Test.Common.MockedHttpClient;\n\npublic interface IMockedHttpResponse\n{\n    int NumberOfResponses { get; }\n    Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request);\n}\n"
  },
  {
    "path": "test/Common/MockedHttpClient/MockedHttpMessageHandler.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\n\nnamespace Bit.Test.Common.MockedHttpClient;\n\npublic class MockedHttpMessageHandler : HttpMessageHandler\n{\n    private readonly List<IHttpRequestMatcher> _matchers = new();\n\n    public List<HttpRequestMessage> CapturedRequests { get; } = new List<HttpRequestMessage>();\n\n    /// <summary>\n    /// The fallback handler to use when the request does not match any of the provided matchers.\n    /// </summary>\n    /// <returns>A Matcher that responds with 404 Not Found</returns>\n    public MockedHttpResponse Fallback { get; set; } = new(HttpStatusCode.NotFound);\n\n    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n    {\n        CapturedRequests.Add(request);\n        var matcher = _matchers.FirstOrDefault(x => x.Matches(request));\n        if (matcher == null)\n        {\n            return await Fallback.RespondToAsync(request);\n        }\n\n        return await matcher.RespondToAsync(request);\n    }\n\n    /// <summary>\n    /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.\n    /// </summary>\n    /// <param name=\"requestMatcher\"></param>\n    /// <typeparam name=\"T\"></typeparam>\n    /// <returns></returns>\n    public T When<T>(T requestMatcher) where T : IHttpRequestMatcher\n    {\n        _matchers.Add(requestMatcher);\n        return requestMatcher;\n    }\n\n    /// <summary>\n    /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.\n    /// </summary>\n    /// <param name=\"requestMatcher\"></param>\n    /// <typeparam name=\"T\"></typeparam>\n    /// <returns></returns>\n    public HttpRequestMatcher When(string uri)\n    {\n        var matcher = new HttpRequestMatcher(uri);\n        _matchers.Add(matcher);\n        return matcher;\n    }\n\n    /// <summary>\n    /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.\n    /// </summary>\n    /// <param name=\"requestMatcher\"></param>\n    /// <typeparam name=\"T\"></typeparam>\n    /// <returns></returns>\n    public HttpRequestMatcher When(Uri uri)\n    {\n        var matcher = new HttpRequestMatcher(uri);\n        _matchers.Add(matcher);\n        return matcher;\n    }\n\n    /// <summary>\n    /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.\n    /// </summary>\n    /// <param name=\"requestMatcher\"></param>\n    /// <typeparam name=\"T\"></typeparam>\n    /// <returns></returns>\n    public HttpRequestMatcher When(HttpMethod method)\n    {\n        var matcher = new HttpRequestMatcher(method);\n        _matchers.Add(matcher);\n        return matcher;\n    }\n\n    /// <summary>\n    /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.\n    /// </summary>\n    /// <param name=\"requestMatcher\"></param>\n    /// <typeparam name=\"T\"></typeparam>\n    /// <returns></returns>\n    public HttpRequestMatcher When(HttpMethod method, string uri)\n    {\n        var matcher = new HttpRequestMatcher(method, uri);\n        _matchers.Add(matcher);\n        return matcher;\n    }\n\n    /// <summary>\n    /// Instantiates a new HttpRequestMessage matcher that will handle requests in fitting with the returned matcher. Configuration can be chained.\n    /// </summary>\n    /// <param name=\"requestMatcher\"></param>\n    /// <typeparam name=\"T\"></typeparam>\n    /// <returns></returns>\n    public HttpRequestMatcher When(Func<HttpRequestMessage, bool> matcher)\n    {\n        var requestMatcher = new HttpRequestMatcher(matcher);\n        _matchers.Add(requestMatcher);\n        return requestMatcher;\n    }\n\n    /// <summary>\n    /// Converts the MockedHttpMessageHandler to a HttpClient that can be used in your tests after setup.\n    /// </summary>\n    /// <returns></returns>\n    public HttpClient ToHttpClient()\n    {\n        return new HttpClient(this);\n    }\n}\n"
  },
  {
    "path": "test/Common/MockedHttpClient/MockedHttpResponse.cs",
    "content": "﻿// FIXME: Update this file to be null safe and then delete the line below\n#nullable disable\n\nusing System.Net;\nusing System.Net.Http.Headers;\nusing System.Text;\n\nnamespace Bit.Test.Common.MockedHttpClient;\n\npublic class MockedHttpResponse : IMockedHttpResponse\n{\n    private MockedHttpResponse _childResponse;\n    private readonly Func<HttpRequestMessage, HttpResponseBuilder, HttpResponseBuilder> _responder;\n\n    public int NumberOfResponses { get; private set; }\n\n    public MockedHttpResponse(HttpStatusCode statusCode)\n    {\n        _responder = (_, builder) => builder.WithStatusCode(statusCode);\n    }\n\n    private MockedHttpResponse(Func<HttpRequestMessage, HttpResponseBuilder, HttpResponseBuilder> responder)\n    {\n        _responder = responder;\n    }\n\n    public MockedHttpResponse WithStatusCode(HttpStatusCode statusCode)\n    {\n        return AddChild((_, builder) => builder.WithStatusCode(statusCode));\n    }\n\n    public MockedHttpResponse WithHeader(string name, string value)\n    {\n        return AddChild((_, builder) => builder.WithHeader(name, value));\n    }\n    public MockedHttpResponse WithHeaders(params KeyValuePair<string, string>[] headers)\n    {\n        return AddChild((_, builder) => headers.Aggregate(builder, (b, header) => b.WithHeader(header.Key, header.Value)));\n    }\n\n    public MockedHttpResponse WithContent(string mediaType, string content)\n    {\n        return WithContent(new StringContent(content, Encoding.UTF8, mediaType));\n    }\n    public MockedHttpResponse WithContent(string mediaType, byte[] content)\n    {\n        return WithContent(new ByteArrayContent(content) { Headers = { ContentType = new MediaTypeHeaderValue(mediaType) } });\n    }\n    public MockedHttpResponse WithContent(HttpContent content)\n    {\n        return AddChild((_, builder) => builder.WithContent(content));\n    }\n\n    public async Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request)\n    {\n        return await RespondToAsync(request, new HttpResponseBuilder());\n    }\n\n    private async Task<HttpResponseMessage> RespondToAsync(HttpRequestMessage request, HttpResponseBuilder currentBuilder)\n    {\n        NumberOfResponses++;\n        var nextBuilder = _responder(request, currentBuilder);\n        return await (_childResponse == null ? nextBuilder.ToHttpResponseAsync() : _childResponse.RespondToAsync(request, nextBuilder));\n    }\n\n    private MockedHttpResponse AddChild(Func<HttpRequestMessage, HttpResponseBuilder, HttpResponseBuilder> responder)\n    {\n        _childResponse = new MockedHttpResponse(responder);\n        return _childResponse;\n    }\n}\n"
  },
  {
    "path": "test/Common/Test/TestCaseHelperTests.cs",
    "content": "﻿using Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Test.Common.Test;\n\npublic class TestCaseHelperTests\n{\n    [Fact]\n    public void GetCombinations_EmptyList()\n    {\n        Assert.Equal(new[] { Array.Empty<int>() }, TestCaseHelper.GetCombinations(Array.Empty<int>()).ToArray());\n    }\n\n    [Fact]\n    public void GetCombinations_OneItemList()\n    {\n        Assert.Equal(new[] { Array.Empty<int>(), new[] { 1 } }, TestCaseHelper.GetCombinations(1));\n    }\n\n    [Fact]\n    public void GetCombinations_TwoItemList()\n    {\n        Assert.Equal(new[] { Array.Empty<int>(), new[] { 2 }, new[] { 1 }, new[] { 1, 2 } }, TestCaseHelper.GetCombinations(1, 2));\n    }\n\n    [Fact]\n    public void GetCombinationsOfMultipleLists_OneOne()\n    {\n        Assert.Equal(new[] { new object[] { 1, \"1\" } }, TestCaseHelper.GetCombinationsOfMultipleLists(new object[] { 1 }, new object[] { \"1\" }));\n    }\n\n\n    [Fact]\n    public void GetCombinationsOfMultipleLists_OneTwo()\n    {\n        Assert.Equal(new[] { new object[] { 1, \"1\" }, new object[] { 1, \"2\" } }, TestCaseHelper.GetCombinationsOfMultipleLists(new object[] { 1 }, new object[] { \"1\", \"2\" }));\n    }\n\n    [Fact]\n    public void GetCombinationsOfMultipleLists_TwoOne()\n    {\n        Assert.Equal(new[] { new object[] { 1, \"1\" }, new object[] { 2, \"1\" } }, TestCaseHelper.GetCombinationsOfMultipleLists(new object[] { 1, 2 }, new object[] { \"1\" }));\n    }\n\n    [Fact]\n    public void GetCombinationsOfMultipleLists_TwoTwo()\n    {\n        Assert.Equal(new[] { new object[] { 1, \"1\" }, new object[] { 1, \"2\" }, new object[] { 2, \"1\" }, new object[] { 2, \"2\" } }, TestCaseHelper.GetCombinationsOfMultipleLists(new object[] { 1, 2 }, new object[] { \"1\", \"2\" }));\n    }\n}\n"
  },
  {
    "path": "test/Core.IntegrationTest/Core.IntegrationTest.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net8.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n\n    <IsPackable>false</IsPackable>\n    <IsTestProject>true</IsTestProject>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"8.0.0\" />\n    <PackageReference Include=\"MartinCostello.Logging.XUnit\" Version=\"0.7.0\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"Rnwood.SmtpServer\" Version=\"3.1.0-ci0868\" />\n    <PackageReference Include=\"xunit\" Version=\"2.9.3\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Using Include=\"Xunit\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Core\\Core.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "test/Core.IntegrationTest/MailKitSmtpMailDeliveryServiceTests.cs",
    "content": "﻿using System.Security.Cryptography;\nusing System.Security.Cryptography.X509Certificates;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Settings;\nusing MailKit.Security;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Rnwood.SmtpServer;\nusing Rnwood.SmtpServer.Extensions.Auth;\nusing Xunit.Abstractions;\n\nnamespace Bit.Core.IntegrationTest;\n\npublic class MailKitSmtpMailDeliveryServiceTests\n{\n    private static int _loggingConfigured;\n    private readonly X509Certificate2 _selfSignedCert;\n\n    public MailKitSmtpMailDeliveryServiceTests(ITestOutputHelper testOutputHelper)\n    {\n        ConfigureSmtpServerLogging(testOutputHelper);\n\n        _selfSignedCert = CreateSelfSignedCert(\"localhost\");\n    }\n\n    private static X509Certificate2 CreateSelfSignedCert(string commonName)\n    {\n        using var rsa = RSA.Create(2048);\n        var certRequest = new CertificateRequest($\"CN={commonName}\", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\n        return certRequest.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1));\n    }\n\n    private static void ConfigureSmtpServerLogging(ITestOutputHelper testOutputHelper)\n    {\n        // The logging in SmtpServer is configured statically so if we add it for each test it duplicates\n        // but we cant add the logger statically either because we need ITestOutputHelper\n        if (Interlocked.CompareExchange(ref _loggingConfigured, 1, 0) == 0)\n        {\n            return;\n        }\n        // Unfortunately this package doesn't public expose its logging infrastructure\n        // so we use private reflection to try and access it. \n        try\n        {\n            var loggingType = typeof(DefaultServerBehaviour).Assembly.GetType(\"Rnwood.SmtpServer.Logging\")\n                ?? throw new Exception(\"No type found in RnWood.SmtpServer named 'Logging'\");\n\n            var factoryProperty = loggingType.GetProperty(\"Factory\")\n                ?? throw new Exception($\"No property named 'Factory' found on class {loggingType.FullName}\");\n\n            var factoryPropertyGet = factoryProperty.GetMethod\n                ?? throw new Exception($\"{loggingType.FullName}.{factoryProperty.Name} does not have a get method.\");\n\n            if (factoryPropertyGet.Invoke(null, null) is not ILoggerFactory loggerFactory)\n            {\n                throw new Exception($\"{loggingType.FullName}.{factoryProperty.Name} is not of type 'ILoggerFactory'\" +\n                    $\"instead it's type '{factoryProperty.PropertyType.FullName}'\");\n            }\n\n            loggerFactory.AddXUnit(testOutputHelper);\n        }\n        catch (Exception ex)\n        {\n            testOutputHelper.WriteLine($\"Failed to configure logging for RnWood.SmtpServer (logging will not be configured):\\n{ex.Message}\");\n        }\n    }\n    private static int RandomPort()\n    {\n        return Random.Shared.Next(50000, 60000);\n    }\n\n    private static GlobalSettings GetSettings(Action<GlobalSettings> configure)\n    {\n        var globalSettings = new GlobalSettings();\n        globalSettings.SiteName = \"TestSiteName\";\n        globalSettings.Mail.ReplyToEmail = \"test@example.com\";\n        globalSettings.Mail.Smtp.Host = \"localhost\";\n        // Set common defaults\n        configure(globalSettings);\n        return globalSettings;\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_SmtpServerUsingSelfSignedCert_CertNotInTrustedRootStore_ThrowsException()\n    {\n        // If an SMTP server is using a self signed cert we currently require\n        // that the certificate for their SMTP server is installed in the root CA\n        // we are building the ability to do so without installing it, when we add that\n        // this test can be copied, and changed to utilize that new feature and instead of\n        // failing it should successfully send the email.\n        var port = RandomPort();\n        var behavior = new DefaultServerBehaviour(false, port, _selfSignedCert);\n        using var smtpServer = new SmtpServer(behavior);\n        smtpServer.Start();\n\n        var globalSettings = GetSettings(gs =>\n        {\n            gs.Mail.Smtp.Port = port;\n            gs.Mail.Smtp.Ssl = true;\n        });\n\n        var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(\n            globalSettings,\n            NullLogger<MailKitSmtpMailDeliveryService>.Instance\n        );\n\n        await Assert.ThrowsAsync<SslHandshakeException>(\n            async () => await mailKitDeliveryService.SendEmailAsync(new MailMessage\n            {\n                Subject = \"Test\",\n                ToEmails = [\"test@example.com\"],\n                TextContent = \"Hi\",\n            }, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token)\n        );\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_Succeeds_WhenCertIsSelfSigned_ServerIsTrusted()\n    {\n        // When the setting `TrustServer = true` is set even if the cert is \n        // self signed and the cert is not trusted in anyway the connection should\n        // still go through.\n        var port = RandomPort();\n        var behavior = new DefaultServerBehaviour(false, port, _selfSignedCert);\n        using var smtpServer = new SmtpServer(behavior);\n        smtpServer.Start();\n\n        var globalSettings = GetSettings(gs =>\n        {\n            gs.Mail.Smtp.Port = port;\n            gs.Mail.Smtp.Ssl = true;\n            gs.Mail.Smtp.TrustServer = true;\n        });\n\n        var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(\n            globalSettings,\n            NullLogger<MailKitSmtpMailDeliveryService>.Instance\n        );\n\n        var tcs = new TaskCompletionSource();\n        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));\n        cts.Token.Register(() => _ = tcs.TrySetCanceled());\n\n        behavior.MessageReceivedEventHandler += (sender, args) =>\n        {\n            if (args.Message.Recipients.Contains(\"test1@example.com\"))\n            {\n                tcs.SetResult();\n            }\n            return Task.CompletedTask;\n        };\n\n        await mailKitDeliveryService.SendEmailAsync(new MailMessage\n        {\n            Subject = \"Test\",\n            ToEmails = [\"test1@example.com\"],\n            TextContent = \"Hi\",\n        }, cts.Token);\n\n        // Wait for email\n        await tcs.Task;\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_FailsConnectingWithTls_ServerDoesNotSupportTls()\n    {\n        // If the SMTP server is not setup to use TLS but our server is expecting it\n        // to, we should fail.\n        var port = RandomPort();\n        var behavior = new DefaultServerBehaviour(false, port);\n        using var smtpServer = new SmtpServer(behavior);\n        smtpServer.Start();\n\n        var globalSettings = GetSettings(gs =>\n        {\n            gs.Mail.Smtp.Port = port;\n            gs.Mail.Smtp.Ssl = true;\n            gs.Mail.Smtp.TrustServer = true;\n        });\n\n        var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(\n            globalSettings,\n            NullLogger<MailKitSmtpMailDeliveryService>.Instance\n        );\n\n        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));\n\n        await Assert.ThrowsAsync<SslHandshakeException>(\n            async () => await mailKitDeliveryService.SendEmailAsync(new MailMessage\n            {\n                Subject = \"Test\",\n                ToEmails = [\"test1@example.com\"],\n                TextContent = \"Hi\",\n            }, cts.Token)\n        );\n    }\n\n    [Fact(Skip = \"Requires permission to privileged port\")]\n    public async Task SendEmailAsync_Works_NoSsl()\n    {\n        // If the SMTP server isn't set up with any SSL/TLS and we dont' expect\n        // any, then the email should go through just fine. Just without encryption.\n        // This test has to use port 25\n        var port = 25;\n        var behavior = new DefaultServerBehaviour(false, port);\n        using var smtpServer = new SmtpServer(behavior);\n        smtpServer.Start();\n\n        var globalSettings = GetSettings(gs =>\n        {\n            gs.Mail.Smtp.Port = port;\n            gs.Mail.Smtp.Ssl = false;\n            gs.Mail.Smtp.StartTls = false;\n        });\n\n        var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(\n            globalSettings,\n            NullLogger<MailKitSmtpMailDeliveryService>.Instance\n        );\n\n        var tcs = new TaskCompletionSource();\n        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));\n        cts.Token.Register(() => _ = tcs.TrySetCanceled());\n\n        behavior.MessageReceivedEventHandler += (sender, args) =>\n        {\n            if (args.Message.Recipients.Contains(\"test1@example.com\"))\n            {\n                tcs.SetResult();\n            }\n            return Task.CompletedTask;\n        };\n\n        await mailKitDeliveryService.SendEmailAsync(new MailMessage\n        {\n            Subject = \"Test\",\n            ToEmails = [\"test1@example.com\"],\n            TextContent = \"Hi\",\n        }, cts.Token);\n\n        // Wait for email\n        await tcs.Task;\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_Succeeds_WhenServerNeedsToAuthenticate()\n    {\n        // When the setting `TrustServer = true` is set even if the cert is \n        // self signed and the cert is not trusted in anyway the connection should\n        // still go through.\n        var port = RandomPort();\n        var behavior = new DefaultServerBehaviour(false, port, _selfSignedCert);\n        behavior.AuthenticationCredentialsValidationRequiredEventHandler += (sender, args) =>\n        {\n            args.AuthenticationResult = AuthenticationResult.Failure;\n            if (args.Credentials is not UsernameAndPasswordAuthenticationCredentials usernameAndPasswordCreds)\n            {\n                return Task.CompletedTask;\n            }\n\n            if (usernameAndPasswordCreds.Username != \"test\" || usernameAndPasswordCreds.Password != \"password\")\n            {\n                return Task.CompletedTask;\n            }\n\n            args.AuthenticationResult = AuthenticationResult.Success;\n            return Task.CompletedTask;\n        };\n        using var smtpServer = new SmtpServer(behavior);\n        smtpServer.Start();\n\n        var globalSettings = GetSettings(gs =>\n        {\n            gs.Mail.Smtp.Port = port;\n            gs.Mail.Smtp.Ssl = true;\n            gs.Mail.Smtp.TrustServer = true;\n\n            gs.Mail.Smtp.Username = \"test\";\n            gs.Mail.Smtp.Password = \"password\";\n        });\n\n        var mailKitDeliveryService = new MailKitSmtpMailDeliveryService(\n            globalSettings,\n            NullLogger<MailKitSmtpMailDeliveryService>.Instance\n        );\n\n        var tcs = new TaskCompletionSource();\n        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));\n        cts.Token.Register(() => _ = tcs.TrySetCanceled());\n\n        behavior.MessageReceivedEventHandler += (sender, args) =>\n        {\n            if (args.Message.Recipients.Contains(\"test1@example.com\"))\n            {\n                tcs.SetResult();\n            }\n            return Task.CompletedTask;\n        };\n\n        await mailKitDeliveryService.SendEmailAsync(new MailMessage\n        {\n            Subject = \"Test\",\n            ToEmails = [\"test1@example.com\"],\n            TextContent = \"Hi\",\n        }, cts.Token);\n\n        // Wait for email\n        await tcs.Task;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Authorization/OrganizationUserUserDetailsAuthorizationHandlerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Authorization;\n\n[SutProviderCustomize]\npublic class OrganizationUserUserDetailsAuthorizationHandlerTests\n{\n    [Theory, CurrentContextOrganizationCustomize]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task ReadAll_Admins_Success(\n        OrganizationUserType userType,\n        CurrentContextOrganization organization,\n        SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)\n    {\n        organization.Type = userType;\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n\n        if (userType == OrganizationUserType.Custom)\n        {\n            organization.Permissions.ManageUsers = true;\n        }\n\n        var context = new AuthorizationHandlerContext(\n            new[] { OrganizationUserUserDetailsOperations.ReadAll },\n            new ClaimsPrincipal(),\n            new OrganizationScope(organization.Id));\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CurrentContextOrganizationCustomize]\n    public async Task ReadAll_ProviderUser_Success(\n        CurrentContextOrganization organization,\n        SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)\n    {\n        organization.Type = OrganizationUserType.User;\n        sutProvider.GetDependency<ICurrentContext>()\n            .ProviderUserForOrgAsync(organization.Id)\n            .Returns(true);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { OrganizationUserUserDetailsOperations.ReadAll },\n            new ClaimsPrincipal(),\n            new OrganizationScope(organization.Id));\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData, CurrentContextOrganizationCustomize]\n    public async Task ReadAll_User_NoSuccess(\n        CurrentContextOrganization organization,\n        SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)\n    {\n        organization.Type = OrganizationUserType.User;\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        var context = new AuthorizationHandlerContext(\n            new[] { OrganizationUserUserDetailsOperations.ReadAll },\n            new ClaimsPrincipal(),\n            new OrganizationScope(organization.Id)\n        );\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ReadAll_NotMember_NoSuccess(\n        CurrentContextOrganization organization,\n        SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)\n    {\n        var context = new AuthorizationHandlerContext(\n            new[] { OrganizationUserUserDetailsOperations.ReadAll },\n            new ClaimsPrincipal(),\n            new OrganizationScope(organization.Id)\n        );\n\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);\n        sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);\n\n        await sutProvider.Sut.HandleAsync(context);\n        Assert.False(context.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/AutoFixture/CurrentContextOrganizationFixtures.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.AdminConsole.AutoFixture;\n\npublic class CurrentContextOrganizationCustomization : ICustomization\n{\n    public Guid Id { get; set; }\n    public OrganizationUserType Type { get; set; }\n    public Permissions Permissions { get; set; } = new();\n    public bool AccessSecretsManager { get; set; }\n\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<CurrentContextOrganization>(composer => composer\n            .With(o => o.Id, Id)\n            .With(o => o.Type, Type)\n            .With(o => o.Permissions, Permissions)\n            .With(o => o.AccessSecretsManager, AccessSecretsManager));\n    }\n}\n\n[AttributeUsage(AttributeTargets.Method)]\npublic class CurrentContextOrganizationCustomizeAttribute : BitCustomizeAttribute\n{\n    public Guid Id { get; set; }\n    public OrganizationUserType Type { get; set; } = OrganizationUserType.User;\n    public Permissions Permissions { get; set; } = new();\n    public bool AccessSecretsManager { get; set; } = false;\n\n    public override ICustomization GetCustomization() => new CurrentContextOrganizationCustomization()\n    {\n        Id = Id,\n        Type = Type,\n        Permissions = Permissions,\n        AccessSecretsManager = AccessSecretsManager\n    };\n}\n\npublic class CurrentContextOrganizationAttribute : CustomizeAttribute\n{\n    public Guid Id { get; set; }\n    public OrganizationUserType Type { get; set; } = OrganizationUserType.User;\n    public Permissions Permissions { get; set; } = new();\n    public bool AccessSecretsManager { get; set; } = false;\n\n    public override ICustomization GetCustomization(ParameterInfo _) => new CurrentContextOrganizationCustomization\n    {\n        Id = Id,\n        Type = Type,\n        Permissions = Permissions,\n        AccessSecretsManager = AccessSecretsManager\n    };\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs",
    "content": "﻿using System.Reflection;\nusing System.Text.Json;\nusing AutoFixture;\nusing AutoFixture.Kernel;\nusing AutoFixture.Xunit2;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.DataProtection;\n\nnamespace Bit.Core.Test.AutoFixture.OrganizationFixtures;\n\npublic class OrganizationCustomization : ICustomization\n{\n    public bool UseGroups { get; set; }\n    public PlanType PlanType { get; set; }\n    public bool UseAutomaticUserConfirmation { get; set; }\n\n    public OrganizationCustomization()\n    {\n\n    }\n\n    public OrganizationCustomization(bool useAutomaticUserConfirmation, PlanType planType)\n    {\n        UseAutomaticUserConfirmation = useAutomaticUserConfirmation;\n        PlanType = planType;\n    }\n\n    public void Customize(IFixture fixture)\n    {\n        var organizationId = Guid.NewGuid();\n        var maxCollections = (short)new Random().Next(10, short.MaxValue);\n        var plan = MockPlans.Plans.FirstOrDefault(p => p.Type == PlanType);\n        var seats = (short)new Random().Next(plan.PasswordManager.BaseSeats, plan.PasswordManager.MaxSeats ?? short.MaxValue);\n        var smSeats = plan.SupportsSecretsManager\n            ? (short?)new Random().Next(plan.SecretsManager.BaseSeats, plan.SecretsManager.MaxSeats ?? short.MaxValue)\n            : null;\n\n        fixture.Customize<Organization>(composer => composer\n            .With(o => o.Id, organizationId)\n            .With(o => o.MaxCollections, maxCollections)\n            .With(o => o.UseGroups, UseGroups)\n            .With(o => o.PlanType, PlanType)\n            .With(o => o.Seats, seats)\n            .With(o => o.SmSeats, smSeats)\n            .With(o => o.UseAutomaticUserConfirmation, UseAutomaticUserConfirmation));\n\n        fixture.Customize<Collection>(composer =>\n            composer\n                .With(c => c.OrganizationId, organizationId)\n                .Without(o => o.CreationDate)\n                .Without(o => o.RevisionDate));\n\n        fixture.Customize<Group>(composer => composer.With(g => g.OrganizationId, organizationId));\n    }\n}\n\ninternal class OrganizationBuilder : ISpecimenBuilder\n{\n    public object Create(object request, ISpecimenContext context)\n    {\n        if (context == null)\n        {\n            throw new ArgumentNullException(nameof(context));\n        }\n\n        var type = request as Type;\n        if (type == null || type != typeof(Organization))\n        {\n            return new NoSpecimen();\n        }\n\n        var fixture = new Fixture();\n        var providers = fixture.Create<Dictionary<TwoFactorProviderType, TwoFactorProvider>>();\n        var organization = new Fixture().WithAutoNSubstitutions().Create<Organization>();\n        organization.SetTwoFactorProviders(providers);\n        return organization;\n    }\n}\n\ninternal class PaidOrganization : ICustomization\n{\n    public PlanType CheckedPlanType { get; set; }\n    public void Customize(IFixture fixture)\n    {\n        var validUpgradePlans = MockPlans.Plans.Where(p => p.Type != PlanType.Free && p.LegacyYear == null).OrderBy(p => p.UpgradeSortOrder).Select(p => p.Type).ToList();\n        var lowestActivePaidPlan = validUpgradePlans.First();\n        CheckedPlanType = CheckedPlanType.Equals(PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType;\n        validUpgradePlans.Remove(lowestActivePaidPlan);\n        fixture.Customize<Organization>(composer => composer\n            .With(o => o.PlanType, CheckedPlanType));\n        fixture.Customize<OrganizationUpgrade>(composer => composer\n            .With(ou => ou.Plan, validUpgradePlans.First()));\n    }\n}\n\ninternal class FreeOrganization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<Organization>(composer => composer\n            .With(o => o.PlanType, PlanType.Free));\n    }\n}\n\ninternal class FreeOrganizationUpgrade : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<Organization>(composer => composer\n            .With(o => o.PlanType, PlanType.Free));\n\n        var plansToIgnore = new List<PlanType> { PlanType.Free, PlanType.Custom };\n        var selectedPlan = MockPlans.Plans.Last(p => !plansToIgnore.Contains(p.Type) && !p.Disabled);\n\n        fixture.Customize<OrganizationUpgrade>(composer => composer\n            .With(ou => ou.Plan, selectedPlan.Type)\n            .With(ou => ou.PremiumAccessAddon, selectedPlan.PasswordManager.HasPremiumAccessOption));\n        fixture.Customize<Organization>(composer => composer\n            .Without(o => o.GatewaySubscriptionId));\n    }\n}\n\ninternal class OrganizationInvite : ICustomization\n{\n    public OrganizationUserType InviteeUserType { get; set; }\n    public OrganizationUserType InvitorUserType { get; set; }\n    public string PermissionsBlob { get; set; }\n    public void Customize(IFixture fixture)\n    {\n        var organizationId = Guid.NewGuid();\n        PermissionsBlob = PermissionsBlob ?? JsonSerializer.Serialize(new Permissions(), new JsonSerializerOptions\n        {\n            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n        });\n        fixture.Customize<Organization>(composer => composer\n            .With(o => o.Id, organizationId)\n            .With(o => o.Seats, (short)100));\n        fixture.Customize<OrganizationUser>(composer => composer\n            .With(ou => ou.OrganizationId, organizationId)\n            .With(ou => ou.Type, InvitorUserType)\n            .With(ou => ou.Permissions, PermissionsBlob));\n        fixture.Customize<OrganizationUserInvite>(composer => composer\n            .With(oi => oi.Type, InviteeUserType));\n        // Set Manage to false, this ensures it doesn't conflict with the other properties during validation\n        fixture.Customize<CollectionAccessSelection>(composer => composer\n            .With(c => c.Manage, false));\n    }\n}\n\npublic class SecretsManagerOrganizationCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        const PlanType planType = PlanType.EnterpriseAnnually;\n        var organizationId = Guid.NewGuid();\n\n        fixture.Customize<Organization>(composer => composer\n            .With(o => o.Id, organizationId)\n            .With(o => o.UseSecretsManager, true)\n            .With(o => o.PlanType, planType)\n            .With(o => o.Plan, MockPlans.Get(planType).Name)\n            .With(o => o.MaxAutoscaleSmSeats, (int?)null)\n            .With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null));\n    }\n}\n\ninternal class TeamsStarterOrganizationCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        var organizationId = Guid.NewGuid();\n        const PlanType planType = PlanType.TeamsStarter;\n\n        fixture.Customize<Organization>(composer =>\n            composer\n                .With(organization => organization.Id, organizationId)\n                .With(organization => organization.PlanType, planType)\n                .With(organization => organization.Seats, 10)\n                .Without(organization => organization.MaxStorageGb));\n    }\n}\n\ninternal class TeamsMonthlyWithAddOnsOrganizationCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        var organizationId = Guid.NewGuid();\n        const PlanType planType = PlanType.TeamsMonthly;\n\n        fixture.Customize<Organization>(composer =>\n            composer\n                .With(organization => organization.Id, organizationId)\n                .With(organization => organization.PlanType, planType)\n                .With(organization => organization.Seats, 20)\n                .With(organization => organization.UseSecretsManager, true)\n                .With(organization => organization.SmSeats, 5)\n                .With(organization => organization.SmServiceAccounts, 53));\n    }\n}\n\npublic class OrganizationCustomizeAttribute : BitCustomizeAttribute\n{\n    public bool UseGroups { get; set; }\n    public PlanType PlanType { get; set; } = PlanType.EnterpriseAnnually;\n    public override ICustomization GetCustomization() => new OrganizationCustomization()\n    {\n        UseGroups = UseGroups,\n        PlanType = PlanType\n    };\n}\n\ninternal class PaidOrganizationCustomizeAttribute : BitCustomizeAttribute\n{\n    public PlanType CheckedPlanType { get; set; } = PlanType.FamiliesAnnually;\n    public override ICustomization GetCustomization() => new PaidOrganization() { CheckedPlanType = CheckedPlanType };\n}\n\ninternal class FreeOrganizationCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new FreeOrganization();\n}\n\ninternal class FreeOrganizationUpgradeCustomize : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new FreeOrganizationUpgrade();\n}\n\ninternal class OrganizationInviteCustomizeAttribute : BitCustomizeAttribute\n{\n    public OrganizationUserType InviteeUserType { get; set; } = OrganizationUserType.Owner;\n    public OrganizationUserType InvitorUserType { get; set; } = OrganizationUserType.Owner;\n    public string PermissionsBlob { get; set; }\n\n    public override ICustomization GetCustomization() => new OrganizationInvite\n    {\n        InviteeUserType = InviteeUserType,\n        InvitorUserType = InvitorUserType,\n        PermissionsBlob = PermissionsBlob,\n    };\n}\n\ninternal class SecretsManagerOrganizationCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() =>\n        new SecretsManagerOrganizationCustomization();\n}\n\ninternal class TeamsStarterOrganizationCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new TeamsStarterOrganizationCustomization();\n}\n\ninternal class TeamsMonthlyWithAddOnsOrganizationCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new TeamsMonthlyWithAddOnsOrganizationCustomization();\n}\n\ninternal class EphemeralDataProtectionCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customizations.Add(new EphemeralDataProtectionProviderBuilder());\n    }\n\n    private class EphemeralDataProtectionProviderBuilder : ISpecimenBuilder\n    {\n        public object Create(object request, ISpecimenContext context)\n        {\n            var type = request as Type;\n            if (type == null || type != typeof(IDataProtectionProvider))\n            {\n                return new NoSpecimen();\n            }\n\n            return new EphemeralDataProtectionProvider();\n        }\n    }\n}\n\ninternal class EphemeralDataProtectionAutoDataAttribute : CustomAutoDataAttribute\n{\n    public EphemeralDataProtectionAutoDataAttribute() : base(new SutProviderCustomization(), new EphemeralDataProtectionCustomization())\n    { }\n}\n\ninternal class OrganizationAttribute(bool useAutomaticUserConfirmation = false, PlanType planType = PlanType.Free) : CustomizeAttribute\n{\n    public override ICustomization GetCustomization(ParameterInfo parameter) =>\n        new OrganizationCustomization(useAutomaticUserConfirmation, planType);\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/AutoFixture/OrganizationPolicyDetailsCustomization.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Test.AdminConsole.AutoFixture;\n\ninternal class OrganizationPolicyDetailsCustomization(\n    PolicyType policyType,\n    OrganizationUserType userType,\n    bool isProvider,\n    OrganizationUserStatusType userStatus) : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<OrganizationPolicyDetails>(composer => composer\n            .With(o => o.PolicyType, policyType)\n            .With(o => o.OrganizationUserType, userType)\n            .With(o => o.IsProvider, isProvider)\n            .With(o => o.OrganizationUserStatus, userStatus)\n            .Without(o => o.PolicyData)); // avoid autogenerating invalid json data\n    }\n}\n\npublic class OrganizationPolicyDetailsAttribute(\n    PolicyType policyType,\n    OrganizationUserType userType = OrganizationUserType.User,\n    bool isProvider = false,\n    OrganizationUserStatusType userStatus = OrganizationUserStatusType.Confirmed) : CustomizeAttribute\n{\n    public override ICustomization GetCustomization(ParameterInfo parameter)\n        => new OrganizationPolicyDetailsCustomization(policyType, userType, isProvider, userStatus);\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/AutoFixture/OrganizationUserFixtures.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\n\npublic class OrganizationUserCustomization : ICustomization\n{\n    public OrganizationUserStatusType Status { get; set; }\n    public OrganizationUserType Type { get; set; }\n\n    public OrganizationUserCustomization(OrganizationUserStatusType status, OrganizationUserType type)\n    {\n        Status = status;\n        Type = type;\n    }\n\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<OrganizationUser>(composer => composer\n            .With(o => o.Type, Type)\n            .With(o => o.Status, Status));\n    }\n}\n\npublic class OrganizationUserAttribute : CustomizeAttribute\n{\n    private readonly OrganizationUserStatusType _status;\n    private readonly OrganizationUserType _type;\n\n    public OrganizationUserAttribute(\n        OrganizationUserStatusType status = OrganizationUserStatusType.Confirmed,\n        OrganizationUserType type = OrganizationUserType.User)\n    {\n        _status = status;\n        _type = type;\n    }\n\n    public override ICustomization GetCustomization(ParameterInfo parameter)\n    {\n        return new OrganizationUserCustomization(_status, _type);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/AutoFixture/OrganizationUserPolicyDetailsFixtures.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\n\nnamespace Bit.Core.Test.AdminConsole.AutoFixture;\n\ninternal class OrganizationUserPolicyDetailsCustomization : ICustomization\n{\n    public PolicyType Type { get; set; }\n    public OrganizationUserStatusType Status { get; set; }\n    public OrganizationUserType UserType { get; set; }\n    public bool IsProvider { get; set; }\n\n    public OrganizationUserPolicyDetailsCustomization(PolicyType type, OrganizationUserStatusType status, OrganizationUserType userType, bool isProvider)\n    {\n        Type = type;\n        Status = status;\n        UserType = userType;\n        IsProvider = isProvider;\n    }\n\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<OrganizationUserPolicyDetails>(composer => composer\n            .With(o => o.OrganizationId, Guid.NewGuid())\n            .With(o => o.PolicyType, Type)\n            .With(o => o.OrganizationUserStatus, Status)\n            .With(o => o.OrganizationUserType, UserType)\n            .With(o => o.IsProvider, IsProvider)\n            .With(o => o.PolicyEnabled, true));\n    }\n}\n\npublic class OrganizationUserPolicyDetailsAttribute : CustomizeAttribute\n{\n    private readonly PolicyType _type;\n    private readonly OrganizationUserStatusType _status;\n    private readonly OrganizationUserType _userType;\n    private readonly bool _isProvider;\n\n    public OrganizationUserPolicyDetailsAttribute(PolicyType type) : this(type, OrganizationUserStatusType.Accepted, OrganizationUserType.User, false)\n    {\n        _type = type;\n    }\n\n    public OrganizationUserPolicyDetailsAttribute(PolicyType type, OrganizationUserStatusType status, OrganizationUserType userType, bool isProvider)\n    {\n        _type = type;\n        _status = status;\n        _userType = userType;\n        _isProvider = isProvider;\n    }\n\n    public override ICustomization GetCustomization(ParameterInfo parameter)\n    {\n        return new OrganizationUserPolicyDetailsCustomization(_type, _status, _userType, _isProvider);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Test.AdminConsole.AutoFixture;\n\ninternal class PolicyDetailsCustomization(\n    PolicyType policyType,\n    OrganizationUserType userType,\n    bool isProvider,\n    OrganizationUserStatusType userStatus) : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<PolicyDetails>(composer => composer\n            .With(o => o.PolicyType, policyType)\n            .With(o => o.OrganizationUserType, userType)\n            .With(o => o.IsProvider, isProvider)\n            .With(o => o.OrganizationUserStatus, userStatus)\n            .Without(o => o.PolicyData)); // avoid autogenerating invalid json data\n    }\n}\n\npublic class PolicyDetailsAttribute(\n    PolicyType policyType,\n    OrganizationUserType userType = OrganizationUserType.User,\n    bool isProvider = false,\n    OrganizationUserStatusType userStatus = OrganizationUserStatusType.Confirmed) : CustomizeAttribute\n{\n    public override ICustomization GetCustomization(ParameterInfo parameter)\n        => new PolicyDetailsCustomization(policyType, userType, isProvider, userStatus);\n}\n\n\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/AutoFixture/PolicyFixtures.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\n\nnamespace Bit.Core.Test.AdminConsole.AutoFixture;\n\ninternal class PolicyCustomization : ICustomization\n{\n    public PolicyType Type { get; set; }\n    public bool Enabled { get; set; }\n    public string? Data { get; set; }\n\n    public PolicyCustomization(PolicyType type, bool enabled, string? data)\n    {\n        Type = type;\n        Enabled = enabled;\n        Data = data;\n    }\n\n    public void Customize(IFixture fixture)\n    {\n        var orgId = Guid.NewGuid();\n\n        fixture.Customize<Policy>(composer => composer\n            .With(o => o.OrganizationId, orgId)\n            .With(o => o.Type, Type)\n            .With(o => o.Enabled, Enabled)\n            .With(o => o.Data, Data));\n\n        fixture.Customize<PolicyStatus>(composer => composer\n            .With(o => o.OrganizationId, orgId)\n            .With(o => o.Type, Type)\n            .With(o => o.Enabled, Enabled)\n            .With(o => o.Data, Data));\n    }\n}\n\npublic class PolicyAttribute : CustomizeAttribute\n{\n    private readonly PolicyType _type;\n    private readonly bool _enabled;\n    private readonly string? _data;\n\n    public PolicyAttribute(PolicyType type, bool enabled = true, string? data = null)\n    {\n        _type = type;\n        _enabled = enabled;\n        _data = data;\n    }\n\n    public override ICustomization GetCustomization(ParameterInfo parameter)\n    {\n        return new PolicyCustomization(_type, _enabled, _data);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\n\nnamespace Bit.Core.Test.AdminConsole.AutoFixture;\n\ninternal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<PolicyUpdate>(composer => composer\n            .With(o => o.Type, type)\n            .With(o => o.Enabled, enabled)\n            .With(o => o.PerformedBy, new StandardUser(Guid.NewGuid(), false)));\n    }\n}\n\npublic class PolicyUpdateAttribute(PolicyType type = PolicyType.FreeFamiliesSponsorshipPolicy, bool enabled = true) : CustomizeAttribute\n{\n    public override ICustomization GetCustomization(ParameterInfo parameter)\n    {\n        return new PolicyUpdateCustomization(type, enabled);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Entities/OrganizationTests.cs",
    "content": "﻿using System.Reflection;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Core.Test.Entities;\n\npublic class OrganizationTests\n{\n    private static readonly Dictionary<TwoFactorProviderType, TwoFactorProvider> _testConfig = new Dictionary<TwoFactorProviderType, TwoFactorProvider>()\n    {\n        [TwoFactorProviderType.OrganizationDuo] = new TwoFactorProvider\n        {\n            Enabled = true,\n            MetaData = new Dictionary<string, object>\n            {\n                [\"IKey\"] = \"IKey_value\",\n                [\"SKey\"] = \"SKey_value\",\n                [\"Host\"] = \"Host_value\",\n            },\n        }\n    };\n\n\n    [Fact]\n    public void SetTwoFactorProviders_Success()\n    {\n        var organization = new Organization();\n        organization.SetTwoFactorProviders(_testConfig);\n\n        using var jsonDocument = JsonDocument.Parse(organization.TwoFactorProviders);\n        var root = jsonDocument.RootElement;\n\n        var duo = AssertHelper.AssertJsonProperty(root, \"6\", JsonValueKind.Object);\n        AssertHelper.AssertJsonProperty(duo, \"Enabled\", JsonValueKind.True);\n        var duoMetaData = AssertHelper.AssertJsonProperty(duo, \"MetaData\", JsonValueKind.Object);\n        var iKey = AssertHelper.AssertJsonProperty(duoMetaData, \"IKey\", JsonValueKind.String).GetString();\n        Assert.Equal(\"IKey_value\", iKey);\n        var sKey = AssertHelper.AssertJsonProperty(duoMetaData, \"SKey\", JsonValueKind.String).GetString();\n        Assert.Equal(\"SKey_value\", sKey);\n        var host = AssertHelper.AssertJsonProperty(duoMetaData, \"Host\", JsonValueKind.String).GetString();\n        Assert.Equal(\"Host_value\", host);\n    }\n\n    [Fact]\n    public void GetTwoFactorProviders_Success()\n    {\n        // This is to get rid of the cached dictionary the SetTwoFactorProviders keeps so we can fully test the JSON reading\n        // It intent is to mimic a storing of the entity in the database and it being read later\n        var tempOrganization = new Organization();\n        tempOrganization.SetTwoFactorProviders(_testConfig);\n        var organization = new Organization\n        {\n            TwoFactorProviders = tempOrganization.TwoFactorProviders,\n        };\n\n        var twoFactorProviders = organization.GetTwoFactorProviders();\n\n        var duo = Assert.Contains(TwoFactorProviderType.OrganizationDuo, (IDictionary<TwoFactorProviderType, TwoFactorProvider>)twoFactorProviders);\n        Assert.True(duo.Enabled);\n        Assert.NotNull(duo.MetaData);\n        var iKey = Assert.Contains(\"IKey\", (IDictionary<string, object>)duo.MetaData);\n        Assert.Equal(\"IKey_value\", iKey);\n        var sKey = Assert.Contains(\"SKey\", (IDictionary<string, object>)duo.MetaData);\n        Assert.Equal(\"SKey_value\", sKey);\n        var host = Assert.Contains(\"Host\", (IDictionary<string, object>)duo.MetaData);\n        Assert.Equal(\"Host_value\", host);\n    }\n\n    [Fact]\n    public void GetTwoFactorProviders_SavedWithName_Success()\n    {\n        var organization = new Organization();\n        // This should save items with the string name of the enum and we will validate that we can read\n        // from that just incase some organizations have it saved that way.\n        organization.TwoFactorProviders = JsonSerializer.Serialize(_testConfig);\n\n        // Preliminary Asserts to make sure we are testing what we want to be testing\n        using var jsonDocument = JsonDocument.Parse(organization.TwoFactorProviders);\n        var root = jsonDocument.RootElement;\n        // This means it saved the enum as its string name\n        AssertHelper.AssertJsonProperty(root, \"OrganizationDuo\", JsonValueKind.Object);\n\n        // Actual checks\n        var twoFactorProviders = organization.GetTwoFactorProviders();\n\n        var duo = Assert.Contains(TwoFactorProviderType.OrganizationDuo, (IDictionary<TwoFactorProviderType, TwoFactorProvider>)twoFactorProviders);\n        Assert.True(duo.Enabled);\n        Assert.NotNull(duo.MetaData);\n        var iKey = Assert.Contains(\"IKey\", (IDictionary<string, object>)duo.MetaData);\n        Assert.Equal(\"IKey_value\", iKey);\n        var sKey = Assert.Contains(\"SKey\", (IDictionary<string, object>)duo.MetaData);\n        Assert.Equal(\"SKey_value\", sKey);\n        var host = Assert.Contains(\"Host\", (IDictionary<string, object>)duo.MetaData);\n        Assert.Equal(\"Host_value\", host);\n    }\n\n    [Fact]\n    public void UseDisableSmAdsForUsers_DefaultValue_IsFalse()\n    {\n        var organization = new Organization();\n\n        Assert.False(organization.UseDisableSmAdsForUsers);\n    }\n\n    [Fact]\n    public void UseDisableSmAdsForUsers_CanBeSetToTrue()\n    {\n        var organization = new Organization\n        {\n            UseDisableSmAdsForUsers = true\n        };\n\n        Assert.True(organization.UseDisableSmAdsForUsers);\n    }\n\n    [Fact]\n    public void UpdateFromLicense_AppliesAllLicenseProperties()\n    {\n        // This test ensures that when a new property is added to OrganizationLicense,\n        // it is also applied to the Organization in UpdateFromLicense().\n        // This is the fourth step in the license synchronization pipeline:\n        // Property → Constant → Claim → Extraction → Application\n\n        // 1. Get all public properties from OrganizationLicense\n        var licenseProperties = typeof(OrganizationLicense)\n            .GetProperties(BindingFlags.Public | BindingFlags.Instance)\n            .Select(p => p.Name)\n            .ToHashSet();\n\n        // 2. Define properties that don't need to be applied to Organization\n        var excludedProperties = new HashSet<string>\n        {\n            // Internal/computed properties\n            \"SignatureBytes\",             // Computed from Signature property\n            \"ValidLicenseVersion\",        // Internal property, not serialized\n            \"CurrentLicenseFileVersion\",  // Constant field, not an instance property\n            \"Hash\",                       // Signature-related, not applied to org\n            \"Signature\",                  // Signature-related, not applied to org\n            \"Token\",                      // The JWT itself, not applied to org\n            \"Version\",                    // License version, not stored on org\n\n            // Properties intentionally excluded from UpdateFromLicense\n            \"Id\",                         // Self-hosted org has its own unique Guid\n            \"MaxStorageGb\",               // Not enforced for self-hosted (per comment in UpdateFromLicense)\n\n            // Properties not stored on Organization model\n            \"LicenseType\",                // Not a property on Organization\n            \"InstallationId\",             // Not a property on Organization\n            \"Issued\",                     // Not a property on Organization\n            \"Refresh\",                    // Not a property on Organization\n            \"ExpirationWithoutGracePeriod\", // Not a property on Organization\n            \"Trial\",                      // Not a property on Organization\n            \"Expires\",                    // Mapped to ExpirationDate on Organization (different name)\n\n            // Deprecated properties not applied\n            \"LimitCollectionCreationDeletion\",      // Deprecated, not applied\n            \"AllowAdminAccessToAllCollectionItems\", // Deprecated, not applied\n        };\n\n        // 3. Get properties that should be applied\n        var propertiesThatShouldBeApplied = licenseProperties\n            .Except(excludedProperties)\n            .ToHashSet();\n\n        // 4. Read Organization.UpdateFromLicense source code\n        var organizationSourcePath = Path.Combine(\n            Directory.GetCurrentDirectory(),\n            \"..\", \"..\", \"..\", \"..\", \"..\", \"src\", \"Core\", \"AdminConsole\", \"Entities\", \"Organization.cs\");\n        var sourceCode = File.ReadAllText(organizationSourcePath);\n\n        // 5. Find all property assignments in UpdateFromLicense method\n        // Pattern matches: PropertyName = license.PropertyName\n        // This regex looks for assignments like \"Name = license.Name\" or \"ExpirationDate = license.Expires\"\n        var assignmentPattern = @\"(\\w+)\\s*=\\s*license\\.(\\w+)\";\n        var matches = Regex.Matches(sourceCode, assignmentPattern);\n\n        var appliedProperties = new HashSet<string>();\n        foreach (Match match in matches)\n        {\n            // Get the license property name (right side of assignment)\n            var licensePropertyName = match.Groups[2].Value;\n            appliedProperties.Add(licensePropertyName);\n        }\n\n        // Special case: Expires is mapped to ExpirationDate\n        if (appliedProperties.Contains(\"Expires\"))\n        {\n            appliedProperties.Add(\"Expires\"); // Already added, but being explicit\n        }\n\n        // 6. Find missing applications\n        var missingApplications = propertiesThatShouldBeApplied\n            .Except(appliedProperties)\n            .OrderBy(p => p)\n            .ToList();\n\n        // 7. Build error message with guidance\n        var errorMessage = \"\";\n        if (missingApplications.Any())\n        {\n            errorMessage = $\"The following OrganizationLicense properties are NOT applied to Organization in UpdateFromLicense():\\n\";\n            errorMessage += string.Join(\"\\n\", missingApplications.Select(p => $\"  - {p}\"));\n            errorMessage += \"\\n\\nPlease add the following lines to Organization.UpdateFromLicense():\\n\";\n            foreach (var prop in missingApplications)\n            {\n                errorMessage += $\"  {prop} = license.{prop};\\n\";\n            }\n            errorMessage += \"\\nNote: If the property maps to a different name on Organization (like Expires → ExpirationDate), adjust accordingly.\";\n        }\n\n        // 8. Assert - if this fails, the error message guides the developer to add the application\n        Assert.True(\n            !missingApplications.Any(),\n            $\"\\n{errorMessage}\");\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Entities/OrganizationUserTests.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Entities;\n\npublic class OrganizationUserTests\n{\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\" \")]\n    public void IsValidResetPasswordKey_InvalidKeys_ReturnsFalse(string? resetPasswordKey)\n    {\n        Assert.False(OrganizationUser.IsValidResetPasswordKey(resetPasswordKey));\n    }\n\n    [Fact]\n    public void IsValidResetPasswordKey_ValidKey_ReturnsTrue()\n    {\n        Assert.True(OrganizationUser.IsValidResetPasswordKey(\"validKey\"));\n    }\n\n    [Fact]\n    public void IsEnrolledInAccountRecovery_NullKey_ReturnsFalse()\n    {\n        var orgUser = new OrganizationUser { ResetPasswordKey = null };\n\n        Assert.False(orgUser.IsEnrolledInAccountRecovery());\n    }\n\n    [Fact]\n    public void IsEnrolledInAccountRecovery_ValidKey_ReturnsTrue()\n    {\n        var orgUser = new OrganizationUser { ResetPasswordKey = \"validKey\" };\n\n        Assert.True(orgUser.IsEnrolledInAccountRecovery());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs",
    "content": "﻿using Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.Test.AdminConsole.Helpers;\n\npublic static class PermissionsHelpers\n{\n    /// <summary>\n    /// Sets the specified permission.\n    /// </summary>\n    /// <param name=\"permissionName\">The permission name specified as a string - using `nameof` is highly recommended.</param>\n    /// <param name=\"value\">The value to set the permission to.</param>\n    /// <returns>No value; this mutates the permissions object.</returns>\n    public static void SetPermission(this Permissions permissions, string permissionName, bool value)\n    {\n        var prop = typeof(Permissions).GetProperty(permissionName);\n        if (prop == null)\n        {\n            throw new NullReferenceException(\"Invalid property name.\");\n        }\n\n        prop.SetValue(permissions, true);\n    }\n\n    /// <summary>\n    /// Return a new Permission object with inverted permissions.\n    /// This is useful to test negative cases, e.g. \"all other permissions should fail\".\n    /// </summary>\n    /// <param name=\"permissions\"></param>\n    /// <returns></returns>\n    public static Permissions Invert(this Permissions permissions)\n    {\n        // Get all false boolean properties of input object\n        var inputsToFlip = permissions\n            .GetType()\n            .GetProperties()\n            .Where(p =>\n                p.PropertyType == typeof(bool) &&\n                (bool)p.GetValue(permissions, null)! == false)\n            .Select(p => p.Name);\n\n        var result = new Permissions();\n\n        // Set these to true on the result object\n        result\n            .GetType()\n            .GetProperties()\n            .Where(p => inputsToFlip.Contains(p.Name))\n            .ToList()\n            .ForEach(p => p.SetValue(result, true));\n\n        return result;\n    }\n\n    /// <summary>\n    /// Returns a sequence of Permission objects, where each Permission object has a different permission flag set.\n    /// </summary>\n    public static IEnumerable<Permissions> GetAllPermissions()\n    {\n        // Get all boolean properties of input object\n        var props = typeof(Permissions)\n            .GetProperties()\n            .Where(p => p.PropertyType == typeof(bool));\n\n        foreach (var prop in props)\n        {\n            var result = new Permissions();\n            prop.SetValue(result, true);\n            yield return result;\n        }\n    }\n\n    /// <summary>\n    /// Returns a sequence of all possible roles and permissions represented as CurrentContextOrganization objects.\n    /// Used largely for authorization testing.\n    /// </summary>\n    /// <returns></returns>\n    public static IEnumerable<CurrentContextOrganization> AllRoles() => new List<CurrentContextOrganization>\n    {\n        new () { Type = OrganizationUserType.Owner },\n        new () { Type = OrganizationUserType.Admin },\n        new () { Type = OrganizationUserType.Custom, Permissions = new Permissions() },\n        new () { Type = OrganizationUserType.Custom, Permissions = new Permissions().Invert() },\n        new () { Type = OrganizationUserType.User },\n    };\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Helpers/PermissionsHelpersTests.cs",
    "content": "﻿using Bit.Core.Models.Data;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Helpers;\n\npublic class PermissionsHelpersTests\n{\n    [Fact]\n    public void Permissions_Invert_InvertsAllPermissions()\n    {\n        var sut = new Permissions\n        {\n            AccessEventLogs = true,\n            AccessReports = true,\n            DeleteAnyCollection = true,\n            ManagePolicies = true,\n            ManageScim = true\n        };\n\n        var result = sut.Invert();\n\n        Assert.True(result is\n        {\n            AccessEventLogs: false,\n            AccessImportExport: true,\n            AccessReports: false,\n            CreateNewCollections: true,\n            EditAnyCollection: true,\n            DeleteAnyCollection: false,\n            ManageGroups: true,\n            ManagePolicies: false,\n            ManageSso: true,\n            ManageUsers: true,\n            ManageResetPassword: true,\n            ManageScim: false\n        });\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Models/Data/SelfHostedOrganizationDetailsTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Test.Billing.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Data;\n\npublic class SelfHostedOrganizationDetailsTests\n{\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_OccupiedSeatCount_ExceedsLicense_Fail(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.Seats = 1;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.False(result);\n        Assert.Contains(\"Remove some users\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_MaxCollections_ExceedsLicense_Fail(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.MaxCollections = 1;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.False(result);\n        Assert.Contains(\"Remove some collections\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_Groups_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseGroups = false;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.False(result);\n        Assert.Contains(\"Your new license does not allow for the use of groups\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_Policies_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UsePolicies = false;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.False(result);\n        Assert.Contains(\"Your new license does not allow for the use of policies\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_DisabledPolicies_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UsePolicies = false;\n        ((List<Policy>)orgDetails.Policies).ForEach(p => p.Enabled = false);\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_Sso_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseSso = false;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.False(result);\n        Assert.Contains(\"Your new license does not allow for the use of SSO\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_DisabledSso_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseSso = false;\n        orgDetails.SsoConfig.Enabled = false;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_NoSso_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseSso = false;\n        orgDetails.SsoConfig = null;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_KeyConnector_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseKeyConnector = false;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.False(result);\n        Assert.Contains(\"Your new license does not allow for the use of Key Connector\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_DisabledKeyConnector_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseKeyConnector = false;\n        orgDetails.SsoConfig.SetData(new SsoConfigurationData() { MemberDecryptionType = MemberDecryptionType.MasterPassword });\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_NoSsoKeyConnector_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseKeyConnector = false;\n        orgDetails.SsoConfig = null;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_Scim_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseScim = false;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.False(result);\n        Assert.Contains(\"Your new plan does not allow the SCIM feature\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_DisabledScim_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseScim = false;\n        ((List<OrganizationConnection<ScimConfig>>)orgDetails.ScimConnections)\n            .ForEach(c => c.SetConfig(new ScimConfig() { Enabled = false }));\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_NoScimConfig_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseScim = false;\n        orgDetails.ScimConnections = null;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_CustomPermissions_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseCustomPermissions = false;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.False(result);\n        Assert.Contains(\"Your new plan does not allow the Custom Permissions feature\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_NoCustomPermissions_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseCustomPermissions = false;\n        ((List<OrganizationUser>)orgDetails.OrganizationUsers).ForEach(ou => ou.Type = OrganizationUserType.User);\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_ResetPassword_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseResetPassword = false;\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.False(result);\n        Assert.Contains(\"Your new license does not allow the Password Reset feature\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public void ValidateForOrganization_DisabledResetPassword_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);\n        orgLicense.UseResetPassword = false;\n        ((List<Policy>)orgDetails.Policies).ForEach(p => p.Enabled = false);\n\n        var result = orgDetails.CanUseLicense(license, out var exception);\n\n        Assert.True(result);\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    private (SelfHostedOrganizationDetails organization, OrganizationLicense license) GetOrganizationAndLicense(List<OrganizationUser> orgUsers,\n        List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)\n    {\n        // The default state is that all features are used by Org and allowed by License\n        // Each test then toggles on/off as necessary\n        policies.ForEach(p => p.Enabled = true);\n        policies.First().Type = PolicyType.ResetPassword;\n\n        ssoConfig.Enabled = true;\n        ssoConfig.SetData(new SsoConfigurationData()\n        {\n            MemberDecryptionType = MemberDecryptionType.KeyConnector,\n        });\n\n        var enabledScimConfig = new ScimConfig() { Enabled = true };\n        scimConnections.ForEach(c => c.Config = enabledScimConfig);\n\n        orgUsers.First().Type = OrganizationUserType.Custom;\n\n        var organization = new SelfHostedOrganizationDetails()\n        {\n            OccupiedSeatCount = 10,\n            CollectionCount = 5,\n            GroupCount = 5,\n            OrganizationUsers = orgUsers,\n            Policies = policies,\n            SsoConfig = ssoConfig,\n            ScimConnections = scimConnections,\n\n            UsePolicies = true,\n            UseSso = true,\n            UseKeyConnector = true,\n            UseScim = true,\n            UseGroups = true,\n            UseDirectory = true,\n            UseEvents = true,\n            UseTotp = true,\n            Use2fa = true,\n            UseApi = true,\n            UseResetPassword = true,\n            SelfHost = true,\n            UsersGetPremium = true,\n            UseCustomPermissions = true,\n        };\n\n        license.Enabled = true;\n        license.PlanType = PlanType.EnterpriseAnnually;\n        license.Seats = 10;\n        license.MaxCollections = 5;\n        license.UsePolicies = true;\n        license.UseSso = true;\n        license.UseKeyConnector = true;\n        license.UseScim = true;\n        license.UseGroups = true;\n        license.UseEvents = true;\n        license.UseDirectory = true;\n        license.UseTotp = true;\n        license.Use2fa = true;\n        license.UseApi = true;\n        license.UseResetPassword = true;\n        license.MaxStorageGb = 1;\n        license.SelfHost = true;\n        license.UsersGetPremium = true;\n        license.UseCustomPermissions = true;\n        license.Version = 11;\n        license.Issued = DateTime.Now;\n        license.Expires = DateTime.Now.AddYears(1);\n\n        return (organization, license);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Models/InviteOrganizationUsersRequestTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nusing static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.InviteOrganizationUserErrorMessages;\n\nnamespace Bit.Core.Test.AdminConsole.Models;\n\npublic class InviteOrganizationUsersRequestTests\n{\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WhenPassedInvalidEmail_ThrowsException(string email, OrganizationUserType type, Permissions permissions, string externalId)\n    {\n        var exception = Assert.Throws<BadRequestException>(() =>\n            new OrganizationUserInviteCommandModel(email, [], [], type, permissions, externalId, false));\n\n        Assert.Contains(InvalidEmailErrorMessage, exception.Message);\n    }\n\n    [Fact]\n    public void Constructor_WhenPassedInvalidCollectionAccessConfiguration_ThrowsException()\n    {\n        const string validEmail = \"test@email.com\";\n\n        var invalidCollectionConfiguration = new CollectionAccessSelection\n        {\n            Manage = true,\n            HidePasswords = true\n        };\n\n        var exception = Assert.Throws<BadRequestException>(() =>\n            new OrganizationUserInviteCommandModel(\n                email: validEmail,\n                assignedCollections: [invalidCollectionConfiguration],\n                groups: [],\n                type: default,\n                permissions: new Permissions(),\n                externalId: string.Empty,\n                accessSecretsManager: false));\n\n        Assert.Equal(InvalidCollectionConfigurationErrorMessage, exception.Message);\n    }\n\n    [Fact]\n    public void Constructor_WhenPassedValidArguments_ReturnsInvite()\n    {\n        const string validEmail = \"test@email.com\";\n        var validCollectionConfiguration = new CollectionAccessSelection { Id = Guid.NewGuid(), Manage = true };\n\n        var invite = new OrganizationUserInviteCommandModel(\n            email: validEmail,\n            assignedCollections: [validCollectionConfiguration],\n            groups: [],\n            type: default,\n            permissions: null,\n            externalId: null,\n            accessSecretsManager: false);\n\n        Assert.NotNull(invite);\n        Assert.Contains(validEmail, invite.Email);\n        Assert.Contains(validCollectionConfiguration, invite.AssignedCollections);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationAuth/Models/AuthRequestUpdateProcessorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationAuth.Models;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationAuth.Models;\n\n[SutProviderCustomize]\npublic class AuthRequestUpdateProcessorTests\n{\n    [Theory]\n    [BitAutoData]\n    public void Process_NoAuthRequestLoaded_Throws(\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        var sut = new AuthRequestUpdateProcessor(null, update, processorConfiguration);\n        Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_RequestIsAlreadyApproved_Throws(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        authRequest = Approve(authRequest);\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_RequestIsAlreadyDenied_Throws(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        authRequest = Deny(authRequest);\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_RequestIsExpired_Throws(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        processorConfiguration.AuthRequestExpiresAfter = new TimeSpan(0, 10, 0);\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-60);\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_UpdateDoesNotMatch_Throws(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        while (authRequest.Id == update.Id)\n        {\n            authRequest.Id = new Guid();\n        }\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_AuthRequestAndOrganizationIdMismatch_Throws(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        while (authRequest.OrganizationId == processorConfiguration.OrganizationId)\n        {\n            authRequest.OrganizationId = new Guid();\n        }\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        Assert.ThrowsAny<AuthRequestUpdateCouldNotBeProcessedException>(() => sut.Process());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_RequestApproved_NoKey_Throws(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        update.Approved = true;\n        update.Key = null;\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        Assert.ThrowsAny<ApprovedAuthRequestIsMissingKeyException>(() => sut.Process());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_RequestApproved_ValidInput_Works(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        update.Approved = true;\n        update.Key = \"key\";\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        sut.Process();\n        Assert.True(sut.ProcessedAuthRequest.Approved);\n        Assert.Equal(sut.ProcessedAuthRequest.Key, update.Key);\n        Assert.NotNull(sut.ProcessedAuthRequest.ResponseDate);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_RequestDenied_ValidInput_Works(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        update.Approved = false;\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        sut.Process();\n        Assert.False(sut.ProcessedAuthRequest.Approved);\n        Assert.Null(sut.ProcessedAuthRequest.Key);\n        Assert.NotNull(sut.ProcessedAuthRequest.ResponseDate);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendPushNotification_RequestIsDenied_DoesNotSend(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        update.Approved = false;\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        var callback = Substitute.For<Func<OrganizationAdminAuthRequest, Task>>();\n        sut.Process();\n        await sut.SendPushNotification(callback);\n        await callback.DidNotReceiveWithAnyArgs()(sut.ProcessedAuthRequest);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendPushNotification_RequestIsApproved_DoesSend(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        update.Approved = true;\n        update.Key = \"key\";\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        var callback = Substitute.For<Func<OrganizationAdminAuthRequest, Task>>();\n        sut.Process();\n        await sut.SendPushNotification(callback);\n        await callback.Received()(sut.ProcessedAuthRequest);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendApprovalEmail_RequestIsDenied_DoesNotSend(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        update.Approved = false;\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        var callback = Substitute.For<Func<OrganizationAdminAuthRequest, string, Task>>();\n        sut.Process();\n        await sut.SendApprovalEmail(callback);\n        await callback.DidNotReceiveWithAnyArgs()(sut.ProcessedAuthRequest, \"string\");\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendApprovalEmail_RequestIsApproved_DoesSend(\n        OrganizationAdminAuthRequest authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    )\n    {\n        (authRequest, processorConfiguration) = UnrespondAndEnsureValid(authRequest, update, processorConfiguration);\n        authRequest.RequestDeviceType = DeviceType.iOS;\n        authRequest.RequestDeviceIdentifier = \"device-id\";\n        update.Approved = true;\n        update.Key = \"key\";\n        var sut = new AuthRequestUpdateProcessor(authRequest, update, processorConfiguration);\n        var callback = Substitute.For<Func<OrganizationAdminAuthRequest, string, Task>>();\n        sut.Process();\n        await sut.SendApprovalEmail(callback);\n        await callback.Received()(sut.ProcessedAuthRequest, \"iOS - device-id\");\n    }\n\n    private static T Approve<T>(T authRequest) where T : AuthRequest\n    {\n        authRequest.Key = \"key\";\n        authRequest.Approved = true;\n        authRequest.ResponseDate = DateTime.UtcNow;\n        return authRequest;\n    }\n\n    private static T Deny<T>(T authRequest) where T : AuthRequest\n    {\n        authRequest.Approved = false;\n        authRequest.ResponseDate = DateTime.UtcNow;\n        return authRequest;\n    }\n\n    private (\n        T AuthRequest,\n        AuthRequestUpdateProcessorConfiguration ProcessorConfiguration\n    ) UnrespondAndEnsureValid<T>(\n        T authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    ) where T : AuthRequest\n    {\n        authRequest.Id = update.Id;\n        authRequest.OrganizationId = processorConfiguration.OrganizationId;\n        authRequest.Key = null;\n        authRequest.Approved = null;\n        authRequest.ResponseDate = null;\n        authRequest.AuthenticationDate = null;\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-1);\n        processorConfiguration.AuthRequestExpiresAfter = new TimeSpan(1, 0, 0);\n        return (authRequest, processorConfiguration);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationAuth/Models/BatchAuthRequestUpdateProcessorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationAuth.Models;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationAuth.Models;\n\n[SutProviderCustomize]\npublic class BatchAuthRequestUpdateProcessorTests\n{\n    [Theory]\n    [BitAutoData]\n    public void Process_NoProcessors_Handled(\n        IEnumerable<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        Action<Exception> errorHandler\n    )\n    {\n        var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);\n        sut.Process(errorHandler);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_BadInput_CallsHandler(\n        List<OrganizationAdminAuthRequest> authRequests,\n        IEnumerable<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration\n    )\n    {\n        // An already approved auth request should break the processor\n        // immediately.\n        authRequests[0].Approved = true;\n        var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);\n        var errorHandler = Substitute.For<Action<Exception>>();\n        sut.Process(errorHandler);\n        errorHandler.ReceivedWithAnyArgs()(new AuthRequestUpdateProcessingException());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Process_ValidInput_Works(\n        List<OrganizationAdminAuthRequest> authRequests,\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        Action<Exception> errorHandler\n    )\n    {\n        (authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);\n        var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);\n        Assert.NotEmpty(sut.Processors);\n        sut.Process(errorHandler);\n        Assert.NotEmpty(sut.Processors.Where(p => p.ProcessedAuthRequest != null));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Save_NoProcessedAuthRequests_IsHandled(\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        Func<IEnumerable<AuthRequest>, Task> saveCallback\n    )\n    {\n        var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);\n        Assert.Empty(sut.Processors);\n        await sut.Save(saveCallback);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Save_ProcessedAuthRequests_IsHandled(\n        List<OrganizationAdminAuthRequest> authRequests,\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        Action<Exception> errorHandler\n    )\n    {\n        (authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);\n        var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);\n        var saveCallback = Substitute.For<Func<IEnumerable<OrganizationAdminAuthRequest>, Task>>();\n        await sut.Process(errorHandler).Save(saveCallback);\n        await saveCallback.ReceivedWithAnyArgs()(Arg.Any<IEnumerable<OrganizationAdminAuthRequest>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendPushNotifications_NoProcessors_IsHandled\n    (\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        Func<AuthRequest, Task> callback\n    )\n    {\n        var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);\n        Assert.Empty(sut.Processors);\n        await sut.SendPushNotifications(callback);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendPushNotifications_HasProcessors_Sends\n    (\n        List<OrganizationAdminAuthRequest> authRequests,\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        Action<Exception> errorHandler\n    )\n    {\n        (authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);\n        var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);\n        var callback = Substitute.For<Func<OrganizationAdminAuthRequest, Task>>();\n        await sut.Process(errorHandler).SendPushNotifications(callback);\n        await callback.ReceivedWithAnyArgs()(Arg.Any<OrganizationAdminAuthRequest>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendApprovalEmailsForProcessedRequests_NoProcessors_IsHandled\n    (\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        Func<AuthRequest, string, Task> callback\n    )\n    {\n        var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);\n        Assert.Empty(sut.Processors);\n        await sut.SendApprovalEmailsForProcessedRequests(callback);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendApprovalEmailsForProcessedRequests_HasProcessors_Sends\n    (\n        List<OrganizationAdminAuthRequest> authRequests,\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        Action<Exception> errorHandler\n    )\n    {\n        (authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);\n        var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);\n        var callback = Substitute.For<Func<OrganizationAdminAuthRequest, string, Task>>();\n        await sut.Process(errorHandler).SendApprovalEmailsForProcessedRequests(callback);\n        await callback.ReceivedWithAnyArgs()(Arg.Any<OrganizationAdminAuthRequest>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task LogOrganizationEventsForProcessedRequests_NoProcessedAuthRequests_IsHandled\n    (\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration\n    )\n    {\n        var sut = new BatchAuthRequestUpdateProcessor(null, updates, configuration);\n        var callback = Substitute.For<Func<IEnumerable<(OrganizationAdminAuthRequest, EventType)>, Task>>();\n        Assert.Empty(sut.Processors);\n        await sut.LogOrganizationEventsForProcessedRequests(callback);\n        await callback.DidNotReceiveWithAnyArgs()(Arg.Any<IEnumerable<(OrganizationAdminAuthRequest, EventType)>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task LogOrganizationEventsForProcessedRequests_HasProcessedAuthRequests_IsHandled\n    (\n        List<OrganizationAdminAuthRequest> authRequests,\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        Action<Exception> errorHandler\n    )\n    {\n        (authRequests[0], updates[0], configuration) = UnrespondAndEnsureValid(authRequests[0], updates[0], configuration);\n        var sut = new BatchAuthRequestUpdateProcessor(authRequests, updates, configuration);\n        var callback = Substitute.For<Func<IEnumerable<(OrganizationAdminAuthRequest, EventType)>, Task>>();\n        await sut.Process(errorHandler).LogOrganizationEventsForProcessedRequests(callback);\n        await callback.ReceivedWithAnyArgs()(Arg.Any<IEnumerable<(OrganizationAdminAuthRequest, EventType)>>());\n    }\n\n    private (\n        T authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration ProcessorConfiguration\n    ) UnrespondAndEnsureValid<T>(\n        T authRequest,\n        OrganizationAuthRequestUpdate update,\n        AuthRequestUpdateProcessorConfiguration processorConfiguration\n    ) where T : AuthRequest\n    {\n        authRequest.Id = update.Id;\n        authRequest.OrganizationId = processorConfiguration.OrganizationId;\n        authRequest.Key = null;\n        authRequest.Approved = null;\n        authRequest.ResponseDate = null;\n        authRequest.AuthenticationDate = null;\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-1);\n        processorConfiguration.AuthRequestExpiresAfter = new TimeSpan(1, 0, 0);\n\n        update.Approved = true;\n        update.Key = \"key\";\n        return (authRequest, update, processorConfiguration);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationAuth/UpdateOrganizationAuthRequestCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationAuth;\nusing Bit.Core.AdminConsole.OrganizationAuth.Models;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Api.Request.AuthRequest;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationAuth;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationAuthRequestCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrgAuthRequest_Approved_SendEmail_Success(\n        DateTime responseDate, string email, DeviceType deviceType, string deviceIdentifier,\n        string requestIpAddress, Guid requestId, Guid userId, bool requestApproved,\n        string encryptedUserKey, SutProvider<UpdateOrganizationAuthRequestCommand> sutProvider)\n    {\n        var expectedDeviceTypeAndIdentifier = $\"{deviceType} - {deviceIdentifier}\";\n\n        sutProvider.GetDependency<IAuthRequestService>()\n            .UpdateAuthRequestAsync(requestId, userId,\n                Arg.Is<AuthRequestUpdateRequestModel>(x =>\n                    x.RequestApproved == requestApproved && x.Key == encryptedUserKey))\n            .Returns(new AuthRequest()\n            {\n                UserId = userId,\n                Approved = true,\n                ResponseDate = responseDate,\n                RequestDeviceType = deviceType,\n                RequestDeviceIdentifier = deviceIdentifier,\n                RequestIpAddress = requestIpAddress,\n            });\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(userId)\n            .Returns(new User()\n            {\n                Email = email\n            });\n\n        await sutProvider.Sut.UpdateAsync(requestId, userId, requestApproved, encryptedUserKey);\n\n        await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(userId);\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendTrustedDeviceAdminApprovalEmailAsync(email, responseDate, requestIpAddress, expectedDeviceTypeAndIdentifier);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrgAuthRequest_Denied_NonExecutes(\n        SutProvider<UpdateOrganizationAuthRequestCommand> sutProvider, Guid requestId, Guid userId,\n        bool requestApproved, string encryptedUserKey)\n    {\n        sutProvider.GetDependency<IAuthRequestService>()\n            .UpdateAuthRequestAsync(requestId, userId,\n                Arg.Is<AuthRequestUpdateRequestModel>(x =>\n                    x.RequestApproved == requestApproved && x.Key == encryptedUserKey))\n            .Returns(new AuthRequest() { Approved = false });\n\n        await sutProvider.Sut.UpdateAsync(requestId, userId, requestApproved, encryptedUserKey);\n\n        await sutProvider.GetDependency<IUserRepository>().DidNotReceive().GetByIdAsync(userId);\n        await sutProvider.GetDependency<IMailService>().DidNotReceive()\n            .SendTrustedDeviceAdminApprovalEmailAsync(Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>(),\n                Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_BatchUpdate_AuthRequestForOrganizationNotFound_DoesNotExecute(\n        SutProvider<UpdateOrganizationAuthRequestCommand> sutProvider,\n        List<OrganizationAuthRequestUpdate> updates,\n        AuthRequestUpdateProcessorConfiguration configuration)\n    {\n        sutProvider.GetDependency<IAuthRequestRepository>().GetManyAdminApprovalRequestsByManyIdsAsync(\n            configuration.OrganizationId,\n            updates.Select(ar => ar.Id)\n        ).ReturnsForAnyArgs((ICollection<OrganizationAdminAuthRequest>)null);\n\n        await sutProvider.Sut.UpdateAsync(configuration.OrganizationId, updates);\n        await sutProvider.GetDependency<IAuthRequestRepository>().DidNotReceiveWithAnyArgs().UpdateManyAsync(Arg.Any<IEnumerable<OrganizationAdminAuthRequest>>());\n        await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushAuthRequestResponseAsync(Arg.Any<AuthRequest>());\n        await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs().SendTrustedDeviceAdminApprovalEmailAsync(\n            Arg.Any<string>(),\n            Arg.Any<DateTime>(),\n            Arg.Any<string>(),\n            Arg.Any<string>()\n        );\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogOrganizationUserEventsAsync(\n            Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>()\n        );\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_BatchUpdate_ValidRequest_SavesAndFiresAllEvents(\n        SutProvider<UpdateOrganizationAuthRequestCommand> sutProvider,\n        List<OrganizationAuthRequestUpdate> updates,\n        List<OrganizationAdminAuthRequest> unprocessedAuthRequests,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        List<OrganizationUser> organizationUsers,\n        List<User> users\n    )\n    {\n        // For this command to work we need the following from external\n        // classes:\n        // 1. A configured expiration timespan for organization auth requests\n        // 2. Some unresponded to auth requests that match the ids provided\n        // 3. A valid user to send emails to\n        // 4. A valid organization user to log events for\n\n        for (int i = 0; i < updates.Count(); i++)\n        {\n            unprocessedAuthRequests[i] = UnrespondAndEnsureValid(unprocessedAuthRequests[i], configuration.OrganizationId);\n            updates[i].Approved = true;\n            updates[i].Key = \"key\";\n            unprocessedAuthRequests[i].Id = updates[i].Id;\n            unprocessedAuthRequests[i].RequestDeviceType = DeviceType.iOS;\n            unprocessedAuthRequests[i].OrganizationUserId = organizationUsers[i].Id;\n            organizationUsers[i].OrganizationId = configuration.OrganizationId;\n            users[i].Id = unprocessedAuthRequests[i].UserId;\n            organizationUsers[i].UserId = unprocessedAuthRequests[i].UserId;\n\n            sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Is(users[i].Id)).Returns(users[i]);\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().PasswordlessAuth.AdminRequestExpiration.Returns(TimeSpan.FromDays(7));\n\n        sutProvider.GetDependency<IAuthRequestRepository>().GetManyAdminApprovalRequestsByManyIdsAsync(\n            configuration.OrganizationId,\n            updates.Select(ar => ar.Id)\n        ).ReturnsForAnyArgs(unprocessedAuthRequests);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(Arg.Is<IEnumerable<Guid>>(\n            list => list.All(x => organizationUsers.Select(y => y.Id).Contains(x)))).Returns(organizationUsers);\n\n        // Call the SUT\n        await sutProvider.Sut.UpdateAsync(configuration.OrganizationId, updates);\n\n        // Assert that because we passed in good data we call a save\n        // operation and raise all events\n        await sutProvider.GetDependency<IAuthRequestRepository>()\n            .Received()\n            .UpdateManyAsync(\n                Arg.Is<IEnumerable<OrganizationAdminAuthRequest>>(list =>\n                    list.Any() &&\n                    list.All(x =>\n                        x.Approved.Value &&\n                        x.Key == \"key\" &&\n                        x.ResponseDate != null &&\n                        unprocessedAuthRequests.Select(y => y.Id).Contains(x.Id))));\n\n        foreach (var authRequest in unprocessedAuthRequests)\n        {\n            await sutProvider.GetDependency<IPushNotificationService>().Received()\n                .PushAuthRequestResponseAsync(Arg.Is<OrganizationAdminAuthRequest>\n                        (ar => ar.Id == authRequest.Id && ar.Approved == true && ar.Key == \"key\"));\n\n            await sutProvider.GetDependency<IMailService>().Received().SendTrustedDeviceAdminApprovalEmailAsync(\n                users.FirstOrDefault(x => x.Id == authRequest.UserId).Email,\n                Arg.Any<DateTime>(),\n                authRequest.RequestIpAddress,\n                $\"iOS - {authRequest.RequestDeviceIdentifier}\"\n            );\n        }\n\n        await sutProvider.GetDependency<IEventService>().Received().LogOrganizationUserEventsAsync(\n            Arg.Is<IEnumerable<(OrganizationUser o, EventType e, DateTime? d)>>(list =>\n                list.Any() && list.All(x => organizationUsers.Any(y => y.Id == x.o.Id) && x.e == EventType.OrganizationUser_ApprovedAuthRequest)\n        ));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_BatchUpdate_AuthRequestIsDenied_DoesNotLeakRejection(\n        SutProvider<UpdateOrganizationAuthRequestCommand> sutProvider,\n        List<OrganizationAuthRequestUpdate> updates,\n        OrganizationAdminAuthRequest unprocessedAuthRequest,\n        AuthRequestUpdateProcessorConfiguration configuration,\n        User user\n    )\n    {\n        // For this command to work we need the following from external\n        // classes:\n        // 1. A configured expiration timespan for organization auth requests\n        // 2. Some unresponded to auth requests that match the ids provided\n        // 3. A valid user to send emails to\n\n        var unprocessedAuthRequests = new List<OrganizationAdminAuthRequest>();\n        unprocessedAuthRequest = UnrespondAndEnsureValid(unprocessedAuthRequest, configuration.OrganizationId);\n        foreach (var update in updates)\n        {\n            update.Approved = false;\n            unprocessedAuthRequest.Id = update.Id;\n            unprocessedAuthRequests.Add(unprocessedAuthRequest);\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().PasswordlessAuth.AdminRequestExpiration.Returns(TimeSpan.FromDays(7));\n\n        sutProvider.GetDependency<IAuthRequestRepository>().GetManyAdminApprovalRequestsByManyIdsAsync(\n            configuration.OrganizationId,\n            updates.Select(ar => ar.Id)\n        ).ReturnsForAnyArgs(unprocessedAuthRequests);\n\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);\n\n        // Call the SUT\n        await sutProvider.Sut.UpdateAsync(configuration.OrganizationId, updates);\n\n        // Assert that because we passed in good data we call a save\n        // operation and raise all events\n        await sutProvider.GetDependency<IAuthRequestRepository>().ReceivedWithAnyArgs().UpdateManyAsync(Arg.Any<IEnumerable<OrganizationAdminAuthRequest>>());\n        await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushAuthRequestResponseAsync(Arg.Any<AuthRequest>());\n        await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs().SendTrustedDeviceAdminApprovalEmailAsync(\n            Arg.Any<string>(),\n            Arg.Any<DateTime>(),\n            Arg.Any<string>(),\n            Arg.Any<string>()\n        );\n        await sutProvider.GetDependency<IEventService>().ReceivedWithAnyArgs().LogOrganizationUserEventsAsync(\n            Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>()\n        );\n    }\n\n    private T UnrespondAndEnsureValid<T>(T authRequest, Guid organizationId) where T : AuthRequest\n    {\n        authRequest.OrganizationId = organizationId;\n        authRequest.Key = null;\n        authRequest.Approved = null;\n        authRequest.ResponseDate = null;\n        authRequest.AuthenticationDate = null;\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);\n        return authRequest;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/AccountRecovery/AdminRecoverAccountCommandTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.AccountRecovery;\n\n[SutProviderCustomize]\npublic class AdminRecoverAccountCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task RecoverAccountAsync_Success(\n        string newMasterPassword,\n        string key,\n        Organization organization,\n        OrganizationUser organizationUser,\n        User user,\n        [Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,\n        SutProvider<AdminRecoverAccountCommand> sutProvider)\n    {\n        // Arrange\n        SetupValidOrganization(sutProvider, organization);\n        SetupValidPolicy(sutProvider, organization, policy);\n        SetupValidOrganizationUser(organizationUser, organization.Id);\n        SetupValidUser(sutProvider, user, organizationUser);\n        SetupSuccessfulPasswordUpdate(sutProvider, user, newMasterPassword);\n\n        // Act\n        var result = await sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key);\n\n        // Assert\n        Assert.True(result.Succeeded);\n        await AssertSuccessAsync(sutProvider, user, key, organization, organizationUser);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RecoverAccountAsync_OrganizationDoesNotExist_ThrowsBadRequest(\n        [OrganizationUser] OrganizationUser organizationUser,\n        string newMasterPassword,\n        string key,\n        SutProvider<AdminRecoverAccountCommand> sutProvider)\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(orgId)\n            .Returns((Organization)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RecoverAccountAsync(orgId, organizationUser, newMasterPassword, key));\n        Assert.Equal(\"Organization does not allow password reset.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RecoverAccountAsync_OrganizationDoesNotAllowResetPassword_ThrowsBadRequest(\n        string newMasterPassword,\n        string key,\n        Organization organization,\n        [OrganizationUser] OrganizationUser organizationUser,\n        SutProvider<AdminRecoverAccountCommand> sutProvider)\n    {\n        // Arrange\n        organization.UseResetPassword = false;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key));\n        Assert.Equal(\"Organization does not allow password reset.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RecoverAccountAsync_InvalidPolicy_ThrowsBadRequest(\n        string newMasterPassword,\n        string key,\n        Organization organization,\n        [Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,\n        SutProvider<AdminRecoverAccountCommand> sutProvider)\n    {\n        // Arrange\n        SetupValidOrganization(sutProvider, organization);\n        SetupValidPolicy(sutProvider, organization, policy);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RecoverAccountAsync(organization.Id, new OrganizationUser { Id = Guid.NewGuid() },\n                newMasterPassword, key));\n        Assert.Equal(\"Organization does not have the password reset policy enabled.\", exception.Message);\n    }\n\n    public static IEnumerable<object[]> InvalidOrganizationUsers()\n    {\n        // Make an organization so we can use its Id\n        var organization = new Fixture().Create<Organization>();\n\n        var nonConfirmed = new OrganizationUser\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organization.Id,\n            Status = OrganizationUserStatusType.Invited\n        };\n        yield return [nonConfirmed, organization];\n\n        var wrongOrganization = new OrganizationUser\n        {\n            Status = OrganizationUserStatusType.Confirmed,\n            OrganizationId = Guid.NewGuid(), // Different org\n            ResetPasswordKey = \"test-key\",\n            UserId = Guid.NewGuid(),\n        };\n        yield return [wrongOrganization, organization];\n\n        var nullResetPasswordKey = new OrganizationUser\n        {\n            Status = OrganizationUserStatusType.Confirmed,\n            OrganizationId = organization.Id,\n            ResetPasswordKey = null,\n            UserId = Guid.NewGuid(),\n        };\n        yield return [nullResetPasswordKey, organization];\n\n        var emptyResetPasswordKey = new OrganizationUser\n        {\n            Status = OrganizationUserStatusType.Confirmed,\n            OrganizationId = organization.Id,\n            ResetPasswordKey = \"\",\n            UserId = Guid.NewGuid(),\n        };\n        yield return [emptyResetPasswordKey, organization];\n\n        var whitespaceResetPasswordKey = new OrganizationUser\n        {\n            Status = OrganizationUserStatusType.Confirmed,\n            OrganizationId = organization.Id,\n            ResetPasswordKey = \" \",\n            UserId = Guid.NewGuid(),\n        };\n        yield return [whitespaceResetPasswordKey, organization];\n\n        var nullUserId = new OrganizationUser\n        {\n            Status = OrganizationUserStatusType.Confirmed,\n            OrganizationId = organization.Id,\n            ResetPasswordKey = \"test-key\",\n            UserId = null,\n        };\n        yield return [nullUserId, organization];\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(InvalidOrganizationUsers))]\n    public async Task RecoverAccountAsync_OrganizationUserIsInvalid_ThrowsBadRequest(\n        OrganizationUser organizationUser,\n        Organization organization,\n        string newMasterPassword,\n        string key,\n        [Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,\n        SutProvider<AdminRecoverAccountCommand> sutProvider)\n    {\n        // Arrange\n        SetupValidOrganization(sutProvider, organization);\n        SetupValidPolicy(sutProvider, organization, policy);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key));\n        Assert.Equal(\"Organization User not valid\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RecoverAccountAsync_UserDoesNotExist_ThrowsNotFoundException(\n        string newMasterPassword,\n        string key,\n        Organization organization,\n        OrganizationUser organizationUser,\n        [Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,\n        SutProvider<AdminRecoverAccountCommand> sutProvider)\n    {\n        // Arrange\n        SetupValidOrganization(sutProvider, organization);\n        SetupValidPolicy(sutProvider, organization, policy);\n        SetupValidOrganizationUser(organizationUser, organization.Id);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(organizationUser.UserId!.Value)\n            .Returns((User)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RecoverAccountAsync_UserUsesKeyConnector_ThrowsBadRequest(\n        string newMasterPassword,\n        string key,\n        Organization organization,\n        OrganizationUser organizationUser,\n        User user,\n        [Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,\n        SutProvider<AdminRecoverAccountCommand> sutProvider)\n    {\n        // Arrange\n        SetupValidOrganization(sutProvider, organization);\n        SetupValidPolicy(sutProvider, organization, policy);\n        SetupValidOrganizationUser(organizationUser, organization.Id);\n        user.UsesKeyConnector = true;\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(organizationUser.UserId!.Value)\n            .Returns(user);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RecoverAccountAsync(organization.Id, organizationUser, newMasterPassword, key));\n        Assert.Equal(\"Cannot reset password of a user with Key Connector.\", exception.Message);\n    }\n\n    private static void SetupValidOrganization(SutProvider<AdminRecoverAccountCommand> sutProvider, Organization organization)\n    {\n        organization.UseResetPassword = true;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n    }\n\n    private static void SetupValidPolicy(SutProvider<AdminRecoverAccountCommand> sutProvider, Organization organization, PolicyStatus policy)\n    {\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.ResetPassword)\n            .Returns(policy);\n    }\n\n    private static void SetupValidOrganizationUser(OrganizationUser organizationUser, Guid orgId)\n    {\n        organizationUser.Status = OrganizationUserStatusType.Confirmed;\n        organizationUser.OrganizationId = orgId;\n        organizationUser.ResetPasswordKey = \"test-key\";\n        organizationUser.Type = OrganizationUserType.User;\n    }\n\n    private static void SetupValidUser(SutProvider<AdminRecoverAccountCommand> sutProvider, User user, OrganizationUser organizationUser)\n    {\n        user.Id = organizationUser.UserId!.Value;\n        user.UsesKeyConnector = false;\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(user.Id)\n            .Returns(user);\n    }\n\n    private static void SetupSuccessfulPasswordUpdate(SutProvider<AdminRecoverAccountCommand> sutProvider, User user, string newMasterPassword)\n    {\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(user, newMasterPassword)\n            .Returns(IdentityResult.Success);\n    }\n\n    private static async Task AssertSuccessAsync(SutProvider<AdminRecoverAccountCommand> sutProvider, User user, string key,\n        Organization organization, OrganizationUser organizationUser)\n    {\n        await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(\n            Arg.Is<User>(u =>\n                u.Id == user.Id &&\n                u.Key == key &&\n                u.ForcePasswordReset == true &&\n                u.RevisionDate == u.AccountRevisionDate &&\n                u.LastPasswordChangeDate == u.RevisionDate));\n\n        await sutProvider.GetDependency<IMailService>().Received(1).SendAdminResetPasswordEmailAsync(\n            Arg.Is(user.Email),\n            Arg.Is(user.Name),\n            Arg.Is(organization.DisplayName()));\n\n        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(\n            Arg.Is(organizationUser),\n            Arg.Is(EventType.OrganizationUser_AdminResetPassword));\n\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(\n            Arg.Is(user.Id));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Groups;\n\n[SutProviderCustomize]\npublic class CreateGroupCommandTests\n{\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task CreateGroup_Success(SutProvider<CreateGroupCommand> sutProvider, Organization organization, Group group)\n    {\n        await sutProvider.Sut.CreateGroupAsync(group, organization);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).CreateAsync(group);\n        await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Created);\n        AssertHelper.AssertRecent(group.CreationDate);\n        AssertHelper.AssertRecent(group.RevisionDate);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task CreateGroup_WithCollections_Success(SutProvider<CreateGroupCommand> sutProvider, Organization organization, Group group, List<CollectionAccessSelection> collections)\n    {\n        // Arrange list of collections to make sure Manage is mutually exclusive\n        for (var i = 0; i < collections.Count; i++)\n        {\n            var cas = collections[i];\n            cas.Manage = i != collections.Count - 1;\n            cas.HidePasswords = i == collections.Count - 1;\n            cas.ReadOnly = i == collections.Count - 1;\n        }\n\n        await sutProvider.Sut.CreateGroupAsync(group, organization, collections);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).CreateAsync(group, collections);\n        await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Created);\n        AssertHelper.AssertRecent(group.CreationDate);\n        AssertHelper.AssertRecent(group.RevisionDate);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task CreateGroup_WithEventSystemUser_Success(SutProvider<CreateGroupCommand> sutProvider, Organization organization, Group group, EventSystemUser eventSystemUser)\n    {\n        await sutProvider.Sut.CreateGroupAsync(group, organization, eventSystemUser);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).CreateAsync(group);\n        await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Created, eventSystemUser);\n        AssertHelper.AssertRecent(group.CreationDate);\n        AssertHelper.AssertRecent(group.RevisionDate);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task CreateGroup_WithNullOrganization_Throws(SutProvider<CreateGroupCommand> sutProvider, Group group, EventSystemUser eventSystemUser)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateGroupAsync(group, null, eventSystemUser));\n\n        Assert.Contains(\"Organization not found\", exception.Message);\n\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogGroupEventAsync(default, default, default);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = false), BitAutoData]\n    public async Task CreateGroup_WithUseGroupsAsFalse_Throws(SutProvider<CreateGroupCommand> sutProvider, Organization organization, Group group, EventSystemUser eventSystemUser)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateGroupAsync(group, organization, eventSystemUser));\n\n        Assert.Contains(\"This organization cannot use groups\", exception.Message);\n\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogGroupEventAsync(default, default, default);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Groups/DeleteGroupCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Groups;\n\n[SutProviderCustomize]\npublic class DeleteGroupCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteGroup_Success(SutProvider<DeleteGroupCommand> sutProvider, Group group)\n    {\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetByIdAsync(group.Id)\n            .Returns(group);\n\n        await sutProvider.Sut.DeleteGroupAsync(group.OrganizationId, group.Id);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).DeleteAsync(group);\n        await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Core.Enums.EventType.Group_Deleted);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteGroup_NotFound_Throws(SutProvider<DeleteGroupCommand> sutProvider, Guid organizationId, Guid groupId)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteGroupAsync(organizationId, groupId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteGroup_MismatchingOrganizationId_Throws(SutProvider<DeleteGroupCommand> sutProvider, Guid organizationId, Guid groupId)\n    {\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetByIdAsync(groupId)\n            .Returns(new Group\n            {\n                Id = groupId,\n                OrganizationId = Guid.NewGuid()\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteGroupAsync(organizationId, groupId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteGroup_WithEventSystemUser_Success(SutProvider<DeleteGroupCommand> sutProvider, Group group,\n        EventSystemUser eventSystemUser)\n    {\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetByIdAsync(group.Id)\n            .Returns(group);\n\n        await sutProvider.Sut.DeleteGroupAsync(group.OrganizationId, group.Id, eventSystemUser);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).DeleteAsync(group);\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogGroupEventAsync(group, Core.Enums.EventType.Group_Deleted, eventSystemUser);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_DeletesGroup(Group group, SutProvider<DeleteGroupCommand> sutProvider)\n    {\n        // Act\n        await sutProvider.Sut.DeleteAsync(group);\n\n        // Assert\n        await sutProvider.GetDependency<IGroupRepository>().Received().DeleteAsync(group);\n        await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Deleted);\n    }\n\n    [Theory, BitAutoData]\n    [OrganizationCustomize]\n    public async Task DeleteManyAsync_DeletesManyGroup(Group group, Group group2, SutProvider<DeleteGroupCommand> sutProvider)\n    {\n        // Arrange\n        var groups = new[] { group, group2 };\n\n        // Act\n        await sutProvider.Sut.DeleteManyAsync(groups);\n\n        // Assert\n        await sutProvider.GetDependency<IGroupRepository>().Received()\n            .DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(groups.Select(g => g.Id))));\n\n        await sutProvider.GetDependency<IEventService>().Received().LogGroupEventsAsync(\n            Arg.Is<IEnumerable<(Group, EventType, EventSystemUser?, DateTime?)>>(a =>\n                a.All(g => groups.Contains(g.Item1) && g.Item2 == EventType.Group_Deleted))\n            );\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Groups;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Groups;\n\n[SutProviderCustomize]\npublic class UpdateGroupCommandTests\n{\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_Success(SutProvider<UpdateGroupCommand> sutProvider, Group group, Group oldGroup,\n        Organization organization)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeUsers(sutProvider, group);\n        ArrangeCollections(sutProvider, group);\n\n        await sutProvider.Sut.UpdateGroupAsync(group, organization);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).ReplaceAsync(group);\n        await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Updated);\n        AssertHelper.AssertRecent(group.RevisionDate);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_WithCollections_Success(SutProvider<UpdateGroupCommand> sutProvider, Group group,\n        Group oldGroup, Organization organization, List<CollectionAccessSelection> collections)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeUsers(sutProvider, group);\n        ArrangeCollections(sutProvider, group);\n\n        // Arrange list of collections to make sure Manage is mutually exclusive\n        for (var i = 0; i < collections.Count; i++)\n        {\n            var cas = collections[i];\n            cas.Manage = i != collections.Count - 1;\n            cas.HidePasswords = i == collections.Count - 1;\n            cas.ReadOnly = i == collections.Count - 1;\n        }\n\n        await sutProvider.Sut.UpdateGroupAsync(group, organization, collections);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).ReplaceAsync(group, collections);\n        await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Updated);\n        AssertHelper.AssertRecent(group.RevisionDate);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_WithEventSystemUser_Success(SutProvider<UpdateGroupCommand> sutProvider, Group group,\n        Group oldGroup, Organization organization, EventSystemUser eventSystemUser)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeUsers(sutProvider, group);\n        ArrangeCollections(sutProvider, group);\n\n        await sutProvider.Sut.UpdateGroupAsync(group, organization, eventSystemUser);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received(1).ReplaceAsync(group);\n        await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Enums.EventType.Group_Updated, eventSystemUser);\n        AssertHelper.AssertRecent(group.RevisionDate);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_WithNullOrganization_Throws(SutProvider<UpdateGroupCommand> sutProvider, Group group,\n        Group oldGroup, EventSystemUser eventSystemUser)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeUsers(sutProvider, group);\n        ArrangeCollections(sutProvider, group);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateGroupAsync(group, null, eventSystemUser));\n\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogGroupEventAsync(default, default, default);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = false), BitAutoData]\n    public async Task UpdateGroup_WithUseGroupsAsFalse_Throws(SutProvider<UpdateGroupCommand> sutProvider,\n        Organization organization, Group group, Group oldGroup, EventSystemUser eventSystemUser)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeUsers(sutProvider, group);\n        ArrangeCollections(sutProvider, group);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.UpdateGroupAsync(group, organization, eventSystemUser));\n\n        Assert.Contains(\"This organization cannot use groups\", exception.Message);\n\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogGroupEventAsync(default, default, default);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_GroupBelongsToDifferentOrganization_Throws(SutProvider<UpdateGroupCommand> sutProvider,\n        Group group, Group oldGroup, Organization organization)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeUsers(sutProvider, group);\n        ArrangeCollections(sutProvider, group);\n\n        // Mismatching orgId\n        oldGroup.OrganizationId = CoreHelpers.GenerateComb();\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateGroupAsync(group, organization));\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_CollectionsBelongsToDifferentOrganization_Throws(SutProvider<UpdateGroupCommand> sutProvider,\n        Group group, Group oldGroup, Organization organization, List<CollectionAccessSelection> collectionAccess)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeUsers(sutProvider, group);\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()\n                .Select(guid => new Collection { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList());\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateGroupAsync(group, organization, collectionAccess));\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_CollectionsDoNotExist_Throws(SutProvider<UpdateGroupCommand> sutProvider,\n        Group group, Group oldGroup, Organization organization, List<CollectionAccessSelection> collectionAccess)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeUsers(sutProvider, group);\n\n        // Return result is missing a collection\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo =>\n            {\n                var result = callInfo.Arg<IEnumerable<Guid>>()\n                    .Select(guid => new Collection { Id = guid, OrganizationId = group.OrganizationId }).ToList();\n                result.RemoveAt(0);\n                return result;\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateGroupAsync(group, organization, collectionAccess));\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_WithDefaultUserCollectionType_Throws(SutProvider<UpdateGroupCommand> sutProvider,\n        Group group, Group oldGroup, Organization organization, List<CollectionAccessSelection> collectionAccess)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeUsers(sutProvider, group);\n\n        // Return collections with DefaultUserCollection type\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()\n                .Select(guid => new Collection { Id = guid, OrganizationId = group.OrganizationId, Type = CollectionType.DefaultUserCollection }).ToList());\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateGroupAsync(group, organization, collectionAccess));\n        Assert.Contains(\"You cannot modify group access for collections with the type as DefaultUserCollection.\", exception.Message);\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_MemberBelongsToDifferentOrganization_Throws(SutProvider<UpdateGroupCommand> sutProvider,\n        Group group, Group oldGroup, Organization organization, IEnumerable<Guid> userAccess)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeCollections(sutProvider, group);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()\n                .Select(guid => new OrganizationUser { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList());\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateGroupAsync(group, organization, null, userAccess));\n    }\n\n    [Theory, OrganizationCustomize(UseGroups = true), BitAutoData]\n    public async Task UpdateGroup_MemberDoesNotExist_Throws(SutProvider<UpdateGroupCommand> sutProvider,\n        Group group, Group oldGroup, Organization organization, IEnumerable<Guid> userAccess)\n    {\n        ArrangeGroup(sutProvider, group, oldGroup);\n        ArrangeCollections(sutProvider, group);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo =>\n            {\n                var result = callInfo.Arg<IEnumerable<Guid>>()\n                    .Select(guid => new OrganizationUser { Id = guid, OrganizationId = group.OrganizationId })\n                    .ToList();\n                result.RemoveAt(0);\n                return result;\n            });\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateGroupAsync(group, organization, null, userAccess));\n    }\n\n    private void ArrangeGroup(SutProvider<UpdateGroupCommand> sutProvider, Group group, Group oldGroup)\n    {\n        oldGroup.OrganizationId = group.OrganizationId;\n        oldGroup.Id = group.Id;\n        sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(group.Id).Returns(oldGroup);\n    }\n\n    private void ArrangeCollections(SutProvider<UpdateGroupCommand> sutProvider, Group group)\n    {\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()\n                .Select(guid => new Collection() { Id = guid, OrganizationId = group.OrganizationId }).ToList());\n    }\n\n    private void ArrangeUsers(SutProvider<UpdateGroupCommand> sutProvider, Group group)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()\n                .Select(guid => new OrganizationUser { Id = guid, OrganizationId = group.OrganizationId }).ToList());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Import;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Fakes;\nusing NSubstitute;\nusing Xunit;\nusing Organization = Bit.Core.AdminConsole.Entities.Organization;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Import;\n\npublic class ImportOrganizationUsersAndGroupsCommandTests\n{\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();\n\n    [Theory, PaidOrganizationCustomize, BitAutoData]\n    public async Task OrgImportCallsInviteOrgUserCommand(\n            SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,\n            Organization org,\n            List<OrganizationUserUserDetails> existingUsers,\n            List<ImportedOrganizationUser> importedUsers,\n            List<ImportedGroup> newGroups)\n    {\n        SetupOrganizationConfigForImport(sutProvider, org, existingUsers, importedUsers);\n\n        var orgUsers = new List<OrganizationUser>();\n\n        // fix mocked email format, mock OrganizationUsers.\n        foreach (var u in importedUsers)\n        {\n            u.Email += \"@bitwardentest.com\";\n            orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId });\n        }\n\n        importedUsers.Add(new ImportedOrganizationUser\n        {\n            Email = existingUsers.First().Email,\n            ExternalId = existingUsers.First().ExternalId\n        });\n\n\n        existingUsers.First().Type = OrganizationUserType.Owner;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n\n        sutProvider.GetDependency<IStripePaymentService>().HasSecretsManagerStandalone(org).Returns(true);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers);\n        sutProvider.GetDependency<IOrganizationRepository>().GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(\n            new OrganizationSeatCounts\n            {\n                Users = existingUsers.Count,\n                Sponsored = 0\n            });\n        sutProvider.GetDependency<IOrganizationService>().InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,\n                Arg.Any<IEnumerable<(OrganizationUserInvite, string)>>())\n            .Returns(orgUsers);\n\n        await sutProvider.Sut.ImportAsync(org.Id, newGroups, importedUsers, new List<string>(), false);\n\n        var expectedNewUsersCount = importedUsers.Count - 1;\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)\n            .UpsertManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => !users.Any()));\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(default);\n\n        // Send Invites\n        await sutProvider.GetDependency<IOrganizationService>().Received(1).\n            InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,\n                    Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites => invites.Count() == expectedNewUsersCount));\n\n        // Send events\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUserUserDetails, EventType, EventSystemUser, DateTime?)>>());\n    }\n\n    [Theory, PaidOrganizationCustomize, BitAutoData]\n    public async Task OverwriteExistingUsers_WhenRemovingUserWithoutMasterPassword_Throws(\n            SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,\n            Organization org, List<OrganizationUserUserDetails> existingUsers)\n    {\n        SetupOrganizationConfigForImport(sutProvider, org, existingUsers, []);\n\n        // Existing user does not have a master password\n        existingUsers.First().HasMasterPassword = false;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.ImportAsync(org.Id, [], [], [], true));\n\n        Assert.Contains(\"Sync failed. To proceed, disable the 'Remove and re-add users during next sync' setting and try again.\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(default);\n        await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs()\n            .InviteUsersAsync(default, default, default, default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUserUserDetails, EventType, EventSystemUser, DateTime?)>>());\n    }\n\n    [Theory, PaidOrganizationCustomize, BitAutoData]\n    public async Task OrgImportCreateNewUsersAndMarryExistingUser(\n            SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,\n            Organization org,\n            List<OrganizationUserUserDetails> existingUsers,\n            List<ImportedOrganizationUser> importedUsers,\n            List<ImportedGroup> newGroups)\n    {\n        SetupOrganizationConfigForImport(sutProvider, org, existingUsers, importedUsers);\n\n        var orgUsers = new List<OrganizationUser>();\n        var reInvitedUser = existingUsers.First();\n        // Existing user has no external ID. This will make the SUT call UpsertManyAsync\n        reInvitedUser.ExternalId = \"\";\n\n        // Mock an existing org user for this \"existing\" user\n        var reInvitedOrgUser = new OrganizationUser { Email = reInvitedUser.Email, Id = reInvitedUser.Id };\n\n        // fix email formatting, mock orgUsers to be returned\n        foreach (var u in existingUsers)\n        {\n            u.Email += \"@bitwardentest.com\";\n            orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId });\n        }\n        foreach (var u in importedUsers)\n        {\n            u.Email += \"@bitwardentest.com\";\n            orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId });\n        }\n\n        // add the existing user to be re-imported\n        importedUsers.Add(new ImportedOrganizationUser\n        {\n            Email = reInvitedUser.Email,\n            ExternalId = reInvitedUser.Email,\n        });\n\n        var expectedNewUsersCount = importedUsers.Count - 1;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<OrganizationUser>([reInvitedOrgUser]));\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers);\n        sutProvider.GetDependency<IOrganizationRepository>().GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(\n            new OrganizationSeatCounts\n            {\n                Users = existingUsers.Count,\n                Sponsored = 0\n            });\n\n        sutProvider.GetDependency<IOrganizationService>().InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,\n                Arg.Any<IEnumerable<(OrganizationUserInvite, string)>>())\n            .Returns(orgUsers);\n\n        await sutProvider.Sut.ImportAsync(org.Id, newGroups, importedUsers, new List<string>(), false);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(default);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(default, default);\n\n        // Upserted existing user\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)\n            .UpsertManyAsync(Arg.Is<IEnumerable<OrganizationUser>>(users => users.Count() == 1 && users.First() == reInvitedOrgUser));\n\n        // Send Invites\n        await sutProvider.GetDependency<IOrganizationService>().Received(1).\n            InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi,\n                    Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(invites => invites.Count() == expectedNewUsersCount));\n\n        // Send events\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUserUserDetails, EventType, EventSystemUser, DateTime?)>>());\n    }\n\n    private void SetupOrganizationConfigForImport(\n            SutProvider<ImportOrganizationUsersAndGroupsCommand> sutProvider,\n            Organization org,\n            List<OrganizationUserUserDetails> existingUsers,\n            List<ImportedOrganizationUser> importedUsers)\n    {\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        org.UseDirectory = true;\n        org.Seats = importedUsers.Count + existingUsers.Count + 1;\n    }\n\n    // Must set real guids in order for dictionary of guids to not throw aggregate exceptions\n    private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository)\n    {\n        organizationUserRepository.CreateManyAsync(Arg.Any<IEnumerable<OrganizationUser>>()).Returns(\n            info =>\n            {\n                var orgUsers = info.Arg<IEnumerable<OrganizationUser>>();\n                foreach (var orgUser in orgUsers)\n                {\n                    orgUser.Id = Guid.NewGuid();\n                }\n\n                return Task.FromResult<ICollection<Guid>>(orgUsers.Select(u => u.Id).ToList());\n            }\n        );\n\n        organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>(), Arg.Any<IEnumerable<CollectionAccessSelection>>()).Returns(\n            info =>\n            {\n                var orgUser = info.Arg<OrganizationUser>();\n                orgUser.Id = Guid.NewGuid();\n                return Task.FromResult<Guid>(orgUser.Id);\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationApiKeys/CreateOrganizationApiKeyCommandTest.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\n\n[SutProviderCustomize]\npublic class CreateOrganizationApiKeyCommandTest\n{\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_CreatesOrganizationApiKey(SutProvider<CreateOrganizationApiKeyCommand> sutProvider,\n        Guid organizationId, OrganizationApiKeyType keyType)\n    {\n        await sutProvider.Sut.CreateAsync(organizationId, keyType);\n\n        await sutProvider.GetDependency<IOrganizationApiKeyRepository>().Received(1)\n            .CreateAsync(Arg.Is<OrganizationApiKey>(o => o.OrganizationId == organizationId\n                                                         && o.Type == keyType));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\n\n[SutProviderCustomize]\npublic class GetOrganizationApiKeyQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationApiKey_HasOne_Returns(SutProvider<GetOrganizationApiKeyQuery> sutProvider,\n        Guid id, Guid organizationId, OrganizationApiKeyType keyType)\n    {\n        sutProvider.GetDependency<IOrganizationApiKeyRepository>()\n            .GetManyByOrganizationIdTypeAsync(organizationId, keyType)\n            .Returns(new List<OrganizationApiKey>\n            {\n                new OrganizationApiKey\n                {\n                    Id = id,\n                    OrganizationId = organizationId,\n                    ApiKey = \"test\",\n                    Type = keyType,\n                    RevisionDate = DateTime.Now.AddDays(-1),\n                },\n            });\n\n        var apiKey = await sutProvider.Sut.GetOrganizationApiKeyAsync(organizationId, keyType);\n        Assert.NotNull(apiKey);\n        Assert.Equal(id, apiKey.Id);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationApiKey_HasTwo_Throws(SutProvider<GetOrganizationApiKeyQuery> sutProvider,\n        Guid organizationId, OrganizationApiKeyType keyType)\n    {\n        sutProvider.GetDependency<IOrganizationApiKeyRepository>()\n            .GetManyByOrganizationIdTypeAsync(organizationId, keyType)\n            .Returns(new List<OrganizationApiKey>\n            {\n                new OrganizationApiKey\n                {\n                    Id = Guid.NewGuid(),\n                    OrganizationId = organizationId,\n                    ApiKey = \"test\",\n                    Type = keyType,\n                    RevisionDate = DateTime.Now.AddDays(-1),\n                },\n                new OrganizationApiKey\n                {\n                    Id = Guid.NewGuid(),\n                    OrganizationId = organizationId,\n                    ApiKey = \"test_other\",\n                    Type = keyType,\n                    RevisionDate = DateTime.Now.AddDays(-1),\n                },\n            });\n\n        await Assert.ThrowsAsync<InvalidOperationException>(\n            async () => await sutProvider.Sut.GetOrganizationApiKeyAsync(organizationId, keyType));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationApiKey_BadType_Throws(SutProvider<GetOrganizationApiKeyQuery> sutProvider,\n        Guid organizationId, OrganizationApiKeyType keyType)\n    {\n        keyType = (OrganizationApiKeyType)byte.MaxValue;\n\n        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(\n            async () => await sutProvider.Sut.GetOrganizationApiKeyAsync(organizationId, keyType));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationApiKeys/RotateOrganizationApiKeyCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationApiKeys;\n\n[SutProviderCustomize]\npublic class RotateOrganizationApiKeyCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task RotateApiKeyAsync_RotatesKey(SutProvider<RotateOrganizationApiKeyCommand> sutProvider,\n        OrganizationApiKey organizationApiKey)\n    {\n        var existingKey = organizationApiKey.ApiKey;\n        organizationApiKey = await sutProvider.Sut.RotateApiKeyAsync(organizationApiKey);\n        Assert.NotEqual(existingKey, organizationApiKey.ApiKey);\n        AssertHelper.AssertRecent(organizationApiKey.RevisionDate);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;\nusing Bit.Core.Models.Data.Organizations.OrganizationConnections;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationConnections;\n\n[SutProviderCustomize]\npublic class CreateOrganizationConnectionCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_CallsCreate(OrganizationConnectionData<BillingSyncConfig> data,\n        SutProvider<CreateOrganizationConnectionCommand> sutProvider)\n    {\n        await sutProvider.Sut.CreateAsync(data);\n\n        await sutProvider.GetDependency<IOrganizationConnectionRepository>().Received(1)\n            .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.ToEntity())));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationConnections/DeleteOrganizationConnectionCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationConnections;\n\n[SutProviderCustomize]\npublic class DeleteOrganizationConnectionCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteAsync_CallsDelete(OrganizationConnection connection,\n        SutProvider<DeleteOrganizationConnectionCommand> sutProvider)\n    {\n        await sutProvider.Sut.DeleteAsync(connection);\n\n        await sutProvider.GetDependency<IOrganizationConnectionRepository>().Received(1)\n            .DeleteAsync(connection);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationConnections;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationConnections;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationConnectionCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_NoId_Fails(OrganizationConnectionData<BillingSyncConfig> data,\n        SutProvider<UpdateOrganizationConnectionCommand> sutProvider)\n    {\n        data.Id = null;\n\n        var exception = await Assert.ThrowsAsync<Exception>(() => sutProvider.Sut.UpdateAsync(data));\n\n        Assert.Contains(\"Cannot update connection, Connection does not exist.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationConnectionRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_ConnectionDoesNotExist_ThrowsNotFound(\n        OrganizationConnectionData<BillingSyncConfig> data,\n        SutProvider<UpdateOrganizationConnectionCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data));\n\n        await sutProvider.GetDependency<IOrganizationConnectionRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_CallsUpsert(OrganizationConnectionData<BillingSyncConfig> data,\n        OrganizationConnection existing,\n        SutProvider<UpdateOrganizationConnectionCommand> sutProvider)\n    {\n        data.Id = existing.Id;\n\n        sutProvider.GetDependency<IOrganizationConnectionRepository>().GetByIdAsync(data.Id.Value).Returns(existing);\n        await sutProvider.Sut.UpdateAsync(data);\n\n        await sutProvider.GetDependency<IOrganizationConnectionRepository>().Received(1)\n            .UpsertAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.ToEntity())));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationConnections;\n\n[SutProviderCustomize]\npublic class ValidateBillingSyncKeyCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateBillingSyncKeyAsync_NullOrganization_Throws(SutProvider<ValidateBillingSyncKeyCommand> sutProvider)\n    {\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.ValidateBillingSyncKeyAsync(null, null));\n    }\n\n    [Theory]\n    [BitAutoData((string)null)]\n    [BitAutoData(\"\")]\n    [BitAutoData(\"       \")]\n    public async Task ValidateBillingSyncKeyAsync_BadString_ReturnsFalse(string billingSyncKey, SutProvider<ValidateBillingSyncKeyCommand> sutProvider)\n    {\n        Assert.False(await sutProvider.Sut.ValidateBillingSyncKeyAsync(new Organization(), billingSyncKey));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateBillingSyncKeyAsync_KeyEquals_ReturnsTrue(SutProvider<ValidateBillingSyncKeyCommand> sutProvider,\n        Organization organization, OrganizationApiKey orgApiKey, string billingSyncKey)\n    {\n        orgApiKey.ApiKey = billingSyncKey;\n\n        sutProvider.GetDependency<IOrganizationApiKeyRepository>()\n            .GetManyByOrganizationIdTypeAsync(organization.Id, OrganizationApiKeyType.BillingSync)\n            .Returns(new[] { orgApiKey });\n\n        Assert.True(await sutProvider.Sut.ValidateBillingSyncKeyAsync(organization, billingSyncKey));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateBillingSyncKeyAsync_KeyDoesNotEqual_ReturnsFalse(SutProvider<ValidateBillingSyncKeyCommand> sutProvider,\n        Organization organization, OrganizationApiKey orgApiKey, string billingSyncKey)\n    {\n        sutProvider.GetDependency<IOrganizationApiKeyRepository>()\n            .GetManyByOrganizationIdTypeAsync(organization.Id, OrganizationApiKeyType.BillingSync)\n            .Returns(new[] { orgApiKey });\n\n        Assert.False(await sutProvider.Sut.ValidateBillingSyncKeyAsync(organization, billingSyncKey));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/CreateOrganizationDomainCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\n[SutProviderCustomize]\npublic class CreateOrganizationDomainCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task CreateAsync_ShouldCreateOrganizationDomainAndLogEvent_WhenDetailsAreValid(OrganizationDomain orgDomain, SutProvider<CreateOrganizationDomainCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(orgDomain.DomainName)\n            .Returns(new List<OrganizationDomain>());\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName)\n            .ReturnsNull();\n        orgDomain.SetNextRunDate(12);\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .CreateAsync(orgDomain)\n            .Returns(orgDomain);\n\n\n        var result = await sutProvider.Sut.CreateAsync(orgDomain);\n\n        Assert.Equal(orgDomain.Id, result.Id);\n        Assert.Equal(orgDomain.OrganizationId, result.OrganizationId);\n        Assert.Null(result.LastCheckedDate);\n        Assert.Equal(orgDomain.Txt, result.Txt);\n        Assert.Equal(orgDomain.Txt.Length == 47, result.Txt.Length == 47);\n        Assert.Equal(orgDomain.NextRunDate, result.NextRunDate);\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_Added);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_ShouldThrowConflictException_WhenDomainIsClaimed(OrganizationDomain orgDomain,\n        SutProvider<CreateOrganizationDomainCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(orgDomain.DomainName)\n            .Returns(new List<OrganizationDomain>()\n            {\n                orgDomain\n            });\n\n        var requestAction = async () => await sutProvider.Sut.CreateAsync(orgDomain);\n\n        var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);\n        Assert.Contains(\"The domain is not available to be claimed.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_ShouldThrowConflictException_WhenEntryIsDuplicatedForOrganization(OrganizationDomain orgDomain,\n        SutProvider<CreateOrganizationDomainCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(orgDomain.DomainName)\n            .Returns(new List<OrganizationDomain>());\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByOrgIdAndDomainNameAsync(orgDomain.OrganizationId, orgDomain.DomainName)\n            .Returns(orgDomain);\n\n        var requestAction = async () => await sutProvider.Sut.CreateAsync(orgDomain);\n\n        var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);\n        Assert.Contains(\"A domain already exists for this organization.\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/DeleteOrganizationDomainCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\n[SutProviderCustomize]\npublic class DeleteOrganizationDomainCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_Success(Guid id, SutProvider<DeleteOrganizationDomainCommand> sutProvider)\n    {\n        var expected = new OrganizationDomain\n        {\n            Id = id,\n            OrganizationId = Guid.NewGuid(),\n            DomainName = \"Test Domain\",\n            Txt = \"btw+test18383838383\"\n        };\n\n        await sutProvider.Sut.DeleteAsync(expected);\n\n        await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1).DeleteAsync(expected);\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_Removed);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\n[SutProviderCustomize]\npublic class GetOrganizationDomainByIdOrganizationIdQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task GetOrganizationDomainByIdAndOrganizationIdAsync_WithExistingParameters_ReturnsExpectedEntity(\n        OrganizationDomain organizationDomain, SutProvider<GetOrganizationDomainByIdOrganizationIdQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)\n            .Returns(organizationDomain);\n\n        var result = await sutProvider.Sut.GetOrganizationDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId);\n\n        await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId);\n\n        Assert.Equal(organizationDomain, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationDomainByIdAndOrganizationIdAsync_WithNonExistingParameters_ReturnsNull(\n        Guid id, Guid organizationId, OrganizationDomain organizationDomain,\n        SutProvider<GetOrganizationDomainByIdOrganizationIdQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)\n            .Returns(organizationDomain);\n\n        var result = await sutProvider.Sut.GetOrganizationDomainByIdOrganizationIdAsync(id, organizationId);\n\n        await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)\n            .GetDomainByIdOrganizationIdAsync(id, organizationId);\n\n        Assert.Null(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationDomainByIdAndOrganizationIdAsync_WithNonExistingId_ReturnsNull(\n        Guid id, OrganizationDomain organizationDomain,\n        SutProvider<GetOrganizationDomainByIdOrganizationIdQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)\n            .Returns(organizationDomain);\n\n        var result = await sutProvider.Sut.GetOrganizationDomainByIdOrganizationIdAsync(id, organizationDomain.OrganizationId);\n\n        await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)\n            .GetDomainByIdOrganizationIdAsync(id, organizationDomain.OrganizationId);\n\n        Assert.Null(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationDomainByIdAndOrganizationIdAsync_WithNonExistingOrgId_ReturnsNull(\n        Guid organizationId, OrganizationDomain organizationDomain,\n        SutProvider<GetOrganizationDomainByIdOrganizationIdQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)\n            .Returns(organizationDomain);\n\n        var result = await sutProvider.Sut.GetOrganizationDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationId);\n\n        await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)\n            .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationId);\n\n        Assert.Null(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByOrganizationIdQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\n[SutProviderCustomize]\npublic class GetOrganizationDomainByOrganizationIdQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task GetDomainsByOrganizationId_CallsGetDomainsByOrganizationIdAsync(Guid orgId,\n        SutProvider<GetOrganizationDomainByOrganizationIdQuery> sutProvider)\n    {\n        await sutProvider.Sut.GetDomainsByOrganizationIdAsync(orgId);\n\n        await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)\n            .GetDomainsByOrganizationIdAsync(orgId);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/OrganizationHasVerifiedDomainsQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\n[SutProviderCustomize]\npublic class OrganizationHasVerifiedDomainsQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task HasVerifiedDomainsAsync_WithVerifiedDomain_ReturnsTrue(\n        OrganizationDomain organizationDomain,\n        SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)\n    {\n        organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)\n            .Returns(new List<OrganizationDomain> { organizationDomain });\n\n        var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);\n\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse(\n        OrganizationDomain organizationDomain,\n        SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)\n            .Returns(new List<OrganizationDomain> { organizationDomain });\n\n        var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);\n\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse(\n        Guid organizationId,\n        SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetDomainsByOrganizationIdAsync(organizationId)\n            .Returns(new List<OrganizationDomain>());\n\n        var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId);\n\n        Assert.False(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;\n\n[SutProviderCustomize]\npublic class VerifyOrganizationDomainCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,\n        SutProvider<VerifyOrganizationDomainCommand> sutProvider)\n    {\n        var expected = new OrganizationDomain\n        {\n            Id = id,\n            OrganizationId = Guid.NewGuid(),\n            DomainName = \"Test Domain\",\n            Txt = \"btw+test18383838383\"\n        };\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(Guid.NewGuid());\n\n        expected.SetVerifiedDate();\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetByIdAsync(id)\n            .Returns(expected);\n\n        var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);\n\n        var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);\n        Assert.Contains(\"Domain has already been verified.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,\n        SutProvider<VerifyOrganizationDomainCommand> sutProvider)\n    {\n        var expected = new OrganizationDomain\n        {\n            Id = id,\n            OrganizationId = Guid.NewGuid(),\n            DomainName = \"Test Domain\",\n            Txt = \"btw+test18383838383\"\n        };\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetByIdAsync(id)\n            .Returns(expected);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(Guid.NewGuid());\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(expected.DomainName)\n            .Returns(new List<OrganizationDomain> { expected });\n\n        var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);\n\n        var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);\n        Assert.Contains(\"The domain is not available to be claimed.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserVerifyOrganizationDomainAsync_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,\n        SutProvider<VerifyOrganizationDomainCommand> sutProvider)\n    {\n        var expected = new OrganizationDomain\n        {\n            Id = id,\n            OrganizationId = Guid.NewGuid(),\n            DomainName = \"Test Domain\",\n            Txt = \"btw+test18383838383\"\n        };\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetByIdAsync(id)\n            .Returns(expected);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(Guid.NewGuid());\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(expected.DomainName)\n            .Returns(new List<OrganizationDomain>());\n\n        sutProvider.GetDependency<IDnsResolverService>()\n            .ResolveAsync(expected.DomainName, Arg.Any<string>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);\n\n        Assert.NotNull(result.VerifiedDate);\n        await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)\n            .ReplaceAsync(Arg.Any<OrganizationDomain>());\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_Verified);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserVerifyOrganizationDomainAsync_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,\n        SutProvider<VerifyOrganizationDomainCommand> sutProvider)\n    {\n        var expected = new OrganizationDomain\n        {\n            Id = id,\n            OrganizationId = Guid.NewGuid(),\n            DomainName = \"Test Domain\",\n            Txt = \"btw+test18383838383\"\n        };\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetByIdAsync(id)\n            .Returns(expected);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(Guid.NewGuid());\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(expected.DomainName)\n            .Returns(new List<OrganizationDomain>());\n\n        sutProvider.GetDependency<IDnsResolverService>()\n            .ResolveAsync(expected.DomainName, Arg.Any<string>())\n            .Returns(false);\n\n        var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);\n\n        Assert.Null(result.VerifiedDate);\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_NotVerified);\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task SystemVerifyOrganizationDomainAsync_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)\n    {\n        var domain = new OrganizationDomain()\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            CreationDate = DateTime.UtcNow,\n            DomainName = \"test.com\",\n            Txt = \"btw+12345\",\n        };\n\n        _ = await sutProvider.Sut.SystemVerifyOrganizationDomainAsync(domain);\n\n        await sutProvider.GetDependency<IEventService>().ReceivedWithAnyArgs(1)\n            .LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,\n                EventSystemUser.DomainVerification);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled(\n        OrganizationDomain domain, Guid userId, SutProvider<VerifyOrganizationDomainCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(domain.DomainName)\n            .Returns([]);\n\n        sutProvider.GetDependency<IDnsResolverService>()\n            .ResolveAsync(domain.DomainName, domain.Txt)\n            .Returns(true);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(userId);\n\n        _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);\n\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .Received(1)\n            .SaveAsync(Arg.Is<SavePolicyModel>(x => x.PolicyUpdate.Type == PolicyType.SingleOrg &&\n                                                    x.PolicyUpdate.OrganizationId == domain.OrganizationId &&\n                                                    x.PolicyUpdate.Enabled &&\n                x.PerformedBy is StandardUser &&\n                x.PerformedBy.UserId == userId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserVerifyOrganizationDomainAsync_UsesVNextSavePolicyCommand(\n        OrganizationDomain domain, Guid userId, SutProvider<VerifyOrganizationDomainCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(domain.DomainName)\n            .Returns([]);\n\n        sutProvider.GetDependency<IDnsResolverService>()\n            .ResolveAsync(domain.DomainName, domain.Txt)\n            .Returns(true);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(userId);\n\n        _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);\n\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .Received(1)\n            .SaveAsync(Arg.Is<SavePolicyModel>(m =>\n                m.PolicyUpdate.Type == PolicyType.SingleOrg &&\n                m.PolicyUpdate.OrganizationId == domain.OrganizationId &&\n                m.PolicyUpdate.Enabled &&\n                m.PerformedBy is StandardUser &&\n                m.PerformedBy.UserId == userId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled(\n        OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(domain.DomainName)\n            .Returns([]);\n\n        sutProvider.GetDependency<IDnsResolverService>()\n            .ResolveAsync(domain.DomainName, domain.Txt)\n            .Returns(false);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(Guid.NewGuid());\n\n        _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);\n\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .DidNotReceive()\n            .SaveAsync(Arg.Any<SavePolicyModel>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain(\n        ICollection<OrganizationUserUserDetails> organizationUsers,\n        OrganizationDomain domain,\n        Organization organization,\n        SutProvider<VerifyOrganizationDomainCommand> sutProvider)\n    {\n        foreach (var organizationUser in organizationUsers)\n        {\n            organizationUser.Email = $\"{organizationUser.Name}@{domain.DomainName}\";\n        }\n\n        var mockedUsers = organizationUsers\n            .Where(x => x.Status != OrganizationUserStatusType.Invited &&\n                        x.Status != OrganizationUserStatusType.Revoked).ToList();\n\n        organization.Id = domain.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .GetClaimedDomainsByDomainNameAsync(domain.DomainName)\n            .Returns([]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(domain.OrganizationId)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IDnsResolverService>()\n            .ResolveAsync(domain.DomainName, domain.Txt)\n            .Returns(true);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId.Returns(Guid.NewGuid());\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(domain.OrganizationId)\n            .Returns(mockedUsers);\n\n        _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);\n\n        await sutProvider.GetDependency<IMailService>().Received().SendClaimedDomainUserEmailAsync(\n            Arg.Is<ClaimedUserDomainClaimedEmails>(x =>\n                x.EmailList.Count(e => e.EndsWith(domain.DomainName)) == mockedUsers.Count &&\n                x.Organization.Id == organization.Id &&\n                x.DomainName == domain.DomainName));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Fakes;\nusing NSubstitute;\nusing Xunit;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;\n\n// Note: test names follow MethodName_StateUnderTest_ExpectedBehavior pattern.\n[SutProviderCustomize]\npublic class AcceptOrgUserCommandTests\n{\n    private readonly IUserService _userService = Substitute.For<IUserService>();\n    private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For<IOrgUserInviteTokenableFactory>();\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();\n\n    // Base AcceptOrgUserAsync method tests ----------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUser_InvitedUserToSingleOrg_AcceptsOrgUser(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // Act\n        var resultOrgUser = await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        // Assert\n        // Verify returned org user details\n        AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);\n\n        // Verify org repository called with updated orgUser\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(\n            Arg.Is<OrganizationUser>(ou => ou.Id == orgUser.Id && ou.Status == OrganizationUserStatusType.Accepted));\n\n        // Verify emails sent to admin\n        await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationAcceptedEmailAsync(\n            Arg.Is<Organization>(o => o.Id == org.Id),\n            Arg.Is<string>(e => e == user.Email),\n            Arg.Is<IEnumerable<string>>(a => a.Contains(adminUserDetails.Email))\n        );\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUser_OrgUserStatusIsRevoked_ReturnsBadRequest(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Common setup\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // Revoke user status\n        orgUser.Status = OrganizationUserStatusType.Revoked;\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));\n\n        Assert.Equal(\"Your organization access has been revoked.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Accepted)]\n    [BitAutoData(OrganizationUserStatusType.Confirmed)]\n    public async Task AcceptOrgUser_OrgUserStatusIsNotInvited_ThrowsBadRequest(\n        OrganizationUserStatusType orgUserStatus,\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // Set status to something other than invited\n        orgUser.Status = orgUserStatus;\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));\n\n        Assert.Equal(\"Already accepted.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // Organization they are trying to join requires 2FA\n        var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId };\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication,\n                OrganizationUserStatusType.Invited)\n            .Returns(Task.FromResult<ICollection<OrganizationUserPolicyDetails>>(\n                new List<OrganizationUserPolicyDetails> { twoFactorPolicy }));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));\n\n        Assert.Equal(\"You cannot join this organization until you enable two-step login on your user account.\",\n            exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // Enable the PolicyRequirements feature flag for the new 2FA path\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n\n        // No SingleOrg policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Organization they are trying to join requires 2FA\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = orgUser.OrganizationId,\n                    OrganizationUserStatus = OrganizationUserStatusType.Invited,\n                    PolicyType = PolicyType.TwoFactorAuthentication,\n                }\n            ]));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));\n\n        Assert.Equal(\"You cannot join this organization until you enable two-step login on your user account.\",\n            exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWith2FAJoining2FARequiredOrg_Succeeds(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // User has 2FA enabled\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(user)\n            .Returns(true);\n\n        // No SingleOrg policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Organization they are trying to join requires 2FA\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = orgUser.OrganizationId,\n                    OrganizationUserStatus = OrganizationUserStatusType.Invited,\n                    PolicyType = PolicyType.TwoFactorAuthentication,\n                }\n            ]));\n\n        await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserJoiningOrgWithout2FARequirement_Succeeds(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // No SingleOrg policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Organization they are trying to join doesn't require 2FA\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = Guid.NewGuid(),\n                    OrganizationUserStatus = OrganizationUserStatusType.Invited,\n                    PolicyType = PolicyType.TwoFactorAuthentication,\n                }\n            ]));\n\n        await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_WithSingleOrgEnabled_UserJoiningOrgWithSingleOrgPolicy_ThrowsBadRequest(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // User is part of another org\n        var otherOrgUser = new OrganizationUser\n        {\n            UserId = user.Id,\n            OrganizationId = Guid.NewGuid(),\n            Status = OrganizationUserStatusType.Confirmed\n        };\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns(Task.FromResult<ICollection<OrganizationUser>>(new List<OrganizationUser> { otherOrgUser }));\n\n        // Target org has SingleOrg policy, user is a regular User (not exempt)\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForTargetOrganization(org.Id));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));\n\n        Assert.Equal(\"You cannot accept this invite until you leave or remove all other organizations.\",\n            exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_UserInOrgWithSingleOrgPolicyAlready_ThrowsBadRequest(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser,\n        OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // Another org the user is in has SingleOrg policy (not the target org)\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForAnotherOrganization());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));\n\n        Assert.Equal(\"You cannot accept this invite because you are in another organization which forbids it.\",\n            exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_NoSingleOrgPolicy_Succeeds(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // No SingleOrg policy applies\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        // No 2FA policy either\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement([]));\n\n        // Act\n        var resultOrgUser = await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        // Assert\n        AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task AcceptOrgUser_AdminOfFreePlanTryingToJoinSecondFreeOrg_ThrowsBadRequest(\n        OrganizationUserType userType,\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n        org.PlanType = PlanType.Free;\n        orgUser.Type = userType;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByFreeOrganizationAdminUserAsync(user.Id)\n            .Returns(1);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));\n\n        Assert.Equal(\"You can only be an admin of one free organization.\", exception.Message);\n    }\n\n    // AcceptOrgUserByOrgIdAsync tests --------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByToken_NewToken_AcceptsUserAndVerifiesEmail(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order\n        // to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n        SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser);\n\n        // Must come after common mocks as they mutate the org user.\n        // Mock tokenable factory to return a token that expires in 5 days\n        _orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)\n        {\n            ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n        });\n\n        var newToken = CreateToken(orgUser);\n\n        // Act\n        var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService);\n\n        // Assert\n        AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);\n\n        // Verify user email verified logic\n        Assert.True(user.EmailVerified);\n        await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(\n            Arg.Is<User>(u => u.Id == user.Id && u.Email == user.Email && user.EmailVerified == true));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByToken_NullOrgUser_ThrowsBadRequest(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Guid orgUserId)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUserId).Returns((OrganizationUser)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUserId, user, \"token\", _userService));\n\n        Assert.Equal(\"User invalid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByToken_GenericInvalidToken_ThrowsBadRequest(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, OrganizationUser orgUser)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUser.Id)\n            .Returns(Task.FromResult(orgUser));\n\n        var invalidToken = \"invalidToken\";\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, invalidToken, _userService));\n\n        Assert.Equal(\"Invalid token.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByToken_ExpiredNewToken_ThrowsBadRequest(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, OrganizationUser orgUser)\n    {\n        // Arrange\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order\n        // to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUser.Id)\n            .Returns(Task.FromResult(orgUser));\n\n        // Must come after common mocks as they mutate the org user.\n        // Mock tokenable factory to return a token that expired yesterday\n        _orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)\n        {\n            ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(-1))\n        });\n\n        var newToken = CreateToken(orgUser);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService));\n\n        Assert.Equal(\"Invalid token.\", exception.Message);\n\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Accepted,\n        \"Invitation already accepted. You will receive an email when your organization membership is confirmed.\")]\n    [BitAutoData(OrganizationUserStatusType.Confirmed,\n        \"You are already part of this organization.\")]\n    public async Task AcceptOrgUserByToken_UserAlreadyInOrg_ThrowsBadRequest(\n        OrganizationUserStatusType statusType,\n        string expectedErrorMessage,\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, OrganizationUser orgUser)\n    {\n        // Arrange\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order\n        // to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUser.Id)\n            .Returns(Task.FromResult(orgUser));\n\n        // Indicate that a user with the given email already exists in the organization\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByOrganizationAsync(orgUser.OrganizationId, user.Email, true)\n            .Returns(1);\n\n        orgUser.Status = statusType;\n\n        // Must come after common mocks as they mutate the org user.\n        // Mock tokenable factory to return valid, new token that expires in 5 days\n        _orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)\n        {\n            ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n        });\n\n        var newToken = CreateToken(orgUser);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService));\n\n        Assert.Equal(expectedErrorMessage, exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByToken_EmailMismatch_ThrowsBadRequest(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, OrganizationUser orgUser)\n    {\n        // Arrange\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order\n        // to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        SetupCommonAcceptOrgUserByTokenMocks(sutProvider, user, orgUser);\n\n        // Modify the orgUser's email to be different from the user's email to simulate the mismatch\n        orgUser.Email = \"mismatchedEmail@example.com\";\n\n        // Must come after common mocks as they mutate the org user.\n        // Mock tokenable factory to return a token that expires in 5 days\n        _orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)\n        {\n            ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n        });\n\n        var newToken = CreateToken(orgUser);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService));\n\n        Assert.Equal(\"User email does not match invite.\", exception.Message);\n    }\n\n\n    // AcceptOrgUserByOrgSsoIdAsync -----------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByOrgSsoIdAsync_ValidData_AcceptsOrgUser(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(org.Identifier)\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .Returns(orgUser);\n\n        // Act\n        var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByOrgSsoIdAsync(org.Identifier, user, _userService);\n\n        // Assert\n        AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByOrgSsoIdAsync_InvalidOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider,\n        string orgSsoIdentifier, User user)\n    {\n        // Arrange\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(orgSsoIdentifier)\n            .Returns((Organization)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptOrgUserByOrgSsoIdAsync(orgSsoIdentifier, user, _userService));\n\n        Assert.Equal(\"Organization invalid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByOrgSsoIdAsync_UserNotInOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider,\n        Organization org, User user)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(org.Identifier)\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .Returns((OrganizationUser)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptOrgUserByOrgSsoIdAsync(org.Identifier, user, _userService));\n\n        Assert.Equal(\"User not found within organization.\", exception.Message);\n    }\n\n    // AcceptOrgUserByOrgIdAsync ---------------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByOrgId_ValidData_AcceptsOrgUser(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(org.Id)\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .Returns(orgUser);\n\n        // Act\n        var resultOrgUser = await sutProvider.Sut.AcceptOrgUserByOrgIdAsync(org.Id, user, _userService);\n\n        // Assert\n        AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByOrgId_InvalidOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider,\n        Guid orgId, User user)\n    {\n        // Arrange\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(orgId)\n            .Returns((Organization)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptOrgUserByOrgIdAsync(orgId, user, _userService));\n\n        Assert.Equal(\"Organization invalid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserByOrgId_UserNotInOrg_ThrowsBadRequest(SutProvider<AcceptOrgUserCommand> sutProvider,\n        Organization org, User user)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(org.Id)\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .Returns((OrganizationUser)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptOrgUserByOrgIdAsync(org.Id, user, _userService));\n\n        Assert.Equal(\"User not found within organization.\", exception.Message);\n    }\n\n    // Auto-confirm policy validation tests --------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_WithAutoConfirmIsNotEnabled_DoesNotCheckCompliance(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // Act\n        var resultOrgUser = await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        // Assert\n        AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);\n\n        await sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>().DidNotReceiveWithAnyArgs()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_WithUserThatIsCompliantWithAutoConfirm_AcceptsUser(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        // Mock auto-confirm enforcement query to return valid (no auto-confirm restrictions)\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));\n\n        // Act\n        var resultOrgUser = await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        // Assert\n        AssertValidAcceptedOrgUser(resultOrgUser, orgUser, user);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(\n            Arg.Is<OrganizationUser>(ou => ou.Id == orgUser.Id && ou.Status == OrganizationUserStatusType.Accepted));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_WithAutoConfirmIsEnabledAndFailsCompliance_ThrowsBadRequestException(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails,\n        OrganizationUser otherOrgUser)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Invalid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser, otherOrgUser], user),\n                new UserCannotBelongToAnotherOrganization()));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));\n\n        // Should get auto-confirm error\n        Assert.Equal(new UserCannotBelongToAnotherOrganization().Message, exception.Message);\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_WithAutoConfirmPolicyEnabled_DeletesEmergencyAccess(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));\n\n        // Act\n        await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .Received(1)\n            .DeleteAllByUserIdAsync(user.Id);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUserAsync_WithAutoConfirmPolicyNotEnabled_DoesNotDeleteEmergencyAccess(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));\n\n        // Act\n        await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdAsync(Arg.Any<Guid>());\n    }\n\n    // Private helpers -------------------------------------------------------------------------------------------------\n\n    /// <summary>\n    ///  Asserts that the given org user is in the expected state after a successful AcceptOrgUserAsync call.\n    ///  For use in happy path tests.\n    /// </summary>\n    private void AssertValidAcceptedOrgUser(OrganizationUser resultOrgUser, OrganizationUser expectedOrgUser, User user)\n    {\n        Assert.NotNull(resultOrgUser);\n        Assert.Equal(OrganizationUserStatusType.Accepted, resultOrgUser.Status);\n        Assert.Equal(expectedOrgUser, resultOrgUser);\n        Assert.Equal(expectedOrgUser.Id, resultOrgUser.Id);\n        Assert.Null(resultOrgUser.Email);\n        Assert.Equal(user.Id, resultOrgUser.UserId);\n\n\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUser_WithAutoConfirmFeatureFlagEnabled_SendsPushNotification(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        await sutProvider.GetDependency<IPushAutoConfirmNotificationCommand>()\n            .Received(1)\n            .PushAsync(user.Id, orgUser.OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AcceptOrgUser_WithAutoConfirmFeatureFlagDisabled_DoesNotSendPushNotification(\n        SutProvider<AcceptOrgUserCommand> sutProvider,\n        User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(false);\n\n        await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);\n\n        await sutProvider.GetDependency<IPushAutoConfirmNotificationCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .PushAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n\n    private void SetupCommonAcceptOrgUserByTokenMocks(SutProvider<AcceptOrgUserCommand> sutProvider, User user, OrganizationUser orgUser)\n    {\n        user.EmailVerified = false;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUser.Id)\n            .Returns(Task.FromResult(orgUser));\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByOrganizationAsync(orgUser.OrganizationId, user.Email, true)\n            .Returns(0);\n    }\n\n    /// <summary>\n    /// Sets up common mock behavior for the AcceptOrgUserAsync tests.\n    /// This method initializes:\n    /// - The invited user's email, status, type, and organization ID.\n    /// - Ensures the user is not part of any other organizations.\n    /// - Confirms the target organization doesn't have a single org policy.\n    /// - Ensures the user doesn't belong to an organization with a single org policy.\n    /// - Assumes the user doesn't have 2FA enabled and the organization doesn't require it.\n    /// - Provides mock data for an admin to validate email functionality.\n    /// - Returns the corresponding organization for the given org ID.\n    /// </summary>\n    private static void SetupCommonAcceptOrgUserMocks(SutProvider<AcceptOrgUserCommand> sutProvider, User user,\n        Organization org,\n        OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)\n    {\n        // Arrange\n        orgUser.Email = user.Email;\n        orgUser.Status = OrganizationUserStatusType.Invited;\n        orgUser.Type = OrganizationUserType.User;\n        orgUser.OrganizationId = org.Id;\n\n        // User is not part of any other orgs\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([]);\n\n        // Org does not require 2FA\n        sutProvider.GetDependency<IPolicyService>().GetPoliciesApplicableToUserAsync(user.Id,\n                PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited)\n            .Returns([]);\n\n        // Provide at least 1 admin to test email functionality\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin)\n            .Returns([adminUserDetails]);\n\n        // Return org\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(org.Id)\n            .Returns(org);\n\n        // No SingleOrg policy by default\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(Arg.Any<Guid>())\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Auto-confirm enforcement query returns valid by default (no restrictions)\n        var request = new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(request)\n            .Returns(Valid(request));\n    }\n\n    private string CreateToken(OrganizationUser orgUser)\n    {\n        var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser);\n        var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable);\n\n        return protectedToken;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/AutomaticallyConfirmOrganizationUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Models.Data.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\n\n[SutProviderCustomize]\npublic class AutomaticallyConfirmOrganizationUserCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_UseMyItemsDisabled_DoesNotCreateCollection(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser,\n        string key,\n        string collectionName,\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.UseMyItems = false;\n        orgUser.OrganizationId = organization.Id;\n\n        SetupRepositoryMocks(sutProvider, organization, orgUser);\n\n        // Mock positive validation result\n        var validationRequest = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            OrganizationUserId = orgUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = collectionName,\n            PerformedBy = null,\n            OrganizationUser = orgUser,\n            Organization = organization\n        };\n        sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUsersValidator>()\n            .ValidateAsync(Arg.Any<AutomaticallyConfirmOrganizationUserValidationRequest>())\n            .Returns(Valid(validationRequest));\n\n        // Mock enabled policy requirement\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            OrganizationUserId = orgUser.Id,\n            IsProvider = false,\n            OrganizationUserStatus = orgUser.Status,\n            OrganizationUserType = orgUser.Type,\n            PolicyType = PolicyType.OrganizationDataOwnership\n        };\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails]));\n\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = orgUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = collectionName,\n            PerformedBy = null\n        };\n\n        // Act\n        await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert - Collection repository should NOT be called\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_UseMyItemsEnabled_CreatesCollection(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser,\n        string key,\n        string collectionName,\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.UseMyItems = true;\n        orgUser.OrganizationId = organization.Id;\n\n        SetupRepositoryMocks(sutProvider, organization, orgUser);\n\n        // Mock positive validation result\n        var validationRequest = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            OrganizationUserId = orgUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = collectionName,\n            PerformedBy = null,\n            OrganizationUser = orgUser,\n            Organization = organization\n        };\n        sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUsersValidator>()\n            .ValidateAsync(Arg.Any<AutomaticallyConfirmOrganizationUserValidationRequest>())\n            .Returns(Valid(validationRequest));\n\n        // Mock enabled policy requirement\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            OrganizationUserId = orgUser.Id,\n            IsProvider = false,\n            OrganizationUserStatus = orgUser.Status,\n            OrganizationUserType = orgUser.Type,\n            PolicyType = PolicyType.OrganizationDataOwnership\n        };\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails]));\n\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = orgUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = collectionName,\n            PerformedBy = null\n        };\n\n        // Act\n        await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert - Collection repository should be called\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser.Id),\n                collectionName);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_UseMyItemsEnabled_PolicyDisabled_DoesNotCreateCollection(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser,\n        string key,\n        string collectionName,\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.UseMyItems = true;\n        orgUser.OrganizationId = organization.Id;\n\n        SetupRepositoryMocks(sutProvider, organization, orgUser);\n\n        // Mock positive validation result\n        var validationRequest = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            OrganizationUserId = orgUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = collectionName,\n            PerformedBy = null,\n            OrganizationUser = orgUser,\n            Organization = organization\n        };\n        sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUsersValidator>()\n            .ValidateAsync(Arg.Any<AutomaticallyConfirmOrganizationUserValidationRequest>())\n            .Returns(Valid(validationRequest));\n\n        // Mock disabled policy requirement\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, []));\n\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = orgUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = collectionName,\n            PerformedBy = null\n        };\n\n        // Act\n        await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert - Collection repository should NOT be called when policy is disabled\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    private static void SetupRepositoryMocks(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        OrganizationUser organizationUser)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Any<AcceptedOrganizationUserToConfirm>())\n            .Returns(true);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUser/OrganizationAutoConfirmEnabledNotificationCommandTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Mail.Mailer.OrganizationUserAutoConfirmation;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.Platform.Mail.Mailer;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\n\n[SutProviderCustomize]\npublic class OrganizationAutoConfirmEnabledNotificationCommandTests\n{\n    [Theory]\n    [OrganizationCustomize, BitAutoData]\n    public async Task SendEmailAsync_NoEmailsProvided_ReturnsNoEmailsWereProvidedError(\n        Organization organization,\n        SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider)\n    {\n        // Arrange\n        SetupGlobalSettings(sutProvider);\n        var request = new OrganizationAutoConfirmEnabledNotificationRequest(organization, []);\n\n        // Act\n        var result = await sutProvider.Sut.SendEmailAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<NoEmailsWereProvided>(result.AsError);\n        await sutProvider.GetDependency<IMailer>()\n            .DidNotReceive()\n            .SendEmail(Arg.Any<OrganizationAutoConfirmationEnabled>());\n    }\n\n    [Theory]\n    [OrganizationCustomize, BitAutoData]\n    public async Task SendEmailAsync_WithValidEmails_SendsEmailWithCorrectProperties(\n        Organization organization,\n        List<string> emails,\n        SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider)\n    {\n        // Arrange\n        const string vaultUrl = \"https://vault.bitwarden.com/\";\n        SetupGlobalSettings(sutProvider, vaultUrl);\n        var request = new OrganizationAutoConfirmEnabledNotificationRequest(organization, emails);\n        var expectedUrl = $\"{vaultUrl}#/organizations/{organization.Id}/settings/policies\";\n\n        // Act\n        var result = await sutProvider.Sut.SendEmailAsync(request);\n\n        // Assert\n        Assert.True(result.IsSuccess);\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<OrganizationAutoConfirmationEnabled>(mail =>\n                mail.ToEmails.SequenceEqual(emails) &&\n                mail.View.WebVaultUrl == expectedUrl &&\n                mail.Subject == $\"Automatic user confirmation is available for {WebUtility.HtmlEncode(organization.Name)}\"));\n    }\n\n    [Theory]\n    [OrganizationCustomize, BitAutoData]\n    public async Task SendEmailAsync_MailerThrowsException_ReturnsEmailSendingFailedError(\n        Organization organization,\n        List<string> emails,\n        SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider)\n    {\n        // Arrange\n        SetupGlobalSettings(sutProvider);\n        sutProvider.GetDependency<IMailer>()\n            .SendEmail(Arg.Any<OrganizationAutoConfirmationEnabled>())\n            .ThrowsAsync(new Exception(\"SMTP failure\"));\n        var request = new OrganizationAutoConfirmEnabledNotificationRequest(organization, emails);\n\n        // Act\n        var result = await sutProvider.Sut.SendEmailAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<EmailSendingFailed>(result.AsError);\n    }\n\n    [Theory]\n    [OrganizationCustomize, BitAutoData]\n    public async Task SendEmailAsync_OrganizationNameWithSpecialCharacters_HtmlEncodesSubject(\n        Organization organization,\n        List<string> emails,\n        SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider)\n    {\n        // Arrange\n        SetupGlobalSettings(sutProvider);\n        organization.Name = \"Test & Company <script>\";\n        var request = new OrganizationAutoConfirmEnabledNotificationRequest(organization, emails);\n\n        // Act\n        await sutProvider.Sut.SendEmailAsync(request);\n\n        // Assert\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<OrganizationAutoConfirmationEnabled>(mail =>\n                mail.Subject == $\"Automatic user confirmation is available for {WebUtility.HtmlEncode(organization.Name)}\"));\n    }\n\n    private static void SetupGlobalSettings(\n        SutProvider<OrganizationAutoConfirmEnabledNotificationCommand> sutProvider,\n        string vaultUrl = \"https://vault.bitwarden.com/\")\n    {\n        var globalSettings = sutProvider.GetDependency<GlobalSettings>();\n        globalSettings.BaseServiceUri = new GlobalSettings.BaseServiceUriSettings(globalSettings) { Vault = vaultUrl };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmOrganizationUsersValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUsers;\n\n[SutProviderCustomize]\npublic class AutomaticallyConfirmOrganizationUsersValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        Organization organization)\n    {\n        // Arrange\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = null,\n            OrganizationUserId = Guid.NewGuid(),\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<UserNotFoundError>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithNullUserId_ReturnsUserNotFoundError(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser)\n    {\n        // Arrange\n        organizationUser.UserId = null;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<UserNotFoundError>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithNullOrganization_ReturnsOrganizationNotFoundError(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        Guid userId)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = null,\n            OrganizationId = organizationUser.OrganizationId,\n            Key = \"test-key\"\n        };\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationNotFound>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithValidAcceptedUser_ReturnsValidResult(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        [Organization(useAutomaticUserConfirmation: true, planType: PlanType.EnterpriseAnnually)] Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        [Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)\n            .Returns(autoConfirmPolicy);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([(user.Id, true)]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([organizationUser]);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(user.Id)\n            .Returns(user);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())\n            .Returns(Valid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,\n                    [organizationUser],\n                    user)));\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n        Assert.Equal(request, result.Request);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithMismatchedOrganizationId_ReturnsOrganizationUserIdIsInvalidError(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        Guid userId)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.OrganizationId = Guid.NewGuid(); // Different from organization.Id\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns([organizationUser]);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationUserIdIsInvalid>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Invited)]\n    [BitAutoData(OrganizationUserStatusType.Revoked)]\n    [BitAutoData(OrganizationUserStatusType.Confirmed)]\n    public async Task ValidateAsync_WithNotAcceptedStatus_ReturnsUserIsNotAcceptedError(\n        OrganizationUserStatusType statusType,\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        Guid userId)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.OrganizationId = organization.Id;\n        organizationUser.Status = statusType;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<UserIsNotAccepted>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task ValidateAsync_WithNonUserType_ReturnsUserIsNotUserTypeError(\n        OrganizationUserType userType,\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        Guid userId)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.OrganizationId = organization.Id;\n        organizationUser.Type = userType;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<UserIsNotUserType>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_UserWithout2FA_And2FARequired_ReturnsError(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        [Organization(useAutomaticUserConfirmation: true)] Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        Guid userId,\n        [Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        var twoFactorPolicyDetails = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            PolicyType = PolicyType.TwoFactorAuthentication\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)\n            .Returns(autoConfirmPolicy);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([(userId, false)]);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(userId)\n            .Returns(new RequireTwoFactorPolicyRequirement([twoFactorPolicyDetails]));\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns([organizationUser]);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<UserDoesNotHaveTwoFactorEnabled>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_UserWith2FA_ReturnsValidResult(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        [Organization(useAutomaticUserConfirmation: true)] Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        [Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)\n            .Returns(autoConfirmPolicy);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([(user.Id, true)]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([organizationUser]);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(user.Id)\n            .Returns(user);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())\n            .Returns(Valid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,\n                    [organizationUser],\n                    user)));\n\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_UserWithout2FA_And2FANotRequired_ReturnsValidResult(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        [Organization(useAutomaticUserConfirmation: true)] Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        [Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)\n            .Returns(autoConfirmPolicy);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([(user.Id, false)]);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement([])); // No 2FA policy\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([organizationUser]);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(user.Id)\n            .Returns(user);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())\n            .Returns(Valid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,\n                    [organizationUser],\n                    user)));\n\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_UserInSingleOrg_ReturnsValidResult(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        [Organization(useAutomaticUserConfirmation: true)] Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        [Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)\n            .Returns(autoConfirmPolicy);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([(user.Id, true)]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([organizationUser]); // Single org\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(user.Id)\n            .Returns(user);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())\n            .Returns(Valid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,\n                    [organizationUser],\n                    user)));\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithAutoConfirmPolicyDisabled_ReturnsAutoConfirmPolicyNotEnabledError(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        Guid userId,\n        [Policy(PolicyType.AutomaticUserConfirmation, false)] PolicyStatus policy)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)\n            .Returns(policy);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([(userId, true)]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns([organizationUser]);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<AutomaticallyConfirmUsersPolicyIsNotEnabled>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithOrganizationUseAutomaticUserConfirmationDisabled_ReturnsAutoConfirmPolicyNotEnabledError(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        [Organization(useAutomaticUserConfirmation: false)] Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        Guid userId,\n        [Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)\n            .Returns(autoConfirmPolicy);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([(userId, true)]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns([organizationUser]);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<AutomaticallyConfirmUsersPolicyIsNotEnabled>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithNonProviderUser_ReturnsValidResult(\n        SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,\n        [Organization(useAutomaticUserConfirmation: true)] Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        [Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = Substitute.For<IActingUser>(),\n            DefaultUserCollectionName = \"test-collection\",\n            OrganizationUser = organizationUser,\n            OrganizationUserId = organizationUser.Id,\n            Organization = organization,\n            OrganizationId = organization.Id,\n            Key = \"test-key\"\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)\n            .Returns(autoConfirmPolicy);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([(user.Id, true)]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([organizationUser]);\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(user.Id)\n            .Returns(user);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>())\n            .Returns(Valid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id,\n                    [organizationUser],\n                    user)));\n\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AutoConfirmUsers/AutomaticallyConfirmUsersCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Models.Data.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Utilities.v2;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUsers;\n\n[SutProviderCustomize]\npublic class AutomaticallyConfirmUsersCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WithValidRequest_ConfirmsUserSuccessfully(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName,\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert\n        Assert.True(result.IsSuccess);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key));\n\n        await AssertSuccessfulOperationsAsync(sutProvider, organizationUser, organization, user, key);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WithInvalidUserOrgId_ReturnsOrganizationUserIdIsInvalidError(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = Guid.NewGuid(); // User belongs to another organization\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName,\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, false, new OrganizationUserIdIsInvalid());\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationUserIdIsInvalid>(result.AsError);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceive()\n            .ConfirmOrganizationUserAsync(Arg.Any<AcceptedOrganizationUserToConfirm>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WhenAlreadyConfirmed_ReturnsNoneSuccess(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName,\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n\n        // Return false to indicate the user is already confirmed\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(x =>\n                x.OrganizationUserId == organizationUser.Id && x.Key == request.Key))\n            .Returns(false);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert\n        Assert.True(result.IsSuccess);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(x =>\n                x.OrganizationUserId == organizationUser.Id && x.Key == request.Key));\n\n        // Verify no side effects occurred\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceive()\n            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceive()\n            .PushSyncOrgKeysAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectionEnabled_CreatesDefaultCollection(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName, // Non-empty to trigger creation\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n        SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, true); // Policy requires collection\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().ConfirmOrganizationUserAsync(\n                Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                    o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert\n        Assert.True(result.IsSuccess);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == organizationUser.Id),\n                defaultCollectionName);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectionDisabled_DoesNotCreateCollection(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = string.Empty, // Empty, so the collection won't be created\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n        SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, false); // Policy doesn't require\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert\n        Assert.True(result.IsSuccess);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WhenCreateDefaultCollectionFails_LogsErrorButReturnsSuccess(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName, // Non-empty to trigger creation\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n        SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)).Returns(true);\n\n        var collectionException = new Exception(\"Collection creation failed\");\n        sutProvider.GetDependency<ICollectionRepository>()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>())\n            .ThrowsAsync(collectionException);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert - side effects are fire-and-forget, so command returns success even if collection creation fails\n        Assert.True(result.IsSuccess);\n\n        sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Error,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString()!.Contains(\"Failed to create default collection for user\")),\n                collectionException,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WhenEventLogFails_LogsErrorButReturnsSuccess(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName,\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))\n            .Returns(true);\n\n        var eventException = new Exception(\"Event logging failed\");\n        sutProvider.GetDependency<IEventService>()\n            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(),\n                EventType.OrganizationUser_AutomaticallyConfirmed,\n                Arg.Any<DateTime?>())\n            .ThrowsAsync(eventException);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert - side effects are fire-and-forget, so command returns success even if event log fails\n        Assert.True(result.IsSuccess);\n\n        sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Error,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString()!.Contains(\"Failed to log OrganizationUser_AutomaticallyConfirmed event\")),\n                eventException,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WhenSendEmailFails_LogsErrorButReturnsSuccess(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName,\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))\n            .Returns(true);\n\n        var emailException = new Exception(\"Email sending failed\");\n        sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()\n            .SendConfirmationAsync(organization, user.Email, organizationUser.AccessSecretsManager)\n            .ThrowsAsync(emailException);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert - side effects are fire-and-forget, so command returns success even if email fails\n        Assert.True(result.IsSuccess);\n\n        sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Error,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString()!.Contains(\"Failed to send OrganizationUserConfirmed\")),\n                emailException,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WhenUserNotFoundForEmail_LogsErrorButReturnsSuccess(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName,\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))\n            .Returns(true);\n\n        // Return null when retrieving user for email\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(user.Id)\n            .Returns((User)null!);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert - side effects are fire-and-forget, so command returns success even if user not found for email\n        Assert.True(result.IsSuccess);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WhenDeleteDeviceRegistrationFails_LogsErrorButReturnsSuccess(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName,\n        Device device)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        device.UserId = user.Id;\n        device.PushToken = \"test-push-token\";\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName,\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))\n            .Returns(true);\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns(new List<Device> { device });\n\n        var deviceException = new Exception(\"Device registration deletion failed\");\n        sutProvider.GetDependency<IPushRegistrationService>()\n            .DeleteUserRegistrationOrganizationAsync(Arg.Any<IEnumerable<string>>(), organization.Id.ToString())\n            .ThrowsAsync(deviceException);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert - side effects are fire-and-forget, so command returns success even if device registration deletion fails\n        Assert.True(result.IsSuccess);\n\n        sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Error,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString()!.Contains(\"Failed to delete device registration\")),\n                deviceException,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WhenPushSyncOrgKeysFails_LogsErrorButReturnsSuccess(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName,\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))\n            .Returns(true);\n\n        var pushException = new Exception(\"Push sync failed\");\n        sutProvider.GetDependency<IPushNotificationService>()\n            .PushSyncOrgKeysAsync(user.Id)\n            .ThrowsAsync(pushException);\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert - side effects are fire-and-forget, so command returns success even if push sync fails\n        Assert.True(result.IsSuccess);\n\n        sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Error,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString()!.Contains(\"Failed to push organization keys\")),\n                pushException,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AutomaticallyConfirmOrganizationUserAsync_WithDevicesWithoutPushToken_FiltersCorrectly(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,\n        User user,\n        Guid performingUserId,\n        string key,\n        string defaultCollectionName,\n        Device deviceWithToken,\n        Device deviceWithoutToken)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organization.Id;\n        deviceWithToken.UserId = user.Id;\n        deviceWithToken.PushToken = \"test-token\";\n        deviceWithoutToken.UserId = user.Id;\n        deviceWithoutToken.PushToken = null;\n        var request = new AutomaticallyConfirmOrganizationUserRequest\n        {\n            OrganizationUserId = organizationUser.Id,\n            OrganizationId = organization.Id,\n            Key = key,\n            DefaultUserCollectionName = defaultCollectionName,\n            PerformedBy = new StandardUser(performingUserId, true)\n        };\n\n        SetupRepositoryMocks(sutProvider, organizationUser, organization, user);\n        SetupValidatorMock(sutProvider, request, organizationUser, organization, true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>\n                o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))\n            .Returns(true);\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns(new List<Device> { deviceWithToken, deviceWithoutToken });\n\n        // Act\n        var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);\n\n        // Assert\n        Assert.True(result.IsSuccess);\n\n        await sutProvider.GetDependency<IPushRegistrationService>()\n            .Received(1)\n            .DeleteUserRegistrationOrganizationAsync(\n                Arg.Is<IEnumerable<string>>(devices =>\n                    devices.Count(d => deviceWithToken.Id.ToString() == d) == 1),\n                organization.Id.ToString());\n    }\n\n    private static void SetupRepositoryMocks(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        OrganizationUser organizationUser,\n        Organization organization,\n        User user)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(user.Id)\n            .Returns(user);\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns(new List<Device>());\n    }\n\n    private static void SetupValidatorMock(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        AutomaticallyConfirmOrganizationUserRequest originalRequest,\n        OrganizationUser organizationUser,\n        Organization organization,\n        bool isValid,\n        Error? error = null)\n    {\n        var validationRequest = new AutomaticallyConfirmOrganizationUserValidationRequest\n        {\n            PerformedBy = originalRequest.PerformedBy,\n            DefaultUserCollectionName = originalRequest.DefaultUserCollectionName,\n            OrganizationUserId = originalRequest.OrganizationUserId,\n            OrganizationUser = organizationUser,\n            OrganizationId = originalRequest.OrganizationId,\n            Organization = organization,\n            Key = originalRequest.Key\n        };\n\n        var validationResult = isValid\n            ? ValidationResultHelpers.Valid(validationRequest)\n            : ValidationResultHelpers.Invalid(validationRequest, error ?? new UserIsNotAccepted());\n\n        sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUsersValidator>()\n            .ValidateAsync(Arg.Any<AutomaticallyConfirmOrganizationUserValidationRequest>())\n            .Returns(validationResult);\n    }\n\n    private static void SetupPolicyRequirementMock(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        Guid userId,\n        Guid organizationId,\n        bool requiresDefaultCollection)\n    {\n        var policyDetails = requiresDefaultCollection\n            ? new List<PolicyDetails> { new() { OrganizationId = organizationId } }\n            : new List<PolicyDetails>();\n\n        var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            requiresDefaultCollection ? OrganizationDataOwnershipState.Enabled : OrganizationDataOwnershipState.Disabled,\n            policyDetails);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)\n            .Returns(policyRequirement);\n    }\n\n    private static async Task AssertSuccessfulOperationsAsync(\n        SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,\n        OrganizationUser organizationUser,\n        Organization organization,\n        User user,\n        string key)\n    {\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(\n                Arg.Is<OrganizationUser>(x => x.Id == organizationUser.Id),\n                EventType.OrganizationUser_AutomaticallyConfirmed,\n                Arg.Any<DateTime?>());\n\n        await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()\n            .Received(1)\n            .SendConfirmationAsync(\n                organization,\n                user.Email,\n                organizationUser.AccessSecretsManager);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncOrgKeysAsync(user.Id);\n\n        await sutProvider.GetDependency<IPushRegistrationService>()\n            .Received(1)\n            .DeleteUserRegistrationOrganizationAsync(\n                Arg.Any<IEnumerable<string>>(),\n                organization.Id.ToString());\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class ConfirmOrganizationUserCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithInvalidStatus_ThrowsBadRequestException(OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser orgUser, string key,\n        SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n        Assert.Contains(\"User not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithWrongOrganization_ThrowsBadRequestException(OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, string key,\n        SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        organizationUserRepository.GetByIdAsync(orgUser.Id).Returns(orgUser);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(confirmingUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n        Assert.Contains(\"User not valid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task ConfirmUserAsync_ToFree_WithExistingAdminOrOwner_ThrowsBadRequestException(OrganizationUserType userType, Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n\n        org.PlanType = PlanType.Free;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        orgUser.Type = userType;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n        Assert.Contains(\"User can only be an admin of one free organization.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Custom, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.Custom, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.EnterpriseAnnually, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.EnterpriseMonthly, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.EnterpriseMonthly2019, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.FamiliesAnnually, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.FamiliesAnnually2025, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.FamiliesAnnually2025, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.FamiliesAnnually2019, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.TeamsAnnually, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.TeamsAnnually2020, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.TeamsAnnually2019, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.TeamsMonthly, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.TeamsMonthly2020, OrganizationUserType.Owner)]\n    [BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Admin)]\n    [BitAutoData(PlanType.TeamsMonthly2019, OrganizationUserType.Owner)]\n    public async Task ConfirmUserAsync_ToNonFree_WithExistingFreeAdminOrOwner_Succeeds(PlanType planType, OrganizationUserType orgUserType, Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n\n        var device = new Device() { Id = Guid.NewGuid(), UserId = user.Id, PushToken = \"pushToken\", Identifier = \"identifier\" };\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns([device]);\n\n        org.PlanType = planType;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        orgUser.Type = orgUserType;\n        orgUser.AccessSecretsManager = false;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(orgUser.UserId.Value).Returns(1);\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);\n\n        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);\n        await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>().Received(1).SendConfirmationAsync(org, user.Email, orgUser.AccessSecretsManager);\n        await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));\n        await sutProvider.GetDependency<IPushRegistrationService>()\n            .Received(1)\n            .DeleteUserRegistrationOrganizationAsync(\n                Arg.Is<IEnumerable<string>>(ids => ids.Contains(device.Id.ToString()) && ids.Count() == 1),\n                org.Id.ToString());\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncOrgKeysAsync(user.Id);\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithTwoFactorPolicyAndTwoFactorDisabled_ThrowsBadRequestException(Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        OrganizationUser orgUserAnotherOrg,\n        [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var policyService = sutProvider.GetDependency<IPolicyService>();\n        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();\n\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUserAnotherOrg });\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n        twoFactorPolicy.OrganizationId = org.Id;\n        policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });\n        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n        Assert.Contains(\"User does not have two-step login enabled.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithTwoFactorPolicyAndTwoFactorEnabled_Succeeds(Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        [OrganizationUserPolicyDetails(PolicyType.TwoFactorAuthentication)] OrganizationUserPolicyDetails twoFactorPolicy,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var policyService = sutProvider.GetDependency<IPolicyService>();\n        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();\n\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n        twoFactorPolicy.OrganizationId = org.Id;\n        policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication).Returns(new[] { twoFactorPolicy });\n        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithSingleOrgPolicyFromConfirmingOrg_ThrowsBadRequest(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        OrganizationUser orgUserAnotherOrg,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.Status = OrganizationUserStatusType.Accepted;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUser, orgUserAnotherOrg });\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        // 2FA check passes (no 2FA policy)\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement([]));\n\n        // Confirming org has SingleOrg policy, user is a regular User (not exempt)\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForTargetOrganization(org.Id));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n        Assert.Contains($\"{user.Email} cannot be confirmed until they leave or remove all other organizations.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithSingleOrgPolicyFromOtherOrg_ThrowsBadRequest(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        OrganizationUser orgUserAnotherOrg,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.Status = OrganizationUserStatusType.Accepted;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = orgUserAnotherOrg.UserId = user.Id;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUser, orgUserAnotherOrg });\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        // 2FA check passes (no 2FA policy)\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement([]));\n\n        // Other org has SingleOrg policy (not the confirming org)\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForAnotherOrganization());\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n        Assert.Contains($\"{user.Email} cannot be confirmed because they are in another organization which forbids it.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_NoSingleOrgPolicy_Succeeds(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        // No SingleOrg policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        // No 2FA policy either\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement([]));\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });\n\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);\n\n        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorRequired_ThrowsBadRequestException(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var featureService = sutProvider.GetDependency<IFeatureService>();\n        var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();\n        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();\n\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n        featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);\n        policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = org.Id,\n                    OrganizationUserStatus = OrganizationUserStatusType.Accepted,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]));\n        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, \"key\", confirmingUser.Id));\n        Assert.Contains(\"User does not have two-step login enabled.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorNotRequired_Succeeds(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var featureService = sutProvider.GetDependency<IFeatureService>();\n        var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();\n        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();\n\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n        featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);\n        policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = Guid.NewGuid(),\n                    OrganizationUserStatus = OrganizationUserStatusType.Invited,\n                    PolicyType = PolicyType.TwoFactorAuthentication,\n                }\n            ]));\n        policyRequirementQuery.GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });\n\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, \"key\", confirmingUser.Id);\n\n        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);\n        await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>().Received(1).SendConfirmationAsync(org, user.Email, orgUser.AccessSecretsManager);\n        await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorEnabled_Succeeds(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var featureService = sutProvider.GetDependency<IFeatureService>();\n        var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();\n        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();\n\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationUserRepository.GetManyByManyUsersAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        organizationRepository.GetByIdAsync(org.Id).Returns(org);\n        userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n        featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);\n        policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = org.Id,\n                    OrganizationUserStatus = OrganizationUserStatusType.Accepted,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]));\n        policyRequirementQuery.GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n        twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });\n\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, \"key\", confirmingUser.Id);\n\n        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);\n        await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>().Received(1).SendConfirmationAsync(org, user.Email, orgUser.AccessSecretsManager);\n        await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection(\n        Organization organization, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = organization.Id;\n        orgUser.UserId = user.Id;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            OrganizationUserId = orgUser.Id,\n            IsProvider = false,\n            OrganizationUserStatus = orgUser.Status,\n            OrganizationUserType = orgUser.Type,\n            PolicyType = PolicyType.OrganizationDataOwnership\n        };\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.UserId!.Value)))\n            .Returns([(orgUser.UserId!.Value, new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails]))]);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser.Id),\n                collectionName);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, \"\");\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted, OrganizationUserType.Owner)] OrganizationUser orgUser, User user,\n        string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.UserId!.Value)))\n            .Returns([(orgUser.UserId!.Value, new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, []))]);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(orgUser.UserId!.Value)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithAutoConfirmEnabledAndUserBelongsToAnotherOrg_ThrowsBadRequest(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        OrganizationUser otherOrgUser, string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        otherOrgUser.UserId = user.Id;\n        otherOrgUser.OrganizationId = Guid.NewGuid(); // Different org\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync([]).ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync([])\n            .ReturnsForAnyArgs([orgUser, otherOrgUser]);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Invalid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(orgUser.Id, [orgUser, otherOrgUser], user),\n                new UserCannotBelongToAnotherOrganization()));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n\n        Assert.Equal(new UserCannotBelongToAnotherOrganization().Message, exception.Message);\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithAutoConfirmEnabledForOtherOrg_ThrowsBadRequest(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        OrganizationUser otherOrgUser, string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        otherOrgUser.UserId = user.Id;\n        otherOrgUser.OrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync([]).ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync([])\n            .ReturnsForAnyArgs([orgUser, otherOrgUser]);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Invalid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser, otherOrgUser], user),\n                new OtherOrganizationDoesNotAllowOtherMembership()));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n\n        Assert.Equal(new OtherOrganizationDoesNotAllowOtherMembership().Message, exception.Message);\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithAutoConfirmEnabledAndUserIsProvider_ThrowsBadRequest(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync([]).ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync([])\n            .ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Invalid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user),\n                new ProviderUsersCannotJoin()));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n\n        Assert.Equal(new ProviderUsersCannotJoin().Message, exception.Message);\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithAutoConfirmNotApplicable_Succeeds(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync([]).ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync([])\n            .ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Act\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);\n\n        // Assert\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);\n        await sutProvider.GetDependency<ISendOrganizationConfirmationCommand>()\n            .Received(1).SendConfirmationAsync(org, user.Email, orgUser.AccessSecretsManager);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithAutoConfirmPolicyEnabled_DeletesEmergencyAccess(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync([]).ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync([])\n            .ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));\n\n        // Act\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .Received(1)\n            .DeleteAllByUserIdAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithAutoConfirmPolicyNotEnabled_DoesNotDeleteEmergencyAccess(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync([]).ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync([])\n            .ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([]));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser], user)));\n\n        // Act\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_WithAutoConfirmValidationBeforeSingleOrgPolicy_ChecksAutoConfirmFirst(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        OrganizationUser otherOrgUser,\n        [OrganizationUserPolicyDetails(PolicyType.SingleOrg)] OrganizationUserPolicyDetails singleOrgPolicy,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange - Setup conditions that would fail BOTH auto-confirm AND single org policy\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser.UserId = user.Id;\n        otherOrgUser.UserId = user.Id;\n        otherOrgUser.OrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync([]).ReturnsForAnyArgs([orgUser]);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync([])\n            .ReturnsForAnyArgs([orgUser, otherOrgUser]);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync([]).ReturnsForAnyArgs([user]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        singleOrgPolicy.OrganizationId = org.Id;\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)\n            .Returns([singleOrgPolicy]);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Invalid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser, otherOrgUser], user),\n                new UserCannotBelongToAnotherOrganization()));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id));\n\n        Assert.Equal(new UserCannotBelongToAnotherOrganization().Message, exception.Message);\n        Assert.NotEqual(\"Cannot confirm this member to the organization until they leave or remove all other organizations.\",\n            exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUsersAsync_WithAutoConfirmEnabled_MixedResults(\n        Organization org, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser3,\n        OrganizationUser otherOrgUser, User user1, User user2, User user3,\n        string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = confirmingUser.OrganizationId = org.Id;\n        orgUser1.UserId = user1.Id;\n        orgUser2.UserId = user2.Id;\n        orgUser3.UserId = user3.Id;\n        otherOrgUser.UserId = user3.Id;\n        otherOrgUser.OrganizationId = Guid.NewGuid();\n\n        var orgUsers = new[] { orgUser1, orgUser2, orgUser3 };\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync([]).ReturnsForAnyArgs(orgUsers);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync([]).ReturnsForAnyArgs([user1, user2, user3]);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync([])\n            .ReturnsForAnyArgs([orgUser1, orgUser2, orgUser3, otherOrgUser]);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(Arg.Any<Guid>())\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = org.Id }]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Is<AutomaticUserConfirmationPolicyEnforcementRequest>(r => r.User.Id == user1.Id), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser1], user1)));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Is<AutomaticUserConfirmationPolicyEnforcementRequest>(r => r.User.Id == user2.Id), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser2], user2)));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Is<AutomaticUserConfirmationPolicyEnforcementRequest>(r => r.User.Id == user3.Id), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Invalid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(org.Id, [orgUser3, otherOrgUser], user3),\n                new OtherOrganizationDoesNotAllowOtherMembership()));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(Arg.Any<Guid>())\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        var keys = orgUsers.ToDictionary(ou => ou.Id, _ => key);\n\n        // Act\n        var result = await sutProvider.Sut.ConfirmUsersAsync(confirmingUser.OrganizationId, keys, confirmingUser.Id);\n\n        // Assert\n        Assert.Equal(3, result.Count);\n        Assert.Empty(result[0].Item2);\n        Assert.Empty(result[1].Item2);\n        Assert.Equal(new OtherOrganizationDoesNotAllowOtherMembership().Message, result[2].Item2);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_UseMyItemsDisabled_DoesNotCreateDefaultCollection(\n        Organization organization, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        organization.UseMyItems = false;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = organization.Id;\n        orgUser.UserId = user.Id;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            OrganizationUserId = orgUser.Id,\n            IsProvider = false,\n            OrganizationUserStatus = orgUser.Status,\n            OrganizationUserType = orgUser.Type,\n            PolicyType = PolicyType.OrganizationDataOwnership\n        };\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails]));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(orgUser.UserId!.Value)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Act\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);\n\n        // Assert - Collection repository should NOT be called\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_UseMyItemsEnabled_CreatesDefaultCollection(\n        Organization organization, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,\n        string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        organization.UseMyItems = true;\n        orgUser.OrganizationId = confirmingUser.OrganizationId = organization.Id;\n        orgUser.UserId = user.Id;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });\n\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            OrganizationUserId = orgUser.Id,\n            IsProvider = false,\n            OrganizationUserStatus = orgUser.Status,\n            OrganizationUserType = orgUser.Type,\n            PolicyType = PolicyType.OrganizationDataOwnership\n        };\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser.UserId!.Value)))\n            .Returns([\n                (orgUser.UserId!.Value, new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails]))\n            ]);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(orgUser.UserId!.Value)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Act\n        await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);\n\n        // Assert - Collection repository should be called\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser.Id),\n                collectionName);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUsersAsync_UseMyItemsDisabled_DoesNotCreateDefaultCollections(\n        Organization organization, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,\n        User user1, User user2, string key1, string key2, string collectionName,\n        SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        organization.UseMyItems = false;\n        orgUser1.OrganizationId = confirmingUser.OrganizationId = organization.Id;\n        orgUser2.OrganizationId = organization.Id;\n        orgUser1.UserId = user1.Id;\n        orgUser2.UserId = user2.Id;\n\n        var keys = new Dictionary<Guid, string>\n        {\n            { orgUser1.Id, key1 },\n            { orgUser2.Id, key2 }\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser1, orgUser2 });\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2 });\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(Arg.Any<Guid>())\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Act\n        await sutProvider.Sut.ConfirmUsersAsync(organization.Id, keys, confirmingUser.Id, collectionName);\n\n        // Assert - Collection repository should NOT be called\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUsersAsync_UseMyItemsEnabled_CreatesDefaultCollections(\n        Organization organization, OrganizationUser confirmingUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2,\n        User user1, User user2, string key1, string key2, string collectionName,\n        SutProvider<ConfirmOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        organization.UseMyItems = true;\n        orgUser1.OrganizationId = confirmingUser.OrganizationId = organization.Id;\n        orgUser2.OrganizationId = organization.Id;\n        orgUser1.UserId = user1.Id;\n        orgUser2.UserId = user2.Id;\n\n        var keys = new Dictionary<Guid, string>\n        {\n            { orgUser1.Id, key1 },\n            { orgUser2.Id, key2 }\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser1, orgUser2 });\n        sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user1, user2 });\n\n        var policyDetails1 = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            OrganizationUserId = orgUser1.Id,\n            IsProvider = false,\n            OrganizationUserStatus = orgUser1.Status,\n            OrganizationUserType = orgUser1.Type,\n            PolicyType = PolicyType.OrganizationDataOwnership\n        };\n        var policyDetails2 = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            OrganizationUserId = orgUser2.Id,\n            IsProvider = false,\n            OrganizationUserStatus = orgUser2.Status,\n            OrganizationUserType = orgUser2.Type,\n            PolicyType = PolicyType.OrganizationDataOwnership\n        };\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))\n            .Returns([\n                (orgUser1.UserId!.Value, new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails1])),\n                (orgUser2.UserId!.Value, new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails2]))\n            ]);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(Arg.Any<Guid>())\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Act\n        await sutProvider.Sut.ConfirmUsersAsync(organization.Id, keys, confirmingUser.Id, collectionName);\n\n        // Assert - Collection repository should be called with correct parameters\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 2 && ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)),\n                collectionName);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/CountNewSmSeatsRequiredQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class CountNewSmSeatsRequiredQueryTests\n{\n    [Theory]\n    [BitAutoData(2, 5, 2, 0)]\n    [BitAutoData(0, 5, 2, 0)]\n    [BitAutoData(6, 5, 2, 3)]\n    [BitAutoData(2, 5, 10, 7)]\n    public async Task CountNewSmSeatsRequiredAsync_ReturnsCorrectCount(\n        int usersToAdd,\n        int organizationSmSeats,\n        int currentOccupiedSmSeats,\n        int expectedNewSmSeatsRequired,\n        Organization organization,\n        SutProvider<CountNewSmSeatsRequiredQuery> sutProvider)\n    {\n        organization.UseSecretsManager = true;\n        organization.SmSeats = organizationSmSeats;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(currentOccupiedSmSeats);\n\n        var result = await sutProvider.Sut.CountNewSmSeatsRequiredAsync(organization.Id, usersToAdd);\n\n        Assert.Equal(expectedNewSmSeatsRequired, result);\n    }\n\n    [Theory]\n    [BitAutoData(0)]\n    [BitAutoData(5)]\n    public async Task CountNewSmSeatsRequiredAsync_WithNullSmSeats_ReturnsZero(\n        int usersToAdd,\n        Organization organization,\n        SutProvider<CountNewSmSeatsRequiredQuery> sutProvider)\n    {\n        const int expected = 0;\n\n        organization.UseSecretsManager = true;\n        organization.SmSeats = null;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var result = await sutProvider.Sut.CountNewSmSeatsRequiredAsync(organization.Id, usersToAdd);\n\n        Assert.Equal(expected, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CountNewSmSeatsRequiredAsync_WithNonExistentOrganizationId_ThrowsNotFound(\n        Guid organizationId, int usersToAdd,\n        SutProvider<CountNewSmSeatsRequiredQuery> sutProvider)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CountNewSmSeatsRequiredAsync(organizationId, usersToAdd));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CountNewSmSeatsRequiredAsync_WithOrganizationUseSecretsManagerFalse_ThrowsNotFound(\n        Organization organization, int usersToAdd,\n        SutProvider<CountNewSmSeatsRequiredQuery> sutProvider)\n    {\n        organization.UseSecretsManager = false;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.CountNewSmSeatsRequiredAsync(organization.Id, usersToAdd));\n        Assert.Contains(\"Organization does not use Secrets Manager\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Utilities.v2;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;\n\n[SutProviderCustomize]\npublic class DeleteClaimedOrganizationUserAccountCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteUserAsync_WithValidSingleUser_CallsDeleteManyUsersAsync(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organizationId;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n        var validationResult = CreateSuccessfulValidationResult(request);\n\n        SetupRepositoryMocks(sutProvider,\n            new List<OrganizationUser> { organizationUser },\n            [user],\n            organizationId,\n            new Dictionary<Guid, bool> { { organizationUser.Id, true } });\n\n        SetupValidatorMock(sutProvider, [validationResult]);\n\n        var result = await sutProvider.Sut.DeleteUserAsync(organizationId, organizationUser.Id, deletingUserId);\n\n        Assert.Equal(organizationUser.Id, result.Id);\n        Assert.True(result.Result.IsSuccess);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)));\n\n        await AssertSuccessfulUserOperations(sutProvider, [user], [organizationUser]);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteManyUsersAsync_WithEmptyUserIds_ReturnsEmptyResults(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        Guid organizationId,\n        Guid deletingUserId)\n    {\n        var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [], deletingUserId);\n\n        Assert.Empty(results);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        User user1,\n        User user2,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser orgUser1,\n        [OrganizationUser] OrganizationUser orgUser2)\n    {\n        // Arrange\n        orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;\n        orgUser1.UserId = user1.Id;\n        orgUser2.UserId = user2.Id;\n\n        var request1 = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = orgUser1.Id,\n            OrganizationUser = orgUser1,\n            User = user1,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n        var request2 = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = orgUser2.Id,\n            OrganizationUser = orgUser2,\n            User = user2,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        var validationResults = new[]\n        {\n            CreateSuccessfulValidationResult(request1),\n            CreateSuccessfulValidationResult(request2)\n        };\n\n        SetupRepositoryMocks(sutProvider,\n            new List<OrganizationUser> { orgUser1, orgUser2 },\n            [user1, user2],\n            organizationId,\n            new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, true } });\n\n        SetupValidatorMock(sutProvider, validationResults);\n\n        var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser1.Id, orgUser2.Id], deletingUserId);\n\n        var resultsList = results.ToList();\n        Assert.Equal(2, resultsList.Count);\n        Assert.All(resultsList, result => Assert.True(result.Result.IsSuccess));\n\n        await AssertSuccessfulUserOperations(sutProvider, [user1, user2], [orgUser1, orgUser2]);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteManyUsersAsync_WithValidationErrors_ReturnsErrorResults(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        Guid organizationId,\n        Guid orgUserId1,\n        Guid orgUserId2,\n        Guid deletingUserId)\n    {\n        // Arrange\n        var request1 = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = orgUserId1,\n            DeletingUserId = deletingUserId\n        };\n        var request2 = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = orgUserId2,\n            DeletingUserId = deletingUserId\n        };\n\n        var validationResults = new[]\n        {\n            CreateFailedValidationResult(request1, new UserNotClaimedError()),\n            CreateFailedValidationResult(request2, new InvalidUserStatusError())\n        };\n\n        SetupRepositoryMocks(sutProvider, [], [], organizationId, new Dictionary<Guid, bool>());\n        SetupValidatorMock(sutProvider, validationResults);\n\n        var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserId1, orgUserId2], deletingUserId);\n\n        var resultsList = results.ToList();\n        Assert.Equal(2, resultsList.Count);\n\n        Assert.Equal(orgUserId1, resultsList[0].Id);\n        Assert.True(resultsList[0].Result.IsError);\n        Assert.IsType<UserNotClaimedError>(resultsList[0].Result.AsError);\n\n        Assert.Equal(orgUserId2, resultsList[1].Id);\n        Assert.True(resultsList[1].Result.IsError);\n        Assert.IsType<InvalidUserStatusError>(resultsList[1].Result.AsError);\n\n        await AssertNoUserOperations(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteManyUsersAsync_WithMixedValidationResults_HandlesPartialSuccessCorrectly(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        User validUser,\n        Guid organizationId,\n        Guid validOrgUserId,\n        Guid invalidOrgUserId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser validOrgUser)\n    {\n        validOrgUser.Id = validOrgUserId;\n        validOrgUser.UserId = validUser.Id;\n        validOrgUser.OrganizationId = organizationId;\n\n        var validRequest = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = validOrgUserId,\n            OrganizationUser = validOrgUser,\n            User = validUser,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n        var invalidRequest = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = invalidOrgUserId,\n            DeletingUserId = deletingUserId\n        };\n\n        var validationResults = new[]\n        {\n            CreateSuccessfulValidationResult(validRequest),\n            CreateFailedValidationResult(invalidRequest, new UserNotFoundError())\n        };\n\n        SetupRepositoryMocks(sutProvider,\n            new List<OrganizationUser> { validOrgUser },\n            [validUser],\n            organizationId,\n            new Dictionary<Guid, bool> { { validOrgUserId, true } });\n\n        SetupValidatorMock(sutProvider, validationResults);\n\n        var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [validOrgUserId, invalidOrgUserId], deletingUserId);\n\n        var resultsList = results.ToList();\n        Assert.Equal(2, resultsList.Count);\n\n        var validResult = resultsList.First(r => r.Id == validOrgUserId);\n        var invalidResult = resultsList.First(r => r.Id == invalidOrgUserId);\n\n        Assert.True(validResult.Result.IsSuccess);\n        Assert.True(invalidResult.Result.IsError);\n        Assert.IsType<UserNotFoundError>(invalidResult.Result.AsError);\n\n        await AssertSuccessfulUserOperations(sutProvider, [validUser], [validOrgUser]);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task DeleteManyUsersAsync_CancelPremiumsAsync_HandlesGatewayExceptionAndLogsWarning(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser orgUser)\n    {\n        orgUser.UserId = user.Id;\n        orgUser.OrganizationId = organizationId;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = orgUser.Id,\n            OrganizationUser = orgUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n        var validationResult = CreateSuccessfulValidationResult(request);\n\n        SetupRepositoryMocks(sutProvider,\n            new List<OrganizationUser> { orgUser },\n            [user],\n            organizationId,\n            new Dictionary<Guid, bool> { { orgUser.Id, true } });\n\n        SetupValidatorMock(sutProvider, [validationResult]);\n\n        var gatewayException = new GatewayException(\"Payment gateway error\");\n        sutProvider.GetDependency<IUserService>()\n            .CancelPremiumAsync(user)\n            .ThrowsAsync(gatewayException);\n\n        var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser.Id], deletingUserId);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList.First().Result.IsSuccess);\n\n        await sutProvider.GetDependency<IUserService>().Received(1).CancelPremiumAsync(user);\n        await AssertSuccessfulUserOperations(sutProvider, [user], [orgUser]);\n\n        sutProvider.GetDependency<ILogger<DeleteClaimedOrganizationUserAccountCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Warning,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString()!.Contains($\"Failed to cancel premium subscription for {user.Id}\")),\n                gatewayException,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateInternalRequests_CreatesCorrectRequestsForAllUsers(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        User user1,\n        User user2,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser orgUser1,\n        [OrganizationUser] OrganizationUser orgUser2)\n    {\n        orgUser1.UserId = user1.Id;\n        orgUser2.UserId = user2.Id;\n        var orgUserIds = new[] { orgUser1.Id, orgUser2.Id };\n        var orgUsers = new List<OrganizationUser> { orgUser1, orgUser2 };\n        var users = new[] { user1, user2 };\n        var claimedStatuses = new Dictionary<Guid, bool> { { orgUser1.Id, true }, { orgUser2.Id, false } };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(orgUsers);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id)))\n            .Returns(users);\n\n        sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(claimedStatuses);\n\n        sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()\n            .ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())\n            .Returns(callInfo =>\n            {\n                var requests = callInfo.Arg<IEnumerable<DeleteUserValidationRequest>>();\n                return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError()));\n            });\n\n        // Act\n        await sutProvider.Sut.DeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()\n            .Received(1)\n            .ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>\n                requests.Count() == 2 &&\n                requests.Any(r => r.OrganizationUserId == orgUser1.Id &&\n                                  r.OrganizationId == organizationId &&\n                                  r.OrganizationUser == orgUser1 &&\n                                  r.User == user1 &&\n                                  r.DeletingUserId == deletingUserId &&\n                                  r.IsClaimed == true) &&\n                requests.Any(r => r.OrganizationUserId == orgUser2.Id &&\n                                  r.OrganizationId == organizationId &&\n                                  r.OrganizationUser == orgUser2 &&\n                                  r.User == user2 &&\n                                  r.DeletingUserId == deletingUserId &&\n                                  r.IsClaimed == false)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetUsersAsync_WithNullUserIds_ReturnsEmptyCollection(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser orgUserWithoutUserId)\n    {\n        orgUserWithoutUserId.UserId = null; // Intentionally setting to null for test case\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<OrganizationUser> { orgUserWithoutUserId });\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()))\n            .Returns([]);\n\n        sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()\n            .ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())\n            .Returns(callInfo =>\n            {\n                var requests = callInfo.Arg<IEnumerable<DeleteUserValidationRequest>>();\n                return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError()));\n            });\n\n        // Act\n        await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserWithoutUserId.Id], deletingUserId);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()\n            .Received(1)\n            .ValidateAsync(Arg.Is<IEnumerable<DeleteUserValidationRequest>>(requests =>\n                requests.Count() == 1 &&\n                requests.Single().User == null));\n\n        await sutProvider.GetDependency<IUserRepository>().Received(1)\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()));\n    }\n\n    private static ValidationResult<DeleteUserValidationRequest> CreateSuccessfulValidationResult(\n        DeleteUserValidationRequest request) =>\n        ValidationResultHelpers.Valid(request);\n\n    private static ValidationResult<DeleteUserValidationRequest> CreateFailedValidationResult(\n        DeleteUserValidationRequest request,\n        Error error) =>\n        ValidationResultHelpers.Invalid(request, error);\n\n    private static void SetupRepositoryMocks(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        ICollection<OrganizationUser> orgUsers,\n        IEnumerable<User> users,\n        Guid organizationId,\n        Dictionary<Guid, bool> claimedStatuses)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(orgUsers);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(users);\n\n        sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(claimedStatuses);\n    }\n\n    private static void SetupValidatorMock(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        IEnumerable<ValidationResult<DeleteUserValidationRequest>> validationResults)\n    {\n        sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountValidator>()\n            .ValidateAsync(Arg.Any<IEnumerable<DeleteUserValidationRequest>>())\n            .Returns(validationResults);\n    }\n\n    private static async Task AssertSuccessfulUserOperations(\n        SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider,\n        IEnumerable<User> expectedUsers,\n        IEnumerable<OrganizationUser> expectedOrgUsers)\n    {\n        var userList = expectedUsers.ToList();\n        var orgUserList = expectedOrgUsers.ToList();\n\n        await sutProvider.GetDependency<IUserRepository>().Received(1)\n            .DeleteManyAsync(Arg.Is<IEnumerable<User>>(users =>\n                userList.All(expectedUser => users.Any(u => u.Id == expectedUser.Id))));\n\n        foreach (var user in userList)\n        {\n            await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(user.Id);\n        }\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events =>\n                orgUserList.All(expectedOrgUser =>\n                    events.Any(e => e.Item1.Id == expectedOrgUser.Id && e.Item2 == EventType.OrganizationUser_Deleted))));\n    }\n\n    private static async Task AssertNoUserOperations(SutProvider<DeleteClaimedOrganizationUserAccountCommand> sutProvider)\n    {\n        await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().DeleteManyAsync(default);\n        await sutProvider.GetDependency<IPushNotificationService>().DidNotReceiveWithAnyArgs().PushLogOutAsync(default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventsAsync(default(IEnumerable<(OrganizationUser, EventType, DateTime?)>));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;\n\n[SutProviderCustomize]\npublic class DeleteClaimedOrganizationUserAccountValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithValidSingleRequest_ReturnsValidResult(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n        organizationUser.OrganizationId = organizationId;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        SetupMocks(sutProvider, organizationId, user.Id);\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsValid);\n        Assert.Equal(request, resultsList[0].Request);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithMultipleValidRequests_ReturnsAllValidResults(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user1,\n        User user2,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2)\n    {\n        orgUser1.UserId = user1.Id;\n        orgUser1.OrganizationId = organizationId;\n\n        orgUser2.UserId = user2.Id;\n        orgUser2.OrganizationId = organizationId;\n\n        var request1 = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = orgUser1.Id,\n            OrganizationUser = orgUser1,\n            User = user1,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        var request2 = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = orgUser2.Id,\n            OrganizationUser = orgUser2,\n            User = user2,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        SetupMocks(sutProvider, organizationId, user1.Id);\n        SetupMocks(sutProvider, organizationId, user2.Id);\n\n        var results = await sutProvider.Sut.ValidateAsync([request1, request2]);\n\n        var resultsList = results.ToList();\n        Assert.Equal(2, resultsList.Count);\n        Assert.All(resultsList, result => Assert.True(result.IsValid));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithNullUser_ReturnsUserNotFoundError(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser organizationUser)\n    {\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = null,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsError);\n        Assert.IsType<UserNotFoundError>(resultsList[0].AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId)\n    {\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = Guid.NewGuid(),\n            OrganizationUser = null,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsError);\n        Assert.IsType<UserNotFoundError>(resultsList[0].AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithInvitedUser_ReturnsInvalidUserStatusError(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsError);\n        Assert.IsType<InvalidUserStatusError>(resultsList[0].AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WhenDeletingYourself_ReturnsCannotDeleteYourselfError(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        [OrganizationUser] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = user.Id,\n            IsClaimed = true\n        };\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsError);\n        Assert.IsType<CannotDeleteYourselfError>(resultsList[0].AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithUnclaimedUser_ReturnsUserNotClaimedError(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = false\n        };\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsError);\n        Assert.IsType<UserNotClaimedError>(resultsList[0].AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsNotOwner_ReturnsCannotDeleteOwnersError(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin);\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsError);\n        Assert.IsType<CannotDeleteOwnersError>(resultsList[0].AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsOwner_ReturnsValidResult(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        SetupMocks(sutProvider, organizationId, user.Id);\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithSoleOwnerOfOrganization_ReturnsSoleOwnerError(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        SetupMocks(sutProvider, organizationId, user.Id);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByOnlyOwnerAsync(user.Id)\n            .Returns(1);\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsError);\n        Assert.IsType<SoleOwnerError>(resultsList[0].AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithSoleProviderOwner_ReturnsSoleProviderError(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        SetupMocks(sutProvider, organizationId, user.Id);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetCountByOnlyOwnerAsync(user.Id)\n            .Returns(1);\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsError);\n        Assert.IsType<SoleProviderError>(resultsList[0].AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_CustomUserDeletingAdmin_ReturnsCannotDeleteAdminsError(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Custom);\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsError);\n        Assert.IsType<CannotDeleteAdminsError>(resultsList[0].AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_AdminDeletingAdmin_ReturnsValidResult(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User user,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser)\n    {\n        organizationUser.UserId = user.Id;\n\n        var request = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = organizationUser.Id,\n            OrganizationUser = organizationUser,\n            User = user,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin);\n\n        var results = await sutProvider.Sut.ValidateAsync([request]);\n\n        var resultsList = results.ToList();\n        Assert.Single(resultsList);\n        Assert.True(resultsList[0].IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithMixedValidAndInvalidRequests_ReturnsCorrespondingResults(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        User validUser,\n        User invalidUser,\n        Guid organizationId,\n        Guid deletingUserId,\n        [OrganizationUser] OrganizationUser validOrgUser,\n        [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser invalidOrgUser)\n    {\n        validOrgUser.UserId = validUser.Id;\n\n        invalidOrgUser.UserId = invalidUser.Id;\n\n        var validRequest = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = validOrgUser.Id,\n            OrganizationUser = validOrgUser,\n            User = validUser,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        var invalidRequest = new DeleteUserValidationRequest\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = invalidOrgUser.Id,\n            OrganizationUser = invalidOrgUser,\n            User = invalidUser,\n            DeletingUserId = deletingUserId,\n            IsClaimed = true\n        };\n\n        SetupMocks(sutProvider, organizationId, validUser.Id);\n\n        var results = await sutProvider.Sut.ValidateAsync([validRequest, invalidRequest]);\n\n        var resultsList = results.ToList();\n        Assert.Equal(2, resultsList.Count);\n\n        var validResult = resultsList.First(r => r.Request == validRequest);\n        var invalidResult = resultsList.First(r => r.Request == invalidRequest);\n\n        Assert.True(validResult.IsValid);\n        Assert.True(invalidResult.IsError);\n        Assert.IsType<InvalidUserStatusError>(invalidResult.AsError);\n    }\n\n    private static void SetupMocks(\n        SutProvider<DeleteClaimedOrganizationUserAccountValidator> sutProvider,\n        Guid organizationId,\n        Guid userId,\n        OrganizationUserType currentUserType = OrganizationUserType.Owner)\n    {\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organizationId)\n            .Returns(currentUserType == OrganizationUserType.Owner);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationAdmin(organizationId)\n            .Returns(currentUserType is OrganizationUserType.Owner or OrganizationUserType.Admin);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationCustom(organizationId)\n            .Returns(currentUserType is OrganizationUserType.Custom);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByOnlyOwnerAsync(userId)\n            .Returns(0);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetCountByOnlyOwnerAsync(userId)\n            .Returns(0);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class GetOrganizationUsersClaimedStatusQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task GetUsersOrganizationManagementStatusAsync_WithNoUsers_ReturnsEmpty(\n        Organization organization,\n        SutProvider<GetOrganizationUsersClaimedStatusQuery> sutProvider)\n    {\n        var result = await sutProvider.Sut.GetUsersOrganizationClaimedStatusAsync(organization.Id, new List<Guid>());\n\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetUsersOrganizationManagementStatusAsync_WithUseOrganizationDomainsEnabled_Success(\n        Organization organization,\n        ICollection<OrganizationUser> usersWithClaimedDomain,\n        SutProvider<GetOrganizationUsersClaimedStatusQuery> sutProvider)\n    {\n        organization.Enabled = true;\n        organization.UseOrganizationDomains = true;\n\n        var userIdWithoutClaimedDomain = Guid.NewGuid();\n        var userIdsToCheck = usersWithClaimedDomain.Select(u => u.Id).Concat(new List<Guid> { userIdWithoutClaimedDomain }).ToList();\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility(organization));\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationWithClaimedDomainsAsync(organization.Id)\n            .Returns(usersWithClaimedDomain);\n\n        var result = await sutProvider.Sut.GetUsersOrganizationClaimedStatusAsync(organization.Id, userIdsToCheck);\n\n        Assert.All(usersWithClaimedDomain, ou => Assert.True(result[ou.Id]));\n        Assert.False(result[userIdWithoutClaimedDomain]);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetUsersOrganizationManagementStatusAsync_WithUseOrganizationDomainsDisabled_ReturnsAllFalse(\n        Organization organization,\n        ICollection<OrganizationUser> usersWithClaimedDomain,\n        SutProvider<GetOrganizationUsersClaimedStatusQuery> sutProvider)\n    {\n        organization.Enabled = true;\n        organization.UseOrganizationDomains = false;\n\n        var userIdWithoutClaimedDomain = Guid.NewGuid();\n        var userIdsToCheck = usersWithClaimedDomain.Select(u => u.Id).Concat(new List<Guid> { userIdWithoutClaimedDomain }).ToList();\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility(organization));\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationWithClaimedDomainsAsync(organization.Id)\n            .Returns(usersWithClaimedDomain);\n\n        var result = await sutProvider.Sut.GetUsersOrganizationClaimedStatusAsync(organization.Id, userIdsToCheck);\n\n        Assert.All(result, r => Assert.False(r.Value));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetUsersOrganizationManagementStatusAsync_WithDisabledOrganization_ReturnsAllFalse(\n        Organization organization,\n        ICollection<OrganizationUser> usersWithClaimedDomain,\n        SutProvider<GetOrganizationUsersClaimedStatusQuery> sutProvider)\n    {\n        organization.Enabled = false;\n\n        var userIdWithoutClaimedDomain = Guid.NewGuid();\n        var userIdsToCheck = usersWithClaimedDomain.Select(u => u.Id).Concat(new List<Guid> { userIdWithoutClaimedDomain }).ToList();\n\n        sutProvider.GetDependency<IApplicationCacheService>()\n            .GetOrganizationAbilityAsync(organization.Id)\n            .Returns(new OrganizationAbility(organization));\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationWithClaimedDomainsAsync(organization.Id)\n            .Returns(usersWithClaimedDomain);\n\n        var result = await sutProvider.Sut.GetUsersOrganizationClaimedStatusAsync(organization.Id, userIdsToCheck);\n\n        Assert.All(result, r => Assert.False(r.Value));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/HasConfirmedOwnersExceptQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class HasConfirmedOwnersExceptQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task HasConfirmedOwnersExcept_WithConfirmedOwner_WithNoException_ReturnsTrue(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)\n            .Returns(new List<OrganizationUser> { owner });\n\n        var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List<Guid>(), true);\n\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasConfirmedOwnersExcept_ExcludingConfirmedOwner_ReturnsFalse(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)\n            .Returns(new List<OrganizationUser> { owner });\n\n        var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List<Guid> { owner.Id }, true);\n\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasConfirmedOwnersExcept_WithInvitedOwner_ReturnsFalse(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.Owner)] OrganizationUser owner,\n        SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)\n            .Returns(new List<OrganizationUser> { owner });\n\n        var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List<Guid>(), true);\n\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task HasConfirmedOwnersExcept_WithConfirmedProviderUser_IncludeProviderTrue_ReturnsTrue(\n        bool includeProvider,\n        Organization organization,\n        ProviderUser providerUser,\n        SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)\n    {\n        providerUser.Status = ProviderUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByOrganizationAsync(organization.Id, ProviderUserStatusType.Confirmed)\n            .Returns(new List<ProviderUser> { providerUser });\n\n        var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List<Guid>(), includeProvider);\n\n        Assert.Equal(includeProvider, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasConfirmedOwnersExceptAsync_WithConfirmedOwners_ReturnsTrue(\n        Guid organizationId,\n        IEnumerable<Guid> organizationUsersId,\n        ICollection<OrganizationUser> owners,\n        SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner)\n            .Returns(owners);\n\n        var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId);\n\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasConfirmedOwnersExceptAsync_WithConfirmedProviders_ReturnsTrue(\n        Guid organizationId,\n        IEnumerable<Guid> organizationUsersId,\n        ICollection<ProviderUser> providerUsers,\n        SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner)\n            .Returns(new List<OrganizationUser>());\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)\n            .Returns(providerUsers);\n\n        var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId);\n\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasConfirmedOwnersExceptAsync_WithNoConfirmedOwnersOrProviders_ReturnsFalse(\n        Guid organizationId,\n        IEnumerable<Guid> organizationUsersId,\n        SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner)\n            .Returns(new List<OrganizationUser>());\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)\n            .Returns(new List<ProviderUser>());\n\n        var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId);\n\n        Assert.False(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/BulkResendOrganizationInvitesCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\n[SutProviderCustomize]\npublic class BulkResendOrganizationInvitesCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task BulkResendInvitesAsync_ValidatesUsersAndSendsBatchInvite(\n        Organization organization,\n        OrganizationUser validUser1,\n        OrganizationUser validUser2,\n        OrganizationUser acceptedUser,\n        OrganizationUser wrongOrgUser,\n        SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)\n    {\n        validUser1.OrganizationId = organization.Id;\n        validUser1.Status = OrganizationUserStatusType.Invited;\n        validUser2.OrganizationId = organization.Id;\n        validUser2.Status = OrganizationUserStatusType.Invited;\n        acceptedUser.OrganizationId = organization.Id;\n        acceptedUser.Status = OrganizationUserStatusType.Accepted;\n        wrongOrgUser.OrganizationId = Guid.NewGuid();\n        wrongOrgUser.Status = OrganizationUserStatusType.Invited;\n\n        var users = new List<OrganizationUser> { validUser1, validUser2, acceptedUser, wrongOrgUser };\n        var userIds = users.Select(u => u.Id).ToList();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(userIds).Returns(users);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var result = (await sutProvider.Sut.BulkResendInvitesAsync(organization.Id, null, userIds)).ToList();\n\n        Assert.Equal(4, result.Count);\n        Assert.Equal(2, result.Count(r => string.IsNullOrEmpty(r.Item2)));\n        Assert.Equal(2, result.Count(r => r.Item2 == \"User invalid.\"));\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .Received(1)\n            .SendInvitesAsync(Arg.Is<SendInvitesRequest>(req =>\n                req.Organization == organization &&\n                req.Users.Length == 2 &&\n                req.InitOrganization == false));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkResendInvitesAsync_AllInvalidUsers_DoesNotSendInvites(\n        Organization organization,\n        List<OrganizationUser> organizationUsers,\n        SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)\n    {\n        foreach (var user in organizationUsers)\n        {\n            user.OrganizationId = organization.Id;\n            user.Status = OrganizationUserStatusType.Confirmed;\n        }\n\n        var userIds = organizationUsers.Select(u => u.Id).ToList();\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(userIds).Returns(organizationUsers);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var result = (await sutProvider.Sut.BulkResendInvitesAsync(organization.Id, null, userIds)).ToList();\n\n        Assert.Equal(organizationUsers.Count, result.Count);\n        Assert.All(result, r => Assert.Equal(\"User invalid.\", r.Item2));\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().DidNotReceive()\n            .SendInvitesAsync(Arg.Any<SendInvitesRequest>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkResendInvitesAsync_OrganizationNotFound_ThrowsNotFoundException(\n        Guid organizationId,\n        List<Guid> userIds,\n        List<OrganizationUser> organizationUsers,\n        SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(userIds).Returns(organizationUsers);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns((Organization?)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.BulkResendInvitesAsync(organizationId, null, userIds));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkResendInvitesAsync_EmptyUserList_ReturnsEmpty(\n        Organization organization,\n        SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)\n    {\n        var emptyUserIds = new List<Guid>();\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(emptyUserIds).Returns(new List<OrganizationUser>());\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var result = await sutProvider.Sut.BulkResendInvitesAsync(organization.Id, null, emptyUserIds);\n\n        Assert.Empty(result);\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().DidNotReceive()\n            .SendInvitesAsync(Arg.Any<SendInvitesRequest>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Helpers/InviteUserOrganizationValidationRequestHelpers.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\nusing Bit.Core.Models.Business;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers;\n\npublic static class InviteUserOrganizationValidationRequestHelpers\n{\n    public static InviteOrganizationUsersValidationRequest GetInviteValidationRequestMock(InviteOrganizationUsersRequest request,\n        InviteOrganization inviteOrganization, Organization organization) =>\n        new()\n        {\n            Invites = request.Invites,\n            InviteOrganization = inviteOrganization,\n            PerformedBy = Guid.Empty,\n            PerformedAt = request.PerformedAt,\n            OccupiedPmSeats = 0,\n            OccupiedSmSeats = 0,\n            PasswordManagerSubscriptionUpdate = new PasswordManagerSubscriptionUpdate(inviteOrganization, 0, 0),\n            SecretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true)\n                .AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager))\n        };\n\n    public static InviteOrganizationUsersValidationRequest WithPasswordManagerUpdate(this InviteOrganizationUsersValidationRequest request, PasswordManagerSubscriptionUpdate passwordManagerSubscriptionUpdate) =>\n        new()\n        {\n            Invites = request.Invites,\n            InviteOrganization = request.InviteOrganization,\n            PerformedBy = request.PerformedBy,\n            PerformedAt = request.PerformedAt,\n            OccupiedPmSeats = request.OccupiedPmSeats,\n            OccupiedSmSeats = request.OccupiedSmSeats,\n            PasswordManagerSubscriptionUpdate = passwordManagerSubscriptionUpdate,\n            SecretsManagerSubscriptionUpdate = request.SecretsManagerSubscriptionUpdate\n        };\n\n    public static InviteOrganizationUsersValidationRequest WithSecretsManagerUpdate(this InviteOrganizationUsersValidationRequest request, SecretsManagerSubscriptionUpdate secretsManagerSubscriptionUpdate) =>\n        new()\n        {\n            Invites = request.Invites,\n            InviteOrganization = request.InviteOrganization,\n            PerformedBy = request.PerformedBy,\n            PerformedAt = request.PerformedAt,\n            OccupiedPmSeats = request.OccupiedPmSeats,\n            OccupiedSmSeats = request.OccupiedSmSeats,\n            PasswordManagerSubscriptionUpdate = request.PasswordManagerSubscriptionUpdate,\n            SecretsManagerSubscriptionUpdate = secretsManagerSubscriptionUpdate\n        };\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs",
    "content": "﻿using System.Net.Mail;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Utilities.Commands;\nusing Bit.Core.AdminConsole.Utilities.Errors;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\nusing static Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers.InviteUserOrganizationValidationRequestHelpers;\nusing Enterprise2019Plan = Bit.Core.Test.Billing.Mocks.Plans.Enterprise2019Plan;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\n[SutProviderCustomize]\npublic class InviteOrganizationUserCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenEmailAlreadyExists_ThenNoInviteIsSentAndNoSeatsAreAdjusted(\n        MailAddress address,\n        Organization organization,\n        OrganizationUser user,\n        FakeTimeProvider timeProvider,\n        string externalId,\n        SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var request = new InviteOrganizationUsersRequest(\n            invites: [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .SelectKnownEmailsAsync(organization.Id, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([user.Email]);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)));\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Failure<ScimInviteOrganizationUsersResponse>>(result);\n        Assert.Equal(NoUsersToInviteError.Code, (result as Failure<ScimInviteOrganizationUsersResponse>)!.Error.Message);\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .SendInvitesAsync(Arg.Any<SendInvitesRequest>());\n\n        await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .UpdateSubscriptionAsync(Arg.Any<Core.Models.Business.SecretsManagerSubscriptionUpdate>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenEmailDoesNotExistAndRequestIsValid_ThenUserIsSavedAndInviteIsSent(\n            MailAddress address,\n            Organization organization,\n            OrganizationUser orgUser,\n            FakeTimeProvider timeProvider,\n            string externalId,\n            SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        orgUser.Email = address.Address;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var request = new InviteOrganizationUsersRequest(\n            invites: [\n                new OrganizationUserInviteCommandModel(\n                    email: orgUser.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .SelectKnownEmailsAsync(organization.Id, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)));\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .CreateManyAsync(Arg.Is<IEnumerable<CreateOrganizationUser>>(users =>\n                users.Any(user => user.OrganizationUser.Email == request.Invites.First().Email)));\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .Received(1)\n            .SendInvitesAsync(Arg.Is<SendInvitesRequest>(invite =>\n                invite.Organization == organization &&\n                invite.Users.Count(x => x.Email == orgUser.Email) == 1));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenEmailIsNewAndRequestIsInvalid_ThenFailureIsReturnedWithValidationFailureReason(\n            MailAddress address,\n            Organization organization,\n            OrganizationUser user,\n            FakeTimeProvider timeProvider,\n            string externalId,\n            SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        const string errorMessage = \"Org cannot add user for some given reason\";\n\n        user.Email = address.Address;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var request = new InviteOrganizationUsersRequest(\n            invites: [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var validationRequest = GetInviteValidationRequestMock(request, inviteOrganization, organization);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .SelectKnownEmailsAsync(organization.Id, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Invalid<InviteOrganizationUsersValidationRequest>(\n                new Error<InviteOrganizationUsersValidationRequest>(errorMessage, validationRequest)));\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Failure<ScimInviteOrganizationUsersResponse>>(result);\n        var failure = result as Failure<ScimInviteOrganizationUsersResponse>;\n\n        Assert.Equal(errorMessage, failure!.Error.Message);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceive()\n            .CreateManyAsync(Arg.Any<IEnumerable<CreateOrganizationUser>>());\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .DidNotReceive()\n            .SendInvitesAsync(Arg.Any<SendInvitesRequest>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenValidInviteCausesOrganizationToReachMaxSeats_ThenOrganizationOwnersShouldBeNotified(\n        MailAddress address,\n        Organization organization,\n        OrganizationUser user,\n        FakeTimeProvider timeProvider,\n        string externalId,\n        OrganizationUserUserDetails ownerDetails,\n        SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n        organization.Seats = 1;\n        organization.MaxAutoscaleSeats = 2;\n        ownerDetails.Type = OrganizationUserType.Owner;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var request = new InviteOrganizationUsersRequest(\n            invites: [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        orgUserRepository\n            .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n        orgUserRepository\n            .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)\n            .Returns([ownerDetails]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)\n                .WithPasswordManagerUpdate(new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);\n\n        Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationMaxSeatLimitReachedEmailAsync(organization,\n                inviteOrganization.MaxAutoScaleSeats.Value,\n                Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenValidInviteCausesOrgToAutoscale_ThenOrganizationOwnersShouldBeNotified(\n            MailAddress address,\n            Organization organization,\n            OrganizationUser user,\n            FakeTimeProvider timeProvider,\n            string externalId,\n            OrganizationUserUserDetails ownerDetails,\n            SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n        organization.Seats = 1;\n        organization.MaxAutoscaleSeats = 2;\n        organization.OwnersNotifiedOfAutoscaling = null;\n        ownerDetails.Type = OrganizationUserType.Owner;\n\n        var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));\n\n        var request = new InviteOrganizationUsersRequest(\n            invites:\n            [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        orgUserRepository\n            .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n        orgUserRepository\n            .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)\n            .Returns([ownerDetails]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(\n                GetInviteValidationRequestMock(request, inviteOrganization, organization)\n                    .WithPasswordManagerUpdate(\n                        new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);\n\n        Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationAutoscaledEmailAsync(organization,\n                inviteOrganization.Seats.Value,\n                Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == ownerDetails.Email)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenValidInviteIncreasesSeats_ThenSeatTotalShouldBeUpdated(\n        MailAddress address,\n        Organization organization,\n        OrganizationUser user,\n        FakeTimeProvider timeProvider,\n        string externalId,\n        OrganizationUserUserDetails ownerDetails,\n        SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n        organization.Seats = 1;\n        organization.MaxAutoscaleSeats = 2;\n        ownerDetails.Type = OrganizationUserType.Owner;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var request = new InviteOrganizationUsersRequest(\n            invites: [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var passwordManagerUpdate = new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1);\n\n        var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        orgUserRepository\n            .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n        orgUserRepository\n            .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)\n            .Returns([ownerDetails]);\n\n        var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        orgRepository.GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)\n                .WithPasswordManagerUpdate(passwordManagerUpdate)));\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);\n\n        await orgRepository.Received(1).IncrementSeatCountAsync(organization.Id, passwordManagerUpdate.SeatsRequiredToAdd, request.PerformedAt.UtcDateTime);\n\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(Arg.Is<Organization>(x => x.Seats == passwordManagerUpdate.UpdatedSeatTotal));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenValidInviteIncreasesSecretsManagerSeats_ThenSecretsManagerShouldBeUpdated(\n    MailAddress address,\n    Organization organization,\n    OrganizationUser user,\n    FakeTimeProvider timeProvider,\n    string externalId,\n    OrganizationUserUserDetails ownerDetails,\n    SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n        organization.Seats = 1;\n        organization.SmSeats = 1;\n        organization.MaxAutoscaleSeats = 2;\n        organization.MaxAutoscaleSmSeats = 2;\n        ownerDetails.Type = OrganizationUserType.Owner;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var request = new InviteOrganizationUsersRequest(\n            invites: [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true)\n            .AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager));\n\n        var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        orgUserRepository\n            .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n        orgUserRepository\n            .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)\n            .Returns([ownerDetails]);\n        orgRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n        {\n            Sponsored = 0,\n            Users = 1\n        });\n        orgUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(1);\n\n        orgRepository.GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)\n                .WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert;\n        Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);\n\n        await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()\n            .Received(1)\n            .UpdateSubscriptionAsync(secretsManagerSubscriptionUpdate);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenAnErrorOccursWhileInvitingUsers_ThenAnySeatChangesShouldBeReverted(\n        MailAddress address,\n        Organization organization,\n        OrganizationUser user,\n        FakeTimeProvider timeProvider,\n        string externalId,\n        OrganizationUserUserDetails ownerDetails,\n        SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n        organization.Seats = 1;\n        organization.SmSeats = 1;\n        organization.MaxAutoscaleSeats = 2;\n        organization.MaxAutoscaleSmSeats = 2;\n        ownerDetails.Type = OrganizationUserType.Owner;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var request = new InviteOrganizationUsersRequest(\n            invites: [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true)\n            .AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager));\n\n        var passwordManagerSubscriptionUpdate =\n            new PasswordManagerSubscriptionUpdate(inviteOrganization, 1, request.Invites.Length);\n\n        var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        orgUserRepository\n            .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n        orgUserRepository\n            .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)\n            .Returns([ownerDetails]);\n\n        var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        orgRepository.GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)\n                .WithPasswordManagerUpdate(passwordManagerSubscriptionUpdate)\n                .WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));\n\n        sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .SendInvitesAsync(Arg.Any<SendInvitesRequest>())\n            .Throws(new Exception(\"Something went wrong\"));\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Failure<ScimInviteOrganizationUsersResponse>>(result);\n        Assert.Equal(FailedToInviteUsersError.Code, (result as Failure<ScimInviteOrganizationUsersResponse>)!.Error.Message);\n\n        // org user revert\n        await orgUserRepository.Received(1).DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(x => x.Count() == 1));\n\n        // SM revert\n        await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()\n            .Received(2)\n            .UpdateSubscriptionAsync(Arg.Any<SecretsManagerSubscriptionUpdate>());\n\n        // PM revert\n        await orgRepository.Received(1).ReplaceAsync(Arg.Any<Organization>());\n\n        await sutProvider.GetDependency<IApplicationCacheService>().Received(2)\n            .UpsertOrganizationAbilityAsync(Arg.Any<Organization>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationIsManagedByAProvider_ThenAnEmailShouldBeSentToTheProvider(\n        MailAddress address,\n        Organization organization,\n        OrganizationUser user,\n        FakeTimeProvider timeProvider,\n        string externalId,\n        OrganizationUserUserDetails ownerDetails,\n        ProviderOrganization providerOrganization,\n        SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n        organization.Seats = 1;\n        organization.SmSeats = 1;\n        organization.MaxAutoscaleSeats = 2;\n        organization.MaxAutoscaleSmSeats = 2;\n        ownerDetails.Type = OrganizationUserType.Owner;\n\n        providerOrganization.OrganizationId = organization.Id;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var request = new InviteOrganizationUsersRequest(\n            invites: [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true)\n            .AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager));\n\n        var passwordManagerSubscriptionUpdate =\n            new PasswordManagerSubscriptionUpdate(inviteOrganization, 1, request.Invites.Length);\n\n        var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        orgUserRepository\n            .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n        orgUserRepository\n            .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)\n            .Returns([ownerDetails]);\n\n        var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        orgRepository.GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)\n                .WithPasswordManagerUpdate(passwordManagerSubscriptionUpdate)\n                .WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>()\n            .GetByOrganizationId(organization.Id)\n            .Returns(providerOrganization);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyDetailsByProviderAsync(providerOrganization.ProviderId, ProviderUserStatusType.Confirmed)\n            .Returns(new List<ProviderUserUserDetails>\n            {\n                new()\n                {\n                    Email = \"provider@email.com\"\n                }\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);\n\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2,\n                Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == \"provider@email.com\")));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationIsManagedByAProviderAndAutoscaleOccurs_ThenAnEmailShouldBeSentToTheProvider(\n        MailAddress address,\n        Organization organization,\n        OrganizationUser user,\n        FakeTimeProvider timeProvider,\n        string externalId,\n        OrganizationUserUserDetails ownerDetails,\n        ProviderOrganization providerOrganization,\n        SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n        organization.Seats = 1;\n        organization.SmSeats = 1;\n        organization.MaxAutoscaleSeats = 2;\n        organization.MaxAutoscaleSmSeats = 2;\n        organization.OwnersNotifiedOfAutoscaling = null;\n        ownerDetails.Type = OrganizationUserType.Owner;\n\n        providerOrganization.OrganizationId = organization.Id;\n\n        var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));\n\n        var request = new InviteOrganizationUsersRequest(\n            invites: [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var secretsManagerSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, inviteOrganization.Plan, true)\n            .AdjustSeats(request.Invites.Count(x => x.AccessSecretsManager));\n\n        var passwordManagerSubscriptionUpdate =\n            new PasswordManagerSubscriptionUpdate(inviteOrganization, 1, request.Invites.Length);\n\n        var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        orgUserRepository\n            .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n        orgUserRepository\n            .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)\n            .Returns([ownerDetails]);\n\n        var orgRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        orgRepository.GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(GetInviteValidationRequestMock(request, inviteOrganization, organization)\n                .WithPasswordManagerUpdate(passwordManagerSubscriptionUpdate)\n                .WithSecretsManagerUpdate(secretsManagerSubscriptionUpdate)));\n\n        sutProvider.GetDependency<IProviderOrganizationRepository>()\n            .GetByOrganizationId(organization.Id)\n            .Returns(providerOrganization);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyDetailsByProviderAsync(providerOrganization.ProviderId, ProviderUserStatusType.Confirmed)\n            .Returns(new List<ProviderUserUserDetails>\n            {\n                new()\n                {\n                    Email = \"provider@email.com\"\n                }\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);\n\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendOrganizationAutoscaledEmailAsync(organization, 1,\n                Arg.Is<IEnumerable<string>>(emails => emails.Any(email => email == \"provider@email.com\")));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationAutoscalesButOwnersHaveAlreadyBeenNotified_ThenAnEmailShouldNotBeSent(\n        MailAddress address,\n        Organization organization,\n        OrganizationUser user,\n        FakeTimeProvider timeProvider,\n        string externalId,\n        OrganizationUserUserDetails ownerDetails,\n        SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n        organization.Seats = 1;\n        organization.MaxAutoscaleSeats = 2;\n        organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;\n        ownerDetails.Type = OrganizationUserType.Owner;\n\n        var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));\n\n        var request = new InviteOrganizationUsersRequest(\n            invites:\n            [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        orgUserRepository\n            .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n        orgUserRepository\n            .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)\n            .Returns([ownerDetails]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(\n                GetInviteValidationRequestMock(request, inviteOrganization, organization)\n                    .WithPasswordManagerUpdate(\n                        new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);\n\n        Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);\n\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceive()\n            .SendOrganizationAutoscaledEmailAsync(Arg.Any<Organization>(),\n                Arg.Any<int>(),\n                Arg.Any<IEnumerable<string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task InviteScimOrganizationUserAsync_WhenAnOrganizationDoesNotAutoScale_ThenAnEmailShouldNotBeSent(\n        MailAddress address,\n        Organization organization,\n        OrganizationUser user,\n        FakeTimeProvider timeProvider,\n        string externalId,\n        OrganizationUserUserDetails ownerDetails,\n        SutProvider<InviteOrganizationUsersCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = address.Address;\n        organization.Seats = 2;\n        organization.MaxAutoscaleSeats = 2;\n        organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;\n        ownerDetails.Type = OrganizationUserType.Owner;\n\n        var inviteOrganization = new InviteOrganization(organization, new Enterprise2019Plan(true));\n\n        var request = new InviteOrganizationUsersRequest(\n            invites:\n            [\n                new OrganizationUserInviteCommandModel(\n                    email: user.Email,\n                    assignedCollections: [],\n                    groups: [],\n                    type: OrganizationUserType.User,\n                    permissions: new Permissions(),\n                    externalId: externalId,\n                    accessSecretsManager: true)\n            ],\n            inviteOrganization: inviteOrganization,\n            performedBy: Guid.Empty,\n            timeProvider.GetUtcNow());\n\n        var orgUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        orgUserRepository\n            .SelectKnownEmailsAsync(inviteOrganization.OrganizationId, Arg.Any<IEnumerable<string>>(), false)\n            .Returns([]);\n        orgUserRepository\n            .GetManyByMinimumRoleAsync(inviteOrganization.OrganizationId, OrganizationUserType.Owner)\n            .Returns([ownerDetails]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IInviteUsersValidator>()\n            .ValidateAsync(Arg.Any<InviteOrganizationUsersValidationRequest>())\n            .Returns(new Valid<InviteOrganizationUsersValidationRequest>(\n                GetInviteValidationRequestMock(request, inviteOrganization, organization)\n                    .WithPasswordManagerUpdate(\n                        new PasswordManagerSubscriptionUpdate(inviteOrganization, organization.Seats.Value, 1))));\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 0 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(0);\n\n        // Act\n        var result = await sutProvider.Sut.InviteScimOrganizationUserAsync(request);\n\n        // Assert\n        Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);\n\n        Assert.NotNull(inviteOrganization.MaxAutoScaleSeats);\n\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceive()\n            .SendOrganizationAutoscaledEmailAsync(Arg.Any<Organization>(),\n                Arg.Any<int>(),\n                Arg.Any<IEnumerable<string>>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\n[SutProviderCustomize]\npublic class ResendOrganizationInviteCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ResendInviteAsync_WhenValidUserAndOrganization_SendsInvite(\n        Organization organization,\n        OrganizationUser organizationUser,\n        SutProvider<ResendOrganizationInviteCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = organization.Id;\n        organizationUser.Status = OrganizationUserStatusType.Invited;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        // Act\n        await sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id);\n\n        // Assert\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .Received(1)\n            .SendInvitesAsync(Arg.Is<SendInvitesRequest>(req =>\n                req.Organization == organization &&\n                req.Users.Length == 1 &&\n                req.Users[0] == organizationUser &&\n                req.InitOrganization == false));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ResendInviteAsync_WhenInitOrganizationTrue_SendsInviteWithInitFlag(\n        Organization organization,\n        OrganizationUser organizationUser,\n        SutProvider<ResendOrganizationInviteCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = organization.Id;\n        organizationUser.Status = OrganizationUserStatusType.Invited;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        // Act\n        await sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id, initOrganization: true);\n\n        // Assert\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .Received(1)\n            .SendInvitesAsync(Arg.Is<SendInvitesRequest>(req =>\n                req.Organization == organization &&\n                req.Users.Length == 1 &&\n                req.Users[0] == organizationUser &&\n                req.InitOrganization == true));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ResendInviteAsync_WhenOrganizationUserInvalid_ThrowsBadRequest(\n        Organization organization,\n        OrganizationUser organizationUser,\n        SutProvider<ResendOrganizationInviteCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = organization.Id;\n        organizationUser.Status = OrganizationUserStatusType.Accepted;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        // Act + Assert\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id));\n\n        Assert.Equal(\"User invalid.\", ex.Message);\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .DidNotReceive()\n            .SendInvitesAsync(Arg.Any<SendInvitesRequest>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ResendInviteAsync_WhenOrganizationNotFound_ThrowsBadRequest(\n        Organization organization,\n        OrganizationUser organizationUser,\n        SutProvider<ResendOrganizationInviteCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = organization.Id;\n        organizationUser.Status = OrganizationUserStatusType.Invited;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns((Organization?)null);\n\n        // Act + Assert\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id));\n\n        Assert.Equal(\"Organization invalid.\", ex.Message);\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .DidNotReceive()\n            .SendInvitesAsync(Arg.Any<SendInvitesRequest>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Fakes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\n\n[SutProviderCustomize]\npublic class SendOrganizationInvitesCommandTests\n{\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();\n\n    [Theory]\n    [OrganizationInviteCustomize, OrganizationCustomize, BitAutoData]\n    public async Task SendInvitesAsync_SsoOrgWithNeverEnabledRequireSsoPolicy_SendsEmailWithoutRequiringSso(\n        Organization organization,\n        SsoConfig ssoConfig,\n        OrganizationUser invite,\n        [Policy(PolicyType.RequireSso, false)] PolicyStatus policy,\n        SutProvider<SendOrganizationInvitesCommand> sutProvider)\n    {\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        // Org must be able to use SSO and policies to trigger this test case\n        organization.UseSso = true;\n        organization.UsePolicies = true;\n\n        ssoConfig.Enabled = true;\n        sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig);\n\n        // Return null policy to mimic new org that's never turned on the require sso policy\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.RequireSso)\n            .Returns(policy);\n\n        // Mock tokenable factory to return a token that expires in 5 days\n        sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()\n            .CreateToken(Arg.Any<OrganizationUser>())\n            .Returns(\n                info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())\n                {\n                    ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n                });\n\n        // Act\n        await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization));\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>\n                info.OrgUserTokenPairs.Count() == 1 &&\n                info.OrgUserTokenPairs.FirstOrDefault(x => x.OrgUser.Email == invite.Email).OrgUser == invite &&\n                info.IsFreeOrg == (organization.PlanType == PlanType.Free) &&\n                info.OrganizationName == organization.Name &&\n                info.OrgSsoLoginRequiredPolicyEnabled == false));\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize, OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_SsoOrgWithNullSsoConfig_SendsInvite(\n        Organization organization,\n        OrganizationUser invite,\n        SutProvider<SendOrganizationInvitesCommand> sutProvider)\n    {\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        // Org must be able to use SSO to trigger this proper test case as we currently only call to retrieve\n        // an org's SSO config if the org can use SSO\n        organization.UseSso = true;\n\n        // Return null for sso config\n        sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(organization.Id).ReturnsNull();\n\n        // Mock tokenable factory to return a token that expires in 5 days\n        sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()\n            .CreateToken(Arg.Any<OrganizationUser>())\n            .Returns(\n                info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())\n                {\n                    ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n                });\n\n        await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization));\n\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>\n                info.OrgUserTokenPairs.Count() == 1 &&\n                info.OrgUserTokenPairs.FirstOrDefault(x => x.OrgUser.Email == invite.Email).OrgUser == invite &&\n                info.IsFreeOrg == (organization.PlanType == PlanType.Free) &&\n                info.OrganizationName == organization.Name));\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.FamiliesAnnually)]\n    [BitAutoData(PlanType.Free)]\n    [BitAutoData(PlanType.Custom)]\n    public async Task SendInvitesAsync_CallsMailServiceWithNewTemplates(\n        PlanType planType,\n        Organization organization,\n        OrganizationUser invite,\n        User invitingUser,\n        SutProvider<SendOrganizationInvitesCommand> sutProvider)\n    {\n        SetupSutProvider(sutProvider);\n\n        // Arrange\n        organization.PlanType = planType;\n        invite.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyByEmailsAsync(Arg.Any<IEnumerable<string>>())\n            .Returns([]);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(invitingUser.Id)\n            .Returns(invitingUser);\n\n        sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()\n            .CreateToken(Arg.Any<OrganizationUser>())\n            .Returns(info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())\n            {\n                ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n            });\n\n        // Act\n        await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id));\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>\n                info.OrgUserTokenPairs.Any(p => p.OrgUser.Email == invite.Email) &&\n                info.InviterEmail == invitingUser.Email));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendInvitesAsync_WithInvitingUserId_PopulatesInviterEmail(\n        Organization organization,\n        OrganizationUser invite,\n        User invitingUser,\n        SutProvider<SendOrganizationInvitesCommand> sutProvider)\n    {\n        SetupSutProvider(sutProvider);\n\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyByEmailsAsync(Arg.Any<IEnumerable<string>>())\n            .Returns([]);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(invitingUser.Id)\n            .Returns(invitingUser);\n\n        sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()\n            .CreateToken(Arg.Any<OrganizationUser>())\n            .Returns(info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())\n            {\n                ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n            });\n\n        // Act\n        await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, invitingUser.Id));\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>\n                info.OrgUserTokenPairs.Any(p => p.OrgUser.Email == invite.Email) &&\n                info.InviterEmail == invitingUser.Email));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendInvitesAsync_WithNullInvitingUserId_SendsEmailWithoutInviter(\n        Organization organization,\n        OrganizationUser invite,\n        SutProvider<SendOrganizationInvitesCommand> sutProvider)\n    {\n        SetupSutProvider(sutProvider);\n\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyByEmailsAsync(Arg.Any<IEnumerable<string>>())\n            .Returns([]);\n\n        sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()\n            .CreateToken(Arg.Any<OrganizationUser>())\n            .Returns(info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())\n            {\n                ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n            });\n\n        // Act - pass null for InvitingUserId\n        await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, null));\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>\n                info.OrgUserTokenPairs.Any(p => p.OrgUser.Email == invite.Email) &&\n                info.InviterEmail == null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendInvitesAsync_WithNonExistentInvitingUserId_SendsEmailWithoutInviter(\n        Organization organization,\n        OrganizationUser invite,\n        Guid nonExistentUserId,\n        SutProvider<SendOrganizationInvitesCommand> sutProvider)\n    {\n        SetupSutProvider(sutProvider);\n\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyByEmailsAsync(Arg.Any<IEnumerable<string>>())\n            .Returns([]);\n\n        // Mock GetByIdAsync to return null for non-existent user\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(nonExistentUserId)\n            .ReturnsNull();\n\n        sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()\n            .CreateToken(Arg.Any<OrganizationUser>())\n            .Returns(info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())\n            {\n                ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n            });\n\n        // Act\n        await sutProvider.Sut.SendInvitesAsync(new SendInvitesRequest([invite], organization, false, nonExistentUserId));\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendUpdatedOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>\n                info.OrgUserTokenPairs.Any(p => p.OrgUser.Email == invite.Email) &&\n                info.InviterEmail == null));\n    }\n\n    private void SetupSutProvider(SutProvider<SendOrganizationInvitesCommand> sutProvider)\n    {\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\n\n[SutProviderCustomize]\npublic class InviteOrganizationUsersValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WhenOrganizationHasSecretsManagerInvitesAndDoesNotHaveEnoughSeatsAvailable_ThenShouldCorrectlyCalculateSeatsToAdd(\n        Organization organization,\n        SutProvider<InviteOrganizationUsersValidator> sutProvider\n    )\n    {\n        organization.Seats = null;\n        organization.SmSeats = 10;\n        organization.UseSecretsManager = true;\n\n        var request = new InviteOrganizationUsersValidationRequest\n        {\n            Invites =\n            [\n                new OrganizationUserInviteCommandModel(\n                    email: \"test@email.com\",\n                    externalId: \"test-external-id\"),\n                new OrganizationUserInviteCommandModel(\n                    email: \"test2@email.com\",\n                    externalId: \"test-external-id2\"),\n                new OrganizationUserInviteCommandModel(\n                    email: \"test3@email.com\",\n                    externalId: \"test-external-id3\")\n            ],\n            InviteOrganization = new InviteOrganization(organization, new Enterprise2023Plan(true)),\n            OccupiedPmSeats = 0,\n            OccupiedSmSeats = 9\n        };\n\n        sutProvider.GetDependency<IStripePaymentService>()\n            .HasSecretsManagerStandalone(request.InviteOrganization)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        _ = await sutProvider.Sut.ValidateAsync(request);\n\n        await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()\n            .Received(1)\n            .ValidateUpdateAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(x =>\n                x.SmSeatsChanged == true && x.SmSeats == 12));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WhenOrganizationHasSecretsManagerInvitesAndHasSeatsAvailable_ThenShouldReturnValid(\n        Organization organization,\n        SutProvider<InviteOrganizationUsersValidator> sutProvider\n    )\n    {\n        organization.Seats = null;\n        organization.SmSeats = 12;\n        organization.UseSecretsManager = true;\n\n        var request = new InviteOrganizationUsersValidationRequest\n        {\n            Invites =\n            [\n                new OrganizationUserInviteCommandModel(\n                    email: \"test@email.com\",\n                    externalId: \"test-external-id\"),\n                new OrganizationUserInviteCommandModel(\n                    email: \"test2@email.com\",\n                    externalId: \"test-external-id2\"),\n                new OrganizationUserInviteCommandModel(\n                    email: \"test3@email.com\",\n                    externalId: \"test-external-id3\")\n            ],\n            InviteOrganization = new InviteOrganization(organization, new Enterprise2023Plan(true)),\n            OccupiedPmSeats = 0,\n            OccupiedSmSeats = 9\n        };\n\n        sutProvider.GetDependency<IStripePaymentService>()\n            .HasSecretsManagerStandalone(request.InviteOrganization)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        Assert.IsType<Valid<InviteOrganizationUsersValidationRequest>>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WhenOrganizationHasSecretsManagerInvitesAndSmSeatUpdateFailsValidation_ThenShouldReturnInvalid(\n        Organization organization,\n        SutProvider<InviteOrganizationUsersValidator> sutProvider\n    )\n    {\n        organization.Seats = null;\n        organization.SmSeats = 5;\n        organization.MaxAutoscaleSmSeats = 5;\n        organization.UseSecretsManager = true;\n\n        var request = new InviteOrganizationUsersValidationRequest\n        {\n            Invites =\n            [\n                new OrganizationUserInviteCommandModel(\n                    email: \"test@email.com\",\n                    externalId: \"test-external-id\"),\n                new OrganizationUserInviteCommandModel(\n                    email: \"test2@email.com\",\n                    externalId: \"test-external-id2\"),\n                new OrganizationUserInviteCommandModel(\n                    email: \"test3@email.com\",\n                    externalId: \"test-external-id3\")\n            ],\n            InviteOrganization = new InviteOrganization(organization, new Enterprise2023Plan(true)),\n            OccupiedPmSeats = 0,\n            OccupiedSmSeats = 4\n        };\n\n        sutProvider.GetDependency<IStripePaymentService>()\n            .HasSecretsManagerStandalone(request.InviteOrganization)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()\n            .ValidateUpdateAsync(Arg.Any<SecretsManagerSubscriptionUpdate>())\n            .Throws(new BadRequestException(\"Some Secrets Manager Failure\"));\n\n        var result = await sutProvider.Sut.ValidateAsync(request);\n\n        Assert.IsType<Invalid<InviteOrganizationUsersValidationRequest>>(result);\n        Assert.Equal(\"Some Secrets Manager Failure\", (result as Invalid<InviteOrganizationUsersValidationRequest>)!.Error.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\n\n[SutProviderCustomize]\npublic class InviteUserOrganizationValidationTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Validate_WhenOrganizationIsFreeTier_ShouldReturnValidResponse(Organization organization, SutProvider<InviteUsersOrganizationValidator> sutProvider)\n    {\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var result = await sutProvider.Sut.ValidateAsync(inviteOrganization);\n\n        Assert.IsType<Valid<InviteOrganization>>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Validate_WhenOrganizationDoesNotHavePaymentMethod_ShouldReturnInvalidResponseWithPaymentMethodMessage(\n        Organization organization, SutProvider<InviteUsersOrganizationValidator> sutProvider)\n    {\n        organization.GatewayCustomerId = string.Empty;\n        organization.Seats = 3;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var result = await sutProvider.Sut.ValidateAsync(inviteOrganization);\n\n        Assert.IsType<Invalid<InviteOrganization>>(result);\n        Assert.Equal(OrganizationNoPaymentMethodFoundError.Code, (result as Invalid<InviteOrganization>)!.Error.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Validate_WhenOrganizationDoesNotHaveSubscription_ShouldReturnInvalidResponseWithSubscriptionMessage(\n        Organization organization, SutProvider<InviteUsersOrganizationValidator> sutProvider)\n    {\n        organization.GatewaySubscriptionId = string.Empty;\n        organization.Seats = 3;\n        organization.MaxAutoscaleSeats = 4;\n\n        var inviteOrganization = new InviteOrganization(organization, new FreePlan());\n\n        var result = await sutProvider.Sut.ValidateAsync(inviteOrganization);\n\n        Assert.IsType<Invalid<InviteOrganization>>(result);\n        Assert.Equal(OrganizationNoSubscriptionFoundError.Code, (result as Invalid<InviteOrganization>)!.Error.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\n\npublic class InviteUserPaymentValidationTests\n{\n    [Theory]\n    [BitAutoData]\n    public void Validate_WhenPlanIsFree_ReturnsValidResponse(Organization organization)\n    {\n        organization.PlanType = PlanType.Free;\n\n        var result = InviteUserPaymentValidation.Validate(new PaymentsSubscription\n        {\n            SubscriptionStatus = StripeConstants.SubscriptionStatus.Active,\n            ProductTierType = new InviteOrganization(organization, new FreePlan()).Plan.ProductTier\n        });\n\n        Assert.IsType<Valid<PaymentsSubscription>>(result);\n    }\n\n    [Fact]\n    public void Validate_WhenSubscriptionIsCanceled_ReturnsInvalidResponse()\n    {\n        var result = InviteUserPaymentValidation.Validate(new PaymentsSubscription\n        {\n            SubscriptionStatus = StripeConstants.SubscriptionStatus.Canceled,\n            ProductTierType = ProductTierType.Enterprise\n        });\n\n        Assert.IsType<Invalid<PaymentsSubscription>>(result);\n        Assert.Equal(PaymentCancelledSubscriptionError.Code, (result as Invalid<PaymentsSubscription>)!.Error.Message);\n    }\n\n    [Fact]\n    public void Validate_WhenSubscriptionIsActive_ReturnsValidResponse()\n    {\n        var result = InviteUserPaymentValidation.Validate(new PaymentsSubscription\n        {\n            SubscriptionStatus = StripeConstants.SubscriptionStatus.Active,\n            ProductTierType = ProductTierType.Enterprise\n        });\n\n        Assert.IsType<Valid<PaymentsSubscription>>(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;\n\n\n[SutProviderCustomize]\npublic class InviteUsersPasswordManagerValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Validate_OrganizationDoesNotHaveSeatsLimit_ShouldReturnValidResult(Organization organization,\n        SutProvider<InviteUsersPasswordManagerValidator> sutProvider)\n    {\n        organization.Seats = null;\n\n        var organizationDto = new InviteOrganization(organization, new FreePlan());\n\n        var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(organizationDto, 0, 0);\n\n        var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate);\n\n        Assert.IsType<Valid<PasswordManagerSubscriptionUpdate>>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Validate_NumberOfSeatsToAddMatchesSeatsAvailable_ShouldReturnValidResult(Organization organization,\n        SutProvider<InviteUsersPasswordManagerValidator> sutProvider)\n    {\n        organization.Seats = 8;\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        var seatsOccupiedByUsers = 4;\n        var additionalSeats = 4;\n\n        var organizationDto = new InviteOrganization(organization, new Enterprise2023Plan(isAnnual: true));\n\n        var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(organizationDto, seatsOccupiedByUsers, additionalSeats);\n\n        var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate);\n\n        Assert.IsType<Valid<PasswordManagerSubscriptionUpdate>>(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Validate_NumberOfSeatsToAddIsGreaterThanMaxSeatsAllowed_ShouldBeInvalidWithSeatLimitMessage(Organization organization,\n        SutProvider<InviteUsersPasswordManagerValidator> sutProvider)\n    {\n        organization.Seats = 4;\n        organization.MaxAutoscaleSeats = 4;\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        var seatsOccupiedByUsers = 4;\n        var additionalSeats = 1;\n\n        var organizationDto = new InviteOrganization(organization, new Enterprise2023Plan(isAnnual: true));\n\n        var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(organizationDto, seatsOccupiedByUsers, additionalSeats);\n\n        var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate);\n\n        Assert.IsType<Invalid<PasswordManagerSubscriptionUpdate>>(result);\n        Assert.Equal(PasswordManagerSeatLimitHasBeenReachedError.Code, (result as Invalid<PasswordManagerSubscriptionUpdate>)!.Error.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Validate_GivenThePlanDoesNotAllowAdditionalSeats_ShouldBeInvalidMessageOfPlanNotAllowingSeats(Organization organization,\n        SutProvider<InviteUsersPasswordManagerValidator> sutProvider)\n    {\n        organization.Seats = 4;\n        organization.MaxAutoscaleSeats = 9;\n        var seatsOccupiedByUsers = 4;\n        var additionalSeats = 4;\n        organization.PlanType = PlanType.Free;\n\n        var organizationDto = new InviteOrganization(organization, new FreePlan());\n\n        var subscriptionUpdate = new PasswordManagerSubscriptionUpdate(organizationDto, seatsOccupiedByUsers, additionalSeats);\n\n        var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate);\n\n        Assert.IsType<Invalid<PasswordManagerSubscriptionUpdate>>(result);\n        Assert.Equal(PasswordManagerPlanDoesNotAllowAdditionalSeatsError.Code, (result as Invalid<PasswordManagerSubscriptionUpdate>)!.Error.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationConfirmation/SendOrganizationConfirmationCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.OrganizationConfirmation;\n\n[SutProviderCustomize]\npublic class SendOrganizationConfirmationCommandTests\n{\n    [Theory]\n    [OrganizationCustomize, BitAutoData]\n    public async Task SendConfirmationAsync_EnterpriseOrg_CallsUpdatedConfirmedEmail(\n        Organization organization,\n        string userEmail,\n        SutProvider<SendOrganizationConfirmationCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually;\n\n        // Act\n        await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, true);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendUpdatedOrganizationConfirmedEmailAsync(organization, userEmail, true);\n    }\n\n    [Theory]\n    [OrganizationCustomize, BitAutoData]\n    public async Task SendConfirmationAsync_FamiliesOrg_CallsUpdatedConfirmedEmail(\n        Organization organization,\n        string userEmail,\n        SutProvider<SendOrganizationConfirmationCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.FamiliesAnnually;\n\n        // Act\n        await sutProvider.Sut.SendConfirmationAsync(organization, userEmail, false);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendUpdatedOrganizationConfirmedEmailAsync(organization, userEmail, false);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class OrganizationUserUserDetailsQueryTests\n{\n    [Theory]\n    [BitAutoData(\" \")]\n    public async Task GetAccountRecoveryEnrolledUsers_InvalidKey_FiltersOut(\n        string invalidKey,\n        Guid orgId,\n        SutProvider<OrganizationUserUserDetailsQuery> sutProvider)\n    {\n        // Arrange\n        var request = new OrganizationUserUserDetailsQueryRequest { OrganizationId = orgId };\n\n        var validUser = CreateOrgUserDetails(orgId, \"valid-key\");\n        var invalidUser = CreateOrgUserDetails(orgId, invalidKey);\n        var allUsers = new[] { validUser, invalidUser };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync_vNext(orgId, false, false)\n            .Returns(allUsers);\n\n        SetupTwoFactorAndClaimedStatus(sutProvider, orgId);\n\n        // Act\n        var result = (await sutProvider.Sut.GetAccountRecoveryEnrolledUsers(request)).ToList();\n\n        // Assert - invalid key user should be filtered out\n        Assert.Single(result);\n        Assert.Equal(validUser.Id, result[0].OrgUser.Id);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetAccountRecoveryEnrolledUsers_NullKey_FiltersOut(\n        Guid orgId,\n        SutProvider<OrganizationUserUserDetailsQuery> sutProvider)\n    {\n        // Arrange\n        var request = new OrganizationUserUserDetailsQueryRequest { OrganizationId = orgId };\n\n        var validUser = CreateOrgUserDetails(orgId, \"valid-key\");\n        var nullUser = CreateOrgUserDetails(orgId, null!);\n        var allUsers = new[] { validUser, nullUser };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync_vNext(orgId, false, false)\n            .Returns(allUsers);\n\n        SetupTwoFactorAndClaimedStatus(sutProvider, orgId);\n\n        // Act\n        var result = (await sutProvider.Sut.GetAccountRecoveryEnrolledUsers(request)).ToList();\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(validUser.Id, result[0].OrgUser.Id);\n    }\n\n    private static OrganizationUserUserDetails CreateOrgUserDetails(Guid orgId, string resetPasswordKey)\n    {\n        return new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = orgId,\n            UserId = Guid.NewGuid(),\n            Status = OrganizationUserStatusType.Confirmed,\n            Type = OrganizationUserType.User,\n            UsesKeyConnector = false,\n            ResetPasswordKey = resetPasswordKey,\n            Email = \"test@example.com\"\n        };\n    }\n\n    private static void SetupTwoFactorAndClaimedStatus(\n        SutProvider<OrganizationUserUserDetailsQuery> sutProvider, Guid orgId)\n    {\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())\n            .Returns(callInfo =>\n            {\n                var users = callInfo.Arg<IEnumerable<OrganizationUserUserDetails>>();\n                return users.Select(u => (user: u, twoFactorIsEnabled: false)).ToList();\n            });\n\n        sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .GetUsersOrganizationClaimedStatusAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo =>\n            {\n                var userIds = callInfo.Arg<IEnumerable<Guid>>();\n                return userIds.ToDictionary(id => id, _ => false);\n            });\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/PushAutoConfirmNotificationCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class PushAutoConfirmNotificationCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task PushAsync_SendsNotificationToAdminsAndOwners(\n        SutProvider<PushAutoConfirmNotificationCommand> sutProvider,\n        Guid userId,\n        Guid organizationId,\n        OrganizationUser orgUser,\n        List<OrganizationUserUserDetails> admins)\n    {\n        foreach (var admin in admins)\n        {\n            admin.UserId = Guid.NewGuid();\n        }\n\n        orgUser.Id = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)\n            .Returns(admins);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)\n            .Returns(new List<OrganizationUserUserDetails>());\n\n        await sutProvider.Sut.PushAsync(userId, organizationId);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(admins.Count)\n            .PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>\n                pn.Type == PushType.AutoConfirm &&\n                pn.Target == NotificationTarget.User &&\n                pn.Payload.OrganizationId == organizationId &&\n                pn.Payload.TargetUserId == userId &&\n                pn.Payload.TargetOrganizationUserId == orgUser.Id &&\n                pn.ExcludeCurrentContext == false));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PushAsync_SendsNotificationToCustomUsersWithManageUsersPermission(\n        SutProvider<PushAutoConfirmNotificationCommand> sutProvider,\n        Guid userId,\n        Guid organizationId,\n        OrganizationUser orgUser,\n        List<OrganizationUserUserDetails> customUsers)\n    {\n        foreach (var customUser in customUsers)\n        {\n            customUser.UserId = Guid.NewGuid();\n            customUser.Permissions = \"{\\\"manageUsers\\\":true}\";\n        }\n\n        orgUser.Id = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)\n            .Returns(new List<OrganizationUserUserDetails>());\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)\n            .Returns(customUsers);\n\n        await sutProvider.Sut.PushAsync(userId, organizationId);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(customUsers.Count)\n            .PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>\n                pn.Type == PushType.AutoConfirm &&\n                pn.Target == NotificationTarget.User &&\n                pn.Payload.OrganizationId == organizationId &&\n                pn.Payload.TargetUserId == userId &&\n                pn.Payload.TargetOrganizationUserId == orgUser.Id &&\n                pn.ExcludeCurrentContext == false));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PushAsync_DoesNotSendToCustomUsersWithoutManageUsersPermission(\n        SutProvider<PushAutoConfirmNotificationCommand> sutProvider,\n        Guid userId,\n        Guid organizationId,\n        OrganizationUser orgUser,\n        List<OrganizationUserUserDetails> customUsers)\n    {\n        foreach (var customUser in customUsers)\n        {\n            customUser.UserId = Guid.NewGuid();\n            customUser.Permissions = \"{\\\"manageUsers\\\":false}\";\n        }\n\n        orgUser.Id = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)\n            .Returns(new List<OrganizationUserUserDetails>());\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)\n            .Returns(customUsers);\n\n        await sutProvider.Sut.PushAsync(userId, organizationId);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushAsync(Arg.Any<PushNotification<AutoConfirmPushNotification>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PushAsync_SendsToAdminsAndCustomUsersWithManageUsers(\n        SutProvider<PushAutoConfirmNotificationCommand> sutProvider,\n        Guid userId,\n        Guid organizationId,\n        OrganizationUser orgUser,\n        List<OrganizationUserUserDetails> admins,\n        List<OrganizationUserUserDetails> customUsersWithPermission,\n        List<OrganizationUserUserDetails> customUsersWithoutPermission)\n    {\n        foreach (var admin in admins)\n        {\n            admin.UserId = Guid.NewGuid();\n        }\n\n        foreach (var customUser in customUsersWithPermission)\n        {\n            customUser.UserId = Guid.NewGuid();\n            customUser.Permissions = \"{\\\"manageUsers\\\":true}\";\n        }\n\n        foreach (var customUser in customUsersWithoutPermission)\n        {\n            customUser.UserId = Guid.NewGuid();\n            customUser.Permissions = \"{\\\"manageUsers\\\":false}\";\n        }\n\n        orgUser.Id = Guid.NewGuid();\n\n        var allCustomUsers = customUsersWithPermission.Concat(customUsersWithoutPermission).ToList();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)\n            .Returns(admins);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)\n            .Returns(allCustomUsers);\n\n        await sutProvider.Sut.PushAsync(userId, organizationId);\n\n        var expectedNotificationCount = admins.Count + customUsersWithPermission.Count;\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(expectedNotificationCount)\n            .PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>\n                pn.Type == PushType.AutoConfirm &&\n                pn.Target == NotificationTarget.User &&\n                pn.Payload.OrganizationId == organizationId &&\n                pn.Payload.TargetUserId == userId &&\n                pn.Payload.TargetOrganizationUserId == orgUser.Id &&\n                pn.ExcludeCurrentContext == false));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PushAsync_SkipsUsersWithoutUserId(\n        SutProvider<PushAutoConfirmNotificationCommand> sutProvider,\n        Guid userId,\n        Guid organizationId,\n        OrganizationUser orgUser,\n        List<OrganizationUserUserDetails> admins)\n    {\n        admins[0].UserId = Guid.NewGuid();\n        admins[1].UserId = null;\n        admins[2].UserId = Guid.NewGuid();\n\n        orgUser.Id = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)\n            .Returns(admins);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)\n            .Returns(new List<OrganizationUserUserDetails>());\n\n        await sutProvider.Sut.PushAsync(userId, organizationId);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(2)\n            .PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>\n                pn.Type == PushType.AutoConfirm));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PushAsync_DeduplicatesUserIds(\n        SutProvider<PushAutoConfirmNotificationCommand> sutProvider,\n        Guid userId,\n        Guid organizationId,\n        OrganizationUser orgUser,\n        Guid duplicateUserId)\n    {\n        var admin1 = new OrganizationUserUserDetails { UserId = duplicateUserId };\n        var admin2 = new OrganizationUserUserDetails { UserId = duplicateUserId };\n        var customUser = new OrganizationUserUserDetails\n        {\n            UserId = duplicateUserId,\n            Permissions = \"{\\\"manageUsers\\\":true}\"\n        };\n\n        orgUser.Id = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organizationId, OrganizationUserType.Admin)\n            .Returns(new List<OrganizationUserUserDetails> { admin1, admin2 });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByRoleAsync(organizationId, OrganizationUserType.Custom)\n            .Returns(new List<OrganizationUserUserDetails> { customUser });\n\n        await sutProvider.Sut.PushAsync(userId, organizationId);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushAsync(Arg.Is<PushNotification<AutoConfirmPushNotification>>(pn =>\n                pn.TargetId == duplicateUserId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task PushAsync_OrganizationUserNotFound_ThrowsException(\n        SutProvider<PushAutoConfirmNotificationCommand> sutProvider,\n        Guid userId,\n        Guid organizationId)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns((OrganizationUser)null);\n\n        var exception = await Assert.ThrowsAsync<Exception>(() =>\n            sutProvider.Sut.PushAsync(userId, organizationId));\n\n        Assert.Equal(\"Organization user not found\", exception.Message);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushAsync(Arg.Any<PushNotification<AutoConfirmPushNotification>>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class RemoveOrganizationUserCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WithDeletingUserId_Success(\n        [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = deletingUser.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(deletingUser.Id)\n            .Returns(deletingUser);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(deletingUser.OrganizationId)\n            .Returns(true);\n\n        // Act\n        await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId);\n\n        // Assert\n        await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .Received(1)\n            .GetUsersOrganizationClaimedStatusAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)));\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .DeleteAsync(organizationUser);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WithDeletingUserId_NotFound_ThrowsException(\n        SutProvider<RemoveOrganizationUserCommand> sutProvider,\n        Guid organizationId, Guid organizationUserId)\n    {\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WithDeletingUserId_MismatchingOrganizationId_ThrowsException(\n        SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUserId)\n            .Returns(new OrganizationUser\n            {\n                Id = organizationUserId,\n                OrganizationId = Guid.NewGuid()\n            });\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WithDeletingUserId_InvalidUser_ThrowsException(\n        OrganizationUser organizationUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, null));\n        Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WithDeletingUserId_RemoveYourself_ThrowsException(\n        OrganizationUser deletingUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(deletingUser.Id)\n            .Returns(deletingUser);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId));\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveYourselfErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WithDeletingUserId_NonOwnerRemoveOwner_ThrowsException(\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,\n        [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = deletingUser.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationAdmin(organizationUser.OrganizationId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUser.UserId));\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveOwnerByNonOwnerErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WhenCustomUserRemovesAdmin_ThrowsException(\n    [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser organizationUser,\n    [OrganizationUser(type: OrganizationUserType.Custom)] OrganizationUser deletingUser,\n    SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = deletingUser.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationCustom(organizationUser.OrganizationId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUser.UserId));\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveAdminByCustomUserErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WithDeletingUserId_RemovingLastOwner_ThrowsException(\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,\n        OrganizationUser deletingUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = deletingUser.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),\n                Arg.Any<bool>())\n            .Returns(false);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(deletingUser.OrganizationId)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUser.UserId));\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);\n        await sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .Received(1)\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), true);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync((OrganizationUser)default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUserAsync_WithDeletingUserId_WhenUserIsManaged_ThrowsException(\n        [OrganizationUser(status: OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser,\n        Guid deletingUserId,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUser.Id)\n            .Returns(orgUser);\n        sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser.Id)))\n            .Returns(new Dictionary<Guid, bool> { { orgUser.Id, true } });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUserAsync(orgUser.OrganizationId, orgUser.Id, deletingUserId));\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveClaimedAccountErrorMessage, exception.Message);\n        await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .Received(1)\n            .GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser.Id)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WithEventSystemUser_Success(\n        [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,\n        EventSystemUser eventSystemUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        // Act\n        await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser);\n\n        // Assert\n        await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .DidNotReceiveWithAnyArgs()\n            .GetUsersOrganizationClaimedStatusAsync(default, default);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .DeleteAsync(organizationUser);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RemoveUser_WithEventSystemUser_NotFound_ThrowsException(\n        SutProvider<RemoveOrganizationUserCommand> sutProvider,\n        Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)\n    {\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RemoveUser_WithEventSystemUser_MismatchingOrganizationId_ThrowsException(\n        SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUserId)\n            .Returns(new OrganizationUser\n            {\n                Id = organizationUserId,\n                OrganizationId = Guid.NewGuid()\n            });\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_WithEventSystemUser_RemovingLastOwner_ThrowsException(\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,\n        EventSystemUser eventSystemUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),\n                Arg.Any<bool>())\n            .Returns(false);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser));\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);\n        await sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .Received(1)\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), true);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_ByUserId_Success(\n        [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),\n                Arg.Any<bool>())\n            .Returns(true);\n\n        await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .DeleteAsync(organizationUser);\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_ByUserId_NotFound_ThrowsException(\n        SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid userId)\n    {\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId));\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync((OrganizationUser)default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException(\n        OrganizationUser organizationUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)\n            .Returns(organizationUser);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.UserId.Value));\n        Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUser_ByUserId_RemovingLastOwner_ThrowsException(\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)\n            .Returns(organizationUser);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),\n                Arg.Any<bool>())\n            .Returns(false);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value));\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);\n        await sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .Received(1)\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),\n                Arg.Any<bool>());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync((OrganizationUser)default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUsers_WithDeletingUserId_Success(\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser,\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory();\n        var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;\n        orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;\n        var organizationUsers = new[] { orgUser1, orgUser2 };\n        var organizationUserIds = organizationUsers.Select(u => u.Id);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(default)\n            .ReturnsForAnyArgs(organizationUsers);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(deletingUser.Id)\n            .Returns(deletingUser);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(deletingUser.OrganizationId)\n            .Returns(true);\n        sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .GetUsersOrganizationClaimedStatusAsync(\n                deletingUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)))\n            .Returns(new Dictionary<Guid, bool> { { orgUser1.Id, false }, { orgUser2.Id, false } });\n\n        // Act\n        var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);\n\n        // Assert\n        Assert.Equal(2, result.Count());\n        Assert.All(result, r => Assert.Empty(r.ErrorMessage));\n        await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .Received(1)\n            .GetUsersOrganizationClaimedStatusAsync(\n                deletingUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventsAsync(\n                Arg.Is<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, DateTime? DateTime)>>(i =>\n                    i.First().OrganizationUser.Id == orgUser1.Id\n                    && i.Last().OrganizationUser.Id == orgUser2.Id\n                    && i.All(u => u.DateTime == eventDate)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUsers_WithDeletingUserId_WithMismatchingOrganizationId_ThrowsException(OrganizationUser organizationUser,\n        OrganizationUser deletingUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        var organizationUsers = new[] { organizationUser };\n        var organizationUserIds = organizationUsers.Select(u => u.Id);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(default)\n            .ReturnsForAnyArgs(organizationUsers);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId));\n        Assert.Contains(RemoveOrganizationUserCommand.UsersInvalidErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUsers_WithDeletingUserId_RemoveYourself_ThrowsException(\n        OrganizationUser deletingUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        var organizationUsers = new[] { deletingUser };\n        var organizationUserIds = organizationUsers.Select(u => u.Id);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(default)\n            .ReturnsForAnyArgs(organizationUsers);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);\n\n        // Assert\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveYourselfErrorMessage, result.First().ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUsers_WithDeletingUserId_NonOwnerRemoveOwner_ThrowsException(\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2,\n        [OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;\n        var organizationUsers = new[] { orgUser1, orgUser2 };\n        var organizationUserIds = organizationUsers.Select(u => u.Id);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(default)\n            .ReturnsForAnyArgs(organizationUsers);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);\n\n        // Assert\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveOwnerByNonOwnerErrorMessage, result.First().ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUsers_WithDeletingUserId_RemovingClaimedUser_ThrowsException(\n        [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser,\n        OrganizationUser deletingUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        orgUser.OrganizationId = deletingUser.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser.Id)))\n            .Returns(new[] { orgUser });\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser.Id)))\n            .Returns(new Dictionary<Guid, bool> { { orgUser.Id, true } });\n\n        // Act\n        var result = await sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUser.UserId);\n\n        // Assert\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, DateTime? DateTime)>>());\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveClaimedAccountErrorMessage, result.First().ErrorMessage);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUsers_WithDeletingUserId_LastOwner_ThrowsException(\n        [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        var organizationUsers = new[] { orgUser };\n        var organizationUserIds = organizationUsers.Select(u => u.Id);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(default)\n            .ReturnsForAnyArgs(organizationUsers);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner)\n            .Returns(organizationUsers);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null));\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUsers_WithEventSystemUser_Success(\n        EventSystemUser eventSystemUser,\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1,\n        OrganizationUser orgUser2)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory();\n        var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;\n        orgUser1.OrganizationId = orgUser2.OrganizationId;\n        var organizationUsers = new[] { orgUser1, orgUser2 };\n        var organizationUserIds = organizationUsers.Select(u => u.Id);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(default)\n            .ReturnsForAnyArgs(organizationUsers);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.RemoveUsersAsync(orgUser1.OrganizationId, organizationUserIds, eventSystemUser);\n\n        // Assert\n        Assert.Equal(2, result.Count());\n        Assert.All(result, r => Assert.Empty(r.ErrorMessage));\n        await sutProvider.GetDependency<IGetOrganizationUsersClaimedStatusQuery>()\n            .DidNotReceiveWithAnyArgs()\n            .GetUsersOrganizationClaimedStatusAsync(default, default);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id)));\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventsAsync(\n                Arg.Is<IEnumerable<(OrganizationUser OrganizationUser, EventType EventType, EventSystemUser EventSystemUser, DateTime? DateTime)>>(\n                    i => i.First().OrganizationUser.Id == orgUser1.Id\n                        && i.Last().OrganizationUser.Id == orgUser2.Id\n                        && i.All(u => u.EventSystemUser == eventSystemUser\n                            && u.DateTime == eventDate)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUsers_WithEventSystemUser_WithMismatchingOrganizationId_ThrowsException(\n        EventSystemUser eventSystemUser,\n        [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        var organizationUsers = new[] { organizationUser };\n        var organizationUserIds = organizationUsers.Select(u => u.Id);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(default)\n            .ReturnsForAnyArgs(organizationUsers);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUsersAsync(Guid.NewGuid(), organizationUserIds, eventSystemUser));\n        Assert.Contains(RemoveOrganizationUserCommand.UsersInvalidErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemoveUsers_WithEventSystemUser_LastOwner_ThrowsException(\n        [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,\n        EventSystemUser eventSystemUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        var organizationUsers = new[] { orgUser };\n        var organizationUserIds = organizationUsers.Select(u => u.Id);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(default)\n            .ReturnsForAnyArgs(organizationUsers);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner)\n            .Returns(organizationUsers);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, eventSystemUser));\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserLeave_Success(\n        [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),\n                Arg.Any<bool>())\n            .Returns(true);\n\n        await sutProvider.Sut.UserLeaveAsync(organizationUser.OrganizationId, organizationUser.UserId.Value);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .DeleteAsync(organizationUser);\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Left);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserLeave_NotFound_ThrowsException(SutProvider<RemoveOrganizationUserCommand> sutProvider,\n        Guid organizationId, Guid userId)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UserLeaveAsync(organizationId, userId));\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync((OrganizationUser)default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserLeave_InvalidUser_ThrowsException(OrganizationUser organizationUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)\n            .Returns(organizationUser);\n\n        var exception = await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UserLeaveAsync(Guid.NewGuid(), organizationUser.UserId.Value));\n\n        Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync((OrganizationUser)default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UserLeave_RemovingLastOwner_ThrowsException(\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,\n        SutProvider<RemoveOrganizationUserCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)\n            .Returns(organizationUser);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),\n                Arg.Any<bool>())\n            .Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UserLeaveAsync(organizationUser.OrganizationId, organizationUser.UserId.Value));\n\n        Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);\n        _ = sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .Received(1)\n            .HasConfirmedOwnersExceptAsync(\n                organizationUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),\n                Arg.Any<bool>());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync((OrganizationUser)default, default);\n    }\n\n    /// <summary>\n    /// Returns a new SutProvider with a FakeTimeProvider registered in the Sut.\n    /// </summary>\n    private static SutProvider<RemoveOrganizationUserCommand> SutProviderFactory()\n    {\n        return new SutProvider<RemoveOrganizationUserCommand>()\n            .WithFakeTimeProvider()\n            .Create();\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser;\n\n[SutProviderCustomize]\npublic class RestoreOrganizationUserCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task RestoreUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncOrgKeysAsync(organizationUser.UserId!.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        RestoreUser_Setup(organization, null, organizationUser, sutProvider);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncOrgKeysAsync(organizationUser.UserId!.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_RestoreThemselves_Fails(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.UserId = owner.Id;\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));\n\n        Assert.Contains(\"you cannot restore yourself\", exception.Message.ToLowerInvariant());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushSyncOrgKeysAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task RestoreUser_AdminRestoreOwner_Fails(OrganizationUserType restoringUserType,\n        Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser restoringUser,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        restoringUser.Type = restoringUserType;\n        RestoreUser_Setup(organization, restoringUser, organizationUser, sutProvider);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id, null));\n\n        Assert.Contains(\"only owners can restore other owners\", exception.Message.ToLowerInvariant());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushSyncOrgKeysAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Invited)]\n    [BitAutoData(OrganizationUserStatusType.Accepted)]\n    [BitAutoData(OrganizationUserStatusType.Confirmed)]\n    public async Task RestoreUser_WithStatusOtherThanRevoked_Fails(OrganizationUserStatusType userStatus, Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.Status = userStatus;\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));\n\n        Assert.Contains(\"already active\", exception.Message.ToLowerInvariant());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushSyncOrgKeysAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.Email = null;\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) });\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())\n            .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });\n\n        var user = new User();\n        user.Email = \"test@bitwarden.com\";\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));\n\n        Assert.Contains(\"test@bitwarden.com is not compliant with the two-step login policy\", exception.Message.ToLowerInvariant());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushSyncOrgKeysAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.Email = null;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) });\n\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = organizationUser.OrganizationId,\n                    OrganizationUserStatus = OrganizationUserStatusType.Revoked,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]));\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(organizationUser.UserId.Value)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        var user = new User();\n        user.Email = \"test@bitwarden.com\";\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));\n\n        Assert.Contains(\"test@bitwarden.com is not compliant with the two-step login policy\", exception.Message.ToLowerInvariant());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushSyncOrgKeysAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())\n            .Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithUser2FAConfigured_Success(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = organizationUser.OrganizationId,\n                    OrganizationUserStatus = OrganizationUserStatusType.Revoked,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]));\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(organizationUser.UserId.Value)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithPolicyRequirementsEnabled_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke\n        secondOrganizationUser.UserId = organizationUser.UserId;\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(organizationUser.UserId.Value)\n            .Returns(new[] { organizationUser, secondOrganizationUser });\n\n        // Mock SingleOrganizationPolicyRequirement via IPolicyRequirementQuery (new path)\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(organizationUser.UserId.Value)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForTargetOrganization(organization.Id));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = organizationUser.OrganizationId,\n                    OrganizationUserStatus = OrganizationUserStatusType.Revoked,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]));\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        var user = new User { Email = \"test@bitwarden.com\" };\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));\n\n        Assert.Contains(\"test@bitwarden.com is not compliant with the single organization and two-step login policy\", exception.Message.ToLowerInvariant());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushSyncOrgKeysAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithOtherOrgSingleOrgPolicy_Fails(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        OrganizationUser otherOrganizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.Email = null; // required to mock that user was previously confirmed\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n\n        // Other org has SingleOrg policy (not the target org)\n        otherOrganizationUser.OrganizationId = Guid.NewGuid();\n        otherOrganizationUser.UserId = organizationUser.UserId;\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(organizationUser.UserId.Value)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForAnotherOrganization());\n\n        // No 2FA policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)\n            .Returns(new RequireTwoFactorPolicyRequirement([]));\n\n        var user = new User { Email = \"test@bitwarden.com\" };\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));\n\n        Assert.Contains(\"test@bitwarden.com cannot be restored because they are in another organization which forbids it.\", exception.Message.ToLowerInvariant());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUse_WithSingleOrgPolicyEnabled_Fails(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.Email = null; // required to mock that user was previously confirmed\n        secondOrganizationUser.UserId = organizationUser.UserId;\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(organizationUser.UserId.Value)\n            .Returns(new[] { organizationUser, secondOrganizationUser });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });\n\n        // Target org has SingleOrg policy, user is a regular User (not exempt)\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(organizationUser.UserId.Value)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForTargetOrganization(organization.Id));\n\n        var user = new User { Email = \"test@bitwarden.com\" };\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));\n\n        Assert.Contains(\"test@bitwarden.com cannot be restored until they leave or remove all other organizations.\", exception.Message.ToLowerInvariant());\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())\n            .Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }\n            ]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });\n\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WhenUserOwningAnotherFreeOrganization_ThenRestoreUserFails(\n        Organization organization,\n        Organization otherOrganization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser organizationUser,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUserOwnerFromDifferentOrg,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organization.PlanType = PlanType.Free;\n        organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke\n\n        orgUserOwnerFromDifferentOrg.UserId = organizationUser.UserId;\n        otherOrganization.Id = orgUserOwnerFromDifferentOrg.OrganizationId;\n        otherOrganization.PlanType = PlanType.Free;\n\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(organizationUser.UserId.Value)\n            .Returns([orgUserOwnerFromDifferentOrg]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetManyByUserIdAsync(organizationUser.UserId.Value)\n            .Returns([otherOrganization]);\n\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())\n            .Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }\n            ]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));\n\n        Assert.Equal(\"User is an owner/admin of another free organization. Please have them upgrade to a paid plan to restore their account.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WhenUserOwningAnotherFreeOrganizationAndIsOnlyAUserInCurrentOrg_ThenUserShouldBeRestored(\n        Organization organization,\n        Organization otherOrganization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUserOwnerFromDifferentOrg,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organization.PlanType = PlanType.Free;\n        organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke\n\n        orgUserOwnerFromDifferentOrg.UserId = organizationUser.UserId;\n        otherOrganization.Id = orgUserOwnerFromDifferentOrg.OrganizationId;\n        otherOrganization.PlanType = PlanType.Free;\n\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        organizationUserRepository\n            .GetManyByUserAsync(organizationUser.UserId.Value)\n            .Returns([orgUserOwnerFromDifferentOrg]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetManyByUserIdAsync(organizationUser.UserId.Value)\n            .Returns([otherOrganization]);\n\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication,\n                Arg.Any<OrganizationUserStatusType>())\n            .Returns([\n                new OrganizationUserPolicyDetails\n                {\n                    OrganizationId = organizationUser.OrganizationId,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });\n\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);\n\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(organizationUser.Id,\n                Arg.Is<OrganizationUserStatusType>(x => x != OrganizationUserStatusType.Revoked));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WhenUserOwningAnotherFreeOrganizationAndCurrentOrgIsNotFree_ThenUserShouldBeRestored(\n        Organization organization,\n        Organization otherOrganization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser organizationUser,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUserOwnerFromDifferentOrg,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organization.PlanType = PlanType.EnterpriseAnnually2023;\n\n        organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke\n\n        orgUserOwnerFromDifferentOrg.UserId = organizationUser.UserId;\n        otherOrganization.Id = orgUserOwnerFromDifferentOrg.OrganizationId;\n        otherOrganization.PlanType = PlanType.Free;\n\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        organizationUserRepository\n            .GetManyByUserAsync(organizationUser.UserId.Value)\n            .Returns([orgUserOwnerFromDifferentOrg]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetManyByUserIdAsync(organizationUser.UserId.Value)\n            .Returns([otherOrganization]);\n\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication,\n                Arg.Any<OrganizationUserStatusType>())\n            .Returns([\n                new OrganizationUserPolicyDetails\n                {\n                    OrganizationId = organizationUser.OrganizationId,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });\n\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);\n\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(organizationUser.Id,\n                Arg.Is<OrganizationUserStatusType>(x => x != OrganizationUserStatusType.Revoked));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_InvitedUserInFreeOrganization_Success(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        organization.PlanType = PlanType.Free;\n        organizationUser.UserId = null;\n        organizationUser.Key = null;\n        organizationUser.Status = OrganizationUserStatusType.Revoked;\n\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, \"\");\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushSyncOrgKeysAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithAutoConfirmPolicyEnabled_DeletesEmergencyAccess(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.Email = null;\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        var user = new User { Id = organizationUser.UserId!.Value, Email = \"test@bitwarden.com\" };\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = organization.Id }]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id, [organizationUser], user)));\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .Received(1)\n            .DeleteAllByUserIdAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithAutoConfirmPolicyNotEnabled_DoesNotDeleteEmergencyAccess(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.Email = null;\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        var user = new User { Id = organizationUser.UserId!.Value, Email = \"test@bitwarden.com\" };\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Valid(new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id, [organizationUser], user)));\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithAutoConfirmNonCompliant_DoesNotDeleteEmergencyAccess(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.Email = null;\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        var user = new User { Id = organizationUser.UserId!.Value, Email = \"test@bitwarden.com\" };\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([new PolicyDetails { OrganizationId = organization.Id }]));\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationPolicyEnforcementValidator>()\n            .IsCompliantAsync(Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(), Arg.Any<AutomaticUserConfirmationPolicyRequirement>())\n            .Returns(Invalid(\n                new AutomaticUserConfirmationPolicyEnforcementRequest(organization.Id, [organizationUser], user),\n                new UserCannotBelongToAnotherOrganization()));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));\n\n        Assert.Contains(\"is not compliant with the automatic user confirmation policy\", exception.Message);\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsers_Success(Organization organization,\n    [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n    [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,\n    [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,\n    SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var eventService = sutProvider.GetDependency<IEventService>();\n        var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();\n        var userService = Substitute.For<IUserService>();\n\n        orgUser1.Email = orgUser2.Email = null; // Mock that users were previously confirmed\n        orgUser1.OrganizationId = orgUser2.OrganizationId = organization.Id;\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))\n            .Returns([orgUser1, orgUser2]);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        twoFactorIsEnabledQuery\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true),\n                (orgUser2.UserId!.Value, false)\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, new[] { orgUser1.Id, orgUser2.Id }, owner.Id, userService, null);\n\n        // Assert\n        Assert.Equal(2, result.Count);\n        Assert.All(result, r => Assert.Empty(r.Item2)); // No error messages\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(orgUser2.Id, OrganizationUserStatusType.Confirmed);\n        await eventService.Received(1)\n            .LogOrganizationUserEventAsync(orgUser1, EventType.OrganizationUser_Restored);\n        await eventService.Received(1)\n            .LogOrganizationUserEventAsync(orgUser2, EventType.OrganizationUser_Restored);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsers_With2FAPolicy_BlocksNonCompliantUser(Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var policyService = sutProvider.GetDependency<IPolicyService>();\n        var userService = Substitute.For<IUserService>();\n\n        orgUser1.Email = orgUser2.Email = null;\n        orgUser3.UserId = null;\n        orgUser3.Key = null;\n        orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id;\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))\n            .Returns(new[] { orgUser1, orgUser2, orgUser3 });\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = \"test@example.com\" });\n\n        // Setup 2FA policy\n        policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())\n            .Returns([new OrganizationUserPolicyDetails { OrganizationId = organization.Id, PolicyType = PolicyType.TwoFactorAuthentication }]);\n\n        // User1 has 2FA, User2 doesn't\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true),\n                (orgUser2.UserId!.Value, false)\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService, null);\n\n        // Assert\n        Assert.Equal(3, result.Count);\n        Assert.Empty(result[0].Item2); // First user should succeed\n        Assert.Contains(\"two-step login\", result[1].Item2); // Second user should fail\n        Assert.Empty(result[2].Item2); // Third user should succeed\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);\n        await organizationUserRepository\n            .DidNotReceive()\n            .RestoreAsync(orgUser2.Id, Arg.Any<OrganizationUserStatusType>());\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsers_WithPolicyRequirementsEnabled_With2FAPolicy_BlocksNonCompliantUser(Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var policyService = sutProvider.GetDependency<IPolicyService>();\n        var userService = Substitute.For<IUserService>();\n\n        orgUser1.Email = orgUser2.Email = null;\n        orgUser3.UserId = null;\n        orgUser3.Key = null;\n        orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id;\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))\n            .Returns(new[] { orgUser1, orgUser2, orgUser3 });\n\n        userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = \"test@example.com\" });\n\n        // Setup 2FA policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(Arg.Any<Guid>())\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = organization.Id,\n                    OrganizationUserStatus = OrganizationUserStatusType.Revoked,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]));\n\n        // No SingleOrg policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(Arg.Any<Guid>())\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // User1 has 2FA, User2 doesn't\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true),\n                (orgUser2.UserId!.Value, false)\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService, null);\n\n        // Assert\n        Assert.Equal(3, result.Count);\n        Assert.Empty(result[0].Item2); // First user should succeed\n        Assert.Contains(\"two-step login\", result[1].Item2); // Second user should fail\n        Assert.Empty(result[2].Item2); // Third user should succeed\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);\n        await organizationUserRepository\n            .DidNotReceive()\n            .RestoreAsync(orgUser2.Id, Arg.Any<OrganizationUserStatusType>());\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsers_UserOwnsAnotherFreeOrganization_BlocksOwnerUserFromBeingRestored(Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUserFromOtherOrg,\n        Organization otherOrganization,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var policyService = sutProvider.GetDependency<IPolicyService>();\n        var userService = Substitute.For<IUserService>();\n\n        orgUser1.Email = orgUser2.Email = null;\n        orgUser3.UserId = null;\n        orgUser3.Key = null;\n        orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id;\n\n        orgUserFromOtherOrg.UserId = orgUser1.UserId;\n        otherOrganization.Id = orgUserFromOtherOrg.OrganizationId;\n        otherOrganization.PlanType = PlanType.Free;\n\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))\n            .Returns([orgUser1, orgUser2, orgUser3]);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = \"test@example.com\" });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([orgUserFromOtherOrg]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetManyByIdsAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUserFromOtherOrg.OrganizationId)))\n            .Returns([otherOrganization]);\n\n\n        // Setup 2FA policy\n        policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())\n            .Returns([new OrganizationUserPolicyDetails { OrganizationId = organization.Id, PolicyType = PolicyType.TwoFactorAuthentication }]);\n\n        // User1 has 2FA, User2 doesn't\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true),\n                (orgUser2.UserId!.Value, false)\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService, null);\n\n        // Assert\n        Assert.Equal(3, result.Count);\n        Assert.Contains(\"owner\", result[0].Item2); // Owner should fail\n        await organizationUserRepository\n            .DidNotReceive()\n            .RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsers_UserOwnsAnotherFreeOrganizationButReactivatingOrgIsPaid_RestoresUser(Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUserFromOtherOrg,\n        Organization otherOrganization,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually2023;\n\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var policyService = sutProvider.GetDependency<IPolicyService>();\n        var userService = Substitute.For<IUserService>();\n\n        orgUser1.OrganizationId = organization.Id;\n\n        orgUserFromOtherOrg.UserId = orgUser1.UserId;\n\n        otherOrganization.Id = orgUserFromOtherOrg.OrganizationId;\n        otherOrganization.PlanType = PlanType.Free;\n\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id)))\n            .Returns([orgUser1]);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        organizationUserRepository\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([orgUserFromOtherOrg]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetManyByIdsAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUserFromOtherOrg.OrganizationId)))\n            .Returns([otherOrganization]);\n\n\n        // Setup 2FA policy\n        policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())\n            .Returns([new OrganizationUserPolicyDetails { OrganizationId = organization.Id, PolicyType = PolicyType.TwoFactorAuthentication }]);\n\n        // User1 has 2FA, User2 doesn't\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value)))\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true)\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id], owner.Id, userService, null);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(string.Empty, result[0].Item2);\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(orgUser1.Id, Arg.Is<OrganizationUserStatusType>(x => x != OrganizationUserStatusType.Revoked));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RestoreUsers_UserOwnsAnotherOrganizationButIsOnlyUserOfCurrentOrganization_UserShouldBeRestored(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUserFromOtherOrg,\n        Organization otherOrganization,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.Free;\n\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var userService = Substitute.For<IUserService>();\n\n        orgUser1.OrganizationId = organization.Id;\n\n        orgUserFromOtherOrg.UserId = orgUser1.UserId;\n\n        otherOrganization.Id = orgUserFromOtherOrg.OrganizationId;\n        otherOrganization.PlanType = PlanType.Free;\n\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id)))\n            .Returns([orgUser1]);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        organizationUserRepository\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([orgUserFromOtherOrg]);\n\n        sutProvider.GetDependency<IPolicyService>().GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())\n            .Returns([new OrganizationUserPolicyDetails { OrganizationId = organization.Id, PolicyType = PolicyType.TwoFactorAuthentication }]);\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id], owner.Id, userService, null);\n\n        Assert.Single(result);\n        Assert.Equal(string.Empty, result[0].Item2);\n        await organizationUserRepository\n            .Received(1)\n            .RestoreAsync(orgUser1.Id, Arg.Is<OrganizationUserStatusType>(x => x != OrganizationUserStatusType.Revoked));\n    }\n\n    private static void RestoreUser_Setup(\n        Organization organization,\n        OrganizationUser? requestingOrganizationUser,\n        OrganizationUser targetOrganizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        if (requestingOrganizationUser != null)\n        {\n            requestingOrganizationUser.OrganizationId = organization.Id;\n        }\n        targetOrganizationUser.OrganizationId = organization.Id;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n        {\n            Sponsored = 0,\n            Users = 1\n        });\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner);\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin));\n\n        // Setup default disabled OrganizationDataOwnershipPolicyRequirement for any user\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(Arg.Any<Guid>())\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, []));\n\n        // Setup default empty SingleOrganizationPolicyRequirement for any user\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(Arg.Any<Guid>())\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n    }\n\n    private static void SetupOrganizationDataOwnershipPolicy(\n        SutProvider<RestoreOrganizationUserCommand> sutProvider,\n        Guid userId,\n        Guid organizationId,\n        OrganizationUserStatusType orgUserStatus,\n        bool policyEnabled)\n    {\n        var policyDetails = policyEnabled\n            ? new List<PolicyDetails>\n              {\n                  new()\n                  {\n                      OrganizationId = organizationId,\n                      OrganizationUserId = Guid.NewGuid(),\n                      OrganizationUserStatus = orgUserStatus,\n                      PolicyType = PolicyType.OrganizationDataOwnership\n                  }\n              }\n            : new List<PolicyDetails>();\n\n        var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            policyEnabled ? OrganizationDataOwnershipState.Enabled : OrganizationDataOwnershipState.Disabled,\n            policyDetails);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)\n            .Returns(policyRequirement);\n    }\n\n    #region Single User Restore - Default Collection Tests\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithDataOwnershipPolicyEnabled_AndConfirmedUser_CreatesDefaultCollection(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        string defaultCollectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.Email = null; // This causes user to restore to Confirmed status\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        SetupOrganizationDataOwnershipPolicy(\n            sutProvider,\n            organizationUser.UserId!.Value,\n            organization.Id,\n            OrganizationUserStatusType.Revoked,\n            policyEnabled: true);\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == organizationUser.Id),\n                defaultCollectionName);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithDataOwnershipPolicyDisabled_DoesNotCreateDefaultCollection(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        string defaultCollectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.Email = null; // This causes user to restore to Confirmed status\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        SetupOrganizationDataOwnershipPolicy(\n            sutProvider,\n            organizationUser.UserId!.Value,\n            organization.Id,\n            OrganizationUserStatusType.Revoked,\n            policyEnabled: false);\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithNullDefaultCollectionName_DoesNotCreateDefaultCollection(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.Email = null; // This causes user to restore to Confirmed status\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        SetupOrganizationDataOwnershipPolicy(\n            sutProvider,\n            organizationUser.UserId!.Value,\n            organization.Id,\n            OrganizationUserStatusType.Revoked,\n            policyEnabled: true);\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData(\"\")]\n    [BitAutoData(\"   \")]\n    public async Task RestoreUser_WithEmptyOrWhitespaceDefaultCollectionName_DoesNotCreateDefaultCollection(\n        string defaultCollectionName,\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.Email = null; // This causes user to restore to Confirmed status\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        SetupOrganizationDataOwnershipPolicy(\n            sutProvider,\n            organizationUser.UserId!.Value,\n            organization.Id,\n            OrganizationUserStatusType.Revoked,\n            policyEnabled: true);\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_UserRestoredToInvitedStatus_DoesNotCreateDefaultCollection(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        string defaultCollectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually; // Non-Free plan to avoid ownership check requiring UserId\n        organizationUser.Email = \"test@example.com\"; // Non-null email means user restores to Invited status\n        organizationUser.UserId = null; // User not linked to account yet\n        organizationUser.Key = null;\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);\n\n        // Assert - User was restored to Invited status, so no collection should be created\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUser_WithNoUserId_DoesNotCreateDefaultCollection(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,\n        string defaultCollectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually; // Non-Free plan to avoid ownership check requiring UserId\n        organizationUser.UserId = null; // No linked user account\n        organizationUser.Email = \"test@example.com\";\n        organizationUser.Key = null;\n        RestoreUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    #endregion\n\n    #region Bulk User Restore - Default Collection Tests\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsers_Bulk_WithDataOwnershipPolicy_CreatesCollectionsForEligibleUsers(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,\n        string defaultCollectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var userService = Substitute.For<IUserService>();\n\n        // orgUser1: Will restore to Confirmed (Email = null)\n        orgUser1.Email = null;\n        orgUser1.OrganizationId = organization.Id;\n\n        // orgUser2: Will restore to Invited (Email not null)\n        orgUser2.Email = \"test@example.com\";\n        orgUser2.UserId = null;\n        orgUser2.Key = null;\n        orgUser2.OrganizationId = organization.Id;\n\n        var orgUser1PolicyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            OrganizationDataOwnershipState.Enabled,\n            [new PolicyDetails { OrganizationId = organization.Id, OrganizationUserId = orgUser1.Id }]);\n\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))\n            .Returns([orgUser1, orgUser2]);\n\n        // Setup bulk policy query - returns org user IDs with policy enabled\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId.Value)))\n            .Returns([(orgUser1.UserId!.Value, orgUser1PolicyRequirement)]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true)\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(\n            organization.Id,\n            [orgUser1.Id, orgUser2.Id],\n            owner.Id,\n            userService,\n            defaultCollectionName);\n\n        // Assert - Only orgUser1 should have a collection created (Confirmed with policy enabled)\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser1.Id),\n                defaultCollectionName);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsers_Bulk_WithMixedPolicyStates_OnlyCreatesForEnabledPolicy(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,\n        string defaultCollectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var userService = Substitute.For<IUserService>();\n\n        // Both users will restore to Confirmed\n        orgUser1.Email = null;\n        orgUser1.OrganizationId = organization.Id;\n        orgUser2.Email = null;\n        orgUser2.OrganizationId = organization.Id;\n\n        var orgUser1PolicyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            OrganizationDataOwnershipState.Enabled,\n            [new PolicyDetails { OrganizationId = organization.Id, OrganizationUserId = orgUser1.Id }]);\n\n        var orgUser2PolicyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            OrganizationDataOwnershipState.Disabled, []);\n\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))\n            .Returns([orgUser1, orgUser2]);\n\n        // Setup bulk policy query - only orgUser1 has policy enabled\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))\n            .Returns([(orgUser1.UserId!.Value, orgUser1PolicyRequirement), (orgUser2.UserId!.Value, orgUser2PolicyRequirement)]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true),\n                (orgUser2.UserId!.Value, true)\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(\n            organization.Id,\n            [orgUser1.Id, orgUser2.Id],\n            owner.Id,\n            userService,\n            defaultCollectionName);\n\n        // Assert - Only orgUser1 should have a collection created (policy enabled)\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser1.Id),\n                defaultCollectionName);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsers_Bulk_WithNullCollectionName_DoesNotCreateAnyCollections(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var userService = Substitute.For<IUserService>();\n\n        // Both users will restore to Confirmed\n        orgUser1.Email = null;\n        orgUser1.OrganizationId = organization.Id;\n        orgUser2.Email = null;\n        orgUser2.OrganizationId = organization.Id;\n\n        var orgUser1PolicyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            OrganizationDataOwnershipState.Enabled,\n            [new PolicyDetails { OrganizationId = organization.Id, OrganizationUserId = orgUser1.Id }]);\n\n        var orgUser2PolicyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            OrganizationDataOwnershipState.Enabled,\n            [new PolicyDetails { OrganizationId = organization.Id, OrganizationUserId = orgUser2.Id }]);\n\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))\n            .Returns([orgUser1, orgUser2]);\n\n        // Setup bulk policy query - both users have policy enabled\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))\n            .Returns([(orgUser1.UserId!.Value, orgUser1PolicyRequirement), (orgUser2.UserId!.Value, orgUser2PolicyRequirement)]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true),\n                (orgUser2.UserId!.Value, true)\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(\n            organization.Id,\n            [orgUser1.Id, orgUser2.Id],\n            owner.Id,\n            userService,\n            null); // Null collection name\n\n        // Assert - No collections should be created\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    #endregion\n\n    #region UseMyItems Tests\n\n    [Theory, BitAutoData]\n    public async Task RestoreUserAsync_UseMyItemsDisabled_DoesNotCreateCollection(\n        Organization organization,\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(status: OrganizationUserStatusType.Revoked)] OrganizationUser orgUser,\n        string collectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser, sutProvider);\n        organization.UseMyItems = false;\n\n        // User will restore to Confirmed\n        orgUser.Email = null;\n        orgUser.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, []));\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(orgUser, owner.Id, collectionName);\n\n        // Assert - No collection should be created\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUserAsync_UseMyItemsEnabled_CreatesCollection(\n        Organization organization,\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(status: OrganizationUserStatusType.Revoked)] OrganizationUser orgUser,\n        string collectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser, sutProvider);\n        organization.UseMyItems = true;\n\n        // User will restore to Confirmed\n        orgUser.Email = null;\n        orgUser.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(orgUser.UserId!.Value)\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, []));\n\n        // Act\n        await sutProvider.Sut.RestoreUserAsync(orgUser, owner.Id, collectionName);\n\n        // Assert - Collection should be created\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser.Id),\n                collectionName);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsersAsync_UseMyItemsDisabled_DoesNotCreateCollections(\n        Organization organization,\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(status: OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,\n        [OrganizationUser(status: OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,\n        string collectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        organization.UseMyItems = false;\n\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var userService = Substitute.For<IUserService>();\n\n        // Both users will restore to Confirmed\n        orgUser1.Email = null;\n        orgUser1.OrganizationId = organization.Id;\n        orgUser2.Email = null;\n        orgUser2.OrganizationId = organization.Id;\n\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))\n            .Returns([orgUser1, orgUser2]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true),\n                (orgUser2.UserId!.Value, true)\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(\n            organization.Id,\n            [orgUser1.Id, orgUser2.Id],\n            owner.Id,\n            userService,\n            collectionName);\n\n        // Assert - No collections should be created\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RestoreUsersAsync_UseMyItemsEnabled_CreatesCollections(\n        Organization organization,\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser(status: OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,\n        [OrganizationUser(status: OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,\n        string collectionName,\n        SutProvider<RestoreOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        RestoreUser_Setup(organization, owner, orgUser1, sutProvider);\n        organization.UseMyItems = true;\n\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var userService = Substitute.For<IUserService>();\n\n        // Both users will restore to Confirmed\n        orgUser1.Email = null;\n        orgUser1.OrganizationId = organization.Id;\n        orgUser2.Email = null;\n        orgUser2.OrganizationId = organization.Id;\n\n        organizationUserRepository\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))\n            .Returns([orgUser1, orgUser2]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>\n            {\n                (orgUser1.UserId!.Value, true),\n                (orgUser2.UserId!.Value, true)\n            });\n\n        var policyDetails1 = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            OrganizationUserId = orgUser1.Id,\n            IsProvider = false,\n            OrganizationUserStatus = OrganizationUserStatusType.Confirmed,\n            OrganizationUserType = orgUser1.Type,\n            PolicyType = PolicyType.OrganizationDataOwnership\n        };\n        var policyDetails2 = new PolicyDetails\n        {\n            OrganizationId = organization.Id,\n            OrganizationUserId = orgUser2.Id,\n            IsProvider = false,\n            OrganizationUserStatus = OrganizationUserStatusType.Confirmed,\n            OrganizationUserType = orgUser2.Type,\n            PolicyType = PolicyType.OrganizationDataOwnership\n        };\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))\n            .Returns([\n                (orgUser1.UserId!.Value, new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails1])),\n                (orgUser2.UserId!.Value, new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails2]))\n            ]);\n\n        // Act\n        var result = await sutProvider.Sut.RestoreUsersAsync(\n            organization.Id,\n            [orgUser1.Id, orgUser2.Id],\n            owner.Id,\n            userService,\n            collectionName);\n\n        // Assert - Collections should be created for both confirmed users\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateDefaultCollectionsAsync(\n                organization.Id,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 2 && ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)),\n                collectionName);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class RevokeNonCompliantOrganizationUserCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task RevokeNonCompliantOrganizationUsersAsync_GivenUnrecognizedUserType_WhenAttemptingToRevoke_ThenErrorShouldBeReturned(\n            Guid organizationId, SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)\n    {\n        var command = new RevokeOrganizationUsersRequest(organizationId, [], new InvalidUser());\n\n        var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);\n\n        Assert.True(result.HasErrors);\n        Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorRequestedByWasNotValid, result.ErrorMessages);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeThemselves_ThenErrorShouldBeReturned(\n            Guid organizationId, OrganizationUserUserDetails revokingUser,\n            SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)\n    {\n        var command = new RevokeOrganizationUsersRequest(organizationId, revokingUser,\n            new StandardUser(revokingUser?.UserId ?? Guid.NewGuid(), true));\n\n        var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);\n\n        Assert.True(result.HasErrors);\n        Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorCannotRevokeSelf, result.ErrorMessages);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeOrgUsersFromAnotherOrg_ThenErrorShouldBeReturned(\n            Guid organizationId, OrganizationUserUserDetails userFromAnotherOrg,\n            SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)\n    {\n        userFromAnotherOrg.OrganizationId = Guid.NewGuid();\n\n        var command = new RevokeOrganizationUsersRequest(organizationId, userFromAnotherOrg,\n            new StandardUser(Guid.NewGuid(), true));\n\n        var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);\n\n        Assert.True(result.HasErrors);\n        Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorInvalidUsers, result.ErrorMessages);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeAllOwnersFromOrg_ThenErrorShouldBeReturned(\n            Guid organizationId, OrganizationUserUserDetails userToRevoke,\n            SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)\n    {\n        userToRevoke.OrganizationId = organizationId;\n\n        var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke,\n            new StandardUser(Guid.NewGuid(), true));\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(false);\n\n        var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);\n\n        Assert.True(result.HasErrors);\n        Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorOrgMustHaveAtLeastOneOwner, result.ErrorMessages);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeOwnerWhenNotAnOwner_ThenErrorShouldBeReturned(\n        Guid organizationId, OrganizationUserUserDetails userToRevoke,\n        SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)\n    {\n        userToRevoke.OrganizationId = organizationId;\n        userToRevoke.Type = OrganizationUserType.Owner;\n\n        var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke,\n            new StandardUser(Guid.NewGuid(), false));\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);\n\n        Assert.True(result.HasErrors);\n        Assert.Contains(RevokeNonCompliantOrganizationUserCommand.ErrorOnlyOwnersCanRevokeOtherOwners, result.ErrorMessages);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserAttemptsToRevokeUserWhoIsAlreadyRevoked_ThenErrorShouldBeReturned(\n        Guid organizationId, OrganizationUserUserDetails userToRevoke,\n        SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)\n    {\n        userToRevoke.OrganizationId = organizationId;\n        userToRevoke.Status = OrganizationUserStatusType.Revoked;\n\n        var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke,\n            new StandardUser(Guid.NewGuid(), true));\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);\n\n        Assert.True(result.HasErrors);\n        Assert.Contains($\"{RevokeNonCompliantOrganizationUserCommand.ErrorUserAlreadyRevoked} Id: {userToRevoke.Id}\", result.ErrorMessages);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RevokeNonCompliantOrganizationUsersAsync_GivenPopulatedRequest_WhenUserHasMultipleInvalidUsers_ThenErrorShouldBeReturned(\n        Guid organizationId, IEnumerable<OrganizationUserUserDetails> usersToRevoke,\n        SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)\n    {\n        var revocableUsers = usersToRevoke.ToList();\n        revocableUsers.ForEach(user => user.OrganizationId = organizationId);\n        revocableUsers[0].Type = OrganizationUserType.Owner;\n        revocableUsers[1].Status = OrganizationUserStatusType.Revoked;\n\n        var command = new RevokeOrganizationUsersRequest(organizationId, revocableUsers,\n            new StandardUser(Guid.NewGuid(), false));\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);\n\n        Assert.True(result.HasErrors);\n        Assert.True(result.ErrorMessages.Count > 1);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RevokeNonCompliantOrganizationUsersAsync_GivenValidPopulatedRequest_WhenUserAttemptsToRevokeAUser_ThenUserShouldBeRevoked(\n        Guid organizationId, OrganizationUserUserDetails userToRevoke,\n        SutProvider<RevokeNonCompliantOrganizationUserCommand> sutProvider)\n    {\n        userToRevoke.OrganizationId = organizationId;\n        userToRevoke.Type = OrganizationUserType.Admin;\n\n        var command = new RevokeOrganizationUsersRequest(organizationId, userToRevoke,\n            new StandardUser(Guid.NewGuid(), false));\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.RevokeNonCompliantOrganizationUsersAsync(command);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RevokeManyByIdAsync(Arg.Is<IEnumerable<Guid>>(x => x.Count() == 1 && x.Contains(userToRevoke.Id)));\n\n        Assert.True(result.Success);\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventsAsync(\n                Arg.Is<IEnumerable<(OrganizationUserUserDetails organizationUser, EventType eventType, DateTime? time\n                    )>>(\n                    x => x.Any(y =>\n                        y.organizationUser.Id == userToRevoke.Id && y.eventType == EventType.OrganizationUser_Revoked)\n                ));\n    }\n\n    public class InvalidUser : IActingUser\n    {\n        public Guid? UserId => Guid.Empty;\n        public bool IsOrganizationOwnerOrProvider => false;\n        public EventSystemUser? SystemUserType => null;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeOrganizationUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class RevokeOrganizationUserCommandTests\n{\n\n    [Theory, BitAutoData]\n    public async Task RevokeUser_Success(\n        Organization organization,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        [OrganizationUser] OrganizationUser organizationUser,\n        SutProvider<RevokeOrganizationUserCommand> sutProvider)\n    {\n        RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);\n\n        await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RevokeAsync(organizationUser.Id);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncOrgKeysAsync(organizationUser.UserId!.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RevokeUser_WithEventSystemUser_Success(\n        Organization organization,\n        [OrganizationUser] OrganizationUser organizationUser,\n        EventSystemUser eventSystemUser,\n        SutProvider<RevokeOrganizationUserCommand> sutProvider)\n    {\n        RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider);\n\n        await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RevokeAsync(organizationUser.Id);\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncOrgKeysAsync(organizationUser.UserId!.Value);\n    }\n\n    private void RestoreRevokeUser_Setup(\n        Organization organization,\n        OrganizationUser? requestingOrganizationUser,\n        OrganizationUser targetOrganizationUser,\n        SutProvider<RevokeOrganizationUserCommand> sutProvider)\n    {\n        if (requestingOrganizationUser != null)\n        {\n            requestingOrganizationUser.OrganizationId = organization.Id;\n        }\n        targetOrganizationUser.OrganizationId = organization.Id;\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/RevokeOrganizationUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\n[SutProviderCustomize]\npublic class RevokeOrganizationUserCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeUsersAsync_WithValidUsers_RevokesUsersAndLogsEvents(\n        SutProvider<RevokeOrganizationUserCommand> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)\n    {\n        // Arrange\n        orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;\n        orgUser1.UserId = Guid.NewGuid();\n        orgUser2.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, false, null);\n        var request = new RevokeOrganizationUsersRequest(\n            organizationId,\n            [orgUser1.Id, orgUser2.Id],\n            actingUser);\n\n        SetupRepositoryMocks(sutProvider, [orgUser1, orgUser2]);\n        SetupValidatorMock(sutProvider, [\n            ValidationResultHelpers.Valid(orgUser1),\n            ValidationResultHelpers.Valid(orgUser2)\n        ]);\n\n        // Act\n        var results = (await sutProvider.Sut.RevokeUsersAsync(request)).ToList();\n\n        // Assert\n        Assert.Equal(2, results.Count);\n        Assert.All(results, r => Assert.True(r.Result.IsSuccess));\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RevokeManyByIdAsync(Arg.Is<IEnumerable<Guid>>(ids =>\n                ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)));\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(\n                events => events.Count() == 2));\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncOrgKeysAsync(orgUser1.UserId!.Value);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncOrgKeysAsync(orgUser2.UserId!.Value);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeUsersAsync_WithSystemUser_LogsEventsWithSystemUserType(\n        SutProvider<RevokeOrganizationUserCommand> sutProvider,\n        Guid organizationId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)\n    {\n        // Arrange\n        orgUser.OrganizationId = organizationId;\n        orgUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(null, false, EventSystemUser.SCIM);\n\n        var request = new RevokeOrganizationUsersRequest(\n            organizationId,\n            [orgUser.Id],\n            actingUser);\n\n        SetupRepositoryMocks(sutProvider, [orgUser]);\n        SetupValidatorMock(sutProvider, [ValidationResultHelpers.Valid(orgUser)]);\n\n        // Act\n        await sutProvider.Sut.RevokeUsersAsync(request);\n\n        // Assert\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>(\n                events => events.All(e => e.Item3 == EventSystemUser.SCIM)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeUsersAsync_WithValidationErrors_ReturnsErrorResults(\n        SutProvider<RevokeOrganizationUserCommand> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)\n    {\n        // Arrange\n        orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;\n\n        var actingUser = CreateActingUser(actingUserId, false, null);\n\n        var request = new RevokeOrganizationUsersRequest(\n            organizationId,\n            [orgUser1.Id, orgUser2.Id],\n            actingUser);\n\n        SetupRepositoryMocks(sutProvider, [orgUser1, orgUser2]);\n        SetupValidatorMock(sutProvider, [\n            ValidationResultHelpers.Invalid(orgUser1, new UserAlreadyRevoked()),\n            ValidationResultHelpers.Valid(orgUser2)\n        ]);\n\n        // Act\n        var results = (await sutProvider.Sut.RevokeUsersAsync(request)).ToList();\n\n        // Assert\n        Assert.Equal(2, results.Count);\n        var result1 = results.Single(r => r.Id == orgUser1.Id);\n        var result2 = results.Single(r => r.Id == orgUser2.Id);\n\n        Assert.True(result1.Result.IsError);\n        Assert.True(result2.Result.IsSuccess);\n\n        // Only the valid user should be revoked\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RevokeManyByIdAsync(Arg.Is<IEnumerable<Guid>>(ids =>\n                ids.Count() == 1 && ids.Contains(orgUser2.Id)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeUsersAsync_WhenPushNotificationFails_ContinuesProcessing(\n        SutProvider<RevokeOrganizationUserCommand> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)\n    {\n        // Arrange\n        orgUser.OrganizationId = organizationId;\n        orgUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, false, null);\n\n        var request = new RevokeOrganizationUsersRequest(\n            organizationId,\n            [orgUser.Id],\n            actingUser);\n\n        SetupRepositoryMocks(sutProvider, [orgUser]);\n        SetupValidatorMock(sutProvider, [ValidationResultHelpers.Valid(orgUser)]);\n\n        sutProvider.GetDependency<IPushNotificationService>()\n            .PushSyncOrgKeysAsync(orgUser.UserId!.Value)\n            .Returns(Task.FromException(new Exception(\"Push notification failed\")));\n\n        // Act\n        var results = (await sutProvider.Sut.RevokeUsersAsync(request)).ToList();\n\n        // Assert\n        Assert.Single(results);\n        Assert.True(results[0].Result.IsSuccess);\n\n        // Should log warning but continue\n        sutProvider.GetDependency<ILogger<RevokeOrganizationUserCommand>>()\n            .Received()\n            .Log(\n                LogLevel.Warning,\n                Arg.Any<EventId>(),\n                Arg.Any<object>(),\n                Arg.Any<Exception>(),\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeUsersAsync_FiltersOutUsersFromDifferentOrganization(\n        SutProvider<RevokeOrganizationUserCommand> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser()] OrganizationUser orgUser,\n        [OrganizationUser()] OrganizationUser userFromDifferentOrg)\n    {\n        // Arrange\n        orgUser.OrganizationId = organizationId;\n        orgUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, false, null);\n        var request = new RevokeOrganizationUsersRequest(\n            organizationId,\n            [orgUser.Id, userFromDifferentOrg.Id],\n            actingUser);\n\n        SetupRepositoryMocks(sutProvider, [orgUser, userFromDifferentOrg]);\n        SetupValidatorMock(sutProvider, [ValidationResultHelpers.Valid(orgUser)]);\n\n        // Act\n        await sutProvider.Sut.RevokeUsersAsync(request);\n\n        // Assert: validator only receives the user from the correct organization\n        await sutProvider.GetDependency<IRevokeOrganizationUserValidator>()\n            .Received(1)\n            .ValidateAsync(Arg.Is<RevokeOrganizationUsersValidationRequest>(r =>\n                r.OrganizationUsersToRevoke.Count == 1 &&\n                r.OrganizationUsersToRevoke.Single().Id == orgUser.Id));\n    }\n\n    private static IActingUser CreateActingUser(Guid? userId, bool isOwnerOrProvider, EventSystemUser? systemUserType) =>\n        (userId, systemUserType) switch\n        {\n            ({ } id, _) => new StandardUser(id, isOwnerOrProvider),\n            (null, { } type) => new SystemUser(type)\n        };\n\n    private static void SetupRepositoryMocks(\n        SutProvider<RevokeOrganizationUserCommand> sutProvider,\n        ICollection<OrganizationUser> organizationUsers)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(organizationUsers);\n    }\n\n    private static void SetupValidatorMock(\n        SutProvider<RevokeOrganizationUserCommand> sutProvider,\n        ICollection<ValidationResult<OrganizationUser>> validationResults)\n    {\n        sutProvider.GetDependency<IRevokeOrganizationUserValidator>()\n            .ValidateAsync(Arg.Any<RevokeOrganizationUsersValidationRequest>())\n            .Returns(validationResults);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/RevokeOrganizationUsersValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;\n\n[SutProviderCustomize]\npublic class RevokeOrganizationUsersValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithValidUsers_ReturnsSuccess(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)\n    {\n        // Arrange\n        orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;\n        orgUser1.UserId = Guid.NewGuid();\n        orgUser2.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, false, null);\n        var request = CreateValidationRequest(\n            organizationId,\n            [orgUser1, orgUser2],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Equal(2, results.Count);\n        Assert.All(results, r => Assert.True(r.IsValid));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithRevokedUser_ReturnsErrorForThatUser(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser revokedUser)\n    {\n        // Arrange\n        revokedUser.OrganizationId = organizationId;\n        revokedUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, false, null);\n        var request = CreateValidationRequest(\n            organizationId,\n            [revokedUser],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Single(results);\n        Assert.True(results.First().IsError);\n        Assert.IsType<UserAlreadyRevoked>(results.First().AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WhenRevokingSelf_ReturnsErrorForThatUser(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)\n    {\n        // Arrange\n        orgUser.OrganizationId = organizationId;\n        orgUser.UserId = actingUserId;\n\n        var actingUser = CreateActingUser(actingUserId, false, null);\n        var request = CreateValidationRequest(\n            organizationId,\n            [orgUser],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Single(results);\n        Assert.True(results.First().IsError);\n        Assert.IsType<CannotRevokeYourself>(results.First().AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WhenNonOwnerRevokesOwner_ReturnsErrorForThatUser(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)\n    {\n        // Arrange\n        ownerUser.OrganizationId = organizationId;\n        ownerUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, false, null);\n        var request = CreateValidationRequest(\n            organizationId,\n            [ownerUser],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Single(results);\n        Assert.True(results.First().IsError);\n        Assert.IsType<OnlyOwnersCanRevokeOwners>(results.First().AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WhenOwnerRevokesOwner_ReturnsSuccess(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)\n    {\n        // Arrange\n        ownerUser.OrganizationId = organizationId;\n        ownerUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, true, null);\n        var request = CreateValidationRequest(\n            organizationId,\n            [ownerUser],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Single(results);\n        Assert.True(results.First().IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithMultipleUsers_SomeValid_ReturnsMixedResults(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser validUser,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser revokedUser)\n    {\n        // Arrange\n        validUser.OrganizationId = revokedUser.OrganizationId = organizationId;\n        validUser.UserId = Guid.NewGuid();\n        revokedUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, false, null);\n        var request = CreateValidationRequest(\n            organizationId,\n            [validUser, revokedUser],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Equal(2, results.Count);\n\n        var validResult = results.Single(r => r.Request.Id == validUser.Id);\n        var errorResult = results.Single(r => r.Request.Id == revokedUser.Id);\n\n        Assert.True(validResult.IsValid);\n        Assert.True(errorResult.IsError);\n        Assert.IsType<UserAlreadyRevoked>(errorResult.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithSystemUser_DoesNotRequireActingUserId(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)\n    {\n        // Arrange\n        orgUser.OrganizationId = organizationId;\n        orgUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(null, false, EventSystemUser.SCIM);\n        var request = CreateValidationRequest(\n            organizationId,\n            [orgUser],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Single(results);\n        Assert.True(results.First().IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithSystemUser_RevokingOwner_ReturnsSuccess(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)\n    {\n        // Arrange\n        ownerUser.OrganizationId = organizationId;\n        ownerUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(null, false, EventSystemUser.SCIM);\n        var request = CreateValidationRequest(\n            organizationId,\n            [ownerUser],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Single(results);\n        Assert.True(results.First().IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WhenRevokingLastOwner_ReturnsErrorForThatUser(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser lastOwner)\n    {\n        // Arrange\n        lastOwner.OrganizationId = organizationId;\n        lastOwner.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, true, null); // Is an owner\n        var request = CreateValidationRequest(\n            organizationId,\n            [lastOwner],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(false);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Single(results);\n        Assert.True(results.First().IsError);\n        Assert.IsType<MustHaveConfirmedOwner>(results.First().AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_WithMultipleValidationErrors_ReturnsAllErrors(\n        SutProvider<RevokeOrganizationUsersValidator> sutProvider,\n        Guid organizationId,\n        Guid actingUserId,\n        [OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser revokedUser,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)\n    {\n        // Arrange\n        revokedUser.OrganizationId = ownerUser.OrganizationId = organizationId;\n        revokedUser.UserId = Guid.NewGuid();\n        ownerUser.UserId = Guid.NewGuid();\n\n        var actingUser = CreateActingUser(actingUserId, false, null); // Not an owner\n        var request = CreateValidationRequest(\n            organizationId,\n            [revokedUser, ownerUser],\n            actingUser);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        // Act\n        var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();\n\n        // Assert\n        Assert.Equal(2, results.Count);\n        Assert.All(results, r => Assert.True(r.IsError));\n\n        Assert.Contains(results, r => r.AsError is UserAlreadyRevoked);\n        Assert.Contains(results, r => r.AsError is OnlyOwnersCanRevokeOwners);\n    }\n\n    private static IActingUser CreateActingUser(Guid? userId, bool isOwnerOrProvider, EventSystemUser? systemUserType) =>\n        (userId, systemUserType) switch\n        {\n            ({ } id, _) => new StandardUser(id, isOwnerOrProvider),\n            (null, { } type) => new SystemUser(type)\n        };\n\n\n    private static RevokeOrganizationUsersValidationRequest CreateValidationRequest(\n        Guid organizationId,\n        ICollection<OrganizationUser> organizationUsers,\n        IActingUser actingUser)\n    {\n        return new RevokeOrganizationUsersValidationRequest(\n            organizationId,\n            organizationUsers,\n            actingUser\n        );\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/SelfRevokeUser/SelfRevokeOrganizationUserCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.SelfRevokeUser;\n\n[SutProviderCustomize]\npublic class SelfRevokeOrganizationUserCommandTests\n{\n    [Theory]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    public async Task SelfRevokeUser_Success(\n        OrganizationUserType userType,\n        Guid organizationId,\n        Guid userId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser organizationUser,\n        SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.Type = userType;\n        organizationUser.OrganizationId = organizationId;\n        organizationUser.UserId = userId;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(organizationUser);\n\n        // Create policy requirement with confirmed user\n        var policyDetails = new List<PolicyDetails>\n        {\n            new()\n            {\n                OrganizationId = organizationId,\n                OrganizationUserId = organizationUser.Id,\n                OrganizationUserStatus = OrganizationUserStatusType.Confirmed,\n                OrganizationUserType = userType,\n                PolicyType = PolicyType.OrganizationDataOwnership\n            }\n        };\n        var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            OrganizationDataOwnershipState.Enabled,\n            policyDetails);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)\n            .Returns(policyRequirement);\n\n        // Act\n        var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);\n\n        // Assert\n        Assert.True(result.IsSuccess);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RevokeAsync(organizationUser.Id);\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_SelfRevoked);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncOrgKeysAsync(userId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SelfRevokeUser_WhenUserNotFound_ReturnsNotFoundError(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns((OrganizationUser)null);\n\n        // Act\n        var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationUserNotFound>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SelfRevokeUser_WhenNotEligible_ReturnsBadRequestError(\n        Guid organizationId,\n        Guid userId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser,\n        SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = organizationId;\n        organizationUser.UserId = userId;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(organizationUser);\n\n        // Policy requirement with no policies (disabled)\n        var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            OrganizationDataOwnershipState.Disabled,\n            Enumerable.Empty<PolicyDetails>());\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)\n            .Returns(policyRequirement);\n\n        // Act\n        var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<NotEligibleForSelfRevoke>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SelfRevokeUser_WhenLastOwner_ReturnsBadRequestError(\n        Guid organizationId,\n        Guid userId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,\n        SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = organizationId;\n        organizationUser.UserId = userId;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(organizationUser);\n\n        // Create policy requirement with confirmed owner\n        var policyDetails = new List<PolicyDetails>\n        {\n            new()\n            {\n                OrganizationId = organizationId,\n                OrganizationUserId = organizationUser.Id,\n                OrganizationUserStatus = OrganizationUserStatusType.Confirmed,\n                OrganizationUserType = OrganizationUserType.Owner,\n                PolicyType = PolicyType.OrganizationDataOwnership\n            }\n        };\n        var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            OrganizationDataOwnershipState.Enabled,\n            policyDetails);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)\n            .Returns(policyRequirement);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>(), true)\n            .Returns(false);\n\n        // Act\n        var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<LastOwnerCannotSelfRevoke>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SelfRevokeUser_WhenOwnerButNotLastOwner_Success(\n        Guid organizationId,\n        Guid userId,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser,\n        SutProvider<SelfRevokeOrganizationUserCommand> sutProvider)\n    {\n        // Arrange\n        organizationUser.OrganizationId = organizationId;\n        organizationUser.UserId = userId;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organizationId, userId)\n            .Returns(organizationUser);\n\n        // Create policy requirement with confirmed owner\n        var policyDetails = new List<PolicyDetails>\n        {\n            new()\n            {\n                OrganizationId = organizationId,\n                OrganizationUserId = organizationUser.Id,\n                OrganizationUserStatus = OrganizationUserStatusType.Confirmed,\n                OrganizationUserType = OrganizationUserType.Owner,\n                PolicyType = PolicyType.OrganizationDataOwnership\n            }\n        };\n        var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(\n            OrganizationDataOwnershipState.Enabled,\n            policyDetails);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)\n            .Returns(policyRequirement);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>(), true)\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.SelfRevokeUserAsync(organizationId, userId);\n\n        // Assert\n        Assert.True(result.IsSuccess);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .RevokeAsync(organizationUser.Id);\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_SelfRevoked);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationUserCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task UpdateUserAsync_NoUserId_Throws(OrganizationUser user, Guid? savingUserId,\n        List<CollectionAccessSelection> collections, List<Guid> groups, SutProvider<UpdateOrganizationUserCommand> sutProvider)\n    {\n        user.Id = default(Guid);\n        var existingUserType = OrganizationUserType.User;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, collections, groups));\n        Assert.Contains(\"invite the user first\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateUserAsync_DifferentOrganizationId_Throws(OrganizationUser user, OrganizationUser originalUser,\n        Guid? savingUserId, SutProvider<UpdateOrganizationUserCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(user.Id).Returns(originalUser);\n        var existingUserType = OrganizationUserType.User;\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, null, null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateUserAsync_CollectionsBelongToDifferentOrganization_Throws(OrganizationUser user, OrganizationUser originalUser,\n        List<CollectionAccessSelection> collectionAccess, Guid? savingUserId, SutProvider<UpdateOrganizationUserCommand> sutProvider,\n        Organization organization)\n    {\n        Setup(sutProvider, organization, user, originalUser);\n\n        // Return collections with different organizationIds from the repository\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()\n                .Select(guid => new Collection { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList());\n\n        var existingUserType = OrganizationUserType.User;\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, collectionAccess, null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateUserAsync_CollectionsDoNotExist_Throws(OrganizationUser user, OrganizationUser originalUser,\n        List<CollectionAccessSelection> collectionAccess, Guid? savingUserId, SutProvider<UpdateOrganizationUserCommand> sutProvider,\n        Organization organization)\n    {\n        Setup(sutProvider, organization, user, originalUser);\n\n        // Return matching collections, except that 1 is missing\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo =>\n            {\n                var result = callInfo.Arg<IEnumerable<Guid>>()\n                    .Select(guid => new Collection { Id = guid, OrganizationId = user.OrganizationId }).ToList();\n                result.RemoveAt(0);\n                return result;\n            });\n        var existingUserType = OrganizationUserType.User;\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, collectionAccess, null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateUserAsync_GroupsBelongToDifferentOrganization_Throws(OrganizationUser user, OrganizationUser originalUser,\n        ICollection<Guid> groupAccess, Guid? savingUserId, SutProvider<UpdateOrganizationUserCommand> sutProvider,\n        Organization organization)\n    {\n        Setup(sutProvider, organization, user, originalUser);\n\n        // Return collections with different organizationIds from the repository\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByManyIds(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()\n                .Select(guid => new Group { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList());\n\n        var existingUserType = OrganizationUserType.User;\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, null, groupAccess));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateUserAsync_GroupsDoNotExist_Throws(OrganizationUser user, OrganizationUser originalUser,\n        ICollection<Guid> groupAccess, Guid? savingUserId, SutProvider<UpdateOrganizationUserCommand> sutProvider,\n        Organization organization)\n    {\n        Setup(sutProvider, organization, user, originalUser);\n\n        // Return matching collections, except that 1 is missing\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByManyIds(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo =>\n            {\n                var result = callInfo.Arg<IEnumerable<Guid>>()\n                    .Select(guid => new Group { Id = guid, OrganizationId = CoreHelpers.GenerateComb() }).ToList();\n                result.RemoveAt(0);\n                return result;\n            });\n        var existingUserType = OrganizationUserType.User;\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateUserAsync(user, existingUserType, savingUserId, null, groupAccess));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateUserAsync_Passes(\n        Organization organization,\n        OrganizationUser oldUserData,\n        OrganizationUser newUserData,\n        List<CollectionAccessSelection> collections,\n        List<Guid> groups,\n        Permissions permissions,\n        [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser savingUser,\n        SutProvider<UpdateOrganizationUserCommand> sutProvider)\n    {\n        Setup(sutProvider, organization, newUserData, oldUserData);\n\n        // Arrange list of collections to make sure Manage is mutually exclusive\n        for (var i = 0; i < collections.Count; i++)\n        {\n            var cas = collections[i];\n            cas.Manage = i != collections.Count - 1;\n            cas.HidePasswords = i == collections.Count - 1;\n            cas.ReadOnly = i == collections.Count - 1;\n        }\n\n        newUserData.Id = oldUserData.Id;\n        newUserData.UserId = oldUserData.UserId;\n        newUserData.OrganizationId = savingUser.OrganizationId = oldUserData.OrganizationId = organization.Id;\n        newUserData.Type = OrganizationUserType.Admin;\n        newUserData.Permissions = JsonSerializer.Serialize(permissions, new JsonSerializerOptions\n        {\n            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n        });\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()\n                .Select(guid => new Collection { Id = guid, OrganizationId = oldUserData.OrganizationId }).ToList());\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByManyIds(Arg.Any<IEnumerable<Guid>>())\n            .Returns(callInfo => callInfo.Arg<IEnumerable<Guid>>()\n                .Select(guid => new Group { Id = guid, OrganizationId = oldUserData.OrganizationId }).ToList());\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByFreeOrganizationAdminUserAsync(newUserData.Id)\n            .Returns(0);\n\n        var existingUserType = OrganizationUserType.User;\n\n        await sutProvider.Sut.UpdateUserAsync(newUserData, existingUserType, savingUser.UserId, collections, groups);\n\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n        await organizationService.Received(1).ValidateOrganizationUserUpdatePermissions(\n            newUserData.OrganizationId,\n            newUserData.Type,\n            oldUserData.Type,\n            Arg.Any<Permissions>());\n        await organizationService.Received(1).ValidateOrganizationCustomPermissionsEnabledAsync(\n            newUserData.OrganizationId,\n            newUserData.Type);\n        await sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().Received(1).HasConfirmedOwnersExceptAsync(\n            newUserData.OrganizationId,\n            Arg.Is<IEnumerable<Guid>>(i => i.Contains(newUserData.Id)));\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task UpdateUserAsync_WhenUpdatingUserToAdminOrOwner_AndExistingUserTypeIsNotAdminOrOwner_WithUserAlreadyAdminOfAnotherFreeOrganization_Throws(\n        OrganizationUserType userType,\n        OrganizationUser oldUserData,\n        OrganizationUser newUserData,\n        Organization organization,\n        SutProvider<UpdateOrganizationUserCommand> sutProvider)\n    {\n        organization.PlanType = PlanType.Free;\n        newUserData.Type = userType;\n\n        Setup(sutProvider, organization, newUserData, oldUserData);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByFreeOrganizationAdminUserAsync(newUserData.UserId!.Value)\n            .Returns(1);\n        var existingUserType = OrganizationUserType.User;\n\n        // Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateUserAsync(newUserData, existingUserType, null, null, null));\n        Assert.Contains(\"User can only be an admin of one free organization.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin, OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Admin, OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Owner, OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner, OrganizationUserType.Owner)]\n    public async Task UpdateUserAsync_WhenUpdatingUserToAdminOrOwner_AndExistingUserTypeIsAdminOrOwner_WithUserAlreadyAdminOfAnotherFreeOrganization_Throws(\n        OrganizationUserType newUserType,\n        OrganizationUserType existingUserType,\n        OrganizationUser oldUserData,\n        OrganizationUser newUserData,\n        Organization organization,\n        SutProvider<UpdateOrganizationUserCommand> sutProvider)\n    {\n        organization.PlanType = PlanType.Free;\n        newUserData.Type = newUserType;\n\n        Setup(sutProvider, organization, newUserData, oldUserData);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByFreeOrganizationAdminUserAsync(newUserData.UserId!.Value)\n            .Returns(2);\n\n        // Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateUserAsync(newUserData, existingUserType, null, null, null));\n        Assert.Contains(\"User can only be an admin of one free organization.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateUserAsync_WithMixedCollectionTypes_FiltersOutDefaultUserCollections(\n        OrganizationUser user, OrganizationUser originalUser, Collection sharedCollection, Collection defaultUserCollection,\n        Guid? savingUserId, SutProvider<UpdateOrganizationUserCommand> sutProvider, Organization organization)\n    {\n        user.Permissions = null;\n        sharedCollection.Type = CollectionType.SharedCollection;\n        defaultUserCollection.Type = CollectionType.DefaultUserCollection;\n        sharedCollection.OrganizationId = defaultUserCollection.OrganizationId = organization.Id;\n\n        Setup(sutProvider, organization, user, originalUser);\n\n        var collectionAccess = new List<CollectionAccessSelection>\n        {\n            new() { Id = sharedCollection.Id, ReadOnly = true, HidePasswords = false, Manage = false },\n            new() { Id = defaultUserCollection.Id, ReadOnly = false, HidePasswords = true, Manage = false }\n        };\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(new List<Collection>\n            {\n                new() { Id = sharedCollection.Id, OrganizationId = user.OrganizationId, Type = CollectionType.SharedCollection },\n                new() { Id = defaultUserCollection.Id, OrganizationId = user.OrganizationId, Type = CollectionType.DefaultUserCollection }\n            });\n\n        await sutProvider.Sut.UpdateUserAsync(user, OrganizationUserType.User, savingUserId, collectionAccess, null);\n\n        // Verify that ReplaceAsync was called with only the shared collection (default user collection filtered out)\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).ReplaceAsync(\n            user,\n            Arg.Is<IEnumerable<CollectionAccessSelection>>(collections =>\n                collections.Count() == 1 &&\n                collections.First().Id == sharedCollection.Id\n            )\n        );\n    }\n\n    private void Setup(SutProvider<UpdateOrganizationUserCommand> sutProvider, Organization organization,\n        OrganizationUser newUser, OrganizationUser oldUser)\n    {\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        newUser.Id = oldUser.Id;\n        newUser.UserId = oldUser.UserId;\n        newUser.OrganizationId = oldUser.OrganizationId = organization.Id;\n        organizationUserRepository.GetByIdAsync(oldUser.Id).Returns(oldUser);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(\n                oldUser.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(i => i.Contains(oldUser.Id)))\n            .Returns(true);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserGroupsCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationUserGroupsCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task UpdateUserGroups_ShouldUpdateUserGroupsAndLogUserEvent(\n        OrganizationUser organizationUser,\n        IEnumerable<Guid> groupIds,\n        SutProvider<UpdateOrganizationUserGroupsCommand> sutProvider)\n    {\n        await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)\n            .UpdateGroupsAsync(organizationUser.Id, groupIds);\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/BulkUpdateOrganizationSubscriptionsCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\nusing OrganizationSubscriptionUpdate = Bit.Core.AdminConsole.Models.Data.Organizations.OrganizationSubscriptionUpdate;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class BulkUpdateOrganizationSubscriptionsCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task BulkUpdateOrganizationSubscriptionsAsync_WhenNoSubscriptionsNeedToBeUpdated_ThenNoSyncsOccur(\n        SutProvider<BulkUpdateOrganizationSubscriptionsCommand> sutProvider)\n    {\n        // Arrange\n        OrganizationSubscriptionUpdate[] subscriptionsToUpdate = [];\n\n        // Act\n        await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate);\n\n        await sutProvider.GetDependency<IStripePaymentService>()\n            .DidNotReceive()\n            .AdjustSeatsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkUpdateOrganizationSubscriptionsAsync_WhenOrgUpdatePassedIn_ThenSyncedThroughPaymentService(\n        Organization organization,\n        SutProvider<BulkUpdateOrganizationSubscriptionsCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually2023;\n        organization.Seats = 2;\n\n        OrganizationSubscriptionUpdate[] subscriptionsToUpdate =\n            [new() { Organization = organization, Plan = new Enterprise2023Plan(true) }];\n\n        // Act\n        await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate);\n\n        await sutProvider.GetDependency<IStripePaymentService>()\n            .Received(1)\n            .AdjustSeatsAsync(\n                Arg.Is<Organization>(x => x.Id == organization.Id),\n                Arg.Is<Plan>(x => x.Type == organization.PlanType),\n                organization.Seats!.Value);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .UpdateSuccessfulOrganizationSyncStatusAsync(\n                Arg.Is<IEnumerable<Guid>>(x => x.Contains(organization.Id)),\n                Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkUpdateOrganizationSubscriptionsAsync_WhenOrgUpdateFails_ThenSyncDoesNotOccur(\n        Organization organization,\n        Exception exception,\n        SutProvider<BulkUpdateOrganizationSubscriptionsCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually2023;\n        organization.Seats = 2;\n\n        OrganizationSubscriptionUpdate[] subscriptionsToUpdate =\n            [new() { Organization = organization, Plan = new Enterprise2023Plan(true) }];\n\n        sutProvider.GetDependency<IStripePaymentService>()\n            .AdjustSeatsAsync(\n                Arg.Is<Organization>(x => x.Id == organization.Id),\n                Arg.Is<Plan>(x => x.Type == organization.PlanType),\n                organization.Seats!.Value).ThrowsAsync(exception);\n\n        // Act\n        await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkUpdateOrganizationSubscriptionsAsync_WhenOneOrgUpdateFailsAndAnotherSucceeds_ThenSyncOccursForTheSuccessfulOrg(\n            Organization successfulOrganization,\n            Organization failedOrganization,\n            Exception exception,\n            SutProvider<BulkUpdateOrganizationSubscriptionsCommand> sutProvider)\n    {\n        // Arrange\n        successfulOrganization.PlanType = PlanType.EnterpriseAnnually2023;\n        successfulOrganization.Seats = 2;\n        failedOrganization.PlanType = PlanType.EnterpriseAnnually2023;\n        failedOrganization.Seats = 2;\n\n        OrganizationSubscriptionUpdate[] subscriptionsToUpdate =\n        [\n            new() { Organization = successfulOrganization, Plan = new Enterprise2023Plan(true) },\n            new() { Organization = failedOrganization, Plan = new Enterprise2023Plan(true) }\n        ];\n\n        sutProvider.GetDependency<IStripePaymentService>()\n            .AdjustSeatsAsync(\n                Arg.Is<Organization>(x => x.Id == failedOrganization.Id),\n                Arg.Is<Plan>(x => x.Type == failedOrganization.PlanType),\n                failedOrganization.Seats!.Value).ThrowsAsync(exception);\n\n        // Act\n        await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate);\n\n        await sutProvider.GetDependency<IStripePaymentService>()\n            .Received(1)\n            .AdjustSeatsAsync(\n                Arg.Is<Organization>(x => x.Id == successfulOrganization.Id),\n                Arg.Is<Plan>(x => x.Type == successfulOrganization.PlanType),\n                successfulOrganization.Seats!.Value);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .UpdateSuccessfulOrganizationSyncStatusAsync(\n                Arg.Is<IEnumerable<Guid>>(x => x.Contains(successfulOrganization.Id)),\n                Arg.Any<DateTime>());\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .UpdateSuccessfulOrganizationSyncStatusAsync(\n                Arg.Is<IEnumerable<Guid>>(x => x.Contains(failedOrganization.Id)),\n                Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkUpdateOrganizationSubscriptionsAsync_WithFeatureFlag_WhenOrgUpdatePassedIn_ThenSyncedThroughCommand(\n        Organization organization,\n        SutProvider<BulkUpdateOrganizationSubscriptionsCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually2023;\n        organization.Seats = 2;\n\n        var plan = new Enterprise2023Plan(true);\n\n        OrganizationSubscriptionUpdate[] subscriptionsToUpdate =\n            [new() { Organization = organization, Plan = plan }];\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        BillingCommandResult<Stripe.Subscription> successResult = new Stripe.Subscription();\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(Arg.Is<Organization>(x => x.Id == organization.Id), Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(successResult);\n\n        // Act\n        await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate);\n\n        // Assert\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Received(1)\n            .Run(\n                Arg.Is<Organization>(x => x.Id == organization.Id),\n                Arg.Is<OrganizationSubscriptionChangeSet>(cs =>\n                    cs.Changes.Count == 1 &&\n                    cs.Changes[0].IsItemQuantityUpdate &&\n                    cs.Changes[0].AsT3.PriceId == plan.PasswordManager.StripeSeatPlanId &&\n                    cs.Changes[0].AsT3.Quantity == organization.Seats!.Value));\n\n        await sutProvider.GetDependency<IStripePaymentService>()\n            .DidNotReceive()\n            .AdjustSeatsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .UpdateSuccessfulOrganizationSyncStatusAsync(\n                Arg.Is<IEnumerable<Guid>>(x => x.Contains(organization.Id)),\n                Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkUpdateOrganizationSubscriptionsAsync_WithFeatureFlag_WhenOrgUpdateFails_ThenSyncDoesNotOccur(\n        Organization organization,\n        SutProvider<BulkUpdateOrganizationSubscriptionsCommand> sutProvider)\n    {\n        // Arrange\n        organization.PlanType = PlanType.EnterpriseAnnually2023;\n        organization.Seats = 2;\n\n        OrganizationSubscriptionUpdate[] subscriptionsToUpdate =\n            [new() { Organization = organization, Plan = new Enterprise2023Plan(true) }];\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        BillingCommandResult<Stripe.Subscription> failureResult = new BadRequest(\"error\");\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(Arg.Any<Organization>(), Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(failureResult);\n\n        // Act\n        await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate);\n\n        // Assert\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any<IEnumerable<Guid>>(), Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task BulkUpdateOrganizationSubscriptionsAsync_WithFeatureFlag_WhenOneFailsAndOneSucceeds_ThenSyncOccursForSuccessfulOrg(\n        Organization successfulOrganization,\n        Organization failedOrganization,\n        SutProvider<BulkUpdateOrganizationSubscriptionsCommand> sutProvider)\n    {\n        // Arrange\n        successfulOrganization.PlanType = PlanType.EnterpriseAnnually2023;\n        successfulOrganization.Seats = 2;\n        failedOrganization.PlanType = PlanType.EnterpriseAnnually2023;\n        failedOrganization.Seats = 2;\n\n        OrganizationSubscriptionUpdate[] subscriptionsToUpdate =\n        [\n            new() { Organization = successfulOrganization, Plan = new Enterprise2023Plan(true) },\n            new() { Organization = failedOrganization, Plan = new Enterprise2023Plan(true) }\n        ];\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        BillingCommandResult<Stripe.Subscription> successResult = new Stripe.Subscription();\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(Arg.Is<Organization>(x => x.Id == successfulOrganization.Id), Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(successResult);\n\n        BillingCommandResult<Stripe.Subscription> failureResult = new BadRequest(\"error\");\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(Arg.Is<Organization>(x => x.Id == failedOrganization.Id), Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(failureResult);\n\n        // Act\n        await sutProvider.Sut.BulkUpdateOrganizationSubscriptionsAsync(subscriptionsToUpdate);\n\n        // Assert\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .UpdateSuccessfulOrganizationSyncStatusAsync(\n                Arg.Is<IEnumerable<Guid>>(x => x.Contains(successfulOrganization.Id)),\n                Arg.Any<DateTime>());\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .UpdateSuccessfulOrganizationSyncStatusAsync(\n                Arg.Is<IEnumerable<Guid>>(x => x.Contains(failedOrganization.Id)),\n                Arg.Any<DateTime>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class GetOrganizationSubscriptionsToUpdateQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationSubscriptionsToUpdateAsync_WhenNoOrganizationsNeedToBeSynced_ThenAnEmptyListIsReturned(\n        SutProvider<GetOrganizationSubscriptionsToUpdateQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOrganizationsForSubscriptionSyncAsync()\n            .Returns([]);\n\n        var result = await sutProvider.Sut.GetOrganizationSubscriptionsToUpdateAsync();\n\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationSubscriptionsToUpdateAsync_WhenOrganizationsNeedToBeSynced_ThenUpdateIsReturnedWithCorrectPlanAndOrg(\n        Organization organization,\n        SutProvider<GetOrganizationSubscriptionsToUpdateQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.EnterpriseAnnually2023;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOrganizationsForSubscriptionSyncAsync()\n            .Returns([organization]);\n\n        sutProvider.GetDependency<IPricingClient>()\n            .ListPlans()\n            .Returns([new Enterprise2023Plan(true)]);\n\n        var result = await sutProvider.Sut.GetOrganizationSubscriptionsToUpdateAsync();\n\n        var matchingUpdate = result.FirstOrDefault(x => x.Organization.Id == organization.Id);\n        Assert.NotNull(matchingUpdate);\n        Assert.Equal(organization.PlanType, matchingUpdate.Plan!.Type);\n        Assert.Equal(organization, matchingUpdate.Organization);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs",
    "content": "﻿using System.Data.Common;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Utilities.v2.Validation;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Fakes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class InitPendingOrganizationCommandTests\n{\n    private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For<IOrgUserInviteTokenableFactory>();\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();\n\n    [Theory, BitAutoData]\n    public async Task Init_Organization_Success(User user, Guid orgId, Guid orgUserId, string publicKey,\n            string privateKey, SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org, OrganizationUser orgUser)\n    {\n        var token = CreateToken(orgUser, orgUserId, sutProvider);\n\n        org.PrivateKey = null;\n        org.PublicKey = null;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(org);\n\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n        var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        await sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, \"\", token);\n\n        await organizationRepository.Received().GetByIdAsync(orgId);\n        await organizationService.Received().UpdateAsync(org);\n        await collectionRepository.DidNotReceiveWithAnyArgs().CreateAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Init_Organization_With_CollectionName_Success(User user, Guid orgId, Guid orgUserId, string publicKey,\n            string privateKey, SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org, string collectionName, OrganizationUser orgUser)\n    {\n        var token = CreateToken(orgUser, orgUserId, sutProvider);\n\n        org.PrivateKey = null;\n        org.PublicKey = null;\n        org.Id = orgId;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(org);\n\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n        var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        await sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, collectionName, token);\n\n        await organizationRepository.Received().GetByIdAsync(orgId);\n        await organizationService.Received().UpdateAsync(org);\n\n        await collectionRepository.Received().CreateAsync(\n            Arg.Any<Collection>(),\n            Arg.Is<List<CollectionAccessSelection>>(l => l == null),\n            Arg.Is<List<CollectionAccessSelection>>(l => l.Any(i => i.Manage == true)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Init_Organization_When_Organization_Is_Enabled(User user, Guid orgId, Guid orgUserId, string publicKey,\n            string privateKey, SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org, OrganizationUser orgUser)\n    {\n        var token = CreateToken(orgUser, orgUserId, sutProvider);\n\n        org.Enabled = true;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(org);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, \"\", token));\n\n        Assert.Equal(\"Organization is already enabled.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Init_Organization_When_Organization_Is_Not_Pending(User user, Guid orgId, Guid orgUserId, string publicKey,\n            string privateKey, SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org, OrganizationUser orgUser)\n    {\n        var token = CreateToken(orgUser, orgUserId, sutProvider);\n\n        org.Status = OrganizationStatusType.Created;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(org);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, \"\", token));\n\n        Assert.Equal(\"Organization is not on a Pending status.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Init_Organization_When_Organization_Has_Public_Key(User user, Guid orgId, Guid orgUserId, string publicKey,\n            string privateKey, SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org, OrganizationUser orgUser)\n    {\n        var token = CreateToken(orgUser, orgUserId, sutProvider);\n\n        org.PublicKey = publicKey;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(org);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, \"\", token));\n\n        Assert.Equal(\"Organization already has a Public Key.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Init_Organization_When_Organization_Has_Private_Key(User user, Guid orgId, Guid orgUserId, string publicKey,\n            string privateKey, SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org, OrganizationUser orgUser)\n    {\n        var token = CreateToken(orgUser, orgUserId, sutProvider);\n\n        org.PublicKey = null;\n        org.PrivateKey = privateKey;\n        org.Enabled = false;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        organizationRepository.GetByIdAsync(orgId).Returns(org);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, \"\", token));\n\n        Assert.Equal(\"Organization already has a Private Key.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitPendingOrganization_WithSingleOrgPolicy_ThrowsBadRequest(\n        User user, Guid orgId, Guid orgUserId, string publicKey,\n        string privateKey, SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org,\n        OrganizationUser orgUser, OrganizationUser orgUserFromAnotherOrg)\n    {\n        var token = CreateToken(orgUser, orgUserId, sutProvider);\n\n        org.PrivateKey = null;\n        org.PublicKey = null;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);\n\n        // User has SingleOrg policy from another org\n        orgUserFromAnotherOrg.OrganizationId = Guid.NewGuid();\n        orgUserFromAnotherOrg.UserId = user.Id;\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForAnotherOrganization());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, \"\", token));\n\n        Assert.Contains(\"You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitPendingOrganization_WithoutSingleOrgPolicy_Succeeds(\n        User user, Guid orgId, Guid orgUserId, string publicKey,\n        string privateKey, SutProvider<InitPendingOrganizationCommand> sutProvider, Organization org, OrganizationUser orgUser)\n    {\n        var token = CreateToken(orgUser, orgUserId, sutProvider);\n\n        org.PrivateKey = null;\n        org.PublicKey = null;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(orgId).Returns(org);\n\n        // No SingleOrg policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(user.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        // Act\n        await sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, \"\", token);\n\n        // Assert\n        await sutProvider.GetDependency<IOrganizationRepository>().Received().GetByIdAsync(orgId);\n        await sutProvider.GetDependency<IOrganizationService>().Received().UpdateAsync(org);\n    }\n\n    private string CreateToken(OrganizationUser orgUser, Guid orgUserId, SutProvider<InitPendingOrganizationCommand> sutProvider)\n    {\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        _orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)\n        {\n            ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n        });\n\n        var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser);\n        var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(orgUserId).Returns(orgUser);\n\n        return protectedToken;\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitPendingOrganizationVNextAsync_NullOrgUser_ReturnsError(\n        InitPendingOrganizationRequest request,\n        SutProvider<InitPendingOrganizationCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(request.OrganizationUserId)\n            .Returns((OrganizationUser?)null);\n\n        var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(request);\n\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationUserNotFoundError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitPendingOrganizationVNextAsync_NullOrg_ReturnsError(\n        OrganizationUser orgUser,\n        InitPendingOrganizationRequest request,\n        SutProvider<InitPendingOrganizationCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(request.OrganizationUserId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns((Organization?)null);\n\n        var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(request);\n\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationNotFoundError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitPendingOrganizationVNextAsync_ValidationFails_ReturnsError(\n        Organization org,\n        OrganizationUser orgUser,\n        InitPendingOrganizationRequest request,\n        SutProvider<InitPendingOrganizationCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(request.OrganizationUserId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(org);\n\n        sutProvider.GetDependency<IInitPendingOrganizationValidator>()\n            .ValidateAsync(Arg.Any<InitPendingOrganizationValidationRequest>())\n            .Returns(callInfo =>\n            {\n                var req = callInfo.Arg<InitPendingOrganizationValidationRequest>();\n                return new ValidationResult<InitPendingOrganizationValidationRequest>(req, new InvalidTokenError());\n            });\n\n        var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(request);\n\n        Assert.True(result.IsError);\n        Assert.IsType<InvalidTokenError>(result.AsError);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .InitializeOrganizationAsync(Arg.Any<Organization>(), Arg.Any<Func<DbConnection, DbTransaction, Task>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitPendingOrganizationVNextAsync_Success(\n        Organization org,\n        OrganizationUser orgUser,\n        InitPendingOrganizationRequest request,\n        SutProvider<InitPendingOrganizationCommand> sutProvider)\n    {\n        var requestWithCollection = request with { CollectionName = \"My Collection\" };\n        SetupSuccessfulValidation(org, orgUser, requestWithCollection, sutProvider);\n\n        var result = await sutProvider.Sut.InitPendingOrganizationVNextAsync(requestWithCollection);\n\n        Assert.False(result.IsError);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .InitializeOrganizationAsync(\n                Arg.Is<Organization>(o =>\n                    o.Enabled == true &&\n                    o.Status == OrganizationStatusType.Created &&\n                    o.PublicKey == requestWithCollection.OrganizationKeys.PublicKey &&\n                    o.PrivateKey == requestWithCollection.OrganizationKeys.WrappedPrivateKey),\n                Arg.Any<Func<DbConnection, DbTransaction, Task>>());\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .BuildConfirmOwnerAction(\n                Arg.Is<OrganizationUser>(ou =>\n                    ou.Status == OrganizationUserStatusType.Confirmed &&\n                    ou.UserId == requestWithCollection.User.Id &&\n                    ou.Key == requestWithCollection.EncryptedOrganizationSymmetricKey &&\n                    ou.Email == null));\n\n        await sutProvider.GetDependency<ICollectionRepository>().Received(1)\n            .CreateAsync(\n                Arg.Is<Collection>(c => c.Name == \"My Collection\" && c.OrganizationId == requestWithCollection.OrganizationId),\n                Arg.Is<IEnumerable<CollectionAccessSelection>>(l => l == null),\n                Arg.Is<IEnumerable<CollectionAccessSelection>>(l => l.Any(i => i.Manage)));\n    }\n\n    private static void SetupSuccessfulValidation(\n        Organization org,\n        OrganizationUser orgUser,\n        InitPendingOrganizationRequest request,\n        SutProvider<InitPendingOrganizationCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(request.OrganizationUserId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(org);\n\n        sutProvider.GetDependency<IInitPendingOrganizationValidator>()\n            .ValidateAsync(Arg.Any<InitPendingOrganizationValidationRequest>())\n            .Returns(callInfo =>\n            {\n                var req = callInfo.Arg<InitPendingOrganizationValidationRequest>();\n                return new ValidationResult<InitPendingOrganizationValidationRequest>(req, new OneOf.Types.None());\n            });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .BuildConfirmOwnerAction(Arg.Any<OrganizationUser>())\n            .Returns((_, __) => Task.CompletedTask);\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(request.User.Id)\n            .Returns(new List<Device>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Fakes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class InitPendingOrganizationValidatorTests\n{\n    private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For<IOrgUserInviteTokenableFactory>();\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_InvalidToken_ReturnsInvalidTokenError(\n        User user,\n        OrganizationUser orgUser,\n        InitPendingOrganizationValidationRequest request,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        SetupTokenFactory(sutProvider);\n        orgUser.Email = user.Email;\n        var validationRequest = request with\n        {\n            User = user,\n            OrganizationUser = orgUser,\n            EmailToken = \"invalid-token\"\n        };\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<InvalidTokenError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_EmailMismatch_ReturnsEmailMismatchError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = \"orguser@example.com\";\n        var token = CreateValidToken(orgUser, sutProvider);\n\n        user.Email = \"differentuser@example.com\";\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<EmailMismatchError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_NullOrgUserEmail_ReturnsInvalidTokenError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        orgUser.Email = null;\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<InvalidTokenError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_OrganizationMismatch_ReturnsOrganizationMismatchError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        orgUser.OrganizationId = Guid.NewGuid();\n        SetValidOrganizationState(org);\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationMismatchError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_OrgEnabled_ReturnsOrganizationAlreadyEnabledError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        org.Enabled = true;\n        org.Status = OrganizationStatusType.Pending;\n        org.PublicKey = null;\n        org.PrivateKey = null;\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationAlreadyEnabledError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_OrgNotPending_ReturnsOrganizationNotPendingError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        org.Enabled = false;\n        org.Status = OrganizationStatusType.Created;\n        org.PublicKey = null;\n        org.PrivateKey = null;\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationNotPendingError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_OrgHasKeys_ReturnsOrganizationHasKeysError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        org.Enabled = false;\n        org.Status = OrganizationStatusType.Pending;\n        org.PublicKey = \"existing-public-key\";\n        org.PrivateKey = \"existing-private-key\";\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<OrganizationHasKeysError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_SingleOrgPolicyViolation_ReturnsError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        SetValidOrganizationState(org);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IPolicyService>()\n            .AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)\n            .Returns(true);\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<SingleOrgPolicyViolationError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_AutoConfirmPolicyViolation_ReturnsError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        SetValidOrganizationState(org);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = org.Id,\n            PolicyType = PolicyType.AutomaticUserConfirmation,\n            OrganizationUserType = OrganizationUserType.Owner,\n            OrganizationUserStatus = OrganizationUserStatusType.Invited\n        };\n\n        var autoConfirmReq = new AutomaticUserConfirmationPolicyRequirement(new[] { policyDetails });\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(autoConfirmReq);\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<SingleOrgPolicyViolationError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_TwoFactorRequired_UserDoesNotHave2FA_ReturnsError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        SetValidOrganizationState(org);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IPolicyService>()\n            .AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)\n            .Returns(false);\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = validationRequest.OrganizationId,\n            PolicyType = PolicyType.TwoFactorAuthentication,\n            OrganizationUserType = OrganizationUserType.Owner,\n            OrganizationUserStatus = OrganizationUserStatusType.Invited\n        };\n\n        var twoFactorReq = new RequireTwoFactorPolicyRequirement(new[] { policyDetails });\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(twoFactorReq);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(user)\n            .Returns(false);\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<TwoFactorRequiredError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_FreeOrgAdminLimitExceeded_ReturnsError(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        SetValidOrganizationState(org);\n        org.PlanType = PlanType.Free;\n        orgUser.Type = OrganizationUserType.Owner;\n\n        SetupPassingPolicies(user, sutProvider);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByFreeOrganizationAdminUserAsync(user.Id)\n            .Returns(1);\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsError);\n        Assert.IsType<FreeOrgAdminLimitError>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_AllValid_ReturnsValid(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        SetValidOrganizationState(org);\n\n        SetupPassingPolicies(user, sutProvider);\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsValid);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_PaidOrg_SkipsFreeOrgLimit_ReturnsValid(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        SetValidOrganizationState(org);\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.Type = OrganizationUserType.Owner;\n\n        SetupPassingPolicies(user, sutProvider);\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsValid);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_FreeOrgNonAdmin_SkipsFreeOrgLimit_ReturnsValid(\n        User user,\n        OrganizationUser orgUser,\n        Organization org,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        orgUser.Email = user.Email;\n        var token = CreateValidToken(orgUser, sutProvider);\n        SetValidOrganizationState(org);\n        org.PlanType = PlanType.Free;\n        orgUser.Type = OrganizationUserType.User;\n\n        SetupPassingPolicies(user, sutProvider);\n\n        var validationRequest = CreateValidationRequest(user, org, orgUser, token);\n        orgUser.OrganizationId = validationRequest.OrganizationId;\n\n        var result = await sutProvider.Sut.ValidateAsync(validationRequest);\n\n        Assert.True(result.IsValid);\n    }\n\n    private void SetupTokenFactory(SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n    }\n\n    private string CreateValidToken(\n        OrganizationUser orgUser,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        SetupTokenFactory(sutProvider);\n\n        _orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser)\n        {\n            ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n        });\n\n        var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser);\n        return _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable);\n    }\n\n    private static void SetValidOrganizationState(Organization org)\n    {\n        org.Enabled = false;\n        org.Status = OrganizationStatusType.Pending;\n        org.PublicKey = null;\n        org.PrivateKey = null;\n    }\n\n    private static InitPendingOrganizationValidationRequest CreateValidationRequest(\n        User user,\n        Organization org,\n        OrganizationUser orgUser,\n        string emailToken)\n    {\n        return new InitPendingOrganizationValidationRequest\n        {\n            User = user,\n            OrganizationId = Guid.NewGuid(),\n            OrganizationUserId = orgUser.Id,\n            OrganizationKeys = new Bit.Core.KeyManagement.Models.Data.PublicKeyEncryptionKeyPairData(\n                wrappedPrivateKey: \"wrapped-private-key\",\n                publicKey: \"public-key\"),\n            CollectionName = null,\n            EmailToken = emailToken,\n            EncryptedOrganizationSymmetricKey = \"encrypted-org-key\",\n            Organization = org,\n            OrganizationUser = orgUser,\n        };\n    }\n\n    private static void SetupPassingPolicies(\n        User user,\n        SutProvider<InitPendingOrganizationValidator> sutProvider)\n    {\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IPolicyService>()\n            .AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)\n            .Returns(false);\n\n        var twoFactorReq = new RequireTwoFactorPolicyRequirement(Enumerable.Empty<PolicyDetails>());\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(twoFactorReq);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDeleteCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class OrganizationDeleteCommandTests\n{\n    [Theory, PaidOrganizationCustomize, BitAutoData]\n    public async Task Delete_Success(Organization organization, SutProvider<OrganizationDeleteCommand> sutProvider)\n    {\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n\n        await sutProvider.Sut.DeleteAsync(organization);\n\n        await organizationRepository.Received().DeleteAsync(organization);\n        await applicationCacheService.Received().DeleteOrganizationAbilityAsync(organization.Id);\n    }\n\n    [Theory, PaidOrganizationCustomize, BitAutoData]\n    public async Task Delete_Fails_KeyConnector(Organization organization, SutProvider<OrganizationDeleteCommand> sutProvider,\n        SsoConfig ssoConfig)\n    {\n        ssoConfig.Enabled = true;\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });\n        var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n\n        ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.DeleteAsync(organization));\n\n        Assert.Contains(\"You cannot delete an Organization that is using Key Connector.\", exception.Message);\n\n        await organizationRepository.DidNotReceiveWithAnyArgs().DeleteAsync(default);\n        await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class OrganizationDisableCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task DisableAsync_WhenOrganizationEnabled_DisablesSuccessfully(\n        Organization organization,\n        DateTime expirationDate,\n        SutProvider<OrganizationDisableCommand> sutProvider)\n    {\n        organization.Enabled = true;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        await sutProvider.Sut.DisableAsync(organization.Id, expirationDate);\n\n        Assert.False(organization.Enabled);\n        Assert.Equal(expirationDate, organization.ExpirationDate);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .ReplaceAsync(organization);\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DisableAsync_WhenOrganizationNotFound_DoesNothing(\n        Guid organizationId,\n        DateTime expirationDate,\n        SutProvider<OrganizationDisableCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organizationId)\n            .Returns((Organization)null);\n\n        await sutProvider.Sut.DisableAsync(organizationId, expirationDate);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .ReplaceAsync(Arg.Any<Organization>());\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .DidNotReceive()\n            .UpsertOrganizationAbilityAsync(Arg.Any<Organization>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task DisableAsync_WhenOrganizationAlreadyDisabled_DoesNothing(\n        Organization organization,\n        DateTime expirationDate,\n        SutProvider<OrganizationDisableCommand> sutProvider)\n    {\n        organization.Enabled = false;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        await sutProvider.Sut.DisableAsync(organization.Id, expirationDate);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .ReplaceAsync(Arg.Any<Organization>());\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .DidNotReceive()\n            .UpsertOrganizationAbilityAsync(Arg.Any<Organization>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class OrganizationEnableCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task EnableAsync_WhenOrganizationDoesNotExist_DoesNothing(\n        Guid organizationId,\n        SutProvider<OrganizationEnableCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organizationId)\n            .Returns((Organization)null);\n\n        await sutProvider.Sut.EnableAsync(organizationId);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .ReplaceAsync(Arg.Any<Organization>());\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .DidNotReceive()\n            .UpsertOrganizationAbilityAsync(Arg.Any<Organization>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task EnableAsync_WhenOrganizationAlreadyEnabled_DoesNothing(\n        Organization organization,\n        SutProvider<OrganizationEnableCommand> sutProvider)\n    {\n        organization.Enabled = true;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        await sutProvider.Sut.EnableAsync(organization.Id);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .ReplaceAsync(Arg.Any<Organization>());\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .DidNotReceive()\n            .UpsertOrganizationAbilityAsync(Arg.Any<Organization>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task EnableAsync_WhenOrganizationDisabled_EnablesAndSaves(\n        Organization organization,\n        SutProvider<OrganizationEnableCommand> sutProvider)\n    {\n        organization.Enabled = false;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        await sutProvider.Sut.EnableAsync(organization.Id);\n\n        Assert.True(organization.Enabled);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .ReplaceAsync(organization);\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task EnableAsync_WithExpiration_WhenOrganizationHasNoGateway_DoesNothing(\n        Organization organization,\n        DateTime expirationDate,\n        SutProvider<OrganizationEnableCommand> sutProvider)\n    {\n        organization.Enabled = false;\n        organization.Gateway = null;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        await sutProvider.Sut.EnableAsync(organization.Id, expirationDate);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .ReplaceAsync(Arg.Any<Organization>());\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .DidNotReceive()\n            .UpsertOrganizationAbilityAsync(Arg.Any<Organization>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task EnableAsync_WithExpiration_WhenValid_EnablesAndSetsExpiration(\n        Organization organization,\n        DateTime expirationDate,\n        SutProvider<OrganizationEnableCommand> sutProvider)\n    {\n        organization.Enabled = false;\n        organization.Gateway = GatewayType.Stripe;\n        organization.RevisionDate = DateTime.UtcNow.AddDays(-1);\n        var originalRevisionDate = organization.RevisionDate;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        await sutProvider.Sut.EnableAsync(organization.Id, expirationDate);\n\n        Assert.True(organization.Enabled);\n        Assert.Equal(expirationDate, organization.ExpirationDate);\n        Assert.True(organization.RevisionDate > originalRevisionDate);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .ReplaceAsync(organization);\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task EnableAsync_WithoutExpiration_DoesNotUpdateRevisionDate(\n        Organization organization,\n        SutProvider<OrganizationEnableCommand> sutProvider)\n    {\n        organization.Enabled = false;\n        var originalRevisionDate = organization.RevisionDate;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        await sutProvider.Sut.EnableAsync(organization.Id);\n\n        Assert.True(organization.Enabled);\n        Assert.Equal(originalRevisionDate, organization.RevisionDate);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .ReplaceAsync(organization);\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(organization);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationInitiateDeleteCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Models.Business.Tokenables;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class OrganizationInitiateDeleteCommandTests\n{\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task InitiateDeleteAsync_ValidAdminUser_Success(OrganizationUserType organizationUserType,\n        Organization organization, User orgAdmin, OrganizationUserOrganizationDetails orgAdminUser,\n        string token, SutProvider<OrganizationInitiateDeleteCommand> sutProvider)\n    {\n        orgAdminUser.Type = organizationUserType;\n        orgAdminUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(orgAdmin.Email)\n            .Returns(orgAdmin);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetDetailsByUserAsync(orgAdmin.Id, organization.Id)\n            .Returns(orgAdminUser);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<OrgDeleteTokenable>>()\n            .Protect(Arg.Any<OrgDeleteTokenable>())\n            .Returns(token);\n\n        await sutProvider.Sut.InitiateDeleteAsync(organization, orgAdmin.Email);\n\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendInitiateDeleteOrganzationEmailAsync(orgAdmin.Email, organization, token);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateDeleteAsync_UserNotFound_ThrowsBadRequest(\n        Organization organization, string email, SutProvider<OrganizationInitiateDeleteCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(email)\n            .Returns((User)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InitiateDeleteAsync(organization, email));\n\n        Assert.Equal(OrganizationInitiateDeleteCommand.OrganizationAdminNotFoundErrorMessage, exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task InitiateDeleteAsync_UserNotOrgAdmin_ThrowsBadRequest(OrganizationUserType organizationUserType,\n        Organization organization, User user, OrganizationUserOrganizationDetails orgUser,\n        SutProvider<OrganizationInitiateDeleteCommand> sutProvider)\n    {\n        orgUser.Type = organizationUserType;\n        orgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(user.Email)\n            .Returns(user);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetDetailsByUserAsync(user.Id, organization.Id)\n            .Returns(orgUser);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InitiateDeleteAsync(organization, user.Email));\n\n        Assert.Equal(OrganizationInitiateDeleteCommand.OrganizationAdminNotFoundErrorMessage, exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Invited)]\n    [BitAutoData(OrganizationUserStatusType.Revoked)]\n    [BitAutoData(OrganizationUserStatusType.Accepted)]\n    public async Task InitiateDeleteAsync_UserNotConfirmed_ThrowsBadRequest(\n        OrganizationUserStatusType organizationUserStatusType,\n        Organization organization, User user, OrganizationUserOrganizationDetails orgUser,\n        SutProvider<OrganizationInitiateDeleteCommand> sutProvider)\n    {\n        orgUser.Type = OrganizationUserType.Admin;\n        orgUser.Status = organizationUserStatusType;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(user.Email)\n            .Returns(user);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetDetailsByUserAsync(user.Id, organization.Id)\n            .Returns(orgUser);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InitiateDeleteAsync(organization, user.Email));\n\n        Assert.Equal(OrganizationInitiateDeleteCommand.OrganizationAdminNotFoundErrorMessage, exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp;\n\n[SutProviderCustomize]\npublic class CloudICloudOrganizationSignUpCommandTests\n{\n    [Theory]\n    [BitAutoData(PlanType.FamiliesAnnually)]\n    [BitAutoData(PlanType.FamiliesAnnually2025)]\n    public async Task SignUp_PM_Family_Passes(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        signup.Plan = planType;\n\n        var plan = MockPlans.Get(signup.Plan);\n\n        signup.AdditionalSeats = 0;\n        signup.PaymentMethodType = PaymentMethodType.Card;\n        signup.PremiumAccessAddon = false;\n        signup.UseSecretsManager = false;\n        signup.IsFromSecretsManagerTrial = false;\n        signup.IsFromProvider = false;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(signup.Owner.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);\n\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(\n            Arg.Is<Organization>(o =>\n                o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats\n                && o.SmSeats == null\n                && o.SmServiceAccounts == null));\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(\n            Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));\n\n        Assert.NotNull(result.Organization);\n        Assert.NotNull(result.OrganizationUser);\n\n        await sutProvider.GetDependency<IOrganizationBillingService>().Received(1).Finalize(\n            Arg.Is<OrganizationSale>(sale =>\n                sale.CustomerSetup.TokenizedPaymentSource.Type == signup.PaymentMethodType.Value &&\n                sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken &&\n                sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry &&\n                sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode &&\n                sale.SubscriptionSetup.PlanType == plan.Type &&\n                sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats &&\n                sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb &&\n                sale.SubscriptionSetup.SecretsManagerOptions == null));\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.FamiliesAnnually)]\n    [BitAutoData(PlanType.FamiliesAnnually2025)]\n    public async Task SignUp_AssignsOwnerToDefaultCollection\n        (PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        signup.Plan = planType;\n        signup.AdditionalSeats = 0;\n        signup.PaymentMethodType = PaymentMethodType.Card;\n        signup.PremiumAccessAddon = false;\n        signup.UseSecretsManager = false;\n        signup.IsFromProvider = false;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(signup.Owner.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Extract orgUserId when created\n        Guid? orgUserId = null;\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .CreateAsync(Arg.Do<OrganizationUser>(ou => orgUserId = ou.Id));\n\n        var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);\n\n        // Assert: created a Can Manage association for the default collection\n        Assert.NotNull(orgUserId);\n        await sutProvider.GetDependency<ICollectionRepository>().Received(1).CreateAsync(\n            Arg.Any<Collection>(),\n            Arg.Is<IEnumerable<CollectionAccessSelection>>(cas => cas == null),\n            Arg.Is<IEnumerable<CollectionAccessSelection>>(cas =>\n                cas.Count() == 1 &&\n                cas.All(c =>\n                    c.Id == orgUserId &&\n                    !c.ReadOnly &&\n                    !c.HidePasswords &&\n                    c.Manage)));\n\n        Assert.NotNull(result.Organization);\n        Assert.NotNull(result.OrganizationUser);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    public async Task SignUp_SM_Passes(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        signup.Plan = planType;\n\n        var plan = MockPlans.Get(signup.Plan);\n\n        signup.UseSecretsManager = true;\n        signup.AdditionalSeats = 15;\n        signup.AdditionalSmSeats = 10;\n        signup.AdditionalServiceAccounts = 20;\n        signup.PaymentMethodType = PaymentMethodType.Card;\n        signup.PremiumAccessAddon = false;\n        signup.IsFromSecretsManagerTrial = false;\n        signup.IsFromProvider = false;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(signup.Owner.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);\n\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(\n            Arg.Is<Organization>(o =>\n                o.Seats == plan.PasswordManager.BaseSeats + signup.AdditionalSeats\n                && o.SmSeats == plan.SecretsManager.BaseSeats + signup.AdditionalSmSeats\n                && o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + signup.AdditionalServiceAccounts));\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).CreateAsync(\n            Arg.Is<OrganizationUser>(o => o.AccessSecretsManager == signup.UseSecretsManager));\n\n        Assert.NotNull(result.Organization);\n        Assert.NotNull(result.OrganizationUser);\n\n        await sutProvider.GetDependency<IOrganizationBillingService>().Received(1).Finalize(\n            Arg.Is<OrganizationSale>(sale =>\n                sale.CustomerSetup.TokenizedPaymentSource.Type == signup.PaymentMethodType.Value &&\n                sale.CustomerSetup.TokenizedPaymentSource.Token == signup.PaymentToken &&\n                sale.CustomerSetup.TaxInformation.Country == signup.TaxInfo.BillingAddressCountry &&\n                sale.CustomerSetup.TaxInformation.PostalCode == signup.TaxInfo.BillingAddressPostalCode &&\n                sale.SubscriptionSetup.PlanType == plan.Type &&\n                sale.SubscriptionSetup.PasswordManagerOptions.Seats == signup.AdditionalSeats &&\n                sale.SubscriptionSetup.PasswordManagerOptions.Storage == signup.AdditionalStorageGb &&\n                sale.SubscriptionSetup.SecretsManagerOptions.Seats == signup.AdditionalSmSeats &&\n                sale.SubscriptionSetup.SecretsManagerOptions.ServiceAccounts == signup.AdditionalServiceAccounts));\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    public async Task SignUp_SM_Throws_WhenManagedByMSP(PlanType planType, OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        signup.Plan = planType;\n        signup.UseSecretsManager = true;\n        signup.AdditionalSeats = 15;\n        signup.AdditionalSmSeats = 10;\n        signup.AdditionalServiceAccounts = 20;\n        signup.PaymentMethodType = PaymentMethodType.Card;\n        signup.PremiumAccessAddon = false;\n        signup.IsFromProvider = true;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SignUpOrganizationAsync(signup));\n        Assert.Contains(\"Organizations with a Managed Service Provider do not support Secrets Manager.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpAsync_SecretManager_AdditionalServiceAccounts_NotAllowedByPlan_ShouldThrowException(OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        signup.AdditionalSmSeats = 0;\n        signup.AdditionalSeats = 0;\n        signup.Plan = PlanType.Free;\n        signup.UseSecretsManager = true;\n        signup.PaymentMethodType = PaymentMethodType.Card;\n        signup.PremiumAccessAddon = false;\n        signup.AdditionalServiceAccounts = 10;\n        signup.AdditionalStorageGb = 0;\n        signup.IsFromProvider = false;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpOrganizationAsync(signup));\n        Assert.Contains(\"Plan does not allow additional Machine Accounts.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpAsync_SMSeatsGreatThanPMSeat_ShouldThrowException(OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        signup.AdditionalSmSeats = 100;\n        signup.AdditionalSeats = 10;\n        signup.Plan = PlanType.EnterpriseAnnually;\n        signup.UseSecretsManager = true;\n        signup.PaymentMethodType = PaymentMethodType.Card;\n        signup.PremiumAccessAddon = false;\n        signup.AdditionalServiceAccounts = 10;\n        signup.IsFromProvider = false;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n           () => sutProvider.Sut.SignUpOrganizationAsync(signup));\n        Assert.Contains(\"You cannot have more Secrets Manager seats than Password Manager seats\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpAsync_InvalidateServiceAccount_ShouldThrowException(OrganizationSignup signup, SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        signup.AdditionalSmSeats = 10;\n        signup.AdditionalSeats = 10;\n        signup.Plan = PlanType.EnterpriseAnnually;\n        signup.UseSecretsManager = true;\n        signup.PaymentMethodType = PaymentMethodType.Card;\n        signup.PremiumAccessAddon = false;\n        signup.AdditionalServiceAccounts = -10;\n        signup.IsFromProvider = false;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpOrganizationAsync(signup));\n        Assert.Contains(\"You can't subtract Machine Accounts!\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpAsync_Free_ExistingFreeOrgAdmin_ThrowsBadRequest(\n        SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var signup = new OrganizationSignup\n        {\n            Plan = PlanType.Free,\n            IsFromProvider = false,\n            Owner = new User { Id = Guid.NewGuid() }\n        };\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id)\n            .Returns(1);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(signup.Owner.Id)\n            .Returns(new SingleOrganizationPolicyRequirement([]));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpOrganizationAsync(signup));\n        Assert.Contains(\"You can only be an admin of one free organization.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    public async Task SignUpAsync_WhenSingleOrgPolicyIsEnabled_OwnerBelongsToAnotherOrgAsUser_ThrowsBadRequest(\n        PlanType planType, OrganizationSignup signup,\n        [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser,\n        SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        signup.Plan = planType;\n        signup.AdditionalSeats = 15;\n        signup.PaymentMethodType = PaymentMethodType.Card;\n        signup.PremiumAccessAddon = false;\n        signup.UseSecretsManager = false;\n        signup.IsFromProvider = false;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        // User has SingleOrg policy from another org\n        organizationUser.UserId = signup.Owner.Id;\n        organizationUser.OrganizationId = Guid.NewGuid();\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(signup.Owner.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForAnotherOrganization());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpOrganizationAsync(signup));\n        Assert.Contains(\"You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    public async Task SignUpAsync_WithoutSingleOrgPolicy_Succeeds(\n        PlanType planType, OrganizationSignup signup,\n        SutProvider<CloudOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        signup.Plan = planType;\n        signup.AdditionalSeats = 15;\n        signup.PaymentMethodType = PaymentMethodType.Card;\n        signup.PremiumAccessAddon = false;\n        signup.UseSecretsManager = false;\n        signup.IsFromProvider = false;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(signup.Plan).Returns(MockPlans.Get(signup.Plan));\n\n        // No SingleOrg policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(signup.Owner.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        // Act\n        var result = await sutProvider.Sut.SignUpOrganizationAsync(signup);\n\n        // Assert\n        Assert.NotNull(result.Organization);\n        Assert.NotNull(result.OrganizationUser);\n\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(Arg.Any<Organization>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp;\n\n[SutProviderCustomize]\npublic class ProviderClientOrganizationSignUpCommandTests\n{\n    [Theory]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    public async Task SignupClientAsync_ValidParameters_CreatesOrganizationSuccessfully(\n        PlanType planType,\n        OrganizationSignup signup,\n        string collectionName,\n        SutProvider<ProviderClientOrganizationSignUpCommand> sutProvider)\n    {\n        signup.Plan = planType;\n        signup.AdditionalSeats = 15;\n        signup.CollectionName = collectionName;\n\n        var plan = MockPlans.Get(signup.Plan);\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(signup.Plan)\n            .Returns(plan);\n\n        var result = await sutProvider.Sut.SignUpClientOrganizationAsync(signup);\n\n        Assert.NotNull(result.Organization);\n        Assert.NotNull(result.DefaultCollection);\n        Assert.Equal(collectionName, result.DefaultCollection.Name);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .CreateAsync(\n                Arg.Is<Organization>(o =>\n                    o.Name == signup.Name &&\n                    o.BillingEmail == signup.BillingEmail &&\n                    o.PlanType == plan.Type &&\n                    o.Seats == signup.AdditionalSeats &&\n                    o.MaxCollections == plan.PasswordManager.MaxCollections &&\n                    o.UsePasswordManager == true &&\n                    o.UseSecretsManager == false &&\n                    o.Status == OrganizationStatusType.Created\n                )\n            );\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateAsync(\n                Arg.Is<Collection>(c =>\n                    c.Name == collectionName &&\n                    c.OrganizationId == result.Organization.Id\n                ),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>()\n            );\n\n        await sutProvider.GetDependency<IOrganizationApiKeyRepository>()\n            .Received(1)\n            .CreateAsync(\n                Arg.Is<OrganizationApiKey>(k =>\n                    k.OrganizationId == result.Organization.Id &&\n                    k.Type == OrganizationApiKeyType.Default\n                )\n            );\n\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(Arg.Is<Organization>(o => o.Id == result.Organization.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignupClientAsync_NullPlan_ThrowsBadRequestException(\n        OrganizationSignup signup,\n        SutProvider<ProviderClientOrganizationSignUpCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(signup.Plan)\n            .Returns((Plan)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpClientOrganizationAsync(signup));\n\n        Assert.Contains(ProviderClientOrganizationSignUpCommand.PlanNullErrorMessage, exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignupClientAsync_NegativeAdditionalSeats_ThrowsBadRequestException(\n        OrganizationSignup signup,\n        SutProvider<ProviderClientOrganizationSignUpCommand> sutProvider)\n    {\n        signup.Plan = PlanType.TeamsMonthly;\n        signup.AdditionalSeats = -5;\n\n        var plan = MockPlans.Get(signup.Plan);\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(signup.Plan)\n            .Returns(plan);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpClientOrganizationAsync(signup));\n\n        Assert.Contains(ProviderClientOrganizationSignUpCommand.AdditionalSeatsNegativeErrorMessage, exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    public async Task SignupClientAsync_WhenExceptionIsThrown_CleanupIsPerformed(\n        PlanType planType,\n        OrganizationSignup signup,\n        SutProvider<ProviderClientOrganizationSignUpCommand> sutProvider)\n    {\n        signup.Plan = planType;\n\n        var plan = MockPlans.Get(signup.Plan);\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(signup.Plan)\n            .Returns(plan);\n\n        sutProvider.GetDependency<IOrganizationApiKeyRepository>()\n            .When(x => x.CreateAsync(Arg.Any<OrganizationApiKey>()))\n            .Do(_ => throw new Exception());\n\n        var thrownException = await Assert.ThrowsAsync<Exception>(\n            () => sutProvider.Sut.SignUpClientOrganizationAsync(signup));\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .DeleteAsync(Arg.Is<Organization>(o => o.Name == signup.Name));\n\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .DeleteOrganizationAbilityAsync(Arg.Any<Guid>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ResellerClientOrganizationSignUpCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp;\n\n[SutProviderCustomize]\npublic class ResellerClientOrganizationSignUpCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpResellerClientAsync_WithValidParameters_CreatesOrganizationSuccessfully(\n        Organization organization,\n        string ownerEmail,\n        SutProvider<ResellerClientOrganizationSignUpCommand> sutProvider)\n    {\n        var result = await sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail);\n\n        Assert.NotNull(result.Organization);\n        Assert.False(result.Organization.Enabled);\n        Assert.Equal(OrganizationStatusType.Pending, result.Organization.Status);\n        Assert.NotNull(result.OwnerOrganizationUser);\n        Assert.Equal(ownerEmail, result.OwnerOrganizationUser.Email);\n        Assert.Equal(OrganizationUserType.Owner, result.OwnerOrganizationUser.Type);\n        Assert.Equal(OrganizationUserStatusType.Invited, result.OwnerOrganizationUser.Status);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .CreateAsync(\n                Arg.Is<Organization>(o =>\n                    o.Id != default &&\n                    o.Name == organization.Name &&\n                    o.Enabled == false &&\n                    o.Status == OrganizationStatusType.Pending\n                )\n            );\n        await sutProvider.GetDependency<IOrganizationApiKeyRepository>()\n            .Received(1)\n            .CreateAsync(\n                Arg.Is<OrganizationApiKey>(k =>\n                    k.OrganizationId == result.Organization.Id &&\n                    k.Type == OrganizationApiKeyType.Default &&\n                    !string.IsNullOrEmpty(k.ApiKey)\n                )\n            );\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(Arg.Is<Organization>(o => o.Id == result.Organization.Id));\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .CreateAsync(\n                Arg.Is<OrganizationUser>(u =>\n                    u.OrganizationId == result.Organization.Id &&\n                    u.Email == ownerEmail &&\n                    u.Type == OrganizationUserType.Owner &&\n                    u.Status == OrganizationUserStatusType.Invited &&\n                    u.UserId == null\n                )\n            );\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .Received(1)\n            .SendInvitesAsync(\n                Arg.Is<SendInvitesRequest>(r =>\n                    r.Users.Count() == 1 &&\n                    r.Users.First().Email == ownerEmail &&\n                    r.Organization.Id == result.Organization.Id &&\n                    r.InitOrganization == true\n                )\n            );\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(\n                Arg.Is<OrganizationUser>(u => u.Email == ownerEmail),\n                EventType.OrganizationUser_Invited\n            );\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpResellerClientAsync_WhenOrganizationRepositoryThrows_PerformsCleanup(\n        Organization organization,\n        string ownerEmail,\n        SutProvider<ResellerClientOrganizationSignUpCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .When(x => x.CreateAsync(Arg.Any<Organization>()))\n            .Do(_ => throw new Exception());\n\n        await Assert.ThrowsAsync<Exception>(\n            () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail));\n\n        await AssertCleanupIsPerformed(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpResellerClientAsync_WhenOrganizationUserCreationFails_PerformsCleanup(\n        Organization organization,\n        string ownerEmail,\n        SutProvider<ResellerClientOrganizationSignUpCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .When(x => x.CreateAsync(Arg.Any<OrganizationUser>()))\n            .Do(_ => throw new Exception());\n\n        await Assert.ThrowsAsync<Exception>(\n            () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail));\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Any<Organization>());\n        await AssertCleanupIsPerformed(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpResellerClientAsync_WhenInvitationSendingFails_PerformsCleanup(\n        Organization organization,\n        string ownerEmail,\n        SutProvider<ResellerClientOrganizationSignUpCommand> sutProvider)\n    {\n        sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .When(x => x.SendInvitesAsync(Arg.Any<SendInvitesRequest>()))\n            .Do(_ => throw new Exception());\n\n        await Assert.ThrowsAsync<Exception>(\n            () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail));\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Any<Organization>());\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Any<OrganizationUser>());\n        await AssertCleanupIsPerformed(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpResellerClientAsync_WhenEventLoggingFails_PerformsCleanup(\n        Organization organization,\n        string ownerEmail,\n        SutProvider<ResellerClientOrganizationSignUpCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IEventService>()\n            .When(x => x.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>()))\n            .Do(_ => throw new Exception());\n\n        await Assert.ThrowsAsync<Exception>(\n            () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail));\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Any<Organization>());\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Any<OrganizationUser>());\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .Received(1)\n            .SendInvitesAsync(Arg.Any<SendInvitesRequest>());\n        await AssertCleanupIsPerformed(sutProvider);\n    }\n\n    private static async Task AssertCleanupIsPerformed(SutProvider<ResellerClientOrganizationSignUpCommand> sutProvider)\n    {\n        await sutProvider.GetDependency<IStripePaymentService>()\n            .Received(1)\n            .CancelAndRecoverChargesAsync(Arg.Any<Organization>());\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .DeleteAsync(Arg.Any<Organization>());\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .DeleteOrganizationAbilityAsync(Arg.Any<Guid>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class OrganizationUpdateCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WhenValidOrganization_UpdatesOrganization(\n        Guid organizationId,\n        string name,\n        string billingEmail,\n        Organization organization,\n        SutProvider<OrganizationUpdateCommand> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n        var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();\n\n        organization.Id = organizationId;\n        organization.GatewayCustomerId = null; // No Stripe customer, but billing update is still called\n\n        organizationRepository\n            .GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var request = new OrganizationUpdateRequest\n        {\n            OrganizationId = organizationId,\n            Name = name,\n            BillingEmail = billingEmail\n        };\n\n        // Act\n        var result = await sutProvider.Sut.UpdateAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(organizationId, result.Id);\n        Assert.Equal(name, result.Name);\n        Assert.Equal(billingEmail.ToLowerInvariant().Trim(), result.BillingEmail);\n\n        await organizationRepository\n            .Received(1)\n            .GetByIdAsync(Arg.Is<Guid>(id => id == organizationId));\n        await organizationService\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(\n                result,\n                EventType.Organization_Updated);\n        await organizationBillingService\n            .Received(1)\n            .UpdateOrganizationNameAndEmail(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WhenOrganizationNotFound_ThrowsNotFoundException(\n        Guid organizationId,\n        string name,\n        string billingEmail,\n        SutProvider<OrganizationUpdateCommand> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        organizationRepository\n            .GetByIdAsync(organizationId)\n            .Returns((Organization)null);\n\n        var request = new OrganizationUpdateRequest\n        {\n            OrganizationId = organizationId,\n            Name = name,\n            BillingEmail = billingEmail\n        };\n\n        // Act/Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(request));\n    }\n\n    [Theory]\n    [BitAutoData(\"\")]\n    [BitAutoData((string)null)]\n    public async Task UpdateAsync_WhenGatewayCustomerIdIsNullOrEmpty_CallsBillingUpdateButHandledGracefully(\n        string gatewayCustomerId,\n        Guid organizationId,\n        Organization organization,\n        SutProvider<OrganizationUpdateCommand> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n        var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();\n\n        organization.Id = organizationId;\n        organization.Name = \"Old Name\";\n        organization.GatewayCustomerId = gatewayCustomerId;\n\n        organizationRepository\n            .GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var request = new OrganizationUpdateRequest\n        {\n            OrganizationId = organizationId,\n            Name = \"New Name\",\n            BillingEmail = organization.BillingEmail\n        };\n\n        // Act\n        var result = await sutProvider.Sut.UpdateAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(organizationId, result.Id);\n        Assert.Equal(\"New Name\", result.Name);\n\n        await organizationService\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(\n                result,\n                EventType.Organization_Updated);\n        await organizationBillingService\n            .Received(1)\n            .UpdateOrganizationNameAndEmail(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WhenKeysProvided_AndNotAlreadySet_SetsKeys(\n        Guid organizationId,\n        string publicKey,\n        string encryptedPrivateKey,\n        Organization organization,\n        SutProvider<OrganizationUpdateCommand> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n\n        organization.Id = organizationId;\n        organization.PublicKey = null;\n        organization.PrivateKey = null;\n\n        organizationRepository\n            .GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var request = new OrganizationUpdateRequest\n        {\n            OrganizationId = organizationId,\n            Name = organization.Name,\n            BillingEmail = organization.BillingEmail,\n            Keys = new PublicKeyEncryptionKeyPairData(\n                wrappedPrivateKey: encryptedPrivateKey,\n                publicKey: publicKey)\n        };\n\n        // Act\n        var result = await sutProvider.Sut.UpdateAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(organizationId, result.Id);\n        Assert.Equal(publicKey, result.PublicKey);\n        Assert.Equal(encryptedPrivateKey, result.PrivateKey);\n\n        await organizationService\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(\n                result,\n                EventType.Organization_Updated);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WhenKeysProvided_AndAlreadySet_DoesNotOverwriteKeys(\n        Guid organizationId,\n        string newPublicKey,\n        string newEncryptedPrivateKey,\n        Organization organization,\n        SutProvider<OrganizationUpdateCommand> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n\n        organization.Id = organizationId;\n        var existingPublicKey = organization.PublicKey;\n        var existingPrivateKey = organization.PrivateKey;\n\n        organizationRepository\n            .GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var request = new OrganizationUpdateRequest\n        {\n            OrganizationId = organizationId,\n            Name = organization.Name,\n            BillingEmail = organization.BillingEmail,\n            Keys = new PublicKeyEncryptionKeyPairData(\n                wrappedPrivateKey: newEncryptedPrivateKey,\n                publicKey: newPublicKey)\n        };\n\n        // Act\n        var result = await sutProvider.Sut.UpdateAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(organizationId, result.Id);\n        Assert.Equal(existingPublicKey, result.PublicKey);\n        Assert.Equal(existingPrivateKey, result.PrivateKey);\n\n        await organizationService\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(\n                result,\n                EventType.Organization_Updated);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_UpdatingNameOnly_UpdatesNameAndNotBillingEmail(\n        Guid organizationId,\n        string newName,\n        Organization organization,\n        SutProvider<OrganizationUpdateCommand> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n        var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();\n\n        organization.Id = organizationId;\n        organization.Name = \"Old Name\";\n        var originalBillingEmail = organization.BillingEmail;\n\n        organizationRepository\n            .GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var request = new OrganizationUpdateRequest\n        {\n            OrganizationId = organizationId,\n            Name = newName,\n            BillingEmail = null\n        };\n\n        // Act\n        var result = await sutProvider.Sut.UpdateAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(organizationId, result.Id);\n        Assert.Equal(newName, result.Name);\n        Assert.Equal(originalBillingEmail, result.BillingEmail);\n\n        await organizationService\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(\n                result,\n                EventType.Organization_Updated);\n        await organizationBillingService\n            .Received(1)\n            .UpdateOrganizationNameAndEmail(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_UpdatingBillingEmailOnly_UpdatesBillingEmailAndNotName(\n        Guid organizationId,\n        string newBillingEmail,\n        Organization organization,\n        SutProvider<OrganizationUpdateCommand> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n        var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();\n\n        organization.Id = organizationId;\n        organization.BillingEmail = \"old@example.com\";\n        var originalName = organization.Name;\n\n        organizationRepository\n            .GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var request = new OrganizationUpdateRequest\n        {\n            OrganizationId = organizationId,\n            Name = null,\n            BillingEmail = newBillingEmail\n        };\n\n        // Act\n        var result = await sutProvider.Sut.UpdateAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(organizationId, result.Id);\n        Assert.Equal(originalName, result.Name);\n        Assert.Equal(newBillingEmail.ToLowerInvariant().Trim(), result.BillingEmail);\n\n        await organizationService\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(\n                result,\n                EventType.Organization_Updated);\n        await organizationBillingService\n            .Received(1)\n            .UpdateOrganizationNameAndEmail(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WhenNoChanges_PreservesBothFields(\n        Guid organizationId,\n        Organization organization,\n        SutProvider<OrganizationUpdateCommand> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationService = sutProvider.GetDependency<IOrganizationService>();\n        var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();\n\n        organization.Id = organizationId;\n        var originalName = organization.Name;\n        var originalBillingEmail = organization.BillingEmail;\n\n        organizationRepository\n            .GetByIdAsync(organizationId)\n            .Returns(organization);\n\n        var request = new OrganizationUpdateRequest\n        {\n            OrganizationId = organizationId,\n            Name = null,\n            BillingEmail = null\n        };\n\n        // Act\n        var result = await sutProvider.Sut.UpdateAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(organizationId, result.Id);\n        Assert.Equal(originalName, result.Name);\n        Assert.Equal(originalBillingEmail, result.BillingEmail);\n\n        await organizationService\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(\n                result,\n                EventType.Organization_Updated);\n        await organizationBillingService\n            .DidNotReceiveWithAnyArgs()\n            .UpdateOrganizationNameAndEmail(Arg.Any<Organization>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_SelfHosted_OnlyUpdatesKeysNotOrganizationDetails(\n        Guid organizationId,\n        string newName,\n        string newBillingEmail,\n        string publicKey,\n        string encryptedPrivateKey,\n        Organization organization,\n        SutProvider<OrganizationUpdateCommand> sutProvider)\n    {\n        // Arrange\n        var organizationBillingService = sutProvider.GetDependency<IOrganizationBillingService>();\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        globalSettings.SelfHosted.Returns(true);\n\n        organization.Id = organizationId;\n        organization.Name = \"Original Name\";\n        organization.BillingEmail = \"original@example.com\";\n        organization.PublicKey = null;\n        organization.PrivateKey = null;\n\n        organizationRepository.GetByIdAsync(organizationId).Returns(organization);\n\n        var request = new OrganizationUpdateRequest\n        {\n            OrganizationId = organizationId,\n            Name = newName, // Should be ignored\n            BillingEmail = newBillingEmail, // Should be ignored\n            Keys = new PublicKeyEncryptionKeyPairData(\n                wrappedPrivateKey: encryptedPrivateKey,\n                publicKey: publicKey)\n        };\n\n        // Act\n        var result = await sutProvider.Sut.UpdateAsync(request);\n\n        // Assert\n        Assert.Equal(\"Original Name\", result.Name); // Not changed\n        Assert.Equal(\"original@example.com\", result.BillingEmail); // Not changed\n        Assert.Equal(publicKey, result.PublicKey); // Changed\n        Assert.Equal(encryptedPrivateKey, result.PrivateKey); // Changed\n\n        await organizationBillingService\n            .DidNotReceiveWithAnyArgs()\n            .UpdateOrganizationNameAndEmail(Arg.Any<Organization>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class OrganizationUpdateKeysCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationKeysAsync_WithoutManageResetPasswordPermission_ThrowsUnauthorizedException(\n        Guid orgId, string publicKey, string privateKey, SutProvider<OrganizationUpdateKeysCommand> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>()\n            .ManageResetPassword(orgId)\n            .Returns(false);\n\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(\n            () => sutProvider.Sut.UpdateOrganizationKeysAsync(orgId, publicKey, privateKey));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationKeysAsync_WhenKeysAlreadyExist_ThrowsBadRequestException(\n        Organization organization, string publicKey, string privateKey,\n        SutProvider<OrganizationUpdateKeysCommand> sutProvider)\n    {\n        organization.PublicKey = \"existingPublicKey\";\n        organization.PrivateKey = \"existingPrivateKey\";\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .ManageResetPassword(organization.Id)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateOrganizationKeysAsync(organization.Id, publicKey, privateKey));\n\n        Assert.Equal(OrganizationUpdateKeysCommand.OrganizationKeysAlreadyExistErrorMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationKeysAsync_WhenKeysDoNotExist_UpdatesOrganization(\n        Organization organization, string publicKey, string privateKey,\n        SutProvider<OrganizationUpdateKeysCommand> sutProvider)\n    {\n        organization.PublicKey = null;\n        organization.PrivateKey = null;\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .ManageResetPassword(organization.Id)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var result = await sutProvider.Sut.UpdateOrganizationKeysAsync(organization.Id, publicKey, privateKey);\n\n        Assert.Equal(publicKey, result.PublicKey);\n        Assert.Equal(privateKey, result.PrivateKey);\n\n        await sutProvider.GetDependency<IOrganizationService>()\n            .Received(1)\n            .UpdateAsync(organization);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommandTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Organizations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;\n\n[SutProviderCustomize]\npublic class SelfHostedOrganizationSignUpCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_WithValidRequest_CreatesOrganizationSuccessfully(\n        User owner, string ownerKey, string collectionName, string publicKey,\n        string privateKey, List<Device> devices,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings);\n\n        SetupCommonMocks(sutProvider, owner);\n        SetupLicenseValidation(sutProvider, license);\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(owner.Id)\n            .Returns(devices);\n\n        // Act\n        var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);\n\n        // Assert\n        Assert.NotNull(result.organization);\n        Assert.NotNull(result.organizationUser);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .CreateAsync(result.organization);\n\n        await sutProvider.GetDependency<IOrganizationApiKeyRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Is<OrganizationApiKey>(key =>\n                key.OrganizationId == result.organization.Id &&\n                key.Type == OrganizationApiKeyType.Default &&\n                !string.IsNullOrEmpty(key.ApiKey)));\n\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(result.organization);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Is<OrganizationUser>(user =>\n                user.OrganizationId == result.organization.Id &&\n                user.UserId == owner.Id &&\n                user.Key == ownerKey &&\n                user.Type == OrganizationUserType.Owner &&\n                user.Status == OrganizationUserStatusType.Confirmed));\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateAsync(\n                Arg.Is<Collection>(c => c.Name == collectionName && c.OrganizationId == result.organization.Id),\n                Arg.Is<IEnumerable<CollectionAccessSelection>>(groups => groups == null),\n                Arg.Is<IEnumerable<CollectionAccessSelection>>(access =>\n                    access.Any(a => a.Id == result.organizationUser.Id && a.Manage && !a.ReadOnly && !a.HidePasswords)));\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncOrgKeysAsync(owner.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_WithPremiumLicense_ThrowsBadRequestException(\n        User owner, string ownerKey, string collectionName,\n        string publicKey, string privateKey,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings, LicenseType.User);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));\n\n        Assert.Contains(\"Premium licenses cannot be applied to an organization\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_WithInvalidLicense_ThrowsBadRequestException(\n        User owner, string ownerKey, string collectionName,\n        string publicKey, string privateKey,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings);\n        license.CanUse(globalSettings, sutProvider.GetDependency<ILicensingService>(), null, out _)\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_WithLicenseAlreadyInUse_ThrowsBadRequestException(\n        User owner, string ownerKey, string collectionName,\n        string publicKey, string privateKey, Organization existingOrganization,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings);\n        existingOrganization.LicenseKey = license.LicenseKey;\n\n        SetupCommonMocks(sutProvider, owner);\n        SetupLicenseValidation(sutProvider, license);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetManyByEnabledAsync()\n            .Returns(new List<Organization> { existingOrganization });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));\n\n        Assert.Contains(\"License is already in use\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_WithClaimsPrincipal_UsesClaimsPrincipalToCreateOrganization(\n        User owner, string ownerKey, string collectionName,\n        string publicKey, string privateKey, ClaimsPrincipal claimsPrincipal,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings);\n\n        SetupCommonMocks(sutProvider, owner);\n        SetupLicenseValidation(sutProvider, license);\n\n        sutProvider.GetDependency<ILicensingService>()\n            .GetClaimsPrincipalFromLicense(license)\n            .Returns(claimsPrincipal);\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(owner.Id)\n            .Returns(new List<Device>());\n\n        // Act\n        var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);\n\n        // Assert\n        Assert.NotNull(result.organization);\n        Assert.NotNull(result.organizationUser);\n\n        sutProvider.GetDependency<ILicensingService>()\n            .Received(1)\n            .GetClaimsPrincipalFromLicense(license);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_WithoutCollectionName_DoesNotCreateCollection(\n        User owner, string ownerKey, string publicKey, string privateKey,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings);\n\n        SetupCommonMocks(sutProvider, owner);\n        SetupLicenseValidation(sutProvider, license);\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(owner.Id)\n            .Returns(new List<Device>());\n\n        // Act\n        var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, null, publicKey, privateKey);\n\n        // Assert\n        Assert.NotNull(result.organization);\n        Assert.NotNull(result.organizationUser);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateAsync(Arg.Any<Collection>(), Arg.Is<IEnumerable<CollectionAccessSelection>>(x => true), Arg.Is<IEnumerable<CollectionAccessSelection>>(x => true));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_WithDevices_RegistersDevicesForPushNotifications(\n        User owner, string ownerKey, string collectionName,\n        string publicKey, string privateKey, List<Device> devices,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings);\n\n        foreach (var device in devices)\n        {\n            device.PushToken = \"push-token-\" + device.Id;\n        }\n\n        SetupCommonMocks(sutProvider, owner);\n        SetupLicenseValidation(sutProvider, license);\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(owner.Id)\n            .Returns(devices);\n\n        // Act\n        var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);\n\n        // Assert\n        Assert.NotNull(result.organization);\n        Assert.NotNull(result.organizationUser);\n\n        var expectedDeviceIds = devices.Select(d => d.Id.ToString());\n        await sutProvider.GetDependency<IPushRegistrationService>()\n            .Received(1)\n            .AddUserRegistrationOrganizationAsync(\n                Arg.Is<IEnumerable<string>>(ids => ids.SequenceEqual(expectedDeviceIds)),\n                result.organization.Id.ToString());\n    }\n\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_OnException_CleansUpOrganization(\n        User owner, string ownerKey, string collectionName,\n        string publicKey, string privateKey,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings);\n\n        SetupCommonMocks(sutProvider, owner);\n        SetupLicenseValidation(sutProvider, license);\n\n        sutProvider.GetDependency<IOrganizationApiKeyRepository>()\n            .CreateAsync(Arg.Any<OrganizationApiKey>())\n            .Throws(new Exception(\"Test exception\"));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<Exception>(\n            () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .DeleteAsync(Arg.Any<Organization>());\n\n        await sutProvider.GetDependency<IApplicationCacheService>()\n            .Received(1)\n            .DeleteOrganizationAbilityAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_WithSingleOrgPolicy_ThrowsBadRequest(\n        User owner, string ownerKey, string collectionName,\n        string publicKey, string privateKey,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings);\n\n        SetupCommonMocks(sutProvider, owner);\n        SetupLicenseValidation(sutProvider, license);\n\n        // User has SingleOrg policy from another org\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(owner.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.EnabledForAnotherOrganization());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey));\n\n        Assert.Contains(\"You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SignUpAsync_WithoutSingleOrgPolicy_Succeeds(\n        User owner, string ownerKey, string collectionName,\n        string publicKey, string privateKey, List<Device> devices,\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        var license = CreateValidOrganizationLicense(globalSettings);\n\n        SetupCommonMocks(sutProvider, owner);\n        SetupLicenseValidation(sutProvider, license);\n\n        // No SingleOrg policy\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(owner.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(owner.Id)\n            .Returns(devices);\n\n        // Act\n        var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey);\n\n        // Assert\n        Assert.NotNull(result.organization);\n        Assert.NotNull(result.organizationUser);\n\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1).CreateAsync(result.organization);\n    }\n\n    private void SetupCommonMocks(\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider,\n        User owner)\n    {\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .CreateAsync(Arg.Any<Organization>())\n            .Returns(callInfo =>\n            {\n                var org = callInfo.Arg<Organization>();\n                org.Id = Guid.NewGuid();\n                return Task.FromResult(org);\n            });\n\n        globalSettings.LicenseDirectory.Returns(\"/tmp/licenses\");\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<SingleOrganizationPolicyRequirement>(owner.Id)\n            .Returns(SingleOrganizationPolicyRequirementTestFactory.NoSinglePolicyOrganizationsForUser());\n    }\n\n    private void SetupLicenseValidation(\n        SutProvider<SelfHostedOrganizationSignUpCommand> sutProvider,\n        OrganizationLicense license)\n    {\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n\n        sutProvider.GetDependency<ILicensingService>()\n            .VerifyLicense(license)\n            .Returns(true);\n\n        license.CanUse(globalSettings, sutProvider.GetDependency<ILicensingService>(), null, out _)\n            .Returns(true);\n    }\n\n    private OrganizationLicense CreateValidOrganizationLicense(\n        IGlobalSettings globalSettings,\n        LicenseType licenseType = LicenseType.Organization)\n    {\n        return new OrganizationLicense\n        {\n            LicenseType = licenseType,\n            Signature = Guid.NewGuid().ToString().Replace('-', '+'),\n            Issued = DateTime.UtcNow.AddDays(-1),\n            Expires = DateTime.UtcNow.AddDays(10),\n            Version = OrganizationLicense.CurrentLicenseFileVersion,\n            InstallationId = globalSettings.Installation.Id,\n            Enabled = true,\n            SelfHost = true\n        };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationOrganizationPolicyComplianceValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\n\n[SutProviderCustomize]\npublic class AutomaticUserConfirmationOrganizationPolicyComplianceValidatorTests\n{\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_AllUsersCompliant_NoProviders_ReturnsValid(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange\n        var orgUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([orgUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_UserInAnotherOrg_ReturnsUserNotCompliantWithSingleOrganization(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange\n        var orgUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        var otherOrgUser = new OrganizationUser\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(), // Different org\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([orgUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([otherOrgUser]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<UserNotCompliantWithSingleOrganization>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_ProviderUsersExist_ReturnsProviderExistsInOrganization(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange\n        var orgUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        var providerUser = new ProviderUser\n        {\n            Id = Guid.NewGuid(),\n            ProviderId = Guid.NewGuid(),\n            UserId = userId\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([orgUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([providerUser]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<ProviderExistsInOrganization>(result.AsError);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_InvitedUsersExcluded_FromSingleOrgCheck(\n        Guid organizationId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange - invited user has null UserId and Invited status\n        var invitedUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = null,\n            Status = OrganizationUserStatusType.Invited,\n            Email = \"invited@example.com\"\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([invitedUser]);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n\n        // Invited users with null UserId should not trigger the single org query\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_InvitedUserWithUserId_ExcludedFromSingleOrgCheck(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange - Invited status users are excluded regardless of UserId\n        var invitedUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId,\n            Status = OrganizationUserStatusType.Invited\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([invitedUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n\n        // Invited users should not be included in the single org compliance query\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => !ids.Any()));\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_UserInAnotherOrgWithInvitedStatus_ReturnsValid(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange\n        var orgUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        // User has an Invited status in another org - should not count as non-compliant\n        var otherOrgUser = new OrganizationUser\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            UserId = userId,\n            Status = OrganizationUserStatusType.Invited\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([orgUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([otherOrgUser]);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_SingleOrgViolationTakesPrecedence_OverProviderCheck(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange - user is in another org AND is a provider user\n        var orgUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        var otherOrgUser = new OrganizationUser\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([orgUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([otherOrgUser]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<UserNotCompliantWithSingleOrganization>(result.AsError);\n\n        // Provider check should not be called since single org check failed first\n        await sutProvider.GetDependency<IProviderUserRepository>()\n            .DidNotReceive()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_MixedUsers_OnlyNonInvitedChecked(\n        Guid organizationId,\n        Guid confirmedUserId,\n        Guid acceptedUserId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange\n        var invitedUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = null,\n            Status = OrganizationUserStatusType.Invited,\n            Email = \"invited@example.com\"\n        };\n\n        var confirmedUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = confirmedUserId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        var acceptedUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = acceptedUserId,\n            Status = OrganizationUserStatusType.Accepted\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([invitedUser, confirmedUser, acceptedUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n\n        // Only confirmed and accepted users should be checked for single org compliance\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids =>\n                ids.Count() == 2 &&\n                ids.Contains(confirmedUserId) &&\n                ids.Contains(acceptedUserId)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_NoOrganizationUsers_ReturnsValid(\n        Guid organizationId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_UserInSameOrgOnly_ReturnsValid(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange\n        var orgUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        // User exists in the same org only (the GetManyByManyUsersAsync returns same-org entry)\n        var sameOrgUser = new OrganizationUser\n        {\n            Id = orgUser.Id,\n            OrganizationId = organizationId,\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([orgUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([sameOrgUser]);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_ProviderCheckIncludesAllUsersWithUserIds(\n        Guid organizationId,\n        Guid userId1,\n        Guid userId2,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange - provider check includes users regardless of Invited status (only excludes null UserId)\n        var confirmedUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId1,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        var invitedUserWithNullId = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = null,\n            Status = OrganizationUserStatusType.Invited,\n            Email = \"invited@example.com\"\n        };\n\n        var acceptedUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId2,\n            Status = OrganizationUserStatusType.Accepted\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([confirmedUser, invitedUserWithNullId, acceptedUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n\n        // Provider check should include all users with non-null UserIds (confirmed + accepted)\n        await sutProvider.GetDependency<IProviderUserRepository>()\n            .Received(1)\n            .GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids =>\n                ids.Count() == 2 &&\n                ids.Contains(userId1) &&\n                ids.Contains(userId2)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsOrganizationCompliantAsync_RevokedUserInAnotherOrg_ReturnsUserNotCompliant(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<AutomaticUserConfirmationOrganizationPolicyComplianceValidator> sutProvider)\n    {\n        // Arrange\n        var revokedUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organizationId,\n            UserId = userId,\n            Status = OrganizationUserStatusType.Revoked\n        };\n\n        var otherOrgUser = new OrganizationUser\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            UserId = userId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(organizationId)\n            .Returns([revokedUser]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns([otherOrgUser]);\n\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organizationId);\n\n        // Act\n        var result = await sutProvider.Sut.IsOrganizationCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<UserNotCompliantWithSingleOrganization>(result.AsError);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\n\n[SutProviderCustomize]\npublic class AutomaticUserConfirmationPolicyEnforcementValidatorTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task IsCompliantAsync_WithPolicyEnabledAndUserIsProviderMember_ReturnsProviderUsersCannotJoinError(\n        SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,\n        OrganizationUser organizationUser,\n        ProviderUser providerUser,\n        User user)\n    {\n        // Arrange\n        organizationUser.UserId = providerUser.UserId = user.Id;\n\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = organizationUser.OrganizationId,\n            PolicyType = PolicyType.AutomaticUserConfirmation\n        };\n\n        var request = new AutomaticUserConfirmationPolicyEnforcementRequest(\n            organizationUser.OrganizationId,\n            [organizationUser],\n            user);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails]));\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([providerUser]);\n\n        // Act\n        var result = await sutProvider.Sut.IsCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<ProviderUsersCannotJoin>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task IsCompliantAsync_WithPolicyEnabledOnOtherOrganization_ReturnsOtherOrganizationDoesNotAllowOtherMembershipError(\n        SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,\n        OrganizationUser organizationUser,\n        OrganizationUser otherOrganizationUser,\n        User user)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        otherOrganizationUser.UserId = user.Id;\n\n        var otherOrgId = Guid.NewGuid();\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = otherOrgId, // Different from organizationUser.OrganizationId\n            PolicyType = PolicyType.AutomaticUserConfirmation\n        };\n\n        var request = new AutomaticUserConfirmationPolicyEnforcementRequest(\n            organizationUser.OrganizationId,\n            [organizationUser, otherOrganizationUser],\n            user);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails]));\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([]);\n\n        // Act\n        var result = await sutProvider.Sut.IsCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<OtherOrganizationDoesNotAllowOtherMembership>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task IsCompliantAsync_WithPolicyDisabledUserIsAMemberOfAnotherOrgReturnsValid(\n        SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,\n        OrganizationUser organizationUser,\n        OrganizationUser otherOrgUser,\n        User user)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        otherOrgUser.UserId = user.Id;\n\n        var request = new AutomaticUserConfirmationPolicyEnforcementRequest(\n            organizationUser.OrganizationId,\n            [organizationUser, otherOrgUser],\n            user);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([]));\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([]);\n\n        // Act\n        var result = await sutProvider.Sut.IsCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task IsCompliantAsync_WithPolicyEnabledUserIsAMemberOfAnotherOrg_ReturnsCannotBeMemberOfAnotherOrgError(\n        SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,\n        OrganizationUser organizationUser,\n        OrganizationUser otherOrgUser,\n        User user)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n        otherOrgUser.UserId = user.Id;\n\n        var request = new AutomaticUserConfirmationPolicyEnforcementRequest(\n            organizationUser.OrganizationId,\n            [organizationUser, otherOrgUser],\n            user);\n\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = organizationUser.OrganizationId,\n            PolicyType = PolicyType.AutomaticUserConfirmation\n        };\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails]));\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([]);\n\n        // Act\n        var result = await sutProvider.Sut.IsCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<UserCannotBelongToAnotherOrganization>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task IsCompliantAsync_WithPolicyEnabledAndChecksConditionsInCorrectOrder_ReturnsFirstFailure(\n        SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,\n        OrganizationUser organizationUser,\n        OrganizationUser otherOrgUser,\n        ProviderUser providerUser,\n        User user)\n    {\n        // Arrange\n        var policyDetails = new PolicyDetails\n        {\n            OrganizationId = organizationUser.OrganizationId,\n            PolicyType = PolicyType.AutomaticUserConfirmation,\n            OrganizationUserId = organizationUser.Id\n        };\n\n        var request = new AutomaticUserConfirmationPolicyEnforcementRequest(\n            organizationUser.OrganizationId,\n            [organizationUser, otherOrgUser],\n            user);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([policyDetails]));\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([providerUser]);\n\n        // Act\n        var result = await sutProvider.Sut.IsCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsError);\n        Assert.IsType<CurrentOrganizationUserIsNotPresentInRequest>(result.AsError);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task IsCompliantAsync_WithPolicyIsEnabledNoOtherOrganizationsAndNotAProvider_ReturnsValid(\n        SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,\n        OrganizationUser organizationUser,\n        User user)\n    {\n        // Arrange\n        organizationUser.UserId = user.Id;\n\n        var request = new AutomaticUserConfirmationPolicyEnforcementRequest(\n            organizationUser.OrganizationId,\n            [organizationUser],\n            user);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([\n                new PolicyDetails\n                {\n                    OrganizationUserId = organizationUser.Id,\n                    OrganizationId = organizationUser.OrganizationId,\n                    PolicyType = PolicyType.AutomaticUserConfirmation,\n                }\n            ]));\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([]);\n\n        // Act\n        var result = await sutProvider.Sut.IsCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task IsCompliantAsync_WithPolicyDisabledForCurrentAndOtherOrg_ReturnsValid(\n        SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,\n        OrganizationUser organizationUser,\n        OrganizationUser otherOrgUser,\n        User user)\n    {\n        // Arrange\n        otherOrgUser.UserId = organizationUser.UserId = user.Id;\n\n        var request = new AutomaticUserConfirmationPolicyEnforcementRequest(\n            organizationUser.OrganizationId,\n            [organizationUser],\n            user);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([]));\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([]);\n\n        // Act\n        var result = await sutProvider.Sut.IsCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task IsCompliantAsync_WithPolicyDisabledForCurrentAndOtherOrgAndIsProvider_ReturnsValid(\n        SutProvider<AutomaticUserConfirmationPolicyEnforcementValidator> sutProvider,\n        OrganizationUser organizationUser,\n        OrganizationUser otherOrgUser,\n        ProviderUser providerUser,\n        User user)\n    {\n        // Arrange\n        providerUser.UserId = otherOrgUser.UserId = organizationUser.UserId = user.Id;\n\n        var request = new AutomaticUserConfirmationPolicyEnforcementRequest(\n            organizationUser.OrganizationId,\n            [organizationUser],\n            user);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([]));\n\n        sutProvider.GetDependency<IProviderUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns([providerUser]);\n\n        // Act\n        var result = await sutProvider.Sut.IsCompliantAsync(request);\n\n        // Assert\n        Assert.True(result.IsValid);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlerHandlerFactoryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing OneOf.Types;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\n\npublic class PolicyEventHandlerHandlerFactoryTests\n{\n    [Fact]\n    public void GetHandler_ReturnsHandler_WhenHandlerExists()\n    {\n        // Arrange\n        var expectedHandler = new FakeSingleOrgDependencyEvent();\n        var factory = new PolicyEventHandlerHandlerFactory([expectedHandler]);\n\n        // Act\n        var result = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.Equal(expectedHandler, result.AsT0);\n    }\n\n    [Fact]\n    public void GetHandler_ReturnsNone_WhenHandlerDoesNotExist()\n    {\n        // Arrange\n        var factory = new PolicyEventHandlerHandlerFactory([new FakeSingleOrgDependencyEvent()]);\n\n        // Act\n        var result = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.RequireSso);\n\n        // Assert\n        Assert.True(result.IsT1);\n        Assert.IsType<None>(result.AsT1);\n    }\n\n    [Fact]\n    public void GetHandler_ReturnsNone_WhenHandlerTypeDoesNotMatch()\n    {\n        // Arrange\n        var factory = new PolicyEventHandlerHandlerFactory([new FakeSingleOrgDependencyEvent()]);\n\n        // Act\n        var result = factory.GetHandler<IPolicyValidationEvent>(PolicyType.SingleOrg);\n\n        // Assert\n        Assert.True(result.IsT1);\n        Assert.IsType<None>(result.AsT1);\n    }\n\n    [Fact]\n    public void GetHandler_ReturnsCorrectHandler_WhenMultipleHandlerTypesExist()\n    {\n        // Arrange\n        var dependencyEvent = new FakeSingleOrgDependencyEvent();\n        var validationEvent = new FakeSingleOrgValidationEvent();\n        var factory = new PolicyEventHandlerHandlerFactory([dependencyEvent, validationEvent]);\n\n        // Act\n        var dependencyResult = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg);\n        var validationResult = factory.GetHandler<IPolicyValidationEvent>(PolicyType.SingleOrg);\n\n        // Assert\n        Assert.True(dependencyResult.IsT0);\n        Assert.Equal(dependencyEvent, dependencyResult.AsT0);\n\n        Assert.True(validationResult.IsT0);\n        Assert.Equal(validationEvent, validationResult.AsT0);\n    }\n\n    [Fact]\n    public void GetHandler_ReturnsCorrectHandler_WhenMultiplePolicyTypesExist()\n    {\n        // Arrange\n        var singleOrgEvent = new FakeSingleOrgDependencyEvent();\n        var requireSsoEvent = new FakeRequireSsoDependencyEvent();\n        var factory = new PolicyEventHandlerHandlerFactory([singleOrgEvent, requireSsoEvent]);\n\n        // Act\n        var singleOrgResult = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg);\n        var requireSsoResult = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.RequireSso);\n\n        // Assert\n        Assert.True(singleOrgResult.IsT0);\n        Assert.Equal(singleOrgEvent, singleOrgResult.AsT0);\n\n        Assert.True(requireSsoResult.IsT0);\n        Assert.Equal(requireSsoEvent, requireSsoResult.AsT0);\n    }\n\n    [Fact]\n    public void GetHandler_Throws_WhenDuplicateHandlersExist()\n    {\n        // Arrange\n        var factory = new PolicyEventHandlerHandlerFactory([\n            new FakeSingleOrgDependencyEvent(),\n            new FakeSingleOrgDependencyEvent()\n        ]);\n\n        // Act & Assert\n        var exception = Assert.Throws<InvalidOperationException>(() =>\n            factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg));\n\n        Assert.Contains(\"Multiple IPolicyUpdateEvent handlers of type IEnforceDependentPoliciesEvent found for PolicyType SingleOrg\", exception.Message);\n        Assert.Contains(\"Expected one IEnforceDependentPoliciesEvent handler per PolicyType\", exception.Message);\n    }\n\n    [Fact]\n    public void GetHandler_ReturnsNone_WhenNoHandlersProvided()\n    {\n        // Arrange\n        var factory = new PolicyEventHandlerHandlerFactory([]);\n\n        // Act\n        var result = factory.GetHandler<IEnforceDependentPoliciesEvent>(PolicyType.SingleOrg);\n\n        // Assert\n        Assert.True(result.IsT1);\n        Assert.IsType<None>(result.AsT1);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementFixtures.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\n\n/// <summary>\n/// Intentionally simplified PolicyRequirement that just holds the input PolicyDetails for us to assert against.\n/// </summary>\npublic class TestPolicyRequirement : IPolicyRequirement\n{\n    public IEnumerable<PolicyDetails> Policies { get; init; } = [];\n}\n\npublic class TestPolicyRequirementFactory(Func<PolicyDetails, bool> enforce) : IPolicyRequirementFactory<TestPolicyRequirement>\n{\n    public PolicyType PolicyType => PolicyType.SingleOrg;\n\n    public bool Enforce(PolicyDetails policyDetails) => enforce(policyDetails);\n\n    public TestPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)\n        => new() { Policies = policyDetails };\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\n\n[SutProviderCustomize]\npublic class PolicyRequirementQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task GetAsync_CallsEnforceCallback(Guid userId)\n    {\n        // Arrange policies\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId };\n        var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId };\n        policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(userId)), PolicyType.SingleOrg)\n            .Returns([thisPolicy, otherPolicy]);\n\n        // Arrange a substitute Enforce function so that we can inspect the received calls\n        var callback = Substitute.For<Func<PolicyDetails, bool>>();\n        callback(Arg.Any<PolicyDetails>()).Returns(x => x.Arg<PolicyDetails>() == thisPolicy);\n\n        // Arrange the sut\n        var factory = new TestPolicyRequirementFactory(callback);\n        var sut = new PolicyRequirementQuery(policyRepository, [factory]);\n\n        // Act\n        var requirement = await sut.GetAsync<TestPolicyRequirement>(userId);\n\n        // Assert\n        Assert.Contains(thisPolicy, requirement.Policies);\n        Assert.DoesNotContain(otherPolicy, requirement.Policies);\n        callback.Received()(Arg.Is(thisPolicy));\n        callback.Received()(Arg.Is(otherPolicy));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_ThrowsIfNoFactoryRegistered(Guid userId)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        var sut = new PolicyRequirementQuery(policyRepository, []);\n\n        var exception = await Assert.ThrowsAsync<NotImplementedException>(()\n            => sut.GetAsync<TestPolicyRequirement>(userId));\n        Assert.Contains(\"No Requirement Factory found\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_HandlesNoPolicies(Guid userId)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(userId)), PolicyType.SingleOrg)\n            .Returns([]);\n\n        var factory = new TestPolicyRequirementFactory(x => x.IsProvider);\n        var sut = new PolicyRequirementQuery(policyRepository, [factory]);\n\n        var requirement = await sut.GetAsync<TestPolicyRequirement>(userId);\n\n        Assert.Empty(requirement.Policies);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsyncVNext_CallsEnforceCallback(Guid userId)\n    {\n        // Arrange policies\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };\n        var otherPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg };\n        policyRepository.GetPolicyDetailsByUserIdAndPolicyTypeAsync(userId, PolicyType.SingleOrg)\n            .Returns([thisPolicy, otherPolicy]);\n\n        // Arrange a substitute Enforce function so that we can inspect the received calls\n        var callback = Substitute.For<Func<PolicyDetails, bool>>();\n        callback(Arg.Any<PolicyDetails>()).Returns(x => x.Arg<PolicyDetails>() == thisPolicy);\n\n        // Arrange the sut\n        var factory = new TestPolicyRequirementFactory(callback);\n        var sut = new PolicyRequirementQuery(policyRepository, [factory]);\n\n        // Act\n        var requirement = await sut.GetAsyncVNext<TestPolicyRequirement>(userId);\n\n        // Assert\n        Assert.Contains(thisPolicy, requirement.Policies);\n        Assert.DoesNotContain(otherPolicy, requirement.Policies);\n        callback.Received()(Arg.Is(thisPolicy));\n        callback.Received()(Arg.Is(otherPolicy));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsyncVNext_ThrowsIfNoFactoryRegistered(Guid userId)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        var sut = new PolicyRequirementQuery(policyRepository, []);\n\n        var exception = await Assert.ThrowsAsync<NotImplementedException>(()\n            => sut.GetAsyncVNext<TestPolicyRequirement>(userId));\n        Assert.Contains(\"No Requirement Factory found\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsyncVNext_HandlesNoPolicies(Guid userId)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        policyRepository.GetPolicyDetailsByUserIdAndPolicyTypeAsync(userId, PolicyType.SingleOrg)\n            .Returns([]);\n\n        var factory = new TestPolicyRequirementFactory(x => x.IsProvider);\n        var sut = new PolicyRequirementQuery(policyRepository, [factory]);\n\n        var requirement = await sut.GetAsyncVNext<TestPolicyRequirement>(userId);\n\n        Assert.Empty(requirement.Policies);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_WithMultipleUserIds_ReturnsRequirementPerUser(Guid userIdA, Guid userIdB)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        var policyA = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userIdA };\n        var policyB = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userIdB };\n        policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(\n                Arg.Any<IEnumerable<Guid>>(), PolicyType.SingleOrg)\n            .Returns([policyA, policyB]);\n\n        var factory = new TestPolicyRequirementFactory(_ => true);\n        var sut = new PolicyRequirementQuery(policyRepository, [factory]);\n\n        var requirements = (await sut.GetAsync<TestPolicyRequirement>([userIdA, userIdB])).ToList();\n\n        Assert.Equal(2, requirements.Count);\n        Assert.Equal(userIdA, requirements[0].UserId);\n        Assert.Equal(userIdB, requirements[1].UserId);\n        Assert.Contains(policyA, requirements[0].Requirement.Policies);\n        Assert.DoesNotContain(policyB, requirements[0].Requirement.Policies);\n        Assert.Contains(policyB, requirements[1].Requirement.Policies);\n        Assert.DoesNotContain(policyA, requirements[1].Requirement.Policies);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_WithMultipleUserIds_CallsEnforceCallback(Guid userIdA, Guid userIdB)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        var policyA = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userIdA };\n        var policyB = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userIdB };\n        policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(\n                Arg.Any<IEnumerable<Guid>>(), PolicyType.SingleOrg)\n            .Returns([policyA, policyB]);\n\n        var callback = Substitute.For<Func<PolicyDetails, bool>>();\n        callback(Arg.Any<PolicyDetails>()).Returns(x => x.Arg<PolicyDetails>() == policyA);\n\n        var factory = new TestPolicyRequirementFactory(callback);\n        var sut = new PolicyRequirementQuery(policyRepository, [factory]);\n\n        var requirements = (await sut.GetAsync<TestPolicyRequirement>([userIdA, userIdB])).ToList();\n\n        Assert.Contains(policyA, requirements[0].Requirement.Policies);\n        Assert.Empty(requirements[1].Requirement.Policies);\n        callback.Received()(Arg.Is(policyA));\n        callback.Received()(Arg.Is(policyB));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_WithMultipleUserIds_FiltersOutPoliciesThatAreNotEnforced(Guid userIdA, Guid userIdB)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        var enforcedPolicyA = new OrganizationPolicyDetails\n        { PolicyType = PolicyType.SingleOrg, UserId = userIdA, IsProvider = false };\n        var notEnforcedPolicyA = new OrganizationPolicyDetails\n        { PolicyType = PolicyType.SingleOrg, UserId = userIdA, IsProvider = true };\n        var enforcedPolicyB = new OrganizationPolicyDetails\n        { PolicyType = PolicyType.SingleOrg, UserId = userIdB, IsProvider = false };\n        policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(\n                Arg.Any<IEnumerable<Guid>>(), PolicyType.SingleOrg)\n            .Returns([enforcedPolicyA, notEnforcedPolicyA, enforcedPolicyB]);\n\n        // Enforce returns false for providers (filtering them out)\n        var factory = new TestPolicyRequirementFactory(p => !p.IsProvider);\n        var sut = new PolicyRequirementQuery(policyRepository, [factory]);\n\n        var requirements = (await sut.GetAsync<TestPolicyRequirement>([userIdA, userIdB])).ToList();\n\n        Assert.Equal(2, requirements.Count);\n        Assert.Contains(enforcedPolicyA, requirements[0].Requirement.Policies);\n        Assert.DoesNotContain(notEnforcedPolicyA, requirements[0].Requirement.Policies);\n        Assert.Contains(enforcedPolicyB, requirements[1].Requirement.Policies);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_WithMultipleUserIds_ThrowsIfNoFactoryRegistered(Guid userIdA, Guid userIdB)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        var sut = new PolicyRequirementQuery(policyRepository, []);\n\n        var exception = await Assert.ThrowsAsync<NotImplementedException>(()\n            => sut.GetAsync<TestPolicyRequirement>([userIdA, userIdB]));\n        Assert.Contains(\"No Requirement Factory found\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_WithMultipleUserIds_HandlesNoPolicies(Guid userIdA, Guid userIdB)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(\n                Arg.Any<IEnumerable<Guid>>(), PolicyType.SingleOrg)\n            .Returns([]);\n\n        var factory = new TestPolicyRequirementFactory(_ => true);\n        var sut = new PolicyRequirementQuery(policyRepository, [factory]);\n\n        var requirements = (await sut.GetAsync<TestPolicyRequirement>([userIdA, userIdB])).ToList();\n\n        Assert.Equal(2, requirements.Count);\n        Assert.Equal(userIdA, requirements[0].UserId);\n        Assert.Equal(userIdB, requirements[1].UserId);\n        Assert.Empty(requirements[0].Requirement.Policies);\n        Assert.Empty(requirements[1].Requirement.Policies);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_WithMultipleUserIds_ReturnsEmptyRequirementForUserWithoutPolicies(\n        Guid userIdA, Guid userIdB)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n        var policyA = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userIdA };\n        // Only userIdA has a policy, userIdB has none\n        policyRepository.GetPolicyDetailsByUserIdsAndPolicyType(\n                Arg.Any<IEnumerable<Guid>>(), PolicyType.SingleOrg)\n            .Returns([policyA]);\n\n        var factory = new TestPolicyRequirementFactory(_ => true);\n        var sut = new PolicyRequirementQuery(policyRepository, [factory]);\n\n        var requirements = (await sut.GetAsync<TestPolicyRequirement>([userIdA, userIdB])).ToList();\n\n        Assert.Equal(2, requirements.Count);\n        Assert.Equal(userIdA, requirements[0].UserId);\n        Assert.Equal(userIdB, requirements[1].UserId);\n        Assert.Contains(policyA, requirements[0].Requirement.Policies);\n        Assert.Empty(requirements[1].Requirement.Policies);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/AutomaticUserConfirmationPolicyRequirementTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Enums;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\npublic class AutomaticUserConfirmationPolicyRequirementTests\n{\n    [Theory]\n    [InlineData(OrganizationUserStatusType.Accepted)]\n    [InlineData(OrganizationUserStatusType.Confirmed)]\n    [InlineData(OrganizationUserStatusType.Revoked)]\n    public void CannotGrantEmergencyAccess_WithActiveStatus_ReturnsTrue(OrganizationUserStatusType status)\n    {\n        var policyDetails = new[]\n        {\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                PolicyType = PolicyType.AutomaticUserConfirmation,\n                OrganizationUserStatus = status\n            }\n        };\n\n        var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);\n\n        Assert.True(sut.GrantorCannotInviteToEmergencyAccess());\n    }\n\n    [Fact]\n    public void CannotGrantEmergencyAccess_WithInvitedStatus_ReturnsFalse()\n    {\n        var policyDetails = new[]\n        {\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                PolicyType = PolicyType.AutomaticUserConfirmation,\n                OrganizationUserStatus = OrganizationUserStatusType.Invited\n            }\n        };\n\n        var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);\n\n        Assert.False(sut.GrantorCannotInviteToEmergencyAccess());\n    }\n\n    [Fact]\n    public void CannotGrantEmergencyAccess_WithNoPolicies_ReturnsFalse()\n    {\n        var sut = new AutomaticUserConfirmationPolicyRequirement([]);\n\n        Assert.False(sut.GrantorCannotInviteToEmergencyAccess());\n    }\n\n    [Theory]\n    [InlineData(OrganizationUserStatusType.Accepted)]\n    [InlineData(OrganizationUserStatusType.Confirmed)]\n    [InlineData(OrganizationUserStatusType.Revoked)]\n    public void CannotBeGrantedEmergencyAccess_WithActiveStatus_ReturnsTrue(OrganizationUserStatusType status)\n    {\n        var policyDetails = new[]\n        {\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                PolicyType = PolicyType.AutomaticUserConfirmation,\n                OrganizationUserStatus = status\n            }\n        };\n\n        var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);\n\n        Assert.True(sut.GranteeCannotAcceptEmergencyAccess());\n    }\n\n    [Fact]\n    public void CannotBeGrantedEmergencyAccess_WithInvitedStatus_ReturnsFalse()\n    {\n        var policyDetails = new[]\n        {\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                PolicyType = PolicyType.AutomaticUserConfirmation,\n                OrganizationUserStatus = OrganizationUserStatusType.Invited\n            }\n        };\n\n        var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);\n\n        Assert.False(sut.GranteeCannotAcceptEmergencyAccess());\n    }\n\n    [Fact]\n    public void CannotBeGrantedEmergencyAccess_WithNoPolicies_ReturnsFalse()\n    {\n        var sut = new AutomaticUserConfirmationPolicyRequirement([]);\n\n        Assert.False(sut.GranteeCannotAcceptEmergencyAccess());\n    }\n\n    [Fact]\n    public void CannotGrantEmergencyAccess_WithMultiplePolicies_OneActive_ReturnsTrue()\n    {\n        var policyDetails = new[]\n        {\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                PolicyType = PolicyType.AutomaticUserConfirmation,\n                OrganizationUserStatus = OrganizationUserStatusType.Invited\n            },\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                PolicyType = PolicyType.AutomaticUserConfirmation,\n                OrganizationUserStatus = OrganizationUserStatusType.Confirmed\n            }\n        };\n\n        var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);\n\n        Assert.True(sut.GrantorCannotInviteToEmergencyAccess());\n    }\n\n    [Fact]\n    public void CannotBeGrantedEmergencyAccess_WithMultiplePolicies_OneActive_ReturnsTrue()\n    {\n        var policyDetails = new[]\n        {\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                PolicyType = PolicyType.AutomaticUserConfirmation,\n                OrganizationUserStatus = OrganizationUserStatusType.Invited\n            },\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                PolicyType = PolicyType.AutomaticUserConfirmation,\n                OrganizationUserStatus = OrganizationUserStatusType.Confirmed\n            }\n        };\n\n        var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);\n\n        Assert.True(sut.GranteeCannotAcceptEmergencyAccess());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactoryTests.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\npublic class BasePolicyRequirementFactoryTests\n{\n    [Theory, AutoData]\n    public void ExemptRoles_DoesNotEnforceAgainstThoseRoles(\n        [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner)] PolicyDetails ownerPolicy,\n        [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Admin)] PolicyDetails adminPolicy,\n        [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Custom)] PolicyDetails customPolicy,\n        [PolicyDetails(PolicyType.SingleOrg)] PolicyDetails userPolicy)\n    {\n        var sut = new TestPolicyRequirementFactory(\n            // These exempt roles are intentionally unusual to make sure we're properly testing the sut\n            [OrganizationUserType.User, OrganizationUserType.Custom],\n            [],\n            false);\n\n        Assert.True(sut.Enforce(ownerPolicy));\n        Assert.True(sut.Enforce(adminPolicy));\n        Assert.False(sut.Enforce(customPolicy));\n        Assert.False(sut.Enforce(userPolicy));\n    }\n\n    [Theory, AutoData]\n    public void ExemptStatuses_DoesNotEnforceAgainstThoseStatuses(\n        [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Invited)] PolicyDetails invitedPolicy,\n        [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Accepted)] PolicyDetails acceptedPolicy,\n        [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails confirmedPolicy,\n        [PolicyDetails(PolicyType.SingleOrg, userStatus: OrganizationUserStatusType.Revoked)] PolicyDetails revokedPolicy)\n    {\n        var sut = new TestPolicyRequirementFactory(\n            [],\n            // These exempt statuses are intentionally unusual to make sure we're properly testing the sut\n            [OrganizationUserStatusType.Confirmed, OrganizationUserStatusType.Accepted],\n            false);\n\n        Assert.True(sut.Enforce(invitedPolicy));\n        Assert.True(sut.Enforce(revokedPolicy));\n        Assert.False(sut.Enforce(confirmedPolicy));\n        Assert.False(sut.Enforce(acceptedPolicy));\n    }\n\n    [Theory, AutoData]\n    public void ExemptProviders_DoesNotEnforceAgainstProviders(\n        [PolicyDetails(PolicyType.SingleOrg, isProvider: true)] PolicyDetails policy)\n    {\n        var sut = new TestPolicyRequirementFactory(\n            [],\n            [],\n            true);\n\n        Assert.False(sut.Enforce(policy));\n    }\n\n    [Theory, AutoData]\n    public void NoExemptions_EnforcesAgainstAdminsAndProviders(\n        [PolicyDetails(PolicyType.SingleOrg, OrganizationUserType.Owner, isProvider: true)] PolicyDetails policy)\n    {\n        var sut = new TestPolicyRequirementFactory(\n            [],\n            [],\n            false);\n\n        Assert.True(sut.Enforce(policy));\n    }\n\n    private class TestPolicyRequirementFactory(\n        IEnumerable<OrganizationUserType> exemptRoles,\n        IEnumerable<OrganizationUserStatusType> exemptStatuses,\n        bool exemptProviders\n        ) : BasePolicyRequirementFactory<TestPolicyRequirement>\n    {\n        public override PolicyType PolicyType => PolicyType.SingleOrg;\n        protected override IEnumerable<OrganizationUserType> ExemptRoles => exemptRoles;\n        protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => exemptStatuses;\n\n        protected override bool ExemptProviders => exemptProviders;\n\n        public override TestPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)\n            => new() { Policies = policyDetails };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/DisableSendPolicyRequirementFactoryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n[SutProviderCustomize]\npublic class DisableSendPolicyRequirementFactoryTests\n{\n    [Theory, BitAutoData]\n    public void DisableSend_IsFalse_IfNoPolicies(SutProvider<DisableSendPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create([]);\n\n        Assert.False(actual.DisableSend);\n    }\n\n    [Theory, BitAutoData]\n    public void DisableSend_IsTrue_IfAnyDisableSendPolicies(\n        [PolicyDetails(PolicyType.DisableSend)] PolicyDetails[] policies,\n        SutProvider<DisableSendPolicyRequirementFactory> sutProvider\n        )\n    {\n        var actual = sutProvider.Sut.Create(policies);\n\n        Assert.True(actual.DisableSend);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirementFactoryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n[SutProviderCustomize]\npublic class OrganizationDataOwnershipPolicyRequirementFactoryTests\n{\n    [Theory, BitAutoData]\n    public void State_WithNoPolicies_ReturnsAllowed(SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create([]);\n\n        Assert.Equal(OrganizationDataOwnershipState.Disabled, actual.State);\n    }\n\n    [Theory, BitAutoData]\n    public void State_WithOrganizationDataOwnershipPolicies_ReturnsRestricted(\n        [PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies,\n        SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create(policies);\n\n        Assert.Equal(OrganizationDataOwnershipState.Enabled, actual.State);\n    }\n\n    [Theory, BitAutoData]\n    public void PolicyType_ReturnsOrganizationDataOwnership(SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        Assert.Equal(PolicyType.OrganizationDataOwnership, sutProvider.Sut.PolicyType);\n    }\n\n    [Theory, BitAutoData]\n    public void EligibleForSelfRevoke_WithConfirmedUser_ReturnsTrue(\n        Guid organizationId,\n        [PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,\n        SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        // Arrange\n        policies[0].OrganizationId = organizationId;\n        var requirement = sutProvider.Sut.Create(policies);\n\n        // Act\n        var result = requirement.EligibleForSelfRevoke(organizationId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public void EligibleForSelfRevoke_WithInvitedUser_ReturnsFalse(\n        Guid organizationId,\n        [PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Invited)] PolicyDetails[] policies,\n        SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        // Arrange\n        policies[0].OrganizationId = organizationId;\n        var requirement = sutProvider.Sut.Create(policies);\n\n        // Act\n        var result = requirement.EligibleForSelfRevoke(organizationId);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public void EligibleForSelfRevoke_WithNoPolicies_ReturnsFalse(\n        Guid organizationId,\n        SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        // Arrange\n        var requirement = sutProvider.Sut.Create([]);\n\n        // Act\n        var result = requirement.EligibleForSelfRevoke(organizationId);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public void EligibleForSelfRevoke_WithDifferentOrganization_ReturnsFalse(\n        Guid organizationId,\n        Guid differentOrganizationId,\n        [PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,\n        SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        // Arrange\n        policies[0].OrganizationId = differentOrganizationId;\n        var requirement = sutProvider.Sut.Create(policies);\n\n        // Act\n        var result = requirement.EligibleForSelfRevoke(organizationId);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public void GetDefaultCollectionRequestOnPolicyEnable_WithConfirmedUser_ReturnsTrue(\n    [PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies,\n    SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        // Arrange\n        var requirement = sutProvider.Sut.Create(policies);\n        var expectedOrganizationUserId = policies[0].OrganizationUserId;\n        var organizationId = policies[0].OrganizationId;\n\n        // Act\n        var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId);\n\n        // Assert\n        Assert.Equal(expectedOrganizationUserId, result.OrganizationUserId);\n        Assert.True(result.ShouldCreateDefaultCollection);\n    }\n\n    [Theory, BitAutoData]\n    public void GetDefaultCollectionRequestOnPolicyEnable_WithAcceptedUser_ReturnsFalse(\n        [PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Accepted)] PolicyDetails[] policies,\n        SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        // Arrange\n        var requirement = sutProvider.Sut.Create(policies);\n        var organizationId = policies[0].OrganizationId;\n\n        // Act\n        var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId);\n\n        // Assert\n        Assert.Equal(Guid.Empty, result.OrganizationUserId);\n        Assert.False(result.ShouldCreateDefaultCollection);\n    }\n\n    [Theory, BitAutoData]\n    public void GetDefaultCollectionRequestOnPolicyEnable_WithNoPolicies_ReturnsFalse(\n        SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        // Arrange\n        var requirement = sutProvider.Sut.Create([]);\n        var organizationId = Guid.NewGuid();\n\n        // Act\n        var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId);\n\n        // Assert\n        Assert.Equal(Guid.Empty, result.OrganizationUserId);\n        Assert.False(result.ShouldCreateDefaultCollection);\n    }\n\n    [Theory, BitAutoData]\n    public void GetDefaultCollectionRequestOnPolicyEnable_WithMixedStatuses(\n        [PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies,\n        SutProvider<OrganizationDataOwnershipPolicyRequirementFactory> sutProvider)\n    {\n        // Arrange\n        var requirement = sutProvider.Sut.Create(policies);\n\n        var confirmedPolicy = policies[0];\n        var acceptedPolicy = policies[1];\n\n        confirmedPolicy.OrganizationUserStatus = OrganizationUserStatusType.Confirmed;\n        acceptedPolicy.OrganizationUserStatus = OrganizationUserStatusType.Accepted;\n\n        // Act\n        var confirmedResult = requirement.GetDefaultCollectionRequestOnPolicyEnable(confirmedPolicy.OrganizationId);\n        var acceptedResult = requirement.GetDefaultCollectionRequestOnPolicyEnable(acceptedPolicy.OrganizationId);\n\n        // Assert\n        Assert.Equal(Guid.Empty, acceptedResult.OrganizationUserId);\n        Assert.False(acceptedResult.ShouldCreateDefaultCollection);\n\n        Assert.Equal(confirmedPolicy.OrganizationUserId, confirmedResult.OrganizationUserId);\n        Assert.True(confirmedResult.ShouldCreateDefaultCollection);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/PolicyDetailsTestExtensions.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Utilities;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\npublic static class PolicyDetailsTestExtensions\n{\n    public static void SetDataModel<T>(this PolicyDetails policyDetails, T data) where T : IPolicyDataModel\n        => policyDetails.PolicyData = CoreHelpers.ClassToJsonData(data);\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireSsoPolicyRequirementFactoryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n[SutProviderCustomize]\npublic class RequireSsoPolicyRequirementFactoryTests\n{\n    [Theory, BitAutoData]\n    public void CanUsePasskeyLogin_WithNoPolicies_ReturnsTrue(\n        SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create([]);\n\n        Assert.True(actual.CanUsePasskeyLogin);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Accepted)]\n    [BitAutoData(OrganizationUserStatusType.Confirmed)]\n    public void CanUsePasskeyLogin_WithoutExemptStatus_ReturnsFalse(\n        OrganizationUserStatusType userStatus,\n        SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create(\n        [\n            new PolicyDetails\n            {\n                PolicyType = PolicyType.RequireSso,\n                OrganizationUserStatus = userStatus\n            }\n        ]);\n\n        Assert.False(actual.CanUsePasskeyLogin);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Revoked)]\n    [BitAutoData(OrganizationUserStatusType.Invited)]\n    public void CanUsePasskeyLogin_WithExemptStatus_ReturnsTrue(\n        OrganizationUserStatusType userStatus,\n        SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create(\n        [\n            new PolicyDetails\n            {\n                PolicyType = PolicyType.RequireSso,\n                OrganizationUserStatus = userStatus\n            }\n        ]);\n\n        Assert.True(actual.CanUsePasskeyLogin);\n    }\n\n    [Theory, BitAutoData]\n    public void SsoRequired_WithNoPolicies_ReturnsFalse(\n        SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create([]);\n\n        Assert.False(actual.SsoRequired);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Revoked)]\n    [BitAutoData(OrganizationUserStatusType.Invited)]\n    [BitAutoData(OrganizationUserStatusType.Accepted)]\n    public void SsoRequired_WithoutExemptStatus_ReturnsFalse(\n        OrganizationUserStatusType userStatus,\n        SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create(\n        [\n            new PolicyDetails\n            {\n                PolicyType = PolicyType.RequireSso,\n                OrganizationUserStatus = userStatus\n            }\n        ]);\n\n        Assert.False(actual.SsoRequired);\n    }\n\n    [Theory, BitAutoData]\n    public void SsoRequired_WithExemptStatus_ReturnsTrue(\n        SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create(\n        [\n            new PolicyDetails\n            {\n                PolicyType = PolicyType.RequireSso,\n                OrganizationUserStatus = OrganizationUserStatusType.Confirmed\n            }\n        ]);\n\n        Assert.True(actual.SsoRequired);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n[SutProviderCustomize]\npublic class RequireTwoFactorPolicyRequirementFactoryTests\n{\n    [Theory]\n    [BitAutoData]\n    public void IsTwoFactorRequiredForOrganization_WithNoPolicies_ReturnsFalse(\n        Guid organizationId,\n        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create([]);\n\n        Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void IsTwoFactorRequiredForOrganization_WithOrganizationPolicy_ReturnsTrue(\n        Guid organizationId,\n        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create(\n        [\n            new PolicyDetails\n            {\n                OrganizationId = organizationId,\n                PolicyType = PolicyType.TwoFactorAuthentication,\n            }\n        ]);\n\n        Assert.True(actual.IsTwoFactorRequiredForOrganization(organizationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void IsTwoFactorRequiredForOrganization_WithOtherOrganizationPolicy_ReturnsFalse(\n        Guid organizationId,\n        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create(\n        [\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                PolicyType = PolicyType.TwoFactorAuthentication,\n            },\n        ]);\n\n        Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId));\n    }\n\n    [Theory, BitAutoData]\n    public void OrganizationsRequiringTwoFactor_WithNoPolicies_ReturnsEmptyCollection(\n        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create([]);\n\n        Assert.Empty(actual.OrganizationsRequiringTwoFactor);\n    }\n\n    [Theory, BitAutoData]\n    public void OrganizationsRequiringTwoFactor_WithMultiplePolicies_ReturnsActiveMemberships(\n        Guid orgId1, Guid orgUserId1, Guid orgId2, Guid orgUserId2,\n        Guid orgId3, Guid orgUserId3, Guid orgId4, Guid orgUserId4,\n        SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)\n    {\n        var policies = new[]\n        {\n            new PolicyDetails\n            {\n                OrganizationId = orgId1,\n                OrganizationUserId = orgUserId1,\n                PolicyType = PolicyType.TwoFactorAuthentication,\n                OrganizationUserStatus = OrganizationUserStatusType.Accepted\n            },\n            new PolicyDetails\n            {\n                OrganizationId = orgId2,\n                OrganizationUserId = orgUserId2,\n                PolicyType = PolicyType.TwoFactorAuthentication,\n                OrganizationUserStatus = OrganizationUserStatusType.Confirmed\n            },\n            new PolicyDetails\n            {\n                OrganizationId = orgId3,\n                OrganizationUserId = orgUserId3,\n                PolicyType = PolicyType.TwoFactorAuthentication,\n                OrganizationUserStatus = OrganizationUserStatusType.Invited\n            },\n            new PolicyDetails\n            {\n                OrganizationId = orgId4,\n                OrganizationUserId = orgUserId4,\n                PolicyType = PolicyType.TwoFactorAuthentication,\n                OrganizationUserStatus = OrganizationUserStatusType.Revoked\n            }\n        };\n\n        var actual = sutProvider.Sut.Create(policies);\n\n        var result = actual.OrganizationsRequiringTwoFactor.ToList();\n        Assert.Equal(2, result.Count);\n        Assert.Contains(result, p => p.OrganizationId == orgId1 && p.OrganizationUserId == orgUserId1);\n        Assert.Contains(result, p => p.OrganizationId == orgId2 && p.OrganizationUserId == orgUserId2);\n        Assert.DoesNotContain(result, p => p.OrganizationId == orgId3 && p.OrganizationUserId == orgUserId3);\n        Assert.DoesNotContain(result, p => p.OrganizationId == orgId4 && p.OrganizationUserId == orgUserId4);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirementFactoryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n[SutProviderCustomize]\npublic class ResetPasswordPolicyRequirementFactoryTests\n{\n    [Theory, BitAutoData]\n    public void AutoEnroll_WithNoPolicies_IsEmpty(SutProvider<ResetPasswordPolicyRequirementFactory> sutProvider, Guid orgId)\n    {\n        var actual = sutProvider.Sut.Create([]);\n\n        Assert.False(actual.AutoEnrollEnabled(orgId));\n    }\n\n    [Theory, BitAutoData]\n    public void AutoEnrollAdministration_WithAnyResetPasswordPolices_ReturnsEnabledOrganizationIds(\n        [PolicyDetails(PolicyType.ResetPassword)] PolicyDetails[] policies,\n        SutProvider<ResetPasswordPolicyRequirementFactory> sutProvider)\n    {\n        policies[0].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });\n        policies[1].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = false });\n        policies[2].SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });\n\n        var actual = sutProvider.Sut.Create(policies);\n\n        Assert.True(actual.AutoEnrollEnabled(policies[0].OrganizationId));\n        Assert.False(actual.AutoEnrollEnabled(policies[1].OrganizationId));\n        Assert.True(actual.AutoEnrollEnabled(policies[2].OrganizationId));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SendOptionsPolicyRequirementFactoryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\n[SutProviderCustomize]\npublic class SendOptionsPolicyRequirementFactoryTests\n{\n    [Theory, BitAutoData]\n    public void DisableHideEmail_IsFalse_IfNoPolicies(SutProvider<SendOptionsPolicyRequirementFactory> sutProvider)\n    {\n        var actual = sutProvider.Sut.Create([]);\n\n        Assert.False(actual.DisableHideEmail);\n    }\n\n    [Theory, BitAutoData]\n    public void DisableHideEmail_IsFalse_IfNotConfigured(\n        [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies,\n        SutProvider<SendOptionsPolicyRequirementFactory> sutProvider\n        )\n    {\n        policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false });\n        policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false });\n\n        var actual = sutProvider.Sut.Create(policies);\n\n        Assert.False(actual.DisableHideEmail);\n    }\n\n    [Theory, BitAutoData]\n    public void DisableHideEmail_IsTrue_IfAnyConfigured(\n        [PolicyDetails(PolicyType.SendOptions)] PolicyDetails[] policies,\n        SutProvider<SendOptionsPolicyRequirementFactory> sutProvider\n        )\n    {\n        policies[0].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = true });\n        policies[1].SetDataModel(new SendOptionsPolicyData { DisableHideEmail = false });\n\n        var actual = sutProvider.Sut.Create(policies);\n\n        Assert.True(actual.DisableHideEmail);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/SingleOrganizationPolicyRequirementTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements.Errors;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\n\npublic class SingleOrganizationPolicyRequirementTests\n{\n    [Fact]\n    public void CanCreateOrganization_WithNoPolicies_ReturnsNoError()\n    {\n        var sut = new SingleOrganizationPolicyRequirement([]);\n\n        var result = sut.CanCreateOrganization();\n\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Accepted)]\n    [BitAutoData(OrganizationUserStatusType.Confirmed)]\n    public void CanCreateOrganization_WithAcceptedOrConfirmedUser_ReturnsError(\n        OrganizationUserStatusType status, Guid orgId)\n    {\n        var sut = new SingleOrganizationPolicyRequirement(\n        [\n            new PolicyDetails\n            {\n                OrganizationId = orgId,\n                OrganizationUserStatus = status,\n                PolicyType = PolicyType.SingleOrg\n            }\n        ]);\n\n        var result = sut.CanCreateOrganization();\n\n        Assert.NotNull(result);\n        Assert.IsType<UserCannotCreateOrg>(result);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Invited)]\n    [BitAutoData(OrganizationUserStatusType.Revoked)]\n    public void CanCreateOrganization_WithInvitedOrRevokedUser_ReturnsNoError(\n        OrganizationUserStatusType status, Guid orgId)\n    {\n        var sut = new SingleOrganizationPolicyRequirement(\n        [\n            new PolicyDetails\n            {\n                OrganizationId = orgId,\n                OrganizationUserStatus = status,\n                PolicyType = PolicyType.SingleOrg\n            }\n        ]);\n\n        var result = sut.CanCreateOrganization();\n\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void CanJoinOrganization_NoPolicyDetails_NoOtherOrgs_ReturnsNoError(Guid targetOrgId, Guid userId)\n    {\n        var sut = new SingleOrganizationPolicyRequirement([]);\n\n        var allOrgUsers = new List<OrganizationUser>\n        {\n            new() { UserId = userId, OrganizationId = targetOrgId }\n        };\n\n        var result = sut.CanJoinOrganization(targetOrgId, allOrgUsers);\n\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void CanJoinOrganization_TargetHasPolicy_UserInOtherOrg_ReturnsTargetOrgError(\n        Guid targetOrgId, Guid otherOrgId, Guid userId)\n    {\n        var sut = new SingleOrganizationPolicyRequirement(\n        [\n            new PolicyDetails\n            {\n                OrganizationId = targetOrgId,\n                OrganizationUserStatus = OrganizationUserStatusType.Accepted,\n                PolicyType = PolicyType.SingleOrg\n            }\n        ]);\n\n        var allOrgUsers = new List<OrganizationUser>\n        {\n            new() { UserId = userId, OrganizationId = targetOrgId },\n            new() { UserId = userId, OrganizationId = otherOrgId }\n        };\n\n        var result = sut.CanJoinOrganization(targetOrgId, allOrgUsers);\n\n        Assert.NotNull(result);\n        Assert.IsType<UserIsAMemberOfAnotherOrganization>(result);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Accepted)]\n    [BitAutoData(OrganizationUserStatusType.Confirmed)]\n    public void CanJoinOrganization_OtherOrgHasPolicy_ReturnsOtherOrgError(OrganizationUserStatusType status,\n        Guid targetOrgId, Guid otherOrgId, Guid userId)\n    {\n        var sut = new SingleOrganizationPolicyRequirement(\n        [\n            new PolicyDetails\n            {\n                OrganizationId = otherOrgId,\n                OrganizationUserStatus = status,\n                PolicyType = PolicyType.SingleOrg\n            }\n        ]);\n\n        var allOrgUsers = new List<OrganizationUser>\n        {\n            new() { UserId = userId, OrganizationId = targetOrgId }\n        };\n\n        var result = sut.CanJoinOrganization(targetOrgId, allOrgUsers);\n\n        Assert.NotNull(result);\n        Assert.IsType<UserIsAMemberOfAnOrganizationThatHasSingleOrgPolicy>(result);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Invited)]\n    [BitAutoData(OrganizationUserStatusType.Revoked)]\n    public void CanJoinOrganization_OtherOrgHasPolicy_WithInvitedOrRevokedUser_ReturnsNull(\n        OrganizationUserStatusType status, Guid targetOrgId, Guid otherOrgId, Guid userId)\n    {\n        var sut = new SingleOrganizationPolicyRequirement(\n        [\n            new PolicyDetails\n            {\n                OrganizationId = otherOrgId,\n                OrganizationUserStatus = status,\n                PolicyType = PolicyType.SingleOrg\n            }\n        ]);\n\n        var allOrgUsers = new List<OrganizationUser>\n        {\n            new() { UserId = userId, OrganizationId = targetOrgId }\n        };\n\n        var result = sut.CanJoinOrganization(targetOrgId, allOrgUsers);\n\n        Assert.Null(result);\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public void CanJoinOrganization_TargetHasPolicy_UserOnlyInTargetOrg_ReturnsNull(\n        Guid targetOrgId, Guid userId)\n    {\n        var sut = new SingleOrganizationPolicyRequirement(\n        [\n            new PolicyDetails\n            {\n                OrganizationId = targetOrgId,\n                OrganizationUserStatus = OrganizationUserStatusType.Accepted,\n                PolicyType = PolicyType.SingleOrg\n            }\n        ]);\n\n        var allOrgUsers = new List<OrganizationUser>\n        {\n            new() { UserId = userId, OrganizationId = targetOrgId }\n        };\n\n        var result = sut.CanJoinOrganization(targetOrgId, allOrgUsers);\n\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void CanJoinOrganization_EmptyOrgUsers_NoPolicies_ReturnsNull(Guid targetOrgId)\n    {\n        var sut = new SingleOrganizationPolicyRequirement([]);\n\n        var result = sut.CanJoinOrganization(targetOrgId, new List<OrganizationUser>());\n\n        Assert.Null(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementsHelper.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\n\npublic static class SingleOrganizationPolicyRequirementTestFactory\n{\n    public static SingleOrganizationPolicyRequirement NoSinglePolicyOrganizationsForUser() => new([]);\n\n    public static SingleOrganizationPolicyRequirement EnabledForTargetOrganization(Guid organizationId) =>\n        new([\n        new PolicyDetails\n        {\n            OrganizationId = organizationId,\n            OrganizationUserId = Guid.NewGuid(),\n            PolicyType = PolicyType.SingleOrg,\n            OrganizationUserStatus = OrganizationUserStatusType.Confirmed,\n            OrganizationUserType = OrganizationUserType.User\n        }\n    ]);\n\n    public static SingleOrganizationPolicyRequirement EnabledForAnotherOrganization() =>\n        new([\n            new PolicyDetails\n            {\n                OrganizationId = Guid.NewGuid(),\n                OrganizationUserId = Guid.NewGuid(),\n                PolicyType = PolicyType.SingleOrg,\n                OrganizationUserStatus = OrganizationUserStatusType.Confirmed,\n                OrganizationUserType = OrganizationUserType.User\n            }\n    ]);\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEventFixtures.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing NSubstitute;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\n\npublic class FakeSingleOrgDependencyEvent : IEnforceDependentPoliciesEvent\n{\n    public PolicyType Type => PolicyType.SingleOrg;\n    public IEnumerable<PolicyType> RequiredPolicies => [];\n}\n\npublic class FakeRequireSsoDependencyEvent : IEnforceDependentPoliciesEvent\n{\n    public PolicyType Type => PolicyType.RequireSso;\n    public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];\n}\n\npublic class FakeVaultTimeoutDependencyEvent : IEnforceDependentPoliciesEvent\n{\n    public PolicyType Type => PolicyType.MaximumVaultTimeout;\n    public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];\n}\n\npublic class FakeSingleOrgValidationEvent : IPolicyValidationEvent\n{\n    public PolicyType Type => PolicyType.SingleOrg;\n\n    public readonly Func<SavePolicyModel, Policy?, Task<string>> ValidateAsyncMock = Substitute.For<Func<SavePolicyModel, Policy?, Task<string>>>();\n\n    public Task<string> ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy)\n    {\n        return ValidateAsyncMock(policyRequest, currentPolicy);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidatorFixtures.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing NSubstitute;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\n\npublic class FakeSingleOrgPolicyValidator : IPolicyValidator\n{\n    public PolicyType Type => PolicyType.SingleOrg;\n    public IEnumerable<PolicyType> RequiredPolicies => Array.Empty<PolicyType>();\n\n    public readonly Func<PolicyUpdate, Policy?, Task<string>> ValidateAsyncMock = Substitute.For<Func<PolicyUpdate, Policy?, Task<string>>>();\n    public readonly Action<PolicyUpdate, Policy?> OnSaveSideEffectsAsyncMock = Substitute.For<Action<PolicyUpdate, Policy?>>();\n\n    public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        return ValidateAsyncMock(policyUpdate, currentPolicy);\n    }\n\n    public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)\n    {\n        OnSaveSideEffectsAsyncMock(policyUpdate, currentPolicy);\n        return Task.FromResult(0);\n    }\n}\npublic class FakeRequireSsoPolicyValidator : IPolicyValidator\n{\n    public PolicyType Type => PolicyType.RequireSso;\n    public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];\n    public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(\"\");\n    public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);\n}\npublic class FakeVaultTimeoutPolicyValidator : IPolicyValidator\n{\n    public PolicyType Type => PolicyType.MaximumVaultTimeout;\n    public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];\n    public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(\"\");\n    public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidatorHelpersTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\n\npublic class PolicyValidatorHelpersTests\n{\n    [Fact]\n    public void ValidateDecryptionOptionsNotEnabled_RequiredByKeyConnector_ValidationError()\n    {\n        var ssoConfig = new SsoConfig();\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });\n\n        var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);\n\n        Assert.Contains(\"Key Connector is enabled\", result);\n    }\n\n    [Fact]\n    public void ValidateDecryptionOptionsNotEnabled_RequiredByTDE_ValidationError()\n    {\n        var ssoConfig = new SsoConfig();\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });\n\n        var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);\n\n        Assert.Contains(\"Trusted device encryption is on\", result);\n    }\n\n    [Fact]\n    public void ValidateDecryptionOptionsNotEnabled_NullSsoConfig_NoValidationError()\n    {\n        var ssoConfig = new SsoConfig();\n        var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);\n\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Fact]\n    public void ValidateDecryptionOptionsNotEnabled_RequiredOptionNotEnabled_NoValidationError()\n    {\n        var ssoConfig = new SsoConfig();\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });\n\n        var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);\n\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Fact]\n    public void ValidateDecryptionOptionsNotEnabled_SsoConfigDisabled_NoValidationError()\n    {\n        var ssoConfig = new SsoConfig();\n        ssoConfig.Enabled = false;\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });\n\n        var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);\n\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/AutomaticUserConfirmationPolicyEventHandlerTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\n[SutProviderCustomize]\npublic class AutomaticUserConfirmationPolicyEventHandlerTests\n{\n    [Theory, BitAutoData]\n    public void RequiredPolicies_IncludesSingleOrg(\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Act\n        var requiredPolicies = sutProvider.Sut.RequiredPolicies;\n\n        // Assert\n        Assert.Contains(PolicyType.SingleOrg, requiredPolicies);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_EnablingPolicy_UsersNotCompliantWithSingleOrg_ReturnsError(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())\n            .Returns(Invalid(request, new UserNotCompliantWithSingleOrganization()));\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);\n\n        // Assert\n        Assert.Contains(\"compliant with the Single organization policy\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_EnablingPolicy_ProviderUsersExist_ReturnsError(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())\n            .Returns(Invalid(request, new ProviderExistsInOrganization()));\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);\n\n        // Assert\n        Assert.Contains(\"Provider user type\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_EnablingPolicy_AllValidationsPassed_ReturnsEmptyString(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())\n            .Returns(Valid(request));\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);\n\n        // Assert\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_PolicyAlreadyEnabled_ReturnsEmptyString(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, currentPolicy);\n\n        // Assert\n        Assert.True(string.IsNullOrEmpty(result));\n\n        await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .DidNotReceive()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_DisablingPolicy_ReturnsEmptyString(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, currentPolicy);\n\n        // Assert\n        Assert.True(string.IsNullOrEmpty(result));\n        await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .DidNotReceive()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_EnablingPolicy_PassesCorrectOrganizationId(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())\n            .Returns(Valid(request));\n\n        // Act\n        await sutProvider.Sut.ValidateAsync(policyUpdate, null);\n\n        // Assert\n        await sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .Received(1)\n            .IsOrganizationCompliantAsync(Arg.Is<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>(\n                r => r.OrganizationId == policyUpdate.OrganizationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_CallsValidateWithPolicyUpdate(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n        var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(policyUpdate.OrganizationId);\n\n        sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()\n            .IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())\n            .Returns(Valid(request));\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);\n\n        // Assert\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePreUpsertSideEffectAsync_EnablingPolicy_DeletesEmergencyAccessForAllOrgUsers(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        var userId1 = Guid.NewGuid();\n        var userId2 = Guid.NewGuid();\n        var orgUsers = new List<OrganizationUser>\n        {\n            new() { UserId = userId1, OrganizationId = policyUpdate.OrganizationId },\n            new() { UserId = userId2, OrganizationId = policyUpdate.OrganizationId },\n            new() { UserId = null, OrganizationId = policyUpdate.OrganizationId } // invited user, no UserId\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(policyUpdate.OrganizationId, null)\n            .Returns(orgUsers);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        // Act\n        await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, null);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .Received(1)\n            .DeleteAllByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 2 && ids.Contains(userId1) && ids.Contains(userId2)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePreUpsertSideEffectAsync_DisablingPolicy_DoesNotDeleteEmergencyAccess(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        // Act\n        await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, currentPolicy);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdsAsync(Arg.Any<ICollection<Guid>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePreUpsertSideEffectAsync_PolicyAlreadyEnabled_DoesNotDeleteEmergencyAccess(\n        [PolicyUpdate(PolicyType.AutomaticUserConfirmation)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.AutomaticUserConfirmation)] Policy currentPolicy,\n        SutProvider<AutomaticUserConfirmationPolicyEventHandler> sutProvider)\n    {\n        // Arrange\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        // Act\n        await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, currentPolicy);\n\n        // Assert\n        await sutProvider.GetDependency<IDeleteEmergencyAccessCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAllByUserIdsAsync(Arg.Any<ICollection<Guid>>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/BlockClaimedDomainAccountCreationPolicyValidatorTests.cs",
    "content": "﻿namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\n[SutProviderCustomize]\npublic class BlockClaimedDomainAccountCreationPolicyValidatorTests\n{\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_EnablingPolicy_NoVerifiedDomains_ValidationError(\n        [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,\n        SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()\n            .HasVerifiedDomainsAsync(policyUpdate.OrganizationId)\n            .Returns(false);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);\n\n        // Assert\n        Assert.Equal(\"You must claim at least one domain to turn on this policy\", result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_EnablingPolicy_HasVerifiedDomains_Success(\n        [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,\n        SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()\n            .HasVerifiedDomainsAsync(policyUpdate.OrganizationId)\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);\n\n        // Assert\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_DisablingPolicy_NoValidation(\n        [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, false)] PolicyUpdate policyUpdate,\n        SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)\n    {\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);\n\n        // Assert\n        Assert.True(string.IsNullOrEmpty(result));\n        await sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()\n            .DidNotReceive()\n            .HasVerifiedDomainsAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_EnablingPolicy_NoVerifiedDomains_ValidationError(\n        [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,\n        SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()\n            .HasVerifiedDomainsAsync(policyUpdate.OrganizationId)\n            .Returns(false);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);\n\n        // Assert\n        Assert.Equal(\"You must claim at least one domain to turn on this policy\", result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_EnablingPolicy_HasVerifiedDomains_Success(\n        [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,\n        SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()\n            .HasVerifiedDomainsAsync(policyUpdate.OrganizationId)\n            .Returns(true);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);\n\n        // Assert\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_NoValidation(\n        [PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, false)] PolicyUpdate policyUpdate,\n        SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);\n\n        // Assert\n        Assert.True(string.IsNullOrEmpty(result));\n        await sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()\n            .DidNotReceive()\n            .HasVerifiedDomainsAsync(Arg.Any<Guid>());\n    }\n\n    [Fact]\n    public void Type_ReturnsBlockClaimedDomainAccountCreation()\n    {\n        // Arrange\n        var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null);\n\n        // Act & Assert\n        Assert.Equal(PolicyType.BlockClaimedDomainAccountCreation, validator.Type);\n    }\n\n    [Fact]\n    public void RequiredPolicies_ReturnsEmpty()\n    {\n        // Arrange\n        var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null);\n\n        // Act\n        var requiredPolicies = validator.RequiredPolicies.ToList();\n\n        // Assert\n        Assert.Empty(requiredPolicies);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\n[SutProviderCustomize]\npublic class FreeFamiliesForEnterprisePolicyValidatorTests\n{\n    [Theory, BitAutoData]\n    public async Task OnSaveSideEffectsAsync_DoesNotNotifyUserWhenPolicyDisabled(\n        Organization organization,\n        List<OrganizationSponsorship> organizationSponsorships,\n        [PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.FreeFamiliesSponsorshipPolicy, true)] Policy policy,\n        SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)\n    {\n\n        policy.Enabled = true;\n        policyUpdate.Enabled = false;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns(organizationSponsorships);\n\n        await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);\n\n        await sutProvider.GetDependency<IMailService>().DidNotReceive()\n            .SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(organizationSponsorships[0].FriendlyName, organizationSponsorships[0].ValidUntil.ToString(),\n                organizationSponsorships[0].SponsoredOrganizationId.ToString(), organization.DisplayName());\n    }\n\n    [Theory, BitAutoData]\n    public async Task OnSaveSideEffectsAsync_DoesNotifyUserWhenPolicyDisabled(\n        Organization organization,\n        List<OrganizationSponsorship> organizationSponsorships,\n        [PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.FreeFamiliesSponsorshipPolicy, true)] Policy policy,\n        SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)\n    {\n\n        policy.Enabled = false;\n        policyUpdate.Enabled = true;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns(organizationSponsorships);\n        // Act\n        await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);\n\n        // Assert\n        var offerAcceptanceDate = organizationSponsorships[0].ValidUntil!.Value.AddDays(-7).ToString(\"MM/dd/yyyy\");\n        await sutProvider.GetDependency<IMailService>().Received(1)\n            .SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(organizationSponsorships[0].FriendlyName, offerAcceptanceDate,\n                organizationSponsorships[0].SponsoredOrganizationId.ToString(), organization.Name);\n\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePreUpsertSideEffectAsync_DoesNotNotifyUserWhenPolicyDisabled(\n        Organization organization,\n        List<OrganizationSponsorship> organizationSponsorships,\n        [PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.FreeFamiliesSponsorshipPolicy, true)] Policy policy,\n        SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)\n    {\n        policy.Enabled = true;\n        policyUpdate.Enabled = false;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns(organizationSponsorships);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);\n\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(default, default, default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePreUpsertSideEffectAsync_DoesNotifyUserWhenPolicyEnabled(\n        Organization organization,\n        List<OrganizationSponsorship> organizationSponsorships,\n        [PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] Policy policy,\n        SutProvider<FreeFamiliesForEnterprisePolicyValidator> sutProvider)\n    {\n        policy.Enabled = false;\n        policyUpdate.Enabled = true;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns(organizationSponsorships);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);\n\n        var offerAcceptanceDate = organizationSponsorships[0].ValidUntil!.Value.AddDays(-7).ToString(\"MM/dd/yyyy\");\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(\n                organizationSponsorships[0].FriendlyName,\n                offerAcceptanceDate,\n                organizationSponsorships[0].SponsoredOrganizationId.ToString(),\n                organization.Name);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\n[SutProviderCustomize]\npublic class OrganizationDataOwnershipPolicyValidatorTests\n{\n    private const string _defaultUserCollectionName = \"Default\";\n\n    [Theory, BitAutoData]\n    public async Task ExecuteSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing(\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,\n        [Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState,\n        Organization organization,\n        SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)\n    {\n        // Arrange\n        postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;\n        previousPolicyState.OrganizationId = policyUpdate.OrganizationId;\n        organization.Id = policyUpdate.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act\n        await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsBulkAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecuteSideEffectsAsync_PolicyBeingDisabled_DoesNothing(\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,\n        [Policy(PolicyType.OrganizationDataOwnership)] Policy previousPolicyState,\n        Organization organization,\n        SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)\n    {\n        // Arrange\n        previousPolicyState.OrganizationId = policyUpdate.OrganizationId;\n        postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;\n        organization.Id = policyUpdate.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act\n        await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsBulkAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecuteSideEffectsAsync_WhenNoUsersExist_DoNothing(\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,\n        [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,\n        OrganizationDataOwnershipPolicyRequirementFactory factory)\n    {\n        // Arrange\n        postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;\n        previousPolicyState.OrganizationId = policyUpdate.OrganizationId;\n\n        var policyRepository = ArrangePolicyRepository([]);\n        var collectionRepository = Substitute.For<ICollectionRepository>();\n\n        var sut = ArrangeSut(factory, policyRepository, collectionRepository);\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act\n        await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert\n        await collectionRepository\n            .DidNotReceive()\n            .CreateDefaultCollectionsBulkAsync(\n                Arg.Any<Guid>(),\n                Arg.Any<IEnumerable<Guid>>(),\n                Arg.Any<string>());\n\n        await policyRepository\n            .Received(1)\n            .GetPolicyDetailsByOrganizationIdAsync(\n                policyUpdate.OrganizationId,\n                PolicyType.OrganizationDataOwnership);\n    }\n\n    public static IEnumerable<object?[]> ShouldUpsertDefaultCollectionsTestCases()\n    {\n        yield return WithExistingPolicy();\n\n        yield return WithNoExistingPolicy();\n        yield break;\n\n        object?[] WithExistingPolicy()\n        {\n            var organizationId = Guid.NewGuid();\n            var postUpdatedPolicy = new Policy\n            {\n                OrganizationId = organizationId,\n                Type = PolicyType.OrganizationDataOwnership,\n                Enabled = true\n            };\n            var previousPolicyState = new Policy\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = organizationId,\n                Type = PolicyType.OrganizationDataOwnership,\n                Enabled = false\n            };\n\n            return new object?[]\n            {\n                postUpdatedPolicy,\n                previousPolicyState\n            };\n        }\n\n        object?[] WithNoExistingPolicy()\n        {\n            var postUpdatedPolicy = new Policy\n            {\n                OrganizationId = new Guid(),\n                Type = PolicyType.OrganizationDataOwnership,\n                Enabled = true\n            };\n\n            const Policy previousPolicyState = null;\n\n            return new object?[]\n            {\n                postUpdatedPolicy,\n                previousPolicyState\n            };\n        }\n    }\n    [Theory]\n    [BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]\n    public async Task ExecuteSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections(\n        Policy postUpdatedPolicy,\n        Policy? previousPolicyState,\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,\n        [OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails,\n        OrganizationDataOwnershipPolicyRequirementFactory factory)\n    {\n        // Arrange\n        var orgPolicyDetailsList = orgPolicyDetails.ToList();\n        foreach (var policyDetail in orgPolicyDetailsList)\n        {\n            policyDetail.OrganizationId = policyUpdate.OrganizationId;\n        }\n\n        var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);\n        var collectionRepository = Substitute.For<ICollectionRepository>();\n\n        var sut = ArrangeSut(factory, policyRepository, collectionRepository);\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act\n        await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert - Should call with all user IDs (repository does internal filtering)\n        await collectionRepository\n            .Received(1)\n            .CreateDefaultCollectionsBulkAsync(\n                policyUpdate.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),\n                _defaultUserCollectionName);\n    }\n\n    private static IEnumerable<object?[]> WhenDefaultCollectionsDoesNotExistTestCases()\n    {\n        yield return [new OrganizationModelOwnershipPolicyModel(null)];\n        yield return [new OrganizationModelOwnershipPolicyModel(\"\")];\n        yield return [new OrganizationModelOwnershipPolicyModel(\"   \")];\n        yield return [new EmptyMetadataModel()];\n    }\n    [Theory]\n    [BitMemberAutoData(nameof(WhenDefaultCollectionsDoesNotExistTestCases))]\n    public async Task ExecuteSideEffectsAsync_WhenDefaultCollectionNameIsInvalid_DoesNothing(\n        IPolicyMetadataModel metadata,\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,\n        [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,\n        Organization organization,\n        SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)\n    {\n        // Arrange\n        postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;\n        previousPolicyState.OrganizationId = policyUpdate.OrganizationId;\n        policyUpdate.Enabled = true;\n        organization.Id = policyUpdate.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        var policyRequest = new SavePolicyModel(policyUpdate, metadata);\n\n        // Act\n        await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceive()\n            .CreateDefaultCollectionsBulkAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());\n    }\n\n    private static IPolicyRepository ArrangePolicyRepository(IEnumerable<OrganizationPolicyDetails> policyDetails)\n    {\n        var policyRepository = Substitute.For<IPolicyRepository>();\n\n        policyRepository\n            .GetPolicyDetailsByOrganizationIdAsync(Arg.Any<Guid>(), PolicyType.OrganizationDataOwnership)\n            .Returns(policyDetails);\n        return policyRepository;\n    }\n\n    private static OrganizationDataOwnershipPolicyValidator ArrangeSut(\n        OrganizationDataOwnershipPolicyRequirementFactory factory,\n        IPolicyRepository policyRepository,\n        ICollectionRepository collectionRepository,\n        bool useMyItems = true)\n    {\n        var organizationRepository = Substitute.For<IOrganizationRepository>();\n        // Default to UseMyItems = true for existing tests\n        organizationRepository.GetByIdAsync(Arg.Any<Guid>())\n            .Returns(callInfo => new Organization\n            {\n                Id = callInfo.Arg<Guid>(),\n                UseMyItems = useMyItems\n            });\n        var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, organizationRepository, [factory]);\n        return sut;\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePostUpsertSideEffectAsync_PolicyAlreadyEnabled_DoesNothing(\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,\n        [Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState,\n        Organization organization,\n        SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)\n    {\n        // Arrange\n        postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;\n        previousPolicyState.OrganizationId = policyUpdate.OrganizationId;\n        organization.Id = policyUpdate.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act\n        await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateDefaultCollectionsBulkAsync(default, default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePostUpsertSideEffectAsync_PolicyBeingDisabled_DoesNothing(\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy,\n        [Policy(PolicyType.OrganizationDataOwnership)] Policy previousPolicyState,\n        Organization organization,\n        SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)\n    {\n        // Arrange\n        previousPolicyState.OrganizationId = policyUpdate.OrganizationId;\n        postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;\n        organization.Id = policyUpdate.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act\n        await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateDefaultCollectionsBulkAsync(default, default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePostUpsertSideEffectAsync_WhenNoUsersExist_DoNothing(\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,\n        [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,\n        OrganizationDataOwnershipPolicyRequirementFactory factory)\n    {\n        // Arrange\n        postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;\n        previousPolicyState.OrganizationId = policyUpdate.OrganizationId;\n\n        var policyRepository = ArrangePolicyRepository([]);\n        var collectionRepository = Substitute.For<ICollectionRepository>();\n\n        var sut = ArrangeSut(factory, policyRepository, collectionRepository);\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act\n        await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert\n        await collectionRepository\n            .DidNotReceiveWithAnyArgs()\n            .CreateDefaultCollectionsBulkAsync(\n                default,\n                default,\n                default);\n\n        await policyRepository\n            .Received(1)\n            .GetPolicyDetailsByOrganizationIdAsync(\n                policyUpdate.OrganizationId,\n                PolicyType.OrganizationDataOwnership);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]\n    public async Task ExecutePostUpsertSideEffectAsync_WithRequirements_ShouldUpsertDefaultCollections(\n        Policy postUpdatedPolicy,\n        Policy? previousPolicyState,\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,\n        [OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails,\n        OrganizationDataOwnershipPolicyRequirementFactory factory)\n    {\n        // Arrange\n        var orgPolicyDetailsList = orgPolicyDetails.ToList();\n        foreach (var policyDetail in orgPolicyDetailsList)\n        {\n            policyDetail.OrganizationId = policyUpdate.OrganizationId;\n        }\n\n        var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);\n        var collectionRepository = Substitute.For<ICollectionRepository>();\n\n        var sut = ArrangeSut(factory, policyRepository, collectionRepository);\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act\n        await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert - Should call with all user IDs (repository does internal filtering)\n        await collectionRepository\n            .Received(1)\n            .CreateDefaultCollectionsBulkAsync(\n                policyUpdate.OrganizationId,\n                Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == 3),\n                _defaultUserCollectionName);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(WhenDefaultCollectionsDoesNotExistTestCases))]\n    public async Task ExecutePostUpsertSideEffectAsync_WhenDefaultCollectionNameIsInvalid_DoesNothing(\n        IPolicyMetadataModel metadata,\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy,\n        [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState,\n        Organization organization,\n        SutProvider<OrganizationDataOwnershipPolicyValidator> sutProvider)\n    {\n        // Arrange\n        postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId;\n        previousPolicyState.OrganizationId = policyUpdate.OrganizationId;\n        policyUpdate.Enabled = true;\n        organization.Id = policyUpdate.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(organization);\n\n        var policyRequest = new SavePolicyModel(policyUpdate, metadata);\n\n        // Act\n        await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateDefaultCollectionsBulkAsync(default, default, default);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]\n    public async Task ExecuteSideEffectsAsync_OrganizationNotFound_ThrowsInvalidOperationException(\n        Policy postUpdatedPolicy,\n        Policy? previousPolicyState,\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,\n        [OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails,\n        OrganizationDataOwnershipPolicyRequirementFactory factory)\n    {\n        // Arrange\n        var orgPolicyDetailsList = orgPolicyDetails.ToList();\n        foreach (var policyDetail in orgPolicyDetailsList)\n        {\n            policyDetail.OrganizationId = policyUpdate.OrganizationId;\n        }\n\n        var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);\n        var collectionRepository = Substitute.For<ICollectionRepository>();\n        var organizationRepository = Substitute.For<IOrganizationRepository>();\n\n        // Return null to simulate organization not found\n        organizationRepository.GetByIdAsync(Arg.Any<Guid>()).Returns((Organization?)null);\n\n        var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, organizationRepository, [factory]);\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<InvalidOperationException>(() =>\n            sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState));\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]\n    public async Task ExecuteSideEffectsAsync_UseMyItemsDisabled_DoesNotCreateCollections(\n        Policy postUpdatedPolicy,\n        Policy? previousPolicyState,\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,\n        [OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable<OrganizationPolicyDetails> orgPolicyDetails,\n        OrganizationDataOwnershipPolicyRequirementFactory factory)\n    {\n        // Arrange\n        var orgPolicyDetailsList = orgPolicyDetails.ToList();\n        foreach (var policyDetail in orgPolicyDetailsList)\n        {\n            policyDetail.OrganizationId = policyUpdate.OrganizationId;\n        }\n\n        var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);\n        var collectionRepository = Substitute.For<ICollectionRepository>();\n\n        var sut = ArrangeSut(factory, policyRepository, collectionRepository, useMyItems: false);\n        var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));\n\n        // Act\n        await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);\n\n        // Assert - Should NOT create collections when UseMyItems is disabled\n        await collectionRepository\n            .DidNotReceive()\n            .CreateDefaultCollectionsBulkAsync(\n                Arg.Any<Guid>(),\n                Arg.Any<IEnumerable<Guid>>(),\n                Arg.Any<string>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\n[SutProviderCustomize]\npublic class OrganizationPolicyValidatorTests\n{\n    [Theory, BitAutoData]\n    public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithNoFactory_ThrowsNotImplementedException(\n        Guid organizationId,\n        SutProvider<TestOrganizationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), []);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotImplementedException>(() =>\n            sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(\n                organizationId, PolicyType.TwoFactorAuthentication));\n\n        Assert.Contains(\"No Requirement Factory found for\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithMultipleUsers_GroupsByUserId(\n        Guid organizationId,\n        Guid userId1,\n        Guid userId2,\n        SutProvider<TestOrganizationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        var policyDetails = new List<OrganizationPolicyDetails>\n        {\n            new() { UserId = userId1, OrganizationId = organizationId },\n            new() { UserId = userId1, OrganizationId = Guid.NewGuid() },\n            new() { UserId = userId2, OrganizationId = organizationId }\n        };\n\n        var factory = Substitute.For<IPolicyRequirementFactory<TestPolicyRequirement>>();\n        factory.Create(Arg.Any<IEnumerable<PolicyDetails>>()).Returns(new TestPolicyRequirement());\n        factory.Enforce(Arg.Any<PolicyDetails>()).Returns(true);\n\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication)\n            .Returns(policyDetails);\n\n        var factories = new List<IPolicyRequirementFactory<IPolicyRequirement>> { factory };\n        var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), factories);\n\n        // Act\n        var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(\n            organizationId, PolicyType.TwoFactorAuthentication);\n\n        // Assert\n        Assert.Equal(2, result.Count());\n\n        factory.Received(2).Create(Arg.Any<IEnumerable<OrganizationPolicyDetails>>());\n        factory.Received(1).Create(Arg.Is<IEnumerable<OrganizationPolicyDetails>>(\n            results => results.Count() == 1 && results.First().UserId == userId2));\n        factory.Received(1).Create(Arg.Is<IEnumerable<OrganizationPolicyDetails>>(\n            results => results.Count() == 2 && results.First().UserId == userId1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetUserPolicyRequirementsByOrganizationIdAsync_ShouldEnforceFilters(\n        Guid organizationId,\n        Guid userId,\n        SutProvider<TestOrganizationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        var adminUser = new OrganizationPolicyDetails()\n        {\n            UserId = userId,\n            OrganizationId = organizationId,\n            OrganizationUserType = OrganizationUserType.Admin\n        };\n\n        var user = new OrganizationPolicyDetails()\n        {\n            UserId = userId,\n            OrganizationId = organizationId,\n            OrganizationUserType = OrganizationUserType.User\n        };\n\n        var policyDetails = new List<OrganizationPolicyDetails>\n        {\n            adminUser,\n            user\n        };\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication)\n            .Returns(policyDetails);\n\n        var factory = Substitute.For<IPolicyRequirementFactory<TestPolicyRequirement>>();\n        factory.Create(Arg.Any<IEnumerable<PolicyDetails>>()).Returns(new TestPolicyRequirement());\n        factory.Enforce(Arg.Is<PolicyDetails>(p => p.OrganizationUserType == OrganizationUserType.Admin))\n            .Returns(true);\n        factory.Enforce(Arg.Is<PolicyDetails>(p => p.OrganizationUserType == OrganizationUserType.User))\n            .Returns(false);\n\n        var factories = new List<IPolicyRequirementFactory<IPolicyRequirement>> { factory };\n        var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), factories);\n\n        // Act\n        var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(\n            organizationId, PolicyType.TwoFactorAuthentication);\n\n        // Assert\n        Assert.Single(result);\n\n        factory.Received(1).Create(Arg.Is<IEnumerable<PolicyDetails>>(policies =>\n            policies.Count() == 1 && policies.First().OrganizationUserType == OrganizationUserType.Admin));\n\n        factory.Received(1).Enforce(Arg.Is<PolicyDetails>(p => ReferenceEquals(p, adminUser)));\n        factory.Received(1).Enforce(Arg.Is<PolicyDetails>(p => ReferenceEquals(p, user)));\n        factory.Received(2).Enforce(Arg.Any<OrganizationPolicyDetails>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithEmptyPolicyDetails_ReturnsEmptyCollection(\n        Guid organizationId,\n        SutProvider<TestOrganizationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        var factory = Substitute.For<IPolicyRequirementFactory<TestPolicyRequirement>>();\n\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication)\n            .Returns(new List<OrganizationPolicyDetails>());\n\n        var factories = new List<IPolicyRequirementFactory<IPolicyRequirement>> { factory };\n        var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency<IPolicyRepository>(), factories);\n\n        // Act\n        var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync<TestPolicyRequirement>(\n            organizationId, PolicyType.TwoFactorAuthentication);\n\n        // Assert\n        Assert.Empty(result);\n        factory.DidNotReceive().Create(Arg.Any<IEnumerable<PolicyDetails>>());\n    }\n}\n\npublic class TestOrganizationPolicyValidator : OrganizationPolicyValidator\n{\n    public TestOrganizationPolicyValidator(\n        IPolicyRepository policyRepository,\n        IEnumerable<IPolicyRequirementFactory<IPolicyRequirement>>? factories = null)\n        : base(policyRepository, factories ?? [])\n    {\n    }\n\n    public async Task<IEnumerable<T>> TestGetUserPolicyRequirementsByOrganizationIdAsync<T>(Guid organizationId, PolicyType policyType)\n        where T : IPolicyRequirement\n    {\n        return await GetUserPolicyRequirementsByOrganizationIdAsync<T>(organizationId, policyType);\n    }\n\n}\n\npublic class TestPolicyRequirement : IPolicyRequirement\n{\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidatorTests.cs",
    "content": "﻿namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\n[SutProviderCustomize]\npublic class RequireSsoPolicyValidatorTests\n{\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_DisablingPolicy_KeyConnectorEnabled_ValidationError(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy policy,\n        SutProvider<RequireSsoPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = true };\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);\n        Assert.Contains(\"Key Connector is enabled\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_DisablingPolicy_TdeEnabled_ValidationError(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy policy,\n        SutProvider<RequireSsoPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = true };\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);\n        Assert.Contains(\"Trusted device encryption is on\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_DisablingPolicy_DecryptionOptionsNotEnabled_Success(\n        [PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.ResetPassword)] Policy policy,\n        SutProvider<RequireSsoPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = false };\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorEnabled_ValidationError(\n        [PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.RequireSso)] Policy policy,\n        SutProvider<RequireSsoPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = true };\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);\n        Assert.Contains(\"Key Connector is enabled\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeEnabled_ValidationError(\n        [PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.RequireSso)] Policy policy,\n        SutProvider<RequireSsoPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = true };\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);\n        Assert.Contains(\"Trusted device encryption is on\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_DecryptionOptionsNotEnabled_Success(\n        [PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.RequireSso)] Policy policy,\n        SutProvider<RequireSsoPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = false };\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\n[SutProviderCustomize]\npublic class ResetPasswordPolicyValidatorTests\n{\n    [Theory]\n    [BitAutoData(true, false)]\n    [BitAutoData(false, true)]\n    [BitAutoData(false, false)]\n    public async Task ValidateAsync_DisablingPolicy_TdeEnabled_ValidationError(\n        bool policyEnabled,\n        bool autoEnrollEnabled,\n        [PolicyUpdate(PolicyType.ResetPassword)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.ResetPassword)] Policy policy,\n        SutProvider<ResetPasswordPolicyValidator> sutProvider)\n    {\n        policyUpdate.Enabled = policyEnabled;\n        policyUpdate.SetDataModel(new ResetPasswordDataModel\n        {\n            AutoEnrollEnabled = autoEnrollEnabled\n        });\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = true };\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);\n        Assert.Contains(\"Trusted device encryption is on and requires this policy.\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_DisablingPolicy_TdeNotEnabled_Success(\n        [PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.ResetPassword)] Policy policy,\n        SutProvider<ResetPasswordPolicyValidator> sutProvider)\n    {\n        policyUpdate.SetDataModel(new ResetPasswordDataModel\n        {\n            AutoEnrollEnabled = false\n        });\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = false };\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Theory]\n    [BitAutoData(true, false)]\n    [BitAutoData(false, true)]\n    [BitAutoData(false, false)]\n    public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeEnabled_ValidationError(\n        bool policyEnabled,\n        bool autoEnrollEnabled,\n        [PolicyUpdate(PolicyType.ResetPassword)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.ResetPassword)] Policy policy,\n        SutProvider<ResetPasswordPolicyValidator> sutProvider)\n    {\n        policyUpdate.Enabled = policyEnabled;\n        policyUpdate.SetDataModel(new ResetPasswordDataModel\n        {\n            AutoEnrollEnabled = autoEnrollEnabled\n        });\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = true };\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);\n        Assert.Contains(\"Trusted device encryption is on and requires this policy.\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeNotEnabled_Success(\n        [PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.ResetPassword)] Policy policy,\n        SutProvider<ResetPasswordPolicyValidator> sutProvider)\n    {\n        policyUpdate.SetDataModel(new ResetPasswordDataModel\n        {\n            AutoEnrollEnabled = false\n        });\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = false };\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.AdminConsole.Utilities.Commands;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\n[SutProviderCustomize]\npublic class SingleOrgPolicyValidatorTests\n{\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_DisablingPolicy_KeyConnectorEnabled_ValidationError(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy policy,\n        SutProvider<SingleOrgPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = true };\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);\n        Assert.Contains(\"Key Connector is enabled\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_DisablingPolicy_KeyConnectorNotEnabled_Success(\n        [PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.ResetPassword)] Policy policy,\n        SutProvider<SingleOrgPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = false };\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy);\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Theory, BitAutoData]\n    public async Task OnSaveSideEffectsAsync_RevokesNonCompliantUsers(\n        [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg, false)] Policy policy,\n        Guid savingUserId,\n        Guid nonCompliantUserId,\n        Organization organization, SutProvider<SingleOrgPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;\n\n        var compliantUser1 = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organization.Id,\n            Type = OrganizationUserType.User,\n            Status = OrganizationUserStatusType.Confirmed,\n            UserId = new Guid(),\n            Email = \"user1@example.com\"\n        };\n\n        var compliantUser2 = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organization.Id,\n            Type = OrganizationUserType.User,\n            Status = OrganizationUserStatusType.Confirmed,\n            UserId = new Guid(),\n            Email = \"user2@example.com\"\n        };\n\n        var nonCompliantUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organization.Id,\n            Type = OrganizationUserType.User,\n            Status = OrganizationUserStatusType.Confirmed,\n            UserId = nonCompliantUserId,\n            Email = \"user3@example.com\"\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns([compliantUser1, compliantUser2, nonCompliantUser]);\n\n        var otherOrganizationUser = new OrganizationUser\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = new Guid(),\n            UserId = nonCompliantUserId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))\n            .Returns([otherOrganizationUser]);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);\n\n        sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())\n            .Returns(new CommandResult());\n\n        await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);\n\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(\n                Arg.Is<RevokeOrganizationUsersRequest>(r =>\n                    r.OrganizationId == organization.Id &&\n                    r.OrganizationUsers.Count() == 1 &&\n                    r.OrganizationUsers.First().Id == nonCompliantUser.Id));\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceive()\n            .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceive()\n            .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email);\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorEnabled_ValidationError(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy policy,\n        SutProvider<SingleOrgPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = true };\n        ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);\n        Assert.Contains(\"Key Connector is enabled\", result, StringComparison.OrdinalIgnoreCase);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorNotEnabled_Success(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy policy,\n        SutProvider<SingleOrgPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = policyUpdate.OrganizationId;\n\n        var ssoConfig = new SsoConfig { Enabled = false };\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns(ssoConfig);\n\n        sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()\n            .HasVerifiedDomainsAsync(policyUpdate.OrganizationId)\n            .Returns(false);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy);\n        Assert.True(string.IsNullOrEmpty(result));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePreUpsertSideEffectAsync_RevokesNonCompliantUsers(\n        [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg, false)] Policy policy,\n        Guid savingUserId,\n        Guid nonCompliantUserId,\n        Organization organization,\n        SutProvider<SingleOrgPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;\n\n        var compliantUser1 = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organization.Id,\n            Type = OrganizationUserType.User,\n            Status = OrganizationUserStatusType.Confirmed,\n            UserId = new Guid(),\n            Email = \"user1@example.com\"\n        };\n\n        var compliantUser2 = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organization.Id,\n            Type = OrganizationUserType.User,\n            Status = OrganizationUserStatusType.Confirmed,\n            UserId = new Guid(),\n            Email = \"user2@example.com\"\n        };\n\n        var nonCompliantUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = organization.Id,\n            Type = OrganizationUserType.User,\n            Status = OrganizationUserStatusType.Confirmed,\n            UserId = nonCompliantUserId,\n            Email = \"user3@example.com\"\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns([compliantUser1, compliantUser2, nonCompliantUser]);\n\n        var otherOrganizationUser = new OrganizationUser\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = new Guid(),\n            UserId = nonCompliantUserId,\n            Status = OrganizationUserStatusType.Confirmed\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByManyUsersAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(nonCompliantUserId)))\n            .Returns([otherOrganizationUser]);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(savingUserId);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization);\n\n        sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())\n            .Returns(new CommandResult());\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);\n\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(\n                Arg.Is<RevokeOrganizationUsersRequest>(r =>\n                    r.OrganizationId == organization.Id &&\n                    r.OrganizationUsers.Count() == 1 &&\n                    r.OrganizationUsers.First().Id == nonCompliantUser.Id));\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceive()\n            .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceive()\n            .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email);\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Bit.Core.AdminConsole.Utilities.Commands;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\n[SutProviderCustomize]\npublic class TwoFactorAuthenticationPolicyValidatorTests\n{\n    [Theory, BitAutoData]\n    public async Task OnSaveSideEffectsAsync_GivenNonCompliantUsersWithoutMasterPassword_Throws(\n        Organization organization,\n        [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,\n        SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            Status = OrganizationUserStatusType.Confirmed,\n            Type = OrganizationUserType.User,\n            Email = \"user3@test.com\",\n            Name = \"TEST\",\n            UserId = Guid.NewGuid(),\n            HasMasterPassword = false\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns([orgUserDetailUserWithout2Fa]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())\n            .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()\n            {\n                (orgUserDetailUserWithout2Fa, false),\n            });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy));\n\n        Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task OnSaveSideEffectsAsync_RevokesOnlyNonCompliantUsers(\n        Organization organization,\n        [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,\n        SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        policy.OrganizationId = policyUpdate.OrganizationId;\n        organization.Id = policyUpdate.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var nonCompliantUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            Status = OrganizationUserStatusType.Confirmed,\n            Type = OrganizationUserType.User,\n            Email = \"user3@test.com\",\n            Name = \"TEST\",\n            UserId = Guid.NewGuid(),\n            HasMasterPassword = true\n        };\n\n        var compliantUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            Status = OrganizationUserStatusType.Confirmed,\n            Type = OrganizationUserType.User,\n            Email = \"user4@test.com\",\n            Name = \"TEST\",\n            UserId = Guid.NewGuid(),\n            HasMasterPassword = true\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns([nonCompliantUser, compliantUser]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())\n            .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()\n            {\n                (nonCompliantUser, false),\n                (compliantUser, true)\n            });\n\n        sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())\n            .Returns(new CommandResult());\n\n        // Act\n        await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy);\n\n        // Assert\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());\n\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(Arg.Is<RevokeOrganizationUsersRequest>(req =>\n                    req.OrganizationId == policyUpdate.OrganizationId &&\n                    req.OrganizationUsers.SequenceEqual(new[] { nonCompliantUser })\n            ));\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),\n                nonCompliantUser.Email);\n\n        // Did not send out an email for compliantUser\n        await sutProvider.GetDependency<IMailService>()\n            .Received(0)\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),\n                compliantUser.Email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePreUpsertSideEffectAsync_GivenNonCompliantUsersWithoutMasterPassword_Throws(\n        Organization organization,\n        [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,\n        SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)\n    {\n        policy.OrganizationId = organization.Id = policyUpdate.OrganizationId;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            Status = OrganizationUserStatusType.Confirmed,\n            Type = OrganizationUserType.User,\n            Email = \"user3@test.com\",\n            Name = \"TEST\",\n            UserId = Guid.NewGuid(),\n            HasMasterPassword = false\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns([orgUserDetailUserWithout2Fa]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())\n            .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()\n            {\n                (orgUserDetailUserWithout2Fa, false),\n            });\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy));\n\n        Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ExecutePreUpsertSideEffectAsync_RevokesOnlyNonCompliantUsers(\n        Organization organization,\n        [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy,\n        SutProvider<TwoFactorAuthenticationPolicyValidator> sutProvider)\n    {\n        // Arrange\n        policy.OrganizationId = policyUpdate.OrganizationId;\n        organization.Id = policyUpdate.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var nonCompliantUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            Status = OrganizationUserStatusType.Confirmed,\n            Type = OrganizationUserType.User,\n            Email = \"user3@test.com\",\n            Name = \"TEST\",\n            UserId = Guid.NewGuid(),\n            HasMasterPassword = true\n        };\n\n        var compliantUser = new OrganizationUserUserDetails\n        {\n            Id = Guid.NewGuid(),\n            Status = OrganizationUserStatusType.Confirmed,\n            Type = OrganizationUserType.User,\n            Email = \"user4@test.com\",\n            Name = \"TEST\",\n            UserId = Guid.NewGuid(),\n            HasMasterPassword = true\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId)\n            .Returns([nonCompliantUser, compliantUser]);\n\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())\n            .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()\n            {\n                (nonCompliantUser, false),\n                (compliantUser, true)\n            });\n\n        sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>())\n            .Returns(new CommandResult());\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        // Act\n        await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy);\n\n        // Assert\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(Arg.Any<RevokeOrganizationUsersRequest>());\n\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(Arg.Is<RevokeOrganizationUsersRequest>(req =>\n                    req.OrganizationId == policyUpdate.OrganizationId &&\n                    req.OrganizationUsers.SequenceEqual(new[] { nonCompliantUser })\n            ));\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),\n                nonCompliantUser.Email);\n\n        // Did not send out an email for compliantUser\n        await sutProvider.GetDependency<IMailService>()\n            .Received(0)\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(),\n                compliantUser.Email);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/UriMatchDefaultPolicyValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;\n\npublic class UriMatchDefaultPolicyValidatorTests\n{\n    private readonly UriMatchDefaultPolicyValidator _validator = new();\n\n    [Fact]\n    // Test that the Type property returns the correct PolicyType for this validator\n    public void Type_ReturnsUriMatchDefaults()\n    {\n        Assert.Equal(PolicyType.UriMatchDefaults, _validator.Type);\n    }\n\n    [Fact]\n    // Test that the RequiredPolicies property returns exactly one policy (SingleOrg) as a prerequisite\n    // for enabling the UriMatchDefaults policy, ensuring proper policy dependency enforcement\n    public void RequiredPolicies_ReturnsSingleOrgPolicy()\n    {\n        var requiredPolicies = _validator.RequiredPolicies.ToList();\n\n        Assert.Single(requiredPolicies);\n        Assert.Contains(PolicyType.SingleOrg, requiredPolicies);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\nusing EventType = Bit.Core.Enums.EventType;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\n\npublic class SavePolicyCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task SaveAsync_NewPolicy_Success([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)\n    {\n        var fakePolicyValidator = new FakeSingleOrgPolicyValidator();\n        fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns(\"\");\n        var sutProvider = SutProviderFactory([fakePolicyValidator]);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]);\n\n        var creationDate = sutProvider.GetDependency<FakeTimeProvider>().Start;\n\n        await sutProvider.Sut.SaveAsync(policyUpdate);\n\n        await fakePolicyValidator.ValidateAsyncMock.Received(1).Invoke(policyUpdate, null);\n        fakePolicyValidator.OnSaveSideEffectsAsyncMock.Received(1).Invoke(policyUpdate, null);\n\n        await AssertPolicySavedAsync(sutProvider, policyUpdate);\n        await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(Arg.Is<Policy>(p =>\n            p.CreationDate == creationDate &&\n            p.RevisionDate == creationDate));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_ExistingPolicy_Success(\n        [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)\n    {\n        var fakePolicyValidator = new FakeSingleOrgPolicyValidator();\n        fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns(\"\");\n        var sutProvider = SutProviderFactory([fakePolicyValidator]);\n\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)\n            .Returns(currentPolicy);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy]);\n\n        // Store mutable properties separately to assert later\n        var id = currentPolicy.Id;\n        var organizationId = currentPolicy.OrganizationId;\n        var type = currentPolicy.Type;\n        var creationDate = currentPolicy.CreationDate;\n        var revisionDate = sutProvider.GetDependency<FakeTimeProvider>().Start;\n\n        await sutProvider.Sut.SaveAsync(policyUpdate);\n\n        await fakePolicyValidator.ValidateAsyncMock.Received(1).Invoke(policyUpdate, currentPolicy);\n        fakePolicyValidator.OnSaveSideEffectsAsyncMock.Received(1).Invoke(policyUpdate, currentPolicy);\n\n        await AssertPolicySavedAsync(sutProvider, policyUpdate);\n        // Additional assertions to ensure certain properties have or have not been updated\n        await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(Arg.Is<Policy>(p =>\n            p.Id == id &&\n            p.OrganizationId == organizationId &&\n            p.Type == type &&\n            p.CreationDate == creationDate &&\n            p.RevisionDate == revisionDate));\n    }\n\n    [Fact]\n    public void Constructor_DuplicatePolicyValidators_Throws()\n    {\n        var exception = Assert.Throws<Exception>(() =>\n            new SavePolicyCommand(\n                Substitute.For<IOrganizationRepository>(),\n                Substitute.For<IEventService>(),\n                Substitute.For<IPolicyRepository>(),\n                [new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],\n                Substitute.For<TimeProvider>(),\n                Substitute.For<IPostSavePolicySideEffect>(),\n                Substitute.For<IPushNotificationService>()));\n        Assert.Contains(\"Duplicate PolicyValidator for SingleOrg policy\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)\n    {\n        var sutProvider = SutProviderFactory();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(Task.FromResult<Organization?>(null));\n\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(policyUpdate));\n\n        Assert.Contains(\"Organization not found\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)\n    {\n        var sutProvider = SutProviderFactory();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(new Organization\n            {\n                Id = policyUpdate.OrganizationId,\n                UsePolicies = false\n            });\n\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(policyUpdate));\n\n        Assert.Contains(\"cannot use policies\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_RequiredPolicyIsNull_Throws(\n        [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate)\n    {\n        var sutProvider = SutProviderFactory([\n            new FakeRequireSsoPolicyValidator(),\n            new FakeSingleOrgPolicyValidator()\n        ]);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([]);\n\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(policyUpdate));\n\n        Assert.Contains(\"Turn on the Single organization policy because it is required for the Require single sign-on authentication policy\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_RequiredPolicyNotEnabled_Throws(\n        [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy)\n    {\n        var sutProvider = SutProviderFactory([\n            new FakeRequireSsoPolicyValidator(),\n            new FakeSingleOrgPolicyValidator()\n        ]);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([singleOrgPolicy]);\n\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(policyUpdate));\n\n        Assert.Contains(\"Turn on the Single organization policy because it is required for the Require single sign-on authentication policy\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_RequiredPolicyEnabled_Success(\n        [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy)\n    {\n        var sutProvider = SutProviderFactory([\n            new FakeRequireSsoPolicyValidator(),\n            new FakeSingleOrgPolicyValidator()\n        ]);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([singleOrgPolicy]);\n\n        await sutProvider.Sut.SaveAsync(policyUpdate);\n        await AssertPolicySavedAsync(sutProvider, policyUpdate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_DependentPolicyIsEnabled_Throws(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy currentPolicy,\n        [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy) // depends on Single Org\n    {\n        var sutProvider = SutProviderFactory([\n            new FakeRequireSsoPolicyValidator(),\n            new FakeSingleOrgPolicyValidator()\n        ]);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy, requireSsoPolicy]);\n\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(policyUpdate));\n\n        Assert.Contains(\"Turn off the Require single sign-on authentication policy because it requires the Single organization policy\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_MultipleDependentPoliciesAreEnabled_Throws(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy currentPolicy,\n        [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy, // depends on Single Org\n        [Policy(PolicyType.MaximumVaultTimeout)] Policy vaultTimeoutPolicy) // depends on Single Org\n    {\n        var sutProvider = SutProviderFactory([\n            new FakeRequireSsoPolicyValidator(),\n            new FakeSingleOrgPolicyValidator(),\n            new FakeVaultTimeoutPolicyValidator()\n        ]);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy, requireSsoPolicy, vaultTimeoutPolicy]);\n\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(policyUpdate));\n\n        Assert.Contains(\"Turn off all of the policies that require the Single organization policy\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_DependentPolicyNotEnabled_Success(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy currentPolicy,\n        [Policy(PolicyType.RequireSso, false)] Policy requireSsoPolicy) // depends on Single Org but is not enabled\n    {\n        var sutProvider = SutProviderFactory([\n            new FakeRequireSsoPolicyValidator(),\n            new FakeSingleOrgPolicyValidator()\n        ]);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy, requireSsoPolicy]);\n\n        await sutProvider.Sut.SaveAsync(policyUpdate);\n\n        await AssertPolicySavedAsync(sutProvider, policyUpdate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_ThrowsOnValidationError([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)\n    {\n        var fakePolicyValidator = new FakeSingleOrgPolicyValidator();\n        fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns(\"Validation error!\");\n        var sutProvider = SutProviderFactory([fakePolicyValidator]);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]);\n\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(policyUpdate));\n\n        Assert.Contains(\"Validation error!\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task VNextSaveAsync_OrganizationDataOwnershipPolicy_ExecutesPostSaveSideEffects(\n        [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory();\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)\n            .Returns(currentPolicy);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy]);\n\n        // Act\n        var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);\n\n        // Assert\n        await sutProvider.GetDependency<IPolicyRepository>()\n            .Received(1)\n            .UpsertAsync(result);\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogPolicyEventAsync(result, EventType.Policy_Updated);\n\n        await sutProvider.GetDependency<IPostSavePolicySideEffect>()\n            .Received(1)\n            .ExecuteSideEffectsAsync(savePolicyModel, result, currentPolicy);\n    }\n\n    [Theory]\n    [BitAutoData(PolicyType.SingleOrg)]\n    [BitAutoData(PolicyType.TwoFactorAuthentication)]\n    public async Task VNextSaveAsync_NonOrganizationDataOwnershipPolicy_DoesNotExecutePostSaveSideEffects(\n         PolicyType policyType,\n         Policy currentPolicy,\n         [PolicyUpdate] PolicyUpdate policyUpdate)\n    {\n        // Arrange\n        policyUpdate.Type = policyType;\n        currentPolicy.Type = policyType;\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n\n\n        var sutProvider = SutProviderFactory();\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)\n            .Returns(currentPolicy);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy]);\n\n        // Act\n        var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);\n\n        // Assert\n        await sutProvider.GetDependency<IPolicyRepository>()\n            .Received(1)\n            .UpsertAsync(result);\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogPolicyEventAsync(result, EventType.Policy_Updated);\n\n        await sutProvider.GetDependency<IPostSavePolicySideEffect>()\n            .DidNotReceiveWithAnyArgs()\n            .ExecuteSideEffectsAsync(default!, default!, default!);\n    }\n\n    [Theory, BitAutoData]\n    public async Task VNextSaveAsync_SendsPushNotification(\n        [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)\n    {\n        // Arrange\n        var fakePolicyValidator = new FakeSingleOrgPolicyValidator();\n        fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns(\"\");\n        var sutProvider = SutProviderFactory([fakePolicyValidator]);\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)\n            .Returns(currentPolicy);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy]);\n\n        // Act\n        var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);\n\n        // Assert\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1)\n            .PushAsync(Arg.Is<PushNotification<SyncPolicyPushNotification>>(p =>\n                p.Type == PushType.PolicyChanged &&\n                p.Target == NotificationTarget.Organization &&\n                p.TargetId == policyUpdate.OrganizationId &&\n                p.ExcludeCurrentContext == false &&\n                p.Payload.OrganizationId == policyUpdate.OrganizationId &&\n                p.Payload.Policy.Id == result.Id &&\n                p.Payload.Policy.Type == policyUpdate.Type &&\n                p.Payload.Policy.Enabled == policyUpdate.Enabled &&\n                p.Payload.Policy.Data == policyUpdate.Data));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_SendsPushNotification([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)\n    {\n        var fakePolicyValidator = new FakeSingleOrgPolicyValidator();\n        fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns(\"\");\n        var sutProvider = SutProviderFactory([fakePolicyValidator]);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]);\n\n        var result = await sutProvider.Sut.SaveAsync(policyUpdate);\n\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1)\n            .PushAsync(Arg.Is<PushNotification<SyncPolicyPushNotification>>(p =>\n                p.Type == PushType.PolicyChanged &&\n                p.Target == NotificationTarget.Organization &&\n                p.TargetId == policyUpdate.OrganizationId &&\n                p.ExcludeCurrentContext == false &&\n                p.Payload.OrganizationId == policyUpdate.OrganizationId &&\n                p.Payload.Policy.Id == result.Id &&\n                p.Payload.Policy.Type == policyUpdate.Type &&\n                p.Payload.Policy.Enabled == policyUpdate.Enabled &&\n                p.Payload.Policy.Data == policyUpdate.Data));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_ExistingPolicy_SendsPushNotificationWithUpdatedPolicy(\n        [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)\n    {\n        var fakePolicyValidator = new FakeSingleOrgPolicyValidator();\n        fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns(\"\");\n        var sutProvider = SutProviderFactory([fakePolicyValidator]);\n\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)\n            .Returns(currentPolicy);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy]);\n\n        var result = await sutProvider.Sut.SaveAsync(policyUpdate);\n\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1)\n            .PushAsync(Arg.Is<PushNotification<SyncPolicyPushNotification>>(p =>\n                p.Type == PushType.PolicyChanged &&\n                p.Target == NotificationTarget.Organization &&\n                p.TargetId == policyUpdate.OrganizationId &&\n                p.ExcludeCurrentContext == false &&\n                p.Payload.OrganizationId == policyUpdate.OrganizationId &&\n                p.Payload.Policy.Id == result.Id &&\n                p.Payload.Policy.Type == policyUpdate.Type &&\n                p.Payload.Policy.Enabled == policyUpdate.Enabled &&\n                p.Payload.Policy.Data == policyUpdate.Data));\n    }\n\n    /// <summary>\n    /// Returns a new SutProvider with the PolicyValidators registered in the Sut.\n    /// </summary>\n    private static SutProvider<SavePolicyCommand> SutProviderFactory(IEnumerable<IPolicyValidator>? policyValidators = null)\n    {\n        return new SutProvider<SavePolicyCommand>()\n            .WithFakeTimeProvider()\n            .SetDependency(policyValidators ?? [])\n            .SetDependency(Substitute.For<IPostSavePolicySideEffect>())\n            .Create();\n    }\n\n    private static void ArrangeOrganization(SutProvider<SavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(new Organization\n            {\n                Id = policyUpdate.OrganizationId,\n                UsePolicies = true\n            });\n    }\n\n    private static async Task AssertPolicyNotSavedAsync(SutProvider<SavePolicyCommand> sutProvider)\n    {\n        await sutProvider.GetDependency<IPolicyRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default!);\n\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogPolicyEventAsync(default, default);\n    }\n\n    private static async Task AssertPolicySavedAsync(SutProvider<SavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)\n    {\n        var expectedPolicy = () => Arg.Is<Policy>(p =>\n            p.Type == policyUpdate.Type &&\n            p.OrganizationId == policyUpdate.OrganizationId &&\n            p.Enabled == policyUpdate.Enabled &&\n            p.Data == policyUpdate.Data);\n\n        await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(expectedPolicy());\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogPolicyEventAsync(expectedPolicy(), EventType.Policy_Updated);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/OrganizationFeatures/Policies/VNextSavePolicyCommandTests.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing OneOf.Types;\nusing Xunit;\nusing EventType = Bit.Core.Enums.EventType;\n\nnamespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;\n\npublic class VNextSavePolicyCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task SaveAsync_NewPolicy_Success([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)\n    {\n        // Arrange\n        var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();\n        fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns(\"\");\n        var sutProvider = SutProviderFactory([\n            new FakeSingleOrgDependencyEvent(),\n            fakePolicyValidationEvent\n        ]);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var newPolicy = new Policy\n        {\n            Type = policyUpdate.Type,\n            OrganizationId = policyUpdate.OrganizationId,\n            Enabled = false\n        };\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([newPolicy]);\n\n        var creationDate = sutProvider.GetDependency<FakeTimeProvider>().Start;\n\n        // Act\n        await sutProvider.Sut.SaveAsync(savePolicyModel);\n\n        // Assert\n        await fakePolicyValidationEvent.ValidateAsyncMock\n            .Received(1)\n            .Invoke(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>());\n\n        await AssertPolicySavedAsync(sutProvider, policyUpdate);\n\n        await sutProvider.GetDependency<IPolicyRepository>()\n            .Received(1)\n            .UpsertAsync(Arg.Is<Policy>(p =>\n                p.CreationDate == creationDate &&\n                p.RevisionDate == creationDate));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_ExistingPolicy_Success(\n        [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)\n    {\n        // Arrange\n        var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();\n        fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns(\"\");\n        var sutProvider = SutProviderFactory([\n            new FakeSingleOrgDependencyEvent(),\n            fakePolicyValidationEvent\n        ]);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        currentPolicy.OrganizationId = policyUpdate.OrganizationId;\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)\n            .Returns(currentPolicy);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy]);\n\n        // Act\n        await sutProvider.Sut.SaveAsync(savePolicyModel);\n\n        // Assert\n        await fakePolicyValidationEvent.ValidateAsyncMock\n            .Received(1)\n            .Invoke(Arg.Any<SavePolicyModel>(), currentPolicy);\n\n        await AssertPolicySavedAsync(sutProvider, policyUpdate);\n\n\n        var revisionDate = sutProvider.GetDependency<FakeTimeProvider>().Start;\n\n        await sutProvider.GetDependency<IPolicyRepository>()\n            .Received(1)\n            .UpsertAsync(Arg.Is<Policy>(p =>\n                p.Id == currentPolicy.Id &&\n                p.OrganizationId == currentPolicy.OrganizationId &&\n                p.Type == currentPolicy.Type &&\n                p.CreationDate == currentPolicy.CreationDate &&\n                p.RevisionDate == revisionDate));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory();\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(Task.FromResult<Organization?>(null));\n\n        // Act\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(savePolicyModel));\n\n        // Assert\n        Assert.Contains(\"Organization not found\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory();\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(new Organization\n            {\n                Id = policyUpdate.OrganizationId,\n                UsePolicies = false\n            });\n\n        // Act\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(savePolicyModel));\n\n        // Assert\n        Assert.Contains(\"cannot use policies\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_RequiredPolicyIsNull_Throws(\n        [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory(\n            [\n                new FakeRequireSsoDependencyEvent(),\n                new FakeSingleOrgDependencyEvent()\n            ]);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var requireSsoPolicy = new Policy\n        {\n            Type = PolicyType.RequireSso,\n            OrganizationId = policyUpdate.OrganizationId,\n            Enabled = false\n        };\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([requireSsoPolicy]);\n\n        // Act\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(savePolicyModel));\n\n        // Assert\n        Assert.Contains(\"Turn on the Single organization policy because it is required for the Require single sign-on authentication policy\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_RequiredPolicyNotEnabled_Throws(\n        [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory(\n            [\n                new FakeRequireSsoDependencyEvent(),\n                new FakeSingleOrgDependencyEvent()\n            ]);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var requireSsoPolicy = new Policy\n        {\n            Type = PolicyType.RequireSso,\n            OrganizationId = policyUpdate.OrganizationId,\n            Enabled = false\n        };\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([singleOrgPolicy, requireSsoPolicy]);\n\n        // Act\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(savePolicyModel));\n\n        // Assert\n        Assert.Contains(\"Turn on the Single organization policy because it is required for the Require single sign-on authentication policy\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_RequiredPolicyEnabled_Success(\n        [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory(\n            [\n                new FakeRequireSsoDependencyEvent(),\n                new FakeSingleOrgDependencyEvent()\n            ]);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var requireSsoPolicy = new Policy\n        {\n            Type = PolicyType.RequireSso,\n            OrganizationId = policyUpdate.OrganizationId,\n            Enabled = false\n        };\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([singleOrgPolicy, requireSsoPolicy]);\n\n        // Act\n        await sutProvider.Sut.SaveAsync(savePolicyModel);\n\n        // Assert\n        await AssertPolicySavedAsync(sutProvider, policyUpdate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_DependentPolicyIsEnabled_Throws(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy currentPolicy,\n        [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory(\n            [\n                new FakeRequireSsoDependencyEvent(),\n                new FakeSingleOrgDependencyEvent()\n            ]);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy, requireSsoPolicy]);\n\n        // Act\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(savePolicyModel));\n\n        // Assert\n        Assert.Contains(\"Turn off the Require single sign-on authentication policy because it requires the Single organization policy\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_MultipleDependentPoliciesAreEnabled_Throws(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy currentPolicy,\n        [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy,\n        [Policy(PolicyType.MaximumVaultTimeout)] Policy vaultTimeoutPolicy)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory(\n            [\n                new FakeRequireSsoDependencyEvent(),\n                new FakeSingleOrgDependencyEvent(),\n                new FakeVaultTimeoutDependencyEvent()\n            ]);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy, requireSsoPolicy, vaultTimeoutPolicy]);\n\n        // Act\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(savePolicyModel));\n\n        // Assert\n        Assert.Contains(\"Turn off all of the policies that require the Single organization policy\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_DependentPolicyNotEnabled_Success(\n        [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate,\n        [Policy(PolicyType.SingleOrg)] Policy currentPolicy,\n        [Policy(PolicyType.RequireSso, false)] Policy requireSsoPolicy)\n    {\n        // Arrange\n        var sutProvider = SutProviderFactory(\n            [\n                new FakeRequireSsoDependencyEvent(),\n                new FakeSingleOrgDependencyEvent()\n            ]);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)\n            .Returns([currentPolicy, requireSsoPolicy]);\n\n        // Act\n        await sutProvider.Sut.SaveAsync(savePolicyModel);\n\n        // Assert\n        await AssertPolicySavedAsync(sutProvider, policyUpdate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_ThrowsOnValidationError([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)\n    {\n        // Arrange\n        var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent();\n        fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any<SavePolicyModel>(), Arg.Any<Policy>()).Returns(\"Validation error!\");\n        var sutProvider = SutProviderFactory([\n            new FakeSingleOrgDependencyEvent(),\n            fakePolicyValidationEvent\n        ]);\n\n        var savePolicyModel = new SavePolicyModel(policyUpdate);\n\n        var singleOrgPolicy = new Policy\n        {\n            Type = PolicyType.SingleOrg,\n            OrganizationId = policyUpdate.OrganizationId,\n            Enabled = false\n        };\n\n        ArrangeOrganization(sutProvider, policyUpdate);\n        sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([singleOrgPolicy]);\n\n        // Act\n        var badRequestException = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(savePolicyModel));\n\n        // Assert\n        Assert.Contains(\"Validation error!\", badRequestException.Message, StringComparison.OrdinalIgnoreCase);\n        await AssertPolicyNotSavedAsync(sutProvider);\n    }\n\n    /// <summary>\n    /// Returns a new SutProvider with the PolicyUpdateEvents registered in the Sut.\n    /// </summary>\n    private static SutProvider<VNextSavePolicyCommand> SutProviderFactory(\n        IEnumerable<IPolicyUpdateEvent>? policyUpdateEvents = null)\n    {\n        var policyEventHandlerFactory = Substitute.For<IPolicyEventHandlerFactory>();\n        var handlers = policyUpdateEvents ?? [];\n\n        // Setup factory to return handlers based on type\n        policyEventHandlerFactory.GetHandler<IEnforceDependentPoliciesEvent>(Arg.Any<PolicyType>())\n            .Returns(callInfo =>\n            {\n                var policyType = callInfo.Arg<PolicyType>();\n                var handler = handlers.OfType<IEnforceDependentPoliciesEvent>().FirstOrDefault(e => e.Type == policyType);\n                return handler != null ? OneOf.OneOf<IEnforceDependentPoliciesEvent, None>.FromT0(handler) : OneOf.OneOf<IEnforceDependentPoliciesEvent, None>.FromT1(new None());\n            });\n\n        policyEventHandlerFactory.GetHandler<IPolicyValidationEvent>(Arg.Any<PolicyType>())\n            .Returns(callInfo =>\n            {\n                var policyType = callInfo.Arg<PolicyType>();\n                var handler = handlers.OfType<IPolicyValidationEvent>().FirstOrDefault(e => e.Type == policyType);\n                return handler != null ? OneOf.OneOf<IPolicyValidationEvent, None>.FromT0(handler) : OneOf.OneOf<IPolicyValidationEvent, None>.FromT1(new None());\n            });\n\n        policyEventHandlerFactory.GetHandler<IOnPolicyPreUpdateEvent>(Arg.Any<PolicyType>())\n            .Returns(new None());\n\n        policyEventHandlerFactory.GetHandler<IOnPolicyPostUpdateEvent>(Arg.Any<PolicyType>())\n            .Returns(new None());\n\n        return new SutProvider<VNextSavePolicyCommand>()\n            .WithFakeTimeProvider()\n            .SetDependency(handlers)\n            .SetDependency(policyEventHandlerFactory)\n            .Create();\n    }\n\n    private static void ArrangeOrganization(SutProvider<VNextSavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(policyUpdate.OrganizationId)\n            .Returns(new Organization\n            {\n                Id = policyUpdate.OrganizationId,\n                UsePolicies = true\n            });\n    }\n\n    private static async Task AssertPolicyNotSavedAsync(SutProvider<VNextSavePolicyCommand> sutProvider)\n    {\n        await sutProvider.GetDependency<IPolicyRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default!);\n\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogPolicyEventAsync(default, default);\n    }\n\n    private static async Task AssertPolicySavedAsync(SutProvider<VNextSavePolicyCommand> sutProvider, PolicyUpdate policyUpdate)\n    {\n        await sutProvider.GetDependency<IPolicyRepository>().Received(1).UpsertAsync(ExpectedPolicy());\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogPolicyEventAsync(ExpectedPolicy(), EventType.Policy_Updated);\n\n        return;\n\n        Policy ExpectedPolicy() => Arg.Is<Policy>(\n            p =>\n                p.Type == policyUpdate.Type\n                && p.OrganizationId == policyUpdate.OrganizationId\n                && p.Enabled == policyUpdate.Enabled\n                && p.Data == policyUpdate.Data);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Services/GroupServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Services;\n\n[SutProviderCustomize]\n[OrganizationCustomize(UseGroups = true)]\npublic class GroupServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_ValidData_DeletesGroup(Group group, SutProvider<GroupService> sutProvider)\n    {\n        await sutProvider.Sut.DeleteAsync(group);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received().DeleteAsync(group);\n        await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Deleted);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_ValidData_WithEventSystemUser_DeletesGroup(Group group, EventSystemUser eventSystemUser, SutProvider<GroupService> sutProvider)\n    {\n        await sutProvider.Sut.DeleteAsync(group, eventSystemUser);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received().DeleteAsync(group);\n        await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Deleted, eventSystemUser);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteUserAsync_ValidData_DeletesUserInGroupRepository(Group group, Organization organization, OrganizationUser organizationUser, SutProvider<GroupService> sutProvider)\n    {\n        group.OrganizationId = organization.Id;\n        organization.UseGroups = true;\n        organizationUser.OrganizationId = organization.Id;\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        await sutProvider.Sut.DeleteUserAsync(group, organizationUser.Id);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received().DeleteUserAsync(group.Id, organizationUser.Id);\n        await sutProvider.GetDependency<IEventService>().Received()\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteUserAsync_ValidData_WithEventSystemUser_DeletesUserInGroupRepository(Group group, Organization organization, OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<GroupService> sutProvider)\n    {\n        group.OrganizationId = organization.Id;\n        organization.UseGroups = true;\n        organizationUser.OrganizationId = organization.Id;\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        await sutProvider.Sut.DeleteUserAsync(group, organizationUser.Id, eventSystemUser);\n\n        await sutProvider.GetDependency<IGroupRepository>().Received().DeleteUserAsync(group.Id, organizationUser.Id);\n        await sutProvider.GetDependency<IEventService>().Received()\n            .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups, eventSystemUser);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteUserAsync_InvalidUser_ThrowsNotFound(Group group, Organization organization, OrganizationUser organizationUser, SutProvider<GroupService> sutProvider)\n    {\n        group.OrganizationId = organization.Id;\n        organization.UseGroups = true;\n        // organizationUser.OrganizationId = organization.Id;\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id)\n            .Returns(organizationUser);\n\n        // user not in organization\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteUserAsync(group, organizationUser.Id));\n        // invalid user\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteUserAsync(group, Guid.NewGuid()));\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs()\n            .DeleteUserAsync(default, default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs()\n            .LogOrganizationUserEventAsync<OrganizationUser>(default, default);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\npublic class IntegrationTypeTests\n{\n    [Fact]\n    public void ToRoutingKey_CloudBillingSync_ThrowsException()\n    {\n        Assert.Throws<ArgumentOutOfRangeException>(() => IntegrationType.CloudBillingSync.ToRoutingKey());\n    }\n\n    [Fact]\n    public void ToRoutingKey_Scim_ThrowsException()\n    {\n        Assert.Throws<ArgumentOutOfRangeException>(() => IntegrationType.Scim.ToRoutingKey());\n    }\n\n    [Fact]\n    public void ToRoutingKey_Slack_Succeeds()\n    {\n        Assert.Equal(\"slack\", IntegrationType.Slack.ToRoutingKey());\n    }\n\n    [Fact]\n    public void ToRoutingKey_Webhook_Succeeds()\n    {\n        Assert.Equal(\"webhook\", IntegrationType.Webhook.ToRoutingKey());\n    }\n\n    [Fact]\n    public void ToRoutingKey_Hec_Succeeds()\n    {\n        Assert.Equal(\"hec\", IntegrationType.Hec.ToRoutingKey());\n    }\n\n    [Fact]\n    public void ToRoutingKey_Datadog_Succeeds()\n    {\n        Assert.Equal(\"datadog\", IntegrationType.Datadog.ToRoutingKey());\n    }\n\n    [Fact]\n    public void ToRoutingKey_Teams_Succeeds()\n    {\n        Assert.Equal(\"teams\", IntegrationType.Teams.ToRoutingKey());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Services/OrganizationDomainServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;\nusing Bit.Core.AdminConsole.Services.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Services;\n\n[SutProviderCustomize]\npublic class OrganizationDomainServiceTests\n{\n\n    [Theory, BitAutoData]\n    public async Task ValidateOrganizationsDomainAsync_CallsDnsResolverServiceAndReplace(SutProvider<OrganizationDomainService> sutProvider)\n    {\n        var domains = new List<OrganizationDomain>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = Guid.NewGuid(),\n                CreationDate = DateTime.UtcNow,\n                DomainName = \"test.com\",\n                Txt = \"btw+12345\",\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = Guid.NewGuid(),\n                CreationDate = DateTime.UtcNow,\n                DomainName = \"test2.com\",\n                Txt = \"btw+6789\"\n            }\n        };\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>().GetManyByNextRunDateAsync(default)\n            .ReturnsForAnyArgs(domains);\n\n        await sutProvider.Sut.ValidateOrganizationsDomainAsync();\n\n        await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().ReceivedWithAnyArgs(2)\n            .SystemVerifyOrganizationDomainAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task OrganizationDomainMaintenanceAsync_CallsDeleteExpiredAsync_WhenExpiredDomainsExist(\n        SutProvider<OrganizationDomainService> sutProvider)\n    {\n        var expiredDomains = new List<OrganizationDomain>\n        {\n            new()\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = Guid.NewGuid(),\n                CreationDate = DateTime.UtcNow,\n                DomainName = \"test.com\",\n                Txt = \"btw+12345\",\n            },\n            new()\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = Guid.NewGuid(),\n                CreationDate = DateTime.UtcNow,\n                DomainName = \"test2.com\",\n                Txt = \"btw+6789\"\n            }\n        };\n        sutProvider.GetDependency<IOrganizationDomainRepository>().GetExpiredOrganizationDomainsAsync()\n            .Returns(expiredDomains);\n\n        await sutProvider.Sut.OrganizationDomainMaintenanceAsync();\n\n        await sutProvider.GetDependency<IOrganizationDomainRepository>().ReceivedWithAnyArgs(1)\n            .DeleteExpiredAsync(7);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Models.Business;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Test.AutoFixture.OrganizationUserFixtures;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Fakes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\nusing Organization = Bit.Core.AdminConsole.Entities.Organization;\nusing OrganizationUser = Bit.Core.Entities.OrganizationUser;\nusing OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite;\n\nnamespace Bit.Core.Test.Services;\n\n[SutProviderCustomize]\npublic class OrganizationServiceTests\n{\n    private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();\n\n    [Theory]\n    [OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,\n         InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_NoEmails_Throws(Organization organization, OrganizationUser invitor,\n        OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)\n    {\n        invite.Emails = null;\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize, OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_DuplicateEmails_PassesWithoutDuplicates(Organization organization, OrganizationUser invitor,\n                [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,\n        OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)\n    {\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        invite.Emails = invite.Emails.Append(invite.Emails.First());\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)\n            .Returns(new[] { owner });\n\n        // Must set guids in order for dictionary of guids to not throw aggregate exceptions\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n\n        await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)\n            .SendInvitesAsync(Arg.Is<SendInvitesRequest>(request =>\n                request.Users.DistinctBy(x => x.Email).Count() == invite.Emails.Distinct().Count() &&\n                request.Organization == organization));\n\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.Admin,\n        InvitorUserType = OrganizationUserType.Owner\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_NoOwner_Throws(Organization organization, OrganizationUser invitor,\n        OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));\n        Assert.Contains(\"Organization must have at least one confirmed owner.\", exception.Message);\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.Owner,\n        InvitorUserType = OrganizationUserType.Admin\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_NonOwnerConfiguringOwner_Throws(Organization organization, OrganizationUserInvite invite,\n        OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)\n    {\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        currentContext.OrganizationAdmin(organization.Id).Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));\n        Assert.Contains(\"only an owner\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.Custom,\n        InvitorUserType = OrganizationUserType.User\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_NonAdminConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,\n        OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.UseCustomPermissions = true;\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        currentContext.OrganizationUser(organization.Id).Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));\n        Assert.Contains(\"your account does not have permission to manage users\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n         InviteeUserType = OrganizationUserType.Custom,\n         InvitorUserType = OrganizationUserType.Admin\n     ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_WithCustomType_WhenUseCustomPermissionsIsFalse_Throws(Organization organization, OrganizationUserInvite invite,\n        OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.UseCustomPermissions = false;\n\n        invite.Permissions = null;\n        invitor.Status = OrganizationUserStatusType.Confirmed;\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)\n            .Returns(new[] { invitor });\n        currentContext.OrganizationOwner(organization.Id).Returns(true);\n        currentContext.ManageUsers(organization.Id).Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));\n        Assert.Contains(\"to enable custom permissions\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n         InviteeUserType = OrganizationUserType.Custom,\n         InvitorUserType = OrganizationUserType.Admin\n     ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_WithCustomType_WhenUseCustomPermissionsIsTrue_Passes(Organization organization, OrganizationUserInvite invite,\n        OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.Seats = 10;\n        organization.UseCustomPermissions = true;\n\n        invite.Permissions = null;\n        invitor.Status = OrganizationUserStatusType.Confirmed;\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n\n        currentContext.OrganizationOwner(organization.Id).Returns(true);\n        currentContext.ManageUsers(organization.Id).Returns(true);\n\n        await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.User)]\n    public async Task InviteUsers_WithNonCustomType_WhenUseCustomPermissionsIsFalse_Passes(OrganizationUserType inviteUserType, Organization organization, OrganizationUserInvite invite,\n        OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.Seats = 10;\n        organization.UseCustomPermissions = false;\n\n        invite.Type = inviteUserType;\n        invite.Permissions = null;\n        invitor.Status = OrganizationUserStatusType.Confirmed;\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n\n        currentContext.OrganizationOwner(organization.Id).Returns(true);\n        currentContext.ManageUsers(organization.Id).Returns(true);\n\n        await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.User,\n        InvitorUserType = OrganizationUserType.Custom\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_CustomUserWithoutManageUsersConfiguringUser_Throws(Organization organization, OrganizationUserInvite invite,\n        OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)\n    {\n        invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = false },\n            new JsonSerializerOptions\n            {\n                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n            });\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n        currentContext.OrganizationCustom(organization.Id).Returns(true);\n        currentContext.ManageUsers(organization.Id).Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));\n        Assert.Contains(\"account does not have permission\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.Admin,\n        InvitorUserType = OrganizationUserType.Custom\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_CustomUserConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,\n        OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)\n    {\n        invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },\n            new JsonSerializerOptions\n            {\n                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n            });\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        currentContext.OrganizationCustom(organization.Id).Returns(true);\n        currentContext.ManageUsers(organization.Id).Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }));\n        Assert.Contains(\"can not manage admins\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.User,\n        InvitorUserType = OrganizationUserType.Owner\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_NoPermissionsObject_Passes(Organization organization, OrganizationUserInvite invite,\n        OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)\n    {\n        invite.Permissions = null;\n        invitor.Status = OrganizationUserStatusType.Confirmed;\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n\n        currentContext.OrganizationOwner(organization.Id).Returns(true);\n        currentContext.ManageUsers(organization.Id).Returns(true);\n\n        await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) });\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.User,\n        InvitorUserType = OrganizationUserType.Custom\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUser_Passes(Organization organization, OrganizationUserInvite invite, string externalId,\n        OrganizationUser invitor,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        // This method is only used to invite 1 user at a time\n        invite.Emails = new[] { invite.Emails.First() };\n\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        InviteUser_ArrangeCurrentContextPermissions(organization, sutProvider);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        // Mock tokenable factory to return a token that expires in 5 days\n        // sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()\n        //     .CreateToken(Arg.Any<OrganizationUser>())\n        //     .Returns(\n        //         info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())\n        //         {\n        //             ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))\n        //         }\n        //     );\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n        SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        await sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId);\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)\n            .SendInvitesAsync(Arg.Is<SendInvitesRequest>(request =>\n                request.Users.Length == 1 &&\n                request.Organization == organization));\n\n        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.User,\n        InvitorUserType = OrganizationUserType.Custom\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUser_InvitingMoreThanOneUser_Throws(Organization organization, OrganizationUserInvite invite, string externalId,\n        OrganizationUser invitor,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId));\n        Assert.Contains(\"This method can only be used to invite a single user.\", exception.Message);\n\n        await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs()\n            .SendOrganizationInviteEmailsAsync(default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceive()\n            .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>());\n        await sutProvider.GetDependency<IEventService>().DidNotReceive()\n            .LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.User,\n        InvitorUserType = OrganizationUserType.Custom\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUser_UserAlreadyInvited_Throws(Organization organization, OrganizationUserInvite invite, string externalId,\n        OrganizationUser invitor,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        // This method is only used to invite 1 user at a time\n        invite.Emails = new[] { invite.Emails.First() };\n\n        // The user has already been invited\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .SelectKnownEmailsAsync(organization.Id, Arg.Any<IEnumerable<string>>(), false)\n            .Returns(new List<string> { invite.Emails.First() });\n\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        InviteUser_ArrangeCurrentContextPermissions(organization, sutProvider);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n        SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut\n            .InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId));\n        Assert.Contains(\"This user has already been invited\", exception.Message);\n\n        // SendOrganizationInvitesCommand and EventService are still called, but with no OrgUsers\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)\n            .SendInvitesAsync(Arg.Is<SendInvitesRequest>(info =>\n                info.Organization == organization &&\n                info.Users.Length == 0));\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(events => !events.Any()));\n    }\n\n    private void InviteUser_ArrangeCurrentContextPermissions(Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n        currentContext.ManageUsers(organization.Id).Returns(true);\n        currentContext.AccessReports(organization.Id).Returns(true);\n        currentContext.ManageGroups(organization.Id).Returns(true);\n        currentContext.ManagePolicies(organization.Id).Returns(true);\n        currentContext.ManageScim(organization.Id).Returns(true);\n        currentContext.ManageSso(organization.Id).Returns(true);\n        currentContext.AccessEventLogs(organization.Id).Returns(true);\n        currentContext.AccessImportExport(organization.Id).Returns(true);\n        currentContext.EditAnyCollection(organization.Id).Returns(true);\n        currentContext.ManageResetPassword(organization.Id).Returns(true);\n        currentContext.GetOrganization(organization.Id)\n            .Returns(new CurrentContextOrganization()\n            {\n                Permissions = new Permissions\n                {\n                    CreateNewCollections = true,\n                    DeleteAnyCollection = true\n                }\n            });\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.User,\n        InvitorUserType = OrganizationUserType.Custom\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_Passes(Organization organization, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,\n        OrganizationUser invitor,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        InviteUser_ArrangeCurrentContextPermissions(organization, sutProvider);\n\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n        SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);\n\n        await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, invites);\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)\n            .SendInvitesAsync(Arg.Is<SendInvitesRequest>(info =>\n                info.Organization == organization &&\n                info.Users.Length == invites.SelectMany(x => x.invite.Emails).Distinct().Count()));\n\n        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.User,\n        InvitorUserType = OrganizationUserType.Custom\n    ), OrganizationCustomize, BitAutoData]\n    public async Task InviteUsers_WithEventSystemUser_Passes(Organization organization, EventSystemUser eventSystemUser, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,\n        OrganizationUser invitor,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks\n        sutProvider.SetDependency(_orgUserInviteTokenDataFactory, \"orgUserInviteTokenDataFactory\");\n        sutProvider.Create();\n\n        invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },\n            new JsonSerializerOptions\n            {\n                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n            });\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n\n        organizationRepository.GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()\n            .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any<IEnumerable<Guid>>())\n            .Returns(true);\n\n        SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n\n        currentContext.ManageUsers(organization.Id).Returns(true);\n\n        await sutProvider.Sut.InviteUsersAsync(organization.Id, invitingUserId: null, eventSystemUser, invites);\n\n        await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().Received(1)\n            .SendInvitesAsync(Arg.Is<SendInvitesRequest>(info =>\n                info.Users.Length == invites.SelectMany(i => i.invite.Emails).Count() &&\n                info.Organization == organization));\n\n        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>());\n    }\n\n    [Theory, BitAutoData, OrganizationCustomize, OrganizationInviteCustomize]\n    public async Task InviteUsers_WithSecretsManager_Passes(Organization organization,\n        IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,\n        OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n\n        // Set up some invites to grant access to SM\n        invites.First().invite.AccessSecretsManager = true;\n        var invitedSmUsers = invites.First().invite.Emails.Count();\n        foreach (var (invite, externalId) in invites.Skip(1))\n        {\n            invite.AccessSecretsManager = false;\n        }\n\n        // Assume we need to add seats for all invited SM users\n        sutProvider.GetDependency<ICountNewSmSeatsRequiredQuery>()\n            .CountNewSmSeatsRequiredAsync(organization.Id, invitedSmUsers).Returns(invitedSmUsers);\n\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n        SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);\n        SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));\n\n        await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites);\n\n        await sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>().Received(1)\n            .UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>\n                update.SmSeats == organization.SmSeats + invitedSmUsers &&\n                !update.SmServiceAccountsChanged &&\n                !update.MaxAutoscaleSmSeatsChanged &&\n                !update.MaxAutoscaleSmSeatsChanged));\n    }\n\n    [Theory, BitAutoData, OrganizationCustomize, OrganizationInviteCustomize]\n    public async Task InviteUsers_WithSecretsManager_WhenErrorIsThrown_RevertsAutoscaling(Organization organization,\n        IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,\n        OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)\n    {\n        var initialSmSeats = organization.SmSeats;\n        InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider);\n\n        // Set up some invites to grant access to SM\n        invites.First().invite.AccessSecretsManager = true;\n        var invitedSmUsers = invites.First().invite.Emails.Count();\n        foreach (var (invite, externalId) in invites.Skip(1))\n        {\n            invite.AccessSecretsManager = false;\n        }\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n\n        // Assume we need to add seats for all invited SM users\n        sutProvider.GetDependency<ICountNewSmSeatsRequiredQuery>()\n            .CountNewSmSeatsRequiredAsync(organization.Id, invitedSmUsers).Returns(invitedSmUsers);\n\n        // Mock SecretsManagerSubscriptionUpdateCommand to actually change the organization's subscription in memory\n        sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()\n            .UpdateSubscriptionAsync(Arg.Any<SecretsManagerSubscriptionUpdate>())\n            .ReturnsForAnyArgs(Task.FromResult(0)).AndDoes(x => organization.SmSeats += invitedSmUsers);\n\n        sutProvider.GetDependency<ISendOrganizationInvitesCommand>()\n            .SendInvitesAsync(Arg.Any<SendInvitesRequest>()).ThrowsAsync<Exception>();\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        await Assert.ThrowsAsync<AggregateException>(async () =>\n            await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites));\n\n        // OrgUser is reverted\n        // Note: we don't know what their guids are so comparing length is the best we can do\n        var invitedEmails = invites.SelectMany(i => i.invite.Emails);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).DeleteManyAsync(\n            Arg.Is<IEnumerable<Guid>>(ids => ids.Count() == invitedEmails.Count()));\n\n        Received.InOrder(() =>\n        {\n            // Initial autoscaling\n            sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()\n                .UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>\n                    update.SmSeats == initialSmSeats + invitedSmUsers &&\n                    !update.SmServiceAccountsChanged &&\n                    !update.MaxAutoscaleSmSeatsChanged &&\n                    !update.MaxAutoscaleSmSeatsChanged));\n\n            // Revert autoscaling\n            sutProvider.GetDependency<IUpdateSecretsManagerSubscriptionCommand>()\n                .UpdateSubscriptionAsync(Arg.Is<SecretsManagerSubscriptionUpdate>(update =>\n                    update.SmSeats == initialSmSeats &&\n                    !update.SmServiceAccountsChanged &&\n                    !update.MaxAutoscaleSmSeatsChanged &&\n                    !update.MaxAutoscaleSmSeatsChanged));\n        });\n    }\n\n    private void InviteUserHelper_ArrangeValidPermissions(Organization organization, OrganizationUser savingUser,\n    SutProvider<OrganizationService> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);\n    }\n\n    [Theory]\n    [PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually)]\n    [BitAutoData(\"Cannot set max seat autoscaling below seat count\", 1, 0, 2, 2)]\n    [BitAutoData(\"Cannot set max seat autoscaling below seat count\", 4, -1, 6, 6)]\n    public async Task Enterprise_UpdateMaxSeatAutoscaling_BadInputThrows(string expectedMessage,\n        int? maxAutoscaleSeats, int seatAdjustment, int? currentSeats, int? currentMaxAutoscaleSeats,\n        Organization organization, SutProvider<OrganizationService> sutProvider)\n        => await UpdateSubscription_BadInputThrows(expectedMessage, maxAutoscaleSeats, seatAdjustment, currentSeats,\n            currentMaxAutoscaleSeats, organization, sutProvider);\n    [Theory]\n    [FreeOrganizationCustomize]\n    [BitAutoData(\"Your plan does not allow seat autoscaling\", 10, 0, null, null)]\n    public async Task Free_UpdateMaxSeatAutoscaling_BadInputThrows(string expectedMessage,\n        int? maxAutoscaleSeats, int seatAdjustment, int? currentSeats, int? currentMaxAutoscaleSeats,\n        Organization organization, SutProvider<OrganizationService> sutProvider)\n        => await UpdateSubscription_BadInputThrows(expectedMessage, maxAutoscaleSeats, seatAdjustment, currentSeats,\n            currentMaxAutoscaleSeats, organization, sutProvider);\n\n    private async Task UpdateSubscription_BadInputThrows(string expectedMessage,\n        int? maxAutoscaleSeats, int seatAdjustment, int? currentSeats, int? currentMaxAutoscaleSeats,\n        Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.Seats = currentSeats;\n        organization.MaxAutoscaleSeats = currentMaxAutoscaleSeats;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id,\n            seatAdjustment, maxAutoscaleSeats));\n\n        Assert.Contains(expectedMessage, exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateSubscription_NoOrganization_Throws(Guid organizationId, SutProvider<OrganizationService> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns((Organization)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateSubscription(organizationId, 0, null));\n    }\n\n    [Theory, SecretsManagerOrganizationCustomize]\n    [BitAutoData(\"You cannot have more Secrets Manager seats than Password Manager seats.\", -1)]\n    public async Task UpdateSubscription_PmSeatAdjustmentLessThanSmSeats_Throws(string expectedMessage,\n        int seatAdjustment, Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.Seats = 100;\n        organization.SmSeats = 100;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, null));\n        Assert.Contains(expectedMessage, actual.Message);\n    }\n\n    [Theory, PaidOrganizationCustomize]\n    [BitAutoData(0, 100, null, true, \"\")]\n    [BitAutoData(0, 100, 100, true, \"\")]\n    [BitAutoData(0, null, 100, true, \"\")]\n    [BitAutoData(1, 100, null, true, \"\")]\n    [BitAutoData(1, 100, 100, false, \"Seat limit has been reached\")]\n    public async Task CanScaleAsync(int seatsToAdd, int? currentSeats, int? maxAutoscaleSeats,\n        bool expectedResult, string expectedFailureMessage, Organization organization,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        organization.Seats = currentSeats;\n        organization.MaxAutoscaleSeats = maxAutoscaleSeats;\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).ReturnsNull();\n\n        var (result, failureMessage) = await sutProvider.Sut.CanScaleAsync(organization, seatsToAdd);\n\n        if (expectedFailureMessage == string.Empty)\n        {\n            Assert.Empty(failureMessage);\n        }\n        else\n        {\n            Assert.Contains(expectedFailureMessage, failureMessage);\n        }\n        Assert.Equal(expectedResult, result);\n    }\n\n    [Theory, PaidOrganizationCustomize, BitAutoData]\n    public async Task CanScaleAsync_FailsOnSelfHosted(Organization organization,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);\n        var (result, failureMessage) = await sutProvider.Sut.CanScaleAsync(organization, 10);\n\n        Assert.False(result);\n        Assert.Contains(\"Cannot autoscale on self-hosted instance\", failureMessage);\n    }\n\n    [Theory, PaidOrganizationCustomize, BitAutoData]\n    public async Task CanScaleAsync_FailsOnResellerManagedOrganization(\n        Organization organization,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var provider = new Provider\n        {\n            Enabled = true,\n            Type = ProviderType.Reseller\n        };\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);\n\n        var (result, failureMessage) = await sutProvider.Sut.CanScaleAsync(organization, 10);\n\n        Assert.False(result);\n        Assert.Contains(\"Seat limit has been reached. Contact your provider to purchase additional seats.\", failureMessage);\n    }\n\n\n    [Theory]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsStarter)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    public void ValidateSecretsManagerPlan_ThrowsException_WhenNoSecretsManagerSeats(PlanType planType, SutProvider<OrganizationService> sutProvider)\n    {\n        var plan = MockPlans.Get(planType);\n        var signup = new OrganizationUpgrade\n        {\n            UseSecretsManager = true,\n            AdditionalSmSeats = 0,\n            AdditionalServiceAccounts = 5,\n            AdditionalSeats = 2\n        };\n\n        var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));\n        Assert.Contains(\"You do not have any Secrets Manager seats!\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Free)]\n    public void ValidateSecretsManagerPlan_ThrowsException_WhenSubtractingSeats(PlanType planType, SutProvider<OrganizationService> sutProvider)\n    {\n        var plan = MockPlans.Get(planType);\n        var signup = new OrganizationUpgrade\n        {\n            UseSecretsManager = true,\n            AdditionalSmSeats = -1,\n            AdditionalServiceAccounts = 5\n        };\n        var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));\n        Assert.Contains(\"You can't subtract Secrets Manager seats!\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Free)]\n    public void ValidateSecretsManagerPlan_ThrowsException_WhenPlanDoesNotAllowAdditionalServiceAccounts(\n        PlanType planType,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var plan = MockPlans.Get(planType);\n        var signup = new OrganizationUpgrade\n        {\n            UseSecretsManager = true,\n            AdditionalSmSeats = 2,\n            AdditionalServiceAccounts = 5,\n            AdditionalSeats = 3\n        };\n        var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));\n        Assert.Contains(\"Plan does not allow additional Machine Accounts.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    public void ValidateSecretsManagerPlan_ThrowsException_WhenMoreSeatsThanPasswordManagerSeats(PlanType planType, SutProvider<OrganizationService> sutProvider)\n    {\n        var plan = MockPlans.Get(planType);\n        var signup = new OrganizationUpgrade\n        {\n            UseSecretsManager = true,\n            AdditionalSmSeats = 4,\n            AdditionalServiceAccounts = 5,\n            AdditionalSeats = 3\n        };\n        var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));\n        Assert.Contains(\"You cannot have more Secrets Manager seats than Password Manager seats.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsStarter)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    public void ValidateSecretsManagerPlan_ThrowsException_WhenSubtractingServiceAccounts(\n        PlanType planType,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var plan = MockPlans.Get(planType);\n        var signup = new OrganizationUpgrade\n        {\n            UseSecretsManager = true,\n            AdditionalSmSeats = 4,\n            AdditionalServiceAccounts = -5,\n            AdditionalSeats = 5\n        };\n        var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));\n        Assert.Contains(\"You can't subtract Machine Accounts!\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Free)]\n    public void ValidateSecretsManagerPlan_ThrowsException_WhenPlanDoesNotAllowAdditionalUsers(\n        PlanType planType,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var plan = MockPlans.Get(planType);\n        var signup = new OrganizationUpgrade\n        {\n            UseSecretsManager = true,\n            AdditionalSmSeats = 2,\n            AdditionalServiceAccounts = 0,\n            AdditionalSeats = 5\n        };\n        var exception = Assert.Throws<BadRequestException>(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup));\n        Assert.Contains(\"Plan does not allow additional users.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsStarter)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    public void ValidateSecretsManagerPlan_ValidPlan_NoExceptionThrown(\n        PlanType planType,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var plan = MockPlans.Get(planType);\n        var signup = new OrganizationUpgrade\n        {\n            UseSecretsManager = true,\n            AdditionalSmSeats = 2,\n            AdditionalServiceAccounts = 0,\n            AdditionalSeats = 4\n        };\n\n        sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup);\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n         InviteeUserType = OrganizationUserType.Custom,\n         InvitorUserType = OrganizationUserType.Custom\n     ), BitAutoData]\n    public async Task ValidateOrganizationUserUpdatePermissions_WithCustomPermission_WhenSavingUserHasCustomPermission_Passes(\n        CurrentContextOrganization organization,\n        OrganizationUserInvite organizationUserInvite,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var invitePermissions = new Permissions { AccessReports = true };\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(organization.Id).Returns(true);\n\n        await sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organization.Id, organizationUserInvite.Type.Value, null, invitePermissions);\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n         InviteeUserType = OrganizationUserType.Owner,\n         InvitorUserType = OrganizationUserType.Admin\n     ), BitAutoData]\n    public async Task ValidateOrganizationUserUpdatePermissions_WithAdminAddingOwner_Throws(\n        Guid organizationId,\n        OrganizationUserInvite organizationUserInvite,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions));\n\n        Assert.Contains(\"only an owner can configure another owner's account.\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n        InviteeUserType = OrganizationUserType.Admin,\n        InvitorUserType = OrganizationUserType.Owner\n    ), BitAutoData]\n    public async Task ValidateOrganizationUserUpdatePermissions_WithoutManageUsersPermission_Throws(\n        Guid organizationId,\n        OrganizationUserInvite organizationUserInvite,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions));\n\n        Assert.Contains(\"your account does not have permission to manage users.\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n         InviteeUserType = OrganizationUserType.Admin,\n         InvitorUserType = OrganizationUserType.Custom\n     ), BitAutoData]\n    public async Task ValidateOrganizationUserUpdatePermissions_WithCustomAddingAdmin_Throws(\n        Guid organizationId,\n        OrganizationUserInvite organizationUserInvite,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions));\n\n        Assert.Contains(\"custom users can not manage admins or owners.\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory]\n    [OrganizationInviteCustomize(\n         InviteeUserType = OrganizationUserType.Custom,\n         InvitorUserType = OrganizationUserType.Custom\n     ), BitAutoData]\n    public async Task ValidateOrganizationUserUpdatePermissions_WithCustomAddingUser_WithoutPermissions_Throws(\n        Guid organizationId,\n        OrganizationUserInvite organizationUserInvite,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        var invitePermissions = new Permissions { AccessReports = true };\n        sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(organizationId).Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, invitePermissions));\n\n        Assert.Contains(\"custom users can only grant the same custom permissions that they have.\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Owner)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.User)]\n    public async Task ValidateOrganizationCustomPermissionsEnabledAsync_WithNotCustomType_IsValid(\n        OrganizationUserType newType,\n        Guid organizationId,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        await sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organizationId, newType);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateOrganizationCustomPermissionsEnabledAsync_NotExistingOrg_ThrowsNotFound(\n        Guid organizationId,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organizationId, OrganizationUserType.Custom));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateOrganizationCustomPermissionsEnabledAsync_WithUseCustomPermissionsDisabled_ThrowsBadRequest(\n        Organization organization,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        organization.UseCustomPermissions = false;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organization.Id, OrganizationUserType.Custom));\n\n        Assert.Contains(\"to enable custom permissions\", exception.Message.ToLowerInvariant());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateOrganizationCustomPermissionsEnabledAsync_WithUseCustomPermissionsEnabled_IsValid(\n        Organization organization,\n        SutProvider<OrganizationService> sutProvider)\n    {\n        organization.UseCustomPermissions = true;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        await sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organization.Id, OrganizationUserType.Custom);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WhenValidOrganization_AndUpdateBillingIsTrue_UpdateStripeCustomerAndOrganization(Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var eventService = sutProvider.GetDependency<IEventService>();\n\n        var requestOptionsReturned = new CustomerUpdateOptions\n        {\n            Email = organization.BillingEmail,\n            Description = organization.DisplayBusinessName(),\n            InvoiceSettings = new CustomerInvoiceSettingsOptions\n            {\n                // This overwrites the existing custom fields for this organization\n                CustomFields =\n                [\n                    new CustomerInvoiceSettingsCustomFieldOptions\n                    {\n                        Name = organization.SubscriberType(),\n                        Value = organization.DisplayName()[..30]\n                    }\n                ]\n            },\n        };\n        organizationRepository\n            .GetByIdentifierAsync(organization.Identifier!)\n            .Returns(organization);\n\n        // Act\n        await sutProvider.Sut.UpdateAsync(organization, updateBilling: true);\n\n        // Assert\n        await organizationRepository\n            .Received(1)\n            .GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));\n        await stripeAdapter\n            .Received(1)\n            .UpdateCustomerAsync(\n                Arg.Is<string>(id => id == organization.GatewayCustomerId),\n                Arg.Is<CustomerUpdateOptions>(options => options.Email == requestOptionsReturned.Email\n                                                         && options.Description == requestOptionsReturned.Description\n                                                         && options.InvoiceSettings.CustomFields.First().Name == requestOptionsReturned.InvoiceSettings.CustomFields.First().Name\n                                                         && options.InvoiceSettings.CustomFields.First().Value == requestOptionsReturned.InvoiceSettings.CustomFields.First().Value)); ;\n        await organizationRepository\n            .Received(1)\n            .ReplaceAsync(Arg.Is<Organization>(org => org == organization));\n        await applicationCacheService\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(Arg.Is<Organization>(org => org == organization));\n        await eventService\n            .Received(1)\n            .LogOrganizationEventAsync(Arg.Is<Organization>(org => org == organization),\n                Arg.Is<EventType>(e => e == EventType.Organization_Updated));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WhenValidOrganization_AndUpdateBillingIsFalse_UpdateOrganization(Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var eventService = sutProvider.GetDependency<IEventService>();\n\n        organizationRepository\n            .GetByIdentifierAsync(organization.Identifier!)\n            .Returns(organization);\n\n        // Act\n        await sutProvider.Sut.UpdateAsync(organization, updateBilling: false);\n\n        // Assert\n        await organizationRepository\n            .Received(1)\n            .GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));\n        await stripeAdapter\n            .DidNotReceiveWithAnyArgs()\n            .UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());\n        await organizationRepository\n            .Received(1)\n            .ReplaceAsync(Arg.Is<Organization>(org => org == organization));\n        await applicationCacheService\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(Arg.Is<Organization>(org => org == organization));\n        await eventService\n            .Received(1)\n            .LogOrganizationEventAsync(Arg.Is<Organization>(org => org == organization),\n                Arg.Is<EventType>(e => e == EventType.Organization_Updated));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WhenOrganizationHasNoId_ThrowsApplicationException(Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        // Arrange\n        organization.Id = Guid.Empty;\n\n        // Act/Assert\n        var exception = await Assert.ThrowsAnyAsync<ApplicationException>(() => sutProvider.Sut.UpdateAsync(organization));\n        Assert.Equal(\"Cannot create org this way. Call SignUpAsync.\", exception.Message);\n\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WhenIdentifierAlreadyExistsForADifferentOrganization_ThrowsBadRequestException(Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        // Arrange\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n        var differentOrganization = new Organization { Id = Guid.NewGuid() };\n\n        organizationRepository\n            .GetByIdentifierAsync(organization.Identifier!)\n            .Returns(differentOrganization);\n\n        // Act/Assert\n        var exception = await Assert.ThrowsAnyAsync<BadRequestException>(() => sutProvider.Sut.UpdateAsync(organization));\n        Assert.Equal(\"Identifier already in use by another organization.\", exception.Message);\n\n        await organizationRepository\n            .Received(1)\n            .GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));\n    }\n\n    [Theory]\n    [BitAutoData(false, true, false, true)]\n    [BitAutoData(true, false, true, false)]\n    public async Task UpdateCollectionManagementSettingsAsync_WhenSettingsChanged_LogsSpecificEvents(\n        bool newLimitCollectionCreation,\n        bool newLimitCollectionDeletion,\n        bool newLimitItemDeletion,\n        bool newAllowAdminAccessToAllCollectionItems,\n        Organization existingOrganization, SutProvider<OrganizationService> sutProvider)\n    {\n        // Arrange\n        existingOrganization.LimitCollectionCreation = false;\n        existingOrganization.LimitCollectionDeletion = false;\n        existingOrganization.LimitItemDeletion = false;\n        existingOrganization.AllowAdminAccessToAllCollectionItems = false;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(existingOrganization.Id)\n            .Returns(existingOrganization);\n\n        var settings = new OrganizationCollectionManagementSettings\n        {\n            LimitCollectionCreation = newLimitCollectionCreation,\n            LimitCollectionDeletion = newLimitCollectionDeletion,\n            LimitItemDeletion = newLimitItemDeletion,\n            AllowAdminAccessToAllCollectionItems = newAllowAdminAccessToAllCollectionItems\n        };\n\n        // Act\n        await sutProvider.Sut.UpdateCollectionManagementSettingsAsync(existingOrganization.Id, settings);\n\n        // Assert\n        var eventService = sutProvider.GetDependency<IEventService>();\n        if (newLimitCollectionCreation)\n        {\n            await eventService.Received(1).LogOrganizationEventAsync(\n                Arg.Is<Organization>(org => org.Id == existingOrganization.Id),\n                Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled));\n        }\n        else\n        {\n            await eventService.DidNotReceive().LogOrganizationEventAsync(\n                Arg.Is<Organization>(org => org.Id == existingOrganization.Id),\n                Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled));\n        }\n\n        if (newLimitCollectionDeletion)\n        {\n            await eventService.Received(1).LogOrganizationEventAsync(\n                Arg.Is<Organization>(org => org.Id == existingOrganization.Id),\n                Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled));\n        }\n        else\n        {\n            await eventService.DidNotReceive().LogOrganizationEventAsync(\n                Arg.Is<Organization>(org => org.Id == existingOrganization.Id),\n                Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled));\n        }\n\n        if (newLimitItemDeletion)\n        {\n            await eventService.Received(1).LogOrganizationEventAsync(\n                Arg.Is<Organization>(org => org.Id == existingOrganization.Id),\n                Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled));\n        }\n        else\n        {\n            await eventService.DidNotReceive().LogOrganizationEventAsync(\n                Arg.Is<Organization>(org => org.Id == existingOrganization.Id),\n                Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled));\n        }\n\n        if (newAllowAdminAccessToAllCollectionItems)\n        {\n            await eventService.Received(1).LogOrganizationEventAsync(\n                Arg.Is<Organization>(org => org.Id == existingOrganization.Id),\n                Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled));\n        }\n        else\n        {\n            await eventService.DidNotReceive().LogOrganizationEventAsync(\n                Arg.Is<Organization>(org => org.Id == existingOrganization.Id),\n                Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled));\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateCollectionManagementSettingsAsync_WhenOrganizationNotFound_ThrowsNotFoundException(\n        Guid organizationId, OrganizationCollectionManagementSettings settings, SutProvider<OrganizationService> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organizationId)\n            .Returns((Organization)null);\n\n        // Act/Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateCollectionManagementSettingsAsync(organizationId, settings));\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .GetByIdAsync(organizationId);\n    }\n\n    [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually), BitAutoData]\n    public async Task AdjustSeatsAsync_WithFeatureFlag_UsesUpdateOrganizationSubscriptionCommand(\n        Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.Seats = 20;\n        organization.GatewayCustomerId = \"cus_123\";\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.UseSecretsManager = false;\n\n        var plan = MockPlans.Get(organization.PlanType);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(plan);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        BillingCommandResult<Subscription> successResult = new Subscription();\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(organization, Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(successResult);\n\n        await sutProvider.Sut.AdjustSeatsAsync(organization.Id, 2);\n\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>().Received(1)\n            .Run(organization, Arg.Any<OrganizationSubscriptionChangeSet>());\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .AdjustSeatsAsync(default, default, default);\n    }\n\n    [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually), BitAutoData]\n    public async Task AdjustSeatsAsync_WithoutFeatureFlag_UsesPaymentService(\n        Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.Seats = 20;\n        organization.GatewayCustomerId = \"cus_123\";\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.UseSecretsManager = false;\n\n        var plan = MockPlans.Get(organization.PlanType);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(plan);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(false);\n\n        await sutProvider.Sut.AdjustSeatsAsync(organization.Id, 2);\n\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .AdjustSeatsAsync(organization, plan, Arg.Any<int>());\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>().DidNotReceiveWithAnyArgs()\n            .Run(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AdjustStorageAsync_OrganizationNotFound_ThrowsNotFoundException(\n        Guid organizationId, SutProvider<OrganizationService> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns((Organization)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.AdjustStorageAsync(organizationId, 1));\n    }\n\n    [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]\n    public async Task AdjustStorageAsync_PlanDoesNotAllowStorage_ThrowsBadRequestException(\n        Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        var plan = MockPlans.Get(organization.PlanType);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AdjustStorageAsync(organization.Id, 1));\n    }\n\n    [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually), BitAutoData]\n    public async Task AdjustStorageAsync_WithFeatureFlag_AtBaseStorage_AddsItem(\n        Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.GatewayCustomerId = \"cus_123\";\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.MaxStorageGb = 1;\n        organization.Storage = 0;\n\n        var plan = MockPlans.Get(organization.PlanType);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(plan);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        BillingCommandResult<Subscription> successResult = new Subscription();\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(organization, Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(successResult);\n\n        await sutProvider.Sut.AdjustStorageAsync(organization.Id, 1);\n\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>().Received(1)\n            .Run(organization, Arg.Is<OrganizationSubscriptionChangeSet>(cs =>\n                cs.Changes.Count == 1 && cs.Changes[0].IsItemAddition));\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .AdjustStorageAsync(default, default, default);\n        Assert.Equal((short)2, organization.MaxStorageGb);\n    }\n\n    [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually), BitAutoData]\n    public async Task AdjustStorageAsync_WithFeatureFlag_AboveBaseStorage_UpdatesItemQuantity(\n        Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.GatewayCustomerId = \"cus_123\";\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.MaxStorageGb = 2;\n        organization.Storage = 0;\n\n        var plan = MockPlans.Get(organization.PlanType);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(plan);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        BillingCommandResult<Subscription> successResult = new Subscription();\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(organization, Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(successResult);\n\n        await sutProvider.Sut.AdjustStorageAsync(organization.Id, 1);\n\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>().Received(1)\n            .Run(organization, Arg.Is<OrganizationSubscriptionChangeSet>(cs =>\n                cs.Changes.Count == 1 && cs.Changes[0].IsItemQuantityUpdate));\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .AdjustStorageAsync(default, default, default);\n        Assert.Equal((short)3, organization.MaxStorageGb);\n    }\n\n    [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually), BitAutoData]\n    public async Task AdjustStorageAsync_WithoutFeatureFlag_UsesPaymentService(\n        Organization organization, SutProvider<OrganizationService> sutProvider)\n    {\n        organization.GatewayCustomerId = \"cus_123\";\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.MaxStorageGb = 1;\n        organization.Storage = 0;\n\n        var plan = MockPlans.Get(organization.PlanType);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(plan);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(false);\n\n        await sutProvider.Sut.AdjustStorageAsync(organization.Id, 1);\n\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .AdjustStorageAsync(organization, Arg.Any<int>(), plan.PasswordManager.StripeStoragePlanId);\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>().DidNotReceiveWithAnyArgs()\n            .Run(default, default);\n    }\n\n    // Must set real guids in order for dictionary of guids to not throw aggregate exceptions\n    private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository)\n    {\n        organizationUserRepository.CreateManyAsync(Arg.Any<IEnumerable<OrganizationUser>>()).Returns(\n            info =>\n            {\n                var orgUsers = info.Arg<IEnumerable<OrganizationUser>>();\n                foreach (var orgUser in orgUsers)\n                {\n                    orgUser.Id = Guid.NewGuid();\n                }\n\n                return Task.FromResult<ICollection<Guid>>(orgUsers.Select(u => u.Id).ToList());\n            }\n        );\n\n        organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>(), Arg.Any<IEnumerable<CollectionAccessSelection>>()).Returns(\n            info =>\n            {\n                var orgUser = info.Arg<OrganizationUser>();\n                orgUser.Id = Guid.NewGuid();\n                return Task.FromResult<Guid>(orgUser.Id);\n            }\n        );\n    }\n\n    // Must set real guids in order for dictionary of guids to not throw aggregate exceptions\n    private void SetupOrgUserRepositoryCreateAsyncMock(IOrganizationUserRepository organizationUserRepository)\n    {\n        organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>(),\n            Arg.Any<IEnumerable<CollectionAccessSelection>>()).Returns(\n            info =>\n            {\n                var orgUser = info.Arg<OrganizationUser>();\n                orgUser.Id = Guid.NewGuid();\n                return Task.FromResult<Guid>(orgUser.Id);\n            }\n        );\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.AdminConsole.Services.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.AdminConsole.Services;\n\n[SutProviderCustomize]\npublic class PolicyServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider<PolicyService> sutProvider)\n    {\n        SetupUserPolicies(userId, sutProvider);\n\n        var result = await sutProvider.Sut\n            .GetPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso);\n\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsOnePolicy(Guid userId, SutProvider<PolicyService> sutProvider)\n    {\n        SetupUserPolicies(userId, sutProvider);\n\n        sutProvider.GetDependency<GlobalSettings>().Sso.EnforceSsoPolicyForAllUsers.Returns(true);\n\n        var result = await sutProvider.Sut\n            .GetPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso);\n\n        Assert.Single(result);\n        Assert.True(result.All(details => details.PolicyEnabled));\n        Assert.True(result.All(details => details.PolicyType == PolicyType.RequireSso));\n        Assert.True(result.All(details => details.OrganizationUserType == OrganizationUserType.Owner));\n        Assert.True(result.All(details => details.OrganizationUserStatus == OrganizationUserStatusType.Confirmed));\n        Assert.True(result.All(details => !details.IsProvider));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPoliciesApplicableToUserAsync_WithDisableTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsNoPolicies(Guid userId, SutProvider<PolicyService> sutProvider)\n    {\n        SetupUserPolicies(userId, sutProvider);\n\n        var result = await sutProvider.Sut\n            .GetPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend);\n\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPoliciesApplicableToUserAsync_WithDisableSendTypeFilter_WithInvitedUserStatusFilter_ReturnsOnePolicy(Guid userId, SutProvider<PolicyService> sutProvider)\n    {\n        SetupUserPolicies(userId, sutProvider);\n\n        var result = await sutProvider.Sut\n            .GetPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend, OrganizationUserStatusType.Invited);\n\n        Assert.Single(result);\n        Assert.True(result.All(details => details.PolicyEnabled));\n        Assert.True(result.All(details => details.PolicyType == PolicyType.DisableSend));\n        Assert.True(result.All(details => details.OrganizationUserType == OrganizationUserType.User));\n        Assert.True(result.All(details => details.OrganizationUserStatus == OrganizationUserStatusType.Invited));\n        Assert.True(result.All(details => !details.IsProvider));\n    }\n\n    [Theory, BitAutoData]\n    public async Task AnyPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsFalse(Guid userId, SutProvider<PolicyService> sutProvider)\n    {\n        SetupUserPolicies(userId, sutProvider);\n\n        var result = await sutProvider.Sut\n            .AnyPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso);\n\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AnyPoliciesApplicableToUserAsync_WithRequireSsoTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsTrue(Guid userId, SutProvider<PolicyService> sutProvider)\n    {\n        SetupUserPolicies(userId, sutProvider);\n\n        sutProvider.GetDependency<GlobalSettings>().Sso.EnforceSsoPolicyForAllUsers.Returns(true);\n\n        var result = await sutProvider.Sut\n            .AnyPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso);\n\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AnyPoliciesApplicableToUserAsync_WithDisableTypeFilter_WithDefaultOrganizationUserStatusFilter_ReturnsFalse(Guid userId, SutProvider<PolicyService> sutProvider)\n    {\n        SetupUserPolicies(userId, sutProvider);\n\n        var result = await sutProvider.Sut\n            .AnyPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend);\n\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AnyPoliciesApplicableToUserAsync_WithDisableSendTypeFilter_WithInvitedUserStatusFilter_ReturnsTrue(Guid userId, SutProvider<PolicyService> sutProvider)\n    {\n        SetupUserPolicies(userId, sutProvider);\n\n        var result = await sutProvider.Sut\n            .AnyPoliciesApplicableToUserAsync(userId, PolicyType.DisableSend, OrganizationUserStatusType.Invited);\n\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetMasterPasswordPolicyForUserAsync_ReturnsEnforcedOptions(User user, SutProvider<PolicyService> sutProvider)\n    {\n        // Arrange: Create three policies with different requirements to test combining behavior\n        var policy1 = new Policy\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            Type = PolicyType.MasterPassword,\n            Enabled = true\n        };\n        policy1.SetDataModel(new MasterPasswordPolicyData\n        {\n            MinComplexity = 3,\n            MinLength = 12,\n            RequireLower = true,\n            RequireUpper = false,\n            RequireNumbers = true,\n            RequireSpecial = false,\n            EnforceOnLogin = true\n        });\n\n        var policy2 = new Policy\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            Type = PolicyType.MasterPassword,\n            Enabled = true\n        };\n        policy2.SetDataModel(new MasterPasswordPolicyData\n        {\n            MinComplexity = 4,\n            MinLength = 10,\n            RequireLower = false,\n            RequireUpper = true,\n            RequireNumbers = false,\n            RequireSpecial = true,\n            EnforceOnLogin = false\n        });\n\n        var policy3 = new Policy\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            Type = PolicyType.MasterPassword,\n            Enabled = true\n        };\n        policy3.SetDataModel(new MasterPasswordPolicyData\n        {\n            MinComplexity = 2,\n            MinLength = 15,\n            RequireLower = false,\n            RequireUpper = false,\n            RequireNumbers = false,\n            RequireSpecial = false,\n            EnforceOnLogin = false\n        });\n\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns([policy1, policy2, policy3]);\n\n        // Act\n        var result = await sutProvider.Sut.GetMasterPasswordPolicyForUserAsync(user);\n\n        // Assert: Verify that policies were combined correctly\n        Assert.NotNull(result);\n\n        // MinComplexity and MinLength should take the highest values\n        Assert.Equal(4, result.MinComplexity); // highest from policy2\n        Assert.Equal(15, result.MinLength); // highest from policy3\n\n        // Boolean flags should use OR logic (true if any policy has true)\n        Assert.True(result.RequireLower); // true from policy1\n        Assert.True(result.RequireUpper); // true from policy2\n        Assert.True(result.RequireNumbers); // true from policy1\n        Assert.True(result.RequireSpecial); // true from policy2\n        Assert.True(result.EnforceOnLogin); // true from policy1\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetMasterPasswordPolicyForUserAsync_WithNoPolicies_ReturnsNull(User user, SutProvider<PolicyService> sutProvider)\n    {\n        // Arrange: No enabled policies\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns(new List<Policy>());\n\n        // Act\n        var result = await sutProvider.Sut.GetMasterPasswordPolicyForUserAsync(user);\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetMasterPasswordPolicyForUserAsync_WithDisabledPolicies_ReturnsNull(User user, SutProvider<PolicyService> sutProvider)\n    {\n        // Arrange: Policies exist but are disabled\n        var disabledPolicy = new Policy\n        {\n            Id = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            Type = PolicyType.MasterPassword,\n            Enabled = false\n        };\n        disabledPolicy.SetDataModel(new MasterPasswordPolicyData\n        {\n            MinComplexity = 3,\n            MinLength = 12\n        });\n\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns(new List<Policy> { disabledPolicy });\n\n        // Act\n        var result = await sutProvider.Sut.GetMasterPasswordPolicyForUserAsync(user);\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    private static void SetupOrg(SutProvider<PolicyService> sutProvider, Guid organizationId, Organization organization)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organizationId)\n            .Returns(Task.FromResult(organization));\n    }\n\n    private static void SetupUserPolicies(Guid userId, SutProvider<PolicyService> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.RequireSso)\n            .Returns(new List<OrganizationUserPolicyDetails>\n            {\n                new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = false, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false},\n                new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false },\n                new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = true }\n            });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.DisableSend)\n            .Returns(new List<OrganizationUserPolicyDetails>\n            {\n                new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = false },\n                new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true }\n            });\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Shared/IValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.Errors;\nusing Bit.Core.AdminConsole.Utilities.Validation;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Shared;\n\npublic class IValidatorTests\n{\n    public class TestClass\n    {\n        public string Name { get; set; } = string.Empty;\n    }\n\n    public record InvalidRequestError<T>(T ErroredValue) : Error<T>(Code, ErroredValue)\n    {\n        public const string Code = \"InvalidRequest\";\n    }\n\n    public class TestClassValidator : IValidator<TestClass>\n    {\n        public Task<ValidationResult<TestClass>> ValidateAsync(TestClass value)\n        {\n            if (string.IsNullOrWhiteSpace(value.Name))\n            {\n                return Task.FromResult<ValidationResult<TestClass>>(\n                    new Invalid<TestClass>(new InvalidRequestError<TestClass>(value)));\n            }\n\n            return Task.FromResult<ValidationResult<TestClass>>(new Valid<TestClass>(value));\n        }\n    }\n\n    [Fact]\n    public async Task ValidateAsync_WhenSomethingIsInvalid_ReturnsInvalidWithError()\n    {\n        var example = new TestClass();\n\n        var result = await new TestClassValidator().ValidateAsync(example);\n\n        Assert.IsType<Invalid<TestClass>>(result);\n        var invalidResult = result as Invalid<TestClass>;\n        Assert.Equal(InvalidRequestError<TestClass>.Code, invalidResult!.Error.Message);\n    }\n\n    [Fact]\n    public async Task ValidateAsync_WhenIsValid_ReturnsValid()\n    {\n        var example = new TestClass { Name = \"Valid\" };\n\n        var result = await new TestClassValidator().ValidateAsync(example);\n\n        Assert.IsType<Valid<TestClass>>(result);\n        var validResult = result as Valid<TestClass>;\n        Assert.Equal(example.Name, validResult.Value.Name);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.Commands;\nusing Bit.Core.AdminConsole.Utilities.Errors;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Utilities.Commands;\n\npublic class CommandResultTests\n{\n    public class TestItem\n    {\n        public Guid Id { get; set; }\n        public string Value { get; set; }\n    }\n\n    public CommandResult<TestItem> BulkAction(IEnumerable<TestItem> items)\n    {\n        var itemList = items.ToList();\n        var successfulItems = items.Where(x => x.Value == \"SuccessfulRequest\").ToArray();\n\n        var failedItems = itemList.Except(successfulItems).ToArray();\n\n        var notFound = failedItems.First(x => x.Value == \"Failed due to not found\");\n        var invalidPermissions = failedItems.First(x => x.Value == \"Failed due to invalid permissions\");\n\n        var notFoundError = new RecordNotFoundError<TestItem>(notFound);\n        var insufficientPermissionsError = new InsufficientPermissionsError<TestItem>(invalidPermissions);\n\n        return new Partial<TestItem>(successfulItems.ToArray(), [notFoundError, insufficientPermissionsError]);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Partial_CommandResult_BulkRequestWithSuccessAndFailures(Guid successId1, Guid failureId1, Guid failureId2)\n    {\n        var listOfRecords = new List<TestItem>\n        {\n            new TestItem() { Id = successId1, Value = \"SuccessfulRequest\" },\n            new TestItem() { Id = failureId1, Value = \"Failed due to not found\" },\n            new TestItem() { Id = failureId2, Value = \"Failed due to invalid permissions\" }\n        };\n\n        var result = BulkAction(listOfRecords);\n\n        Assert.IsType<Partial<TestItem>>(result);\n\n        var failures = (result as Partial<TestItem>).Failures.ToArray();\n        var success = (result as Partial<TestItem>).Successes.First();\n\n        Assert.Equal(listOfRecords.First(), success);\n        Assert.Equal(2, failures.Length);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLoggerTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Utilities.DebuggingInstruments;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Utilities.DebuggingInstruments;\n\npublic class UserInviteDebuggingLoggerTests\n{\n    [Fact]\n    public void LogUserInviteStateDiagnostics_WhenInvitedUserHasNoEmail_LogsWarning()\n    {\n        // Arrange\n        var organizationUser = new OrganizationUser\n        {\n            OrganizationId = Guid.Parse(\"3e1f2196-9ad6-4ba7-b69d-ba33bc25f774\"),\n            Status = OrganizationUserStatusType.Invited,\n            Email = string.Empty,\n            UserId = Guid.Parse(\"93fbddd1-e96d-491d-a38b-6966ff59ac28\"),\n            Id = Guid.Parse(\"326f043f-afdc-47e5-9646-a76ab709b69a\"),\n        };\n\n        var logger = Substitute.For<ILogger>();\n\n        // Act\n        logger.LogUserInviteStateDiagnostics(organizationUser);\n\n        // Assert\n        logger.Received(1).Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(errorMessage =>\n                errorMessage.ToString().Contains(\"Warning invalid invited state\")\n                && errorMessage.ToString().Contains(organizationUser.OrganizationId.ToString())\n                && errorMessage.ToString().Contains(organizationUser.UserId.ToString())\n                && errorMessage.ToString().Contains(organizationUser.Id.ToString())\n            ),\n            null,\n            Arg.Any<Func<object, Exception, string>>()\n        );\n    }\n\n    public static IEnumerable<object[]> ConfirmedOrAcceptedTestCases =>\n    [\n       new object[] { OrganizationUserStatusType.Accepted },\n        new object[] { OrganizationUserStatusType.Confirmed },\n    ];\n\n    [Theory]\n    [MemberData(nameof(ConfirmedOrAcceptedTestCases))]\n    public void LogUserInviteStateDiagnostics_WhenNonInvitedUserHasEmail_LogsWarning(OrganizationUserStatusType userStatusType)\n    {\n        // Arrange\n        var organizationUser = new OrganizationUser\n        {\n            OrganizationId = Guid.Parse(\"3e1f2196-9ad6-4ba7-b69d-ba33bc25f774\"),\n            Status = userStatusType,\n            Email = \"someone@example.com\",\n            UserId = Guid.Parse(\"93fbddd1-e96d-491d-a38b-6966ff59ac28\"),\n            Id = Guid.Parse(\"326f043f-afdc-47e5-9646-a76ab709b69a\"),\n        };\n\n        var logger = Substitute.For<ILogger>();\n\n        // Act\n        logger.LogUserInviteStateDiagnostics(organizationUser);\n\n        // Assert\n        logger.Received(1).Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(errorMessage =>\n                errorMessage.ToString().Contains(\"Warning invalid confirmed or accepted state\")\n                && errorMessage.ToString().Contains(organizationUser.OrganizationId.ToString())\n                && errorMessage.ToString().Contains(organizationUser.UserId.ToString())\n                && errorMessage.ToString().Contains(organizationUser.Id.ToString())\n                // Ensure that no PII is included in the log.\n                && !errorMessage.ToString().Contains(organizationUser.Email)\n            ),\n            null,\n            Arg.Any<Func<object, Exception, string>>()\n        );\n    }\n\n\n    public static List<object[]> ShouldNotLogTestCases =>\n    [\n        new object[] { new OrganizationUser { Status = OrganizationUserStatusType.Accepted, Email = null } },\n        new object[] { new OrganizationUser { Status = OrganizationUserStatusType.Confirmed, Email = null } },\n        new object[] { new OrganizationUser { Status = OrganizationUserStatusType.Invited, Email = \"someone@example.com\" } },\n        new object[] { new OrganizationUser { Status = OrganizationUserStatusType.Revoked, Email = null } },\n    ];\n\n    [Theory]\n    [MemberData(nameof(ShouldNotLogTestCases))]\n    public void LogUserInviteStateDiagnostics_WhenStateAreValid_ShouldNotLog(OrganizationUser user)\n    {\n        var logger = Substitute.For<ILogger>();\n\n        // Act\n        logger.LogUserInviteStateDiagnostics(user);\n\n        // Assert\n        logger.DidNotReceive().Log(\n            Arg.Any<LogLevel>(),\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception, string>>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.AdminConsole.Utilities;\nusing Bit.Core.Models.Data;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Utilities;\n\npublic class IntegrationTemplateProcessorTests\n{\n    [Theory, BitAutoData]\n    public void ReplaceTokens_ReplacesSingleToken(EventMessage eventMessage)\n    {\n        var template = \"Event #Type# occurred.\";\n        var expected = $\"Event {eventMessage.Type} occurred.\";\n        var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);\n\n        Assert.Equal(expected, result);\n    }\n\n    [Theory, BitAutoData]\n    public void ReplaceTokens_ReplacesMultipleTokens(EventMessage eventMessage)\n    {\n        var template = \"Event #Type#, User (id: #UserId#).\";\n        var expected = $\"Event {eventMessage.Type}, User (id: {eventMessage.UserId}).\";\n        var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);\n\n        Assert.Equal(expected, result);\n    }\n\n    [Theory, BitAutoData]\n    public void ReplaceTokens_LeavesUnknownTokensUnchanged(EventMessage eventMessage)\n    {\n        var template = \"Event #Type#, User (id: #UserId#), Details: #UnknownKey#.\";\n        var expected = $\"Event {eventMessage.Type}, User (id: {eventMessage.UserId}), Details: #UnknownKey#.\";\n        var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);\n\n        Assert.Equal(expected, result);\n    }\n\n    [Theory, BitAutoData]\n    public void ReplaceTokens_WithNullProperty_InsertsEmptyString(EventMessage eventMessage)\n    {\n        eventMessage.UserId = null;\n\n        var template = \"Event #Type#, User (id: #UserId#).\";\n        var expected = $\"Event {eventMessage.Type}, User (id: ).\";\n        var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);\n\n        Assert.Equal(expected, result);\n    }\n\n    [Theory, BitAutoData]\n    public void ReplaceTokens_TokensWithNonmatchingCase_LeavesTokensUnchanged(EventMessage eventMessage)\n    {\n        var template = \"Event #type#, User (id: #UserId#).\";\n        var expected = $\"Event #type#, User (id: {eventMessage.UserId}).\";\n        var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);\n\n        Assert.Equal(expected, result);\n    }\n\n    [Theory, BitAutoData]\n    public void ReplaceTokens_NoTokensPresent_ReturnsOriginalString(EventMessage eventMessage)\n    {\n        var template = \"System is running normally.\";\n        var expected = \"System is running normally.\";\n        var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);\n\n        Assert.Equal(expected, result);\n    }\n\n    [Theory, BitAutoData]\n    public void ReplaceTokens_TemplateIsEmpty_ReturnsOriginalString(EventMessage eventMessage)\n    {\n        var emptyTemplate = \"\";\n        var expectedEmpty = \"\";\n\n        Assert.Equal(expectedEmpty, IntegrationTemplateProcessor.ReplaceTokens(emptyTemplate, eventMessage));\n    }\n\n    [Theory]\n    [InlineData(\"User name is #UserName#\")]\n    [InlineData(\"Email: #UserEmail#\")]\n    [InlineData(\"User type = #UserType#\")]\n    public void TemplateRequiresUser_ContainingKeys_ReturnsTrue(string template)\n    {\n        var result = IntegrationTemplateProcessor.TemplateRequiresUser(template);\n        Assert.True(result);\n    }\n\n    [Theory]\n    [InlineData(\"#UserId#\")]  // This is on the base class, not fetched, so should be false\n    [InlineData(\"No User Tokens\")]\n    [InlineData(\"\")]\n    public void TemplateRequiresUser_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template)\n    {\n        var result = IntegrationTemplateProcessor.TemplateRequiresUser(template);\n        Assert.False(result);\n    }\n\n    [Theory]\n    [InlineData(\"Acting user is #ActingUserName#\")]\n    [InlineData(\"Acting user's email is #ActingUserEmail#\")]\n    [InlineData(\"Acting user's type is #ActingUserType#\")]\n    public void TemplateRequiresActingUser_ContainingKeys_ReturnsTrue(string template)\n    {\n        var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template);\n        Assert.True(result);\n    }\n\n    [Theory]\n    [InlineData(\"No ActiveUser tokens\")]\n    [InlineData(\"#ActiveUserId#\")]  // This is on the base class, not fetched, so should be false\n    [InlineData(\"\")]\n    public void TemplateRequiresActingUser_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template)\n    {\n        var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template);\n        Assert.False(result);\n    }\n\n    [Theory]\n    [InlineData(\"Group name is #GroupName#!\")]\n    [InlineData(\"Group: #GroupName#\")]\n    public void TemplateRequiresGroup_ContainingKeys_ReturnsTrue(string template)\n    {\n        var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template);\n        Assert.True(result);\n    }\n\n    [Theory]\n    [InlineData(\"#GroupId#\")]  // This is on the base class, not fetched, so should be false\n    [InlineData(\"No Group Tokens\")]\n    [InlineData(\"\")]\n    public void TemplateRequiresGroup_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template)\n    {\n        var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template);\n        Assert.False(result);\n    }\n\n    [Theory]\n    [InlineData(\"Organization: #OrganizationName#\")]\n    [InlineData(\"Welcome to #OrganizationName#\")]\n    public void TemplateRequiresOrganization_ContainingKeys_ReturnsTrue(string template)\n    {\n        var result = IntegrationTemplateProcessor.TemplateRequiresOrganization(template);\n        Assert.True(result);\n    }\n\n    [Theory]\n    [InlineData(\"No organization tokens\")]\n    [InlineData(\"#OrganizationId#\")]  // This is on the base class, not fetched, so should be false\n    [InlineData(\"\")]\n    public void TemplateRequiresOrganization_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template)\n    {\n        var result = IntegrationTemplateProcessor.TemplateRequiresOrganization(template);\n        Assert.False(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AdminConsole/Utilities/PolicyDataValidatorTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.Utilities;\nusing Bit.Core.Exceptions;\nusing Xunit;\n\nnamespace Bit.Core.Test.AdminConsole.Utilities;\n\npublic class PolicyDataValidatorTests\n{\n    [Fact]\n    public void ValidateAndSerialize_NullData_ReturnsNull()\n    {\n        var result = PolicyDataValidator.ValidateAndSerialize(null, PolicyType.MasterPassword);\n\n        Assert.Null(result);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_ValidData_ReturnsSerializedJson()\n    {\n        var data = new Dictionary<string, object>\n        {\n            { \"minLength\", 12 },\n            { \"minComplexity\", 4 }\n        };\n\n        var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);\n\n        Assert.NotNull(result);\n        Assert.Contains(\"\\\"minLength\\\":12\", result);\n        Assert.Contains(\"\\\"minComplexity\\\":4\", result);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_InvalidDataType_ThrowsBadRequestException()\n    {\n        var data = new Dictionary<string, object> { { \"minLength\", \"not a number\" } };\n\n        var exception = Assert.Throws<BadRequestException>(() =>\n            PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));\n\n        Assert.Contains(\"Invalid data for MasterPassword policy\", exception.Message);\n        Assert.Contains(\"minLength\", exception.Message);\n    }\n\n    [Fact]\n    public void ValidateAndDeserializeMetadata_NullMetadata_ReturnsEmptyMetadataModel()\n    {\n        var result = PolicyDataValidator.ValidateAndDeserializeMetadata(null, PolicyType.SingleOrg);\n\n        Assert.IsType<EmptyMetadataModel>(result);\n    }\n\n    [Fact]\n    public void ValidateAndDeserializeMetadata_ValidMetadata_ReturnsModel()\n    {\n        var metadata = new Dictionary<string, object> { { \"defaultUserCollectionName\", \"collection name\" } };\n\n        var result = PolicyDataValidator.ValidateAndDeserializeMetadata(metadata, PolicyType.OrganizationDataOwnership);\n\n        Assert.IsType<OrganizationModelOwnershipPolicyModel>(result);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_ExcessiveMinLength_ThrowsBadRequestException()\n    {\n        var data = new Dictionary<string, object> { { \"minLength\", 129 } };\n\n        var exception = Assert.Throws<BadRequestException>(() =>\n            PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));\n\n        Assert.Contains(\"Invalid data for MasterPassword policy\", exception.Message);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_ExcessiveMinComplexity_ThrowsBadRequestException()\n    {\n        var data = new Dictionary<string, object> { { \"minComplexity\", 5 } };\n\n        var exception = Assert.Throws<BadRequestException>(() =>\n            PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));\n\n        Assert.Contains(\"Invalid data for MasterPassword policy\", exception.Message);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_MinLengthAtMinimum_Succeeds()\n    {\n        var data = new Dictionary<string, object> { { \"minLength\", 12 } };\n\n        var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);\n\n        Assert.NotNull(result);\n        Assert.Contains(\"\\\"minLength\\\":12\", result);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_MinLengthAtMaximum_Succeeds()\n    {\n        var data = new Dictionary<string, object> { { \"minLength\", 128 } };\n\n        var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);\n\n        Assert.NotNull(result);\n        Assert.Contains(\"\\\"minLength\\\":128\", result);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_MinLengthBelowMinimum_ThrowsBadRequestException()\n    {\n        var data = new Dictionary<string, object> { { \"minLength\", 11 } };\n\n        var exception = Assert.Throws<BadRequestException>(() =>\n            PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));\n\n        Assert.Contains(\"Invalid data for MasterPassword policy\", exception.Message);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_MinComplexityAtMinimum_Succeeds()\n    {\n        var data = new Dictionary<string, object> { { \"minComplexity\", 0 } };\n\n        var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);\n\n        Assert.NotNull(result);\n        Assert.Contains(\"\\\"minComplexity\\\":0\", result);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_MinComplexityAtMaximum_Succeeds()\n    {\n        var data = new Dictionary<string, object> { { \"minComplexity\", 4 } };\n\n        var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);\n\n        Assert.NotNull(result);\n        Assert.Contains(\"\\\"minComplexity\\\":4\", result);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_MinComplexityBelowMinimum_ThrowsBadRequestException()\n    {\n        var data = new Dictionary<string, object> { { \"minComplexity\", -1 } };\n\n        var exception = Assert.Throws<BadRequestException>(() =>\n            PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));\n\n        Assert.Contains(\"Invalid data for MasterPassword policy\", exception.Message);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_NullMinLength_Succeeds()\n    {\n        var data = new Dictionary<string, object>\n        {\n            { \"minComplexity\", 2 }\n            // minLength is omitted, should be null\n        };\n\n        var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);\n\n        Assert.NotNull(result);\n        Assert.Contains(\"\\\"minComplexity\\\":2\", result);\n    }\n\n    [Fact]\n    public void ValidateAndSerialize_MultipleInvalidFields_ThrowsBadRequestException()\n    {\n        var data = new Dictionary<string, object>\n        {\n            { \"minLength\", 200 },\n            { \"minComplexity\", 10 }\n        };\n\n        var exception = Assert.Throws<BadRequestException>(() =>\n            PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));\n\n        Assert.Contains(\"Invalid data for MasterPassword policy\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Attributes/MarketingInitiativeValidationAttributeTests.cs",
    "content": "﻿using Bit.Core.Auth.Attributes;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Attributes;\n\npublic class MarketingInitiativeValidationAttributeTests\n{\n    [Fact]\n    public void IsValid_NullValue_ReturnsTrue()\n    {\n        var sut = new MarketingInitiativeValidationAttribute();\n\n        var actual = sut.IsValid(null);\n\n        Assert.True(actual);\n    }\n\n    [Theory]\n    [InlineData(MarketingInitiativeConstants.Premium)]\n    public void IsValid_AcceptedValue_ReturnsTrue(string value)\n    {\n        var sut = new MarketingInitiativeValidationAttribute();\n\n        var actual = sut.IsValid(value);\n\n        Assert.True(actual);\n    }\n\n    [Theory]\n    [InlineData(\"invalid\")]\n    [InlineData(\"\")]\n    [InlineData(\"Premium\")]          // case sensitive - capitalized\n    [InlineData(\"PREMIUM\")]          // case sensitive - uppercase\n    [InlineData(\"premium \")]         // trailing space\n    [InlineData(\" premium\")]         // leading space\n    public void IsValid_InvalidStringValue_ReturnsFalse(string value)\n    {\n        var sut = new MarketingInitiativeValidationAttribute();\n\n        var actual = sut.IsValid(value);\n\n        Assert.False(actual);\n    }\n\n    [Theory]\n    [InlineData(123)]                   // integer\n    [InlineData(true)]                  // boolean\n    [InlineData(45.67)]                 // double\n    public void IsValid_NonStringValue_ReturnsFalse(object value)\n    {\n        var sut = new MarketingInitiativeValidationAttribute();\n\n        var actual = sut.IsValid(value);\n\n        Assert.False(actual);\n    }\n\n    [Fact]\n    public void ErrorMessage_ContainsAcceptedValues()\n    {\n        var sut = new MarketingInitiativeValidationAttribute();\n\n        var errorMessage = sut.ErrorMessage;\n\n        Assert.NotNull(errorMessage);\n        Assert.Contains(\"premium\", errorMessage);\n        Assert.Contains(\"Marketing initiative type must be one of:\", errorMessage);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/AutoFixture/RegisterFinishRequestModelFixtures.cs",
    "content": "﻿using System.ComponentModel.DataAnnotations;\nusing AutoFixture;\nusing Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Enums;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.Auth.AutoFixture;\n\ninternal class RegisterFinishRequestModelCustomization : ICustomization\n{\n    [StrictEmailAddress, StringLength(256)]\n    public required string Email { get; set; }\n    public required KdfType Kdf { get; set; }\n    public required int KdfIterations { get; set; }\n    public string? EmailVerificationToken { get; set; }\n    public string? OrgInviteToken { get; set; }\n    public string? OrgSponsoredFreeFamilyPlanToken { get; set; }\n    public string? AcceptEmergencyAccessInviteToken { get; set; }\n    public string? ProviderInviteToken { get; set; }\n\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<RegisterFinishRequestModel>(composer => composer\n            .With(o => o.Email, Email)\n            .With(o => o.Kdf, Kdf)\n            .With(o => o.KdfIterations, KdfIterations)\n            .With(o => o.EmailVerificationToken, EmailVerificationToken)\n            .With(o => o.OrgInviteToken, OrgInviteToken)\n            .With(o => o.OrgSponsoredFreeFamilyPlanToken, OrgSponsoredFreeFamilyPlanToken)\n            .With(o => o.AcceptEmergencyAccessInviteToken, AcceptEmergencyAccessInviteToken)\n            .With(o => o.ProviderInviteToken, ProviderInviteToken)\n            .Without(o => o.MasterPasswordAuthentication)\n            .Without(o => o.MasterPasswordUnlock));\n    }\n}\n\npublic class RegisterFinishRequestModelCustomizeAttribute : BitCustomizeAttribute\n{\n    public string _email { get; set; } = \"{0}@email.com\";\n    public KdfType _kdf { get; set; } = KdfType.PBKDF2_SHA256;\n    public int _kdfIterations { get; set; } = AuthConstants.PBKDF2_ITERATIONS.Default;\n    public string? _emailVerificationToken { get; set; }\n    public string? _orgInviteToken { get; set; }\n    public string? _orgSponsoredFreeFamilyPlanToken { get; set; }\n    public string? _acceptEmergencyAccessInviteToken { get; set; }\n    public string? _providerInviteToken { get; set; }\n\n    public override ICustomization GetCustomization() => new RegisterFinishRequestModelCustomization()\n    {\n        Email = _email,\n        Kdf = _kdf,\n        KdfIterations = _kdfIterations,\n        EmailVerificationToken = _emailVerificationToken,\n        OrgInviteToken = _orgInviteToken,\n        OrgSponsoredFreeFamilyPlanToken = _orgSponsoredFreeFamilyPlanToken,\n        AcceptEmergencyAccessInviteToken = _acceptEmergencyAccessInviteToken,\n        ProviderInviteToken = _providerInviteToken\n    };\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Entities/AuthRequestTests.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Entities;\n\npublic class AuthRequestTests\n{\n    [Fact]\n    public void IsValidForAuthentication_WithValidRequest_ReturnsTrue()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var accessCode = \"test-access-code\";\n        var authRequest = new AuthRequest\n        {\n            UserId = userId,\n            Type = AuthRequestType.AuthenticateAndUnlock,\n            ResponseDate = DateTime.UtcNow,\n            Approved = true,\n            CreationDate = DateTime.UtcNow,\n            AuthenticationDate = null,\n            AccessCode = accessCode\n        };\n\n        // Act\n        var result = authRequest.IsValidForAuthentication(userId, accessCode);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Fact]\n    public void IsValidForAuthentication_WithWrongUserId_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var differentUserId = Guid.NewGuid();\n        var accessCode = \"test-access-code\";\n        var authRequest = new AuthRequest\n        {\n            UserId = userId,\n            Type = AuthRequestType.AuthenticateAndUnlock,\n            ResponseDate = DateTime.UtcNow,\n            Approved = true,\n            CreationDate = DateTime.UtcNow,\n            AuthenticationDate = null,\n            AccessCode = accessCode\n        };\n\n        // Act\n        var result = authRequest.IsValidForAuthentication(differentUserId, accessCode);\n\n        // Assert\n        Assert.False(result, \"Auth request should not validate for a different user\");\n    }\n\n    [Fact]\n    public void IsValidForAuthentication_WithWrongAccessCode_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var authRequest = new AuthRequest\n        {\n            UserId = userId,\n            Type = AuthRequestType.AuthenticateAndUnlock,\n            ResponseDate = DateTime.UtcNow,\n            Approved = true,\n            CreationDate = DateTime.UtcNow,\n            AuthenticationDate = null,\n            AccessCode = \"correct-code\"\n        };\n\n        // Act\n        var result = authRequest.IsValidForAuthentication(userId, \"wrong-code\");\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void IsValidForAuthentication_WithoutResponseDate_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var accessCode = \"test-access-code\";\n        var authRequest = new AuthRequest\n        {\n            UserId = userId,\n            Type = AuthRequestType.AuthenticateAndUnlock,\n            ResponseDate = null, // Not responded to\n            Approved = true,\n            CreationDate = DateTime.UtcNow,\n            AuthenticationDate = null,\n            AccessCode = accessCode\n        };\n\n        // Act\n        var result = authRequest.IsValidForAuthentication(userId, accessCode);\n\n        // Assert\n        Assert.False(result, \"Unanswered auth requests should not be valid\");\n    }\n\n    [Fact]\n    public void IsValidForAuthentication_WithApprovedFalse_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var accessCode = \"test-access-code\";\n        var authRequest = new AuthRequest\n        {\n            UserId = userId,\n            Type = AuthRequestType.AuthenticateAndUnlock,\n            ResponseDate = DateTime.UtcNow,\n            Approved = false, // Denied\n            CreationDate = DateTime.UtcNow,\n            AuthenticationDate = null,\n            AccessCode = accessCode\n        };\n\n        // Act\n        var result = authRequest.IsValidForAuthentication(userId, accessCode);\n\n        // Assert\n        Assert.False(result, \"Denied auth requests should not be valid\");\n    }\n\n    [Fact]\n    public void IsValidForAuthentication_WithApprovedNull_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var accessCode = \"test-access-code\";\n        var authRequest = new AuthRequest\n        {\n            UserId = userId,\n            Type = AuthRequestType.AuthenticateAndUnlock,\n            ResponseDate = DateTime.UtcNow,\n            Approved = null, // Pending\n            CreationDate = DateTime.UtcNow,\n            AuthenticationDate = null,\n            AccessCode = accessCode\n        };\n\n        // Act\n        var result = authRequest.IsValidForAuthentication(userId, accessCode);\n\n        // Assert\n        Assert.False(result, \"Pending auth requests should not be valid\");\n    }\n\n    [Fact]\n    public void IsValidForAuthentication_WithExpiredRequest_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var accessCode = \"test-access-code\";\n        var authRequest = new AuthRequest\n        {\n            UserId = userId,\n            Type = AuthRequestType.AuthenticateAndUnlock,\n            ResponseDate = DateTime.UtcNow,\n            Approved = true,\n            CreationDate = DateTime.UtcNow.AddMinutes(-20), // Expired (15 min timeout)\n            AuthenticationDate = null,\n            AccessCode = accessCode\n        };\n\n        // Act\n        var result = authRequest.IsValidForAuthentication(userId, accessCode);\n\n        // Assert\n        Assert.False(result, \"Expired auth requests should not be valid\");\n    }\n\n    [Fact]\n    public void IsValidForAuthentication_WithWrongType_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var accessCode = \"test-access-code\";\n        var authRequest = new AuthRequest\n        {\n            UserId = userId,\n            Type = AuthRequestType.Unlock, // Wrong type\n            ResponseDate = DateTime.UtcNow,\n            Approved = true,\n            CreationDate = DateTime.UtcNow,\n            AuthenticationDate = null,\n            AccessCode = accessCode\n        };\n\n        // Act\n        var result = authRequest.IsValidForAuthentication(userId, accessCode);\n\n        // Assert\n        Assert.False(result, \"Only AuthenticateAndUnlock type should be valid\");\n    }\n\n    [Fact]\n    public void IsValidForAuthentication_WithAlreadyUsed_ReturnsFalse()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var accessCode = \"test-access-code\";\n        var authRequest = new AuthRequest\n        {\n            UserId = userId,\n            Type = AuthRequestType.AuthenticateAndUnlock,\n            ResponseDate = DateTime.UtcNow,\n            Approved = true,\n            CreationDate = DateTime.UtcNow,\n            AuthenticationDate = DateTime.UtcNow, // Already used\n            AccessCode = accessCode\n        };\n\n        // Act\n        var result = authRequest.IsValidForAuthentication(userId, accessCode);\n\n        // Assert\n        Assert.False(result, \"Auth requests should only be valid for one-time use\");\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Identity/AuthenticationTwoFactorTokenProviderTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Identity;\n\npublic class AuthenticationTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests<AuthenticatorTokenProvider>\n{\n    public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Authenticator;\n\n    public static IEnumerable<object[]> CanGenerateTwoFactorTokenAsyncData\n        => SetupCanGenerateData(\n            (\n                new Dictionary<string, object>\n                {\n                    [\"Key\"] = \"stuff\",\n                },\n                true\n            ),\n            (\n                new Dictionary<string, object>\n                {\n                    [\"Key\"] = \"\"\n                },\n                false\n            )\n        );\n\n    [Theory, BitMemberAutoData(nameof(CanGenerateTwoFactorTokenAsyncData))]\n    public override async Task RunCanGenerateTwoFactorTokenAsync(Dictionary<string, object> metaData, bool expectedResponse,\n        User user, SutProvider<AuthenticatorTokenProvider> sutProvider)\n    {\n        await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Identity/BaseTwoFactorTokenProviderTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Identity;\n\n[SutProviderCustomize]\npublic abstract class BaseTwoFactorTokenProviderTests<T>\n    where T : IUserTwoFactorTokenProvider<User>\n{\n    public abstract TwoFactorProviderType TwoFactorProviderType { get; }\n\n    protected static IEnumerable<object[]> SetupCanGenerateData(params (Dictionary<string, object> MetaData, bool ExpectedResponse)[] data)\n    {\n        return data.Select(d =>\n            new object[]\n            {\n                d.MetaData,\n                d.ExpectedResponse,\n            });\n    }\n\n    protected virtual IUserService AdditionalSetup(SutProvider<T> sutProvider, User user)\n    {\n        var userService = Substitute.For<IUserService>();\n\n        sutProvider.GetDependency<IServiceProvider>()\n            .GetService(typeof(IUserService))\n            .Returns(userService);\n\n        SetupUserService(userService, user);\n\n        return userService;\n    }\n\n    protected virtual void SetupUserService(IUserService userService, User user)\n    {\n        userService\n            .CanAccessPremium(user)\n            .Returns(true);\n    }\n\n    protected static UserManager<User> SubstituteUserManager()\n    {\n        return new UserManager<User>(Substitute.For<IUserStore<User>>(),\n            Substitute.For<IOptions<IdentityOptions>>(),\n            Substitute.For<IPasswordHasher<User>>(),\n            Enumerable.Empty<IUserValidator<User>>(),\n            Enumerable.Empty<IPasswordValidator<User>>(),\n            Substitute.For<ILookupNormalizer>(),\n            Substitute.For<IdentityErrorDescriber>(),\n            Substitute.For<IServiceProvider>(),\n            Substitute.For<ILogger<UserManager<User>>>());\n    }\n\n    protected void MockDatabase(User user, Dictionary<string, object> metaData)\n    {\n        var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType] = new TwoFactorProvider\n            {\n                Enabled = true,\n                MetaData = metaData,\n            },\n        };\n\n        user.TwoFactorProviders = JsonHelpers.LegacySerialize(providers);\n    }\n\n    public virtual async Task RunCanGenerateTwoFactorTokenAsync(Dictionary<string, object> metaData, bool expectedResponse,\n        User user, SutProvider<T> sutProvider)\n    {\n        var userManager = SubstituteUserManager();\n        MockDatabase(user, metaData);\n\n        var response = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(userManager, user);\n        Assert.Equal(expectedResponse, response);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing Duo = DuoUniversal;\n\nnamespace Bit.Core.Test.Auth.Identity;\n\npublic class DuoUniversalTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests<DuoUniversalTokenProvider>\n{\n    private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For<IDuoUniversalTokenService>();\n    public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Duo;\n\n    public static IEnumerable<object[]> CanGenerateTwoFactorTokenAsyncData\n        => SetupCanGenerateData(\n            ( // correct data\n                new Dictionary<string, object>\n                {\n                    [\"ClientId\"] = new string('c', 20),\n                    [\"ClientSecret\"] = new string('s', 40),\n                    [\"Host\"] = \"https://api-abcd1234.duosecurity.com\",\n                },\n                true\n            ),\n            ( // correct data duo federal\n                new Dictionary<string, object>\n                {\n                    [\"ClientId\"] = new string('c', 20),\n                    [\"ClientSecret\"] = new string('s', 40),\n                    [\"Host\"] = \"https://api-abcd1234.duofederal.com\",\n                },\n                true\n            ),\n            ( // correct data duo federal\n                new Dictionary<string, object>\n                {\n                    [\"ClientId\"] = new string('c', 20),\n                    [\"ClientSecret\"] = new string('s', 40),\n                    [\"Host\"] = \"https://api-abcd1234.duofederal.com\",\n                },\n                true\n            ),\n            ( // invalid host\n                new Dictionary<string, object>\n                {\n                    [\"ClientId\"] = new string('c', 20),\n                    [\"ClientSecret\"] = new string('s', 40),\n                    [\"Host\"] = \"\",\n                },\n                false\n            ),\n            ( // clientId missing\n                new Dictionary<string, object>\n                {\n                    [\"ClientSecret\"] = new string('s', 40),\n                    [\"Host\"] = \"https://api-abcd1234.duofederal.com\",\n                },\n                false\n            )\n        );\n\n    public static IEnumerable<object[]> NonPremiumCanGenerateTwoFactorTokenAsyncData\n        => SetupCanGenerateData(\n            ( // correct data\n                new Dictionary<string, object>\n                {\n                    [\"ClientId\"] = new string('c', 20),\n                    [\"ClientSecret\"] = new string('s', 40),\n                    [\"Host\"] = \"https://api-abcd1234.duosecurity.com\",\n                },\n                false\n            )\n        );\n\n    [Theory, BitMemberAutoData(nameof(CanGenerateTwoFactorTokenAsyncData))]\n    public override async Task RunCanGenerateTwoFactorTokenAsync(Dictionary<string, object> metaData, bool expectedResponse,\n        User user, SutProvider<DuoUniversalTokenProvider> sutProvider)\n    {\n        // Arrange\n        AdditionalSetup(sutProvider, user);\n        user.Premium = true;\n        user.PremiumExpirationDate = DateTime.UtcNow.AddDays(1);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())\n            .Returns(expectedResponse);\n\n        // Act\n        // Assert\n        await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider);\n    }\n\n    [Theory, BitMemberAutoData(nameof(NonPremiumCanGenerateTwoFactorTokenAsyncData))]\n    public async Task CanGenerateTwoFactorTokenAsync_UserCanNotAccessPremium_ReturnsNull(Dictionary<string, object> metaData, bool expectedResponse,\n    User user, SutProvider<DuoUniversalTokenProvider> sutProvider)\n    {\n        // Arrange\n        AdditionalSetup(sutProvider, user);\n\n        user.Premium = false;\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())\n            .Returns(expectedResponse);\n\n        // Act\n        // Assert\n        await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GenerateToken_Success_ReturnsAuthUrl(\n        User user, SutProvider<DuoUniversalTokenProvider> sutProvider, string authUrl)\n    {\n        // Arrange\n        SetUpProperDuoUniversalTokenService(user, sutProvider);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .GenerateAuthUrl(\n                Arg.Any<Duo.Client>(),\n                Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(),\n                user)\n            .Returns(authUrl);\n\n        // Act\n        var token = await sutProvider.Sut.GenerateAsync(\"purpose\", SubstituteUserManager(), user);\n\n        // Assert\n        Assert.NotNull(token);\n        Assert.Equal(token, authUrl);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GenerateToken_DuoClientNull_ReturnsNull(\n        User user, SutProvider<DuoUniversalTokenProvider> sutProvider)\n    {\n        // Arrange\n        user.Premium = true;\n        user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();\n        AdditionalSetup(sutProvider, user);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())\n            .Returns(true);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())\n            .Returns(null as Duo.Client);\n\n        // Act\n        var token = await sutProvider.Sut.GenerateAsync(\"purpose\", SubstituteUserManager(), user);\n\n        // Assert\n        Assert.Null(token);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GenerateToken_UserCanNotAccessPremium_ReturnsNull(\n    User user, SutProvider<DuoUniversalTokenProvider> sutProvider)\n    {\n        // Arrange\n        user.Premium = false;\n        user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();\n        AdditionalSetup(sutProvider, user);\n\n        // Act\n        var token = await sutProvider.Sut.GenerateAsync(\"purpose\", SubstituteUserManager(), user);\n\n        // Assert\n        Assert.Null(token);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateToken_ValidToken_ReturnsTrue(\n        User user, SutProvider<DuoUniversalTokenProvider> sutProvider, string token)\n    {\n        // Arrange\n        SetUpProperDuoUniversalTokenService(user, sutProvider);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n                .RequestDuoValidationAsync(\n                    Arg.Any<Duo.Client>(),\n                    Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(),\n                    user,\n                    token)\n                .Returns(true);\n\n        // Act\n        var response = await sutProvider.Sut.ValidateAsync(\"purpose\", token, SubstituteUserManager(), user);\n\n        // Assert\n        Assert.True(response);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateToken_DuoClientNull_ReturnsFalse(\n    User user, SutProvider<DuoUniversalTokenProvider> sutProvider, string token)\n    {\n        user.Premium = true;\n        user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();\n        AdditionalSetup(sutProvider, user);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())\n            .Returns(true);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())\n            .Returns(null as Duo.Client);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(\"purpose\", token, SubstituteUserManager(), user);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    /// <summary>\n    /// Ensures that the IDuoUniversalTokenService is properly setup for the test.\n    /// This ensures that the private GetDuoClientAsync, and GetDuoTwoFactorProvider\n    /// methods will return true enabling the test to execute on the correct path.\n    /// </summary>\n    /// <param name=\"user\">user from calling test</param>\n    /// <param name=\"sutProvider\">self</param>\n    private void SetUpProperDuoUniversalTokenService(User user, SutProvider<DuoUniversalTokenProvider> sutProvider)\n    {\n        user.Premium = true;\n        user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();\n        var client = BuildDuoClient();\n\n        AdditionalSetup(sutProvider, user);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n                .HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())\n                .Returns(true);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n                .BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())\n                .Returns(client);\n    }\n\n    private Duo.Client BuildDuoClient()\n    {\n        var clientId = new string('c', 20);\n        var clientSecret = new string('s', 40);\n        return new Duo.ClientBuilder(clientId, clientSecret, \"api-abcd1234.duosecurity.com\", \"redirectUrl\").Build();\n    }\n\n    private string GetTwoFactorDuoProvidersJson()\n    {\n        return\n            \"{\\\"2\\\":{\\\"Enabled\\\":true,\\\"MetaData\\\":{\\\"ClientSecret\\\":\\\"secretClientSecret\\\",\\\"ClientId\\\":\\\"clientId\\\",\\\"Host\\\":\\\"example.com\\\"}}}\";\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs",
    "content": "﻿using Bit.Core;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Options;\nusing NSubstitute;\nusing Xunit;\n\n[SutProviderCustomize]\npublic class EmailTokenProviderTests\n{\n    private readonly IDistributedCache _cache;\n\n    public EmailTokenProviderTests()\n    {\n        _cache = Substitute.For<IDistributedCache>();\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GenerateAsync_GeneratesSixDigitToken_WhenFeatureFlagIsEnabled(User user)\n    {\n        // Arrange\n        var purpose = \"test-purpose\";\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.Otp6Digits).Returns(true);\n        var tokenProvider = new EmailTokenProvider(_cache, featureService);\n\n        // Act\n        var code = await tokenProvider.GenerateAsync(purpose, SubstituteUserManager(), user);\n\n        // Assert\n        Assert.Equal(6, code.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GenerateAsync_GeneratesEightDigitToken_WhenFeatureFlagIsDisabled(User user)\n    {\n        // Arrange\n        var purpose = \"test-purpose\";\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.Otp6Digits).Returns(false);\n        var tokenProvider = new EmailTokenProvider(_cache, featureService);\n        // Act\n        var code = await tokenProvider.GenerateAsync(purpose, SubstituteUserManager(), user);\n\n        // Assert\n        Assert.Equal(8, code.Length);\n    }\n\n    protected static UserManager<User> SubstituteUserManager()\n    {\n        return new UserManager<User>(Substitute.For<IUserStore<User>>(),\n            Substitute.For<IOptions<IdentityOptions>>(),\n            Substitute.For<IPasswordHasher<User>>(),\n            Enumerable.Empty<IUserValidator<User>>(),\n            Enumerable.Empty<IPasswordValidator<User>>(),\n            Substitute.For<ILookupNormalizer>(),\n            Substitute.For<IdentityErrorDescriber>(),\n            Substitute.For<IServiceProvider>(),\n            Substitute.For<ILogger<UserManager<User>>>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Identity;\n\npublic class EmailTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests<EmailTwoFactorTokenProvider>\n{\n    public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Email;\n\n    public static IEnumerable<object[]> CanGenerateTwoFactorTokenAsyncData\n        => SetupCanGenerateData(\n            (\n                new Dictionary<string, object>\n                {\n                    [\"Email\"] = \"test@email.com\",\n                },\n                true\n            ),\n            (\n                new Dictionary<string, object>\n                {\n                    [\"NotEmail\"] = \"value\",\n                },\n                false\n            ),\n            (\n                new Dictionary<string, object>\n                {\n                    [\"Email\"] = \"\",\n                },\n                false\n            )\n        );\n\n    [Theory, BitMemberAutoData(nameof(CanGenerateTwoFactorTokenAsyncData))]\n    public override async Task RunCanGenerateTwoFactorTokenAsync(Dictionary<string, object> metaData, bool expectedResponse,\n        User user, SutProvider<EmailTwoFactorTokenProvider> sutProvider)\n    {\n        await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GenerateAsync_ShouldReturnSixDigitToken_WithFeatureFlagEnabled(\n        User user, SutProvider<EmailTwoFactorTokenProvider> sutProvider)\n    {\n        // Arrange\n        user.TwoFactorProviders = GetTwoFactorEmailProvidersJson();\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.Otp6Digits)\n            .Returns(true);\n\n        // Act\n        var token = await sutProvider.Sut.GenerateAsync(\"purpose\", SubstituteUserManager(), user);\n\n        // Assert\n        Assert.NotNull(token);\n        Assert.Equal(6, token.Length);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GenerateAsync_ShouldReturnSixDigitToken_WithFeatureFlagDisabled(\n        User user, SutProvider<EmailTwoFactorTokenProvider> sutProvider)\n    {\n        // Arrange\n        user.TwoFactorProviders = GetTwoFactorEmailProvidersJson();\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.Otp6Digits)\n            .Returns(false);\n\n        // Act\n        var token = await sutProvider.Sut.GenerateAsync(\"purpose\", SubstituteUserManager(), user);\n\n        // Assert\n        Assert.NotNull(token);\n        Assert.Equal(6, token.Length);\n    }\n\n    private string GetTwoFactorEmailProvidersJson()\n    {\n        return\n            \"{\\\"1\\\":{\\\"Enabled\\\":true,\\\"MetaData\\\":{\\\"Email\\\":\\\"test@email.com\\\"}}}\";\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Identity/OrganizationDuoUniversalTwoFactorTokenProviderTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing Duo = DuoUniversal;\n\nnamespace Bit.Core.Test.Auth.Identity;\n\n[SutProviderCustomize]\npublic class OrganizationDuoUniversalTwoFactorTokenProviderTests\n{\n    private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For<IDuoUniversalTokenService>();\n    private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = Substitute.For<IDataProtectorTokenFactory<DuoUserStateTokenable>>();\n\n    // Happy path\n    [Theory]\n    [BitAutoData]\n    public async Task CanGenerateTwoFactorTokenAsync_ReturnsTrue(\n        Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)\n    {\n        // Arrange\n        organization.Enabled = true;\n        organization.Use2fa = true;\n        SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);\n\n        // Act\n        var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanGenerateTwoFactorTokenAsync_DuoTwoFactorNotEnabled_ReturnsFalse(\n        Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)\n    {\n        // Arrange\n        organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderNotEnabledJson();\n        organization.Use2fa = true;\n        organization.Enabled = true;\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n                .HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())\n                .Returns(true);\n        // Act\n        var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CanGenerateTwoFactorTokenAsync_BadMetaData_ProviderNull_ReturnsFalse(\n        Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)\n    {\n        // Arrange\n        organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();\n        organization.Use2fa = true;\n        organization.Enabled = true;\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n                .HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())\n                .Returns(false);\n        // Act\n        var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetDuoTwoFactorProvider_OrganizationNull_ReturnsNull(\n        SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)\n    {\n        // Act\n        var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetDuoTwoFactorProvider_OrganizationNotEnabled_ReturnsNull(\n        Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)\n    {\n        // Arrange\n        SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);\n        organization.Enabled = false;\n\n        // Act\n        var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetDuoTwoFactorProvider_OrganizationUse2FAFalse_ReturnsNull(\n        Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)\n    {\n        // Arrange\n        SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);\n        organization.Use2fa = false;\n\n        // Act\n        var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetDuoClient_ProviderNull_ReturnsNull(\n        SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)\n    {\n        // Act\n        var result = await sutProvider.Sut.GenerateAsync(null, default);\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetDuoClient_DuoClientNull_ReturnsNull(\n        SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,\n        Organization organization)\n    {\n        // Arrange\n        organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();\n        organization.Use2fa = true;\n        organization.Enabled = true;\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())\n            .Returns(true);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())\n            .Returns(null as Duo.Client);\n\n        // Act\n        var result = await sutProvider.Sut.GenerateAsync(organization, default);\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GenerateAsync_ReturnsAuthUrl(\n        SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,\n        Organization organization,\n        User user,\n        string AuthUrl)\n    {\n        // Arrange\n        SetUpProperOrganizationDuoUniversalTokenService(BuildDuoClient(), organization, sutProvider);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .GenerateAuthUrl(Arg.Any<Duo.Client>(), Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(), user)\n            .Returns(AuthUrl);\n\n        // Act\n        var result = await sutProvider.Sut.GenerateAsync(organization, user);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(AuthUrl, result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GenerateAsync_ClientNull_ReturnsNull(\n        SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,\n        Organization organization,\n        User user)\n    {\n        // Arrange\n        SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);\n\n        // Act\n        var result = await sutProvider.Sut.GenerateAsync(organization, user);\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_TokenValid_ReturnsTrue(\n        SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,\n        Organization organization,\n        User user,\n        string token)\n    {\n        // Arrange\n        SetUpProperOrganizationDuoUniversalTokenService(BuildDuoClient(), organization, sutProvider);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .RequestDuoValidationAsync(Arg.Any<Duo.Client>(), Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(), user, token)\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(token, organization, user);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateAsync_ClientNull_ReturnsFalse(\n    SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,\n    Organization organization,\n    User user,\n    string token)\n    {\n        // Arrange\n        SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n            .RequestDuoValidationAsync(Arg.Any<Duo.Client>(), Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(), user, token)\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateAsync(token, organization, user);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    /// <summary>\n    /// Ensures that the IDuoUniversalTokenService is properly setup for the test.\n    /// This ensures that the private GetDuoClientAsync, and GetDuoTwoFactorProvider\n    /// methods will return true enabling the test to execute on the correct path.\n    ///\n    /// BitAutoData cannot create the Duo.Client since it does not have a public constructor\n    /// so we have to use the ClientBUilder(), the client is not used meaningfully in the tests.\n    /// </summary>\n    /// <param name=\"user\">user from calling test</param>\n    /// <param name=\"sutProvider\">self</param>\n    private void SetUpProperOrganizationDuoUniversalTokenService(\n        Duo.Client client, Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)\n    {\n        organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();\n        organization.Enabled = true;\n        organization.Use2fa = true;\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n                .HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())\n                .Returns(true);\n\n        sutProvider.GetDependency<IDuoUniversalTokenService>()\n                .BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())\n                .Returns(client);\n    }\n\n    private Duo.Client BuildDuoClient()\n    {\n        var clientId = new string('c', 20);\n        var clientSecret = new string('s', 40);\n        return new Duo.ClientBuilder(clientId, clientSecret, \"api-abcd1234.duosecurity.com\", \"redirectUrl\").Build();\n    }\n\n    private string GetTwoFactorOrganizationDuoProviderJson()\n    {\n        return\n            \"{\\\"6\\\":{\\\"Enabled\\\":true,\\\"MetaData\\\":{\\\"ClientSecret\\\":\\\"secretClientSecret\\\",\\\"ClientId\\\":\\\"clientId\\\",\\\"Host\\\":\\\"example.com\\\"}}}\";\n    }\n\n    private string GetTwoFactorOrganizationDuoProviderNotEnabledJson()\n    {\n        return\n            \"{\\\"6\\\":{\\\"Enabled\\\":false,\\\"MetaData\\\":{\\\"ClientSecret\\\":\\\"secretClientSecret\\\",\\\"ClientId\\\":\\\"clientId\\\",\\\"Host\\\":\\\"example.com\\\"}}}\";\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Identity/OtpTokenProviderTests.cs",
    "content": "﻿using System.Text;\nusing Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Options;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Identity;\n\n[SutProviderCustomize]\npublic class OtpTokenProviderTests\n{\n    private readonly string _defaultTokenProviderName = \"DefaultOtpProvider\";\n\n    private readonly DefaultOtpTokenProviderOptions _defaultOtpTokenProviderOptions = new()\n    {\n        TokenLength = 6,\n        TokenAlpha = false,\n        TokenNumeric = true\n    };\n\n    [Theory, BitAutoData]\n    public async Task GenerateTokenAsync_Success_ReturnsToken(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>()\n            .Value.Returns(_defaultOtpTokenProviderOptions);\n        sutProvider.Create();\n\n        // Act\n        var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.NotEmpty(result);\n        Assert.Equal(6, result.Length); // Default length\n        Assert.True(result.All(char.IsDigit)); // Default is numeric only\n\n        // Verify cache was called with correct key\n        var expectedCacheKey = $\"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}\";\n        await sutProvider.GetDependency<IDistributedCache>()\n            .Received(1)\n            .SetAsync(expectedCacheKey, Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GenerateTokenAsync_CustomConfiguration_ReturnsCorrectFormat(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string tokenProviderName,\n        string purpose,\n        string uniqueIdentifier)\n    {\n        // Arrange\n        var otpConfig = new DefaultOtpTokenProviderOptions\n        {\n            TokenLength = 8,\n            TokenAlpha = true,\n            TokenNumeric = true\n        };\n\n        sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>()\n            .Value.Returns(otpConfig);\n        sutProvider.Create();\n\n        // Act\n        var result = await sutProvider.Sut.GenerateTokenAsync(tokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(8, result.Length);\n        Assert.Contains(result, char.IsLetterOrDigit);\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task GenerateTokenAsync_NumericOnly_ReturnsOnlyDigits(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier)\n    {\n        // Arrange\n        var otpConfig = new DefaultOtpTokenProviderOptions\n        {\n            TokenLength = 10,\n            TokenAlpha = false,\n            TokenNumeric = true\n        };\n\n        sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>()\n            .Value.Returns(otpConfig);\n        sutProvider.Create();\n\n        // Act\n        var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.Equal(10, result.Length);\n        Assert.True(result.All(char.IsDigit));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_ValidToken_ReturnsTrue(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier,\n        string token)\n    {\n        // Arrange\n        var expectedCacheKey = $\"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}\";\n        var tokenBytes = Encoding.UTF8.GetBytes(token);\n\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(expectedCacheKey)\n            .Returns(tokenBytes);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.True(result);\n\n        // Verify token was removed from cache after successful validation\n        await sutProvider.GetDependency<IDistributedCache>()\n            .Received(1)\n            .RemoveAsync(expectedCacheKey);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_InvalidToken_ReturnsFalse(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier,\n        string token,\n        string wrongToken)\n    {\n        // Arrange\n        var expectedCacheKey = $\"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}\";\n        var tokenBytes = Encoding.UTF8.GetBytes(wrongToken); // Different token in cache\n\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(expectedCacheKey)\n            .Returns(tokenBytes);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.False(result);\n\n        // Verify token was NOT removed from cache for invalid validation\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .RemoveAsync(expectedCacheKey);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_TokenNotFound_ReturnsFalse(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier,\n        string token)\n    {\n        // Arrange\n        var expectedCacheKey = $\"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}\";\n\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(expectedCacheKey)\n            .Returns((byte[])null); // Token not found in cache\n\n        // Act\n        var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.False(result);\n\n        // Verify removal was not attempted\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_EmptyToken_ReturnsFalse(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier)\n    {\n        // Act\n        var result = await sutProvider.Sut.ValidateTokenAsync(\"\", _defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_NullToken_ReturnsFalse(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier)\n    {\n        // Act\n        var result = await sutProvider.Sut.ValidateTokenAsync(null, _defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    // Tests for null/empty purpose and uniqueIdentifier parameters\n    [Theory, BitAutoData]\n    public async Task GenerateTokenAsync_NullPurpose_ReturnsNull(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string uniqueIdentifier)\n    {\n        // Act\n        var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, null, uniqueIdentifier);\n\n        // Assert\n        Assert.Null(result);\n\n        // Verify cache was not called\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GenerateTokenAsync_EmptyPurpose_ReturnsNull(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string uniqueIdentifier)\n    {\n        // Act\n        var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, \"\", uniqueIdentifier);\n\n        // Assert\n        Assert.Null(result);\n\n        // Verify cache was not called\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GenerateTokenAsync_NullUniqueIdentifier_ReturnsNull(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose)\n    {\n        // Act\n        var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, null);\n\n        // Assert\n        Assert.Null(result);\n\n        // Verify cache was not called\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GenerateTokenAsync_EmptyUniqueIdentifier_ReturnsNull(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose)\n    {\n        // Act\n        var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, \"\");\n\n        // Assert\n        Assert.Null(result);\n\n        // Verify cache was not called\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_NullPurpose_ReturnsFalse(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string token,\n        string uniqueIdentifier)\n    {\n        // Act\n        var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, null, uniqueIdentifier);\n\n        // Assert\n        Assert.False(result);\n\n        // Verify cache was not called\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .GetAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_EmptyPurpose_ReturnsFalse(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string token,\n        string uniqueIdentifier)\n    {\n        // Act\n        var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, \"\", uniqueIdentifier);\n\n        // Assert\n        Assert.False(result);\n\n        // Verify cache was not called\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .GetAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_NullUniqueIdentifier_ReturnsFalse(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string token,\n        string purpose)\n    {\n        // Act\n        var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, null);\n\n        // Assert\n        Assert.False(result);\n\n        // Verify cache was not called\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .GetAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_EmptyUniqueIdentifier_ReturnsFalse(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string token,\n        string purpose)\n    {\n        // Act\n        var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, \"\");\n\n        // Assert\n        Assert.False(result);\n\n        // Verify cache was not called\n        await sutProvider.GetDependency<IDistributedCache>()\n            .DidNotReceive()\n            .GetAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GenerateTokenAsync_OverwritesExistingToken(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>()\n            .Value.Returns(_defaultOtpTokenProviderOptions);\n        sutProvider.Create();\n\n        // Act - Generate token twice with same parameters\n        var firstToken = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier);\n        var secondToken = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.NotEqual(firstToken, secondToken); // Should be different tokens\n\n        var expectedCacheKey = $\"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}\";\n        await sutProvider.GetDependency<IDistributedCache>()\n            .Received(2) // Called twice - once for each generation\n            .SetAsync(expectedCacheKey, Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task CacheKeyFormat_IsCorrect(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>()\n            .Value.Returns(_defaultOtpTokenProviderOptions);\n        sutProvider.Create();\n\n        // Act\n        await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        var expectedCacheKey = $\"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}\";\n        await sutProvider.GetDependency<IDistributedCache>()\n            .Received(1)\n            .SetAsync(expectedCacheKey, Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateTokenAsync_CaseSensitive(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier)\n    {\n        // Arrange\n        var token = \"ABC123\";\n        var expectedCacheKey = $\"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}\";\n        var tokenBytes = Encoding.UTF8.GetBytes(token);\n\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(expectedCacheKey)\n            .Returns(tokenBytes);\n\n        // Act & Assert\n        var validResult = await sutProvider.Sut.ValidateTokenAsync(\"ABC123\", _defaultTokenProviderName, purpose, uniqueIdentifier);\n        Assert.True(validResult);\n\n        // Reset the cache mock to return the token again\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(expectedCacheKey)\n            .Returns(tokenBytes);\n\n        var invalidResult = await sutProvider.Sut.ValidateTokenAsync(\"abc123\", _defaultTokenProviderName, purpose, uniqueIdentifier);\n        Assert.False(invalidResult);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RoundTrip_GenerateAndValidate_Success(\n        SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider,\n        string purpose,\n        string uniqueIdentifier)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>()\n            .Value.Returns(_defaultOtpTokenProviderOptions);\n        sutProvider.Create();\n\n        var expectedCacheKey = $\"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}\";\n        byte[] storedToken = null;\n\n        // Setup cache to capture stored token and return it on get\n        sutProvider.GetDependency<IDistributedCache>()\n            .When(x => x.SetAsync(expectedCacheKey, Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>()))\n            .Do(callInfo => storedToken = callInfo.ArgAt<byte[]>(1));\n\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(expectedCacheKey)\n            .Returns(callInfo => storedToken);\n\n        // Act\n        var generatedToken = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier);\n        var isValid = await sutProvider.Sut.ValidateTokenAsync(generatedToken, _defaultTokenProviderName, purpose, uniqueIdentifier);\n\n        // Assert\n        Assert.True(isValid);\n        Assert.NotNull(generatedToken);\n        Assert.NotEmpty(generatedToken);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/IdentityServer/TokenRetrievalTests.cs",
    "content": "﻿using Bit.Core.Auth.IdentityServer;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.Primitives;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.IdentityServer;\n\npublic class TokenRetrievalTests\n{\n    private readonly Func<HttpRequest, string> _sut = TokenRetrieval.FromAuthorizationHeaderOrQueryString();\n\n    [Fact]\n    public void RetrieveToken_FromHeader_ReturnsToken()\n    {\n        // Arrange\n        var headers = new HeaderDictionary\n        {\n            { \"Authorization\", \"Bearer test_value\" },\n            { \"X-Test-Header\", \"random_value\" }\n        };\n\n        var request = Substitute.For<HttpRequest>();\n\n        request.Headers.Returns(headers);\n\n        // Act\n        var token = _sut(request);\n\n        // Assert\n        Assert.Equal(\"test_value\", token);\n    }\n\n    [Fact]\n    public void RetrieveToken_FromQueryString_ReturnsToken()\n    {\n        // Arrange\n        var queryString = new Dictionary<string, StringValues>\n        {\n            { \"access_token\", \"test_value\" },\n            { \"test-query\", \"random_value\" }\n        };\n\n        var request = Substitute.For<HttpRequest>();\n        request.Query.Returns(new QueryCollection(queryString));\n\n        // Act\n        var token = _sut(request);\n\n        // Assert\n        Assert.Equal(\"test_value\", token);\n    }\n\n    [Fact]\n    public void RetrieveToken_HasBoth_ReturnsHeaderToken()\n    {\n        // Arrange\n        var queryString = new Dictionary<string, StringValues>\n        {\n            { \"access_token\", \"query_string_token\" },\n            { \"test-query\", \"random_value\" }\n        };\n\n        var headers = new HeaderDictionary\n        {\n            { \"Authorization\", \"Bearer header_token\" },\n            { \"X-Test-Header\", \"random_value\" }\n        };\n\n        var request = Substitute.For<HttpRequest>();\n        request.Headers.Returns(headers);\n        request.Query.Returns(new QueryCollection(queryString));\n\n        // Act\n        var token = _sut(request);\n\n        // Assert\n        Assert.Equal(\"header_token\", token);\n    }\n\n    [Fact]\n    public void RetrieveToken_NoToken_ReturnsNull()\n    {\n        // Arrange\n        var request = Substitute.For<HttpRequest>();\n\n        // Act\n        var token = _sut(request);\n\n        // Assert\n        Assert.Null(token);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstantsSnapshotTests.cs",
    "content": "﻿using Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Models.Api.Request.Accounts;\n\n/// <summary>\n/// Snapshot tests to ensure the string constants in <see cref=\"MarketingInitiativeConstants\"/> do not change unintentionally.\n/// If you intentionally change any of these values, please update the tests to reflect the new expected values.\n/// </summary>\npublic class MarketingInitiativeConstantsSnapshotTests\n{\n    [Fact]\n    public void MarketingInitiativeConstants_HaveCorrectValues()\n    {\n        // Assert\n        Assert.Equal(\"premium\", MarketingInitiativeConstants.Premium);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModelTests.cs",
    "content": "﻿using Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Api.Request;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Models.Api.Request.Accounts;\n\npublic class RegisterFinishRequestModelTests\n{\n    private static List<System.ComponentModel.DataAnnotations.ValidationResult> Validate(RegisterFinishRequestModel model)\n    {\n        var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>();\n        System.ComponentModel.DataAnnotations.Validator.TryValidateObject(\n            model,\n            new System.ComponentModel.DataAnnotations.ValidationContext(model),\n            results,\n            true);\n        return results;\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void GetTokenType_Returns_EmailVerification(string email, string masterPasswordHash,\n        string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string emailVerificationToken)\n    {\n        // Arrange\n        var model = new RegisterFinishRequestModel\n        {\n            Email = email,\n            MasterPasswordHash = masterPasswordHash,\n            UserSymmetricKey = userSymmetricKey,\n            UserAsymmetricKeys = userAsymmetricKeys,\n            Kdf = kdf,\n            KdfIterations = kdfIterations,\n            EmailVerificationToken = emailVerificationToken\n        };\n\n        // Act\n        Assert.Equal(RegisterFinishTokenType.EmailVerification, model.GetTokenType());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void GetTokenType_Returns_OrganizationInvite(string email, string masterPasswordHash,\n        string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string orgInviteToken, Guid organizationUserId)\n    {\n        // Arrange\n        var model = new RegisterFinishRequestModel\n        {\n            Email = email,\n            MasterPasswordHash = masterPasswordHash,\n            UserSymmetricKey = userSymmetricKey,\n            UserAsymmetricKeys = userAsymmetricKeys,\n            Kdf = kdf,\n            KdfIterations = kdfIterations,\n            OrgInviteToken = orgInviteToken,\n            OrganizationUserId = organizationUserId\n        };\n\n        // Act\n        Assert.Equal(RegisterFinishTokenType.OrganizationInvite, model.GetTokenType());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void GetTokenType_Returns_OrgSponsoredFreeFamilyPlan(string email, string masterPasswordHash,\n        string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string orgSponsoredFreeFamilyPlanToken)\n    {\n        // Arrange\n        var model = new RegisterFinishRequestModel\n        {\n            Email = email,\n            MasterPasswordHash = masterPasswordHash,\n            UserSymmetricKey = userSymmetricKey,\n            UserAsymmetricKeys = userAsymmetricKeys,\n            Kdf = kdf,\n            KdfIterations = kdfIterations,\n            OrgSponsoredFreeFamilyPlanToken = orgSponsoredFreeFamilyPlanToken\n        };\n\n        // Act\n        Assert.Equal(RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan, model.GetTokenType());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void GetTokenType_Returns_EmergencyAccessInvite(string email, string masterPasswordHash,\n        string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)\n    {\n        // Arrange\n        var model = new RegisterFinishRequestModel\n        {\n            Email = email,\n            MasterPasswordHash = masterPasswordHash,\n            UserSymmetricKey = userSymmetricKey,\n            UserAsymmetricKeys = userAsymmetricKeys,\n            Kdf = kdf,\n            KdfIterations = kdfIterations,\n            AcceptEmergencyAccessInviteToken = acceptEmergencyAccessInviteToken,\n            AcceptEmergencyAccessId = acceptEmergencyAccessId\n        };\n\n        // Act\n        Assert.Equal(RegisterFinishTokenType.EmergencyAccessInvite, model.GetTokenType());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void GetTokenType_Returns_ProviderInvite(string email, string masterPasswordHash,\n        string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string providerInviteToken, Guid providerUserId)\n    {\n        // Arrange\n        var model = new RegisterFinishRequestModel\n        {\n            Email = email,\n            MasterPasswordHash = masterPasswordHash,\n            UserSymmetricKey = userSymmetricKey,\n            UserAsymmetricKeys = userAsymmetricKeys,\n            Kdf = kdf,\n            KdfIterations = kdfIterations,\n            ProviderInviteToken = providerInviteToken,\n            ProviderUserId = providerUserId\n        };\n\n        // Act\n        Assert.Equal(RegisterFinishTokenType.ProviderInvite, model.GetTokenType());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void GetTokenType_Returns_Invalid(string email, string masterPasswordHash,\n        string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations)\n    {\n        // Arrange\n        var model = new RegisterFinishRequestModel\n        {\n            Email = email,\n            MasterPasswordHash = masterPasswordHash,\n            UserSymmetricKey = userSymmetricKey,\n            UserAsymmetricKeys = userAsymmetricKeys,\n            Kdf = kdf,\n            KdfIterations = kdfIterations\n        };\n\n        // Act\n        var result = Assert.Throws<InvalidOperationException>(() => model.GetTokenType());\n        Assert.Equal(\"Invalid token type.\", result.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void ToUser_Returns_User(string email, string masterPasswordHash, string masterPasswordHint,\n        string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations,\n        int? kdfMemory, int? kdfParallelism)\n    {\n        // Arrange\n        var model = new RegisterFinishRequestModel\n        {\n            Email = email,\n            MasterPasswordHash = masterPasswordHash,\n            MasterPasswordHint = masterPasswordHint,\n            UserSymmetricKey = userSymmetricKey,\n            UserAsymmetricKeys = userAsymmetricKeys,\n            Kdf = kdf,\n            KdfIterations = kdfIterations,\n            KdfMemory = kdfMemory,\n            KdfParallelism = kdfParallelism\n        };\n\n        // Act\n        var result = model.ToUser();\n\n        // Assert\n        Assert.Equal(email, result.Email);\n        Assert.Equal(masterPasswordHint, result.MasterPasswordHint);\n        Assert.Equal(kdf, result.Kdf);\n        Assert.Equal(kdfIterations, result.KdfIterations);\n        Assert.Equal(kdfMemory, result.KdfMemory);\n        Assert.Equal(kdfParallelism, result.KdfParallelism);\n        Assert.Equal(userSymmetricKey, result.Key);\n        Assert.Equal(userAsymmetricKeys.PublicKey, result.PublicKey);\n        Assert.Equal(userAsymmetricKeys.EncryptedPrivateKey, result.PrivateKey);\n    }\n\n    [Fact]\n    public void Validate_WhenBothAuthAndRootHashProvidedButNotEqual_ReturnsMismatchError()\n    {\n        var model = new RegisterFinishRequestModel\n        {\n            Email = \"user@example.com\",\n            MasterPasswordHash = \"root-hash\",\n            UserAsymmetricKeys = new KeysRequestModel { PublicKey = \"pk\", EncryptedPrivateKey = \"sk\" },\n            // Provide both unlock and authentication with valid KDF so only the mismatch rule fires\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },\n                MasterKeyWrappedUserKey = \"wrapped\",\n                Salt = \"salt\"\n            },\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },\n                MasterPasswordAuthenticationHash = \"auth-hash\", // different than root\n                Salt = \"salt\"\n            },\n            // Provide any valid token so we don't fail token validation\n            EmailVerificationToken = \"token\"\n        };\n\n        var results = Validate(model);\n\n        Assert.Contains(results, r =>\n            r.ErrorMessage == $\"{nameof(MasterPasswordAuthenticationDataRequestModel.MasterPasswordAuthenticationHash)} and root level {nameof(RegisterFinishRequestModel.MasterPasswordHash)} provided and are not equal. Only provide one.\");\n    }\n\n    [Fact]\n    public void Validate_WhenAuthProvidedButUnlockMissing_ReturnsUnlockMissingError()\n    {\n        var model = new RegisterFinishRequestModel\n        {\n            Email = \"user@example.com\",\n            UserAsymmetricKeys = new KeysRequestModel { PublicKey = \"pk\", EncryptedPrivateKey = \"sk\" },\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },\n                MasterPasswordAuthenticationHash = \"auth-hash\",\n                Salt = \"salt\"\n            },\n            EmailVerificationToken = \"token\"\n        };\n\n        var results = Validate(model);\n\n        Assert.Contains(results, r => r.ErrorMessage == \"MasterPasswordUnlock not found on RequestModel\");\n    }\n\n    [Fact]\n    public void Validate_WhenUnlockProvidedButAuthMissing_ReturnsAuthMissingError()\n    {\n        var model = new RegisterFinishRequestModel\n        {\n            Email = \"user@example.com\",\n            UserAsymmetricKeys = new KeysRequestModel { PublicKey = \"pk\", EncryptedPrivateKey = \"sk\" },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },\n                MasterKeyWrappedUserKey = \"wrapped\",\n                Salt = \"salt\"\n            },\n            EmailVerificationToken = \"token\"\n        };\n\n        var results = Validate(model);\n\n        Assert.Contains(results, r => r.ErrorMessage == \"MasterPasswordAuthentication not found on RequestModel\");\n    }\n\n    [Fact]\n    public void Validate_WhenNeitherAuthNorUnlock_AndRootKdfMissing_ReturnsBothRootKdfErrors()\n    {\n        var model = new RegisterFinishRequestModel\n        {\n            Email = \"user@example.com\",\n            UserAsymmetricKeys = new KeysRequestModel { PublicKey = \"pk\", EncryptedPrivateKey = \"sk\" },\n            // No MasterPasswordUnlock, no MasterPasswordAuthentication\n            // No root Kdf and KdfIterations to trigger both errors\n            EmailVerificationToken = \"token\"\n        };\n\n        var results = Validate(model);\n\n        Assert.Contains(results, r => r.ErrorMessage == $\"{nameof(RegisterFinishRequestModel.Kdf)} not found on RequestModel\");\n        Assert.Contains(results, r => r.ErrorMessage == $\"{nameof(RegisterFinishRequestModel.KdfIterations)} not found on RequestModel\");\n    }\n\n    [Fact]\n    public void Validate_WhenAuthAndRootHashBothMissing_ReturnsMissingHashErrorOnly()\n    {\n        var model = new RegisterFinishRequestModel\n        {\n            Email = \"user@example.com\",\n            UserAsymmetricKeys = new KeysRequestModel { PublicKey = \"pk\", EncryptedPrivateKey = \"sk\" },\n            // Both MasterPasswordAuthentication and MasterPasswordHash are missing\n            MasterPasswordAuthentication = null,\n            MasterPasswordHash = null,\n            // Provide valid root KDF to avoid root KDF errors\n            Kdf = KdfType.PBKDF2_SHA256,\n            KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,\n            EmailVerificationToken = \"token\" // avoid token error\n        };\n\n        var results = Validate(model);\n\n        // Only the new missing hash error should be present\n        Assert.Single(results);\n        Assert.Equal($\"{nameof(MasterPasswordAuthenticationDataRequestModel.MasterPasswordAuthenticationHash)} and {nameof(RegisterFinishRequestModel.MasterPasswordHash)} not found on request, one needs to be defined.\", results[0].ErrorMessage);\n        Assert.Contains(nameof(MasterPasswordAuthenticationDataRequestModel.MasterPasswordAuthenticationHash), results[0].MemberNames);\n        Assert.Contains(nameof(RegisterFinishRequestModel.MasterPasswordHash), results[0].MemberNames);\n    }\n\n    [Fact]\n    public void Validate_WhenAllFieldsValidWithSubModels_IsValid()\n    {\n        var model = new RegisterFinishRequestModel\n        {\n            Email = \"user@example.com\",\n            UserAsymmetricKeys = new KeysRequestModel { PublicKey = \"pk\", EncryptedPrivateKey = \"sk\" },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },\n                MasterKeyWrappedUserKey = \"wrapped\",\n                Salt = \"salt\"\n            },\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },\n                MasterPasswordAuthenticationHash = \"auth-hash\",\n                Salt = \"salt\"\n            },\n            EmailVerificationToken = \"token\"\n        };\n\n        var results = Validate(model);\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_WhenNoValidRegistrationTokenProvided_ReturnsTokenErrorOnly()\n    {\n        var model = new RegisterFinishRequestModel\n        {\n            Email = \"user@example.com\",\n            UserAsymmetricKeys = new KeysRequestModel { PublicKey = \"pk\", EncryptedPrivateKey = \"sk\" },\n            MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel\n            {\n                Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },\n                MasterKeyWrappedUserKey = \"wrapped\",\n                Salt = \"salt\"\n            },\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel\n            {\n                Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },\n                MasterPasswordAuthenticationHash = \"auth-hash\",\n                Salt = \"salt\"\n            }\n            // No token fields set\n        };\n\n        var results = Validate(model);\n\n        Assert.Single(results);\n        Assert.Equal(\"No valid registration token provided\", results[0].ErrorMessage);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Models/Business/Tokenables/EmergencyAccessInviteTokenableTests.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Tokens;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Models.Business.Tokenables;\n\npublic class EmergencyAccessInviteTokenableTests\n{\n    [Theory, AutoData]\n    public void SerializationSetsCorrectDateTime(EmergencyAccess emergencyAccess)\n    {\n        var token = new EmergencyAccessInviteTokenable(emergencyAccess, 2);\n        Assert.Equal(Tokenable.FromToken<EmergencyAccessInviteTokenable>(token.ToToken().ToString()).ExpirationDate,\n            token.ExpirationDate,\n            TimeSpan.FromMilliseconds(10));\n    }\n\n    [Fact]\n    public void IsInvalidIfIdentifierIsWrong()\n    {\n        var token = new EmergencyAccessInviteTokenable(DateTime.MaxValue)\n        {\n            Email = \"email\",\n            Id = Guid.NewGuid(),\n            Identifier = \"not correct\"\n        };\n\n        Assert.False(token.Valid);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Models/Business/Tokenables/OrgUserInviteTokenableTests.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\nusing Xunit;\n\n\nnamespace Bit.Core.Test.Auth.Models.Business.Tokenables;\n\n// Note: test names follow MethodName_StateUnderTest_ExpectedBehavior pattern.\npublic class OrgUserInviteTokenableTests\n{\n    // Allow a small tolerance for possible execution delays or clock precision.\n    private readonly TimeSpan _timeTolerance = TimeSpan.FromMilliseconds(10);\n\n    /// <summary>\n    /// Tests that the default constructor sets the expiration date to the expected duration.\n    /// </summary>\n    [Fact]\n    public void Constructor_DefaultInitialization_ExpirationSetToExpectedDuration()\n    {\n        var token = new OrgUserInviteTokenable();\n        var expectedExpiration = DateTime.UtcNow + OrgUserInviteTokenable.GetTokenLifetime();\n\n        Assert.True(TimesAreCloseEnough(expectedExpiration, token.ExpirationDate, _timeTolerance));\n    }\n\n    /// <summary>\n    /// Tests that the constructor sets the properties correctly from a valid OrganizationUser object.\n    /// </summary>\n    [Theory, AutoData]\n    public void Constructor_ValidOrgUser_PropertiesSetFromOrgUser(OrganizationUser orgUser)\n    {\n        var token = new OrgUserInviteTokenable(orgUser);\n\n        Assert.Equal(orgUser.Id, token.OrgUserId);\n        Assert.Equal(orgUser.Email, token.OrgUserEmail);\n    }\n\n    /// <summary>\n    /// Tests that the constructor sets the properties to default values when given a null OrganizationUser object.\n    /// </summary>\n    [Fact]\n    public void Constructor_NullOrgUser_PropertiesSetToDefault()\n    {\n        var token = new OrgUserInviteTokenable(null);\n\n        Assert.Equal(default, token.OrgUserId);\n        Assert.Equal(default, token.OrgUserEmail);\n    }\n\n    /// <summary>\n    /// Tests that a custom expiration date is preserved after token initialization.\n    /// </summary>\n    [Fact]\n    public void Constructor_CustomExpirationDate_ExpirationMatchesProvidedValue()\n    {\n        var customExpiration = DateTime.UtcNow.AddHours(3);\n        var token = new OrgUserInviteTokenable\n        {\n            ExpirationDate = customExpiration\n        };\n\n        Assert.True(TimesAreCloseEnough(customExpiration, token.ExpirationDate, _timeTolerance));\n    }\n\n    /// <summary>\n    ///  Tests the validity of a token initialized with a null org user.\n    /// </summary>\n    [Fact]\n    public void Valid_NullOrgUser_ReturnsFalse()\n    {\n        var token = new OrgUserInviteTokenable(null);\n\n        Assert.False(token.Valid);\n    }\n\n    /// <summary>\n    /// Tests the validity of a token with a non-matching identifier.\n    /// </summary>\n    [Fact]\n    public void Valid_WrongIdentifier_ReturnsFalse()\n    {\n        var token = new OrgUserInviteTokenable\n        {\n            Identifier = \"IncorrectIdentifier\"\n        };\n\n        Assert.False(token.Valid);\n    }\n\n    /// <summary>\n    /// Tests the validity of the token when the OrgUserId is set to default.\n    /// </summary>\n    [Fact]\n    public void Valid_DefaultOrgUserId_ReturnsFalse()\n    {\n        var token = new OrgUserInviteTokenable\n        {\n            OrgUserId = default // Guid.Empty\n        };\n\n        Assert.False(token.Valid);\n    }\n\n    /// <summary>\n    /// Tests the validity of the token when the OrgUserEmail is null or empty.\n    /// </summary>\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    public void Valid_NullOrEmptyOrgUserEmail_ReturnsFalse(string? email)\n    {\n        var token = new OrgUserInviteTokenable\n        {\n            OrgUserEmail = email\n        };\n\n        Assert.False(token.Valid);\n    }\n\n\n    /// <summary>\n    /// Tests the validity of the token when the token is expired.\n    /// </summary>\n    [Fact]\n    public void Valid_ExpiredToken_ReturnsFalse()\n    {\n        var expiredDate = DateTime.UtcNow.AddHours(-3);\n        var token = new OrgUserInviteTokenable\n        {\n            ExpirationDate = expiredDate\n        };\n\n        Assert.False(token.Valid);\n    }\n\n\n    /// <summary>\n    /// Tests the TokenIsValid method when given a null OrganizationUser object.\n    /// </summary>\n    [Fact]\n    public void TokenIsValid_NullOrgUser_ReturnsFalse()\n    {\n        var token = new OrgUserInviteTokenable(null);\n\n        Assert.False(token.TokenIsValid(null));\n    }\n\n    /// <summary>\n    /// Tests the TokenIsValid method when the OrgUserId does not match.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_WrongUserId_ReturnsFalse(OrganizationUser orgUser)\n    {\n        var token = new OrgUserInviteTokenable(orgUser)\n        {\n            OrgUserId = Guid.NewGuid() // Force a different ID\n        };\n\n        Assert.False(token.TokenIsValid(orgUser));\n    }\n\n    /// <summary>\n    /// Tests the TokenIsValid method when the OrgUserEmail does not match.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_WrongEmail_ReturnsFalse(OrganizationUser orgUser)\n    {\n        var token = new OrgUserInviteTokenable(orgUser)\n        {\n            OrgUserEmail = \"wrongemail@example.com\" // Force a different email\n        };\n\n        Assert.False(token.TokenIsValid(orgUser));\n    }\n\n    /// <summary>\n    /// Tests the TokenIsValid method when both OrgUserId and OrgUserEmail match.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_MatchingUserIdAndEmail_ReturnsTrue(OrganizationUser orgUser)\n    {\n        var token = new OrgUserInviteTokenable(orgUser);\n\n        Assert.True(token.TokenIsValid(orgUser));\n    }\n\n    /// <summary>\n    /// Tests the TokenIsValid method to ensure email comparison is case-insensitive.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_EmailCaseInsensitiveComparison_ReturnsTrue(OrganizationUser orgUser)\n    {\n        var token = new OrgUserInviteTokenable(orgUser);\n\n        // Modify the orgUser's email case\n        orgUser.Email = orgUser.Email.ToUpperInvariant();\n\n        Assert.True(token.TokenIsValid(orgUser));\n    }\n\n\n    /// <summary>\n    /// Tests the TokenIsValid method when the token is expired.\n    /// Should return true as TokenIsValid only validates token data -- not token expiration.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_ExpiredToken_ReturnsTrue(OrganizationUser orgUser)\n    {\n        var expiredDate = DateTime.UtcNow.AddHours(-3);\n        var token = new OrgUserInviteTokenable(orgUser)\n        {\n            ExpirationDate = expiredDate\n        };\n\n        Assert.True(token.TokenIsValid(orgUser));\n    }\n\n    /// <summary>\n    /// Tests the deserialization of a token to ensure that the ExpirationDate is preserved.\n    /// </summary>\n    [Theory, AutoData]\n    public void FromToken_SerializedToken_PreservesExpirationDate(OrganizationUser orgUser)\n    {\n        // Arbitrary time for testing\n        var expectedDateTime = DateTime.UtcNow.AddHours(-3);\n        var token = new OrgUserInviteTokenable(orgUser)\n        {\n            ExpirationDate = expectedDateTime\n        };\n\n        var result = Tokenable.FromToken<OrgUserInviteTokenable>(token.ToToken());\n\n        Assert.True(TimesAreCloseEnough(expectedDateTime, result.ExpirationDate, _timeTolerance));\n    }\n\n    /// <summary>\n    /// Tests the deserialization of a token to ensure that the OrgUserId property is preserved.\n    /// </summary>\n    [Theory, AutoData]\n    public void FromToken_SerializedToken_PreservesOrgUserId(OrganizationUser orgUser)\n    {\n        var token = new OrgUserInviteTokenable(orgUser);\n        var result = Tokenable.FromToken<OrgUserInviteTokenable>(token.ToToken());\n        Assert.Equal(orgUser.Id, result.OrgUserId);\n    }\n\n    /// <summary>\n    /// Tests the deserialization of a token to ensure that the OrgUserEmail property is preserved.\n    /// </summary>\n    [Theory, AutoData]\n    public void FromToken_SerializedToken_PreservesOrgUserEmail(OrganizationUser orgUser)\n    {\n        var token = new OrgUserInviteTokenable(orgUser);\n        var result = Tokenable.FromToken<OrgUserInviteTokenable>(token.ToToken());\n        Assert.Equal(orgUser.Email, result.OrgUserEmail);\n    }\n\n    private bool TimesAreCloseEnough(DateTime time1, DateTime time2, TimeSpan tolerance)\n    {\n        return (time1 - time2).Duration() < tolerance;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Models/Business/Tokenables/RegistrationEmailVerificationTokenableTests.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Bit.Core.Tokens;\n\nnamespace Bit.Core.Test.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Xunit;\n\npublic class RegistrationEmailVerificationTokenableTests\n{\n    // Allow a small tolerance for possible execution delays or clock precision to avoid flaky tests.\n    private static readonly TimeSpan _timeTolerance = TimeSpan.FromMilliseconds(10);\n\n    /// <summary>\n    /// Tests the default constructor behavior when passed null/default values.\n    /// </summary>\n    [Fact]\n    public void Constructor_NullEmail_ThrowsArgumentNullException()\n    {\n        Assert.Throws<ArgumentNullException>(() => new RegistrationEmailVerificationTokenable(null, null, default));\n    }\n\n    /// <summary>\n    /// Tests the default constructor behavior when passed required values but null values for optional props.\n    /// </summary>\n    [Theory, AutoData]\n    public void Constructor_NullOptionalProps_PropertiesSetToDefault(string email)\n    {\n        var token = new RegistrationEmailVerificationTokenable(email, null, default);\n\n        Assert.Equal(email, token.Email);\n        Assert.Equal(default, token.Name);\n        Assert.Equal(default, token.ReceiveMarketingEmails);\n    }\n\n    /// <summary>\n    /// Tests that when a valid inputs are provided to the constructor, the resulting token properties match the user.\n    /// </summary>\n    [Theory, AutoData]\n    public void Constructor_ValidInputs_PropertiesSetFromInputs(string email, string name, bool receiveMarketingEmails)\n    {\n        var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);\n\n        Assert.Equal(email, token.Email);\n        Assert.Equal(name, token.Name);\n        Assert.Equal(receiveMarketingEmails, token.ReceiveMarketingEmails);\n    }\n\n    /// <summary>\n    /// Tests the default expiration behavior immediately after initialization.\n    /// </summary>\n    [Fact]\n    public void Constructor_AfterInitialization_ExpirationSetToExpectedDuration()\n    {\n        var token = new RegistrationEmailVerificationTokenable();\n        var expectedExpiration = DateTime.UtcNow + SsoEmail2faSessionTokenable.GetTokenLifetime();\n\n        Assert.True(expectedExpiration - token.ExpirationDate < _timeTolerance);\n    }\n\n    /// <summary>\n    /// Tests that a custom expiration date is preserved after token initialization.\n    /// </summary>\n    [Fact]\n    public void Constructor_CustomExpirationDate_ExpirationMatchesProvidedValue()\n    {\n        var customExpiration = DateTime.UtcNow.AddHours(3);\n        var token = new RegistrationEmailVerificationTokenable\n        {\n            ExpirationDate = customExpiration\n        };\n\n        Assert.True((customExpiration - token.ExpirationDate).Duration() < _timeTolerance);\n    }\n\n\n    /// <summary>\n    /// Tests the validity of a token with a non-matching identifier.\n    /// </summary>\n    [Theory, AutoData]\n    public void Valid_WrongIdentifier_ReturnsFalse(string email, string name, bool receiveMarketingEmails)\n    {\n        var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails) { Identifier = \"InvalidIdentifier\" };\n\n        Assert.False(token.Valid);\n    }\n\n    /// <summary>\n    /// Tests the token validity when the token is initialized with valid inputs.\n    /// </summary>\n    [Theory, AutoData]\n    public void Valid_ValidInputs_ReturnsTrue(string email, string name, bool receiveMarketingEmails)\n    {\n        var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);\n\n        Assert.True(token.Valid);\n    }\n\n    /// <summary>\n    /// Tests the token validity when an incorrect email is provided\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_WrongEmail_ReturnsFalse(string email, string name, bool receiveMarketingEmails)\n    {\n        var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);\n\n        Assert.False(token.TokenIsValid(\"wrong@email.com\"));\n    }\n\n\n    /// <summary>\n    /// Tests the deserialization of a token to ensure that the expiration date is preserved.\n    /// </summary>\n    [Theory, AutoData]\n    public void FromToken_SerializedToken_PreservesExpirationDate(string email, string name, bool receiveMarketingEmails)\n    {\n        var expectedDateTime = DateTime.UtcNow.AddHours(-5);\n        var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails)\n        {\n            ExpirationDate = expectedDateTime\n        };\n\n        var result = Tokenable.FromToken<RegistrationEmailVerificationTokenable>(token.ToToken());\n\n        Assert.Equal(expectedDateTime, result.ExpirationDate, precision: _timeTolerance);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenableTests.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Entities;\nusing Bit.Core.Tokens;\nusing Xunit;\nnamespace Bit.Core.Test.Auth.Models.Business.Tokenables;\n\n// Note: these test names follow MethodName_StateUnderTest_ExpectedBehavior pattern.\npublic class SsoEmail2faSessionTokenableTests\n{\n    // Allow a small tolerance for possible execution delays or clock precision to avoid flaky tests.\n    private static readonly TimeSpan _timeTolerance = TimeSpan.FromMilliseconds(10);\n\n    /// <summary>\n    /// Tests the default constructor behavior when passed a null user.\n    /// </summary>\n    [Fact]\n    public void Constructor_NullUser_PropertiesSetToDefault()\n    {\n        var token = new SsoEmail2faSessionTokenable(null);\n\n        Assert.Equal(default, token.Id);\n        Assert.Equal(default, token.Email);\n    }\n\n    /// <summary>\n    /// Tests that when a valid user is provided to the constructor, the resulting token properties match the user.\n    /// </summary>\n    [Theory, AutoData]\n    public void Constructor_ValidUser_PropertiesSetFromUser(User user)\n    {\n        var token = new SsoEmail2faSessionTokenable(user);\n\n        Assert.Equal(user.Id, token.Id);\n        Assert.Equal(user.Email, token.Email);\n    }\n\n    /// <summary>\n    /// Tests the default expiration behavior immediately after initialization.\n    /// </summary>\n    [Fact]\n    public void Constructor_AfterInitialization_ExpirationSetToExpectedDuration()\n    {\n        var token = new SsoEmail2faSessionTokenable();\n        var expectedExpiration = DateTime.UtcNow + SsoEmail2faSessionTokenable.GetTokenLifetime();\n\n        Assert.True(expectedExpiration - token.ExpirationDate < _timeTolerance);\n    }\n\n    /// <summary>\n    /// Tests that a custom expiration date is preserved after token initialization.\n    /// </summary>\n    [Fact]\n    public void Constructor_CustomExpirationDate_ExpirationMatchesProvidedValue()\n    {\n        var customExpiration = DateTime.UtcNow.AddHours(3);\n        var token = new SsoEmail2faSessionTokenable\n        {\n            ExpirationDate = customExpiration\n        };\n\n        Assert.True((customExpiration - token.ExpirationDate).Duration() < _timeTolerance);\n    }\n\n    /// <summary>\n    /// Tests the validity of a token initialized with a null user.\n    /// </summary>\n    [Fact]\n    public void Valid_NullUser_ReturnsFalse()\n    {\n        var token = new SsoEmail2faSessionTokenable(null);\n\n        Assert.False(token.Valid);\n    }\n\n    /// <summary>\n    /// Tests the validity of a token with a non-matching identifier.\n    /// </summary>\n    [Theory, AutoData]\n    public void Valid_WrongIdentifier_ReturnsFalse(User user)\n    {\n        var token = new SsoEmail2faSessionTokenable(user)\n        {\n            Identifier = \"not correct\"\n        };\n\n        Assert.False(token.Valid);\n    }\n\n    /// <summary>\n    /// Tests the token validity when user ID is null.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_NullUserId_ReturnsFalse(User user)\n    {\n        user.Id = default; // Guid.Empty\n        var token = new SsoEmail2faSessionTokenable(user);\n\n        Assert.False(token.TokenIsValid(user));\n    }\n\n    /// <summary>\n    /// Tests the token validity when user's email is null.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_NullEmail_ReturnsFalse(User user)\n    {\n        user.Email = null;\n        var token = new SsoEmail2faSessionTokenable(user);\n\n        Assert.False(token.TokenIsValid(user));\n    }\n\n    /// <summary>\n    /// Tests the token validity when user ID and email match the token properties.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_MatchingUserIdAndEmail_ReturnsTrue(User user)\n    {\n        var token = new SsoEmail2faSessionTokenable(user);\n\n        Assert.True(token.TokenIsValid(user));\n    }\n\n    /// <summary>\n    /// Ensures that the token is invalid when the provided user's ID doesn't match the token's ID.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_WrongUserId_ReturnsFalse(User user)\n    {\n        // Given a token initialized with a user's details\n        var token = new SsoEmail2faSessionTokenable(user);\n\n        // modify the user's ID\n        user.Id = Guid.NewGuid();\n\n        // Then the token should be considered invalid\n        Assert.False(token.TokenIsValid(user));\n    }\n\n    /// <summary>\n    /// Ensures that the token is invalid when the provided user's email doesn't match the token's email.\n    /// </summary>\n    [Theory, AutoData]\n    public void TokenIsValid_WrongEmail_ReturnsFalse(User user)\n    {\n        // Given a token initialized with a user's details\n        var token = new SsoEmail2faSessionTokenable(user);\n\n        // modify the user's email\n        user.Email = \"nonMatchingEmail@example.com\";\n\n        // Then the token should be considered invalid\n        Assert.False(token.TokenIsValid(user));\n    }\n\n    /// <summary>\n    /// Tests the deserialization of a token to ensure that the expiration date is preserved.\n    /// </summary>\n    [Theory, AutoData]\n    public void FromToken_SerializedToken_PreservesExpirationDate(User user)\n    {\n        var expectedDateTime = DateTime.UtcNow.AddHours(-5);\n        var token = new SsoEmail2faSessionTokenable(user)\n        {\n            ExpirationDate = expectedDateTime\n        };\n\n        var result = Tokenable.FromToken<SsoEmail2faSessionTokenable>(token.ToToken());\n\n        Assert.Equal(expectedDateTime, result.ExpirationDate, precision: _timeTolerance);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Models.Business.Tokenables;\n\npublic class SsoTokenableTests\n{\n    [Fact]\n    public void CanHandleNullOrganization()\n    {\n        var token = new SsoTokenable(null, default);\n\n        Assert.Equal(default, token.OrganizationId);\n        Assert.Equal(default, token.DomainHint);\n    }\n\n    [Fact]\n    public void TokenWithNullOrganizationIsInvalid()\n    {\n        var token = new SsoTokenable(null, 500)\n        {\n            ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1)\n        };\n\n        Assert.False(token.Valid);\n    }\n\n    [Theory, BitAutoData]\n    public void TokenValidityCheckNullOrganizationIsInvalid(Organization organization)\n    {\n        var token = new SsoTokenable(organization, 500)\n        {\n            ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1)\n        };\n\n        Assert.False(token.TokenIsValid(null));\n    }\n\n    [Theory, AutoData]\n    public void SetsDataFromOrganization(Organization organization)\n    {\n        var token = new SsoTokenable(organization, default);\n\n        Assert.Equal(organization.Id, token.OrganizationId);\n        Assert.Equal(organization.Identifier, token.DomainHint);\n    }\n\n    [Fact]\n    public void SetsExpirationFromConstructor()\n    {\n        var expectedDateTime = DateTime.UtcNow.AddSeconds(500);\n        var token = new SsoTokenable(null, 500);\n\n        Assert.Equal(expectedDateTime, token.ExpirationDate, TimeSpan.FromMilliseconds(10));\n    }\n\n    [Theory, AutoData]\n    public void SerializationSetsCorrectDateTime(Organization organization)\n    {\n        var expectedDateTime = DateTime.UtcNow.AddHours(-5);\n        var token = new SsoTokenable(organization, default)\n        {\n            ExpirationDate = expectedDateTime\n        };\n\n        var result = Tokenable.FromToken<SsoTokenable>(token.ToToken());\n\n        Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10));\n    }\n\n    [Theory, AutoData]\n    public void TokenIsValidFailsWhenExpired(Organization organization)\n    {\n        var expectedDateTime = DateTime.UtcNow.AddHours(-5);\n        var token = new SsoTokenable(organization, default)\n        {\n            ExpirationDate = expectedDateTime\n        };\n\n        var result = token.TokenIsValid(organization);\n\n        Assert.False(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Models/Business/Tokenables/WebAuthnCredentialCreateOptionsTokenableTests.cs",
    "content": "﻿using Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Fido2NetLib;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Models.Business.Tokenables;\n\npublic class WebAuthnCredentialCreateOptionsTokenableTests\n{\n    [Theory, BitAutoData]\n    public void Valid_TokenWithoutUser_ReturnsFalse(CredentialCreateOptions createOptions)\n    {\n        var token = new WebAuthnCredentialCreateOptionsTokenable(null, createOptions);\n\n        var isValid = token.Valid;\n\n        Assert.False(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void Valid_TokenWithoutOptions_ReturnsFalse(User user)\n    {\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, null);\n\n        var isValid = token.Valid;\n\n        Assert.False(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void Valid_NewlyCreatedToken_ReturnsTrue(User user, CredentialCreateOptions createOptions)\n    {\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);\n\n        var isValid = token.Valid;\n\n        Assert.True(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void ValidIsValid_TokenWithoutUser_ReturnsFalse(User user, CredentialCreateOptions createOptions)\n    {\n        var token = new WebAuthnCredentialCreateOptionsTokenable(null, createOptions);\n\n        var isValid = token.TokenIsValid(user);\n\n        Assert.False(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void ValidIsValid_TokenWithoutOptions_ReturnsFalse(User user)\n    {\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, null);\n\n        var isValid = token.TokenIsValid(user);\n\n        Assert.False(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void ValidIsValid_NonMatchingUsers_ReturnsFalse(User user1, User user2, CredentialCreateOptions createOptions)\n    {\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user1, createOptions);\n\n        var isValid = token.TokenIsValid(user2);\n\n        Assert.False(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void ValidIsValid_SameUser_ReturnsTrue(User user, CredentialCreateOptions createOptions)\n    {\n        var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);\n\n        var isValid = token.TokenIsValid(user);\n\n        Assert.True(isValid);\n    }\n}\n\n"
  },
  {
    "path": "test/Core.Test/Auth/Models/Business/Tokenables/WebAuthnLoginAssertionOptionsTokenableTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Fido2NetLib;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Models.Business.Tokenables;\n\npublic class WebAuthnLoginAssertionOptionsTokenableTests\n{\n    [Theory, BitAutoData]\n    public void Valid_TokenWithoutOptions_ReturnsFalse(WebAuthnLoginAssertionOptionsScope scope)\n    {\n        var token = new WebAuthnLoginAssertionOptionsTokenable(scope, null);\n\n        var isValid = token.Valid;\n\n        Assert.False(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void Valid_NewlyCreatedToken_ReturnsTrue(WebAuthnLoginAssertionOptionsScope scope, AssertionOptions createOptions)\n    {\n        var token = new WebAuthnLoginAssertionOptionsTokenable(scope, createOptions);\n\n\n        var isValid = token.Valid;\n\n        Assert.True(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void ValidIsValid_TokenWithoutOptions_ReturnsFalse(WebAuthnLoginAssertionOptionsScope scope)\n    {\n        var token = new WebAuthnLoginAssertionOptionsTokenable(scope, null);\n\n        var isValid = token.TokenIsValid(scope);\n\n        Assert.False(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void ValidIsValid_NonMatchingScope_ReturnsFalse(WebAuthnLoginAssertionOptionsScope scope1, WebAuthnLoginAssertionOptionsScope scope2, AssertionOptions createOptions)\n    {\n        var token = new WebAuthnLoginAssertionOptionsTokenable(scope1, createOptions);\n\n        var isValid = token.TokenIsValid(scope2);\n\n        Assert.False(isValid);\n    }\n\n    [Theory, BitAutoData]\n    public void ValidIsValid_SameScope_ReturnsTrue(WebAuthnLoginAssertionOptionsScope scope, AssertionOptions createOptions)\n    {\n        var token = new WebAuthnLoginAssertionOptionsTokenable(scope, createOptions);\n\n        var isValid = token.TokenIsValid(scope);\n\n        Assert.True(isValid);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Services/AuthRequestServiceTests.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Exceptions;\nusing Bit.Core.Auth.Models.Api.Request.AuthRequest;\nusing Bit.Core.Auth.Services.Implementations;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\n#nullable enable\n\nnamespace Bit.Core.Test.Auth.Services;\n\n[SutProviderCustomize]\npublic class AuthRequestServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task GetAuthRequestAsync_IfDifferentUser_ReturnsNull(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest,\n        Guid authRequestId,\n        Guid userId)\n    {\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequestId)\n            .Returns(authRequest);\n\n        var foundAuthRequest = await sutProvider.Sut.GetAuthRequestAsync(authRequestId, userId);\n\n        Assert.Null(foundAuthRequest);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAuthRequestAsync_IfSameUser_ReturnsAuthRequest(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest,\n        Guid authRequestId)\n    {\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequestId)\n            .Returns(authRequest);\n\n        var foundAuthRequest = await sutProvider.Sut.GetAuthRequestAsync(authRequestId, authRequest.UserId);\n\n        Assert.NotNull(foundAuthRequest);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetValidatedAuthRequestAsync_IfCodeNotValid_ReturnsNull(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest,\n        string accessCode)\n    {\n        authRequest.CreationDate = DateTime.UtcNow;\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, accessCode);\n\n        Assert.Null(foundAuthRequest);\n    }\n\n    /// <summary>\n    /// Story: AdminApproval AuthRequests should have a longer expiration time by default and non-AdminApproval ones\n    /// should expire after 15 minutes by default.\n    /// </summary>\n    [Theory]\n    [BitAutoData(AuthRequestType.AdminApproval, \"-10.00:00:00\")]\n    [BitAutoData(AuthRequestType.AuthenticateAndUnlock, \"-00:16:00\")]\n    [BitAutoData(AuthRequestType.Unlock, \"-00:16:00\")]\n    public async Task GetValidatedAuthRequestAsync_IfExpired_ReturnsNull(\n        AuthRequestType authRequestType,\n        TimeSpan creationTimeBeforeNow,\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest)\n    {\n        authRequest.Type = authRequestType;\n        authRequest.CreationDate = DateTime.UtcNow.Add(creationTimeBeforeNow);\n        authRequest.Approved = false;\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);\n\n        Assert.Null(foundAuthRequest);\n    }\n\n    /// <summary>\n    /// Story: Once a AdminApproval type has been approved it has a different expiration time based on time\n    /// after the response.\n    /// </summary>\n    [Theory]\n    [BitAutoData]\n    public async Task GetValidatedAuthRequestAsync_AdminApprovalApproved_HasLongerExpiration_ReturnsRequest(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest)\n    {\n        authRequest.Type = AuthRequestType.AdminApproval;\n        authRequest.Approved = true;\n        authRequest.ResponseDate = DateTime.UtcNow.Add(TimeSpan.FromHours(-13));\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        var validatedAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);\n\n        Assert.Null(validatedAuthRequest);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetValidatedAuthRequestAsync_IfValid_ReturnsAuthRequest(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest)\n    {\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-2);\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .PasswordlessAuth\n            .Returns(new GlobalSettings.PasswordlessAuthSettings());\n\n        var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);\n\n        Assert.NotNull(foundAuthRequest);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAuthRequestAsync_NoUser_ThrowsBadRequest(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequestCreateRequestModel createModel)\n    {\n        sutProvider.GetDependency<ICurrentContext>()\n            .DeviceType\n            .Returns(DeviceType.Android);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(createModel.Email)\n            .Returns((User?)null);\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAuthRequestAsync_NoKnownDevice_ThrowsBadRequest(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequestCreateRequestModel createModel,\n        User user)\n    {\n        user.Email = createModel.Email;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(createModel.Email)\n            .Returns(user);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .DeviceType\n            .Returns(DeviceType.Android);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .PasswordlessAuth.KnownDevicesOnly\n            .Returns(true);\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));\n    }\n\n    /// <summary>\n    /// Story: Non-AdminApproval requests should be created without a known device if the settings is set to <c>false</c>\n    /// Non-AdminApproval ones should also have a push notification sent about them.\n    /// </summary>\n    [Theory]\n    [BitAutoData(AuthRequestType.AuthenticateAndUnlock)]\n    [BitAutoData(AuthRequestType.Unlock)]\n    [BitAutoData(new object?[1] { null })]\n    public async Task CreateAuthRequestAsync_CreatesAuthRequest(\n        AuthRequestType? authRequestType,\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequestCreateRequestModel createModel,\n        User user)\n    {\n        user.Email = createModel.Email;\n        createModel.Type = authRequestType;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(createModel.Email)\n            .Returns(user);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .DeviceType\n            .Returns(DeviceType.Android);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .IpAddress\n            .Returns(\"1.1.1.1\");\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .PasswordlessAuth.KnownDevicesOnly\n            .Returns(false);\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .CreateAsync(Arg.Any<AuthRequest>())\n            .Returns(c => c.ArgAt<AuthRequest>(0));\n\n        var createdAuthRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received()\n            .PushAuthRequestAsync(createdAuthRequest);\n\n        await sutProvider.GetDependency<IAuthRequestRepository>()\n            .Received()\n            .CreateAsync(createdAuthRequest);\n\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendDeviceApprovalRequestedNotificationEmailAsync(\n                Arg.Any<IEnumerable<string>>(),\n                Arg.Any<Guid>(),\n                Arg.Any<string>(),\n                Arg.Any<string>());\n    }\n\n    /// <summary>\n    /// Story: Since an AllowAnonymous endpoint calls this method we need\n    /// to verify that a device was able to be found via ICurrentContext\n    /// </summary>\n    [Theory]\n    [BitAutoData(AuthRequestType.AuthenticateAndUnlock)]\n    [BitAutoData(AuthRequestType.Unlock)]\n    public async Task CreateAuthRequestAsync_NoDeviceType_ThrowsBadRequest(\n        AuthRequestType authRequestType,\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequestCreateRequestModel createModel,\n        User user)\n    {\n        user.Email = createModel.Email;\n        createModel.Type = authRequestType;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(createModel.Email)\n            .Returns(user);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .DeviceType\n            .Returns((DeviceType?)null);\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));\n    }\n\n    /// <summary>\n    /// Story: If a user happens to exist to more than one organization, we will send the device approval request to\n    /// each of them.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task CreateAuthRequestAsync_AdminApproval_CreatesForEachOrganization_SendsEmails(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequestCreateRequestModel createModel,\n        User user,\n        OrganizationUser organizationUser1,\n        OrganizationUserUserDetails admin1,\n        OrganizationUserUserDetails customUser1,\n        OrganizationUser organizationUser2,\n        OrganizationUserUserDetails admin2,\n        OrganizationUserUserDetails admin3,\n        OrganizationUserUserDetails customUser2)\n    {\n        createModel.Type = AuthRequestType.AdminApproval;\n        user.Email = createModel.Email;\n        organizationUser1.UserId = user.Id;\n        organizationUser2.UserId = user.Id;\n        customUser1.Permissions = CoreHelpers.ClassToJsonData(new Permissions\n        {\n            ManageResetPassword = false,\n        });\n        customUser2.Permissions = CoreHelpers.ClassToJsonData(new Permissions\n        {\n            ManageResetPassword = true,\n        });\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(user.Email)\n            .Returns(user);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .DeviceType\n            .Returns(DeviceType.ChromeExtension);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .PasswordlessAuth.KnownDevicesOnly\n            .Returns(false);\n\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns(new List<OrganizationUser>\n            {\n                organizationUser1,\n                organizationUser2,\n            });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Admin)\n            .Returns(\n            [\n                admin1,\n            ]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Custom)\n            .Returns(\n            [\n                customUser1,\n            ]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Admin)\n            .Returns(\n            [\n                admin2,\n                admin3,\n            ]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByRoleAsync(organizationUser2.OrganizationId, OrganizationUserType.Custom)\n            .Returns(\n            [\n                customUser2,\n            ]);\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .CreateAsync(Arg.Any<AuthRequest>())\n            .Returns(c => c.ArgAt<AuthRequest>(0));\n\n        var authRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);\n\n        Assert.Equal(organizationUser1.OrganizationId, authRequest.OrganizationId);\n\n        await sutProvider.GetDependency<IAuthRequestRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser1.OrganizationId));\n\n        await sutProvider.GetDependency<IAuthRequestRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser2.OrganizationId));\n\n        await sutProvider.GetDependency<IAuthRequestRepository>()\n            .Received(2)\n            .CreateAsync(Arg.Any<AuthRequest>());\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendDeviceApprovalRequestedNotificationEmailAsync(\n                Arg.Is<IEnumerable<string>>(emails => emails.Count() == 1 && emails.Contains(admin1.Email)),\n                organizationUser1.OrganizationId,\n                user.Email,\n                user.Name);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendDeviceApprovalRequestedNotificationEmailAsync(\n                Arg.Is<IEnumerable<string>>(emails => emails.Count() == 3 &&\n                                                      emails.Contains(admin2.Email) && emails.Contains(admin3.Email) &&\n                                                      emails.Contains(customUser2.Email)),\n                organizationUser2.OrganizationId,\n                user.Email,\n                user.Name);\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task CreateAuthRequestAsync_AdminApproval_AndNoAdminEmails_ShouldNotSendNotificationEmails(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequestCreateRequestModel createModel,\n        User user,\n        OrganizationUser organizationUser1)\n    {\n        createModel.Type = AuthRequestType.AdminApproval;\n        user.Email = createModel.Email;\n        organizationUser1.UserId = user.Id;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(user.Email)\n            .Returns(user);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .DeviceType\n            .Returns(DeviceType.ChromeExtension);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .UserId\n            .Returns(user.Id);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .PasswordlessAuth.KnownDevicesOnly\n            .Returns(false);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(user.Id)\n            .Returns(new List<OrganizationUser>\n            {\n                organizationUser1,\n            });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Admin)\n            .Returns([]);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByRoleAsync(organizationUser1.OrganizationId, OrganizationUserType.Custom)\n            .Returns([]);\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .CreateAsync(Arg.Any<AuthRequest>())\n            .Returns(c => c.ArgAt<AuthRequest>(0));\n\n        var authRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);\n\n        Assert.Equal(organizationUser1.OrganizationId, authRequest.OrganizationId);\n\n        await sutProvider.GetDependency<IAuthRequestRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser1.OrganizationId));\n\n        await sutProvider.GetDependency<IAuthRequestRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Any<AuthRequest>());\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(0)\n            .SendDeviceApprovalRequestedNotificationEmailAsync(\n                Arg.Any<IEnumerable<string>>(),\n                Arg.Any<Guid>(),\n                Arg.Any<string>(),\n                Arg.Any<string>());\n\n        sutProvider.GetDependency<ILogger<AuthRequestService>>()\n            .Received(1)\n            .LogWarning(\"There are no admin emails to send to.\");\n    }\n\n    /// <summary>\n    /// Story: When an <see cref=\"AuthRequest\"> is approved we want to update it in the database so it cannot have\n    /// it's status changed again and we want to push a notification to let the user know of the approval.\n    /// In the case of the AdminApproval we also want to log an event.\n    /// </summary>\n    [Theory]\n    [BitAutoData(AuthRequestType.AdminApproval, \"7b055ea1-38be-42d0-b2e4-becb2340f8df\")]\n    [BitAutoData(AuthRequestType.Unlock, null)]\n    [BitAutoData(AuthRequestType.AuthenticateAndUnlock, null)]\n    public async Task UpdateAuthRequestAsync_ValidResponse_SendsResponse(\n        AuthRequestType authRequestType,\n        Guid? organizationId,\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest)\n    {\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);\n        authRequest.Approved = null;\n        authRequest.OrganizationId = organizationId;\n        authRequest.Type = authRequestType;\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        var device = new Device\n        {\n            Id = Guid.NewGuid(),\n            Identifier = \"test_identifier\",\n        };\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetByIdentifierAsync(device.Identifier, authRequest.UserId)\n            .Returns(device);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())\n            .Returns(new OrganizationUser\n            {\n                UserId = authRequest.UserId,\n                OrganizationId = organizationId.GetValueOrDefault(),\n            });\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .PasswordlessAuth\n            .Returns(new GlobalSettings.PasswordlessAuthSettings());\n\n        var updateModel = new AuthRequestUpdateRequestModel\n        {\n            Key = \"test_key\",\n            DeviceIdentifier = \"test_identifier\",\n            RequestApproved = true,\n            MasterPasswordHash = \"my_hash\",\n        };\n\n        var udpatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel);\n\n        Assert.Equal(\"my_hash\", udpatedAuthRequest.MasterPasswordHash);\n\n        // On approval, the response date should be set to current date\n        Assert.NotNull(udpatedAuthRequest.ResponseDate);\n        AssertHelper.AssertRecent(udpatedAuthRequest.ResponseDate!.Value);\n\n        await sutProvider.GetDependency<IAuthRequestRepository>()\n            .Received(1)\n            .ReplaceAsync(udpatedAuthRequest);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushAuthRequestResponseAsync(udpatedAuthRequest);\n\n        var expectedNumberOfCalls = organizationId.HasValue ? 1 : 0;\n        await sutProvider.GetDependency<IEventService>()\n            .Received(expectedNumberOfCalls)\n            .LogOrganizationUserEventAsync(\n                Arg.Is<OrganizationUser>(ou => ou.UserId == authRequest.UserId && ou.OrganizationId == organizationId),\n                EventType.OrganizationUser_ApprovedAuthRequest);\n    }\n\n    /// <summary>\n    /// Story: When an <see cref=\"AuthRequest\"> is rejected we want to update it in the database so it cannot have\n    /// it's status changed again but we do not want to send a push notification to the original device\n    /// so as to not leak that it was rejected. In the case of an AdminApproval type we do want to log an event though\n    /// </summary>\n    [Theory]\n    [BitAutoData(AuthRequestType.AdminApproval, \"7b055ea1-38be-42d0-b2e4-becb2340f8df\")]\n    [BitAutoData(AuthRequestType.Unlock, null)]\n    [BitAutoData(AuthRequestType.AuthenticateAndUnlock, null)]\n    public async Task UpdateAuthRequestAsync_ResponseNotApproved_DoesNotLeakRejection(\n        AuthRequestType authRequestType,\n        Guid? organizationId,\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest)\n    {\n        // Give it a recent creation time which is valid for all types of AuthRequests\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);\n        authRequest.Type = authRequestType;\n        // Has not been decided already\n        authRequest.Approved = null;\n        authRequest.OrganizationId = organizationId;\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        // Setup a device for all requests even though it will not be called for verification in a AdminApproval\n        var device = new Device\n        {\n            Id = Guid.NewGuid(),\n            Identifier = \"test_identifier\",\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .PasswordlessAuth\n            .Returns(new GlobalSettings.PasswordlessAuthSettings());\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetByIdentifierAsync(device.Identifier, authRequest.UserId)\n            .Returns(device);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())\n            .Returns(new OrganizationUser\n            {\n                UserId = authRequest.UserId,\n                OrganizationId = organizationId.GetValueOrDefault(),\n            });\n\n        var updateModel = new AuthRequestUpdateRequestModel\n        {\n            Key = \"test_key\",\n            DeviceIdentifier = \"test_identifier\",\n            RequestApproved = false,\n            MasterPasswordHash = \"my_hash\",\n        };\n\n        var udpatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel);\n\n        Assert.Equal(udpatedAuthRequest.MasterPasswordHash, authRequest.MasterPasswordHash);\n        Assert.False(udpatedAuthRequest.Approved);\n        Assert.NotNull(udpatedAuthRequest.ResponseDate);\n        AssertHelper.AssertRecent(udpatedAuthRequest.ResponseDate!.Value);\n\n        await sutProvider.GetDependency<IAuthRequestRepository>()\n            .Received()\n            .ReplaceAsync(udpatedAuthRequest);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .DidNotReceiveWithAnyArgs()\n            .PushAuthRequestResponseAsync(udpatedAuthRequest);\n\n        var expectedNumberOfCalls = organizationId.HasValue ? 1 : 0;\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(expectedNumberOfCalls)\n            .LogOrganizationUserEventAsync(\n                Arg.Is<OrganizationUser>(ou => ou.UserId == authRequest.UserId && ou.OrganizationId == organizationId),\n                EventType.OrganizationUser_RejectedAuthRequest);\n    }\n\n    /// <summary>\n    /// Story: A bad actor is able to get ahold of the request id of a valid <see cref=\"AuthRequest\" />\n    /// and tries to approve it from their own Bitwarden account. We need to validate that the currently signed in user\n    /// is the same user that originally created the request and we want to pretend it does not exist at all by throwing\n    /// NotFoundException.\n    /// </summary>\n    [Theory]\n    [BitAutoData(AuthRequestType.AuthenticateAndUnlock)]\n    [BitAutoData(AuthRequestType.Unlock)]\n    public async Task UpdateAuthRequestAsync_InvalidUser_ThrowsNotFound(\n        AuthRequestType authRequestType,\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest,\n        Guid authenticatedUserId)\n    {\n        // Give it a recent creation date so that it is valid\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);\n        // The request hasn't been Approved/Disapproved already\n        authRequest.Approved = null;\n        // Has an type that needs the UserId property validated\n        authRequest.Type = authRequestType;\n\n        // Auth request should not be null\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        var updateModel = new AuthRequestUpdateRequestModel\n        {\n            Key = \"test_key\",\n            DeviceIdentifier = \"test_identifier\",\n            RequestApproved = true,\n            MasterPasswordHash = \"my_hash\",\n        };\n\n        // Give it a randomly generated userId such that it won't be valid for the AuthRequest\n        await Assert.ThrowsAsync<NotFoundException>(\n            async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authenticatedUserId, updateModel));\n    }\n\n    /// <summary>\n    /// Story: A user created this auth request and does not approve/reject the request\n    /// for 16 minutes, which is past the default expiration time. This auth request\n    /// will be purged from the database soon but might exist for some amount of time after it's expiration\n    /// this method should throw a NotFoundException since it theoretically should not exist, this\n    /// could be a user finally clicking Approve after the request sitting on their phone for a while.\n    /// </summary>\n    [Theory]\n    [BitAutoData(AuthRequestType.AuthenticateAndUnlock, \"-00:16:00\")]\n    [BitAutoData(AuthRequestType.Unlock, \"-00:16:00\")]\n    [BitAutoData(AuthRequestType.AdminApproval, \"-8.00:00:00\")]\n    public async Task UpdateAuthRequestAsync_OldAuthRequest_ThrowsNotFound(\n        AuthRequestType authRequestType,\n        TimeSpan timeBeforeCreation,\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest)\n    {\n        // AuthRequest's have a default valid lifetime of only 15 minutes, make it older than that\n        authRequest.CreationDate = DateTime.UtcNow.Add(timeBeforeCreation);\n        // Make it so that the user has not made a decision on this request\n        authRequest.Approved = null;\n        // Make it one of the types that doesn't have longer expiration i.e AdminApproval\n        authRequest.Type = authRequestType;\n\n        // The item should still exist in the database\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        // Represents the user finally clicking approve.\n        var updateModel = new AuthRequestUpdateRequestModel\n        {\n            Key = \"test_key\",\n            DeviceIdentifier = \"test_identifier\",\n            RequestApproved = true,\n            MasterPasswordHash = \"my_hash\",\n        };\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));\n    }\n\n    /// <summary>\n    /// Story: non-AdminApproval types need to validate that the device used to respond to the\n    /// request is a known device to the authenticated user.\n    /// </summary>\n    [Theory]\n    [BitAutoData(AuthRequestType.AuthenticateAndUnlock)]\n    [BitAutoData(AuthRequestType.Unlock)]\n    public async Task UpdateAuthRequestAsync_InvalidDeviceIdentifier_ThrowsBadRequest(\n        AuthRequestType authRequestType,\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest)\n    {\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);\n        authRequest.Approved = null;\n        authRequest.Type = authRequestType;\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetByIdentifierAsync(\"invalid_identifier\", authRequest.UserId)\n            .Returns((Device?)null);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .PasswordlessAuth\n            .Returns(new GlobalSettings.PasswordlessAuthSettings());\n\n        var updateModel = new AuthRequestUpdateRequestModel\n        {\n            Key = \"test_key\",\n            DeviceIdentifier = \"invalid_identifier\",\n            RequestApproved = true,\n            MasterPasswordHash = \"my_hash\",\n        };\n\n        await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));\n    }\n\n    /// <summary>\n    /// Story: Once the destiny of an AuthRequest has been decided, it should be considered immutable\n    /// and new update request should be blocked.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task UpdateAuthRequestAsync_AlreadyApprovedOrRejected_ThrowsDuplicateAuthRequestException(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest)\n    {\n        authRequest.Approved = true;\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        var updateModel = new AuthRequestUpdateRequestModel\n        {\n            Key = \"test_key\",\n            DeviceIdentifier = \"test_identifier\",\n            RequestApproved = true,\n            MasterPasswordHash = \"my_hash\",\n        };\n\n        await Assert.ThrowsAsync<DuplicateAuthRequestException>(\n            async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));\n    }\n\n    /// <summary>\n    /// Story: An admin approves a request for one of their org users. For auditing purposes we need to\n    /// log an event that correlates the action for who the request was approved for. On approval we also need to\n    /// push the notification to the user.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task UpdateAuthRequestAsync_AdminApproved_LogsEvent(\n        SutProvider<AuthRequestService> sutProvider,\n        AuthRequest authRequest,\n        OrganizationUser organizationUser)\n    {\n        authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);\n        authRequest.Type = AuthRequestType.AdminApproval;\n        authRequest.OrganizationId = organizationUser.OrganizationId;\n        authRequest.Approved = null;\n\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequest.Id)\n            .Returns(authRequest);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(authRequest.OrganizationId!.Value, authRequest.UserId)\n            .Returns(organizationUser);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .PasswordlessAuth\n            .Returns(new GlobalSettings.PasswordlessAuthSettings());\n\n        var updateModel = new AuthRequestUpdateRequestModel\n        {\n            Key = \"test_key\",\n            RequestApproved = true,\n            MasterPasswordHash = \"my_hash\",\n        };\n\n        var updatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel);\n\n        Assert.Equal(\"my_hash\", updatedAuthRequest.MasterPasswordHash);\n        Assert.Equal(\"test_key\", updatedAuthRequest.Key);\n        Assert.True(updatedAuthRequest.Approved);\n        Assert.NotNull(updatedAuthRequest.ResponseDate);\n        AssertHelper.AssertRecent(updatedAuthRequest.ResponseDate!.Value);\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogOrganizationUserEventAsync(\n                Arg.Is(organizationUser), Arg.Is(EventType.OrganizationUser_ApprovedAuthRequest));\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushAuthRequestResponseAsync(authRequest);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAuthRequestAsync_BadId_ThrowsNotFound(\n        SutProvider<AuthRequestService> sutProvider,\n        Guid authRequestId)\n    {\n        sutProvider.GetDependency<IAuthRequestRepository>()\n            .GetByIdAsync(authRequestId)\n            .Returns((AuthRequest?)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAuthRequestAsync(\n            authRequestId, Guid.NewGuid(), new AuthRequestUpdateRequestModel()));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Services/DuoUniversalTokenServiceTests.cs",
    "content": "﻿using Bit.Core.Auth.Identity.TokenProviders;\nusing Bit.Core.Auth.Models;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Services;\n\n[SutProviderCustomize]\npublic class DuoUniversalTokenServiceTests\n{\n    [Theory]\n    [BitAutoData(\"\", \"ClientId\", \"ClientSecret\")]\n    [BitAutoData(\"api-valid.duosecurity.com\", \"\", \"ClientSecret\")]\n    [BitAutoData(\"api-valid.duosecurity.com\", \"ClientId\", \"\")]\n    public async void ValidateDuoConfiguration_InvalidConfig_ReturnsFalse(\n        string host, string clientId, string clientSecret, SutProvider<DuoUniversalTokenService> sutProvider)\n    {\n        // Arrange\n        /* AutoData handles arrangement */\n\n        // Act\n        var result = await sutProvider.Sut.ValidateDuoConfiguration(clientSecret, clientId, host);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData(true, \"api-valid.duosecurity.com\")]\n    [BitAutoData(false, \"invalid\")]\n    [BitAutoData(false, \"api-valid.duosecurity.com\", null, \"clientSecret\")]\n    [BitAutoData(false, \"api-valid.duosecurity.com\", \"ClientId\", null)]\n    [BitAutoData(false, \"api-valid.duosecurity.com\", null, null)]\n    public void HasProperDuoMetadata_ReturnMatchesExpected(\n        bool expectedResponse, string host, string clientId,\n        string clientSecret, SutProvider<DuoUniversalTokenService> sutProvider)\n    {\n        // Arrange\n        var metaData = new Dictionary<string, object> { [\"Host\"] = host };\n\n        if (clientId != null)\n        {\n            metaData.Add(\"ClientId\", clientId);\n        }\n\n        if (clientSecret != null)\n        {\n            metaData.Add(\"ClientSecret\", clientSecret);\n        }\n\n        var provider = new TwoFactorProvider\n        {\n            MetaData = metaData\n        };\n\n        // Act\n        var result = sutProvider.Sut.HasProperDuoMetadata(provider);\n\n        // Assert\n        Assert.Equal(result, expectedResponse);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void HasProperDuoMetadata_ProviderIsNull_ReturnsFalse(\n        SutProvider<DuoUniversalTokenService> sutProvider)\n    {\n        // Act\n        var result = sutProvider.Sut.HasProperDuoMetadata(null);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData(\"api-valid.duosecurity.com\", true)]\n    [BitAutoData(\"api-valid.duofederal.com\", true)]\n    [BitAutoData(\"invalid\", false)]\n    public void ValidDuoHost_HostIsValid_ReturnTrue(\n        string host, bool expectedResponse)\n    {\n        // Act\n        var result = DuoUniversalTokenService.ValidDuoHost(host);\n\n        // Assert\n        Assert.Equal(result, expectedResponse);\n    }\n\n\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Services/SsoConfigServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Services;\n\n[SutProviderCustomize]\npublic class SsoConfigServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task SaveAsync_ExistingItem_UpdatesRevisionDateOnly(SutProvider<SsoConfigService> sutProvider,\n        Organization organization)\n    {\n        var utcNow = DateTime.UtcNow;\n\n        var ssoConfig = new SsoConfig\n        {\n            Id = 1,\n            Data = \"{}\",\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow.AddDays(-10),\n        };\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .UpsertAsync(ssoConfig).Returns(Task.CompletedTask);\n\n        await sutProvider.Sut.SaveAsync(ssoConfig, organization);\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().Received()\n            .UpsertAsync(ssoConfig);\n\n        Assert.Equal(utcNow.AddDays(-10), ssoConfig.CreationDate);\n        Assert.True(ssoConfig.RevisionDate - utcNow < TimeSpan.FromSeconds(1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_NewItem_UpdatesCreationAndRevisionDate(SutProvider<SsoConfigService> sutProvider,\n        Organization organization)\n    {\n        var utcNow = DateTime.UtcNow;\n\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = \"{}\",\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow.AddDays(-10),\n        };\n\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .UpsertAsync(ssoConfig).Returns(Task.CompletedTask);\n\n        await sutProvider.Sut.SaveAsync(ssoConfig, organization);\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().Received()\n            .UpsertAsync(ssoConfig);\n\n        Assert.True(ssoConfig.CreationDate - utcNow < TimeSpan.FromSeconds(1));\n        Assert.True(ssoConfig.RevisionDate - utcNow < TimeSpan.FromSeconds(1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_PreventDisablingKeyConnector(SutProvider<SsoConfigService> sutProvider,\n        Organization organization)\n    {\n        var utcNow = DateTime.UtcNow;\n\n        var oldSsoConfig = new SsoConfig\n        {\n            Id = 1,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow.AddDays(-10),\n        };\n\n        var newSsoConfig = new SsoConfig\n        {\n            Id = 1,\n            Data = \"{}\",\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow,\n        };\n\n        var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();\n        ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(oldSsoConfig);\n        ssoConfigRepository.UpsertAsync(newSsoConfig).Returns(Task.CompletedTask);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(organization.Id)\n            .Returns(new[] { new OrganizationUserUserDetails { UsesKeyConnector = true } });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(newSsoConfig, organization));\n\n        Assert.Contains(\"Key Connector cannot be disabled at this moment.\", exception.Message);\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_AllowDisablingKeyConnectorWhenNoUserIsUsingIt(\n        SutProvider<SsoConfigService> sutProvider, Organization organization)\n    {\n        var utcNow = DateTime.UtcNow;\n\n        var oldSsoConfig = new SsoConfig\n        {\n            Id = 1,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector,\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow.AddDays(-10),\n        };\n\n        var newSsoConfig = new SsoConfig\n        {\n            Id = 1,\n            Data = \"{}\",\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow,\n        };\n\n        var ssoConfigRepository = sutProvider.GetDependency<ISsoConfigRepository>();\n        ssoConfigRepository.GetByOrganizationIdAsync(organization.Id).Returns(oldSsoConfig);\n        ssoConfigRepository.UpsertAsync(newSsoConfig).Returns(Task.CompletedTask);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyDetailsByOrganizationAsync(organization.Id)\n            .Returns(new[] { new OrganizationUserUserDetails { UsesKeyConnector = false } });\n\n        await sutProvider.Sut.SaveAsync(newSsoConfig, organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_KeyConnector_SingleOrgNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,\n        Organization organization,\n        [Policy(PolicyType.SingleOrg, false)] PolicyStatus policy)\n    {\n        var utcNow = DateTime.UtcNow;\n\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector,\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow.AddDays(-10),\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>().RunAsync(\n            Arg.Any<Guid>(), PolicyType.SingleOrg).Returns(policy);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(ssoConfig, organization));\n\n        Assert.Contains(\"Key Connector requires the Single Organization policy to be enabled.\", exception.Message);\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_KeyConnector_SsoPolicyNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,\n        Organization organization,\n        [Policy(PolicyType.SingleOrg, true)] PolicyStatus singleOrgPolicy,\n        [Policy(PolicyType.RequireSso, false)] PolicyStatus requireSsoPolicy)\n    {\n        var utcNow = DateTime.UtcNow;\n\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector,\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow.AddDays(-10),\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>().RunAsync(\n            Arg.Any<Guid>(), PolicyType.SingleOrg).Returns(singleOrgPolicy);\n        sutProvider.GetDependency<IPolicyQuery>().RunAsync(\n            Arg.Any<Guid>(), PolicyType.RequireSso).Returns(requireSsoPolicy);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(ssoConfig, organization));\n\n        Assert.Contains(\"Key Connector requires the Single Sign-On Authentication policy to be enabled.\", exception.Message);\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_KeyConnector_SsoConfigNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,\n        Organization organization,\n        [Policy(PolicyType.SingleOrg, true)] PolicyStatus policy)\n    {\n        var utcNow = DateTime.UtcNow;\n\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector,\n            }.Serialize(),\n            Enabled = false,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow.AddDays(-10),\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>().RunAsync(\n            Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(policy);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(ssoConfig, organization));\n\n        Assert.Contains(\"You must enable SSO to use Key Connector.\", exception.Message);\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_KeyConnector_KeyConnectorAbilityNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,\n        Organization organization,\n        [Policy(PolicyType.SingleOrg, true)] PolicyStatus policy)\n    {\n        var utcNow = DateTime.UtcNow;\n\n        organization.UseKeyConnector = false;\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector,\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow.AddDays(-10),\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>().RunAsync(\n            Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(policy);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(ssoConfig, organization));\n\n        Assert.Contains(\"Organization cannot use Key Connector.\", exception.Message);\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_KeyConnector_Success(SutProvider<SsoConfigService> sutProvider,\n        Organization organization,\n        [Policy(PolicyType.SingleOrg, true)] PolicyStatus policy)\n    {\n        var utcNow = DateTime.UtcNow;\n\n        organization.UseKeyConnector = true;\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.KeyConnector,\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = organization.Id,\n            CreationDate = utcNow.AddDays(-10),\n            RevisionDate = utcNow.AddDays(-10),\n        };\n\n        sutProvider.GetDependency<IPolicyQuery>().RunAsync(\n            Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(policy);\n\n        await sutProvider.Sut.SaveAsync(ssoConfig, organization);\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_Tde_Enable_Required_Policies(SutProvider<SsoConfigService> sutProvider, Organization organization)\n    {\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = organization.Id,\n        };\n\n        await sutProvider.Sut.SaveAsync(ssoConfig, organization);\n\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>().Received(1)\n            .SaveAsync(\n                Arg.Is<SavePolicyModel>(t => t.PolicyUpdate.Type == PolicyType.SingleOrg &&\n                                             t.PolicyUpdate.OrganizationId == organization.Id &&\n                                             t.PolicyUpdate.Enabled)\n            );\n\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>().Received(1)\n            .SaveAsync(\n                Arg.Is<SavePolicyModel>(t => t.PolicyUpdate.Type == PolicyType.ResetPassword &&\n                                             t.PolicyUpdate.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled &&\n                                             t.PolicyUpdate.OrganizationId == organization.Id &&\n                                             t.PolicyUpdate.Enabled)\n            );\n\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>().Received(1)\n            .SaveAsync(\n                Arg.Is<SavePolicyModel>(t => t.PolicyUpdate.Type == PolicyType.RequireSso &&\n                                             t.PolicyUpdate.OrganizationId == organization.Id &&\n                                             t.PolicyUpdate.Enabled)\n            );\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_Tde_UsesVNextSavePolicyCommand(\n        SutProvider<SsoConfigService> sutProvider, Organization organization)\n    {\n        var ssoConfig = new SsoConfig\n        {\n            Id = default,\n            Data = new SsoConfigurationData\n            {\n                MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,\n            }.Serialize(),\n            Enabled = true,\n            OrganizationId = organization.Id,\n        };\n\n        await sutProvider.Sut.SaveAsync(ssoConfig, organization);\n\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .Received(1)\n            .SaveAsync(Arg.Is<SavePolicyModel>(m =>\n                m.PolicyUpdate.Type == PolicyType.SingleOrg &&\n                m.PolicyUpdate.OrganizationId == organization.Id &&\n                m.PolicyUpdate.Enabled &&\n                m.PerformedBy is SystemUser));\n\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .Received(1)\n            .SaveAsync(Arg.Is<SavePolicyModel>(m =>\n                m.PolicyUpdate.Type == PolicyType.ResetPassword &&\n                m.PolicyUpdate.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled &&\n                m.PolicyUpdate.OrganizationId == organization.Id &&\n                m.PolicyUpdate.Enabled &&\n                m.PerformedBy is SystemUser));\n\n        await sutProvider.GetDependency<IVNextSavePolicyCommand>()\n            .Received(1)\n            .SaveAsync(Arg.Is<SavePolicyModel>(m =>\n                m.PolicyUpdate.Type == PolicyType.RequireSso &&\n                m.PolicyUpdate.OrganizationId == organization.Id &&\n                m.PolicyUpdate.Enabled &&\n                m.PerformedBy is SystemUser));\n\n        await sutProvider.GetDependency<ISsoConfigRepository>().ReceivedWithAnyArgs()\n            .UpsertAsync(default);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Services;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Core.Auth.Enums;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.Services;\n\n[SutProviderCustomize]\npublic class TwoFactorEmailServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task SendTwoFactorEmailAsync_Success(SutProvider<TwoFactorEmailService> sutProvider, User user)\n    {\n        var email = user.Email.ToLowerInvariant();\n        var token = \"thisisatokentocompare\";\n        var IpAddress = \"1.1.1.1\";\n        var deviceType = DeviceType.Android;\n\n        var context = sutProvider.GetDependency<ICurrentContext>();\n        context.DeviceType = deviceType;\n        context.IpAddress = IpAddress;\n\n        var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();\n        userTwoFactorTokenProvider\n            .CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(true));\n        userTwoFactorTokenProvider\n            .GenerateAsync(\"TwoFactor\", Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(token));\n\n        var userManager = sutProvider.GetDependency<UserManager<User>>();\n        userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider);\n\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new TwoFactorProvider\n            {\n                MetaData = new Dictionary<string, object> { [\"Email\"] = email },\n                Enabled = true\n            }\n        });\n        await sutProvider.Sut.SendTwoFactorEmailAsync(user);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType.ToString(),\n                TwoFactorEmailPurpose.Login);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendTwoFactorSetupEmailAsync_Success(SutProvider<TwoFactorEmailService> sutProvider, User user)\n    {\n        var email = user.Email.ToLowerInvariant();\n        var token = \"thisisatokentocompare\";\n        var IpAddress = \"1.1.1.1\";\n        var deviceType = DeviceType.Android;\n\n        var context = sutProvider.GetDependency<ICurrentContext>();\n        context.DeviceType = deviceType;\n        context.IpAddress = IpAddress;\n\n        var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();\n        userTwoFactorTokenProvider\n            .CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(true));\n        userTwoFactorTokenProvider\n            .GenerateAsync(\"TwoFactor\", Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(token));\n\n        var userManager = sutProvider.GetDependency<UserManager<User>>();\n        userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider);\n\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new TwoFactorProvider\n            {\n                MetaData = new Dictionary<string, object> { [\"Email\"] = email },\n                Enabled = true\n            }\n        });\n\n        await sutProvider.Sut.SendTwoFactorSetupEmailAsync(user);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType.ToString(),\n                TwoFactorEmailPurpose.Setup);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendNewDeviceVerificationEmailAsync_Success(SutProvider<TwoFactorEmailService> sutProvider, User user)\n    {\n        var email = user.Email.ToLowerInvariant();\n        var token = \"thisisatokentocompare\";\n        var IpAddress = \"1.1.1.1\";\n        var deviceType = DeviceType.Android;\n\n        var context = sutProvider.GetDependency<ICurrentContext>();\n        context.DeviceType = deviceType;\n        context.IpAddress = IpAddress;\n\n        var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();\n        userTwoFactorTokenProvider\n            .CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(true));\n        userTwoFactorTokenProvider\n            .GenerateAsync(\"otp:\" + user.Email, Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(token));\n\n        var userManager = sutProvider.GetDependency<UserManager<User>>();\n        userManager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, userTwoFactorTokenProvider);\n\n        await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType.ToString(),\n                TwoFactorEmailPurpose.NewDeviceVerification);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderOnUser(SutProvider<TwoFactorEmailService> sutProvider, User user)\n    {\n        user.TwoFactorProviders = null;\n\n        await Assert.ThrowsAsync<ArgumentNullException>(\"No email.\", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderMetadataOnUser(SutProvider<TwoFactorEmailService> sutProvider, User user)\n    {\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new TwoFactorProvider\n            {\n                MetaData = null,\n                Enabled = true\n            }\n        });\n\n        await Assert.ThrowsAsync<ArgumentNullException>(\"No email.\", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderEmailMetadataOnUser(SutProvider<TwoFactorEmailService> sutProvider, User user)\n    {\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new TwoFactorProvider\n            {\n                MetaData = new Dictionary<string, object> { [\"qweqwe\"] = user.Email.ToLowerInvariant() },\n                Enabled = true\n            }\n        });\n\n        await Assert.ThrowsAsync<ArgumentNullException>(\"No email.\", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendNewDeviceVerificationEmailAsync_ExceptionBecauseUserNull(SutProvider<TwoFactorEmailService> sutProvider)\n    {\n        await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.SendNewDeviceVerificationEmailAsync(null));\n    }\n\n    [Theory]\n    [BitAutoData(DeviceType.UnknownBrowser, \"Unknown Browser\")]\n    [BitAutoData(DeviceType.Android, \"Android\")]\n    public async Task SendTwoFactorEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName,\n        SutProvider<TwoFactorEmailService> sutProvider,\n        User user)\n    {\n        var email = user.Email.ToLowerInvariant();\n        var token = \"thisisatokentocompare\";\n        var IpAddress = \"1.1.1.1\";\n\n        var context = sutProvider.GetDependency<ICurrentContext>();\n        context.DeviceType = deviceType;\n        context.IpAddress = IpAddress;\n\n        var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();\n        userTwoFactorTokenProvider\n            .CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(true));\n        userTwoFactorTokenProvider\n            .GenerateAsync(\"TwoFactor\", Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(token));\n\n        var userManager = sutProvider.GetDependency<UserManager<User>>();\n        userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider);\n\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new TwoFactorProvider\n            {\n                MetaData = new Dictionary<string, object> { [\"Email\"] = email },\n                Enabled = true\n            }\n        });\n\n        await sutProvider.Sut.SendTwoFactorEmailAsync(user);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendTwoFactorEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), deviceTypeName, TwoFactorEmailPurpose.Login);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SendTwoFactorEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(SutProvider<TwoFactorEmailService> sutProvider, User user)\n    {\n        var email = user.Email.ToLowerInvariant();\n        var token = \"thisisatokentocompare\";\n        var IpAddress = \"1.1.1.1\";\n\n        var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();\n        userTwoFactorTokenProvider\n            .CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(true));\n        userTwoFactorTokenProvider\n            .GenerateAsync(\"TwoFactor\", Arg.Any<UserManager<User>>(), user)\n            .Returns(Task.FromResult(token));\n\n        var context = Substitute.For<ICurrentContext>();\n        context.DeviceType = null;\n        context.IpAddress = IpAddress;\n\n        var userManager = sutProvider.GetDependency<UserManager<User>>();\n        userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider);\n\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new TwoFactorProvider\n            {\n                MetaData = new Dictionary<string, object> { [\"Email\"] = email },\n                Enabled = true\n            }\n        });\n\n        await sutProvider.Sut.SendTwoFactorEmailAsync(user);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendTwoFactorEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), \"Unknown Browser\", Arg.Any<TwoFactorEmailPurpose>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/DeviceTrust/UntrustDevicesCommandTests.cs",
    "content": "﻿using Bit.Core.Auth.UserFeatures.DeviceTrust;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.WebAuthnLogin;\n\n[SutProviderCustomize]\npublic class UntrustDevicesCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task SetsKeysToNull(SutProvider<UntrustDevicesCommand> sutProvider, User user)\n    {\n        var deviceId = Guid.NewGuid();\n        // Arrange\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns([new Device\n            {\n                Id = deviceId,\n                EncryptedPrivateKey = \"encryptedPrivateKey\",\n                EncryptedPublicKey = \"encryptedPublicKey\",\n                EncryptedUserKey = \"encryptedUserKey\"\n            }]);\n\n        // Act\n        await sutProvider.Sut.UntrustDevices(user, new List<Guid> { deviceId });\n\n        // Assert\n        await sutProvider.GetDependency<IDeviceRepository>()\n            .Received()\n            .UpsertAsync(Arg.Is<Device>(d =>\n                d.Id == deviceId &&\n                d.EncryptedPrivateKey == null &&\n                d.EncryptedPublicKey == null &&\n                d.EncryptedUserKey == null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RejectsWrongUser(SutProvider<UntrustDevicesCommand> sutProvider, User user)\n    {\n        var deviceId = Guid.NewGuid();\n        // Arrange\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns([]);\n\n        // Act\n        await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>\n            await sutProvider.Sut.UntrustDevices(user, new List<Guid> { deviceId }));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/EmergencyAccess/DeleteEmergencyAccessCommandTests.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Commands;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Mail.Mailer;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.EmergencyAccess;\n\n[SutProviderCustomize]\npublic class DeleteEmergencyAccessCommandTests\n{\n    /// <summary>\n    /// Verifies that attempting to delete a non-existent emergency access record\n    /// throws a <see cref=\"BadRequestException\"/> and does not call delete or send email.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteByIdAndUserIdAsync_EmergencyAccessNotFound_ThrowsBadRequestAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        Guid emergencyAccessId,\n        Guid userId)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetDetailsByIdAsync(emergencyAccessId)\n            .Returns((EmergencyAccessDetails?)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.DeleteByIdAndUserIdAsync(emergencyAccessId, userId));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n        await sutProvider.GetDependency<IMailer>()\n            .DidNotReceiveWithAnyArgs()\n            .SendEmail<EmergencyAccessRemoveGranteesMailView>(default);\n    }\n\n    /// <summary>\n    /// Verifies that an emergency access record is deleted by ID and user ID,\n    /// and that a notification email is sent to the grantor.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteByIdAndUserIdAsync_DeletesEmergencyAccessAndSendsEmailAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails emergencyAccessDetails)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetDetailsByIdAsync(emergencyAccessDetails.Id)\n            .Returns(emergencyAccessDetails);\n\n        await sutProvider.Sut.DeleteByIdAndUserIdAsync(emergencyAccessDetails.Id, emergencyAccessDetails.GrantorId);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteAsync(emergencyAccessDetails);\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(emergencyAccessDetails.GrantorEmail) &&\n                mail.View.RemovedGranteeEmails.Contains(emergencyAccessDetails.GranteeEmail)));\n    }\n\n    /// <summary>\n    /// Verifies that when the grantor email is null, the record is deleted\n    /// but no email notification is sent.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteByIdAndUserIdAsync_NullGrantorEmail_DeletesButDoesNotSendEmailAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails emergencyAccessDetails)\n    {\n        emergencyAccessDetails.GrantorEmail = null;\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetDetailsByIdAsync(emergencyAccessDetails.Id)\n            .Returns(emergencyAccessDetails);\n\n        await sutProvider.Sut.DeleteByIdAndUserIdAsync(emergencyAccessDetails.Id, emergencyAccessDetails.GrantorId);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteAsync(emergencyAccessDetails);\n        await sutProvider.GetDependency<IMailer>()\n            .DidNotReceiveWithAnyArgs()\n            .SendEmail<EmergencyAccessRemoveGranteesMailView>(default);\n        sutProvider.GetDependency<ILogger<DeleteEmergencyAccessCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Warning,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString().Contains(emergencyAccessDetails.GrantorId.ToString())\n                    && o.ToString().Contains(\"GrantorEmail missing: True\")\n                    && o.ToString().Contains(\"GranteeEmail missing: False\")),\n                null,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    /// <summary>\n    /// Verifies that when the grantee email is null, the record is deleted\n    /// but no email notification is sent.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteByIdAndUserIdAsync_NullGranteeEmail_DeletesButDoesNotSendEmailAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails emergencyAccessDetails)\n    {\n        emergencyAccessDetails.GranteeEmail = null;\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetDetailsByIdAsync(emergencyAccessDetails.Id)\n            .Returns(emergencyAccessDetails);\n\n        await sutProvider.Sut.DeleteByIdAndUserIdAsync(emergencyAccessDetails.Id, emergencyAccessDetails.GrantorId);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteAsync(emergencyAccessDetails);\n        await sutProvider.GetDependency<IMailer>()\n            .DidNotReceiveWithAnyArgs()\n            .SendEmail<EmergencyAccessRemoveGranteesMailView>(default);\n        sutProvider.GetDependency<ILogger<DeleteEmergencyAccessCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Warning,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString().Contains(emergencyAccessDetails.GrantorId.ToString())\n                    && o.ToString().Contains(\"GrantorEmail missing: False\")\n                    && o.ToString().Contains(\"GranteeEmail missing: True\")),\n                null,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    /// <summary>\n    /// Verifies that a grantee (not just a grantor) can delete an emergency access record,\n    /// and that the grantor still receives a notification email.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteByIdAndUserIdAsync_GranteeDeletes_DeletesAndSendsEmailAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails emergencyAccessDetails)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetDetailsByIdAsync(emergencyAccessDetails.Id)\n            .Returns(emergencyAccessDetails);\n\n        // Act as the grantee, not the grantor\n        await sutProvider.Sut.DeleteByIdAndUserIdAsync(emergencyAccessDetails.Id, emergencyAccessDetails.GranteeId.Value);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteAsync(emergencyAccessDetails);\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(emergencyAccessDetails.GrantorEmail) &&\n                mail.View.RemovedGranteeEmails.Contains(emergencyAccessDetails.GranteeEmail)));\n    }\n\n    /// <summary>\n    /// Verifies that a user who is neither the grantor nor the grantee cannot delete\n    /// the emergency access record and receives a <see cref=\"BadRequestException\"/>.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteByIdAndUserIdAsync_UnauthorizedUser_ThrowsBadRequestAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails emergencyAccessDetails,\n        Guid unauthorizedUserId)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetDetailsByIdAsync(emergencyAccessDetails.Id)\n            .Returns(emergencyAccessDetails);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.DeleteByIdAndUserIdAsync(emergencyAccessDetails.Id, unauthorizedUserId));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n        await sutProvider.GetDependency<IMailer>()\n            .DidNotReceiveWithAnyArgs()\n            .SendEmail<EmergencyAccessRemoveGranteesMailView>(default);\n    }\n\n    /// <summary>\n    /// Verifies that <see cref=\"IDeleteEmergencyAccessCommand.DeleteAllByUserIdAsync\"/> correctly\n    /// delegates to <see cref=\"IDeleteEmergencyAccessCommand.DeleteAllByUserIdsAsync\"/>\n    /// using a single-element collection containing the provided user ID.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdAsync_DelegatesToDeleteAllByUserIdsAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails emergencyAccessDetails,\n        Guid userId)\n    {\n        emergencyAccessDetails.GranteeId = userId;\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids => ids.Contains(userId)))\n            .Returns([emergencyAccessDetails]);\n\n        await sutProvider.Sut.DeleteAllByUserIdAsync(userId);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .GetManyDetailsByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 1 && ids.Contains(userId)));\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 1 && ids.Contains(emergencyAccessDetails.Id)));\n    }\n\n    /// <summary>\n    /// Verifies that passing an empty list of user IDs does not attempt to delete or send email.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_EmptyList_DoesNotDeleteOrSendEmailAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(Arg.Any<ICollection<Guid>>())\n            .Returns([]);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync([]);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n        await sutProvider.GetDependency<IMailer>()\n            .DidNotReceiveWithAnyArgs()\n            .SendEmail<EmergencyAccessRemoveGranteesMailView>(default);\n    }\n\n    /// <summary>\n    /// Verifies that when user IDs don't match any emergency access records,\n    /// the method does not attempt to delete or send email.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_NoRecordsFound_DoesNotDeleteOrSendEmailAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        List<Guid> userIds)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(userIds)\n            .Returns([]);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync(userIds);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n        await sutProvider.GetDependency<IMailer>()\n            .DidNotReceiveWithAnyArgs()\n            .SendEmail<EmergencyAccessRemoveGranteesMailView>(default);\n    }\n\n    /// <summary>\n    /// Verifies that when a single user ID is a grantee with multiple grantors,\n    /// all records are deleted and each grantor receives one email notification.\n    ///\n    /// Scenario: Alice is a grantee with emergency access TO Bob's, Carol's, and David's vaults.\n    /// When Alice is removed, Bob, Carol, and David each receive an email notification.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_SingleUserIdAsGranteeOnly_NotifiesGrantorsAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails bobAliceRecord,\n        EmergencyAccessDetails carolAliceRecord,\n        EmergencyAccessDetails davidAliceRecord,\n        Guid granteeUserIdAlice)\n    {\n        // Alice (grantee) has emergency access to Bob's, Carol's, and David's vaults\n        bobAliceRecord.GranteeId = granteeUserIdAlice; // Bob granted Alice access to Bob's vault\n        carolAliceRecord.GranteeId = granteeUserIdAlice; // Carol granted Alice access to Carol's vault\n        davidAliceRecord.GranteeId = granteeUserIdAlice; // David granted Alice access to David's vault\n\n        var allDetails = new List<EmergencyAccessDetails>\n        {\n            bobAliceRecord,\n            carolAliceRecord,\n            davidAliceRecord\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids => ids.Contains(granteeUserIdAlice)))\n            .Returns(allDetails);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync([granteeUserIdAlice]);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 3 &&\n                ids.Contains(bobAliceRecord.Id) &&\n                ids.Contains(carolAliceRecord.Id) &&\n                ids.Contains(davidAliceRecord.Id)));\n        // Each grantor gets one email\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(bobAliceRecord.GrantorEmail)));\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(carolAliceRecord.GrantorEmail)));\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(davidAliceRecord.GrantorEmail)));\n    }\n\n    /// <summary>\n    /// Verifies that when a single user ID is a grantor with multiple grantees,\n    /// all records are deleted and the grantor is notified about their grantees being removed.\n    ///\n    /// Scenario: Bob is a grantor who has given Alice and Carol emergency access to his vault.\n    /// When Bob is removed, Bob receives ONE email listing both Alice and Carol.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_SingleUserIdAsGrantorOnly_NotifiesGrantorAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails bobAliceRecord,\n        EmergencyAccessDetails bobCarolRecord,\n        Guid grantorUserIdBob)\n    {\n        // Bob (grantor) has given Alice and Carol emergency access to his vault\n        bobAliceRecord.GrantorId = grantorUserIdBob; // Bob granted Alice access to his vault\n        bobCarolRecord.GrantorId = grantorUserIdBob; // Bob granted Carol access to his vault\n        bobAliceRecord.GrantorEmail = \"bob@example.com\";\n        bobCarolRecord.GrantorEmail = \"bob@example.com\";\n\n        var allDetails = new List<EmergencyAccessDetails>\n        {\n            bobAliceRecord,\n            bobCarolRecord\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids => ids.Contains(grantorUserIdBob)))\n            .Returns(allDetails);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync([grantorUserIdBob]);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 2 &&\n                ids.Contains(bobAliceRecord.Id) &&\n                ids.Contains(bobCarolRecord.Id)));\n        // Grantor receives one email listing both their grantees being removed\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(bobAliceRecord.GrantorEmail) &&\n                mail.View.RemovedGranteeEmails.Contains(bobAliceRecord.GranteeEmail) &&\n                mail.View.RemovedGranteeEmails.Contains(bobCarolRecord.GranteeEmail)));\n    }\n\n    /// <summary>\n    /// Verifies that when a user ID is both a grantor and a grantee,\n    /// all affected grantors are notified: the user's grantors (for grantee role)\n    /// AND the user themselves (for grantor role with their grantees).\n    ///\n    /// Scenario: Bob plays both roles:\n    /// - As GRANTEE: Bob has emergency access to Alice's and Carol's vaults\n    /// - As GRANTOR: Bob has given David and Emma emergency access to his vault\n    /// When Bob is removed, THREE emails are sent:\n    /// 1. Alice receives email: \"Bob removed\"\n    /// 2. Carol receives email: \"Bob removed\"\n    /// 3. Bob receives email: \"David and Emma removed\" (notified about his own grantees)\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_SingleUserIdBothRoles_NotifiesAllGrantorsAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails aliceBobRecord,\n        EmergencyAccessDetails carolBobRecord,\n        EmergencyAccessDetails bobDavidRecord,\n        EmergencyAccessDetails bobEmmaRecord,\n        Guid userIdBob)\n    {\n        // Bob as GRANTEE: has emergency access to Alice's and Carol's vaults\n        aliceBobRecord.GranteeId = userIdBob; // Alice granted Bob access to Alice's vault\n        aliceBobRecord.GranteeEmail = \"bob@example.com\";\n        carolBobRecord.GranteeId = userIdBob; // Carol granted Bob access to Carol's vault\n        carolBobRecord.GranteeEmail = \"bob@example.com\";\n\n        // Bob as GRANTOR: has given David and Emma emergency access to his vault\n        bobDavidRecord.GrantorId = userIdBob; // Bob granted David access to his vault\n        bobDavidRecord.GrantorEmail = \"bob@example.com\";\n        bobEmmaRecord.GrantorId = userIdBob; // Bob granted Emma access to his vault\n        bobEmmaRecord.GrantorEmail = \"bob@example.com\";\n\n        var allDetails = new List<EmergencyAccessDetails>\n        {\n            aliceBobRecord,\n            carolBobRecord,\n            bobDavidRecord,\n            bobEmmaRecord\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids => ids.Contains(userIdBob)))\n            .Returns(allDetails);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync([userIdBob]);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 4 &&\n                ids.Contains(aliceBobRecord.Id) &&\n                ids.Contains(carolBobRecord.Id) &&\n                ids.Contains(bobDavidRecord.Id) &&\n                ids.Contains(bobEmmaRecord.Id)));\n\n        // Email 1: Alice receives \"Bob removed\" (Bob was grantee to Alice's vault)\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(aliceBobRecord.GrantorEmail) &&\n                mail.View.RemovedGranteeEmails.Contains(\"bob@example.com\")));\n\n        // Email 2: Carol receives \"Bob removed\" (Bob was grantee to Carol's vault)\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(carolBobRecord.GrantorEmail) &&\n                mail.View.RemovedGranteeEmails.Contains(\"bob@example.com\")));\n\n        // Email 3: Bob receives \"David and Emma removed\" (Bob was grantor, his grantees removed)\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(\"bob@example.com\") &&\n                mail.View.RemovedGranteeEmails.Contains(bobDavidRecord.GranteeEmail) &&\n                mail.View.RemovedGranteeEmails.Contains(bobEmmaRecord.GranteeEmail)));\n    }\n\n    /// <summary>\n    /// Verifies that multiple user IDs as grantees are properly deleted\n    /// and their respective grantors are notified.\n    ///\n    /// Scenario: Alice and Bob are both grantees (to different grantors' vaults).\n    /// - Alice has emergency access to Carol's vault\n    /// - Bob has emergency access to David's vault\n    /// When Alice and Bob are removed, Carol and David each receive separate email notifications.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_MultipleUserIdsAllGrantees_SendsMultipleEmailsAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails carolAliceRecord,\n        EmergencyAccessDetails davidBobRecord,\n        Guid granteeUserIdAlice,\n        Guid granteeUserIdBob)\n    {\n        carolAliceRecord.GranteeId = granteeUserIdAlice; // Carol granted Alice access to Carol's vault\n        davidBobRecord.GranteeId = granteeUserIdBob; // David granted Bob access to David's vault\n\n        var allDetails = new List<EmergencyAccessDetails>\n        {\n            carolAliceRecord,\n            davidBobRecord\n        };\n\n        var userIds = new List<Guid> { granteeUserIdAlice, granteeUserIdBob };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(userIds)\n            .Returns(allDetails);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync(userIds);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 2 &&\n                ids.Contains(carolAliceRecord.Id) &&\n                ids.Contains(davidBobRecord.Id)));\n        // Carol gets email about Alice being removed\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(carolAliceRecord.GrantorEmail)));\n        // David gets email about Bob being removed\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(davidBobRecord.GrantorEmail)));\n    }\n\n    /// <summary>\n    /// Verifies that multiple user IDs as grantors are properly deleted\n    /// and each grantor is notified about their grantees being removed.\n    ///\n    /// Scenario: Bob and Carol are both grantors (vault owners with grantees).\n    /// - Bob has given Alice emergency access to his vault\n    /// - Carol has given David emergency access to her vault\n    /// When Bob and Carol are removed, they each receive separate email notifications:\n    /// - Bob receives email: \"Alice removed\"\n    /// - Carol receives email: \"David removed\"\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_MultipleUserIdsAllGrantors_NotifiesEachGrantorAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        EmergencyAccessDetails bobAliceRecord,\n        EmergencyAccessDetails carolDavidRecord,\n        Guid grantorUserIdBob,\n        Guid grantorUserIdCarol)\n    {\n        bobAliceRecord.GrantorId = grantorUserIdBob; // Bob (grantor) has given Alice emergency access\n        carolDavidRecord.GrantorId = grantorUserIdCarol; // Carol (grantor) has given David emergency access\n\n        var allDetails = new List<EmergencyAccessDetails>\n        {\n            bobAliceRecord,\n            carolDavidRecord\n        };\n\n        var userIds = new List<Guid> { grantorUserIdBob, grantorUserIdCarol };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(userIds)\n            .Returns(allDetails);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync(userIds);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 2 &&\n                ids.Contains(bobAliceRecord.Id) &&\n                ids.Contains(carolDavidRecord.Id)));\n\n        // Bob gets email about Alice being removed\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(bobAliceRecord.GrantorEmail) &&\n                mail.View.RemovedGranteeEmails.Contains(bobAliceRecord.GranteeEmail)));\n\n        // Carol gets email about David being removed\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(carolDavidRecord.GrantorEmail) &&\n                mail.View.RemovedGranteeEmails.Contains(carolDavidRecord.GranteeEmail)));\n    }\n\n    /// <summary>\n    /// Verifies that when multiple grantees share overlapping grantors,\n    /// each grantor receives exactly one email with only their specific removed grantees.\n    ///\n    /// Scenario: Ali and Bob are grantees being removed, with overlapping grantors:\n    /// - Cara granted Ali emergency access to her vault\n    /// - Dave granted Ali and Bob emergency access to his vault\n    /// - Eve granted Bob emergency access to her vault\n    /// Expected email notifications:\n    /// - Cara receives email: \"Ali removed\" (only Ali, not Bob)\n    /// - Dave receives email: \"Ali and Bob removed\" (both, since Dave is shared)\n    /// - Eve receives email: \"Bob removed\" (only Bob, not Ali)\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_MultipleUsersOverlappingGrantors_EachGrantorGetsCorrectSubsetAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        Guid granteeUserIdAli,\n        Guid granteeUserIdBob,\n        string grantorEmailCara,\n        string grantorEmailDave,\n        string grantorEmailEve,\n        string granteeEmailAli,\n        string granteeEmailBob)\n    {\n        // GrantorId is not set on these records as the command only uses GrantorEmail for\n        // grouping and notification — GrantorId plays no role in the logic under test.\n\n        // Cara (grantor) granted Ali emergency access to her vault\n        var caraAliRecord = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GranteeId = granteeUserIdAli,\n            GranteeEmail = granteeEmailAli,\n            GrantorEmail = grantorEmailCara\n        };\n\n        // Dave (grantor) granted Ali emergency access to his vault\n        var daveAliRecord = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GranteeId = granteeUserIdAli,\n            GranteeEmail = granteeEmailAli,\n            GrantorEmail = grantorEmailDave // Dave also granted Bob access\n        };\n\n        // Dave (grantor) granted Bob emergency access to his vault\n        var daveBobRecord = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GranteeId = granteeUserIdBob,\n            GranteeEmail = granteeEmailBob,\n            GrantorEmail = grantorEmailDave // Dave also granted Ali access\n        };\n\n        // Eve (grantor) granted Bob emergency access to her vault\n        var eveBobRecord = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GranteeId = granteeUserIdBob,\n            GranteeEmail = granteeEmailBob,\n            GrantorEmail = grantorEmailEve\n        };\n\n        var allDetails = new List<EmergencyAccessDetails> { caraAliRecord, daveAliRecord, daveBobRecord, eveBobRecord };\n        var userIdsToDelete = new List<Guid> { granteeUserIdAli, granteeUserIdBob };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(userIdsToDelete)\n            .Returns(allDetails);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync(userIdsToDelete);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 4 &&\n                ids.Contains(caraAliRecord.Id) &&\n                ids.Contains(daveAliRecord.Id) &&\n                ids.Contains(daveBobRecord.Id) &&\n                ids.Contains(eveBobRecord.Id)));\n        // Cara gets email with only Ali\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(grantorEmailCara) &&\n                mail.View.RemovedGranteeEmails.Contains(granteeEmailAli)));\n        // Dave gets email with both Ali and Bob (shared grantor)\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(grantorEmailDave) &&\n                mail.View.RemovedGranteeEmails.Contains(granteeEmailAli) &&\n                mail.View.RemovedGranteeEmails.Contains(granteeEmailBob)));\n        // Eve gets email with only Bob\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(grantorEmailEve) &&\n                mail.View.RemovedGranteeEmails.Contains(granteeEmailBob)));\n    }\n\n    /// <summary>\n    /// Verifies that records with null grantee emails are handled gracefully\n    /// and don't cause errors during email notification processing.\n    ///\n    /// Scenario: Bob granted Alice emergency access to his vault. Alice accepted, so EA.Email\n    /// was cleared and only her user account held her email. Alice's account was later deleted,\n    /// leaving both GranteeU.Email (LEFT JOIN miss) and EA.Email null — so GranteeEmail is null.\n    /// The record is deleted but no email is sent because there's no valid grantee email to include.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_NullGranteeEmail_HandledGracefullyAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        Guid granteeUserIdAlice,\n        string grantorEmailBob)\n    {\n        // Alice accepted EA (EA.Email cleared), then her account was deleted (LEFT JOIN miss) — GranteeEmail is null\n        var bobAliceRecord = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GranteeId = granteeUserIdAlice, // Alice's user ID (account since deleted)\n            GranteeEmail = null, // Null: EA.Email was cleared on accept, user account no longer exists\n            GrantorEmail = grantorEmailBob // Bob's email\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids => ids.Contains(granteeUserIdAlice)))\n            .Returns([bobAliceRecord]);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync([granteeUserIdAlice]);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 1 && ids.Contains(bobAliceRecord.Id)));\n        // Email should not be sent if grantee email is null\n        await sutProvider.GetDependency<IMailer>()\n            .DidNotReceiveWithAnyArgs()\n            .SendEmail<EmergencyAccessRemoveGranteesMailView>(default);\n        sutProvider.GetDependency<ILogger<DeleteEmergencyAccessCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Warning,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString().Contains(granteeUserIdAlice.ToString())\n                    && o.ToString().Contains(\"missing GranteeEmail\")),\n                null,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    /// <summary>\n    /// Verifies that records with null grantor emails are filtered out\n    /// and don't cause errors during email notification processing.\n    ///\n    /// Scenario: Bob granted Alice emergency access to his vault, then Bob's account was deleted.\n    /// Unlike GranteeEmail (which falls back to EA.Email), GrantorEmail has no fallback — it comes\n    /// entirely from the LEFT JOIN on the User table. When the grantor's account is deleted,\n    /// the LEFT JOIN misses and GrantorEmail is null. The record is deleted but no email is sent.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_NullGrantorEmail_DeletesButDoesNotSendEmailAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        Guid grantorUserIdBob,\n        string granteeEmailAlice)\n    {\n        // Bob's account was deleted — LEFT JOIN misses, no fallback column exists for GrantorEmail\n        var bobAliceRecord = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GrantorId = grantorUserIdBob, // Bob's user ID (account since deleted)\n            GrantorEmail = null, // Null: no EA.Email fallback exists for grantors, account no longer exists\n            GranteeEmail = granteeEmailAlice // Alice's email\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids => ids.Contains(grantorUserIdBob)))\n            .Returns([bobAliceRecord]);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync([grantorUserIdBob]);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 1 && ids.Contains(bobAliceRecord.Id)));\n        // Email should not be sent if grantor email is null\n        await sutProvider.GetDependency<IMailer>()\n            .DidNotReceiveWithAnyArgs()\n            .SendEmail<EmergencyAccessRemoveGranteesMailView>(default);\n        sutProvider.GetDependency<ILogger<DeleteEmergencyAccessCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Warning,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString().Contains(grantorUserIdBob.ToString())\n                    && o.ToString().Contains(\"missing GrantorEmail\")),\n                null,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    /// <summary>\n    /// Verifies that duplicate grantee emails for the same grantor are deduplicated\n    /// so the grantor receives exactly one email listing each grantee address only once.\n    ///\n    /// Scenario: Bob has two EA records pointing to the same grantee email (e.g., from\n    /// a re-invite edge case where the prior record wasn't cleaned up). When Bob is removed,\n    /// he receives ONE email listing the grantee's email only once — not duplicated.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_DuplicateGranteeEmails_DeduplicatesEmailsInNotificationAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        Guid grantorUserIdBob,\n        string granteeEmailAlice)\n    {\n        const string grantorEmailBob = \"bob@example.com\";\n\n        // Two records sharing the same grantor and grantee email — grantee email should be deduplicated\n        var bobAliceRecord1 = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GrantorId = grantorUserIdBob,\n            GrantorEmail = grantorEmailBob,\n            GranteeEmail = granteeEmailAlice\n        };\n        var bobAliceRecord2 = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GrantorId = grantorUserIdBob,\n            GrantorEmail = grantorEmailBob,\n            GranteeEmail = granteeEmailAlice // Same grantee email as record 1\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids => ids.Contains(grantorUserIdBob)))\n            .Returns([bobAliceRecord1, bobAliceRecord2]);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync([grantorUserIdBob]);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 2 &&\n                ids.Contains(bobAliceRecord1.Id) &&\n                ids.Contains(bobAliceRecord2.Id)));\n        // Bob receives one email with the grantee email appearing exactly once\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(grantorEmailBob) &&\n                mail.View.RemovedGranteeEmails.Count() == 1 &&\n                mail.View.RemovedGranteeEmails.Contains(granteeEmailAlice)));\n    }\n\n    /// <summary>\n    /// Verifies that when a grantor has multiple grantees and only some have null emails,\n    /// the grantor still receives an email listing only the non-null grantee emails.\n    ///\n    /// Scenario: Bob granted Alice and Carol emergency access to his vault.\n    /// Carol's account was later deleted, leaving her GranteeEmail null.\n    /// When Bob is removed, he receives ONE email listing only Alice — Carol is excluded\n    /// because there is no valid email address to include.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAllByUserIdsAsync_PartialNullGranteeEmails_SendsEmailForNonNullGranteesOnlyAsync(\n        SutProvider<DeleteEmergencyAccessCommand> sutProvider,\n        Guid grantorUserIdBob,\n        Guid granteeUserIdCarol,\n        string granteeEmailAlice)\n    {\n        const string grantorEmailBob = \"bob@example.com\";\n\n        // Alice's record: valid grantee email\n        var bobAliceRecord = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GrantorId = grantorUserIdBob,\n            GrantorEmail = grantorEmailBob,\n            GranteeEmail = granteeEmailAlice\n        };\n\n        // Carol's record: null grantee email (her account was deleted)\n        var bobCarolRecord = new EmergencyAccessDetails\n        {\n            Id = Guid.NewGuid(),\n            GrantorId = grantorUserIdBob,\n            GrantorEmail = grantorEmailBob,\n            GranteeId = granteeUserIdCarol, // Carol's user ID (account since deleted)\n            GranteeEmail = null\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetManyDetailsByUserIdsAsync(Arg.Is<ICollection<Guid>>(ids => ids.Contains(grantorUserIdBob)))\n            .Returns([bobAliceRecord, bobCarolRecord]);\n\n        await sutProvider.Sut.DeleteAllByUserIdsAsync([grantorUserIdBob]);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<ICollection<Guid>>(ids =>\n                ids.Count == 2 &&\n                ids.Contains(bobAliceRecord.Id) &&\n                ids.Contains(bobCarolRecord.Id)));\n        // Bob receives one email listing only Alice (Carol excluded — null grantee email)\n        await sutProvider.GetDependency<IMailer>()\n            .Received(1)\n            .SendEmail(Arg.Is<EmergencyAccessRemoveGranteesMail>(mail =>\n                mail.ToEmails.Contains(grantorEmailBob) &&\n                mail.View.RemovedGranteeEmails.Count() == 1 &&\n                mail.View.RemovedGranteeEmails.Contains(granteeEmailAlice)));\n        // Carol's record (null grantee email) should trigger a warning with her user ID\n        sutProvider.GetDependency<ILogger<DeleteEmergencyAccessCommand>>()\n            .Received(1)\n            .Log(\n                LogLevel.Warning,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString().Contains(granteeUserIdCarol.ToString())\n                    && o.ToString().Contains(\"missing GranteeEmail\")),\n                null,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/EmergencyAccess/EmergencyAccessMailTests.cs",
    "content": "﻿using Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Platform.Mail.Mailer;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.EmergencyAccess;\n\n[SutProviderCustomize]\npublic class EmergencyAccessMailTests\n{\n    // Constant values for all Emergency Access emails\n    private const string _emergencyAccessHelpUrl = \"https://bitwarden.com/help/emergency-access/\";\n    private const string _emergencyAccessMailSubject = \"Emergency contacts removed\";\n\n    /// <summary>\n    /// Documents how to construct and send the emergency access removal email.\n    /// 1. Inject IMailer into their command/service\n    /// 2. Construct EmergencyAccessRemoveGranteesMail as shown below\n    /// 3. Call mailer.SendEmail(mail)\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task SendEmergencyAccessRemoveGranteesEmail_SingleGrantee_Success(\n        string grantorEmail,\n        string granteeEmail)\n    {\n        // Arrange\n        var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n        var deliveryService = Substitute.For<IMailDeliveryService>();\n        var mailer = new Mailer(\n            new HandlebarMailRenderer(logger, globalSettings),\n            deliveryService);\n\n        var mail = new EmergencyAccessRemoveGranteesMail\n        {\n            ToEmails = [grantorEmail],\n            View = new EmergencyAccessRemoveGranteesMailView\n            {\n                RemovedGranteeEmails = [granteeEmail]\n            }\n        };\n\n        MailMessage sentMessage = null;\n        await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message =>\n            sentMessage = message\n        ));\n\n        // Act\n        await mailer.SendEmail(mail);\n\n        // Assert\n        Assert.NotNull(sentMessage);\n        Assert.Contains(grantorEmail, sentMessage.ToEmails);\n\n        // Verify the content contains the grantee name\n        Assert.Contains(granteeEmail, sentMessage.TextContent);\n        Assert.Contains(granteeEmail, sentMessage.HtmlContent);\n    }\n\n    /// <summary>\n    /// Documents handling multiple removed grantees in a single email.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task SendEmergencyAccessRemoveGranteesEmail_MultipleGrantees_RendersAllNames(\n        string grantorEmail)\n    {\n        // Arrange\n        var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n        var deliveryService = Substitute.For<IMailDeliveryService>();\n        var mailer = new Mailer(\n            new HandlebarMailRenderer(logger, globalSettings),\n            deliveryService);\n\n        var granteeEmails = new[] { \"Alice@test.dev\", \"Bob@test.dev\", \"Carol@test.dev\" };\n\n        var mail = new EmergencyAccessRemoveGranteesMail\n        {\n            ToEmails = [grantorEmail],\n            View = new EmergencyAccessRemoveGranteesMailView\n            {\n                RemovedGranteeEmails = granteeEmails\n            }\n        };\n\n        MailMessage sentMessage = null;\n        await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message =>\n            sentMessage = message\n        ));\n\n        // Act\n        await mailer.SendEmail(mail);\n\n        // Assert - All grantee names should appear in the email\n        Assert.NotNull(sentMessage);\n        foreach (var granteeEmail in granteeEmails)\n        {\n            Assert.Contains(granteeEmail, sentMessage.TextContent);\n            Assert.Contains(granteeEmail, sentMessage.HtmlContent);\n        }\n    }\n\n    /// <summary>\n    /// Validates the required GranteeNames for the email view model.\n    /// </summary>\n    [Theory, BitAutoData]\n    public void EmergencyAccessRemoveGranteesMailView_GranteeNames_AreRequired(\n        string grantorEmail)\n    {\n        // Arrange - Shows the minimum required to construct the email\n        var mail = new EmergencyAccessRemoveGranteesMail\n        {\n            ToEmails = [grantorEmail],  // Required: who to send to\n            View = new EmergencyAccessRemoveGranteesMailView\n            {\n                // Required: at least one removed grantee name\n                RemovedGranteeEmails = [\"Example Grantee\"]\n            }\n        };\n\n        // Assert\n        Assert.NotNull(mail);\n        Assert.NotNull(mail.View);\n        Assert.NotEmpty(mail.View.RemovedGranteeEmails);\n    }\n\n    /// <summary>\n    /// Ensure consistency with help pages link and email subject.\n    /// </summary>\n    /// <param name=\"grantorEmail\"></param>\n    /// <param name=\"granteeName\"></param>\n    [Theory, BitAutoData]\n    public void EmergencyAccessRemoveGranteesMailView_SubjectAndHelpLink_MatchesExpectedValues(string grantorEmail, string granteeName)\n    {\n        // Arrange\n        var mail = new EmergencyAccessRemoveGranteesMail\n        {\n            ToEmails = [grantorEmail],\n            View = new EmergencyAccessRemoveGranteesMailView { RemovedGranteeEmails = [granteeName] }\n        };\n\n        // Assert\n        Assert.NotNull(mail);\n        Assert.NotNull(mail.View);\n        Assert.Equal(_emergencyAccessMailSubject, mail.Subject);\n        Assert.Equal(_emergencyAccessHelpUrl, mail.View.EmergencyAccessHelpPageUrl);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/EmergencyAccess/EmergencyAccessServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.UserFeatures.EmergencyAccess;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.EmergencyAccess;\n\n[SutProviderCustomize]\npublic class EmergencyAccessServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task InviteAsync_UserWithOutPremium_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider, User invitingUser, string email, int waitTime)\n    {\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime));\n\n        Assert.Contains(\"Not a premium user.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n                        .DidNotReceiveWithAnyArgs().CreateAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InviteAsync_UserWithKeyConnector_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider, User invitingUser, string email, int waitTime)\n    {\n        invitingUser.UsesKeyConnector = true;\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime));\n\n        Assert.Contains(\"You cannot use Emergency Access Takeover because you are using Key Connector\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n                        .DidNotReceiveWithAnyArgs().CreateAsync(default);\n    }\n\n    [Theory]\n    // Case 1: grantor and contact email are identical\n    // Case 2: grantor and contact email match case-insensitively\n    [BitAutoData(\"test@example.com\", \"test@example.com\")]\n    [BitAutoData(\"test@example.com\", \"TEST@EXAMPLE.COM\")]\n    public async Task InviteAsync_GrantorInvitesSelf_ThrowsBadRequest(\n        string grantorEmail, string contactEmail, SutProvider<EmergencyAccessService> sutProvider, User invitingUser, int waitTime)\n    {\n        invitingUser.Email = grantorEmail;\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteAsync(invitingUser, contactEmail, EmergencyAccessType.View, waitTime));\n\n        Assert.Contains(\"You cannot add yourself as an emergency access contact.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n                        .DidNotReceiveWithAnyArgs().CreateAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData(EmergencyAccessType.Takeover)]\n    [BitAutoData(EmergencyAccessType.View)]\n    public async Task InviteAsync_ReturnsEmergencyAccessObject(\n        EmergencyAccessType accessType, SutProvider<EmergencyAccessService> sutProvider, User invitingUser, string email, int waitTime)\n    {\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);\n\n        var result = await sutProvider.Sut.InviteAsync(invitingUser, email, accessType, waitTime);\n\n        Assert.NotNull(result);\n        Assert.Equal(accessType, result.Type);\n        Assert.Equal(invitingUser.Id, result.GrantorId);\n        Assert.Equal(email, result.Email);\n        Assert.Equal(EmergencyAccessStatusType.Invited, result.Status);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n                         .Received(1)\n                         .CreateAsync(Arg.Any<Core.Auth.Entities.EmergencyAccess>());\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n                   .Received(1)\n                   .Protect(Arg.Any<EmergencyAccessInviteTokenable>());\n        await sutProvider.GetDependency<IMailService>()\n                         .Received(1)\n                         .SendEmergencyAccessInviteEmailAsync(Arg.Any<Core.Auth.Entities.EmergencyAccess>(), Arg.Any<string>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task InviteAsync_FeatureFlagEnabled_GrantorInAutoConfirmOrg_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider, User invitingUser, string email, int waitTime)\n    {\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(invitingUser.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([\n                new PolicyDetails { OrganizationUserStatus = OrganizationUserStatusType.Confirmed }\n            ]));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime));\n\n        Assert.Contains(\"You cannot invite emergency contacts because you are a member of an organization that uses Automatic User Confirmation.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs().CreateAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InviteAsync_FeatureFlagEnabled_GrantorNotInAutoConfirmOrg_Succeeds(\n        SutProvider<EmergencyAccessService> sutProvider, User invitingUser, string email, int waitTime)\n    {\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(invitingUser.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([]));\n\n        var result = await sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime);\n\n        Assert.NotNull(result);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1).CreateAsync(Arg.Any<Core.Auth.Entities.EmergencyAccess>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task InviteAsync_FeatureFlagDisabled_GrantorInAutoConfirmOrg_Succeeds(\n        SutProvider<EmergencyAccessService> sutProvider, User invitingUser, string email, int waitTime)\n    {\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(false);\n\n        var result = await sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime);\n\n        Assert.NotNull(result);\n        await sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .DidNotReceiveWithAnyArgs()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAsync_EmergencyAccessNull_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider, User user)\n    {\n        EmergencyAccessDetails emergencyAccess = null;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetDetailsByIdGrantorIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.GetAsync(new Guid(), user.Id));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResendInviteAsync_EmergencyAccessNull_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User invitingUser,\n        Guid emergencyAccessId)\n    {\n        Core.Auth.Entities.EmergencyAccess emergencyAccess = null;\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IMailService>()\n                .DidNotReceiveWithAnyArgs()\n                .SendEmergencyAccessInviteEmailAsync(default, default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResendInviteAsync_InvitingUserIdNotGrantorUserId_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User invitingUser,\n        Guid emergencyAccessId)\n    {\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Status = EmergencyAccessStatusType.Invited,\n            GrantorId = Guid.NewGuid(),\n            Type = EmergencyAccessType.Takeover,\n        }; ;\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IMailService>()\n                         .DidNotReceiveWithAnyArgs()\n                         .SendEmergencyAccessInviteEmailAsync(default, default, default);\n    }\n\n    [Theory]\n    [BitAutoData(EmergencyAccessStatusType.Accepted)]\n    [BitAutoData(EmergencyAccessStatusType.Confirmed)]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)]\n    public async Task ResendInviteAsync_EmergencyAccessStatusInvalid_ThrowsBadRequest(\n        EmergencyAccessStatusType statusType,\n        SutProvider<EmergencyAccessService> sutProvider,\n        User invitingUser,\n        Guid emergencyAccessId)\n    {\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Status = statusType,\n            GrantorId = invitingUser.Id,\n            Type = EmergencyAccessType.Takeover,\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IMailService>()\n                         .DidNotReceiveWithAnyArgs()\n                         .SendEmergencyAccessInviteEmailAsync(default, default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ResendInviteAsync_SendsInviteAsync(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User invitingUser,\n        Guid emergencyAccessId)\n    {\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Status = EmergencyAccessStatusType.Invited,\n            GrantorId = invitingUser.Id,\n            Type = EmergencyAccessType.Takeover,\n        }; ;\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(emergencyAccess);\n\n        await sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n                         .Received(1)\n                         .Protect(Arg.Any<EmergencyAccessInviteTokenable>());\n        await sutProvider.GetDependency<IMailService>()\n                         .Received(1)\n                         .SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUser.Name, Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_EmergencyAccessNull_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider, User acceptingUser, string token)\n    {\n        Core.Auth.Entities.EmergencyAccess emergencyAccess = null;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(new Guid(), acceptingUser, token, sutProvider.GetDependency<IUserService>()));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_CannotUnprotectToken_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User acceptingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string token)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n        .TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())\n        .Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>()));\n\n        Assert.Contains(\"Invalid token.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_TokenDataInvalid_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User acceptingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        Core.Auth.Entities.EmergencyAccess wrongEmergencyAccess,\n        string token)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n                {\n                    callInfo[1] = new EmergencyAccessInviteTokenable(wrongEmergencyAccess, 1);\n                    return true;\n                });\n\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>()));\n\n        Assert.Contains(\"Invalid token.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_AcceptedStatus_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User acceptingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string token)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.Accepted;\n        emergencyAccess.Email = acceptingUser.Email;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);\n                return true;\n            });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>()));\n\n        Assert.Contains(\"Invitation already accepted. You will receive an email when the grantor confirms you as an emergency access contact.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_NotInvitedStatus_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User acceptingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string token)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.Confirmed;\n        emergencyAccess.Email = acceptingUser.Email;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);\n                return true;\n            });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>()));\n\n        Assert.Contains(\"Invitation already accepted.\", exception.Message);\n    }\n\n    [Theory(Skip = \"Code not reachable, Tokenable checks email match in IsValid()\"), BitAutoData]\n    public async Task AcceptUserAsync_EmergencyAccessEmailDoesNotMatch_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User acceptingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string token)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.Invited;\n        emergencyAccess.Email = acceptingUser.Email;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);\n                return true;\n            });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>()));\n\n        Assert.Contains(\"User email does not match invite.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_FeatureFlagEnabled_GranteeInAutoConfirmOrg_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User acceptingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string token)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.Invited;\n        emergencyAccess.Email = acceptingUser.Email;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);\n                return true;\n            });\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(acceptingUser.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([\n                new PolicyDetails { OrganizationUserStatus = OrganizationUserStatusType.Confirmed }\n            ]));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>()));\n\n        Assert.Contains(\"You cannot accept emergency access invitations because you are a member of an organization that uses Automatic User Confirmation.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_FeatureFlagEnabled_GranteeNotInAutoConfirmOrg_Succeeds(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User acceptingUser,\n        User invitingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string token)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.Invited;\n        emergencyAccess.Email = acceptingUser.Email;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(Arg.Any<Guid>())\n            .Returns(invitingUser);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);\n                return true;\n            });\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(acceptingUser.Id)\n            .Returns(new AutomaticUserConfirmationPolicyRequirement([]));\n\n        await sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>());\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Accepted));\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_FeatureFlagDisabled_GranteeInAutoConfirmOrg_Succeeds(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User acceptingUser,\n        User invitingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string token)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.Invited;\n        emergencyAccess.Email = acceptingUser.Email;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(Arg.Any<Guid>())\n            .Returns(invitingUser);\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);\n                return true;\n            });\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)\n            .Returns(false);\n\n        await sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>());\n\n        await sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .DidNotReceiveWithAnyArgs()\n            .GetAsync<AutomaticUserConfirmationPolicyRequirement>(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task AcceptUserAsync_ReplaceEmergencyAccess_SendsEmail_Success(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User acceptingUser,\n        User invitingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string token)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.Invited;\n        emergencyAccess.Email = acceptingUser.Email;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserService>()\n                .GetUserByIdAsync(Arg.Any<Guid>())\n                .Returns(invitingUser);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);\n                return true;\n            });\n\n        await sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>());\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .Received(1)\n                .ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Accepted));\n\n        await sutProvider.GetDependency<IMailService>()\n                .Received(1)\n                .SendEmergencyAccessAcceptedEmailAsync(acceptingUser.Email, invitingUser.Email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_EmergencyAccessNull_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User invitingUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns((Core.Auth.Entities.EmergencyAccess)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_EmergencyAccessGrantorIdNotEqual_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User randomUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess)\n    {\n        emergencyAccess.GrantorId = Guid.NewGuid();\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, randomUser.Id));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_EmergencyAccessGranteeIdNotEqual_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User randomUser,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess)\n    {\n        emergencyAccess.GranteeId = Guid.NewGuid();\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, randomUser.Id));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_GrantorDeletes_Success(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User grantor,\n        User grantee,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess)\n    {\n        emergencyAccess.GrantorId = grantor.Id;\n        emergencyAccess.GranteeId = grantee.Id;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        await sutProvider.Sut.DeleteAsync(emergencyAccess.Id, grantor.Id);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteAsync(emergencyAccess);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_GranteeDeletes_Success(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User grantor,\n        User grantee,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess)\n    {\n        emergencyAccess.GrantorId = grantor.Id;\n        emergencyAccess.GranteeId = grantee.Id;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        await sutProvider.Sut.DeleteAsync(emergencyAccess.Id, grantee.Id);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .DeleteAsync(emergencyAccess);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_EmergencyAccessNull_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string key,\n        User grantorUser)\n    {\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns((Core.Auth.Entities.EmergencyAccess)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_EmergencyAccessStatusIsNotAccepted_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string key,\n        User grantorUser)\n    {\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(emergencyAccess.Id)\n                .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_EmergencyAccessGrantorIdNotEqualToConfirmingUserId_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string key,\n        User grantorUser)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider, User confirmingUser, string key)\n    {\n        confirmingUser.UsesKeyConnector = true;\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Status = EmergencyAccessStatusType.Accepted,\n            GrantorId = confirmingUser.Id,\n            Type = EmergencyAccessType.Takeover,\n        };\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(confirmingUser.Id)\n            .Returns(confirmingUser);\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ConfirmUserAsync(new Guid(), key, confirmingUser.Id));\n\n        Assert.Contains(\"You cannot use Emergency Access Takeover because you are using Key Connector\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConfirmUserAsync_ConfirmsAndReplacesEmergencyAccess_Success(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        string key,\n        User grantorUser,\n        User granteeUser)\n    {\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.Accepted;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .GetByIdAsync(Arg.Any<Guid>())\n                .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(grantorUser.Id)\n            .Returns(grantorUser);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(emergencyAccess.GranteeId.Value)\n            .Returns(granteeUser);\n\n        await sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n                .Received(1)\n                .ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendEmergencyAccessConfirmedEmailAsync(grantorUser.Name, granteeUser.Email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_PremiumCannotUpdate_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider, User savingUser)\n    {\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Type = EmergencyAccessType.Takeover,\n            GrantorId = savingUser.Id,\n        };\n\n        sutProvider.GetDependency<IUserService>()\n            .CanAccessPremium(savingUser)\n            .Returns(false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser));\n\n        Assert.Contains(\"Not a premium user.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_EmergencyAccessGrantorIdNotEqualToSavingUserId_ThrowsBadRequest(\n    SutProvider<EmergencyAccessService> sutProvider, User savingUser)\n    {\n        savingUser.Premium = true;\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Type = EmergencyAccessType.Takeover,\n            GrantorId = new Guid(),\n        };\n\n        sutProvider.GetDependency<IUserService>()\n            .GetUserByIdAsync(savingUser.Id)\n            .Returns(savingUser);\n        sutProvider.GetDependency<IUserService>()\n            .CanAccessPremium(savingUser)\n            .Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_GrantorUserWithKeyConnectorCannotTakeover_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider, User grantorUser)\n    {\n        grantorUser.UsesKeyConnector = true;\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Type = EmergencyAccessType.Takeover,\n            GrantorId = grantorUser.Id,\n        };\n\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser);\n        userService.CanAccessPremium(grantorUser).Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser));\n\n        Assert.Contains(\"You cannot use Emergency Access Takeover because you are using Key Connector\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_GrantorUserWithKeyConnectorCanView_SavesEmergencyAccess(\n        SutProvider<EmergencyAccessService> sutProvider, User grantorUser)\n    {\n        grantorUser.UsesKeyConnector = true;\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Type = EmergencyAccessType.View,\n            GrantorId = grantorUser.Id,\n        };\n\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser);\n        userService.CanAccessPremium(grantorUser).Returns(true);\n\n        await sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .ReplaceAsync(emergencyAccess);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SaveAsync_ValidRequest_SavesEmergencyAccess(\n        SutProvider<EmergencyAccessService> sutProvider, User grantorUser)\n    {\n        grantorUser.UsesKeyConnector = false;\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Type = EmergencyAccessType.Takeover,\n            GrantorId = grantorUser.Id,\n        };\n\n        var userService = sutProvider.GetDependency<IUserService>();\n        userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser);\n        userService.CanAccessPremium(grantorUser).Returns(true);\n\n        await sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .ReplaceAsync(emergencyAccess);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateAsync_EmergencyAccessNull_ThrowBadRequest(\n    SutProvider<EmergencyAccessService> sutProvider, User initiatingUser)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns((Core.Auth.Entities.EmergencyAccess)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateAsync_EmergencyAccessGranteeIdNotEqual_ThrowBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User initiatingUser)\n    {\n        emergencyAccess.GranteeId = new Guid();\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(emergencyAccess.Id)\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateAsync_EmergencyAccessStatusIsNotConfirmed_ThrowBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User initiatingUser)\n    {\n        emergencyAccess.GranteeId = initiatingUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.Invited;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(emergencyAccess.Id)\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)\n    {\n        grantor.UsesKeyConnector = true;\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Status = EmergencyAccessStatusType.Confirmed,\n            GranteeId = initiatingUser.Id,\n            GrantorId = grantor.Id,\n            Type = EmergencyAccessType.Takeover,\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(grantor.Id)\n            .Returns(grantor);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser));\n\n        Assert.Contains(\"You cannot takeover an account that is using Key Connector\", exception.Message);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateAsync_UserWithKeyConnectorCanView_Success(\n        SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)\n    {\n        grantor.UsesKeyConnector = true;\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Status = EmergencyAccessStatusType.Confirmed,\n            GranteeId = initiatingUser.Id,\n            GrantorId = grantor.Id,\n            Type = EmergencyAccessType.View,\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(grantor.Id)\n            .Returns(grantor);\n\n        await sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));\n    }\n\n    [Theory, BitAutoData]\n    public async Task InitiateAsync_RequestIsCorrect_Success(\n        SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)\n    {\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            Status = EmergencyAccessStatusType.Confirmed,\n            GranteeId = initiatingUser.Id,\n            GrantorId = grantor.Id,\n            Type = EmergencyAccessType.Takeover,\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(grantor.Id)\n            .Returns(grantor);\n\n        await sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ApproveAsync_EmergencyAccessNull_ThrowsBadrequest(\n        SutProvider<EmergencyAccessService> sutProvider)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns((Core.Auth.Entities.EmergencyAccess)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ApproveAsync(new Guid(), null));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ApproveAsync_EmergencyAccessGrantorIdNotEquatToApproving_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User grantorUser)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(EmergencyAccessStatusType.Invited)]\n    [BitAutoData(EmergencyAccessStatusType.Accepted)]\n    [BitAutoData(EmergencyAccessStatusType.Confirmed)]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)]\n    public async Task ApproveAsync_EmergencyAccessStatusNotRecoveryInitiated_ThrowsBadRequest(\n        EmergencyAccessStatusType statusType,\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User grantorUser)\n    {\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.Status = statusType;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ApproveAsync_Success(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User grantorUser,\n        User granteeUser)\n    {\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(granteeUser);\n\n        await sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser);\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryApproved));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RejectAsync_EmergencyAccessIdNull_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User GrantorUser)\n    {\n        emergencyAccess.GrantorId = GrantorUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.Accepted;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns((Core.Auth.Entities.EmergencyAccess)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RejectAsync_EmergencyAccessGrantorIdNotEqualToRequestUser_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User GrantorUser)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.Accepted;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(EmergencyAccessStatusType.Invited)]\n    [BitAutoData(EmergencyAccessStatusType.Accepted)]\n    [BitAutoData(EmergencyAccessStatusType.Confirmed)]\n    public async Task RejectAsync_EmergencyAccessStatusNotValid_ThrowsBadRequest(\n        EmergencyAccessStatusType statusType,\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User GrantorUser)\n    {\n        emergencyAccess.GrantorId = GrantorUser.Id;\n        emergencyAccess.Status = statusType;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)]\n    public async Task RejectAsync_Success(\n        EmergencyAccessStatusType statusType,\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User GrantorUser,\n        User GranteeUser)\n    {\n        emergencyAccess.GrantorId = GrantorUser.Id;\n        emergencyAccess.Status = statusType;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(GranteeUser);\n\n        await sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser);\n\n        await sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPoliciesAsync_RequestNotValidEmergencyAccessNull_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns((Core.Auth.Entities.EmergencyAccess)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.GetPoliciesAsync(default, default));\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(EmergencyAccessStatusType.Invited)]\n    [BitAutoData(EmergencyAccessStatusType.Accepted)]\n    [BitAutoData(EmergencyAccessStatusType.Confirmed)]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)]\n    public async Task GetPoliciesAsync_RequestNotValidStatusType_ThrowsBadRequest(\n        EmergencyAccessStatusType statusType,\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Status = statusType;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser));\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPoliciesAsync_RequestNotValidType_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.View;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser));\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task GetPoliciesAsync_OrganizationUserTypeNotOwner_ReturnsNull(\n        OrganizationUserType userType,\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser,\n        User grantorUser,\n        OrganizationUser grantorOrganizationUser)\n    {\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(emergencyAccess.GrantorId)\n            .Returns(grantorUser);\n\n        grantorOrganizationUser.UserId = grantorUser.Id;\n        grantorOrganizationUser.Type = userType;\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(grantorUser.Id)\n            .Returns([grantorOrganizationUser]);\n\n        var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser);\n        Assert.Null(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPoliciesAsync_OrganizationUserEmpty_ReturnsNull(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser,\n        User grantorUser)\n    {\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(emergencyAccess.GrantorId)\n            .Returns(grantorUser);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(grantorUser.Id)\n            .Returns([]);\n\n\n        var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser);\n        Assert.Null(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPoliciesAsync_ReturnsNotNull(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser,\n        User grantorUser,\n        OrganizationUser grantorOrganizationUser)\n    {\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(emergencyAccess.GrantorId)\n            .Returns(grantorUser);\n\n        grantorOrganizationUser.UserId = grantorUser.Id;\n        grantorOrganizationUser.Type = OrganizationUserType.Owner;\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(grantorUser.Id)\n            .Returns([grantorOrganizationUser]);\n\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetManyByUserIdAsync(grantorUser.Id)\n            .Returns([]);\n\n        var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser);\n        Assert.NotNull(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task TakeoverAsync_RequestNotValid_EmergencyAccessIsNull_ThrowsBadRequest(\n    SutProvider<EmergencyAccessService> sutProvider)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns((Core.Auth.Entities.EmergencyAccess)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.TakeoverAsync(default, default));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task TakeoverAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest(\n    SutProvider<EmergencyAccessService> sutProvider,\n    Core.Auth.Entities.EmergencyAccess emergencyAccess,\n    User granteeUser)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(EmergencyAccessStatusType.Invited)]\n    [BitAutoData(EmergencyAccessStatusType.Accepted)]\n    [BitAutoData(EmergencyAccessStatusType.Confirmed)]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)]\n    public async Task TakeoverAsync_RequestNotValid_StatusType_ThrowsBadRequest(\n        EmergencyAccessStatusType statusType,\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Status = statusType;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task TakeoverAsync_RequestNotValid_TypeIsView_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.View;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task TakeoverAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        User granteeUser,\n        User grantor)\n    {\n        grantor.UsesKeyConnector = true;\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            GrantorId = grantor.Id,\n            GranteeId = granteeUser.Id,\n            Status = EmergencyAccessStatusType.RecoveryApproved,\n            Type = EmergencyAccessType.Takeover,\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(grantor.Id)\n            .Returns(grantor);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser));\n\n        Assert.Contains(\"You cannot takeover an account that is using Key Connector\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task TakeoverAsync_Success_ReturnsEmergencyAccessAndGrantorUser(\n    SutProvider<EmergencyAccessService> sutProvider,\n    User granteeUser,\n    User grantor)\n    {\n        grantor.UsesKeyConnector = false;\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            GrantorId = grantor.Id,\n            GranteeId = granteeUser.Id,\n            Status = EmergencyAccessStatusType.RecoveryApproved,\n            Type = EmergencyAccessType.Takeover,\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(grantor.Id)\n            .Returns(grantor);\n\n        var result = await sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser);\n\n        Assert.Equal(result.Item1, emergencyAccess);\n        Assert.Equal(result.Item2, grantor);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PasswordAsync_RequestNotValid_EmergencyAccessIsNull_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider)\n    {\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns((Core.Auth.Entities.EmergencyAccess)null);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.PasswordAsync(default, default, default, default));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PasswordAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser)\n    {\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(EmergencyAccessStatusType.Invited)]\n    [BitAutoData(EmergencyAccessStatusType.Accepted)]\n    [BitAutoData(EmergencyAccessStatusType.Confirmed)]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)]\n    public async Task PasswordAsync_RequestNotValid_StatusType_ThrowsBadRequest(\n        EmergencyAccessStatusType statusType,\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Status = statusType;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PasswordAsync_RequestNotValid_TypeIsView_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.View;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default));\n\n        Assert.Contains(\"Emergency Access not valid.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PasswordAsync_NonOrgUser_Success(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser,\n        User grantorUser,\n        string key,\n        string passwordHash)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(emergencyAccess.GrantorId)\n            .Returns(grantorUser);\n\n        await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .UpdatePasswordHash(grantorUser, passwordHash);\n        await sutProvider.GetDependency<IUserRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<User>(u => u.VerifyDevices == false && u.Key == key));\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task PasswordAsync_OrgUser_NotOrganizationOwner_RemovedFromOrganization_Success(\n        OrganizationUserType userType,\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser,\n        User grantorUser,\n        OrganizationUser organizationUser,\n        string key,\n        string passwordHash)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(emergencyAccess.GrantorId)\n            .Returns(grantorUser);\n\n        organizationUser.UserId = grantorUser.Id;\n        organizationUser.Type = userType;\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(grantorUser.Id)\n            .Returns([organizationUser]);\n\n        await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .UpdatePasswordHash(grantorUser, passwordHash);\n        await sutProvider.GetDependency<IUserRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<User>(u => u.VerifyDevices == false && u.Key == key));\n        await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()\n            .Received(1)\n            .RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PasswordAsync_OrgUser_IsOrganizationOwner_NotRemovedFromOrganization_Success(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser,\n        User grantorUser,\n        OrganizationUser organizationUser,\n        string key,\n        string passwordHash)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.GrantorId = grantorUser.Id;\n        emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(emergencyAccess.GrantorId)\n            .Returns(grantorUser);\n\n        organizationUser.UserId = grantorUser.Id;\n        organizationUser.Type = OrganizationUserType.Owner;\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(grantorUser.Id)\n            .Returns([organizationUser]);\n\n        await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .UpdatePasswordHash(grantorUser, passwordHash);\n        await sutProvider.GetDependency<IUserRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<User>(u => u.VerifyDevices == false && u.Key == key));\n        await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()\n            .Received(0)\n            .RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task PasswordAsync_Disables_NewDeviceVerification_And_TwoFactorProviders_On_The_Grantor(\n        SutProvider<EmergencyAccessService> sutProvider, User requestingUser, User grantor)\n    {\n        grantor.UsesKeyConnector = true;\n        grantor.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new TwoFactorProvider\n            {\n                MetaData = new Dictionary<string, object> { [\"Email\"] = \"asdfasf\" },\n                Enabled = true\n            }\n        });\n        var emergencyAccess = new Core.Auth.Entities.EmergencyAccess\n        {\n            GrantorId = grantor.Id,\n            GranteeId = requestingUser.Id,\n            Status = EmergencyAccessStatusType.RecoveryApproved,\n            Type = EmergencyAccessType.Takeover,\n        };\n\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(grantor.Id)\n            .Returns(grantor);\n\n        await sutProvider.Sut.PasswordAsync(Guid.NewGuid(), requestingUser, \"blablahash\", \"blablakey\");\n\n        Assert.Empty(grantor.GetTwoFactorProviders());\n        Assert.False(grantor.VerifyDevices);\n        await sutProvider.GetDependency<IUserRepository>().Received().ReplaceAsync(grantor);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ViewAsync_EmergencyAccessTypeNotView_ThrowsBadRequest(\n        SutProvider<EmergencyAccessService> sutProvider,\n        Core.Auth.Entities.EmergencyAccess emergencyAccess,\n        User granteeUser)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.ViewAsync(emergencyAccess.Id, granteeUser));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetAttachmentDownloadAsync_EmergencyAccessTypeNotView_ThrowsBadRequest(\n    SutProvider<EmergencyAccessService> sutProvider,\n    Core.Auth.Entities.EmergencyAccess emergencyAccess,\n    User granteeUser)\n    {\n        emergencyAccess.GranteeId = granteeUser.Id;\n        emergencyAccess.Type = EmergencyAccessType.Takeover;\n        sutProvider.GetDependency<IEmergencyAccessRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(emergencyAccess);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.GetAttachmentDownloadAsync(emergencyAccess.Id, default, default, granteeUser));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs",
    "content": "﻿using System.Text;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.UserFeatures.Registration.Implementations;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Tokens;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.DataProtection;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.AspNetCore.WebUtilities;\nusing NSubstitute;\nusing Xunit;\nusing EmergencyAccessEntity = Bit.Core.Auth.Entities.EmergencyAccess;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.Registration;\n\n[SutProviderCustomize]\npublic class RegisterUserCommandTests\n{\n    // -----------------------------------------------------------------------------------------------\n    // RegisterUser tests\n    // -----------------------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUser_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Success);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUser(user);\n\n        // Assert\n        Assert.True(result.Succeeded);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(user);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendWelcomeEmailAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUser_WhenCreateUserFails_ReturnsIdentityResultFailed(SutProvider<RegisterUserCommand> sutProvider, User user)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Failed());\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUser(user);\n\n        // Assert\n        Assert.False(result.Succeeded);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(user);\n\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceive()\n            .SendWelcomeEmailAsync(Arg.Any<User>());\n    }\n\n    // -----------------------------------------------------------------------------------------------\n    // RegisterSSOAutoProvisionedUserAsync tests\n    // -----------------------------------------------------------------------------------------------\n    [Theory, BitAutoData]\n    public async Task RegisterSSOAutoProvisionedUserAsync_Success(\n        User user,\n        Organization organization,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Id = Guid.NewGuid();\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n        organization.Id = Guid.NewGuid();\n        organization.Name = \"Test Organization\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);\n\n        // Assert\n        Assert.True(result.Succeeded);\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RegisterSSOAutoProvisionedUserAsync_UserRegistrationFails_ReturnsFailedResult(\n        User user,\n        Organization organization,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)\n            .Returns(false);\n\n        var expectedError = new IdentityError();\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Failed(expectedError));\n\n        // Act\n        var result = await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);\n\n        // Assert\n        Assert.False(result.Succeeded);\n        Assert.Contains(expectedError, result.Errors);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceive()\n            .SendOrganizationUserWelcomeEmailAsync(Arg.Any<User>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    public async Task RegisterSSOAutoProvisionedUserAsync_EnterpriseOrg_SendsOrganizationWelcomeEmail(\n        PlanType planType,\n        User user,\n        Organization organization,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n        organization.PlanType = planType;\n        organization.Name = \"Enterprise Org\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns((OrganizationUser)null);\n\n        // Act\n        await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserWelcomeEmailAsync(user, organization.Name);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RegisterSSOAutoProvisionedUserAsync_FeatureFlagDisabled_SendsLegacyWelcomeEmail(\n        User user,\n        Organization organization,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)\n            .Returns(false);\n\n        // Act\n        await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendWelcomeEmailAsync(user);\n    }\n\n    // -----------------------------------------------------------------------------------------------\n    // RegisterUserWithOrganizationInviteToken tests\n    // -----------------------------------------------------------------------------------------------\n\n    // Simple happy path test\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaOrganizationInviteToken_NoOrgInviteOrOrgUserIdOrReferenceData_Succeeds(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n        user.ReferenceData = null;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, null, null);\n\n        // Assert\n        Assert.True(result.Succeeded);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(user, masterPasswordHash);\n    }\n\n    // Complex happy path test\n    [Theory]\n    [BitAutoData(false, null)]\n    [BitAutoData(true, \"sampleInitiationPath\")]\n    [BitAutoData(true, \"Secrets Manager trial\")]\n    public async Task RegisterUserViaOrganizationInviteToken_ComplexHappyPath_Succeeds(bool addUserReferenceData, string initiationPath,\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId,\n        [Policy(PolicyType.TwoFactorAuthentication, true)] PolicyStatus policy)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .DisableUserRegistration.Returns(false);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .DisableUserRegistration.Returns(true);\n\n        orgUser.Email = user.Email;\n        orgUser.Id = orgUserId;\n\n        var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()\n            .TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = orgInviteTokenable;\n                return true;\n            });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUserId)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication)\n            .Returns(policy);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        user.ReferenceData = addUserReferenceData ? $\"{{\\\"initiationPath\\\":\\\"{initiationPath}\\\"}}\" : null;\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId);\n\n        // Assert\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetByIdAsync(orgUserId);\n\n        await sutProvider.GetDependency<IPolicyQuery>()\n            .Received(1)\n            .RunAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication);\n\n        sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .SetTwoFactorProvider(user, TwoFactorProviderType.Email);\n\n        // example serialized data: {\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"0dbf746c-deaf-4318-811e-d98ea7155075\"}}}\n        var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new TwoFactorProvider\n            {\n                MetaData = new Dictionary<string, object> { [\"Email\"] = user.Email.ToLowerInvariant() },\n                Enabled = true\n            }\n        };\n\n        var serializedTwoFactorProviders =\n            JsonHelpers.LegacySerialize(twoFactorProviders, JsonHelpers.LegacyEnumKeyResolver);\n\n        Assert.Equal(user.TwoFactorProviders, serializedTwoFactorProviders);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(Arg.Is<User>(u => u.EmailVerified == true && u.ApiKey != null), masterPasswordHash);\n\n        if (addUserReferenceData)\n        {\n            if (initiationPath.Contains(\"Secrets Manager trial\"))\n            {\n                await sutProvider.GetDependency<IMailService>()\n                    .Received(1)\n                    .SendTrialInitiationEmailAsync(user.Email);\n            }\n            else\n            {\n                await sutProvider.GetDependency<IMailService>()\n                    .Received(1)\n                    .SendWelcomeEmailAsync(user);\n            }\n        }\n        else\n        {\n            // Even if user doesn't have reference data, we should send them welcome email\n            await sutProvider.GetDependency<IMailService>()\n                .Received(1)\n                .SendWelcomeEmailAsync(user);\n        }\n\n        Assert.True(result.Succeeded);\n\n    }\n\n    [Theory]\n    [BitAutoData(\"invalidOrgInviteToken\")]\n    [BitAutoData(\"nullOrgInviteToken\")]\n    [BitAutoData(\"nullOrgUserId\")]\n    public async Task RegisterUserViaOrganizationInviteToken_MissingOrInvalidOrgInviteDataWithDisabledOpenRegistration_ThrowsBadRequestException(string scenario,\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .DisableUserRegistration.Returns(true);\n\n        switch (scenario)\n        {\n            case \"invalidOrgInviteToken\":\n                orgUser.Email = null; // make org user not match user and thus make tokenable invalid\n                var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);\n\n                sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()\n                    .TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())\n                    .Returns(callInfo =>\n                    {\n                        callInfo[1] = orgInviteTokenable;\n                        return true;\n                    });\n                break;\n            case \"nullOrgInviteToken\":\n                orgInviteToken = null;\n                break;\n            case \"nullOrgUserId\":\n                orgUserId = default;\n                break;\n        }\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));\n        Assert.Equal(\"Open registration has been disabled by the system administrator.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(\"invalidOrgInviteToken\")]\n    [BitAutoData(\"nullOrgInviteToken\")]\n    [BitAutoData(\"nullOrgUserId\")]\n    public async Task RegisterUserViaOrganizationInviteToken_MissingOrInvalidOrgInviteDataWithEnabledOpenRegistration_ThrowsBadRequestException(string scenario,\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .DisableUserRegistration.Returns(false);\n\n        string expectedErrorMessage = null;\n        switch (scenario)\n        {\n            case \"invalidOrgInviteToken\":\n                orgUser.Email = null; // make org user not match user and thus make tokenable invalid\n                var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);\n\n                sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()\n                    .TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())\n                    .Returns(callInfo =>\n                    {\n                        callInfo[1] = orgInviteTokenable;\n                        return true;\n                    });\n\n                expectedErrorMessage = \"Organization invite token is invalid.\";\n                break;\n            case \"nullOrgInviteToken\":\n                orgInviteToken = null;\n                expectedErrorMessage = \"Organization user id cannot be provided without an organization invite token.\";\n                break;\n            case \"nullOrgUserId\":\n                orgUserId = default;\n                expectedErrorMessage = \"Organization invite token cannot be validated without an organization user id.\";\n                break;\n        }\n\n        user.ReferenceData = null;\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        // Act\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));\n        Assert.Equal(expectedErrorMessage, exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromDifferentOrg_ThrowsBadRequestException(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId,\n        [Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy)\n    {\n        // Arrange\n        user.Email = \"user@blocked-domain.com\";\n        orgUser.Email = user.Email;\n        orgUser.Id = orgUserId;\n        var blockingOrganizationId = Guid.NewGuid(); // Different org that has the domain blocked\n        orgUser.OrganizationId = Guid.NewGuid(); // The org they're trying to join\n\n        var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()\n            .TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = orgInviteTokenable;\n                return true;\n            });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUserId)\n            .Returns(orgUser);\n\n        // Mock the new overload that excludes the organization - it should return true (domain IS blocked by another org)\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"blocked-domain.com\", orgUser.OrganizationId)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)\n            .Returns(policy);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));\n        Assert.Equal(\"This email address is claimed by an organization using Bitwarden.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromSameOrg_Succeeds(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId,\n        [Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy)\n    {\n        // Arrange\n        user.Email = \"user@company-domain.com\";\n        user.ReferenceData = null;\n        orgUser.Email = user.Email;\n        orgUser.Id = orgUserId;\n        // The organization owns the domain and is trying to invite the user\n        orgUser.OrganizationId = Guid.NewGuid();\n\n        var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()\n            .TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = orgInviteTokenable;\n                return true;\n            });\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUserId)\n            .Returns(orgUser);\n\n        // Mock the new overload - it should return false (domain is NOT blocked by OTHER orgs)\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"company-domain.com\", orgUser.OrganizationId)\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)\n            .Returns(policy);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId);\n\n        // Assert\n        Assert.True(result.Succeeded);\n        await sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .Received(1)\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"company-domain.com\", orgUser.OrganizationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaOrganizationInviteToken_WithValidTokenButNullOrgUser_ThrowsBadRequestException(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)\n    {\n        // Arrange\n        user.Email = \"user@example.com\";\n        orgUser.Email = user.Email;\n        orgUser.Id = orgUserId;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())\n            .Returns(false);\n\n        var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()\n            .TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = orgInviteTokenable;\n                return true;\n            });\n\n        // Mock GetByIdAsync to return null - simulating a deleted or non-existent organization user\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUserId)\n            .Returns((OrganizationUser)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));\n        Assert.Equal(\"Invalid organization user invitation.\", exception.Message);\n\n        // Verify that GetByIdAsync was called\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetByIdAsync(orgUserId);\n\n        // Verify that user creation was never attempted\n        await sutProvider.GetDependency<IUserService>()\n            .DidNotReceive()\n            .CreateUserAsync(Arg.Any<User>(), Arg.Any<string>());\n    }\n\n    // -----------------------------------------------------------------------------------------------\n    // RegisterUserViaEmailVerificationToken tests\n    // -----------------------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaEmailVerificationToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()\n            .TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials);\n                return true;\n            });\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken);\n\n        // Assert\n        Assert.True(result.Succeeded);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(Arg.Is<User>(u => u.Name == user.Name && u.EmailVerified == true && u.ApiKey != null), masterPasswordHash);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendWelcomeEmailAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaEmailVerificationToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()\n            .TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new RegistrationEmailVerificationTokenable(\"wrongEmail@test.com\", user.Name, receiveMarketingMaterials);\n                return true;\n            });\n\n        // Act & Assert\n        var result = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken));\n        Assert.Equal(\"Invalid email verification token.\", result.Message);\n\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaEmailVerificationToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .DisableUserRegistration = true;\n\n        // Act & Assert\n        var result = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken));\n        Assert.Equal(\"Open registration has been disabled by the system administrator.\", result.Message);\n\n    }\n\n    // -----------------------------------------------------------------------------------------------\n    // RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken tests\n    // -----------------------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,\n        string orgSponsoredFreeFamilyPlanInviteToken)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IValidateRedemptionTokenCommand>()\n            .ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)\n            .Returns((true, new OrganizationSponsorship()));\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken);\n\n        // Assert\n        Assert.True(result.Succeeded);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(Arg.Is<User>(u => u.Name == user.Name && u.EmailVerified == true && u.ApiKey != null), masterPasswordHash);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendWelcomeEmailAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,\n        string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IValidateRedemptionTokenCommand>()\n            .ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)\n            .Returns((false, new OrganizationSponsorship()));\n\n        // Act & Assert\n        var result = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken));\n        Assert.Equal(\"Invalid org sponsored free family plan token.\", result.Message);\n\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,\n        string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .DisableUserRegistration = true;\n\n        // Act & Assert\n        var result = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken));\n        Assert.Equal(\"Open registration has been disabled by the system administrator.\", result.Message);\n    }\n\n    // -----------------------------------------------------------------------------------------------\n    // RegisterUserViaAcceptEmergencyAccessInviteToken tests\n    // -----------------------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_Succeeds(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,\n        EmergencyAccessEntity emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n        emergencyAccess.Email = user.Email;\n        emergencyAccess.Id = acceptEmergencyAccessId;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 10);\n                return true;\n            });\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId);\n\n        // Assert\n        Assert.True(result.Succeeded);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(Arg.Is<User>(u => u.Name == user.Name && u.EmailVerified == true && u.ApiKey != null), masterPasswordHash);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendWelcomeEmailAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,\n        string masterPasswordHash, EmergencyAccessEntity emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n        emergencyAccess.Email = \"wrong@email.com\";\n        emergencyAccess.Id = acceptEmergencyAccessId;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 10);\n                return true;\n            });\n\n        // Act & Assert\n        var result = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId));\n        Assert.Equal(\"Invalid accept emergency access invite token.\", result.Message);\n\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,\n        string masterPasswordHash, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .DisableUserRegistration = true;\n\n        // Act & Assert\n        var result = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId));\n        Assert.Equal(\"Open registration has been disabled by the system administrator.\", result.Message);\n    }\n\n    // -----------------------------------------------------------------------------------------------\n    // RegisterUserViaProviderInviteToken tests\n    // -----------------------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaProviderInviteToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider,\n        User user, string masterPasswordHash, Guid providerUserId)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        // Start with plaintext\n        var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);\n        var decryptedProviderInviteToken = $\"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}\";\n\n        // Get the byte array of the plaintext\n        var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);\n\n        // Base64 encode the byte array (this is passed to protector.protect(bytes))\n        var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);\n\n        var mockDataProtector = Substitute.For<IDataProtector>();\n\n        // Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)\n        mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);\n\n        sutProvider.GetDependency<IDataProtectionProvider>()\n            .CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(mockDataProtector);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .OrganizationInviteExpirationHours.Returns(120); // 5 days\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        // Using sutProvider in the parameters of the function means that the constructor has already run for the\n        // command so we have to recreate it in order for our mock overrides to be used.\n        sutProvider.Create();\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId);\n\n        // Assert\n        Assert.True(result.Succeeded);\n\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(Arg.Is<User>(u => u.Name == user.Name && u.EmailVerified == true && u.ApiKey != null), masterPasswordHash);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendWelcomeEmailAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaProviderInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider,\n        User user, string masterPasswordHash, Guid providerUserId)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        // Start with plaintext\n        var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);\n        var decryptedProviderInviteToken = $\"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}\";\n\n        // Get the byte array of the plaintext\n        var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);\n\n        // Base64 encode the byte array (this is passed to protector.protect(bytes))\n        var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);\n\n        var mockDataProtector = Substitute.For<IDataProtector>();\n\n        // Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)\n        mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);\n\n        sutProvider.GetDependency<IDataProtectionProvider>()\n            .CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(mockDataProtector);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .OrganizationInviteExpirationHours.Returns(120); // 5 days\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        // Using sutProvider in the parameters of the function means that the constructor has already run for the\n        // command so we have to recreate it in order for our mock overrides to be used.\n        sutProvider.Create();\n\n        // Act & Assert\n        var result = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, Guid.NewGuid()));\n        Assert.Equal(\"Invalid provider invite token.\", result.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaProviderInviteToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider,\n        User user, string masterPasswordHash, Guid providerUserId)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        // Start with plaintext\n        var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);\n        var decryptedProviderInviteToken = $\"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}\";\n\n        // Get the byte array of the plaintext\n        var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);\n\n        // Base64 encode the byte array (this is passed to protector.protect(bytes))\n        var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);\n\n        var mockDataProtector = Substitute.For<IDataProtector>();\n\n        // Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)\n        mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);\n\n        sutProvider.GetDependency<IDataProtectionProvider>()\n            .CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(mockDataProtector);\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .DisableUserRegistration = true;\n\n        // Using sutProvider in the parameters of the function means that the constructor has already run for the\n        // command so we have to recreate it in order for our mock overrides to be used.\n        sutProvider.Create();\n\n        // Act & Assert\n        var result = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId));\n        Assert.Equal(\"Open registration has been disabled by the system administrator.\", result.Message);\n    }\n\n    // -----------------------------------------------------------------------------------------------\n    // Domain blocking tests (BlockClaimedDomainAccountCreation policy)\n    // -----------------------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUser_BlockedDomain_ThrowsBadRequestException(\n        SutProvider<RegisterUserCommand> sutProvider, User user)\n    {\n        // Arrange\n        user.Email = \"user@blocked-domain.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"blocked-domain.com\")\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUser(user));\n        Assert.Equal(\"This email address is claimed by an organization using Bitwarden.\", exception.Message);\n\n        // Verify user creation was never attempted\n        await sutProvider.GetDependency<IUserService>()\n            .DidNotReceive()\n            .CreateUserAsync(Arg.Any<User>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUser_AllowedDomain_Succeeds(\n        SutProvider<RegisterUserCommand> sutProvider, User user)\n    {\n        // Arrange\n        user.Email = \"user@allowed-domain.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"allowed-domain.com\")\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Success);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUser(user);\n\n        // Assert\n        Assert.True(result.Succeeded);\n        await sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .Received(1)\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"allowed-domain.com\");\n    }\n\n    // SendWelcomeEmail tests\n    // -----------------------------------------------------------------------------------------------\n    [Theory]\n    [BitAutoData(PlanType.FamiliesAnnually)]\n    [BitAutoData(PlanType.FamiliesAnnually2019)]\n    [BitAutoData(PlanType.FamiliesAnnually2025)]\n    [BitAutoData(PlanType.Free)]\n    public async Task SendWelcomeEmail_FamilyOrg_SendsFamilyWelcomeEmail(\n        PlanType planType,\n        User user,\n        Organization organization,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n        organization.PlanType = planType;\n        organization.Name = \"Family Org\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), organization.Id)\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns((OrganizationUser)null);\n\n        // Act\n        await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, organization.Name);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaEmailVerificationToken_BlockedDomain_ThrowsBadRequestException(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,\n        string emailVerificationToken, bool receiveMarketingMaterials)\n    {\n        // Arrange\n        user.Email = \"user@blocked-domain.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"blocked-domain.com\")\n            .Returns(true);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()\n            .TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials);\n                return true;\n            });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken));\n        Assert.Equal(\"This email address is claimed by an organization using Bitwarden.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_BlockedDomain_ThrowsBadRequestException(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,\n        string orgSponsoredFreeFamilyPlanInviteToken)\n    {\n        // Arrange\n        user.Email = \"user@blocked-domain.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"blocked-domain.com\")\n            .Returns(true);\n\n        sutProvider.GetDependency<IValidateRedemptionTokenCommand>()\n            .ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)\n            .Returns((true, new OrganizationSponsorship()));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken));\n        Assert.Equal(\"This email address is claimed by an organization using Bitwarden.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_BlockedDomain_ThrowsBadRequestException(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,\n        EmergencyAccessEntity emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)\n    {\n        // Arrange\n        user.Email = \"user@blocked-domain.com\";\n        emergencyAccess.Email = user.Email;\n        emergencyAccess.Id = acceptEmergencyAccessId;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"blocked-domain.com\")\n            .Returns(true);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()\n            .TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 10);\n                return true;\n            });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId));\n        Assert.Equal(\"This email address is claimed by an organization using Bitwarden.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaProviderInviteToken_BlockedDomain_ThrowsBadRequestException(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, Guid providerUserId)\n    {\n        // Arrange\n        user.Email = \"user@blocked-domain.com\";\n\n        // Start with plaintext\n        var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);\n        var decryptedProviderInviteToken = $\"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}\";\n\n        // Get the byte array of the plaintext\n        var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);\n\n        // Base64 encode the byte array (this is passed to protector.protect(bytes))\n        var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);\n\n        var mockDataProtector = Substitute.For<IDataProtector>();\n\n        // Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)\n        mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);\n\n        sutProvider.GetDependency<IDataProtectionProvider>()\n            .CreateProtector(\"ProviderServiceDataProtector\")\n            .Returns(mockDataProtector);\n\n        sutProvider.GetDependency<IGlobalSettings>()\n            .OrganizationInviteExpirationHours.Returns(120); // 5 days\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"blocked-domain.com\")\n            .Returns(true);\n\n        // Using sutProvider in the parameters of the function means that the constructor has already run for the\n        // command so we have to recreate it in order for our mock overrides to be used.\n        sutProvider.Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId));\n        Assert.Equal(\"This email address is claimed by an organization using Bitwarden.\", exception.Message);\n    }\n\n    // -----------------------------------------------------------------------------------------------\n    // Invalid email format tests\n    // -----------------------------------------------------------------------------------------------\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUser_InvalidEmailFormat_ThrowsBadRequestException(\n        SutProvider<RegisterUserCommand> sutProvider, User user)\n    {\n        // Arrange\n        user.Email = \"invalid-email-format\";\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUser(user));\n        Assert.Equal(\"Invalid email address format.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegisterUserViaEmailVerificationToken_InvalidEmailFormat_ThrowsBadRequestException(\n        SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,\n        string emailVerificationToken, bool receiveMarketingMaterials)\n    {\n        // Arrange\n        user.Email = \"invalid-email-format\";\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()\n            .TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials);\n                return true;\n            });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken));\n        Assert.Equal(\"Invalid email address format.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendWelcomeEmail_OrganizationNull_SendsIndividualWelcomeEmail(\n        User user,\n        OrganizationUser orgUser,\n        string orgInviteToken,\n        string masterPasswordHash,\n        [Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n        user.ReferenceData = null;\n        orgUser.Email = user.Email;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUser.Id)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)\n            .Returns(policy);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(orgUser.OrganizationId)\n            .Returns((Organization)null);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)\n            .Returns(true);\n\n        var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()\n            .TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = orgInviteTokenable;\n                return true;\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUser.Id);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendIndividualUserWelcomeEmailAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendWelcomeEmail_OrganizationDisplayNameNull_SendsIndividualWelcomeEmail(\n        User user,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n        Organization organization = new Organization\n        {\n            Name = null\n        };\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)\n            .Returns(true);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns((OrganizationUser)null);\n\n        // Act\n        await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendIndividualUserWelcomeEmailAsync(user);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationWelcomeEmailDetailsAsync_HappyPath_ReturnsOrganizationWelcomeEmailDetails(\n        Organization organization,\n        User user,\n        OrganizationUser orgUser,\n        string masterPasswordHash,\n        string orgInviteToken,\n        [Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = $\"test+{Guid.NewGuid()}@example.com\";\n        user.ReferenceData = null;\n        orgUser.Email = user.Email;\n        organization.PlanType = PlanType.EnterpriseAnnually;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>(), Arg.Any<Guid?>())\n            .Returns(false);\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user, masterPasswordHash)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUser.Id)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)\n            .Returns(policy);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(orgUser.OrganizationId)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.MjmlWelcomeEmailTemplates)\n            .Returns(true);\n\n        var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);\n\n        sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()\n            .TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())\n            .Returns(callInfo =>\n            {\n                callInfo[1] = orgInviteTokenable;\n                return true;\n            });\n\n        // Act\n        var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUser.Id);\n\n        // Assert\n        Assert.True(result.Succeeded);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .GetByIdAsync(orgUser.OrganizationId);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserWelcomeEmailAsync(user, organization.DisplayName());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RegisterSSOAutoProvisionedUserAsync_WithBlockedDomain_ThrowsException(\n        User user,\n        Organization organization,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = \"user@blocked-domain.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"blocked-domain.com\", organization.Id)\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization));\n        Assert.Equal(\"This email address is claimed by an organization using Bitwarden.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RegisterSSOAutoProvisionedUserAsync_WithOwnClaimedDomain_Succeeds(\n        User user,\n        Organization organization,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = \"user@company-domain.com\";\n\n        // Domain is claimed by THIS organization, so it should be allowed\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"company-domain.com\", organization.Id)\n            .Returns(false); // Not blocked because organization.Id is excluded\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Success);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);\n\n        // Assert\n        Assert.True(result.Succeeded);\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RegisterSSOAutoProvisionedUserAsync_WithNonClaimedDomain_Succeeds(\n        User user,\n        Organization organization,\n        SutProvider<RegisterUserCommand> sutProvider)\n    {\n        // Arrange\n        user.Email = \"user@unclaimed-domain.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"unclaimed-domain.com\", organization.Id)\n            .Returns(false); // Domain is not claimed by any org\n\n        sutProvider.GetDependency<IUserService>()\n            .CreateUserAsync(user)\n            .Returns(IdentityResult.Success);\n\n        // Act\n        var result = await sutProvider.Sut.RegisterSSOAutoProvisionedUserAsync(user, organization);\n\n        // Assert\n        Assert.True(result.Succeeded);\n        await sutProvider.GetDependency<IUserService>()\n            .Received(1)\n            .CreateUserAsync(user);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/Registration/SendVerificationEmailForRegistrationCommandTests.cs",
    "content": "﻿using Bit.Core.Auth.Models.Api.Request.Accounts;\nusing Bit.Core.Auth.Models.Business.Tokenables;\nusing Bit.Core.Auth.UserFeatures.Registration.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.Registration;\n\n[SutProviderCustomize]\npublic class SendVerificationEmailForRegistrationCommandTests\n{\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenIsNewUserAndEnableEmailVerificationTrue_SendsEmailAndReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n        string name, bool receiveMarketingEmails)\n    {\n        // Arrange\n        var email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(email)\n            .ReturnsNull();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .EnableEmailVerification = true;\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        var mockedToken = \"token\";\n        sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()\n            .Protect(Arg.Any<RegistrationEmailVerificationTokenable>())\n            .Returns(mockedToken);\n\n        // Act\n        var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendRegistrationVerificationEmailAsync(email, mockedToken, null);\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenFromMarketingIsPremium_SendsEmailWithMarketingParameterAndReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n        string name, bool receiveMarketingEmails)\n    {\n        // Arrange\n        var email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(email)\n            .ReturnsNull();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .EnableEmailVerification = true;\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        var mockedToken = \"token\";\n        sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()\n            .Protect(Arg.Any<RegistrationEmailVerificationTokenable>())\n            .Returns(mockedToken);\n\n        var fromMarketing = MarketingInitiativeConstants.Premium;\n\n        // Act\n        var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, fromMarketing);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendRegistrationVerificationEmailAsync(email, mockedToken, fromMarketing);\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenIsExistingUserAndEnableEmailVerificationTrue_ReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n        string name, bool receiveMarketingEmails)\n    {\n        // Arrange\n        var email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(email)\n            .Returns(new User());\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .EnableEmailVerification = true;\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        var mockedToken = \"token\";\n        sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()\n            .Protect(Arg.Any<RegistrationEmailVerificationTokenable>())\n            .Returns(mockedToken);\n\n        // Act\n        var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);\n\n        // Assert\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceive()\n            .SendRegistrationVerificationEmailAsync(email, mockedToken, null);\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenIsNewUserAndEnableEmailVerificationFalse_ReturnsToken(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n        string name, bool receiveMarketingEmails)\n    {\n        // Arrange\n        var email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(email)\n            .ReturnsNull();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .EnableEmailVerification = false;\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        var mockedToken = \"token\";\n        sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()\n            .Protect(Arg.Any<RegistrationEmailVerificationTokenable>())\n            .Returns(mockedToken);\n\n        // Act\n        var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);\n\n        // Assert\n        Assert.Equal(mockedToken, result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenOpenRegistrationDisabled_ThrowsBadRequestException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n        string name, bool receiveMarketingEmails)\n    {\n        // Arrange\n        var email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = true;\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenIsExistingUserAndEnableEmailVerificationFalse_ThrowsBadRequestException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n        string name, bool receiveMarketingEmails)\n    {\n        // Arrange\n        var email = $\"test+{Guid.NewGuid()}@example.com\";\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(email)\n            .Returns(new User());\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .EnableEmailVerification = false;\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())\n            .Returns(false);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenNullEmail_ThrowsArgumentNullException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n   string name, bool receiveMarketingEmails)\n    {\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n\n        await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run(null, name, receiveMarketingEmails, null));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenEmptyEmail_ThrowsArgumentNullException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n   string name, bool receiveMarketingEmails)\n    {\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n        await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run(\"\", name, receiveMarketingEmails, null));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenBlockedDomain_ThrowsBadRequestException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n        string name, bool receiveMarketingEmails)\n    {\n        // Arrange\n        var email = $\"test+{Guid.NewGuid()}@blockedcompany.com\";\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"blockedcompany.com\")\n            .Returns(true);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));\n        Assert.Equal(\"This email address is claimed by an organization using Bitwarden.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_WhenAllowedDomain_Succeeds(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n        string name, bool receiveMarketingEmails)\n    {\n        // Arrange\n        var email = $\"test+{Guid.NewGuid()}@allowedcompany.com\";\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByEmailAsync(email)\n            .ReturnsNull();\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .EnableEmailVerification = false;\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n\n        sutProvider.GetDependency<IOrganizationDomainRepository>()\n            .HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(\"allowedcompany.com\")\n            .Returns(false);\n\n        var mockedToken = \"token\";\n        sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()\n            .Protect(Arg.Any<RegistrationEmailVerificationTokenable>())\n            .Returns(mockedToken);\n\n        // Act\n        var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails, null);\n\n        // Assert\n        Assert.Equal(mockedToken, result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendVerificationEmailForRegistrationCommand_InvalidEmailFormat_ThrowsBadRequestException(\n        SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,\n        string name, bool receiveMarketingEmails)\n    {\n        // Arrange\n        var email = \"invalid-email-format\";\n\n        sutProvider.GetDependency<GlobalSettings>()\n            .DisableUserRegistration = false;\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.Run(email, name, receiveMarketingEmails, null));\n        Assert.Equal(\"Invalid email address format.\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Auth.UserFeatures.SendAccess;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.SendAccess;\n\npublic class SendAccessClaimsPrincipalExtensionsTests\n{\n    [Fact]\n    public void GetSendId_ReturnsGuid_WhenClaimIsPresentAndValid()\n    {\n        // Arrange\n        var guid = Guid.NewGuid();\n        var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, guid.ToString()) };\n        var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        var result = principal.GetSendId();\n\n        // Assert\n        Assert.Equal(guid, result);\n    }\n\n    [Fact]\n    public void GetSendId_ThrowsInvalidOperationException_WhenClaimIsMissing()\n    {\n        // Arrange\n        var principal = new ClaimsPrincipal(new ClaimsIdentity());\n\n        // Act & Assert\n        var ex = Assert.Throws<InvalidOperationException>(() => principal.GetSendId());\n        Assert.Equal(\"send_id claim not found.\", ex.Message);\n    }\n\n    [Fact]\n    public void GetSendId_ThrowsInvalidOperationException_WhenClaimValueIsInvalid()\n    {\n        // Arrange\n        var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, \"not-a-guid\") };\n        var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act & Assert\n        var ex = Assert.Throws<InvalidOperationException>(() => principal.GetSendId());\n        Assert.Equal(\"Invalid send_id claim value.\", ex.Message);\n    }\n\n    [Fact]\n    public void GetSendId_ThrowsArgumentNullException_WhenPrincipalIsNull()\n    {\n        // Act & Assert\n        Assert.Throws<ArgumentNullException>(() => SendAccessClaimsPrincipalExtensions.GetSendId(null));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/Sso/UserSsoOrganizationIdentifierQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Sso;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.Sso;\n\n[SutProviderCustomize]\npublic class UserSsoOrganizationIdentifierQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task GetSsoOrganizationIdentifierAsync_UserHasSingleConfirmedOrganization_ReturnsIdentifier(\n        SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,\n        Guid userId,\n        Organization organization,\n        OrganizationUser organizationUser)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.OrganizationId = organization.Id;\n        organizationUser.Status = OrganizationUserStatusType.Confirmed;\n        organization.Identifier = \"test-org-identifier\";\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns([organizationUser]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        // Act\n        var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);\n\n        // Assert\n        Assert.Equal(\"test-org-identifier\", result);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByUserAsync(userId);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .GetByIdAsync(organization.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSsoOrganizationIdentifierAsync_UserHasNoOrganizations_ReturnsNull(\n        SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,\n        Guid userId)\n    {\n        // Arrange\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns(Array.Empty<OrganizationUser>());\n\n        // Act\n        var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);\n\n        // Assert\n        Assert.Null(result);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByUserAsync(userId);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .GetByIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSsoOrganizationIdentifierAsync_UserHasMultipleConfirmedOrganizations_ReturnsNull(\n        SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,\n        Guid userId,\n        OrganizationUser organizationUser1,\n        OrganizationUser organizationUser2)\n    {\n        // Arrange\n        organizationUser1.UserId = userId;\n        organizationUser1.Status = OrganizationUserStatusType.Confirmed;\n        organizationUser2.UserId = userId;\n        organizationUser2.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns([organizationUser1, organizationUser2]);\n\n        // Act\n        var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);\n\n        // Assert\n        Assert.Null(result);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByUserAsync(userId);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .GetByIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Invited)]\n    [BitAutoData(OrganizationUserStatusType.Accepted)]\n    [BitAutoData(OrganizationUserStatusType.Revoked)]\n    public async Task GetSsoOrganizationIdentifierAsync_UserHasOnlyInvitedOrganization_ReturnsNull(\n        OrganizationUserStatusType status,\n        SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,\n        Guid userId,\n        OrganizationUser organizationUser)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.Status = status;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns([organizationUser]);\n\n        // Act\n        var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);\n\n        // Assert\n        Assert.Null(result);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByUserAsync(userId);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceive()\n            .GetByIdAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSsoOrganizationIdentifierAsync_UserHasMixedStatusOrganizations_OnlyOneConfirmed_ReturnsIdentifier(\n        SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,\n        Guid userId,\n        Organization organization,\n        OrganizationUser confirmedOrgUser,\n        OrganizationUser invitedOrgUser,\n        OrganizationUser revokedOrgUser)\n    {\n        // Arrange\n        confirmedOrgUser.UserId = userId;\n        confirmedOrgUser.OrganizationId = organization.Id;\n        confirmedOrgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        invitedOrgUser.UserId = userId;\n        invitedOrgUser.Status = OrganizationUserStatusType.Invited;\n\n        revokedOrgUser.UserId = userId;\n        revokedOrgUser.Status = OrganizationUserStatusType.Revoked;\n\n        organization.Identifier = \"mixed-status-org\";\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns(new[] { confirmedOrgUser, invitedOrgUser, revokedOrgUser });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        // Act\n        var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);\n\n        // Assert\n        Assert.Equal(\"mixed-status-org\", result);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByUserAsync(userId);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .GetByIdAsync(organization.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSsoOrganizationIdentifierAsync_OrganizationNotFound_ReturnsNull(\n        SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,\n        Guid userId,\n        OrganizationUser organizationUser)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns([organizationUser]);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organizationUser.OrganizationId)\n            .Returns((Organization)null);\n\n        // Act\n        var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);\n\n        // Assert\n        Assert.Null(result);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByUserAsync(userId);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .GetByIdAsync(organizationUser.OrganizationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSsoOrganizationIdentifierAsync_OrganizationIdentifierIsNull_ReturnsNull(\n        SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,\n        Guid userId,\n        Organization organization,\n        OrganizationUser organizationUser)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.OrganizationId = organization.Id;\n        organizationUser.Status = OrganizationUserStatusType.Confirmed;\n        organization.Identifier = null;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns(new[] { organizationUser });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        // Act\n        var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);\n\n        // Assert\n        Assert.Null(result);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByUserAsync(userId);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .GetByIdAsync(organization.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSsoOrganizationIdentifierAsync_OrganizationIdentifierIsEmpty_ReturnsEmpty(\n        SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,\n        Guid userId,\n        Organization organization,\n        OrganizationUser organizationUser)\n    {\n        // Arrange\n        organizationUser.UserId = userId;\n        organizationUser.OrganizationId = organization.Id;\n        organizationUser.Status = OrganizationUserStatusType.Confirmed;\n        organization.Identifier = string.Empty;\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByUserAsync(userId)\n            .Returns(new[] { organizationUser });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        // Act\n        var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);\n\n        // Assert\n        Assert.Equal(string.Empty, result);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetManyByUserAsync(userId);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .GetByIdAsync(organization.Id);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/TdeOffboarding/TdeOffboardingPasswordCommandTests.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.UserMasterPassword;\n\n[SutProviderCustomize]\npublic class TdeOffboardingPasswordTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task TdeOffboardingPasswordCommand_Success(SutProvider<TdeOffboardingPasswordCommand> sutProvider,\n        User user, string masterPassword, string key, string hint, OrganizationUserOrganizationDetails orgUserDetails, SsoUser ssoUser)\n    {\n        // Arrange\n        user.MasterPassword = null;\n\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(IdentityResult.Success);\n\n        orgUserDetails.UseSso = true;\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByUserAsync(user.Id)\n            .Returns(new List<OrganizationUserOrganizationDetails> { orgUserDetails });\n\n        sutProvider.GetDependency<ISsoUserRepository>()\n            .GetByUserIdOrganizationIdAsync(orgUserDetails.OrganizationId, user.Id)\n            .Returns(ssoUser);\n\n        var ssoConfig = new SsoConfig();\n        var ssoConfigData = ssoConfig.GetData();\n        ssoConfigData.MemberDecryptionType = MemberDecryptionType.MasterPassword;\n        ssoConfig.SetData(ssoConfigData);\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(orgUserDetails.OrganizationId)\n            .Returns(ssoConfig);\n\n        // Act\n        var result = await sutProvider.Sut.UpdateTdeOffboardingPasswordAsync(user, masterPassword, key, hint);\n\n        // Assert\n        Assert.Equal(IdentityResult.Success, result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task TdeOffboardingPasswordCommand_RejectWithTdeEnabled(SutProvider<TdeOffboardingPasswordCommand> sutProvider,\n        User user, string masterPassword, string key, string hint, OrganizationUserOrganizationDetails orgUserDetails, SsoUser ssoUser)\n    {\n        // Arrange\n        user.MasterPassword = null;\n\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)\n            .Returns(IdentityResult.Success);\n\n        orgUserDetails.UseSso = true;\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyDetailsByUserAsync(user.Id)\n            .Returns(new List<OrganizationUserOrganizationDetails> { orgUserDetails });\n\n        sutProvider.GetDependency<ISsoUserRepository>()\n            .GetByUserIdOrganizationIdAsync(orgUserDetails.OrganizationId, user.Id)\n            .Returns(ssoUser);\n\n        var ssoConfig = new SsoConfig();\n        var ssoConfigData = ssoConfig.GetData();\n        ssoConfigData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;\n        ssoConfig.SetData(ssoConfigData);\n        sutProvider.GetDependency<ISsoConfigRepository>()\n            .GetByOrganizationIdAsync(orgUserDetails.OrganizationId)\n            .Returns(ssoConfig);\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateTdeOffboardingPasswordAsync(user, masterPassword, key, hint));\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task TdeOffboardingPasswordCommand_RejectWithMasterPassword(SutProvider<TdeOffboardingPasswordCommand> sutProvider,\n        User user, string masterPassword, string key, string hint)\n    {\n        // the user already has a master password, so the off-boarding request should fail, since off-boarding only applies to passwordless TDE users\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateTdeOffboardingPasswordAsync(user, masterPassword, key, hint));\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/TwoFactorAuth/CompleteTwoFactorWebAuthnRegistrationCommandTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Fido2NetLib;\nusing Fido2NetLib.Objects;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;\n\n[SutProviderCustomize]\npublic class CompleteTwoFactorWebAuthnRegistrationCommandTests\n{\n    /// <summary>\n    /// The \"Start\" command will have set the in-process credential registration request to \"pending\" status.\n    /// The purpose of Complete is to consume and enshrine this pending credential.\n    /// </summary>\n    private static void SetupWebAuthnProviderWithPending(User user, int credentialCount)\n    {\n        var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();\n        var metadata = new Dictionary<string, object>();\n\n        // Add existing credentials\n        for (var i = 1; i <= credentialCount; i++)\n        {\n            metadata[$\"Key{i}\"] = new TwoFactorProvider.WebAuthnData\n            {\n                Name = $\"Key {i}\",\n                Descriptor = new PublicKeyCredentialDescriptor([(byte)i]),\n                PublicKey = [(byte)i],\n                UserHandle = [(byte)i],\n                SignatureCounter = 0,\n                CredType = \"public-key\",\n                RegDate = DateTime.UtcNow,\n                AaGuid = Guid.NewGuid()\n            };\n        }\n\n        // Add pending registration\n        var pendingOptions = new CredentialCreateOptions\n        {\n            Challenge = [1, 2, 3],\n            Rp = new PublicKeyCredentialRpEntity(\"example.com\", \"example.com\", \"\"),\n            User = new Fido2User\n            {\n                Id = user.Id.ToByteArray(),\n                Name = user.Email ?? \"test@example.com\",\n                DisplayName = user.Name ?? \"Test User\"\n            },\n            PubKeyCredParams = []\n        };\n        metadata[\"pending\"] = pendingOptions.ToJson();\n\n        providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider { Enabled = true, MetaData = metadata };\n\n        user.SetTwoFactorProviders(providers);\n    }\n\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task CompleteWebAuthRegistrationAsync_BelowLimit_Succeeds(bool hasPremium,\n        SutProvider<CompleteTwoFactorWebAuthnRegistrationCommand> sutProvider, User user,\n        AuthenticatorAttestationRawResponse deviceResponse)\n    {\n        // Arrange - User should have 1 available credential, determined by maximum allowed for tested Premium status.\n        var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings\n        {\n            PremiumMaximumAllowedCredentials = 10,\n            NonPremiumMaximumAllowedCredentials = 5\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;\n\n        user.Premium = hasPremium;\n        user.Id = Guid.NewGuid();\n        user.Email = \"test@example.com\";\n\n        sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);\n\n        SetupWebAuthnProviderWithPending(user,\n            credentialCount: hasPremium\n                ? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials - 1\n                : maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials - 1);\n\n        var mockFido2 = sutProvider.GetDependency<IFido2>();\n        mockFido2.MakeNewCredentialAsync(\n                Arg.Any<AuthenticatorAttestationRawResponse>(),\n                Arg.Any<CredentialCreateOptions>(),\n                Arg.Any<IsCredentialIdUniqueToUserAsyncDelegate>())\n            .Returns(new Fido2.CredentialMakeResult(\"ok\", \"\",\n                new AttestationVerificationSuccess\n                {\n                    Aaguid = Guid.NewGuid(),\n                    Counter = 0,\n                    CredentialId = [1, 2, 3],\n                    CredType = \"public-key\",\n                    PublicKey = [4, 5, 6],\n                    Status = \"ok\",\n                    User = new Fido2User\n                    {\n                        Id = user.Id.ToByteArray(),\n                        Name = user.Email ?? \"test@example.com\",\n                        DisplayName = user.Name ?? \"Test User\"\n                    }\n                }));\n\n        // Act\n        var result =\n            await sutProvider.Sut.CompleteTwoFactorWebAuthnRegistrationAsync(user, 5, \"NewKey\", deviceResponse);\n\n        // Assert\n        // Note that, contrary to the \"Start\" command, \"Complete\" does not suppress logging for the update providers invocation.\n        Assert.True(result);\n        await sutProvider.GetDependency<IUserService>().Received(1)\n            .UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);\n    }\n\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task CompleteWebAuthRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(bool hasPremium,\n        SutProvider<CompleteTwoFactorWebAuthnRegistrationCommand> sutProvider, User user,\n        AuthenticatorAttestationRawResponse deviceResponse)\n    {\n        // Arrange - time-of-check/time-of-use scenario: user now has 10 credentials (at limit)\n        var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings\n        {\n            PremiumMaximumAllowedCredentials = 10,\n            NonPremiumMaximumAllowedCredentials = 5\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;\n\n        user.Premium = hasPremium;\n        sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);\n\n\n        SetupWebAuthnProviderWithPending(user,\n            credentialCount: hasPremium\n                ? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials\n                : maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CompleteTwoFactorWebAuthnRegistrationAsync(user, 11, \"NewKey\", deviceResponse));\n\n        Assert.Equal(\"Maximum allowed WebAuthn credential count exceeded.\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/TwoFactorAuth/DeleteTwoFactorWebAuthnCredentialCommandTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Fido2NetLib.Objects;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;\n\n[SutProviderCustomize]\npublic class DeleteTwoFactorWebAuthnCredentialCommandTests\n{\n    private static void SetupWebAuthnProvider(User user, int credentialCount)\n    {\n        var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();\n        var metadata = new Dictionary<string, object>();\n\n        // Add credentials as Key1, Key2, Key3, etc.\n        for (var i = 1; i <= credentialCount; i++)\n        {\n            metadata[$\"Key{i}\"] = new TwoFactorProvider.WebAuthnData\n            {\n                Name = $\"Key {i}\",\n                Descriptor = new PublicKeyCredentialDescriptor([(byte)i]),\n                PublicKey = [(byte)i],\n                UserHandle = [(byte)i],\n                SignatureCounter = 0,\n                CredType = \"public-key\",\n                RegDate = DateTime.UtcNow,\n                AaGuid = Guid.NewGuid()\n            };\n        }\n\n        providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider { Enabled = true, MetaData = metadata };\n\n        user.SetTwoFactorProviders(providers);\n    }\n\n    /// <summary>\n    /// When the user has multiple WebAuthn credentials and requests deletion of an existing key,\n    /// the command should remove it, persist via UserService, and return true.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_KeyExistsWithMultipleKeys_RemovesKeyAndReturnsTrue(\n        SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)\n    {\n        // Arrange\n        SetupWebAuthnProvider(user, 3);\n        var keyIdToDelete = 2;\n\n        // Act\n        var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, keyIdToDelete);\n\n        // Assert\n        Assert.True(result);\n\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);\n        Assert.NotNull(provider?.MetaData);\n        Assert.False(provider.MetaData.ContainsKey($\"Key{keyIdToDelete}\"));\n        Assert.Equal(2, provider.MetaData.Count);\n\n        await sutProvider.GetDependency<IUserService>().Received(1)\n            .UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn);\n    }\n\n    /// <summary>\n    /// When the requested key does not exist, the command should return false\n    /// and not call UserService.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_KeyDoesNotExist_ReturnsFalse(\n        SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)\n    {\n        // Arrange\n        SetupWebAuthnProvider(user, 2);\n        var nonExistentKeyId = 99;\n\n        // Act\n        var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, nonExistentKeyId);\n\n        // Assert\n        Assert.False(result);\n\n        await sutProvider.GetDependency<IUserService>().DidNotReceive()\n            .UpdateTwoFactorProviderAsync(Arg.Any<User>(), Arg.Any<TwoFactorProviderType>());\n    }\n\n    /// <summary>\n    /// Users must retain at least one WebAuthn credential. When only one key remains,\n    /// deletion should be rejected to prevent lockout.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_OnlyOneKeyRemaining_ReturnsFalse(\n        SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)\n    {\n        // Arrange\n        SetupWebAuthnProvider(user, 1);\n        var keyIdToDelete = 1;\n\n        // Act\n        var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, keyIdToDelete);\n\n        // Assert\n        Assert.False(result);\n\n        // Key should still exist\n        var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);\n        Assert.NotNull(provider?.MetaData);\n        Assert.True(provider.MetaData.ContainsKey($\"Key{keyIdToDelete}\"));\n\n        await sutProvider.GetDependency<IUserService>().DidNotReceive()\n            .UpdateTwoFactorProviderAsync(Arg.Any<User>(), Arg.Any<TwoFactorProviderType>());\n    }\n\n    /// <summary>\n    /// When the user has no two-factor providers configured, deletion should return false.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_NoProviders_ReturnsFalse(\n        SutProvider<DeleteTwoFactorWebAuthnCredentialCommand> sutProvider, User user)\n    {\n        // Arrange - user with no providers (clear any AutoFixture-generated ones)\n        user.SetTwoFactorProviders(null);\n\n        // Act\n        var result = await sutProvider.Sut.DeleteTwoFactorWebAuthnCredentialAsync(user, 1);\n\n        // Assert\n        Assert.False(result);\n\n        await sutProvider.GetDependency<IUserService>().DidNotReceive()\n            .UpdateTwoFactorProviderAsync(Arg.Any<User>(), Arg.Any<TwoFactorProviderType>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/TwoFactorAuth/StartTwoFactorWebAuthnRegistrationCommandTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Fido2NetLib;\nusing Fido2NetLib.Objects;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;\n\n[SutProviderCustomize]\npublic class StartTwoFactorWebAuthnRegistrationCommandTests\n{\n    private static void SetupWebAuthnProvider(User user, int credentialCount)\n    {\n        var providers = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();\n        var metadata = new Dictionary<string, object>();\n\n        // Add credentials as Key1, Key2, Key3, etc.\n        for (var i = 1; i <= credentialCount; i++)\n        {\n            metadata[$\"Key{i}\"] = new TwoFactorProvider.WebAuthnData\n            {\n                Name = $\"Key {i}\",\n                Descriptor = new PublicKeyCredentialDescriptor([(byte)i]),\n                PublicKey = [(byte)i],\n                UserHandle = [(byte)i],\n                SignatureCounter = 0,\n                CredType = \"public-key\",\n                RegDate = DateTime.UtcNow,\n                AaGuid = Guid.NewGuid()\n            };\n        }\n\n        providers[TwoFactorProviderType.WebAuthn] = new TwoFactorProvider { Enabled = true, MetaData = metadata };\n\n        user.SetTwoFactorProviders(providers);\n    }\n\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task StartWebAuthnRegistrationAsync_BelowLimit_Succeeds(\n        bool hasPremium, SutProvider<StartTwoFactorWebAuthnRegistrationCommand> sutProvider, User user)\n    {\n        // Arrange - User should have 1 available credential, determined by maximum allowed for tested Premium status.\n        var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings\n        {\n            PremiumMaximumAllowedCredentials = 10,\n            NonPremiumMaximumAllowedCredentials = 5\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;\n\n        user.Premium = hasPremium;\n        user.Id = Guid.NewGuid();\n        user.Email = \"test@example.com\";\n\n        sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);\n\n        SetupWebAuthnProvider(user,\n            credentialCount: hasPremium\n                ? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials - 1\n                : maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials - 1);\n\n        var mockFido2 = sutProvider.GetDependency<IFido2>();\n        mockFido2.RequestNewCredential(\n                Arg.Any<Fido2User>(),\n                Arg.Any<List<PublicKeyCredentialDescriptor>>(),\n                Arg.Any<AuthenticatorSelection>(),\n                Arg.Any<AttestationConveyancePreference>())\n            .Returns(new CredentialCreateOptions\n            {\n                Challenge = [1, 2, 3],\n                Rp = new PublicKeyCredentialRpEntity(\"example.com\", \"example.com\", \"\"),\n                User = new Fido2User { Id = user.Id.ToByteArray(), Name = user.Email, DisplayName = user.Name },\n                PubKeyCredParams = []\n            });\n\n        // Act\n        var result = await sutProvider.Sut.StartTwoFactorWebAuthnRegistrationAsync(user);\n\n        // Assert\n        Assert.NotNull(result);\n        await sutProvider.GetDependency<IUserService>().Received(1)\n            .UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, false);\n    }\n\n    /// <summary>\n    /// \"Start\" provides the first half of a two-part process for registering a new WebAuthn 2FA credential.\n    /// To provide the best (most aggressive) UX possible, \"Start\" performs boundary validation of the ability to engage\n    /// in this flow based on current number of configured credentials. If the user is out of available credential slots,\n    /// Start should throw a BadRequestException for the client to handle.\n    /// </summary>\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public async Task StartWebAuthnRegistrationAsync_ExceedsLimit_ThrowsBadRequestException(\n        bool hasPremium, SutProvider<StartTwoFactorWebAuthnRegistrationCommand> sutProvider, User user)\n    {\n        // Arrange - User should have 1 available credential, determined by maximum allowed for tested Premium status.\n        var maximumAllowedCredentialsGlobalSetting = new Core.Settings.GlobalSettings.WebAuthnSettings\n        {\n            PremiumMaximumAllowedCredentials = 10,\n            NonPremiumMaximumAllowedCredentials = 5\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().WebAuthn = maximumAllowedCredentialsGlobalSetting;\n\n        user.Premium = hasPremium;\n        user.Id = Guid.NewGuid();\n        user.Email = \"test@example.com\";\n\n        sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumAccessAsync(user.Id).Returns(hasPremium);\n\n        SetupWebAuthnProvider(user,\n            credentialCount: hasPremium\n                ? maximumAllowedCredentialsGlobalSetting.PremiumMaximumAllowedCredentials\n                : maximumAllowedCredentialsGlobalSetting.NonPremiumMaximumAllowedCredentials);\n\n        var mockFido2 = sutProvider.GetDependency<IFido2>();\n        mockFido2.RequestNewCredential(\n                Arg.Any<Fido2User>(),\n                Arg.Any<List<PublicKeyCredentialDescriptor>>(),\n                Arg.Any<AuthenticatorSelection>(),\n                Arg.Any<AttestationConveyancePreference>())\n            .Returns(new CredentialCreateOptions\n            {\n                Challenge = [1, 2, 3],\n                Rp = new PublicKeyCredentialRpEntity(\"example.com\", \"example.com\", \"\"),\n                User = new Fido2User { Id = user.Id.ToByteArray(), Name = user.Email, DisplayName = user.Name },\n                PubKeyCredParams = []\n            });\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.StartTwoFactorWebAuthnRegistrationAsync(user));\n        Assert.Equal(\"Maximum allowed WebAuthn credential count exceeded.\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs",
    "content": "﻿using Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;\nusing Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth;\n\n[SutProviderCustomize]\npublic class TwoFactorIsEnabledQueryTests\n{\n    [Theory]\n    [BitAutoData(TwoFactorProviderType.Authenticator)]\n    [BitAutoData(TwoFactorProviderType.Email)]\n    [BitAutoData(TwoFactorProviderType.Remember)]\n    [BitAutoData(TwoFactorProviderType.OrganizationDuo)]\n    [BitAutoData(TwoFactorProviderType.WebAuthn)]\n    public async Task TwoFactorIsEnabledQuery_WithProviderTypeNotRequiringPremium_ReturnsAllTwoFactorEnabled(\n        TwoFactorProviderType freeProviderType,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        List<UserWithCalculatedPremium> usersWithCalculatedPremium)\n    {\n        // Arrange\n        var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList();\n        var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { freeProviderType, new TwoFactorProvider { Enabled = true } } // Does not require premium\n        };\n\n        foreach (var user in usersWithCalculatedPremium)\n        {\n            user.HasPremiumAccess = false;\n            user.SetTwoFactorProviders(twoFactorProviders);\n        }\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.All(userIds.Contains)))\n            .Returns(usersWithCalculatedPremium);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);\n\n        // Assert\n        foreach (var userDetail in usersWithCalculatedPremium)\n        {\n            Assert.Contains(result, res => res.userId == userDetail.Id && res.twoFactorIsEnabled == true);\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task TwoFactorIsEnabledQuery_DatabaseReturnsEmpty_ResultEmpty(\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        List<UserWithCalculatedPremium> usersWithCalculatedPremium)\n    {\n        // Arrange\n        var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList();\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [BitAutoData((IEnumerable<Guid>)null)]\n    [BitAutoData([])]\n    public async Task TwoFactorIsEnabledQuery_UserIdsNullorEmpty_ResultEmpty(\n    IEnumerable<Guid> userIds,\n    SutProvider<TwoFactorIsEnabledQuery> sutProvider)\n    {\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task TwoFactorIsEnabledQuery_WithNoTwoFactorEnabled_ReturnsAllTwoFactorDisabled(\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        List<UserWithCalculatedPremium> usersWithCalculatedPremium)\n    {\n        // Arrange\n        var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList();\n        var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { TwoFactorProviderType.Email, new TwoFactorProvider { Enabled = false } }\n        };\n\n        foreach (var user in usersWithCalculatedPremium)\n        {\n            user.SetTwoFactorProviders(twoFactorProviders);\n        }\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.All(userIds.Contains)))\n            .Returns(usersWithCalculatedPremium);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);\n\n        // Assert\n        foreach (var userDetail in usersWithCalculatedPremium)\n        {\n            Assert.Contains(result, res => res.userId == userDetail.Id && res.twoFactorIsEnabled == false);\n        }\n    }\n\n    [Theory]\n    [BitAutoData(TwoFactorProviderType.Duo)]\n    [BitAutoData(TwoFactorProviderType.YubiKey)]\n    public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_ReturnsMixedResults(\n        TwoFactorProviderType premiumProviderType,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        List<UserWithCalculatedPremium> usersWithCalculatedPremium)\n    {\n        // Arrange\n        var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList();\n        var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { TwoFactorProviderType.Email, new TwoFactorProvider { Enabled = false } },\n            { premiumProviderType, new TwoFactorProvider { Enabled = true } }\n        };\n\n        foreach (var user in usersWithCalculatedPremium)\n        {\n            user.HasPremiumAccess = usersWithCalculatedPremium.IndexOf(user) == 0; // Only the first user has premium access\n            user.SetTwoFactorProviders(twoFactorProviders);\n        }\n\n        var hasPremiumAccessQueryResults = usersWithCalculatedPremium.ToDictionary(\n            k => k.Id,\n            v => v.HasPremiumAccess);\n\n        sutProvider.GetDependency<IHasPremiumAccessQuery>()\n            .HasPremiumAccessAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(hasPremiumAccessQueryResults);\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(i => i.All(userIds.Contains)))\n            .Returns(usersWithCalculatedPremium);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);\n\n        // Assert\n        foreach (var userDetail in usersWithCalculatedPremium)\n        {\n            Assert.Contains(result, res => res.userId == userDetail.Id && res.twoFactorIsEnabled == userDetail.HasPremiumAccess);\n        }\n    }\n\n    [Theory]\n    [BitAutoData(\"\")]\n    [BitAutoData(\"{}\")]\n    [BitAutoData((string)null)]\n    public async Task TwoFactorIsEnabledQuery_WithNullOrEmptyTwoFactorProviders_ReturnsAllTwoFactorDisabled(\n        string twoFactorProviders,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        List<UserPremiumAccess> usersWithCalculatedPremium)\n    {\n        // Arrange\n        var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList();\n        var usersWithoutTwoFactorProviders = usersWithCalculatedPremium.Select(u => new User\n        {\n            Id = u.Id,\n            TwoFactorProviders = twoFactorProviders\n        });\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync(Arg.Any<IEnumerable<Guid>>())\n            .Returns(usersWithoutTwoFactorProviders);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);\n\n        // Assert\n        foreach (var userDetail in usersWithCalculatedPremium)\n        {\n            Assert.Contains(result, res => res.userId == userDetail.Id && res.twoFactorIsEnabled == false);\n        }\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task TwoFactorIsEnabledQuery_UserIdNull_ReturnsFalse(\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider)\n    {\n        // Arrange\n        var user = new TestTwoFactorProviderUser\n        {\n            Id = null\n        };\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData(TwoFactorProviderType.Authenticator)]\n    [BitAutoData(TwoFactorProviderType.Email)]\n    [BitAutoData(TwoFactorProviderType.Remember)]\n    [BitAutoData(TwoFactorProviderType.OrganizationDuo)]\n    [BitAutoData(TwoFactorProviderType.WebAuthn)]\n    public async Task TwoFactorIsEnabledQuery_WithProviderTypeNotRequiringPremium_ReturnsTrue(\n        TwoFactorProviderType freeProviderType,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        User user)\n    {\n        // Arrange\n        var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { freeProviderType, new TwoFactorProvider { Enabled = true } }\n        };\n\n        user.SetTwoFactorProviders(twoFactorProviders);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task TwoFactorIsEnabledQuery_WithNoTwoFactorEnabled_ReturnsFalse(\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        User user)\n    {\n        // Arrange\n        var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { TwoFactorProviderType.Email, new TwoFactorProvider { Enabled = false } }\n        };\n\n        user.SetTwoFactorProviders(twoFactorProviders);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData(TwoFactorProviderType.Duo)]\n    [BitAutoData(TwoFactorProviderType.YubiKey)]\n    public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_WithoutPremium_ReturnsFalse(\n        TwoFactorProviderType premiumProviderType,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        UserWithCalculatedPremium user)\n    {\n        // Arrange\n        var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { premiumProviderType, new TwoFactorProvider { Enabled = true } }\n        };\n\n        user.SetTwoFactorProviders(twoFactorProviders);\n\n        sutProvider.GetDependency<IHasPremiumAccessQuery>()\n            .HasPremiumAccessAsync(user.Id)\n            .Returns(false);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);\n\n        // Assert\n        Assert.False(result);\n\n        await sutProvider.GetDependency<IHasPremiumAccessQuery>()\n            .ReceivedWithAnyArgs(1)\n            .HasPremiumAccessAsync(user.Id);\n    }\n\n    [Theory]\n    [BitAutoData(TwoFactorProviderType.Duo)]\n    [BitAutoData(TwoFactorProviderType.YubiKey)]\n    public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_WithPremium_ReturnsTrue(\n        TwoFactorProviderType premiumProviderType,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        UserWithCalculatedPremium user)\n    {\n        // Arrange\n        var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { premiumProviderType, new TwoFactorProvider { Enabled = true } }\n        };\n\n        user.SetTwoFactorProviders(twoFactorProviders);\n\n        sutProvider.GetDependency<IHasPremiumAccessQuery>()\n            .HasPremiumAccessAsync(user.Id)\n            .Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);\n\n        // Assert\n        Assert.True(result);\n\n        await sutProvider.GetDependency<IHasPremiumAccessQuery>()\n            .ReceivedWithAnyArgs(1)\n            .HasPremiumAccessAsync(user.Id);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task TwoFactorIsEnabledQuery_WithNullTwoFactorProviders_ReturnsFalse(\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        User user)\n    {\n        // Arrange\n        user.TwoFactorProviders = null; // No two-factor providers configured\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData((IEnumerable<Guid>)null)]\n    [BitAutoData([])]\n    public async Task TwoFactorIsEnabledAsync_WithNoUserIds_ReturnsEmpty(\n        IEnumerable<Guid> userIds,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider)\n    {\n        // Arrange\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [BitAutoData(TwoFactorProviderType.Duo)]\n    [BitAutoData(TwoFactorProviderType.YubiKey)]\n    public async Task TwoFactorIsEnabledAsync_WithMixedScenarios_ReturnsCorrectResults(\n        TwoFactorProviderType premiumProviderType,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        User user1,\n        User user2,\n        User user3)\n    {\n        // Arrange\n        var users = new List<User> { user1, user2, user3 };\n        var userIds = users.Select(u => u.Id).ToList();\n\n        // User 1: Non-premium provider → 2FA enabled\n        user1.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { TwoFactorProviderType.Authenticator, new TwoFactorProvider { Enabled = true } }\n        });\n\n        // User 2: Premium provider + has premium → 2FA enabled\n        user2.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { premiumProviderType, new TwoFactorProvider { Enabled = true } }\n        });\n\n        // User 3: Premium provider + no premium → 2FA disabled\n        user3.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { premiumProviderType, new TwoFactorProvider { Enabled = true } }\n        });\n\n        var premiumStatus = new Dictionary<Guid, bool>\n        {\n            { user2.Id, true },\n            { user3.Id, false }\n        };\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(userIds)))\n            .Returns(users);\n\n        sutProvider.GetDependency<IHasPremiumAccessQuery>()\n            .HasPremiumAccessAsync(Arg.Is<IEnumerable<Guid>>(ids =>\n                ids.Count() == 2 && ids.Contains(user2.Id) && ids.Contains(user3.Id)))\n            .Returns(premiumStatus);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);\n\n        // Assert\n        Assert.Contains(result, res => res.userId == user1.Id && res.twoFactorIsEnabled == true);  // Non-premium provider\n        Assert.Contains(result, res => res.userId == user2.Id && res.twoFactorIsEnabled == true);  // Premium + has premium\n        Assert.Contains(result, res => res.userId == user3.Id && res.twoFactorIsEnabled == false); // Premium + no premium\n    }\n\n    [Theory]\n    [BitAutoData(TwoFactorProviderType.Duo)]\n    [BitAutoData(TwoFactorProviderType.YubiKey)]\n    public async Task TwoFactorIsEnabledAsync_OnlyChecksPremiumAccessForUsersWhoNeedIt(\n        TwoFactorProviderType premiumProviderType,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        User user1,\n        User user2,\n        User user3)\n    {\n        // Arrange\n        var users = new List<User> { user1, user2, user3 };\n        var userIds = users.Select(u => u.Id).ToList();\n\n        // User 1: Has non-premium provider - should NOT trigger premium check\n        user1.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { TwoFactorProviderType.Authenticator, new TwoFactorProvider { Enabled = true } }\n        });\n\n        // User 2 & 3: Have only premium providers - SHOULD trigger premium check\n        user2.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { premiumProviderType, new TwoFactorProvider { Enabled = true } }\n        });\n        user3.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { premiumProviderType, new TwoFactorProvider { Enabled = true } }\n        });\n\n        var premiumStatus = new Dictionary<Guid, bool>\n        {\n            { user2.Id, true },\n            { user3.Id, false }\n        };\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(userIds)))\n            .Returns(users);\n\n        sutProvider.GetDependency<IHasPremiumAccessQuery>()\n            .HasPremiumAccessAsync(Arg.Is<IEnumerable<Guid>>(ids =>\n                ids.Count() == 2 && ids.Contains(user2.Id) && ids.Contains(user3.Id)))\n            .Returns(premiumStatus);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);\n\n        // Assert - Verify optimization: premium checked ONLY for users 2 and 3 (not user 1)\n        await sutProvider.GetDependency<IHasPremiumAccessQuery>()\n            .Received(1)\n            .HasPremiumAccessAsync(Arg.Is<IEnumerable<Guid>>(ids =>\n                ids.Count() == 2 && ids.Contains(user2.Id) && ids.Contains(user3.Id)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task TwoFactorIsEnabledAsync_WithNoUserIds_ReturnsAllTwoFactorDisabled(\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        List<OrganizationUserUserDetails> users)\n    {\n        // Arrange\n        foreach (var user in users)\n        {\n            user.UserId = null;\n        }\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(users);\n\n        // Assert\n        foreach (var user in users)\n        {\n            Assert.Contains(result, res => res.user.Equals(user) && res.twoFactorIsEnabled == false);\n        }\n\n        // No UserIds were supplied so no calls to the UserRepository should have been made\n        await sutProvider.GetDependency<IUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .GetManyAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData(TwoFactorProviderType.Authenticator, true)]  // Non-premium provider\n    [BitAutoData(TwoFactorProviderType.Duo, true)]            // Premium provider with premium access\n    [BitAutoData(TwoFactorProviderType.YubiKey, false)]       // Premium provider without premium access\n    public async Task TwoFactorIsEnabledAsync_SingleUser_VariousScenarios(\n        TwoFactorProviderType providerType,\n        bool hasPremiumAccess,\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        User user)\n    {\n        // Arrange\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { providerType, new TwoFactorProvider { Enabled = true } }\n        });\n\n        sutProvider.GetDependency<IHasPremiumAccessQuery>()\n            .HasPremiumAccessAsync(user.Id)\n            .Returns(hasPremiumAccess);\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);\n\n        // Assert\n        var requiresPremium = TwoFactorProvider.RequiresPremium(providerType);\n        var expectedResult = !requiresPremium || hasPremiumAccess;\n        Assert.Equal(expectedResult, result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task TwoFactorIsEnabledAsync_WithNoEnabledProviders_ReturnsFalse(\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        User user)\n    {\n        // Arrange\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            { TwoFactorProviderType.Email, new TwoFactorProvider { Enabled = false } }\n        });\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task TwoFactorIsEnabledAsync_WithNullProviders_ReturnsFalse(\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        User user)\n    {\n        // Arrange\n        user.TwoFactorProviders = null;\n\n        // Act\n        var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task TwoFactorIsEnabledAsync_UserNotFound_ThrowsNotFoundException(\n        SutProvider<TwoFactorIsEnabledQuery> sutProvider,\n        Guid userId)\n    {\n        // Arrange\n        var testUser = new TestTwoFactorProviderUser\n        {\n            Id = userId,\n            TwoFactorProviders = null\n        };\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(userId)\n            .Returns((User)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(\n            async () => await sutProvider.Sut.TwoFactorIsEnabledAsync(testUser));\n    }\n\n    private class TestTwoFactorProviderUser : ITwoFactorProvidersUser\n    {\n        public Guid? Id { get; set; }\n        public string TwoFactorProviders { get; set; }\n        public bool Premium { get; set; }\n        public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()\n        {\n            return JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(TwoFactorProviders);\n        }\n\n        public Guid? GetUserId()\n        {\n            return Id;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.UserMasterPassword;\n\n[SutProviderCustomize]\npublic class SetInitialMasterPasswordCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_Success(SutProvider<SetInitialMasterPasswordCommand> sutProvider,\n        User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings,\n        Organization org, OrganizationUser orgUser, string serverSideHash, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        var model = CreateValidModel(user, accountKeys, kdfSettings, org.Identifier, masterPasswordHint);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(org.Identifier)\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IPasswordHasher<User>>()\n            .HashPassword(user, model.MasterPasswordAuthentication.MasterPasswordAuthenticationHash)\n            .Returns(serverSideHash);\n\n        // Mock SetMasterPassword to return a specific UpdateUserData delegate\n        UpdateUserData mockUpdateUserData = (connection, transaction) => Task.CompletedTask;\n        sutProvider.GetDependency<IUserRepository>()\n            .SetMasterPassword(user.Id, model.MasterPasswordUnlock, serverSideHash, model.MasterPasswordHint)\n            .Returns(mockUpdateUserData);\n\n        // Act\n        await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model);\n\n        // Assert\n        await sutProvider.GetDependency<IUserRepository>().Received(1)\n            .SetV2AccountCryptographicStateAsync(\n                user.Id,\n                model.AccountKeys,\n                Arg.Do<IEnumerable<UpdateUserData>>(actions =>\n                {\n                    var actionsList = actions.ToList();\n                    Assert.Single(actionsList);\n                    Assert.Same(mockUpdateUserData, actionsList[0]);\n                }));\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_ChangedPassword);\n\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)\n            .AcceptOrgUserAsync(orgUser, user, sutProvider.GetDependency<IUserService>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_UserAlreadyHasPassword_ThrowsBadRequestException(\n        SutProvider<SetInitialMasterPasswordCommand> sutProvider,\n        User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = \"existing-key\";\n        var model = CreateValidModel(user, accountKeys, kdfSettings, orgSsoIdentifier, masterPasswordHint);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));\n        Assert.Equal(\"User already has a master password set.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_AccountKeysNull_ThrowsBadRequestException(\n        SutProvider<SetInitialMasterPasswordCommand> sutProvider,\n        User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        var model = CreateValidModel(user, null, kdfSettings, orgSsoIdentifier, masterPasswordHint);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));\n        Assert.Equal(\"Account keys are required.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(\"wrong-salt\", null)]\n    [BitAutoData([null, \"wrong-salt\"])]\n    [BitAutoData(\"wrong-salt\", \"different-wrong-salt\")]\n    public async Task SetInitialMasterPassword_InvalidSalt_ThrowsBadRequestException(\n        string? authSaltOverride, string? unlockSaltOverride,\n        SutProvider<SetInitialMasterPasswordCommand> sutProvider,\n        User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        var correctSalt = user.GetMasterPasswordSalt();\n        var model = new SetInitialMasterPasswordDataModel\n        {\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationData\n            {\n                Salt = authSaltOverride ?? correctSalt,\n                MasterPasswordAuthenticationHash = \"hash\",\n                Kdf = kdfSettings\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockData\n            {\n                Salt = unlockSaltOverride ?? correctSalt,\n                MasterKeyWrappedUserKey = \"wrapped-key\",\n                Kdf = kdfSettings\n            },\n            AccountKeys = accountKeys,\n            OrgSsoIdentifier = orgSsoIdentifier,\n            MasterPasswordHint = masterPasswordHint\n        };\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));\n        Assert.Equal(\"Invalid master password salt.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_InvalidOrgSsoIdentifier_ThrowsBadRequestException(\n        SutProvider<SetInitialMasterPasswordCommand> sutProvider,\n        User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        var model = CreateValidModel(user, accountKeys, kdfSettings, orgSsoIdentifier, masterPasswordHint);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(orgSsoIdentifier)\n            .ReturnsNull();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));\n        Assert.Equal(\"Organization SSO identifier is invalid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_UserNotFoundInOrganization_ThrowsBadRequestException(\n        SutProvider<SetInitialMasterPasswordCommand> sutProvider,\n        User user, UserAccountKeysData accountKeys, KdfSettings kdfSettings, Organization org, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        var model = CreateValidModel(user, accountKeys, kdfSettings, org.Identifier, masterPasswordHint);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(org.Identifier)\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .ReturnsNull();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, model));\n        Assert.Equal(\"User not found within organization.\", exception.Message);\n    }\n\n    private static SetInitialMasterPasswordDataModel CreateValidModel(\n        User user, UserAccountKeysData? accountKeys, KdfSettings kdfSettings,\n        string orgSsoIdentifier, string? masterPasswordHint)\n    {\n        var salt = user.GetMasterPasswordSalt();\n        return new SetInitialMasterPasswordDataModel\n        {\n            MasterPasswordAuthentication = new MasterPasswordAuthenticationData\n            {\n                Salt = salt,\n                MasterPasswordAuthenticationHash = \"hash\",\n                Kdf = kdfSettings\n            },\n            MasterPasswordUnlock = new MasterPasswordUnlockData\n            {\n                Salt = salt,\n                MasterKeyWrappedUserKey = \"wrapped-key\",\n                Kdf = kdfSettings\n            },\n            AccountKeys = accountKeys,\n            OrgSsoIdentifier = orgSsoIdentifier,\n            MasterPasswordHint = masterPasswordHint\n        };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/UserMasterPassword/SetInitialMasterPasswordCommandV1Tests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.UserMasterPassword;\n\n[SutProviderCustomize]\npublic class SetInitialMasterPasswordCommandV1Tests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_Success(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider,\n        User user, string masterPassword, string key, string orgIdentifier,\n        Organization org, OrganizationUser orgUser)\n    {\n        // Arrange\n        user.MasterPassword = null;\n\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(orgIdentifier)\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .Returns(orgUser);\n\n        // Act\n        var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);\n\n        // Assert\n        Assert.Equal(IdentityResult.Success, result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_UserIsNull_ThrowsArgumentNullException(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, string masterPassword, string key, string orgIdentifier)\n    {\n        // Act & Assert\n        await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(null, masterPassword, key, orgIdentifier));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_AlreadyHasPassword_ReturnsFalse(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, User user, string masterPassword, string key, string orgIdentifier)\n    {\n        // Arrange\n        user.MasterPassword = \"ExistingPassword\";\n\n        // Act\n        var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);\n\n        // Assert\n        Assert.False(result.Succeeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_NullOrgSsoIdentifier_ThrowsBadRequestException(\n        SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, User user, string masterPassword, string key)\n    {\n        // Arrange\n        user.MasterPassword = null;\n        string orgSsoIdentifier = null;\n\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)\n            .Returns(IdentityResult.Success);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgSsoIdentifier));\n        Assert.Equal(\"Organization SSO Identifier required.\", exception.Message);\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_InvalidOrganization_Throws(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, User user, string masterPassword, string key, string orgIdentifier)\n    {\n        // Arrange\n        user.MasterPassword = null;\n\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(orgIdentifier)\n            .ReturnsNull();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier));\n        Assert.Equal(\"Organization invalid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_UserNotFoundInOrganization_Throws(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider, User user, string masterPassword, string key, Organization org)\n    {\n        // Arrange\n        user.MasterPassword = null;\n\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(Arg.Any<string>())\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .ReturnsNull();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, org.Identifier));\n        Assert.Equal(\"User not found within organization.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_ConfirmedOrgUser_DoesNotCallAcceptOrgUser(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider,\n        User user, string masterPassword, string key, string orgIdentifier, Organization org, OrganizationUser orgUser)\n    {\n        // Arrange\n        user.MasterPassword = null;\n\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(orgIdentifier)\n            .Returns(org);\n\n        orgUser.Status = OrganizationUserStatusType.Confirmed;\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .Returns(orgUser);\n\n\n        // Act\n        var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);\n\n        // Assert\n        Assert.Equal(IdentityResult.Success, result);\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().DidNotReceive().AcceptOrgUserAsync(Arg.Any<OrganizationUser>(), Arg.Any<User>(), Arg.Any<IUserService>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetInitialMasterPassword_InvitedOrgUser_CallsAcceptOrgUser(SutProvider<SetInitialMasterPasswordCommandV1> sutProvider,\n        User user, string masterPassword, string key, string orgIdentifier, Organization org, OrganizationUser orgUser)\n    {\n        // Arrange\n        user.MasterPassword = null;\n\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), true, false)\n            .Returns(IdentityResult.Success);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(orgIdentifier)\n            .Returns(org);\n\n        orgUser.Status = OrganizationUserStatusType.Invited;\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .Returns(orgUser);\n\n        // Act\n        var result = await sutProvider.Sut.SetInitialMasterPasswordAsync(user, masterPassword, key, orgIdentifier);\n\n        // Assert\n        Assert.Equal(IdentityResult.Success, result);\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1).AcceptOrgUserAsync(orgUser, user, sutProvider.GetDependency<IUserService>());\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/UserMasterPassword/TdeSetPasswordCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Auth.UserFeatures.UserMasterPassword;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.UserMasterPassword;\n\n[SutProviderCustomize]\npublic class TdeSetPasswordCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task OnboardMasterPassword_Success(SutProvider<TdeSetPasswordCommand> sutProvider,\n        User user, KdfSettings kdfSettings,\n        Organization org, OrganizationUser orgUser, string serverSideHash, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        user.PublicKey = \"public-key\";\n        user.PrivateKey = \"private-key\";\n        var model = CreateValidModel(user, kdfSettings, org.Identifier, masterPasswordHint);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(org.Identifier)\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .Returns(orgUser);\n\n        sutProvider.GetDependency<IPasswordHasher<User>>()\n            .HashPassword(user, model.MasterPasswordAuthentication.MasterPasswordAuthenticationHash)\n            .Returns(serverSideHash);\n\n        // Mock SetMasterPassword to return a specific UpdateUserData delegate\n        UpdateUserData mockUpdateUserData = (connection, transaction) => Task.CompletedTask;\n        sutProvider.GetDependency<IUserRepository>()\n            .SetMasterPassword(user.Id, model.MasterPasswordUnlock, serverSideHash, model.MasterPasswordHint)\n            .Returns(mockUpdateUserData);\n\n        // Act\n        await sutProvider.Sut.SetMasterPasswordAsync(user, model);\n\n        // Assert\n        await sutProvider.GetDependency<IUserRepository>().Received(1)\n            .UpdateUserDataAsync(Arg.Do<IEnumerable<UpdateUserData>>(actions =>\n            {\n                var actionsList = actions.ToList();\n                Assert.Single(actionsList);\n                Assert.Same(mockUpdateUserData, actionsList[0]);\n            }));\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_ChangedPassword);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task OnboardMasterPassword_UserAlreadyHasPassword_ThrowsBadRequestException(\n        SutProvider<TdeSetPasswordCommand> sutProvider,\n        User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = \"existing-key\";\n        var model = CreateValidModel(user, kdfSettings, orgSsoIdentifier, masterPasswordHint);\n\n        // Act & Assert\n        var exception =\n            await Assert.ThrowsAsync<BadRequestException>(async () =>\n                await sutProvider.Sut.SetMasterPasswordAsync(user, model));\n        Assert.Equal(\"User already has a master password set.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData([null, \"private-key\"])]\n    [BitAutoData(\"public-key\", null)]\n    [BitAutoData([null, null])]\n    public async Task OnboardMasterPassword_MissingAccountKeys_ThrowsBadRequestException(\n        string? publicKey, string? privateKey,\n        SutProvider<TdeSetPasswordCommand> sutProvider,\n        User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        user.PublicKey = publicKey;\n        user.PrivateKey = privateKey;\n        var model = CreateValidModel(user, kdfSettings, orgSsoIdentifier, masterPasswordHint);\n\n        // Act & Assert\n        var exception =\n            await Assert.ThrowsAsync<BadRequestException>(async () =>\n                await sutProvider.Sut.SetMasterPasswordAsync(user, model));\n        Assert.Equal(\"TDE user account keys must be set before setting initial master password.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData(\"wrong-salt\", null)]\n    [BitAutoData([null, \"wrong-salt\"])]\n    [BitAutoData(\"wrong-salt\", \"different-wrong-salt\")]\n    public async Task OnboardMasterPassword_InvalidSalt_ThrowsBadRequestException(\n        string? authSaltOverride, string? unlockSaltOverride,\n        SutProvider<TdeSetPasswordCommand> sutProvider,\n        User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        user.PublicKey = \"public-key\";\n        user.PrivateKey = \"private-key\";\n        var correctSalt = user.GetMasterPasswordSalt();\n        var model = new SetInitialMasterPasswordDataModel\n        {\n            MasterPasswordAuthentication =\n                new MasterPasswordAuthenticationData\n                {\n                    Salt = authSaltOverride ?? correctSalt,\n                    MasterPasswordAuthenticationHash = \"hash\",\n                    Kdf = kdfSettings\n                },\n            MasterPasswordUnlock = new MasterPasswordUnlockData\n            {\n                Salt = unlockSaltOverride ?? correctSalt,\n                MasterKeyWrappedUserKey = \"wrapped-key\",\n                Kdf = kdfSettings\n            },\n            AccountKeys = null,\n            OrgSsoIdentifier = orgSsoIdentifier,\n            MasterPasswordHint = masterPasswordHint\n        };\n\n        // Act & Assert\n        var exception =\n            await Assert.ThrowsAsync<BadRequestException>(async () =>\n                await sutProvider.Sut.SetMasterPasswordAsync(user, model));\n        Assert.Equal(\"Invalid master password salt.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task OnboardMasterPassword_InvalidOrgSsoIdentifier_ThrowsBadRequestException(\n        SutProvider<TdeSetPasswordCommand> sutProvider,\n        User user, KdfSettings kdfSettings, string orgSsoIdentifier, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        user.PublicKey = \"public-key\";\n        user.PrivateKey = \"private-key\";\n        var model = CreateValidModel(user, kdfSettings, orgSsoIdentifier, masterPasswordHint);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(orgSsoIdentifier)\n            .ReturnsNull();\n\n        // Act & Assert\n        var exception =\n            await Assert.ThrowsAsync<BadRequestException>(async () =>\n                await sutProvider.Sut.SetMasterPasswordAsync(user, model));\n        Assert.Equal(\"Organization SSO identifier is invalid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task OnboardMasterPassword_UserNotFoundInOrganization_ThrowsBadRequestException(\n        SutProvider<TdeSetPasswordCommand> sutProvider,\n        User user, KdfSettings kdfSettings, Organization org, string masterPasswordHint)\n    {\n        // Arrange\n        user.Key = null;\n        user.PublicKey = \"public-key\";\n        user.PrivateKey = \"private-key\";\n        var model = CreateValidModel(user, kdfSettings, org.Identifier, masterPasswordHint);\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdentifierAsync(org.Identifier)\n            .Returns(org);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(org.Id, user.Id)\n            .ReturnsNull();\n\n        // Act & Assert\n        var exception =\n            await Assert.ThrowsAsync<BadRequestException>(async () =>\n                await sutProvider.Sut.SetMasterPasswordAsync(user, model));\n        Assert.Equal(\"User not found within organization.\", exception.Message);\n    }\n\n    private static SetInitialMasterPasswordDataModel CreateValidModel(\n        User user, KdfSettings kdfSettings, string orgSsoIdentifier, string? masterPasswordHint)\n    {\n        var salt = user.GetMasterPasswordSalt();\n        return new SetInitialMasterPasswordDataModel\n        {\n            MasterPasswordAuthentication =\n                new MasterPasswordAuthenticationData\n                {\n                    Salt = salt,\n                    MasterPasswordAuthenticationHash = \"hash\",\n                    Kdf = kdfSettings\n                },\n            MasterPasswordUnlock =\n                new MasterPasswordUnlockData\n                {\n                    Salt = salt,\n                    MasterKeyWrappedUserKey = \"wrapped-key\",\n                    Kdf = kdfSettings\n                },\n            AccountKeys = null,\n            OrgSsoIdentifier = orgSsoIdentifier,\n            MasterPasswordHint = masterPasswordHint\n        };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/WebAuthnLogin/AssertWebAuthnLoginCredentialCommandTests.cs",
    "content": "﻿using System.Text;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Fido2NetLib;\nusing Fido2NetLib.Objects;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.WebAuthnLogin;\n\n[SutProviderCustomize]\npublic class AssertWebAuthnLoginCredentialCommandTests\n{\n    [Theory, BitAutoData]\n    internal async Task InvalidUserHandle_ThrowsBadRequestException(SutProvider<AssertWebAuthnLoginCredentialCommand> sutProvider, AssertionOptions options, AuthenticatorAssertionRawResponse response)\n    {\n        // Arrange\n        response.Response.UserHandle = Encoding.UTF8.GetBytes(\"invalid-user-handle\");\n\n        // Act\n        var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response);\n\n        // Assert\n        await Assert.ThrowsAsync<BadRequestException>(result);\n    }\n\n    [Theory, BitAutoData]\n    internal async Task UserNotFound_ThrowsBadRequestException(SutProvider<AssertWebAuthnLoginCredentialCommand> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response)\n    {\n        // Arrange\n        response.Response.UserHandle = user.Id.ToByteArray();\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).ReturnsNull();\n\n        // Act\n        var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response);\n\n        // Assert\n        await Assert.ThrowsAsync<BadRequestException>(result);\n    }\n\n    [Theory, BitAutoData]\n    internal async Task NoMatchingCredentialExists_ThrowsBadRequestException(SutProvider<AssertWebAuthnLoginCredentialCommand> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response)\n    {\n        // Arrange\n        response.Response.UserHandle = user.Id.ToByteArray();\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { });\n\n        // Act\n        var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response);\n\n        // Assert\n        await Assert.ThrowsAsync<BadRequestException>(result);\n    }\n\n    [Theory, BitAutoData]\n    internal async Task AssertionFails_ThrowsBadRequestException(SutProvider<AssertWebAuthnLoginCredentialCommand> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response, WebAuthnCredential credential, AssertionVerificationResult assertionResult)\n    {\n        // Arrange\n        var credentialId = Guid.NewGuid().ToByteArray();\n        credential.CredentialId = CoreHelpers.Base64UrlEncode(credentialId);\n        response.Id = credentialId;\n        response.Response.UserHandle = user.Id.ToByteArray();\n        assertionResult.Status = \"Not ok\";\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential });\n        sutProvider.GetDependency<IFido2>().MakeAssertionAsync(response, options, Arg.Any<byte[]>(), Arg.Any<uint>(), Arg.Any<IsUserHandleOwnerOfCredentialIdAsync>())\n            .Returns(assertionResult);\n\n        // Act\n        var result = async () => await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response);\n\n        // Assert\n        await Assert.ThrowsAsync<BadRequestException>(result);\n    }\n\n    [Theory, BitAutoData]\n    internal async Task AssertionSucceeds_ReturnsUserAndCredential(SutProvider<AssertWebAuthnLoginCredentialCommand> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response, WebAuthnCredential credential, AssertionVerificationResult assertionResult)\n    {\n        // Arrange\n        var credentialId = Guid.NewGuid().ToByteArray();\n        credential.CredentialId = CoreHelpers.Base64UrlEncode(credentialId);\n        response.Id = credentialId;\n        response.Response.UserHandle = user.Id.ToByteArray();\n        assertionResult.Status = \"ok\";\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential });\n        sutProvider.GetDependency<IFido2>().MakeAssertionAsync(response, options, Arg.Any<byte[]>(), Arg.Any<uint>(), Arg.Any<IsUserHandleOwnerOfCredentialIdAsync>())\n            .Returns(assertionResult);\n\n        // Act\n        var result = await sutProvider.Sut.AssertWebAuthnLoginCredential(options, response);\n\n        // Assert\n        var (userResult, credentialResult) = result;\n        Assert.Equal(user, userResult);\n        Assert.Equal(credential, credentialResult);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/WebAuthnLogin/CreateWebAuthnLoginCredentialCommandTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Fido2NetLib;\nusing Fido2NetLib.Objects;\nusing NSubstitute;\nusing Xunit;\nusing static Fido2NetLib.Fido2;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.WebAuthnLogin;\n\n[SutProviderCustomize]\npublic class CreateWebAuthnLoginCredentialCommandTests\n{\n    [Theory, BitAutoData]\n    internal async Task ExceedsExistingCredentialsLimit_ReturnsFalse(SutProvider<CreateWebAuthnLoginCredentialCommand> sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator<WebAuthnCredential> credentialGenerator)\n    {\n        // Arrange\n        var existingCredentials = credentialGenerator.Take(CreateWebAuthnLoginCredentialCommand.MaxCredentialsPerUser).ToList();\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(existingCredentials);\n\n        // Act\n        var result = await sutProvider.Sut.CreateWebAuthnLoginCredentialAsync(user, \"name\", options, response, false, null, null, null);\n\n        // Assert\n        Assert.Null(result);\n        await sutProvider.GetDependency<IWebAuthnCredentialRepository>().DidNotReceive().CreateAsync(Arg.Any<WebAuthnCredential>());\n    }\n\n    [Theory, BitAutoData]\n    internal async Task DoesNotExceedExistingCredentialsLimit_CreatesCredential(SutProvider<CreateWebAuthnLoginCredentialCommand> sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator<WebAuthnCredential> credentialGenerator)\n    {\n        // Arrange\n        var existingCredentials = credentialGenerator.Take(CreateWebAuthnLoginCredentialCommand.MaxCredentialsPerUser - 1).ToList();\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(existingCredentials);\n        sutProvider.GetDependency<IFido2>().MakeNewCredentialAsync(\n            response, options, Arg.Any<IsCredentialIdUniqueToUserAsyncDelegate>(), Arg.Any<byte[]>(), Arg.Any<CancellationToken>()\n        ).Returns(MakeCredentialResult());\n\n        // Act\n        var result = await sutProvider.Sut.CreateWebAuthnLoginCredentialAsync(user, \"name\", options, response, false, null, null, null);\n\n        // Assert\n        Assert.NotNull(result);\n        await sutProvider.GetDependency<IWebAuthnCredentialRepository>().Received().CreateAsync(Arg.Any<WebAuthnCredential>());\n    }\n\n    private CredentialMakeResult MakeCredentialResult()\n    {\n        return new CredentialMakeResult(\"ok\", \"\", new AttestationVerificationSuccess\n        {\n            Aaguid = new Guid(),\n            Counter = 0,\n            CredentialId = new Guid().ToByteArray(),\n            CredType = \"public-key\",\n            PublicKey = new byte[0],\n            Status = \"ok\",\n            User = new Fido2User(),\n        });\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Auth/UserFeatures/WebAuthnLogin/GetWebAuthnLoginCredentialCreateOptionsCommandTests.cs",
    "content": "﻿using Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Repositories;\nusing Bit.Core.Auth.UserFeatures.WebAuthnLogin.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Fido2NetLib;\nusing Fido2NetLib.Objects;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Auth.UserFeatures.WebAuthnLogin;\n\n[SutProviderCustomize]\npublic class GetWebAuthnLoginCredentialCreateOptionsTests\n{\n    [Theory, BitAutoData]\n    internal async Task NoExistingCredentials_ReturnsOptionsWithoutExcludedCredentials(SutProvider<GetWebAuthnLoginCredentialCreateOptionsCommand> sutProvider, User user)\n    {\n        // Arrange\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns(new List<WebAuthnCredential>());\n\n        // Act\n        var result = await sutProvider.Sut.GetWebAuthnLoginCredentialCreateOptionsAsync(user);\n\n        // Assert\n        sutProvider.GetDependency<IFido2>()\n            .Received()\n            .RequestNewCredential(\n                Arg.Any<Fido2User>(),\n                Arg.Is<List<PublicKeyCredentialDescriptor>>(list => list.Count == 0),\n                Arg.Any<AuthenticatorSelection>(),\n                Arg.Any<AttestationConveyancePreference>(),\n                Arg.Any<AuthenticationExtensionsClientInputs>());\n    }\n\n    [Theory, BitAutoData]\n    internal async Task HasExistingCredential_ReturnsOptionsWithExcludedCredential(SutProvider<GetWebAuthnLoginCredentialCreateOptionsCommand> sutProvider, User user, WebAuthnCredential credential)\n    {\n        // Arrange\n        sutProvider.GetDependency<IWebAuthnCredentialRepository>()\n            .GetManyByUserIdAsync(user.Id)\n            .Returns(new List<WebAuthnCredential> { credential });\n\n        // Act\n        var result = await sutProvider.Sut.GetWebAuthnLoginCredentialCreateOptionsAsync(user);\n\n        // Assert\n        sutProvider.GetDependency<IFido2>()\n            .Received()\n            .RequestNewCredential(\n                Arg.Any<Fido2User>(),\n                Arg.Is<List<PublicKeyCredentialDescriptor>>(list => list.Count == 1),\n                Arg.Any<AuthenticatorSelection>(),\n                Arg.Any<AttestationConveyancePreference>(),\n                Arg.Any<AuthenticationExtensionsClientInputs>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AutoFixture/Attributes/CiSkippedTheory.cs",
    "content": "﻿namespace Bit.Core.Test.AutoFixture.Attributes;\n\npublic sealed class CiSkippedTheory : Xunit.TheoryAttribute\n{\n    private static bool IsGithubActions() => Environment.GetEnvironmentVariable(\"CI\") != null;\n    public CiSkippedTheory()\n    {\n        if (IsGithubActions())\n        {\n            Skip = \"Ignore during CI builds\";\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AutoFixture/AutoFixtureExtensions.cs",
    "content": "﻿using System.Linq.Expressions;\nusing AutoFixture.Dsl;\n\nnamespace Bit.Core.Test.AutoFixture;\n\npublic static class AutoFixtureExtensions\n{\n    /// <summary>\n    /// Registers that a writable Guid property should be assigned a random value that is derived from the given seed.\n    /// </summary>\n    /// <remarks>\n    /// This can be used to generate random Guids that are deterministic based on the seed and thus can be re-used for\n    /// different entities that share the same identifiers. e.g. Collections, CollectionUsers, and CollectionGroups can\n    /// all have the same Guids generate for their \"collection id\" properties.\n    /// </remarks>\n    /// <param name=\"composer\"></param>\n    /// <param name=\"propertyPicker\">The Guid property to register</param>\n    /// <param name=\"seed\">The random seed to use for random Guid generation</param>\n    public static IPostprocessComposer<T> WithGuidFromSeed<T>(this IPostprocessComposer<T> composer, Expression<Func<T, Guid>> propertyPicker, int seed)\n    {\n        var rnd = new Random(seed);\n        return composer.With(propertyPicker, () =>\n        {\n            // While not as random/unique as Guid.NewGuid(), this is works well enough for testing purposes.\n            var bytes = new byte[16];\n            rnd.NextBytes(bytes);\n            return new Guid(bytes);\n        });\n    }\n\n    /// <summary>\n    /// Registers that a writable property should be assigned a value from the given list.\n    /// </summary>\n    /// <remarks>\n    /// The value will be assigned in the order that the list is enumerated. Values will wrap around to the beginning\n    /// should the end of the list be reached.\n    /// </remarks>\n    /// <param name=\"composer\"></param>\n    /// <param name=\"propertyPicker\"></param>\n    /// <param name=\"values\"></param>\n    public static IPostprocessComposer<T> WithValueFromList<T, TValue>(\n        this IPostprocessComposer<T> composer,\n        Expression<Func<T, TValue>> propertyPicker,\n        ICollection<TValue> values)\n    {\n        var index = 0;\n        return composer.With(propertyPicker, () =>\n        {\n            var value = values.ElementAt(index);\n            index = (index + 1) % values.Count;\n            return value;\n        });\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AutoFixture/CollectionAccessSelectionFixtures.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.Models.Data;\n\nnamespace Bit.Core.Test.AutoFixture;\n\npublic class CollectionAccessSelectionCustomization : ICustomization\n{\n    public bool Manage { get; set; }\n    public bool ReadOnly { get; set; }\n    public bool HidePasswords { get; set; }\n\n    public CollectionAccessSelectionCustomization(bool manage)\n    {\n        Manage = manage;\n        ReadOnly = !manage;\n        HidePasswords = !manage;\n    }\n\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<CollectionAccessSelection>(composer => composer\n            .With(o => o.Manage, Manage)\n            .With(o => o.ReadOnly, ReadOnly)\n            .With(o => o.HidePasswords, HidePasswords));\n    }\n}\n\npublic class CollectionAccessSelectionCustomizeAttribute : CustomizeAttribute\n{\n    private readonly bool _manage;\n\n    public CollectionAccessSelectionCustomizeAttribute(bool manage = false)\n    {\n        _manage = manage;\n    }\n\n    public override ICustomization GetCustomization(ParameterInfo parameter)\n    {\n        return new CollectionAccessSelectionCustomization(_manage);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AutoFixture/CurrentContextFixtures.cs",
    "content": "﻿using AutoFixture;\nusing AutoFixture.Kernel;\nusing Bit.Core.Context;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.AutoFixture.CurrentContextFixtures;\n\ninternal class CurrentContext : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customizations.Add(new CurrentContextBuilder());\n    }\n}\n\ninternal class CurrentContextBuilder : ISpecimenBuilder\n{\n    public object Create(object request, ISpecimenContext context)\n    {\n        if (context == null)\n        {\n            throw new ArgumentNullException(nameof(context));\n        }\n        if (!(request is Type typeRequest))\n        {\n            return new NoSpecimen();\n        }\n        if (typeof(ICurrentContext) != typeRequest)\n        {\n            return new NoSpecimen();\n        }\n\n        var obj = new Fixture().WithAutoNSubstitutions().Create<ICurrentContext>();\n        obj.Organizations = context.Create<List<CurrentContextOrganization>>();\n        return obj;\n    }\n}\n\ninternal class CurrentContextCustomize : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new CurrentContext();\n}\n"
  },
  {
    "path": "test/Core.Test/AutoFixture/GlobalSettingsFixtures.cs",
    "content": "﻿using System.Reflection;\nusing System.Text;\nusing AutoFixture;\nusing AutoFixture.Kernel;\nusing AutoFixture.Xunit2;\nusing Bit.Core.Test.Helpers.Factories;\nusing Microsoft.AspNetCore.DataProtection;\nusing NSubstitute;\n\nnamespace Bit.Test.Common.AutoFixture;\n\npublic class GlobalSettingsBuilder : ISpecimenBuilder\n{\n    public object Create(object request, ISpecimenContext context)\n    {\n        if (context == null)\n        {\n            throw new ArgumentNullException(nameof(context));\n        }\n\n        var fixture = new Fixture();\n\n        if (request is not ParameterInfo pi)\n        {\n            return new NoSpecimen();\n        }\n\n        if (pi.ParameterType == typeof(Bit.Core.Settings.GlobalSettings))\n        {\n            return GlobalSettingsFactory.GlobalSettings;\n        }\n\n        if (pi.ParameterType == typeof(IDataProtectionProvider))\n        {\n            var dataProtector = Substitute.For<IDataProtector>();\n            dataProtector.Unprotect(Arg.Any<byte[]>())\n                .Returns(data =>\n                    Encoding.UTF8.GetBytes(Core.Constants.DatabaseFieldProtectedPrefix +\n                                           Encoding.UTF8.GetString((byte[])data[0])));\n\n            var dataProtectionProvider = Substitute.For<IDataProtectionProvider>();\n            dataProtectionProvider.CreateProtector(Core.Constants.DatabaseFieldProtectorPurpose)\n                .Returns(dataProtector);\n\n            return dataProtectionProvider;\n        }\n\n        return new NoSpecimen();\n    }\n}\n\npublic class GlobalSettingsCustomizeAttribute : CustomizeAttribute\n{\n    public override ICustomization GetCustomization(ParameterInfo parameter) => new GlobalSettings();\n}\n"
  },
  {
    "path": "test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;\n\npublic class OrganizationSponsorshipCustomizeAttribute : BitCustomizeAttribute\n{\n    public bool ToDelete = false;\n    public override ICustomization GetCustomization() => ToDelete ?\n        new ToDeleteOrganizationSponsorship() :\n        new ValidOrganizationSponsorship();\n}\n\npublic class ValidOrganizationSponsorship : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<OrganizationSponsorship>(composer => composer\n            .With(s => s.ToDelete, false)\n            .With(s => s.LastSyncDate, DateTime.UtcNow.AddDays(new Random().Next(-90, 0))));\n    }\n}\n\npublic class ToDeleteOrganizationSponsorship : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<OrganizationSponsorship>(composer => composer\n            .With(s => s.ToDelete, true));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AutoFixture/QueueClientFixtures.cs",
    "content": "﻿#nullable enable\nusing AutoFixture;\nusing AutoFixture.Kernel;\nusing Azure.Storage.Queues;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\n\nnamespace Bit.Core.Test.AutoFixture;\n\npublic class QueueClientBuilder : ISpecimenBuilder\n{\n    public object Create(object request, ISpecimenContext context)\n    {\n        var type = request as Type;\n        if (type == typeof(QueueClient))\n        {\n            return Substitute.For<QueueClient>();\n        }\n\n        return new NoSpecimen();\n    }\n}\n\npublic class QueueClientCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new QueueClientFixtures();\n}\n\npublic class QueueClientFixtures : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customizations.Add(new QueueClientBuilder());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AutoFixture/SubscriptionInfoCustomization.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Models.Business;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.AutoFixture;\n\npublic class SubscriptionInfoCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new SubscriptionInfoCustomization();\n}\npublic class SubscriptionInfoCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        // The Subscription property uses the external Stripe library, which Autofixture doesn't handle\n        fixture.Customize<SubscriptionInfo>(c => c.Without(s => s.Subscription));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/AutoFixture/UserFixtures.cs",
    "content": "﻿using AutoFixture;\nusing AutoFixture.Kernel;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.AutoFixture.UserFixtures;\n\npublic class UserBuilder : ISpecimenBuilder\n{\n    public object Create(object request, ISpecimenContext context)\n    {\n        if (context == null)\n        {\n            throw new ArgumentNullException(nameof(context));\n        }\n\n        var type = request as Type;\n        if (type == typeof(User))\n        {\n            var fixture = new Fixture();\n            var providers = fixture.Create<Dictionary<TwoFactorProviderType, TwoFactorProvider>>();\n            var user = fixture.WithAutoNSubstitutions().Create<User>();\n            user.SetTwoFactorProviders(providers);\n            return user;\n        }\n        else if (type == typeof(List<User>))\n        {\n            var fixture = new Fixture();\n            var users = fixture.WithAutoNSubstitutions().CreateMany<User>(2);\n            foreach (var user in users)\n            {\n                var providers = fixture.Create<Dictionary<TwoFactorProviderType, TwoFactorProvider>>();\n                user.SetTwoFactorProviders(providers);\n            }\n            return users;\n        }\n\n        return new NoSpecimen();\n    }\n}\n\ninternal class UserCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new UserFixture();\n}\n\npublic class UserFixture : ICustomization\n{\n    public virtual void Customize(IFixture fixture)\n    {\n        fixture.Customizations.Add(new GlobalSettingsBuilder());\n        fixture.Customizations.Add(new UserBuilder());\n        fixture.Customizations.Add(new OrganizationBuilder());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/AutoFixture/OrganizationLicenseCustomization.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.Billing.AutoFixture;\n\npublic class OrganizationLicenseCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new OrganizationLicenseCustomization();\n}\npublic class OrganizationLicenseCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<OrganizationLicense>(composer => composer\n            .With(o => o.Signature, Guid.NewGuid().ToString().Replace('-', '+')));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs",
    "content": "﻿using System.Globalization;\nusing Bit.Core.Billing.Extensions;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Extensions;\n\npublic class InvoiceExtensionsTests\n{\n    private static Invoice CreateInvoiceWithLines(params InvoiceLineItem[] lineItems)\n    {\n        return new Invoice\n        {\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = lineItems?.ToList() ?? new List<InvoiceLineItem>()\n            }\n        };\n    }\n\n    #region FormatForProvider Tests\n\n    [Fact]\n    public void FormatForProvider_NullLines_ReturnsEmptyList()\n    {\n        // Arrange\n        var invoice = new Invoice\n        {\n            Lines = null\n        };\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public void FormatForProvider_EmptyLines_ReturnsEmptyList()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines();\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public void FormatForProvider_NullLineItem_SkipsNullLine()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(null);\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public void FormatForProvider_LineWithNullDescription_SkipsLine()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem { Description = null, Quantity = 1, Amount = 1000 }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public void FormatForProvider_ProviderPortalTeams_FormatsCorrectly()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Provider Portal - Teams (at $6.00 / month)\",\n                Quantity = 5,\n                Amount = 3000\n            }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"5 × Manage service provider (at $6.00 / month)\", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_ProviderPortalEnterprise_FormatsCorrectly()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Provider Portal - Enterprise (at $4.00 / month)\",\n                Quantity = 10,\n                Amount = 4000\n            }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"10 × Manage service provider (at $4.00 / month)\", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_ProviderPortalWithoutPriceInfo_FormatsWithoutPrice()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Provider Portal - Teams\",\n                Quantity = 3,\n                Amount = 1800\n            }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"3 × Manage service provider \", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_BusinessUnitPortalEnterprise_FormatsCorrectly()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Business Unit Portal - Enterprise (at $5.00 / month)\",\n                Quantity = 8,\n                Amount = 4000\n            }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"8 × Manage service provider (at $5.00 / month)\", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_BusinessUnitPortalGeneric_FormatsCorrectly()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Business Unit Portal (at $3.00 / month)\",\n                Quantity = 2,\n                Amount = 600\n            }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"2 × Manage service provider (at $3.00 / month)\", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_TaxLineWithPriceInfo_FormatsCorrectly()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Tax (at $2.00 / month)\",\n                Quantity = 1,\n                Amount = 200\n            }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"1 × Tax (at $2.00 / month)\", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_TaxLineWithoutPriceInfo_CalculatesPrice()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Tax\",\n                Quantity = 2,\n                Amount = 400 // $4.00 total, $2.00 per item\n            }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"2 × Tax (at $2.00 / month)\", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_TaxLineWithZeroQuantity_DoesNotCalculatePrice()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Tax\",\n                Quantity = 0,\n                Amount = 200\n            }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"0 × Tax \", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_OtherLineItem_ReturnsAsIs()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Some other service\",\n                Quantity = 1,\n                Amount = 1000\n            }\n        );\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"Some other service\", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_InvoiceLevelTax_AddsToResult()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Provider Portal - Teams\",\n                Quantity = 1,\n                Amount = 600\n            }\n        );\n\n        invoice.TotalTaxes = [new InvoiceTotalTax { Amount = 120 }]; // $1.20 in cents\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Equal(2, result.Count);\n        Assert.Equal(\"1 × Manage service provider \", result[0]);\n        Assert.Equal(\"1 × Tax (at $1.20 / month)\", result[1]);\n    }\n\n    [Fact]\n    public void FormatForProvider_NoInvoiceLevelTax_DoesNotAddTax()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Provider Portal - Teams\",\n                Quantity = 1,\n                Amount = 600\n            }\n        );\n        invoice.TotalTaxes = [];\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"1 × Manage service provider \", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_ZeroInvoiceLevelTax_DoesNotAddTax()\n    {\n        // Arrange\n        var invoice = CreateInvoiceWithLines(\n            new InvoiceLineItem\n            {\n                Description = \"Provider Portal - Teams\",\n                Quantity = 1,\n                Amount = 600\n            }\n        );\n        invoice.TotalTaxes = [new InvoiceTotalTax { Amount = 0 }];\n        var subscription = new Subscription();\n\n        // Act\n        var result = invoice.FormatForProvider(subscription);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(\"1 × Manage service provider \", result[0]);\n    }\n\n    [Fact]\n    public void FormatForProvider_ComplexScenario_HandlesAllLineTypes()\n    {\n        // Set culture to en-US to ensure consistent decimal formatting in tests\n        // This ensures tests pass on all machines regardless of system locale\n        var originalCulture = Thread.CurrentThread.CurrentCulture;\n        var originalUICulture = Thread.CurrentThread.CurrentUICulture;\n        try\n        {\n            Thread.CurrentThread.CurrentCulture = new CultureInfo(\"en-US\");\n            Thread.CurrentThread.CurrentUICulture = new CultureInfo(\"en-US\");\n\n            // Arrange\n            var lineItems = new StripeList<InvoiceLineItem>();\n            lineItems.Data = new List<InvoiceLineItem>\n        {\n            new InvoiceLineItem\n            {\n                Description = \"Provider Portal - Teams (at $6.00 / month)\", Quantity = 5, Amount = 3000\n            },\n            new InvoiceLineItem\n            {\n                Description = \"Provider Portal - Enterprise (at $4.00 / month)\", Quantity = 10, Amount = 4000\n            },\n            new InvoiceLineItem { Description = \"Tax\", Quantity = 1, Amount = 800 },\n            new InvoiceLineItem { Description = \"Custom Service\", Quantity = 2, Amount = 2000 }\n        };\n\n            var invoice = new Invoice\n            {\n                Lines = lineItems,\n                TotalTaxes = [new InvoiceTotalTax { Amount = 200 }] // Additional $2.00 tax\n            };\n            var subscription = new Subscription();\n\n            // Act\n            var result = invoice.FormatForProvider(subscription);\n\n            // Assert\n            Assert.Equal(5, result.Count);\n            Assert.Equal(\"5 × Manage service provider (at $6.00 / month)\", result[0]);\n            Assert.Equal(\"10 × Manage service provider (at $4.00 / month)\", result[1]);\n            Assert.Equal(\"1 × Tax (at $8.00 / month)\", result[2]);\n            Assert.Equal(\"Custom Service\", result[3]);\n            Assert.Equal(\"1 × Tax (at $2.00 / month)\", result[4]);\n        }\n        finally\n        {\n            Thread.CurrentThread.CurrentCulture = originalCulture;\n            Thread.CurrentThread.CurrentUICulture = originalUICulture;\n        }\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Extensions/StripeExtensions.cs",
    "content": "﻿using Bit.Core.Billing.Payment.Models;\nusing Stripe;\n\nnamespace Bit.Core.Test.Billing.Extensions;\n\npublic static class StripeExtensions\n{\n    public static bool HasExpansions(this BaseOptions options, params string[] expansions)\n        => expansions.All(expansion => options.Expand.Contains(expansion));\n\n    public static bool Matches(this AddressOptions address, BillingAddress billingAddress) =>\n        address.Country == billingAddress.Country &&\n        address.PostalCode == billingAddress.PostalCode &&\n        address.Line1 == billingAddress.Line1 &&\n        address.Line2 == billingAddress.Line2 &&\n        address.City == billingAddress.City &&\n        address.State == billingAddress.State;\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Licenses/LicenseConstantsTests.cs",
    "content": "﻿using System.Reflection;\nusing Bit.Core.Billing.Licenses;\nusing Bit.Core.Billing.Organizations.Models;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Licenses;\n\npublic class LicenseConstantsTests\n{\n    [Fact]\n    public void OrganizationLicenseConstants_HasConstantForEveryLicenseProperty()\n    {\n        // This test ensures that when a new property is added to OrganizationLicense,\n        // a corresponding constant is added to OrganizationLicenseConstants.\n        // This is the first step in the license synchronization pipeline:\n        // Property → Constant → Claim → Extraction → Application\n\n        // 1. Get all public properties from OrganizationLicense\n        var licenseProperties = typeof(OrganizationLicense)\n            .GetProperties(BindingFlags.Public | BindingFlags.Instance)\n            .Select(p => p.Name)\n            .ToHashSet();\n\n        // 2. Get all constants from OrganizationLicenseConstants\n        var constants = typeof(OrganizationLicenseConstants)\n            .GetFields(BindingFlags.Public | BindingFlags.Static)\n            .Where(f => f.IsLiteral && !f.IsInitOnly)\n            .Select(f => f.GetValue(null) as string)\n            .ToHashSet();\n\n        // 3. Define properties that don't need constants (internal/computed/non-claims properties)\n        var excludedProperties = new HashSet<string>\n        {\n            \"SignatureBytes\",             // Computed from Signature property\n            \"ValidLicenseVersion\",        // Internal property, not serialized\n            \"CurrentLicenseFileVersion\",  // Constant field, not an instance property\n            \"Hash\",                       // Signature-related, not in claims system\n            \"Signature\",                  // Signature-related, not in claims system\n            \"Token\",                      // The JWT itself, not a claim within the token\n            \"Version\"                     // Not in claims system (only in deprecated property-based licenses)\n        };\n\n        // 4. Find license properties without corresponding constants\n        var propertiesWithoutConstants = licenseProperties\n            .Except(constants)\n            .Except(excludedProperties)\n            .OrderBy(p => p)\n            .ToList();\n\n        // 5. Build error message with guidance\n        var errorMessage = \"\";\n        if (propertiesWithoutConstants.Any())\n        {\n            errorMessage = $\"The following OrganizationLicense properties don't have constants in OrganizationLicenseConstants:\\n\";\n            errorMessage += string.Join(\"\\n\", propertiesWithoutConstants.Select(p => $\"  - {p}\"));\n            errorMessage += \"\\n\\nPlease add the following constants to OrganizationLicenseConstants:\\n\";\n            foreach (var prop in propertiesWithoutConstants)\n            {\n                errorMessage += $\"  public const string {prop} = nameof({prop});\\n\";\n            }\n        }\n\n        // 6. Assert - if this fails, the error message guides the developer to add the constant\n        Assert.True(\n            !propertiesWithoutConstants.Any(),\n            $\"\\n{errorMessage}\");\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactoryTests.cs",
    "content": "﻿using System.Reflection;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Licenses;\nusing Bit.Core.Billing.Licenses.Models;\nusing Bit.Core.Billing.Licenses.Services.Implementations;\nusing Bit.Core.Models.Business;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Licenses.Services.Implementations;\n\npublic class OrganizationLicenseClaimsFactoryTests\n{\n    [Theory, BitAutoData]\n    public async Task GenerateClaims_CreatesClaimsForAllConstants(Organization organization)\n    {\n        // This test ensures that when a constant is added to OrganizationLicenseConstants,\n        // it is also added to the OrganizationLicenseClaimsFactory to generate claims.\n        // This is the second step in the license synchronization pipeline:\n        // Property → Constant → Claim → Extraction → Application\n\n        // 1. Populate all nullable properties to ensure claims can be generated\n        // The factory only adds claims for properties that have values\n        organization.Name = \"Test Organization\";\n        organization.BillingEmail = \"billing@test.com\";\n        organization.BusinessName = \"Test Business\";\n        organization.Plan = \"Enterprise\";\n        organization.LicenseKey = \"test-license-key\";\n        organization.Seats = 100;\n        organization.MaxCollections = 50;\n        organization.MaxStorageGb = 10;\n        organization.SmSeats = 25;\n        organization.SmServiceAccounts = 10;\n        organization.ExpirationDate = DateTime.UtcNow.AddYears(1); // Ensure org is not expired\n\n        // Create a LicenseContext with a minimal SubscriptionInfo to trigger conditional claims\n        // ExpirationWithoutGracePeriod is only generated for active, non-trial, annual subscriptions\n        var licenseContext = new LicenseContext\n        {\n            InstallationId = Guid.NewGuid(),\n            SubscriptionInfo = new SubscriptionInfo\n            {\n                Subscription = new SubscriptionInfo.BillingSubscription(null!)\n                {\n                    TrialEndDate = DateTime.UtcNow.AddDays(-30), // Trial ended in the past\n                    PeriodStartDate = DateTime.UtcNow,\n                    PeriodEndDate = DateTime.UtcNow.AddDays(365), // Annual subscription (>180 days)\n                    Status = \"active\"\n                }\n            }\n        };\n\n        // 2. Generate claims\n        var factory = new OrganizationLicenseClaimsFactory();\n        var claims = await factory.GenerateClaims(organization, licenseContext);\n\n        // 3. Get all constants from OrganizationLicenseConstants\n        var allConstants = typeof(OrganizationLicenseConstants)\n            .GetFields(BindingFlags.Public | BindingFlags.Static)\n            .Where(f => f.IsLiteral && !f.IsInitOnly)\n            .Select(f => f.GetValue(null) as string)\n            .ToHashSet();\n\n        // 4. Get claim types from generated claims\n        var generatedClaimTypes = claims.Select(c => c.Type).ToHashSet();\n\n        // 5. Find constants that don't have corresponding claims\n        var constantsWithoutClaims = allConstants\n            .Except(generatedClaimTypes)\n            .OrderBy(c => c)\n            .ToList();\n\n        // 6. Build error message with guidance\n        var errorMessage = \"\";\n        if (constantsWithoutClaims.Any())\n        {\n            errorMessage = $\"The following constants in OrganizationLicenseConstants are NOT generated as claims in OrganizationLicenseClaimsFactory:\\n\";\n            errorMessage += string.Join(\"\\n\", constantsWithoutClaims.Select(c => $\"  - {c}\"));\n            errorMessage += \"\\n\\nPlease add the following claims to OrganizationLicenseClaimsFactory.GenerateClaims():\\n\";\n            foreach (var constant in constantsWithoutClaims)\n            {\n                errorMessage += $\"  new(nameof(OrganizationLicenseConstants.{constant}), entity.{constant}.ToString()),\\n\";\n            }\n            errorMessage += \"\\nNote: If the property is nullable, you may need to add it conditionally.\";\n        }\n\n        // 7. Assert - if this fails, the error message guides the developer to add claim generation\n        Assert.True(\n            !constantsWithoutClaims.Any(),\n            $\"\\n{errorMessage}\");\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/MockPlans.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.Test.Billing.Mocks.Plans;\n\nnamespace Bit.Core.Test.Billing.Mocks;\n\npublic class MockPlans\n{\n    public static List<Plan> Plans =>\n    [\n        new CustomPlan(),\n        new Enterprise2019Plan(false),\n        new Enterprise2019Plan(true),\n        new Enterprise2020Plan(false),\n        new Enterprise2020Plan(true),\n        new Enterprise2023Plan(false),\n        new Enterprise2023Plan(true),\n        new EnterprisePlan(false),\n        new EnterprisePlan(true),\n        new Families2019Plan(),\n        new Families2025Plan(),\n        new FamiliesPlan(),\n        new FreePlan(),\n        new Teams2019Plan(false),\n        new Teams2019Plan(true),\n        new Teams2020Plan(false),\n        new Teams2020Plan(true),\n        new Teams2023Plan(false),\n        new Teams2023Plan(true),\n        new TeamsPlan(false),\n        new TeamsPlan(true),\n        new TeamsStarterPlan(),\n        new TeamsStarterPlan2023()\n    ];\n\n    public static Plan Get(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType)!;\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/CustomPlan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record CustomPlan : Plan\n{\n    public CustomPlan()\n    {\n        Type = PlanType.Custom;\n        PasswordManager = new CustomPasswordManagerFeatures();\n    }\n\n    private record CustomPasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public CustomPasswordManagerFeatures()\n        {\n            AllowSeatAutoscale = true;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/Enterprise2019Plan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record Enterprise2019Plan : Plan\n{\n    public Enterprise2019Plan(bool isAnnual)\n    {\n        Type = isAnnual ? PlanType.EnterpriseAnnually2019 : PlanType.EnterpriseMonthly2019;\n        ProductTier = ProductTierType.Enterprise;\n        Name = isAnnual ? \"Enterprise (Annually) 2019\" : \"Enterprise (Monthly) 2019\";\n        IsAnnual = isAnnual;\n        NameLocalizationKey = \"planNameEnterprise\";\n        DescriptionLocalizationKey = \"planDescEnterprise\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasPolicies = true;\n        HasSelfHost = true;\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        HasSso = true;\n        HasOrganizationDomains = true;\n        HasKeyConnector = true;\n        HasScim = true;\n        HasResetPassword = true;\n        UsersGetPremium = true;\n        HasCustomPermissions = true;\n        HasMyItems = true;\n\n        UpgradeSortOrder = 4;\n        DisplaySortOrder = 4;\n        LegacyYear = 2020;\n\n        SecretsManager = new Enterprise2019SecretsManagerFeatures(isAnnual);\n        PasswordManager = new Enterprise2019PasswordManagerFeatures(isAnnual);\n    }\n\n    private record Enterprise2019SecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public Enterprise2019SecretsManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 200;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeSeatPlanId = \"secrets-manager-enterprise-seat-annually\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-annually\";\n                SeatPrice = 144;\n                AdditionalPricePerServiceAccount = 6;\n            }\n            else\n            {\n                StripeSeatPlanId = \"secrets-manager-enterprise-seat-monthly\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-monthly\";\n                SeatPrice = 13;\n                AdditionalPricePerServiceAccount = 0.5M;\n            }\n        }\n    }\n\n    private record Enterprise2019PasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public Enterprise2019PasswordManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BaseStorageGb = 1;\n\n            HasAdditionalStorageOption = true;\n            HasAdditionalSeatsOption = true;\n\n            AllowSeatAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeStoragePlanId = \"storage-gb-annually\";\n                StripeSeatPlanId = \"enterprise-org-seat-annually\";\n                SeatPrice = 36;\n                AdditionalStoragePricePerGb = 4;\n            }\n            else\n            {\n                StripeSeatPlanId = \"enterprise-org-seat-monthly\";\n                StripeStoragePlanId = \"storage-gb-monthly\";\n                SeatPrice = 4M;\n                AdditionalStoragePricePerGb = 0.5M;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/Enterprise2020Plan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record Enterprise2020Plan : Plan\n{\n    public Enterprise2020Plan(bool isAnnual)\n    {\n        Type = isAnnual ? PlanType.EnterpriseAnnually2020 : PlanType.EnterpriseMonthly2020;\n        ProductTier = ProductTierType.Enterprise;\n        Name = isAnnual ? \"Enterprise (Annually) 2020\" : \"Enterprise (Monthly) 2020\";\n        IsAnnual = isAnnual;\n        NameLocalizationKey = \"planNameEnterprise\";\n        DescriptionLocalizationKey = \"planDescEnterprise\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasPolicies = true;\n        HasSelfHost = true;\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        HasSso = true;\n        HasOrganizationDomains = true;\n        HasKeyConnector = true;\n        HasScim = true;\n        HasResetPassword = true;\n        UsersGetPremium = true;\n        HasCustomPermissions = true;\n        HasMyItems = true;\n\n        UpgradeSortOrder = 4;\n        DisplaySortOrder = 4;\n        LegacyYear = 2023;\n\n        PasswordManager = new Enterprise2020PasswordManagerFeatures(isAnnual);\n        SecretsManager = new Enterprise2020SecretsManagerFeatures(isAnnual);\n    }\n\n    private record Enterprise2020SecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public Enterprise2020SecretsManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 200;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeSeatPlanId = \"secrets-manager-enterprise-seat-annually\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-annually\";\n                SeatPrice = 144;\n                AdditionalPricePerServiceAccount = 6;\n            }\n            else\n            {\n                StripeSeatPlanId = \"secrets-manager-enterprise-seat-monthly\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-monthly\";\n                SeatPrice = 13;\n                AdditionalPricePerServiceAccount = 0.5M;\n            }\n        }\n    }\n\n    private record Enterprise2020PasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public Enterprise2020PasswordManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BaseStorageGb = 1;\n\n            HasAdditionalStorageOption = true;\n            HasAdditionalSeatsOption = true;\n\n            AllowSeatAutoscale = true;\n\n            if (isAnnual)\n            {\n                AdditionalStoragePricePerGb = 4;\n                StripeStoragePlanId = \"storage-gb-annually\";\n                StripeSeatPlanId = \"2020-enterprise-org-seat-annually\";\n                SeatPrice = 60;\n            }\n            else\n            {\n                StripeSeatPlanId = \"2020-enterprise-seat-monthly\";\n                StripeStoragePlanId = \"storage-gb-monthly\";\n                SeatPrice = 6;\n                AdditionalStoragePricePerGb = 0.5M;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/EnterprisePlan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record EnterprisePlan : Plan\n{\n    public EnterprisePlan(bool isAnnual)\n    {\n        Type = isAnnual ? PlanType.EnterpriseAnnually : PlanType.EnterpriseMonthly;\n        ProductTier = ProductTierType.Enterprise;\n        Name = isAnnual ? \"Enterprise (Annually)\" : \"Enterprise (Monthly)\";\n        IsAnnual = isAnnual;\n        NameLocalizationKey = \"planNameEnterprise\";\n        DescriptionLocalizationKey = \"planDescEnterprise\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasPolicies = true;\n        HasSelfHost = true;\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        HasSso = true;\n        HasOrganizationDomains = true;\n        HasKeyConnector = true;\n        HasScim = true;\n        HasResetPassword = true;\n        UsersGetPremium = true;\n        HasCustomPermissions = true;\n        HasMyItems = true;\n\n        UpgradeSortOrder = 4;\n        DisplaySortOrder = 4;\n\n        PasswordManager = new EnterprisePasswordManagerFeatures(isAnnual);\n        SecretsManager = new EnterpriseSecretsManagerFeatures(isAnnual);\n    }\n\n    private record EnterpriseSecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public EnterpriseSecretsManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 50;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeSeatPlanId = \"secrets-manager-enterprise-seat-annually\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-2024-annually\";\n                SeatPrice = 144;\n                AdditionalPricePerServiceAccount = 12;\n            }\n            else\n            {\n                StripeSeatPlanId = \"secrets-manager-enterprise-seat-monthly\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-2024-monthly\";\n                SeatPrice = 13;\n                AdditionalPricePerServiceAccount = 1;\n            }\n        }\n    }\n\n    private record EnterprisePasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public EnterprisePasswordManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BaseStorageGb = 1;\n\n            HasAdditionalStorageOption = true;\n            HasAdditionalSeatsOption = true;\n\n            AllowSeatAutoscale = true;\n\n            if (isAnnual)\n            {\n                AdditionalStoragePricePerGb = 4;\n                StripeStoragePlanId = \"storage-gb-annually\";\n                StripeSeatPlanId = \"2023-enterprise-org-seat-annually\";\n                StripeProviderPortalSeatPlanId = \"password-manager-provider-portal-enterprise-annually-2024\";\n                SeatPrice = 72;\n                ProviderPortalSeatPrice = 72;\n            }\n            else\n            {\n                StripeSeatPlanId = \"2023-enterprise-seat-monthly\";\n                StripeProviderPortalSeatPlanId = \"password-manager-provider-portal-enterprise-monthly-2024\";\n                StripeStoragePlanId = \"storage-gb-monthly\";\n                SeatPrice = 7;\n                ProviderPortalSeatPrice = 6;\n                AdditionalStoragePricePerGb = 0.5M;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/EnterprisePlan2023.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record Enterprise2023Plan : Plan\n{\n    public Enterprise2023Plan(bool isAnnual)\n    {\n        Type = isAnnual ? PlanType.EnterpriseAnnually2023 : PlanType.EnterpriseMonthly2023;\n        ProductTier = ProductTierType.Enterprise;\n        Name = isAnnual ? \"Enterprise (Annually)\" : \"Enterprise (Monthly)\";\n        IsAnnual = isAnnual;\n        NameLocalizationKey = \"planNameEnterprise\";\n        DescriptionLocalizationKey = \"planDescEnterprise\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasPolicies = true;\n        HasSelfHost = true;\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        HasSso = true;\n        HasOrganizationDomains = true;\n        HasKeyConnector = true;\n        HasScim = true;\n        HasResetPassword = true;\n        UsersGetPremium = true;\n        HasCustomPermissions = true;\n        HasMyItems = true;\n\n        UpgradeSortOrder = 4;\n        DisplaySortOrder = 4;\n\n        LegacyYear = 2024;\n\n        PasswordManager = new Enterprise2023PasswordManagerFeatures(isAnnual);\n        SecretsManager = new Enterprise2023SecretsManagerFeatures(isAnnual);\n    }\n\n    private record Enterprise2023SecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public Enterprise2023SecretsManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 200;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeSeatPlanId = \"secrets-manager-enterprise-seat-annually\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-annually\";\n                SeatPrice = 144;\n                AdditionalPricePerServiceAccount = 6;\n            }\n            else\n            {\n                StripeSeatPlanId = \"secrets-manager-enterprise-seat-monthly\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-monthly\";\n                SeatPrice = 13;\n                AdditionalPricePerServiceAccount = 0.5M;\n            }\n        }\n    }\n\n    private record Enterprise2023PasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public Enterprise2023PasswordManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BaseStorageGb = 1;\n\n            HasAdditionalStorageOption = true;\n            HasAdditionalSeatsOption = true;\n\n            AllowSeatAutoscale = true;\n\n            if (isAnnual)\n            {\n                AdditionalStoragePricePerGb = 4;\n                StripeStoragePlanId = \"storage-gb-annually\";\n                StripeSeatPlanId = \"2023-enterprise-org-seat-annually\";\n                SeatPrice = 72;\n            }\n            else\n            {\n                StripeSeatPlanId = \"2023-enterprise-seat-monthly\";\n                StripeStoragePlanId = \"storage-gb-monthly\";\n                SeatPrice = 7;\n                AdditionalStoragePricePerGb = 0.5M;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/Families2019Plan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record Families2019Plan : Plan\n{\n    public Families2019Plan()\n    {\n        Type = PlanType.FamiliesAnnually2019;\n        ProductTier = ProductTierType.Families;\n        Name = \"Families 2019\";\n        IsAnnual = true;\n        NameLocalizationKey = \"planNameFamilies\";\n        DescriptionLocalizationKey = \"planDescFamilies\";\n\n        TrialPeriodDays = 7;\n\n        HasSelfHost = true;\n        HasTotp = true;\n\n        UpgradeSortOrder = 1;\n        DisplaySortOrder = 1;\n        LegacyYear = 2020;\n\n        PasswordManager = new Families2019PasswordManagerFeatures();\n    }\n\n    private record Families2019PasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public Families2019PasswordManagerFeatures()\n        {\n            BaseSeats = 5;\n            BaseStorageGb = 1;\n            MaxSeats = 5;\n\n            HasAdditionalStorageOption = true;\n            HasPremiumAccessOption = true;\n\n            StripePlanId = \"personal-org-annually\";\n            StripeStoragePlanId = \"personal-storage-gb-annually\";\n            StripePremiumAccessPlanId = \"personal-org-premium-access-annually\";\n            BasePrice = 12;\n            AdditionalStoragePricePerGb = 4;\n            PremiumAccessOptionPrice = 40;\n\n            AllowSeatAutoscale = false;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/Families2025Plan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record Families2025Plan : Plan\n{\n    public Families2025Plan()\n    {\n        Type = PlanType.FamiliesAnnually2025;\n        ProductTier = ProductTierType.Families;\n        Name = \"Families 2025\";\n        IsAnnual = true;\n        NameLocalizationKey = \"planNameFamilies\";\n        DescriptionLocalizationKey = \"planDescFamilies\";\n\n        TrialPeriodDays = 7;\n\n        HasSelfHost = true;\n        HasTotp = true;\n        UsersGetPremium = true;\n\n        UpgradeSortOrder = 1;\n        DisplaySortOrder = 1;\n\n        PasswordManager = new Families2025PasswordManagerFeatures();\n    }\n\n    private record Families2025PasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public Families2025PasswordManagerFeatures()\n        {\n            BaseSeats = 6;\n            BaseStorageGb = 1;\n            MaxSeats = 6;\n\n            HasAdditionalStorageOption = true;\n\n            StripePlanId = \"2020-families-org-annually\";\n            StripeStoragePlanId = \"personal-storage-gb-annually\";\n            BasePrice = 40;\n            AdditionalStoragePricePerGb = 4;\n\n            AllowSeatAutoscale = false;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/FamiliesPlan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record FamiliesPlan : Plan\n{\n    public FamiliesPlan()\n    {\n        Type = PlanType.FamiliesAnnually;\n        ProductTier = ProductTierType.Families;\n        Name = \"Families\";\n        IsAnnual = true;\n        NameLocalizationKey = \"planNameFamilies\";\n        DescriptionLocalizationKey = \"planDescFamilies\";\n\n        TrialPeriodDays = 7;\n\n        HasSelfHost = true;\n        HasTotp = true;\n        UsersGetPremium = true;\n\n        UpgradeSortOrder = 1;\n        DisplaySortOrder = 1;\n\n        PasswordManager = new FamiliesPasswordManagerFeatures();\n    }\n\n    private record FamiliesPasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public FamiliesPasswordManagerFeatures()\n        {\n            BaseSeats = 6;\n            BaseStorageGb = 1;\n            MaxSeats = 6;\n\n            HasAdditionalStorageOption = true;\n\n            StripePlanId = \"2020-families-org-annually\";\n            StripeStoragePlanId = \"personal-storage-gb-annually\";\n            BasePrice = 40;\n            AdditionalStoragePricePerGb = 4;\n\n            AllowSeatAutoscale = false;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/FreePlan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record FreePlan : Plan\n{\n    public FreePlan()\n    {\n        Type = PlanType.Free;\n        ProductTier = ProductTierType.Free;\n        Name = \"Free\";\n        NameLocalizationKey = \"planNameFree\";\n        DescriptionLocalizationKey = \"planDescFree\";\n\n        UpgradeSortOrder = -1; // Always the lowest plan, cannot be upgraded to\n        DisplaySortOrder = -1;\n\n        PasswordManager = new FreePasswordManagerFeatures();\n        SecretsManager = new FreeSecretsManagerFeatures();\n    }\n\n    private record FreeSecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public FreeSecretsManagerFeatures()\n        {\n            BaseSeats = 2;\n            BaseServiceAccount = 3;\n            MaxProjects = 3;\n            MaxSeats = 2;\n            MaxServiceAccounts = 3;\n\n            AllowSeatAutoscale = false;\n        }\n    }\n\n    private record FreePasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public FreePasswordManagerFeatures()\n        {\n            BaseSeats = 2;\n            MaxCollections = 2;\n            MaxSeats = 2;\n\n            AllowSeatAutoscale = false;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/Teams2019Plan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record Teams2019Plan : Plan\n{\n    public Teams2019Plan(bool isAnnual)\n    {\n        Type = isAnnual ? PlanType.TeamsAnnually2019 : PlanType.TeamsMonthly2019;\n        ProductTier = ProductTierType.Teams;\n        Name = isAnnual ? \"Teams (Annually) 2019\" : \"Teams (Monthly) 2019\";\n        IsAnnual = isAnnual;\n        NameLocalizationKey = \"planNameTeams\";\n        DescriptionLocalizationKey = \"planDescTeams\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        UsersGetPremium = true;\n\n        UpgradeSortOrder = 3;\n        DisplaySortOrder = 3;\n        LegacyYear = 2020;\n\n        SecretsManager = new Teams2019SecretsManagerFeatures(isAnnual);\n        PasswordManager = new Teams2019PasswordManagerFeatures(isAnnual);\n    }\n\n    private record Teams2019SecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public Teams2019SecretsManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 50;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeSeatPlanId = \"secrets-manager-teams-seat-annually\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-annually\";\n                SeatPrice = 72;\n                AdditionalPricePerServiceAccount = 6;\n            }\n            else\n            {\n                StripeSeatPlanId = \"secrets-manager-teams-seat-monthly\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-monthly\";\n                SeatPrice = 7;\n                AdditionalPricePerServiceAccount = 0.5M;\n            }\n        }\n    }\n\n    private record Teams2019PasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public Teams2019PasswordManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 5;\n            BaseStorageGb = 1;\n\n            HasAdditionalStorageOption = true;\n            HasAdditionalSeatsOption = true;\n\n            AllowSeatAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripePlanId = \"teams-org-annually\";\n                StripeStoragePlanId = \"storage-gb-annually\";\n                StripeSeatPlanId = \"teams-org-seat-annually\";\n                SeatPrice = 24;\n                BasePrice = 60;\n                AdditionalStoragePricePerGb = 4;\n            }\n            else\n            {\n                StripePlanId = \"teams-org-monthly\";\n                StripeSeatPlanId = \"teams-org-seat-monthly\";\n                StripeStoragePlanId = \"storage-gb-monthly\";\n                BasePrice = 8;\n                SeatPrice = 2.5M;\n                AdditionalStoragePricePerGb = 0.5M;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/Teams2020Plan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record Teams2020Plan : Plan\n{\n    public Teams2020Plan(bool isAnnual)\n    {\n        Type = isAnnual ? PlanType.TeamsAnnually2020 : PlanType.TeamsMonthly2020;\n        ProductTier = ProductTierType.Teams;\n        Name = isAnnual ? \"Teams (Annually) 2020\" : \"Teams (Monthly) 2020\";\n        IsAnnual = isAnnual;\n        NameLocalizationKey = \"planNameTeams\";\n        DescriptionLocalizationKey = \"planDescTeams\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        UsersGetPremium = true;\n\n        UpgradeSortOrder = 3;\n        DisplaySortOrder = 3;\n        LegacyYear = 2023;\n\n        PasswordManager = new Teams2020PasswordManagerFeatures(isAnnual);\n        SecretsManager = new Teams2020SecretsManagerFeatures(isAnnual);\n    }\n\n    private record Teams2020SecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public Teams2020SecretsManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 50;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeSeatPlanId = \"secrets-manager-teams-seat-annually\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-annually\";\n                SeatPrice = 72;\n                AdditionalPricePerServiceAccount = 6;\n            }\n            else\n            {\n                StripeSeatPlanId = \"secrets-manager-teams-seat-monthly\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-monthly\";\n                SeatPrice = 7;\n                AdditionalPricePerServiceAccount = 0.5M;\n            }\n        }\n    }\n\n    private record Teams2020PasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public Teams2020PasswordManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BaseStorageGb = 1;\n            BasePrice = 0;\n\n            HasAdditionalStorageOption = true;\n            HasAdditionalSeatsOption = true;\n\n            AllowSeatAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeStoragePlanId = \"storage-gb-annually\";\n                StripeSeatPlanId = \"2020-teams-org-seat-annually\";\n                SeatPrice = 36;\n                AdditionalStoragePricePerGb = 4;\n            }\n            else\n            {\n                StripeSeatPlanId = \"2020-teams-org-seat-monthly\";\n                StripeStoragePlanId = \"storage-gb-monthly\";\n                SeatPrice = 4;\n                AdditionalStoragePricePerGb = 0.5M;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/TeamsPlan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record TeamsPlan : Plan\n{\n    public TeamsPlan(bool isAnnual)\n    {\n        Type = isAnnual ? PlanType.TeamsAnnually : PlanType.TeamsMonthly;\n        ProductTier = ProductTierType.Teams;\n        Name = isAnnual ? \"Teams (Annually)\" : \"Teams (Monthly)\";\n        IsAnnual = isAnnual;\n        NameLocalizationKey = \"planNameTeams\";\n        DescriptionLocalizationKey = \"planDescTeams\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        UsersGetPremium = true;\n        HasScim = true;\n\n        UpgradeSortOrder = 3;\n        DisplaySortOrder = 3;\n\n        PasswordManager = new TeamsPasswordManagerFeatures(isAnnual);\n        SecretsManager = new TeamsSecretsManagerFeatures(isAnnual);\n    }\n\n    private record TeamsSecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public TeamsSecretsManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 20;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeSeatPlanId = \"secrets-manager-teams-seat-annually\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-2024-annually\";\n                SeatPrice = 72;\n                AdditionalPricePerServiceAccount = 12;\n            }\n            else\n            {\n                StripeSeatPlanId = \"secrets-manager-teams-seat-monthly\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-2024-monthly\";\n                SeatPrice = 7;\n                AdditionalPricePerServiceAccount = 1;\n            }\n        }\n    }\n\n    private record TeamsPasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public TeamsPasswordManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BaseStorageGb = 1;\n            BasePrice = 0;\n\n            HasAdditionalStorageOption = true;\n            HasAdditionalSeatsOption = true;\n\n            AllowSeatAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeStoragePlanId = \"storage-gb-annually\";\n                StripeSeatPlanId = \"2023-teams-org-seat-annually\";\n                SeatPrice = 48;\n                AdditionalStoragePricePerGb = 4;\n            }\n            else\n            {\n                StripeSeatPlanId = \"2023-teams-org-seat-monthly\";\n                StripeProviderPortalSeatPlanId = \"password-manager-provider-portal-teams-monthly-2024\";\n                StripeStoragePlanId = \"storage-gb-monthly\";\n                SeatPrice = 5;\n                ProviderPortalSeatPrice = 4;\n                AdditionalStoragePricePerGb = 0.5M;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/TeamsPlan2023.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record Teams2023Plan : Plan\n{\n    public Teams2023Plan(bool isAnnual)\n    {\n        Type = isAnnual ? PlanType.TeamsAnnually2023 : PlanType.TeamsMonthly2023;\n        ProductTier = ProductTierType.Teams;\n        Name = isAnnual ? \"Teams (Annually)\" : \"Teams (Monthly)\";\n        IsAnnual = isAnnual;\n        NameLocalizationKey = \"planNameTeams\";\n        DescriptionLocalizationKey = \"planDescTeams\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        UsersGetPremium = true;\n\n        UpgradeSortOrder = 3;\n        DisplaySortOrder = 3;\n\n        LegacyYear = 2024;\n\n        PasswordManager = new Teams2023PasswordManagerFeatures(isAnnual);\n        SecretsManager = new Teams2023SecretsManagerFeatures(isAnnual);\n    }\n\n    private record Teams2023SecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public Teams2023SecretsManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 50;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeSeatPlanId = \"secrets-manager-teams-seat-annually\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-annually\";\n                SeatPrice = 72;\n                AdditionalPricePerServiceAccount = 6;\n            }\n            else\n            {\n                StripeSeatPlanId = \"secrets-manager-teams-seat-monthly\";\n                StripeServiceAccountPlanId = \"secrets-manager-service-account-monthly\";\n                SeatPrice = 7;\n                AdditionalPricePerServiceAccount = 0.5M;\n            }\n        }\n    }\n\n    private record Teams2023PasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public Teams2023PasswordManagerFeatures(bool isAnnual)\n        {\n            BaseSeats = 0;\n            BaseStorageGb = 1;\n            BasePrice = 0;\n\n            HasAdditionalStorageOption = true;\n            HasAdditionalSeatsOption = true;\n\n            AllowSeatAutoscale = true;\n\n            if (isAnnual)\n            {\n                StripeStoragePlanId = \"storage-gb-annually\";\n                StripeSeatPlanId = \"2023-teams-org-seat-annually\";\n                SeatPrice = 48;\n                AdditionalStoragePricePerGb = 4;\n            }\n            else\n            {\n                StripeSeatPlanId = \"2023-teams-org-seat-monthly\";\n                StripeStoragePlanId = \"storage-gb-monthly\";\n                SeatPrice = 5;\n                AdditionalStoragePricePerGb = 0.5M;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record TeamsStarterPlan : Plan\n{\n    public TeamsStarterPlan()\n    {\n        Type = PlanType.TeamsStarter;\n        ProductTier = ProductTierType.TeamsStarter;\n        Name = \"Teams (Starter)\";\n        NameLocalizationKey = \"planNameTeamsStarter\";\n        DescriptionLocalizationKey = \"planDescTeams\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        UsersGetPremium = true;\n\n        UpgradeSortOrder = 2;\n        DisplaySortOrder = 2;\n\n        PasswordManager = new TeamsStarterPasswordManagerFeatures();\n        SecretsManager = new TeamsStarterSecretsManagerFeatures();\n\n        LegacyYear = 2024;\n    }\n\n    private record TeamsStarterSecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public TeamsStarterSecretsManagerFeatures()\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 20;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            StripeSeatPlanId = \"secrets-manager-teams-seat-monthly\";\n            StripeServiceAccountPlanId = \"secrets-manager-service-account-2024-monthly\";\n            SeatPrice = 7;\n            AdditionalPricePerServiceAccount = 1;\n        }\n    }\n\n    private record TeamsStarterPasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public TeamsStarterPasswordManagerFeatures()\n        {\n            BaseSeats = 10;\n            BaseStorageGb = 1;\n            BasePrice = 20;\n\n            MaxSeats = 10;\n\n            HasAdditionalStorageOption = true;\n\n            StripePlanId = \"teams-org-starter\";\n            StripeStoragePlanId = \"storage-gb-monthly\";\n            AdditionalStoragePricePerGb = 0.5M;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Mocks/Plans/TeamsStarterPlan2023.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.StaticStore;\n\nnamespace Bit.Core.Test.Billing.Mocks.Plans;\n\npublic record TeamsStarterPlan2023 : Plan\n{\n    public TeamsStarterPlan2023()\n    {\n        Type = PlanType.TeamsStarter2023;\n        ProductTier = ProductTierType.TeamsStarter;\n        Name = \"Teams (Starter)\";\n        NameLocalizationKey = \"planNameTeamsStarter\";\n        DescriptionLocalizationKey = \"planDescTeams\";\n        CanBeUsedByBusiness = true;\n\n        TrialPeriodDays = 7;\n\n        HasGroups = true;\n        HasDirectory = true;\n        HasEvents = true;\n        HasTotp = true;\n        Has2fa = true;\n        HasApi = true;\n        UsersGetPremium = true;\n\n        UpgradeSortOrder = 2;\n        DisplaySortOrder = 2;\n\n        PasswordManager = new TeamsStarter2023PasswordManagerFeatures();\n        SecretsManager = new TeamsStarter2023SecretsManagerFeatures();\n        LegacyYear = 2024;\n    }\n\n    private record TeamsStarter2023SecretsManagerFeatures : SecretsManagerPlanFeatures\n    {\n        public TeamsStarter2023SecretsManagerFeatures()\n        {\n            BaseSeats = 0;\n            BasePrice = 0;\n            BaseServiceAccount = 50;\n\n            HasAdditionalSeatsOption = true;\n            HasAdditionalServiceAccountOption = true;\n\n            AllowSeatAutoscale = true;\n            AllowServiceAccountsAutoscale = true;\n\n            StripeSeatPlanId = \"secrets-manager-teams-seat-monthly\";\n            StripeServiceAccountPlanId = \"secrets-manager-service-account-monthly\";\n            SeatPrice = 7;\n            AdditionalPricePerServiceAccount = 0.5M;\n        }\n    }\n\n    private record TeamsStarter2023PasswordManagerFeatures : PasswordManagerPlanFeatures\n    {\n        public TeamsStarter2023PasswordManagerFeatures()\n        {\n            BaseSeats = 10;\n            BaseStorageGb = 1;\n            BasePrice = 20;\n\n            MaxSeats = 10;\n\n            HasAdditionalStorageOption = true;\n\n            StripePlanId = \"teams-org-starter\";\n            StripeStoragePlanId = \"storage-gb-monthly\";\n            AdditionalStoragePricePerGb = 0.5M;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Models/BillingInfo.cs",
    "content": "﻿using Bit.Core.Billing.Models;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Models;\n\npublic class BillingInfoTests\n{\n    [Fact]\n    public void BillingInvoice_Amount_ShouldComeFrom_InvoiceTotal()\n    {\n        var invoice = new Stripe.Invoice\n        {\n            AmountDue = 1000,\n            Total = 2000,\n        };\n\n        var billingInvoice = new BillingHistoryInfo.BillingInvoice(invoice);\n\n        // Should have been set from Total\n        Assert.Equal(20M, billingInvoice.Amount);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Models/Business/OrganizationLicenseFileFixtures.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Enums;\n\nnamespace Bit.Core.Test.Billing.Models.Business;\n\n/// <summary>\n/// Contains test data for OrganizationLicense tests, including json strings for each OrganizationLicense version.\n/// If you increment the OrganizationLicense version (e.g. because you've added a property to it), you must add the\n/// json string for your new version to the LicenseVersions dictionary in this class.\n/// See OrganizationLicenseTests.GenerateLicenseFileJsonString to help you do this.\n/// </summary>\npublic static class OrganizationLicenseFileFixtures\n{\n    public const string InstallationId = \"78900000-0000-0000-0000-000000000123\";\n\n    private const string Version12 =\n        \"{\\n  'LicenseKey': 'myLicenseKey',\\n  'InstallationId': '78900000-0000-0000-0000-000000000123',\\n  'Id': '12300000-0000-0000-0000-000000000456',\\n  'Name': 'myOrg',\\n  'BillingEmail': 'myBillingEmail',\\n  'BusinessName': 'myBusinessName',\\n  'Enabled': true,\\n  'Plan': 'myPlan',\\n  'PlanType': 11,\\n  'Seats': 10,\\n  'MaxCollections': 2,\\n  'UsePolicies': true,\\n  'UseSso': true,\\n  'UseKeyConnector': true,\\n  'UseScim': true,\\n  'UseGroups': true,\\n  'UseEvents': true,\\n  'UseDirectory': true,\\n  'UseTotp': true,\\n  'Use2fa': true,\\n  'UseApi': true,\\n  'UseResetPassword': true,\\n  'MaxStorageGb': 100,\\n  'SelfHost': true,\\n  'UsersGetPremium': true,\\n  'UseCustomPermissions': true,\\n  'Version': 11,\\n  'Issued': '2023-11-23T03:15:41.632267Z',\\n  'Refresh': '2023-11-30T03:15:41.632267Z',\\n  'Expires': '2023-11-30T03:15:41.632267Z',\\n  'ExpirationWithoutGracePeriod': null,\\n  'Trial': true,\\n  'LicenseType': 1,\\n  'Hash': 'eMSljdMAlFiiVYP/DI8LwNtSZZy6cJaC\\\\u002BAdmYGd1RTs=',\\n  'Signature': ''\\n}\";\n\n    private const string Version13 =\n        \"{\\n  'LicenseKey': 'myLicenseKey',\\n  'InstallationId': '78900000-0000-0000-0000-000000000123',\\n  'Id': '12300000-0000-0000-0000-000000000456',\\n  'Name': 'myOrg',\\n  'BillingEmail': 'myBillingEmail',\\n  'BusinessName': 'myBusinessName',\\n  'Enabled': true,\\n  'Plan': 'myPlan',\\n  'PlanType': 11,\\n  'Seats': 10,\\n  'MaxCollections': 2,\\n  'UsePolicies': true,\\n  'UseSso': true,\\n  'UseKeyConnector': true,\\n  'UseScim': true,\\n  'UseGroups': true,\\n  'UseEvents': true,\\n  'UseDirectory': true,\\n  'UseTotp': true,\\n  'Use2fa': true,\\n  'UseApi': true,\\n  'UseResetPassword': true,\\n  'MaxStorageGb': 100,\\n  'SelfHost': true,\\n  'UsersGetPremium': true,\\n  'UseCustomPermissions': true,\\n  'Version': 12,\\n  'Issued': '2023-11-23T03:25:24.265409Z',\\n  'Refresh': '2023-11-30T03:25:24.265409Z',\\n  'Expires': '2023-11-30T03:25:24.265409Z',\\n  'ExpirationWithoutGracePeriod': null,\\n  'UsePasswordManager': true,\\n  'UseSecretsManager': true,\\n  'SmSeats': 5,\\n  'SmServiceAccounts': 8,\\n  'Trial': true,\\n  'LicenseType': 1,\\n  'Hash': 'hZ4WcSX/7ooRZ6asDRMJ/t0K5hZkQdvkgEyy6wY\\\\u002BwQk=',\\n  'Signature': ''\\n}\";\n\n    private const string Version14 =\n        \"{\\n  'LicenseKey': 'myLicenseKey',\\n  'InstallationId': '78900000-0000-0000-0000-000000000123',\\n  'Id': '12300000-0000-0000-0000-000000000456',\\n  'Name': 'myOrg',\\n  'BillingEmail': 'myBillingEmail',\\n  'BusinessName': 'myBusinessName',\\n  'Enabled': true,\\n  'Plan': 'myPlan',\\n  'PlanType': 11,\\n  'Seats': 10,\\n  'MaxCollections': 2,\\n  'UsePolicies': true,\\n  'UseSso': true,\\n  'UseKeyConnector': true,\\n  'UseScim': true,\\n  'UseGroups': true,\\n  'UseEvents': true,\\n  'UseDirectory': true,\\n  'UseTotp': true,\\n  'Use2fa': true,\\n  'UseApi': true,\\n  'UseResetPassword': true,\\n  'MaxStorageGb': 100,\\n  'SelfHost': true,\\n  'UsersGetPremium': true,\\n  'UseCustomPermissions': true,\\n  'Version': 13,\\n  'Issued': '2023-11-29T22:42:33.970597Z',\\n  'Refresh': '2023-12-06T22:42:33.970597Z',\\n  'Expires': '2023-12-06T22:42:33.970597Z',\\n  'ExpirationWithoutGracePeriod': null,\\n  'UsePasswordManager': true,\\n  'UseSecretsManager': true,\\n  'SmSeats': 5,\\n  'SmServiceAccounts': 8,\\n  'LimitCollectionCreationDeletion': true,\\n  'Trial': true,\\n  'LicenseType': 1,\\n  'Hash': '4G2u\\\\u002BWKO9EOiVnDVNr7uPxxRkv7TtaOmDl7kAYH05Fw=',\\n  'Signature': ''\\n}\";\n\n    private const string Version15 =\n        \"{\\n  'LicenseKey': 'myLicenseKey',\\n  'InstallationId': '78900000-0000-0000-0000-000000000123',\\n  'Id': '12300000-0000-0000-0000-000000000456',\\n  'Name': 'myOrg',\\n  'BillingEmail': 'myBillingEmail',\\n  'BusinessName': 'myBusinessName',\\n  'Enabled': true,\\n  'Plan': 'myPlan',\\n  'PlanType': 11,\\n  'Seats': 10,\\n  'MaxCollections': 2,\\n  'UsePolicies': true,\\n  'UseSso': true,\\n  'UseKeyConnector': true,\\n  'UseScim': true,\\n  'UseGroups': true,\\n  'UseEvents': true,\\n  'UseDirectory': true,\\n  'UseTotp': true,\\n  'Use2fa': true,\\n  'UseApi': true,\\n  'UseResetPassword': true,\\n  'MaxStorageGb': 100,\\n  'SelfHost': true,\\n  'UsersGetPremium': true,\\n  'UseCustomPermissions': true,\\n  'Version': 14,\\n  'Issued': '2023-12-14T02:03:33.374297Z',\\n  'Refresh': '2023-12-07T22:42:33.970597Z',\\n  'Expires': '2023-12-21T02:03:33.374297Z',\\n  'ExpirationWithoutGracePeriod': null,\\n  'UsePasswordManager': true,\\n  'UseSecretsManager': true,\\n  'SmSeats': 5,\\n  'SmServiceAccounts': 8,\\n  'LimitCollectionCreationDeletion': true,\\n  'AllowAdminAccessToAllCollectionItems': true,\\n  'Trial': true,\\n  'LicenseType': 1,\\n  'Hash': 'EZl4IvJaa1E5mPmlfp4p5twAtlmaxlF1yoZzVYP4vog=',\\n  'Signature': ''\\n}\";\n\n    private const string Version16 =\n        \"{\\n'LicenseKey': 'myLicenseKey',\\n'InstallationId': '78900000-0000-0000-0000-000000000123',\\n'Id': '12300000-0000-0000-0000-000000000456',\\n'Name': 'myOrg',\\n'BillingEmail': 'myBillingEmail',\\n'BusinessName': 'myBusinessName',\\n'Enabled': true,\\n'Plan': 'myPlan',\\n'PlanType': 11,\\n'Seats': 10,\\n'MaxCollections': 2,\\n'UsePolicies': true,\\n'UseSso': true,\\n'UseKeyConnector': true,\\n'UseScim': true,\\n'UseGroups': true,\\n'UseEvents': true,\\n'UseDirectory': true,\\n'UseTotp': true,\\n'Use2fa': true,\\n'UseApi': true,\\n'UseResetPassword': true,\\n'MaxStorageGb': 100,\\n'SelfHost': true,\\n'UsersGetPremium': true,\\n'UseCustomPermissions': true,\\n'Version': 15,\\n'Issued': '2025-05-16T20:50:09.036931Z',\\n'Refresh': '2025-05-23T20:50:09.036931Z',\\n'Expires': '2025-05-23T20:50:09.036931Z',\\n'ExpirationWithoutGracePeriod': null,\\n'UsePasswordManager': true,\\n'UseSecretsManager': true,\\n'SmSeats': 5,\\n'SmServiceAccounts': 8,\\n'UseRiskInsights': false,\\n'LimitCollectionCreationDeletion': true,\\n'AllowAdminAccessToAllCollectionItems': true,\\n'Trial': true,\\n'LicenseType': 1,\\n'UseOrganizationDomains': true,\\n'UseAdminSponsoredFamilies': false,\\n'Hash': 'k3M9SpHKUo0TmuSnNipeZleCHxgcEycKRXYl9BAg30Q=',\\n'Signature': '',\\n'Token': null\\n}\";\n\n    private static readonly Dictionary<int, string> LicenseVersions = new() { { 12, Version12 }, { 13, Version13 }, { 14, Version14 }, { 15, Version15 }, { 16, Version16 } };\n\n    public static OrganizationLicense GetVersion(int licenseVersion)\n    {\n        if (!LicenseVersions.ContainsKey(licenseVersion))\n        {\n            throw new Exception(\n                    $\"Cannot find serialized license version {licenseVersion}. You must add this to OrganizationLicenseFileFixtures when adding a new license version.\");\n        }\n\n        var json = LicenseVersions.GetValueOrDefault(licenseVersion).Replace(\"'\", \"\\\"\");\n        var license = JsonSerializer.Deserialize<OrganizationLicense>(json);\n\n        if (license.Version != licenseVersion - 1)\n        {\n            // license.Version is 1 behind. e.g. if we requested version 13, then license.Version == 12. If not,\n            // the json string is probably for a different version and won't give us accurate test results.\n            throw new Exception(\n                $\"License version {licenseVersion} in OrganizationLicenseFileFixtures did not match the expected version number. Make sure the json string is correct.\");\n        }\n\n        return license;\n    }\n\n    /// <summary>\n    /// The organization used to generate the license file json strings in this class.\n    /// All its properties should be initialized with literal, non-default values.\n    /// If you add an Organization property value, please add a value here as well.\n    /// </summary>\n    public static Organization OrganizationFactory() =>\n        new()\n        {\n            Id = new Guid(\"12300000-0000-0000-0000-000000000456\"),\n            Identifier = \"myIdentifier\",\n            Name = \"myOrg\",\n            BusinessName = \"myBusinessName\",\n            BusinessAddress1 = \"myBusinessAddress1\",\n            BusinessAddress2 = \"myBusinessAddress2\",\n            BusinessAddress3 = \"myBusinessAddress3\",\n            BusinessCountry = \"myBusinessCountry\",\n            BusinessTaxNumber = \"myBusinessTaxNumber\",\n            BillingEmail = \"myBillingEmail\",\n            Plan = \"myPlan\",\n            PlanType = PlanType.EnterpriseAnnually2020,\n            Seats = 10,\n            MaxCollections = 2,\n            UsePolicies = true,\n            UseSso = true,\n            UseOrganizationDomains = true,\n            UseKeyConnector = true,\n            UseScim = true,\n            UseGroups = true,\n            UseDirectory = true,\n            UseEvents = true,\n            UseTotp = true,\n            Use2fa = true,\n            UseApi = true,\n            UseResetPassword = true,\n            UseSecretsManager = true,\n            SelfHost = true,\n            UsersGetPremium = true,\n            UseCustomPermissions = true,\n            Storage = 100000,\n            MaxStorageGb = 100,\n            Gateway = GatewayType.Stripe,\n            GatewayCustomerId = \"myGatewayCustomerId\",\n            GatewaySubscriptionId = \"myGatewaySubscriptionId\",\n            ReferenceData = \"myReferenceData\",\n            Enabled = true,\n            LicenseKey = \"myLicenseKey\",\n            PublicKey = \"myPublicKey\",\n            PrivateKey = \"myPrivateKey\",\n            TwoFactorProviders = \"myTwoFactorProviders\",\n            ExpirationDate = new DateTime(2024, 12, 24),\n            CreationDate = new DateTime(2022, 10, 22),\n            RevisionDate = new DateTime(2023, 11, 23),\n            MaxAutoscaleSeats = 100,\n            OwnersNotifiedOfAutoscaling = new DateTime(2020, 5, 10),\n            Status = OrganizationStatusType.Created,\n            UsePasswordManager = true,\n            SmSeats = 5,\n            SmServiceAccounts = 8,\n            MaxAutoscaleSmSeats = 101,\n            MaxAutoscaleSmServiceAccounts = 102,\n            LimitCollectionCreation = true,\n            LimitCollectionDeletion = true,\n            AllowAdminAccessToAllCollectionItems = true,\n        };\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Models.Business;\n\npublic class OrganizationLicenseTests\n{\n    /// <summary>\n    /// Verifies that when the license file is loaded from disk using the current OrganizationLicense class,\n    /// it matches the Organization it was generated for.\n    /// This guards against the risk that properties added in later versions are accidentally included in the validation\n    /// </summary>\n    [Theory]\n    [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion)] // Previous version (this property is 1 behind)\n    [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion + 1)] // Current version\n    public void OrganizationLicense_LoadedFromDisk_VerifyData_Passes(int licenseVersion, ClaimsPrincipal claimsPrincipal)\n    {\n        var license = OrganizationLicenseFileFixtures.GetVersion(licenseVersion);\n\n        // These licenses will naturally expire over time, but we still want them to be able to test\n        license.Expires = DateTime.MaxValue;\n\n        var organization = OrganizationLicenseFileFixtures.OrganizationFactory();\n        var globalSettings = Substitute.For<IGlobalSettings>();\n        globalSettings.Installation.Returns(new GlobalSettings.InstallationSettings\n        {\n            Id = new Guid(OrganizationLicenseFileFixtures.InstallationId)\n        });\n        Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings));\n    }\n\n    /// <summary>\n    /// Known good GetDataBytes output for hash data (forHash: true) for all OrganizationLicense versions.\n    /// These values were verified to be correct on initial implementation and serve as regression baselines.\n    /// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version.\n    /// </summary>\n    private static readonly Dictionary<int, string> _knownGoodOrganizationLicenseHashData = new()\n    {\n        { 1, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UseTotp:true|Version:1\" },\n        { 2, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:2\" },\n        { 3, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:3\" },\n        { 4, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:4\" },\n        { 5, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:5\" },\n        { 6, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseTotp:true|Version:6\" },\n        { 7, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:7\" },\n        { 8, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:8\" },\n        { 9, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:9\" },\n        { 10, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:10\" },\n        { 11, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:11\" },\n        { 12, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:12\" },\n        { 13, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:13\" },\n        { 14, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:14\" },\n        { 15, \"license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:15\" },\n        { 16, \"license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:16\" }\n    };\n\n    /// <summary>\n    /// Known good GetDataBytes output for signature data (forHash: false) for all OrganizationLicense versions.\n    /// These values were verified to be correct on initial implementation and serve as regression baselines.\n    /// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version.\n    /// </summary>\n    private static readonly Dictionary<int, string> _knownGoodOrganizationLicenseSignatureData = new()\n    {\n        { 1, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:WSyM/Q+vgOuWeF6XBH+RSfUqvf7NDtP3fgNfcbXYqKc=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UseTotp:true|Version:1\" },\n        { 2, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:n4g3leUf/egbnKk+/VgkJTvdxw2YRH6/zGgx89h+J60=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:2\" },\n        { 3, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:zDoMNV/c8YpUypc+FmBoPyj73qOsg4snsMOJDcKFp9k=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:3\" },\n        { 4, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:Y2sP9phSZ9GqbCC+PMp1KdnUhjfNaqNg6uzfUydrKZM=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:4\" },\n        { 5, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:PudZKNV7YAWJogm8BJf3wZIL+lESf3qzV/pQlZPPJjY=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsersGetPremium:true|UseTotp:true|Version:5\" },\n        { 6, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:7SjSYQENeAW4pUnXtsPaux2uipIWNWJz9VIrNW2gVsI=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseTotp:true|Version:6\" },\n        { 7, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:ujf4/zlDXv1g6ktlk9XBj/u3BkRZG+p5I00piGDiWp8=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:7\" },\n        { 8, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:GEM3AyWbQknnlDtoxyhw0QK7edYS2C/bffX5+p4G9ig=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:8\" },\n        { 9, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:5SF14wtEieiA9hjj+BTcrggHcx7dLEGbH+HLksvK79o=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseSso:true|UseTotp:true|Version:9\" },\n        { 10, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:NmbIpfiZUNxSvwbaolbUmItQCHIcVCTjfraR/NBlmvE=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:10\" },\n        { 11, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|Expires:1740787200|Hash:dGZBQT/PORsuT/W2oRrngcjTboTyfZZVpDZBHshVK6Y=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:11\" },\n        { 12, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:rWecCXB0kuqi/RW3C8u2rLZRDMR49W3W4Q3eL2tZ3j8=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSso:true|UseTotp:true|Version:12\" },\n        { 13, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:15fwM5v5Ba+t7JlD4ToYvtZmAoShWC3DrOD0lM5kXGE=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:13\" },\n        { 14, \"license:organization|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:2bTNBiH2G/Nzv6UVD1BNJQBGjT9et0UO8ComQofS8uo=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:14\" },\n        { 15, \"license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:3VjOyWJu38N4epIzhDzjRR80zQ651wnYkQCd+DIzeAs=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:15\" },\n        { 16, \"license:organization|AllowAdminAccessToAllCollectionItems:true|BillingEmail:myBillingEmail|BusinessName:|Enabled:true|ExpirationWithoutGracePeriod:|Expires:1740787200|Hash:Oo5KFBoX8pMcklJ4oJAqgv77/WA8+gDPxq6+/Fjffwc=|Id:12300000-0000-0000-0000-000000000456|InstallationId:78900000-0000-0000-0000-000000000123|Issued:1758888001|LicenseKey:myLicenseKey|LimitCollectionCreationDeletion:true|MaxCollections:2|MaxStorageGb:100|Name:myOrg|Plan:myPlan|PlanType:11|Refresh:1761480001|Seats:10|SelfHost:true|SmSeats:5|SmServiceAccounts:8|Trial:false|Use2fa:true|UseApi:true|UseCustomPermissions:true|UseDirectory:true|UseEvents:true|UseGroups:true|UseKeyConnector:true|UsePasswordManager:true|UsePolicies:true|UseResetPassword:true|UsersGetPremium:true|UseScim:true|UseSecretsManager:true|UseSso:true|UseTotp:true|Version:16\" }\n    };\n\n    /// <summary>\n    /// Regression test that verifies GetDataBytes output for hash data (forHash: true) remains stable across all OrganizationLicense versions.\n    /// This protects against accidental changes to the data format that would break backward compatibility.\n    /// If this test fails, it means the hash data format has changed and existing licenses may no longer validate.\n    /// </summary>\n    [Fact]\n    public void OrganizationLicense_GetDataBytes_HashData_AllVersions()\n    {\n        // Verify each version produces the expected hash data format\n        for (var version = 1; version <= 16; version++)\n        {\n            var license = CreateDeterministicOrganizationLicense(version);\n            var actualHashData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: true));\n            Assert.Equal(_knownGoodOrganizationLicenseHashData[version], actualHashData);\n        }\n    }\n\n    /// <summary>\n    /// Regression test that verifies GetDataBytes output for signature data (forHash: false) remains stable across all OrganizationLicense versions.\n    /// This protects against accidental changes to the data format that would break backward compatibility.\n    /// If this test fails, it means the signature data format has changed and existing licenses may no longer validate.\n    /// </summary>\n    [Fact]\n    public void OrganizationLicense_GetDataBytes_SignatureData_AllVersions()\n    {\n        // Verify each version produces the expected signature data format\n        for (var version = 1; version <= 16; version++)\n        {\n            var license = CreateDeterministicOrganizationLicense(version);\n            var actualSignatureData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: false));\n            Assert.Equal(_knownGoodOrganizationLicenseSignatureData[version], actualSignatureData);\n        }\n    }\n\n    /// <summary>\n    /// Validates that the OrganizationLicense version remains frozen at version 15.\n    /// License versions should no longer be incremented. Use the JWT Token property to add new claims instead.\n    /// If this test fails, it means someone attempted to increment the license version, which is no longer allowed.\n    /// </summary>\n    [Fact]\n    public void OrganizationLicense_CurrentVersion_ShouldRemainFrozen()\n    {\n        const int expectedVersion = 15;\n        var actualVersion = OrganizationLicense.CurrentLicenseFileVersion;\n\n        Assert.True(actualVersion == expectedVersion, $@\"\nERROR: OrganizationLicense.CurrentLicenseFileVersion has been changed from {expectedVersion} to {actualVersion}\n\nLicense versions are now frozen and should not be incremented.\n\nInstead of incrementing the version:\n- Use the JWT Token property to add new claims\n- Add your new capabilities as claims in the Token\n- This allows for more flexible licensing without breaking backward compatibility\n\nIf you believe you need to change the version for a valid reason, please discuss with the team first.\n\");\n    }\n\n    /// <summary>\n    /// Creates a deterministic OrganizationLicense for testing hash values.\n    /// All property values are fixed to ensure reproducible hashes.\n    /// </summary>\n    private static OrganizationLicense CreateDeterministicOrganizationLicense(int version)\n    {\n        var organization = CreateDeterministicOrganization();\n        var subscriptionInfo = CreateDeterministicSubscriptionInfo();\n        var installationId = new Guid(\"78900000-0000-0000-0000-000000000123\");\n        var mockLicensingService = CreateMockLicensingService();\n\n        var license = new OrganizationLicense(organization, subscriptionInfo, installationId, mockLicensingService, version);\n\n        // Override timestamps to deterministic values (constructor sets them to DateTime.UtcNow)\n        license.Issued = new DateTime(2025, 9, 26, 12, 0, 1, DateTimeKind.Utc); // Corresponds to 1759501361 Unix timestamp\n        license.Refresh = new DateTime(2025, 10, 26, 12, 0, 1, DateTimeKind.Utc); // Corresponds to 1762093361 Unix timestamp\n\n        // Recalculate hash with the deterministic Issued/Refresh values\n        license.Hash = Convert.ToBase64String(license.ComputeHash());\n        license.Signature = Convert.ToBase64String(mockLicensingService.SignLicense(license));\n\n        return license;\n    }\n\n    /// <summary>\n    /// Creates an Organization with deterministic property values for reproducible testing.\n    /// </summary>\n    private static Organization CreateDeterministicOrganization()\n    {\n        return new Organization\n        {\n            Id = new Guid(\"12300000-0000-0000-0000-000000000456\"),\n            Identifier = \"myIdentifier\",\n            Name = \"myOrg\",\n            BillingEmail = \"myBillingEmail\",\n            Plan = \"myPlan\",\n            PlanType = PlanType.EnterpriseAnnually2020,\n            Seats = 10,\n            MaxCollections = 2,\n            UsePolicies = true,\n            UseSso = true,\n            UseKeyConnector = true,\n            UseScim = true,\n            UseGroups = true,\n            UseEvents = true,\n            UseDirectory = true,\n            UseTotp = true,\n            Use2fa = true,\n            UseApi = true,\n            UseResetPassword = true,\n            MaxStorageGb = 100,\n            SelfHost = true,\n            UsersGetPremium = true,\n            UseCustomPermissions = true,\n            Enabled = true,\n            LicenseKey = \"myLicenseKey\",\n            UsePasswordManager = true,\n            UseSecretsManager = true,\n            SmSeats = 5,\n            SmServiceAccounts = 8,\n            UseRiskInsights = false,\n            LimitCollectionCreation = true,\n            LimitCollectionDeletion = true,\n            AllowAdminAccessToAllCollectionItems = true,\n            UseOrganizationDomains = true,\n            UseAdminSponsoredFamilies = false,\n            UseDisableSmAdsForUsers = false,\n            UsePhishingBlocker = false,\n            UseMyItems = false,\n        };\n    }\n\n    /// <summary>\n    /// Creates a SubscriptionInfo with deterministic dates for reproducible testing.\n    /// </summary>\n    private static SubscriptionInfo CreateDeterministicSubscriptionInfo()\n    {\n        var stripeSubscription = new Subscription\n        {\n            Status = \"active\",\n            TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),\n            TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc),\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),\n                        CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)\n                    }\n                ]\n            }\n        };\n\n        return new SubscriptionInfo\n        {\n            UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice\n            {\n                Date = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)\n            },\n            Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription)\n        };\n    }\n\n    /// <summary>\n    /// Creates a mock ILicensingService that returns a deterministic signature.\n    /// </summary>\n    private static ILicensingService CreateMockLicensingService()\n    {\n        var mockService = Substitute.For<ILicensingService>();\n        mockService.SignLicense(Arg.Any<ILicense>())\n            .Returns([0x00, 0x01, 0x02, 0x03]); // Dummy signature for hash testing\n        return mockService;\n    }\n\n    /// <summary>\n    /// Verifies that UseDisableSmAdsForUsers claim is properly generated in the license Token\n    /// and that VerifyData correctly validates the claim.\n    /// </summary>\n    [Theory]\n    [BitAutoData(true)]\n    [BitAutoData(false)]\n    public void OrganizationLicense_UseDisableSmAdsForUsers_ClaimGenerationAndValidation(bool useDisableSmAdsForUsers, ClaimsPrincipal claimsPrincipal)\n    {\n        // Arrange\n        var organization = CreateDeterministicOrganization();\n        organization.UseDisableSmAdsForUsers = useDisableSmAdsForUsers;\n\n        var subscriptionInfo = CreateDeterministicSubscriptionInfo();\n        var installationId = new Guid(\"78900000-0000-0000-0000-000000000123\");\n        var mockLicensingService = CreateMockLicensingService();\n\n        var license = new OrganizationLicense(organization, subscriptionInfo, installationId, mockLicensingService);\n        license.Expires = DateTime.MaxValue; // Prevent expiration during test\n\n        var globalSettings = Substitute.For<IGlobalSettings>();\n        globalSettings.Installation.Returns(new GlobalSettings.InstallationSettings\n        {\n            Id = installationId\n        });\n\n        // Act & Assert - Verify VerifyData passes with the UseDisableSmAdsForUsers value\n        Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Models/Business/UserLicenseTests.cs",
    "content": "﻿using Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Business;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Models.Business;\n\npublic class UserLicenseTests\n{\n    /// <summary>\n    /// Known good GetDataBytes output for hash data (forHash: true) for UserLicense version 1.\n    /// This value was verified to be correct on initial implementation and serves as a regression baseline.\n    /// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version.\n    /// </summary>\n    private const string _knownGoodUserLicenseHashData = \"license:user|Email:test@example.com|Expires:1736208000|Id:12300000-0000-0000-0000-000000000789|LicenseKey:myUserLicenseKey|MaxStorageGb:10|Name:Test User|Premium:true|Trial:false|Version:1\";\n\n    /// <summary>\n    /// Known good GetDataBytes output for signature data (forHash: false) for UserLicense version 1.\n    /// This value was verified to be correct on initial implementation and serves as a regression baseline.\n    /// NOTE: License versions are now frozen. Use the JWT Token property to add new claims instead of incrementing the version.\n    /// </summary>\n    private const string _knownGoodUserLicenseSignatureData = \"license:user|Email:test@example.com|Expires:1736208000|Hash:oZEopNmWvWQNE3Lnsh/LP2OPo6+IHxjTcpdIse/viQk=|Id:12300000-0000-0000-0000-000000000789|Issued:1758888041|LicenseKey:myUserLicenseKey|MaxStorageGb:10|Name:Test User|Premium:true|Refresh:1735603200|Trial:false|Version:1\";\n\n    /// <summary>\n    /// Regression test that verifies GetDataBytes output for hash data (forHash: true) remains stable for UserLicense version 1.\n    /// This protects against accidental changes to the data format that would break backward compatibility.\n    /// If this test fails, it means the hash data format has changed and existing licenses may no longer validate.\n    /// </summary>\n    [Fact]\n    public void UserLicense_GetDataBytes_HashData_Version1()\n    {\n        var license = CreateDeterministicUserLicense();\n        var actualHashData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: true));\n        Assert.Equal(_knownGoodUserLicenseHashData, actualHashData);\n    }\n\n    /// <summary>\n    /// Regression test that verifies GetDataBytes output for signature data (forHash: false) remains stable for UserLicense version 1.\n    /// This protects against accidental changes to the data format that would break backward compatibility.\n    /// If this test fails, it means the signature data format has changed and existing licenses may no longer validate.\n    /// </summary>\n    [Fact]\n    public void UserLicense_GetDataBytes_SignatureData_Version1()\n    {\n        var license = CreateDeterministicUserLicense();\n        var actualSignatureData = System.Text.Encoding.UTF8.GetString(license.GetDataBytes(forHash: false));\n        Assert.Equal(_knownGoodUserLicenseSignatureData, actualSignatureData);\n    }\n\n    /// <summary>\n    /// Validates that the UserLicense version remains frozen at version 1.\n    /// License versions should no longer be incremented. Use the JWT Token property to add new claims instead.\n    /// If this test fails, it means someone attempted to add version 2 support, which is no longer allowed.\n    /// </summary>\n    [Fact]\n    public void UserLicense_CurrentVersion_ShouldRemainFrozen()\n    {\n        const int expectedMaxVersion = 1;\n\n        var user = CreateDeterministicUser();\n        var subscriptionInfo = CreateDeterministicSubscriptionInfo();\n        var mockLicensingService = CreateMockLicensingService();\n\n        // Verify that version 2 is NOT supported (should throw NotSupportedException)\n        var exception = Assert.Throws<NotSupportedException>(() =>\n            new UserLicense(user, subscriptionInfo, mockLicensingService, version: 2));\n\n        // If the exception message changes or we don't get an exception, fail with helpful guidance\n        if (exception == null)\n        {\n            var errorMessage = $@\"\nERROR: UserLicense now supports version 2 or higher\n\nLicense versions are now frozen and should not be incremented.\n\nInstead of incrementing the version:\n- Use the JWT Token property to add new claims\n- Add your new capabilities as claims in the Token\n- This allows for more flexible licensing without breaking backward compatibility\n\nIf you believe you need to change the version for a valid reason, please discuss with the team first.\n\";\n            Assert.Fail(errorMessage);\n        }\n\n        // Verify we still support version 1\n        var license = new UserLicense(user, subscriptionInfo, mockLicensingService, version: expectedMaxVersion);\n        Assert.NotNull(license);\n    }\n\n    /// <summary>\n    /// Creates a deterministic UserLicense for testing hash values.\n    /// All property values are fixed to ensure reproducible hashes.\n    /// </summary>\n    private static UserLicense CreateDeterministicUserLicense()\n    {\n        var user = CreateDeterministicUser();\n        var subscriptionInfo = CreateDeterministicSubscriptionInfo();\n        var mockLicensingService = CreateMockLicensingService();\n\n        var license = new UserLicense(user, subscriptionInfo, mockLicensingService, version: 1);\n\n        // Override timestamps to deterministic values (constructor sets them to DateTime.UtcNow)\n        license.Issued = new DateTime(2025, 9, 26, 12, 0, 41, DateTimeKind.Utc); // Corresponds to 1759502041 Unix timestamp\n        license.Refresh = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc); // Corresponds to 1735603200 Unix timestamp\n\n        // Recalculate hash with the deterministic Issued/Refresh values\n        license.Hash = Convert.ToBase64String(license.ComputeHash());\n        license.Signature = Convert.ToBase64String(mockLicensingService.SignLicense(license));\n\n        return license;\n    }\n\n    /// <summary>\n    /// Creates a User with deterministic property values for reproducible testing.\n    /// </summary>\n    private static User CreateDeterministicUser()\n    {\n        return new User\n        {\n            Id = new Guid(\"12300000-0000-0000-0000-000000000789\"),\n            Name = \"Test User\",\n            Email = \"test@example.com\",\n            LicenseKey = \"myUserLicenseKey\",\n            Premium = true,\n            MaxStorageGb = 10,\n            PremiumExpirationDate = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)\n        };\n    }\n\n    /// <summary>\n    /// Creates a SubscriptionInfo with deterministic dates for reproducible testing.\n    /// </summary>\n    private static SubscriptionInfo CreateDeterministicSubscriptionInfo()\n    {\n        var stripeSubscription = new Subscription\n        {\n            Status = \"active\",\n            TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),\n            TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc),\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),\n                        CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)\n                    }\n                ]\n            }\n        };\n\n        return new SubscriptionInfo\n        {\n            UpcomingInvoice = new SubscriptionInfo.BillingUpcomingInvoice\n            {\n                Date = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc)\n            },\n            Subscription = new SubscriptionInfo.BillingSubscription(stripeSubscription)\n        };\n    }\n\n    /// <summary>\n    /// Creates a mock ILicensingService that returns a deterministic signature.\n    /// </summary>\n    private static ILicensingService CreateMockLicensingService()\n    {\n        var mockService = Substitute.For<ILicensingService>();\n        mockService.SignLicense(Arg.Any<ILicense>())\n            .Returns([0x00, 0x01, 0x02, 0x03]); // Dummy signature for hash testing\n        return mockService;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\nusing static Bit.Core.Billing.Constants.StripeConstants;\n\nnamespace Bit.Core.Test.Billing.Organizations.Commands;\n\npublic class PreviewOrganizationTaxCommandTests\n{\n    private readonly ILogger<PreviewOrganizationTaxCommand> _logger = Substitute.For<ILogger<PreviewOrganizationTaxCommand>>();\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly ISubscriptionDiscountService _subscriptionDiscountService = Substitute.For<ISubscriptionDiscountService>();\n    private readonly PreviewOrganizationTaxCommand _command;\n    private readonly User _user;\n\n    public PreviewOrganizationTaxCommandTests()\n    {\n        _user = new User { Id = Guid.NewGuid(), Email = \"test@example.com\" };\n        _command = new PreviewOrganizationTaxCommand(_logger, _pricingClient, _stripeAdapter, _subscriptionDiscountService);\n    }\n\n    #region Subscription Purchase\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_SponsoredPasswordManager_ReturnsCorrectTaxAmounts()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Families,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 6,\n                AdditionalStorage = 0,\n                Sponsored = true\n            }\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new FamiliesPlan();\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],\n            Total = 5500\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(5.00m, tax);\n        Assert.Equal(55.00m, total);\n\n        // Verify the correct Stripe API call for sponsored subscription\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2021-family-for-enterprise-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 1 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_StandaloneSecretsManager_ReturnsCorrectTaxAmounts()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            SecretsManager = new OrganizationSubscriptionPurchase.SecretsManagerSelections\n            {\n                Seats = 3,\n                AdditionalServiceAccounts = 0,\n                Standalone = true\n            }\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"CA\",\n            PostalCode = \"K1A 0A6\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 750 }],\n            Total = 8250\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(7.50m, tax);\n        Assert.Equal(82.50m, total);\n\n        // Verify the correct Stripe API call for standalone secrets manager\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"CA\" &&\n            options.CustomerDetails.Address.PostalCode == \"K1A 0A6\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"2023-teams-org-seat-monthly\" && item.Quantity == 5) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-teams-seat-monthly\" && item.Quantity == 3) &&\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == CouponIDs.SecretsManagerStandalone));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_StandardPurchaseWithStorage_ReturnsCorrectTaxAmounts()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Enterprise,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 10,\n                AdditionalStorage = 5,\n                Sponsored = false\n            },\n            SecretsManager = new OrganizationSubscriptionPurchase.SecretsManagerSelections\n            {\n                Seats = 8,\n                AdditionalServiceAccounts = 3,\n                Standalone = false\n            }\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"GB\",\n            PostalCode = \"SW1A 1AA\",\n            TaxId = new TaxID(\"gb_vat\", \"123456789\")\n        };\n\n        var plan = new EnterprisePlan(true);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 1200 }],\n            Total = 12200\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(12.00m, tax);\n        Assert.Equal(122.00m, total);\n\n        // Verify the correct Stripe API call for comprehensive purchase with storage and service accounts\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"GB\" &&\n            options.CustomerDetails.Address.PostalCode == \"SW1A 1AA\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.CustomerDetails.TaxIds.Count == 1 &&\n            options.CustomerDetails.TaxIds[0].Type == \"gb_vat\" &&\n            options.CustomerDetails.TaxIds[0].Value == \"123456789\" &&\n            options.SubscriptionDetails.Items.Count == 4 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"2023-enterprise-org-seat-annually\" && item.Quantity == 10) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"storage-gb-annually\" && item.Quantity == 5) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-enterprise-seat-annually\" && item.Quantity == 8) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-service-account-2024-annually\" && item.Quantity == 3) &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_FamiliesTier_NoSecretsManager_ReturnsCorrectTaxAmounts()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Families,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 6,\n                AdditionalStorage = 0,\n                Sponsored = false\n            }\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"90210\"\n        };\n\n        var plan = new FamiliesPlan();\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify the correct Stripe API call for Families tier (non-seat-based plan)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"90210\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2020-families-org-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 6 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_BusinessUseNonUSCountry_UsesTaxExemptReverse()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 3,\n                AdditionalStorage = 0,\n                Sponsored = false\n            }\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"DE\",\n            PostalCode = \"10115\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 0 }],\n            Total = 2700\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(0.00m, tax);\n        Assert.Equal(27.00m, total);\n\n        // Verify the correct Stripe API call for business use in non-US country (tax exempt reverse)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"DE\" &&\n            options.CustomerDetails.Address.PostalCode == \"10115\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 3 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_BusinessUseSwitzerland_UsesTaxExemptNone()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 3,\n                AdditionalStorage = 0,\n                Sponsored = false\n            }\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"CH\",\n            PostalCode = \"3001\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 220 }],\n            Total = 2920\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(2.20m, tax);\n        Assert.Equal(29.20m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"CH\" &&\n            options.CustomerDetails.Address.PostalCode == \"3001\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 3 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_SpanishNIFTaxId_AddsEUVATTaxId()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Enterprise,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 15,\n                AdditionalStorage = 0,\n                Sponsored = false\n            }\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"ES\",\n            PostalCode = \"28001\",\n            TaxId = new TaxID(TaxIdType.SpanishNIF, \"12345678Z\")\n        };\n\n        var plan = new EnterprisePlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 2100 }],\n            Total = 12100\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(21.00m, tax);\n        Assert.Equal(121.00m, total);\n\n        // Verify the correct Stripe API call for Spanish NIF that adds both Spanish NIF and EU VAT tax IDs\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"ES\" &&\n            options.CustomerDetails.Address.PostalCode == \"28001\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.CustomerDetails.TaxIds.Count == 2 &&\n            options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.SpanishNIF && t.Value == \"12345678Z\") &&\n            options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.EUVAT && t.Value == \"ES12345678Z\") &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-enterprise-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 15 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_TeamsWithCoupon_IgnoresCoupon()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [\"TEST_COUPON_20\"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify coupon is ignored for Teams plans (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_EnterpriseWithCoupon_IgnoresCoupon()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Enterprise,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 10,\n                AdditionalStorage = 5,\n                Sponsored = false\n            },\n            SecretsManager = new OrganizationSubscriptionPurchase.SecretsManagerSelections\n            {\n                Seats = 8,\n                AdditionalServiceAccounts = 2,\n                Standalone = false\n            },\n            Coupons = [\"ENTERPRISE_DISCOUNT_15\"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"CA\",\n            PostalCode = \"K1A 0A6\"\n        };\n\n        var plan = new EnterprisePlan(true);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 1200 }],\n            Total = 13200\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(12.00m, tax);\n        Assert.Equal(132.00m, total);\n\n        // Verify coupon is ignored for Enterprise plans (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"CA\" &&\n            options.CustomerDetails.Address.PostalCode == \"K1A 0A6\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.SubscriptionDetails.Items.Count == 4 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"2023-enterprise-org-seat-annually\" && item.Quantity == 10) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"storage-gb-annually\" && item.Quantity == 5) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-enterprise-seat-annually\" && item.Quantity == 8) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-service-account-2024-annually\" && item.Quantity == 2) &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_SponsoredPlanWithCoupon_IgnoresCoupon()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Families,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 6,\n                AdditionalStorage = 0,\n                Sponsored = true\n            },\n            Coupons = [\"TEST_COUPON_IGNORED\"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new FamiliesPlan();\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],\n            Total = 5500\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(5.00m, tax);\n        Assert.Equal(55.00m, total);\n\n        // Verify coupon is ignored for sponsored plans (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2021-family-for-enterprise-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 1 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_StandaloneSecretsManagerWithCoupon_UsesSystemCoupon()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            SecretsManager = new OrganizationSubscriptionPurchase.SecretsManagerSelections\n            {\n                Seats = 3,\n                AdditionalServiceAccounts = 0,\n                Standalone = true\n            },\n            Coupons = [\"USER_COUPON_IGNORED\"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"CA\",\n            PostalCode = \"K1A 0A6\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 750 }],\n            Total = 8250\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(7.50m, tax);\n        Assert.Equal(82.50m, total);\n\n        // Verify user coupon is ignored and system coupon (SecretsManagerStandalone) is used instead\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"CA\" &&\n            options.CustomerDetails.Address.PostalCode == \"K1A 0A6\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"2023-teams-org-seat-monthly\" && item.Quantity == 5) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-teams-seat-monthly\" && item.Quantity == 3) &&\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == CouponIDs.SecretsManagerStandalone));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_EmptyStringCoupon_TreatedAsNull()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = null\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify empty string coupon is treated same as null (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_NullCoupon_NoDiscountApplied()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            }\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify null coupon results in no discounts applied\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_WhitespaceOnlyCoupon_TreatedAsNull()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [\"   \"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        // Whitespace-only strings are now trimmed and treated as null/empty, so no discount is applied\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify whitespace-only coupon is treated as null (no discount applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_TeamsWithCouponWithWhitespace_IgnoresCoupon()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [\"  TEST_COUPON_20  \"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify coupon is ignored for Teams plans (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_TeamsWithLongCoupon_IgnoresCoupon()\n    {\n        // Very long coupon string (200 characters)\n        var longCoupon = new string('A', 200);\n\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [longCoupon]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify coupon is ignored for Teams plans (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_TeamsWithSpecialCharactersCoupon_IgnoresCoupon()\n    {\n        // Coupon with special characters (hyphens, underscores, numbers are common in Stripe coupon IDs)\n        var specialCoupon = \"TEST-COUPON_2024-50%OFF\";\n\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [specialCoupon]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify coupon is ignored for Teams plans (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionPurchase_TeamsWithUnicodeCoupon_IgnoresCoupon()\n    {\n        // Coupon with unicode characters (though unlikely for real Stripe coupons, tests edge case)\n        var unicodeCoupon = \"TEST-COUPON-2024\";\n\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [unicodeCoupon]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify coupon is ignored for Teams plans (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    #endregion\n\n    #region Subscription Plan Change\n\n    [Fact]\n    public async Task Run_OrganizationPlanChange_FreeOrganizationToTeams_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.Free,\n            UseSecretsManager = false\n        };\n\n        var planChange = new OrganizationSubscriptionPlanChange\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 120 }],\n            Total = 1320\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, planChange, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(1.20m, tax);\n        Assert.Equal(13.20m, total);\n\n        // Verify the correct Stripe API call for free organization upgrade to Teams\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 2 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationPlanChange_FreeOrganizationToFamilies_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.Free,\n            UseSecretsManager = true\n        };\n\n        var planChange = new OrganizationSubscriptionPlanChange\n        {\n            Tier = ProductTierType.Families,\n            Cadence = PlanCadenceType.Annually\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"CA\",\n            PostalCode = \"K1A 0A6\"\n        };\n\n        var plan = new FamiliesPlan();\n        _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 400 }],\n            Total = 4400\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, planChange, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(4.00m, tax);\n        Assert.Equal(44.00m, total);\n\n        // Verify the correct Stripe API call for free organization upgrade to Families (no SM for Families)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"CA\" &&\n            options.CustomerDetails.Address.PostalCode == \"K1A 0A6\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2020-families-org-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 1 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationPlanChange_FamiliesOrganizationToTeams_UsesOrganizationSeats()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.FamiliesAnnually,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\",\n            UseSecretsManager = false,\n            Seats = 6\n        };\n\n        var planChange = new OrganizationSubscriptionPlanChange\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Annually\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"10012\"\n        };\n\n        var currentPlan = new FamiliesPlan();\n        var newPlan = new TeamsPlan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(newPlan);\n\n        var subscriptionItems = new List<SubscriptionItem>\n        {\n            new() { Price = new Price { Id = \"2020-families-org-annually\" }, Quantity = 1 }\n        };\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Items = new StripeList<SubscriptionItem> { Data = subscriptionItems },\n            Customer = new Customer { Discount = null }\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax\n            {\n                Amount = 900\n            }\n            ],\n            Total = 9900\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, planChange, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(9.00m, tax);\n        Assert.Equal(99.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"10012\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 6 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationPlanChange_FamiliesOrganizationToEnterprise_UsesOrganizationSeats()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.FamiliesAnnually,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\",\n            UseSecretsManager = false,\n            Seats = 6\n        };\n\n        var planChange = new OrganizationSubscriptionPlanChange\n        {\n            Tier = ProductTierType.Enterprise,\n            Cadence = PlanCadenceType.Annually\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"10012\"\n        };\n\n        var currentPlan = new FamiliesPlan();\n        var newPlan = new EnterprisePlan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(newPlan);\n\n        var subscriptionItems = new List<SubscriptionItem>\n        {\n            new() { Price = new Price { Id = \"2020-families-org-annually\" }, Quantity = 1 }\n        };\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Items = new StripeList<SubscriptionItem> { Data = subscriptionItems },\n            Customer = new Customer { Discount = null }\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax\n            {\n                Amount = 1200\n            }\n            ],\n            Total = 13200\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, planChange, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(12.00m, tax);\n        Assert.Equal(132.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"10012\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-enterprise-org-seat-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 6 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationPlanChange_FreeOrganizationWithSecretsManagerToEnterprise_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.Free,\n            UseSecretsManager = true\n        };\n\n        var planChange = new OrganizationSubscriptionPlanChange\n        {\n            Tier = ProductTierType.Enterprise,\n            Cadence = PlanCadenceType.Annually\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"GB\",\n            PostalCode = \"SW1A 1AA\"\n        };\n\n        var plan = new EnterprisePlan(true);\n        _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],\n            Total = 8800\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, planChange, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(8.00m, tax);\n        Assert.Equal(88.00m, total);\n\n        // Verify the correct Stripe API call for free organization with SM to Enterprise\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"GB\" &&\n            options.CustomerDetails.Address.PostalCode == \"SW1A 1AA\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"2023-enterprise-org-seat-annually\" && item.Quantity == 2) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-enterprise-seat-annually\" && item.Quantity == 2) &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationPlanChange_ExistingSubscriptionUpgrade_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.TeamsMonthly,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\",\n            UseSecretsManager = true\n        };\n\n        var planChange = new OrganizationSubscriptionPlanChange\n        {\n            Tier = ProductTierType.Enterprise,\n            Cadence = PlanCadenceType.Annually\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"DE\",\n            PostalCode = \"10115\"\n        };\n\n        var currentPlan = new TeamsPlan(false);\n        var newPlan = new EnterprisePlan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(newPlan);\n\n        // Mock existing subscription with items - using NEW plan IDs since command looks for new plan prices\n        var subscriptionItems = new List<SubscriptionItem>\n        {\n            new() { Price = new Price { Id = \"2023-teams-org-seat-monthly\" }, Quantity = 8 },\n            new() { Price = new Price { Id = \"storage-gb-annually\" }, Quantity = 3 },\n            new() { Price = new Price { Id = \"secrets-manager-enterprise-seat-annually\" }, Quantity = 5 },\n            new() { Price = new Price { Id = \"secrets-manager-service-account-2024-annually\" }, Quantity = 10 }\n        };\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Items = new StripeList<SubscriptionItem> { Data = subscriptionItems },\n            Customer = new Customer { Discount = null }\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 1500 }],\n            Total = 16500\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, planChange, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(15.00m, tax);\n        Assert.Equal(165.00m, total);\n\n        // Verify the correct Stripe API call for existing subscription upgrade\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"DE\" &&\n            options.CustomerDetails.Address.PostalCode == \"10115\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.SubscriptionDetails.Items.Count == 4 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"2023-enterprise-org-seat-annually\" && item.Quantity == 8) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"storage-gb-annually\" && item.Quantity == 3) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-enterprise-seat-annually\" && item.Quantity == 5) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-service-account-2024-annually\" && item.Quantity == 10) &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationPlanChange_ExistingSubscriptionWithDiscount_PreservesCoupon()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.TeamsAnnually,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\",\n            UseSecretsManager = false\n        };\n\n        var planChange = new OrganizationSubscriptionPlanChange\n        {\n            Tier = ProductTierType.Enterprise,\n            Cadence = PlanCadenceType.Annually\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"90210\"\n        };\n\n        var currentPlan = new TeamsPlan(true);\n        var newPlan = new EnterprisePlan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(newPlan);\n\n        // Mock existing subscription with discount\n        var subscriptionItems = new List<SubscriptionItem>\n        {\n            new() { Price = new Price { Id = \"2023-teams-org-seat-annually\" }, Quantity = 5 }\n        };\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Items = new StripeList<SubscriptionItem> { Data = subscriptionItems },\n            Customer = new Customer\n            {\n                Discount = new Discount\n                {\n                    Coupon = new Coupon { Id = \"EXISTING_DISCOUNT_50\" }\n                }\n            }\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],\n            Total = 6600\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, planChange, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(6.00m, tax);\n        Assert.Equal(66.00m, total);\n\n        // Verify the correct Stripe API call preserves existing discount\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"90210\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-enterprise-org-seat-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == \"EXISTING_DISCOUNT_50\"));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationPlanChange_OrganizationWithoutGatewayIds_ReturnsBadRequest()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.TeamsMonthly,\n            GatewayCustomerId = null,\n            GatewaySubscriptionId = null\n        };\n\n        var planChange = new OrganizationSubscriptionPlanChange\n        {\n            Tier = ProductTierType.Enterprise,\n            Cadence = PlanCadenceType.Annually\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var result = await _command.Run(organization, planChange, billingAddress);\n\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Organization does not have a subscription.\", badRequest.Response);\n\n        // Verify no Stripe API calls were made\n        await _stripeAdapter.DidNotReceive().CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>());\n        await _stripeAdapter.DidNotReceive().GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());\n    }\n\n    #endregion\n\n    #region Subscription Update\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionUpdate_PasswordManagerSeatsOnly_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.TeamsMonthly,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\"\n        };\n\n        var update = new OrganizationSubscriptionUpdate\n        {\n            PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections\n            {\n                Seats = 10,\n                AdditionalStorage = null\n            }\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        var customer = new Customer\n        {\n            Address = new Address { Country = \"US\", PostalCode = \"12345\" },\n            Discount = null,\n            TaxIds = null\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = customer\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],\n            Total = 6600\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, update);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(6.00m, tax);\n        Assert.Equal(66.00m, total);\n\n        // Verify the correct Stripe API call for PM seats only\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 10 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionUpdate_PasswordManagerWithStorage_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\"\n        };\n\n        var update = new OrganizationSubscriptionUpdate\n        {\n            PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections\n            {\n                Seats = 15,\n                AdditionalStorage = 5\n            }\n        };\n\n        var plan = new EnterprisePlan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        var customer = new Customer\n        {\n            Address = new Address { Country = \"CA\", PostalCode = \"K1A 0A6\" },\n            Discount = null,\n            TaxIds = null\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = customer\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 1200 }],\n            Total = 13200\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, update);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(12.00m, tax);\n        Assert.Equal(132.00m, total);\n\n        // Verify the correct Stripe API call for PM seats + storage\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"CA\" &&\n            options.CustomerDetails.Address.PostalCode == \"K1A 0A6\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"2023-enterprise-org-seat-annually\" && item.Quantity == 15) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"storage-gb-annually\" && item.Quantity == 5) &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionUpdate_SecretsManagerOnly_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.TeamsAnnually,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\"\n        };\n\n        var update = new OrganizationSubscriptionUpdate\n        {\n            SecretsManager = new OrganizationSubscriptionUpdate.SecretsManagerSelections\n            {\n                Seats = 8,\n                AdditionalServiceAccounts = null\n            }\n        };\n\n        var plan = new TeamsPlan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        var customer = new Customer\n        {\n            Address = new Address { Country = \"DE\", PostalCode = \"10115\" },\n            Discount = null,\n            TaxIds = null\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = customer\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],\n            Total = 8800\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, update);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(8.00m, tax);\n        Assert.Equal(88.00m, total);\n\n        // Verify the correct Stripe API call for SM seats only\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"DE\" &&\n            options.CustomerDetails.Address.PostalCode == \"10115\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"secrets-manager-teams-seat-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 8 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionUpdate_SecretsManagerWithServiceAccounts_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.EnterpriseMonthly,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\"\n        };\n\n        var update = new OrganizationSubscriptionUpdate\n        {\n            SecretsManager = new OrganizationSubscriptionUpdate.SecretsManagerSelections\n            {\n                Seats = 12,\n                AdditionalServiceAccounts = 20\n            }\n        };\n\n        var plan = new EnterprisePlan(false);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        var customer = new Customer\n        {\n            Address = new Address { Country = \"GB\", PostalCode = \"SW1A 1AA\" },\n            Discount = null,\n            TaxIds = new StripeList<TaxId>\n            {\n                Data = [new TaxId { Type = \"gb_vat\", Value = \"GB123456789\" }]\n            }\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = customer\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 1500 }],\n            Total = 16500\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, update);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(15.00m, tax);\n        Assert.Equal(165.00m, total);\n\n        // Verify the correct Stripe API call for SM seats + service accounts with tax ID\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"GB\" &&\n            options.CustomerDetails.Address.PostalCode == \"SW1A 1AA\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.CustomerDetails.TaxIds.Count == 1 &&\n            options.CustomerDetails.TaxIds[0].Type == \"gb_vat\" &&\n            options.CustomerDetails.TaxIds[0].Value == \"GB123456789\" &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-enterprise-seat-monthly\" && item.Quantity == 12) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-service-account-2024-monthly\" && item.Quantity == 20) &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionUpdate_ComprehensiveUpdate_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\"\n        };\n\n        var update = new OrganizationSubscriptionUpdate\n        {\n            PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections\n            {\n                Seats = 25,\n                AdditionalStorage = 10\n            },\n            SecretsManager = new OrganizationSubscriptionUpdate.SecretsManagerSelections\n            {\n                Seats = 15,\n                AdditionalServiceAccounts = 30\n            }\n        };\n\n        var plan = new EnterprisePlan(true);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        var customer = new Customer\n        {\n            Address = new Address { Country = \"ES\", PostalCode = \"28001\" },\n            Discount = new Discount\n            {\n                Coupon = new Coupon { Id = \"ENTERPRISE_DISCOUNT_20\" }\n            },\n            TaxIds = new StripeList<TaxId>\n            {\n                Data = [new TaxId { Type = TaxIdType.SpanishNIF, Value = \"12345678Z\" }]\n            }\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = customer\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 2500 }],\n            Total = 27500\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, update);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(25.00m, tax);\n        Assert.Equal(275.00m, total);\n\n        // Verify the correct Stripe API call for comprehensive update with discount and Spanish tax ID\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"ES\" &&\n            options.CustomerDetails.Address.PostalCode == \"28001\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.Reverse &&\n            options.CustomerDetails.TaxIds.Count == 2 &&\n            options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.SpanishNIF && t.Value == \"12345678Z\") &&\n            options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.EUVAT && t.Value == \"ES12345678Z\") &&\n            options.SubscriptionDetails.Items.Count == 4 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"2023-enterprise-org-seat-annually\" && item.Quantity == 25) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"storage-gb-annually\" && item.Quantity == 10) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-enterprise-seat-annually\" && item.Quantity == 15) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"secrets-manager-service-account-2024-annually\" && item.Quantity == 30) &&\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == \"ENTERPRISE_DISCOUNT_20\"));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionUpdate_FamiliesTierPersonalUsage_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.FamiliesAnnually,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\"\n        };\n\n        var update = new OrganizationSubscriptionUpdate\n        {\n            PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections\n            {\n                Seats = 6,\n                AdditionalStorage = 2\n            }\n        };\n\n        var plan = new FamiliesPlan();\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        var customer = new Customer\n        {\n            Address = new Address { Country = \"AU\", PostalCode = \"2000\" },\n            Discount = null,\n            TaxIds = null\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = customer\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],\n            Total = 5500\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, update);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(5.00m, tax);\n        Assert.Equal(55.00m, total);\n\n        // Verify the correct Stripe API call for Families tier (personal usage, no business tax exemption)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"AU\" &&\n            options.CustomerDetails.Address.PostalCode == \"2000\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"2020-families-org-annually\" && item.Quantity == 6) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == \"personal-storage-gb-annually\" && item.Quantity == 2) &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionUpdate_OrganizationWithoutGatewayIds_ReturnsBadRequest()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.TeamsMonthly,\n            GatewayCustomerId = null,\n            GatewaySubscriptionId = null\n        };\n\n        var update = new OrganizationSubscriptionUpdate\n        {\n            PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections\n            {\n                Seats = 5\n            }\n        };\n\n        var result = await _command.Run(organization, update);\n\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Organization does not have a subscription.\", badRequest.Response);\n\n        // Verify no Stripe API calls were made\n        await _stripeAdapter.DidNotReceive().CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>());\n        await _stripeAdapter.DidNotReceive().GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task Run_OrganizationSubscriptionUpdate_ZeroValuesExcluded_ReturnsCorrectTaxAmounts()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            PlanType = PlanType.TeamsMonthly,\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\"\n        };\n\n        var update = new OrganizationSubscriptionUpdate\n        {\n            PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0  // Should be excluded\n            },\n            SecretsManager = new OrganizationSubscriptionUpdate.SecretsManagerSelections\n            {\n                Seats = 0,  // Should be excluded entirely (including service accounts)\n                AdditionalServiceAccounts = 10\n            }\n        };\n\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        var customer = new Customer\n        {\n            Address = new Address { Country = \"US\", PostalCode = \"90210\" },\n            Discount = null,\n            TaxIds = null\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = customer\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_test123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(organization, update);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify only PM seats are included (storage=0 excluded, SM seats=0 so entire SM excluded)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"90210\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    #endregion\n\n    #region Coupon Validation\n\n    [Fact]\n    public async Task Run_FamiliesOrganizationWithValidCoupon_ValidatesCouponAndAppliesDiscount()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Families,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 6,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [\"VALID_FAMILIES_DISCOUNT\"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new FamiliesPlan();\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_FAMILIES_DISCOUNT\" })),\n            DiscountTierType.Families).Returns(true);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        await _subscriptionDiscountService.Received(1).ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_FAMILIES_DISCOUNT\" })),\n            DiscountTierType.Families);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == \"VALID_FAMILIES_DISCOUNT\"));\n    }\n\n    [Fact]\n    public async Task Run_FamiliesOrganizationWithInvalidCoupon_ProceedsWithoutDiscount()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Families,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 6,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [\"INVALID_COUPON\"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new FamiliesPlan();\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"INVALID_COUPON\" })),\n            DiscountTierType.Families).Returns(false);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        await _subscriptionDiscountService.Received(1).ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"INVALID_COUPON\" })),\n            DiscountTierType.Families);\n\n        // Verify invalid coupon is silently ignored (no discount applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2020-families-org-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 6 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_TeamsOrganizationWithCoupon_IgnoresCoupon()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [\"TEAMS_COUPON\"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new TeamsPlan(isAnnual: false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        // Verify coupon validation was NOT called for Teams (only Families plans use coupons)\n        await _subscriptionDiscountService.DidNotReceive().ValidateDiscountEligibilityForUserAsync(\n            Arg.Any<User>(),\n            Arg.Any<IReadOnlyList<string>>(),\n            Arg.Any<DiscountTierType>());\n\n        // Verify coupon is ignored for Teams plans (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-teams-org-seat-monthly\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 5 &&\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_EnterpriseOrganizationWithCoupon_IgnoresCoupon()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Enterprise,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 10,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [\"ENTERPRISE_COUPON\"]\n        };\n\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var plan = new EnterprisePlan(isAnnual: true);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],\n            Total = 6600\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(6.00m, tax);\n        Assert.Equal(66.00m, total);\n\n        // Verify coupon validation was NOT called for Enterprise (only Families plans use coupons)\n        await _subscriptionDiscountService.DidNotReceive().ValidateDiscountEligibilityForUserAsync(\n            Arg.Any<User>(),\n            Arg.Any<IReadOnlyList<string>>(),\n            Arg.Any<DiscountTierType>());\n\n        // Verify coupon is ignored for Enterprise plans (no discounts applied)\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.CustomerDetails.TaxExempt == TaxExempt.None &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == \"2023-enterprise-org-seat-annually\" &&\n            options.SubscriptionDetails.Items[0].Quantity == 10 &&\n            options.Discounts == null));\n    }\n\n    #endregion\n\n    #region Multi-coupon support\n\n    [Fact]\n    public async Task Run_WithMultipleValidCoupons_AppliesBothToInvoicePreview()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Families,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 6,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [\"COUPON_ONE\", \"COUPON_TWO\"]\n        };\n\n        var billingAddress = new BillingAddress { Country = \"US\", PostalCode = \"12345\" };\n        var plan = new FamiliesPlan();\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"COUPON_ONE\", \"COUPON_TWO\" })),\n            DiscountTierType.Families).Returns(true);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 200 }],\n            Total = 2200\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts != null &&\n            options.Discounts.Count == 2 &&\n            options.Discounts.Any(d => d.Coupon == \"COUPON_ONE\") &&\n            options.Discounts.Any(d => d.Coupon == \"COUPON_TWO\")));\n    }\n\n    [Fact]\n    public async Task Run_WithStandaloneSecretsManagerAndCoupons_IgnoresUserCoupons()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Teams,\n            Cadence = PlanCadenceType.Monthly,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 5,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            SecretsManager = new OrganizationSubscriptionPurchase.SecretsManagerSelections\n            {\n                Seats = 3,\n                AdditionalServiceAccounts = 0,\n                Standalone = true\n            },\n            Coupons = [\"COUPON_ONE\", \"COUPON_TWO\"]\n        };\n\n        var billingAddress = new BillingAddress { Country = \"US\", PostalCode = \"12345\" };\n        var plan = new TeamsPlan(false);\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],\n            Total = 5500\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n\n        // User coupons ignored; system coupon applied for standalone SM\n        await _subscriptionDiscountService.DidNotReceive().ValidateDiscountEligibilityForUserAsync(\n            Arg.Any<User>(), Arg.Any<IReadOnlyList<string>>(), Arg.Any<DiscountTierType>());\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == CouponIDs.SecretsManagerStandalone));\n    }\n\n    [Fact]\n    public async Task Run_WithMixedValidAndInvalidCoupons_SkipsAllDiscounts()\n    {\n        var purchase = new OrganizationSubscriptionPurchase\n        {\n            Tier = ProductTierType.Families,\n            Cadence = PlanCadenceType.Annually,\n            PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections\n            {\n                Seats = 6,\n                AdditionalStorage = 0,\n                Sponsored = false\n            },\n            Coupons = [\"VALID_COUPON\", \"INVALID_COUPON\"]\n        };\n\n        var billingAddress = new BillingAddress { Country = \"US\", PostalCode = \"12345\" };\n        var plan = new FamiliesPlan();\n        _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_COUPON\", \"INVALID_COUPON\" })),\n            DiscountTierType.Families).Returns(false);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, purchase, billingAddress);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts == null || options.Discounts.Count == 0));\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs",
    "content": "﻿using System.Reflection;\nusing System.Security.Claims;\nusing System.Text.RegularExpressions;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Licenses;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\nusing JsonSerializer = System.Text.Json.JsonSerializer;\n\nnamespace Bit.Core.Test.Billing.Organizations.Commands;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationLicenseCommandTests\n{\n    private static string LicenseDirectory => Path.GetDirectoryName(OrganizationLicenseDirectory.Value);\n    private static Lazy<string> OrganizationLicenseDirectory => new(() =>\n    {\n        // Create a temporary directory to write the license file to\n        var directory = Path.Combine(Path.GetTempPath(), \"bitwarden/\");\n        if (!Directory.Exists(directory))\n        {\n            Directory.CreateDirectory(directory);\n        }\n        return directory;\n    });\n\n    [Theory, BitAutoData]\n    public async Task UpdateLicenseAsync_UpdatesLicenseFileAndOrganization(\n        SelfHostedOrganizationDetails selfHostedOrg,\n        OrganizationLicense license,\n        SutProvider<UpdateOrganizationLicenseCommand> sutProvider)\n    {\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        globalSettings.LicenseDirectory = LicenseDirectory;\n        globalSettings.SelfHosted = true;\n\n        // Passing values for OrganizationLicense.CanUse\n        // NSubstitute cannot override non-virtual members so we have to ensure the real method passes\n        license.Enabled = true;\n        license.Issued = DateTime.Now.AddDays(-1);\n        license.Expires = DateTime.Now.AddDays(1);\n        license.Version = OrganizationLicense.CurrentLicenseFileVersion;\n        license.InstallationId = globalSettings.Installation.Id;\n        license.LicenseType = LicenseType.Organization;\n        sutProvider.GetDependency<ILicensingService>().VerifyLicense(license).Returns(true);\n        sutProvider.GetDependency<ILicensingService>()\n            .GetClaimsPrincipalFromLicense(license)\n            .Returns((ClaimsPrincipal)null);\n\n        // Passing values for SelfHostedOrganizationDetails.CanUseLicense\n        // NSubstitute cannot override non-virtual members so we have to ensure the real method passes\n        license.Seats = null;\n        license.MaxCollections = null;\n        license.UseGroups = true;\n        license.UsePolicies = true;\n        license.UseSso = true;\n        license.UseKeyConnector = true;\n        license.UseScim = true;\n        license.UseCustomPermissions = true;\n        license.UseResetPassword = true;\n\n        try\n        {\n            await sutProvider.Sut.UpdateLicenseAsync(selfHostedOrg, license, null);\n\n            // Assertion: should have saved the license file to disk\n            var filePath = Path.Combine(LicenseDirectory, \"organization\", $\"{selfHostedOrg.Id}.json\");\n            await using var fs = File.OpenRead(filePath);\n            var licenseFromFile = await JsonSerializer.DeserializeAsync<OrganizationLicense>(fs);\n\n            AssertHelper.AssertPropertyEqual(license, licenseFromFile, \"SignatureBytes\");\n\n            // Assertion: should have updated and saved the organization\n            // Properties excluded from the comparison below are exceptions to the rule that the Organization mirrors\n            // the OrganizationLicense\n            await sutProvider.GetDependency<IOrganizationService>()\n                .Received(1)\n                .ReplaceAndUpdateCacheAsync(Arg.Is<Organization>(\n                    org => AssertPropertyEqual(license, org,\n                        \"Id\", \"MaxStorageGb\", \"Issued\", \"Refresh\", \"Version\", \"Trial\", \"LicenseType\",\n                        \"Hash\", \"Signature\", \"SignatureBytes\", \"InstallationId\", \"Expires\",\n                        \"ExpirationWithoutGracePeriod\", \"Token\", \"LimitCollectionCreationDeletion\",\n                        \"LimitCollectionCreation\", \"LimitCollectionDeletion\", \"AllowAdminAccessToAllCollectionItems\") &&\n                         // Same property but different name, use explicit mapping\n                         org.ExpirationDate == license.Expires));\n        }\n        finally\n        {\n            // Clean up temporary directory\n            Directory.Delete(OrganizationLicenseDirectory.Value, true);\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateLicenseAsync_WithClaimsPrincipal_ExtractsAllPropertiesFromClaims(\n        SelfHostedOrganizationDetails selfHostedOrg,\n        OrganizationLicense license,\n        SutProvider<UpdateOrganizationLicenseCommand> sutProvider)\n    {\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        globalSettings.LicenseDirectory = LicenseDirectory;\n        globalSettings.SelfHosted = true;\n\n        // Setup license for CanUse validation\n        license.Enabled = true;\n        license.Issued = DateTime.Now.AddDays(-1);\n        license.Expires = DateTime.Now.AddDays(1);\n        license.Version = OrganizationLicense.CurrentLicenseFileVersion;\n        license.InstallationId = globalSettings.Installation.Id;\n        license.LicenseType = LicenseType.Organization;\n        license.Token = \"test-token\"; // Indicates this is a claims-based license\n        sutProvider.GetDependency<ILicensingService>().VerifyLicense(license).Returns(true);\n\n        // Create a ClaimsPrincipal with all organization license claims\n        var claims = new List<Claim>\n        {\n            new(OrganizationLicenseConstants.LicenseType, ((int)LicenseType.Organization).ToString()),\n            new(OrganizationLicenseConstants.InstallationId, globalSettings.Installation.Id.ToString()),\n            new(OrganizationLicenseConstants.Name, \"Test Organization\"),\n            new(OrganizationLicenseConstants.BillingEmail, \"billing@test.com\"),\n            new(OrganizationLicenseConstants.BusinessName, \"Test Business\"),\n            new(OrganizationLicenseConstants.PlanType, ((int)PlanType.EnterpriseAnnually).ToString()),\n            new(OrganizationLicenseConstants.Seats, \"100\"),\n            new(OrganizationLicenseConstants.MaxCollections, \"50\"),\n            new(OrganizationLicenseConstants.UsePolicies, \"true\"),\n            new(OrganizationLicenseConstants.UseSso, \"true\"),\n            new(OrganizationLicenseConstants.UseKeyConnector, \"true\"),\n            new(OrganizationLicenseConstants.UseScim, \"true\"),\n            new(OrganizationLicenseConstants.UseGroups, \"true\"),\n            new(OrganizationLicenseConstants.UseDirectory, \"true\"),\n            new(OrganizationLicenseConstants.UseEvents, \"true\"),\n            new(OrganizationLicenseConstants.UseTotp, \"true\"),\n            new(OrganizationLicenseConstants.Use2fa, \"true\"),\n            new(OrganizationLicenseConstants.UseApi, \"true\"),\n            new(OrganizationLicenseConstants.UseResetPassword, \"true\"),\n            new(OrganizationLicenseConstants.Plan, \"Enterprise\"),\n            new(OrganizationLicenseConstants.SelfHost, \"true\"),\n            new(OrganizationLicenseConstants.UsersGetPremium, \"true\"),\n            new(OrganizationLicenseConstants.UseCustomPermissions, \"true\"),\n            new(OrganizationLicenseConstants.Enabled, \"true\"),\n            new(OrganizationLicenseConstants.Expires, DateTime.Now.AddDays(1).ToString(\"O\")),\n            new(OrganizationLicenseConstants.LicenseKey, \"test-license-key\"),\n            new(OrganizationLicenseConstants.UsePasswordManager, \"true\"),\n            new(OrganizationLicenseConstants.UseSecretsManager, \"true\"),\n            new(OrganizationLicenseConstants.SmSeats, \"25\"),\n            new(OrganizationLicenseConstants.SmServiceAccounts, \"10\"),\n            new(OrganizationLicenseConstants.UseRiskInsights, \"true\"),\n            new(OrganizationLicenseConstants.UseOrganizationDomains, \"true\"),\n            new(OrganizationLicenseConstants.UseAdminSponsoredFamilies, \"true\"),\n            new(OrganizationLicenseConstants.UseAutomaticUserConfirmation, \"true\"),\n            new(OrganizationLicenseConstants.UseDisableSmAdsForUsers, \"true\"),\n            new(OrganizationLicenseConstants.UsePhishingBlocker, \"true\"),\n            new(OrganizationLicenseConstants.MaxStorageGb, \"5\"),\n            new(OrganizationLicenseConstants.Issued, DateTime.Now.AddDays(-1).ToString(\"O\")),\n            new(OrganizationLicenseConstants.Refresh, DateTime.Now.AddMonths(1).ToString(\"O\")),\n            new(OrganizationLicenseConstants.ExpirationWithoutGracePeriod, DateTime.Now.AddMonths(12).ToString(\"O\")),\n            new(OrganizationLicenseConstants.Trial, \"false\"),\n            new(OrganizationLicenseConstants.LimitCollectionCreationDeletion, \"true\"),\n            new(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems, \"true\"),\n            new(OrganizationLicenseConstants.UseMyItems, \"true\")\n        };\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        sutProvider.GetDependency<ILicensingService>()\n            .GetClaimsPrincipalFromLicense(license)\n            .Returns(claimsPrincipal);\n\n        // Setup selfHostedOrg for CanUseLicense validation\n        selfHostedOrg.OccupiedSeatCount = 50; // Less than the 100 seats in the license\n        selfHostedOrg.CollectionCount = 10; // Less than the 50 max collections in the license\n        selfHostedOrg.GroupCount = 1;\n        selfHostedOrg.UseGroups = true;\n        selfHostedOrg.UsePolicies = true;\n        selfHostedOrg.UseSso = true;\n        selfHostedOrg.UseKeyConnector = true;\n        selfHostedOrg.UseScim = true;\n        selfHostedOrg.UseCustomPermissions = true;\n        selfHostedOrg.UseResetPassword = true;\n\n        try\n        {\n            await sutProvider.Sut.UpdateLicenseAsync(selfHostedOrg, license, null);\n\n            // Assertion: license file should be written to disk\n            var filePath = Path.Combine(LicenseDirectory, \"organization\", $\"{selfHostedOrg.Id}.json\");\n            await using var fs = File.OpenRead(filePath);\n            var licenseFromFile = await JsonSerializer.DeserializeAsync<OrganizationLicense>(fs);\n\n            AssertHelper.AssertPropertyEqual(license, licenseFromFile, \"SignatureBytes\");\n\n            // Assertion: organization should be updated with ALL properties extracted from claims\n            await sutProvider.GetDependency<IOrganizationService>()\n                .Received(1)\n                .ReplaceAndUpdateCacheAsync(Arg.Is<Organization>(org =>\n                    org.Name == \"Test Organization\" &&\n                    org.BillingEmail == \"billing@test.com\" &&\n                    org.BusinessName == \"Test Business\" &&\n                    org.PlanType == PlanType.EnterpriseAnnually &&\n                    org.Seats == 100 &&\n                    org.MaxCollections == 50 &&\n                    org.UsePolicies == true &&\n                    org.UseSso == true &&\n                    org.UseKeyConnector == true &&\n                    org.UseScim == true &&\n                    org.UseGroups == true &&\n                    org.UseDirectory == true &&\n                    org.UseEvents == true &&\n                    org.UseTotp == true &&\n                    org.Use2fa == true &&\n                    org.UseApi == true &&\n                    org.UseResetPassword == true &&\n                    org.Plan == \"Enterprise\" &&\n                    org.SelfHost == true &&\n                    org.UsersGetPremium == true &&\n                    org.UseCustomPermissions == true &&\n                    org.Enabled == true &&\n                    org.LicenseKey == \"test-license-key\" &&\n                    org.UsePasswordManager == true &&\n                    org.UseSecretsManager == true &&\n                    org.SmSeats == 25 &&\n                    org.SmServiceAccounts == 10 &&\n                    org.UseRiskInsights == true &&\n                    org.UseOrganizationDomains == true &&\n                    org.UseAdminSponsoredFamilies == true &&\n                    org.UseAutomaticUserConfirmation == true &&\n                    org.UseDisableSmAdsForUsers == true &&\n                    org.UsePhishingBlocker == true &&\n                    org.UseMyItems));\n        }\n        finally\n        {\n            // Clean up temporary directory\n            if (Directory.Exists(OrganizationLicenseDirectory.Value))\n            {\n                Directory.Delete(OrganizationLicenseDirectory.Value, true);\n            }\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateLicenseAsync_WrongInstallationIdInClaims_ThrowsBadRequestException(\n        SelfHostedOrganizationDetails selfHostedOrg,\n        OrganizationLicense license,\n        SutProvider<UpdateOrganizationLicenseCommand> sutProvider)\n    {\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        globalSettings.LicenseDirectory = LicenseDirectory;\n        globalSettings.SelfHosted = true;\n\n        // Setup license for CanUse validation\n        license.Enabled = true;\n        license.Issued = DateTime.Now.AddDays(-1);\n        license.Expires = DateTime.Now.AddDays(1);\n        license.Version = OrganizationLicense.CurrentLicenseFileVersion;\n        license.LicenseType = LicenseType.Organization;\n        license.Token = \"test-token\"; // Indicates this is a claims-based license\n        sutProvider.GetDependency<ILicensingService>().VerifyLicense(license).Returns(true);\n\n        // Create a ClaimsPrincipal with WRONG installation ID\n        var wrongInstallationId = Guid.NewGuid(); // Different from globalSettings.Installation.Id\n        var claims = new List<Claim>\n        {\n            new(OrganizationLicenseConstants.LicenseType, ((int)LicenseType.Organization).ToString()),\n            new(OrganizationLicenseConstants.InstallationId, wrongInstallationId.ToString()),\n            new(OrganizationLicenseConstants.Enabled, \"true\"),\n            new(OrganizationLicenseConstants.SelfHost, \"true\")\n        };\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        sutProvider.GetDependency<ILicensingService>()\n            .GetClaimsPrincipalFromLicense(license)\n            .Returns(claimsPrincipal);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateLicenseAsync(selfHostedOrg, license, null));\n\n        Assert.Contains(\"The installation ID does not match the current installation.\", exception.Message);\n\n        // Verify organization was NOT saved\n        await sutProvider.GetDependency<IOrganizationService>()\n            .DidNotReceive()\n            .ReplaceAndUpdateCacheAsync(Arg.Any<Organization>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateLicenseAsync_ExpiredLicenseWithoutClaims_ThrowsBadRequestException(\n        SelfHostedOrganizationDetails selfHostedOrg,\n        OrganizationLicense license,\n        SutProvider<UpdateOrganizationLicenseCommand> sutProvider)\n    {\n        var globalSettings = sutProvider.GetDependency<IGlobalSettings>();\n        globalSettings.LicenseDirectory = LicenseDirectory;\n        globalSettings.SelfHosted = true;\n\n        // Setup legacy license (no Token, no claims)\n        license.Token = null; // Legacy license\n        license.Enabled = true;\n        license.Issued = DateTime.Now.AddDays(-2);\n        license.Expires = DateTime.Now.AddDays(-1); // Expired yesterday\n        license.Version = OrganizationLicense.CurrentLicenseFileVersion;\n        license.InstallationId = globalSettings.Installation.Id;\n        license.LicenseType = LicenseType.Organization;\n        license.SelfHost = true;\n\n        sutProvider.GetDependency<ILicensingService>().VerifyLicense(license).Returns(true);\n        sutProvider.GetDependency<ILicensingService>()\n            .GetClaimsPrincipalFromLicense(license)\n            .Returns((ClaimsPrincipal)null); // No claims for legacy license\n\n        // Passing values for SelfHostedOrganizationDetails.CanUseLicense\n        license.Seats = null;\n        license.MaxCollections = null;\n        license.UseGroups = true;\n        license.UsePolicies = true;\n        license.UseSso = true;\n        license.UseKeyConnector = true;\n        license.UseScim = true;\n        license.UseCustomPermissions = true;\n        license.UseResetPassword = true;\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateLicenseAsync(selfHostedOrg, license, null));\n\n        Assert.Contains(\"The license has expired.\", exception.Message);\n\n        // Verify organization was NOT saved\n        await sutProvider.GetDependency<IOrganizationService>()\n            .DidNotReceive()\n            .ReplaceAndUpdateCacheAsync(Arg.Any<Organization>());\n    }\n\n    [Fact]\n    public async Task UpdateLicenseAsync_ExtractsAllClaimsBasedProperties_WhenClaimsPrincipalProvided()\n    {\n        // This test ensures that when new properties are added to OrganizationLicense,\n        // they are automatically extracted from JWT claims in UpdateOrganizationLicenseCommand.\n        // If a new constant is added to OrganizationLicenseConstants but not extracted,\n        // this test will fail with a clear message showing which properties are missing.\n\n        // 1. Get all OrganizationLicenseConstants\n        var constantFields = typeof(OrganizationLicenseConstants)\n            .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.GetField)\n            .Where(f => f.IsLiteral && !f.IsInitOnly)\n            .Select(f => f.GetValue(null) as string)\n            .ToList();\n\n        // 2. Define properties that should be excluded (not claims-based or intentionally not extracted)\n        var excludedProperties = new HashSet<string>\n        {\n            \"Version\",        // Not in claims system (only in deprecated property-based licenses)\n            \"Hash\",           // Signature-related, not extracted from claims\n            \"Signature\",      // Signature-related, not extracted from claims\n            \"SignatureBytes\", // Computed from Signature, not a claim\n            \"Token\",          // The JWT itself, not extracted from claims\n            \"Id\"              // Cloud org ID from license, not used - self-hosted org has its own separate ID\n        };\n\n        // 3. Get properties that should be extracted from claims\n        var propertiesThatShouldBeExtracted = constantFields\n            .Where(c => !excludedProperties.Contains(c))\n            .ToHashSet();\n\n        // 4. Read UpdateOrganizationLicenseCommand source code\n        var commandSourcePath = Path.Combine(\n            Directory.GetCurrentDirectory(),\n            \"..\", \"..\", \"..\", \"..\", \"..\",\n            \"src\", \"Core\", \"Billing\", \"Organizations\", \"Commands\", \"UpdateOrganizationLicenseCommand.cs\");\n        var sourceCode = await File.ReadAllTextAsync(commandSourcePath);\n\n        // 5. Find all GetValue calls that extract properties from claims\n        // Pattern matches: license.PropertyName = claimsPrincipal.GetValue<Type>(OrganizationLicenseConstants.PropertyName)\n        var extractedProperties = new HashSet<string>();\n        var getValuePattern = @\"claimsPrincipal\\.GetValue<[^>]+>\\(OrganizationLicenseConstants\\.(\\w+)\\)\";\n        var matches = Regex.Matches(sourceCode, getValuePattern);\n\n        foreach (Match match in matches)\n        {\n            extractedProperties.Add(match.Groups[1].Value);\n        }\n\n        // 6. Find missing extractions\n        var missingExtractions = propertiesThatShouldBeExtracted\n            .Except(extractedProperties)\n            .OrderBy(p => p)\n            .ToList();\n\n        // 7. Build error message with guidance if there are missing extractions\n        var errorMessage = \"\";\n        if (missingExtractions.Any())\n        {\n            errorMessage = $\"The following constants in OrganizationLicenseConstants are NOT extracted from claims in UpdateOrganizationLicenseCommand:\\n\";\n            errorMessage += string.Join(\"\\n\", missingExtractions.Select(p => $\"  - {p}\"));\n            errorMessage += \"\\n\\nPlease add the following lines to UpdateOrganizationLicenseCommand.cs in the 'if (claimsPrincipal != null)' block:\\n\";\n            foreach (var prop in missingExtractions)\n            {\n                errorMessage += $\"  license.{prop} = claimsPrincipal.GetValue<TYPE>(OrganizationLicenseConstants.{prop});\\n\";\n            }\n        }\n\n        // 8. Assert - if this fails, the error message guides the developer to add the extraction\n        // Note: We don't check for \"extra extractions\" because that would be a compile error\n        // (can't reference OrganizationLicenseConstants.Foo if Foo doesn't exist)\n        Assert.True(\n            !missingExtractions.Any(),\n            $\"\\n{errorMessage}\");\n    }\n\n    // Wrapper to compare 2 objects that are different types\n    private bool AssertPropertyEqual(OrganizationLicense expected, Organization actual, params string[] excludedPropertyStrings)\n    {\n        AssertHelper.AssertPropertyEqual(expected, actual, excludedPropertyStrings);\n        return true;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationSubscriptionCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Organizations.Commands;\n\nusing static StripeConstants;\n\npublic class UpdateOrganizationSubscriptionCommandTests\n{\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly UpdateOrganizationSubscriptionCommand _command;\n\n    public UpdateOrganizationSubscriptionCommandTests()\n    {\n        _command = new UpdateOrganizationSubscriptionCommand(\n            Substitute.For<ILogger<UpdateOrganizationSubscriptionCommand>>(),\n            _stripeAdapter);\n    }\n\n    [Fact]\n    public async Task Run_SubscriptionNotFound_ReturnsBadRequest()\n    {\n        var organization = CreateOrganization();\n\n        _stripeAdapter\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns<Subscription>(_ => throw new StripeException { StripeError = new StripeError { Code = ErrorCodes.ResourceMissing } });\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.IsT1);\n        Assert.Equal(\"We couldn't find your subscription.\", result.AsT1.Response);\n    }\n\n    [Theory]\n    [InlineData(SubscriptionStatus.Canceled)]\n    [InlineData(SubscriptionStatus.Incomplete)]\n    [InlineData(SubscriptionStatus.IncompleteExpired)]\n    [InlineData(SubscriptionStatus.Unpaid)]\n    [InlineData(SubscriptionStatus.Paused)]\n    public async Task Run_InvalidSubscriptionStatus_ReturnsBadRequest(string status)\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(status: status, items: [(\"price_seats\", \"si_1\", 5)]);\n\n        _stripeAdapter\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.IsT1);\n        Assert.Equal(\"Your subscription cannot be updated in its current status.\", result.AsT1.Response);\n    }\n\n    [Theory]\n    [InlineData(SubscriptionStatus.Active)]\n    [InlineData(SubscriptionStatus.Trialing)]\n    [InlineData(SubscriptionStatus.PastDue)]\n    public async Task Run_ValidSubscriptionStatus_DoesNotReturnStatusError(string status)\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(status: status, items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n    }\n\n    [Fact]\n    public async Task Run_EmptyChangeSet_ReturnsConflict()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet { Changes = [] };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.IsT2);\n        Assert.Equal(\"No changes were provided for the organization subscription update\", result.AsT2.Response);\n    }\n\n    [Fact]\n    public async Task Run_AddItem_DuplicatePrice_ReturnsBadRequest()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new AddItem(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.IsT1);\n        Assert.Contains(\"price_seats\", result.AsT1.Response);\n    }\n\n    [Fact]\n    public async Task Run_AddItem_Valid_CreatesCorrectOptions()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new AddItem(\"price_storage\", 3)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.Items.Count == 1 &&\n                options.Items[0].Price == \"price_storage\" &&\n                options.Items[0].Quantity == 3));\n    }\n\n    [Fact]\n    public async Task Run_ChangeItemPrice_MissingCurrentPrice_ReturnsBadRequest()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new ChangeItemPrice(\"price_nonexistent\", \"price_new\", null)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.IsT1);\n        Assert.Contains(\"price_nonexistent\", result.AsT1.Response);\n    }\n\n    [Fact]\n    public async Task Run_ChangeItemPrice_Valid_PreservesExistingQuantity()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_monthly\", \"si_1\", 10)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new ChangeItemPrice(\"price_monthly\", \"price_annual\", null)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.Items.Count == 1 &&\n                options.Items[0].Id == \"si_1\" &&\n                options.Items[0].Price == \"price_annual\" &&\n                options.Items[0].Quantity == 10));\n    }\n\n    [Fact]\n    public async Task Run_ChangeItemPrice_WithExplicitQuantity_UsesProvidedQuantity()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_monthly\", \"si_1\", 10)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new ChangeItemPrice(\"price_monthly\", \"price_annual\", 20)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.Items[0].Quantity == 20));\n    }\n\n    [Fact]\n    public async Task Run_RemoveItem_MissingPrice_ReturnsBadRequest()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new RemoveItem(\"price_nonexistent\")]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.IsT1);\n        Assert.Contains(\"price_nonexistent\", result.AsT1.Response);\n    }\n\n    [Fact]\n    public async Task Run_RemoveItem_Valid_SetsDeletedTrue()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5), (\"price_storage\", \"si_2\", 1)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new RemoveItem(\"price_storage\")]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.Items.Count == 1 &&\n                options.Items[0].Id == \"si_2\" &&\n                options.Items[0].Deleted == true));\n    }\n\n    [Fact]\n    public async Task Run_StripeExceptionDuringUpdate_ReturnsUnhandled()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        _stripeAdapter\n            .UpdateSubscriptionAsync(subscription.Id, Arg.Any<SubscriptionUpdateOptions>())\n            .Returns<Subscription>(_ => throw new StripeException { StripeError = new StripeError { Code = \"api_error\" } });\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.IsT3);\n    }\n\n    [Fact]\n    public async Task Run_UpdateItemQuantity_MissingPrice_ReturnsBadRequest()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_nonexistent\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.IsT1);\n        Assert.Contains(\"price_nonexistent\", result.AsT1.Response);\n    }\n\n    [Fact]\n    public async Task Run_UpdateItemQuantity_Valid_CreatesCorrectOptions()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 15)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.Items.Count == 1 &&\n                options.Items[0].Id == \"si_1\" &&\n                options.Items[0].Price == \"price_seats\" &&\n                options.Items[0].Quantity == 15));\n    }\n\n    [Fact]\n    public async Task Run_UpdateItemQuantity_ZeroQuantity_SetsDeletedTrue()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 0)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.Items[0].Id == \"si_1\" &&\n                options.Items[0].Deleted == true));\n    }\n\n    [Fact]\n    public async Task Run_StructuralChange_SetsAlwaysInvoiceProration()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        // AddItem is structural (IsStructural = !IsItemQuantityUpdate = true)\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new AddItem(\"price_storage\", 1)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.ProrationBehavior == ProrationBehavior.AlwaysInvoice));\n    }\n\n    [Fact]\n    public async Task Run_NonStructuralChange_SetsCreateProrationsProration()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        // UpdateItemQuantity is non-structural\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.ProrationBehavior == ProrationBehavior.CreateProrations));\n    }\n\n    [Fact]\n    public async Task Run_StructuralChange_ChargeAutomatically_SetsPendingIfIncomplete()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            collectionMethod: CollectionMethod.ChargeAutomatically,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new AddItem(\"price_storage\", 1)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.PaymentBehavior == PaymentBehavior.PendingIfIncomplete));\n    }\n\n    [Fact]\n    public async Task Run_StructuralChange_SendInvoice_NoPaymentBehavior()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            collectionMethod: CollectionMethod.SendInvoice,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new AddItem(\"price_storage\", 1)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.PaymentBehavior == null));\n    }\n\n    [Fact]\n    public async Task Run_NonStructuralChange_ChargeAutomatically_NoPaymentBehavior()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            collectionMethod: CollectionMethod.ChargeAutomatically,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.PaymentBehavior == null));\n    }\n\n    [Fact]\n    public async Task Run_AnnualBilling_NonStructural_Active_SetsPendingInvoiceItemInterval()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            status: SubscriptionStatus.Active,\n            billingInterval: Intervals.Year,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.PendingInvoiceItemInterval != null &&\n                options.PendingInvoiceItemInterval.Interval == Intervals.Month));\n    }\n\n    [Fact]\n    public async Task Run_AnnualBilling_NonStructural_Trialing_NoPendingInvoiceItemInterval()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            status: SubscriptionStatus.Trialing,\n            billingInterval: Intervals.Year,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.PendingInvoiceItemInterval == null));\n    }\n\n    [Fact]\n    public async Task Run_AnnualBilling_Structural_NoPendingInvoiceItemInterval()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            status: SubscriptionStatus.Active,\n            billingInterval: Intervals.Year,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new AddItem(\"price_storage\", 1)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.PendingInvoiceItemInterval == null));\n    }\n\n    [Fact]\n    public async Task Run_MonthlyBilling_NonStructural_NoPendingInvoiceItemInterval()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            billingInterval: Intervals.Month,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.PendingInvoiceItemInterval == null));\n    }\n\n    [Fact]\n    public async Task Run_SendInvoice_Structural_DraftInvoice_FinalizesAndSends()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            collectionMethod: CollectionMethod.SendInvoice,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        var updatedSubscription = CreateSubscription(\n            collectionMethod: CollectionMethod.SendInvoice,\n            items: [(\"price_seats\", \"si_1\", 5), (\"price_storage\", \"si_2\", 1)]);\n        updatedSubscription.LatestInvoiceId = \"inv_123\";\n\n        _stripeAdapter\n            .UpdateSubscriptionAsync(subscription.Id, Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(updatedSubscription);\n\n        var draftInvoice = new Invoice { Id = \"inv_123\", Status = InvoiceStatus.Draft };\n        _stripeAdapter.GetInvoiceAsync(\"inv_123\", Arg.Any<InvoiceGetOptions>()).Returns(draftInvoice);\n\n        var finalizedInvoice = new Invoice { Id = \"inv_123\", Status = InvoiceStatus.Open };\n        _stripeAdapter\n            .FinalizeInvoiceAsync(\"inv_123\", Arg.Is<InvoiceFinalizeOptions>(o => o.AutoAdvance == false))\n            .Returns(finalizedInvoice);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new AddItem(\"price_storage\", 1)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).GetInvoiceAsync(\"inv_123\", Arg.Any<InvoiceGetOptions>());\n        await _stripeAdapter.Received(1).FinalizeInvoiceAsync(\"inv_123\", Arg.Any<InvoiceFinalizeOptions>());\n        await _stripeAdapter.Received(1).SendInvoiceAsync(\"inv_123\");\n    }\n\n    [Fact]\n    public async Task Run_SendInvoice_Structural_NonDraftInvoice_DoesNotFinalizeOrSend()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            collectionMethod: CollectionMethod.SendInvoice,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        var updatedSubscription = CreateSubscription(\n            collectionMethod: CollectionMethod.SendInvoice,\n            items: [(\"price_seats\", \"si_1\", 5), (\"price_storage\", \"si_2\", 1)]);\n        updatedSubscription.LatestInvoiceId = \"inv_123\";\n\n        _stripeAdapter\n            .UpdateSubscriptionAsync(subscription.Id, Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(updatedSubscription);\n\n        var openInvoice = new Invoice { Id = \"inv_123\", Status = InvoiceStatus.Open };\n        _stripeAdapter.GetInvoiceAsync(\"inv_123\", Arg.Any<InvoiceGetOptions>()).Returns(openInvoice);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new AddItem(\"price_storage\", 1)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).GetInvoiceAsync(\"inv_123\", Arg.Any<InvoiceGetOptions>());\n        await _stripeAdapter.DidNotReceive().FinalizeInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceFinalizeOptions>());\n        await _stripeAdapter.DidNotReceive().SendInvoiceAsync(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task Run_ChargeAutomatically_Structural_DoesNotProcessInvoice()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            collectionMethod: CollectionMethod.ChargeAutomatically,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        var updatedSubscription = CreateSubscription(\n            collectionMethod: CollectionMethod.ChargeAutomatically,\n            items: [(\"price_seats\", \"si_1\", 5), (\"price_storage\", \"si_2\", 1)]);\n        updatedSubscription.LatestInvoiceId = \"inv_123\";\n\n        _stripeAdapter\n            .UpdateSubscriptionAsync(subscription.Id, Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(updatedSubscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new AddItem(\"price_storage\", 1)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.DidNotReceive().GetInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceGetOptions>());\n        await _stripeAdapter.DidNotReceive().FinalizeInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceFinalizeOptions>());\n        await _stripeAdapter.DidNotReceive().SendInvoiceAsync(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task Run_SendInvoice_NonStructural_DoesNotProcessInvoice()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(\n            collectionMethod: CollectionMethod.SendInvoice,\n            items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.DidNotReceive().GetInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceGetOptions>());\n    }\n\n    [Fact]\n    public async Task Run_NonUSCustomer_NotReverseExempt_UpdatesTaxExemption()\n    {\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Address = new Address { Country = \"DE\" },\n            TaxExempt = TaxExempt.None\n        };\n\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(customer: customer, items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        await _command.Run(organization, changeSet);\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.TaxExempt == TaxExempt.Reverse));\n    }\n\n    [Fact]\n    public async Task Run_NonUSCustomer_AlreadyReverseExempt_DoesNotUpdateTaxExemption()\n    {\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Address = new Address { Country = \"DE\" },\n            TaxExempt = TaxExempt.Reverse\n        };\n\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(customer: customer, items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        await _command.Run(organization, changeSet);\n\n        await _stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Run_USCustomer_DoesNotUpdateTaxExemption()\n    {\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Address = new Address { Country = \"US\" },\n            TaxExempt = TaxExempt.None\n        };\n\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(customer: customer, items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        await _command.Run(organization, changeSet);\n\n        await _stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Run_SwissCustomer_WithNone_DoesNotUpdateTaxExemption()\n    {\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Address = new Address { Country = \"CH\" },\n            TaxExempt = TaxExempt.None\n        };\n\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(customer: customer, items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        await _command.Run(organization, changeSet);\n\n        await _stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Run_SwissCustomer_WithReverse_UpdatesTaxExemptToNone()\n    {\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Address = new Address { Country = \"CH\" },\n            TaxExempt = TaxExempt.Reverse\n        };\n\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(customer: customer, items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        await _command.Run(organization, changeSet);\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.TaxExempt == TaxExempt.None));\n    }\n\n    [Theory]\n    [InlineData(\"CH\")]\n    [InlineData(\"US\")]\n    [InlineData(\"DE\")]\n    public async Task Run_CustomerWithExemptStatus_DoesNotUpdateTaxExemption(string country)\n    {\n        // \"exempt\" is a manual designation (e.g. non-profit) and must never be overwritten automatically.\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Address = new Address { Country = country },\n            TaxExempt = TaxExempt.Exempt\n        };\n\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(customer: customer, items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        await _command.Run(organization, changeSet);\n\n        await _stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Run_CustomerWithNullAddress_DoesNotUpdateTaxExemption()\n    {\n        var customer = new Customer { Id = \"cus_123\", Address = null };\n\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(customer: customer, items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes = [new UpdateItemQuantity(\"price_seats\", 10)]\n        };\n\n        await _command.Run(organization, changeSet);\n\n        await _stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Fact]\n    public async Task Run_MultipleChanges_AllValid_CreatesAllItems()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items:\n        [\n            (\"price_seats\", \"si_1\", 5),\n            (\"price_monthly\", \"si_2\", 5)\n        ]);\n\n        SetupGetSubscription(organization, subscription);\n        SetupUpdateSubscription(subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes =\n            [\n                new UpdateItemQuantity(\"price_seats\", 10),\n                new ChangeItemPrice(\"price_monthly\", \"price_annual\", null),\n                new AddItem(\"price_storage\", 1)\n            ]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.Success);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(subscription.Id,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.Items.Count == 3));\n    }\n\n    [Fact]\n    public async Task Run_MultipleChanges_SecondInvalid_ReturnsBadRequest()\n    {\n        var organization = CreateOrganization();\n        var subscription = CreateSubscription(items: [(\"price_seats\", \"si_1\", 5)]);\n\n        SetupGetSubscription(organization, subscription);\n\n        var changeSet = new OrganizationSubscriptionChangeSet\n        {\n            Changes =\n            [\n                new UpdateItemQuantity(\"price_seats\", 10),\n                new RemoveItem(\"price_nonexistent\")\n            ]\n        };\n\n        var result = await _command.Run(organization, changeSet);\n\n        Assert.True(result.IsT1);\n\n        await _stripeAdapter.DidNotReceive().UpdateSubscriptionAsync(\n            Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n    }\n\n    private static Organization CreateOrganization() => new()\n    {\n        Id = Guid.NewGuid(),\n        GatewaySubscriptionId = \"sub_123\"\n    };\n\n    private static Subscription CreateSubscription(\n        string status = SubscriptionStatus.Active,\n        string collectionMethod = CollectionMethod.ChargeAutomatically,\n        string billingInterval = Intervals.Month,\n        Customer? customer = null,\n        params (string priceId, string itemId, long quantity)[] items)\n    {\n        return new Subscription\n        {\n            Id = \"sub_123\",\n            Status = status,\n            CollectionMethod = collectionMethod,\n            Customer = customer ?? new Customer\n            {\n                Id = \"cus_123\",\n                Address = new Address { Country = \"US\" },\n                TaxExempt = TaxExempt.None\n            },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = items.Select(i => new SubscriptionItem\n                {\n                    Id = i.itemId,\n                    Price = new Price\n                    {\n                        Id = i.priceId,\n                        Recurring = new PriceRecurring { Interval = billingInterval }\n                    },\n                    Quantity = i.quantity\n                }).ToList()\n            }\n        };\n    }\n\n    private void SetupGetSubscription(Organization organization, Subscription subscription)\n    {\n        _stripeAdapter\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n    }\n\n    private void SetupUpdateSubscription(Subscription subscription)\n    {\n        _stripeAdapter\n            .UpdateSubscriptionAsync(subscription.Id, Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(subscription);\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Commands/UpgradeOrganizationPlanVNextCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Organizations.Commands;\n\npublic class UpgradeOrganizationPlanVNextCommandTests\n{\n    private readonly IOrganizationBillingService _organizationBillingService = Substitute.For<IOrganizationBillingService>();\n    private readonly IOrganizationService _organizationService = Substitute.For<IOrganizationService>();\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IUpdateOrganizationSubscriptionCommand _updateOrganizationSubscriptionCommand = Substitute.For<IUpdateOrganizationSubscriptionCommand>();\n    private readonly UpgradeOrganizationPlanVNextCommand _command;\n\n    public UpgradeOrganizationPlanVNextCommandTests()\n    {\n        _command = new UpgradeOrganizationPlanVNextCommand(\n            Substitute.For<ILogger<UpgradeOrganizationPlanVNextCommand>>(),\n            _organizationBillingService,\n            _organizationService,\n            _pricingClient,\n            _updateOrganizationSubscriptionCommand);\n    }\n\n    [Fact]\n    public async Task Run_SamePlan_ReturnsBadRequest()\n    {\n        var organization = CreateOrganization(PlanType.TeamsAnnually);\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var targetPlan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        Assert.True(result.IsT1);\n        Assert.Equal(\"Your organization is already on this plan.\", result.AsT1.Response);\n    }\n\n    [Fact]\n    public async Task Run_Downgrade_ReturnsBadRequest()\n    {\n        var organization = CreateOrganization(PlanType.EnterpriseAnnually);\n        var currentPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n        var targetPlan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        Assert.True(result.IsT1);\n        Assert.Equal(\"You can't downgrade your organization's plan.\", result.AsT1.Response);\n    }\n\n    [Fact]\n    public async Task Run_NoGatewayCustomerId_ReturnsConflict()\n    {\n        var organization = CreateOrganization(PlanType.TeamsAnnually, gatewayCustomerId: null);\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var targetPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        Assert.True(result.IsT2);\n    }\n\n    [Fact]\n    public async Task Run_UpgradeFromFree_FinalizesAndUpdatesOrganization()\n    {\n        var organization = CreateOrganization(\n            PlanType.Free,\n            gatewaySubscriptionId: null,\n            seats: 2);\n        var currentPlan = MockPlans.Get(PlanType.Free);\n        var targetPlan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        Assert.True(result.IsT0);\n        await _organizationBillingService.Received(1).Finalize(Arg.Any<OrganizationSale>());\n        await _organizationService.Received(1).ReplaceAndUpdateCacheAsync(organization, null);\n        Assert.Equal(targetPlan.Name, organization.Plan);\n        Assert.Equal(targetPlan.Type, organization.PlanType);\n        Assert.Equal(targetPlan.PasswordManager.BaseStorageGb, organization.MaxStorageGb);\n        Assert.Null(organization.SmServiceAccounts);\n    }\n\n    [Fact]\n    public async Task Run_UpgradeFromFree_WithKeys_BackfillsKeys()\n    {\n        var organization = CreateOrganization(\n            PlanType.Free,\n            gatewaySubscriptionId: null,\n            seats: 2);\n        var currentPlan = MockPlans.Get(PlanType.Free);\n        var targetPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var keys = new PublicKeyEncryptionKeyPairData(\"wrappedPrivateKey\", \"publicKey\");\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n\n        var result = await _command.Run(organization, targetPlan, keys);\n\n        Assert.True(result.IsT0);\n        Assert.Equal(\"publicKey\", organization.PublicKey);\n        Assert.Equal(\"wrappedPrivateKey\", organization.PrivateKey);\n    }\n\n    [Fact]\n    public async Task Run_UpgradeFromFree_SetsSecretsManagerOnSale()\n    {\n        var organization = CreateOrganization(\n            PlanType.Free,\n            gatewaySubscriptionId: null,\n            seats: 2,\n            useSecretsManager: true,\n            smSeats: 2);\n        var currentPlan = MockPlans.Get(PlanType.Free);\n        var targetPlan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        Assert.True(result.IsT0);\n        await _organizationBillingService.Received(1).Finalize(\n            Arg.Is<OrganizationSale>(s =>\n                s.SubscriptionSetup.SecretsManagerOptions != null));\n    }\n\n    [Fact]\n    public async Task Run_UpgradeFromFree_WithSecretsManager_SetsSmServiceAccountsToNewPlanBase()\n    {\n        var freePlan = MockPlans.Get(PlanType.Free);\n        var organization = CreateOrganization(\n            PlanType.Free,\n            gatewaySubscriptionId: null,\n            seats: 2,\n            useSecretsManager: true,\n            smSeats: 2,\n            smServiceAccounts: freePlan.SecretsManager.BaseServiceAccount);\n        var targetPlan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(freePlan);\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        Assert.True(result.IsT0);\n        Assert.Equal(targetPlan.SecretsManager.BaseServiceAccount, organization.SmServiceAccounts);\n    }\n\n    [Fact]\n    public async Task Run_PaidUpgrade_ChangesPasswordManagerPrice()\n    {\n        var organization = CreateOrganization(PlanType.TeamsAnnually);\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var targetPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        SetupSubscriptionCommandSuccess();\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        await _updateOrganizationSubscriptionCommand.Received(1).Run(\n            organization,\n            Arg.Is<OrganizationSubscriptionChangeSet>(cs =>\n                cs.Changes.Count >= 1 &&\n                cs.Changes.Any(c => c.IsItemPriceChange)));\n        await _organizationService.Received(1).ReplaceAndUpdateCacheAsync(organization, null);\n        Assert.Equal(targetPlan.Name, organization.Plan);\n        Assert.Equal(targetPlan.Type, organization.PlanType);\n    }\n\n    [Fact]\n    public async Task Run_PaidUpgrade_WithExtraStorage_ChangesStoragePrice()\n    {\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var organization = CreateOrganization(\n            PlanType.TeamsAnnually,\n            maxStorageGb: (short)(currentPlan.PasswordManager.BaseStorageGb + 1));\n        var targetPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        SetupSubscriptionCommandSuccess();\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        await _updateOrganizationSubscriptionCommand.Received(1).Run(\n            organization,\n            Arg.Is<OrganizationSubscriptionChangeSet>(cs =>\n                cs.Changes.Count(c => c.IsItemPriceChange) == 2));\n    }\n\n    [Fact]\n    public async Task Run_PaidUpgrade_WithSecretsManager_ChangesSmSeatPrice()\n    {\n        var organization = CreateOrganization(\n            PlanType.TeamsAnnually,\n            useSecretsManager: true,\n            smSeats: 5);\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var targetPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        SetupSubscriptionCommandSuccess();\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        await _updateOrganizationSubscriptionCommand.Received(1).Run(\n            organization,\n            Arg.Is<OrganizationSubscriptionChangeSet>(cs =>\n                cs.Changes.Count(c => c.IsItemPriceChange) >= 2));\n    }\n\n    [Fact]\n    public async Task Run_PaidUpgrade_WithSmServiceAccountsAboveBase_ChangesServiceAccountPrice()\n    {\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var organization = CreateOrganization(\n            PlanType.TeamsAnnually,\n            useSecretsManager: true,\n            smSeats: 5,\n            smServiceAccounts: currentPlan.SecretsManager.BaseServiceAccount + 10);\n\n        var targetPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        SetupSubscriptionCommandSuccess();\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        // PM seat + SM seat + SM service account = 3 price changes\n        await _updateOrganizationSubscriptionCommand.Received(1).Run(\n            organization,\n            Arg.Is<OrganizationSubscriptionChangeSet>(cs =>\n                cs.Changes.Count(c => c.IsItemPriceChange) == 3));\n    }\n\n    [Fact]\n    public async Task Run_PaidUpgrade_UpdatesAllOrganizationPlanProperties()\n    {\n        var organization = CreateOrganization(PlanType.TeamsAnnually);\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var targetPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        SetupSubscriptionCommandSuccess();\n\n        await _command.Run(organization, targetPlan, null);\n\n        Assert.Equal(targetPlan.Name, organization.Plan);\n        Assert.Equal(targetPlan.Type, organization.PlanType);\n        Assert.Equal(targetPlan.PasswordManager.MaxCollections, organization.MaxCollections);\n        Assert.Equal(targetPlan.HasPolicies, organization.UsePolicies);\n        Assert.Equal(targetPlan.HasSso, organization.UseSso);\n        Assert.Equal(targetPlan.HasKeyConnector, organization.UseKeyConnector);\n        Assert.Equal(targetPlan.HasScim, organization.UseScim);\n        Assert.Equal(targetPlan.HasGroups, organization.UseGroups);\n        Assert.Equal(targetPlan.HasDirectory, organization.UseDirectory);\n        Assert.Equal(targetPlan.HasEvents, organization.UseEvents);\n        Assert.Equal(targetPlan.HasTotp, organization.UseTotp);\n        Assert.Equal(targetPlan.Has2fa, organization.Use2fa);\n        Assert.Equal(targetPlan.HasApi, organization.UseApi);\n        Assert.Equal(targetPlan.HasResetPassword, organization.UseResetPassword);\n        Assert.Equal(targetPlan.HasSelfHost, organization.SelfHost);\n        Assert.Equal(targetPlan.UsersGetPremium, organization.UsersGetPremium);\n        Assert.Equal(targetPlan.HasCustomPermissions, organization.UseCustomPermissions);\n        Assert.Equal(targetPlan.HasOrganizationDomains, organization.UseOrganizationDomains);\n        Assert.Equal(targetPlan.AutomaticUserConfirmation, organization.UseAutomaticUserConfirmation);\n        Assert.Equal(targetPlan.HasMyItems, organization.UseMyItems);\n    }\n\n    [Fact]\n    public async Task Run_PaidUpgrade_WithKeys_BackfillsKeys()\n    {\n        var organization = CreateOrganization(PlanType.TeamsAnnually);\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var targetPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n        var keys = new PublicKeyEncryptionKeyPairData(\"wrappedPrivateKey\", \"publicKey\");\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        SetupSubscriptionCommandSuccess();\n\n        await _command.Run(organization, targetPlan, keys);\n\n        Assert.Equal(\"publicKey\", organization.PublicKey);\n        Assert.Equal(\"wrappedPrivateKey\", organization.PrivateKey);\n    }\n\n    [Fact]\n    public async Task Run_PaidUpgrade_WithoutKeys_DoesNotOverwriteKeys()\n    {\n        var organization = CreateOrganization(PlanType.TeamsAnnually);\n        organization.PublicKey = \"existingPublic\";\n        organization.PrivateKey = \"existingPrivate\";\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var targetPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n        SetupSubscriptionCommandSuccess();\n\n        await _command.Run(organization, targetPlan, null);\n\n        Assert.Equal(\"existingPublic\", organization.PublicKey);\n        Assert.Equal(\"existingPrivate\", organization.PrivateKey);\n    }\n\n    [Fact]\n    public async Task Run_PaidUpgrade_CommandFailure_PropagatesResult()\n    {\n        var organization = CreateOrganization(PlanType.TeamsAnnually);\n        var currentPlan = MockPlans.Get(PlanType.TeamsAnnually);\n        var targetPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan);\n\n        BillingCommandResult<Subscription> failureResult = new BadRequest(\"Stripe error\");\n        _updateOrganizationSubscriptionCommand\n            .Run(organization, Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(failureResult);\n\n        var result = await _command.Run(organization, targetPlan, null);\n\n        // Result is mapped through — BadRequest becomes T1\n        Assert.True(result.IsT1);\n        await _organizationService.DidNotReceive().ReplaceAndUpdateCacheAsync(Arg.Any<Organization>(), Arg.Any<EventType?>());\n    }\n\n    private void SetupSubscriptionCommandSuccess()\n    {\n        BillingCommandResult<Subscription> successResult = new Subscription();\n        _updateOrganizationSubscriptionCommand\n            .Run(Arg.Any<Organization>(), Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(successResult);\n    }\n\n    private static Organization CreateOrganization(\n        PlanType planType,\n        string? gatewayCustomerId = \"cus_test123\",\n        string? gatewaySubscriptionId = \"sub_test123\",\n        int? seats = 10,\n        short? maxStorageGb = null,\n        bool useSecretsManager = false,\n        int? smSeats = null,\n        int? smServiceAccounts = null) => new()\n        {\n            Id = Guid.NewGuid(),\n            PlanType = planType,\n            Plan = MockPlans.Get(planType).Name,\n            GatewayCustomerId = gatewayCustomerId,\n            GatewaySubscriptionId = gatewaySubscriptionId,\n            Seats = seats,\n            MaxStorageGb = maxStorageGb,\n            UseSecretsManager = useSecretsManager,\n            SmSeats = smSeats,\n            SmServiceAccounts = smServiceAccounts\n        };\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Models/OrganizationSaleTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Models.Business;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Organizations.Models;\n\npublic class OrganizationSaleTests\n{\n    [Fact]\n    public void From_WithUserCoupons_PopulatesCustomerSetupCoupons()\n    {\n        var organization = new Organization();\n        var signup = new OrganizationSignup\n        {\n            IsFromProvider = false,\n            IsFromSecretsManagerTrial = false,\n            Coupons = new[] { \"COUPON_ONE\", \"COUPON_TWO\" }\n        };\n\n        var sale = OrganizationSale.From(organization, signup);\n\n        Assert.NotNull(sale.CustomerSetup);\n        Assert.Equal(new[] { \"COUPON_ONE\", \"COUPON_TWO\" }, sale.CustomerSetup.Coupons);\n    }\n\n    [Fact]\n    public void From_WithNoCoupons_CustomerSetupCouponsIsNull()\n    {\n        var organization = new Organization();\n        var signup = new OrganizationSignup\n        {\n            IsFromProvider = false,\n            IsFromSecretsManagerTrial = false,\n            Coupons = null\n        };\n\n        var sale = OrganizationSale.From(organization, signup);\n\n        Assert.NotNull(sale.CustomerSetup);\n        Assert.Null(sale.CustomerSetup.Coupons);\n    }\n\n    [Fact]\n    public void From_WithProviderSignup_UsesMSPCouponAndIgnoresUserCoupons()\n    {\n        var organization = new Organization();\n        var signup = new OrganizationSignup\n        {\n            IsFromProvider = true,\n            Coupons = [\"USER_COUPON\"]\n        };\n\n        var sale = OrganizationSale.From(organization, signup);\n\n        Assert.NotNull(sale.CustomerSetup);\n        Assert.Equal(new[] { StripeConstants.CouponIDs.LegacyMSPDiscount }, sale.CustomerSetup.Coupons);\n    }\n\n    [Fact]\n    public void From_WithSMTrialSignup_UsesSMCouponAndIgnoresUserCoupons()\n    {\n        var organization = new Organization();\n        var signup = new OrganizationSignup\n        {\n            IsFromProvider = false,\n            IsFromSecretsManagerTrial = true,\n            Coupons = [\"USER_COUPON\"]\n        };\n\n        var sale = OrganizationSale.From(organization, signup);\n\n        Assert.NotNull(sale.CustomerSetup);\n        Assert.Equal(new[] { StripeConstants.CouponIDs.SecretsManagerStandalone }, sale.CustomerSetup.Coupons);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Models/OrganizationSubscriptionChangeSetTests.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Test.Billing.Mocks;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Organizations.Models;\n\npublic class OrganizationSubscriptionChangeTests\n{\n    [Fact]\n    public void ImplicitConversion_AddItem_SetsCorrectFlags()\n    {\n        OrganizationSubscriptionChange change = new AddItem(\"price_123\", 5);\n\n        Assert.True(change.IsItemAddition);\n        Assert.False(change.IsItemPriceChange);\n        Assert.False(change.IsItemRemoval);\n        Assert.False(change.IsItemQuantityUpdate);\n        Assert.True(change.IsStructural);\n    }\n\n    [Fact]\n    public void ImplicitConversion_ChangeItemPrice_SetsCorrectFlags()\n    {\n        OrganizationSubscriptionChange change = new ChangeItemPrice(\"price_old\", \"price_new\", null);\n\n        Assert.False(change.IsItemAddition);\n        Assert.True(change.IsItemPriceChange);\n        Assert.False(change.IsItemRemoval);\n        Assert.False(change.IsItemQuantityUpdate);\n        Assert.True(change.IsStructural);\n    }\n\n    [Fact]\n    public void ImplicitConversion_RemoveItem_SetsCorrectFlags()\n    {\n        OrganizationSubscriptionChange change = new RemoveItem(\"price_123\");\n\n        Assert.False(change.IsItemAddition);\n        Assert.False(change.IsItemPriceChange);\n        Assert.True(change.IsItemRemoval);\n        Assert.False(change.IsItemQuantityUpdate);\n        Assert.True(change.IsStructural);\n    }\n\n    [Fact]\n    public void ImplicitConversion_UpdateItemQuantity_SetsCorrectFlags()\n    {\n        OrganizationSubscriptionChange change = new UpdateItemQuantity(\"price_123\", 10);\n\n        Assert.False(change.IsItemAddition);\n        Assert.False(change.IsItemPriceChange);\n        Assert.False(change.IsItemRemoval);\n        Assert.True(change.IsItemQuantityUpdate);\n        Assert.False(change.IsStructural);\n    }\n\n    [Fact]\n    public void ImplicitConversion_UpdateItemQuantityToZero_IsStructural()\n    {\n        OrganizationSubscriptionChange change = new UpdateItemQuantity(\"price_123\", 0);\n\n        Assert.True(change.IsItemQuantityUpdate);\n        Assert.True(change.IsStructural);\n    }\n}\n\npublic class OrganizationSubscriptionChangeSetTests\n{\n    [Fact]\n    public void UpdatePasswordManagerSeats_CreatesCorrectChangeSet()\n    {\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        var changeSet = OrganizationSubscriptionChangeSet.UpdatePasswordManagerSeats(plan, 25);\n\n        var change = Assert.Single(changeSet.Changes);\n        Assert.True(change.IsItemQuantityUpdate);\n        Assert.False(change.IsStructural);\n\n        var update = change.AsT3;\n        Assert.Equal(plan.PasswordManager.StripeSeatPlanId, update.PriceId);\n        Assert.Equal(25, update.Quantity);\n    }\n\n    [Fact]\n    public void UpdateStorage_CreatesCorrectChangeSet()\n    {\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        var changeSet = OrganizationSubscriptionChangeSet.UpdateStorage(plan, 3);\n\n        var change = Assert.Single(changeSet.Changes);\n        Assert.True(change.IsItemQuantityUpdate);\n\n        var update = change.AsT3;\n        Assert.Equal(plan.PasswordManager.StripeStoragePlanId, update.PriceId);\n        Assert.Equal(3, update.Quantity);\n    }\n\n    [Fact]\n    public void UpdateSecretsManagerSeats_CreatesCorrectChangeSet()\n    {\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        var changeSet = OrganizationSubscriptionChangeSet.UpdateSecretsManagerSeats(plan, 10);\n\n        var change = Assert.Single(changeSet.Changes);\n        Assert.True(change.IsItemQuantityUpdate);\n\n        var update = change.AsT3;\n        Assert.Equal(plan.SecretsManager.StripeSeatPlanId, update.PriceId);\n        Assert.Equal(10, update.Quantity);\n    }\n\n    [Fact]\n    public void UpdateSecretsManagerServiceAccounts_CreatesCorrectChangeSet()\n    {\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        var changeSet = OrganizationSubscriptionChangeSet.UpdateSecretsManagerServiceAccounts(plan, 50);\n\n        var change = Assert.Single(changeSet.Changes);\n        Assert.True(change.IsItemQuantityUpdate);\n\n        var update = change.AsT3;\n        Assert.Equal(plan.SecretsManager.StripeServiceAccountPlanId, update.PriceId);\n        Assert.Equal(50, update.Quantity);\n    }\n}\n\npublic class OrganizationSubscriptionChangeSetBuilderTests\n{\n    [Fact]\n    public void AddItem_AddsToChanges()\n    {\n        var changeSet = new OrganizationSubscriptionChangeSetBuilder()\n            .AddItem(\"price_add\", 3)\n            .Build();\n\n        var change = Assert.Single(changeSet.Changes);\n        Assert.True(change.IsItemAddition);\n\n        var item = change.AsT0;\n        Assert.Equal(\"price_add\", item.PriceId);\n        Assert.Equal(3, item.Quantity);\n    }\n\n    [Fact]\n    public void ChangeItemPrice_AddsToChanges()\n    {\n        var changeSet = new OrganizationSubscriptionChangeSetBuilder()\n            .ChangeItemPrice(\"price_old\", \"price_new\")\n            .Build();\n\n        var change = Assert.Single(changeSet.Changes);\n        Assert.True(change.IsItemPriceChange);\n\n        var item = change.AsT1;\n        Assert.Equal(\"price_old\", item.CurrentPriceId);\n        Assert.Equal(\"price_new\", item.UpdatedPriceId);\n        Assert.Null(item.Quantity);\n    }\n\n    [Fact]\n    public void ChangeItemPrice_WithQuantity_AddsToChanges()\n    {\n        var changeSet = new OrganizationSubscriptionChangeSetBuilder()\n            .ChangeItemPrice(\"price_old\", \"price_new\", 7)\n            .Build();\n\n        var change = Assert.Single(changeSet.Changes);\n        var item = change.AsT1;\n        Assert.Equal(7, item.Quantity);\n    }\n\n    [Fact]\n    public void RemoveItem_AddsToChanges()\n    {\n        var changeSet = new OrganizationSubscriptionChangeSetBuilder()\n            .RemoveItem(\"price_remove\")\n            .Build();\n\n        var change = Assert.Single(changeSet.Changes);\n        Assert.True(change.IsItemRemoval);\n\n        var item = change.AsT2;\n        Assert.Equal(\"price_remove\", item.PriceId);\n    }\n\n    [Fact]\n    public void UpdateItemQuantity_AddsToChanges()\n    {\n        var changeSet = new OrganizationSubscriptionChangeSetBuilder()\n            .UpdateItemQuantity(\"price_qty\", 15)\n            .Build();\n\n        var change = Assert.Single(changeSet.Changes);\n        Assert.True(change.IsItemQuantityUpdate);\n\n        var item = change.AsT3;\n        Assert.Equal(\"price_qty\", item.PriceId);\n        Assert.Equal(15, item.Quantity);\n    }\n\n    [Fact]\n    public void Build_WithMultipleChanges_PreservesOrder()\n    {\n        var changeSet = new OrganizationSubscriptionChangeSetBuilder()\n            .AddItem(\"price_1\", 1)\n            .RemoveItem(\"price_2\")\n            .ChangeItemPrice(\"price_3\", \"price_4\")\n            .UpdateItemQuantity(\"price_5\", 10)\n            .Build();\n\n        Assert.Equal(4, changeSet.Changes.Count);\n        Assert.True(changeSet.Changes[0].IsItemAddition);\n        Assert.True(changeSet.Changes[1].IsItemRemoval);\n        Assert.True(changeSet.Changes[2].IsItemPriceChange);\n        Assert.True(changeSet.Changes[3].IsItemQuantityUpdate);\n    }\n\n    [Fact]\n    public void Build_WithNoChanges_ReturnsEmptyChangeSet()\n    {\n        var changeSet = new OrganizationSubscriptionChangeSetBuilder()\n            .Build();\n\n        Assert.Empty(changeSet.Changes);\n    }\n\n    [Fact]\n    public void Build_ReturnsReadOnlyChanges()\n    {\n        var changeSet = new OrganizationSubscriptionChangeSetBuilder()\n            .AddItem(\"price_1\", 1)\n            .Build();\n\n        Assert.IsAssignableFrom<IReadOnlyList<OrganizationSubscriptionChange>>(changeSet.Changes);\n    }\n\n    [Fact]\n    public void Build_MixedStructuralAndNonStructural()\n    {\n        var changeSet = new OrganizationSubscriptionChangeSetBuilder()\n            .AddItem(\"price_add\", 1)\n            .UpdateItemQuantity(\"price_qty\", 5)\n            .Build();\n\n        Assert.Equal(2, changeSet.Changes.Count);\n        Assert.True(changeSet.Changes[0].IsStructural);\n        Assert.False(changeSet.Changes[1].IsStructural);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Platform.Installations;\nusing Bit.Core.Test.AutoFixture;\nusing Bit.Core.Test.Billing.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Organizations.Queries;\n\n[SubscriptionInfoCustomize]\n[OrganizationLicenseCustomize]\n[SutProviderCustomize]\npublic class GetCloudOrganizationLicenseQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetLicenseAsync_InvalidInstallationId_Throws(\n        SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,\n        Organization organization, Guid installationId, int version)\n    {\n        sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).ReturnsNull();\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetLicenseAsync(organization, installationId, version));\n        Assert.Contains(\"Invalid installation id\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetLicenseAsync_DisabledOrganization_Throws(\n        SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,\n        Organization organization, Guid installationId, Installation installation)\n    {\n        installation.Enabled = false;\n        sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetLicenseAsync(organization, installationId));\n        Assert.Contains(\"Invalid installation id\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetLicenseAsync_CreatesAndReturns(SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,\n        Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,\n        byte[] licenseSignature)\n    {\n        installation.Enabled = true;\n        sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);\n        sutProvider.GetDependency<IStripePaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);\n        sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);\n\n        var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);\n\n        Assert.Equal(LicenseType.Organization, result.LicenseType);\n        Assert.Equal(organization.Id, result.Id);\n        Assert.Equal(installationId, result.InstallationId);\n        Assert.Equal(licenseSignature, result.SignatureBytes);\n        Assert.Equal(string.Empty, result.Token);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetLicenseAsync_WhenFeatureFlagEnabled_CreatesToken(\n        SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,\n        Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,\n        byte[] licenseSignature, string token)\n    {\n        installation.Enabled = true;\n        sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);\n        sutProvider.GetDependency<IStripePaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);\n        sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);\n        sutProvider.GetDependency<ILicensingService>()\n            .CreateOrganizationTokenAsync(organization, installationId, subInfo)\n            .Returns(token);\n\n        var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);\n\n        Assert.Equal(token, result.Token);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription(\n        SutProvider<GetCloudOrganizationLicenseQuery> sutProvider,\n        Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,\n        byte[] licenseSignature, Provider provider)\n    {\n        organization.Status = OrganizationStatusType.Managed;\n        organization.ExpirationDate = null;\n\n        subInfo.Subscription = new SubscriptionInfo.BillingSubscription(new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        CurrentPeriodStart = DateTime.UtcNow,\n                        CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)\n                    }\n                ]\n            }\n        });\n\n        installation.Enabled = true;\n        sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);\n        sutProvider.GetDependency<IStripePaymentService>().GetSubscriptionAsync(provider).Returns(subInfo);\n        sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);\n\n        var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);\n\n        Assert.Equal(LicenseType.Organization, result.LicenseType);\n        Assert.Equal(organization.Id, result.Id);\n        Assert.Equal(installationId, result.InstallationId);\n        Assert.Equal(licenseSignature, result.SignatureBytes);\n        Assert.Equal(DateTime.UtcNow.AddYears(1).Date, result.Expires!.Value.Date);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Organizations.Queries;\n\n[SutProviderCustomize]\npublic class GetOrganizationMetadataQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task Run_SelfHosted_ReturnsDefault(\n        Organization organization,\n        SutProvider<GetOrganizationMetadataQuery> sutProvider)\n    {\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);\n\n        var result = await sutProvider.Sut.Run(organization);\n\n        Assert.Equal(OrganizationMetadata.Default, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NoGatewaySubscriptionId_ReturnsDefaultWithOccupiedSeats(\n        Organization organization,\n        SutProvider<GetOrganizationMetadataQuery> sutProvider)\n    {\n        organization.GatewaySubscriptionId = null;\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Users = 10, Sponsored = 0 });\n\n        var result = await sutProvider.Sut.Run(organization);\n\n        Assert.NotNull(result);\n        Assert.False(result.IsOnSecretsManagerStandalone);\n        Assert.Equal(10, result.OrganizationOccupiedSeats);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NullCustomer_ReturnsDefaultWithOccupiedSeats(\n        Organization organization,\n        SutProvider<GetOrganizationMetadataQuery> sutProvider)\n    {\n        organization.GatewaySubscriptionId = \"sub_123\";\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Users = 5, Sponsored = 0 });\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomer(organization)\n            .ReturnsNull();\n\n        var result = await sutProvider.Sut.Run(organization);\n\n        Assert.NotNull(result);\n        Assert.False(result.IsOnSecretsManagerStandalone);\n        Assert.Equal(5, result.OrganizationOccupiedSeats);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NullSubscription_ReturnsDefaultWithOccupiedSeats(\n        Organization organization,\n        SutProvider<GetOrganizationMetadataQuery> sutProvider)\n    {\n        organization.GatewaySubscriptionId = \"sub_123\";\n\n        var customer = new Customer();\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Users = 7, Sponsored = 0 });\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomer(organization)\n            .Returns(customer);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.Contains(\"discounts.coupon.applies_to\")))\n            .ReturnsNull();\n\n        var result = await sutProvider.Sut.Run(organization);\n\n        Assert.NotNull(result);\n        Assert.False(result.IsOnSecretsManagerStandalone);\n        Assert.Equal(7, result.OrganizationOccupiedSeats);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithSecretsManagerStandaloneCoupon_ReturnsMetadataWithFlag(\n        Organization organization,\n        SutProvider<GetOrganizationMetadataQuery> sutProvider)\n    {\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.PlanType = PlanType.EnterpriseAnnually;\n\n        var productId = \"product_123\";\n        var customer = new Customer();\n\n        var subscription = new Subscription\n        {\n            Discounts =\n            [\n                new Discount\n                {\n                    Coupon = new Coupon\n                    {\n                        Id = StripeConstants.CouponIDs.SecretsManagerStandalone,\n                        AppliesTo = new CouponAppliesTo\n                        {\n                            Products = [productId]\n                        }\n                    }\n                }\n            ],\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Plan = new Plan\n                        {\n                            ProductId = productId\n                        }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Users = 15, Sponsored = 0 });\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomer(organization)\n            .Returns(customer);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.Contains(\"discounts.coupon.applies_to\")))\n            .Returns(subscription);\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        var result = await sutProvider.Sut.Run(organization);\n\n        Assert.NotNull(result);\n        Assert.True(result.IsOnSecretsManagerStandalone);\n        Assert.Equal(15, result.OrganizationOccupiedSeats);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithoutSecretsManagerStandaloneCoupon_ReturnsMetadataWithoutFlag(\n        Organization organization,\n        SutProvider<GetOrganizationMetadataQuery> sutProvider)\n    {\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.PlanType = PlanType.TeamsAnnually;\n\n        var customer = new Customer();\n\n        var subscription = new Subscription\n        {\n            Discounts = null,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Plan = new Plan\n                        {\n                            ProductId = \"product_123\"\n                        }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Users = 20, Sponsored = 0 });\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomer(organization)\n            .Returns(customer);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.Contains(\"discounts.coupon.applies_to\")))\n            .Returns(subscription);\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        var result = await sutProvider.Sut.Run(organization);\n\n        Assert.NotNull(result);\n        Assert.False(result.IsOnSecretsManagerStandalone);\n        Assert.Equal(20, result.OrganizationOccupiedSeats);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_CouponDoesNotApplyToSubscriptionProducts_ReturnsFalseForStandaloneFlag(\n        Organization organization,\n        SutProvider<GetOrganizationMetadataQuery> sutProvider)\n    {\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.PlanType = PlanType.EnterpriseAnnually;\n\n        var customer = new Customer();\n\n        var subscription = new Subscription\n        {\n            Discounts =\n            [\n                new Discount\n                {\n                    Coupon = new Coupon\n                    {\n                        Id = StripeConstants.CouponIDs.SecretsManagerStandalone,\n                        AppliesTo = new CouponAppliesTo\n                        {\n                            Products = [\"different_product_id\"]\n                        }\n                    }\n                }\n            ],\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Plan = new Plan\n                        {\n                            ProductId = \"product_123\"\n                        }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Users = 12, Sponsored = 0 });\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomer(organization)\n            .Returns(customer);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.Contains(\"discounts.coupon.applies_to\")))\n            .Returns(subscription);\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        var result = await sutProvider.Sut.Run(organization);\n\n        Assert.NotNull(result);\n        Assert.False(result.IsOnSecretsManagerStandalone);\n        Assert.Equal(12, result.OrganizationOccupiedSeats);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_PlanDoesNotSupportSecretsManager_ReturnsFalseForStandaloneFlag(\n        Organization organization,\n        SutProvider<GetOrganizationMetadataQuery> sutProvider)\n    {\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.PlanType = PlanType.FamiliesAnnually;\n\n        var productId = \"product_123\";\n        var customer = new Customer();\n\n        var subscription = new Subscription\n        {\n            Discounts =\n            [\n                new Discount\n                {\n                    Coupon = new Coupon\n                    {\n                        Id = StripeConstants.CouponIDs.SecretsManagerStandalone,\n                        AppliesTo = new CouponAppliesTo\n                        {\n                            Products = [productId]\n                        }\n                    }\n                }\n            ],\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Plan = new Plan\n                        {\n                            ProductId = productId\n                        }\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Users = 8, Sponsored = 0 });\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomer(organization)\n            .Returns(customer);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.Contains(\"discounts.coupon.applies_to\")))\n            .Returns(subscription);\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        var result = await sutProvider.Sut.Run(organization);\n\n        Assert.NotNull(result);\n        Assert.False(result.IsOnSecretsManagerStandalone);\n        Assert.Equal(8, result.OrganizationOccupiedSeats);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Stripe.Tax;\nusing Stripe.TestHelpers;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Organizations.Queries;\n\nusing static StripeConstants;\n\n[SutProviderCustomize]\npublic class GetOrganizationWarningsQueryTests\n{\n    private static readonly string[] _requiredExpansions = [\"customer.tax_ids\", \"latest_invoice\", \"test_clock\"];\n\n    [Theory, BitAutoData]\n    public async Task Run_NoSubscription_NoWarnings(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .ReturnsNull();\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            FreeTrial: null,\n            InactiveSubscription: null,\n            ResellerRenewal: null\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_FreeTrialWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        var now = DateTime.UtcNow;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Trialing,\n                TrialEnd = now.AddDays(7),\n                Customer = new Customer\n                {\n                    InvoiceSettings = new CustomerInvoiceSettings(),\n                    Metadata = new Dictionary<string, string>()\n                },\n                TestClock = new TestClock\n                {\n                    FrozenTime = now\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);\n        sutProvider.GetDependency<IHasPaymentMethodQuery>().Run(organization).Returns(false);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            FreeTrial.RemainingTrialDays: 7\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_FreeTrialWarning_WithPaymentMethod_NoWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        var now = DateTime.UtcNow;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Trialing,\n                TrialEnd = now.AddDays(7),\n                Customer = new Customer\n                {\n                    InvoiceSettings = new CustomerInvoiceSettings(),\n                    Metadata = new Dictionary<string, string>()\n                },\n                TestClock = new TestClock\n                {\n                    FrozenTime = now\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);\n        sutProvider.GetDependency<IHasPaymentMethodQuery>().Run(organization).Returns(true);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.FreeTrial);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_OrganizationEnabled_NoInactiveSubscriptionWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.Enabled = true;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Status = SubscriptionStatus.Unpaid,\n                Customer = new Customer\n                {\n                    InvoiceSettings = new CustomerInvoiceSettings(),\n                    Metadata = new Dictionary<string, string>()\n                }\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.InactiveSubscription);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_InactiveSubscriptionWarning_ContactProvider(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.Enabled = false;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Customer = new Customer(),\n                Status = SubscriptionStatus.Unpaid\n            });\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)\n            .Returns(new Provider());\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            InactiveSubscription.Resolution: \"contact_provider\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethod(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.Enabled = false;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Customer = new Customer(),\n                Status = SubscriptionStatus.Unpaid\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            InactiveSubscription.Resolution: \"add_payment_method\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_InactiveSubscriptionWarning_Resubscribe(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.Enabled = false;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Customer = new Customer(),\n                Status = SubscriptionStatus.Canceled\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            InactiveSubscription.Resolution: \"resubscribe\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_InactiveSubscriptionWarning_ContactOwner(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.Enabled = false;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Customer = new Customer(),\n                Status = SubscriptionStatus.Unpaid\n            });\n\n        sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(false);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            InactiveSubscription.Resolution: \"contact_owner\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_ResellerRenewalWarning_Upcoming(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        var now = DateTime.UtcNow;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                CollectionMethod = CollectionMethod.SendInvoice,\n                Customer = new Customer(),\n                Status = SubscriptionStatus.Active,\n                Items = new StripeList<SubscriptionItem>\n                {\n                    Data =\n                    [\n                        new SubscriptionItem\n                        {\n                            CurrentPeriodEnd = now.AddDays(10)\n                        }\n                    ]\n                },\n                TestClock = new TestClock\n                {\n                    FrozenTime = now\n                }\n            });\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)\n            .Returns(new Provider\n            {\n                Type = ProviderType.Reseller\n            });\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            ResellerRenewal.Type: \"upcoming\"\n        });\n\n        Assert.Equal(now.AddDays(10), response.ResellerRenewal.Upcoming!.RenewalDate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_ResellerRenewalWarning_Issued(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        var now = DateTime.UtcNow;\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                CollectionMethod = CollectionMethod.SendInvoice,\n                Customer = new Customer(),\n                Status = SubscriptionStatus.Active,\n                LatestInvoice = new Invoice\n                {\n                    Status = InvoiceStatus.Open,\n                    DueDate = now.AddDays(30),\n                    Created = now\n                },\n                TestClock = new TestClock\n                {\n                    FrozenTime = now\n                }\n            });\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)\n            .Returns(new Provider\n            {\n                Type = ProviderType.Reseller\n            });\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            ResellerRenewal.Type: \"issued\"\n        });\n\n        Assert.Equal(now, response.ResellerRenewal.Issued!.IssuedDate);\n        Assert.Equal(now.AddDays(30), response.ResellerRenewal.Issued!.DueDate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_ResellerRenewalWarning_PastDue(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        var now = DateTime.UtcNow;\n\n        const string subscriptionId = \"subscription_id\";\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>\n                options.Expand.SequenceEqual(_requiredExpansions)\n            ))\n            .Returns(new Subscription\n            {\n                Id = subscriptionId,\n                CollectionMethod = CollectionMethod.SendInvoice,\n                Customer = new Customer(),\n                Status = SubscriptionStatus.PastDue,\n                TestClock = new TestClock\n                {\n                    FrozenTime = now\n                }\n            });\n\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id)\n            .Returns(new Provider\n            {\n                Type = ProviderType.Reseller\n            });\n\n        var dueDate = now.AddDays(-10);\n\n        sutProvider.GetDependency<IStripeAdapter>().SearchInvoiceAsync(Arg.Is<InvoiceSearchOptions>(options =>\n            options.Query == $\"subscription:'{subscriptionId}' status:'open'\")).Returns([\n            new Invoice { DueDate = dueDate, Created = dueDate.AddDays(-30) }\n        ]);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            ResellerRenewal.Type: \"past_due\"\n        });\n\n        Assert.Equal(dueDate.AddDays(30), response.ResellerRenewal.PastDue!.SuspensionDate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_USCustomer_NoTaxIdWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"US\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_CHCustomer_NoTaxIdWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CH\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_FreeCustomer_NoTaxIdWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.Free;\n\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CA\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NotOwner_NoTaxIdWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.TeamsAnnually;\n\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CA\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organization.Id)\n            .Returns(false);\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_HasProvider_NoTaxIdWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.TeamsAnnually;\n\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CA\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organization.Id)\n            .Returns(true);\n\n        sutProvider.GetDependency<IProviderRepository>()\n            .GetByOrganizationIdAsync(organization.Id)\n            .Returns(new Provider());\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NoRegistrationInCountry_NoTaxIdWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.TeamsAnnually;\n\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CA\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organization.Id)\n            .Returns(true);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = new List<Registration>\n                {\n                    new() { Country = \"GB\" }\n                }\n            });\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_TaxIdWarning_Missing(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.TeamsAnnually;\n\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CA\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId>() },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organization.Id)\n            .Returns(true);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = new List<Registration>\n                {\n                    new() { Country = \"CA\" }\n                }\n            });\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            TaxId.Type: \"tax_id_missing\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_TaxIdWarning_PendingVerification(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.EnterpriseAnnually;\n\n        var taxId = new TaxId\n        {\n            Verification = new TaxIdVerification\n            {\n                Status = TaxIdVerificationStatus.Pending\n            }\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CA\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organization.Id)\n            .Returns(true);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = new List<Registration>\n                {\n                    new() { Country = \"CA\" }\n                }\n            });\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            TaxId.Type: \"tax_id_pending_verification\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_Has_TaxIdWarning_FailedVerification(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.TeamsAnnually;\n\n        var taxId = new TaxId\n        {\n            Verification = new TaxIdVerification\n            {\n                Status = TaxIdVerificationStatus.Unverified\n            }\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CA\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organization.Id)\n            .Returns(true);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = new List<Registration>\n                {\n                    new() { Country = \"CA\" }\n                }\n            });\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.True(response is\n        {\n            TaxId.Type: \"tax_id_failed_verification\"\n        });\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_VerifiedTaxId_NoTaxIdWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.TeamsAnnually;\n\n        var taxId = new TaxId\n        {\n            Verification = new TaxIdVerification\n            {\n                Status = TaxIdVerificationStatus.Verified\n            }\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CA\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organization.Id)\n            .Returns(true);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = new List<Registration>\n                {\n                    new() { Country = \"CA\" }\n                }\n            });\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.TaxId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NullVerification_NoTaxIdWarning(\n        Organization organization,\n        SutProvider<GetOrganizationWarningsQuery> sutProvider)\n    {\n        organization.PlanType = PlanType.TeamsAnnually;\n\n        var taxId = new TaxId\n        {\n            Verification = null\n        };\n\n        var subscription = new Subscription\n        {\n            Customer = new Customer\n            {\n                Address = new Address { Country = \"CA\" },\n                TaxIds = new StripeList<TaxId> { Data = new List<TaxId> { taxId } },\n                InvoiceSettings = new CustomerInvoiceSettings(),\n                Metadata = new Dictionary<string, string>()\n            }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetSubscription(organization, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<ICurrentContext>()\n            .OrganizationOwner(organization.Id)\n            .Returns(true);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())\n            .Returns(new StripeList<Registration>\n            {\n                Data = new List<Registration>\n                {\n                    new() { Country = \"CA\" }\n                }\n            });\n\n        var response = await sutProvider.Sut.Run(organization);\n\n        Assert.Null(response.TaxId);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQueryTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Queries;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Billing.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Organizations.Queries;\n\n[SutProviderCustomize]\npublic class GetSelfHostedOrganizationLicenseQueryTests\n{\n    private static SutProvider<GetSelfHostedOrganizationLicenseQuery> GetSutProvider(BillingSyncConfig config,\n        string apiResponse = null)\n    {\n        return new SutProvider<GetSelfHostedOrganizationLicenseQuery>()\n            .ConfigureBaseIdentityClientService($\"licenses/organization/{config.CloudOrganizationId}\",\n                HttpMethod.Get, apiResponse: apiResponse);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationLicenseCustomize]\n    public async Task GetLicenseAsync_Success(Organization organization,\n        OrganizationConnection<BillingSyncConfig> billingSyncConnection, BillingSyncConfig config, OrganizationLicense license)\n    {\n        var sutProvider = GetSutProvider(config, JsonSerializer.Serialize(license));\n        billingSyncConnection.Enabled = true;\n        billingSyncConnection.Config = config;\n\n        var result = await sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection);\n        AssertHelper.AssertPropertyEqual(result, license);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetLicenseAsync_WhenNotSelfHosted_Throws(Organization organization,\n        OrganizationConnection billingSyncConnection, BillingSyncConfig config)\n    {\n        var sutProvider = GetSutProvider(config);\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection));\n        Assert.Contains(\"only available for self-hosted\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetLicenseAsync_WhenCloudCommunicationDisabled_Throws(Organization organization,\n        OrganizationConnection billingSyncConnection, BillingSyncConfig config)\n    {\n        var sutProvider = GetSutProvider(config);\n        sutProvider.GetDependency<IGlobalSettings>().EnableCloudCommunication = false;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection));\n        Assert.Contains(\"Cloud communication is disabled\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetLicenseAsync_WhenCantUseConnection_Throws(Organization organization,\n        OrganizationConnection<BillingSyncConfig> billingSyncConnection, BillingSyncConfig config)\n    {\n        var sutProvider = GetSutProvider(config);\n        billingSyncConnection.Enabled = false;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection));\n        Assert.Contains(\"Connection disabled\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetLicenseAsync_WhenNullResponse_Throws(Organization organization,\n        OrganizationConnection<BillingSyncConfig> billingSyncConnection, BillingSyncConfig config)\n    {\n        var sutProvider = GetSutProvider(config);\n        billingSyncConnection.Enabled = true;\n        billingSyncConnection.Config = config;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection));\n        Assert.Contains(\"An error has occurred. Check your internet connection and ensure the billing token is correct.\",\n            exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Payment.Clients;\nusing Bit.Core.Billing.Payment.Commands;\nusing Bit.Core.Entities;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\nusing Invoice = BitPayLight.Models.Invoice.Invoice;\n\nnamespace Bit.Core.Test.Billing.Payment.Commands;\n\nusing static BitPayConstants;\n\npublic class CreateBitPayInvoiceForCreditCommandTests\n{\n    private readonly IBitPayClient _bitPayClient = Substitute.For<IBitPayClient>();\n    private readonly GlobalSettings _globalSettings = new()\n    {\n        BitPay = new GlobalSettings.BitPaySettings\n        {\n            NotificationUrl = \"https://example.com/bitpay/notification\",\n            WebhookKey = \"test-webhook-key\"\n        }\n    };\n    private const string _redirectUrl = \"https://bitwarden.com/redirect\";\n    private readonly CreateBitPayInvoiceForCreditCommand _command;\n\n    public CreateBitPayInvoiceForCreditCommandTests()\n    {\n        _command = new CreateBitPayInvoiceForCreditCommand(\n            _bitPayClient,\n            _globalSettings,\n            Substitute.For<ILogger<CreateBitPayInvoiceForCreditCommand>>());\n    }\n\n    [Fact]\n    public async Task Run_User_CreatesInvoice_ReturnsInvoiceUrl()\n    {\n        var user = new User { Id = Guid.NewGuid(), Email = \"user@gmail.com\" };\n\n        _bitPayClient.CreateInvoice(Arg.Is<Invoice>(options =>\n            options.Buyer.Email == user.Email &&\n            options.Buyer.Name == user.Email &&\n            options.NotificationUrl == $\"{_globalSettings.BitPay.NotificationUrl}?key={_globalSettings.BitPay.WebhookKey}\" &&\n            options.PosData == $\"userId:{user.Id},{PosDataKeys.AccountCredit}\" &&\n            // ReSharper disable once CompareOfFloatsByEqualityOperator\n            options.Price == Convert.ToDouble(10M) &&\n            options.RedirectUrl == _redirectUrl)).Returns(new Invoice { Url = \"https://bitpay.com/invoice/123\" });\n\n        var result = await _command.Run(user, 10M, _redirectUrl);\n\n        Assert.True(result.IsT0);\n        var invoiceUrl = result.AsT0;\n        Assert.Equal(\"https://bitpay.com/invoice/123\", invoiceUrl);\n    }\n\n    [Fact]\n    public async Task Run_Organization_CreatesInvoice_ReturnsInvoiceUrl()\n    {\n        var organization = new Organization { Id = Guid.NewGuid(), BillingEmail = \"organization@example.com\", Name = \"Organization\" };\n\n        _bitPayClient.CreateInvoice(Arg.Is<Invoice>(options =>\n            options.Buyer.Email == organization.BillingEmail &&\n            options.Buyer.Name == organization.Name &&\n            options.NotificationUrl == $\"{_globalSettings.BitPay.NotificationUrl}?key={_globalSettings.BitPay.WebhookKey}\" &&\n            options.PosData == $\"organizationId:{organization.Id},{PosDataKeys.AccountCredit}\" &&\n            // ReSharper disable once CompareOfFloatsByEqualityOperator\n            options.Price == Convert.ToDouble(10M) &&\n            options.RedirectUrl == _redirectUrl)).Returns(new Invoice { Url = \"https://bitpay.com/invoice/123\" });\n\n        var result = await _command.Run(organization, 10M, _redirectUrl);\n\n        Assert.True(result.IsT0);\n        var invoiceUrl = result.AsT0;\n        Assert.Equal(\"https://bitpay.com/invoice/123\", invoiceUrl);\n    }\n\n    [Fact]\n    public async Task Run_Provider_CreatesInvoice_ReturnsInvoiceUrl()\n    {\n        var provider = new Provider { Id = Guid.NewGuid(), BillingEmail = \"organization@example.com\", Name = \"Provider\" };\n\n        _bitPayClient.CreateInvoice(Arg.Is<Invoice>(options =>\n            options.Buyer.Email == provider.BillingEmail &&\n            options.Buyer.Name == provider.Name &&\n            options.NotificationUrl == $\"{_globalSettings.BitPay.NotificationUrl}?key={_globalSettings.BitPay.WebhookKey}\" &&\n            options.PosData == $\"providerId:{provider.Id},{PosDataKeys.AccountCredit}\" &&\n            // ReSharper disable once CompareOfFloatsByEqualityOperator\n            options.Price == Convert.ToDouble(10M) &&\n            options.RedirectUrl == _redirectUrl)).Returns(new Invoice { Url = \"https://bitpay.com/invoice/123\" });\n\n        var result = await _command.Run(provider, 10M, _redirectUrl);\n\n        Assert.True(result.IsT0);\n        var invoiceUrl = result.AsT0;\n        Assert.Equal(\"https://bitpay.com/invoice/123\", invoiceUrl);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Commands;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Test.Billing.Extensions;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Payment.Commands;\n\nusing static StripeConstants;\n\npublic class UpdateBillingAddressCommandTests\n{\n    private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly UpdateBillingAddressCommand _command;\n\n    public UpdateBillingAddressCommandTests()\n    {\n        _command = new UpdateBillingAddressCommand(\n            Substitute.For<ILogger<UpdateBillingAddressCommand>>(),\n            _subscriberService,\n            _stripeAdapter);\n    }\n\n    [Fact]\n    public async Task Run_PersonalOrganization_MakesCorrectInvocations_ReturnsBillingAddress()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.FamiliesAnnually,\n            GatewayCustomerId = \"cus_123\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\",\n            Line1 = \"123 Main St.\",\n            Line2 = \"Suite 100\",\n            City = \"New York\",\n            State = \"NY\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\",\n                Line1 = \"123 Main St.\",\n                Line2 = \"Suite 100\",\n                City = \"New York\",\n                State = \"NY\"\n            },\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                    }\n                ]\n            }\n        };\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\")\n        )).Returns(customer);\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n        var output = result.AsT0;\n        Assert.Equivalent(input, output);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));\n    }\n\n    [Fact]\n    public async Task Run_PersonalOrganization_NoCurrentCustomer_MakesCorrectInvocations_ReturnsBillingAddress()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.FamiliesAnnually,\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\",\n            Line1 = \"123 Main St.\",\n            Line2 = \"Suite 100\",\n            City = \"New York\",\n            State = \"NY\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\",\n                Line1 = \"123 Main St.\",\n                Line2 = \"Suite 100\",\n                City = \"New York\",\n                State = \"NY\"\n            },\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                    }\n                ]\n            }\n        };\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\")\n        )).Returns(customer);\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n        var output = result.AsT0;\n        Assert.Equivalent(input, output);\n\n        await _subscriberService.Received(1).CreateStripeCustomer(organization);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));\n    }\n\n    [Fact]\n    public async Task Run_BusinessOrganization_MakesCorrectInvocations_ReturnsBillingAddress()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_123\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\",\n            Line1 = \"123 Main St.\",\n            Line2 = \"Suite 100\",\n            City = \"New York\",\n            State = \"NY\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\",\n                Line1 = \"123 Main St.\",\n                Line2 = \"Suite 100\",\n                City = \"New York\",\n                State = \"NY\"\n            },\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                    }\n                ]\n            }\n        };\n\n        _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(new Customer { TaxExempt = TaxExempt.None });\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\", \"tax_ids\") &&\n            options.TaxExempt == TaxExempt.None\n        )).Returns(customer);\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n        var output = result.AsT0;\n        Assert.Equivalent(input, output);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));\n    }\n\n    [Fact]\n    public async Task Run_BusinessOrganization_RemovingTaxId_MakesCorrectInvocations_ReturnsBillingAddress()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_123\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\",\n            Line1 = \"123 Main St.\",\n            Line2 = \"Suite 100\",\n            City = \"New York\",\n            State = \"NY\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\",\n                Line1 = \"123 Main St.\",\n                Line2 = \"Suite 100\",\n                City = \"New York\",\n                State = \"NY\"\n            },\n            Id = organization.GatewayCustomerId,\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                    }\n                ]\n            },\n            TaxIds = new StripeList<TaxId>\n            {\n                Data =\n                [\n                    new TaxId { Id = \"tax_id_123\", Type = \"us_ein\", Value = \"123456789\" }\n                ]\n            }\n        };\n\n        _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(new Customer { TaxExempt = TaxExempt.None });\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\", \"tax_ids\") &&\n            options.TaxExempt == TaxExempt.None\n        )).Returns(customer);\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n        var output = result.AsT0;\n        Assert.Equivalent(input, output);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));\n\n        await _stripeAdapter.Received(1).DeleteTaxIdAsync(customer.Id, \"tax_id_123\");\n    }\n\n    [Fact]\n    public async Task Run_NonUSBusinessOrganization_MakesCorrectInvocations_ReturnsBillingAddress()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_123\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"DE\",\n            PostalCode = \"10115\",\n            Line1 = \"Friedrichstraße 123\",\n            Line2 = \"Stock 3\",\n            City = \"Berlin\",\n            State = \"Berlin\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"DE\",\n                PostalCode = \"10115\",\n                Line1 = \"Friedrichstraße 123\",\n                Line2 = \"Stock 3\",\n                City = \"Berlin\",\n                State = \"Berlin\"\n            },\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                    }\n                ]\n            }\n        };\n\n        _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(new Customer { TaxExempt = TaxExempt.None });\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\", \"tax_ids\") &&\n            options.TaxExempt == TaxExempt.Reverse\n        )).Returns(customer);\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n        var output = result.AsT0;\n        Assert.Equivalent(input, output);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));\n    }\n\n    [Fact]\n    public async Task Run_SwissBusinessOrganization_MakesCorrectInvocations_ReturnsBillingAddress()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_123\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"CH\",\n            PostalCode = \"3001\",\n            Line1 = \"Bundesgasse 1\",\n            Line2 = string.Empty,\n            City = \"Bern\",\n            State = \"BE\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"CH\",\n                PostalCode = \"3001\",\n                Line1 = \"Bundesgasse 1\",\n                Line2 = string.Empty,\n                City = \"Bern\",\n                State = \"BE\"\n            },\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                    }\n                ]\n            }\n        };\n\n        _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(new Customer { TaxExempt = TaxExempt.None });\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\", \"tax_ids\") &&\n            options.TaxExempt == TaxExempt.None\n        )).Returns(customer);\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n        var output = result.AsT0;\n        Assert.Equivalent(input, output);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));\n    }\n\n    [Fact]\n    public async Task Run_BusinessOrganizationWithSpanishCIF_MakesCorrectInvocations_ReturnsBillingAddress()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_123\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"ES\",\n            PostalCode = \"28001\",\n            Line1 = \"Calle de Serrano 41\",\n            Line2 = \"Planta 3\",\n            City = \"Madrid\",\n            State = \"Madrid\",\n            TaxId = new TaxID(TaxIdType.SpanishNIF, \"A12345678\")\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"ES\",\n                PostalCode = \"28001\",\n                Line1 = \"Calle de Serrano 41\",\n                Line2 = \"Planta 3\",\n                City = \"Madrid\",\n                State = \"Madrid\"\n            },\n            Id = organization.GatewayCustomerId,\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                    }\n                ]\n            }\n        };\n\n        _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(new Customer { TaxExempt = TaxExempt.None });\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\", \"tax_ids\") &&\n            options.TaxExempt == TaxExempt.Reverse\n        )).Returns(customer);\n\n        _stripeAdapter\n            .CreateTaxIdAsync(customer.Id,\n                Arg.Is<TaxIdCreateOptions>(options => options.Type == TaxIdType.EUVAT))\n            .Returns(new TaxId { Type = TaxIdType.EUVAT, Value = \"ESA12345678\" });\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n        var output = result.AsT0;\n        Assert.Equivalent(input with { TaxId = new TaxID(TaxIdType.EUVAT, \"ESA12345678\") }, output);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));\n\n        await _stripeAdapter.Received(1).CreateTaxIdAsync(organization.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(\n            options => options.Type == TaxIdType.SpanishNIF &&\n                       options.Value == input.TaxId.Value));\n    }\n\n    [Fact]\n    public async Task Run_BusinessOrganization_UpdatingWithSameTaxId_DeletesBeforeCreating()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_123\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\",\n            Line1 = \"123 Main St.\",\n            Line2 = \"Suite 100\",\n            City = \"New York\",\n            State = \"NY\",\n            TaxId = new TaxID(\"us_ein\", \"987654321\")\n        };\n\n        var existingTaxId = new TaxId { Id = \"tax_id_123\", Type = \"us_ein\", Value = \"987654321\" };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\",\n                Line1 = \"123 Main St.\",\n                Line2 = \"Suite 100\",\n                City = \"New York\",\n                State = \"NY\"\n            },\n            Id = organization.GatewayCustomerId,\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                    }\n                ]\n            },\n            TaxIds = new StripeList<TaxId>\n            {\n                Data = [existingTaxId]\n            }\n        };\n\n        _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(new Customer { TaxExempt = TaxExempt.None });\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\", \"tax_ids\") &&\n            options.TaxExempt == TaxExempt.None\n        )).Returns(customer);\n\n        var newTaxId = new TaxId { Id = \"tax_id_456\", Type = \"us_ein\", Value = \"987654321\" };\n        _stripeAdapter.CreateTaxIdAsync(customer.Id, Arg.Is<TaxIdCreateOptions>(\n            options => options.Type == \"us_ein\" && options.Value == \"987654321\"\n        )).Returns(newTaxId);\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n        var output = result.AsT0;\n        Assert.Equivalent(input, output);\n\n        // Verify that deletion happens before creation\n        Received.InOrder(() =>\n        {\n            _stripeAdapter.DeleteTaxIdAsync(customer.Id, existingTaxId.Id);\n            _stripeAdapter.CreateTaxIdAsync(customer.Id, Arg.Any<TaxIdCreateOptions>());\n        });\n\n        await _stripeAdapter.Received(1).DeleteTaxIdAsync(customer.Id, existingTaxId.Id);\n        await _stripeAdapter.Received(1).CreateTaxIdAsync(customer.Id, Arg.Is<TaxIdCreateOptions>(\n            options => options.Type == \"us_ein\" && options.Value == \"987654321\"));\n    }\n\n    [Fact]\n    public async Task Run_SwissBusinessOrganization_WithReverse_CorrectsTaxExemptToNone()\n    {\n        // CH is a direct-tax country — \"reverse\" is not preserved. A customer moving from a\n        // non-direct-tax country (where \"reverse\" was correctly set) to Switzerland should have\n        // their tax_exempt corrected to \"none\".\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_123\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"CH\",\n            PostalCode = \"3001\",\n            Line1 = \"Bundesgasse 1\",\n            Line2 = string.Empty,\n            City = \"Bern\",\n            State = \"BE\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"CH\",\n                PostalCode = \"3001\",\n                Line1 = \"Bundesgasse 1\",\n                Line2 = string.Empty,\n                City = \"Bern\",\n                State = \"BE\"\n            },\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }\n                    }\n                ]\n            }\n        };\n\n        _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(new Customer { TaxExempt = TaxExempt.Reverse });\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\", \"tax_ids\") &&\n            options.TaxExempt == TaxExempt.None\n        )).Returns(customer);\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n        var output = result.AsT0;\n        Assert.Equivalent(input, output);\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(organization.GatewayCustomerId,\n            Arg.Is<CustomerUpdateOptions>(options => options.TaxExempt == TaxExempt.None));\n    }\n\n    [Fact]\n    public async Task Run_BusinessOrganizationWithExemptStatus_PreservesExempt()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually,\n            GatewayCustomerId = \"cus_123\",\n            GatewaySubscriptionId = \"sub_123\"\n        };\n\n        var input = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\",\n            Line1 = \"123 Main St.\",\n            City = \"New York\",\n            State = \"NY\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\",\n                Line1 = \"123 Main St.\",\n                City = \"New York\",\n                State = \"NY\"\n            },\n            Subscriptions = new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Id = organization.GatewaySubscriptionId,\n                        AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }\n                    }\n                ]\n            }\n        };\n\n        _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(new Customer { TaxExempt = TaxExempt.Exempt });\n\n        _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Address.Matches(input) &&\n            options.HasExpansions(\"subscriptions\", \"tax_ids\") &&\n            options.TaxExempt == TaxExempt.Exempt\n        )).Returns(customer);\n\n        var result = await _command.Run(organization, input);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(organization.GatewayCustomerId,\n            Arg.Is<CustomerUpdateOptions>(options => options.TaxExempt == TaxExempt.Exempt));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Payment.Commands;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Billing.Extensions;\nusing Braintree;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\nusing Address = Stripe.Address;\nusing Customer = Stripe.Customer;\nusing PaymentMethod = Stripe.PaymentMethod;\n\nnamespace Bit.Core.Test.Billing.Payment.Commands;\n\nusing static StripeConstants;\n\npublic class UpdatePaymentMethodCommandTests\n{\n    private readonly IBraintreeGateway _braintreeGateway = Substitute.For<IBraintreeGateway>();\n    private readonly IBraintreeService _braintreeService = Substitute.For<IBraintreeService>();\n    private readonly IGlobalSettings _globalSettings = Substitute.For<IGlobalSettings>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();\n    private readonly UpdatePaymentMethodCommand _command;\n\n    public UpdatePaymentMethodCommandTests()\n    {\n        _command = new UpdatePaymentMethodCommand(\n            _braintreeGateway,\n            _braintreeService,\n            _globalSettings,\n            Substitute.For<ILogger<UpdatePaymentMethodCommand>>(),\n            _stripeAdapter,\n            _subscriberService);\n    }\n\n    [Fact]\n    public async Task Run_BankAccount_MakesCorrectInvocations_ReturnsMaskedBankAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            GatewayCustomerId = \"cus_123\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        const string token = \"TOKEN\";\n\n        var setupIntent = new SetupIntent\n        {\n            Id = \"seti_123\",\n            PaymentMethod =\n                new PaymentMethod\n                {\n                    Type = \"us_bank_account\",\n                    UsBankAccount = new PaymentMethodUsBankAccount { BankName = \"Chase\", Last4 = \"9999\" }\n                },\n            NextAction = new SetupIntentNextAction\n            {\n                VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits\n                {\n                    HostedVerificationUrl = \"https://example.com\"\n                }\n            },\n            Status = \"requires_action\"\n        };\n\n        _stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n            options.PaymentMethod == token && options.HasExpansions(\"data.payment_method\"))).Returns([setupIntent]);\n\n        var result = await _command.Run(organization,\n            new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = token }, new BillingAddress\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            });\n\n        Assert.True(result.IsT0);\n        var maskedPaymentMethod = result.AsT0;\n        Assert.True(maskedPaymentMethod.IsT0);\n        var maskedBankAccount = maskedPaymentMethod.AsT0;\n        Assert.Equal(\"Chase\", maskedBankAccount.BankName);\n        Assert.Equal(\"9999\", maskedBankAccount.Last4);\n        Assert.Equal(\"https://example.com\", maskedBankAccount.HostedVerificationUrl);\n\n        await _stripeAdapter.Received(1).UpdateSetupIntentAsync(setupIntent.Id,\n            Arg.Is<SetupIntentUpdateOptions>(options => options.Customer == customer.Id));\n    }\n\n    [Fact]\n    public async Task Run_BankAccount_NoCurrentCustomer_MakesCorrectInvocations_ReturnsMaskedBankAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        const string token = \"TOKEN\";\n\n        var setupIntent = new SetupIntent\n        {\n            Id = \"seti_123\",\n            PaymentMethod =\n                new PaymentMethod\n                {\n                    Type = \"us_bank_account\",\n                    UsBankAccount = new PaymentMethodUsBankAccount { BankName = \"Chase\", Last4 = \"9999\" }\n                },\n            NextAction = new SetupIntentNextAction\n            {\n                VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits\n                {\n                    HostedVerificationUrl = \"https://example.com\"\n                }\n            },\n            Status = \"requires_action\"\n        };\n\n        _stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n            options.PaymentMethod == token && options.HasExpansions(\"data.payment_method\"))).Returns([setupIntent]);\n\n        var result = await _command.Run(organization,\n            new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = token }, new BillingAddress\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            });\n\n        Assert.True(result.IsT0);\n        var maskedPaymentMethod = result.AsT0;\n        Assert.True(maskedPaymentMethod.IsT0);\n        var maskedBankAccount = maskedPaymentMethod.AsT0;\n        Assert.Equal(\"Chase\", maskedBankAccount.BankName);\n        Assert.Equal(\"9999\", maskedBankAccount.Last4);\n        Assert.Equal(\"https://example.com\", maskedBankAccount.HostedVerificationUrl);\n\n        await _subscriberService.Received(1).CreateStripeCustomer(organization);\n\n        await _stripeAdapter.Received(1).UpdateSetupIntentAsync(setupIntent.Id,\n            Arg.Is<SetupIntentUpdateOptions>(options => options.Customer == customer.Id));\n    }\n\n    [Fact]\n    public async Task Run_BankAccount_StripeToPayPal_MakesCorrectInvocations_ReturnsMaskedBankAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            GatewayCustomerId = \"cus_123\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            },\n            Id = \"cus_123\",\n            Metadata = new Dictionary<string, string>\n            {\n                [MetadataKeys.BraintreeCustomerId] = \"braintree_customer_id\"\n            }\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        const string token = \"TOKEN\";\n\n        var setupIntent = new SetupIntent\n        {\n            Id = \"seti_123\",\n            PaymentMethod =\n                new PaymentMethod\n                {\n                    Type = \"us_bank_account\",\n                    UsBankAccount = new PaymentMethodUsBankAccount { BankName = \"Chase\", Last4 = \"9999\" }\n                },\n            NextAction = new SetupIntentNextAction\n            {\n                VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits\n                {\n                    HostedVerificationUrl = \"https://example.com\"\n                }\n            },\n            Status = \"requires_action\"\n        };\n\n        _stripeAdapter.ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n            options.PaymentMethod == token && options.HasExpansions(\"data.payment_method\"))).Returns([setupIntent]);\n\n        var result = await _command.Run(organization,\n            new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = token }, new BillingAddress\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            });\n\n        Assert.True(result.IsT0);\n        var maskedPaymentMethod = result.AsT0;\n        Assert.True(maskedPaymentMethod.IsT0);\n        var maskedBankAccount = maskedPaymentMethod.AsT0;\n        Assert.Equal(\"Chase\", maskedBankAccount.BankName);\n        Assert.Equal(\"9999\", maskedBankAccount.Last4);\n        Assert.Equal(\"https://example.com\", maskedBankAccount.HostedVerificationUrl);\n\n        await _stripeAdapter.Received(1).UpdateSetupIntentAsync(setupIntent.Id,\n            Arg.Is<SetupIntentUpdateOptions>(options => options.Customer == customer.Id));\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>\n            options.Metadata[MetadataKeys.BraintreeCustomerId] == string.Empty &&\n            options.Metadata[MetadataKeys.RetiredBraintreeCustomerId] == \"braintree_customer_id\"));\n    }\n\n    [Fact]\n    public async Task Run_Card_MakesCorrectInvocations_ReturnsMaskedCard()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            GatewayCustomerId = \"cus_123\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            },\n            Id = \"cus_123\",\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        const string token = \"TOKEN\";\n\n        _stripeAdapter\n            .AttachPaymentMethodAsync(token,\n                Arg.Is<PaymentMethodAttachOptions>(options => options.Customer == customer.Id))\n            .Returns(new PaymentMethod\n            {\n                Type = \"card\",\n                Card = new PaymentMethodCard\n                {\n                    Brand = \"visa\",\n                    Last4 = \"9999\",\n                    ExpMonth = 1,\n                    ExpYear = 2028\n                }\n            });\n\n        var result = await _command.Run(organization,\n            new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = token }, new BillingAddress\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            });\n\n        Assert.True(result.IsT0);\n        var maskedPaymentMethod = result.AsT0;\n        Assert.True(maskedPaymentMethod.IsT1);\n        var maskedCard = maskedPaymentMethod.AsT1;\n        Assert.Equal(\"visa\", maskedCard.Brand);\n        Assert.Equal(\"9999\", maskedCard.Last4);\n        Assert.Equal(\"01/2028\", maskedCard.Expiration);\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,\n            Arg.Is<CustomerUpdateOptions>(options => options.InvoiceSettings.DefaultPaymentMethod == token));\n    }\n\n    [Fact]\n    public async Task Run_Card_PropagateBillingAddress_MakesCorrectInvocations_ReturnsMaskedCard()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            GatewayCustomerId = \"cus_123\"\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        const string token = \"TOKEN\";\n\n        _stripeAdapter\n            .AttachPaymentMethodAsync(token,\n                Arg.Is<PaymentMethodAttachOptions>(options => options.Customer == customer.Id))\n            .Returns(new PaymentMethod\n            {\n                Type = \"card\",\n                Card = new PaymentMethodCard\n                {\n                    Brand = \"visa\",\n                    Last4 = \"9999\",\n                    ExpMonth = 1,\n                    ExpYear = 2028\n                }\n            });\n\n        var result = await _command.Run(organization,\n            new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = token }, new BillingAddress\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            });\n\n        Assert.True(result.IsT0);\n        var maskedPaymentMethod = result.AsT0;\n        Assert.True(maskedPaymentMethod.IsT1);\n        var maskedCard = maskedPaymentMethod.AsT1;\n        Assert.Equal(\"visa\", maskedCard.Brand);\n        Assert.Equal(\"9999\", maskedCard.Last4);\n        Assert.Equal(\"01/2028\", maskedCard.Expiration);\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,\n            Arg.Is<CustomerUpdateOptions>(options => options.InvoiceSettings.DefaultPaymentMethod == token));\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,\n            Arg.Is<CustomerUpdateOptions>(options => options.Address.Country == \"US\" && options.Address.PostalCode == \"12345\"));\n    }\n\n    [Fact]\n    public async Task Run_PayPal_ExistingBraintreeCustomer_MakesCorrectInvocations_ReturnsMaskedPayPalAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            GatewayCustomerId = \"cus_123\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            },\n            Id = \"cus_123\",\n            Metadata = new Dictionary<string, string>\n            {\n                [MetadataKeys.BraintreeCustomerId] = \"braintree_customer_id\"\n            }\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n        braintreeCustomer.Id.Returns(\"braintree_customer_id\");\n        var existing = Substitute.For<PayPalAccount>();\n        existing.Email.Returns(\"user@gmail.com\");\n        existing.IsDefault.Returns(true);\n        existing.Token.Returns(\"EXISTING\");\n        braintreeCustomer.PaymentMethods.Returns([existing]);\n\n        _braintreeService.GetCustomer(customer).Returns(braintreeCustomer);\n\n        var customerGateway = Substitute.For<ICustomerGateway>();\n        _braintreeGateway.Customer.Returns(customerGateway);\n\n        var paymentMethodGateway = Substitute.For<IPaymentMethodGateway>();\n        var updated = Substitute.For<PayPalAccount>();\n        updated.Email.Returns(\"user@gmail.com\");\n        updated.Token.Returns(\"UPDATED\");\n        var updatedResult = Substitute.For<Result<Braintree.PaymentMethod>>();\n        updatedResult.Target.Returns(updated);\n        paymentMethodGateway.CreateAsync(Arg.Is<PaymentMethodRequest>(options =>\n                options.CustomerId == braintreeCustomer.Id && options.PaymentMethodNonce == \"TOKEN\"))\n            .Returns(updatedResult);\n        _braintreeGateway.PaymentMethod.Returns(paymentMethodGateway);\n\n        var result = await _command.Run(organization,\n            new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = \"TOKEN\" },\n            new BillingAddress { Country = \"US\", PostalCode = \"12345\" });\n\n        Assert.True(result.IsT0);\n        var maskedPaymentMethod = result.AsT0;\n        Assert.True(maskedPaymentMethod.IsT2);\n        var maskedPayPalAccount = maskedPaymentMethod.AsT2;\n        Assert.Equal(\"user@gmail.com\", maskedPayPalAccount.Email);\n\n        await customerGateway.Received(1).UpdateAsync(braintreeCustomer.Id,\n            Arg.Is<CustomerRequest>(options => options.DefaultPaymentMethodToken == updated.Token));\n        await paymentMethodGateway.Received(1).DeleteAsync(existing.Token);\n    }\n\n    [Fact]\n    public async Task Run_PayPal_NewBraintreeCustomer_MakesCorrectInvocations_ReturnsMaskedPayPalAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            GatewayCustomerId = \"cus_123\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            },\n            Id = \"cus_123\",\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        _globalSettings.BaseServiceUri.Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings())\n        {\n            CloudRegion = \"US\"\n        });\n\n        var customerGateway = Substitute.For<ICustomerGateway>();\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n        braintreeCustomer.Id.Returns(\"braintree_customer_id\");\n        var payPalAccount = Substitute.For<PayPalAccount>();\n        payPalAccount.Email.Returns(\"user@gmail.com\");\n        payPalAccount.IsDefault.Returns(true);\n        payPalAccount.Token.Returns(\"NONCE\");\n        braintreeCustomer.PaymentMethods.Returns([payPalAccount]);\n        var createResult = Substitute.For<Result<Braintree.Customer>>();\n        createResult.Target.Returns(braintreeCustomer);\n        customerGateway.CreateAsync(Arg.Is<CustomerRequest>(options =>\n            options.Id.StartsWith(organization.BraintreeCustomerIdPrefix() + organization.Id.ToString(\"N\").ToLower()) &&\n            options.CustomFields[organization.BraintreeIdField()] == organization.Id.ToString() &&\n            options.CustomFields[organization.BraintreeCloudRegionField()] == \"US\" &&\n            options.Email == organization.BillingEmailAddress() &&\n            options.PaymentMethodNonce == \"TOKEN\")).Returns(createResult);\n        _braintreeGateway.Customer.Returns(customerGateway);\n\n        var result = await _command.Run(organization,\n            new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = \"TOKEN\" },\n            new BillingAddress { Country = \"US\", PostalCode = \"12345\" });\n\n        Assert.True(result.IsT0);\n        var maskedPaymentMethod = result.AsT0;\n        Assert.True(maskedPaymentMethod.IsT2);\n        var maskedPayPalAccount = maskedPaymentMethod.AsT2;\n        Assert.Equal(\"user@gmail.com\", maskedPayPalAccount.Email);\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.Metadata[MetadataKeys.BraintreeCustomerId] == \"braintree_customer_id\"));\n    }\n\n    [Fact]\n    public async Task Run_PayPal_MissingBraintreeCustomer_CreatesNewBraintreeCustomer_ReturnsMaskedPayPalAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            GatewayCustomerId = \"cus_123\"\n        };\n\n        var customer = new Customer\n        {\n            Address = new Address\n            {\n                Country = \"US\",\n                PostalCode = \"12345\"\n            },\n            Id = \"cus_123\",\n            Metadata = new Dictionary<string, string>\n            {\n                [MetadataKeys.BraintreeCustomerId] = \"missing_braintree_customer_id\"\n            }\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        // BraintreeService.GetCustomer returns null when the Braintree customer doesn't exist\n        _braintreeService.GetCustomer(customer).Returns((Braintree.Customer?)null);\n\n        _globalSettings.BaseServiceUri.Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings())\n        {\n            CloudRegion = \"US\"\n        });\n\n        var customerGateway = Substitute.For<ICustomerGateway>();\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n        braintreeCustomer.Id.Returns(\"new_braintree_customer_id\");\n        var payPalAccount = Substitute.For<PayPalAccount>();\n        payPalAccount.Email.Returns(\"user@gmail.com\");\n        payPalAccount.IsDefault.Returns(true);\n        payPalAccount.Token.Returns(\"NONCE\");\n        braintreeCustomer.PaymentMethods.Returns([payPalAccount]);\n        var createResult = Substitute.For<Result<Braintree.Customer>>();\n        createResult.Target.Returns(braintreeCustomer);\n        customerGateway.CreateAsync(Arg.Is<CustomerRequest>(options =>\n            options.Id.StartsWith(organization.BraintreeCustomerIdPrefix() + organization.Id.ToString(\"N\").ToLower()) &&\n            options.CustomFields[organization.BraintreeIdField()] == organization.Id.ToString() &&\n            options.CustomFields[organization.BraintreeCloudRegionField()] == \"US\" &&\n            options.Email == organization.BillingEmailAddress() &&\n            options.PaymentMethodNonce == \"TOKEN\")).Returns(createResult);\n        _braintreeGateway.Customer.Returns(customerGateway);\n\n        var result = await _command.Run(organization,\n            new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = \"TOKEN\" },\n            new BillingAddress { Country = \"US\", PostalCode = \"12345\" });\n\n        Assert.True(result.IsT0);\n        var maskedPaymentMethod = result.AsT0;\n        Assert.True(maskedPaymentMethod.IsT2);\n        var maskedPayPalAccount = maskedPaymentMethod.AsT2;\n        Assert.Equal(\"user@gmail.com\", maskedPayPalAccount.Email);\n\n        // Verify a new Braintree customer was created (not FindAsync called)\n        await customerGateway.DidNotReceive().FindAsync(Arg.Any<string>());\n        await customerGateway.Received(1).CreateAsync(Arg.Any<CustomerRequest>());\n\n        // Verify Stripe metadata was updated with the new Braintree customer ID\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id,\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.Metadata[MetadataKeys.BraintreeCustomerId] == \"new_braintree_customer_id\"));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Billing.Payment.Models;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Payment.Models;\n\npublic class MaskedPaymentMethodTests\n{\n    [Fact]\n    public void Write_Read_BankAccount_Succeeds()\n    {\n        MaskedPaymentMethod input = new MaskedBankAccount\n        {\n            BankName = \"Chase\",\n            Last4 = \"9999\",\n            HostedVerificationUrl = \"https://example.com\"\n        };\n\n        var json = JsonSerializer.Serialize(input);\n\n        var output = JsonSerializer.Deserialize<MaskedPaymentMethod>(json);\n        Assert.NotNull(output);\n        Assert.True(output.IsT0);\n\n        Assert.Equivalent(input.AsT0, output.AsT0);\n    }\n\n    [Fact]\n    public void Write_Read_BankAccount_WithOptions_Succeeds()\n    {\n        MaskedPaymentMethod input = new MaskedBankAccount\n        {\n            BankName = \"Chase\",\n            Last4 = \"9999\",\n            HostedVerificationUrl = \"https://example.com\"\n        };\n\n        var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };\n\n        var json = JsonSerializer.Serialize(input, jsonSerializerOptions);\n\n        var output = JsonSerializer.Deserialize<MaskedPaymentMethod>(json, jsonSerializerOptions);\n        Assert.NotNull(output);\n        Assert.True(output.IsT0);\n\n        Assert.Equivalent(input.AsT0, output.AsT0);\n    }\n\n    [Fact]\n    public void Write_Read_Card_Succeeds()\n    {\n        MaskedPaymentMethod input = new MaskedCard\n        {\n            Brand = \"visa\",\n            Last4 = \"9999\",\n            Expiration = \"01/2028\"\n        };\n\n        var json = JsonSerializer.Serialize(input);\n\n        var output = JsonSerializer.Deserialize<MaskedPaymentMethod>(json);\n        Assert.NotNull(output);\n        Assert.True(output.IsT1);\n\n        Assert.Equivalent(input.AsT1, output.AsT1);\n    }\n\n    [Fact]\n    public void Write_Read_PayPal_Succeeds()\n    {\n        MaskedPaymentMethod input = new MaskedPayPalAccount\n        {\n            Email = \"paypal-user@gmail.com\"\n        };\n\n        var json = JsonSerializer.Serialize(input);\n\n        var output = JsonSerializer.Deserialize<MaskedPaymentMethod>(json);\n        Assert.NotNull(output);\n        Assert.True(output.IsT2);\n\n        Assert.Equivalent(input.AsT2, output.AsT2);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Models/PaymentMethodTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Billing.Payment.Models;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Payment.Models;\n\npublic class PaymentMethodTests\n{\n    [Theory]\n    [InlineData(\"{\\\"cardNumber\\\":\\\"1234\\\"}\")]\n    [InlineData(\"{\\\"type\\\":\\\"unknown_type\\\",\\\"data\\\":\\\"value\\\"}\")]\n    [InlineData(\"{\\\"type\\\":\\\"invalid\\\",\\\"token\\\":\\\"test-token\\\"}\")]\n    [InlineData(\"{\\\"type\\\":\\\"invalid\\\"}\")]\n    public void Read_ShouldThrowJsonException_OnInvalidOrMissingType(string json)\n    {\n        // Arrange\n        var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };\n\n        // Act & Assert\n        Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<PaymentMethod>(json, options));\n    }\n\n    [Theory]\n    [InlineData(\"{\\\"type\\\":\\\"card\\\"}\")]\n    [InlineData(\"{\\\"type\\\":\\\"card\\\",\\\"token\\\":\\\"\\\"}\")]\n    [InlineData(\"{\\\"type\\\":\\\"card\\\",\\\"token\\\":null}\")]\n    public void Read_ShouldThrowJsonException_OnInvalidTokenizedPaymentMethodToken(string json)\n    {\n        // Arrange\n        var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };\n\n        // Act & Assert\n        Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<PaymentMethod>(json, options));\n    }\n\n    // Tokenized payment method deserialization\n    [Theory]\n    [InlineData(\"bankAccount\", TokenizablePaymentMethodType.BankAccount)]\n    [InlineData(\"card\", TokenizablePaymentMethodType.Card)]\n    [InlineData(\"payPal\", TokenizablePaymentMethodType.PayPal)]\n    public void Read_ShouldDeserializeTokenizedPaymentMethods(string typeString, TokenizablePaymentMethodType expectedType)\n    {\n        // Arrange\n        var json = $\"{{\\\"type\\\":\\\"{typeString}\\\",\\\"token\\\":\\\"test-token\\\"}}\";\n        var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };\n\n        // Act\n        var result = JsonSerializer.Deserialize<PaymentMethod>(json, options);\n\n        // Assert\n        Assert.True(result.IsTokenized);\n        Assert.Equal(expectedType, result.AsT0.Type);\n        Assert.Equal(\"test-token\", result.AsT0.Token);\n    }\n\n    // Non-tokenized payment method deserialization\n    [Theory]\n    [InlineData(\"accountcredit\", NonTokenizablePaymentMethodType.AccountCredit)]\n    public void Read_ShouldDeserializeNonTokenizedPaymentMethods(string typeString, NonTokenizablePaymentMethodType expectedType)\n    {\n        // Arrange\n        var json = $\"{{\\\"type\\\":\\\"{typeString}\\\"}}\";\n        var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };\n\n        // Act\n        var result = JsonSerializer.Deserialize<PaymentMethod>(json, options);\n\n        // Assert\n        Assert.True(result.IsNonTokenized);\n        Assert.Equal(expectedType, result.AsT1.Type);\n    }\n\n    // Tokenized payment method serialization\n    [Theory]\n    [InlineData(TokenizablePaymentMethodType.BankAccount, \"bankaccount\")]\n    [InlineData(TokenizablePaymentMethodType.Card, \"card\")]\n    [InlineData(TokenizablePaymentMethodType.PayPal, \"paypal\")]\n    public void Write_ShouldSerializeTokenizedPaymentMethods(TokenizablePaymentMethodType type, string expectedTypeString)\n    {\n        // Arrange\n        var paymentMethod = new PaymentMethod(new TokenizedPaymentMethod\n        {\n            Type = type,\n            Token = \"test-token\"\n        });\n        var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };\n\n        // Act\n        var json = JsonSerializer.Serialize(paymentMethod, options);\n\n        // Assert\n        Assert.Contains($\"\\\"type\\\":\\\"{expectedTypeString}\\\"\", json);\n        Assert.Contains(\"\\\"token\\\":\\\"test-token\\\"\", json);\n    }\n\n    // Non-tokenized payment method serialization\n    [Theory]\n    [InlineData(NonTokenizablePaymentMethodType.AccountCredit, \"accountcredit\")]\n    public void Write_ShouldSerializeNonTokenizedPaymentMethods(NonTokenizablePaymentMethodType type, string expectedTypeString)\n    {\n        // Arrange\n        var paymentMethod = new PaymentMethod(new NonTokenizedPaymentMethod { Type = type });\n        var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } };\n\n        // Act\n        var json = JsonSerializer.Serialize(paymentMethod, options);\n\n        // Assert\n        Assert.Contains($\"\\\"type\\\":\\\"{expectedTypeString}\\\"\", json);\n        Assert.DoesNotContain(\"token\", json);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Queries/GetApplicableDiscountsQueryTests.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Payment.Queries;\n\n[SutProviderCustomize]\npublic class GetApplicableDiscountsQueryTests\n{\n    private static IDictionary<DiscountTierType, bool> DiscountDictionary(bool eligibilitySetting)\n        => Enum.GetValues<DiscountTierType>().ToDictionary(t => t, _ => eligibilitySetting);\n\n    [Theory, BitAutoData]\n    public async Task Run_NoEligibleDiscounts_ReturnsEmptyArray(\n        User user,\n        SutProvider<GetApplicableDiscountsQuery> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .GetEligibleDiscountsAsync(user)\n            .Returns([]);\n\n        // Act\n        var result = await sutProvider.Sut.Run(user);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.Empty(result.AsT0);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_EligibleDiscounts_ReturnsMappedResponseModels(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<GetApplicableDiscountsQuery> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .GetEligibleDiscountsAsync(user)\n            .Returns(new List<DiscountEligibility> { new(discount, DiscountDictionary(true)) });\n\n        // Act\n        var result = await sutProvider.Sut.Run(user);\n\n        // Assert\n        Assert.True(result.IsT0);\n        var models = result.AsT0;\n        var model = Assert.Single(models);\n        Assert.Equal(discount.StripeCouponId, model.StripeCouponId);\n        Assert.Equal(discount.PercentOff, model.PercentOff);\n        Assert.Equal(discount.AmountOff, model.AmountOff);\n        Assert.Equal(discount.Duration, model.Duration);\n        Assert.Equal(discount.Name, model.Name);\n        Assert.Equal(discount.StartDate, model.StartDate);\n        Assert.Equal(discount.EndDate, model.EndDate);\n        Assert.All(model.TierEligibility!.Values, Assert.True);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_DiscountWithAllTiersEligible_MapsAllTierEligibilityTrue(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<GetApplicableDiscountsQuery> sutProvider)\n    {\n        // Arrange\n        var tierEligibility = DiscountDictionary(true);\n\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .GetEligibleDiscountsAsync(user)\n            .Returns(new List<DiscountEligibility> { new(discount, tierEligibility) });\n\n        // Act\n        var result = await sutProvider.Sut.Run(user);\n\n        // Assert\n        var model = Assert.Single(result.AsT0);\n        Assert.NotNull(model.TierEligibility);\n        Assert.All(model.TierEligibility.Values, Assert.True);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_DiscountWithPartialTierEligibility_MapsSpecificTierEligibility(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<GetApplicableDiscountsQuery> sutProvider)\n    {\n        // Arrange\n        var tierEligibility = new Dictionary<DiscountTierType, bool>\n        {\n            { DiscountTierType.Premium, true },\n            { DiscountTierType.Families, false }\n        };\n\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .GetEligibleDiscountsAsync(user)\n            .Returns(new List<DiscountEligibility> { new(discount, tierEligibility) });\n\n        // Act\n        var result = await sutProvider.Sut.Run(user);\n\n        // Assert\n        var model = Assert.Single(result.AsT0);\n        Assert.NotNull(model.TierEligibility);\n        Assert.True(model.TierEligibility[DiscountTierType.Premium]);\n        Assert.False(model.TierEligibility[DiscountTierType.Families]);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_MultipleEligibleDiscounts_ReturnsAllMappedResponseModels(\n        User user,\n        SubscriptionDiscount firstDiscount,\n        SubscriptionDiscount secondDiscount,\n        SutProvider<GetApplicableDiscountsQuery> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .GetEligibleDiscountsAsync(user)\n            .Returns(new List<DiscountEligibility>\n            {\n                new(firstDiscount, DiscountDictionary(true)),\n                new(secondDiscount, DiscountDictionary(true))\n            });\n\n        // Act\n        var result = await sutProvider.Sut.Run(user);\n\n        // Assert\n        Assert.True(result.IsT0);\n        var models = result.AsT0;\n        Assert.Equal(2, models.Length);\n        Assert.Contains(models, m => m.StripeCouponId == firstDiscount.StripeCouponId);\n        Assert.Contains(models, m => m.StripeCouponId == secondDiscount.StripeCouponId);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Queries/GetBillingAddressQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Test.Billing.Extensions;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Payment.Queries;\n\npublic class GetBillingAddressQueryTests\n{\n    private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();\n    private readonly GetBillingAddressQuery _query;\n\n    public GetBillingAddressQueryTests()\n    {\n        _query = new GetBillingAddressQuery(_subscriberService);\n    }\n\n    [Fact]\n    public async Task Run_ForUserWithNoAddress_ReturnsNull()\n    {\n        var user = new User();\n\n        var customer = new Customer();\n\n        _subscriberService.GetCustomer(user, Arg.Is<CustomerGetOptions>(\n            options => options.Expand == null)).Returns(customer);\n\n        var billingAddress = await _query.Run(user);\n\n        Assert.Null(billingAddress);\n    }\n\n    [Fact]\n    public async Task Run_ForUserWithAddress_ReturnsBillingAddress()\n    {\n        var user = new User();\n\n        var address = GetAddress();\n\n        var customer = new Customer\n        {\n            Address = address\n        };\n\n        _subscriberService.GetCustomer(user, Arg.Is<CustomerGetOptions>(\n            options => options.Expand == null)).Returns(customer);\n\n        var billingAddress = await _query.Run(user);\n\n        AssertEquality(address, billingAddress);\n    }\n\n    [Fact]\n    public async Task Run_ForPersonalOrganizationWithNoAddress_ReturnsNull()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.FamiliesAnnually\n        };\n\n        var customer = new Customer();\n\n        _subscriberService.GetCustomer(organization, Arg.Is<CustomerGetOptions>(\n            options => options.Expand == null)).Returns(customer);\n\n        var billingAddress = await _query.Run(organization);\n\n        Assert.Null(billingAddress);\n    }\n\n    [Fact]\n    public async Task Run_ForPersonalOrganizationWithAddress_ReturnsBillingAddress()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.FamiliesAnnually\n        };\n\n        var address = GetAddress();\n\n        var customer = new Customer\n        {\n            Address = address\n        };\n\n        _subscriberService.GetCustomer(organization, Arg.Is<CustomerGetOptions>(\n            options => options.Expand == null)).Returns(customer);\n\n        var billingAddress = await _query.Run(organization);\n\n        AssertEquality(customer.Address, billingAddress);\n    }\n\n    [Fact]\n    public async Task Run_ForBusinessOrganizationWithNoAddress_ReturnsNull()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        var customer = new Customer();\n\n        _subscriberService.GetCustomer(organization, Arg.Is<CustomerGetOptions>(\n            options => options.HasExpansions(\"tax_ids\"))).Returns(customer);\n\n        var billingAddress = await _query.Run(organization);\n\n        Assert.Null(billingAddress);\n    }\n\n    [Fact]\n    public async Task Run_ForBusinessOrganizationWithAddressAndTaxId_ReturnsBillingAddressWithTaxId()\n    {\n        var organization = new Organization\n        {\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        var address = GetAddress();\n\n        var taxId = GetTaxId();\n\n        var customer = new Customer\n        {\n            Address = address,\n            TaxIds = new StripeList<TaxId>\n            {\n                Data = [taxId]\n            }\n        };\n\n        _subscriberService.GetCustomer(organization, Arg.Is<CustomerGetOptions>(\n            options => options.HasExpansions(\"tax_ids\"))).Returns(customer);\n\n        var billingAddress = await _query.Run(organization);\n\n        AssertEquality(address, taxId, billingAddress);\n    }\n\n    [Fact]\n    public async Task Run_ForProviderWithAddressAndTaxId_ReturnsBillingAddressWithTaxId()\n    {\n        var provider = new Provider();\n\n        var address = GetAddress();\n\n        var taxId = GetTaxId();\n\n        var customer = new Customer\n        {\n            Address = address,\n            TaxIds = new StripeList<TaxId>\n            {\n                Data = [taxId]\n            }\n        };\n\n        _subscriberService.GetCustomer(provider, Arg.Is<CustomerGetOptions>(\n            options => options.HasExpansions(\"tax_ids\"))).Returns(customer);\n\n        var billingAddress = await _query.Run(provider);\n\n        AssertEquality(address, taxId, billingAddress);\n    }\n\n    private static void AssertEquality(Address address, BillingAddress? billingAddress)\n    {\n        Assert.NotNull(billingAddress);\n        Assert.Equal(address.Country, billingAddress.Country);\n        Assert.Equal(address.PostalCode, billingAddress.PostalCode);\n        Assert.Equal(address.Line1, billingAddress.Line1);\n        Assert.Equal(address.Line2, billingAddress.Line2);\n        Assert.Equal(address.City, billingAddress.City);\n        Assert.Equal(address.State, billingAddress.State);\n    }\n\n    private static void AssertEquality(Address address, TaxId taxId, BillingAddress? billingAddress)\n    {\n        AssertEquality(address, billingAddress);\n        Assert.NotNull(billingAddress!.TaxId);\n        Assert.Equal(taxId.Type, billingAddress.TaxId!.Code);\n        Assert.Equal(taxId.Value, billingAddress.TaxId!.Value);\n    }\n\n    private static Address GetAddress() => new()\n    {\n        Country = \"US\",\n        PostalCode = \"12345\",\n        Line1 = \"123 Main St.\",\n        Line2 = \"Suite 100\",\n        City = \"New York\",\n        State = \"NY\"\n    };\n\n    private static TaxId GetTaxId() => new() { Type = \"us_ein\", Value = \"123456789\" };\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Queries/GetCreditQueryTests.cs",
    "content": "﻿using Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Payment.Queries;\n\npublic class GetCreditQueryTests\n{\n    private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();\n    private readonly GetCreditQuery _query;\n\n    public GetCreditQueryTests()\n    {\n        _query = new GetCreditQuery(_subscriberService);\n    }\n\n    [Fact]\n    public async Task Run_NoCustomer_ReturnsNull()\n    {\n        _subscriberService.GetCustomer(Arg.Any<ISubscriber>()).ReturnsNull();\n\n        var credit = await _query.Run(Substitute.For<ISubscriber>());\n\n        Assert.Null(credit);\n    }\n\n    [Fact]\n    public async Task Run_ReturnsCredit()\n    {\n        _subscriberService.GetCustomer(Arg.Any<ISubscriber>()).Returns(new Customer { Balance = -1000 });\n\n        var credit = await _query.Run(Substitute.For<ISubscriber>());\n\n        Assert.NotNull(credit);\n        Assert.Equal(10M, credit);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Extensions;\nusing Braintree;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\nusing Customer = Stripe.Customer;\nusing PaymentMethod = Stripe.PaymentMethod;\n\nnamespace Bit.Core.Test.Billing.Payment.Queries;\n\nusing static StripeConstants;\n\npublic class GetPaymentMethodQueryTests\n{\n    private readonly IBraintreeService _braintreeService = Substitute.For<IBraintreeService>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();\n    private readonly GetPaymentMethodQuery _query;\n\n    public GetPaymentMethodQueryTests()\n    {\n        _query = new GetPaymentMethodQuery(\n            _braintreeService,\n            _stripeAdapter,\n            _subscriberService);\n    }\n\n    [Fact]\n    public async Task Run_NoCustomer_ReturnsNull()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).ReturnsNull();\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.Null(maskedPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_NoPaymentMethod_ReturnsNull()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).Returns(customer);\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.Null(maskedPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_NoPaymentMethod_BraintreeCustomerNotFound_ReturnsNull()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>\n            {\n                [MetadataKeys.BraintreeCustomerId] = \"non_existent_braintree_customer_id\"\n            }\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).Returns(customer);\n\n        _braintreeService.GetCustomer(customer).ReturnsNull();\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.Null(maskedPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_BankAccount_FromPaymentMethod_ReturnsMaskedBankAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            InvoiceSettings = new CustomerInvoiceSettings\n            {\n                DefaultPaymentMethod = new PaymentMethod\n                {\n                    Type = \"us_bank_account\",\n                    UsBankAccount = new PaymentMethodUsBankAccount { BankName = \"Chase\", Last4 = \"9999\" }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).Returns(customer);\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.NotNull(maskedPaymentMethod);\n        Assert.True(maskedPaymentMethod.IsT0);\n        var maskedBankAccount = maskedPaymentMethod.AsT0;\n        Assert.Equal(\"Chase\", maskedBankAccount.BankName);\n        Assert.Equal(\"9999\", maskedBankAccount.Last4);\n        Assert.Null(maskedBankAccount.HostedVerificationUrl);\n    }\n\n    [Fact]\n    public async Task Run_BankAccount_FromSource_ReturnsMaskedBankAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            DefaultSource = new BankAccount\n            {\n                BankName = \"Chase\",\n                Last4 = \"9999\",\n                Status = \"verified\"\n            },\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).Returns(customer);\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.NotNull(maskedPaymentMethod);\n        Assert.True(maskedPaymentMethod.IsT0);\n        var maskedBankAccount = maskedPaymentMethod.AsT0;\n        Assert.Equal(\"Chase\", maskedBankAccount.BankName);\n        Assert.Equal(\"9999\", maskedBankAccount.Last4);\n        Assert.Null(maskedBankAccount.HostedVerificationUrl);\n    }\n\n    [Fact]\n    public async Task Run_BankAccount_FromSetupIntent_ReturnsMaskedBankAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).Returns(customer);\n\n        _stripeAdapter\n            .ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n                options.Customer == customer.Id &&\n                options.HasExpansions(\"data.payment_method\")))\n            .Returns(\n            [\n                new SetupIntent\n                {\n                    PaymentMethod = new PaymentMethod\n                    {\n                        Type = \"us_bank_account\",\n                        UsBankAccount = new PaymentMethodUsBankAccount { BankName = \"Chase\", Last4 = \"9999\" }\n                    },\n                    NextAction = new SetupIntentNextAction\n                    {\n                        VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits\n                        {\n                            HostedVerificationUrl = \"https://example.com\"\n                        }\n                    },\n                    Status = \"requires_action\"\n                }\n            ]);\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.NotNull(maskedPaymentMethod);\n        Assert.True(maskedPaymentMethod.IsT0);\n        var maskedBankAccount = maskedPaymentMethod.AsT0;\n        Assert.Equal(\"Chase\", maskedBankAccount.BankName);\n        Assert.Equal(\"9999\", maskedBankAccount.Last4);\n        Assert.Equal(\"https://example.com\", maskedBankAccount.HostedVerificationUrl);\n    }\n\n    [Fact]\n    public async Task Run_Card_FromPaymentMethod_ReturnsMaskedCard()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            InvoiceSettings = new CustomerInvoiceSettings\n            {\n                DefaultPaymentMethod = new PaymentMethod\n                {\n                    Type = \"card\",\n                    Card = new PaymentMethodCard\n                    {\n                        Brand = \"visa\",\n                        Last4 = \"9999\",\n                        ExpMonth = 1,\n                        ExpYear = 2028\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).Returns(customer);\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.NotNull(maskedPaymentMethod);\n        Assert.True(maskedPaymentMethod.IsT1);\n        var maskedCard = maskedPaymentMethod.AsT1;\n        Assert.Equal(\"visa\", maskedCard.Brand);\n        Assert.Equal(\"9999\", maskedCard.Last4);\n        Assert.Equal(\"01/2028\", maskedCard.Expiration);\n    }\n\n    [Fact]\n    public async Task Run_Card_FromSource_ReturnsMaskedCard()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            DefaultSource = new Card\n            {\n                Brand = \"visa\",\n                Last4 = \"9999\",\n                ExpMonth = 1,\n                ExpYear = 2028\n            },\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).Returns(customer);\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.NotNull(maskedPaymentMethod);\n        Assert.True(maskedPaymentMethod.IsT1);\n        var maskedCard = maskedPaymentMethod.AsT1;\n        Assert.Equal(\"visa\", maskedCard.Brand);\n        Assert.Equal(\"9999\", maskedCard.Last4);\n        Assert.Equal(\"01/2028\", maskedCard.Expiration);\n    }\n\n    [Fact]\n    public async Task Run_Card_FromSourceCard_ReturnsMaskedCard()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            DefaultSource = new Source\n            {\n                Card = new SourceCard\n                {\n                    Brand = \"Visa\",\n                    Last4 = \"9999\",\n                    ExpMonth = 1,\n                    ExpYear = 2028\n                }\n            },\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).Returns(customer);\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.NotNull(maskedPaymentMethod);\n        Assert.True(maskedPaymentMethod.IsT1);\n        var maskedCard = maskedPaymentMethod.AsT1;\n        Assert.Equal(\"visa\", maskedCard.Brand);\n        Assert.Equal(\"9999\", maskedCard.Last4);\n        Assert.Equal(\"01/2028\", maskedCard.Expiration);\n    }\n\n    [Fact]\n    public async Task Run_PayPalAccount_ReturnsMaskedPayPalAccount()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Metadata = new Dictionary<string, string>\n            {\n                [MetadataKeys.BraintreeCustomerId] = \"braintree_customer_id\"\n            }\n        };\n\n        _subscriberService.GetCustomer(organization,\n            Arg.Is<CustomerGetOptions>(options =>\n                options.HasExpansions(\"default_source\", \"invoice_settings.default_payment_method\"))).Returns(customer);\n\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n        var payPalAccount = Substitute.For<PayPalAccount>();\n        payPalAccount.Email.Returns(\"user@gmail.com\");\n        payPalAccount.IsDefault.Returns(true);\n        braintreeCustomer.PaymentMethods.Returns([payPalAccount]);\n        _braintreeService.GetCustomer(customer).Returns(braintreeCustomer);\n\n        var maskedPaymentMethod = await _query.Run(organization);\n\n        Assert.NotNull(maskedPaymentMethod);\n        Assert.True(maskedPaymentMethod.IsT2);\n        var maskedPayPalAccount = maskedPaymentMethod.AsT2;\n        Assert.Equal(\"user@gmail.com\", maskedPayPalAccount.Email);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Payment/Queries/HasPaymentMethodQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Test.Billing.Extensions;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Payment.Queries;\n\nusing static StripeConstants;\n\npublic class HasPaymentMethodQueryTests\n{\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();\n    private readonly HasPaymentMethodQuery _query;\n\n    public HasPaymentMethodQueryTests()\n    {\n        _query = new HasPaymentMethodQuery(\n            _stripeAdapter,\n            _subscriberService);\n    }\n\n    [Fact]\n    public async Task Run_NoCustomer_ReturnsFalse()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        _subscriberService.GetCustomer(organization).ReturnsNull();\n\n        var hasPaymentMethod = await _query.Run(organization);\n\n        Assert.False(hasPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_NoPaymentMethod_ReturnsFalse()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        var hasPaymentMethod = await _query.Run(organization);\n\n        Assert.False(hasPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_HasDefaultPaymentMethodId_ReturnsTrue()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            InvoiceSettings = new CustomerInvoiceSettings\n            {\n                DefaultPaymentMethodId = \"pm_123\"\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        var hasPaymentMethod = await _query.Run(organization);\n\n        Assert.True(hasPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_HasDefaultSourceId_ReturnsTrue()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            DefaultSourceId = \"card_123\",\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        var hasPaymentMethod = await _query.Run(organization);\n\n        Assert.True(hasPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_HasUnverifiedBankAccount_ReturnsTrue()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        _stripeAdapter\n            .ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n                options.Customer == customer.Id &&\n                options.HasExpansions(\"data.payment_method\")))\n            .Returns(\n            [\n                new SetupIntent\n                {\n                    Status = \"requires_action\",\n                    NextAction = new SetupIntentNextAction\n                    {\n                        VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()\n                    },\n                    PaymentMethod = new PaymentMethod\n                    {\n                        UsBankAccount = new PaymentMethodUsBankAccount()\n                    }\n                }\n            ]);\n\n        var hasPaymentMethod = await _query.Run(organization);\n\n        Assert.True(hasPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_HasBraintreeCustomerId_ReturnsTrue()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>\n            {\n                [MetadataKeys.BraintreeCustomerId] = \"braintree_customer_id\"\n            }\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        var hasPaymentMethod = await _query.Run(organization);\n\n        Assert.True(hasPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_NoSetupIntents_ReturnsFalse()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        _stripeAdapter\n            .ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n                options.Customer == customer.Id &&\n                options.HasExpansions(\"data.payment_method\")))\n            .Returns(new List<SetupIntent>());\n\n        var hasPaymentMethod = await _query.Run(organization);\n\n        Assert.False(hasPaymentMethod);\n    }\n\n    [Fact]\n    public async Task Run_SetupIntentNotBankAccount_ReturnsFalse()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid()\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            InvoiceSettings = new CustomerInvoiceSettings(),\n            Metadata = new Dictionary<string, string>()\n        };\n\n        _subscriberService.GetCustomer(organization).Returns(customer);\n\n        _stripeAdapter\n            .ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>\n                options.Customer == customer.Id &&\n                options.HasExpansions(\"data.payment_method\")))\n            .Returns(\n            [\n                new SetupIntent\n                {\n                    PaymentMethod = new PaymentMethod\n                    {\n                        Type = \"card\"\n                    },\n                    Status = \"succeeded\"\n                }\n            ]);\n\n        var hasPaymentMethod = await _query.Run(organization);\n\n        Assert.False(hasPaymentMethod);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Portal/Commands/CreateBillingPortalSessionCommandTests.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Portal.Commands;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Stripe;\nusing Stripe.BillingPortal;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Portal.Commands;\n\nusing static StripeConstants;\n\npublic class CreateBillingPortalSessionCommandTests\n{\n    private readonly ILogger<CreateBillingPortalSessionCommand> _logger = Substitute.For<ILogger<CreateBillingPortalSessionCommand>>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly CreateBillingPortalSessionCommand _command;\n    private readonly User _user;\n\n    public CreateBillingPortalSessionCommandTests()\n    {\n        _command = new CreateBillingPortalSessionCommand(_logger, _stripeAdapter);\n        _user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = \"test@example.com\",\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = \"sub_test123\"\n        };\n    }\n\n    [Fact]\n    public async Task Run_WithValidUser_ReturnsPortalUrl()\n    {\n        // Arrange\n        var returnUrl = \"https://example.com/billing\";\n        var expectedUrl = \"https://billing.stripe.com/session/test123\";\n        var session = new Session { Url = expectedUrl };\n        var subscription = new Subscription { Id = _user.GatewaySubscriptionId, Status = SubscriptionStatus.Active };\n\n        _stripeAdapter.GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _stripeAdapter.CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>())\n            .Returns(session);\n\n        // Act\n        var result = await _command.Run(_user, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.Equal(expectedUrl, result.AsT0);\n\n        await _stripeAdapter.Received(1).GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>());\n        await _stripeAdapter.Received(1).CreateBillingPortalSessionAsync(\n            Arg.Is<SessionCreateOptions>(o =>\n                o.Customer == _user.GatewayCustomerId &&\n                o.ReturnUrl == returnUrl));\n    }\n\n    [Fact]\n    public async Task Run_WithoutGatewayCustomerId_ReturnsConflict()\n    {\n        // Arrange\n        var userWithoutCustomerId = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = \"test@example.com\",\n            GatewayCustomerId = null\n        };\n        var returnUrl = \"https://example.com/billing\";\n\n        // Act\n        var result = await _command.Run(userWithoutCustomerId, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT2);\n        var conflict = result.AsT2;\n        Assert.Equal(\"Unable to create billing portal session. Please contact support for assistance.\", conflict.Response);\n\n        await _stripeAdapter.DidNotReceive().CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>());\n\n        _logger.Received(1).Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => o.ToString()!.Contains(\"does not have a Stripe customer ID\") && o.ToString()!.Contains(userWithoutCustomerId.Id.ToString())),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task Run_WithEmptyGatewayCustomerId_ReturnsConflict()\n    {\n        // Arrange\n        var userWithEmptyCustomerId = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = \"test@example.com\",\n            GatewayCustomerId = string.Empty\n        };\n        var returnUrl = \"https://example.com/billing\";\n\n        // Act\n        var result = await _command.Run(userWithEmptyCustomerId, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT2);\n        var conflict = result.AsT2;\n        Assert.Equal(\"Unable to create billing portal session. Please contact support for assistance.\", conflict.Response);\n\n        await _stripeAdapter.DidNotReceive().CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>());\n\n        _logger.Received(1).Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => o.ToString()!.Contains(\"does not have a Stripe customer ID\") && o.ToString()!.Contains(userWithEmptyCustomerId.Id.ToString())),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task Run_WhenStripeThrowsException_ReturnsUnhandled()\n    {\n        // Arrange\n        var returnUrl = \"https://example.com/billing\";\n        var subscription = new Subscription { Id = _user.GatewaySubscriptionId, Status = SubscriptionStatus.Active };\n        var stripeException = new StripeException { StripeError = new StripeError { Code = \"api_error\" } };\n\n        _stripeAdapter.GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _stripeAdapter.CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>())\n            .Throws(stripeException);\n\n        // Act\n        var result = await _command.Run(_user, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT3);\n        var unhandled = result.AsT3;\n        Assert.Equal(stripeException, unhandled.Exception);\n\n        await _stripeAdapter.Received(1).GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>());\n        await _stripeAdapter.Received(1).CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>());\n    }\n\n    [Fact]\n    public async Task Run_WithDifferentReturnUrls_UsesCorrectUrl()\n    {\n        // Arrange\n        var returnUrl1 = \"https://example.com/billing\";\n        var returnUrl2 = \"https://different.com/account\";\n        var session = new Session { Url = \"https://billing.stripe.com/session/test123\" };\n        var subscription = new Subscription { Id = _user.GatewaySubscriptionId, Status = SubscriptionStatus.Active };\n\n        _stripeAdapter.GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _stripeAdapter.CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>())\n            .Returns(session);\n\n        // Act\n        var result1 = await _command.Run(_user, returnUrl1);\n        var result2 = await _command.Run(_user, returnUrl2);\n\n        // Assert\n        Assert.True(result1.IsT0);\n        Assert.True(result2.IsT0);\n\n        await _stripeAdapter.Received(1).CreateBillingPortalSessionAsync(\n            Arg.Is<SessionCreateOptions>(o => o.ReturnUrl == returnUrl1));\n        await _stripeAdapter.Received(1).CreateBillingPortalSessionAsync(\n            Arg.Is<SessionCreateOptions>(o => o.ReturnUrl == returnUrl2));\n    }\n\n    [Fact]\n    public async Task Run_WithoutGatewaySubscriptionId_ReturnsConflict()\n    {\n        // Arrange\n        var userWithoutSubscriptionId = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = \"test@example.com\",\n            GatewayCustomerId = \"cus_test123\",\n            GatewaySubscriptionId = null\n        };\n        var returnUrl = \"https://example.com/billing\";\n\n        // Act\n        var result = await _command.Run(userWithoutSubscriptionId, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT2);\n        var conflict = result.AsT2;\n        Assert.Equal(\"Unable to create billing portal session. Please contact support for assistance.\", conflict.Response);\n\n        await _stripeAdapter.DidNotReceive().GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());\n        await _stripeAdapter.DidNotReceive().CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>());\n\n        _logger.Received(1).Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => o.ToString()!.Contains(\"does not have a subscription\") && o.ToString()!.Contains(userWithoutSubscriptionId.Id.ToString())),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task Run_WithActiveSubscription_ReturnsPortalUrl()\n    {\n        // Arrange\n        var returnUrl = \"https://example.com/billing\";\n        var expectedUrl = \"https://billing.stripe.com/session/test123\";\n        var session = new Session { Url = expectedUrl };\n        var subscription = new Subscription { Id = _user.GatewaySubscriptionId, Status = SubscriptionStatus.Active };\n\n        _stripeAdapter.GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _stripeAdapter.CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>())\n            .Returns(session);\n\n        // Act\n        var result = await _command.Run(_user, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.Equal(expectedUrl, result.AsT0);\n\n        await _stripeAdapter.Received(1).GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task Run_WithPastDueSubscription_ReturnsPortalUrl()\n    {\n        // Arrange\n        var returnUrl = \"https://example.com/billing\";\n        var expectedUrl = \"https://billing.stripe.com/session/test456\";\n        var session = new Session { Url = expectedUrl };\n        var subscription = new Subscription { Id = _user.GatewaySubscriptionId, Status = SubscriptionStatus.PastDue };\n\n        _stripeAdapter.GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _stripeAdapter.CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>())\n            .Returns(session);\n\n        // Act\n        var result = await _command.Run(_user, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.Equal(expectedUrl, result.AsT0);\n\n        await _stripeAdapter.Received(1).GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task Run_WithCanceledSubscription_ReturnsBadRequest()\n    {\n        // Arrange\n        var returnUrl = \"https://example.com/billing\";\n        var subscription = new Subscription { Id = _user.GatewaySubscriptionId, Status = SubscriptionStatus.Canceled };\n\n        _stripeAdapter.GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        var result = await _command.Run(_user, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Your subscription cannot be managed in its current status.\", badRequest.Response);\n\n        await _stripeAdapter.DidNotReceive().CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>());\n\n        _logger.Received(1).Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => o.ToString()!.Contains(\"not eligible for portal access\") && o.ToString()!.Contains(_user.Id.ToString())),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task Run_WithIncompleteSubscription_ReturnsBadRequest()\n    {\n        // Arrange\n        var returnUrl = \"https://example.com/billing\";\n        var subscription = new Subscription { Id = _user.GatewaySubscriptionId, Status = SubscriptionStatus.Incomplete };\n\n        _stripeAdapter.GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        var result = await _command.Run(_user, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Your subscription cannot be managed in its current status.\", badRequest.Response);\n\n        await _stripeAdapter.DidNotReceive().CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>());\n    }\n\n    [Fact]\n    public async Task Run_WhenSubscriptionFetchFails_ReturnsConflict()\n    {\n        // Arrange\n        var returnUrl = \"https://example.com/billing\";\n        var stripeException = new StripeException { StripeError = new StripeError { Code = \"resource_missing\" } };\n\n        _stripeAdapter.GetSubscriptionAsync(_user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Throws(stripeException);\n\n        // Act\n        var result = await _command.Run(_user, returnUrl);\n\n        // Assert\n        Assert.True(result.IsT2);\n        var conflict = result.AsT2;\n        Assert.Equal(\"Unable to create billing portal session. Please contact support for assistance.\", conflict.Response);\n\n        await _stripeAdapter.DidNotReceive().CreateBillingPortalSessionAsync(Arg.Any<SessionCreateOptions>());\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => o.ToString()!.Contains(\"Failed to fetch subscription\") && o.ToString()!.Contains(_user.Id.ToString())),\n            stripeException,\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs",
    "content": "﻿using Bit.Core.Billing;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Extensions;\nusing Bit.Core.Billing.Payment.Commands;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Braintree;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\nusing Address = Stripe.Address;\nusing PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;\nusing PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;\nusing StripeCustomer = Stripe.Customer;\nusing StripeSubscription = Stripe.Subscription;\n\nnamespace Bit.Core.Test.Billing.Premium.Commands;\n\npublic class CreatePremiumCloudHostedSubscriptionCommandTests\n{\n    private readonly IBraintreeGateway _braintreeGateway = Substitute.For<IBraintreeGateway>();\n    private readonly IBraintreeService _braintreeService = Substitute.For<IBraintreeService>();\n    private readonly IGlobalSettings _globalSettings = Substitute.For<IGlobalSettings>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();\n    private readonly IUserService _userService = Substitute.For<IUserService>();\n    private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IHasPaymentMethodQuery _hasPaymentMethodQuery = Substitute.For<IHasPaymentMethodQuery>();\n    private readonly IUpdatePaymentMethodCommand _updatePaymentMethodCommand = Substitute.For<IUpdatePaymentMethodCommand>();\n    private readonly ISubscriptionDiscountService _subscriptionDiscountService = Substitute.For<ISubscriptionDiscountService>();\n    private readonly CreatePremiumCloudHostedSubscriptionCommand _command;\n\n    public CreatePremiumCloudHostedSubscriptionCommandTests()\n    {\n        var baseServiceUri = Substitute.For<IBaseServiceUriSettings>();\n        baseServiceUri.CloudRegion.Returns(\"US\");\n        _globalSettings.BaseServiceUri.Returns(baseServiceUri);\n\n        // Setup default premium plan with standard pricing\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },\n            Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 }\n        };\n        _pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);\n\n        _command = new CreatePremiumCloudHostedSubscriptionCommand(\n            _braintreeGateway,\n            _braintreeService,\n            _globalSettings,\n            _stripeAdapter,\n            _subscriberService,\n            _userService,\n            _pushNotificationService,\n            Substitute.For<ILogger<CreatePremiumCloudHostedSubscriptionCommand>>(),\n            _pricingClient,\n            _hasPaymentMethodQuery,\n            _updatePaymentMethodCommand,\n            _subscriptionDiscountService);\n    }\n\n    #region Helper Methods\n\n    private static PremiumSubscriptionPurchase CreateSubscriptionPurchase(\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress,\n        short additionalStorageGb = 0,\n        string[]? coupons = null)\n    {\n        return new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = additionalStorageGb,\n            Coupons = coupons\n        };\n    }\n\n    private static StripeCustomer CreateMockCustomer(string customerId = \"cust_123\", string country = \"US\", string postalCode = \"12345\")\n    {\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = customerId;\n        mockCustomer.Address = new Address { Country = country, PostalCode = postalCode };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n        return mockCustomer;\n    }\n\n    private static StripeSubscription CreateMockActiveSubscription(string subscriptionId = \"sub_123\")\n    {\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = subscriptionId;\n        mockSubscription.Status = \"active\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n        return mockSubscription;\n    }\n\n    #endregion\n\n    [Theory, BitAutoData]\n    public async Task Run_UserAlreadyPremium_ReturnsBadRequest(\n        User user,\n        PremiumSubscriptionPurchase subscriptionPurchase)\n    {\n        // Arrange\n        user.Premium = true;\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Already a premium user.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NegativeStorageAmount_ReturnsBadRequest(\n        User user,\n        PremiumSubscriptionPurchase subscriptionPurchase)\n    {\n        // Arrange\n        user.Premium = false;\n        subscriptionPurchase = subscriptionPurchase with { AdditionalStorageGb = -1 };\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Additional storage must be greater than 0.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidPaymentMethodTypes_Card_Success(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"cust_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"active\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        var mockInvoice = Substitute.For<Invoice>();\n\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _stripeAdapter.Received(1).CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        await _userService.Received(1).SaveUserAsync(user);\n        await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidPaymentMethodTypes_PayPal_Success(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.PayPal;\n        paymentMethod.Token = \"paypal_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"cust_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>\n        {\n            [Core.Billing.Utilities.BraintreeCustomerIdKey] = \"bt_customer_123\"\n        };\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"incomplete\";\n        mockSubscription.LatestInvoiceId = \"in_123\";\n\n        var mockInvoice = Substitute.For<Invoice>();\n\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n        _subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns(\"bt_customer_123\");\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _stripeAdapter.Received(1).CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        await _subscriberService.Received(1).CreateBraintreeCustomer(user, paymentMethod.Token);\n        await _stripeAdapter.Received(1).UpdateInvoiceAsync(mockSubscription.LatestInvoiceId,\n            Arg.Is<InvoiceUpdateOptions>(opts =>\n                opts.AutoAdvance == false &&\n                opts.Expand != null &&\n                opts.Expand.Contains(\"customer\")));\n        await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), mockInvoice);\n        await _userService.Received(1).SaveUserAsync(user);\n        await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidRequestWithAdditionalStorage_Success(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n        const short additionalStorage = 2;\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = additionalStorage,\n            Coupons = null\n        };\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"cust_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"active\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        var mockInvoice = Substitute.For<Invoice>();\n\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.True(user.Premium);\n        Assert.Equal((short)(1 + additionalStorage), user.MaxStorageGb);\n        Assert.NotNull(user.LicenseKey);\n        Assert.Equal(20, user.LicenseKey.Length);\n        Assert.NotEqual(default, user.RevisionDate);\n        await _userService.Received(1).SaveUserAsync(user);\n        await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserHasExistingGatewayCustomerIdAndPaymentMethod_UsesExistingCustomer(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = \"existing_customer_123\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"existing_customer_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"active\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        var mockInvoice = Substitute.For<Invoice>();\n\n        // Mock that the user has a payment method (this is the key difference from the credit purchase case)\n        _hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());\n        await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());\n        await _updatePaymentMethodCommand.DidNotReceive().Run(Arg.Any<User>(), Arg.Any<TokenizedPaymentMethod>(), Arg.Any<BillingAddress>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserPreviouslyPurchasedCreditWithoutPaymentMethod_UpdatesPaymentMethodAndCreatesSubscription(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = \"existing_customer_123\"; // Customer exists from previous credit purchase\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"existing_customer_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"active\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        var mockInvoice = Substitute.For<Invoice>();\n        MaskedPaymentMethod mockMaskedPaymentMethod = new MaskedCard\n        {\n            Brand = \"visa\",\n            Last4 = \"1234\",\n            Expiration = \"12/2025\"\n        };\n\n        // Mock that the user does NOT have a payment method (simulating credit purchase scenario)\n        _hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(false);\n        _updatePaymentMethodCommand.Run(Arg.Any<User>(), Arg.Any<TokenizedPaymentMethod>(), Arg.Any<BillingAddress>())\n            .Returns(mockMaskedPaymentMethod);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        // Verify that update payment method was called (new behavior for credit purchase case)\n        await _updatePaymentMethodCommand.Received(1).Run(user, paymentMethod, billingAddress);\n        // Verify GetCustomerOrThrow was called after updating payment method\n        await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());\n        // Verify no new customer was created\n        await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());\n        // Verify subscription was created\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        // Verify user was updated correctly\n        Assert.True(user.Premium);\n        await _userService.Received(1).SaveUserAsync(user);\n        await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_PayPalWithIncompleteSubscription_SetsPremiumTrue(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        user.PremiumExpirationDate = null;\n        paymentMethod.Type = TokenizablePaymentMethodType.PayPal;\n        paymentMethod.Token = \"paypal_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"cust_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>\n        {\n            [Core.Billing.Utilities.BraintreeCustomerIdKey] = \"bt_customer_123\"\n        };\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"incomplete\";\n        mockSubscription.LatestInvoiceId = \"in_123\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        var mockInvoice = Substitute.For<Invoice>();\n\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);\n        _subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns(\"bt_customer_123\");\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.True(user.Premium);\n        Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);\n        await _stripeAdapter.Received(1).UpdateInvoiceAsync(mockSubscription.LatestInvoiceId,\n            Arg.Is<InvoiceUpdateOptions>(opts =>\n                opts.AutoAdvance == false &&\n                opts.Expand != null &&\n                opts.Expand.Contains(\"customer\")));\n        await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), mockInvoice);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NonPayPalWithActiveSubscription_SetsPremiumTrue(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"cust_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"active\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        var mockInvoice = Substitute.For<Invoice>();\n\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.True(user.Premium);\n        Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_SubscriptionStatusDoesNotMatchPatterns_DoesNotSetPremium(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        user.PremiumExpirationDate = null;\n        paymentMethod.Type = TokenizablePaymentMethodType.PayPal;\n        paymentMethod.Token = \"paypal_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"cust_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>\n        {\n            [Core.Billing.Utilities.BraintreeCustomerIdKey] = \"bt_customer_123\"\n        };\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"active\"; // PayPal + active doesn't match pattern\n        mockSubscription.LatestInvoiceId = \"in_123\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        var mockInvoice = Substitute.For<Invoice>();\n\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);\n        _subscriberService.CreateBraintreeCustomer(Arg.Any<User>(), Arg.Any<string>()).Returns(\"bt_customer_123\");\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.False(user.Premium);\n        Assert.Null(user.PremiumExpirationDate);\n        await _stripeAdapter.Received(1).UpdateInvoiceAsync(mockSubscription.LatestInvoiceId,\n            Arg.Is<InvoiceUpdateOptions>(opts =>\n                opts.AutoAdvance == false &&\n                opts.Expand != null &&\n                opts.Expand.Contains(\"customer\")));\n        await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), mockInvoice);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_AccountCredit_WithExistingCustomer_Success(\n        User user,\n        NonTokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = \"existing_customer_123\";\n        paymentMethod.Type = NonTokenizablePaymentMethodType.AccountCredit;\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"existing_customer_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"active\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        var mockInvoice = Substitute.For<Invoice>();\n\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateInvoiceAsync(Arg.Any<string>(), Arg.Any<InvoiceUpdateOptions>()).Returns(mockInvoice);\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>());\n        await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());\n        Assert.True(user.Premium);\n        Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NonTokenizedPaymentWithoutExistingCustomer_ThrowsBillingException(\n        User user,\n        NonTokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        // No existing gateway customer ID\n        user.GatewayCustomerId = null;\n        paymentMethod.Type = NonTokenizablePaymentMethodType.AccountCredit;\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        //Assert\n        Assert.True(result.IsT3); // Assuming T3 is the Unhandled result\n        Assert.IsType<BillingException>(result.AsT3.Exception);\n        // Verify no customer was created or subscription attempted\n        await _stripeAdapter.DidNotReceive().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>());\n        await _stripeAdapter.DidNotReceive().CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithAdditionalStorage_SetsCorrectMaxStorageGb(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n        const short additionalStorage = 2;\n\n        // Setup premium plan with 5GB provided storage\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },\n            Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 }\n        };\n        _pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"cust_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var mockSubscription = Substitute.For<StripeSubscription>();\n        mockSubscription.Id = \"sub_123\";\n        mockSubscription.Status = \"active\";\n        mockSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = additionalStorage,\n            Coupons = null\n        };\n\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        Assert.Equal((short)3, user.MaxStorageGb); // 1 (provided) + 2 (additional) = 3\n        await _userService.Received(1).SaveUserAsync(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserWithCanceledSubscription_AllowsResubscribe(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true; // User still has Premium flag set\n        user.GatewayCustomerId = \"existing_customer_123\";\n        user.GatewaySubscriptionId = \"sub_canceled_123\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var existingCanceledSubscription = Substitute.For<StripeSubscription>();\n        existingCanceledSubscription.Id = \"sub_canceled_123\";\n        existingCanceledSubscription.Status = \"canceled\"; // Terminal status\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"existing_customer_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var newSubscription = Substitute.For<StripeSubscription>();\n        newSubscription.Id = \"sub_new_123\";\n        newSubscription.Status = \"active\";\n        newSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId).Returns(existingCanceledSubscription);\n        _hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        // Act\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0\n        };\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0); // Should succeed, not return \"Already a premium user\"\n        Assert.True(user.Premium);\n        Assert.Equal(newSubscription.Id, user.GatewaySubscriptionId);\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        await _userService.Received(1).SaveUserAsync(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserWithIncompleteExpiredSubscription_AllowsResubscribe(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true; // User still has Premium flag set\n        user.GatewayCustomerId = \"existing_customer_123\";\n        user.GatewaySubscriptionId = \"sub_incomplete_expired_123\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var existingExpiredSubscription = Substitute.For<StripeSubscription>();\n        existingExpiredSubscription.Id = \"sub_incomplete_expired_123\";\n        existingExpiredSubscription.Status = \"incomplete_expired\"; // Terminal status\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"existing_customer_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var newSubscription = Substitute.For<StripeSubscription>();\n        newSubscription.Id = \"sub_new_123\";\n        newSubscription.Status = \"active\";\n        newSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId).Returns(existingExpiredSubscription);\n        _hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        // Act\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0\n        };\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0); // Should succeed, not return \"Already a premium user\"\n        Assert.True(user.Premium);\n        Assert.Equal(newSubscription.Id, user.GatewaySubscriptionId);\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        await _userService.Received(1).SaveUserAsync(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserWithActiveSubscription_PremiumTrue_ReturnsBadRequest(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_active_123\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n\n        var existingActiveSubscription = Substitute.For<StripeSubscription>();\n        existingActiveSubscription.Id = \"sub_active_123\";\n        existingActiveSubscription.Status = \"active\"; // NOT a terminal status\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId).Returns(existingActiveSubscription);\n\n        // Act\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0\n        };\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Already a premium user.\", badRequest.Response);\n        // Verify no subscription creation was attempted\n        await _stripeAdapter.DidNotReceive().CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_SubscriptionFetchThrows_ProceedsWithCreation(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = \"existing_customer_123\";\n        user.GatewaySubscriptionId = \"sub_nonexistent_123\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        // Simulate Stripe exception when fetching subscription (e.g., subscription doesn't exist)\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId)\n            .Returns<StripeSubscription>(_ => throw new Stripe.StripeException(\"Subscription not found\"));\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"existing_customer_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var newSubscription = Substitute.For<StripeSubscription>();\n        newSubscription.Id = \"sub_new_123\";\n        newSubscription.Status = \"active\";\n        newSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        _hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        // Act\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0\n        };\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert - Should proceed successfully despite the exception\n        Assert.True(result.IsT0);\n        Assert.True(user.Premium);\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        await _userService.Received(1).SaveUserAsync(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ResubscribeWithTerminalSubscription_UpdatesPaymentMethod(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewayCustomerId = \"existing_customer_123\";\n        user.GatewaySubscriptionId = \"sub_canceled_123\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"new_card_token_456\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var existingCanceledSubscription = Substitute.For<StripeSubscription>();\n        existingCanceledSubscription.Id = \"sub_canceled_123\";\n        existingCanceledSubscription.Status = \"canceled\"; // Terminal status\n\n        var mockCustomer = Substitute.For<StripeCustomer>();\n        mockCustomer.Id = \"existing_customer_123\";\n        mockCustomer.Address = new Address { Country = \"US\", PostalCode = \"12345\" };\n        mockCustomer.Metadata = new Dictionary<string, string>();\n\n        var newSubscription = Substitute.For<StripeSubscription>();\n        newSubscription.Id = \"sub_new_123\";\n        newSubscription.Status = \"active\";\n        newSubscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data =\n            [\n                new SubscriptionItem\n                {\n                    CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)\n                }\n            ]\n        };\n\n        MaskedPaymentMethod mockMaskedPaymentMethod = new MaskedCard\n        {\n            Brand = \"visa\",\n            Last4 = \"4567\",\n            Expiration = \"12/2026\"\n        };\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId).Returns(existingCanceledSubscription);\n        _hasPaymentMethodQuery.Run(Arg.Any<User>()).Returns(true); // Has old payment method\n        _updatePaymentMethodCommand.Run(Arg.Any<User>(), Arg.Any<TokenizedPaymentMethod>(), Arg.Any<BillingAddress>())\n            .Returns(mockMaskedPaymentMethod);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        // Act\n        var subscriptionPurchase = new PremiumSubscriptionPurchase\n        {\n            PaymentMethod = paymentMethod,\n            BillingAddress = billingAddress,\n            AdditionalStorageGb = 0\n        };\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        // Verify payment method was updated because of terminal subscription\n        await _updatePaymentMethodCommand.Received(1).Run(user, paymentMethod, billingAddress);\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        await _userService.Received(1).SaveUserAsync(user);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidCoupon_AppliesCouponSuccessfully(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n\n        var subscriptionPurchase = CreateSubscriptionPurchase(paymentMethod, billingAddress, coupons: [\"VALID_COUPON\"]);\n        var mockCustomer = CreateMockCustomer();\n        var mockSubscription = CreateMockActiveSubscription();\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_COUPON\" })), DiscountTierType.Premium)\n            .Returns(true);\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _subscriptionDiscountService.Received(1).ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_COUPON\" })), DiscountTierType.Premium);\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(opts =>\n            opts.Discounts != null &&\n            opts.Discounts.Count == 1 &&\n            opts.Discounts[0].Coupon == \"VALID_COUPON\"));\n        await _userService.Received(1).SaveUserAsync(user);\n        await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_InvalidCoupon_ReturnsBadRequest(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n\n        var subscriptionPurchase = CreateSubscriptionPurchase(paymentMethod, billingAddress, coupons: [\"INVALID_COUPON\"]);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"INVALID_COUPON\" })), DiscountTierType.Premium)\n            .Returns(false);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Discount expired. Please review your cart total and try again\", badRequest.Response);\n        await _subscriptionDiscountService.Received(1).ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"INVALID_COUPON\" })), DiscountTierType.Premium);\n        await _stripeAdapter.DidNotReceive().CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());\n        await _pushNotificationService.DidNotReceive().PushSyncVaultAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserNotEligibleForCoupon_ReturnsBadRequest(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = \"existing_customer_123\";\n        user.GatewaySubscriptionId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n\n        var subscriptionPurchase = CreateSubscriptionPurchase(paymentMethod, billingAddress, coupons: [\"NEW_USER_ONLY_COUPON\"]);\n\n        // User has previous subscriptions, so they're not eligible\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"NEW_USER_ONLY_COUPON\" })), DiscountTierType.Premium)\n            .Returns(false);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Discount expired. Please review your cart total and try again\", badRequest.Response);\n        await _subscriptionDiscountService.Received(1).ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"NEW_USER_ONLY_COUPON\" })), DiscountTierType.Premium);\n        await _stripeAdapter.DidNotReceive().CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n        await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());\n        await _pushNotificationService.DidNotReceive().PushSyncVaultAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_CouponWithWhitespace_TrimsCouponCode(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n\n        var subscriptionPurchase = CreateSubscriptionPurchase(paymentMethod, billingAddress, coupons: [\"  WHITESPACE_COUPON  \"]);\n        var mockCustomer = CreateMockCustomer();\n        var mockSubscription = CreateMockActiveSubscription();\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"WHITESPACE_COUPON\" })), DiscountTierType.Premium)\n            .Returns(true);\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n        _subscriberService.GetCustomerOrThrow(Arg.Any<User>(), Arg.Any<CustomerGetOptions>()).Returns(mockCustomer);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        // Verify the coupon was trimmed before validation\n        await _subscriptionDiscountService.Received(1).ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"WHITESPACE_COUPON\" })), DiscountTierType.Premium);\n        // Verify the coupon was trimmed before passing to Stripe\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(opts =>\n            opts.Discounts != null &&\n            opts.Discounts.Count == 1 &&\n            opts.Discounts[0].Coupon == \"WHITESPACE_COUPON\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithMultipleValidCoupons_CreatesSubscriptionWithAllCoupons(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n\n        var subscriptionPurchase = CreateSubscriptionPurchase(paymentMethod, billingAddress, coupons: [\"COUPON_ONE\", \"COUPON_TWO\"]);\n        var mockCustomer = CreateMockCustomer();\n        var mockSubscription = CreateMockActiveSubscription();\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"COUPON_ONE\", \"COUPON_TWO\" })), DiscountTierType.Premium).Returns(true);\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(opts =>\n            opts.Discounts != null &&\n            opts.Discounts.Count == 2 &&\n            opts.Discounts.Any(d => d.Coupon == \"COUPON_ONE\") &&\n            opts.Discounts.Any(d => d.Coupon == \"COUPON_TWO\")));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithOneInvalidCoupon_ReturnsBadRequest(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n\n        var subscriptionPurchase = CreateSubscriptionPurchase(paymentMethod, billingAddress, coupons: [\"VALID_COUPON\", \"INVALID_COUPON\"]);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            user, Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_COUPON\", \"INVALID_COUPON\" })), DiscountTierType.Premium).Returns(false);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT1);\n        Assert.Equal(\"Discount expired. Please review your cart total and try again\", result.AsT1.Response);\n        await _stripeAdapter.DidNotReceive().CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithNullCoupons_CreatesSubscriptionWithoutDiscount(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n\n        var subscriptionPurchase = CreateSubscriptionPurchase(paymentMethod, billingAddress, coupons: null);\n        var mockCustomer = CreateMockCustomer();\n        var mockSubscription = CreateMockActiveSubscription();\n\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _subscriptionDiscountService.DidNotReceive().ValidateDiscountEligibilityForUserAsync(\n            Arg.Any<User>(), Arg.Any<IReadOnlyList<string>>(), Arg.Any<DiscountTierType>());\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(opts =>\n            opts.Discounts == null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithEmptyCouponsArray_CreatesSubscriptionWithoutDiscount(\n        User user,\n        TokenizedPaymentMethod paymentMethod,\n        BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        user.Email = \"test@example.com\";\n        paymentMethod.Type = TokenizablePaymentMethodType.Card;\n        paymentMethod.Token = \"card_token_123\";\n\n        var subscriptionPurchase = CreateSubscriptionPurchase(paymentMethod, billingAddress, coupons: []);\n        var mockCustomer = CreateMockCustomer();\n        var mockSubscription = CreateMockActiveSubscription();\n\n        _stripeAdapter.CreateCustomerAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(mockCustomer);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);\n\n        // Act\n        var result = await _command.Run(user, subscriptionPurchase);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _subscriptionDiscountService.DidNotReceive().ValidateDiscountEligibilityForUserAsync(\n            Arg.Any<User>(), Arg.Any<IReadOnlyList<string>>(), Arg.Any<DiscountTierType>());\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(opts =>\n            opts.Discounts == null));\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommandTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Services;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Premium.Commands;\n\npublic class CreatePremiumSelfHostedSubscriptionCommandTests\n{\n    private readonly ILicensingService _licensingService = Substitute.For<ILicensingService>();\n    private readonly IUserService _userService = Substitute.For<IUserService>();\n    private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();\n    private readonly CreatePremiumSelfHostedSubscriptionCommand _command;\n\n    public CreatePremiumSelfHostedSubscriptionCommandTests()\n    {\n        _command = new CreatePremiumSelfHostedSubscriptionCommand(\n            _licensingService,\n            _userService,\n            _pushNotificationService,\n            Substitute.For<ILogger<CreatePremiumSelfHostedSubscriptionCommand>>());\n    }\n\n    [Fact]\n    public async Task Run_UserAlreadyPremium_ReturnsBadRequest()\n    {\n        // Arrange\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Premium = true\n        };\n\n        var license = new UserLicense\n        {\n            LicenseKey = \"test_key\",\n            Expires = DateTime.UtcNow.AddYears(1)\n        };\n\n        // Act\n        var result = await _command.Run(user, license);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Already a premium user.\", badRequest.Response);\n    }\n\n    [Fact]\n    public async Task Run_InvalidLicense_ReturnsBadRequest()\n    {\n        // Arrange\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Premium = false\n        };\n\n        var license = new UserLicense\n        {\n            LicenseKey = \"invalid_key\",\n            Expires = DateTime.UtcNow.AddYears(1)\n        };\n\n        _licensingService.VerifyLicense(license).Returns(false);\n\n        // Act\n        var result = await _command.Run(user, license);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Invalid license.\", badRequest.Response);\n    }\n\n    [Fact]\n    public async Task Run_LicenseCannotBeUsed_EmailNotVerified_ReturnsBadRequest()\n    {\n        // Arrange\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Premium = false,\n            Email = \"test@example.com\",\n            EmailVerified = false\n        };\n\n        var license = new UserLicense\n        {\n            LicenseKey = \"test_key\",\n            Expires = DateTime.UtcNow.AddYears(1),\n            Token = \"valid_token\"\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[]\n        {\n            new Claim(\"Email\", \"test@example.com\")\n        }));\n\n        _licensingService.VerifyLicense(license).Returns(true);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);\n\n        // Act\n        var result = await _command.Run(user, license);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Contains(\"The user's email is not verified.\", badRequest.Response);\n    }\n\n    [Fact]\n    public async Task Run_LicenseCannotBeUsed_EmailMismatch_ReturnsBadRequest()\n    {\n        // Arrange\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Premium = false,\n            Email = \"user@example.com\",\n            EmailVerified = true\n        };\n\n        var license = new UserLicense\n        {\n            LicenseKey = \"test_key\",\n            Expires = DateTime.UtcNow.AddYears(1),\n            Token = \"valid_token\"\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[]\n        {\n            new Claim(\"Email\", \"license@example.com\")\n        }));\n\n        _licensingService.VerifyLicense(license).Returns(true);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);\n\n        // Act\n        var result = await _command.Run(user, license);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Contains(\"The user's email does not match the license email.\", badRequest.Response);\n    }\n\n    [Fact]\n    public async Task Run_ValidRequest_Success()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var user = new User\n        {\n            Id = userId,\n            Premium = false,\n            Email = \"test@example.com\",\n            EmailVerified = true\n        };\n\n        var license = new UserLicense\n        {\n            LicenseKey = \"test_key_12345\",\n            Expires = DateTime.UtcNow.AddYears(1),\n            Token = \"valid_token\"\n        };\n\n        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[]\n        {\n            new Claim(\"Email\", \"test@example.com\")\n        }));\n\n        _licensingService.VerifyLicense(license).Returns(true);\n        _licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal);\n\n        // Act\n        var result = await _command.Run(user, license);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify user was updated correctly\n        Assert.True(user.Premium);\n        Assert.NotNull(user.LicenseKey);\n        Assert.Equal(license.LicenseKey, user.LicenseKey);\n        Assert.NotEqual(default, user.RevisionDate);\n\n        // Verify services were called\n        await _licensingService.Received(1).WriteUserLicenseAsync(user, license);\n        await _userService.Received(1).SaveUserAsync(user);\n        await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\nusing static Bit.Core.Billing.Constants.StripeConstants;\nusing PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;\nusing PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;\n\nnamespace Bit.Core.Test.Billing.Premium.Commands;\n\npublic class PreviewPremiumTaxCommandTests\n{\n    private readonly ILogger<PreviewPremiumTaxCommand> _logger = Substitute.For<ILogger<PreviewPremiumTaxCommand>>();\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly ISubscriptionDiscountService _subscriptionDiscountService = Substitute.For<ISubscriptionDiscountService>();\n    private readonly PreviewPremiumTaxCommand _command;\n    private readonly User _user;\n\n    public PreviewPremiumTaxCommandTests()\n    {\n        // Setup default premium plan with standard pricing\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new PremiumPurchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually },\n            Storage = new PremiumPurchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal }\n        };\n        _pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);\n\n        _user = new User { Id = Guid.NewGuid(), Email = \"test@example.com\" };\n\n        _command = new PreviewPremiumTaxCommand(_logger, _pricingClient, _stripeAdapter, _subscriptionDiscountService);\n    }\n\n    #region Helper Methods\n\n    private static PremiumPurchasePreview CreatePreview(short additionalStorageGb = 0, string[]? coupons = null)\n    {\n        return new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = additionalStorageGb,\n            Coupons = coupons\n        };\n    }\n\n    private static BillingAddress CreateBillingAddress(string country = \"US\", string postalCode = \"12345\")\n    {\n        return new BillingAddress\n        {\n            Country = country,\n            PostalCode = postalCode\n        };\n    }\n\n    #endregion\n\n    [Fact]\n    public async Task Run_PremiumWithoutStorage_ReturnsCorrectTaxAmounts()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually &&\n            options.SubscriptionDetails.Items[0].Quantity == 1));\n    }\n\n    [Fact]\n    public async Task Run_PremiumWithAdditionalStorage_ReturnsCorrectTaxAmounts()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"CA\",\n            PostalCode = \"K1A 0A6\"\n        };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 500 }],\n            Total = 5500\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = 5,\n            Coupons = null\n        };\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(5.00m, tax);\n        Assert.Equal(55.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"CA\" &&\n            options.CustomerDetails.Address.PostalCode == \"K1A 0A6\" &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == Prices.PremiumAnnually && item.Quantity == 1) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == Prices.StoragePlanPersonal && item.Quantity == 5)));\n    }\n\n    [Fact]\n    public async Task Run_PremiumWithZeroStorage_ExcludesStorageFromItems()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"GB\",\n            PostalCode = \"SW1A 1AA\"\n        };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 250 }],\n            Total = 2750\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(2.50m, tax);\n        Assert.Equal(27.50m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"GB\" &&\n            options.CustomerDetails.Address.PostalCode == \"SW1A 1AA\" &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually &&\n            options.SubscriptionDetails.Items[0].Quantity == 1));\n    }\n\n    [Fact]\n    public async Task Run_PremiumWithLargeStorage_HandlesMultipleStorageUnits()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"DE\",\n            PostalCode = \"10115\"\n        };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],\n            Total = 8800\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = 20,\n            Coupons = null\n        };\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(8.00m, tax);\n        Assert.Equal(88.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"DE\" &&\n            options.CustomerDetails.Address.PostalCode == \"10115\" &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == Prices.PremiumAnnually && item.Quantity == 1) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == Prices.StoragePlanPersonal && item.Quantity == 20)));\n    }\n\n    [Fact]\n    public async Task Run_PremiumInternationalAddress_UsesCorrectAddressInfo()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"AU\",\n            PostalCode = \"2000\"\n        };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 450 }],\n            Total = 4950\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = 10,\n            Coupons = null\n        };\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(4.50m, tax);\n        Assert.Equal(49.50m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"AU\" &&\n            options.CustomerDetails.Address.PostalCode == \"2000\" &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == Prices.PremiumAnnually && item.Quantity == 1) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == Prices.StoragePlanPersonal && item.Quantity == 10)));\n    }\n\n    [Fact]\n    public async Task Run_PremiumNoTax_ReturnsZeroTax()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"97330\" // Example of a tax-free jurisdiction\n        };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 0 }],\n            Total = 3000\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(0.00m, tax);\n        Assert.Equal(30.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"97330\" &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually &&\n            options.SubscriptionDetails.Items[0].Quantity == 1));\n    }\n\n    [Fact]\n    public async Task Run_NegativeStorage_TreatedAsZero()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"FR\",\n            PostalCode = \"75001\"\n        };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 600 }],\n            Total = 6600\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = -5,\n            Coupons = null\n        };\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(6.00m, tax);\n        Assert.Equal(66.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"FR\" &&\n            options.CustomerDetails.Address.PostalCode == \"75001\" &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually &&\n            options.SubscriptionDetails.Items[0].Quantity == 1));\n    }\n\n    [Fact]\n    public async Task Run_AmountConversion_CorrectlyConvertsStripeAmounts()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        // Stripe amounts are in cents\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 123 }], // $1.23\n            Total = 3123 // $31.23\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(1.23m, tax);\n        Assert.Equal(31.23m, total);\n    }\n\n    [Fact]\n    public async Task Run_WithValidCoupon_IncludesCouponInInvoicePreview()\n    {\n        var billingAddress = CreateBillingAddress();\n        var preview = CreatePreview(coupons: [\"VALID_COUPON_CODE\"]);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_COUPON_CODE\" })),\n            DiscountTierType.Premium).Returns(true);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == \"VALID_COUPON_CODE\" &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually &&\n            options.SubscriptionDetails.Items[0].Quantity == 1));\n    }\n\n    [Fact]\n    public async Task Run_WithCouponAndStorage_IncludesBothInInvoicePreview()\n    {\n        var billingAddress = CreateBillingAddress(country: \"CA\", postalCode: \"K1A 0A6\");\n        var preview = CreatePreview(additionalStorageGb: 5, coupons: [\"STORAGE_DISCOUNT\"]);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"STORAGE_DISCOUNT\" })),\n            DiscountTierType.Premium).Returns(true);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 450 }],\n            Total = 4950\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(4.50m, tax);\n        Assert.Equal(49.50m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"CA\" &&\n            options.CustomerDetails.Address.PostalCode == \"K1A 0A6\" &&\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == \"STORAGE_DISCOUNT\" &&\n            options.SubscriptionDetails.Items.Count == 2 &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == Prices.PremiumAnnually && item.Quantity == 1) &&\n            options.SubscriptionDetails.Items.Any(item =>\n                item.Price == Prices.StoragePlanPersonal && item.Quantity == 5)));\n    }\n\n    [Fact]\n    public async Task Run_WithCouponWhitespace_TrimsCouponCode()\n    {\n        var billingAddress = CreateBillingAddress(country: \"GB\", postalCode: \"SW1A 1AA\");\n        var preview = CreatePreview(coupons: [\"  WHITESPACE_COUPON  \"]);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"WHITESPACE_COUPON\" })),\n            DiscountTierType.Premium).Returns(true);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 250 }],\n            Total = 2750\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(2.50m, tax);\n        Assert.Equal(27.50m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"GB\" &&\n            options.CustomerDetails.Address.PostalCode == \"SW1A 1AA\" &&\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == \"WHITESPACE_COUPON\" &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually &&\n            options.SubscriptionDetails.Items[0].Quantity == 1));\n    }\n\n    [Fact]\n    public async Task Run_WithNullCoupon_ExcludesCouponFromInvoicePreview()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = 0,\n            Coupons = null\n        };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.Discounts == null &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually &&\n            options.SubscriptionDetails.Items[0].Quantity == 1));\n    }\n\n    [Fact]\n    public async Task Run_WithEmptyCoupon_ExcludesCouponFromInvoicePreview()\n    {\n        var billingAddress = new BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var preview = new PremiumPurchasePreview\n        {\n            AdditionalStorageGb = 0,\n            Coupons = [\"\"]\n        };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.Currency == \"usd\" &&\n            options.CustomerDetails.Address.Country == \"US\" &&\n            options.CustomerDetails.Address.PostalCode == \"12345\" &&\n            options.Discounts == null &&\n            options.SubscriptionDetails.Items.Count == 1 &&\n            options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually &&\n            options.SubscriptionDetails.Items[0].Quantity == 1));\n    }\n\n    [Fact]\n    public async Task Run_WithValidCoupon_ValidatesCouponAndAppliesDiscount()\n    {\n        var billingAddress = CreateBillingAddress();\n        var preview = CreatePreview(coupons: [\"VALID_DISCOUNT\"]);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_DISCOUNT\" })),\n            DiscountTierType.Premium).Returns(true);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 200 }],\n            Total = 2200\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(2.00m, tax);\n        Assert.Equal(22.00m, total);\n\n        await _subscriptionDiscountService.Received(1).ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_DISCOUNT\" })),\n            DiscountTierType.Premium);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts != null &&\n            options.Discounts.Count == 1 &&\n            options.Discounts[0].Coupon == \"VALID_DISCOUNT\"));\n    }\n\n    [Fact]\n    public async Task Run_WithInvalidCoupon_IgnoresCouponAndProceeds()\n    {\n        var billingAddress = CreateBillingAddress();\n        var preview = CreatePreview(coupons: [\"INVALID_COUPON\"]);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"INVALID_COUPON\" })),\n            DiscountTierType.Premium).Returns(false);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        await _subscriptionDiscountService.Received(1).ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"INVALID_COUPON\" })),\n            DiscountTierType.Premium);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts == null || options.Discounts.Count == 0));\n    }\n\n    [Fact]\n    public async Task Run_WithCouponForUserWithPreviousSubscription_IgnoresCouponAndProceeds()\n    {\n        var billingAddress = CreateBillingAddress();\n        var preview = CreatePreview(coupons: [\"NEW_USER_ONLY\"]);\n\n        // User has previous subscription, so validation fails\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"NEW_USER_ONLY\" })),\n            DiscountTierType.Premium).Returns(false);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n        var (tax, total) = result.AsT0;\n        Assert.Equal(3.00m, tax);\n        Assert.Equal(33.00m, total);\n\n        await _subscriptionDiscountService.Received(1).ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"NEW_USER_ONLY\" })),\n            DiscountTierType.Premium);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts == null || options.Discounts.Count == 0));\n    }\n\n    [Fact]\n    public async Task Run_WithMultipleValidCoupons_AppliesBothToInvoicePreview()\n    {\n        var billingAddress = CreateBillingAddress();\n        var preview = CreatePreview(coupons: [\"COUPON_ONE\", \"COUPON_TWO\"]);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"COUPON_ONE\", \"COUPON_TWO\" })),\n            DiscountTierType.Premium).Returns(true);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 200 }],\n            Total = 2200\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts != null &&\n            options.Discounts.Count == 2 &&\n            options.Discounts.Any(d => d.Coupon == \"COUPON_ONE\") &&\n            options.Discounts.Any(d => d.Coupon == \"COUPON_TWO\")));\n    }\n\n    [Fact]\n    public async Task Run_WithMixedValidAndInvalidCoupons_SkipsAllDiscounts()\n    {\n        var billingAddress = CreateBillingAddress();\n        var preview = CreatePreview(coupons: [\"VALID_COUPON\", \"INVALID_COUPON\"]);\n\n        _subscriptionDiscountService.ValidateDiscountEligibilityForUserAsync(\n            _user,\n            Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_COUPON\", \"INVALID_COUPON\" })),\n            DiscountTierType.Premium).Returns(false);\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts == null || options.Discounts.Count == 0));\n    }\n\n    [Fact]\n    public async Task Run_WithNullCoupons_DoesNotApplyDiscounts()\n    {\n        var billingAddress = CreateBillingAddress();\n        var preview = new PremiumPurchasePreview { AdditionalStorageGb = 0, Coupons = null };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n\n        await _subscriptionDiscountService.DidNotReceive().ValidateDiscountEligibilityForUserAsync(\n            Arg.Any<User>(), Arg.Any<IReadOnlyList<string>>(), Arg.Any<DiscountTierType>());\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts == null));\n    }\n\n    [Fact]\n    public async Task Run_WithEmptyCouponsArray_DoesNotApplyDiscounts()\n    {\n        var billingAddress = CreateBillingAddress();\n        var preview = new PremiumPurchasePreview { AdditionalStorageGb = 0, Coupons = [] };\n\n        var invoice = new Invoice\n        {\n            TotalTaxes = [new InvoiceTotalTax { Amount = 300 }],\n            Total = 3300\n        };\n\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>()).Returns(invoice);\n\n        var result = await _command.Run(_user, preview, billingAddress);\n\n        Assert.True(result.IsT0);\n\n        await _subscriptionDiscountService.DidNotReceive().ValidateDiscountEligibilityForUserAsync(\n            Arg.Any<User>(), Arg.Any<IReadOnlyList<string>>(), Arg.Any<DiscountTierType>());\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>\n            options.Discounts == null));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Premium/Commands/PreviewPremiumUpgradeProrationCommandTests.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Payment.Models;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\nusing PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;\n\nnamespace Bit.Core.Test.Billing.Premium.Commands;\n\npublic class PreviewPremiumUpgradeProrationCommandTests\n{\n    private readonly ILogger<PreviewPremiumUpgradeProrationCommand> _logger = Substitute.For<ILogger<PreviewPremiumUpgradeProrationCommand>>();\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly PreviewPremiumUpgradeProrationCommand _command;\n\n    public PreviewPremiumUpgradeProrationCommandTests()\n    {\n        _command = new PreviewPremiumUpgradeProrationCommand(\n            _logger,\n            _pricingClient,\n            _stripeAdapter);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserWithoutPremium_ReturnsBadRequest(User user, BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = false;\n\n        // Act\n        var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"User does not have an active Premium subscription.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserWithoutGatewaySubscriptionId_ReturnsBadRequest(User user, BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = null;\n\n        // Act\n        var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"User does not have an active Premium subscription.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidUpgrade_ReturnsProrationAmounts(User user, BillingAddress billingAddress)\n    {\n        // Arrange - Setup valid Premium user\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        // Setup Premium plans\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"premium-annually\",\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"storage-gb-annually\",\n                Price = 4m,\n                Provided = 1\n            }\n        };\n\n        var premiumPlans = new List<PremiumPlan> { premiumPlan };\n\n        // Setup current Stripe subscription\n        var now = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);\n        var currentPeriodEnd = now.AddMonths(6);\n        var currentSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Customer = new Customer\n            {\n                Id = \"cus_123\",\n                Discount = null\n            },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new()\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" },\n                        CurrentPeriodEnd = currentPeriodEnd\n                    }\n                }\n            }\n        };\n\n        // Setup target organization plan\n        var targetPlan = new TeamsPlan(isAnnual: true);\n\n        // Setup invoice preview response\n        var invoice = new Invoice\n        {\n            Total = 5000, // $50.00\n            TotalTaxes = new List<InvoiceTotalTax>\n            {\n                new() { Amount = 500 } // $5.00\n            },\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = new List<InvoiceLineItem>\n                {\n                    new() { Amount = 5000 }  // $50.00 for new plan\n                }\n            },\n            PeriodEnd = now\n        };\n\n        // Configure mocks\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);\n        _stripeAdapter.GetSubscriptionAsync(\n            \"sub_123\",\n            Arg.Any<SubscriptionGetOptions>())\n            .Returns(currentSubscription);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        // Act\n        var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT0);\n        var proration = result.AsT0;\n        Assert.Equal(50.00m, proration.NewPlanProratedAmount);\n        Assert.Equal(0m, proration.Credit);\n        Assert.Equal(5.00m, proration.Tax);\n        Assert.Equal(50.00m, proration.Total);\n        Assert.Equal(6, proration.NewPlanProratedMonths); // 6 months remaining\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidUpgrade_ExtractsProrationCredit(User user, BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"premium-annually\",\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"storage-gb-annually\",\n                Price = 4m,\n                Provided = 1\n            }\n        };\n        var premiumPlans = new List<PremiumPlan> { premiumPlan };\n\n        // Use fixed time to avoid DateTime.UtcNow differences\n        var now = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);\n        var currentPeriodEnd = now.AddDays(45); // 1.5 months ~ 2 months rounded\n        var currentSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Customer = new Customer { Id = \"cus_123\", Discount = null },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new() { Id = \"si_premium\", Price = new Price { Id = \"premium-annually\" }, CurrentPeriodEnd = currentPeriodEnd }\n                }\n            }\n        };\n\n        var targetPlan = new TeamsPlan(isAnnual: true);\n\n        // Invoice with negative line item (proration credit)\n        var invoice = new Invoice\n        {\n            Total = 4000, // $40.00\n            TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 400 } }, // $4.00\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = new List<InvoiceLineItem>\n                {\n                    new() { Amount = -1000 },  // -$10.00 credit from unused Premium\n                    new() { Amount = 5000 }    // $50.00 for new plan\n                }\n            },\n            PeriodEnd = now\n        };\n\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>())\n            .Returns(currentSubscription);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        // Act\n        var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT0);\n        var proration = result.AsT0;\n        Assert.Equal(50.00m, proration.NewPlanProratedAmount);\n        Assert.Equal(10.00m, proration.Credit);  // Proration credit\n        Assert.Equal(4.00m, proration.Tax);\n        Assert.Equal(40.00m, proration.Total);\n        Assert.Equal(2, proration.NewPlanProratedMonths); // 45 days rounds to 2 months\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidUpgrade_AlwaysUsesOneSeat(User user, BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"premium-annually\",\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"storage-gb-annually\",\n                Price = 4m,\n                Provided = 1\n            }\n        };\n        var premiumPlans = new List<PremiumPlan> { premiumPlan };\n\n        var currentSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Customer = new Customer { Id = \"cus_123\", Discount = null },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new() { Id = \"si_premium\", Price = new Price { Id = \"premium-annually\" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }\n                }\n            }\n        };\n\n        var targetPlan = new TeamsPlan(isAnnual: true);\n\n        var invoice = new Invoice\n        {\n            Total = 5000,\n            TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },\n            Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },\n            PeriodEnd = DateTime.UtcNow\n        };\n\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>())\n            .Returns(currentSubscription);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        // Act\n        await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert - Verify that the subscription item quantity is always 1 and has Id\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(\n            Arg.Is<InvoiceCreatePreviewOptions>(options =>\n                options.SubscriptionDetails.Items.Any(item =>\n                    item.Id == \"si_premium\" &&\n                    item.Price == targetPlan.PasswordManager.StripeSeatPlanId &&\n                    item.Quantity == 1)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidUpgrade_DeletesPremiumSubscriptionItems(User user, BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"premium-annually\",\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"storage-gb-annually\",\n                Price = 4m,\n                Provided = 1\n            }\n        };\n        var premiumPlans = new List<PremiumPlan> { premiumPlan };\n\n        var currentSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Customer = new Customer { Id = \"cus_123\", Discount = null },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new() { Id = \"si_password_manager\", Price = new Price { Id = \"premium-annually\" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) },\n                    new() { Id = \"si_storage\", Price = new Price { Id = \"storage-gb-annually\" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }\n                }\n            }\n        };\n\n        var targetPlan = new TeamsPlan(isAnnual: true);\n\n        var invoice = new Invoice\n        {\n            Total = 5000,\n            TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },\n            Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },\n            PeriodEnd = DateTime.UtcNow\n        };\n\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>())\n            .Returns(currentSubscription);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        // Act\n        await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert - Verify password manager item is modified and storage item is deleted\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(\n            Arg.Is<InvoiceCreatePreviewOptions>(options =>\n                // Password manager item should be modified to new plan price, not deleted\n                options.SubscriptionDetails.Items.Any(item =>\n                    item.Id == \"si_password_manager\" &&\n                    item.Price == targetPlan.PasswordManager.StripeSeatPlanId &&\n                    item.Deleted != true) &&\n                // Storage item should be deleted\n                options.SubscriptionDetails.Items.Any(item =>\n                    item.Id == \"si_storage\" && item.Deleted == true)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NonSeatBasedPlan_UsesStripePlanId(User user, BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"premium-annually\",\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"storage-gb-annually\",\n                Price = 4m,\n                Provided = 1\n            }\n        };\n        var premiumPlans = new List<PremiumPlan> { premiumPlan };\n\n        var currentSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Customer = new Customer { Id = \"cus_123\", Discount = null },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new() { Id = \"si_premium\", Price = new Price { Id = \"premium-annually\" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }\n                }\n            }\n        };\n\n        var targetPlan = new FamiliesPlan(); // families is non seat based\n\n        var invoice = new Invoice\n        {\n            Total = 5000,\n            TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },\n            Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },\n            PeriodEnd = DateTime.UtcNow\n        };\n\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(targetPlan);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>())\n            .Returns(currentSubscription);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        // Act\n        await _command.Run(user, PlanType.FamiliesAnnually, billingAddress);\n\n        // Assert - Verify non-seat-based plan uses StripePlanId with quantity 1 and modifies existing item\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(\n            Arg.Is<InvoiceCreatePreviewOptions>(options =>\n                options.SubscriptionDetails.Items.Any(item =>\n                    item.Id == \"si_premium\" &&\n                    item.Price == targetPlan.PasswordManager.StripePlanId &&\n                    item.Quantity == 1)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidUpgrade_CreatesCorrectInvoicePreviewOptions(User user, BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n        billingAddress.Country = \"US\";\n        billingAddress.PostalCode = \"12345\";\n\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"premium-annually\",\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"storage-gb-annually\",\n                Price = 4m,\n                Provided = 1\n            }\n        };\n        var premiumPlans = new List<PremiumPlan> { premiumPlan };\n\n        var currentSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Customer = new Customer { Id = \"cus_123\", Discount = null },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new() { Id = \"si_premium\", Price = new Price { Id = \"premium-annually\" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }\n                }\n            }\n        };\n\n        var targetPlan = new TeamsPlan(isAnnual: true);\n\n        var invoice = new Invoice\n        {\n            Total = 5000,\n            TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },\n            Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },\n            PeriodEnd = DateTime.UtcNow\n        };\n\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>())\n            .Returns(currentSubscription);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        // Act\n        await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert - Verify all invoice preview options are correct\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(\n            Arg.Is<InvoiceCreatePreviewOptions>(options =>\n                options.AutomaticTax.Enabled == true &&\n                options.Customer == \"cus_123\" &&\n                options.Subscription == \"sub_123\" &&\n                options.CustomerDetails.Address.Country == \"US\" &&\n                options.CustomerDetails.Address.PostalCode == \"12345\" &&\n                options.SubscriptionDetails.ProrationBehavior == \"always_invoice\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_SeatBasedPlan_UsesStripeSeatPlanId(User user, BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"premium-annually\",\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"storage-gb-annually\",\n                Price = 4m,\n                Provided = 1\n            }\n        };\n        var premiumPlans = new List<PremiumPlan> { premiumPlan };\n\n        var currentSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Customer = new Customer { Id = \"cus_123\", Discount = null },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new() { Id = \"si_premium\", Price = new Price { Id = \"premium-annually\" }, CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) }\n                }\n            }\n        };\n\n        // Use Teams which is seat-based\n        var targetPlan = new TeamsPlan(isAnnual: true);\n\n        var invoice = new Invoice\n        {\n            Total = 5000,\n            TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },\n            Lines = new StripeList<InvoiceLineItem> { Data = new List<InvoiceLineItem> { new() { Amount = 5000 } } },\n            PeriodEnd = DateTime.UtcNow\n        };\n\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>())\n            .Returns(currentSubscription);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        // Act\n        await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert - Verify seat-based plan uses StripeSeatPlanId with quantity 1 and modifies existing item\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(\n            Arg.Is<InvoiceCreatePreviewOptions>(options =>\n                options.SubscriptionDetails.Items.Any(item =>\n                    item.Id == \"si_premium\" &&\n                    item.Price == targetPlan.PasswordManager.StripeSeatPlanId &&\n                    item.Quantity == 1)));\n    }\n\n    [Theory]\n    [InlineData(0, 1)]     // Less than 15 days, minimum 1 month\n    [InlineData(1, 1)]     // 1 day = 1 month minimum\n    [InlineData(14, 1)]    // 14 days = 1 month minimum\n    [InlineData(15, 1)]    // 15 days rounds to 1 month\n    [InlineData(30, 1)]    // 30 days = 1 month\n    [InlineData(44, 1)]    // 44 days rounds to 1 month\n    [InlineData(45, 2)]    // 45 days rounds to 2 months\n    [InlineData(60, 2)]    // 60 days = 2 months\n    [InlineData(90, 3)]    // 90 days = 3 months\n    [InlineData(180, 6)]   // 180 days = 6 months\n    [InlineData(365, 12)]  // 365 days rounds to 12 months\n    public async Task Run_ValidUpgrade_CalculatesNewPlanProratedMonthsCorrectly(int daysRemaining, int expectedMonths)\n    {\n        // Arrange\n        var user = new User\n        {\n            Premium = true,\n            GatewaySubscriptionId = \"sub_123\",\n            GatewayCustomerId = \"cus_123\"\n        };\n        var billingAddress = new Core.Billing.Payment.Models.BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\"\n        };\n\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"premium-annually\",\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"storage-gb-annually\",\n                Price = 4m,\n                Provided = 1\n            }\n        };\n        var premiumPlans = new List<PremiumPlan> { premiumPlan };\n\n        // Use fixed time to avoid DateTime.UtcNow differences\n        var now = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);\n        var currentPeriodEnd = now.AddDays(daysRemaining);\n        var currentSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Customer = new Customer { Id = \"cus_123\", Discount = null },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new() { Id = \"si_premium\", Price = new Price { Id = \"premium-annually\" }, CurrentPeriodEnd = currentPeriodEnd }\n                }\n            }\n        };\n\n        var targetPlan = new TeamsPlan(isAnnual: true);\n\n        var invoice = new Invoice\n        {\n            Total = 5000,\n            TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 500 } },\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = new List<InvoiceLineItem> { new() { Amount = 5000 } }\n            },\n            PeriodEnd = now\n        };\n\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>())\n            .Returns(currentSubscription);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        // Act\n        var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT0);\n        var proration = result.AsT0;\n        Assert.Equal(expectedMonths, proration.NewPlanProratedMonths);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ValidUpgrade_ReturnsNewPlanProratedAmountCorrectly(User user, BillingAddress billingAddress)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"premium-annually\",\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n            {\n                StripePriceId = \"storage-gb-annually\",\n                Price = 4m,\n                Provided = 1\n            }\n        };\n        var premiumPlans = new List<PremiumPlan> { premiumPlan };\n\n        var now = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);\n        var currentPeriodEnd = now.AddMonths(3);\n        var currentSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Customer = new Customer { Id = \"cus_123\", Discount = null },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new() { Id = \"si_premium\", Price = new Price { Id = \"premium-annually\" }, CurrentPeriodEnd = currentPeriodEnd }\n                }\n            }\n        };\n\n        var targetPlan = new TeamsPlan(isAnnual: true);\n\n        // Invoice showing new plan cost, credit, and net\n        var invoice = new Invoice\n        {\n            Total = 4500, // $45.00 net after $5 credit\n            TotalTaxes = new List<InvoiceTotalTax> { new() { Amount = 450 } }, // $4.50\n            Lines = new StripeList<InvoiceLineItem>\n            {\n                Data = new List<InvoiceLineItem>\n                {\n                    new() { Amount = -500 },  // -$5.00 credit\n                    new() { Amount = 5000 }   // $50.00 for new plan\n                }\n            },\n            PeriodEnd = now\n        };\n\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(targetPlan);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>())\n            .Returns(currentSubscription);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        // Act\n        var result = await _command.Run(user, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT0);\n        var proration = result.AsT0;\n\n        Assert.Equal(50.00m, proration.NewPlanProratedAmount);\n        Assert.Equal(5.00m, proration.Credit);\n        Assert.Equal(4.50m, proration.Tax);\n        Assert.Equal(45.00m, proration.Total);\n    }\n}\n\n"
  },
  {
    "path": "test/Core.Test/Billing/Premium/Commands/UpdatePremiumStorageCommandTests.cs",
    "content": "﻿using Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Models;\nusing Bit.Core.Entities;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\nusing static Bit.Core.Billing.Constants.StripeConstants;\nusing PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;\nusing PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;\n\nnamespace Bit.Core.Test.Billing.Premium.Commands;\n\npublic class UpdatePremiumStorageCommandTests\n{\n    private readonly IBraintreeService _braintreeService = Substitute.For<IBraintreeService>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly IUserService _userService = Substitute.For<IUserService>();\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly UpdatePremiumStorageCommand _command;\n\n    public UpdatePremiumStorageCommandTests()\n    {\n        var premiumPlan = new PremiumPlan\n        {\n            Name = \"Premium\",\n            Available = true,\n            LegacyYear = null,\n            Seat = new PremiumPurchasable { Price = 10M, StripePriceId = \"price_premium\", Provided = 1 },\n            Storage = new PremiumPurchasable { Price = 4M, StripePriceId = \"price_storage\", Provided = 1 }\n        };\n        _pricingClient.ListPremiumPlans().Returns([premiumPlan]);\n\n        _command = new UpdatePremiumStorageCommand(\n            _braintreeService,\n            _stripeAdapter,\n            _userService,\n            _pricingClient,\n            Substitute.For<ILogger<UpdatePremiumStorageCommand>>());\n    }\n\n    private Subscription CreateMockSubscription(string subscriptionId, int? storageQuantity = null, bool isPayPal = false)\n    {\n        var items = new List<SubscriptionItem>\n        {\n            // Always add the seat item\n            new()\n            {\n                Id = \"si_seat\",\n                Price = new Price { Id = \"price_premium\" },\n                Quantity = 1\n            }\n        };\n\n        // Add storage item if quantity is provided\n        if (storageQuantity is > 0)\n        {\n            items.Add(new SubscriptionItem\n            {\n                Id = \"si_storage\",\n                Price = new Price { Id = \"price_storage\" },\n                Quantity = storageQuantity.Value\n            });\n        }\n\n        var customer = new Customer\n        {\n            Id = \"cus_123\",\n            Metadata = isPayPal ? new Dictionary<string, string> { { MetadataKeys.BraintreeCustomerId, \"braintree_123\" } } : new Dictionary<string, string>()\n        };\n\n        return new Subscription\n        {\n            Id = subscriptionId,\n            CustomerId = \"cus_123\",\n            Customer = customer,\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = items\n            }\n        };\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserNotPremium_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        user.Premium = false;\n\n        // Act\n        var result = await _command.Run(user, 5);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"User does not have a premium subscription.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NegativeStorage_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 5;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 4);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        // Act\n        var result = await _command.Run(user, -5);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Additional storage cannot be negative.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_StorageExceedsMaximum_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 5;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 4);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        // Act\n        var result = await _command.Run(user, 100);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Maximum storage is 100 GB.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NoMaxStorageGb_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = null;\n\n        // Act\n        var result = await _command.Run(user, 5);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"User has no access to storage.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_StorageExceedsCurrentUsage_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 10;\n        user.Storage = 5L * 1024 * 1024 * 1024; // 5 GB currently used\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 9);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        // Act\n        var result = await _command.Run(user, 0);\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Contains(\"You are currently using\", badRequest.Response);\n        Assert.Contains(\"Delete some stored data first\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_SameStorageAmount_Idempotent(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 5;\n        user.Storage = 2L * 1024 * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 4);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        // Act\n        var result = await _command.Run(user, 4);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify subscription was fetched but NOT updated\n        await _stripeAdapter.Received(1).GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>());\n        await _stripeAdapter.DidNotReceive().UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n        await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_IncreaseStorage_Success(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 5;\n        user.Storage = 2L * 1024 * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 4);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        // Act\n        var result = await _command.Run(user, 9);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify subscription was updated\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 &&\n                opts.Items[0].Id == \"si_storage\" &&\n                opts.Items[0].Quantity == 9 &&\n                opts.ProrationBehavior == \"always_invoice\"));\n\n        // Verify user was saved\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>\n            u.Id == user.Id &&\n            u.MaxStorageGb == 10));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_AddStorageFromZero_Success(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 1;\n        user.Storage = 500L * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\");\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        // Act\n        var result = await _command.Run(user, 9);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify subscription was updated with new storage item\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 &&\n                opts.Items[0].Price == \"price_storage\" &&\n                opts.Items[0].Quantity == 9));\n\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 10));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_DecreaseStorage_Success(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 10;\n        user.Storage = 2L * 1024 * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 9);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        // Act\n        var result = await _command.Run(user, 2);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify subscription was updated\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 &&\n                opts.Items[0].Id == \"si_storage\" &&\n                opts.Items[0].Quantity == 2));\n\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 3));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_RemoveAllAdditionalStorage_Success(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 10;\n        user.Storage = 500L * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 9);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        // Act\n        var result = await _command.Run(user, 0);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify subscription item was deleted\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 &&\n                opts.Items[0].Id == \"si_storage\" &&\n                opts.Items[0].Deleted == true));\n\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_MaximumStorage_Success(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 5;\n        user.Storage = 2L * 1024 * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 4);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        // Act\n        var result = await _command.Run(user, 99);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items[0].Quantity == 99));\n\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 100));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_IncreaseStorage_PayPal_Success(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 5;\n        user.Storage = 2L * 1024 * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 4, isPayPal: true);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var draftInvoice = new Invoice { Id = \"in_draft\" };\n        _stripeAdapter.CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>()).Returns(draftInvoice);\n\n        var finalizedInvoice = new Invoice\n        {\n            Id = \"in_finalized\",\n            Customer = new Customer { Id = \"cus_123\" }\n        };\n        _stripeAdapter.FinalizeInvoiceAsync(\"in_draft\", Arg.Any<InvoiceFinalizeOptions>()).Returns(finalizedInvoice);\n\n        // Act\n        var result = await _command.Run(user, 9);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify subscription was updated with CreateProrations\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 &&\n                opts.Items[0].Id == \"si_storage\" &&\n                opts.Items[0].Quantity == 9 &&\n                opts.ProrationBehavior == \"create_prorations\"));\n\n        // Verify draft invoice was created\n        await _stripeAdapter.Received(1).CreateInvoiceAsync(\n            Arg.Is<InvoiceCreateOptions>(opts =>\n                opts.Customer == \"cus_123\" &&\n                opts.Subscription == \"sub_123\" &&\n                opts.AutoAdvance == false &&\n                opts.CollectionMethod == \"charge_automatically\"));\n\n        // Verify invoice was finalized\n        await _stripeAdapter.Received(1).FinalizeInvoiceAsync(\n            \"in_draft\",\n            Arg.Is<InvoiceFinalizeOptions>(opts =>\n                opts.AutoAdvance == false &&\n                opts.Expand.Contains(\"customer\")));\n\n        // Verify Braintree payment was processed\n        await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), finalizedInvoice);\n\n        // Verify user was saved\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>\n            u.Id == user.Id &&\n            u.MaxStorageGb == 10));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_AddStorageFromZero_PayPal_Success(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 1;\n        user.Storage = 500L * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", isPayPal: true);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var draftInvoice = new Invoice { Id = \"in_draft\" };\n        _stripeAdapter.CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>()).Returns(draftInvoice);\n\n        var finalizedInvoice = new Invoice\n        {\n            Id = \"in_finalized\",\n            Customer = new Customer { Id = \"cus_123\" }\n        };\n        _stripeAdapter.FinalizeInvoiceAsync(\"in_draft\", Arg.Any<InvoiceFinalizeOptions>()).Returns(finalizedInvoice);\n\n        // Act\n        var result = await _command.Run(user, 9);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify subscription was updated with new storage item\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 &&\n                opts.Items[0].Price == \"price_storage\" &&\n                opts.Items[0].Quantity == 9 &&\n                opts.ProrationBehavior == \"create_prorations\"));\n\n        // Verify invoice creation and payment flow\n        await _stripeAdapter.Received(1).CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>());\n        await _stripeAdapter.Received(1).FinalizeInvoiceAsync(\"in_draft\", Arg.Any<InvoiceFinalizeOptions>());\n        await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), finalizedInvoice);\n\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 10));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_DecreaseStorage_PayPal_Success(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 10;\n        user.Storage = 2L * 1024 * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 9, isPayPal: true);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var draftInvoice = new Invoice { Id = \"in_draft\" };\n        _stripeAdapter.CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>()).Returns(draftInvoice);\n\n        var finalizedInvoice = new Invoice\n        {\n            Id = \"in_finalized\",\n            Customer = new Customer { Id = \"cus_123\" }\n        };\n        _stripeAdapter.FinalizeInvoiceAsync(\"in_draft\", Arg.Any<InvoiceFinalizeOptions>()).Returns(finalizedInvoice);\n\n        // Act\n        var result = await _command.Run(user, 2);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify subscription was updated\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 &&\n                opts.Items[0].Id == \"si_storage\" &&\n                opts.Items[0].Quantity == 2 &&\n                opts.ProrationBehavior == \"create_prorations\"));\n\n        // Verify invoice creation and payment flow\n        await _stripeAdapter.Received(1).CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>());\n        await _stripeAdapter.Received(1).FinalizeInvoiceAsync(\"in_draft\", Arg.Any<InvoiceFinalizeOptions>());\n        await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), finalizedInvoice);\n\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 3));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_RemoveAllAdditionalStorage_PayPal_Success(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.MaxStorageGb = 10;\n        user.Storage = 500L * 1024 * 1024;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var subscription = CreateMockSubscription(\"sub_123\", 9, isPayPal: true);\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\", Arg.Any<SubscriptionGetOptions>()).Returns(subscription);\n\n        var draftInvoice = new Invoice { Id = \"in_draft\" };\n        _stripeAdapter.CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>()).Returns(draftInvoice);\n\n        var finalizedInvoice = new Invoice\n        {\n            Id = \"in_finalized\",\n            Customer = new Customer { Id = \"cus_123\" }\n        };\n        _stripeAdapter.FinalizeInvoiceAsync(\"in_draft\", Arg.Any<InvoiceFinalizeOptions>()).Returns(finalizedInvoice);\n\n        // Act\n        var result = await _command.Run(user, 0);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        // Verify subscription item was deleted\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 &&\n                opts.Items[0].Id == \"si_storage\" &&\n                opts.Items[0].Deleted == true &&\n                opts.ProrationBehavior == \"create_prorations\"));\n\n        // Verify invoice creation and payment flow\n        await _stripeAdapter.Received(1).CreateInvoiceAsync(Arg.Any<InvoiceCreateOptions>());\n        await _stripeAdapter.Received(1).FinalizeInvoiceAsync(\"in_draft\", Arg.Any<InvoiceFinalizeOptions>());\n        await _braintreeService.Received(1).PayInvoice(Arg.Any<SubscriberId>(), finalizedInvoice);\n\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u => u.MaxStorageGb == 1));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Premium.Commands;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\nusing PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;\nusing PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;\n\nnamespace Bit.Core.Test.Billing.Premium.Commands;\n\npublic class UpgradePremiumToOrganizationCommandTests\n{\n    // Concrete test implementation of the abstract Plan record\n    private record TestPlan : Core.Models.StaticStore.Plan\n    {\n        public TestPlan(\n            PlanType planType,\n            string? stripePlanId = null,\n            string? stripeSeatPlanId = null,\n            string? stripePremiumAccessPlanId = null,\n            string? stripeStoragePlanId = null,\n            int baseSeats = 1)\n        {\n            Type = planType;\n            ProductTier = ProductTierType.Teams;\n            Name = \"Test Plan\";\n            IsAnnual = true;\n            NameLocalizationKey = \"\";\n            DescriptionLocalizationKey = \"\";\n            CanBeUsedByBusiness = true;\n            HasSelfHost = false;\n            HasPolicies = false;\n            HasGroups = false;\n            HasDirectory = false;\n            HasEvents = false;\n            HasTotp = false;\n            Has2fa = false;\n            HasApi = false;\n            HasSso = false;\n            HasOrganizationDomains = false;\n            HasKeyConnector = false;\n            HasScim = false;\n            HasResetPassword = false;\n            UsersGetPremium = false;\n            HasCustomPermissions = false;\n            UpgradeSortOrder = 0;\n            DisplaySortOrder = 0;\n            LegacyYear = null;\n            Disabled = false;\n            PasswordManager = new PasswordManagerPlanFeatures\n            {\n                StripePlanId = stripePlanId,\n                StripeSeatPlanId = stripeSeatPlanId,\n                StripePremiumAccessPlanId = stripePremiumAccessPlanId,\n                StripeStoragePlanId = stripeStoragePlanId,\n                BasePrice = 0,\n                SeatPrice = 0,\n                ProviderPortalSeatPrice = 0,\n                AllowSeatAutoscale = true,\n                HasAdditionalSeatsOption = true,\n                BaseSeats = baseSeats,\n                HasPremiumAccessOption = !string.IsNullOrEmpty(stripePremiumAccessPlanId),\n                PremiumAccessOptionPrice = 0,\n                MaxSeats = null,\n                BaseStorageGb = 1,\n                HasAdditionalStorageOption = !string.IsNullOrEmpty(stripeStoragePlanId),\n                AdditionalStoragePricePerGb = 0,\n                MaxCollections = null\n            };\n            SecretsManager = null;\n        }\n    }\n\n    private static Core.Models.StaticStore.Plan CreateTestPlan(\n        PlanType planType,\n        string? stripePlanId = null,\n        string? stripeSeatPlanId = null,\n        string? stripePremiumAccessPlanId = null,\n        string? stripeStoragePlanId = null,\n        int baseSeats = 1) =>\n        new TestPlan(planType, stripePlanId, stripeSeatPlanId, stripePremiumAccessPlanId, stripeStoragePlanId, baseSeats);\n\n    private static PremiumPlan CreateTestPremiumPlan(\n        string seatPriceId = \"premium-annually\",\n        string storagePriceId = \"personal-storage-gb-annually\",\n        bool available = true)\n    {\n        return new PremiumPlan\n        {\n            Name = \"Premium\",\n            LegacyYear = null,\n            Available = available,\n            Seat = new PremiumPurchasable\n            {\n                StripePriceId = seatPriceId,\n                Price = 10m,\n                Provided = 1\n            },\n            Storage = new PremiumPurchasable\n            {\n                StripePriceId = storagePriceId,\n                Price = 4m,\n                Provided = 1\n            }\n        };\n    }\n\n    private static List<PremiumPlan> CreateTestPremiumPlansList()\n    {\n        return new List<PremiumPlan>\n        {\n            // Current available plan\n            CreateTestPremiumPlan(available: true),\n            // Legacy plan from 2020\n            CreateTestPremiumPlan(\"premium-annually-2020\", \"personal-storage-gb-annually-2020\", available: false)\n        };\n    }\n\n\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly IUserService _userService = Substitute.For<IUserService>();\n    private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();\n    private readonly IOrganizationUserRepository _organizationUserRepository = Substitute.For<IOrganizationUserRepository>();\n    private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository = Substitute.For<IOrganizationApiKeyRepository>();\n    private readonly ICollectionRepository _collectionRepository = Substitute.For<ICollectionRepository>();\n    private readonly IApplicationCacheService _applicationCacheService = Substitute.For<IApplicationCacheService>();\n    private readonly IPushNotificationService _pushNotificationService = Substitute.For<IPushNotificationService>();\n    private readonly ILogger<UpgradePremiumToOrganizationCommand> _logger = Substitute.For<ILogger<UpgradePremiumToOrganizationCommand>>();\n    private readonly UpgradePremiumToOrganizationCommand _command;\n\n    public UpgradePremiumToOrganizationCommandTests()\n    {\n        _command = new UpgradePremiumToOrganizationCommand(\n            _logger,\n            _pricingClient,\n            _stripeAdapter,\n            _userService,\n            _organizationRepository,\n            _organizationUserRepository,\n            _organizationApiKeyRepository,\n            _collectionRepository,\n            _applicationCacheService,\n            _pushNotificationService);\n    }\n\n    private static Core.Billing.Payment.Models.BillingAddress CreateTestBillingAddress() =>\n        new() { Country = \"US\", PostalCode = \"12345\" };\n\n    [Theory, BitAutoData]\n    public async Task Run_UserNotPremium_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        user.Premium = false;\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"User does not have an active Premium subscription.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserNoGatewaySubscriptionId_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = null;\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"User does not have an active Premium subscription.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserEmptyGatewaySubscriptionId_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"\";\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"User does not have an active Premium subscription.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_SuccessfulUpgrade_SeatBasedPlan_ReturnsSuccess(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n        user.Id = Guid.NewGuid();\n\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" },\n                        CurrentPeriodEnd = currentPeriodEnd\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(\n            PlanType.TeamsAnnually,\n            stripeSeatPlanId: \"teams-seat-annually\"\n        );\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\")\n            .Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(Task.FromResult(mockSubscription));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 && // Only 1 item: modify existing password manager item (no storage to delete)\n                opts.Items.Any(i => i.Id == \"si_premium\" && i.Price == \"teams-seat-annually\" && i.Quantity == 1 && i.Deleted != true)));\n\n        await _organizationRepository.Received(1).CreateAsync(Arg.Is<Organization>(o =>\n            o.Name == \"My Organization\" &&\n            o.Gateway == GatewayType.Stripe &&\n            o.GatewaySubscriptionId == \"sub_123\" &&\n            o.GatewayCustomerId == \"cus_123\"));\n        await _organizationUserRepository.Received(1).CreateAsync(Arg.Is<OrganizationUser>(ou =>\n            ou.Key == \"encrypted-key\" &&\n            ou.Status == OrganizationUserStatusType.Confirmed));\n        await _organizationApiKeyRepository.Received(1).CreateAsync(Arg.Any<OrganizationApiKey>());\n\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>\n            u.Premium == false &&\n            u.GatewaySubscriptionId == null &&\n            u.GatewayCustomerId == null));\n\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_SuccessfulUpgrade_NonSeatBasedPlan_ReturnsSuccess(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" },\n                        CurrentPeriodEnd = currentPeriodEnd\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(\n            PlanType.FamiliesAnnually,\n            stripePlanId: \"families-plan-annually\",\n            stripeSeatPlanId: null, // Non-seat-based\n            baseSeats: 6\n        );\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\")\n            .Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(Task.FromResult(mockSubscription));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Families Org\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.FamiliesAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 && // Only 1 item: modify existing password manager item (no storage to delete)\n                opts.Items.Any(i => i.Id == \"si_premium\" && i.Price == \"families-plan-annually\" && i.Quantity == 1 && i.Deleted != true)));\n\n        await _organizationRepository.Received(1).CreateAsync(Arg.Is<Organization>(o =>\n            o.Name == \"My Families Org\" &&\n            o.Seats == 6));\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>\n            u.Premium == false &&\n            u.GatewaySubscriptionId == null));\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task Run_AddsMetadataWithOriginalPremiumPriceId(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" },\n                        CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>\n            {\n                [\"userId\"] = user.Id.ToString()\n            }\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(\n            PlanType.TeamsAnnually,\n            stripeSeatPlanId: \"teams-seat-annually\"\n        );\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\")\n            .Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(Task.FromResult(mockSubscription));\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&\n                opts.Metadata.ContainsKey(StripeConstants.MetadataKeys.UserId) &&\n                opts.Metadata[StripeConstants.MetadataKeys.UserId] == string.Empty)); // Removes userId to unlink from User\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserOnLegacyPremiumPlan_SuccessfullyDeletesLegacyItems(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium_legacy\",\n                        Price = new Price { Id = \"premium-annually-2020\" }, // Legacy price ID\n                        CurrentPeriodEnd = currentPeriodEnd\n                    },\n                    new SubscriptionItem\n                    {\n                        Id = \"si_storage_legacy\",\n                        Price = new Price { Id = \"personal-storage-gb-annually-2020\" }, // Legacy storage price ID\n                        CurrentPeriodEnd = currentPeriodEnd\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(\n            PlanType.TeamsAnnually,\n            stripeSeatPlanId: \"teams-seat-annually\"\n        );\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\")\n            .Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(Task.FromResult(mockSubscription));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        // Verify that legacy password manager item is modified and legacy storage is deleted\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 2 && // 1 modified (legacy PM to new price) + 1 deleted (legacy storage)\n                opts.Items.Count(i => i.Id == \"si_premium_legacy\" && i.Price == \"teams-seat-annually\" && i.Quantity == 1 && i.Deleted != true) == 1 && // Legacy PM modified\n                opts.Items.Count(i => i.Deleted == true && i.Id == \"si_storage_legacy\") == 1)); // Legacy storage deleted\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UserHasPremiumPlusOtherProducts_OnlyDeletesPremiumItems(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" },\n                        CurrentPeriodEnd = currentPeriodEnd\n                    },\n                    new SubscriptionItem\n                    {\n                        Id = \"si_other_product\",\n                        Price = new Price { Id = \"some-other-product-id\" }, // Non-premium item\n                        CurrentPeriodEnd = currentPeriodEnd\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(\n            PlanType.TeamsAnnually,\n            stripeSeatPlanId: \"teams-seat-annually\"\n        );\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\")\n            .Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(Task.FromResult(mockSubscription));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        // Verify that ONLY the premium password manager item is modified (not other products)\n        // Note: We modify the specific premium item by ID, so other products are untouched\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.Items.Count == 1 && // Only modify premium password manager item\n                opts.Items.Count(i => i.Id == \"si_premium\" && i.Price == \"teams-seat-annually\" && i.Quantity == 1 && i.Deleted != true) == 1 && // Premium item modified\n                opts.Items.Count(i => i.Id == \"si_other_product\") == 0)); // Other product NOT in update (untouched)\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_NoPremiumSubscriptionItemFound_ReturnsBadRequest(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_other\",\n                        Price = new Price { Id = \"some-other-product\" }, // Not a premium plan\n                        CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\")\n            .Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Premium subscription password manager item not found.\", badRequest.Response);\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UpdatesCustomerBillingAddress(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        var billingAddress = new Core.Billing.Payment.Models.BillingAddress { Country = \"US\", PostalCode = \"12345\" };\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(opts =>\n                opts.Address.Country == \"US\" &&\n                opts.Address.PostalCode == \"12345\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_EnablesAutomaticTaxOnSubscription(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.AutomaticTax != null &&\n                opts.AutomaticTax.Enabled == true));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_UsesAlwaysInvoiceProrationBehavior(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                opts.ProrationBehavior == \"always_invoice\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_ModifiesExistingSubscriptionItem_NotDeleteAndRecreate(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        // Verify that the subscription item was modified, not deleted\n        await _stripeAdapter.Received(1).UpdateSubscriptionAsync(\n            \"sub_123\",\n            Arg.Is<SubscriptionUpdateOptions>(opts =>\n                // Should have an item with the original ID being modified\n                opts.Items.Any(item =>\n                    item.Id == \"si_premium\" &&\n                    item.Price == \"teams-seat-annually\" &&\n                    item.Quantity == 1 &&\n                    item.Deleted != true)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_CreatesOrganizationWithCorrectSettings(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _organizationRepository.Received(1).CreateAsync(\n            Arg.Is<Organization>(org =>\n                org.Name == \"My Organization\" &&\n                org.BillingEmail == user.Email &&\n                org.PlanType == PlanType.TeamsAnnually &&\n                org.Seats == 1 &&\n                org.Gateway == GatewayType.Stripe &&\n                org.GatewayCustomerId == \"cus_123\" &&\n                org.GatewaySubscriptionId == \"sub_123\" &&\n                org.Enabled == true));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_CreatesOrganizationApiKeyWithCorrectType(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _organizationApiKeyRepository.Received(1).CreateAsync(\n            Arg.Is<OrganizationApiKey>(apiKey =>\n                apiKey.Type == OrganizationApiKeyType.Default &&\n                !string.IsNullOrEmpty(apiKey.ApiKey)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_CreatesOrganizationUserAsOwnerWithAllPermissions(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _organizationUserRepository.Received(1).CreateAsync(\n            Arg.Is<OrganizationUser>(orgUser =>\n                orgUser.UserId == user.Id &&\n                orgUser.Type == OrganizationUserType.Owner &&\n                orgUser.Status == OrganizationUserStatusType.Confirmed));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_SetsOrganizationPublicAndPrivateKeys(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"test-public-key\", \"test-encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        await _organizationRepository.Received(1).CreateAsync(\n            Arg.Is<Organization>(org =>\n                org.PublicKey == \"test-public-key\" &&\n                org.PrivateKey == \"test-encrypted-private-key\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithCollectionName_CreatesDefaultCollection(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _collectionRepository.CreateAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(), Arg.Any<IEnumerable<CollectionAccessSelection>>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Collection>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", \"Default Collection\", PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _collectionRepository.Received(1).CreateAsync(\n            Arg.Is<Collection>(c => c.Name == \"Default Collection\"),\n            Arg.Is<IEnumerable<CollectionAccessSelection>>(x => x == null),\n            Arg.Is<IEnumerable<CollectionAccessSelection>>(access =>\n                access.Count() == 1 &&\n                access.First().Manage == true &&\n                access.First().ReadOnly == false &&\n                access.First().HidePasswords == false));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithoutCollectionName_DoesNotCreateCollection(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        await _collectionRepository.DidNotReceive().CreateAsync(\n            Arg.Any<Collection>(),\n            Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n            Arg.Any<IEnumerable<CollectionAccessSelection>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_CollectionCreationFails_UpgradeStillSucceeds(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        // Mock collection repository to throw exception\n        _collectionRepository\n            .When(x => x.CreateAsync(\n                Arg.Any<Collection>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n                Arg.Any<IEnumerable<CollectionAccessSelection>>()))\n            .Do(_ => throw new InvalidOperationException(\"Database error\"));\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", \"Default Collection\", PlanType.TeamsAnnually, CreateTestBillingAddress());\n\n        // Assert\n        Assert.True(result.IsT0);\n        var organizationId = result.AsT0;\n        Assert.NotEqual(Guid.Empty, organizationId);\n\n        // Verify that core upgrade operations still completed successfully\n        await _organizationRepository.Received(1).CreateAsync(Arg.Any<Organization>());\n        await _organizationUserRepository.Received(1).CreateAsync(Arg.Any<OrganizationUser>());\n        await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>\n            u.Premium == false &&\n            u.GatewaySubscriptionId == null));\n\n        // Verify collection creation was attempted\n        await _collectionRepository.Received(1).CreateAsync(\n            Arg.Any<Collection>(),\n            Arg.Any<IEnumerable<CollectionAccessSelection>>(),\n            Arg.Any<IEnumerable<CollectionAccessSelection>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithNoTaxId_SetsTaxExemptToNone_DoesNotCreateTaxId(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        var billingAddress = new Core.Billing.Payment.Models.BillingAddress\n        {\n            Country = \"US\",\n            PostalCode = \"12345\",\n            TaxId = null\n        };\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", \"Default Collection\", PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.TaxExempt == StripeConstants.TaxExempt.None));\n        await _stripeAdapter.DidNotReceive().CreateTaxIdAsync(Arg.Any<string>(), Arg.Any<TaxIdCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithTaxId_SetsTaxExemptToReverse_CreatesOneTaxId(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _stripeAdapter.CreateTaxIdAsync(Arg.Any<string>(), Arg.Any<TaxIdCreateOptions>()).Returns(new TaxId());\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        var billingAddress = new Core.Billing.Payment.Models.BillingAddress\n        {\n            Country = \"DE\",\n            PostalCode = \"10115\",\n            TaxId = new Core.Billing.Payment.Models.TaxID(\"eu_vat\", \"DE123456789\")\n        };\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", \"Default Collection\", PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT0);\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.TaxExempt == StripeConstants.TaxExempt.Reverse));\n        await _stripeAdapter.Received(1).CreateTaxIdAsync(\n            \"cus_123\",\n            Arg.Is<TaxIdCreateOptions>(options =>\n                options.Type == \"eu_vat\" &&\n                options.Value == \"DE123456789\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithSpanishNIF_SetsTaxExemptToReverse_CreatesBothSpanishNIFAndEUVAT(User user)\n    {\n        // Arrange\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new SubscriptionItem\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _stripeAdapter.CreateTaxIdAsync(Arg.Any<string>(), Arg.Any<TaxIdCreateOptions>()).Returns(new TaxId());\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        var billingAddress = new Core.Billing.Payment.Models.BillingAddress\n        {\n            Country = \"ES\",\n            PostalCode = \"28001\",\n            TaxId = new Core.Billing.Payment.Models.TaxID(StripeConstants.TaxIdType.SpanishNIF, \"A12345678\")\n        };\n\n        // Act\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", \"Default Collection\", PlanType.TeamsAnnually, billingAddress);\n\n        // Assert\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.TaxExempt == StripeConstants.TaxExempt.Reverse));\n\n        // Verify Spanish NIF was created\n        await _stripeAdapter.Received(1).CreateTaxIdAsync(\n            \"cus_123\",\n            Arg.Is<TaxIdCreateOptions>(options =>\n                options.Type == StripeConstants.TaxIdType.SpanishNIF &&\n                options.Value == \"A12345678\"));\n\n        // Verify EU VAT was created with ES prefix\n        await _stripeAdapter.Received(1).CreateTaxIdAsync(\n            \"cus_123\",\n            Arg.Is<TaxIdCreateOptions>(options =>\n                options.Type == StripeConstants.TaxIdType.EUVAT &&\n                options.Value == \"ESA12345678\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Run_WithSwissCountry_SetsTaxExemptToNone(User user)\n    {\n        user.Premium = true;\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.GatewayCustomerId = \"cus_123\";\n\n        var mockSubscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new()\n                    {\n                        Id = \"si_premium\",\n                        Price = new Price { Id = \"premium-annually\" }\n                    }\n                }\n            },\n            Metadata = new Dictionary<string, string>()\n        };\n\n        var mockPremiumPlans = CreateTestPremiumPlansList();\n        var mockPlan = CreateTestPlan(PlanType.TeamsAnnually, stripeSeatPlanId: \"teams-seat-annually\");\n\n        _stripeAdapter.GetSubscriptionAsync(\"sub_123\").Returns(mockSubscription);\n        _pricingClient.ListPremiumPlans().Returns(mockPremiumPlans);\n        _pricingClient.GetPlanOrThrow(PlanType.TeamsAnnually).Returns(mockPlan);\n        _stripeAdapter.UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>()).Returns(mockSubscription);\n        _stripeAdapter.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>()).Returns(Task.FromResult(new Customer()));\n        _organizationRepository.CreateAsync(Arg.Any<Organization>()).Returns(callInfo => Task.FromResult(callInfo.Arg<Organization>()));\n        _organizationApiKeyRepository.CreateAsync(Arg.Any<OrganizationApiKey>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationApiKey>()));\n        _organizationUserRepository.CreateAsync(Arg.Any<OrganizationUser>()).Returns(callInfo => Task.FromResult(callInfo.Arg<OrganizationUser>()));\n        _applicationCacheService.UpsertOrganizationAbilityAsync(Arg.Any<Organization>()).Returns(Task.CompletedTask);\n        _userService.SaveUserAsync(user).Returns(Task.CompletedTask);\n\n        var billingAddress = new Core.Billing.Payment.Models.BillingAddress\n        {\n            Country = \"CH\",\n            PostalCode = \"8001\",\n            TaxId = null\n        };\n\n        var result = await _command.Run(user, \"My Organization\", \"encrypted-key\", \"public-key\", \"encrypted-private-key\", null, PlanType.TeamsAnnually, billingAddress);\n\n        Assert.True(result.IsT0);\n        await _stripeAdapter.Received(1).UpdateCustomerAsync(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.TaxExempt == StripeConstants.TaxExempt.None));\n        await _stripeAdapter.DidNotReceive().CreateTaxIdAsync(Arg.Any<string>(), Arg.Any<TaxIdCreateOptions>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Premium/Queries/HasPremiumAccessQueryTests.cs",
    "content": "﻿using Bit.Core.Billing.Premium.Models;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Premium.Queries;\n\n[SutProviderCustomize]\npublic class HasPremiumAccessQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task HasPremiumAccessAsync_WhenUserHasPersonalPremium_ReturnsTrue(\n        UserPremiumAccess user,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        user.PersonalPremium = true;\n        user.OrganizationPremium = false;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessAsync(user.Id)\n            .Returns(user);\n\n        // Act\n        var result = await sutProvider.Sut.HasPremiumAccessAsync(user.Id);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumAccessAsync_WhenUserHasNoPersonalPremiumButHasOrgPremium_ReturnsTrue(\n        UserPremiumAccess user,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        user.PersonalPremium = false;\n        user.OrganizationPremium = true; // Has org premium\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessAsync(user.Id)\n            .Returns(user);\n\n        // Act\n        var result = await sutProvider.Sut.HasPremiumAccessAsync(user.Id);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumAccessAsync_WhenUserHasNoPersonalPremiumAndNoOrgPremium_ReturnsFalse(\n        UserPremiumAccess user,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        user.PersonalPremium = false;\n        user.OrganizationPremium = false;\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessAsync(user.Id)\n            .Returns(user);\n\n        // Act\n        var result = await sutProvider.Sut.HasPremiumAccessAsync(user.Id);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumAccessAsync_WhenUserNotFound_ThrowsNotFoundException(\n        Guid userId,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessAsync(userId)\n            .Returns((UserPremiumAccess?)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.HasPremiumAccessAsync(userId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumFromOrganizationAsync_WhenUserHasNoOrganizations_ReturnsFalse(\n        UserPremiumAccess user,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        user.PersonalPremium = false;\n        user.OrganizationPremium = false; // No premium from anywhere\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessAsync(user.Id)\n            .Returns(user);\n\n        // Act\n        var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumFromOrganizationAsync_WhenUserHasPremiumFromOrg_ReturnsTrue(\n        UserPremiumAccess user,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        user.PersonalPremium = false; // No personal premium\n        user.OrganizationPremium = true; // But has premium from org\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessAsync(user.Id)\n            .Returns(user);\n\n        // Act\n        var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumFromOrganizationAsync_WhenUserHasOnlyPersonalPremium_ReturnsFalse(\n        UserPremiumAccess user,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        user.PersonalPremium = true; // Has personal premium\n        user.OrganizationPremium = false; // Not in any org that grants premium\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessAsync(user.Id)\n            .Returns(user);\n\n        // Act\n        var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);\n\n        // Assert\n        Assert.False(result); // Should return false because user is not in an org that grants premium\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumFromOrganizationAsync_WhenUserHasBothPersonalAndOrgPremium_ReturnsTrue(\n        UserPremiumAccess user,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        user.PersonalPremium = true; // Has personal premium\n        user.OrganizationPremium = true; // Also in an org that grants premium\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessAsync(user.Id)\n            .Returns(user);\n\n        // Act\n        var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);\n\n        // Assert\n        Assert.True(result); // Should return true because user IS in an org that grants premium (regardless of personal premium)\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumFromOrganizationAsync_WhenUserNotFound_ThrowsNotFoundException(\n        Guid userId,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessAsync(userId)\n            .Returns((UserPremiumAccess?)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.HasPremiumFromOrganizationAsync(userId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumAccessAsync_Bulk_WhenEmptyList_ReturnsEmptyDictionary(\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        var userIds = new List<Guid>();\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessByIdsAsync(userIds)\n            .Returns(new List<UserPremiumAccess>());\n\n        // Act\n        var result = await sutProvider.Sut.HasPremiumAccessAsync(userIds);\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumAccessAsync_Bulk_ReturnsCorrectStatus(\n        UserPremiumAccess user1,\n        UserPremiumAccess user2,\n        UserPremiumAccess user3,\n        SutProvider<HasPremiumAccessQuery> sutProvider)\n    {\n        // Arrange\n        user1.PersonalPremium = true;\n        user1.OrganizationPremium = false;\n        user2.PersonalPremium = false;\n        user2.OrganizationPremium = false;\n        user3.PersonalPremium = false;\n        user3.OrganizationPremium = true;\n\n        var users = new List<UserPremiumAccess> { user1, user2, user3 };\n        var userIds = users.Select(u => u.Id).ToList();\n\n        sutProvider.GetDependency<IUserRepository>()\n            .GetPremiumAccessByIdsAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(userIds)))\n            .Returns(users);\n\n        // Act\n        var result = await sutProvider.Sut.HasPremiumAccessAsync(userIds);\n\n        // Assert\n        Assert.Equal(3, result.Count);\n        Assert.True(result[user1.Id]);  // Personal premium\n        Assert.False(result[user2.Id]); // No premium\n        Assert.True(result[user3.Id]);  // Organization premium\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Pricing/PricingClientTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Core.Billing;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing RichardSzalay.MockHttp;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.Billing.Pricing;\n\n[SutProviderCustomize]\npublic class PricingClientTests\n{\n    #region GetLookupKey Tests (via GetPlan)\n\n    [Fact]\n    public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagEnabled_UsesFamilies2025LookupKey()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        var planJson = CreatePlanJson(\"families-2025\", \"Families 2025\", \"families\", 40M, \"price_id\");\n\n        mockHttp.Expect(HttpMethod.Get, \"https://test.com/plans/organization/families-2025\")\n            .Respond(\"application/json\", planJson);\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization/*\")\n            .Respond(\"application/json\", planJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);\n        mockHttp.VerifyNoOutstandingExpectation();\n    }\n\n    [Fact]\n    public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagDisabled_UsesFamiliesLookupKey()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        var planJson = CreatePlanJson(\"families\", \"Families\", \"families\", 40M, \"price_id\");\n\n        mockHttp.Expect(HttpMethod.Get, \"https://test.com/plans/organization/families\")\n            .Respond(\"application/json\", planJson);\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization/*\")\n            .Respond(\"application/json\", planJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);\n\n        // Assert\n        Assert.NotNull(result);\n        // PreProcessFamiliesPreMigrationPlan should change \"families\" to \"families-2025\" when FF is disabled\n        Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);\n        mockHttp.VerifyNoOutstandingExpectation();\n    }\n\n    #endregion\n\n    #region PreProcessFamiliesPreMigrationPlan Tests (via GetPlan)\n\n    [Fact]\n    public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagDisabled_ReturnsFamiliesAnnually2025PlanType()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        // billing-pricing returns \"families\" lookup key because the flag is off\n        var planJson = CreatePlanJson(\"families\", \"Families\", \"families\", 40M, \"price_id\");\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization/*\")\n            .Respond(\"application/json\", planJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);\n\n        // Assert\n        Assert.NotNull(result);\n        // PreProcessFamiliesPreMigrationPlan should convert the families lookup key to families-2025\n        // and the PlanAdapter should assign the correct FamiliesAnnually2025 plan type\n        Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);\n        mockHttp.VerifyNoOutstandingExpectation();\n    }\n\n    [Fact]\n    public async Task GetPlan_WithFamiliesAnnually2025AndFeatureFlagEnabled_ReturnsFamiliesAnnually2025PlanType()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        var planJson = CreatePlanJson(\"families-2025\", \"Families\", \"families\", 40M, \"price_id\");\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization/*\")\n            .Respond(\"application/json\", planJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);\n\n        // Assert\n        Assert.NotNull(result);\n        // PreProcessFamiliesPreMigrationPlan should ignore the lookup key because the flag is on\n        // and the PlanAdapter should assign the correct FamiliesAnnually2025 plan type\n        Assert.Equal(PlanType.FamiliesAnnually2025, result.Type);\n        mockHttp.VerifyNoOutstandingExpectation();\n    }\n\n    [Fact]\n    public async Task GetPlan_WithFamiliesAnnuallyAndFeatureFlagEnabled_ReturnsFamiliesAnnuallyPlanType()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        var planJson = CreatePlanJson(\"families\", \"Families\", \"families\", 40M, \"price_id\");\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization/*\")\n            .Respond(\"application/json\", planJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually);\n\n        // Assert\n        Assert.NotNull(result);\n        // PreProcessFamiliesPreMigrationPlan should ignore the lookup key because the flag is on\n        // and the PlanAdapter should assign the correct FamiliesAnnually plan type\n        Assert.Equal(PlanType.FamiliesAnnually, result.Type);\n        mockHttp.VerifyNoOutstandingExpectation();\n    }\n\n    [Fact]\n    public async Task GetPlan_WithOtherLookupKey_KeepsLookupKeyUnchanged()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        var planJson = CreatePlanJson(\"enterprise-annually\", \"Enterprise\", \"enterprise\", 144M, \"price_id\");\n\n        mockHttp.Expect(HttpMethod.Get, \"https://test.com/plans/organization/enterprise-annually\")\n            .Respond(\"application/json\", planJson);\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization/*\")\n            .Respond(\"application/json\", planJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.GetPlan(PlanType.EnterpriseAnnually);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(PlanType.EnterpriseAnnually, result.Type);\n        mockHttp.VerifyNoOutstandingExpectation();\n    }\n\n    #endregion\n\n    #region ListPlans Tests\n\n    [Fact]\n    public async Task ListPlans_WithFeatureFlagDisabled_ReturnsListWithPreProcessing()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        // biling-pricing would return \"families\" because the flag is disabled\n        var plansJson = $@\"[\n            {CreatePlanJson(\"families\", \"Families\", \"families\", 40M, \"price_id\")},\n            {CreatePlanJson(\"enterprise-annually\", \"Enterprise\", \"enterprise\", 144M, \"price_id\")}\n        ]\";\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization\")\n            .Respond(\"application/json\", plansJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(false);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.ListPlans();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(2, result.Count);\n        // First plan should have been preprocessed from \"families\" to \"families-2025\"\n        Assert.Equal(PlanType.FamiliesAnnually2025, result[0].Type);\n        // Second plan should remain unchanged\n        Assert.Equal(PlanType.EnterpriseAnnually, result[1].Type);\n        mockHttp.VerifyNoOutstandingExpectation();\n    }\n\n    [Fact]\n    public async Task ListPlans_WithFeatureFlagEnabled_ReturnsListWithoutPreProcessing()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        var plansJson = $@\"[\n            {CreatePlanJson(\"families\", \"Families\", \"families\", 40M, \"price_id\")}\n        ]\";\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization\")\n            .Respond(\"application/json\", plansJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.ListPlans();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Single(result);\n        // Plan should remain as FamiliesAnnually when FF is enabled\n        Assert.Equal(PlanType.FamiliesAnnually, result[0].Type);\n        mockHttp.VerifyNoOutstandingExpectation();\n    }\n\n    #endregion\n\n    #region GetPlan - Additional Coverage\n\n    [Theory, BitAutoData]\n    public async Task GetPlan_WhenSelfHosted_ReturnsNull(\n        SutProvider<PricingClient> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<GlobalSettings>();\n        globalSettings.SelfHosted = true;\n\n        // Act\n        var result = await sutProvider.Sut.GetPlan(PlanType.FamiliesAnnually2025);\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPlan_WhenLookupKeyNotFound_ReturnsNull(\n        SutProvider<PricingClient> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;\n\n        // Act - Using PlanType that doesn't have a lookup key mapping\n        var result = await sutProvider.Sut.GetPlan(unchecked((PlanType)999));\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    [Fact]\n    public async Task GetPlan_WhenPricingServiceReturnsNotFound_ReturnsNull()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization/*\")\n            .Respond(HttpStatusCode.NotFound);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.GetPlan(PlanType.FamiliesAnnually2025);\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    [Fact]\n    public async Task GetPlan_WhenPricingServiceReturnsError_ThrowsBillingException()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization/*\")\n            .Respond(HttpStatusCode.InternalServerError);\n\n        var featureService = Substitute.For<IFeatureService>();\n        featureService.IsEnabled(FeatureFlagKeys.PM26462_Milestone_3).Returns(true);\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BillingException>(() =>\n            pricingClient.GetPlan(PlanType.FamiliesAnnually2025));\n    }\n\n    #endregion\n\n    #region ListPlans - Additional Coverage\n\n    [Theory, BitAutoData]\n    public async Task ListPlans_WhenSelfHosted_ReturnsEmptyList(\n        SutProvider<PricingClient> sutProvider)\n    {\n        // Arrange\n        var globalSettings = sutProvider.GetDependency<GlobalSettings>();\n        globalSettings.SelfHosted = true;\n\n        // Act\n        var result = await sutProvider.Sut.ListPlans();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task ListPlans_WhenPricingServiceReturnsError_ThrowsBillingException()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        mockHttp.When(HttpMethod.Get, \"*/plans/organization\")\n            .Respond(HttpStatusCode.InternalServerError);\n\n        var featureService = Substitute.For<IFeatureService>();\n\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BillingException>(() =>\n            pricingClient.ListPlans());\n    }\n\n    #endregion\n\n    #region ListPremiumPlans Tests\n\n    [Fact]\n    public async Task ListPremiumPlans_Success_ReturnsPremiumPlans()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        var plansJson = $@\"[\n            {CreatePremiumPlanJson(\"Premium\", true, null, 10M, \"price_premium\", 4M, \"price_storage\", 1)},\n            {CreatePremiumPlanJson(\"Premium Legacy\", false, 2019, 10M, \"price_premium_legacy\", 4M, \"price_storage_legacy\", 1)}\n        ]\";\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/premium\")\n            .Respond(\"application/json\", plansJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.ListPremiumPlans();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(2, result.Count);\n        Assert.Equal(\"Premium\", result[0].Name);\n        Assert.True(result[0].Available);\n        Assert.Null(result[0].LegacyYear);\n        Assert.Equal(10M, result[0].Seat.Price);\n        Assert.Equal(\"price_premium\", result[0].Seat.StripePriceId);\n        Assert.Equal(4M, result[0].Storage.Price);\n        Assert.Equal(\"price_storage\", result[0].Storage.StripePriceId);\n        Assert.Equal(1, result[0].Storage.Provided);\n        Assert.Equal(\"Premium Legacy\", result[1].Name);\n        Assert.False(result[1].Available);\n        Assert.Equal(2019, result[1].LegacyYear);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ListPremiumPlans_WhenSelfHosted_ReturnsEmptyList(\n        SutProvider<PricingClient> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<GlobalSettings>().SelfHosted = true;\n\n        // Act\n        var result = await sutProvider.Sut.ListPremiumPlans();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task ListPremiumPlans_WhenPricingServiceReturnsError_ThrowsBillingException()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        mockHttp.When(HttpMethod.Get, \"*/plans/premium\")\n            .Respond(HttpStatusCode.InternalServerError);\n\n        var featureService = Substitute.For<IFeatureService>();\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BillingException>(() =>\n            pricingClient.ListPremiumPlans());\n    }\n\n    #endregion\n\n    #region GetAvailablePremiumPlan Tests\n\n    [Fact]\n    public async Task GetAvailablePremiumPlan_WithAvailablePlan_ReturnsIt()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        var plansJson = $@\"[\n            {CreatePremiumPlanJson(\"Premium Legacy\", false, 2019, 10M, \"price_legacy\", 4M, \"price_storage_legacy\", 1)},\n            {CreatePremiumPlanJson(\"Premium\", true, null, 10M, \"price_premium\", 4M, \"price_storage\", 1)}\n        ]\";\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/premium\")\n            .Respond(\"application/json\", plansJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act\n        var result = await pricingClient.GetAvailablePremiumPlan();\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(\"Premium\", result.Name);\n        Assert.True(result.Available);\n    }\n\n    [Fact]\n    public async Task GetAvailablePremiumPlan_WithNoAvailablePlan_ThrowsNotFoundException()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        var plansJson = $@\"[\n            {CreatePremiumPlanJson(\"Premium Legacy\", false, 2019, 10M, \"price_legacy\", 4M, \"price_storage_legacy\", 1)}\n        ]\";\n\n        mockHttp.When(HttpMethod.Get, \"*/plans/premium\")\n            .Respond(\"application/json\", plansJson);\n\n        var featureService = Substitute.For<IFeatureService>();\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            pricingClient.GetAvailablePremiumPlan());\n    }\n\n    [Fact]\n    public async Task GetAvailablePremiumPlan_WithEmptyList_ThrowsNotFoundException()\n    {\n        // Arrange\n        var mockHttp = new MockHttpMessageHandler();\n        mockHttp.When(HttpMethod.Get, \"*/plans/premium\")\n            .Respond(\"application/json\", \"[]\");\n\n        var featureService = Substitute.For<IFeatureService>();\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n\n        var httpClient = new HttpClient(mockHttp)\n        {\n            BaseAddress = new Uri(\"https://test.com/\")\n        };\n\n        var logger = Substitute.For<ILogger<PricingClient>>();\n        var pricingClient = new PricingClient(featureService, globalSettings, httpClient, logger);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            pricingClient.GetAvailablePremiumPlan());\n    }\n\n    #endregion\n\n    private static string CreatePlanJson(\n        string lookupKey,\n        string name,\n        string tier,\n        decimal seatsPrice,\n        string seatsStripePriceId,\n        int seatsQuantity = 1)\n    {\n        return $@\"{{\n            \"\"lookupKey\"\": \"\"{lookupKey}\"\",\n            \"\"name\"\": \"\"{name}\"\",\n            \"\"tier\"\": \"\"{tier}\"\",\n            \"\"features\"\": [],\n            \"\"seats\"\": {{\n                \"\"type\"\": \"\"packaged\"\",\n                \"\"quantity\"\": {seatsQuantity},\n                \"\"price\"\": {seatsPrice},\n                \"\"stripePriceId\"\": \"\"{seatsStripePriceId}\"\"\n            }},\n            \"\"canUpgradeTo\"\": [],\n            \"\"additionalData\"\": {{\n                \"\"nameLocalizationKey\"\": \"\"{lookupKey}Name\"\",\n                \"\"descriptionLocalizationKey\"\": \"\"{lookupKey}Description\"\"\n            }}\n        }}\";\n    }\n\n    private static string CreatePremiumPlanJson(\n        string name,\n        bool available,\n        int? legacyYear,\n        decimal seatPrice,\n        string seatStripePriceId,\n        decimal storagePrice,\n        string storageStripePriceId,\n        int storageProvided)\n    {\n        var legacyYearJson = legacyYear.HasValue ? legacyYear.Value.ToString() : \"null\";\n        return $@\"{{\n            \"\"name\"\": \"\"{name}\"\",\n            \"\"available\"\": {available.ToString().ToLower()},\n            \"\"legacyYear\"\": {legacyYearJson},\n            \"\"seat\"\": {{\n                \"\"stripePriceId\"\": \"\"{seatStripePriceId}\"\",\n                \"\"price\"\": {seatPrice},\n                \"\"provided\"\": 0\n            }},\n            \"\"storage\"\": {{\n                \"\"stripePriceId\"\": \"\"{storageStripePriceId}\"\",\n                \"\"price\"\": {storagePrice},\n                \"\"provided\"\": {storageProvided}\n            }}\n        }}\";\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Services/DiscountAudienceFilters/AllUsersFilterTests.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Services.DiscountAudienceFilters;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Entities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Services.DiscountAudienceFilters;\n\npublic class AllUsersFilterTests\n{\n    private readonly AllUsersFilter _sut = new();\n    private readonly User _user = new();\n\n    [Fact]\n    public async Task IsUserEligible_NullStripeProductIds_ReturnsAllTiersTrue()\n    {\n        var discount = new SubscriptionDiscount { StripeProductIds = null };\n\n        var result = await _sut.IsUserEligible(_user, discount);\n\n        Assert.All(result.Values, Assert.True);\n    }\n\n    [Fact]\n    public async Task IsUserEligible_EmptyStripeProductIds_ReturnsAllTiersTrue()\n    {\n        var discount = new SubscriptionDiscount { StripeProductIds = [] };\n\n        var result = await _sut.IsUserEligible(_user, discount);\n\n        Assert.All(result.Values, Assert.True);\n    }\n\n    [Fact]\n    public async Task IsUserEligible_PremiumProductId_ReturnsPremiumTrueOthersFalse()\n    {\n        var discount = new SubscriptionDiscount { StripeProductIds = [StripeConstants.ProductIDs.Premium] };\n\n        var result = await _sut.IsUserEligible(_user, discount);\n\n        Assert.True(result[DiscountTierType.Premium]);\n        Assert.False(result[DiscountTierType.Families]);\n    }\n\n    [Fact]\n    public async Task IsUserEligible_FamiliesProductId_ReturnsFamiliesTrueOthersFalse()\n    {\n        var discount = new SubscriptionDiscount { StripeProductIds = [StripeConstants.ProductIDs.Families] };\n\n        var result = await _sut.IsUserEligible(_user, discount);\n\n        Assert.True(result[DiscountTierType.Families]);\n        Assert.False(result[DiscountTierType.Premium]);\n    }\n\n\n\n    [Fact]\n    public async Task IsUserEligible_UnknownProductId_ReturnsAllFalse()\n    {\n        var discount = new SubscriptionDiscount { StripeProductIds = [\"prod_unknown\"] };\n\n        var result = await _sut.IsUserEligible(_user, discount);\n\n        Assert.DoesNotContain(result.Values, v => v);\n    }\n\n    [Fact]\n    public async Task IsUserEligible_MultipleKnownProductIds_ReturnsMappedTiersTrue()\n    {\n        var discount = new SubscriptionDiscount\n        {\n            StripeProductIds = [StripeConstants.ProductIDs.Premium, StripeConstants.ProductIDs.Families]\n        };\n\n        var result = await _sut.IsUserEligible(_user, discount);\n\n        Assert.True(result[DiscountTierType.Premium]);\n        Assert.True(result[DiscountTierType.Families]);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Services/DiscountAudienceFilters/DiscountAudienceFilterFactoryTests.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Services.DiscountAudienceFilters;\nusing Bit.Core.Repositories;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Services.DiscountAudienceFilters;\n\npublic class DiscountAudienceFilterFactoryTests\n{\n    private readonly DiscountAudienceFilterFactory _sut = new(\n    [\n        new AllUsersFilter(),\n        new UserHasNoPreviousSubscriptionsFilter(\n            Substitute.For<IStripeAdapter>(),\n            Substitute.For<IOrganizationUserRepository>(),\n            Substitute.For<IPricingClient>())\n    ]);\n\n    [Fact]\n    public void GetFilter_UserHasNoPreviousSubscriptions_ReturnsCorrectFilter()\n    {\n        // Act\n        var filter = _sut.GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions);\n\n        // Assert\n        Assert.IsType<UserHasNoPreviousSubscriptionsFilter>(filter);\n    }\n\n    [Fact]\n    public void GetFilter_AllUsers_ReturnsCorrectFilter()\n    {\n        // Act\n        var filter = _sut.GetFilter(DiscountAudienceType.AllUsers);\n\n        // Assert\n        Assert.IsType<AllUsersFilter>(filter);\n    }\n\n    [Fact]\n    public void GetFilter_UnknownAudienceType_ReturnsNull()\n    {\n        // Act\n        var filter = _sut.GetFilter((DiscountAudienceType)99);\n\n        // Assert\n        Assert.Null(filter);\n    }\n\n    [Fact]\n    public void GetFilter_AllDefinedAudienceTypes_ReturnsExpectedFilter()\n    {\n        // Arrange\n        var allAudienceTypes = Enum.GetValues<DiscountAudienceType>();\n\n        // Act & Assert: All defined audience types must return a non-null filter\n        foreach (var audienceType in allAudienceTypes)\n        {\n            var filter = _sut.GetFilter(audienceType);\n            Assert.NotNull(filter);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Services/DiscountAudienceFilters/UserHasNoPreviousSubscriptionsFilterTests.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Services.DiscountAudienceFilters;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\nusing PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan;\nusing Purchasable = Bit.Core.Billing.Pricing.Premium.Purchasable;\n\nnamespace Bit.Core.Test.Billing.Services.DiscountAudienceFilters;\n\npublic class UserHasNoPreviousSubscriptionsFilterTests\n{\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n\n    private readonly IOrganizationUserRepository _organizationUserRepository =\n        Substitute.For<IOrganizationUserRepository>();\n\n    private readonly UserHasNoPreviousSubscriptionsFilter _sut;\n\n    public UserHasNoPreviousSubscriptionsFilterTests()\n    {\n        _sut = new UserHasNoPreviousSubscriptionsFilter(\n            _stripeAdapter,\n            _organizationUserRepository,\n            _pricingClient);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_PremiumProductId_UserHasPremium_ReturnsEligibleForPremiumFalse(\n        User user,\n        SubscriptionDiscount discount)\n    {\n        user.Premium = true;\n        discount.StripeProductIds = [StripeConstants.ProductIDs.Premium];\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.False(result[DiscountTierType.Premium]);\n        await _pricingClient.DidNotReceive().ListPremiumPlans();\n        await _stripeAdapter.DidNotReceive().ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>());\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_PremiumProductId_HasGatewayCustomerId_NoPreviousPremiumSubscription_ReturnsEligibleForPremiumTrue(\n        User user,\n        SubscriptionDiscount discount)\n    {\n        user.Premium = false;\n        user.GatewayCustomerId = \"cus_123\";\n        discount.StripeProductIds = [StripeConstants.ProductIDs.Premium];\n        const string premiumPriceId = \"price_premium\";\n\n        _pricingClient\n            .ListPremiumPlans()\n            .Returns([new PremiumPlan { Seat = new Purchasable { StripePriceId = premiumPriceId } }]);\n\n        _stripeAdapter\n            .ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(new StripeList<Subscription> { Data = [] });\n\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.True(result[DiscountTierType.Premium]);\n        await _pricingClient.Received(1).ListPremiumPlans();\n        await _stripeAdapter.Received(1).ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_PremiumProductId_HasGatewayCustomerId_HasPreviousPremiumSubscription_ReturnsEligibleForPremiumFalse(\n        User user,\n        SubscriptionDiscount discount)\n    {\n        user.Premium = false;\n        user.GatewayCustomerId = \"cus_123\";\n        discount.StripeProductIds = [StripeConstants.ProductIDs.Premium];\n        const string premiumPriceId = \"price_premium\";\n\n        _pricingClient\n            .ListPremiumPlans()\n            .Returns([new PremiumPlan { Seat = new Purchasable { StripePriceId = premiumPriceId } }]);\n\n        _stripeAdapter\n            .ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Items = new StripeList<SubscriptionItem>\n                        {\n                            Data = [new SubscriptionItem { Price = new Price { Id = premiumPriceId } }]\n                        }\n                    }\n                ]\n            });\n\n        _organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed)\n            .Returns(new List<OrganizationUserOrganizationDetails>\n            {\n                new() { Type = OrganizationUserType.Owner, PlanType = PlanType.FamiliesAnnually }\n            });\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.False(result[DiscountTierType.Premium]);\n        await _pricingClient.Received(1).ListPremiumPlans();\n        await _stripeAdapter.Received(1).ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_PremiumProductId_NoPremium_NoGatewayCustomerId_ReturnsEligibleForPremiumTrue(\n        User user,\n        SubscriptionDiscount discount)\n    {\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        discount.StripeProductIds = [StripeConstants.ProductIDs.Premium];\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.True(result[DiscountTierType.Premium]);\n        await _pricingClient.DidNotReceive().ListPremiumPlans();\n        await _stripeAdapter.DidNotReceive().ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_FamilyProductId_UserDoesNotOwnFamiliesOrg_ReturnsFamiliesTrue(\n        User user,\n        SubscriptionDiscount discount)\n    {\n        discount.StripeProductIds = [StripeConstants.ProductIDs.Families];\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.True(result[DiscountTierType.Families]);\n        await _organizationUserRepository.Received(1)\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_FamilyProductId_UserOwnsFamiliesOrg_ReturnsFamiliesFalse(\n        User user,\n        SubscriptionDiscount discount)\n    {\n        discount.StripeProductIds = [StripeConstants.ProductIDs.Families];\n\n        _organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed)\n            .Returns(new List<OrganizationUserOrganizationDetails>\n            {\n                new() { Type = OrganizationUserType.Owner, PlanType = PlanType.FamiliesAnnually }\n            });\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.False(result[DiscountTierType.Families]);\n        await _organizationUserRepository.Received(1)\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_NoProductIds_UserOwnsFamiliesOrg_ReturnsPremiumTrueFamiliesFalse(\n        User user,\n        SubscriptionDiscount discount)\n    {\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        discount.StripeProductIds = null;\n\n        _organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed)\n            .Returns(new List<OrganizationUserOrganizationDetails>\n            {\n                new() { Type = OrganizationUserType.Owner, PlanType = PlanType.FamiliesAnnually }\n            });\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.True(result[DiscountTierType.Premium]);\n        Assert.False(result[DiscountTierType.Families]);\n        await _organizationUserRepository.Received(1)\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);\n        await _pricingClient.DidNotReceive().ListPremiumPlans();\n        await _stripeAdapter.DidNotReceive().ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_NoProductIds_UserDoesNotHavePremium_UserDoesNotOwnFamiliesOrg_ReturnsPremiumTrueFamiliesTrue(\n        User user,\n        SubscriptionDiscount discount)\n    {\n        user.Premium = false;\n        user.GatewayCustomerId = null;\n        discount.StripeProductIds = null;\n\n        _organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed)\n            .Returns(new List<OrganizationUserOrganizationDetails>());\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.True(result[DiscountTierType.Premium]);\n        Assert.True(result[DiscountTierType.Families]);\n        await _organizationUserRepository.Received(1)\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);\n        await _pricingClient.DidNotReceive().ListPremiumPlans();\n        await _stripeAdapter.DidNotReceive().ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task\n        IsUserEligible_NoProductIds_UserHasPremiumSubscription_DoesNotOwnFamiliesOrg_ReturnsPremiumFalseFamiliesTrue(\n            User user,\n            SubscriptionDiscount discount)\n    {\n        user.Premium = true;\n        discount.StripeProductIds = null;\n\n        _organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed)\n            .Returns(new List<OrganizationUserOrganizationDetails>());\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.False(result[DiscountTierType.Premium]);\n        Assert.True(result[DiscountTierType.Families]);\n        await _organizationUserRepository.Received(1)\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_SameUser_MultiplePremiumDiscounts_OnlyCallsStripeOnce(\n        User user,\n        SubscriptionDiscount discount1,\n        SubscriptionDiscount discount2)\n    {\n        user.Premium = false;\n        user.GatewayCustomerId = \"cus_123\";\n        discount1.StripeProductIds = [StripeConstants.ProductIDs.Premium];\n        discount2.StripeProductIds = [StripeConstants.ProductIDs.Premium];\n        const string premiumPriceId = \"price_premium\";\n\n        _pricingClient\n            .ListPremiumPlans()\n            .Returns([new PremiumPlan { Seat = new Purchasable { StripePriceId = premiumPriceId } }]);\n\n        _stripeAdapter\n            .ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(new StripeList<Subscription> { Data = [] });\n\n        await _sut.IsUserEligible(user, discount1);\n        await _sut.IsUserEligible(user, discount2);\n\n        await _pricingClient.Received(1).ListPremiumPlans();\n        await _stripeAdapter.Received(1).ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsUserEligible_SameUser_MultipleFamiliesDiscounts_OnlyCallsOrgRepoOnce(\n        User user,\n        SubscriptionDiscount discount1,\n        SubscriptionDiscount discount2)\n    {\n        discount1.StripeProductIds = [StripeConstants.ProductIDs.Families];\n        discount2.StripeProductIds = [StripeConstants.ProductIDs.Families];\n\n        _organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed)\n            .Returns(new List<OrganizationUserOrganizationDetails>());\n\n        await _sut.IsUserEligible(user, discount1);\n        await _sut.IsUserEligible(user, discount2);\n\n        await _organizationUserRepository.Received(1)\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);\n    }\n\n    [Theory, BitAutoData]\n    public async Task\n        IsUserEligible_NoProductIds_UserHadPastPremiumSubscription_OwnsFamiliesOrg_ReturnsPremiumFalseFamiliesFalse(\n            User user,\n            SubscriptionDiscount discount)\n    {\n        user.Premium = false;\n        user.GatewayCustomerId = \"cus_123\";\n        discount.StripeProductIds = null;\n        const string premiumPriceId = \"price_premium\";\n\n        _pricingClient\n            .ListPremiumPlans()\n            .Returns([new PremiumPlan { Seat = new Purchasable { StripePriceId = premiumPriceId } }]);\n\n        _stripeAdapter\n            .ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>())\n            .Returns(new StripeList<Subscription>\n            {\n                Data =\n                [\n                    new Subscription\n                    {\n                        Items = new StripeList<SubscriptionItem>\n                        {\n                            Data = [new SubscriptionItem { Price = new Price { Id = premiumPriceId } }]\n                        }\n                    }\n                ]\n            });\n\n        _organizationUserRepository\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed)\n            .Returns(new List<OrganizationUserOrganizationDetails>\n            {\n                new() { Type = OrganizationUserType.Owner, PlanType = PlanType.FamiliesAnnually }\n            });\n\n        var result = await _sut.IsUserEligible(user, discount);\n\n        Assert.False(result[DiscountTierType.Premium]);\n        Assert.False(result[DiscountTierType.Families]);\n        await _organizationUserRepository.Received(1)\n            .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed);\n        await _pricingClient.Received(1).ListPremiumPlans();\n        await _stripeAdapter.Received(1).ListSubscriptionsAsync(Arg.Any<SubscriptionListOptions>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Services/LicensingServiceTests.cs",
    "content": "﻿using System.Text.Json;\nusing AutoFixture;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Billing.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Services;\n\n[SutProviderCustomize]\npublic class LicensingServiceTests\n{\n    private static string licenseFilePath(Guid orgId) =>\n        Path.Combine(OrganizationLicenseDirectory.Value, $\"{orgId}.json\");\n    private static string userLicenseFilePath(Guid userId) =>\n        Path.Combine(UserLicenseDirectory.Value, $\"{userId}.json\");\n    private static string LicenseDirectory => Path.GetDirectoryName(OrganizationLicenseDirectory.Value);\n    private static Lazy<string> OrganizationLicenseDirectory => new(() =>\n    {\n        var directory = Path.Combine(Path.GetTempPath(), \"organization\");\n        if (!Directory.Exists(directory))\n        {\n            Directory.CreateDirectory(directory);\n        }\n        return directory;\n    });\n    private static Lazy<string> UserLicenseDirectory => new(() =>\n    {\n        var directory = Path.Combine(Path.GetTempPath(), \"user\");\n        if (!Directory.Exists(directory))\n        {\n            Directory.CreateDirectory(directory);\n        }\n        return directory;\n    });\n\n    public static SutProvider<LicensingService> GetSutProvider()\n    {\n        var fixture = new Fixture().WithAutoNSubstitutions();\n\n        var settings = fixture.Create<IGlobalSettings>();\n        settings.LicenseDirectory = LicenseDirectory;\n        settings.SelfHosted = true;\n\n        return new SutProvider<LicensingService>(fixture)\n            .SetDependency(settings)\n            .Create();\n    }\n\n    [Theory, BitAutoData, OrganizationLicenseCustomize]\n    public async Task ReadOrganizationLicense(Organization organization, OrganizationLicense license)\n    {\n        var sutProvider = GetSutProvider();\n\n        File.WriteAllText(licenseFilePath(organization.Id), JsonSerializer.Serialize(license));\n\n        var actual = await sutProvider.Sut.ReadOrganizationLicenseAsync(organization);\n        try\n        {\n            Assert.Equal(JsonSerializer.Serialize(license), JsonSerializer.Serialize(actual));\n        }\n        finally\n        {\n            Directory.Delete(OrganizationLicenseDirectory.Value, true);\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task WriteUserLicense_CreatesFileWithCorrectContent(User user, UserLicense license)\n    {\n        // Arrange\n        var sutProvider = GetSutProvider();\n        var expectedFilePath = userLicenseFilePath(user.Id);\n\n        try\n        {\n            // Act\n            await sutProvider.Sut.WriteUserLicenseAsync(user, license);\n\n            // Assert\n            Assert.True(File.Exists(expectedFilePath));\n            var fileContent = await File.ReadAllTextAsync(expectedFilePath);\n            var actualLicense = JsonSerializer.Deserialize<UserLicense>(fileContent);\n\n            Assert.Equal(license.LicenseKey, actualLicense.LicenseKey);\n            Assert.Equal(license.Id, actualLicense.Id);\n            Assert.Equal(license.Expires, actualLicense.Expires);\n        }\n        finally\n        {\n            // Cleanup\n            if (Directory.Exists(UserLicenseDirectory.Value))\n            {\n                Directory.Delete(UserLicenseDirectory.Value, true);\n            }\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task WriteUserLicense_CreatesDirectoryIfNotExists(User user, UserLicense license)\n    {\n        // Arrange\n        var sutProvider = GetSutProvider();\n\n        // Ensure directory doesn't exist\n        if (Directory.Exists(UserLicenseDirectory.Value))\n        {\n            Directory.Delete(UserLicenseDirectory.Value, true);\n        }\n\n        try\n        {\n            // Act\n            await sutProvider.Sut.WriteUserLicenseAsync(user, license);\n\n            // Assert\n            Assert.True(Directory.Exists(UserLicenseDirectory.Value));\n            Assert.True(File.Exists(userLicenseFilePath(user.Id)));\n        }\n        finally\n        {\n            // Cleanup\n            if (Directory.Exists(UserLicenseDirectory.Value))\n            {\n                Directory.Delete(UserLicenseDirectory.Value, true);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Models.Sales;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Organizations.Services;\nusing Bit.Core.Billing.Payment.Queries;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Services;\n\n[SutProviderCustomize]\npublic class OrganizationBillingServiceTests\n{\n\n    #region Finalize - Trial Settings\n\n    [Theory, BitAutoData]\n    public async Task NoPaymentMethodAndTrialPeriod_SetsMissingPaymentMethodCancelBehavior(\n        Organization organization,\n        User owner,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n        organization.PlanType = PlanType.TeamsAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.TeamsAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = owner\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.TeamsAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<IHasPaymentMethodQuery>()\n            .Run(organization)\n            .Returns(false);\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        SubscriptionCreateOptions capturedOptions = null;\n        sutProvider.GetDependency<IStripeAdapter>()\n            .CreateSubscriptionAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))\n            .Returns(new Subscription\n            {\n                Id = \"sub_test123\",\n                Status = StripeConstants.SubscriptionStatus.Trialing\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .ReplaceAsync(organization)\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await sutProvider.Sut.Finalize(sale);\n\n        // Assert\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n\n        Assert.NotNull(capturedOptions);\n        Assert.Equal(7, capturedOptions.TrialPeriodDays);\n        Assert.NotNull(capturedOptions.TrialSettings);\n        Assert.NotNull(capturedOptions.TrialSettings.EndBehavior);\n        Assert.Equal(\"cancel\", capturedOptions.TrialSettings.EndBehavior.MissingPaymentMethod);\n    }\n\n    [Theory, BitAutoData]\n    public async Task NoPaymentMethodButNoTrial_DoesNotSetMissingPaymentMethodBehavior(\n        Organization organization,\n        User owner,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n        organization.PlanType = PlanType.TeamsAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.TeamsAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = true // This will result in TrialPeriodDays = 0\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = owner\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.TeamsAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<IHasPaymentMethodQuery>()\n            .Run(organization)\n            .Returns(false);\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        SubscriptionCreateOptions capturedOptions = null;\n        sutProvider.GetDependency<IStripeAdapter>()\n            .CreateSubscriptionAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))\n            .Returns(new Subscription\n            {\n                Id = \"sub_test123\",\n                Status = StripeConstants.SubscriptionStatus.Active\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .ReplaceAsync(organization)\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await sutProvider.Sut.Finalize(sale);\n\n        // Assert\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n\n        Assert.NotNull(capturedOptions);\n        Assert.Equal(0, capturedOptions.TrialPeriodDays);\n        Assert.Null(capturedOptions.TrialSettings);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPaymentMethodAndTrialPeriod_DoesNotSetMissingPaymentMethodBehavior(\n        Organization organization,\n        User owner,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n        organization.PlanType = PlanType.TeamsAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.TeamsAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = owner\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.TeamsAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<IHasPaymentMethodQuery>()\n            .Run(organization)\n            .Returns(true); // Has payment method\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        SubscriptionCreateOptions capturedOptions = null;\n        sutProvider.GetDependency<IStripeAdapter>()\n            .CreateSubscriptionAsync(Arg.Do<SubscriptionCreateOptions>(options => capturedOptions = options))\n            .Returns(new Subscription\n            {\n                Id = \"sub_test123\",\n                Status = StripeConstants.SubscriptionStatus.Trialing\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .ReplaceAsync(organization)\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await sutProvider.Sut.Finalize(sale);\n\n        // Assert\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n\n        Assert.NotNull(capturedOptions);\n        Assert.Equal(7, capturedOptions.TrialPeriodDays);\n        Assert.Null(capturedOptions.TrialSettings);\n    }\n\n    #endregion\n\n    #region Finalize - Coupon Validation\n\n    [Theory, BitAutoData]\n    public async Task Finalize_WithValidCoupon_SuccessfullyCreatesSubscription(\n        Organization organization,\n        User owner,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.FamiliesAnnually);\n        organization.PlanType = PlanType.FamiliesAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var customerSetup = new CustomerSetup\n        {\n            Coupons = [\"VALID_COUPON\"]\n        };\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.FamiliesAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            CustomerSetup = customerSetup,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = owner\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.FamiliesAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .ValidateDiscountEligibilityForUserAsync(\n                owner,\n                Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_COUPON\" })),\n                DiscountTierType.Families)\n            .Returns(true);\n\n        sutProvider.GetDependency<IHasPaymentMethodQuery>()\n            .Run(organization)\n            .Returns(true);\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>())\n            .Returns(new Subscription\n            {\n                Id = \"sub_test123\",\n                Status = StripeConstants.SubscriptionStatus.Active\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .ReplaceAsync(organization)\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await sutProvider.Sut.Finalize(sale);\n\n        // Assert\n        await sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .Received(1)\n            .ValidateDiscountEligibilityForUserAsync(\n                owner,\n                Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_COUPON\" })),\n                DiscountTierType.Families);\n\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(opts =>\n                opts.Discounts != null && opts.Discounts.Count == 1 && opts.Discounts[0].Coupon == \"VALID_COUPON\"));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Finalize_WithInvalidCoupon_ThrowsBadRequestException(\n        Organization organization,\n        User owner,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.FamiliesAnnually);\n        organization.PlanType = PlanType.FamiliesAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var customerSetup = new CustomerSetup\n        {\n            Coupons = [\"INVALID_COUPON\"]\n        };\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.FamiliesAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            CustomerSetup = customerSetup,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = owner\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.FamiliesAnnually)\n            .Returns(plan);\n\n        // Return false to simulate invalid coupon\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .ValidateDiscountEligibilityForUserAsync(\n                owner,\n                Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"INVALID_COUPON\" })),\n                DiscountTierType.Families)\n            .Returns(false);\n\n        sutProvider.GetDependency<IHasPaymentMethodQuery>()\n            .Run(organization)\n            .Returns(true);\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Finalize(sale));\n        Assert.Equal(\"Discount expired. Please review your cart total and try again\", exception.Message);\n\n        await sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .Received(1)\n            .ValidateDiscountEligibilityForUserAsync(\n                owner,\n                Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"INVALID_COUPON\" })),\n                DiscountTierType.Families);\n\n        // Verify subscription was NOT created\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .DidNotReceive()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Finalize_WithNullCoupon_SkipsValidation(\n        Organization organization,\n        User owner,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n        organization.PlanType = PlanType.TeamsAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var customerSetup = new CustomerSetup\n        {\n            Coupons = null\n        };\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.TeamsAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            CustomerSetup = customerSetup,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = owner\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.TeamsAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<IHasPaymentMethodQuery>()\n            .Run(organization)\n            .Returns(true);\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>())\n            .Returns(new Subscription\n            {\n                Id = \"sub_test123\",\n                Status = StripeConstants.SubscriptionStatus.Active\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .ReplaceAsync(organization)\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await sutProvider.Sut.Finalize(sale);\n\n        // Assert - Validation should NOT be called\n        await sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .DidNotReceive()\n            .ValidateDiscountEligibilityForUserAsync(Arg.Any<User>(), Arg.Any<IReadOnlyList<string>>(), Arg.Any<DiscountTierType>());\n\n        // Subscription should still be created\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Finalize_WithCouponOutsideDateRange_ThrowsBadRequestException(\n        Organization organization,\n        User owner,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.FamiliesAnnually);\n        organization.PlanType = PlanType.FamiliesAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var customerSetup = new CustomerSetup\n        {\n            Coupons = [\"EXPIRED_COUPON\"]\n        };\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.FamiliesAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            CustomerSetup = customerSetup,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = owner\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.FamiliesAnnually)\n            .Returns(plan);\n\n        // Return false to simulate expired coupon (outside valid date range)\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .ValidateDiscountEligibilityForUserAsync(\n                owner,\n                Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"EXPIRED_COUPON\" })),\n                DiscountTierType.Families)\n            .Returns(false);\n\n        sutProvider.GetDependency<IHasPaymentMethodQuery>()\n            .Run(organization)\n            .Returns(true);\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Finalize(sale));\n        Assert.Equal(\"Discount expired. Please review your cart total and try again\", exception.Message);\n\n        await sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .Received(1)\n            .ValidateDiscountEligibilityForUserAsync(\n                owner,\n                Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"EXPIRED_COUPON\" })),\n                DiscountTierType.Families);\n\n        // Verify subscription was NOT created\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .DidNotReceive()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Finalize_WithMultipleValidCoupons_AppliesAllToSubscription(\n        Organization organization,\n        User owner,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.FamiliesAnnually);\n        organization.PlanType = PlanType.FamiliesAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var customerSetup = new CustomerSetup\n        {\n            Coupons = [\"COUPON_ONE\", \"COUPON_TWO\"]\n        };\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.FamiliesAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            CustomerSetup = customerSetup,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = owner\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.FamiliesAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .ValidateDiscountEligibilityForUserAsync(\n                owner,\n                Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"COUPON_ONE\", \"COUPON_TWO\" })),\n                DiscountTierType.Families)\n            .Returns(true);\n\n        sutProvider.GetDependency<IHasPaymentMethodQuery>()\n            .Run(organization)\n            .Returns(true);\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>())\n            .Returns(new Subscription\n            {\n                Id = \"sub_test123\",\n                Status = StripeConstants.SubscriptionStatus.Active\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .ReplaceAsync(organization)\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await sutProvider.Sut.Finalize(sale);\n\n        // Assert\n        await sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .Received(1)\n            .ValidateDiscountEligibilityForUserAsync(\n                owner,\n                Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"COUPON_ONE\", \"COUPON_TWO\" })),\n                DiscountTierType.Families);\n\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(opts =>\n                opts.Discounts != null &&\n                opts.Discounts.Count == 2 &&\n                opts.Discounts.Any(d => d.Coupon == \"COUPON_ONE\") &&\n                opts.Discounts.Any(d => d.Coupon == \"COUPON_TWO\")));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Finalize_WithOneInvalidCoupon_ThrowsBadRequestException(\n        Organization organization,\n        User owner,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.FamiliesAnnually);\n        organization.PlanType = PlanType.FamiliesAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var customerSetup = new CustomerSetup\n        {\n            Coupons = [\"VALID_COUPON\", \"INVALID_COUPON\"]\n        };\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.FamiliesAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            CustomerSetup = customerSetup,\n            SubscriptionSetup = subscriptionSetup,\n            Owner = owner\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.FamiliesAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<ISubscriptionDiscountService>()\n            .ValidateDiscountEligibilityForUserAsync(\n                owner,\n                Arg.Is<IReadOnlyList<string>>(a => a.SequenceEqual(new[] { \"VALID_COUPON\", \"INVALID_COUPON\" })),\n                DiscountTierType.Families)\n            .Returns(false);\n\n        sutProvider.GetDependency<IHasPaymentMethodQuery>()\n            .Run(organization)\n            .Returns(true);\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }\n        };\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Finalize(sale));\n        Assert.Equal(\"Discount expired. Please review your cart total and try again\", exception.Message);\n\n        // Verify subscription was NOT created\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .DidNotReceive()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task Finalize_BusinessWithExemptStatus_DoesNotUpdateTaxExemption(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n        organization.PlanType = PlanType.TeamsAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.TeamsAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            SubscriptionSetup = subscriptionSetup\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported },\n            Address = new Address { Country = \"DE\" },\n            TaxExempt = StripeConstants.TaxExempt.Exempt\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.TeamsAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>())\n            .Returns(new Subscription\n            {\n                Id = \"sub_test123\",\n                Status = StripeConstants.SubscriptionStatus.Active\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .ReplaceAsync(organization)\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await sutProvider.Sut.Finalize(sale);\n\n        // Assert\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .DidNotReceive()\n            .UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());\n    }\n\n    #endregion\n\n    [Theory, BitAutoData]\n    public async Task Finalize_SwissBusinessWithReverse_CorrectsTaxExemptToNone(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n        organization.PlanType = PlanType.TeamsAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.TeamsAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            SubscriptionSetup = subscriptionSetup\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported },\n            Address = new Address { Country = \"CH\" },\n            TaxExempt = StripeConstants.TaxExempt.Reverse\n        };\n\n        var correctedCustomer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported },\n            Address = new Address { Country = \"CH\" },\n            TaxExempt = StripeConstants.TaxExempt.None\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.TeamsAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateCustomerAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>\n                options.TaxExempt == StripeConstants.TaxExempt.None))\n            .Returns(correctedCustomer);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>())\n            .Returns(new Subscription\n            {\n                Id = \"sub_test123\",\n                Status = StripeConstants.SubscriptionStatus.Active\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .ReplaceAsync(organization)\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await sutProvider.Sut.Finalize(sale);\n\n        // Assert\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .UpdateCustomerAsync(\"cus_test123\",\n                Arg.Is<CustomerUpdateOptions>(options =>\n                    options.TaxExempt == StripeConstants.TaxExempt.None));\n    }\n\n    [Theory, BitAutoData]\n    public async Task Finalize_USBusinessWithReverseExempt_CorrectsTaxExemptToNone(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n        organization.PlanType = PlanType.TeamsAnnually;\n        organization.GatewayCustomerId = \"cus_test123\";\n        organization.GatewaySubscriptionId = null;\n\n        var subscriptionSetup = new SubscriptionSetup\n        {\n            PlanType = PlanType.TeamsAnnually,\n            PasswordManagerOptions = new SubscriptionSetup.PasswordManager\n            {\n                Seats = 5,\n                Storage = null,\n                PremiumAccess = false\n            },\n            SecretsManagerOptions = null,\n            SkipTrial = false\n        };\n\n        var sale = new OrganizationSale\n        {\n            Organization = organization,\n            SubscriptionSetup = subscriptionSetup\n        };\n\n        var customer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported },\n            Address = new Address { Country = \"US\" },\n            TaxExempt = StripeConstants.TaxExempt.Reverse\n        };\n\n        var correctedCustomer = new Customer\n        {\n            Id = \"cus_test123\",\n            Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported },\n            Address = new Address { Country = \"US\" },\n            TaxExempt = StripeConstants.TaxExempt.None\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.TeamsAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<ISubscriberService>()\n            .GetCustomerOrThrow(organization, Arg.Any<CustomerGetOptions>())\n            .Returns(customer);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateCustomerAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>\n                options.TaxExempt == StripeConstants.TaxExempt.None))\n            .Returns(correctedCustomer);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>())\n            .Returns(new Subscription\n            {\n                Id = \"sub_test123\",\n                Status = StripeConstants.SubscriptionStatus.Active\n            });\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .ReplaceAsync(organization)\n            .Returns(Task.CompletedTask);\n\n        // Act\n        await sutProvider.Sut.Finalize(sale);\n\n        // Assert: UpdateCustomerAsync called with TaxExempt = \"none\" to correct the erroneous \"reverse\"\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .UpdateCustomerAsync(customer.Id, Arg.Is<CustomerUpdateOptions>(options =>\n                options.TaxExempt == StripeConstants.TaxExempt.None));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationNameAndEmail_UpdatesStripeCustomer(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        organization.Name = \"Short name\";\n\n        CustomerUpdateOptions capturedOptions = null;\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateCustomerAsync(\n                Arg.Is<string>(id => id == organization.GatewayCustomerId),\n                Arg.Do<CustomerUpdateOptions>(options => capturedOptions = options))\n            .Returns(new Customer());\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);\n\n        // Assert\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .UpdateCustomerAsync(\n                organization.GatewayCustomerId,\n                Arg.Any<CustomerUpdateOptions>());\n\n        Assert.NotNull(capturedOptions);\n        Assert.Equal(organization.BillingEmail, capturedOptions.Email);\n        Assert.Equal(organization.DisplayName(), capturedOptions.Description);\n        Assert.NotNull(capturedOptions.InvoiceSettings);\n        Assert.NotNull(capturedOptions.InvoiceSettings.CustomFields);\n        Assert.Single(capturedOptions.InvoiceSettings.CustomFields);\n\n        var customField = capturedOptions.InvoiceSettings.CustomFields.First();\n        Assert.Equal(organization.SubscriberType(), customField.Name);\n        Assert.Equal(organization.DisplayName(), customField.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationNameAndEmail_WhenNameIsLong_UsesFullName(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        var longName = \"This is a very long organization name that exceeds thirty characters\";\n        organization.Name = longName;\n\n        CustomerUpdateOptions capturedOptions = null;\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateCustomerAsync(\n                Arg.Is<string>(id => id == organization.GatewayCustomerId),\n                Arg.Do<CustomerUpdateOptions>(options => capturedOptions = options))\n            .Returns(new Customer());\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);\n\n        // Assert\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .Received(1)\n            .UpdateCustomerAsync(\n                organization.GatewayCustomerId,\n                Arg.Any<CustomerUpdateOptions>());\n\n        Assert.NotNull(capturedOptions);\n        Assert.NotNull(capturedOptions.InvoiceSettings);\n        Assert.NotNull(capturedOptions.InvoiceSettings.CustomFields);\n\n        var customField = capturedOptions.InvoiceSettings.CustomFields.First();\n        Assert.Equal(longName, customField.Value);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationNameAndEmail_WhenGatewayCustomerIdIsNull_LogsWarningAndReturns(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        organization.GatewayCustomerId = null;\n        organization.Name = \"Test Organization\";\n        organization.BillingEmail = \"billing@example.com\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);\n\n        // Assert\n        await stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationNameAndEmail_WhenGatewayCustomerIdIsEmpty_LogsWarningAndReturns(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        organization.GatewayCustomerId = \"\";\n        organization.Name = \"Test Organization\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);\n\n        // Assert\n        await stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationNameAndEmail_WhenNameIsNull_LogsWarningAndReturns(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        organization.Name = null;\n        organization.GatewayCustomerId = \"cus_test123\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);\n\n        // Assert\n        await stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationNameAndEmail_WhenNameIsEmpty_LogsWarningAndReturns(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        organization.Name = \"\";\n        organization.GatewayCustomerId = \"cus_test123\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);\n\n        // Assert\n        await stripeAdapter.DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateOrganizationNameAndEmail_WhenBillingEmailIsNull_UpdatesWithNull(\n        Organization organization,\n        SutProvider<OrganizationBillingService> sutProvider)\n    {\n        // Arrange\n        organization.Name = \"Test Organization\";\n        organization.BillingEmail = null;\n        organization.GatewayCustomerId = \"cus_test123\";\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        // Act\n        await sutProvider.Sut.UpdateOrganizationNameAndEmail(organization);\n\n        // Assert\n        await stripeAdapter.Received(1).UpdateCustomerAsync(\n            organization.GatewayCustomerId,\n            Arg.Is<CustomerUpdateOptions>(options =>\n                options.Email == null &&\n                options.Description == organization.Name));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Services.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.BitStripe;\nusing Bit.Core.Repositories;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Services;\n\npublic class PaymentHistoryServiceTests\n{\n    [Fact]\n    public async Task GetInvoiceHistoryAsync_Succeeds()\n    {\n        // Arrange\n        var subscriber = new Organization { GatewayCustomerId = \"cus_id\", GatewaySubscriptionId = \"sub_id\" };\n        var invoices = new List<Invoice> { new() { Id = \"in_id\" } };\n        var stripeAdapter = Substitute.For<IStripeAdapter>();\n        stripeAdapter.ListInvoicesAsync(Arg.Any<StripeInvoiceListOptions>()).Returns(invoices);\n        var transactionRepository = Substitute.For<ITransactionRepository>();\n        var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository);\n\n        // Act\n        var result = await paymentHistoryService.GetInvoiceHistoryAsync(subscriber);\n\n        // Assert\n        Assert.NotEmpty(result);\n        Assert.Single(result);\n        await stripeAdapter.Received(1).ListInvoicesAsync(Arg.Any<StripeInvoiceListOptions>());\n    }\n\n    [Fact]\n    public async Task GetInvoiceHistoryAsync_SubscriberNull_ReturnsNull()\n    {\n        // Arrange\n        var paymentHistoryService = new PaymentHistoryService(\n            Substitute.For<IStripeAdapter>(),\n            Substitute.For<ITransactionRepository>());\n\n        // Act\n        var result = await paymentHistoryService.GetInvoiceHistoryAsync(null);\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task GetTransactionHistoryAsync_Succeeds()\n    {\n        // Arrange\n        var subscriber = new Organization { Id = Guid.NewGuid() };\n        var transactions = new List<Transaction> { new() { Id = Guid.NewGuid() } };\n        var transactionRepository = Substitute.For<ITransactionRepository>();\n        transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id, Arg.Any<int>(), Arg.Any<DateTime?>()).Returns(transactions);\n        var stripeAdapter = Substitute.For<IStripeAdapter>();\n        var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository);\n\n        // Act\n        var result = await paymentHistoryService.GetTransactionHistoryAsync(subscriber);\n\n        // Assert\n        Assert.NotEmpty(result);\n        Assert.Single(result);\n        await transactionRepository.Received(1).GetManyByOrganizationIdAsync(subscriber.Id, Arg.Any<int>(), Arg.Any<DateTime?>());\n    }\n\n    [Fact]\n    public async Task GetTransactionHistoryAsync_SubscriberNull_ReturnsNull()\n    {\n        // Arrange\n        var paymentHistoryService = new PaymentHistoryService(\n            Substitute.For<IStripeAdapter>(),\n            Substitute.For<ITransactionRepository>());\n\n        // Act\n        var result = await paymentHistoryService.GetTransactionHistoryAsync(null);\n\n        // Assert\n        Assert.Empty(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Services/StripePaymentServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Services.Implementations;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.Billing.Mocks.Plans;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\nusing static StripeConstants;\n\n[SutProviderCustomize]\npublic class StripePaymentServiceTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithCustomerDiscount_ReturnsDiscountFromCustomer(\n        SutProvider<StripePaymentService> sutProvider,\n        User subscriber)\n    {\n        // Arrange\n        subscriber.Gateway = GatewayType.Stripe;\n        subscriber.GatewayCustomerId = \"cus_test123\";\n        subscriber.GatewaySubscriptionId = \"sub_test123\";\n\n        var customerDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,\n                PercentOff = 20m,\n                AmountOff = 1400\n            },\n            End = null\n        };\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\",\n            Customer = new Customer\n            {\n                Discount = customerDiscount\n            },\n            Discounts = new List<Discount>(), // Empty list\n            Items = new StripeList<SubscriptionItem> { Data = [] }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(\n                subscriber.GatewaySubscriptionId,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);\n\n        // Assert\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);\n        Assert.Equal(20m, result.CustomerDiscount.PercentOff);\n        Assert.Equal(14.00m, result.CustomerDiscount.AmountOff); // Converted from cents\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithoutCustomerDiscount_FallsBackToSubscriptionDiscounts(\n        SutProvider<StripePaymentService> sutProvider,\n        User subscriber)\n    {\n        // Arrange\n        subscriber.Gateway = GatewayType.Stripe;\n        subscriber.GatewayCustomerId = \"cus_test123\";\n        subscriber.GatewaySubscriptionId = \"sub_test123\";\n\n        var subscriptionDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,\n                PercentOff = 15m,\n                AmountOff = null\n            },\n            End = null\n        };\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\",\n            Customer = new Customer\n            {\n                Discount = null // No customer discount\n            },\n            Discounts = new List<Discount> { subscriptionDiscount },\n            Items = new StripeList<SubscriptionItem> { Data = [] }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(\n                subscriber.GatewaySubscriptionId,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);\n\n        // Assert - Should use subscription discount as fallback\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);\n        Assert.Equal(15m, result.CustomerDiscount.PercentOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithBothDiscounts_PrefersCustomerDiscount(\n        SutProvider<StripePaymentService> sutProvider,\n        User subscriber)\n    {\n        // Arrange\n        subscriber.Gateway = GatewayType.Stripe;\n        subscriber.GatewayCustomerId = \"cus_test123\";\n        subscriber.GatewaySubscriptionId = \"sub_test123\";\n\n        var customerDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = StripeConstants.CouponIDs.Milestone2SubscriptionDiscount,\n                PercentOff = 25m\n            },\n            End = null\n        };\n\n        var subscriptionDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = \"different-coupon-id\",\n                PercentOff = 10m\n            },\n            End = null\n        };\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\",\n            Customer = new Customer\n            {\n                Discount = customerDiscount // Should prefer this\n            },\n            Discounts = new List<Discount> { subscriptionDiscount },\n            Items = new StripeList<SubscriptionItem> { Data = [] }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(\n                subscriber.GatewaySubscriptionId,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);\n\n        // Assert - Should prefer customer discount over subscription discount\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(StripeConstants.CouponIDs.Milestone2SubscriptionDiscount, result.CustomerDiscount.Id);\n        Assert.Equal(25m, result.CustomerDiscount.PercentOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithNoDiscounts_ReturnsNullDiscount(\n        SutProvider<StripePaymentService> sutProvider,\n        User subscriber)\n    {\n        // Arrange\n        subscriber.Gateway = GatewayType.Stripe;\n        subscriber.GatewayCustomerId = \"cus_test123\";\n        subscriber.GatewaySubscriptionId = \"sub_test123\";\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\",\n            Customer = new Customer\n            {\n                Discount = null\n            },\n            Discounts = new List<Discount>(), // Empty list, no discounts\n            Items = new StripeList<SubscriptionItem> { Data = [] }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(\n                subscriber.GatewaySubscriptionId,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);\n\n        // Assert\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithMultipleSubscriptionDiscounts_SelectsFirstDiscount(\n        SutProvider<StripePaymentService> sutProvider,\n        User subscriber)\n    {\n        // Arrange - Multiple subscription-level discounts, no customer discount\n        subscriber.Gateway = GatewayType.Stripe;\n        subscriber.GatewayCustomerId = \"cus_test123\";\n        subscriber.GatewaySubscriptionId = \"sub_test123\";\n\n        var firstDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = \"coupon-10-percent\",\n                PercentOff = 10m\n            },\n            End = null\n        };\n\n        var secondDiscount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = \"coupon-20-percent\",\n                PercentOff = 20m\n            },\n            End = null\n        };\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\",\n            Customer = new Customer\n            {\n                Discount = null // No customer discount\n            },\n            // Multiple subscription discounts - FirstOrDefault() should select the first one\n            Discounts = new List<Discount> { firstDiscount, secondDiscount },\n            Items = new StripeList<SubscriptionItem> { Data = [] }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(\n                subscriber.GatewaySubscriptionId,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);\n\n        // Assert - Should select the first discount from the list (FirstOrDefault() behavior)\n        Assert.NotNull(result.CustomerDiscount);\n        Assert.Equal(\"coupon-10-percent\", result.CustomerDiscount.Id);\n        Assert.Equal(10m, result.CustomerDiscount.PercentOff);\n        // Verify the second discount was not selected\n        Assert.NotEqual(\"coupon-20-percent\", result.CustomerDiscount.Id);\n        Assert.NotEqual(20m, result.CustomerDiscount.PercentOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithNullCustomer_HandlesGracefully(\n        SutProvider<StripePaymentService> sutProvider,\n        User subscriber)\n    {\n        // Arrange - Subscription with null Customer (defensive null check scenario)\n        subscriber.Gateway = GatewayType.Stripe;\n        subscriber.GatewayCustomerId = \"cus_test123\";\n        subscriber.GatewaySubscriptionId = \"sub_test123\";\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\",\n            Customer = null, // Customer not expanded or null\n            Discounts = new List<Discount>(), // Empty discounts\n            Items = new StripeList<SubscriptionItem> { Data = [] }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(\n                subscriber.GatewaySubscriptionId,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);\n\n        // Assert - Should handle null Customer gracefully without throwing NullReferenceException\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithNullDiscounts_HandlesGracefully(\n        SutProvider<StripePaymentService> sutProvider,\n        User subscriber)\n    {\n        // Arrange - Subscription with null Discounts (defensive null check scenario)\n        subscriber.Gateway = GatewayType.Stripe;\n        subscriber.GatewayCustomerId = \"cus_test123\";\n        subscriber.GatewaySubscriptionId = \"sub_test123\";\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\",\n            Customer = new Customer\n            {\n                Discount = null // No customer discount\n            },\n            Discounts = null, // Discounts not expanded or null\n            Items = new StripeList<SubscriptionItem> { Data = [] }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(\n                subscriber.GatewaySubscriptionId,\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);\n\n        // Assert - Should handle null Discounts gracefully without throwing NullReferenceException\n        Assert.Null(result.CustomerDiscount);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_VerifiesCorrectExpandOptions(\n        SutProvider<StripePaymentService> sutProvider,\n        User subscriber)\n    {\n        // Arrange\n        subscriber.Gateway = GatewayType.Stripe;\n        subscriber.GatewayCustomerId = \"cus_test123\";\n        subscriber.GatewaySubscriptionId = \"sub_test123\";\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = \"active\",\n            CollectionMethod = \"charge_automatically\",\n            Customer = new Customer { Discount = null },\n            Discounts = new List<Discount>(), // Empty list\n            Items = new StripeList<SubscriptionItem> { Data = [] }\n        };\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        stripeAdapter\n            .GetSubscriptionAsync(\n                Arg.Any<string>(),\n                Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        // Act\n        await sutProvider.Sut.GetSubscriptionAsync(subscriber);\n\n        // Assert - Verify expand options are correct\n        await stripeAdapter.Received(1).GetSubscriptionAsync(\n            subscriber.GatewaySubscriptionId,\n            Arg.Is<SubscriptionGetOptions>(o =>\n                o.Expand.Contains(\"customer.discount.coupon.applies_to\") &&\n                o.Expand.Contains(\"discounts.coupon.applies_to\") &&\n                o.Expand.Contains(\"test_clock\")));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetSubscriptionAsync_WithEmptyGatewaySubscriptionId_ReturnsEmptySubscriptionInfo(\n        SutProvider<StripePaymentService> sutProvider,\n        User subscriber)\n    {\n        // Arrange\n        subscriber.GatewaySubscriptionId = null;\n\n        // Act\n        var result = await sutProvider.Sut.GetSubscriptionAsync(subscriber);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Null(result.Subscription);\n        Assert.Null(result.CustomerDiscount);\n        Assert.Null(result.UpcomingInvoice);\n\n        // Verify no Stripe API calls were made\n        await sutProvider.GetDependency<IStripeAdapter>()\n            .DidNotReceive()\n            .GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());\n    }\n\n    #region AdjustSubscription — CompleteSubscriptionUpdate tax exempt alignment\n\n    [Theory, BitAutoData]\n    public async Task AdjustSubscription_WhenNonDirectTaxCountry_SetsReverseCharge(\n        SutProvider<StripePaymentService> sutProvider,\n        Organization organization)\n    {\n        var plan = new EnterprisePlan(isAnnual: true);\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.Seats = 0;\n        organization.UseSecretsManager = false;\n        organization.MaxStorageGb = null;\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Status = \"active\",\n            Customer = new Customer\n            {\n                Id = \"cus_123\",\n                Address = new Address { Country = \"DE\" },\n                TaxExempt = TaxExempt.None\n            },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Plan = new Stripe.Plan { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Quantity = 0\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.EnterpriseAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(new Subscription { Id = \"sub_123\", LatestInvoiceId = \"inv_123\" });\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetInvoiceAsync(\"inv_123\", Arg.Any<InvoiceGetOptions>())\n            .Returns(new Invoice { Id = \"inv_123\", AmountDue = 0, Status = InvoiceStatus.Paid });\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(\"cus_123\")\n            .Returns(new Customer { Id = \"cus_123\" });\n\n        await sutProvider.Sut.AdjustSubscription(organization, plan, 0, false, null, null, 0);\n\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateCustomerAsync(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(o => o.TaxExempt == TaxExempt.Reverse));\n    }\n\n    [Theory, BitAutoData]\n    public async Task AdjustSubscription_WhenUSWithManualReverse_CorrectsTaxExemptToNone(\n        SutProvider<StripePaymentService> sutProvider,\n        Organization organization)\n    {\n        var plan = new EnterprisePlan(isAnnual: true);\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.Seats = 0;\n        organization.UseSecretsManager = false;\n        organization.MaxStorageGb = null;\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Status = \"active\",\n            Customer = new Customer\n            {\n                Id = \"cus_123\",\n                Address = new Address { Country = \"US\" },\n                TaxExempt = TaxExempt.Reverse\n            },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Plan = new Stripe.Plan { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Quantity = 0\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.EnterpriseAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(new Subscription { Id = \"sub_123\", LatestInvoiceId = \"inv_123\" });\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetInvoiceAsync(\"inv_123\", Arg.Any<InvoiceGetOptions>())\n            .Returns(new Invoice { Id = \"inv_123\", AmountDue = 0, Status = InvoiceStatus.Paid });\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(\"cus_123\")\n            .Returns(new Customer { Id = \"cus_123\" });\n\n        await sutProvider.Sut.AdjustSubscription(organization, plan, 0, false, null, null, 0);\n\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateCustomerAsync(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(o => o.TaxExempt == TaxExempt.None));\n    }\n\n    [Theory, BitAutoData]\n    public async Task AdjustSubscription_WhenSwissWithReverse_CorrectsTaxExemptToNone(\n        SutProvider<StripePaymentService> sutProvider,\n        Organization organization)\n    {\n        // CH is a direct-tax country — \"reverse\" is not preserved; it should be corrected to \"none\".\n        var plan = new EnterprisePlan(isAnnual: true);\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.Seats = 0;\n        organization.UseSecretsManager = false;\n        organization.MaxStorageGb = null;\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Status = \"active\",\n            Customer = new Customer\n            {\n                Id = \"cus_123\",\n                Address = new Address { Country = \"CH\" },\n                TaxExempt = TaxExempt.Reverse\n            },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Plan = new Stripe.Plan { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Quantity = 0\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.EnterpriseAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(new Subscription { Id = \"sub_123\", LatestInvoiceId = \"inv_123\" });\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetInvoiceAsync(\"inv_123\", Arg.Any<InvoiceGetOptions>())\n            .Returns(new Invoice { Id = \"inv_123\", AmountDue = 0, Status = InvoiceStatus.Paid });\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(\"cus_123\")\n            .Returns(new Customer { Id = \"cus_123\" });\n\n        await sutProvider.Sut.AdjustSubscription(organization, plan, 0, false, null, null, 0);\n\n        await sutProvider.GetDependency<IStripeAdapter>().Received(1).UpdateCustomerAsync(\n            \"cus_123\",\n            Arg.Is<CustomerUpdateOptions>(options => options.TaxExempt == TaxExempt.None));\n    }\n\n    [Theory, BitAutoData]\n    public async Task AdjustSubscription_WhenCustomerIsExempt_DoesNotUpdateTaxExemption(\n        SutProvider<StripePaymentService> sutProvider,\n        Organization organization)\n    {\n        var plan = new EnterprisePlan(isAnnual: true);\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        organization.GatewaySubscriptionId = \"sub_123\";\n        organization.Seats = 0;\n        organization.UseSecretsManager = false;\n        organization.MaxStorageGb = null;\n\n        var subscription = new Subscription\n        {\n            Id = \"sub_123\",\n            Status = \"active\",\n            Customer = new Customer\n            {\n                Id = \"cus_123\",\n                Address = new Address { Country = \"DE\" },\n                TaxExempt = TaxExempt.Exempt\n            },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem\n                    {\n                        Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Plan = new Stripe.Plan { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Quantity = 0\n                    }\n                ]\n            }\n        };\n\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(PlanType.EnterpriseAnnually)\n            .Returns(plan);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())\n            .Returns(new Subscription { Id = \"sub_123\", LatestInvoiceId = \"inv_123\" });\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetInvoiceAsync(\"inv_123\", Arg.Any<InvoiceGetOptions>())\n            .Returns(new Invoice { Id = \"inv_123\", AmountDue = 0, Status = InvoiceStatus.Paid });\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(\"cus_123\")\n            .Returns(new Customer { Id = \"cus_123\" });\n\n        await sutProvider.Sut.AdjustSubscription(organization, plan, 0, false, null, null, 0);\n\n        await sutProvider.GetDependency<IStripeAdapter>().DidNotReceive().UpdateCustomerAsync(\n            Arg.Any<string>(),\n            Arg.Any<CustomerUpdateOptions>());\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Services/SubscriberServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Services.Implementations;\nusing Bit.Core.Billing.Tax.Models;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Braintree;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing NSubstitute.ReturnsExtensions;\nusing Stripe;\nusing Xunit;\n\nusing static Bit.Core.Test.Billing.Utilities;\nusing Address = Stripe.Address;\nusing Customer = Stripe.Customer;\nusing PaymentMethod = Stripe.PaymentMethod;\nusing Subscription = Stripe.Subscription;\n\nnamespace Bit.Core.Test.Billing.Services;\n\n[SutProviderCustomize]\npublic class SubscriberServiceTests\n{\n    #region CancelSubscription\n\n    [Theory, BitAutoData]\n    public async Task CancelSubscription_SubscriptionInactive_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var subscription = new Subscription\n        {\n            Status = \"canceled\"\n        };\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        stripeAdapter\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .Returns(subscription);\n\n        await ThrowsBillingExceptionAsync(() =>\n            sutProvider.Sut.CancelSubscription(organization, new OffboardingSurveyResponse(), false));\n\n        await stripeAdapter\n            .DidNotReceiveWithAnyArgs()\n            .UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n\n        await stripeAdapter\n            .DidNotReceiveWithAnyArgs()\n            .CancelSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task CancelSubscription_CancelImmediately_BelongsToOrganization_UpdatesSubscription_CancelSubscriptionImmediately(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var userId = Guid.NewGuid();\n\n        const string subscriptionId = \"subscription_id\";\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = \"active\",\n            Metadata = new Dictionary<string, string>\n            {\n                { \"organizationId\", \"organization_id\" }\n            }\n        };\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        stripeAdapter\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .Returns(subscription);\n\n        var offboardingSurveyResponse = new OffboardingSurveyResponse\n        {\n            UserId = userId,\n            Reason = \"missing_features\",\n            Feedback = \"Lorem ipsum\"\n        };\n\n        await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, true);\n\n        await stripeAdapter\n            .Received(1)\n            .UpdateSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(\n                options => options.Metadata[\"cancellingUserId\"] == userId.ToString()));\n\n        await stripeAdapter\n            .Received(1)\n            .CancelSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>\n                options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&\n                options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CancelSubscription_CancelImmediately_BelongsToUser_CancelSubscriptionImmediately(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var userId = Guid.NewGuid();\n\n        const string subscriptionId = \"subscription_id\";\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = \"active\",\n            Metadata = new Dictionary<string, string>\n            {\n                { \"userId\", \"user_id\" }\n            }\n        };\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        stripeAdapter\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .Returns(subscription);\n\n        var offboardingSurveyResponse = new OffboardingSurveyResponse\n        {\n            UserId = userId,\n            Reason = \"missing_features\",\n            Feedback = \"Lorem ipsum\"\n        };\n\n        await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, true);\n\n        await stripeAdapter\n            .DidNotReceiveWithAnyArgs()\n            .UpdateSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());\n\n        await stripeAdapter\n            .Received(1)\n            .CancelSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionCancelOptions>(options =>\n                options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&\n                options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CancelSubscription_DoNotCancelImmediately_UpdateSubscriptionToCancelAtEndOfPeriod(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var userId = Guid.NewGuid();\n\n        const string subscriptionId = \"subscription_id\";\n\n        organization.ExpirationDate = DateTime.UtcNow.AddDays(5);\n\n        var subscription = new Subscription\n        {\n            Id = subscriptionId,\n            Status = \"active\"\n        };\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        stripeAdapter\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .Returns(subscription);\n\n        var offboardingSurveyResponse = new OffboardingSurveyResponse\n        {\n            UserId = userId,\n            Reason = \"missing_features\",\n            Feedback = \"Lorem ipsum\"\n        };\n\n        await sutProvider.Sut.CancelSubscription(organization, offboardingSurveyResponse, false);\n\n        await stripeAdapter\n            .Received(1)\n            .UpdateSubscriptionAsync(subscriptionId, Arg.Is<SubscriptionUpdateOptions>(options =>\n                options.CancelAtPeriodEnd == true &&\n                options.CancellationDetails.Comment == offboardingSurveyResponse.Feedback &&\n                options.CancellationDetails.Feedback == offboardingSurveyResponse.Reason &&\n                options.Metadata[\"cancellingUserId\"] == userId.ToString()));\n\n        await stripeAdapter\n            .DidNotReceiveWithAnyArgs()\n            .CancelSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());\n    }\n\n    #endregion\n\n    #region GetCustomer\n\n    [Theory, BitAutoData]\n    public async Task GetCustomer_NullSubscriber_ThrowsArgumentNullException(\n        SutProvider<SubscriberService> sutProvider)\n        => await Assert.ThrowsAsync<ArgumentNullException>(\n            async () => await sutProvider.Sut.GetCustomer(null));\n\n    [Theory, BitAutoData]\n    public async Task GetCustomer_NoGatewayCustomerId_ReturnsNull(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        organization.GatewayCustomerId = null;\n\n        var customer = await sutProvider.Sut.GetCustomer(organization);\n\n        Assert.Null(customer);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetCustomer_NoCustomer_ReturnsNull(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId)\n            .ReturnsNull();\n\n        var customer = await sutProvider.Sut.GetCustomer(organization);\n\n        Assert.Null(customer);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetCustomer_StripeException_ReturnsNull(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId)\n            .ThrowsAsync<StripeException>();\n\n        var customer = await sutProvider.Sut.GetCustomer(organization);\n\n        Assert.Null(customer);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetCustomer_Succeeds(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var customer = new Customer();\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(customer);\n\n        var gotCustomer = await sutProvider.Sut.GetCustomer(organization);\n\n        Assert.Equivalent(customer, gotCustomer);\n    }\n\n    #endregion\n\n    #region GetCustomerOrThrow\n\n    [Theory, BitAutoData]\n    public async Task GetCustomerOrThrow_NullSubscriber_ThrowsArgumentNullException(\n        SutProvider<SubscriberService> sutProvider)\n        => await Assert.ThrowsAsync<ArgumentNullException>(\n            async () => await sutProvider.Sut.GetCustomerOrThrow(null));\n\n    [Theory, BitAutoData]\n    public async Task GetCustomerOrThrow_NoGatewayCustomerId_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        organization.GatewayCustomerId = null;\n\n        await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetCustomerOrThrow_NoCustomer_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId)\n            .ReturnsNull();\n\n        await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetCustomerOrThrow(organization));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetCustomerOrThrow_StripeException_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var stripeException = new StripeException();\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId)\n            .ThrowsAsync(stripeException);\n\n        await ThrowsBillingExceptionAsync(\n            async () => await sutProvider.Sut.GetCustomerOrThrow(organization),\n            message: \"An error occurred while trying to retrieve a Stripe customer\",\n            innerException: stripeException);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetCustomerOrThrow_Succeeds(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var customer = new Customer();\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId)\n            .Returns(customer);\n\n        var gotCustomer = await sutProvider.Sut.GetCustomerOrThrow(organization);\n\n        Assert.Equivalent(customer, gotCustomer);\n    }\n\n    #endregion\n\n    #region GetPaymentSource\n\n    [Theory, BitAutoData]\n    public async Task GetPaymentSource_NullSubscriber_ThrowsArgumentNullException(\n        SutProvider<SubscriberService> sutProvider) =>\n        await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));\n\n    [Theory, BitAutoData]\n    public async Task GetPaymentSource_Braintree_NoDefaultPaymentMethod_ReturnsNull(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        const string braintreeCustomerId = \"braintree_customer_id\";\n\n        var customer = new Customer\n        {\n            Id = provider.GatewayCustomerId,\n            Metadata = new Dictionary<string, string>\n            {\n                [Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,\n                Arg.Is<CustomerGetOptions>(\n                    options => options.Expand.Contains(\"default_source\") &&\n                               options.Expand.Contains(\"invoice_settings.default_payment_method\")))\n            .Returns(customer);\n\n        var (_, customerGateway, _) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());\n\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n\n        braintreeCustomer.Id.Returns(braintreeCustomerId);\n\n        braintreeCustomer.PaymentMethods.Returns([]);\n\n        customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);\n\n        var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);\n\n        Assert.Null(paymentMethod);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPaymentSource_Braintree_PayPalAccount_Succeeds(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        const string braintreeCustomerId = \"braintree_customer_id\";\n\n        var customer = new Customer\n        {\n            Id = provider.GatewayCustomerId,\n            Metadata = new Dictionary<string, string>\n            {\n                [Core.Billing.Utilities.BraintreeCustomerIdKey] = braintreeCustomerId\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,\n                Arg.Is<CustomerGetOptions>(\n                    options => options.Expand.Contains(\"default_source\") &&\n                               options.Expand.Contains(\"invoice_settings.default_payment_method\")))\n            .Returns(customer);\n\n        var (_, customerGateway, _) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());\n\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n\n        braintreeCustomer.Id.Returns(braintreeCustomerId);\n\n        var payPalAccount = Substitute.For<PayPalAccount>();\n\n        payPalAccount.IsDefault.Returns(true);\n\n        payPalAccount.Email.Returns(\"a@example.com\");\n\n        braintreeCustomer.PaymentMethods.Returns([payPalAccount]);\n\n        customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);\n\n        var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);\n\n        Assert.Equal(PaymentMethodType.PayPal, paymentMethod.Type);\n        Assert.Equal(\"a@example.com\", paymentMethod.Description);\n        Assert.False(paymentMethod.NeedsVerification);\n    }\n\n    // TODO: Determine if we need to test Braintree.CreditCard\n\n    // TODO: Determine if we need to test Braintree.UsBankAccount\n\n    [Theory, BitAutoData]\n    public async Task GetPaymentSource_Stripe_BankAccountPaymentMethod_Succeeds(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var customer = new Customer\n        {\n            InvoiceSettings = new CustomerInvoiceSettings\n            {\n                DefaultPaymentMethod = new PaymentMethod\n                {\n                    Type = StripeConstants.PaymentMethodTypes.USBankAccount,\n                    UsBankAccount = new PaymentMethodUsBankAccount\n                    {\n                        BankName = \"Chase\",\n                        Last4 = \"9999\"\n                    }\n                }\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,\n                Arg.Is<CustomerGetOptions>(\n                    options => options.Expand.Contains(\"default_source\") &&\n                               options.Expand.Contains(\"invoice_settings.default_payment_method\")))\n            .Returns(customer);\n\n        var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);\n\n        Assert.Equal(PaymentMethodType.BankAccount, paymentMethod.Type);\n        Assert.Equal(\"Chase, *9999\", paymentMethod.Description);\n        Assert.False(paymentMethod.NeedsVerification);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPaymentSource_Stripe_CardPaymentMethod_Succeeds(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var customer = new Customer\n        {\n            InvoiceSettings = new CustomerInvoiceSettings\n            {\n                DefaultPaymentMethod = new PaymentMethod\n                {\n                    Type = StripeConstants.PaymentMethodTypes.Card,\n                    Card = new PaymentMethodCard\n                    {\n                        Brand = \"Visa\",\n                        Last4 = \"9999\",\n                        ExpMonth = 9,\n                        ExpYear = 2028\n                    }\n                }\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,\n                Arg.Is<CustomerGetOptions>(\n                    options => options.Expand.Contains(\"default_source\") &&\n                               options.Expand.Contains(\"invoice_settings.default_payment_method\")))\n            .Returns(customer);\n\n        var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);\n\n        Assert.Equal(PaymentMethodType.Card, paymentMethod.Type);\n        Assert.Equal(\"VISA, *9999, 09/2028\", paymentMethod.Description);\n        Assert.False(paymentMethod.NeedsVerification);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPaymentSource_Stripe_SetupIntentForBankAccount_Succeeds(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var customer = new Customer { Id = provider.GatewayCustomerId };\n\n        sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,\n                Arg.Is<CustomerGetOptions>(options => options.Expand.Contains(\"default_source\") &&\n                                                      options.Expand.Contains(\n                                                          \"invoice_settings.default_payment_method\")))\n            .Returns(customer);\n\n        var setupIntent = new SetupIntent\n        {\n            Id = \"setup_intent_id\",\n            Status = \"requires_action\",\n            NextAction =\n                new SetupIntentNextAction\n                {\n                    VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()\n                },\n            PaymentMethod = new PaymentMethod\n            {\n                UsBankAccount = new PaymentMethodUsBankAccount { BankName = \"Chase\", Last4 = \"9999\" }\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>().ListSetupIntentsAsync(\n            Arg.Is<SetupIntentListOptions>(options =>\n                options.Customer == customer.Id &&\n                options.Expand.Contains(\"data.payment_method\")))\n            .Returns([setupIntent]);\n\n        var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);\n\n        Assert.Equal(PaymentMethodType.BankAccount, paymentMethod.Type);\n        Assert.Equal(\"Chase, *9999\", paymentMethod.Description);\n        Assert.True(paymentMethod.NeedsVerification);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPaymentSource_Stripe_LegacyBankAccount_Succeeds(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var customer = new Customer\n        {\n            DefaultSource = new BankAccount { Status = \"verified\", BankName = \"Chase\", Last4 = \"9999\" }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,\n                Arg.Is<CustomerGetOptions>(options => options.Expand.Contains(\"default_source\") &&\n                                                      options.Expand.Contains(\n                                                          \"invoice_settings.default_payment_method\")))\n            .Returns(customer);\n\n        var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);\n\n        Assert.Equal(PaymentMethodType.BankAccount, paymentMethod.Type);\n        Assert.Equal(\"Chase, *9999 - Verified\", paymentMethod.Description);\n        Assert.False(paymentMethod.NeedsVerification);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPaymentSource_Stripe_LegacyCard_Succeeds(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var customer = new Customer\n        {\n            DefaultSource = new Card { Brand = \"Visa\", Last4 = \"9999\", ExpMonth = 9, ExpYear = 2028 }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,\n                Arg.Is<CustomerGetOptions>(options => options.Expand.Contains(\"default_source\") &&\n                                                      options.Expand.Contains(\n                                                          \"invoice_settings.default_payment_method\")))\n            .Returns(customer);\n\n        var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);\n\n        Assert.Equal(PaymentMethodType.Card, paymentMethod.Type);\n        Assert.Equal(\"VISA, *9999, 09/2028\", paymentMethod.Description);\n        Assert.False(paymentMethod.NeedsVerification);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetPaymentSource_Stripe_LegacySourceCard_Succeeds(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var customer = new Customer\n        {\n            DefaultSource = new Source\n            {\n                Card = new SourceCard\n                {\n                    Brand = \"Visa\",\n                    Last4 = \"9999\",\n                    ExpMonth = 9,\n                    ExpYear = 2028\n                }\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>().GetCustomerAsync(provider.GatewayCustomerId,\n                Arg.Is<CustomerGetOptions>(\n                    options => options.Expand.Contains(\"default_source\") &&\n                               options.Expand.Contains(\"invoice_settings.default_payment_method\")))\n            .Returns(customer);\n\n        var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);\n\n        Assert.Equal(PaymentMethodType.Card, paymentMethod.Type);\n        Assert.Equal(\"VISA, *9999, 09/2028\", paymentMethod.Description);\n        Assert.False(paymentMethod.NeedsVerification);\n    }\n\n    #endregion\n\n    #region GetSubscription\n    [Theory, BitAutoData]\n    public async Task GetSubscription_NullSubscriber_ThrowsArgumentNullException(\n        SutProvider<SubscriberService> sutProvider)\n        => await Assert.ThrowsAsync<ArgumentNullException>(\n            async () => await sutProvider.Sut.GetSubscription(null));\n\n    [Theory, BitAutoData]\n    public async Task GetSubscription_NoGatewaySubscriptionId_ReturnsNull(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        organization.GatewaySubscriptionId = null;\n\n        var subscription = await sutProvider.Sut.GetSubscription(organization);\n\n        Assert.Null(subscription);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscription_NoSubscription_ReturnsNull(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .ReturnsNull();\n\n        var subscription = await sutProvider.Sut.GetSubscription(organization);\n\n        Assert.Null(subscription);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscription_StripeException_ReturnsNull(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .ThrowsAsync<StripeException>();\n\n        var subscription = await sutProvider.Sut.GetSubscription(organization);\n\n        Assert.Null(subscription);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscription_Succeeds(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var subscription = new Subscription();\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .Returns(subscription);\n\n        var gotSubscription = await sutProvider.Sut.GetSubscription(organization);\n\n        Assert.Equivalent(subscription, gotSubscription);\n    }\n    #endregion\n\n    #region GetSubscriptionOrThrow\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionOrThrow_NullSubscriber_ThrowsArgumentNullException(\n        SutProvider<SubscriberService> sutProvider)\n        => await Assert.ThrowsAsync<ArgumentNullException>(\n            async () => await sutProvider.Sut.GetSubscriptionOrThrow(null));\n\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionOrThrow_NoGatewaySubscriptionId_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        organization.GatewaySubscriptionId = null;\n\n        await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionOrThrow_NoSubscription_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .ReturnsNull();\n\n        await ThrowsBillingExceptionAsync(async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization));\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionOrThrow_StripeException_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var stripeException = new StripeException();\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .ThrowsAsync(stripeException);\n\n        await ThrowsBillingExceptionAsync(\n            async () => await sutProvider.Sut.GetSubscriptionOrThrow(organization),\n            message: \"An error occurred while trying to retrieve a Stripe subscription\",\n            innerException: stripeException);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetSubscriptionOrThrow_Succeeds(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var subscription = new Subscription();\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetSubscriptionAsync(organization.GatewaySubscriptionId)\n            .Returns(subscription);\n\n        var gotSubscription = await sutProvider.Sut.GetSubscriptionOrThrow(organization);\n\n        Assert.Equivalent(subscription, gotSubscription);\n    }\n    #endregion\n\n    #region RemovePaymentMethod\n    [Theory, BitAutoData]\n    public async Task RemovePaymentMethod_NullSubscriber_ThrowsArgumentNullException(\n        SutProvider<SubscriberService> sutProvider) =>\n        await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.RemovePaymentSource(null));\n\n    [Theory, BitAutoData]\n    public async Task RemovePaymentMethod_Braintree_NoCustomer_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        const string braintreeCustomerId = \"1\";\n\n        var stripeCustomer = new Customer\n        {\n            Metadata = new Dictionary<string, string>\n            {\n                { \"btCustomerId\", braintreeCustomerId }\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(stripeCustomer);\n\n        var (braintreeGateway, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());\n\n        customerGateway.FindAsync(braintreeCustomerId).ReturnsNull();\n\n        braintreeGateway.Customer.Returns(customerGateway);\n\n        await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentSource(organization));\n\n        await customerGateway.Received(1).FindAsync(braintreeCustomerId);\n\n        await customerGateway.DidNotReceiveWithAnyArgs()\n            .UpdateAsync(Arg.Any<string>(), Arg.Any<CustomerRequest>());\n\n        await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemovePaymentMethod_Braintree_NoPaymentMethod_NoOp(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        const string braintreeCustomerId = \"1\";\n\n        var stripeCustomer = new Customer\n        {\n            Metadata = new Dictionary<string, string>\n            {\n                { \"btCustomerId\", braintreeCustomerId }\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(stripeCustomer);\n\n        var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());\n\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n\n        braintreeCustomer.PaymentMethods.Returns([]);\n\n        customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);\n\n        await sutProvider.Sut.RemovePaymentSource(organization);\n\n        await customerGateway.Received(1).FindAsync(braintreeCustomerId);\n\n        await customerGateway.DidNotReceiveWithAnyArgs().UpdateAsync(Arg.Any<string>(), Arg.Any<CustomerRequest>());\n\n        await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemovePaymentMethod_Braintree_CustomerUpdateFails_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        const string braintreeCustomerId = \"1\";\n        const string braintreePaymentMethodToken = \"TOKEN\";\n\n        var stripeCustomer = new Customer\n        {\n            Metadata = new Dictionary<string, string>\n            {\n                { \"btCustomerId\", braintreeCustomerId }\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(stripeCustomer);\n\n        var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());\n\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n\n        var paymentMethod = Substitute.For<Braintree.PaymentMethod>();\n        paymentMethod.Token.Returns(braintreePaymentMethodToken);\n        paymentMethod.IsDefault.Returns(true);\n\n        braintreeCustomer.PaymentMethods.Returns([\n            paymentMethod\n        ]);\n\n        customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);\n\n        var updateBraintreeCustomerResult = Substitute.For<Result<Braintree.Customer>>();\n        updateBraintreeCustomerResult.IsSuccess().Returns(false);\n\n        customerGateway.UpdateAsync(\n                braintreeCustomerId,\n                Arg.Is<CustomerRequest>(request => request.DefaultPaymentMethodToken == null))\n            .Returns(updateBraintreeCustomerResult);\n\n        await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentSource(organization));\n\n        await customerGateway.Received(1).FindAsync(braintreeCustomerId);\n\n        await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>\n            request.DefaultPaymentMethodToken == null));\n\n        await paymentMethodGateway.DidNotReceiveWithAnyArgs().DeleteAsync(paymentMethod.Token);\n\n        await customerGateway.DidNotReceive().UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>\n            request.DefaultPaymentMethodToken == paymentMethod.Token));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemovePaymentMethod_Braintree_PaymentMethodDeleteFails_RollBack_ThrowsBillingException(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        const string braintreeCustomerId = \"1\";\n        const string braintreePaymentMethodToken = \"TOKEN\";\n\n        var stripeCustomer = new Customer\n        {\n            Metadata = new Dictionary<string, string>\n            {\n                { \"btCustomerId\", braintreeCustomerId }\n            }\n        };\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(stripeCustomer);\n\n        var (_, customerGateway, paymentMethodGateway) = SetupBraintree(sutProvider.GetDependency<IBraintreeGateway>());\n\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n\n        var paymentMethod = Substitute.For<Braintree.PaymentMethod>();\n        paymentMethod.Token.Returns(braintreePaymentMethodToken);\n        paymentMethod.IsDefault.Returns(true);\n\n        braintreeCustomer.PaymentMethods.Returns([\n            paymentMethod\n        ]);\n\n        customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);\n\n        var updateBraintreeCustomerResult = Substitute.For<Result<Braintree.Customer>>();\n        updateBraintreeCustomerResult.IsSuccess().Returns(true);\n\n        customerGateway.UpdateAsync(braintreeCustomerId, Arg.Any<CustomerRequest>())\n            .Returns(updateBraintreeCustomerResult);\n\n        var deleteBraintreePaymentMethodResult = Substitute.For<Result<Braintree.PaymentMethod>>();\n        deleteBraintreePaymentMethodResult.IsSuccess().Returns(false);\n\n        paymentMethodGateway.DeleteAsync(paymentMethod.Token).Returns(deleteBraintreePaymentMethodResult);\n\n        await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentSource(organization));\n\n        await customerGateway.Received(1).FindAsync(braintreeCustomerId);\n\n        await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>\n            request.DefaultPaymentMethodToken == null));\n\n        await paymentMethodGateway.Received(1).DeleteAsync(paymentMethod.Token);\n\n        await customerGateway.Received(1).UpdateAsync(braintreeCustomerId, Arg.Is<CustomerRequest>(request =>\n            request.DefaultPaymentMethodToken == paymentMethod.Token));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemovePaymentMethod_Stripe_Legacy_RemovesSources(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        const string bankAccountId = \"bank_account_id\";\n        const string cardId = \"card_id\";\n\n        var sources = new List<IPaymentSource>\n        {\n            new BankAccount { Id = bankAccountId }, new Card { Id = cardId }\n        };\n\n        var stripeCustomer = new Customer { Sources = new StripeList<IPaymentSource> { Data = sources } };\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        stripeAdapter\n            .GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(stripeCustomer);\n\n        stripeAdapter\n            .ListPaymentMethodsAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())\n            .Returns(GetPaymentMethodsAsync(new List<PaymentMethod>()));\n\n        await sutProvider.Sut.RemovePaymentSource(organization);\n\n        await stripeAdapter.Received(1).DeleteBankAccountAsync(stripeCustomer.Id, bankAccountId);\n\n        await stripeAdapter.Received(1).DeleteCardAsync(stripeCustomer.Id, cardId);\n\n        await stripeAdapter.DidNotReceiveWithAnyArgs()\n            .DetachPaymentMethodAsync(Arg.Any<string>(), Arg.Any<PaymentMethodDetachOptions>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RemovePaymentMethod_Stripe_DetachesPaymentMethods(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        const string bankAccountId = \"bank_account_id\";\n        const string cardId = \"card_id\";\n\n        var sources = new List<IPaymentSource>();\n\n        var stripeCustomer = new Customer { Sources = new StripeList<IPaymentSource> { Data = sources } };\n\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        stripeAdapter\n            .GetCustomerAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())\n            .Returns(stripeCustomer);\n\n        stripeAdapter\n            .ListPaymentMethodsAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())\n            .Returns(GetPaymentMethodsAsync(new List<PaymentMethod>\n            {\n                new ()\n                {\n                    Id = bankAccountId\n                },\n                new ()\n                {\n                    Id = cardId\n                }\n            }));\n\n        await sutProvider.Sut.RemovePaymentSource(organization);\n\n        await stripeAdapter.DidNotReceiveWithAnyArgs().DeleteBankAccountAsync(Arg.Any<string>(), Arg.Any<string>());\n\n        await stripeAdapter.DidNotReceiveWithAnyArgs().DeleteCardAsync(Arg.Any<string>(), Arg.Any<string>());\n\n        await stripeAdapter.Received(1)\n            .DetachPaymentMethodAsync(bankAccountId);\n\n        await stripeAdapter.Received(1)\n            .DetachPaymentMethodAsync(cardId);\n    }\n\n    private static async IAsyncEnumerable<PaymentMethod> GetPaymentMethodsAsync(\n        IEnumerable<PaymentMethod> paymentMethods)\n    {\n        foreach (var paymentMethod in paymentMethods)\n        {\n            yield return paymentMethod;\n        }\n\n        await Task.CompletedTask;\n    }\n\n    private static (IBraintreeGateway, ICustomerGateway, IPaymentMethodGateway) SetupBraintree(\n        IBraintreeGateway braintreeGateway)\n    {\n        var customerGateway = Substitute.For<ICustomerGateway>();\n        var paymentMethodGateway = Substitute.For<IPaymentMethodGateway>();\n\n        braintreeGateway.Customer.Returns(customerGateway);\n        braintreeGateway.PaymentMethod.Returns(paymentMethodGateway);\n\n        return (braintreeGateway, customerGateway, paymentMethodGateway);\n    }\n    #endregion\n\n    #region UpdateTaxInformation\n\n    [Theory, BitAutoData]\n    public async Task UpdateTaxInformation_NullSubscriber_ThrowsArgumentNullException(\n        SutProvider<SubscriberService> sutProvider) =>\n        await Assert.ThrowsAsync<ArgumentNullException>(\n        () => sutProvider.Sut.UpdateTaxInformation(null, null));\n\n    [Theory, BitAutoData]\n    public async Task UpdateTaxInformation_NullTaxInformation_ThrowsArgumentNullException(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider) =>\n        await Assert.ThrowsAsync<ArgumentNullException>(\n            () => sutProvider.Sut.UpdateTaxInformation(provider, null));\n\n    [Theory, BitAutoData]\n    public async Task UpdateTaxInformation_NonUser_MakesCorrectInvocations(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = \"tax_id_1\", Type = \"us_ein\" }] } };\n\n        stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerGetOptions>(\n            options => options.Expand.Contains(\"tax_ids\"))).Returns(customer);\n\n        var taxInformation = new TaxInformation(\n            \"US\",\n            \"12345\",\n            \"123456789\",\n            \"us_ein\",\n            \"123 Example St.\",\n            null,\n            \"Example Town\",\n            \"NY\");\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateCustomerAsync(\n                Arg.Is<string>(p => p == provider.GatewayCustomerId),\n                Arg.Is<CustomerUpdateOptions>(options =>\n                    options.Address.Country == \"US\" &&\n                    options.Address.PostalCode == \"12345\" &&\n                    options.Address.Line1 == \"123 Example St.\" &&\n                    options.Address.Line2 == null &&\n                    options.Address.City == \"Example Town\" &&\n                    options.Address.State == \"NY\"))\n            .Returns(new Customer\n            {\n                Id = provider.GatewayCustomerId,\n                Address = new Address\n                {\n                    Country = \"US\",\n                    PostalCode = \"12345\",\n                    Line1 = \"123 Example St.\",\n                    Line2 = null,\n                    City = \"Example Town\",\n                    State = \"NY\"\n                },\n                TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = \"tax_id_1\", Type = \"us_ein\" }] },\n                Subscriptions = new StripeList<Subscription>\n                {\n                    Data = [\n                        new Subscription\n                        {\n                            Id = provider.GatewaySubscriptionId,\n                            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                        }\n                    ]\n                }\n            });\n\n        var subscription = new Subscription { Items = new StripeList<SubscriptionItem>() };\n        sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(Arg.Any<string>())\n            .Returns(subscription);\n\n        await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation);\n\n        await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(\n            options =>\n                options.Address.Country == taxInformation.Country &&\n                options.Address.PostalCode == taxInformation.PostalCode &&\n                options.Address.Line1 == taxInformation.Line1 &&\n                options.Address.Line2 == taxInformation.Line2 &&\n                options.Address.City == taxInformation.City &&\n                options.Address.State == taxInformation.State));\n\n        await stripeAdapter.Received(1).DeleteTaxIdAsync(provider.GatewayCustomerId, \"tax_id_1\");\n\n        await stripeAdapter.Received(1).CreateTaxIdAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(\n            options => options.Type == \"us_ein\" &&\n                       options.Value == taxInformation.TaxId));\n\n        await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateTaxInformation_NonUser_ReverseCharge_MakesCorrectInvocations(\n        Provider provider,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n\n        var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = \"tax_id_1\", Type = \"us_ein\" }] } };\n\n        stripeAdapter.GetCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerGetOptions>(\n            options => options.Expand.Contains(\"tax_ids\"))).Returns(customer);\n\n        var taxInformation = new TaxInformation(\n            \"CA\",\n            \"12345\",\n            \"123456789\",\n            \"us_ein\",\n            \"123 Example St.\",\n            null,\n            \"Example Town\",\n            \"NY\");\n\n        sutProvider.GetDependency<IStripeAdapter>()\n            .UpdateCustomerAsync(\n                Arg.Is<string>(p => p == provider.GatewayCustomerId),\n                Arg.Is<CustomerUpdateOptions>(options =>\n                    options.Address.Country == \"CA\" &&\n                    options.Address.PostalCode == \"12345\" &&\n                    options.Address.Line1 == \"123 Example St.\" &&\n                    options.Address.Line2 == null &&\n                    options.Address.City == \"Example Town\" &&\n                    options.Address.State == \"NY\"))\n            .Returns(new Customer\n            {\n                Id = provider.GatewayCustomerId,\n                Address = new Address\n                {\n                    Country = \"CA\",\n                    PostalCode = \"12345\",\n                    Line1 = \"123 Example St.\",\n                    Line2 = null,\n                    City = \"Example Town\",\n                    State = \"NY\"\n                },\n                TaxIds = new StripeList<TaxId> { Data = [new TaxId { Id = \"tax_id_1\", Type = \"us_ein\" }] },\n                Subscriptions = new StripeList<Subscription>\n                {\n                    Data = [\n                        new Subscription\n                        {\n                            Id = provider.GatewaySubscriptionId,\n                            CustomerId = provider.GatewayCustomerId,\n                            AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }\n                        }\n                    ]\n                }\n            });\n\n        var subscription = new Subscription { Items = new StripeList<SubscriptionItem>() };\n        sutProvider.GetDependency<IStripeAdapter>().GetSubscriptionAsync(Arg.Any<string>())\n            .Returns(subscription);\n\n        await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation);\n\n        await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(\n            options =>\n                options.Address.Country == taxInformation.Country &&\n                options.Address.PostalCode == taxInformation.PostalCode &&\n                options.Address.Line1 == taxInformation.Line1 &&\n                options.Address.Line2 == taxInformation.Line2 &&\n                options.Address.City == taxInformation.City &&\n                options.Address.State == taxInformation.State));\n\n        await stripeAdapter.Received(1).DeleteTaxIdAsync(provider.GatewayCustomerId, \"tax_id_1\");\n\n        await stripeAdapter.Received(1).CreateTaxIdAsync(provider.GatewayCustomerId, Arg.Is<TaxIdCreateOptions>(\n            options => options.Type == \"us_ein\" &&\n                       options.Value == taxInformation.TaxId));\n\n        await stripeAdapter.Received(1).UpdateCustomerAsync(provider.GatewayCustomerId,\n            Arg.Is<CustomerUpdateOptions>(options => options.TaxExempt == StripeConstants.TaxExempt.Reverse));\n\n        await stripeAdapter.Received(1).UpdateSubscriptionAsync(provider.GatewaySubscriptionId,\n            Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));\n    }\n\n    #endregion\n\n    #region IsValidGatewayCustomerIdAsync\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewayCustomerIdAsync_NullSubscriber_ThrowsArgumentNullException(\n        SutProvider<SubscriberService> sutProvider)\n    {\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            sutProvider.Sut.IsValidGatewayCustomerIdAsync(null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewayCustomerIdAsync_NullGatewayCustomerId_ReturnsTrue(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        organization.GatewayCustomerId = null;\n\n        var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);\n\n        Assert.True(result);\n        await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()\n            .GetCustomerAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewayCustomerIdAsync_EmptyGatewayCustomerId_ReturnsTrue(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        organization.GatewayCustomerId = \"\";\n\n        var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);\n\n        Assert.True(result);\n        await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()\n            .GetCustomerAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewayCustomerIdAsync_ValidCustomerId_ReturnsTrue(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId).Returns(new Customer());\n\n        var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);\n\n        Assert.True(result);\n        await stripeAdapter.Received(1).GetCustomerAsync(organization.GatewayCustomerId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewayCustomerIdAsync_InvalidCustomerId_ReturnsFalse(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var stripeException = new StripeException { StripeError = new StripeError { Code = \"resource_missing\" } };\n        stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId).Throws(stripeException);\n\n        var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization);\n\n        Assert.False(result);\n        await stripeAdapter.Received(1).GetCustomerAsync(organization.GatewayCustomerId);\n    }\n\n    #endregion\n\n    #region IsValidGatewaySubscriptionIdAsync\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewaySubscriptionIdAsync_NullSubscriber_ThrowsArgumentNullException(\n        SutProvider<SubscriberService> sutProvider)\n    {\n        await Assert.ThrowsAsync<ArgumentNullException>(() =>\n            sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(null));\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewaySubscriptionIdAsync_NullGatewaySubscriptionId_ReturnsTrue(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        organization.GatewaySubscriptionId = null;\n\n        var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);\n\n        Assert.True(result);\n        await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()\n            .GetSubscriptionAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewaySubscriptionIdAsync_EmptyGatewaySubscriptionId_ReturnsTrue(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        organization.GatewaySubscriptionId = \"\";\n\n        var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);\n\n        Assert.True(result);\n        await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()\n            .GetSubscriptionAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewaySubscriptionIdAsync_ValidSubscriptionId_ReturnsTrue(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Returns(new Subscription());\n\n        var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);\n\n        Assert.True(result);\n        await stripeAdapter.Received(1).GetSubscriptionAsync(organization.GatewaySubscriptionId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsValidGatewaySubscriptionIdAsync_InvalidSubscriptionId_ReturnsFalse(\n        Organization organization,\n        SutProvider<SubscriberService> sutProvider)\n    {\n        var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();\n        var stripeException = new StripeException { StripeError = new StripeError { Code = \"resource_missing\" } };\n        stripeAdapter.GetSubscriptionAsync(organization.GatewaySubscriptionId).Throws(stripeException);\n\n        var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization);\n\n        Assert.False(result);\n        await stripeAdapter.Received(1).GetSubscriptionAsync(organization.GatewaySubscriptionId);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Services/SubscriptionDiscountServiceTests.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Services.DiscountAudienceFilters;\nusing Bit.Core.Billing.Services.Implementations;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Bit.Core.Billing.Subscriptions.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Services;\n\n[SutProviderCustomize]\npublic class SubscriptionDiscountServiceTests\n{\n    private static IDictionary<DiscountTierType, bool> DiscountDictionary(bool eligibilitySetting)\n        => Enum.GetValues<DiscountTierType>().ToDictionary(t => t, _ => eligibilitySetting);\n\n    [Theory, BitAutoData]\n    public async Task GetEligibleDiscountsAsync_NoActiveDiscounts_ReturnsEmpty(\n        User user,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([]);\n\n        // Act\n        var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user);\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetEligibleDiscountsAsync_AllUsersDiscount_ReturnsDiscount(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange\n        discount.AudienceType = DiscountAudienceType.AllUsers;\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([discount]);\n\n        var filter = Substitute.For<IDiscountAudienceFilter>();\n        filter.IsUserEligible(user, discount).Returns(DiscountDictionary(true));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.AllUsers)\n            .Returns(filter);\n\n        // Act\n        var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user);\n\n        // Assert\n        Assert.Contains(result, e => e.Discount == discount);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetEligibleDiscountsAsync_UserIsEligibleForDiscount_ReturnsDiscount(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange\n        discount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions;\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([discount]);\n\n        var filter = Substitute.For<IDiscountAudienceFilter>();\n        filter.IsUserEligible(user, discount).Returns(DiscountDictionary(true));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions)\n            .Returns(filter);\n\n        // Act\n        var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user);\n\n        // Assert\n        Assert.Contains(result, e => e.Discount == discount);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetEligibleDiscountsAsync_UserIsIneligibleForDiscount_ReturnsEmpty(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange\n        discount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions;\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([discount]);\n\n        var filter = Substitute.For<IDiscountAudienceFilter>();\n        filter.IsUserEligible(user, discount).Returns(DiscountDictionary(false));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions)\n            .Returns(filter);\n\n        // Act\n        var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user);\n\n        // Assert\n        Assert.DoesNotContain(result, e => e.Discount == discount);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetEligibleDiscountsAsync_NoFilterForAudienceType_ReturnsEmpty(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange\n        discount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions;\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([discount]);\n\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions)\n            .ReturnsNull();\n\n        // Act\n        var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user);\n\n        // Assert\n        Assert.DoesNotContain(result, e => e.Discount == discount);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetEligibleDiscountsAsync_MixedDiscounts_ReturnsOnlyEligible(\n        User user,\n        SubscriptionDiscount allUsersDiscount,\n        SubscriptionDiscount eligibleDiscount,\n        SubscriptionDiscount ineligibleDiscount,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange\n        allUsersDiscount.AudienceType = DiscountAudienceType.AllUsers;\n        eligibleDiscount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions;\n        ineligibleDiscount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions;\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([allUsersDiscount, eligibleDiscount, ineligibleDiscount]);\n\n        var allUsersFilter = Substitute.For<IDiscountAudienceFilter>();\n        allUsersFilter.IsUserEligible(user, allUsersDiscount).Returns(DiscountDictionary(true));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.AllUsers)\n            .Returns(allUsersFilter);\n\n        var filter = Substitute.For<IDiscountAudienceFilter>();\n        filter.IsUserEligible(user, eligibleDiscount).Returns(DiscountDictionary(true));\n        filter.IsUserEligible(user, ineligibleDiscount).Returns(DiscountDictionary(false));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions)\n            .Returns(filter);\n\n        // Act\n        var result = await sutProvider.Sut.GetEligibleDiscountsAsync(user);\n\n        // Assert\n        Assert.Contains(result, e => e.Discount == allUsersDiscount);\n        Assert.Contains(result, e => e.Discount == eligibleDiscount);\n        Assert.DoesNotContain(result, e => e.Discount == ineligibleDiscount);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateDiscountEligibilityForUserAsync_CouponNotInEligibleDiscounts_ReturnsFalse(\n        User user,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange — no active discounts, so the requested coupon won't be found\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([]);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync(user, [\"invalid\"], DiscountTierType.Premium);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateDiscountEligibilityForUserAsync_CouponFound_TierEligible_ReturnsTrue(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange\n        discount.AudienceType = DiscountAudienceType.AllUsers;\n        discount.StartDate = DateTime.UtcNow.AddDays(-1);\n        discount.EndDate = DateTime.UtcNow.AddDays(30);\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([discount]);\n\n        var filter = Substitute.For<IDiscountAudienceFilter>();\n        filter.IsUserEligible(user, discount).Returns(DiscountDictionary(true));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.AllUsers)\n            .Returns(filter);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync(\n            user, [discount.StripeCouponId], DiscountTierType.Premium);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateDiscountEligibilityForUserAsync_CouponFound_TierNotEligible_ReturnsFalse(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange — discount exists and is active but user is not eligible for this audience type\n        discount.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions;\n        discount.StartDate = DateTime.UtcNow.AddDays(-1);\n        discount.EndDate = DateTime.UtcNow.AddDays(30);\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([discount]);\n\n        var filter = Substitute.For<IDiscountAudienceFilter>();\n        filter.IsUserEligible(user, discount).Returns(DiscountDictionary(false));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions)\n            .Returns(filter);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync(\n            user, [discount.StripeCouponId], DiscountTierType.Families);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateDiscountEligibilityForUserAsync_InactiveDiscount_ReturnsFalse(\n        User user,\n        SubscriptionDiscount discount,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange — expired discount is not returned by GetActiveDiscountsAsync, so won't appear in eligible set\n        discount.StartDate = DateTime.UtcNow.AddDays(-30);\n        discount.EndDate = DateTime.UtcNow.AddDays(-1);\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([]);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync(\n            user, [discount.StripeCouponId], DiscountTierType.Premium);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateDiscountEligibilityForUserAsync_MultipleCoupons_AllEligible_ReturnsTrue(\n        User user,\n        SubscriptionDiscount discount1,\n        SubscriptionDiscount discount2,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange\n        discount1.AudienceType = DiscountAudienceType.AllUsers;\n        discount1.StartDate = DateTime.UtcNow.AddDays(-1);\n        discount1.EndDate = DateTime.UtcNow.AddDays(30);\n        discount2.AudienceType = DiscountAudienceType.AllUsers;\n        discount2.StartDate = DateTime.UtcNow.AddDays(-1);\n        discount2.EndDate = DateTime.UtcNow.AddDays(30);\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([discount1, discount2]);\n\n        var filter = Substitute.For<IDiscountAudienceFilter>();\n        filter.IsUserEligible(user, discount1).Returns(DiscountDictionary(true));\n        filter.IsUserEligible(user, discount2).Returns(DiscountDictionary(true));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.AllUsers)\n            .Returns(filter);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync(\n            user, [discount1.StripeCouponId, discount2.StripeCouponId], DiscountTierType.Premium);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateDiscountEligibilityForUserAsync_MultipleCoupons_OneNotEligible_ReturnsFalse(\n        User user,\n        SubscriptionDiscount discount1,\n        SubscriptionDiscount discount2,\n        SutProvider<SubscriptionDiscountService> sutProvider)\n    {\n        // Arrange — discount1 is eligible, discount2 is not\n        discount1.AudienceType = DiscountAudienceType.AllUsers;\n        discount1.StartDate = DateTime.UtcNow.AddDays(-1);\n        discount1.EndDate = DateTime.UtcNow.AddDays(30);\n        discount2.AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions;\n        discount2.StartDate = DateTime.UtcNow.AddDays(-1);\n        discount2.EndDate = DateTime.UtcNow.AddDays(30);\n\n        sutProvider.GetDependency<ISubscriptionDiscountRepository>()\n            .GetActiveDiscountsAsync()\n            .Returns([discount1, discount2]);\n\n        var allUsersFilter = Substitute.For<IDiscountAudienceFilter>();\n        allUsersFilter.IsUserEligible(user, discount1).Returns(DiscountDictionary(true));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.AllUsers)\n            .Returns(allUsersFilter);\n\n        var restrictedFilter = Substitute.For<IDiscountAudienceFilter>();\n        restrictedFilter.IsUserEligible(user, discount2).Returns(DiscountDictionary(false));\n        sutProvider.GetDependency<IDiscountAudienceFilterFactory>()\n            .GetFilter(DiscountAudienceType.UserHasNoPreviousSubscriptions)\n            .Returns(restrictedFilter);\n\n        // Act\n        var result = await sutProvider.Sut.ValidateDiscountEligibilityForUserAsync(\n            user, [discount1.StripeCouponId, discount2.StripeCouponId], DiscountTierType.Premium);\n\n        // Assert\n        Assert.False(result);\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Subscriptions/Entities/SubscriptionDiscountTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Subscriptions.Entities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Subscriptions.Entities;\n\npublic class SubscriptionDiscountTests\n{\n    [Fact]\n    public void StripeProductIds_CanSerializeToJson()\n    {\n        // Arrange\n        var discount = new SubscriptionDiscount\n        {\n            StripeCouponId = \"test-coupon\",\n            StripeProductIds = new List<string> { \"prod_123\", \"prod_456\" },\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow,\n            EndDate = DateTime.UtcNow.AddDays(30),\n            AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions\n        };\n\n        // Act\n        var json = JsonSerializer.Serialize(discount.StripeProductIds);\n\n        // Assert\n        Assert.Equal(\"[\\\"prod_123\\\",\\\"prod_456\\\"]\", json);\n    }\n\n    [Fact]\n    public void StripeProductIds_CanDeserializeFromJson()\n    {\n        // Arrange\n        var json = \"[\\\"prod_123\\\",\\\"prod_456\\\"]\";\n\n        // Act\n        var result = JsonSerializer.Deserialize<List<string>>(json);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(2, result.Count);\n        Assert.Contains(\"prod_123\", result);\n        Assert.Contains(\"prod_456\", result);\n    }\n\n    [Fact]\n    public void StripeProductIds_HandlesNull()\n    {\n        // Arrange\n        var discount = new SubscriptionDiscount\n        {\n            StripeCouponId = \"test-coupon\",\n            StripeProductIds = null,\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow,\n            EndDate = DateTime.UtcNow.AddDays(30),\n            AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions\n        };\n\n        // Act\n        var json = JsonSerializer.Serialize(discount.StripeProductIds);\n\n        // Assert\n        Assert.Equal(\"null\", json);\n    }\n\n    [Fact]\n    public void StripeProductIds_HandlesEmptyCollection()\n    {\n        // Arrange\n        var discount = new SubscriptionDiscount\n        {\n            StripeCouponId = \"test-coupon\",\n            StripeProductIds = new List<string>(),\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow,\n            EndDate = DateTime.UtcNow.AddDays(30),\n            AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions\n        };\n\n        // Act\n        var json = JsonSerializer.Serialize(discount.StripeProductIds);\n\n        // Assert\n        Assert.Equal(\"[]\", json);\n    }\n\n    [Fact]\n    public void Validate_RejectsEndDateBeforeStartDate()\n    {\n        // Arrange\n        var discount = new SubscriptionDiscount\n        {\n            StripeCouponId = \"test-coupon\",\n            Duration = \"once\",\n            StartDate = DateTime.UtcNow.AddDays(30),\n            EndDate = DateTime.UtcNow, // EndDate before StartDate\n            AudienceType = DiscountAudienceType.UserHasNoPreviousSubscriptions\n        };\n\n        // Act\n        var validationResults = discount.Validate(new System.ComponentModel.DataAnnotations.ValidationContext(discount)).ToList();\n\n        // Assert\n        Assert.Single(validationResults);\n        Assert.Contains(\"EndDate\", validationResults[0].MemberNames);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Subscriptions/Queries/GetBitwardenSubscriptionQueryTests.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Models;\nusing Bit.Core.Billing.Subscriptions.Queries;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Subscriptions.Queries;\n\nusing static StripeConstants;\n\npublic class GetBitwardenSubscriptionQueryTests\n{\n    private readonly ILogger<GetBitwardenSubscriptionQuery> _logger = Substitute.For<ILogger<GetBitwardenSubscriptionQuery>>();\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly GetBitwardenSubscriptionQuery _query;\n\n    public GetBitwardenSubscriptionQueryTests()\n    {\n        _query = new GetBitwardenSubscriptionQuery(\n            _logger,\n            _pricingClient,\n            _stripeAdapter);\n    }\n\n    [Fact]\n    public async Task Run_UserWithoutGatewaySubscriptionId_ReturnsNull()\n    {\n        var user = CreateUser();\n        user.GatewaySubscriptionId = null;\n\n        var result = await _query.Run(user);\n\n        Assert.Null(result);\n        await _stripeAdapter.DidNotReceive().GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task Run_UserWithEmptyGatewaySubscriptionId_ReturnsNull()\n    {\n        var user = CreateUser();\n        user.GatewaySubscriptionId = string.Empty;\n\n        var result = await _query.Run(user);\n\n        Assert.Null(result);\n        await _stripeAdapter.DidNotReceive().GetSubscriptionAsync(Arg.Any<string>(), Arg.Any<SubscriptionGetOptions>());\n    }\n\n    [Fact]\n    public async Task Run_StripeSubscriptionNotFound_ReturnsNull()\n    {\n        var user = CreateUser();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .ThrowsAsync(new StripeException { StripeError = new StripeError { Code = ErrorCodes.ResourceMissing } });\n\n        var result = await _query.Run(user);\n\n        Assert.Null(result);\n    }\n\n    [Fact]\n    public async Task Run_StripeExceptionNotResourceMissing_Throws()\n    {\n        var user = CreateUser();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .ThrowsAsync(new StripeException { StripeError = new StripeError { Code = \"api_error\" } });\n\n        await Assert.ThrowsAsync<StripeException>(() => _query.Run(user));\n    }\n\n    [Fact]\n    public async Task Run_IncompleteStatus_ReturnsBitwardenSubscriptionWithSuspension()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Incomplete);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(SubscriptionStatus.Incomplete, result.Status);\n        Assert.NotNull(result.Suspension);\n        Assert.Equal(subscription.Created.AddHours(23), result.Suspension);\n        Assert.Equal(1, result.GracePeriod);\n        Assert.Null(result.NextCharge);\n        Assert.Null(result.CancelAt);\n    }\n\n    [Fact]\n    public async Task Run_IncompleteExpiredStatus_ReturnsBitwardenSubscriptionWithSuspension()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.IncompleteExpired);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(SubscriptionStatus.IncompleteExpired, result.Status);\n        Assert.NotNull(result.Suspension);\n        Assert.Equal(subscription.Created.AddHours(23), result.Suspension);\n        Assert.Equal(1, result.GracePeriod);\n    }\n\n    [Fact]\n    public async Task Run_TrialingStatus_ReturnsBitwardenSubscriptionWithNextCharge()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Trialing);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(SubscriptionStatus.Trialing, result.Status);\n        Assert.NotNull(result.NextCharge);\n        Assert.Equal(subscription.Items.First().CurrentPeriodEnd, result.NextCharge);\n        Assert.Null(result.Suspension);\n        Assert.Null(result.GracePeriod);\n    }\n\n    [Fact]\n    public async Task Run_ActiveStatus_ReturnsBitwardenSubscriptionWithNextCharge()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(SubscriptionStatus.Active, result.Status);\n        Assert.NotNull(result.NextCharge);\n        Assert.Equal(subscription.Items.First().CurrentPeriodEnd, result.NextCharge);\n        Assert.Null(result.Suspension);\n        Assert.Null(result.GracePeriod);\n    }\n\n    [Fact]\n    public async Task Run_ActiveStatusWithCancelAt_ReturnsCancelAt()\n    {\n        var user = CreateUser();\n        var cancelAt = DateTime.UtcNow.AddMonths(1);\n        var subscription = CreateSubscription(SubscriptionStatus.Active, cancelAt: cancelAt);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(SubscriptionStatus.Active, result.Status);\n        Assert.Equal(cancelAt, result.CancelAt);\n    }\n\n    [Fact]\n    public async Task Run_PastDueStatus_WithOpenInvoices_ReturnsSuspension()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.PastDue, collectionMethod: \"charge_automatically\");\n        var premiumPlans = CreatePremiumPlans();\n        var openInvoice = CreateInvoice();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n        _stripeAdapter.SearchInvoiceAsync(Arg.Any<InvoiceSearchOptions>())\n            .Returns([openInvoice]);\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(SubscriptionStatus.PastDue, result.Status);\n        Assert.NotNull(result.Suspension);\n        Assert.Equal(openInvoice.Created.AddDays(14), result.Suspension);\n        Assert.Equal(14, result.GracePeriod);\n    }\n\n    [Fact]\n    public async Task Run_PastDueStatus_WithoutOpenInvoices_ReturnsNoSuspension()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.PastDue);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n        _stripeAdapter.SearchInvoiceAsync(Arg.Any<InvoiceSearchOptions>())\n            .Returns([]);\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(SubscriptionStatus.PastDue, result.Status);\n        Assert.Null(result.Suspension);\n        Assert.Null(result.GracePeriod);\n    }\n\n    [Fact]\n    public async Task Run_UnpaidStatus_WithOpenInvoices_ReturnsSuspension()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Unpaid, collectionMethod: \"charge_automatically\");\n        var premiumPlans = CreatePremiumPlans();\n        var openInvoice = CreateInvoice();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n        _stripeAdapter.SearchInvoiceAsync(Arg.Any<InvoiceSearchOptions>())\n            .Returns([openInvoice]);\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(SubscriptionStatus.Unpaid, result.Status);\n        Assert.NotNull(result.Suspension);\n        Assert.Equal(14, result.GracePeriod);\n    }\n\n    [Fact]\n    public async Task Run_CanceledStatus_ReturnsCanceledDate()\n    {\n        var user = CreateUser();\n        var canceledAt = DateTime.UtcNow.AddDays(-5);\n        var subscription = CreateSubscription(SubscriptionStatus.Canceled, canceledAt: canceledAt);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(SubscriptionStatus.Canceled, result.Status);\n        Assert.Equal(canceledAt, result.Canceled);\n        Assert.Null(result.Suspension);\n        Assert.Null(result.NextCharge);\n    }\n\n    [Fact]\n    public async Task Run_UnmanagedStatus_ThrowsConflictException()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(\"unmanaged_status\");\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        await Assert.ThrowsAsync<ConflictException>(() => _query.Run(user));\n    }\n\n    [Fact]\n    public async Task Run_WithAdditionalStorage_IncludesStorageInCart()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active, includeStorage: true);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.NotNull(result.Cart.PasswordManager.AdditionalStorage);\n        Assert.Equal(\"additionalStorageGB\", result.Cart.PasswordManager.AdditionalStorage.TranslationKey);\n        Assert.Equal(2, result.Cart.PasswordManager.AdditionalStorage.Quantity);\n        Assert.NotNull(result.Storage);\n    }\n\n    [Fact]\n    public async Task Run_WithoutAdditionalStorage_ExcludesStorageFromCart()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active, includeStorage: false);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Null(result.Cart.PasswordManager.AdditionalStorage);\n        Assert.NotNull(result.Storage);\n    }\n\n    [Fact]\n    public async Task Run_WithCartLevelDiscount_IncludesDiscountInCart()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active);\n        subscription.Customer.Discount = CreateDiscount(discountType: \"cart\");\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.NotNull(result.Cart.Discount);\n        Assert.Equal(BitwardenDiscountType.PercentOff, result.Cart.Discount.Type);\n        Assert.Equal(20, result.Cart.Discount.Value);\n    }\n\n    [Fact]\n    public async Task Run_WithProductLevelDiscount_IncludesDiscountInCartItem()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active);\n        var productDiscount = CreateDiscount(discountType: \"product\", productId: \"prod_premium_seat\");\n        subscription.Discounts = [productDiscount];\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.NotNull(result.Cart.PasswordManager.Seats.Discount);\n        Assert.Equal(BitwardenDiscountType.PercentOff, result.Cart.PasswordManager.Seats.Discount.Type);\n    }\n\n    [Fact]\n    public async Task Run_WithoutMaxStorageGb_ReturnsNullStorage()\n    {\n        var user = CreateUser();\n        user.MaxStorageGb = null;\n        var subscription = CreateSubscription(SubscriptionStatus.Active);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Null(result.Storage);\n    }\n\n    [Fact]\n    public async Task Run_CalculatesStorageCorrectly()\n    {\n        var user = CreateUser();\n        user.Storage = 5368709120; // 5 GB in bytes\n        user.MaxStorageGb = 10;\n        var subscription = CreateSubscription(SubscriptionStatus.Active, includeStorage: true);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.NotNull(result.Storage);\n        Assert.Equal(10, result.Storage.Available);\n        Assert.Equal(5.0, result.Storage.Used);\n        Assert.NotEmpty(result.Storage.ReadableUsed);\n    }\n\n    [Fact]\n    public async Task Run_TaxEstimation_WithInvoiceUpcomingNoneError_ReturnsZeroTax()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .ThrowsAsync(new StripeException { StripeError = new StripeError { Code = ErrorCodes.InvoiceUpcomingNone } });\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(0, result.Cart.EstimatedTax);\n    }\n\n    [Fact]\n    public async Task Run_MissingPasswordManagerSeatsItem_ThrowsConflictException()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active);\n        subscription.Items = new StripeList<SubscriptionItem>\n        {\n            Data = []\n        };\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n\n        await Assert.ThrowsAsync<ConflictException>(() => _query.Run(user));\n    }\n\n    [Fact]\n    public async Task Run_IncludesEstimatedTax()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active);\n        var premiumPlans = CreatePremiumPlans();\n        var invoice = CreateInvoicePreview(totalTax: 500); // $5.00 tax\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(invoice);\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(5.0m, result.Cart.EstimatedTax);\n    }\n\n    [Fact]\n    public async Task Run_SetsCadenceToAnnually()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(PlanCadenceType.Annually, result.Cart.Cadence);\n    }\n\n    [Fact]\n    public async Task Run_UserOnLegacyPricing_ReturnsCostFromPricingService()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active, legacyPricing: true);\n        var premiumPlans = CreatePremiumPlans();\n        var availablePlan = premiumPlans.First(p => p.Available);\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n\n        var previewInvoice = CreateInvoicePreview(totalTax: 150);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(previewInvoice);\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(availablePlan.Seat.Price, result.Cart.PasswordManager.Seats.Cost);\n        Assert.Equal(1.50m, result.Cart.EstimatedTax);\n    }\n\n    [Fact]\n    public async Task Run_UserOnLegacyPricing_CallsPreviewInvoiceWithRebuiltSubscription()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active, legacyPricing: true);\n        var premiumPlans = CreatePremiumPlans();\n        var availablePlan = premiumPlans.First(p => p.Available);\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n\n        var previewInvoice = CreateInvoicePreview();\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(previewInvoice);\n\n        await _query.Run(user);\n\n        await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(\n            Arg.Is<InvoiceCreatePreviewOptions>(opts =>\n                opts.Subscription == null &&\n                opts.AutomaticTax != null &&\n                opts.AutomaticTax.Enabled == true &&\n                opts.SubscriptionDetails != null &&\n                opts.SubscriptionDetails.Items.Any(i =>\n                    i.Price == availablePlan.Seat.StripePriceId &&\n                    i.Quantity == 1)));\n    }\n\n    [Fact]\n    public async Task Run_UserOnCurrentPricing_ReturnsCostFromSubscriptionItem()\n    {\n        var user = CreateUser();\n        var subscription = CreateSubscription(SubscriptionStatus.Active, legacyPricing: false);\n        var premiumPlans = CreatePremiumPlans();\n\n        _stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, Arg.Any<SubscriptionGetOptions>())\n            .Returns(subscription);\n        _pricingClient.ListPremiumPlans().Returns(premiumPlans);\n        _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())\n            .Returns(CreateInvoicePreview());\n\n        var result = await _query.Run(user);\n\n        Assert.NotNull(result);\n        Assert.Equal(19.80m, result.Cart.PasswordManager.Seats.Cost);\n    }\n\n    #region Helper Methods\n\n    private static User CreateUser()\n    {\n        return new User\n        {\n            Id = Guid.NewGuid(),\n            GatewaySubscriptionId = \"sub_test123\",\n            MaxStorageGb = 1,\n            Storage = 1073741824 // 1 GB in bytes\n        };\n    }\n\n    private static Subscription CreateSubscription(\n        string status,\n        bool includeStorage = false,\n        bool legacyPricing = false,\n        DateTime? cancelAt = null,\n        DateTime? canceledAt = null,\n        string collectionMethod = \"charge_automatically\")\n    {\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var seatPriceId = legacyPricing ? \"price_legacy_premium_seat\" : \"price_premium_seat\";\n        var seatUnitAmount = legacyPricing ? 1000 : 1980;\n        var items = new List<SubscriptionItem>\n        {\n            new()\n            {\n                Id = \"si_premium_seat\",\n                Price = new Price\n                {\n                    Id = seatPriceId,\n                    UnitAmountDecimal = seatUnitAmount,\n                    Product = new Product { Id = \"prod_premium_seat\" }\n                },\n                Quantity = 1,\n                CurrentPeriodStart = DateTime.UtcNow,\n                CurrentPeriodEnd = currentPeriodEnd\n            }\n        };\n\n        if (includeStorage)\n        {\n            items.Add(new SubscriptionItem\n            {\n                Id = \"si_storage\",\n                Price = new Price\n                {\n                    Id = \"price_storage\",\n                    UnitAmountDecimal = 400,\n                    Product = new Product { Id = \"prod_storage\" }\n                },\n                Quantity = 2,\n                CurrentPeriodStart = DateTime.UtcNow,\n                CurrentPeriodEnd = currentPeriodEnd\n            });\n        }\n\n        return new Subscription\n        {\n            Id = \"sub_test123\",\n            Status = status,\n            Created = DateTime.UtcNow.AddMonths(-1),\n            AutomaticTax = new SubscriptionAutomaticTax { Enabled = true },\n            Customer = new Customer\n            {\n                Id = \"cus_test123\",\n                Discount = null\n            },\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = items\n            },\n            CancelAt = cancelAt,\n            CanceledAt = canceledAt,\n            CollectionMethod = collectionMethod,\n            Discounts = []\n        };\n    }\n\n    private static List<Bit.Core.Billing.Pricing.Premium.Plan> CreatePremiumPlans()\n    {\n        return\n        [\n            new()\n            {\n                Name = \"Premium\",\n                Available = true,\n                Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n                {\n                    StripePriceId = \"price_premium_seat\",\n                    Price = 19.80m,\n                    Provided = 1\n                },\n                Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n                {\n                    StripePriceId = \"price_storage\",\n                    Price = 4.0m,\n                    Provided = 1\n                }\n            },\n            new()\n            {\n                Name = \"Premium\",\n                Available = false,\n                LegacyYear = 2024,\n                Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable\n                {\n                    StripePriceId = \"price_legacy_premium_seat\",\n                    Price = 10.0m,\n                    Provided = 1\n                },\n                Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable\n                {\n                    StripePriceId = \"price_storage\",\n                    Price = 4.0m,\n                    Provided = 1\n                }\n            }\n        ];\n    }\n\n    private static Invoice CreateInvoice()\n    {\n        return new Invoice\n        {\n            Id = \"in_test123\",\n            Created = DateTime.UtcNow.AddDays(-10),\n            PeriodEnd = DateTime.UtcNow.AddDays(-5),\n            Attempted = true,\n            Status = \"open\"\n        };\n    }\n\n    private static Invoice CreateInvoicePreview(long totalTax = 0)\n    {\n        var taxes = totalTax > 0\n            ? new List<InvoiceTotalTax> { new() { Amount = totalTax } }\n            : new List<InvoiceTotalTax>();\n\n        return new Invoice\n        {\n            Id = \"in_preview\",\n            TotalTaxes = taxes\n        };\n    }\n\n    private static Discount CreateDiscount(string discountType = \"cart\", string? productId = null)\n    {\n        var coupon = new Coupon\n        {\n            Valid = true,\n            PercentOff = 20,\n            AppliesTo = discountType == \"product\" && productId != null\n                ? new CouponAppliesTo { Products = [productId] }\n                : new CouponAppliesTo { Products = [] }\n        };\n\n        return new Discount\n        {\n            Coupon = coupon\n        };\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Billing.Subscriptions.Commands;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.Billing.Mocks;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing.Subscriptions;\n\nusing static StripeConstants;\n\npublic class RestartSubscriptionCommandTests\n{\n    private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();\n    private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();\n    private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();\n    private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();\n    private readonly RestartSubscriptionCommand _command;\n\n    public RestartSubscriptionCommandTests()\n    {\n        _command = new RestartSubscriptionCommand(\n            Substitute.For<Microsoft.Extensions.Logging.ILogger<RestartSubscriptionCommand>>(),\n            _organizationRepository,\n            _pricingClient,\n            _stripeAdapter,\n            _subscriberService);\n    }\n\n    [Fact]\n    public async Task Run_SubscriptionNotCanceled_ReturnsBadRequest()\n    {\n        var organization = new Organization { Id = Guid.NewGuid() };\n\n        var subscription = new Subscription { Status = SubscriptionStatus.Active };\n        _subscriberService.GetSubscription(organization).Returns(subscription);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Cannot restart a subscription that is not canceled.\", badRequest.Response);\n    }\n\n    [Fact]\n    public async Task Run_NoExistingSubscription_ReturnsBadRequest()\n    {\n        var organization = new Organization { Id = Guid.NewGuid() };\n\n        _subscriberService.GetSubscription(organization).Returns((Subscription)null);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT1);\n        var badRequest = result.AsT1;\n        Assert.Equal(\"Cannot restart a subscription that is not canceled.\", badRequest.Response);\n    }\n\n    [Fact]\n    public async Task Run_Provider_ReturnsUnhandledWithNotSupportedException()\n    {\n        var provider = new Provider { Id = Guid.NewGuid() };\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_123\"\n        };\n\n        _subscriberService.GetSubscription(provider).Returns(existingSubscription);\n\n        var result = await _command.Run(provider);\n\n        Assert.True(result.IsT3);\n        var unhandled = result.AsT3;\n        Assert.IsType<NotSupportedException>(unhandled.Exception);\n    }\n\n    [Fact]\n    public async Task Run_User_ReturnsUnhandledWithNotSupportedException()\n    {\n        var user = new User { Id = Guid.NewGuid() };\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_123\"\n        };\n\n        _subscriberService.GetSubscription(user).Returns(existingSubscription);\n\n        var result = await _command.Run(user);\n\n        Assert.True(result.IsT3);\n        var unhandled = result.AsT3;\n        Assert.IsType<NotSupportedException>(unhandled.Exception);\n    }\n\n    [Fact]\n    public async Task Run_Organization_MissingPasswordManagerItem_ReturnsUnhandledWithConflictException()\n    {\n        var organizationId = Guid.NewGuid();\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        var plan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = \"some-other-price-id\" }, Quantity = 10 }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(existingSubscription);\n        _pricingClient.ListPlans().Returns([plan]);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT3);\n        var unhandled = result.AsT3;\n        Assert.IsType<ConflictException>(unhandled.Exception);\n        Assert.Equal(\"Organization's subscription does not have a Password Manager subscription item.\", unhandled.Exception.Message);\n    }\n\n    [Fact]\n    public async Task Run_Organization_PlanNotFound_ReturnsUnhandledWithConflictException()\n    {\n        var organizationId = Guid.NewGuid();\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = \"some-price-id\" }, Quantity = 10 }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(existingSubscription);\n        // Return a plan list that doesn't contain the organization's plan type\n        _pricingClient.ListPlans().Returns([MockPlans.Get(PlanType.TeamsAnnually)]);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT3);\n        var unhandled = result.AsT3;\n        Assert.IsType<ConflictException>(unhandled.Exception);\n        Assert.Equal(\"Could not find plan for organization's plan type\", unhandled.Exception.Message);\n    }\n\n    [Fact]\n    public async Task Run_Organization_DisabledPlanWithNoEnabledReplacement_ReturnsUnhandledWithConflictException()\n    {\n        var organizationId = Guid.NewGuid();\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = PlanType.EnterpriseAnnually2023\n        };\n\n        var oldPlan = new DisabledEnterprisePlan2023(true);\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_old\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(existingSubscription);\n        // Return only the disabled plan, with no enabled replacement\n        _pricingClient.ListPlans().Returns([oldPlan]);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT3);\n        var unhandled = result.AsT3;\n        Assert.IsType<ConflictException>(unhandled.Exception);\n        Assert.Equal(\"Could not find the current, enabled plan for organization's tier and cadence\", unhandled.Exception.Message);\n    }\n\n    [Fact]\n    public async Task Run_Organization_WithNonDisabledPlan_PasswordManagerOnly_Success()\n    {\n        var organizationId = Guid.NewGuid();\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = PlanType.EnterpriseAnnually\n        };\n\n        var plan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_123\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 10 }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() }\n        };\n\n        var newSubscription = new Subscription\n        {\n            Id = \"sub_new\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]\n            }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(existingSubscription);\n        _pricingClient.ListPlans().Returns([plan]);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>\n            options.AutomaticTax.Enabled == true &&\n            options.CollectionMethod == CollectionMethod.ChargeAutomatically &&\n            options.Customer == \"cus_123\" &&\n            options.Items.Count == 1 &&\n            options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&\n            options.Items[0].Quantity == 10 &&\n            options.Metadata[\"organizationId\"] == organizationId.ToString() &&\n            options.OffSession == true &&\n            options.TrialPeriodDays == 0));\n\n        await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>\n            org.Id == organizationId &&\n            org.GatewaySubscriptionId == \"sub_new\" &&\n            org.Enabled == true &&\n            org.ExpirationDate == currentPeriodEnd &&\n            org.PlanType == PlanType.EnterpriseAnnually));\n    }\n\n    [Fact]\n    public async Task Run_Organization_WithNonDisabledPlan_WithStorage_Success()\n    {\n        var organizationId = Guid.NewGuid();\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = PlanType.TeamsAnnually\n        };\n\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_456\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 5 },\n                    new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 3 }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() }\n        };\n\n        var newSubscription = new Subscription\n        {\n            Id = \"sub_new_2\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]\n            }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(existingSubscription);\n        _pricingClient.ListPlans().Returns([plan]);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>\n            options.Items.Count == 2 &&\n            options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&\n            options.Items[0].Quantity == 5 &&\n            options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&\n            options.Items[1].Quantity == 3));\n\n        await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>\n            org.Id == organizationId &&\n            org.GatewaySubscriptionId == \"sub_new_2\" &&\n            org.Enabled == true));\n    }\n\n    [Fact]\n    public async Task Run_Organization_WithSecretsManager_Success()\n    {\n        var organizationId = Guid.NewGuid();\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = PlanType.EnterpriseMonthly\n        };\n\n        var plan = MockPlans.Get(PlanType.EnterpriseMonthly);\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_789\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 15 },\n                    new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 2 },\n                    new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 10 },\n                    new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeServiceAccountPlanId }, Quantity = 100 }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() }\n        };\n\n        var newSubscription = new Subscription\n        {\n            Id = \"sub_new_3\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]\n            }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(existingSubscription);\n        _pricingClient.ListPlans().Returns([plan]);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>\n            options.Items.Count == 4 &&\n            options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&\n            options.Items[0].Quantity == 15 &&\n            options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&\n            options.Items[1].Quantity == 2 &&\n            options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId &&\n            options.Items[2].Quantity == 10 &&\n            options.Items[3].Price == plan.SecretsManager.StripeServiceAccountPlanId &&\n            options.Items[3].Quantity == 100));\n\n        await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>\n            org.Id == organizationId &&\n            org.GatewaySubscriptionId == \"sub_new_3\" &&\n            org.Enabled == true));\n    }\n\n    [Fact]\n    public async Task Run_Organization_WithDisabledPlan_UpgradesToNewPlan_Success()\n    {\n        var organizationId = Guid.NewGuid();\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = PlanType.EnterpriseAnnually2023\n        };\n\n        var oldPlan = new DisabledEnterprisePlan2023(true);\n        var newPlan = MockPlans.Get(PlanType.EnterpriseAnnually);\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_old\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeSeatPlanId }, Quantity = 20 },\n                    new SubscriptionItem { Price = new Price { Id = oldPlan.PasswordManager.StripeStoragePlanId }, Quantity = 5 }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() }\n        };\n\n        var newSubscription = new Subscription\n        {\n            Id = \"sub_upgraded\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]\n            }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(existingSubscription);\n        _pricingClient.ListPlans().Returns([oldPlan, newPlan]);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>\n            options.Items.Count == 2 &&\n            options.Items[0].Price == newPlan.PasswordManager.StripeSeatPlanId &&\n            options.Items[0].Quantity == 20 &&\n            options.Items[1].Price == newPlan.PasswordManager.StripeStoragePlanId &&\n            options.Items[1].Quantity == 5));\n\n        await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>\n            org.Id == organizationId &&\n            org.GatewaySubscriptionId == \"sub_upgraded\" &&\n            org.Enabled == true &&\n            org.PlanType == PlanType.EnterpriseAnnually &&\n            org.Plan == newPlan.Name &&\n            org.SelfHost == newPlan.HasSelfHost &&\n            org.UsePolicies == newPlan.HasPolicies &&\n            org.UseGroups == newPlan.HasGroups &&\n            org.UseDirectory == newPlan.HasDirectory &&\n            org.UseEvents == newPlan.HasEvents &&\n            org.UseTotp == newPlan.HasTotp &&\n            org.Use2fa == newPlan.Has2fa &&\n            org.UseApi == newPlan.HasApi &&\n            org.UseSso == newPlan.HasSso &&\n            org.UseOrganizationDomains == newPlan.HasOrganizationDomains &&\n            org.UseKeyConnector == newPlan.HasKeyConnector &&\n            org.UseScim == newPlan.HasScim &&\n            org.UseResetPassword == newPlan.HasResetPassword &&\n            org.UsersGetPremium == newPlan.UsersGetPremium &&\n            org.UseCustomPermissions == newPlan.HasCustomPermissions));\n    }\n\n    [Fact]\n    public async Task Run_Organization_WithStorageAndSecretManagerButNoServiceAccounts_Success()\n    {\n        var organizationId = Guid.NewGuid();\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = PlanType.TeamsAnnually\n        };\n\n        var plan = MockPlans.Get(PlanType.TeamsAnnually);\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_complex\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 12 },\n                    new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId }, Quantity = 8 },\n                    new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 6 }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() }\n        };\n\n        var newSubscription = new Subscription\n        {\n            Id = \"sub_complex\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]\n            }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(existingSubscription);\n        _pricingClient.ListPlans().Returns([plan]);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>\n            options.Items.Count == 3 &&\n            options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&\n            options.Items[0].Quantity == 12 &&\n            options.Items[1].Price == plan.PasswordManager.StripeStoragePlanId &&\n            options.Items[1].Quantity == 8 &&\n            options.Items[2].Price == plan.SecretsManager.StripeSeatPlanId &&\n            options.Items[2].Quantity == 6));\n\n        await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>\n            org.Id == organizationId &&\n            org.GatewaySubscriptionId == \"sub_complex\" &&\n            org.Enabled == true));\n    }\n\n    [Fact]\n    public async Task Run_Organization_WithSecretsManagerOnly_NoServiceAccounts_Success()\n    {\n        var organizationId = Guid.NewGuid();\n        var currentPeriodEnd = DateTime.UtcNow.AddMonths(1);\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = PlanType.TeamsMonthly\n        };\n\n        var plan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        var existingSubscription = new Subscription\n        {\n            Status = SubscriptionStatus.Canceled,\n            CustomerId = \"cus_sm\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data =\n                [\n                    new SubscriptionItem { Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId }, Quantity = 8 },\n                    new SubscriptionItem { Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId }, Quantity = 5 }\n                ]\n            },\n            Metadata = new Dictionary<string, string> { [\"organizationId\"] = organizationId.ToString() }\n        };\n\n        var newSubscription = new Subscription\n        {\n            Id = \"sub_sm\",\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd }]\n            }\n        };\n\n        _subscriberService.GetSubscription(organization).Returns(existingSubscription);\n        _pricingClient.ListPlans().Returns([plan]);\n        _stripeAdapter.CreateSubscriptionAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(newSubscription);\n\n        var result = await _command.Run(organization);\n\n        Assert.True(result.IsT0);\n\n        await _stripeAdapter.Received(1).CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(options =>\n            options.Items.Count == 2 &&\n            options.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&\n            options.Items[0].Quantity == 8 &&\n            options.Items[1].Price == plan.SecretsManager.StripeSeatPlanId &&\n            options.Items[1].Quantity == 5));\n\n        await _organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org =>\n            org.Id == organizationId &&\n            org.GatewaySubscriptionId == \"sub_sm\" &&\n            org.Enabled == true));\n    }\n\n    private record DisabledEnterprisePlan2023 : Bit.Core.Models.StaticStore.Plan\n    {\n        public DisabledEnterprisePlan2023(bool isAnnual)\n        {\n            Type = PlanType.EnterpriseAnnually2023;\n            ProductTier = ProductTierType.Enterprise;\n            Name = \"Enterprise (Annually) 2023\";\n            IsAnnual = isAnnual;\n            NameLocalizationKey = \"planNameEnterprise\";\n            DescriptionLocalizationKey = \"planDescEnterprise\";\n            CanBeUsedByBusiness = true;\n            TrialPeriodDays = 7;\n            HasPolicies = true;\n            HasSelfHost = true;\n            HasGroups = true;\n            HasDirectory = true;\n            HasEvents = true;\n            HasTotp = true;\n            Has2fa = true;\n            HasApi = true;\n            HasSso = true;\n            HasOrganizationDomains = true;\n            HasKeyConnector = true;\n            HasScim = true;\n            HasResetPassword = true;\n            UsersGetPremium = true;\n            HasCustomPermissions = true;\n            UpgradeSortOrder = 4;\n            DisplaySortOrder = 4;\n            LegacyYear = 2024;\n            Disabled = true;\n\n            PasswordManager = new PasswordManagerFeatures(isAnnual);\n            SecretsManager = new SecretsManagerFeatures(isAnnual);\n        }\n\n        private record SecretsManagerFeatures : SecretsManagerPlanFeatures\n        {\n            public SecretsManagerFeatures(bool isAnnual)\n            {\n                BaseSeats = 0;\n                BasePrice = 0;\n                BaseServiceAccount = 200;\n                HasAdditionalSeatsOption = true;\n                HasAdditionalServiceAccountOption = true;\n                AllowSeatAutoscale = true;\n                AllowServiceAccountsAutoscale = true;\n\n                if (isAnnual)\n                {\n                    StripeSeatPlanId = \"secrets-manager-enterprise-seat-annually-2023\";\n                    StripeServiceAccountPlanId = \"secrets-manager-service-account-2023-annually\";\n                    SeatPrice = 144;\n                    AdditionalPricePerServiceAccount = 12;\n                }\n                else\n                {\n                    StripeSeatPlanId = \"secrets-manager-enterprise-seat-monthly-2023\";\n                    StripeServiceAccountPlanId = \"secrets-manager-service-account-2023-monthly\";\n                    SeatPrice = 13;\n                    AdditionalPricePerServiceAccount = 1;\n                }\n            }\n        }\n\n        private record PasswordManagerFeatures : PasswordManagerPlanFeatures\n        {\n            public PasswordManagerFeatures(bool isAnnual)\n            {\n                BaseSeats = 0;\n                BaseStorageGb = 1;\n                HasAdditionalStorageOption = true;\n                HasAdditionalSeatsOption = true;\n                AllowSeatAutoscale = true;\n\n                if (isAnnual)\n                {\n                    AdditionalStoragePricePerGb = 4;\n                    StripeStoragePlanId = \"storage-gb-annually\";\n                    StripeSeatPlanId = \"2023-enterprise-org-seat-annually-old\";\n                    SeatPrice = 72;\n                }\n                else\n                {\n                    StripeSeatPlanId = \"2023-enterprise-seat-monthly-old\";\n                    StripeStoragePlanId = \"storage-gb-monthly\";\n                    SeatPrice = 7;\n                    AdditionalStoragePricePerGb = 0.5M;\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Tax/TaxHelpersTests.cs",
    "content": "﻿using Bit.Core.Billing.Tax.Utilities;\nusing Xunit;\nusing CountryAbbreviations = Bit.Core.Constants.CountryAbbreviations;\nusing TaxExempt = Bit.Core.Billing.Constants.StripeConstants.TaxExempt;\n\nnamespace Bit.Core.Test.Billing.Tax;\n\npublic class TaxHelpersTests\n{\n\n    [Theory]\n    [InlineData(CountryAbbreviations.UnitedStates, true)]\n    [InlineData(CountryAbbreviations.Switzerland, true)]\n    [InlineData(\"DE\", false)]\n    [InlineData(null, false)]\n    [InlineData(\"\", false)]\n    public void IsDirectTaxCountry_ReturnsExpectedResult(string? country, bool expected)\n    {\n        var result = TaxHelpers.IsDirectTaxCountry(country);\n        Assert.Equal(expected, result);\n    }\n\n    [Theory]\n    [InlineData(\"DE\", TaxExempt.None, TaxExempt.Reverse)]                              // non-direct-tax → Reverse\n    [InlineData(CountryAbbreviations.UnitedStates, TaxExempt.Reverse, TaxExempt.None)] // US Reverse → None (direct-tax)\n    [InlineData(CountryAbbreviations.Switzerland, null, TaxExempt.None)]               // CH no existing status → None\n    [InlineData(CountryAbbreviations.UnitedStates, TaxExempt.None, TaxExempt.None)]   // US already None → None\n    [InlineData(CountryAbbreviations.Switzerland, TaxExempt.Reverse, TaxExempt.None)] // CH Reverse → None (direct-tax, not preserved)\n    [InlineData(\"DE\", TaxExempt.Reverse, TaxExempt.Reverse)]                          // non-direct-tax already Reverse → Reverse\n    [InlineData(null, TaxExempt.None, TaxExempt.Reverse)]                             // unknown country → Reverse\n    [InlineData(\"DE\", TaxExempt.Exempt, TaxExempt.Exempt)]                            // exempt always preserved — non-direct-tax country\n    [InlineData(CountryAbbreviations.UnitedStates, TaxExempt.Exempt, TaxExempt.Exempt)] // exempt always preserved — direct-tax country\n    [InlineData(CountryAbbreviations.Switzerland, TaxExempt.Exempt, TaxExempt.Exempt)]  // exempt always preserved — CH\n    public void DetermineTaxExemptStatus_ReturnsExpectedResult(\n        string? country,\n        string? currentTaxExempt,\n        string expected)\n    {\n        var result = TaxHelpers.DetermineTaxExemptStatus(country, currentTaxExempt);\n        Assert.Equal(expected, result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Billing/Utilities.cs",
    "content": "﻿using Bit.Core.Billing;\nusing Xunit;\n\nnamespace Bit.Core.Test.Billing;\n\npublic static class Utilities\n{\n    public static async Task ThrowsBillingExceptionAsync(\n        Func<Task> function,\n        string response = null,\n        string message = null,\n        Exception innerException = null)\n    {\n        var expected = new BillingException(response, message, innerException);\n\n        var actual = await Assert.ThrowsAsync<BillingException>(function);\n\n        Assert.Equal(expected.Response, actual.Response);\n        Assert.Equal(expected.Message, actual.Message);\n        Assert.Equal(expected.InnerException, actual.InnerException);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/ConstantsTests.cs",
    "content": "﻿using Xunit;\n\nnamespace Bit.Core.Test;\n\npublic class ConstantsTests\n{\n    public class RangeConstantTests\n    {\n        [Fact]\n        public void Constructor_WithValidValues_SetsProperties()\n        {\n            // Arrange\n            const int min = 0;\n            const int max = 10;\n            const int defaultValue = 5;\n\n            // Act\n            var rangeConstant = new RangeConstant(min, max, defaultValue);\n\n            // Assert\n            Assert.Equal(min, rangeConstant.Min);\n            Assert.Equal(max, rangeConstant.Max);\n            Assert.Equal(defaultValue, rangeConstant.Default);\n        }\n\n        [Fact]\n        public void Constructor_WithInvalidValues_ThrowsArgumentOutOfRangeException()\n        {\n            Assert.Throws<ArgumentOutOfRangeException>(() => new RangeConstant(10, 0, 5));\n        }\n\n        [Fact]\n        public void Constructor_WithDefaultValueOutsideRange_ThrowsArgumentOutOfRangeException()\n        {\n            Assert.Throws<ArgumentOutOfRangeException>(() => new RangeConstant(0, 10, 20));\n        }\n\n        [Theory]\n        [InlineData(5)]\n        [InlineData(0)]\n        [InlineData(10)]\n        public void InsideRange_WithValidValues_ReturnsTrue(int number)\n        {\n            // Arrange\n            var rangeConstant = new RangeConstant(0, 10, 5);\n\n            // Act\n            bool result = rangeConstant.InsideRange(number);\n\n            // Assert\n            Assert.True(result);\n        }\n\n        [Theory]\n        [InlineData(-1)]\n        [InlineData(11)]\n        public void InsideRange_WithInvalidValues_ReturnsFalse(int number)\n        {\n            // Arrange\n            var rangeConstant = new RangeConstant(0, 10, 5);\n\n            // Act\n            bool result = rangeConstant.InsideRange(number);\n\n            // Assert\n            Assert.False(result);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Context/CurrentContextTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.AdminConsole.Context;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Auth.Identity;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Context;\n\n[SutProviderCustomize]\npublic class CurrentContextTests\n{\n    #region BuildAsync(HttpContext) Tests\n\n    [Theory, BitAutoData]\n    public async Task BuildAsync_HttpContext_SetsHttpContext(\n        SutProvider<CurrentContext> sutProvider)\n    {\n        var httpContext = new DefaultHttpContext();\n        var globalSettings = new Core.Settings.GlobalSettings();\n        // Act\n        await sutProvider.Sut.BuildAsync(httpContext, globalSettings);\n\n        // Assert\n        Assert.Equal(httpContext, sutProvider.Sut.HttpContext);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildAsync_HttpContext_OnlyBuildsOnce(\n        SutProvider<CurrentContext> sutProvider)\n    {\n        var httpContext = new DefaultHttpContext();\n        var globalSettings = new Core.Settings.GlobalSettings();\n        // Arrange\n        await sutProvider.Sut.BuildAsync(httpContext, globalSettings);\n        var firstContext = sutProvider.Sut.HttpContext;\n\n        var secondHttpContext = new DefaultHttpContext();\n\n        // Act\n        await sutProvider.Sut.BuildAsync(secondHttpContext, globalSettings);\n\n        // Assert\n        Assert.Equal(firstContext, sutProvider.Sut.HttpContext);\n        Assert.NotEqual(secondHttpContext, sutProvider.Sut.HttpContext);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildAsync_HttpContext_SetsDeviceIdentifier(\n        SutProvider<CurrentContext> sutProvider,\n        string expectedValue)\n    {\n        var httpContext = new DefaultHttpContext();\n        var globalSettings = new Core.Settings.GlobalSettings();\n        sutProvider.Sut.DeviceIdentifier = null;\n        // Arrange\n        httpContext.Request.Headers[\"Device-Identifier\"] = expectedValue;\n\n        // Act\n        await sutProvider.Sut.BuildAsync(httpContext, globalSettings);\n\n        // Assert\n        Assert.Equal(expectedValue, sutProvider.Sut.DeviceIdentifier);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildAsync_HttpContext_SetsCountryName(\n        SutProvider<CurrentContext> sutProvider,\n        string countryName)\n    {\n        var httpContext = new DefaultHttpContext();\n        var globalSettings = new Core.Settings.GlobalSettings();\n        // Arrange\n        httpContext.Request.Headers[\"country-name\"] = countryName;\n\n        // Act\n        await sutProvider.Sut.BuildAsync(httpContext, globalSettings);\n\n        // Assert\n        Assert.Equal(countryName, sutProvider.Sut.CountryName);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildAsync_HttpContext_SetsDeviceType(\n        SutProvider<CurrentContext> sutProvider)\n    {\n        var httpContext = new DefaultHttpContext();\n        var globalSettings = new Core.Settings.GlobalSettings();\n        // Arrange\n        var deviceType = DeviceType.Android;\n        httpContext.Request.Headers[\"Device-Type\"] = ((int)deviceType).ToString();\n\n        // Act\n        await sutProvider.Sut.BuildAsync(httpContext, globalSettings);\n\n        // Assert\n        Assert.Equal(deviceType, sutProvider.Sut.DeviceType);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildAsync_HttpContext_SetsClientVersion(\n        SutProvider<CurrentContext> sutProvider)\n    {\n        var httpContext = new DefaultHttpContext();\n        var globalSettings = new Core.Settings.GlobalSettings();\n        // Arrange\n        var version = \"2024.1.0\";\n        httpContext.Request.Headers[\"Bitwarden-Client-Version\"] = version;\n        httpContext.Request.Headers[\"Is-Prerelease\"] = \"1\";\n\n        // Act\n        await sutProvider.Sut.BuildAsync(httpContext, globalSettings);\n\n        // Assert\n        Assert.Equal(new Version(version), sutProvider.Sut.ClientVersion);\n        Assert.True(sutProvider.Sut.ClientVersionIsPrerelease);\n    }\n\n    #endregion\n\n    #region SetContextAsync Tests\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_NullUser_DoesNotThrow(\n        SutProvider<CurrentContext> sutProvider)\n    {\n        // Act & Assert\n        await sutProvider.Sut.SetContextAsync(null);\n        // Should not throw\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_UserWithNoClaims_DoesNotThrow(\n        SutProvider<CurrentContext> sutProvider)\n    {\n        // Arrange\n        var user = new ClaimsPrincipal();\n\n        // Act & Assert\n        await sutProvider.Sut.SetContextAsync(user);\n        // Should not throw\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_SendClient_ShortCircuits(\n        SutProvider<CurrentContext> sutProvider,\n        Guid userId)\n    {\n        // Arrange\n        sutProvider.Sut.UserId = null;\n        var claims = new List<Claim>\n        {\n            new(Claims.Type, IdentityClientType.Send.ToString()),\n            new(\"sub\", userId.ToString())\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Equal(IdentityClientType.Send, sutProvider.Sut.IdentityClientType);\n        Assert.Null(sutProvider.Sut.UserId); // Should not be set for Send clients\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_RegularUser_SetsUserId(\n        SutProvider<CurrentContext> sutProvider,\n        Guid userId,\n        string clientId)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(\"sub\", userId.ToString()),\n            new(\"client_id\", clientId)\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Equal(userId, sutProvider.Sut.UserId);\n        Assert.Equal(clientId, sutProvider.Sut.ClientId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_InstallationClient_SetsInstallationId(\n        SutProvider<CurrentContext> sutProvider,\n        Guid installationId)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(\"client_id\", \"installation.12345\"),\n            new(\"client_sub\", installationId.ToString())\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Equal(installationId, sutProvider.Sut.InstallationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_OrganizationClient_SetsOrganizationId(\n        SutProvider<CurrentContext> sutProvider,\n        Guid organizationId)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(\"client_id\", \"organization.12345\"),\n            new(\"client_sub\", organizationId.ToString())\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Equal(organizationId, sutProvider.Sut.OrganizationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_ServiceAccount_SetsServiceAccountOrganizationId(\n        SutProvider<CurrentContext> sutProvider,\n        Guid organizationId)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(Claims.Type, IdentityClientType.ServiceAccount.ToString()),\n            new(Claims.Organization, organizationId.ToString())\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Equal(IdentityClientType.ServiceAccount, sutProvider.Sut.IdentityClientType);\n        Assert.Equal(organizationId, sutProvider.Sut.ServiceAccountOrganizationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_WithDeviceClaims_SetsDeviceInfo(\n        SutProvider<CurrentContext> sutProvider,\n        string deviceIdentifier)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(Claims.Device, deviceIdentifier),\n            new(Claims.DeviceType, ((int)DeviceType.iOS).ToString())\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Equal(deviceIdentifier, sutProvider.Sut.DeviceIdentifier);\n        Assert.Equal(DeviceType.iOS, sutProvider.Sut.DeviceType);\n    }\n\n    #endregion\n\n    #region Organization Claims Tests\n\n    [Theory]\n    [BitAutoData(Claims.OrganizationOwner, OrganizationUserType.Owner)]\n    [BitAutoData(Claims.OrganizationAdmin, OrganizationUserType.Admin)]\n    [BitAutoData(Claims.OrganizationUser, OrganizationUserType.User)]\n    public async Task SetContextAsync_OrganizationClaims_SetsOrganizations(\n        string userOrgAssociation,\n        OrganizationUserType userType,\n        SutProvider<CurrentContext> sutProvider,\n        Guid org1Id,\n        Guid org2Id)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(userOrgAssociation, org1Id.ToString()),\n            new(userOrgAssociation, org2Id.ToString()),\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Equal(2, sutProvider.Sut.Organizations.Count);\n        Assert.All(sutProvider.Sut.Organizations, org => Assert.Equal(userType, org.Type));\n        Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org1Id);\n        Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org2Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_OrganizationCustomClaims_SetsOrganizationsWithPermissions(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(Claims.OrganizationCustom, orgId.ToString()),\n            new(\"accesseventlogs\", orgId.ToString()),\n            new(\"manageusers\", orgId.ToString())\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Single(sutProvider.Sut.Organizations);\n        var org = sutProvider.Sut.Organizations.First();\n        Assert.Equal(OrganizationUserType.Custom, org.Type);\n        Assert.Equal(orgId, org.Id);\n        Assert.True(org.Permissions.AccessEventLogs);\n        Assert.True(org.Permissions.ManageUsers);\n        Assert.False(org.Permissions.ManageGroups);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_SecretsManagerAccess_SetsAccessSecretsManager(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(Claims.OrganizationOwner, orgId.ToString()),\n            new(Claims.SecretsManagerAccess, orgId.ToString())\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Single(sutProvider.Sut.Organizations);\n        Assert.True(sutProvider.Sut.Organizations.First().AccessSecretsManager);\n    }\n\n    #endregion\n\n    #region Provider Claims Tests\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_ProviderAdminClaims_SetsProviders(\n        SutProvider<CurrentContext> sutProvider,\n        Guid providerId)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(Claims.ProviderAdmin, providerId.ToString())\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Single(sutProvider.Sut.Providers);\n        Assert.Equal(ProviderUserType.ProviderAdmin, sutProvider.Sut.Providers.First().Type);\n        Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetContextAsync_ProviderServiceUserClaims_SetsProviders(\n        SutProvider<CurrentContext> sutProvider,\n        Guid providerId)\n    {\n        // Arrange\n        var claims = new List<Claim>\n        {\n            new(Claims.ProviderServiceUser, providerId.ToString())\n        };\n        var user = new ClaimsPrincipal(new ClaimsIdentity(claims));\n\n        // Act\n        await sutProvider.Sut.SetContextAsync(user);\n\n        // Assert\n        Assert.Single(sutProvider.Sut.Providers);\n        Assert.Equal(ProviderUserType.ServiceUser, sutProvider.Sut.Providers.First().Type);\n        Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id);\n    }\n\n    #endregion\n\n    #region Organization Permission Tests\n\n    [Theory, BitAutoData]\n    public async Task OrganizationUser_WithDirectAccess_ReturnsTrue(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization>\n        {\n            new() { Id = orgId, Type = OrganizationUserType.User }\n        };\n\n        // Act\n        var result = await sutProvider.Sut.OrganizationUser(orgId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task OrganizationUser_WithoutAccess_ReturnsFalse(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization>();\n\n        // Act\n        var result = await sutProvider.Sut.OrganizationUser(orgId);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task OrganizationAdmin_WithAdminAccess_ReturnsTrue(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization>\n        {\n            new() { Id = orgId, Type = OrganizationUserType.Admin }\n        };\n\n        // Act\n        var result = await sutProvider.Sut.OrganizationAdmin(orgId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task OrganizationOwner_WithOwnerAccess_ReturnsTrue(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization>\n        {\n            new() { Id = orgId, Type = OrganizationUserType.Owner }\n        };\n\n        // Act\n        var result = await sutProvider.Sut.OrganizationOwner(orgId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task OrganizationCustom_WithCustomAccess_ReturnsTrue(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization>\n        {\n            new() { Id = orgId, Type = OrganizationUserType.Custom }\n        };\n\n        // Act\n        var result = await sutProvider.Sut.OrganizationCustom(orgId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AccessEventLogs_WithPermission_ReturnsTrue(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization>\n        {\n            new()\n            {\n                Id = orgId,\n                Type = OrganizationUserType.Custom,\n                Permissions = new Permissions { AccessEventLogs = true }\n            }\n        };\n\n        // Act\n        var result = await sutProvider.Sut.AccessEventLogs(orgId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    #endregion\n\n    #region Provider Permission Tests\n\n    [Theory, BitAutoData]\n    public void ProviderProviderAdmin_WithAdminAccess_ReturnsTrue(\n        SutProvider<CurrentContext> sutProvider,\n        Guid providerId)\n    {\n        // Arrange\n        sutProvider.Sut.Providers = new List<CurrentContextProvider>\n        {\n            new() { Id = providerId, Type = ProviderUserType.ProviderAdmin }\n        };\n\n        // Act\n        var result = sutProvider.Sut.ProviderProviderAdmin(providerId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public void ProviderUser_WithAnyAccess_ReturnsTrue(\n        SutProvider<CurrentContext> sutProvider,\n        Guid providerId)\n    {\n        // Arrange\n        sutProvider.Sut.Providers = new List<CurrentContextProvider>\n        {\n            new() { Id = providerId, Type = ProviderUserType.ServiceUser }\n        };\n\n        // Act\n        var result = sutProvider.Sut.ProviderUser(providerId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    #endregion\n\n    #region Secrets Manager Tests\n\n    [Theory, BitAutoData]\n    public void AccessSecretsManager_WithServiceAccount_ReturnsTrue(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.ServiceAccountOrganizationId = orgId;\n\n        // Act\n        var result = sutProvider.Sut.AccessSecretsManager(orgId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public void AccessSecretsManager_WithOrgAccess_ReturnsTrue(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization>\n        {\n            new() { Id = orgId, AccessSecretsManager = true }\n        };\n\n        // Act\n        var result = sutProvider.Sut.AccessSecretsManager(orgId);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public void AccessSecretsManager_WithoutAccess_ReturnsFalse(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization>\n        {\n            new() { Id = orgId, AccessSecretsManager = false }\n        };\n\n        // Act\n        var result = sutProvider.Sut.AccessSecretsManager(orgId);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    #endregion\n\n    #region Membership Loading Tests\n\n    [Theory, BitAutoData]\n    public async Task OrganizationMembershipAsync_LoadsFromRepository(\n        SutProvider<CurrentContext> sutProvider,\n        Guid userId,\n        List<OrganizationUserOrganizationDetails> userOrgs)\n    {\n        // Arrange\n        sutProvider.Sut.UserId = userId;\n        sutProvider.Sut.Organizations = null;\n        var organizationUserRepository = Substitute.For<IOrganizationUserRepository>();\n        userOrgs.ForEach(org => org.Status = OrganizationUserStatusType.Confirmed);\n\n        // Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test.\n        userOrgs.ForEach(org => org.Permissions = \"{}\");\n        organizationUserRepository.GetManyDetailsByUserAsync(userId)\n            .Returns(userOrgs);\n\n        // Act\n        var result = await sutProvider.Sut.OrganizationMembershipAsync(organizationUserRepository, userId);\n\n        // Assert\n        Assert.Equal(userOrgs.Count, result.Count);\n        Assert.Equal(userId, sutProvider.Sut.UserId);\n        await organizationUserRepository.Received(1).GetManyDetailsByUserAsync(userId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProviderMembershipAsync_LoadsFromRepository(\n        SutProvider<CurrentContext> sutProvider,\n        Guid userId,\n        List<ProviderUser> userProviders)\n    {\n        // Arrange\n        sutProvider.Sut.UserId = userId;\n        sutProvider.Sut.Providers = null;\n\n        var providerUserRepository = Substitute.For<IProviderUserRepository>();\n        userProviders.ForEach(provider => provider.Status = ProviderUserStatusType.Confirmed);\n\n        // Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test.\n        userProviders.ForEach(provider => provider.Permissions = \"{}\");\n        providerUserRepository.GetManyByUserAsync(userId)\n            .Returns(userProviders);\n\n        // Act\n        var result = await sutProvider.Sut.ProviderMembershipAsync(providerUserRepository, userId);\n\n        // Assert\n        Assert.Equal(userProviders.Count, result.Count);\n        Assert.Equal(userId, sutProvider.Sut.UserId);\n        await providerUserRepository.Received(1).GetManyByUserAsync(userId);\n    }\n\n    #endregion\n\n    #region Utility Tests\n\n    [Theory, BitAutoData]\n    public void GetOrganization_WithExistingOrg_ReturnsOrganization(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        var org = new CurrentContextOrganization { Id = orgId };\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization> { org };\n\n        // Act\n        var result = sutProvider.Sut.GetOrganization(orgId);\n\n        // Assert\n        Assert.Equal(org, result);\n    }\n\n    [Theory, BitAutoData]\n    public void GetOrganization_WithNonExistingOrg_ReturnsNull(\n        SutProvider<CurrentContext> sutProvider,\n        Guid orgId)\n    {\n        // Arrange\n        sutProvider.Sut.Organizations = new List<CurrentContextOrganization>();\n\n        // Act\n        var result = sutProvider.Sut.GetOrganization(orgId);\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Core.Test/Core.Test.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <IsPackable>false</IsPackable>\n    <RootNamespace>Bit.Core.Test</RootNamespace>\n    <!-- These opt outs should be removed when all warnings are addressed -->\n    <WarningsNotAsErrors>$(WarningsNotAsErrors);CA1304;CA1305</WarningsNotAsErrors>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"coverlet.collector\" Version=\"$(CoverletCollectorVersion)\">\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Microsoft.Extensions.Diagnostics.Testing\" Version=\"9.3.0\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"$(MicrosoftNetTestSdkVersion)\" />\n    <PackageReference Include=\"NSubstitute\" Version=\"$(NSubstituteVersion)\" />\n    <PackageReference Include=\"xunit\" Version=\"$(XUnitVersion)\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"$(XUnitRunnerVisualStudioVersion)\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"AutoFixture.Xunit2\" Version=\"$(AutoFixtureXUnit2Version)\" />\n    <PackageReference Include=\"AutoFixture.AutoNSubstitute\" Version=\"$(AutoFixtureAutoNSubstituteVersion)\" />\n    <PackageReference Include=\"Kralizek.AutoFixture.Extensions.MockHttp\" Version=\"2.1.0\" />\n  </ItemGroup>\n  <ItemGroup>\n    <ProjectReference Include=\"..\\..\\src\\Core\\Core.csproj\" />\n    <ProjectReference Include=\"..\\Common\\Common.csproj\" />\n  </ItemGroup>\n  <ItemGroup>\n    <None Remove=\"Utilities\\data\\embeddedResource.txt\" />\n  </ItemGroup>\n  <ItemGroup>\n    <!-- Email templates uses .hbs extension, they must be included for emails to work -->\n    <EmbeddedResource Include=\"**\\*.hbs\" />\n\n    <EmbeddedResource Include=\"Utilities\\data\\embeddedResource.txt\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "test/Core.Test/Dirt/EventIntegrations/EventIntegrationServiceCollectionExtensionsTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Dirt.Services.NoopImplementations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Utilities;\nusing Microsoft.Bot.Builder;\nusing Microsoft.Bot.Builder.Integration.AspNet.Core;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Microsoft.Extensions.Hosting;\nusing NSubstitute;\nusing StackExchange.Redis;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.EventIntegrations;\n\npublic class EventIntegrationServiceCollectionExtensionsTests\n{\n    private readonly IServiceCollection _services;\n    private readonly GlobalSettings _globalSettings;\n\n    public EventIntegrationServiceCollectionExtensionsTests()\n    {\n        _services = new ServiceCollection();\n        _globalSettings = CreateGlobalSettings([]);\n\n        // Add required infrastructure services\n        _services.TryAddSingleton(_globalSettings);\n        _services.TryAddSingleton<IGlobalSettings>(_globalSettings);\n        _services.AddLogging();\n\n        // Mock Redis connection for cache\n        _services.AddSingleton(Substitute.For<IConnectionMultiplexer>());\n\n        // Mock required repository dependencies for commands\n        _services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationRepository>());\n        _services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationConfigurationRepository>());\n        _services.TryAddScoped(_ => Substitute.For<IOrganizationRepository>());\n    }\n\n    [Fact]\n    public void AddEventIntegrationsCommandsQueries_RegistersAllServices()\n    {\n        _services.AddEventIntegrationsCommandsQueries(_globalSettings);\n\n        using var provider = _services.BuildServiceProvider();\n\n        var cache = provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName);\n        Assert.NotNull(cache);\n\n        var validator = provider.GetRequiredService<IOrganizationIntegrationConfigurationValidator>();\n        Assert.NotNull(validator);\n\n        using var scope = provider.CreateScope();\n        var sp = scope.ServiceProvider;\n\n        Assert.NotNull(sp.GetService<ICreateOrganizationIntegrationCommand>());\n        Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationCommand>());\n        Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationCommand>());\n        Assert.NotNull(sp.GetService<IGetOrganizationIntegrationsQuery>());\n\n        Assert.NotNull(sp.GetService<ICreateOrganizationIntegrationConfigurationCommand>());\n        Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationConfigurationCommand>());\n        Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationConfigurationCommand>());\n        Assert.NotNull(sp.GetService<IGetOrganizationIntegrationConfigurationsQuery>());\n    }\n\n    [Fact]\n    public void AddEventIntegrationsCommandsQueries_CommandsQueries_AreRegisteredAsScoped()\n    {\n        _services.AddEventIntegrationsCommandsQueries(_globalSettings);\n\n        var createIntegrationDescriptor = _services.First(s =>\n            s.ServiceType == typeof(ICreateOrganizationIntegrationCommand));\n        var createConfigDescriptor = _services.First(s =>\n            s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand));\n\n        Assert.Equal(ServiceLifetime.Scoped, createIntegrationDescriptor.Lifetime);\n        Assert.Equal(ServiceLifetime.Scoped, createConfigDescriptor.Lifetime);\n    }\n\n    [Fact]\n    public void AddEventIntegrationsCommandsQueries_CommandsQueries_DifferentInstancesPerScope()\n    {\n        _services.AddEventIntegrationsCommandsQueries(_globalSettings);\n\n        var provider = _services.BuildServiceProvider();\n\n        ICreateOrganizationIntegrationCommand? instance1, instance2, instance3;\n        using (var scope1 = provider.CreateScope())\n        {\n            instance1 = scope1.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();\n        }\n        using (var scope2 = provider.CreateScope())\n        {\n            instance2 = scope2.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();\n        }\n        using (var scope3 = provider.CreateScope())\n        {\n            instance3 = scope3.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();\n        }\n\n        Assert.NotNull(instance1);\n        Assert.NotNull(instance2);\n        Assert.NotNull(instance3);\n        Assert.NotSame(instance1, instance2);\n        Assert.NotSame(instance2, instance3);\n        Assert.NotSame(instance1, instance3);\n    }\n\n    [Fact]\n    public void AddEventIntegrationsCommandsQueries_CommandsQueries__SameInstanceWithinScope()\n    {\n        _services.AddEventIntegrationsCommandsQueries(_globalSettings);\n        var provider = _services.BuildServiceProvider();\n\n        using var scope = provider.CreateScope();\n        var instance1 = scope.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();\n        var instance2 = scope.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();\n\n        Assert.NotNull(instance1);\n        Assert.NotNull(instance2);\n        Assert.Same(instance1, instance2);\n    }\n\n    [Fact]\n    public void AddEventIntegrationsCommandsQueries_MultipleCalls_IsIdempotent()\n    {\n        _services.AddEventIntegrationsCommandsQueries(_globalSettings);\n        _services.AddEventIntegrationsCommandsQueries(_globalSettings);\n        _services.AddEventIntegrationsCommandsQueries(_globalSettings);\n\n        var createConfigCmdDescriptors = _services.Where(s =>\n            s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand)).ToList();\n        Assert.Single(createConfigCmdDescriptors);\n\n        var updateIntegrationCmdDescriptors = _services.Where(s =>\n            s.ServiceType == typeof(IUpdateOrganizationIntegrationCommand)).ToList();\n        Assert.Single(updateIntegrationCmdDescriptors);\n    }\n\n    [Fact]\n    public void AddOrganizationIntegrationCommandsQueries_RegistersAllIntegrationServices()\n    {\n        _services.AddOrganizationIntegrationCommandsQueries();\n\n        Assert.Contains(_services, s => s.ServiceType == typeof(ICreateOrganizationIntegrationCommand));\n        Assert.Contains(_services, s => s.ServiceType == typeof(IUpdateOrganizationIntegrationCommand));\n        Assert.Contains(_services, s => s.ServiceType == typeof(IDeleteOrganizationIntegrationCommand));\n        Assert.Contains(_services, s => s.ServiceType == typeof(IGetOrganizationIntegrationsQuery));\n    }\n\n    [Fact]\n    public void AddOrganizationIntegrationCommandsQueries_MultipleCalls_IsIdempotent()\n    {\n        _services.AddOrganizationIntegrationCommandsQueries();\n        _services.AddOrganizationIntegrationCommandsQueries();\n        _services.AddOrganizationIntegrationCommandsQueries();\n\n        var createCmdDescriptors = _services.Where(s =>\n            s.ServiceType == typeof(ICreateOrganizationIntegrationCommand)).ToList();\n        Assert.Single(createCmdDescriptors);\n    }\n\n    [Fact]\n    public void AddOrganizationIntegrationConfigurationCommandsQueries_RegistersAllConfigurationServices()\n    {\n        _services.AddOrganizationIntegrationConfigurationCommandsQueries();\n\n        Assert.Contains(_services, s => s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand));\n        Assert.Contains(_services, s => s.ServiceType == typeof(IUpdateOrganizationIntegrationConfigurationCommand));\n        Assert.Contains(_services, s => s.ServiceType == typeof(IDeleteOrganizationIntegrationConfigurationCommand));\n        Assert.Contains(_services, s => s.ServiceType == typeof(IGetOrganizationIntegrationConfigurationsQuery));\n    }\n\n    [Fact]\n    public void AddOrganizationIntegrationConfigurationCommandsQueries_MultipleCalls_IsIdempotent()\n    {\n        _services.AddOrganizationIntegrationConfigurationCommandsQueries();\n        _services.AddOrganizationIntegrationConfigurationCommandsQueries();\n        _services.AddOrganizationIntegrationConfigurationCommandsQueries();\n\n        var createCmdDescriptors = _services.Where(s =>\n            s.ServiceType == typeof(ICreateOrganizationIntegrationConfigurationCommand)).ToList();\n        Assert.Single(createCmdDescriptors);\n    }\n\n    [Fact]\n    public void IsRabbitMqEnabled_AllSettingsPresent_ReturnsTrue()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\"\n        });\n\n        Assert.True(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void IsRabbitMqEnabled_MissingHostName_ReturnsFalse()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = null,\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\"\n        });\n\n        Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void IsRabbitMqEnabled_MissingUsername_ReturnsFalse()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = null,\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\"\n        });\n\n        Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void IsRabbitMqEnabled_MissingPassword_ReturnsFalse()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = null,\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\"\n        });\n\n        Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void IsRabbitMqEnabled_MissingEventExchangeName_ReturnsFalse()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = null,\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\"\n        });\n\n        Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void IsRabbitMqEnabled_MissingIntegrationExchangeName_ReturnsFalse()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = null\n        });\n\n        Assert.False(EventIntegrationsServiceCollectionExtensions.IsRabbitMqEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void IsAzureServiceBusEnabled_AllSettingsPresent_ReturnsTrue()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:AzureServiceBus:ConnectionString\"] = \"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:EventTopicName\"] = \"events\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName\"] = \"integration\"\n        });\n\n        Assert.True(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void IsAzureServiceBusEnabled_MissingConnectionString_ReturnsFalse()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:AzureServiceBus:ConnectionString\"] = null,\n            [\"GlobalSettings:EventLogging:AzureServiceBus:EventTopicName\"] = \"events\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName\"] = \"integration\"\n        });\n\n        Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void IsAzureServiceBusEnabled_MissingEventTopicName_ReturnsFalse()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:AzureServiceBus:ConnectionString\"] = \"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:EventTopicName\"] = null,\n            [\"GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName\"] = \"integration\"\n        });\n\n        Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void IsAzureServiceBusEnabled_MissingIntegrationTopicName_ReturnsFalse()\n    {\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:AzureServiceBus:ConnectionString\"] = \"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:EventTopicName\"] = \"events\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName\"] = null\n        });\n\n        Assert.False(EventIntegrationsServiceCollectionExtensions.IsAzureServiceBusEnabled(globalSettings));\n    }\n\n    [Fact]\n    public void AddSlackService_AllSettingsPresent_RegistersSlackService()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:Slack:ClientId\"] = \"test-client-id\",\n            [\"GlobalSettings:Slack:ClientSecret\"] = \"test-client-secret\",\n            [\"GlobalSettings:Slack:Scopes\"] = \"test-scopes\"\n        });\n\n        services.TryAddSingleton(globalSettings);\n        services.AddLogging();\n        services.AddSlackService(globalSettings);\n\n        var provider = services.BuildServiceProvider();\n        var slackService = provider.GetService<ISlackService>();\n\n        Assert.NotNull(slackService);\n        Assert.IsType<SlackService>(slackService);\n\n        var httpClientDescriptor = services.FirstOrDefault(s =>\n            s.ServiceType == typeof(IHttpClientFactory));\n        Assert.NotNull(httpClientDescriptor);\n    }\n\n    [Fact]\n    public void AddSlackService_SettingsMissing_RegistersNoopService()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:Slack:ClientId\"] = null,\n            [\"GlobalSettings:Slack:ClientSecret\"] = null,\n            [\"GlobalSettings:Slack:Scopes\"] = null\n        });\n\n        services.AddSlackService(globalSettings);\n\n        var provider = services.BuildServiceProvider();\n        var slackService = provider.GetService<ISlackService>();\n\n        Assert.NotNull(slackService);\n        Assert.IsType<NoopSlackService>(slackService);\n    }\n\n    [Fact]\n    public void AddTeamsService_AllSettingsPresent_RegistersTeamsServices()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:Teams:ClientId\"] = \"test-client-id\",\n            [\"GlobalSettings:Teams:ClientSecret\"] = \"test-client-secret\",\n            [\"GlobalSettings:Teams:Scopes\"] = \"test-scopes\"\n        });\n\n        services.TryAddSingleton(globalSettings);\n        services.AddLogging();\n        services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationRepository>());\n        services.AddTeamsService(globalSettings);\n\n        var provider = services.BuildServiceProvider();\n\n        var teamsService = provider.GetService<ITeamsService>();\n        Assert.NotNull(teamsService);\n        Assert.IsType<TeamsService>(teamsService);\n\n        var bot = provider.GetService<IBot>();\n        Assert.NotNull(bot);\n        Assert.IsType<TeamsService>(bot);\n\n        var adapter = provider.GetService<IBotFrameworkHttpAdapter>();\n        Assert.NotNull(adapter);\n        Assert.IsType<BotFrameworkHttpAdapter>(adapter);\n\n        var httpClientDescriptor = services.FirstOrDefault(s =>\n            s.ServiceType == typeof(IHttpClientFactory));\n        Assert.NotNull(httpClientDescriptor);\n    }\n\n    [Fact]\n    public void AddTeamsService_SettingsMissing_RegistersNoopService()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:Teams:ClientId\"] = null,\n            [\"GlobalSettings:Teams:ClientSecret\"] = null,\n            [\"GlobalSettings:Teams:Scopes\"] = null\n        });\n\n        services.AddTeamsService(globalSettings);\n\n        var provider = services.BuildServiceProvider();\n        var teamsService = provider.GetService<ITeamsService>();\n\n        Assert.NotNull(teamsService);\n        Assert.IsType<NoopTeamsService>(teamsService);\n    }\n\n    [Fact]\n    public void AddRabbitMqIntegration_RegistersEventIntegrationHandler()\n    {\n        var services = new ServiceCollection();\n        var listenerConfig = new TestListenerConfiguration();\n\n        // Add required dependencies\n        services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());\n        services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());\n        services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());\n        services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());\n        services.TryAddSingleton(Substitute.For<IGroupRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());\n        services.AddLogging();\n\n        services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);\n\n        var provider = services.BuildServiceProvider();\n        var handler = provider.GetRequiredKeyedService<IEventMessageHandler>(listenerConfig.RoutingKey);\n\n        Assert.NotNull(handler);\n    }\n\n    [Fact]\n    public void AddRabbitMqIntegration_RegistersEventListenerService()\n    {\n        var services = new ServiceCollection();\n        var listenerConfig = new TestListenerConfiguration();\n\n        // Add required dependencies\n        services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());\n        services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());\n        services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());\n        services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());\n        services.TryAddSingleton(Substitute.For<IGroupRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());\n        services.TryAddSingleton(Substitute.For<IRabbitMqService>());\n        services.AddLogging();\n\n        var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n        services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);\n        var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n\n        // AddRabbitMqIntegration should register 2 hosted services (Event + Integration listeners)\n        Assert.Equal(2, afterCount - beforeCount);\n    }\n\n    [Fact]\n    public void AddRabbitMqIntegration_RegistersIntegrationListenerService()\n    {\n        var services = new ServiceCollection();\n        var listenerConfig = new TestListenerConfiguration();\n\n        // Add required dependencies\n        services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());\n        services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());\n        services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());\n        services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());\n        services.TryAddSingleton(Substitute.For<IGroupRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());\n        services.TryAddSingleton(Substitute.For<IRabbitMqService>());\n        services.TryAddSingleton(Substitute.For<IIntegrationHandler<WebhookIntegrationConfigurationDetails>>());\n        services.TryAddSingleton(TimeProvider.System);\n        services.AddLogging();\n\n        var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n        services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);\n        var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n\n        // AddRabbitMqIntegration should register 2 hosted services (Event + Integration listeners)\n        Assert.Equal(2, afterCount - beforeCount);\n    }\n\n    [Fact]\n    public void AddAzureServiceBusIntegration_RegistersEventIntegrationHandler()\n    {\n        var services = new ServiceCollection();\n        var listenerConfig = new TestListenerConfiguration();\n\n        // Add required dependencies\n        services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());\n        services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());\n        services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());\n        services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());\n        services.TryAddSingleton(Substitute.For<IGroupRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());\n        services.AddLogging();\n\n        services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);\n\n        var provider = services.BuildServiceProvider();\n        var handler = provider.GetRequiredKeyedService<IEventMessageHandler>(listenerConfig.RoutingKey);\n\n        Assert.NotNull(handler);\n    }\n\n    [Fact]\n    public void AddAzureServiceBusIntegration_RegistersEventListenerService()\n    {\n        var services = new ServiceCollection();\n        var listenerConfig = new TestListenerConfiguration();\n\n        // Add required dependencies\n        services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());\n        services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());\n        services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());\n        services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());\n        services.TryAddSingleton(Substitute.For<IGroupRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());\n        services.TryAddSingleton(Substitute.For<IAzureServiceBusService>());\n        services.AddLogging();\n\n        var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n        services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);\n        var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n\n        // AddAzureServiceBusIntegration should register 2 hosted services (Event + Integration listeners)\n        Assert.Equal(2, afterCount - beforeCount);\n    }\n\n    [Fact]\n    public void AddAzureServiceBusIntegration_RegistersIntegrationListenerService()\n    {\n        var services = new ServiceCollection();\n        var listenerConfig = new TestListenerConfiguration();\n\n        // Add required dependencies\n        services.TryAddSingleton(Substitute.For<IEventIntegrationPublisher>());\n        services.TryAddSingleton(Substitute.For<IIntegrationFilterService>());\n        services.TryAddKeyedSingleton(EventIntegrationsCacheConstants.CacheName, Substitute.For<IFusionCache>());\n        services.TryAddSingleton(Substitute.For<IOrganizationIntegrationConfigurationRepository>());\n        services.TryAddSingleton(Substitute.For<IGroupRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationRepository>());\n        services.TryAddSingleton(Substitute.For<IOrganizationUserRepository>());\n        services.TryAddSingleton(Substitute.For<IAzureServiceBusService>());\n        services.TryAddSingleton(Substitute.For<IIntegrationHandler<WebhookIntegrationConfigurationDetails>>());\n        services.AddLogging();\n\n        var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n        services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, TestListenerConfiguration>(listenerConfig);\n        var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n\n        // AddAzureServiceBusIntegration should register 2 hosted services (Event + Integration listeners)\n        Assert.Equal(2, afterCount - beforeCount);\n    }\n\n    [Fact]\n    public void AddEventIntegrationServices_RegistersCommonServices()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings([]);\n\n        // Add prerequisites\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());\n        services.AddLogging();\n\n        services.AddEventIntegrationServices(globalSettings);\n\n        // Verify common services are registered\n        Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationFilterService));\n        Assert.Contains(services, s => s.ServiceType == typeof(TimeProvider));\n\n        // Verify HttpClients for handlers are registered\n        var httpClientDescriptors = services.Where(s => s.ServiceType == typeof(IHttpClientFactory)).ToList();\n        Assert.NotEmpty(httpClientDescriptors);\n    }\n\n    [Fact]\n    public void AddEventIntegrationServices_RegistersIntegrationHandlers()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings([]);\n\n        // Add prerequisites\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());\n        services.AddLogging();\n\n        services.AddEventIntegrationServices(globalSettings);\n\n        // Verify integration handlers are registered\n        Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<SlackIntegrationConfigurationDetails>));\n        Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<WebhookIntegrationConfigurationDetails>));\n        Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<DatadogIntegrationConfigurationDetails>));\n        Assert.Contains(services, s => s.ServiceType == typeof(IIntegrationHandler<TeamsIntegrationConfigurationDetails>));\n    }\n\n    [Fact]\n    public void AddEventIntegrationServices_RabbitMqEnabled_RegistersRabbitMqListeners()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\"\n        });\n\n        // Add prerequisites\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());\n        services.AddLogging();\n\n        var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n        services.AddEventIntegrationServices(globalSettings);\n        var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n\n        // Should register 11 hosted services for RabbitMQ: 1 repository + 5*2 integration listeners (event+integration)\n        Assert.Equal(11, afterCount - beforeCount);\n    }\n\n    [Fact]\n    public void AddEventIntegrationServices_AzureServiceBusEnabled_RegistersAzureListeners()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:AzureServiceBus:ConnectionString\"] = \"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:EventTopicName\"] = \"events\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName\"] = \"integration\"\n        });\n\n        // Add prerequisites\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());\n        services.AddLogging();\n\n        var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n        services.AddEventIntegrationServices(globalSettings);\n        var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n\n        // Should register 11 hosted services for Azure Service Bus: 1 repository + 5*2 integration listeners (event+integration)\n        Assert.Equal(11, afterCount - beforeCount);\n    }\n\n    [Fact]\n    public void AddEventIntegrationServices_BothEnabled_AzureServiceBusTakesPrecedence()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:ConnectionString\"] = \"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:EventTopicName\"] = \"events\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName\"] = \"integration\"\n        });\n\n        // Add prerequisites\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());\n        services.AddLogging();\n\n        var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n        services.AddEventIntegrationServices(globalSettings);\n        var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n\n        // Should register 11 hosted services for Azure Service Bus: 1 repository + 5*2 integration listeners (event+integration)\n        // NO RabbitMQ services should be enabled because ASB takes precedence\n        Assert.Equal(11, afterCount - beforeCount);\n    }\n\n    [Fact]\n    public void AddEventIntegrationServices_NeitherEnabled_RegistersNoListeners()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings([]);\n\n        // Add prerequisites\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());\n        services.AddLogging();\n\n        var beforeCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n        services.AddEventIntegrationServices(globalSettings);\n        var afterCount = services.Count(s => s.ServiceType == typeof(IHostedService));\n\n        // Should register no hosted services when neither RabbitMQ nor Azure Service Bus is enabled\n        Assert.Equal(0, afterCount - beforeCount);\n    }\n\n    [Fact]\n    public void AddEventWriteServices_AzureServiceBusEnabled_RegistersAzureServices()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:AzureServiceBus:ConnectionString\"] = \"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:EventTopicName\"] = \"events\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName\"] = \"integration\"\n        });\n\n        services.AddEventWriteServices(globalSettings);\n\n        Assert.Contains(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(AzureServiceBusService));\n        Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(EventIntegrationEventWriteService));\n    }\n\n    [Fact]\n    public void AddEventWriteServices_RabbitMqEnabled_RegistersRabbitMqServices()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\"\n        });\n\n        services.AddEventWriteServices(globalSettings);\n\n        Assert.Contains(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(RabbitMqService));\n        Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(EventIntegrationEventWriteService));\n    }\n\n    [Fact]\n    public void AddEventWriteServices_EventsConnectionStringPresent_RegistersAzureQueueService()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:Events:ConnectionString\"] = \"DefaultEndpointsProtocol=https;AccountName=test;AccountKey=test;EndpointSuffix=core.windows.net\",\n            [\"GlobalSettings:Events:QueueName\"] = \"event\"\n        });\n\n        services.AddEventWriteServices(globalSettings);\n\n        Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(AzureQueueEventWriteService));\n    }\n\n    [Fact]\n    public void AddEventWriteServices_SelfHosted_RegistersRepositoryService()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:SelfHosted\"] = \"true\"\n        });\n\n        services.AddEventWriteServices(globalSettings);\n\n        Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(RepositoryEventWriteService));\n    }\n\n    [Fact]\n    public void AddEventWriteServices_NothingEnabled_RegistersNoopService()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings([]);\n\n        services.AddEventWriteServices(globalSettings);\n\n        Assert.Contains(services, s => s.ServiceType == typeof(IEventWriteService) && s.ImplementationType == typeof(NoopEventWriteService));\n    }\n\n    [Fact]\n    public void AddEventWriteServices_AzureTakesPrecedenceOverRabbitMq()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:AzureServiceBus:ConnectionString\"] = \"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:EventTopicName\"] = \"events\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName\"] = \"integration\",\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\"\n        });\n\n        services.AddEventWriteServices(globalSettings);\n\n        // Should use Azure Service Bus, not RabbitMQ\n        Assert.Contains(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(AzureServiceBusService));\n        Assert.DoesNotContain(services, s => s.ServiceType == typeof(IEventIntegrationPublisher) && s.ImplementationType == typeof(RabbitMqService));\n    }\n\n    [Fact]\n    public void AddAzureServiceBusListeners_AzureServiceBusEnabled_RegistersAzureServiceBusServices()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:AzureServiceBus:ConnectionString\"] = \"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:EventTopicName\"] = \"events\",\n            [\"GlobalSettings:EventLogging:AzureServiceBus:IntegrationTopicName\"] = \"integration\"\n        });\n\n        // Add prerequisites\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());\n        services.AddLogging();\n\n        services.AddAzureServiceBusListeners(globalSettings);\n\n        Assert.Contains(services, s => s.ServiceType == typeof(IAzureServiceBusService));\n        Assert.Contains(services, s => s.ServiceType == typeof(IEventRepository));\n        Assert.Contains(services, s => s.ServiceType == typeof(AzureTableStorageEventHandler));\n    }\n\n    [Fact]\n    public void AddAzureServiceBusListeners_AzureServiceBusDisabled_ReturnsEarly()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings([]);\n\n        var initialCount = services.Count;\n        services.AddAzureServiceBusListeners(globalSettings);\n        var finalCount = services.Count;\n\n        Assert.Equal(initialCount, finalCount);\n    }\n\n    [Fact]\n    public void AddRabbitMqListeners_RabbitMqEnabled_RegistersRabbitMqServices()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings(new Dictionary<string, string?>\n        {\n            [\"GlobalSettings:EventLogging:RabbitMq:HostName\"] = \"localhost\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Username\"] = \"user\",\n            [\"GlobalSettings:EventLogging:RabbitMq:Password\"] = \"pass\",\n            [\"GlobalSettings:EventLogging:RabbitMq:EventExchangeName\"] = \"exchange\",\n            [\"GlobalSettings:EventLogging:RabbitMq:IntegrationExchangeName\"] = \"integration\"\n        });\n\n        // Add prerequisites\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton(Substitute.For<IConnectionMultiplexer>());\n        services.AddLogging();\n\n        services.AddRabbitMqListeners(globalSettings);\n\n        Assert.Contains(services, s => s.ServiceType == typeof(IRabbitMqService));\n        Assert.Contains(services, s => s.ServiceType == typeof(EventRepositoryHandler));\n    }\n\n    [Fact]\n    public void AddRabbitMqListeners_RabbitMqDisabled_ReturnsEarly()\n    {\n        var services = new ServiceCollection();\n        var globalSettings = CreateGlobalSettings([]);\n\n        var initialCount = services.Count;\n        services.AddRabbitMqListeners(globalSettings);\n        var finalCount = services.Count;\n\n        Assert.Equal(initialCount, finalCount);\n    }\n\n    private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data)\n    {\n        var config = new ConfigurationBuilder()\n            .AddInMemoryCollection(data)\n            .Build();\n\n        var settings = new GlobalSettings();\n        config.GetSection(\"GlobalSettings\").Bind(settings);\n        return settings;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/CreateOrganizationIntegrationConfigurationCommandTests.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\n\n[SutProviderCustomize]\npublic class CreateOrganizationIntegrationConfigurationCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task CreateAsync_Success_CreatesConfigurationAndInvalidatesCache(\n        SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        integration.Type = IntegrationType.Webhook;\n        configuration.OrganizationIntegrationId = integrationId;\n        configuration.EventType = EventType.User_LoggedIn;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .CreateAsync(configuration)\n            .Returns(configuration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()\n            .ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .CreateAsync(configuration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                organizationId,\n                integration.Type,\n                configuration.EventType.Value));\n        // Also verify RemoveByTagAsync was NOT called\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n        Assert.Equal(configuration, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_WildcardSuccess_CreatesConfigurationAndInvalidatesCache(\n        SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        integration.Type = IntegrationType.Webhook;\n        configuration.OrganizationIntegrationId = integrationId;\n        configuration.EventType = null;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .CreateAsync(configuration)\n            .Returns(configuration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()\n            .ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .CreateAsync(configuration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId,\n                integration.Type));\n        // Also verify RemoveAsync was NOT called\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        Assert.Equal(configuration, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_IntegrationDoesNotExist_ThrowsNotFound(\n        SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns((OrganizationIntegration)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(\n        SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = Guid.NewGuid(); // Different organization\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_ValidationFails_ThrowsBadRequest(\n        SutProvider<CreateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()\n            .ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(false);\n\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        configuration.OrganizationIntegrationId = integrationId;\n        configuration.Template = \"template\";\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.CreateAsync(organizationId, integrationId, configuration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/DeleteOrganizationIntegrationConfigurationCommandTests.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\n\n[SutProviderCustomize]\npublic class DeleteOrganizationIntegrationConfigurationCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_Success_DeletesConfigurationAndInvalidatesCache(\n        SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        integration.Type = IntegrationType.Webhook;\n        configuration.Id = configurationId;\n        configuration.OrganizationIntegrationId = integrationId;\n        configuration.EventType = EventType.User_LoggedIn;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns(configuration);\n\n        await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .GetByIdAsync(configurationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .DeleteAsync(configuration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                organizationId,\n                integration.Type,\n                configuration.EventType.Value));\n        // Also verify RemoveByTagAsync was NOT called\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_WildcardSuccess_DeletesConfigurationAndInvalidatesCache(\n        SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        integration.Type = IntegrationType.Webhook;\n        configuration.Id = configurationId;\n        configuration.OrganizationIntegrationId = integrationId;\n        configuration.EventType = null;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns(configuration);\n\n        await sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .GetByIdAsync(configurationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .DeleteAsync(configuration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId,\n                integration.Type));\n        // Also verify RemoveAsync was NOT called\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(\n        SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns((OrganizationIntegration)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .GetByIdAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(\n        SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = Guid.NewGuid(); // Different organization\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .GetByIdAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_ConfigurationDoesNotExist_ThrowsNotFound(\n        SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns((OrganizationIntegrationConfiguration)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_ConfigurationDoesNotBelongToIntegration_ThrowsNotFound(\n        SutProvider<DeleteOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration configuration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        configuration.Id = configurationId;\n        configuration.OrganizationIntegrationId = Guid.NewGuid(); // Different integration\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns(configuration);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DeleteAsync(organizationId, integrationId, configurationId));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .DeleteAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/GetOrganizationIntegrationConfigurationsQueryTests.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\n\n[SutProviderCustomize]\npublic class GetOrganizationIntegrationConfigurationsQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task GetManyByIntegrationAsync_Success_ReturnsConfigurations(\n        SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration,\n        List<OrganizationIntegrationConfiguration> configurations)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetManyByIntegrationAsync(integrationId)\n            .Returns(configurations);\n\n        var result = await sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .GetManyByIntegrationAsync(integrationId);\n        Assert.Equal(configurations.Count, result.Count);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetManyByIntegrationAsync_NoConfigurations_ReturnsEmptyList(\n        SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetManyByIntegrationAsync(integrationId)\n            .Returns([]);\n\n        var result = await sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId);\n\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetManyByIntegrationAsync_IntegrationDoesNotExist_ThrowsNotFound(\n        SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,\n        Guid organizationId,\n        Guid integrationId)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns((OrganizationIntegration)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .GetManyByIntegrationAsync(Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetManyByIntegrationAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(\n        SutProvider<GetOrganizationIntegrationConfigurationsQuery> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = Guid.NewGuid(); // Different organization\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.GetManyByIntegrationAsync(organizationId, integrationId));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .GetManyByIntegrationAsync(Arg.Any<Guid>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrationConfigurations/UpdateOrganizationIntegrationConfigurationCommandTests.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationIntegrationConfigurationCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_Success_UpdatesConfigurationAndInvalidatesCache(\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration existingConfiguration,\n        OrganizationIntegrationConfiguration updatedConfiguration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        integration.Type = IntegrationType.Webhook;\n        existingConfiguration.Id = configurationId;\n        existingConfiguration.OrganizationIntegrationId = integrationId;\n        existingConfiguration.EventType = EventType.User_LoggedIn;\n        updatedConfiguration.Id = configurationId;\n        updatedConfiguration.OrganizationIntegrationId = integrationId;\n        existingConfiguration.EventType = EventType.User_LoggedIn;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns(existingConfiguration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()\n            .ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .GetByIdAsync(configurationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .ReplaceAsync(updatedConfiguration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                organizationId,\n                integration.Type,\n                existingConfiguration.EventType.Value));\n        // Also verify RemoveByTagAsync was NOT called\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n        Assert.Equal(updatedConfiguration, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WildcardSuccess_UpdatesConfigurationAndInvalidatesCache(\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration existingConfiguration,\n        OrganizationIntegrationConfiguration updatedConfiguration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        integration.Type = IntegrationType.Webhook;\n        existingConfiguration.Id = configurationId;\n        existingConfiguration.OrganizationIntegrationId = integrationId;\n        existingConfiguration.EventType = null;\n        updatedConfiguration.Id = configurationId;\n        updatedConfiguration.OrganizationIntegrationId = integrationId;\n        updatedConfiguration.EventType = null;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns(existingConfiguration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()\n            .ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .GetByIdAsync(configurationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .ReplaceAsync(updatedConfiguration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId,\n                integration.Type));\n        // Also verify RemoveAsync was NOT called\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        Assert.Equal(updatedConfiguration, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ChangedEventType_UpdatesConfigurationAndInvalidatesCacheForBothTypes(\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration existingConfiguration,\n        OrganizationIntegrationConfiguration updatedConfiguration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        integration.Type = IntegrationType.Webhook;\n        existingConfiguration.Id = configurationId;\n        existingConfiguration.OrganizationIntegrationId = integrationId;\n        existingConfiguration.EventType = EventType.User_LoggedIn;\n        updatedConfiguration.Id = configurationId;\n        updatedConfiguration.OrganizationIntegrationId = integrationId;\n        updatedConfiguration.EventType = EventType.Cipher_Created;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns(existingConfiguration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()\n            .ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(true);\n\n        var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .GetByIdAsync(configurationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)\n            .ReplaceAsync(updatedConfiguration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                organizationId,\n                integration.Type,\n                existingConfiguration.EventType.Value));\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveAsync(EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(\n                organizationId,\n                integration.Type,\n                updatedConfiguration.EventType.Value));\n        // Verify RemoveByTagAsync was NOT called since both are specific event types\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n        Assert.Equal(updatedConfiguration, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegrationConfiguration updatedConfiguration)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns((OrganizationIntegration)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .GetByIdAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration updatedConfiguration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = Guid.NewGuid(); // Different organization\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .GetByIdAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ConfigurationDoesNotExist_ThrowsNotFound(\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration updatedConfiguration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns((OrganizationIntegrationConfiguration)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ConfigurationDoesNotBelongToIntegration_ThrowsNotFound(\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration existingConfiguration,\n        OrganizationIntegrationConfiguration updatedConfiguration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        existingConfiguration.Id = configurationId;\n        existingConfiguration.OrganizationIntegrationId = Guid.NewGuid(); // Different integration\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns(existingConfiguration);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ValidationFails_ThrowsBadRequest(\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        Guid configurationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration existingConfiguration,\n        OrganizationIntegrationConfiguration updatedConfiguration)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()\n            .ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(false);\n\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        existingConfiguration.Id = configurationId;\n        existingConfiguration.OrganizationIntegrationId = integrationId;\n        updatedConfiguration.Id = configurationId;\n        updatedConfiguration.OrganizationIntegrationId = integrationId;\n        updatedConfiguration.Template = \"template\";\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(configurationId)\n            .Returns(existingConfiguration);\n\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, configurationId, updatedConfiguration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().DidNotReceive()\n            .ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ChangedFromWildcardToSpecific_InvalidatesAllCaches(\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration existingConfiguration,\n        OrganizationIntegrationConfiguration updatedConfiguration,\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider)\n    {\n        integration.OrganizationId = organizationId;\n        existingConfiguration.OrganizationIntegrationId = integrationId;\n        existingConfiguration.EventType = null; // Wildcard\n        updatedConfiguration.EventType = EventType.User_LoggedIn; // Specific\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId).Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()\n            .ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(true);\n\n        await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration);\n\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId,\n                integration.Type));\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_ChangedFromSpecificToWildcard_InvalidatesAllCaches(\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration,\n        OrganizationIntegrationConfiguration existingConfiguration,\n        OrganizationIntegrationConfiguration updatedConfiguration,\n        SutProvider<UpdateOrganizationIntegrationConfigurationCommand> sutProvider)\n    {\n        integration.OrganizationId = organizationId;\n        existingConfiguration.OrganizationIntegrationId = integrationId;\n        existingConfiguration.EventType = EventType.User_LoggedIn; // Specific\n        updatedConfiguration.EventType = null; // Wildcard\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId).Returns(integration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()\n            .GetByIdAsync(existingConfiguration.Id).Returns(existingConfiguration);\n        sutProvider.GetDependency<IOrganizationIntegrationConfigurationValidator>()\n            .ValidateConfiguration(Arg.Any<IntegrationType>(), Arg.Any<OrganizationIntegrationConfiguration>())\n            .Returns(true);\n\n        await sutProvider.Sut.UpdateAsync(organizationId, integrationId, existingConfiguration.Id, updatedConfiguration);\n\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId,\n                integration.Type));\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveAsync(Arg.Any<string>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/CreateOrganizationIntegrationCommandTests.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;\n\n[SutProviderCustomize]\npublic class CreateOrganizationIntegrationCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task CreateAsync_Success_CreatesIntegrationAndInvalidatesCache(\n        SutProvider<CreateOrganizationIntegrationCommand> sutProvider,\n        OrganizationIntegration integration)\n    {\n        integration.Type = IntegrationType.Webhook;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(integration.OrganizationId)\n            .Returns([]);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .CreateAsync(integration)\n            .Returns(integration);\n\n        var result = await sutProvider.Sut.CreateAsync(integration);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetManyByOrganizationAsync(integration.OrganizationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .CreateAsync(integration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                integration.OrganizationId,\n                integration.Type));\n        Assert.Equal(integration, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_DuplicateType_ThrowsBadRequest(\n        SutProvider<CreateOrganizationIntegrationCommand> sutProvider,\n        OrganizationIntegration integration,\n        OrganizationIntegration existingIntegration)\n    {\n        integration.Type = IntegrationType.Webhook;\n        existingIntegration.Type = IntegrationType.Webhook;\n        existingIntegration.OrganizationId = integration.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(integration.OrganizationId)\n            .Returns([existingIntegration]);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.CreateAsync(integration));\n\n        Assert.Contains(\"An integration of this type already exists\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()\n            .CreateAsync(Arg.Any<OrganizationIntegration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_DifferentType_Success(\n        SutProvider<CreateOrganizationIntegrationCommand> sutProvider,\n        OrganizationIntegration integration,\n        OrganizationIntegration existingIntegration)\n    {\n        integration.Type = IntegrationType.Webhook;\n        existingIntegration.Type = IntegrationType.Slack;\n        existingIntegration.OrganizationId = integration.OrganizationId;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(integration.OrganizationId)\n            .Returns([existingIntegration]);\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .CreateAsync(integration)\n            .Returns(integration);\n\n        var result = await sutProvider.Sut.CreateAsync(integration);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .CreateAsync(integration);\n        Assert.Equal(integration, result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/DeleteOrganizationIntegrationCommandTests.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;\n\n[SutProviderCustomize]\npublic class DeleteOrganizationIntegrationCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_Success_DeletesIntegrationAndInvalidatesCache(\n        SutProvider<DeleteOrganizationIntegrationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = organizationId;\n        integration.Type = IntegrationType.Webhook;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n\n        await sutProvider.Sut.DeleteAsync(organizationId, integrationId);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .DeleteAsync(integration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId,\n                integration.Type));\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(\n        SutProvider<DeleteOrganizationIntegrationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns((OrganizationIntegration)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DeleteAsync(organizationId, integrationId));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()\n            .DeleteAsync(Arg.Any<OrganizationIntegration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(\n        SutProvider<DeleteOrganizationIntegrationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration integration)\n    {\n        integration.Id = integrationId;\n        integration.OrganizationId = Guid.NewGuid(); // Different organization\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(integration);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.DeleteAsync(organizationId, integrationId));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()\n            .DeleteAsync(Arg.Any<OrganizationIntegration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/GetOrganizationIntegrationsQueryTests.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;\n\n[SutProviderCustomize]\npublic class GetOrganizationIntegrationsQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task GetManyByOrganizationAsync_CallsRepository(\n        SutProvider<GetOrganizationIntegrationsQuery> sutProvider,\n        Guid organizationId,\n        List<OrganizationIntegration> integrations)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns(integrations);\n\n        var result = await sutProvider.Sut.GetManyByOrganizationAsync(organizationId);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetManyByOrganizationAsync(organizationId);\n        Assert.Equal(integrations.Count, result.Count);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetManyByOrganizationAsync_NoIntegrations_ReturnsEmptyList(\n        SutProvider<GetOrganizationIntegrationsQuery> sutProvider,\n        Guid organizationId)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetManyByOrganizationAsync(organizationId)\n            .Returns([]);\n\n        var result = await sutProvider.Sut.GetManyByOrganizationAsync(organizationId);\n\n        Assert.Empty(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/EventIntegrations/OrganizationIntegrations/UpdateOrganizationIntegrationCommandTests.cs",
    "content": "﻿using Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.EventIntegrations.OrganizationIntegrations;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationIntegrationCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_Success_UpdatesIntegrationAndInvalidatesCache(\n        SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration existingIntegration,\n        OrganizationIntegration updatedIntegration)\n    {\n        existingIntegration.Id = integrationId;\n        existingIntegration.OrganizationId = organizationId;\n        existingIntegration.Type = IntegrationType.Webhook;\n        updatedIntegration.Id = integrationId;\n        updatedIntegration.OrganizationId = organizationId;\n        updatedIntegration.Type = IntegrationType.Webhook;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(existingIntegration);\n\n        var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration);\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .GetByIdAsync(integrationId);\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)\n            .ReplaceAsync(updatedIntegration);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n                organizationId,\n                existingIntegration.Type));\n        Assert.Equal(updatedIntegration, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(\n        SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration updatedIntegration)\n    {\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns((OrganizationIntegration)null);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()\n            .ReplaceAsync(Arg.Any<OrganizationIntegration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(\n        SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration existingIntegration,\n        OrganizationIntegration updatedIntegration)\n    {\n        existingIntegration.Id = integrationId;\n        existingIntegration.OrganizationId = Guid.NewGuid(); // Different organization\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(existingIntegration);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()\n            .ReplaceAsync(Arg.Any<OrganizationIntegration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_IntegrationIsDifferentType_ThrowsNotFound(\n        SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,\n        Guid organizationId,\n        Guid integrationId,\n        OrganizationIntegration existingIntegration,\n        OrganizationIntegration updatedIntegration)\n    {\n        existingIntegration.Id = integrationId;\n        existingIntegration.OrganizationId = organizationId;\n        existingIntegration.Type = IntegrationType.Webhook;\n        updatedIntegration.Id = integrationId;\n        updatedIntegration.OrganizationId = organizationId;\n        updatedIntegration.Type = IntegrationType.Hec; // Different Type\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByIdAsync(integrationId)\n            .Returns(existingIntegration);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration));\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()\n            .ReplaceAsync(Arg.Any<OrganizationIntegration>());\n        await sutProvider.GetDependency<IFusionCache>().DidNotReceive()\n            .RemoveByTagAsync(Arg.Any<string>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationHandlerResultTests.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\n\npublic class IntegrationHandlerResultTests\n{\n    [Theory, BitAutoData]\n    public void Succeed_SetsSuccessTrue_CategoryNull(IntegrationMessage message)\n    {\n        var result = IntegrationHandlerResult.Succeed(message);\n\n        Assert.True(result.Success);\n        Assert.Null(result.Category);\n        Assert.Equal(message, result.Message);\n        Assert.Null(result.FailureReason);\n    }\n\n    [Theory, BitAutoData]\n    public void Fail_WithCategory_SetsSuccessFalse_CategorySet(IntegrationMessage message)\n    {\n        var category = IntegrationFailureCategory.AuthenticationFailed;\n        var failureReason = \"Invalid credentials\";\n\n        var result = IntegrationHandlerResult.Fail(message, category, failureReason);\n\n        Assert.False(result.Success);\n        Assert.Equal(category, result.Category);\n        Assert.Equal(failureReason, result.FailureReason);\n        Assert.Equal(message, result.Message);\n    }\n\n    [Theory, BitAutoData]\n    public void Fail_WithDelayUntil_SetsDelayUntilDate(IntegrationMessage message)\n    {\n        var delayUntil = DateTime.UtcNow.AddMinutes(5);\n\n        var result = IntegrationHandlerResult.Fail(\n            message,\n            IntegrationFailureCategory.RateLimited,\n            \"Rate limited\",\n            delayUntil\n        );\n\n        Assert.Equal(delayUntil, result.DelayUntilDate);\n    }\n\n    [Theory, BitAutoData]\n    public void Retryable_RateLimited_ReturnsTrue(IntegrationMessage message)\n    {\n        var result = IntegrationHandlerResult.Fail(\n            message,\n            IntegrationFailureCategory.RateLimited,\n            \"Rate limited\"\n        );\n\n        Assert.True(result.Retryable);\n    }\n\n    [Theory, BitAutoData]\n    public void Retryable_TransientError_ReturnsTrue(IntegrationMessage message)\n    {\n        var result = IntegrationHandlerResult.Fail(\n            message,\n            IntegrationFailureCategory.TransientError,\n            \"Temporary network issue\"\n        );\n\n        Assert.True(result.Retryable);\n    }\n\n    [Theory, BitAutoData]\n    public void Retryable_AuthenticationFailed_ReturnsFalse(IntegrationMessage message)\n    {\n        var result = IntegrationHandlerResult.Fail(\n            message,\n            IntegrationFailureCategory.AuthenticationFailed,\n            \"Invalid token\"\n        );\n\n        Assert.False(result.Retryable);\n    }\n\n    [Theory, BitAutoData]\n    public void Retryable_ConfigurationError_ReturnsFalse(IntegrationMessage message)\n    {\n        var result = IntegrationHandlerResult.Fail(\n            message,\n            IntegrationFailureCategory.ConfigurationError,\n            \"Channel not found\"\n        );\n\n        Assert.False(result.Retryable);\n    }\n\n    [Theory, BitAutoData]\n    public void Retryable_ServiceUnavailable_ReturnsTrue(IntegrationMessage message)\n    {\n        var result = IntegrationHandlerResult.Fail(\n            message,\n            IntegrationFailureCategory.ServiceUnavailable,\n            \"Service is down\"\n        );\n\n        Assert.True(result.Retryable);\n    }\n\n    [Theory, BitAutoData]\n    public void Retryable_PermanentFailure_ReturnsFalse(IntegrationMessage message)\n    {\n        var result = IntegrationHandlerResult.Fail(\n            message,\n            IntegrationFailureCategory.PermanentFailure,\n            \"Permanent failure\"\n        );\n\n        Assert.False(result.Retryable);\n    }\n\n    [Theory, BitAutoData]\n    public void Retryable_SuccessCase_ReturnsFalse(IntegrationMessage message)\n    {\n        var result = IntegrationHandlerResult.Succeed(message);\n\n        Assert.False(result.Retryable);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationMessageTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\n\npublic class IntegrationMessageTests\n{\n    private const string _messageId = \"TestMessageId\";\n    private const string _organizationId = \"TestOrganizationId\";\n\n    [Fact]\n    public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate()\n    {\n        var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>\n        {\n            Configuration = new WebhookIntegrationConfigurationDetails(new Uri(\"https://localhost\"), \"Bearer\", \"AUTH-TOKEN\"),\n            MessageId = _messageId,\n            OrganizationId = _organizationId,\n            RetryCount = 2,\n            RenderedTemplate = string.Empty,\n            DelayUntilDate = null\n        };\n\n        var baseline = DateTime.UtcNow;\n        message.ApplyRetry(baseline);\n\n        Assert.Equal(3, message.RetryCount);\n        Assert.NotNull(message.DelayUntilDate);\n        Assert.True(message.DelayUntilDate > baseline);\n    }\n\n    [Fact]\n    public void FromToJson_SerializesCorrectly()\n    {\n        var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>\n        {\n            Configuration = new WebhookIntegrationConfigurationDetails(new Uri(\"https://localhost\"), \"Bearer\", \"AUTH-TOKEN\"),\n            MessageId = _messageId,\n            OrganizationId = _organizationId,\n            RenderedTemplate = \"This is the message\",\n            IntegrationType = IntegrationType.Webhook,\n            RetryCount = 2,\n            DelayUntilDate = DateTime.UtcNow\n        };\n\n        var json = message.ToJson();\n        var result = IntegrationMessage<WebhookIntegrationConfigurationDetails>.FromJson(json);\n\n        Assert.NotNull(result);\n        Assert.Equal(message.Configuration, result.Configuration);\n        Assert.Equal(message.MessageId, result.MessageId);\n        Assert.Equal(message.OrganizationId, result.OrganizationId);\n        Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);\n        Assert.Equal(message.IntegrationType, result.IntegrationType);\n        Assert.Equal(message.RetryCount, result.RetryCount);\n        Assert.Equal(message.DelayUntilDate, result.DelayUntilDate);\n    }\n\n    [Fact]\n    public void FromJson_InvalidJson_ThrowsJsonException()\n    {\n        var json = \"{ Invalid JSON\";\n        Assert.Throws<JsonException>(() => IntegrationMessage<WebhookIntegrationConfigurationDetails>.FromJson(json));\n    }\n\n    [Fact]\n    public void ToJson_BaseIntegrationMessage_DeserializesCorrectly()\n    {\n        var message = new IntegrationMessage\n        {\n            MessageId = _messageId,\n            OrganizationId = _organizationId,\n            RenderedTemplate = \"This is the message\",\n            IntegrationType = IntegrationType.Webhook,\n            RetryCount = 2,\n            DelayUntilDate = DateTime.UtcNow\n        };\n\n        var json = message.ToJson();\n        var result = JsonSerializer.Deserialize<IntegrationMessage>(json);\n\n        Assert.Equal(message.MessageId, result.MessageId);\n        Assert.Equal(message.OrganizationId, result.OrganizationId);\n        Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);\n        Assert.Equal(message.IntegrationType, result.IntegrationType);\n        Assert.Equal(message.RetryCount, result.RetryCount);\n        Assert.Equal(message.DelayUntilDate, result.DelayUntilDate);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationOAuthStateTests.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Time.Testing;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\n\npublic class IntegrationOAuthStateTests\n{\n    private readonly FakeTimeProvider _fakeTimeProvider = new(\n        new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc)\n    );\n\n    [Theory, BitAutoData]\n    public void FromIntegration_ToString_RoundTripsCorrectly(OrganizationIntegration integration)\n    {\n        var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);\n        var parsed = IntegrationOAuthState.FromString(state.ToString(), _fakeTimeProvider);\n\n        Assert.NotNull(parsed);\n        Assert.Equal(state.IntegrationId, parsed.IntegrationId);\n        Assert.True(parsed.ValidateOrg(integration.OrganizationId));\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(\"   \")]\n    [InlineData(\"not-a-valid-state\")]\n    public void FromString_InvalidString_ReturnsNull(string state)\n    {\n        var parsed = IntegrationOAuthState.FromString(state, _fakeTimeProvider);\n\n        Assert.Null(parsed);\n    }\n\n    [Fact]\n    public void FromString_InvalidGuid_ReturnsNull()\n    {\n        var badState = $\"not-a-guid.ABCD1234.1706313600\";\n\n        var parsed = IntegrationOAuthState.FromString(badState, _fakeTimeProvider);\n\n        Assert.Null(parsed);\n    }\n\n    [Theory, BitAutoData]\n    public void FromString_ExpiredState_ReturnsNull(OrganizationIntegration integration)\n    {\n        var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);\n\n        // Advance time 30 minutes to exceed the 20-minute max age\n        _fakeTimeProvider.Advance(TimeSpan.FromMinutes(30));\n\n        var parsed = IntegrationOAuthState.FromString(state.ToString(), _fakeTimeProvider);\n\n        Assert.Null(parsed);\n    }\n\n    [Theory, BitAutoData]\n    public void ValidateOrg_WithCorrectOrgId_ReturnsTrue(OrganizationIntegration integration)\n    {\n        var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);\n\n        Assert.True(state.ValidateOrg(integration.OrganizationId));\n    }\n\n    [Theory, BitAutoData]\n    public void ValidateOrg_WithWrongOrgId_ReturnsFalse(OrganizationIntegration integration)\n    {\n        var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);\n\n        Assert.False(state.ValidateOrg(Guid.NewGuid()));\n    }\n\n    [Theory, BitAutoData]\n    public void ValidateOrg_ModifiedTimestamp_ReturnsFalse(OrganizationIntegration integration)\n    {\n        var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);\n        var parts = state.ToString().Split('.');\n\n        parts[2] = $\"{_fakeTimeProvider.GetUtcNow().ToUnixTimeSeconds() - 1}\";\n        var modifiedState = IntegrationOAuthState.FromString(string.Join(\".\", parts), _fakeTimeProvider);\n\n        Assert.True(state.ValidateOrg(integration.OrganizationId));\n        Assert.NotNull(modifiedState);\n        Assert.False(modifiedState.ValidateOrg(integration.OrganizationId));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs",
    "content": "﻿#nullable enable\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\n\npublic class IntegrationTemplateContextTests\n{\n    [Theory, BitAutoData]\n    public void EventMessage_ReturnsSerializedJsonOfEvent(EventMessage eventMessage)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage: eventMessage);\n        var expected = JsonSerializer.Serialize(eventMessage);\n\n        Assert.Equal(expected, sut.EventMessage);\n    }\n\n    [Theory, BitAutoData]\n    public void DateIso8601_ReturnsIso8601FormattedDate(EventMessage eventMessage)\n    {\n        var testDate = new DateTime(2025, 10, 27, 13, 30, 0, DateTimeKind.Utc);\n        eventMessage.Date = testDate;\n        var sut = new IntegrationTemplateContext(eventMessage);\n\n        var result = sut.DateIso8601;\n\n        Assert.Equal(\"2025-10-27T13:30:00.0000000Z\", result);\n        // Verify it's valid ISO 8601\n        Assert.True(DateTime.TryParse(result, out _));\n    }\n\n    [Theory, BitAutoData]\n    public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails user)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { User = user };\n\n        Assert.Equal(user.Name, sut.UserName);\n    }\n\n    [Theory, BitAutoData]\n    public void UserName_WhenUserIsNull_ReturnsNull(EventMessage eventMessage)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { User = null };\n\n        Assert.Null(sut.UserName);\n    }\n\n    [Theory, BitAutoData]\n    public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails user)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { User = user };\n\n        Assert.Equal(user.Email, sut.UserEmail);\n    }\n\n    [Theory, BitAutoData]\n    public void UserEmail_WhenUserIsNull_ReturnsNull(EventMessage eventMessage)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { User = null };\n\n        Assert.Null(sut.UserEmail);\n    }\n\n    [Theory, BitAutoData]\n    public void UserType_WhenUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails user)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { User = user };\n\n        Assert.Equal(user.Type, sut.UserType);\n    }\n\n    [Theory, BitAutoData]\n    public void UserType_WhenUserIsNull_ReturnsNull(EventMessage eventMessage)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { User = null };\n\n        Assert.Null(sut.UserType);\n    }\n\n    [Theory, BitAutoData]\n    public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails actingUser)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };\n\n        Assert.Equal(actingUser.Name, sut.ActingUserName);\n    }\n\n    [Theory, BitAutoData]\n    public void ActingUserName_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null };\n\n        Assert.Null(sut.ActingUserName);\n    }\n\n    [Theory, BitAutoData]\n    public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails actingUser)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };\n\n        Assert.Equal(actingUser.Email, sut.ActingUserEmail);\n    }\n\n    [Theory, BitAutoData]\n    public void ActingUserEmail_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null };\n\n        Assert.Null(sut.ActingUserEmail);\n    }\n\n    [Theory, BitAutoData]\n    public void ActingUserType_WhenActingUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails actingUser)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };\n\n        Assert.Equal(actingUser.Type, sut.ActingUserType);\n    }\n\n    [Theory, BitAutoData]\n    public void ActingUserType_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null };\n\n        Assert.Null(sut.ActingUserType);\n    }\n\n    [Theory, BitAutoData]\n    public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { Organization = organization };\n\n        Assert.Equal(organization.DisplayName(), sut.OrganizationName);\n    }\n\n    [Theory, BitAutoData]\n    public void OrganizationName_WhenOrganizationIsNull_ReturnsNull(EventMessage eventMessage)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { Organization = null };\n\n        Assert.Null(sut.OrganizationName);\n    }\n\n    [Theory, BitAutoData]\n    public void GroupName_WhenGroupIsSet_ReturnsName(EventMessage eventMessage, Group group)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { Group = group };\n\n        Assert.Equal(group.Name, sut.GroupName);\n    }\n\n    [Theory, BitAutoData]\n    public void GroupName_WhenGroupIsNull_ReturnsNull(EventMessage eventMessage)\n    {\n        var sut = new IntegrationTemplateContext(eventMessage) { Group = null };\n\n        Assert.Null(sut.GroupName);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Models/Data/EventIntegrations/OrganizationIntegrationConfigurationDetailsTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\n\npublic class OrganizationIntegrationConfigurationDetailsTests\n{\n    [Fact]\n    public void MergedConfiguration_WithValidConfigAndIntegration_ReturnsMergedJson()\n    {\n        var config = new { config = \"A new config value\" };\n        var integration = new { integration = \"An integration value\" };\n        var expectedObj = new { integration = \"An integration value\", config = \"A new config value\" };\n        var expected = JsonSerializer.Serialize(expectedObj);\n\n        var sut = new OrganizationIntegrationConfigurationDetails();\n        sut.Configuration = JsonSerializer.Serialize(config);\n        sut.IntegrationConfiguration = JsonSerializer.Serialize(integration);\n\n        var result = sut.MergedConfiguration;\n        Assert.Equal(expected, result.ToJsonString());\n    }\n\n    [Fact]\n    public void MergedConfiguration_WithSameKeyIndConfigAndIntegration_GivesPrecedenceToConfiguration()\n    {\n        var config = new { config = \"A new config value\" };\n        var integration = new { config = \"An integration value\" };\n        var expectedObj = new { config = \"A new config value\" };\n        var expected = JsonSerializer.Serialize(expectedObj);\n\n        var sut = new OrganizationIntegrationConfigurationDetails();\n        sut.Configuration = JsonSerializer.Serialize(config);\n        sut.IntegrationConfiguration = JsonSerializer.Serialize(integration);\n\n        var result = sut.MergedConfiguration;\n        Assert.Equal(expected, result.ToJsonString());\n    }\n\n    [Fact]\n    public void MergedConfiguration_WithInvalidJsonConfigAndIntegration_ReturnsEmptyJson()\n    {\n        var expectedObj = new { };\n        var expected = JsonSerializer.Serialize(expectedObj);\n\n        var sut = new OrganizationIntegrationConfigurationDetails();\n        sut.Configuration = \"Not JSON\";\n        sut.IntegrationConfiguration = \"Not JSON\";\n\n        var result = sut.MergedConfiguration;\n        Assert.Equal(expected, result.ToJsonString());\n    }\n\n    [Fact]\n    public void MergedConfiguration_WithNullConfigAndIntegration_ReturnsEmptyJson()\n    {\n        var expectedObj = new { };\n        var expected = JsonSerializer.Serialize(expectedObj);\n\n        var sut = new OrganizationIntegrationConfigurationDetails();\n        sut.Configuration = null;\n        sut.IntegrationConfiguration = null;\n\n        var result = sut.MergedConfiguration;\n        Assert.Equal(expected, result.ToJsonString());\n    }\n\n    [Fact]\n    public void MergedConfiguration_WithValidIntegrationAndNullConfig_ReturnsIntegrationJson()\n    {\n        var integration = new { integration = \"An integration value\" };\n        var expectedObj = new { integration = \"An integration value\" };\n        var expected = JsonSerializer.Serialize(expectedObj);\n\n        var sut = new OrganizationIntegrationConfigurationDetails();\n        sut.Configuration = null;\n        sut.IntegrationConfiguration = JsonSerializer.Serialize(integration);\n\n        var result = sut.MergedConfiguration;\n        Assert.Equal(expected, result.ToJsonString());\n    }\n\n    [Fact]\n    public void MergedConfiguration_WithValidConfigAndNullIntegration_ReturnsConfigJson()\n    {\n        var config = new { config = \"A new config value\" };\n        var expectedObj = new { config = \"A new config value\" };\n        var expected = JsonSerializer.Serialize(expectedObj);\n\n        var sut = new OrganizationIntegrationConfigurationDetails();\n        sut.Configuration = JsonSerializer.Serialize(config);\n        sut.IntegrationConfiguration = null;\n\n        var result = sut.MergedConfiguration;\n        Assert.Equal(expected, result.ToJsonString());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Models/Data/EventIntegrations/TestListenerConfiguration.cs",
    "content": "﻿using Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\n\nnamespace Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\n\npublic class TestListenerConfiguration : IIntegrationListenerConfiguration\n{\n    public string EventQueueName => \"event_queue\";\n    public string EventSubscriptionName => \"event_subscription\";\n    public string EventTopicName => \"event_topic\";\n    public IntegrationType IntegrationType => IntegrationType.Webhook;\n    public string IntegrationQueueName => \"integration_queue\";\n    public string IntegrationRetryQueueName => \"integration_retry_queue\";\n    public string IntegrationSubscriptionName => \"integration_subscription\";\n    public string IntegrationTopicName => \"integration_topic\";\n    public int MaxRetries => 3;\n    public int EventMaxConcurrentCalls => 1;\n    public int EventPrefetchCount => 0;\n    public int IntegrationMaxConcurrentCalls => 1;\n    public int IntegrationPrefetchCount => 0;\n    public string RoutingKey => IntegrationType.ToRoutingKey();\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Models/Data/ReportFileTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Models.Data;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Models.Data;\n\npublic class ReportFileTests\n{\n    [Fact]\n    public void DefaultValues_AreCorrect()\n    {\n        var data = new ReportFile();\n\n        Assert.Null(data.Id);\n        Assert.Equal(string.Empty, data.FileName);\n        Assert.Equal(0, data.Size);\n        Assert.True(data.Validated);\n    }\n\n    [Fact]\n    public void Serialize_RoundTrip_PreservesAllProperties()\n    {\n        var original = new ReportFile\n        {\n            Id = \"file-123\",\n            FileName = \"report.json\",\n            Size = 1048576,\n            Validated = false\n        };\n\n        var json = JsonSerializer.Serialize(original);\n        var deserialized = JsonSerializer.Deserialize<ReportFile>(json);\n\n        Assert.NotNull(deserialized);\n        Assert.Equal(original.Id, deserialized.Id);\n        Assert.Equal(original.FileName, deserialized.FileName);\n        Assert.Equal(original.Size, deserialized.Size);\n        Assert.Equal(original.Validated, deserialized.Validated);\n    }\n\n    [Fact]\n    public void Serialize_SizeIsWrittenAsString()\n    {\n        var data = new ReportFile\n        {\n            Id = \"file-456\",\n            FileName = \"report.json\",\n            Size = 9876543210\n        };\n\n        var json = JsonSerializer.Serialize(data);\n\n        Assert.Contains(\"\\\"9876543210\\\"\", json);\n    }\n\n    [Fact]\n    public void Deserialize_SizeCanBeReadFromString()\n    {\n        var json = \"\"\"{\"Id\":\"file-789\",\"FileName\":\"test.json\",\"Size\":\"5000\",\"Validated\":true}\"\"\";\n\n        var data = JsonSerializer.Deserialize<ReportFile>(json);\n\n        Assert.NotNull(data);\n        Assert.Equal(5000, data.Size);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Models/Data/Teams/TeamsBotCredentialProviderTests.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.Teams;\nusing Microsoft.Bot.Connector.Authentication;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Models.Data.Teams;\n\npublic class TeamsBotCredentialProviderTests\n{\n    private string _clientId = \"client id\";\n    private string _clientSecret = \"client secret\";\n\n    [Fact]\n    public async Task IsValidAppId_MustMatchClientId()\n    {\n        var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);\n\n        Assert.True(await sut.IsValidAppIdAsync(_clientId));\n        Assert.False(await sut.IsValidAppIdAsync(\"Different id\"));\n    }\n\n    [Fact]\n    public async Task GetAppPasswordAsync_MatchingClientId_ReturnsClientSecret()\n    {\n        var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);\n        var password = await sut.GetAppPasswordAsync(_clientId);\n        Assert.Equal(_clientSecret, password);\n    }\n\n    [Fact]\n    public async Task GetAppPasswordAsync_NotMatchingClientId_ReturnsNull()\n    {\n        var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);\n        Assert.Null(await sut.GetAppPasswordAsync(\"Different id\"));\n    }\n\n    [Fact]\n    public async Task IsAuthenticationDisabledAsync_ReturnsFalse()\n    {\n        var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);\n        Assert.False(await sut.IsAuthenticationDisabledAsync());\n    }\n\n    [Fact]\n    public async Task ValidateIssuerAsync_ExpectedIssuer_ReturnsTrue()\n    {\n        var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);\n        Assert.True(await sut.ValidateIssuerAsync(AuthenticationConstants.ToBotFromChannelTokenIssuer));\n    }\n\n    [Fact]\n    public async Task ValidateIssuerAsync_UnexpectedIssuer_ReturnsFalse()\n    {\n        var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret);\n        Assert.False(await sut.ValidateIssuerAsync(\"unexpected issuer\"));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/AddOrganizationReportCommandTests.cs",
    "content": "﻿\nusing AutoFixture;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class AddOrganizationReportCommandTests\n{\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddOrganizationReportAsync_ShouldReturnOrganizationReport(\n        SutProvider<AddOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<AddOrganizationReportRequest>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(fixture.Create<Organization>());\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .CreateAsync(Arg.Any<OrganizationReport>())\n            .Returns(c => c.Arg<OrganizationReport>());\n\n        // Act\n        var result = await sutProvider.Sut.AddOrganizationReportAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowError(\n        SutProvider<AddOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<AddOrganizationReportRequest>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(null as Organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));\n        Assert.Equal(\"Invalid Organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddOrganizationReportAsync_WithInvalidUrl_ShouldThrowError(\n        SutProvider<AddOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<AddOrganizationReportRequest>()\n                        .Without(_ => _.ReportData)\n                        .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(fixture.Create<Organization>());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));\n        Assert.Equal(\"Report Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddOrganizationReportAsync_Multiples_WithInvalidOrganizationId_ShouldThrowError(\n        SutProvider<AddOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<AddOrganizationReportRequest>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(null as Organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));\n        Assert.Equal(\"Invalid Organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddOrganizationReportAsync_Multiples_WithInvalidUrl_ShouldThrowError(\n        SutProvider<AddOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<AddOrganizationReportRequest>()\n                        .Without(_ => _.ReportData)\n                        .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(fixture.Create<Organization>());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));\n        Assert.Equal(\"Report Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddOrganizationReportAsync_WithNullOrganizationId_ShouldThrowError(\n        SutProvider<AddOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<AddOrganizationReportRequest>()\n            .With(x => x.OrganizationId, default(Guid))\n            .Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddOrganizationReportAsync(request));\n        Assert.Equal(\"Invalid Organization\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class AddPasswordHealthReportApplicationCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task AddPasswordHealthReportApplicationAsync_WithValidRequest_ShouldReturnPasswordHealthReportApplication(\n        SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<AddPasswordHealthReportApplicationRequest>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(fixture.Create<Organization>());\n\n        sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .CreateAsync(Arg.Any<PasswordHealthReportApplication>())\n            .Returns(c => c.Arg<PasswordHealthReportApplication>());\n\n        // Act\n        var result = await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddPasswordHealthReportApplicationAsync_WithInvalidOrganizationId_ShouldThrowError(\n        SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<AddPasswordHealthReportApplicationRequest>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(null as Organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request));\n        Assert.Equal(\"Invalid Organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddPasswordHealthReportApplicationAsync_WithInvalidUrl_ShouldThrowError(\n        SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<AddPasswordHealthReportApplicationRequest>()\n                        .Without(_ => _.Url)\n                        .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(fixture.Create<Organization>());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request));\n        Assert.Equal(\"URL is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddPasswordHealthReportApplicationAsync_Multiples_WithInvalidOrganizationId_ShouldThrowError(\n        SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<AddPasswordHealthReportApplicationRequest>()\n                        .Without(_ => _.OrganizationId)\n                        .CreateMany(2).ToList();\n\n        request[0].OrganizationId = Guid.NewGuid();\n        request[1].OrganizationId = Guid.Empty;\n\n        // only return an organization for the first request and null for the second\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Is<Guid>(x => x == request[0].OrganizationId))\n            .Returns(fixture.Create<Organization>());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request));\n        Assert.Equal(\"Invalid Organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddPasswordHealthReportApplicationAsync_Multiples_WithInvalidUrl_ShouldThrowError(\n        SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<AddPasswordHealthReportApplicationRequest>()\n                        .CreateMany(2).ToList();\n\n        request[1].Url = string.Empty;\n\n        // return an organization for both requests\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(fixture.Create<Organization>());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request));\n        Assert.Equal(\"URL is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AddPasswordHealthReportApplicationAsync_Multiples_WithValidRequest_ShouldBeSuccessful(\n    SutProvider<AddPasswordHealthReportApplicationCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.CreateMany<AddPasswordHealthReportApplicationRequest>(2);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(Arg.Any<Guid>())\n            .Returns(fixture.Create<Organization>());\n\n        sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .CreateAsync(Arg.Any<PasswordHealthReportApplication>())\n            .Returns(c => c.Arg<PasswordHealthReportApplication>());\n\n        // Act\n        var result = await sutProvider.Sut.AddPasswordHealthReportApplicationAsync(request);\n\n        // Assert\n        Assert.True(result.Count() == 2);\n        sutProvider.GetDependency<IOrganizationRepository>().Received(2);\n        sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>().Received(2);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class DeletePasswordHealthReportApplicationCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task DropPasswordHealthReportApplicationAsync_withValidRequest_Success(\n        SutProvider<DropPasswordHealthReportApplicationCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var passwordHealthReportApplications = fixture.CreateMany<PasswordHealthReportApplication>(2).ToList();\n        // only take one id from the list - we only want to drop one record\n        var request = fixture.Build<DropPasswordHealthReportApplicationRequest>()\n                        .With(x => x.PasswordHealthReportApplicationIds,\n                            passwordHealthReportApplications.Select(x => x.Id).Take(1).ToList())\n                        .Create();\n\n        sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .GetByOrganizationIdAsync(Arg.Any<Guid>())\n            .Returns(passwordHealthReportApplications);\n\n        // Act\n        await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request);\n\n        // Assert\n        await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .Received(1)\n            .GetByOrganizationIdAsync(request.OrganizationId);\n\n        await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .Received(1)\n            .DeleteAsync(Arg.Is<PasswordHealthReportApplication>(_ =>\n                request.PasswordHealthReportApplicationIds.Contains(_.Id)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task DropPasswordHealthReportApplicationAsync_withValidRequest_nothingToDrop(\n        SutProvider<DropPasswordHealthReportApplicationCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var passwordHealthReportApplications = fixture.CreateMany<PasswordHealthReportApplication>(2).ToList();\n        // we are passing invalid data\n        var request = fixture.Build<DropPasswordHealthReportApplicationRequest>()\n                .With(x => x.PasswordHealthReportApplicationIds, new List<Guid> { Guid.NewGuid() })\n                        .Create();\n\n        sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .GetByOrganizationIdAsync(Arg.Any<Guid>())\n            .Returns(passwordHealthReportApplications);\n\n        // Act\n        await sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request);\n\n        // Assert\n        await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .Received(1)\n            .GetByOrganizationIdAsync(request.OrganizationId);\n\n        await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .Received(0)\n            .DeleteAsync(Arg.Any<PasswordHealthReportApplication>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task DropPasswordHealthReportApplicationAsync_withNodata_fails(\n        SutProvider<DropPasswordHealthReportApplicationCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        // we are passing invalid data\n        var request = fixture.Build<DropPasswordHealthReportApplicationRequest>()\n                .Create();\n\n        sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .GetByOrganizationIdAsync(Arg.Any<Guid>())\n            .Returns(null as List<PasswordHealthReportApplication>);\n\n        // Act\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n                sutProvider.Sut.DropPasswordHealthReportApplicationAsync(request));\n\n        // Assert\n        await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .Received(1)\n            .GetByOrganizationIdAsync(request.OrganizationId);\n\n        await sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .Received(0)\n            .DeleteAsync(Arg.Any<PasswordHealthReportApplication>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportApplicationDataQueryTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class GetOrganizationReportApplicationDataQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportApplicationDataAsync_WithValidParams_ShouldReturnApplicationData(\n        SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var organizationId = fixture.Create<Guid>();\n        var reportId = fixture.Create<Guid>();\n        var applicationDataResponse = fixture.Build<OrganizationReportApplicationDataResponse>()\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetApplicationDataAsync(reportId)\n            .Returns(applicationDataResponse);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId);\n\n        // Assert\n        Assert.NotNull(result);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(1).GetApplicationDataAsync(reportId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportApplicationDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)\n    {\n        // Arrange\n        var reportId = Guid.NewGuid();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(Guid.Empty, reportId));\n\n        Assert.Equal(\"OrganizationId is required.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().GetApplicationDataAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportApplicationDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(\n        SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, Guid.Empty));\n\n        Assert.Equal(\"ReportId is required.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().GetApplicationDataAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportApplicationDataAsync_WhenDataNotFound_ShouldThrowNotFoundException(\n        SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var reportId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetApplicationDataAsync(reportId)\n            .Returns((OrganizationReportApplicationDataResponse)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId));\n\n        Assert.Equal(\"Organization report application data not found.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportApplicationDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(\n        SutProvider<GetOrganizationReportApplicationDataQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var reportId = Guid.NewGuid();\n        var expectedMessage = \"Database connection failed\";\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetApplicationDataAsync(reportId)\n            .Throws(new InvalidOperationException(expectedMessage));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId));\n\n        Assert.Equal(expectedMessage, exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportDataQueryTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class GetOrganizationReportDataQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportDataAsync_WithValidParams_ShouldReturnReportData(\n        SutProvider<GetOrganizationReportDataQuery> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var organizationId = fixture.Create<Guid>();\n        var reportId = fixture.Create<Guid>();\n        var reportDataResponse = fixture.Build<OrganizationReportDataResponse>()\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetReportDataAsync(reportId)\n            .Returns(reportDataResponse);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId);\n\n        // Assert\n        Assert.NotNull(result);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(1).GetReportDataAsync(reportId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<GetOrganizationReportDataQuery> sutProvider)\n    {\n        // Arrange\n        var reportId = Guid.NewGuid();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportDataAsync(Guid.Empty, reportId));\n\n        Assert.Equal(\"OrganizationId is required.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().GetReportDataAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(\n        SutProvider<GetOrganizationReportDataQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, Guid.Empty));\n\n        Assert.Equal(\"ReportId is required.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().GetReportDataAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportDataAsync_WhenDataNotFound_ShouldThrowNotFoundException(\n        SutProvider<GetOrganizationReportDataQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var reportId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetReportDataAsync(reportId)\n            .Returns((OrganizationReportDataResponse)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId));\n\n        Assert.Equal(\"Organization report data not found.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(\n        SutProvider<GetOrganizationReportDataQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var reportId = Guid.NewGuid();\n        var expectedMessage = \"Database connection failed\";\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetReportDataAsync(reportId)\n            .Throws(new InvalidOperationException(expectedMessage));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId));\n\n        Assert.Equal(expectedMessage, exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQueryTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class GetOrganizationReportSummaryDataByDateRangeQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithValidParams_ShouldReturnSummaryData(\n        SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var organizationId = fixture.Create<Guid>();\n        var reportId = fixture.Create<Guid>();\n        var startDate = DateTime.UtcNow.AddDays(-30);\n        var endDate = DateTime.UtcNow;\n        var summaryDataList = fixture.Build<OrganizationReportSummaryDataResponse>()\n            .CreateMany(3).ToList();\n        summaryDataList[0].RevisionDate = DateTime.UtcNow; // most recent\n        summaryDataList[1].RevisionDate = DateTime.UtcNow.AddDays(-1);\n        summaryDataList[2].RevisionDate = DateTime.UtcNow.AddDays(-2);\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetSummaryDataByDateRangeAsync(Arg.Any<Guid>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())\n            .Returns(summaryDataList);\n\n        sutProvider\n            .GetDependency<IFusionCache>()\n            .GetOrSetAsync(\n                key: Arg.Any<string?>(),\n                factory: Arg.Any<Func<object, CancellationToken, Task<IEnumerable<OrganizationReportSummaryDataResponse>>>>(),\n                options: Arg.Any<FusionCacheEntryOptions>(),\n                tags: Arg.Any<IEnumerable<string>>())\n            .Returns(callInfo =>\n            {\n                var factory = callInfo.ArgAt<Func<FusionCacheFactoryExecutionContext<IEnumerable<OrganizationReportSummaryDataResponse>>, CancellationToken, Task<IEnumerable<OrganizationReportSummaryDataResponse>>>>(1);\n                return new ValueTask<IEnumerable<OrganizationReportSummaryDataResponse>>(factory.Invoke(null, CancellationToken.None));\n            });\n\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(3, result.Count());\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(1).GetSummaryDataByDateRangeAsync(Arg.Any<Guid>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataByDateRangeAsync_ShouldReturnTopSixResults(\n        SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var organizationId = fixture.Create<Guid>();\n        var reportId = fixture.Create<Guid>();\n        var startDate = DateTime.UtcNow.AddDays(-30);\n        var endDate = DateTime.UtcNow;\n        var summaryDataList = fixture.Build<OrganizationReportSummaryDataResponse>()\n            .CreateMany(12)\n            .ToList();\n        summaryDataList[0].RevisionDate = DateTime.UtcNow; // most recent\n        summaryDataList[1].RevisionDate = DateTime.UtcNow.AddDays(-1);\n        summaryDataList[2].RevisionDate = DateTime.UtcNow.AddDays(-2);\n        summaryDataList[3].RevisionDate = DateTime.UtcNow.AddDays(-3);\n        summaryDataList[4].RevisionDate = DateTime.UtcNow.AddDays(-4);\n        summaryDataList[5].RevisionDate = DateTime.UtcNow.AddDays(-5);\n        summaryDataList[6].RevisionDate = DateTime.UtcNow.AddDays(-6);\n        summaryDataList[7].RevisionDate = DateTime.UtcNow.AddDays(-7);\n        summaryDataList[8].RevisionDate = DateTime.UtcNow.AddDays(-8);\n        summaryDataList[9].RevisionDate = DateTime.UtcNow.AddDays(-9);\n        summaryDataList[10].RevisionDate = DateTime.UtcNow.AddDays(-10);\n        summaryDataList[11].RevisionDate = DateTime.UtcNow.AddDays(-11);\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetSummaryDataByDateRangeAsync(Arg.Any<Guid>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())\n            .Returns(summaryDataList);\n\n        sutProvider\n            .GetDependency<IFusionCache>()\n            .GetOrSetAsync(\n                key: Arg.Any<string?>(),\n                factory: Arg.Any<Func<object, CancellationToken, Task<IEnumerable<OrganizationReportSummaryDataResponse>>>>(),\n                options: Arg.Any<FusionCacheEntryOptions>(),\n                tags: Arg.Any<IEnumerable<string>>())\n            .Returns(callInfo =>\n            {\n                var factory = callInfo.ArgAt<Func<FusionCacheFactoryExecutionContext<IEnumerable<OrganizationReportSummaryDataResponse>>, CancellationToken, Task<IEnumerable<OrganizationReportSummaryDataResponse>>>>(1);\n                return new ValueTask<IEnumerable<OrganizationReportSummaryDataResponse>>(factory.Invoke(null, CancellationToken.None));\n            });\n\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(6, result.Count());\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(1).GetSummaryDataByDateRangeAsync(Arg.Any<Guid>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)\n    {\n        // Arrange\n        var reportId = Guid.NewGuid();\n        var startDate = DateTime.UtcNow.AddDays(-30);\n        var endDate = DateTime.UtcNow;\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(Guid.Empty, startDate, endDate));\n\n        Assert.Equal(\"OrganizationId is required\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive()\n            .GetSummaryDataByDateRangeAsync(\n                Arg.Any<Guid>(),\n                Arg.Any<DateTime>(),\n                Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithStartDateAfterEndDate_ShouldThrowBadRequestException(\n        SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var reportId = Guid.NewGuid();\n        var startDate = DateTime.UtcNow;\n        var endDate = DateTime.UtcNow.AddDays(-30);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate));\n\n        Assert.Equal(\"StartDate must be earlier than or equal to EndDate\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().GetSummaryDataByDateRangeAsync(Arg.Any<Guid>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithEmptyResult_ShouldReturnEmptyList(\n        SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var reportId = Guid.NewGuid();\n        var startDate = DateTime.UtcNow.AddDays(-30);\n        var endDate = DateTime.UtcNow;\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)\n            .Returns(new List<OrganizationReportSummaryDataResponse>());\n\n        sutProvider\n            .GetDependency<IFusionCache>()\n            .GetOrSetAsync(\n                key: Arg.Any<string?>(),\n                factory: Arg.Any<Func<object, CancellationToken, Task<IEnumerable<OrganizationReportSummaryDataResponse>>>>(),\n                options: Arg.Any<FusionCacheEntryOptions>(),\n                tags: Arg.Any<IEnumerable<string>>())\n            .Returns(callInfo =>\n            {\n                var factory = callInfo.ArgAt<Func<FusionCacheFactoryExecutionContext<IEnumerable<OrganizationReportSummaryDataResponse>>, CancellationToken, Task<IEnumerable<OrganizationReportSummaryDataResponse>>>>(1);\n                return new ValueTask<IEnumerable<OrganizationReportSummaryDataResponse>>(factory.Invoke(null, CancellationToken.None));\n            });\n\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WhenRepositoryThrowsException_ShouldPropagateException(\n        SutProvider<GetOrganizationReportSummaryDataByDateRangeQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var reportId = Guid.NewGuid();\n        var startDate = DateTime.UtcNow.AddDays(-30);\n        var endDate = DateTime.UtcNow;\n        var expectedMessage = \"Database connection failed\";\n\n        var repo = sutProvider.GetDependency<IOrganizationReportRepository>();\n\n        repo\n            .GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)\n            .Throws(new InvalidOperationException(expectedMessage));\n\n        sutProvider\n            .GetDependency<IFusionCache>()\n            .GetOrSetAsync(\n                key: Arg.Any<string?>(),\n                factory: Arg.Any<Func<object, CancellationToken, Task<IEnumerable<OrganizationReportSummaryDataResponse>>>>(),\n                options: Arg.Any<FusionCacheEntryOptions>(),\n                tags: Arg.Any<IEnumerable<string>>())\n            .Returns(callInfo =>\n            {\n                var factory = callInfo.ArgAt<Func<FusionCacheFactoryExecutionContext<IEnumerable<OrganizationReportSummaryDataResponse>>, CancellationToken, Task<IEnumerable<OrganizationReportSummaryDataResponse>>>>(1);\n                return new ValueTask<IEnumerable<OrganizationReportSummaryDataResponse>>(factory.Invoke(null, CancellationToken.None));\n            });\n\n\n        // Act & Assert\n        // var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>\n        //     await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate));\n\n        var results = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);\n\n        // Assert\n        // since the IFusionCache has a failsafe, \n        // the exception from the repository should be caught and logged, and an empty list should be returned\n        Assert.NotNull(results);\n        Assert.Empty(results);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataQueryTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Dirt.Models.Data;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class GetOrganizationReportSummaryDataQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataAsync_WithValidParams_ShouldReturnSummaryData(\n        SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var organizationId = fixture.Create<Guid>();\n        var reportId = fixture.Create<Guid>();\n        var summaryDataResponse = fixture.Build<OrganizationReportSummaryDataResponse>()\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetSummaryDataAsync(reportId)\n            .Returns(summaryDataResponse);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId);\n\n        // Assert\n        Assert.NotNull(result);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(1).GetSummaryDataAsync(reportId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)\n    {\n        // Arrange\n        var reportId = Guid.NewGuid();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(Guid.Empty, reportId));\n\n        Assert.Equal(\"OrganizationId is required.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().GetSummaryDataAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(\n        SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, Guid.Empty));\n\n        Assert.Equal(\"ReportId is required.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().GetSummaryDataAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataAsync_WhenDataNotFound_ShouldThrowNotFoundException(\n        SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var reportId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetSummaryDataAsync(reportId)\n            .Returns((OrganizationReportSummaryDataResponse)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId));\n\n        Assert.Equal(\"Organization report summary data not found.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetOrganizationReportSummaryDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(\n        SutProvider<GetOrganizationReportSummaryDataQuery> sutProvider)\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var reportId = Guid.NewGuid();\n        var expectedMessage = \"Database connection failed\";\n\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetSummaryDataAsync(reportId)\n            .Throws(new InvalidOperationException(expectedMessage));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>\n            await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId));\n\n        Assert.Equal(expectedMessage, exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class GetPasswordHealthReportApplicationQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task GetPasswordHealthReportApplicationAsync_WithValidOrganizationId_ShouldReturnPasswordHealthReportApplication(\n        SutProvider<GetPasswordHealthReportApplicationQuery> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var organizationId = fixture.Create<Guid>();\n        sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .GetByOrganizationIdAsync(Arg.Any<Guid>())\n            .Returns(fixture.CreateMany<PasswordHealthReportApplication>(2).ToList());\n\n        // Act\n        var result = await sutProvider.Sut.GetPasswordHealthReportApplicationAsync(organizationId);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.True(result.Count() == 2);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetPasswordHealthReportApplicationAsync_WithInvalidOrganizationId_ShouldFail(\n        SutProvider<GetPasswordHealthReportApplicationQuery> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        sutProvider.GetDependency<IPasswordHealthReportApplicationRepository>()\n            .GetByOrganizationIdAsync(Arg.Is<Guid>(x => x == Guid.Empty))\n            .Returns(new List<PasswordHealthReportApplication>());\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.GetPasswordHealthReportApplicationAsync(Guid.Empty));\n\n        // Assert\n        Assert.Equal(\"OrganizationId is required.\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportApplicationDataCommandTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationReportApplicationDataCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithValidRequest_ShouldReturnUpdatedReport(\n        SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()\n            .With(x => x.Id, Guid.NewGuid())\n            .With(x => x.OrganizationId, Guid.NewGuid())\n            .With(x => x.ApplicationData, \"updated application data\")\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.Id)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n        var updatedReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.Id)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.Id)\n            .Returns(existingReport);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData)\n            .Returns(updatedReport);\n\n        // Act\n        var result = await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(updatedReport.Id, result.Id);\n        Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(1).UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()\n            .With(x => x.OrganizationId, Guid.Empty)\n            .Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));\n\n        Assert.Equal(\"OrganizationId is required\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().UpdateApplicationDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()\n            .With(x => x.Id, Guid.Empty)\n            .Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));\n\n        Assert.Equal(\"Id is required\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().UpdateApplicationDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithInvalidOrganization_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns((Organization)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));\n\n        Assert.Equal(\"Invalid Organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyApplicationData_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()\n            .With(x => x.ApplicationData, string.Empty)\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));\n\n        Assert.Equal(\"Application Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithNullApplicationData_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportApplicationDataRequest>()\n            .With(x => x.ApplicationData, (string)null)\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));\n\n        Assert.Equal(\"Application Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithNonExistentReport_ShouldThrowNotFoundException(\n        SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();\n        var organization = fixture.Create<Organization>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.Id)\n            .Returns((OrganizationReport)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));\n\n        Assert.Equal(\"Organization report not found\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.Id)\n            .With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.Id)\n            .Returns(existingReport);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));\n\n        Assert.Equal(\"Organization report does not belong to the specified organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportApplicationDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(\n        SutProvider<UpdateOrganizationReportApplicationDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportApplicationDataRequest>();\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.Id)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.Id)\n            .Returns(existingReport);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData)\n            .Throws(new InvalidOperationException(\"Database connection failed\"));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request));\n\n        Assert.Equal(\"Database connection failed\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportCommandTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationReportCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithValidRequest_ShouldReturnUpdatedReport(\n        SutProvider<UpdateOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportRequest>()\n            .With(x => x.ReportId, Guid.NewGuid())\n            .With(x => x.OrganizationId, Guid.NewGuid())\n            .With(x => x.ReportData, \"updated report data\")\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n        var updatedReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .With(x => x.ReportData, request.ReportData)\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns(existingReport);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .UpsertAsync(Arg.Any<OrganizationReport>())\n            .Returns(Task.CompletedTask);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns(existingReport, updatedReport);\n\n        // Act\n        var result = await sutProvider.Sut.UpdateOrganizationReportAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(updatedReport.Id, result.Id);\n        Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);\n        Assert.Equal(updatedReport.ReportData, result.ReportData);\n\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1).GetByIdAsync(request.OrganizationId);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(2).GetByIdAsync(request.ReportId);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(1).UpsertAsync(Arg.Any<OrganizationReport>());\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(OrganizationReportCacheConstants.BuildCacheTagForOrganizationReports(request.OrganizationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportRequest>()\n            .With(x => x.OrganizationId, Guid.Empty)\n            .Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportAsync(request));\n\n        Assert.Equal(\"OrganizationId is required\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().UpsertAsync(Arg.Any<OrganizationReport>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithEmptyReportId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportRequest>()\n            .With(x => x.ReportId, Guid.Empty)\n            .Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportAsync(request));\n\n        Assert.Equal(\"ReportId is required\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().UpsertAsync(Arg.Any<OrganizationReport>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithInvalidOrganization_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportRequest>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns((Organization)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportAsync(request));\n\n        Assert.Equal(\"Invalid Organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithEmptyReportData_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportRequest>()\n            .With(x => x.ReportData, string.Empty)\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportAsync(request));\n\n        Assert.Equal(\"Report Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithNullReportData_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportRequest>()\n            .With(x => x.ReportData, (string)null)\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportAsync(request));\n\n        Assert.Equal(\"Report Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithNonExistentReport_ShouldThrowNotFoundException(\n        SutProvider<UpdateOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportRequest>();\n        var organization = fixture.Create<Organization>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns((OrganizationReport)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportAsync(request));\n\n        Assert.Equal(\"Organization report not found\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportRequest>();\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns(existingReport);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportAsync(request));\n\n        Assert.Equal(\"Organization report does not belong to the specified organization\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportDataCommandTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationReportDataCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithValidRequest_ShouldReturnUpdatedReport(\n        SutProvider<UpdateOrganizationReportDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportDataRequest>()\n            .With(x => x.ReportId, Guid.NewGuid())\n            .With(x => x.OrganizationId, Guid.NewGuid())\n            .With(x => x.ReportData, \"updated report data\")\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n        var updatedReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns(existingReport);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData)\n            .Returns(updatedReport);\n\n        // Act\n        var result = await sutProvider.Sut.UpdateOrganizationReportDataAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(updatedReport.Id, result.Id);\n        Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(1).UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportDataRequest>()\n            .With(x => x.OrganizationId, Guid.Empty)\n            .Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));\n\n        Assert.Equal(\"OrganizationId is required\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().UpdateReportDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithEmptyReportId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportDataRequest>()\n            .With(x => x.ReportId, Guid.Empty)\n            .Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));\n\n        Assert.Equal(\"ReportId is required\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().UpdateReportDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithInvalidOrganization_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportDataRequest>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns((Organization)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));\n\n        Assert.Equal(\"Invalid Organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithEmptyReportData_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportDataRequest>()\n            .With(x => x.ReportData, string.Empty)\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));\n\n        Assert.Equal(\"Report Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithNullReportData_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportDataRequest>()\n            .With(x => x.ReportData, (string)null)\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));\n\n        Assert.Equal(\"Report Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithNonExistentReport_ShouldThrowNotFoundException(\n        SutProvider<UpdateOrganizationReportDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportDataRequest>();\n        var organization = fixture.Create<Organization>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns((OrganizationReport)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));\n\n        Assert.Equal(\"Organization report not found\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportDataRequest>();\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns(existingReport);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));\n\n        Assert.Equal(\"Organization report does not belong to the specified organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportDataAsync_WhenRepositoryThrowsException_ShouldPropagateException(\n        SutProvider<UpdateOrganizationReportDataCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportDataRequest>();\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns(existingReport);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData)\n            .Throws(new InvalidOperationException(\"Database connection failed\"));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportDataAsync(request));\n\n        Assert.Equal(\"Database connection failed\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportSummaryCommandTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Reports.ReportFeatures;\nusing Bit.Core.Dirt.Reports.ReportFeatures.Requests;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.ReportFeatures;\n\n[SutProviderCustomize]\npublic class UpdateOrganizationReportSummaryCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithValidRequest_ShouldReturnUpdatedReport(\n        SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()\n            .With(x => x.ReportId, Guid.NewGuid())\n            .With(x => x.OrganizationId, Guid.NewGuid())\n            .With(x => x.SummaryData, \"updated summary data\")\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n        var updatedReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns(existingReport);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData)\n            .Returns(updatedReport);\n\n        // Act\n        var result = await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Equal(updatedReport.Id, result.Id);\n        Assert.Equal(updatedReport.OrganizationId, result.OrganizationId);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .Received(1).UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData);\n        await sutProvider.GetDependency<IFusionCache>().Received(1)\n            .RemoveByTagAsync(OrganizationReportCacheConstants.BuildCacheTagForOrganizationReports(request.OrganizationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()\n            .With(x => x.OrganizationId, Guid.Empty)\n            .Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));\n\n        Assert.Equal(\"OrganizationId is required\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().UpdateSummaryDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithEmptyReportId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()\n            .With(x => x.ReportId, Guid.Empty)\n            .Create();\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));\n\n        Assert.Equal(\"ReportId is required\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationReportRepository>()\n            .DidNotReceive().UpdateSummaryDataAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithInvalidOrganization_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns((Organization)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));\n\n        Assert.Equal(\"Invalid Organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithEmptySummaryData_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()\n            .With(x => x.SummaryData, string.Empty)\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));\n\n        Assert.Equal(\"Summary Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithNullSummaryData_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Build<UpdateOrganizationReportSummaryRequest>()\n            .With(x => x.SummaryData, (string)null)\n            .Create();\n\n        var organization = fixture.Create<Organization>();\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));\n\n        Assert.Equal(\"Summary Data is required\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithNonExistentReport_ShouldThrowNotFoundException(\n        SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();\n        var organization = fixture.Create<Organization>();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns((OrganizationReport)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<NotFoundException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));\n\n        Assert.Equal(\"Organization report not found\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException(\n        SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns(existingReport);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));\n\n        Assert.Equal(\"Organization report does not belong to the specified organization\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateOrganizationReportSummaryAsync_WhenRepositoryThrowsException_ShouldPropagateException(\n        SutProvider<UpdateOrganizationReportSummaryCommand> sutProvider)\n    {\n        // Arrange\n        var fixture = new Fixture();\n        var request = fixture.Create<UpdateOrganizationReportSummaryRequest>();\n        var organization = fixture.Create<Organization>();\n        var existingReport = fixture.Build<OrganizationReport>()\n            .With(x => x.Id, request.ReportId)\n            .With(x => x.OrganizationId, request.OrganizationId)\n            .Create();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(request.OrganizationId)\n            .Returns(organization);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .GetByIdAsync(request.ReportId)\n            .Returns(existingReport);\n        sutProvider.GetDependency<IOrganizationReportRepository>()\n            .UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData)\n            .Throws(new InvalidOperationException(\"Database connection failed\"));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>\n            await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request));\n\n        Assert.Equal(\"Database connection failed\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/AzureQueueEventWriteServiceTests.cs",
    "content": "﻿using Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\npublic class AzureQueueEventWriteServiceTests\n{\n    private readonly AzureQueueEventWriteService _sut;\n\n    private readonly GlobalSettings _globalSettings;\n    private readonly IEventRepository _eventRepository;\n\n    public AzureQueueEventWriteServiceTests()\n    {\n        _globalSettings = new GlobalSettings();\n        _eventRepository = Substitute.For<IEventRepository>();\n\n        _sut = new AzureQueueEventWriteService(\n            _globalSettings\n        );\n    }\n\n    // Remove this test when we add actual tests. It only proves that\n    // we've properly constructed the system under test.\n    [Fact(Skip = \"Needs additional work\")]\n    public void ServiceExists()\n    {\n        Assert.NotNull(_sut);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/AzureServiceBusEventListenerServiceTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Text.Json;\nusing Azure.Messaging.ServiceBus;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class AzureServiceBusEventListenerServiceTests\n{\n    private const string _messageId = \"messageId\";\n    private readonly TestListenerConfiguration _config = new();\n    private readonly ILogger _logger = Substitute.For<ILogger>();\n\n    private SutProvider<AzureServiceBusEventListenerService<TestListenerConfiguration>> GetSutProvider()\n    {\n        var loggerFactory = Substitute.For<ILoggerFactory>();\n        loggerFactory.CreateLogger<object>().ReturnsForAnyArgs(_logger);\n        return new SutProvider<AzureServiceBusEventListenerService<TestListenerConfiguration>>()\n            .SetDependency(_config)\n            .SetDependency(loggerFactory)\n            .Create();\n    }\n\n    [Fact]\n    public void Constructor_CreatesLogWithCorrectCategory()\n    {\n        var sutProvider = GetSutProvider();\n\n        var fullName = typeof(AzureServiceBusEventListenerService<>).FullName ?? \"\";\n        var tickIndex = fullName.IndexOf('`');\n        var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName;\n        var categoryName = cleanedName + '.' + _config.EventSubscriptionName;\n\n        sutProvider.GetDependency<ILoggerFactory>().Received(1).CreateLogger(categoryName);\n    }\n\n    [Fact]\n    public void Constructor_CreatesProcessor()\n    {\n        var sutProvider = GetSutProvider();\n\n        sutProvider.GetDependency<IAzureServiceBusService>().Received(1).CreateProcessor(\n            Arg.Is(_config.EventTopicName),\n            Arg.Is(_config.EventSubscriptionName),\n            Arg.Any<ServiceBusProcessorOptions>()\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args)\n    {\n        var sutProvider = GetSutProvider();\n\n        await sutProvider.Sut.ProcessErrorAsync(args);\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError()\n    {\n        var sutProvider = GetSutProvider();\n        await sutProvider.Sut.ProcessReceivedMessageAsync(string.Empty, _messageId);\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<JsonException>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError()\n    {\n        var sutProvider = GetSutProvider();\n        await sutProvider.Sut.ProcessReceivedMessageAsync(\"{ Invalid JSON }\", _messageId);\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => (o.ToString() ?? \"\").Contains(\"Invalid JSON\")),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError()\n    {\n        var sutProvider = GetSutProvider();\n        await sutProvider.Sut.ProcessReceivedMessageAsync(\n            \"{ \\\"not a valid\\\", \\\"list of event messages\\\" }\",\n            _messageId\n        );\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<JsonException>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError()\n    {\n        var sutProvider = GetSutProvider();\n        await sutProvider.Sut.ProcessReceivedMessageAsync(\n            JsonSerializer.Serialize(DateTime.UtcNow), // wrong object - not EventMessage\n            _messageId\n        );\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<JsonException>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message)\n    {\n        var sutProvider = GetSutProvider();\n        await sutProvider.Sut.ProcessReceivedMessageAsync(\n            JsonSerializer.Serialize(message),\n            _messageId\n        );\n\n        await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleEventAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { \"IdempotencyId\" })));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable<EventMessage> messages)\n    {\n        var sutProvider = GetSutProvider();\n        await sutProvider.Sut.ProcessReceivedMessageAsync(\n            JsonSerializer.Serialize(messages),\n            _messageId\n        );\n\n        await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleManyEventsAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { \"IdempotencyId\" })));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/AzureServiceBusIntegrationListenerServiceTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Text.Json;\nusing Azure.Messaging.ServiceBus;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class AzureServiceBusIntegrationListenerServiceTests\n{\n    private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();\n    private readonly ILogger _logger = Substitute.For<ILogger>();\n    private readonly IAzureServiceBusService _serviceBusService = Substitute.For<IAzureServiceBusService>();\n    private readonly TestListenerConfiguration _config = new();\n\n    private SutProvider<AzureServiceBusIntegrationListenerService<TestListenerConfiguration>> GetSutProvider()\n    {\n        var loggerFactory = Substitute.For<ILoggerFactory>();\n        loggerFactory.CreateLogger<object>().ReturnsForAnyArgs(_logger);\n        return new SutProvider<AzureServiceBusIntegrationListenerService<TestListenerConfiguration>>()\n            .SetDependency(_config)\n            .SetDependency(loggerFactory)\n            .SetDependency(_handler)\n            .SetDependency(_serviceBusService)\n            .Create();\n    }\n\n    [Fact]\n    public void Constructor_CreatesLogWithCorrectCategory()\n    {\n        var sutProvider = GetSutProvider();\n\n        var fullName = typeof(AzureServiceBusIntegrationListenerService<>).FullName ?? \"\";\n        var tickIndex = fullName.IndexOf('`');\n        var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName;\n        var categoryName = cleanedName + '.' + _config.IntegrationSubscriptionName;\n\n        sutProvider.GetDependency<ILoggerFactory>().Received(1).CreateLogger(categoryName);\n    }\n\n    [Fact]\n    public void Constructor_CreatesProcessor()\n    {\n        var sutProvider = GetSutProvider();\n\n        sutProvider.GetDependency<IAzureServiceBusService>().Received(1).CreateProcessor(\n            Arg.Is(_config.IntegrationTopicName),\n            Arg.Is(_config.IntegrationSubscriptionName),\n            Arg.Any<ServiceBusProcessorOptions>()\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args)\n    {\n        var sutProvider = GetSutProvider();\n        await sutProvider.Sut.ProcessErrorAsync(args);\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.RetryCount = 0;\n\n        var result = IntegrationHandlerResult.Fail(\n            message: message,\n            category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable\n            failureReason: \"403\");\n        _handler.HandleAsync(Arg.Any<string>()).Returns(result);\n\n        var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());\n        Assert.NotNull(expected);\n\n        Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));\n\n        await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));\n        await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());\n        _logger.Received().Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => (o.ToString() ?? \"\").Contains(\"Integration failure - non-recoverable error or max retries exceeded.\")),\n            Arg.Any<Exception?>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.RetryCount = _config.MaxRetries;\n        var result = IntegrationHandlerResult.Fail(\n            message: message,\n            category: IntegrationFailureCategory.TransientError, // Retryable\n            failureReason: \"403\");\n        _handler.HandleAsync(Arg.Any<string>()).Returns(result);\n\n        var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());\n        Assert.NotNull(expected);\n\n        Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));\n\n        await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));\n        await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());\n        _logger.Received().Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => (o.ToString() ?? \"\").Contains(\"Integration failure - non-recoverable error or max retries exceeded.\")),\n            Arg.Any<Exception?>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.RetryCount = 0;\n\n        var result = IntegrationHandlerResult.Fail(\n            message: message,\n            category: IntegrationFailureCategory.TransientError, // Retryable\n            failureReason: \"403\");\n        _handler.HandleAsync(Arg.Any<string>()).Returns(result);\n\n        var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());\n        Assert.NotNull(expected);\n\n        Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));\n\n        await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));\n        await _serviceBusService.Received(1).PublishToRetryAsync(message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)\n    {\n        var sutProvider = GetSutProvider();\n        var result = IntegrationHandlerResult.Succeed(message);\n        _handler.HandleAsync(Arg.Any<string>()).Returns(result);\n\n        var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());\n        Assert.NotNull(expected);\n\n        Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson()));\n\n        await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));\n        await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());\n    }\n\n    [Fact]\n    public async Task HandleMessageAsync_UnknownError_LogsError()\n    {\n        var sutProvider = GetSutProvider();\n        _handler.HandleAsync(Arg.Any<string>()).ThrowsAsync<JsonException>();\n\n        Assert.True(await sutProvider.Sut.HandleMessageAsync(\"Bad JSON\"));\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => (o.ToString() ?? \"\").Contains(\"Unhandled error processing ASB message\")),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception?, string>>());\n\n        await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any<IIntegrationMessage>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/DatadogIntegrationHandlerTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Bit.Test.Common.MockedHttpClient;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class DatadogIntegrationHandlerTests\n{\n    private readonly MockedHttpMessageHandler _handler;\n    private readonly HttpClient _httpClient;\n    private const string _apiKey = \"AUTH_TOKEN\";\n    private static readonly Uri _datadogUri = new Uri(\"https://localhost\");\n\n    public DatadogIntegrationHandlerTests()\n    {\n        _handler = new MockedHttpMessageHandler();\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.OK)\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n        _httpClient = _handler.ToHttpClient();\n    }\n\n    private SutProvider<DatadogIntegrationHandler> GetSutProvider()\n    {\n        var clientFactory = Substitute.For<IHttpClientFactory>();\n        clientFactory.CreateClient(DatadogIntegrationHandler.HttpClientName).Returns(_httpClient);\n\n        return new SutProvider<DatadogIntegrationHandler>()\n            .SetDependency(clientFactory)\n            .WithFakeTimeProvider()\n            .Create();\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.True(result.Success);\n        Assert.Equal(result.Message, message);\n        Assert.Null(result.FailureReason);\n\n        sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(\n            Arg.Is(AssertHelper.AssertPropertyEqual(DatadogIntegrationHandler.HttpClientName))\n        );\n\n        Assert.Single(_handler.CapturedRequests);\n        var request = _handler.CapturedRequests[0];\n        Assert.NotNull(request);\n        Assert.NotNull(request.Content);\n        var returned = await request.Content.ReadAsStringAsync();\n\n        Assert.Equal(HttpMethod.Post, request.Method);\n        Assert.Equal(_apiKey, request.Headers.GetValues(\"DD-API-KEY\").Single());\n        Assert.Equal(_datadogUri, request.RequestUri);\n        AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);\n        var retryAfter = now.AddSeconds(60);\n\n        sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);\n        message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);\n\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.TooManyRequests)\n            .WithHeader(\"Retry-After\", \"60\")\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.True(result.Retryable);\n        Assert.Equal(result.Message, message);\n        Assert.True(result.DelayUntilDate.HasValue);\n        Assert.Equal(retryAfter, result.DelayUntilDate.Value);\n        Assert.Equal(\"Too Many Requests\", result.FailureReason);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);\n        var retryAfter = now.AddSeconds(60);\n        message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);\n\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.TooManyRequests)\n            .WithHeader(\"Retry-After\", retryAfter.ToString(\"r\"))\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.True(result.Retryable);\n        Assert.Equal(result.Message, message);\n        Assert.True(result.DelayUntilDate.HasValue);\n        Assert.Equal(retryAfter, result.DelayUntilDate.Value);\n        Assert.Equal(\"Too Many Requests\", result.FailureReason);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);\n\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.InternalServerError)\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.True(result.Retryable);\n        Assert.Equal(result.Message, message);\n        Assert.False(result.DelayUntilDate.HasValue);\n        Assert.Equal(\"Internal Server Error\", result.FailureReason);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<DatadogIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri);\n\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.TemporaryRedirect)\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.False(result.Retryable);\n        Assert.Equal(result.Message, message);\n        Assert.Null(result.DelayUntilDate);\n        Assert.Equal(\"Temporary Redirect\", result.FailureReason);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/EventIntegrationEventWriteServiceTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Models.Data;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class EventIntegrationEventWriteServiceTests\n{\n    private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();\n    private readonly EventIntegrationEventWriteService Subject;\n\n    public EventIntegrationEventWriteServiceTests()\n    {\n        Subject = new EventIntegrationEventWriteService(_eventIntegrationPublisher);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_EventPublishedToEventQueue(EventMessage eventMessage)\n    {\n        await Subject.CreateAsync(eventMessage);\n        await _eventIntegrationPublisher.Received(1).PublishEventAsync(\n            body: Arg.Is<string>(body => AssertJsonStringsMatch(eventMessage, body)),\n            organizationId: Arg.Is<string>(orgId => eventMessage.OrganizationId.ToString().Equals(orgId)));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateManyAsync_EventsPublishedToEventQueue(IEnumerable<EventMessage> eventMessages)\n    {\n        var eventMessage = eventMessages.First();\n        await Subject.CreateManyAsync(eventMessages);\n        await _eventIntegrationPublisher.Received(1).PublishEventAsync(\n            body: Arg.Is<string>(body => AssertJsonStringsMatch(eventMessages, body)),\n            organizationId: Arg.Is<string>(orgId => eventMessage.OrganizationId.ToString().Equals(orgId)));\n    }\n\n    [Fact]\n    public async Task CreateManyAsync_EmptyList_DoesNothing()\n    {\n        await Subject.CreateManyAsync([]);\n        await _eventIntegrationPublisher.DidNotReceiveWithAnyArgs().PublishEventAsync(Arg.Any<string>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task DisposeAsync_DisposesEventIntegrationPublisher()\n    {\n        await Subject.DisposeAsync();\n        await _eventIntegrationPublisher.Received(1).DisposeAsync();\n    }\n\n    private static bool AssertJsonStringsMatch(EventMessage expected, string body)\n    {\n        var actual = JsonSerializer.Deserialize<EventMessage>(body);\n        AssertHelper.AssertPropertyEqual(expected, actual, new[] { \"IdempotencyId\" });\n        return true;\n    }\n\n    private static bool AssertJsonStringsMatch(IEnumerable<EventMessage> expected, string body)\n    {\n        using var actual = JsonSerializer.Deserialize<IEnumerable<EventMessage>>(body).GetEnumerator();\n\n        foreach (var expectedMessage in expected)\n        {\n            actual.MoveNext();\n            AssertHelper.AssertPropertyEqual(expectedMessage, actual.Current, new[] { \"IdempotencyId\" });\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/EventIntegrationHandlerTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\nusing ZiggyCreatures.Caching.Fusion;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class EventIntegrationHandlerTests\n{\n    private const string _templateBase = \"Date: #Date#, Type: #Type#, UserId: #UserId#\";\n    private const string _templateWithGroup = \"Group: #GroupName#\";\n    private const string _templateWithOrganization = \"Org: #OrganizationName#\";\n    private const string _templateWithUser = \"#UserName#, #UserEmail#, #UserType#\";\n    private const string _templateWithActingUser = \"#ActingUserName#, #ActingUserEmail#, #ActingUserType#\";\n    private static readonly Guid _organizationId = Guid.NewGuid();\n    private static readonly Uri _uri = new Uri(\"https://localhost\");\n    private static readonly Uri _uri2 = new Uri(\"https://example.com\");\n    private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();\n    private readonly ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> _logger =\n        Substitute.For<ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>>();\n\n    private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(\n        List<OrganizationIntegrationConfigurationDetails> configurations)\n    {\n        var cache = Substitute.For<IFusionCache>();\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),\n            options: Arg.Any<FusionCacheEntryOptions>(),\n            tags: Arg.Any<IEnumerable<string>>()\n        ).Returns(configurations);\n\n        return new SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>()\n            .SetDependency(cache)\n            .SetDependency(_eventIntegrationPublisher)\n            .SetDependency(IntegrationType.Webhook)\n            .SetDependency(_logger)\n            .Create();\n    }\n\n    private static IntegrationMessage<WebhookIntegrationConfigurationDetails> ExpectedMessage(string template)\n    {\n        return new IntegrationMessage<WebhookIntegrationConfigurationDetails>()\n        {\n            IntegrationType = IntegrationType.Webhook,\n            MessageId = \"TestMessageId\",\n            OrganizationId = _organizationId.ToString(),\n            Configuration = new WebhookIntegrationConfigurationDetails(_uri),\n            RenderedTemplate = template,\n            RetryCount = 0,\n            DelayUntilDate = null\n        };\n    }\n\n    private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()\n    {\n        return [];\n    }\n\n    private static List<OrganizationIntegrationConfigurationDetails> OneConfiguration(string template)\n    {\n        var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();\n        config.Configuration = null;\n        config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });\n        config.Template = template;\n\n        return [config];\n    }\n\n    private static List<OrganizationIntegrationConfigurationDetails> TwoConfigurations(string template)\n    {\n        var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();\n        config.Configuration = null;\n        config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });\n        config.Template = template;\n        var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();\n        config2.Configuration = null;\n        config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri2 });\n        config2.Template = template;\n\n        return [config, config2];\n    }\n\n    private static List<OrganizationIntegrationConfigurationDetails> InvalidFilterConfiguration()\n    {\n        var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();\n        config.Configuration = null;\n        config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });\n        config.Template = _templateBase;\n        config.Filters = \"Invalid Configuration!\";\n\n        return [config];\n    }\n\n    private static List<OrganizationIntegrationConfigurationDetails> ValidFilterConfiguration()\n    {\n        var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();\n        config.Configuration = null;\n        config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });\n        config.Template = _templateBase;\n        config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup());\n\n        return [config];\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_ActingUserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails actingUser)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.OrganizationId ??= Guid.NewGuid();\n        eventMessage.ActingUserId ??= Guid.NewGuid();\n\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()\n        ).Returns(actingUser);\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);\n\n        await cache.Received(1).GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()\n        );\n\n        Assert.Equal(actingUser, context.ActingUser);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_ActingUserIdNull_SkipsCache(EventMessage eventMessage)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.OrganizationId ??= Guid.NewGuid();\n        eventMessage.ActingUserId = null;\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);\n\n        await cache.DidNotReceive().GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()\n        );\n        Assert.Null(context.ActingUser);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_ActingUserOrganizationIdNull_SkipsCache(EventMessage eventMessage)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.OrganizationId = null;\n        eventMessage.ActingUserId ??= Guid.NewGuid();\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);\n\n        await cache.DidNotReceive().GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()\n        );\n        Assert.Null(context.ActingUser);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_ActingUserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails actingUser)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        eventMessage.OrganizationId ??= Guid.NewGuid();\n        eventMessage.ActingUserId ??= Guid.NewGuid();\n        organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(\n            eventMessage.OrganizationId.Value,\n            eventMessage.ActingUserId.Value).Returns(actingUser);\n\n        // Capture the factory function passed to the cache\n        Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(f => capturedFactory = f)\n        ).Returns(actingUser);\n\n        await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);\n\n        Assert.NotNull(capturedFactory);\n        var result = await capturedFactory(null!, CancellationToken.None);\n\n        await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync(\n            eventMessage.OrganizationId.Value,\n            eventMessage.ActingUserId.Value);\n        Assert.Equal(actingUser, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_GroupIdPresent_UsesCache(EventMessage eventMessage, Group group)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.GroupId ??= Guid.NewGuid();\n\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()\n        ).Returns(group);\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);\n\n        await cache.Received(1).GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()\n        );\n        Assert.Equal(group, context.Group);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_GroupIdNull_SkipsCache(EventMessage eventMessage)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n        eventMessage.GroupId = null;\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);\n\n        await cache.DidNotReceive().GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()\n        );\n        Assert.Null(context.Group);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_GroupFactory_CallsGroupRepository(EventMessage eventMessage, Group group)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n        var groupRepository = sutProvider.GetDependency<IGroupRepository>();\n\n        eventMessage.GroupId ??= Guid.NewGuid();\n        groupRepository.GetByIdAsync(eventMessage.GroupId.Value).Returns(group);\n\n        // Capture the factory function passed to the cache\n        Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>? capturedFactory = null;\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>(f => capturedFactory = f)\n        ).Returns(group);\n\n        await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);\n\n        Assert.NotNull(capturedFactory);\n        var result = await capturedFactory(null!, CancellationToken.None);\n\n        await groupRepository.Received(1).GetByIdAsync(eventMessage.GroupId.Value);\n        Assert.Equal(group, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_OrganizationIdPresent_UsesCache(EventMessage eventMessage, Organization organization)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.OrganizationId ??= Guid.NewGuid();\n\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()\n        ).Returns(organization);\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);\n\n        await cache.Received(1).GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()\n        );\n        Assert.Equal(organization, context.Organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_OrganizationIdNull_SkipsCache(EventMessage eventMessage)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.OrganizationId = null;\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);\n\n        await cache.DidNotReceive().GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()\n        );\n        Assert.Null(context.Organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_OrganizationFactory_CallsOrganizationRepository(EventMessage eventMessage, Organization organization)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n        var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();\n\n        eventMessage.OrganizationId ??= Guid.NewGuid();\n        organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value).Returns(organization);\n\n        // Capture the factory function passed to the cache\n        Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>? capturedFactory = null;\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>(f => capturedFactory = f)\n        ).Returns(organization);\n\n        await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);\n\n        Assert.NotNull(capturedFactory);\n        var result = await capturedFactory(null!, CancellationToken.None);\n\n        await organizationRepository.Received(1).GetByIdAsync(eventMessage.OrganizationId.Value);\n        Assert.Equal(organization, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_UserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails userDetails)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.OrganizationId ??= Guid.NewGuid();\n        eventMessage.UserId ??= Guid.NewGuid();\n\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()\n        ).Returns(userDetails);\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);\n\n        await cache.Received(1).GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()\n        );\n\n        Assert.Equal(userDetails, context.User);\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_UserIdNull_SkipsCache(EventMessage eventMessage)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.OrganizationId = null;\n        eventMessage.UserId ??= Guid.NewGuid();\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);\n\n        await cache.DidNotReceive().GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()\n        );\n\n        Assert.Null(context.User);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_OrganizationUserIdNull_SkipsCache(EventMessage eventMessage)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.OrganizationId ??= Guid.NewGuid();\n        eventMessage.UserId = null;\n\n        var context = await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);\n\n        await cache.DidNotReceive().GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()\n        );\n\n        Assert.Null(context.User);\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_UserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails userDetails)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n        var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();\n\n        eventMessage.OrganizationId ??= Guid.NewGuid();\n        eventMessage.UserId ??= Guid.NewGuid();\n        organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(\n            eventMessage.OrganizationId.Value,\n            eventMessage.UserId.Value).Returns(userDetails);\n\n        // Capture the factory function passed to the cache\n        Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(f => capturedFactory = f)\n        ).Returns(userDetails);\n\n        await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);\n\n        Assert.NotNull(capturedFactory);\n        var result = await capturedFactory(null!, CancellationToken.None);\n\n        await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync(\n            eventMessage.OrganizationId.Value,\n            eventMessage.UserId.Value);\n        Assert.Equal(userDetails, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        eventMessage.ActingUserId ??= Guid.NewGuid();\n        eventMessage.GroupId ??= Guid.NewGuid();\n        eventMessage.OrganizationId ??= Guid.NewGuid();\n        eventMessage.UserId ??= Guid.NewGuid();\n\n        await sutProvider.Sut.BuildContextAsync(eventMessage, _templateBase);\n\n        await cache.DidNotReceive().GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>()\n        );\n        await cache.DidNotReceive().GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>()\n        );\n        await cache.DidNotReceive().GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>()\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)\n    {\n        var sutProvider = GetSutProvider(NoConfigurations());\n        var cache = sutProvider.GetDependency<IFusionCache>();\n        cache.GetOrSetAsync<List<OrganizationIntegrationConfigurationDetails>>(\n            Arg.Any<string>(),\n            Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),\n            Arg.Any<FusionCacheEntryOptions>()\n        ).Returns(NoConfigurations());\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n        Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_NoOrganizationId_DoesNothing(EventMessage eventMessage)\n    {\n        var sutProvider = GetSutProvider(OneConfiguration(_templateBase));\n        eventMessage.OrganizationId = null;\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n        Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)\n    {\n        eventMessage.OrganizationId = _organizationId;\n        var sutProvider = GetSutProvider(OneConfiguration(_templateBase));\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n\n        var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(\n            $\"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}\"\n        );\n\n        Assert.Single(_eventIntegrationPublisher.ReceivedCalls());\n        await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(\n            AssertHelper.AssertPropertyEqual(expectedMessage, new[] { \"MessageId\" })));\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage)\n    {\n        eventMessage.OrganizationId = _organizationId;\n        var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n\n        var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(\n            $\"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}\"\n        );\n        await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(\n            AssertHelper.AssertPropertyEqual(expectedMessage, new[] { \"MessageId\" })));\n\n        expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2);\n        await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(\n            AssertHelper.AssertPropertyEqual(expectedMessage, new[] { \"MessageId\" })));\n\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)\n    {\n        eventMessage.OrganizationId = _organizationId;\n        var sutProvider = GetSutProvider(ValidFilterConfiguration());\n        sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(\n            Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(false);\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n        Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage(EventMessage eventMessage)\n    {\n        eventMessage.OrganizationId = _organizationId;\n        var sutProvider = GetSutProvider(ValidFilterConfiguration());\n        sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(\n            Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(true);\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n\n        var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(\n            $\"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}\"\n        );\n\n        Assert.Single(_eventIntegrationPublisher.ReceivedCalls());\n        await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(\n            AssertHelper.AssertPropertyEqual(expectedMessage, new[] { \"MessageId\" })));\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_InvalidFilter_LogsErrorDoesNothing(EventMessage eventMessage)\n    {\n        eventMessage.OrganizationId = _organizationId;\n        var sutProvider = GetSutProvider(InvalidFilterConfiguration());\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n        Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<JsonException>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)\n    {\n        eventMessages.ForEach(e => e.OrganizationId = _organizationId);\n        var sutProvider = GetSutProvider(NoConfigurations());\n\n        await sutProvider.Sut.HandleManyEventsAsync(eventMessages);\n        Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessages(List<EventMessage> eventMessages)\n    {\n        eventMessages.ForEach(e => e.OrganizationId = _organizationId);\n        var sutProvider = GetSutProvider(OneConfiguration(_templateBase));\n\n        await sutProvider.Sut.HandleManyEventsAsync(eventMessages);\n\n        foreach (var eventMessage in eventMessages)\n        {\n            var expectedMessage = ExpectedMessage(\n                $\"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}\"\n            );\n            await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(\n                AssertHelper.AssertPropertyEqual(expectedMessage, new[] { \"MessageId\", \"OrganizationId\" })));\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(\n        List<EventMessage> eventMessages)\n    {\n        eventMessages.ForEach(e => e.OrganizationId = _organizationId);\n        var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));\n\n        await sutProvider.Sut.HandleManyEventsAsync(eventMessages);\n\n        foreach (var eventMessage in eventMessages)\n        {\n            var expectedMessage = ExpectedMessage(\n                $\"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}\"\n            );\n            await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(\n                expectedMessage, new[] { \"MessageId\", \"OrganizationId\" })));\n\n            expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2);\n            await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(\n                expectedMessage, new[] { \"MessageId\", \"OrganizationId\" })));\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_CapturedFactories_CallConfigurationRepository(EventMessage eventMessage)\n    {\n        eventMessage.OrganizationId = _organizationId;\n        var sutProvider = GetSutProvider(NoConfigurations());\n        var cache = sutProvider.GetDependency<IFusionCache>();\n        var configurationRepository = sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>();\n\n        var configs = OneConfiguration(_templateBase);\n\n        configurationRepository.GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook).Returns(configs);\n\n        // Capture the factory function - there will be 1 call that returns both specific and wildcard matches\n        Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>? capturedFactory = null;\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(f\n                => capturedFactory = f),\n            options: Arg.Any<FusionCacheEntryOptions>(),\n            tags: Arg.Any<IEnumerable<string>>()\n        ).Returns(new List<OrganizationIntegrationConfigurationDetails>());\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n\n        // Verify factory was captured\n        Assert.NotNull(capturedFactory);\n\n        // Execute the captured factory to trigger repository call\n        await capturedFactory(null!, CancellationToken.None);\n\n        await configurationRepository.Received(1).GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_ConfigurationCacheOptions_SetsDurationToConstant(EventMessage eventMessage)\n    {\n        eventMessage.OrganizationId = _organizationId;\n        var sutProvider = GetSutProvider(NoConfigurations());\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        FusionCacheEntryOptions? capturedOption = null;\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),\n            options: Arg.Do<FusionCacheEntryOptions>(opt => capturedOption = opt),\n            tags: Arg.Any<IEnumerable<string>?>()\n        ).Returns(new List<OrganizationIntegrationConfigurationDetails>());\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n\n        Assert.NotNull(capturedOption);\n        Assert.Equal(EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails,\n                     capturedOption.Duration);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_ConfigurationCache_AddsOrganizationIntegrationTag(EventMessage eventMessage)\n    {\n        eventMessage.OrganizationId = _organizationId;\n        var sutProvider = GetSutProvider(NoConfigurations());\n        var cache = sutProvider.GetDependency<IFusionCache>();\n\n        IEnumerable<string>? capturedTags = null;\n        cache.GetOrSetAsync(\n            key: Arg.Any<string>(),\n            factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),\n            options: Arg.Any<FusionCacheEntryOptions>(),\n            tags: Arg.Do<IEnumerable<string>>(t => capturedTags = t)\n        ).Returns(new List<OrganizationIntegrationConfigurationDetails>());\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n\n        var expectedTag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(\n            _organizationId,\n            IntegrationType.Webhook\n        );\n        Assert.NotNull(capturedTags);\n        Assert.Contains(expectedTag, capturedTags);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_SubstituteTemplateTags(EventMessage eventMessage)\n    {\n        eventMessage.OrganizationId = _organizationId;\n        var templateJson = @\"{\n                \"\"bw_serviceName\"\": \"\"bitwarden\"\",\n                \"\"ddsource\"\": \"\"bitwarden\"\",\n                \"\"service\"\": \"\"event-logs\"\",\n                \"\"event\"\": {\n                    \"\"object\"\": \"\"event\"\",\n                    \"\"type\"\": \"\"#TypeId#\"\",\n                    \"\"typeName\"\": \"\"#Type#\"\",\n                    \"\"userId\"\": \"\"#UserId#\"\",\n                    \"\"organizationId\"\": \"\"#OrganizationId#\"\",\n                    \"\"providerId\"\": \"\"#ProviderId#\"\",\n                    \"\"cipherId\"\": \"\"#CipherId#\"\",\n                    \"\"collectionId\"\": \"\"#CollectionId#\"\",\n                    \"\"groupId\"\": \"\"#GroupId#\"\",\n                    \"\"policyId\"\": \"\"#PolicyId#\"\",\n                    \"\"organizationUserId\"\": \"\"#OrganizationUserId#\"\",\n                    \"\"providerUserId\"\": \"\"#ProviderUserId#\"\",\n                    \"\"providerOrganizationId\"\": \"\"#ProviderOrganizationId#\"\",\n                    \"\"actingUserId\"\": \"\"#ActingUserId#\"\",\n                    \"\"installationId\"\": \"\"#InstallationId#\"\",\n                    \"\"date\"\": \"\"#DateIso8601#\"\",\n                    \"\"deviceType\"\": \"\"#DeviceType#\"\",\n                    \"\"deviceTypeId\"\": \"\"#DeviceTypeId#\"\",\n                    \"\"ipAddress\"\": \"\"#IpAddress#\"\",\n                    \"\"systemUser\"\": \"\"#SystemUser#\"\",\n                    \"\"domainName\"\": \"\"#DomainName#\"\",\n                    \"\"secretId\"\": \"\"#SecretId#\"\",\n                    \"\"projectId\"\": \"\"#ProjectId#\"\",\n                    \"\"serviceAccountId\"\": \"\"#ServiceAccountId#\"\"\n                }\n            }\";\n        var sutProvider = GetSutProvider(OneConfiguration(templateJson));\n\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n\n        var deviceTypeId = eventMessage.DeviceType is not null ? (int)eventMessage.DeviceType : (int?)null;\n        var systemUser = eventMessage.SystemUser is not null ? (int)eventMessage.SystemUser : (int?)null;\n\n        var parsedJson = $@\"{{\n            \"\"bw_serviceName\"\": \"\"bitwarden\"\",\n            \"\"ddsource\"\": \"\"bitwarden\"\",\n            \"\"service\"\": \"\"event-logs\"\",\n            \"\"event\"\": {{\n                \"\"object\"\": \"\"event\"\",\n                \"\"type\"\": \"\"{(int)eventMessage.Type}\"\",\n                \"\"typeName\"\": \"\"{eventMessage.Type}\"\",\n                \"\"userId\"\": \"\"{eventMessage.UserId}\"\",\n                \"\"organizationId\"\": \"\"{eventMessage.OrganizationId}\"\",\n                \"\"providerId\"\": \"\"{eventMessage.ProviderId}\"\",\n                \"\"cipherId\"\": \"\"{eventMessage.CipherId}\"\",\n                \"\"collectionId\"\": \"\"{eventMessage.CollectionId}\"\",\n                \"\"groupId\"\": \"\"{eventMessage.GroupId}\"\",\n                \"\"policyId\"\": \"\"{eventMessage.PolicyId}\"\",\n                \"\"organizationUserId\"\": \"\"{eventMessage.OrganizationUserId}\"\",\n                \"\"providerUserId\"\": \"\"{eventMessage.ProviderUserId}\"\",\n                \"\"providerOrganizationId\"\": \"\"{eventMessage.ProviderOrganizationId}\"\",\n                \"\"actingUserId\"\": \"\"{eventMessage.ActingUserId}\"\",\n                \"\"installationId\"\": \"\"{eventMessage.InstallationId}\"\",\n                \"\"date\"\": \"\"{eventMessage.Date.ToString(\"o\")}\"\",\n                \"\"deviceType\"\": \"\"{eventMessage.DeviceType}\"\",\n                \"\"deviceTypeId\"\": \"\"{deviceTypeId}\"\",\n                \"\"ipAddress\"\": \"\"{eventMessage.IpAddress}\"\",\n                \"\"systemUser\"\": \"\"{systemUser}\"\",\n                \"\"domainName\"\": \"\"{eventMessage.DomainName}\"\",\n                \"\"secretId\"\": \"\"{eventMessage.SecretId}\"\",\n                \"\"projectId\"\": \"\"{eventMessage.ProjectId}\"\",\n                \"\"serviceAccountId\"\": \"\"{eventMessage.ServiceAccountId}\"\"\n            }}\n        }}\";\n        var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(\n            parsedJson\n        );\n\n        Assert.Single(_eventIntegrationPublisher.ReceivedCalls());\n        await _eventIntegrationPublisher.Received(1)\n        .PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage, new[] { \"MessageId\", \"RenderedTemplate\" })));\n\n        // compare renderedTemplate\n        var receivedCalls = _eventIntegrationPublisher.ReceivedCalls().ToList();\n        Assert.Single(receivedCalls);\n\n        var publishCall = receivedCalls.First();\n        var actualMessage = publishCall.GetArguments()[0] as IntegrationMessage<WebhookIntegrationConfigurationDetails>;\n\n        Assert.NotNull(actualMessage);\n        Assert.True(JsonStringsAreEqual(expectedMessage.RenderedTemplate!, actualMessage.RenderedTemplate!),\n            $\"Expected: {expectedMessage.RenderedTemplate}\\nActual: {actualMessage.RenderedTemplate}\");\n    }\n\n    private bool JsonStringsAreEqual(string expectedJson, string actualJson)\n    {\n        var expectedDoc = JsonNode.Parse(expectedJson);\n        var actualDoc = JsonNode.Parse(actualJson);\n        return JsonNode.DeepEquals(expectedDoc, actualDoc);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/EventRepositoryHandlerTests.cs",
    "content": "﻿using Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class EventRepositoryHandlerTests\n{\n    [Theory, BitAutoData]\n    public async Task HandleEventAsync_WritesEventToIEventWriteService(\n        EventMessage eventMessage,\n        SutProvider<EventRepositoryHandler> sutProvider)\n    {\n        await sutProvider.Sut.HandleEventAsync(eventMessage);\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(eventMessage))\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleManyEventAsync_WritesEventsToIEventWriteService(\n        IEnumerable<EventMessage> eventMessages,\n        SutProvider<EventRepositoryHandler> sutProvider)\n    {\n        await sutProvider.Sut.HandleManyEventsAsync(eventMessages);\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(eventMessages))\n        );\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/EventServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\n[SutProviderCustomize]\npublic class EventServiceTests\n{\n    public static IEnumerable<object[]> InstallationIdTestCases => TestCaseHelper.GetCombinationsOfMultipleLists(\n        new object[] { Guid.NewGuid(), null },\n        Enum.GetValues<EventType>().Select(e => (object)e)\n    ).Select(p => p.ToArray());\n\n    [Theory, BitAutoData]\n    public async Task LogGroupEvent_LogsRequiredInfo(Group group, EventType eventType, DateTime date,\n        Guid actingUserId, Guid providerId, string ipAddress, DeviceType deviceType, SutProvider<EventService> sutProvider)\n    {\n        var orgAbilities = new Dictionary<Guid, OrganizationAbility>()\n        {\n            { group.OrganizationId, new OrganizationAbility() { UseEvents = true, Enabled = true } }\n        };\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(orgAbilities);\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);\n        sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);\n        sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);\n\n        await sutProvider.Sut.LogGroupEventAsync(group, eventType, date);\n\n        var expected = new List<IEvent>() {\n            new EventMessage()\n            {\n                IpAddress = ipAddress,\n                DeviceType = deviceType,\n                OrganizationId = group.OrganizationId,\n                GroupId = group.Id,\n                Type = eventType,\n                ActingUserId = actingUserId,\n                ProviderId = providerId,\n                Date = date,\n                SystemUser = null\n            }\n        };\n\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { \"IdempotencyId\" })));\n    }\n\n    [Theory, BitAutoData]\n    public async Task LogGroupEvent_WithEventSystemUser_LogsRequiredInfo(Group group, EventType eventType, EventSystemUser eventSystemUser, DateTime date,\n        Guid actingUserId, Guid providerId, string ipAddress, DeviceType deviceType, SutProvider<EventService> sutProvider)\n    {\n        var orgAbilities = new Dictionary<Guid, OrganizationAbility>()\n        {\n            { group.OrganizationId, new OrganizationAbility() { UseEvents = true, Enabled = true } }\n        };\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(orgAbilities);\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);\n        sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);\n        sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);\n\n        await sutProvider.Sut.LogGroupEventAsync(group, eventType, eventSystemUser, date);\n\n        var eventMessage = new EventMessage()\n        {\n            IpAddress = ipAddress,\n            DeviceType = deviceType,\n            OrganizationId = group.OrganizationId,\n            GroupId = group.Id,\n            Type = eventType,\n            ActingUserId = actingUserId,\n            ProviderId = providerId,\n            Date = date,\n            SystemUser = eventSystemUser\n        };\n\n        if (eventSystemUser is EventSystemUser.SCIM)\n        {\n            eventMessage.DeviceType = DeviceType.Server;\n        }\n\n        var expected = new List<IEvent>() {\n            eventMessage\n        };\n\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { \"IdempotencyId\" })));\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(InstallationIdTestCases))]\n    public async Task LogOrganizationEvent_ProvidesInstallationId(Guid? installationId, EventType eventType,\n        Organization organization, SutProvider<EventService> sutProvider)\n    {\n        organization.Enabled = true;\n        organization.UseEvents = true;\n\n        sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);\n\n        await sutProvider.Sut.LogOrganizationEventAsync(organization, eventType);\n\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateAsync(Arg.Is<IEvent>(e =>\n            e.OrganizationId == organization.Id &&\n            e.Type == eventType &&\n            e.InstallationId == installationId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task LogOrganizationEvent_WithEventSystemUser_LogsRequiredInfo(Organization organization, EventType eventType,\n        EventSystemUser eventSystemUser, DateTime date, Guid providerId, SutProvider<EventService> sutProvider)\n    {\n        organization.Enabled = true;\n        organization.UseEvents = true;\n\n        sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);\n\n        await sutProvider.Sut.LogOrganizationEventAsync(organization, eventType, eventSystemUser, date);\n\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateAsync(Arg.Is<IEvent>(e =>\n            e.OrganizationId == organization.Id &&\n            e.Type == eventType &&\n            e.SystemUser == eventSystemUser &&\n            e.DeviceType == DeviceType.Server &&\n            e.Date == date &&\n            e.ProviderId == providerId));\n    }\n\n    [Theory, BitAutoData]\n    public async Task LogOrganizationUserEvent_LogsRequiredInfo(OrganizationUser orgUser, EventType eventType, DateTime date,\n        Guid actingUserId, Guid providerId, string ipAddress, DeviceType deviceType, SutProvider<EventService> sutProvider)\n    {\n        var orgAbilities = new Dictionary<Guid, OrganizationAbility>()\n        {\n            {orgUser.OrganizationId, new OrganizationAbility() { UseEvents = true, Enabled = true } }\n        };\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(orgAbilities);\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);\n        sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);\n        sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);\n\n        await sutProvider.Sut.LogOrganizationUserEventAsync(orgUser, eventType, date);\n\n        var expected = new List<IEvent>() {\n            new EventMessage()\n            {\n                IpAddress = ipAddress,\n                DeviceType = deviceType,\n                OrganizationId = orgUser.OrganizationId,\n                UserId = orgUser.UserId,\n                OrganizationUserId = orgUser.Id,\n                ProviderId = providerId,\n                Type = eventType,\n                ActingUserId = actingUserId,\n                Date = date\n            }\n        };\n\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { \"IdempotencyId\" })));\n    }\n\n    [Theory, BitAutoData]\n    public async Task LogOrganizationUserEvent_WithEventSystemUser_LogsRequiredInfo(OrganizationUser orgUser, EventType eventType, EventSystemUser eventSystemUser, DateTime date,\n        Guid actingUserId, Guid providerId, string ipAddress, SutProvider<EventService> sutProvider)\n    {\n        var orgAbilities = new Dictionary<Guid, OrganizationAbility>()\n        {\n            {orgUser.OrganizationId, new OrganizationAbility() { UseEvents = true, Enabled = true } }\n        };\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(orgAbilities);\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);\n        sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);\n\n        await sutProvider.Sut.LogOrganizationUserEventAsync(orgUser, eventType, eventSystemUser, date);\n\n        var expected = new List<IEvent>() {\n            new EventMessage()\n            {\n                IpAddress = ipAddress,\n                OrganizationId = orgUser.OrganizationId,\n                UserId = orgUser.UserId,\n                OrganizationUserId = orgUser.Id,\n                ProviderId = providerId,\n                Type = eventType,\n                ActingUserId = actingUserId,\n                Date = date,\n                SystemUser = eventSystemUser\n            }\n        };\n\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { \"IdempotencyId\" })));\n    }\n\n    [Theory, BitAutoData]\n    public async Task LogProviderUserEvent_LogsRequiredInfo(ProviderUser providerUser, EventType eventType, DateTime date,\n        Guid actingUserId, Guid providerId, string ipAddress, DeviceType deviceType, SutProvider<EventService> sutProvider)\n    {\n        var providerAbilities = new Dictionary<Guid, ProviderAbility>()\n        {\n            {providerUser.ProviderId, new ProviderAbility() { UseEvents = true, Enabled = true } }\n        };\n        sutProvider.GetDependency<IApplicationCacheService>().GetProviderAbilitiesAsync().Returns(providerAbilities);\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);\n        sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);\n        sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);\n\n        await sutProvider.Sut.LogProviderUserEventAsync(providerUser, eventType, date);\n\n        var expected = new List<IEvent>() {\n            new EventMessage()\n            {\n                IpAddress = ipAddress,\n                DeviceType = deviceType,\n                ProviderId = providerUser.ProviderId,\n                UserId = providerUser.UserId,\n                ProviderUserId = providerUser.Id,\n                Type = eventType,\n                ActingUserId = actingUserId,\n                Date = date\n            }\n        };\n\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { \"IdempotencyId\" })));\n    }\n\n    [Theory, BitAutoData]\n    public async Task LogProviderOrganizationEventsAsync_LogsRequiredInfo(Provider provider, ICollection<ProviderOrganization> providerOrganizations, EventType eventType, DateTime date,\n        Guid actingUserId, Guid providerId, string ipAddress, DeviceType deviceType, SutProvider<EventService> sutProvider)\n    {\n        foreach (var providerOrganization in providerOrganizations)\n        {\n            providerOrganization.ProviderId = provider.Id;\n        }\n\n        var providerAbilities = new Dictionary<Guid, ProviderAbility>()\n        {\n            { provider.Id, new ProviderAbility() { UseEvents = true, Enabled = true } }\n        };\n        sutProvider.GetDependency<IApplicationCacheService>().GetProviderAbilitiesAsync().Returns(providerAbilities);\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);\n        sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);\n        sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);\n        sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);\n\n        await sutProvider.Sut.LogProviderOrganizationEventsAsync(providerOrganizations.Select(po => (po, eventType, (DateTime?)date)));\n\n        var expected = providerOrganizations.Select(po =>\n            new EventMessage()\n            {\n                DeviceType = deviceType,\n                IpAddress = ipAddress,\n                ProviderId = provider.Id,\n                ProviderOrganizationId = po.Id,\n                Type = eventType,\n                ActingUserId = actingUserId,\n                Date = date\n            }).ToList();\n\n        await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { \"IdempotencyId\" })));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/IntegrationFilterFactoryTests.cs",
    "content": "﻿using Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Models.Data;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\npublic class IntegrationFilterFactoryTests\n{\n    [Theory, BitAutoData]\n    public void BuildEqualityFilter_ReturnsCorrectMatch(EventMessage message)\n    {\n        var different = Guid.NewGuid();\n        var expected = Guid.NewGuid();\n        message.UserId = expected;\n\n        var filter = IntegrationFilterFactory.BuildEqualityFilter<Guid?>(\"UserId\");\n\n        Assert.True(filter(message, expected));\n        Assert.False(filter(message, different));\n    }\n\n    [Theory, BitAutoData]\n    public void BuildEqualityFilter_UserIdIsNull_ReturnsFalse(EventMessage message)\n    {\n        message.UserId = null;\n\n        var filter = IntegrationFilterFactory.BuildEqualityFilter<Guid?>(\"UserId\");\n\n        Assert.False(filter(message, Guid.NewGuid()));\n    }\n\n    [Theory, BitAutoData]\n    public void BuildInFilter_ReturnsCorrectMatch(EventMessage message)\n    {\n        var match = Guid.NewGuid();\n        message.UserId = match;\n        var inList = new List<Guid?> { Guid.NewGuid(), match, Guid.NewGuid() };\n        var outList = new List<Guid?> { Guid.NewGuid(), Guid.NewGuid() };\n\n        var filter = IntegrationFilterFactory.BuildInFilter<Guid?>(\"UserId\");\n\n        Assert.True(filter(message, inList));\n        Assert.False(filter(message, outList));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/IntegrationFilterServiceTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Text.Json;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Models.Data;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\npublic class IntegrationFilterServiceTests\n{\n    private readonly IntegrationFilterService _service = new();\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_EqualsUserId_Matches(EventMessage eventMessage)\n    {\n        var userId = Guid.NewGuid();\n        eventMessage.UserId = userId;\n\n        var group = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new()\n                {\n                    Property = \"UserId\",\n                    Operation = IntegrationFilterOperation.Equals,\n                    Value = userId\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.True(result);\n\n        var jsonGroup = JsonSerializer.Serialize(group);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_EqualsUserIdString_Matches(EventMessage eventMessage)\n    {\n        var userId = Guid.NewGuid();\n        eventMessage.UserId = userId;\n\n        var group = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new()\n                {\n                    Property = \"UserId\",\n                    Operation = IntegrationFilterOperation.Equals,\n                    Value = userId.ToString()\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.True(result);\n\n        var jsonGroup = JsonSerializer.Serialize(group);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_EqualsUserId_DoesNotMatch(EventMessage eventMessage)\n    {\n        eventMessage.UserId = Guid.NewGuid();\n        var otherUserId = Guid.NewGuid();\n\n        var group = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new()\n                {\n                    Property = \"UserId\",\n                    Operation = IntegrationFilterOperation.Equals,\n                    Value = otherUserId\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.False(result);\n\n        var jsonGroup = JsonSerializer.Serialize(group);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_NotEqualsUniqueUserId_ReturnsTrue(EventMessage eventMessage)\n    {\n        var otherId = Guid.NewGuid();\n        eventMessage.UserId = otherId;\n\n        var group = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new()\n                {\n                    Property = \"UserId\",\n                    Operation = IntegrationFilterOperation.NotEquals,\n                    Value = Guid.NewGuid()\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.True(result);\n\n        var jsonGroup = JsonSerializer.Serialize(group);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_NotEqualsMatchingUserId_ReturnsFalse(EventMessage eventMessage)\n    {\n        var id = Guid.NewGuid();\n        eventMessage.UserId = id;\n\n        var group = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new()\n                {\n                    Property = \"UserId\",\n                    Operation = IntegrationFilterOperation.NotEquals,\n                    Value = id\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.False(result);\n\n        var jsonGroup = JsonSerializer.Serialize(group);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_InCollectionId_Matches(EventMessage eventMessage)\n    {\n        var id = Guid.NewGuid();\n        eventMessage.CollectionId = id;\n\n        var group = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new()\n                {\n                    Property = \"CollectionId\",\n                    Operation = IntegrationFilterOperation.In,\n                    Value = new Guid?[] { Guid.NewGuid(), id }\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.True(result);\n\n        var jsonGroup = JsonSerializer.Serialize(group);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_InCollectionId_DoesNotMatch(EventMessage eventMessage)\n    {\n        eventMessage.CollectionId = Guid.NewGuid();\n\n        var group = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new()\n                {\n                    Property = \"CollectionId\",\n                    Operation = IntegrationFilterOperation.In,\n                    Value = new Guid?[] { Guid.NewGuid(), Guid.NewGuid() }\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.False(result);\n\n        var jsonGroup = JsonSerializer.Serialize(group);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_NotInCollectionIdUniqueId_ReturnsTrue(EventMessage eventMessage)\n    {\n        eventMessage.CollectionId = Guid.NewGuid();\n\n        var group = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new()\n                {\n                    Property = \"CollectionId\",\n                    Operation = IntegrationFilterOperation.NotIn,\n                    Value = new Guid?[] { Guid.NewGuid(), Guid.NewGuid() }\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.True(result);\n\n        var jsonGroup = JsonSerializer.Serialize(group);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_NotInCollectionIdPresent_ReturnsFalse(EventMessage eventMessage)\n    {\n        var matchId = Guid.NewGuid();\n        eventMessage.CollectionId = matchId;\n\n        var group = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new()\n                {\n                    Property = \"CollectionId\",\n                    Operation = IntegrationFilterOperation.NotIn,\n                    Value = new Guid?[] { Guid.NewGuid(), matchId }\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.False(result);\n\n        var jsonGroup = JsonSerializer.Serialize(group);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_NestedGroups_AllMatch(EventMessage eventMessage)\n    {\n        var id = Guid.NewGuid();\n        var collectionId = Guid.NewGuid();\n        eventMessage.UserId = id;\n        eventMessage.CollectionId = collectionId;\n\n        var nestedGroup = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Rules =\n            [\n                new() { Property = \"UserId\", Operation = IntegrationFilterOperation.Equals, Value = id },\n                new()\n                {\n                    Property = \"CollectionId\",\n                    Operation = IntegrationFilterOperation.In,\n                    Value = new Guid?[] { collectionId, Guid.NewGuid() }\n                }\n            ]\n        };\n\n        var topGroup = new IntegrationFilterGroup\n        {\n            AndOperator = true,\n            Groups = [nestedGroup]\n        };\n\n        var result = _service.EvaluateFilterGroup(topGroup, eventMessage);\n        Assert.True(result);\n\n        var jsonGroup = JsonSerializer.Serialize(topGroup);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_NestedGroups_AnyMatch(EventMessage eventMessage)\n    {\n        var id = Guid.NewGuid();\n        var collectionId = Guid.NewGuid();\n        eventMessage.UserId = id;\n        eventMessage.CollectionId = collectionId;\n\n        var nestedGroup = new IntegrationFilterGroup\n        {\n            AndOperator = false,\n            Rules =\n            [\n                new() { Property = \"UserId\", Operation = IntegrationFilterOperation.Equals, Value = id },\n                new()\n                {\n                    Property = \"CollectionId\",\n                    Operation = IntegrationFilterOperation.In,\n                    Value = new Guid?[] { Guid.NewGuid() }\n                }\n            ]\n        };\n\n        var topGroup = new IntegrationFilterGroup\n        {\n            AndOperator = false,\n            Groups = [nestedGroup]\n        };\n\n        var result = _service.EvaluateFilterGroup(topGroup, eventMessage);\n        Assert.True(result);\n\n        var jsonGroup = JsonSerializer.Serialize(topGroup);\n        var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);\n        Assert.NotNull(roundtrippedGroup);\n        Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_UnknownProperty_ReturnsFalse(EventMessage eventMessage)\n    {\n        var group = new IntegrationFilterGroup\n        {\n            Rules =\n            [\n                new() { Property = \"NotARealProperty\", Operation = IntegrationFilterOperation.Equals, Value = \"test\" }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_UnsupportedOperation_ReturnsFalse(EventMessage eventMessage)\n    {\n        var group = new IntegrationFilterGroup\n        {\n            Rules =\n            [\n                new()\n                {\n                    Property = \"UserId\",\n                    Operation = (IntegrationFilterOperation)999, // Unknown operation\n                    Value = eventMessage.UserId\n                }\n            ]\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_WrongTypeForInList_ThrowsException(EventMessage eventMessage)\n    {\n        var group = new IntegrationFilterGroup\n        {\n            Rules =\n            [\n                new()\n                {\n                    Property = \"CollectionId\",\n                    Operation = IntegrationFilterOperation.In,\n                    Value = \"not an array\" // Should be Guid[]\n                }\n            ]\n        };\n\n        Assert.Throws<InvalidCastException>(() =>\n            _service.EvaluateFilterGroup(group, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_NullValue_ThrowsException(EventMessage eventMessage)\n    {\n        var group = new IntegrationFilterGroup\n        {\n            Rules =\n            [\n                new()\n                {\n                    Property = \"UserId\",\n                    Operation = IntegrationFilterOperation.Equals,\n                    Value = null\n                }\n            ]\n        };\n\n        Assert.Throws<InvalidCastException>(() =>\n            _service.EvaluateFilterGroup(group, eventMessage));\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_EmptyRuleList_ReturnsTrue(EventMessage eventMessage)\n    {\n        var group = new IntegrationFilterGroup\n        {\n            Rules = [],\n            Groups = [],\n            AndOperator = true\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.True(result); // Nothing to fail, returns true by design\n    }\n\n    [Theory, BitAutoData]\n    public void EvaluateFilterGroup_InvalidNestedGroup_ReturnsFalse(EventMessage eventMessage)\n    {\n        var group = new IntegrationFilterGroup\n        {\n            Groups =\n            [\n                new()\n                {\n                    Rules =\n                    [\n                        new()\n                        {\n                            Property = \"Nope\",\n                            Operation = IntegrationFilterOperation.Equals,\n                            Value = \"bad\"\n                        }\n                    ]\n                }\n            ],\n            AndOperator = true\n        };\n\n        var result = _service.EvaluateFilterGroup(group, eventMessage);\n        Assert.False(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/IntegrationHandlerTests.cs",
    "content": "﻿using System.Net;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Services;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\npublic class IntegrationHandlerTests\n{\n    [Fact]\n    public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage()\n    {\n        var sut = new TestIntegrationHandler();\n        var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()\n        {\n            Configuration = new WebhookIntegrationConfigurationDetails(new Uri(\"https://localhost\"), \"Bearer\", \"AUTH-TOKEN\"),\n            MessageId = \"TestMessageId\",\n            OrganizationId = \"TestOrganizationId\",\n            IntegrationType = IntegrationType.Webhook,\n            RenderedTemplate = \"Template\",\n            DelayUntilDate = null,\n            RetryCount = 0\n        };\n\n        var result = await sut.HandleAsync(expected.ToJson());\n        var typedResult = Assert.IsType<IntegrationMessage<WebhookIntegrationConfigurationDetails>>(result.Message);\n\n        Assert.Equal(expected.MessageId, typedResult.MessageId);\n        Assert.Equal(expected.OrganizationId, typedResult.OrganizationId);\n        Assert.Equal(expected.Configuration, typedResult.Configuration);\n        Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);\n        Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);\n    }\n\n    [Theory]\n    [InlineData(HttpStatusCode.Unauthorized)]\n    [InlineData(HttpStatusCode.Forbidden)]\n    public void ClassifyHttpStatusCode_AuthenticationFailed(HttpStatusCode code)\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.AuthenticationFailed,\n            TestIntegrationHandler.Classify(code));\n    }\n\n    [Theory]\n    [InlineData(HttpStatusCode.NotFound)]\n    [InlineData(HttpStatusCode.Gone)]\n    [InlineData(HttpStatusCode.MovedPermanently)]\n    [InlineData(HttpStatusCode.TemporaryRedirect)]\n    [InlineData(HttpStatusCode.PermanentRedirect)]\n    public void ClassifyHttpStatusCode_ConfigurationError(HttpStatusCode code)\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.ConfigurationError,\n            TestIntegrationHandler.Classify(code));\n    }\n\n    [Fact]\n    public void ClassifyHttpStatusCode_TooManyRequests_IsRateLimited()\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.RateLimited,\n            TestIntegrationHandler.Classify(HttpStatusCode.TooManyRequests));\n    }\n\n    [Fact]\n    public void ClassifyHttpStatusCode_RequestTimeout_IsTransient()\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.TransientError,\n            TestIntegrationHandler.Classify(HttpStatusCode.RequestTimeout));\n    }\n\n    [Theory]\n    [InlineData(HttpStatusCode.InternalServerError)]\n    [InlineData(HttpStatusCode.BadGateway)]\n    [InlineData(HttpStatusCode.GatewayTimeout)]\n    public void ClassifyHttpStatusCode_Common5xx_AreTransient(HttpStatusCode code)\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.TransientError,\n            TestIntegrationHandler.Classify(code));\n    }\n\n    [Fact]\n    public void ClassifyHttpStatusCode_ServiceUnavailable_IsServiceUnavailable()\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.ServiceUnavailable,\n            TestIntegrationHandler.Classify(HttpStatusCode.ServiceUnavailable));\n    }\n\n    [Fact]\n    public void ClassifyHttpStatusCode_NotImplemented_IsPermanentFailure()\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.PermanentFailure,\n            TestIntegrationHandler.Classify(HttpStatusCode.NotImplemented));\n    }\n\n    [Fact]\n    public void FClassifyHttpStatusCode_Unhandled3xx_IsConfigurationError()\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.ConfigurationError,\n            TestIntegrationHandler.Classify(HttpStatusCode.Found));\n    }\n\n    [Fact]\n    public void ClassifyHttpStatusCode_Unhandled4xx_IsConfigurationError()\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.ConfigurationError,\n            TestIntegrationHandler.Classify(HttpStatusCode.BadRequest));\n    }\n\n    [Fact]\n    public void ClassifyHttpStatusCode_Unhandled5xx_IsServiceUnavailable()\n    {\n        Assert.Equal(\n            IntegrationFailureCategory.ServiceUnavailable,\n            TestIntegrationHandler.Classify(HttpStatusCode.HttpVersionNotSupported));\n    }\n\n    [Fact]\n    public void ClassifyHttpStatusCode_UnknownCode_DefaultsToServiceUnavailable()\n    {\n        // cast an out-of-range value to ensure default path is stable\n        Assert.Equal(\n            IntegrationFailureCategory.ServiceUnavailable,\n            TestIntegrationHandler.Classify((HttpStatusCode)799));\n    }\n\n    private class TestIntegrationHandler : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>\n    {\n        public override Task<IntegrationHandlerResult> HandleAsync(\n            IntegrationMessage<WebhookIntegrationConfigurationDetails> message)\n        {\n            return Task.FromResult(IntegrationHandlerResult.Succeed(message: message));\n        }\n\n        public static IntegrationFailureCategory Classify(HttpStatusCode code) => ClassifyHttpStatusCode(code);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/OrganizationIntegrationConfigurationValidatorTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Enums;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\npublic class OrganizationIntegrationConfigurationValidatorTests\n{\n    private readonly OrganizationIntegrationConfigurationValidator _sut = new();\n\n    [Fact]\n    public void ValidateConfiguration_CloudBillingSyncIntegration_ReturnsFalse()\n    {\n        var configuration = new OrganizationIntegrationConfiguration\n        {\n            Configuration = \"{}\",\n            Template = \"template\"\n        };\n\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.CloudBillingSync, configuration));\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\"    \")]\n    public void ValidateConfiguration_EmptyTemplate_ReturnsFalse(string? template)\n    {\n        var config1 = new OrganizationIntegrationConfiguration\n        {\n            Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration(ChannelId: \"C12345\")),\n            Template = template\n        };\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Slack, config1));\n\n        var config2 = new OrganizationIntegrationConfiguration\n        {\n            Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri(\"https://example.com\"))),\n            Template = template\n        };\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, config2));\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(\"    \")]\n    public void ValidateConfiguration_EmptyNonNullConfiguration_ReturnsFalse(string? config)\n    {\n        var config1 = new OrganizationIntegrationConfiguration\n        {\n            Configuration = config,\n            Template = \"template\"\n        };\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Hec, config1));\n\n        var config2 = new OrganizationIntegrationConfiguration\n        {\n            Configuration = config,\n            Template = \"template\"\n        };\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Datadog, config2));\n\n        var config3 = new OrganizationIntegrationConfiguration\n        {\n            Configuration = config,\n            Template = \"template\"\n        };\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Teams, config3));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_NullConfiguration_ReturnsTrue()\n    {\n        var config1 = new OrganizationIntegrationConfiguration\n        {\n            Configuration = null,\n            Template = \"template\"\n        };\n        Assert.True(_sut.ValidateConfiguration(IntegrationType.Hec, config1));\n\n        var config2 = new OrganizationIntegrationConfiguration\n        {\n            Configuration = null,\n            Template = \"template\"\n        };\n        Assert.True(_sut.ValidateConfiguration(IntegrationType.Datadog, config2));\n\n        var config3 = new OrganizationIntegrationConfiguration\n        {\n            Configuration = null,\n            Template = \"template\"\n        };\n        Assert.True(_sut.ValidateConfiguration(IntegrationType.Teams, config3));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_InvalidJsonConfiguration_ReturnsFalse()\n    {\n        var config = new OrganizationIntegrationConfiguration\n        {\n            Configuration = \"{not valid json}\",\n            Template = \"template\"\n        };\n\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Slack, config));\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, config));\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Hec, config));\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Datadog, config));\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Teams, config));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_InvalidJsonFilters_ReturnsFalse()\n    {\n        var configuration = new OrganizationIntegrationConfiguration\n        {\n            Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri(\"https://example.com\"))),\n            Template = \"template\",\n            Filters = \"{Not valid json}\"\n        };\n\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_ScimIntegration_ReturnsFalse()\n    {\n        var configuration = new OrganizationIntegrationConfiguration\n        {\n            Configuration = \"{}\",\n            Template = \"template\"\n        };\n\n        Assert.False(_sut.ValidateConfiguration(IntegrationType.Scim, configuration));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_ValidSlackConfiguration_ReturnsTrue()\n    {\n        var configuration = new OrganizationIntegrationConfiguration\n        {\n            Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration(ChannelId: \"C12345\")),\n            Template = \"template\"\n        };\n\n        Assert.True(_sut.ValidateConfiguration(IntegrationType.Slack, configuration));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_ValidSlackConfigurationWithFilters_ReturnsTrue()\n    {\n        var configuration = new OrganizationIntegrationConfiguration\n        {\n            Configuration = JsonSerializer.Serialize(new SlackIntegrationConfiguration(\"C12345\")),\n            Template = \"template\",\n            Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()\n            {\n                AndOperator = true,\n                Rules = [\n                    new IntegrationFilterRule()\n                    {\n                        Operation = IntegrationFilterOperation.Equals,\n                        Property = \"CollectionId\",\n                        Value = Guid.NewGuid()\n                    }\n                ],\n                Groups = []\n            })\n        };\n\n        Assert.True(_sut.ValidateConfiguration(IntegrationType.Slack, configuration));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_ValidNoAuthWebhookConfiguration_ReturnsTrue()\n    {\n        var configuration = new OrganizationIntegrationConfiguration\n        {\n            Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri(\"https://localhost\"))),\n            Template = \"template\"\n        };\n\n        Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_ValidWebhookConfiguration_ReturnsTrue()\n    {\n        var configuration = new OrganizationIntegrationConfiguration\n        {\n            Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(\n                Uri: new Uri(\"https://localhost\"),\n                Scheme: \"Bearer\",\n                Token: \"AUTH-TOKEN\")),\n            Template = \"template\"\n        };\n\n        Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_ValidWebhookConfigurationWithFilters_ReturnsTrue()\n    {\n        var configuration = new OrganizationIntegrationConfiguration\n        {\n            Configuration = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(\n                Uri: new Uri(\"https://example.com\"),\n                Scheme: \"Bearer\",\n                Token: \"AUTH-TOKEN\")),\n            Template = \"template\",\n            Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()\n            {\n                AndOperator = true,\n                Rules = [\n                    new IntegrationFilterRule()\n                    {\n                        Operation = IntegrationFilterOperation.Equals,\n                        Property = \"CollectionId\",\n                        Value = Guid.NewGuid()\n                    }\n                ],\n                Groups = []\n            })\n        };\n\n        Assert.True(_sut.ValidateConfiguration(IntegrationType.Webhook, configuration));\n    }\n\n    [Fact]\n    public void ValidateConfiguration_UnknownIntegrationType_ReturnsFalse()\n    {\n        var unknownType = (IntegrationType)999;\n        var configuration = new OrganizationIntegrationConfiguration\n        {\n            Configuration = \"{}\",\n            Template = \"template\"\n        };\n\n        Assert.False(_sut.ValidateConfiguration(unknownType, configuration));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/RabbitMqEventListenerServiceTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Text.Json;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing RabbitMQ.Client;\nusing RabbitMQ.Client.Events;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class RabbitMqEventListenerServiceTests\n{\n    private readonly TestListenerConfiguration _config = new();\n    private readonly ILogger _logger = Substitute.For<ILogger>();\n\n    private SutProvider<RabbitMqEventListenerService<TestListenerConfiguration>> GetSutProvider()\n    {\n        var loggerFactory = Substitute.For<ILoggerFactory>();\n        loggerFactory.CreateLogger<object>().ReturnsForAnyArgs(_logger);\n        return new SutProvider<RabbitMqEventListenerService<TestListenerConfiguration>>()\n            .SetDependency(_config)\n            .SetDependency(loggerFactory)\n            .Create();\n    }\n\n    [Fact]\n    public void Constructor_CreatesLogWithCorrectCategory()\n    {\n        var sutProvider = GetSutProvider();\n\n        var fullName = typeof(RabbitMqEventListenerService<>).FullName ?? \"\";\n        var tickIndex = fullName.IndexOf('`');\n        var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName;\n        var categoryName = cleanedName + '.' + _config.EventQueueName;\n\n        sutProvider.GetDependency<ILoggerFactory>().Received(1).CreateLogger(categoryName);\n    }\n\n    [Fact]\n    public async Task StartAsync_CreatesQueue()\n    {\n        var sutProvider = GetSutProvider();\n        var cancellationToken = CancellationToken.None;\n        await sutProvider.Sut.StartAsync(cancellationToken);\n\n        await sutProvider.GetDependency<IRabbitMqService>().Received(1).CreateEventQueueAsync(\n            Arg.Is(_config.EventQueueName),\n            Arg.Is(cancellationToken)\n        );\n    }\n\n    [Fact]\n    public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError()\n    {\n        var sutProvider = GetSutProvider();\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: Array.Empty<byte>());\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<JsonException>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError()\n    {\n        var sutProvider = GetSutProvider();\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: JsonSerializer.SerializeToUtf8Bytes(\"{ Invalid JSON\"));\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => (o.ToString() ?? \"\").Contains(\"Invalid JSON\")),\n            Arg.Any<Exception>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError()\n    {\n        var sutProvider = GetSutProvider();\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: JsonSerializer.SerializeToUtf8Bytes(new[] { \"not a valid\", \"list of event messages\" }));\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<JsonException>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Fact]\n    public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError()\n    {\n        var sutProvider = GetSutProvider();\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: JsonSerializer.SerializeToUtf8Bytes(DateTime.UtcNow));  // wrong object - not EventMessage\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);\n\n        _logger.Received(1).Log(\n            LogLevel.Error,\n            Arg.Any<EventId>(),\n            Arg.Any<object>(),\n            Arg.Any<JsonException>(),\n            Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message)\n    {\n        var sutProvider = GetSutProvider();\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: JsonSerializer.SerializeToUtf8Bytes(message));\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);\n\n        await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleEventAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { \"IdempotencyId\" })));\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable<EventMessage> messages)\n    {\n        var sutProvider = GetSutProvider();\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: JsonSerializer.SerializeToUtf8Bytes(messages));\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs);\n\n        await sutProvider.GetDependency<IEventMessageHandler>().Received(1).HandleManyEventsAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { \"IdempotencyId\" })));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/RabbitMqIntegrationListenerServiceTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Text;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Core.Test.Dirt.Models.Data.EventIntegrations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing RabbitMQ.Client;\nusing RabbitMQ.Client.Events;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class RabbitMqIntegrationListenerServiceTests\n{\n    private readonly DateTime _now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);\n    private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();\n    private readonly ILogger _logger = Substitute.For<ILogger>();\n    private readonly IRabbitMqService _rabbitMqService = Substitute.For<IRabbitMqService>();\n    private readonly TestListenerConfiguration _config = new();\n\n    private SutProvider<RabbitMqIntegrationListenerService<TestListenerConfiguration>> GetSutProvider()\n    {\n        var loggerFactory = Substitute.For<ILoggerFactory>();\n        loggerFactory.CreateLogger<object>().ReturnsForAnyArgs(_logger);\n        var sutProvider = new SutProvider<RabbitMqIntegrationListenerService<TestListenerConfiguration>>()\n            .SetDependency(_config)\n            .SetDependency(_handler)\n            .SetDependency(loggerFactory)\n            .SetDependency(_rabbitMqService)\n            .WithFakeTimeProvider()\n            .Create();\n        sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(_now);\n\n        return sutProvider;\n    }\n\n    [Fact]\n    public void Constructor_CreatesLogWithCorrectCategory()\n    {\n        var sutProvider = GetSutProvider();\n\n        var fullName = typeof(RabbitMqIntegrationListenerService<>).FullName ?? \"\";\n        var tickIndex = fullName.IndexOf('`');\n        var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName;\n        var categoryName = cleanedName + '.' + _config.IntegrationQueueName;\n\n        sutProvider.GetDependency<ILoggerFactory>().Received(1).CreateLogger(categoryName);\n    }\n\n    [Fact]\n    public async Task StartAsync_CreatesQueues()\n    {\n        var sutProvider = GetSutProvider();\n        var cancellationToken = CancellationToken.None;\n        await sutProvider.Sut.StartAsync(cancellationToken);\n\n        await sutProvider.GetDependency<IRabbitMqService>().Received(1).CreateIntegrationQueuesAsync(\n            Arg.Is(_config.IntegrationQueueName),\n            Arg.Is(_config.IntegrationRetryQueueName),\n            Arg.Is(((IIntegrationListenerConfiguration)_config).RoutingKey),\n            Arg.Is(cancellationToken)\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessReceivedMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)\n    {\n        var sutProvider = GetSutProvider();\n        var cancellationToken = CancellationToken.None;\n        await sutProvider.Sut.StartAsync(cancellationToken);\n\n        message.DelayUntilDate = null;\n        message.RetryCount = 0;\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: Encoding.UTF8.GetBytes(message.ToJson())\n        );\n        var result = IntegrationHandlerResult.Fail(\n            message: message,\n            category: IntegrationFailureCategory.AuthenticationFailed, // NOT retryable\n            failureReason: \"403\");\n        _handler.HandleAsync(Arg.Any<string>()).Returns(result);\n\n        var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());\n        Assert.NotNull(expected);\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);\n\n        await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));\n\n        await _rabbitMqService.Received(1).PublishToDeadLetterAsync(\n            Arg.Any<IChannel>(),\n            Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { \"DelayUntilDate\" })),\n            Arg.Any<CancellationToken>());\n\n        _logger.Received().Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => (o.ToString() ?? \"\").Contains(\"Integration failure - non-retryable.\")),\n            Arg.Any<Exception?>(),\n            Arg.Any<Func<object, Exception?, string>>());\n\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .PublishToRetryAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessReceivedMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)\n    {\n        var sutProvider = GetSutProvider();\n        var cancellationToken = CancellationToken.None;\n        await sutProvider.Sut.StartAsync(cancellationToken);\n\n        message.DelayUntilDate = null;\n        message.RetryCount = _config.MaxRetries;\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: Encoding.UTF8.GetBytes(message.ToJson())\n        );\n        var result = IntegrationHandlerResult.Fail(\n            message: message,\n            category: IntegrationFailureCategory.TransientError, // Retryable\n            failureReason: \"403\");\n        _handler.HandleAsync(Arg.Any<string>()).Returns(result);\n\n        var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());\n        Assert.NotNull(expected);\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);\n\n        expected.ApplyRetry(result.DelayUntilDate);\n        await _rabbitMqService.Received(1).PublishToDeadLetterAsync(\n            Arg.Any<IChannel>(),\n            Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { \"DelayUntilDate\" })),\n            Arg.Any<CancellationToken>());\n\n        _logger.Received().Log(\n            LogLevel.Warning,\n            Arg.Any<EventId>(),\n            Arg.Is<object>(o => (o.ToString() ?? \"\").Contains(\"Integration failure - max retries exceeded.\")),\n            Arg.Any<Exception?>(),\n            Arg.Any<Func<object, Exception?, string>>());\n\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .PublishToRetryAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessReceivedMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)\n    {\n        var sutProvider = GetSutProvider();\n        var cancellationToken = CancellationToken.None;\n        await sutProvider.Sut.StartAsync(cancellationToken);\n\n        message.DelayUntilDate = null;\n        message.RetryCount = 0;\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: Encoding.UTF8.GetBytes(message.ToJson())\n        );\n        var result = IntegrationHandlerResult.Fail(\n            message: message,\n            category: IntegrationFailureCategory.TransientError, // Retryable\n            failureReason: \"403\");\n        _handler.HandleAsync(Arg.Any<string>()).Returns(result);\n\n        var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());\n        Assert.NotNull(expected);\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);\n\n        await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson()));\n\n        expected.ApplyRetry(result.DelayUntilDate);\n        await _rabbitMqService.Received(1).PublishToRetryAsync(\n            Arg.Any<IChannel>(),\n            Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { \"DelayUntilDate\" })),\n            Arg.Any<CancellationToken>());\n\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .PublishToDeadLetterAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessReceivedMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)\n    {\n        var sutProvider = GetSutProvider();\n        var cancellationToken = CancellationToken.None;\n        await sutProvider.Sut.StartAsync(cancellationToken);\n\n        message.DelayUntilDate = null;\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: Encoding.UTF8.GetBytes(message.ToJson())\n        );\n        var result = IntegrationHandlerResult.Succeed(message);\n        _handler.HandleAsync(Arg.Any<string>()).Returns(result);\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);\n\n        await _handler.Received(1).HandleAsync(Arg.Is(message.ToJson()));\n\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .PublishToRetryAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .PublishToDeadLetterAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ProcessReceivedMessageAsync_TooEarlyRetry_RepublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)\n    {\n        var sutProvider = GetSutProvider();\n        var cancellationToken = CancellationToken.None;\n        await sutProvider.Sut.StartAsync(cancellationToken);\n\n        message.DelayUntilDate = _now.AddMinutes(1);\n        var eventArgs = new BasicDeliverEventArgs(\n            consumerTag: string.Empty,\n            deliveryTag: 0,\n            redelivered: true,\n            exchange: string.Empty,\n            routingKey: string.Empty,\n            new BasicProperties(),\n            body: Encoding.UTF8.GetBytes(message.ToJson())\n        );\n\n        await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken);\n\n        await _rabbitMqService.Received(1)\n            .RepublishToRetryQueueAsync(Arg.Any<IChannel>(), Arg.Any<BasicDeliverEventArgs>());\n\n        await _handler.DidNotReceiveWithAnyArgs().HandleAsync(Arg.Any<string>());\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .PublishToRetryAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());\n        await _rabbitMqService.DidNotReceiveWithAnyArgs()\n            .PublishToDeadLetterAsync(Arg.Any<IChannel>(), Arg.Any<IntegrationMessage>(), Arg.Any<CancellationToken>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/RepositoryEventWriteServiceTests.cs",
    "content": "﻿using Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\npublic class RepositoryEventWriteServiceTests\n{\n    private readonly RepositoryEventWriteService _sut;\n\n    private readonly IEventRepository _eventRepository;\n\n    public RepositoryEventWriteServiceTests()\n    {\n        _eventRepository = Substitute.For<IEventRepository>();\n\n        _sut = new RepositoryEventWriteService(_eventRepository);\n    }\n\n    // Remove this test when we add actual tests. It only proves that\n    // we've properly constructed the system under test.\n    [Fact]\n    public void ServiceExists()\n    {\n        Assert.NotNull(_sut);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/SlackIntegrationHandlerTests.cs",
    "content": "﻿using Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Models.Data.Slack;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class SlackIntegrationHandlerTests\n{\n    private readonly ISlackService _slackService = Substitute.For<ISlackService>();\n    private readonly string _channelId = \"C12345\";\n    private readonly string _token = \"xoxb-test-token\";\n\n    private SutProvider<SlackIntegrationHandler> GetSutProvider()\n    {\n        return new SutProvider<SlackIntegrationHandler>()\n            .SetDependency(_slackService)\n            .Create();\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<SlackIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token);\n\n        _slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n            .Returns(new SlackSendMessageResponse() { Ok = true, Channel = _channelId });\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.True(result.Success);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_token)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))\n        );\n    }\n\n    [Theory]\n    [InlineData(\"service_unavailable\")]\n    [InlineData(\"ratelimited\")]\n    [InlineData(\"rate_limited\")]\n    [InlineData(\"internal_error\")]\n    [InlineData(\"message_limit_exceeded\")]\n    public async Task HandleAsync_FailedRetryableRequest_ReturnsFailureWithRetryable(string error)\n    {\n        var sutProvider = GetSutProvider();\n        var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()\n        {\n            Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),\n            MessageId = \"MessageId\",\n            RenderedTemplate = \"Test Message\"\n        };\n\n        _slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n            .Returns(new SlackSendMessageResponse() { Ok = false, Channel = _channelId, Error = error });\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.True(result.Retryable);\n        Assert.NotNull(result.FailureReason);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_token)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))\n        );\n    }\n\n    [Theory]\n    [InlineData(\"access_denied\")]\n    [InlineData(\"channel_not_found\")]\n    [InlineData(\"token_expired\")]\n    [InlineData(\"token_revoked\")]\n    public async Task HandleAsync_FailedNonRetryableRequest_ReturnsNonRetryableFailure(string error)\n    {\n        var sutProvider = GetSutProvider();\n        var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()\n        {\n            Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),\n            MessageId = \"MessageId\",\n            RenderedTemplate = \"Test Message\"\n        };\n\n        _slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n            .Returns(new SlackSendMessageResponse() { Ok = false, Channel = _channelId, Error = error });\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.False(result.Retryable);\n        Assert.NotNull(result.FailureReason);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_token)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))\n        );\n    }\n\n    [Fact]\n    public async Task HandleAsync_NullResponse_ReturnsRetryableFailure()\n    {\n        var sutProvider = GetSutProvider();\n        var message = new IntegrationMessage<SlackIntegrationConfigurationDetails>()\n        {\n            Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token),\n            MessageId = \"MessageId\",\n            RenderedTemplate = \"Test Message\"\n        };\n\n        _slackService.SendSlackMessageByChannelIdAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())\n            .Returns((SlackSendMessageResponse?)null);\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.True(result.Retryable); // Null response is classified as TransientError (retryable)\n        Assert.Equal(\"Slack response was null\", result.FailureReason);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_token)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))\n        );\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/SlackServiceTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\nusing System.Text.Json;\nusing System.Web;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.MockedHttpClient;\nusing NSubstitute;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class SlackServiceTests\n{\n    private readonly MockedHttpMessageHandler _handler;\n    private readonly HttpClient _httpClient;\n    private const string _token = \"xoxb-test-token\";\n\n    public SlackServiceTests()\n    {\n        _handler = new MockedHttpMessageHandler();\n        _httpClient = _handler.ToHttpClient();\n    }\n\n    private SutProvider<SlackService> GetSutProvider()\n    {\n        var clientFactory = Substitute.For<IHttpClientFactory>();\n        clientFactory.CreateClient(SlackService.HttpClientName).Returns(_httpClient);\n\n        var globalSettings = Substitute.For<GlobalSettings>();\n        globalSettings.Slack.ApiBaseUrl.Returns(\"https://slack.com/api\");\n\n        return new SutProvider<SlackService>()\n            .SetDependency(clientFactory)\n            .SetDependency(globalSettings)\n            .Create();\n    }\n\n    [Fact]\n    public async Task GetChannelIdsAsync_ReturnsCorrectChannelIds()\n    {\n        var response = JsonSerializer.Serialize(\n            new\n            {\n                ok = true,\n                channels =\n                    new[] {\n                        new { id = \"C12345\", name = \"general\" },\n                        new { id = \"C67890\", name = \"random\" }\n                    },\n                response_metadata = new { next_cursor = \"\" }\n            }\n        );\n        _handler.When(HttpMethod.Get)\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(response));\n\n        var sutProvider = GetSutProvider();\n        var channelNames = new List<string> { \"general\", \"random\" };\n        var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);\n\n        Assert.Equal(2, result.Count);\n        Assert.Contains(\"C12345\", result);\n        Assert.Contains(\"C67890\", result);\n    }\n\n    [Fact]\n    public async Task GetChannelIdsAsync_WithPagination_ReturnsCorrectChannelIds()\n    {\n        var firstPageResponse = JsonSerializer.Serialize(\n            new\n            {\n                ok = true,\n                channels = new[] { new { id = \"C12345\", name = \"general\" } },\n                response_metadata = new { next_cursor = \"next_cursor_value\" }\n            }\n        );\n        var secondPageResponse = JsonSerializer.Serialize(\n            new\n            {\n                ok = true,\n                channels = new[] { new { id = \"C67890\", name = \"random\" } },\n                response_metadata = new { next_cursor = \"\" }\n            }\n        );\n\n        _handler.When(\"https://slack.com/api/conversations.list?types=public_channel%2cprivate_channel&limit=1000\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(firstPageResponse));\n        _handler.When(\"https://slack.com/api/conversations.list?types=public_channel%2cprivate_channel&limit=1000&cursor=next_cursor_value\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(secondPageResponse));\n\n        var sutProvider = GetSutProvider();\n        var channelNames = new List<string> { \"general\", \"random\" };\n\n        var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);\n\n        Assert.Equal(2, result.Count);\n        Assert.Contains(\"C12345\", result);\n        Assert.Contains(\"C67890\", result);\n    }\n\n    [Fact]\n    public async Task GetChannelIdsAsync_ApiError_ReturnsEmptyResult()\n    {\n        var errorResponse = JsonSerializer.Serialize(\n            new { ok = false, error = \"rate_limited\" }\n        );\n\n        _handler.When(HttpMethod.Get)\n            .RespondWith(HttpStatusCode.TooManyRequests)\n            .WithContent(new StringContent(errorResponse));\n\n        var sutProvider = GetSutProvider();\n        var channelNames = new List<string> { \"general\", \"random\" };\n\n        var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task GetChannelIdsAsync_NoChannelsFound_ReturnsEmptyResult()\n    {\n        var emptyResponse = JsonSerializer.Serialize(\n            new\n            {\n                ok = true,\n                channels = Array.Empty<string>(),\n                response_metadata = new { next_cursor = \"\" }\n            });\n\n        _handler.When(HttpMethod.Get)\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(emptyResponse));\n\n        var sutProvider = GetSutProvider();\n        var channelNames = new List<string> { \"general\", \"random\" };\n        var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task GetChannelIdAsync_NoChannelFound_ReturnsEmptyResult()\n    {\n        var emptyResponse = JsonSerializer.Serialize(\n            new\n            {\n                ok = true,\n                channels = Array.Empty<string>(),\n                response_metadata = new { next_cursor = \"\" }\n            });\n\n        _handler.When(HttpMethod.Get)\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(emptyResponse));\n\n        var sutProvider = GetSutProvider();\n        var result = await sutProvider.Sut.GetChannelIdAsync(_token, \"general\");\n\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task GetChannelIdAsync_ReturnsCorrectChannelId()\n    {\n        var sutProvider = GetSutProvider();\n        var response = new\n        {\n            ok = true,\n            channels = new[]\n            {\n                new { id = \"C12345\", name = \"general\" },\n                new { id = \"C67890\", name = \"random\" }\n            },\n            response_metadata = new { next_cursor = \"\" }\n        };\n\n        _handler.When(HttpMethod.Get)\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(JsonSerializer.Serialize(response)));\n\n        var result = await sutProvider.Sut.GetChannelIdAsync(_token, \"general\");\n\n        Assert.Equal(\"C12345\", result);\n    }\n\n    [Fact]\n    public async Task GetDmChannelByEmailAsync_ReturnsCorrectDmChannelId()\n    {\n        var sutProvider = GetSutProvider();\n        var email = \"user@example.com\";\n        var userId = \"U12345\";\n        var dmChannelId = \"D67890\";\n\n        var userResponse = new\n        {\n            ok = true,\n            user = new { id = userId }\n        };\n\n        var dmResponse = new\n        {\n            ok = true,\n            channel = new { id = dmChannelId }\n        };\n\n        _handler.When($\"https://slack.com/api/users.lookupByEmail?email={email}\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));\n\n        _handler.When(\"https://slack.com/api/conversations.open\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(JsonSerializer.Serialize(dmResponse)));\n\n        var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);\n\n        Assert.Equal(dmChannelId, result);\n    }\n\n    [Fact]\n    public async Task GetDmChannelByEmailAsync_ApiErrorDmResponse_ReturnsEmptyString()\n    {\n        var sutProvider = GetSutProvider();\n        var email = \"user@example.com\";\n        var userId = \"U12345\";\n\n        var userResponse = new\n        {\n            ok = true,\n            user = new { id = userId }\n        };\n\n        var dmResponse = new\n        {\n            ok = false,\n            error = \"An error occured\"\n        };\n\n        _handler.When($\"https://slack.com/api/users.lookupByEmail?email={email}\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));\n\n        _handler.When(\"https://slack.com/api/conversations.open\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(JsonSerializer.Serialize(dmResponse)));\n\n        var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public async Task GetDmChannelByEmailAsync_ApiErrorUnparsableDmResponse_ReturnsEmptyString()\n    {\n        var sutProvider = GetSutProvider();\n        var email = \"user@example.com\";\n        var userId = \"U12345\";\n\n        var userResponse = new\n        {\n            ok = true,\n            user = new { id = userId }\n        };\n\n        _handler.When($\"https://slack.com/api/users.lookupByEmail?email={email}\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));\n\n        _handler.When(\"https://slack.com/api/conversations.open\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(\"NOT JSON\"));\n\n        var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public async Task GetDmChannelByEmailAsync_ApiErrorUserResponse_ReturnsEmptyString()\n    {\n        var sutProvider = GetSutProvider();\n        var email = \"user@example.com\";\n\n        var userResponse = new\n        {\n            ok = false,\n            error = \"An error occurred\"\n        };\n\n        _handler.When($\"https://slack.com/api/users.lookupByEmail?email={email}\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));\n\n        var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public async Task GetDmChannelByEmailAsync_ApiErrorUnparsableUserResponse_ReturnsEmptyString()\n    {\n        var sutProvider = GetSutProvider();\n        var email = \"user@example.com\";\n\n        _handler.When($\"https://slack.com/api/users.lookupByEmail?email={email}\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(\"Not JSON\"));\n\n        var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public void GetRedirectUrl_ReturnsCorrectUrl()\n    {\n        var sutProvider = GetSutProvider();\n        var clientId = sutProvider.GetDependency<GlobalSettings>().Slack.ClientId;\n        var scopes = sutProvider.GetDependency<GlobalSettings>().Slack.Scopes;\n        var callbackUrl = \"https://example.com/callback\";\n        var state = Guid.NewGuid().ToString();\n        var result = sutProvider.Sut.GetRedirectUrl(callbackUrl, state);\n\n        var uri = new Uri(result);\n        var query = HttpUtility.ParseQueryString(uri.Query);\n\n        Assert.Equal(clientId, query[\"client_id\"]);\n        Assert.Equal(scopes, query[\"scope\"]);\n        Assert.Equal(callbackUrl, query[\"redirect_uri\"]);\n        Assert.Equal(state, query[\"state\"]);\n        Assert.Equal(\"slack.com\", uri.Host);\n        Assert.Equal(\"/oauth/v2/authorize\", uri.AbsolutePath);\n    }\n\n    [Fact]\n    public async Task ObtainTokenViaOAuth_ReturnsAccessToken_WhenSuccessful()\n    {\n        var sutProvider = GetSutProvider();\n        var jsonResponse = JsonSerializer.Serialize(new\n        {\n            ok = true,\n            access_token = \"test-access-token\"\n        });\n\n        _handler.When(\"https://slack.com/api/oauth.v2.access\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(jsonResponse));\n\n        var result = await sutProvider.Sut.ObtainTokenViaOAuth(\"test-code\", \"https://example.com/callback\");\n\n        Assert.Equal(\"test-access-token\", result);\n    }\n\n    [Theory]\n    [InlineData(\"test-code\", \"\")]\n    [InlineData(\"\", \"https://example.com/callback\")]\n    [InlineData(\"\", \"\")]\n    public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenCodeOrRedirectUrlIsEmpty(string code, string redirectUrl)\n    {\n        var sutProvider = GetSutProvider();\n        var result = await sutProvider.Sut.ObtainTokenViaOAuth(code, redirectUrl);\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenErrorResponse()\n    {\n        var sutProvider = GetSutProvider();\n        var jsonResponse = JsonSerializer.Serialize(new\n        {\n            ok = false,\n            error = \"invalid_code\"\n        });\n\n        _handler.When(\"https://slack.com/api/oauth.v2.access\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(jsonResponse));\n\n        var result = await sutProvider.Sut.ObtainTokenViaOAuth(\"test-code\", \"https://example.com/callback\");\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenHttpCallFails()\n    {\n        var sutProvider = GetSutProvider();\n        _handler.When(\"https://slack.com/api/oauth.v2.access\")\n            .RespondWith(HttpStatusCode.InternalServerError)\n            .WithContent(new StringContent(string.Empty));\n\n        var result = await sutProvider.Sut.ObtainTokenViaOAuth(\"test-code\", \"https://example.com/callback\");\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public async Task SendSlackMessageByChannelId_Success_ReturnsSuccessfulResponse()\n    {\n        var sutProvider = GetSutProvider();\n        var channelId = \"C12345\";\n        var message = \"Hello, Slack!\";\n\n        var jsonResponse = JsonSerializer.Serialize(new\n        {\n            ok = true,\n            channel = channelId,\n        });\n\n        _handler.When(HttpMethod.Post)\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(jsonResponse));\n\n        var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);\n\n        // Response was parsed correctly\n        Assert.NotNull(result);\n        Assert.True(result.Ok);\n\n        // Request was sent correctly\n        Assert.Single(_handler.CapturedRequests);\n        var request = _handler.CapturedRequests[0];\n        Assert.NotNull(request);\n        Assert.Equal(HttpMethod.Post, request.Method);\n        Assert.NotNull(request.Headers.Authorization);\n        Assert.Equal($\"Bearer {_token}\", request.Headers.Authorization.ToString());\n        Assert.NotNull(request.Content);\n        var returned = (await request.Content.ReadAsStringAsync());\n        var json = JsonDocument.Parse(returned);\n        Assert.Equal(message, json.RootElement.GetProperty(\"text\").GetString() ?? string.Empty);\n        Assert.Equal(channelId, json.RootElement.GetProperty(\"channel\").GetString() ?? string.Empty);\n    }\n\n    [Fact]\n    public async Task SendSlackMessageByChannelId_Failure_ReturnsErrorResponse()\n    {\n        var sutProvider = GetSutProvider();\n        var channelId = \"C12345\";\n        var message = \"Hello, Slack!\";\n\n        var jsonResponse = JsonSerializer.Serialize(new\n        {\n            ok = false,\n            channel = channelId,\n            error = \"error\"\n        });\n\n        _handler.When(HttpMethod.Post)\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(jsonResponse));\n\n        var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);\n\n        // Response was parsed correctly\n        Assert.NotNull(result);\n        Assert.False(result.Ok);\n        Assert.NotNull(result.Error);\n    }\n\n    [Fact]\n    public async Task SendSlackMessageByChannelIdAsync_InvalidJson_ReturnsNull()\n    {\n        var sutProvider = GetSutProvider();\n        var channelId = \"C12345\";\n        var message = \"Hello world!\";\n\n        _handler.When(HttpMethod.Post)\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(\"Not JSON\"));\n\n        var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);\n\n        Assert.Null(result);\n    }\n\n    [Fact]\n    public async Task SendSlackMessageByChannelIdAsync_HttpServerError_ReturnsNull()\n    {\n        var sutProvider = GetSutProvider();\n        var channelId = \"C12345\";\n        var message = \"Hello world!\";\n\n        _handler.When(HttpMethod.Post)\n            .RespondWith(HttpStatusCode.InternalServerError)\n            .WithContent(new StringContent(string.Empty));\n\n        var result = await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);\n\n        Assert.Null(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/TeamsIntegrationHandlerTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Services;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.Rest;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class TeamsIntegrationHandlerTests\n{\n    private readonly ITeamsService _teamsService = Substitute.For<ITeamsService>();\n    private readonly string _channelId = \"C12345\";\n    private readonly Uri _serviceUrl = new Uri(\"http://localhost\");\n\n    private SutProvider<TeamsIntegrationHandler> GetSutProvider()\n    {\n        return new SutProvider<TeamsIntegrationHandler>()\n            .SetDependency(_teamsService)\n            .Create();\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.True(result.Success);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_ArgumentException_ReturnsConfigurationError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);\n\n        sutProvider.GetDependency<ITeamsService>()\n            .SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())\n            .ThrowsAsync(new ArgumentException(\"argument error\"));\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category);\n        Assert.False(result.Retryable);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_JsonException_ReturnsPermanentFailure(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);\n\n        sutProvider.GetDependency<ITeamsService>()\n            .SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())\n            .ThrowsAsync(new JsonException(\"JSON error\"));\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.Equal(IntegrationFailureCategory.PermanentFailure, result.Category);\n        Assert.False(result.Retryable);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_UriFormatException_ReturnsConfigurationError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);\n\n        sutProvider.GetDependency<ITeamsService>()\n            .SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())\n            .ThrowsAsync(new UriFormatException(\"Bad URI\"));\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.Equal(IntegrationFailureCategory.ConfigurationError, result.Category);\n        Assert.False(result.Retryable);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_HttpExceptionForbidden_ReturnsAuthenticationFailed(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);\n\n        sutProvider.GetDependency<ITeamsService>()\n            .SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())\n            .ThrowsAsync(new HttpOperationException(\"Server error\")\n            {\n                Response = new HttpResponseMessageWrapper(\n                        new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden),\n                        \"Forbidden\"\n                    )\n            }\n            );\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.Equal(IntegrationFailureCategory.AuthenticationFailed, result.Category);\n        Assert.False(result.Retryable);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_HttpExceptionTooManyRequests_ReturnsRateLimited(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);\n\n        sutProvider.GetDependency<ITeamsService>()\n            .SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())\n            .ThrowsAsync(new HttpOperationException(\"Server error\")\n            {\n                Response = new HttpResponseMessageWrapper(\n                        new HttpResponseMessage(System.Net.HttpStatusCode.TooManyRequests),\n                        \"Too Many Requests\"\n                    )\n            }\n            );\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.Equal(IntegrationFailureCategory.RateLimited, result.Category);\n        Assert.True(result.Retryable);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))\n        );\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_UnknownException_ReturnsTransientError(IntegrationMessage<TeamsIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl);\n\n        sutProvider.GetDependency<ITeamsService>()\n            .SendMessageToChannelAsync(Arg.Any<Uri>(), Arg.Any<string>(), Arg.Any<string>())\n            .ThrowsAsync(new Exception(\"Unknown error\"));\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.Equal(IntegrationFailureCategory.TransientError, result.Category);\n        Assert.True(result.Retryable);\n        Assert.Equal(result.Message, message);\n\n        await sutProvider.GetDependency<ITeamsService>().Received(1).SendMessageToChannelAsync(\n            Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)),\n            Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate))\n        );\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/TeamsServiceTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Net;\nusing System.Text.Json;\nusing System.Web;\nusing Bit.Core.Dirt.Entities;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Models.Data.Teams;\nusing Bit.Core.Dirt.Repositories;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.MockedHttpClient;\nusing NSubstitute;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class TeamsServiceTests\n{\n    private readonly MockedHttpMessageHandler _handler;\n    private readonly HttpClient _httpClient;\n\n    public TeamsServiceTests()\n    {\n        _handler = new MockedHttpMessageHandler();\n        _httpClient = _handler.ToHttpClient();\n    }\n\n    private SutProvider<TeamsService> GetSutProvider()\n    {\n        var clientFactory = Substitute.For<IHttpClientFactory>();\n        clientFactory.CreateClient(TeamsService.HttpClientName).Returns(_httpClient);\n\n        var globalSettings = Substitute.For<GlobalSettings>();\n        globalSettings.Teams.LoginBaseUrl.Returns(\"https://login.example.com\");\n        globalSettings.Teams.GraphBaseUrl.Returns(\"https://graph.example.com\");\n\n        return new SutProvider<TeamsService>()\n            .SetDependency(clientFactory)\n            .SetDependency(globalSettings)\n            .Create();\n    }\n\n    [Fact]\n    public void GetRedirectUrl_ReturnsCorrectUrl()\n    {\n        var sutProvider = GetSutProvider();\n        var clientId = sutProvider.GetDependency<GlobalSettings>().Teams.ClientId;\n        var scopes = sutProvider.GetDependency<GlobalSettings>().Teams.Scopes;\n        var callbackUrl = \"https://example.com/callback\";\n        var state = Guid.NewGuid().ToString();\n        var result = sutProvider.Sut.GetRedirectUrl(callbackUrl, state);\n\n        var uri = new Uri(result);\n        var query = HttpUtility.ParseQueryString(uri.Query);\n\n        Assert.Equal(clientId, query[\"client_id\"]);\n        Assert.Equal(scopes, query[\"scope\"]);\n        Assert.Equal(callbackUrl, query[\"redirect_uri\"]);\n        Assert.Equal(state, query[\"state\"]);\n        Assert.Equal(\"login.example.com\", uri.Host);\n        Assert.Equal(\"/common/oauth2/v2.0/authorize\", uri.AbsolutePath);\n    }\n\n    [Fact]\n    public async Task ObtainTokenViaOAuth_Success_ReturnsAccessToken()\n    {\n        var sutProvider = GetSutProvider();\n        var jsonResponse = JsonSerializer.Serialize(new\n        {\n            access_token = \"test-access-token\"\n        });\n\n        _handler.When(\"https://login.example.com/common/oauth2/v2.0/token\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(jsonResponse));\n\n        var result = await sutProvider.Sut.ObtainTokenViaOAuth(\"test-code\", \"https://example.com/callback\");\n\n        Assert.Equal(\"test-access-token\", result);\n    }\n\n    [Theory]\n    [InlineData(\"test-code\", \"\")]\n    [InlineData(\"\", \"https://example.com/callback\")]\n    [InlineData(\"\", \"\")]\n    public async Task ObtainTokenViaOAuth_CodeOrRedirectUrlIsEmpty_ReturnsEmptyString(string code, string redirectUrl)\n    {\n        var sutProvider = GetSutProvider();\n        var result = await sutProvider.Sut.ObtainTokenViaOAuth(code, redirectUrl);\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public async Task ObtainTokenViaOAuth_HttpFailure_ReturnsEmptyString()\n    {\n        var sutProvider = GetSutProvider();\n        _handler.When(\"https://login.example.com/common/oauth2/v2.0/token\")\n            .RespondWith(HttpStatusCode.InternalServerError)\n            .WithContent(new StringContent(string.Empty));\n\n        var result = await sutProvider.Sut.ObtainTokenViaOAuth(\"test-code\", \"https://example.com/callback\");\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public async Task ObtainTokenViaOAuth_UnknownResponse_ReturnsEmptyString()\n    {\n        var sutProvider = GetSutProvider();\n\n        _handler.When(\"https://login.example.com/common/oauth2/v2.0/token\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(\"Not an expected response\"));\n\n        var result = await sutProvider.Sut.ObtainTokenViaOAuth(\"test-code\", \"https://example.com/callback\");\n\n        Assert.Equal(string.Empty, result);\n    }\n\n    [Fact]\n    public async Task GetJoinedTeamsAsync_Success_ReturnsTeams()\n    {\n        var sutProvider = GetSutProvider();\n\n        var jsonResponse = JsonSerializer.Serialize(new\n        {\n            value = new[]\n            {\n                new { id = \"team1\", displayName = \"Team One\" },\n                new { id = \"team2\", displayName = \"Team Two\" }\n            }\n        });\n\n        _handler.When(\"https://graph.example.com/me/joinedTeams\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(jsonResponse));\n\n        var result = await sutProvider.Sut.GetJoinedTeamsAsync(\"fake-access-token\");\n\n        Assert.Equal(2, result.Count);\n        Assert.Contains(result, t => t is { Id: \"team1\", DisplayName: \"Team One\" });\n        Assert.Contains(result, t => t is { Id: \"team2\", DisplayName: \"Team Two\" });\n    }\n\n    [Fact]\n    public async Task GetJoinedTeamsAsync_ServerReturnsEmpty_ReturnsEmptyList()\n    {\n        var sutProvider = GetSutProvider();\n\n        var jsonResponse = JsonSerializer.Serialize(new { value = (object?)null });\n\n        _handler.When(\"https://graph.example.com/me/joinedTeams\")\n            .RespondWith(HttpStatusCode.OK)\n            .WithContent(new StringContent(jsonResponse));\n\n        var result = await sutProvider.Sut.GetJoinedTeamsAsync(\"fake-access-token\");\n\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Fact]\n    public async Task GetJoinedTeamsAsync_ServerErrorCode_ReturnsEmptyList()\n    {\n        var sutProvider = GetSutProvider();\n\n        _handler.When(\"https://graph.example.com/me/joinedTeams\")\n            .RespondWith(HttpStatusCode.Unauthorized)\n            .WithContent(new StringContent(\"Unauthorized\"));\n\n        var result = await sutProvider.Sut.GetJoinedTeamsAsync(\"fake-access-token\");\n\n        Assert.NotNull(result);\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleIncomingAppInstall_Success_UpdatesTeamsIntegration(\n        OrganizationIntegration integration)\n    {\n        var sutProvider = GetSutProvider();\n        var tenantId = Guid.NewGuid().ToString();\n        var teamId = Guid.NewGuid().ToString();\n        var conversationId = Guid.NewGuid().ToString();\n        var serviceUrl = new Uri(\"https://localhost\");\n        var initiatedConfiguration = new TeamsIntegration(TenantId: tenantId, Teams:\n        [\n            new TeamInfo() { Id = teamId, DisplayName = \"test team\", TenantId = tenantId },\n            new TeamInfo() { Id = Guid.NewGuid().ToString(), DisplayName = \"other team\", TenantId = tenantId },\n            new TeamInfo() { Id = Guid.NewGuid().ToString(), DisplayName = \"third team\", TenantId = tenantId }\n        ]);\n        integration.Configuration = JsonSerializer.Serialize(initiatedConfiguration);\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId)\n            .Returns(integration);\n\n        OrganizationIntegration? capturedIntegration = null;\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .UpsertAsync(Arg.Do<OrganizationIntegration>(x => capturedIntegration = x));\n\n        await sutProvider.Sut.HandleIncomingAppInstallAsync(\n            conversationId: conversationId,\n            serviceUrl: serviceUrl,\n            teamId: teamId,\n            tenantId: tenantId\n        );\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId);\n        Assert.NotNull(capturedIntegration);\n        var configuration = JsonSerializer.Deserialize<TeamsIntegration>(capturedIntegration.Configuration ?? string.Empty);\n        Assert.NotNull(configuration);\n        Assert.NotNull(configuration.ServiceUrl);\n        Assert.Equal(serviceUrl, configuration.ServiceUrl);\n        Assert.Equal(conversationId, configuration.ChannelId);\n    }\n\n    [Fact]\n    public async Task HandleIncomingAppInstall_NoIntegrationMatched_DoesNothing()\n    {\n        var sutProvider = GetSutProvider();\n        await sutProvider.Sut.HandleIncomingAppInstallAsync(\n            conversationId: \"conversationId\",\n            serviceUrl: new Uri(\"https://localhost\"),\n            teamId: \"teamId\",\n            tenantId: \"tenantId\"\n        );\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId(\"tenantId\", \"teamId\");\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive().UpsertAsync(Arg.Any<OrganizationIntegration>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleIncomingAppInstall_MatchedIntegrationAlreadySetup_DoesNothing(\n        OrganizationIntegration integration)\n    {\n        var sutProvider = GetSutProvider();\n        var tenantId = Guid.NewGuid().ToString();\n        var teamId = Guid.NewGuid().ToString();\n        var initiatedConfiguration = new TeamsIntegration(\n            TenantId: tenantId,\n            Teams: [new TeamInfo() { Id = teamId, DisplayName = \"test team\", TenantId = tenantId }],\n            ChannelId: \"ChannelId\",\n            ServiceUrl: new Uri(\"https://localhost\")\n        );\n        integration.Configuration = JsonSerializer.Serialize(initiatedConfiguration);\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId)\n            .Returns(integration);\n\n        await sutProvider.Sut.HandleIncomingAppInstallAsync(\n            conversationId: \"conversationId\",\n            serviceUrl: new Uri(\"https://localhost\"),\n            teamId: teamId,\n            tenantId: tenantId\n        );\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId);\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive().UpsertAsync(Arg.Any<OrganizationIntegration>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleIncomingAppInstall_MatchedIntegrationWithMissingConfiguration_DoesNothing(\n        OrganizationIntegration integration)\n    {\n        var sutProvider = GetSutProvider();\n        integration.Configuration = null;\n\n        sutProvider.GetDependency<IOrganizationIntegrationRepository>()\n            .GetByTeamsConfigurationTenantIdTeamId(\"tenantId\", \"teamId\")\n            .Returns(integration);\n\n        await sutProvider.Sut.HandleIncomingAppInstallAsync(\n            conversationId: \"conversationId\",\n            serviceUrl: new Uri(\"https://localhost\"),\n            teamId: \"teamId\",\n            tenantId: \"tenantId\"\n        );\n\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1).GetByTeamsConfigurationTenantIdTeamId(\"tenantId\", \"teamId\");\n        await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive().UpsertAsync(Arg.Any<OrganizationIntegration>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Dirt/Services/WebhookIntegrationHandlerTests.cs",
    "content": "﻿using System.Net;\nusing System.Net.Http.Headers;\nusing Bit.Core.Dirt.Models.Data.EventIntegrations;\nusing Bit.Core.Dirt.Services.Implementations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Bit.Test.Common.MockedHttpClient;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Dirt.Services;\n\n[SutProviderCustomize]\npublic class WebhookIntegrationHandlerTests\n{\n    private readonly MockedHttpMessageHandler _handler;\n    private readonly HttpClient _httpClient;\n    private const string _scheme = \"Bearer\";\n    private const string _token = \"AUTH_TOKEN\";\n    private static readonly Uri _webhookUri = new Uri(\"https://localhost\");\n\n    public WebhookIntegrationHandlerTests()\n    {\n        _handler = new MockedHttpMessageHandler();\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.OK)\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n        _httpClient = _handler.ToHttpClient();\n    }\n\n    private SutProvider<WebhookIntegrationHandler> GetSutProvider()\n    {\n        var clientFactory = Substitute.For<IHttpClientFactory>();\n        clientFactory.CreateClient(WebhookIntegrationHandler.HttpClientName).Returns(_httpClient);\n\n        return new SutProvider<WebhookIntegrationHandler>()\n            .SetDependency(clientFactory)\n            .WithFakeTimeProvider()\n            .Create();\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_SuccessfulRequestWithoutAuth_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri);\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.True(result.Success);\n        Assert.Equal(result.Message, message);\n        Assert.Null(result.FailureReason);\n\n        sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(\n            Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))\n        );\n\n        Assert.Single(_handler.CapturedRequests);\n        var request = _handler.CapturedRequests[0];\n        Assert.NotNull(request);\n        Assert.NotNull(request.Content);\n        var returned = await request.Content.ReadAsStringAsync();\n\n        Assert.Equal(HttpMethod.Post, request.Method);\n        Assert.Null(request.Headers.Authorization);\n        Assert.Equal(_webhookUri, request.RequestUri);\n        AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_SuccessfulRequestWithAuthorizationHeader_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.True(result.Success);\n        Assert.Equal(result.Message, message);\n        Assert.Null(result.FailureReason);\n\n        sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(\n            Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))\n        );\n\n        Assert.Single(_handler.CapturedRequests);\n        var request = _handler.CapturedRequests[0];\n        Assert.NotNull(request);\n        Assert.NotNull(request.Content);\n        var returned = await request.Content.ReadAsStringAsync();\n\n        Assert.Equal(HttpMethod.Post, request.Method);\n        Assert.Equal(new AuthenticationHeaderValue(_scheme, _token), request.Headers.Authorization);\n        Assert.Equal(_webhookUri, request.RequestUri);\n        AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);\n        var retryAfter = now.AddSeconds(60);\n\n        sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);\n        message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);\n\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.TooManyRequests)\n            .WithHeader(\"Retry-After\", \"60\")\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.True(result.Retryable);\n        Assert.Equal(result.Message, message);\n        Assert.True(result.DelayUntilDate.HasValue);\n        Assert.Equal(retryAfter, result.DelayUntilDate.Value);\n        Assert.Equal(\"Too Many Requests\", result.FailureReason);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsDelayUntilDate(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);\n        var retryAfter = now.AddSeconds(60);\n        message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);\n\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.TooManyRequests)\n            .WithHeader(\"Retry-After\", retryAfter.ToString(\"r\"))\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.True(result.Retryable);\n        Assert.Equal(result.Message, message);\n        Assert.True(result.DelayUntilDate.HasValue);\n        Assert.Equal(retryAfter, result.DelayUntilDate.Value);\n        Assert.Equal(\"Too Many Requests\", result.FailureReason);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);\n\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.InternalServerError)\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.True(result.Retryable);\n        Assert.Equal(result.Message, message);\n        Assert.False(result.DelayUntilDate.HasValue);\n        Assert.Equal(\"Internal Server Error\", result.FailureReason);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)\n    {\n        var sutProvider = GetSutProvider();\n        message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);\n\n        _handler.Fallback\n            .WithStatusCode(HttpStatusCode.TemporaryRedirect)\n            .WithContent(new StringContent(\"<html><head><title>test</title></head><body>test</body></html>\"));\n\n        var result = await sutProvider.Sut.HandleAsync(message);\n\n        Assert.False(result.Success);\n        Assert.False(result.Retryable);\n        Assert.Equal(result.Message, message);\n        Assert.Null(result.DelayUntilDate);\n        Assert.Equal(\"Temporary Redirect\", result.FailureReason);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Entities/OrganizationConnectionTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Entities;\n\npublic class OrganizationConnectionTests\n{\n    [Theory]\n    [BitAutoData]\n    public void OrganizationConnection_CanUse_Success(Guid connectionId, Guid organizationId)\n    {\n        var connection = new OrganizationConnection<ScimConfig>()\n        {\n            Id = connectionId,\n            OrganizationId = organizationId,\n            Enabled = true,\n            Type = OrganizationConnectionType.Scim,\n            Config = new ScimConfig() { Enabled = true }\n        };\n\n        Assert.True(connection.Validate<ScimConfig>(out var exception));\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void OrganizationConnection_CanUse_WhenDisabled_ReturnsFalse(Guid connectionId, Guid organizationId)\n    {\n\n        var connection = new OrganizationConnection<ScimConfig>()\n        {\n            Id = connectionId,\n            OrganizationId = organizationId,\n            Enabled = false,\n            Type = OrganizationConnectionType.Scim,\n            Config = new ScimConfig() { Enabled = true }\n        };\n\n        Assert.False(connection.Validate<ScimConfig>(out var exception));\n        Assert.Contains(\"Connection disabled\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void OrganizationConnection_CanUse_WhenNoConfig_ReturnsFalse(Guid connectionId, Guid organizationId)\n    {\n        var connection = new OrganizationConnection<ScimConfig>()\n        {\n            Id = connectionId,\n            OrganizationId = organizationId,\n            Enabled = true,\n            Type = OrganizationConnectionType.Scim,\n        };\n\n        Assert.False(connection.Validate<ScimConfig>(out var exception));\n        Assert.Contains(\"No saved Connection config\", exception);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void OrganizationConnection_CanUse_WhenConfigInvalid_ReturnsFalse(Guid connectionId, Guid organizationId)\n    {\n        var connection = new OrganizationConnection<ScimConfig>()\n        {\n            Id = connectionId,\n            OrganizationId = organizationId,\n            Enabled = true,\n            Type = OrganizationConnectionType.Scim,\n            Config = new ScimConfig() { Enabled = false }\n        };\n\n        Assert.False(connection.Validate<ScimConfig>(out var exception));\n        Assert.Contains(\"Scim Config is disabled\", exception);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Entities/UserTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Entities;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Core.Test.Entities;\n\npublic class UserTests\n{\n    //                              KB     MB     GB\n    public const long Multiplier = 1024 * 1024 * 1024;\n\n    [Fact]\n    public void StorageBytesRemaining_HasMax_DoesNotHaveStorage_ReturnsMaxAsBytes()\n    {\n        short maxStorageGb = 1;\n\n        var user = new User\n        {\n            MaxStorageGb = maxStorageGb,\n            Storage = null,\n        };\n\n        var bytesRemaining = user.StorageBytesRemaining();\n\n        Assert.Equal(bytesRemaining, maxStorageGb * Multiplier);\n    }\n\n    [Theory]\n    [InlineData(2, 1 * Multiplier, 1 * Multiplier)]\n\n    public void StorageBytesRemaining_HasMax_HasStorage_ReturnRemainingStorage(short maxStorageGb, long storageBytes, long expectedRemainingBytes)\n    {\n        var user = new User\n        {\n            MaxStorageGb = maxStorageGb,\n            Storage = storageBytes,\n        };\n\n        var bytesRemaining = user.StorageBytesRemaining();\n\n        Assert.Equal(expectedRemainingBytes, bytesRemaining);\n    }\n\n    private static readonly Dictionary<TwoFactorProviderType, TwoFactorProvider> _testTwoFactorConfig = new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n    {\n        [TwoFactorProviderType.WebAuthn] = new TwoFactorProvider\n        {\n            Enabled = true,\n            MetaData = new Dictionary<string, object>\n            {\n                [\"Item\"] = \"thing\",\n            },\n        },\n        [TwoFactorProviderType.Email] = new TwoFactorProvider\n        {\n            Enabled = false,\n            MetaData = new Dictionary<string, object>\n            {\n                [\"Email\"] = \"test@email.com\",\n            },\n        },\n    };\n\n    [Fact]\n    public void SetTwoFactorProviders_Success()\n    {\n        var user = new User();\n        user.SetTwoFactorProviders(_testTwoFactorConfig);\n\n        using var jsonDocument = JsonDocument.Parse(user.TwoFactorProviders);\n        var root = jsonDocument.RootElement;\n\n        var webAuthn = AssertHelper.AssertJsonProperty(root, \"7\", JsonValueKind.Object);\n        AssertHelper.AssertJsonProperty(webAuthn, \"Enabled\", JsonValueKind.True);\n        var webMetaData = AssertHelper.AssertJsonProperty(webAuthn, \"MetaData\", JsonValueKind.Object);\n        AssertHelper.AssertJsonProperty(webMetaData, \"Item\", JsonValueKind.String);\n\n        var email = AssertHelper.AssertJsonProperty(root, \"1\", JsonValueKind.Object);\n        AssertHelper.AssertJsonProperty(email, \"Enabled\", JsonValueKind.False);\n        var emailMetaData = AssertHelper.AssertJsonProperty(email, \"MetaData\", JsonValueKind.Object);\n        AssertHelper.AssertJsonProperty(emailMetaData, \"Email\", JsonValueKind.String);\n    }\n\n    [Fact]\n    public void GetTwoFactorProviders_Success()\n    {\n        // This is to get rid of the cached dictionary the SetTwoFactorProviders keeps so we can fully test the JSON reading\n        // It intent is to mimic a storing of the entity in the database and it being read later\n        var tempUser = new User();\n        tempUser.SetTwoFactorProviders(_testTwoFactorConfig);\n        var user = new User\n        {\n            TwoFactorProviders = tempUser.TwoFactorProviders,\n        };\n\n        var twoFactorProviders = user.GetTwoFactorProviders();\n\n        var webAuthn = Assert.Contains(TwoFactorProviderType.WebAuthn, (IDictionary<TwoFactorProviderType, TwoFactorProvider>)twoFactorProviders);\n        Assert.True(webAuthn.Enabled);\n        Assert.NotNull(webAuthn.MetaData);\n        var webAuthnMetaDataItem = Assert.Contains(\"Item\", (IDictionary<string, object>)webAuthn.MetaData);\n        Assert.Equal(\"thing\", webAuthnMetaDataItem);\n\n        var email = Assert.Contains(TwoFactorProviderType.Email, (IDictionary<TwoFactorProviderType, TwoFactorProvider>)twoFactorProviders);\n        Assert.False(email.Enabled);\n        Assert.NotNull(email.MetaData);\n        var emailMetaDataEmail = Assert.Contains(\"Email\", (IDictionary<string, object>)email.MetaData);\n        Assert.Equal(\"test@email.com\", emailMetaDataEmail);\n    }\n\n    [Fact]\n    public void GetTwoFactorProviders_SavedWithName_Success()\n    {\n        var user = new User();\n        // This should save items with the string name of the enum and we will validate that we can read\n        // from that just incase some users have it saved that way.\n        user.TwoFactorProviders = JsonSerializer.Serialize(_testTwoFactorConfig);\n\n        // Preliminary Asserts to make sure we are testing what we want to be testing\n        using var jsonDocument = JsonDocument.Parse(user.TwoFactorProviders);\n        var root = jsonDocument.RootElement;\n        // This means it saved the enum as its string name\n        AssertHelper.AssertJsonProperty(root, \"WebAuthn\", JsonValueKind.Object);\n        AssertHelper.AssertJsonProperty(root, \"Email\", JsonValueKind.Object);\n\n        // Actual checks\n        var twoFactorProviders = user.GetTwoFactorProviders();\n\n        var webAuthn = Assert.Contains(TwoFactorProviderType.WebAuthn, (IDictionary<TwoFactorProviderType, TwoFactorProvider>)twoFactorProviders);\n        Assert.True(webAuthn.Enabled);\n        Assert.NotNull(webAuthn.MetaData);\n        var webAuthnMetaDataItem = Assert.Contains(\"Item\", (IDictionary<string, object>)webAuthn.MetaData);\n        Assert.Equal(\"thing\", webAuthnMetaDataItem);\n\n        var email = Assert.Contains(TwoFactorProviderType.Email, (IDictionary<TwoFactorProviderType, TwoFactorProvider>)twoFactorProviders);\n        Assert.False(email.Enabled);\n        Assert.NotNull(email.MetaData);\n        var emailMetaDataEmail = Assert.Contains(\"Email\", (IDictionary<string, object>)email.MetaData);\n        Assert.Equal(\"test@email.com\", emailMetaDataEmail);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Extensions/SubscriberExtensionsTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Billing.Extensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Extensions;\n\npublic class SubscriberExtensionsTests\n{\n    [Theory]\n    [InlineData(\"Alexandria Villanueva Gonzalez Pablo\", \"Alexandria Villanueva Gonzalez\")]\n    [InlineData(\"John Snow\", \"John Snow\")]\n    public void GetFormattedInvoiceName_Returns_FirstThirtyCaractersOfName(string name, string expected)\n    {\n        // arrange\n        var provider = new Provider { Name = name };\n\n        // act\n        var actual = provider.GetFormattedInvoiceName();\n\n        // assert\n        Assert.Equal(expected, actual);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Helpers/Factories.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Microsoft.Extensions.Configuration;\n\nnamespace Bit.Core.Test.Helpers.Factories;\n\npublic static class GlobalSettingsFactory\n{\n    public static GlobalSettings GlobalSettings { get; } = new();\n    static GlobalSettingsFactory()\n    {\n        var configBuilder = new ConfigurationBuilder().AddUserSecrets(\"bitwarden-Api\");\n        var Configuration = configBuilder.Build();\n        ConfigurationBinder.Bind(Configuration.GetSection(\"GlobalSettings\"), GlobalSettings);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/Authorization/KeyConnectorAuthorizationHandlerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Authorization;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.Authorization;\n\n[SutProviderCustomize]\npublic class KeyConnectorAuthorizationHandlerTests\n{\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_UserCanUseKeyConnector_Success(\n        User user,\n        ClaimsPrincipal claimsPrincipal,\n        SutProvider<KeyConnectorAuthorizationHandler> sutProvider)\n    {\n        // Arrange\n        user.UsesKeyConnector = false;\n        sutProvider.GetDependency<ICurrentContext>().Organizations\n            .Returns(new List<CurrentContextOrganization>());\n\n        var requirement = KeyConnectorOperations.Use;\n        var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_UserAlreadyUsesKeyConnector_Fails(\n        User user,\n        ClaimsPrincipal claimsPrincipal,\n        SutProvider<KeyConnectorAuthorizationHandler> sutProvider)\n    {\n        // Arrange\n        user.UsesKeyConnector = true;\n        sutProvider.GetDependency<ICurrentContext>().Organizations\n            .Returns(new List<CurrentContextOrganization>());\n\n        var requirement = KeyConnectorOperations.Use;\n        var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_UserIsOwner_Fails(\n        User user,\n        Guid organizationId,\n        ClaimsPrincipal claimsPrincipal,\n        SutProvider<KeyConnectorAuthorizationHandler> sutProvider)\n    {\n        // Arrange\n        user.UsesKeyConnector = false;\n        var organizations = new List<CurrentContextOrganization>\n        {\n            new() { Id = organizationId, Type = OrganizationUserType.Owner }\n        };\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);\n\n        var requirement = KeyConnectorOperations.Use;\n        var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_UserIsAdmin_Fails(\n        User user,\n        Guid organizationId,\n        ClaimsPrincipal claimsPrincipal,\n        SutProvider<KeyConnectorAuthorizationHandler> sutProvider)\n    {\n        // Arrange\n        user.UsesKeyConnector = false;\n        var organizations = new List<CurrentContextOrganization>\n        {\n            new() { Id = organizationId, Type = OrganizationUserType.Admin }\n        };\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);\n\n        var requirement = KeyConnectorOperations.Use;\n        var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_UserIsRegularMember_Success(\n        User user,\n        Guid organizationId,\n        ClaimsPrincipal claimsPrincipal,\n        SutProvider<KeyConnectorAuthorizationHandler> sutProvider)\n    {\n        // Arrange\n        user.UsesKeyConnector = false;\n        var organizations = new List<CurrentContextOrganization>\n        {\n            new() { Id = organizationId, Type = OrganizationUserType.User }\n        };\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);\n\n        var requirement = KeyConnectorOperations.Use;\n        var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);\n\n        // Act\n        await sutProvider.Sut.HandleAsync(context);\n\n        // Assert\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory, BitAutoData]\n    public async Task HandleRequirementAsync_UnsupportedRequirement_ThrowsArgumentException(\n        User user,\n        ClaimsPrincipal claimsPrincipal,\n        SutProvider<KeyConnectorAuthorizationHandler> sutProvider)\n    {\n        // Arrange\n        user.UsesKeyConnector = false;\n        sutProvider.GetDependency<ICurrentContext>().Organizations\n            .Returns(new List<CurrentContextOrganization>());\n\n        var unsupportedRequirement = new KeyConnectorOperationsRequirement(\"UnsupportedOperation\");\n        var context = new AuthorizationHandlerContext([unsupportedRequirement], claimsPrincipal, user);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(context));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/Commands/RegenerateUserAsymmetricKeysCommandTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Data;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Commands;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.Commands;\n\n[SutProviderCustomize]\npublic class RegenerateUserAsymmetricKeysCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task RegenerateKeysAsync_NoCurrentContext_NotFoundException(\n        SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,\n        UserAsymmetricKeys userAsymmetricKeys)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsNullForAnyArgs();\n        var usersOrganizationAccounts = new List<OrganizationUser>();\n        var designatedEmergencyAccess = new List<EmergencyAccessDetails>();\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,\n            usersOrganizationAccounts, designatedEmergencyAccess));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RegenerateKeysAsync_UserHasNoSharedAccess_Success(\n        SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,\n        UserAsymmetricKeys userAsymmetricKeys)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId);\n        var usersOrganizationAccounts = new List<OrganizationUser>();\n        var designatedEmergencyAccess = new List<EmergencyAccessDetails>();\n\n        await sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,\n            usersOrganizationAccounts, designatedEmergencyAccess);\n\n        await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()\n            .Received(1)\n            .RegenerateUserAsymmetricKeysAsync(Arg.Is(userAsymmetricKeys));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushSyncSettingsAsync(Arg.Is(userAsymmetricKeys.UserId));\n    }\n\n    [Theory]\n    [BitAutoData(false, false, true)]\n    [BitAutoData(false, true, false)]\n    [BitAutoData(false, true, true)]\n    [BitAutoData(true, false, false)]\n    [BitAutoData(true, false, true)]\n    [BitAutoData(true, true, false)]\n    [BitAutoData(true, true, true)]\n    public async Task RegenerateKeysAsync_UserIdMisMatch_NotFoundException(\n        bool userAsymmetricKeysMismatch,\n        bool orgMismatch,\n        bool emergencyAccessMismatch,\n        SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,\n        UserAsymmetricKeys userAsymmetricKeys,\n        ICollection<OrganizationUser> usersOrganizationAccounts,\n        ICollection<EmergencyAccessDetails> designatedEmergencyAccess)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId\n            .ReturnsForAnyArgs(userAsymmetricKeysMismatch ? new Guid() : userAsymmetricKeys.UserId);\n\n        if (!orgMismatch)\n        {\n            usersOrganizationAccounts =\n                SetupOrganizationUserAccounts(userAsymmetricKeys.UserId, usersOrganizationAccounts);\n        }\n\n        if (!emergencyAccessMismatch)\n        {\n            designatedEmergencyAccess = SetupEmergencyAccess(userAsymmetricKeys.UserId, designatedEmergencyAccess);\n        }\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,\n            usersOrganizationAccounts, designatedEmergencyAccess));\n\n        await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()\n            .ReceivedWithAnyArgs(0)\n            .RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .ReceivedWithAnyArgs(0)\n            .PushSyncSettingsAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserStatusType.Confirmed)]\n    [BitAutoData(OrganizationUserStatusType.Revoked)]\n    public async Task RegenerateKeysAsync_UserInOrganizations_BadRequestException(\n        OrganizationUserStatusType organizationUserStatus,\n        SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,\n        UserAsymmetricKeys userAsymmetricKeys,\n        ICollection<OrganizationUser> usersOrganizationAccounts)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId);\n        usersOrganizationAccounts = CreateInOrganizationAccounts(userAsymmetricKeys.UserId, organizationUserStatus,\n            usersOrganizationAccounts);\n        var designatedEmergencyAccess = new List<EmergencyAccessDetails>();\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,\n            usersOrganizationAccounts, designatedEmergencyAccess));\n\n        await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()\n            .ReceivedWithAnyArgs(0)\n            .RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .ReceivedWithAnyArgs(0)\n            .PushSyncSettingsAsync(Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData(EmergencyAccessStatusType.Confirmed)]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)]\n    [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)]\n    public async Task RegenerateKeysAsync_UserHasDesignatedEmergencyAccess_BadRequestException(\n        EmergencyAccessStatusType statusType,\n        SutProvider<RegenerateUserAsymmetricKeysCommand> sutProvider,\n        UserAsymmetricKeys userAsymmetricKeys,\n        ICollection<EmergencyAccessDetails> designatedEmergencyAccess)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.ReturnsForAnyArgs(userAsymmetricKeys.UserId);\n        designatedEmergencyAccess =\n            CreateDesignatedEmergencyAccess(userAsymmetricKeys.UserId, statusType, designatedEmergencyAccess);\n        var usersOrganizationAccounts = new List<OrganizationUser>();\n\n\n        await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegenerateKeysAsync(userAsymmetricKeys,\n            usersOrganizationAccounts, designatedEmergencyAccess));\n\n        await sutProvider.GetDependency<IUserAsymmetricKeysRepository>()\n            .ReceivedWithAnyArgs(0)\n            .RegenerateUserAsymmetricKeysAsync(Arg.Any<UserAsymmetricKeys>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .ReceivedWithAnyArgs(0)\n            .PushSyncSettingsAsync(Arg.Any<Guid>());\n    }\n\n    private static ICollection<OrganizationUser> CreateInOrganizationAccounts(Guid userId,\n        OrganizationUserStatusType organizationUserStatus, ICollection<OrganizationUser> organizationUserAccounts)\n    {\n        foreach (var organizationUserAccount in organizationUserAccounts)\n        {\n            organizationUserAccount.UserId = userId;\n            organizationUserAccount.Status = organizationUserStatus;\n        }\n\n        return organizationUserAccounts;\n    }\n\n    private static ICollection<EmergencyAccessDetails> CreateDesignatedEmergencyAccess(Guid userId,\n        EmergencyAccessStatusType status, ICollection<EmergencyAccessDetails> designatedEmergencyAccess)\n    {\n        foreach (var designated in designatedEmergencyAccess)\n        {\n            designated.GranteeId = userId;\n            designated.Status = status;\n        }\n\n        return designatedEmergencyAccess;\n    }\n\n    private static ICollection<OrganizationUser> SetupOrganizationUserAccounts(Guid userId,\n        ICollection<OrganizationUser> organizationUserAccounts)\n    {\n        foreach (var organizationUserAccount in organizationUserAccounts)\n        {\n            organizationUserAccount.UserId = userId;\n        }\n\n        return organizationUserAccounts;\n    }\n\n    private static ICollection<EmergencyAccessDetails> SetupEmergencyAccess(Guid userId,\n        ICollection<EmergencyAccessDetails> emergencyAccessDetails)\n    {\n        foreach (var emergencyAccessDetail in emergencyAccessDetails)\n        {\n            emergencyAccessDetail.GranteeId = userId;\n        }\n\n        return emergencyAccessDetails;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/Commands/SetKeyConnectorKeyCommandTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Commands;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Http;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.Commands;\n\n[SutProviderCustomize]\npublic class SetKeyConnectorKeyCommandTests\n{\n\n    [Theory, BitAutoData]\n    public async Task SetKeyConnectorKeyForUserAsync_Success_SetsAccountKeys(\n        User user,\n        KeyConnectorKeysData data,\n        SutProvider<SetKeyConnectorKeyCommand> sutProvider)\n    {\n        // Set up valid V2 encryption data\n        if (data.AccountKeys!.SignatureKeyPair != null)\n        {\n            data.AccountKeys.SignatureKeyPair.SignatureAlgorithm = \"ed25519\";\n        }\n\n        var expectedAccountKeysData = data.AccountKeys.ToAccountKeysData();\n\n        // Arrange\n        user.UsesKeyConnector = false;\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n        var httpContext = Substitute.For<HttpContext>();\n        httpContext.User.Returns(new ClaimsPrincipal());\n        currentContext.HttpContext.Returns(httpContext);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())\n            .Returns(AuthorizationResult.Success());\n\n        var userRepository = sutProvider.GetDependency<IUserRepository>();\n        var mockUpdateUserData = Substitute.For<UpdateUserData>();\n        userRepository.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey!)\n            .Returns(mockUpdateUserData);\n\n        // Act\n        await sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data);\n\n        // Assert\n\n        userRepository\n            .Received(1)\n            .SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey);\n\n        await userRepository\n            .Received(1)\n            .SetV2AccountCryptographicStateAsync(\n                user.Id,\n                Arg.Is<UserAccountKeysData>(data =>\n                    data.PublicKeyEncryptionKeyPairData.PublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.PublicKey &&\n                    data.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey &&\n                    data.PublicKeyEncryptionKeyPairData.SignedPublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey &&\n                    data.SignatureKeyPairData!.SignatureAlgorithm == expectedAccountKeysData.SignatureKeyPairData!.SignatureAlgorithm &&\n                    data.SignatureKeyPairData.WrappedSigningKey == expectedAccountKeysData.SignatureKeyPairData.WrappedSigningKey &&\n                    data.SignatureKeyPairData.VerifyingKey == expectedAccountKeysData.SignatureKeyPairData.VerifyingKey &&\n                    data.SecurityStateData!.SecurityState == expectedAccountKeysData.SecurityStateData!.SecurityState &&\n                    data.SecurityStateData.SecurityVersion == expectedAccountKeysData.SecurityStateData.SecurityVersion),\n                Arg.Is<IEnumerable<UpdateUserData>>(actions =>\n                    actions.Count() == 1 && actions.First() == mockUpdateUserData));\n\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);\n\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>()\n            .Received(1)\n            .AcceptOrgUserByOrgSsoIdAsync(data.OrgIdentifier, user, sutProvider.GetDependency<IUserService>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task SetKeyConnectorKeyForUserAsync_UserCantUseKeyConnector_ThrowsException(\n        User user,\n        KeyConnectorKeysData data,\n        SutProvider<SetKeyConnectorKeyCommand> sutProvider)\n    {\n        // Arrange\n        user.UsesKeyConnector = true;\n        var currentContext = sutProvider.GetDependency<ICurrentContext>();\n        var httpContext = Substitute.For<HttpContext>();\n        httpContext.User.Returns(new ClaimsPrincipal());\n        currentContext.HttpContext.Returns(httpContext);\n\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())\n            .Returns(AuthorizationResult.Failed());\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data));\n\n        sutProvider.GetDependency<IUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .SetKeyConnectorUserKey(Arg.Any<Guid>(), Arg.Any<string>());\n\n        await sutProvider.GetDependency<IUserRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>(), Arg.Any<IEnumerable<UpdateUserData>>());\n\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogUserEventAsync(Arg.Any<Guid>(), Arg.Any<EventType>());\n\n        await sutProvider.GetDependency<IAcceptOrgUserCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .AcceptOrgUserByOrgSsoIdAsync(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<IUserService>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs",
    "content": "﻿#nullable enable\n\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Kdf.Implementations;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.Kdf;\n\n[SutProviderCustomize]\npublic class ChangeKdfCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ChangeKdfAsync_ChangesKdfAsync(SutProvider<ChangeKdfCommand> sutProvider, User user)\n    {\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(true));\n        sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(IdentityResult.Success));\n\n        var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = kdf,\n            MasterPasswordAuthenticationHash = \"newMasterPassword\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = kdf,\n            MasterKeyWrappedUserKey = \"masterKeyWrappedUserKey\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n\n        await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData);\n\n        await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u =>\n            u.Id == user.Id\n            && u.Kdf == Enums.KdfType.Argon2id\n            && u.KdfIterations == 4\n            && u.KdfMemory == 512\n            && u.KdfParallelism == 4\n        ));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider<ChangeKdfCommand> sutProvider)\n    {\n        var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = kdf,\n            MasterPasswordAuthenticationHash = \"newMasterPassword\",\n            Salt = \"salt\"\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = kdf,\n            MasterKeyWrappedUserKey = \"masterKeyWrappedUserKey\",\n            Salt = \"salt\"\n        };\n\n        await Assert.ThrowsAsync<ArgumentNullException>(async () =>\n            await sutProvider.Sut.ChangeKdfAsync(null!, \"masterPassword\", authenticationData, unlockData));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider<ChangeKdfCommand> sutProvider,\n        User user)\n    {\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(false));\n\n        var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = kdf,\n            MasterPasswordAuthenticationHash = \"newMasterPassword\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = kdf,\n            MasterKeyWrappedUserKey = \"masterKeyWrappedUserKey\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n\n        var result = await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData);\n        Assert.False(result.Succeeded);\n        Assert.Contains(result.Errors, e => e.Code == \"PasswordMismatch\");\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task\n        ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOff_UpdatesUserCorrectlyAndLogsOut(\n            SutProvider<ChangeKdfCommand> sutProvider, User user)\n    {\n        var constantKdf = new KdfSettings\n        {\n            KdfType = Enums.KdfType.Argon2id,\n            Iterations = 5,\n            Memory = 1024,\n            Parallelism = 4\n        };\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = constantKdf,\n            MasterPasswordAuthenticationHash = \"new-auth-hash\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = constantKdf,\n            MasterKeyWrappedUserKey = \"new-wrapped-key\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(true));\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>())\n            .Returns(Task.FromResult(IdentityResult.Success));\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Any<string>()).Returns(false);\n\n        await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData);\n\n        await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u =>\n            u.Id == user.Id\n            && u.Kdf == constantKdf.KdfType\n            && u.KdfIterations == constantKdf.Iterations\n            && u.KdfMemory == constantKdf.Memory\n            && u.KdfParallelism == constantKdf.Parallelism\n            && u.Key == \"new-wrapped-key\"\n        ));\n        await sutProvider.GetDependency<IUserService>().Received(1).UpdatePasswordHash(user,\n            authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: true);\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(user.Id);\n        sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task\n        ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOn_UpdatesUserCorrectlyAndDoesNotLogOut(\n            SutProvider<ChangeKdfCommand> sutProvider, User user)\n    {\n        var constantKdf = new KdfSettings\n        {\n            KdfType = Enums.KdfType.Argon2id,\n            Iterations = 5,\n            Memory = 1024,\n            Parallelism = 4\n        };\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = constantKdf,\n            MasterPasswordAuthenticationHash = \"new-auth-hash\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = constantKdf,\n            MasterKeyWrappedUserKey = \"new-wrapped-key\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(true));\n        sutProvider.GetDependency<IUserService>()\n            .UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>())\n            .Returns(Task.FromResult(IdentityResult.Success));\n        sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Any<string>()).Returns(true);\n\n        await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData);\n\n        await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u =>\n            u.Id == user.Id\n            && u.Kdf == constantKdf.KdfType\n            && u.KdfIterations == constantKdf.Iterations\n            && u.KdfMemory == constantKdf.Memory\n            && u.KdfParallelism == constantKdf.Parallelism\n            && u.Key == \"new-wrapped-key\"\n        ));\n        await sutProvider.GetDependency<IUserService>().Received(1).UpdatePasswordHash(user,\n            authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: false);\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1)\n            .PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KdfChange);\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncSettingsAsync(user.Id);\n        sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(\n        SutProvider<ChangeKdfCommand> sutProvider, User user)\n    {\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(true));\n\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = new KdfSettings\n            {\n                KdfType = Enums.KdfType.Argon2id,\n                Iterations = 4,\n                Memory = 512,\n                Parallelism = 4\n            },\n            MasterPasswordAuthenticationHash = \"new-auth-hash\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = new KdfSettings { KdfType = Enums.KdfType.PBKDF2_SHA256, Iterations = 100000 },\n            MasterKeyWrappedUserKey = \"new-wrapped-key\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user,\n        KdfSettings kdf)\n    {\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(true));\n\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = kdf,\n            MasterPasswordAuthenticationHash = \"new-auth-hash\",\n            Salt = \"different-salt\"\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = kdf,\n            MasterKeyWrappedUserKey = \"new-wrapped-key\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user,\n        KdfSettings kdf)\n    {\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(true));\n\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = kdf,\n            MasterPasswordAuthenticationHash = \"new-auth-hash\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = kdf,\n            MasterKeyWrappedUserKey = \"new-wrapped-key\",\n            Salt = \"different-salt\"\n        };\n        await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider<ChangeKdfCommand> sutProvider,\n        User user)\n    {\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(true));\n        var failedResult = IdentityResult.Failed(new IdentityError { Code = \"TestFail\", Description = \"Test fail\" });\n        sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(failedResult));\n\n        var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = kdf,\n            MasterPasswordAuthenticationHash = \"newMasterPassword\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = kdf,\n            MasterKeyWrappedUserKey = \"masterKeyWrappedUserKey\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n\n        var result = await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData);\n\n        Assert.False(result.Succeeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(\n        SutProvider<ChangeKdfCommand> sutProvider, User user)\n    {\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(true));\n\n        // Create invalid KDF settings (iterations too low for PBKDF2)\n        var invalidKdf = new KdfSettings\n        {\n            KdfType = Enums.KdfType.PBKDF2_SHA256,\n            Iterations = 1000, // This is below the minimum of 600,000\n            Memory = null,\n            Parallelism = null\n        };\n\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = invalidKdf,\n            MasterPasswordAuthenticationHash = \"new-auth-hash\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = invalidKdf,\n            MasterKeyWrappedUserKey = \"new-wrapped-key\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData));\n\n        Assert.Equal(\"KDF settings are invalid.\", exception.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(\n        SutProvider<ChangeKdfCommand> sutProvider, User user)\n    {\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())\n            .Returns(Task.FromResult(true));\n\n        // Create invalid Argon2 KDF settings (memory too high)\n        var invalidKdf = new KdfSettings\n        {\n            KdfType = Enums.KdfType.Argon2id,\n            Iterations = 3, // Valid\n            Memory = 2048, // This is above the maximum of 1024\n            Parallelism = 4 // Valid\n        };\n\n        var authenticationData = new MasterPasswordAuthenticationData\n        {\n            Kdf = invalidKdf,\n            MasterPasswordAuthenticationHash = \"new-auth-hash\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n        var unlockData = new MasterPasswordUnlockData\n        {\n            Kdf = invalidKdf,\n            MasterKeyWrappedUserKey = \"new-wrapped-key\",\n            Salt = user.GetMasterPasswordSalt()\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.ChangeKdfAsync(user, \"masterPassword\", authenticationData, unlockData));\n\n        Assert.Equal(\"KDF settings are invalid.\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/Models/Data/V2UpgradeTokenDataTests.cs",
    "content": "﻿using Bit.Core.KeyManagement.Models.Data;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.Models.Data;\n\npublic class V2UpgradeTokenDataTests\n{\n    private static readonly string _mockEncryptedType2String =\n        \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n    private static readonly string _mockEncryptedType7String = \"7.AOs41Hd8OQiCPXjyJKCiDA==\";\n\n    [Fact]\n    public void ToJson_SerializesCorrectly()\n    {\n        var data = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedType2String\n        };\n\n        var json = data.ToJson();\n\n        var expected = $\"{{\\\"WrappedUserKey1\\\":\\\"{_mockEncryptedType7String}\\\",\\\"WrappedUserKey2\\\":\\\"{_mockEncryptedType2String}\\\"}}\";\n        Assert.Equal(expected, json);\n    }\n\n    [Fact]\n    public void FromJson_ValidJson_DeserializesCorrectly()\n    {\n        var json = $\"{{\\\"WrappedUserKey1\\\":\\\"{_mockEncryptedType7String}\\\",\\\"WrappedUserKey2\\\":\\\"{_mockEncryptedType2String}\\\"}}\";\n\n        var result = V2UpgradeTokenData.FromJson(json);\n\n        Assert.NotNull(result);\n        Assert.Equal(_mockEncryptedType7String, result.WrappedUserKey1);\n        Assert.Equal(_mockEncryptedType2String, result.WrappedUserKey2);\n    }\n\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"\")]\n    [InlineData(\"   \")]\n    public void FromJson_NullOrEmptyInput_ReturnsNull(string? input)\n    {\n        var result = V2UpgradeTokenData.FromJson(input);\n\n        Assert.Null(result);\n    }\n\n    [Fact]\n    public void FromJson_InvalidJson_ReturnsNull()\n    {\n        var result = V2UpgradeTokenData.FromJson(\"{\\\"invalid\\\": json}\");\n\n        Assert.Null(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/Queries/KeyConnectorConfirmationDetailsQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Queries;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.Queries;\n\n[SutProviderCustomize]\npublic class KeyConnectorConfirmationDetailsQueryTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Run_OrganizationNotFound_Throws(SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider,\n        Guid userId, string orgSsoIdentifier)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Run(orgSsoIdentifier, userId));\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ReceivedWithAnyArgs(0)\n            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Run_OrganizationNotKeyConnector_Throws(\n        SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider,\n        Guid userId, string orgSsoIdentifier, Organization org)\n    {\n        org.Identifier = orgSsoIdentifier;\n        org.UseKeyConnector = false;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(orgSsoIdentifier).Returns(org);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Run(orgSsoIdentifier, userId));\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .ReceivedWithAnyArgs(0)\n            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Run_OrganizationUserNotFound_Throws(SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider,\n        Guid userId, string orgSsoIdentifier\n        , Organization org)\n    {\n        org.Identifier = orgSsoIdentifier;\n        org.UseKeyConnector = true;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(orgSsoIdentifier).Returns(org);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(Task.FromResult<OrganizationUser>(null));\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Run(orgSsoIdentifier, userId));\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetByOrganizationAsync(org.Id, userId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Run_Success(SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider, Guid userId,\n        string orgSsoIdentifier\n        , Organization org, OrganizationUser orgUser)\n    {\n        org.Identifier = orgSsoIdentifier;\n        org.UseKeyConnector = true;\n        orgUser.OrganizationId = org.Id;\n        orgUser.UserId = userId;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(orgSsoIdentifier).Returns(org);\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(org.Id, userId)\n            .Returns(orgUser);\n\n        var result = await sutProvider.Sut.Run(orgSsoIdentifier, userId);\n\n        Assert.Equal(org.Name, result.OrganizationName);\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetByOrganizationAsync(org.Id, userId);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/Queries/UserAccountKeysQuery.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Queries;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.Queries;\n\n[SutProviderCustomize]\npublic class UserAccountKeysQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task V1User_Success(SutProvider<UserAccountKeysQuery> sutProvider, User user)\n    {\n        var result = await sutProvider.Sut.Run(user);\n        Assert.Equal(user.GetPublicKeyEncryptionKeyPair().PublicKey, result.PublicKeyEncryptionKeyPairData.PublicKey);\n        Assert.Equal(user.GetPublicKeyEncryptionKeyPair().WrappedPrivateKey, result.PublicKeyEncryptionKeyPairData.WrappedPrivateKey);\n    }\n\n    [Theory, BitAutoData]\n    public async Task V2User_Success(SutProvider<UserAccountKeysQuery> sutProvider, User user)\n    {\n        user.SecurityState = \"v2\";\n        user.SecurityVersion = 2;\n        var signatureKeyPairRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        signatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519, \"wrappedSigningKey\", \"verifyingKey\"));\n        var result = await sutProvider.Sut.Run(user);\n        Assert.Equal(user.GetPublicKeyEncryptionKeyPair().PublicKey, result.PublicKeyEncryptionKeyPairData.PublicKey);\n        Assert.Equal(user.GetPublicKeyEncryptionKeyPair().WrappedPrivateKey, result.PublicKeyEncryptionKeyPairData.WrappedPrivateKey);\n        Assert.Equal(user.GetPublicKeyEncryptionKeyPair().SignedPublicKey, result.PublicKeyEncryptionKeyPairData.SignedPublicKey);\n\n        Assert.NotNull(result.SignatureKeyPairData);\n        Assert.Equal(\"wrappedSigningKey\", result.SignatureKeyPairData.WrappedSigningKey);\n        Assert.Equal(\"verifyingKey\", result.SignatureKeyPairData.VerifyingKey);\n\n        Assert.Equal(user.SecurityState, result.SecurityStateData.SecurityState);\n        Assert.Equal(user.GetSecurityVersion(), result.SecurityStateData.SecurityVersion);\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/SendPasswordHasherTests.cs",
    "content": "﻿using Bit.Core.KeyManagement.Sends;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.Sends;\n\n[SutProviderCustomize]\npublic class SendPasswordHasherTests\n{\n    [Theory]\n    [BitAutoData(PasswordVerificationResult.Success)]\n    [BitAutoData(PasswordVerificationResult.SuccessRehashNeeded)]\n    void VerifyPasswordHash_WithValidMatching_ReturnsTrue(\n        PasswordVerificationResult passwordVerificationResult,\n        SutProvider<SendPasswordHasher> sutProvider,\n        string sendPasswordHash,\n        string inputPasswordHash)\n    {\n        // Arrange\n        sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()\n            .VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), sendPasswordHash, inputPasswordHash)\n            .Returns(passwordVerificationResult);\n\n        // Act\n        var result = sutProvider.Sut.PasswordHashMatches(sendPasswordHash, inputPasswordHash);\n\n        // Assert\n        Assert.True(result);\n        sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()\n            .Received(1)\n            .VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), sendPasswordHash, inputPasswordHash);\n    }\n\n    [Theory, BitAutoData]\n    void VerifyPasswordHash_WithNonMatchingPasswords_ReturnsFalse(\n        SutProvider<SendPasswordHasher> sutProvider,\n        string sendPasswordHash,\n        string inputPasswordHash)\n    {\n        // Arrange\n        sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()\n            .VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), sendPasswordHash, inputPasswordHash)\n            .Returns(PasswordVerificationResult.Failed);\n\n        // Act\n        var result = sutProvider.Sut.PasswordHashMatches(sendPasswordHash, inputPasswordHash);\n\n        // Assert\n        Assert.False(result);\n        sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()\n            .Received(1)\n            .VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), sendPasswordHash, inputPasswordHash);\n    }\n\n    [Theory]\n    [InlineData(null, \"inputPassword\")]\n    [InlineData(\"\", \"inputPassword\")]\n    [InlineData(\"   \", \"inputPassword\")]\n    [InlineData(\"sendPassword\", null)]\n    [InlineData(\"sendPassword\", \"\")]\n    [InlineData(\"sendPassword\", \"   \")]\n    [InlineData(null, null)]\n    [InlineData(\"\", \"\")]\n    public void VerifyPasswordHash_WithNullOrEmptyParameters_ReturnsFalse(\n        string? sendPasswordHash,\n        string? inputPasswordHash)\n    {\n        // Arrange\n        var passwordHasher = Substitute.For<IPasswordHasher<SendPasswordHasherMarker>>();\n        var sut = new SendPasswordHasher(passwordHasher);\n\n        // Act\n        var result = sut.PasswordHashMatches(sendPasswordHash, inputPasswordHash);\n\n        // Assert\n        Assert.False(result);\n        passwordHasher.DidNotReceive().VerifyHashedPassword(Arg.Any<SendPasswordHasherMarker>(), Arg.Any<string>(), Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    void HashPasswordHash_WithValidInput_ReturnsHashedPassword(\n        SutProvider<SendPasswordHasher> sutProvider,\n        string clientHashedPassword,\n        string expectedHashedResult)\n    {\n        // Arrange\n        sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()\n            .HashPassword(Arg.Any<SendPasswordHasherMarker>(), clientHashedPassword)\n            .Returns(expectedHashedResult);\n\n        // Act\n        var result = sutProvider.Sut.HashOfClientPasswordHash(clientHashedPassword);\n\n        // Assert\n        Assert.Equal(expectedHashedResult, result);\n        sutProvider.GetDependency<IPasswordHasher<SendPasswordHasherMarker>>()\n            .Received(1)\n            .HashPassword(Arg.Any<SendPasswordHasherMarker>(), clientHashedPassword);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.KeyManagement.Enums;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.KeyManagement.Repositories;\nusing Bit.Core.KeyManagement.UserKey.Implementations;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.UserKey;\n\n[SutProviderCustomize]\npublic class RotateUserAccountKeysCommandTests\n{\n    private static readonly string _mockEncryptedType2String =\n        \"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\";\n    private static readonly string _mockEncryptedType2String2 =\n        \"2.06CDSJjTZaigYHUuswIq5A==|trxgZl2RCkYrrmCvGE9WNA==|w5p05eI5wsaYeSyWtsAPvBX63vj798kIMxBTfSB0BQg=\";\n    private static readonly string _mockEncryptedType7String = \"7.AOs41Hd8OQiCPXjyJKCiDA==\";\n    private static readonly string _mockEncryptedType7String2 = \"7.Mi1iaXR3YXJkZW4tZGF0YQo=\";\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_WrongOldMasterPassword_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,\n        RotateUserAccountKeysData model)\n    {\n        user.Email = model.MasterPasswordUnlockData.Email;\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(false);\n\n        var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);\n\n        Assert.NotEqual(IdentityResult.Success, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_UserIsNull_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider,\n          RotateUserAccountKeysData model)\n    {\n        await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_EmailChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,\n        RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        model.MasterPasswordUnlockData.Email = user.Email + \".different-domain\";\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n        await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_KdfChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,\n        RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256;\n        model.MasterPasswordUnlockData.KdfIterations = 600000;\n        model.MasterPasswordUnlockData.KdfMemory = null;\n        model.MasterPasswordUnlockData.KdfParallelism = null;\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n        await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,\n        RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = \"new-public\";\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n\n        await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,\n        RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n\n        var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);\n        Assert.Equal(IdentityResult.Success, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_UpgradeV1ToV2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,\n        RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n\n        var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);\n        Assert.Equal(IdentityResult.Success, result);\n        Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);\n    }\n\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = \"new-public\";\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_V2User_PrivateKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV2ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n        model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType2String;\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_V1User_PrivateKeyNotAesCbcHmac_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n        model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType7String;\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"The provided account private key was not wrapped with AES-256-CBC-HMAC\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);\n        Assert.Empty(saveEncryptedDataActions);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_V2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV2ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);\n        Assert.NotEmpty(saveEncryptedDataActions);\n        Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);\n    }\n\n\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_V2User_VerifyingKeyMismatch_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV2ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n        model.AccountKeys.SignatureKeyPairData.VerifyingKey = \"different-verifying-key\";\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"The provided verifying key does not match the user's current verifying key.\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_V2User_SignedPublicKeyNullOrEmpty_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV2ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n        model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"No signed public key provided, but the user already has a signature key pair.\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_V2User_WrappedSigningKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV2ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n        model.AccountKeys.SignatureKeyPairData.WrappedSigningKey = _mockEncryptedType2String;\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"The provided signing key data is not wrapped with XChaCha20-Poly1305.\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeys_UpgradeToV2_InvalidVerifyingKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n        model.AccountKeys.SignatureKeyPairData.VerifyingKey = \"\";\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"The provided signature key pair data does not contain a valid verifying key.\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_UpgradeToV2_IncorrectlyWrappedPrivateKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n        model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType2String;\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"The provided private key encryption key is not wrapped with XChaCha20-Poly1305.\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSignedPublicKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n        model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"No signed public key provided, but the user already has a signature key pair.\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSecurityState_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n        model.AccountKeys.SecurityStateData = null;\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"No signed security state provider for V2 user\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_RotateV2_NoSignatureKeyPair_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV2ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n        model.AccountKeys.SignatureKeyPairData = null;\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"Signature key pair data is required for V2 encryption.\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_GetEncryptionType_EmptyString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n        model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = \"\";\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"Invalid encryption type string.\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAccountKeysAsync_GetEncryptionType_InvalidString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n        model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = \"9.xxx\";\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n        var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));\n        Assert.Equal(\"Invalid encryption type string.\", ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateUserData_RevisionDateChanged_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        var oldDate = new DateTime(2017, 1, 1);\n\n        var cipher = Substitute.For<Cipher>();\n        cipher.RevisionDate = oldDate;\n        model.Ciphers = [cipher];\n\n        var folder = Substitute.For<Folder>();\n        folder.RevisionDate = oldDate;\n        model.Folders = [folder];\n\n        var send = Substitute.For<Send>();\n        send.RevisionDate = oldDate;\n        model.Sends = [send];\n\n        var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();\n\n        sutProvider.Sut.UpdateUserData(model, user, saveEncryptedDataActions);\n        foreach (var dataAction in saveEncryptedDataActions)\n        {\n            await dataAction.Invoke();\n        }\n\n        var updatedCiphers = sutProvider.GetDependency<ICipherRepository>()\n            .ReceivedCalls()\n            .FirstOrDefault(call => call.GetMethodInfo().Name == \"UpdateForKeyRotation\")?\n            .GetArguments()[1] as IEnumerable<Cipher>;\n        foreach (var updatedCipher in updatedCiphers!)\n        {\n            var oldCipher = model.Ciphers.FirstOrDefault(c => c.Id == updatedCipher.Id);\n            Assert.NotEqual(oldDate, updatedCipher.RevisionDate);\n        }\n\n        var updatedFolders = sutProvider.GetDependency<IFolderRepository>()\n            .ReceivedCalls()\n            .FirstOrDefault(call => call.GetMethodInfo().Name == \"UpdateForKeyRotation\")?\n            .GetArguments()[1] as IEnumerable<Folder>;\n        foreach (var updatedFolder in updatedFolders!)\n        {\n            var oldFolder = model.Folders.FirstOrDefault(f => f.Id == updatedFolder.Id);\n            Assert.NotEqual(oldDate, updatedFolder.RevisionDate);\n        }\n\n        var updatedSends = sutProvider.GetDependency<ISendRepository>()\n            .ReceivedCalls()\n            .FirstOrDefault(call => call.GetMethodInfo().Name == \"UpdateForKeyRotation\")?\n            .GetArguments()[1] as IEnumerable<Send>;\n        foreach (var updatedSend in updatedSends!)\n        {\n            var oldSend = model.Sends.FirstOrDefault(s => s.Id == updatedSend.Id);\n            Assert.NotEqual(oldDate, updatedSend.RevisionDate);\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_WithV2UpgradeToken_NoLogout(\n        SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        // Arrange\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();\n        model.V2UpgradeToken = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedType2String\n        };\n\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n\n        // Act\n        await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);\n\n        // Assert - Security stamp is not updated\n        Assert.Equal(originalSecurityStamp, user.SecurityStamp);\n\n        // Assert - Token is stored on user\n        Assert.NotNull(user.V2UpgradeToken);\n        Assert.Contains(_mockEncryptedType7String, user.V2UpgradeToken);\n        Assert.Contains(_mockEncryptedType2String, user.V2UpgradeToken);\n\n        // Assert - Push notification sent with KeyRotation reason\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1)\n            .PushLogOutAsync(user.Id, false, Enums.PushNotificationLogOutReason.KeyRotation);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_WithoutV2UpgradeToken_Logout(\n        SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        // Arrange\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();\n        user.V2UpgradeToken = null;\n        model.V2UpgradeToken = null;\n\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n\n        // Act\n        await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);\n\n        // Assert - Security stamp is updated\n        Assert.NotEqual(originalSecurityStamp, user.SecurityStamp);\n\n        // Assert - Token is not stored on user\n        Assert.Null(user.V2UpgradeToken);\n\n        // Assert - Push notification sent without reason\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1)\n            .PushLogOutAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_WithExistingToken_WithoutNewToken_ClearsStaleToken(\n        SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        // Arrange\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();\n\n        // User has existing stale token from previous rotation\n        var staleToken = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedType2String\n        };\n        user.V2UpgradeToken = staleToken.ToJson();\n\n        // Model does NOT provide new token\n        model.V2UpgradeToken = null;\n\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n\n        // Act\n        await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);\n\n        // Assert - Stale token explicitly cleared\n        Assert.Null(user.V2UpgradeToken);\n\n        // Assert - Security stamp is updated (logout behavior)\n        Assert.NotEqual(originalSecurityStamp, user.SecurityStamp);\n\n        // Assert - Push notification sent without reason (standard logout)\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1)\n            .PushLogOutAsync(user.Id);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_WithExistingToken_WithNewToken_UpdatesToken(\n        SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        // Arrange\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV1ExistingUser(user, signatureRepository);\n        SetV1ModelUser(model);\n\n        var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();\n\n        // User has existing token from previous rotation\n        var oldToken = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedType2String\n        };\n        user.V2UpgradeToken = oldToken.ToJson();\n\n        // Model provides NEW token\n        model.V2UpgradeToken = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedType7String2,\n            WrappedUserKey2 = _mockEncryptedType2String2\n        };\n\n        sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n\n        // Act\n        await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);\n\n        // Assert - Security stamp is not updated (no logout)\n        Assert.Equal(originalSecurityStamp, user.SecurityStamp);\n\n        // Assert - Token contains new wrapped keys\n        Assert.NotNull(user.V2UpgradeToken);\n        Assert.Contains(_mockEncryptedType7String2, user.V2UpgradeToken);\n        Assert.Contains(_mockEncryptedType2String2, user.V2UpgradeToken);\n\n        // Assert - Token does NOT contain old wrapped keys\n        Assert.DoesNotContain(oldToken.WrappedUserKey1, user.V2UpgradeToken);\n        Assert.DoesNotContain(oldToken.WrappedUserKey2, user.V2UpgradeToken);\n\n        // Assert - Push notification sent with KeyRotation reason (no logout)\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1)\n            .PushLogOutAsync(user.Id, false, Enums.PushNotificationLogOutReason.KeyRotation);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RotateUserAccountKeysAsync_V2User_WithV2UpgradeToken_IgnoresTokenAndLogsOut(\n        SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)\n    {\n        // Arrange\n        SetTestKdfAndSaltForUserAndModel(user, model);\n        var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();\n        SetV2ExistingUser(user, signatureRepository);\n        SetV2ModelUser(model);\n\n        var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();\n        model.V2UpgradeToken = new V2UpgradeTokenData\n        {\n            WrappedUserKey1 = _mockEncryptedType7String,\n            WrappedUserKey2 = _mockEncryptedType2String\n        };\n\n        sutProvider.GetDependency<IUserService>()\n            .CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)\n            .Returns(true);\n\n        // Act\n        await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);\n\n        // Assert - Token is NOT stored (V2 users don't need upgrade token)\n        Assert.Null(user.V2UpgradeToken);\n\n        // Assert - Security stamp IS updated (full logout)\n        Assert.NotEqual(originalSecurityStamp, user.SecurityStamp);\n\n        // Assert - Standard logout push, not KeyRotation reason\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1)\n            .PushLogOutAsync(user.Id);\n    }\n\n    // Helper functions to set valid test parameters that match each other to the model and user.\n    private static void SetTestKdfAndSaltForUserAndModel(User user, RotateUserAccountKeysData model)\n    {\n        user.Kdf = Enums.KdfType.Argon2id;\n        user.KdfIterations = 3;\n        user.KdfMemory = 64;\n        user.KdfParallelism = 4;\n        model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;\n        model.MasterPasswordUnlockData.KdfIterations = 3;\n        model.MasterPasswordUnlockData.KdfMemory = 64;\n        model.MasterPasswordUnlockData.KdfParallelism = 4;\n        // The email is the salt for the KDF and is validated currently.\n        user.Email = model.MasterPasswordUnlockData.Email;\n    }\n\n    private static void SetV1ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository)\n    {\n        user.PrivateKey = _mockEncryptedType2String;\n        user.PublicKey = \"public\";\n        user.SignedPublicKey = null;\n        userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).ReturnsNull();\n    }\n\n    private static void SetV2ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository)\n    {\n        user.PrivateKey = _mockEncryptedType7String;\n        user.PublicKey = \"public\";\n        user.SignedPublicKey = \"signed-public\";\n        userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(SignatureAlgorithm.Ed25519, _mockEncryptedType7String, \"verifying-key\"));\n    }\n\n    private static void SetV1ModelUser(RotateUserAccountKeysData model)\n    {\n        model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(_mockEncryptedType2String, \"public\", null);\n        model.AccountKeys.SignatureKeyPairData = null;\n        model.AccountKeys.SecurityStateData = null;\n    }\n\n    private static void SetV2ModelUser(RotateUserAccountKeysData model)\n    {\n        model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(_mockEncryptedType7String, \"public\", \"signed-public\");\n        model.AccountKeys.SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, _mockEncryptedType7String, \"verifying-key\");\n        model.AccountKeys.SecurityStateData = new SecurityStateData\n        {\n            SecurityState = \"abc\",\n            SecurityVersion = 2,\n        };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/KeyManagement/Utilities/EncryptionParsingTests.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.KeyManagement.Utilities;\nusing Bit.Test.Common.Constants;\nusing Xunit;\n\nnamespace Bit.Core.Test.KeyManagement.Utilities;\n\npublic class EncryptionParsingTests\n{\n    [Fact]\n    public void GetEncryptionType_WithNull_ThrowsArgumentNullException()\n    {\n        Assert.Throws<ArgumentNullException>(() => EncryptionParsing.GetEncryptionType(null));\n    }\n\n    [Theory]\n    [InlineData(\"2\")] // missing '.' separator\n    [InlineData(\"abc.def\")] // non-numeric prefix\n    [InlineData(\"8.any\")] // undefined enum value\n    [InlineData(\"255.any\")] // out of defined enum range\n    public void GetEncryptionType_WithInvalidString_ThrowsArgumentException(string input)\n    {\n        Assert.Throws<ArgumentException>(() => EncryptionParsing.GetEncryptionType(input));\n    }\n\n    [Theory]\n    [InlineData(TestEncryptionConstants.AES256_CBC_B64_Encstring, EncryptionType.AesCbc256_B64)]\n    [InlineData(TestEncryptionConstants.AES256_CBC_HMAC_Encstring, EncryptionType.AesCbc256_HmacSha256_B64)]\n    [InlineData(TestEncryptionConstants.RSA2048_OAEPSHA1_B64_Encstring, EncryptionType.Rsa2048_OaepSha1_B64)]\n    [InlineData(TestEncryptionConstants.V2PrivateKey, EncryptionType.XChaCha20Poly1305_B64)]\n    [InlineData(TestEncryptionConstants.V2WrappedSigningKey, EncryptionType.XChaCha20Poly1305_B64)]\n    [InlineData(TestEncryptionConstants.AES256_CBC_HMAC_EmptySuffix, EncryptionType.AesCbc256_HmacSha256_B64)] // empty suffix still valid\n    [InlineData(TestEncryptionConstants.XCHACHA20POLY1305_B64_Encstring, EncryptionType.XChaCha20Poly1305_B64)]\n    public void GetEncryptionType_WithValidString_ReturnsExpected(string input, EncryptionType expected)\n    {\n        var result = EncryptionParsing.GetEncryptionType(input);\n        Assert.Equal(expected, result);\n    }\n}\n\n"
  },
  {
    "path": "test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs",
    "content": "﻿#nullable enable\nusing System.ComponentModel.DataAnnotations;\nusing System.Text.Json;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Api;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Api.Request;\n\npublic class PushSendRequestModelTests\n{\n    [Fact]\n    public void Validate_UserIdOrganizationIdInstallationIdNull_Invalid()\n    {\n        var model = new PushSendRequestModel<string>\n        {\n            UserId = null,\n            OrganizationId = null,\n            InstallationId = null,\n            Type = PushType.SyncCiphers,\n            Payload = \"test\"\n        };\n\n        var results = Validate(model);\n\n        Assert.Single(results);\n        Assert.Contains(results,\n            result => result.ErrorMessage == \"UserId or OrganizationId or InstallationId is required.\");\n    }\n\n    [Fact]\n    public void Validate_UserIdProvidedOrganizationIdInstallationIdNull_Valid()\n    {\n        var model = new PushSendRequestModel<string>\n        {\n            UserId = Guid.NewGuid(),\n            OrganizationId = null,\n            InstallationId = null,\n            Type = PushType.SyncCiphers,\n            Payload = \"test\"\n        };\n\n        var results = Validate(model);\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_OrganizationIdProvidedUserIdInstallationIdNull_Valid()\n    {\n        var model = new PushSendRequestModel<string>\n        {\n            UserId = null,\n            OrganizationId = Guid.NewGuid(),\n            InstallationId = null,\n            Type = PushType.SyncCiphers,\n            Payload = \"test\"\n        };\n\n        var results = Validate(model);\n\n        Assert.Empty(results);\n    }\n\n    [Fact]\n    public void Validate_InstallationIdProvidedUserIdOrganizationIdNull_Valid()\n    {\n        var model = new PushSendRequestModel<string>\n        {\n            UserId = null,\n            OrganizationId = null,\n            InstallationId = Guid.NewGuid(),\n            Type = PushType.SyncCiphers,\n            Payload = \"test\"\n        };\n\n        var results = Validate(model);\n\n        Assert.Empty(results);\n    }\n\n    [Theory]\n    [BitAutoData(\"Payload\")]\n    [BitAutoData(\"Type\")]\n    public void Validate_RequiredFieldNotProvided_Invalid(string requiredField)\n    {\n        var model = new PushSendRequestModel<string>\n        {\n            UserId = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            Type = PushType.SyncCiphers,\n            Payload = \"test\"\n        };\n\n        var dictionary = new Dictionary<string, object?>();\n        foreach (var property in model.GetType().GetProperties())\n        {\n            if (property.Name == requiredField)\n            {\n                continue;\n            }\n\n            dictionary[property.Name] = property.GetValue(model);\n        }\n\n        var serialized = JsonSerializer.Serialize(dictionary, JsonHelpers.IgnoreWritingNull);\n        var jsonException =\n            Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<PushSendRequestModel<string>>(serialized));\n        Assert.Contains($\"missing required properties, including the following: {requiredField}\",\n            jsonException.Message);\n    }\n\n    [Fact]\n    public void Validate_AllFieldsPresent_Valid()\n    {\n        var model = new PushSendRequestModel<string>\n        {\n            UserId = Guid.NewGuid(),\n            OrganizationId = Guid.NewGuid(),\n            Type = PushType.SyncCiphers,\n            Payload = \"test payload\",\n            Identifier = Guid.NewGuid().ToString(),\n            ClientType = ClientType.All,\n            DeviceId = Guid.NewGuid()\n        };\n\n        var results = Validate(model);\n\n        Assert.Empty(results);\n    }\n\n    private static List<ValidationResult> Validate<T>(PushSendRequestModel<T> model)\n    {\n        var results = new List<ValidationResult>();\n        Validator.TryValidateObject(model, new ValidationContext(model), results, true);\n        return results;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/Business/BillingCustomerDiscountTests.cs",
    "content": "﻿using Bit.Core.Models.Business;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Business;\n\npublic class BillingCustomerDiscountTests\n{\n    [Theory]\n    [BitAutoData]\n    public void Constructor_PercentageDiscount_SetsIdActivePercentOffAndAppliesTo(string couponId)\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = 25.5m,\n                AmountOff = null,\n                AppliesTo = new CouponAppliesTo\n                {\n                    Products = new List<string> { \"product1\", \"product2\" }\n                }\n            },\n            End = null // Active discount\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Equal(couponId, result.Id);\n        Assert.True(result.Active);\n        Assert.Equal(25.5m, result.PercentOff);\n        Assert.Null(result.AmountOff);\n        Assert.NotNull(result.AppliesTo);\n        Assert.Equal(2, result.AppliesTo.Count);\n        Assert.Contains(\"product1\", result.AppliesTo);\n        Assert.Contains(\"product2\", result.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_AmountDiscount_ConvertsFromCentsToDollars(string couponId)\n    {\n        // Arrange - Stripe sends 1400 cents for $14.00\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = null,\n                AmountOff = 1400, // 1400 cents\n                AppliesTo = new CouponAppliesTo\n                {\n                    Products = new List<string>()\n                }\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Equal(couponId, result.Id);\n        Assert.True(result.Active);\n        Assert.Null(result.PercentOff);\n        Assert.Equal(14.00m, result.AmountOff); // Converted to dollars\n        Assert.NotNull(result.AppliesTo);\n        Assert.Empty(result.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_InactiveDiscount_SetsActiveToFalse(string couponId)\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = 15m\n            },\n            End = DateTime.UtcNow.AddDays(-1) // Expired discount\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Equal(couponId, result.Id);\n        Assert.False(result.Active);\n        Assert.Equal(15m, result.PercentOff);\n    }\n\n    [Fact]\n    public void Constructor_NullCoupon_SetsDiscountPropertiesToNull()\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = null,\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Null(result.Id);\n        Assert.True(result.Active);\n        Assert.Null(result.PercentOff);\n        Assert.Null(result.AmountOff);\n        Assert.Null(result.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_NullAmountOff_SetsAmountOffToNull(string couponId)\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = 10m,\n                AmountOff = null\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Null(result.AmountOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_ZeroAmountOff_ConvertsCorrectly(string couponId)\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                AmountOff = 0\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Equal(0m, result.AmountOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_LargeAmountOff_ConvertsCorrectly(string couponId)\n    {\n        // Arrange - $100.00 discount\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                AmountOff = 10000 // 10000 cents = $100.00\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Equal(100.00m, result.AmountOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_SmallAmountOff_ConvertsCorrectly(string couponId)\n    {\n        // Arrange - $0.50 discount\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                AmountOff = 50 // 50 cents = $0.50\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Equal(0.50m, result.AmountOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_BothDiscountTypes_SetsPercentOffAndAmountOff(string couponId)\n    {\n        // Arrange - Coupon with both percentage and amount (edge case)\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = 20m,\n                AmountOff = 500 // $5.00\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Equal(20m, result.PercentOff);\n        Assert.Equal(5.00m, result.AmountOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithNullAppliesTo_SetsAppliesToNull(string couponId)\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = 10m,\n                AppliesTo = null\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Null(result.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithNullProductsList_SetsAppliesToNull(string couponId)\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = 10m,\n                AppliesTo = new CouponAppliesTo\n                {\n                    Products = null\n                }\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Null(result.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithDecimalAmountOff_RoundsCorrectly(string couponId)\n    {\n        // Arrange - 1425 cents = $14.25\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                AmountOff = 1425\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Equal(14.25m, result.AmountOff);\n    }\n\n    [Fact]\n    public void Constructor_DefaultConstructor_InitializesAllPropertiesToNullOrFalse()\n    {\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount();\n\n        // Assert\n        Assert.Null(result.Id);\n        Assert.False(result.Active);\n        Assert.Null(result.PercentOff);\n        Assert.Null(result.AmountOff);\n        Assert.Null(result.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithFutureEndDate_SetsActiveToFalse(string couponId)\n    {\n        // Arrange - Discount expires in the future\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = 20m\n            },\n            End = DateTime.UtcNow.AddDays(30) // Expires in 30 days\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.False(result.Active); // Should be inactive because End is not null\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithPastEndDate_SetsActiveToFalse(string couponId)\n    {\n        // Arrange - Discount already expired\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = 20m\n            },\n            End = DateTime.UtcNow.AddDays(-30) // Expired 30 days ago\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.False(result.Active); // Should be inactive because End is not null\n    }\n\n    [Fact]\n    public void Constructor_WithNullCouponId_SetsIdToNull()\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = null,\n                PercentOff = 20m\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Null(result.Id);\n        Assert.True(result.Active);\n        Assert.Equal(20m, result.PercentOff);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithNullPercentOff_SetsPercentOffToNull(string couponId)\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = null,\n                AmountOff = 1000\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.Null(result.PercentOff);\n        Assert.Equal(10.00m, result.AmountOff);\n    }\n\n    [Fact]\n    public void Constructor_WithCompleteStripeDiscount_MapsAllProperties()\n    {\n        // Arrange - Comprehensive test with all Stripe Discount properties set\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = \"premium_discount_2024\",\n                PercentOff = 25m,\n                AmountOff = 1500, // $15.00\n                AppliesTo = new CouponAppliesTo\n                {\n                    Products = new List<string> { \"prod_premium\", \"prod_family\", \"prod_teams\" }\n                }\n            },\n            End = null // Active\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert - Verify all properties mapped correctly\n        Assert.Equal(\"premium_discount_2024\", result.Id);\n        Assert.True(result.Active);\n        Assert.Equal(25m, result.PercentOff);\n        Assert.Equal(15.00m, result.AmountOff);\n        Assert.NotNull(result.AppliesTo);\n        Assert.Equal(3, result.AppliesTo.Count);\n        Assert.Contains(\"prod_premium\", result.AppliesTo);\n        Assert.Contains(\"prod_family\", result.AppliesTo);\n        Assert.Contains(\"prod_teams\", result.AppliesTo);\n    }\n\n    [Fact]\n    public void Constructor_WithMinimalStripeDiscount_HandlesNullsGracefully()\n    {\n        // Arrange - Minimal Stripe Discount with most properties null\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = null,\n                PercentOff = null,\n                AmountOff = null,\n                AppliesTo = null\n            },\n            End = DateTime.UtcNow.AddDays(10) // Has end date\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert - Should handle all nulls gracefully\n        Assert.Null(result.Id);\n        Assert.False(result.Active);\n        Assert.Null(result.PercentOff);\n        Assert.Null(result.AmountOff);\n        Assert.Null(result.AppliesTo);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void Constructor_WithEmptyProductsList_PreservesEmptyList(string couponId)\n    {\n        // Arrange\n        var discount = new Discount\n        {\n            Coupon = new Coupon\n            {\n                Id = couponId,\n                PercentOff = 10m,\n                AppliesTo = new CouponAppliesTo\n                {\n                    Products = new List<string>() // Empty but not null\n                }\n            },\n            End = null\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingCustomerDiscount(discount);\n\n        // Assert\n        Assert.NotNull(result.AppliesTo);\n        Assert.Empty(result.AppliesTo);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Business;\n\npublic class CompleteSubscriptionUpdateTests\n{\n    [Theory]\n    [BitAutoData]\n    [TeamsStarterOrganizationCustomize]\n    public void UpgradeItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions(\n        Organization organization)\n    {\n        var teamsStarterPlan = MockPlans.Get(PlanType.TeamsStarter);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = teamsStarterPlan.PasswordManager.StripePlanId },\n                        Quantity = 1\n                    }\n                }\n            }\n        };\n\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        var updatedSubscriptionData = new SubscriptionData\n        {\n            Plan = teamsMonthlyPlan,\n            PurchasedPasswordManagerSeats = 20\n        };\n\n        var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsStarterPlan, updatedSubscriptionData);\n\n        var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);\n\n        Assert.Single(upgradeItemOptions);\n\n        var passwordManagerOptions = upgradeItemOptions.First();\n\n        Assert.Equal(subscription.Items.Data.FirstOrDefault()?.Id, passwordManagerOptions.Id);\n        Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);\n        Assert.Equal(updatedSubscriptionData.PurchasedPasswordManagerSeats, passwordManagerOptions.Quantity);\n        Assert.Null(passwordManagerOptions.Deleted);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [TeamsMonthlyWithAddOnsOrganizationCustomize]\n    public void UpgradeItemOptions_TeamsWithSMToEnterpriseWithSM_ReturnsCorrectOptions(\n        Organization organization)\n    {\n        // 5 purchased, 1 base\n        organization.MaxStorageGb = 6;\n\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"password_manager_subscription_item\",\n                        Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeSeatPlanId },\n                        Quantity = organization.Seats!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"secrets_manager_subscription_item\",\n                        Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeSeatPlanId },\n                        Quantity = organization.SmSeats!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"secrets_manager_service_accounts_subscription_item\",\n                        Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },\n                        Quantity = organization.SmServiceAccounts!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"password_manager_storage_subscription_item\",\n                        Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeStoragePlanId },\n                        Quantity = organization.Storage!.Value\n                    }\n                }\n            }\n        };\n\n        var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly);\n\n        var updatedSubscriptionData = new SubscriptionData\n        {\n            Plan = enterpriseMonthlyPlan,\n            PurchasedPasswordManagerSeats = 50,\n            SubscribedToSecretsManager = true,\n            PurchasedSecretsManagerSeats = 30,\n            PurchasedAdditionalSecretsManagerServiceAccounts = 10,\n            PurchasedAdditionalStorage = 10\n        };\n\n        var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData);\n\n        var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);\n\n        Assert.Equal(4, upgradeItemOptions.Count);\n\n        var passwordManagerOptions = upgradeItemOptions.FirstOrDefault(options =>\n            options.Price == enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId);\n\n        var passwordManagerSubscriptionItem =\n            subscription.Items.Data.FirstOrDefault(item => item.Id == \"password_manager_subscription_item\");\n\n        Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);\n        Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);\n        Assert.Equal(updatedSubscriptionData.PurchasedPasswordManagerSeats, passwordManagerOptions.Quantity);\n        Assert.Null(passwordManagerOptions.Deleted);\n\n        var secretsManagerOptions = upgradeItemOptions.FirstOrDefault(options =>\n            options.Price == enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId);\n\n        var secretsManagerSubscriptionItem =\n            subscription.Items.Data.FirstOrDefault(item => item.Id == \"secrets_manager_subscription_item\");\n\n        Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);\n        Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);\n        Assert.Equal(updatedSubscriptionData.PurchasedSecretsManagerSeats, secretsManagerOptions.Quantity);\n        Assert.Null(secretsManagerOptions.Deleted);\n\n        var serviceAccountsOptions = upgradeItemOptions.FirstOrDefault(options =>\n            options.Price == enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);\n\n        var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>\n            item.Id == \"secrets_manager_service_accounts_subscription_item\");\n\n        Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);\n        Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);\n        Assert.Equal(updatedSubscriptionData.PurchasedAdditionalSecretsManagerServiceAccounts, serviceAccountsOptions.Quantity);\n        Assert.Null(serviceAccountsOptions.Deleted);\n\n        var storageOptions = upgradeItemOptions.FirstOrDefault(options =>\n            options.Price == enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId);\n\n        var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == \"password_manager_storage_subscription_item\");\n\n        Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);\n        Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);\n        Assert.Equal(updatedSubscriptionData.PurchasedAdditionalStorage, storageOptions.Quantity);\n        Assert.Null(storageOptions.Deleted);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [TeamsMonthlyWithAddOnsOrganizationCustomize]\n    public void UpgradeItemOptions_TeamsWithSMToEnterpriseWithoutSM_ReturnsCorrectOptions(\n        Organization organization)\n    {\n        // 5 purchased, 1 base\n        organization.MaxStorageGb = 6;\n\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"password_manager_subscription_item\",\n                        Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeSeatPlanId },\n                        Quantity = organization.Seats!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"secrets_manager_subscription_item\",\n                        Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeSeatPlanId },\n                        Quantity = organization.SmSeats!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"secrets_manager_service_accounts_subscription_item\",\n                        Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },\n                        Quantity = organization.SmServiceAccounts!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"password_manager_storage_subscription_item\",\n                        Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeStoragePlanId },\n                        Quantity = organization.Storage!.Value\n                    }\n                }\n            }\n        };\n\n        var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly);\n\n        var updatedSubscriptionData = new SubscriptionData\n        {\n            Plan = enterpriseMonthlyPlan,\n            PurchasedPasswordManagerSeats = 50,\n            SubscribedToSecretsManager = false,\n            PurchasedSecretsManagerSeats = 0,\n            PurchasedAdditionalSecretsManagerServiceAccounts = 0,\n            PurchasedAdditionalStorage = 10\n        };\n\n        var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData);\n\n        var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);\n\n        Assert.Equal(4, upgradeItemOptions.Count);\n\n        var passwordManagerOptions = upgradeItemOptions.FirstOrDefault(options =>\n            options.Price == enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId);\n\n        var passwordManagerSubscriptionItem =\n            subscription.Items.Data.FirstOrDefault(item => item.Id == \"password_manager_subscription_item\");\n\n        Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);\n        Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);\n        Assert.Equal(updatedSubscriptionData.PurchasedPasswordManagerSeats, passwordManagerOptions.Quantity);\n        Assert.Null(passwordManagerOptions.Deleted);\n\n        var secretsManagerOptions = upgradeItemOptions.FirstOrDefault(options =>\n            options.Price == enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId);\n\n        var secretsManagerSubscriptionItem =\n            subscription.Items.Data.FirstOrDefault(item => item.Id == \"secrets_manager_subscription_item\");\n\n        Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);\n        Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);\n        Assert.Equal(updatedSubscriptionData.PurchasedSecretsManagerSeats, secretsManagerOptions.Quantity);\n        Assert.True(secretsManagerOptions.Deleted);\n\n        var serviceAccountsOptions = upgradeItemOptions.FirstOrDefault(options =>\n            options.Price == enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);\n\n        var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>\n            item.Id == \"secrets_manager_service_accounts_subscription_item\");\n\n        Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);\n        Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);\n        Assert.Equal(updatedSubscriptionData.PurchasedAdditionalSecretsManagerServiceAccounts, serviceAccountsOptions.Quantity);\n        Assert.True(serviceAccountsOptions.Deleted);\n\n        var storageOptions = upgradeItemOptions.FirstOrDefault(options =>\n            options.Price == enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId);\n\n        var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == \"password_manager_storage_subscription_item\");\n\n        Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);\n        Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);\n        Assert.Equal(updatedSubscriptionData.PurchasedAdditionalStorage, storageOptions.Quantity);\n        Assert.Null(storageOptions.Deleted);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [TeamsStarterOrganizationCustomize]\n    public void RevertItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions(\n        Organization organization)\n    {\n        var teamsStarterPlan = MockPlans.Get(PlanType.TeamsStarter);\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeSeatPlanId },\n                        Quantity = 20\n                    }\n                }\n            }\n        };\n\n        var updatedSubscriptionData = new SubscriptionData\n        {\n            Plan = teamsMonthlyPlan,\n            PurchasedPasswordManagerSeats = 20\n        };\n\n        var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsStarterPlan, updatedSubscriptionData);\n\n        var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);\n\n        Assert.Single(revertItemOptions);\n\n        var passwordManagerOptions = revertItemOptions.First();\n\n        Assert.Equal(subscription.Items.Data.FirstOrDefault()?.Id, passwordManagerOptions.Id);\n        Assert.Equal(teamsStarterPlan.PasswordManager.StripePlanId, passwordManagerOptions.Price);\n        Assert.Equal(1, passwordManagerOptions.Quantity);\n        Assert.Null(passwordManagerOptions.Deleted);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [TeamsMonthlyWithAddOnsOrganizationCustomize]\n    public void RevertItemOptions_TeamsWithSMToEnterpriseWithSM_ReturnsCorrectOptions(\n        Organization organization)\n    {\n        // 5 purchased, 1 base\n        organization.MaxStorageGb = 6;\n\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n        var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"password_manager_subscription_item\",\n                        Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId },\n                        Quantity = organization.Seats!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"secrets_manager_subscription_item\",\n                        Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId },\n                        Quantity = organization.SmSeats!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"secrets_manager_service_accounts_subscription_item\",\n                        Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },\n                        Quantity = organization.SmServiceAccounts!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"password_manager_storage_subscription_item\",\n                        Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId },\n                        Quantity = organization.Storage!.Value\n                    }\n                }\n            }\n        };\n\n        var updatedSubscriptionData = new SubscriptionData\n        {\n            Plan = enterpriseMonthlyPlan,\n            PurchasedPasswordManagerSeats = 50,\n            SubscribedToSecretsManager = true,\n            PurchasedSecretsManagerSeats = 30,\n            PurchasedAdditionalSecretsManagerServiceAccounts = 10,\n            PurchasedAdditionalStorage = 10\n        };\n\n        var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData);\n\n        var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);\n\n        Assert.Equal(4, revertItemOptions.Count);\n\n        var passwordManagerOptions = revertItemOptions.FirstOrDefault(options =>\n            options.Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId);\n\n        var passwordManagerSubscriptionItem =\n            subscription.Items.Data.FirstOrDefault(item => item.Id == \"password_manager_subscription_item\");\n\n        Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);\n        Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);\n        Assert.Equal(organization.Seats - teamsMonthlyPlan.PasswordManager.BaseSeats, passwordManagerOptions.Quantity);\n        Assert.Null(passwordManagerOptions.Deleted);\n\n        var secretsManagerOptions = revertItemOptions.FirstOrDefault(options =>\n            options.Price == teamsMonthlyPlan.SecretsManager.StripeSeatPlanId);\n\n        var secretsManagerSubscriptionItem =\n            subscription.Items.Data.FirstOrDefault(item => item.Id == \"secrets_manager_subscription_item\");\n\n        Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);\n        Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);\n        Assert.Equal(organization.SmSeats - teamsMonthlyPlan.SecretsManager.BaseSeats, secretsManagerOptions.Quantity);\n        Assert.Null(secretsManagerOptions.Deleted);\n\n        var serviceAccountsOptions = revertItemOptions.FirstOrDefault(options =>\n            options.Price == teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);\n\n        var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>\n            item.Id == \"secrets_manager_service_accounts_subscription_item\");\n\n        Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);\n        Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);\n        Assert.Equal(organization.SmServiceAccounts - teamsMonthlyPlan.SecretsManager.BaseServiceAccount, serviceAccountsOptions.Quantity);\n        Assert.Null(serviceAccountsOptions.Deleted);\n\n        var storageOptions = revertItemOptions.FirstOrDefault(options =>\n            options.Price == teamsMonthlyPlan.PasswordManager.StripeStoragePlanId);\n\n        var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == \"password_manager_storage_subscription_item\");\n\n        Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);\n        Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);\n        Assert.Equal(organization.MaxStorageGb - teamsMonthlyPlan.PasswordManager.BaseStorageGb, storageOptions.Quantity);\n        Assert.Null(storageOptions.Deleted);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [TeamsMonthlyWithAddOnsOrganizationCustomize]\n    public void RevertItemOptions_TeamsWithSMToEnterpriseWithoutSM_ReturnsCorrectOptions(\n        Organization organization)\n    {\n        // 5 purchased, 1 base\n        organization.MaxStorageGb = 6;\n\n        var teamsMonthlyPlan = MockPlans.Get(PlanType.TeamsMonthly);\n        var enterpriseMonthlyPlan = MockPlans.Get(PlanType.EnterpriseMonthly);\n\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"password_manager_subscription_item\",\n                        Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId },\n                        Quantity = organization.Seats!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"secrets_manager_subscription_item\",\n                        Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId },\n                        Quantity = organization.SmSeats!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"secrets_manager_service_accounts_subscription_item\",\n                        Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },\n                        Quantity = organization.SmServiceAccounts!.Value\n                    },\n                    new ()\n                    {\n                        Id = \"password_manager_storage_subscription_item\",\n                        Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId },\n                        Quantity = organization.Storage!.Value\n                    }\n                }\n            }\n        };\n\n        var updatedSubscriptionData = new SubscriptionData\n        {\n            Plan = enterpriseMonthlyPlan,\n            PurchasedPasswordManagerSeats = 50,\n            SubscribedToSecretsManager = false,\n            PurchasedSecretsManagerSeats = 0,\n            PurchasedAdditionalSecretsManagerServiceAccounts = 0,\n            PurchasedAdditionalStorage = 10\n        };\n\n        var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, teamsMonthlyPlan, updatedSubscriptionData);\n\n        var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);\n\n        Assert.Equal(4, revertItemOptions.Count);\n\n        var passwordManagerOptions = revertItemOptions.FirstOrDefault(options =>\n            options.Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId);\n\n        var passwordManagerSubscriptionItem =\n            subscription.Items.Data.FirstOrDefault(item => item.Id == \"password_manager_subscription_item\");\n\n        Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);\n        Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);\n        Assert.Equal(organization.Seats - teamsMonthlyPlan.PasswordManager.BaseSeats, passwordManagerOptions.Quantity);\n        Assert.Null(passwordManagerOptions.Deleted);\n\n        var secretsManagerOptions = revertItemOptions.FirstOrDefault(options =>\n            options.Price == teamsMonthlyPlan.SecretsManager.StripeSeatPlanId);\n\n        var secretsManagerSubscriptionItem =\n            subscription.Items.Data.FirstOrDefault(item => item.Id == \"secrets_manager_subscription_item\");\n\n        Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);\n        Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);\n        Assert.Equal(organization.SmSeats - teamsMonthlyPlan.SecretsManager.BaseSeats, secretsManagerOptions.Quantity);\n        Assert.Null(secretsManagerOptions.Deleted);\n\n        var serviceAccountsOptions = revertItemOptions.FirstOrDefault(options =>\n            options.Price == teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);\n\n        var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>\n            item.Id == \"secrets_manager_service_accounts_subscription_item\");\n\n        Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);\n        Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);\n        Assert.Equal(organization.SmServiceAccounts - teamsMonthlyPlan.SecretsManager.BaseServiceAccount, serviceAccountsOptions.Quantity);\n        Assert.Null(serviceAccountsOptions.Deleted);\n\n        var storageOptions = revertItemOptions.FirstOrDefault(options =>\n            options.Price == teamsMonthlyPlan.PasswordManager.StripeStoragePlanId);\n\n        var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == \"password_manager_storage_subscription_item\");\n\n        Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);\n        Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);\n        Assert.Equal(organization.MaxStorageGb - teamsMonthlyPlan.PasswordManager.BaseStorageGb, storageOptions.Quantity);\n        Assert.Null(storageOptions.Deleted);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/Business/SeatSubscriptionUpdateTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Business;\n\npublic class SeatSubscriptionUpdateTests\n{\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsStarter)]\n\n    public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)\n    {\n        var plan = MockPlans.Get(planType);\n        organization.PlanType = planType;\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Quantity = 1\n                    }\n                }\n            }\n        };\n        var update = new SeatSubscriptionUpdate(organization, plan, 100);\n\n        var options = update.UpgradeItemsOptions(subscription);\n\n        Assert.Single(options);\n        Assert.Equal(plan.PasswordManager.StripeSeatPlanId, options[0].Plan);\n        Assert.Equal(100, options[0].Quantity);\n        Assert.Null(options[0].Deleted);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)\n    {\n        var plan = MockPlans.Get(planType);\n        organization.PlanType = planType;\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = plan.PasswordManager.StripeSeatPlanId },\n                        Quantity = 100\n                    }\n                }\n            }\n        };\n        var update = new SeatSubscriptionUpdate(organization, plan, 100);\n        update.UpgradeItemsOptions(subscription);\n\n        var options = update.RevertItemsOptions(subscription);\n\n        Assert.Single(options);\n        Assert.Equal(plan.PasswordManager.StripeSeatPlanId, options[0].Plan);\n        Assert.Equal(organization.Seats, options[0].Quantity);\n        Assert.Null(options[0].Deleted);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/Business/SecretsManagerSubscriptionUpdateTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Business;\n\n[SecretsManagerOrganizationCustomize]\npublic class SecretsManagerSubscriptionUpdateTests\n{\n    private static TheoryData<Plan> ToPlanTheory(List<PlanType> types)\n    {\n        var theoryData = new TheoryData<Plan>();\n        var plans = types.Select(MockPlans.Get).ToArray();\n        theoryData.AddRange(plans);\n        return theoryData;\n    }\n\n    public static TheoryData<Plan> NonSmPlans =>\n        ToPlanTheory([PlanType.Custom, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2025, PlanType.FamiliesAnnually2019]);\n\n    public static TheoryData<Plan> SmPlans => ToPlanTheory([\n        PlanType.EnterpriseAnnually2019,\n        PlanType.EnterpriseAnnually,\n        PlanType.TeamsMonthly2019,\n        PlanType.TeamsAnnually2020,\n        PlanType.TeamsMonthly,\n        PlanType.TeamsAnnually2019,\n        PlanType.TeamsAnnually2020,\n        PlanType.TeamsAnnually,\n        PlanType.TeamsStarter\n    ]);\n\n    [Theory]\n    [BitMemberAutoData(nameof(NonSmPlans))]\n    public Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(\n        Plan plan,\n        Organization organization)\n    {\n        // Arrange\n        organization.PlanType = plan.Type;\n\n        // Act\n        var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, plan, false));\n\n        // Assert\n        Assert.Contains(\"Invalid Secrets Manager plan\", exception.Message, StringComparison.InvariantCultureIgnoreCase);\n        return Task.CompletedTask;\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(SmPlans))]\n    public void UpdateSubscription_WithNonSecretsManagerPlanType_DoesNotThrowException(\n        Plan plan,\n        Organization organization)\n    {\n        // Arrange\n        organization.PlanType = plan.Type;\n\n        // Act\n        var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, plan, false));\n\n        // Assert\n        Assert.Null(ex);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/Business/ServiceAccountSubscriptionUpdateTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Business;\n\npublic class ServiceAccountSubscriptionUpdateTests\n{\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsStarter)]\n\n    public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)\n    {\n        var plan = MockPlans.Get(planType);\n        organization.PlanType = planType;\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = plan.SecretsManager.StripeServiceAccountPlanId },\n                        Quantity = 1\n                    }\n                }\n            }\n        };\n        var update = new ServiceAccountSubscriptionUpdate(organization, plan, 3);\n\n        var options = update.UpgradeItemsOptions(subscription);\n\n        Assert.Single(options);\n        Assert.Equal(plan.SecretsManager.StripeServiceAccountPlanId, options[0].Plan);\n        Assert.Equal(3, options[0].Quantity);\n        Assert.Null(options[0].Deleted);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)\n    {\n        var plan = MockPlans.Get(planType);\n        organization.PlanType = planType;\n        var quantity = 5;\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = plan.SecretsManager.StripeServiceAccountPlanId },\n                        Quantity = quantity\n                    }\n                }\n            }\n        };\n        var update = new ServiceAccountSubscriptionUpdate(organization, plan, quantity);\n        update.UpgradeItemsOptions(subscription);\n\n        var options = update.RevertItemsOptions(subscription);\n\n        Assert.Single(options);\n        Assert.Equal(plan.SecretsManager.StripeServiceAccountPlanId, options[0].Plan);\n        Assert.Equal(quantity, options[0].Quantity);\n        Assert.Null(options[0].Deleted);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/Business/SmSeatSubscriptionUpdateTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Business;\n\npublic class SmSeatSubscriptionUpdateTests\n{\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsStarter)]\n\n    public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)\n    {\n        var plan = MockPlans.Get(planType);\n        organization.PlanType = planType;\n        var quantity = 3;\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId },\n                        Quantity = quantity\n                    }\n                }\n            }\n        };\n        var update = new SmSeatSubscriptionUpdate(organization, plan, quantity);\n\n        var options = update.UpgradeItemsOptions(subscription);\n\n        Assert.Single(options);\n        Assert.Equal(plan.SecretsManager.StripeSeatPlanId, options[0].Plan);\n        Assert.Equal(quantity, options[0].Quantity);\n        Assert.Null(options[0].Deleted);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType, Organization organization)\n    {\n        var plan = MockPlans.Get(planType);\n        organization.PlanType = planType;\n        var quantity = 5;\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = plan.SecretsManager.StripeSeatPlanId },\n                        Quantity = quantity\n                    }\n                }\n            }\n        };\n        var update = new SmSeatSubscriptionUpdate(organization, plan, quantity);\n        update.UpgradeItemsOptions(subscription);\n\n        var options = update.RevertItemsOptions(subscription);\n\n        Assert.Single(options);\n        Assert.Equal(plan.SecretsManager.StripeSeatPlanId, options[0].Plan);\n        Assert.Equal(organization.SmSeats, options[0].Quantity);\n        Assert.Null(options[0].Deleted);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/Business/StorageSubscriptionUpdateTests.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Business;\n\npublic class StorageSubscriptionUpdateTests\n{\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsStarter)]\n\n    public void UpgradeItemsOptions_ReturnsCorrectOptions(PlanType planType)\n    {\n        var plan = MockPlans.Get(planType);\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId },\n                        Quantity = 1\n                    }\n                }\n            }\n        };\n        var update = new StorageSubscriptionUpdate(\"plan_id\", 100);\n\n        var options = update.UpgradeItemsOptions(subscription);\n\n        Assert.Single(options);\n        Assert.Equal(\"plan_id\", options[0].Plan);\n        Assert.Equal(100, options[0].Quantity);\n        Assert.Null(options[0].Deleted);\n    }\n\n    [Fact]\n    public void RevertItemsOptions_ThrowsExceptionIfPrevStorageIsNull()\n    {\n        var subscription = new Subscription();\n        var update = new StorageSubscriptionUpdate(\"plan_id\", 100);\n\n        Assert.Throws<Exception>(() => update.RevertItemsOptions(subscription));\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsStarter)]\n    public void RevertItemsOptions_ReturnsCorrectOptions(PlanType planType)\n    {\n        var plan = MockPlans.Get(planType);\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = new List<SubscriptionItem>\n                {\n                    new ()\n                    {\n                        Id = \"subscription_item\",\n                        Price = new Price { Id = plan.PasswordManager.StripeStoragePlanId },\n                        Quantity = 100\n                    }\n                }\n            }\n        };\n        var update = new StorageSubscriptionUpdate(plan.PasswordManager.StripeStoragePlanId, 100);\n        update.UpgradeItemsOptions(subscription);\n\n        var options = update.RevertItemsOptions(subscription);\n\n        Assert.Single(options);\n        Assert.Equal(plan.PasswordManager.StripeStoragePlanId, options[0].Plan);\n        Assert.Equal(100, options[0].Quantity);\n        Assert.Null(options[0].Deleted);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/Business/SubscriptionInfoTests.cs",
    "content": "﻿using Bit.Core.Models.Business;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Business;\n\npublic class SubscriptionInfoTests\n{\n    [Fact]\n    public void BillingSubscriptionItem_NullPlan_HandlesGracefully()\n    {\n        // Arrange - SubscriptionItem with null Plan\n        var subscriptionItem = new SubscriptionItem\n        {\n            Plan = null,\n            Quantity = 1\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingSubscription.BillingSubscriptionItem(subscriptionItem);\n\n        // Assert - Should handle null Plan gracefully\n        Assert.Null(result.ProductId);\n        Assert.Null(result.Name);\n        Assert.Equal(0m, result.Amount); // Defaults to 0 when Plan is null\n        Assert.Null(result.Interval);\n        Assert.Equal(1, result.Quantity);\n        Assert.False(result.SponsoredSubscriptionItem);\n        Assert.False(result.AddonSubscriptionItem);\n    }\n\n    [Fact]\n    public void BillingSubscriptionItem_NullAmount_SetsToZero()\n    {\n        // Arrange - SubscriptionItem with Plan but null Amount\n        var subscriptionItem = new SubscriptionItem\n        {\n            Plan = new Plan\n            {\n                ProductId = \"prod_test\",\n                Nickname = \"Test Plan\",\n                Amount = null, // Null amount\n                Interval = \"month\"\n            },\n            Quantity = 1\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingSubscription.BillingSubscriptionItem(subscriptionItem);\n\n        // Assert - Should default to 0 when Amount is null\n        Assert.Equal(\"prod_test\", result.ProductId);\n        Assert.Equal(\"Test Plan\", result.Name);\n        Assert.Equal(0m, result.Amount); // Business rule: defaults to 0 when null\n        Assert.Equal(\"month\", result.Interval);\n        Assert.Equal(1, result.Quantity);\n    }\n\n    [Fact]\n    public void BillingSubscriptionItem_ZeroAmount_PreservesZero()\n    {\n        // Arrange - SubscriptionItem with Plan and zero Amount\n        var subscriptionItem = new SubscriptionItem\n        {\n            Plan = new Plan\n            {\n                ProductId = \"prod_test\",\n                Nickname = \"Test Plan\",\n                Amount = 0, // Zero amount (0 cents)\n                Interval = \"month\"\n            },\n            Quantity = 1\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingSubscription.BillingSubscriptionItem(subscriptionItem);\n\n        // Assert - Should preserve zero amount\n        Assert.Equal(\"prod_test\", result.ProductId);\n        Assert.Equal(\"Test Plan\", result.Name);\n        Assert.Equal(0m, result.Amount); // Zero amount preserved\n        Assert.Equal(\"month\", result.Interval);\n    }\n\n    [Fact]\n    public void BillingUpcomingInvoice_ZeroAmountDue_ConvertsToZero()\n    {\n        // Arrange - Invoice with zero AmountDue\n        // Note: Stripe's Invoice.AmountDue is non-nullable long, so we test with 0\n        // The null-coalescing operator (?? 0) in the constructor handles the case where\n        // ConvertFromStripeMinorUnits returns null, but since AmountDue is non-nullable,\n        // this test verifies the conversion path works correctly for zero values\n        var invoice = new Invoice\n        {\n            AmountDue = 0, // Zero amount due (0 cents)\n            Created = DateTime.UtcNow\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingUpcomingInvoice(invoice);\n\n        // Assert - Should convert zero correctly\n        Assert.Equal(0m, result.Amount);\n        Assert.NotNull(result.Date);\n    }\n\n    [Fact]\n    public void BillingUpcomingInvoice_ValidAmountDue_ConvertsCorrectly()\n    {\n        // Arrange - Invoice with valid AmountDue\n        var invoice = new Invoice\n        {\n            AmountDue = 2500, // 2500 cents = $25.00\n            Created = DateTime.UtcNow\n        };\n\n        // Act\n        var result = new SubscriptionInfo.BillingUpcomingInvoice(invoice);\n\n        // Assert - Should convert correctly\n        Assert.Equal(25.00m, result.Amount); // Converted from cents\n        Assert.NotNull(result.Date);\n    }\n}\n\n"
  },
  {
    "path": "test/Core.Test/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenableTests.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business.Tokenables;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.Business.Tokenables;\n\npublic class OrganizationSponsorshipOfferTokenableTests\n{\n    public static IEnumerable<object[]> PlanSponsorshipTypes() => Enum.GetValues<PlanSponsorshipType>().Select(x => new object[] { x });\n\n    [Fact]\n    public void IsInvalidIfIdentifierIsWrong()\n    {\n        var token = new OrganizationSponsorshipOfferTokenable()\n        {\n            Email = \"email\",\n            Id = Guid.NewGuid(),\n            Identifier = \"not correct\",\n            SponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,\n        };\n\n        Assert.False(token.Valid);\n    }\n\n    [Fact]\n    public void IsInvalidIfIdIsDefault()\n    {\n        var token = new OrganizationSponsorshipOfferTokenable()\n        {\n            Email = \"email\",\n            Id = default,\n            SponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,\n        };\n\n        Assert.False(token.Valid);\n    }\n\n\n    [Fact]\n    public void IsInvalidIfEmailIsEmpty()\n    {\n        var token = new OrganizationSponsorshipOfferTokenable()\n        {\n            Email = \"\",\n            Id = Guid.NewGuid(),\n            SponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,\n        };\n\n        Assert.False(token.Valid);\n    }\n\n    [Theory, BitAutoData]\n    public void IsValid_Success(OrganizationSponsorship sponsorship)\n    {\n        var token = new OrganizationSponsorshipOfferTokenable(sponsorship);\n\n        Assert.True(token.IsValid(sponsorship, sponsorship.OfferedToEmail));\n    }\n\n    [Theory, BitAutoData]\n    public void IsValid_RequiresNonNullSponsorship(OrganizationSponsorship sponsorship)\n    {\n        var token = new OrganizationSponsorshipOfferTokenable(sponsorship);\n\n        Assert.False(token.IsValid(null, sponsorship.OfferedToEmail));\n    }\n\n    [Theory, BitAutoData]\n    public void IsValid_RequiresCurrentEmailToBeSameAsOfferedToEmail(OrganizationSponsorship sponsorship, string currentEmail)\n    {\n        var token = new OrganizationSponsorshipOfferTokenable(sponsorship);\n\n        Assert.False(token.IsValid(sponsorship, currentEmail));\n    }\n\n    [Theory, BitAutoData]\n    public void IsValid_RequiresSameSponsorshipId(OrganizationSponsorship sponsorship1, OrganizationSponsorship sponsorship2)\n    {\n        sponsorship1.Id = sponsorship2.Id;\n\n        var token = new OrganizationSponsorshipOfferTokenable(sponsorship1);\n\n        Assert.False(token.IsValid(sponsorship2, sponsorship1.OfferedToEmail));\n    }\n\n    [Theory, BitAutoData]\n    public void IsValid_RequiresSameEmail(OrganizationSponsorship sponsorship1, OrganizationSponsorship sponsorship2)\n    {\n        sponsorship1.OfferedToEmail = sponsorship2.OfferedToEmail;\n\n        var token = new OrganizationSponsorshipOfferTokenable(sponsorship1);\n\n        Assert.False(token.IsValid(sponsorship2, sponsorship1.OfferedToEmail));\n    }\n\n    [Theory, BitAutoData]\n    public void Constructor_GrabsIdFromSponsorship(OrganizationSponsorship sponsorship)\n    {\n        var token = new OrganizationSponsorshipOfferTokenable(sponsorship);\n\n        Assert.Equal(sponsorship.Id, token.Id);\n    }\n\n    [Theory, BitAutoData]\n    public void Constructor_GrabsEmailFromSponsorshipOfferedToEmail(OrganizationSponsorship sponsorship)\n    {\n        var token = new OrganizationSponsorshipOfferTokenable(sponsorship);\n\n        Assert.Equal(sponsorship.OfferedToEmail, token.Email);\n    }\n\n    [Theory, BitMemberAutoData(nameof(PlanSponsorshipTypes))]\n    public void Constructor_GrabsSponsorshipType(PlanSponsorshipType planSponsorshipType,\n        OrganizationSponsorship sponsorship)\n    {\n        sponsorship.PlanSponsorshipType = planSponsorshipType;\n        var token = new OrganizationSponsorshipOfferTokenable(sponsorship);\n\n        Assert.Equal(sponsorship.PlanSponsorshipType, token.SponsorshipType);\n    }\n\n    [Theory, BitAutoData]\n    public void Constructor_DefaultId_Throws(OrganizationSponsorship sponsorship)\n    {\n        sponsorship.Id = default;\n\n        Assert.Throws<ArgumentException>(() => new OrganizationSponsorshipOfferTokenable(sponsorship));\n    }\n\n    [Theory, BitAutoData]\n    public void Constructor_NoOfferedToEmail_Throws(OrganizationSponsorship sponsorship)\n    {\n        sponsorship.OfferedToEmail = null;\n\n        Assert.Throws<ArgumentException>(() => new OrganizationSponsorshipOfferTokenable(sponsorship));\n    }\n\n    [Theory, BitAutoData]\n    public void Constructor_EmptyOfferedToEmail_Throws(OrganizationSponsorship sponsorship)\n    {\n        sponsorship.OfferedToEmail = \"\";\n\n        Assert.Throws<ArgumentException>(() => new OrganizationSponsorshipOfferTokenable(sponsorship));\n    }\n\n    [Theory, BitAutoData]\n    public void Constructor_NoPlanSponsorshipType_Throws(OrganizationSponsorship sponsorship)\n    {\n        sponsorship.PlanSponsorshipType = null;\n\n        Assert.Throws<ArgumentException>(() => new OrganizationSponsorshipOfferTokenable(sponsorship));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/OrganizationConnectionConfigs/BillingSyncConfigTests.cs",
    "content": "﻿using Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.OrganizationConnectionConfigs;\n\npublic class BillingSyncConfigTests\n{\n    [Theory]\n    [BitAutoData]\n    public void BillingSyncConfig_CanUse_Success(string billingSyncKey)\n    {\n        var config = new BillingSyncConfig() { BillingSyncKey = billingSyncKey };\n\n        Assert.True(config.Validate(out var exception));\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Fact]\n    public void BillingSyncConfig_CanUse_WhenNoKey_ReturnsFalse()\n    {\n        var config = new BillingSyncConfig();\n\n        Assert.False(config.Validate(out var exception));\n        Assert.Contains(\"Failed to get Billing Sync Key\", exception);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/OrganizationConnectionConfigs/ScimConfigTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models.OrganizationConnectionConfigs;\n\npublic class ScimConfigTests\n{\n    [Fact]\n    public void ScimConfig_CanUse_Success()\n    {\n        var config = new ScimConfig() { Enabled = true };\n        Assert.True(config.Validate(out var exception));\n        Assert.True(string.IsNullOrEmpty(exception));\n    }\n\n    [Fact]\n    public void ScimConfig_CanUse_WhenDisabled_ReturnsFalse()\n    {\n        var config = new ScimConfig() { Enabled = false };\n        Assert.False(config.Validate(out var exception));\n        Assert.Contains(\"Config is disabled\", exception);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Models/PermissionsTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Utilities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Models;\n\npublic class PermissionsTests\n{\n    private static readonly string _exampleSerializedPermissions = string.Concat(\n        \"{\",\n        \"\\\"accessEventLogs\\\": false,\",\n        \"\\\"accessImportExport\\\": false,\",\n        \"\\\"accessReports\\\": false,\",\n        \"\\\"createNewCollections\\\": true,\",\n        \"\\\"editAnyCollection\\\": true,\",\n        \"\\\"deleteAnyCollection\\\": true,\",\n        \"\\\"manageGroups\\\": false,\",\n        \"\\\"managePolicies\\\": false,\",\n        \"\\\"manageSso\\\": false,\",\n        \"\\\"manageUsers\\\": false,\",\n        \"\\\"manageResetPassword\\\": false,\",\n        \"\\\"manageScim\\\": false\",\n        \"}\");\n\n    [Fact]\n    public void Serialization_Success()\n    {\n        var permissions = new Permissions\n        {\n            AccessEventLogs = false,\n            AccessImportExport = false,\n            AccessReports = false,\n            CreateNewCollections = true,\n            EditAnyCollection = true,\n            DeleteAnyCollection = true,\n            ManageGroups = false,\n            ManagePolicies = false,\n            ManageSso = false,\n            ManageUsers = false,\n            ManageResetPassword = false,\n            ManageScim = false,\n        };\n\n        // minify expected json\n        var expected = JsonSerializer.Serialize(permissions, JsonHelpers.CamelCase);\n\n        var actual = JsonSerializer.Serialize(\n            JsonHelpers.DeserializeOrNew<Permissions>(_exampleSerializedPermissions, JsonHelpers.CamelCase),\n            JsonHelpers.CamelCase);\n\n        Assert.Equal(expected, actual);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/Authorization/NotificationAuthorizationHandlerTest.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.NotificationCenter.Authorization;\n\nusing System.Security.Claims;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Entities;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\n[SutProviderCustomize]\n[NotificationCustomize]\npublic class NotificationAuthorizationHandlerTests\n{\n    private static void SetupUserPermission(SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Guid? userId = null, Guid? organizationId = null, bool canAccessReports = false)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<ICurrentContext>().GetOrganization(organizationId.GetValueOrDefault(Guid.NewGuid()))\n            .Returns(new CurrentContextOrganization());\n        sutProvider.GetDependency<ICurrentContext>().AccessReports(organizationId.GetValueOrDefault(Guid.NewGuid()))\n            .Returns(canAccessReports);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task HandleAsync_UnsupportedNotificationOperationRequirement_Throws(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n        var requirement = new NotificationOperationsRequirement(\"UnsupportedOperation\");\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(context));\n    }\n\n    [Theory]\n    [BitAutoData(nameof(NotificationOperations.Read))]\n    [BitAutoData(nameof(NotificationOperations.Create))]\n    [BitAutoData(nameof(NotificationOperations.Update))]\n    public async Task HandleAsync_NotLoggedIn_Unauthorized(\n        string requirementName,\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, userId: null);\n        var requirement = new NotificationOperationsRequirement(requirementName);\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(nameof(NotificationOperations.Read))]\n    [BitAutoData(nameof(NotificationOperations.Create))]\n    [BitAutoData(nameof(NotificationOperations.Update))]\n    public async Task HandleAsync_ResourceEmpty_Unauthorized(\n        string requirementName,\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n        var requirement = new NotificationOperationsRequirement(requirementName);\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: true)]\n    public async Task HandleAsync_ReadRequirementGlobalNotification_Authorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n\n        var requirement = NotificationOperations.Read;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(false)]\n    [BitAutoData(true)]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_ReadRequirementUserNotMatching_Unauthorized(\n        bool hasOrganizationId,\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid(), notification.OrganizationId);\n\n        if (!hasOrganizationId)\n        {\n            notification.OrganizationId = null;\n        }\n\n        var requirement = NotificationOperations.Read;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [BitAutoData(false)]\n    [BitAutoData(true)]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_ReadRequirementOrganizationNotMatching_Unauthorized(\n        bool hasUserId,\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId, Guid.NewGuid());\n\n        if (!hasUserId)\n        {\n            notification.UserId = null;\n        }\n\n        var requirement = NotificationOperations.Read;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(false, true)]\n    [BitAutoData(true, false)]\n    [BitAutoData(true, true)]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_ReadRequirement_Authorized(\n        bool hasUserId,\n        bool hasOrganizationId,\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId);\n\n        if (!hasUserId)\n        {\n            notification.UserId = null;\n        }\n\n        if (!hasOrganizationId)\n        {\n            notification.OrganizationId = null;\n        }\n\n        var requirement = NotificationOperations.Read;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: true)]\n    public async Task HandleAsync_CreateRequirementGlobalNotification_Unauthorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n        var requirement = NotificationOperations.Create;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_CreateRequirementUserNotMatching_Unauthorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid(), notification.OrganizationId);\n\n        notification.OrganizationId = null;\n\n        var requirement = NotificationOperations.Create;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_CreateRequirementOrganizationNotMatching_Unauthorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId, Guid.NewGuid());\n\n        var requirement = NotificationOperations.Create;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_CreateRequirementOrganizationUserNoAccessReportsPermission_Unauthorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, canAccessReports: false);\n\n        var requirement = NotificationOperations.Create;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_CreateRequirementUserNotPartOfOrganization_Authorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId);\n\n        notification.OrganizationId = null;\n\n        var requirement = NotificationOperations.Create;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(false)]\n    [BitAutoData(true)]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_CreateRequirementOrganizationUserCanAccessReports_Authorized(\n        bool hasUserId,\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, true);\n\n        if (!hasUserId)\n        {\n            notification.UserId = null;\n        }\n\n        var requirement = NotificationOperations.Create;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    // TODO\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: true)]\n    public async Task HandleAsync_UpdateRequirementGlobalNotification_Unauthorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n        var requirement = NotificationOperations.Update;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_UpdateRequirementUserNotMatching_Unauthorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid(), notification.OrganizationId);\n\n        notification.OrganizationId = null;\n\n        var requirement = NotificationOperations.Update;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_UpdateRequirementOrganizationNotMatching_Unauthorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId, Guid.NewGuid());\n\n        var requirement = NotificationOperations.Update;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_UpdateRequirementOrganizationUserNoAccessReportsPermission_Unauthorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, canAccessReports: false);\n\n        var requirement = NotificationOperations.Update;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_UpdateRequirementUserNotPartOfOrganization_Authorized(\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId);\n\n        notification.OrganizationId = null;\n\n        var requirement = NotificationOperations.Update;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(false)]\n    [BitAutoData(true)]\n    [NotificationCustomize(global: false)]\n    public async Task HandleAsync_UpdateRequirementOrganizationUserCanAccessReports_Authorized(\n        bool hasUserId,\n        SutProvider<NotificationAuthorizationHandler> sutProvider,\n        Notification notification, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, true);\n\n        if (!hasUserId)\n        {\n            notification.UserId = null;\n        }\n\n        var requirement = NotificationOperations.Update;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notification);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/Authorization/NotificationStatusAuthorizationHandlerTest.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.NotificationCenter.Authorization;\n\nusing System.Security.Claims;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Entities;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\n[SutProviderCustomize]\n[NotificationStatusCustomize]\npublic class NotificationStatusAuthorizationHandlerTests\n{\n    private static void SetupUserPermission(SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        Guid? userId = null)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task HandleAsync_UnsupportedNotificationOperationRequirement_Throws(\n        SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n        var requirement = new NotificationStatusOperationsRequirement(\"UnsupportedOperation\");\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notificationStatus);\n\n        await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(context));\n    }\n\n    [Theory]\n    [BitAutoData(nameof(NotificationStatusOperations.Read))]\n    [BitAutoData(nameof(NotificationStatusOperations.Create))]\n    [BitAutoData(nameof(NotificationStatusOperations.Update))]\n    public async Task HandleAsync_NotLoggedIn_Unauthorized(\n        string requirementName,\n        SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, userId: null);\n        var requirement = new NotificationStatusOperationsRequirement(requirementName);\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notificationStatus);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData(nameof(NotificationStatusOperations.Read))]\n    [BitAutoData(nameof(NotificationStatusOperations.Create))]\n    [BitAutoData(nameof(NotificationStatusOperations.Update))]\n    public async Task HandleAsync_ResourceEmpty_Unauthorized(\n        string requirementName,\n        SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n        var requirement = new NotificationStatusOperationsRequirement(requirementName);\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, null);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task HandleAsync_ReadRequirementUserNotMatching_Unauthorized(\n        SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n\n        var requirement = NotificationStatusOperations.Read;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notificationStatus);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task HandleAsync_ReadRequirement_Authorized(\n        SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notificationStatus.UserId);\n\n        var requirement = NotificationStatusOperations.Read;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notificationStatus);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task HandleAsync_CreateRequirementUserNotMatching_Unauthorized(\n        SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n\n        var requirement = NotificationStatusOperations.Create;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notificationStatus);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task HandleAsync_CreateRequirement_Authorized(\n        SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notificationStatus.UserId);\n\n        var requirement = NotificationStatusOperations.Create;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notificationStatus);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task HandleAsync_UpdateRequirementUserNotMatching_Unauthorized(\n        SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, Guid.NewGuid());\n\n        var requirement = NotificationStatusOperations.Update;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notificationStatus);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task HandleAsync_UpdateRequirement_Authorized(\n        SutProvider<NotificationStatusAuthorizationHandler> sutProvider,\n        NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal)\n    {\n        SetupUserPermission(sutProvider, notificationStatus.UserId);\n\n        var requirement = NotificationStatusOperations.Update;\n        var context = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },\n            claimsPrincipal, notificationStatus);\n\n        await sutProvider.Sut.HandleAsync(context);\n\n        Assert.True(context.HasSucceeded);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/AutoFixture/NotificationFixtures.cs",
    "content": "﻿#nullable enable\nusing AutoFixture;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.NotificationCenter.AutoFixture;\n\npublic class NotificationCustomization(bool global) : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<Notification>(composer =>\n        {\n            var postprocessComposer = composer.With(n => n.Id, Guid.NewGuid())\n                .With(n => n.Global, global);\n\n            postprocessComposer = global\n                ? postprocessComposer.Without(n => n.UserId)\n                : postprocessComposer.With(n => n.UserId, Guid.NewGuid());\n\n            return global\n                ? postprocessComposer.Without(n => n.OrganizationId)\n                : postprocessComposer.With(n => n.OrganizationId, Guid.NewGuid());\n        });\n    }\n}\n\npublic class NotificationCustomizeAttribute(bool global = true) : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new NotificationCustomization(global);\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusDetailsFixtures.cs",
    "content": "﻿#nullable enable\nusing AutoFixture;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.NotificationCenter.AutoFixture;\n\npublic class NotificationStatusDetailsCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<NotificationStatusDetails>(composer =>\n        {\n            return composer.With(n => n.Id, Guid.NewGuid())\n                .With(n => n.UserId, Guid.NewGuid())\n                .With(n => n.OrganizationId, Guid.NewGuid());\n        });\n    }\n}\n\npublic class NotificationStatusDetailsListCustomization(int count) : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        var customization = new NotificationStatusDetailsCustomization();\n        fixture.Customize<IEnumerable<NotificationStatusDetails>>(composer => composer.FromFactory(() =>\n        {\n            var notifications = new List<NotificationStatusDetails>();\n            for (var i = 0; i < count; i++)\n            {\n                customization.Customize(fixture);\n                var notificationStatusDetails = fixture.Create<NotificationStatusDetails>();\n                notifications.Add(notificationStatusDetails);\n            }\n\n            return notifications;\n        }));\n    }\n}\n\npublic class NotificationStatusDetailsCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new NotificationStatusDetailsCustomization();\n}\n\npublic class NotificationStatusDetailsListCustomizeAttribute(int count) : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new NotificationStatusDetailsListCustomization(count);\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusFixtures.cs",
    "content": "﻿#nullable enable\nusing AutoFixture;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.NotificationCenter.AutoFixture;\n\npublic class NotificationStatusCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<NotificationStatus>(composer => composer.With(ns => ns.NotificationId, Guid.NewGuid())\n            .With(ns => ns.UserId, Guid.NewGuid()));\n    }\n}\n\npublic class NotificationStatusCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new NotificationStatusCustomization();\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs",
    "content": "﻿#nullable enable\nusing System.Security.Claims;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.NotificationCenter.Commands;\n\n[SutProviderCustomize]\n[NotificationCustomize]\npublic class CreateNotificationCommandTest\n{\n    private static void Setup(SutProvider<CreateNotificationCommand> sutProvider,\n        Notification notification, bool authorized = false)\n    {\n        sutProvider.GetDependency<INotificationRepository>()\n            .CreateAsync(notification)\n            .Returns(notification);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notification,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationOperations.Create)))\n            .Returns(authorized ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_AuthorizationFailed_NotFoundException(\n        SutProvider<CreateNotificationCommand> sutProvider,\n        Notification notification)\n    {\n        Setup(sutProvider, notification, authorized: false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(notification));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_Authorized_NotificationCreated(\n        SutProvider<CreateNotificationCommand> sutProvider,\n        Notification notification)\n    {\n        Setup(sutProvider, notification, true);\n\n        var newNotification = await sutProvider.Sut.CreateAsync(notification);\n\n        Assert.Equal(notification, newNotification);\n        Assert.Equal(DateTime.UtcNow, notification.CreationDate, TimeSpan.FromMinutes(1));\n        Assert.Equal(notification.CreationDate, notification.RevisionDate);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushNotificationAsync(newNotification);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_Authorized_NotificationPushSkipped(\n        SutProvider<CreateNotificationCommand> sutProvider,\n        Notification notification)\n    {\n        Setup(sutProvider, notification, true);\n\n        var newNotification = await sutProvider.Sut.CreateAsync(notification, false);\n\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(newNotification);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs",
    "content": "﻿#nullable enable\nusing System.Security.Claims;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.NotificationCenter.Commands;\n\n[SutProviderCustomize]\n[NotificationCustomize]\n[NotificationStatusCustomize]\npublic class CreateNotificationStatusCommandTest\n{\n    private static void Setup(SutProvider<CreateNotificationStatusCommand> sutProvider,\n        Notification? notification, NotificationStatus notificationStatus,\n        bool authorizedNotification = false, bool authorizedCreate = false)\n    {\n        sutProvider.GetDependency<INotificationRepository>()\n            .GetByIdAsync(notificationStatus.NotificationId)\n            .Returns(notification);\n        sutProvider.GetDependency<INotificationStatusRepository>()\n            .CreateAsync(notificationStatus)\n            .Returns(notificationStatus);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notification ?? Arg.Any<Notification>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationOperations.Read)))\n            .Returns(authorizedNotification ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notificationStatus,\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationStatusOperations.Create)))\n            .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_NotificationNotFound_NotFoundException(\n        SutProvider<CreateNotificationStatusCommand> sutProvider,\n        NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notification: null, notificationStatus, true, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(notificationStatus));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_NotificationReadNotAuthorized_NotFoundException(\n        SutProvider<CreateNotificationStatusCommand> sutProvider,\n        Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notification, notificationStatus, authorizedNotification: false, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(notificationStatus));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_CreateNotAuthorized_NotFoundException(\n        SutProvider<CreateNotificationStatusCommand> sutProvider,\n        Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notification, notificationStatus, true, authorizedCreate: false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(notificationStatus));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateAsync_NotificationFoundAuthorized_NotificationStatusCreated(\n        SutProvider<CreateNotificationStatusCommand> sutProvider,\n        Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notification, notificationStatus, true, true);\n\n        var newNotificationStatus = await sutProvider.Sut.CreateAsync(notificationStatus);\n\n        Assert.Equal(notificationStatus, newNotificationStatus);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushNotificationStatusAsync(notification, notificationStatus);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs",
    "content": "﻿#nullable enable\nusing System.Security.Claims;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.NotificationCenter.Commands;\n\n[SutProviderCustomize]\n[NotificationCustomize]\n[NotificationStatusCustomize]\npublic class MarkNotificationDeletedCommandTest\n{\n    private static void Setup(SutProvider<MarkNotificationDeletedCommand> sutProvider,\n        Guid notificationId, Guid? userId, Notification? notification, NotificationStatus? notificationStatus,\n        bool authorizedNotification = false, bool authorizedCreate = false, bool authorizedUpdate = false)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<INotificationRepository>()\n            .GetByIdAsync(notificationId)\n            .Returns(notification);\n        sutProvider.GetDependency<INotificationStatusRepository>()\n            .GetByNotificationIdAndUserIdAsync(notificationId, userId ?? Arg.Any<Guid>())\n            .Returns(notificationStatus);\n        sutProvider.GetDependency<INotificationStatusRepository>()\n            .CreateAsync(Arg.Any<NotificationStatus>());\n        sutProvider.GetDependency<INotificationStatusRepository>()\n            .UpdateAsync(notificationStatus ?? Arg.Any<NotificationStatus>());\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notification ?? Arg.Any<Notification>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationOperations.Read)))\n            .Returns(authorizedNotification ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notificationStatus ?? Arg.Any<NotificationStatus>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationStatusOperations.Create)))\n            .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notificationStatus ?? Arg.Any<NotificationStatus>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationStatusOperations.Update)))\n            .Returns(authorizedUpdate ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<INotificationStatusRepository>().ClearReceivedCalls();\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkDeletedAsync_NotLoggedIn_NotFoundException(\n        SutProvider<MarkNotificationDeletedCommand> sutProvider,\n        Guid notificationId, Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkDeletedAsync_NotificationNotFound_NotFoundException(\n        SutProvider<MarkNotificationDeletedCommand> sutProvider,\n        Guid notificationId, Guid userId, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkDeletedAsync_ReadRequirementNotificationNotAuthorized_NotFoundException(\n        SutProvider<MarkNotificationDeletedCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus, authorizedNotification: false,\n            true, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkDeletedAsync_CreateRequirementNotAuthorized_NotFoundException(\n        SutProvider<MarkNotificationDeletedCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true,\n            authorizedCreate: false, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkDeletedAsync_UpdateRequirementNotAuthorized_NotFoundException(\n        SutProvider<MarkNotificationDeletedCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true,\n            authorizedUpdate: false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkDeletedAsync_NotificationStatusNotFoundCreateAuthorized_NotificationStatusCreated(\n        SutProvider<MarkNotificationDeletedCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true);\n        var expectedNotificationStatus = new NotificationStatus\n        {\n            NotificationId = notificationId,\n            UserId = userId,\n            ReadDate = null,\n            DeletedDate = DateTime.UtcNow\n        };\n\n        await sutProvider.Sut.MarkDeletedAsync(notificationId);\n\n        await sutProvider.GetDependency<INotificationStatusRepository>().Received(1)\n            .CreateAsync(Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(expectedNotificationStatus, ns)));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushNotificationStatusAsync(notification,\n                Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(expectedNotificationStatus, ns)));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkDeletedAsync_NotificationStatusFoundCreateAuthorized_NotificationStatusUpdated(\n        SutProvider<MarkNotificationDeletedCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, true);\n\n        await sutProvider.Sut.MarkDeletedAsync(notificationId);\n\n        await sutProvider.GetDependency<INotificationStatusRepository>().Received(1)\n            .UpdateAsync(Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(notificationStatus, ns)));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushNotificationStatusAsync(notification,\n                Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(notificationStatus, ns)));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    private static void AssertNotificationStatus(NotificationStatus expectedNotificationStatus,\n        NotificationStatus? actualNotificationStatus)\n    {\n        Assert.NotNull(actualNotificationStatus);\n        Assert.Equal(expectedNotificationStatus.NotificationId, actualNotificationStatus.NotificationId);\n        Assert.Equal(expectedNotificationStatus.UserId, actualNotificationStatus.UserId);\n        Assert.Equal(expectedNotificationStatus.ReadDate, actualNotificationStatus.ReadDate);\n        Assert.NotEqual(expectedNotificationStatus.DeletedDate, actualNotificationStatus.DeletedDate);\n        Assert.NotNull(actualNotificationStatus.DeletedDate);\n        Assert.Equal(DateTime.UtcNow, actualNotificationStatus.DeletedDate.Value, TimeSpan.FromMinutes(1));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs",
    "content": "﻿#nullable enable\nusing System.Security.Claims;\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.NotificationCenter.Commands;\n\n[SutProviderCustomize]\n[NotificationCustomize]\n[NotificationStatusCustomize]\npublic class MarkNotificationReadCommandTest\n{\n    private static void Setup(SutProvider<MarkNotificationReadCommand> sutProvider,\n        Guid notificationId, Guid? userId, Notification? notification, NotificationStatus? notificationStatus,\n        bool authorizedNotification = false, bool authorizedCreate = false, bool authorizedUpdate = false)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<INotificationRepository>()\n            .GetByIdAsync(notificationId)\n            .Returns(notification);\n        sutProvider.GetDependency<INotificationStatusRepository>()\n            .GetByNotificationIdAndUserIdAsync(notificationId, userId ?? Arg.Any<Guid>())\n            .Returns(notificationStatus);\n        sutProvider.GetDependency<INotificationStatusRepository>()\n            .CreateAsync(Arg.Any<NotificationStatus>());\n        sutProvider.GetDependency<INotificationStatusRepository>()\n            .UpdateAsync(notificationStatus ?? Arg.Any<NotificationStatus>());\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notification ?? Arg.Any<Notification>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationOperations.Read)))\n            .Returns(authorizedNotification ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notificationStatus ?? Arg.Any<NotificationStatus>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationStatusOperations.Create)))\n            .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notificationStatus ?? Arg.Any<NotificationStatus>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationStatusOperations.Update)))\n            .Returns(authorizedUpdate ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<INotificationStatusRepository>().ClearReceivedCalls();\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkReadAsync_NotLoggedIn_NotFoundException(\n        SutProvider<MarkNotificationReadCommand> sutProvider,\n        Guid notificationId, Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkReadAsync_NotificationNotFound_NotFoundException(\n        SutProvider<MarkNotificationReadCommand> sutProvider,\n        Guid notificationId, Guid userId, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkReadAsync_ReadRequirementNotificationNotAuthorized_NotFoundException(\n        SutProvider<MarkNotificationReadCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus, authorizedNotification: false,\n            true, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkReadAsync_CreateRequirementNotAuthorized_NotFoundException(\n        SutProvider<MarkNotificationReadCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true,\n            authorizedCreate: false, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkReadAsync_UpdateRequirementNotAuthorized_NotFoundException(\n        SutProvider<MarkNotificationReadCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true,\n            authorizedUpdate: false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkReadAsync_NotificationStatusNotFoundCreateAuthorized_NotificationStatusCreated(\n        SutProvider<MarkNotificationReadCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true);\n        var expectedNotificationStatus = new NotificationStatus\n        {\n            NotificationId = notificationId,\n            UserId = userId,\n            ReadDate = DateTime.UtcNow,\n            DeletedDate = null\n        };\n\n        await sutProvider.Sut.MarkReadAsync(notificationId);\n\n        await sutProvider.GetDependency<INotificationStatusRepository>().Received(1)\n            .CreateAsync(Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(expectedNotificationStatus, ns)));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushNotificationStatusAsync(notification,\n                Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(expectedNotificationStatus, ns)));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task MarkReadAsync_NotificationStatusFoundCreateAuthorized_NotificationStatusUpdated(\n        SutProvider<MarkNotificationReadCommand> sutProvider,\n        Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, true);\n\n        await sutProvider.Sut.MarkReadAsync(notificationId);\n\n        await sutProvider.GetDependency<INotificationStatusRepository>().Received(1)\n            .UpdateAsync(Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(notificationStatus, ns)));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushNotificationStatusAsync(notification,\n                Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(notificationStatus, ns)));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n    }\n\n    private static void AssertNotificationStatus(NotificationStatus expectedNotificationStatus,\n        NotificationStatus? actualNotificationStatus)\n    {\n        Assert.NotNull(actualNotificationStatus);\n        Assert.Equal(expectedNotificationStatus.NotificationId, actualNotificationStatus.NotificationId);\n        Assert.Equal(expectedNotificationStatus.UserId, actualNotificationStatus.UserId);\n        Assert.NotEqual(expectedNotificationStatus.ReadDate, actualNotificationStatus.ReadDate);\n        Assert.NotNull(actualNotificationStatus.ReadDate);\n        Assert.Equal(DateTime.UtcNow, actualNotificationStatus.ReadDate.Value, TimeSpan.FromMinutes(1));\n        Assert.Equal(expectedNotificationStatus.DeletedDate, actualNotificationStatus.DeletedDate);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs",
    "content": "﻿#nullable enable\nusing System.Security.Claims;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Commands;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Enums;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.NotificationCenter.Commands;\n\n[SutProviderCustomize]\n[NotificationCustomize]\npublic class UpdateNotificationCommandTest\n{\n    private static void Setup(SutProvider<UpdateNotificationCommand> sutProvider,\n        Guid notificationId, Notification? notification, bool authorized = false)\n    {\n        sutProvider.GetDependency<INotificationRepository>()\n            .GetByIdAsync(notificationId)\n            .Returns(notification);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notification ?? Arg.Any<Notification>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationOperations.Update)))\n            .Returns(authorized ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n\n        sutProvider.GetDependency<INotificationRepository>().ClearReceivedCalls();\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_NotificationNotFound_NotFoundException(\n        SutProvider<UpdateNotificationCommand> sutProvider,\n        Notification notification)\n    {\n        Setup(sutProvider, notification.Id, notification: null, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(notification));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_AuthorizationFailed_NotFoundException(\n        SutProvider<UpdateNotificationCommand> sutProvider,\n        Notification notification)\n    {\n        Setup(sutProvider, notification.Id, notification, authorized: false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(notification));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationAsync(Arg.Any<Notification>());\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateAsync_Authorized_NotificationCreated(\n        SutProvider<UpdateNotificationCommand> sutProvider,\n        Notification notification)\n    {\n        notification.Priority = Priority.Medium;\n        notification.ClientType = ClientType.Web;\n        notification.Title = \"Title\";\n        notification.Body = \"Body\";\n        notification.RevisionDate = DateTime.UtcNow.AddMinutes(-60);\n\n        Setup(sutProvider, notification.Id, notification, true);\n\n        var notificationToUpdate = CoreHelpers.CloneObject(notification);\n        notificationToUpdate.Priority = Priority.High;\n        notificationToUpdate.ClientType = ClientType.Mobile;\n        notificationToUpdate.Title = \"Updated Title\";\n        notificationToUpdate.Body = \"Updated Body\";\n        notificationToUpdate.RevisionDate = DateTime.UtcNow.AddMinutes(-30);\n\n        await sutProvider.Sut.UpdateAsync(notificationToUpdate);\n\n        await sutProvider.GetDependency<INotificationRepository>().Received(1)\n            .ReplaceAsync(Arg.Is<Notification>(n =>\n                // Not updated fields\n                n.Id == notificationToUpdate.Id && n.Global == notificationToUpdate.Global &&\n                n.UserId == notificationToUpdate.UserId && n.OrganizationId == notificationToUpdate.OrganizationId &&\n                n.CreationDate == notificationToUpdate.CreationDate &&\n                // Updated fields\n                n.Priority == notificationToUpdate.Priority && n.ClientType == notificationToUpdate.ClientType &&\n                n.Title == notificationToUpdate.Title && n.Body == notificationToUpdate.Body &&\n                DateTime.UtcNow - n.RevisionDate < TimeSpan.FromMinutes(1)));\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(1)\n            .PushNotificationAsync(notification);\n        await sutProvider.GetDependency<IPushNotificationService>()\n            .Received(0)\n            .PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/Queries/GetNotificationStatusDetailsForUserQueryTest.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Data;\nusing Bit.Core.NotificationCenter.Models.Filter;\nusing Bit.Core.NotificationCenter.Queries;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.NotificationCenter.Queries;\n\n[SutProviderCustomize]\n[NotificationStatusDetailsCustomize]\npublic class GetNotificationStatusDetailsForUserQueryTest\n{\n    private static void Setup(SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,\n        List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter statusFilter, Guid? userId,\n        PageOptions pageOptions, string? continuationToken)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<INotificationRepository>()\n            .GetByUserIdAndStatusAsync(userId.GetValueOrDefault(Guid.NewGuid()), Arg.Any<ClientType>(), statusFilter,\n                pageOptions)\n            .Returns(new PagedResult<NotificationStatusDetails>\n            {\n                Data = notificationsStatusDetails,\n                ContinuationToken = continuationToken\n            });\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByUserIdStatusFilterAsync_NotLoggedIn_NotFoundException(\n        SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,\n        List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter,\n        PageOptions pageOptions, string? continuationToken)\n    {\n        Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, userId: null, pageOptions,\n            continuationToken);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter, pageOptions));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByUserIdStatusFilterAsync_NotificationsFound_Returned(\n        SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,\n        List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter,\n        PageOptions pageOptions, string? continuationToken)\n    {\n        Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, Guid.NewGuid(), pageOptions,\n            continuationToken);\n\n        var actualNotificationsStatusDetailsPagedResult =\n            await sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter, pageOptions);\n\n        Assert.NotNull(actualNotificationsStatusDetailsPagedResult);\n        Assert.Equal(notificationsStatusDetails, actualNotificationsStatusDetailsPagedResult.Data);\n        Assert.Equal(continuationToken, actualNotificationsStatusDetailsPagedResult.ContinuationToken);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/NotificationCenter/Queries/GetNotificationStatusForUserTest.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Context;\nusing Bit.Core.Exceptions;\nusing Bit.Core.NotificationCenter.Queries;\nusing Bit.Core.NotificationCenter.Repositories;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.NotificationCenter.Queries;\n\nusing System.Security.Claims;\nusing Bit.Core.NotificationCenter.Authorization;\nusing Bit.Core.NotificationCenter.Entities;\nusing Microsoft.AspNetCore.Authorization;\nusing NSubstitute;\nusing Xunit;\n\n[SutProviderCustomize]\n[NotificationStatusCustomize]\npublic class GetNotificationStatusForUserQueryTest\n{\n    private static void Setup(SutProvider<GetNotificationStatusForUserQuery> sutProvider,\n        Guid notificationId, NotificationStatus? notificationStatus, Guid? userId, bool authorized = false)\n    {\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);\n        sutProvider.GetDependency<INotificationStatusRepository>()\n            .GetByNotificationIdAndUserIdAsync(notificationId, userId.GetValueOrDefault(Guid.NewGuid()))\n            .Returns(notificationStatus);\n        sutProvider.GetDependency<IAuthorizationService>()\n            .AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), notificationStatus ?? Arg.Any<NotificationStatus>(),\n                Arg.Is<IEnumerable<IAuthorizationRequirement>>(reqs =>\n                    reqs.Contains(NotificationStatusOperations.Read)))\n            .Returns(authorized ? AuthorizationResult.Success() : AuthorizationResult.Failed());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByUserIdStatusFilterAsync_UserNotLoggedIn_NotFoundException(\n        SutProvider<GetNotificationStatusForUserQuery> sutProvider,\n        Guid notificationId, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, notificationStatus, userId: null, true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByUserIdStatusFilterAsync_NotificationStatusNotFound_NotFoundException(\n        SutProvider<GetNotificationStatusForUserQuery> sutProvider,\n        Guid notificationId)\n    {\n        Setup(sutProvider, notificationId, notificationStatus: null, Guid.NewGuid(), true);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByUserIdStatusFilterAsync_AuthorizationFailed_NotFoundException(\n        SutProvider<GetNotificationStatusForUserQuery> sutProvider,\n        Guid notificationId, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, notificationStatus, Guid.NewGuid(), authorized: false);\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task GetByUserIdStatusFilterAsync_NotificationFoundAuthorized_Returned(\n        SutProvider<GetNotificationStatusForUserQuery> sutProvider,\n        Guid notificationId, NotificationStatus notificationStatus)\n    {\n        Setup(sutProvider, notificationId, notificationStatus, Guid.NewGuid(), true);\n\n        var actualNotificationStatus = await sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId);\n\n        Assert.Equal(notificationStatus, actualNotificationStatus);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Vault.AutoFixture;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationCollections;\n\n[SutProviderCustomize]\npublic class BulkAddCollectionAccessCommandTests\n{\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task AddAccessAsync_Success(SutProvider<BulkAddCollectionAccessCommand> sutProvider,\n        Organization org,\n        ICollection<Collection> collections,\n        ICollection<OrganizationUser> organizationUsers,\n        ICollection<Group> groups,\n        IEnumerable<CollectionUser> collectionUsers,\n        IEnumerable<CollectionGroup> collectionGroups)\n    {\n        SetCollectionsToSharedType(collections);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))\n            )\n            .Returns(organizationUsers);\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByManyIds(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))\n            )\n            .Returns(groups);\n\n        var userAccessSelections = ToAccessSelection(collectionUsers);\n        var groupAccessSelections = ToAccessSelection(collectionGroups);\n        await sutProvider.Sut.AddAccessAsync(collections,\n            userAccessSelections,\n            groupAccessSelections\n        );\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(\n            Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(userAccessSelections.Select(u => u.Id)))\n        );\n        await sutProvider.GetDependency<IGroupRepository>().Received().GetManyByManyIds(\n            Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(groupAccessSelections.Select(g => g.Id)))\n        );\n\n        await sutProvider.GetDependency<ICollectionRepository>().Received().CreateOrUpdateAccessForManyAsync(\n            org.Id,\n            Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collections.Select(c => c.Id))),\n            userAccessSelections,\n            groupAccessSelections);\n\n        await sutProvider.GetDependency<IEventService>().Received().LogCollectionEventsAsync(\n            Arg.Is<IEnumerable<(Collection, EventType, DateTime?)>>(\n                events => events.All(e =>\n                    collections.Contains(e.Item1) &&\n                    e.Item2 == EventType.Collection_Updated &&\n                    e.Item3.HasValue\n                )\n            )\n        );\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task ValidateRequestAsync_NoCollectionsProvided_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider)\n    {\n        var exception =\n            await Assert.ThrowsAsync<BadRequestException>(\n                () => sutProvider.Sut.AddAccessAsync(null, null, null));\n\n        Assert.Contains(\"No collections were provided.\", exception.Message);\n\n        await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIdsAsync(default);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);\n    }\n\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task ValidateRequestAsync_NoCollection_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,\n        IEnumerable<CollectionUser> collectionUsers,\n        IEnumerable<CollectionGroup> collectionGroups)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(Enumerable.Empty<Collection>().ToList(),\n            ToAccessSelection(collectionUsers),\n            ToAccessSelection(collectionGroups)\n        ));\n\n        Assert.Contains(\"No collections were provided.\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task ValidateRequestAsync_DifferentOrgs_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,\n        ICollection<Collection> collections,\n        IEnumerable<CollectionUser> collectionUsers,\n        IEnumerable<CollectionGroup> collectionGroups)\n    {\n        SetCollectionsToSharedType(collections);\n\n        collections.First().OrganizationId = Guid.NewGuid();\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,\n            ToAccessSelection(collectionUsers),\n            ToAccessSelection(collectionGroups)\n        ));\n\n        Assert.Contains(\"All collections must belong to the same organization.\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task ValidateRequestAsync_MissingUser_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,\n        IList<Collection> collections,\n        IList<OrganizationUser> organizationUsers,\n        IEnumerable<CollectionUser> collectionUsers,\n        IEnumerable<CollectionGroup> collectionGroups)\n    {\n        SetCollectionsToSharedType(collections);\n\n        organizationUsers.RemoveAt(0);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))\n            )\n            .Returns(organizationUsers);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,\n            ToAccessSelection(collectionUsers),\n            ToAccessSelection(collectionGroups)\n        ));\n\n        Assert.Contains(\"One or more users do not exist.\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(\n            Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))\n        );\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task ValidateRequestAsync_UserWrongOrg_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,\n        IList<Collection> collections,\n        IList<OrganizationUser> organizationUsers,\n        IEnumerable<CollectionUser> collectionUsers,\n        IEnumerable<CollectionGroup> collectionGroups)\n    {\n        SetCollectionsToSharedType(collections);\n\n        organizationUsers.First().OrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))\n            )\n            .Returns(organizationUsers);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,\n            ToAccessSelection(collectionUsers),\n            ToAccessSelection(collectionGroups)\n        ));\n\n        Assert.Contains(\"One or more users do not belong to the same organization as the collection being assigned.\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(\n            Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))\n        );\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task ValidateRequestAsync_MissingGroup_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,\n        IList<Collection> collections,\n        IList<OrganizationUser> organizationUsers,\n        IList<Group> groups,\n        IEnumerable<CollectionUser> collectionUsers,\n        IEnumerable<CollectionGroup> collectionGroups)\n    {\n        SetCollectionsToSharedType(collections);\n\n        groups.RemoveAt(0);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))\n            )\n            .Returns(organizationUsers);\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByManyIds(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))\n            )\n            .Returns(groups);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,\n            ToAccessSelection(collectionUsers),\n            ToAccessSelection(collectionGroups)\n        ));\n\n        Assert.Contains(\"One or more groups do not exist.\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(\n            Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))\n        );\n        await sutProvider.GetDependency<IGroupRepository>().Received().GetManyByManyIds(\n            Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))\n        );\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task ValidateRequestAsync_GroupWrongOrg_Failure(SutProvider<BulkAddCollectionAccessCommand> sutProvider,\n        IList<Collection> collections,\n        IList<OrganizationUser> organizationUsers,\n        IList<Group> groups,\n        IEnumerable<CollectionUser> collectionUsers,\n        IEnumerable<CollectionGroup> collectionGroups)\n    {\n        SetCollectionsToSharedType(collections);\n\n        groups.First().OrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyAsync(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))\n            )\n            .Returns(organizationUsers);\n\n        sutProvider.GetDependency<IGroupRepository>()\n            .GetManyByManyIds(\n                Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))\n            )\n            .Returns(groups);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,\n            ToAccessSelection(collectionUsers),\n            ToAccessSelection(collectionGroups)\n        ));\n\n        Assert.Contains(\"One or more groups do not belong to the same organization as the collection being assigned.\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>().Received().GetManyAsync(\n            Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId)))\n        );\n        await sutProvider.GetDependency<IGroupRepository>().Received().GetManyByManyIds(\n            Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionGroups.Select(u => u.GroupId)))\n        );\n    }\n\n    [Theory, BitAutoData, CollectionCustomization]\n    public async Task AddAccessAsync_WithDefaultUserCollectionType_ThrowsBadRequest(SutProvider<BulkAddCollectionAccessCommand> sutProvider,\n        IList<Collection> collections,\n        IEnumerable<CollectionUser> collectionUsers,\n        IEnumerable<CollectionGroup> collectionGroups)\n    {\n        // Arrange\n        collections.First().Type = CollectionType.DefaultUserCollection;\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.AddAccessAsync(collections,\n            ToAccessSelection(collectionUsers),\n            ToAccessSelection(collectionGroups)\n        ));\n\n        Assert.Contains(\"You cannot add access to collections with the type as DefaultUserCollection.\", exception.Message);\n\n        await sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs().CreateOrUpdateAccessForManyAsync(default, default, default, default);\n        await sutProvider.GetDependency<IEventService>().DidNotReceiveWithAnyArgs().LogCollectionEventsAsync(default);\n        await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetManyAsync(default);\n        await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyByManyIds(default);\n    }\n\n    private static void SetCollectionsToSharedType(IEnumerable<Collection> collections)\n    {\n        foreach (var collection in collections)\n        {\n            collection.Type = CollectionType.SharedCollection;\n        }\n    }\n\n    private static ICollection<CollectionAccessSelection> ToAccessSelection(IEnumerable<CollectionUser> collectionUsers)\n    {\n        return collectionUsers.Select(cu => new CollectionAccessSelection\n        {\n            Id = cu.OrganizationUserId,\n            Manage = cu.Manage,\n            HidePasswords = cu.HidePasswords,\n            ReadOnly = cu.ReadOnly\n        }).ToList();\n    }\n    private static ICollection<CollectionAccessSelection> ToAccessSelection(IEnumerable<CollectionGroup> collectionGroups)\n    {\n        return collectionGroups.Select(cg => new CollectionAccessSelection\n        {\n            Id = cg.GroupId,\n            Manage = cg.Manage,\n            HidePasswords = cg.HidePasswords,\n            ReadOnly = cg.ReadOnly\n        }).ToList();\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateCollectionCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationCollections;\n\n[SutProviderCustomize]\n[OrganizationCustomize]\npublic class CreateCollectionCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task CreateAsync_WithoutGroupsAndUsers_CreatesCollection(\n        Organization organization, Collection collection,\n        SutProvider<CreateCollectionCommand> sutProvider)\n    {\n        collection.Id = default;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        var utcNow = DateTime.UtcNow;\n\n        await sutProvider.Sut.CreateAsync(collection, null, null);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateAsync(\n                collection,\n                Arg.Is<List<CollectionAccessSelection>>(l => l == null),\n                Arg.Is<List<CollectionAccessSelection>>(l => l == null));\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogCollectionEventAsync(collection, EventType.Collection_Created);\n        Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1));\n        Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_WithGroupsAndUsers_CreatesCollectionWithGroupsAndUsers(\n        Organization organization, Collection collection,\n        [CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> groups,\n        IEnumerable<CollectionAccessSelection> users,\n        SutProvider<CreateCollectionCommand> sutProvider)\n    {\n        collection.Id = default;\n        organization.UseGroups = true;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        var utcNow = DateTime.UtcNow;\n\n        await sutProvider.Sut.CreateAsync(collection, groups, users);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateAsync(\n                collection,\n                Arg.Is<List<CollectionAccessSelection>>(l => l.Any(i => i.Manage == true)),\n                Arg.Any<List<CollectionAccessSelection>>());\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogCollectionEventAsync(collection, EventType.Collection_Created);\n        Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1));\n        Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_WithOrganizationUseGroupDisabled_CreatesCollectionWithoutGroups(\n        Organization organization, Collection collection,\n        [CollectionAccessSelectionCustomize] IEnumerable<CollectionAccessSelection> groups,\n        [CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> users,\n        SutProvider<CreateCollectionCommand> sutProvider)\n    {\n        collection.Id = default;\n        organization.UseGroups = false;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        var utcNow = DateTime.UtcNow;\n\n        await sutProvider.Sut.CreateAsync(collection, groups, users);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .CreateAsync(\n                collection,\n                Arg.Is<List<CollectionAccessSelection>>(l => l == null),\n                Arg.Is<List<CollectionAccessSelection>>(l => l.Any(i => i.Manage == true)));\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogCollectionEventAsync(collection, EventType.Collection_Created);\n        Assert.True(collection.CreationDate - utcNow < TimeSpan.FromSeconds(1));\n        Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_WithNonExistingOrganizationId_ThrowsBadRequest(\n        Collection collection, SutProvider<CreateCollectionCommand> sutProvider)\n    {\n        collection.Id = default;\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(collection));\n        Assert.Contains(\"Organization not found\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default, default, default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_WithoutManageAccess_ThrowsBadRequest(\n        Organization organization, Collection collection,\n        [CollectionAccessSelectionCustomize] IEnumerable<CollectionAccessSelection> users,\n        SutProvider<CreateCollectionCommand> sutProvider)\n    {\n        collection.Id = default;\n        organization.AllowAdminAccessToAllCollectionItems = false;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(collection, null, users));\n        Assert.Contains(\"At least one member or group must have can manage permission.\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default, default, default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_WithExceedsOrganizationMaxCollections_ThrowsBadRequest(\n        Organization organization, Collection collection,\n        [CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> users,\n        SutProvider<CreateCollectionCommand> sutProvider)\n    {\n        collection.Id = default;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetCountByOrganizationIdAsync(organization.Id)\n            .Returns(organization.MaxCollections.Value);\n\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(collection, null, users));\n        Assert.Equal($@\"You have reached the maximum number of collections ({organization.MaxCollections.Value}) for this organization.\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default, default, default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_WithInvalidManageAssociations_ThrowsBadRequest(\n        Organization organization, Collection collection, SutProvider<CreateCollectionCommand> sutProvider)\n    {\n        collection.Id = default;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var invalidGroups = new List<CollectionAccessSelection>\n        {\n            new() { Id = Guid.NewGuid(), Manage = true, ReadOnly = true }\n        };\n\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(collection, invalidGroups, null));\n        Assert.Contains(\"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default, default, default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateAsync_WithDefaultUserCollectionType_ThrowsBadRequest(\n        Organization organization, Collection collection, SutProvider<CreateCollectionCommand> sutProvider)\n    {\n        collection.Id = default;\n        collection.Type = CollectionType.DefaultUserCollection;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(collection));\n        Assert.Contains(\"You cannot create a collection with the type as DefaultUserCollection.\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .CreateAsync(default, default, default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommandTests.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationConnections;\n\n[SutProviderCustomize]\npublic class DeleteCollectionCommandTests\n{\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationCustomize]\n    public async Task DeleteAsync_DeletesCollection(Collection collection, SutProvider<DeleteCollectionCommand> sutProvider)\n    {\n        // Act\n        await sutProvider.Sut.DeleteAsync(collection);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>().Received().DeleteAsync(collection);\n        await sutProvider.GetDependency<IEventService>().Received().LogCollectionEventAsync(collection, EventType.Collection_Deleted, Arg.Any<DateTime>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationCustomize]\n    public async Task DeleteManyAsync_DeletesManyCollections(Collection collection, Collection collection2, SutProvider<DeleteCollectionCommand> sutProvider)\n    {\n        // Arrange\n        var collectionIds = new[] { collection.Id, collection2.Id };\n        collection.Type = collection2.Type = CollectionType.SharedCollection;\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(collectionIds)\n            .Returns(new List<Collection> { collection, collection2 });\n\n        // Act\n        await sutProvider.Sut.DeleteManyAsync(collectionIds);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>().Received()\n            .DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.ToArray().SequenceEqual(collectionIds)));\n\n        await sutProvider.GetDependency<IEventService>().Received().LogCollectionEventsAsync(\n            Arg.Is<IEnumerable<(Collection, EventType, DateTime?)>>(a =>\n            a.All(c => collectionIds.Contains(c.Item1.Id) && c.Item2 == EventType.Collection_Deleted)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationCustomize]\n    public async Task DeleteAsync_WithDefaultUserCollectionType_ThrowsBadRequest(Collection collection, SutProvider<DeleteCollectionCommand> sutProvider)\n    {\n        // Arrange\n        collection.Type = CollectionType.DefaultUserCollection;\n\n        // Act & Assert\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(collection));\n        Assert.Contains(\"You cannot delete a collection with the type as DefaultUserCollection.\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationCustomize]\n    public async Task DeleteManyAsync_WithDefaultUserCollectionType_ThrowsBadRequest(Collection collection, Collection collection2, SutProvider<DeleteCollectionCommand> sutProvider)\n    {\n        // Arrange\n        collection.Type = CollectionType.DefaultUserCollection;\n        collection2.Type = CollectionType.SharedCollection;\n        var collections = new List<Collection> { collection, collection2 };\n\n        // Act & Assert\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteManyAsync(collections));\n        Assert.Contains(\"You cannot delete collections with the type as DefaultUserCollection.\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventsAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationCustomize]\n    public async Task DeleteManyAsync_WithManyCollections_DeletesAllCollections(SutProvider<DeleteCollectionCommand> sutProvider)\n    {\n        // Arrange - Create 100 collections to test bulk delete performance\n        var collections = new List<Collection>();\n        var collectionIds = new List<Guid>();\n\n        for (int i = 0; i < 100; i++)\n        {\n            var collection = new Collection\n            {\n                Id = Guid.NewGuid(),\n                OrganizationId = Guid.NewGuid(),\n                Type = CollectionType.SharedCollection,\n                Name = $\"Collection {i}\"\n            };\n            collections.Add(collection);\n            collectionIds.Add(collection.Id);\n        }\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.SequenceEqual(collectionIds)))\n            .Returns(collections);\n\n        // Act\n        await sutProvider.Sut.DeleteManyAsync(collectionIds);\n\n        // Assert\n        await sutProvider.GetDependency<ICollectionRepository>().Received()\n            .DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.ToArray().SequenceEqual(collectionIds.ToArray())));\n\n        await sutProvider.GetDependency<IEventService>().Received().LogCollectionEventsAsync(\n            Arg.Is<IEnumerable<(Collection, EventType, DateTime?)>>(a =>\n                a.Count() == 100 &&\n                a.All(c => collectionIds.Contains(c.Item1.Id) && c.Item2 == EventType.Collection_Deleted)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    [OrganizationCustomize]\n    public async Task DeleteManyAsync_WhenEventLoggingFails_StillDeletesCollections(Collection collection, SutProvider<DeleteCollectionCommand> sutProvider)\n    {\n        // Arrange\n        var collectionIds = new[] { collection.Id };\n        collection.Type = CollectionType.SharedCollection;\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByManyIdsAsync(collectionIds)\n            .Returns(new List<Collection> { collection });\n\n        sutProvider.GetDependency<IEventService>()\n            .LogCollectionEventsAsync(Arg.Any<IEnumerable<(Collection, EventType, DateTime?)>>())\n            .Returns<Task>(_ => throw new Exception(\"Event logging failed\"));\n\n        // Act - Should not throw exception even though event logging fails\n        await sutProvider.Sut.DeleteManyAsync(collectionIds);\n\n        // Assert - Collections should still be deleted\n        await sutProvider.GetDependency<ICollectionRepository>().Received()\n            .DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.ToArray().SequenceEqual(collectionIds)));\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.OrganizationFeatures.OrganizationCollections;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationCollections;\n\n[SutProviderCustomize]\n[OrganizationCustomize]\npublic class UpdateCollectionCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WithoutGroupsAndUsers_ReplacesCollection(\n        Organization organization, Collection collection, SutProvider<UpdateCollectionCommand> sutProvider)\n    {\n        var creationDate = collection.CreationDate;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        var utcNow = DateTime.UtcNow;\n\n        await sutProvider.Sut.UpdateAsync(collection, null, null);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .ReplaceAsync(\n                collection,\n                Arg.Is<List<CollectionAccessSelection>>(l => l == null),\n                Arg.Is<List<CollectionAccessSelection>>(l => l == null));\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogCollectionEventAsync(collection, EventType.Collection_Updated);\n        Assert.Equal(collection.CreationDate, creationDate);\n        Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WithGroupsAndUsers_ReplacesCollectionWithGroupsAndUsers(\n        Organization organization, Collection collection,\n        [CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> groups,\n        IEnumerable<CollectionAccessSelection> users,\n        SutProvider<UpdateCollectionCommand> sutProvider)\n    {\n        var creationDate = collection.CreationDate;\n        organization.UseGroups = true;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        var utcNow = DateTime.UtcNow;\n\n        await sutProvider.Sut.UpdateAsync(collection, groups, users);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .ReplaceAsync(\n                collection,\n                Arg.Is<List<CollectionAccessSelection>>(l => l.Any(i => i.Manage == true)),\n                Arg.Any<List<CollectionAccessSelection>>());\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogCollectionEventAsync(collection, EventType.Collection_Updated);\n        Assert.Equal(collection.CreationDate, creationDate);\n        Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WithOrganizationUseGroupDisabled_ReplacesCollectionWithoutGroups(\n        Organization organization, Collection collection,\n        [CollectionAccessSelectionCustomize] IEnumerable<CollectionAccessSelection> groups,\n        [CollectionAccessSelectionCustomize(true)] IEnumerable<CollectionAccessSelection> users,\n        SutProvider<UpdateCollectionCommand> sutProvider)\n    {\n        var creationDate = collection.CreationDate;\n        organization.UseGroups = false;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        var utcNow = DateTime.UtcNow;\n\n        await sutProvider.Sut.UpdateAsync(collection, groups, users);\n\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .Received(1)\n            .ReplaceAsync(\n                collection,\n                Arg.Is<List<CollectionAccessSelection>>(l => l == null),\n                Arg.Is<List<CollectionAccessSelection>>(l => l.Any(i => i.Manage == true)));\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogCollectionEventAsync(collection, EventType.Collection_Updated);\n        Assert.Equal(collection.CreationDate, creationDate);\n        Assert.True(collection.RevisionDate - utcNow < TimeSpan.FromSeconds(1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WithNonExistingOrganizationId_ThrowsBadRequest(\n        Collection collection, SutProvider<UpdateCollectionCommand> sutProvider)\n    {\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateAsync(collection));\n        Assert.Contains(\"Organization not found\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default, default, default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WithoutManageAccess_ThrowsBadRequest(\n        Organization organization, Collection collection,\n        [CollectionAccessSelectionCustomize] IEnumerable<CollectionAccessSelection> users,\n        SutProvider<UpdateCollectionCommand> sutProvider)\n    {\n        organization.AllowAdminAccessToAllCollectionItems = false;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateAsync(collection, null, users));\n        Assert.Contains(\"At least one member or group must have can manage permission.\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default, default, default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WithInvalidManageAssociations_ThrowsBadRequest(\n        Organization organization, Collection collection, SutProvider<UpdateCollectionCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        var invalidGroups = new List<CollectionAccessSelection>\n        {\n            new() { Id = Guid.NewGuid(), Manage = true, HidePasswords = true }\n        };\n\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateAsync(collection, invalidGroups, null));\n        Assert.Contains(\"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default, default, default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateAsync_WithDefaultUserCollectionType_ThrowsBadRequest(\n        Organization organization, Collection collection, SutProvider<UpdateCollectionCommand> sutProvider)\n    {\n        collection.Type = CollectionType.DefaultUserCollection;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateAsync(collection));\n        Assert.Contains(\"You cannot edit a collection with the type as DefaultUserCollection.\", ex.Message);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default);\n        await sutProvider.GetDependency<ICollectionRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .ReplaceAsync(default, default, default);\n        await sutProvider.GetDependency<IEventService>()\n            .DidNotReceiveWithAnyArgs()\n            .LogCollectionEventAsync(default, default);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommandTestsBase.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing NSubstitute;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;\n\npublic abstract class CancelSponsorshipCommandTestsBase : FamiliesForEnterpriseTestsBase\n{\n    protected async Task AssertRemovedSponsoredPaymentAsync<T>(Organization sponsoredOrg,\nOrganizationSponsorship sponsorship, SutProvider<T> sutProvider)\n    {\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .RemoveOrganizationSponsorshipAsync(sponsoredOrg, sponsorship);\n        await sutProvider.GetDependency<IOrganizationRepository>().Received(1).UpsertAsync(sponsoredOrg);\n        if (sponsorship != null)\n        {\n            await sutProvider.GetDependency<IMailService>().Received(1)\n                .SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(sponsoredOrg.BillingEmailAddress(), sponsorship.ValidUntil.GetValueOrDefault());\n        }\n    }\n\n    protected async Task AssertDeletedSponsorshipAsync<T>(OrganizationSponsorship sponsorship,\n        SutProvider<T> sutProvider)\n    {\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)\n            .DeleteAsync(sponsorship);\n    }\n\n    protected static async Task AssertDidNotRemoveSponsorshipAsync<T>(SutProvider<T> sutProvider)\n    {\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    protected async Task AssertRemovedSponsorshipAsync<T>(OrganizationSponsorship sponsorship,\n        SutProvider<T> sutProvider)\n    {\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)\n            .DeleteAsync(sponsorship);\n    }\n\n    protected static async Task AssertDidNotRemoveSponsoredPaymentAsync<T>(SutProvider<T> sutProvider)\n    {\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .RemoveOrganizationSponsorshipAsync(default, default);\n        await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n        await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs()\n            .SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(default, default);\n    }\n\n    protected static async Task AssertDidNotDeleteSponsorshipAsync<T>(SutProvider<T> sutProvider)\n    {\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()\n            .DeleteAsync(default);\n    }\n\n    protected static async Task AssertDidNotUpdateSponsorshipAsync<T>(SutProvider<T> sutProvider)\n    {\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    protected static async Task AssertUpdatedSponsorshipAsync<T>(OrganizationSponsorship sponsorship,\n        SutProvider<T> sutProvider)\n    {\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1).UpsertAsync(sponsorship);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudRevokeSponsorshipCommandTests.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\nusing Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\n[SutProviderCustomize]\n[OrganizationSponsorshipCustomize]\npublic class CloudRevokeSponsorshipCommandTests : CancelSponsorshipCommandTestsBase\n{\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest(\n        SutProvider<CloudRevokeSponsorshipCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RevokeSponsorshipAsync(null));\n\n        Assert.Contains(\"You are not currently sponsoring an organization.\", exception.Message);\n        await AssertDidNotDeleteSponsorshipAsync(sutProvider);\n        await AssertDidNotUpdateSponsorshipAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeSponsorship_SponsorshipNotRedeemed_DeletesSponsorship(OrganizationSponsorship sponsorship,\n        SutProvider<CloudRevokeSponsorshipCommand> sutProvider)\n    {\n        sponsorship.SponsoredOrganizationId = null;\n\n        await sutProvider.Sut.RevokeSponsorshipAsync(sponsorship);\n        await AssertDeletedSponsorshipAsync(sponsorship, sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeSponsorship_SponsorshipRedeemed_MarksForDelete(OrganizationSponsorship sponsorship,\n        SutProvider<CloudRevokeSponsorshipCommand> sutProvider)\n    {\n        await sutProvider.Sut.RevokeSponsorshipAsync(sponsorship);\n\n        Assert.True(sponsorship.ToDelete);\n        await AssertUpdatedSponsorshipAsync(sponsorship, sutProvider);\n        await AssertDidNotDeleteSponsorshipAsync(sutProvider);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\n[SutProviderCustomize]\npublic class CloudSyncSponsorshipsCommandTests : FamiliesForEnterpriseTestsBase\n{\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_SponsoringOrgNotFound_ThrowsBadRequest(\n        IEnumerable<OrganizationSponsorshipData> sponsorshipsData,\n        SutProvider<CloudSyncSponsorshipsCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SyncOrganization(null, sponsorshipsData));\n\n        Assert.Contains(\"Failed to sync sponsorship - missing organization.\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_NoSponsorships_EarlyReturn(\n        Organization organization,\n        SutProvider<CloudSyncSponsorshipsCommand> sutProvider)\n    {\n        var result = await sutProvider.Sut.SyncOrganization(organization, Enumerable.Empty<OrganizationSponsorshipData>());\n\n        Assert.Empty(result.Item1.SponsorshipsBatch);\n        Assert.Empty(result.Item2);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(NonEnterprisePlanTypes))]\n    public async Task SyncOrganization_BadSponsoringOrgPlan_NoSync(\n        PlanType planType,\n        Organization organization, IEnumerable<OrganizationSponsorshipData> sponsorshipsData,\n        SutProvider<CloudSyncSponsorshipsCommand> sutProvider)\n    {\n        organization.PlanType = planType;\n\n        await sutProvider.Sut.SyncOrganization(organization, sponsorshipsData);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_Success_RecordsEvent(Organization organization,\n        SutProvider<CloudSyncSponsorshipsCommand> sutProvider)\n    {\n        await sutProvider.Sut.SyncOrganization(organization, Array.Empty<OrganizationSponsorshipData>());\n\n        await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationEventAsync(organization, EventType.Organization_SponsorshipsSynced, Arg.Any<DateTime?>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_OneExisting_OneNew_Success(SutProvider<CloudSyncSponsorshipsCommand> sutProvider,\n        Organization sponsoringOrganization, OrganizationSponsorship existingSponsorship, OrganizationSponsorship newSponsorship)\n    {\n        // Arrange\n        sponsoringOrganization.Enabled = true;\n        sponsoringOrganization.PlanType = PlanType.EnterpriseAnnually;\n\n        existingSponsorship.ToDelete = false;\n        newSponsorship.ToDelete = false;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id)\n            .Returns(new List<OrganizationSponsorship>\n            {\n                existingSponsorship,\n            });\n\n        // Act\n        var (syncData, toEmailSponsorships) = await sutProvider.Sut.SyncOrganization(sponsoringOrganization, new[]\n        {\n            new OrganizationSponsorshipData(existingSponsorship),\n            new OrganizationSponsorshipData(newSponsorship),\n        });\n\n        // Assert\n        // Should have updated the cloud copy for each item given\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .Received(1)\n            .UpsertManyAsync(Arg.Is<IEnumerable<OrganizationSponsorship>>(sponsorships => sponsorships.Count() == 2));\n\n        // Neither were marked as delete, should not have deleted\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n\n        // Only one sponsorship was new so it should only send one\n        Assert.Single(toEmailSponsorships);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_TwoToDelete_OneCanDelete_Success(SutProvider<CloudSyncSponsorshipsCommand> sutProvider,\n        Organization sponsoringOrganization, OrganizationSponsorship canDeleteSponsorship, OrganizationSponsorship cannotDeleteSponsorship)\n    {\n        // Arrange\n        sponsoringOrganization.PlanType = PlanType.EnterpriseAnnually;\n\n        canDeleteSponsorship.ToDelete = true;\n        canDeleteSponsorship.SponsoredOrganizationId = null;\n\n        cannotDeleteSponsorship.ToDelete = true;\n        cannotDeleteSponsorship.SponsoredOrganizationId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id)\n            .Returns(new List<OrganizationSponsorship>\n            {\n                canDeleteSponsorship,\n                cannotDeleteSponsorship,\n            });\n\n        // Act\n        var (syncData, toEmailSponsorships) = await sutProvider.Sut.SyncOrganization(sponsoringOrganization, new[]\n        {\n            new OrganizationSponsorshipData(canDeleteSponsorship),\n            new OrganizationSponsorshipData(cannotDeleteSponsorship),\n        });\n\n        // Assert\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .Received(1)\n            .UpsertManyAsync(Arg.Is<IEnumerable<OrganizationSponsorship>>(sponsorships => sponsorships.Count() == 2));\n\n        // Deletes the sponsorship that had delete requested and is not sponsoring an org\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Is<IEnumerable<Guid>>(toDeleteIds =>\n                toDeleteIds.Count() == 1 && toDeleteIds.ElementAt(0) == canDeleteSponsorship.Id));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_BadData_DoesNotSave(SutProvider<CloudSyncSponsorshipsCommand> sutProvider,\n        Organization sponsoringOrganization, OrganizationSponsorship badOrganizationSponsorship)\n    {\n        sponsoringOrganization.PlanType = PlanType.EnterpriseAnnually;\n\n        badOrganizationSponsorship.ToDelete = true;\n        badOrganizationSponsorship.LastSyncDate = null;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id)\n            .Returns(new List<OrganizationSponsorship>());\n\n        var (syncData, toEmailSponsorships) = await sutProvider.Sut.SyncOrganization(sponsoringOrganization, new[]\n        {\n            new OrganizationSponsorshipData(badOrganizationSponsorship),\n        });\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_OrgDisabledForFourMonths_DoesNotSave(SutProvider<CloudSyncSponsorshipsCommand> sutProvider,\n        Organization sponsoringOrganization, OrganizationSponsorship organizationSponsorship)\n    {\n        sponsoringOrganization.PlanType = PlanType.EnterpriseAnnually;\n        sponsoringOrganization.Enabled = false;\n        sponsoringOrganization.ExpirationDate = DateTime.UtcNow.AddDays(-120);\n\n        organizationSponsorship.ToDelete = false;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id)\n            .Returns(new List<OrganizationSponsorship>());\n\n        var (syncData, toEmailSponsorships) = await sutProvider.Sut.SyncOrganization(sponsoringOrganization, new[]\n        {\n            new OrganizationSponsorshipData(organizationSponsorship),\n        });\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/OrganizationSponsorshipRenewCommandTests.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\nusing Bit.Core.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\n[SutProviderCustomize]\npublic class OrganizationSponsorshipRenewCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateExpirationDate_UpdatesValidUntil(OrganizationSponsorship sponsorship, DateTime expireDate,\n        SutProvider<OrganizationSponsorshipRenewCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>().GetBySponsoredOrganizationIdAsync(sponsorship.SponsoredOrganizationId.Value).Returns(sponsorship);\n\n        await sutProvider.Sut.UpdateExpirationDateAsync(sponsorship.SponsoredOrganizationId.Value, expireDate);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)\n            .UpsertAsync(sponsorship);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/RemoveSponsorshipCommandTests.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\nusing Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;\n\n[SutProviderCustomize]\n[OrganizationSponsorshipCustomize]\npublic class RemoveSponsorshipCommandTests : CancelSponsorshipCommandTestsBase\n{\n    [Theory]\n    [BitAutoData]\n    public async Task RemoveSponsorship_SponsoredOrgNull_ThrowsBadRequest(OrganizationSponsorship sponsorship,\n        SutProvider<RemoveSponsorshipCommand> sutProvider)\n    {\n        sponsorship.SponsoredOrganizationId = null;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RemoveSponsorshipAsync(sponsorship));\n\n        Assert.Contains(\"The requested organization is not currently being sponsored.\", exception.Message);\n        Assert.False(sponsorship.ToDelete);\n        await AssertDidNotDeleteSponsorshipAsync(sutProvider);\n        await AssertDidNotUpdateSponsorshipAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RemoveSponsorship_SponsorshipNotFound_ThrowsBadRequest(SutProvider<RemoveSponsorshipCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RemoveSponsorshipAsync(null));\n\n        Assert.Contains(\"The requested organization is not currently being sponsored.\", exception.Message);\n        await AssertDidNotDeleteSponsorshipAsync(sutProvider);\n        await AssertDidNotUpdateSponsorshipAsync(sutProvider);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;\n\n[SutProviderCustomize]\n[OrganizationSponsorshipCustomize]\npublic class SendSponsorshipOfferCommandTests : FamiliesForEnterpriseTestsBase\n{\n    [Theory]\n    [BitAutoData]\n    public async Task SendSponsorshipOffer_SendSponsorshipOfferAsync_ExistingAccount_Success(OrganizationSponsorship sponsorship, string sponsoringOrgName, User user, SutProvider<SendSponsorshipOfferCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(sponsorship.OfferedToEmail).Returns(user);\n\n        await sutProvider.Sut.SendSponsorshipOfferAsync(sponsorship, sponsoringOrgName);\n\n        await sutProvider.GetDependency<IMailService>().Received(1).SendFamiliesForEnterpriseOfferEmailAsync(sponsoringOrgName, sponsorship.OfferedToEmail, true, Arg.Any<string>());\n    }\n\n\n    [Theory]\n    [BitAutoData]\n    public async Task SendSponsorshipOffer_SendSponsorshipOfferAsync_NewAccount_Success(OrganizationSponsorship sponsorship, string sponsoringOrgName, SutProvider<SendSponsorshipOfferCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IUserRepository>().GetByEmailAsync(sponsorship.OfferedToEmail).Returns((User)null);\n\n        await sutProvider.Sut.SendSponsorshipOfferAsync(sponsorship, sponsoringOrgName);\n\n        await sutProvider.GetDependency<IMailService>().Received(1).SendFamiliesForEnterpriseOfferEmailAsync(sponsoringOrgName, sponsorship.OfferedToEmail, false, Arg.Any<string>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ResendSponsorshipOffer_SponsoringOrgNotFound_ThrowsBadRequest(\n        OrganizationUser orgUser, OrganizationSponsorship sponsorship,\n        SutProvider<SendSponsorshipOfferCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SendSponsorshipOfferAsync(null, orgUser, sponsorship));\n\n        Assert.Contains(\"Cannot find the requested sponsoring organization.\", exception.Message);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ResendSponsorshipOffer_SponsoringOrgUserNotFound_ThrowsBadRequest(Organization org,\n        OrganizationSponsorship sponsorship, SutProvider<SendSponsorshipOfferCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SendSponsorshipOfferAsync(org, null, sponsorship));\n\n        Assert.Contains(\"Only confirmed users can sponsor other organizations.\", exception.Message);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))]\n    public async Task ResendSponsorshipOffer_SponsoringOrgUserNotConfirmed_ThrowsBadRequest(OrganizationUserStatusType status,\n        Organization org, OrganizationUser orgUser, OrganizationSponsorship sponsorship,\n        SutProvider<SendSponsorshipOfferCommand> sutProvider)\n    {\n        orgUser.Status = status;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SendSponsorshipOfferAsync(org, orgUser, sponsorship));\n\n        Assert.Contains(\"Only confirmed users can sponsor other organizations.\", exception.Message);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ResendSponsorshipOffer_SponsorshipNotFound_ThrowsBadRequest(Organization org,\n        OrganizationUser orgUser,\n        SutProvider<SendSponsorshipOfferCommand> sutProvider)\n    {\n        orgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SendSponsorshipOfferAsync(org, orgUser, null));\n\n        Assert.Contains(\"Cannot find an outstanding sponsorship offer for this organization.\", exception.Message);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ResendSponsorshipOffer_NoOfferToEmail_ThrowsBadRequest(Organization org,\n        OrganizationUser orgUser, OrganizationSponsorship sponsorship,\n        SutProvider<SendSponsorshipOfferCommand> sutProvider)\n    {\n        orgUser.Status = OrganizationUserStatusType.Confirmed;\n        sponsorship.OfferedToEmail = null;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SendSponsorshipOfferAsync(org, orgUser, sponsorship));\n\n        Assert.Contains(\"Cannot find an outstanding sponsorship offer for this organization.\", exception.Message);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Stripe;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\n[SutProviderCustomize]\n[OrganizationSponsorshipCustomize]\npublic class SetUpSponsorshipCommandTests : FamiliesForEnterpriseTestsBase\n{\n    [Theory]\n    [BitAutoData]\n    public async Task SetUpSponsorship_SponsorshipNotFound_ThrowsBadRequest(Organization org,\n        SutProvider<SetUpSponsorshipCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SetUpSponsorshipAsync(null, org));\n\n        Assert.Contains(\"No unredeemed sponsorship offer exists for you.\", exception.Message);\n        await AssertDidNotSetUpAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SetUpSponsorship_OrgAlreadySponsored_ThrowsBadRequest(Organization org,\n        OrganizationSponsorship sponsorship, OrganizationSponsorship existingSponsorship,\n        SutProvider<SetUpSponsorshipCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(org.Id).Returns(existingSponsorship);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org));\n\n        Assert.Contains(\"Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first.\", exception.Message);\n        await AssertDidNotSetUpAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(FamiliesPlanTypes))]\n    public async Task SetUpSponsorship_TooLongSinceLastSync_ThrowsBadRequest(PlanType planType, Organization org,\n        OrganizationSponsorship sponsorship,\n        SutProvider<SetUpSponsorshipCommand> sutProvider)\n    {\n        org.PlanType = planType;\n        sponsorship.LastSyncDate = DateTime.UtcNow.AddDays(-365);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org));\n\n        Assert.Contains(\"This sponsorship offer is more than 6 months old and has expired.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .Received(1)\n            .DeleteAsync(sponsorship);\n        await AssertDidNotSetUpAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(NonFamiliesPlanTypes))]\n    public async Task SetUpSponsorship_OrgNotFamilies_ThrowsBadRequest(PlanType planType,\n        OrganizationSponsorship sponsorship, Organization org,\n        SutProvider<SetUpSponsorshipCommand> sutProvider)\n    {\n        org.PlanType = planType;\n        sponsorship.LastSyncDate = DateTime.UtcNow;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org));\n\n        Assert.Contains(\"Can only redeem sponsorship offer on families organizations.\", exception.Message);\n        await AssertDidNotSetUpAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(FamiliesPlanTypes))]\n    public async Task SetUpSponsorship_FeatureFlagOff_UsesSponsorOrganizationAsync(PlanType planType,\n        OrganizationSponsorship sponsorship, Organization org,\n        SutProvider<SetUpSponsorshipCommand> sutProvider)\n    {\n        org.PlanType = planType;\n        sponsorship.LastSyncDate = DateTime.UtcNow;\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(false);\n\n        await sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org);\n\n        await sutProvider.GetDependency<IStripePaymentService>()\n            .Received(1)\n            .SponsorOrganizationAsync(org, sponsorship);\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .Run(default, default);\n        await AssertDidSetUpAsync(sutProvider, sponsorship, org);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(FamiliesPlanTypes))]\n    public async Task SetUpSponsorship_FeatureFlagOn_UsesUpdateOrganizationSubscriptionCommand(PlanType planType,\n        OrganizationSponsorship sponsorship, Organization org,\n        SutProvider<SetUpSponsorshipCommand> sutProvider)\n    {\n        org.PlanType = planType;\n        sponsorship.LastSyncDate = DateTime.UtcNow;\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        var existingPlan = MockPlans.Get(planType);\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(planType)\n            .Returns(existingPlan);\n\n        var expectedPeriodEnd = DateTime.UtcNow.AddYears(1);\n        var subscription = new Subscription\n        {\n            Items = new StripeList<SubscriptionItem>\n            {\n                Data = [new SubscriptionItem { CurrentPeriodEnd = expectedPeriodEnd }]\n            }\n        };\n        BillingCommandResult<Subscription> successResult = subscription;\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(org, Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(successResult);\n\n        await sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org);\n\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Received(1)\n            .Run(org, Arg.Any<OrganizationSubscriptionChangeSet>());\n        await sutProvider.GetDependency<IStripePaymentService>()\n            .DidNotReceiveWithAnyArgs()\n            .SponsorOrganizationAsync(default, default);\n        Assert.Equal(expectedPeriodEnd, org.ExpirationDate);\n        Assert.Equal(expectedPeriodEnd, sponsorship.ValidUntil);\n        await AssertDidSetUpAsync(sutProvider, sponsorship, org);\n    }\n\n    private static async Task AssertDidNotSetUpAsync(SutProvider<SetUpSponsorshipCommand> sutProvider)\n    {\n        await sutProvider.GetDependency<IStripePaymentService>()\n            .DidNotReceiveWithAnyArgs()\n            .SponsorOrganizationAsync(default, default);\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .Run(default, default);\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertAsync(default);\n    }\n\n    private static async Task AssertDidSetUpAsync(SutProvider<SetUpSponsorshipCommand> sutProvider,\n        OrganizationSponsorship sponsorship, Organization org)\n    {\n        await sutProvider.GetDependency<IOrganizationRepository>()\n            .Received(1)\n            .UpsertAsync(org);\n        Assert.Equal(org.Id, sponsorship.SponsoredOrganizationId);\n        Assert.Null(sponsorship.OfferedToEmail);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .Received(1)\n            .UpsertAsync(sponsorship);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommandTests.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Models.Business.Tokenables;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\nusing Bit.Core.Repositories;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\n[SutProviderCustomize]\npublic class ValidateRedemptionTokenCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateRedemptionTokenAsync_CannotUnprotect_ReturnsFalse(SutProvider<ValidateRedemptionTokenCommand> sutProvider,\n        string encryptedString)\n    {\n        sutProvider\n            .GetDependency<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>()\n            .TryUnprotect(encryptedString, out _)\n            .Returns(call =>\n            {\n                call[1] = null;\n                return false;\n            });\n\n        var (valid, sponsorship) = await sutProvider.Sut.ValidateRedemptionTokenAsync(encryptedString, null);\n        Assert.False(valid);\n        Assert.Null(sponsorship);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateRedemptionTokenAsync_NoSponsorship_ReturnsFalse(SutProvider<ValidateRedemptionTokenCommand> sutProvider,\n        string encryptedString, OrganizationSponsorshipOfferTokenable tokenable)\n    {\n        sutProvider\n            .GetDependency<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>()\n            .TryUnprotect(encryptedString, out _)\n            .Returns(call =>\n            {\n                call[1] = tokenable;\n                return true;\n            });\n\n        var (valid, sponsorship) = await sutProvider.Sut.ValidateRedemptionTokenAsync(encryptedString, \"test@email.com\");\n        Assert.False(valid);\n        Assert.Null(sponsorship);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ValidateRedemptionTokenAsync_ValidSponsorship_ReturnsFalse(SutProvider<ValidateRedemptionTokenCommand> sutProvider,\n        string encryptedString, string email, OrganizationSponsorshipOfferTokenable tokenable)\n    {\n        tokenable.Email = email;\n\n        sutProvider\n            .GetDependency<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>()\n            .TryUnprotect(encryptedString, out _)\n            .Returns(call =>\n            {\n                call[1] = tokenable;\n                return true;\n            });\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetByIdAsync(tokenable.Id)\n            .Returns(new OrganizationSponsorship\n            {\n                Id = tokenable.Id,\n                PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,\n                OfferedToEmail = email\n            });\n\n        var (valid, sponsorship) = await sutProvider.Sut\n            .ValidateRedemptionTokenAsync(encryptedString, email);\n\n        Assert.True(valid);\n        Assert.NotNull(sponsorship);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Entities;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;\n\n[SutProviderCustomize]\n[OrganizationSponsorshipCustomize]\npublic class ValidateSponsorshipCommandTests : CancelSponsorshipCommandTestsBase\n{\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitAutoData]\n    public async Task ValidateSponsorshipAsync_NoSponsoredOrg_EarlyReturn(Guid sponsoredOrgId,\n        SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrgId).Returns((Organization)null);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrgId);\n\n        Assert.False(result);\n        await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider);\n        await AssertDidNotDeleteSponsorshipAsync(sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitAutoData]\n    public async Task ValidateSponsorshipAsync_NoExistingSponsorship_UpdatesStripePlan(Organization sponsoredOrg,\n        SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, null, sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitAutoData]\n    public async Task ValidateSponsorshipAsync_SponsoringOrgDefault_UpdatesStripePlan(Organization sponsoredOrg,\n        OrganizationSponsorship existingSponsorship, SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        existingSponsorship.SponsoringOrganizationId = default;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider);\n        await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitAutoData]\n    public async Task ValidateSponsorshipAsync_SponsoringOrgUserDefault_UpdatesStripePlan(Organization sponsoredOrg,\n        OrganizationSponsorship existingSponsorship, SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        existingSponsorship.SponsoringOrganizationUserId = default;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider);\n        await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitAutoData]\n    public async Task ValidateSponsorshipAsync_SponsorshipTypeNull_UpdatesStripePlan(Organization sponsoredOrg,\n        OrganizationSponsorship existingSponsorship, SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        existingSponsorship.PlanSponsorshipType = null;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider);\n        await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitAutoData]\n    public async Task ValidateSponsorshipAsync_SponsoringOrgNotFound_UpdatesStripePlan(Organization sponsoredOrg,\n        OrganizationSponsorship existingSponsorship, SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider);\n        await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitMemberAutoData(nameof(NonEnterprisePlanTypes))]\n    public async Task ValidateSponsorshipAsync_SponsoringOrgNotEnterprise_UpdatesStripePlan(PlanType planType,\n        Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg,\n        SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = planType;\n        existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider);\n        await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitMemberAutoData(nameof(EnterprisePlanTypes))]\n    public async Task ValidateSponsorshipAsync_SponsoringOrgDisabledLongerThanGrace_UpdatesStripePlan(PlanType planType,\n        Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg,\n        SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = planType;\n        sponsoringOrg.Enabled = false;\n        sponsoringOrg.ExpirationDate = DateTime.UtcNow.AddDays(-100);\n        existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider);\n        await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [OrganizationSponsorshipCustomize(ToDelete = true)]\n    [BitMemberAutoData(nameof(EnterprisePlanTypes))]\n    public async Task ValidateSponsorshipAsync_ToDeleteSponsorship_IsInvalid(PlanType planType,\n        Organization sponsoredOrg, OrganizationSponsorship sponsorship, Organization sponsoringOrg,\n        SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = planType;\n        sponsoringOrg.Enabled = true;\n        sponsorship.SponsoringOrganizationId = sponsoringOrg.Id;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(sponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, sponsorship, sutProvider);\n        await AssertDeletedSponsorshipAsync(sponsorship, sutProvider);\n    }\n\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitMemberAutoData(nameof(EnterprisePlanTypes))]\n    public async Task ValidateSponsorshipAsync_SponsoringOrgDisabledUnknownTime_UpdatesStripePlan(PlanType planType,\n        Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg,\n        SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = planType;\n        sponsoringOrg.Enabled = false;\n        sponsoringOrg.ExpirationDate = null;\n        existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider);\n        await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitMemberAutoData(nameof(EnterprisePlanTypes))]\n    public async Task ValidateSponsorshipAsync_SponsoringOrgDisabledLessThanGrace_Valid(PlanType planType,\n        Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg,\n        SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = planType;\n        sponsoringOrg.Enabled = true;\n        sponsoringOrg.ExpirationDate = DateTime.UtcNow.AddDays(-1);\n        existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.True(result);\n\n        await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider);\n        await AssertDidNotRemoveSponsorshipAsync(sutProvider);\n    }\n\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitMemberAutoData(nameof(EnterprisePlanTypes))]\n    public async Task ValidateSponsorshipAsync_Valid(PlanType planType,\n        Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg,\n        SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = planType;\n        sponsoringOrg.Enabled = true;\n        existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.True(result);\n\n        await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider);\n        await AssertDidNotDeleteSponsorshipAsync(sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitMemberAutoData(nameof(EnterprisePlanTypes))]\n    public async Task ValidateSponsorshipAsync_AdminInitiatedButUseAdminSponsoredFamiliesFalse_IsInvalid(\n        PlanType planType, Organization sponsoredOrg, OrganizationSponsorship existingSponsorship,\n        Organization sponsoringOrg, SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = planType;\n        sponsoringOrg.UseAdminSponsoredFamilies = false;\n        existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id;\n        existingSponsorship.IsAdminInitiated = true;\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.False(result);\n        await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider);\n        await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider);\n    }\n\n    [Theory(Skip = \"Temporarily disabled\")]\n    [BitMemberAutoData(nameof(EnterprisePlanTypes))]\n    public async Task ValidateSponsorshipAsync_AdminInitiatedAndUseAdminSponsoredFamiliesTrue_ContinuesValidation(\n        PlanType planType, Organization sponsoredOrg, OrganizationSponsorship existingSponsorship,\n        Organization sponsoringOrg, SutProvider<ValidateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = planType;\n        sponsoringOrg.UseAdminSponsoredFamilies = true;\n        existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id;\n        existingSponsorship.IsAdminInitiated = true;\n        existingSponsorship.ToDelete = false;\n        existingSponsorship.LastSyncDate = null; // Not a self-hosted sponsorship\n\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg);\n\n        var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id);\n\n        Assert.True(result);\n        await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider);\n        await AssertDidNotDeleteSponsorshipAsync(sutProvider);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;\n\n[SutProviderCustomize]\npublic class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase\n{\n    private bool SponsorshipValidator(OrganizationSponsorship sponsorship, OrganizationSponsorship expectedSponsorship)\n    {\n        try\n        {\n            AssertHelper.AssertPropertyEqual(sponsorship, expectedSponsorship, nameof(OrganizationSponsorship.Id));\n            return true;\n        }\n        catch\n        {\n            return false;\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateSponsorship_OfferedToNotFound_ThrowsBadRequest(OrganizationUser orgUser, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).ReturnsNull();\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, false, null));\n\n        Assert.Contains(\"Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(null!);\n    }\n\n    [Theory, BitAutoData]\n    public async Task CreateSponsorship_OfferedToSelf_ThrowsBadRequest(OrganizationUser orgUser, string sponsoredEmail, User user, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        user.Email = sponsoredEmail;\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, default, false, null));\n\n        Assert.Contains(\"Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(null!);\n    }\n\n    [Theory, BitMemberAutoData(nameof(NonEnterprisePlanTypes))]\n    public async Task CreateSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan,\n        Organization org, OrganizationUser orgUser, User user, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        org.PlanType = sponsoringOrgPlan;\n        orgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, false, null));\n\n        Assert.Contains(\"Specified Organization cannot sponsor other organizations.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(null!);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))]\n    public async Task CreateSponsorship_BadSponsoringUserStatus_ThrowsBadRequest(\n        OrganizationUserStatusType statusType, Organization org, OrganizationUser orgUser, User user,\n        SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.Status = statusType;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, false, null));\n\n        Assert.Contains(\"Only confirmed users can sponsor other organizations.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(null!);\n    }\n\n    [Theory]\n    [OrganizationSponsorshipCustomize]\n    [BitAutoData]\n    public async Task CreateSponsorship_AlreadySponsoring_Throws(Organization org,\n        OrganizationUser orgUser, User user, OrganizationSponsorship sponsorship,\n        SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        org.PlanType = PlanType.EnterpriseAnnually;\n        orgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetBySponsoringOrganizationUserIdAsync(orgUser.Id).Returns(sponsorship);\n\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(orgUser.UserId.Value);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, sponsorship.PlanSponsorshipType!.Value, null, null, false, null));\n\n        Assert.Contains(\"Can only sponsor one organization per Organization User.\", exception.Message);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()\n            .CreateAsync(null!);\n    }\n\n    public static readonly OrganizationUserStatusType[] UnconfirmedOrganizationUsersStatuses = Enum\n        .GetValues<OrganizationUserStatusType>()\n        .Where(x => x != OrganizationUserStatusType.Confirmed)\n        .ToArray();\n\n    [Theory]\n    [BitMemberAutoData(nameof(UnconfirmedOrganizationUsersStatuses))]\n    public async Task CreateSponsorship_ThrowsBadRequestException_WhenMemberDoesNotHaveConfirmedStatusInOrganization(\n        OrganizationUserStatusType status, Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user,\n        string sponsoredEmail, string friendlyName, Guid sponsorshipId,\n        SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;\n        sponsoringOrgUser.Status = status;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo =>\n        {\n            var sponsorship = callInfo.Arg<OrganizationSponsorship>();\n            sponsorship.Id = sponsorshipId;\n        });\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId.Value);\n\n\n        var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>\n            await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null));\n\n        Assert.Equal(\"Only confirmed users can sponsor other organizations.\", actual.Message);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user,\n        string sponsoredEmail, string friendlyName, Guid sponsorshipId, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;\n        sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo =>\n        {\n            var sponsorship = callInfo.Arg<OrganizationSponsorship>();\n            sponsorship.Id = sponsorshipId;\n        });\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId.Value);\n\n        // Setup for checking available seats\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)\n            .Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 0\n            });\n\n\n        await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,\n            PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null);\n\n        var expectedSponsorship = new OrganizationSponsorship\n        {\n            Id = sponsorshipId,\n            SponsoringOrganizationId = sponsoringOrg.Id,\n            SponsoringOrganizationUserId = sponsoringOrgUser.Id,\n            FriendlyName = friendlyName,\n            OfferedToEmail = sponsoredEmail,\n            PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,\n            IsAdminInitiated = false,\n            Notes = null\n        };\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)\n            .UpsertAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateSponsorship_CreateSponsorshipThrows_RevertsDatabase(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user,\n        string sponsoredEmail, string friendlyName, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;\n        sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        var expectedException = new Exception();\n        OrganizationSponsorship createdSponsorship = null;\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>().UpsertAsync(null!).ThrowsForAnyArgs(callInfo =>\n        {\n            createdSponsorship = callInfo.ArgAt<OrganizationSponsorship>(0);\n            createdSponsorship.Id = Guid.NewGuid();\n            return expectedException;\n        });\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(sponsoringOrgUser.UserId.Value);\n\n        var actualException = await Assert.ThrowsAsync<Exception>(() =>\n            sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,\n                PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null));\n        Assert.Same(expectedException, actualException);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)\n            .DeleteAsync(createdSponsorship);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task CreateSponsorship_MissingManageUsersPermission_ThrowsUnauthorizedException(\n        Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail,\n        string friendlyName, Guid sponsorshipId, Guid currentUserId, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;\n        sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo =>\n        {\n            var sponsorship = callInfo.Arg<OrganizationSponsorship>();\n            sponsorship.Id = sponsorshipId;\n        });\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns([\n            new()\n            {\n                Id = sponsoringOrg.Id,\n                Permissions = new Permissions(),\n                Type = OrganizationUserType.Custom\n            }\n        ]);\n\n\n        var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>\n            await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,\n                PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, null));\n\n        Assert.Equal(\"You do not have permissions to send sponsorships on behalf of the organization\", actual.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.User)]\n    [BitAutoData(OrganizationUserType.Custom)]\n    public async Task CreateSponsorship_InvalidUserType_ThrowsUnauthorizedException(\n        OrganizationUserType organizationUserType,\n        Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail,\n        string friendlyName, Guid sponsorshipId, Guid currentUserId, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;\n        sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo =>\n        {\n            var sponsorship = callInfo.Arg<OrganizationSponsorship>();\n            sponsorship.Id = sponsorshipId;\n        });\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns([\n            new()\n            {\n                Id = sponsoringOrg.Id,\n                Permissions = new Permissions(),\n                Type = organizationUserType\n            }\n        ]);\n\n        var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>\n            await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,\n                PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, null));\n\n        Assert.Equal(\"You do not have permissions to send sponsorships on behalf of the organization\", actual.Message);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CreateSponsorship_CreatesAdminInitiatedSponsorship(\n        OrganizationUserType organizationUserType,\n        Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail,\n        string friendlyName, Guid sponsorshipId, Guid currentUserId, string notes, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;\n        sponsoringOrg.UseAdminSponsoredFamilies = true;\n        sponsoringOrg.Seats = 10;\n        sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo =>\n        {\n            var sponsorship = callInfo.Arg<OrganizationSponsorship>();\n            sponsorship.Id = sponsorshipId;\n        });\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns([\n            new()\n            {\n                Id = sponsoringOrg.Id,\n                Permissions = new Permissions { ManageUsers = true },\n                Type = organizationUserType\n            }\n        ]);\n\n        // Setup for checking available seats - organization has plenty of seats\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)\n            .Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 5\n            });\n\n        var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,\n            PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes);\n\n\n        var expectedSponsorship = new OrganizationSponsorship\n        {\n            Id = sponsorshipId,\n            SponsoringOrganizationId = sponsoringOrg.Id,\n            SponsoringOrganizationUserId = sponsoringOrgUser.Id,\n            FriendlyName = friendlyName,\n            OfferedToEmail = sponsoredEmail,\n            PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,\n            IsAdminInitiated = true,\n            Notes = notes\n        };\n\n        Assert.True(SponsorshipValidator(expectedSponsorship, actual));\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)\n            .CreateAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));\n\n        // Verify we didn't need to add seats\n        await sutProvider.GetDependency<IOrganizationService>().DidNotReceive()\n            .AutoAddSeatsAsync(Arg.Any<Organization>(), Arg.Any<int>());\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CreateSponsorship_CreatesAdminInitiatedSponsorship_AutoscalesWhenNeeded(\n        OrganizationUserType organizationUserType,\n        Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail,\n        string friendlyName, Guid sponsorshipId, Guid currentUserId, string notes, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;\n        sponsoringOrg.UseAdminSponsoredFamilies = true;\n        sponsoringOrg.Seats = 10;\n        sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo =>\n        {\n            var sponsorship = callInfo.Arg<OrganizationSponsorship>();\n            sponsorship.Id = sponsorshipId;\n        });\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns([\n            new()\n            {\n                Id = sponsoringOrg.Id,\n                Permissions = new Permissions { ManageUsers = true },\n                Type = organizationUserType\n            }\n        ]);\n\n        // Setup for checking available seats - organization has no available seats\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)\n            .Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 10\n            });\n\n        // Setup for checking if can scale\n        sutProvider.GetDependency<IOrganizationService>()\n            .CanScaleAsync(sponsoringOrg, 1)\n            .Returns((true, \"\"));\n\n        var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,\n            PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes);\n\n\n        var expectedSponsorship = new OrganizationSponsorship\n        {\n            Id = sponsorshipId,\n            SponsoringOrganizationId = sponsoringOrg.Id,\n            SponsoringOrganizationUserId = sponsoringOrgUser.Id,\n            FriendlyName = friendlyName,\n            OfferedToEmail = sponsoredEmail,\n            PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,\n            IsAdminInitiated = true,\n            Notes = notes\n        };\n\n        Assert.True(SponsorshipValidator(expectedSponsorship, actual));\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)\n            .CreateAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));\n\n        // Verify we needed to add seats\n        await sutProvider.GetDependency<IOrganizationService>().Received(1)\n            .AutoAddSeatsAsync(sponsoringOrg, 1);\n    }\n\n    [Theory]\n    [BitAutoData(OrganizationUserType.Admin)]\n    [BitAutoData(OrganizationUserType.Owner)]\n    public async Task CreateSponsorship_CreatesAdminInitiatedSponsorship_ThrowsWhenCannotAutoscale(\n        OrganizationUserType organizationUserType,\n        Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail,\n        string friendlyName, Guid sponsorshipId, Guid currentUserId, string notes, SutProvider<CreateSponsorshipCommand> sutProvider)\n    {\n        sponsoringOrg.PlanType = PlanType.EnterpriseAnnually;\n        sponsoringOrg.UseAdminSponsoredFamilies = true;\n        sponsoringOrg.Seats = 10;\n        sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed;\n\n        sutProvider.GetDependency<IUserService>().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user);\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo =>\n        {\n            var sponsorship = callInfo.Arg<OrganizationSponsorship>();\n            sponsorship.Id = sponsorshipId;\n        });\n        sutProvider.GetDependency<ICurrentContext>().UserId.Returns(currentUserId);\n        sutProvider.GetDependency<ICurrentContext>().Organizations.Returns([\n            new()\n            {\n                Id = sponsoringOrg.Id,\n                Permissions = new Permissions { ManageUsers = true },\n                Type = organizationUserType\n            }\n        ]);\n\n        // Setup for checking available seats - organization has no available seats\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id)\n            .Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 10\n            });\n\n        // Setup for checking if can scale - cannot scale\n        var failureReason = \"Seat limit has been reached.\";\n        sutProvider.GetDependency<IOrganizationService>()\n            .CanScaleAsync(sponsoringOrg, 1)\n            .Returns((false, failureReason));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,\n                PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes));\n\n        Assert.Equal(failureReason, exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs",
    "content": "﻿using Bit.Core.Billing.Enums;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.Billing.Mocks;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;\n\npublic abstract class FamiliesForEnterpriseTestsBase\n{\n    public static IEnumerable<object[]> EnterprisePlanTypes =>\n        Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier == ProductTierType.Enterprise).Select(p => new object[] { p });\n\n    public static IEnumerable<object[]> NonEnterprisePlanTypes =>\n        Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Enterprise).Select(p => new object[] { p });\n\n    public static IEnumerable<object[]> FamiliesPlanTypes =>\n        Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier == ProductTierType.Families).Select(p => new object[] { p });\n\n    public static IEnumerable<object[]> NonFamiliesPlanTypes =>\n        Enum.GetValues<PlanType>().Where(p => MockPlans.Get(p).ProductTier != ProductTierType.Families).Select(p => new object[] { p });\n\n    public static IEnumerable<object[]> NonConfirmedOrganizationUsersStatuses =>\n        Enum.GetValues<OrganizationUserStatusType>()\n            .Where(s => s != OrganizationUserStatusType.Confirmed)\n            .Select(s => new object[] { s });\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedRevokeSponsorshipCommandTests.cs",
    "content": "﻿using Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;\nusing Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;\n\n[SutProviderCustomize]\n[OrganizationSponsorshipCustomize]\npublic class SelfHostedRevokeSponsorshipCommandTests : CancelSponsorshipCommandTestsBase\n{\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest(\n        SutProvider<SelfHostedRevokeSponsorshipCommand> sutProvider)\n    {\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.RevokeSponsorshipAsync(null));\n\n        Assert.Contains(\"You are not currently sponsoring an organization.\", exception.Message);\n        await AssertDidNotDeleteSponsorshipAsync(sutProvider);\n        await AssertDidNotUpdateSponsorshipAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeSponsorship_SponsorshipNotSynced_DeletesSponsorship(OrganizationSponsorship sponsorship,\n        SutProvider<SelfHostedRevokeSponsorshipCommand> sutProvider)\n    {\n        sponsorship.LastSyncDate = null;\n\n        await sutProvider.Sut.RevokeSponsorshipAsync(sponsorship);\n        await AssertDeletedSponsorshipAsync(sponsorship, sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task RevokeSponsorship_SponsorshipSynced_MarksForDeletion(OrganizationSponsorship sponsorship,\n        SutProvider<SelfHostedRevokeSponsorshipCommand> sutProvider)\n    {\n        sponsorship.LastSyncDate = DateTime.UtcNow;\n\n        await sutProvider.Sut.RevokeSponsorshipAsync(sponsorship);\n\n        Assert.True(sponsorship.ToDelete);\n        await AssertUpdatedSponsorshipAsync(sponsorship, sutProvider);\n        await AssertDidNotDeleteSponsorshipAsync(sutProvider);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommandTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Api.Response.OrganizationSponsorships;\nusing Bit.Core.Models.Data.Organizations.OrganizationSponsorships;\nusing Bit.Core.Models.OrganizationConnectionConfigs;\nusing Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;\n\npublic class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTestsBase\n{\n    private static SutProvider<SelfHostedSyncSponsorshipsCommand> GetSutProvider(string apiResponse = null)\n    {\n        return new SutProvider<SelfHostedSyncSponsorshipsCommand>()\n            .ConfigureBaseIdentityClientService(\"organization/sponsorship/sync\",\n                HttpMethod.Post, apiResponse: apiResponse);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_BillingSyncConnectionDisabled_ThrowsBadRequest(\n        Guid cloudOrganizationId, OrganizationConnection billingSyncConnection)\n    {\n        var sutProvider = GetSutProvider();\n        billingSyncConnection.Enabled = false;\n        billingSyncConnection.SetConfig(new BillingSyncConfig\n        {\n            BillingSyncKey = \"okslkcslkjf\"\n        });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection));\n\n        Assert.Contains($\"Connection disabled\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_BillingSyncConfigEmpty_ThrowsBadRequest(\n        Guid cloudOrganizationId, OrganizationConnection billingSyncConnection)\n    {\n        var sutProvider = GetSutProvider();\n        billingSyncConnection.Config = \"\";\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection));\n\n        Assert.Contains($\"No saved Connection config\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SyncOrganization_CloudCommunicationDisabled_EarlyReturn(\n        Guid cloudOrganizationId, OrganizationConnection billingSyncConnection)\n    {\n        var sutProvider = GetSutProvider();\n        sutProvider.GetDependency<IGlobalSettings>().EnableCloudCommunication = false;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection));\n\n        Assert.Contains($\"Cloud communication is disabled\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n    }\n\n    [Theory]\n    [OrganizationSponsorshipCustomize]\n    [BitAutoData]\n    public async Task SyncOrganization_SyncsSponsorships(\n        Guid cloudOrganizationId, OrganizationConnection billingSyncConnection, IEnumerable<OrganizationSponsorship> sponsorships)\n    {\n        var syncJsonResponse = JsonSerializer.Serialize(new OrganizationSponsorshipSyncResponseModel(\n            new OrganizationSponsorshipSyncData\n            {\n                SponsorshipsBatch = sponsorships.Select(o => new OrganizationSponsorshipData(o))\n            }));\n\n        var sutProvider = GetSutProvider(syncJsonResponse);\n\n        billingSyncConnection.SetConfig(new BillingSyncConfig\n        {\n            BillingSyncKey = \"okslkcslkjf\"\n        });\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(Arg.Any<Guid>()).Returns(sponsorships.ToList());\n\n        await sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .DeleteManyAsync(default);\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .Received(1)\n            .UpsertManyAsync(Arg.Any<IEnumerable<OrganizationSponsorship>>());\n    }\n\n    [Theory]\n    [OrganizationSponsorshipCustomize(ToDelete = true)]\n    [BitAutoData]\n    public async Task SyncOrganization_DeletesSponsorships(\n        Guid cloudOrganizationId, OrganizationConnection billingSyncConnection, IEnumerable<OrganizationSponsorship> sponsorships)\n    {\n        var syncJsonResponse = JsonSerializer.Serialize(new OrganizationSponsorshipSyncResponseModel(\n            new OrganizationSponsorshipSyncData\n            {\n                SponsorshipsBatch = sponsorships.Select(o => new OrganizationSponsorshipData(o) { CloudSponsorshipRemoved = true })\n            }));\n\n        var sutProvider = GetSutProvider(syncJsonResponse);\n        billingSyncConnection.SetConfig(new BillingSyncConfig\n        {\n            BillingSyncKey = \"okslkcslkjf\"\n        });\n        sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .GetManyBySponsoringOrganizationAsync(Arg.Any<Guid>()).Returns(sponsorships.ToList());\n\n        await sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection);\n\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .Received(1)\n            .DeleteManyAsync(Arg.Any<IEnumerable<Guid>>());\n        await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()\n            .DidNotReceiveWithAnyArgs()\n            .UpsertManyAsync(default);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/AddSecretsManagerSubscriptionCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions;\nusing Bit.Core.Services;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSubscriptionUpdate;\n[SutProviderCustomize]\npublic class AddSecretsManagerSubscriptionCommandTests\n{\n    [Theory]\n    [BitAutoData(PlanType.TeamsAnnually2019)]\n    [BitAutoData(PlanType.TeamsAnnually2020)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly2019)]\n    [BitAutoData(PlanType.TeamsMonthly2020)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsStarter)]\n    [BitAutoData(PlanType.EnterpriseAnnually2019)]\n    [BitAutoData(PlanType.EnterpriseAnnually2020)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.EnterpriseMonthly2019)]\n    [BitAutoData(PlanType.EnterpriseMonthly2020)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    public async Task SignUpAsync_ReturnsSuccessAndClientSecret_WhenOrganizationAndPlanExist(PlanType planType,\n        SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,\n        int additionalServiceAccounts,\n        int additionalSmSeats,\n        Organization organization,\n        bool useSecretsManager)\n    {\n        organization.PlanType = planType;\n\n        var plan = MockPlans.Get(organization.PlanType);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(plan);\n\n        await sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts);\n\n        sutProvider.GetDependency<IOrganizationService>().Received(1)\n            .ValidateSecretsManagerPlan(plan, Arg.Is<OrganizationUpgrade>(c =>\n                c.UseSecretsManager == useSecretsManager &&\n                c.AdditionalSmSeats == additionalSmSeats &&\n                c.AdditionalServiceAccounts == additionalServiceAccounts &&\n                c.AdditionalSeats == organization.Seats.GetValueOrDefault()));\n\n        await sutProvider.GetDependency<IStripePaymentService>().Received()\n            .AddSecretsManagerToSubscription(organization, plan, additionalSmSeats, additionalServiceAccounts);\n\n        // TODO: call ReferenceEventService - see AC-1481\n\n        await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(Arg.Is<Organization>(c =>\n            c.SmSeats == plan.SecretsManager.BaseSeats + additionalSmSeats &&\n            c.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + additionalServiceAccounts &&\n            c.UseSecretsManager == true));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpAsync_ThrowsNotFoundException_WhenOrganizationIsNull(\n        SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,\n        int additionalServiceAccounts,\n        int additionalSmSeats)\n    {\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.SignUpAsync(null, additionalSmSeats, additionalServiceAccounts));\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpAsync_ThrowsGatewayException_WhenGatewayCustomerIdIsNullOrWhitespace(\n        SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,\n        Organization organization,\n        int additionalServiceAccounts,\n        int additionalSmSeats)\n    {\n        organization.GatewayCustomerId = null;\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));\n        Assert.Contains(\"No payment method found.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpAsync_ThrowsGatewayException_WhenGatewaySubscriptionIdIsNullOrWhitespace(\n        SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,\n        Organization organization,\n        int additionalServiceAccounts,\n        int additionalSmSeats)\n    {\n        organization.GatewaySubscriptionId = null;\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.SignUpAsync(organization, additionalSmSeats, additionalServiceAccounts));\n        Assert.Contains(\"No subscription found.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpAsync_ThrowsException_WhenOrganizationAlreadyHasSecretsManager(\n        SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,\n        Organization organization)\n    {\n        organization.UseSecretsManager = true;\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpAsync(organization, 10, 10));\n\n        Assert.Contains(\"Organization already uses Secrets Manager\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SignUpAsync_ThrowsException_WhenOrganizationIsManagedByMSP(\n        SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider,\n        Organization organization,\n        Provider provider)\n    {\n        organization.UseSecretsManager = false;\n        provider.Type = ProviderType.Msp;\n        sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.SignUpAsync(organization, 10, 10));\n\n        Assert.Contains(\"Organizations with a Managed Service Provider do not support Secrets Manager.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    private static async Task VerifyDependencyNotCalledAsync(SutProvider<AddSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceive()\n            .AddSecretsManagerToSubscription(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>(), Arg.Any<int>());\n\n        // TODO: call ReferenceEventService - see AC-1481\n\n        await sutProvider.GetDependency<IOrganizationService>().DidNotReceive().ReplaceAndUpdateCacheAsync(Arg.Any<Organization>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Organizations.Models;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Models.StaticStore;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing Subscription = Stripe.Subscription;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSubscriptionUpdate;\n\n[SutProviderCustomize]\n[SecretsManagerOrganizationCustomize]\npublic class UpdateSecretsManagerSubscriptionCommandTests\n{\n    private static TheoryData<Plan> ToPlanTheory(List<PlanType> types)\n    {\n        var theoryData = new TheoryData<Plan>();\n        var plans = types.Select(MockPlans.Get).ToArray();\n        theoryData.AddRange(plans);\n        return theoryData;\n    }\n\n    public static TheoryData<Plan> AllTeamsAndEnterprise\n        => ToPlanTheory([\n            PlanType.EnterpriseAnnually2019,\n            PlanType.EnterpriseAnnually2020,\n            PlanType.EnterpriseAnnually,\n            PlanType.EnterpriseMonthly2019,\n            PlanType.EnterpriseMonthly2020,\n            PlanType.EnterpriseMonthly,\n            PlanType.TeamsMonthly2019,\n            PlanType.TeamsMonthly2020,\n            PlanType.TeamsMonthly,\n            PlanType.TeamsAnnually2019,\n            PlanType.TeamsAnnually2020,\n            PlanType.TeamsAnnually,\n            PlanType.TeamsStarter\n        ]);\n\n    public static TheoryData<Plan> CurrentTeamsAndEnterprise\n        => ToPlanTheory([\n            PlanType.EnterpriseAnnually,\n            PlanType.EnterpriseMonthly,\n            PlanType.TeamsMonthly,\n            PlanType.TeamsAnnually,\n            PlanType.TeamsStarter\n        ]);\n\n    [Theory]\n    [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]\n    public async Task UpdateSubscriptionAsync_UpdateEverything_ValidInput_Passes(\n        Plan plan,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = plan.Type;\n        organization.Seats = 400;\n        organization.SmSeats = 10;\n        organization.MaxAutoscaleSmSeats = 20;\n        organization.SmServiceAccounts = 200;\n        organization.MaxAutoscaleSmServiceAccounts = 350;\n\n        var updateSmSeats = 15;\n        var updateSmServiceAccounts = 300;\n        var updateMaxAutoscaleSmSeats = 16;\n        var updateMaxAutoscaleSmServiceAccounts = 301;\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = updateSmSeats,\n            SmServiceAccounts = updateSmServiceAccounts,\n            MaxAutoscaleSmSeats = updateMaxAutoscaleSmSeats,\n            MaxAutoscaleSmServiceAccounts = updateMaxAutoscaleSmServiceAccounts\n        };\n\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase);\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .AdjustServiceAccountsAsync(organization, plan, update.SmServiceAccountsExcludingBase);\n\n        // TODO: call ReferenceEventService - see AC-1481\n\n        AssertUpdatedOrganization(() => Arg.Is<Organization>(org =>\n                org.Id == organization.Id &&\n                org.SmSeats == updateSmSeats &&\n                org.MaxAutoscaleSmSeats == updateMaxAutoscaleSmSeats &&\n                org.SmServiceAccounts == updateSmServiceAccounts &&\n                org.MaxAutoscaleSmServiceAccounts == updateMaxAutoscaleSmServiceAccounts),\n                sutProvider);\n\n        await sutProvider\n            .GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendSecretsManagerMaxSeatLimitReachedEmailAsync(default, default, default);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(default, default, default);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(CurrentTeamsAndEnterprise))]\n    public async Task UpdateSubscriptionAsync_ValidInput_WithNullMaxAutoscale_Passes(\n        Plan plan,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = plan.Type;\n        organization.Seats = 20;\n\n        const int updateSmSeats = 15;\n        const int updateSmServiceAccounts = 450;\n\n        // Ensure that SmSeats is different from the original organization.SmSeats\n        organization.SmSeats = updateSmSeats + 5;\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = updateSmSeats,\n            MaxAutoscaleSmSeats = null,\n            SmServiceAccounts = updateSmServiceAccounts,\n            MaxAutoscaleSmServiceAccounts = null\n        };\n\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase);\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .AdjustServiceAccountsAsync(organization, plan, update.SmServiceAccountsExcludingBase);\n\n        // TODO: call ReferenceEventService - see AC-1481\n\n        AssertUpdatedOrganization(() => Arg.Is<Organization>(org =>\n                org.Id == organization.Id &&\n                org.SmSeats == updateSmSeats &&\n                org.MaxAutoscaleSmSeats == null &&\n                org.SmServiceAccounts == updateSmServiceAccounts &&\n                org.MaxAutoscaleSmServiceAccounts == null),\n            sutProvider);\n\n        await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs().SendSecretsManagerMaxSeatLimitReachedEmailAsync(default, default, default);\n        await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs().SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(default, default, default);\n    }\n\n    [Theory]\n    [BitAutoData(false, \"Cannot update subscription on a self-hosted instance.\")]\n    [BitAutoData(true, \"Cannot autoscale on a self-hosted instance.\")]\n    public async Task UpdatingSubscription_WhenSelfHosted_ThrowsBadRequestException(\n        bool autoscaling,\n        string expectedError,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, autoscaling).AdjustSeats(2);\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(expectedError, exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_NoSecretsManagerAccess_ThrowsException(\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider,\n        Organization organization)\n    {\n        var plan = MockPlans.Get(organization.PlanType);\n\n        organization.UseSecretsManager = false;\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n             () => sutProvider.Sut.UpdateSubscriptionAsync(update));\n\n        Assert.Contains(\"Organization has no access to Secrets Manager.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]\n    public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewayCustomerId_ThrowsException(\n        Plan plan,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = plan.Type;\n        organization.GatewayCustomerId = null;\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"No payment method found.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]\n    public async Task UpdateSubscriptionAsync_PaidPlan_NullGatewaySubscriptionId_ThrowsException(\n        Plan plan,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = plan.Type;\n        organization.GatewaySubscriptionId = null;\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"No subscription found.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]\n    public async Task AdjustServiceAccountsAsync_WithEnterpriseOrTeamsPlans_Success(\n        Plan plan,\n        Guid organizationId,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        var organizationSeats = plan.SecretsManager.BaseSeats + 10;\n        var organizationMaxAutoscaleSeats = 20;\n        var organizationServiceAccounts = plan.SecretsManager.BaseServiceAccount + 10;\n        var organizationMaxAutoscaleServiceAccounts = 300;\n\n        var organization = new Organization\n        {\n            Id = organizationId,\n            PlanType = plan.Type,\n            GatewayCustomerId = \"1\",\n            GatewaySubscriptionId = \"2\",\n            UseSecretsManager = true,\n            SmSeats = organizationSeats,\n            MaxAutoscaleSmSeats = organizationMaxAutoscaleSeats,\n            SmServiceAccounts = organizationServiceAccounts,\n            MaxAutoscaleSmServiceAccounts = organizationMaxAutoscaleServiceAccounts\n        };\n\n        var smServiceAccountsAdjustment = 10;\n        var expectedSmServiceAccounts = organizationServiceAccounts + smServiceAccountsAdjustment;\n        var expectedSmServiceAccountsExcludingBase = expectedSmServiceAccounts - plan.SecretsManager.BaseServiceAccount;\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(10);\n\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1).AdjustServiceAccountsAsync(\n            Arg.Is<Organization>(o => o.Id == organizationId),\n            plan,\n            expectedSmServiceAccountsExcludingBase);\n        // TODO: call ReferenceEventService - see AC-1481\n        AssertUpdatedOrganization(() => Arg.Is<Organization>(o =>\n                o.Id == organizationId\n                && o.SmSeats == organizationSeats\n                && o.MaxAutoscaleSmSeats == organizationMaxAutoscaleSeats\n                && o.SmServiceAccounts == expectedSmServiceAccounts\n                && o.MaxAutoscaleSmServiceAccounts == organizationMaxAutoscaleServiceAccounts), sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_UpdateSeatCount_AndExistingSeatsDoNotReachAutoscaleLimit_NoEmailSent(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        // Arrange\n        // Make sure Password Manager seats is greater or equal to Secrets Manager seats\n        const int initialSeatCount = 9;\n        const int maxSeatCount = 20;\n        // This represents the total number of users allowed in the organization.\n        organization.Seats = maxSeatCount;\n        // This represents the number of Secrets Manager users allowed in the organization.\n        organization.SmSeats = initialSeatCount;\n        // This represents the upper limit of Secrets Manager seats that can be automatically scaled.\n        organization.MaxAutoscaleSmSeats = maxSeatCount;\n\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        var plan = MockPlans.Get(organization.PlanType);\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = 8,\n            MaxAutoscaleSmSeats = maxSeatCount\n        };\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(5);\n\n        // Act\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        // Assert\n\n        // Currently being called once each for different validation methods\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(2)\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);\n\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendSecretsManagerMaxSeatLimitReachedEmailAsync(Arg.Any<Organization>(), Arg.Any<int>(), Arg.Any<IEnumerable<string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_ExistingSeatsReachAutoscaleLimit_EmailOwners(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        // Arrange\n        const int initialSeatCount = 5;\n        const int maxSeatCount = 10;\n\n        // This represents the total number of users allowed in the organization.\n        organization.Seats = maxSeatCount;\n        // This represents the number of Secrets Manager users allowed in the organization.\n        organization.SmSeats = initialSeatCount;\n        // This represents the upper limit of Secrets Manager seats that can be automatically scaled.\n        organization.MaxAutoscaleSmSeats = maxSeatCount;\n\n        var ownerDetailsList = new List<OrganizationUserUserDetails> { new() { Email = \"owner@example.com\" } };\n        organization.PlanType = PlanType.EnterpriseAnnually;\n        var plan = MockPlans.Get(organization.PlanType);\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = maxSeatCount,\n            MaxAutoscaleSmSeats = maxSeatCount\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(maxSeatCount);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organization.Id, OrganizationUserType.Owner)\n            .Returns(ownerDetailsList);\n\n        // Act\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        // Assert\n\n        await sutProvider.GetDependency<IOrganizationUserRepository>()\n            .Received(1)\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendSecretsManagerMaxSeatLimitReachedEmailAsync(Arg.Is(organization),\n                Arg.Is(maxSeatCount),\n                Arg.Is<IEnumerable<string>>(emails => emails.Contains(ownerDetailsList[0].Email)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_OrgWithNullSmSeatOnSeatsAdjustment_ThrowsException(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.SmSeats = null;\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateSubscriptionAsync(update));\n\n        Assert.Contains(\"Organization has no Secrets Manager seat limit, no need to adjust seats\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_SmSeatAutoscaling_Subtracting_ThrowsBadRequestException(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(-2);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Cannot use autoscaling to subtract seats.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Free)]\n    public async Task UpdateSubscriptionAsync_WithHasAdditionalSeatsOptionFalse_ThrowsBadRequestException(\n        PlanType planType,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = planType;\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustSeats(1);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"You have reached the maximum number of Secrets Manager seats (2) for this plan\",\n            exception.Message, StringComparison.InvariantCultureIgnoreCase);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SmSeatAutoscaling_MaxLimitReached_ThrowsBadRequestException(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.SmSeats = 9;\n        organization.MaxAutoscaleSmSeats = 10;\n\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustSeats(2);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Secrets Manager seat limit has been reached.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_SeatsAdjustmentGreaterThanMaxAutoscaleSeats_ThrowsException(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = organization.SmSeats + 10,\n            MaxAutoscaleSmSeats = organization.SmSeats + 5\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Cannot set max seat autoscaling below seat count.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_ThrowsBadRequestException_WhenSmSeatsLessThanOne(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = 0,\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(8);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"You must have at least 1 Secrets Manager seat.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_ThrowsBadRequestException_WhenOccupiedSeatsExceedNewSeatTotal(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.SmSeats = 8;\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = 7,\n        };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(8);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"8 users are currently occupying Secrets Manager seats. You cannot decrease your subscription below your current occupied seat count\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_UpdateServiceAccounts_AndExistingServiceAccountsCountDoesNotReachAutoscaleLimit_NoEmailSent(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        // Arrange\n        var smServiceAccounts = 300;\n        var existingServiceAccountCount = 299;\n\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmServiceAccounts = smServiceAccounts,\n            MaxAutoscaleSmServiceAccounts = smServiceAccounts\n        };\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .GetServiceAccountCountByOrganizationIdAsync(organization.Id)\n            .Returns(existingServiceAccountCount);\n\n        // Act\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        // Assert\n        await sutProvider.GetDependency<IServiceAccountRepository>()\n            .Received(1)\n            .GetServiceAccountCountByOrganizationIdAsync(organization.Id);\n\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(\n                Arg.Any<Organization>(),\n                Arg.Any<int>(),\n                Arg.Any<IEnumerable<string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_ExistingServiceAccountsReachAutoscaleLimit_EmailOwners(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        var smServiceAccounts = 300;\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmServiceAccounts = smServiceAccounts,\n            MaxAutoscaleSmServiceAccounts = smServiceAccounts\n        };\n        var ownerDetailsList = new List<OrganizationUserUserDetails> { new() { Email = \"owner@example.com\" } };\n\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .GetServiceAccountCountByOrganizationIdAsync(organization.Id)\n            .Returns(smServiceAccounts);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByMinimumRoleAsync(organization.Id, OrganizationUserType.Owner)\n            .Returns(ownerDetailsList);\n\n\n        // Act\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        // Assert\n\n        await sutProvider.GetDependency<IServiceAccountRepository>()\n            .Received(1)\n            .GetServiceAccountCountByOrganizationIdAsync(organization.Id);\n\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Arg.Is(organization),\n                Arg.Is(smServiceAccounts),\n                Arg.Is<IEnumerable<string>>(emails => emails.Contains(ownerDetailsList[0].Email)));\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task AdjustServiceAccountsAsync_ThrowsBadRequestException_WhenSmServiceAccountsIsNull(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.SmServiceAccounts = null;\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Organization has no machine accounts limit, no need to adjust machine accounts\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_ServiceAccountAutoscaling_Subtracting_ThrowsBadRequestException(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(-2);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Cannot use autoscaling to subtract machine accounts.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Free)]\n    public async Task UpdateSubscriptionAsync_WithHasAdditionalServiceAccountOptionFalse_ThrowsBadRequestException(\n        PlanType planType,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = planType;\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false).AdjustServiceAccounts(1);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"You have reached the maximum number of machine accounts (3) for this plan\",\n            exception.Message, StringComparison.InvariantCultureIgnoreCase);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task ServiceAccountAutoscaling_MaxLimitReached_ThrowsBadRequestException(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.SmServiceAccounts = 9;\n        organization.MaxAutoscaleSmServiceAccounts = 10;\n\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, true).AdjustServiceAccounts(2);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Secrets Manager machine account limit has been reached.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_ServiceAccountsGreaterThanMaxAutoscaleSeats_ThrowsException(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        const int smServiceAccount = 15;\n        const int maxAutoscaleSmServiceAccounts = 10;\n\n        organization.SmServiceAccounts = smServiceAccount - 5;\n        organization.MaxAutoscaleSmServiceAccounts = 2 * smServiceAccount;\n\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmServiceAccounts = smServiceAccount,\n            MaxAutoscaleSmServiceAccounts = maxAutoscaleSmServiceAccounts\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Cannot set max machine accounts autoscaling below machine account amount\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_ServiceAccountsLessThanPlanMinimum_ThrowsException(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        const int newSmServiceAccounts = 49;\n\n        organization.SmServiceAccounts = newSmServiceAccounts - 10;\n\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmServiceAccounts = newSmServiceAccounts,\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Plan has a minimum of 50 machine accounts\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]\n    public async Task UpdateSmServiceAccounts_WhenCurrentServiceAccountsIsGreaterThanNew_ThrowsBadRequestException(\n        Plan plan,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        var currentServiceAccounts = 301;\n        organization.PlanType = plan.Type;\n        organization.SmServiceAccounts = currentServiceAccounts;\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { SmServiceAccounts = 201 };\n\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .GetServiceAccountCountByOrganizationIdAsync(organization.Id)\n            .Returns(currentServiceAccounts);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Your organization currently has 301 machine accounts. You cannot decrease your subscription below your current machine account usage\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateSubscriptionAsync_ThrowsBadRequestException_WhenMaxAutoscaleSeatsBelowSeatCount(\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        const int smSeats = 10;\n        const int maxAutoscaleSmSeats = 5;\n\n        organization.SmSeats = smSeats - 1;\n        organization.MaxAutoscaleSmSeats = smSeats * 2;\n\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = smSeats,\n            MaxAutoscaleSmSeats = maxAutoscaleSmSeats\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Cannot set max seat autoscaling below seat count.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Free)]\n    public async Task UpdateMaxAutoscaleSmSeats_ThrowsBadRequestException_WhenExceedsPlanMaxUsers(\n        PlanType planType,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = planType;\n        organization.SmSeats = 2;\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            MaxAutoscaleSmSeats = 3\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Your plan has a Secrets Manager seat limit of 2, but you have specified a max autoscale count of 3.Reduce your max autoscale count.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Free)]\n    public async Task UpdateMaxAutoscaleSmSeats_ThrowsBadRequestException_WhenPlanDoesNotAllowAutoscale(\n        PlanType planType,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = planType;\n        organization.SmSeats = 2;\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            MaxAutoscaleSmSeats = 2\n        };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Your plan does not allow Secrets Manager seat autoscaling\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.Free)]\n    public async Task UpdateMaxAutoscaleSmServiceAccounts_ThrowsBadRequestException_WhenPlanDoesNotAllowAutoscale(\n        PlanType planType,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = planType;\n        organization.SmServiceAccounts = 3;\n\n        var plan = MockPlans.Get(organization.PlanType);\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { MaxAutoscaleSmServiceAccounts = 3 };\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSubscriptionAsync(update));\n        Assert.Contains(\"Your plan does not allow machine accounts autoscaling.\", exception.Message);\n        await VerifyDependencyNotCalledAsync(sutProvider);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]\n    public async Task UpdateSubscriptionAsync_WithFeatureFlag_AboveBaseServiceAccounts_UpdatesItemQuantity(\n        Plan plan,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = plan.Type;\n        organization.Seats = 400;\n        organization.SmSeats = 10;\n        organization.MaxAutoscaleSmSeats = 20;\n        organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount + 10;\n        organization.MaxAutoscaleSmServiceAccounts = 350;\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = 15,\n            SmServiceAccounts = plan.SecretsManager.BaseServiceAccount + 20,\n            MaxAutoscaleSmSeats = 16,\n            MaxAutoscaleSmServiceAccounts = 351\n        };\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        BillingCommandResult<Subscription> successResult = new Subscription();\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(organization, Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(successResult);\n\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>().Received(1)\n            .Run(organization, Arg.Is<OrganizationSubscriptionChangeSet>(cs =>\n                cs.Changes.Count == 2 &&\n                cs.Changes[0].IsItemQuantityUpdate &&\n                cs.Changes[1].IsItemQuantityUpdate));\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .AdjustSmSeatsAsync(default, default, default);\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .AdjustServiceAccountsAsync(default, default, default);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]\n    public async Task UpdateSubscriptionAsync_WithFeatureFlag_AtBaseServiceAccounts_AddsItem(\n        Plan plan,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = plan.Type;\n        organization.Seats = 400;\n        organization.SmSeats = 10;\n        organization.MaxAutoscaleSmSeats = 20;\n        organization.SmServiceAccounts = plan.SecretsManager.BaseServiceAccount;\n        organization.MaxAutoscaleSmServiceAccounts = 350;\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = 15,\n            SmServiceAccounts = plan.SecretsManager.BaseServiceAccount + 10,\n            MaxAutoscaleSmSeats = 16,\n            MaxAutoscaleSmServiceAccounts = 351\n        };\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        BillingCommandResult<Subscription> successResult = new Subscription();\n        sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>()\n            .Run(organization, Arg.Any<OrganizationSubscriptionChangeSet>())\n            .Returns(successResult);\n\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>().Received(1)\n            .Run(organization, Arg.Is<OrganizationSubscriptionChangeSet>(cs =>\n                cs.Changes.Count == 2 &&\n                cs.Changes[0].IsItemQuantityUpdate &&\n                cs.Changes[1].IsItemAddition));\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .AdjustSmSeatsAsync(default, default, default);\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .AdjustServiceAccountsAsync(default, default, default);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]\n    public async Task UpdateSubscriptionAsync_WithFeatureFlag_OnlyAutoscaleLimitsChanged_SkipsCommand(\n        Plan plan,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = plan.Type;\n        organization.Seats = 400;\n        organization.SmSeats = 10;\n        organization.MaxAutoscaleSmSeats = 20;\n        organization.SmServiceAccounts = 200;\n        organization.MaxAutoscaleSmServiceAccounts = 350;\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = organization.SmSeats,\n            SmServiceAccounts = organization.SmServiceAccounts,\n            MaxAutoscaleSmSeats = 25,\n            MaxAutoscaleSmServiceAccounts = 400\n        };\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>().DidNotReceiveWithAnyArgs()\n            .Run(default, default);\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .AdjustSmSeatsAsync(default, default, default);\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceiveWithAnyArgs()\n            .AdjustServiceAccountsAsync(default, default, default);\n\n        AssertUpdatedOrganization(() => Arg.Is<Organization>(org =>\n                org.Id == organization.Id &&\n                org.MaxAutoscaleSmSeats == 25 &&\n                org.MaxAutoscaleSmServiceAccounts == 400),\n            sutProvider);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(AllTeamsAndEnterprise))]\n    public async Task UpdateSubscriptionAsync_WithoutFeatureFlag_UpdateSeatsAndServiceAccounts_UsesPaymentService(\n        Plan plan,\n        Organization organization,\n        SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        organization.PlanType = plan.Type;\n        organization.Seats = 400;\n        organization.SmSeats = 10;\n        organization.MaxAutoscaleSmSeats = 20;\n        organization.SmServiceAccounts = 200;\n        organization.MaxAutoscaleSmServiceAccounts = 350;\n\n        var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)\n        {\n            SmSeats = 15,\n            SmServiceAccounts = 300,\n            MaxAutoscaleSmSeats = 16,\n            MaxAutoscaleSmServiceAccounts = 301\n        };\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(false);\n\n        await sutProvider.Sut.UpdateSubscriptionAsync(update);\n\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .AdjustSmSeatsAsync(organization, plan, update.SmSeatsExcludingBase);\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .AdjustServiceAccountsAsync(organization, plan, update.SmServiceAccountsExcludingBase);\n        await sutProvider.GetDependency<IUpdateOrganizationSubscriptionCommand>().DidNotReceiveWithAnyArgs()\n            .Run(default, default);\n    }\n\n    private static async Task VerifyDependencyNotCalledAsync(SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceive()\n            .AdjustSmSeatsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());\n        await sutProvider.GetDependency<IStripePaymentService>().DidNotReceive()\n            .AdjustServiceAccountsAsync(Arg.Any<Organization>(), Arg.Any<Plan>(), Arg.Any<int>());\n        // TODO: call ReferenceEventService - see AC-1481\n        await sutProvider.GetDependency<IMailService>().DidNotReceive()\n            .SendOrganizationMaxSeatLimitReachedEmailAsync(Arg.Any<Organization>(), Arg.Any<int>(),\n                Arg.Any<IEnumerable<string>>());\n\n        await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n        await sutProvider.GetDependency<IApplicationCacheService>().DidNotReceiveWithAnyArgs().UpsertOrganizationAbilityAsync(default);\n    }\n\n    private void AssertUpdatedOrganization(Func<Organization> organizationMatcher, SutProvider<UpdateSecretsManagerSubscriptionCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organizationMatcher());\n        sutProvider.GetDependency<IApplicationCacheService>().Received(1).UpsertOrganizationAbilityAsync(organizationMatcher());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.Billing;\nusing Bit.Core.Billing.Commands;\nusing Bit.Core.Billing.Enums;\nusing Bit.Core.Billing.Organizations.Commands;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.KeyManagement.Models.Data;\nusing Bit.Core.Models.Business;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.OrganizationFeatures.OrganizationSubscriptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.SecretsManager.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Test.AutoFixture.OrganizationFixtures;\nusing Bit.Core.Test.Billing.Mocks;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing OneOf.Types;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.OrganizationSubscriptionUpdate;\n\n[SutProviderCustomize]\npublic class UpgradeOrganizationPlanCommandTests\n{\n    private static void SetupOrganizationOwner(SutProvider<UpgradeOrganizationPlanCommand> sutProvider, Organization organization, User owner)\n    {\n        var ownerOrganizationUser = new OrganizationUser\n        {\n            OrganizationId = organization.Id,\n            UserId = owner.Id,\n            Type = OrganizationUserType.Owner\n        };\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)\n            .Returns(new[] { ownerOrganizationUser });\n        sutProvider.GetDependency<IUserRepository>()\n            .GetByIdAsync(owner.Id)\n            .Returns(owner);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpgradePlan_OrganizationIsNull_Throws(Guid organizationId, OrganizationUpgrade upgrade,\n            SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(Task.FromResult<Organization>(null));\n        var exception = await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpgradePlan_GatewayCustomIdIsNull_Throws(Organization organization, OrganizationUpgrade upgrade,\n            SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        organization.GatewayCustomerId = string.Empty;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));\n        Assert.Contains(\"no payment method\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpgradePlan_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade,\n            SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        upgrade.Plan = organization.PlanType;\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));\n        Assert.Contains(\"already on this plan\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpgradePlan_SM_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        upgrade.Plan = organization.PlanType;\n        upgrade.UseSecretsManager = true;\n        upgrade.AdditionalSmSeats = 10;\n        upgrade.AdditionalServiceAccounts = 10;\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));\n        var exception = await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));\n        Assert.Contains(\"already on this plan\", exception.Message);\n    }\n\n    [Theory]\n    [FreeOrganizationUpgradeCustomize, BitAutoData]\n    public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,\n        User owner,\n        [Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,\n            SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        SetupOrganizationOwner(sutProvider, organization, owner);\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())\n            .Returns(policy);\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));\n        upgrade.AdditionalSmSeats = 10;\n        upgrade.AdditionalSeats = 10;\n        upgrade.Plan = PlanType.TeamsAnnually;\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);\n        await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);\n    }\n\n    [Theory]\n    [BitAutoData(PlanType.TeamsStarter)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    public async Task UpgradePlan_FromFamilies_Passes(\n        PlanType planType,\n        Organization organization,\n        OrganizationUpgrade organizationUpgrade,\n        [Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        organization.PlanType = PlanType.FamiliesAnnually;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));\n\n        organizationUpgrade.AdditionalSeats = 30;\n        organizationUpgrade.UseSecretsManager = true;\n        organizationUpgrade.AdditionalSmSeats = 20;\n        organizationUpgrade.AdditionalServiceAccounts = 5;\n        organizationUpgrade.AdditionalStorageGb = 3;\n        organizationUpgrade.Plan = planType;\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organizationUpgrade.Plan).Returns(MockPlans.Get(organizationUpgrade.Plan));\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())\n            .Returns(policy);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade);\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1).AdjustSubscription(\n            organization,\n            MockPlans.Get(planType),\n            organizationUpgrade.AdditionalSeats,\n            organizationUpgrade.UseSecretsManager,\n            organizationUpgrade.AdditionalSmSeats,\n            5,\n            3);\n        await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);\n    }\n\n    [Theory, FreeOrganizationUpgradeCustomize]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsStarter)]\n    public async Task UpgradePlan_SM_Passes(PlanType planType, Organization organization, OrganizationUpgrade upgrade,\n        User owner,\n        [Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        SetupOrganizationOwner(sutProvider, organization, owner);\n        upgrade.Plan = planType;\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));\n\n        var plan = MockPlans.Get(upgrade.Plan);\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())\n            .Returns(policy);\n\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n\n        upgrade.AdditionalSeats = 15;\n        upgrade.AdditionalSmSeats = 10;\n        upgrade.AdditionalServiceAccounts = 20;\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);\n\n        await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(\n            Arg.Is<Organization>(o =>\n                o.Seats == plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats\n                && o.SmSeats == plan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats\n                && o.SmServiceAccounts == plan.SecretsManager.BaseServiceAccount + upgrade.AdditionalServiceAccounts));\n\n        Assert.True(result.Item1);\n        Assert.Null(result.Item2);\n    }\n\n    [Theory, FreeOrganizationUpgradeCustomize]\n    [BitAutoData(PlanType.EnterpriseMonthly)]\n    [BitAutoData(PlanType.EnterpriseAnnually)]\n    [BitAutoData(PlanType.TeamsMonthly)]\n    [BitAutoData(PlanType.TeamsAnnually)]\n    [BitAutoData(PlanType.TeamsStarter)]\n    public async Task UpgradePlan_SM_NotEnoughSmSeats_Throws(PlanType planType, Organization organization, OrganizationUpgrade upgrade,\n        [Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        upgrade.Plan = planType;\n        upgrade.AdditionalSeats = 15;\n        upgrade.AdditionalSmSeats = 1;\n        upgrade.AdditionalServiceAccounts = 0;\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));\n\n        organization.SmSeats = 2;\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())\n            .Returns(policy);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(2);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));\n        Assert.Contains(\"Your organization currently has 2 Secrets Manager seats filled. Your new plan only has\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default);\n    }\n\n    [Theory, FreeOrganizationUpgradeCustomize]\n    [BitAutoData(PlanType.EnterpriseMonthly, 201)]\n    [BitAutoData(PlanType.EnterpriseAnnually, 201)]\n    [BitAutoData(PlanType.TeamsMonthly, 51)]\n    [BitAutoData(PlanType.TeamsAnnually, 51)]\n    [BitAutoData(PlanType.TeamsStarter, 51)]\n    public async Task UpgradePlan_SM_NotEnoughServiceAccounts_Throws(PlanType planType, int currentServiceAccounts,\n     Organization organization, OrganizationUpgrade upgrade,\n     [Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,\n     SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        upgrade.Plan = planType;\n        upgrade.AdditionalSeats = 15;\n        upgrade.AdditionalSmSeats = 1;\n        upgrade.AdditionalServiceAccounts = 0;\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(upgrade.Plan).Returns(MockPlans.Get(upgrade.Plan));\n\n        organization.SmSeats = 1;\n        organization.SmServiceAccounts = currentServiceAccounts;\n        sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType).Returns(MockPlans.Get(organization.PlanType));\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())\n            .Returns(policy);\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts\n            {\n                Sponsored = 0,\n                Users = 1\n            });\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id).Returns(1);\n        sutProvider.GetDependency<IServiceAccountRepository>()\n            .GetServiceAccountCountByOrganizationIdAsync(organization.Id).Returns(currentServiceAccounts);\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));\n        Assert.Contains($\"Your organization currently has {currentServiceAccounts} machine accounts. Your new plan only allows\", exception.Message);\n\n        await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default);\n    }\n\n    [Theory]\n    [FreeOrganizationUpgradeCustomize, BitAutoData]\n    public async Task UpgradePlan_WhenOrganizationIsMissingPublicAndPrivateKeys_Backfills(\n        Organization organization,\n        OrganizationUpgrade upgrade,\n        User owner,\n        string newPublicKey,\n        string newPrivateKey,\n        [Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        SetupOrganizationOwner(sutProvider, organization, owner);\n        organization.PublicKey = null;\n        organization.PrivateKey = null;\n\n        upgrade.Plan = PlanType.TeamsAnnually;\n        upgrade.Keys = new PublicKeyEncryptionKeyPairData(\n            wrappedPrivateKey: newPrivateKey,\n            publicKey: newPublicKey);\n        upgrade.AdditionalSeats = 10;\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())\n            .Returns(policy);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(upgrade.Plan)\n            .Returns(MockPlans.Get(upgrade.Plan));\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 });\n\n        // Act\n        await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);\n\n        // Assert\n        Assert.Equal(newPublicKey, organization.PublicKey);\n        Assert.Equal(newPrivateKey, organization.PrivateKey);\n        await sutProvider.GetDependency<IOrganizationService>()\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(organization);\n    }\n\n    [Theory]\n    [FreeOrganizationUpgradeCustomize, BitAutoData]\n    public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotOverwriteWithNull(\n        Organization organization,\n        OrganizationUpgrade upgrade,\n        User owner,\n        [Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        SetupOrganizationOwner(sutProvider, organization, owner);\n        // Arrange\n        const string existingPublicKey = \"existing-public-key\";\n        const string existingPrivateKey = \"existing-private-key\";\n\n        organization.PublicKey = existingPublicKey;\n        organization.PrivateKey = existingPrivateKey;\n\n        upgrade.Plan = PlanType.TeamsAnnually;\n        upgrade.Keys = null;\n        upgrade.AdditionalSeats = 10;\n\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())\n            .Returns(policy);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(upgrade.Plan)\n            .Returns(MockPlans.Get(upgrade.Plan));\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 });\n\n        // Act\n        await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);\n\n        // Assert\n        Assert.Equal(existingPublicKey, organization.PublicKey);\n        Assert.Equal(existingPrivateKey, organization.PrivateKey);\n        await sutProvider.GetDependency<IOrganizationService>()\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpgradePlan_FeatureFlagOn_OrganizationIsNull_Throws(\n        Guid organizationId,\n        OrganizationUpgrade upgrade,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organizationId)\n            .Returns(Task.FromResult<Organization>(null));\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n\n        await Assert.ThrowsAsync<NotFoundException>(\n            () => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade));\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpgradePlan_FeatureFlagOn_DelegatesToVNextCommand(\n        Organization organization,\n        OrganizationUpgrade upgrade,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(upgrade.Plan)\n            .Returns(MockPlans.Get(upgrade.Plan));\n\n        BillingCommandResult<None> successResult = new None();\n        sutProvider.GetDependency<IUpgradeOrganizationPlanVNextCommand>()\n            .Run(organization, MockPlans.Get(upgrade.Plan), upgrade.Keys)\n            .Returns(successResult);\n\n        var result = await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);\n\n        Assert.True(result.Item1);\n        Assert.Null(result.Item2);\n        await sutProvider.GetDependency<IUpgradeOrganizationPlanVNextCommand>()\n            .Received(1)\n            .Run(organization, MockPlans.Get(upgrade.Plan), upgrade.Keys);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpgradePlan_FeatureFlagOn_VNextFailure_ThrowsBillingException(\n        Organization organization,\n        OrganizationUpgrade upgrade,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand)\n            .Returns(true);\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(upgrade.Plan)\n            .Returns(MockPlans.Get(upgrade.Plan));\n\n        BillingCommandResult<None> failureResult = new BadRequest(\"Something went wrong\");\n        sutProvider.GetDependency<IUpgradeOrganizationPlanVNextCommand>()\n            .Run(organization, MockPlans.Get(upgrade.Plan), upgrade.Keys)\n            .Returns(failureResult);\n\n        var exception = await Assert.ThrowsAsync<BillingException>(\n            () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));\n        Assert.Equal(\"Something went wrong\", exception.Response);\n    }\n\n    [Theory]\n    [FreeOrganizationUpgradeCustomize, BitAutoData]\n    public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotBackfillWithNewKeys(\n        Organization organization,\n        OrganizationUpgrade upgrade,\n        User owner,\n        [Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,\n        SutProvider<UpgradeOrganizationPlanCommand> sutProvider)\n    {\n        SetupOrganizationOwner(sutProvider, organization, owner);\n        // Arrange\n        const string existingPublicKey = \"existing-public-key\";\n        const string existingPrivateKey = \"existing-private-key\";\n        const string newPublicKey = \"new-public-key\";\n        const string newPrivateKey = \"new-private-key\";\n\n        organization.PublicKey = existingPublicKey;\n        organization.PrivateKey = existingPrivateKey;\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>())\n            .Returns(policy);\n\n        upgrade.Plan = PlanType.TeamsAnnually;\n        upgrade.Keys = new PublicKeyEncryptionKeyPairData(\n            wrappedPrivateKey: newPrivateKey,\n            publicKey: newPublicKey);\n        upgrade.AdditionalSeats = 10;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(organization.PlanType)\n            .Returns(MockPlans.Get(organization.PlanType));\n        sutProvider.GetDependency<IPricingClient>()\n            .GetPlanOrThrow(upgrade.Plan)\n            .Returns(MockPlans.Get(upgrade.Plan));\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)\n            .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 });\n\n        // Act\n        await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);\n\n        // Assert\n        Assert.Equal(existingPublicKey, organization.PublicKey);\n        Assert.Equal(existingPrivateKey, organization.PrivateKey);\n        await sutProvider.GetDependency<IOrganizationService>()\n            .Received(1)\n            .ReplaceAndUpdateCacheAsync(organization);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/OrganizationFeatures/Policies/PolicyQueryTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing NSubstitute.ReturnsExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.OrganizationFeatures.Policies;\n\n[SutProviderCustomize]\npublic class PolicyQueryTests\n{\n    [Theory, BitAutoData]\n    public async Task RunAsync_WithExistingPolicy_ReturnsPolicy(SutProvider<PolicyQuery> sutProvider,\n        Policy policy)\n    {\n        // Arrange\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(policy.OrganizationId, policy.Type)\n            .Returns(policy);\n\n        // Act\n        var policyData = await sutProvider.Sut.RunAsync(policy.OrganizationId, policy.Type);\n\n        // Assert\n        Assert.Equal(policy.Data, policyData.Data);\n        Assert.Equal(policy.Type, policyData.Type);\n        Assert.Equal(policy.Enabled, policyData.Enabled);\n        Assert.Equal(policy.OrganizationId, policyData.OrganizationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RunAsync_WithNonExistentPolicy_ReturnsDefaultDisabledPolicy(\n        SutProvider<PolicyQuery> sutProvider,\n        Guid organizationId,\n        PolicyType policyType)\n    {\n        // Arrange\n        sutProvider.GetDependency<IPolicyRepository>()\n            .GetByOrganizationIdTypeAsync(organizationId, policyType)\n            .ReturnsNull();\n\n        // Act\n        var policyData = await sutProvider.Sut.RunAsync(organizationId, policyType);\n\n        // Assert\n        Assert.Equal(organizationId, policyData.OrganizationId);\n        Assert.Equal(policyType, policyData.Type);\n        Assert.False(policyData.Enabled);\n        Assert.Null(policyData.Data);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Installations/Commands/UpdateInstallationCommandTests.cs",
    "content": "﻿using Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Platform.Installations.Tests;\n\n[SutProviderCustomize]\npublic class UpdateInstallationCommandTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateLastActivityDateAsync_WithDefaultGuid_ThrowsException(SutProvider<UpdateInstallationCommand> sutProvider)\n    {\n        // Arrange\n        var defaultGuid = default(Guid);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<Exception>(\n            () => sutProvider.Sut.UpdateLastActivityDateAsync(defaultGuid));\n\n        Assert.Contains(\"invalid installation id\", exception.Message);\n\n        await sutProvider\n            .GetDependency<IInstallationRepository>()\n            .DidNotReceive()\n            .UpsertAsync(Arg.Any<Installation>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateLastActivityDateAsync_WithNonExistentInstallation_ThrowsException(\n        Guid installationId,\n        SutProvider<UpdateInstallationCommand> sutProvider)\n    {\n        // Arrange\n        sutProvider\n            .GetDependency<IGetInstallationQuery>()\n            .GetByIdAsync(installationId)\n            .Returns((Installation)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<Exception>(\n            () => sutProvider.Sut.UpdateLastActivityDateAsync(installationId));\n\n        Assert.Contains(\"no installation was found\", exception.Message);\n\n        await sutProvider\n            .GetDependency<IInstallationRepository>()\n            .DidNotReceive()\n            .UpsertAsync(Arg.Any<Installation>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task UpdateLastActivityDateAsync_ShouldUpdateLastActivityDate(\n        Installation installation\n    )\n    {\n        // Arrange\n        var sutProvider = new SutProvider<UpdateInstallationCommand>()\n            .WithFakeTimeProvider()\n            .Create();\n\n        var someDate = new DateTime(2014, 11, 3, 18, 27, 0, DateTimeKind.Utc);\n        sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(someDate);\n\n        sutProvider\n            .GetDependency<IGetInstallationQuery>()\n            .GetByIdAsync(installation.Id)\n            .Returns(installation);\n\n        // Act\n        await sutProvider.Sut.UpdateLastActivityDateAsync(installation.Id);\n\n        // Assert\n        await sutProvider\n            .GetDependency<IInstallationRepository>()\n            .Received(1)\n            .UpsertAsync(Arg.Is<Installation>(inst => inst.LastActivityDate == someDate));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Mail/DomainClaimedEmailRenderTest.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Platform.Mail.Enqueuing;\nusing Bit.Core.Services.Mail;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Mail;\n\npublic class DomainClaimedEmailRenderTest\n{\n    [Fact]\n    public async Task RenderDomainClaimedEmail_ToVerifyTemplate()\n    {\n        var globalSettings = new GlobalSettings\n        {\n            Mail = new GlobalSettings.MailSettings\n            {\n                ReplyToEmail = \"no-reply@bitwarden.com\",\n                Smtp = new GlobalSettings.MailSettings.SmtpSettings\n                {\n                    Host = \"localhost\",\n                    Port = 1025,\n                    StartTls = false,\n                    Ssl = false\n                }\n            },\n            SiteName = \"Bitwarden\"\n        };\n\n        var mailDeliveryService = Substitute.For<IMailDeliveryService>();\n        var mailEnqueuingService = new BlockingMailEnqueuingService();\n        var distributedCache = Substitute.For<IDistributedCache>();\n        var logger = Substitute.For<ILogger<HandlebarsMailService>>();\n\n        var mailService = new HandlebarsMailService(\n            globalSettings,\n            mailDeliveryService,\n            mailEnqueuingService,\n            distributedCache,\n            logger\n        );\n\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            Name = \"Acme Corporation\"\n        };\n\n        var testEmails = new List<string>\n        {\n            \"alice@acme.com\",\n            \"bob@acme.com\",\n            \"charlie@acme.com\"\n        };\n\n        var emailList = new ClaimedUserDomainClaimedEmails(\n            testEmails,\n            organization,\n            \"acme.com\"\n        );\n\n        await mailService.SendClaimedDomainUserEmailAsync(emailList);\n\n        await mailDeliveryService.Received(3).SendEmailAsync(Arg.Any<Bit.Core.Models.Mail.MailMessage>());\n\n        var calls = mailDeliveryService.ReceivedCalls()\n            .Where(call => call.GetMethodInfo().Name == \"SendEmailAsync\")\n            .ToList();\n\n        Assert.Equal(3, calls.Count);\n\n        foreach (var call in calls)\n        {\n            var mailMessage = call.GetArguments()[0] as Bit.Core.Models.Mail.MailMessage;\n            Assert.NotNull(mailMessage);\n\n            var recipient = mailMessage.ToEmails.First();\n\n            Assert.Contains(\"@acme.com\", mailMessage.HtmlContent);\n            Assert.Contains(recipient, mailMessage.HtmlContent);\n            Assert.DoesNotContain(\"[at]\", mailMessage.HtmlContent);\n            Assert.DoesNotContain(\"[dot]\", mailMessage.HtmlContent);\n        }\n    }\n\n    [Fact(Skip = \"For local development - requires MailCatcher at localhost:10250\")]\n    public async Task SendDomainClaimedEmail_ToMailCatcher()\n    {\n        var globalSettings = new GlobalSettings\n        {\n            Mail = new GlobalSettings.MailSettings\n            {\n                ReplyToEmail = \"no-reply@bitwarden.com\",\n                Smtp = new GlobalSettings.MailSettings.SmtpSettings\n                {\n                    Host = \"localhost\",\n                    Port = 10250,\n                    StartTls = false,\n                    Ssl = false\n                }\n            },\n            SiteName = \"Bitwarden\"\n        };\n\n        var mailDeliveryLogger = Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>();\n        var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, mailDeliveryLogger);\n        var mailEnqueuingService = new BlockingMailEnqueuingService();\n        var distributedCache = Substitute.For<IDistributedCache>();\n        var logger = Substitute.For<ILogger<HandlebarsMailService>>();\n\n        var mailService = new HandlebarsMailService(\n            globalSettings,\n            mailDeliveryService,\n            mailEnqueuingService,\n            distributedCache,\n            logger\n        );\n\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            Name = \"Acme Corporation\"\n        };\n\n        var testEmails = new List<string>\n        {\n            \"alice@acme.com\",\n            \"bob@acme.com\"\n        };\n\n        var emailList = new ClaimedUserDomainClaimedEmails(\n            testEmails,\n            organization,\n            \"acme.com\"\n        );\n\n        await mailService.SendClaimedDomainUserEmailAsync(emailList);\n    }\n\n    [Fact(Skip = \"This test sends actual emails and is for manual template verification only\")]\n    public async Task RenderDomainClaimedEmail_WithSpecialCharacters()\n    {\n        var globalSettings = new GlobalSettings\n        {\n            Mail = new GlobalSettings.MailSettings\n            {\n                Smtp = new GlobalSettings.MailSettings.SmtpSettings\n                {\n                    Host = \"localhost\",\n                    Port = 1025,\n                    StartTls = false,\n                    Ssl = false\n                }\n            },\n            SiteName = \"Bitwarden\"\n        };\n\n        var mailDeliveryService = Substitute.For<IMailDeliveryService>();\n        var mailEnqueuingService = new BlockingMailEnqueuingService();\n        var distributedCache = Substitute.For<IDistributedCache>();\n        var logger = Substitute.For<ILogger<HandlebarsMailService>>();\n\n        var mailService = new HandlebarsMailService(\n            globalSettings,\n            mailDeliveryService,\n            mailEnqueuingService,\n            distributedCache,\n            logger\n        );\n\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            Name = \"Test Corp & Co.\"\n        };\n\n        var testEmails = new List<string>\n        {\n            \"test.user+tag@example.com\"\n        };\n\n        var emailList = new ClaimedUserDomainClaimedEmails(\n            testEmails,\n            organization,\n            \"example.com\"\n        );\n\n        await mailService.SendClaimedDomainUserEmailAsync(emailList);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Mailer/HandlebarMailRendererTests.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Platform.Mailer.TestMail;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Mailer;\n\npublic class HandlebarMailRendererTests\n{\n    [Fact]\n    public async Task RenderAsync_ReturnsExpectedHtmlAndTxt()\n    {\n        var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n        var renderer = new HandlebarMailRenderer(logger, globalSettings);\n\n        var view = new TestMailView { Name = \"John Smith\" };\n\n        var (html, txt) = await renderer.RenderAsync(view);\n\n        Assert.Equal(\"Hello <b>John Smith</b>\", html.Trim());\n        Assert.Equal(\"Hello John Smith\", txt.Trim());\n    }\n\n    [Fact]\n    public async Task RenderAsync_LoadsFromDisk_WhenSelfHostedAndFileExists()\n    {\n        var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();\n        var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());\n        Directory.CreateDirectory(tempDir);\n\n        try\n        {\n            var globalSettings = new GlobalSettings\n            {\n                SelfHosted = true,\n                MailTemplateDirectory = tempDir\n            };\n\n            // Create test template files on disk\n            var htmlTemplatePath = Path.Combine(tempDir, \"Bit.Core.Test.Platform.Mailer.TestMail.TestMailView.html.hbs\");\n            var txtTemplatePath = Path.Combine(tempDir, \"Bit.Core.Test.Platform.Mailer.TestMail.TestMailView.text.hbs\");\n            await File.WriteAllTextAsync(htmlTemplatePath, \"Custom HTML: <b>{{Name}}</b>\");\n            await File.WriteAllTextAsync(txtTemplatePath, \"Custom TXT: {{Name}}\");\n\n            var renderer = new HandlebarMailRenderer(logger, globalSettings);\n            var view = new TestMailView { Name = \"Jane Doe\" };\n\n            var (html, txt) = await renderer.RenderAsync(view);\n\n            Assert.Equal(\"Custom HTML: <b>Jane Doe</b>\", html.Trim());\n            Assert.Equal(\"Custom TXT: Jane Doe\", txt.Trim());\n        }\n        finally\n        {\n            // Cleanup\n            if (Directory.Exists(tempDir))\n            {\n                Directory.Delete(tempDir, true);\n            }\n        }\n    }\n\n    [Theory]\n    [InlineData(\"../../../etc/passwd\")]\n    [InlineData(\"../../../../malicious.txt\")]\n    [InlineData(\"../../malicious.txt\")]\n    [InlineData(\"../malicious.txt\")]\n    public async Task ReadSourceFromDiskAsync_PrevenetsPathTraversal_WhenMaliciousPathProvided(string maliciousPath)\n    {\n        var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();\n        var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());\n        Directory.CreateDirectory(tempDir);\n\n        try\n        {\n            var globalSettings = new GlobalSettings\n            {\n                SelfHosted = true,\n                MailTemplateDirectory = tempDir\n            };\n\n            // Create a malicious file outside the template directory\n            var maliciousFile = Path.Combine(Path.GetTempPath(), \"malicious.txt\");\n            await File.WriteAllTextAsync(maliciousFile, \"Malicious Content\");\n\n            var renderer = new HandlebarMailRenderer(logger, globalSettings);\n\n            // Use reflection to call the private ReadSourceFromDiskAsync method\n            var method = typeof(HandlebarMailRenderer).GetMethod(\"ReadSourceFromDiskAsync\",\n                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);\n            var task = (Task<string?>)method!.Invoke(renderer, new object[] { maliciousPath })!;\n            var result = await task;\n\n            // Should return null and not load the malicious file\n            Assert.Null(result);\n\n            // Verify that a warning was logged for the path traversal attempt\n            logger.Received(1).Log(\n                LogLevel.Warning,\n                Arg.Any<EventId>(),\n                Arg.Any<object>(),\n                Arg.Any<Exception>(),\n                Arg.Any<Func<object, Exception, string>>());\n\n            // Cleanup malicious file\n            if (File.Exists(maliciousFile))\n            {\n                File.Delete(maliciousFile);\n            }\n        }\n        finally\n        {\n            // Cleanup\n            if (Directory.Exists(tempDir))\n            {\n                Directory.Delete(tempDir, true);\n            }\n        }\n    }\n\n    [Fact]\n    public async Task ReadSourceFromDiskAsync_AllowsValidFileWithDifferentCase_WhenCaseInsensitiveFileSystem()\n    {\n        var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();\n        var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());\n        Directory.CreateDirectory(tempDir);\n\n        try\n        {\n            var globalSettings = new GlobalSettings\n            {\n                SelfHosted = true,\n                MailTemplateDirectory = tempDir\n            };\n\n            // Create a test template file\n            var templateFileName = \"TestTemplate.hbs\";\n            var templatePath = Path.Combine(tempDir, templateFileName);\n            await File.WriteAllTextAsync(templatePath, \"Test Content\");\n\n            var renderer = new HandlebarMailRenderer(logger, globalSettings);\n\n            // Try to read with different case (should work on case-insensitive file systems like Windows/macOS)\n            var method = typeof(HandlebarMailRenderer).GetMethod(\"ReadSourceFromDiskAsync\",\n                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);\n            var task = (Task<string?>)method!.Invoke(renderer, new object[] { templateFileName })!;\n            var result = await task;\n\n            // Should successfully read the file\n            Assert.Equal(\"Test Content\", result);\n\n            // Verify no warning was logged\n            logger.DidNotReceive().Log(\n                LogLevel.Warning,\n                Arg.Any<EventId>(),\n                Arg.Any<object>(),\n                Arg.Any<Exception>(),\n                Arg.Any<Func<object, Exception, string>>());\n        }\n        finally\n        {\n            // Cleanup\n            if (Directory.Exists(tempDir))\n            {\n                Directory.Delete(tempDir, true);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Mailer/MailerTest.cs",
    "content": "﻿using Bit.Core.Models.Mail;\nusing Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Platform.Mail.Mailer;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.Platform.Mailer.TestMail;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Mailer;\n\npublic class MailerTest\n{\n    [Fact]\n    public async Task SendEmailAsync()\n    {\n        var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();\n        var globalSettings = new GlobalSettings { SelfHosted = false };\n        var deliveryService = Substitute.For<IMailDeliveryService>();\n\n        var mailer = new Core.Platform.Mail.Mailer.Mailer(new HandlebarMailRenderer(logger, globalSettings), deliveryService);\n\n        var mail = new TestMail.TestMail()\n        {\n            ToEmails = [\"test@bw.com\"],\n            View = new TestMailView() { Name = \"John Smith\" }\n        };\n\n        MailMessage? sentMessage = null;\n        await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message =>\n            sentMessage = message\n        ));\n\n        await mailer.SendEmail(mail);\n\n        Assert.NotNull(sentMessage);\n        Assert.Contains(\"test@bw.com\", sentMessage.ToEmails);\n        Assert.Equal(\"Test Email\", sentMessage.Subject);\n        Assert.Equivalent(\"Hello John Smith\", sentMessage.TextContent.Trim());\n        Assert.Equivalent(\"Hello <b>John Smith</b>\", sentMessage.HtmlContent.Trim());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Mailer/TestMail/TestMailView.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Mailer;\n\nnamespace Bit.Core.Test.Platform.Mailer.TestMail;\n\npublic class TestMailView : BaseMailView\n{\n    public required string Name { get; init; }\n}\n\npublic class TestMail : BaseMail<TestMailView>\n{\n    public override string Subject { get; set; } = \"Test Email\";\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Mailer/TestMail/TestMailView.html.hbs",
    "content": "﻿Hello <b>{{ Name }}</b>\n"
  },
  {
    "path": "test/Core.Test/Platform/Mailer/TestMail/TestMailView.text.hbs",
    "content": "﻿Hello {{ Name }}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs",
    "content": "﻿#nullable enable\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing Azure.Storage.Queues;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Test.AutoFixture;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Push.Engines;\n\n[QueueClientCustomize]\n[SutProviderCustomize]\npublic class AzureQueuePushEngineTests\n{\n    private static readonly Guid _deviceId = Guid.Parse(\"c4730f80-caaa-4772-97bd-5c0d23a2baa3\");\n    private static readonly string _deviceIdentifier = \"test_device_identifier\";\n    private readonly FakeTimeProvider _fakeTimeProvider;\n    private readonly Core.Settings.GlobalSettings _globalSettings = new();\n\n    public AzureQueuePushEngineTests()\n    {\n        _fakeTimeProvider = new();\n        _fakeTimeProvider.SetUtcNow(DateTime.UtcNow);\n    }\n\n    [Theory]\n    [InlineData(\"6a5bbe1b-cf16-49a6-965f-5c2eac56a531\", null)]\n    [InlineData(null, \"b9a3fcb4-2447-45c1-aad2-24de43c88c44\")]\n    public async Task PushSyncCipherCreateAsync_SendsExpectedResponse(string? userId, string? organizationId)\n    {\n        var collectionId = Guid.NewGuid();\n\n        var cipher = new Cipher\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId != null ? Guid.Parse(userId) : null,\n            OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 1,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = cipher.Id,\n                [\"UserId\"] = cipher.UserId,\n                [\"OrganizationId\"] = cipher.OrganizationId,\n                [\"CollectionIds\"] = new JsonArray(collectionId),\n                [\"RevisionDate\"] = cipher.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        if (!cipher.UserId.HasValue)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"UserId\");\n        }\n\n        if (!cipher.OrganizationId.HasValue)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"OrganizationId\");\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"CollectionIds\");\n        }\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCipherCreateAsync(cipher, [collectionId]),\n            expectedPayload\n        );\n    }\n\n    [Theory]\n    [InlineData(\"6a5bbe1b-cf16-49a6-965f-5c2eac56a531\", null)]\n    [InlineData(null, \"b9a3fcb4-2447-45c1-aad2-24de43c88c44\")]\n    public async Task PushSyncCipherUpdateAsync_SendsExpectedResponse(string? userId, string? organizationId)\n    {\n        var collectionId = Guid.NewGuid();\n\n        var cipher = new Cipher\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId != null ? Guid.Parse(userId) : null,\n            OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 0,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = cipher.Id,\n                [\"UserId\"] = cipher.UserId,\n                [\"OrganizationId\"] = cipher.OrganizationId,\n                [\"CollectionIds\"] = new JsonArray(collectionId),\n                [\"RevisionDate\"] = cipher.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        if (!cipher.UserId.HasValue)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"UserId\");\n        }\n\n        if (!cipher.OrganizationId.HasValue)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"OrganizationId\");\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"CollectionIds\");\n        }\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCipherUpdateAsync(cipher, [collectionId]),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncCipherDeleteAsync_SendsExpectedResponse()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var cipher = new Cipher\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            OrganizationId = null,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 2,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = cipher.Id,\n                [\"UserId\"] = cipher.UserId,\n                [\"RevisionDate\"] = cipher.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCipherDeleteAsync(cipher),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncFolderCreateAsync_SendsExpectedResponse()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var folder = new Folder\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 7,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = folder.Id,\n                [\"UserId\"] = folder.UserId,\n                [\"RevisionDate\"] = folder.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncFolderCreateAsync(folder),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncFolderUpdateAsync_SendsExpectedResponse()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var folder = new Folder\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 8,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = folder.Id,\n                [\"UserId\"] = folder.UserId,\n                [\"RevisionDate\"] = folder.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncFolderUpdateAsync(folder),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncFolderDeleteAsync_SendsExpectedResponse()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var folder = new Folder\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 3,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = folder.Id,\n                [\"UserId\"] = folder.UserId,\n                [\"RevisionDate\"] = folder.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncFolderDeleteAsync(folder),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncCiphersAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 4,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = _fakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCiphersAsync(userId),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncVaultAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 5,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = _fakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncVaultAsync(userId),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncOrganizationsAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 17,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = _fakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncOrganizationsAsync(userId),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncOrgKeysAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 6,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = _fakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncOrgKeysAsync(userId),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSettingsAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 10,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = _fakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSettingsAsync(userId),\n            expectedPayload\n        );\n    }\n\n    [Theory]\n    [InlineData(true, null)]\n    [InlineData(true, PushNotificationLogOutReason.KdfChange)]\n    [InlineData(false, null)]\n    [InlineData(false, PushNotificationLogOutReason.KdfChange)]\n    public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext,\n        PushNotificationLogOutReason? reason)\n    {\n        var userId = Guid.NewGuid();\n\n        var payload = new JsonObject\n        {\n            [\"UserId\"] = userId\n        };\n        if (reason != null)\n        {\n            payload[\"Reason\"] = (int)reason;\n        }\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 11,\n            [\"Payload\"] = payload,\n        };\n\n        if (excludeCurrentContext)\n        {\n            expectedPayload[\"ContextId\"] = _deviceIdentifier;\n        }\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSendCreateAsync_SendsExpectedResponse()\n    {\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 12,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = send.Id,\n                [\"UserId\"] = send.UserId,\n                [\"RevisionDate\"] = send.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSendCreateAsync(send),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSendUpdateAsync_SendsExpectedResponse()\n    {\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 13,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = send.Id,\n                [\"UserId\"] = send.UserId,\n                [\"RevisionDate\"] = send.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSendUpdateAsync(send),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSendDeleteAsync_SendsExpectedResponse()\n    {\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 14,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = send.Id,\n                [\"UserId\"] = send.UserId,\n                [\"RevisionDate\"] = send.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSendDeleteAsync(send),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushAuthRequestAsync_SendsExpectedResponse()\n    {\n        var authRequest = new AuthRequest\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 15,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = authRequest.Id,\n                [\"UserId\"] = authRequest.UserId,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushAuthRequestAsync(authRequest),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushAuthRequestResponseAsync_SendsExpectedResponse()\n    {\n        var authRequest = new AuthRequest\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 16,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = authRequest.Id,\n                [\"UserId\"] = authRequest.UserId,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushAuthRequestResponseAsync(authRequest),\n            expectedPayload\n        );\n    }\n\n    [Theory]\n    [InlineData(true, null, null)]\n    [InlineData(false, \"e8e08ce8-8a26-4a65-913a-ba1d8c478b2f\", null)]\n    [InlineData(false, null, \"2f53ee32-edf9-4169-b276-760fe92e03bf\")]\n    public async Task PushNotificationAsync_SendsExpectedResponse(bool global, string? userId, string? organizationId)\n    {\n        var notification = new Notification\n        {\n            Id = Guid.NewGuid(),\n            Priority = Priority.High,\n            Global = global,\n            ClientType = ClientType.All,\n            UserId = userId != null ? Guid.Parse(userId) : null,\n            OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null,\n            Title = \"My Title\",\n            Body = \"My Body\",\n            CreationDate = DateTime.UtcNow.AddDays(-1),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 20,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = notification.Id,\n                [\"Priority\"] = 3,\n                [\"Global\"] = global,\n                [\"ClientType\"] = 0,\n                [\"UserId\"] = notification.UserId,\n                [\"OrganizationId\"] = notification.OrganizationId,\n                [\"InstallationId\"] = _globalSettings.Installation.Id,\n                [\"Title\"] = notification.Title,\n                [\"Body\"] = notification.Body,\n                [\"CreationDate\"] = notification.CreationDate,\n                [\"RevisionDate\"] = notification.RevisionDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        if (!global)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"InstallationId\");\n        }\n\n        if (!notification.UserId.HasValue)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"UserId\");\n        }\n\n        if (!notification.OrganizationId.HasValue)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"OrganizationId\");\n        }\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushNotificationAsync(notification),\n            expectedPayload\n        );\n    }\n\n    [Theory]\n    [InlineData(true, null, null)]\n    [InlineData(false, \"e8e08ce8-8a26-4a65-913a-ba1d8c478b2f\", null)]\n    [InlineData(false, null, \"2f53ee32-edf9-4169-b276-760fe92e03bf\")]\n    public async Task PushNotificationStatusAsync_SendsExpectedResponse(bool global, string? userId, string? organizationId)\n    {\n        var notification = new Notification\n        {\n            Id = Guid.NewGuid(),\n            Priority = Priority.High,\n            Global = global,\n            ClientType = ClientType.All,\n            UserId = userId != null ? Guid.Parse(userId) : null,\n            OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null,\n            Title = \"My Title\",\n            Body = \"My Body\",\n            CreationDate = DateTime.UtcNow.AddDays(-1),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var notificationStatus = new NotificationStatus\n        {\n            ReadDate = DateTime.UtcNow,\n            DeletedDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 21,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = notification.Id,\n                [\"Priority\"] = 3,\n                [\"Global\"] = global,\n                [\"ClientType\"] = 0,\n                [\"UserId\"] = notification.UserId,\n                [\"OrganizationId\"] = notification.OrganizationId,\n                [\"InstallationId\"] = _globalSettings.Installation.Id,\n                [\"Title\"] = notification.Title,\n                [\"Body\"] = notification.Body,\n                [\"CreationDate\"] = notification.CreationDate,\n                [\"RevisionDate\"] = notification.RevisionDate,\n                [\"ReadDate\"] = notificationStatus.ReadDate,\n                [\"DeletedDate\"] = notificationStatus.DeletedDate,\n            },\n            [\"ContextId\"] = _deviceIdentifier,\n        };\n\n        if (!global)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"InstallationId\");\n        }\n\n        if (!notification.UserId.HasValue)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"UserId\");\n        }\n\n        if (!notification.OrganizationId.HasValue)\n        {\n            expectedPayload[\"Payload\"]!.AsObject().Remove(\"OrganizationId\");\n        }\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushNotificationStatusAsync(notification, notificationStatus),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            Enabled = true,\n            LimitCollectionCreation = true,\n            LimitCollectionDeletion = true,\n            LimitItemDeletion = true,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 19,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"OrganizationId\"] = organization.Id,\n                [\"LimitCollectionCreation\"] = organization.LimitCollectionCreation,\n                [\"LimitCollectionDeletion\"] = organization.LimitCollectionDeletion,\n                [\"LimitItemDeletion\"] = organization.LimitItemDeletion,\n            },\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncOrganizationCollectionManagementSettingsAsync(organization),\n            expectedPayload\n        );\n    }\n\n    [Fact]\n    public async Task PushRefreshSecurityTasksAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Type\"] = 22,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = _fakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushRefreshSecurityTasksAsync(userId),\n            expectedPayload\n        );\n    }\n\n    // [Fact]\n    // public async Task SendPayloadToInstallationAsync_ThrowsNotImplementedException()\n    // {\n    //     await Assert.ThrowsAsync<NotImplementedException>(\n    //         async () => await sut.SendPayloadToInstallationAsync(\"installation_id\", PushType.AuthRequest, new {}, null)\n    //     );\n    // }\n\n    // [Fact]\n    // public async Task SendPayloadToUserAsync_ThrowsNotImplementedException()\n    // {\n    //     await Assert.ThrowsAsync<NotImplementedException>(\n    //         async () => await _sut.SendPayloadToUserAsync(\"user_id\", PushType.AuthRequest, new {}, null)\n    //     );\n    // }\n\n    // [Fact]\n    // public async Task SendPayloadToOrganizationAsync_ThrowsNotImplementedException()\n    // {\n    //     await Assert.ThrowsAsync<NotImplementedException>(\n    //         async () => await _sut.SendPayloadToOrganizationAsync(\"organization_id\", PushType.AuthRequest, new {}, null)\n    //     );\n    // }\n\n    private async Task VerifyNotificationAsync(Func<IPushNotificationService, Task> test, JsonNode expectedMessage)\n    {\n        var queueClient = Substitute.For<QueueClient>();\n\n        var httpContextAccessor = Substitute.For<IHttpContextAccessor>();\n\n        var httpContext = new DefaultHttpContext();\n\n        var serviceCollection = new ServiceCollection();\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.DeviceIdentifier = _deviceIdentifier;\n        serviceCollection.AddSingleton(currentContext);\n\n        httpContext.RequestServices = serviceCollection.BuildServiceProvider();\n\n        httpContextAccessor.HttpContext\n            .Returns(httpContext);\n\n        var globalSettings = new Core.Settings.GlobalSettings();\n\n        var sut = new AzureQueuePushEngine(\n            queueClient,\n            httpContextAccessor,\n            globalSettings,\n            NullLogger<AzureQueuePushEngine>.Instance\n        );\n\n        await test(new EngineWrapper(sut, _fakeTimeProvider, _globalSettings.Installation.Id));\n\n        // Hoist equality checker outside the expression so that we\n        // can more easily place a breakpoint\n        var checkEquality = (string actual) =>\n        {\n            var actualNode = JsonNode.Parse(actual);\n            return JsonNode.DeepEquals(actualNode, expectedMessage);\n        };\n\n        await queueClient\n            .Received(1)\n            .SendMessageAsync(Arg.Is<string>((actual) => checkEquality(actual)));\n    }\n\n    private static bool MatchMessage<T>(PushType pushType, string message, IEquatable<T> expectedPayloadEquatable,\n        string contextId)\n    {\n        var pushNotificationData = JsonSerializer.Deserialize<PushNotificationData<T>>(message);\n        return pushNotificationData != null &&\n               pushNotificationData.Type == pushType &&\n               expectedPayloadEquatable.Equals(pushNotificationData.Payload) &&\n               pushNotificationData.ContextId == contextId;\n    }\n\n    private class NotificationPushNotificationEquals(\n        Notification notification,\n        NotificationStatus? notificationStatus,\n        Guid? installationId)\n        : IEquatable<NotificationPushNotification>\n    {\n        public bool Equals(NotificationPushNotification? other)\n        {\n            return other != null &&\n                   other.Id == notification.Id &&\n                   other.Priority == notification.Priority &&\n                   other.Global == notification.Global &&\n                   other.ClientType == notification.ClientType &&\n                   other.UserId.HasValue == notification.UserId.HasValue &&\n                   other.UserId == notification.UserId &&\n                   other.OrganizationId.HasValue == notification.OrganizationId.HasValue &&\n                   other.OrganizationId == notification.OrganizationId &&\n                   other.ClientType == notification.ClientType &&\n                   other.InstallationId == installationId &&\n                   other.Title == notification.Title &&\n                   other.Body == notification.Body &&\n                   other.CreationDate == notification.CreationDate &&\n                   other.RevisionDate == notification.RevisionDate &&\n                   other.ReadDate == notificationStatus?.ReadDate &&\n                   other.DeletedDate == notificationStatus?.DeletedDate;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs",
    "content": "﻿using System.Text.Json.Nodes;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.Extensions.Logging.Abstractions;\n\nnamespace Bit.Core.Test.Platform.Push.Engines;\n\npublic class NotificationsApiPushEngineTests : PushTestBase\n{\n    public NotificationsApiPushEngineTests()\n    {\n        GlobalSettings.BaseServiceUri.InternalNotifications = \"https://localhost:7777\";\n        GlobalSettings.BaseServiceUri.InternalIdentity = \"https://localhost:8888\";\n    }\n\n    protected override string ExpectedClientUrl() => \"https://localhost:7777/send\";\n\n    protected override IPushEngine CreateService()\n    {\n        return new NotificationsApiPushEngine(\n            HttpClientFactory,\n            GlobalSettings,\n            HttpContextAccessor,\n            NullLogger<NotificationsApiPushEngine>.Instance\n        );\n    }\n\n    protected override JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid collectionId)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 1,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = cipher.Id,\n                [\"UserId\"] = cipher.UserId,\n                [\"OrganizationId\"] = null,\n                [\"CollectionIds\"] = new JsonArray(collectionId),\n                [\"RevisionDate\"] = cipher.RevisionDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n    protected override JsonNode GetPushSyncCipherUpdatePayload(Cipher cipher, Guid collectionId)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 0,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = cipher.Id,\n                [\"UserId\"] = cipher.UserId,\n                [\"OrganizationId\"] = null,\n                [\"CollectionIds\"] = new JsonArray(collectionId),\n                [\"RevisionDate\"] = cipher.RevisionDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n    protected override JsonNode GetPushSyncCipherDeletePayload(Cipher cipher)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 2,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = cipher.Id,\n                [\"UserId\"] = cipher.UserId,\n                [\"OrganizationId\"] = null,\n                [\"CollectionIds\"] = null,\n                [\"RevisionDate\"] = cipher.RevisionDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushSyncFolderCreatePayload(Folder folder)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 7,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = folder.Id,\n                [\"UserId\"] = folder.UserId,\n                [\"RevisionDate\"] = folder.RevisionDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushSyncFolderUpdatePayload(Folder folder)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 8,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = folder.Id,\n                [\"UserId\"] = folder.UserId,\n                [\"RevisionDate\"] = folder.RevisionDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushSyncFolderDeletePayload(Folder folder)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 3,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = folder.Id,\n                [\"UserId\"] = folder.UserId,\n                [\"RevisionDate\"] = folder.RevisionDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushSyncCiphersPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 4,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ContextId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncVaultPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 5,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ContextId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncOrganizationsPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 17,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ContextId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncOrgKeysPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 6,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ContextId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncSettingsPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 10,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ContextId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext,\n        PushNotificationLogOutReason? reason)\n    {\n        JsonNode? contextId = excludeCurrentContext ? DeviceIdentifier : null;\n\n        return new JsonObject\n        {\n            [\"Type\"] = 11,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Reason\"] = reason != null ? (int)reason : null\n            },\n            [\"ContextId\"] = contextId,\n        };\n    }\n\n    protected override JsonNode GetPushSendCreatePayload(Send send)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 12,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = send.Id,\n                [\"UserId\"] = send.UserId,\n                [\"RevisionDate\"] = send.RevisionDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushSendUpdatePayload(Send send)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 13,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = send.Id,\n                [\"UserId\"] = send.UserId,\n                [\"RevisionDate\"] = send.RevisionDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushSendDeletePayload(Send send)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 14,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = send.Id,\n                [\"UserId\"] = send.UserId,\n                [\"RevisionDate\"] = send.RevisionDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushAuthRequestPayload(AuthRequest authRequest)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 15,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = authRequest.Id,\n                [\"UserId\"] = authRequest.UserId,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushAuthRequestResponsePayload(AuthRequest authRequest)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 16,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = authRequest.Id,\n                [\"UserId\"] = authRequest.UserId,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushNotificationResponsePayload(Notification notification, Guid? userId, Guid? organizationId)\n    {\n        JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null;\n\n        return new JsonObject\n        {\n            [\"Type\"] = 20,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = notification.Id,\n                [\"Priority\"] = 3,\n                [\"Global\"] = notification.Global,\n                [\"ClientType\"] = 0,\n                [\"UserId\"] = notification.UserId,\n                [\"OrganizationId\"] = notification.OrganizationId,\n                [\"TaskId\"] = notification.TaskId,\n                [\"InstallationId\"] = installationId,\n                [\"Title\"] = notification.Title,\n                [\"Body\"] = notification.Body,\n                [\"CreationDate\"] = notification.CreationDate,\n                [\"RevisionDate\"] = notification.RevisionDate,\n                [\"ReadDate\"] = null,\n                [\"DeletedDate\"] = null,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushNotificationStatusResponsePayload(Notification notification, NotificationStatus notificationStatus, Guid? userId, Guid? organizationId)\n    {\n        JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null;\n\n        return new JsonObject\n        {\n            [\"Type\"] = 21,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = notification.Id,\n                [\"Priority\"] = 3,\n                [\"Global\"] = notification.Global,\n                [\"ClientType\"] = 0,\n                [\"UserId\"] = notification.UserId,\n                [\"OrganizationId\"] = notification.OrganizationId,\n                [\"InstallationId\"] = installationId,\n                [\"TaskId\"] = notification.TaskId,\n                [\"Title\"] = notification.Title,\n                [\"Body\"] = notification.Body,\n                [\"CreationDate\"] = notification.CreationDate,\n                [\"RevisionDate\"] = notification.RevisionDate,\n                [\"ReadDate\"] = notificationStatus.ReadDate,\n                [\"DeletedDate\"] = notificationStatus.DeletedDate,\n            },\n            [\"ContextId\"] = DeviceIdentifier,\n        };\n    }\n\n    protected override JsonNode GetPushSyncOrganizationStatusResponsePayload(Organization organization)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 18,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"OrganizationId\"] = organization.Id,\n                [\"Enabled\"] = organization.Enabled,\n            },\n            [\"ContextId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(Organization organization)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 19,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"OrganizationId\"] = organization.Id,\n                [\"LimitCollectionCreation\"] = organization.LimitCollectionCreation,\n                [\"LimitCollectionDeletion\"] = organization.LimitCollectionDeletion,\n                [\"LimitItemDeletion\"] = organization.LimitItemDeletion,\n            },\n            [\"ContextId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushRefreshSecurityTasksResponsePayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"Type\"] = 22,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ContextId\"] = null,\n        };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/Engines/PushTestBase.cs",
    "content": "﻿using System.IdentityModel.Tokens.Jwt;\nusing System.Net;\nusing System.Net.Http.Json;\nusing System.Text.Json;\nusing System.Text.Json.Nodes;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing RichardSzalay.MockHttp;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Push.Engines;\n\npublic class EngineWrapper(IPushEngine pushEngine, FakeTimeProvider fakeTimeProvider, Guid installationId) : IPushNotificationService\n{\n    public Guid InstallationId { get; } = installationId;\n\n    public TimeProvider TimeProvider { get; } = fakeTimeProvider;\n\n    public ILogger Logger => NullLogger<EngineWrapper>.Instance;\n\n    public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class\n        => pushEngine.PushAsync(pushNotification);\n\n    public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds)\n        => pushEngine.PushCipherAsync(cipher, pushType, collectionIds);\n}\n\npublic abstract class PushTestBase\n{\n    protected static readonly string DeviceIdentifier = \"test_device_identifier\";\n\n    protected readonly MockHttpMessageHandler MockClient = new();\n    protected readonly MockHttpMessageHandler MockIdentityClient = new();\n\n    protected readonly IHttpClientFactory HttpClientFactory;\n    protected readonly GlobalSettings GlobalSettings;\n    protected readonly IHttpContextAccessor HttpContextAccessor;\n    protected readonly FakeTimeProvider FakeTimeProvider;\n\n    public PushTestBase()\n    {\n        HttpClientFactory = Substitute.For<IHttpClientFactory>();\n\n        // Mock HttpClient\n        HttpClientFactory.CreateClient(\"client\")\n            .Returns(new HttpClient(MockClient));\n\n        HttpClientFactory.CreateClient(\"identity\")\n            .Returns(new HttpClient(MockIdentityClient));\n\n        GlobalSettings = new GlobalSettings();\n        HttpContextAccessor = Substitute.For<IHttpContextAccessor>();\n\n        FakeTimeProvider = new FakeTimeProvider();\n\n        FakeTimeProvider.SetUtcNow(DateTimeOffset.UtcNow);\n    }\n\n    protected abstract IPushEngine CreateService();\n\n    protected abstract string ExpectedClientUrl();\n\n    protected abstract JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid collectionId);\n    protected abstract JsonNode GetPushSyncCipherUpdatePayload(Cipher cipher, Guid collectionId);\n    protected abstract JsonNode GetPushSyncCipherDeletePayload(Cipher cipher);\n    protected abstract JsonNode GetPushSyncFolderCreatePayload(Folder folder);\n    protected abstract JsonNode GetPushSyncFolderUpdatePayload(Folder folder);\n    protected abstract JsonNode GetPushSyncFolderDeletePayload(Folder folder);\n    protected abstract JsonNode GetPushSyncCiphersPayload(Guid userId);\n    protected abstract JsonNode GetPushSyncVaultPayload(Guid userId);\n    protected abstract JsonNode GetPushSyncOrganizationsPayload(Guid userId);\n    protected abstract JsonNode GetPushSyncOrgKeysPayload(Guid userId);\n    protected abstract JsonNode GetPushSyncSettingsPayload(Guid userId);\n    protected abstract JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext,\n        PushNotificationLogOutReason? reason);\n    protected abstract JsonNode GetPushSendCreatePayload(Send send);\n    protected abstract JsonNode GetPushSendUpdatePayload(Send send);\n    protected abstract JsonNode GetPushSendDeletePayload(Send send);\n    protected abstract JsonNode GetPushAuthRequestPayload(AuthRequest authRequest);\n    protected abstract JsonNode GetPushAuthRequestResponsePayload(AuthRequest authRequest);\n    protected abstract JsonNode GetPushNotificationResponsePayload(Notification notification, Guid? userId, Guid? organizationId);\n    protected abstract JsonNode GetPushNotificationStatusResponsePayload(Notification notification, NotificationStatus notificationStatus, Guid? userId, Guid? organizationId);\n    protected abstract JsonNode GetPushSyncOrganizationStatusResponsePayload(Organization organization);\n    protected abstract JsonNode GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(Organization organization);\n    protected abstract JsonNode GetPushRefreshSecurityTasksResponsePayload(Guid userId);\n\n    [Fact]\n    public async Task PushSyncCipherCreateAsync_SendsExpectedResponse()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var cipher = new Cipher\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            OrganizationId = null,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCipherCreateAsync(cipher, [collectionId]),\n            GetPushSyncCipherCreatePayload(cipher, collectionId)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncCipherUpdateAsync_SendsExpectedResponse()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var cipher = new Cipher\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            OrganizationId = null,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCipherUpdateAsync(cipher, [collectionId]),\n            GetPushSyncCipherUpdatePayload(cipher, collectionId)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncCipherDeleteAsync_SendsExpectedResponse()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var cipher = new Cipher\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            OrganizationId = null,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCipherDeleteAsync(cipher),\n            GetPushSyncCipherDeletePayload(cipher)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncFolderCreateAsync_SendsExpectedResponse()\n    {\n        var folder = new Folder\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncFolderCreateAsync(folder),\n            GetPushSyncFolderCreatePayload(folder)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncFolderUpdateAsync_SendsExpectedResponse()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var folder = new Folder\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncFolderUpdateAsync(folder),\n            GetPushSyncFolderUpdatePayload(folder)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncFolderDeleteAsync_SendsExpectedResponse()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var folder = new Folder\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncFolderDeleteAsync(folder),\n            GetPushSyncFolderDeletePayload(folder)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncCiphersAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCiphersAsync(userId),\n            GetPushSyncCiphersPayload(userId)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncVaultAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncVaultAsync(userId),\n            GetPushSyncVaultPayload(userId)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncOrganizationsAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncOrganizationsAsync(userId),\n            GetPushSyncOrganizationsPayload(userId)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncOrgKeysAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncOrgKeysAsync(userId),\n            GetPushSyncOrgKeysPayload(userId)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSettingsAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSettingsAsync(userId),\n            GetPushSyncSettingsPayload(userId)\n        );\n    }\n\n    [Theory]\n    [InlineData(true, null)]\n    [InlineData(true, PushNotificationLogOutReason.KdfChange)]\n    [InlineData(false, null)]\n    [InlineData(false, PushNotificationLogOutReason.KdfChange)]\n    public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext,\n        PushNotificationLogOutReason? reason)\n    {\n        var userId = Guid.NewGuid();\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason),\n            GetPushLogOutPayload(userId, excludeCurrentContext, reason)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSendCreateAsync_SendsExpectedResponse()\n    {\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSendCreateAsync(send),\n            GetPushSendCreatePayload(send)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSendUpdateAsync_SendsExpectedResponse()\n    {\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSendUpdateAsync(send),\n            GetPushSendUpdatePayload(send)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSendDeleteAsync_SendsExpectedResponse()\n    {\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSendDeleteAsync(send),\n            GetPushSendDeletePayload(send)\n        );\n    }\n\n    [Fact]\n    public async Task PushAuthRequestAsync_SendsExpectedResponse()\n    {\n        var authRequest = new AuthRequest\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushAuthRequestAsync(authRequest),\n            GetPushAuthRequestPayload(authRequest)\n        );\n    }\n\n    [Fact]\n    public async Task PushAuthRequestResponseAsync_SendsExpectedResponse()\n    {\n        var authRequest = new AuthRequest\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushAuthRequestResponseAsync(authRequest),\n            GetPushAuthRequestResponsePayload(authRequest)\n        );\n    }\n\n    [Theory]\n    [InlineData(true, null, null)]\n    [InlineData(false, \"e8e08ce8-8a26-4a65-913a-ba1d8c478b2f\", null)]\n    [InlineData(false, null, \"2f53ee32-edf9-4169-b276-760fe92e03bf\")]\n    public async Task PushNotificationAsync_SendsExpectedResponse(bool global, string? userId, string? organizationId)\n    {\n        var notification = new Notification\n        {\n            Id = Guid.NewGuid(),\n            Priority = Priority.High,\n            Global = global,\n            ClientType = ClientType.All,\n            UserId = userId != null ? Guid.Parse(userId) : null,\n            OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null,\n            TaskId = Guid.NewGuid(),\n            Title = \"My Title\",\n            Body = \"My Body\",\n            CreationDate = DateTime.UtcNow.AddDays(-1),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushNotificationAsync(notification),\n            GetPushNotificationResponsePayload(notification, notification.UserId, notification.OrganizationId)\n        );\n    }\n\n    [Theory]\n    [InlineData(true, null, null)]\n    [InlineData(false, \"e8e08ce8-8a26-4a65-913a-ba1d8c478b2f\", null)]\n    [InlineData(false, null, \"2f53ee32-edf9-4169-b276-760fe92e03bf\")]\n    public async Task PushNotificationStatusAsync_SendsExpectedResponse(bool global, string? userId, string? organizationId)\n    {\n        var notification = new Notification\n        {\n            Id = Guid.NewGuid(),\n            Priority = Priority.High,\n            Global = global,\n            ClientType = ClientType.All,\n            UserId = userId != null ? Guid.Parse(userId) : null,\n            OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null,\n            TaskId = Guid.NewGuid(),\n            Title = \"My Title\",\n            Body = \"My Body\",\n            CreationDate = DateTime.UtcNow.AddDays(-1),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var notificationStatus = new NotificationStatus\n        {\n            ReadDate = DateTime.UtcNow,\n            DeletedDate = DateTime.UtcNow,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushNotificationStatusAsync(notification, notificationStatus),\n            GetPushNotificationStatusResponsePayload(notification, notificationStatus, notification.UserId, notification.OrganizationId)\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse()\n    {\n        var organization = new Organization\n        {\n            Id = Guid.NewGuid(),\n            Enabled = true,\n            LimitCollectionCreation = true,\n            LimitCollectionDeletion = true,\n            LimitItemDeletion = true,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncOrganizationCollectionManagementSettingsAsync(organization),\n            GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(organization)\n        );\n    }\n\n    [Fact]\n    public async Task PushRefreshSecurityTasksAsync_SendsExpectedResponse()\n    {\n        var userId = Guid.NewGuid();\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushRefreshSecurityTasksAsync(userId),\n            GetPushRefreshSecurityTasksResponsePayload(userId)\n        );\n    }\n\n    private async Task VerifyNotificationAsync(\n        Func<IPushNotificationService, Task> test,\n        JsonNode expectedRequestBody\n    )\n    {\n        var httpContext = new DefaultHttpContext();\n\n        var serviceCollection = new ServiceCollection();\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.DeviceIdentifier = DeviceIdentifier;\n        serviceCollection.AddSingleton(currentContext);\n\n        httpContext.RequestServices = serviceCollection.BuildServiceProvider();\n\n        HttpContextAccessor.HttpContext\n            .Returns(httpContext);\n\n        var connectTokenRequest = MockIdentityClient\n            .Expect(HttpMethod.Post, \"https://localhost:8888/connect/token\")\n            .Respond(HttpStatusCode.OK, JsonContent.Create(new\n            {\n                access_token = CreateAccessToken(DateTime.UtcNow.AddDays(1)),\n            }));\n\n        JsonNode actualNode = null;\n\n        var clientRequest = MockClient\n            .Expect(HttpMethod.Post, ExpectedClientUrl())\n            .With(request =>\n            {\n                if (request.Content is not JsonContent jsonContent)\n                {\n                    return false;\n                }\n\n                // TODO: What options?\n                var actualString = JsonSerializer.Serialize(jsonContent.Value);\n                actualNode = JsonNode.Parse(actualString);\n\n                return JsonNode.DeepEquals(actualNode, expectedRequestBody);\n            })\n            .Respond(HttpStatusCode.OK);\n\n        await test(new EngineWrapper(CreateService(), FakeTimeProvider, GlobalSettings.Installation.Id));\n\n        Assert.NotNull(actualNode);\n\n        Assert.Equal(expectedRequestBody, actualNode, EqualityComparer<JsonNode>.Create(JsonNode.DeepEquals));\n\n        Assert.Equal(1, MockClient.GetMatchCount(clientRequest));\n    }\n\n    protected static string CreateAccessToken(DateTime expirationTime)\n    {\n        var tokenHandler = new JwtSecurityTokenHandler();\n        var token = new JwtSecurityToken(expires: expirationTime);\n        return tokenHandler.WriteToken(token);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs",
    "content": "﻿#nullable enable\n\nusing System.Text.Json.Nodes;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Repositories;\nusing Bit.Core.Settings;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\n\nnamespace Bit.Core.Test.Platform.Push.Engines;\n\npublic class RelayPushNotificationServiceTests : PushTestBase\n{\n    private static readonly Guid _deviceId = Guid.Parse(\"c4730f80-caaa-4772-97bd-5c0d23a2baa3\");\n    private readonly IDeviceRepository _deviceRepository;\n\n    public RelayPushNotificationServiceTests()\n    {\n        _deviceRepository = Substitute.For<IDeviceRepository>();\n\n        _deviceRepository.GetByIdentifierAsync(DeviceIdentifier)\n            .Returns(new Device\n            {\n                Id = _deviceId,\n            });\n\n        GlobalSettings.PushRelayBaseUri = \"https://localhost:7777\";\n        GlobalSettings.Installation.Id = Guid.Parse(\"478c608a-99fd-452a-94f0-af271654e6ee\");\n        GlobalSettings.Installation.IdentityUri = \"https://localhost:8888\";\n    }\n\n    protected override IPushEngine CreateService()\n    {\n        return new RelayPushEngine(\n            HttpClientFactory,\n            _deviceRepository,\n            GlobalSettings,\n            HttpContextAccessor,\n            NullLogger<RelayPushEngine>.Instance\n        );\n    }\n\n    protected override string ExpectedClientUrl() => \"https://localhost:7777/push/send\";\n\n    protected override JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid collectionIds)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = cipher.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 1,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = cipher.Id,\n                [\"UserId\"] = cipher.UserId,\n                [\"OrganizationId\"] = null,\n                // Currently CollectionIds are not passed along from the method signature\n                // to the request body.\n                [\"CollectionIds\"] = null,\n                [\"RevisionDate\"] = cipher.RevisionDate,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncCipherUpdatePayload(Cipher cipher, Guid collectionIds)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = cipher.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 0,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = cipher.Id,\n                [\"UserId\"] = cipher.UserId,\n                [\"OrganizationId\"] = null,\n                // Currently CollectionIds are not passed along from the method signature\n                // to the request body.\n                [\"CollectionIds\"] = null,\n                [\"RevisionDate\"] = cipher.RevisionDate,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncCipherDeletePayload(Cipher cipher)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = cipher.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 2,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = cipher.Id,\n                [\"UserId\"] = cipher.UserId,\n                [\"OrganizationId\"] = null,\n                [\"CollectionIds\"] = null,\n                [\"RevisionDate\"] = cipher.RevisionDate,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncFolderCreatePayload(Folder folder)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = folder.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 7,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = folder.Id,\n                [\"UserId\"] = folder.UserId,\n                [\"RevisionDate\"] = folder.RevisionDate,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncFolderUpdatePayload(Folder folder)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = folder.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 8,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = folder.Id,\n                [\"UserId\"] = folder.UserId,\n                [\"RevisionDate\"] = folder.RevisionDate,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncFolderDeletePayload(Folder folder)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = folder.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 3,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = folder.Id,\n                [\"UserId\"] = folder.UserId,\n                [\"RevisionDate\"] = folder.RevisionDate,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncCiphersPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = null,\n            [\"Type\"] = 4,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncVaultPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = null,\n            [\"Type\"] = 5,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncOrganizationsPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = null,\n            [\"Type\"] = 17,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncOrgKeysPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = null,\n            [\"Type\"] = 6,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushSyncSettingsPayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = null,\n            [\"Type\"] = 10,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext,\n        PushNotificationLogOutReason? reason)\n    {\n        JsonNode? identifier = excludeCurrentContext ? DeviceIdentifier : null;\n\n        return new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = identifier,\n            [\"Type\"] = 11,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Reason\"] = reason != null ? (int)reason : null\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n    protected override JsonNode GetPushSendCreatePayload(Send send)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = send.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 12,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = send.Id,\n                [\"UserId\"] = send.UserId,\n                [\"RevisionDate\"] = send.RevisionDate,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n    protected override JsonNode GetPushSendUpdatePayload(Send send)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = send.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 13,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = send.Id,\n                [\"UserId\"] = send.UserId,\n                [\"RevisionDate\"] = send.RevisionDate,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n    protected override JsonNode GetPushSendDeletePayload(Send send)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = send.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 14,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = send.Id,\n                [\"UserId\"] = send.UserId,\n                [\"RevisionDate\"] = send.RevisionDate,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n    protected override JsonNode GetPushAuthRequestPayload(AuthRequest authRequest)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = authRequest.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 15,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = authRequest.Id,\n                [\"UserId\"] = authRequest.UserId,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n    protected override JsonNode GetPushAuthRequestResponsePayload(AuthRequest authRequest)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = authRequest.UserId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 16,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = authRequest.Id,\n                [\"UserId\"] = authRequest.UserId,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n    protected override JsonNode GetPushNotificationResponsePayload(Notification notification, Guid? userId, Guid? organizationId)\n    {\n        JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null;\n\n        return new JsonObject\n        {\n            [\"UserId\"] = notification.UserId,\n            [\"OrganizationId\"] = notification.OrganizationId,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 20,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = notification.Id,\n                [\"Priority\"] = 3,\n                [\"Global\"] = notification.Global,\n                [\"ClientType\"] = 0,\n                [\"UserId\"] = userId,\n                [\"OrganizationId\"] = organizationId,\n                [\"TaskId\"] = notification.TaskId,\n                [\"InstallationId\"] = installationId,\n                [\"Title\"] = notification.Title,\n                [\"Body\"] = notification.Body,\n                [\"CreationDate\"] = notification.CreationDate,\n                [\"RevisionDate\"] = notification.RevisionDate,\n                [\"ReadDate\"] = null,\n                [\"DeletedDate\"] = null,\n            },\n            [\"ClientType\"] = 0,\n            [\"InstallationId\"] = installationId?.DeepClone(),\n        };\n    }\n    protected override JsonNode GetPushNotificationStatusResponsePayload(Notification notification, NotificationStatus notificationStatus, Guid? userId, Guid? organizationId)\n    {\n        JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null;\n\n        return new JsonObject\n        {\n            [\"UserId\"] = notification.UserId,\n            [\"OrganizationId\"] = notification.OrganizationId,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = DeviceIdentifier,\n            [\"Type\"] = 21,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"Id\"] = notification.Id,\n                [\"Priority\"] = 3,\n                [\"Global\"] = notification.Global,\n                [\"ClientType\"] = 0,\n                [\"UserId\"] = notification.UserId,\n                [\"OrganizationId\"] = notification.OrganizationId,\n                [\"InstallationId\"] = installationId,\n                [\"TaskId\"] = notification.TaskId,\n                [\"Title\"] = notification.Title,\n                [\"Body\"] = notification.Body,\n                [\"CreationDate\"] = notification.CreationDate,\n                [\"RevisionDate\"] = notification.RevisionDate,\n                [\"ReadDate\"] = notificationStatus.ReadDate,\n                [\"DeletedDate\"] = notificationStatus.DeletedDate,\n            },\n            [\"ClientType\"] = 0,\n            [\"InstallationId\"] = installationId?.DeepClone(),\n        };\n    }\n    protected override JsonNode GetPushSyncOrganizationStatusResponsePayload(Organization organization)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = null,\n            [\"OrganizationId\"] = organization.Id,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = null,\n            [\"Type\"] = 18,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"OrganizationId\"] = organization.Id,\n                [\"Enabled\"] = organization.Enabled,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n    protected override JsonNode GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(Organization organization)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = null,\n            [\"OrganizationId\"] = organization.Id,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = null,\n            [\"Type\"] = 19,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"OrganizationId\"] = organization.Id,\n                [\"LimitCollectionCreation\"] = organization.LimitCollectionCreation,\n                [\"LimitCollectionDeletion\"] = organization.LimitCollectionDeletion,\n                [\"LimitItemDeletion\"] = organization.LimitItemDeletion,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n\n    protected override JsonNode GetPushRefreshSecurityTasksResponsePayload(Guid userId)\n    {\n        return new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"OrganizationId\"] = null,\n            [\"DeviceId\"] = _deviceId,\n            [\"Identifier\"] = null,\n            [\"Type\"] = 22,\n            [\"Payload\"] = new JsonObject\n            {\n                [\"UserId\"] = userId,\n                [\"Date\"] = FakeTimeProvider.GetUtcNow().UtcDateTime,\n            },\n            [\"ClientType\"] = null,\n            [\"InstallationId\"] = null,\n        };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/MultiServicePushNotificationServiceTests.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Push;\n\npublic class MultiServicePushNotificationServiceTests\n{\n    private readonly IPushEngine _fakeEngine1;\n    private readonly IPushEngine _fakeEngine2;\n\n    private readonly MultiServicePushNotificationService _sut;\n\n    public MultiServicePushNotificationServiceTests()\n    {\n        _fakeEngine1 = Substitute.For<IPushEngine>();\n        _fakeEngine2 = Substitute.For<IPushEngine>();\n\n        _sut = new MultiServicePushNotificationService(\n            [_fakeEngine1, _fakeEngine2],\n            NullLogger<MultiServicePushNotificationService>.Instance,\n            new GlobalSettings(),\n            new FakeTimeProvider()\n        );\n    }\n\n#if DEBUG // This test requires debug code in the sut to work properly\n    [Fact]\n    public async Task PushAsync_CallsAllEngines()\n    {\n        var notification = new PushNotification<object>\n        {\n            Target = NotificationTarget.User,\n            TargetId = Guid.NewGuid(),\n            Type = PushType.AuthRequest,\n            Payload = new { },\n            ExcludeCurrentContext = false,\n        };\n\n        await _sut.PushAsync(notification);\n\n        await _fakeEngine1\n            .Received(1)\n            .PushAsync(Arg.Is<PushNotification<object>>(n => ReferenceEquals(n, notification)));\n\n        await _fakeEngine2\n            .Received(1)\n            .PushAsync(Arg.Is<PushNotification<object>>(n => ReferenceEquals(n, notification)));\n    }\n\n#endif\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/NotificationHub/NotificationHubConnectionTests.cs",
    "content": "﻿using Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Push.NotificationHub;\n\npublic class NotificationHubConnectionTests\n{\n    [Fact]\n    public void IsValid_ConnectionStringIsNull_ReturnsFalse()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = null,\n            HubName = \"hub\",\n            RegistrationStartDate = DateTime.UtcNow,\n            RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n        };\n\n        // Act\n        var connection = NotificationHubConnection.From(hub);\n\n        // Assert\n        Assert.False(connection.IsValid);\n    }\n\n    [Fact]\n    public void IsValid_HubNameIsNull_ReturnsFalse()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = \"Endpoint=sb://example.servicebus.windows.net/;\",\n            HubName = null,\n            RegistrationStartDate = DateTime.UtcNow,\n            RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n        };\n\n        // Act\n        var connection = NotificationHubConnection.From(hub);\n\n        // Assert\n        Assert.False(connection.IsValid);\n    }\n\n    [Fact]\n    public void IsValid_ConnectionStringAndHubNameAreNotNull_ReturnsTrue()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = \"connection\",\n            HubName = \"hub\",\n            RegistrationStartDate = DateTime.UtcNow,\n            RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n        };\n\n        // Act\n        var connection = NotificationHubConnection.From(hub);\n\n        // Assert\n        Assert.True(connection.IsValid);\n    }\n\n    [Fact]\n    public void RegistrationEnabled_QueryTimeIsBeforeStartDate_ReturnsFalse()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = \"connection\",\n            HubName = \"hub\",\n            RegistrationStartDate = DateTime.UtcNow.AddDays(1),\n            RegistrationEndDate = DateTime.UtcNow.AddDays(2)\n        };\n        var connection = NotificationHubConnection.From(hub);\n\n        // Act\n        var result = connection.RegistrationEnabled(DateTime.UtcNow);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void RegistrationEnabled_QueryTimeIsAfterEndDate_ReturnsFalse()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = \"connection\",\n            HubName = \"hub\",\n            RegistrationStartDate = DateTime.UtcNow,\n            RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n        };\n        var connection = NotificationHubConnection.From(hub);\n\n        // Act\n        var result = connection.RegistrationEnabled(DateTime.UtcNow.AddDays(2));\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void RegistrationEnabled_NullStartDate_ReturnsFalse()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = \"connection\",\n            HubName = \"hub\",\n            RegistrationStartDate = null,\n            RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n        };\n        var connection = NotificationHubConnection.From(hub);\n\n        // Act\n        var result = connection.RegistrationEnabled(DateTime.UtcNow);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void RegistrationEnabled_QueryTimeIsBetweenStartDateAndEndDate_ReturnsTrue()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = \"connection\",\n            HubName = \"hub\",\n            RegistrationStartDate = DateTime.UtcNow,\n            RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n        };\n        var connection = NotificationHubConnection.From(hub);\n\n        // Act\n        var result = connection.RegistrationEnabled(DateTime.UtcNow.AddHours(1));\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Fact]\n    public void RegistrationEnabled_CombTimeIsBeforeStartDate_ReturnsFalse()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = \"connection\",\n            HubName = \"hub\",\n            RegistrationStartDate = DateTime.UtcNow.AddDays(1),\n            RegistrationEndDate = DateTime.UtcNow.AddDays(2)\n        };\n        var connection = NotificationHubConnection.From(hub);\n\n        // Act\n        var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow));\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void RegistrationEnabled_CombTimeIsAfterEndDate_ReturnsFalse()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = \"connection\",\n            HubName = \"hub\",\n            RegistrationStartDate = DateTime.UtcNow,\n            RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n        };\n        var connection = NotificationHubConnection.From(hub);\n\n        // Act\n        var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow.AddDays(2)));\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void RegistrationEnabled_CombTimeIsBetweenStartDateAndEndDate_ReturnsTrue()\n    {\n        // Arrange\n        var hub = new GlobalSettings.NotificationHubSettings()\n        {\n            ConnectionString = \"connection\",\n            HubName = \"hub\",\n            RegistrationStartDate = DateTime.UtcNow,\n            RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n        };\n        var connection = NotificationHubConnection.From(hub);\n\n        // Act\n        var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow.AddHours(1)));\n\n        // Assert\n        Assert.True(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/NotificationHub/NotificationHubPoolTests.cs",
    "content": "﻿using Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\nusing static Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.Platform.Push.NotificationHub;\n\npublic class NotificationHubPoolTests\n{\n    [Fact]\n    public void NotificationHubPool_WarnsOnMissingConnectionString()\n    {\n        // Arrange\n        var globalSettings = new GlobalSettings()\n        {\n            NotificationHubPool = new NotificationHubPoolSettings()\n            {\n                NotificationHubs = new() {\n                    new() {\n                        ConnectionString = null,\n                        HubName = \"hub\",\n                        RegistrationStartDate = DateTime.UtcNow,\n                        RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n                    }\n                }\n            }\n        };\n        var logger = Substitute.For<ILogger<NotificationHubPool>>();\n\n        // Act\n        var sut = new NotificationHubPool(logger, globalSettings);\n\n        // Assert\n        logger.Received().Log(LogLevel.Warning, Arg.Any<EventId>(),\n            Arg.Is<object>(o => o.ToString() == \"Invalid notification hub settings: hub\"),\n            null,\n            Arg.Any<Func<object, Exception, string>>());\n    }\n\n    [Fact]\n    public void NotificationHubPool_WarnsOnMissingHubName()\n    {\n        // Arrange\n        var globalSettings = new GlobalSettings()\n        {\n            NotificationHubPool = new NotificationHubPoolSettings()\n            {\n                NotificationHubs = new() {\n                    new() {\n                        ConnectionString = \"connection\",\n                        HubName = null,\n                        RegistrationStartDate = DateTime.UtcNow,\n                        RegistrationEndDate = DateTime.UtcNow.AddDays(1)\n                    }\n                }\n            }\n        };\n        var logger = Substitute.For<ILogger<NotificationHubPool>>();\n\n        // Act\n        var sut = new NotificationHubPool(logger, globalSettings);\n\n        // Assert\n        logger.Received().Log(LogLevel.Warning, Arg.Any<EventId>(),\n            Arg.Is<object>(o => o.ToString() == \"Invalid notification hub settings: hub name missing\"),\n            null,\n            Arg.Any<Func<object, Exception, string>>());\n    }\n\n    [Fact]\n    public void NotificationHubPool_ClientFor_ThrowsOnNoValidHubs()\n    {\n        // Arrange\n        var globalSettings = new GlobalSettings()\n        {\n            NotificationHubPool = new NotificationHubPoolSettings()\n            {\n                NotificationHubs = new() {\n                    new() {\n                        ConnectionString = \"connection\",\n                        HubName = \"hub\",\n                        RegistrationStartDate = null,\n                        RegistrationEndDate = null,\n                    }\n                }\n            }\n        };\n        var logger = Substitute.For<ILogger<NotificationHubPool>>();\n        var sut = new NotificationHubPool(logger, globalSettings);\n\n        // Act\n        Action act = () => sut.ClientFor(Guid.NewGuid());\n\n        // Assert\n        Assert.Throws<InvalidOperationException>(act);\n    }\n\n    [Fact]\n    public void NotificationHubPool_ClientFor_ReturnsClient()\n    {\n        // Arrange\n        var globalSettings = new GlobalSettings()\n        {\n            NotificationHubPool = new NotificationHubPoolSettings()\n            {\n                NotificationHubs = new() {\n                    new() {\n                        ConnectionString = \"Endpoint=sb://example.servicebus.windows.net/;SharedAccessKey=example///example=\",\n                        HubName = \"hub\",\n                        RegistrationStartDate = DateTime.UtcNow.AddMinutes(-1),\n                        RegistrationEndDate = DateTime.UtcNow.AddDays(1),\n                    }\n                }\n            }\n        };\n        var logger = Substitute.For<ILogger<NotificationHubPool>>();\n        var sut = new NotificationHubPool(logger, globalSettings);\n\n        // Act\n        var client = sut.ClientFor(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow));\n\n        // Assert\n        Assert.NotNull(client);\n    }\n\n    [Fact]\n    public void NotificationHubPool_AllClients_ReturnsProxy()\n    {\n        // Arrange\n        var globalSettings = new GlobalSettings()\n        {\n            NotificationHubPool = new NotificationHubPoolSettings()\n            {\n                NotificationHubs = new() {\n                    new() {\n                        ConnectionString = \"connection\",\n                        HubName = \"hub\",\n                        RegistrationStartDate = DateTime.UtcNow,\n                        RegistrationEndDate = DateTime.UtcNow.AddDays(1),\n                    }\n                }\n            }\n        };\n        var logger = Substitute.For<ILogger<NotificationHubPool>>();\n        var sut = new NotificationHubPool(logger, globalSettings);\n\n        // Act\n        var proxy = sut.AllClients;\n\n        // Assert\n        Assert.NotNull(proxy);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/NotificationHub/NotificationHubProxyTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Test.Common.AutoFixture;\nusing Microsoft.Azure.NotificationHubs;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Push.NotificationHub;\n\npublic class NotificationHubProxyTests\n{\n    private readonly IEnumerable<INotificationHubClient> _clients;\n    public NotificationHubProxyTests()\n    {\n        _clients = new Fixture().WithAutoNSubstitutions().CreateMany<INotificationHubClient>();\n    }\n\n    public static IEnumerable<object[]> ClientMethods =\n    [\n        [\n            (NotificationHubClientProxy c) => c.SendTemplateNotificationAsync(new Dictionary<string, string>() { { \"key\", \"value\" } }, \"tag\"),\n            (INotificationHubClient c) => c.SendTemplateNotificationAsync(Arg.Is<Dictionary<string, string>>((a) => a.Keys.Count == 1 && a.ContainsKey(\"key\") && a[\"key\"] == \"value\"), \"tag\"),\n        ],\n    ];\n\n    [Theory]\n    [MemberData(nameof(ClientMethods))]\n    public async void CallsAllClients(Func<NotificationHubClientProxy, Task> proxyMethod, Func<INotificationHubClient, Task> clientMethod)\n    {\n        var clients = _clients.ToArray();\n        var proxy = new NotificationHubClientProxy(clients);\n\n        await proxyMethod(proxy);\n\n        foreach (var client in clients)\n        {\n            await clientMethod(client.Received());\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.Json.Nodes;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Context;\nusing Bit.Core.Enums;\nusing Bit.Core.Models;\nusing Bit.Core.NotificationCenter.Entities;\nusing Bit.Core.NotificationCenter.Enums;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Repositories;\nusing Bit.Core.Test.NotificationCenter.AutoFixture;\nusing Bit.Core.Test.Platform.Push.Engines;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Vault.Entities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Microsoft.Extensions.Time.Testing;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Push.NotificationHub;\n\n[SutProviderCustomize]\n[NotificationStatusCustomize]\npublic class NotificationHubPushNotificationServiceTests\n{\n    private static readonly string _deviceIdentifier = \"test_device_identifier\";\n    private static readonly DateTime _now = DateTime.UtcNow;\n    private static readonly Guid _installationId = Guid.Parse(\"da73177b-513f-4444-b582-595c890e1022\");\n\n    [Fact]\n    public async Task PushSyncCipherCreateAsync_SendExpectedData()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var userId = Guid.NewGuid();\n\n        var cipher = new Cipher\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = cipher.Id,\n            [\"UserId\"] = cipher.UserId,\n            [\"OrganizationId\"] = cipher.OrganizationId,\n            [\"CollectionIds\"] = new JsonArray(collectionId),\n            [\"RevisionDate\"] = cipher.RevisionDate,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCipherCreateAsync(cipher, [collectionId]),\n            PushType.SyncCipherCreate,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncCipherUpdateAsync_SendExpectedData()\n    {\n        var collectionId = Guid.NewGuid();\n\n        var userId = Guid.NewGuid();\n\n        var cipher = new Cipher\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = cipher.Id,\n            [\"UserId\"] = cipher.UserId,\n            [\"OrganizationId\"] = cipher.OrganizationId,\n            [\"CollectionIds\"] = new JsonArray(collectionId),\n            [\"RevisionDate\"] = cipher.RevisionDate,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCipherUpdateAsync(cipher, [collectionId]),\n            PushType.SyncCipherUpdate,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncCipherDeleteAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var cipher = new Cipher\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = cipher.Id,\n            [\"UserId\"] = cipher.UserId,\n            [\"OrganizationId\"] = cipher.OrganizationId,\n            [\"CollectionIds\"] = null,\n            [\"RevisionDate\"] = cipher.RevisionDate,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCipherDeleteAsync(cipher),\n            PushType.SyncLoginDelete,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncFolderCreateAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var folder = new Folder\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = folder.Id,\n            [\"UserId\"] = folder.UserId,\n            [\"RevisionDate\"] = folder.RevisionDate,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncFolderCreateAsync(folder),\n            PushType.SyncFolderCreate,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncFolderUpdateAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var folder = new Folder\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = folder.Id,\n            [\"UserId\"] = folder.UserId,\n            [\"RevisionDate\"] = folder.RevisionDate,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncFolderUpdateAsync(folder),\n            PushType.SyncFolderUpdate,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSendCreateAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = send.Id,\n            [\"UserId\"] = send.UserId,\n            [\"RevisionDate\"] = send.RevisionDate,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSendCreateAsync(send),\n            PushType.SyncSendCreate,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushAuthRequestAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var authRequest = new AuthRequest\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = authRequest.Id,\n            [\"UserId\"] = authRequest.UserId,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushAuthRequestAsync(authRequest),\n            PushType.AuthRequest,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushAuthRequestResponseAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var authRequest = new AuthRequest\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = authRequest.Id,\n            [\"UserId\"] = authRequest.UserId,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushAuthRequestResponseAsync(authRequest),\n            PushType.AuthRequestResponse,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSendUpdateAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = send.Id,\n            [\"UserId\"] = send.UserId,\n            [\"RevisionDate\"] = send.RevisionDate,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSendUpdateAsync(send),\n            PushType.SyncSendUpdate,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSendDeleteAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = send.Id,\n            [\"UserId\"] = send.UserId,\n            [\"RevisionDate\"] = send.RevisionDate,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSendDeleteAsync(send),\n            PushType.SyncSendDelete,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncCiphersAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"Date\"] = _now,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncCiphersAsync(userId),\n            PushType.SyncCiphers,\n            expectedPayload,\n            $\"(template:payload_userId:{userId})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncVaultAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"Date\"] = _now,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncVaultAsync(userId),\n            PushType.SyncVault,\n            expectedPayload,\n            $\"(template:payload_userId:{userId})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncOrganizationsAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"Date\"] = _now,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncOrganizationsAsync(userId),\n            PushType.SyncOrganizations,\n            expectedPayload,\n            $\"(template:payload_userId:{userId})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncOrgKeysAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"Date\"] = _now,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncOrgKeysAsync(userId),\n            PushType.SyncOrgKeys,\n            expectedPayload,\n            $\"(template:payload_userId:{userId})\"\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncSettingsAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"Date\"] = _now,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncSettingsAsync(userId),\n            PushType.SyncSettings,\n            expectedPayload,\n            $\"(template:payload_userId:{userId})\"\n        );\n    }\n\n    [Theory]\n    [InlineData(true, null)]\n    [InlineData(true, PushNotificationLogOutReason.KdfChange)]\n    [InlineData(false, null)]\n    [InlineData(false, PushNotificationLogOutReason.KdfChange)]\n    public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext, PushNotificationLogOutReason? reason)\n    {\n        var userId = Guid.NewGuid();\n\n        var expectedPayload = new JsonObject\n        {\n            [\"UserId\"] = userId,\n            [\"Reason\"] = reason != null ? (int)reason : null,\n        };\n\n        var expectedTag = excludeCurrentContext\n            ? $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n            : $\"(template:payload_userId:{userId})\";\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason),\n            PushType.LogOut,\n            expectedPayload,\n            expectedTag\n        );\n    }\n\n    [Fact]\n    public async Task PushSyncFolderDeleteAsync_SendExpectedData()\n    {\n        var userId = Guid.NewGuid();\n\n        var folder = new Folder\n        {\n            Id = Guid.NewGuid(),\n            UserId = userId,\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = folder.Id,\n            [\"UserId\"] = folder.UserId,\n            [\"RevisionDate\"] = folder.RevisionDate,\n        };\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushSyncFolderDeleteAsync(folder),\n            PushType.SyncFolderDelete,\n            expectedPayload,\n            $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\"\n        );\n    }\n\n    [Theory]\n    [InlineData(true, null, null)]\n    [InlineData(false, \"e8e08ce8-8a26-4a65-913a-ba1d8c478b2f\", null)]\n    [InlineData(false, null, \"2f53ee32-edf9-4169-b276-760fe92e03bf\")]\n    public async Task PushNotificationAsync_SendExpectedData(bool global, string? userId, string? organizationId)\n    {\n        var notification = new Notification\n        {\n            Id = Guid.NewGuid(),\n            Priority = Priority.High,\n            Global = global,\n            ClientType = ClientType.All,\n            UserId = userId != null ? Guid.Parse(userId) : null,\n            OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null,\n            TaskId = Guid.NewGuid(),\n            Title = \"My Title\",\n            Body = \"My Body\",\n            CreationDate = DateTime.UtcNow.AddDays(-1),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        JsonNode? installationId = global ? _installationId : null;\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = notification.Id,\n            [\"Priority\"] = 3,\n            [\"Global\"] = global,\n            [\"ClientType\"] = 0,\n            [\"UserId\"] = notification.UserId,\n            [\"OrganizationId\"] = notification.OrganizationId,\n            [\"TaskId\"] = notification.TaskId,\n            [\"InstallationId\"] = installationId,\n            [\"Title\"] = notification.Title,\n            [\"Body\"] = notification.Body,\n            [\"CreationDate\"] = notification.CreationDate,\n            [\"RevisionDate\"] = notification.RevisionDate,\n            [\"ReadDate\"] = null,\n            [\"DeletedDate\"] = null,\n        };\n\n        string expectedTag;\n\n        if (global)\n        {\n            expectedTag = $\"(template:payload && installationId:{_installationId} && !deviceIdentifier:{_deviceIdentifier})\";\n        }\n        else if (notification.OrganizationId.HasValue)\n        {\n            expectedTag = \"(template:payload && organizationId:2f53ee32-edf9-4169-b276-760fe92e03bf && !deviceIdentifier:test_device_identifier)\";\n        }\n        else\n        {\n            expectedTag = $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\";\n        }\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushNotificationAsync(notification),\n            PushType.Notification,\n            expectedPayload,\n            expectedTag\n        );\n    }\n\n    [Theory]\n    [InlineData(true, null, null)]\n    [InlineData(false, \"e8e08ce8-8a26-4a65-913a-ba1d8c478b2f\", null)]\n    [InlineData(false, null, \"2f53ee32-edf9-4169-b276-760fe92e03bf\")]\n    public async Task PushNotificationStatusAsync_SendExpectedData(bool global, string? userId, string? organizationId)\n    {\n        var notification = new Notification\n        {\n            Id = Guid.NewGuid(),\n            Priority = Priority.High,\n            Global = global,\n            ClientType = ClientType.All,\n            UserId = userId != null ? Guid.Parse(userId) : null,\n            OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null,\n            Title = \"My Title\",\n            Body = \"My Body\",\n            CreationDate = DateTime.UtcNow.AddDays(-1),\n            RevisionDate = DateTime.UtcNow,\n        };\n\n        var notificationStatus = new NotificationStatus\n        {\n            ReadDate = DateTime.UtcNow.AddDays(-1),\n            DeletedDate = DateTime.UtcNow,\n        };\n\n        JsonNode? installationId = global ? _installationId : null;\n\n        var expectedPayload = new JsonObject\n        {\n            [\"Id\"] = notification.Id,\n            [\"Priority\"] = 3,\n            [\"Global\"] = global,\n            [\"ClientType\"] = 0,\n            [\"UserId\"] = notification.UserId,\n            [\"OrganizationId\"] = notification.OrganizationId,\n            [\"TaskId\"] = notification.TaskId,\n            [\"InstallationId\"] = installationId,\n            [\"Title\"] = notification.Title,\n            [\"Body\"] = notification.Body,\n            [\"CreationDate\"] = notification.CreationDate,\n            [\"RevisionDate\"] = notification.RevisionDate,\n            [\"ReadDate\"] = notificationStatus.ReadDate,\n            [\"DeletedDate\"] = notificationStatus.DeletedDate,\n        };\n\n        string expectedTag;\n\n        if (global)\n        {\n            expectedTag = $\"(template:payload && installationId:{_installationId} && !deviceIdentifier:{_deviceIdentifier})\";\n        }\n        else if (notification.OrganizationId.HasValue)\n        {\n            expectedTag = \"(template:payload && organizationId:2f53ee32-edf9-4169-b276-760fe92e03bf && !deviceIdentifier:test_device_identifier)\";\n        }\n        else\n        {\n            expectedTag = $\"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})\";\n        }\n\n        await VerifyNotificationAsync(\n            async sut => await sut.PushNotificationStatusAsync(notification, notificationStatus),\n            PushType.NotificationStatus,\n            expectedPayload,\n            expectedTag\n        );\n    }\n\n    private async Task VerifyNotificationAsync(Func<IPushNotificationService, Task> test,\n        PushType type, JsonNode expectedPayload, string tag)\n    {\n        var installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();\n\n        var notificationHubPool = Substitute.For<INotificationHubPool>();\n\n        var notificationHubProxy = Substitute.For<INotificationHubProxy>();\n\n        notificationHubPool.AllClients\n            .Returns(notificationHubProxy);\n\n        var httpContextAccessor = Substitute.For<IHttpContextAccessor>();\n\n        var httpContext = new DefaultHttpContext();\n\n        var serviceCollection = new ServiceCollection();\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.DeviceIdentifier = _deviceIdentifier;\n        serviceCollection.AddSingleton(currentContext);\n\n        httpContext.RequestServices = serviceCollection.BuildServiceProvider();\n\n        httpContextAccessor.HttpContext\n            .Returns(httpContext);\n\n        var globalSettings = new Core.Settings.GlobalSettings();\n        globalSettings.Installation.Id = _installationId;\n\n        var fakeTimeProvider = new FakeTimeProvider();\n\n        fakeTimeProvider.SetUtcNow(_now);\n\n        var sut = new NotificationHubPushEngine(\n            installationDeviceRepository,\n            notificationHubPool,\n            httpContextAccessor,\n            NullLogger<NotificationHubPushEngine>.Instance,\n            globalSettings\n        );\n\n        // Act\n        await test(new EngineWrapper(sut, fakeTimeProvider, _installationId));\n\n        // Assert\n        var calls = notificationHubProxy.ReceivedCalls();\n        var methodInfo = typeof(INotificationHubProxy).GetMethod(nameof(INotificationHubProxy.SendTemplateNotificationAsync));\n        var call = Assert.Single(calls, c => c.GetMethodInfo() == methodInfo);\n\n        var arguments = call.GetArguments();\n\n        var dictionaryArg = (Dictionary<string, string>)arguments[0]!;\n        var tagArg = (string)arguments[1]!;\n\n        Assert.Equal(2, dictionaryArg.Count);\n        Assert.True(dictionaryArg.TryGetValue(\"type\", out var typeString));\n        Assert.True(byte.TryParse(typeString, out var typeByte));\n        Assert.Equal(type, (PushType)typeByte);\n\n        Assert.True(dictionaryArg.TryGetValue(\"payload\", out var payloadString));\n        var actualPayloadNode = JsonNode.Parse(payloadString);\n\n        Assert.True(JsonNode.DeepEquals(expectedPayload, actualPayloadNode));\n\n        Assert.Equal(tag, tagArg);\n    }\n\n    private static NotificationPushNotification ToNotificationPushNotification(Notification notification,\n        NotificationStatus? notificationStatus, Guid? installationId) =>\n        new()\n        {\n            Id = notification.Id,\n            Priority = notification.Priority,\n            Global = notification.Global,\n            ClientType = notification.ClientType,\n            UserId = notification.UserId,\n            OrganizationId = notification.OrganizationId,\n            InstallationId = installationId,\n            TaskId = notification.TaskId,\n            Title = notification.Title,\n            Body = notification.Body,\n            CreationDate = notification.CreationDate,\n            RevisionDate = notification.RevisionDate,\n            ReadDate = notificationStatus?.ReadDate,\n            DeletedDate = notificationStatus?.DeletedDate\n        };\n\n    private static async Task AssertSendTemplateNotificationAsync(\n        SutProvider<NotificationHubPushEngine> sutProvider, PushType type, object payload, string tag)\n    {\n        await sutProvider.GetDependency<INotificationHubPool>()\n            .Received(1)\n            .AllClients\n            .Received(1)\n            .SendTemplateNotificationAsync(\n                Arg.Is<IDictionary<string, string>>(dictionary => MatchingSendPayload(dictionary, type, payload)),\n                tag);\n    }\n\n    private static bool MatchingSendPayload(IDictionary<string, string> dictionary, PushType type, object payload)\n    {\n        return dictionary.ContainsKey(\"type\") && dictionary[\"type\"].Equals(((byte)type).ToString()) &&\n               dictionary.ContainsKey(\"payload\") && dictionary[\"payload\"].Equals(JsonSerializer.Serialize(payload));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/PushServiceCollectionExtensionsTests.cs",
    "content": "﻿using Bit.Core.Auth.Models.Data;\nusing Bit.Core.Entities;\nusing Bit.Core.KeyManagement.UserKey;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Repositories;\nusing Bit.Core.Repositories.Noop;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Push;\n\npublic class PushServiceCollectionExtensionsTests\n{\n    [Fact]\n    public void AddPush_SelfHosted_NoConfig_NoEngines()\n    {\n        var services = Build(new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"true\" },\n            { \"GlobalSettings:Installation:Id\", Guid.NewGuid().ToString() },\n        });\n\n        _ = services.GetRequiredService<IPushNotificationService>();\n        var engines = services.GetServices<IPushEngine>();\n\n        Assert.Empty(engines);\n    }\n\n    [Fact]\n    public void AddPush_SelfHosted_ConfiguredForRelay_RelayEngineAdded()\n    {\n        var services = Build(new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"true\" },\n            { \"GlobalSettings:Installation:Id\", Guid.NewGuid().ToString() },\n            { \"GlobalSettings:Installation:Key\", \"some_key\"},\n            { \"GlobalSettings:PushRelayBaseUri\", \"https://example.com\" },\n        });\n\n        _ = services.GetRequiredService<IPushNotificationService>();\n        var engines = services.GetServices<IPushEngine>();\n\n        var engine = Assert.Single(engines);\n        Assert.IsType<RelayPushEngine>(engine);\n    }\n\n    [Fact]\n    public void AddPush_SelfHosted_ConfiguredForApi_ApiEngineAdded()\n    {\n        var services = Build(new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"true\" },\n            { \"GlobalSettings:Installation:Id\", Guid.NewGuid().ToString() },\n            { \"GlobalSettings:InternalIdentityKey\", \"some_key\"},\n            { \"GlobalSettings:BaseServiceUri\", \"https://example.com\" },\n        });\n\n        _ = services.GetRequiredService<IPushNotificationService>();\n        var engines = services.GetServices<IPushEngine>();\n\n        var engine = Assert.Single(engines);\n        Assert.IsType<NotificationsApiPushEngine>(engine);\n    }\n\n    [Fact]\n    public void AddPush_SelfHosted_ConfiguredForRelayAndApi_TwoEnginesAdded()\n    {\n        var services = Build(new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"true\" },\n            { \"GlobalSettings:Installation:Id\", Guid.NewGuid().ToString() },\n            { \"GlobalSettings:Installation:Key\", \"some_key\"},\n            { \"GlobalSettings:PushRelayBaseUri\", \"https://example.com\" },\n            { \"GlobalSettings:InternalIdentityKey\", \"some_key\"},\n            { \"GlobalSettings:BaseServiceUri\", \"https://example.com\" },\n        });\n\n        _ = services.GetRequiredService<IPushNotificationService>();\n        var engines = services.GetServices<IPushEngine>();\n\n        Assert.Collection(\n            engines,\n            e => Assert.IsType<RelayPushEngine>(e),\n            e => Assert.IsType<NotificationsApiPushEngine>(e)\n        );\n    }\n\n    [Fact]\n    public void AddPush_Cloud_NoConfig_AddsNotificationHub()\n    {\n        var services = Build(new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"false\" },\n        });\n\n        _ = services.GetRequiredService<IPushNotificationService>();\n        var engines = services.GetServices<IPushEngine>();\n\n        var engine = Assert.Single(engines);\n        Assert.IsType<NotificationHubPushEngine>(engine);\n    }\n\n    [Fact]\n    public void AddPush_Cloud_HasNotificationConnectionString_TwoEngines()\n    {\n        var services = Build(new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"false\" },\n            { \"GlobalSettings:Notifications:ConnectionString\", \"UseDevelopmentStorage=true\" },\n        });\n\n        _ = services.GetRequiredService<IPushNotificationService>();\n        var engines = services.GetServices<IPushEngine>();\n\n        Assert.Collection(\n            engines,\n            e => Assert.IsType<NotificationHubPushEngine>(e),\n            e => Assert.IsType<AzureQueuePushEngine>(e)\n        );\n    }\n\n    [Fact]\n    public void AddPush_Cloud_CalledTwice_DoesNotAddServicesTwice()\n    {\n        var services = new ServiceCollection();\n\n        var config = new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"false\" },\n            { \"GlobalSettings:Notifications:ConnectionString\", \"UseDevelopmentStorage=true\" },\n        };\n\n        AddServices(services, config);\n\n        var initialCount = services.Count;\n\n        // Add services again\n        AddServices(services, config);\n\n        Assert.Equal(initialCount, services.Count);\n    }\n\n    private static ServiceProvider Build(Dictionary<string, string?> initialData)\n    {\n        var services = new ServiceCollection();\n\n        AddServices(services, initialData);\n\n        return services.BuildServiceProvider();\n    }\n\n    private static void AddServices(IServiceCollection services, Dictionary<string, string?> initialData)\n    {\n        // A minimal service collection is always expected to have logging, config, and global settings\n        // pre-registered. \n\n        services.AddLogging();\n\n        var config = new ConfigurationBuilder()\n            .AddInMemoryCollection(initialData)\n            .Build();\n\n        services.TryAddSingleton(config);\n        var globalSettings = new GlobalSettings();\n        config.GetSection(\"GlobalSettings\").Bind(globalSettings);\n\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton<IGlobalSettings>(globalSettings);\n\n        // Temporary until AddPush can add it themselves directly.\n        services.TryAddSingleton<IDeviceRepository, StubDeviceRepository>();\n\n        // Temporary until AddPush can add it themselves directly.\n        services.TryAddSingleton<IInstallationDeviceRepository, InstallationDeviceRepository>();\n\n        services.AddPush(globalSettings);\n    }\n\n    private class StubDeviceRepository : IDeviceRepository\n    {\n        public Task ClearPushTokenAsync(Guid id) => throw new NotImplementedException();\n        public Task<Device> CreateAsync(Device obj) => throw new NotImplementedException();\n        public Task DeleteAsync(Device obj) => throw new NotImplementedException();\n        public Task<Device?> GetByIdAsync(Guid id, Guid userId) => throw new NotImplementedException();\n        public Task<Device?> GetByIdAsync(Guid id) => throw new NotImplementedException();\n        public Task<Device?> GetByIdentifierAsync(string identifier) => throw new NotImplementedException();\n        public Task<Device?> GetByIdentifierAsync(string identifier, Guid userId) => throw new NotImplementedException();\n        public Task<ICollection<Device>> GetManyByUserIdAsync(Guid userId) => throw new NotImplementedException();\n        public Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId) => throw new NotImplementedException();\n        public Task ReplaceAsync(Device obj) => throw new NotImplementedException();\n        public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<Device> devices) => throw new NotImplementedException();\n        public Task UpsertAsync(Device obj) => throw new NotImplementedException();\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/Push/PushTypeTests.cs",
    "content": "﻿using System.Diagnostics;\nusing System.Reflection;\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Push;\n\npublic class PushTypeTests\n{\n    [Fact]\n    public void AllEnumMembersHaveUniqueValue()\n    {\n        // No enum member should use the same value as another named member.\n\n        var usedNumbers = new HashSet<byte>();\n        var enumMembers = Enum.GetValues<PushType>();\n\n        foreach (var enumMember in enumMembers)\n        {\n            if (!usedNumbers.Add((byte)enumMember))\n            {\n                Assert.Fail($\"Enum number value ({(byte)enumMember}) on {enumMember} is already in use.\");\n            }\n        }\n    }\n\n    [Fact]\n    public void AllEnumMembersHaveNotificationInfoAttribute()\n    {\n        // Every enum member should be annotated with [NotificationInfo]\n\n        foreach (var member in typeof(PushType).GetMembers(BindingFlags.Public | BindingFlags.Static))\n        {\n            var notificationInfoAttribute = member.GetCustomAttribute<NotificationInfoAttribute>();\n            if (notificationInfoAttribute is null)\n            {\n                Assert.Fail($\"PushType.{member.Name} is missing a required [NotificationInfo(\\\"team-name\\\", typeof(MyType))] attribute.\");\n            }\n        }\n    }\n\n    [Fact]\n    public void AllEnumValuesAreInSequence()\n    {\n        // There should not be any gaps in the numbers defined for an enum, that being if someone last defined 22\n        // the next number used should be 23 not 24 or any other number.\n\n        var sortedValues = Enum.GetValues<PushType>()\n            .Order()\n            .ToArray();\n\n        Debug.Assert(sortedValues.Length > 0);\n\n        var lastValue = sortedValues[0];\n\n        foreach (var value in sortedValues[1..])\n        {\n            var expectedValue = ++lastValue;\n\n            Assert.Equal(expectedValue, value);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/PushRegistration/NotificationHubPushRegistrationServiceTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.Enums;\nusing Bit.Core.Platform.Push.Internal;\nusing Bit.Core.Platform.PushRegistration;\nusing Bit.Core.Platform.PushRegistration.Internal;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Azure.NotificationHubs;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.NotificationHub;\n\n[SutProviderCustomize]\npublic class NotificationHubPushRegistrationServiceTests\n{\n    [Theory]\n    [RepeatingPatternBitAutoData([null, \"\", \" \"])]\n    public async Task CreateOrUpdateRegistrationAsync_PushTokenNullOrEmpty_InstallationNotCreated(string? pushToken,\n        SutProvider<NotificationHubPushRegistrationService> sutProvider, Guid deviceId, Guid userId, Guid identifier,\n        Guid organizationId, Guid installationId)\n    {\n        await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(),\n            identifier.ToString(), DeviceType.Android, [organizationId.ToString()], installationId);\n\n        sutProvider.GetDependency<INotificationHubPool>()\n            .Received(0)\n            .ClientFor(deviceId);\n    }\n\n    [Theory]\n    [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])]\n    public async Task CreateOrUpdateRegistrationAsync_DeviceTypeAndroid_InstallationCreated(bool identifierNull,\n        bool partOfOrganizationId, bool installationIdNull,\n        SutProvider<NotificationHubPushRegistrationService> sutProvider, Guid deviceId, Guid userId, Guid? identifier,\n        Guid organizationId, Guid installationId)\n    {\n        var notificationHubClient = Substitute.For<INotificationHubClient>();\n        sutProvider.GetDependency<INotificationHubPool>().ClientFor(Arg.Any<Guid>()).Returns(notificationHubClient);\n\n        var pushToken = \"test push token\";\n\n        await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(),\n            identifierNull ? null : identifier.ToString(), DeviceType.Android,\n            partOfOrganizationId ? [organizationId.ToString()] : [],\n            installationIdNull ? Guid.Empty : installationId);\n\n        sutProvider.GetDependency<INotificationHubPool>()\n            .Received(1)\n            .ClientFor(deviceId);\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation =>\n                installation.InstallationId == deviceId.ToString() &&\n                installation.PushChannel == pushToken &&\n                installation.Platform == NotificationPlatform.FcmV1 &&\n                installation.Tags.Contains($\"userId:{userId}\") &&\n                installation.Tags.Contains(\"clientType:Mobile\") &&\n                (identifierNull || installation.Tags.Contains($\"deviceIdentifier:{identifier}\")) &&\n                (!partOfOrganizationId || installation.Tags.Contains($\"organizationId:{organizationId}\")) &&\n                (installationIdNull || installation.Tags.Contains($\"installationId:{installationId}\")) &&\n                installation.Templates.Count == 3));\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation => MatchingInstallationTemplate(\n                installation.Templates, \"template:payload\",\n                \"{\\\"message\\\":{\\\"data\\\":{\\\"type\\\":\\\"$(type)\\\",\\\"payload\\\":\\\"$(payload)\\\"}}}\",\n                new List<string?>\n                {\n                    \"template:payload\",\n                    $\"template:payload_userId:{userId}\",\n                    \"clientType:Mobile\",\n                    identifierNull ? null : $\"template:payload_deviceIdentifier:{identifier}\",\n                    partOfOrganizationId ? $\"organizationId:{organizationId}\" : null,\n                    installationIdNull ? null : $\"installationId:{installationId}\",\n                })));\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation => MatchingInstallationTemplate(\n                installation.Templates, \"template:message\",\n                \"{\\\"message\\\":{\\\"data\\\":{\\\"type\\\":\\\"$(type)\\\"},\\\"notification\\\":{\\\"title\\\":\\\"$(title)\\\",\\\"body\\\":\\\"$(message)\\\"}}}\",\n                new List<string?>\n                {\n                    \"template:message\",\n                    $\"template:message_userId:{userId}\",\n                    \"clientType:Mobile\",\n                    identifierNull ? null : $\"template:message_deviceIdentifier:{identifier}\",\n                    partOfOrganizationId ? $\"organizationId:{organizationId}\" : null,\n                    installationIdNull ? null : $\"installationId:{installationId}\",\n                })));\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation => MatchingInstallationTemplate(\n                installation.Templates, \"template:badgeMessage\",\n                \"{\\\"message\\\":{\\\"data\\\":{\\\"type\\\":\\\"$(type)\\\"},\\\"notification\\\":{\\\"title\\\":\\\"$(title)\\\",\\\"body\\\":\\\"$(message)\\\"}}}\",\n                new List<string?>\n                {\n                    \"template:badgeMessage\",\n                    $\"template:badgeMessage_userId:{userId}\",\n                    \"clientType:Mobile\",\n                    identifierNull ? null : $\"template:badgeMessage_deviceIdentifier:{identifier}\",\n                    partOfOrganizationId ? $\"organizationId:{organizationId}\" : null,\n                    installationIdNull ? null : $\"installationId:{installationId}\",\n                })));\n    }\n\n    [Theory]\n    [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])]\n    public async Task CreateOrUpdateRegistrationAsync_DeviceTypeIOS_InstallationCreated(bool identifierNull,\n        bool partOfOrganizationId, bool installationIdNull,\n        SutProvider<NotificationHubPushRegistrationService> sutProvider, Guid deviceId, Guid userId, Guid identifier,\n        Guid organizationId, Guid installationId)\n    {\n        var notificationHubClient = Substitute.For<INotificationHubClient>();\n        sutProvider.GetDependency<INotificationHubPool>().ClientFor(Arg.Any<Guid>()).Returns(notificationHubClient);\n\n        var pushToken = \"test push token\";\n\n        await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(),\n            identifierNull ? null : identifier.ToString(), DeviceType.iOS,\n            partOfOrganizationId ? [organizationId.ToString()] : [],\n            installationIdNull ? Guid.Empty : installationId);\n\n        sutProvider.GetDependency<INotificationHubPool>()\n            .Received(1)\n            .ClientFor(deviceId);\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation =>\n                installation.InstallationId == deviceId.ToString() &&\n                installation.PushChannel == pushToken &&\n                installation.Platform == NotificationPlatform.Apns &&\n                installation.Tags.Contains($\"userId:{userId}\") &&\n                installation.Tags.Contains(\"clientType:Mobile\") &&\n                (identifierNull || installation.Tags.Contains($\"deviceIdentifier:{identifier}\")) &&\n                (!partOfOrganizationId || installation.Tags.Contains($\"organizationId:{organizationId}\")) &&\n                (installationIdNull || installation.Tags.Contains($\"installationId:{installationId}\")) &&\n                installation.Templates.Count == 3));\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation => MatchingInstallationTemplate(\n                installation.Templates, \"template:payload\",\n                \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"payload\\\":\\\"$(payload)\\\"},\\\"aps\\\":{\\\"content-available\\\":1}}\",\n                new List<string?>\n                {\n                    \"template:payload\",\n                    $\"template:payload_userId:{userId}\",\n                    \"clientType:Mobile\",\n                    identifierNull ? null : $\"template:payload_deviceIdentifier:{identifier}\",\n                    partOfOrganizationId ? $\"organizationId:{organizationId}\" : null,\n                    installationIdNull ? null : $\"installationId:{installationId}\",\n                })));\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation => MatchingInstallationTemplate(\n                installation.Templates, \"template:message\",\n                \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\"},\\\"aps\\\":{\\\"alert\\\":\\\"$(message)\\\",\\\"badge\\\":null,\\\"content-available\\\":1}}\",\n                new List<string?>\n                {\n                    \"template:message\",\n                    $\"template:message_userId:{userId}\",\n                    \"clientType:Mobile\",\n                    identifierNull ? null : $\"template:message_deviceIdentifier:{identifier}\",\n                    partOfOrganizationId ? $\"organizationId:{organizationId}\" : null,\n                    installationIdNull ? null : $\"installationId:{installationId}\",\n                })));\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation => MatchingInstallationTemplate(\n                installation.Templates, \"template:badgeMessage\",\n                \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\"},\\\"aps\\\":{\\\"alert\\\":\\\"$(message)\\\",\\\"badge\\\":\\\"#(badge)\\\",\\\"content-available\\\":1}}\",\n                new List<string?>\n                {\n                    \"template:badgeMessage\",\n                    $\"template:badgeMessage_userId:{userId}\",\n                    \"clientType:Mobile\",\n                    identifierNull ? null : $\"template:badgeMessage_deviceIdentifier:{identifier}\",\n                    partOfOrganizationId ? $\"organizationId:{organizationId}\" : null,\n                    installationIdNull ? null : $\"installationId:{installationId}\",\n                })));\n    }\n\n    [Theory]\n    [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])]\n    public async Task CreateOrUpdateRegistrationAsync_DeviceTypeAndroidAmazon_InstallationCreated(bool identifierNull,\n        bool partOfOrganizationId, bool installationIdNull,\n        SutProvider<NotificationHubPushRegistrationService> sutProvider, Guid deviceId,\n        Guid userId, Guid identifier, Guid organizationId, Guid installationId)\n    {\n        var notificationHubClient = Substitute.For<INotificationHubClient>();\n        sutProvider.GetDependency<INotificationHubPool>().ClientFor(Arg.Any<Guid>()).Returns(notificationHubClient);\n\n        var pushToken = \"test push token\";\n\n        await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(),\n            identifierNull ? null : identifier.ToString(), DeviceType.AndroidAmazon,\n            partOfOrganizationId ? [organizationId.ToString()] : [],\n            installationIdNull ? Guid.Empty : installationId);\n\n        sutProvider.GetDependency<INotificationHubPool>()\n            .Received(1)\n            .ClientFor(deviceId);\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation =>\n                installation.InstallationId == deviceId.ToString() &&\n                installation.PushChannel == pushToken &&\n                installation.Platform == NotificationPlatform.Adm &&\n                installation.Tags.Contains($\"userId:{userId}\") &&\n                installation.Tags.Contains(\"clientType:Mobile\") &&\n                (identifierNull || installation.Tags.Contains($\"deviceIdentifier:{identifier}\")) &&\n                (!partOfOrganizationId || installation.Tags.Contains($\"organizationId:{organizationId}\")) &&\n                (installationIdNull || installation.Tags.Contains($\"installationId:{installationId}\")) &&\n                installation.Templates.Count == 3));\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation => MatchingInstallationTemplate(\n                installation.Templates, \"template:payload\",\n                \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"payload\\\":\\\"$(payload)\\\"}}\",\n                new List<string?>\n                {\n                    \"template:payload\",\n                    $\"template:payload_userId:{userId}\",\n                    \"clientType:Mobile\",\n                    identifierNull ? null : $\"template:payload_deviceIdentifier:{identifier}\",\n                    partOfOrganizationId ? $\"organizationId:{organizationId}\" : null,\n                    installationIdNull ? null : $\"installationId:{installationId}\",\n                })));\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation => MatchingInstallationTemplate(\n                installation.Templates, \"template:message\",\n                \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"message\\\":\\\"$(message)\\\"}}\",\n                new List<string?>\n                {\n                    \"template:message\",\n                    $\"template:message_userId:{userId}\",\n                    \"clientType:Mobile\",\n                    identifierNull ? null : $\"template:message_deviceIdentifier:{identifier}\",\n                    partOfOrganizationId ? $\"organizationId:{organizationId}\" : null,\n                    installationIdNull ? null : $\"installationId:{installationId}\",\n                })));\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation => MatchingInstallationTemplate(\n                installation.Templates, \"template:badgeMessage\",\n                \"{\\\"data\\\":{\\\"type\\\":\\\"#(type)\\\",\\\"message\\\":\\\"$(message)\\\"}}\",\n                new List<string?>\n                {\n                    \"template:badgeMessage\",\n                    $\"template:badgeMessage_userId:{userId}\",\n                    \"clientType:Mobile\",\n                    identifierNull ? null : $\"template:badgeMessage_deviceIdentifier:{identifier}\",\n                    partOfOrganizationId ? $\"organizationId:{organizationId}\" : null,\n                    installationIdNull ? null : $\"installationId:{installationId}\",\n                })));\n    }\n\n    [Theory]\n    [BitAutoData(DeviceType.ChromeBrowser)]\n    [BitAutoData(DeviceType.ChromeExtension)]\n    [BitAutoData(DeviceType.MacOsDesktop)]\n    public async Task CreateOrUpdateRegistrationAsync_DeviceTypeNotMobile_InstallationCreated(DeviceType deviceType,\n        SutProvider<NotificationHubPushRegistrationService> sutProvider, Guid deviceId, Guid userId, Guid identifier,\n        Guid organizationId, Guid installationId)\n    {\n        var notificationHubClient = Substitute.For<INotificationHubClient>();\n        sutProvider.GetDependency<INotificationHubPool>().ClientFor(Arg.Any<Guid>()).Returns(notificationHubClient);\n\n        var pushToken = \"test push token\";\n\n        await sutProvider.Sut.CreateOrUpdateRegistrationAsync(new PushRegistrationData(pushToken), deviceId.ToString(), userId.ToString(),\n            identifier.ToString(), deviceType, [organizationId.ToString()], installationId);\n\n        sutProvider.GetDependency<INotificationHubPool>()\n            .Received(1)\n            .ClientFor(deviceId);\n        await notificationHubClient\n            .Received(1)\n            .CreateOrUpdateInstallationAsync(Arg.Is<Installation>(installation =>\n                installation.InstallationId == deviceId.ToString() &&\n                installation.PushChannel == pushToken &&\n                installation.Tags.Contains($\"userId:{userId}\") &&\n                installation.Tags.Contains($\"clientType:{DeviceTypes.ToClientType(deviceType)}\") &&\n                installation.Tags.Contains($\"deviceIdentifier:{identifier}\") &&\n                installation.Tags.Contains($\"organizationId:{organizationId}\") &&\n                installation.Tags.Contains($\"installationId:{installationId}\") &&\n                installation.Templates.Count == 0));\n    }\n\n    private static bool MatchingInstallationTemplate(IDictionary<string, InstallationTemplate> templates, string key,\n        string body, List<string?> tags)\n    {\n        var tagsNoNulls = tags.FindAll(tag => tag != null);\n        return templates.ContainsKey(key) && templates[key].Body == body &&\n               templates[key].Tags.Count == tagsNoNulls.Count &&\n               templates[key].Tags.All(tagsNoNulls.Contains);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/PushRegistration/PushRegistrationServiceCollectionExtensionsTests.cs",
    "content": "﻿using Bit.Core.Platform.Push;\nusing Bit.Core.Platform.PushRegistration.Internal;\nusing Bit.Core.Repositories;\nusing Bit.Core.Repositories.Noop;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Configuration;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.DependencyInjection.Extensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.PushRegistration;\n\npublic class PushRegistrationServiceCollectionExtensionsTests\n{\n    [Fact]\n    public void AddPushRegistration_Cloud_CreatesNotificationHubRegistrationService()\n    {\n        var services = Build(new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"false\" },\n        });\n\n        var pushRegistrationService = services.GetRequiredService<IPushRegistrationService>();\n        Assert.IsType<NotificationHubPushRegistrationService>(pushRegistrationService);\n    }\n\n    [Fact]\n    public void AddPushRegistration_SelfHosted_NoOtherConfig_ReturnsNoopRegistrationService()\n    {\n        var services = Build(new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"true\" },\n        });\n\n        var pushRegistrationService = services.GetRequiredService<IPushRegistrationService>();\n        Assert.IsType<NoopPushRegistrationService>(pushRegistrationService);\n    }\n\n    [Fact]\n    public void AddPushRegistration_SelfHosted_RelayConfig_ReturnsRelayRegistrationService()\n    {\n        var services = Build(new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"true\" },\n            { \"GlobalSettings:PushRelayBaseUri\", \"https://example.com\" },\n            { \"GlobalSettings:Installation:Key\", \"some_key\" },\n        });\n\n        var pushRegistrationService = services.GetRequiredService<IPushRegistrationService>();\n        Assert.IsType<RelayPushRegistrationService>(pushRegistrationService);\n    }\n\n    [Fact]\n    public void AddPushRegistration_MultipleTimes_NoAdditionalServices()\n    {\n        var services = new ServiceCollection();\n\n        var config = new Dictionary<string, string?>\n        {\n            { \"GlobalSettings:SelfHosted\", \"true\" },\n            { \"GlobalSettings:PushRelayBaseUri\", \"https://example.com\" },\n            { \"GlobalSettings:Installation:Key\", \"some_key\" },\n        };\n\n        AddServices(services, config);\n\n        // Add services again\n        services.AddPushRegistration();\n\n        var provider = services.BuildServiceProvider();\n\n        Assert.Single(provider.GetServices<IPushRegistrationService>());\n    }\n\n    private static ServiceProvider Build(Dictionary<string, string?> initialData)\n    {\n        var services = new ServiceCollection();\n\n        AddServices(services, initialData);\n\n        return services.BuildServiceProvider();\n    }\n\n    private static void AddServices(IServiceCollection services, Dictionary<string, string?> initialData)\n    {\n        // A minimal service collection is always expected to have logging, config, and global settings\n        // pre-registered. \n\n        services.AddLogging();\n\n        var config = new ConfigurationBuilder()\n            .AddInMemoryCollection(initialData)\n            .Build();\n\n        services.TryAddSingleton(config);\n        var globalSettings = new GlobalSettings();\n        config.GetSection(\"GlobalSettings\").Bind(globalSettings);\n\n        services.TryAddSingleton(globalSettings);\n        services.TryAddSingleton<IGlobalSettings>(globalSettings);\n\n\n        // Temporary until AddPushRegistration can add it themselves directly.\n        services.TryAddSingleton<IInstallationDeviceRepository, InstallationDeviceRepository>();\n\n        services.AddPushRegistration();\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Platform/PushRegistration/RelayPushRegistrationServiceTests.cs",
    "content": "﻿using Bit.Core.Platform.PushRegistration.Internal;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Platform.Push.Services;\n\npublic class RelayPushRegistrationServiceTests\n{\n    private readonly RelayPushRegistrationService _sut;\n\n    private readonly IHttpClientFactory _httpFactory;\n    private readonly GlobalSettings _globalSettings;\n    private readonly ILogger<RelayPushRegistrationService> _logger;\n\n    public RelayPushRegistrationServiceTests()\n    {\n        _globalSettings = new GlobalSettings();\n        _httpFactory = Substitute.For<IHttpClientFactory>();\n        _logger = Substitute.For<ILogger<RelayPushRegistrationService>>();\n\n        _sut = new RelayPushRegistrationService(\n            _httpFactory,\n            _globalSettings,\n            _logger\n        );\n    }\n\n    // Remove this test when we add actual tests. It only proves that\n    // we've properly constructed the system under test.\n    [Fact(Skip = \"Needs additional work\")]\n    public void ServiceExists()\n    {\n        Assert.NotNull(_sut);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Resources/VerifyResources.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Resources;\n\npublic class VerifyResources\n{\n    [Theory]\n    [MemberData(nameof(GetResources))]\n    public void Resource_FoundAndReadable(string resourceName)\n    {\n        var assembly = typeof(CoreHelpers).Assembly;\n\n        using (var resource = assembly.GetManifestResourceStream(resourceName))\n        {\n            Assert.NotNull(resource);\n            Assert.True(resource.CanRead);\n        }\n    }\n\n    public static IEnumerable<object[]> GetResources()\n    {\n        yield return new[] { \"Bit.Core.licensing.cer\" };\n        yield return new[] { \"Bit.Core.MailTemplates.Handlebars.AddedCredit.html.hbs\" };\n        yield return new[] { \"Bit.Core.MailTemplates.Handlebars.Layouts.Basic.html.hbs\" };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/SecretsManager/AutoFixture/ProjectFixtures.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\n\npublic class ProjectCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        var projectId = Guid.NewGuid();\n\n        fixture.Customize<Project>(composer => composer\n            .With(p => p.Id, projectId)\n            .Without(s => s.Secrets));\n    }\n}\n\npublic class ProjectCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new ProjectCustomization();\n}\n"
  },
  {
    "path": "test/Core.Test/SecretsManager/AutoFixture/SecretFixtures.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture;\n\npublic class SecretCustomization : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        var secretId = Guid.NewGuid();\n\n        fixture.Customize<Secret>(composer => composer\n            .With(o => o.Id, secretId)\n            .Without(s => s.Projects));\n    }\n}\n\npublic class SecretCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new SecretCustomization();\n}\n"
  },
  {
    "path": "test/Core.Test/SecretsManager/Models/ProjectServiceAccountsAccessPoliciesTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Xunit;\n\nnamespace Bit.Core.Test.SecretsManager.Models;\n\npublic class ProjectServiceAccountsAccessPoliciesTests\n{\n    [Fact]\n    public void GetPolicyUpdates_NoChanges_ReturnsEmptyList()\n    {\n        var serviceAccountId1 = Guid.NewGuid();\n        var serviceAccountId2 = Guid.NewGuid();\n        var projectId = Guid.NewGuid();\n\n        var existing = new ProjectServiceAccountsAccessPolicies\n        {\n            ServiceAccountAccessPolicies = new List<ServiceAccountProjectAccessPolicy>\n            {\n                new() { ServiceAccountId = serviceAccountId1, GrantedProjectId = projectId, Read = true, Write = true },\n                new() {  ServiceAccountId = serviceAccountId2, GrantedProjectId = projectId, Read = false, Write = true }\n            }\n        };\n\n        var result = existing.GetPolicyUpdates(existing);\n\n        Assert.Empty(result.ServiceAccountAccessPolicyUpdates);\n    }\n\n    [Fact]\n    public void GetPolicyUpdates_ReturnsCorrectPolicyChanges()\n    {\n        var serviceAccountId1 = Guid.NewGuid();\n        var serviceAccountId2 = Guid.NewGuid();\n        var serviceAccountId3 = Guid.NewGuid();\n        var serviceAccountId4 = Guid.NewGuid();\n        var projectId = Guid.NewGuid();\n\n        var existing = new ProjectServiceAccountsAccessPolicies\n        {\n            ServiceAccountAccessPolicies = new List<ServiceAccountProjectAccessPolicy>\n            {\n                new() { ServiceAccountId = serviceAccountId1, GrantedProjectId = projectId, Read = true, Write = true },\n                new() { ServiceAccountId = serviceAccountId3, GrantedProjectId = projectId, Read = true, Write = true },\n                new() { ServiceAccountId = serviceAccountId4, GrantedProjectId = projectId, Read = true, Write = true }\n            }\n        };\n\n        var requested = new ProjectServiceAccountsAccessPolicies\n        {\n            ServiceAccountAccessPolicies = new List<ServiceAccountProjectAccessPolicy>\n            {\n                new() { ServiceAccountId = serviceAccountId1, GrantedProjectId = projectId, Read = true, Write = false },\n                new() { ServiceAccountId = serviceAccountId2, GrantedProjectId = projectId, Read = false, Write = true },\n                new() { ServiceAccountId = serviceAccountId3, GrantedProjectId = projectId, Read = true, Write = true }\n            }\n        };\n\n\n        var result = existing.GetPolicyUpdates(requested);\n\n        Assert.Contains(serviceAccountId2, result.ServiceAccountAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Create)\n            .Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));\n\n        Assert.Contains(serviceAccountId4, result.ServiceAccountAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Delete)\n            .Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));\n\n        Assert.Contains(serviceAccountId1, result.ServiceAccountAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Update)\n            .Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));\n\n        Assert.DoesNotContain(serviceAccountId3, result.ServiceAccountAccessPolicyUpdates\n            .Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/SecretsManager/Models/SecretAccessPoliciesTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Xunit;\n\nnamespace Bit.Core.Test.SecretsManager.Models;\n\n[SutProviderCustomize]\n[ProjectCustomize]\npublic class SecretAccessPoliciesTests\n{\n    [Theory]\n    [BitAutoData]\n    public void GetPolicyUpdates_NoChanges_ReturnsEmptyList(SecretAccessPolicies data)\n    {\n        var result = data.GetPolicyUpdates(data);\n\n        Assert.Empty(result.UserAccessPolicyUpdates);\n        Assert.Empty(result.GroupAccessPolicyUpdates);\n        Assert.Empty(result.ServiceAccountAccessPolicyUpdates);\n    }\n\n    [Fact]\n    public void GetPolicyUpdates_ReturnsCorrectPolicyChanges()\n    {\n        var secretId = Guid.NewGuid();\n        var updatedId = Guid.NewGuid();\n        var createId = Guid.NewGuid();\n        var unChangedId = Guid.NewGuid();\n        var deleteId = Guid.NewGuid();\n\n        var existing = new SecretAccessPolicies\n        {\n            UserAccessPolicies = new List<UserSecretAccessPolicy>\n            {\n                new() { OrganizationUserId = updatedId, GrantedSecretId = secretId, Read = true, Write = true },\n                new() { OrganizationUserId = unChangedId, GrantedSecretId = secretId, Read = true, Write = true },\n                new() { OrganizationUserId = deleteId, GrantedSecretId = secretId, Read = true, Write = true }\n            },\n            GroupAccessPolicies = new List<GroupSecretAccessPolicy>\n            {\n                new() { GroupId = updatedId, GrantedSecretId = secretId, Read = true, Write = true },\n                new() { GroupId = unChangedId, GrantedSecretId = secretId, Read = true, Write = true },\n                new() { GroupId = deleteId, GrantedSecretId = secretId, Read = true, Write = true }\n            },\n            ServiceAccountAccessPolicies = new List<ServiceAccountSecretAccessPolicy>\n            {\n                new() { ServiceAccountId = updatedId, GrantedSecretId = secretId, Read = true, Write = true },\n                new() { ServiceAccountId = unChangedId, GrantedSecretId = secretId, Read = true, Write = true },\n                new() { ServiceAccountId = deleteId, GrantedSecretId = secretId, Read = true, Write = true }\n            }\n        };\n\n        var requested = new SecretAccessPolicies\n        {\n            UserAccessPolicies = new List<UserSecretAccessPolicy>\n            {\n                new() { OrganizationUserId = updatedId, GrantedSecretId = secretId, Read = true, Write = false },\n                new() { OrganizationUserId = createId, GrantedSecretId = secretId, Read = false, Write = true },\n                new() { OrganizationUserId = unChangedId, GrantedSecretId = secretId, Read = true, Write = true }\n            },\n            GroupAccessPolicies = new List<GroupSecretAccessPolicy>\n            {\n                new() { GroupId = updatedId, GrantedSecretId = secretId, Read = true, Write = false },\n                new() { GroupId = createId, GrantedSecretId = secretId, Read = false, Write = true },\n                new() { GroupId = unChangedId, GrantedSecretId = secretId, Read = true, Write = true }\n            },\n            ServiceAccountAccessPolicies = new List<ServiceAccountSecretAccessPolicy>\n            {\n                new() { ServiceAccountId = updatedId, GrantedSecretId = secretId, Read = true, Write = false },\n                new() { ServiceAccountId = createId, GrantedSecretId = secretId, Read = false, Write = true },\n                new() { ServiceAccountId = unChangedId, GrantedSecretId = secretId, Read = true, Write = true }\n            }\n        };\n\n\n        var result = existing.GetPolicyUpdates(requested);\n\n        Assert.Contains(createId, result.UserAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Create)\n            .Select(pu => pu.AccessPolicy.OrganizationUserId!.Value));\n        Assert.Contains(createId, result.GroupAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Create)\n            .Select(pu => pu.AccessPolicy.GroupId!.Value));\n        Assert.Contains(createId, result.ServiceAccountAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Create)\n            .Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));\n\n        Assert.Contains(deleteId, result.UserAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Delete)\n            .Select(pu => pu.AccessPolicy.OrganizationUserId!.Value));\n        Assert.Contains(deleteId, result.GroupAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Delete)\n            .Select(pu => pu.AccessPolicy.GroupId!.Value));\n        Assert.Contains(deleteId, result.ServiceAccountAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Delete)\n            .Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));\n\n        Assert.Contains(updatedId, result.UserAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Update)\n            .Select(pu => pu.AccessPolicy.OrganizationUserId!.Value));\n        Assert.Contains(updatedId, result.GroupAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Update)\n            .Select(pu => pu.AccessPolicy.GroupId!.Value));\n        Assert.Contains(updatedId, result.ServiceAccountAccessPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Update)\n            .Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));\n\n        Assert.DoesNotContain(unChangedId, result.UserAccessPolicyUpdates\n            .Select(pu => pu.AccessPolicy.OrganizationUserId!.Value));\n        Assert.DoesNotContain(unChangedId, result.GroupAccessPolicyUpdates\n            .Select(pu => pu.AccessPolicy.GroupId!.Value));\n        Assert.DoesNotContain(unChangedId, result.ServiceAccountAccessPolicyUpdates\n            .Select(pu => pu.AccessPolicy.ServiceAccountId!.Value));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/SecretsManager/Models/ServiceAccountGrantedPoliciesTests.cs",
    "content": "﻿#nullable enable\nusing Bit.Core.SecretsManager.Entities;\nusing Bit.Core.SecretsManager.Enums.AccessPolicies;\nusing Bit.Core.SecretsManager.Models.Data;\nusing Xunit;\n\nnamespace Bit.Core.Test.SecretsManager.Models;\n\npublic class ServiceAccountGrantedPoliciesTests\n{\n    [Fact]\n    public void GetPolicyUpdates_NoChanges_ReturnsEmptyLists()\n    {\n        var projectId1 = Guid.NewGuid();\n        var projectId2 = Guid.NewGuid();\n\n        var existing = new ServiceAccountGrantedPolicies\n        {\n            ProjectGrantedPolicies = new List<ServiceAccountProjectAccessPolicy>\n            {\n                new() { GrantedProjectId = projectId1, Read = true, Write = true },\n                new() { GrantedProjectId = projectId2, Read = false, Write = true }\n            }\n        };\n\n        var result = existing.GetPolicyUpdates(existing);\n\n        Assert.Empty(result.ProjectGrantedPolicyUpdates);\n    }\n\n    [Fact]\n    public void GetPolicyUpdates_ReturnsCorrectPolicyChanges()\n    {\n        var projectId1 = Guid.NewGuid();\n        var projectId2 = Guid.NewGuid();\n        var projectId3 = Guid.NewGuid();\n        var projectId4 = Guid.NewGuid();\n\n        var existing = new ServiceAccountGrantedPolicies\n        {\n            ProjectGrantedPolicies = new List<ServiceAccountProjectAccessPolicy>\n            {\n                new() { GrantedProjectId = projectId1, Read = true, Write = true },\n                new() { GrantedProjectId = projectId3, Read = true, Write = true },\n                new() { GrantedProjectId = projectId4, Read = true, Write = true }\n            }\n        };\n\n        var requested = new ServiceAccountGrantedPolicies\n        {\n            ProjectGrantedPolicies = new List<ServiceAccountProjectAccessPolicy>\n            {\n                new() { GrantedProjectId = projectId1, Read = true, Write = false },\n                new() { GrantedProjectId = projectId2, Read = false, Write = true },\n                new() { GrantedProjectId = projectId3, Read = true, Write = true }\n            }\n        };\n\n\n        var result = existing.GetPolicyUpdates(requested);\n\n        Assert.Contains(projectId2, result.ProjectGrantedPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Create)\n            .Select(pu => pu.AccessPolicy.GrantedProjectId!.Value));\n\n        Assert.Contains(projectId4, result.ProjectGrantedPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Delete)\n            .Select(pu => pu.AccessPolicy.GrantedProjectId!.Value));\n\n        Assert.Contains(projectId1, result.ProjectGrantedPolicyUpdates\n            .Where(pu => pu.Operation == AccessPolicyOperation.Update)\n            .Select(pu => pu.AccessPolicy.GrantedProjectId!.Value));\n\n        Assert.DoesNotContain(projectId3, result.ProjectGrantedPolicyUpdates\n            .Select(pu => pu.AccessPolicy.GrantedProjectId!.Value));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/AmazonSesMailDeliveryServiceTests.cs",
    "content": "﻿using Amazon.SimpleEmail;\nusing Amazon.SimpleEmail.Model;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\npublic class AmazonSesMailDeliveryServiceTests : IDisposable\n{\n    private readonly AmazonSesMailDeliveryService _sut;\n\n    private readonly GlobalSettings _globalSettings;\n    private readonly IWebHostEnvironment _hostingEnvironment;\n    private readonly ILogger<AmazonSesMailDeliveryService> _logger;\n    private readonly IAmazonSimpleEmailService _amazonSimpleEmailService;\n\n    public AmazonSesMailDeliveryServiceTests()\n    {\n        _globalSettings = new GlobalSettings\n        {\n            Amazon =\n                    {\n                        AccessKeyId = \"AccessKeyId-AmazonSesMailDeliveryServiceTests\",\n                        AccessKeySecret = \"AccessKeySecret-AmazonSesMailDeliveryServiceTests\",\n                        Region = \"Region-AmazonSesMailDeliveryServiceTests\"\n                    }\n        };\n\n        _hostingEnvironment = Substitute.For<IWebHostEnvironment>();\n        _logger = Substitute.For<ILogger<AmazonSesMailDeliveryService>>();\n        _amazonSimpleEmailService = Substitute.For<IAmazonSimpleEmailService>();\n\n        _sut = new AmazonSesMailDeliveryService(\n            _globalSettings,\n            _hostingEnvironment,\n            _logger,\n            _amazonSimpleEmailService\n        );\n    }\n\n    public void Dispose()\n    {\n        _sut?.Dispose();\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_CallsSendEmailAsync_WhenMessageIsValid()\n    {\n        var mailMessage = new MailMessage\n        {\n            ToEmails = new List<string> { \"ToEmails\" },\n            BccEmails = new List<string> { \"BccEmails\" },\n            Subject = \"Subject\",\n            HtmlContent = \"HtmlContent\",\n            TextContent = \"TextContent\",\n            Category = \"Category\"\n        };\n\n        await _sut.SendEmailAsync(mailMessage);\n\n        await _amazonSimpleEmailService.Received(1).SendEmailAsync(\n            Arg.Do<SendEmailRequest>(request =>\n            {\n                Assert.False(string.IsNullOrEmpty(request.Source));\n\n                Assert.Single(request.Destination.ToAddresses);\n                Assert.Equal(mailMessage.ToEmails.First(), request.Destination.ToAddresses.First());\n\n                Assert.Equal(mailMessage.Subject, request.Message.Subject.Data);\n                Assert.Equal(mailMessage.HtmlContent, request.Message.Body.Html.Data);\n                Assert.Equal(mailMessage.TextContent, request.Message.Body.Text.Data);\n\n                Assert.Single(request.Destination.BccAddresses);\n                Assert.Equal(mailMessage.BccEmails.First(), request.Destination.BccAddresses.First());\n\n                Assert.Contains(request.Tags, x => x.Name == \"Environment\");\n                Assert.Contains(request.Tags, x => x.Name == \"Sender\");\n                Assert.Contains(request.Tags, x => x.Name == \"Category\");\n            }));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/DeviceServiceTests.cs",
    "content": "﻿using System.Runtime.CompilerServices;\nusing Bit.Core.Auth.Models.Api.Request;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Platform.PushRegistration;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\n[SutProviderCustomize]\npublic class DeviceServiceTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task SaveAsync_IdProvided_UpdatedRevisionDateAndPushRegistration(Guid id, Guid userId,\n        Guid organizationId1, Guid organizationId2, Guid installationId,\n        OrganizationUserOrganizationDetails organizationUserOrganizationDetails1,\n        OrganizationUserOrganizationDetails organizationUserOrganizationDetails2)\n    {\n        organizationUserOrganizationDetails1.OrganizationId = organizationId1;\n        organizationUserOrganizationDetails2.OrganizationId = organizationId2;\n\n        var deviceRepo = Substitute.For<IDeviceRepository>();\n        var pushRepo = Substitute.For<IPushRegistrationService>();\n        var organizationUserRepository = Substitute.For<IOrganizationUserRepository>();\n        organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType?>())\n            .Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]);\n        var globalSettings = Substitute.For<IGlobalSettings>();\n        globalSettings.Installation.Id.Returns(installationId);\n        var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings);\n\n        var device = new Device\n        {\n            Id = id,\n            Name = \"test device\",\n            Type = DeviceType.Android,\n            UserId = userId,\n            PushToken = \"testToken\",\n            Identifier = \"testid\"\n        };\n        await deviceService.SaveAsync(device);\n\n        Assert.True(device.RevisionDate - DateTime.UtcNow < TimeSpan.FromSeconds(1));\n        await pushRepo.Received(1).CreateOrUpdateRegistrationAsync(Arg.Is<PushRegistrationData>(v => v.Token == \"testToken\"), id.ToString(),\n            userId.ToString(), \"testid\", DeviceType.Android,\n            Arg.Do<IEnumerable<string>>(organizationIds =>\n            {\n                var organizationIdsList = organizationIds.ToList();\n                Assert.Equal(2, organizationIdsList.Count);\n                Assert.Contains(organizationId1.ToString(), organizationIdsList);\n                Assert.Contains(organizationId2.ToString(), organizationIdsList);\n            }), installationId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task SaveAsync_IdNotProvided_CreatedAndPushRegistration(Guid userId, Guid organizationId1,\n        Guid organizationId2, Guid installationId,\n        OrganizationUserOrganizationDetails organizationUserOrganizationDetails1,\n        OrganizationUserOrganizationDetails organizationUserOrganizationDetails2)\n    {\n        organizationUserOrganizationDetails1.OrganizationId = organizationId1;\n        organizationUserOrganizationDetails2.OrganizationId = organizationId2;\n\n        var deviceRepo = Substitute.For<IDeviceRepository>();\n        var pushRepo = Substitute.For<IPushRegistrationService>();\n        var organizationUserRepository = Substitute.For<IOrganizationUserRepository>();\n        organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType?>())\n            .Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]);\n        var globalSettings = Substitute.For<IGlobalSettings>();\n        globalSettings.Installation.Id.Returns(installationId);\n        var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings);\n\n        var device = new Device\n        {\n            Name = \"test device\",\n            Type = DeviceType.Android,\n            UserId = userId,\n            PushToken = \"testToken\",\n            Identifier = \"testid\"\n        };\n        await deviceService.SaveAsync(device);\n\n        await pushRepo.Received(1).CreateOrUpdateRegistrationAsync(Arg.Is<PushRegistrationData>(v => v.Token == \"testToken\"),\n            Arg.Do<string>(id => Guid.TryParse(id, out var _)), userId.ToString(), \"testid\", DeviceType.Android,\n            Arg.Do<IEnumerable<string>>(organizationIds =>\n            {\n                var organizationIdsList = organizationIds.ToList();\n                Assert.Equal(2, organizationIdsList.Count);\n                Assert.Contains(organizationId1.ToString(), organizationIdsList);\n                Assert.Contains(organizationId2.ToString(), organizationIdsList);\n            }), installationId);\n    }\n\n    /// <summary>\n    /// Story: A user chose to keep trust in one of their current trusted devices, but not in another one of their\n    /// devices. We will rotate the trust of the currently signed in device as well as the device they chose but will\n    /// remove the trust of the device they didn't give new keys for.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task UpdateDevicesTrustAsync_Works(\n        SutProvider<DeviceService> sutProvider,\n        Guid currentUserId,\n        Device deviceOne,\n        Device deviceTwo,\n        Device deviceThree)\n    {\n        SetupOldTrust(deviceOne);\n        SetupOldTrust(deviceTwo);\n        SetupOldTrust(deviceThree);\n\n        deviceOne.Identifier = \"current_device\";\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(currentUserId)\n            .Returns(new List<Device> { deviceOne, deviceTwo, deviceThree, });\n\n        var currentDeviceModel = new DeviceKeysUpdateRequestModel\n        {\n            EncryptedPublicKey = \"current_encrypted_public_key\",\n            EncryptedUserKey = \"current_encrypted_user_key\",\n        };\n\n        var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel>\n        {\n            new OtherDeviceKeysUpdateRequestModel\n            {\n                DeviceId = deviceTwo.Id,\n                EncryptedPublicKey = \"encrypted_public_key_two\",\n                EncryptedUserKey = \"encrypted_user_key_two\",\n            },\n        };\n\n        await sutProvider.Sut.UpdateDevicesTrustAsync(\"current_device\", currentUserId, currentDeviceModel,\n            alteredDeviceModels);\n\n        // Updating trust, \"current\" or \"other\" only needs to change the EncryptedPublicKey & EncryptedUserKey\n        await sutProvider.GetDependency<IDeviceRepository>()\n            .Received(1)\n            .UpsertAsync(Arg.Is<Device>(d =>\n                d.Id == deviceOne.Id &&\n                d.EncryptedPublicKey == \"current_encrypted_public_key\" &&\n                d.EncryptedUserKey == \"current_encrypted_user_key\" &&\n                d.EncryptedPrivateKey == \"old_private_deviceOne\"));\n\n        await sutProvider.GetDependency<IDeviceRepository>()\n            .Received(1)\n            .UpsertAsync(Arg.Is<Device>(d =>\n                d.Id == deviceTwo.Id &&\n                d.EncryptedPublicKey == \"encrypted_public_key_two\" &&\n                d.EncryptedUserKey == \"encrypted_user_key_two\" &&\n                d.EncryptedPrivateKey == \"old_private_deviceTwo\"));\n\n        // Clearing trust should remove all key values\n        await sutProvider.GetDependency<IDeviceRepository>()\n            .Received(1)\n            .UpsertAsync(Arg.Is<Device>(d =>\n                d.Id == deviceThree.Id &&\n                d.EncryptedPublicKey == null &&\n                d.EncryptedUserKey == null &&\n                d.EncryptedPrivateKey == null));\n\n        // Should have recieved a total of 3 calls, the ones asserted above\n        await sutProvider.GetDependency<IDeviceRepository>()\n            .Received(3)\n            .UpsertAsync(Arg.Any<Device>());\n\n        static void SetupOldTrust(Device device, [CallerArgumentExpression(nameof(device))] string expression = null)\n        {\n            device.EncryptedPublicKey = $\"old_public_{expression}\";\n            device.EncryptedPrivateKey = $\"old_private_{expression}\";\n            device.EncryptedUserKey = $\"old_user_{expression}\";\n        }\n    }\n\n    /// <summary>\n    /// Story: This could result from a poor implementation of this method, if they attempt add trust to a device\n    /// that doesn't already have trust. They would have to create brand new values and for that values to be accurate\n    /// they would technically have all the values needed to trust a device, that is why we don't consider this bad\n    /// enough to throw but do skip it because we'd rather keep number of ways for trust to be added to the endpoint we\n    /// already have.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task UpdateDevicesTrustAsync_DoesNotUpdateUntrustedDevices(\n        SutProvider<DeviceService> sutProvider,\n        Guid currentUserId,\n        Device deviceOne,\n        Device deviceTwo)\n    {\n        deviceOne.Identifier = \"current_device\";\n\n        // Make deviceTwo untrusted\n        deviceTwo.EncryptedUserKey = string.Empty;\n        deviceTwo.EncryptedPublicKey = string.Empty;\n        deviceTwo.EncryptedPrivateKey = string.Empty;\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(currentUserId)\n            .Returns(new List<Device> { deviceOne, deviceTwo, });\n\n        var currentDeviceModel = new DeviceKeysUpdateRequestModel\n        {\n            EncryptedPublicKey = \"current_encrypted_public_key\",\n            EncryptedUserKey = \"current_encrypted_user_key\",\n        };\n\n        var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel>\n        {\n            new OtherDeviceKeysUpdateRequestModel\n            {\n                DeviceId = deviceTwo.Id,\n                EncryptedPublicKey = \"encrypted_public_key_two\",\n                EncryptedUserKey = \"encrypted_user_key_two\",\n            },\n        };\n\n        await sutProvider.Sut.UpdateDevicesTrustAsync(\"current_device\", currentUserId, currentDeviceModel,\n            alteredDeviceModels);\n\n        // Check that UpsertAsync was called for the trusted device\n        await sutProvider.GetDependency<IDeviceRepository>()\n            .Received(1)\n            .UpsertAsync(Arg.Is<Device>(d =>\n                d.Id == deviceOne.Id &&\n                d.EncryptedPublicKey == \"current_encrypted_public_key\" &&\n                d.EncryptedUserKey == \"current_encrypted_user_key\"));\n\n        // Check that UpsertAsync was not called for the untrusted device\n        await sutProvider.GetDependency<IDeviceRepository>()\n            .DidNotReceive()\n            .UpsertAsync(Arg.Is<Device>(d => d.Id == deviceTwo.Id));\n    }\n\n    /// <summary>\n    /// Story: This should only happen if someone were to take the access token from a different device and try to rotate\n    /// a device that they don't actually have.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task UpdateDevicesTrustAsync_ThrowsNotFoundException_WhenCurrentDeviceIdentifierDoesNotExist(\n        SutProvider<DeviceService> sutProvider,\n        Guid currentUserId,\n        Device deviceOne,\n        Device deviceTwo)\n    {\n        deviceOne.Identifier = \"some_other_device\";\n        deviceTwo.Identifier = \"another_device\";\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(currentUserId)\n            .Returns(new List<Device> { deviceOne, deviceTwo, });\n\n        var currentDeviceModel = new DeviceKeysUpdateRequestModel\n        {\n            EncryptedPublicKey = \"current_encrypted_public_key\",\n            EncryptedUserKey = \"current_encrypted_user_key\",\n        };\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            sutProvider.Sut.UpdateDevicesTrustAsync(\"current_device\", currentUserId, currentDeviceModel,\n                Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>()));\n    }\n\n    /// <summary>\n    /// Story: This should only happen from a poorly implemented user of this method but important to enforce someone\n    /// using the method correctly, a device should only be rotated intentionally and including it as both the current\n    /// device and one of the users other device would mean they could rotate it twice and we aren't sure\n    /// which one they would want to win out.\n    /// </summary>\n    [Theory, BitAutoData]\n    public async Task UpdateDevicesTrustAsync_ThrowsBadRequestException_WhenCurrentDeviceIsIncludedInAlteredDevices(\n        SutProvider<DeviceService> sutProvider,\n        Guid currentUserId,\n        Device deviceOne,\n        Device deviceTwo)\n    {\n        deviceOne.Identifier = \"current_device\";\n\n        sutProvider.GetDependency<IDeviceRepository>()\n            .GetManyByUserIdAsync(currentUserId)\n            .Returns(new List<Device> { deviceOne, deviceTwo, });\n\n        var currentDeviceModel = new DeviceKeysUpdateRequestModel\n        {\n            EncryptedPublicKey = \"current_encrypted_public_key\",\n            EncryptedUserKey = \"current_encrypted_user_key\",\n        };\n\n        var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel>\n        {\n            new OtherDeviceKeysUpdateRequestModel\n            {\n                DeviceId = deviceOne.Id, // current device is included in alteredDevices\n                EncryptedPublicKey = \"encrypted_public_key_one\",\n                EncryptedUserKey = \"encrypted_user_key_one\",\n            },\n        };\n\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.UpdateDevicesTrustAsync(\"current_device\", currentUserId, currentDeviceModel,\n                alteredDeviceModels));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/HandlebarsMailServiceTests.cs",
    "content": "﻿using System.Reflection;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.Auth.Entities;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models.Business;\nusing Bit.Core.Entities;\nusing Bit.Core.Models.Mail;\nusing Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Platform.Mail.Enqueuing;\nusing Bit.Core.Services;\nusing Bit.Core.Services.Mail;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\npublic class HandlebarsMailServiceTests\n{\n    private readonly HandlebarsMailService _sut;\n\n    private readonly GlobalSettings _globalSettings;\n    private readonly IMailDeliveryService _mailDeliveryService;\n    private readonly IMailEnqueuingService _mailEnqueuingService;\n    private readonly IDistributedCache _distributedCache;\n    private readonly ILogger<HandlebarsMailService> _logger;\n\n    public HandlebarsMailServiceTests()\n    {\n        _globalSettings = new GlobalSettings();\n        _mailDeliveryService = Substitute.For<IMailDeliveryService>();\n        _mailEnqueuingService = Substitute.For<IMailEnqueuingService>();\n        _distributedCache = Substitute.For<IDistributedCache>();\n        _logger = Substitute.For<ILogger<HandlebarsMailService>>();\n\n        _sut = new HandlebarsMailService(\n            _globalSettings,\n            _mailDeliveryService,\n            _mailEnqueuingService,\n            _distributedCache,\n            _logger\n        );\n    }\n\n    [Fact]\n    public async Task SendFailedTwoFactorAttemptEmailAsync_FirstCall_SendsEmail()\n    {\n        // Arrange\n        var email = \"test@example.com\";\n        var failedType = TwoFactorProviderType.Email;\n        var utcNow = DateTime.UtcNow;\n        var ip = \"192.168.1.1\";\n\n        _distributedCache.GetAsync(Arg.Any<string>()).Returns((byte[])null);\n\n        // Act\n        await _sut.SendFailedTwoFactorAttemptEmailAsync(email, failedType, utcNow, ip);\n\n        // Assert\n        await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any<MailMessage>());\n        await _distributedCache.Received(1).SetAsync(\n            Arg.Is<string>(key => key == $\"FailedTwoFactorAttemptEmail_{email}\"),\n            Arg.Any<byte[]>(),\n            Arg.Any<DistributedCacheEntryOptions>()\n        );\n    }\n\n    [Fact]\n    public async Task SendFailedTwoFactorAttemptEmailAsync_SecondCallWithinHour_DoesNotSendEmail()\n    {\n        // Arrange\n        var email = \"test@example.com\";\n        var failedType = TwoFactorProviderType.Email;\n        var utcNow = DateTime.UtcNow;\n        var ip = \"192.168.1.1\";\n\n        // Simulate cache hit (email was already sent)\n        _distributedCache.GetAsync(Arg.Any<string>()).Returns([1]);\n\n        // Act\n        await _sut.SendFailedTwoFactorAttemptEmailAsync(email, failedType, utcNow, ip);\n\n        // Assert\n        await _mailDeliveryService.DidNotReceive().SendEmailAsync(Arg.Any<MailMessage>());\n        await _distributedCache.DidNotReceive().SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n    }\n\n    [Fact]\n    public async Task SendFailedTwoFactorAttemptEmailAsync_DifferentEmails_SendsBothEmails()\n    {\n        // Arrange\n        var email1 = \"test1@example.com\";\n        var email2 = \"test2@example.com\";\n        var failedType = TwoFactorProviderType.Email;\n        var utcNow = DateTime.UtcNow;\n        var ip = \"192.168.1.1\";\n\n        _distributedCache.GetAsync(Arg.Any<string>()).Returns((byte[])null);\n\n        // Act\n        await _sut.SendFailedTwoFactorAttemptEmailAsync(email1, failedType, utcNow, ip);\n        await _sut.SendFailedTwoFactorAttemptEmailAsync(email2, failedType, utcNow, ip);\n\n        // Assert\n        await _mailDeliveryService.Received(2).SendEmailAsync(Arg.Any<MailMessage>());\n        await _distributedCache.Received(1).SetAsync(\n            Arg.Is<string>(key => key == $\"FailedTwoFactorAttemptEmail_{email1}\"),\n            Arg.Any<byte[]>(),\n            Arg.Any<DistributedCacheEntryOptions>()\n        );\n        await _distributedCache.Received(1).SetAsync(\n            Arg.Is<string>(key => key == $\"FailedTwoFactorAttemptEmail_{email2}\"),\n            Arg.Any<byte[]>(),\n            Arg.Any<DistributedCacheEntryOptions>()\n        );\n    }\n\n    [Fact(Skip = \"For local development\")]\n    public async Task SendAllEmails()\n    {\n        // This test is only opt in and is more for development purposes.\n        // This will send all emails to the test email address so that they can be viewed.\n        var namedParameters = new Dictionary<(string, Type), object>\n        {\n            // TODO: Switch to use env variable\n            { (\"email\", typeof(string)), \"test@bitwarden.com\" },\n            { (\"user\", typeof(User)), new User\n            {\n                Id = Guid.NewGuid(),\n                Email = \"test@bitwarden.com\",\n            }},\n            { (\"userId\", typeof(Guid)), Guid.NewGuid() },\n            { (\"token\", typeof(string)), \"test_token\" },\n            { (\"fromEmail\", typeof(string)), \"test@bitwarden.com\" },\n            { (\"toEmail\", typeof(string)), \"test@bitwarden.com\" },\n            { (\"newEmailAddress\", typeof(string)), \"test@bitwarden.com\" },\n            { (\"hint\", typeof(string)), \"Test Hint\" },\n            { (\"organizationName\", typeof(string)), \"Test Organization Name\" },\n            { (\"orgUser\", typeof(OrganizationUser)), new OrganizationUser\n            {\n                Id = Guid.NewGuid(),\n                Email = \"test@bitwarden.com\",\n                OrganizationId = Guid.NewGuid(),\n\n            }},\n            { (\"token\", typeof(ExpiringToken)), new ExpiringToken(\"test_token\", DateTime.UtcNow.AddDays(1))},\n            { (\"organization\", typeof(Organization)), new Organization\n            {\n                Id = Guid.NewGuid(),\n                Name = \"Test Organization Name\",\n                Seats = 5\n            }},\n            { (\"initialSeatCount\", typeof(int)), 5},\n            { (\"ownerEmails\", typeof(IEnumerable<string>)), new [] { \"test@bitwarden.com\" }},\n            { (\"maxSeatCount\", typeof(int)), 5 },\n            { (\"userIdentifier\", typeof(string)), \"test_user\" },\n            { (\"adminEmails\", typeof(IEnumerable<string>)), new [] { \"test@bitwarden.com\" }},\n            { (\"returnUrl\", typeof(string)), \"https://bitwarden.com/\" },\n            { (\"amount\", typeof(decimal)), 1.00M },\n            { (\"dueDate\", typeof(DateTime)), DateTime.UtcNow.AddDays(1) },\n            { (\"items\", typeof(List<string>)), new List<string> { \"test@bitwarden.com\" }},\n            { (\"mentionInvoices\", typeof(bool)), true },\n            { (\"emails\", typeof(IEnumerable<string>)), new [] { \"test@bitwarden.com\" }},\n            { (\"deviceType\", typeof(string)), \"Mobile\" },\n            { (\"timestamp\", typeof(DateTime)), DateTime.UtcNow.AddDays(1)},\n            { (\"ip\", typeof(string)), \"127.0.0.1\" },\n            { (\"emergencyAccess\", typeof(EmergencyAccess)), new EmergencyAccess\n            {\n                Id = Guid.NewGuid(),\n                Email = \"test@bitwarden.com\",\n            }},\n            { (\"granteeEmail\", typeof(string)), \"test@bitwarden.com\" },\n            { (\"grantorName\", typeof(string)), \"Test User\" },\n            { (\"initiatingName\", typeof(string)), \"Test\" },\n            { (\"approvingName\", typeof(string)), \"Test Name\" },\n            { (\"rejectingName\", typeof(string)), \"Test Name\" },\n            { (\"provider\", typeof(Provider)), new Provider\n            {\n                Id = Guid.NewGuid(),\n            }},\n            { (\"name\", typeof(string)), \"Test Name\" },\n            { (\"ea\", typeof(EmergencyAccess)), new EmergencyAccess\n            {\n                Id = Guid.NewGuid(),\n                Email = \"test@bitwarden.com\",\n            }},\n            { (\"userName\", typeof(string)), \"testUser\" },\n            { (\"orgName\", typeof(string)), \"Test Org Name\" },\n            { (\"providerName\", typeof(string)), \"testProvider\" },\n            { (\"providerUser\", typeof(ProviderUser)), new ProviderUser\n            {\n                ProviderId = Guid.NewGuid(),\n                Id = Guid.NewGuid(),\n            }},\n            { (\"familyUserEmail\", typeof(string)), \"test@bitwarden.com\" },\n            { (\"sponsorEmail\", typeof(string)), \"test@bitwarden.com\" },\n            { (\"familyOrgName\", typeof(string)), \"Test Org Name\" },\n            // Swap existingAccount to true or false to generate different versions of the SendFamiliesForEnterpriseOfferEmailAsync emails.\n            { (\"existingAccount\", typeof(bool)), false },\n            { (\"sponsorshipEndDate\", typeof(DateTime)), DateTime.UtcNow.AddDays(1)},\n            { (\"sponsorOrgName\", typeof(string)), \"Sponsor Test Org Name\" },\n            { (\"expirationDate\", typeof(DateTime)), DateTime.Now.AddDays(3) },\n            { (\"utcNow\", typeof(DateTime)), DateTime.UtcNow },\n        };\n\n        var globalSettings = new GlobalSettings\n        {\n            Mail = new GlobalSettings.MailSettings\n            {\n                Smtp = new GlobalSettings.MailSettings.SmtpSettings\n                {\n                    Host = \"localhost\",\n                    TrustServer = true,\n                    Port = 10250,\n                },\n                ReplyToEmail = \"noreply@bitwarden.com\",\n            },\n            SiteName = \"Bitwarden\",\n        };\n\n        var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>());\n        var distributedCache = Substitute.For<IDistributedCache>();\n        var logger = Substitute.For<ILogger<HandlebarsMailService>>();\n\n        var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService(), distributedCache, logger);\n\n        var sendMethods = typeof(IMailService).GetMethods(BindingFlags.Public | BindingFlags.Instance)\n            .Where(m => m.Name.StartsWith(\"Send\") && m.Name != \"SendEnqueuedMailMessageAsync\");\n\n        foreach (var sendMethod in sendMethods)\n        {\n            await InvokeMethod(sendMethod);\n        }\n\n        async Task InvokeMethod(MethodInfo method)\n        {\n            var parameters = method.GetParameters();\n            var args = new object[parameters.Length];\n\n            for (var i = 0; i < parameters.Length; i++)\n            {\n                if (!namedParameters.TryGetValue((parameters[i].Name, parameters[i].ParameterType), out var value))\n                {\n                    throw new InvalidOperationException($\"Couldn't find a parameter for name '{parameters[i].Name}' and type '{parameters[i].ParameterType.FullName}'\");\n                }\n\n                args[i] = value;\n            }\n\n            await (Task)method.Invoke(handlebarsService, args);\n        }\n    }\n\n    [Fact]\n    public async Task SendIndividualUserWelcomeEmailAsync_SendsCorrectEmail()\n    {\n        // Arrange\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = \"test@example.com\"\n        };\n\n        // Act\n        await _sut.SendIndividualUserWelcomeEmailAsync(user);\n\n        // Assert\n        await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>\n            m.MetaData != null &&\n            m.ToEmails.Contains(\"test@example.com\") &&\n            m.Subject == \"Welcome to Bitwarden!\" &&\n            m.Category == \"Welcome\"));\n    }\n\n    [Fact]\n    public async Task SendOrganizationUserWelcomeEmailAsync_SendsCorrectEmailWithOrganizationName()\n    {\n        // Arrange\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = \"user@company.com\"\n        };\n        var organizationName = \"Bitwarden Corp\";\n\n        // Act\n        await _sut.SendOrganizationUserWelcomeEmailAsync(user, organizationName);\n\n        // Assert\n        await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>\n            m.MetaData != null &&\n            m.ToEmails.Contains(\"user@company.com\") &&\n            m.Subject == \"Welcome to Bitwarden!\" &&\n            m.HtmlContent.Contains(\"Bitwarden Corp\") &&\n            m.Category == \"Welcome\"));\n    }\n\n    [Fact]\n    public async Task SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync_SendsCorrectEmailWithFamilyTemplate()\n    {\n        // Arrange\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = \"family@example.com\"\n        };\n        var familyOrganizationName = \"Smith Family\";\n\n        // Act\n        await _sut.SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, familyOrganizationName);\n\n        // Assert\n        await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>\n            m.MetaData != null &&\n            m.ToEmails.Contains(\"family@example.com\") &&\n            m.Subject == \"Welcome to Bitwarden!\" &&\n            m.HtmlContent.Contains(\"Smith Family\") &&\n            m.Category == \"Welcome\"));\n    }\n\n    [Theory]\n    [InlineData(\"Acme Corp\", \"Acme Corp\")]\n    [InlineData(\"Company & Associates\", \"Company &amp; Associates\")]\n    [InlineData(\"Test \\\"Quoted\\\" Org\", \"Test &quot;Quoted&quot; Org\")]\n    public async Task SendOrganizationUserWelcomeEmailAsync_SanitizesOrganizationNameForEmail(string inputOrgName, string expectedSanitized)\n    {\n        // Arrange\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = \"test@example.com\"\n        };\n\n        // Act\n        await _sut.SendOrganizationUserWelcomeEmailAsync(user, inputOrgName);\n\n        // Assert\n        await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>\n            m.HtmlContent.Contains(expectedSanitized) &&\n            !m.HtmlContent.Contains(\"<script>\") && // Ensure script tags are removed\n            m.Category == \"Welcome\"));\n    }\n\n    [Theory]\n    [InlineData(\"test@example.com\")]\n    [InlineData(\"user+tag@domain.co.uk\")]\n    [InlineData(\"admin@organization.org\")]\n    public async Task SendIndividualUserWelcomeEmailAsync_HandlesVariousEmailFormats(string email)\n    {\n        // Arrange\n        var user = new User\n        {\n            Id = Guid.NewGuid(),\n            Email = email\n        };\n\n        // Act\n        await _sut.SendIndividualUserWelcomeEmailAsync(user);\n\n        // Assert\n        await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Is<MailMessage>(m =>\n            m.ToEmails.Contains(email)));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/Implementations/BraintreeServiceTests.cs",
    "content": "﻿using Bit.Core.Billing.Constants;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Braintree;\nusing Braintree.Exceptions;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nusing BraintreeService = Bit.Core.Services.Implementations.BraintreeService;\nusing Customer = Stripe.Customer;\n\nnamespace Bit.Core.Test.Services.Implementations;\n\npublic class BraintreeServiceTests\n{\n    private readonly ICustomerGateway _customerGateway;\n    private readonly BraintreeService _sut;\n\n    public BraintreeServiceTests()\n    {\n        var braintreeGateway = Substitute.For<IBraintreeGateway>();\n        _customerGateway = Substitute.For<ICustomerGateway>();\n        braintreeGateway.Customer.Returns(_customerGateway);\n\n        var globalSettings = Substitute.For<IGlobalSettings>();\n        var logger = Substitute.For<ILogger<BraintreeService>>();\n        var mailService = Substitute.For<IMailService>();\n        var stripeAdapter = Substitute.For<IStripeAdapter>();\n\n        _sut = new BraintreeService(\n            braintreeGateway,\n            globalSettings,\n            logger,\n            mailService,\n            stripeAdapter);\n    }\n\n    #region GetCustomer\n\n    [Fact]\n    public async Task GetCustomer_NoBraintreeCustomerIdInMetadata_ReturnsNull()\n    {\n        // Arrange\n        var stripeCustomer = new Customer\n        {\n            Id = \"cus_123\",\n            Metadata = new Dictionary<string, string>()\n        };\n\n        // Act\n        var result = await _sut.GetCustomer(stripeCustomer);\n\n        // Assert\n        Assert.Null(result);\n        await _customerGateway.DidNotReceiveWithAnyArgs().FindAsync(Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task GetCustomer_BraintreeCustomerFound_ReturnsCustomer()\n    {\n        // Arrange\n        const string braintreeCustomerId = \"bt_customer_123\";\n\n        var stripeCustomer = new Customer\n        {\n            Id = \"cus_123\",\n            Metadata = new Dictionary<string, string>\n            {\n                [StripeConstants.MetadataKeys.BraintreeCustomerId] = braintreeCustomerId\n            }\n        };\n\n        var braintreeCustomer = Substitute.For<Braintree.Customer>();\n\n        _customerGateway\n            .FindAsync(braintreeCustomerId)\n            .Returns(braintreeCustomer);\n\n        // Act\n        var result = await _sut.GetCustomer(stripeCustomer);\n\n        // Assert\n        Assert.NotNull(result);\n        Assert.Same(braintreeCustomer, result);\n        await _customerGateway.Received(1).FindAsync(braintreeCustomerId);\n    }\n\n    [Fact]\n    public async Task GetCustomer_BraintreeCustomerNotFound_LogsWarningAndReturnsNull()\n    {\n        // Arrange\n        const string braintreeCustomerId = \"bt_non_existent_customer\";\n\n        var stripeCustomer = new Customer\n        {\n            Id = \"cus_123\",\n            Metadata = new Dictionary<string, string>\n            {\n                [StripeConstants.MetadataKeys.BraintreeCustomerId] = braintreeCustomerId\n            }\n        };\n\n        _customerGateway\n            .FindAsync(braintreeCustomerId)\n            .Returns<Braintree.Customer>(_ => throw new NotFoundException());\n\n        // Act\n        var result = await _sut.GetCustomer(stripeCustomer);\n\n        // Assert\n        Assert.Null(result);\n        await _customerGateway.Received(1).FindAsync(braintreeCustomerId);\n    }\n\n    #endregion\n}\n"
  },
  {
    "path": "test/Core.Test/Services/Implementations/FeatureRoutedCacheServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.AbilitiesCache;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Entities.Provider;\nusing Bit.Core.AdminConsole.Models.Data.Provider;\nusing Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Services.Implementations;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.Services.Implementations;\n\n[SutProviderCustomize]\npublic class FeatureRoutedCacheServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task GetOrganizationAbilitiesAsync_ReturnsFromInMemoryService(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        IDictionary<Guid, OrganizationAbility> expectedResult)\n    {\n        // Arrange\n        sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .GetOrganizationAbilitiesAsync()\n            .Returns(expectedResult);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync();\n\n        // Assert\n        Assert.Equal(expectedResult, result);\n        await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .Received(1)\n            .GetOrganizationAbilitiesAsync();\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationAbilityAsync_ReturnsFromInMemoryService(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Guid orgId,\n        OrganizationAbility expectedResult)\n    {\n        // Arrange\n        sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .GetOrganizationAbilityAsync(orgId)\n            .Returns(expectedResult);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId);\n\n        // Assert\n        Assert.Equal(expectedResult, result);\n        await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .Received(1)\n            .GetOrganizationAbilityAsync(orgId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetProviderAbilitiesAsync_ReturnsFromInMemoryService(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        IDictionary<Guid, ProviderAbility> expectedResult)\n    {\n        // Arrange\n        sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .GetProviderAbilitiesAsync()\n            .Returns(expectedResult);\n\n        // Act\n        var result = await sutProvider.Sut.GetProviderAbilitiesAsync();\n\n        // Assert\n        Assert.Equal(expectedResult, result);\n        await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .Received(1)\n            .GetProviderAbilitiesAsync();\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetProviderAbilityAsync_WhenProviderExists_ReturnsAbility(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        ProviderAbility providerAbility)\n    {\n        // Arrange\n        var allAbilities = new Dictionary<Guid, ProviderAbility> { [providerAbility.Id] = providerAbility };\n        sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .GetProviderAbilitiesAsync()\n            .Returns(allAbilities);\n\n        // Act\n        var result = await sutProvider.Sut.GetProviderAbilityAsync(providerAbility.Id);\n\n        // Assert\n        Assert.Equal(providerAbility, result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetProviderAbilityAsync_WhenProviderDoesNotExist_ReturnsNull(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Guid providerId)\n    {\n        // Arrange\n        sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .GetProviderAbilitiesAsync()\n            .Returns(new Dictionary<Guid, ProviderAbility>());\n\n        // Act\n        var result = await sutProvider.Sut.GetProviderAbilityAsync(providerId);\n\n        // Assert\n        Assert.Null(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetProviderAbilitiesAsync_ReturnsOnlyMatchingAbilities(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        ProviderAbility matchedAbility,\n        ProviderAbility unmatchedAbility)\n    {\n        // Arrange\n        var allAbilities = new Dictionary<Guid, ProviderAbility>\n        {\n            [matchedAbility.Id] = matchedAbility,\n            [unmatchedAbility.Id] = unmatchedAbility\n        };\n        sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .GetProviderAbilitiesAsync()\n            .Returns(allAbilities);\n\n        // Act\n        var result = await sutProvider.Sut.GetProviderAbilitiesAsync([matchedAbility.Id]);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(matchedAbility, result[matchedAbility.Id]);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetProviderAbilitiesAsync_WhenNoIdsMatched_ReturnsEmptyDictionary(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Guid missingProviderId)\n    {\n        // Arrange\n        sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .GetProviderAbilitiesAsync()\n            .Returns(new Dictionary<Guid, ProviderAbility>());\n\n        // Act\n        var result = await sutProvider.Sut.GetProviderAbilitiesAsync([missingProviderId]);\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationAbilitiesAsync_ReturnsOnlyMatchingAbilities(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        OrganizationAbility matchedAbility,\n        OrganizationAbility unmatchedAbility)\n    {\n        // Arrange\n        var allAbilities = new Dictionary<Guid, OrganizationAbility>\n        {\n            [matchedAbility.Id] = matchedAbility,\n            [unmatchedAbility.Id] = unmatchedAbility\n        };\n        sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .GetOrganizationAbilitiesAsync()\n            .Returns(allAbilities);\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync([matchedAbility.Id]);\n\n        // Assert\n        Assert.Single(result);\n        Assert.Equal(matchedAbility, result[matchedAbility.Id]);\n    }\n\n    [Theory, BitAutoData]\n    public async Task GetOrganizationAbilitiesAsync_WhenNoIdsMatched_ReturnsEmptyDictionary(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Guid missingOrgId)\n    {\n        // Arrange\n        sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .GetOrganizationAbilitiesAsync()\n            .Returns(new Dictionary<Guid, OrganizationAbility>());\n\n        // Act\n        var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync([missingOrgId]);\n\n        // Assert\n        Assert.Empty(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpsertOrganizationAbilityAsync_CallsInMemoryService(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Organization organization)\n    {\n        // Act\n        await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization);\n\n        // Assert\n        await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .Received(1)\n            .UpsertOrganizationAbilityAsync(organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpsertProviderAbilityAsync_CallsInMemoryService(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Provider provider)\n    {\n        // Act\n        await sutProvider.Sut.UpsertProviderAbilityAsync(provider);\n\n        // Assert\n        await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .Received(1)\n            .UpsertProviderAbilityAsync(provider);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteOrganizationAbilityAsync_CallsInMemoryService(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Guid organizationId)\n    {\n        // Act\n        await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId);\n\n        // Assert\n        await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .Received(1)\n            .DeleteOrganizationAbilityAsync(organizationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DeleteProviderAbilityAsync_CallsInMemoryService(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Guid providerId)\n    {\n        // Act\n        await sutProvider.Sut.DeleteProviderAbilityAsync(providerId);\n\n        // Assert\n        await sutProvider.GetDependency<IVCurrentInMemoryApplicationCacheService>()\n            .Received(1)\n            .DeleteProviderAbilityAsync(providerId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BaseUpsertOrganizationAbilityAsync_CallsServiceBusCache(\n        Organization organization)\n    {\n        // Arrange\n        var currentCacheService = CreateCurrentCacheMockService();\n        var sut = new FeatureRoutedCacheService(currentCacheService);\n\n        // Act\n        await sut.BaseUpsertOrganizationAbilityAsync(organization);\n\n        // Assert\n        await currentCacheService\n            .Received(1)\n            .BaseUpsertOrganizationAbilityAsync(organization);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BaseUpsertOrganizationAbilityAsync_WhenServiceIsNotServiceBusCache_ThrowsException(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Organization organization)\n    {\n        // Act\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(\n            () => sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization));\n\n        // Assert\n        Assert.Equal(ExpectedErrorMessage, ex.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BaseDeleteOrganizationAbilityAsync_CallsServiceBusCache(\n        Guid organizationId)\n    {\n        // Arrange\n        var currentCacheService = CreateCurrentCacheMockService();\n        var sut = new FeatureRoutedCacheService(currentCacheService);\n\n        // Act\n        await sut.BaseDeleteOrganizationAbilityAsync(organizationId);\n\n        // Assert\n        await currentCacheService\n            .Received(1)\n            .BaseDeleteOrganizationAbilityAsync(organizationId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task BaseDeleteOrganizationAbilityAsync_WhenServiceIsNotServiceBusCache_ThrowsException(\n        SutProvider<FeatureRoutedCacheService> sutProvider,\n        Guid organizationId)\n    {\n        // Act\n        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>\n            sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId));\n\n        // Assert\n        Assert.Equal(ExpectedErrorMessage, ex.Message);\n    }\n\n    /// <summary>\n    /// Our SUT uses a method that is not part of IVCurrentInMemoryApplicationCacheService,\n    /// so AutoFixture's auto-created mock won't work.\n    /// </summary>\n    private static InMemoryServiceBusApplicationCacheService CreateCurrentCacheMockService()\n    {\n        return Substitute.For<InMemoryServiceBusApplicationCacheService>(\n            Substitute.For<IOrganizationRepository>(),\n            Substitute.For<IProviderRepository>(),\n            new GlobalSettings\n            {\n                ProjectName = \"BitwardenTest\",\n                ServiceBus = new GlobalSettings.ServiceBusSettings\n                {\n                    ConnectionString = \"Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test\",\n                    ApplicationCacheTopicName = \"test-topic\",\n                    ApplicationCacheSubscriptionName = \"test-subscription\"\n                }\n            });\n    }\n\n    private static string ExpectedErrorMessage =>\n        \"Expected inMemoryApplicationCacheService to be of type InMemoryServiceBusApplicationCacheService\";\n}\n"
  },
  {
    "path": "test/Core.Test/Services/InMemoryApplicationCacheServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\npublic class InMemoryApplicationCacheServiceTests\n{\n    private readonly InMemoryApplicationCacheService _sut;\n\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IProviderRepository _providerRepository;\n\n    public InMemoryApplicationCacheServiceTests()\n    {\n        _organizationRepository = Substitute.For<IOrganizationRepository>();\n        _providerRepository = Substitute.For<IProviderRepository>();\n\n        _sut = new InMemoryApplicationCacheService(_organizationRepository, _providerRepository);\n    }\n\n    // Remove this test when we add actual tests. It only proves that\n    // we've properly constructed the system under test.\n    [Fact]\n    public void ServiceExists()\n    {\n        Assert.NotNull(_sut);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/InMemoryServiceBusApplicationCacheServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Repositories;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\npublic class InMemoryServiceBusApplicationCacheServiceTests\n{\n    private readonly InMemoryServiceBusApplicationCacheService _sut;\n\n    private readonly IOrganizationRepository _organizationRepository;\n    private readonly IProviderRepository _providerRepository;\n    private readonly GlobalSettings _globalSettings;\n\n    public InMemoryServiceBusApplicationCacheServiceTests()\n    {\n        _organizationRepository = Substitute.For<IOrganizationRepository>();\n        _providerRepository = Substitute.For<IProviderRepository>();\n        _globalSettings = new GlobalSettings();\n\n        _sut = new InMemoryServiceBusApplicationCacheService(\n            _organizationRepository,\n            _providerRepository,\n            _globalSettings\n        );\n    }\n\n    // Remove this test when we add actual tests. It only proves that\n    // we've properly constructed the system under test.\n    [Fact(Skip = \"Needs additional work\")]\n    public void ServiceExists()\n    {\n        Assert.NotNull(_sut);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs",
    "content": "﻿using AutoFixture;\nusing Bit.Core.Context;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing LaunchDarkly.Sdk.Server.Interfaces;\nusing NSubstitute;\nusing Xunit;\nusing GlobalSettings = Bit.Core.Settings.GlobalSettings;\n\nnamespace Bit.Core.Test.Services;\n\n[SutProviderCustomize]\npublic class LaunchDarklyFeatureServiceTests\n{\n    private const string _fakeFeatureKey = \"somekey\";\n    private const string _fakeSdkKey = \"somesdkkey\";\n\n    private static SutProvider<LaunchDarklyFeatureService> GetSutProvider(IGlobalSettings globalSettings)\n    {\n        globalSettings.ProjectName = \"LaunchDarkly Tests\";\n\n        var currentContext = Substitute.For<ICurrentContext>();\n        currentContext.DeviceIdentifier.Returns(Guid.NewGuid().ToString());\n        currentContext.UserId.Returns(Guid.NewGuid());\n        currentContext.ClientVersion.Returns(new Version(AssemblyHelpers.GetVersion()));\n        currentContext.ClientVersionIsPrerelease.Returns(true);\n        currentContext.DeviceType.Returns(Enums.DeviceType.ChromeBrowser);\n\n        var client = Substitute.For<ILdClient>();\n\n        var fixture = new Fixture();\n        return new SutProvider<LaunchDarklyFeatureService>(fixture)\n            .SetDependency(globalSettings)\n            .SetDependency(currentContext)\n            .SetDependency(client)\n            .Create();\n    }\n\n    [Theory, BitAutoData]\n    public void DefaultFeatureValue_WhenSelfHost(string key)\n    {\n        var sutProvider = GetSutProvider(new GlobalSettings { SelfHosted = true });\n\n        Assert.False(sutProvider.Sut.IsEnabled(key));\n    }\n\n    [Fact]\n    public void DefaultFeatureValue_NoSdkKey()\n    {\n        var sutProvider = GetSutProvider(new GlobalSettings());\n\n        Assert.False(sutProvider.Sut.IsEnabled(_fakeFeatureKey));\n    }\n\n    [Fact(Skip = \"For local development\")]\n    public void FeatureValue_Boolean()\n    {\n        var settings = new GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } };\n\n        var sutProvider = GetSutProvider(settings);\n\n        Assert.False(sutProvider.Sut.IsEnabled(_fakeFeatureKey));\n    }\n\n    [Fact(Skip = \"For local development\")]\n    public void FeatureValue_Int()\n    {\n        var settings = new GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } };\n\n        var sutProvider = GetSutProvider(settings);\n\n        Assert.Equal(0, sutProvider.Sut.GetIntVariation(_fakeFeatureKey));\n    }\n\n    [Fact(Skip = \"For local development\")]\n    public void FeatureValue_String()\n    {\n        var settings = new GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } };\n\n        var sutProvider = GetSutProvider(settings);\n\n        Assert.Null(sutProvider.Sut.GetStringVariation(_fakeFeatureKey));\n    }\n\n    [Fact(Skip = \"For local development\")]\n    public void GetAll()\n    {\n        var sutProvider = GetSutProvider(new GlobalSettings());\n\n        var results = sutProvider.Sut.GetAll();\n\n        Assert.NotNull(results);\n        Assert.NotEmpty(results);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/LocalAttachmentStorageServiceTests.cs",
    "content": "﻿using System.Text;\nusing AutoFixture;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.AutoFixture.CipherAttachmentMetaData;\nusing Bit.Core.Test.AutoFixture.CipherFixtures;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.DataProtection;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\n[SutProviderCustomize]\npublic class LocalAttachmentStorageServiceTests\n{\n\n    private void AssertFileCreation(string expectedPath, string expectedFileContents)\n    {\n        Assert.True(File.Exists(expectedPath));\n        Assert.Equal(expectedFileContents, File.ReadAllText(expectedPath));\n    }\n\n    [Theory]\n    [InlineCustomAutoData(new[] { typeof(UserCipher), typeof(MetaData) })]\n    [InlineCustomAutoData(new[] { typeof(UserCipher), typeof(MetaDataWithoutContainer) })]\n    [InlineCustomAutoData(new[] { typeof(UserCipher), typeof(MetaDataWithoutKey) })]\n    public async Task UploadNewAttachmentAsync_Success(string stream, Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            await sutProvider.Sut.UploadNewAttachmentAsync(new MemoryStream(Encoding.UTF8.GetBytes(stream)),\n                cipher, attachmentData);\n\n            AssertFileCreation($\"{tempDirectory}/{cipher.Id}/{attachmentData.AttachmentId}\", stream);\n        }\n    }\n\n    [Theory]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaData) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutContainer) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutKey) })]\n    public async Task UploadShareAttachmentAsync_Success(string stream, Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            await sutProvider.Sut.UploadShareAttachmentAsync(new MemoryStream(Encoding.UTF8.GetBytes(stream)),\n                cipher.Id, cipher.OrganizationId.Value, attachmentData);\n\n            AssertFileCreation($\"{tempDirectory}/temp/{cipher.Id}/{cipher.OrganizationId}/{attachmentData.AttachmentId}\", stream);\n        }\n    }\n\n    [Theory]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaData) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutContainer) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutKey) })]\n    public async Task StartShareAttachmentAsync_NoSource_NoWork(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            await sutProvider.Sut.StartShareAttachmentAsync(cipher.Id, cipher.OrganizationId.Value, attachmentData);\n\n            Assert.False(File.Exists($\"{tempDirectory}/{cipher.Id}/{attachmentData.AttachmentId}\"));\n            Assert.False(File.Exists($\"{tempDirectory}/{cipher.Id}/{attachmentData.AttachmentId}\"));\n        }\n    }\n\n    [Theory]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaData) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutContainer) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutKey) })]\n    public async Task StartShareAttachmentAsync_NoDest_NoWork(string source, Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            var sourcePath = $\"{tempDirectory}/temp/{cipher.Id}/{cipher.OrganizationId}/{attachmentData.AttachmentId}\";\n            var destPath = $\"{tempDirectory}/{cipher.Id}/{attachmentData.AttachmentId}\";\n            var rollBackPath = $\"{tempDirectory}/temp/{cipher.Id}/{attachmentData.AttachmentId}\";\n            Directory.CreateDirectory(Path.GetDirectoryName(sourcePath));\n            File.WriteAllText(sourcePath, source);\n\n            await sutProvider.Sut.StartShareAttachmentAsync(cipher.Id, cipher.OrganizationId.Value, attachmentData);\n\n            Assert.True(File.Exists(sourcePath));\n            Assert.Equal(source, File.ReadAllText(sourcePath));\n            Assert.False(File.Exists(destPath));\n            Assert.False(File.Exists(rollBackPath));\n        }\n    }\n\n\n    [Theory]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaData) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutContainer) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutKey) })]\n    public async Task StartShareAttachmentAsync_Success(string source, string destOriginal, Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            await StartShareAttachmentAsync(source, destOriginal, cipher, attachmentData, tempDirectory);\n        }\n    }\n\n    [Theory]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaData) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutContainer) })]\n    [InlineCustomAutoData(new[] { typeof(OrganizationCipher), typeof(MetaDataWithoutKey) })]\n    public async Task RollbackShareAttachmentAsync_Success(string source, string destOriginal, Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            var sourcePath = $\"{tempDirectory}/temp/{cipher.Id}/{cipher.OrganizationId}/{attachmentData.AttachmentId}\";\n            var destPath = $\"{tempDirectory}/{cipher.Id}/{attachmentData.AttachmentId}\";\n            var rollBackPath = $\"{tempDirectory}/temp/{cipher.Id}/{attachmentData.AttachmentId}\";\n\n            await StartShareAttachmentAsync(source, destOriginal, cipher, attachmentData, tempDirectory);\n            await sutProvider.Sut.RollbackShareAttachmentAsync(cipher.Id, cipher.OrganizationId.Value, attachmentData, \"Not Used Here\");\n\n            Assert.True(File.Exists(destPath));\n            Assert.Equal(destOriginal, File.ReadAllText(destPath));\n            Assert.False(File.Exists(sourcePath));\n            Assert.False(File.Exists(rollBackPath));\n        }\n    }\n\n    [Theory]\n    [InlineCustomAutoData(new[] { typeof(UserCipher), typeof(MetaData) })]\n    [InlineCustomAutoData(new[] { typeof(UserCipher), typeof(MetaDataWithoutContainer) })]\n    [InlineCustomAutoData(new[] { typeof(UserCipher), typeof(MetaDataWithoutKey) })]\n    public async Task DeleteAttachmentAsync_Success(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            var expectedPath = $\"{tempDirectory}/{cipher.Id}/{attachmentData.AttachmentId}\";\n            Directory.CreateDirectory(Path.GetDirectoryName(expectedPath));\n            File.Create(expectedPath).Close();\n\n            await sutProvider.Sut.DeleteAttachmentAsync(cipher.Id, attachmentData);\n\n            Assert.False(File.Exists(expectedPath));\n        }\n    }\n\n    [Theory, BitAutoData]\n    [UserCipherCustomize]\n    public async Task UserCipher_CleanupAsync_Success(Cipher cipher) => await CleanupAsync_Success(cipher);\n    [Theory, BitAutoData]\n    [OrganizationCipherCustomize]\n    public async Task OrganizationCipher_CleanupAsync_Success(Cipher cipher) => await CleanupAsync_Success(cipher);\n\n    private async Task CleanupAsync_Success(Cipher cipher)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            var tempPath = $\"{tempDirectory}/temp/{cipher.Id}\";\n            var permPath = $\"{tempDirectory}/{cipher.Id}\";\n            Directory.CreateDirectory(tempPath);\n            Directory.CreateDirectory(permPath);\n\n            await sutProvider.Sut.CleanupAsync(cipher.Id);\n\n            Assert.False(Directory.Exists(tempPath));\n            Assert.True(Directory.Exists(permPath));\n        }\n    }\n\n    [Theory, BitAutoData]\n    [UserCipherCustomize]\n    public async Task UserCipher_DeleteAttachmentsForCipherAsync_Success(Cipher cipher) => await DeleteAttachmentsForCipherAsync_Success(cipher);\n    [Theory, BitAutoData]\n    [OrganizationCipherCustomize]\n    public async Task OrganizationCipher_DeleteAttachmentsForCipherAsync_Success(Cipher cipher) => await DeleteAttachmentsForCipherAsync_Success(cipher);\n\n    private async Task DeleteAttachmentsForCipherAsync_Success(Cipher cipher)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            var tempPath = $\"{tempDirectory}/temp/{cipher.Id}\";\n            var permPath = $\"{tempDirectory}/{cipher.Id}\";\n            Directory.CreateDirectory(tempPath);\n            Directory.CreateDirectory(permPath);\n\n            await sutProvider.Sut.DeleteAttachmentsForCipherAsync(cipher.Id);\n\n            Assert.True(Directory.Exists(tempPath));\n            Assert.False(Directory.Exists(permPath));\n        }\n    }\n\n    private async Task StartShareAttachmentAsync(string source, string destOriginal, Cipher cipher,\n        CipherAttachment.MetaData attachmentData, TempDirectory tempDirectory)\n    {\n        var sutProvider = GetSutProvider(tempDirectory);\n\n        var sourcePath = $\"{tempDirectory}/temp/{cipher.Id}/{cipher.OrganizationId}/{attachmentData.AttachmentId}\";\n        var destPath = $\"{tempDirectory}/{cipher.Id}/{attachmentData.AttachmentId}\";\n        var rollBackPath = $\"{tempDirectory}/temp/{cipher.Id}/{attachmentData.AttachmentId}\";\n        Directory.CreateDirectory(Path.GetDirectoryName(sourcePath));\n        Directory.CreateDirectory(Path.GetDirectoryName(destPath));\n        File.WriteAllText(sourcePath, source);\n        File.WriteAllText(destPath, destOriginal);\n\n        await sutProvider.Sut.StartShareAttachmentAsync(cipher.Id, cipher.OrganizationId.Value, attachmentData);\n\n        Assert.False(File.Exists(sourcePath));\n        Assert.True(File.Exists(destPath));\n        Assert.Equal(source, File.ReadAllText(destPath));\n        Assert.True(File.Exists(rollBackPath));\n        Assert.Equal(destOriginal, File.ReadAllText(rollBackPath));\n    }\n\n    private SutProvider<LocalAttachmentStorageService> GetSutProvider(TempDirectory tempDirectory)\n    {\n        var fixture = new Fixture().WithAutoNSubstitutions();\n        fixture.Freeze<IGlobalSettings>().Attachment.BaseDirectory.Returns(tempDirectory.Directory);\n        fixture.Freeze<IGlobalSettings>().Attachment.BaseUrl.Returns(Guid.NewGuid().ToString());\n        fixture.Freeze<IGlobalSettings>().BaseServiceUri.Api.Returns(\"https://api.example.com\");\n        fixture.Register<IDataProtectionProvider>(() => new EphemeralDataProtectionProvider());\n\n        return new SutProvider<LocalAttachmentStorageService>(fixture).Create();\n    }\n\n    [Theory]\n    [InlineCustomAutoData(new[] { typeof(UserCipher), typeof(MetaData) })]\n    public async Task GetAttachmentDownloadUrlAsync_ReturnsSignedUrl(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            var url = await sutProvider.Sut.GetAttachmentDownloadUrlAsync(cipher, attachmentData);\n\n            Assert.Contains(\"ciphers/attachment/download\", url);\n            Assert.Contains(\"token=\", url);\n            Assert.StartsWith(\"https://api.example.com\", url);\n        }\n    }\n\n    [Theory]\n    [InlineCustomAutoData(new[] { typeof(UserCipher), typeof(MetaData) })]\n    public async Task GetAttachmentDownloadUrlAsync_TokenCanBeParsedBack(Cipher cipher, CipherAttachment.MetaData attachmentData)\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            var url = await sutProvider.Sut.GetAttachmentDownloadUrlAsync(cipher, attachmentData);\n\n            // Extract token from URL\n            var uri = new Uri(url);\n            var query = System.Web.HttpUtility.ParseQueryString(uri.Query);\n            var token = query[\"token\"];\n\n            var (parsedCipherId, parsedAttachmentId) = sutProvider.Sut.ParseAttachmentDownloadToken(token);\n\n            Assert.Equal(cipher.Id, parsedCipherId);\n            Assert.Equal(attachmentData.AttachmentId, parsedAttachmentId);\n        }\n    }\n\n    [Fact]\n    public void ParseAttachmentDownloadToken_InvalidToken_ThrowsNotFoundException()\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            Assert.Throws<NotFoundException>(\n                () => sutProvider.Sut.ParseAttachmentDownloadToken(\"invalid-token\"));\n        }\n    }\n\n    [Fact]\n    public void ParseAttachmentDownloadToken_InvalidFormat_ThrowsNotFoundException()\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            // Create a valid token but with invalid payload format (no pipe separator)\n            var provider = new EphemeralDataProtectionProvider();\n            var protector = provider\n                .CreateProtector(LocalAttachmentStorageService.AttachmentDownloadProtectorPurpose)\n                .ToTimeLimitedDataProtector();\n            var token = protector.Protect(\"invalid-data-without-pipe\", TimeSpan.FromMinutes(1));\n\n            Assert.Throws<NotFoundException>(\n                () => sutProvider.Sut.ParseAttachmentDownloadToken(token));\n        }\n    }\n\n    [Fact]\n    public void ParseAttachmentDownloadToken_InvalidGuid_ThrowsNotFoundException()\n    {\n        using (var tempDirectory = new TempDirectory())\n        {\n            var sutProvider = GetSutProvider(tempDirectory);\n\n            Assert.Throws<NotFoundException>(\n                () => sutProvider.Sut.ParseAttachmentDownloadToken(\"not-a-real-token\"));\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/MailKitSmtpMailDeliveryServiceTests.cs",
    "content": "﻿using Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Settings;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\npublic class MailKitSmtpMailDeliveryServiceTests\n{\n    private readonly MailKitSmtpMailDeliveryService _sut;\n\n    private readonly GlobalSettings _globalSettings;\n    private readonly ILogger<MailKitSmtpMailDeliveryService> _logger;\n\n    public MailKitSmtpMailDeliveryServiceTests()\n    {\n        _globalSettings = new GlobalSettings();\n        _logger = Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>();\n\n        _globalSettings.Mail.Smtp.Host = \"unittests.example.com\";\n        _globalSettings.Mail.ReplyToEmail = \"noreply@unittests.example.com\";\n\n        _sut = new MailKitSmtpMailDeliveryService(\n            _globalSettings,\n            _logger\n        );\n    }\n\n    // Remove this test when we add actual tests. It only proves that\n    // we've properly constructed the system under test.\n    [Fact]\n    public void ServiceExists()\n    {\n        Assert.NotNull(_sut);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/PlayIdServiceTests.cs",
    "content": "﻿using Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\n[SutProviderCustomize]\npublic class PlayIdServiceTests\n{\n    [Theory]\n    [BitAutoData]\n    public void InPlay_WhenPlayIdSetAndDevelopment_ReturnsTrue(\n        string playId,\n        SutProvider<PlayIdService> sutProvider)\n    {\n        sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Development);\n        sutProvider.Sut.PlayId = playId;\n\n        var result = sutProvider.Sut.InPlay(out var resultPlayId);\n\n        Assert.True(result);\n        Assert.Equal(playId, resultPlayId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void InPlay_WhenPlayIdSetButNotDevelopment_ReturnsFalse(\n        string playId,\n        SutProvider<PlayIdService> sutProvider)\n    {\n        sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Production);\n        sutProvider.Sut.PlayId = playId;\n\n        var result = sutProvider.Sut.InPlay(out var resultPlayId);\n\n        Assert.False(result);\n        Assert.Equal(playId, resultPlayId);\n    }\n\n    [Theory]\n    [BitAutoData((string?)null)]\n    [BitAutoData(\"\")]\n    public void InPlay_WhenPlayIdNullOrEmptyAndDevelopment_ReturnsFalse(\n        string? playId,\n        SutProvider<PlayIdService> sutProvider)\n    {\n        sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Development);\n        sutProvider.Sut.PlayId = playId;\n\n        var result = sutProvider.Sut.InPlay(out var resultPlayId);\n\n        Assert.False(result);\n        Assert.Empty(resultPlayId);\n    }\n\n    [Theory]\n    [BitAutoData]\n    public void PlayId_CanGetAndSet(string playId)\n    {\n        var hostEnvironment = Substitute.For<IHostEnvironment>();\n        var sut = new PlayIdService(hostEnvironment);\n\n        sut.PlayId = playId;\n\n        Assert.Equal(playId, sut.PlayId);\n    }\n}\n\n[SutProviderCustomize]\npublic class NeverPlayIdServicesTests\n{\n    [Fact]\n    public void InPlay_ReturnsFalse()\n    {\n        var sut = new NeverPlayIdServices();\n\n        var result = sut.InPlay(out var playId);\n\n        Assert.False(result);\n        Assert.Empty(playId);\n    }\n\n    [Theory]\n    [InlineData(\"test-play-id\")]\n    [InlineData(null)]\n    public void PlayId_SetterDoesNothing_GetterReturnsNull(string? value)\n    {\n        var sut = new NeverPlayIdServices();\n\n        sut.PlayId = value;\n\n        Assert.Null(sut.PlayId);\n    }\n}\n\n[SutProviderCustomize]\npublic class PlayIdSingletonServiceTests\n{\n    public static IEnumerable<object[]> SutProvider()\n    {\n        var sutProvider = new SutProvider<PlayIdSingletonService>();\n        var httpContext = sutProvider.CreateDependency<HttpContext>();\n        var serviceProvider = sutProvider.CreateDependency<IServiceProvider>();\n        var hostEnvironment = sutProvider.CreateDependency<IHostEnvironment>();\n        var playIdService = new PlayIdService(hostEnvironment);\n        sutProvider.SetDependency(playIdService);\n        httpContext.RequestServices.Returns(serviceProvider);\n        serviceProvider.GetService<PlayIdService>().Returns(playIdService);\n        serviceProvider.GetRequiredService<PlayIdService>().Returns(playIdService);\n        sutProvider.CreateDependency<IHttpContextAccessor>().HttpContext.Returns(httpContext);\n        sutProvider.Create();\n        return [[sutProvider]];\n    }\n\n    private void PrepHttpContext(\n        SutProvider<PlayIdSingletonService> sutProvider)\n    {\n        var httpContext = sutProvider.CreateDependency<HttpContext>();\n        var serviceProvider = sutProvider.CreateDependency<IServiceProvider>();\n        var PlayIdService = sutProvider.CreateDependency<PlayIdService>();\n        httpContext.RequestServices.Returns(serviceProvider);\n        serviceProvider.GetRequiredService<PlayIdService>().Returns(PlayIdService);\n        sutProvider.GetDependency<IHttpContextAccessor>().HttpContext.Returns(httpContext);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(SutProvider))]\n    public void InPlay_WhenNoHttpContext_ReturnsFalse(\n        SutProvider<PlayIdSingletonService> sutProvider)\n    {\n        sutProvider.GetDependency<IHttpContextAccessor>().HttpContext.Returns((HttpContext?)null);\n        sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Development);\n\n        var result = sutProvider.Sut.InPlay(out var playId);\n\n        Assert.False(result);\n        Assert.Empty(playId);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(SutProvider))]\n    public void InPlay_WhenNotDevelopment_ReturnsFalse(\n        SutProvider<PlayIdSingletonService> sutProvider,\n        string playIdValue)\n    {\n        var scopedPlayIdService = sutProvider.GetDependency<PlayIdService>();\n        scopedPlayIdService.PlayId = playIdValue;\n        sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Production);\n\n        var result = sutProvider.Sut.InPlay(out var playId);\n\n        Assert.False(result);\n        Assert.Empty(playId);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(SutProvider))]\n    public void InPlay_WhenDevelopmentAndHttpContextWithPlayId_ReturnsTrue(\n        SutProvider<PlayIdSingletonService> sutProvider,\n        string playIdValue)\n    {\n        sutProvider.GetDependency<PlayIdService>().PlayId = playIdValue;\n        sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Development);\n\n        var result = sutProvider.Sut.InPlay(out var playId);\n\n        Assert.True(result);\n        Assert.Equal(playIdValue, playId);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(SutProvider))]\n    public void PlayId_SetterSetsOnScopedService(\n        SutProvider<PlayIdSingletonService> sutProvider,\n        string playIdValue)\n    {\n        var scopedPlayIdService = sutProvider.GetDependency<PlayIdService>();\n\n        sutProvider.Sut.PlayId = playIdValue;\n\n        Assert.Equal(playIdValue, scopedPlayIdService.PlayId);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(SutProvider))]\n    public void PlayId_WhenNoHttpContext_GetterReturnsNull(\n        SutProvider<PlayIdSingletonService> sutProvider)\n    {\n        sutProvider.GetDependency<IHttpContextAccessor>().HttpContext.Returns((HttpContext?)null);\n\n        var result = sutProvider.Sut.PlayId;\n\n        Assert.Null(result);\n    }\n\n    [Theory]\n    [BitMemberAutoData(nameof(SutProvider))]\n    public void PlayId_WhenNoHttpContext_SetterDoesNotThrow(\n        SutProvider<PlayIdSingletonService> sutProvider,\n        string playIdValue)\n    {\n        sutProvider.GetDependency<IHttpContextAccessor>().HttpContext.Returns((HttpContext?)null);\n\n        sutProvider.Sut.PlayId = playIdValue;\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/PlayItemServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.Entities;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\n[SutProviderCustomize]\npublic class PlayItemServiceTests\n{\n    [Theory]\n    [BitAutoData]\n    public async Task Record_User_WhenInPlay_RecordsPlayItem(\n        string playId,\n        User user,\n        SutProvider<PlayItemService> sutProvider)\n    {\n        sutProvider.GetDependency<IPlayIdService>()\n            .InPlay(out Arg.Any<string>())\n            .Returns(x =>\n            {\n                x[0] = playId;\n                return true;\n            });\n\n        await sutProvider.Sut.Record(user);\n\n        await sutProvider.GetDependency<IPlayItemRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Is<PlayItem>(pd =>\n                pd.PlayId == playId &&\n                pd.UserId == user.Id &&\n                pd.OrganizationId == null));\n\n        sutProvider.GetDependency<ILogger<PlayItemService>>()\n            .Received(1)\n            .Log(\n                LogLevel.Information,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString().Contains(user.Id.ToString()) && o.ToString().Contains(playId)),\n                null,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Record_User_WhenNotInPlay_DoesNotRecordPlayItem(\n        User user,\n        SutProvider<PlayItemService> sutProvider)\n    {\n        sutProvider.GetDependency<IPlayIdService>()\n            .InPlay(out Arg.Any<string>())\n            .Returns(x =>\n            {\n                x[0] = null;\n                return false;\n            });\n\n        await sutProvider.Sut.Record(user);\n\n        await sutProvider.GetDependency<IPlayItemRepository>()\n            .DidNotReceive()\n            .CreateAsync(Arg.Any<PlayItem>());\n\n        sutProvider.GetDependency<ILogger<PlayItemService>>()\n            .DidNotReceive()\n            .Log(\n                LogLevel.Information,\n                Arg.Any<EventId>(),\n                Arg.Any<object>(),\n                Arg.Any<Exception>(),\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Record_Organization_WhenInPlay_RecordsPlayItem(\n        string playId,\n        Organization organization,\n        SutProvider<PlayItemService> sutProvider)\n    {\n        sutProvider.GetDependency<IPlayIdService>()\n            .InPlay(out Arg.Any<string>())\n            .Returns(x =>\n            {\n                x[0] = playId;\n                return true;\n            });\n\n        await sutProvider.Sut.Record(organization);\n\n        await sutProvider.GetDependency<IPlayItemRepository>()\n            .Received(1)\n            .CreateAsync(Arg.Is<PlayItem>(pd =>\n                pd.PlayId == playId &&\n                pd.OrganizationId == organization.Id &&\n                pd.UserId == null));\n\n        sutProvider.GetDependency<ILogger<PlayItemService>>()\n            .Received(1)\n            .Log(\n                LogLevel.Information,\n                Arg.Any<EventId>(),\n                Arg.Is<object>(o => o.ToString().Contains(organization.Id.ToString()) && o.ToString().Contains(playId)),\n                null,\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n\n    [Theory]\n    [BitAutoData]\n    public async Task Record_Organization_WhenNotInPlay_DoesNotRecordPlayItem(\n        Organization organization,\n        SutProvider<PlayItemService> sutProvider)\n    {\n        sutProvider.GetDependency<IPlayIdService>()\n            .InPlay(out Arg.Any<string>())\n            .Returns(x =>\n            {\n                x[0] = null;\n                return false;\n            });\n\n        await sutProvider.Sut.Record(organization);\n\n        await sutProvider.GetDependency<IPlayItemRepository>()\n            .DidNotReceive()\n            .CreateAsync(Arg.Any<PlayItem>());\n\n        sutProvider.GetDependency<ILogger<PlayItemService>>()\n            .DidNotReceive()\n            .Log(\n                LogLevel.Information,\n                Arg.Any<EventId>(),\n                Arg.Any<object>(),\n                Arg.Any<Exception>(),\n                Arg.Any<Func<object, Exception?, string>>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/SendGridMailDeliveryServiceTests.cs",
    "content": "﻿using Bit.Core.Models.Mail;\nusing Bit.Core.Platform.Mail.Delivery;\nusing Bit.Core.Settings;\nusing Microsoft.AspNetCore.Hosting;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing SendGrid;\nusing SendGrid.Helpers.Mail;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\npublic class SendGridMailDeliveryServiceTests : IDisposable\n{\n    private readonly SendGridMailDeliveryService _sut;\n\n    private readonly GlobalSettings _globalSettings;\n    private readonly IWebHostEnvironment _hostingEnvironment;\n    private readonly ILogger<SendGridMailDeliveryService> _logger;\n    private readonly ISendGridClient _sendGridClient;\n\n    public SendGridMailDeliveryServiceTests()\n    {\n        _globalSettings = new GlobalSettings\n        {\n            Mail =\n            {\n                SendGridApiKey = \"SendGridApiKey\",\n                SendGridApiHost = \"https://api.sendgrid.com\"\n            }\n        };\n\n        _hostingEnvironment = Substitute.For<IWebHostEnvironment>();\n        _logger = Substitute.For<ILogger<SendGridMailDeliveryService>>();\n        _sendGridClient = Substitute.For<ISendGridClient>();\n\n        _sut = new SendGridMailDeliveryService(\n            _sendGridClient,\n            _globalSettings,\n            _hostingEnvironment,\n            _logger\n        );\n    }\n\n    public void Dispose()\n    {\n        _sut?.Dispose();\n    }\n\n    [Fact]\n    public async Task SendEmailAsync_CallsSendEmailAsync_WhenMessageIsValid()\n    {\n        var mailMessage = new MailMessage\n        {\n            ToEmails = new List<string> { \"ToEmails\" },\n            BccEmails = new List<string> { \"BccEmails\" },\n            Subject = \"Subject\",\n            HtmlContent = \"HtmlContent\",\n            TextContent = \"TextContent\",\n            Category = \"Category\"\n        };\n\n        _sendGridClient.SendEmailAsync(Arg.Any<SendGridMessage>()).Returns(\n            new Response(System.Net.HttpStatusCode.OK, null, null));\n        await _sut.SendEmailAsync(mailMessage);\n\n        await _sendGridClient.Received(1).SendEmailAsync(\n            Arg.Do<SendGridMessage>(msg =>\n            {\n                msg.Received(1).AddTos(new List<EmailAddress> { new EmailAddress(mailMessage.ToEmails.First()) });\n                msg.Received(1).AddBccs(new List<EmailAddress> { new EmailAddress(mailMessage.ToEmails.First()) });\n\n                Assert.Equal(mailMessage.Subject, msg.Subject);\n                Assert.Equal(mailMessage.HtmlContent, msg.HtmlContent);\n                Assert.Equal(mailMessage.TextContent, msg.PlainTextContent);\n\n                Assert.Contains(\"type:Category\", msg.Categories);\n                Assert.Contains(msg.Categories, x => x.StartsWith(\"env:\"));\n                Assert.Contains(msg.Categories, x => x.StartsWith(\"sender:\"));\n\n                msg.Received(1).SetClickTracking(false, false);\n                msg.Received(1).SetOpenTracking(false);\n            }));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Services/UserServiceTests.cs",
    "content": "﻿using System.Security.Claims;\nusing System.Text.Json;\nusing Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;\nusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Auth.Enums;\nusing Bit.Core.Auth.Models;\nusing Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;\nusing Bit.Core.Billing.Models.Business;\nusing Bit.Core.Billing.Premium.Queries;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Services;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Models.Data.Organizations;\nusing Bit.Core.Models.Data.Organizations.OrganizationUsers;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Settings;\nusing Bit.Core.Test.AdminConsole.AutoFixture;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.AspNetCore.Identity;\nusing Microsoft.Extensions.Caching.Distributed;\nusing Microsoft.Extensions.Options;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Services;\n\n[SutProviderCustomize]\npublic class UserServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task SaveUserAsync_SetsNameToNull_WhenNameIsEmpty(SutProvider<UserService> sutProvider, User user)\n    {\n        user.Name = string.Empty;\n        await sutProvider.Sut.SaveUserAsync(user);\n        Assert.Null(user.Name);\n    }\n\n    [Theory, BitAutoData]\n    public async Task UpdateLicenseAsync_Success(SutProvider<UserService> sutProvider,\n        User user, UserLicense userLicense)\n    {\n        using var tempDir = new TempDirectory();\n\n        var now = DateTime.UtcNow;\n        userLicense.Issued = now.AddDays(-10);\n        userLicense.Expires = now.AddDays(10);\n        userLicense.Version = 1;\n        userLicense.Premium = true;\n\n        user.EmailVerified = true;\n        user.Email = userLicense.Email;\n\n        sutProvider.GetDependency<IGlobalSettings>().SelfHosted = true;\n        sutProvider.GetDependency<IGlobalSettings>().LicenseDirectory = tempDir.Directory;\n        sutProvider.GetDependency<ILicensingService>()\n            .VerifyLicense(userLicense)\n            .Returns(true);\n        sutProvider.GetDependency<ILicensingService>()\n            .GetClaimsPrincipalFromLicense(userLicense)\n            .Returns((ClaimsPrincipal)null);\n\n        await sutProvider.Sut.UpdateLicenseAsync(user, userLicense);\n\n        var filePath = Path.Combine(tempDir.Directory, \"user\", $\"{user.Id}.json\");\n        Assert.True(File.Exists(filePath));\n        var document = JsonDocument.Parse(File.OpenRead(filePath));\n        var root = document.RootElement;\n        Assert.Equal(JsonValueKind.Object, root.ValueKind);\n        // Sort of a lazy way to test that it is indented but not sure of a better way\n        Assert.Contains('\\n', root.GetRawText());\n        AssertHelper.AssertJsonProperty(root, \"LicenseKey\", JsonValueKind.String);\n        AssertHelper.AssertJsonProperty(root, \"Id\", JsonValueKind.String);\n        AssertHelper.AssertJsonProperty(root, \"Premium\", JsonValueKind.True);\n        var versionProp = AssertHelper.AssertJsonProperty(root, \"Version\", JsonValueKind.Number);\n        Assert.Equal(1, versionProp.GetInt32());\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider<UserService> sutProvider, User user)\n    {\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id).Returns(new List<OrganizationUser>());\n        Assert.False(await sutProvider.Sut.HasPremiumFromOrganization(user));\n\n    }\n\n    [Theory]\n    [BitAutoData(false, true)]\n    [BitAutoData(true, false)]\n    public async Task HasPremiumFromOrganization_Returns_False_If_Org_Not_Eligible(bool orgEnabled, bool orgUsersGetPremium, SutProvider<UserService> sutProvider, User user, OrganizationUser orgUser, Organization organization)\n    {\n        orgUser.OrganizationId = organization.Id;\n        organization.Enabled = orgEnabled;\n        organization.UsersGetPremium = orgUsersGetPremium;\n        var orgAbilities = new Dictionary<Guid, OrganizationAbility>() { { organization.Id, new OrganizationAbility(organization) } };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id).Returns(new List<OrganizationUser>() { orgUser });\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(orgAbilities);\n\n        Assert.False(await sutProvider.Sut.HasPremiumFromOrganization(user));\n    }\n\n    [Theory, BitAutoData]\n    public async Task HasPremiumFromOrganization_Returns_True_If_Org_Eligible(SutProvider<UserService> sutProvider, User user, OrganizationUser orgUser, Organization organization)\n    {\n        orgUser.OrganizationId = organization.Id;\n        organization.Enabled = true;\n        var orgAbilities = new Dictionary<Guid, OrganizationAbility>() { { organization.Id, new OrganizationAbility(organization) } };\n\n        sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id).Returns(new List<OrganizationUser>() { orgUser });\n        sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(orgAbilities);\n        sutProvider.GetDependency<IHasPremiumAccessQuery>().HasPremiumFromOrganizationAsync(user.Id).Returns(true);\n\n        Assert.True(await sutProvider.Sut.HasPremiumFromOrganization(user));\n    }\n\n    [Flags]\n    public enum ShouldCheck\n    {\n        Password = 0x1,\n        OTP = 0x2,\n    }\n\n    [Theory]\n    // A user who has a password, and the password is valid should only check for that password\n    [BitAutoData(true, \"test_password\", true, ShouldCheck.Password)]\n    // A user who does not have a password, should only check if the OTP is valid\n    [BitAutoData(false, \"otp_token\", true, ShouldCheck.OTP)]\n    // A user who has a password but supplied a OTP, it will check password first and then try OTP\n    [BitAutoData(true, \"otp_token\", true, ShouldCheck.Password | ShouldCheck.OTP)]\n    // A user who does not have a password and supplied an invalid OTP token, should only check OTP and return invalid\n    [BitAutoData(false, \"bad_otp_token\", false, ShouldCheck.OTP)]\n    // A user who does have a password but they supply a bad one, we will check both but it will still be invalid\n    [BitAutoData(true, \"bad_test_password\", false, ShouldCheck.Password | ShouldCheck.OTP)]\n    public async Task VerifySecretAsync_Works(\n        bool shouldHavePassword, string secret, bool expectedIsVerified, ShouldCheck shouldCheck, // inline theory data\n        User user) // AutoFixture injected data\n    {\n        // Arrange\n        SetupUserAndDevice(user, shouldHavePassword);\n\n        var sutProvider = new SutProvider<UserService>()\n            .CreateWithUserServiceCustomizations(user);\n\n        // Setup the fake password verification\n        sutProvider.GetDependency<IUserPasswordStore<User>>()\n            .GetPasswordHashAsync(user, Arg.Any<CancellationToken>())\n            .Returns(Task.FromResult(\"hashed_test_password\"));\n\n        sutProvider.GetDependency<IPasswordHasher<User>>()\n            .VerifyHashedPassword(user, \"hashed_test_password\", \"test_password\")\n            .Returns(PasswordVerificationResult.Success);\n\n        var actualIsVerified = await sutProvider.Sut.VerifySecretAsync(user, secret);\n\n        Assert.Equal(expectedIsVerified, actualIsVerified);\n\n        await sutProvider.GetDependency<IUserTwoFactorTokenProvider<User>>()\n            .Received(shouldCheck.HasFlag(ShouldCheck.OTP) ? 1 : 0)\n            .ValidateAsync(Arg.Any<string>(), secret, Arg.Any<UserManager<User>>(), user);\n\n        sutProvider.GetDependency<IPasswordHasher<User>>()\n            .Received(shouldCheck.HasFlag(ShouldCheck.Password) ? 1 : 0)\n            .VerifyHashedPassword(user, \"hashed_test_password\", secret);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsClaimedByAnyOrganizationAsync_WithManagingEnabledOrganization_ReturnsTrue(\n        SutProvider<UserService> sutProvider, Guid userId, Organization organization)\n    {\n        organization.Enabled = true;\n        organization.UseOrganizationDomains = true;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByVerifiedUserEmailDomainAsync(userId)\n            .Returns(new[] { organization });\n\n        var result = await sutProvider.Sut.IsClaimedByAnyOrganizationAsync(userId);\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsClaimedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse(\n        SutProvider<UserService> sutProvider, Guid userId, Organization organization)\n    {\n        organization.Enabled = false;\n        organization.UseOrganizationDomains = true;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByVerifiedUserEmailDomainAsync(userId)\n            .Returns(new[] { organization });\n\n        var result = await sutProvider.Sut.IsClaimedByAnyOrganizationAsync(userId);\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task IsClaimedByAnyOrganizationAsync_WithOrganizationUseOrganizationDomaisFalse_ReturnsFalse(\n        SutProvider<UserService> sutProvider, Guid userId, Organization organization)\n    {\n        organization.Enabled = true;\n        organization.UseOrganizationDomains = false;\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByVerifiedUserEmailDomainAsync(userId)\n            .Returns(new[] { organization });\n\n        var result = await sutProvider.Sut.IsClaimedByAnyOrganizationAsync(userId);\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail(\n        SutProvider<UserService> sutProvider, User user,\n        Organization organization1, Guid organizationUserId1,\n        Organization organization2, Guid organizationUserId2)\n    {\n        // Arrange\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new() { Enabled = true }\n        });\n        organization1.Enabled = organization2.Enabled = true;\n        organization1.UseSso = organization2.UseSso = true;\n\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)\n            .Returns(\n            [\n                new OrganizationUserPolicyDetails\n                {\n                    OrganizationId = organization1.Id,\n                    OrganizationUserId = organizationUserId1,\n                    PolicyType = PolicyType.TwoFactorAuthentication,\n                    PolicyEnabled = true\n                },\n                new OrganizationUserPolicyDetails\n                {\n                    OrganizationId = organization2.Id,\n                    OrganizationUserId = organizationUserId2,\n                    PolicyType = PolicyType.TwoFactorAuthentication,\n                    PolicyEnabled = true\n                }\n            ]);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization1.Id)\n            .Returns(organization1);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization2.Id)\n            .Returns(organization2);\n        var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);\n\n        // Act\n        await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);\n\n        // Assert\n        await sutProvider.GetDependency<IUserRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_Disabled2fa);\n\n        // Revoke the user from the first organization\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(\n                Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization1.Id &&\n                    r.OrganizationUsers.First().Id == organizationUserId1 &&\n                    r.OrganizationUsers.First().OrganizationId == organization1.Id));\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization1.DisplayName(), user.Email);\n\n        // Remove the user from the second organization\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(\n                Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization2.Id &&\n                    r.OrganizationUsers.First().Id == organizationUserId2 &&\n                    r.OrganizationUsers.First().OrganizationId == organization2.Id));\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail(\n        SutProvider<UserService> sutProvider, User user,\n        Organization organization1, Guid organizationUserId1,\n        Organization organization2, Guid organizationUserId2)\n    {\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new() { Enabled = true }\n        });\n        organization1.Enabled = organization2.Enabled = true;\n        organization1.UseSso = organization2.UseSso = true;\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = organization1.Id,\n                    OrganizationUserId = organizationUserId1,\n                    OrganizationUserStatus = OrganizationUserStatusType.Accepted,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                },\n                new PolicyDetails\n                {\n                    OrganizationId = organization2.Id,\n                    OrganizationUserId = organizationUserId2,\n                    OrganizationUserStatus = OrganizationUserStatusType.Confirmed,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]));\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetManyByIdsAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organization1.Id) && ids.Contains(organization2.Id)))\n            .Returns(new[] { organization1, organization2 });\n        var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);\n\n        await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);\n\n        await sutProvider.GetDependency<IUserRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_Disabled2fa);\n\n        // Revoke the user from the first organization\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(\n                Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization1.Id &&\n                    r.OrganizationUsers.First().Id == organizationUserId1 &&\n                    r.OrganizationUsers.First().OrganizationId == organization1.Id));\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization1.DisplayName(), user.Email);\n\n        // Remove the user from the second organization\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .Received(1)\n            .RevokeNonCompliantOrganizationUsersAsync(\n                Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization2.Id &&\n                    r.OrganizationUsers.First().Id == organizationUserId2 &&\n                    r.OrganizationUsers.First().OrganizationId == organization2.Id));\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DisableTwoFactorProviderAsync_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(\n        SutProvider<UserService> sutProvider, User user, Organization organization)\n    {\n        // Arrange\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new() { Enabled = true },\n            [TwoFactorProviderType.Remember] = new() { Enabled = true }\n        });\n        sutProvider.GetDependency<IPolicyService>()\n            .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)\n            .Returns(\n            [\n                new OrganizationUserPolicyDetails\n                {\n                    OrganizationId = organization.Id,\n                    PolicyType = PolicyType.TwoFactorAuthentication,\n                    PolicyEnabled = true\n                }\n            ]);\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(user)\n            .Returns(true);\n        var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Remember] = new() { Enabled = true }\n        }, JsonHelpers.LegacyEnumKeyResolver);\n\n        // Act\n        await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);\n\n        // Assert\n        await sutProvider.GetDependency<IUserRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .RevokeNonCompliantOrganizationUsersAsync(default);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);\n    }\n\n    [Theory, BitAutoData]\n    public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(\n        SutProvider<UserService> sutProvider, User user, Organization organization)\n    {\n        user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Email] = new() { Enabled = true },\n            [TwoFactorProviderType.Remember] = new() { Enabled = true }\n        });\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)\n            .Returns(new RequireTwoFactorPolicyRequirement(\n            [\n                new PolicyDetails\n                {\n                    OrganizationId = organization.Id,\n                    OrganizationUserStatus = OrganizationUserStatusType.Accepted,\n                    PolicyType = PolicyType.TwoFactorAuthentication\n                }\n            ]));\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()\n            .TwoFactorIsEnabledAsync(user)\n            .Returns(true);\n        var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>\n        {\n            [TwoFactorProviderType.Remember] = new() { Enabled = true }\n        }, JsonHelpers.LegacyEnumKeyResolver);\n\n        await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);\n\n        await sutProvider.GetDependency<IUserRepository>()\n            .Received(1)\n            .ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));\n        await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()\n            .DidNotReceiveWithAnyArgs()\n            .RevokeNonCompliantOrganizationUsersAsync(default);\n        await sutProvider.GetDependency<IMailService>()\n            .DidNotReceiveWithAnyArgs()\n            .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);\n    }\n\n    [Theory]\n    [BitAutoData(\"\")]\n    [BitAutoData(\"null\")]\n    public async Task SendOTPAsync_UserEmailNull_ThrowsBadRequest(\n        string email,\n        SutProvider<UserService> sutProvider, User user)\n    {\n        user.Email = email == \"null\" ? null : \"\";\n        var expectedMessage = \"No user email.\";\n        try\n        {\n            await sutProvider.Sut.SendOTPAsync(user);\n        }\n        catch (BadRequestException ex)\n        {\n            Assert.Equal(ex.Message, expectedMessage);\n            await sutProvider.GetDependency<IMailService>()\n                .DidNotReceive()\n                .SendOTPEmailAsync(Arg.Any<string>(), Arg.Any<string>());\n        }\n    }\n\n    [Theory, BitAutoData]\n    public async Task ActiveNewDeviceVerificationException_UserNotInCache_ReturnsFalseAsync(\n        SutProvider<UserService> sutProvider)\n    {\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(Arg.Any<string>())\n            .Returns(null as byte[]);\n\n        var result = await sutProvider.Sut.ActiveNewDeviceVerificationException(Guid.NewGuid());\n\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ActiveNewDeviceVerificationException_UserInCache_ReturnsTrueAsync(\n        SutProvider<UserService> sutProvider)\n    {\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(Arg.Any<string>())\n            .Returns([1]);\n\n        var result = await sutProvider.Sut.ActiveNewDeviceVerificationException(Guid.NewGuid());\n\n        Assert.True(result);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ToggleNewDeviceVerificationException_UserInCache_RemovesUserFromCache(\n        SutProvider<UserService> sutProvider)\n    {\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(Arg.Any<string>())\n            .Returns([1]);\n\n        await sutProvider.Sut.ToggleNewDeviceVerificationException(Guid.NewGuid());\n\n        await sutProvider.GetDependency<IDistributedCache>()\n                .DidNotReceive()\n                .SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n        await sutProvider.GetDependency<IDistributedCache>()\n                .Received(1)\n                .RemoveAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ToggleNewDeviceVerificationException_UserNotInCache_AddsUserToCache(\n        SutProvider<UserService> sutProvider)\n    {\n        sutProvider.GetDependency<IDistributedCache>()\n            .GetAsync(Arg.Any<string>())\n            .Returns(null as byte[]);\n\n        await sutProvider.Sut.ToggleNewDeviceVerificationException(Guid.NewGuid());\n\n        await sutProvider.GetDependency<IDistributedCache>()\n                .Received(1)\n                .SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>());\n        await sutProvider.GetDependency<IDistributedCache>()\n                .DidNotReceive()\n                .RemoveAsync(Arg.Any<string>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task RecoverTwoFactorAsync_CorrectCode_ReturnsTrueAndProcessesPolicies(\n        User user, SutProvider<UserService> sutProvider)\n    {\n        // Arrange\n        var recoveryCode = \"1234\";\n        user.TwoFactorRecoveryCode = recoveryCode;\n\n        // Act\n        var response = await sutProvider.Sut.RecoverTwoFactorAsync(user, recoveryCode);\n\n        // Assert\n        Assert.True(response);\n        Assert.Null(user.TwoFactorProviders);\n        // Make sure a new code was generated for the user\n        Assert.NotEqual(recoveryCode, user.TwoFactorRecoveryCode);\n        await sutProvider.GetDependency<IMailService>()\n            .Received(1)\n            .SendRecoverTwoFactorEmail(Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());\n        await sutProvider.GetDependency<IEventService>()\n            .Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_Recovered2fa);\n    }\n\n    [Theory, BitAutoData]\n    public async Task RecoverTwoFactorAsync_IncorrectCode_ReturnsFalse(\n        User user, SutProvider<UserService> sutProvider)\n    {\n        // Arrange\n        var recoveryCode = \"1234\";\n        user.TwoFactorRecoveryCode = \"4567\";\n\n        // Act\n        var response = await sutProvider.Sut.RecoverTwoFactorAsync(user, recoveryCode);\n\n        // Assert\n        Assert.False(response);\n        Assert.NotNull(user.TwoFactorProviders);\n    }\n\n    [Theory]\n    [BitAutoData(\"wrapped-user-key\")]\n    [BitAutoData(\"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=\")]\n    public async Task ConvertToKeyConnectorAsync_WrappedUserKeyProvided_SetsWrappedUserKey(\n        string wrappedUserKey,\n        SutProvider<UserService> sutProvider,\n        User user)\n    {\n        // Arrange\n        user.UsesKeyConnector = false;\n        user.MasterPassword = \"master-password\";\n        user.Key = \"old-key\";\n        sutProvider.GetDependency<ICurrentContext>().Organizations = [];\n\n        // Act\n        var result = await sutProvider.Sut.ConvertToKeyConnectorAsync(user, wrappedUserKey);\n\n        // Assert\n        Assert.True(result.Succeeded);\n        Assert.True(user.UsesKeyConnector);\n        Assert.Null(user.MasterPassword);\n        Assert.Equal(wrappedUserKey, user.Key);\n        Assert.Equal(user.RevisionDate, user.AccountRevisionDate);\n        await sutProvider.GetDependency<IUserRepository>().Received(1)\n            .ReplaceAsync(Arg.Is<User>(u =>\n                u == user &&\n                u.Key == wrappedUserKey &&\n                u.MasterPassword == null &&\n                u.UsesKeyConnector));\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ConvertToKeyConnectorAsync_WrappedUserKeyNull_DoesNotOverwriteExistingKey(\n        SutProvider<UserService> sutProvider,\n        User user)\n    {\n        // Arrange\n        const string existingUserKey = \"existing-user-key\";\n        user.UsesKeyConnector = false;\n        user.MasterPassword = \"master-password\";\n        user.Key = existingUserKey;\n        sutProvider.GetDependency<ICurrentContext>().Organizations = [];\n\n        // Act\n        var result = await sutProvider.Sut.ConvertToKeyConnectorAsync(user, null);\n\n        // Assert\n        Assert.True(result.Succeeded);\n        Assert.True(user.UsesKeyConnector);\n        Assert.Null(user.MasterPassword);\n        Assert.Equal(existingUserKey, user.Key);\n        Assert.Equal(user.RevisionDate, user.AccountRevisionDate);\n\n        await sutProvider.GetDependency<IUserRepository>().Received(1)\n            .ReplaceAsync(Arg.Is<User>(u =>\n                u == user &&\n                u.Key == existingUserKey &&\n                u.MasterPassword == null &&\n                u.UsesKeyConnector));\n\n        await sutProvider.GetDependency<IEventService>().Received(1)\n            .LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);\n    }\n\n    private static void SetupUserAndDevice(User user,\n        bool shouldHavePassword)\n    {\n        if (shouldHavePassword)\n        {\n            user.MasterPassword = \"test_password\";\n        }\n        else\n        {\n            user.MasterPassword = null;\n        }\n    }\n\n    [Theory]\n    [BitAutoData(\"\")]\n    [BitAutoData(\" \")]\n    [BitAutoData(\"\\t\")]\n    public async Task AdminResetPasswordAsync_EmptyOrWhitespaceResetPasswordKey_ThrowsBadRequest(\n        string resetPasswordKey,\n        SutProvider<UserService> sutProvider,\n        Organization organization,\n        OrganizationUser orgUser,\n        [Policy(PolicyType.ResetPassword, true)] PolicyStatus policy)\n    {\n        // Arrange\n        organization.UseResetPassword = true;\n        orgUser.Status = OrganizationUserStatusType.Confirmed;\n        orgUser.OrganizationId = organization.Id;\n        orgUser.ResetPasswordKey = resetPasswordKey;\n        orgUser.UserId = Guid.NewGuid();\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n        sutProvider.GetDependency<IPolicyQuery>()\n            .RunAsync(organization.Id, PolicyType.ResetPassword)\n            .Returns(policy);\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByIdAsync(orgUser.Id)\n            .Returns(orgUser);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.AdminResetPasswordAsync(\n                OrganizationUserType.Owner, organization.Id, orgUser.Id, \"newPassword\", \"key\"));\n        Assert.Equal(\"Organization User not valid\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task AdjustStorageAsync_NullUser_ThrowsArgumentNullException(\n        SutProvider<UserService> sutProvider)\n    {\n        await Assert.ThrowsAsync<ArgumentNullException>(\n            () => sutProvider.Sut.AdjustStorageAsync(null, 1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task AdjustStorageAsync_NotPremium_ThrowsBadRequestException(\n        User user, SutProvider<UserService> sutProvider)\n    {\n        user.Premium = false;\n\n        await Assert.ThrowsAsync<BadRequestException>(\n            () => sutProvider.Sut.AdjustStorageAsync(user, 1));\n    }\n\n    [Theory, BitAutoData]\n    public async Task AdjustStorageAsync_Success_CallsPaymentServiceAndSavesUser(\n        User user, SutProvider<UserService> sutProvider)\n    {\n        user.Premium = true;\n        user.GatewayCustomerId = \"cus_123\";\n        user.GatewaySubscriptionId = \"sub_123\";\n        user.MaxStorageGb = 1;\n        user.Storage = 0;\n\n        var premiumPlan = new Bit.Core.Billing.Pricing.Premium.Plan\n        {\n            Name = \"Premium\",\n            Available = true,\n            Seat = new Bit.Core.Billing.Pricing.Premium.Purchasable { StripePriceId = \"premium-seat\", Price = 10, Provided = 1 },\n            Storage = new Bit.Core.Billing.Pricing.Premium.Purchasable { StripePriceId = \"storage-gb-annually\", Price = 4, Provided = 1 }\n        };\n\n        sutProvider.GetDependency<IPricingClient>().GetAvailablePremiumPlan().Returns(premiumPlan);\n\n        await sutProvider.Sut.AdjustStorageAsync(user, 1);\n\n        await sutProvider.GetDependency<IStripePaymentService>().Received(1)\n            .AdjustStorageAsync(user, Arg.Any<int>(), premiumPlan.Storage.StripePriceId);\n    }\n}\n\npublic static class UserServiceSutProviderExtensions\n{\n    /// <summary>\n    /// Arranges a fake token provider. Must call as part of a builder pattern that ends in Create(), as it modifies\n    /// the SutProvider build chain.\n    /// </summary>\n    private static SutProvider<UserService> SetFakeTokenProvider(this SutProvider<UserService> sutProvider, User user)\n    {\n        var fakeUserTwoFactorProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();\n\n        fakeUserTwoFactorProvider\n            .GenerateAsync(Arg.Any<string>(), Arg.Any<UserManager<User>>(), user)\n            .Returns(\"OTP_TOKEN\");\n\n        fakeUserTwoFactorProvider\n            .ValidateAsync(Arg.Any<string>(), Arg.Is<string>(s => s != \"otp_token\"), Arg.Any<UserManager<User>>(), user)\n            .Returns(false);\n\n        fakeUserTwoFactorProvider\n            .ValidateAsync(Arg.Any<string>(), \"otp_token\", Arg.Any<UserManager<User>>(), user)\n            .Returns(true);\n\n        var fakeIdentityOptions = Substitute.For<IOptions<IdentityOptions>>();\n\n        fakeIdentityOptions\n            .Value\n            .Returns(new IdentityOptions\n            {\n                Tokens = new TokenOptions\n                {\n                    ProviderMap = new Dictionary<string, TokenProviderDescriptor>()\n                    {\n                        [\"Email\"] = new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<User>))\n                        {\n                            ProviderInstance = fakeUserTwoFactorProvider,\n                        }\n                    }\n                }\n            });\n\n        sutProvider.SetDependency(fakeIdentityOptions);\n        // Also set the fake provider dependency so that we can retrieve it easily via GetDependency\n        sutProvider.SetDependency(fakeUserTwoFactorProvider);\n\n        return sutProvider;\n    }\n\n    /// <summary>\n    /// Properly registers IUserPasswordStore as IUserStore so it's injected when the sut is initialized.\n    /// </summary>\n    private static SutProvider<UserService> SetUserPasswordStore(this SutProvider<UserService> sutProvider)\n    {\n        var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>();\n\n        // IUserPasswordStore must be registered under the IUserStore parameter to be properly injected\n        // because this is what the constructor expects\n        sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore);\n\n        // Also store it under its own type for retrieval and configuration\n        sutProvider.SetDependency(substitutedUserPasswordStore);\n\n        return sutProvider;\n    }\n\n    /// <summary>\n    /// This is a hack: when autofixture initializes the sut in sutProvider, it overwrites the public\n    /// PasswordHasher property with a new substitute, so it loses the configured sutProvider mock.\n    /// This doesn't usually happen because our dependencies are not usually public.\n    /// Call this AFTER SutProvider.Create().\n    /// </summary>\n    private static SutProvider<UserService> FixPasswordHasherBug(this SutProvider<UserService> sutProvider)\n    {\n        // Get the configured sutProvider mock and assign it back to the public property in the base class\n        sutProvider.Sut.PasswordHasher = sutProvider.GetDependency<IPasswordHasher<User>>();\n        return sutProvider;\n    }\n\n    /// <summary>\n    /// A helper that combines all SutProvider configuration usually required for UserService.\n    /// Call this instead of SutProvider.Create, after any additional configuration your test needs.\n    /// </summary>\n    public static SutProvider<UserService> CreateWithUserServiceCustomizations(this SutProvider<UserService> sutProvider, User user)\n        => sutProvider\n            .SetUserPasswordStore()\n            .SetFakeTokenProvider(user)\n            .Create()\n            .FixPasswordHasherBug();\n\n}\n"
  },
  {
    "path": "test/Core.Test/Settings/GlobalSettingsTests.cs",
    "content": "﻿using Bit.Core.Settings;\nusing Xunit;\n\nnamespace Bit.Core.Test.Settings;\n\npublic class GlobalSettingsTests\n{\n    public class SqlSettingsTests\n    {\n        private const string _testingConnectionString =\n            \"Server=server;Database=database;User Id=user;Password=password;\";\n\n        private const string _testingReadOnlyConnectionString =\n            \"Server=server_read;Database=database_read;User Id=user_read;Password=password_read;\";\n\n        [Fact]\n        public void ConnectionString_ValueInDoubleQuotes_Stripped()\n        {\n            var settings = new GlobalSettings.SqlSettings { ConnectionString = $\"\\\"{_testingConnectionString}\\\"\", };\n\n            Assert.Equal(_testingConnectionString, settings.ConnectionString);\n        }\n\n        [Fact]\n        public void ConnectionString_ValueWithoutDoubleQuotes_TheSameValue()\n        {\n            var settings = new GlobalSettings.SqlSettings { ConnectionString = _testingConnectionString };\n\n            Assert.Equal(_testingConnectionString, settings.ConnectionString);\n        }\n\n        [Fact]\n        public void ConnectionString_SetTwice_ReturnsSecondConnectionString()\n        {\n            var settings = new GlobalSettings.SqlSettings { ConnectionString = _testingConnectionString };\n\n            Assert.Equal(_testingConnectionString, settings.ConnectionString);\n\n            var newConnectionString = $\"{_testingConnectionString}_new\";\n            settings.ConnectionString = newConnectionString;\n\n            Assert.Equal(newConnectionString, settings.ConnectionString);\n        }\n\n        [Fact]\n        public void ReadOnlyConnectionString_ValueInDoubleQuotes_Stripped()\n        {\n            var settings = new GlobalSettings.SqlSettings\n            {\n                ReadOnlyConnectionString = $\"\\\"{_testingReadOnlyConnectionString}\\\"\",\n            };\n\n            Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString);\n        }\n\n        [Fact]\n        public void ReadOnlyConnectionString_ValueWithoutDoubleQuotes_TheSameValue()\n        {\n            var settings = new GlobalSettings.SqlSettings\n            {\n                ReadOnlyConnectionString = _testingReadOnlyConnectionString\n            };\n\n            Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString);\n        }\n\n        [Fact]\n        public void ReadOnlyConnectionString_NotSet_DefaultsToConnectionString()\n        {\n            var settings = new GlobalSettings.SqlSettings { ConnectionString = _testingConnectionString };\n\n            Assert.Equal(_testingConnectionString, settings.ReadOnlyConnectionString);\n        }\n\n        [Fact]\n        public void ReadOnlyConnectionString_Set_ReturnsReadOnlyConnectionString()\n        {\n            var settings = new GlobalSettings.SqlSettings\n            {\n                ConnectionString = _testingConnectionString,\n                ReadOnlyConnectionString = _testingReadOnlyConnectionString\n            };\n\n            Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString);\n        }\n\n        [Fact]\n        public void ReadOnlyConnectionString_SetTwice_ReturnsSecondReadOnlyConnectionString()\n        {\n            var settings = new GlobalSettings.SqlSettings\n            {\n                ConnectionString = _testingConnectionString,\n                ReadOnlyConnectionString = _testingReadOnlyConnectionString\n            };\n\n            Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString);\n\n            var newReadOnlyConnectionString = $\"{_testingReadOnlyConnectionString}_new\";\n            settings.ReadOnlyConnectionString = newReadOnlyConnectionString;\n\n            Assert.Equal(newReadOnlyConnectionString, settings.ReadOnlyConnectionString);\n        }\n\n        [Fact]\n        public void ReadOnlyConnectionString_NotSetAndConnectionStringSetTwice_ReturnsSecondConnectionString()\n        {\n            var settings = new GlobalSettings.SqlSettings { ConnectionString = _testingConnectionString };\n\n            Assert.Equal(_testingConnectionString, settings.ReadOnlyConnectionString);\n\n            var newConnectionString = $\"{_testingConnectionString}_new\";\n            settings.ConnectionString = newConnectionString;\n\n            Assert.Equal(newConnectionString, settings.ReadOnlyConnectionString);\n        }\n\n        [Fact]\n        public void ReadOnlyConnectionString_SetAndConnectionStringSetTwice_ReturnsReadOnlyConnectionString()\n        {\n            var settings = new GlobalSettings.SqlSettings\n            {\n                ConnectionString = _testingConnectionString,\n                ReadOnlyConnectionString = _testingReadOnlyConnectionString\n            };\n\n            Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString);\n\n            var newConnectionString = $\"{_testingConnectionString}_new\";\n            settings.ConnectionString = newConnectionString;\n\n            Assert.Equal(_testingReadOnlyConnectionString, settings.ReadOnlyConnectionString);\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/TempDirectory.cs",
    "content": "﻿namespace Bit.Core.Test;\n\npublic class TempDirectory : IDisposable\n{\n    public string Directory { get; private set; }\n\n    public TempDirectory()\n    {\n        Directory = Path.Combine(Path.GetTempPath(), $\"bitwarden_{Guid.NewGuid().ToString().Replace(\"-\", \"\")}\");\n    }\n\n    public override string ToString() => Directory;\n\n    #region IDisposable implementation\n    ~TempDirectory()\n    {\n        Dispose(false);\n    }\n\n    public void Dispose()\n    {\n        Dispose(true);\n        GC.SuppressFinalize(this);\n    }\n\n    public void Dispose(bool disposing)\n    {\n        if (disposing)\n        {\n            try\n            {\n                System.IO.Directory.Delete(Directory, true);\n            }\n            catch { }\n        }\n    }\n    # endregion\n}\n"
  },
  {
    "path": "test/Core.Test/Tokens/DataProtectorTokenFactoryTests.cs",
    "content": "﻿using System.Security.Cryptography;\nusing AutoFixture;\nusing Bit.Core.Tokens;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Bit.Test.Common.Helpers;\nusing Microsoft.AspNetCore.DataProtection;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tokens;\n\n[SutProviderCustomize]\npublic class DataProtectorTokenFactoryTests\n{\n    public static SutProvider<DataProtectorTokenFactory<TestTokenable>> GetSutProvider()\n    {\n        var fixture = new Fixture();\n        return new SutProvider<DataProtectorTokenFactory<TestTokenable>>(fixture)\n            .SetDependency<IDataProtectionProvider>(fixture.Create<EphemeralDataProtectionProvider>())\n            .Create();\n    }\n\n    [Theory, BitAutoData]\n    public void CanRoundTripTokenables(TestTokenable tokenable)\n    {\n        var sutProvider = GetSutProvider();\n\n        var token = sutProvider.Sut.Protect(tokenable);\n        var recoveredTokenable = sutProvider.Sut.Unprotect(token);\n\n        AssertHelper.AssertPropertyEqual(tokenable, recoveredTokenable);\n    }\n\n    [Theory, BitAutoData]\n    public void PrependsClearText(TestTokenable tokenable)\n    {\n        var sutProvider = GetSutProvider();\n\n        var token = sutProvider.Sut.Protect(tokenable);\n\n        Assert.StartsWith(sutProvider.GetDependency<string>(\"clearTextPrefix\"), token);\n    }\n\n    [Theory, BitAutoData]\n    public void EncryptsToken(TestTokenable tokenable)\n    {\n        var sutProvider = GetSutProvider();\n        var prefix = sutProvider.GetDependency<string>(\"clearTextPrefix\");\n\n        var token = sutProvider.Sut.Protect(tokenable);\n\n        Assert.NotEqual(new Token(token).RemovePrefix(prefix), tokenable.ToToken());\n    }\n\n    [Theory, BitAutoData]\n    public void ThrowsIfUnprotectFails(TestTokenable tokenable)\n    {\n        var sutProvider = GetSutProvider();\n\n        var token = sutProvider.Sut.Protect(tokenable);\n        token += \"stuff to make sure decryption fails\";\n\n        Assert.Throws<CryptographicException>(() => sutProvider.Sut.Unprotect(token));\n    }\n\n    [Theory, BitAutoData]\n    public void TryUnprotect_FalseIfUnprotectFails(TestTokenable tokenable)\n    {\n        var sutProvider = GetSutProvider();\n        var token = sutProvider.Sut.Protect(tokenable) + \"fail decryption\";\n\n        var result = sutProvider.Sut.TryUnprotect(token, out var data);\n\n        Assert.False(result);\n        Assert.Null(data);\n    }\n\n    [Theory, BitAutoData]\n    public void TokenValid_FalseIfUnprotectFails(TestTokenable tokenable)\n    {\n        var sutProvider = GetSutProvider();\n        var token = sutProvider.Sut.Protect(tokenable) + \"fail decryption\";\n\n        var result = sutProvider.Sut.TokenValid(token);\n\n        Assert.False(result);\n    }\n\n\n    [Theory, BitAutoData]\n    public void TokenValid_FalseIfTokenInvalid(TestTokenable tokenable)\n    {\n        var sutProvider = GetSutProvider();\n\n        tokenable.ForceInvalid = true;\n        var token = sutProvider.Sut.Protect(tokenable);\n\n        var result = sutProvider.Sut.TokenValid(token);\n\n        Assert.False(result);\n    }\n\n    [Theory, BitAutoData]\n    public void TryUnprotect_TrueIfSuccess(TestTokenable tokenable)\n    {\n        var sutProvider = GetSutProvider();\n        var token = sutProvider.Sut.Protect(tokenable);\n\n        var result = sutProvider.Sut.TryUnprotect(token, out var data);\n\n        Assert.True(result);\n        AssertHelper.AssertPropertyEqual(tokenable, data);\n    }\n\n    [Theory, BitAutoData]\n    public void TokenValid_TrueIfSuccess(TestTokenable tokenable)\n    {\n        tokenable.ForceInvalid = false;\n        var sutProvider = GetSutProvider();\n        var token = sutProvider.Sut.Protect(tokenable);\n\n        var result = sutProvider.Sut.TokenValid(token);\n\n        Assert.True(result);\n    }\n\n}\n"
  },
  {
    "path": "test/Core.Test/Tokens/ExpiringTokenTests.cs",
    "content": "﻿using System.Text.Json;\nusing AutoFixture.Xunit2;\nusing Bit.Core.Utilities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tokens;\n\npublic class ExpiringTokenTests\n{\n    [Theory, AutoData]\n    public void ExpirationSerializesToEpochMilliseconds(DateTime expirationDate)\n    {\n        var sut = new TestExpiringTokenable\n        {\n            ExpirationDate = expirationDate\n        };\n\n        var result = JsonSerializer.Serialize(sut);\n        var expectedDate = CoreHelpers.ToEpocMilliseconds(expirationDate);\n\n        Assert.Contains($\"\\\"ExpirationDate\\\":{expectedDate}\", result);\n    }\n\n    [Theory, AutoData]\n    public void ExpirationSerializationRoundTrip(DateTime expirationDate)\n    {\n        var sut = new TestExpiringTokenable\n        {\n            ExpirationDate = expirationDate\n        };\n\n        var intermediate = JsonSerializer.Serialize(sut);\n        var result = JsonSerializer.Deserialize<TestExpiringTokenable>(intermediate);\n\n        Assert.Equal(sut.ExpirationDate, result.ExpirationDate, TimeSpan.FromMilliseconds(100));\n    }\n\n    [Fact]\n    public void InvalidIfPastExpiryDate()\n    {\n        var sut = new TestExpiringTokenable\n        {\n            ExpirationDate = DateTime.UtcNow.AddHours(-1)\n        };\n\n        Assert.False(sut.Valid);\n    }\n\n    [Fact]\n    public void ValidIfWithinExpirationAndTokenReportsValid()\n    {\n        var sut = new TestExpiringTokenable\n        {\n            ExpirationDate = DateTime.UtcNow.AddHours(1)\n        };\n\n        Assert.True(sut.Valid);\n    }\n\n    [Fact]\n    public void HonorsTokenIsValidAbstractMember()\n    {\n        var sut = new TestExpiringTokenable(forceInvalid: true)\n        {\n            ExpirationDate = DateTime.UtcNow.AddHours(1)\n        };\n\n        Assert.False(sut.Valid);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Tokens/TokenTests.cs",
    "content": "﻿using AutoFixture.Xunit2;\nusing Bit.Core.Tokens;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tokens;\n\npublic class TokenTests\n{\n    [Theory, AutoData]\n    public void InitializeWithString_ReturnsString(string initString)\n    {\n        var token = new Token(initString);\n\n        Assert.Equal(initString, token.ToString());\n    }\n\n    [Theory, AutoData]\n    public void AddsPrefix(Token token, string prefix)\n    {\n        Assert.Equal($\"{prefix}{token.ToString()}\", token.WithPrefix(prefix).ToString());\n    }\n\n    [Theory, AutoData]\n    public void RemovePrefix_WithPrefix_RemovesPrefix(string initString, string prefix)\n    {\n        var token = new Token(initString).WithPrefix(prefix);\n\n        Assert.Equal(initString, token.RemovePrefix(prefix).ToString());\n    }\n\n    [Theory, AutoData]\n    public void RemovePrefix_WithoutPrefix_Throws(Token token, string prefix)\n    {\n        var exception = Assert.Throws<BadTokenException>(() => token.RemovePrefix(prefix));\n\n        Assert.Equal($\"Expected prefix, {prefix}, was not present.\", exception.Message);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Tools/AutoFixture/SendFixtures.cs",
    "content": "﻿using System.Reflection;\nusing AutoFixture;\nusing AutoFixture.Xunit2;\nusing Bit.Core.Tools.Entities;\nusing Bit.Test.Common.AutoFixture.Attributes;\n\nnamespace Bit.Core.Test.Tools.AutoFixture.SendFixtures;\n\ninternal class UserSend : ICustomization\n{\n    public Guid? UserId { get; set; }\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<Send>(composer => composer\n            .With(s => s.UserId, UserId ?? Guid.NewGuid())\n            .Without(s => s.OrganizationId));\n    }\n}\n\ninternal class UserSendCustomizeAttribute : BitCustomizeAttribute\n{\n    public override ICustomization GetCustomization() => new UserSend();\n}\n\ninternal class NewUserSend : ICustomization\n{\n    public void Customize(IFixture fixture)\n    {\n        fixture.Customize<Send>(composer => composer\n            .With(s => s.Id, Guid.Empty)\n            .Without(s => s.OrganizationId));\n    }\n}\n\ninternal class NewUserSendCustomizeAttribute : CustomizeAttribute\n{\n    public override ICustomization GetCustomization(ParameterInfo parameterInfo)\n        => new NewUserSend();\n}\n\n"
  },
  {
    "path": "test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.Enums;\nusing Bit.Core.AdminConsole.Models.Data.Organizations.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.AdminConsole.Services;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.CipherFixtures;\nusing Bit.Core.Tools.ImportFeatures;\nusing Bit.Core.Vault.Entities;\nusing Bit.Core.Vault.Models.Data;\nusing Bit.Core.Vault.Repositories;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tools.ImportFeatures;\n\n[UserCipherCustomize]\n[SutProviderCustomize]\npublic class ImportCiphersAsyncCommandTests\n{\n    [Theory, BitAutoData]\n    public async Task ImportIntoIndividualVaultAsync_Success(\n        Guid importingUserId,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IPolicyService>()\n            .AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership)\n            .Returns(false);\n\n        sutProvider.GetDependency<IFolderRepository>()\n            .GetManyByUserIdAsync(importingUserId)\n            .Returns(new List<Folder>());\n\n        var folders = new List<Folder> { new Folder { UserId = importingUserId } };\n\n        var folderRelationships = new List<KeyValuePair<int, int>>();\n\n        // Act\n        await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);\n\n        // Assert\n        await sutProvider.GetDependency<ICipherRepository>()\n            .Received(1)\n            .CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success(\n        Guid importingUserId,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider)\n    {\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(importingUserId)\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(\n                OrganizationDataOwnershipState.Disabled,\n                []));\n\n        sutProvider.GetDependency<IFolderRepository>()\n            .GetManyByUserIdAsync(importingUserId)\n            .Returns(new List<Folder>());\n\n        var folders = new List<Folder> { new Folder { UserId = importingUserId } };\n\n        var folderRelationships = new List<KeyValuePair<int, int>>();\n\n        await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);\n\n        await sutProvider.GetDependency<ICipherRepository>()\n            .Received(1)\n            .CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException(\n        List<Folder> folders,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider)\n    {\n        var userId = Guid.NewGuid();\n        folders.ForEach(f => f.UserId = userId);\n        ciphers.ForEach(c => c.UserId = userId);\n\n        sutProvider.GetDependency<IPolicyService>()\n            .AnyPoliciesApplicableToUserAsync(userId, PolicyType.OrganizationDataOwnership)\n            .Returns(true);\n\n        var folderRelationships = new List<KeyValuePair<int, int>>();\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, userId));\n\n        Assert.Equal(\"You cannot import items into your personal vault because you are a member of an organization which forbids it.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyEnabled_ThrowsBadRequestException(\n        List<Folder> folders,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider)\n    {\n        var userId = Guid.NewGuid();\n        folders.ForEach(f => f.UserId = userId);\n        ciphers.ForEach(c => c.UserId = userId);\n\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(\n                OrganizationDataOwnershipState.Enabled,\n                [new PolicyDetails()]));\n\n        var folderRelationships = new List<KeyValuePair<int, int>>();\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, userId));\n\n        Assert.Equal(\"You cannot import items into your personal vault because you are a member of an organization which forbids it.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportIntoIndividualVaultAsync_FavoriteCiphers_PersistsFavoriteInfo(\n        Guid importingUserId,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider\n    )\n    {\n        sutProvider.GetDependency<IFeatureService>()\n            .IsEnabled(FeatureFlagKeys.PolicyRequirements)\n            .Returns(true);\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>()\n            .GetAsync<OrganizationDataOwnershipPolicyRequirement>(importingUserId)\n            .Returns(new OrganizationDataOwnershipPolicyRequirement(\n                OrganizationDataOwnershipState.Disabled,\n                []));\n\n        sutProvider.GetDependency<IFolderRepository>()\n            .GetManyByUserIdAsync(importingUserId)\n            .Returns(new List<Folder>());\n\n        var folders = new List<Folder>();\n        var folderRelationships = new List<KeyValuePair<int, int>>();\n\n        ciphers.ForEach(c =>\n        {\n            c.UserId = importingUserId;\n            c.Favorite = true;\n        });\n\n        await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);\n\n        await sutProvider.GetDependency<ICipherRepository>()\n            .Received(1)\n            .CreateAsync(importingUserId, Arg.Is<IEnumerable<Cipher>>(ciphers => ciphers.All(c => c.Favorites == $\"{{\\\"{importingUserId.ToString().ToUpperInvariant()}\\\":true}}\")), Arg.Any<List<Folder>>());\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportIntoOrganizationalVaultAsync_Success(\n        Organization organization,\n        Guid importingUserId,\n        OrganizationUser importingOrganizationUser,\n        List<Collection> collections,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider)\n    {\n        organization.MaxCollections = null;\n        importingOrganizationUser.OrganizationId = organization.Id;\n\n        foreach (var collection in collections)\n        {\n            collection.OrganizationId = organization.Id;\n        }\n\n        foreach (var cipher in ciphers)\n        {\n            cipher.OrganizationId = organization.Id;\n        }\n\n        KeyValuePair<int, int>[] collectionRelationships = {\n            new(0, 0),\n            new(1, 1),\n            new(2, 2)\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organization.Id, importingUserId)\n            .Returns(importingOrganizationUser);\n\n        // Set up a collection that already exists in the organization\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(organization.Id)\n            .Returns(new List<Collection> { collections[0] });\n\n        await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);\n\n        await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(\n            ciphers,\n            Arg.Is<IEnumerable<Collection>>(cols => cols.Count() == collections.Count - 1 &&\n                        !cols.Any(c => c.Id == collections[0].Id) && // Check that the collection that already existed in the organization was not added\n                        cols.All(c => collections.Any(x => c.Name == x.Name))),\n            Arg.Is<IEnumerable<CollectionCipher>>(c => c.Count() == ciphers.Count),\n            Arg.Is<IEnumerable<CollectionUser>>(cus =>\n                cus.Count() == collections.Count - 1 &&\n                !cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization\n                cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true)));\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportIntoOrganizationalVaultAsync_ThrowsBadRequestException(\n        Organization organization,\n        Guid importingUserId,\n        OrganizationUser importingOrganizationUser,\n        List<Collection> collections,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider)\n    {\n        organization.MaxCollections = 1;\n        importingOrganizationUser.OrganizationId = organization.Id;\n\n        foreach (var collection in collections)\n        {\n            collection.OrganizationId = organization.Id;\n        }\n\n        foreach (var cipher in ciphers)\n        {\n            cipher.OrganizationId = organization.Id;\n        }\n\n        KeyValuePair<int, int>[] collectionRelationships = {\n            new(0, 0),\n            new(1, 1),\n            new(2, 2)\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organization.Id, importingUserId)\n            .Returns(importingOrganizationUser);\n\n        // Set up a collection that already exists in the organization\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(organization.Id)\n            .Returns(new List<Collection> { collections[0] });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId));\n\n        Assert.Equal(\"This organization can only have a maximum of \" +\n        $\"{organization.MaxCollections} collections.\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportIntoOrganizationalVaultAsync_WithNullImportingOrgUser_SkipsCollectionUserCreation(\n        Organization organization,\n        Guid importingUserId,\n        List<Collection> collections,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider)\n    {\n        organization.MaxCollections = null;\n\n        foreach (var collection in collections)\n        {\n            collection.OrganizationId = organization.Id;\n        }\n\n        foreach (var cipher in ciphers)\n        {\n            cipher.OrganizationId = organization.Id;\n        }\n\n        KeyValuePair<int, int>[] collectionRelationships = {\n            new(0, 0),\n            new(1, 1),\n            new(2, 2)\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        // Simulate provider-created org with no members - importing user is NOT an org member\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organization.Id, importingUserId)\n            .Returns((OrganizationUser)null);\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(organization.Id)\n            .Returns(new List<Collection>());\n\n        await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);\n\n        // Verify ciphers were created but no CollectionUser entries were created (because the organization user (importingUserId) is null)\n        await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(\n            ciphers,\n            Arg.Is<IEnumerable<Collection>>(cols => cols.Count() == collections.Count),\n            Arg.Is<IEnumerable<CollectionCipher>>(cc => cc.Count() == ciphers.Count),\n            Arg.Is<IEnumerable<CollectionUser>>(cus => !cus.Any()));\n\n        await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ImportIntoIndividualVaultAsync_WithArchivedCiphers_PreservesArchiveStatus(\n        Guid importingUserId,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider)\n    {\n        var archivedDate = DateTime.UtcNow.AddDays(-1);\n        ciphers[0].UserId = importingUserId;\n        ciphers[0].ArchivedDate = archivedDate;\n\n        sutProvider.GetDependency<IPolicyService>()\n            .AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership)\n            .Returns(false);\n\n        sutProvider.GetDependency<IFolderRepository>()\n            .GetManyByUserIdAsync(importingUserId)\n            .Returns(new List<Folder>());\n\n        var folders = new List<Folder>();\n        var folderRelationships = new List<KeyValuePair<int, int>>();\n\n        await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);\n\n        await sutProvider.GetDependency<ICipherRepository>()\n            .Received(1)\n            .CreateAsync(importingUserId,\n                Arg.Is<List<CipherDetails>>(c =>\n                    c[0].Archives != null &&\n                    c[0].Archives.Contains(importingUserId.ToString().ToUpperInvariant()) &&\n                    c[0].Archives.Contains(archivedDate.ToString(\"yyyy-MM-ddTHH:mm:ss.fffffffZ\"))),\n                Arg.Any<List<Folder>>());\n    }\n\n    /*\n     * Archive functionality is a per-user function. When importing archived ciphers into an organization vault,\n     * the Archives field should be set for the importing user only. This allows the importing user to see\n     * items as archived, while other organization members will not see them as archived.\n     */\n    [Theory, BitAutoData]\n    public async Task ImportIntoOrganizationalVaultAsync_WithArchivedCiphers_SetsArchivesForImportingUserOnly(\n        Organization organization,\n        Guid importingUserId,\n        OrganizationUser importingOrganizationUser,\n        List<Collection> collections,\n        List<CipherDetails> ciphers,\n        SutProvider<ImportCiphersCommand> sutProvider)\n    {\n        var archivedDate = DateTime.UtcNow.AddDays(-1);\n        organization.MaxCollections = null;\n        importingOrganizationUser.OrganizationId = organization.Id;\n\n        foreach (var collection in collections)\n        {\n            collection.OrganizationId = organization.Id;\n        }\n\n        foreach (var cipher in ciphers)\n        {\n            cipher.OrganizationId = organization.Id;\n        }\n\n        ciphers[0].ArchivedDate = archivedDate;\n        ciphers[0].Archives = null;\n\n        KeyValuePair<int, int>[] collectionRelationships = {\n            new(0, 0),\n            new(1, 1),\n            new(2, 2)\n        };\n\n        sutProvider.GetDependency<IOrganizationRepository>()\n            .GetByIdAsync(organization.Id)\n            .Returns(organization);\n\n        sutProvider.GetDependency<IOrganizationUserRepository>()\n            .GetByOrganizationAsync(organization.Id, importingUserId)\n            .Returns(importingOrganizationUser);\n\n        sutProvider.GetDependency<ICollectionRepository>()\n            .GetManyByOrganizationIdAsync(organization.Id)\n            .Returns(new List<Collection>());\n\n        await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);\n\n        await sutProvider.GetDependency<ICipherRepository>()\n            .Received(1)\n            .CreateAsync(\n                Arg.Is<List<CipherDetails>>(c =>\n                    c[0].ArchivedDate == archivedDate &&\n                    c[0].Archives != null &&\n                    c[0].Archives.Contains(importingUserId.ToString().ToUpperInvariant()) &&\n                    c[0].Archives.Contains(archivedDate.ToString(\"yyyy-MM-ddTHH:mm:ss.fffffffZ\"))),\n                Arg.Any<IEnumerable<Collection>>(),\n                Arg.Any<IEnumerable<CollectionCipher>>(),\n                Arg.Any<IEnumerable<CollectionUser>>());\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Tools/Models/Data/SendFileDataTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tools.Models.Data;\n\npublic class SendFileDataTests\n{\n    [Fact]\n    public void Serialize_Success()\n    {\n        var sut = new SendFileData\n        {\n            Id = \"test\",\n            Size = 100,\n            FileName = \"thing.pdf\",\n            Validated = true,\n        };\n\n        var json = JsonSerializer.Serialize(sut);\n        var document = JsonDocument.Parse(json);\n        var root = document.RootElement;\n        AssertHelper.AssertJsonProperty(root, \"Size\", JsonValueKind.String);\n        Assert.False(root.TryGetProperty(\"SizeString\", out _));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Commands;\nusing Bit.Core.Tools.Services;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tools.Services;\n\npublic class AnonymousSendCommandTests\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly ISendFileStorageService _sendFileStorageService;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly ISendAuthorizationService _sendAuthorizationService;\n    private readonly AnonymousSendCommand _anonymousSendCommand;\n\n    public AnonymousSendCommandTests()\n    {\n        _sendRepository = Substitute.For<ISendRepository>();\n        _sendFileStorageService = Substitute.For<ISendFileStorageService>();\n        _pushNotificationService = Substitute.For<IPushNotificationService>();\n        _sendAuthorizationService = Substitute.For<ISendAuthorizationService>();\n\n        _anonymousSendCommand = new AnonymousSendCommand(\n            _sendRepository,\n            _sendFileStorageService,\n            _pushNotificationService,\n            _sendAuthorizationService);\n    }\n\n    [Fact]\n    public async Task GetSendFileDownloadUrlAsync_Success_ReturnsDownloadUrl()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            AccessCount = 0,\n            Data = JsonSerializer.Serialize(new { Id = \"fileId123\" })\n        };\n        var fileId = \"fileId123\";\n        var password = \"testPassword\";\n        var expectedUrl = \"https://example.com/download\";\n\n        _sendAuthorizationService\n            .SendCanBeAccessed(send, password)\n            .Returns(SendAccessResult.Granted);\n\n        _sendFileStorageService\n            .GetSendFileDownloadUrlAsync(send, fileId)\n            .Returns(expectedUrl);\n\n        // Act\n        var result =\n            await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password);\n\n        // Assert\n        Assert.Equal(expectedUrl, result.Item1);\n        Assert.Equal(1, send.AccessCount);\n\n        await _sendRepository.Received(1).ReplaceAsync(send);\n        await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);\n    }\n\n    [Fact]\n    public async Task GetSendFileDownloadUrlAsync_AccessDenied_ReturnsNullWithReasons()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            AccessCount = 0\n        };\n        var fileId = \"fileId123\";\n        var password = \"wrongPassword\";\n\n        _sendAuthorizationService\n            .SendCanBeAccessed(send, password)\n            .Returns(SendAccessResult.Denied);\n\n        // Act\n        var result =\n            await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password);\n\n        // Assert\n        Assert.Null(result.Item1);\n        Assert.Equal(SendAccessResult.Denied, result.Item2);\n        Assert.Equal(0, send.AccessCount);\n\n        await _sendRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default);\n        await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncSendUpdateAsync(default);\n    }\n\n    [Fact]\n    public async Task GetSendFileDownloadUrlAsync_NotFileSend_ThrowsBadRequestException()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.Text\n        };\n        var fileId = \"fileId123\";\n        var password = \"testPassword\";\n\n        // Act & Assert\n        await Assert.ThrowsAsync<BadRequestException>(() =>\n            _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Platform.Push;\nusing Bit.Core.Services;\nusing Bit.Core.Test.AutoFixture.CurrentContextFixtures;\nusing Bit.Core.Test.Tools.AutoFixture.SendFixtures;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Commands;\nusing Bit.Core.Tools.SendFeatures.Commands.Interfaces;\nusing Bit.Core.Tools.Services;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing NSubstitute.ExceptionExtensions;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tools.Services;\n\n[SutProviderCustomize]\n[CurrentContextCustomize]\n[UserSendCustomize]\npublic class NonAnonymousSendCommandTests\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly ISendFileStorageService _sendFileStorageService;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly ISendValidationService _sendValidationService;\n    private readonly IFeatureService _featureService;\n    private readonly ICurrentContext _currentContext;\n    private readonly ISendCoreHelperService _sendCoreHelperService;\n    private readonly NonAnonymousSendCommand _nonAnonymousSendCommand;\n\n    private readonly ILogger<NonAnonymousSendCommand> _logger;\n\n    public NonAnonymousSendCommandTests()\n    {\n        _sendRepository = Substitute.For<ISendRepository>();\n        _sendFileStorageService = Substitute.For<ISendFileStorageService>();\n        _pushNotificationService = Substitute.For<IPushNotificationService>();\n        _featureService = Substitute.For<IFeatureService>();\n        _sendValidationService = Substitute.For<ISendValidationService>();\n        _currentContext = Substitute.For<ICurrentContext>();\n        _sendCoreHelperService = Substitute.For<ISendCoreHelperService>();\n        _logger = Substitute.For<ILogger<NonAnonymousSendCommand>>();\n\n        _nonAnonymousSendCommand = new NonAnonymousSendCommand(\n            _sendRepository,\n            _sendFileStorageService,\n            _pushNotificationService,\n            _sendValidationService,\n            _sendCoreHelperService,\n            _logger\n        );\n    }\n\n    // Disable Send policy check\n    [Theory]\n    [InlineData(SendType.File)]\n    [InlineData(SendType.Text)]\n    public async Task SaveSendAsync_DisableSend_Applies_throws(SendType sendType)\n    {\n        // Arrange\n        var send = new Send\n        {\n            Id = default,\n            Type = sendType,\n            UserId = Guid.NewGuid()\n        };\n\n        var user = new User\n        {\n            Id = send.UserId.Value,\n            Email = \"test@example.com\"\n        };\n\n        // Configure validation service to throw when DisableSend policy applies\n        _sendValidationService.ValidateUserCanSaveAsync(send.UserId.Value, send)\n            .Throws(new BadRequestException(\"Due to an Enterprise Policy, you are only able to delete an existing Send.\"));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveSendAsync(send));\n\n        Assert.Contains(\"Enterprise Policy\", exception.Message);\n\n        // Verify the validation service was called\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(send.UserId.Value, send);\n\n        // Verify repository was not called since exception was thrown\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n    }\n\n    [Theory]\n    [InlineData(true)]  // New Send (Id is default)\n    [InlineData(false)] // Existing Send (Id is not default)\n    public async Task SaveSendAsync_DisableSend_DoesntApply_success(bool isNewSend)\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = isNewSend ? default : Guid.NewGuid(),\n            Type = SendType.Text,\n            UserId = userId,\n            Data = \"Text with Notes\"\n        };\n\n        var initialDate = DateTime.UtcNow.AddMinutes(-5);\n        send.RevisionDate = initialDate;\n\n        // Configure validation service to NOT throw (policy doesn't apply)\n        _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask);\n\n        // Set up context for reference event\n        _currentContext.ClientId.Returns(\"test-client\");\n        _currentContext.ClientVersion.Returns(Version.Parse(\"1.0.0\"));\n\n        // Act\n        await _nonAnonymousSendCommand.SaveSendAsync(send);\n\n        // Assert\n        // Verify validation was checked\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send);\n\n        if (isNewSend)\n        {\n            // For new Sends\n            await _sendRepository.Received(1).CreateAsync(send);\n            await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send);\n        }\n        else\n        {\n            // For existing Sends\n            await _sendRepository.Received(1).UpsertAsync(send);\n            Assert.NotEqual(initialDate, send.RevisionDate);\n            await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);\n        }\n    }\n\n    [Theory]\n    [InlineData(true)]  // New Send (Id is default)\n    [InlineData(false)] // Existing Send (Id is not default)\n    public async Task SaveSendAsync_DisableHideEmail_Applies_throws(bool isNewSend)\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = isNewSend ? default : Guid.NewGuid(),\n            Type = SendType.Text,\n            UserId = userId,\n            HideEmail = true\n        };\n\n        // Configure validation service to throw when HideEmail policy applies\n        _sendValidationService.ValidateUserCanSaveAsync(userId, send)\n            .Throws(new BadRequestException(\"Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.\"));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveSendAsync(send));\n\n        Assert.Contains(\"hide your email address\", exception.Message);\n\n        // Verify validation was called\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send);\n\n        // Verify repository was not called (exception prevented save)\n        if (isNewSend)\n        {\n            await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        }\n        else\n        {\n            await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        }\n\n        // Verify push notification wasn't sent\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Theory]\n    [InlineData(true)]  // New Send (Id is default)\n    [InlineData(false)] // Existing Send (Id is not default)\n    public async Task SaveSendAsync_DisableHideEmail_DoesntApply_success(bool isNewSend)\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = isNewSend ? default : Guid.NewGuid(),\n            Type = SendType.Text,\n            UserId = userId,\n            HideEmail = true  // Setting HideEmail to true\n        };\n\n        var initialDate = DateTime.UtcNow.AddMinutes(-5);\n        send.RevisionDate = initialDate;\n\n        // Configure validation service to NOT throw (policy doesn't apply)\n        _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask);\n\n        // Set up context for reference event\n        _currentContext.ClientId.Returns(\"test-client\");\n        _currentContext.ClientVersion.Returns(Version.Parse(\"1.0.0\"));\n\n        // Act\n        await _nonAnonymousSendCommand.SaveSendAsync(send);\n\n        // Assert\n        // Verify validation was checked\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send);\n\n        if (isNewSend)\n        {\n            // For new Sends\n            await _sendRepository.Received(1).CreateAsync(send);\n            await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send);\n        }\n        else\n        {\n            // For existing Sends\n            await _sendRepository.Received(1).UpsertAsync(send);\n            Assert.NotEqual(initialDate, send.RevisionDate);\n            await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);\n        }\n    }\n\n    [Theory]\n    [InlineData(SendType.File)]\n    [InlineData(SendType.Text)]\n    public async Task SaveSendAsync_DisableSend_Applies_Throws_vNext(SendType sendType)\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = default,\n            Type = sendType,\n            UserId = userId\n        };\n\n        // Configure validation service to throw when DisableSend policy applies in vNext implementation\n        _sendValidationService.ValidateUserCanSaveAsync(userId, send)\n            .Returns(Task.FromException(new BadRequestException(\"Due to an Enterprise Policy, you are only able to delete an existing Send.\")));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveSendAsync(send));\n\n        Assert.Contains(\"Enterprise Policy\", exception.Message);\n\n        // Verify validation service was called\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send);\n\n        // Verify repository and notification methods were not called\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Theory]\n    [InlineData(true)]  // New Send (Id is default)\n    [InlineData(false)] // Existing Send (Id is not default)\n    public async Task SaveSendAsync_DisableSend_DoesntApply_Success_vNext(bool isNewSend)\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = isNewSend ? default : Guid.NewGuid(),\n            Type = SendType.Text,\n            UserId = userId,\n            Data = \"Text with Notes\"\n        };\n\n        var initialDate = DateTime.UtcNow.AddMinutes(-5);\n        send.RevisionDate = initialDate;\n\n        // Configure validation service to return success for vNext implementation\n        _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask);\n\n        // Set up context for reference event\n        _currentContext.ClientId.Returns(\"test-client\");\n        _currentContext.ClientVersion.Returns(Version.Parse(\"1.0.0\"));\n\n        // Enable feature flag for policy requirements (vNext path)\n        _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);\n\n        // Act\n        await _nonAnonymousSendCommand.SaveSendAsync(send);\n\n        // Assert\n        // Verify validation was checked with vNext path\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send);\n\n        if (isNewSend)\n        {\n            // For new Sends\n            await _sendRepository.Received(1).CreateAsync(send);\n            await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send);\n        }\n        else\n        {\n            // For existing Sends\n            await _sendRepository.Received(1).UpsertAsync(send);\n            Assert.NotEqual(initialDate, send.RevisionDate);\n            await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);\n        }\n    }\n\n    // Send Options Policy - Disable Hide Email check\n    [Theory]\n    [InlineData(true)]  // New Send (Id is default)\n    [InlineData(false)] // Existing Send (Id is not default)\n    public async Task SaveSendAsync_DisableHideEmail_Applies_Throws_vNext(bool isNewSend)\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = isNewSend ? default : Guid.NewGuid(),\n            Type = SendType.Text,\n            UserId = userId,\n            HideEmail = true\n        };\n\n        // Enable feature flag for policy requirements (vNext path)\n        _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);\n\n        // Configure validation service to throw when DisableHideEmail policy applies in vNext implementation\n        _sendValidationService.ValidateUserCanSaveAsync(userId, send)\n            .Throws(new BadRequestException(\"Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.\"));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveSendAsync(send));\n\n        Assert.Contains(\"hide your email address\", exception.Message);\n\n        // Verify validation was called\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send);\n\n        // Verify repository was not called (exception prevented save)\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n\n        // Verify push notification wasn't sent\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Theory]\n    [InlineData(true)]  // New Send (Id is default)\n    [InlineData(false)] // Existing Send (Id is not default)\n    public async Task SaveSendAsync_DisableHideEmail_Applies_ButEmailNotHidden_Success_vNext(bool isNewSend)\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = isNewSend ? default : Guid.NewGuid(),\n            Type = SendType.Text,\n            UserId = userId,\n            HideEmail = false  // Email is not hidden, so policy doesn't block\n        };\n\n        var initialDate = DateTime.UtcNow.AddMinutes(-5);\n        send.RevisionDate = initialDate;\n\n        // Enable feature flag for policy requirements (vNext path)\n        _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);\n\n        // Configure validation service to allow saves when HideEmail is false\n        _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask);\n\n        // Set up context for reference event\n        _currentContext.ClientId.Returns(\"test-client\");\n        _currentContext.ClientVersion.Returns(Version.Parse(\"1.0.0\"));\n\n        // Act\n        await _nonAnonymousSendCommand.SaveSendAsync(send);\n\n        // Assert\n        // Verify validation was called with vNext path\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send);\n\n        if (isNewSend)\n        {\n            // For new Sends\n            await _sendRepository.Received(1).CreateAsync(send);\n            await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send);\n        }\n        else\n        {\n            // For existing Sends\n            await _sendRepository.Received(1).UpsertAsync(send);\n            Assert.NotEqual(initialDate, send.RevisionDate);\n            await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);\n        }\n    }\n\n    [Fact]\n    public async Task SaveSendAsync_ExistingSend_Updates()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var sendId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = sendId,\n            Type = SendType.Text,\n            UserId = userId,\n            Data = \"Some text data\"\n        };\n\n        var initialDate = DateTime.UtcNow.AddMinutes(-5);\n        send.RevisionDate = initialDate;\n\n        // Act\n        await _nonAnonymousSendCommand.SaveSendAsync(send);\n\n        // Assert\n        // Verify validation was called\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send);\n\n        // Verify repository was called with updated send\n        await _sendRepository.Received(1).UpsertAsync(send);\n\n        // Check that the revision date was updated\n        Assert.NotEqual(initialDate, send.RevisionDate);\n\n        // Verify push notification was sent for the update\n        await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_TextType_ThrowsBadRequest()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.Text, // Text type instead of File\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 1024L; // 1KB\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"not of type \\\"file\\\"\", exception.Message);\n\n        // Verify no further methods were called\n        await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_EmptyFile_ThrowsBadRequest()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 0L; // Empty file\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"No file data\", exception.Message);\n\n        // Verify no methods were called after validation failed\n        await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_UserCannotAccessPremium_ThrowsBadRequest()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 1024L; // 1KB\n\n        // Configure validation service to throw when checking storage\n        _sendValidationService.StorageRemainingForSendAsync(send)\n            .Throws(new BadRequestException(\"You must have premium status to use file Sends.\"));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"premium status\", exception.Message);\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify no further methods were called\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_UserHasUnconfirmedEmail_ThrowsBadRequest()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 1024L; // 1KB\n\n        // Configure validation service to pass storage check\n        _sendValidationService.StorageRemainingForSendAsync(send).Returns(10240L); // 10KB remaining\n\n        // Configure validation service to throw when checking user can save\n        _sendValidationService.When(x => x.ValidateUserCanSaveAsync(userId, send))\n            .Throw(new BadRequestException(\"You must confirm your email before creating a Send.\"));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"confirm your email\", exception.Message);\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify SaveSendAsync attempted to be called, triggering email validation\n        await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send);\n\n        // Verify no repository or notification methods were called after validation failed\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_UserCanAccessPremium_HasNoStorage_ThrowsBadRequest()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 1024L; // 1KB\n\n        // Configure validation service to return 0 storage remaining\n        _sendValidationService.StorageRemainingForSendAsync(send).Returns(0L);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"Not enough storage available\", exception.Message);\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify no further methods were called\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_UserCanAccessPremium_StorageFull_ThrowsBadRequest()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 1024L; // 1KB\n\n        // Configure validation service to return less storage remaining than needed\n        _sendValidationService.StorageRemainingForSendAsync(send).Returns(512L); // Only 512 bytes available\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"Not enough storage available\", exception.Message);\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify no further methods were called\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsSelfHosted_GiantFile_ThrowsBadRequest()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 15L * 1024L * 1024L; // 15 MB\n\n        // Configure validation service to return insufficient storage\n        _sendValidationService.StorageRemainingForSendAsync(send)\n            .Returns(10L * 1024L * 1024L); // 10 MB remaining\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"Not enough storage available\", exception.Message);\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify no further methods were called\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsNotSelfHosted_TwoGigabyteFile_ThrowsBadRequest()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 2L * 1024L * 1024L * 1024L; // 2MB\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"Max file size is \", exception.Message);\n\n        // Verify no further methods were called\n        await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsNotSelfHosted_NotEnoughSpace_ThrowsBadRequest()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 2L * 1024L * 1024L; // 2MB\n\n        // Configure validation service to return 1 MB storage remaining\n        _sendValidationService.StorageRemainingForSendAsync(send)\n            .Returns(1L * 1024L * 1024L);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"Not enough storage available\", exception.Message);\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify no further methods were called\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_ThrowsBadRequest()\n    {\n        // Arrange\n        var organizationId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            OrganizationId = organizationId\n        };\n\n        var fileData = new SendFileData\n        {\n            FileName = \"test.txt\"\n        };\n\n        const long fileLength = 1000;\n\n        // Set up validation service to return 0 storage remaining\n        // This simulates the case when an organization's max storage is null\n        _sendValidationService.StorageRemainingForSendAsync(send).Returns(0L);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Equal(\"Not enough storage available.\", exception.Message);\n\n        // Verify the method was called exactly once\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_TwoGBFile_ThrowsBadRequest()\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            OrganizationId = orgId,\n            UserId = null\n        };\n        var fileData = new SendFileData();\n        var fileLength = 2L * 1024L * 1024L; // 2 MB\n\n        // Configure validation service to throw BadRequest when checking storage for org without storage\n        _sendValidationService.StorageRemainingForSendAsync(send)\n            .Throws(new BadRequestException(\"This organization cannot use file sends.\"));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"This organization cannot use file sends\", exception.Message);\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify no further methods were called\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsOneGB_TwoGBFile_ThrowsBadRequest()\n    {\n        // Arrange\n        var orgId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            OrganizationId = orgId,\n            UserId = null\n        };\n        var fileData = new SendFileData();\n        var fileLength = 2L * 1024L * 1024L; // 2 MB\n\n        _sendValidationService.StorageRemainingForSendAsync(send)\n            .Returns(1L * 1024L * 1024L); // 1 MB remaining\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        Assert.Contains(\"Not enough storage available\", exception.Message);\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify no further methods were called\n        await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());\n        await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_HasEnoughStorage_Success()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 500L * 1024L; // 500KB\n        var expectedFileId = \"generatedfileid\";\n        var expectedUploadUrl = \"https://upload.example.com/url\";\n\n        // Configure storage validation to return more storage than needed\n        _sendValidationService.StorageRemainingForSendAsync(send)\n            .Returns(1024L * 1024L); // 1MB remaining\n\n        // Configure file storage service to return upload URL\n        _sendFileStorageService.GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>())\n            .Returns(expectedUploadUrl);\n\n        // Set up string generator to return predictable file ID\n        _sendCoreHelperService.SecureRandomString(32, false, false)\n            .Returns(expectedFileId);\n\n        // Act\n        var result = await _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength);\n\n        // Assert\n        Assert.Equal(expectedUploadUrl, result);\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify upload URL was requested\n        await _sendFileStorageService.Received(1).GetSendFileUploadUrlAsync(send, expectedFileId);\n    }\n\n    [Fact]\n    public async Task SaveFileSendAsync_HasEnoughStorage_SendFileThrows_CleansUp()\n    {\n        // Arrange\n        var userId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = userId\n        };\n        var fileData = new SendFileData();\n        var fileLength = 500L * 1024L; // 500KB\n        var expectedFileId = \"generatedfileid\";\n\n        // Configure storage validation to return more storage than needed\n        _sendValidationService.StorageRemainingForSendAsync(send)\n            .Returns(1024L * 1024L); // 1MB remaining\n\n        // Set up string generator to return predictable file ID\n        _sendCoreHelperService.SecureRandomString(32, false, false)\n            .Returns(expectedFileId);\n\n        // Configure file storage service to throw exception when getting upload URL\n        _sendFileStorageService.GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>())\n            .Throws(new Exception(\"Storage service unavailable\"));\n\n        // Act & Assert\n        await Assert.ThrowsAsync<Exception>(() =>\n            _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength));\n\n        // Verify storage validation was called\n        await _sendValidationService.Received(1).StorageRemainingForSendAsync(send);\n\n        // Verify file was cleaned up after failure\n        await _sendFileStorageService.Received(1).DeleteFileAsync(send, expectedFileId);\n    }\n\n    [Fact]\n    public async Task UpdateFileToExistingSendAsync_SendNull_ThrowsBadRequest()\n    {\n        // Arrange\n        Stream stream = new MemoryStream();\n        Send send = null;\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send));\n\n        Assert.Equal(\"Send does not have file data\", exception.Message);\n\n        // Verify no interactions with storage service\n        await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync(\n            Arg.Any<Stream>(), Arg.Any<Send>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task UpdateFileToExistingSendAsync_SendDataNull_ThrowsBadRequest()\n    {\n        // Arrange\n        Stream stream = new MemoryStream();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = Guid.NewGuid(),\n            Data = null // Send exists but has null Data property\n        };\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send));\n\n        Assert.Equal(\"Send does not have file data\", exception.Message);\n\n        // Verify no interactions with storage service\n        await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync(\n            Arg.Any<Stream>(), Arg.Any<Send>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task UpdateFileToExistingSendAsync_NotFileType_ThrowsBadRequest()\n    {\n        // Arrange\n        Stream stream = new MemoryStream();\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.Text, // Not a file type\n            UserId = Guid.NewGuid(),\n            Data = \"{\\\"someData\\\":\\\"value\\\"}\" // Has data, but not file data\n        };\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send));\n\n        Assert.Equal(\"Not a File Type Send.\", exception.Message);\n\n        // Verify no interactions with storage service\n        await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync(\n            Arg.Any<Stream>(), Arg.Any<Send>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task UpdateFileToExistingSendAsync_StreamPositionRestToZero_Success()\n    {\n        // Arrange\n        var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 });\n        stream.Position = 2;\n        var sendId = Guid.NewGuid();\n        var userId = Guid.NewGuid();\n        var fileId = \"existingfileid123\";\n\n        var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false };\n        var send = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.File,\n            Data = JsonSerializer.Serialize(sendFileData)\n        };\n\n        // Setup validation to succeed\n        _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, Arg.Any<long>(), Arg.Any<long>()).Returns((true, sendFileData.Size));\n\n        // Act\n        await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send);\n\n        // Assert\n        // Verify file was uploaded with correct parameters\n        await _sendFileStorageService.Received(1).UploadNewFileAsync(\n            Arg.Is<Stream>(s => s == stream && s.Position == 0), // Ensure stream position is reset\n            Arg.Is<Send>(s => s.Id == sendId && s.UserId == userId),\n            Arg.Is<string>(id => id == fileId)\n        );\n    }\n\n\n    [Fact]\n    public async Task UploadFileToExistingSendAsync_Success()\n    {\n        // Arrange\n        var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 });\n        stream.Position = 2; // Simulate a non-zero position\n        var sendId = Guid.NewGuid();\n        var userId = Guid.NewGuid();\n        var fileId = \"existingfileid123\";\n\n        var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false };\n        var send = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.File,\n            Data = JsonSerializer.Serialize(sendFileData)\n        };\n\n        _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, Arg.Any<long>(), Arg.Any<long>()).Returns((true, sendFileData.Size));\n\n        // Act\n        await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send);\n\n        // Assert\n        // Verify file was uploaded with correct parameters\n        await _sendFileStorageService.Received(1).UploadNewFileAsync(\n            Arg.Is<Stream>(s => s == stream && s.Position == 0), // Ensure stream position is reset\n            Arg.Is<Send>(s => s.Id == sendId && s.UserId == userId),\n            Arg.Is<string>(id => id == fileId)\n        );\n    }\n\n    [Fact]\n    public async Task UpdateFileToExistingSendAsync_InvalidSize_ThrowsBadRequest()\n    {\n        // Arrange\n        var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 });\n        var sendId = Guid.NewGuid();\n        var userId = Guid.NewGuid();\n        var fileId = \"existingfileid123\";\n\n        var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false };\n        var send = new Send\n        {\n            Id = sendId,\n            UserId = userId,\n            Type = SendType.File,\n            Data = JsonSerializer.Serialize(sendFileData)\n        };\n\n        // Configure storage service to upload successfully\n        _sendFileStorageService.UploadNewFileAsync(\n                Arg.Any<Stream>(), Arg.Any<Send>(), Arg.Any<string>())\n            .Returns(Task.CompletedTask);\n\n        // Configure validation to fail due to file size mismatch\n        _nonAnonymousSendCommand.ConfirmFileSize(send)\n            .Returns(false);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send));\n\n        Assert.Equal(\"File received does not match expected file length.\", exception.Message);\n    }\n\n    [Fact]\n    public async Task GetSendFileDownloadUrlAsync_WithTextSend_ThrowsBadRequest()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.Text,\n            UserId = Guid.NewGuid()\n        };\n        var fileId = \"somefile123\";\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() =>\n            _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId));\n\n        Assert.Equal(\"Can only get a download URL for a file type of Send\", exception.Message);\n\n        // Verify no storage service methods were called\n        await _sendFileStorageService.DidNotReceive()\n            .GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task GetSendFileDownloadUrlAsync_WithDisabledSend_ReturnsDenied()\n    {\n        // Arrange\n        var fileId = \"file123\";\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = Guid.NewGuid(),\n            Disabled = true,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n\n        // Act\n        var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);\n\n        // Assert\n        Assert.Null(url);\n        Assert.Equal(SendAccessResult.Denied, result);\n\n        // Verify no repository updates occurred\n        await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive()\n            .GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task GetSendFileDownloadUrlAsync_WithMaxAccessCountReached_ReturnsDenied()\n    {\n        // Arrange\n        var fileId = \"file123\";\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = Guid.NewGuid(),\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            AccessCount = 5,\n            MaxAccessCount = 5\n        };\n\n        // Act\n        var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);\n\n        // Assert\n        Assert.Null(url);\n        Assert.Equal(SendAccessResult.Denied, result);\n\n        // Verify no repository updates occurred\n        await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive()\n            .GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task GetSendFileDownloadUrlAsync_WithExpiredSend_ReturnsDenied()\n    {\n        // Arrange\n        var fileId = \"file123\";\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = Guid.NewGuid(),\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired yesterday\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n\n        // Act\n        var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);\n\n        // Assert\n        Assert.Null(url);\n        Assert.Equal(SendAccessResult.Denied, result);\n\n        // Verify no repository updates occurred\n        await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive()\n            .GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task GetSendFileDownloadUrlAsync_WithDeletionDatePassed_ReturnsDenied()\n    {\n        // Arrange\n        var fileId = \"file123\";\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = Guid.NewGuid(),\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(-1), // Deletion date has passed\n            ExpirationDate = null,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n\n        // Act\n        var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);\n\n        // Assert\n        Assert.Null(url);\n        Assert.Equal(SendAccessResult.Denied, result);\n\n        // Verify no repository updates occurred\n        await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());\n        await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());\n        await _sendFileStorageService.DidNotReceive()\n            .GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());\n    }\n\n    [Fact]\n    public async Task GetSendFileDownloadUrlAsync_WithValidSend_ReturnsUrlAndIncrementsAccessCount()\n    {\n        // Arrange\n        var fileId = \"file123\";\n        var expectedUrl = \"https://download.example.com/file123\";\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            Type = SendType.File,\n            UserId = Guid.NewGuid(),\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            AccessCount = 3,\n            MaxAccessCount = 10\n        };\n\n        _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl);\n\n        // Act\n        var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);\n\n        // Assert\n        Assert.Equal(expectedUrl, url);\n        Assert.Equal(SendAccessResult.Granted, result);\n\n        // Verify access count was incremented\n        Assert.Equal(4, send.AccessCount);\n\n        // Verify repository was updated\n        await _sendRepository.Received(1).ReplaceAsync(send);\n        await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);\n\n        // Verify file storage service was called\n        await _sendFileStorageService.Received(1).GetSendFileDownloadUrlAsync(send, fileId);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_WithDisabledSend_ReturnsFalse()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Disabled = true,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n\n        // Act\n        var result = INonAnonymousSendCommand.SendCanBeAccessed(send);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_WithMaxAccessCountReached_ReturnsFalse()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            AccessCount = 10,\n            MaxAccessCount = 10\n        };\n\n        // Act\n        var result = INonAnonymousSendCommand.SendCanBeAccessed(send);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_WithExpiredSend_ReturnsFalse()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = DateTime.UtcNow.AddDays(-1),\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n\n        // Act\n        var result = INonAnonymousSendCommand.SendCanBeAccessed(send);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_WithDeletionDatePassed_ReturnsFalse()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(-1),\n            ExpirationDate = null,\n            AccessCount = 0,\n            MaxAccessCount = null\n        };\n\n        // Act\n        var result = INonAnonymousSendCommand.SendCanBeAccessed(send);\n\n        // Assert\n        Assert.False(result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_WithValidSend_ReturnsTrue()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = DateTime.UtcNow.AddDays(7),\n            AccessCount = 5,\n            MaxAccessCount = 10\n        };\n\n        // Act\n        var result = INonAnonymousSendCommand.SendCanBeAccessed(send);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_WithNullMaxAccessCount_ReturnsTrue()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            AccessCount = 100,\n            MaxAccessCount = null\n        };\n\n        // Act\n        var result = INonAnonymousSendCommand.SendCanBeAccessed(send);\n\n        // Assert\n        Assert.True(result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_WithNullExpirationDate_ReturnsTrue()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null,\n            AccessCount = 0,\n            MaxAccessCount = 10\n        };\n\n        // Act\n        var result = INonAnonymousSendCommand.SendCanBeAccessed(send);\n\n        // Assert\n        Assert.True(result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs",
    "content": "﻿using Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Queries;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tools.Services;\n\npublic class SendAuthenticationQueryTests\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly SendAuthenticationQuery _sendAuthenticationQuery;\n\n    public SendAuthenticationQueryTests()\n    {\n        _sendRepository = Substitute.For<ISendRepository>();\n        _sendAuthenticationQuery = new SendAuthenticationQuery(_sendRepository);\n    }\n\n    [Fact]\n    public void Constructor_WithNullRepository_ThrowsArgumentNullException()\n    {\n        // Act & Assert\n        var exception = Assert.Throws<ArgumentNullException>(() => new SendAuthenticationQuery(null));\n        Assert.Equal(\"sendRepository\", exception.ParamName);\n    }\n\n    [Theory]\n    [MemberData(nameof(AuthenticationMethodTestCases))]\n    public async Task GetAuthenticationMethod_ReturnsExpectedAuthenticationMethod(Send? send, Type expectedType)\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        Assert.IsType(expectedType, result);\n    }\n\n    [Theory]\n    [MemberData(nameof(EmailsParsingTestCases))]\n    public async Task GetAuthenticationMethod_WithEmails_ParsesEmailsCorrectly(string emailString, string[] expectedEmails)\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null, AuthType.Email);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        var emailOtp = Assert.IsType<EmailOtp>(result);\n        Assert.Equal(expectedEmails, emailOtp.emails);\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_WithBothEmailsAndPassword_ReturnsEmailOtp()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: \"person@company.com\", password: \"hashedpassword\", AuthType.Email);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        Assert.IsType<EmailOtp>(result);\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_CallsRepositoryWithCorrectSendId()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null, AuthType.None);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_WhenRepositoryThrows_PropagatesException()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var expectedException = new InvalidOperationException(\"Repository error\");\n        _sendRepository.GetByIdAsync(sendId).Returns(Task.FromException<Send?>(expectedException));\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>\n            _sendAuthenticationQuery.GetAuthenticationMethod(sendId));\n        Assert.Same(expectedException, exception);\n    }\n\n    public static IEnumerable<object[]> AuthenticationMethodTestCases()\n    {\n        yield return new object[] { null, typeof(NeverAuthenticate) };\n        yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null, AuthType.None), typeof(SendInaccessible) };\n        yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null, AuthType.None), typeof(SendInaccessible) };\n        yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: \"person@company.com\", password: null, AuthType.Email), typeof(EmailOtp) };\n        yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: \"hashedpassword\", AuthType.Password), typeof(ResourcePassword) };\n        yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null, AuthType.None), typeof(NotAuthenticated) };\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_WithDisabledSend_ReturnsSendInaccessible()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = sendId,\n            AccessCount = 0,\n            MaxAccessCount = 10,\n            Emails = \"person@company.com\",\n            Password = null,\n            AuthType = AuthType.Email,\n            Disabled = true,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        Assert.IsType<SendInaccessible>(result);\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_WithExpiredSend_ReturnsSendInaccessible()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = sendId,\n            AccessCount = 0,\n            MaxAccessCount = 10,\n            Emails = \"person@company.com\",\n            Password = null,\n            AuthType = AuthType.Email,\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = DateTime.UtcNow.AddDays(-1) // Expired yesterday\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        Assert.IsType<SendInaccessible>(result);\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_WithDeletionDatePassed_ReturnsSendInaccessible()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = sendId,\n            AccessCount = 0,\n            MaxAccessCount = 10,\n            Emails = \"person@company.com\",\n            Password = null,\n            AuthType = AuthType.Email,\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(-1), // Should have been deleted yesterday\n            ExpirationDate = null\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        Assert.IsType<SendInaccessible>(result);\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_WithDeletionDateEqualToNow_ReturnsSendInaccessible()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var now = DateTime.UtcNow;\n        var send = new Send\n        {\n            Id = sendId,\n            AccessCount = 0,\n            MaxAccessCount = 10,\n            Emails = \"person@company.com\",\n            Password = null,\n            AuthType = AuthType.Email,\n            Disabled = false,\n            DeletionDate = now, // DeletionDate <= DateTime.UtcNow\n            ExpirationDate = null\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        Assert.IsType<SendInaccessible>(result);\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_WithAccessCountEqualToMaxAccessCount_ReturnsSendInaccessible()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = sendId,\n            AccessCount = 5,\n            MaxAccessCount = 5,\n            Emails = \"person@company.com\",\n            Password = null,\n            AuthType = AuthType.Email,\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        Assert.IsType<SendInaccessible>(result);\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_WithNullMaxAccessCount_DoesNotRestrictAccess()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = sendId,\n            AccessCount = 1000,\n            MaxAccessCount = null, // No limit\n            Emails = \"person@company.com\",\n            Password = null,\n            AuthType = AuthType.Email,\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        Assert.IsType<EmailOtp>(result);\n    }\n\n    [Fact]\n    public async Task GetAuthenticationMethod_WithNullExpirationDate_DoesNotExpire()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = new Send\n        {\n            Id = sendId,\n            AccessCount = 0,\n            MaxAccessCount = 10,\n            Emails = \"person@company.com\",\n            Password = null,\n            AuthType = AuthType.Email,\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null // No expiration\n        };\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act\n        var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);\n\n        // Assert\n        Assert.IsType<EmailOtp>(result);\n    }\n\n    public static IEnumerable<object[]> EmailsParsingTestCases()\n    {\n        yield return new object[] { \"person@company.com\", new[] { \"person@company.com\" } };\n        yield return new object[] { \"person1@company.com,person2@company.com\", new[] { \"person1@company.com\", \"person2@company.com\" } };\n        yield return new object[] { \" person1@company.com , person2@company.com \", new[] { \"person1@company.com\", \"person2@company.com\" } };\n        yield return new object[] { \"person1@company.com,,person2@company.com\", new[] { \"person1@company.com\", \"person2@company.com\" } };\n        yield return new object[] { \" , person1@company.com,  ,person2@company.com, \", new[] { \"person1@company.com\", \"person2@company.com\" } };\n    }\n\n    private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password, AuthType? authType)\n    {\n        return new Send\n        {\n            Id = Guid.NewGuid(),\n            AccessCount = accessCount,\n            MaxAccessCount = maxAccessCount,\n            Emails = emails,\n            Password = password,\n            AuthType = authType,\n            Disabled = false,\n            DeletionDate = DateTime.UtcNow.AddDays(7),\n            ExpirationDate = null\n        };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs",
    "content": "﻿using Bit.Core.Platform.Push;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Models.Data;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.Services;\nusing Microsoft.AspNetCore.Identity;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tools.Services;\n\npublic class SendAuthorizationServiceTests\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly IPasswordHasher<Bit.Core.Entities.User> _passwordHasher;\n    private readonly IPushNotificationService _pushNotificationService;\n    private readonly SendAuthorizationService _sendAuthorizationService;\n\n    public SendAuthorizationServiceTests()\n    {\n        _sendRepository = Substitute.For<ISendRepository>();\n        _passwordHasher = Substitute.For<IPasswordHasher<Bit.Core.Entities.User>>();\n        _pushNotificationService = Substitute.For<IPushNotificationService>();\n\n        _sendAuthorizationService = new SendAuthorizationService(\n            _sendRepository,\n            _passwordHasher,\n            _pushNotificationService);\n    }\n\n\n    [Fact]\n    public void SendCanBeAccessed_Success_ReturnsTrue()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            MaxAccessCount = 10,\n            AccessCount = 5,\n            ExpirationDate = DateTime.UtcNow.AddYears(1),\n            DeletionDate = DateTime.UtcNow.AddYears(1),\n            Disabled = false,\n            Password = \"hashedPassword123\"\n        };\n\n        const string password = \"TEST\";\n\n        _passwordHasher\n            .VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), send.Password, password)\n            .Returns(PasswordVerificationResult.Success);\n\n        // Act\n        var result =\n            _sendAuthorizationService.SendCanBeAccessed(send, password);\n\n        // Assert\n        Assert.Equal(SendAccessResult.Granted, result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_NullMaxAccess_Success()\n    {\n        // Arrange\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            MaxAccessCount = null,\n            AccessCount = 5,\n            ExpirationDate = DateTime.UtcNow.AddYears(1),\n            DeletionDate = DateTime.UtcNow.AddYears(1),\n            Disabled = false,\n            Password = \"hashedPassword123\"\n        };\n\n        const string password = \"TEST\";\n\n        _passwordHasher\n            .VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), send.Password, password)\n            .Returns(PasswordVerificationResult.Success);\n\n        // Act\n        var result = _sendAuthorizationService.SendCanBeAccessed(send, password);\n\n        // Assert\n        Assert.Equal(SendAccessResult.Granted, result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_NullSend_DoesNotGrantAccess()\n    {\n        // Arrange\n        _passwordHasher\n            .VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), \"TEST\", \"TEST\")\n            .Returns(PasswordVerificationResult.Success);\n\n        // Act\n        var result =\n            _sendAuthorizationService.SendCanBeAccessed(null, \"TEST\");\n\n        // Assert\n        Assert.Equal(SendAccessResult.Denied, result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_RehashNeeded_RehashesPassword()\n    {\n        // Arrange\n        var now = DateTime.UtcNow;\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            MaxAccessCount = null,\n            AccessCount = 5,\n            ExpirationDate = now.AddYears(1),\n            DeletionDate = now.AddYears(1),\n            Disabled = false,\n            Password = \"TEST\"\n        };\n\n        _passwordHasher\n            .VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), \"TEST\", \"TEST\")\n            .Returns(PasswordVerificationResult.SuccessRehashNeeded);\n\n        // Act\n        var result =\n            _sendAuthorizationService.SendCanBeAccessed(send, \"TEST\");\n\n        // Assert\n        _passwordHasher\n            .Received(1)\n            .HashPassword(Arg.Any<Bit.Core.Entities.User>(), \"TEST\");\n\n        Assert.Equal(SendAccessResult.Granted, result);\n    }\n\n    [Fact]\n    public void SendCanBeAccessed_VerifyFailed_PasswordInvalidReturnsTrue()\n    {\n        // Arrange\n        var now = DateTime.UtcNow;\n        var send = new Send\n        {\n            Id = Guid.NewGuid(),\n            UserId = Guid.NewGuid(),\n            MaxAccessCount = null,\n            AccessCount = 5,\n            ExpirationDate = now.AddYears(1),\n            DeletionDate = now.AddYears(1),\n            Disabled = false,\n            Password = \"TEST\"\n        };\n\n        _passwordHasher\n            .VerifyHashedPassword(Arg.Any<Bit.Core.Entities.User>(), \"TEST\", \"TEST\")\n            .Returns(PasswordVerificationResult.Failed);\n\n        // Act\n        var result =\n            _sendAuthorizationService.SendCanBeAccessed(send, \"TEST\");\n\n        // Assert\n        Assert.Equal(SendAccessResult.PasswordInvalid, result);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Tools/Services/SendOwnerQueryTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Repositories;\nusing Bit.Core.Tools.SendFeatures.Queries;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tools.Services;\n\npublic class SendOwnerQueryTests\n{\n    private readonly ISendRepository _sendRepository;\n    private readonly IUserService _userService;\n    private readonly SendOwnerQuery _sendOwnerQuery;\n    private readonly Guid _currentUserId = Guid.NewGuid();\n    private readonly ClaimsPrincipal _user;\n\n    public SendOwnerQueryTests()\n    {\n        _sendRepository = Substitute.For<ISendRepository>();\n        _userService = Substitute.For<IUserService>();\n        _user = new ClaimsPrincipal();\n        _userService.GetProperUserId(_user).Returns(_currentUserId);\n        _sendOwnerQuery = new SendOwnerQuery(_sendRepository, _userService);\n    }\n\n    [Fact]\n    public async Task Get_WithValidSendOwnedByUser_ReturnsExpectedSend()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var expectedSend = CreateSend(sendId, _currentUserId);\n        _sendRepository.GetByIdAsync(sendId).Returns(expectedSend);\n\n        // Act\n        var result = await _sendOwnerQuery.Get(sendId, _user);\n\n        // Assert\n        Assert.Same(expectedSend, result);\n        await _sendRepository.Received(1).GetByIdAsync(sendId);\n    }\n\n    [Fact]\n    public async Task Get_WithNonExistentSend_ThrowsNotFoundException()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        _sendRepository.GetByIdAsync(sendId).Returns((Send?)null);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => _sendOwnerQuery.Get(sendId, _user));\n    }\n\n    [Fact]\n    public async Task Get_WithSendOwnedByDifferentUser_ThrowsNotFoundException()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var differentUserId = Guid.NewGuid();\n        var send = CreateSend(sendId, differentUserId);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n\n        // Act & Assert\n        await Assert.ThrowsAsync<NotFoundException>(() => _sendOwnerQuery.Get(sendId, _user));\n    }\n\n    [Fact]\n    public async Task Get_WithNullCurrentUserId_ThrowsBadRequestException()\n    {\n        // Arrange\n        var sendId = Guid.NewGuid();\n        var send = CreateSend(sendId, _currentUserId);\n        _sendRepository.GetByIdAsync(sendId).Returns(send);\n        var nullUser = new ClaimsPrincipal();\n        _userService.GetProperUserId(nullUser).Returns((Guid?)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sendOwnerQuery.Get(sendId, nullUser));\n        Assert.Equal(\"invalid user.\", exception.Message);\n    }\n\n    [Fact]\n    public async Task GetOwned_ReturnsAllSendsIncludingEmailOTP()\n    {\n        // Arrange\n        var sends = new List<Send>\n        {\n            CreateSend(Guid.NewGuid(), _currentUserId, emails: null),\n            CreateSend(Guid.NewGuid(), _currentUserId, emails: \"test@example.com\"),\n            CreateSend(Guid.NewGuid(), _currentUserId, emails: \"other@example.com\")\n        };\n        _sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(sends);\n\n        // Act\n        var result = await _sendOwnerQuery.GetOwned(_user);\n\n        // Assert\n        Assert.Equal(3, result.Count);\n        Assert.Contains(sends[0], result);\n        Assert.Contains(sends[1], result);\n        Assert.Contains(sends[2], result);\n        await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId);\n    }\n\n    [Fact]\n    public async Task GetOwned_WithNullCurrentUserId_ThrowsBadRequestException()\n    {\n        // Arrange\n        var nullUser = new ClaimsPrincipal();\n        _userService.GetProperUserId(nullUser).Returns((Guid?)null);\n\n        // Act & Assert\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sendOwnerQuery.GetOwned(nullUser));\n        Assert.Equal(\"invalid user.\", exception.Message);\n    }\n\n    [Fact]\n    public async Task GetOwned_WithEmptyCollection_ReturnsEmptyCollection()\n    {\n        // Arrange\n        var emptySends = new List<Send>();\n        _sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(emptySends);\n\n        // Act\n        var result = await _sendOwnerQuery.GetOwned(_user);\n\n        // Assert\n        Assert.Empty(result);\n        await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId);\n    }\n\n    private static Send CreateSend(Guid id, Guid userId, string? emails = null)\n    {\n        return new Send\n        {\n            Id = id,\n            UserId = userId,\n            Emails = emails\n        };\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Tools/Services/SendValidationServiceTests.cs",
    "content": "﻿using Bit.Core.AdminConsole.Entities;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies;\nusing Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;\nusing Bit.Core.Billing.Pricing;\nusing Bit.Core.Billing.Pricing.Premium;\nusing Bit.Core.Entities;\nusing Bit.Core.Exceptions;\nusing Bit.Core.Repositories;\nusing Bit.Core.Services;\nusing Bit.Core.Tools.Entities;\nusing Bit.Core.Tools.Enums;\nusing Bit.Core.Tools.Services;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing NSubstitute;\nusing Xunit;\n\nnamespace Bit.Core.Test.Tools.Services;\n\n[SutProviderCustomize]\npublic class SendValidationServiceTests\n{\n    [Theory, BitAutoData]\n    public async Task StorageRemainingForSendAsync_OrgGrantedPremiumUser_UsesPricingService(\n        SutProvider<SendValidationService> sutProvider,\n        Send send,\n        User user)\n    {\n        // Arrange\n        send.UserId = user.Id;\n        send.OrganizationId = null;\n        send.Type = SendType.File;\n        user.Premium = false;\n        user.Storage = 1024L * 1024L * 1024L; // 1 GB used\n        user.EmailVerified = true;\n\n        sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = false;\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);\n\n        var premiumPlan = new Plan\n        {\n            Storage = new Purchasable { Provided = 5 }\n        };\n        sutProvider.GetDependency<IPricingClient>().GetAvailablePremiumPlan().Returns(premiumPlan);\n\n        // Act\n        var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);\n\n        // Assert\n        await sutProvider.GetDependency<IPricingClient>().Received(1).GetAvailablePremiumPlan();\n        Assert.True(result > 0);\n    }\n\n    [Theory, BitAutoData]\n    public async Task StorageRemainingForSendAsync_IndividualPremium_DoesNotCallPricingService(\n        SutProvider<SendValidationService> sutProvider,\n        Send send,\n        User user)\n    {\n        // Arrange\n        send.UserId = user.Id;\n        send.OrganizationId = null;\n        send.Type = SendType.File;\n        user.Premium = true;\n        user.MaxStorageGb = 10;\n        user.EmailVerified = true;\n\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);\n\n        // Assert - should NOT call pricing service for individual premium users\n        await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();\n    }\n\n    [Theory, BitAutoData]\n    public async Task StorageRemainingForSendAsync_SelfHosted_DoesNotCallPricingService(\n        SutProvider<SendValidationService> sutProvider,\n        Send send,\n        User user)\n    {\n        // Arrange\n        send.UserId = user.Id;\n        send.OrganizationId = null;\n        send.Type = SendType.File;\n        user.Premium = false;\n        user.EmailVerified = true;\n\n        sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = true;\n        sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);\n        sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);\n\n        // Act\n        var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);\n\n        // Assert - should NOT call pricing service for self-hosted\n        await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();\n    }\n\n    [Theory, BitAutoData]\n    public async Task StorageRemainingForSendAsync_OrgSend_DoesNotCallPricingService(\n        SutProvider<SendValidationService> sutProvider,\n        Send send,\n        Organization org)\n    {\n        // Arrange\n        send.UserId = null;\n        send.OrganizationId = org.Id;\n        send.Type = SendType.File;\n        org.MaxStorageGb = 100;\n\n        sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);\n\n        // Act\n        var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);\n\n        // Assert - should NOT call pricing service for org sends\n        await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateUserCanSaveAsync_WhenDisableSendPolicyEnforced_CannotCreateSend(\n        SutProvider<SendValidationService> sutProvider, Send send, Guid userId)\n    {\n        sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<DisableSendPolicyRequirement>(userId)\n            .Returns(new DisableSendPolicyRequirement { DisableSend = true });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send));\n        Assert.Contains(\"you are only able to delete an existing Send\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateUserCanSaveAsync_WhenSendOptionsPolicyProhibitsHidingEmail_CannotHideEmail(\n        SutProvider<SendValidationService> sutProvider, Send send, Guid userId)\n    {\n        send.HideEmail = true;\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<DisableSendPolicyRequirement>(userId)\n            .Returns(new DisableSendPolicyRequirement { DisableSend = false });\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<SendOptionsPolicyRequirement>(userId)\n            .Returns(new SendOptionsPolicyRequirement { DisableHideEmail = true });\n\n        var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.ValidateUserCanSaveAsync(userId, send));\n        Assert.Contains(\"you are not allowed to hide your email address\", exception.Message);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateUserCanSaveAsync_WhenSendOptionsPolicyProhibitsHidingEmail_CanShowEmail(\n        SutProvider<SendValidationService> sutProvider, Send send, Guid userId)\n    {\n        send.HideEmail = false;\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<DisableSendPolicyRequirement>(userId)\n            .Returns(new DisableSendPolicyRequirement { DisableSend = false });\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<SendOptionsPolicyRequirement>(userId)\n            .Returns(new SendOptionsPolicyRequirement { DisableHideEmail = true });\n\n        // No exception implies success\n        await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send);\n    }\n\n    [Theory, BitAutoData]\n    public async Task ValidateUserCanSaveAsync_WhenPoliciesDoNotApply_Success(\n        SutProvider<SendValidationService> sutProvider, Send send, Guid userId)\n    {\n        send.HideEmail = true;\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<DisableSendPolicyRequirement>(userId)\n            .Returns(new DisableSendPolicyRequirement { DisableSend = false });\n\n        sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<SendOptionsPolicyRequirement>(userId)\n            .Returns(new SendOptionsPolicyRequirement { DisableHideEmail = false });\n\n        // No exception implies success\n        await sutProvider.Sut.ValidateUserCanSaveAsync(userId, send);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Utilities/AssemblyHelpersTests.cs",
    "content": "﻿using Bit.Core.Utilities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Utilities;\n\npublic class AssemblyHelpersTests\n{\n    [Fact]\n    public void ReturnsValidVersionAndGitHash()\n    {\n        var version = AssemblyHelpers.GetVersion();\n        _ = Version.Parse(version);\n\n        var gitHash = AssemblyHelpers.GetGitHash();\n        Assert.NotNull(gitHash);\n        Assert.Equal(8, gitHash.Length);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Utilities/AuthorizationServiceExtensionTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Exceptions;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Authorization.Infrastructure;\nusing NSubstitute;\nusing Xunit;\nusing AuthorizationServiceExtensions = Bit.Core.Utilities.AuthorizationServiceExtensions;\n\nnamespace Bit.Core.Test.Utilities;\n\npublic class AuthorizationServiceExtensionTests\n{\n    [Fact]\n    async Task AuthorizeOrThrowAsync_ThrowsNotFoundException_IfResourceIsNull()\n    {\n        var authorizationService = Substitute.For<IAuthorizationService>();\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            AuthorizationServiceExtensions.AuthorizeOrThrowAsync(authorizationService, new ClaimsPrincipal(),\n                null, new OperationAuthorizationRequirement()));\n    }\n\n    [Fact]\n    async Task AuthorizeOrThrowAsync_ThrowsNotFoundException_IfAuthorizationFails()\n    {\n        var authorizationService = Substitute.For<IAuthorizationService>();\n        var claimsPrincipal = new ClaimsPrincipal();\n        var requirement = new OperationAuthorizationRequirement();\n        var resource = new object();\n\n        authorizationService\n            .AuthorizeAsync(claimsPrincipal, resource, Arg.Is<IEnumerable<IAuthorizationRequirement>>(r =>\n                r.First() == requirement))\n            .Returns(AuthorizationResult.Failed());\n\n        await Assert.ThrowsAsync<NotFoundException>(() =>\n            AuthorizationServiceExtensions.AuthorizeOrThrowAsync(authorizationService, claimsPrincipal, resource, requirement));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Utilities/BulkAuthorizationHandlerTests.cs",
    "content": "﻿using System.Security.Claims;\nusing Bit.Core.Utilities;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Authorization.Infrastructure;\nusing Xunit;\n\nnamespace Bit.Core.Test.Utilities;\n\npublic class BulkAuthorizationHandlerTests\n{\n    [Fact]\n    public async Task HandleRequirementAsync_SingleResource_Success()\n    {\n        var handler = new TestBulkAuthorizationHandler();\n        var context = new AuthorizationHandlerContext(\n            new[] { new TestOperationRequirement() },\n            new ClaimsPrincipal(),\n            new TestResource());\n        await handler.HandleAsync(context);\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Fact]\n    public async Task HandleRequirementAsync_BulkResource_Success()\n    {\n        var handler = new TestBulkAuthorizationHandler();\n        var context = new AuthorizationHandlerContext(\n            new[] { new TestOperationRequirement() },\n            new ClaimsPrincipal(),\n            new[] { new TestResource(), new TestResource() });\n        await handler.HandleAsync(context);\n        Assert.True(context.HasSucceeded);\n    }\n\n    [Fact]\n    public async Task HandleRequirementAsync_NoResources_Failure()\n    {\n        var handler = new TestBulkAuthorizationHandler();\n        var context = new AuthorizationHandlerContext(\n            new[] { new TestOperationRequirement() },\n            new ClaimsPrincipal(),\n            null);\n        await handler.HandleAsync(context);\n        Assert.False(context.HasSucceeded);\n    }\n\n    [Fact]\n    public async Task HandleRequirementAsync_WrongResourceType_Failure()\n    {\n        var handler = new TestBulkAuthorizationHandler();\n        var context = new AuthorizationHandlerContext(\n            new[] { new TestOperationRequirement() },\n            new ClaimsPrincipal(),\n            new object());\n        await handler.HandleAsync(context);\n        Assert.False(context.HasSucceeded);\n    }\n\n    private class TestOperationRequirement : OperationAuthorizationRequirement { }\n\n    private class TestResource { }\n\n    private class TestBulkAuthorizationHandler : BulkAuthorizationHandler<TestOperationRequirement, TestResource>\n    {\n        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,\n            TestOperationRequirement requirement,\n            ICollection<TestResource> resources)\n        {\n            context.Succeed(requirement);\n            return Task.CompletedTask;\n        }\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Utilities/CoreHelpersTests.cs",
    "content": "﻿using System.Text;\nusing AutoFixture;\nusing Bit.Core.AdminConsole.Context;\nusing Bit.Core.AdminConsole.Enums.Provider;\nusing Bit.Core.Context;\nusing Bit.Core.Entities;\nusing Bit.Core.Enums;\nusing Bit.Core.Test.AutoFixture.UserFixtures;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.AutoFixture;\nusing Bit.Test.Common.AutoFixture.Attributes;\nusing Duende.IdentityModel;\nusing Microsoft.AspNetCore.DataProtection;\nusing Xunit;\n\nnamespace Bit.Core.Test.Utilities;\n\npublic class CoreHelpersTests\n{\n    public static IEnumerable<object[]> _epochTestCases = new[]\n    {\n        new object[] {new DateTime(2020, 12, 30, 11, 49, 12, DateTimeKind.Utc), 1609328952000L},\n    };\n\n    [Fact]\n    public void GenerateComb_Success()\n    {\n        // Arrange & Act\n        var comb = CoreHelpers.GenerateComb();\n\n        // Assert\n        Assert.NotEqual(Guid.Empty, comb);\n        // TODO: Add more asserts to make sure important aspects of\n        // the comb are working properly\n    }\n\n    public static IEnumerable<object[]> GuidSeedCases = [\n        [\n            Guid.Parse(\"a58db474-43d8-42f1-b4ee-0c17647cd0c0\"), // Input Guid\n            new DateTime(2022, 3, 12, 12, 12, 0, DateTimeKind.Utc), // Input Time\n        ],\n        [\n            Guid.Parse(\"f776e6ee-511f-4352-bb28-88513002bdeb\"),\n            new DateTime(2021, 5, 10, 10, 52, 0, DateTimeKind.Utc),\n        ],\n        [\n            Guid.Parse(\"51a25fc7-3cad-497d-8e2f-8d77011648a1\"),\n            new DateTime(1999, 2, 26, 16, 53, 13, DateTimeKind.Utc),\n        ],\n        [\n            Guid.Parse(\"bfb8f353-3b32-4a9e-bef6-24fe0b54bfb0\"),\n            new DateTime(2024, 10, 20, 1, 32, 16, DateTimeKind.Utc),\n        ]\n    ];\n    public static IEnumerable<object[]> GenerateCombCases = GuidSeedCases.Zip([\n            Guid.Parse(\"a58db474-43d8-42f1-b4ee-ae5600c90cc1\"), // Expected Comb for each Guid Seed case\n        Guid.Parse(\"f776e6ee-511f-4352-bb28-ad2400b313c1\"),\n        Guid.Parse(\"51a25fc7-3cad-497d-8e2f-8d77011649cd\"),\n        Guid.Parse(\"bfb8f353-3b32-4a9e-bef6-b20f00195780\"),\n    ]).Select((zip) => new object[] { zip.Item1[0], zip.Item1[1], zip.Item2 });\n\n    [Theory]\n    [MemberData(nameof(GenerateCombCases))]\n    public void GenerateComb_WithInputs_Success(Guid inputGuid, DateTime inputTime, Guid expectedComb)\n    {\n        var comb = CoreHelpers.GenerateComb(inputGuid, inputTime);\n\n        Assert.Equal(expectedComb, comb);\n    }\n\n    [Theory]\n    [MemberData(nameof(GuidSeedCases))]\n    public void DateFromComb_WithComb_Success(Guid inputGuid, DateTime inputTime)\n    {\n        var comb = CoreHelpers.GenerateComb(inputGuid, inputTime);\n        var inverseComb = CoreHelpers.DateFromComb(comb);\n\n        Assert.Equal(inputTime, inverseComb, TimeSpan.FromMilliseconds(4));\n    }\n\n    [Theory]\n    [InlineData(\"00000000-0000-0000-0000-000000000000\", 1, 0)]\n    [InlineData(\"00000000-0000-0000-0000-000000000001\", 1, 0)]\n    [InlineData(\"00000000-0000-0000-0000-000000000000\", 500, 430)]\n    [InlineData(\"00000000-0000-0000-0000-000000000001\", 500, 430)]\n    [InlineData(\"10000000-0000-0000-0000-000000000001\", 500, 454)]\n    [InlineData(\"00000000-0000-0100-0000-000000000001\", 500, 19)]\n    public void BinForComb_Success(string guidString, int nbins, int expectedBin)\n    {\n        var guid = Guid.Parse(guidString);\n        var bin = CoreHelpers.BinForComb(guid, nbins);\n\n        Assert.Equal(expectedBin, bin);\n    }\n\n    /*\n    [Fact]\n    public void ToGuidIdArrayTVP_Success()\n    {\n        // Arrange\n        var item0 = Guid.NewGuid();\n        var item1 = Guid.NewGuid();\n\n        var ids = new[] { item0, item1 };\n\n        // Act\n        var dt = ids.ToGuidIdArrayTVP();\n\n        // Assert\n        Assert.Single(dt.Columns);\n        Assert.Equal(\"GuidId\", dt.Columns[0].ColumnName);\n        Assert.Equal(2, dt.Rows.Count);\n        Assert.Equal(item0, dt.Rows[0][0]);\n        Assert.Equal(item1, dt.Rows[1][0]);\n    }\n    */\n\n    // TODO: Test the other ToArrayTVP Methods\n\n    [Theory]\n    [InlineData(\"12345&6789\", \"123456789\")]\n    [InlineData(\"abcdef\", \"ABCDEF\")]\n    [InlineData(\"1!@#$%&*()_+\", \"1\")]\n    [InlineData(\"\\u00C6123abc\\u00C7\", \"123ABC\")]\n    [InlineData(\"123\\u00C6ABC\", \"123ABC\")]\n    [InlineData(\"\\r\\nHello\", \"E\")]\n    [InlineData(\"\\tdef\", \"DEF\")]\n    [InlineData(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV1234567890\", \"ABCDEFABCDEF1234567890\")]\n    public void CleanCertificateThumbprint_Success(string input, string output)\n    {\n        // Arrange & Act\n        var sanitizedInput = CoreHelpers.CleanCertificateThumbprint(input);\n\n        // Assert\n        Assert.Equal(output, sanitizedInput);\n    }\n\n    // TODO: Add more tests\n    [Theory]\n    [MemberData(nameof(_epochTestCases))]\n    public void ToEpocMilliseconds_Success(DateTime date, long milliseconds)\n    {\n        // Act & Assert\n        Assert.Equal(milliseconds, CoreHelpers.ToEpocMilliseconds(date));\n    }\n\n    [Theory]\n    [MemberData(nameof(_epochTestCases))]\n    public void FromEpocMilliseconds(DateTime date, long milliseconds)\n    {\n        // Act & Assert\n        Assert.Equal(date, CoreHelpers.FromEpocMilliseconds(milliseconds));\n    }\n\n    [Fact]\n    public void SecureRandomString_Success()\n    {\n        // Arrange & Act\n        var @string = CoreHelpers.SecureRandomString(8);\n\n        // Assert\n        // TODO: Should probably add more Asserts down the line\n        Assert.Equal(8, @string.Length);\n    }\n\n    [Theory]\n    [InlineData(1, \"1 Bytes\")]\n    [InlineData(-5L, \"-5 Bytes\")]\n    [InlineData(1023L, \"1023 Bytes\")]\n    [InlineData(1024L, \"1 KB\")]\n    [InlineData(1025L, \"1 KB\")]\n    [InlineData(-1023L, \"-1023 Bytes\")]\n    [InlineData(-1024L, \"-1 KB\")]\n    [InlineData(-1025L, \"-1 KB\")]\n    [InlineData(1048575L, \"1024 KB\")]\n    [InlineData(1048576L, \"1 MB\")]\n    [InlineData(1048577L, \"1 MB\")]\n    [InlineData(-1048575L, \"-1024 KB\")]\n    [InlineData(-1048576L, \"-1 MB\")]\n    [InlineData(-1048577L, \"-1 MB\")]\n    [InlineData(1073741823L, \"1024 MB\")]\n    [InlineData(1073741824L, \"1 GB\")]\n    [InlineData(1073741825L, \"1 GB\")]\n    [InlineData(-1073741823L, \"-1024 MB\")]\n    [InlineData(-1073741824L, \"-1 GB\")]\n    [InlineData(-1073741825L, \"-1 GB\")]\n    [InlineData(long.MaxValue, \"8589934592 GB\")]\n    public void ReadableBytesSize_Success(long size, string readable)\n    {\n        // Act & Assert\n        Assert.Equal(readable, CoreHelpers.ReadableBytesSize(size));\n    }\n\n    [Fact]\n    public void CloneObject_Success()\n    {\n        var original = new { Message = \"Message\" };\n\n        var copy = CoreHelpers.CloneObject(original);\n\n        Assert.Equal(original.Message, copy.Message);\n    }\n\n    [Fact]\n    public void ExtendQuery_AddNewParameter_Success()\n    {\n        // Arrange\n        var uri = new Uri(\"https://bitwarden.com/?param1=value1\");\n\n        // Act\n        var newUri = CoreHelpers.ExtendQuery(uri,\n            new Dictionary<string, string> { { \"param2\", \"value2\" } });\n\n        // Assert\n        Assert.Equal(\"https://bitwarden.com/?param1=value1&param2=value2\", newUri.ToString());\n    }\n\n    [Fact]\n    public void ExtendQuery_AddTwoNewParameters_Success()\n    {\n        // Arrange\n        var uri = new Uri(\"https://bitwarden.com/?param1=value1\");\n\n        // Act\n        var newUri = CoreHelpers.ExtendQuery(uri,\n            new Dictionary<string, string>\n            {\n                { \"param2\", \"value2\" },\n                { \"param3\", \"value3\" }\n            });\n\n        // Assert\n        Assert.Equal(\"https://bitwarden.com/?param1=value1&param2=value2&param3=value3\", newUri.ToString());\n    }\n\n    [Fact]\n    public void ExtendQuery_AddExistingParameter_Success()\n    {\n        // Arrange\n        var uri = new Uri(\"https://bitwarden.com/?param1=value1&param2=value2\");\n\n        // Act\n        var newUri = CoreHelpers.ExtendQuery(uri,\n            new Dictionary<string, string> { { \"param1\", \"test_value\" } });\n\n        // Assert\n        Assert.Equal(\"https://bitwarden.com/?param1=test_value&param2=value2\", newUri.ToString());\n    }\n\n    [Fact]\n    public void ExtendQuery_AddNoParameters_Success()\n    {\n        // Arrange\n        const string startingUri = \"https://bitwarden.com/?param1=value1\";\n\n        var uri = new Uri(startingUri);\n\n        // Act\n        var newUri = CoreHelpers.ExtendQuery(uri, new Dictionary<string, string>());\n\n        // Assert\n        Assert.Equal(startingUri, newUri.ToString());\n    }\n\n    [Theory]\n    [InlineData(\"bücher.com\", \"xn--bcher-kva.com\")]\n    [InlineData(\"bücher.cömé\", \"xn--bcher-kva.xn--cm-cja4c\")]\n    [InlineData(\"hello@bücher.com\", \"hello@xn--bcher-kva.com\")]\n    [InlineData(\"hello@world.cömé\", \"hello@world.xn--cm-cja4c\")]\n    [InlineData(\"hello@bücher.cömé\", \"hello@xn--bcher-kva.xn--cm-cja4c\")]\n    [InlineData(\"ascii.com\", \"ascii.com\")]\n    [InlineData(\"\", \"\")]\n    [InlineData(null, null)]\n    public void PunyEncode_Success(string? text, string? expected)\n    {\n        var actual = CoreHelpers.PunyEncode(text);\n        Assert.Equal(expected, actual);\n    }\n\n    [Fact]\n    public void GetEmbeddedResourceContentsAsync_Success()\n    {\n        var fileContents = CoreHelpers.GetEmbeddedResourceContentsAsync(\"data.embeddedResource.txt\");\n        Assert.Equal(\"Contents of embeddedResource.txt\\n\", fileContents.Replace(\"\\r\\n\", \"\\n\"));\n    }\n\n    [Theory, BitAutoData, UserCustomize]\n    public void BuildIdentityClaims_BaseClaims_Success(User user, bool isPremium)\n    {\n        var expected = new Dictionary<string, string>\n        {\n            { \"premium\", isPremium ? \"true\" : \"false\" },\n            { JwtClaimTypes.Email, user.Email },\n            { JwtClaimTypes.EmailVerified, user.EmailVerified ? \"true\" : \"false\" },\n            { JwtClaimTypes.Name, user.Name },\n            { \"sstamp\", user.SecurityStamp },\n        }.ToList();\n\n        var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty<CurrentContextOrganization>(),\n            Array.Empty<CurrentContextProvider>(), isPremium);\n\n        foreach (var claim in expected)\n        {\n            Assert.Contains(claim, actual);\n        }\n        Assert.Equal(expected.Count, actual.Count);\n    }\n\n    [Theory, BitAutoData, UserCustomize]\n    public void BuildIdentityClaims_NonCustomOrganizationUserType_Success(User user)\n    {\n        var fixture = new Fixture().WithAutoNSubstitutions();\n        foreach (var organizationUserType in Enum.GetValues<OrganizationUserType>().Except(new[] { OrganizationUserType.Custom }))\n        {\n            var org = fixture.Create<CurrentContextOrganization>();\n            org.Type = organizationUserType;\n\n            var expected = new KeyValuePair<string, string>($\"org{organizationUserType.ToString().ToLower()}\", org.Id.ToString());\n            var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty<CurrentContextProvider>(), false);\n\n            Assert.Contains(expected, actual);\n        }\n    }\n\n    [Theory, BitAutoData, UserCustomize]\n    public void BuildIdentityClaims_CustomOrganizationUserClaims_Success(User user, CurrentContextOrganization org)\n    {\n        var fixture = new Fixture().WithAutoNSubstitutions();\n        org.Type = OrganizationUserType.Custom;\n\n        var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty<CurrentContextProvider>(), false);\n        foreach (var (permitted, claimName) in org.Permissions.ClaimsMap)\n        {\n            var claim = new KeyValuePair<string, string>(claimName, org.Id.ToString());\n            if (permitted)\n            {\n\n                Assert.Contains(claim, actual);\n            }\n            else\n            {\n                Assert.DoesNotContain(claim, actual);\n            }\n        }\n    }\n\n    [Theory, BitAutoData, UserCustomize]\n    public void BuildIdentityClaims_ProviderClaims_Success(User user)\n    {\n        var fixture = new Fixture().WithAutoNSubstitutions();\n        var providers = new List<CurrentContextProvider>();\n        foreach (var providerUserType in Enum.GetValues<ProviderUserType>())\n        {\n            var provider = fixture.Create<CurrentContextProvider>();\n            provider.Type = providerUserType;\n            providers.Add(provider);\n        }\n\n        var claims = new List<KeyValuePair<string, string>>();\n\n        if (providers.Any())\n        {\n            foreach (var group in providers.GroupBy(o => o.Type))\n            {\n                switch (group.Key)\n                {\n                    case ProviderUserType.ProviderAdmin:\n                        foreach (var provider in group)\n                        {\n                            claims.Add(new KeyValuePair<string, string>(\"providerprovideradmin\", provider.Id.ToString()));\n                        }\n                        break;\n                    case ProviderUserType.ServiceUser:\n                        foreach (var provider in group)\n                        {\n                            claims.Add(new KeyValuePair<string, string>(\"providerserviceuser\", provider.Id.ToString()));\n                        }\n                        break;\n                }\n            }\n        }\n\n        var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty<CurrentContextOrganization>(), providers, false);\n        foreach (var claim in claims)\n        {\n            Assert.Contains(claim, actual);\n        }\n    }\n\n    public static IEnumerable<object[]> TokenIsValidData()\n    {\n        return new[]\n        {\n            new object[]\n            {\n                \"first_part 476669d4-9642-4af8-9b29-9366efad4ed3 test@email.com {0}\", // unprotectedTokenTemplate\n                \"first_part\", // firstPart\n                \"test@email.com\", // email\n                Guid.Parse(\"476669d4-9642-4af8-9b29-9366efad4ed3\"), // id\n                DateTime.UtcNow.AddHours(-1), // creationTime\n                12, // expirationInHours\n                true, // isValid\n            }\n        };\n    }\n\n    [Theory]\n    [MemberData(nameof(TokenIsValidData))]\n    public void TokenIsValid_Success(string unprotectedTokenTemplate, string firstPart, string userEmail, Guid id, DateTime creationTime, double expirationInHours, bool isValid)\n    {\n        var protector = new TestDataProtector(string.Format(unprotectedTokenTemplate, CoreHelpers.ToEpocMilliseconds(creationTime)));\n\n        Assert.Equal(isValid, CoreHelpers.TokenIsValid(firstPart, protector, \"protected_token\", userEmail, id, expirationInHours));\n    }\n\n    private class TestDataProtector : IDataProtector\n    {\n        private readonly string _token;\n        public TestDataProtector(string token)\n        {\n            _token = token;\n        }\n        public IDataProtector CreateProtector(string purpose) => throw new NotImplementedException();\n        public byte[] Protect(byte[] plaintext) => throw new NotImplementedException();\n        public byte[] Unprotect(byte[] protectedData)\n        {\n            return Encoding.UTF8.GetBytes(_token);\n        }\n    }\n\n    [Theory]\n    [InlineData(\"hi@email.com\", \"hi@email.com\")] // Short email with no room to obfuscate\n    [InlineData(\"name@email.com\", \"na**@email.com\")] // Can obfuscate\n    [InlineData(\"reallylongnamethatnooneshouldhave@email\", \"re*******************************@email\")] // Really long email and no .com, .net, etc\n    [InlineData(\"name@\", \"name@\")] // @ symbol but no domain\n    [InlineData(\"\", \"\")] // Empty string\n    [InlineData(null, null)] // null\n    public void ObfuscateEmail_Success(string? input, string? expected)\n    {\n        Assert.Equal(expected, CoreHelpers.ObfuscateEmail(input));\n    }\n\n    [Theory]\n    [InlineData(\"user@example.com\")]\n    [InlineData(\"user@example.com \")]\n    [InlineData(\"user.name@example.com\")]\n    public void GetEmailDomain_Success(string email)\n    {\n        Assert.Equal(\"example.com\", CoreHelpers.GetEmailDomain(email));\n    }\n\n    [Theory]\n    [InlineData(\"\")]\n    [InlineData(null)]\n    [InlineData(\"userexample.com\")]\n    [InlineData(\"user@\")]\n    [InlineData(\"@example.com\")]\n    [InlineData(\"user@ex@ample.com\")]\n    public void GetEmailDomain_ReturnsNull(string? wrongEmail)\n    {\n        Assert.Null(CoreHelpers.GetEmailDomain(wrongEmail));\n    }\n\n    [Theory]\n    [InlineData(\"hello world\")]\n    [InlineData(\" hello world \")]\n    [InlineData(\"hello\\tworld\")]\n    [InlineData(\"hello\\r\\nworld\")]\n    [InlineData(\"hello\\nworld\")]\n    public void ReplaceWhiteSpace_Success(string email)\n    {\n        Assert.Equal(\"helloworld\", CoreHelpers.ReplaceWhiteSpace(email, string.Empty));\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Utilities/CustomRedisProcessingStrategyTests.cs",
    "content": "﻿using AspNetCoreRateLimit;\nusing Bit.Core.Settings;\nusing Bit.Core.Utilities;\nusing Microsoft.Extensions.Caching.Memory;\nusing Microsoft.Extensions.Logging;\nusing NSubstitute;\nusing StackExchange.Redis;\nusing Xunit;\n\nnamespace Bit.Core.Test.Utilities;\n\npublic class CustomRedisProcessingStrategyTests\n{\n    #region Sample RateLimit Options for Testing\n\n    private readonly GlobalSettings _sampleSettings = new()\n    {\n        DistributedIpRateLimiting = new GlobalSettings.DistributedIpRateLimitingSettings\n        {\n            Enabled = true,\n            MaxRedisTimeoutsThreshold = 2,\n            SlidingWindowSeconds = 5\n        }\n    };\n\n    private readonly ClientRequestIdentity _sampleClientId = new()\n    {\n        ClientId = \"test\",\n        ClientIp = \"127.0.0.1\",\n        HttpVerb = \"GET\",\n        Path = \"/\"\n    };\n\n    private readonly RateLimitRule _sampleRule = new() { Endpoint = \"/\", Limit = 5, Period = \"1m\", PeriodTimespan = TimeSpan.FromMinutes(1) };\n\n    private readonly RateLimitOptions _sampleOptions = new() { };\n\n    #endregion\n\n    private readonly ICounterKeyBuilder _mockCounterKeyBuilder = Substitute.For<ICounterKeyBuilder>();\n    private IDatabase _mockDb;\n\n    public CustomRedisProcessingStrategyTests()\n    {\n        _mockCounterKeyBuilder.Build(Arg.Any<ClientRequestIdentity>(), Arg.Any<RateLimitRule>())\n            .Returns(_sampleClientId.ClientId);\n    }\n\n    [Fact]\n    public async Task IncrementRateLimitCount_When_RedisIsHealthy()\n    {\n        // Arrange\n        var strategy = BuildProcessingStrategy();\n\n        // Act\n        var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,\n            CancellationToken.None);\n\n        // Assert\n        Assert.Equal(1, result.Count);\n        VerifyRedisCalls(1);\n    }\n\n    [Fact]\n    public async Task SkipRateLimit_When_RedisIsDown()\n    {\n        // Arrange\n        var strategy = BuildProcessingStrategy(false);\n\n        // Act\n        var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,\n            CancellationToken.None);\n\n        // Assert\n        Assert.Equal(0, result.Count);\n        VerifyRedisNotCalled();\n    }\n\n    [Fact]\n    public async Task SkipRateLimit_When_TimeoutThresholdExceeded()\n    {\n        // Arrange\n        var mockCache = Substitute.For<IMemoryCache>();\n        object existingCount = new CustomRedisProcessingStrategy.TimeoutCounter\n        {\n            Count = _sampleSettings.DistributedIpRateLimiting.MaxRedisTimeoutsThreshold + 1\n        };\n        mockCache.TryGetValue(Arg.Any<object>(), out existingCount).ReturnsForAnyArgs(x =>\n        {\n            x[1] = existingCount;\n            return true;\n        });\n\n        var strategy = BuildProcessingStrategy(mockCache: mockCache);\n\n        // Act\n        var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,\n            CancellationToken.None);\n\n        // Assert\n        Assert.Equal(0, result.Count);\n        VerifyRedisNotCalled();\n    }\n\n    [Fact]\n    public async Task SkipRateLimit_When_RedisTimeoutException()\n    {\n        // Arrange\n        var mockCache = Substitute.For<IMemoryCache>();\n        var mockCacheEntry = Substitute.For<ICacheEntry>();\n        mockCache.CreateEntry(Arg.Any<object>()).Returns(mockCacheEntry);\n\n        var strategy = BuildProcessingStrategy(mockCache: mockCache, throwRedisTimeout: true);\n\n        // Act\n        var result = await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,\n            CancellationToken.None);\n\n        var timeoutCounter = ((CustomRedisProcessingStrategy.TimeoutCounter)mockCacheEntry.Value);\n\n        // Assert\n        Assert.Equal(0, result.Count); // Skip rate limiting\n        VerifyRedisCalls(1);\n\n        Assert.Equal(1, timeoutCounter.Count); // Timeout count increased/cached\n        Assert.NotNull(mockCacheEntry.AbsoluteExpiration);\n        mockCache.Received().CreateEntry(Arg.Any<object>());\n    }\n\n    [Fact]\n    public async Task BackoffRedis_After_ThresholdExceeded()\n    {\n        // Arrange\n        var memoryCache = new MemoryCache(new MemoryCacheOptions());\n        var strategy = BuildProcessingStrategy(mockCache: memoryCache, throwRedisTimeout: true);\n\n        // Act\n\n        // Redis Timeout 1\n        await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,\n            CancellationToken.None);\n\n        // Redis Timeout 2\n        await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,\n            CancellationToken.None);\n\n        // Skip Redis\n        await strategy.ProcessRequestAsync(_sampleClientId, _sampleRule, _mockCounterKeyBuilder, _sampleOptions,\n            CancellationToken.None);\n\n        // Assert\n        VerifyRedisCalls(_sampleSettings.DistributedIpRateLimiting.MaxRedisTimeoutsThreshold);\n    }\n\n    private void VerifyRedisCalls(int times)\n    {\n        _mockDb\n            .Received(times)\n            .ScriptEvaluateAsync(Arg.Any<LuaScript>(), Arg.Any<object>(), Arg.Any<CommandFlags>());\n    }\n\n    private void VerifyRedisNotCalled()\n    {\n        _mockDb\n            .DidNotReceive()\n            .ScriptEvaluateAsync(Arg.Any<LuaScript>(), Arg.Any<object>(), Arg.Any<CommandFlags>());\n    }\n\n    private CustomRedisProcessingStrategy BuildProcessingStrategy(\n        bool isRedisConnected = true,\n        bool throwRedisTimeout = false,\n        IMemoryCache mockCache = null)\n    {\n        var mockRedisConnection = Substitute.For<IConnectionMultiplexer>();\n\n        mockRedisConnection.IsConnected.Returns(isRedisConnected);\n\n        _mockDb = Substitute.For<IDatabase>();\n\n        var mockScriptEvaluate = _mockDb\n            .ScriptEvaluateAsync(Arg.Any<LuaScript>(), Arg.Any<object>(), Arg.Any<CommandFlags>());\n\n        if (throwRedisTimeout)\n        {\n            mockScriptEvaluate.Returns<RedisResult>(x => throw new RedisTimeoutException(\"Timeout\", CommandStatus.WaitingToBeSent));\n        }\n        else\n        {\n            mockScriptEvaluate.Returns(RedisResult.Create(1));\n        }\n\n        mockRedisConnection.GetDatabase(Arg.Any<int>(), Arg.Any<object>())\n            .Returns(_mockDb);\n\n        var mockLogger = Substitute.For<ILogger<CustomRedisProcessingStrategy>>();\n        var mockConfig = Substitute.For<IRateLimitConfiguration>();\n\n        mockCache ??= Substitute.For<IMemoryCache>();\n\n        return new CustomRedisProcessingStrategy(mockRedisConnection, mockConfig,\n            mockLogger, mockCache, _sampleSettings);\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Utilities/EncryptedStringAttributeTests.cs",
    "content": "﻿using Bit.Core.Enums;\nusing Bit.Core.Utilities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Utilities;\n\npublic class EncryptedStringAttributeTests\n{\n    [Theory]\n    [InlineData(null)]\n    [InlineData(\"aXY=|Y3Q=\")] // Valid AesCbc256_B64\n    [InlineData(\"aXY=|Y3Q=|cnNhQ3Q=\")] // Valid AesCbc128_HmacSha256_B64\n    [InlineData(\"Rsa2048_OaepSha256_B64.cnNhQ3Q=\")]\n    [InlineData(\"0.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Valid AesCbc256_B64 as a number\n    [InlineData(\"AesCbc256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Valid AesCbc256_B64 as a number\n    [InlineData(\"1.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Valid AesCbc128_HmacSha256_B64 as a number\n    [InlineData(\"AesCbc128_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Valid AesCbc128_HmacSha256_B64 as a string\n    [InlineData(\"2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Valid AesCbc256_HmacSha256_B64 as a number\n    [InlineData(\"AesCbc256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Valid AesCbc256_HmacSha256_B64 as a string\n    [InlineData(\"3.QmFzZTY0UGFydA==\")] // Valid Rsa2048_OaepSha256_B64 as a number\n    [InlineData(\"Rsa2048_OaepSha256_B64.QmFzZTY0UGFydA==\")] // Valid Rsa2048_OaepSha256_B64 as a string\n    [InlineData(\"4.QmFzZTY0UGFydA==\")] // Valid Rsa2048_OaepSha1_B64 as a number\n    [InlineData(\"Rsa2048_OaepSha1_B64.QmFzZTY0UGFydA==\")] // Valid Rsa2048_OaepSha1_B64 as a string\n    [InlineData(\"5.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Valid Rsa2048_OaepSha256_HmacSha256_B64 as a number\n    [InlineData(\"Rsa2048_OaepSha256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Valid Rsa2048_OaepSha256_HmacSha256_B64 as a string\n    [InlineData(\"6.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Valid Rsa2048_OaepSha1_HmacSha256_B64 as a number\n    [InlineData(\"Rsa2048_OaepSha1_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")]\n    public void IsValid_ReturnsTrue_WhenValid(string? input)\n    {\n        var sut = new EncryptedStringAttribute();\n\n        var actual = sut.IsValid(input);\n\n        Assert.True(actual);\n    }\n\n    [Theory]\n    [InlineData(\"Test\")] // Plain text injection attack - DoS vulnerability regression test\n    [InlineData(\"Hello World\")] // Plain text injection attack\n    [InlineData(\"SecretPassword123\")] // Plain text injection attack\n    [InlineData(\"\")] // Empty string\n    [InlineData(\".\")] // Split Character but two empty parts\n    [InlineData(\"|\")] // One encrypted part split character but empty parts\n    [InlineData(\"||\")] // Two encrypted part split character but empty parts\n    [InlineData(\"!|!\")] // Invalid base 64\n    [InlineData(\"Rsa2048_OaepSha1_HmacSha256_B64.1\")] // Invalid length\n    [InlineData(\"Rsa2048_OaepSha1_HmacSha256_B64.|\")] // Empty iv & ct\n    [InlineData(\"AesCbc128_HmacSha256_B64.1\")] // Invalid length\n    [InlineData(\"AesCbc128_HmacSha256_B64.aXY=|Y3Q=|\")] // Empty mac\n    [InlineData(\"Rsa2048_OaepSha1_HmacSha256_B64.aXY=|Y3Q=|\")] // Empty mac\n    [InlineData(\"Rsa2048_OaepSha256_B64.1|2\")] // Invalid length\n    [InlineData(\"Rsa2048_OaepSha1_HmacSha256_B64.aXY=|\")] // Empty mac\n    [InlineData(\"254.QmFzZTY0UGFydA==\")] // Bad Encryption type number\n    [InlineData(\"0.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid AesCbc256_B64 as a number\n    [InlineData(\"AesCbc256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid AesCbc256_B64 as a number\n    [InlineData(\"1.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid AesCbc128_HmacSha256_B64 as a number\n    [InlineData(\"AesCbc128_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid AesCbc128_HmacSha256_B64 as a string\n    [InlineData(\"2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid AesCbc256_HmacSha256_B64 as a number\n    [InlineData(\"AesCbc256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid AesCbc256_HmacSha256_B64 as a string\n    [InlineData(\"3.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid Rsa2048_OaepSha256_B64 as a number\n    [InlineData(\"Rsa2048_OaepSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid Rsa2048_OaepSha256_B64 as a string\n    [InlineData(\"4.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid Rsa2048_OaepSha1_B64 as a number\n    [InlineData(\"Rsa2048_OaepSha1_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid Rsa2048_OaepSha1_B64 as a string\n    [InlineData(\"5.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid Rsa2048_OaepSha256_HmacSha256_B64 as a number\n    [InlineData(\"Rsa2048_OaepSha256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid Rsa2048_OaepSha256_HmacSha256_B64 as a string\n    [InlineData(\"6.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==\")] // Invalid Rsa2048_OaepSha1_HmacSha256_B64 as a number\n    [InlineData(\"Rsa2048_OaepSha1_HmacSha256_B64.QmFzZTY0UGFydA==\")] // Invalid Rsa2048_OaepSha1_HmacSha256_B64 as a string\n    public void IsValid_ReturnsFalse_WhenInvalid(string input)\n    {\n        var sut = new EncryptedStringAttribute();\n\n        var actual = sut.IsValid(input);\n\n        Assert.False(actual);\n    }\n\n    [Fact]\n    public void EncryptionTypeMap_HasEntry_ForEachEnumValue()\n    {\n        var enumValues = Enum.GetValues<EncryptionType>();\n        Assert.Equal(enumValues.Length, EncryptedStringAttribute._encryptionTypeToRequiredPiecesMap.Count);\n\n        foreach (var enumValue in enumValues)\n        {\n            // Go a step further and ensure that the map contains a value for each value instead of just casting\n            // a random number for one of the keys.\n            Assert.True(EncryptedStringAttribute._encryptionTypeToRequiredPiecesMap.ContainsKey(enumValue));\n        }\n    }\n\n    [Fact]\n    public void CheckForUnderlyingTypeChange()\n    {\n        var underlyingType = typeof(EncryptionType).GetEnumUnderlyingType();\n        var expectedType = typeof(byte);\n\n        Assert.True(underlyingType == expectedType,\n            $\"Hello future person, it seems you have changed the underlying type for {nameof(EncryptionType)}, \" +\n            $\"that is totally fine you just also need to change the line for {expectedType.Name}.TryParse in \" +\n            $\"{nameof(EncryptedStringAttribute)} to {underlyingType.Name}.TryParse (but you can probably use the alias)\" +\n            \"and then update this test!\");\n    }\n}\n"
  },
  {
    "path": "test/Core.Test/Utilities/EnumMemberJsonConverterTests.cs",
    "content": "﻿using System.Runtime.Serialization;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Utilities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Utilities;\n\npublic class EnumMemberJsonConverterTests\n{\n    [Fact]\n    public void Serialize_WithEnumMemberAttribute_UsesAttributeValue()\n    {\n        // Arrange\n        var obj = new EnumConverterTestObject\n        {\n            Status = EnumConverterTestStatus.InProgress\n        };\n        const string expectedJsonString = \"{\\\"Status\\\":\\\"in_progress\\\"}\";\n\n        // Act\n        var jsonString = JsonSerializer.Serialize(obj);\n\n        // Assert\n        Assert.Equal(expectedJsonString, jsonString);\n    }\n\n    [Fact]\n    public void Serialize_WithoutEnumMemberAttribute_UsesEnumName()\n    {\n        // Arrange\n        var obj = new EnumConverterTestObject\n        {\n            Status = EnumConverterTestStatus.Pending\n        };\n        const string expectedJsonString = \"{\\\"Status\\\":\\\"Pending\\\"}\";\n\n        // Act\n        var jsonString = JsonSerializer.Serialize(obj);\n\n        // Assert\n        Assert.Equal(expectedJsonString, jsonString);\n    }\n\n    [Fact]\n    public void Serialize_MultipleValues_SerializesCorrectly()\n    {\n        // Arrange\n        var obj = new EnumConverterTestObjectWithMultiple\n        {\n            Status1 = EnumConverterTestStatus.Active,\n            Status2 = EnumConverterTestStatus.InProgress,\n            Status3 = EnumConverterTestStatus.Pending\n        };\n        const string expectedJsonString = \"{\\\"Status1\\\":\\\"active\\\",\\\"Status2\\\":\\\"in_progress\\\",\\\"Status3\\\":\\\"Pending\\\"}\";\n\n        // Act\n        var jsonString = JsonSerializer.Serialize(obj);\n\n        // Assert\n        Assert.Equal(expectedJsonString, jsonString);\n    }\n\n    [Fact]\n    public void Deserialize_WithEnumMemberAttribute_ReturnsCorrectEnumValue()\n    {\n        // Arrange\n        const string json = \"{\\\"Status\\\":\\\"in_progress\\\"}\";\n\n        // Act\n        var obj = JsonSerializer.Deserialize<EnumConverterTestObject>(json);\n\n        // Assert\n        Assert.Equal(EnumConverterTestStatus.InProgress, obj.Status);\n    }\n\n    [Fact]\n    public void Deserialize_WithoutEnumMemberAttribute_ReturnsCorrectEnumValue()\n    {\n        // Arrange\n        const string json = \"{\\\"Status\\\":\\\"Pending\\\"}\";\n\n        // Act\n        var obj = JsonSerializer.Deserialize<EnumConverterTestObject>(json);\n\n        // Assert\n        Assert.Equal(EnumConverterTestStatus.Pending, obj.Status);\n    }\n\n    [Fact]\n    public void Deserialize_MultipleValues_DeserializesCorrectly()\n    {\n        // Arrange\n        const string json = \"{\\\"Status1\\\":\\\"active\\\",\\\"Status2\\\":\\\"in_progress\\\",\\\"Status3\\\":\\\"Pending\\\"}\";\n\n        // Act\n        var obj = JsonSerializer.Deserialize<EnumConverterTestObjectWithMultiple>(json);\n\n        // Assert\n        Assert.Equal(EnumConverterTestStatus.Active, obj.Status1);\n        Assert.Equal(EnumConverterTestStatus.InProgress, obj.Status2);\n        Assert.Equal(EnumConverterTestStatus.Pending, obj.Status3);\n    }\n\n    [Fact]\n    public void Deserialize_InvalidEnumString_ThrowsJsonException()\n    {\n        // Arrange\n        const string json = \"{\\\"Status\\\":\\\"invalid_value\\\"}\";\n\n        // Act & Assert\n        var exception = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<EnumConverterTestObject>(json));\n        Assert.Contains(\"Unable to convert 'invalid_value' to EnumConverterTestStatus\", exception.Message);\n    }\n\n    [Fact]\n    public void Deserialize_EmptyString_ThrowsJsonException()\n    {\n        // Arrange\n        const string json = \"{\\\"Status\\\":\\\"\\\"}\";\n\n        // Act & Assert\n        var exception = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<EnumConverterTestObject>(json));\n        Assert.Contains(\"Unable to convert '' to EnumConverterTestStatus\", exception.Message);\n    }\n\n    [Fact]\n    public void RoundTrip_WithEnumMemberAttribute_PreservesValue()\n    {\n        // Arrange\n        var originalObj = new EnumConverterTestObject\n        {\n            Status = EnumConverterTestStatus.Completed\n        };\n\n        // Act\n        var json = JsonSerializer.Serialize(originalObj);\n        var deserializedObj = JsonSerializer.Deserialize<EnumConverterTestObject>(json);\n\n        // Assert\n        Assert.Equal(originalObj.Status, deserializedObj.Status);\n    }\n\n    [Fact]\n    public void RoundTrip_WithoutEnumMemberAttribute_PreservesValue()\n    {\n        // Arrange\n        var originalObj = new EnumConverterTestObject\n        {\n            Status = EnumConverterTestStatus.Pending\n        };\n\n        // Act\n        var json = JsonSerializer.Serialize(originalObj);\n        var deserializedObj = JsonSerializer.Deserialize<EnumConverterTestObject>(json);\n\n        // Assert\n        Assert.Equal(originalObj.Status, deserializedObj.Status);\n    }\n\n    [Fact]\n    public void Serialize_AllEnumValues_ProducesExpectedStrings()\n    {\n        // Arrange & Act & Assert\n        Assert.Equal(\"\\\"Pending\\\"\", JsonSerializer.Serialize(EnumConverterTestStatus.Pending, CreateOptions()));\n        Assert.Equal(\"\\\"active\\\"\", JsonSerializer.Serialize(EnumConverterTestStatus.Active, CreateOptions()));\n        Assert.Equal(\"\\\"in_progress\\\"\", JsonSerializer.Serialize(EnumConverterTestStatus.InProgress, CreateOptions()));\n        Assert.Equal(\"\\\"completed\\\"\", JsonSerializer.Serialize(EnumConverterTestStatus.Completed, CreateOptions()));\n    }\n\n    [Fact]\n    public void Deserialize_AllEnumValues_ReturnsCorrectEnums()\n    {\n        // Arrange & Act & Assert\n        Assert.Equal(EnumConverterTestStatus.Pending, JsonSerializer.Deserialize<EnumConverterTestStatus>(\"\\\"Pending\\\"\", CreateOptions()));\n        Assert.Equal(EnumConverterTestStatus.Active, JsonSerializer.Deserialize<EnumConverterTestStatus>(\"\\\"active\\\"\", CreateOptions()));\n        Assert.Equal(EnumConverterTestStatus.InProgress, JsonSerializer.Deserialize<EnumConverterTestStatus>(\"\\\"in_progress\\\"\", CreateOptions()));\n        Assert.Equal(EnumConverterTestStatus.Completed, JsonSerializer.Deserialize<EnumConverterTestStatus>(\"\\\"completed\\\"\", CreateOptions()));\n    }\n\n    private static JsonSerializerOptions CreateOptions()\n    {\n        var options = new JsonSerializerOptions();\n        options.Converters.Add(new EnumMemberJsonConverter<EnumConverterTestStatus>());\n        return options;\n    }\n}\n\npublic class EnumConverterTestObject\n{\n    [JsonConverter(typeof(EnumMemberJsonConverter<EnumConverterTestStatus>))]\n    public EnumConverterTestStatus Status { get; set; }\n}\n\npublic class EnumConverterTestObjectWithMultiple\n{\n    [JsonConverter(typeof(EnumMemberJsonConverter<EnumConverterTestStatus>))]\n    public EnumConverterTestStatus Status1 { get; set; }\n\n    [JsonConverter(typeof(EnumMemberJsonConverter<EnumConverterTestStatus>))]\n    public EnumConverterTestStatus Status2 { get; set; }\n\n    [JsonConverter(typeof(EnumMemberJsonConverter<EnumConverterTestStatus>))]\n    public EnumConverterTestStatus Status3 { get; set; }\n}\n\npublic enum EnumConverterTestStatus\n{\n    Pending, // No EnumMemberAttribute\n\n    [EnumMember(Value = \"active\")]\n    Active,\n\n    [EnumMember(Value = \"in_progress\")]\n    InProgress,\n\n    [EnumMember(Value = \"completed\")]\n    Completed\n}\n"
  },
  {
    "path": "test/Core.Test/Utilities/JsonHelpersTests.cs",
    "content": "﻿using System.Text.Json;\nusing Bit.Core.Utilities;\nusing Xunit;\n\nnamespace Bit.Core.Test.Helpers;\n\npublic class JsonHelpersTests\n{\n    private static void CompareJson<T>(T value, JsonSerializerOptions options, Newtonsoft.Json.JsonSerializerSettings settings)\n    {\n        var stgJson = JsonSerializer.Serialize(value, options);\n        var nsJson = Newtonsoft.Json.JsonConvert.SerializeObject(value, settings);\n\n        Assert.Equal(stgJson, nsJson);\n    }\n\n\n    [Fact]\n    public void DefaultJsonOptions()\n    {\n        var testObject = new SimpleTestObject\n        {\n            Id = 0,\n            Name = \"Test\",\n        };\n\n        CompareJson(testObject, JsonHelpers.Default, new Newtonsoft.Json.JsonSerializerSettings());\n    }\n\n    [Fact]\n    public void IndentedJsonOptions()\n    {\n        var testObject = new SimpleTestObject\n        {\n            Id = 10,\n            Name = \"Test Name\"\n        };\n\n        CompareJson(testObject, JsonHelpers.Indented, new Newtonsoft.Json.JsonSerializerSettings\n        {\n            Formatting = Newtonsoft.Json.Formatting.Indented,\n        });\n    }\n\n    [Fact]\n    public void NullValueHandlingJsonOptions()\n    {\n        var testObject = new SimpleTestObject\n        {\n            Id = 14,\n            Name = null,\n        };\n\n        CompareJson(testObject, JsonHelpers.IgnoreWritingNull, new Newtonsoft.Json.JsonSerializerSettings\n        {\n            NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,\n        });\n    }\n}\n\npublic class SimpleTestObject\n{\n    public int Id { get; set; }\n    public string Name { get; set; }\n}\n"
  },
  {
    "path": "test/Core.Test/Utilities/PermissiveStringConverterTests.cs",
    "content": "﻿using System.Globalization;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Bit.Core.Utilities;\nusing Bit.Test.Common.Helpers;\nusing Xunit;\n\nnamespace Bit.Core.Test.Utilities;\n\npublic class PermissiveStringConverterTests\n{\n    private const string numberJson = \"{ \\\"StringProp\\\": 1, \\\"EnumerableStringProp\\\": [ 2, 3 ]}\";\n    private const string stringJson = \"{ \\\"StringProp\\\": \\\"1\\\", \\\"EnumerableStringProp\\\": [ \\\"2\\\", \\\"3\\\" ]}\";\n    private const string nullAndEmptyJson = \"{ \\\"StringProp\\\": null, \\\"EnumerableStringProp\\\": [] }\";\n    private const string singleValueJson = \"{ \\\"StringProp\\\": 1, \\\"EnumerableStringProp\\\": \\\"Hello!\\\" }\";\n    private const string nullJson = \"{ \\\"StringProp\\\": null, \\\"EnumerableStringProp\\\": null }\";\n    private const string boolJson = \"{ \\\"StringProp\\\": true, \\\"EnumerableStringProp\\\": [ false, 1.2]}\";\n    private const string objectJsonOne = \"{ \\\"StringProp\\\": { \\\"Message\\\": \\\"Hi\\\"}, \\\"EnumerableStringProp\\\": []}\";\n    private const string objectJsonTwo = \"{ \\\"StringProp\\\": \\\"Hi\\\", \\\"EnumerableStringProp\\\": {}}\";\n    private readonly string bigNumbersJson =\n    \"{ \\\"StringProp\\\":\" + decimal.MinValue + \", \\\"EnumerableStringProp\\\": [\" + ulong.MaxValue + \", \" + long.MinValue + \"]}\";\n\n    [Theory]\n    [InlineData(numberJson)]\n    [InlineData(stringJson)]\n    public void Read_Success(string json)\n    {\n        var obj = JsonSerializer.Deserialize<TestObject>(json);\n        Assert.Equal(\"1\", obj.StringProp);\n        Assert.Equal(2, obj.EnumerableStringProp.Count());\n        Assert.Equal(\"2\", obj.EnumerableStringProp.ElementAt(0));\n        Assert.Equal(\"3\", obj.EnumerableStringProp.ElementAt(1));\n    }\n\n    [Fact]\n    public void Read_Boolean_Success()\n    {\n        var obj = JsonSerializer.Deserialize<TestObject>(boolJson);\n        Assert.Equal(\"True\", obj.StringProp);\n        Assert.Equal(2, obj.EnumerableStringProp.Count());\n        Assert.Equal(\"False\", obj.EnumerableStringProp.ElementAt(0));\n        Assert.Equal(\"1.2\", obj.EnumerableStringProp.ElementAt(1));\n    }\n\n    [Fact]\n    public void Read_Float_Success_Culture()\n    {\n        var ci = new CultureInfo(\"sv-SE\");\n        Thread.CurrentThread.CurrentCulture = ci;\n        Thread.CurrentThread.CurrentUICulture = ci;\n\n        var obj = JsonSerializer.Deserialize<TestObject>(boolJson);\n        Assert.Equal(\"1.2\", obj.EnumerableStringProp.ElementAt(1));\n    }\n\n    [Fact]\n    public void Read_BigNumbers_Success()\n    {\n        var obj = JsonSerializer.Deserialize<TestObject>(bigNumbersJson);\n        Assert.Equal(decimal.MinValue.ToString(), obj.StringProp);\n        Assert.Equal(2, obj.EnumerableStringProp.Count());\n        Assert.Equal(ulong.MaxValue.ToString(), obj.EnumerableStringProp.ElementAt(0));\n        Assert.Equal(long.MinValue.ToString(), obj.EnumerableStringProp.ElementAt(1));\n    }\n\n    [Fact]\n    public void Read_SingleValue_Success()\n    {\n        var obj = JsonSerializer.Deserialize<TestObject>(singleValueJson);\n        Assert.Equal(\"1\", obj.StringProp);\n        Assert.Single(obj.EnumerableStringProp);\n        Assert.Equal(\"Hello!\", obj.EnumerableStringProp.ElementAt(0));\n    }\n\n    [Fact]\n    public void Read_NullAndEmptyJson_Success()\n    {\n        var obj = JsonSerializer.Deserialize<TestObject>(nullAndEmptyJson);\n        Assert.Null(obj.StringProp);\n        Assert.Empty(obj.EnumerableStringProp);\n    }\n\n    [Fact]\n    public void Read_Null_Success()\n    {\n        var obj = JsonSerializer.Deserialize<TestObject>(nullJson);\n        Assert.Null(obj.StringProp);\n        Assert.Null(obj.EnumerableStringProp);\n    }\n\n    [Theory]\n    [InlineData(objectJsonOne)]\n    [InlineData(objectJsonTwo)]\n    public void Read_Object_Throws(string json)\n    {\n        var exception = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<TestObject>(json));\n    }\n\n    [Fact]\n    public void Write_Success()\n    {\n        var json = JsonSerializer.Serialize(new TestObject\n        {\n            StringProp = \"1\",\n            EnumerableStringProp = new List<string>\n            {\n                \"2\",\n                \"3\",\n            },\n        });\n\n        var jsonElement = JsonDocument.Parse(json).RootElement;\n\n        var stringProp = AssertHelper.AssertJsonProperty(jsonElement, \"StringProp\", JsonValueKind.String);\n        Assert.Equal(\"1\", stringProp.GetString());\n        var list = AssertHelper.AssertJsonProperty(jsonElement, \"EnumerableStringProp\", JsonValueKind.Array);\n        Assert.Equal(2, list.GetArrayLength());\n        var firstElement = list[0];\n        Assert.Equal(JsonValueKind.String, firstElement.ValueKind);\n        Assert.Equal(\"2\", firstElement.GetString());\n        var secondElement = list[1];\n        Assert.Equal(JsonValueKind.String, secondElement.ValueKind);\n        Assert.Equal(\"3\", secondElement.GetString());\n    }\n\n    [Fact]\n    public void Write_Null()\n    {\n        // When the values are null the converters aren't actually ran and it automatically serializes null\n        var json = JsonSerializer.Serialize(new TestObject\n        {\n            StringProp = null,\n            EnumerableStringProp = null,\n        });\n\n        var jsonElement = JsonDocument.Parse(json).RootElement;\n\n        AssertHelper.AssertJsonProperty(jsonElement, \"StringProp\", JsonValueKind.Null);\n        AssertHelper.AssertJsonProperty(jsonElement, \"EnumerableStringProp\", JsonValueKind.Null);\n    }\n\n    [Fact]\n    public void Write_Empty()\n    {\n        // When the values are null the converters aren't actually ran and it automatically serializes null\n        var json = JsonSerializer.Serialize(new TestObject\n        {\n            StringProp = \"\",\n            EnumerableStringProp = Enumerable.Empty<string>(),\n        });\n\n        var jsonElement = JsonDocument.Parse(json).RootElement;\n\n        var stringVal = AssertHelper.AssertJsonProperty(jsonElement, \"StringProp\", JsonValueKind.String).GetString();\n        Assert.Equal(\"\", stringVal);\n        var array = AssertHelper.AssertJsonProperty(jsonElement, \"EnumerableStringProp\", JsonValueKind.Array);\n        Assert.Equal(0, array.GetArrayLength());\n    }\n}\n\npublic class TestObject\n{\n    [JsonConverter(typeof(PermissiveStringConverter))]\n    public string StringProp { get; set; }\n\n    [JsonConverter(typeof(PermissiveStringEnumerableConverter))]\n    public IEnumerable<string> EnumerableStringProp { get; set; }\n}\n"
  },
  {
    "path": "util/Migrator/DbScripts_finalization/.gitkeep",
    "content": ""
  },
  {
    "path": "util/Migrator/DbScripts_transition/.gitkeep",
    "content": ""
  }
]